Summary
When a context graph is lazily created by a write to a previously-unknown CG ID — e.g. dkg_share / POST /api/shared-memory/write against nonexistent-cg-12345 — the daemon happily creates the CG and accepts the write, but the resulting CG is invisible to the Node UI and to every consumer of GET /api/context-graph/list. It does not appear in the Projects tab, nor in the Context Graphs tab.
CGs created explicitly via dkg_context_graph_create (POST /api/context-graph/create) appear in both tabs as expected.
Reproduction
- Start a clean daemon:
pnpm dkg start.
- From a Hermes (or OpenClaw) chat, share into a CG ID that doesn't exist:
Use dkg_share to share "fail test" against nonexistent-cg-12345.
- Tool response is success-shaped —
triplesWritten: 1, with subject and root_entities. So the write went through.
- Open the Node UI. The CG
nonexistent-cg-12345 does not appear in Projects or Context Graphs.
- Confirm via the daemon list endpoint:
TOK=$(grep -v '^#' ~/.dkg-dev/auth.token | tr -d '[:space:]')
curl -s -X GET "http://127.0.0.1:9200/api/context-graph/list" \
-H "Authorization: Bearer $TOK" | jq '.contextGraphs[].id'
The lazy CG is missing from the list. The data is there (you can SPARQL-query it directly via /api/query), it's just not in the canonical CG inventory.
For contrast, after dkg_context_graph_create({ name: "Explicit", id: "explicit-test" }) the CG immediately shows up in both UI tabs and in the list endpoint.
Root cause
Two paths create CG state, and they write different things:
-
Explicit creation (agent.createContextGraph() at packages/agent/src/dkg-agent.ts:3878-3886) writes the ontology metadata triples that the listing query depends on, including:
<did:dkg:context-graph:<id>> rdf:type dkg:Paranet .
<did:dkg:context-graph:<id>> schema:name "<name>" .
<did:dkg:context-graph:<id>> dkg:creator "<addr>" .
<did:dkg:context-graph:<id>> dkg:createdAt "<ts>" .
<did:dkg:context-graph:<id>> dkg:accessPolicy "public|private" .
These land in either the ontology or the agents system graph.
-
Lazy creation (graphManager.ensureContextGraph() at packages/storage/src/graph-manager.ts:79-87) only calls store.createGraph(...) for the five storage graph URIs (/, /_meta, /_private, /_shared_memory, /_shared_memory_meta). It writes zero ontology metadata triples. It's invoked by every share/publish path: dkg-publisher.ts:333 (share), :536 (conditionalShare), :1004 (publishFromSharedMemory), publish-handler.ts:216, update-handler.ts:143.
The listing query at agent.listContextGraphs() (packages/agent/src/dkg-agent.ts:6761-6800) is filtered on the rdf:type triple:
SELECT ?ctxGraph ... WHERE {
GRAPH <ontology> { ?ctxGraph rdf:type dkg:Paranet . ... }
} UNION {
GRAPH <agents> { ?ctxGraph rdf:type dkg:Paranet . ... }
}
Without that triple the lazy CG is invisible. Same reasoning makes it invisible to:
GET /api/context-graph/list (the daemon route, used by Node UI)
dkg_list_context_graphs (OpenClaw + Hermes tool surfaces)
- The chat-memory project picker in
packages/node-ui/src/chat-memory.ts:512
Affected surfaces
Anything that writes to a CG without going through explicit registration first creates a ghost:
POST /api/shared-memory/write (dkg_share + bulk SWM writes)
POST /api/publish (dkg_publish two-call helper)
POST /api/assertion/create if the CG was never registered (need to verify)
- Cross-CG promotion targets that don't exist locally yet
Fix options
Option A — make ensureContextGraph write the metadata too
Extend graphManager.ensureContextGraph (or a new ensureContextGraphRegistered helper called immediately after) to also insert the rdf:type dkg:Paranet + dkg:createdAt + dkg:creator triples in the agents graph. Auto-set dkg:accessPolicy to "public" by default since lazy creates can't ask the operator about curation.
Pros: minimal-touch, every existing write path benefits automatically.
Cons: a write to a typo'd CG ID still silently creates a ghost project — the discoverability gets fixed but the underlying "any write creates a CG" footgun remains. Operators may end up with dozens of stub CGs from misspellings.
Option B — fail share/publish on unknown CG, force explicit creation
Make agent.share() / agent.publish() / etc. reject writes to unregistered CGs with a clear error pointing the caller to dkg_context_graph_create. Keep the existing register_if_needed: true opt-in on dkg_shared_memory_publish for the chain-registration case.
Pros: enforces the "explicit > implicit" pattern that #413's context_graph_id-required unification already started; eliminates the ghost-CG class of bug entirely.
Cons: breaking change for any caller relying on lazy creation; requires either a deprecation window or a flag.
Option C — hybrid
Lazy creation stays available but emits a structured warning in the response payload (createdImplicitly: true or similar) and the metadata gets written so the CG is visible. Document in SKILL.md that explicit creation is preferred.
Pros: combines A's ergonomics with discoverability and signal that something unusual happened.
Cons: extra response field that downstream consumers need to learn about.
Recommendation: Option A as the smallest fix that closes the visibility gap, with a separate follow-up to consider Option B once the operator-prefs around lazy creation are clearer. Document in SKILL.md regardless.
Acceptance criteria
Out of scope
- The deeper question of whether lazy CG creation should be allowed at all (Option B) — file as a follow-up if the ergonomic tradeoff is worth a deprecation cycle.
- On-chain registration of lazy CGs — separate concern (
register_if_needed: true already covers the explicit case).
Related
Summary
When a context graph is lazily created by a write to a previously-unknown CG ID — e.g.
dkg_share/POST /api/shared-memory/writeagainstnonexistent-cg-12345— the daemon happily creates the CG and accepts the write, but the resulting CG is invisible to the Node UI and to every consumer ofGET /api/context-graph/list. It does not appear in the Projects tab, nor in the Context Graphs tab.CGs created explicitly via
dkg_context_graph_create(POST /api/context-graph/create) appear in both tabs as expected.Reproduction
pnpm dkg start.triplesWritten: 1, withsubjectandroot_entities. So the write went through.nonexistent-cg-12345does not appear in Projects or Context Graphs.The lazy CG is missing from the list. The data is there (you can SPARQL-query it directly via
/api/query), it's just not in the canonical CG inventory.For contrast, after
dkg_context_graph_create({ name: "Explicit", id: "explicit-test" })the CG immediately shows up in both UI tabs and in the list endpoint.Root cause
Two paths create CG state, and they write different things:
Explicit creation (
agent.createContextGraph()atpackages/agent/src/dkg-agent.ts:3878-3886) writes the ontology metadata triples that the listing query depends on, including:These land in either the ontology or the agents system graph.
Lazy creation (
graphManager.ensureContextGraph()atpackages/storage/src/graph-manager.ts:79-87) only callsstore.createGraph(...)for the five storage graph URIs (/,/_meta,/_private,/_shared_memory,/_shared_memory_meta). It writes zero ontology metadata triples. It's invoked by every share/publish path:dkg-publisher.ts:333(share),:536(conditionalShare),:1004(publishFromSharedMemory),publish-handler.ts:216,update-handler.ts:143.The listing query at
agent.listContextGraphs()(packages/agent/src/dkg-agent.ts:6761-6800) is filtered on the rdf:type triple:Without that triple the lazy CG is invisible. Same reasoning makes it invisible to:
GET /api/context-graph/list(the daemon route, used by Node UI)dkg_list_context_graphs(OpenClaw + Hermes tool surfaces)packages/node-ui/src/chat-memory.ts:512Affected surfaces
Anything that writes to a CG without going through explicit registration first creates a ghost:
POST /api/shared-memory/write(dkg_share+ bulk SWM writes)POST /api/publish(dkg_publishtwo-call helper)POST /api/assertion/createif the CG was never registered (need to verify)Fix options
Option A — make
ensureContextGraphwrite the metadata tooExtend
graphManager.ensureContextGraph(or a newensureContextGraphRegisteredhelper called immediately after) to also insert therdf:type dkg:Paranet+dkg:createdAt+dkg:creatortriples in the agents graph. Auto-setdkg:accessPolicyto "public" by default since lazy creates can't ask the operator about curation.Pros: minimal-touch, every existing write path benefits automatically.
Cons: a write to a typo'd CG ID still silently creates a ghost project — the discoverability gets fixed but the underlying "any write creates a CG" footgun remains. Operators may end up with dozens of stub CGs from misspellings.
Option B — fail share/publish on unknown CG, force explicit creation
Make
agent.share()/agent.publish()/ etc. reject writes to unregistered CGs with a clear error pointing the caller todkg_context_graph_create. Keep the existingregister_if_needed: trueopt-in ondkg_shared_memory_publishfor the chain-registration case.Pros: enforces the "explicit > implicit" pattern that #413's
context_graph_id-required unification already started; eliminates the ghost-CG class of bug entirely.Cons: breaking change for any caller relying on lazy creation; requires either a deprecation window or a flag.
Option C — hybrid
Lazy creation stays available but emits a structured warning in the response payload (
createdImplicitly: trueor similar) and the metadata gets written so the CG is visible. Document in SKILL.md that explicit creation is preferred.Pros: combines A's ergonomics with discoverability and signal that something unusual happened.
Cons: extra response field that downstream consumers need to learn about.
Recommendation: Option A as the smallest fix that closes the visibility gap, with a separate follow-up to consider Option B once the operator-prefs around lazy creation are clearer. Document in SKILL.md regardless.
Acceptance criteria
dkg_share/dkg_publish/ any other write to a previously-unknown CG ID, that CG appears inGET /api/context-graph/listand in the Node UI's Projects + Context Graphs tabs.agent.listContextGraphs()returns a row withname,creator,createdAt,accessPolicypopulated (auto-defaulted as needed — e.g.name = id,accessPolicy = "public"since the lazy path has no operator input).dkg_context_graph_create).packages/agent/test/(or wherever the daemon-route tests live) that POSTs to/api/shared-memory/writeagainst a fresh CG ID, then assertsGET /api/context-graph/listincludes that ID.Out of scope
register_if_needed: truealready covers the explicit case).Related
dkg_share(surfaced this gap during PR fix(adapter-hermes): mint unique subjects, quote literals, full escape coverage in dkg_share (#414) #418's manual smoke; the round-6 test againstnonexistent-cg-12345revealed that the daemon happily wrote into a ghost CG).dkg_sharehardening (same manual-smoke recipe, same observation).dkg_shareparity asks.