@ai-dossier/sched 0.68.2 → 0.68.3-next.552.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -380,7 +380,54 @@ This applies to `issue:<n>` unit dispatch (`dispatchAssignments`). `batch:<id>`
380
380
  through a separate pass with its own claim/reconcile logic — see
381
381
  [Batch dispatch (#523)](#batch-dispatch-523) below.
382
382
 
383
- ### Supervised deployment (#679)
383
+ ### Supervised deployment (#679, #945)
384
+
385
+ **The way to run the engine is `ai-dossier sched service install`** (run it from the project's
386
+ checkout). The engine is safe to kill at any instant, but it cannot restart itself — the service is
387
+ what makes it come back by itself, so no session ever has to start, stop or babysit it.
388
+
389
+ ```bash
390
+ ai-dossier sched service install [--alert-issue <n>] # systemd user unit; cron watchdog if no systemd
391
+ ai-dossier sched service status # supervised? engine live? heartbeat?
392
+ ai-dossier sched service uninstall
393
+ ```
394
+
395
+ - **systemd** (default when a user manager is running): writes
396
+ `~/.config/systemd/user/dossier-sched-<project>.service` — `Restart=always` (a clean exit, such as
397
+ a graceful stop, comes back too) with **crash-loop bounds**: exponential `RestartSec` backoff
398
+ (10 s up to 300 s, systemd >= 254; older versions keep 10 s) and `StartLimitBurst=10` per
399
+ `StartLimitIntervalSec=600` — past that the unit is left `failed` for an operator
400
+ (`systemctl --user reset-failed <unit>`); `KillMode=process`, `WorkingDirectory` = the project's
401
+ **main checkout** (never a linked worktree; validated against `--project`, override with
402
+ `--repo-dir`), and the node/nvm `PATH` at install time (absolute entries only). Install refuses
403
+ node / entry-point / repo paths that are missing, relative, or under a temp or `worktrees/`
404
+ directory, and warns that an nvm node path must be re-pinned (re-run `install`) after a node
405
+ upgrade. It runs `sched start --auto-upgrade` (opt out with `--no-auto-upgrade`); after a
406
+ completed tick, a stale engine installs the latest CLI and re-execs the same command before
407
+ another state write. Detached agents keep running and the new engine reattaches by pid. The
408
+ new version migrates state on load; older engines reject newer schemas. If the Node runtime
409
+ cannot replace the process directly, exit code 75 asks systemd/the watchdog to restart the
410
+ updated entry point.
411
+ Output goes to the journal (`journalctl --user -u <unit>`) and the durable trail is the
412
+ project's `events.jsonl`. Run `loginctl enable-linger $USER` once so it starts at boot without a
413
+ login. Re-running `install` is idempotent; a changed unit is restarted (agents keep running).
414
+ - **cron fallback** (no systemd): a tagged crontab block with `@reboot` and a per-minute
415
+ `sched ensure-running` watchdog (`%` is escaped for cron). The watchdog starts a detached engine
416
+ (output to `<sched-dir>/engine.log`) when no live engine holds the lease, after raising the
417
+ once-per-episode stale-lease alert; a healthy minute does nothing (no `gh` call). Restarts back
418
+ off exponentially while the engine keeps dying: at a 60 s cadence the first restarts are
419
+ immediate, then gaps of 2, 4, 8 … minutes up to 30, and a live engine resets it. The existing
420
+ crontab is read strictly — only "no crontab for <user>" counts as empty; any other `crontab -l`
421
+ failure aborts the install and changes nothing. The engine takes the lease atomically, so two
422
+ watchdogs — or a watchdog plus the systemd unit — never run two engines. To stop the engine on
423
+ purpose under the watchdog: `sched ensure-running --disable` (`--enable` to resume).
424
+ - **Crash alerts are deduplicated:** repeats of the same alert kind within an hour are ONE tracking
425
+ comment on `--alert-issue`, edited with a repeat count, not a comment per restart.
426
+ - `install` / `uninstall` exit non-zero and print `✗` when anything failed (systemctl, crontab).
427
+ `--no-activate` renders the unit / cron block without calling `systemctl` or `crontab` and says
428
+ plainly that nothing is installed; `--print` prints it.
429
+
430
+ The manual shapes below remain valid, and explain the rules the service encodes.
384
431
 
385
432
  Two supported shapes, one rule: **dispatched agents must outlive the tick or engine that
386
433
  spawned them.** Agents are spawned detached and unref'd precisely so they survive a sched
@@ -470,6 +517,7 @@ Only SIGINT had a handler, so a SIGTERM/SIGHUP (another session's restart, a clo
470
517
  terminal) killed the engine with no line and no lease release. That is now logged; if the
471
518
  2026-09-29 cause was SIGKILL/OOM, the missing `engine-exit` plus the stale lease is now the
472
519
  documented signature (`journalctl -k` for the OOM window).
520
+
473
521
  ### Restart never discards work (#945, #940)
474
522
 
475
523
  Before the engine respawns an agent onto a worktree that already exists — a batch **tail**, a
@@ -520,8 +568,8 @@ last pushed head while local work exists, the engine does **not** respawn. Rows
520
568
  fields (the engine's own post-landing gate rows) and clean rows are never evidence; an older dirty
521
569
  row is superseded by a newer clean or failed one. It blocks the batch `tail-dirty-worktree`, with the evidence and the
522
570
  rescue ref in the journal and `sched status`. Commit or discard the work in the worktree, then
523
- `sched resume --batch <id>`. A tail that left only unpushed commits is respawned, told to resume
524
- from the rescue ref. `sched resume --batch` deliberately does NOT re-run this guard: by resuming, the operator has looked at the worktree.
571
+ `sched resume --batch <id>`. A tail that left only unpushed commits is respawned, told to resume
572
+ from the rescue ref. `sched resume --batch` deliberately does NOT re-run this guard: by resuming, the operator has looked at the worktree.
525
573
 
526
574
  ### Zombie-run fencing (#504)
527
575
 
@@ -1 +1 @@
1
- {"version":3,"file":"batch-dispatch.d.ts","sourceRoot":"","sources":["../src/batch-dispatch.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"batch-dispatch.d.ts","sourceRoot":"","sources":["../src/batch-dispatch.ts"],"names":[],"mappings":"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"}
@@ -1424,15 +1424,28 @@ function claimAndSetup(deps, config, dispatch, batchId, now, result) {
1424
1424
  abortSetup(`member-worktree-prep-failed:${memberPrep.reason}`, `member worktree prep failed: ${memberPrep.reason} (member 1 #${firstIssue}, branch ${memberBranchFor(batchId, 1, firstIssue)})`);
1425
1425
  return;
1426
1426
  }
1427
+ // Persist the completed setup and the batch's executing/member-1 assignment
1428
+ // BEFORE spawning. If SIGKILL lands between this write and the provider
1429
+ // process creation, the next engine sees `executing` + an assigned slot and
1430
+ // `reconcileMemberSlot` resumes from the deterministic member worktree. The
1431
+ // previous single lock pass spawned first and only saved afterward, leaving
1432
+ // a kill window with batch=status ready + an assigned slot that no tick arm
1433
+ // could claim again.
1427
1434
  deps.store.withLock((s) => ({
1428
- state: landSetup(deps, s, batchId, setup, now, { member_dispatch: 'serial' }, 1, (next) => {
1429
- const slot = slotFor(next, batchId);
1430
- return slot
1431
- ? spawnMember(deps, dispatch, next, slot, batchId, now, result, memberPrep)
1432
- : next;
1433
- }),
1435
+ state: landSetup(deps, s, batchId, setup, now, { member_dispatch: 'serial' }, 1, (next) => next),
1434
1436
  result: undefined,
1435
1437
  }));
1438
+ deps.store.withLock((s) => {
1439
+ const freshBatch = (0, state_1.findBatch)(s, batchId);
1440
+ const slot = slotFor(s, batchId);
1441
+ if (!freshBatch || freshBatch.status !== 'executing' || !slot) {
1442
+ return { state: s, result: undefined };
1443
+ }
1444
+ return {
1445
+ state: spawnMember(deps, dispatch, s, slot, batchId, now, result, memberPrep),
1446
+ result: undefined,
1447
+ };
1448
+ });
1436
1449
  }
1437
1450
  /**
1438
1451
  * Land a completed `runBatchSetup` on the batch entry (inside the caller's
@@ -2965,6 +2978,45 @@ function reconcileMemberSlot(deps, config, dispatch, batchId, slot, now, result)
2965
2978
  release, () => runLog?.append());
2966
2979
  }
2967
2980
  }
2981
+ /**
2982
+ * Recover the durable setup→member handoff when an engine dies after
2983
+ * `landSetup` saved `batch.status=executing` but before the member process was
2984
+ * spawned and its pid was saved. The slot is still `assigned`; release that
2985
+ * claim and reuse `spawnMemberContinuation`, whose deterministic member
2986
+ * worktree path adopts the already-prepared tree. This is the batch equivalent
2987
+ * of `engine.ts`'s assigned-before-spawn recovery rail.
2988
+ */
2989
+ function recoverAssignedMemberSlot(deps, config, dispatch, batchId, slot, now, result) {
2990
+ const current = deps.store.load();
2991
+ const batch = (0, state_1.findBatch)(current, batchId);
2992
+ if (!batch ||
2993
+ batch.status !== 'executing' ||
2994
+ isParallelBatch(batch) ||
2995
+ slot.status !== 'assigned' ||
2996
+ slot.pid !== null) {
2997
+ return;
2998
+ }
2999
+ const released = deps.store.withLock((state) => {
3000
+ const fresh = (0, state_1.findBatch)(state, batchId);
3001
+ const freshSlot = slotFor(state, batchId);
3002
+ if (!fresh ||
3003
+ fresh.status !== 'executing' ||
3004
+ isParallelBatch(fresh) ||
3005
+ !freshSlot ||
3006
+ freshSlot.status !== 'assigned' ||
3007
+ freshSlot.pid !== null) {
3008
+ return { state, result: false };
3009
+ }
3010
+ return { state: releaseSlot(state, batchId, now), result: true };
3011
+ });
3012
+ if (!released)
3013
+ return;
3014
+ journalEvent(deps, 'assigned-recovered', unit(batchId), {
3015
+ slot: slot.id,
3016
+ detail: 'engine restarted after a serial batch assignment was persisted but before member spawn was recorded',
3017
+ });
3018
+ spawnMemberContinuation(deps, config, dispatch, batchId, now, result);
3019
+ }
2968
3020
  /**
2969
3021
  * Park a member that left the batch before landing, record the exit, and
2970
3022
  * dissolve if this tips the batch past its `dissolve_policy` threshold (#563;
@@ -5355,6 +5407,13 @@ function runBatchTick(deps, config, dispatch) {
5355
5407
  reconcileParallelBatch(deps, config, batchDispatch, batch.id, now, result, paused);
5356
5408
  continue;
5357
5409
  }
5410
+ if (batch.status === 'executing' &&
5411
+ !isParallelBatch(batch) &&
5412
+ slot?.status === 'assigned' &&
5413
+ slot.pid === null) {
5414
+ recoverAssignedMemberSlot(deps, config, batchDispatch, batch.id, slot, now, result);
5415
+ continue;
5416
+ }
5358
5417
  if (slot && (slot.status === 'running' || slot.status === 'assigned')) {
5359
5418
  if (batch.status === 'executing') {
5360
5419
  reconcileMemberSlot(deps, config, batchDispatch, batch.id, slot, now, result);