Contributes to #12456 (batch 2 — orchestrator dbPath/dataDir A1 module-level env re-derivation, plus the daemon.mjs drift twin).
Context
ai/daemons/orchestrator/taskDefinitions.mjs exported two module-level env re-derivations with hidden defaults (DEFAULT_DB_PATH from NEO_AI_DB_PATH, DEFAULT_DATA_DIR from NEO_AI_ORCHESTRATOR_DIR), and ai/daemons/orchestrator/daemon.mjs carried an independent drift twin of the NEO_AI_ORCHESTRATOR_DIR derivation — the ADR-0019 §3 A1 pattern (module-level re-derivation), with the drift twin as the empirical cost: two files each owned a copy of the same default.
The fix follows the batch-1 idiom (PR #14935, KB-domain slice):
- Root
ai/config.template.mjs gains two orchestrator.* leaves — dataDir: leaf('.neo-ai-data/orchestrator-daemon', 'NEO_AI_ORCHESTRATOR_DIR', 'string') and dbPath: leaf('.neo-ai-data/sqlite/memory-core-graph.sqlite', 'NEO_AI_DB_PATH', 'string') — with JSDoc naming what each owns. The exact previous defaults are preserved verbatim (relative paths, resolved against the daemon's cwd == repo root).
taskDefinitions.mjs: the exported constants become leaf-fed (DEFAULT_DB_PATH = AiConfig.orchestrator.dbPath, DEFAULT_DATA_DIR = AiConfig.orchestrator.dataDir), so the 5-consumer surface (Orchestrator, MaintenanceBackpressureService, TenantRepoSyncService, RecoveryActuatorService, ConfiguredTaskDefinitionsService/specs) stays byte-stable — no consumer chasing in this batch.
daemon.mjs: the duplicate derivation is gone. As an entrypoint it reads the leaf directly (AiConfig.orchestrator.dataDir), matching its existing AiConfig.orchestrator.devSyncRoots use-site style.
- The gitignored
ai/config.mjs overlay (a full materialized template copy) received the identical leaf edit, so the canonical deployment does not trip the boot-time freshness guard.
Bootstrap-chain finding (the C1 question, V-B-A'd): every importer of taskDefinitions.mjs — the four export consumers plus ConfiguredTaskDefinitionsService and the three orchestrator specs — already imports AiConfig (and the Neo namespace bootstrap) itself, so the added import introduces zero marginal bootstrap weight and no cycle (the config chain config.mjs → ConfigProvider → src/state/Provider never reaches back into ai/daemons/**). The one real behavior change: taskDefinitions.mjs is no longer importable without the entrypoint Neo bootstrap (src/core/Compare.mjs references the global Neo at module scope). No such bootstrap-free consumer exists anywhere in the repo (swept ai/, test/, bin/, buildScripts/); the daemon entrypoints bootstrap at module top before any class import, and all specs import src/Neo.mjs + core/_export.mjs first.
Evidence: 287/287 targeted tests green (see Test Evidence), including the integration spec that boots the real daemon.mjs child process with NEO_AI_ORCHESTRATOR_DIR/NEO_AI_DB_PATH env overrides; standalone smoke runs confirm the leaf-fed exports resolve the exact previous defaults and that both env bindings still override through the leaves.
Deltas from ticket
- Lazy accessors in
daemon.mjs instead of eager module-level path.join consts. A straight const PID_FILE = path.join(AiConfig.orchestrator.dataDir, …) would evaluate during module load — before the boot-time assertConfigFresh guard runs — so a stale overlay (missing exactly the leaves this PR adds) would crash with a cryptic path TypeError instead of the guard's actionable --migrate-config message. daemonDataDir() / pidFilePath() / logFilePath() defer the reads to use sites, all of which run post-guard. This preserves the guard's designed contract for precisely the drift class this PR creates in the wild.
- The
DEFAULT_* export surface itself (the §3 B1 pattern) is intentionally kept — batch-2 kills the A1 env re-derivation while holding the consumer surface stable; collapsing the re-exports into direct use-site reads is a later, consumer-chasing slice.
- Overlay-ahead sync: the operator's live
ai/config.mjs overlay had gained an mcpHttpHost leaf (from an in-flight lane) after this worktree's setup copy; the worktree overlay was re-synced from the live copy before applying the leaf edit, so both overlay copies stay byte-identical and ahead-of-template drift is preserved (harmless: the freshness guard only checks the template → overlay direction).
Test Evidence
All runs with --workers=1, port 18180 cleared before each run:
test/playwright/unit/ai/daemons/orchestrator/daemon.spec.mjs + Orchestrator.spec.mjs + Orchestrator.invariants.spec.mjs — 101 passed (32.8s). Covers the source-level invariants (fail-loud direct reads, no defensive ?., devSyncRoots direct read) and the taskDefinitions purity contracts.
services/MaintenanceBackpressureService.spec.mjs + services/RecoveryActuatorService.spec.mjs + services/TenantRepoSyncService.spec.mjs — 87 passed (33.4s). The three DEFAULT_DATA_DIR consumers.
services/ProcessSupervisorService.spec.mjs + services/TaskStateService.spec.mjs + ai/scripts/setup/initServerConfigs.spec.mjs — 96 passed (14.0s). Includes the template/overlay drift-detection machinery the new leaves flow through.
test/playwright/integration/ai/daemons/workspaceSafety.spec.mjs (integration config) — 3 passed (7.2s). Boots the real orchestrator daemon.mjs as a child process with env-overridden NEO_AI_ORCHESTRATOR_DIR/NEO_AI_DB_PATH, self-bootstraps sqlite, reaches [Orchestrator] Started — end-to-end proof of the lazy-accessor boot path and the leaf env bindings.
node --check green on all four touched files (template, overlay, taskDefinitions, daemon); block-alignment check clean.
- Smoke (bootstrap + dynamic import):
DEFAULT_DB_PATH === '.neo-ai-data/sqlite/memory-core-graph.sqlite', DEFAULT_DATA_DIR === '.neo-ai-data/orchestrator-daemon' (exact previous literals), and both env vars override through the leaves.
Post-Merge Validation
Commits
- dd6211c45 refactor(orchestrator): ADR-0019 batch-2 — dbPath/dataDir provider leaves replace module-level env derivation (#12456)
Authored by Mnemosyne (Claude Fable 5, Claude Code). Session b956ba53-01ed-4ea6-a1e5-62969f887bc3.
Contributes to #12456 (batch 2 — orchestrator dbPath/dataDir A1 module-level env re-derivation, plus the daemon.mjs drift twin).
Context
ai/daemons/orchestrator/taskDefinitions.mjsexported two module-level env re-derivations with hidden defaults (DEFAULT_DB_PATHfromNEO_AI_DB_PATH,DEFAULT_DATA_DIRfromNEO_AI_ORCHESTRATOR_DIR), andai/daemons/orchestrator/daemon.mjscarried an independent drift twin of theNEO_AI_ORCHESTRATOR_DIRderivation — the ADR-0019 §3 A1 pattern (module-level re-derivation), with the drift twin as the empirical cost: two files each owned a copy of the same default.The fix follows the batch-1 idiom (PR #14935, KB-domain slice):
ai/config.template.mjsgains twoorchestrator.*leaves —dataDir: leaf('.neo-ai-data/orchestrator-daemon', 'NEO_AI_ORCHESTRATOR_DIR', 'string')anddbPath: leaf('.neo-ai-data/sqlite/memory-core-graph.sqlite', 'NEO_AI_DB_PATH', 'string')— with JSDoc naming what each owns. The exact previous defaults are preserved verbatim (relative paths, resolved against the daemon's cwd == repo root).taskDefinitions.mjs: the exported constants become leaf-fed (DEFAULT_DB_PATH = AiConfig.orchestrator.dbPath,DEFAULT_DATA_DIR = AiConfig.orchestrator.dataDir), so the 5-consumer surface (Orchestrator, MaintenanceBackpressureService, TenantRepoSyncService, RecoveryActuatorService, ConfiguredTaskDefinitionsService/specs) stays byte-stable — no consumer chasing in this batch.daemon.mjs: the duplicate derivation is gone. As an entrypoint it reads the leaf directly (AiConfig.orchestrator.dataDir), matching its existingAiConfig.orchestrator.devSyncRootsuse-site style.ai/config.mjsoverlay (a full materialized template copy) received the identical leaf edit, so the canonical deployment does not trip the boot-time freshness guard.Bootstrap-chain finding (the C1 question, V-B-A'd): every importer of
taskDefinitions.mjs— the four export consumers plusConfiguredTaskDefinitionsServiceand the three orchestrator specs — already importsAiConfig(and the Neo namespace bootstrap) itself, so the added import introduces zero marginal bootstrap weight and no cycle (the config chainconfig.mjs → ConfigProvider → src/state/Providernever reaches back intoai/daemons/**). The one real behavior change:taskDefinitions.mjsis no longer importable without the entrypoint Neo bootstrap (src/core/Compare.mjsreferences the globalNeoat module scope). No such bootstrap-free consumer exists anywhere in the repo (sweptai/,test/,bin/,buildScripts/); the daemon entrypoints bootstrap at module top before any class import, and all specs importsrc/Neo.mjs+core/_export.mjsfirst.Evidence: 287/287 targeted tests green (see Test Evidence), including the integration spec that boots the real
daemon.mjschild process withNEO_AI_ORCHESTRATOR_DIR/NEO_AI_DB_PATHenv overrides; standalone smoke runs confirm the leaf-fed exports resolve the exact previous defaults and that both env bindings still override through the leaves.Deltas from ticket
daemon.mjsinstead of eager module-levelpath.joinconsts. A straightconst PID_FILE = path.join(AiConfig.orchestrator.dataDir, …)would evaluate during module load — before the boot-timeassertConfigFreshguard runs — so a stale overlay (missing exactly the leaves this PR adds) would crash with a crypticpathTypeError instead of the guard's actionable--migrate-configmessage.daemonDataDir()/pidFilePath()/logFilePath()defer the reads to use sites, all of which run post-guard. This preserves the guard's designed contract for precisely the drift class this PR creates in the wild.DEFAULT_*export surface itself (the §3 B1 pattern) is intentionally kept — batch-2 kills the A1 env re-derivation while holding the consumer surface stable; collapsing the re-exports into direct use-site reads is a later, consumer-chasing slice.ai/config.mjsoverlay had gained anmcpHttpHostleaf (from an in-flight lane) after this worktree's setup copy; the worktree overlay was re-synced from the live copy before applying the leaf edit, so both overlay copies stay byte-identical and ahead-of-template drift is preserved (harmless: the freshness guard only checks the template → overlay direction).Test Evidence
All runs with
--workers=1, port 18180 cleared before each run:test/playwright/unit/ai/daemons/orchestrator/daemon.spec.mjs+Orchestrator.spec.mjs+Orchestrator.invariants.spec.mjs— 101 passed (32.8s). Covers the source-level invariants (fail-loud direct reads, no defensive?.,devSyncRootsdirect read) and the taskDefinitions purity contracts.services/MaintenanceBackpressureService.spec.mjs+services/RecoveryActuatorService.spec.mjs+services/TenantRepoSyncService.spec.mjs— 87 passed (33.4s). The threeDEFAULT_DATA_DIRconsumers.services/ProcessSupervisorService.spec.mjs+services/TaskStateService.spec.mjs+ai/scripts/setup/initServerConfigs.spec.mjs— 96 passed (14.0s). Includes the template/overlay drift-detection machinery the new leaves flow through.test/playwright/integration/ai/daemons/workspaceSafety.spec.mjs(integration config) — 3 passed (7.2s). Boots the real orchestratordaemon.mjsas a child process with env-overriddenNEO_AI_ORCHESTRATOR_DIR/NEO_AI_DB_PATH, self-bootstraps sqlite, reaches[Orchestrator] Started— end-to-end proof of the lazy-accessor boot path and the leaf env bindings.node --checkgreen on all four touched files (template, overlay, taskDefinitions, daemon); block-alignment check clean.DEFAULT_DB_PATH === '.neo-ai-data/sqlite/memory-core-graph.sqlite',DEFAULT_DATA_DIR === '.neo-ai-data/orchestrator-daemon'(exact previous literals), and both env vars override through the leaves.Post-Merge Validation
orchestrator.log/ PID files continue under.neo-ai-data/orchestrator-daemon, and the graph DB opens at.neo-ai-data/sqlite/memory-core-graph.sqlite(or their env overrides where set).Commits
Authored by Mnemosyne (Claude Fable 5, Claude Code). Session b956ba53-01ed-4ea6-a1e5-62969f887bc3.