UNPKG

@gguf/claw

Version:

Multi-channel AI gateway with extensible messaging integrations

1,388 lines (1,373 loc) 71.7 kB
import { o as createSubsystemLogger, pn as resolveRequiredHomeDir, tt as normalizeE164 } from "./entry.js"; import { D as resolveProcessScopedMap, O as isPidAlive } from "./auth-profiles-DFa1zzNy.js"; import { i as buildAgentMainSessionKey, l as normalizeAgentId, n as DEFAULT_AGENT_ID, u as normalizeMainKey } from "./session-key-BGiG_JcT.js"; import { C as parseDurationMs, f as parseByteSize, i as loadConfig } from "./config-B2kL1ciP.js"; import { c as listDeliverableMessageChannels, l as normalizeMessageChannel } from "./message-channel-CVHJDItx.js"; import { r as hasInterSessionUserProvenance } from "./input-provenance-D0lNkCf6.js"; import { t as normalizeChatType } from "./chat-type-CeFzWU-6.js"; import { r as normalizeChannelId } from "./plugins-RqhjLCb6.js"; import { c as normalizeHyphenSlug, t as getChannelDock } from "./dock-BZuwgj1O.js"; import { n as resolveConversationLabel } from "./conversation-label-B7Ya7NPL.js"; import { a as resolveSessionTranscriptPathInDir, c as resolveStorePath, i as resolveSessionTranscriptPath, n as resolveSessionFilePath, t as resolveDefaultSessionStorePath } from "./paths-CXpciDEv.js"; import { n as extractToolCallNames, r as hasToolCall } from "./transcript-tools-DZf7NLcg.js"; import { t as emitSessionTranscriptUpdate } from "./transcript-events-CMT5U7Om.js"; import os from "node:os"; import path from "node:path"; import fs from "node:fs"; import fs$1 from "node:fs/promises"; import crypto from "node:crypto"; import { CURRENT_SESSION_VERSION, SessionManager } from "@mariozechner/pi-coding-agent"; //#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/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 cleanStaleLockFiles(params) { const sessionsDir = path.resolve(params.sessionsDir); const staleMs = resolvePositiveMs(params.staleMs, DEFAULT_STALE_MS); const removeStale = params.removeStale !== false; const nowMs = params.nowMs ?? Date.now(); let entries = []; try { entries = await fs$1.readdir(sessionsDir, { withFileTypes: true }); } catch (err) { if (err.code === "ENOENT") return { locks: [], cleaned: [] }; throw err; } const locks = []; const cleaned = []; const lockEntries = entries.filter((entry) => entry.name.endsWith(".jsonl.lock")).toSorted((a, b) => a.name.localeCompare(b.name)); for (const entry of lockEntries) { const lockPath = path.join(sessionsDir, entry.name); const lockInfo = { lockPath, ...inspectLockPayload(await readLockPayload(lockPath), staleMs, nowMs), removed: false }; if (lockInfo.stale && removeStale) { await fs$1.rm(lockPath, { force: true }); lockInfo.removed = true; cleaned.push(lockInfo); params.log?.warn?.(`removed stale session lock: ${lockPath} (${lockInfo.staleReasons.join(", ") || "unknown"})`); } locks.push(lockInfo); } return { locks, cleaned }; } 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/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 snapshotSessionOrigin(entry) { if (!entry?.origin) return; return { ...entry.origin }; } 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 resolveMainSessionKeyFromConfig() { return resolveMainSessionKey(loadConfig()); } 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/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("|")); /** * Remove all injected inbound metadata prefix blocks from `text`. * * Each block has the shape: * * ``` * <sentinel-line> * ```json * { … } * ``` * ``` * * Returns the original string reference unchanged when no metadata is present * (fast path — zero allocation). */ function stripInboundMetadata(text) { if (!text || !SENTINEL_FAST_RE.test(text)) return text; const lines = text.split("\n"); const result = []; let inMetaBlock = false; let inFencedJson = false; for (let i = 0; i < lines.length; i++) { const line = lines[i]; if (!inMetaBlock && INBOUND_META_SENTINELS.some((s) => line.startsWith(s))) { inMetaBlock = true; inFencedJson = false; continue; } if (inMetaBlock) { if (!inFencedJson && line.trim() === "```json") { inFencedJson = true; continue; } if (inFencedJson) { if (line.trim() === "```") { inMetaBlock = false; inFencedJson = false; } continue; } if (line.trim() === "") continue; inMetaBlock = false; } result.push(line); } return result.join("\n").replace(/^\n+/, ""); } function stripLeadingInboundMetadata(text) { if (!text || !SENTINEL_FAST_RE.test(text)) return text; const lines = text.split("\n"); let index = 0; while (index < lines.length && lines[index] === "") index++; if (index >= lines.length) return ""; if (!INBOUND_META_SENTINELS.some((s) => lines[index].startsWith(s))) return text; while (index < lines.length) { const line = lines[index]; if (!INBOUND_META_SENTINELS.some((s) => line.startsWith(s))) break; index++; if (index < lines.length && lines[index].trim() === "```json") { index++; while (index < lines.length && lines[index].trim() !== "```") index++; if (index < lines.length && lines[index].trim() === "```") index++; } else return text; while (index < lines.length && lines[index].trim() === "") index++; } return lines.slice(index).join("\n"); } //#endregion //#region src/shared/chat-envelope.ts const ENVELOPE_PREFIX = /^\[([^\]]+)\]\s*/; const ENVELOPE_CHANNELS = [ "WebChat", "WhatsApp", "Telegram", "Signal", "Slack", "Discord", "Google Chat", "iMessage", "Teams", "Matrix", "Zalo", "Zalo Personal", "BlueBubbles" ]; const MESSAGE_ID_LINE = /^\s*\[message_id:\s*[^\]]+\]\s*$/i; function looksLikeEnvelopeHeader(header) { if (/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}Z\b/.test(header)) return true; if (/\d{4}-\d{2}-\d{2} \d{2}:\d{2}\b/.test(header)) return true; return ENVELOPE_CHANNELS.some((label) => header.startsWith(`${label} `)); } function stripEnvelope(text) { const match = text.match(ENVELOPE_PREFIX); if (!match) return text; if (!looksLikeEnvelopeHeader(match[1] ?? "")) return text; return text.slice(match[0].length); } function stripMessageIdHints(text) { if (!text.includes("[message_id:")) return text; const lines = text.split(/\r?\n/); const filtered = lines.filter((line) => !MESSAGE_ID_LINE.test(line)); return filtered.length === lines.length ? text : filtered.join("\n"); } //#endregion //#region src/gateway/chat-sanitize.ts function stripEnvelopeFromContent(content) { let changed = false; return { content: content.map((item) => { if (!item || typeof item !== "object") return item; const entry = item; if (entry.type !== "text" || typeof entry.text !== "string") return item; const stripped = stripMessageIdHints(stripEnvelope(stripInboundMetadata(entry.text))); if (stripped === entry.text) return item; changed = true; return { ...entry, text: stripped }; }), changed }; } function stripEnvelopeFromMessage(message) { if (!message || typeof message !== "object") return message; const entry = message; if ((typeof entry.role === "string" ? entry.role.toLowerCase() : "") !== "user") return message; let changed = false; const next = { ...entry }; if (typeof entry.content === "string") { const stripped = stripMessageIdHints(stripEnvelope(stripInboundMetadata(entry.content))); if (stripped !== entry.content) { next.content = stripped; changed = true; } } else if (Array.isArray(entry.content)) { const updated = stripEnvelopeFromContent(entry.content); if (updated.changed) { next.content = updated.content; changed = true; } } else if (typeof entry.text === "string") { const stripped = stripMessageIdHints(stripEnvelope(stripInboundMetadata(entry.text))); if (stripped !== entry.text) { next.text = stripped; changed = true; } } return changed ? next : message; } function stripEnvelopeFromMessages(messages) { if (messages.length === 0) return messages; let changed = false; const next = messages.map((message) => { const stripped = stripEnvelopeFromMessage(message); if (stripped !== message) changed = true; return stripped; }); return changed ? next : messages; } //#endregion //#region src/gateway/session-utils.fs.ts const sessionTitleFieldsCache = /* @__PURE__ */ new Map(); const MAX_SESSION_TITLE_FIELDS_CACHE_ENTRIES = 5e3; function readSessionTitleFieldsCacheKey(filePath, opts) { return `${filePath}\t${opts?.includeInterSession === true ? "1" : "0"}`; } function getCachedSessionTitleFields(cacheKey, stat) { const cached = sessionTitleFieldsCache.get(cacheKey); if (!cached) return null; if (cached.mtimeMs !== stat.mtimeMs || cached.size !== stat.size) { sessionTitleFieldsCache.delete(cacheKey); return null; } sessionTitleFieldsCache.delete(cacheKey); sessionTitleFieldsCache.set(cacheKey, cached); return { firstUserMessage: cached.firstUserMessage, lastMessagePreview: cached.lastMessagePreview }; } function setCachedSessionTitleFields(cacheKey, stat, value) { sessionTitleFieldsCache.set(cacheKey, { ...value, mtimeMs: stat.mtimeMs, size: stat.size }); while (sessionTitleFieldsCache.size > MAX_SESSION_TITLE_FIELDS_CACHE_ENTRIES) { const oldestKey = sessionTitleFieldsCache.keys().next().value; if (typeof oldestKey !== "string" || !oldestKey) break; sessionTitleFieldsCache.delete(oldestKey); } } function readSessionMessages(sessionId, storePath, sessionFile) { const filePath = resolveSessionTranscriptCandidates(sessionId, storePath, sessionFile).find((p) => fs.existsSync(p)); if (!filePath) return []; const lines = fs.readFileSync(filePath, "utf-8").split(/\r?\n/); const messages = []; for (const line of lines) { if (!line.trim()) continue; try { const parsed = JSON.parse(line); if (parsed?.message) { messages.push(parsed.message); continue; } if (parsed?.type === "compaction") { const ts = typeof parsed.timestamp === "string" ? Date.parse(parsed.timestamp) : NaN; const timestamp = Number.isFinite(ts) ? ts : Date.now(); messages.push({ role: "system", content: [{ type: "text", text: "Compaction" }], timestamp, __openclaw: { kind: "compaction", id: typeof parsed.id === "string" ? parsed.id : void 0 } }); } } catch {} } return messages; } 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 MAX_LINES_TO_SCAN = 10; function readSessionTitleFieldsFromTranscript(sessionId, storePath, sessionFile, agentId, opts) { const filePath = resolveSessionTranscriptCandidates(sessionId, storePath, sessionFile, agentId).find((p) => fs.existsSync(p)); if (!filePath) return { firstUserMessage: null, lastMessagePreview: null }; let stat; try { stat = fs.statSync(filePath); } catch { return { firstUserMessage: null, lastMessagePreview: null }; } const cacheKey = readSessionTitleFieldsCacheKey(filePath, opts); const cached = getCachedSessionTitleFields(cacheKey, stat); if (cached) return cached; if (stat.size === 0) { const empty = { firstUserMessage: null, lastMessagePreview: null }; setCachedSessionTitleFields(cacheKey, stat, empty); return empty; } let fd = null; try { fd = fs.openSync(filePath, "r"); const size = stat.size; let firstUserMessage = null; try { const chunk = readTranscriptHeadChunk(fd); if (chunk) firstUserMessage = extractFirstUserMessageFromTranscriptChunk(chunk, opts); } catch {} let lastMessagePreview = null; try { lastMessagePreview = readLastMessagePreviewFromOpenTranscript({ fd, size }); } catch {} const result = { firstUserMessage, lastMessagePreview }; setCachedSessionTitleFields(cacheKey, stat, result); return result; } catch { return { firstUserMessage: null, lastMessagePreview: null }; } finally { if (fd !== null) try { fs.closeSync(fd); } catch {} } } function extractTextFromContent(content) { if (typeof content === "string") return content.trim() || null; if (!Array.isArray(content)) return null; for (const part of content) { if (!part || typeof part.text !== "string") continue; if (part.type === "text" || part.type === "output_text" || part.type === "input_text") { const trimmed = part.text.trim(); if (trimmed) return trimmed; } } return null; } function readTranscriptHeadChunk(fd, maxBytes = 8192) { const buf = Buffer.alloc(maxBytes); const bytesRead = fs.readSync(fd, buf, 0, buf.length, 0); if (bytesRead <= 0) return null; return buf.toString("utf-8", 0, bytesRead); } function extractFirstUserMessageFromTranscriptChunk(chunk, opts) { const lines = chunk.split(/\r?\n/).slice(0, MAX_LINES_TO_SCAN); for (const line of lines) { if (!line.trim()) continue; try { const msg = JSON.parse(line)?.message; if (msg?.role !== "user") continue; if (opts?.includeInterSession !== true && hasInterSessionUserProvenance(msg)) continue; const text = extractTextFromContent(msg.content); if (text) return text; } catch {} } return null; } const LAST_MSG_MAX_BYTES = 16384; const LAST_MSG_MAX_LINES = 20; function readLastMessagePreviewFromOpenTranscript(params) { const readStart = Math.max(0, params.size - LAST_MSG_MAX_BYTES); const readLen = Math.min(params.size, LAST_MSG_MAX_BYTES); const buf = Buffer.alloc(readLen); fs.readSync(params.fd, buf, 0, readLen, readStart); const tailLines = buf.toString("utf-8").split(/\r?\n/).filter((l) => l.trim()).slice(-LAST_MSG_MAX_LINES); for (let i = tailLines.length - 1; i >= 0; i--) { const line = tailLines[i]; try { const msg = JSON.parse(line)?.message; if (msg?.role !== "user" && msg?.role !== "assistant") continue; const text = extractTextFromContent(msg.content); if (text) return text; } catch {} } return null; } const PREVIEW_READ_SIZES = [ 64 * 1024, 256 * 1024, 1024 * 1024 ]; const PREVIEW_MAX_LINES = 200; function normalizeRole(role, isTool) { if (isTool) return "tool"; switch ((role ?? "").toLowerCase()) { case "user": return "user"; case "assistant": return "assistant"; case "system": return "system"; case "tool": return "tool"; default: return "other"; } } function truncatePreviewText(text, maxChars) { if (maxChars <= 0 || text.length <= maxChars) return text; if (maxChars <= 3) return text.slice(0, maxChars); return `${text.slice(0, maxChars - 3)}...`; } function extractPreviewText(message) { if (typeof message.content === "string") { const trimmed = message.content.trim(); return trimmed ? trimmed : null; } if (Array.isArray(message.content)) { const parts = message.content.map((entry) => typeof entry?.text === "string" ? entry.text : "").filter((text) => text.trim().length > 0); if (parts.length > 0) return parts.join("\n").trim(); } if (typeof message.text === "string") { const trimmed = message.text.trim(); return trimmed ? trimmed : null; } return null; } function isToolCall(message) { return hasToolCall(message); } function extractToolNames(message) { return extractToolCallNames(message); } function extractMediaSummary(message) { if (!Array.isArray(message.content)) return null; for (const entry of message.content) { const raw = typeof entry?.type === "string" ? entry.type.trim().toLowerCase() : ""; if (!raw || raw === "text" || raw === "toolcall" || raw === "tool_call") continue; return `[${raw}]`; } return null; } function buildPreviewItems(messages, maxItems, maxChars) { const items = []; for (const message of messages) { const toolCall = isToolCall(message); const role = normalizeRole(message.role, toolCall); let text = extractPreviewText(message); if (!text) { const toolNames = extractToolNames(message); if (toolNames.length > 0) { const shown = toolNames.slice(0, 2); const overflow = toolNames.length - shown.length; text = `call ${shown.join(", ")}`; if (overflow > 0) text += ` +${overflow}`; } } if (!text) text = extractMediaSummary(message); if (!text) continue; let trimmed = text.trim(); if (!trimmed) continue; if (role === "user") trimmed = stripEnvelope(trimmed); trimmed = truncatePreviewText(trimmed, maxChars); items.push({ role, text: trimmed }); } if (items.length <= maxItems) return items; return items.slice(-maxItems); } function readRecentMessagesFromTranscript(filePath, maxMessages, readBytes) { let fd = null; try { fd = fs.openSync(filePath, "r"); const size = fs.fstatSync(fd).size; if (size === 0) return []; const readStart = Math.max(0, size - readBytes); const readLen = Math.min(size, readBytes); const buf = Buffer.alloc(readLen); fs.readSync(fd, buf, 0, readLen, readStart); const tailLines = buf.toString("utf-8").split(/\r?\n/).filter((l) => l.trim()).slice(-PREVIEW_MAX_LINES); const collected = []; for (let i = tailLines.length - 1; i >= 0; i--) { const line = tailLines[i]; try { const msg = JSON.parse(line)?.message; if (msg && typeof msg === "object") { collected.push(msg); if (collected.length >= maxMessages) break; } } catch {} } return collected.toReversed(); } catch { return []; } finally { if (fd !== null) fs.closeSync(fd); } } function readSessionPreviewItemsFromTranscript(sessionId, storePath, sessionFile, agentId, maxItems, maxChars) { const filePath = resolveSessionTranscriptCandidates(sessionId, storePath, sessionFile, agentId).find((p) => fs.existsSync(p)); if (!filePath) return []; const boundedItems = Math.max(1, Math.min(maxItems, 50)); const boundedChars = Math.max(20, Math.min(maxChars, 2e3)); for (const readSize of PREVIEW_READ_SIZES) { const messages = readRecentMessagesFromTranscript(filePath, boundedItems, readSize); if (messages.length > 0 || readSize === PREVIEW_READ_SIZES[PREVIEW_READ_SIZES.length - 1]) return buildPreviewItems(messages, boundedItems, boundedChars); } return []; } //#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(retr