Skip to content

bug(agent,storage): lazily-created context graphs are invisible to GET /api/context-graph/list and the Node UI #419

@Jurij89

Description

@Jurij89

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

  1. Start a clean daemon: pnpm dkg start.
  2. 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.

  3. Tool response is success-shaped — triplesWritten: 1, with subject and root_entities. So the write went through.
  4. Open the Node UI. The CG nonexistent-cg-12345 does not appear in Projects or Context Graphs.
  5. 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

  • After dkg_share / dkg_publish / any other write to a previously-unknown CG ID, that CG appears in GET /api/context-graph/list and in the Node UI's Projects + Context Graphs tabs.
  • The lazy-created CG carries enough metadata that agent.listContextGraphs() returns a row with name, creator, createdAt, accessPolicy populated (auto-defaulted as needed — e.g. name = id, accessPolicy = "public" since the lazy path has no operator input).
  • Existing explicit-creation flows are unchanged (no regression on dkg_context_graph_create).
  • Test coverage: a new test in packages/agent/test/ (or wherever the daemon-route tests live) that POSTs to /api/shared-memory/write against a fresh CG ID, then asserts GET /api/context-graph/list includes that ID.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions