Skip to content

Commit 674bb9a

Browse files
feat(autoresearch): Improve autoresearch display components (#3556)
Co-authored-by: posthog[bot] <206114724+posthog[bot]@users.noreply.github.com>
1 parent 17394fe commit 674bb9a

25 files changed

Lines changed: 1680 additions & 770 deletions

apps/code/snapshots.yml

Lines changed: 12 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -40,62 +40,18 @@ snapshots:
4040
hash: v1.k4693efd2.58144cdaf083f0ff98b136a5bae61686a2a856a0fce7835d5d11647e106b0b4f.-G4zNrpohMozh41d_NvfZ-pD1DUHkANR7XkHBaV6CCQ
4141
archive-archivedtasksview--with-missing-task--light:
4242
hash: v1.k4693efd2.2aad97fbf529d8e0c0896105051e140af8322d9c699653b0c60d6375dd2ea4c3.mc318EfElvU4a7rtpUNV9VJbFkYNpei4y4vhOcf3GRI
43-
autoresearch-configurable-full-dashboard--before-baseline--dark:
44-
hash: v1.k4693efd2.34b322085d4a0cc5723c4cc8b984fd81cf89809714f11de742605666e6979db2.D9JzgYGAuE95x0QkPVy4uBUYgUIkwrCCA08KMKXkKcg
45-
autoresearch-configurable-full-dashboard--before-baseline--light:
46-
hash: v1.k4693efd2.b4b99cafb1e4d7694726e86243910650f8ccb9156a8e81e5497f0705ab4ac383.H1b2sQFL1TV2nIDVf2RDkTSsUoA79-JM_FYlD-DTGzE
47-
autoresearch-configurable-full-dashboard--completed-run--dark:
48-
hash: v1.k4693efd2.2d10fc5b66537e110c096ca6349e1b46beffc2fcc8314e20a08a2c919c392ec7.xK39m0L7j5UNMcu1hrpcg7nPSPV0zwQLIq2UFbmoeaM
49-
autoresearch-configurable-full-dashboard--completed-run--light:
50-
hash: v1.k4693efd2.c03f43fe95a2dd45a722daacb531779cb6ebba5808b9790a4ccabbec8e16150f.95LMNvhl0FSreegkTDvktiIn_Pu47xydgq5KqtUOsCY
51-
autoresearch-configurable-full-dashboard--playground--dark:
52-
hash: v1.k4693efd2.67b1deb79c2c8a600bc77090e45a1586645df4af8a862ae90566583ba74e0c8c.0ae-YcXq7CDAUW5tZWa1nCJ2bK8XzG0U-f_0onDkF_Y
53-
autoresearch-configurable-full-dashboard--playground--light:
54-
hash: v1.k4693efd2.3944a480bec8fc56e71d16b8c44fd6dacb36736dad00c7116b24bb2f56bc3131.V-VD__LGWRlzMVNRv8QxglnuD1i_xHRBZg2fmwfXa70
55-
autoresearch-observability--active-experiment--dark:
56-
hash: v1.k4693efd2.7635193c423ee44a6f8ac40f1a2c885cec49e9131e187ff164b03b051ea71143.NSfHpkBCo_0DJaaVHP71WGkWASEwHc48GBXF4o4VUhc
57-
autoresearch-observability--active-experiment--light:
58-
hash: v1.k4693efd2.e5c9b0dfc9d814f5022401d9b6c8271165c28a28f74b4708e155c4800e7b690a.8TP9QEzM6yKB-_gt61fVEAsj6DRry5LFAUIsHVINo_8
59-
autoresearch-observability--waiting-for-plan--dark:
60-
hash: v1.k4693efd2.36e182d9438abfdbda7251f25255a1f6db00192111eaf6dde5c4adf74aa5ed85.jw3FQJBnplmsYY-xEJXq8MulEs2bReiud2xK4eYNVDI
61-
autoresearch-observability--waiting-for-plan--light:
62-
hash: v1.k4693efd2.261edaaca1e0d8b9932ca1af0d79e35365d6e53345252869b3ef79ebb0530d61.ejvug_EfWycvKL5XL93pJFLyxr22HfuK7lV6nqIRAco
63-
autoresearch-research-map--establishing-baseline--dark:
64-
hash: v1.k4693efd2.5162214d6100856e8a233f364cb0035845ae943dcac07afa0267ddc4d07bc53d.sDh8juzfN9gbXqX8suNdCWAEUqhuyRlUy4jEsh2ETlQ
65-
autoresearch-research-map--establishing-baseline--light:
66-
hash: v1.k4693efd2.21a9c5d13c2ca2592e5c73312ffcc39f16868bf38427ede85cba1f2b476d9de5.hJ9ITN88WX-wzhjiDY40OLDd_DBCV4hCq3FPSy-1e8w
67-
autoresearch-research-map--findings-by-code-area--dark:
68-
hash: v1.k4693efd2.24241937b04bc1845e1c0d5d6561f84f32d3512d32628fedb7a8be571d46c4e0._rYrAun17fUqJJEmIuYJQC6svmW2qQmJsSa2zMcpoao
69-
autoresearch-research-map--findings-by-code-area--light:
70-
hash: v1.k4693efd2.db68afd28cd0e204ab0ca02602b66adff99ec157918303a02a8f228d5fa23aeb.0iIfBWYqdNGwka2L3P-qS37Y_T0AWGHwgEDixWQL9X8
71-
autoresearch-results-and-summary--baseline-just-recorded--dark:
72-
hash: v1.k4693efd2.fd89d5a250d7c9ba75656208f5fa85a1aff2d27f5e7a62d4dbe19c374668b0ed.oM02rII7mDCEH5GFyZ0oJgzizclMQWMb1S7hn0F8vZA
73-
autoresearch-results-and-summary--baseline-just-recorded--light:
74-
hash: v1.k4693efd2.cdb1596e320e7afa01d150626530df417351bbc5562cac2be32d9df77e3ea738.YzCAe389zQW1meP8ie4HfA2qIj65qCz7FX0oWu0daPg
75-
autoresearch-results-and-summary--completed-run--dark:
76-
hash: v1.k4693efd2.9aed29fb95ba712a8a8bb04cbd4c4f704879525c91b57db620cf4a91238d7017.5CT3vEkBAVI1-Ekd8x2WTHoBodZcebTUAeXMa6hdtQE
77-
autoresearch-results-and-summary--completed-run--light:
78-
hash: v1.k4693efd2.c37fe3b4fa73de73dabc3dc8480292bde19d4d84f4820819ec48ad3be3c5d698.ElWCwCPyshrgyzvqTDxx7WuGvtGFzZmnpvusc0LBlAI
79-
autoresearch-rundashboard--maximize-completed--dark:
80-
hash: v1.k4693efd2.80aa97a95ac4830be972d1366b77567b85f935dd368175bcdabe40a76d6b353c.Ne1OrLmuP1SEO4m3fiAcmatww9ivj3JLrDBMr_H5O5Y
81-
autoresearch-rundashboard--maximize-completed--light:
82-
hash: v1.k4693efd2.618b533f0d5e83f66f157deb8d1c4d8175f130c1b21d65a09f0bc3dab1dd0400.qTssqMO2-FgGs4NBvZPVHh7s0qgzkc-YSLCyqSqm9iA
83-
autoresearch-rundashboard--minimize-in-progress--dark:
84-
hash: v1.k4693efd2.d13eebe8c14696a1cd48b6b29097e0d96841b36f2ba9fecc299615d01c256d7f.qjZSf4ehb8NGy72JTAb_PhsCL9EVosZ6UZb6HwQPCNU
85-
autoresearch-rundashboard--minimize-in-progress--light:
86-
hash: v1.k4693efd2.eadc597c9bc11eca735f15c903ae63f09a57e4f781374ca29ea2d3213243889f.ZAeEfQLOMhLC_hwkcoDYpfRVbVIdT4MTskEQscvtJQU
87-
autoresearch-rundashboard--no-iterations-yet--dark:
88-
hash: v1.k4693efd2.a6940dbc2b9658b27b378a9682aa0a620aa9257264dfbd4fcb50171b67ab3bbc.L5nlctLAoM5-lzijBFcR4dZPEcvRBbtE88esDyjxjXA
89-
autoresearch-rundashboard--no-iterations-yet--light:
90-
hash: v1.k4693efd2.1a3a3769b49045c8ff70c895c6a840c8652cd2a34dfe52ddac26a2e27d367400.Wv7VCEDJVF8GYlXQC1j10XznFekbNVR4lXPO8lmd9XA
91-
autoresearch-runtime-stats--before-usage-update--dark:
92-
hash: v1.k4693efd2.f31283c7e391d88a500507127c4bf327b6ba8e47be04ca975b140c07824cf032.Z07es_PMn-0tcNOj5f11fMTbzm_juPqb0PCXjEbrSjY
93-
autoresearch-runtime-stats--before-usage-update--light:
94-
hash: v1.k4693efd2.cdbd6673dad805fd4f5763e2d050c8bb6db7a5b84725dccd9f530d951db920cf.m8ngO8I6Jsb1OcWI_6zltKZdjsK97tF0XcjImqpJS8k
95-
autoresearch-runtime-stats--with-context-usage--dark:
96-
hash: v1.k4693efd2.b76d39ed8142d4449cb5befcde538bb11a6bffcbed66e81bdc3aa8e2746b7e06.GSIfBsgE-3RYuqlLUpluZ87C6-KjBUaTj2eu_a5elZ8
97-
autoresearch-runtime-stats--with-context-usage--light:
98-
hash: v1.k4693efd2.18889b5eda942cd19b0f7be00a87925d632529ca4111fac180d6339f7d34d1ca.X3s9IXrx2-TdQaJEjf1td3hwvNzeIp3Xq3uQh9Gwt5A
43+
autoresearch-dashboard--active-run--dark:
44+
hash: v1.k4693efd2.4b0e10f2aff5473d6dda9983e889748831b6dd930d345c074dde8cff7e95f979.QMSLjN0Ey631SdSe2D-SvT62ftICU65KnsiuZR_LQPo
45+
autoresearch-dashboard--active-run--light:
46+
hash: v1.k4693efd2.d190408b5d3c167d64da65b15581c903ade25b43a5de0254779f337647ec93f6.hAJZFZjDp0c1LwlXDyQ-mcLVwC1xwEUVVQth-NHlGAw
47+
autoresearch-dashboard--completed-run--dark:
48+
hash: v1.k4693efd2.74ee05d10c67c6878ee4c19059cc66d22af2c5d18ba27ef584c04f421ab80302.OD5XadbFRLiMCl2Scp7tIS3oew-jSh02LQmiqSdO0bM
49+
autoresearch-dashboard--completed-run--light:
50+
hash: v1.k4693efd2.910162d6bf5ad1588b5453e4f5464e2d3c12ee77c890e58a9bc60c2ae6d936d1.SzyIVMxc-AMRfA1X3_Vi59wmQ1xIljuwAw2Hn2jKXKc
51+
autoresearch-dashboard--researching-baseline--dark:
52+
hash: v1.k4693efd2.8936e61d920c886326a8a252f6eb937e66caa5ae43b76daf6f8ead0e9f9a05f3.5ZC4Fj11UEOvgDP7owvjlydg61nBBF2-cMxiltr5Gk0
53+
autoresearch-dashboard--researching-baseline--light:
54+
hash: v1.k4693efd2.35673a495a589ea3862abaf7ef46bdb4c3bb87c0bf79a563a8bf5782e7b6e74d.gW02sNxx7mUnKRvoexWGTRnjYgglyBrSLtz37Wic5TA
9955
billing-usagemeter--exceeded--dark:
10056
hash: v1.k4693efd2.dc026c5cace4e44c4cc713b95654e278d7bf0439a4862e9ceea39a033974815d.m5Be4dAOwwmt-UlEnhbhrKiym1rlGUiGZ4oyUonUoVg
10157
billing-usagemeter--exceeded--light:

packages/core/src/autoresearch/autoresearch.test.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,7 @@ function makeRun(
270270
researchFindings: [],
271271
iterations: [],
272272
startedAt: 1_000,
273+
pauseIntervals: [],
273274
endedAt: null,
274275
endReason: null,
275276
interruptedReason: null,
@@ -941,6 +942,46 @@ describe("AutoresearchService", () => {
941942
});
942943

943944
describe("pause and resume", () => {
945+
it("records and settles paused duration", () => {
946+
vi.setSystemTime(1_000);
947+
const run = service.startRun(baseConfig);
948+
949+
vi.setSystemTime(11_000);
950+
service.pauseRun(run.id);
951+
expect(activeRun().pausedAt).toBe(11_000);
952+
expect(activeRun().pausedDurationMs).toBe(0);
953+
954+
vi.setSystemTime(31_000);
955+
service.resumeRun(run.id);
956+
expect(activeRun().pausedAt).toBeNull();
957+
expect(activeRun().pausedDurationMs).toBe(20_000);
958+
expect(activeRun().pauseIntervals).toEqual([
959+
{ startedAt: 11_000, endedAt: 31_000 },
960+
]);
961+
});
962+
963+
it("excludes interruption downtime and records its interval", async () => {
964+
vi.setSystemTime(1_000);
965+
service.startRun(baseConfig);
966+
await flush();
967+
968+
vi.setSystemTime(11_000);
969+
sessionStoreSetters.updateSession(TASK_RUN_ID, { status: "error" });
970+
expect(activeRun().status).toBe("interrupted");
971+
expect(activeRun().pausedAt).toBe(11_000);
972+
973+
vi.setSystemTime(31_000);
974+
sessionStoreSetters.updateSession(TASK_RUN_ID, { status: "connected" });
975+
await flush();
976+
977+
expect(activeRun().status).toBe("running");
978+
expect(activeRun().pausedAt).toBeNull();
979+
expect(activeRun().pausedDurationMs).toBe(20_000);
980+
expect(activeRun().pauseIntervals).toEqual([
981+
{ startedAt: 11_000, endedAt: 31_000 },
982+
]);
983+
});
984+
944985
it("records iterations while paused but does not continue the loop", () => {
945986
const run = service.startRun(baseConfig);
946987
service.pauseRun(run.id);
@@ -989,13 +1030,22 @@ describe("AutoresearchService", () => {
9891030
});
9901031

9911032
it("resume waits for the agent when a turn is in flight", () => {
1033+
vi.setSystemTime(1_000);
9921034
const run = service.startRun(baseConfig);
1035+
1036+
vi.setSystemTime(11_000);
9931037
service.pauseRun(run.id);
9941038
beginTurn();
9951039
sentPrompts = [];
9961040

1041+
vi.setSystemTime(31_000);
9971042
service.resumeRun(run.id);
9981043
expect(sentPrompts).toHaveLength(0);
1044+
expect(activeRun().pausedAt).toBeNull();
1045+
expect(activeRun().pausedDurationMs).toBe(20_000);
1046+
expect(activeRun().pauseIntervals).toEqual([
1047+
{ startedAt: 11_000, endedAt: 31_000 },
1048+
]);
9991049

10001050
completeTurn(reportText(10));
10011051
expect(activeRun().iterations).toHaveLength(1);

packages/core/src/autoresearch/autoresearch.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,9 @@ export class AutoresearchService {
162162
researchFindings: [],
163163
iterations: [],
164164
startedAt: Date.now(),
165+
pausedAt: null,
166+
pausedDurationMs: 0,
167+
pauseIntervals: [],
165168
endedAt: null,
166169
endReason: null,
167170
interruptedReason: null,
@@ -213,6 +216,7 @@ export class AutoresearchService {
213216
this.clearPendingReaction(runId);
214217
this.clearRecoveryTimer(runId);
215218
this.remindersSent.delete(runId);
219+
this.pauseClock(runId);
216220
autoresearchStoreActions.setRunStatus(runId, "paused");
217221
this.persist(runId);
218222
this.restoreOriginalStage(run);
@@ -231,6 +235,7 @@ export class AutoresearchService {
231235
return;
232236
}
233237

238+
this.settlePausedClock(runId);
234239
this.clearRecoveryTimer(runId);
235240
const session = getSessionForTask(
236241
sessionStore.getState(),
@@ -257,6 +262,7 @@ export class AutoresearchService {
257262
autoresearchStoreActions.setRunStatus(runId, "interrupted", {
258263
interruptedReason: run.interruptedReason ?? "session-error",
259264
});
265+
this.pauseClock(runId);
260266
this.persist(runId);
261267
void this.attemptRecovery(runId);
262268
return;
@@ -765,6 +771,7 @@ export class AutoresearchService {
765771
if (run?.status === "running") {
766772
this.clearPendingReaction(runId);
767773
this.remindersSent.delete(runId);
774+
this.pauseClock(runId);
768775
autoresearchStoreActions.setRunStatus(runId, "paused");
769776
this.persist(runId);
770777
this.restoreOriginalStage(run);
@@ -806,6 +813,7 @@ export class AutoresearchService {
806813
this.clearPendingReaction(runId);
807814
// A fresh interruption gets its own reminder budget on resume.
808815
this.remindersSent.delete(runId);
816+
this.pauseClock(runId);
809817
autoresearchStoreActions.setRunStatus(runId, "interrupted", {
810818
interruptedReason: reason,
811819
lastError,
@@ -885,6 +893,7 @@ export class AutoresearchService {
885893
return;
886894
}
887895

896+
this.settlePausedClock(runId);
888897
this.clearRecoveryTimer(runId);
889898
const session = getSessionForTask(
890899
sessionStore.getState(),
@@ -915,6 +924,7 @@ export class AutoresearchService {
915924
status: "completed" | "stopped" | "failed",
916925
options: { endReason: AutoresearchEndReason; lastError?: string },
917926
): void {
927+
this.settlePausedClock(runId);
918928
const run = autoresearchStore.getState().runs[runId];
919929
autoresearchStoreActions.setRunStatus(runId, status, options);
920930
this.persist(runId);
@@ -928,6 +938,29 @@ export class AutoresearchService {
928938
if (run) this.restoreOriginalStage(run);
929939
}
930940

941+
private pauseClock(runId: string): void {
942+
const run = autoresearchStore.getState().runs[runId];
943+
if (!run || run.pausedAt != null) return;
944+
autoresearchStoreActions.setPauseTiming(
945+
runId,
946+
Date.now(),
947+
run.pausedDurationMs ?? 0,
948+
run.pauseIntervals ?? [],
949+
);
950+
}
951+
952+
private settlePausedClock(runId: string): void {
953+
const run = autoresearchStore.getState().runs[runId];
954+
if (!run || run.pausedAt == null) return;
955+
const endedAt = Date.now();
956+
autoresearchStoreActions.setPauseTiming(
957+
runId,
958+
null,
959+
(run.pausedDurationMs ?? 0) + Math.max(0, endedAt - run.pausedAt),
960+
[...(run.pauseIntervals ?? []), { startedAt: run.pausedAt, endedAt }],
961+
);
962+
}
963+
931964
private clearPendingReaction(runId: string): void {
932965
const timer = this.pendingReactions.get(runId);
933966
if (timer) clearTimeout(timer);

packages/core/src/autoresearch/autoresearchStore.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
type AutoresearchEndReason,
44
type AutoresearchInterruptionReason,
55
type AutoresearchIteration,
6+
type AutoresearchPauseInterval,
67
type AutoresearchPhase,
78
type AutoresearchResearchFinding,
89
type AutoresearchRun,
@@ -75,6 +76,20 @@ export const autoresearchStoreActions = {
7576
updateRun(runId, (run) => ({ ...run, phase }));
7677
},
7778

79+
setPauseTiming(
80+
runId: string,
81+
pausedAt: number | null,
82+
pausedDurationMs: number,
83+
pauseIntervals: AutoresearchPauseInterval[],
84+
): void {
85+
updateRun(runId, (run) => ({
86+
...run,
87+
pausedAt,
88+
pausedDurationMs,
89+
pauseIntervals,
90+
}));
91+
},
92+
7893
setRunStatus(
7994
runId: string,
8095
status: AutoresearchRunStatus,

packages/core/src/autoresearch/schemas.test.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { describe, expect, it } from "vitest";
1+
import { describe, expect, it, vi } from "vitest";
22
import {
33
AUTORESEARCH_MAX_ITERATIONS_LIMIT,
44
autoresearchConfigSchema,
@@ -86,10 +86,13 @@ describe("parseStoredAutoresearchRun", () => {
8686
expect(run?.interruptedReason).toBeNull();
8787
});
8888

89-
it("restores a running run as an app-restart interruption", () => {
89+
it("restores a running run as a paused app-restart interruption", () => {
90+
vi.setSystemTime(20_000);
9091
const run = parseStoredAutoresearchRun(storedRun("running"));
9192
expect(run?.status).toBe("interrupted");
9293
expect(run?.interruptedReason).toBe("app-restart");
94+
expect(run?.pausedAt).toBe(20_000);
95+
expect(run?.pausedDurationMs).toBe(0);
9396
});
9497

9598
it("defaults interruptedReason for blobs persisted before the field existed", () => {

packages/core/src/autoresearch/schemas.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,14 @@ export function isTerminalRunStatus(status: AutoresearchRunStatus): boolean {
124124
export const autoresearchPhaseSchema = z.enum(["implement", "measure"]);
125125
export type AutoresearchPhase = z.infer<typeof autoresearchPhaseSchema>;
126126

127+
export const autoresearchPauseIntervalSchema = z.object({
128+
startedAt: z.number(),
129+
endedAt: z.number(),
130+
});
131+
export type AutoresearchPauseInterval = z.infer<
132+
typeof autoresearchPauseIntervalSchema
133+
>;
134+
127135
export const autoresearchRunSchema = z.object({
128136
id: z.string().min(1),
129137
config: autoresearchConfigSchema,
@@ -150,6 +158,9 @@ export const autoresearchRunSchema = z.object({
150158
researchFindings: z.array(autoresearchResearchFindingSchema).default([]),
151159
iterations: z.array(autoresearchIterationSchema),
152160
startedAt: z.number(),
161+
pausedAt: z.number().nullable().optional(),
162+
pausedDurationMs: z.number().min(0).optional(),
163+
pauseIntervals: z.array(autoresearchPauseIntervalSchema).optional(),
153164
endedAt: z.number().nullable(),
154165
endReason: autoresearchEndReasonSchema.nullable(),
155166
interruptedReason: autoresearchInterruptionReasonSchema
@@ -176,12 +187,17 @@ export function parseStoredAutoresearchRun(
176187
}
177188
const parsed = autoresearchRunSchema.safeParse(raw);
178189
if (!parsed.success) return null;
179-
const run = parsed.data;
190+
let run = parsed.data;
191+
if (run.status === "paused" && run.pausedAt === undefined) {
192+
run = { ...run, pausedAt: Date.now(), pausedDurationMs: 0 };
193+
}
180194
if (run.status !== "running") return run;
181195
return {
182196
...run,
183197
status: "interrupted",
184198
interruptedReason: "app-restart",
199+
pausedAt: Date.now(),
200+
pausedDurationMs: run.pausedDurationMs ?? 0,
185201
};
186202
}
187203

0 commit comments

Comments
 (0)