diff --git a/apps/backend/src/lib/config/index.tsx b/apps/backend/src/lib/config/index.tsx index bd3234209..a6d962890 100644 --- a/apps/backend/src/lib/config/index.tsx +++ b/apps/backend/src/lib/config/index.tsx @@ -721,7 +721,9 @@ export async function cancelConfigAgentRun(options: { } await tx.configAgentRun.update({ where: { id: options.runId }, - data: { status: "cancelled", finishedAt: new Date(options.nowMs), sandboxId: null, stage: null, baseCommitSha: null }, + // Clear the captured change too: a cancelled run is abandoned, so its diff/base + // must not linger in the API shape or be replayable by the commit route. + data: { status: "cancelled", finishedAt: new Date(options.nowMs), sandboxId: null, stage: null, baseCommitSha: null, diff: null }, }); return { cancelled: true, sandboxId: run.sandboxId ?? undefined, previousStatus: run.status }; }); diff --git a/apps/dashboard/src/components/config-update/progress-content.tsx b/apps/dashboard/src/components/config-update/progress-content.tsx index 3be108ce7..d37af3eb9 100644 --- a/apps/dashboard/src/components/config-update/progress-content.tsx +++ b/apps/dashboard/src/components/config-update/progress-content.tsx @@ -26,20 +26,27 @@ function stageIndex(stage: AgentStage | null | undefined): number { /** * Live "seconds since the run started" counter. The run's `startedAt` is a - * wall-clock epoch value; we capture the start→now offset against a fresh - * monotonic anchor and then advance on that monotonic clock. The anchor and - * offset are recomputed whenever `startedAt` changes (e.g. when a flow renders - * the box before its real start timestamp is known), so the counter resets - * instead of freezing a stale offset. Recomputing the offset every render — - * which is what the old code did — re-added the elapsed time on top of the - * monotonic delta and made the timer tick at ~2× speed. + * wall-clock epoch value, with `0` as the "not started yet" sentinel — until a + * real start time arrives the counter stays at 0 (otherwise it would briefly + * read ~epoch-since-1970). For a real `startedAt` we capture the start→now offset + * against a fresh monotonic anchor and advance on that monotonic clock. Both are + * recomputed whenever `startedAt` changes (e.g. when a flow renders the box + * before its real start timestamp is known), so the counter resets instead of + * freezing a stale offset. Recomputing the offset every render — which the old + * code did — re-added the elapsed time on top of the monotonic delta and made + * the timer tick at ~2× speed. */ +function elapsedOffsetMs(startedAt: number): number { + return startedAt > 0 ? Math.max(0, currentEpochMsFromPerformance() - startedAt) : 0; +} + function useElapsedSeconds(startedAt: number): number { - const [elapsedMs, setElapsedMs] = useState(() => Math.max(0, currentEpochMsFromPerformance() - startedAt)); + const [elapsedMs, setElapsedMs] = useState(() => elapsedOffsetMs(startedAt)); useEffect(() => { - const anchorPerfMs = performance.now(); - const offsetMs = Math.max(0, currentEpochMsFromPerformance() - startedAt); + const offsetMs = elapsedOffsetMs(startedAt); setElapsedMs(offsetMs); + if (startedAt <= 0) return; + const anchorPerfMs = performance.now(); const t = setInterval(() => setElapsedMs(offsetMs + (performance.now() - anchorPerfMs)), 1000); return () => clearInterval(t); }, [startedAt]); diff --git a/packages/shared/src/config-eval.ts b/packages/shared/src/config-eval.ts index c8bd39b16..9efe83709 100644 --- a/packages/shared/src/config-eval.ts +++ b/packages/shared/src/config-eval.ts @@ -130,8 +130,11 @@ import.meta.vitest?.test("evalConfigFileContent rejects missing config import ta `, "/tmp/hexclave-missing-import-config.ts")).toThrow(); }); -import.meta.vitest?.test("evalConfigFileContent rejects syntactically invalid content", ({ expect }) => { - // jiti surfaces a ParseError (not a ConfigFileEvalError), so callers route this - // to "Failed to load config file" rather than "Invalid config". - expect(() => evalConfigFileContent("export const config = {", "stack.config.ts")).toThrow(); +import.meta.vitest?.test("evalConfigFileContent surfaces invalid syntax as a loader error, not ConfigFileEvalError", ({ expect }) => { + // A malformed file fails inside jiti's parser, so the thrown error is a loader + // error — NOT a ConfigFileEvalError. Callers depend on that distinction to route + // it to "Failed to load config file" rather than "Invalid config". + const evalInvalid = () => evalConfigFileContent("export const config = {", "stack.config.ts"); + expect(evalInvalid).toThrow(); + expect(evalInvalid).not.toThrow(ConfigFileEvalError); });