/** * Signet Agent Memory Hook * * Unified memory system for all AI harnesses (OpenClaw, Claude Code, OpenCode, Codex) * Features: hybrid search (vector + BM25), auto-embedding, cross-harness persistence * * Spec: Signet v0.2.1 - https://signetai.sh */ import { spawn } from "node:child_process"; import path from "node:path"; import os from "node:os"; import fs from "node:fs/promises"; const MEMORY_SCRIPT = path.join(os.homedir(), ".agents/memory/scripts/memory.py"); const MEMORY_MD_PATH = path.join(os.homedir(), ".agents/memory/MEMORY.md"); /** * Run memory.py command and return stdout * @param {string[]} args * @returns {Promise} */ async function runMemoryScript(args) { return new Promise((resolve, reject) => { const proc = spawn("python3", [MEMORY_SCRIPT, ...args], { timeout: 5000, }); let stdout = ""; let stderr = ""; proc.stdout.on("data", (data) => { stdout += data.toString(); }); proc.stderr.on("data", (data) => { stderr += data.toString(); }); proc.on("close", (code) => { if (code === 0) { resolve(stdout.trim()); } else { reject(new Error(stderr || `memory.py exited with code ${code}`)); } }); proc.on("error", (err) => { reject(err); }); }); } /** * Read recent messages from session file for memory extraction */ async function getRecentSessionContent(sessionFilePath) { try { const content = await fs.readFile(sessionFilePath, "utf-8"); const lines = content.trim().split("\n"); // Get last 20 lines (recent conversation) const recentLines = lines.slice(-20); const messages = []; for (const line of recentLines) { 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) { const text = Array.isArray(msg.content) ? msg.content.find((c) => c.type === "text")?.text : msg.content; if (text && !text.startsWith("/")) { messages.push(`${role}: ${text}`); } } } } catch { // Skip invalid JSON lines } } return messages.join("\n"); } catch { return null; } } /** * Handle /remember command */ async function handleRemember(event) { const context = event.context || {}; const args = context.args || ""; if (!args.trim()) { event.messages.push("🧠 Usage: /remember \n\nPrefixes:\n- `critical:` for pinned memories\n- `[tag1,tag2]:` for tagged memories"); return; } try { // Detect harness from session key or default to openclaw const harness = event.sessionKey?.includes("claude") ? "claude-code" : event.sessionKey?.includes("opencode") ? "opencode" : "openclaw"; const result = await runMemoryScript([ "save", "--mode", "explicit", "--who", harness, "--project", context.cwd || os.homedir(), "--content", args.trim() ]); event.messages.push(`🧠 ${result}`); } catch (err) { event.messages.push(`🧠 Error saving memory: ${err.message}`); } } /** * Handle /recall command */ async function handleRecall(event) { const context = event.context || {}; const args = context.args || ""; if (!args.trim()) { event.messages.push("🧠 Usage: /recall "); return; } try { const result = await runMemoryScript(["query", args.trim(), "--limit", "10"]); if (result) { event.messages.push(`🧠 Memory search results:\n\n${result}`); } else { event.messages.push("🧠 No memories found matching your query."); } } catch (err) { event.messages.push(`🧠 Error querying memory: ${err.message}`); } } /** * Run the sync-memory-context script to update AGENTS.md */ async function runSyncScript() { const syncScript = path.join(os.homedir(), "clawd/scripts/sync-memory-context.sh"); return new Promise((resolve, reject) => { const proc = spawn("bash", [syncScript], { timeout: 5000 }); let stdout = ""; let stderr = ""; proc.stdout.on("data", (data) => { stdout += data.toString(); }); proc.stderr.on("data", (data) => { stderr += data.toString(); }); proc.on("close", (code) => { if (code === 0) resolve(stdout.trim()); else reject(new Error(stderr || `sync script exited with code ${code}`)); }); proc.on("error", reject); }); } /** * Handle /context command - load MEMORY.md + db memories * This gives clawdbot the same context that claude code gets at session start * Also syncs memory into AGENTS.md for future sessions */ async function handleContext(event) { // First, sync memory to AGENTS.md try { await runSyncScript(); console.log("[agent-memory] Synced memory context to AGENTS.md"); } catch (err) { console.warn("[agent-memory] Failed to sync memory to AGENTS.md:", err.message); } try { const result = await runMemoryScript([ "load", "--mode", "session-start", "--project", os.homedir() ]); if (result) { event.messages.push(`🧠 **Memory Context Loaded**\n\n${result}`); } else { event.messages.push("🧠 No memory context available."); } } catch (err) { // fallback: try to read MEMORY.md directly try { const currentMd = await fs.readFile(MEMORY_MD_PATH, "utf-8"); if (currentMd.trim()) { event.messages.push(`🧠 **Memory Context**\n\n${currentMd.trim()}`); } else { event.messages.push("🧠 No memory context available."); } } catch { event.messages.push(`🧠 Error loading context: ${err.message}`); } } } /** * Handle /new command - save session context before reset */ async function handleNew(event) { const context = event.context || {}; const sessionEntry = context.previousSessionEntry || context.sessionEntry || {}; const sessionFile = sessionEntry.sessionFile; if (!sessionFile) { console.log("[agent-memory] No session file found, skipping auto-save"); return; } try { const sessionContent = await getRecentSessionContent(sessionFile); if (!sessionContent || sessionContent.length < 100) { console.log("[agent-memory] Session too short for auto-extraction"); return; } // Save a summary marker that this session ended const now = new Date().toISOString(); const summaryContent = `[clawdbot-session-end]: Session ended at ${now}. Last conversation:\n${sessionContent.slice(0, 500)}`; // Detect harness from session key const harness = event.sessionKey?.includes("claude") ? "claude-code" : event.sessionKey?.includes("opencode") ? "opencode" : "openclaw"; await runMemoryScript([ "save", "--mode", "explicit", "--who", harness, "--project", context.cwd || "global", "--content", summaryContent ]); console.log("[agent-memory] Session context saved to memory database"); } catch (err) { console.error("[agent-memory] Failed to save session memory:", err.message); } } /** * Main hook handler */ const agentMemoryHandler = async (event) => { // Check if memory script exists try { await fs.access(MEMORY_SCRIPT); } catch { console.warn("[agent-memory] Memory script not found at", MEMORY_SCRIPT); return; } if (event.type !== "command") { return; } switch (event.action) { case "remember": await handleRemember(event); break; case "recall": await handleRecall(event); break; case "context": await handleContext(event); break; case "new": await handleNew(event); break; } }; export default agentMemoryHandler;