|
|
@@ -2,6 +2,8 @@ import fs from "fs";
|
|
|
import os from "os";
|
|
|
import path from "path";
|
|
|
|
|
|
+const CHAT_LOG_FILE = "/tmp/openclaw-chat.log";
|
|
|
+
|
|
|
// ---------------------------------------------------------------------------
|
|
|
// Config — openclaw does NOT inject cfg into hook events.
|
|
|
// Read from env vars; fall back to ~/.openclaw/mem0.json for convenience.
|
|
|
@@ -55,6 +57,9 @@ function loadPluginCfg() {
|
|
|
knowledgeLimit: Number(
|
|
|
process.env.MEM0_KNOWLEDGE_LIMIT || fileCfg.knowledgeLimit || 5
|
|
|
),
|
|
|
+ debugCapture:
|
|
|
+ (process.env.MEM0_DEBUG_CAPTURE ||
|
|
|
+ String(fileCfg.debugCapture ?? "false")) === "true",
|
|
|
};
|
|
|
}
|
|
|
|
|
|
@@ -160,6 +165,56 @@ function getAudioPath(context: any): string | undefined {
|
|
|
return undefined;
|
|
|
}
|
|
|
|
|
|
+function readLastAssistantMessage(sessionKey?: string): string | undefined {
|
|
|
+ try {
|
|
|
+ if (!fs.existsSync(CHAT_LOG_FILE)) return undefined;
|
|
|
+ const payload = fs.readFileSync(CHAT_LOG_FILE, "utf8");
|
|
|
+ const lines = payload.split(/\r?\n/).filter(Boolean);
|
|
|
+
|
|
|
+ // First pass: strict session match.
|
|
|
+ for (let i = lines.length - 1; i >= 0; i--) {
|
|
|
+ try {
|
|
|
+ const entry = JSON.parse(lines[i]);
|
|
|
+ if (
|
|
|
+ sessionKey &&
|
|
|
+ entry?.sessionKey &&
|
|
|
+ entry.sessionKey !== sessionKey
|
|
|
+ ) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ const assistantText = entry?.messages?.[1]?.content;
|
|
|
+ if (typeof assistantText === "string" && assistantText.trim()) {
|
|
|
+ return assistantText.trim();
|
|
|
+ }
|
|
|
+ } catch {
|
|
|
+ // skip malformed lines
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Second pass fallback: latest assistant message from any session.
|
|
|
+ if (sessionKey) {
|
|
|
+ for (let i = lines.length - 1; i >= 0; i--) {
|
|
|
+ try {
|
|
|
+ const entry = JSON.parse(lines[i]);
|
|
|
+ const assistantText = entry?.messages?.[1]?.content;
|
|
|
+ if (typeof assistantText === "string" && assistantText.trim()) {
|
|
|
+ console.warn(
|
|
|
+ "[mem0-auto-capture] assistant lookup fallback to global latest",
|
|
|
+ { requestedSessionKey: sessionKey, logSessionKey: entry?.sessionKey }
|
|
|
+ );
|
|
|
+ return assistantText.trim();
|
|
|
+ }
|
|
|
+ } catch {
|
|
|
+ // skip malformed lines
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch {
|
|
|
+ // ignore missing or unreadable log
|
|
|
+ }
|
|
|
+ return undefined;
|
|
|
+}
|
|
|
+
|
|
|
async function transcribeAudio(localPath: string): Promise<string> {
|
|
|
const buffer = fs.readFileSync(localPath);
|
|
|
const blob = new Blob([buffer]);
|
|
|
@@ -250,6 +305,23 @@ function formatKnowledgeCitation(meta: Record<string, any> = {}): string {
|
|
|
return parts.length > 0 ? `(from: ${parts.join(", ")})` : "";
|
|
|
}
|
|
|
|
|
|
+function formatAge(createdAt?: string): string {
|
|
|
+ if (!createdAt) return "";
|
|
|
+ const created = new Date(createdAt).getTime();
|
|
|
+ if (!Number.isFinite(created)) return "";
|
|
|
+ const deltaMs = Date.now() - created;
|
|
|
+ if (deltaMs < 0) return "just now";
|
|
|
+
|
|
|
+ const minute = 60_000;
|
|
|
+ const hour = 60 * minute;
|
|
|
+ const day = 24 * hour;
|
|
|
+
|
|
|
+ if (deltaMs < minute) return "just now";
|
|
|
+ if (deltaMs < hour) return `${Math.floor(deltaMs / minute)}m ago`;
|
|
|
+ if (deltaMs < day) return `${Math.floor(deltaMs / hour)}h ago`;
|
|
|
+ return `${Math.floor(deltaMs / day)}d ago`;
|
|
|
+}
|
|
|
+
|
|
|
function buildInjectionBlock(
|
|
|
personalResults: MemoryResult[] | null,
|
|
|
knowledgeResults: MemoryResult[] | null,
|
|
|
@@ -266,7 +338,13 @@ function buildInjectionBlock(
|
|
|
// endpoint was down — omit the section entirely, already logged
|
|
|
} else if (personal.length > 0) {
|
|
|
const lines = personal
|
|
|
- .map((r) => `- ${r.memory}`)
|
|
|
+ .map((r) => {
|
|
|
+ const createdAt =
|
|
|
+ (r as any)?.created_at ||
|
|
|
+ (r.metadata && (r.metadata as any).created_at);
|
|
|
+ const age = formatAge(createdAt);
|
|
|
+ return age ? `- [${age}] ${r.memory}` : `- ${r.memory}`;
|
|
|
+ })
|
|
|
.join("\n");
|
|
|
sections.push(`[MEMORY - Personal]\n${lines}`);
|
|
|
}
|
|
|
@@ -451,8 +529,17 @@ export default async function handler(event: HookEvent) {
|
|
|
}
|
|
|
if (!shouldCapture) return;
|
|
|
|
|
|
- const captureText = recent.join("\n");
|
|
|
- const hash = simpleHash(captureText);
|
|
|
+ const assistantMessage = readLastAssistantMessage(event.sessionKey);
|
|
|
+ const userCaptureText = recent.join("\n");
|
|
|
+
|
|
|
+ const captureMessages: Array<{ role: "user" | "assistant"; content: string }> = [
|
|
|
+ { role: "user", content: userCaptureText },
|
|
|
+ ];
|
|
|
+ if (assistantMessage) {
|
|
|
+ captureMessages.push({ role: "assistant", content: assistantMessage });
|
|
|
+ }
|
|
|
+
|
|
|
+ const hash = simpleHash(JSON.stringify(captureMessages));
|
|
|
const lastCapture = recentlyCaptured.get(hash);
|
|
|
if (lastCapture && Date.now() - lastCapture < CAPTURE_DEDUP_MS) {
|
|
|
console.log("[mem0-auto-capture] skipped duplicate");
|
|
|
@@ -461,15 +548,27 @@ export default async function handler(event: HookEvent) {
|
|
|
recentlyCaptured.set(hash, Date.now());
|
|
|
|
|
|
try {
|
|
|
- console.log("[mem0-auto-capture]", {
|
|
|
- userId,
|
|
|
- captureTrigger,
|
|
|
- text: captureText.slice(0, 160),
|
|
|
- });
|
|
|
+ if (pluginCfg.debugCapture) {
|
|
|
+ console.log("[mem0-auto-capture]", {
|
|
|
+ sessionKey: event.sessionKey,
|
|
|
+ userId,
|
|
|
+ captureTrigger,
|
|
|
+ hasAssistantMessage: !!assistantMessage,
|
|
|
+ messages: captureMessages,
|
|
|
+ });
|
|
|
+ } else {
|
|
|
+ console.log("[mem0-auto-capture]", {
|
|
|
+ sessionKey: event.sessionKey,
|
|
|
+ userId,
|
|
|
+ captureTrigger,
|
|
|
+ hasAssistantMessage: !!assistantMessage,
|
|
|
+ messageCount: captureMessages.length,
|
|
|
+ });
|
|
|
+ }
|
|
|
await fetch(`${pluginCfg.baseUrl}/memories`, {
|
|
|
method: "POST",
|
|
|
headers: { "Content-Type": "application/json" },
|
|
|
- body: JSON.stringify({ text: captureText, userId }),
|
|
|
+ body: JSON.stringify({ userId, messages: captureMessages }),
|
|
|
});
|
|
|
} catch (err) {
|
|
|
console.error("[mem0-auto-capture] write failed:", err);
|