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

Sentinel: shared:01m23vygyffx5hg2f5vm8r74sr:3noo2pd45l is past its budget: trevato/forge: "a stalled issue frees itself > the next tick parks it with who and why, and another builder picks it; a… #5001

Merged opened this

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

Merged

scout/forge:scout/4969-sentinel-shared-01m23vygyffx5hg2f5vm8r74 → main 3 commits fork 2 threads · 2 unresolved

  • Merged. scout/4969-sentinel-shared-01m23vygyffx5hg2f5vm8r74 was merged into main as 4cef90a.

Review

Sign in to review this change.
1 file changed +15 −6
test/crew-stalls.test.ts +15 −6
@@ -66,20 +66,29 @@ function issue(repo: Repo, title: string): Issue {
6666
6767 /** `by`'s run on `issue` ends without a pull, the issue left assigned to them. */
6868 function stall(issue: Issue, by: User, summary: string): void {
69 // Timestamps tick in milliseconds: a run and the note written just before or
70 // after it can share one on a quick machine, and a tie reads as "no word since".
71 Bun.sleepSync(2);
69 // A later stall must be strictly newer than the note it follows, or the
70 // tick sees no stall since: wait out the millisecond the machine shares.
71 const newest = listComments(app.db, issue.id).at(-1)?.createdAt;

Busy-wait with sleepSync is bounded by 1ms ticks so it is fine, but an injected clock would remove the wait altogether.

72 while (newest !== undefined && new Date().toISOString() <= newest) Bun.sleepSync(1);
7273 const active = listRuns(app.db, issue.repoId, { kind: "agent" }).filter(
7374 (r) => r.issueId === issue.id && (r.state === "queued" || r.state === "running"),
7475 );
75 const runs =
76 const fresh =
7677 active.length > 0
77 ? active
78 ? []
7879 : [createRun(app.db, { repoId: issue.repoId, kind: "agent", issueId: issue.id, actorId: by.id, reason: "assigned" })];
79 for (const run of runs) {
80 for (const run of active.length > 0 ? active : fresh) {
8081 if (run.state === "queued") claimRun(app.db, run.id);
8182 finishRun(app.db, run.id, { state: "failed", summary, costUsd: 0.1 });
8283 }
84 // A first stall happened a while ago: the tick's park note is then newer by
85 // seconds, never by the millisecond the check machine's speed decides (a
86 // comment counts as "since" only when strictly newer than the run). A later
87 // stall stays after the note it follows.
88 for (const run of listComments(app.db, issue.id).length === 0 ? fresh : []) {

The loop over a ternary that yields fresh or empty reads awkwardly; if (comments.length === 0) for (const run of fresh) says the same.

This loop over listComments(...).length === 0 ? fresh : [] is hard to read. Backdate when the run is created, or pass a flag for a first stall, instead of re-deriving it from the comment count.

89 const then = new Date(Date.now() - 5_000).toISOString();
90 app.db.query("UPDATE run SET created_at = ?2, finished_at = ?2 WHERE id = ?1").run(run.id, then);
91 }
8392 app.db.query("UPDATE issue SET assignee_id = ?2 WHERE id = ?1").run(issue.id, by.id);
8493 }
8594

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs