@ai-dossier/sched 0.67.0 → 0.68.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 +52 -0
- package/dist/batch-dispatch.d.ts.map +1 -1
- package/dist/batch-dispatch.js +116 -2
- package/dist/batch-dispatch.js.map +1 -1
- package/dist/engine.d.ts +7 -0
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +128 -3
- package/dist/engine.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +19 -3
- package/dist/index.js.map +1 -1
- package/dist/preserve.d.ts +194 -0
- package/dist/preserve.d.ts.map +1 -0
- package/dist/preserve.js +675 -0
- package/dist/preserve.js.map +1 -0
- package/dist/project.d.ts +2 -0
- package/dist/project.d.ts.map +1 -1
- package/dist/project.js +1 -0
- package/dist/project.js.map +1 -1
- package/dist/scheduler.d.ts.map +1 -1
- package/dist/scheduler.js +3 -0
- package/dist/scheduler.js.map +1 -1
- package/dist/status.d.ts.map +1 -1
- package/dist/status.js +3 -1
- package/dist/status.js.map +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -464,6 +464,58 @@ Only SIGINT had a handler, so a SIGTERM/SIGHUP (another session's restart, a clo
|
|
|
464
464
|
terminal) killed the engine with no line and no lease release. That is now logged; if the
|
|
465
465
|
2026-09-29 cause was SIGKILL/OOM, the missing `engine-exit` plus the stale lease is now the
|
|
466
466
|
documented signature (`journalctl -k` for the OOM window).
|
|
467
|
+
### Restart never discards work (#945, #940)
|
|
468
|
+
|
|
469
|
+
Before the engine respawns an agent onto a worktree that already exists — a batch **tail**, a
|
|
470
|
+
batch **member** reusing its worktree, an issue **takeover** — it preserves whatever the dead
|
|
471
|
+
agent left there (`preserve.ts`):
|
|
472
|
+
|
|
473
|
+
- a WIP commit under the **non-branch** ref `refs/sched-rescue/<unit>-<timestamp>` (adds no branch,
|
|
474
|
+
triggers no CI), holding every tracked change (`git add -u`, deletions and staged renames
|
|
475
|
+
included), the unpushed commits, and those untracked / staged-new files that pass the **secret
|
|
476
|
+
and size filter**, journaled as `work-preserved` (`branch` = the ref). **Only tracked changes and
|
|
477
|
+
unpushed commits are ever pushed.** A rescue whose tree also holds untracked / staged-new files
|
|
478
|
+
stays **local** (the ref lives in the local repo; the worktree and its `.git` survive a respawn
|
|
479
|
+
anyway) and says `local only` in the journal and the respawn prompt. The final tree is filtered
|
|
480
|
+
once more: any path new relative to HEAD (a rename into a secret name, `git add -N`, staged-new)
|
|
481
|
+
matching `.env*`, `*.env.*`, `*.pem`/`*.key` (and `*.pem.*`/`*.key.*`), `id_rsa*`, `id_ed25519*`,
|
|
482
|
+
`*credentials*`, `*secret*`, `*token*`, `*.p12`, `*.pfx`, `*.tfstate*`, `*.tfvars`, `kubeconfig`,
|
|
483
|
+
`.kube/`, `.docker/config.json`, `.pypirc`, `.vault-token`, `.htpasswd`, `.s3cfg`, `.npmrc`,
|
|
484
|
+
`.netrc`, `.ssh/` … is dropped. Untracked files over 1 MiB, past 500 files / 16 MiB, tracked
|
|
485
|
+
changes over 50 MiB and submodule/nested-repo contents are not captured; they stay in the
|
|
486
|
+
worktree and the journal / prompt count what was skipped and why. This filter is
|
|
487
|
+
defence-in-depth: the rescue bypasses the pre-commit secret scan;
|
|
488
|
+
- **TTL is housekeeping, not privacy.** A pushed rescue ref is **world-readable on a public
|
|
489
|
+
repository, and deleting a ref does NOT unpublish it** (GitHub keeps objects reachable by SHA).
|
|
490
|
+
Rescue refs older than 14 days (by the timestamp in the ref name) are deleted locally and on
|
|
491
|
+
origin (listed with `git ls-remote`, so refs pushed from other clones are covered) together with
|
|
492
|
+
their `sched-rescue-pushed-<sha>` markers: at `sched start` and then daily outside the state
|
|
493
|
+
lock. A ref whose remote deletion fails for any reason but "already gone" is kept and retried.
|
|
494
|
+
Find live ones with `git for-each-ref refs/sched-rescue`;
|
|
495
|
+
- built with a throwaway index, so the worktree, its index and its HEAD are untouched — the
|
|
496
|
+
respawned agent still finds the files in place, and preservation cannot itself lose work;
|
|
497
|
+
- idempotent: an unchanged tree reuses its existing rescue ref instead of minting one per tick;
|
|
498
|
+
- the respawned agent's prompt gets a `PRESERVED WORK` instruction naming the ref and forbidding a
|
|
499
|
+
reset to the last pushed head. A failed push is journaled (`pushed: false` in the detail) and is
|
|
500
|
+
not fatal; an unpreservable worktree journals `work-preserve-failed` and is left as it is.
|
|
501
|
+
|
|
502
|
+
A takeover's worktree path comes from a setup milestone (an issue comment), so it is acted on only
|
|
503
|
+
when `git worktree list` registers it in this repository, it is under the sanctioned `worktrees/`
|
|
504
|
+
roots, it is not the main checkout, and no batch holds it (`takeoverWorktreeRefusal`; a refusal is
|
|
505
|
+
journaled `work-preserve-failed` and never blocks the respawn). **The takeover rescue runs outside
|
|
506
|
+
the state lock** (it shells out to `gh`, `git` and `git push`): a stalled unit enters recovery on
|
|
507
|
+
one tick and its takeover is spawned on the next, after the rescue. Takeover preservation is on
|
|
508
|
+
only when the engine is given a `rescueExec` (the CLI wires one, 120 s per call).
|
|
509
|
+
|
|
510
|
+
**Batch tail, respawn only:** when the previous tail exited unverified and its worktree holds
|
|
511
|
+
uncommitted files, or the NEWEST `gate.batch` row in `caps.jsonl` for that worktree (matched by realpath of `cwd`) is a
|
|
512
|
+
passing run on a **dirty** tree (`dirty: true` or `git_probe` set — #941's fields) newer than the
|
|
513
|
+
last pushed head while local work exists, the engine does **not** respawn. Rows without #941's
|
|
514
|
+
fields (the engine's own post-landing gate rows) and clean rows are never evidence; an older dirty
|
|
515
|
+
row is superseded by a newer clean or failed one. It blocks the batch `tail-dirty-worktree`, with the evidence and the
|
|
516
|
+
rescue ref in the journal and `sched status`. Commit or discard the work in the worktree, then
|
|
517
|
+
`sched resume --batch <id>`. A tail that left only unpushed commits is respawned, told to resume
|
|
518
|
+
from the rescue ref. `sched resume --batch` deliberately does NOT re-run this guard: by resuming, the operator has looked at the worktree.
|
|
467
519
|
|
|
468
520
|
### Zombie-run fencing (#504)
|
|
469
521
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"batch-dispatch.d.ts","sourceRoot":"","sources":["../src/batch-dispatch.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8DG;
|
|
1
|
+
{"version":3,"file":"batch-dispatch.d.ts","sourceRoot":"","sources":["../src/batch-dispatch.ts"],"names":[],"mappings":"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"}
|
package/dist/batch-dispatch.js
CHANGED
|
@@ -106,6 +106,7 @@ exports.reconcileKeptWorktrees = reconcileKeptWorktrees;
|
|
|
106
106
|
exports.memberDispatchModeFor = memberDispatchModeFor;
|
|
107
107
|
exports.runBatchTick = runBatchTick;
|
|
108
108
|
const fs = __importStar(require("node:fs"));
|
|
109
|
+
const os = __importStar(require("node:os"));
|
|
109
110
|
const path = __importStar(require("node:path"));
|
|
110
111
|
const core_1 = require("@ai-dossier/core");
|
|
111
112
|
const worktree_pool_1 = require("@ai-dossier/worktree-pool");
|
|
@@ -117,6 +118,7 @@ const dossier_root_1 = require("./dossier-root");
|
|
|
117
118
|
const groundtruth_1 = require("./groundtruth");
|
|
118
119
|
const journal_1 = require("./journal");
|
|
119
120
|
const merge_mechanism_1 = require("./merge-mechanism");
|
|
121
|
+
const preserve_1 = require("./preserve");
|
|
120
122
|
const readiness_1 = require("./readiness");
|
|
121
123
|
const recovery_1 = require("./recovery");
|
|
122
124
|
const run_log_1 = require("./run-log");
|
|
@@ -166,6 +168,98 @@ function sanitizeUntrustedText(value) {
|
|
|
166
168
|
function journalEvent(deps, event, unitId, extra = {}) {
|
|
167
169
|
deps.journal.append((0, journal_1.unitEvent)(event, unitId, extra), deps.now());
|
|
168
170
|
}
|
|
171
|
+
/**
|
|
172
|
+
* #945: preserve whatever a dead agent left in `worktree` before a respawn
|
|
173
|
+
* reuses it (see `preserve.ts`). Journals the outcome; returns the preserved
|
|
174
|
+
* work for the respawned agent's prompt, or undefined when there was nothing to
|
|
175
|
+
* preserve (clean) or preservation was not possible (failure is journaled, the
|
|
176
|
+
* worktree is untouched either way — the respawn is not made worse by it).
|
|
177
|
+
*/
|
|
178
|
+
function preserveForRespawn(deps, unitId, worktree, now) {
|
|
179
|
+
const outcome = (0, preserve_1.preserveWork)(deps.exec, { worktree, unit: unitId, now });
|
|
180
|
+
return journalPreserveOutcome(deps, unitId, worktree, outcome);
|
|
181
|
+
}
|
|
182
|
+
function journalPreserveOutcome(deps, unitId, worktree, outcome) {
|
|
183
|
+
if (outcome.kind === 'preserved') {
|
|
184
|
+
const w = outcome.work;
|
|
185
|
+
// A REUSED rescue was journaled when it was minted; a respawn loop must not
|
|
186
|
+
// add a line per tick.
|
|
187
|
+
if (!w.reused) {
|
|
188
|
+
const skipped = (0, preserve_1.skippedSummary)(w.skipped);
|
|
189
|
+
journalEvent(deps, 'work-preserved', unitId, {
|
|
190
|
+
branch: w.ref,
|
|
191
|
+
worktree,
|
|
192
|
+
detail: `${w.dirty_files} uncommitted file(s) + ${w.unpushed_commits} unpushed commit(s) on head ${w.head} preserved as ${w.sha} at ${w.ref} (${w.pushed ? 'pushed' : w.local_only ? 'local only — includes untracked files, never pushed' : 'NOT pushed — local ref only'})${skipped ? `; ${skipped}` : ''}; the respawned agent is told to resume from it`,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
return w;
|
|
196
|
+
}
|
|
197
|
+
if (outcome.kind === 'failed' && (0, preserve_1.firstOccurrence)(deps.journal, `${unitId}|${outcome.reason}`)) {
|
|
198
|
+
journalEvent(deps, 'work-preserve-failed', unitId, {
|
|
199
|
+
worktree,
|
|
200
|
+
detail: `${outcome.reason} — ${outcome.probe.dirty_files} uncommitted file(s), ${outcome.probe.unpushed_commits} unpushed commit(s) left in place in the worktree`,
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
return undefined;
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* #940 ask 1 + #945: before RESPAWNING a batch tail onto its batch worktree,
|
|
207
|
+
* preserve what the dead tail left there, and refuse the respawn when that
|
|
208
|
+
* includes uncommitted files or a passing DIRTY `gate.batch` run past the last
|
|
209
|
+
* pushed head (`findGatedWorkEvidence`) — the operator decides commit vs
|
|
210
|
+
* discard (`tail-dirty-worktree`, resumable via `sched resume --batch`, which
|
|
211
|
+
* deliberately does not re-run this guard: the operator has looked). Clean or
|
|
212
|
+
* only-unpushed: respawn, told to resume from the rescue ref.
|
|
213
|
+
*/
|
|
214
|
+
function guardTailRespawn(deps, batch, now) {
|
|
215
|
+
const worktree = batch.worktree;
|
|
216
|
+
if (worktree === null)
|
|
217
|
+
return { block: null, instruction: null };
|
|
218
|
+
const unitId = unit(batch.id);
|
|
219
|
+
const outcome = (0, preserve_1.preserveWork)(deps.exec, { worktree, unit: unitId, now });
|
|
220
|
+
const work = journalPreserveOutcome(deps, unitId, worktree, outcome);
|
|
221
|
+
if (outcome.kind === 'skipped')
|
|
222
|
+
return { block: null, instruction: null };
|
|
223
|
+
const probe = outcome.kind === 'preserved' || outcome.kind === 'failed' || outcome.kind === 'clean'
|
|
224
|
+
? outcome.probe
|
|
225
|
+
: null;
|
|
226
|
+
if (probe === null)
|
|
227
|
+
return { block: null, instruction: null };
|
|
228
|
+
const since = (0, preserve_1.pushedHeadDate)(deps.exec, worktree);
|
|
229
|
+
const gate = since === null
|
|
230
|
+
? null
|
|
231
|
+
: (0, preserve_1.findGatedWorkEvidence)({
|
|
232
|
+
capsFile: path.join(deps.homeDir ?? os.homedir(), '.dossier', 'caps.jsonl'),
|
|
233
|
+
worktree,
|
|
234
|
+
sinceIso: since,
|
|
235
|
+
headTree: headTreeOf(deps, worktree),
|
|
236
|
+
hasLocalWork: probe.dirty_files > 0 || probe.unpushed_commits > 0,
|
|
237
|
+
now,
|
|
238
|
+
});
|
|
239
|
+
// Stray untracked files (logs, scratch) that the rescue deliberately left out must not wedge a
|
|
240
|
+
// respawn: block on tracked changes, on untracked work the rescue captured, or when the
|
|
241
|
+
// state is unknowable (a failed rescue counts everything as dirty).
|
|
242
|
+
const rescuedUntracked = work?.untracked_included ?? 0;
|
|
243
|
+
const dirty = outcome.kind === 'failed'
|
|
244
|
+
? probe.dirty_files > 0
|
|
245
|
+
: probe.tracked_dirty_files > 0 || rescuedUntracked > 0;
|
|
246
|
+
if (dirty || gate !== null) {
|
|
247
|
+
// Its own event: `work-preserved` above already said (once) what was saved, or
|
|
248
|
+
// `work-preserve-failed` said it was not — this line must not claim otherwise.
|
|
249
|
+
journalEvent(deps, 'tail-respawn-refused', unitId, {
|
|
250
|
+
worktree,
|
|
251
|
+
reason: 'tail-dirty-worktree',
|
|
252
|
+
detail: `tail respawn refused: ${probe.dirty_files} uncommitted file(s) at head ${probe.head}${gate ? `; a gate.batch ok row at ${gate.timestamp} ran on a dirty tree newer than the last pushed head` : ''}${work ? `; a rescue exists at ${work.ref} (${work.sha})` : outcome.kind === 'failed' ? '; NO rescue could be made' : ''}. Commit or discard in ${worktree}, then \`sched resume --batch ${batch.id}\``,
|
|
253
|
+
});
|
|
254
|
+
return { block: 'tail-dirty-worktree', instruction: null };
|
|
255
|
+
}
|
|
256
|
+
return { block: null, instruction: work ? (0, preserve_1.preservedWorkInstruction)(work) : null };
|
|
257
|
+
}
|
|
258
|
+
/** HEAD's tree id (for gate-evidence comparison), null when unreadable. */
|
|
259
|
+
function headTreeOf(deps, worktree) {
|
|
260
|
+
const tree = deps.exec('git', ['rev-parse', 'HEAD^{tree}'], worktree);
|
|
261
|
+
return tree === null || tree.trim() === '' ? null : tree.trim();
|
|
262
|
+
}
|
|
169
263
|
/** Whether the slot's agent is still alive (pid-start-safe: a reused pid reads dead). */
|
|
170
264
|
function agentAlive(deps, slot) {
|
|
171
265
|
return slot.pid !== null && deps.spawnDeps.isAlive(slot.pid, slot.pid_start ?? undefined);
|
|
@@ -1160,7 +1254,9 @@ function spawnMemberAgent(deps, dispatch, state, slot, batchId, now, result, tar
|
|
|
1160
1254
|
// #822: a member respawned after running the wrong procedure.
|
|
1161
1255
|
(batch.reprompted_members.some((r) => r.issue === memberIssue)
|
|
1162
1256
|
? (0, dispatch_1.wrongProcedureDirective)(memberIssue, batchId)
|
|
1163
|
-
: '')
|
|
1257
|
+
: '') +
|
|
1258
|
+
// #945: a respawn onto a reused worktree resumes from what was preserved there.
|
|
1259
|
+
(target.member.preserved ? `\n\n${(0, preserve_1.preservedWorkInstruction)(target.member.preserved)}` : '');
|
|
1164
1260
|
const logFile = (0, dispatch_1.batchMemberLogPath)(deps.store.runsDir, batchId, target.index, memberIssue);
|
|
1165
1261
|
// #629: captured BEFORE spawning, mirroring `engine.ts`'s own
|
|
1166
1262
|
// `spawnAndRecord` — the log is per-role and append-mode, so the size at
|
|
@@ -1451,6 +1547,10 @@ function spawnMemberContinuation(deps, config, dispatch, batchId, now, result) {
|
|
|
1451
1547
|
worktree: batch.member_worktree,
|
|
1452
1548
|
poolClaimed: batch.member_pool_claimed,
|
|
1453
1549
|
};
|
|
1550
|
+
// #945: preserve whatever the previous agent left BEFORE the respawn, outside the lock.
|
|
1551
|
+
const preserved = preserveForRespawn(deps, `batch:${batchId}#${memberIssue}`, member.worktree, now);
|
|
1552
|
+
if (preserved)
|
|
1553
|
+
member.preserved = preserved;
|
|
1454
1554
|
}
|
|
1455
1555
|
else {
|
|
1456
1556
|
if (batch.member_worktree !== null) {
|
|
@@ -1514,12 +1614,23 @@ function spawnTailAgent(deps, config, dispatch, batchId, now, result) {
|
|
|
1514
1614
|
// #887: read outside the store lock (a cache miss runs `gh api`).
|
|
1515
1615
|
const mechanism = deps.groundTruth.mergeMechanism?.();
|
|
1516
1616
|
const pending = (0, state_1.findBatch)(before, batchId);
|
|
1617
|
+
// #945/#940: a RESPAWN (the previous tail exited unverified) never lands on
|
|
1618
|
+
// work it cannot see — preserve first, outside the store lock.
|
|
1619
|
+
let resumeNote = null;
|
|
1517
1620
|
if (pending) {
|
|
1518
1621
|
const refusal = tailDispatchRefusal(before, pending);
|
|
1519
1622
|
if (refusal !== null) {
|
|
1520
1623
|
blockBatchForOperator(deps, config, batchId, { reason: refusal, milestonePhase: 'batch-review' }, now, result);
|
|
1521
1624
|
return;
|
|
1522
1625
|
}
|
|
1626
|
+
if (pending.agent_exits?.phase === 'tail' && pending.agent_exits.count >= 1) {
|
|
1627
|
+
const guard = guardTailRespawn(deps, pending, now);
|
|
1628
|
+
if (guard.block !== null) {
|
|
1629
|
+
blockBatchForOperator(deps, config, batchId, { reason: guard.block, milestonePhase: 'batch-review' }, now, result);
|
|
1630
|
+
return;
|
|
1631
|
+
}
|
|
1632
|
+
resumeNote = guard.instruction;
|
|
1633
|
+
}
|
|
1523
1634
|
}
|
|
1524
1635
|
claimAndSpawn(deps, config, batchId, 'reviewing', now, (state, slot) => {
|
|
1525
1636
|
const batch = (0, state_1.findBatch)(state, batchId);
|
|
@@ -1545,7 +1656,7 @@ function spawnTailAgent(deps, config, dispatch, batchId, now, result) {
|
|
|
1545
1656
|
const cmd = spawnSpec.cmd;
|
|
1546
1657
|
const prompt = (0, dispatch_1.buildBatchTailPrompt)(dispatch.batchTailPrompt, batchId, batch.anchor, landed, batch.worktree,
|
|
1547
1658
|
// #887: the repo's detected merge mechanism decides detached vs attached ship.
|
|
1548
|
-
mechanism);
|
|
1659
|
+
mechanism) + (resumeNote === null ? '' : `\n\n${resumeNote}`);
|
|
1549
1660
|
const logFile = (0, dispatch_1.batchTailLogPath)(deps.store.runsDir, batchId);
|
|
1550
1661
|
// #629: fences `handleDeadDispatchApiError`'s classification to this
|
|
1551
1662
|
// dispatch's slice — see `spawnMember`'s identical comment.
|
|
@@ -4480,6 +4591,9 @@ function spawnParallelMembers(deps, config, dispatch, batchId, now, result) {
|
|
|
4480
4591
|
worktree: next.run.worktree,
|
|
4481
4592
|
poolClaimed: next.run.pool_claimed,
|
|
4482
4593
|
};
|
|
4594
|
+
const preserved = preserveForRespawn(deps, (0, state_1.batchMemberUnit)(batchId, next.issue), member.worktree, now);
|
|
4595
|
+
if (preserved)
|
|
4596
|
+
member.preserved = preserved;
|
|
4483
4597
|
}
|
|
4484
4598
|
else {
|
|
4485
4599
|
// The serial-member fields describe no member of a parallel batch —
|