fix: keep code agent turn reads projection-only (#1594)
This commit is contained in:
@@ -2308,7 +2308,7 @@ test("cloud api running trace reports current projection without AgentRun event
|
||||
}
|
||||
});
|
||||
|
||||
test("cloud api turn status backfills terminal AgentRun projection on read (#1555)", async () => {
|
||||
test("cloud api turn status does not backfill terminal AgentRun projection on GET (#1585)", async () => {
|
||||
const calls = [];
|
||||
const traceId = "trc_issue1422_turn_status_fast_path";
|
||||
const runId = "run_issue1422_turn_status_fast_path";
|
||||
@@ -2383,14 +2383,13 @@ test("cloud api turn status backfills terminal AgentRun projection on read (#155
|
||||
});
|
||||
assert.equal(response.status, 200);
|
||||
const body = await response.json();
|
||||
assert.equal(body.status, "completed");
|
||||
assert.equal(body.terminal, true);
|
||||
assert.equal(body.agentRun.runStatus, "claimed");
|
||||
assert.equal(body.agentRun.commandState, "completed");
|
||||
assert.equal(body.agentRun.terminalStatus, "completed");
|
||||
assert.match(body.finalResponse.text, /issue 1555 read-side backfill/u);
|
||||
assert.equal(calls.filter((call) => call.path === `/api/v1/runs/${runId}/events`).length, 1);
|
||||
assert.equal(calls.some((call) => call.path === `/api/v1/runs/${runId}/commands/${commandId}/result`), true);
|
||||
assert.equal(body.status, "running");
|
||||
assert.equal(body.terminal, false);
|
||||
assert.equal(body.projectionStatus, "projecting");
|
||||
assert.equal(body.agentRun.commandState, "running");
|
||||
assert.equal(body.agentRun.terminalStatus ?? null, null);
|
||||
assert.equal(body.finalResponse, null);
|
||||
assert.deepEqual(calls, []);
|
||||
} finally {
|
||||
await new Promise((resolve, reject) => {
|
||||
server.close((error) => (error ? reject(error) : resolve()));
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
cancelAgentRunChatTurn,
|
||||
codeAgentAgentRunAdapterEnabled,
|
||||
initialAgentRunChatResult,
|
||||
agentRunTerminalResultRefreshSatisfied,
|
||||
loadPersistedAgentRunResult,
|
||||
steerAgentRunChatTurn,
|
||||
submitAgentRunChatTurn,
|
||||
@@ -1242,68 +1241,6 @@ function codeAgentTurnStatusRefreshOptions(options = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
async function refreshAgentRunProjectionForCompatRead({ traceId, result = null, trace = null, options = {} } = {}) {
|
||||
if (!safeTraceId(traceId) || !shouldRefreshAgentRunProjectionOnRead(result)) return { result, runnerTrace: trace, refreshError: null };
|
||||
const traceStore = options.traceStore ?? defaultCodeAgentTraceStore;
|
||||
const refreshOptions = codeAgentTurnStatusRefreshOptions(options);
|
||||
const performanceStore = options.backendPerformanceStore ?? options.backendPerformance;
|
||||
const startedAt = nowMs();
|
||||
performanceStore?.recordWorkbenchProjectorCandidate?.({ status: "seen", reason: "read_side_refresh" });
|
||||
try {
|
||||
const synced = await syncAgentRunChatResult({
|
||||
traceId,
|
||||
currentResult: result,
|
||||
options: refreshOptions,
|
||||
traceStore,
|
||||
forceResultSync: agentRunProjectionObservedTerminal(result, trace)
|
||||
});
|
||||
const payload = synced.result ?? result;
|
||||
if (payload && isTraceCommandTerminalStatus(payload.status)) {
|
||||
await recordCodeAgentTerminalTurnStatusEffects({
|
||||
payload,
|
||||
params: codeAgentReadSideTerminalParams(payload, options),
|
||||
options: refreshOptions
|
||||
});
|
||||
}
|
||||
performanceStore?.recordWorkbenchProjectorBatch?.({ phase: "turn_finalize", status: "ok", durationMs: nowMs() - startedAt });
|
||||
return { result: payload, runnerTrace: synced.runnerTrace ?? traceStore.snapshot(traceId), refreshError: null };
|
||||
} catch (error) {
|
||||
performanceStore?.recordWorkbenchProjectorBatch?.({ phase: "turn_finalize", status: error?.code === "agentrun_timeout" ? "timeout" : "error", durationMs: nowMs() - startedAt });
|
||||
performanceStore?.recordWorkbenchProjectorError?.({ reason: error?.code ?? "agentrun_read_side_sync_failed" });
|
||||
traceStore.append(traceId, {
|
||||
type: "projection_refresh",
|
||||
status: "degraded",
|
||||
label: "agentrun:read_side_sync:failed",
|
||||
errorCode: error?.code ?? "agentrun_read_side_sync_failed",
|
||||
message: error?.message ?? "AgentRun read-side projection refresh failed",
|
||||
terminal: false,
|
||||
valuesPrinted: false
|
||||
});
|
||||
return { result, runnerTrace: traceStore.snapshot(traceId), refreshError: error };
|
||||
}
|
||||
}
|
||||
|
||||
function shouldRefreshAgentRunProjectionOnRead(result = {}) {
|
||||
const agentRun = result?.agentRun && typeof result.agentRun === "object" ? result.agentRun : null;
|
||||
if (!agentRun?.runId || !agentRun?.commandId) return false;
|
||||
if (isTraceCommandTerminalStatus(result?.status) && agentRunTerminalResultRefreshSatisfied(result, result?.traceId)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
function codeAgentReadSideTerminalParams(payload = {}, options = {}) {
|
||||
const session = payload.session && typeof payload.session === "object" ? payload.session : null;
|
||||
const sessionReuse = payload.sessionReuse && typeof payload.sessionReuse === "object" ? payload.sessionReuse : null;
|
||||
return {
|
||||
traceId: payload.traceId,
|
||||
conversationId: safeConversationId(payload.conversationId ?? session?.conversationId ?? sessionReuse?.conversationId) || null,
|
||||
sessionId: safeSessionId(payload.sessionId ?? session?.sessionId ?? sessionReuse?.sessionId) || null,
|
||||
threadId: safeOpaqueId(payload.threadId ?? session?.threadId ?? sessionReuse?.threadId) || null,
|
||||
projectId: payload.projectId ?? null,
|
||||
ownerUserId: payload.ownerUserId ?? options.actor?.id,
|
||||
ownerRole: payload.ownerRole ?? options.actor?.role
|
||||
};
|
||||
}
|
||||
|
||||
function codeAgentRequestScopedOptions(options = {}) {
|
||||
return {
|
||||
...options,
|
||||
@@ -1341,16 +1278,10 @@ async function readCodeAgentCompatProjection(traceId, options = {}) {
|
||||
if (result?.ownerUserId && !readModel.canReadOwner(result.ownerUserId)) return forbiddenTurnSnapshot(traceId);
|
||||
const projectionTrace = await readModel.traceSnapshot(traceId);
|
||||
let trace = traceSnapshotWithTerminalEvidence(projectionTrace, result, traceId, null);
|
||||
const refreshed = await refreshAgentRunProjectionForCompatRead({ traceId, result, trace, options });
|
||||
if (refreshed.runnerTrace) trace = traceSnapshotWithTerminalEvidence(refreshed.runnerTrace, result, traceId, refreshed.refreshError ?? null);
|
||||
if (refreshed.result && refreshed.result !== result) {
|
||||
result = refreshed.result;
|
||||
trace = traceSnapshotWithTerminalEvidence(refreshed.runnerTrace ?? trace, result, traceId, refreshed.refreshError ?? null);
|
||||
}
|
||||
const projection = readModel.projectionDiagnostics({ traceId, result, trace, refreshError: refreshed.refreshError ?? null });
|
||||
recordCodeAgentProjectionMetrics(options, { result, trace, projection, refreshError: refreshed.refreshError ?? null });
|
||||
const projection = readModel.projectionDiagnostics({ traceId, result, trace, refreshError: null });
|
||||
recordCodeAgentProjectionMetrics(options, { result, trace, projection, refreshError: null });
|
||||
const found = Boolean(result || session || (trace && trace.status !== "missing") || trace?.persisted === true);
|
||||
return { result, session, trace, projection, found, refreshError: refreshed.refreshError ?? null };
|
||||
return { result, session, trace, projection, found, refreshError: null };
|
||||
}
|
||||
|
||||
function projectedSessionTraceResult(session, traceId) {
|
||||
@@ -2736,6 +2667,8 @@ function traceSnapshotWithTerminalEvidence(snapshot, result, traceId, refreshErr
|
||||
ok: true,
|
||||
status: "expired",
|
||||
traceStatus: "expired",
|
||||
projectionStatus: "projecting",
|
||||
projectionHealth: "degraded",
|
||||
persisted: true,
|
||||
retention: traceRetentionSummary("expired"),
|
||||
conversationId: evidence.conversationId,
|
||||
|
||||
Reference in New Issue
Block a user