diff --git a/internal/cloud/access-control.ts b/internal/cloud/access-control.ts index 2158edca..a9909e94 100644 --- a/internal/cloud/access-control.ts +++ b/internal/cloud/access-control.ts @@ -1847,12 +1847,43 @@ function publicSession(session) { return { id: session.id, userId: session.userI function publicAccessTuple(tuple) { return tuple ? { userId: tuple.userId, relation: tuple.relation, object: tuple.object, createdByAdminId: tuple.createdByAdminId, createdAt: tuple.createdAt } : null; } function toolsFromTuples(tuples = []) { return Object.fromEntries(HWLAB_TOOL_IDS.map((toolId) => [toolId, tuples.some((tuple) => tuple.object === openFgaObject("tool", toolId) && tuple.relation === "can_use")])); } function defaultDeniedToolCapabilities() { return { contractVersion: "admin-access-v1", actor: null, tools: Object.fromEntries(HWLAB_TOOL_IDS.map((toolId) => [toolId, { allowed: false, authorization: { contractVersion: "openfga-authorization-v1", mode: "enforce", allowed: false, decisionSource: "missing-owner", degraded: false } }])), valuesRedacted: true }; } -function normalizeAgentSessionOwnerRecord(input, existing, now) { return { id: input.sessionId, projectId: input.projectId ?? existing?.projectId ?? "prj_v02_code_agent", agentId: input.agentId ?? existing?.agentId ?? "hwlab-code-agent", status: input.status ?? existing?.status ?? "active", startedAt: existing?.startedAt ?? input.startedAt ?? now, endedAt: input.endedAt ?? existing?.endedAt ?? null, ownerUserId: input.ownerUserId, conversationId: input.conversationId ?? existing?.conversationId ?? null, threadId: input.threadId ?? existing?.threadId ?? null, lastTraceId: input.traceId ?? input.lastTraceId ?? existing?.lastTraceId ?? null, session: mergeAgentSessionOwnerEvidence(input.session, existing?.session), updatedAt: now }; } -function mergeAgentSessionOwnerEvidence(nextValue, existingValue) { +function normalizeAgentSessionOwnerRecord(input, existing, now) { + const nextTraceId = safeTraceIdLocal(input.traceId ?? input.lastTraceId); + const existingLastTraceId = safeTraceIdLocal(existing?.lastTraceId); + const historicalTraceUpdate = Boolean(nextTraceId && existingLastTraceId && nextTraceId !== existingLastTraceId && isHistoricalTraceBackfill(input.session, nextTraceId)); + return { + id: input.sessionId, + projectId: input.projectId ?? existing?.projectId ?? "prj_v02_code_agent", + agentId: input.agentId ?? existing?.agentId ?? "hwlab-code-agent", + status: input.status ?? existing?.status ?? "active", + startedAt: existing?.startedAt ?? input.startedAt ?? now, + endedAt: input.endedAt ?? existing?.endedAt ?? null, + ownerUserId: input.ownerUserId, + conversationId: input.conversationId ?? existing?.conversationId ?? null, + threadId: input.threadId ?? existing?.threadId ?? null, + lastTraceId: historicalTraceUpdate ? existingLastTraceId : nextTraceId || existingLastTraceId || null, + session: mergeAgentSessionOwnerEvidence(input.session, existing?.session, { historicalTraceUpdate }), + updatedAt: now + }; +} +function isHistoricalTraceBackfill(sessionValue, traceId) { + const snapshot = normalizeObject(sessionValue); + if (snapshot.historicalTraceBackfill === true || snapshot.source === "agent-session-trace-repair") return true; + if (Array.isArray(snapshot.messages) && snapshot.messages.length > 0) return false; + if (agentSessionTraceEvidence({ session: snapshot }, traceId)) return true; + return Boolean(snapshot.finalResponse || snapshot.traceSummary || snapshot.agentRun); +} +function mergeAgentSessionOwnerEvidence(nextValue, existingValue, options = {}) { const existing = normalizeObject(existingValue); const next = normalizeObject(nextValue); const merged = { ...existing, ...next }; - merged.traceResults = mergeAgentSessionTraceResults(existing.traceResults, next.traceResults ?? next.traceResult ?? next); + merged.traceResults = mergeAgentSessionTraceResults(mergeAgentSessionTraceResults(existing.traceResults, existing), next.traceResults ?? next.traceResult ?? next); + if (options.historicalTraceUpdate === true) { + for (const field of ["agentRun", "finalResponse", "traceSummary", "traceId", "sessionStatus", "sessionLifecycleStatus"]) { + if (existing[field] !== undefined) merged[field] = existing[field]; + else delete merged[field]; + } + } const existingMessages = Array.isArray(existing.messages) ? existing.messages : null; const nextMessages = Array.isArray(next.messages) ? next.messages : null; if (nextMessages?.length) { diff --git a/internal/cloud/code-agent-agentrun-adapter.ts b/internal/cloud/code-agent-agentrun-adapter.ts index 70afbb7c..ab87a8ad 100644 --- a/internal/cloud/code-agent-agentrun-adapter.ts +++ b/internal/cloud/code-agent-agentrun-adapter.ts @@ -613,7 +613,9 @@ export async function loadPersistedAgentRunResult(traceId, options = {}) { 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); - const mayUseTopLevelAgentRun = latestTraceId === safeId || agentRunTraceId === safeId || (!latestTraceId && !agentRunTraceId); + const topLevelFinalTraceId = safeTraceId(session?.session?.finalResponse?.traceId ?? session?.session?.traceSummary?.traceId); + const topLevelConflictsTrace = (agentRunTraceId && agentRunTraceId !== safeId) || (topLevelFinalTraceId && topLevelFinalTraceId !== safeId); + const mayUseTopLevelAgentRun = !topLevelConflictsTrace && (latestTraceId === safeId || agentRunTraceId === safeId || topLevelFinalTraceId === safeId || (!latestTraceId && !agentRunTraceId && !topLevelFinalTraceId)); const agentRun = traceEvidence?.agentRun ?? (mayUseTopLevelAgentRun ? topLevelAgentRun : null); const restored = persistedAgentRunResultFromSession({ traceId: safeId, session, agentRun, traceEvidence, options }); if (restored) return restored; diff --git a/internal/cloud/server-agent-chat.test.ts b/internal/cloud/server-agent-chat.test.ts index 60d6dd09..f876f1bb 100644 --- a/internal/cloud/server-agent-chat.test.ts +++ b/internal/cloud/server-agent-chat.test.ts @@ -1880,6 +1880,23 @@ test("cloud api repairs historical same-session AgentRun trace after lastTraceId assert.ok(repair); assert.equal(repair.session.traceResults[firstTraceId].agentRun.commandId, firstCommandId); assert.equal(ownerSessions.get("ses_issue955_historical").session.traceResults[firstTraceId].finalResponse.text, firstFinalText); + const repairedSession = ownerSessions.get("ses_issue955_historical"); + assert.equal(repairedSession.lastTraceId, secondTraceId); + assert.equal(repairedSession.session.agentRun.commandId, secondCommandId); + assert.equal(repairedSession.session.finalResponse.text, secondFinalText); + + const secondResultResponse = await fetch(`http://127.0.0.1:${port}/v1/agent/chat/result/${secondTraceId}`, { + headers: { cookie: "hwlab_session=test-stub-session" } + }); + assert.equal(secondResultResponse.status, 200); + const secondResult = await secondResultResponse.json(); + const secondText = JSON.stringify(secondResult); + assert.equal(secondResult.traceId, secondTraceId); + assert.equal(secondResult.status, "completed"); + assert.equal(secondResult.agentRun.commandId, secondCommandId); + assert.match(secondText, /编译成功/u); + assert.doesNotMatch(secondText, /目前只有一个 HWPOD 可用/u); + assert.ok(calls.some((call) => call.path === `/api/v1/runs/${runId}/commands/${secondCommandId}/result`)); } finally { await new Promise((resolve, reject) => { server.close((error) => (error ? reject(error) : resolve()));