Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions packages/core/src/autoresearch/autoresearch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,7 @@ function makeRun(
researchFindings: [],
iterations: [],
startedAt: 1_000,
pauseIntervals: [],
endedAt: null,
endReason: null,
interruptedReason: null,
Expand Down Expand Up @@ -941,6 +942,46 @@ describe("AutoresearchService", () => {
});

describe("pause and resume", () => {
it("records and settles paused duration", () => {
vi.setSystemTime(1_000);
const run = service.startRun(baseConfig);

vi.setSystemTime(11_000);
service.pauseRun(run.id);
expect(activeRun().pausedAt).toBe(11_000);
expect(activeRun().pausedDurationMs).toBe(0);

vi.setSystemTime(31_000);
service.resumeRun(run.id);
expect(activeRun().pausedAt).toBeNull();
expect(activeRun().pausedDurationMs).toBe(20_000);
expect(activeRun().pauseIntervals).toEqual([
{ startedAt: 11_000, endedAt: 31_000 },
]);
});

it("excludes interruption downtime and records its interval", async () => {
vi.setSystemTime(1_000);
service.startRun(baseConfig);
await flush();

vi.setSystemTime(11_000);
sessionStoreSetters.updateSession(TASK_RUN_ID, { status: "error" });
expect(activeRun().status).toBe("interrupted");
expect(activeRun().pausedAt).toBe(11_000);

vi.setSystemTime(31_000);
sessionStoreSetters.updateSession(TASK_RUN_ID, { status: "connected" });
await flush();

expect(activeRun().status).toBe("running");
expect(activeRun().pausedAt).toBeNull();
expect(activeRun().pausedDurationMs).toBe(20_000);
expect(activeRun().pauseIntervals).toEqual([
{ startedAt: 11_000, endedAt: 31_000 },
]);
});

it("records iterations while paused but does not continue the loop", () => {
const run = service.startRun(baseConfig);
service.pauseRun(run.id);
Expand Down
33 changes: 33 additions & 0 deletions packages/core/src/autoresearch/autoresearch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,9 @@ export class AutoresearchService {
researchFindings: [],
iterations: [],
startedAt: Date.now(),
pausedAt: null,
pausedDurationMs: 0,
pauseIntervals: [],
endedAt: null,
endReason: null,
interruptedReason: null,
Expand Down Expand Up @@ -213,6 +216,7 @@ export class AutoresearchService {
this.clearPendingReaction(runId);
this.clearRecoveryTimer(runId);
this.remindersSent.delete(runId);
this.pauseClock(runId);
autoresearchStoreActions.setRunStatus(runId, "paused");
this.persist(runId);
this.restoreOriginalStage(run);
Expand All @@ -231,6 +235,7 @@ export class AutoresearchService {
return;
}

this.settlePausedClock(runId);
Comment thread
MattPua marked this conversation as resolved.
this.clearRecoveryTimer(runId);
const session = getSessionForTask(
sessionStore.getState(),
Expand All @@ -257,6 +262,7 @@ export class AutoresearchService {
autoresearchStoreActions.setRunStatus(runId, "interrupted", {
interruptedReason: run.interruptedReason ?? "session-error",
});
this.pauseClock(runId);
this.persist(runId);
void this.attemptRecovery(runId);
return;
Expand Down Expand Up @@ -765,6 +771,7 @@ export class AutoresearchService {
if (run?.status === "running") {
this.clearPendingReaction(runId);
this.remindersSent.delete(runId);
this.pauseClock(runId);
autoresearchStoreActions.setRunStatus(runId, "paused");
this.persist(runId);
this.restoreOriginalStage(run);
Expand Down Expand Up @@ -806,6 +813,7 @@ export class AutoresearchService {
this.clearPendingReaction(runId);
// A fresh interruption gets its own reminder budget on resume.
this.remindersSent.delete(runId);
this.pauseClock(runId);
autoresearchStoreActions.setRunStatus(runId, "interrupted", {
interruptedReason: reason,
lastError,
Expand Down Expand Up @@ -885,6 +893,7 @@ export class AutoresearchService {
return;
}

this.settlePausedClock(runId);
this.clearRecoveryTimer(runId);
const session = getSessionForTask(
sessionStore.getState(),
Expand Down Expand Up @@ -915,6 +924,7 @@ export class AutoresearchService {
status: "completed" | "stopped" | "failed",
options: { endReason: AutoresearchEndReason; lastError?: string },
): void {
this.settlePausedClock(runId);
const run = autoresearchStore.getState().runs[runId];
autoresearchStoreActions.setRunStatus(runId, status, options);
this.persist(runId);
Expand All @@ -928,6 +938,29 @@ export class AutoresearchService {
if (run) this.restoreOriginalStage(run);
}

private pauseClock(runId: string): void {
const run = autoresearchStore.getState().runs[runId];
if (!run || run.pausedAt != null) return;
autoresearchStoreActions.setPauseTiming(
runId,
Date.now(),
run.pausedDurationMs ?? 0,
run.pauseIntervals ?? [],
);
}

private settlePausedClock(runId: string): void {
const run = autoresearchStore.getState().runs[runId];
if (!run || run.pausedAt == null) return;
const endedAt = Date.now();
autoresearchStoreActions.setPauseTiming(
runId,
null,
(run.pausedDurationMs ?? 0) + Math.max(0, endedAt - run.pausedAt),
[...(run.pauseIntervals ?? []), { startedAt: run.pausedAt, endedAt }],
);
}

private clearPendingReaction(runId: string): void {
const timer = this.pendingReactions.get(runId);
if (timer) clearTimeout(timer);
Expand Down
15 changes: 15 additions & 0 deletions packages/core/src/autoresearch/autoresearchStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
type AutoresearchEndReason,
type AutoresearchInterruptionReason,
type AutoresearchIteration,
type AutoresearchPauseInterval,
type AutoresearchPhase,
type AutoresearchResearchFinding,
type AutoresearchRun,
Expand Down Expand Up @@ -75,6 +76,20 @@ export const autoresearchStoreActions = {
updateRun(runId, (run) => ({ ...run, phase }));
},

setPauseTiming(
runId: string,
pausedAt: number | null,
pausedDurationMs: number,
pauseIntervals: AutoresearchPauseInterval[],
): void {
updateRun(runId, (run) => ({
...run,
pausedAt,
pausedDurationMs,
pauseIntervals,
}));
},

setRunStatus(
runId: string,
status: AutoresearchRunStatus,
Expand Down
16 changes: 15 additions & 1 deletion packages/core/src/autoresearch/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,14 @@ export function isTerminalRunStatus(status: AutoresearchRunStatus): boolean {
export const autoresearchPhaseSchema = z.enum(["implement", "measure"]);
export type AutoresearchPhase = z.infer<typeof autoresearchPhaseSchema>;

export const autoresearchPauseIntervalSchema = z.object({
startedAt: z.number(),
endedAt: z.number(),
});
export type AutoresearchPauseInterval = z.infer<
typeof autoresearchPauseIntervalSchema
>;

export const autoresearchRunSchema = z.object({
id: z.string().min(1),
config: autoresearchConfigSchema,
Expand All @@ -150,6 +158,9 @@ export const autoresearchRunSchema = z.object({
researchFindings: z.array(autoresearchResearchFindingSchema).default([]),
iterations: z.array(autoresearchIterationSchema),
startedAt: z.number(),
pausedAt: z.number().nullable().optional(),
pausedDurationMs: z.number().min(0).optional(),
pauseIntervals: z.array(autoresearchPauseIntervalSchema).optional(),
endedAt: z.number().nullable(),
endReason: autoresearchEndReasonSchema.nullable(),
interruptedReason: autoresearchInterruptionReasonSchema
Expand All @@ -176,7 +187,10 @@ export function parseStoredAutoresearchRun(
}
const parsed = autoresearchRunSchema.safeParse(raw);
if (!parsed.success) return null;
const run = parsed.data;
let run = parsed.data;
if (run.status === "paused" && run.pausedAt === undefined) {
run = { ...run, pausedAt: Date.now(), pausedDurationMs: 0 };
}
Comment thread
MattPua marked this conversation as resolved.
if (run.status !== "running") return run;
return {
...run,
Expand Down
39 changes: 37 additions & 2 deletions packages/core/src/autoresearch/stats.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { AutoresearchIteration, AutoresearchRun } from "./schemas";
import {
computeBest,
evaluateContinuation,
getAutoresearchElapsedMs,
isImprovement,
meetsTarget,
summarizeRun,
Expand Down Expand Up @@ -32,6 +33,11 @@ function makeRun(overrides: {
direction?: "maximize" | "minimize";
targetValue?: number | null;
maxIterations?: number;
startedAt?: number;
pausedAt?: number | null;
pausedDurationMs?: number;
pauseIntervals?: AutoresearchRun["pauseIntervals"];
endedAt?: number | null;
}): AutoresearchRun {
return {
id: "ar-1",
Expand All @@ -54,14 +60,43 @@ function makeRun(overrides: {
originalEffort: null,
researchFindings: [],
iterations: overrides.iterations ?? [],
startedAt: 0,
endedAt: null,
startedAt: overrides.startedAt ?? 0,
pausedAt: overrides.pausedAt,
pausedDurationMs: overrides.pausedDurationMs,
pauseIntervals: overrides.pauseIntervals ?? [],
endedAt: overrides.endedAt ?? null,
endReason: null,
interruptedReason: null,
lastError: null,
};
}

describe("getAutoresearchElapsedMs", () => {
it("freezes while paused and excludes completed pause intervals", () => {
expect(
getAutoresearchElapsedMs(
makeRun({ startedAt: 1_000, pausedAt: 11_000 }),
31_000,
),
).toBe(10_000);
expect(
getAutoresearchElapsedMs(
makeRun({ startedAt: 1_000, pausedDurationMs: 20_000 }),
36_000,
),
).toBe(15_000);
expect(
getAutoresearchElapsedMs(
makeRun({
startedAt: 1_000,
pauseIntervals: [{ startedAt: 11_000, endedAt: 31_000 }],
}),
36_000,
),
).toBe(15_000);
});
});

describe("isImprovement", () => {
it.each([
["maximize", 5, 3, true],
Expand Down
20 changes: 20 additions & 0 deletions packages/core/src/autoresearch/stats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,26 @@ import type {
AutoresearchRun,
} from "./schemas";

export function getAutoresearchElapsedMs(
run: AutoresearchRun,
now: number,
): number {
const effectiveEnd = run.endedAt ?? run.pausedAt ?? now;
const trackedPausedDurationMs = (run.pauseIntervals ?? []).reduce(
(total, interval) => {
const overlapStart = Math.max(run.startedAt, interval.startedAt);
const overlapEnd = Math.min(effectiveEnd, interval.endedAt);
return total + Math.max(0, overlapEnd - overlapStart);
},
0,
);
const pausedDurationMs = Math.max(
trackedPausedDurationMs,
run.pausedDurationMs ?? 0,
);
return Math.max(0, effectiveEnd - run.startedAt - pausedDurationMs);
}

export function isImprovement(
candidate: number,
reference: number | null,
Expand Down
Loading
Loading