LearnNewsExamplesServices
Frontmatter
id11827
titleTech-debt: hardcoded DEFAULT_X constants bypassing ai/config.template.mjs substrate (5+ surfaces)
stateClosed
labels
enhancementairefactoringarchitecturemodel-experience
assigneesneo-gpt
createdAtMay 23, 2026, 10:54 AM
updatedAtMay 23, 2026, 3:28 PM
githubUrlhttps://github.com/neomjs/neo/issues/11827
authorneo-opus-ada
commentsCount0
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
blockedBy[]
blocking[]
closedAtMay 23, 2026, 3:28 PM

Tech-debt: hardcoded DEFAULT_X constants bypassing ai/config.template.mjs substrate (5+ surfaces)

Closed v13.0.0/archive-v13-0-0-chunk-13 enhancementairefactoringarchitecturemodel-experience
neo-opus-ada
neo-opus-ada commented on May 23, 2026, 10:54 AM

Context

Filed per /tech-debt-radar sweep triggered by operator-direction 2026-05-23 ~09:00Z: "ai/daemons/TaskDefinitions.mjs => hardcore anti-pattern, bypassing our config files. this is literally worth /tech-debt-radar."

Originating empirical trigger: PR #11825 (cycle-2 wrong-direction implementation of #11075) — added AiConfig.orchestrator?.intervals?.X ?? DEFAULT_X fallback chain, KEEPING the hardcoded constants as "defense-in-depth fallback". Operator clarified this PRESERVES the anti-pattern rather than removing it. PR #11825 closed concurrently with this filing. #11075's original scope ("migrate orchestrator magic numbers to ai/config.template.mjs") is the correct intent; "migrate" means MOVE source-of-truth, NOT add parallel fallback.

The Anti-Pattern

ai/config.template.mjs is the canonical Tier-1 config substrate (extended at daemon-boot by operator's gitignored ai/config.mjs via loadLocalAiConfig()). Hardcoded DEFAULT_X_*_MS / DEFAULT_X_INTERVAL_* constants in service / daemon / script files duplicate values that ALSO live in config.template.mjs, creating:

  1. Two sources of truth — drift risk when one is updated without the other (already observed below)
  2. Bypass-the-config substrate — consumers importing the local constant directly never hit the operator's override path
  3. Discoverability fog — new agents adding a service create their own local DEFAULT_X instead of consulting / extending config.template.mjs (memory anchor: #11642 KB Alerting Service author had to grep "where is aiConfig.knowledgeBase defined")
  4. Empirical drift already observedSwarmHeartbeatService.mjs:37 declares LOCAL DEFAULT_POLL_INTERVAL_MS = 5 * 60 * 1000 (5 min) while TaskDefinitions.mjs:7 declares DEFAULT_POLL_INTERVAL_MS = 3000 (3 sec). Same name, 100× value drift.

The Audit (5+ surfaces identified)

File:line Constant Value In ai/config.template.mjs? Status
ai/daemons/TaskDefinitions.mjs:7 DEFAULT_POLL_INTERVAL_MS 3000 orchestrator.intervals.pollMs Duplicate — DELETE constant
ai/daemons/TaskDefinitions.mjs:8 DEFAULT_SUMMARY_SWEEP_INTERVAL_MS 600000 orchestrator.intervals.summarySweepMs Duplicate — DELETE constant
ai/daemons/TaskDefinitions.mjs:9 DEFAULT_KB_SYNC_INTERVAL_MS 1800000 orchestrator.intervals.kbSyncMs Duplicate — DELETE constant
ai/daemons/TaskDefinitions.mjs:10 DEFAULT_BACKUP_INTERVAL_MS 86400000 orchestrator.intervals.backupMs Duplicate — DELETE constant
ai/daemons/TaskDefinitions.mjs:11 DEFAULT_PRIMARY_DEV_SYNC_INTERVAL_MS 600000 orchestrator.intervals.primaryDevSyncMs Duplicate — DELETE constant
ai/daemons/TaskDefinitions.mjs:13 DEFAULT_DREAM_INTERVAL_MS 3600000 orchestrator.intervals.dreamMs Duplicate — DELETE constant
ai/daemons/TaskDefinitions.mjs:15 DEFAULT_GOLDEN_PATH_INTERVAL_MS 3600000 orchestrator.intervals.goldenPathMs Duplicate — DELETE constant
ai/daemons/TaskDefinitions.mjs:17 DEFAULT_SWARM_HEARTBEAT_INTERVAL_MS 300000 orchestrator.intervals.swarmHeartbeatMs Duplicate — DELETE constant
ai/daemons/SwarmHeartbeatService.mjs:37 DEFAULT_POLL_INTERVAL_MS (LOCAL) 300000 Drift! (TaskDefinitions value is 3000) Drift surface — DELETE local + read from config
ai/services/knowledge-base/helpers/KbAlertRuleEngine.mjs:56 DEFAULT_COOLDOWN_MS 3600000 knowledgeBase.alertingCooldownMs Duplicate — DELETE constant
ai/daemons/services/HeavyMaintenanceLeaseService.mjs:8 DEFAULT_HEAVY_MAINTENANCE_LEASE_TTL_MS 21600000 ✗ not in config Missing in config — ADD to config.template + MIGRATE consumer
ai/scripts/heartbeatLock.mjs:26 DEFAULT_STALE_LOCK_MS 600000 ✗ not in config Missing in config — ADD to config.template + MIGRATE consumer

Sweep query used: grep -rnE "^export const DEFAULT_[A-Z_]+_MS|^export const DEFAULT_[A-Z_]+(INTERVAL|RETENTION|THRESHOLD|TIMEOUT)" ai --include='*.mjs'. May miss locally-declared (non-exported) constants — broader audit pass at implementation time may surface more.

The Architectural Reality

Canonical substrate path (already exists, just under-used):

// ai/config.template.mjs — Tier-1 schema + defaults (git-checked)
const defaultConfig = {
    orchestrator: {
        intervals: {
            pollMs           : 3000,
            summarySweepMs   : 10 * 60 * 1000,
            kbSyncMs         : 30 * 60 * 1000,
            backupMs         : DAY_MS,
            // ...
        }
    },
    knowledgeBase: {
        alertingCooldownMs: 60 * 60 * 1000,
        // ...
    }
};

// ai/config.mjs — operator-customized runtime config (gitignored)
// loadLocalAiConfig() merges this into AiConfig singleton at daemon-boot

// Consumer pattern (substrate-correct):
import AiConfig from '<path>/ai/config.template.mjs';
const pollMs = options.pollMs ?? AiConfig.orchestrator?.intervals?.pollMs;
// NO `?? DEFAULT_X` legacy fallback. Config IS the source of truth.

The Fix (per surface)

Surface 1: TaskDefinitions.mjs interval constants (8 entries) — DELETE all DEFAULT_X_INTERVAL_MS exports. Migrate consumers (Orchestrator.mjs lines 32-42 imports + lines 211-331 static-config-block + lines 393-413 construct-time chain; Orchestrator.spec.mjs test imports) to read from AiConfig.orchestrator.intervals.X. No legacy fallback. Optional chain ?. for pre-load safety only.

Surface 2: SwarmHeartbeatService.mjs:37 local DEFAULT_POLL_INTERVAL_MS — DELETE the local constant. Migrate to AiConfig.orchestrator.intervals.swarmHeartbeatMs. Note: the local value (5 min) differs from TaskDefinitions value (3 sec) — the canonical config value is 5 min (5 * 60 * 1000); local-drift was the bug, config is correct.

Surface 3: KbAlertRuleEngine.mjs:56 DEFAULT_COOLDOWN_MS — DELETE. Migrate consumers to read from AiConfig.knowledgeBase.alertingCooldownMs.

Surface 4-5: HeavyMaintenanceLeaseService.mjs:8 + heartbeatLock.mjs:26 — ADD entries to ai/config.template.mjs (under orchestrator.intervals or new orchestrator.locks section as appropriate). MIGRATE consumers. DELETE local constants.

Contract Ledger Matrix

Target Surface Source of Authority Proposed Behavior Fallback Docs Evidence
ai/daemons/TaskDefinitions.mjs This ticket + #11075 original intent All 8 DEFAULT_X_INTERVAL_MS exports DELETED None — config substrate is canonical inline JSDoc grep returns zero matches post-merge
6 consumer sites (Orchestrator.mjs construct-time chain) aiConfig.orchestrator.intervals Read from config; no ?? DEFAULT_X fallback options arg → env var → config (no constant fallback) inline comment regression test asserts non-default config value reaches consumer
2 missing-in-config sites (HeavyMaintenanceLease + heartbeatLock) This ticket New entries ADDED to ai/config.template.mjs None — config canonical template doc-comments substrate now discoverable from canonical location

Decision Record Impact

none to aligned-with — this is substrate-cleanup, not a new architectural primitive. May warrant a brief ADR note about "config.template.mjs as Tier-1 source-of-truth, NOT a parallel-source pattern" if the lesson recurs.

Acceptance Criteria

  • AC1ai/daemons/TaskDefinitions.mjs: 8 DEFAULT_X_INTERVAL_MS constants DELETED. Imports + consumer sites in Orchestrator.mjs migrated to read from AiConfig.orchestrator.intervals.X. No legacy-constant fallback retained.
  • AC2ai/daemons/SwarmHeartbeatService.mjs:37 local DEFAULT_POLL_INTERVAL_MS DELETED. Migrated to AiConfig.orchestrator.intervals.swarmHeartbeatMs. (Note: empirical drift discovery — local 5 min vs TaskDefinitions 3 sec; canonical config value 5 min is the intended).
  • AC3ai/services/knowledge-base/helpers/KbAlertRuleEngine.mjs:56 DEFAULT_COOLDOWN_MS DELETED. Migrated to AiConfig.knowledgeBase.alertingCooldownMs.
  • AC4ai/config.template.mjs extended with HEAVY_MAINTENANCE_LEASE_TTL_MS + STALE_LOCK_MS (or appropriately-named keys per service-boundary placement). HeavyMaintenanceLeaseService.mjs:8 + heartbeatLock.mjs:26 migrated to read from the new config entries; local constants DELETED.
  • AC5 — Regression tests added per surface: non-default AiConfig.X value reaches the consumer (similar pattern to #11075 Orchestrator regression test from PR #11825 — that test should be retained + replicated for other consumers).
  • AC6 — Broader audit pass at implementation time: re-run grep + manually check for NON-exported (file-local) DEFAULT_X constants that share the same anti-pattern. Add additional surfaces to AC list if found.
  • AC7 — Update ADR or learn/agentos/ doc clarifying ai/config.template.mjs as Tier-1 source-of-truth (NOT a parallel-source pattern). Prevents recurrence.

Out of Scope

  • package.json interval constants — operator-facing scripts have a different concern profile.
  • learn/agentos/sandman-handoff-format.md constants (if any) — separate substrate.
  • MCP server config templates (memory-core / knowledge-base / etc.) — these have their own config.template.mjs ↔ config.mjs pattern that's correct.

Avoided Traps

  • Add AiConfig fallback while keeping DEFAULT_X as defense-in-depth — explicit anti-pattern per operator-direction (the wrong path my PR #11825 took). Config is canonical; legacy fallback PRESERVES the anti-pattern.
  • Migrate one file at a time with separate PRs — fragmented PRs would leave drift surfaces partially-migrated for extended windows. Recommend SINGLE PR covering all 5 surfaces + new config entries together, OR epic + per-AC subs if scope warrants.
  • Treat this as a "migration" that preserves backward compatibility — there's no external consumer to preserve. These constants are internal substrate; cleanup is non-breaking by definition.
  • Add ?? someValue static fallback in the consumer — same anti-pattern shape. The optional chain ?. handles the rare pre-init case by returning undefined; tests assert against the resolved config value.

Related

  • Originating ticket: #11075 — "Exploration: Migrate orchestrator magic numbers + retention constants to ai/config.template.mjs". This ticket REFINES #11075's scope to make the "migrate" verb explicit (DELETE + replace, not add parallel fallback).
  • Wrong-direction PR (closed): PR #11825 — implemented the "add parallel fallback" misinterpretation. Cycle-2 GPT review caught a wrong-singleton bug + I fixed it but the deeper substrate-bypass remained. Operator surfaced the substrate-correction.
  • Empirical drift anchor: SwarmHeartbeatService.mjs:37 (5 min) vs TaskDefinitions.mjs:7 (3 sec) — same constant name, 100× value drift, demonstrating exactly why two-source-of-truth is the anti-pattern.
  • /tech-debt-radar sweep: triggered by operator direction this turn.

Origin Session ID: 0c4a787e-00ad-4e98-ab09-29f0f1248489

tobiu closed this issue on May 23, 2026, 3:28 PM