---
id: 10176
title: Mailbox identity observability + Claude Desktop onboarding docs
state: CLOSED
labels:
  - documentation
  - enhancement
  - ai
  - architecture
assignees:
  - neo-opus-ada
createdAt: '2026-04-22T14:06:14Z'
updatedAt: '2026-04-23T13:58:27Z'
githubUrl: 'https://github.com/neomjs/neo/issues/10176'
author: neo-opus-ada
commentsCount: 0
parentIssue: 10139
subIssues: []
subIssuesCompleted: 0
subIssuesTotal: 0
blockedBy: []
blocking: []
closedAt: '2026-04-23T13:56:16Z'
---
# Mailbox identity observability + Claude Desktop onboarding docs

# Mailbox identity observability + Claude Desktop onboarding docs

### Context

Two friction points surfaced during the 2026-04-22 A2A handshake bootstrapping session that neither block Phase 4 work (unlike #10174) nor fit cleanly as subs of existing tickets, but materially improve the next agent's experience. Aggregated here to trade review throughput for speed of delivery.

### The Problem

**Friction 1 — Silent identity-binding failures.** `healthcheck` currently returns `{status: 'healthy', ...}` even when `StdioIdentityResolver` bound no agent identity AND the AgentIdentity graph node lookup failed. The mailbox and permission subsystems then throw `"Cannot <op>: no agent identity context bound"` on every call. Diagnosing the chain today requires reading `StdioIdentityResolver.mjs` → running `ps -E | grep NEO_AGENT_IDENTITY` → querying `seedAgentIdentities.mjs` state manually. One MCP tool call should surface all of it.

**Friction 2 — Missing Claude Desktop setup docs.** `.github/AI_QUICK_START.md` currently documents Option A (Gemini CLI) and Option B (Antigravity OS). The Claude Desktop harness — which now embeds Claude Code — is absent. Its MCP config lives at `~/Library/Application Support/Claude/claude_desktop_config.json`, a path that is neither obvious nor documented anywhere in the repo. The empirical bootstrap also exposed two more gotchas worth capturing: `NEO_AGENT_IDENTITY` must live in the per-server `env` block (not shell export), and for multi-harness dev boxes the `.neo-ai-data` folder must be unified across checkouts (symlink is the current tactical convention).

### The Architectural Reality

**Friction 1:**
- `ai/mcp/server/memory-core/Server.mjs` (lines 226-238, `logIdentityStatus`) — already logs the identity state to stdout at boot. Surface the same shape in the healthcheck tool response.
- `ai/mcp/server/memory-core/Server.mjs` (`healthcheckHandler` — locate) — add an `identity` block alongside `session`, `database`, `features`. Non-fatal: unbound identity must NOT flip `status: 'healthy' → 'unhealthy'`. The mailbox is an optional feature; unbound is a valid single-tenant mode.

**Friction 2:**
- `.github/AI_QUICK_START.md` §4 "Choosing Your Agent Environment" — add Option C: Claude Desktop (Claude Code embedded).
- `.github/AI_QUICK_START.md` §5 "Understanding the Configuration Files" — add "Core Configuration (Claude Desktop)" subsection with the `claude_desktop_config.json` shape including the `NEO_AGENT_IDENTITY` env slot per MCP server.
- `.github/AI_QUICK_START.md` — add a short section on the shared `.neo-ai-data` convention for multi-harness dev boxes (symlink pattern from antigravity → github checkout, rationale: unified SQLite graph substrate).

### The Fix

1. `healthcheck` tool response gains `identity: {source, bound, nodeId}` block.
   - `source`: `'env-var' | 'gh-cli' | 'oidc' | 'unresolved'`
   - `bound`: boolean — `true` iff the resolved login matched a seeded AgentIdentity graph node
   - `nodeId`: the bound `@login` node ID, or `null`
   - `status` stays `'healthy'` regardless of `bound` — this is observability, not a gate

2. `AI_QUICK_START.md` updated:
   - §4: Option C Claude Desktop added alongside Gemini CLI / Antigravity
   - §5: New "Core Configuration (Claude Desktop)" subsection with the `claude_desktop_config.json` shape example (memory-core + the other three Neo MCP servers, with `NEO_AGENT_IDENTITY` shown in the memory-core env)
   - New subsection: "Multi-harness dev: unifying `.neo-ai-data`" — documents the symlink convention (`ln -s <primary>/.neo-ai-data <secondary>/.neo-ai-data`), the rationale (shared SQLite graph → A2A mailbox works across harnesses), and the `better-sqlite3` WAL-mode concurrency note (safe for multiple reader + serialized writer processes)
   - Restart gotcha noted once: MCP config changes require a FULL harness quit (⌘Q on macOS), not just a window close

### Acceptance Criteria

- [ ] `healthcheck` response includes `identity: {source, bound, nodeId}`
- [ ] Response schema validation test pins the new shape
- [ ] Unbound-identity case: `identity.bound === false`, `identity.nodeId === null`, `status === 'healthy'` (backward compatible)
- [ ] Bound-identity case (env-var source): `identity.source === 'env-var'`, `identity.bound === true`, `identity.nodeId === '@<login>'`
- [ ] gh-cli fallback source path tested
- [ ] `AI_QUICK_START.md` §4 lists Claude Desktop as Option C
- [ ] `AI_QUICK_START.md` §5 includes `claude_desktop_config.json` example with `NEO_AGENT_IDENTITY` shown in the memory-core `env` block
- [ ] `AI_QUICK_START.md` documents the `.neo-ai-data` symlink convention
- [ ] Restart-gotcha (⌘Q, not window close) noted once in the setup flow
- [ ] `learn/agentos/tooling/MemoryCoreMcpAuth.md` cross-references the new healthcheck shape in its Troubleshooting section (replacing the "Check startup logs for `[neo-memory-core MCP] Identity: ...`" guidance with "Call `healthcheck` and inspect `identity.*`")

### Out of Scope

- Auto-seeding identity nodes at memory-core boot (security concern: would mask misconfiguration; agents should fail loud on first mailbox op instead)
- Health-state degradation based on `identity.bound`
- OIDC-source identity state (SSE transport) — tested path exists per `#10000`; this ticket adds the stdio observability that matches
- Documentation for the Antigravity-side mirror of `.neo-ai-data` — the current symlink convention is sufficient tactical guidance; architectural unification belongs to `#9999`

### Avoided Traps

- **"Make `healthcheck` return `status: unhealthy` when unbound."** Rejected — mailbox is an optional feature; single-tenant stdio-mode without a bound identity is a valid and supported configuration. Conflating "mailbox accessible" with "healthy" would break all human-dev workflows that have `gh` authenticated but no `NEO_AGENT_IDENTITY` pin.
- **"Document claude_desktop_config.json in the `.claude/` directory so it sits next to the config it shadows."** Rejected — `.claude/settings.json` is a repo-tracked Claude Code convention file that doesn't own MCP servers on macOS Claude Desktop. Mixing the documentation across both locations introduces the exact ambiguity that cost this session two restarts to diagnose. `AI_QUICK_START.md` is the single source of truth for all harness setup; Claude Desktop belongs there alongside Gemini CLI and Antigravity.
- **"File the symlink convention as a separate ticket."** Rejected — the convention only exists because of the mailbox bootstrap pain in this same session, and its docs-home is identical. Aggregated for throughput.

### Related

- Parent: `#10139` Mailbox A2A primitive (epic; both concerns emerged from mailbox bootstrapping UX)
- Builds on: `#10145` (identity substrate that this observability surfaces), `#10144` (AgentIdentity seed convention)
- Sibling: `#10174` (mailbox runtime-correctness fix; this ticket is the observability/docs dual)
- Potentially supersedes informal troubleshooting in `learn/agentos/tooling/MemoryCoreMcpAuth.md` §Troubleshooting

Origin Session ID: `d907e342-28fc-4eb6-8c51-355772dbfb44`

## Timeline

- 2026-04-22T14:06:16Z @neo-opus-ada added the `documentation` label
- 2026-04-22T14:06:16Z @neo-opus-ada added the `enhancement` label
- 2026-04-22T14:06:16Z @neo-opus-ada added the `ai` label
- 2026-04-22T14:06:17Z @neo-opus-ada added the `architecture` label
- 2026-04-22T14:06:45Z @neo-opus-ada added parent issue #10139
- 2026-04-22T14:24:46Z @neo-opus-ada cross-referenced by PR #10178
- 2026-04-22T15:54:45Z @neo-opus-ada cross-referenced by #10181
- 2026-04-23T09:34:25Z @neo-opus-ada cross-referenced by #10224
- 2026-04-23T09:41:19Z @neo-opus-ada cross-referenced by PR #10225
- 2026-04-23T12:02:13Z @neo-opus-ada cross-referenced by #10232
- 2026-04-23T12:45:20Z @neo-opus-ada cross-referenced by PR #10236
- 2026-04-23T13:18:44Z @neo-opus-ada assigned to @neo-opus-ada
- 2026-04-23T13:26:07Z @neo-opus-ada cross-referenced by PR #10239
- 2026-04-23T13:27:00Z @neo-opus-ada cross-referenced by #10240
- 2026-04-23T13:56:17Z @tobiu referenced in commit `057130b` - "feat(memory-core): healthcheck identity observability block (#10176) (#10239)

Adds `identity: {source, bound, nodeId}` to the MCP healthcheck response so the
diagnostic chain for "identity unbound" failures collapses from 8+ tool calls
(ps + lsof + env inspection + graph queries + startup-log grep) to a single
`healthcheck` invocation.

Empirical motivation: four sessions across two days (0327771f, 15852d91,
8968b9f6, 24aa1fa1) paid the multi-tool-call diagnostic tax running down the
same root-cause chain. Post-#10232 self-seed + post-#10190 cache coherence,
the remaining class of "why is identity unbound" failures lives UPSTREAM of
the graph substrate (env-var not reaching spawn, gh-CLI timeout, resolver
chain failure). The healthcheck field surfaces this directly:

  identity.source: 'env-var' | 'gh-cli' | 'oidc' | 'unresolved'
  identity.bound:  boolean — graph-node match present
  identity.nodeId: '@<login>' | null

Status stays 'healthy' regardless of bound — unbound identity is a valid
single-tenant fallthrough per the MemoryCoreMcpAuth contract. This is pure
observability, not a health gate. Aligns with "surface, don't obscure"
discipline from PR #10227.

Implementation:
- HealthService.mjs gains a module-scope `buildIdentityBlock(state)` pure
  function (exported for testability) + private #stdioIdentityState field +
  public setStdioIdentityState() setter. #performHealthCheck payload picks
  up the identity block via buildIdentityBlock().
- Server.mjs calls HealthService.setStdioIdentityState(this.stdioIdentity)
  after resolveStdioIdentity() completes in the stdio boot path. SSE
  transport is out of scope (per-request OIDC identity is orthogonal to
  process-level stdio identity).
- openapi.yaml HealthCheckResponse schema extended with the identity block
  including enum constraint on source + nullable nodeId.

Test coverage: HealthService.spec.mjs exercises six projection cases against
buildIdentityBlock — null state, explicit unresolved, env-var bound, gh-cli
bound, resolved-but-unbound (the diagnostic shape this PR exists to surface),
and missing-source defense-in-depth. 6/6 green.

Troubleshooting guide updated: MemoryCoreMcpAuth.md replaces the startup-log-
grep guidance with the healthcheck-as-primary-diagnostic flow, retaining the
log line as a documented fallback for log-only workflows.

Out of scope for this PR (deferred to fast-follow ticket): AI_QUICK_START.md
Claude Desktop onboarding section + .neo-ai-data symlink convention docs
(ticket #10176 ACs that belong with operator-onboarding, not the
substrate-observability substrate shipped here).

Co-authored-by: tobiu <tobiasuhlig78@gmail.com>"
- 2026-04-23T13:56:17Z @tobiu closed this issue
- 2026-04-23T14:16:56Z @neo-opus-ada cross-referenced by #10241
- 2026-04-23T15:15:17Z @neo-opus-ada cross-referenced by PR #10242
- 2026-04-23T18:01:59Z @neo-opus-ada cross-referenced by PR #10254
- 2026-04-23T22:46:18Z @neo-opus-ada cross-referenced by PR #10268
- 2026-04-23T23:32:12Z @tobiu referenced in commit `f70c48f` - "feat(memory-core): surface effective chromadb topology in /health (#10127) (#10268)

Extends HealthService.healthcheck() with a database.topology observability
block exposing mode (unified|federated), coordinates ({host, port}), and
resolvedVia (the config key path the resolver read). Closes the in-band
observability gap for sub-epic #10015: operators can now verify
NEO_CHROMA_UNIFIED=true took effect from a single healthcheck call,
instead of inspecting logs or re-running config through node -e.

- Add module-scope buildTopologyBlock(cfg) as a pure projection, mirroring
  the buildIdentityBlock precedent from #10176. Delegates coordinate
  resolution to ChromaManager.resolveChromaCoordinates (extracted in #10001
  specifically for this kind of aggregation) and computes resolvedVia
  from chromaUnified. Defensive try/catch keeps healthcheck resilient when
  the resolver throws on missing engines.kb.chroma — surfaces misconfig as
  observable data (coordinates: null, error: <message>), per PR #10227's
  surface-don't-obscure principle.
- Wire buildTopologyBlock(aiConfig) into #performHealthCheck's payload
  under database.topology, before connectivity checks so operators still
  see target coordinates when status is unhealthy.
- Extend HealthService.spec.mjs with three topology tests covering
  federated mode, unified mode, and unified-with-missing-kb-chroma misconfig.
- Document the full healthcheck response shape in learn/agentos/MemoryCore.md,
  folding in the deferred doc pass from #10001 + #10007 and covering the
  #10176 identity and #10017 migration observability blocks.

Resolves #10127

Co-authored-by: tobiu <tobiasuhlig78@gmail.com>"
- 2026-04-27T16:05:28Z @neo-opus-ada cross-referenced by #10432
- 2026-05-05T18:17:10Z @neo-opus-ada cross-referenced by PR #10767
- 2026-05-05T18:21:36Z @neo-opus-ada cross-referenced by PR #10768
- 2026-05-05T18:26:17Z @neo-opus-ada cross-referenced by #10770
- 2026-05-05T18:43:19Z @neo-opus-ada cross-referenced by #10773
- 2026-05-05T18:55:38Z @neo-opus-ada referenced in commit `fb1d33d` - "feat(memory-core): surface active embedding provider in healthcheck (#10723)

Adds buildEmbeddingProviderBlock pure projection to HealthService.mjs, wired
into the healthcheck payload as a new top-level providers.embedding field.
Mirrors the buildIdentityBlock + buildTopologyBlock module-scope pure
projection pattern from #10176 / #10127, enabling pure unit-test coverage
without bootstrapping Memory Core runtime singletons.

Reports {active, host, model, dimensions} for the chromaEmbeddingProvider
in effect at boot:
- gemini (cloud): null host, embeddingModel from aiConfig.embeddingModel
- openAiCompatible (local): host + embeddingModel from aiConfig.openAiCompatible
- ollama (local): host + embeddingModel from aiConfig.ollama
- unrecognized: surfaces error field naming the misconfig directly

Operator config path documented in learn/agentos/SharedDeployment.md
"Embedding provider" subsection: env vars NEO_CHROMA_EMBEDDING_PROVIDER,
NEO_OPENAI_COMPATIBLE_HOST, NEO_OPENAI_COMPATIBLE_EMBEDDING_MODEL,
NEO_VECTOR_DIMENSION (load-bearing — must match the active model's actual
output dimensions; mismatch is silent in collection writes but breaks
retrieval).

Substrate observation flagged in SharedDeployment.md: the OAI-compatible
embedding path is implemented inside TextEmbeddingService.embedText[s] (raw
fetch to /v1/embeddings), NOT routed through the OpenAiCompatible provider
class which exposes only generate / stream (chat completions). Provider
abstraction is functional but asymmetric; future hardening should
consolidate. Out of scope for #10723.

6 unit tests cover the projection across all 3 supported providers, the
unrecognized-provider error surface, defensive missing-config handling,
and dimension preservation invariance. All pass at HEAD."
- 2026-05-05T19:00:23Z @tobiu referenced in commit `24fa125` - "feat(memory-core): surface active embedding provider in healthcheck (#10723) (#10767)

Adds buildEmbeddingProviderBlock pure projection to HealthService.mjs, wired
into the healthcheck payload as a new top-level providers.embedding field.
Mirrors the buildIdentityBlock + buildTopologyBlock module-scope pure
projection pattern from #10176 / #10127, enabling pure unit-test coverage
without bootstrapping Memory Core runtime singletons.

Reports {active, host, model, dimensions} for the chromaEmbeddingProvider
in effect at boot:
- gemini (cloud): null host, embeddingModel from aiConfig.embeddingModel
- openAiCompatible (local): host + embeddingModel from aiConfig.openAiCompatible
- ollama (local): host + embeddingModel from aiConfig.ollama
- unrecognized: surfaces error field naming the misconfig directly

Operator config path documented in learn/agentos/SharedDeployment.md
"Embedding provider" subsection: env vars NEO_CHROMA_EMBEDDING_PROVIDER,
NEO_OPENAI_COMPATIBLE_HOST, NEO_OPENAI_COMPATIBLE_EMBEDDING_MODEL,
NEO_VECTOR_DIMENSION (load-bearing — must match the active model's actual
output dimensions; mismatch is silent in collection writes but breaks
retrieval).

Substrate observation flagged in SharedDeployment.md: the OAI-compatible
embedding path is implemented inside TextEmbeddingService.embedText[s] (raw
fetch to /v1/embeddings), NOT routed through the OpenAiCompatible provider
class which exposes only generate / stream (chat completions). Provider
abstraction is functional but asymmetric; future hardening should
consolidate. Out of scope for #10723.

6 unit tests cover the projection across all 3 supported providers, the
unrecognized-provider error surface, defensive missing-config handling,
and dimension preservation invariance. All pass at HEAD.

Co-authored-by: tobiu <tobiasuhlig78@gmail.com>"
- 2026-05-05T19:30:41Z @neo-opus-ada cross-referenced by #10779
- 2026-05-05T20:00:36Z @neo-opus-ada cross-referenced by #10783
- 2026-05-06T07:16:45Z @neo-opus-ada cross-referenced by PR #10798
- 2026-05-06T08:57:55Z @neo-opus-ada cross-referenced by PR #10806
- 2026-05-07T21:12:36Z @neo-opus-ada cross-referenced by PR #10930

