.agents/hooks/agent-memory/handler.js.bak

279 lines
8.6 KiB
JavaScript

/**
* 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<string>}
*/
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 <content>\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 <search query>");
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;