@ai-dossier/sched 0.50.0 → 0.51.0
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 +59 -5
- package/dist/batch-dispatch.d.ts +24 -0
- package/dist/batch-dispatch.d.ts.map +1 -1
- package/dist/batch-dispatch.js +229 -14
- package/dist/batch-dispatch.js.map +1 -1
- package/dist/groundtruth.d.ts +11 -0
- package/dist/groundtruth.d.ts.map +1 -1
- package/dist/groundtruth.js +26 -0
- package/dist/groundtruth.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -6
- package/dist/index.js.map +1 -1
- package/dist/recovery.d.ts +5 -0
- package/dist/recovery.d.ts.map +1 -1
- package/dist/recovery.js +11 -6
- package/dist/recovery.js.map +1 -1
- package/dist/scheduler.d.ts.map +1 -1
- package/dist/scheduler.js +8 -2
- package/dist/scheduler.js.map +1 -1
- package/dist/state.d.ts +3 -1
- package/dist/state.d.ts.map +1 -1
- package/dist/state.js +18 -1
- package/dist/state.js.map +1 -1
- package/dist/status.d.ts.map +1 -1
- package/dist/status.js +29 -1
- package/dist/status.js.map +1 -1
- package/dist/types.d.ts +24 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +5 -1
- package/dist/types.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -31,7 +31,7 @@ ai-dossier sched start --once # a single reconcile+refill tick (cron-style)
|
|
|
31
31
|
ai-dossier sched status # queue (+pr/cleanup), engine lease, slots, batches, blocked/failed; --anchors adds the ledger anchor sweep (#768) plus orphaned anchors no longer in state.batches (#790)
|
|
32
32
|
ai-dossier sched pause # prevent every new agent process; live units keep running
|
|
33
33
|
ai-dossier sched resume
|
|
34
|
-
ai-dossier sched stop --issue 42 # terminate one full-cycle agent and record it stopped (no recovery)
|
|
34
|
+
ai-dossier sched stop --issue 42 # terminate one full-cycle agent (or stop a parked batch member, #832) and record it stopped (no recovery)
|
|
35
35
|
ai-dossier sched stop --batch b1 # terminate every batch agent (incl. parallel members) and stop unfinished members
|
|
36
36
|
ai-dossier sched abandon --issue 42 --reason "operator abort"
|
|
37
37
|
ai-dossier sched abandon --batch b1 # dissolve; every unshipped member — parked and validated ones included — requeues as full-cycle
|
|
@@ -53,8 +53,10 @@ directory directly, reconstructing costs from the raw dispatch logs rather than
|
|
|
53
53
|
`pause` prevents every new agent process, including escalation and same-tier recovery
|
|
54
54
|
takeovers; it does not terminate agents that are already running. `stop --issue <n>` is the
|
|
55
55
|
single-command stop path: it PID-start-safely terminates that issue's live agent, releases its
|
|
56
|
-
slot, and records a terminal `stopped` outcome that will not recover or escalate.
|
|
57
|
-
|
|
56
|
+
slot, and records a terminal `stopped` outcome that will not recover or escalate. A slot-mode
|
|
57
|
+
member still in a live batch (pending, in work, or validated) must instead be stopped through
|
|
58
|
+
`stop --batch <id>` — a parked (`handed-back`/`evicted`) member can be stopped on its own (#832) —
|
|
59
|
+
which terminates every agent
|
|
58
60
|
the batch holds (its own slot plus one per running parallel member, `batch:<id>#<issue>`) and atomically stops the batch and its unfinished members. `abandon` only records failure
|
|
59
61
|
and releases the slot; it intentionally does not terminate its process.
|
|
60
62
|
|
|
@@ -247,7 +249,9 @@ where every mechanical supervision decision is code, not remembered prose:
|
|
|
247
249
|
`reconcileRecovering`, held in `recovering` until `sched resume`) and every batch
|
|
248
250
|
respawn wedge in `runBatchTick` (tail, member continuation, fix, report, and the
|
|
249
251
|
`ready` → `claimAndSetup` claim — `runValidate`'s local suite run is unaffected) stop
|
|
250
|
-
respawning into the wall, which previously ignored `paused` entirely.
|
|
252
|
+
respawning into the wall, which previously ignored `paused` entirely. (Unverified
|
|
253
|
+
tail/report exits that are NOT API errors are capped separately since #832 —
|
|
254
|
+
`respawn-cap:tail`, or a merged batch closed without its report.) `dispatch-
|
|
251
255
|
health.ts` is its own module (not `engine.ts`) specifically so it is shared by BOTH
|
|
252
256
|
dispatch paths without `engine.ts` and `batch-dispatch.ts` importing each other.
|
|
253
257
|
Batch dispatch logs (per-role, append-mode) are fenced to `log_offset_at_spawn`,
|
|
@@ -1011,6 +1015,51 @@ Dissolve never throws validated work away:
|
|
|
1011
1015
|
State schema 1.24.0: `IssueStatus` gains `handed-back`; `EvictionRecord` gains optional
|
|
1012
1016
|
`kind`/`branch`, `FailureEvidence` optional `branch`.
|
|
1013
1017
|
|
|
1018
|
+
### The tail runs over landed members; a tail verdict is final (#832)
|
|
1019
|
+
|
|
1020
|
+
- **Landed members only.** The tail (aggregate review + integrate/ship) is dispatched with
|
|
1021
|
+
`{members}` = the batch's validated members (`validatedMembersOf`), never the raw
|
|
1022
|
+
`batch.members`, which still lists members that were stopped, handed back, evicted,
|
|
1023
|
+
parked or requeued. Before claiming a slot the engine makes the tail's own two-way check
|
|
1024
|
+
for free (`tailMembersRefusal`): no landed members blocks `no-landed-members`; when
|
|
1025
|
+
`batch.ranges` records boundary commits, a landed member with none blocks
|
|
1026
|
+
`members-mismatch:no-boundary-commit-<n>`, and a boundary commit for a member that did
|
|
1027
|
+
not land blocks `members-mismatch:unlanded-boundary-commit-<n>`. No tail agent is spent.
|
|
1028
|
+
- **`sched stop --issue` on a parked member.** A `handed-back`/`evicted` member of a live
|
|
1029
|
+
batch can be stopped on its own (the operator's cleanup after a hand-back). It moves to
|
|
1030
|
+
`stopped` and keeps its exit record, so it stays out of the landed set. A member still in
|
|
1031
|
+
the batch (pending, in work, or validated) is still refused: use `sched stop --batch`.
|
|
1032
|
+
- **A blocked tail blocks the batch.** A `batch-review` or `batch-ship` `status=blocked`
|
|
1033
|
+
milestone posted by THIS tail dispatch (fenced by the slot's `spawned_at`, which tail and
|
|
1034
|
+
report spawns now stamp; a slot from an older engine with no `spawned_at` skips this
|
|
1035
|
+
check and falls to the capped path below) releases the slot and blocks the batch
|
|
1036
|
+
`tail-blocked:<reason>` once, in one lock. It is never read as an unverified exit to
|
|
1037
|
+
respawn. The agent's milestone is already on the anchor, so the engine posts none. The
|
|
1038
|
+
reason is reduced to a slug.
|
|
1039
|
+
- **Per-phase respawn cap.** `BatchEntry.agent_exits` (`{ phase: 'tail'|'report', count }`,
|
|
1040
|
+
schema 1.26.0) counts unverified exits of the phase's agent; each is journaled with its
|
|
1041
|
+
position (`unverified exit 2/3 (log: …)`). #629 API-error exits are not counted: they
|
|
1042
|
+
have their own pause. After `MAX_BATCH_AGENT_RESPAWNS` (2) respawns, the next unverified
|
|
1043
|
+
exit ends the phase:
|
|
1044
|
+
- **tail** → the batch blocks `respawn-cap:tail`;
|
|
1045
|
+
- **report** → the batch (whose PR is already merged and deployed) closes
|
|
1046
|
+
`deployed → reported → done` without its report, journaling `report-failed
|
|
1047
|
+
respawn-cap:report — batch closed without its report`. A block would not hold there:
|
|
1048
|
+
`reconcileStaleBlockedBatches` walks a blocked batch with a merged PR straight back to
|
|
1049
|
+
`deployed`, and the report agent would respawn.
|
|
1050
|
+
|
|
1051
|
+
Verified progress (review done, park, report done) clears the count, and so does blocking
|
|
1052
|
+
the batch. A spawn that finds the count already spent (a crash between the count and the
|
|
1053
|
+
block) refuses instead of spawning. Before #832, b-20260924-04's tail was respawned four
|
|
1054
|
+
times at the strong tier (~375k tokens) until an operator stopped the batch.
|
|
1055
|
+
- **Clearing these blocks.** `no-landed-members`, `members-mismatch:*`, `tail-blocked:*` and
|
|
1056
|
+
`respawn-cap:tail` have no `sched resume --batch` recheck. `sched status` names the
|
|
1057
|
+
evidence (the anchor's blocked milestone, or the tail log) and the exits: fix the cause
|
|
1058
|
+
on the integration branch and open its PR by hand (`gh pr create --head <branch> --base
|
|
1059
|
+
<base>`; once it merges, `reconcileStaleBlockedBatches` settles the batch), or `sched
|
|
1060
|
+
abandon --batch <id>` (requeues the landed members full-cycle; for `no-landed-members`
|
|
1061
|
+
there is nothing to lose).
|
|
1062
|
+
|
|
1014
1063
|
## API surface
|
|
1015
1064
|
|
|
1016
1065
|
```ts
|
|
@@ -1183,6 +1232,9 @@ import {
|
|
|
1183
1232
|
isMemberComplete, isMemberBlocked, // member milestone predicates (member-trail gated: mode=slot or batch=<id>, #677)
|
|
1184
1233
|
isBatchTailParked, // batch-ship awaiting-merge + pr= — the batch park signal
|
|
1185
1234
|
isBatchPhaseDone, // <phase> done on the anchor (batch-review/batch-report)
|
|
1235
|
+
batchPhaseBlockedReason, // #832: slugged reason of a `<phase> blocked` anchor milestone posted by THIS dispatch; null otherwise
|
|
1236
|
+
MAX_BATCH_AGENT_RESPAWNS, // #832: tail/report respawns after unverified exits before the phase ends (2)
|
|
1237
|
+
type BatchAgentExits, type BatchAgentPhase, // #832: BatchEntry.agent_exits ({ phase: 'tail'|'report', count })
|
|
1186
1238
|
batchOfUnit, // batch:<id> → <id>; null for issue units or malformed ids
|
|
1187
1239
|
} from '@ai-dossier/sched';
|
|
1188
1240
|
```
|
|
@@ -1314,7 +1366,7 @@ after-the-fact recovery, not a missing-data bug.
|
|
|
1314
1366
|
queue data.
|
|
1315
1367
|
- **Schema**: state/config files from #460 (schema 1.0.0), #464 (1.1.0), #468 (1.2.0),
|
|
1316
1368
|
#472 (1.3.0), #500 (1.4.0), #505 (1.5.0), #504 (1.6.0), #523 (1.7.0) and #524 (1.8.0)
|
|
1317
|
-
load and migrate to the current schema (1.
|
|
1369
|
+
load and migrate to the current schema (1.26.0 — 1.24.0 was #810: no backfill,
|
|
1318
1370
|
`kind`/`branch` optional, absent = an `evicted` record with no branch) automatically
|
|
1319
1371
|
(slot `branch`/`last_head`/`pid_start`, slot `role` (inferred from the
|
|
1320
1372
|
unit's queue entry, with the persisted `phase` as a fallback — #500), entry
|
|
@@ -1333,6 +1385,8 @@ after-the-fact recovery, not a missing-data bug.
|
|
|
1333
1385
|
`IssueStatus` gains `handed-back`, `EvictionRecord`/`FailureEvidence` gain optional
|
|
1334
1386
|
`kind`/`branch` — #810, schema 1.24.0; batch `pr_detect_ambiguous_reason`/`_since`/`_ticks`
|
|
1335
1387
|
backfill to `null`/`null`/`0` — #789, schema 1.25.0).
|
|
1388
|
+
Schema 1.26.0 (#832): `BatchEntry` gains `agent_exits` (the per-phase tail/report
|
|
1389
|
+
respawn counter); `null` is backfilled on load.
|
|
1336
1390
|
- **`max_slots`** bounds live units (`assigned | running | recovering`); dependency
|
|
1337
1391
|
edges gate readiness — an issue with an unmerged dependency, and a batch behind an
|
|
1338
1392
|
unmerged batch, are never runnable.
|
package/dist/batch-dispatch.d.ts
CHANGED
|
@@ -172,6 +172,20 @@ export interface MemberFailure {
|
|
|
172
172
|
* scheduler lands it there.
|
|
173
173
|
*/
|
|
174
174
|
export declare function memberBranchFor(batchId: string, memberIndex: number, issue: number): string;
|
|
175
|
+
/**
|
|
176
|
+
* #832: why the tail must NOT be dispatched over `landed`, or null when it may.
|
|
177
|
+
* The deterministic half of the check the tail agent itself makes before
|
|
178
|
+
* integrating (the members it is told about and the members it derives from
|
|
179
|
+
* the integration branch's boundary commits must agree) — made here for free
|
|
180
|
+
* instead of by a strong-tier agent that then exits `members-mismatch`.
|
|
181
|
+
* `batch.ranges` is the recorded boundary attribution (recomputed at every
|
|
182
|
+
* landing); when it is empty nothing was attributed, so there is nothing to
|
|
183
|
+
* cross-check. Invariant this relies on: `ranges` names only landed members —
|
|
184
|
+
* every exit path that reverts a member also drops its range
|
|
185
|
+
* (`recovery.ts`'s partial dissolve); a path that re-derived `ranges` from
|
|
186
|
+
* `git log` after a revert would make this refuse a healthy batch.
|
|
187
|
+
*/
|
|
188
|
+
export declare function tailMembersRefusal(batch: BatchEntry, landed: readonly number[]): string | null;
|
|
175
189
|
/**
|
|
176
190
|
* Evict the current member and either dissolve, or continue the batch via
|
|
177
191
|
* `advanceMemberOrValidate` — the shared tail of both member-failure rails
|
|
@@ -238,6 +252,16 @@ export declare function resumeBlockedGate(deps: BatchDispatchDeps, config: Sched
|
|
|
238
252
|
blockedBy?: 'inconclusive' | 'unevidenced-failure';
|
|
239
253
|
detail?: string;
|
|
240
254
|
};
|
|
255
|
+
/**
|
|
256
|
+
* #832: how many times one tail-work phase's agent (`tail` or `report`) is
|
|
257
|
+
* RESPAWNED after exiting unverified — the first dispatch plus this many
|
|
258
|
+
* respawns; the next unverified exit ends the phase instead. The #629 class:
|
|
259
|
+
* the tick loop's "same wedge" arm respawns a dead tail/report agent on its
|
|
260
|
+
* own, so without a cap an agent that exits without a verdict is
|
|
261
|
+
* redispatched forever (b-20260924-04: the strong-tier tail four times,
|
|
262
|
+
* ~375k tokens, stopped only by hand).
|
|
263
|
+
*/
|
|
264
|
+
export declare const MAX_BATCH_AGENT_RESPAWNS = 2;
|
|
241
265
|
/**
|
|
242
266
|
* #686: reconcile a `blocked` batch whose work demonstrably shipped anyway.
|
|
243
267
|
* `reconcilePrWatch` above only watches `awaiting-merge` batches, and
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"batch-dispatch.d.ts","sourceRoot":"","sources":["../src/batch-dispatch.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8DG;AAUH,OAAO,EAIL,KAAK,YAAY,EAGjB,KAAK,gBAAgB,EAGtB,MAAM,gBAAgB,CAAC;AAUxB,OAAO,EAaL,KAAK,gBAAgB,EAGrB,KAAK,SAAS,EACf,MAAM,YAAY,CAAC;AAEpB,OAAO,
|
|
1
|
+
{"version":3,"file":"batch-dispatch.d.ts","sourceRoot":"","sources":["../src/batch-dispatch.ts"],"names":[],"mappings":"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"}
|
package/dist/batch-dispatch.js
CHANGED
|
@@ -96,7 +96,9 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
96
96
|
};
|
|
97
97
|
})();
|
|
98
98
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
99
|
+
exports.MAX_BATCH_AGENT_RESPAWNS = void 0;
|
|
99
100
|
exports.memberBranchFor = memberBranchFor;
|
|
101
|
+
exports.tailMembersRefusal = tailMembersRefusal;
|
|
100
102
|
exports.evictMemberAndContinue = evictMemberAndContinue;
|
|
101
103
|
exports.resumeBlockedGate = resumeBlockedGate;
|
|
102
104
|
exports.reconcileStaleBlockedBatches = reconcileStaleBlockedBatches;
|
|
@@ -1309,6 +1311,50 @@ function spawnMemberContinuation(deps, config, dispatch, batchId, now, result) {
|
|
|
1309
1311
|
}
|
|
1310
1312
|
claimAndSpawn(deps, config, batchId, 'member', now, (nextState, slot) => spawnMember(deps, dispatch, nextState, slot, batchId, now, result, member));
|
|
1311
1313
|
}
|
|
1314
|
+
/**
|
|
1315
|
+
* #832: why the tail must NOT be dispatched over `landed`, or null when it may.
|
|
1316
|
+
* The deterministic half of the check the tail agent itself makes before
|
|
1317
|
+
* integrating (the members it is told about and the members it derives from
|
|
1318
|
+
* the integration branch's boundary commits must agree) — made here for free
|
|
1319
|
+
* instead of by a strong-tier agent that then exits `members-mismatch`.
|
|
1320
|
+
* `batch.ranges` is the recorded boundary attribution (recomputed at every
|
|
1321
|
+
* landing); when it is empty nothing was attributed, so there is nothing to
|
|
1322
|
+
* cross-check. Invariant this relies on: `ranges` names only landed members —
|
|
1323
|
+
* every exit path that reverts a member also drops its range
|
|
1324
|
+
* (`recovery.ts`'s partial dissolve); a path that re-derived `ranges` from
|
|
1325
|
+
* `git log` after a revert would make this refuse a healthy batch.
|
|
1326
|
+
*/
|
|
1327
|
+
function tailMembersRefusal(batch, landed) {
|
|
1328
|
+
if (landed.length === 0)
|
|
1329
|
+
return 'no-landed-members';
|
|
1330
|
+
if (batch.ranges.length === 0)
|
|
1331
|
+
return null;
|
|
1332
|
+
const attributed = new Set(batch.ranges.map((r) => r.issue));
|
|
1333
|
+
const missing = landed.filter((issue) => !attributed.has(issue));
|
|
1334
|
+
if (missing.length > 0)
|
|
1335
|
+
return `members-mismatch:no-boundary-commit-${missing.join(',')}`;
|
|
1336
|
+
// The tail derives members from the boundary commits too, and refuses when
|
|
1337
|
+
// the two lists disagree in EITHER direction (review-issue Aggregate Step 1).
|
|
1338
|
+
const landedSet = new Set(landed);
|
|
1339
|
+
const unlanded = [...attributed].filter((issue) => !landedSet.has(issue));
|
|
1340
|
+
return unlanded.length > 0
|
|
1341
|
+
? `members-mismatch:unlanded-boundary-commit-${unlanded.join(',')}`
|
|
1342
|
+
: null;
|
|
1343
|
+
}
|
|
1344
|
+
/**
|
|
1345
|
+
* #832: the one pre-dispatch verdict for a tail — checked before a slot is
|
|
1346
|
+
* claimed and again under the claim's lock. A tail whose respawns are spent
|
|
1347
|
+
* (a crash landed between counting the exit that spent them and blocking the
|
|
1348
|
+
* batch) is refused, as is a members mismatch. Only a batch in a tail phase
|
|
1349
|
+
* is judged; anything else is not the tail's to refuse.
|
|
1350
|
+
*/
|
|
1351
|
+
function tailDispatchRefusal(state, batch) {
|
|
1352
|
+
if (batch.status !== 'reviewing' && batch.status !== 'shipping')
|
|
1353
|
+
return null;
|
|
1354
|
+
if (agentRespawnsSpent(batch, 'tail'))
|
|
1355
|
+
return 'respawn-cap:tail';
|
|
1356
|
+
return tailMembersRefusal(batch, (0, state_1.validatedMembersOf)(state, batch));
|
|
1357
|
+
}
|
|
1312
1358
|
/**
|
|
1313
1359
|
* Tail/report/fix dispatches (this function, `spawnReportAgent`,
|
|
1314
1360
|
* `reconcileFixSlot`) are NOT recorded live to `runs.jsonl` — only member
|
|
@@ -1317,11 +1363,27 @@ function spawnMemberContinuation(deps, config, dispatch, batchId, now, result) {
|
|
|
1317
1363
|
* the only way to see their cost today, reconstructed from the raw log
|
|
1318
1364
|
* after the fact. Wiring in live recording for these later means repeating
|
|
1319
1365
|
* `spawnMember`'s own #564 fix first: none of these three spawn functions'
|
|
1320
|
-
* patches
|
|
1366
|
+
* patches stamped `SlotEntry.spawned_at` either, so a `recordXRunLog` guarded
|
|
1321
1367
|
* on `spawned_at !== null` (mirroring `recordMemberRunLog`) would silently
|
|
1322
|
-
* no-op forever, exactly like the original bug.
|
|
1368
|
+
* no-op forever, exactly like the original bug. #832 made the tail and report
|
|
1369
|
+
* spawns stamp it (their hand-back fence needs it); the fix agent still does
|
|
1370
|
+
* not.
|
|
1323
1371
|
*/
|
|
1324
1372
|
function spawnTailAgent(deps, config, dispatch, batchId, now, result) {
|
|
1373
|
+
// #832: the tail runs over the LANDED members only — never `batch.members`,
|
|
1374
|
+
// which still lists a member that was stopped, handed back, evicted or
|
|
1375
|
+
// parked. A refusal blocks the batch before any agent is spawned (and
|
|
1376
|
+
// before a slot is claimed — `blockBatchForOperator` posts a milestone, an
|
|
1377
|
+
// exec that must not run under the store lock).
|
|
1378
|
+
const before = deps.store.load();
|
|
1379
|
+
const pending = (0, state_1.findBatch)(before, batchId);
|
|
1380
|
+
if (pending) {
|
|
1381
|
+
const refusal = tailDispatchRefusal(before, pending);
|
|
1382
|
+
if (refusal !== null) {
|
|
1383
|
+
blockBatchForOperator(deps, config, batchId, { reason: refusal, milestonePhase: 'batch-review' }, now, result);
|
|
1384
|
+
return;
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1325
1387
|
claimAndSpawn(deps, config, batchId, 'reviewing', now, (state, slot) => {
|
|
1326
1388
|
const batch = (0, state_1.findBatch)(state, batchId);
|
|
1327
1389
|
if (!batch || batch.worktree === null || batch.anchor === null) {
|
|
@@ -1331,9 +1393,20 @@ function spawnTailAgent(deps, config, dispatch, batchId, now, result) {
|
|
|
1331
1393
|
});
|
|
1332
1394
|
return releaseSlot(state, batchId, now);
|
|
1333
1395
|
}
|
|
1396
|
+
const landed = (0, state_1.validatedMembersOf)(state, batch);
|
|
1397
|
+
const lateRefusal = tailDispatchRefusal(state, batch);
|
|
1398
|
+
if (lateRefusal !== null) {
|
|
1399
|
+
// The state changed between the pre-check and the lock (a concurrent
|
|
1400
|
+
// stop/park) — release, and the next tick's pre-check (reading the same
|
|
1401
|
+
// persisted state) blocks the batch with this reason.
|
|
1402
|
+
journalEvent(deps, 'slot-released', unit(batchId), {
|
|
1403
|
+
detail: `tail not dispatched: ${lateRefusal} (state changed before the claim; the next tick blocks the batch)`,
|
|
1404
|
+
});
|
|
1405
|
+
return releaseSlot(state, batchId, now);
|
|
1406
|
+
}
|
|
1334
1407
|
const spawnSpec = (0, dispatch_1.resolveTierSpawn)(dispatch, 'strong', batch.anchor);
|
|
1335
1408
|
const cmd = spawnSpec.cmd;
|
|
1336
|
-
const prompt = (0, dispatch_1.buildBatchTailPrompt)(dispatch.batchTailPrompt, batchId, batch.anchor,
|
|
1409
|
+
const prompt = (0, dispatch_1.buildBatchTailPrompt)(dispatch.batchTailPrompt, batchId, batch.anchor, landed, batch.worktree);
|
|
1337
1410
|
const logFile = (0, dispatch_1.batchTailLogPath)(deps.store.runsDir, batchId);
|
|
1338
1411
|
// #629: fences `handleDeadDispatchApiError`'s classification to this
|
|
1339
1412
|
// dispatch's slice — see `spawnMember`'s identical comment.
|
|
@@ -1356,6 +1429,14 @@ function spawnTailAgent(deps, config, dispatch, batchId, now, result) {
|
|
|
1356
1429
|
phase: 'reviewing',
|
|
1357
1430
|
last_progress_at: now.toISOString(),
|
|
1358
1431
|
log_offset_at_spawn: logOffset,
|
|
1432
|
+
// #832: the fence `reconcileTailSlot` reads — without it a
|
|
1433
|
+
// `batch-review blocked` left on the anchor by an EARLIER dispatch
|
|
1434
|
+
// (or a previous batch on the same anchor, #605) would block this one.
|
|
1435
|
+
// The fence tolerates `DISPATCH_FENCE_TOLERANCE_MS` (60s) of clock skew,
|
|
1436
|
+
// so a blocked milestone posted less than a minute before this spawn
|
|
1437
|
+
// still counts — only a resume that re-dispatches the tail that fast
|
|
1438
|
+
// could trip it.
|
|
1439
|
+
spawned_at: now.toISOString(),
|
|
1359
1440
|
};
|
|
1360
1441
|
const next = slot.status === 'assigned' ? (0, state_1.transitionSlot)(state, slot.id, 'running', patch, now) : state;
|
|
1361
1442
|
deps.journal.append((0, journal_1.unitEvent)('spawned', unit(batchId), {
|
|
@@ -1370,6 +1451,13 @@ function spawnTailAgent(deps, config, dispatch, batchId, now, result) {
|
|
|
1370
1451
|
});
|
|
1371
1452
|
}
|
|
1372
1453
|
function spawnReportAgent(deps, config, dispatch, batchId, now, result) {
|
|
1454
|
+
// #832: respawns already spent (a crash landed between counting the exit
|
|
1455
|
+
// that spent them and closing the batch) — close it now, never spawn.
|
|
1456
|
+
const pending = (0, state_1.findBatch)(deps.store.load(), batchId);
|
|
1457
|
+
if (pending && pending.status === 'deployed' && agentRespawnsSpent(pending, 'report')) {
|
|
1458
|
+
closeWithoutReport(deps, batchId, 'respawn-cap:report', now, result);
|
|
1459
|
+
return;
|
|
1460
|
+
}
|
|
1373
1461
|
claimAndSpawn(deps, config, batchId, 'report', now, (state, slot) => {
|
|
1374
1462
|
const batch = (0, state_1.findBatch)(state, batchId);
|
|
1375
1463
|
if (!batch || batch.anchor === null) {
|
|
@@ -1406,6 +1494,8 @@ function spawnReportAgent(deps, config, dispatch, batchId, now, result) {
|
|
|
1406
1494
|
phase: 'report',
|
|
1407
1495
|
last_progress_at: now.toISOString(),
|
|
1408
1496
|
log_offset_at_spawn: logOffset,
|
|
1497
|
+
// #832: the fence `reconcileReportSlot`'s hand-back check reads.
|
|
1498
|
+
spawned_at: now.toISOString(),
|
|
1409
1499
|
};
|
|
1410
1500
|
const next = slot.status === 'assigned'
|
|
1411
1501
|
? (0, state_1.transitionSlot)(state, slot.id, 'running', patchState, now)
|
|
@@ -2565,7 +2655,98 @@ function reconcileFixSlot(deps, config, batchId, slot, now, result) {
|
|
|
2565
2655
|
}
|
|
2566
2656
|
evictOffender(deps, config, batchId, offenderRecord.issue, 'overlap', now, result);
|
|
2567
2657
|
}
|
|
2568
|
-
|
|
2658
|
+
/**
|
|
2659
|
+
* #832: how many times one tail-work phase's agent (`tail` or `report`) is
|
|
2660
|
+
* RESPAWNED after exiting unverified — the first dispatch plus this many
|
|
2661
|
+
* respawns; the next unverified exit ends the phase instead. The #629 class:
|
|
2662
|
+
* the tick loop's "same wedge" arm respawns a dead tail/report agent on its
|
|
2663
|
+
* own, so without a cap an agent that exits without a verdict is
|
|
2664
|
+
* redispatched forever (b-20260924-04: the strong-tier tail four times,
|
|
2665
|
+
* ~375k tokens, stopped only by hand).
|
|
2666
|
+
*/
|
|
2667
|
+
exports.MAX_BATCH_AGENT_RESPAWNS = 2;
|
|
2668
|
+
/** #832: whether `batch` has already spent its respawns for `phase`. */
|
|
2669
|
+
function agentRespawnsSpent(batch, phase) {
|
|
2670
|
+
return (batch.agent_exits !== null &&
|
|
2671
|
+
batch.agent_exits.phase === phase &&
|
|
2672
|
+
batch.agent_exits.count > exports.MAX_BATCH_AGENT_RESPAWNS);
|
|
2673
|
+
}
|
|
2674
|
+
/**
|
|
2675
|
+
* #832: count one unverified exit of `phase`'s agent, release its slot, and
|
|
2676
|
+
* journal the exit with its position against the cap — one lock, so the count
|
|
2677
|
+
* and the release cannot come apart. Returns `'capped'` when this exit spent
|
|
2678
|
+
* the phase's last respawn (the caller ends the phase), else `'respawn'`.
|
|
2679
|
+
* The count is per phase — another phase's exits start it over — and verified
|
|
2680
|
+
* progress clears it (`clearAgentExits`).
|
|
2681
|
+
*/
|
|
2682
|
+
function releaseAndCountAgentExit(deps, batchId, phase, failed, now) {
|
|
2683
|
+
const count = deps.store.withLock((s) => {
|
|
2684
|
+
const b = (0, state_1.findBatch)(s, batchId);
|
|
2685
|
+
const prior = b?.agent_exits;
|
|
2686
|
+
const next = prior && prior.phase === phase ? prior.count + 1 : 1;
|
|
2687
|
+
let n = releaseSlot(s, batchId, now);
|
|
2688
|
+
if (b)
|
|
2689
|
+
n = (0, state_1.patchBatch)(n, batchId, { agent_exits: { phase, count: next } }, now);
|
|
2690
|
+
return { state: n, result: next };
|
|
2691
|
+
});
|
|
2692
|
+
journalEvent(deps, failed.event, unit(batchId), {
|
|
2693
|
+
...failed.extra,
|
|
2694
|
+
detail: `${String(failed.extra.detail ?? 'unverified exit')} ${count}/${exports.MAX_BATCH_AGENT_RESPAWNS + 1} (log: ${failed.log})`,
|
|
2695
|
+
});
|
|
2696
|
+
return count > exports.MAX_BATCH_AGENT_RESPAWNS ? 'capped' : 'respawn';
|
|
2697
|
+
}
|
|
2698
|
+
/** #832: verified progress in a tail-work phase — the respawn count starts over. */
|
|
2699
|
+
function clearAgentExits(s, batchId, now) {
|
|
2700
|
+
const b = (0, state_1.findBatch)(s, batchId);
|
|
2701
|
+
return b && b.agent_exits !== null ? (0, state_1.patchBatch)(s, batchId, { agent_exits: null }, now) : s;
|
|
2702
|
+
}
|
|
2703
|
+
/**
|
|
2704
|
+
* #832: the tail posted its own `batch-review`/`batch-ship blocked` — release
|
|
2705
|
+
* its slot and block the batch with that reason in ONE lock (no milestone to
|
|
2706
|
+
* post: the agent's is already the anchor's record, so no exec runs under the
|
|
2707
|
+
* lock), so a crash can never leave the slot released with the batch still
|
|
2708
|
+
* `reviewing` for the wedge arm to respawn. The agent decided; respawning it
|
|
2709
|
+
* would only get the same answer again.
|
|
2710
|
+
*/
|
|
2711
|
+
function blockOnTailHandBack(deps, config, batch, reason, now, result) {
|
|
2712
|
+
const rDeps = recoveryDeps(deps, config, batch, now);
|
|
2713
|
+
deps.store.withLock((s) => {
|
|
2714
|
+
const released = releaseSlot(s, batch.id, now);
|
|
2715
|
+
const b = (0, state_1.findBatch)(released, batch.id);
|
|
2716
|
+
if (!b || types_1.TERMINAL_BATCH_STATUSES.has(b.status) || b.status === 'blocked') {
|
|
2717
|
+
return { state: released, result: undefined };
|
|
2718
|
+
}
|
|
2719
|
+
const blocked = (0, recovery_1.blockBatch)(released, batch.id, { reason, milestonePhase: 'batch-review', milestoneAlreadyPosted: true }, rDeps);
|
|
2720
|
+
return { state: blocked.state, result: undefined };
|
|
2721
|
+
});
|
|
2722
|
+
result.failed.push(unit(batch.id));
|
|
2723
|
+
}
|
|
2724
|
+
/**
|
|
2725
|
+
* #832: close a MERGED batch whose report agent spent its respawns, without
|
|
2726
|
+
* its report. The report is the batch's summary, not its work — the PR is
|
|
2727
|
+
* merged and deployed — so the batch goes `deployed → reported → done` and
|
|
2728
|
+
* is torn down like any finished batch. Blocking it instead would not stop
|
|
2729
|
+
* anything: `reconcileStaleBlockedBatches` walks a blocked batch with a
|
|
2730
|
+
* merged PR straight back to `deployed`, and the report agent would respawn.
|
|
2731
|
+
*/
|
|
2732
|
+
function closeWithoutReport(deps, batchId, why, now, result) {
|
|
2733
|
+
const closed = deps.store.withLock((s) => {
|
|
2734
|
+
const b = (0, state_1.findBatch)(s, batchId);
|
|
2735
|
+
if (!b || b.status !== 'deployed')
|
|
2736
|
+
return { state: s, result: false };
|
|
2737
|
+
let n = (0, state_1.transitionBatch)(s, batchId, 'reported', { agent_exits: null }, now);
|
|
2738
|
+
n = (0, state_1.transitionBatch)(n, batchId, 'done', {}, now);
|
|
2739
|
+
return { state: n, result: true };
|
|
2740
|
+
});
|
|
2741
|
+
if (!closed)
|
|
2742
|
+
return;
|
|
2743
|
+
journalEvent(deps, 'report-failed', unit(batchId), {
|
|
2744
|
+
detail: `${why} — batch closed without its report (the PR is merged); post the batch report by hand if one is wanted`,
|
|
2745
|
+
});
|
|
2746
|
+
result.failed.push(unit(batchId));
|
|
2747
|
+
teardownBatch(deps, batchId);
|
|
2748
|
+
}
|
|
2749
|
+
function reconcileTailSlot(deps, config, batchId, slot, now, result) {
|
|
2569
2750
|
const state = deps.store.load();
|
|
2570
2751
|
const batch = (0, state_1.findBatch)(state, batchId);
|
|
2571
2752
|
if (!batch || batch.anchor === null)
|
|
@@ -2580,10 +2761,28 @@ function reconcileTailSlot(deps, batchId, slot, now, result) {
|
|
|
2580
2761
|
const b = (0, state_1.findBatch)(s, batchId);
|
|
2581
2762
|
if (!b || b.status !== 'reviewing')
|
|
2582
2763
|
return { state: s, result: undefined };
|
|
2583
|
-
|
|
2764
|
+
const n = clearAgentExits(s, batchId, now);
|
|
2765
|
+
return { state: (0, state_1.transitionBatch)(n, batchId, 'shipping', {}, now), result: undefined };
|
|
2584
2766
|
});
|
|
2585
2767
|
return;
|
|
2586
2768
|
}
|
|
2769
|
+
// #832: the tail handed back (`batch-review`/`batch-ship blocked`, e.g.
|
|
2770
|
+
// `members-mismatch`) — block the batch with its reason, never respawn.
|
|
2771
|
+
// Checked before liveness: the agent is done deciding whether or not its
|
|
2772
|
+
// process has exited yet, and the block releases its slot. Only for a slot
|
|
2773
|
+
// that recorded its dispatch time: a tail spawned before #832 stamped
|
|
2774
|
+
// `spawned_at` has no fence, and an unfenced read would let an older
|
|
2775
|
+
// `blocked` milestone on the anchor block this dispatch — that exit falls
|
|
2776
|
+
// through to the (capped) unverified path instead.
|
|
2777
|
+
const handedBack = slot.spawned_at === null
|
|
2778
|
+
? null
|
|
2779
|
+
: (0, groundtruth_1.batchPhaseBlockedReason)(milestone, ['batch-review', 'batch-ship'], slot.spawned_at);
|
|
2780
|
+
if (handedBack !== null) {
|
|
2781
|
+
if (slot.pid !== null && !dead)
|
|
2782
|
+
deps.spawnDeps.kill(slot.pid, slot.pid_start ?? undefined);
|
|
2783
|
+
blockOnTailHandBack(deps, config, batch, `tail-blocked:${handedBack}`, now, result);
|
|
2784
|
+
return;
|
|
2785
|
+
}
|
|
2587
2786
|
if ((0, groundtruth_1.isBatchTailParked)(milestone)) {
|
|
2588
2787
|
const pr = (0, groundtruth_1.prOfMilestone)(milestone);
|
|
2589
2788
|
journalEvent(deps, 'pr-parked', unit(batchId), { pr: pr ?? undefined });
|
|
@@ -2595,6 +2794,7 @@ function reconcileTailSlot(deps, batchId, slot, now, result) {
|
|
|
2595
2794
|
const b = (0, state_1.findBatch)(n, batchId);
|
|
2596
2795
|
if (!b)
|
|
2597
2796
|
return { state: n, result: undefined };
|
|
2797
|
+
n = clearAgentExits(n, batchId, now);
|
|
2598
2798
|
n = b.status === 'reviewing' ? (0, state_1.transitionBatch)(n, batchId, 'shipping', {}, now) : n;
|
|
2599
2799
|
n = (0, state_1.transitionBatch)(n, batchId, 'awaiting-merge', { pr }, now);
|
|
2600
2800
|
return { state: n, result: undefined };
|
|
@@ -2610,12 +2810,20 @@ function reconcileTailSlot(deps, batchId, slot, now, result) {
|
|
|
2610
2810
|
if (handleDeadDispatchApiError(deps, batchId, (0, dispatch_1.batchTailLogPath)(deps.store.runsDir, batchId), slot.log_offset_at_spawn ?? 0, now)) {
|
|
2611
2811
|
return;
|
|
2612
2812
|
}
|
|
2613
|
-
|
|
2614
|
-
|
|
2813
|
+
// #832: counted — once the respawns are spent the batch blocks instead.
|
|
2814
|
+
const outcome = releaseAndCountAgentExit(deps, batchId, 'tail', {
|
|
2815
|
+
event: 'unit-failed',
|
|
2816
|
+
extra: { reason: 'tail-agent-exited-unverified' },
|
|
2817
|
+
log: (0, dispatch_1.batchTailLogPath)(deps.store.runsDir, batchId),
|
|
2818
|
+
}, now);
|
|
2819
|
+
if (outcome === 'capped') {
|
|
2820
|
+
blockBatchForOperator(deps, config, batchId, { reason: 'respawn-cap:tail', milestonePhase: 'batch-review' }, now, result);
|
|
2821
|
+
return;
|
|
2822
|
+
}
|
|
2615
2823
|
result.failed.push(unit(batchId));
|
|
2616
2824
|
}
|
|
2617
2825
|
}
|
|
2618
|
-
function reconcileReportSlot(deps, batchId, slot, now, result) {
|
|
2826
|
+
function reconcileReportSlot(deps, config, batchId, slot, now, result) {
|
|
2619
2827
|
const state = deps.store.load();
|
|
2620
2828
|
const batch = (0, state_1.findBatch)(state, batchId);
|
|
2621
2829
|
if (!batch || batch.anchor === null)
|
|
@@ -2630,7 +2838,7 @@ function reconcileReportSlot(deps, batchId, slot, now, result) {
|
|
|
2630
2838
|
// #629: a verified report completion is proof dispatch is healthy —
|
|
2631
2839
|
// reset the confirmed-failure streak, mirroring the per-issue path's
|
|
2632
2840
|
// own verified-complete branch.
|
|
2633
|
-
let n = (0, dispatch_health_1.resetDispatchApiErrorStreak)(releaseSlot(s, batchId, now));
|
|
2841
|
+
let n = clearAgentExits((0, dispatch_health_1.resetDispatchApiErrorStreak)(releaseSlot(s, batchId, now)), batchId, now);
|
|
2634
2842
|
const b = (0, state_1.findBatch)(n, batchId);
|
|
2635
2843
|
if (!b || b.status !== 'deployed')
|
|
2636
2844
|
return { state: n, result: undefined };
|
|
@@ -2650,8 +2858,15 @@ function reconcileReportSlot(deps, batchId, slot, now, result) {
|
|
|
2650
2858
|
if (handleDeadDispatchApiError(deps, batchId, (0, dispatch_1.batchReportLogPath)(deps.store.runsDir, batchId), slot.log_offset_at_spawn ?? 0, now)) {
|
|
2651
2859
|
return;
|
|
2652
2860
|
}
|
|
2653
|
-
|
|
2654
|
-
|
|
2861
|
+
// #832: counted — once the respawns are spent the merged batch closes
|
|
2862
|
+
// without its report (see `closeWithoutReport` for why not a block).
|
|
2863
|
+
const outcome = releaseAndCountAgentExit(deps, batchId, 'report', {
|
|
2864
|
+
event: 'report-failed',
|
|
2865
|
+
extra: { detail: 'unverified exit' },
|
|
2866
|
+
log: (0, dispatch_1.batchReportLogPath)(deps.store.runsDir, batchId),
|
|
2867
|
+
}, now);
|
|
2868
|
+
if (outcome === 'capped')
|
|
2869
|
+
closeWithoutReport(deps, batchId, 'respawn-cap:report', now, result);
|
|
2655
2870
|
}
|
|
2656
2871
|
}
|
|
2657
2872
|
// --- PR watch for `awaiting-merge` batches (mirrors `pollParkedPrs`/`reconcileParked`) ---
|
|
@@ -4158,7 +4373,7 @@ function runBatchTick(deps, config, dispatch) {
|
|
|
4158
4373
|
if (!fresh || types_1.TERMINAL_BATCH_STATUSES.has(fresh.status) || fresh.status === 'blocked') {
|
|
4159
4374
|
return { state, result: undefined };
|
|
4160
4375
|
}
|
|
4161
|
-
const blocked = (0, state_1.transitionBatch)(state, batchId, 'blocked', { blocked_reason: 'reconcile-error' }, now);
|
|
4376
|
+
const blocked = (0, state_1.transitionBatch)(state, batchId, 'blocked', { blocked_reason: 'reconcile-error', agent_exits: null }, now);
|
|
4162
4377
|
return { state: (0, state_1.releaseAllBatchSlots)(blocked, batchId, now), result: undefined };
|
|
4163
4378
|
});
|
|
4164
4379
|
journalEvent(deps, 'unit-failed', unit(batchId), { reason: 'reconcile-error', detail });
|
|
@@ -4272,10 +4487,10 @@ function runBatchTick(deps, config, dispatch) {
|
|
|
4272
4487
|
reconcileFixSlot(deps, config, batch.id, slot, now, result);
|
|
4273
4488
|
}
|
|
4274
4489
|
else if (batch.status === 'reviewing' || batch.status === 'shipping') {
|
|
4275
|
-
reconcileTailSlot(deps, batch.id, slot, now, result);
|
|
4490
|
+
reconcileTailSlot(deps, config, batch.id, slot, now, result);
|
|
4276
4491
|
}
|
|
4277
4492
|
else if (batch.status === 'deployed') {
|
|
4278
|
-
reconcileReportSlot(deps, batch.id, slot, now, result);
|
|
4493
|
+
reconcileReportSlot(deps, config, batch.id, slot, now, result);
|
|
4279
4494
|
}
|
|
4280
4495
|
continue;
|
|
4281
4496
|
}
|