LearnNewsExamplesServices
Frontmatter
id10249
titlebindAgentIdentity drops return value — GraphService.getNode Promise is not awaited (#10241 follow-up)
stateClosed
labels
bugairegressionarchitecturecore
assigneesneo-opus-ada
createdAtApr 23, 2026, 6:45 PM
updatedAtJun 7, 2026, 7:20 PM
githubUrlhttps://github.com/neomjs/neo/issues/10249
authorneo-opus-ada
commentsCount0
parentIssue10186
subIssues[]
subIssuesCompleted0
subIssuesTotal0
blockedBy[]
blocking[]
closedAtApr 23, 2026, 6:53 PM

bindAgentIdentity drops return value — GraphService.getNode Promise is not awaited (#10241 follow-up)

Closed v13.0.0/archive-v13-0-0-chunk-5 bugairegressionarchitecturecore
neo-opus-ada
neo-opus-ada commented on Apr 23, 2026, 6:45 PM

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:96HealthService.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.

// `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

  • await added on GraphService.getNode in bindAgentIdentity (Server.mjs:226) with inline rationale comment
  • npm run ai:mcp-client -- --server memory-core --call-tool healthcheck returns identity.bound: true, nodeId: "@<login>" on fresh cold-start under the full swarm state
  • list_messages returns without "no agent identity context bound" error for a resolved stdio identity
  • If reorder included: startup log shows correct bound identity in initial telemetry snapshot
  • Regression test in test/playwright/unit/ai/mcp/server/memory-core/Server.spec.mjs asserting bindAgentIdentity returns the resolved node ID string, not undefined — would fail if await is removed

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>'.

tobiu referenced in commit 27b1dea - "fix(memory-core): await GraphService.getNode Promise in bindAgentIdentity (#10249) (#10250) on Apr 23, 2026, 6:53 PM
tobiu closed this issue on Apr 23, 2026, 6:53 PM
tobiu referenced in commit 2405528 - "feat(ai): migrate memory-core/Server to extend BaseServer + beforeToolDispatch hook (#10965) (#10977) on May 8, 2026, 6:45 PM