@ai-dossier/sched 0.17.0 → 0.18.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 +92 -9
- package/dist/batch-dispatch.d.ts +16 -8
- package/dist/batch-dispatch.d.ts.map +1 -1
- package/dist/batch-dispatch.js +107 -15
- 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 +4 -2
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +49 -44
- package/dist/engine.js.map +1 -1
- package/dist/enqueue.d.ts +22 -0
- package/dist/enqueue.d.ts.map +1 -1
- package/dist/enqueue.js +33 -0
- package/dist/enqueue.js.map +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +14 -4
- package/dist/index.js.map +1 -1
- package/dist/persist.d.ts.map +1 -1
- package/dist/persist.js +10 -1
- package/dist/persist.js.map +1 -1
- package/dist/readiness.d.ts +38 -4
- package/dist/readiness.d.ts.map +1 -1
- package/dist/readiness.js +57 -8
- package/dist/readiness.js.map +1 -1
- package/dist/recovery.d.ts.map +1 -1
- package/dist/recovery.js +5 -0
- 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/scheduler.d.ts +29 -4
- package/dist/scheduler.d.ts.map +1 -1
- package/dist/scheduler.js +62 -4
- package/dist/scheduler.js.map +1 -1
- package/dist/state.d.ts +5 -2
- package/dist/state.d.ts.map +1 -1
- package/dist/state.js +29 -1
- package/dist/state.js.map +1 -1
- package/dist/types.d.ts +40 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +15 -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
|
|
@@ -183,6 +187,12 @@ strategy and the `batch-preserved` journal event (see Batch failure recovery abo
|
|
|
183
187
|
are behavioral, not persisted-shape changes, so they carry no schema-version bump of their
|
|
184
188
|
own.
|
|
185
189
|
|
|
190
|
+
Config schema moves to 1.8.0 (#565): a new top-level `default_batch_priority` key
|
|
191
|
+
(integer) — the `BatchEntry.priority` a batch gets when created with no explicit
|
|
192
|
+
`batch_priority` (see Unit priority below). Absent → `DEFAULT_BATCH_PRIORITY` (10); an
|
|
193
|
+
invalid value degrades the whole config file to built-in defaults, same contract as every
|
|
194
|
+
other field.
|
|
195
|
+
|
|
186
196
|
Two engine-safety policies were explicit product decisions on #464:
|
|
187
197
|
|
|
188
198
|
- **Pid identity is hybrid-verified (decision 1, option C).** Every spawn records the
|
|
@@ -377,6 +387,49 @@ load, backfilling `null`: no label re-check ever ran under them, so the first ti
|
|
|
377
387
|
the upgrade polls immediately rather than waiting out a throttle window it has no
|
|
378
388
|
evidence for — the exact backfill, not a guess.
|
|
379
389
|
|
|
390
|
+
Schema 1.10.0 (#565): `QueueEntry` gains `priority` (integer, default 0) and `BatchEntry`
|
|
391
|
+
gains `priority` (integer, default `DEFAULT_BATCH_PRIORITY` = 10) — see Unit priority
|
|
392
|
+
below. 1.9.0 and earlier states migrate on load, backfilling absent OR explicit `null` to
|
|
393
|
+
those same defaults: nothing before this field existed was ever weighted differently, so
|
|
394
|
+
the backfill is exact, not a guess.
|
|
395
|
+
|
|
396
|
+
## Unit priority (#565)
|
|
397
|
+
|
|
398
|
+
`readiness.ts`'s `runnableUnits` ranks EVERY candidate — issues and batches together — by
|
|
399
|
+
`priority` desc, then readiness age (`updated_at`) asc, then a numeric tiebreak (an
|
|
400
|
+
issue's own number, or a batch's anchor issue) asc, via the exported `compareByPriority`
|
|
401
|
+
comparator and its `entryRank`/`batchRank` helpers. A batch's default priority
|
|
402
|
+
(`DEFAULT_BATCH_PRIORITY`, 10, or the configured `default_batch_priority`) outranks a
|
|
403
|
+
full-cycle entry's default (0) so a ready batch is offered a free slot before a
|
|
404
|
+
same-readiness issue competing for it — closing the gap
|
|
405
|
+
`docs/reports/batch-pilot-2-execution.md` §13.4 found (an operator manually deferring
|
|
406
|
+
full-cycle entries by hand so a batch could claim its slot).
|
|
407
|
+
|
|
408
|
+
Applying that ordering to who actually DISPATCHES took more than sorting: `engine.ts`'s
|
|
409
|
+
`dispatchAssignments` (the issue-only dispatch pass) now runs `computeAssignments` as a
|
|
410
|
+
READ-ONLY dry run over both kinds each tick, discards the returned state, and applies
|
|
411
|
+
only the issue winners — reserving a free slot for a higher-priority ready batch instead
|
|
412
|
+
of handing it to a same-tick issue. `batch-dispatch.ts`'s own ready-batch claim loop
|
|
413
|
+
(which never goes through `computeAssignments`/`runnableUnits` itself) later that same
|
|
414
|
+
tick claims the capacity the reservation left free, sorted by the same comparator when
|
|
415
|
+
more than one batch is ready. The reservation is gated on the batch pass actually being
|
|
416
|
+
configured (`batchExec`/`runBatchSuite`) — without that gate, a `ready` batch with
|
|
417
|
+
nothing able to claim it would withdraw capacity from issues forever instead of one tick.
|
|
418
|
+
|
|
419
|
+
`sched enqueue --priority <n>` sets a full-cycle entry's own priority; with `--mode slot`
|
|
420
|
+
it instead sets the BATCH's priority — a batch-level fact like `anchor`/`run_id`,
|
|
421
|
+
agreement-checked on a later join (an incremental `--more-members-expected` call, or a
|
|
422
|
+
manifest split across `--from-manifest` calls, must never silently re-point it). `sched
|
|
423
|
+
reprioritize --issue <n>|--batch <id> --priority <n>` adjusts a queued unit's weight in
|
|
424
|
+
place — no abandon/re-enqueue round trip, which would also reset every other field
|
|
425
|
+
`enqueueEntries` does not accept as a re-supply — and deliberately does not bump
|
|
426
|
+
`updated_at` (the readiness-age tiebreak), journaling a `reprioritized` event with the
|
|
427
|
+
previous value instead. `sched status`'s Queue and Batches tables both show a `priority`
|
|
428
|
+
column; a slot-mode member's own priority is never read by the scheduler (only the BATCH
|
|
429
|
+
row governs assignment), so the Queue table renders `-` for it rather than a number that
|
|
430
|
+
looks load-bearing but is not. A batch dissolve (`recovery.ts`'s `dissolveBatch`) carries
|
|
431
|
+
the parent batch's priority forward onto both split halves.
|
|
432
|
+
|
|
380
433
|
## Batch dispatch (#523)
|
|
381
434
|
|
|
382
435
|
`batch-dispatch.ts`'s `runBatchTick` — called from `tick()` after the issue-level pass,
|
|
@@ -453,7 +506,13 @@ import {
|
|
|
453
506
|
enqueueEntries, // validated queue appends (cycles, dupes, mode/batch rules)
|
|
454
507
|
parseManifest, // batch-prep JSON → EnqueueInput[]
|
|
455
508
|
computeAssignments, // pure: fill idle slots with runnable units, bounded by max_slots
|
|
456
|
-
runnableUnits, // pure: which units may run right now (dep-gated)
|
|
509
|
+
runnableUnits, // pure: which units may run right now (dep-gated), in assignment
|
|
510
|
+
// order — priority desc → readiness age → issue/anchor (#565)
|
|
511
|
+
compareByPriority, // the priority/age/tiebreak comparator runnableUnits sorts with —
|
|
512
|
+
// also used directly by batch-dispatch.ts's ready-batch claim loop
|
|
513
|
+
entryRank, batchRank, // PriorityRank of a QueueEntry / BatchEntry (#565)
|
|
514
|
+
reprioritizeIssue, // sched reprioritize --issue: adjust priority in place, no
|
|
515
|
+
reprioritizeBatch, // abandon/re-enqueue round trip; refuses a terminal unit
|
|
457
516
|
tick, // one engine cycle: reconcile + verify + refill + spawn,
|
|
458
517
|
// and since #468: park-watch, teardown, report dispatch
|
|
459
518
|
runLoop, // the sched start loop (tick, sleep, repeat)
|
|
@@ -528,7 +587,8 @@ import {
|
|
|
528
587
|
transitionIssue, transitionBatch, transitionSlot, // typed §D transitions
|
|
529
588
|
TRANSITIONS, // the transition tables themselves (for previews)
|
|
530
589
|
buildStatusReport, // machine-readable status incl. blocked/failed sets
|
|
531
|
-
validateState, // strict persisted-state validation (1.0.0-1.
|
|
590
|
+
validateState, // strict persisted-state validation (1.0.0-1.9.0 files migrate)
|
|
591
|
+
DEFAULT_ISSUE_PRIORITY, DEFAULT_BATCH_PRIORITY, // priority defaults (0 / 10, #565)
|
|
532
592
|
IllegalTransitionError, EnqueueError, CorruptStateError, LockTimeoutError,
|
|
533
593
|
SchedNotFoundError,
|
|
534
594
|
EngineTooOldError, // state schema newer than installed engine — not corruption (#537)
|
|
@@ -542,6 +602,11 @@ import {
|
|
|
542
602
|
type SchedRunLogInput, // buildSchedRunLogEntry's input shape
|
|
543
603
|
dispatchLogPath, // <runsDir>/<unit>.log — shared by spawn (offset) and record (read)
|
|
544
604
|
fileSizeOrZero, // byte size of the dispatch log at spawn time, or 0
|
|
605
|
+
// #564: reconstruct a batch's dispatch costs from raw per-unit logs on
|
|
606
|
+
// disk, for batches with no runs.jsonl coverage (pre-#564, or torn down)
|
|
607
|
+
listBatchDispatchLogs, // every raw dispatch log found for a batch id, parsed from its filename
|
|
608
|
+
buildBatchRunLogEntries, // ...to RunLogEntry rows, same shape a live dispatch produces
|
|
609
|
+
type BatchLogEntry, // one parsed log entry (member/tail/report/fix)
|
|
545
610
|
runBatchTick, // #523: one batch reconcile+refill pass; called by tick() after
|
|
546
611
|
// the issue pass — loads/saves state itself, holds no lock
|
|
547
612
|
// across the call
|
|
@@ -635,7 +700,25 @@ journaled `run-log-no-usage` with a `reason` — `log-unreadable`, `log-empty`,
|
|
|
635
700
|
`no-usage-events` — so a row of dashes in `sched stats` can be explained without
|
|
636
701
|
re-deriving it. A successful append is journaled `run-log-recorded`; a failed one,
|
|
637
702
|
`run-log-failed` with the target file. Dispatches ended by `sched abandon` release the
|
|
638
|
-
slot without recording, so they are not costed.
|
|
703
|
+
slot without recording, so they are not costed. (`finalizeRunLogEntry` in `run-log.ts`
|
|
704
|
+
is the single implementation of this journal-then-append tail, shared by
|
|
705
|
+
`recordDispatchRunLog` here and batch dispatch's `recordMemberRunLog` below — #564.)
|
|
706
|
+
|
|
707
|
+
**Batch members (#564).** `batch-dispatch.ts` spawns members/tail/report/fix agents
|
|
708
|
+
directly (`deps.spawnDeps.spawn()`), bypassing `recordDispatchRunLog` above entirely —
|
|
709
|
+
`runs.jsonl` had zero coverage for batches even after #524/#531 shipped the per-issue
|
|
710
|
+
capture. `recordMemberRunLog` (`batch-dispatch.ts`) closes that gap for MEMBER
|
|
711
|
+
dispatches, attributed to the same `issue:<n>` unit scheme ordinary dispatches use, so a
|
|
712
|
+
member's cost shows up in the default `sched stats` view with no new read-side logic.
|
|
713
|
+
Tail/report/fix agents still never write to `runs.jsonl` (wiring that in needs each of
|
|
714
|
+
their spawn functions to stamp `SlotEntry.spawned_at` first, same as the original
|
|
715
|
+
member bug); `sched stats --batch <id>` (`packages/sched/src/batch-stats.ts`) instead
|
|
716
|
+
recovers their cost — and any historical batch's, predating #564 or already torn down —
|
|
717
|
+
by reading the raw dispatch logs on disk directly, the same recovery a human previously
|
|
718
|
+
did by hand (`docs/reports/batch-pilot-2-execution.md` §13). Tokens/cost/model reproduce
|
|
719
|
+
exactly; `Duration`/`Tier` are always `-` for a `--batch`-reconstructed row (a raw log
|
|
720
|
+
carries neither the dispatch's spawn time nor its tier) — a structural limit of
|
|
721
|
+
after-the-fact recovery, not a missing-data bug.
|
|
639
722
|
|
|
640
723
|
- **Crash safety**: a process killed between writes leaves the previous complete state,
|
|
641
724
|
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 {
|
|
@@ -127,14 +132,17 @@ export interface BatchTickResult {
|
|
|
127
132
|
}
|
|
128
133
|
/**
|
|
129
134
|
* One batch reconcile+refill pass. Called from `engine.ts`'s `tick()` after
|
|
130
|
-
* the issue-level pass —
|
|
131
|
-
* the
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
*
|
|
135
|
+
* the issue-level pass — this pass never claims a slot the issue pass already
|
|
136
|
+
* gave to an issue (see the module doc: a batch's OWN claim never goes
|
|
137
|
+
* through `computeAssignments`/`runnableUnits`). It is not, however, run on
|
|
138
|
+
* leftovers: `dispatchAssignments` (#565) reserves capacity ahead of time for
|
|
139
|
+
* any ready batch that outranks a competing issue in `runnableUnits`'
|
|
140
|
+
* priority order, so a higher-priority batch is not starved by same-tick
|
|
141
|
+
* issue dispatch — see that function's doc for the reservation mechanics.
|
|
142
|
+
* Loads and saves state itself via `deps.store.withLock` — the caller holds
|
|
143
|
+
* no lock across this call. `deps.exec` and `deps.runSuite` are mandatory;
|
|
144
|
+
* `deps.runCapability` is independently optional (AC2's incremental gate is
|
|
145
|
+
* itself a "when available" fast path).
|
|
138
146
|
*/
|
|
139
147
|
export declare function runBatchTick(deps: BatchDispatchDeps, config: SchedConfig, dispatch: ResolvedDispatch): BatchTickResult;
|
|
140
148
|
//# sourceMappingURL=batch-dispatch.d.ts.map
|
|
@@ -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;AAExC,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;AAsiDD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,YAAY,CAC1B,IAAI,EAAE,iBAAiB,EACvB,MAAM,EAAE,WAAW,EACnB,QAAQ,EAAE,gBAAgB,GACzB,eAAe,CAsFjB"}
|
package/dist/batch-dispatch.js
CHANGED
|
@@ -103,7 +103,9 @@ const attribution_1 = require("./attribution");
|
|
|
103
103
|
const dispatch_1 = require("./dispatch");
|
|
104
104
|
const groundtruth_1 = require("./groundtruth");
|
|
105
105
|
const journal_1 = require("./journal");
|
|
106
|
+
const readiness_1 = require("./readiness");
|
|
106
107
|
const recovery_1 = require("./recovery");
|
|
108
|
+
const run_log_1 = require("./run-log");
|
|
107
109
|
const scheduler_1 = require("./scheduler");
|
|
108
110
|
const state_1 = require("./state");
|
|
109
111
|
const teardown_1 = require("./teardown");
|
|
@@ -469,7 +471,7 @@ function spawnMember(deps, dispatch, state, slot, batchId, now, result) {
|
|
|
469
471
|
const spawnSpec = (0, dispatch_1.resolveTierSpawn)(dispatch, tier, memberIssue);
|
|
470
472
|
const cmd = spawnSpec.cmd;
|
|
471
473
|
const prompt = (0, dispatch_1.buildMemberPrompt)(dispatch.memberPrompt, memberIssue, batchId, batch.worktree);
|
|
472
|
-
const logFile =
|
|
474
|
+
const logFile = (0, dispatch_1.batchMemberLogPath)(deps.store.runsDir, batchId, batch.executing_member, memberIssue);
|
|
473
475
|
let pid;
|
|
474
476
|
try {
|
|
475
477
|
pid = deps.spawnDeps.spawn(cmd, prompt, logFile);
|
|
@@ -488,6 +490,10 @@ function spawnMember(deps, dispatch, state, slot, batchId, now, result) {
|
|
|
488
490
|
pid_start: deps.spawnDeps.processStart(pid),
|
|
489
491
|
phase: 'member',
|
|
490
492
|
last_progress_at: now.toISOString(),
|
|
493
|
+
// #564: `engine.ts`'s own `spawnUnit` stamps this at spawn time
|
|
494
|
+
// (line ~745) — `spawnMember` never did, so `recordMemberRunLog`'s
|
|
495
|
+
// `slot.spawned_at === null` guard silently skipped every member.
|
|
496
|
+
spawned_at: now.toISOString(),
|
|
491
497
|
};
|
|
492
498
|
const next = slot.status === 'assigned' || slot.status === 'recovering'
|
|
493
499
|
? (0, state_1.transitionSlot)(withStatus, slot.id, 'running', patch, now)
|
|
@@ -529,6 +535,11 @@ function claimAndSetup(deps, config, dispatch, batchId, now, result) {
|
|
|
529
535
|
const batch = (0, state_1.findBatch)(state, batchId);
|
|
530
536
|
if (!batch)
|
|
531
537
|
return;
|
|
538
|
+
// #565 AC2: "journaled on each assignment" — the issue-dispatch path
|
|
539
|
+
// (`engine.ts`'s `dispatchAssignments`) already does this on its own
|
|
540
|
+
// 'assigned' event; mirrored here so a batch's claim carries the same
|
|
541
|
+
// audit trail.
|
|
542
|
+
journalEvent(deps, 'assigned', unit(batchId), { slot: claimedSlot, priority: batch.priority });
|
|
532
543
|
const setup = runBatchSetup(deps, batch, now);
|
|
533
544
|
const poster = (0, recovery_1.createExecMilestonePoster)(deps.exec, { repoDir: deps.repoDir });
|
|
534
545
|
if (!setup.ok) {
|
|
@@ -617,6 +628,18 @@ function claimAndSpawn(deps, config, batchId, phase, now, spawn) {
|
|
|
617
628
|
function spawnMemberContinuation(deps, config, dispatch, batchId, now, result) {
|
|
618
629
|
claimAndSpawn(deps, config, batchId, 'member', now, (state, slot) => spawnMember(deps, dispatch, state, slot, batchId, now, result));
|
|
619
630
|
}
|
|
631
|
+
/**
|
|
632
|
+
* Tail/report/fix dispatches (this function, `spawnReportAgent`,
|
|
633
|
+
* `reconcileFixSlot`) are NOT recorded live to `runs.jsonl` — only member
|
|
634
|
+
* dispatches got that treatment in #564 (`spawnMember`'s call into
|
|
635
|
+
* `recordMemberRunLog`). `sched stats --batch <id>` (`batch-stats.ts`) is
|
|
636
|
+
* the only way to see their cost today, reconstructed from the raw log
|
|
637
|
+
* after the fact. Wiring in live recording for these later means repeating
|
|
638
|
+
* `spawnMember`'s own #564 fix first: none of these three spawn functions'
|
|
639
|
+
* patches stamp `SlotEntry.spawned_at` either, so a `recordXRunLog` guarded
|
|
640
|
+
* on `spawned_at !== null` (mirroring `recordMemberRunLog`) would silently
|
|
641
|
+
* no-op forever, exactly like the original bug.
|
|
642
|
+
*/
|
|
620
643
|
function spawnTailAgent(deps, config, dispatch, batchId, now, result) {
|
|
621
644
|
claimAndSpawn(deps, config, batchId, 'reviewing', now, (state, slot) => {
|
|
622
645
|
const batch = (0, state_1.findBatch)(state, batchId);
|
|
@@ -630,7 +653,7 @@ function spawnTailAgent(deps, config, dispatch, batchId, now, result) {
|
|
|
630
653
|
const spawnSpec = (0, dispatch_1.resolveTierSpawn)(dispatch, 'strong', batch.anchor);
|
|
631
654
|
const cmd = spawnSpec.cmd;
|
|
632
655
|
const prompt = (0, dispatch_1.buildBatchTailPrompt)(dispatch.batchTailPrompt, batchId, batch.anchor, batch.members, batch.worktree);
|
|
633
|
-
const logFile =
|
|
656
|
+
const logFile = (0, dispatch_1.batchTailLogPath)(deps.store.runsDir, batchId);
|
|
634
657
|
let pid;
|
|
635
658
|
try {
|
|
636
659
|
pid = deps.spawnDeps.spawn(cmd, prompt, logFile);
|
|
@@ -680,7 +703,7 @@ function spawnReportAgent(deps, config, dispatch, batchId, now, result) {
|
|
|
680
703
|
const spawnSpec = (0, dispatch_1.resolveTierSpawn)(dispatch, 'mechanical', batch.anchor);
|
|
681
704
|
const cmd = spawnSpec.cmd;
|
|
682
705
|
const prompt = (0, dispatch_1.buildBatchReportPrompt)(dispatch.batchReportPrompt, batchId, batch.anchor, prNumber);
|
|
683
|
-
const logFile =
|
|
706
|
+
const logFile = (0, dispatch_1.batchReportLogPath)(deps.store.runsDir, batchId);
|
|
684
707
|
let pid;
|
|
685
708
|
try {
|
|
686
709
|
pid = deps.spawnDeps.spawn(cmd, prompt, logFile);
|
|
@@ -812,7 +835,7 @@ function runValidate(deps, config, dispatch, batchId, now, result) {
|
|
|
812
835
|
return;
|
|
813
836
|
}
|
|
814
837
|
claimAndSpawn(deps, config, batchId, 'fixing', now, (s, slot) => {
|
|
815
|
-
const logFile =
|
|
838
|
+
const logFile = (0, dispatch_1.batchFixLogPath)(deps.store.runsDir, batchId, offender);
|
|
816
839
|
let pid;
|
|
817
840
|
try {
|
|
818
841
|
pid = deps.spawnDeps.spawn(fixDispatch.command, fixDispatch.prompt, logFile);
|
|
@@ -909,6 +932,51 @@ function evictMemberAndContinue(deps, config, dispatch, batchId, batch, memberIs
|
|
|
909
932
|
}
|
|
910
933
|
advanceMemberOrValidate(deps, config, dispatch, batchId, batch.members.length, batch.executing_member, memberIssue, now, result);
|
|
911
934
|
}
|
|
935
|
+
/**
|
|
936
|
+
* Record one member dispatch's tokens/cost to `runs.jsonl` (#564) — the
|
|
937
|
+
* `batch-dispatch.ts` analogue of `engine.ts`'s `recordDispatchRunLog`.
|
|
938
|
+
* Batch members never go through `engine.ts`'s per-unit spawn/record path
|
|
939
|
+
* (`spawnMember` calls `deps.spawnDeps.spawn()` directly), so #524's capture
|
|
940
|
+
* never covered them; this closes that gap using the exact same
|
|
941
|
+
* `buildSchedRunLogEntry`/`appendSchedRunLog` machinery, attributed to
|
|
942
|
+
* `issue:<memberIssue>` — the SAME unit scheme ordinary issue dispatches use,
|
|
943
|
+
* so a member's cost shows up in the default `sched stats` view with no new
|
|
944
|
+
* unit format for the read side to special-case.
|
|
945
|
+
*
|
|
946
|
+
* Exactly-once per dispatch, mirroring `recordDispatchRunLog`'s own
|
|
947
|
+
* invariant: called from both of `reconcileMemberSlot`'s exit branches
|
|
948
|
+
* (member complete, member blocked/dead) — a batch never redispatches the
|
|
949
|
+
* same member slot (eviction requeues it as an independent full-cycle run
|
|
950
|
+
* instead), so unlike `engine.ts`'s per-unit log, a member's log file is
|
|
951
|
+
* always one-shot and reading from offset 0 is always correct.
|
|
952
|
+
*/
|
|
953
|
+
function recordMemberRunLog(deps, dispatch, state, batchId, batch, memberIssue, slot, now) {
|
|
954
|
+
if (slot.status !== 'running' || slot.spawned_at === null) {
|
|
955
|
+
journalEvent(deps, 'run-log-skipped', unit(batchId), {
|
|
956
|
+
issue: memberIssue,
|
|
957
|
+
reason: slot.spawned_at === null ? 'never-spawned' : `already-recorded-${slot.status}`,
|
|
958
|
+
slot: slot.id,
|
|
959
|
+
});
|
|
960
|
+
return;
|
|
961
|
+
}
|
|
962
|
+
const tier = (0, state_1.findEntry)(state, memberIssue)?.tier ?? 'mid';
|
|
963
|
+
const { cmd, model } = (0, dispatch_1.resolveTierSpawn)(dispatch, tier, memberIssue);
|
|
964
|
+
const logFile = (0, dispatch_1.batchMemberLogPath)(deps.store.runsDir, batchId, batch.executing_member, memberIssue);
|
|
965
|
+
const logContent = (0, run_log_1.readDispatchLog)(logFile, 0);
|
|
966
|
+
const runEntry = (0, run_log_1.buildSchedRunLogEntry)({
|
|
967
|
+
unit: `issue:${memberIssue}`,
|
|
968
|
+
role: 'batch-member',
|
|
969
|
+
cmd0: cmd[0],
|
|
970
|
+
cmd,
|
|
971
|
+
logContent,
|
|
972
|
+
spawnedAt: slot.spawned_at,
|
|
973
|
+
completedAt: now,
|
|
974
|
+
configuredModel: model,
|
|
975
|
+
cwd: deps.repoDir,
|
|
976
|
+
tier,
|
|
977
|
+
});
|
|
978
|
+
(0, run_log_1.finalizeRunLogEntry)(runEntry, logContent, deps.homeDir, (event, extra) => journalEvent(deps, event, unit(batchId), extra), { issue: memberIssue, log: logFile });
|
|
979
|
+
}
|
|
912
980
|
function reconcileMemberSlot(deps, config, dispatch, batchId, slot, now, result) {
|
|
913
981
|
const state0 = deps.store.load();
|
|
914
982
|
const batch = (0, state_1.findBatch)(state0, batchId);
|
|
@@ -926,6 +994,10 @@ function reconcileMemberSlot(deps, config, dispatch, batchId, slot, now, result)
|
|
|
926
994
|
issue: memberIssue,
|
|
927
995
|
detail: 'member review done',
|
|
928
996
|
}), now);
|
|
997
|
+
// The member's own agent process is done regardless of what the
|
|
998
|
+
// incremental gate below decides — record its telemetry once here (#564)
|
|
999
|
+
// rather than at each of this branch's two later exit points.
|
|
1000
|
+
recordMemberRunLog(deps, dispatch, state0, batchId, batch, memberIssue, slot, now);
|
|
929
1001
|
// Incremental gate (#523 AC2): typecheck + focused tests via `cap run`,
|
|
930
1002
|
// when the repo has a manifest for them — a second, independent check
|
|
931
1003
|
// that the member's own self-reported "done" is real, matching this
|
|
@@ -975,6 +1047,7 @@ function reconcileMemberSlot(deps, config, dispatch, batchId, slot, now, result)
|
|
|
975
1047
|
: dead
|
|
976
1048
|
? 'agent-exited-unverified'
|
|
977
1049
|
: 'member-blocked';
|
|
1050
|
+
recordMemberRunLog(deps, dispatch, state0, batchId, batch, memberIssue, slot, now);
|
|
978
1051
|
journalEvent(deps, 'unit-failed', unit(batchId), {
|
|
979
1052
|
issue: memberIssue,
|
|
980
1053
|
reason,
|
|
@@ -1234,21 +1307,40 @@ function teardownBatch(deps, batchId) {
|
|
|
1234
1307
|
// --- Entry point ---
|
|
1235
1308
|
/**
|
|
1236
1309
|
* One batch reconcile+refill pass. Called from `engine.ts`'s `tick()` after
|
|
1237
|
-
* the issue-level pass —
|
|
1238
|
-
* the
|
|
1239
|
-
*
|
|
1240
|
-
*
|
|
1241
|
-
*
|
|
1242
|
-
*
|
|
1243
|
-
*
|
|
1244
|
-
*
|
|
1310
|
+
* the issue-level pass — this pass never claims a slot the issue pass already
|
|
1311
|
+
* gave to an issue (see the module doc: a batch's OWN claim never goes
|
|
1312
|
+
* through `computeAssignments`/`runnableUnits`). It is not, however, run on
|
|
1313
|
+
* leftovers: `dispatchAssignments` (#565) reserves capacity ahead of time for
|
|
1314
|
+
* any ready batch that outranks a competing issue in `runnableUnits`'
|
|
1315
|
+
* priority order, so a higher-priority batch is not starved by same-tick
|
|
1316
|
+
* issue dispatch — see that function's doc for the reservation mechanics.
|
|
1317
|
+
* Loads and saves state itself via `deps.store.withLock` — the caller holds
|
|
1318
|
+
* no lock across this call. `deps.exec` and `deps.runSuite` are mandatory;
|
|
1319
|
+
* `deps.runCapability` is independently optional (AC2's incremental gate is
|
|
1320
|
+
* itself a "when available" fast path).
|
|
1245
1321
|
*/
|
|
1246
1322
|
function runBatchTick(deps, config, dispatch) {
|
|
1247
1323
|
const result = emptyResult();
|
|
1248
1324
|
const now = deps.now();
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1325
|
+
// #565: when more ready batches exist than free capacity, claim them in
|
|
1326
|
+
// the same priority order `runnableUnits` would (desc priority → asc
|
|
1327
|
+
// readiness age → anchor) — this loop never goes through
|
|
1328
|
+
// `computeAssignments`/`runnableUnits` itself (see the module doc), so it
|
|
1329
|
+
// applies the shared comparator/rank helpers directly. The ORDER and
|
|
1330
|
+
// MEMBERSHIP of `readyOrder` are frozen from a snapshot taken once up
|
|
1331
|
+
// front — a batch that becomes `ready` mid-pass waits for the next tick;
|
|
1332
|
+
// only the per-batch `slotFor`/`status` re-check inside the loop reads
|
|
1333
|
+
// fresh state (one `store.load()` per iteration, in case an earlier claim
|
|
1334
|
+
// in this same pass changed things).
|
|
1335
|
+
const readyOrder = [...deps.store.load().batches]
|
|
1336
|
+
.filter((b) => b.status === 'ready')
|
|
1337
|
+
.sort((a, b) => (0, readiness_1.compareByPriority)((0, readiness_1.batchRank)(a), (0, readiness_1.batchRank)(b)))
|
|
1338
|
+
.map((b) => b.id);
|
|
1339
|
+
for (const batchId of readyOrder) {
|
|
1340
|
+
const state = deps.store.load();
|
|
1341
|
+
const batch = (0, state_1.findBatch)(state, batchId);
|
|
1342
|
+
if (batch && batch.status === 'ready' && slotFor(state, batchId) === undefined) {
|
|
1343
|
+
claimAndSetup(deps, config, dispatch, batchId, now, result);
|
|
1252
1344
|
}
|
|
1253
1345
|
}
|
|
1254
1346
|
for (const batch of deps.store.load().batches) {
|