UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

205 lines (204 loc) • 9.99 kB
import { l as normalizeOptionalString } from "./string-coerce-1VuiLaaR.mjs"; import { l as readErrorName } from "./error-coercion-W9BtigRm.mjs"; import "./errors-BvOkmQM5.mjs"; import { r as normalizeProviderId } from "./provider-id-D1VImdKk.mjs"; import { a as normalizeCliTranscriptCursor, i as normalizeCliSessionReseedReceipt } from "./cli-session-binding-Dx1VbkI8.mjs"; import { s as isFailoverError } from "./error-Cmm-AuUb.mjs"; import "./failover-error-JM6M2oyF.mjs"; import crypto from "node:crypto"; //#region src/agents/cli-session.ts /** * CLI session persistence helpers. * Keeps provider-keyed session bindings and reuse fingerprints in one * normalized session-store contract. */ const CLAUDE_CLI_BACKEND_ID = "claude-cli"; /** Whether a failover proves the provider-side conversation can no longer be resumed. */ function isCliSessionInvalidatingFailoverReason(reason) { return reason === "session_expired"; } /** Hash CLI session-sensitive text so reuse checks can compare stable fingerprints. */ function hashCliSessionText(value) { const trimmed = normalizeOptionalString(value); if (!trimmed) return; return crypto.createHash("sha256").update(trimmed).digest("hex"); } const CALLER_SURFACE_TOOL_NAMES = /* @__PURE__ */ new Set([ "screen", "show_widget", "suggest_task", "dismiss_task" ]); function hashCliToolNames(toolNames) { const stableNames = toolNames.filter((name) => !CALLER_SURFACE_TOOL_NAMES.has(name)).toSorted(); return hashCliSessionText(JSON.stringify(stableNames)); } /** Projects explicit native continuity; diagnostic sessionId can name the local session. */ function applyCliSessionBindingResult(entry, provider, meta) { if (meta?.clearCliSessionBinding === true) clearCliSession(entry, provider); else if (meta?.cliSessionBinding?.sessionId.trim()) setCliSessionBinding(entry, provider, meta.cliSessionBinding); else return false; return true; } /** Revalidates the exact turn owner at native continuity's synchronous commit edge. */ function assertCliSessionBindingResultCommitAllowed(meta, assertSettlementCurrent, abortSignal) { assertSettlementCurrent(); if (meta?.clearCliSessionBinding !== true) abortSignal?.throwIfAborted(); } /** Store a CLI session binding and mirror it to the provider-keyed session-id map. */ function setCliSessionBinding(entry, provider, binding) { const normalized = normalizeProviderId(provider); const trimmed = binding.sessionId.trim(); if (!trimmed) return; const previousBinding = entry.cliSessionBindings?.[normalized]; const previousReceipt = normalizeOptionalString(previousBinding?.sessionId) === trimmed ? normalizeCliSessionReseedReceipt(previousBinding?.reseedReceipt) : void 0; const reseedReceipt = normalizeCliSessionReseedReceipt(binding.reseedReceipt) ?? previousReceipt; const resumeCheckpointId = normalizeOptionalString(binding.resumeCheckpointId); const authProfileId = normalizeOptionalString(binding.authProfileId); const authEpoch = normalizeOptionalString(binding.authEpoch); const nextBinding = { sessionId: trimmed, ...resumeCheckpointId ? { resumeCheckpointId } : {}, ...binding.forceReuse === true ? { forceReuse: true } : {}, ...binding.forkNextResume === true ? { forkNextResume: true } : {}, ...authProfileId ? { authProfileId } : {}, ...authEpoch ? { authEpoch } : {}, ...typeof binding.authEpochVersion === "number" && Number.isFinite(binding.authEpochVersion) ? { authEpochVersion: binding.authEpochVersion } : {} }; const transcriptCursor = normalizeCliTranscriptCursor(binding.transcriptCursor ?? (previousBinding?.sessionId === trimmed ? previousBinding.transcriptCursor : void 0)); if (transcriptCursor) nextBinding.transcriptCursor = transcriptCursor; for (const field of [ "extraSystemPromptHash", "messageToolPolicyHash", "promptToolNamesHash", "cwdHash", "mcpConfigHash", "mcpResumeHash" ]) { const value = normalizeOptionalString(binding[field]); if (value) nextBinding[field] = value; } if (reseedReceipt) nextBinding.reseedReceipt = reseedReceipt; entry.cliSessionBindings = { ...entry.cliSessionBindings, [normalized]: nextBinding }; entry.cliSessionIds = { ...entry.cliSessionIds, [normalized]: trimmed }; } function clearCliSession(entry, provider) { const normalized = normalizeProviderId(provider); if (entry.cliSessionBindings?.[normalized] !== void 0) { const next = { ...entry.cliSessionBindings }; delete next[normalized]; entry.cliSessionBindings = Object.keys(next).length > 0 ? next : void 0; } if (entry.cliSessionIds?.[normalized] !== void 0) { const next = { ...entry.cliSessionIds }; delete next[normalized]; entry.cliSessionIds = Object.keys(next).length > 0 ? next : void 0; } if (normalized === CLAUDE_CLI_BACKEND_ID) entry.claudeCliSessionId = void 0; } /** Cancellation invalidates an unfinished replacement, not established continuity. */ function shouldClearInterruptedCliSessionBinding(params) { return params.interrupted && params.bindingReplacedDuringRun; } function shouldClearFailedCliSessionBinding(params) { if (!normalizeOptionalString(params.binding?.sessionId)) return false; if (params.hasNewGeneratedMediaTask === true) return false; if (isFailoverError(params.error)) return isCliSessionInvalidatingFailoverReason(params.error.reason); return shouldClearInterruptedCliSessionBinding({ interrupted: readErrorName(params.error) === "AbortError", bindingReplacedDuringRun: params.bindingReplacedDuringRun === true }); } function resolveCliSessionClearReason(error) { return isFailoverError(error) ? error.reason : readErrorName(error) ?? "error"; } const CLI_SESSION_DRIFT_NOTE_PREFIX = "OpenClaw resumed this CLI session after prompt content changed."; function buildCliSessionDriftNote(reasons) { return `${CLI_SESSION_DRIFT_NOTE_PREFIX} Follow the current turn's instructions; changed=${reasons.join(",")}.`; } const CLI_SESSION_DRIFT_NOTE_PREFIXES = [ buildCliSessionDriftNote(["system-prompt"]), buildCliSessionDriftNote(["prompt-tools"]), buildCliSessionDriftNote(["system-prompt", "prompt-tools"]) ].map((note) => `${note}\n\n`); function stripCliSessionDriftNote(text) { for (const prefix of CLI_SESSION_DRIFT_NOTE_PREFIXES) if (text.startsWith(prefix)) return text.slice(prefix.length); return text; } const DEFAULT_MESSAGE_TOOL_POLICY_HASH = hashCliSessionText(JSON.stringify({ sourceReplyDeliveryMode: "automatic", requireExplicitMessageTarget: false })); const LEGACY_EXPLICIT_FALSE_MESSAGE_TOOL_POLICY_HASH = hashCliSessionText(JSON.stringify({ requireExplicitMessageTarget: false })); function normalizeCliMessageToolPolicyHash(value) { const hash = normalizeOptionalString(value); return hash === void 0 || hash === LEGACY_EXPLICIT_FALSE_MESSAGE_TOOL_POLICY_HASH ? DEFAULT_MESSAGE_TOOL_POLICY_HASH : hash; } function resolveCliSessionReuse(params) { const binding = params.binding; const sessionId = normalizeOptionalString(binding?.sessionId); if (!sessionId) return { mode: "none" }; if (binding?.forceReuse === true) return { mode: "reuse", sessionId }; const currentAuthProfileId = normalizeOptionalString(params.authProfileId); const currentAuthEpoch = normalizeOptionalString(params.authEpoch); const currentExtraSystemPromptHash = normalizeOptionalString(params.extraSystemPromptHash); const currentMessageToolPolicyHash = normalizeCliMessageToolPolicyHash(params.messageToolPolicyHash); const currentPromptToolNamesHash = normalizeOptionalString(params.promptToolNamesHash); const currentCwdHash = normalizeOptionalString(params.cwdHash); const currentMcpConfigHash = normalizeOptionalString(params.mcpConfigHash); const currentMcpResumeHash = normalizeOptionalString(params.mcpResumeHash); const storedAuthProfileId = normalizeOptionalString(binding?.authProfileId); const storedAuthEpoch = normalizeOptionalString(binding?.authEpoch); const hasMatchingVersionedAuthEpoch = binding?.authEpochVersion === params.authEpochVersion && storedAuthEpoch !== void 0 && currentAuthEpoch !== void 0 && storedAuthEpoch === currentAuthEpoch; if (storedAuthProfileId !== currentAuthProfileId && !hasMatchingVersionedAuthEpoch) return { mode: "invalidate", invalidatedReason: "auth-profile" }; if (binding?.authEpochVersion === params.authEpochVersion && storedAuthEpoch !== currentAuthEpoch) return { mode: "invalidate", invalidatedReason: "auth-epoch" }; if (normalizeCliMessageToolPolicyHash(binding?.messageToolPolicyHash) !== currentMessageToolPolicyHash) return { mode: "invalidate", invalidatedReason: "message-policy" }; const storedCwdHash = normalizeOptionalString(binding?.cwdHash); if (storedCwdHash !== void 0 && storedCwdHash !== currentCwdHash) return { mode: "invalidate", invalidatedReason: "cwd" }; const storedMcpResumeHash = normalizeOptionalString(binding?.mcpResumeHash); if (storedMcpResumeHash && currentMcpResumeHash) { if (storedMcpResumeHash !== currentMcpResumeHash) return { mode: "invalidate", invalidatedReason: "mcp" }; } else if (normalizeOptionalString(binding?.mcpConfigHash) !== currentMcpConfigHash) return { mode: "invalidate", invalidatedReason: "mcp" }; if (normalizeOptionalString(binding?.promptToolNamesHash) !== currentPromptToolNamesHash) return { mode: "invalidate", invalidatedReason: "prompt-tools" }; if (normalizeOptionalString(binding?.extraSystemPromptHash) !== currentExtraSystemPromptHash) return { mode: "reuse-with-drift", sessionId, drift: { reasons: ["system-prompt"] } }; return { mode: "reuse", sessionId }; } //#endregion export { hashCliSessionText as a, resolveCliSessionClearReason as c, shouldClearFailedCliSessionBinding as d, shouldClearInterruptedCliSessionBinding as f, clearCliSession as i, resolveCliSessionReuse as l, assertCliSessionBindingResultCommitAllowed as n, hashCliToolNames as o, stripCliSessionDriftNote as p, buildCliSessionDriftNote as r, isCliSessionInvalidatingFailoverReason as s, applyCliSessionBindingResult as t, setCliSessionBinding as u };