Resolves Lane A of #11503 umbrella. Sibling to closed Lanes B (#11505 / PR #11506) and C (#11507 / PR #11509).
FAIR-band: in-band [12/30] — prio-0 continuation per operator elevation 2026-05-16T22:55Z; completes the heavy-maintenance mutex AT THE SCHEDULER LAYER (Lane C closed the manual-CLI surface; Lane A closes the daemon-scheduler set + cross-poll proof).
Context
V-B-A on ai/daemons/Orchestrator.mjs:43-49 confirms backup is NOT currently in DEFAULT_HEAVY_MAINTENANCE_TASK_NAMES:
export const DEFAULT_HEAVY_MAINTENANCE_TASK_NAMES = Object.freeze([
'summary',
'kbSync',
PRIMARY_DEV_SYNC_TASK_NAME,
DREAM_TASK_NAME,
GOLDEN_PATH_TASK_NAME
]);
backup IS defined in TaskDefinitions.mjs:81-87 (executes buildScripts/ai/backup.mjs which exports KB Chroma, Memory Core Chroma, and SQLite graph state) but is missing from the orchestrator-side heavy-maintenance set. Per #11503's umbrella Problem-statement section "Backup is not in the heavy set", this is the AC1 gap.
V-B-A on test/playwright/unit/ai/daemons/Orchestrator.spec.mjs lines 134-230 confirms the existing heavy-maintenance test coverage is summary-vs-kbSync ONLY (both same-poll backpressure at line 134 and cross-poll deferral at line 184). The other heavy classes (dream, golden-path, primary-dev-sync, and now backup) have NO cross-poll deferral coverage. Per #11503's AC2 ("focused orchestrator tests prove daemon-lifetime deferral across polls for every heavy task class, including dream and golden-path, not just same-poll summary-vs-KB"), this is a coverage gap.
The backup-isolation test at line 244 is unrelated — it covers backup scheduling failures, NOT backup participating in heavy-maintenance backpressure.
The Problem
Today's empirical anchor (2026-05-16T19:27Z): orchestrator kbSync wedge cascade. While Lane C closes the manual-CLI bypass surface, the daemon-side backup task can still overlap with summary, kbSync, primary-dev-sync, dream, or golden-path because it's not in the heavy set. A scheduled backup landing during a long Dream cycle (LLM-heavy graph writes) would produce concurrent Chroma + SQLite contention exactly like the wedge class Lane B + C are designed to prevent.
The cross-poll coverage gap is the second half: even if backup is added to the set today, a future refactor could silently regress one of the 6 heavy classes' deferral behavior because there is no per-class test pinning the cross-poll contract. The umbrella explicitly calls this out.
Architectural Reality
ai/daemons/Orchestrator.mjs:43-49 — DEFAULT_HEAVY_MAINTENANCE_TASK_NAMES frozen array (missing 'backup')
ai/daemons/Orchestrator.mjs:270-274 — heavyMaintenanceTaskNames_ reactive config defaults to the constant
ai/daemons/Orchestrator.mjs:336 — orchestrator constructor cloning the default into instance state
ai/daemons/TaskDefinitions.mjs:81-87 — backup task definition (already wired into orchestrator scheduling; just not classified as heavy)
test/playwright/unit/ai/daemons/Orchestrator.spec.mjs:134 — same-poll backpressure test (summary→kbSync)
test/playwright/unit/ai/daemons/Orchestrator.spec.mjs:184 — cross-poll deferral test (summary→kbSync); does NOT cover dream, golden-path, primary-dev-sync, backup
test/playwright/unit/ai/daemons/Orchestrator.spec.mjs:80 — state-key sanity test already includes 'backup' in expected keys (confirming backup is wired into TaskStateService); this is the empirical proof the task exists at the scheduler layer, it's just missing the heavy-classification flag
The Fix
Code change (1 line):
@@ ai/daemons/Orchestrator.mjs @@
export const DEFAULT_HEAVY_MAINTENANCE_TASK_NAMES = Object.freeze([
'summary',
'kbSync',
+ 'backup',
PRIMARY_DEV_SYNC_TASK_NAME,
DREAM_TASK_NAME,
GOLDEN_PATH_TASK_NAME
]);
Test additions (cross-poll deferral coverage per AC2):
Add focused per-heavy-class cross-poll deferral tests in Orchestrator.spec.mjs covering each of the 6 heavy classes:
backup deferred when another heavy task is running (NEW class)
dream deferred when another heavy task is running (gap fill)
golden-path deferred when another heavy task is running (gap fill)
primary-dev-sync deferred when another heavy task is running (gap fill)
- Confirm symmetric: each heavy class can ALSO act as the blocker
The existing summary↔kbSync coverage (lines 134 + 184) stays as-is; new tests are additive.
Test shape can be table-driven (single test factory iterating over heavy-task pairs) OR per-class tests mirroring the existing structure. Either is acceptable; prefer the shape that produces clearest failure messages.
Contract Ledger Matrix
| Target Surface |
Source of Authority |
Proposed Behavior |
Fallback |
Docs |
Evidence |
DEFAULT_HEAVY_MAINTENANCE_TASK_NAMES constant |
Orchestrator.mjs:43-49, #11503 umbrella AC1 |
Includes 'backup' alongside summary/kbSync/primary-dev-sync/dream/golden-path |
N/A (compile-time constant) |
JSDoc on constant + adjacent comment naming AC1/Lane A |
Spec assertion on constant contents + per-class cross-poll deferral tests |
| Heavy-maintenance backpressure (cross-poll) |
Orchestrator.poll() + activeHeavyTask + #11503 AC2 |
Each of the 6 heavy classes (summary, kbSync, backup, primary-dev-sync, dream, golden-path) defers when ANY other heavy task is running across polls |
Non-error skip + recordTaskOutcome with reasonCode: 'heavy-maintenance-backpressure' |
JSDoc on heavyMaintenanceTaskNames_ config |
Focused per-class test in Orchestrator.spec.mjs |
| Operator log surfaces |
writeLog calls in Orchestrator |
Existing Deferring [task name] INFO log shape extended to backup class |
Sparse logs, no WARN flood |
(no doc change — log shape already established by line 134/184 tests) |
Test assertion on log contents per class |
Acceptance Criteria
Out of Scope
- Lane D (
PrimaryRepoSyncService.runKbSync() nested-cascade observability) — separate ticket, may be folded post-V-B-A trace if Lane C's syncKnowledgeBase.mjs wrapping covers it transitively
- Lane E (observability / stale-lease health surfaces) — separate ticket
- HeavyMaintenanceLeaseService consumer-guidance JSDoc (cycle-1+cycle-2 friction-to-gold from PR #11509) — separate small follow-up ticket per @neo-gpt's routing recommendation, NOT bundled into this PR
- Refactoring
DEFAULT_HEAVY_MAINTENANCE_TASK_NAMES from frozen array into Set or other structure — out of scope; constant shape is stable enough
- Adding new heavy-task classes beyond the 6 — none identified as candidates today; this ticket pins the current set, not extends it
Avoided Traps
- Adding
backup to the set without per-class cross-poll tests: rejected — the umbrella's AC2 explicitly calls out the gap. Without per-class tests, a future refactor can silently regress one class's deferral behavior because only summary↔kbSync is currently pinned. Empirical: PR #11509's cycle-2 review caught exactly this class of structural-correctness gap (no test pinning the invariant) — same lesson applies here at the scheduler-set level.
- Table-driven test that asserts ALL pairs: not required by AC2; full N×N coverage adds combinatorial test time without proportional regression-prevention value. Per-class deferral tests + state-key sanity test (line 80) provide the necessary coverage with linear test count.
- Renaming
DEFAULT_HEAVY_MAINTENANCE_TASK_NAMES to clarify "set vs default": rejected — default is the established prefix for the export pattern (see also DEFAULT_PRIMARY_DEV_SYNC_ROOTS_CONFIG etc.). Renaming would create downstream callsite churn for zero substrate value.
- Bundling Lane D V-B-A trace into this PR: rejected per #11503 lane separation. Lane D's call-path investigation (does
PrimaryRepoSyncService.runKbSync() import services directly or shell out to the now-wrapped syncKnowledgeBase.mjs?) is independent work and deserves its own commit + reasoning trail.
Related
- Parent umbrella: #11503 (Enforce heavy-maintenance mutex across Agent OS tasks)
- Sibling lane (closed): #11505 / PR #11506 (Lane B — lease primitive)
- Sibling lane (closed): #11507 / PR #11509 (Lane C — manual CLI script adoption)
- Sibling offshoot (closed): #11511 / PR #11512 (Golden Path light-maintenance classification)
- Coordination anchor: @neo-gpt MESSAGE:3c42e809-d1e9-43ca-a10c-9bdc9a08eb7d (2026-05-17T01:14:56Z) acknowledged Lane A pickup as non-colliding with @neo-gpt's #11475 review lane
- Empirical anchor: 2026-05-16T19:27Z wedge cascade (orchestrator kbSync stuck; Chroma contention) — the failure class this umbrella's three remaining lanes (A, D, E) progressively close
Handoff Retrieval Hints
- Retrieval Hint:
cross-poll heavy maintenance deferral backup orchestrator
- Retrieval Hint: Commit SHA
a5c638069 (Lane C merge into dev) — branch off origin/dev after this commit for clean separation
- Retrieval Hint:
Orchestrator.mjs DEFAULT_HEAVY_MAINTENANCE_TASK_NAMES (exact symbol-anchor for the code change)
- Retrieval Hint: Lane C ticket #11507 + PR #11509 are the closest sibling for shape/scope reference
Origin Session ID: f662d055-a35b-446a-83ff-5fc859604722
Resolves Lane A of #11503 umbrella. Sibling to closed Lanes B (#11505 / PR #11506) and C (#11507 / PR #11509).
FAIR-band: in-band [12/30] — prio-0 continuation per operator elevation 2026-05-16T22:55Z; completes the heavy-maintenance mutex AT THE SCHEDULER LAYER (Lane C closed the manual-CLI surface; Lane A closes the daemon-scheduler set + cross-poll proof).
Context
V-B-A on
ai/daemons/Orchestrator.mjs:43-49confirmsbackupis NOT currently inDEFAULT_HEAVY_MAINTENANCE_TASK_NAMES:export const DEFAULT_HEAVY_MAINTENANCE_TASK_NAMES = Object.freeze([ 'summary', 'kbSync', PRIMARY_DEV_SYNC_TASK_NAME, DREAM_TASK_NAME, GOLDEN_PATH_TASK_NAME ]);backupIS defined inTaskDefinitions.mjs:81-87(executesbuildScripts/ai/backup.mjswhich exports KB Chroma, Memory Core Chroma, and SQLite graph state) but is missing from the orchestrator-side heavy-maintenance set. Per #11503's umbrella Problem-statement section "Backup is not in the heavy set", this is the AC1 gap.V-B-A on
test/playwright/unit/ai/daemons/Orchestrator.spec.mjslines 134-230 confirms the existing heavy-maintenance test coverage issummary-vs-kbSyncONLY (both same-poll backpressure at line 134 and cross-poll deferral at line 184). The other heavy classes (dream,golden-path,primary-dev-sync, and nowbackup) have NO cross-poll deferral coverage. Per #11503's AC2 ("focused orchestrator tests prove daemon-lifetime deferral across polls for every heavy task class, includingdreamandgolden-path, not just same-poll summary-vs-KB"), this is a coverage gap.The backup-isolation test at line 244 is unrelated — it covers backup scheduling failures, NOT backup participating in heavy-maintenance backpressure.
The Problem
Today's empirical anchor (2026-05-16T19:27Z): orchestrator kbSync wedge cascade. While Lane C closes the manual-CLI bypass surface, the daemon-side
backuptask can still overlap withsummary,kbSync,primary-dev-sync,dream, orgolden-pathbecause it's not in the heavy set. A scheduled backup landing during a long Dream cycle (LLM-heavy graph writes) would produce concurrent Chroma + SQLite contention exactly like the wedge class Lane B + C are designed to prevent.The cross-poll coverage gap is the second half: even if
backupis added to the set today, a future refactor could silently regress one of the 6 heavy classes' deferral behavior because there is no per-class test pinning the cross-poll contract. The umbrella explicitly calls this out.Architectural Reality
ai/daemons/Orchestrator.mjs:43-49—DEFAULT_HEAVY_MAINTENANCE_TASK_NAMESfrozen array (missing'backup')ai/daemons/Orchestrator.mjs:270-274—heavyMaintenanceTaskNames_reactive config defaults to the constantai/daemons/Orchestrator.mjs:336— orchestrator constructor cloning the default into instance stateai/daemons/TaskDefinitions.mjs:81-87—backuptask definition (already wired into orchestrator scheduling; just not classified as heavy)test/playwright/unit/ai/daemons/Orchestrator.spec.mjs:134— same-poll backpressure test (summary→kbSync)test/playwright/unit/ai/daemons/Orchestrator.spec.mjs:184— cross-poll deferral test (summary→kbSync); does NOT coverdream,golden-path,primary-dev-sync,backuptest/playwright/unit/ai/daemons/Orchestrator.spec.mjs:80— state-key sanity test already includes'backup'in expected keys (confirming backup is wired into TaskStateService); this is the empirical proof the task exists at the scheduler layer, it's just missing the heavy-classification flagThe Fix
Code change (1 line):
@@ ai/daemons/Orchestrator.mjs @@ export const DEFAULT_HEAVY_MAINTENANCE_TASK_NAMES = Object.freeze([ 'summary', 'kbSync', + 'backup', PRIMARY_DEV_SYNC_TASK_NAME, DREAM_TASK_NAME, GOLDEN_PATH_TASK_NAME ]);Test additions (cross-poll deferral coverage per AC2):
Add focused per-heavy-class cross-poll deferral tests in
Orchestrator.spec.mjscovering each of the 6 heavy classes:backupdeferred when another heavy task is running (NEW class)dreamdeferred when another heavy task is running (gap fill)golden-pathdeferred when another heavy task is running (gap fill)primary-dev-syncdeferred when another heavy task is running (gap fill)The existing summary↔kbSync coverage (lines 134 + 184) stays as-is; new tests are additive.
Test shape can be table-driven (single test factory iterating over heavy-task pairs) OR per-class tests mirroring the existing structure. Either is acceptable; prefer the shape that produces clearest failure messages.
Contract Ledger Matrix
DEFAULT_HEAVY_MAINTENANCE_TASK_NAMESconstantOrchestrator.mjs:43-49, #11503 umbrella AC1'backup'alongside summary/kbSync/primary-dev-sync/dream/golden-pathOrchestrator.poll()+activeHeavyTask+ #11503 AC2runningacross pollsrecordTaskOutcomewithreasonCode: 'heavy-maintenance-backpressure'heavyMaintenanceTaskNames_configOrchestrator.spec.mjswriteLogcalls in OrchestratorDeferring [task name]INFO log shape extended to backup classAcceptance Criteria
'backup'is added toDEFAULT_HEAVY_MAINTENANCE_TASK_NAMESinai/daemons/Orchestrator.mjsDEFAULT_HEAVY_MAINTENANCE_TASK_NAMEScontains exactly the 6 expected classes (regression-pin against accidental removal)backupblocked by an activesummary/kbSync/dream/golden-path(at least one blocking pair to provebackupis in the deferral path)dreamblocked by another heavy task (AC2-umbrella gap fill)golden-pathblocked by another heavy task (AC2-umbrella gap fill)primary-dev-syncblocked by another heavy task (AC2-umbrella gap fill)createTestOrchestrator(...)factory +recordTaskOutcome+writeLogcapture pattern from lines 134-230 (no new test substrate)Orchestrator.spec.mjstests continue to pass (no regression in same-poll backpressure or backup-isolation paths)Out of Scope
PrimaryRepoSyncService.runKbSync()nested-cascade observability) — separate ticket, may be folded post-V-B-A trace if Lane C'ssyncKnowledgeBase.mjswrapping covers it transitivelyDEFAULT_HEAVY_MAINTENANCE_TASK_NAMESfrom frozen array into Set or other structure — out of scope; constant shape is stable enoughAvoided Traps
backupto the set without per-class cross-poll tests: rejected — the umbrella's AC2 explicitly calls out the gap. Without per-class tests, a future refactor can silently regress one class's deferral behavior because only summary↔kbSync is currently pinned. Empirical: PR #11509's cycle-2 review caught exactly this class of structural-correctness gap (no test pinning the invariant) — same lesson applies here at the scheduler-set level.DEFAULT_HEAVY_MAINTENANCE_TASK_NAMESto clarify "set vs default": rejected —defaultis the established prefix for the export pattern (see alsoDEFAULT_PRIMARY_DEV_SYNC_ROOTS_CONFIGetc.). Renaming would create downstream callsite churn for zero substrate value.PrimaryRepoSyncService.runKbSync()import services directly or shell out to the now-wrappedsyncKnowledgeBase.mjs?) is independent work and deserves its own commit + reasoning trail.Related
Handoff Retrieval Hints
cross-poll heavy maintenance deferral backup orchestratora5c638069(Lane C merge into dev) — branch offorigin/devafter this commit for clean separationOrchestrator.mjs DEFAULT_HEAVY_MAINTENANCE_TASK_NAMES(exact symbol-anchor for the code change)Origin Session ID: f662d055-a35b-446a-83ff-5fc859604722