@ai-dossier/sched 0.23.5 → 0.25.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 +103 -11
- package/dist/batch-dispatch.d.ts.map +1 -1
- package/dist/batch-dispatch.js +154 -9
- package/dist/batch-dispatch.js.map +1 -1
- package/dist/dispatch-health.d.ts +84 -0
- package/dist/dispatch-health.d.ts.map +1 -0
- package/dist/dispatch-health.js +127 -0
- package/dist/dispatch-health.js.map +1 -0
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +128 -56
- package/dist/engine.js.map +1 -1
- package/dist/scheduler.d.ts.map +1 -1
- package/dist/scheduler.js +11 -1
- package/dist/scheduler.js.map +1 -1
- package/dist/state.d.ts +4 -1
- package/dist/state.d.ts.map +1 -1
- package/dist/state.js +30 -1
- package/dist/state.js.map +1 -1
- package/dist/status.d.ts +9 -1
- package/dist/status.d.ts.map +1 -1
- package/dist/status.js +2 -0
- package/dist/status.js.map +1 -1
- package/dist/types.d.ts +47 -2
- 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/dist/engine.js
CHANGED
|
@@ -73,6 +73,7 @@ exports.runLoop = runLoop;
|
|
|
73
73
|
const core_1 = require("@ai-dossier/core");
|
|
74
74
|
const batch_dispatch_1 = require("./batch-dispatch");
|
|
75
75
|
const dispatch_1 = require("./dispatch");
|
|
76
|
+
const dispatch_health_1 = require("./dispatch-health");
|
|
76
77
|
const groundtruth_1 = require("./groundtruth");
|
|
77
78
|
const journal_1 = require("./journal");
|
|
78
79
|
const labels_1 = require("./labels");
|
|
@@ -725,7 +726,14 @@ function writeFence(ctx, unit, issue, slot, truth) {
|
|
|
725
726
|
* `unit-failed` line otherwise — and that indistinguishability is exactly
|
|
726
727
|
* what cost imboard-monorepo#3999 (docs/agent-traps.md).
|
|
727
728
|
*/
|
|
728
|
-
function failOrAdoptOpenPr(ctx, state, unit, slot, report,
|
|
729
|
+
function failOrAdoptOpenPr(ctx, state, unit, slot, report,
|
|
730
|
+
// #629: never `'dispatch-failure'` in practice — `enterRecovery` only
|
|
731
|
+
// reaches this function under `escalate: true` (its cap-check is itself
|
|
732
|
+
// gated on `escalate`), and a `dispatch-failure` cause always passes
|
|
733
|
+
// `escalate: false`. Kept as the two-member union `enterRecovery` uses
|
|
734
|
+
// before its escalate check, so a maintainer reading this signature does
|
|
735
|
+
// not read "a spend wall can terminally fail a unit" as a live path.
|
|
736
|
+
causeEvent, cause, evidence) {
|
|
729
737
|
let prCheck = 'skipped';
|
|
730
738
|
if (!report && causeEvent === 'verify-incomplete') {
|
|
731
739
|
if (slot.branch === null) {
|
|
@@ -776,7 +784,7 @@ function failOrAdoptOpenPr(ctx, state, unit, slot, report, causeEvent, cause, ev
|
|
|
776
784
|
// with `causeEvent === 'verify-incomplete'` thread it through
|
|
777
785
|
// `completeUnitOrRecover`: `reconcileRunning`'s dead-pid rail, from
|
|
778
786
|
// `recordDispatchRunLog`'s own read, and (since #620) `reconcileSlots`'
|
|
779
|
-
// `exited`/`verifying` rail, from `
|
|
787
|
+
// `exited`/`verifying` rail, from `readDispatchSignalsForSlot`. The two agree —
|
|
780
788
|
// the dispatch log is static once the agent has exited — so the tool name
|
|
781
789
|
// survives regardless of which tick reaches this decision. It stays
|
|
782
790
|
// OPTIONAL: a slice with no parseable `tool_use` yields null, hence the
|
|
@@ -798,7 +806,14 @@ function failOrAdoptOpenPr(ctx, state, unit, slot, report, causeEvent, cause, ev
|
|
|
798
806
|
* mechanical-starting ladder and fail MERGED-AWARE at the cap: the PR is
|
|
799
807
|
* already merged, so dependents stay released.
|
|
800
808
|
*/
|
|
801
|
-
function enterRecovery(ctx, state, unit, causeEvent, cause, truth, evidence = {}
|
|
809
|
+
function enterRecovery(ctx, state, unit, causeEvent, cause, truth, evidence = {},
|
|
810
|
+
// #629: `escalate: false` for a CONFIRMED provider API error — it must
|
|
811
|
+
// never consume the per-unit escalation ladder or the ESCALATION_CAP
|
|
812
|
+
// (a spend/rate wall is not the issue's fault), so the respawn keeps the
|
|
813
|
+
// slot's CURRENT tier and `recoveries` count unchanged. Every other caller
|
|
814
|
+
// keeps the default (`true`), unaffected.
|
|
815
|
+
options = {}) {
|
|
816
|
+
const escalate = options.escalate ?? true;
|
|
802
817
|
const issue = (0, journal_1.issueOfUnit)(unit);
|
|
803
818
|
if (issue === null)
|
|
804
819
|
return state;
|
|
@@ -809,21 +824,36 @@ function enterRecovery(ctx, state, unit, causeEvent, cause, truth, evidence = {}
|
|
|
809
824
|
return state;
|
|
810
825
|
killUnitAgent(ctx, state, unit);
|
|
811
826
|
// #524: only the STALL path kills a still-live, not-yet-recorded agent —
|
|
812
|
-
// `causeEvent === 'verify-incomplete'`
|
|
813
|
-
// AFTER the agent's exit was already detected and
|
|
814
|
-
// branch of `reconcileRunning`; recording again
|
|
815
|
-
// that same dispatch. #591: a stall kill still has
|
|
816
|
-
// reading for `last_tool` — a hung agent's last
|
|
817
|
-
// an operator what it hung in.
|
|
827
|
+
// `causeEvent === 'verify-incomplete'`/`'dispatch-failure'` arrive from
|
|
828
|
+
// `completeUnitOrRecover` AFTER the agent's exit was already detected and
|
|
829
|
+
// recorded by the dead-pid branch of `reconcileRunning`; recording again
|
|
830
|
+
// here would double-count that same dispatch. #591: a stall kill still has
|
|
831
|
+
// a fresh log slice worth reading for `last_tool` — a hung agent's last
|
|
832
|
+
// tool is exactly what tells an operator what it hung in.
|
|
818
833
|
if (causeEvent === 'stalled') {
|
|
819
|
-
const stallLastTool = recordDispatchRunLog(ctx, state, slot, unit);
|
|
834
|
+
const stallLastTool = recordDispatchRunLog(ctx, state, slot, unit).lastTool;
|
|
820
835
|
if (stallLastTool !== null)
|
|
821
836
|
evidence = { ...evidence, last_tool: stallLastTool };
|
|
822
837
|
}
|
|
823
838
|
const report = (0, state_1.isReportSlot)(slot);
|
|
824
|
-
|
|
825
|
-
if (
|
|
826
|
-
|
|
839
|
+
let resolvedTier;
|
|
840
|
+
if (escalate) {
|
|
841
|
+
const escalated = report ? (0, dispatch_1.reportTierFor)(slot.recoveries + 1) : (0, dispatch_1.escalateTier)(entry.tier);
|
|
842
|
+
if (slot.recoveries >= types_1.ESCALATION_CAP || escalated === null) {
|
|
843
|
+
// #629: unreachable with `causeEvent === 'dispatch-failure'` — that
|
|
844
|
+
// cause always passes `escalate: false` above, so this branch (and the
|
|
845
|
+
// narrower `causeEvent` type `failOrAdoptOpenPr` declares) is never
|
|
846
|
+
// actually asked to terminally fail a unit over a provider wall.
|
|
847
|
+
return failOrAdoptOpenPr(ctx, state, unit, slot, report, causeEvent, cause, evidence);
|
|
848
|
+
}
|
|
849
|
+
resolvedTier = escalated;
|
|
850
|
+
}
|
|
851
|
+
else {
|
|
852
|
+
// An unescalated redispatch keeps the CURRENT tier — `reportTierFor`
|
|
853
|
+
// evaluated at the unchanged `slot.recoveries` for a report slot (mirrors
|
|
854
|
+
// `recordDispatchRunLog`'s own tier expression), `entry.tier` verbatim
|
|
855
|
+
// otherwise — never `escalateTier`/`recoveries + 1`.
|
|
856
|
+
resolvedTier = report ? ((0, dispatch_1.reportTierFor)(slot.recoveries) ?? entry.tier) : entry.tier;
|
|
827
857
|
}
|
|
828
858
|
// Fence BEFORE the respawn (#504 AC1/AC4): `killUnitAgent` above only reaches a pid
|
|
829
859
|
// this process can see and signal, and #472 proved that is not the same as a dead
|
|
@@ -832,17 +862,17 @@ function enterRecovery(ctx, state, unit, causeEvent, cause, truth, evidence = {}
|
|
|
832
862
|
const fenced = writeFence(ctx, unit, issue, slot, truth);
|
|
833
863
|
let next = (0, state_1.transitionSlot)(state, slot.id, 'recovering', {
|
|
834
864
|
pid: null,
|
|
835
|
-
recoveries: slot.recoveries + 1,
|
|
865
|
+
recoveries: escalate ? slot.recoveries + 1 : slot.recoveries,
|
|
836
866
|
gen: fenced ?? slot.gen,
|
|
837
867
|
// Only a fence that actually landed starts the short takeover watch: an
|
|
838
868
|
// unfenced redispatch is already degraded, and cutting its allowance down
|
|
839
869
|
// would compound one failure with another.
|
|
840
870
|
fenced_at: fenced === null ? null : now.toISOString(),
|
|
841
871
|
}, now);
|
|
842
|
-
if (!report) {
|
|
872
|
+
if (!report && escalate) {
|
|
843
873
|
next = {
|
|
844
874
|
...next,
|
|
845
|
-
entries: next.entries.map((e) => e.issue === issue ? { ...e, tier:
|
|
875
|
+
entries: next.entries.map((e) => e.issue === issue ? { ...e, tier: resolvedTier, updated_at: now.toISOString() } : e),
|
|
846
876
|
};
|
|
847
877
|
}
|
|
848
878
|
journal(ctx, causeEvent, unit, {
|
|
@@ -851,10 +881,22 @@ function enterRecovery(ctx, state, unit, causeEvent, cause, truth, evidence = {}
|
|
|
851
881
|
...(slot.last_progress_at !== null ? { last_progress_at: slot.last_progress_at } : {}),
|
|
852
882
|
...evidence,
|
|
853
883
|
});
|
|
884
|
+
// #629: an UNESCALATED redispatch (a confirmed provider API error) has no
|
|
885
|
+
// natural bound — `recoveries`/`ESCALATION_CAP` never advance for it — so
|
|
886
|
+
// once the dispatch-health pause has fired, respawning it every tick would
|
|
887
|
+
// reproduce the exact incident this fix exists to stop, just on the
|
|
888
|
+
// per-issue rail instead of the batch one (`runBatchTick` gets the
|
|
889
|
+
// equivalent gate below). Hold the slot in `recovering`, unspawned, until
|
|
890
|
+
// `sched resume`: `reconcileRecovering` (below) is what retries it once the
|
|
891
|
+
// pause clears — no separate re-classification happens while parked, since
|
|
892
|
+
// this function is reached exactly once per dead dispatch.
|
|
893
|
+
if (!escalate && next.paused) {
|
|
894
|
+
return next;
|
|
895
|
+
}
|
|
854
896
|
journal(ctx, 'redispatched', unit, {
|
|
855
|
-
tier:
|
|
897
|
+
tier: resolvedTier,
|
|
856
898
|
slot: slot.id,
|
|
857
|
-
...(0, dispatch_1.journalCmdModelFields)((0, dispatch_1.resolveTierSpawn)(ctx.dispatch,
|
|
899
|
+
...(0, dispatch_1.journalCmdModelFields)((0, dispatch_1.resolveTierSpawn)(ctx.dispatch, resolvedTier, issue)),
|
|
858
900
|
});
|
|
859
901
|
ctx.result.redispatched.push(unit);
|
|
860
902
|
// Respawn immediately on the recovering rail — recovering → running. A
|
|
@@ -1125,22 +1167,24 @@ function journalStaleMilestoneIfIgnored(ctx, state, unit, slot, truth, verifiedC
|
|
|
1125
1167
|
});
|
|
1126
1168
|
return (0, state_1.patchSlot)(state, slot.id, { stale_milestone_ignored_for: slot.spawned_at }, now);
|
|
1127
1169
|
}
|
|
1170
|
+
const NO_DISPATCH_SIGNALS = { lastTool: null, apiError: null };
|
|
1128
1171
|
/**
|
|
1129
|
-
* Read THIS dispatch's last tool call
|
|
1130
|
-
* pure, side-effect-free parse, safe to
|
|
1131
|
-
*
|
|
1132
|
-
* `
|
|
1133
|
-
*
|
|
1134
|
-
*
|
|
1135
|
-
*
|
|
1136
|
-
*
|
|
1137
|
-
*
|
|
1138
|
-
* same answer every time.
|
|
1172
|
+
* Read THIS dispatch's last tool call and API-error classification (#591,
|
|
1173
|
+
* #620, #629) from its log slice — a pure, side-effect-free parse, safe to
|
|
1174
|
+
* call from any slot status and any number of times: unlike
|
|
1175
|
+
* `recordDispatchRunLog` below, it writes nothing to `runs.jsonl`, so it
|
|
1176
|
+
* carries no exactly-once constraint. Exists because a `verify-incomplete`/
|
|
1177
|
+
* `dispatch-failure` decision can land on a LATER tick than the one that
|
|
1178
|
+
* detected the dead pid (e.g. ground truth was unreachable in between) — by
|
|
1179
|
+
* then the slot has moved past `running` and `recordDispatchRunLog`'s guard
|
|
1180
|
+
* refuses to re-read, but the dispatch's log file is static once the agent
|
|
1181
|
+
* has exited, so re-parsing it here yields the same answer every time.
|
|
1139
1182
|
*/
|
|
1140
|
-
function
|
|
1183
|
+
function readDispatchSignalsForSlot(ctx, slot, unit) {
|
|
1141
1184
|
if (slot.spawned_at === null)
|
|
1142
|
-
return
|
|
1143
|
-
|
|
1185
|
+
return NO_DISPATCH_SIGNALS;
|
|
1186
|
+
const { content } = dispatchLogSlice(ctx, slot, unit);
|
|
1187
|
+
return { lastTool: (0, core_1.parseLastToolUse)(content), apiError: (0, core_1.parseDispatchApiError)(content) };
|
|
1144
1188
|
}
|
|
1145
1189
|
/**
|
|
1146
1190
|
* THIS dispatch's slice of the per-unit log: path, start offset, contents.
|
|
@@ -1187,7 +1231,7 @@ function recordDispatchRunLog(ctx, state, slot, unit) {
|
|
|
1187
1231
|
reason: slot.spawned_at === null ? 'never-spawned' : `already-recorded-${slot.status}`,
|
|
1188
1232
|
slot: slot.id,
|
|
1189
1233
|
});
|
|
1190
|
-
return
|
|
1234
|
+
return NO_DISPATCH_SIGNALS;
|
|
1191
1235
|
}
|
|
1192
1236
|
const issue = (0, journal_1.issueOfUnit)(unit);
|
|
1193
1237
|
const entry = issue === null ? undefined : (0, state_1.findEntry)(state, issue);
|
|
@@ -1199,7 +1243,7 @@ function recordDispatchRunLog(ctx, state, slot, unit) {
|
|
|
1199
1243
|
journal(ctx, 'run-log-skipped', unit, {
|
|
1200
1244
|
reason: issue === null ? 'not-an-issue-unit' : 'entry-gone',
|
|
1201
1245
|
});
|
|
1202
|
-
return
|
|
1246
|
+
return NO_DISPATCH_SIGNALS;
|
|
1203
1247
|
}
|
|
1204
1248
|
// Report slots ride the same tier the cycle escalation ladder set at
|
|
1205
1249
|
// dispatch time, EXCEPT `entry.tier` is deliberately not updated for a
|
|
@@ -1228,7 +1272,9 @@ function recordDispatchRunLog(ctx, state, slot, unit) {
|
|
|
1228
1272
|
(0, run_log_1.finalizeRunLogEntry)(runEntry, logContent, ctx.deps.homeDir, (event, extra) => journal(ctx, event, unit, extra), { log: logFile, offset });
|
|
1229
1273
|
// #591: the last tool this dispatch called, so an unverified exit attributes to a
|
|
1230
1274
|
// concrete cause (e.g. `Monitor`) without opening the transcript — see `enterRecovery`.
|
|
1231
|
-
|
|
1275
|
+
// #629: whether this SAME dispatch was a confirmed provider API error — see
|
|
1276
|
+
// `completeUnitOrRecover`.
|
|
1277
|
+
return { lastTool: (0, core_1.parseLastToolUse)(logContent), apiError: (0, core_1.parseDispatchApiError)(logContent) };
|
|
1232
1278
|
}
|
|
1233
1279
|
/** Reconcile one running slot against its polled ground truth. */
|
|
1234
1280
|
function reconcileRunning(ctx, state, slot, truth, unit) {
|
|
@@ -1237,9 +1283,9 @@ function reconcileRunning(ctx, state, slot, truth, unit) {
|
|
|
1237
1283
|
// is DETECTED, never trusted as completion (AC2/AC3).
|
|
1238
1284
|
if (slot.pid !== null && !ctx.deps.spawnDeps.isAlive(slot.pid, slot.pid_start ?? undefined)) {
|
|
1239
1285
|
journal(ctx, 'exit-detected', unit, { pid: slot.pid, slot: slot.id });
|
|
1240
|
-
const
|
|
1286
|
+
const signals = recordDispatchRunLog(ctx, state, slot, unit);
|
|
1241
1287
|
const exited = (0, state_1.transitionSlot)(state, slot.id, 'exited', {}, now);
|
|
1242
|
-
return completeUnitOrRecover(ctx, exited, unit, truth, 'verify-complete',
|
|
1288
|
+
return completeUnitOrRecover(ctx, exited, unit, truth, 'verify-complete', signals);
|
|
1243
1289
|
}
|
|
1244
1290
|
// The milestone poll FAILED (gh outage, missing binary) — unreachable is NOT
|
|
1245
1291
|
// known-absent (decision 2, option A): stall and advance decisions pause for
|
|
@@ -1365,11 +1411,13 @@ function recordDispatchOutcome(ctx, state, unit, slot, suspect) {
|
|
|
1365
1411
|
* event.
|
|
1366
1412
|
*/
|
|
1367
1413
|
function completeUnitOrRecover(ctx, state, unit, truth, via,
|
|
1368
|
-
// #591: the last tool
|
|
1369
|
-
//
|
|
1370
|
-
//
|
|
1371
|
-
//
|
|
1372
|
-
// `
|
|
1414
|
+
// #591/#629: the last tool called AND whether the exit was a confirmed
|
|
1415
|
+
// provider API error (from `recordDispatchRunLog`'s dead-pid reading),
|
|
1416
|
+
// threaded through to the unverified-exit decision below. The
|
|
1417
|
+
// `external-advance` caller passes none — it DOES record a run log
|
|
1418
|
+
// (`reconcileRunning`, #524), but ground truth already confirmed
|
|
1419
|
+
// completion, so this path never reaches that decision and neither signal
|
|
1420
|
+
// has anything to attribute.
|
|
1373
1421
|
//
|
|
1374
1422
|
// #620: a THUNK, not a value, for the deferred `exited`/`verifying` rail — that caller
|
|
1375
1423
|
// has no read of its own to hand over and would otherwise have to read the dispatch log
|
|
@@ -1378,7 +1426,7 @@ function completeUnitOrRecover(ctx, state, unit, truth, via,
|
|
|
1378
1426
|
// MAX_DISPATCH_LOG_BYTES (32 MiB) and its size is set by the spawned agent, so an
|
|
1379
1427
|
// eager read is up to 32 MiB per slot per tick for as long as an outage holds slots in
|
|
1380
1428
|
// `verifying`. Resolved once, only on the branch that consumes it.
|
|
1381
|
-
|
|
1429
|
+
dispatchSignals = NO_DISPATCH_SIGNALS) {
|
|
1382
1430
|
const now = ctx.deps.now();
|
|
1383
1431
|
let next = state;
|
|
1384
1432
|
let slot = slotOf(next, unit);
|
|
@@ -1403,12 +1451,12 @@ lastTool = null) {
|
|
|
1403
1451
|
// takes the unit (AC2's "never inferred from agent exit" cut both ways:
|
|
1404
1452
|
// the park IS the milestone, the merge is not).
|
|
1405
1453
|
if (entry !== undefined && entry.status === 'dispatched' && (0, groundtruth_1.isParkedMilestone)(truth.milestone)) {
|
|
1406
|
-
// A verified park is also proof dispatch is healthy (#505) — reset
|
|
1407
|
-
//
|
|
1408
|
-
// healthy park sandwiched between two unrelated units' suspect
|
|
1409
|
-
// would be invisible to the
|
|
1410
|
-
//
|
|
1411
|
-
const parked = recordDispatchOutcome(ctx, next, unit, slot, false);
|
|
1454
|
+
// A verified park is also proof dispatch is healthy (#505/#629) — reset
|
|
1455
|
+
// both streaks exactly like the sibling `completeUnit` branch below, or a
|
|
1456
|
+
// healthy park sandwiched between two unrelated units' suspect/api-error
|
|
1457
|
+
// exits would be invisible to the correlation and could still tip it
|
|
1458
|
+
// into a false-positive pause.
|
|
1459
|
+
const parked = (0, dispatch_health_1.resetDispatchApiErrorStreak)(recordDispatchOutcome(ctx, next, unit, slot, false));
|
|
1412
1460
|
const pr = (0, groundtruth_1.prOfMilestone)(truth.milestone); // non-null: isParkedMilestone guarantees it
|
|
1413
1461
|
if (pr === null) {
|
|
1414
1462
|
// Structurally unreachable, and deliberately not silent if it ever is:
|
|
@@ -1442,18 +1490,30 @@ lastTool = null) {
|
|
|
1442
1490
|
const verifiedComplete = (0, groundtruth_1.isVerifiedComplete)(truth.milestone, closedSignal, slot.spawned_at);
|
|
1443
1491
|
next = journalStaleMilestoneIfIgnored(ctx, next, unit, slot, truth, verifiedComplete, closedSignal);
|
|
1444
1492
|
if (verifiedComplete) {
|
|
1445
|
-
|
|
1493
|
+
const completed = (0, dispatch_health_1.resetDispatchApiErrorStreak)(recordDispatchOutcome(ctx, next, unit, slot, false));
|
|
1494
|
+
return completeUnit(ctx, completed, unit, via);
|
|
1495
|
+
}
|
|
1496
|
+
const resolved = typeof dispatchSignals === 'function' ? dispatchSignals() : dispatchSignals;
|
|
1497
|
+
// #629: a CONFIRMED provider API error is a dispatch failure, never an
|
|
1498
|
+
// unverified exit — it skips `recordDispatchOutcome`'s timing-based
|
|
1499
|
+
// suspect check entirely (the classification is deterministic, not a
|
|
1500
|
+
// heuristic) and never reaches `enterRecovery`'s escalation.
|
|
1501
|
+
if (resolved.apiError) {
|
|
1502
|
+
// `journalFailure: false` — `enterRecovery` journals its own `causeEvent`
|
|
1503
|
+
// (`'dispatch-failure'`) with this same evidence immediately below; without
|
|
1504
|
+
// the flag this rail double-journals every confirmed error (#629 review).
|
|
1505
|
+
const recorded = (0, dispatch_health_1.recordDispatchApiError)((event, u, extra) => journal(ctx, event, u, extra), next, unit, resolved.apiError, { journalFailure: false });
|
|
1506
|
+
return enterRecovery(ctx, recorded, unit, 'dispatch-failure', (0, dispatch_health_1.dispatchApiErrorDetail)(resolved.apiError), truth, (0, dispatch_health_1.dispatchApiErrorFields)(resolved.apiError), { escalate: false });
|
|
1446
1507
|
}
|
|
1447
1508
|
const suspect = msSinceLastProgress(slot, now) < types_1.SUSPECT_DISPATCH_WINDOW_MS;
|
|
1448
1509
|
next = recordDispatchOutcome(ctx, next, unit, slot, suspect);
|
|
1449
|
-
const resolvedLastTool = typeof lastTool === 'function' ? lastTool() : lastTool;
|
|
1450
1510
|
return enterRecovery(ctx, next, unit, 'verify-incomplete', 'unverified-exit', truth, {
|
|
1451
1511
|
observed: truth.milestone
|
|
1452
1512
|
? `milestone ${truth.milestone.phase}/${truth.milestone.status}; closed=${truth.closed}`
|
|
1453
1513
|
: `no milestone; closed=${truth.closed}`,
|
|
1454
1514
|
// #591: attributes the unverified exit to a concrete cause (e.g. `Monitor`)
|
|
1455
1515
|
// without opening the transcript. Omitted when the log yielded no tool_use.
|
|
1456
|
-
...(
|
|
1516
|
+
...(resolved.lastTool !== null ? { last_tool: resolved.lastTool } : {}),
|
|
1457
1517
|
});
|
|
1458
1518
|
}
|
|
1459
1519
|
/** Re-attach or spawn a slot left `assigned` by a crash between assign and spawn. */
|
|
@@ -1472,6 +1532,14 @@ function reconcileAssigned(ctx, state, slot, unit) {
|
|
|
1472
1532
|
}
|
|
1473
1533
|
/** Reconcile a `recovering` slot: respawn with the escalated tier. */
|
|
1474
1534
|
function reconcileRecovering(ctx, state, unit) {
|
|
1535
|
+
// #629: while paused, do not resume a `recovering` slot's respawn — this is
|
|
1536
|
+
// the crash-recovery rail (a sched restart caught a slot between
|
|
1537
|
+
// `enterRecovery`'s transition and its own `spawnUnit` call, OR `enterRecovery`
|
|
1538
|
+
// itself deliberately parked an unescalated redispatch here, see its own
|
|
1539
|
+
// pause check). Either way, `sched resume` is what lets the tick loop reach
|
|
1540
|
+
// this function again and actually respawn.
|
|
1541
|
+
if (state.paused)
|
|
1542
|
+
return state;
|
|
1475
1543
|
return spawnUnit(ctx, state, unit);
|
|
1476
1544
|
}
|
|
1477
1545
|
// --- Tick phases ---
|
|
@@ -1513,7 +1581,7 @@ function reconcileSlots(ctx, state, polled) {
|
|
|
1513
1581
|
// whatever `reconcileRunning` saw. Passed as a thunk so the read
|
|
1514
1582
|
// happens only on the tick that actually reaches the unverified-exit
|
|
1515
1583
|
// decision, not on every tick an outage holds the slot here.
|
|
1516
|
-
next = completeUnitOrRecover(ctx, next, unit, truth, 'verify-complete', () =>
|
|
1584
|
+
next = completeUnitOrRecover(ctx, next, unit, truth, 'verify-complete', () => readDispatchSignalsForSlot(ctx, slot, unit));
|
|
1517
1585
|
break;
|
|
1518
1586
|
case 'recovering':
|
|
1519
1587
|
next = reconcileRecovering(ctx, next, unit);
|
|
@@ -2040,10 +2108,14 @@ function sleep(ms, shouldStop) {
|
|
|
2040
2108
|
resolve();
|
|
2041
2109
|
}
|
|
2042
2110
|
}, Math.min(dispatch_1.STOP_POLL_MAX_MS, Math.max(dispatch_1.STOP_POLL_MIN_MS, Math.floor(ms / 10))));
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2111
|
+
// NEVER unref these handles (#679). They are what holds the event loop
|
|
2112
|
+
// open between ticks: spawned agents are detached and unref'd on purpose
|
|
2113
|
+
// (dispatch.ts), so if the sleep timers are unref'd too, nothing keeps
|
|
2114
|
+
// the process alive once a tick's async work settles — the loop drains
|
|
2115
|
+
// and `sched start` exits cleanly after its first tick, silently
|
|
2116
|
+
// behaving like `--once`. Ref'd timers cost nothing here: SIGINT still
|
|
2117
|
+
// stops the engine promptly, because the ref'd stop-check polls
|
|
2118
|
+
// shouldStop() and resolves this sleep early.
|
|
2047
2119
|
});
|
|
2048
2120
|
}
|
|
2049
2121
|
//# sourceMappingURL=engine.js.map
|