openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
219 lines (218 loc) • 9.91 kB
JavaScript
import { l as normalizeOptionalString } from "../../string-coerce-CIXf7egm.js";
import { n as isVitestRuntimeEnv } from "../../test-runtime-env-DQDRzsLt.js";
import "../../env-M3R40TOb.js";
import { w as root } from "../../fs-safe-B6pvPGnf.js";
import { m as shortenHomePath } from "../../utils-P__uGsPB.js";
import { m as resolveAgentWorkspaceDir } from "../../agent-scope-config-DcbEhP0R.js";
import { w as resolveStateDir } from "../../paths-D2sRr1a_.js";
import { O as parseAgentSessionKey, b as toAgentStoreSessionKey } from "../../session-key-BnWWjqNc.js";
import { t as createSubsystemLogger } from "../../subsystem-Dy2tqXOS.js";
import { o as resolveSessionStorePathCore } from "../../paths-CXdaYWF_.js";
import { l as resolveAgentIdByWorkspacePath } from "../../agent-scope-DbtJyKUL.js";
import { x as runWithGatewayIndependentRootWorkContinuation } from "../../gateway-work-admission-R1IpuDim.js";
import { o as resolveUserTimezone } from "../../date-time-C7YbYLih.js";
import { p as createMemoryWriteProvenanceObserver } from "../../tool-loop-detection-config-Bg911KiY.js";
import { t as generateSlugViaLLM } from "../../llm-slug-generator-ndgdJkMk.js";
import { i as resolveHookConfig } from "../../config-DWWH6LIu.js";
import { t as captureSessionMemoryTranscript } from "../../capture-CE5KKlzx.js";
import { r as isSessionAutoResetReason } from "../../session-auto-reset-BxcwX9Yg.js";
import path from "node:path";
import fs from "node:fs/promises";
import os from "node:os";
//#region src/hooks/bundled/session-memory/handler.ts
/**
* Session memory hook handler
*
* Saves session context to memory when /new or /reset command is triggered
* Creates a new dated memory file with a timestamp slug by default
*/
const log = createSubsystemLogger("hooks/session-memory");
function pickDateTimePart(parts, type) {
return parts.find((part) => part.type === type)?.value;
}
function formatLocalSessionTimestamp(date, timeZone) {
const parts = new Intl.DateTimeFormat("en-US", {
timeZone,
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
hourCycle: "h23"
}).formatToParts(date);
const year = pickDateTimePart(parts, "year") ?? String(date.getFullYear()).padStart(4, "0");
const month = pickDateTimePart(parts, "month") ?? String(date.getMonth() + 1).padStart(2, "0");
const day = pickDateTimePart(parts, "day") ?? String(date.getDate()).padStart(2, "0");
const hour = pickDateTimePart(parts, "hour") ?? String(date.getHours()).padStart(2, "0");
const minute = pickDateTimePart(parts, "minute") ?? String(date.getMinutes()).padStart(2, "0");
const second = pickDateTimePart(parts, "second") ?? String(date.getSeconds()).padStart(2, "0");
return {
date: `${year}-${month}-${day}`,
time: `${hour}:${minute}:${second}`,
timeSlug: `${hour}${minute}`
};
}
async function resolveAvailableMemoryFilename(params) {
const basename = `${params.dateStr}-${params.slug}`;
let suffix = 1;
while (true) {
const filename = suffix === 1 ? `${basename}.md` : `${basename}-${suffix}.md`;
try {
await fs.access(path.join(params.memoryDir, filename));
suffix += 1;
} catch (err) {
if (err.code === "ENOENT") return filename;
throw err;
}
}
}
function resolveDisplaySessionKey(params) {
if (!params.cfg || !params.workspaceDir) return params.sessionKey;
const workspaceAgentId = resolveAgentIdByWorkspacePath(params.cfg, params.workspaceDir);
const parsed = parseAgentSessionKey(params.sessionKey);
if (!workspaceAgentId || !parsed || workspaceAgentId === parsed.agentId) return params.sessionKey;
return toAgentStoreSessionKey({
agentId: workspaceAgentId,
requestKey: parsed.rest
});
}
const pendingSessionMemoryWrites = /* @__PURE__ */ new Set();
function requireSessionMemoryAgentId(event) {
const agentId = normalizeOptionalString(event.context?.agentId);
if (!agentId) throw new Error("Session memory hook contract requires context.agentId");
return agentId;
}
async function flushSessionMemoryWritesForTest() {
await Promise.allSettled(pendingSessionMemoryWrites);
}
async function saveSessionMemoryNow(event, agentId, transcript) {
try {
log.debug("Session memory hook triggered", {
action: event.action,
type: event.type
});
const context = event.context || {};
const cfg = context.cfg;
const contextWorkspaceDir = typeof context.workspaceDir === "string" && context.workspaceDir.trim().length > 0 ? context.workspaceDir : void 0;
const workspaceDir = contextWorkspaceDir || (cfg ? resolveAgentWorkspaceDir(cfg, agentId) : path.join(resolveStateDir(process.env, os.homedir), "workspace"));
const displaySessionKey = resolveDisplaySessionKey({
cfg,
workspaceDir: contextWorkspaceDir,
sessionKey: event.sessionKey
});
const memoryDir = path.join(workspaceDir, "memory");
await fs.mkdir(memoryDir, { recursive: true });
const now = new Date(event.timestamp);
const userTimezone = resolveUserTimezone(cfg?.agents?.defaults?.userTimezone ?? process.env.TZ);
const localTimestamp = formatLocalSessionTimestamp(now, userTimezone);
const dateStr = localTimestamp.date;
const sessionEntry = event.type === "command" ? context.previousSessionEntry || context.sessionEntry || {} : context.sessionEntry || {};
const currentSessionId = typeof sessionEntry.sessionId === "string" && sessionEntry.sessionId.trim() ? sessionEntry.sessionId.trim() : void 0;
log.debug("Session context resolved", {
sessionId: currentSessionId,
hasCfg: Boolean(cfg)
});
const hookConfig = resolveHookConfig(cfg, "session-memory");
let slug = null;
if (transcript.status === "unavailable") log.warn("Session transcript unavailable for memory capture", {
sessionKey: event.sessionKey,
error: transcript.reason
});
if (currentSessionId) {
const allowLlmSlug = !isVitestRuntimeEnv() && hookConfig?.llmSlug === true;
if (transcript.status === "available" && transcript.content && cfg && allowLlmSlug) {
log.debug("Calling generateSlugViaLLM...");
const slugModel = typeof hookConfig?.model === "string" ? hookConfig.model : void 0;
slug = await generateSlugViaLLM({
sessionContent: transcript.content,
cfg,
agentId,
model: slugModel
});
log.debug("Generated slug", { slug });
}
}
if (!slug) {
slug = localTimestamp.timeSlug;
log.debug("Using fallback timestamp slug", { slug });
}
const filename = await resolveAvailableMemoryFilename({
memoryDir,
dateStr,
slug
});
const memoryFilePath = path.join(memoryDir, filename);
log.debug("Memory file path resolved", {
filename,
path: shortenHomePath(memoryFilePath)
});
const timeStr = localTimestamp.time;
const sessionId = sessionEntry.sessionId || "unknown";
const boundaryDetail = event.type === "session" ? `- **Reason**: ${context.reason || "unknown"}` : `- **Source**: ${context.commandSource || "unknown"}`;
const entryParts = [
`# Session: ${dateStr} ${timeStr} ${userTimezone}`,
"",
`- **Session Key**: ${displaySessionKey}`,
`- **Session ID**: ${sessionId}`,
boundaryDetail,
""
];
if (transcript.status === "available" && transcript.content) entryParts.push("## Conversation Summary", "", transcript.content, "");
else if (transcript.status === "unavailable") entryParts.push("## Conversation Summary", "", `> Transcript content was unavailable: ${JSON.stringify(transcript.reason)}`, "");
const entry = entryParts.join("\n");
const memoryRoot = await root(memoryDir);
const provenanceObserver = createMemoryWriteProvenanceObserver({
mutationRoot: workspaceDir,
workspaceDir,
resolveOriginClass: () => transcript.status === "available" ? transcript.originClass : "agent",
sessionId: currentSessionId,
sessionKey: event.sessionKey,
now: () => now.getTime()
});
const commit = () => memoryRoot.write(filename, entry, { encoding: "utf-8" });
await provenanceObserver.write({
absolutePath: memoryFilePath,
contentBefore: "",
contentAfter: entry,
commit
});
log.debug("Memory file written successfully");
const relPath = shortenHomePath(memoryFilePath);
log.info(`Session context saved to ${relPath}`);
} catch (err) {
if (err instanceof Error) log.error("Failed to save session memory", {
errorName: err.name,
errorMessage: err.message,
stack: err.stack
});
else log.error("Failed to save session memory", { error: String(err) });
}
}
const saveSessionToMemory = (event) => {
const isResetCommand = event.action === "new" || event.action === "reset";
const isAutoReset = event.type === "session" && event.action === "auto-reset" && isSessionAutoResetReason(event.context.reason);
if ((event.type !== "command" || !isResetCommand) && !isAutoReset) return;
const agentId = requireSessionMemoryAgentId(event);
const context = event.context;
const sessionEntry = event.type === "command" ? context.previousSessionEntry ?? context.sessionEntry : context.sessionEntry;
const cfg = context.cfg;
const transcript = context.previousSessionMemory ?? (sessionEntry?.sessionId ? captureSessionMemoryTranscript({
agentId,
sessionId: sessionEntry.sessionId,
sessionKey: event.sessionKey,
storePath: typeof context.storePath === "string" && context.storePath.trim() ? context.storePath.trim() : resolveSessionStorePathCore(cfg?.session?.store, { agentId })
}, cfg) : {
status: "available",
content: null,
originClass: "agent"
});
const writePromise = isAutoReset ? saveSessionMemoryNow(event, agentId, transcript) : runWithGatewayIndependentRootWorkContinuation(() => saveSessionMemoryNow(event, agentId, transcript), "hooks:session-memory");
pendingSessionMemoryWrites.add(writePromise);
writePromise.finally(() => {
pendingSessionMemoryWrites.delete(writePromise);
});
if (isAutoReset) return writePromise;
};
//#endregion
export { saveSessionToMemory as default, flushSessionMemoryWritesForTest };