@ai-dossier/sched 0.16.0 → 0.17.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 +76 -21
- package/dist/batch-dispatch.d.ts +5 -0
- package/dist/batch-dispatch.d.ts.map +1 -1
- package/dist/batch-dispatch.js +87 -14
- package/dist/batch-dispatch.js.map +1 -1
- package/dist/batch-stats.d.ts +62 -0
- package/dist/batch-stats.d.ts.map +1 -0
- package/dist/batch-stats.js +185 -0
- package/dist/batch-stats.js.map +1 -0
- package/dist/dispatch.d.ts +13 -0
- package/dist/dispatch.d.ts.map +1 -1
- package/dist/dispatch.js +25 -0
- package/dist/dispatch.js.map +1 -1
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +3 -31
- package/dist/engine.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -4
- package/dist/index.js.map +1 -1
- package/dist/persist.d.ts.map +1 -1
- package/dist/persist.js +25 -5
- package/dist/persist.js.map +1 -1
- package/dist/recovery.d.ts +39 -10
- package/dist/recovery.d.ts.map +1 -1
- package/dist/recovery.js +162 -17
- package/dist/recovery.js.map +1 -1
- package/dist/run-log.d.ts +19 -0
- package/dist/run-log.d.ts.map +1 -1
- package/dist/run-log.js +46 -0
- package/dist/run-log.js.map +1 -1
- package/dist/types.d.ts +42 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +16 -3
- package/dist/types.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -34,14 +34,18 @@ ai-dossier sched resume
|
|
|
34
34
|
ai-dossier sched abandon --issue 42 --reason "operator abort"
|
|
35
35
|
ai-dossier sched abandon --batch b1 # dissolve; members requeue as full-cycle
|
|
36
36
|
ai-dossier sched stats --issues 4..9 # per-issue tokens/cost from ~/.dossier/runs.jsonl (#524)
|
|
37
|
+
ai-dossier sched stats --batch b1 --project owner-repo # batch member/tail/report/fix costs from raw dispatch logs (#564)
|
|
37
38
|
```
|
|
38
39
|
|
|
39
|
-
Every subcommand except `stats` takes `--project <slug>` (default:
|
|
40
|
-
current directory, falling back to the repo basename — fleet-cycle's
|
|
41
|
-
`--json`. `stats` reads `~/.dossier/runs.jsonl`, a
|
|
42
|
-
per-project state — it takes `--json` and `--issues` only;
|
|
43
|
-
below for the resulting cross-repo caveat (the same issue
|
|
44
|
-
together).
|
|
40
|
+
Every subcommand except `stats` (without `--batch`) takes `--project <slug>` (default:
|
|
41
|
+
`owner-repo` of the current directory, falling back to the repo basename — fleet-cycle's
|
|
42
|
+
convention) and `--json`. `stats` without `--batch` reads `~/.dossier/runs.jsonl`, a
|
|
43
|
+
single global file, not the per-project state — it takes `--json` and `--issues` only;
|
|
44
|
+
see "runs.jsonl telemetry" below for the resulting cross-repo caveat (the same issue
|
|
45
|
+
number in two repos sums together). `stats --batch <id>` instead takes `--project` like
|
|
46
|
+
every other subcommand and reads that project's `~/.dossier/sched/<project>/runs/`
|
|
47
|
+
directory directly, reconstructing costs from the raw dispatch logs rather than
|
|
48
|
+
`runs.jsonl` (#564) — see "Batch members (#564)" below.
|
|
45
49
|
|
|
46
50
|
Since #507, `enqueue` additionally reads each candidate issue's live GitHub labels (one
|
|
47
51
|
`gh issue view --json labels` call per issue, resolved against the current directory's repo
|
|
@@ -172,6 +176,17 @@ containing a `blocked` batch with "unknown batch status", bricking that project'
|
|
|
172
176
|
`SchedStore.load()`. Resume or abandon any blocked batch before downgrading past this
|
|
173
177
|
version.
|
|
174
178
|
|
|
179
|
+
Config schema moves to 1.7.0 (#563): a new top-level `dissolve_policy` key —
|
|
180
|
+
`{ fraction, min_evictions_before_dissolve }`, both required when the key is present —
|
|
181
|
+
overrides the batch dissolve threshold (default `{ fraction: 1/3,
|
|
182
|
+
min_evictions_before_dissolve: 1 }`, RFC-0001 §F.8's ⅓ with no additional floor). An
|
|
183
|
+
absent key falls back wholesale to the default; an invalid one degrades the WHOLE config
|
|
184
|
+
file to built-in defaults, same as every other config field (`loadConfig`'s
|
|
185
|
+
degrade-to-defaults contract). This release also adds the `partial` `dissolveBatch`
|
|
186
|
+
strategy and the `batch-preserved` journal event (see Batch failure recovery above) — both
|
|
187
|
+
are behavioral, not persisted-shape changes, so they carry no schema-version bump of their
|
|
188
|
+
own.
|
|
189
|
+
|
|
175
190
|
Two engine-safety policies were explicit product decisions on #464:
|
|
176
191
|
|
|
177
192
|
- **Pid identity is hybrid-verified (decision 1, option C).** Every spawn records the
|
|
@@ -248,7 +263,11 @@ against real scratch repos.
|
|
|
248
263
|
```
|
|
249
264
|
validating → attributing → fixing (ONE bounded attempt) → validating
|
|
250
265
|
→ evicting (revert the member's commits) → validating
|
|
251
|
-
>
|
|
266
|
+
evictions > max(ceil(N × fraction), min_evictions_before_dissolve), or a
|
|
267
|
+
revert conflict → dissolving → members requeued (`dissolve_policy`, #563)
|
|
268
|
+
same threshold crossed, but the survivors' re-run suite came back green
|
|
269
|
+
→ reviewing (batch preserved; only the evicted
|
|
270
|
+
members requeue — strategy=partial, #563)
|
|
252
271
|
suite report unreadable, after the fallback retry when one applied
|
|
253
272
|
→ blocked → validating (nothing requeued/reverted; #562)
|
|
254
273
|
awaiting-merge (CONFLICTING | auto-merge-blocked)
|
|
@@ -280,13 +299,24 @@ awaiting-merge (CONFLICTING | auto-merge-blocked)
|
|
|
280
299
|
aborted so the worktree is clean and the batch dissolves — the reverts that already
|
|
281
300
|
landed ride along on the abandoned branch, which is why it is abandoned rather than
|
|
282
301
|
reused. An eviction group that reaches an already-shipped member dissolves instead of
|
|
283
|
-
reverting merged work.
|
|
302
|
+
reverting merged work. Crossing the dissolve trigger no longer always dissolves (#563):
|
|
303
|
+
if the re-run suite came back green for the survivors — and every evicted member's
|
|
304
|
+
commits were actually found and reverted — the batch is PRESERVED instead: trimmed to
|
|
305
|
+
its survivors and carried straight to `reviewing`, only the evicted members requeue. A
|
|
306
|
+
red or unreadable re-run, or an evicted member whose commits were never found on the
|
|
307
|
+
branch, still dissolves in full.
|
|
284
308
|
4. **Dissolve (AC3)** — `dissolveBatch` marks the batch `dissolved` and requeues every
|
|
285
|
-
UNSHIPPED member: `full` (each as its own full-cycle run)
|
|
309
|
+
UNSHIPPED member: `full` (each as its own full-cycle run), `halved` (one or two fresh
|
|
286
310
|
`forming` half-batches — a single remaining member yields one — entries retagged,
|
|
287
|
-
eviction groups inherited where they survive the split)
|
|
288
|
-
|
|
289
|
-
|
|
311
|
+
eviction groups inherited where they survive the split), or `partial` (#563 — never
|
|
312
|
+
marks the batch `dissolved` at all: drops the evicted members from `batch.members` and
|
|
313
|
+
its `eviction_groups`/`ranges`, transitions `validating → reviewing`, and requeues
|
|
314
|
+
nothing itself, since the caller's eviction loop already did; falls through to `full`
|
|
315
|
+
if that would leave zero survivors, so an empty batch never ships). Shipped and
|
|
316
|
+
terminal members keep their outcome; nothing green is discarded, and no git runs — the
|
|
317
|
+
batch branch is simply left behind unmerged, since sched deletes nothing. Every dissolve
|
|
318
|
+
decision — all three strategies — journals its policy inputs (`N=`, `evictions=`,
|
|
319
|
+
`threshold=`), so it is explainable without re-deriving the formula.
|
|
290
320
|
5. **PR conflict (AC4)** — `handlePrConflict` rebases the batch branch, re-runs the suite
|
|
291
321
|
and re-ships ONCE. A second occurrence, a conflicting rebase, a failed fetch, an
|
|
292
322
|
unusable `base_branch`, a checkout that is not on the batch branch, or a red suite
|
|
@@ -299,12 +329,13 @@ awaiting-merge (CONFLICTING | auto-merge-blocked)
|
|
|
299
329
|
with no `anchor` or no `run_id` cannot post — the CLI requires both — so the milestone
|
|
300
330
|
it could not post is journaled in full instead of vanishing.
|
|
301
331
|
|
|
302
|
-
|
|
332
|
+
Twelve journal events carry the detail: `suite-failed`, `attributed`, `fix-dispatched`,
|
|
303
333
|
`fix-resolved`, `member-evicted`, `revert-conflict`, `batch-rebased`, `batch-dissolved`,
|
|
304
|
-
`batch-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
334
|
+
`batch-preserved` (#563 — the dissolve threshold was crossed but the survivors' re-run
|
|
335
|
+
suite came back green, so the batch ships them instead of dissolving), `batch-blocked`
|
|
336
|
+
(#562 — the suite report was unreadable), `batch-split` and `milestone-post-failed`, plus
|
|
337
|
+
`git-failed` for any git command that returned non-zero (the injected `ExecFn` collapses
|
|
338
|
+
every git failure into `null`, so the command that produced one is always recorded).
|
|
308
339
|
|
|
309
340
|
Schema 1.3.0 carries the new state: `BatchEntry` gains `anchor`, `branch`, `run_id`,
|
|
310
341
|
`eviction_groups`, `evictions`, `fix_attempts` and `rebase_attempts`; `QueueEntry` gains
|
|
@@ -483,8 +514,8 @@ import {
|
|
|
483
514
|
beginFixAttempt, // the ONE bounded mid-tier fix dispatch instruction
|
|
484
515
|
resolveFixAttempt, // record its outcome, back to validating
|
|
485
516
|
evictMembers, // revert + requeue with evidence + suite re-run + dissolve check
|
|
486
|
-
checkDissolveTrigger, // pure: >
|
|
487
|
-
dissolveBatch, // full
|
|
517
|
+
checkDissolveTrigger, // pure: evicted > max(ceil(N × fraction), min floor) — dissolve_policy, #563
|
|
518
|
+
dissolveBatch, // full | halved | partial (#563); preserves everything green
|
|
488
519
|
blockBatch, // #562: unreadable suite report → blocked; no requeue, no revert
|
|
489
520
|
type BlockOptions, // { reason, milestonePhase? } for blockBatch
|
|
490
521
|
handlePrConflict, // rebase + re-ship once, then dissolve into halves
|
|
@@ -515,6 +546,11 @@ import {
|
|
|
515
546
|
type SchedRunLogInput, // buildSchedRunLogEntry's input shape
|
|
516
547
|
dispatchLogPath, // <runsDir>/<unit>.log — shared by spawn (offset) and record (read)
|
|
517
548
|
fileSizeOrZero, // byte size of the dispatch log at spawn time, or 0
|
|
549
|
+
// #564: reconstruct a batch's dispatch costs from raw per-unit logs on
|
|
550
|
+
// disk, for batches with no runs.jsonl coverage (pre-#564, or torn down)
|
|
551
|
+
listBatchDispatchLogs, // every raw dispatch log found for a batch id, parsed from its filename
|
|
552
|
+
buildBatchRunLogEntries, // ...to RunLogEntry rows, same shape a live dispatch produces
|
|
553
|
+
type BatchLogEntry, // one parsed log entry (member/tail/report/fix)
|
|
518
554
|
runBatchTick, // #523: one batch reconcile+refill pass; called by tick() after
|
|
519
555
|
// the issue pass — loads/saves state itself, holds no lock
|
|
520
556
|
// across the call
|
|
@@ -551,7 +587,8 @@ telemetry" below.
|
|
|
551
587
|
├── config.json # durable intent: max_slots, stall_timeout_ms, reconcile_interval_ms,
|
|
552
588
|
│ # pr_poll_interval_ms, dispatch (incl. report_prompt,
|
|
553
589
|
│ # phase_stall_timeout_ms, fence_takeover_timeout_ms, tiers — #527,
|
|
554
|
-
│ # suite_command — #562), auto_upgrade — #537
|
|
590
|
+
│ # suite_command — #562), auto_upgrade — #537,
|
|
591
|
+
│ # dissolve_policy — #563
|
|
555
592
|
├── events.jsonl # append-only event journal (the operator's flight recorder)
|
|
556
593
|
├── runs/ # per-unit agent output logs (issue-<n>.log)
|
|
557
594
|
└── .sched-lock/ # cross-process directory mutex (pid; stolen from dead holders)
|
|
@@ -607,7 +644,25 @@ journaled `run-log-no-usage` with a `reason` — `log-unreadable`, `log-empty`,
|
|
|
607
644
|
`no-usage-events` — so a row of dashes in `sched stats` can be explained without
|
|
608
645
|
re-deriving it. A successful append is journaled `run-log-recorded`; a failed one,
|
|
609
646
|
`run-log-failed` with the target file. Dispatches ended by `sched abandon` release the
|
|
610
|
-
slot without recording, so they are not costed.
|
|
647
|
+
slot without recording, so they are not costed. (`finalizeRunLogEntry` in `run-log.ts`
|
|
648
|
+
is the single implementation of this journal-then-append tail, shared by
|
|
649
|
+
`recordDispatchRunLog` here and batch dispatch's `recordMemberRunLog` below — #564.)
|
|
650
|
+
|
|
651
|
+
**Batch members (#564).** `batch-dispatch.ts` spawns members/tail/report/fix agents
|
|
652
|
+
directly (`deps.spawnDeps.spawn()`), bypassing `recordDispatchRunLog` above entirely —
|
|
653
|
+
`runs.jsonl` had zero coverage for batches even after #524/#531 shipped the per-issue
|
|
654
|
+
capture. `recordMemberRunLog` (`batch-dispatch.ts`) closes that gap for MEMBER
|
|
655
|
+
dispatches, attributed to the same `issue:<n>` unit scheme ordinary dispatches use, so a
|
|
656
|
+
member's cost shows up in the default `sched stats` view with no new read-side logic.
|
|
657
|
+
Tail/report/fix agents still never write to `runs.jsonl` (wiring that in needs each of
|
|
658
|
+
their spawn functions to stamp `SlotEntry.spawned_at` first, same as the original
|
|
659
|
+
member bug); `sched stats --batch <id>` (`packages/sched/src/batch-stats.ts`) instead
|
|
660
|
+
recovers their cost — and any historical batch's, predating #564 or already torn down —
|
|
661
|
+
by reading the raw dispatch logs on disk directly, the same recovery a human previously
|
|
662
|
+
did by hand (`docs/reports/batch-pilot-2-execution.md` §13). Tokens/cost/model reproduce
|
|
663
|
+
exactly; `Duration`/`Tier` are always `-` for a `--batch`-reconstructed row (a raw log
|
|
664
|
+
carries neither the dispatch's spawn time nor its tier) — a structural limit of
|
|
665
|
+
after-the-fact recovery, not a missing-data bug.
|
|
611
666
|
|
|
612
667
|
- **Crash safety**: a process killed between writes leaves the previous complete state,
|
|
613
668
|
never a partial file; restart resumes identically (proved by `restart.test.ts`) —
|
package/dist/batch-dispatch.d.ts
CHANGED
|
@@ -114,6 +114,11 @@ export interface BatchDispatchDeps {
|
|
|
114
114
|
*/
|
|
115
115
|
runCapability?: (worktree: string, capabilityId: string) => CapOutcome;
|
|
116
116
|
fsExists?: FsExists;
|
|
117
|
+
/**
|
|
118
|
+
* Home directory for `~/.dossier/runs.jsonl` (#564) — mirrors `EngineDeps.homeDir`.
|
|
119
|
+
* Undefined defers to `appendSchedRunLog`'s own `os.homedir()` default.
|
|
120
|
+
*/
|
|
121
|
+
homeDir?: string;
|
|
117
122
|
}
|
|
118
123
|
/** What one `runBatchTick` call did, merged into `engine.ts`'s `TickResult` by the caller. */
|
|
119
124
|
export interface BatchTickResult {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"batch-dispatch.d.ts","sourceRoot":"","sources":["../src/batch-dispatch.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6DG;AAiBH,OAAO,
|
|
1
|
+
{"version":3,"file":"batch-dispatch.d.ts","sourceRoot":"","sources":["../src/batch-dispatch.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6DG;AAiBH,OAAO,EASL,KAAK,gBAAgB,EAErB,KAAK,SAAS,EACf,MAAM,YAAY,CAAC;AACpB,OAAO,EACL,KAAK,WAAW,EAOjB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,KAAK,OAAO,EAAa,MAAM,WAAW,CAAC;AACpD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAC5C,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AACxC,OAAO,EAUL,KAAK,WAAW,EACjB,MAAM,YAAY,CAAC;AAYpB,OAAO,EAAE,KAAK,QAAQ,EAA2D,MAAM,YAAY,CAAC;AACpG,OAAO,KAAK,EAKV,WAAW,EAIZ,MAAM,SAAS,CAAC;AAGjB;;;;;;GAMG;AACH,MAAM,MAAM,UAAU,GAAG,IAAI,GAAG,aAAa,GAAG,mBAAmB,GAAG,wBAAwB,CAAC;AAE/F,8DAA8D;AAC9D,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,UAAU,CAAC;IAClB,OAAO,EAAE,OAAO,CAAC;IACjB,WAAW,EAAE,WAAW,CAAC;IACzB,SAAS,EAAE,SAAS,CAAC;IACrB,GAAG,EAAE,MAAM,IAAI,CAAC;IAChB,uFAAuF;IACvF,OAAO,EAAE,MAAM,CAAC;IAChB,0FAA0F;IAC1F,IAAI,EAAE,MAAM,CAAC;IACb;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,sGAAsG;IACtG,QAAQ,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,WAAW,CAAC;IAC5C;;;;;;;;;;;;;;OAcG;IACH,aAAa,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,KAAK,UAAU,CAAC;IACvE,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,8FAA8F;AAC9F,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,8FAA8F;IAC9F,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;AAiiDD;;;;;;;;;;GAUG;AACH,wBAAgB,YAAY,CAC1B,IAAI,EAAE,iBAAiB,EACvB,MAAM,EAAE,WAAW,EACnB,QAAQ,EAAE,gBAAgB,GACzB,eAAe,CAsEjB"}
|
package/dist/batch-dispatch.js
CHANGED
|
@@ -104,9 +104,11 @@ const dispatch_1 = require("./dispatch");
|
|
|
104
104
|
const groundtruth_1 = require("./groundtruth");
|
|
105
105
|
const journal_1 = require("./journal");
|
|
106
106
|
const recovery_1 = require("./recovery");
|
|
107
|
+
const run_log_1 = require("./run-log");
|
|
107
108
|
const scheduler_1 = require("./scheduler");
|
|
108
109
|
const state_1 = require("./state");
|
|
109
110
|
const teardown_1 = require("./teardown");
|
|
111
|
+
const types_1 = require("./types");
|
|
110
112
|
function emptyResult() {
|
|
111
113
|
return { spawned: [], completed: [], parked: [], mergeAccepted: [], failed: [], blocked: [] };
|
|
112
114
|
}
|
|
@@ -190,13 +192,14 @@ function safeSuite(deps, batchId, worktree) {
|
|
|
190
192
|
return { ok: false, failing: [], readable: false, detail };
|
|
191
193
|
}
|
|
192
194
|
}
|
|
193
|
-
function recoveryDeps(deps, batch, now) {
|
|
195
|
+
function recoveryDeps(deps, config, batch, now) {
|
|
194
196
|
return {
|
|
195
197
|
exec: deps.exec,
|
|
196
198
|
repoDir: batch.worktree ?? deps.repoDir,
|
|
197
199
|
journal: deps.journal,
|
|
198
200
|
postMilestone: (0, recovery_1.createExecMilestonePoster)(deps.exec, { repoDir: deps.repoDir }),
|
|
199
201
|
runSuite: batch.worktree !== null ? () => deps.runSuite(batch.worktree) : undefined,
|
|
202
|
+
dissolvePolicy: (0, types_1.resolveDissolvePolicy)(config.dissolve_policy),
|
|
200
203
|
now: () => now,
|
|
201
204
|
};
|
|
202
205
|
}
|
|
@@ -467,7 +470,7 @@ function spawnMember(deps, dispatch, state, slot, batchId, now, result) {
|
|
|
467
470
|
const spawnSpec = (0, dispatch_1.resolveTierSpawn)(dispatch, tier, memberIssue);
|
|
468
471
|
const cmd = spawnSpec.cmd;
|
|
469
472
|
const prompt = (0, dispatch_1.buildMemberPrompt)(dispatch.memberPrompt, memberIssue, batchId, batch.worktree);
|
|
470
|
-
const logFile =
|
|
473
|
+
const logFile = (0, dispatch_1.batchMemberLogPath)(deps.store.runsDir, batchId, batch.executing_member, memberIssue);
|
|
471
474
|
let pid;
|
|
472
475
|
try {
|
|
473
476
|
pid = deps.spawnDeps.spawn(cmd, prompt, logFile);
|
|
@@ -486,6 +489,10 @@ function spawnMember(deps, dispatch, state, slot, batchId, now, result) {
|
|
|
486
489
|
pid_start: deps.spawnDeps.processStart(pid),
|
|
487
490
|
phase: 'member',
|
|
488
491
|
last_progress_at: now.toISOString(),
|
|
492
|
+
// #564: `engine.ts`'s own `spawnUnit` stamps this at spawn time
|
|
493
|
+
// (line ~745) — `spawnMember` never did, so `recordMemberRunLog`'s
|
|
494
|
+
// `slot.spawned_at === null` guard silently skipped every member.
|
|
495
|
+
spawned_at: now.toISOString(),
|
|
489
496
|
};
|
|
490
497
|
const next = slot.status === 'assigned' || slot.status === 'recovering'
|
|
491
498
|
? (0, state_1.transitionSlot)(withStatus, slot.id, 'running', patch, now)
|
|
@@ -615,6 +622,18 @@ function claimAndSpawn(deps, config, batchId, phase, now, spawn) {
|
|
|
615
622
|
function spawnMemberContinuation(deps, config, dispatch, batchId, now, result) {
|
|
616
623
|
claimAndSpawn(deps, config, batchId, 'member', now, (state, slot) => spawnMember(deps, dispatch, state, slot, batchId, now, result));
|
|
617
624
|
}
|
|
625
|
+
/**
|
|
626
|
+
* Tail/report/fix dispatches (this function, `spawnReportAgent`,
|
|
627
|
+
* `reconcileFixSlot`) are NOT recorded live to `runs.jsonl` — only member
|
|
628
|
+
* dispatches got that treatment in #564 (`spawnMember`'s call into
|
|
629
|
+
* `recordMemberRunLog`). `sched stats --batch <id>` (`batch-stats.ts`) is
|
|
630
|
+
* the only way to see their cost today, reconstructed from the raw log
|
|
631
|
+
* after the fact. Wiring in live recording for these later means repeating
|
|
632
|
+
* `spawnMember`'s own #564 fix first: none of these three spawn functions'
|
|
633
|
+
* patches stamp `SlotEntry.spawned_at` either, so a `recordXRunLog` guarded
|
|
634
|
+
* on `spawned_at !== null` (mirroring `recordMemberRunLog`) would silently
|
|
635
|
+
* no-op forever, exactly like the original bug.
|
|
636
|
+
*/
|
|
618
637
|
function spawnTailAgent(deps, config, dispatch, batchId, now, result) {
|
|
619
638
|
claimAndSpawn(deps, config, batchId, 'reviewing', now, (state, slot) => {
|
|
620
639
|
const batch = (0, state_1.findBatch)(state, batchId);
|
|
@@ -628,7 +647,7 @@ function spawnTailAgent(deps, config, dispatch, batchId, now, result) {
|
|
|
628
647
|
const spawnSpec = (0, dispatch_1.resolveTierSpawn)(dispatch, 'strong', batch.anchor);
|
|
629
648
|
const cmd = spawnSpec.cmd;
|
|
630
649
|
const prompt = (0, dispatch_1.buildBatchTailPrompt)(dispatch.batchTailPrompt, batchId, batch.anchor, batch.members, batch.worktree);
|
|
631
|
-
const logFile =
|
|
650
|
+
const logFile = (0, dispatch_1.batchTailLogPath)(deps.store.runsDir, batchId);
|
|
632
651
|
let pid;
|
|
633
652
|
try {
|
|
634
653
|
pid = deps.spawnDeps.spawn(cmd, prompt, logFile);
|
|
@@ -678,7 +697,7 @@ function spawnReportAgent(deps, config, dispatch, batchId, now, result) {
|
|
|
678
697
|
const spawnSpec = (0, dispatch_1.resolveTierSpawn)(dispatch, 'mechanical', batch.anchor);
|
|
679
698
|
const cmd = spawnSpec.cmd;
|
|
680
699
|
const prompt = (0, dispatch_1.buildBatchReportPrompt)(dispatch.batchReportPrompt, batchId, batch.anchor, prNumber);
|
|
681
|
-
const logFile =
|
|
700
|
+
const logFile = (0, dispatch_1.batchReportLogPath)(deps.store.runsDir, batchId);
|
|
682
701
|
let pid;
|
|
683
702
|
try {
|
|
684
703
|
pid = deps.spawnDeps.spawn(cmd, prompt, logFile);
|
|
@@ -747,7 +766,7 @@ function runValidate(deps, config, dispatch, batchId, now, result) {
|
|
|
747
766
|
if (!batch || batch.worktree === null)
|
|
748
767
|
return;
|
|
749
768
|
const suite = safeSuite(deps, batchId, batch.worktree);
|
|
750
|
-
const rDeps = recoveryDeps(deps, batch, now);
|
|
769
|
+
const rDeps = recoveryDeps(deps, config, batch, now);
|
|
751
770
|
const poster = (0, recovery_1.createExecMilestonePoster)(deps.exec, { repoDir: deps.repoDir });
|
|
752
771
|
if (suite.ok) {
|
|
753
772
|
if (batch.anchor !== null && batch.run_id !== null) {
|
|
@@ -810,7 +829,7 @@ function runValidate(deps, config, dispatch, batchId, now, result) {
|
|
|
810
829
|
return;
|
|
811
830
|
}
|
|
812
831
|
claimAndSpawn(deps, config, batchId, 'fixing', now, (s, slot) => {
|
|
813
|
-
const logFile =
|
|
832
|
+
const logFile = (0, dispatch_1.batchFixLogPath)(deps.store.runsDir, batchId, offender);
|
|
814
833
|
let pid;
|
|
815
834
|
try {
|
|
816
835
|
pid = deps.spawnDeps.spawn(fixDispatch.command, fixDispatch.prompt, logFile);
|
|
@@ -839,12 +858,12 @@ function runValidate(deps, config, dispatch, batchId, now, result) {
|
|
|
839
858
|
return next;
|
|
840
859
|
});
|
|
841
860
|
}
|
|
842
|
-
function evictOffender(deps,
|
|
861
|
+
function evictOffender(deps, config, batchId, offender, attribution, now, result) {
|
|
843
862
|
const state = deps.store.load();
|
|
844
863
|
const batch = (0, state_1.findBatch)(state, batchId);
|
|
845
864
|
if (!batch)
|
|
846
865
|
return;
|
|
847
|
-
const rDeps = recoveryDeps(deps, batch, now);
|
|
866
|
+
const rDeps = recoveryDeps(deps, config, batch, now);
|
|
848
867
|
const outcome = (0, recovery_1.evictMembers)(state, batchId, { issues: [offender], reason: 'suite-red-after-fix', attribution, ranges: batch.ranges }, rDeps);
|
|
849
868
|
deps.store.withLock((s) => ({
|
|
850
869
|
state: applyBatchAndIssues(s, outcome.state, batchId, outcome.requeued),
|
|
@@ -900,13 +919,58 @@ function advanceMemberOrValidate(deps, config, dispatch, batchId, memberCount, c
|
|
|
900
919
|
* (self-reported blocked, and the incremental gate below).
|
|
901
920
|
*/
|
|
902
921
|
function evictMemberAndContinue(deps, config, dispatch, batchId, batch, memberIssue, reason, now, result) {
|
|
903
|
-
const dissolved = evictMemberDirectly(deps, batchId, memberIssue, reason, now);
|
|
922
|
+
const dissolved = evictMemberDirectly(deps, config, batchId, memberIssue, reason, now);
|
|
904
923
|
if (dissolved) {
|
|
905
924
|
result.failed.push(unit(batchId));
|
|
906
925
|
return;
|
|
907
926
|
}
|
|
908
927
|
advanceMemberOrValidate(deps, config, dispatch, batchId, batch.members.length, batch.executing_member, memberIssue, now, result);
|
|
909
928
|
}
|
|
929
|
+
/**
|
|
930
|
+
* Record one member dispatch's tokens/cost to `runs.jsonl` (#564) — the
|
|
931
|
+
* `batch-dispatch.ts` analogue of `engine.ts`'s `recordDispatchRunLog`.
|
|
932
|
+
* Batch members never go through `engine.ts`'s per-unit spawn/record path
|
|
933
|
+
* (`spawnMember` calls `deps.spawnDeps.spawn()` directly), so #524's capture
|
|
934
|
+
* never covered them; this closes that gap using the exact same
|
|
935
|
+
* `buildSchedRunLogEntry`/`appendSchedRunLog` machinery, attributed to
|
|
936
|
+
* `issue:<memberIssue>` — the SAME unit scheme ordinary issue dispatches use,
|
|
937
|
+
* so a member's cost shows up in the default `sched stats` view with no new
|
|
938
|
+
* unit format for the read side to special-case.
|
|
939
|
+
*
|
|
940
|
+
* Exactly-once per dispatch, mirroring `recordDispatchRunLog`'s own
|
|
941
|
+
* invariant: called from both of `reconcileMemberSlot`'s exit branches
|
|
942
|
+
* (member complete, member blocked/dead) — a batch never redispatches the
|
|
943
|
+
* same member slot (eviction requeues it as an independent full-cycle run
|
|
944
|
+
* instead), so unlike `engine.ts`'s per-unit log, a member's log file is
|
|
945
|
+
* always one-shot and reading from offset 0 is always correct.
|
|
946
|
+
*/
|
|
947
|
+
function recordMemberRunLog(deps, dispatch, state, batchId, batch, memberIssue, slot, now) {
|
|
948
|
+
if (slot.status !== 'running' || slot.spawned_at === null) {
|
|
949
|
+
journalEvent(deps, 'run-log-skipped', unit(batchId), {
|
|
950
|
+
issue: memberIssue,
|
|
951
|
+
reason: slot.spawned_at === null ? 'never-spawned' : `already-recorded-${slot.status}`,
|
|
952
|
+
slot: slot.id,
|
|
953
|
+
});
|
|
954
|
+
return;
|
|
955
|
+
}
|
|
956
|
+
const tier = (0, state_1.findEntry)(state, memberIssue)?.tier ?? 'mid';
|
|
957
|
+
const { cmd, model } = (0, dispatch_1.resolveTierSpawn)(dispatch, tier, memberIssue);
|
|
958
|
+
const logFile = (0, dispatch_1.batchMemberLogPath)(deps.store.runsDir, batchId, batch.executing_member, memberIssue);
|
|
959
|
+
const logContent = (0, run_log_1.readDispatchLog)(logFile, 0);
|
|
960
|
+
const runEntry = (0, run_log_1.buildSchedRunLogEntry)({
|
|
961
|
+
unit: `issue:${memberIssue}`,
|
|
962
|
+
role: 'batch-member',
|
|
963
|
+
cmd0: cmd[0],
|
|
964
|
+
cmd,
|
|
965
|
+
logContent,
|
|
966
|
+
spawnedAt: slot.spawned_at,
|
|
967
|
+
completedAt: now,
|
|
968
|
+
configuredModel: model,
|
|
969
|
+
cwd: deps.repoDir,
|
|
970
|
+
tier,
|
|
971
|
+
});
|
|
972
|
+
(0, run_log_1.finalizeRunLogEntry)(runEntry, logContent, deps.homeDir, (event, extra) => journalEvent(deps, event, unit(batchId), extra), { issue: memberIssue, log: logFile });
|
|
973
|
+
}
|
|
910
974
|
function reconcileMemberSlot(deps, config, dispatch, batchId, slot, now, result) {
|
|
911
975
|
const state0 = deps.store.load();
|
|
912
976
|
const batch = (0, state_1.findBatch)(state0, batchId);
|
|
@@ -924,6 +988,10 @@ function reconcileMemberSlot(deps, config, dispatch, batchId, slot, now, result)
|
|
|
924
988
|
issue: memberIssue,
|
|
925
989
|
detail: 'member review done',
|
|
926
990
|
}), now);
|
|
991
|
+
// The member's own agent process is done regardless of what the
|
|
992
|
+
// incremental gate below decides — record its telemetry once here (#564)
|
|
993
|
+
// rather than at each of this branch's two later exit points.
|
|
994
|
+
recordMemberRunLog(deps, dispatch, state0, batchId, batch, memberIssue, slot, now);
|
|
927
995
|
// Incremental gate (#523 AC2): typecheck + focused tests via `cap run`,
|
|
928
996
|
// when the repo has a manifest for them — a second, independent check
|
|
929
997
|
// that the member's own self-reported "done" is real, matching this
|
|
@@ -973,6 +1041,7 @@ function reconcileMemberSlot(deps, config, dispatch, batchId, slot, now, result)
|
|
|
973
1041
|
: dead
|
|
974
1042
|
? 'agent-exited-unverified'
|
|
975
1043
|
: 'member-blocked';
|
|
1044
|
+
recordMemberRunLog(deps, dispatch, state0, batchId, batch, memberIssue, slot, now);
|
|
976
1045
|
journalEvent(deps, 'unit-failed', unit(batchId), {
|
|
977
1046
|
issue: memberIssue,
|
|
978
1047
|
reason,
|
|
@@ -997,7 +1066,8 @@ function reconcileMemberSlot(deps, config, dispatch, batchId, slot, now, result)
|
|
|
997
1066
|
* and a commit range to revert; a member evicted here has neither). Returns
|
|
998
1067
|
* whether the batch dissolved.
|
|
999
1068
|
*/
|
|
1000
|
-
function evictMemberDirectly(deps, batchId, memberIssue, reason, now) {
|
|
1069
|
+
function evictMemberDirectly(deps, config, batchId, memberIssue, reason, now) {
|
|
1070
|
+
const dissolvePolicy = (0, types_1.resolveDissolvePolicy)(config.dissolve_policy);
|
|
1001
1071
|
// Pass 1 (pure — requeue + record the eviction): safe to run entirely
|
|
1002
1072
|
// inside the lock, unlike `dissolveBatch` below, which shells out
|
|
1003
1073
|
// (`deps.exec`/`postMilestone`) and so must NOT hold the lock while it runs.
|
|
@@ -1029,7 +1099,10 @@ function evictMemberDirectly(deps, batchId, memberIssue, reason, now) {
|
|
|
1029
1099
|
],
|
|
1030
1100
|
}, now);
|
|
1031
1101
|
const updated = (0, state_1.findBatch)(next, batchId);
|
|
1032
|
-
return {
|
|
1102
|
+
return {
|
|
1103
|
+
state: next,
|
|
1104
|
+
result: updated !== undefined && (0, recovery_1.checkDissolveTrigger)(updated, dissolvePolicy),
|
|
1105
|
+
};
|
|
1033
1106
|
});
|
|
1034
1107
|
if (!triggered)
|
|
1035
1108
|
return false;
|
|
@@ -1040,7 +1113,7 @@ function evictMemberDirectly(deps, batchId, memberIssue, reason, now) {
|
|
|
1040
1113
|
const batch = (0, state_1.findBatch)(state, batchId);
|
|
1041
1114
|
if (!batch)
|
|
1042
1115
|
return false;
|
|
1043
|
-
const rDeps = recoveryDeps(deps, batch, now);
|
|
1116
|
+
const rDeps = recoveryDeps(deps, config, batch, now);
|
|
1044
1117
|
const outcome = (0, recovery_1.dissolveBatch)(state, batchId, { strategy: 'full', reason: 'eviction-threshold' }, rDeps);
|
|
1045
1118
|
deps.store.withLock((s) => ({
|
|
1046
1119
|
state: applyBatchAndIssues(s, outcome.state, batchId, outcome.requeued),
|
|
@@ -1061,7 +1134,7 @@ function reconcileFixSlot(deps, config, batchId, slot, now, result) {
|
|
|
1061
1134
|
return;
|
|
1062
1135
|
deps.store.withLock((s) => ({ state: releaseSlot(s, batchId, now), result: undefined }));
|
|
1063
1136
|
const suite = safeSuite(deps, batchId, batch.worktree);
|
|
1064
|
-
const rDeps = recoveryDeps(deps, batch, now);
|
|
1137
|
+
const rDeps = recoveryDeps(deps, config, batch, now);
|
|
1065
1138
|
const { state: resolved } = (0, recovery_1.resolveFixAttempt)(deps.store.load(), batchId, offenderRecord.issue, suite.ok ? 'green' : 'red', rDeps);
|
|
1066
1139
|
deps.store.withLock((s) => ({
|
|
1067
1140
|
state: applyBatchAndIssues(s, resolved, batchId, []),
|
|
@@ -1295,7 +1368,7 @@ function runBatchTick(deps, config, dispatch) {
|
|
|
1295
1368
|
? [...b.fix_attempts].reverse().find((a) => a.outcome === 'dispatched')
|
|
1296
1369
|
: undefined;
|
|
1297
1370
|
if (b && offenderRecord) {
|
|
1298
|
-
const rDeps = recoveryDeps(deps, b, now);
|
|
1371
|
+
const rDeps = recoveryDeps(deps, config, b, now);
|
|
1299
1372
|
const { state: resolved } = (0, recovery_1.resolveFixAttempt)(state, batch.id, offenderRecord.issue, 'red', rDeps);
|
|
1300
1373
|
deps.store.withLock((s) => ({
|
|
1301
1374
|
state: applyBatchAndIssues(s, resolved, batch.id, []),
|