diff --git a/internal/cloud/access-control.ts b/internal/cloud/access-control.ts index a9909e94..89c276cb 100644 --- a/internal/cloud/access-control.ts +++ b/internal/cloud/access-control.ts @@ -2447,6 +2447,7 @@ function redactTraceFinalResponse(value) { const response = normalizeObject(value); if (Object.keys(response).length === 0) return undefined; return pruneEmpty({ + traceId: textOr(response.traceId, ""), role: textOr(response.role, ""), status: textOr(response.status, ""), messageId: textOr(response.messageId, ""), diff --git a/internal/cloud/code-agent-agentrun-adapter.ts b/internal/cloud/code-agent-agentrun-adapter.ts index ab87a8ad..e97901f7 100644 --- a/internal/cloud/code-agent-agentrun-adapter.ts +++ b/internal/cloud/code-agent-agentrun-adapter.ts @@ -609,7 +609,7 @@ export async function loadPersistedAgentRunResult(traceId, options = {}) { const safeId = safeTraceId(traceId); if (!safeId || typeof options.accessController?.getAgentSessionByTraceId !== "function") return null; const session = await options.accessController.getAgentSessionByTraceId(safeId); - const traceEvidence = agentSessionTraceEvidence(session, safeId); + const traceEvidence = validAgentSessionTraceEvidence(session, safeId); const latestTraceId = safeTraceId(session?.lastTraceId ?? session?.session?.lastTraceId ?? session?.session?.traceId); const topLevelAgentRun = session?.session?.agentRun && typeof session.session.agentRun === "object" ? session.session.agentRun : null; const agentRunTraceId = safeTraceId(topLevelAgentRun?.traceId); @@ -624,6 +624,7 @@ export async function loadPersistedAgentRunResult(traceId, options = {}) { function persistedAgentRunResultFromSession({ traceId, session, agentRun, traceEvidence, options = {} }) { if (!agentRun || typeof agentRun !== "object" || !agentRun.runId || !agentRun.commandId) return null; + if (!agentRunEvidenceMatchesTrace(agentRun, traceId, traceEvidence)) return null; const status = firstNonEmpty(traceEvidence?.status, session?.status === "canceled" ? "canceled" : null, agentRun.terminalStatus, agentRun.commandState, "running"); return { accepted: true, @@ -767,6 +768,34 @@ function agentSessionTraceEvidence(session, traceId) { return evidence ? { ...evidence, traceId: id } : null; } +function validAgentSessionTraceEvidence(session, traceId) { + const evidence = agentSessionTraceEvidence(session, traceId); + if (!evidence) return null; + return agentRunEvidenceMatchesTrace(evidence.agentRun, traceId, evidence, { requireAgentRunTraceId: true }) ? evidence : null; +} + +function agentRunEvidenceMatchesTrace(agentRun, traceId, traceEvidence = null, options = {}) { + const id = safeTraceId(traceId); + if (!id) return false; + const run = agentRun && typeof agentRun === "object" ? agentRun : null; + const runTraceId = safeTraceId(run?.traceId); + if (run?.commandId && runTraceId && runTraceId !== id) return false; + if (options.requireAgentRunTraceId === true && run?.commandId && runTraceId !== id) return false; + const providerTrace = run?.providerTrace && typeof run.providerTrace === "object" ? run.providerTrace : null; + const summary = traceEvidence?.traceSummary && typeof traceEvidence.traceSummary === "object" ? traceEvidence.traceSummary : null; + const summaryRun = summary?.agentRun && typeof summary.agentRun === "object" ? summary.agentRun : null; + const finalResponse = traceEvidence?.finalResponse && typeof traceEvidence.finalResponse === "object" ? traceEvidence.finalResponse : null; + const traceIds = [runTraceId, providerTrace?.traceId, summary?.traceId, finalResponse?.traceId] + .map((value) => safeTraceId(value)) + .filter(Boolean); + if (traceIds.some((value) => value !== id)) return false; + const commandIds = [run?.commandId, providerTrace?.commandId, summaryRun?.commandId] + .map((value) => text(value)) + .filter(Boolean); + if (commandIds.length > 1 && new Set(commandIds).size > 1) return false; + return true; +} + export function agentRunSessionEvidence(payload = {}) { if (!payload?.agentRun) return {}; return { @@ -785,6 +814,7 @@ export function agentRunSessionEvidence(payload = {}) { runStatus: payload.agentRun.runStatus ?? null, commandState: payload.agentRun.commandState ?? null, terminalStatus: payload.agentRun.terminalStatus ?? null, + traceId: payload.agentRun.traceId ?? payload.traceId ?? payload.providerTrace?.traceId ?? null, lastSeq: payload.agentRun.lastSeq ?? 0, providerId: payload.agentRun.providerId ?? DEFAULT_PROVIDER_ID, reuseEligible: payload.agentRun.reuseEligible ?? false, diff --git a/internal/cloud/server-agent-chat.test.ts b/internal/cloud/server-agent-chat.test.ts index f876f1bb..6697cbee 100644 --- a/internal/cloud/server-agent-chat.test.ts +++ b/internal/cloud/server-agent-chat.test.ts @@ -1822,6 +1822,19 @@ test("cloud api repairs historical same-session AgentRun trace after lastTraceId agentRun: { adapter: "agentrun-v01", managerUrl: `http://127.0.0.1:${agentRunPort}`, runId, commandId: secondCommandId, traceId: secondTraceId, backendProfile: "deepseek", terminalStatus: "completed", lastSeq: 67, valuesPrinted: false }, finalResponse: { text: secondFinalText, textChars: secondFinalText.length, role: "assistant", status: "completed", traceId: secondTraceId, valuesPrinted: false }, traceSummary: { traceId: secondTraceId, source: "agent-session-snapshot", sourceEventCount: 31, terminalStatus: "completed", agentRun: { runId, commandId: secondCommandId, lastSeq: 67, valuesPrinted: false }, valuesPrinted: false }, + traceResults: { + [secondTraceId]: { + traceId: secondTraceId, + status: "completed", + conversationId: "cnv_issue955_historical", + sessionId: "ses_issue955_historical", + threadId: "thread-issue955-historical", + finalResponse: { text: firstFinalText, textChars: firstFinalText.length, role: "assistant", status: "completed", traceId: secondTraceId, valuesPrinted: false }, + traceSummary: { traceId: secondTraceId, source: "agent-session-trace-repair", sourceEventCount: 4, terminalStatus: "completed", agentRun: { runId, commandId: firstCommandId, lastSeq: 35, valuesPrinted: false }, valuesPrinted: false }, + agentRun: { adapter: "agentrun-v01", managerUrl: `http://127.0.0.1:${agentRunPort}`, runId, commandId: firstCommandId, backendProfile: "deepseek", terminalStatus: "completed", lastSeq: 35, valuesPrinted: false }, + valuesRedacted: true + } + }, valuesRedacted: true } })); @@ -1894,9 +1907,13 @@ test("cloud api repairs historical same-session AgentRun trace after lastTraceId assert.equal(secondResult.traceId, secondTraceId); assert.equal(secondResult.status, "completed"); assert.equal(secondResult.agentRun.commandId, secondCommandId); + assert.equal(secondResult.agentRun.traceId, secondTraceId); + assert.equal(secondResult.finalResponse.traceId, secondTraceId); assert.match(secondText, /编译成功/u); assert.doesNotMatch(secondText, /目前只有一个 HWPOD 可用/u); assert.ok(calls.some((call) => call.path === `/api/v1/runs/${runId}/commands/${secondCommandId}/result`)); + assert.equal(ownerSessions.get("ses_issue955_historical").session.traceResults[secondTraceId].agentRun.commandId, secondCommandId); + assert.equal(ownerSessions.get("ses_issue955_historical").session.traceResults[secondTraceId].finalResponse.text, secondFinalText); } finally { await new Promise((resolve, reject) => { server.close((error) => (error ? reject(error) : resolve())); diff --git a/internal/cloud/server-code-agent-http.ts b/internal/cloud/server-code-agent-http.ts index dc6ffb2e..b711c09d 100644 --- a/internal/cloud/server-code-agent-http.ts +++ b/internal/cloud/server-code-agent-http.ts @@ -1526,6 +1526,7 @@ function codeAgentSessionOwnerEvidence(payload = {}, params = {}) { const traceSummary = codeAgentTraceSummaryEvidence(payload, traceId, finalResponse); const messages = codeAgentConversationMessagesEvidence(payload, params, traceId, finalResponse); const firstUserMessagePreview = messages.find((message) => message.role === "user")?.text?.slice(0, 240) ?? null; + const traceResult = codeAgentTraceResultEvidence(payload, params, traceId, finalResponse, traceSummary); return { provider: payload.provider ?? null, model: payload.model ?? null, @@ -1541,6 +1542,7 @@ function codeAgentSessionOwnerEvidence(payload = {}, params = {}) { projectId: payload.projectId ?? params.projectId ?? null, finalResponse, traceSummary, + ...(traceResult ? { traceResults: { [traceResult.traceId]: traceResult } } : {}), ...(messages.length ? { messages, messageCount: messages.length } : {}), ...(firstUserMessagePreview ? { firstUserMessagePreview } : {}), ...agentRunSessionEvidence(payload), @@ -1549,6 +1551,33 @@ function codeAgentSessionOwnerEvidence(payload = {}, params = {}) { }; } +function codeAgentTraceResultEvidence(payload = {}, params = {}, traceId = null, finalResponse = null, traceSummary = null) { + const resolvedTraceId = safeTraceId(traceId); + if (!resolvedTraceId) return null; + const agentRun = agentRunSessionEvidence(payload).agentRun ?? null; + const resultSession = payload.session && typeof payload.session === "object" ? payload.session : null; + const sessionReuse = payload.sessionReuse && typeof payload.sessionReuse === "object" ? payload.sessionReuse : null; + const conversationId = safeConversationId(payload.conversationId ?? agentRun?.conversationId ?? resultSession?.conversationId ?? sessionReuse?.conversationId ?? params.conversationId) || null; + const sessionId = safeSessionId(payload.sessionId ?? resultSession?.sessionId ?? sessionReuse?.sessionId ?? params.sessionId) || null; + const threadId = safeOpaqueId(payload.threadId ?? resultSession?.threadId ?? sessionReuse?.threadId ?? params.threadId ?? agentRun?.threadId) || null; + if (!agentRun && !finalResponse && !traceSummary) return null; + return { + traceId: resolvedTraceId, + status: payload.status ?? agentRun?.terminalStatus ?? agentRun?.commandState ?? null, + conversationId, + sessionId, + threadId, + messageId: finalResponse?.messageId ?? payload.messageId ?? null, + createdAt: payload.createdAt ?? finalResponse?.createdAt ?? null, + updatedAt: payload.updatedAt ?? finalResponse?.updatedAt ?? null, + finalResponse, + traceSummary, + agentRun, + valuesRedacted: true, + secretMaterialStored: false + }; +} + function codeAgentConversationMessagesEvidence(payload = {}, params = {}, traceId = null, finalResponse = null) { const userText = boundedConversationMessageText(params.message ?? params.prompt ?? payload.userMessage ?? payload.prompt); if (!userText) return [];