UNPKG

@gguf/claw

Version:

Multi-channel AI gateway with extensible messaging integrations

1,297 lines (1,282 loc) 267 kB
import { d as resolveAgentIdFromSessionKey, i as buildAgentMainSessionKey, l as normalizeAgentId, n as DEFAULT_AGENT_ID, u as normalizeMainKey } from "./session-key-9yEYlIQe.js"; import { D as normalizeE164, R as truncateUtf16Safe, j as resolveUserPath, st as resolvePreferredOpenClawTmpDir, t as CHANNEL_IDS } from "./registry-C8pj8ctW.js"; import { i as resolveGatewayPort, t as STATE_DIR, u as resolveRequiredHomeDir } from "./paths-DJmOcr7Q.js"; import { $ as resolveProcessScopedMap, et as isPidAlive } from "./model-selection-DQIwoYb8.js"; import { V as parseDurationMs, a as writeConfigFile, n as loadConfig, s as parseByteSize, t as createConfigIO } from "./config-DA0pxYcO.js"; import { a as buildImageResizeSideGrid, i as IMAGE_REDUCE_QUALITY_STEPS, n as openFileWithinRoot, s as getImageMetadata, t as SafeOpenError, u as resizeToJpeg } from "./fs-safe-CGcLaY9D.js"; import { i as defaultRuntime, t as createSubsystemLogger } from "./subsystem-BfLMZ8kW.js"; import { D as runExec, _ as DEFAULT_SOUL_FILENAME, b as ensureAgentWorkspace, f as DEFAULT_AGENTS_FILENAME, g as DEFAULT_IDENTITY_FILENAME, h as DEFAULT_HEARTBEAT_FILENAME, m as DEFAULT_BOOTSTRAP_FILENAME, n as resolveAgentConfig, p as DEFAULT_AGENT_WORKSPACE_DIR, u as resolveSessionAgentId, v as DEFAULT_TOOLS_FILENAME, y as DEFAULT_USER_FILENAME } from "./agent-scope-OWMdRegz.js"; import { d as wrapOwnerOnlyToolExecution, f as sanitizeContentBlocksImages } from "./common-CCPGi5xr.js"; import { a as syncSkillsToWorkspace, h as safeEqualSecret, l as resolveSandboxInputPath, m as sanitizeEnvVars, u as resolveSandboxPath } from "./skills-OZUqNHBX.js"; import { n as formatErrorMessage, t as extractErrorCode } from "./errors-Bv8oZiTO.js"; import { t as SsrFBlockedError } from "./ssrf-DxmqkHJJ.js"; import { A as DEFAULT_OPENCLAW_BROWSER_PROFILE_NAME, C as DEFAULT_AI_SNAPSHOT_EFFICIENT_DEPTH, D as DEFAULT_BROWSER_EVALUATE_ENABLED, E as DEFAULT_BROWSER_DEFAULT_PROFILE_NAME, O as DEFAULT_OPENCLAW_BROWSER_COLOR, S as stopChromeExtensionRelayServer, T as DEFAULT_AI_SNAPSHOT_MAX_CHARS, _ as fetchJson, a as resolveOpenClawUserDataDir, c as captureScreenshot, d as normalizeCdpWsUrl, f as snapshotAria, g as appendCdpPath, h as withBrowserNavigationPolicy, i as launchOpenClawChrome, j as isLoopbackHost, k as DEFAULT_OPENCLAW_BROWSER_ENABLED, l as createTargetViaCdp, m as assertBrowserNavigationAllowed, n as isChromeCdpReady, o as stopOpenClawChrome, p as InvalidBrowserNavigationUrlError, r as isChromeReachable, s as resolveBrowserExecutableForPlatform, v as fetchOk, w as DEFAULT_AI_SNAPSHOT_EFFICIENT_MAX_CHARS, x as ensureChromeExtensionRelayServer } from "./chrome-32CP7Tm2.js"; import { i as parseBooleanValue, t as formatCliCommand } from "./command-format-sreXrOOr.js"; import { A as normalizeHyphenSlug, d as getChannelDock, o as normalizeThinkLevel } from "./thinking-DFXDZFyu.js"; import { t as normalizeChatType } from "./chat-type-B5__aQIT.js"; import { r as normalizeChannelId } from "./plugins-BpKonLQ1.js"; import { o as listDeliverableMessageChannels, s as normalizeMessageChannel } from "./message-channel-C3OrO29D.js"; import { n as resolveConversationLabel } from "./conversation-label-2FLz1ei_.js"; import { a as resolveSessionTranscriptPathInDir, i as resolveSessionTranscriptPath, n as resolveSessionFilePath, s as resolveStorePath, t as resolveDefaultSessionStorePath } from "./paths-a1ZEI8nz.js"; import { t as emitSessionTranscriptUpdate } from "./transcript-events-Cj85Mq0h.js"; import { i as saveMediaBuffer, t as ensureMediaDir } from "./store-BJRhX9Bi.js"; import path, { posix } from "node:path"; import fs, { existsSync, realpathSync } from "node:fs"; import os from "node:os"; import fs$1 from "node:fs/promises"; import { spawn } from "node:child_process"; import crypto, { createHash } from "node:crypto"; import { CURRENT_SESSION_VERSION, SessionManager } from "@mariozechner/pi-coding-agent"; import express from "express"; //#region src/config/sessions/group.ts const getGroupSurfaces = () => new Set([...listDeliverableMessageChannels(), "webchat"]); function normalizeGroupLabel(raw) { return normalizeHyphenSlug(raw); } function shortenGroupId(value) { const trimmed = value?.trim() ?? ""; if (!trimmed) return ""; if (trimmed.length <= 14) return trimmed; return `${trimmed.slice(0, 6)}...${trimmed.slice(-4)}`; } function buildGroupDisplayName(params) { const providerKey = (params.provider?.trim().toLowerCase() || "group").trim(); const groupChannel = params.groupChannel?.trim(); const space = params.space?.trim(); const subject = params.subject?.trim(); const detail = (groupChannel && space ? `${space}${groupChannel.startsWith("#") ? "" : "#"}${groupChannel}` : groupChannel || subject || space || "") || ""; const fallbackId = params.id?.trim() || params.key; const rawLabel = detail || fallbackId; let token = normalizeGroupLabel(rawLabel); if (!token) token = normalizeGroupLabel(shortenGroupId(rawLabel)); if (!params.groupChannel && token.startsWith("#")) token = token.replace(/^#+/, ""); if (token && !/^[@#]/.test(token) && !token.startsWith("g-") && !token.includes("#")) token = `g-${token}`; return token ? `${providerKey}:${token}` : providerKey; } function resolveGroupSessionKey(ctx) { const from = typeof ctx.From === "string" ? ctx.From.trim() : ""; const chatType = ctx.ChatType?.trim().toLowerCase(); const normalizedChatType = chatType === "channel" ? "channel" : chatType === "group" ? "group" : void 0; const isWhatsAppGroupId = from.toLowerCase().endsWith("@g.us"); if (!(normalizedChatType === "group" || normalizedChatType === "channel" || from.includes(":group:") || from.includes(":channel:") || isWhatsAppGroupId)) return null; const providerHint = ctx.Provider?.trim().toLowerCase(); const parts = from.split(":").filter(Boolean); const head = parts[0]?.trim().toLowerCase() ?? ""; const headIsSurface = head ? getGroupSurfaces().has(head) : false; const provider = headIsSurface ? head : providerHint ?? (isWhatsAppGroupId ? "whatsapp" : void 0); if (!provider) return null; const second = parts[1]?.trim().toLowerCase(); const secondIsKind = second === "group" || second === "channel"; const kind = secondIsKind ? second : from.includes(":channel:") || normalizedChatType === "channel" ? "channel" : "group"; const finalId = (headIsSurface ? secondIsKind ? parts.slice(2).join(":") : parts.slice(1).join(":") : from).trim().toLowerCase(); if (!finalId) return null; return { key: `${provider}:${kind}:${finalId}`, channel: provider, id: finalId, chatType: kind === "channel" ? "channel" : "group" }; } //#endregion //#region src/config/sessions/metadata.ts const mergeOrigin = (existing, next) => { if (!existing && !next) return; const merged = existing ? { ...existing } : {}; if (next?.label) merged.label = next.label; if (next?.provider) merged.provider = next.provider; if (next?.surface) merged.surface = next.surface; if (next?.chatType) merged.chatType = next.chatType; if (next?.from) merged.from = next.from; if (next?.to) merged.to = next.to; if (next?.accountId) merged.accountId = next.accountId; if (next?.threadId != null && next.threadId !== "") merged.threadId = next.threadId; return Object.keys(merged).length > 0 ? merged : void 0; }; function deriveSessionOrigin(ctx) { const label = resolveConversationLabel(ctx)?.trim(); const provider = normalizeMessageChannel(typeof ctx.OriginatingChannel === "string" && ctx.OriginatingChannel || ctx.Surface || ctx.Provider); const surface = ctx.Surface?.trim().toLowerCase(); const chatType = normalizeChatType(ctx.ChatType) ?? void 0; const from = ctx.From?.trim(); const to = (typeof ctx.OriginatingTo === "string" ? ctx.OriginatingTo : ctx.To)?.trim() ?? void 0; const accountId = ctx.AccountId?.trim(); const threadId = ctx.MessageThreadId ?? void 0; const origin = {}; if (label) origin.label = label; if (provider) origin.provider = provider; if (surface) origin.surface = surface; if (chatType) origin.chatType = chatType; if (from) origin.from = from; if (to) origin.to = to; if (accountId) origin.accountId = accountId; if (threadId != null && threadId !== "") origin.threadId = threadId; return Object.keys(origin).length > 0 ? origin : void 0; } function deriveGroupSessionPatch(params) { const resolution = params.groupResolution ?? resolveGroupSessionKey(params.ctx); if (!resolution?.channel) return null; const channel = resolution.channel; const subject = params.ctx.GroupSubject?.trim(); const space = params.ctx.GroupSpace?.trim(); const explicitChannel = params.ctx.GroupChannel?.trim(); const normalizedChannel = normalizeChannelId(channel); const isChannelProvider = Boolean(normalizedChannel && getChannelDock(normalizedChannel)?.capabilities.chatTypes.includes("channel")); const nextGroupChannel = explicitChannel ?? ((resolution.chatType === "channel" || isChannelProvider) && subject && subject.startsWith("#") ? subject : void 0); const nextSubject = nextGroupChannel ? void 0 : subject; const patch = { chatType: resolution.chatType ?? "group", channel, groupId: resolution.id }; if (nextSubject) patch.subject = nextSubject; if (nextGroupChannel) patch.groupChannel = nextGroupChannel; if (space) patch.space = space; const displayName = buildGroupDisplayName({ provider: channel, subject: nextSubject ?? params.existing?.subject, groupChannel: nextGroupChannel ?? params.existing?.groupChannel, space: space ?? params.existing?.space, id: resolution.id, key: params.sessionKey }); if (displayName) patch.displayName = displayName; return patch; } function deriveSessionMetaPatch(params) { const groupPatch = deriveGroupSessionPatch(params); const origin = deriveSessionOrigin(params.ctx); if (!groupPatch && !origin) return null; const patch = groupPatch ? { ...groupPatch } : {}; const mergedOrigin = mergeOrigin(params.existing?.origin, origin); if (mergedOrigin) patch.origin = mergedOrigin; return Object.keys(patch).length > 0 ? patch : null; } //#endregion //#region src/config/sessions/main-session.ts function resolveMainSessionKey(cfg) { if (cfg?.session?.scope === "global") return "global"; const agents = cfg?.agents?.list ?? []; return buildAgentMainSessionKey({ agentId: normalizeAgentId(agents.find((agent) => agent?.default)?.id ?? agents[0]?.id ?? DEFAULT_AGENT_ID), mainKey: normalizeMainKey(cfg?.session?.mainKey) }); } function resolveAgentMainSessionKey(params) { const mainKey = normalizeMainKey(params.cfg?.session?.mainKey); return buildAgentMainSessionKey({ agentId: params.agentId, mainKey }); } function resolveExplicitAgentSessionKey(params) { const agentId = params.agentId?.trim(); if (!agentId) return; return resolveAgentMainSessionKey({ cfg: params.cfg, agentId }); } function canonicalizeMainSessionAlias(params) { const raw = params.sessionKey.trim(); if (!raw) return raw; const agentId = normalizeAgentId(params.agentId); const mainKey = normalizeMainKey(params.cfg?.session?.mainKey); const agentMainSessionKey = buildAgentMainSessionKey({ agentId, mainKey }); const agentMainAliasKey = buildAgentMainSessionKey({ agentId, mainKey: "main" }); const isMainAlias = raw === "main" || raw === mainKey || raw === agentMainSessionKey || raw === agentMainAliasKey; if (params.cfg?.session?.scope === "global" && isMainAlias) return "global"; if (isMainAlias) return agentMainSessionKey; return raw; } //#endregion //#region src/config/sessions/types.ts function mergeSessionEntry(existing, patch) { const sessionId = patch.sessionId ?? existing?.sessionId ?? crypto.randomUUID(); const updatedAt = Math.max(existing?.updatedAt ?? 0, patch.updatedAt ?? 0, Date.now()); if (!existing) return { ...patch, sessionId, updatedAt }; return { ...existing, ...patch, sessionId, updatedAt }; } function resolveFreshSessionTotalTokens(entry) { const total = entry?.totalTokens; if (typeof total !== "number" || !Number.isFinite(total) || total < 0) return; if (entry?.totalTokensFresh === false) return; return total; } const DEFAULT_RESET_TRIGGERS = ["/new", "/reset"]; const DEFAULT_IDLE_MINUTES = 60; //#endregion //#region src/config/sessions/reset.ts const DEFAULT_RESET_MODE = "daily"; const DEFAULT_RESET_AT_HOUR = 4; const THREAD_SESSION_MARKERS = [":thread:", ":topic:"]; const GROUP_SESSION_MARKERS = [":group:", ":channel:"]; function isThreadSessionKey(sessionKey) { const normalized = (sessionKey ?? "").toLowerCase(); if (!normalized) return false; return THREAD_SESSION_MARKERS.some((marker) => normalized.includes(marker)); } function resolveSessionResetType(params) { if (params.isThread || isThreadSessionKey(params.sessionKey)) return "thread"; if (params.isGroup) return "group"; const normalized = (params.sessionKey ?? "").toLowerCase(); if (GROUP_SESSION_MARKERS.some((marker) => normalized.includes(marker))) return "group"; return "direct"; } function resolveThreadFlag(params) { if (params.messageThreadId != null) return true; if (params.threadLabel?.trim()) return true; if (params.threadStarterBody?.trim()) return true; if (params.parentSessionKey?.trim()) return true; return isThreadSessionKey(params.sessionKey); } function resolveDailyResetAtMs(now, atHour) { const normalizedAtHour = normalizeResetAtHour(atHour); const resetAt = new Date(now); resetAt.setHours(normalizedAtHour, 0, 0, 0); if (now < resetAt.getTime()) resetAt.setDate(resetAt.getDate() - 1); return resetAt.getTime(); } function resolveSessionResetPolicy(params) { const sessionCfg = params.sessionCfg; const baseReset = params.resetOverride ?? sessionCfg?.reset; const typeReset = params.resetOverride ? void 0 : sessionCfg?.resetByType?.[params.resetType] ?? (params.resetType === "direct" ? (sessionCfg?.resetByType)?.dm : void 0); const hasExplicitReset = Boolean(baseReset || sessionCfg?.resetByType); const legacyIdleMinutes = params.resetOverride ? void 0 : sessionCfg?.idleMinutes; const mode = typeReset?.mode ?? baseReset?.mode ?? (!hasExplicitReset && legacyIdleMinutes != null ? "idle" : DEFAULT_RESET_MODE); const atHour = normalizeResetAtHour(typeReset?.atHour ?? baseReset?.atHour ?? DEFAULT_RESET_AT_HOUR); const idleMinutesRaw = typeReset?.idleMinutes ?? baseReset?.idleMinutes ?? legacyIdleMinutes; let idleMinutes; if (idleMinutesRaw != null) { const normalized = Math.floor(idleMinutesRaw); if (Number.isFinite(normalized)) idleMinutes = Math.max(normalized, 1); } else if (mode === "idle") idleMinutes = DEFAULT_IDLE_MINUTES; return { mode, atHour, idleMinutes }; } function resolveChannelResetConfig(params) { const resetByChannel = params.sessionCfg?.resetByChannel; if (!resetByChannel) return; const normalized = normalizeMessageChannel(params.channel); const fallback = params.channel?.trim().toLowerCase(); const key = normalized ?? fallback; if (!key) return; return resetByChannel[key] ?? resetByChannel[key.toLowerCase()]; } function evaluateSessionFreshness(params) { const dailyResetAt = params.policy.mode === "daily" ? resolveDailyResetAtMs(params.now, params.policy.atHour) : void 0; const idleExpiresAt = params.policy.idleMinutes != null ? params.updatedAt + params.policy.idleMinutes * 6e4 : void 0; const staleDaily = dailyResetAt != null && params.updatedAt < dailyResetAt; const staleIdle = idleExpiresAt != null && params.now > idleExpiresAt; return { fresh: !(staleDaily || staleIdle), dailyResetAt, idleExpiresAt }; } function normalizeResetAtHour(value) { if (typeof value !== "number" || !Number.isFinite(value)) return DEFAULT_RESET_AT_HOUR; const normalized = Math.floor(value); if (!Number.isFinite(normalized)) return DEFAULT_RESET_AT_HOUR; if (normalized < 0) return 0; if (normalized > 23) return 23; return normalized; } //#endregion //#region src/config/sessions/session-key.ts function deriveSessionKey(scope, ctx) { if (scope === "global") return "global"; const resolvedGroup = resolveGroupSessionKey(ctx); if (resolvedGroup) return resolvedGroup.key; return (ctx.From ? normalizeE164(ctx.From) : "") || "unknown"; } /** * Resolve the session key with a canonical direct-chat bucket (default: "main"). * All non-group direct chats collapse to this bucket; groups stay isolated. */ function resolveSessionKey(scope, ctx, mainKey) { const explicit = ctx.SessionKey?.trim(); if (explicit) return explicit.toLowerCase(); const raw = deriveSessionKey(scope, ctx); if (scope === "global") return raw; const canonical = buildAgentMainSessionKey({ agentId: DEFAULT_AGENT_ID, mainKey: normalizeMainKey(mainKey) }); if (!(raw.includes(":group:") || raw.includes(":channel:"))) return canonical; return `agent:${DEFAULT_AGENT_ID}:${raw}`; } //#endregion //#region src/agents/session-write-lock.ts const CLEANUP_SIGNALS = [ "SIGINT", "SIGTERM", "SIGQUIT", "SIGABRT" ]; const CLEANUP_STATE_KEY = Symbol.for("openclaw.sessionWriteLockCleanupState"); const HELD_LOCKS_KEY = Symbol.for("openclaw.sessionWriteLockHeldLocks"); const WATCHDOG_STATE_KEY = Symbol.for("openclaw.sessionWriteLockWatchdogState"); const DEFAULT_STALE_MS = 1800 * 1e3; const DEFAULT_MAX_HOLD_MS = 300 * 1e3; const DEFAULT_WATCHDOG_INTERVAL_MS = 6e4; const DEFAULT_TIMEOUT_GRACE_MS = 120 * 1e3; const MAX_LOCK_HOLD_MS = 2147e6; const HELD_LOCKS = resolveProcessScopedMap(HELD_LOCKS_KEY); function resolveCleanupState() { const proc = process; if (!proc[CLEANUP_STATE_KEY]) proc[CLEANUP_STATE_KEY] = { registered: false, cleanupHandlers: /* @__PURE__ */ new Map() }; return proc[CLEANUP_STATE_KEY]; } function resolveWatchdogState() { const proc = process; if (!proc[WATCHDOG_STATE_KEY]) proc[WATCHDOG_STATE_KEY] = { started: false, intervalMs: DEFAULT_WATCHDOG_INTERVAL_MS }; return proc[WATCHDOG_STATE_KEY]; } function resolvePositiveMs(value, fallback, opts = {}) { if (typeof value !== "number" || Number.isNaN(value) || value <= 0) return fallback; if (value === Number.POSITIVE_INFINITY) return opts.allowInfinity ? value : fallback; if (!Number.isFinite(value)) return fallback; return value; } function resolveSessionLockMaxHoldFromTimeout(params) { const minMs = resolvePositiveMs(params.minMs, DEFAULT_MAX_HOLD_MS); const timeoutMs = resolvePositiveMs(params.timeoutMs, minMs, { allowInfinity: true }); if (timeoutMs === Number.POSITIVE_INFINITY) return MAX_LOCK_HOLD_MS; const graceMs = resolvePositiveMs(params.graceMs, DEFAULT_TIMEOUT_GRACE_MS); return Math.min(MAX_LOCK_HOLD_MS, Math.max(minMs, timeoutMs + graceMs)); } async function releaseHeldLock(normalizedSessionFile, held, opts = {}) { if (HELD_LOCKS.get(normalizedSessionFile) !== held) return false; if (opts.force) held.count = 0; else { held.count -= 1; if (held.count > 0) return false; } if (held.releasePromise) { await held.releasePromise.catch(() => void 0); return true; } HELD_LOCKS.delete(normalizedSessionFile); held.releasePromise = (async () => { try { await held.handle.close(); } catch {} try { await fs$1.rm(held.lockPath, { force: true }); } catch {} })(); try { await held.releasePromise; return true; } finally { held.releasePromise = void 0; } } /** * Synchronously release all held locks. * Used during process exit when async operations aren't reliable. */ function releaseAllLocksSync() { for (const [sessionFile, held] of HELD_LOCKS) { try { if (typeof held.handle.close === "function") held.handle.close().catch(() => {}); } catch {} try { fs.rmSync(held.lockPath, { force: true }); } catch {} HELD_LOCKS.delete(sessionFile); } } async function runLockWatchdogCheck(nowMs = Date.now()) { let released = 0; for (const [sessionFile, held] of HELD_LOCKS.entries()) { const heldForMs = nowMs - held.acquiredAt; if (heldForMs <= held.maxHoldMs) continue; console.warn(`[session-write-lock] releasing lock held for ${heldForMs}ms (max=${held.maxHoldMs}ms): ${held.lockPath}`); if (await releaseHeldLock(sessionFile, held, { force: true })) released += 1; } return released; } function ensureWatchdogStarted(intervalMs) { const watchdogState = resolveWatchdogState(); if (watchdogState.started) return; watchdogState.started = true; watchdogState.intervalMs = intervalMs; watchdogState.timer = setInterval(() => { runLockWatchdogCheck().catch(() => {}); }, intervalMs); watchdogState.timer.unref?.(); } function handleTerminationSignal(signal) { releaseAllLocksSync(); const cleanupState = resolveCleanupState(); if (process.listenerCount(signal) === 1) { const handler = cleanupState.cleanupHandlers.get(signal); if (handler) { process.off(signal, handler); cleanupState.cleanupHandlers.delete(signal); } try { process.kill(process.pid, signal); } catch {} } } function registerCleanupHandlers() { const cleanupState = resolveCleanupState(); if (!cleanupState.registered) { cleanupState.registered = true; process.on("exit", () => { releaseAllLocksSync(); }); } ensureWatchdogStarted(DEFAULT_WATCHDOG_INTERVAL_MS); for (const signal of CLEANUP_SIGNALS) { if (cleanupState.cleanupHandlers.has(signal)) continue; try { const handler = () => handleTerminationSignal(signal); cleanupState.cleanupHandlers.set(signal, handler); process.on(signal, handler); } catch {} } } async function readLockPayload(lockPath) { try { const raw = await fs$1.readFile(lockPath, "utf8"); const parsed = JSON.parse(raw); const payload = {}; if (typeof parsed.pid === "number") payload.pid = parsed.pid; if (typeof parsed.createdAt === "string") payload.createdAt = parsed.createdAt; return payload; } catch { return null; } } function inspectLockPayload(payload, staleMs, nowMs) { const pid = typeof payload?.pid === "number" ? payload.pid : null; const pidAlive = pid !== null ? isPidAlive(pid) : false; const createdAt = typeof payload?.createdAt === "string" ? payload.createdAt : null; const createdAtMs = createdAt ? Date.parse(createdAt) : NaN; const ageMs = Number.isFinite(createdAtMs) ? Math.max(0, nowMs - createdAtMs) : null; const staleReasons = []; if (pid === null) staleReasons.push("missing-pid"); else if (!pidAlive) staleReasons.push("dead-pid"); if (ageMs === null) staleReasons.push("invalid-createdAt"); else if (ageMs > staleMs) staleReasons.push("too-old"); return { pid, pidAlive, createdAt, ageMs, stale: staleReasons.length > 0, staleReasons }; } async function acquireSessionWriteLock(params) { registerCleanupHandlers(); const timeoutMs = resolvePositiveMs(params.timeoutMs, 1e4, { allowInfinity: true }); const staleMs = resolvePositiveMs(params.staleMs, DEFAULT_STALE_MS); const maxHoldMs = resolvePositiveMs(params.maxHoldMs, DEFAULT_MAX_HOLD_MS); const sessionFile = path.resolve(params.sessionFile); const sessionDir = path.dirname(sessionFile); await fs$1.mkdir(sessionDir, { recursive: true }); let normalizedDir = sessionDir; try { normalizedDir = await fs$1.realpath(sessionDir); } catch {} const normalizedSessionFile = path.join(normalizedDir, path.basename(sessionFile)); const lockPath = `${normalizedSessionFile}.lock`; const allowReentrant = params.allowReentrant ?? true; const held = HELD_LOCKS.get(normalizedSessionFile); if (allowReentrant && held) { held.count += 1; return { release: async () => { await releaseHeldLock(normalizedSessionFile, held); } }; } const startedAt = Date.now(); let attempt = 0; while (Date.now() - startedAt < timeoutMs) { attempt += 1; try { const handle = await fs$1.open(lockPath, "wx"); const createdAt = (/* @__PURE__ */ new Date()).toISOString(); await handle.writeFile(JSON.stringify({ pid: process.pid, createdAt }, null, 2), "utf8"); const createdHeld = { count: 1, handle, lockPath, acquiredAt: Date.now(), maxHoldMs }; HELD_LOCKS.set(normalizedSessionFile, createdHeld); return { release: async () => { await releaseHeldLock(normalizedSessionFile, createdHeld); } }; } catch (err) { if (err.code !== "EEXIST") throw err; if (inspectLockPayload(await readLockPayload(lockPath), staleMs, Date.now()).stale) { await fs$1.rm(lockPath, { force: true }); continue; } const delay = Math.min(1e3, 50 * attempt); await new Promise((r) => setTimeout(r, delay)); } } const payload = await readLockPayload(lockPath); const owner = typeof payload?.pid === "number" ? `pid=${payload.pid}` : "unknown"; throw new Error(`session file locked (timeout ${timeoutMs}ms): ${owner} ${lockPath}`); } const __testing = { cleanupSignals: [...CLEANUP_SIGNALS], handleTerminationSignal, releaseAllLocksSync, runLockWatchdogCheck }; //#endregion //#region src/sessions/input-provenance.ts const INPUT_PROVENANCE_KIND_VALUES = [ "external_user", "inter_session", "internal_system" ]; function normalizeOptionalString(value) { if (typeof value !== "string") return; const trimmed = value.trim(); return trimmed ? trimmed : void 0; } function isInputProvenanceKind(value) { return typeof value === "string" && INPUT_PROVENANCE_KIND_VALUES.includes(value); } function normalizeInputProvenance(value) { if (!value || typeof value !== "object") return; const record = value; if (!isInputProvenanceKind(record.kind)) return; return { kind: record.kind, sourceSessionKey: normalizeOptionalString(record.sourceSessionKey), sourceChannel: normalizeOptionalString(record.sourceChannel), sourceTool: normalizeOptionalString(record.sourceTool) }; } function applyInputProvenanceToUserMessage(message, inputProvenance) { if (!inputProvenance) return message; if (message.role !== "user") return message; if (normalizeInputProvenance(message.provenance)) return message; return { ...message, provenance: inputProvenance }; } function isInterSessionInputProvenance(value) { return normalizeInputProvenance(value)?.kind === "inter_session"; } function hasInterSessionUserProvenance(message) { if (!message || message.role !== "user") return false; return isInterSessionInputProvenance(message.provenance); } //#endregion //#region src/utils/transcript-tools.ts const TOOL_CALL_TYPES$1 = new Set([ "tool_use", "toolcall", "tool_call" ]); const TOOL_RESULT_TYPES = new Set(["tool_result", "tool_result_error"]); const normalizeType = (value) => { if (typeof value !== "string") return ""; return value.trim().toLowerCase(); }; const extractToolCallNames = (message) => { const names = /* @__PURE__ */ new Set(); const toolNameRaw = message.toolName ?? message.tool_name; if (typeof toolNameRaw === "string" && toolNameRaw.trim()) names.add(toolNameRaw.trim()); const content = message.content; if (!Array.isArray(content)) return Array.from(names); for (const entry of content) { if (!entry || typeof entry !== "object") continue; const block = entry; const type = normalizeType(block.type); if (!TOOL_CALL_TYPES$1.has(type)) continue; const name = block.name; if (typeof name === "string" && name.trim()) names.add(name.trim()); } return Array.from(names); }; const countToolResults = (message) => { const content = message.content; if (!Array.isArray(content)) return { total: 0, errors: 0 }; let total = 0; let errors = 0; for (const entry of content) { if (!entry || typeof entry !== "object") continue; const block = entry; const type = normalizeType(block.type); if (!TOOL_RESULT_TYPES.has(type)) continue; total += 1; if (block.is_error === true) errors += 1; } return { total, errors }; }; //#endregion //#region src/auto-reply/reply/strip-inbound-meta.ts /** * Strips OpenClaw-injected inbound metadata blocks from a user-role message * text before it is displayed in any UI surface (TUI, webchat, macOS app). * * Background: `buildInboundUserContextPrefix` in `inbound-meta.ts` prepends * structured metadata blocks (Conversation info, Sender info, reply context, * etc.) directly to the stored user message content so the LLM can access * them. These blocks are AI-facing only and must never surface in user-visible * chat history. */ /** * Sentinel strings that identify the start of an injected metadata block. * Must stay in sync with `buildInboundUserContextPrefix` in `inbound-meta.ts`. */ const INBOUND_META_SENTINELS = [ "Conversation info (untrusted metadata):", "Sender (untrusted metadata):", "Thread starter (untrusted, for context):", "Replied message (untrusted, for context):", "Forwarded message context (untrusted metadata):", "Chat history since last reply (untrusted, for context):" ]; const SENTINEL_FAST_RE = new RegExp(INBOUND_META_SENTINELS.map((s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|")); //#endregion //#region src/gateway/session-utils.fs.ts function resolveSessionTranscriptCandidates(sessionId, storePath, sessionFile, agentId) { const candidates = []; const pushCandidate = (resolve) => { try { candidates.push(resolve()); } catch {} }; if (storePath) { const sessionsDir = path.dirname(storePath); if (sessionFile) pushCandidate(() => resolveSessionFilePath(sessionId, { sessionFile }, { sessionsDir, agentId })); pushCandidate(() => resolveSessionTranscriptPathInDir(sessionId, sessionsDir)); } else if (sessionFile) if (agentId) pushCandidate(() => resolveSessionFilePath(sessionId, { sessionFile }, { agentId })); else { const trimmed = sessionFile.trim(); if (trimmed) candidates.push(path.resolve(trimmed)); } if (agentId) pushCandidate(() => resolveSessionTranscriptPath(sessionId, agentId)); const home = resolveRequiredHomeDir(process.env, os.homedir); const legacyDir = path.join(home, ".openclaw", "sessions"); pushCandidate(() => resolveSessionTranscriptPathInDir(sessionId, legacyDir)); return Array.from(new Set(candidates)); } function archiveFileOnDisk(filePath, reason) { const archived = `${filePath}.${reason}.${(/* @__PURE__ */ new Date()).toISOString().replaceAll(":", "-")}`; fs.renameSync(filePath, archived); return archived; } /** * Archives all transcript files for a given session. * Best-effort: silently skips files that don't exist or fail to rename. */ function archiveSessionTranscripts(opts) { const archived = []; for (const candidate of resolveSessionTranscriptCandidates(opts.sessionId, opts.storePath, opts.sessionFile, opts.agentId)) { if (!fs.existsSync(candidate)) continue; try { archived.push(archiveFileOnDisk(candidate, opts.reason)); } catch {} } return archived; } function restoreArchiveTimestamp(raw) { const [datePart, timePart] = raw.split("T"); if (!datePart || !timePart) return raw; return `${datePart}T${timePart.replace(/-/g, ":")}`; } function parseArchivedTimestamp(fileName, reason) { const marker = `.${reason}.`; const index = fileName.lastIndexOf(marker); if (index < 0) return null; const raw = fileName.slice(index + marker.length); if (!raw) return null; const timestamp = Date.parse(restoreArchiveTimestamp(raw)); return Number.isNaN(timestamp) ? null : timestamp; } async function cleanupArchivedSessionTranscripts(opts) { if (!Number.isFinite(opts.olderThanMs) || opts.olderThanMs < 0) return { removed: 0, scanned: 0 }; const now = opts.nowMs ?? Date.now(); const reason = opts.reason ?? "deleted"; const directories = Array.from(new Set(opts.directories.map((dir) => path.resolve(dir)))); let removed = 0; let scanned = 0; for (const dir of directories) { const entries = await fs.promises.readdir(dir).catch(() => []); for (const entry of entries) { const timestamp = parseArchivedTimestamp(entry, reason); if (timestamp == null) continue; scanned += 1; if (now - timestamp <= opts.olderThanMs) continue; const fullPath = path.join(dir, entry); if (!(await fs.promises.stat(fullPath).catch(() => null))?.isFile()) continue; await fs.promises.rm(fullPath).catch(() => void 0); removed += 1; } } return { removed, scanned }; } function jsonUtf8Bytes(value) { try { return Buffer.byteLength(JSON.stringify(value), "utf8"); } catch { return Buffer.byteLength(String(value), "utf8"); } } function capArrayByJsonBytes(items, maxBytes) { if (items.length === 0) return { items, bytes: 2 }; const parts = items.map((item) => jsonUtf8Bytes(item)); let bytes = 2 + parts.reduce((a, b) => a + b, 0) + (items.length - 1); let start = 0; while (bytes > maxBytes && start < items.length - 1) { bytes -= parts[start] + 1; start += 1; } return { items: start > 0 ? items.slice(start) : items, bytes }; } const PREVIEW_READ_SIZES = [ 64 * 1024, 256 * 1024, 1024 * 1024 ]; //#endregion //#region src/utils/account-id.ts function normalizeAccountId(value) { if (typeof value !== "string") return; return value.trim() || void 0; } //#endregion //#region src/utils/delivery-context.ts function normalizeDeliveryContext(context) { if (!context) return; const channel = typeof context.channel === "string" ? normalizeMessageChannel(context.channel) ?? context.channel.trim() : void 0; const to = typeof context.to === "string" ? context.to.trim() : void 0; const accountId = normalizeAccountId(context.accountId); const threadId = typeof context.threadId === "number" && Number.isFinite(context.threadId) ? Math.trunc(context.threadId) : typeof context.threadId === "string" ? context.threadId.trim() : void 0; const normalizedThreadId = typeof threadId === "string" ? threadId ? threadId : void 0 : threadId; if (!channel && !to && !accountId && normalizedThreadId == null) return; const normalized = { channel: channel || void 0, to: to || void 0, accountId }; if (normalizedThreadId != null) normalized.threadId = normalizedThreadId; return normalized; } function normalizeSessionDeliveryFields(source) { if (!source) return { deliveryContext: void 0, lastChannel: void 0, lastTo: void 0, lastAccountId: void 0, lastThreadId: void 0 }; const merged = mergeDeliveryContext(normalizeDeliveryContext({ channel: source.lastChannel ?? source.channel, to: source.lastTo, accountId: source.lastAccountId, threadId: source.lastThreadId }), normalizeDeliveryContext(source.deliveryContext)); if (!merged) return { deliveryContext: void 0, lastChannel: void 0, lastTo: void 0, lastAccountId: void 0, lastThreadId: void 0 }; return { deliveryContext: merged, lastChannel: merged.channel, lastTo: merged.to, lastAccountId: merged.accountId, lastThreadId: merged.threadId }; } function deliveryContextFromSession(entry) { if (!entry) return; return normalizeSessionDeliveryFields({ channel: entry.channel, lastChannel: entry.lastChannel, lastTo: entry.lastTo, lastAccountId: entry.lastAccountId, lastThreadId: entry.lastThreadId ?? entry.deliveryContext?.threadId ?? entry.origin?.threadId, deliveryContext: entry.deliveryContext }).deliveryContext; } function mergeDeliveryContext(primary, fallback) { const normalizedPrimary = normalizeDeliveryContext(primary); const normalizedFallback = normalizeDeliveryContext(fallback); if (!normalizedPrimary && !normalizedFallback) return; return normalizeDeliveryContext({ channel: normalizedPrimary?.channel ?? normalizedFallback?.channel, to: normalizedPrimary?.to ?? normalizedFallback?.to, accountId: normalizedPrimary?.accountId ?? normalizedFallback?.accountId, threadId: normalizedPrimary?.threadId ?? normalizedFallback?.threadId }); } function deliveryContextKey(context) { const normalized = normalizeDeliveryContext(context); if (!normalized?.channel || !normalized?.to) return; const threadId = normalized.threadId != null && normalized.threadId !== "" ? String(normalized.threadId) : ""; return `${normalized.channel}|${normalized.to}|${normalized.accountId ?? ""}|${threadId}`; } //#endregion //#region src/config/cache-utils.ts function resolveCacheTtlMs(params) { const { envValue, defaultTtlMs } = params; if (envValue) { const parsed = Number.parseInt(envValue, 10); if (Number.isFinite(parsed) && parsed >= 0) return parsed; } return defaultTtlMs; } function isCacheEnabled(ttlMs) { return ttlMs > 0; } function getFileMtimeMs(filePath) { try { return fs.statSync(filePath).mtimeMs; } catch { return; } } //#endregion //#region src/config/sessions/store.ts const log = createSubsystemLogger("sessions/store"); const SESSION_STORE_CACHE = /* @__PURE__ */ new Map(); const DEFAULT_SESSION_STORE_TTL_MS = 45e3; function isSessionStoreRecord(value) { return !!value && typeof value === "object" && !Array.isArray(value); } function getSessionStoreTtl() { return resolveCacheTtlMs({ envValue: process.env.OPENCLAW_SESSION_CACHE_TTL_MS, defaultTtlMs: DEFAULT_SESSION_STORE_TTL_MS }); } function isSessionStoreCacheEnabled() { return isCacheEnabled(getSessionStoreTtl()); } function isSessionStoreCacheValid(entry) { const now = Date.now(); const ttl = getSessionStoreTtl(); return now - entry.loadedAt <= ttl; } function invalidateSessionStoreCache(storePath) { SESSION_STORE_CACHE.delete(storePath); } function normalizeSessionEntryDelivery(entry) { const normalized = normalizeSessionDeliveryFields({ channel: entry.channel, lastChannel: entry.lastChannel, lastTo: entry.lastTo, lastAccountId: entry.lastAccountId, lastThreadId: entry.lastThreadId ?? entry.deliveryContext?.threadId ?? entry.origin?.threadId, deliveryContext: entry.deliveryContext }); const nextDelivery = normalized.deliveryContext; const sameDelivery = (entry.deliveryContext?.channel ?? void 0) === nextDelivery?.channel && (entry.deliveryContext?.to ?? void 0) === nextDelivery?.to && (entry.deliveryContext?.accountId ?? void 0) === nextDelivery?.accountId && (entry.deliveryContext?.threadId ?? void 0) === nextDelivery?.threadId; const sameLast = entry.lastChannel === normalized.lastChannel && entry.lastTo === normalized.lastTo && entry.lastAccountId === normalized.lastAccountId && entry.lastThreadId === normalized.lastThreadId; if (sameDelivery && sameLast) return entry; return { ...entry, deliveryContext: nextDelivery, lastChannel: normalized.lastChannel, lastTo: normalized.lastTo, lastAccountId: normalized.lastAccountId, lastThreadId: normalized.lastThreadId }; } function removeThreadFromDeliveryContext(context) { if (!context || context.threadId == null) return context; const next = { ...context }; delete next.threadId; return next; } function normalizeSessionStore(store) { for (const [key, entry] of Object.entries(store)) { if (!entry) continue; const normalized = normalizeSessionEntryDelivery(entry); if (normalized !== entry) store[key] = normalized; } } function loadSessionStore(storePath, opts = {}) { if (!opts.skipCache && isSessionStoreCacheEnabled()) { const cached = SESSION_STORE_CACHE.get(storePath); if (cached && isSessionStoreCacheValid(cached)) { if (getFileMtimeMs(storePath) === cached.mtimeMs) return structuredClone(cached.store); invalidateSessionStoreCache(storePath); } } let store = {}; let mtimeMs = getFileMtimeMs(storePath); const maxReadAttempts = process.platform === "win32" ? 3 : 1; const retryBuf = maxReadAttempts > 1 ? new Int32Array(new SharedArrayBuffer(4)) : void 0; for (let attempt = 0; attempt < maxReadAttempts; attempt++) try { const raw = fs.readFileSync(storePath, "utf-8"); if (raw.length === 0 && attempt < maxReadAttempts - 1) { Atomics.wait(retryBuf, 0, 0, 50); continue; } const parsed = JSON.parse(raw); if (isSessionStoreRecord(parsed)) store = parsed; mtimeMs = getFileMtimeMs(storePath) ?? mtimeMs; break; } catch { if (attempt < maxReadAttempts - 1) { Atomics.wait(retryBuf, 0, 0, 50); continue; } } for (const entry of Object.values(store)) { if (!entry || typeof entry !== "object") continue; const rec = entry; if (typeof rec.channel !== "string" && typeof rec.provider === "string") { rec.channel = rec.provider; delete rec.provider; } if (typeof rec.lastChannel !== "string" && typeof rec.lastProvider === "string") { rec.lastChannel = rec.lastProvider; delete rec.lastProvider; } if (typeof rec.groupChannel !== "string" && typeof rec.room === "string") { rec.groupChannel = rec.room; delete rec.room; } else if ("room" in rec) delete rec.room; } if (!opts.skipCache && isSessionStoreCacheEnabled()) SESSION_STORE_CACHE.set(storePath, { store: structuredClone(store), loadedAt: Date.now(), storePath, mtimeMs }); return structuredClone(store); } function readSessionUpdatedAt(params) { try { return loadSessionStore(params.storePath)[params.sessionKey]?.updatedAt; } catch { return; } } const DEFAULT_SESSION_PRUNE_AFTER_MS = 720 * 60 * 60 * 1e3; const DEFAULT_SESSION_MAX_ENTRIES = 500; const DEFAULT_SESSION_ROTATE_BYTES = 10485760; const DEFAULT_SESSION_MAINTENANCE_MODE = "warn"; function resolvePruneAfterMs(maintenance) { const raw = maintenance?.pruneAfter ?? maintenance?.pruneDays; if (raw === void 0 || raw === null || raw === "") return DEFAULT_SESSION_PRUNE_AFTER_MS; try { return parseDurationMs(String(raw).trim(), { defaultUnit: "d" }); } catch { return DEFAULT_SESSION_PRUNE_AFTER_MS; } } function resolveRotateBytes(maintenance) { const raw = maintenance?.rotateBytes; if (raw === void 0 || raw === null || raw === "") return DEFAULT_SESSION_ROTATE_BYTES; try { return parseByteSize(String(raw).trim(), { defaultUnit: "b" }); } catch { return DEFAULT_SESSION_ROTATE_BYTES; } } /** * Resolve maintenance settings from openclaw.json (`session.maintenance`). * Falls back to built-in defaults when config is missing or unset. */ function resolveMaintenanceConfig() { let maintenance; try { maintenance = loadConfig().session?.maintenance; } catch {} return { mode: maintenance?.mode ?? DEFAULT_SESSION_MAINTENANCE_MODE, pruneAfterMs: resolvePruneAfterMs(maintenance), maxEntries: maintenance?.maxEntries ?? DEFAULT_SESSION_MAX_ENTRIES, rotateBytes: resolveRotateBytes(maintenance) }; } /** * Remove entries whose `updatedAt` is older than the configured threshold. * Entries without `updatedAt` are kept (cannot determine staleness). * Mutates `store` in-place. */ function pruneStaleEntries(store, overrideMaxAgeMs, opts = {}) { const maxAgeMs = overrideMaxAgeMs ?? resolveMaintenanceConfig().pruneAfterMs; const cutoffMs = Date.now() - maxAgeMs; let pruned = 0; for (const [key, entry] of Object.entries(store)) if (entry?.updatedAt != null && entry.updatedAt < cutoffMs) { opts.onPruned?.({ key, entry }); delete store[key]; pruned++; } if (pruned > 0 && opts.log !== false) log.info("pruned stale session entries", { pruned, maxAgeMs }); return pruned; } /** * Cap the store to the N most recently updated entries. * Entries without `updatedAt` are sorted last (removed first when over limit). * Mutates `store` in-place. */ function getEntryUpdatedAt(entry) { return entry?.updatedAt ?? Number.NEGATIVE_INFINITY; } function getActiveSessionMaintenanceWarning(params) { const activeSessionKey = params.activeSessionKey.trim(); if (!activeSessionKey) return null; const activeEntry = params.store[activeSessionKey]; if (!activeEntry) return null; const cutoffMs = (params.nowMs ?? Date.now()) - params.pruneAfterMs; const wouldPrune = activeEntry.updatedAt != null ? activeEntry.updatedAt < cutoffMs : false; const keys = Object.keys(params.store); const wouldCap = keys.length > params.maxEntries && keys.toSorted((a, b) => getEntryUpdatedAt(params.store[b]) - getEntryUpdatedAt(params.store[a])).slice(params.maxEntries).includes(activeSessionKey); if (!wouldPrune && !wouldCap) return null; return { activeSessionKey, activeUpdatedAt: activeEntry.updatedAt, totalEntries: keys.length, pruneAfterMs: params.pruneAfterMs, maxEntries: params.maxEntries, wouldPrune, wouldCap }; } function capEntryCount(store, overrideMax, opts = {}) { const maxEntries = overrideMax ?? resolveMaintenanceConfig().maxEntries; const keys = Object.keys(store); if (keys.length <= maxEntries) return 0; const toRemove = keys.toSorted((a, b) => { const aTime = getEntryUpdatedAt(store[a]); return getEntryUpdatedAt(store[b]) - aTime; }).slice(maxEntries); for (const key of toRemove) delete store[key]; if (opts.log !== false) log.info("capped session entry count", { removed: toRemove.length, maxEntries }); return toRemove.length; } async function getSessionFileSize(storePath) { try { return (await fs.promises.stat(storePath)).size; } catch { return null; } } /** * Rotate the sessions file if it exceeds the configured size threshold. * Renames the current file to `sessions.json.bak.{timestamp}` and cleans up * old rotation backups, keeping only the 3 most recent `.bak.*` files. */ async function rotateSessionFile(storePath, overrideBytes) { const maxBytes = overrideBytes ?? resolveMaintenanceConfig().rotateBytes; const fileSize = await getSessionFileSize(storePath); if (fileSize == null) return false; if (fileSize <= maxBytes) return false; const backupPath = `${storePath}.bak.${Date.now()}`; try { await fs.promises.rename(storePath, backupPath); log.info("rotated session store file", { backupPath: path.basename(backupPath), sizeBytes: fileSize }); } catch { return false; } try { const dir = path.dirname(storePath); const baseName = path.basename(storePath); const backups = (await fs.promises.readdir(dir)).filter((f) => f.startsWith(`${baseName}.bak.`)).toSorted().toReversed(); const maxBackups = 3; if (backups.length > maxBackups) { const toDelete = backups.slice(maxBackups); for (const old of toDelete) await fs.promises.unlink(path.join(dir, old)).catch(() => void 0); log.info("cleaned up old session store backups", { deleted: toDelete.length }); } } catch {} return true; } async function saveSessionStoreUnlocked(storePath, store, opts) { invalidateSessionStoreCache(storePath); normalizeSessionStore(store); if (!opts?.skipMaintenance) { const maintenance = resolveMaintenanceConfig(); if (maintenance.mode === "warn") { const activeSessionKey = opts?.activeSessionKey?.trim(); if (activeSessionKey) { const warning = getActiveSessionMaintenanceWarning({ store, activeSessionKey, pruneAfterMs: maintenance.pruneAfterMs, maxEntries: maintenance.maxEntries }); if (warning) { log.warn("session maintenance would evict active session; skipping enforcement", { activeSessionKey: warning.activeSessionKey, wouldPrune: warning.wouldPrune, wouldCap: warning.wouldCap, pruneAfterMs: warning.pruneAfterMs, maxEntries: warning.maxEntries }); await opts?.onWarn?.(warning); } } } else { const prunedSessionFiles = /* @__PURE__ */ new Map(); pruneStaleEntries(store, maintenance.pruneAfterMs, { onPruned: ({ entry }) => { if (!prunedSessionFiles.has(entry.sessionId) || entry.sessionFile) prunedSessionFiles.set(entry.sessionId, entry.sessionFile); } }); capEntryCount(store, maintenance.maxEntries); const archivedDirs = /* @__PURE__ */ new Set(); for (const [sessionId, sessionFile] of prunedSessionFiles) { const archived = archiveSessionTranscripts({ sessionId, storePath, sessionFile, reason: "deleted" }); for (const archivedPath of archived) archivedDirs.add(path.dirname(archivedPath)); } if (archivedDirs.size > 0) await cleanupArchivedSessionTranscripts({ directories: [...archivedDirs], olderThanMs: maintenance.pruneAfterMs, reason: "deleted" }); await rotateSessionFile(storePath, maintenance.rotateBytes); } } await fs.promises.mkdir(path.dirname(storePath), { recursive: true }); const json = JSON.stringify(store, null, 2); if (process.platform === "win32") { const tmp = `${storePath}.${process.pid}.${crypto.randomUUID()}.tmp`; try { await fs.promises.writeFile(tmp, json, "utf-8"); for (let i = 0; i < 5; i++) try { await fs.promises.rename(tmp, storePath); break; } catch { if (i < 4) await new Promise((r) => setTimeout(r, 50 * (i + 1))); if (i === 4) console.warn(`[session-store] rename failed after 5 attempts: ${storePath}`); } } catch (err) { if ((err && typeof err === "object" && "code" in err ? String(err.code) : null) === "ENOENT") return; throw err; } finally { await fs.promises.rm(tmp, { force: true }).catch(() => void 0); } return; } const tmp = `${storePath}.${process.pid}.${crypto.randomUUID()}.tmp`; try { await fs.promises.writeFile(tmp, json, { mode: 384, encoding: "utf-8" }); await fs.promises.rename(tmp, storePath); await fs.promises.chmod(storePath, 384); } catch (err) { if ((err && typeof err === "object" && "code" in err ? String(err.code) : null) === "ENOENT") { try { await fs.promises.mkdir(path.dirname(storePath), { recursive: true }); await fs.promises.writeFile(storePath, json, { mode: 384, encoding: "utf-8" }); await fs.promises.chmod(storePath, 384); } catch (err2) { if ((err2 && typeof err2 === "object" && "code" in err2 ? String(err2.code) : null) === "ENOENT") return; throw err2; } return; } throw err; } finally { await fs.promises.rm(tmp, { force: true }); } } async function updateSessionStore(storePath, mutator, opts) { return await withSessionStoreLock(storePath, async () => { const store = loadSessionStore(storePath, { skipCache: true }); const result = await mutator(store); await saveSessionStoreUnlocked(storePath, store, opts); return result; }); } const LOCK_QUEUES = /* @__PURE__ */ new Map(); function lockTimeoutError(storePath) { return /* @__PURE__ */ new Error(`timeout waiting for session store lock: ${storePath}`); } function getOrCreateLockQueue(storePath) { const existing = LOCK_QUEUES.get(storePath); if (existing) return existing; const created = { running: false, pending: [] }; LOCK_QUEUES.set(storePath, created); return created; } async function drainSessionStoreLockQueue(storePath) { const queue = LOCK_QUEUES.get(storePath); if (!queue || queue.running) return; queue.running =