From 717d800847a32e7973a448728ff8d7979133abd4 Mon Sep 17 00:00:00 2001 From: Saikrishna1876 Date: Sat, 14 Mar 2026 18:42:26 +0530 Subject: [PATCH 1/5] fix: timer stops counting when waiting for user input (#1069) - Excludes user input wait time from work duration in UI - Increases timeouts in app-server and tests for reliability --- apps/server/src/codexAppServerManager.ts | 28 ++-- apps/server/src/git/Layers/GitCore.test.ts | 15 ++- .../Layers/CheckpointReactor.test.ts | 2 +- apps/server/vitest.config.ts | 4 +- apps/web/public/mockServiceWorker.js | 2 +- apps/web/src/components/ChatView.tsx | 28 +++- .../src/components/chat/MessagesTimeline.tsx | 53 ++++++-- apps/web/src/session-logic.test.ts | 121 ++++++++++++++++++ apps/web/src/session-logic.ts | 99 +++++++++++--- 9 files changed, 305 insertions(+), 47 deletions(-) diff --git a/apps/server/src/codexAppServerManager.ts b/apps/server/src/codexAppServerManager.ts index a8a8ce460..21ad0837c 100644 --- a/apps/server/src/codexAppServerManager.ts +++ b/apps/server/src/codexAppServerManager.ts @@ -164,6 +164,8 @@ const RECOVERABLE_THREAD_RESUME_ERROR_SNIPPETS = [ const CODEX_DEFAULT_MODEL = "gpt-5.3-codex"; const CODEX_SPARK_MODEL = "gpt-5.3-codex-spark"; const CODEX_SPARK_DISABLED_PLAN_TYPES = new Set(["free", "go", "plus"]); +const CODEX_LONG_RUNNING_TOOL_TIMEOUT_SEC = 315_360_000; +const CODEX_LONG_RUNNING_STREAM_IDLE_TIMEOUT_MS = 315_360_000_000; function asObject(value: unknown): Record | undefined { if (!value || typeof value !== "object") { @@ -548,15 +550,25 @@ export class CodexAppServerManager extends EventEmitter { yield* checkoutGitBranch({ cwd: source, branch: featureBranch }); const core = yield* GitCore; yield* Effect.promise(() => - vi.waitFor(async () => { - const details = await Effect.runPromise(core.statusDetails(source)); - expect(details.branch).toBe(featureBranch); - expect(details.aheadCount).toBe(0); - expect(details.behindCount).toBe(1); - }), + vi.waitFor( + async () => { + const details = await Effect.runPromise(core.statusDetails(source)); + expect(details.branch).toBe(featureBranch); + expect(details.aheadCount).toBe(0); + expect(details.behindCount).toBe(1); + }, + { timeout: 10_000 }, + ), ); }), ); diff --git a/apps/server/src/orchestration/Layers/CheckpointReactor.test.ts b/apps/server/src/orchestration/Layers/CheckpointReactor.test.ts index 09773b71d..3b4b491d5 100644 --- a/apps/server/src/orchestration/Layers/CheckpointReactor.test.ts +++ b/apps/server/src/orchestration/Layers/CheckpointReactor.test.ts @@ -862,7 +862,7 @@ describe("CheckpointReactor", () => { }), ); - const deadline = Date.now() + 2000; + const deadline = Date.now() + 10_000; const waitForRollbackCalls = async (): Promise => { if (harness.provider.rollbackConversation.mock.calls.length >= 2) { return; diff --git a/apps/server/vitest.config.ts b/apps/server/vitest.config.ts index cb86636fb..aa32a8d1e 100644 --- a/apps/server/vitest.config.ts +++ b/apps/server/vitest.config.ts @@ -6,8 +6,8 @@ export default mergeConfig( baseConfig, defineConfig({ test: { - testTimeout: 15_000, - hookTimeout: 15_000, + testTimeout: 240_000, + hookTimeout: 60_000, }, }), ); diff --git a/apps/web/public/mockServiceWorker.js b/apps/web/public/mockServiceWorker.js index 85e901012..daa58d0f1 100644 --- a/apps/web/public/mockServiceWorker.js +++ b/apps/web/public/mockServiceWorker.js @@ -7,7 +7,7 @@ * - Please do NOT modify this file. */ -const PACKAGE_VERSION = '2.12.9' +const PACKAGE_VERSION = '2.12.10' const INTEGRITY_CHECKSUM = '4db4a41e972cec1b64cc569c66952d82' const IS_MOCKED_RESPONSE = Symbol('isMockedResponse') const activeClientIds = new Set() diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index 9f625762c..afebf8daa 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -58,6 +58,7 @@ import { hasToolActivityForTurn, isLatestTurnSettled, formatElapsed, + formatTurnWorkElapsedExcludingUserInputWait, } from "../session-logic"; import { isScrollContainerNearBottom } from "../chat-scroll"; import { @@ -568,7 +569,6 @@ export default function ChatView({ threadId }: ChatViewProps) { const phase = derivePhase(activeThread?.session ?? null); const isSendBusy = sendPhase !== "idle"; const isPreparingWorktree = sendPhase === "preparing-worktree"; - const isWorking = phase === "running" || isSendBusy || isConnecting || isRevertingCheckpoint; const nowIso = new Date(nowTick).toISOString(); const activeWorkStartedAt = deriveActiveWorkStartedAt( activeLatestTurn, @@ -593,6 +593,10 @@ export default function ChatView({ threadId }: ChatViewProps) { [threadActivities], ); const activePendingUserInput = pendingUserInputs[0] ?? null; + const isWaitingForUserInput = activePendingUserInput !== null; + const isWorking = + (phase === "running" || isSendBusy || isConnecting || isRevertingCheckpoint) && + !isWaitingForUserInput; const activePendingDraftAnswers = useMemo( () => activePendingUserInput @@ -862,14 +866,27 @@ export default function ChatView({ threadId }: ChatViewProps) { if (!activeLatestTurn.completedAt) return null; if (!latestTurnHasToolActivity) return null; - const elapsed = formatElapsed(activeLatestTurn.startedAt, activeLatestTurn.completedAt); + const elapsed = formatTurnWorkElapsedExcludingUserInputWait( + activeLatestTurn.startedAt, + activeLatestTurn.completedAt, + threadActivities, + ); return elapsed ? `Worked for ${elapsed}` : null; }, [ activeLatestTurn?.completedAt, activeLatestTurn?.startedAt, latestTurnHasToolActivity, latestTurnSettled, + threadActivities, ]); + const workingForLabel = useMemo(() => { + if (!isWorking) return null; + if (!activeWorkStartedAt) return null; + return ( + formatTurnWorkElapsedExcludingUserInputWait(activeWorkStartedAt, nowIso, threadActivities) ?? + formatElapsed(activeWorkStartedAt, nowIso) + ); + }, [activeWorkStartedAt, isWorking, nowIso, threadActivities]); const completionDividerBeforeEntryId = useMemo(() => { if (!latestTurnSettled) return null; if (!activeLatestTurn?.startedAt) return null; @@ -1900,14 +1917,14 @@ export default function ChatView({ threadId }: ChatViewProps) { : "local"; useEffect(() => { - if (phase !== "running") return; + if (phase !== "running" || isWaitingForUserInput) return; const timer = window.setInterval(() => { setNowTick(Date.now()); }, 1000); return () => { window.clearInterval(timer); }; - }, [phase]); + }, [isWaitingForUserInput, phase]); const beginSendPhase = useCallback((nextPhase: Exclude) => { setSendStartedAt((current) => current ?? new Date().toISOString()); @@ -3278,6 +3295,9 @@ export default function ChatView({ threadId }: ChatViewProps) { key={activeThread.id} hasMessages={timelineEntries.length > 0} isWorking={isWorking} + isWaitingForUserInput={isWaitingForUserInput} + waitingStartedAt={activePendingUserInput?.createdAt ?? null} + workingForLabel={workingForLabel} activeTurnInProgress={isWorking || !latestTurnSettled} activeTurnStartedAt={activeWorkStartedAt} scrollContainer={messagesScrollElement} diff --git a/apps/web/src/components/chat/MessagesTimeline.tsx b/apps/web/src/components/chat/MessagesTimeline.tsx index e30801041..d22a5c653 100644 --- a/apps/web/src/components/chat/MessagesTimeline.tsx +++ b/apps/web/src/components/chat/MessagesTimeline.tsx @@ -43,6 +43,9 @@ const ALWAYS_UNVIRTUALIZED_TAIL_ROWS = 8; interface MessagesTimelineProps { hasMessages: boolean; isWorking: boolean; + isWaitingForUserInput: boolean; + waitingStartedAt: string | null; + workingForLabel: string | null; activeTurnInProgress: boolean; activeTurnStartedAt: string | null; scrollContainer: HTMLDivElement | null; @@ -67,6 +70,9 @@ interface MessagesTimelineProps { export const MessagesTimeline = memo(function MessagesTimeline({ hasMessages, isWorking, + isWaitingForUserInput, + waitingStartedAt, + workingForLabel, activeTurnInProgress, activeTurnStartedAt, scrollContainer, @@ -113,7 +119,7 @@ export const MessagesTimeline = memo(function MessagesTimeline({ return () => { observer.disconnect(); }; - }, [hasMessages, isWorking]); + }, [hasMessages, isWorking, isWaitingForUserInput]); const rows = useMemo(() => { const nextRows: TimelineRow[] = []; @@ -175,10 +181,23 @@ export const MessagesTimeline = memo(function MessagesTimeline({ id: "working-indicator-row", createdAt: activeTurnStartedAt, }); + } else if (isWaitingForUserInput) { + nextRows.push({ + kind: "waiting", + id: "waiting-indicator-row", + createdAt: waitingStartedAt ?? activeTurnStartedAt, + }); } return nextRows; - }, [timelineEntries, completionDividerBeforeEntryId, isWorking, activeTurnStartedAt]); + }, [ + timelineEntries, + completionDividerBeforeEntryId, + isWorking, + activeTurnStartedAt, + isWaitingForUserInput, + waitingStartedAt, + ]); const firstUnvirtualizedRowIndex = useMemo(() => { const firstTailRowIndex = Math.max(rows.length - ALWAYS_UNVIRTUALIZED_TAIL_ROWS, 0); @@ -189,7 +208,7 @@ export const MessagesTimeline = memo(function MessagesTimeline({ let firstCurrentTurnRowIndex = -1; if (!Number.isNaN(turnStartedAtMs)) { firstCurrentTurnRowIndex = rows.findIndex((row) => { - if (row.kind === "working") return true; + if (row.kind === "working" || row.kind === "waiting") return true; if (!row.createdAt) return false; const rowCreatedAtMs = Date.parse(row.createdAt); return !Number.isNaN(rowCreatedAtMs) && rowCreatedAtMs >= turnStartedAtMs; @@ -233,7 +252,7 @@ export const MessagesTimeline = memo(function MessagesTimeline({ if (!row) return 96; if (row.kind === "work") return 112; if (row.kind === "proposed-plan") return estimateTimelineProposedPlanHeight(row.proposedPlan); - if (row.kind === "working") return 40; + if (row.kind === "working" || row.kind === "waiting") return 40; return estimateTimelineMessageHeight(row.message, { timelineWidthPx }); }, measureElement: measureVirtualElement, @@ -518,17 +537,32 @@ export const MessagesTimeline = memo(function MessagesTimeline({ - {row.createdAt - ? `Working for ${formatWorkingTimer(row.createdAt, nowIso) ?? "0s"}` - : "Working..."} + {workingForLabel + ? `Working for ${workingForLabel}` + : row.createdAt + ? `Working for ${formatWorkingTimer(row.createdAt, nowIso) ?? "0s"}` + : "Working..."} + + + + )} + + {row.kind === "waiting" && ( +
+
+ + + + + Waiting for you
)} ); - if (!hasMessages && !isWorking) { + if (!hasMessages && !isWorking && !isWaitingForUserInput) { return (

@@ -597,7 +631,8 @@ type TimelineRow = createdAt: string; proposedPlan: TimelineProposedPlan; } - | { kind: "working"; id: string; createdAt: string | null }; + | { kind: "working"; id: string; createdAt: string | null } + | { kind: "waiting"; id: string; createdAt: string | null }; function estimateTimelineProposedPlanHeight(proposedPlan: TimelineProposedPlan): number { const estimatedLines = Math.max(1, Math.ceil(proposedPlan.planMarkdown.length / 72)); diff --git a/apps/web/src/session-logic.test.ts b/apps/web/src/session-logic.test.ts index 74ba3a814..a4f59d98b 100644 --- a/apps/web/src/session-logic.test.ts +++ b/apps/web/src/session-logic.test.ts @@ -10,6 +10,7 @@ import { deriveTimelineEntries, deriveWorkLogEntries, findLatestProposedPlan, + formatTurnWorkElapsedExcludingUserInputWait, hasToolActivityForTurn, isLatestTurnSettled, } from "./session-logic"; @@ -222,6 +223,126 @@ describe("derivePendingUserInputs", () => { }); }); +describe("formatTurnWorkElapsedExcludingUserInputWait", () => { + it("subtracts a fully resolved wait interval from total elapsed", () => { + const startIso = "2026-02-23T00:00:00.000Z"; + const endIso = "2026-02-23T00:01:00.000Z"; + const activities: OrchestrationThreadActivity[] = [ + makeActivity({ + id: "wait-requested", + createdAt: "2026-02-23T00:00:10.000Z", + kind: "user-input.requested", + summary: "User input requested", + tone: "info", + payload: { requestId: "req-wait-1" }, + }), + makeActivity({ + id: "wait-resolved", + createdAt: "2026-02-23T00:00:40.000Z", + kind: "user-input.resolved", + summary: "User input submitted", + tone: "info", + payload: { requestId: "req-wait-1" }, + }), + ]; + + expect(formatTurnWorkElapsedExcludingUserInputWait(startIso, endIso, activities)).toBe("30s"); + }); + + it("subtracts an open wait interval up to endIso (freezes elapsed at request time)", () => { + const startIso = "2026-02-23T00:00:00.000Z"; + const endIso = "2026-02-23T00:01:00.000Z"; + const activities: OrchestrationThreadActivity[] = [ + makeActivity({ + id: "wait-requested-open", + createdAt: "2026-02-23T00:00:10.000Z", + kind: "user-input.requested", + summary: "User input requested", + tone: "info", + payload: { requestId: "req-open-1" }, + }), + ]; + + expect(formatTurnWorkElapsedExcludingUserInputWait(startIso, endIso, activities)).toBe("10s"); + }); + + it("ignores malformed payloads / missing requestIds safely", () => { + const startIso = "2026-02-23T00:00:00.000Z"; + const endIso = "2026-02-23T00:01:00.000Z"; + const activities: OrchestrationThreadActivity[] = [ + makeActivity({ + id: "wait-requested-missing", + createdAt: "2026-02-23T00:00:10.000Z", + kind: "user-input.requested", + summary: "User input requested", + tone: "info", + payload: {}, + }), + makeActivity({ + id: "wait-resolved-missing", + createdAt: "2026-02-23T00:00:20.000Z", + kind: "user-input.resolved", + summary: "User input submitted", + tone: "info", + payload: {}, + }), + makeActivity({ + id: "wait-requested-non-string", + createdAt: "2026-02-23T00:00:30.000Z", + kind: "user-input.requested", + summary: "User input requested", + tone: "info", + payload: { requestId: 123 }, + }), + ]; + + expect(formatTurnWorkElapsedExcludingUserInputWait(startIso, endIso, activities)).toBe("1m"); + }); + + it("handles multiple wait intervals (sum of overlaps)", () => { + const startIso = "2026-02-23T00:00:00.000Z"; + const endIso = "2026-02-23T00:02:00.000Z"; + const activities: OrchestrationThreadActivity[] = [ + makeActivity({ + id: "wait-1-requested", + createdAt: "2026-02-23T00:00:10.000Z", + kind: "user-input.requested", + summary: "User input requested", + tone: "info", + payload: { requestId: "req-multi-1" }, + }), + makeActivity({ + id: "wait-1-resolved", + createdAt: "2026-02-23T00:00:20.000Z", + kind: "user-input.resolved", + summary: "User input submitted", + tone: "info", + payload: { requestId: "req-multi-1" }, + }), + makeActivity({ + id: "wait-2-requested", + createdAt: "2026-02-23T00:01:00.000Z", + kind: "user-input.requested", + summary: "User input requested", + tone: "info", + payload: { requestId: "req-multi-2" }, + }), + makeActivity({ + id: "wait-2-resolved", + createdAt: "2026-02-23T00:01:30.000Z", + kind: "user-input.resolved", + summary: "User input submitted", + tone: "info", + payload: { requestId: "req-multi-2" }, + }), + ]; + + expect(formatTurnWorkElapsedExcludingUserInputWait(startIso, endIso, activities)).toBe( + "1m 20s", + ); + }); +}); + describe("deriveActivePlanState", () => { it("returns the latest plan update for the active turn", () => { const activities: OrchestrationThreadActivity[] = [ diff --git a/apps/web/src/session-logic.ts b/apps/web/src/session-logic.ts index e389f10e2..28e183013 100644 --- a/apps/web/src/session-logic.ts +++ b/apps/web/src/session-logic.ts @@ -116,6 +116,85 @@ export function formatElapsed(startIso: string, endIso: string | undefined): str return formatDuration(endedAt - startedAt); } +function payloadFromActivity( + activity: OrchestrationThreadActivity, +): Record | null { + return activity.payload && typeof activity.payload === "object" + ? (activity.payload as Record) + : null; +} + +function requestIdFromPayload(payload: Record | null): ApprovalRequestId | null { + return payload && typeof payload.requestId === "string" + ? ApprovalRequestId.makeUnsafe(payload.requestId) + : null; +} + +export function formatTurnWorkElapsedExcludingUserInputWait( + startIso: string, + endIso: string, + activities: ReadonlyArray, +): string | null { + const startMs = Date.parse(startIso); + const endMs = Date.parse(endIso); + if (Number.isNaN(startMs) || Number.isNaN(endMs) || endMs < startMs) { + return null; + } + + const ordered = [...activities].toSorted(compareActivitiesByOrder); + const openWaitStartByRequestId = new Map(); + let waitingMs = 0; + + for (const activity of ordered) { + if (activity.kind !== "user-input.requested" && activity.kind !== "user-input.resolved") { + continue; + } + + const payload = payloadFromActivity(activity); + const requestId = requestIdFromPayload(payload); + if (!requestId) { + continue; + } + + const activityMs = Date.parse(activity.createdAt); + if (Number.isNaN(activityMs)) { + if (activity.kind === "user-input.resolved") { + openWaitStartByRequestId.delete(requestId); + } + continue; + } + + if (activity.kind === "user-input.requested") { + if (!openWaitStartByRequestId.has(requestId)) { + openWaitStartByRequestId.set(requestId, activityMs); + } + continue; + } + + const waitStartMs = openWaitStartByRequestId.get(requestId); + openWaitStartByRequestId.delete(requestId); + if (waitStartMs === undefined) { + continue; + } + + const overlapStartMs = Math.max(startMs, waitStartMs); + const overlapEndMs = Math.min(endMs, activityMs); + if (overlapEndMs > overlapStartMs) { + waitingMs += overlapEndMs - overlapStartMs; + } + } + + for (const waitStartMs of openWaitStartByRequestId.values()) { + const overlapStartMs = Math.max(startMs, waitStartMs); + if (endMs > overlapStartMs) { + waitingMs += endMs - overlapStartMs; + } + } + + const effectiveMs = Math.max(0, endMs - startMs - waitingMs); + return formatDuration(effectiveMs); +} + type LatestTurnTiming = Pick; type SessionActivityState = Pick; @@ -163,14 +242,8 @@ export function derivePendingApprovals( const ordered = [...activities].toSorted(compareActivitiesByOrder); for (const activity of ordered) { - const payload = - activity.payload && typeof activity.payload === "object" - ? (activity.payload as Record) - : null; - const requestId = - payload && typeof payload.requestId === "string" - ? ApprovalRequestId.makeUnsafe(payload.requestId) - : null; + const payload = payloadFromActivity(activity); + const requestId = requestIdFromPayload(payload); const requestKind = payload && (payload.requestKind === "command" || @@ -268,14 +341,8 @@ export function derivePendingUserInputs( const ordered = [...activities].toSorted(compareActivitiesByOrder); for (const activity of ordered) { - const payload = - activity.payload && typeof activity.payload === "object" - ? (activity.payload as Record) - : null; - const requestId = - payload && typeof payload.requestId === "string" - ? ApprovalRequestId.makeUnsafe(payload.requestId) - : null; + const payload = payloadFromActivity(activity); + const requestId = requestIdFromPayload(payload); if (activity.kind === "user-input.requested" && requestId) { const questions = parseUserInputQuestions(payload); From ff8596dbe168d8849492f8f5abd0495e92a50c9f Mon Sep 17 00:00:00 2001 From: Saikrishna1876 Date: Sat, 14 Mar 2026 18:59:41 +0530 Subject: [PATCH 2/5] fix: avoid double-counting overlapping user input waits --- apps/web/src/session-logic.ts | 44 ++++++++++++++++++++++++++++------- 1 file changed, 35 insertions(+), 9 deletions(-) diff --git a/apps/web/src/session-logic.ts b/apps/web/src/session-logic.ts index 28e183013..5c3c17d8d 100644 --- a/apps/web/src/session-logic.ts +++ b/apps/web/src/session-logic.ts @@ -143,7 +143,15 @@ export function formatTurnWorkElapsedExcludingUserInputWait( const ordered = [...activities].toSorted(compareActivitiesByOrder); const openWaitStartByRequestId = new Map(); - let waitingMs = 0; + const waitIntervals: Array<{ startMs: number; endMs: number }> = []; + + const recordWaitInterval = (waitStartMs: number, waitEndMs: number) => { + const overlapStartMs = Math.max(startMs, waitStartMs); + const overlapEndMs = Math.min(endMs, waitEndMs); + if (overlapEndMs > overlapStartMs) { + waitIntervals.push({ startMs: overlapStartMs, endMs: overlapEndMs }); + } + }; for (const activity of ordered) { if (activity.kind !== "user-input.requested" && activity.kind !== "user-input.resolved") { @@ -177,18 +185,36 @@ export function formatTurnWorkElapsedExcludingUserInputWait( continue; } - const overlapStartMs = Math.max(startMs, waitStartMs); - const overlapEndMs = Math.min(endMs, activityMs); - if (overlapEndMs > overlapStartMs) { - waitingMs += overlapEndMs - overlapStartMs; - } + recordWaitInterval(waitStartMs, activityMs); } for (const waitStartMs of openWaitStartByRequestId.values()) { - const overlapStartMs = Math.max(startMs, waitStartMs); - if (endMs > overlapStartMs) { - waitingMs += endMs - overlapStartMs; + recordWaitInterval(waitStartMs, endMs); + } + + const mergedWaitIntervals = waitIntervals.toSorted( + (left, right) => left.startMs - right.startMs || left.endMs - right.endMs, + ); + let waitingMs = 0; + let currentInterval: { startMs: number; endMs: number } | null = null; + + for (const interval of mergedWaitIntervals) { + if (!currentInterval) { + currentInterval = interval; + continue; } + + if (interval.startMs <= currentInterval.endMs) { + currentInterval.endMs = Math.max(currentInterval.endMs, interval.endMs); + continue; + } + + waitingMs += currentInterval.endMs - currentInterval.startMs; + currentInterval = interval; + } + + if (currentInterval) { + waitingMs += currentInterval.endMs - currentInterval.startMs; } const effectiveMs = Math.max(0, endMs - startMs - waitingMs); From e3a7e9f257411c44321cb791b6de822a757bde71 Mon Sep 17 00:00:00 2001 From: Saikrishna1876 Date: Sat, 14 Mar 2026 19:14:51 +0530 Subject: [PATCH 3/5] fix: revert timeout increases from user-input timer fix --- apps/server/src/codexAppServerManager.ts | 29 +++++-------------- apps/server/src/git/Layers/GitCore.test.ts | 15 ++++------ .../Layers/CheckpointReactor.test.ts | 2 +- apps/server/vitest.config.ts | 4 +-- apps/web/public/mockServiceWorker.js | 2 +- 5 files changed, 18 insertions(+), 34 deletions(-) diff --git a/apps/server/src/codexAppServerManager.ts b/apps/server/src/codexAppServerManager.ts index 21ad0837c..f00c6fee3 100644 --- a/apps/server/src/codexAppServerManager.ts +++ b/apps/server/src/codexAppServerManager.ts @@ -164,9 +164,6 @@ const RECOVERABLE_THREAD_RESUME_ERROR_SNIPPETS = [ const CODEX_DEFAULT_MODEL = "gpt-5.3-codex"; const CODEX_SPARK_MODEL = "gpt-5.3-codex-spark"; const CODEX_SPARK_DISABLED_PLAN_TYPES = new Set(["free", "go", "plus"]); -const CODEX_LONG_RUNNING_TOOL_TIMEOUT_SEC = 315_360_000; -const CODEX_LONG_RUNNING_STREAM_IDLE_TIMEOUT_MS = 315_360_000_000; - function asObject(value: unknown): Record | undefined { if (!value || typeof value !== "object") { return undefined; @@ -550,25 +547,15 @@ export class CodexAppServerManager extends EventEmitter { yield* checkoutGitBranch({ cwd: source, branch: featureBranch }); const core = yield* GitCore; yield* Effect.promise(() => - vi.waitFor( - async () => { - const details = await Effect.runPromise(core.statusDetails(source)); - expect(details.branch).toBe(featureBranch); - expect(details.aheadCount).toBe(0); - expect(details.behindCount).toBe(1); - }, - { timeout: 10_000 }, - ), + vi.waitFor(async () => { + const details = await Effect.runPromise(core.statusDetails(source)); + expect(details.branch).toBe(featureBranch); + expect(details.aheadCount).toBe(0); + expect(details.behindCount).toBe(1); + }), ); }), ); diff --git a/apps/server/src/orchestration/Layers/CheckpointReactor.test.ts b/apps/server/src/orchestration/Layers/CheckpointReactor.test.ts index 3b4b491d5..09773b71d 100644 --- a/apps/server/src/orchestration/Layers/CheckpointReactor.test.ts +++ b/apps/server/src/orchestration/Layers/CheckpointReactor.test.ts @@ -862,7 +862,7 @@ describe("CheckpointReactor", () => { }), ); - const deadline = Date.now() + 10_000; + const deadline = Date.now() + 2000; const waitForRollbackCalls = async (): Promise => { if (harness.provider.rollbackConversation.mock.calls.length >= 2) { return; diff --git a/apps/server/vitest.config.ts b/apps/server/vitest.config.ts index aa32a8d1e..cb86636fb 100644 --- a/apps/server/vitest.config.ts +++ b/apps/server/vitest.config.ts @@ -6,8 +6,8 @@ export default mergeConfig( baseConfig, defineConfig({ test: { - testTimeout: 240_000, - hookTimeout: 60_000, + testTimeout: 15_000, + hookTimeout: 15_000, }, }), ); diff --git a/apps/web/public/mockServiceWorker.js b/apps/web/public/mockServiceWorker.js index daa58d0f1..85e901012 100644 --- a/apps/web/public/mockServiceWorker.js +++ b/apps/web/public/mockServiceWorker.js @@ -7,7 +7,7 @@ * - Please do NOT modify this file. */ -const PACKAGE_VERSION = '2.12.10' +const PACKAGE_VERSION = '2.12.9' const INTEGRITY_CHECKSUM = '4db4a41e972cec1b64cc569c66952d82' const IS_MOCKED_RESPONSE = Symbol('isMockedResponse') const activeClientIds = new Set() From 2f5b413942e1ad943242f90c537d8b58fe247363 Mon Sep 17 00:00:00 2001 From: Saikrishna Ambeti <106900645+Saikrishna1876@users.noreply.github.com> Date: Sat, 14 Mar 2026 19:18:30 +0530 Subject: [PATCH 4/5] Add asObject function to handle unknown values --- apps/server/src/codexAppServerManager.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/server/src/codexAppServerManager.ts b/apps/server/src/codexAppServerManager.ts index f00c6fee3..a8a8ce460 100644 --- a/apps/server/src/codexAppServerManager.ts +++ b/apps/server/src/codexAppServerManager.ts @@ -164,6 +164,7 @@ const RECOVERABLE_THREAD_RESUME_ERROR_SNIPPETS = [ const CODEX_DEFAULT_MODEL = "gpt-5.3-codex"; const CODEX_SPARK_MODEL = "gpt-5.3-codex-spark"; const CODEX_SPARK_DISABLED_PLAN_TYPES = new Set(["free", "go", "plus"]); + function asObject(value: unknown): Record | undefined { if (!value || typeof value !== "object") { return undefined; From a52aa83802451e5a79fd54f7d68b05103087faca Mon Sep 17 00:00:00 2001 From: Saikrishna1876 Date: Sat, 14 Mar 2026 19:42:51 +0530 Subject: [PATCH 5/5] fix: precompute user input wait intervals for live timers --- apps/web/src/components/ChatView.tsx | 18 ++++-- apps/web/src/session-logic.test.ts | 85 ++++++++++++++++++++++++++-- apps/web/src/session-logic.ts | 76 ++++++++++++++++--------- 3 files changed, 140 insertions(+), 39 deletions(-) diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index afebf8daa..d7f47167b 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -53,6 +53,7 @@ import { deriveTimelineEntries, deriveActiveWorkStartedAt, deriveActivePlanState, + deriveUserInputWaitIntervals, findLatestProposedPlan, deriveWorkLogEntries, hasToolActivityForTurn, @@ -592,6 +593,10 @@ export default function ChatView({ threadId }: ChatViewProps) { () => derivePendingUserInputs(threadActivities), [threadActivities], ); + const userInputWaitIntervals = useMemo( + () => deriveUserInputWaitIntervals(threadActivities), + [threadActivities], + ); const activePendingUserInput = pendingUserInputs[0] ?? null; const isWaitingForUserInput = activePendingUserInput !== null; const isWorking = @@ -869,7 +874,7 @@ export default function ChatView({ threadId }: ChatViewProps) { const elapsed = formatTurnWorkElapsedExcludingUserInputWait( activeLatestTurn.startedAt, activeLatestTurn.completedAt, - threadActivities, + userInputWaitIntervals, ); return elapsed ? `Worked for ${elapsed}` : null; }, [ @@ -877,16 +882,19 @@ export default function ChatView({ threadId }: ChatViewProps) { activeLatestTurn?.startedAt, latestTurnHasToolActivity, latestTurnSettled, - threadActivities, + userInputWaitIntervals, ]); const workingForLabel = useMemo(() => { if (!isWorking) return null; if (!activeWorkStartedAt) return null; return ( - formatTurnWorkElapsedExcludingUserInputWait(activeWorkStartedAt, nowIso, threadActivities) ?? - formatElapsed(activeWorkStartedAt, nowIso) + formatTurnWorkElapsedExcludingUserInputWait( + activeWorkStartedAt, + nowIso, + userInputWaitIntervals, + ) ?? formatElapsed(activeWorkStartedAt, nowIso) ); - }, [activeWorkStartedAt, isWorking, nowIso, threadActivities]); + }, [activeWorkStartedAt, isWorking, nowIso, userInputWaitIntervals]); const completionDividerBeforeEntryId = useMemo(() => { if (!latestTurnSettled) return null; if (!activeLatestTurn?.startedAt) return null; diff --git a/apps/web/src/session-logic.test.ts b/apps/web/src/session-logic.test.ts index a4f59d98b..fc5ec1748 100644 --- a/apps/web/src/session-logic.test.ts +++ b/apps/web/src/session-logic.test.ts @@ -8,6 +8,7 @@ import { derivePendingApprovals, derivePendingUserInputs, deriveTimelineEntries, + deriveUserInputWaitIntervals, deriveWorkLogEntries, findLatestProposedPlan, formatTurnWorkElapsedExcludingUserInputWait, @@ -246,7 +247,13 @@ describe("formatTurnWorkElapsedExcludingUserInputWait", () => { }), ]; - expect(formatTurnWorkElapsedExcludingUserInputWait(startIso, endIso, activities)).toBe("30s"); + expect( + formatTurnWorkElapsedExcludingUserInputWait( + startIso, + endIso, + deriveUserInputWaitIntervals(activities), + ), + ).toBe("30s"); }); it("subtracts an open wait interval up to endIso (freezes elapsed at request time)", () => { @@ -263,7 +270,13 @@ describe("formatTurnWorkElapsedExcludingUserInputWait", () => { }), ]; - expect(formatTurnWorkElapsedExcludingUserInputWait(startIso, endIso, activities)).toBe("10s"); + expect( + formatTurnWorkElapsedExcludingUserInputWait( + startIso, + endIso, + deriveUserInputWaitIntervals(activities), + ), + ).toBe("10s"); }); it("ignores malformed payloads / missing requestIds safely", () => { @@ -296,7 +309,13 @@ describe("formatTurnWorkElapsedExcludingUserInputWait", () => { }), ]; - expect(formatTurnWorkElapsedExcludingUserInputWait(startIso, endIso, activities)).toBe("1m"); + expect( + formatTurnWorkElapsedExcludingUserInputWait( + startIso, + endIso, + deriveUserInputWaitIntervals(activities), + ), + ).toBe("1m"); }); it("handles multiple wait intervals (sum of overlaps)", () => { @@ -337,9 +356,63 @@ describe("formatTurnWorkElapsedExcludingUserInputWait", () => { }), ]; - expect(formatTurnWorkElapsedExcludingUserInputWait(startIso, endIso, activities)).toBe( - "1m 20s", - ); + expect( + formatTurnWorkElapsedExcludingUserInputWait( + startIso, + endIso, + deriveUserInputWaitIntervals(activities), + ), + ).toBe("1m 20s"); + }); + + it("sorts intervals and preserves open waits for live timers", () => { + const activities: OrchestrationThreadActivity[] = [ + makeActivity({ + id: "wait-requested-late", + createdAt: "2026-02-23T00:00:20.000Z", + kind: "user-input.requested", + summary: "User input requested", + tone: "info", + payload: { requestId: "req-seq-1" }, + sequence: 2, + }), + makeActivity({ + id: "wait-resolved-late", + createdAt: "2026-02-23T00:00:30.000Z", + kind: "user-input.resolved", + summary: "User input submitted", + tone: "info", + payload: { requestId: "req-seq-1" }, + sequence: 3, + }), + makeActivity({ + id: "wait-requested-early", + createdAt: "2026-02-23T00:00:10.000Z", + kind: "user-input.requested", + summary: "User input requested", + tone: "info", + payload: { requestId: "req-seq-2" }, + sequence: 1, + }), + makeActivity({ + id: "wait-requested-open", + createdAt: "2026-02-23T00:00:40.000Z", + kind: "user-input.requested", + summary: "User input requested", + tone: "info", + payload: { requestId: "req-open-2" }, + sequence: 4, + }), + ]; + + expect(deriveUserInputWaitIntervals(activities)).toEqual([ + { startMs: Date.parse("2026-02-23T00:00:10.000Z"), endMs: null }, + { + startMs: Date.parse("2026-02-23T00:00:20.000Z"), + endMs: Date.parse("2026-02-23T00:00:30.000Z"), + }, + { startMs: Date.parse("2026-02-23T00:00:40.000Z"), endMs: null }, + ]); }); }); diff --git a/apps/web/src/session-logic.ts b/apps/web/src/session-logic.ts index 5c3c17d8d..8e81b8268 100644 --- a/apps/web/src/session-logic.ts +++ b/apps/web/src/session-logic.ts @@ -56,6 +56,11 @@ export interface PendingUserInput { questions: ReadonlyArray; } +export interface UserInputWaitInterval { + startMs: number; + endMs: number | null; +} + export interface ActivePlanState { createdAt: string; turnId: TurnId | null; @@ -130,28 +135,12 @@ function requestIdFromPayload(payload: Record | null): Approval : null; } -export function formatTurnWorkElapsedExcludingUserInputWait( - startIso: string, - endIso: string, +export function deriveUserInputWaitIntervals( activities: ReadonlyArray, -): string | null { - const startMs = Date.parse(startIso); - const endMs = Date.parse(endIso); - if (Number.isNaN(startMs) || Number.isNaN(endMs) || endMs < startMs) { - return null; - } - +): UserInputWaitInterval[] { const ordered = [...activities].toSorted(compareActivitiesByOrder); const openWaitStartByRequestId = new Map(); - const waitIntervals: Array<{ startMs: number; endMs: number }> = []; - - const recordWaitInterval = (waitStartMs: number, waitEndMs: number) => { - const overlapStartMs = Math.max(startMs, waitStartMs); - const overlapEndMs = Math.min(endMs, waitEndMs); - if (overlapEndMs > overlapStartMs) { - waitIntervals.push({ startMs: overlapStartMs, endMs: overlapEndMs }); - } - }; + const waitIntervals: UserInputWaitInterval[] = []; for (const activity of ordered) { if (activity.kind !== "user-input.requested" && activity.kind !== "user-input.resolved") { @@ -185,32 +174,63 @@ export function formatTurnWorkElapsedExcludingUserInputWait( continue; } - recordWaitInterval(waitStartMs, activityMs); + if (activityMs <= waitStartMs) { + continue; + } + + waitIntervals.push({ startMs: waitStartMs, endMs: activityMs }); } for (const waitStartMs of openWaitStartByRequestId.values()) { - recordWaitInterval(waitStartMs, endMs); + waitIntervals.push({ startMs: waitStartMs, endMs: null }); } - const mergedWaitIntervals = waitIntervals.toSorted( - (left, right) => left.startMs - right.startMs || left.endMs - right.endMs, + return waitIntervals.toSorted( + (left, right) => + left.startMs - right.startMs || + (left.endMs ?? Number.POSITIVE_INFINITY) - (right.endMs ?? Number.POSITIVE_INFINITY), ); +} + +export function formatTurnWorkElapsedExcludingUserInputWait( + startIso: string, + endIso: string, + waitIntervals: ReadonlyArray, +): string | null { + const startMs = Date.parse(startIso); + const endMs = Date.parse(endIso); + if (Number.isNaN(startMs) || Number.isNaN(endMs) || endMs < startMs) { + return null; + } + let waitingMs = 0; let currentInterval: { startMs: number; endMs: number } | null = null; - for (const interval of mergedWaitIntervals) { + for (const interval of waitIntervals) { + const waitEndMs = interval.endMs ?? endMs; + const overlapStartMs = Math.max(startMs, interval.startMs); + const overlapEndMs = Math.min(endMs, waitEndMs); + if (overlapEndMs <= overlapStartMs) { + continue; + } + + const overlappingInterval = { + startMs: overlapStartMs, + endMs: overlapEndMs, + }; + if (!currentInterval) { - currentInterval = interval; + currentInterval = overlappingInterval; continue; } - if (interval.startMs <= currentInterval.endMs) { - currentInterval.endMs = Math.max(currentInterval.endMs, interval.endMs); + if (overlappingInterval.startMs <= currentInterval.endMs) { + currentInterval.endMs = Math.max(currentInterval.endMs, overlappingInterval.endMs); continue; } waitingMs += currentInterval.endMs - currentInterval.startMs; - currentInterval = interval; + currentInterval = overlappingInterval; } if (currentInterval) {