/* * SPEC: PJ2026-0104010803 Workbench唯一投影 draft-2026-06-19-p1-agentrun-incremental-cursor; PJ2026-010205 HWLAB接入 draft-2026-06-17-r0. * Responsibility: Pure cursor planning for AgentRun events projection; never derives cursor by scanning trace events or result payloads. */ const DEFAULT_AGENTRUN_EVENTS_PAGE_LIMIT = 500; const MAX_AGENTRUN_EVENTS_PAGE_LIMIT = 1000; export function buildAgentRunProjectionEventsFetchPlan({ projectionState = null, agentRun = null, pageLimit = DEFAULT_AGENTRUN_EVENTS_PAGE_LIMIT } = {}) { const state = normalizeProjectionState(projectionState); const mapping = normalizeAgentRunMapping(agentRun); const runId = textValue(state?.sourceRunId ?? state?.runId ?? mapping.runId); const commandId = textValue(state?.sourceCommandId ?? state?.commandId ?? mapping.commandId); const stateSeq = maxNonNegativeInteger(state?.lastSourceSeq, state?.lastAgentRunSeq); const mappingSeq = maxNonNegativeInteger(mapping.lastSeq); return { runId, commandId, afterSeq: Math.max(stateSeq, mappingSeq), limit: boundedPageLimit(pageLimit), cursorSource: state ? "projection-state" : "agent-run-result", valuesPrinted: false }; } export function buildWorkbenchProjectionStateUpdate({ currentState = null, result = null, agentRun = null, eventsResponse = null, resultSyncState = null, projectionStatus = null, projectionHealth = null, error = null, now = () => new Date().toISOString() } = {}) { const previous = normalizeProjectionState(currentState) ?? {}; const mapping = normalizeAgentRunMapping(agentRun ?? result?.agentRun); const timestamp = timestampValue(now); const traceId = textValue(previous.traceId ?? result?.traceId ?? mapping.traceId); const runId = textValue(previous.sourceRunId ?? previous.runId ?? mapping.runId); const commandId = textValue(previous.sourceCommandId ?? previous.commandId ?? mapping.commandId); const lastSourceSeq = Math.max( maxNonNegativeInteger(previous.lastSourceSeq, previous.lastAgentRunSeq), maxNonNegativeInteger(mapping.lastSeq), maxNonNegativeInteger(eventsResponse?.traceLastSeq) ); const sourceLatestSeq = Math.max( maxNonNegativeInteger(previous.sourceLatestSeq, previous.upstreamLatestSeq), maxNonNegativeInteger(eventsResponse?.maxSeq), lastSourceSeq ); const nextResultSyncState = textValue(resultSyncState ?? previous.resultSyncState) || "not_started"; const nextProjectionStatus = textValue(projectionStatus) || inferProjectionStatus({ lastSourceSeq, sourceLatestSeq, resultSyncState: nextResultSyncState, previous }); const nextProjectionHealth = textValue(projectionHealth) || (error ? "degraded" : "healthy"); return { ...previous, traceId, sessionId: textValue(previous.sessionId ?? result?.sessionId ?? mapping.sessionId) || null, conversationId: textValue(previous.conversationId ?? result?.conversationId ?? mapping.conversationId) || null, threadId: textValue(previous.threadId ?? result?.threadId ?? mapping.threadId) || null, ownerUserId: textValue(previous.ownerUserId ?? result?.ownerUserId) || null, ownerRole: textValue(previous.ownerRole ?? result?.ownerRole) || null, managerUrl: textValue(previous.managerUrl ?? mapping.managerUrl) || null, backendProfile: textValue(previous.backendProfile ?? mapping.backendProfile) || null, providerId: textValue(previous.providerId ?? mapping.providerId) || null, sourceRunId: runId, sourceCommandId: commandId, runId, commandId, lastSourceSeq, lastAgentRunSeq: lastSourceSeq, lastProjectedSeq: Math.max(maxNonNegativeInteger(previous.lastProjectedSeq), lastSourceSeq), sourceLatestSeq, upstreamLatestSeq: sourceLatestSeq, projectionStatus: nextProjectionStatus, projectionHealth: nextProjectionHealth, resultSyncState: nextResultSyncState, lastProjectedAt: lastSourceSeq > maxNonNegativeInteger(previous.lastSourceSeq, previous.lastAgentRunSeq) ? timestamp : previous.lastProjectedAt ?? timestamp, lastResultSyncAt: nextResultSyncState === "synced" ? timestamp : previous.lastResultSyncAt ?? null, lastErrorCode: error ? textValue(error.code ?? error.errorCode ?? "projection_sync_failed") : null, lastErrorMessage: error ? textValue(error.message ?? "Projection sync failed.") : null, failureCount: error ? maxNonNegativeInteger(previous.failureCount) + 1 : 0, nextRetryAt: error ? previous.nextRetryAt ?? null : null, createdAt: previous.createdAt ?? timestamp, updatedAt: timestamp, valuesPrinted: false }; } export function agentRunResultSyncDeferred({ forceResultSync = false, options = {}, env = process.env } = {}) { if (forceResultSync) return false; if (options.deferAgentRunResultSync === true) return true; const configured = String(env?.HWLAB_CODE_AGENT_AGENTRUN_RESULT_SYNC_IN_RUNNING ?? "").trim().toLowerCase(); return ["0", "false", "off", "no"].includes(configured); } function normalizeProjectionState(value) { if (!value || typeof value !== "object" || Array.isArray(value)) return null; return value; } function normalizeAgentRunMapping(value) { return value && typeof value === "object" && !Array.isArray(value) ? value : {}; } function inferProjectionStatus({ lastSourceSeq, sourceLatestSeq, resultSyncState, previous }) { if (["pending", "timed_out", "failed"].includes(resultSyncState)) return "terminal"; if (sourceLatestSeq > lastSourceSeq) return "projecting"; return textValue(previous?.projectionStatus) || "projecting"; } function boundedPageLimit(value) { const parsed = Number.parseInt(String(value ?? ""), 10); if (!Number.isInteger(parsed) || parsed <= 0) return DEFAULT_AGENTRUN_EVENTS_PAGE_LIMIT; return Math.min(parsed, MAX_AGENTRUN_EVENTS_PAGE_LIMIT); } function maxNonNegativeInteger(...values) { let max = 0; for (const value of values) { const parsed = Number.parseInt(String(value ?? ""), 10); if (Number.isInteger(parsed) && parsed >= 0 && parsed > max) max = parsed; } return max; } function timestampValue(now) { const value = typeof now === "function" ? now() : now; const parsed = Date.parse(String(value ?? "")); return Number.isFinite(parsed) ? new Date(parsed).toISOString() : new Date().toISOString(); } function textValue(value) { return String(value ?? "").trim(); }