---
id: 11513
title: 'Lane A of #11503 — add `backup` to DEFAULT_HEAVY_MAINTENANCE_TASK_NAMES + cross-poll deferral tests'
state: CLOSED
labels:
  - enhancement
  - ai
  - testing
  - architecture
assignees:
  - neo-opus-ada
createdAt: '2026-05-17T01:19:08Z'
updatedAt: '2026-05-17T06:22:07Z'
githubUrl: 'https://github.com/neomjs/neo/issues/11513'
author: neo-opus-ada
commentsCount: 0
parentIssue: 11503
subIssues: []
subIssuesCompleted: 0
subIssuesTotal: 0
blockedBy: []
blocking: []
closedAt: '2026-05-17T06:22:07Z'
---
# Lane A of #11503 — add `backup` to DEFAULT_HEAVY_MAINTENANCE_TASK_NAMES + cross-poll deferral tests

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`:

```js
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):**

```diff
@@ 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

- [ ] **AC1**: `'backup'` is added to `DEFAULT_HEAVY_MAINTENANCE_TASK_NAMES` in `ai/daemons/Orchestrator.mjs`
- [ ] **AC2**: Test asserts `DEFAULT_HEAVY_MAINTENANCE_TASK_NAMES` contains exactly the 6 expected classes (regression-pin against accidental removal)
- [ ] **AC3**: Cross-poll deferral test for `backup` blocked by an active `summary` / `kbSync` / `dream` / `golden-path` (at least one blocking pair to prove `backup` is in the deferral path)
- [ ] **AC4**: Cross-poll deferral test for `dream` blocked by another heavy task (AC2-umbrella gap fill)
- [ ] **AC5**: Cross-poll deferral test for `golden-path` blocked by another heavy task (AC2-umbrella gap fill)
- [ ] **AC6**: Cross-poll deferral test for `primary-dev-sync` blocked by another heavy task (AC2-umbrella gap fill)
- [ ] **AC7**: All new tests follow the existing `createTestOrchestrator(...)` factory + `recordTaskOutcome` + `writeLog` capture pattern from lines 134-230 (no new test substrate)
- [ ] **AC8**: Existing `Orchestrator.spec.mjs` tests continue to pass (no regression in same-poll backpressure or backup-isolation paths)

## 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

## Timeline

- 2026-05-17T01:19:10Z @neo-opus-ada added the `enhancement` label
- 2026-05-17T01:19:10Z @neo-opus-ada added the `ai` label
- 2026-05-17T01:19:10Z @neo-opus-ada added the `testing` label
- 2026-05-17T01:19:10Z @neo-opus-ada added the `architecture` label
- 2026-05-17T01:19:18Z @neo-opus-ada assigned to @neo-opus-ada
- 2026-05-17T01:19:27Z @neo-opus-ada added parent issue #11503
- 2026-05-17T01:26:39Z @neo-opus-ada cross-referenced by PR #11514
- 2026-05-17T01:52:51Z @neo-opus-ada cross-referenced by #11519
- 2026-05-17T02:18:53Z @neo-opus-ada cross-referenced by PR #11521
- 2026-05-17T06:22:07Z @tobiu closed this issue
- 2026-05-17T06:22:07Z @tobiu referenced in commit `4e0d0db` - "feat(ai): add backup to heavy-maintenance set + cross-poll deferral tests — Lane A of #11503 (#11513) (#11514)

Closes Lane A of #11503 umbrella per AC1 + AC2. Sibling to closed Lanes B (#11506)
and C (#11509).

## Code change

1. Add `'backup'` to `DEFAULT_HEAVY_MAINTENANCE_TASK_NAMES` in `ai/daemons/Orchestrator.mjs`
   so backup is recognized as a heavy-maintenance class for both blocker-side
   (`findActiveHeavyMaintenanceTask`) and deferred-candidate-side (the executor wrapper)
   checks.

2. Wrap `backup`'s scheduling in `Orchestrator.poll()` with `executeMaintenanceTask(...)`
   so a due backup defers when ANY other heavy task is active. Adding to the heavy set
   alone is necessary but NOT sufficient — the deferral check on the candidate side
   only fires through `createMaintenanceExecutor`. Sibling tasks (summary/kbSync/dream/
   primary-dev-sync) all route through this wrapper; backup must too. This asymmetry was
   surfaced by the per-class deferral tests (AC3) — exactly the regression-pin value
   the umbrella AC2 calls out.

3. Add JSDoc to `DEFAULT_HEAVY_MAINTENANCE_TASK_NAMES` documenting the membership
   rationale (heavy class members + intentional exclusions like golden-path light per
   #11511 / PR #11512) so future readers understand why each class is in or out.

## Tests added (5)

1. **AC2 — constant-contents pin**: `DEFAULT_HEAVY_MAINTENANCE_TASK_NAMES` contains
   exactly the 5 expected classes (summary, kbSync, backup, primary-dev-sync, dream);
   frozen-array invariant verified; defensive assertion that golden-path is OUT.
2. **AC3 — backup deferred by summary**: cross-poll deferral coverage for backup
   (caught the executor-wrapping gap during cycle).
3. **AC3 symmetric — backup blocking kbSync**: proves backup is a valid blocker too
   (the heavy-set-membership half of AC1).
4. **AC4 — dream deferred by summary**: umbrella AC2 gap fill (no per-class dream
   deferral test existed prior).
5. **AC6 — primary-dev-sync deferred by summary**: umbrella AC2 gap fill.

## Test evidence

- npm run test-unit -- test/playwright/unit/ai/daemons/Orchestrator.spec.mjs -> 17/17 passed in 986ms
  - 12 baseline tests unchanged + 5 new
- Existing summary<->kbSync coverage at lines 134 + 184 unchanged
- Existing golden-path dream-dependency tests at lines 222-302 unchanged (golden-path's
  separate light-maintenance contract per #11511 / #11512 is independent of this change)

## Out of scope

- Lane D (PrimaryRepoSyncService.runKbSync() nested cascade observability) — follows
  as separate small lane after V-B-A trace
- Lane E (observability / stale-lease health surfaces) — separate ticket
- HeavyMaintenanceLeaseService consumer-side JSDoc (friction-to-gold from PR #11509
  cycle-1+cycle-2) — separate small follow-up per @neo-gpt routing

Co-authored-by: tobiu <tobiasuhlig78@gmail.com>"
- 2026-05-17T06:23:12Z @tobiu referenced in commit `55dadc7` - "feat(ai): annotate runKbSync cascade as kbSync lifecycle (#11520) (#11521)

* feat(ai): annotate runKbSync cascade as kbSync lifecycle — Lane D narrow of #11503 (#11520)

Closes Lane D narrow observability of #11503 umbrella (AC8: nested KB cascade no
longer hidden as primary-dev-sync only). Sibling to closed Lanes B (#11506) and
C (#11509), and in-flight Lanes A (PR #11514) and cross-daemon (#11519).

## Code change

PrimaryRepoSyncService.runKbSync() now annotates the cascade as a first-class
kbSync task lifecycle event:
- taskStateService.markStarted('kbSync', 'cascaded-from-primary-dev-sync') before spawn
- markCompleted on success / markFailed(taskName, e.status||1) on cascade failure
- healthService.recordTaskOutcome('kbSync', 'running'|'completed'|'failed',
  {parent: 'primary-dev-sync', reason, ...}) at lifecycle points
- Cascade failure rethrows to preserve current caller semantics
- Both services optional-chained for backward compatibility

Threading: orchestrator's primary-dev-sync invocation already passes both
services into PrimaryRepoSyncService.runTask. Added the pass-through chain
through syncPrimaryDev -> syncConfiguredDevRoots / syncDevRoot /
resolveMetaAndPull -> runKbSync. The singular syncConfiguredDevRoot path
(non-primary roots) passes runKbSync: false so the cascade never fires from
that path and services threading would be dead-weight.

## Out of scope

- Cross-daemon coverage + lease-inheritance contract: #11519 (separate ticket,
  self-claimed; queued post-merge of #11514/#11518)
- TaskStateService / HealthService schema changes: none needed; existing
  details-object on recordTaskOutcome carries arbitrary {parent} field
- Cascade-as-separate-task-class in TaskDefinitions: rejected — semantically
  IS the same kbSync class; provenance distinction is the annotation's job

## Test evidence

- npm run test-unit -- test/playwright/unit/ai/daemons/services/PrimaryRepoSyncService.spec.mjs
  -> 16/16 passed in 690ms (12 baseline + 4 new covering AC1-AC4 + AC8)
- npm run test-unit -- test/playwright/unit/ai/daemons/Orchestrator.spec.mjs
  -> 12/12 passed (baseline; will become 17/17 after PR #11514 Lane A merges)

The 4 new spec tests cover:
1. Success path: markStarted + markCompleted + 2 recordTaskOutcome events with parent annotation
2. Failure path: markStarted + markFailed(code) + 2 recordTaskOutcome events; cascade error rethrown
3. Backward-compat: no services injected -> pure shell-out, no annotation, no throw
4. Custom parentTaskName: annotation reason + details.parent adapt to parentTaskName option

## Authority anchors

- Parent umbrella: #11503 (Enforce heavy-maintenance mutex across Agent OS tasks),
  AC8 explicit
- Sibling lane (closed): #11505 / PR #11506 (Lane B - lease primitive)
- Sibling lane (closed): #11507 / PR #11509 (Lane C - manual CLI adoption)
- Sibling lane (in flight): #11513 / PR #11514 (Lane A - backup heavy-maintenance)
- Sibling lane (queued): #11519 (cross-daemon coverage + env-var inheritance)
- Lead-call: @neo-gpt MESSAGE:fb84293e-325b-4f0f-bf76-473d9ad342ff (narrow Lane D
  is observability, NOT coverage; coverage was transitively closed by Lane C)
- V-B-A trace: #11503 comment IC_kwDODSospM8AAAABClwhYQ (cascade DOES route
  through Lane C-wrapped script; observability is the remaining gap)

(#11520)

* fix(ai): address PR #11521 cycle 1 — strict temporal ordering test + threaded-option JSDoc (#11520)

Per @neo-gpt cycle 1 review (PRR_kwDODSospM8AAAABAJRHVA), two narrow Required Actions:

1. **Strengthen success-path test for AC6 temporal ordering**: previous test asserted
   end-state of `events` and `outcomes` arrays but did NOT verify WHEN annotations fire
   relative to execFileSyncFn. AC6 requires markStarted + running outcome BEFORE exec
   begins; markCompleted + completed outcome AFTER exec returns. Without ordering pin,
   a future refactor moving annotations around exec could pass end-state tests while
   violating the observability lifecycle contract.

   Test rewritten with a strict call-sequence array — instrumented helpers push
   synchronous markers on every call (state-started / health-running / exec /
   state-completed / health-completed); assertion is on exact sequence equality.
   Fail-mode: any reordering of the 5-step sequence breaks the test.

2. **Add JSDoc for threaded service options**: the `taskStateService` and
   `healthService` option-bag pass-through was added to `syncPrimaryDev()`,
   `syncConfiguredDevRoots()`, `syncDevRoot()`, and `resolveMetaAndPull()` in
   commit 29abe3a10 but @param entries were not added. Added on all 4 methods
   with optional-chained semantics + Lane D narrow attribution.

No runtime behavior change. Test-strengthening + JSDoc only.

Test evidence:
- npm run test-unit -- test/playwright/unit/ai/daemons/services/PrimaryRepoSyncService.spec.mjs
  -> 16/16 passed in 781ms (12 baseline + 4 new; strict-ordering test is now the
  load-bearing #11520 AC6 assertion)

(#11520)

---------

Co-authored-by: tobiu <tobiasuhlig78@gmail.com>"

