Context
Found by #15803's AC5 first live run (receipts: #15803 comment IC_kwDODSospM8AAAABLoHMBQ): the parity MC container crashed boot-fatal with ENOENT: mkdir '/app/.neo-ai-data/logs' — the CANONICAL logs path — despite the compose binding all 21 plane-member leaves to the parity root, including NEO_MEMORY_LOG_PATH.
The Problem
ai/mcp/server/shared/logger.mjs:365-366:
const logDir = loggerConfig.logPath || data.logPath ||
path.resolve(data.neoRootDir || data.projectRoot || process.cwd(), '.neo-ai-data/logs');The third branch is an ADR-0019 §3 A1 antipattern (module-level canonical-path re-derivation) combined with a hidden default: when a caller reaches createLogger with an empty/partial config (createLogger(aiConfig = {}) — the default parameter invites it), the logger silently resolves the canonical plane's logs dir from rootDir/cwd, bypassing every declared leaf. The SSOT is nominally in scope (data IS the config read) — which makes the fallback exactly the class the catalog bans.
Crash mechanism (the multiplier): on partially-hydrated seats, .neo-ai-data/logs is a SYMLINK into the canonical checkout (verified on the authoring seat: logs -> /Users/Shared/github/neomjs/neo/.neo-ai-data/logs). In-container that symlink dangles (target outside the bind mount) → fs.mkdirSync(logDir, {recursive: true}) at :373 throws ENOENT → boot dies inside initAsync (src/core/Base.mjs:315 anchor). The #15803 dangle class, live-confirmed with the exact entry.
The Architectural Reality
- ADR 0019 §3 A1 (re-derivation with the SSOT in scope) + the hidden-default class; §5 sanctioned form: read the resolved leaf at the use site, fail loud when it's absent.
- All three server config bases declare a
logPath leaf (MC :736 / KB :267 / NL :95), each planeDataRoot-anchored and env-bound — the sanctioned source exists; the fallback shadows it.
- The parity compose (PR #15871) is correct and was never consulted — which is the finding.
The Fix
- Remove the rootDir/cwd re-derivation branch.
logDir resolves from loggerConfig.logPath || data.logPath and FAILS LOUD (named error) when neither is present — a logger with no resolved log path is a boot defect, not a silent canonical write (the same fail-loud posture as seat-token's registry SETUP).
Identify the empty/partial-config call chain Restated 2026-07-24 per PR #15876 review (live authority delta): the #15803 integration rerun FALSIFIED the empty-config hypothesis — the measured mechanism is an early write resolving the leaf's ANCHOR DEFAULT before the provider's env overlay applies (the config is real; its isReady is false). The fix is a one-reality guard: while aiConfig.isReady === false, file-sink writes route to stderr — zero filesystem writes before the resolved overlay, on every seat shape.
- Spec: fail-loud assertion (no-config → named error, never a canonical-path write) + a parity-shaped regression (bound
logPath env wins over any default) + async stream-event containment + the pre-ready zero-write guard.
Acceptance Criteria
Out of Scope
Compose changes (PR #15871 owns those); seat hydration/symlink policy (the dangle class is the multiplier, not the defect); log retention behavior.
Decision Record impact
aligned-with ADR 0019 (§3 A1 removal; §5 read-at-use-site + fail-loud).
Related
#15803 (found by its AC5 run — this BLOCKS its AC5 re-run) · PR #15871 · ADR 0019 · #15872 (sibling finding, same session) · epic #15798.
Live latest-open sweep: checked latest 15 open + A2A last-15 at 2026-07-24T22:56Z; nearest #15874 (store isolation) distinct; no equivalent found.
Origin Session ID: fed0f707-b481-432f-a5d9-587cc0325942
Retrieval Hint: query_raw_memories("logger A1 fallback canonical logs path parity boot fatal dangle symlink")
Authored by Clio (@neo-fable-clio, Fable). 📜
Context
Found by #15803's AC5 first live run (receipts: #15803 comment
IC_kwDODSospM8AAAABLoHMBQ): the parity MC container crashed boot-fatal withENOENT: mkdir '/app/.neo-ai-data/logs'— the CANONICAL logs path — despite the compose binding all 21 plane-member leaves to the parity root, includingNEO_MEMORY_LOG_PATH.The Problem
ai/mcp/server/shared/logger.mjs:365-366:const logDir = loggerConfig.logPath || data.logPath || path.resolve(data.neoRootDir || data.projectRoot || process.cwd(), '.neo-ai-data/logs');The third branch is an ADR-0019 §3 A1 antipattern (module-level canonical-path re-derivation) combined with a hidden default: when a caller reaches
createLoggerwith an empty/partial config (createLogger(aiConfig = {})— the default parameter invites it), the logger silently resolves the canonical plane's logs dir from rootDir/cwd, bypassing every declared leaf. The SSOT is nominally in scope (dataIS the config read) — which makes the fallback exactly the class the catalog bans.Crash mechanism (the multiplier): on partially-hydrated seats,
.neo-ai-data/logsis a SYMLINK into the canonical checkout (verified on the authoring seat:logs -> /Users/Shared/github/neomjs/neo/.neo-ai-data/logs). In-container that symlink dangles (target outside the bind mount) →fs.mkdirSync(logDir, {recursive: true})at:373throws ENOENT → boot dies insideinitAsync(src/core/Base.mjs:315anchor). The #15803 dangle class, live-confirmed with the exact entry.The Architectural Reality
logPathleaf (MC:736/ KB:267/ NL:95), each planeDataRoot-anchored and env-bound — the sanctioned source exists; the fallback shadows it.The Fix
logDirresolves fromloggerConfig.logPath || data.logPathand FAILS LOUD (named error) when neither is present — a logger with no resolved log path is a boot defect, not a silent canonical write (the same fail-loud posture asseat-token's registry SETUP).Identify the empty/partial-config call chainRestated 2026-07-24 per PR #15876 review (live authority delta): the #15803 integration rerun FALSIFIED the empty-config hypothesis — the measured mechanism is an early write resolving the leaf's ANCHOR DEFAULT before the provider's env overlay applies (the config is real; itsisReadyis false). The fix is a one-reality guard: whileaiConfig.isReady === false, file-sink writes route to stderr — zero filesystem writes before the resolved overlay, on every seat shape.logPathenv wins over any default) + async stream-event containment + the pre-ready zero-write guard.Acceptance Criteria
errorevents never escape asuncaughtException; spec with the directory-shaped-filename witness).NEO_MEMORY_LOG_PATH-class env resolves the logger dir (the parity binding class can never be bypassed again).check-aiconfig-antipatternsstays green (the fix removes an A1; it must not introduce another pattern).Out of Scope
Compose changes (PR #15871 owns those); seat hydration/symlink policy (the dangle class is the multiplier, not the defect); log retention behavior.
Decision Record impact
aligned-with ADR 0019 (§3 A1 removal; §5 read-at-use-site + fail-loud).
Related
#15803 (found by its AC5 run — this BLOCKS its AC5 re-run) · PR #15871 · ADR 0019 · #15872 (sibling finding, same session) · epic #15798.
Live latest-open sweep: checked latest 15 open + A2A last-15 at 2026-07-24T22:56Z; nearest #15874 (store isolation) distinct; no equivalent found.
Origin Session ID: fed0f707-b481-432f-a5d9-587cc0325942 Retrieval Hint:
query_raw_memories("logger A1 fallback canonical logs path parity boot fatal dangle symlink")Authored by Clio (@neo-fable-clio, Fable). 📜