107 lines
5.7 KiB
TypeScript
107 lines
5.7 KiB
TypeScript
/*
|
|
* SPEC: PJ2026-0104010803 Workbench唯一投影 draft-2026-06-18-p0-unique-projection; PJ2026-010205 HWLAB接入 draft-2026-06-17-r0.
|
|
* 职责: WorkbenchProjectionFinalizer 组件入口。以 checkpoint/轮询驱动 AgentRun facts 追平,不让 GET path 承担 finalize/repair。
|
|
*/
|
|
import { appendProjectionDiagnostic } from "./workbench-projection-writer.ts";
|
|
|
|
export function scheduleWorkbenchProjectionFinalizer({ traceId, currentResult = null, traceStore, getCachedResult, isCanceled, isTerminal, syncResult, onTerminal, activitySignature, noResponseTimeoutMs = null, pollIntervalMs = 1000, sleep = defaultSleep, performanceStore = null } = {}) {
|
|
if (!traceId || !currentResult?.agentRun?.runId || !currentResult?.agentRun?.commandId || typeof syncResult !== "function") return null;
|
|
let lastActivityAt = Date.now();
|
|
let lastActivitySignature = activitySignature?.(currentResult, traceStore?.snapshot?.(traceId)) ?? null;
|
|
let idleDegraded = false;
|
|
performanceStore?.recordWorkbenchProjectorCandidate?.({ status: "scheduled", reason: "finalizer" });
|
|
|
|
setImmediate(() => {
|
|
void (async () => {
|
|
let result = currentResult;
|
|
let lastError = null;
|
|
let nextDelayMs = pollIntervalMs;
|
|
while (true) {
|
|
const cached = getCachedResult?.(traceId);
|
|
if (isCanceled?.(cached)) return;
|
|
if (cached && isTerminal?.(cached, traceStore?.snapshot?.(traceId))) {
|
|
performanceStore?.recordWorkbenchProjectorCandidate?.({ status: "completed", reason: "cached_terminal" });
|
|
onTerminal?.(cached, { preserveLastTraceId: true });
|
|
return;
|
|
}
|
|
const syncStartedAt = Date.now();
|
|
try {
|
|
const synced = await syncResult({ result });
|
|
performanceStore?.recordWorkbenchProjectorBatch?.({ phase: "candidate_scan", status: "ok", durationMs: Date.now() - syncStartedAt });
|
|
result = synced?.result ?? result;
|
|
nextDelayMs = projectionFinalizerDelayMs(synced?.polling, pollIntervalMs);
|
|
const runnerTrace = synced?.runnerTrace ?? traceStore?.snapshot?.(traceId);
|
|
if (result && isTerminal?.(result, runnerTrace)) {
|
|
performanceStore?.recordWorkbenchProjectorCandidate?.({ status: "completed", reason: "synced_terminal" });
|
|
onTerminal?.(result, { preserveLastTraceId: true });
|
|
return;
|
|
}
|
|
const signature = activitySignature?.(result, runnerTrace);
|
|
if (signature && signature !== lastActivitySignature) {
|
|
lastActivitySignature = signature;
|
|
lastActivityAt = Date.now();
|
|
idleDegraded = false;
|
|
}
|
|
lastError = null;
|
|
} catch (error) {
|
|
performanceStore?.recordWorkbenchProjectorBatch?.({ phase: "candidate_scan", status: error?.code === "agentrun_timeout" ? "timeout" : "error", durationMs: Date.now() - syncStartedAt });
|
|
performanceStore?.recordWorkbenchProjectorError?.({ reason: error?.code ?? "finalizer_sync_failed" });
|
|
nextDelayMs = projectionFinalizerDelayMs(error?.polling, pollIntervalMs);
|
|
lastError = error;
|
|
}
|
|
const idleMs = Date.now() - lastActivityAt;
|
|
if (noResponseTimeoutMs && idleMs >= noResponseTimeoutMs && !idleDegraded) {
|
|
performanceStore?.recordWorkbenchProjectorCandidate?.({ status: "degraded", reason: "no_response_idle_timeout" });
|
|
performanceStore?.recordWorkbenchProjectorError?.({ reason: lastError?.code ?? "no_response_idle_timeout" });
|
|
appendProjectionDiagnostic(traceStore, traceId, {
|
|
type: "turn-status",
|
|
status: "degraded",
|
|
label: "projection-finalizer:no-response-idle-timeout",
|
|
errorCode: lastError?.code ?? "no_response_idle_timeout",
|
|
degradedReason: lastError ? "projection_activity_stale" : "no_response_idle_timeout",
|
|
message: lastError?.message ?? `AgentRun projection has no new activity for ${idleMs}ms; idle threshold ${noResponseTimeoutMs}ms.`,
|
|
runId: result?.agentRun?.runId ?? currentResult.agentRun.runId,
|
|
commandId: result?.agentRun?.commandId ?? currentResult.agentRun.commandId,
|
|
timeoutMs: noResponseTimeoutMs,
|
|
idleMs,
|
|
lastActivityAt: new Date(lastActivityAt).toISOString(),
|
|
waitingFor: "agentrun-result-activity",
|
|
terminal: false
|
|
});
|
|
idleDegraded = true;
|
|
}
|
|
await sleep(nextDelayMs);
|
|
}
|
|
})().catch((error) => {
|
|
performanceStore?.recordWorkbenchProjectorError?.({ reason: error?.code ?? "workbench_projection_finalizer_crashed" });
|
|
appendProjectionDiagnostic(traceStore, traceId, {
|
|
type: "turn-status",
|
|
status: "degraded",
|
|
label: "projection-finalizer:sync-crashed",
|
|
errorCode: error?.code ?? "workbench_projection_finalizer_crashed",
|
|
message: error?.message ?? "Workbench projection finalizer crashed.",
|
|
runId: currentResult.agentRun.runId,
|
|
commandId: currentResult.agentRun.commandId,
|
|
waitingFor: "agentrun-result",
|
|
terminal: false
|
|
});
|
|
});
|
|
});
|
|
return { scheduled: true, traceId, noResponseTimeoutMs, pollIntervalMs, valuesPrinted: false };
|
|
}
|
|
|
|
function defaultSleep(ms) {
|
|
return new Promise((resolve) => setTimeout(resolve, Math.max(0, Number(ms) || 0)));
|
|
}
|
|
|
|
function projectionFinalizerDelayMs(polling = null, fallbackMs = 1000) {
|
|
const fallback = positiveInteger(fallbackMs, 1000);
|
|
const backoffMs = positiveInteger(polling?.backoffMs, 0);
|
|
return backoffMs > 0 ? Math.max(fallback, backoffMs) : fallback;
|
|
}
|
|
|
|
function positiveInteger(value, fallback) {
|
|
const parsed = Number.parseInt(String(value ?? ""), 10);
|
|
return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
|
|
}
|