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

An agent run's log says how long the run took to its first push, when its pull opens #5751

Merged opened this

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

Merged

scout/forge:scout/3780-an-agent-run-s-log-says-how-long-the-run → main 1 commit fork

  • Merged. scout/3780-an-agent-run-s-log-says-how-long-the-run was merged into main as 6d56644.

Review

Sign in to review this change.
4 files changed +46 −4
src/apps/timing.ts +7 −3
@@ -38,10 +38,14 @@ export class Stopwatch {
3838 /**
3939 * `timing: <what> 0.4s after the deploy started — turn 0.0s · checkout
4040 * 0.3s · …`, every lap so far in the order it was taken. `since` names
41 * the start in the line's words; a run says "the run started".
41 * the start in the line's words; a run says "the run started". `running`
42 * names a step not yet over, shown with the time it has taken so far.
4243 */
43 line(what: string, since = "the deploy started"): string {
44 const steps = this.laps.map((l) => `${l.name} ${seconds(l.ms)}`).join(" · ");
44 line(what: string, since = "the deploy started", running?: string): string {
45 const laps = [...this.laps];
46 // The step still going, counted to now without taking its lap.
47 if (running !== undefined) laps.push({ name: running, ms: this.now() - this.last });
48 const steps = laps.map((l) => `${l.name} ${seconds(l.ms)}`).join(" · ");
4549 return `timing: ${what} ${seconds(this.elapsed())} after ${since}${steps === "" ? "" : ` — ${steps}`}`;
4650 }
4751 }
src/crew/early.ts +9
@@ -73,6 +73,8 @@ export interface EarlyInput {
7373 /** A pull already stands for this work: nothing to open. */
7474 continuing: boolean;
7575 intervalMs?: number;
76 /** Called once when this watcher opens the pull; a throw is logged, not raised. */
77 onOpened?: () => void;
7678 }
7779
7880 /**
@@ -135,6 +137,13 @@ export function watchForPull(input: EarlyInput): EarlyPull {
135137 ? `opened pull #${opened.value.number} at the first push, in progress`
136138 : `could not open the pull early: ${JSON.stringify(opened.error)}`,
137139 );
140 if (opened.ok && input.onOpened !== undefined) {
141 try {
142 input.onOpened();
143 } catch (err) {
144 input.write(`could not write the first-push timing: ${String(err)}`);
145 }
146 }
138147 };
139148 const every = input.intervalMs ?? WATCH_MS;
140149 let looks = 0;
src/crew/runner.ts +2
@@ -1342,6 +1342,8 @@ async function work(
13421342 write,
13431343 variantGroup: run.variantGroup,
13441344 continuing: pullStands,
1345 onOpened: () =>
1346 write(watch.line("first push", "the run started", "harness to first push")),
13451347 });
13461348 let result = await invoke(
13471349 prompt,
test/crew-run-timing.test.ts +28 −1
@@ -5,9 +5,10 @@
55 */
66
77 import { afterAll, beforeAll, describe, expect, test } from "bun:test";
8 import { mkdirSync, rmSync } from "node:fs";
8 import { mkdirSync, rmSync, writeFileSync } from "node:fs";
99 import { join } from "node:path";
1010 import { assignIssue, openIssue } from "../src/actions/issues.ts";
11 import { listPulls } from "../src/core/pulls.ts";
1112 import { getAgent } from "../src/core/agents.ts";
1213 import { workdirFor } from "../src/crew/runner.ts";
1314 import { readRunLog } from "../src/runs/log.ts";
@@ -61,6 +62,32 @@ describe("an agent run's timing line", () => {
6162 );
6263 }, 30_000);
6364
65 test("a pull opened at the first push gets its line before the end line", async () => {
66 rmSync(join(standIn.dataDir, "release"), { force: true });
67 // The stand-in lingers after its push; release it once the pull is open.
68 const releasing = (async () => {
69 const deadline = Date.now() + 30_000;
70 while (Date.now() < deadline) {
71 const open = listPulls(standIn.app.db, standIn.repo.id, {
72 state: "open",
73 limit: 50,
74 });
75 if (open.some((p) => p.title === "Sow the beans")) break;
76 await Bun.sleep(100);
77 }
78 writeFileSync(join(standIn.dataDir, "release"), "");
79 })();
80 const { run, lines, timing } = await runOn("Sow the beans", undefined, "LINGER-ME");
81 await releasing;
82 expect(run.state).toBe("ok");
83 expect(timing).toHaveLength(2);
84 expect(timing[0]).toMatch(
85 /^timing: first push \d+\.\ds after the run started — clone \d+\.\ds · brief \d+\.\ds · harness to first push \d+\.\ds$/,
86 );
87 expect(lines.indexOf(timing[0]!)).toBeLessThan(lines.indexOf(timing[1]!));
88 expect(lines.at(-1)).toBe(timing[1]!);
89 }, 60_000);
90
6491 test("a run whose clone cannot be prepared still ends with the line", async () => {
6592 const { run, lines, timing } = await runOn("Prune the roses", (number) => {
6693 const agent = getAgent(standIn.app.db, standIn.bot.id);

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs