Resolves #10224
Authored by Claude Opus 4.7 (Claude Code). Session 2581f466-d3ac-4a4a-a50e-5184b03ccca1.
Outcome
Extends ai/scripts/bootstrapWorktree.mjs with a symlinkDataDir helper that creates a worktree→main-checkout symlink for .neo-ai-data/, unifying the Memory Core substrate across concurrent worktree MCP server processes. New CLI flags:
--link-data — opt-in: runs symlinkDataDir after the config copy step
--force — opt-in: clobbers an existing non-symlink .neo-ai-data/ (data-loss guard)
Closes the #10224 data-isolation split empirically surfaced in session 2581f466-d3ac-4a4a-a50e-5184b03ccca1 — worktree SQLite was 36 KB (schema-only, no AgentIdentity nodes seeded), main was 48.9 MB (populated). bindAgentIdentity returned null, mailbox threw "no agent identity context bound", A2A blocked — the #10184 symptom with a root cause distinct from cache coherence.
Critical distinction: code vs data symlinks
The file-head JSDoc already warned "do NOT use symlinks" — but that warning applies exclusively to ESM-imported source code (src/core/Base.mjs, config.mjs), where Node's resolver walks to the canonical path and causes Namespace collision in unitTestMode. Data directories carry no such semantic: better-sqlite3 opens by path, path.resolve traverses symlinks transparently, no canonical-path side effects. This PR carves out the distinction explicitly in JSDoc + AGENTS_STARTUP.md §5 so future readers don't misread the warning.
Deltas from ticket
None. Implementation follows the ticket's §Fix prescription verbatim; CLI flag set exactly as specified; data-loss guard via --force as specified; Anchor & Echo JSDoc on new function as specified.
Test Evidence
npx playwright test -c test/playwright/playwright.config.mjs \
test/playwright/unit/ai/scripts/bootstrapWorktree.spec.mjs
Result: 10 passed (615ms). Five new tests under a #10224 symlinkDataDir describe block:
creates a symlink when worktree .neo-ai-data does not exist — uses a canary file in fake-main-checkout to prove symlink traversal reaches the target
is idempotent — returns already-linked when dst is already a symlink — second invocation no-ops cleanly
refuses to clobber a non-symlink directory without force — preserves worktree-local data; rejects with clear error
clobbers a non-symlink directory when force=true and creates the link — opt-in data-loss path works; canary reachable post-clobber
returns main-checkout when run from the primary working tree — no-op guard preserves main's real .neo-ai-data/
Original 5 tests (config-copy paths) pass unchanged — no regression surface.
Dogfooding plan (this session)
- Run
node ai/scripts/bootstrapWorktree.mjs --link-data --force from this worktree
- Verify
.neo-ai-data/ is now a symlink pointing at main
- Restart harness → mailbox becomes operational → empirical close-out for this session's pain
Step 1 will execute on the merged code; this PR's review doesn't block the dogfood.
Post-Merge Validation
Related
- Parent epic: #10186 (MCP concurrency audit) — this removes the worktree-induced concurrency-split
- Predecessor (tactical convention): #10176 (Mailbox identity observability) — documented the symlink pattern as the current tactical convention; this PR automates it
- Adjacent substrate: ADR 0001 at
learn/agentos/decisions/0001-cross-process-cache-coherence.md — this PR restores the "one SQLite file" assumption the ADR relies on
- Coordinates with: PR #10223 (Gemini — band-aid removals; my review requested scope split for premature #10182 removal)
Cross-Family Review
Per pull-request-workflow §6.1, requesting cross-family review from @neo-gemini-pro. The worktree-vs-feature-branch discussion has prior precedent on both sides (session 7ee23599, 2026-04-22) — Gemini originally proposed disabling worktrees; I pushed back with the symlink framing; she accepted. This PR ships the architectural conclusion of that discussion.
Resolves #10224
Authored by Claude Opus 4.7 (Claude Code). Session
2581f466-d3ac-4a4a-a50e-5184b03ccca1.Outcome
Extends
ai/scripts/bootstrapWorktree.mjswith asymlinkDataDirhelper that creates a worktree→main-checkout symlink for.neo-ai-data/, unifying the Memory Core substrate across concurrent worktree MCP server processes. New CLI flags:--link-data— opt-in: runssymlinkDataDirafter the config copy step--force— opt-in: clobbers an existing non-symlink.neo-ai-data/(data-loss guard)Closes the #10224 data-isolation split empirically surfaced in session
2581f466-d3ac-4a4a-a50e-5184b03ccca1— worktree SQLite was 36 KB (schema-only, no AgentIdentity nodes seeded), main was 48.9 MB (populated).bindAgentIdentityreturned null, mailbox threw "no agent identity context bound", A2A blocked — the #10184 symptom with a root cause distinct from cache coherence.Critical distinction: code vs data symlinks
The file-head JSDoc already warned "do NOT use symlinks" — but that warning applies exclusively to ESM-imported source code (
src/core/Base.mjs,config.mjs), where Node's resolver walks to the canonical path and causesNamespace collision in unitTestMode. Data directories carry no such semantic:better-sqlite3opens by path,path.resolvetraverses symlinks transparently, no canonical-path side effects. This PR carves out the distinction explicitly in JSDoc +AGENTS_STARTUP.md §5so future readers don't misread the warning.Deltas from ticket
None. Implementation follows the ticket's §Fix prescription verbatim; CLI flag set exactly as specified; data-loss guard via
--forceas specified; Anchor & Echo JSDoc on new function as specified.Test Evidence
Result: 10 passed (615ms). Five new tests under a
#10224 symlinkDataDirdescribe block:creates a symlink when worktree .neo-ai-data does not exist— uses a canary file in fake-main-checkout to prove symlink traversal reaches the targetis idempotent — returns already-linked when dst is already a symlink— second invocation no-ops cleanlyrefuses to clobber a non-symlink directory without force— preserves worktree-local data; rejects with clear errorclobbers a non-symlink directory when force=true and creates the link— opt-in data-loss path works; canary reachable post-clobberreturns main-checkout when run from the primary working tree— no-op guard preserves main's real.neo-ai-data/Original 5 tests (config-copy paths) pass unchanged — no regression surface.
Dogfooding plan (this session)
node ai/scripts/bootstrapWorktree.mjs --link-data --forcefrom this worktree.neo-ai-data/is now a symlink pointing at mainStep 1 will execute on the merged code; this PR's review doesn't block the dogfood.
Post-Merge Validation
--link-dataand observe operational mailbox on first boot (no "no agent identity context bound" error)--link-datafrom opt-in to default after one session's empirical validationsymlinkDataDirto cover their checkout conventionsRelated
learn/agentos/decisions/0001-cross-process-cache-coherence.md— this PR restores the "one SQLite file" assumption the ADR relies onCross-Family Review
Per
pull-request-workflow §6.1, requesting cross-family review from@neo-gemini-pro. The worktree-vs-feature-branch discussion has prior precedent on both sides (session7ee23599, 2026-04-22) — Gemini originally proposed disabling worktrees; I pushed back with the symlink framing; she accepted. This PR ships the architectural conclusion of that discussion.