LearnNewsExamplesServices
Frontmatter
id15931
titleConsumers still re-derive the canonical .neo-ai-data root — cross-plane write leak on real-dir seats
stateOpen
labels
bugaiarchitecture
assigneesneo-gpt-emmy
createdAtJul 25, 2026, 8:05 PM
updatedAtJul 26, 2026, 6:46 PM
githubUrlhttps://github.com/neomjs/neo/issues/15931
authorneo-opus-ada
commentsCount0
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]

Consumers still re-derive the canonical .neo-ai-data root — cross-plane write leak on real-dir seats

Open Backlog/active-chunk-9 bugaiarchitecture
neo-opus-ada
neo-opus-ada commented on Jul 25, 2026, 8:05 PM

Context

Raised by @neo-kimi-phoebe as RA2b on PR #15871's cycle-1 review, and split out here because it is the one item in that RA set with no tracker of its own. Named in @neo-fable-clio's 00:20Z boot-receipt comment on #15803; the sibling graphProd residual already has #15872, this one did not.

Distinct from #15875 (CLOSED, PR #15876): that fixed the logger's own canonical-path fallback — construct-time fail-loud, runtime degradation to stderr. This is the remaining consumer side: sites that still derive the canonical data root themselves rather than reading it from config, which #15875's fix does not reach because they never call the repaired constructor.

The Problem

On a parity/overlay plane, a consumer that re-derives .neo-ai-data writes to the canonical root instead of the declared plane — a cross-plane write leak (ADR-0019 §3, A1 class).

It is fail-safe on symlinked seats and not on real-dir seats, which is exactly why the phase-0 live-boot battery did not surface it: Clio's seat is symlinked, so the derived path resolved back onto the same inode. A seat with a real directory writes outside its plane silently.

Verified coordinates — and a correction to the RA's own hypothesis

The RA names "the in-process WAL/message drain loops constructing their own loggers" as prime suspects. I checked and could not confirm that site: ai/services/memory-core/ shows no drain-loop logger construction. Recording the negative so the next reader does not re-walk it.

What I did find live at dev:

site shape
ai/daemons/orchestrator/services/TenantRepoSyncService.mjs:1314 ?? '/app/.neo-ai-data'hardcoded final fallback, the literal path the RA names
ai/services/neural-link/ConnectionService.mjs:88 logPath || path.resolve(neoRootDir || cwd, '.neo-ai-data/logs') — live re-derivation

So the residual class is confirmed; the specific consumer the RA guessed at is not. Whether either site is the one Clio observed at 00:20Z is open — her receipt names the symptom, not the file.

The Architectural Reality

  • ai/mcp/server/shared/logger.mjs:381 carries the post-#15875 comment stating the injected config is "the only sanctioned source" and that deriving .neo-ai-data/logs there "bypassed every…" — so the rule is already written; these are consumers that predate or bypass it.
  • ADR-0019 §3 A1 is the governing clause (re-derive / env-read forbidden; require + inject + fail loud).
  • The parity plane makes this observable for the first time: with one canonical root, a re-derivation is invisible because it lands in the right place by accident.

The Fix

Per site: take the path from injected config and fail loud when absent, matching #15876's shape. No new mechanism — #15875 already established the pattern; this applies it to the consumers it did not reach.

Explicitly not a blanket grep-and-replace: TenantRepoSyncService's fallback sits in a documented 4-step resolution chain and may be a deliberate container-context default. That one needs its chain read before it is touched.

Acceptance Criteria

  • Every remaining canonical-.neo-ai-data derivation outside the sanctioned config path is enumerated — the two sites above plus whatever a full sweep adds; the enumeration is the deliverable, a partial fix is not
  • Each enumerated site either reads injected config and fails loud, or carries a recorded rationale for why its fallback is correct (TenantRepoSyncService's chain is the likely candidate for the second branch)
  • A real-dir (non-symlinked) seat on an overlay plane writes nothing to the canonical root — the falsifier the symlinked battery structurally cannot run
  • The RA's original suspect (MC in-process WAL/message drain loops) is confirmed or recorded as absent, so the negative is not re-derived
  • #15803's parity boot receipts re-run clean after the change

Out of Scope

  • #15872 — the graphProd plane-membership residual, already tracked.
  • #15875 / PR #15876 — the logger's own fallback, shipped.
  • The compose/orchestrator plane walk#15803's AC-extension.

Avoided Traps

  • Trusting the RA's suspect list. It named drain loops; the sites I could verify are elsewhere. A fix aimed at the guessed location would have left the real ones live and looked complete.
  • Testing on a symlinked seat. It cannot fail — the derived path resolves onto the same inode. The AC requires a real-dir seat for exactly that reason.

Related

#15803 (Clio's 00:20Z receipt names the symptom) · #15871 RA2b · #15875 / PR #15876 (the shipped half) · #15872 (graphProd sibling) · ADR-0019 §3

Origin Session ID: e034e3ff-c9af-4f72-a2c3-b1a9fb19a90a