Remove secret exclusions from .gitignore (local-only repo). Add openclaw runtime state: credentials, identity, devices, hooks, telegram, secrets, agent configs. Exclude noisy/binary data: sessions, sqlite, media, temp files. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
239 lines
9.8 KiB
JavaScript
239 lines
9.8 KiB
JavaScript
import { c as resolveAgentWorkspaceDir } from "../../run-with-concurrency-Cuc1THN9.js";
|
|
import { c as resolveStateDir } from "../../paths-hfkBoC7i.js";
|
|
import { t as createSubsystemLogger } from "../../subsystem-C-Cf_MFK.js";
|
|
import { B as resolveAgentIdFromSessionKey } from "../../workspace-CaW79EXh.js";
|
|
import "../../logger-BW8uLq6f.js";
|
|
import "../../model-selection-BU6wl1le.js";
|
|
import "../../github-copilot-token-CQmATy5E.js";
|
|
import "../../legacy-names-BAf61_0I.js";
|
|
import "../../thinking-B5B36ffe.js";
|
|
import "../../tokens-CT3nywWU.js";
|
|
import "../../pi-embedded-C6ITuRXf.js";
|
|
import "../../plugins-BZr8LJrk.js";
|
|
import "../../accounts-D4KOSoV2.js";
|
|
import "../../send-BLQvMYTW.js";
|
|
import "../../send-DyQ6zcob.js";
|
|
import "../../deliver-ClGktCjk.js";
|
|
import "../../diagnostic-B9sgiG77.js";
|
|
import "../../accounts-cJqOTvBI.js";
|
|
import "../../image-ops-D4vlUR_L.js";
|
|
import "../../send-D4CMR9ev.js";
|
|
import "../../pi-model-discovery--C0FuY_K.js";
|
|
import { pt as hasInterSessionUserProvenance } from "../../pi-embedded-helpers-CkWXaNFn.js";
|
|
import "../../chrome-u1QjWgKY.js";
|
|
import "../../frontmatter-CZF6xkL3.js";
|
|
import "../../skills-B24U0XQQ.js";
|
|
import "../../path-alias-guards-CouH80Zp.js";
|
|
import "../../redact-DSv8X-3F.js";
|
|
import "../../errors-_LEe37ld.js";
|
|
import { c as writeFileWithinRoot } from "../../fs-safe-DOYVoR6M.js";
|
|
import "../../proxy-env-BZseFuIl.js";
|
|
import "../../store-BteyapSQ.js";
|
|
import "../../paths-Co-u8IhA.js";
|
|
import "../../tool-images-C0W994KU.js";
|
|
import "../../image-fMgabouP.js";
|
|
import "../../audio-transcription-runner-DfRfzdqH.js";
|
|
import "../../fetch-JzejSI-7.js";
|
|
import "../../fetch-guard-C3LWD6FT.js";
|
|
import "../../api-key-rotation-CLI6TxVv.js";
|
|
import "../../proxy-fetch-CbII9--S.js";
|
|
import "../../ir-D_UJzvhu.js";
|
|
import "../../render-7C7EDC8_.js";
|
|
import "../../target-errors-C8xePsI5.js";
|
|
import "../../commands-registry-DJWLO-6B.js";
|
|
import "../../skill-commands-B6iXy7Nx.js";
|
|
import "../../fetch-CONQGbzL.js";
|
|
import "../../channel-activity-CVe33Aey.js";
|
|
import "../../tables-DushlpuO.js";
|
|
import "../../send-CHthYes-.js";
|
|
import "../../outbound-attachment-3soL6fn0.js";
|
|
import "../../send-DYCEGbmH.js";
|
|
import "../../proxy-BzwL4n0W.js";
|
|
import "../../manager-DS9FBMMG.js";
|
|
import "../../query-expansion-DUWWrH-g.js";
|
|
import { generateSlugViaLLM } from "../../llm-slug-generator.js";
|
|
import { t as resolveHookConfig } from "../../config-Bs6iYHRw.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, "");
|
|
await writeFileWithinRoot({
|
|
rootDir: memoryDir,
|
|
relativePath: filename,
|
|
data: entryParts.join("\n"),
|
|
encoding: "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 };
|