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);
|
}).catch(reject);
|
||||||
});
|
});
|
||||||
assert.equal(completed.assistantText, "最终回复");
|
assert.equal(completed.assistantText, "最终回复");
|
||||||
|
assert.equal(completed.runnerTrace?.events?.length, 2);
|
||||||
assert.equal(resultPolls, 2);
|
assert.equal(resultPolls, 2);
|
||||||
assert.deepEqual(fetches, [
|
assert.deepEqual(fetches, [
|
||||||
"/v1/agent/chat/result/trc_terminal_first",
|
"/v1/agent/chat/result/trc_terminal_first",
|
||||||
"/v1/agent/chat/trace/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 {
|
} finally {
|
||||||
globalThis.fetch = originalFetch;
|
globalThis.fetch = originalFetch;
|
||||||
|
|||||||
@@ -162,7 +162,8 @@ function firstNonEmptyString(...values: unknown[]): string | null {
|
|||||||
* unresponsive for `inactivityTimeoutMs`). In normal operation the
|
* unresponsive for `inactivityTimeoutMs`). In normal operation the
|
||||||
* 1.5s polling cadence keeps `lastActivityAt` fresh and the inactivity
|
* 1.5s polling cadence keeps `lastActivityAt` fresh and the inactivity
|
||||||
* abort never triggers.
|
* 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
|
* - 5xx -> keep polling, do NOT call `onActivity` (the inactivity
|
||||||
* window should detect the real backend outage).
|
* window should detect the real backend outage).
|
||||||
* - 4xx (non-5xx) -> `onInfrastructureError` and return.
|
* - 4xx (non-5xx) -> `onInfrastructureError` and return.
|
||||||
@@ -197,6 +198,7 @@ export async function subscribeToTrace(config: TraceSubscriptionConfig): Promise
|
|||||||
let lastEventCount = -1;
|
let lastEventCount = -1;
|
||||||
let lastStatus: string | null = null;
|
let lastStatus: string | null = null;
|
||||||
let lastUpdatedAt: string | null = null;
|
let lastUpdatedAt: string | null = null;
|
||||||
|
let terminalResultWithText: AgentChatResultResponse | null = null;
|
||||||
let terminalResultWithoutText: AgentChatResultResponse | null = null;
|
let terminalResultWithoutText: AgentChatResultResponse | null = null;
|
||||||
|
|
||||||
for (;;) {
|
for (;;) {
|
||||||
@@ -216,10 +218,10 @@ export async function subscribeToTrace(config: TraceSubscriptionConfig): Promise
|
|||||||
onActivity();
|
onActivity();
|
||||||
if (isTerminalStatus(resultPolled.data.status)) {
|
if (isTerminalStatus(resultPolled.data.status)) {
|
||||||
if (firstNonEmptyResultText(resultPolled.data)) {
|
if (firstNonEmptyResultText(resultPolled.data)) {
|
||||||
onComplete(resultPolled.data);
|
terminalResultWithText = resultPolled.data;
|
||||||
return;
|
} else {
|
||||||
|
terminalResultWithoutText = resultPolled.data;
|
||||||
}
|
}
|
||||||
terminalResultWithoutText = resultPolled.data;
|
|
||||||
}
|
}
|
||||||
} else if (!resultPolled.ok && resultPolled.status >= 500) {
|
} else if (!resultPolled.ok && resultPolled.status >= 500) {
|
||||||
// server hiccup: keep polling, do NOT call onActivity — the
|
// server hiccup: keep polling, do NOT call onActivity — the
|
||||||
@@ -260,6 +262,18 @@ export async function subscribeToTrace(config: TraceSubscriptionConfig): Promise
|
|||||||
};
|
};
|
||||||
onSnapshot(snapshot);
|
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)) {
|
if (isTerminalStatus(status)) {
|
||||||
const last = events.at(-1);
|
const last = events.at(-1);
|
||||||
const traceTerminal = {
|
const traceTerminal = {
|
||||||
@@ -277,8 +291,16 @@ export async function subscribeToTrace(config: TraceSubscriptionConfig): Promise
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (!tracePolled.ok && tracePolled.status >= 500) {
|
} else if (!tracePolled.ok && tracePolled.status >= 500) {
|
||||||
|
if (terminalResultWithText) {
|
||||||
|
onComplete(terminalResultWithText);
|
||||||
|
return;
|
||||||
|
}
|
||||||
// server hiccup: keep polling, no activity update.
|
// server hiccup: keep polling, no activity update.
|
||||||
} else if (!tracePolled.ok) {
|
} else if (!tracePolled.ok) {
|
||||||
|
if (terminalResultWithText) {
|
||||||
|
onComplete(terminalResultWithText);
|
||||||
|
return;
|
||||||
|
}
|
||||||
onInfrastructureError(tracePolled.error ?? "Code Agent trace poll failed (non-5xx)");
|
onInfrastructureError(tracePolled.error ?? "Code Agent trace poll failed (non-5xx)");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user