LearnNewsExamplesServices
Frontmatter
id12097
titleOrchestrator config-as-SSOT enforcement: collapse env-or-config getters into envBindings, drop in-code env reads + resolver bypasses
stateClosed
labels
enhancementairefactoringarchitecture
assigneesneo-opus-4-7
createdAtMay 27, 2026, 6:20 PM
updatedAtMay 27, 2026, 9:11 PM
githubUrlhttps://github.com/neomjs/neo/issues/12097
authorneo-opus-4-7
commentsCount0
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
blockedBy[]
blocking[]
closedAtMay 27, 2026, 9:11 PM

Orchestrator config-as-SSOT enforcement: collapse env-or-config getters into envBindings, drop in-code env reads + resolver bypasses

Closedenhancementairefactoringarchitecture
neo-opus-4-7
neo-opus-4-7 commented on May 27, 2026, 6:20 PM

Premise

Operator-direct audit at 2026-05-27T15:50Z surfaced systematic config/env-pattern violations in the orchestrator surface, predating the foundational NO HIDDEN DEFAULT VALUE FALLBACKS IN CODE contract enforcement (PR #12061 cycle-4 anchor). The orchestrator was authored before the config-as-SSOT envBindings discipline was codified; with the contract now established, the existing patterns are debt.

Audit verbatim from operator: "are you still aware of our config and env var patterns? this one did not get created inside this session. however, it is not the only spot. the orchestrator needs a review."

Audit findings

Quantified across ai/daemons/orchestrator/:

Pattern 1 — Env.parse*('NEO_X') ?? AiConfig.X getters (29 instances in Orchestrator.mjs:391-445+)

Per-access env probe + ?? fallback to config-default in code, bypassing the envBindings mechanism entirely. Examples:

get pollIntervalMs()      { return Env.parseNumber('NEO_ORCHESTRATOR_POLL_INTERVAL_MS')   ?? AiConfig.orchestrator.intervals.pollMs;   }
get kbSyncEnabled()       { return Env.parseBool('NEO_ORCHESTRATOR_KB_SYNC_ENABLED')      ?? resolveDeploymentEnabled('kbSyncEnabled'); }
get mlxEnabled()          { return Env.parseBool('NEO_ORCHESTRATOR_MLX_ENABLED')          ?? !!AiConfig.orchestrator.mlx?.enabled; }

The envBindings.orchestrator.* declaration in ai/config.template.mjs is the canonical surface for env-var override; runtime getters compete with that and create split source of truth.

Pattern 2 — Resolver functions (5 instances)

File Function Issue
Orchestrator.mjs:94 resolvePrimaryDevSyncRootsConfig({envValue, configValue}) Custom env-vs-config precedence resolver — operator's anchor example
Orchestrator.mjs:112 resolvePrimaryDevSyncRootsSource({envValue}) Returns env-var-name vs config-key-name — observability noise that should live at the consumer or in config
Orchestrator.mjs:129 resolveDeploymentEnabled(key) Encodes null-means-deployment-default semantics in code; the operator-facing config template shows null but the runtime value comes from a code path the template doesn't expose
Orchestrator.mjs:144 resolveCloudOnlyEnabled(key) Inverse of above
MaintenanceBackpressureService.mjs:119 resolveHeavyMaintenanceLeasePath({heavyMaintenanceLeasePath, dataDir}) Should be a config-template getter or a clean constant per dataDir

Pattern 3 — Direct process.env.X || hardcoded (SwarmHeartbeatService.mjs:60)

return process.env.NEO_HEARTBEAT_ALIVE_PATH || path.resolve(__dirname, '../../../../.neo-ai-data/wake-daemon/heartbeat.alive')

Bypasses both config template + env binding parser. Hardcoded fallback path is exactly the || substitution the operator contract bans.

Pattern 4 — Default-parameter env reads (PrimaryRepoSyncService.mjs:159, 215)

@param {String[]|String|undefined|null} [options.devSyncRootsConfig=process.env.NEO_ORCHESTRATOR_DEV_SYNC_ROOTS]

Even in JSDoc the env read is the default — same anti-pattern at the function-signature layer.

Prescription

Single coherent PR collapsing all four patterns into config-as-SSOT compliance:

  1. Add missing envBindings.orchestrator.* in ai/config.template.mjs for all 29 env vars currently read by Orchestrator getters (intervals.* + tenantRepoSync.{sweepCadenceMs,jitterRatio} + dream/goldenPath/swarmHeartbeat intervals + swarmHeartbeat.{identity,targetSource,targets} + localOnly/cloudOnly enabled flags + mlx.{enabled,model,port} + lms.{enabled,model,port}).
  2. Delete the 29 getters from Orchestrator.mjs. Consumers read AiConfig.orchestrator.X.Y directly (env-applied at config-load time).
  3. Collapse resolveDeploymentEnabled + resolveCloudOnlyEnabled — replace with config-template getters (or store the deployment-default-resolved value at config load, not at every access).
  4. Fix SwarmHeartbeatService.mjs:60 — move hardcoded path to ai/config.template.mjs defaultConfig.heartbeatAlivePath + env binding via Env.parsePath; service reads AiConfig.heartbeatAlivePath.
  5. Fix PrimaryRepoSyncService.mjs:159+215 — remove env defaults from function signatures + JSDoc; callers pass the value explicitly.
  6. Collapse resolvePrimaryDevSyncRootsConfig + resolvePrimaryDevSyncRootsSource — the precedence belongs in envBindings; the source-label belongs at the consumer (or in config metadata).

Acceptance Criteria

  • AC1: All 29 Env.parse*('NEO_X') ?? AiConfig.X getters in Orchestrator.mjs deleted; consumers read AiConfig.orchestrator.X.Y directly
  • AC2: envBindings.orchestrator.* in ai/config.template.mjs contains every env var previously read by a deleted getter (Env.parseNumber/Env.parseBool/Env.parseString declarations)
  • AC3: resolveDeploymentEnabled + resolveCloudOnlyEnabled removed from Orchestrator.mjs (replaced with config-time resolution)
  • AC4: resolvePrimaryDevSyncRootsConfig + resolvePrimaryDevSyncRootsSource removed (collapsed into envBindings precedence)
  • AC5: SwarmHeartbeatService.mjs:60 reads AiConfig.X (no process.env.X || hardcoded-path)
  • AC6: PrimaryRepoSyncService.mjs:159+215 no [options.X=process.env.Y] default-parameter env reads
  • AC7: MaintenanceBackpressureService.resolveHeavyMaintenanceLeasePath reviewed for the same anti-pattern; collapsed into config if applicable
  • AC8: All existing Orchestrator/services tests pass; new tests added asserting env-override flows through envBindings (not through deleted code paths)
  • AC9: §15.6 diagnostic clean on added/modified lines
  • AC10: Capability-framing audit pass (zero downstream-deployment-partner mentions in any artifact)

Avoided Traps

  • ❌ Don't ship as 3-4 split PRs per per-pattern scope — operator-directed single-PR consolidation (do we REALLY need 3-4 PRs, or can we do it with 1-2? go!)
  • ❌ Don't keep "deprecated for backwards compat" wrappers around the deleted getters — clean cut; consumers update in the same PR
  • ❌ Don't add new || / ?? hardcoded patterns in the replacement code — pure config-as-SSOT
  • ❌ Don't touch unrelated orchestrator surfaces (refactor scope strictly limited to env/config pattern enforcement)

Related

  • Operator-stated foundational contract: feedback_no_hidden_default_fallbacks.md (banked from PR #12061 cycle-4)
  • Architectural pattern source: post-M6 service-decomposition refactor that thinned Orchestrator.mjs
  • Sandman restoration cascade: this PR is post-cascade cleanup (PR #12092 + #12093 + #12095 + #12096 just merged)
tobiu referenced in commit e468f30 - "refactor(orchestrator): collapse env-or-config getters into envBindings; drop in-code env reads + resolver bypasses (#12097) (#12098) on May 27, 2026, 9:11 PM
tobiu closed this issue on May 27, 2026, 9:11 PM