





















@@ -5471,6 +5471,111 @@ describe("runCodexAppServerAttempt", () => {
54715471).toBe(false);
54725472});
547354735474+it("arms completion idle watch after non-assistant rawResponseItem/completed with no active items", async () => {
5475+let notify: (notification: CodexServerNotification) => Promise<void> = async () => undefined;
5476+let handleRequest:
5477+| ((request: { id: string; method: string; params?: unknown }) => Promise<unknown>)
5478+| undefined;
5479+const warn = vi.spyOn(embeddedAgentLog, "warn").mockImplementation(() => undefined);
5480+const request = vi.fn(async (method: string) => {
5481+if (method === "thread/start") {
5482+return threadStartResult("thread-1");
5483+}
5484+if (method === "turn/start") {
5485+return turnStartResult("turn-1", "inProgress");
5486+}
5487+return {};
5488+});
5489+setCodexAppServerClientFactoryForTest(
5490+async () =>
5491+({
5492+ request,
5493+addNotificationHandler: (handler: typeof notify) => {
5494+notify = handler;
5495+return () => undefined;
5496+},
5497+addRequestHandler: (
5498+handler: (request: {
5499+id: string;
5500+method: string;
5501+params?: unknown;
5502+}) => Promise<unknown>,
5503+) => {
5504+handleRequest = handler;
5505+return () => undefined;
5506+},
5507+}) as never,
5508+);
5509+const params = createParams(
5510+path.join(tempDir, "session.jsonl"),
5511+path.join(tempDir, "workspace"),
5512+);
5513+params.timeoutMs = 60_000;
5514+5515+const run = runCodexAppServerAttempt(params, {
5516+turnCompletionIdleTimeoutMs: 5,
5517+turnAssistantCompletionIdleTimeoutMs: 500,
5518+turnTerminalIdleTimeoutMs: 500,
5519+});
5520+await vi.waitFor(() => expect(handleRequest).toBeTypeOf("function"), fastWait);
5521+5522+const toolResult = (await handleRequest?.({
5523+id: "request-tool-1",
5524+method: "item/tool/call",
5525+params: {
5526+threadId: "thread-1",
5527+turnId: "turn-1",
5528+callId: "call-1",
5529+namespace: null,
5530+tool: "message",
5531+arguments: { action: "send", text: "already sent" },
5532+},
5533+})) as { success?: boolean };
5534+expect(toolResult.success).toBe(false);
5535+// Send a rawResponseItem/completed with type "reasoning" — this does NOT
5536+// qualify as postToolRawAssistantCompletionNeedsTerminalGuard (which
5537+// requires type=message + role=assistant + text preview). Before the fix,
5538+// this would disarm the completion idle watch via the catch-all disarm
5539+// block, leaving only the 30-minute terminal timeout. After the fix,
5540+// rawResponseItemCompletedWithNoActiveItems keeps the 60s (here 5ms)
5541+// completion idle watch armed.
5542+await notify({
5543+method: "rawResponseItem/completed",
5544+params: {
5545+threadId: "thread-1",
5546+turnId: "turn-1",
5547+item: {
5548+type: "reasoning",
5549+id: "raw-reasoning-1",
5550+},
5551+},
5552+});
5553+5554+const result = await run;
5555+expect(result.aborted).toBe(true);
5556+expect(result.timedOut).toBe(true);
5557+expect(result.promptError).toBe(
5558+"codex app-server turn idle timed out waiting for turn/completed",
5559+);
5560+const completionWarnCall = warn.mock.calls.find(
5561+([message]) => message === "codex app-server turn idle timed out waiting for completion",
5562+);
5563+expect(completionWarnCall).toBeDefined();
5564+const completionWarnData = completionWarnCall?.[1] as
5565+| { lastActivityReason?: string; timeoutMs?: number }
5566+| undefined;
5567+expect(completionWarnData?.timeoutMs).toBe(5);
5568+expect(completionWarnData?.lastActivityReason).toBe("notification:rawResponseItem/completed");
5569+// The terminal idle watch (500ms) should NOT have fired — the shorter
5570+// completion idle watch (5ms) should catch the stall first.
5571+expect(
5572+warn.mock.calls.some(
5573+([message]) =>
5574+message === "codex app-server turn idle timed out waiting for terminal event",
5575+),
5576+).toBe(false);
5577+});
5578+54745579it("releases the session when Codex accepts a turn but never sends progress", async () => {
54755580const harness = createStartedThreadHarness();
54765581const params = createParams(
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。