Public: anyone can read this; the crew can change it.
Sentinel: The guard parked 33 runs in trevato/forge in a day #2198
Check · Review · Land · Live done — Landed and live in production.
mason/forge:mason/2197-sentinel-the-guard-parked-33-runs-in-tre → main 1 commit fork
2 files
changed +32
−2
src/crew/guard.ts +6 −2
| @@ -142,7 +142,11 @@ function capFrom(db: Database, repo: Repo, issue: Issue, now: Date): string { | ||
| 142 | 142 | * who may direct the crew last spoke on it. A run a restart or the |
| 143 | 143 | * provider cut short, a resumed run and a skipped one are not new starts; |
| 144 | 144 | * nor is a reviewer's run on a pull, which only answers a push the counted |
| 145 | * runs made and is bounded by the hand-over (`HANDOVER_ROUNDS`). | |
| 145 | * runs made and is bounded by the hand-over (`HANDOVER_ROUNDS`). Nor is a | |
| 146 | * run bringing a pull up to a main that moved, or answering its red check: | |
| 147 | * neither is the agent going round, and both already have their own bound | |
| 148 | * (`CHECK_FAILED_LIMIT` over `automaticRunsSince`), which parks or releases | |
| 149 | * the pull itself — counted here too, they parked pulls that were working. | |
| 146 | 150 | */ |
| 147 | 151 | export function capReached( |
| 148 | 152 | db: Database, |
| @@ -156,7 +160,7 @@ export function capReached( | ||
| 156 | 160 | `SELECT COUNT(*) AS n FROM run |
| 157 | 161 | WHERE issue_id = ?1 AND kind = 'agent' AND created_at > ?2 |
| 158 | 162 | AND attempt = 0 |
| 159 | AND reason != 'review' | |
| 163 | AND reason NOT IN ('review', 'conflict', 'check_failed') | |
| 160 | 164 | AND summary NOT LIKE 'interrupted: %' |
| 161 | 165 | AND summary NOT LIKE 'skipped: %'`, |
| 162 | 166 | ) |
test/crew-guard.test.ts +26
| @@ -342,6 +342,32 @@ describe("an issue cannot loop", () => { | ||
| 342 | 342 | ).toBe("queued"); |
| 343 | 343 | }); |
| 344 | 344 | |
| 345 | test("bringing a pull up to main and answering its red check are bounded on their own, not by the cap", () => { | |
| 346 | const issue = held("Quieter toasts"); | |
| 347 | for (let n = 0; n < ISSUE_RUN_CAP - 1; n++) { | |
| 348 | ran(issue, undefined, "ok", "feedback"); | |
| 349 | ran(issue, undefined, "ok", "conflict"); | |
| 350 | ran(issue, undefined, "ok", "check_failed"); | |
| 351 | } | |
| 352 | const before = guards("capped"); | |
| 353 | expect( | |
| 354 | queueAgentRun(app, fresh(), issue, bot, "assigned", alice.id)?.state, | |
| 355 | ).toBe("queued"); | |
| 356 | expect(guards("capped")).toBe(before); | |
| 357 | }); | |
| 358 | ||
| 359 | test("the agent's own starts still reach the cap", () => { | |
| 360 | const issue = held("Louder toasts"); | |
| 361 | ran(issue); | |
| 362 | for (let n = 1; n < ISSUE_RUN_CAP; n++) { | |
| 363 | ran(issue, undefined, "ok", "feedback"); | |
| 364 | ran(issue, undefined, "ok", "conflict"); | |
| 365 | } | |
| 366 | expect( | |
| 367 | queueAgentRun(app, fresh(), issue, bot, "assigned", alice.id), | |
| 368 | ).toBeUndefined(); | |
| 369 | }); | |
| 370 | ||
| 345 | 371 | test("the cap is a setting", () => { |
| 346 | 372 | const issue = held("Softer shadow"); |
| 347 | 373 | ran(issue); |
new above
offline