fix: recover AgentRun terminal projection
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* SPEC: PJ2026-0104010803 Workbench唯一投影 draft-2026-06-19-p0-projector-resume; draft-2026-06-20-p1-zero-split-durable-realtime; draft-2026-06-20-p3-read-side-zero; PJ2026-010403 API契约 draft-2026-06-18-r1; PJ2026-010401 Web工作台 draft-2026-06-18-r1; PJ2026-01060505 Workbench Performance draft-2026-06-17-p0
|
||||
* SPEC: PJ2026-0104010803 Workbench唯一投影 draft-2026-06-20-p2-terminal-outbox-recovery; PJ2026-010403 API契约 draft-2026-06-20-p2-terminal-outbox-recovery; PJ2026-010401 Web工作台 draft-2026-06-20-p2-terminal-outbox-recovery; PJ2026-01060505 Workbench Performance draft-2026-06-17-p0
|
||||
* 职责: Workbench REST read model。只读投影 session/message/turn/trace facts,不执行 AgentRun sync、billing finalize 或 workspace repair。
|
||||
*/
|
||||
import { createHash } from "node:crypto";
|
||||
@@ -22,7 +22,7 @@ const MAX_PAGE_LIMIT = 100;
|
||||
const DEFAULT_WORKBENCH_SSE_HEARTBEAT_MS = 15000;
|
||||
const WORKBENCH_SESSION_LIST_PAGE_FAMILIES = Object.freeze(["sessions"]);
|
||||
const WORKBENCH_SESSION_DETAIL_FAMILIES = Object.freeze(["sessions", "messages", "parts", "turns", "checkpoints"]);
|
||||
const WORKBENCH_TRACE_EVENT_METADATA_FAMILIES = Object.freeze(["sessions", "checkpoints"]);
|
||||
const WORKBENCH_TRACE_EVENT_METADATA_FAMILIES = Object.freeze(["sessions", "messages", "turns", "checkpoints"]);
|
||||
const WORKBENCH_TRACE_EVENT_PAGE_FAMILIES = Object.freeze(["traceEvents"]);
|
||||
export async function handleWorkbenchReadModelHttp(request, response, url, options = {}) {
|
||||
try {
|
||||
@@ -467,6 +467,26 @@ function canActorReadFactSession(session, actor) {
|
||||
return session.ownerUserId === actor.id;
|
||||
}
|
||||
|
||||
async function visibleFactSessionForTrace(readModel, facts, actor, traceId) {
|
||||
const visible = visibleFactSessions(facts, actor);
|
||||
const byLastTrace = visible.find((item) => item.lastTraceId === traceId) ?? null;
|
||||
if (byLastTrace) return { session: byLastTrace, facts };
|
||||
const relatedSessionIds = uniqueText([
|
||||
factTurnForTrace(facts, traceId)?.sessionId,
|
||||
factArray(facts.messages).find((message) => message.traceId === traceId)?.sessionId,
|
||||
factCheckpointForTrace(facts, traceId)?.sessionId
|
||||
]);
|
||||
const alreadyLoaded = visible.find((item) => relatedSessionIds.includes(factSessionId(item))) ?? null;
|
||||
if (alreadyLoaded) return { session: alreadyLoaded, facts };
|
||||
for (const sessionId of relatedSessionIds) {
|
||||
const result = await queryFactsByRouteId(readModel, sessionId, { families: ["sessions"], limit: 1 });
|
||||
if (result.error) return { error: result.error };
|
||||
const session = visibleFactSessions(result.facts, actor)[0] ?? null;
|
||||
if (session) return { session, facts: mergeFactSets(facts, result.facts) };
|
||||
}
|
||||
return { session: null, facts };
|
||||
}
|
||||
|
||||
function factSessionMatchesRouteId(session, routeId) {
|
||||
const sessionId = safeSessionId(routeId);
|
||||
if (sessionId && factSessionId(session) === sessionId) return true;
|
||||
@@ -1034,8 +1054,10 @@ async function handleWorkbenchTraceEventPage(response, url, options, actor, rawT
|
||||
const pageOptions = tracePageOptions(url);
|
||||
const metadata = await readModel.queryFacts({ traceId, families: WORKBENCH_TRACE_EVENT_METADATA_FAMILIES, limit: 1 });
|
||||
if (metadata.error) return sendJson(response, 503, workbenchProjectionStoreError(metadata.error));
|
||||
const metadataSessions = visibleFactSessions(metadata.facts, actor);
|
||||
const session = metadataSessions.find((item) => item.lastTraceId === traceId) ?? metadataSessions[0] ?? null;
|
||||
const resolved = await visibleFactSessionForTrace(readModel, metadata.facts, actor, traceId);
|
||||
if (resolved.error) return sendJson(response, 503, workbenchProjectionStoreError(resolved.error));
|
||||
const session = resolved.session;
|
||||
const metadataFacts = resolved.facts ?? metadata.facts;
|
||||
if (!session) {
|
||||
const context = await readModel.queryFacts({ traceId, families: ["sessions", "turns", "checkpoints"], limit: MAX_PAGE_LIMIT });
|
||||
if (context.error) return sendJson(response, 503, workbenchProjectionStoreError(context.error));
|
||||
@@ -1049,7 +1071,7 @@ async function handleWorkbenchTraceEventPage(response, url, options, actor, rawT
|
||||
}
|
||||
return sendJson(response, 404, workbenchError("workbench_trace_not_found", "Workbench trace is not visible to the current actor.", { traceId }));
|
||||
}
|
||||
const projection = factProjectionForTrace(metadata.facts, traceId);
|
||||
const projection = factProjectionForTrace(metadataFacts, traceId);
|
||||
const pageResult = await readModel.queryFacts({ traceId, families: WORKBENCH_TRACE_EVENT_PAGE_FAMILIES, afterProjectedSeq: pageOptions.afterProjectedSeq, limit: pageOptions.limit + 1 });
|
||||
if (pageResult.error) return sendJson(response, 503, workbenchProjectionStoreError(pageResult.error));
|
||||
const page = traceEventPageFromFacts(pageResult.facts.traceEvents, pageOptions, { total: projection.lastProjectedSeq, traceStatus: session.status });
|
||||
|
||||
Reference in New Issue
Block a user