Skip to content

Commit 74039cd

Browse files
committed
feat: improve autoresearch live observability
Generated-By: PostHog Code Task-Id: 0de359df-d297-4750-b6a2-1aa301a85429
1 parent 13f7ab6 commit 74039cd

17 files changed

Lines changed: 1482 additions & 141 deletions

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

Lines changed: 41 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);

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.ts

Lines changed: 15 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,7 +187,10 @@ 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,

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

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type { AutoresearchIteration, AutoresearchRun } from "./schemas";
33
import {
44
computeBest,
55
evaluateContinuation,
6+
getAutoresearchElapsedMs,
67
isImprovement,
78
meetsTarget,
89
summarizeRun,
@@ -32,6 +33,11 @@ function makeRun(overrides: {
3233
direction?: "maximize" | "minimize";
3334
targetValue?: number | null;
3435
maxIterations?: number;
36+
startedAt?: number;
37+
pausedAt?: number | null;
38+
pausedDurationMs?: number;
39+
pauseIntervals?: AutoresearchRun["pauseIntervals"];
40+
endedAt?: number | null;
3541
}): AutoresearchRun {
3642
return {
3743
id: "ar-1",
@@ -54,14 +60,43 @@ function makeRun(overrides: {
5460
originalEffort: null,
5561
researchFindings: [],
5662
iterations: overrides.iterations ?? [],
57-
startedAt: 0,
58-
endedAt: null,
63+
startedAt: overrides.startedAt ?? 0,
64+
pausedAt: overrides.pausedAt,
65+
pausedDurationMs: overrides.pausedDurationMs,
66+
pauseIntervals: overrides.pauseIntervals ?? [],
67+
endedAt: overrides.endedAt ?? null,
5968
endReason: null,
6069
interruptedReason: null,
6170
lastError: null,
6271
};
6372
}
6473

74+
describe("getAutoresearchElapsedMs", () => {
75+
it("freezes while paused and excludes completed pause intervals", () => {
76+
expect(
77+
getAutoresearchElapsedMs(
78+
makeRun({ startedAt: 1_000, pausedAt: 11_000 }),
79+
31_000,
80+
),
81+
).toBe(10_000);
82+
expect(
83+
getAutoresearchElapsedMs(
84+
makeRun({ startedAt: 1_000, pausedDurationMs: 20_000 }),
85+
36_000,
86+
),
87+
).toBe(15_000);
88+
expect(
89+
getAutoresearchElapsedMs(
90+
makeRun({
91+
startedAt: 1_000,
92+
pauseIntervals: [{ startedAt: 11_000, endedAt: 31_000 }],
93+
}),
94+
36_000,
95+
),
96+
).toBe(15_000);
97+
});
98+
});
99+
65100
describe("isImprovement", () => {
66101
it.each([
67102
["maximize", 5, 3, true],

packages/core/src/autoresearch/stats.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,26 @@ import type {
99
AutoresearchRun,
1010
} from "./schemas";
1111

12+
export function getAutoresearchElapsedMs(
13+
run: AutoresearchRun,
14+
now: number,
15+
): number {
16+
const effectiveEnd = run.endedAt ?? run.pausedAt ?? now;
17+
const trackedPausedDurationMs = (run.pauseIntervals ?? []).reduce(
18+
(total, interval) => {
19+
const overlapStart = Math.max(run.startedAt, interval.startedAt);
20+
const overlapEnd = Math.min(effectiveEnd, interval.endedAt);
21+
return total + Math.max(0, overlapEnd - overlapStart);
22+
},
23+
0,
24+
);
25+
const pausedDurationMs = Math.max(
26+
trackedPausedDurationMs,
27+
run.pausedDurationMs ?? 0,
28+
);
29+
return Math.max(0, effectiveEnd - run.startedAt - pausedDurationMs);
30+
}
31+
1232
export function isImprovement(
1333
candidate: number,
1434
reference: number | null,

0 commit comments

Comments
 (0)