Context
PR #10242 (merged as 016581461) shipped busy_timeout=5000 as the nominal fix for #10241's boot-time identity binding race. Empirically, the bind still fails under the full swarm state: healthcheck.identity.bound: false across 4 consecutive cold-restart cycles on Claude Code this session. The current session's diagnostic work identified the actual root cause: GraphService.getNode returns a Promise (Neo framework-level singleton method wrapping), and the merged bindAgentIdentity code path does not await it.
Ironically, PR #10242's earlier retry-loop drafts had the correct await. When the retry was reverted per reviewer feedback (mine), the await was dropped alongside. The root-cause fix was present at one point and then accidentally removed during iteration — a peer-review regression I own.
The Problem
bindAgentIdentity at Server.mjs:226 currently:
const node = GraphService.getNode({id: graphNodeId});
if (node) {
return node.id;
}GraphService.getNode(...) returns a Promise. node is a Promise object. node.id on a Promise is undefined. bindAgentIdentity returns undefined. stdioIdentity.agentIdentityNodeId = undefined. HealthService.buildIdentityBlock projects !!undefined → false, undefined || null → null. Healthcheck shows bound: false, nodeId: null.
Empirical reproduction (via ai/mcp/client/mcp-cli.mjs fresh-spawn)
| Variant |
healthcheck.identity.bound |
list_messages |
| Current dev (no await) |
false |
"no agent identity context bound" |
| Reorder-only (Gemini's initial proposal) |
false |
fails |
await only |
true |
returns messages |
await + reorder |
true |
returns messages |
DIAG output that pinned the mechanism
[DIAG #10241] typeof node=object isPromise=true keys=
[DIAG #10241] JSON.stringify(node)={}
[DIAG #10241] awaited Promise → JSON.stringify(awaited)={"id":"@neo-opus-ada","type":"AgentIdentity",...}The Architectural Reality
Primary fix surface:
ai/mcp/server/memory-core/Server.mjs:226 — missing await on GraphService.getNode
Secondary boot-sequence observability concern (independent, same file):
ai/mcp/server/memory-core/Server.mjs:96 — HealthService.healthcheck() runs BEFORE identity resolution
ai/mcp/server/memory-core/Server.mjs:117 — identity resolution runs INSIDE stdio transport-connect branch
- Runtime MCP healthcheck tool calls DO see correct state (setStdioIdentityState runs pre-transport-connect)
- But boot-time telemetry logged at startup reflects pre-bind null — observability drift, not functional bug
Framework mechanism behind the Promise wrapping:
src/core/Base.mjs + Neo.setupClass — singleton method return wrapping. Exact mechanism is a separate KB_GAP investigation (why does a synchronous method body return a Promise at call time?). Not blocking this fix — the await at the call site is the minimal correct surface regardless of framework internals.
The Fix
Primary (required)
Add await on Server.mjs:226 with inline rationale comment citing the Promise-returning framework semantics so a future reviewer does not classify the await as decorative.
const node = await GraphService.getNode({id: graphNodeId});Complementary (optional, same PR)
Elevate identity resolution OUT of the stdio transport-connect branch and BEFORE the startup HealthService.healthcheck() call, so boot telemetry reflects the bound state from the first snapshot. No runtime-bind correctness impact; strictly observability. Gemini's proposal on this is correct.
Acceptance Criteria
Out of Scope
- Investigating WHY Neo singleton methods return Promises (separate KB_GAP ticket — architectural curiosity, not blocking)
- Refactoring
GraphService.getNode to be unambiguously sync or async
- Any changes to SQLite pragmas, WAL handling, busy_timeout — #10242's
busy_timeout=5000 is unrelated to this bug and stays
- Any changes to the identity self-seed loop or its try/catch wrappers
Avoided Traps
- "Just remove the
await — getNode is a sync method": rejected. Empirically proven via DIAG output that getNode returns a Promise at runtime. This was my own first-review mistake on #10242 that caused the regression; memorialized here to prevent recurrence.
- "Add more retries / longer backoff": rejected. #10241's Avoided Traps section already rules this out. The
await is the primitive; retry would mask the same bug at a different layer.
- "Refactor getNode to be async explicitly": rejected for this ticket's scope. The Promise-wrapping happens in framework-level setupClass or singleton proxy semantics, not in getNode itself. Fixing at the call site is the minimal correct surface.
Related
- #10241 — closed but incomplete; this ticket is the actual root-cause fix
- PR #10242 — merged via
016581461; landed busy_timeout + boot self-seed try/catch wrappers + reverted the retry-with-delete (which coincidentally dropped the load-bearing await)
- #10139 — A2A Mailbox primitive (grand-parent); Priority 0 closes for real when this lands
- #10186 — MCP concurrency audit epic
- #10245 / #10246 — documentation retrospectives from #10242's review cycle (adjacent, not dependency)
Origin Session ID
Origin Session ID: 5f746864-85c4-4b2c-bd79-64c76a8a2be3
Handoff Retrieval Hints
Retrieval Hint: "bindAgentIdentity Promise getNode await undefined"
Retrieval Hint: "10241 follow-up boot time identity race root cause"
Commit-range anchor: dev HEAD = a94cde372 at ticket filing
Reproduction one-liner
NEO_AGENT_IDENTITY=<any-seeded-identity> npm run ai:mcp-client -- --server memory-core --call-tool healthcheck --args '{}'On current dev (pre-fix): identity: {source: 'env-var', bound: false, nodeId: null}.
Post-fix: bound: true, nodeId: '@<login>'.
Context
PR #10242 (merged as
016581461) shippedbusy_timeout=5000as the nominal fix for #10241's boot-time identity binding race. Empirically, the bind still fails under the full swarm state:healthcheck.identity.bound: falseacross 4 consecutive cold-restart cycles on Claude Code this session. The current session's diagnostic work identified the actual root cause:GraphService.getNodereturns a Promise (Neo framework-level singleton method wrapping), and the mergedbindAgentIdentitycode path does notawaitit.Ironically, PR #10242's earlier retry-loop drafts had the correct
await. When the retry was reverted per reviewer feedback (mine), theawaitwas dropped alongside. The root-cause fix was present at one point and then accidentally removed during iteration — a peer-review regression I own.The Problem
bindAgentIdentityatServer.mjs:226currently:const node = GraphService.getNode({id: graphNodeId}); if (node) { return node.id; }GraphService.getNode(...)returns a Promise.nodeis a Promise object.node.idon a Promise isundefined.bindAgentIdentityreturnsundefined.stdioIdentity.agentIdentityNodeId = undefined.HealthService.buildIdentityBlockprojects!!undefined → false, undefined || null → null. Healthcheck showsbound: false, nodeId: null.Empirical reproduction (via
ai/mcp/client/mcp-cli.mjsfresh-spawn)healthcheck.identity.boundlist_messagesawaitonlyawait+ reorderDIAG output that pinned the mechanism
[DIAG #10241] typeof node=object isPromise=true keys= [DIAG #10241] JSON.stringify(node)={} [DIAG #10241] awaited Promise → JSON.stringify(awaited)={"id":"@neo-opus-ada","type":"AgentIdentity",...}The Architectural Reality
Primary fix surface:
ai/mcp/server/memory-core/Server.mjs:226— missingawaitonGraphService.getNodeSecondary boot-sequence observability concern (independent, same file):
ai/mcp/server/memory-core/Server.mjs:96—HealthService.healthcheck()runs BEFORE identity resolutionai/mcp/server/memory-core/Server.mjs:117— identity resolution runs INSIDE stdio transport-connect branchFramework mechanism behind the Promise wrapping:
src/core/Base.mjs+Neo.setupClass— singleton method return wrapping. Exact mechanism is a separate KB_GAP investigation (why does a synchronous method body return a Promise at call time?). Not blocking this fix — theawaitat the call site is the minimal correct surface regardless of framework internals.The Fix
Primary (required)
Add
awaitonServer.mjs:226with inline rationale comment citing the Promise-returning framework semantics so a future reviewer does not classify theawaitas decorative.// `GraphService.getNode` returns a Promise (Neo singleton method wrapper); // awaiting it unpacks to `{id, type, name, ...}`. Without `await`, `node.id` // on the Promise object is `undefined` — the actual root cause of #10241's // boot-time bind failure that busy_timeout/retry/reorder patches only // partially masked during #10242's review cycle. const node = await GraphService.getNode({id: graphNodeId});Complementary (optional, same PR)
Elevate identity resolution OUT of the stdio transport-connect branch and BEFORE the startup
HealthService.healthcheck()call, so boot telemetry reflects the bound state from the first snapshot. No runtime-bind correctness impact; strictly observability. Gemini's proposal on this is correct.Acceptance Criteria
awaitadded onGraphService.getNodeinbindAgentIdentity(Server.mjs:226) with inline rationale commentnpm run ai:mcp-client -- --server memory-core --call-tool healthcheckreturnsidentity.bound: true, nodeId: "@<login>"on fresh cold-start under the full swarm statelist_messagesreturns without "no agent identity context bound" error for a resolved stdio identitytest/playwright/unit/ai/mcp/server/memory-core/Server.spec.mjsassertingbindAgentIdentityreturns the resolved node ID string, notundefined— would fail ifawaitis removedOut of Scope
GraphService.getNodeto be unambiguously sync or asyncbusy_timeout=5000is unrelated to this bug and staysAvoided Traps
await— getNode is a sync method": rejected. Empirically proven via DIAG output that getNode returns a Promise at runtime. This was my own first-review mistake on #10242 that caused the regression; memorialized here to prevent recurrence.awaitis the primitive; retry would mask the same bug at a different layer.Related
016581461; landed busy_timeout + boot self-seed try/catch wrappers + reverted the retry-with-delete (which coincidentally dropped the load-bearingawait)Origin Session ID
Origin Session ID:
5f746864-85c4-4b2c-bd79-64c76a8a2be3Handoff Retrieval Hints
Retrieval Hint:
"bindAgentIdentity Promise getNode await undefined"Retrieval Hint:"10241 follow-up boot time identity race root cause"Commit-range anchor: dev HEAD =a94cde372at ticket filingReproduction one-liner
NEO_AGENT_IDENTITY=<any-seeded-identity> npm run ai:mcp-client -- --server memory-core --call-tool healthcheck --args '{}'On current dev (pre-fix):
identity: {source: 'env-var', bound: false, nodeId: null}. Post-fix:bound: true, nodeId: '@<login>'.