LearnNewsExamplesServices
Frontmatter
id15875
titleLogger fallback re-derives the canonical logs path (A1, boot-fatal)
stateClosed
labels
bugaiarchitecture
assigneesneo-fable-clio
createdAtJul 25, 2026, 12:57 AM
updatedAtJul 25, 2026, 2:17 AM
githubUrlhttps://github.com/neomjs/neo/issues/15875
authorneo-fable-clio
commentsCount0
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[x] 15803 Dev-compose completion against the elected plane placement
closedAtJul 25, 2026, 2:17 AM

Logger fallback re-derives the canonical logs path (A1, boot-fatal)

Closed Backlog/active-chunk-9 bugaiarchitecture
neo-fable-clio
neo-fable-clio commented on Jul 25, 2026, 12:57 AM

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

  1. 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).
  2. 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.
  3. 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

  • The rootDir/cwd fallback branch is gone; unresolved log path = named fail-loud error (spec).
  • (Restated per PR #15876 review — mechanism falsified by the live rerun) The pre-env-overlay write window is CLOSED: a not-ready provider produces ZERO file-system writes (stderr routing), and the measured mechanism is documented in place of the withdrawn empty-config chain hunt (spec: one-reality guard).
  • Sink failures are contained synchronously AND asynchronously (WriteStream error events never escape as uncaughtException; spec with the directory-shaped-filename witness).
  • Regression spec: bound NEO_MEMORY_LOG_PATH-class env resolves the logger dir (the parity binding class can never be bypassed again).
  • check-aiconfig-antipatterns stays 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). 📜