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:
- Two sources of truth — drift risk when one is updated without the other (already observed below)
- Bypass-the-config substrate — consumers importing the local constant directly never hit the operator's override path
- 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")
- Empirical drift already observed —
SwarmHeartbeatService.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):
const defaultConfig = {
orchestrator: {
intervals: {
pollMs : 3000,
summarySweepMs : 10 * 60 * 1000,
kbSyncMs : 30 * 60 * 1000,
backupMs : DAY_MS,
}
},
knowledgeBase: {
alertingCooldownMs: 60 * 60 * 1000,
}
};
import AiConfig from '<path>/ai/config.template.mjs';
const pollMs = options.pollMs ?? AiConfig.orchestrator?.intervals?.pollMs;
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
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
Context
Filed per
/tech-debt-radarsweep 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_Xfallback 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.mjsis the canonical Tier-1 config substrate (extended at daemon-boot by operator's gitignoredai/config.mjsvialoadLocalAiConfig()). HardcodedDEFAULT_X_*_MS/DEFAULT_X_INTERVAL_*constants in service / daemon / script files duplicate values that ALSO live inconfig.template.mjs, creating:DEFAULT_Xinstead of consulting / extendingconfig.template.mjs(memory anchor: #11642 KB Alerting Service author had to grep "where is aiConfig.knowledgeBase defined")SwarmHeartbeatService.mjs:37declares LOCALDEFAULT_POLL_INTERVAL_MS = 5 * 60 * 1000(5 min) whileTaskDefinitions.mjs:7declaresDEFAULT_POLL_INTERVAL_MS = 3000(3 sec). Same name, 100× value drift.The Audit (5+ surfaces identified)
ai/daemons/TaskDefinitions.mjs:7DEFAULT_POLL_INTERVAL_MSorchestrator.intervals.pollMsai/daemons/TaskDefinitions.mjs:8DEFAULT_SUMMARY_SWEEP_INTERVAL_MSorchestrator.intervals.summarySweepMsai/daemons/TaskDefinitions.mjs:9DEFAULT_KB_SYNC_INTERVAL_MSorchestrator.intervals.kbSyncMsai/daemons/TaskDefinitions.mjs:10DEFAULT_BACKUP_INTERVAL_MSorchestrator.intervals.backupMsai/daemons/TaskDefinitions.mjs:11DEFAULT_PRIMARY_DEV_SYNC_INTERVAL_MSorchestrator.intervals.primaryDevSyncMsai/daemons/TaskDefinitions.mjs:13DEFAULT_DREAM_INTERVAL_MSorchestrator.intervals.dreamMsai/daemons/TaskDefinitions.mjs:15DEFAULT_GOLDEN_PATH_INTERVAL_MSorchestrator.intervals.goldenPathMsai/daemons/TaskDefinitions.mjs:17DEFAULT_SWARM_HEARTBEAT_INTERVAL_MSorchestrator.intervals.swarmHeartbeatMsai/daemons/SwarmHeartbeatService.mjs:37DEFAULT_POLL_INTERVAL_MS(LOCAL)ai/services/knowledge-base/helpers/KbAlertRuleEngine.mjs:56DEFAULT_COOLDOWN_MSknowledgeBase.alertingCooldownMsai/daemons/services/HeavyMaintenanceLeaseService.mjs:8DEFAULT_HEAVY_MAINTENANCE_LEASE_TTL_MSai/scripts/heartbeatLock.mjs:26DEFAULT_STALE_LOCK_MSSweep 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_MSexports. Migrate consumers (Orchestrator.mjslines 32-42 imports + lines 211-331 static-config-block + lines 393-413 construct-time chain;Orchestrator.spec.mjstest imports) to read fromAiConfig.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(underorchestrator.intervalsor neworchestrator.lockssection as appropriate). MIGRATE consumers. DELETE local constants.Contract Ledger Matrix
ai/daemons/TaskDefinitions.mjsDEFAULT_X_INTERVAL_MSexports DELETED?? DEFAULT_XfallbackDecision Record Impact
nonetoaligned-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
ai/daemons/TaskDefinitions.mjs: 8DEFAULT_X_INTERVAL_MSconstants DELETED. Imports + consumer sites inOrchestrator.mjsmigrated to read fromAiConfig.orchestrator.intervals.X. No legacy-constant fallback retained.ai/daemons/SwarmHeartbeatService.mjs:37localDEFAULT_POLL_INTERVAL_MSDELETED. Migrated toAiConfig.orchestrator.intervals.swarmHeartbeatMs. (Note: empirical drift discovery — local 5 min vs TaskDefinitions 3 sec; canonical config value 5 min is the intended).ai/services/knowledge-base/helpers/KbAlertRuleEngine.mjs:56DEFAULT_COOLDOWN_MSDELETED. Migrated toAiConfig.knowledgeBase.alertingCooldownMs.ai/config.template.mjsextended withHEAVY_MAINTENANCE_LEASE_TTL_MS+STALE_LOCK_MS(or appropriately-named keys per service-boundary placement).HeavyMaintenanceLeaseService.mjs:8+heartbeatLock.mjs:26migrated to read from the new config entries; local constants DELETED.AiConfig.Xvalue reaches the consumer (similar pattern to #11075 Orchestrator regression test from PR #11825 — that test should be retained + replicated for other consumers).DEFAULT_Xconstants that share the same anti-pattern. Add additional surfaces to AC list if found.learn/agentos/doc clarifyingai/config.template.mjsas Tier-1 source-of-truth (NOT a parallel-source pattern). Prevents recurrence.Out of Scope
package.jsoninterval constants — operator-facing scripts have a different concern profile.learn/agentos/sandman-handoff-format.mdconstants (if any) — separate substrate.config.template.mjs ↔ config.mjspattern that's correct.Avoided Traps
?? someValuestatic 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
SwarmHeartbeatService.mjs:37(5 min) vsTaskDefinitions.mjs:7(3 sec) — same constant name, 100× value drift, demonstrating exactly why two-source-of-truth is the anti-pattern.Origin Session ID:
0c4a787e-00ad-4e98-ab09-29f0f1248489