/* * 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, polling = null, retryParams = 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(mapping.runId) || textValue(previous.sourceRunId ?? previous.runId); const commandId = textValue(mapping.commandId) || textValue(previous.sourceCommandId ?? previous.commandId); const previousRunId = textValue(previous.sourceRunId ?? previous.runId); const previousCommandId = textValue(previous.sourceCommandId ?? previous.commandId); const sourceChanged = Boolean((previousRunId && runId && previousRunId !== runId) || (previousCommandId && commandId && previousCommandId !== commandId)); const previousCursorSeq = sourceChanged ? 0 : maxNonNegativeInteger(previous.lastSourceSeq, previous.lastAgentRunSeq); const lastSourceSeq = Math.max( previousCursorSeq, maxNonNegativeInteger(mapping.lastSeq), maxNonNegativeInteger(eventsResponse?.traceLastSeq) ); const sourceLatestSeq = Math.max( sourceChanged ? 0 : maxNonNegativeInteger(previous.sourceLatestSeq, previous.upstreamLatestSeq), maxNonNegativeInteger(eventsResponse?.maxSeq), lastSourceSeq ); const nextRetryParams = normalizeProjectionRetryParams(retryParams) ?? normalizeProjectionRetryParams(previous.retryParams); const nextPolling = normalizeProjectionPolling(polling); 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"); const nextRetryAt = nextPolling?.nextRetryAt ?? (error ? previous.nextRetryAt ?? null : null); 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: sourceChanged ? lastSourceSeq : Math.max(maxNonNegativeInteger(previous.lastProjectedSeq), lastSourceSeq), sourceLatestSeq, upstreamLatestSeq: sourceLatestSeq, projectionStatus: nextProjectionStatus, projectionHealth: nextProjectionHealth, resultSyncState: nextResultSyncState, lastProjectedAt: sourceChanged || 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, pollCount: nextPolling?.pollCount ?? maxNonNegativeInteger(previous.pollCount), noProgressPollCount: nextPolling?.noProgressPollCount ?? (error ? maxNonNegativeInteger(previous.noProgressPollCount) : 0), backoffMs: nextPolling?.backoffMs ?? 0, backoffReason: nextPolling?.backoffReason ?? null, rootCause: nextPolling?.rootCause ?? null, terminalConsistencyGap: nextPolling?.terminalConsistencyGap ?? null, retryParams: nextRetryParams, 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 normalizeProjectionRetryParams(value) { if (!value || typeof value !== "object" || Array.isArray(value)) return null; const message = textValue(value.message ?? value.prompt ?? value.text); if (!message) return null; const result = { message, prompt: message, text: message, sessionId: textValue(value.sessionId) || null, conversationId: textValue(value.conversationId) || null, threadId: textValue(value.threadId) || null, projectId: textValue(value.projectId) || null, ownerUserId: textValue(value.ownerUserId) || null, ownerRole: textValue(value.ownerRole) || null, providerProfile: textValue(value.providerProfile ?? value.codeAgentProviderProfile ?? value.backendProfile) || null, codeAgentProviderProfile: textValue(value.codeAgentProviderProfile ?? value.providerProfile ?? value.backendProfile) || null, backendProfile: textValue(value.backendProfile ?? value.providerProfile ?? value.codeAgentProviderProfile) || null, providerId: textValue(value.providerId) || null, valuesPrinted: false }; for (const [key, item] of Object.entries(result)) { if (item === null || item === "") delete result[key]; } return result; } function normalizeProjectionPolling(value) { if (!value || typeof value !== "object" || Array.isArray(value)) return null; const result = { pollCount: maxNonNegativeInteger(value.pollCount), noProgressPollCount: maxNonNegativeInteger(value.noProgressPollCount), backoffMs: maxNonNegativeInteger(value.backoffMs), backoffReason: clippedText(value.backoffReason, 80), rootCause: clippedText(value.rootCause, 120), terminalConsistencyGap: maxNonNegativeInteger(value.terminalConsistencyGap), nextRetryAt: timestampText(value.nextRetryAt) }; return result; } 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(); } function clippedText(value, limit) { const text = textValue(value); if (!text) return null; return text.length > limit ? text.slice(0, limit) : text; } function timestampText(value) { const text = textValue(value); if (!text) return null; const parsed = Date.parse(text); return Number.isFinite(parsed) ? new Date(parsed).toISOString() : null; }