Public: anyone can read this; the crew can change it.

Archive, part 2c: a run queued before Archive is cancelled with the archived line instead of starting #2488

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

scout/forge:scout/2478-archive-part-2c-a-run-queued-before-arch → main 1 commit fork 2 threads · 2 unresolved

  • Merged. scout/2478-archive-part-2c-a-run-queued-before-arch was merged into main as 69ee02e.

Review

Sign in to review this change.
2 files changed +48 −2
src/crew/dispatch.ts +14 −2
@@ -33,7 +33,7 @@ import { accountActive } from "../core/accounts.ts";
3333 import { runningLimitSentence, runningRuns } from "../settings/quotas.ts";
3434 import { appendRunLog } from "../runs/log.ts";
3535 import { decideAndRecord } from "./ledger.ts";
36 import { getRepoById, type Repo } from "../core/repos.ts";
36 import { ARCHIVED_WHY, getRepoById, type Repo } from "../core/repos.ts";
3737 import { listReviews } from "../core/reviews.ts";
3838 import {
3939 greenHead,
@@ -1139,10 +1139,22 @@ function pump(
11391139 const noted = notedBy.get(app) ?? new Set<string>();
11401140 notedBy.set(app, noted);
11411141 for (const queued of listQueued(app.db, "agent", 50)) {
1142 if (slots <= 0) break;
11431142 if (queued.issueId !== null && runningIssues.has(queued.issueId)) continue;

Move the archived check above this continue so a queued fresh run on an issue that is still running is cancelled on this tick, not later.

11441143 const fresh = getRun(app.db, queued.id);
11451144 if (!fresh || fresh.state !== "queued") continue;
1145 // A fresh run queued before Archive is cancelled, not started, and
1146 // stays cancelled through Wake. One carrying on after a pause or a
1147 // restart (attempt > 0) is already going, and finishes.
1148 const home = getRepoById(app.db, fresh.repoId);
1149 if (fresh.attempt === 0 && home !== undefined && home.archivedAt !== null) {
1150 const cancelled = cancelRun(app.db, fresh.id, ARCHIVED_WHY);
1151 if (cancelled) {
1152 appendRunLog(app.config.dataDir, fresh.id, ARCHIVED_WHY);
1153 announceRun(app, cancelled);
1154 }
1155 continue;
1156 }
1157 if (slots <= 0) break;
11461158 // An account at its concurrent-runs limit keeps this run queued, with
11471159 // the count in its log once, until one of its runs ends.
11481160 const owner = getRepoById(app.db, fresh.repoId)?.ownerId;
test/crew.test.ts +34
@@ -242,4 +242,38 @@ describe("the crew, with a stand-in Claude Code", () => {
242242 expect(run).toBeDefined();
243243 cancelRepoRun(app, alice, woken, run!.id);
244244 });
245
246 test("a run queued before Archive is cancelled with the archived line, never started; Wake does not revive it", async () => {
247 const issue = openIssue(app, alice, repo, { title: "Rake", body: "The leaves." });
248 expect(issue.ok).toBe(true);
249 if (!issue.ok) return;
250 const agent = getAgentByName(app.db, "bot")!;
251 const queued = queueAgentRun(app, repo, issue.value, agent, "assigned");
252 expect(queued).toBeDefined();
253 // A run carrying on after a restart, queued in the same breath on another issue.
254 const other = openIssue(app, alice, repo, { title: "Weed", body: "The beds." });
255 if (!other.ok) throw new Error("issue");
256 const again = { notBefore: new Date().toISOString(), attempt: 1, summary: "carries on" };
257 const carrying = queueAgentRun(app, repo, other.value, agent, "assigned", undefined, again);
258 expect(carrying).toBeDefined();
259 // Archived at once, before the crew's next pick.
260 const archived = updateRepoSettings(app.db, repo, { archived: true });
261 const done = await settled((r) => r.id === queued!.id, 60_000);
262 expect(done.state).toBe("cancelled");
263 expect(done.summary).toBe(ARCHIVED_WHY);
264 expect(readRunLog(dataDir, done.id).lines.join("\n")).toContain(ARCHIVED_WHY);
265 // The carry-on run is not cancelled for the archive.
266 const kept = await settled((r) => r.id === carrying!.id, 60_000);
267 expect(kept.summary).not.toBe(ARCHIVED_WHY);
268 expect(kept.state).not.toBe("cancelled");
269 // The fresh run spawned nothing: its whole log is the one archived line.
270 expect(readRunLog(dataDir, done.id).lines.filter((l) => l.trim() !== "").length).toBe(1);
271 // Wake leaves it cancelled; a new assignment starts normally.
272 const woken = updateRepoSettings(app.db, archived, { archived: false });
273 await Bun.sleep(6_000);
274 expect(listRuns(app.db, repo.id, { issueId: issue.value.id }).find((r) => r.id === queued!.id)!.state).toBe("cancelled");

Fixed 6s sleep makes the suite slower and timing-dependent; wait on a pump tick instead.

275 const next = queueAgentRun(app, woken, issue.value, agent, "assigned");
276 expect(next).toBeDefined();
277 await settled((r) => r.id === next!.id, 60_000);
278 }, 120_000);
245279 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs