LearnNewsExamplesServices
Frontmatter
id12408
titleSurface a healthcheck warning when an env-pinned identity is unbound
stateClosed
labels
enhancementaiarchitecturemodel-experience
assigneesneo-gpt
createdAtJun 3, 2026, 3:37 AM
updatedAtJun 3, 2026, 4:20 AM
githubUrlhttps://github.com/neomjs/neo/issues/12408
authorneo-opus-grace
commentsCount2
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
blockedBy[]
blocking[]
closedAtJun 3, 2026, 4:20 AM

Surface a healthcheck warning when an env-pinned identity is unbound

Closed v13.0.0/archive-v13-0-0-chunk-15 enhancementaiarchitecturemodel-experience
neo-opus-grace
neo-opus-grace commented on Jun 3, 2026, 3:37 AM

Context

Discovered live on 2026-06-03 while bringing up the @neo-opus-grace Claude-sibling identity (epic #11812 / contract #11821 / seed PR #12398). The memory-core MCP server was launched with NEO_AGENT_IDENTITY=neo-opus-grace (env-pinned via the harness claude_desktop_config.json env block + the workspace .env), but the identity failed to bind: four consecutive healthcheck calls returned identity: {source: 'env-var', bound: false, nodeId: null} while the same payload reported status: 'healthy' and details: ['Connected to the orchestrator-managed ChromaDB instance', 'All features are operational'].

Root cause was benign-but-real: the @neo-opus-grace AgentIdentity node was not yet materialized in the live graph (the server's clone had missed a pull of identityRoots.mjs, so the GraphService boot self-seed never created it). The binding code is correctbound:false is the right answer when the node is genuinely absent. The friction was that the healthcheck — the surface agents and operators actually query — gave no signal that an explicitly pinned identity had failed to bind. It took a multi-turn manual investigation (resolver → seeder → graph probe) to reach a conclusion one healthcheck call could have surfaced.

The Problem

HealthService.buildIdentityBlock projects the stdio identity into {source, bound, nodeId} and, by deliberate design, does not flip status — its docstring states "Unbound identity is a valid single-tenant fallthrough ... pure observability, not a health gate." That design is correct and must be preserved: a local human dev with no NEO_AGENT_IDENTITY and an unauthenticated gh is legitimately unbound, and health must stay green.

But there are two qualitatively different unbound states the healthcheck currently renders identically:

  1. Benignsource: 'unresolved' or source: 'gh-cli' with no matching node: local single-tenant dev. Expected. No signal warranted.
  2. Misconfigurationsource: 'env-var' with bound: false: an operator/harness explicitly pinned NEO_AGENT_IDENTITY=<x> that resolves to no AgentIdentity graph node. Almost always a real defect — unseeded new identity, typo'd login, or a stale identityRoots.mjs clone. Yet status stays healthy and details[] says "All features are operational."

A boot-time logger.warn does fire (Server.mjs:310) and logIdentityStatus() logs at startup — so this is not fully silent. But boot logs are out-of-band: an agent self-diagnosing via the healthcheck MCP tool (or an operator reading its JSON) never sees them. The healthcheck is the canonical programmatic diagnostic surface, and it omits the one signal that would have collapsed this debug loop.

The Architectural Reality

  • Identity projection: buildIdentityBlock(stdioIdentityState){source, bound, nodeId}ai/services/memory-core/HealthService.mjs:81-92. Intentionally status-neutral (docstring ~lines 73-76).
  • Payload assembly: the healthcheck payload sets identity: buildIdentityBlock(this.#stdioIdentityState) at HealthService.mjs:1147 and appends operator-facing strings via payload.details.push(...) (HealthService.mjs:1210-1211 — the "Connected..." / "All features are operational" lines).
  • Existing out-of-band boot warn: bindAgentIdentitylogger.warn('... AgentIdentity graph lookup failed for @<login>: Node not found ...')ai/mcp/server/memory-core/Server.mjs:310.
  • Binding itself is correct (awaits getNode; the prior missing-await bug was #10249): Server.mjs:285-316.

The Fix

In the healthcheck payload assembly (around HealthService.mjs:1147-1211), after computing the identity block, add a single conditional observability signal for the misconfiguration case only — identity.source === 'env-var' && identity.bound === false:

  • Push a WARN-shaped entry into details[], e.g.: "WARN: NEO_AGENT_IDENTITY is pinned to '<login>' but resolved to no AgentIdentity graph node (bound:false). Likely unseeded — run ai/scripts/setup/seedAgentIdentities.mjs, or confirm the identity exists in ai/graph/identityRoots.mjs."
  • Optionally expose a structured identity.warning field (string|null) from buildIdentityBlock for programmatic agent consumption (self-repair skill, watchdog) so agents don't have to string-match details[].

Explicitly do not flip status away from healthy — preserve the single-tenant-fallthrough design. This is a surfacing change, not a gating change.

Contract Ledger Matrix

Target Surface Source of Authority Proposed Behavior Fallback Docs Evidence
healthcheck.details[] HealthService payload assembly (HealthService.mjs:1147-1211) append WARN entry when identity.source==='env-var' && !identity.bound no entry for benign unresolved/gh-cli unbound inline JSDoc on buildIdentityBlock live repro this session (4× bound:false under env-pin)
identity.warning (new, optional) buildIdentityBlock (HealthService.mjs:81) string diagnostic when env-pinned+unbound, else null omit / null (additive, back-compat) buildIdentityBlock docstring unit test on projection
status HealthService health-gate contract UNCHANGED — stays healthy n/a (explicitly not gated) docstring HealthService.mjs:73-76 preserves single-tenant design

Decision Record impact

none — aligned-with the existing "surface, don't obscure / unbound ≠ health gate" observability principle documented in buildIdentityBlock. No ADR amended. If a reviewer judges the optional identity.warning field a healthcheck-schema contract change, treat as aligned-with the MemoryCoreMcpAuth observability contract (additive, back-compatible).

Acceptance Criteria

  • AC1: When identity.source === 'env-var' and identity.bound === false, healthcheck.details[] contains a WARN entry naming the pinned login and the remediation (seedAgentIdentities.mjs / identityRoots.mjs).
  • AC2: When unbound via source: 'unresolved' or source: 'gh-cli' (benign single-tenant), no such WARN is emitted.
  • AC3: status remains healthy in all unbound cases (no health-gate regression); existing details[] strings unchanged.
  • AC4: If identity.warning is added, a buildIdentityBlock unit test asserts it is a non-empty string for env-var+unbound and null otherwise.
  • AC5: Unit coverage in test/playwright/unit/ai/services/memory-core/ for the three projection shapes (env-var+unbound→warn, gh-cli+unbound→no warn, bound→no warn).

Out of Scope

  • Fixing why a given identity is unseeded (clone staleness, boot self-seed timing) — that's the seed path (GraphService boot-seed / seedAgentIdentities.mjs), tracked under the #11812 identity bring-up. This ticket only surfaces the state.
  • Flipping status to degraded/unhealthy on unbound identity — explicitly rejected (breaks single-tenant fallthrough).
  • Auto-running the seeder from the healthcheck — observability must not mutate state.
  • Any change to StdioIdentityResolver resolution chain or bindAgentIdentity logic (both correct post-#10249).

Avoided Traps

  • Gate health on binding — rejected: a no-identity local dev session is legitimately unbound; flipping status would false-alarm every single-tenant boot. The signal must distinguish env-pinned (intentional) from unresolved (benign).
  • "It already warns at boot, so no-op" — rejected: the boot logger.warn (Server.mjs:310) is out-of-band; agents/operators self-diagnose via the healthcheck tool JSON, which is exactly where the signal is missing. Empirically this gap cost a multi-turn debug session.
  • Duplicate of #10249 — rejected: #10249 (CLOSED) fixed a missing-await bug that made bound:false spurious. Here bound:false is correct; the ask is to surface the correct-but-pinned-unbound state. Inverse concern, different surface.

Related

  • #10249 (CLOSED) — missing-await bug that made bound:false spurious; fixed via PR #10250 / 27b1dea. This ticket is the observability complement, not a re-open. #10249 also raised a "secondary boot-sequence observability concern" (boot-snapshot ordering), addressed by the #10977 boot reorder — distinct from this healthcheck-surface signal.
  • #11812 / #11821 / PR #12398 — Claude-sibling @neo-opus-grace identity bring-up; the live context that surfaced this gap.
  • ai/scripts/setup/seedAgentIdentities.mjs — the remediation the new WARN should point operators/agents to.

Origin Session ID

Origin Session ID: cee752a1-1297-4b61-a05b-3d0d506f4274 (note: Memory Core session IDs rotated repeatedly during this debug window and the authoring identity was itself mis-bound per the bound:false context above — prefer the semantic Handoff Retrieval Hints below over the session pointer).

Handoff Retrieval Hints

  • Retrieval Hint: "healthcheck identity bound false env-var observability warning"
  • Retrieval Hint: "NEO_AGENT_IDENTITY pinned unbound seedAgentIdentities healthcheck"
  • Retrieval Hint: "neo-opus-grace bring-up identity not seeded bound false 2026-06-03"
  • Commit anchor: dev HEAD 281f44870 at filing.
tobiu referenced in commit b87fd42 - "feat(memory-core): surface env-pinned identity warnings (#12408) (#12409) on Jun 3, 2026, 4:20 AM
tobiu closed this issue on Jun 3, 2026, 4:20 AM