@gguf/claw
Version:
Multi-channel AI gateway with extensible messaging integrations
1,388 lines (1,373 loc) • 71.7 kB
JavaScript
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