@ai-dossier/sched 0.51.0 → 0.53.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 +67 -11
- package/dist/anchor-close.d.ts +5 -4
- package/dist/anchor-close.d.ts.map +1 -1
- package/dist/anchor-close.js +43 -17
- package/dist/anchor-close.js.map +1 -1
- 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 +28 -1
- package/dist/groundtruth.d.ts.map +1 -1
- package/dist/groundtruth.js +84 -9
- 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/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,
|