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
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/4969-sentinel-shared-01m23vygyffx5hg2f5vm8r74 → main 3 commits fork 2 threads · 2 unresolved
1 file
changed +15
−6
test/crew-stalls.test.ts +15 −6
| @@ -66,20 +66,29 @@ function issue(repo: Repo, title: string): Issue { | ||
| 66 | 66 | |
| 67 | 67 | /** `by`'s run on `issue` ends without a pull, the issue left assigned to them. */ |
| 68 | 68 | 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; | |
| 72 | while (newest !== undefined && new Date().toISOString() <= newest) Bun.sleepSync(1); | |
| 72 | 73 | const active = listRuns(app.db, issue.repoId, { kind: "agent" }).filter( |
| 73 | 74 | (r) => r.issueId === issue.id && (r.state === "queued" || r.state === "running"), |
| 74 | 75 | ); |
| 75 | const runs = | |
| 76 | const fresh = | |
| 76 | 77 | active.length > 0 |
| 77 | ? active | |
| 78 | ? [] | |
| 78 | 79 | : [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) { | |
| 80 | 81 | if (run.state === "queued") claimRun(app.db, run.id); |
| 81 | 82 | finishRun(app.db, run.id, { state: "failed", summary, costUsd: 0.1 }); |
| 82 | 83 | } |
| 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; This loop over |
||
| 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 | } | |
| 83 | 92 | app.db.query("UPDATE issue SET assignee_id = ?2 WHERE id = ?1").run(issue.id, by.id); |
| 84 | 93 | } |
| 85 | 94 |
new above
offline
Busy-wait with sleepSync is bounded by 1ms ticks so it is fine, but an injected clock would remove the wait altogether.