Spaces:
Paused
Paused
File size: 6,702 Bytes
fb4d8fe | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 | /**
* Session memory hook handler
*
* Saves session context to memory when /new command is triggered
* Creates a new dated memory file with LLM-generated slug
*/
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { fileURLToPath } from "node:url";
import type { OpenClawConfig } from "../../../config/config.js";
import type { HookHandler } from "../../hooks.js";
import { resolveAgentWorkspaceDir } from "../../../agents/agent-scope.js";
import { resolveAgentIdFromSessionKey } from "../../../routing/session-key.js";
import { resolveHookConfig } from "../../config.js";
/**
* Read recent messages from session file for slug generation
*/
async function getRecentSessionContent(
sessionFilePath: string,
messageCount: number = 15,
): Promise<string | null> {
try {
const content = await fs.readFile(sessionFilePath, "utf-8");
const lines = content.trim().split("\n");
// Parse JSONL and extract user/assistant messages first
const allMessages: string[] = [];
for (const line of lines) {
try {
const entry = JSON.parse(line);
// Session files have entries with type="message" containing a nested message object
if (entry.type === "message" && entry.message) {
const msg = entry.message;
const role = msg.role;
if ((role === "user" || role === "assistant") && msg.content) {
// Extract text content
const text = Array.isArray(msg.content)
? msg.content.find((c: any) => c.type === "text")?.text
: msg.content;
if (text && !text.startsWith("/")) {
allMessages.push(`${role}: ${text}`);
}
}
}
} catch {
// Skip invalid JSON lines
}
}
// Then slice to get exactly messageCount messages
const recentMessages = allMessages.slice(-messageCount);
return recentMessages.join("\n");
} catch {
return null;
}
}
/**
* Save session context to memory when /new command is triggered
*/
const saveSessionToMemory: HookHandler = async (event) => {
// Only trigger on 'new' command
if (event.type !== "command" || event.action !== "new") {
return;
}
try {
console.log("[session-memory] Hook triggered for /new command");
const context = event.context || {};
const cfg = context.cfg as OpenClawConfig | undefined;
const agentId = resolveAgentIdFromSessionKey(event.sessionKey);
const workspaceDir = cfg
? resolveAgentWorkspaceDir(cfg, agentId)
: path.join(os.homedir(), ".openclaw", "workspace");
const memoryDir = path.join(workspaceDir, "memory");
await fs.mkdir(memoryDir, { recursive: true });
// Get today's date for filename
const now = new Date(event.timestamp);
const dateStr = now.toISOString().split("T")[0]; // YYYY-MM-DD
// Generate descriptive slug from session using LLM
const sessionEntry = (context.previousSessionEntry || context.sessionEntry || {}) as Record<
string,
unknown
>;
const currentSessionId = sessionEntry.sessionId as string;
const currentSessionFile = sessionEntry.sessionFile as string;
console.log("[session-memory] Current sessionId:", currentSessionId);
console.log("[session-memory] Current sessionFile:", currentSessionFile);
console.log("[session-memory] cfg present:", !!cfg);
const sessionFile = currentSessionFile || undefined;
// Read message count from hook config (default: 15)
const hookConfig = resolveHookConfig(cfg, "session-memory");
const messageCount =
typeof hookConfig?.messages === "number" && hookConfig.messages > 0
? hookConfig.messages
: 15;
let slug: string | null = null;
let sessionContent: string | null = null;
if (sessionFile) {
// Get recent conversation content
sessionContent = await getRecentSessionContent(sessionFile, messageCount);
console.log("[session-memory] sessionContent length:", sessionContent?.length || 0);
if (sessionContent && cfg) {
console.log("[session-memory] Calling generateSlugViaLLM...");
// Dynamically import the LLM slug generator (avoids module caching issues)
// When compiled, handler is at dist/hooks/bundled/session-memory/handler.js
// Going up ../.. puts us at dist/hooks/, so just add llm-slug-generator.js
const openclawRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
const slugGenPath = path.join(openclawRoot, "llm-slug-generator.js");
const { generateSlugViaLLM } = await import(slugGenPath);
// Use LLM to generate a descriptive slug
slug = await generateSlugViaLLM({ sessionContent, cfg });
console.log("[session-memory] Generated slug:", slug);
}
}
// If no slug, use timestamp
if (!slug) {
const timeSlug = now.toISOString().split("T")[1].split(".")[0].replace(/:/g, "");
slug = timeSlug.slice(0, 4); // HHMM
console.log("[session-memory] Using fallback timestamp slug:", slug);
}
// Create filename with date and slug
const filename = `${dateStr}-${slug}.md`;
const memoryFilePath = path.join(memoryDir, filename);
console.log("[session-memory] Generated filename:", filename);
console.log("[session-memory] Full path:", memoryFilePath);
// Format time as HH:MM:SS UTC
const timeStr = now.toISOString().split("T")[1].split(".")[0];
// Extract context details
const sessionId = (sessionEntry.sessionId as string) || "unknown";
const source = (context.commandSource as string) || "unknown";
// Build Markdown entry
const entryParts = [
`# Session: ${dateStr} ${timeStr} UTC`,
"",
`- **Session Key**: ${event.sessionKey}`,
`- **Session ID**: ${sessionId}`,
`- **Source**: ${source}`,
"",
];
// Include conversation content if available
if (sessionContent) {
entryParts.push("## Conversation Summary", "", sessionContent, "");
}
const entry = entryParts.join("\n");
// Write to new memory file
await fs.writeFile(memoryFilePath, entry, "utf-8");
console.log("[session-memory] Memory file written successfully");
// Log completion (but don't send user-visible confirmation - it's internal housekeeping)
const relPath = memoryFilePath.replace(os.homedir(), "~");
console.log(`[session-memory] Session context saved to ${relPath}`);
} catch (err) {
console.error(
"[session-memory] Failed to save session memory:",
err instanceof Error ? err.message : String(err),
);
}
};
export default saveSessionToMemory;
|