@ai-dossier/sched 0.70.1-next.732.1 → 0.70.2-next.735.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
@@ -202,6 +202,31 @@ where every mechanical supervision decision is code, not remembered prose:
202
202
  tick; a runnable unit never waits while a slot is idle (pinned by a regression test).
203
203
  Refill was always synchronous; what previously had no journal trace was the release
204
204
  itself — see `slot-released` below (#525).
205
+ **Auto-upgrade and child supervision (#1244):** `sched start --auto-upgrade`
206
+ (including `--once`) defers installation and process replacement while any
207
+ recorded slot agent is alive, or this engine/its tick worker still owns a child
208
+ awaiting its exit callback. This includes parallel members, tail, fix and report
209
+ agents, even after their slot is released. Upgrades retry at later tick boundaries
210
+ once those agents drain; the old engine never writes after installing the update.
211
+ On Linux a `/proc/<pid>/stat` state of `Z` is dead for reconciliation, not an
212
+ unkillable agent. Owned children are reaped by libuv when their event loop runs;
213
+ detecting `Z` alone does not reap a child adopted from an older engine image.
214
+ If Linux reports inherited zombies after all live agents and known child
215
+ callbacks drain (including after a tick-worker crash), the engine exits with
216
+ code 75 instead of re-executing: the OS reaper collects the children and the
217
+ service/watchdog starts a fresh process on retained state. An unsupervised
218
+ engine must be started again by its operator. A deliberate upgrade deferral
219
+ is journaled as `engine-auto-upgrade-deferred` with recorded slot/PID and child
220
+ information; unchanged deferrals are deduplicated, and drain is recorded once.
221
+
222
+ To recover an existing `agent-unkillable` batch without abandoning its work:
223
+ `ai-dossier sched resume --batch <id> --project <slug>`. It rechecks every
224
+ recorded batch/member agent's PID and start identity under the state lock and
225
+ refuses while any is genuinely alive. Once all are dead (including zombies),
226
+ it resumes the retained executing position. Members, landed commits, worktrees,
227
+ branches and dispatch profile stay intact; the next tick finishes the original
228
+ member decision. This does not toggle the global pause state. Avoid `abandon`
229
+ when the goal is to keep the batch.
205
230
  6. **Journal (AC6)** — every event (assigned, spawned, exit-detected, external-advance,
206
231
  progress, stalled, redispatched, fence-written, fence-failed, unit-failed,
207
232
  dependents-blocked, slot-released, suspect-dispatch, dispatch-unhealthy, dispatch-failure,
@@ -403,8 +428,8 @@ ai-dossier sched service uninstall
403
428
  node / entry-point / repo paths that are missing, relative, or under a temp or `worktrees/`
404
429
  directory, and warns that an nvm node path must be re-pinned (re-run `install`) after a node
405
430
  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
431
+ completed tick and after all recorded agents and owned children drain, a stale engine
432
+ installs the latest CLI and re-execs the same command before another state write. The
408
433
  new version migrates state on load; older engines reject newer schemas. If the Node runtime
409
434
  cannot replace the process directly, exit code 75 asks systemd/the watchdog to restart the
410
435
  updated entry point.
@@ -165,6 +165,16 @@ export interface MemberFailure {
165
165
  */
166
166
  kind?: MemberExitKind;
167
167
  }
168
+ /** #1244: this block originates only in executing's member stop ladder.
169
+ * Keep slots and member runs so normal reconciliation finishes that exact
170
+ * decision, rather than redispatching members or discarding landed work.
171
+ * Liveness and transition share the state lock with dispatch/stop.
172
+ */
173
+ export declare function resumeUnkillableBatch(store: SchedStore, spawnDeps: Pick<SpawnDeps, 'isAlive'>, batchId: string, now?: Date): {
174
+ batch: string;
175
+ outcome: 'resumed';
176
+ from: 'agent-unkillable';
177
+ };
168
178
  /**
169
179
  * The current member's own branch: `batch/<id>-m<n>-<issue>` (#677) — member
170
180
  * index and issue both in the name, and deterministic from persisted state,
@@ -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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"}
@@ -97,6 +97,7 @@ var __importStar = (this && this.__importStar) || (function () {
97
97
  })();
98
98
  Object.defineProperty(exports, "__esModule", { value: true });
99
99
  exports.MAX_LAND_PUSH_RETRIES = exports.MAX_BATCH_AGENT_RESPAWNS = exports.tailMembersRefusal = void 0;
100
+ exports.resumeUnkillableBatch = resumeUnkillableBatch;
100
101
  exports.memberBranchFor = memberBranchFor;
101
102
  exports.evictMemberAndContinue = evictMemberAndContinue;
102
103
  exports.resumeBlockedGate = resumeBlockedGate;
@@ -264,6 +265,27 @@ function headTreeOf(deps, worktree) {
264
265
  function agentAlive(deps, slot) {
265
266
  return slot.pid !== null && deps.spawnDeps.isAlive(slot.pid, slot.pid_start ?? undefined);
266
267
  }
268
+ /** #1244: this block originates only in executing's member stop ladder.
269
+ * Keep slots and member runs so normal reconciliation finishes that exact
270
+ * decision, rather than redispatching members or discarding landed work.
271
+ * Liveness and transition share the state lock with dispatch/stop.
272
+ */
273
+ function resumeUnkillableBatch(store, spawnDeps, batchId, now = new Date()) {
274
+ return store.withLock((state) => {
275
+ const batch = (0, state_1.findBatch)(state, batchId);
276
+ if (batch?.status !== 'blocked' || batch.blocked_reason !== 'agent-unkillable') {
277
+ throw new types_1.SchedNotFoundError(`Batch ${batchId} is not blocked on agent-unkillable`);
278
+ }
279
+ const live = (0, state_1.slotsForBatch)(state, batchId).filter((slot) => slot.pid !== null && spawnDeps.isAlive(slot.pid, slot.pid_start ?? undefined));
280
+ if (live.length > 0) {
281
+ throw new types_1.SchedNotFoundError(`Batch ${batchId} still has live agent(s) in slot(s) ${live.map((s) => s.id).join(',')} — wait for them to exit before resuming`);
282
+ }
283
+ return {
284
+ state: (0, state_1.transitionBatch)(state, batchId, 'executing', { blocked_reason: null }, now),
285
+ result: { batch: batchId, outcome: 'resumed', from: 'agent-unkillable' },
286
+ };
287
+ });
288
+ }
267
289
  /**
268
290
  * Stop the slot's agent if it is still alive (pid-start-safe: a reused pid is
269
291
  * never signalled). SIGTERM only — no wait, no escalation. A decision that
@@ -2708,8 +2730,8 @@ function currentDispatchStamp(stamp, spawnedAt, now) {
2708
2730
  }
2709
2731
  /**
2710
2732
  * Stop the slot's agent before a decision that must not run alongside it
2711
- * (#844). Returns `true` while it is still alive — the caller waits and
2712
- * decides on a later tick — and `false` once it is gone (or never had a pid).
2733
+ * (#844). Returns waiting while alive, gone once dead (or without a pid),
2734
+ * and unkillable once SIGKILL's bound expires and the batch blocks.
2713
2735
  *
2714
2736
  * The first sight sends SIGTERM, stamps `kill_sent_at` and journals
2715
2737
  * `member-stop-requested` once; each later tick
@@ -2718,7 +2740,7 @@ function currentDispatchStamp(stamp, spawnedAt, now) {
2718
2740
  * `kill-escalated` once (`kill_escalated_at`). Pre-#844 this path re-sent
2719
2741
  * SIGTERM forever, so an agent ignoring it held its member indefinitely.
2720
2742
  */
2721
- function stopAgentBeforeDeciding(deps, config, batchId, memberIssue, slot, now, result) {
2743
+ function stopAgentBeforeDeciding(deps, config, batchId, memberIssue, slot, now, result, reason) {
2722
2744
  if (!agentAlive(deps, slot))
2723
2745
  return 'gone';
2724
2746
  const pid = slot.pid;
@@ -2745,8 +2767,10 @@ function stopAgentBeforeDeciding(deps, config, batchId, memberIssue, slot, now,
2745
2767
  slot: slot.id,
2746
2768
  detail: `agent remained alive ${dispatch_1.KILL_ESCALATION_MS / 1000}s after SIGKILL — blocking for operator`,
2747
2769
  });
2748
- blockBatchForOperator(deps, config, batchId, { reason: 'agent-unkillable' }, now, result);
2749
2770
  }
2771
+ // A crash after the stamp but before the block must finish the block on
2772
+ // the next tick; the stamp deduplicates telemetry, not the transition.
2773
+ blockBatchForOperator(deps, config, batchId, { reason: 'agent-unkillable' }, now, result);
2750
2774
  return 'unkillable';
2751
2775
  }
2752
2776
  const firstEscalation = escalate && currentDispatchStamp(slot.kill_escalated_at, slot.spawned_at, now) === null;
@@ -2768,6 +2792,7 @@ function stopAgentBeforeDeciding(deps, config, batchId, memberIssue, slot, now,
2768
2792
  }
2769
2793
  if (sentAt === null) {
2770
2794
  journalEvent(deps, 'member-stop-requested', unit(batchId), {
2795
+ reason,
2771
2796
  issue: memberIssue,
2772
2797
  pid,
2773
2798
  slot: slot.id,
@@ -2803,7 +2828,7 @@ function repromptWrongProcedure(deps, config, batchId, memberIssue, milestoneAt,
2803
2828
  // dying full-cycle run could still push, post, or open a PR. Signal it and
2804
2829
  // decide on a later tick, once it is gone — and so the re-prompt's
2805
2830
  // `reprompted_at` fence postdates everything it could still post.
2806
- if (stopAgentBeforeDeciding(deps, config, batchId, memberIssue, slot, now, result) !== 'gone')
2831
+ if (stopAgentBeforeDeciding(deps, config, batchId, memberIssue, slot, now, result, 'wrong-procedure') !== 'gone')
2807
2832
  return 'waiting';
2808
2833
  const outcome = deps.store.withLock((s) => {
2809
2834
  const b = (0, state_1.findBatch)(s, batchId);
@@ -2860,7 +2885,7 @@ function wrongProcedureFailure(lastTool, shippedPr = null) {
2860
2885
  function decideWrongProcedure(deps, config, batchId, memberIssue, milestone, slot, release, lastTool, now, result) {
2861
2886
  const shipped = (0, groundtruth_1.wrongProcedureShippedPr)(milestone);
2862
2887
  if (shipped !== null) {
2863
- if (stopAgentBeforeDeciding(deps, config, batchId, memberIssue, slot, now, result) !== 'gone')
2888
+ if (stopAgentBeforeDeciding(deps, config, batchId, memberIssue, slot, now, result, 'wrong-procedure') !== 'gone')
2864
2889
  return 'waiting';
2865
2890
  // #844: the caller's eviction releases the slot in its own write.
2866
2891
  return wrongProcedureFailure(lastTool(), shipped);
@@ -4759,7 +4784,7 @@ function reconcileParallelMemberSlot(deps, config, dispatch, batchId, run, slot,
4759
4784
  // The member's tree may be rebased or removed immediately after its slot
4760
4785
  // is released. Keep this slot draining until the agent is really gone so
4761
4786
  // the #844 SIGKILL bound remains available to a SIGTERM-ignoring agent.
4762
- if (stopAgentBeforeDeciding(deps, config, batchId, run.issue, slot, now, result) !== 'gone')
4787
+ if (stopAgentBeforeDeciding(deps, config, batchId, run.issue, slot, now, result, 'member-complete') !== 'gone')
4763
4788
  return;
4764
4789
  journalEvent(deps, 'external-advance', unit(batchId), {
4765
4790
  issue: run.issue,