@ai-dossier/sched 0.51.0 → 0.52.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 +60 -10
- package/dist/batch-dispatch.d.ts +2 -14
- package/dist/batch-dispatch.d.ts.map +1 -1
- package/dist/batch-dispatch.js +145 -43
- package/dist/batch-dispatch.js.map +1 -1
- package/dist/dispatch.d.ts +9 -0
- package/dist/dispatch.d.ts.map +1 -1
- package/dist/dispatch.js +19 -1
- package/dist/dispatch.js.map +1 -1
- package/dist/groundtruth.d.ts +18 -1
- package/dist/groundtruth.d.ts.map +1 -1
- package/dist/groundtruth.js +38 -0
- package/dist/groundtruth.js.map +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -5
- package/dist/index.js.map +1 -1
- package/dist/scheduler.d.ts +34 -0
- package/dist/scheduler.d.ts.map +1 -1
- package/dist/scheduler.js +83 -1
- package/dist/scheduler.js.map +1 -1
- package/dist/state.d.ts +14 -0
- package/dist/state.d.ts.map +1 -1
- package/dist/state.js +52 -4
- package/dist/state.js.map +1 -1
- package/dist/status.d.ts.map +1 -1
- package/dist/status.js +20 -12
- package/dist/status.js.map +1 -1
- package/dist/types.d.ts +31 -5
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +4 -1
- package/dist/types.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -31,6 +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 resume --batch b1 # recheck a gate-inconclusive block (#583), or re-run gate + tail over a batch blocked over landed work (#822)
|
|
34
35
|
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
36
|
ai-dossier sched stop --batch b1 # terminate every batch agent (incl. parallel members) and stop unfinished members
|
|
36
37
|
ai-dossier sched abandon --issue 42 --reason "operator abort"
|
|
@@ -771,6 +772,9 @@ failure rails: executing → dissolving (a member self-reports blocked)
|
|
|
771
772
|
validating → blocked (suite report unreadable, #562) → validating
|
|
772
773
|
executing → blocked (gate-inconclusive:<cap>, #583) → executing
|
|
773
774
|
(`sched resume --batch <id>` re-runs the gate; nothing requeued/reverted)
|
|
775
|
+
(dissolve refused | tail) → blocked (dissolve-refused:* / tail-blocked:* /
|
|
776
|
+
members-mismatch:* / respawn-cap:tail) → validating
|
|
777
|
+
(`sched resume --batch <id>` re-runs gate + tail over the landed members, #822)
|
|
774
778
|
```
|
|
775
779
|
|
|
776
780
|
- **One shared worktree/branch per batch**, claimed once by a deterministic (no LLM)
|
|
@@ -1004,13 +1008,15 @@ Dissolve never throws validated work away:
|
|
|
1004
1008
|
`gh pr create --head <integration branch> --base <base>` — once it merges,
|
|
1005
1009
|
`reconcileStaleBlockedBatches` settles the validated members to shipped; for a red suite
|
|
1006
1010
|
or a partial revert, inspect/fix the branch first; `sched abandon --batch` requeues them
|
|
1007
|
-
full-cycle instead.
|
|
1011
|
+
full-cycle instead. Since #822, `sched resume --batch <id>` re-runs the gate and the tail
|
|
1012
|
+
over the validated members instead (see [below](#resuming-a-batch-blocked-over-landed-work-822)). Without validated members it dissolves
|
|
1008
1013
|
as before, except that in-flight members with a member branch are parked `evicted` and
|
|
1009
1014
|
every requeue carries the batch's dispatch profile (`carryDispatchProfile: false` only
|
|
1010
1015
|
for `dispatch-profile-missing:*`, where the profile itself is what broke).
|
|
1011
1016
|
- Not changed: the `halved` PR-conflict split still re-batches its members, and
|
|
1012
1017
|
aggregate-suite evictions (`evictMembers`, post-landing, member branch already deleted)
|
|
1013
|
-
still requeue full-cycle — now on the batch profile.
|
|
1018
|
+
still requeue full-cycle — now on the batch profile. Both, plus an engine-set requeue base,
|
|
1019
|
+
are tracked in #840.
|
|
1014
1020
|
|
|
1015
1021
|
State schema 1.24.0: `IssueStatus` gains `handed-back`; `EvictionRecord` gains optional
|
|
1016
1022
|
`kind`/`branch`, `FailureEvidence` optional `branch`.
|
|
@@ -1052,13 +1058,50 @@ State schema 1.24.0: `IssueStatus` gains `handed-back`; `EvictionRecord` gains o
|
|
|
1052
1058
|
the batch. A spawn that finds the count already spent (a crash between the count and the
|
|
1053
1059
|
block) refuses instead of spawning. Before #832, b-20260924-04's tail was respawned four
|
|
1054
1060
|
times at the strong tier (~375k tokens) until an operator stopped the batch.
|
|
1055
|
-
- **Clearing these blocks.** `
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1061
|
+
- **Clearing these blocks.** `sched status` names the evidence (the anchor's blocked
|
|
1062
|
+
milestone, or the tail log) and the exits. For `members-mismatch:*`, `tail-blocked:*` and
|
|
1063
|
+
`respawn-cap:tail`: fix the cause, then `sched resume --batch <id>` (#822, below), or open
|
|
1064
|
+
the integration branch's PR by hand (`gh pr create --head <branch> --base <base>`; once it
|
|
1065
|
+
merges, `reconcileStaleBlockedBatches` settles the batch), or `sched abandon --batch <id>`
|
|
1066
|
+
(requeues the landed members full-cycle). `no-landed-members` has nothing to resume:
|
|
1067
|
+
abandon it.
|
|
1068
|
+
|
|
1069
|
+
### Resuming a batch blocked over landed work (#822)
|
|
1070
|
+
|
|
1071
|
+
`sched resume --batch <id>` on a batch blocked `dissolve-refused:*` (#810) or on one of #832's
|
|
1072
|
+
tail blocks (`tail-blocked:*`, `members-mismatch:*`, `respawn-cap:tail`) —
|
|
1073
|
+
`LANDED_RESUMABLE_BLOCK_PREFIXES` — calls `resumeLandedBatch`: `blocked → validating`, with
|
|
1074
|
+
`blocked_reason`/`agent_exits` cleared and `executing_member` pinned to the last member (so
|
|
1075
|
+
`runValidate` never re-admits a member the dissolve released). The engine's next tick
|
|
1076
|
+
re-runs the batch gate and then the tail over the landed members only; a gate still red
|
|
1077
|
+
re-blocks it once. It journals `batch-resumed` (naming any tail exits it cleared). Refused,
|
|
1078
|
+
with the reason, when no member is landed, the batch still holds a slot, it has no
|
|
1079
|
+
integration worktree/branch/anchor, a PR is already recorded against it (let the stale-blocked
|
|
1080
|
+
reconcile settle it), or the tail's own members check would refuse the same landed set again
|
|
1081
|
+
(`tailMembersRefusal`). The CLI also refuses when the integration worktree is gone from disk
|
|
1082
|
+
or a profiled batch's dispatch profile no longer resolves (restore it first).
|
|
1083
|
+
`gate-inconclusive:*` keeps its own recheck (`resumeBlockedGate`).
|
|
1084
|
+
|
|
1085
|
+
### Wrong-procedure members are re-prompted once (#822)
|
|
1086
|
+
|
|
1087
|
+
A member whose dispatch posts a full-cycle-line milestone (`gate` … `report`) carrying neither
|
|
1088
|
+
`batch=` nor `mode=slot` ran the WRONG PROCEDURE (imboard #4174 ran full-cycle as a
|
|
1089
|
+
member: `review done next=ship`) — `isWrongProcedureMilestone`, fenced to this dispatch. A
|
|
1090
|
+
hand-back shape (`blocked`, `review partial`) never counts: it keeps its own path. Both
|
|
1091
|
+
member rails then:
|
|
1092
|
+
|
|
1093
|
+
- stop a live agent and wait for it to be gone before anything else (a dying full-cycle run
|
|
1094
|
+
could still push, post or open a PR in that worktree);
|
|
1095
|
+
- if the milestone shows the run already SHIPPED (a `ship`/`report` phase or a `pr=` key —
|
|
1096
|
+
`wrongProcedureShippedPr`), evict it at once `wrong-procedure-shipped`, naming the stray PR
|
|
1097
|
+
(`stray_pr`) for the operator to close or reconcile — a re-prompt cannot undo a PR;
|
|
1098
|
+
- otherwise, the first time, record it in `BatchEntry.reprompted_members`
|
|
1099
|
+
(`{ issue, milestone_at, reprompted_at }`, schema 1.27.0) and journal `member-reprompted`:
|
|
1100
|
+
it respawns in place, in the same worktree, with `wrongProcedureDirective` appended to its
|
|
1101
|
+
prompt (engine values only — no milestone text);
|
|
1102
|
+
- a later wrong-procedure milestone posted strictly after `reprompted_at` (no skew tolerance:
|
|
1103
|
+
the killed first dispatch's milestones would otherwise still count inside the 60s fence)
|
|
1104
|
+
evicts it `wrong-procedure` (parked `evicted`, branch kept, #810).
|
|
1062
1105
|
|
|
1063
1106
|
## API surface
|
|
1064
1107
|
|
|
@@ -1225,6 +1268,11 @@ import {
|
|
|
1225
1268
|
type CapOutcome, // ok | task-failed | automation-broken | capability-unavailable
|
|
1226
1269
|
type CapabilityGateResult, // {outcome: CapOutcome, outputTail?, reason?} — runCapability's return shape (#583)
|
|
1227
1270
|
resumeBlockedGate, // #583: sched resume --batch <id> — re-run the gate that blocked a batch
|
|
1271
|
+
resumeLandedBatch, // #822: sched resume --batch <id> over landed work — blocked → validating
|
|
1272
|
+
isLandedResumableBlock, LANDED_RESUMABLE_BLOCK_PREFIXES, // #822: which blocks resumeLandedBatch accepts
|
|
1273
|
+
isWrongProcedureMilestone, wrongProcedureShippedPr, // #822: wrong-procedure detection; did the stray run already ship?
|
|
1274
|
+
wrongProcedureDirective, WRONG_PROCEDURE_MARKER, // #822: the re-prompt text and its fixed opening
|
|
1275
|
+
type RepromptRecord, // #822: BatchEntry.reprompted_members ({ issue, milestone_at, reprompted_at })
|
|
1228
1276
|
buildMemberPrompt, buildBatchTailPrompt, buildBatchReportPrompt, // #523 prompt builders (#677: member carries {issue}/{batch}/{worktree}/{integration_branch})
|
|
1229
1277
|
memberBranchFor, // #677: the member branch name, `batch/<id>-m<n>-<issue>` — one definition, every recovery surface derives from it
|
|
1230
1278
|
DEFAULT_MEMBER_PROMPT_TEMPLATE, DEFAULT_BATCH_TAIL_PROMPT_TEMPLATE,
|
|
@@ -1366,7 +1414,7 @@ after-the-fact recovery, not a missing-data bug.
|
|
|
1366
1414
|
queue data.
|
|
1367
1415
|
- **Schema**: state/config files from #460 (schema 1.0.0), #464 (1.1.0), #468 (1.2.0),
|
|
1368
1416
|
#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)
|
|
1369
|
-
load and migrate to the current schema (1.
|
|
1417
|
+
load and migrate to the current schema (1.27.0 — 1.24.0 was #810: no backfill,
|
|
1370
1418
|
`kind`/`branch` optional, absent = an `evicted` record with no branch) automatically
|
|
1371
1419
|
(slot `branch`/`last_head`/`pid_start`, slot `role` (inferred from the
|
|
1372
1420
|
unit's queue entry, with the persisted `phase` as a fallback — #500), entry
|
|
@@ -1387,6 +1435,8 @@ after-the-fact recovery, not a missing-data bug.
|
|
|
1387
1435
|
backfill to `null`/`null`/`0` — #789, schema 1.25.0).
|
|
1388
1436
|
Schema 1.26.0 (#832): `BatchEntry` gains `agent_exits` (the per-phase tail/report
|
|
1389
1437
|
respawn counter); `null` is backfilled on load.
|
|
1438
|
+
Schema 1.27.0 (#822): `BatchEntry` gains `reprompted_members`
|
|
1439
|
+
(`{ issue, milestone_at, reprompted_at }` records; `[]` backfilled).
|
|
1390
1440
|
- **`max_slots`** bounds live units (`assigned | running | recovering`); dependency
|
|
1391
1441
|
edges gate readiness — an issue with an unmerged dependency, and a batch behind an
|
|
1392
1442
|
unmerged batch, are never runnable.
|
package/dist/batch-dispatch.d.ts
CHANGED
|
@@ -68,8 +68,10 @@ import { type Journal } from './journal';
|
|
|
68
68
|
import type { SchedStore } from './persist';
|
|
69
69
|
import type { ExecFn } from './project';
|
|
70
70
|
import { type BatchSuiteContext, type SuiteResult } from './recovery';
|
|
71
|
+
import { tailMembersRefusal } from './state';
|
|
71
72
|
import { type FsExists } from './teardown';
|
|
72
73
|
import type { BatchEntry, CapabilityGateResult, MemberDispatchMode, MemberExitKind, SchedConfig, SchedState } from './types';
|
|
74
|
+
export { tailMembersRefusal };
|
|
73
75
|
export type { CapOutcome } from './types';
|
|
74
76
|
/** Everything batch dispatch needs from the outside world. */
|
|
75
77
|
export interface BatchDispatchDeps {
|
|
@@ -172,20 +174,6 @@ export interface MemberFailure {
|
|
|
172
174
|
* scheduler lands it there.
|
|
173
175
|
*/
|
|
174
176
|
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;
|
|
189
177
|
/**
|
|
190
178
|
* Evict the current member and either dissolve, or continue the batch via
|
|
191
179
|
* `advanceMemberOrValidate` — the shared tail of both member-failure rails
|
|
@@ -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,
|
|
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,9 +96,8 @@ 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
|
+
exports.MAX_BATCH_AGENT_RESPAWNS = exports.tailMembersRefusal = void 0;
|
|
100
100
|
exports.memberBranchFor = memberBranchFor;
|
|
101
|
-
exports.tailMembersRefusal = tailMembersRefusal;
|
|
102
101
|
exports.evictMemberAndContinue = evictMemberAndContinue;
|
|
103
102
|
exports.resumeBlockedGate = resumeBlockedGate;
|
|
104
103
|
exports.reconcileStaleBlockedBatches = reconcileStaleBlockedBatches;
|
|
@@ -119,6 +118,7 @@ const recovery_1 = require("./recovery");
|
|
|
119
118
|
const run_log_1 = require("./run-log");
|
|
120
119
|
const scheduler_1 = require("./scheduler");
|
|
121
120
|
const state_1 = require("./state");
|
|
121
|
+
Object.defineProperty(exports, "tailMembersRefusal", { enumerable: true, get: function () { return state_1.tailMembersRefusal; } });
|
|
122
122
|
const teardown_1 = require("./teardown");
|
|
123
123
|
const types_1 = require("./types");
|
|
124
124
|
function emptyResult() {
|
|
@@ -162,6 +162,12 @@ function sanitizeUntrustedText(value) {
|
|
|
162
162
|
function journalEvent(deps, event, unitId, extra = {}) {
|
|
163
163
|
deps.journal.append((0, journal_1.unitEvent)(event, unitId, extra), deps.now());
|
|
164
164
|
}
|
|
165
|
+
/** Stop the slot's agent if it is still alive (pid-start-safe: a reused pid is never signalled). */
|
|
166
|
+
function killLiveAgent(deps, slot) {
|
|
167
|
+
if (slot.pid !== null && deps.spawnDeps.isAlive(slot.pid, slot.pid_start ?? undefined)) {
|
|
168
|
+
deps.spawnDeps.kill(slot.pid, slot.pid_start ?? undefined);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
165
171
|
/**
|
|
166
172
|
* Read a dead batch dispatch's own log and classify it (#629 AC3) — tail,
|
|
167
173
|
* member, fix and report all reach a `dead` agent the same way (`pid` no
|
|
@@ -1016,7 +1022,11 @@ function spawnMemberAgent(deps, dispatch, state, slot, batchId, now, result, tar
|
|
|
1016
1022
|
// #677: the prompt carries THIS member's own worktree and the integration
|
|
1017
1023
|
// branch it was cut from — `member-cycle`'s `worktree`/`integration_branch`
|
|
1018
1024
|
// inputs. The shared batch worktree is no longer a member surface at all.
|
|
1019
|
-
const prompt = (0, dispatch_1.buildMemberPrompt)(dispatch.memberPrompt, memberIssue, batchId, target.member.worktree, batch.branch, review)
|
|
1025
|
+
const prompt = (0, dispatch_1.buildMemberPrompt)(dispatch.memberPrompt, memberIssue, batchId, target.member.worktree, batch.branch, review) +
|
|
1026
|
+
// #822: a member respawned after running the wrong procedure.
|
|
1027
|
+
(batch.reprompted_members.some((r) => r.issue === memberIssue)
|
|
1028
|
+
? (0, dispatch_1.wrongProcedureDirective)(memberIssue, batchId)
|
|
1029
|
+
: '');
|
|
1020
1030
|
const logFile = (0, dispatch_1.batchMemberLogPath)(deps.store.runsDir, batchId, target.index, memberIssue);
|
|
1021
1031
|
// #629: captured BEFORE spawning, mirroring `engine.ts`'s own
|
|
1022
1032
|
// `spawnAndRecord` — the log is per-role and append-mode, so the size at
|
|
@@ -1311,36 +1321,6 @@ function spawnMemberContinuation(deps, config, dispatch, batchId, now, result) {
|
|
|
1311
1321
|
}
|
|
1312
1322
|
claimAndSpawn(deps, config, batchId, 'member', now, (nextState, slot) => spawnMember(deps, dispatch, nextState, slot, batchId, now, result, member));
|
|
1313
1323
|
}
|
|
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
1324
|
/**
|
|
1345
1325
|
* #832: the one pre-dispatch verdict for a tail — checked before a slot is
|
|
1346
1326
|
* claimed and again under the claim's lock. A tail whose respawns are spent
|
|
@@ -1353,7 +1333,7 @@ function tailDispatchRefusal(state, batch) {
|
|
|
1353
1333
|
return null;
|
|
1354
1334
|
if (agentRespawnsSpent(batch, 'tail'))
|
|
1355
1335
|
return 'respawn-cap:tail';
|
|
1356
|
-
return tailMembersRefusal(batch, (0, state_1.validatedMembersOf)(state, batch));
|
|
1336
|
+
return (0, state_1.tailMembersRefusal)(batch, (0, state_1.validatedMembersOf)(state, batch));
|
|
1357
1337
|
}
|
|
1358
1338
|
/**
|
|
1359
1339
|
* Tail/report/fix dispatches (this function, `spawnReportAgent`,
|
|
@@ -2091,7 +2071,7 @@ function resumeBlockedGate(deps, config, dispatch, batchId, now) {
|
|
|
2091
2071
|
// `suite-unreadable`) — name the actual reason and the operator path
|
|
2092
2072
|
// instead of a bare illegal-transition error that reads like a bug.
|
|
2093
2073
|
if (batch.status === 'blocked') {
|
|
2094
|
-
throw new types_1.SchedNotFoundError(`Batch ${batchId} is blocked on '${batch.blocked_reason ?? '?'}', which has no gate recheck: inspect the batch worktree/state named in its journal, then \`sched abandon --batch ${batchId}\` if it is a dead end`);
|
|
2074
|
+
throw new types_1.SchedNotFoundError(`Batch ${batchId} is blocked on '${batch.blocked_reason ?? '?'}', which has no gate recheck (a batch blocked over landed work — dissolve-refused / tail-blocked / members-mismatch / respawn-cap:tail — resumes via \`sched resume --batch\`, #822): inspect the batch worktree/state named in its journal, then \`sched abandon --batch ${batchId}\` if it is a dead end`);
|
|
2095
2075
|
}
|
|
2096
2076
|
throw new types_1.IllegalTransitionError('batch', batch.status, 'executing');
|
|
2097
2077
|
}
|
|
@@ -2367,8 +2347,113 @@ function readMemberDispatch(deps, batchId, memberIssue, slot, now) {
|
|
|
2367
2347
|
milestone,
|
|
2368
2348
|
dead,
|
|
2369
2349
|
blockedNow: (0, groundtruth_1.isMemberBlocked)(milestone, slot.spawned_at),
|
|
2350
|
+
wrongProcedure: (0, groundtruth_1.isWrongProcedureMilestone)(milestone, slot.spawned_at),
|
|
2370
2351
|
};
|
|
2371
2352
|
}
|
|
2353
|
+
/**
|
|
2354
|
+
* #822: whether a wrong-procedure read counts against THIS dispatch of
|
|
2355
|
+
* `memberIssue`. After a re-prompt, only a milestone posted strictly after
|
|
2356
|
+
* the re-prompt does: the killed first dispatch's milestones still sit
|
|
2357
|
+
* inside the respawn's 60s fence tolerance and would otherwise evict the
|
|
2358
|
+
* member the moment it respawns. No tolerance here — the engine and the
|
|
2359
|
+
* member's CLI stamp times on the same host.
|
|
2360
|
+
*/
|
|
2361
|
+
function wrongProcedureCounts(batch, memberIssue, read) {
|
|
2362
|
+
if (!read.wrongProcedure || read.milestone === null)
|
|
2363
|
+
return false;
|
|
2364
|
+
const prior = batch.reprompted_members.find((r) => r.issue === memberIssue);
|
|
2365
|
+
if (prior === undefined)
|
|
2366
|
+
return true;
|
|
2367
|
+
const at = Date.parse(read.milestone.at);
|
|
2368
|
+
const repromptedAt = Date.parse(prior.reprompted_at);
|
|
2369
|
+
return Number.isNaN(at) || Number.isNaN(repromptedAt) ? false : at > repromptedAt;
|
|
2370
|
+
}
|
|
2371
|
+
/**
|
|
2372
|
+
* #822 (#810 proposal 3): the member's dispatch ran the wrong procedure (a
|
|
2373
|
+
* full-cycle-shaped trail — imboard #4174 ran full-cycle as a batch member).
|
|
2374
|
+
* That is a dispatch mistake, not the member's work failing, so the first
|
|
2375
|
+
* time it is RE-PROMPTED: a live agent is stopped (a full-cycle run left going
|
|
2376
|
+
* would open its own PR), the slot is released, and the member is recorded in
|
|
2377
|
+
* `reprompted_members` — the wedge arm (serial) or `spawnParallelMembers`
|
|
2378
|
+
* (parallel) respawns it in place, in the same worktree, with
|
|
2379
|
+
* `wrongProcedureDirective` appended. Returns `'evict'` when the member was
|
|
2380
|
+
* already re-prompted once: the caller evicts it `wrong-procedure`.
|
|
2381
|
+
*/
|
|
2382
|
+
function repromptWrongProcedure(deps, batchId, memberIssue, milestoneAt, slot, release, now) {
|
|
2383
|
+
// Never respawn into (or release) a worktree the old agent is still in: a
|
|
2384
|
+
// dying full-cycle run could still push, post, or open a PR. Signal it and
|
|
2385
|
+
// decide on a later tick, once it is gone — and so the re-prompt's
|
|
2386
|
+
// `reprompted_at` fence postdates everything it could still post.
|
|
2387
|
+
if (slot.pid !== null && deps.spawnDeps.isAlive(slot.pid, slot.pid_start ?? undefined)) {
|
|
2388
|
+
deps.spawnDeps.kill(slot.pid, slot.pid_start ?? undefined);
|
|
2389
|
+
return 'waiting';
|
|
2390
|
+
}
|
|
2391
|
+
const outcome = deps.store.withLock((s) => {
|
|
2392
|
+
const released = release(s);
|
|
2393
|
+
const b = (0, state_1.findBatch)(released, batchId);
|
|
2394
|
+
if (!b || b.reprompted_members.some((r) => r.issue === memberIssue)) {
|
|
2395
|
+
return { state: released, result: 'evict' };
|
|
2396
|
+
}
|
|
2397
|
+
return {
|
|
2398
|
+
state: (0, state_1.patchBatch)(released, batchId, {
|
|
2399
|
+
reprompted_members: [
|
|
2400
|
+
...b.reprompted_members,
|
|
2401
|
+
{ issue: memberIssue, milestone_at: milestoneAt, reprompted_at: now.toISOString() },
|
|
2402
|
+
],
|
|
2403
|
+
}, now),
|
|
2404
|
+
result: 'reprompted',
|
|
2405
|
+
};
|
|
2406
|
+
});
|
|
2407
|
+
if (outcome === 'reprompted') {
|
|
2408
|
+
journalEvent(deps, 'member-reprompted', unit(batchId), {
|
|
2409
|
+
issue: memberIssue,
|
|
2410
|
+
reason: 'wrong-procedure',
|
|
2411
|
+
milestone_at: milestoneAt,
|
|
2412
|
+
detail: `${WRONG_PROCEDURE_SHAPE} — respawning it once in place with a member-cycle directive`,
|
|
2413
|
+
});
|
|
2414
|
+
}
|
|
2415
|
+
return outcome;
|
|
2416
|
+
}
|
|
2417
|
+
/** #822: the evidence line every wrong-procedure journal/eviction names. */
|
|
2418
|
+
const WRONG_PROCEDURE_SHAPE = 'member posted a full-cycle-shaped milestone (no batch=/mode=slot key)';
|
|
2419
|
+
/** #822: the eviction a member earns for running the wrong procedure after its one re-prompt. */
|
|
2420
|
+
function wrongProcedureFailure(lastTool, shippedPr = null) {
|
|
2421
|
+
return {
|
|
2422
|
+
reason: shippedPr === null ? 'wrong-procedure' : 'wrong-procedure-shipped',
|
|
2423
|
+
detail: shippedPr === null
|
|
2424
|
+
? `${WRONG_PROCEDURE_SHAPE} again after its one re-prompt`
|
|
2425
|
+
: `${WRONG_PROCEDURE_SHAPE} and its full-cycle run already reached ship (PR ${shippedPr === 'unknown' ? 'not named' : `#${shippedPr}`}) — evicted without a re-prompt; close or reconcile that PR by hand`,
|
|
2426
|
+
kind: 'evicted',
|
|
2427
|
+
extraKv: {
|
|
2428
|
+
...(lastTool ? { last_tool: lastTool } : {}),
|
|
2429
|
+
...(typeof shippedPr === 'number' ? { stray_pr: String(shippedPr) } : {}),
|
|
2430
|
+
},
|
|
2431
|
+
};
|
|
2432
|
+
}
|
|
2433
|
+
/**
|
|
2434
|
+
* #822: the one wrong-procedure decision shared by both member rails —
|
|
2435
|
+
* `'waiting'` (the old agent is still dying), `'reprompted'`, or the
|
|
2436
|
+
* eviction failure to apply. A run that already shipped is evicted without
|
|
2437
|
+
* a re-prompt.
|
|
2438
|
+
*/
|
|
2439
|
+
function decideWrongProcedure(deps, batchId, memberIssue, milestone, slot, release, lastTool, now) {
|
|
2440
|
+
const shipped = (0, groundtruth_1.wrongProcedureShippedPr)(milestone);
|
|
2441
|
+
if (shipped !== null) {
|
|
2442
|
+
if (slot.pid !== null && deps.spawnDeps.isAlive(slot.pid, slot.pid_start ?? undefined)) {
|
|
2443
|
+
deps.spawnDeps.kill(slot.pid, slot.pid_start ?? undefined);
|
|
2444
|
+
return 'waiting';
|
|
2445
|
+
}
|
|
2446
|
+
const tool = lastTool();
|
|
2447
|
+
deps.store.withLock((s) => ({ state: release(s), result: undefined }));
|
|
2448
|
+
return wrongProcedureFailure(tool, shipped);
|
|
2449
|
+
}
|
|
2450
|
+
const verdict = repromptWrongProcedure(deps, batchId, memberIssue, milestone.at, slot, release, now);
|
|
2451
|
+
if (verdict === 'waiting')
|
|
2452
|
+
return verdict;
|
|
2453
|
+
// The finished dispatch's run-log entry, exactly once (never while waiting).
|
|
2454
|
+
const tool = lastTool();
|
|
2455
|
+
return verdict === 'reprompted' ? verdict : wrongProcedureFailure(tool);
|
|
2456
|
+
}
|
|
2372
2457
|
/**
|
|
2373
2458
|
* The eviction a blocked-or-dead member dispatch earns (shared by both rails).
|
|
2374
2459
|
* #605: only a milestone THIS dispatch posted may name the reason. A dead
|
|
@@ -2438,6 +2523,16 @@ function reconcileMemberSlot(deps, config, dispatch, batchId, slot, now, result)
|
|
|
2438
2523
|
completeMemberGate(deps, config, dispatch, batchId, batch, memberIssue, now, result);
|
|
2439
2524
|
return;
|
|
2440
2525
|
}
|
|
2526
|
+
// #822: a wrong-procedure trail is re-prompted once, then evicted — decided
|
|
2527
|
+
// before the dead/blocked rail, which would otherwise evict it as an
|
|
2528
|
+
// unverified exit at the first sight.
|
|
2529
|
+
if (wrongProcedureCounts(batch, memberIssue, read) && read.milestone !== null) {
|
|
2530
|
+
const decision = decideWrongProcedure(deps, batchId, memberIssue, read.milestone, slot, (s) => releaseSlot(s, batchId, now), () => recordMemberRunLog(deps, dispatch, state0, batchId, batch.executing_member, memberIssue, slot, now), now);
|
|
2531
|
+
if (typeof decision === 'object') {
|
|
2532
|
+
evictMemberAndContinue(deps, config, dispatch, batchId, batch, memberIssue, decision, now, result);
|
|
2533
|
+
}
|
|
2534
|
+
return;
|
|
2535
|
+
}
|
|
2441
2536
|
if (read.dead &&
|
|
2442
2537
|
!read.blockedNow &&
|
|
2443
2538
|
handleDeadDispatchApiError(deps, batchId, (0, dispatch_1.batchMemberLogPath)(deps.store.runsDir, batchId, batch.executing_member, memberIssue), slot.log_offset_at_spawn ?? 0, now)) {
|
|
@@ -2778,8 +2873,8 @@ function reconcileTailSlot(deps, config, batchId, slot, now, result) {
|
|
|
2778
2873
|
? null
|
|
2779
2874
|
: (0, groundtruth_1.batchPhaseBlockedReason)(milestone, ['batch-review', 'batch-ship'], slot.spawned_at);
|
|
2780
2875
|
if (handedBack !== null) {
|
|
2781
|
-
if (
|
|
2782
|
-
deps
|
|
2876
|
+
if (!dead)
|
|
2877
|
+
killLiveAgent(deps, slot);
|
|
2783
2878
|
blockOnTailHandBack(deps, config, batch, `tail-blocked:${handedBack}`, now, result);
|
|
2784
2879
|
return;
|
|
2785
2880
|
}
|
|
@@ -3904,9 +3999,7 @@ function reconcileParallelMemberSlot(deps, config, dispatch, batchId, run, slot,
|
|
|
3904
3999
|
// into that tree with its pid no longer tracked anywhere (so neither a
|
|
3905
4000
|
// dissolve nor `sched stop --batch` could stop it). Its work is done —
|
|
3906
4001
|
// stop it first.
|
|
3907
|
-
|
|
3908
|
-
deps.spawnDeps.kill(slot.pid, slot.pid_start ?? undefined);
|
|
3909
|
-
}
|
|
4002
|
+
killLiveAgent(deps, slot);
|
|
3910
4003
|
journalEvent(deps, 'external-advance', unit(batchId), {
|
|
3911
4004
|
issue: run.issue,
|
|
3912
4005
|
detail: 'member review done',
|
|
@@ -3929,6 +4022,17 @@ function reconcileParallelMemberSlot(deps, config, dispatch, batchId, run, slot,
|
|
|
3929
4022
|
result.completed.push(memberUnit);
|
|
3930
4023
|
return;
|
|
3931
4024
|
}
|
|
4025
|
+
// #822: re-prompt once, then evict — see the serial rail.
|
|
4026
|
+
const parallelBatch = (0, state_1.findBatch)(state0, batchId);
|
|
4027
|
+
if (parallelBatch &&
|
|
4028
|
+
wrongProcedureCounts(parallelBatch, run.issue, read) &&
|
|
4029
|
+
read.milestone !== null) {
|
|
4030
|
+
const decision = decideWrongProcedure(deps, batchId, run.issue, read.milestone, slot, release, () => recordMemberRunLog(deps, dispatch, state0, batchId, run.index, run.issue, slot, now), now);
|
|
4031
|
+
if (typeof decision === 'object') {
|
|
4032
|
+
evictParallelMember(deps, config, batchId, run, decision, now, result);
|
|
4033
|
+
}
|
|
4034
|
+
return;
|
|
4035
|
+
}
|
|
3932
4036
|
if (read.dead &&
|
|
3933
4037
|
!read.blockedNow &&
|
|
3934
4038
|
handleDeadDispatchApiError(deps, batchId, (0, dispatch_1.batchMemberLogPath)(deps.store.runsDir, batchId, run.index, run.issue), slot.log_offset_at_spawn ?? 0, now, { releaseWith: release })) {
|
|
@@ -4239,9 +4343,7 @@ function stopMemberAgent(deps, batchId, issue, now) {
|
|
|
4239
4343
|
const slot = (0, state_1.slotForBatchMember)(deps.store.load(), batchId, issue);
|
|
4240
4344
|
if (slot === undefined)
|
|
4241
4345
|
return;
|
|
4242
|
-
|
|
4243
|
-
deps.spawnDeps.kill(slot.pid, slot.pid_start ?? undefined);
|
|
4244
|
-
}
|
|
4346
|
+
killLiveAgent(deps, slot);
|
|
4245
4347
|
deps.store.withLock((s) => ({
|
|
4246
4348
|
state: (0, state_1.releaseBatchMemberSlot)(s, batchId, issue, now),
|
|
4247
4349
|
result: undefined,
|