fix: persist workbench projection cursor

This commit is contained in:
lyon
2026-06-19 21:20:11 +08:00
parent 00852f6f95
commit 3f83852f60
10 changed files with 805 additions and 58 deletions
+76 -8
View File
@@ -1,5 +1,5 @@
// SPEC: PJ2026-01060505 Workbench Performance draft-2026-06-17-p0.
// Responsibility: AgentRun v0.1 adapter and upstream timing projection for Cloud API observability.
// SPEC: PJ2026-0104010803 Workbench唯一投影 draft-2026-06-19-p1-agentrun-incremental-cursor; PJ2026-010205 HWLAB接入 draft-2026-06-17-r0; PJ2026-01060505 Workbench Performance draft-2026-06-17-p0.
// Responsibility: AgentRun v0.1 adapter, incremental Workbench projection cursor sync, and upstream timing projection for Cloud API observability.
import { createHash, randomUUID } from "node:crypto";
@@ -15,6 +15,7 @@ import {
truthyFlag
} from "./server-http-utils.ts";
import { backendPerformanceRouteTemplate, currentBackendPerformanceContext } from "./backend-performance.ts";
import { agentRunResultSyncDeferred, buildAgentRunProjectionEventsFetchPlan, buildWorkbenchProjectionStateUpdate } from "./workbench-projection-cursor.ts";
const ADAPTER_ID = "agentrun-v01";
const DEFAULT_AGENTRUN_MGR_URL = "http://agentrun-mgr.agentrun-v01.svc.cluster.local:8080";
@@ -451,12 +452,29 @@ export async function syncAgentRunChatResult({ traceId, currentResult = null, op
const fetchImpl = options.fetchImpl ?? globalThis.fetch;
const managerUrl = resolveAgentRunManagerUrl(env, mapped.agentRun.managerUrl);
const timeoutMs = parsePositiveInteger(env.HWLAB_CODE_AGENT_AGENTRUN_HTTP_TIMEOUT_MS, 20_000);
const previousLastSeq = Number(mapped.agentRun.lastSeq ?? 0);
const eventsResponse = refreshEvents ? await measuredAgentRunEventsFetch({ performanceStore, fetchImpl, managerUrl, timeoutMs, env, mapping: { ...mapped.agentRun, traceSummary: mapped.traceSummary } }) : null;
const projectionState = await loadWorkbenchProjectionStateForAgentRun(options.runtimeStore, { traceId, agentRun: mapped.agentRun });
const fetchPlan = buildAgentRunProjectionEventsFetchPlan({
projectionState,
agentRun: mapped.agentRun,
pageLimit: env.HWLAB_CODE_AGENT_AGENTRUN_EVENTS_PAGE_LIMIT
});
const previousLastSeq = Number(fetchPlan.afterSeq ?? mapped.agentRun.lastSeq ?? 0);
const eventsResponse = refreshEvents ? await measuredAgentRunEventsFetch({ performanceStore, fetchImpl, managerUrl, timeoutMs, env, mapping: { ...mapped.agentRun, lastSeq: fetchPlan.afterSeq, afterSeqOverride: fetchPlan.afterSeq, eventsPageLimit: fetchPlan.limit, traceSummary: mapped.traceSummary } }) : null;
if (eventsResponse) appendAgentRunEventsToTrace(traceStore, traceId, eventsResponse.events, mapped.agentRun);
const nextLastSeq = eventsResponse ? agentRunTraceCursorSeq(eventsResponse, mapped.agentRun.lastSeq) : mapped.agentRun.lastSeq;
recordAgentRunEventsProjectionMetrics(performanceStore, eventsResponse, previousLastSeq, nextLastSeq);
if (!forceResultSync && !agentRunCommandResultSyncRequired(mapped, eventsResponse)) {
const terminalFromEvents = agentRunTerminalStatusFromEvents(eventsResponse?.events);
await writeWorkbenchProjectionStateForAgentRun(options.runtimeStore, {
currentState: projectionState,
result: mapped,
agentRun: { ...mapped.agentRun, lastSeq: nextLastSeq },
eventsResponse,
resultSyncState: terminalFromEvents ? "pending" : null,
projectionStatus: terminalFromEvents ? "terminal" : null,
now: options.now
});
const deferResultSync = agentRunResultSyncDeferred({ forceResultSync, options, env });
if (!forceResultSync && (deferResultSync || !agentRunCommandResultSyncRequired(mapped, eventsResponse))) {
const nextMapping = withAgentRunRunnerJobCount({
...mapped.agentRun,
lastSeq: nextLastSeq,
@@ -464,6 +482,7 @@ export async function syncAgentRunChatResult({ traceId, currentResult = null, op
runStatus: mapped.agentRun.runStatus ?? null,
commandState: mapped.agentRun.commandState ?? null,
terminalStatus: null,
resultSyncState: terminalFromEvents ? "pending" : mapped.agentRun.resultSyncState ?? null,
updatedAt: nowIso(options.now),
valuesPrinted: false
});
@@ -471,7 +490,22 @@ export async function syncAgentRunChatResult({ traceId, currentResult = null, op
options.codeAgentChatResults?.set?.(traceId, payload);
return { result: payload, runnerTrace: payload.runnerTrace ?? traceStore.snapshot(traceId), found: true, eventsRefreshed: Boolean(eventsResponse), resultSynced: false };
}
const result = await measuredAgentRunResultFetch({ performanceStore, fetchImpl, managerUrl, timeoutMs, env, mapped, eventsResponse });
let result;
try {
result = await measuredAgentRunResultFetch({ performanceStore, fetchImpl, managerUrl, timeoutMs, env, mapped, eventsResponse });
} catch (error) {
await writeWorkbenchProjectionStateForAgentRun(options.runtimeStore, {
currentState: projectionState,
result: mapped,
agentRun: { ...mapped.agentRun, lastSeq: nextLastSeq },
eventsResponse,
resultSyncState: agentRunMetricStatusForError(error) === "timeout" ? "timed_out" : "failed",
projectionStatus: terminalFromEvents ? "terminal" : null,
error,
now: options.now
});
throw error;
}
const nextMapping = withAgentRunRunnerJobCount({
...mapped.agentRun,
...agentRunResultRefs(result),
@@ -486,9 +520,39 @@ export async function syncAgentRunChatResult({ traceId, currentResult = null, op
const base = { ...mapped, agentRun: nextMapping, updatedAt: nowIso(options.now) };
const payload = agentRunResultToCodeAgentPayload({ base, result, traceStore, traceId, appendResultEvent });
options.codeAgentChatResults?.set?.(traceId, payload);
await writeWorkbenchProjectionStateForAgentRun(options.runtimeStore, {
currentState: projectionState,
result: payload,
agentRun: nextMapping,
eventsResponse,
resultSyncState: "synced",
projectionStatus: isAgentRunTerminalStatus(payload?.status) ? "terminal" : null,
now: options.now
});
return { result: payload, runnerTrace: payload.runnerTrace ?? traceStore.snapshot(traceId), found: true, eventsRefreshed: Boolean(eventsResponse), resultSynced: true };
}
async function loadWorkbenchProjectionStateForAgentRun(runtimeStore, { traceId, agentRun = {} } = {}) {
if (!runtimeStore || typeof runtimeStore.getWorkbenchProjectionState !== "function") return null;
try {
const result = await runtimeStore.getWorkbenchProjectionState({ traceId });
const state = result?.projectionState ?? null;
if (!state) return null;
if (text(state.runId ?? state.sourceRunId) !== text(agentRun.runId)) return null;
if (text(state.commandId ?? state.sourceCommandId) !== text(agentRun.commandId)) return null;
return state;
} catch {
return null;
}
}
async function writeWorkbenchProjectionStateForAgentRun(runtimeStore, params = {}) {
if (!runtimeStore || typeof runtimeStore.writeWorkbenchProjectionState !== "function") return null;
const projectionState = buildWorkbenchProjectionStateUpdate(params);
if (!projectionState.traceId || !projectionState.runId || !projectionState.commandId) return null;
return await runtimeStore.writeWorkbenchProjectionState({ projectionState });
}
function agentRunCommandResultSyncRequired(mapped = {}, eventsResponse = null) {
const agentRun = mapped?.agentRun && typeof mapped.agentRun === "object" ? mapped.agentRun : {};
if (isAgentRunTerminalStatus(mapped?.status)) return true;
@@ -1875,8 +1939,12 @@ function appendAgentRunEventsToTrace(traceStore, traceId, events, mapping = {})
async function fetchAgentRunEventsForTrace({ fetchImpl, managerUrl, timeoutMs, env, mapping = {} }) {
const runId = requiredString(mapping.runId, "runId");
const currentCommandId = typeof mapping.commandId === "string" ? mapping.commandId : "";
const { afterSeq, endSeq } = agentRunTraceReplayWindow(mapping);
const path = `/api/v1/runs/${encodeURIComponent(runId)}/events?afterSeq=${encodeURIComponent(String(afterSeq))}&limit=500`;
const replayWindow = agentRunTraceReplayWindow(mapping);
const afterSeqOverride = Number(mapping.afterSeqOverride);
const afterSeq = Number.isFinite(afterSeqOverride) && afterSeqOverride >= 0 ? Math.floor(afterSeqOverride) : replayWindow.afterSeq;
const endSeq = Number.isFinite(afterSeqOverride) && afterSeqOverride >= 0 ? 0 : replayWindow.endSeq;
const limit = parsePositiveInteger(mapping.eventsPageLimit ?? env?.HWLAB_CODE_AGENT_AGENTRUN_EVENTS_PAGE_LIMIT, 500);
const path = `/api/v1/runs/${encodeURIComponent(runId)}/events?afterSeq=${encodeURIComponent(String(afterSeq))}&limit=${encodeURIComponent(String(limit))}`;
const response = await agentRunJson(fetchImpl, managerUrl, path, { method: "GET", timeoutMs, env });
const rawEvents = Array.isArray(response?.items) ? response.items : [];
const events = rawEvents.filter((event) => agentRunEventBelongsToTrace(event, { currentCommandId, afterSeq, endSeq }));