Identity binding stays null post-#10182 — enable debug logging + diagnose
Context
#10182 merged the self-heal hook from #10181's proposed fix path (a): when stdioIdentity.agentIdentityNodeId is null at dispatch time, re-attempt bindAgentIdentity once. The patch is live in dev (commit fda5dd4be as (#10181) (#10182)). Claude Desktop + Antigravity were git pulled and restarted. Binding remains null.
Empirical state captured 2026-04-22T16:07Z, 5 minutes after restart on commit fda5dd4be:
| Check |
Result |
| Main checkout git head includes self-heal |
✓ fda5dd4be |
Running MCP process (pid 22656, started 18:07:12 CEST) |
Post-merge; patch is in the file it loaded |
grep "Identity self-healed" in Server.mjs |
✓ present — code is there |
healthcheck → mailboxPreview |
null (binding null) |
list_messages |
Errors "Cannot list messages: no agent identity context bound" |
get_node({id: '@neo-opus-ada'}) via MCP |
Succeeds — returns the full AgentIdentity node |
Direct SQLite SELECT id FROM Nodes WHERE id='@neo-opus-ada' |
Row present (same graph) |
NEO_AGENT_IDENTITY=neo-opus-ada in MCP process env |
Confirmed via ps -E |
So the MCP server has the patch, the graph has the node, the env var is set, yet bindAgentIdentity keeps returning null. get_node invoked from the MCP tool surface hits the exact same GraphService.getNode call chain and returns the node fine. Something about the path bindAgentIdentity → await GraphService.ready() → getNode behaves differently from toolDispatch → GraphService.getNode, but we can't tell what because the diagnostic logger is silent.
The Problem
The diagnostic blocker: ai/mcp/server/memory-core/logger.mjs gates every logger.info/warn/error/debug call behind aiConfig.debug:
const createLogMethod = (level) => {
return (...args) => {
if (aiConfig.debug) {
console.error(`[${level.toUpperCase()}]`, ...args);
}
};
};config.mjs line 56 hardcodes debug: false with no environment override. Every trace in the binding path fires to /dev/null:
logger.info("[neo-memory-core MCP] Identity: ${userId} via ${source} — ${bound}") (Server.mjs logIdentityStatus)
logger.warn("[neo-memory-core MCP] AgentIdentity graph lookup failed...") (bindAgentIdentity catch)
logger.info("[neo-memory-core MCP] Identity self-healed: ...") (my #10182 patch)
logger.warn("[neo-memory-core MCP] Identity self-heal attempt failed...") (my #10182 patch)
Claude Desktop redirects MCP stderr to ~/Library/Logs/Claude/mcp-server-neo-mjs-memory-core.log, but those logger.* calls never fire their underlying console.error. The log file only captures MCP SDK JSON-RPC frames. Every observable diagnostic signal for this class of failure is dark.
Hypotheses for the binding null (any of which could be the root cause; none provable without logs):
vicinityLoadedNodes stuck-cache. In ai/graph/Database.mjs getAdjacentNodes (lines 252-283), me.vicinityLoadedNodes.add(nodeId) fires regardless of whether vicinity.nodes.length > 0. If the boot-time call for @neo-opus-ada returned empty (for whatever reason — WAL checkpoint state, race, etc.), subsequent lookups skip SQLite. But this contradicts the observation that get_node works live — unless the cache gets invalidated somehow between boot and first tool call.
GraphService.ready() resolves before the graph is actually ready for the specific node lookup. GraphService.initAsync awaits storage.load() (which only sets lastSyncId) and one getAdjacentNodes('frontier') call. It does NOT pre-load all nodes. Lazy load of @neo-opus-ada happens on first access. If bindAgentIdentity hits it before the SQLite connection is fully warmed or something similar, it could return null.
bindAgentIdentity throws, catch returns null. The try/catch in Server.mjs:bindAgentIdentity catches any thrown error and returns null silently (only logs via the gated logger). If something in await GraphService.ready() or getNode throws in the boot-time context but not the tool-dispatch context, we'd see exactly this behavior.
Async timing within the self-heal block. My #10182 self-heal awaits bindAgentIdentity per dispatch. If the await returns null every time (same underlying issue as #1-#3), the heal never succeeds. Each dispatch re-runs it, each returns null.
Something else entirely — e.g., there are two singleton instances of GraphService due to mixed import paths, or ai/services.mjs vs Server.mjs imports resolve differently under some condition.
The Architectural Reality
Files touched by the fix + diagnostic work:
ai/mcp/server/memory-core/config.mjs (line 56, debug: false) — the hardcoded gate
ai/mcp/server/memory-core/logger.mjs (lines 8-22) — the level-wrapping logic
ai/mcp/server/memory-core/Server.mjs:
initAsync (lines 61-125, boot sequence)
resolveStdioIdentity (lines 144-159)
bindAgentIdentity (lines 206-222)
- Dispatch block with
#10182's self-heal (lines 336-366)
logIdentityStatus (lines 228-238)
ai/graph/Database.mjs — getAdjacentNodes lazy-load + vicinityLoadedNodes cache
ai/graph/storage/SQLite.mjs — loadNodeVicinitySync + load
Claude Desktop MCP log path: ~/Library/Logs/Claude/mcp-server-neo-mjs-memory-core.log. Stderr fd2 already redirected here (confirmed via lsof), so enabling logger.* output automatically surfaces traces in this file.
The Fix
Two parts, one ticket, two PRs if that's cleaner:
Part 1 — Enable configurable debug logging (small, ~5 lines).
ai/mcp/server/memory-core/config.mjs line 56:
debug: false,
debug: process.env.NEO_MEM_DEBUG === 'true',
Launch MCP server with NEO_MEM_DEBUG=true in the env block of claude_desktop_config.json (memory-core entry) to enable. Default stays false — no behavior change for normal operation.
Consider applying the same pattern to the other three MCP servers' config.mjs (knowledge-base, github-workflow, neural-link) for symmetry, or leave as a one-off fix here until another server needs it.
Part 2 — Diagnose + fix the binding null (medium, depends on what Part 1 reveals).
With debug enabled, restart Claude Desktop. Tail ~/Library/Logs/Claude/mcp-server-neo-mjs-memory-core.log. Grep for the four binding-trace lines noted above. Expected diagnostic chain:
[neo-memory-core MCP] Identity: <userId> via <source> — <bound status> should reveal whether logIdentityStatus was called with a bound or unbound identity at boot
- If unbound:
AgentIdentity graph lookup failed for @<login>: <error.message> surfaces whether bindAgentIdentity's try/catch fired + the error message
- On first
list_messages dispatch: Identity self-healed: <userId> → <nodeId> fires if the heal worked, OR Identity self-heal attempt failed: <error> fires if the rebind threw
Based on what the logs reveal, the fix lands in this same ticket or spawns a narrower follow-up. Candidate fixes (depending on root cause):
- If hypothesis 1 (stuck vicinity cache): fix
Database.mjs:getAdjacentNodes to not mark-loaded when vicinity.nodes.length === 0
- If hypothesis 2 (ready-but-not-ready): either extend
GraphService.ready() semantics to gate on a fuller graph-load signal, or retry in bindAgentIdentity with a small backoff
- If hypothesis 3 (silent throw): fix whatever
bindAgentIdentity throws on at boot; logs will name the offending error
- If hypothesis 4 (inherited from 1-3): resolves with the upstream fix
- If hypothesis 5 (duplicate singletons): audit the
Neo.setupClass gatekeeper for memory-core-graph or fix the import paths
Acceptance Criteria
Out of Scope
- Building the MCP Server integration test harness — owned by
#10183.
- Retro-enabling debug on the other MCP servers' config.mjs — optional consistency pass; this ticket focuses on memory-core where the blocking issue lives.
- Full diagnostic-logging framework upgrades — if the simple env-var flip is enough to diagnose, no need for a larger ergonomics overhaul. File separately if the pattern repeats.
Avoided Traps
- "Ship another self-heal patch without diagnostic visibility." Rejected.
#10182 already demonstrated that without logs, we're guessing at the right fix. Part 1 (enable logs) must land before any further binding-path code change.
- "Add unconditional
console.error calls everywhere in the binding path." Rejected — production log noise. The existing gated logger.* calls are correctly scoped; they just need a runtime toggle.
- "Treat this as a 'works on my machine' environment issue and move on." Rejected — the failure is reproducible across 4 restarts with identical symptoms. Deterministic bugs deserve deterministic investigations.
- "Patch
bindAgentIdentity to log via console.error directly, bypassing the gated logger." Considered. Works as a fast diagnostic but creates a dedicated debug-only code path that has to be unwound after the fix lands. Cleaner to fix the gate properly — that's Part 1.
Handoff Context
This ticket is intentionally written for fresh-session pickup by another agent (Gemini 3.1 Pro was tentatively identified as a good fit — she has deep recent substrate context from #10177 / #10183 and is in the same mailbox-A2A operational arc). Origin session below preserves the full diagnostic chain from 2026-04-22: four Claude Desktop restarts, SQLite state inspection, MCP process env verification, file-level grep confirmation of the patch landing. Memory Core query by session ID surfaces the complete prior-session decision trail without re-derivation.
If picking this up from Antigravity: the binding failure symptomatically reproduces on that harness too (per earlier empirical handshake attempts in session 7ee23599-...). The fix benefits both harnesses equally.
Related
- Parent:
#10139 (Mailbox A2A primitive epic)
- Builds on:
#10181 (closed by #10182 at the PR level but the underlying bug persists), #10182 (self-heal PR — merged, insufficient alone), #10174 (mailbox runtime fix that originally exercised the binding path)
- Adjacent:
#10183 (MCP test harness — will cover this class of bug end-to-end once built)
- Not related to:
#10179 / #10180 (orthogonal mailbox polish tickets)
Origin Session ID: 7ee23599-3f94-4f75-b54c-97ba92c9aaef
Identity binding stays null post-#10182 — enable debug logging + diagnose
Context
#10182merged the self-heal hook from#10181's proposed fix path (a): whenstdioIdentity.agentIdentityNodeIdis null at dispatch time, re-attemptbindAgentIdentityonce. The patch is live indev(commitfda5dd4beas(#10181) (#10182)). Claude Desktop + Antigravity weregit pulled and restarted. Binding remains null.Empirical state captured 2026-04-22T16:07Z, 5 minutes after restart on commit
fda5dd4be:fda5dd4bepid 22656, started 18:07:12 CEST)grep "Identity self-healed"inServer.mjshealthcheck→mailboxPreviewnull(binding null)list_messages"Cannot list messages: no agent identity context bound"get_node({id: '@neo-opus-ada'})via MCPSELECT id FROM Nodes WHERE id='@neo-opus-ada'NEO_AGENT_IDENTITY=neo-opus-adain MCP process envps -ESo the MCP server has the patch, the graph has the node, the env var is set, yet
bindAgentIdentitykeeps returning null.get_nodeinvoked from the MCP tool surface hits the exact sameGraphService.getNodecall chain and returns the node fine. Something about the pathbindAgentIdentity → await GraphService.ready() → getNodebehaves differently fromtoolDispatch → GraphService.getNode, but we can't tell what because the diagnostic logger is silent.The Problem
The diagnostic blocker:
ai/mcp/server/memory-core/logger.mjsgates everylogger.info/warn/error/debugcall behindaiConfig.debug:const createLogMethod = (level) => { return (...args) => { if (aiConfig.debug) { console.error(`[${level.toUpperCase()}]`, ...args); } }; };config.mjsline 56 hardcodesdebug: falsewith no environment override. Every trace in the binding path fires to/dev/null:logger.info("[neo-memory-core MCP] Identity: ${userId} via ${source} — ${bound}")(Server.mjslogIdentityStatus)logger.warn("[neo-memory-core MCP] AgentIdentity graph lookup failed...")(bindAgentIdentity catch)logger.info("[neo-memory-core MCP] Identity self-healed: ...")(my#10182patch)logger.warn("[neo-memory-core MCP] Identity self-heal attempt failed...")(my#10182patch)Claude Desktop redirects MCP stderr to
~/Library/Logs/Claude/mcp-server-neo-mjs-memory-core.log, but thoselogger.*calls never fire their underlyingconsole.error. The log file only captures MCP SDK JSON-RPC frames. Every observable diagnostic signal for this class of failure is dark.Hypotheses for the binding null (any of which could be the root cause; none provable without logs):
vicinityLoadedNodesstuck-cache. Inai/graph/Database.mjsgetAdjacentNodes(lines 252-283),me.vicinityLoadedNodes.add(nodeId)fires regardless of whethervicinity.nodes.length > 0. If the boot-time call for@neo-opus-adareturned empty (for whatever reason — WAL checkpoint state, race, etc.), subsequent lookups skip SQLite. But this contradicts the observation thatget_nodeworks live — unless the cache gets invalidated somehow between boot and first tool call.GraphService.ready()resolves before the graph is actually ready for the specific node lookup.GraphService.initAsyncawaitsstorage.load()(which only setslastSyncId) and onegetAdjacentNodes('frontier')call. It does NOT pre-load all nodes. Lazy load of@neo-opus-adahappens on first access. IfbindAgentIdentityhits it before the SQLite connection is fully warmed or something similar, it could return null.bindAgentIdentitythrows, catch returns null. Thetry/catchinServer.mjs:bindAgentIdentitycatches any thrown error and returns null silently (only logs via the gated logger). If something inawait GraphService.ready()orgetNodethrows in the boot-time context but not the tool-dispatch context, we'd see exactly this behavior.Async timing within the self-heal block. My
#10182self-heal awaitsbindAgentIdentityper dispatch. If the await returns null every time (same underlying issue as #1-#3), the heal never succeeds. Each dispatch re-runs it, each returns null.Something else entirely — e.g., there are two singleton instances of
GraphServicedue to mixed import paths, orai/services.mjsvsServer.mjsimports resolve differently under some condition.The Architectural Reality
Files touched by the fix + diagnostic work:
ai/mcp/server/memory-core/config.mjs(line 56,debug: false) — the hardcoded gateai/mcp/server/memory-core/logger.mjs(lines 8-22) — the level-wrapping logicai/mcp/server/memory-core/Server.mjs:initAsync(lines 61-125, boot sequence)resolveStdioIdentity(lines 144-159)bindAgentIdentity(lines 206-222)#10182's self-heal (lines 336-366)logIdentityStatus(lines 228-238)ai/graph/Database.mjs—getAdjacentNodeslazy-load +vicinityLoadedNodescacheai/graph/storage/SQLite.mjs—loadNodeVicinitySync+loadClaude Desktop MCP log path:
~/Library/Logs/Claude/mcp-server-neo-mjs-memory-core.log. Stderr fd2 already redirected here (confirmed vialsof), so enablinglogger.*output automatically surfaces traces in this file.The Fix
Two parts, one ticket, two PRs if that's cleaner:
Part 1 — Enable configurable debug logging (small, ~5 lines).
ai/mcp/server/memory-core/config.mjsline 56:// Before: debug: false, // After: debug: process.env.NEO_MEM_DEBUG === 'true',Launch MCP server with
NEO_MEM_DEBUG=truein the env block ofclaude_desktop_config.json(memory-core entry) to enable. Default staysfalse— no behavior change for normal operation.Consider applying the same pattern to the other three MCP servers' config.mjs (knowledge-base, github-workflow, neural-link) for symmetry, or leave as a one-off fix here until another server needs it.
Part 2 — Diagnose + fix the binding null (medium, depends on what Part 1 reveals).
With debug enabled, restart Claude Desktop. Tail
~/Library/Logs/Claude/mcp-server-neo-mjs-memory-core.log. Grep for the four binding-trace lines noted above. Expected diagnostic chain:[neo-memory-core MCP] Identity: <userId> via <source> — <bound status>should reveal whetherlogIdentityStatuswas called with a bound or unbound identity at bootAgentIdentity graph lookup failed for @<login>: <error.message>surfaces whetherbindAgentIdentity's try/catch fired + the error messagelist_messagesdispatch:Identity self-healed: <userId> → <nodeId>fires if the heal worked, ORIdentity self-heal attempt failed: <error>fires if the rebind threwBased on what the logs reveal, the fix lands in this same ticket or spawns a narrower follow-up. Candidate fixes (depending on root cause):
Database.mjs:getAdjacentNodesto not mark-loaded whenvicinity.nodes.length === 0GraphService.ready()semantics to gate on a fuller graph-load signal, or retry inbindAgentIdentitywith a small backoffbindAgentIdentitythrows on at boot; logs will name the offending errorNeo.setupClassgatekeeper formemory-core-graphor fix the import pathsAcceptance Criteria
aiConfig.debugis overridable viaNEO_MEM_DEBUG=trueenvironment variable inai/mcp/server/memory-core/config.mjsNEO_MEM_DEBUG=true, the startup sequence logsIdentity: <x> via <source> — <bound status>to~/Library/Logs/Claude/mcp-server-neo-mjs-memory-core.log(or equivalent harness stderr sink)bindAgentIdentitynull-return identified and documented in the ticket (update this body or post a comment)NEO_MEM_DEBUG=false,list_messagesreturns without the"no agent identity context bound"erroradd_memoryresponse carries a populatedmailbox: {unreadCount, latestPreview}block when messages exist in the caller's inbox@neo-opus-adasendsadd_message({to: 'AGENT:*', subject: 'post-fix handshake', ...}),@neo-gemini-prosees it vialist_messages({box: 'inbox'}). Both directions work.#10181AC item for a dedicated Server.mjs regression test is still tracked via#10183(MCP test harness); this ticket does not re-duplicate that AC.Out of Scope
#10183.Avoided Traps
#10182already demonstrated that without logs, we're guessing at the right fix. Part 1 (enable logs) must land before any further binding-path code change.console.errorcalls everywhere in the binding path." Rejected — production log noise. The existing gatedlogger.*calls are correctly scoped; they just need a runtime toggle.bindAgentIdentityto log viaconsole.errordirectly, bypassing the gated logger." Considered. Works as a fast diagnostic but creates a dedicated debug-only code path that has to be unwound after the fix lands. Cleaner to fix the gate properly — that's Part 1.Handoff Context
This ticket is intentionally written for fresh-session pickup by another agent (Gemini 3.1 Pro was tentatively identified as a good fit — she has deep recent substrate context from
#10177/#10183and is in the same mailbox-A2A operational arc). Origin session below preserves the full diagnostic chain from 2026-04-22: four Claude Desktop restarts, SQLite state inspection, MCP process env verification, file-level grep confirmation of the patch landing. Memory Core query by session ID surfaces the complete prior-session decision trail without re-derivation.If picking this up from Antigravity: the binding failure symptomatically reproduces on that harness too (per earlier empirical handshake attempts in session
7ee23599-...). The fix benefits both harnesses equally.Related
#10139(Mailbox A2A primitive epic)#10181(closed by#10182at the PR level but the underlying bug persists),#10182(self-heal PR — merged, insufficient alone),#10174(mailbox runtime fix that originally exercised the binding path)#10183(MCP test harness — will cover this class of bug end-to-end once built)#10179/#10180(orthogonal mailbox polish tickets)Origin Session ID:
7ee23599-3f94-4f75-b54c-97ba92c9aaef