LearnNewsExamplesServices
Frontmatter
id12005
titleDelete daemon.mjs router dead code; consolidate mlx/lms config into Orchestrator getters (thin-Orchestrator completion)
stateClosed
labels
airefactoringarchitecture
assigneesneo-opus-4-7
createdAtMay 26, 2026, 2:12 AM
updatedAtMay 26, 2026, 2:59 AM
githubUrlhttps://github.com/neomjs/neo/issues/12005
authorneo-opus-4-7
commentsCount0
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
blockedBy[]
blocking[]
closedAtMay 26, 2026, 2:59 AM

Delete daemon.mjs router dead code; consolidate mlx/lms config into Orchestrator getters (thin-Orchestrator completion)

Closedairefactoringarchitecture
neo-opus-4-7
neo-opus-4-7 commented on May 26, 2026, 2:12 AM

The 25+-ticket "thin Orchestrator" refactor wave moved behavior into purpose-shaped places (registry pattern at scheduling/registry.mjs, per-lane getDueTask pure functions, singleton services like SwarmHeartbeatService/TaskStateService/ProcessSupervisorService). But the daemon.mjs config router was left behind — it still pre-resolves every lane's interval + enable-toggle + lane-internal config into a flat options bag that Orchestrator.start() discards because the Orchestrator has its own getter pattern that supersedes it.

V-B-A — daemon.mjs is doing dead routing

ai/daemons/orchestrator/daemon.mjs::resolveOrchestratorStartOptions (lines 182-268) maps 13 keys into a flat options bag:

Daemon-resolver key Orchestrator getter that supersedes it
summarySweepIntervalMs get summarySweepIntervalMs() at Orchestrator.mjs:347
kbSyncIntervalMs get kbSyncIntervalMs() at Orchestrator.mjs:348
backupIntervalMs get backupIntervalMs() at Orchestrator.mjs:349
primaryDevSyncIntervalMs get primaryDevSyncIntervalMs() at Orchestrator.mjs:350
dreamIntervalMs get dreamIntervalMs() at Orchestrator.mjs:371
goldenPathIntervalMs get goldenPathIntervalMs() at Orchestrator.mjs:372
swarmHeartbeatIntervalMs get swarmHeartbeatIntervalMs() at Orchestrator.mjs:373
kbSyncEnabled get kbSyncEnabled() at Orchestrator.mjs:389
primaryDevSyncEnabled get primaryDevSyncEnabled() at Orchestrator.mjs:390
bridgeDaemonEnabled get bridgeDaemonEnabled() at Orchestrator.mjs:392
swarmHeartbeatEnabled get swarmHeartbeatEnabled() at Orchestrator.mjs:393
goldenPathRepoEnrichmentEnabled get goldenPathRepoEnrichmentEnabled() at Orchestrator.mjs:394

Each Orchestrator getter does the IDENTICAL env-override + AiConfig.orchestrator.X fallback the daemon resolver does (lines 347-394 pattern: return Env.parseNumber('NEO_ORCHESTRATOR_X') ?? AiConfig.orchestrator.intervals.X;). Orchestrator.start() at Orchestrator.mjs:416-489 consumes ONLY scriptDir, dataDir, dbPath, logFile, stateFile, heavyMaintenanceLeasePath, taskDefinitions, nodeBin, mlxEnabled/mlxModel/mlxPort, lmsEnabled/lmsModel/lmsPort, primaryDevSyncRootsConfig — none of the 12 resolver-produced keys.

The resolver's output is discarded at the start() boundary. ~80 lines of pure router boilerplate (the function itself + assignConfigInterval helper + assignLocalOnlyToggle helper + spread call site at line 352).

Why tests didn't catch this

test/playwright/unit/ai/daemons/orchestrator/daemon.spec.mjs has 8+ test blocks asserting the resolver's output shape. The tests verify what the function returns, not what consumes the return value. Classic tests-as-architectural-anchor failure mode — they pin the dead code in place by giving it visible green coverage.

resolveMlxConfig / resolveLmsConfig — live but architecturally borderline

daemon.mjs:286-326. These ARE consumed: startOrchestrator resolves them → passes mlxEnabled/mlxModel/mlxPort/lmsEnabled/lmsModel/lmsPort to Orchestrator.start({...}) → forwarded to buildTaskDefinitions(...) at Orchestrator.mjs:431-436.

Flow: daemon.mjs (resolve) → Orchestrator.start (pass-through) → buildTaskDefinitions (consume). The mlx/lms env-override + fallback pattern is identical-shape to the interval-getter pattern. Substrate-correct shape: Orchestrator owns the resolution via getters (get mlxEnabled(), get mlxModel(), etc.), and buildTaskDefinitions either reads from this.X or is invoked with the Orchestrator-resolved values.

The Fix

Three sub-changes, all surgical:

  1. Delete resolveOrchestratorStartOptions + assignConfigInterval + assignLocalOnlyToggle from daemon.mjs. Delete the spread ...resolveOrchestratorStartOptions(...) at line 352. Net daemon.mjs: -85 LOC.

  2. Consolidate resolveMlxConfig / resolveLmsConfig into Orchestrator getters at the existing getter cluster. Pattern matches the interval-getter pattern already in place:

       get mlxEnabled() { return Env.parseBool('NEO_ORCHESTRATOR_MLX_ENABLED') ?? !!AiConfig.orchestrator.mlx?.enabled; }
    get mlxModel()   { return process.env.NEO_ORCHESTRATOR_MLX_MODEL || AiConfig.orchestrator.mlx?.model; }
    get mlxPort()    { return process.env.NEO_ORCHESTRATOR_MLX_PORT  || AiConfig.orchestrator.mlx?.port; }
    // ditto for lms

    Remove resolveMlxConfig / resolveLmsConfig from daemon.mjs. Update buildTaskDefinitions call in Orchestrator.start() to read this.mlxEnabled/this.mlxModel/this.mlxPort/this.lmsEnabled/this.lmsModel/this.lmsPort directly. Net daemon.mjs: -50 LOC, Orchestrator.mjs: +6 getters.

  3. Collapse daemon.mjs::startOrchestrator to:

       export async function startOrchestrator(options = {}) {
        fs.ensureDirSync(DAEMON_DATA_DIR);
        await enforceSingleton();
        setupCleanupHandlers();
        await loadLocalAiConfig();
    
        return Orchestrator.start({
            dataDir: DAEMON_DATA_DIR,
            primaryDevSyncRootsConfig: AiConfig.orchestrator?.devSyncRoots,
            ...options  // test-injection seams only
        });
    }

    The Orchestrator picks up everything else from its own getters reading AiConfig + env directly.

  4. Delete the corresponding daemon.spec.mjs test blocks for the removed resolvers (the tests verify dead code; removing the code retires the tests). Add new Orchestrator getter coverage for mlxEnabled/mlxModel/mlxPort/lmsEnabled/lmsModel/lmsPort mirroring the existing interval-getter test patterns.

Contract Ledger

Surface Source of Authority Proposed Behavior Fallback / Edge Case Docs Evidence
daemon.mjs::startOrchestrator This ticket Thin process-boot wrapper only: PID/signal mgmt + dotenv + Neo bootstrap + loadLocalAiConfig + delegating to Orchestrator.start({dataDir, primaryDevSyncRootsConfig, ...options}) Test-injection options (scriptDir, dbPath, taskDefinitions, etc.) still forwarded via spread Updated JSDoc reflecting thin-boot shape Live smoke: orchestrator boots + all lanes resolve their cadence via getter chain
Orchestrator mlx/lms getters This ticket New: get mlxEnabled() / mlxModel() / mlxPort() / lmsEnabled() / lmsModel() / lmsPort() — same env+AiConfig pattern as interval getters AiConfig.orchestrator.mlx?.enabled / .model / .port defaults (optional-chaining for missing config blocks) JSDoc on each getter Unit test: env override wins, AiConfig fallback works, undefined-block tolerated
Orchestrator.start(options) This ticket Reads mlx/lms config from this.X getters inside; buildTaskDefinitions call uses this.mlxEnabled/this.mlxModel/... options.mlxEnabled / etc. removed from documented signature (only test-injection seams remain) Updated JSDoc on start() options Existing orchestrator.spec.mjs coverage continues passing
daemon.spec.mjs This ticket Removes resolveOrchestratorStartOptions / resolveMlxConfig / resolveLmsConfig test blocks (they test removed code) None None All other daemon.spec tests continue passing

Acceptance Criteria

  • AC1 — daemon.mjs::resolveOrchestratorStartOptions, assignConfigInterval, assignLocalOnlyToggle deleted entirely.
  • AC2 — daemon.mjs::resolveMlxConfig, resolveLmsConfig deleted; Orchestrator exposes 6 new getters (mlxEnabled, mlxModel, mlxPort, lmsEnabled, lmsModel, lmsPort) mirroring the existing env+AiConfig fallback pattern.
  • AC3 — Orchestrator.start() consumes mlx/lms config via this.X getters (not via options.X); buildTaskDefinitions call updated.
  • AC4 — daemon.mjs::startOrchestrator collapses to thin boot wrapper (PID, signals, Neo bootstrap, config load, delegate). Only test-injection seams remain in the options signature.
  • AC5 — daemon.spec.mjs test blocks for the removed resolvers deleted. New Orchestrator getter test coverage for mlx/lms added (mirrors interval-getter test patterns).
  • AC6 — Live smoke: npm run ai:orchestrator boots cleanly; all lanes (summary, kbSync, backup, primaryDevSync, dream, goldenPath, swarmHeartbeat) resolve their cadence correctly via getters; mlx/lms tasks (when enabled in config) launch correctly.
  • AC7 — Static grep clean: grep -rn "resolveOrchestratorStartOptions\|resolveMlxConfig\|resolveLmsConfig\|assignConfigInterval\|assignLocalOnlyToggle" ai/ test/ --include='*.mjs' returns zero matches.
  • AC8 — Cross-family review per pull-request §6.1.

Out of Scope

  • Lane-internal config ownership beyond mlx/lms (e.g., moving summarySweepIntervalMs INTO scheduling/summary.mjs as a self-reading constant). The Orchestrator-getter pattern is the shipped substrate-correct shape; respecting it is sufficient. Deeper "lanes own their entire config" refactor is a separate substrate-evolution lane if needed.
  • Process-management primitives (PID file, signal handlers, log file routing). These legitimately belong in daemon.mjs as the process-boot entry point.
  • loadLocalAiConfig — config substrate bootstrap is correctly a daemon-level concern; runs before the Orchestrator class loads.
  • Adjacent lanes #11994 (gate-fix on PR #12004) and #12003 (candidate-discovery). Different file surfaces, no overlap.

Avoided Traps

  • Preserving resolveOrchestratorStartOptions as a "backwards-compat shim" — operator's standing rule from earlier sessions: "no backwards compatibility for these config migrations." The flat-options bag was always discarded; preserving it would extend the dead-code lifetime.
  • Moving mlx/lms config INTO each task definition's module (deeper than the Orchestrator getter pattern). That'd diverge from the established interval-getter pattern; consistency wins for now.
  • Keeping the daemon.spec.mjs blocks "for archaeology" — removed-code tests are debt. The removed code's intent is preserved in this ticket body + git history.

Related

  • 25+-ticket "thin Orchestrator" refactor wave that motivated the substrate-evolution direction this ticket completes
  • #11991 (config class-substrate completion) — established class Config extends BaseConfig pattern that makes AiConfig.orchestrator.X chain reliable for the getters to read
  • #11986/#11987 (lms server lifecycle as orchestrator task) — established mlx/lms as supervised-process lanes; this ticket consolidates their config to match the rest of the lane-config substrate
  • Sibling lanes: #11994 / PR #12004 (Shape B gate fix), #12003 (active-a2a-participants candidate discovery) — adjacent substrate, non-overlapping files

Handoff Retrieval Hint: "daemon.mjs router cleanup resolveOrchestratorStartOptions dead code Orchestrator getters mlx lms config consolidation thin Orchestrator completion".

tobiu referenced in commit 0c95af0 - "refactor(orchestrator): delete daemon.mjs router dead code; consolidate mlx/lms config into Orchestrator getters (#12005) (#12006) on May 26, 2026, 2:59 AM
tobiu closed this issue on May 26, 2026, 2:59 AM