Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .cursor/rules/dkg-annotate.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -121,13 +121,13 @@ The authoritative reference is [`packages/cli/skills/dkg-node/SKILL.md`](mdc:pac

**Read**: `dkg_memory_search` (was `dkg_search`), `dkg_query` (was `dkg_sparql`), `dkg_get_entity`, `dkg_list_context_graphs` (was `dkg_list_projects`), `dkg_sub_graph_list` (was `dkg_list_subgraphs`), `dkg_list_activity`, `dkg_get_agent`, `dkg_knowledge_asset_query`, `dkg_knowledge_asset_history`, `dkg_status`.

**Write (auto-shared via `autoShare: true`)**: `dkg_knowledge_asset_create` + `dkg_knowledge_asset_write` + `dkg_knowledge_asset_share`, `dkg_knowledge_asset_discard`, `dkg_share` (direct SWM), `dkg_sub_graph_create`.
**Write (auto-shared via `autoShare: true`)**: `dkg_knowledge_asset_create` (pass `quads`+`alsoShareSwm:true` for a create→write→seal→share one-shot) + `dkg_knowledge_asset_write` + `dkg_knowledge_asset_share`, `dkg_knowledge_asset_discard`, `dkg_sub_graph_create`. (There is no loose-SWM `dkg_share` tool — author a named knowledge asset.)

**Inter-agent**: `dkg_send_message`, `dkg_check_inbox` / `dkg_read_messages`, `dkg_subscribe`.

**HUMAN-GATED**: `dkg_shared_memory_publish` (SWM → VM, on-chain, costs TRAC). Don't call without explicit operator instruction.
**HUMAN-GATED**: `dkg_knowledge_asset_publish` (the sealed named KA → VM, on-chain, costs TRAC). Don't call without explicit operator instruction.

**Retired (do not call)**: `dkg_annotate_turn`, `dkg_propose_decision`, `dkg_add_task`, `dkg_comment`, `dkg_request_vm_publish`, `dkg_set_session_privacy`, `dkg_get_chat`, `dkg_get_ontology`, `dkg_search`, `dkg_sparql`, `dkg_list_projects`, `dkg_list_subgraphs`.
**Retired (do not call)**: `dkg_publish`, `dkg_shared_memory_publish`, `dkg_share`, `dkg_annotate_turn`, `dkg_propose_decision`, `dkg_add_task`, `dkg_comment`, `dkg_request_vm_publish`, `dkg_set_session_privacy`, `dkg_get_chat`, `dkg_get_ontology`, `dkg_search`, `dkg_sparql`, `dkg_list_projects`, `dkg_list_subgraphs`.

## Query gotchas

Expand All @@ -147,7 +147,7 @@ Things that consistently bite agents writing SPARQL via `dkg_query`:

- **Don't fabricate URIs.** Look-before-mint or skip the edge.
- **Don't skip turns to "save tokens".** A three-step annotation is cheap. Coverage wins.
- **Don't publish to VM via MCP.** `dkg_shared_memory_publish` is operator-gated.
- **Don't publish to VM via MCP.** `dkg_knowledge_asset_publish` is operator-gated.
- **Don't normalise slugs in your `dkg_memory_search` query.** Pass the unnormalised label.
- **Don't try to compute the turn URI yourself.** The `<dkg-session-context>` block gives it to you. Reading capture-chat's session state files is brittle and unnecessary.
- **Don't write `chat:Turn` entities yourself.** The capture hook owns them. You write annotations that LINK to them.
Expand Down
15 changes: 9 additions & 6 deletions bench/analyze-publish-async-get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,9 +163,10 @@ async function analyzeGetFlow(config: BenchmarkConfig, payloadSize: PayloadSizeL
const payload = traceSync(trace, 'setup', 'createPayload', [], 'Generate the asset to publish and read back.', () => (
createPayload(config, `analysis-get-${payloadSize}`, 1, 'sync', false)
));
await traceSharedMemoryWrite(trace, client, config, payload);
await traceAsync(trace, 'setup', 'publishFromSharedMemory', ['promoteSharedRoot'], 'Finalize staged shared-memory content into verifiable memory for the read path.', () => (
client.publishFromSharedMemory(config.contextGraphId, { rootEntities: [payload.rootEntity] }, false)
// publishAssertion stages the quads internally (matches the ESBench path),
// so no separate traceSharedMemoryWrite here — it would double-write SWM.
await traceAsync(trace, 'setup', 'publishAssertion', ['promoteSharedRoot'], 'Finalize the named knowledge asset into verifiable memory for the read path.', () => (
client.publishAssertion(config.contextGraphId, `analysis-get-${payloadSize}`, payload.quads, { clearAfter: false })
), { rootEntity: payload.rootEntity });
const sparql = traceSync(trace, 'measured', 'getSparql', [], 'Build the read query for the published root entity.', () => getSparql(payload.rootEntity));
const response = await traceAsync(trace, 'measured', 'query', ['layer'], 'Read the published marker from the configured memory view.', () => (
Expand All @@ -184,9 +185,11 @@ async function analyzeSyncPublishFlow(config: BenchmarkConfig, payloadSize: Payl
const payload = traceSync(trace, 'setup', 'createPayload', [], 'Generate the payload for synchronous publish.', () => (
createPayload(config, `analysis-sync-${payloadSize}`, 1, 'sync', false)
));
await traceSharedMemoryWrite(trace, client, config, payload);
await traceAsync(trace, 'measured', 'publishFromSharedMemory', ['promoteSharedRoot'], 'Synchronously finalize staged shared-memory content into verifiable memory.', () => (
client.publishFromSharedMemory(config.contextGraphId, { rootEntities: [payload.rootEntity] }, false)
// publishAssertion stages the quads internally (matches the ESBench path), so no
// separate traceSharedMemoryWrite here — it would double-write SWM and make the
// measured publish unrepresentative of the canonical create/write/share/publish flow.
await traceAsync(trace, 'measured', 'publishAssertion', ['promoteSharedRoot'], 'Synchronously publish the named knowledge asset into verifiable memory.', () => (
client.publishAssertion(config.contextGraphId, `analysis-sync-${payloadSize}`, payload.quads, { clearAfter: false })
), { rootEntity: payload.rootEntity });
traceSync(trace, 'cleanup', 'clear', ['Map.clear'], 'Clear all in-memory layers after the representative run.', () => client.clear());
});
Expand Down
33 changes: 23 additions & 10 deletions bench/publish-async-get.bench.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,16 @@ export default defineSuite({
},
{
beforeIteration: async () => {
readPayload = createPayload(config, `esbench-get-${sequence++}`, 1, 'sync', false);
await readClient.sharedMemoryWrite(config.contextGraphId, readPayload.quads);
await readClient.publishFromSharedMemory(
// The KA name must be an assertion-name-safe slug — derive it from the safe
// run id, NOT readPayload.rootEntity (an RDF URN with ':' / '/' that
// validateAssertionName rejects, which would fail the bench before measuring).
const name = `esbench-get-${sequence++}`;
readPayload = createPayload(config, name, 1, 'sync', false);
await readClient.publishAssertion(
config.contextGraphId,
{ rootEntities: [readPayload.rootEntity] },
false,
name,
readPayload.quads,
{ clearAfter: false },
);
},
afterIteration: () => {
Expand All @@ -71,23 +75,32 @@ export default defineSuite({
);

let syncPayload: BenchmarkPayload | undefined;
let syncName = '';
const syncClient = new LayeredDkgBenchmarkClient();
benchAsyncWithHooks(
scene,
'synchronous publish with finalization',
async () => {
const payload = requirePayload(syncPayload, 'synchronous publish with finalization');
const result = await syncClient.publishFromSharedMemory(
const result = await syncClient.publishAssertion(
config.contextGraphId,
{ rootEntities: [payload.rootEntity] },
false,
// Safe slug from the run id — NOT payload.rootEntity (an RDF URN that
// validateAssertionName rejects); same fix as the get/read flow above.
syncName,
payload.quads,
{ clearAfter: false },
);
if (!result.kaId) throw new Error('sync publish did not finalize a knowledge collection');
},
{
beforeIteration: async () => {
syncPayload = createPayload(config, `esbench-sync-${sequence++}`, 1, 'sync', false);
await syncClient.sharedMemoryWrite(config.contextGraphId, syncPayload.quads);
// The measured publishAssertion stages the quads internally — no separate
// sharedMemoryWrite here, or the sync flow would double-write SWM and stop
// matching the canonical create/write/share/publish path (cf. analyze-* and
// the get/read flow). The async flow below keeps its write — it needs the
// returned shareOperationId for the publisher enqueue/lift path.
syncName = `esbench-sync-${sequence++}`;
syncPayload = createPayload(config, syncName, 1, 'sync', false);
},
afterIteration: () => {
syncPayload = undefined;
Expand Down
15 changes: 10 additions & 5 deletions bench/support/layered-dkg-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,16 +83,21 @@ export class LayeredDkgBenchmarkClient implements BenchmarkClient {
return { shareOperationId };
}

async publishFromSharedMemory(
async publishAssertion(
contextGraphId: string,
selection: 'all' | { rootEntities: string[] },
clearAfter = false,
_name: string,
quads: Quad[],
options?: { clearAfter?: boolean },
) {
const roots = selection === 'all' ? [...this.sharedWorkingMemory.keys()] : selection.rootEntities;
// Named-KA one-shot: stage the quads (create → write → share) then publish
// its roots to verifiable memory. Mirrors the create → /vm/publish flow the
// real benchmark client (ApiClient.publishAssertion) drives.
await this.sharedMemoryWrite(contextGraphId, quads);
const roots = uniqueSubjects(quads);
const kaId = `kc-${++this.kcSequence}`;
for (const rootEntity of roots) {
this.promoteSharedRoot(contextGraphId, rootEntity, kaId);
if (clearAfter) this.sharedWorkingMemory.delete(rootEntity);
if (options?.clearAfter) this.sharedWorkingMemory.delete(rootEntity);
}
return {
kaId,
Expand Down
27 changes: 18 additions & 9 deletions devnet/_bootstrap/bootstrap.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -350,7 +350,7 @@ async function main() {
// v10-stress test uses for the publisher nonce race.
await new Promise((r) => setTimeout(r, 2000));
const pubRes = await fetch(
`http://127.0.0.1:${core.apiPort}/api/shared-memory/publish`,
`http://127.0.0.1:${core.apiPort}/api/knowledge-assets/${encodeURIComponent(name)}/vm/publish`,
Comment thread
Jurij89 marked this conversation as resolved.
{
method: 'POST',
headers: {
Expand All @@ -359,20 +359,25 @@ async function main() {
},
body: JSON.stringify({
contextGraphId: CONTEXT_GRAPH,
assertionName: name,
}),
},
);
if (!pubRes.ok) {
// The per-KA /vm/publish route returns a NON-OK status (e.g. 502) for a
// TENTATIVE publish (publisher nonce race), unlike the legacy endpoint
// which returned 200 + status:'tentative'. Read the body before throwing
// so the transient tentative/kaId:'0' outcome is still retried, and only a
// genuine hard failure throws.
const isTentative = (b) => !!b && (b.status === 'tentative' || b.kaId === '0');
let pubJson = await pubRes.json().catch(() => null);
if (!pubRes.ok && !isTentative(pubJson)) {
throw new Error(
`publish #${i} on core${core.num} failed: ${pubRes.status} ${await pubRes.text()}`,
`publish #${i} on core${core.num} failed: ${pubRes.status} ${pubJson ? JSON.stringify(pubJson) : ''}`,
);
}
let pubJson = await pubRes.json();
if (pubJson.status === 'tentative' || pubJson.kaId === '0') {
if (isTentative(pubJson)) {
await new Promise((r) => setTimeout(r, 2000));
const retry = await fetch(
`http://127.0.0.1:${core.apiPort}/api/shared-memory/publish`,
`http://127.0.0.1:${core.apiPort}/api/knowledge-assets/${encodeURIComponent(name)}/vm/publish`,
{
method: 'POST',
headers: {
Expand All @@ -381,11 +386,15 @@ async function main() {
},
body: JSON.stringify({
contextGraphId: CONTEXT_GRAPH,
assertionName: name,
}),
},
);
pubJson = await retry.json();
pubJson = await retry.json().catch(() => null);
if (!retry.ok && !isTentative(pubJson)) {
throw new Error(
`publish #${i} retry on core${core.num} failed: ${retry.status} ${pubJson ? JSON.stringify(pubJson) : ''}`,
);
}
}
publishLog.push({
core: core.num,
Expand Down
29 changes: 18 additions & 11 deletions devnet/agent-provenance/automated.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -862,8 +862,9 @@ describe('Agent provenance — automated 5-node devnet validation', () => {
// 1. Register a fresh custodial agent on core2's daemon. The daemon
// generates a secp256k1 keypair, persists it in agent-keystore.json,
// and returns { agentAddress, authToken, publicKey, privateKey }.
// 2. Use the agent's bearer token to call core2's publish endpoint
// directly (POST /api/shared-memory/write + /api/shared-memory/publish).
// 2. Use the agent's bearer token to stage a named KA on core2
// (POST /api/knowledge-assets {finalize, alsoShareSwm}) + publish it
// (POST /api/knowledge-assets/:name/vm/publish).
// 3. The daemon's publish route resolves the bearer → agent address →
// AgentKeyRecord → custodial private key, and threads it down to
// DKGPublisher as `authorPrivateKey`. The publisher signs the
Expand Down Expand Up @@ -913,16 +914,21 @@ describe('Agent provenance — automated 5-node devnet validation', () => {
const epoch: bigint = await s.chronos.getCurrentEpoch();
const beforeEps: bigint = await s.eps.getNodeEpochPublishingAllocation(core2.identityId, epoch);

// 3. Write a single triple to SWM via the agent's token, then publish.
// 3. Stage a single triple as a named KA (create → write → seal → share)
// via the agent's token, then publish it to VM with the canonical
// per-KA route. (The legacy loose write + selection-publish bridge was
// removed; the named-KA lifecycle is the only publish path now.)
const ts = Date.now();
const subjectIri = `urn:test:mode-b:${ts}`;
const writeRes = await fetch(
`http://127.0.0.1:${core2.apiPort}/api/shared-memory/write`,
const assertionName = `mode-b-${ts}`;
const createRes = await fetch(
Comment thread
Jurij89 marked this conversation as resolved.
`http://127.0.0.1:${core2.apiPort}/api/knowledge-assets`,
{
method: 'POST',
headers: agentHeaders,
body: JSON.stringify({
contextGraphId: CONTEXT_GRAPH,
name: assertionName,
quads: [
{
subject: subjectIri,
Expand All @@ -937,30 +943,31 @@ describe('Agent provenance — automated 5-node devnet validation', () => {
graph: `did:dkg:context-graph:${CONTEXT_GRAPH}`,
},
],
finalize: true,
alsoShareSwm: true,
}),
},
);
if (!writeRes.ok) {
if (!createRes.ok) {
throw new Error(
`core2 /api/shared-memory/write failed: ${writeRes.status} ${await writeRes.text()}`,
`core2 /api/knowledge-assets create failed: ${createRes.status} ${await createRes.text()}`,
);
}

const publishRes = await fetch(
`http://127.0.0.1:${core2.apiPort}/api/shared-memory/publish`,
`http://127.0.0.1:${core2.apiPort}/api/knowledge-assets/${encodeURIComponent(assertionName)}/vm/publish`,
{
method: 'POST',
headers: agentHeaders,
body: JSON.stringify({
contextGraphId: CONTEXT_GRAPH,
selection: { rootEntities: [subjectIri] },
clearAfter: true,
// Per-KA /vm/publish clears only this KA's own roots; no CG-wide clear.
}),
},
);
if (!publishRes.ok) {
throw new Error(
`core2 /api/shared-memory/publish failed: ${publishRes.status} ${await publishRes.text()}`,
`core2 vm/publish failed: ${publishRes.status} ${await publishRes.text()}`,
);
}
const publishJson = (await publishRes.json()) as {
Expand Down
Loading
Loading