@gguf/claw
Version:
Multi-channel AI gateway with extensible messaging integrations
246 lines (244 loc) • 10.3 kB
JavaScript
import { s as resolveStateDir } from "../../paths-CyR9Pa1R.js";
import { d as resolveAgentIdFromSessionKey } from "../../session-key-CgcjHuX_.js";
import "../../registry-BmY4gNy6.js";
import { s as resolveAgentWorkspaceDir } from "../../agent-scope-BLYwrEEA.js";
import { t as createSubsystemLogger } from "../../subsystem-B5g771Td.js";
import "../../workspace-D9IzNnST.js";
import "../../tokens-BRx7o_ZS.js";
import "../../pi-embedded-BDHP7G3o.js";
import "../../normalize-DZvZ0Eiv.js";
import "../../accounts-D7irbKbS.js";
import "../../boolean-B8-BqKGQ.js";
import "../../command-format-Ck2WauwF.js";
import "../../bindings-DWtXcdc8.js";
import "../../send-CbPQXDCf.js";
import "../../plugins-CJYXNO4s.js";
import "../../send-VMzh7zM1.js";
import "../../deliver-C5sD6Dv0.js";
import "../../diagnostic-CoT2YQH4.js";
import "../../diagnostic-session-state-DmrztgHU.js";
import "../../accounts-DSVHyeYh.js";
import "../../send-xJGV2Pmu.js";
import "../../fs-safe-B8-WkEXM.js";
import "../../model-auth-BPNt9bHS.js";
import "../../github-copilot-token-BQ98eazZ.js";
import "../../pi-model-discovery-C-yOXpma.js";
import "../../message-channel-BDlvrUZF.js";
import { ot as hasInterSessionUserProvenance } from "../../pi-embedded-helpers-BfjCe8vu.js";
import "../../config-C0k_b9gg.js";
import "../../manifest-registry-Dovhiqzt.js";
import "../../common-y9ALHdVZ.js";
import "../../chrome-DVeI6N8j.js";
import "../../frontmatter-BT4H5SZG.js";
import "../../skills-Ci_2Mah4.js";
import "../../redact-BBbIZgau.js";
import "../../errors-3KjSwJLH.js";
import "../../ssrf-B8OrDkCk.js";
import "../../store-Be0QxLPZ.js";
import "../../thinking-Bfk6_7EU.js";
import "../../accounts-C5646aU0.js";
import "../../paths-gnW-md4M.js";
import "../../image-Do9rxbj3.js";
import "../../reply-prefix-BaCBHwi7.js";
import "../../manager-D7zJM2Ly.js";
import "../../gemini-auth-DkBnXstb.js";
import "../../sqlite-DJh2xLr6.js";
import "../../retry-BxB_D6Pn.js";
import "../../chunk-TRVny4T1.js";
import "../../markdown-tables-DO9qAEKa.js";
import "../../local-roots-tMzVEQD2.js";
import "../../ir-CRTCIHIG.js";
import "../../render-CDCvpfhh.js";
import "../../commands-registry-DqNdrksb.js";
import "../../skill-commands-pI2wkJrZ.js";
import "../../runner-CLSBcNny.js";
import "../../fetch-DtI0mtzx.js";
import "../../send-hD-4IHpX.js";
import "../../outbound-attachment-BChLBLPg.js";
import "../../send-Cm_4TZEY.js";
import "../../resolve-route-ClURVRZM.js";
import "../../channel-activity-LT3oBvO7.js";
import "../../tables-xDr7OSBa.js";
import "../../proxy-CBJ1upuz.js";
import "../../replies-BMmKlXtC.js";
import { generateSlugViaLLM } from "../../llm-slug-generator.js";
import { t as resolveHookConfig } from "../../config-ClSsetRi.js";
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
//#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 LLM-generated slug
*/
const log = createSubsystemLogger("hooks/session-memory");
/**
* Read recent messages from session file for slug generation
*/
async function getRecentSessionContent(sessionFilePath, messageCount = 15) {
try {
const lines = (await fs.readFile(sessionFilePath, "utf-8")).trim().split("\n");
const allMessages = [];
for (const line of lines) try {
const entry = JSON.parse(line);
if (entry.type === "message" && entry.message) {
const msg = entry.message;
const role = msg.role;
if ((role === "user" || role === "assistant") && msg.content) {
if (role === "user" && hasInterSessionUserProvenance(msg)) continue;
const text = Array.isArray(msg.content) ? msg.content.find((c) => c.type === "text")?.text : msg.content;
if (text && !text.startsWith("/")) allMessages.push(`${role}: ${text}`);
}
}
} catch {}
return allMessages.slice(-messageCount).join("\n");
} catch {
return null;
}
}
/**
* Try the active transcript first; if /new already rotated it,
* fallback to the latest .jsonl.reset.* sibling.
*/
async function getRecentSessionContentWithResetFallback(sessionFilePath, messageCount = 15) {
const primary = await getRecentSessionContent(sessionFilePath, messageCount);
if (primary) return primary;
try {
const dir = path.dirname(sessionFilePath);
const resetPrefix = `${path.basename(sessionFilePath)}.reset.`;
const resetCandidates = (await fs.readdir(dir)).filter((name) => name.startsWith(resetPrefix)).toSorted();
if (resetCandidates.length === 0) return primary;
const latestResetPath = path.join(dir, resetCandidates[resetCandidates.length - 1]);
const fallback = await getRecentSessionContent(latestResetPath, messageCount);
if (fallback) log.debug("Loaded session content from reset fallback", {
sessionFilePath,
latestResetPath
});
return fallback || primary;
} catch {
return primary;
}
}
function stripResetSuffix(fileName) {
const resetIndex = fileName.indexOf(".reset.");
return resetIndex === -1 ? fileName : fileName.slice(0, resetIndex);
}
async function findPreviousSessionFile(params) {
try {
const files = await fs.readdir(params.sessionsDir);
const fileSet = new Set(files);
const baseFromReset = params.currentSessionFile ? stripResetSuffix(path.basename(params.currentSessionFile)) : void 0;
if (baseFromReset && fileSet.has(baseFromReset)) return path.join(params.sessionsDir, baseFromReset);
const trimmedSessionId = params.sessionId?.trim();
if (trimmedSessionId) {
const canonicalFile = `${trimmedSessionId}.jsonl`;
if (fileSet.has(canonicalFile)) return path.join(params.sessionsDir, canonicalFile);
const topicVariants = files.filter((name) => name.startsWith(`${trimmedSessionId}-topic-`) && name.endsWith(".jsonl") && !name.includes(".reset.")).toSorted().toReversed();
if (topicVariants.length > 0) return path.join(params.sessionsDir, topicVariants[0]);
}
if (!params.currentSessionFile) return;
const nonResetJsonl = files.filter((name) => name.endsWith(".jsonl") && !name.includes(".reset.")).toSorted().toReversed();
if (nonResetJsonl.length > 0) return path.join(params.sessionsDir, nonResetJsonl[0]);
} catch {}
}
/**
* Save session context to memory when /new or /reset command is triggered
*/
const saveSessionToMemory = async (event) => {
const isResetCommand = event.action === "new" || event.action === "reset";
if (event.type !== "command" || !isResetCommand) return;
try {
log.debug("Hook triggered for reset/new command", { action: event.action });
const context = event.context || {};
const cfg = context.cfg;
const agentId = resolveAgentIdFromSessionKey(event.sessionKey);
const workspaceDir = cfg ? resolveAgentWorkspaceDir(cfg, agentId) : path.join(resolveStateDir(process.env, os.homedir), "workspace");
const memoryDir = path.join(workspaceDir, "memory");
await fs.mkdir(memoryDir, { recursive: true });
const now = new Date(event.timestamp);
const dateStr = now.toISOString().split("T")[0];
const sessionEntry = context.previousSessionEntry || context.sessionEntry || {};
const currentSessionId = sessionEntry.sessionId;
let currentSessionFile = sessionEntry.sessionFile || void 0;
if (!currentSessionFile || currentSessionFile.includes(".reset.")) {
const sessionsDirs = /* @__PURE__ */ new Set();
if (currentSessionFile) sessionsDirs.add(path.dirname(currentSessionFile));
sessionsDirs.add(path.join(workspaceDir, "sessions"));
for (const sessionsDir of sessionsDirs) {
const recoveredSessionFile = await findPreviousSessionFile({
sessionsDir,
currentSessionFile,
sessionId: currentSessionId
});
if (!recoveredSessionFile) continue;
currentSessionFile = recoveredSessionFile;
log.debug("Found previous session file", { file: currentSessionFile });
break;
}
}
log.debug("Session context resolved", {
sessionId: currentSessionId,
sessionFile: currentSessionFile,
hasCfg: Boolean(cfg)
});
const sessionFile = currentSessionFile || void 0;
const hookConfig = resolveHookConfig(cfg, "session-memory");
const messageCount = typeof hookConfig?.messages === "number" && hookConfig.messages > 0 ? hookConfig.messages : 15;
let slug = null;
let sessionContent = null;
if (sessionFile) {
sessionContent = await getRecentSessionContentWithResetFallback(sessionFile, messageCount);
log.debug("Session content loaded", {
length: sessionContent?.length ?? 0,
messageCount
});
const allowLlmSlug = !(process.env.OPENCLAW_TEST_FAST === "1" || process.env.VITEST === "true" || process.env.VITEST === "1" || false) && hookConfig?.llmSlug !== false;
if (sessionContent && cfg && allowLlmSlug) {
log.debug("Calling generateSlugViaLLM...");
slug = await generateSlugViaLLM({
sessionContent,
cfg
});
log.debug("Generated slug", { slug });
}
}
if (!slug) {
slug = now.toISOString().split("T")[1].split(".")[0].replace(/:/g, "").slice(0, 4);
log.debug("Using fallback timestamp slug", { slug });
}
const filename = `${dateStr}-${slug}.md`;
const memoryFilePath = path.join(memoryDir, filename);
log.debug("Memory file path resolved", {
filename,
path: memoryFilePath.replace(os.homedir(), "~")
});
const timeStr = now.toISOString().split("T")[1].split(".")[0];
const sessionId = sessionEntry.sessionId || "unknown";
const source = context.commandSource || "unknown";
const entryParts = [
`# Session: ${dateStr} ${timeStr} UTC`,
"",
`- **Session Key**: ${event.sessionKey}`,
`- **Session ID**: ${sessionId}`,
`- **Source**: ${source}`,
""
];
if (sessionContent) entryParts.push("## Conversation Summary", "", sessionContent, "");
const entry = entryParts.join("\n");
await fs.writeFile(memoryFilePath, entry, "utf-8");
log.debug("Memory file written successfully");
const relPath = memoryFilePath.replace(os.homedir(), "~");
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) });
}
};
//#endregion
export { saveSessionToMemory as default };