fix(v02): persist terminal trace before refresh (#861)
Co-authored-by: Codex Agent <codex@hwlab.local>
This commit is contained in:
@@ -121,11 +121,70 @@ test("subscribeToTrace keeps polling result when trace terminal lacks final resp
|
||||
}).catch(reject);
|
||||
});
|
||||
assert.equal(completed.assistantText, "最终回复");
|
||||
assert.equal(completed.runnerTrace?.events?.length, 2);
|
||||
assert.equal(resultPolls, 2);
|
||||
assert.deepEqual(fetches, [
|
||||
"/v1/agent/chat/result/trc_terminal_first",
|
||||
"/v1/agent/chat/trace/trc_terminal_first",
|
||||
"/v1/agent/chat/result/trc_terminal_first"
|
||||
"/v1/agent/chat/result/trc_terminal_first",
|
||||
"/v1/agent/chat/trace/trc_terminal_first"
|
||||
]);
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
if (globalThis.window) {
|
||||
globalThis.window.setTimeout = originalSetTimeout ?? globalThis.window.setTimeout;
|
||||
globalThis.window.clearTimeout = originalClearTimeout ?? globalThis.window.clearTimeout;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test("subscribeToTrace preserves trace when terminal result with text arrives before trace poll", async () => {
|
||||
const originalFetch = globalThis.fetch;
|
||||
const originalSetTimeout = globalThis.window?.setTimeout;
|
||||
const originalClearTimeout = globalThis.window?.clearTimeout;
|
||||
const fetches: string[] = [];
|
||||
const events: TraceEvent[] = [
|
||||
{ seq: 1, label: "agentrun:request:accepted", status: "accepted" },
|
||||
{ seq: 2, label: "agentrun:terminal:failed", type: "terminal", status: "failed", message: "provider failed" },
|
||||
{ seq: 3, label: "agentrun:result:failed", type: "result", status: "failed", message: "provider failed" }
|
||||
];
|
||||
globalThis.window = {
|
||||
...(globalThis.window ?? {}),
|
||||
setTimeout: ((callback: () => void) => { callback(); return 1; }) as typeof window.setTimeout,
|
||||
clearTimeout: (() => undefined) as typeof window.clearTimeout
|
||||
} as typeof window;
|
||||
globalThis.fetch = (async (input: RequestInfo | URL) => {
|
||||
const url = String(input);
|
||||
fetches.push(url);
|
||||
if (url.includes("/result/")) {
|
||||
return jsonResponse({ status: "failed", traceId: "trc_failed_fast", assistantText: "provider failed" });
|
||||
}
|
||||
if (url.includes("/trace/")) {
|
||||
return jsonResponse({ status: "failed", traceId: "trc_failed_fast", events, eventCount: events.length });
|
||||
}
|
||||
return jsonResponse({});
|
||||
}) as typeof fetch;
|
||||
|
||||
try {
|
||||
const completed = await new Promise<AgentChatResultResponse>((resolve, reject) => {
|
||||
void subscribeToTrace({
|
||||
traceId: "trc_failed_fast",
|
||||
initial: { status: "running", traceId: "trc_failed_fast", resultUrl: "/v1/agent/chat/result/trc_failed_fast" },
|
||||
onActivity: () => undefined,
|
||||
onSnapshot: () => undefined,
|
||||
onComplete: resolve,
|
||||
onInfrastructureError: (error) => reject(new Error(error)),
|
||||
signal: new AbortController().signal,
|
||||
inactivityTimeoutMs: 1000
|
||||
}).catch(reject);
|
||||
});
|
||||
assert.equal(completed.assistantText, "provider failed");
|
||||
assert.equal(completed.runnerTrace?.events?.length, 3);
|
||||
assert.equal(completed.runnerTrace?.eventCount, 3);
|
||||
assert.equal(completed.traceEvents?.length, 3);
|
||||
assert.deepEqual(fetches, [
|
||||
"/v1/agent/chat/result/trc_failed_fast",
|
||||
"/v1/agent/chat/trace/trc_failed_fast"
|
||||
]);
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
|
||||
@@ -162,7 +162,8 @@ function firstNonEmptyString(...values: unknown[]): string | null {
|
||||
* unresponsive for `inactivityTimeoutMs`). In normal operation the
|
||||
* 1.5s polling cadence keeps `lastActivityAt` fresh and the inactivity
|
||||
* abort never triggers.
|
||||
* - Terminal status -> `onComplete` and return.
|
||||
* - Terminal status -> poll trace once before `onComplete` so refresh keeps
|
||||
* a replayable trace panel even when result text arrives first.
|
||||
* - 5xx -> keep polling, do NOT call `onActivity` (the inactivity
|
||||
* window should detect the real backend outage).
|
||||
* - 4xx (non-5xx) -> `onInfrastructureError` and return.
|
||||
@@ -197,6 +198,7 @@ export async function subscribeToTrace(config: TraceSubscriptionConfig): Promise
|
||||
let lastEventCount = -1;
|
||||
let lastStatus: string | null = null;
|
||||
let lastUpdatedAt: string | null = null;
|
||||
let terminalResultWithText: AgentChatResultResponse | null = null;
|
||||
let terminalResultWithoutText: AgentChatResultResponse | null = null;
|
||||
|
||||
for (;;) {
|
||||
@@ -216,10 +218,10 @@ export async function subscribeToTrace(config: TraceSubscriptionConfig): Promise
|
||||
onActivity();
|
||||
if (isTerminalStatus(resultPolled.data.status)) {
|
||||
if (firstNonEmptyResultText(resultPolled.data)) {
|
||||
onComplete(resultPolled.data);
|
||||
return;
|
||||
terminalResultWithText = resultPolled.data;
|
||||
} else {
|
||||
terminalResultWithoutText = resultPolled.data;
|
||||
}
|
||||
terminalResultWithoutText = resultPolled.data;
|
||||
}
|
||||
} else if (!resultPolled.ok && resultPolled.status >= 500) {
|
||||
// server hiccup: keep polling, do NOT call onActivity — the
|
||||
@@ -260,6 +262,18 @@ export async function subscribeToTrace(config: TraceSubscriptionConfig): Promise
|
||||
};
|
||||
onSnapshot(snapshot);
|
||||
}
|
||||
if (terminalResultWithText) {
|
||||
const last = events.at(-1);
|
||||
const traceSnapshot = snapshotToTraceSnapshot({
|
||||
...tracePolled.data,
|
||||
traceId,
|
||||
events,
|
||||
eventCount,
|
||||
lastEventLabel: String(last?.label ?? last?.type ?? "")
|
||||
} as AgentChatResultResponse);
|
||||
onComplete(mergeTraceResults(terminalResultWithText, traceSnapshot));
|
||||
return;
|
||||
}
|
||||
if (isTerminalStatus(status)) {
|
||||
const last = events.at(-1);
|
||||
const traceTerminal = {
|
||||
@@ -277,8 +291,16 @@ export async function subscribeToTrace(config: TraceSubscriptionConfig): Promise
|
||||
}
|
||||
}
|
||||
} else if (!tracePolled.ok && tracePolled.status >= 500) {
|
||||
if (terminalResultWithText) {
|
||||
onComplete(terminalResultWithText);
|
||||
return;
|
||||
}
|
||||
// server hiccup: keep polling, no activity update.
|
||||
} else if (!tracePolled.ok) {
|
||||
if (terminalResultWithText) {
|
||||
onComplete(terminalResultWithText);
|
||||
return;
|
||||
}
|
||||
onInfrastructureError(tracePolled.error ?? "Code Agent trace poll failed (non-5xx)");
|
||||
return;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user