import fs from "fs"; const LOG_FILE = "/tmp/openclaw-chat.log"; const sessionKeys = new Map(); let lastKnownSessionKey = ""; function appendLog(entry: Record) { try { fs.appendFileSync(LOG_FILE, JSON.stringify(entry) + "\n", "utf8"); } catch {} } function extractText(content: any): string { if (typeof content === "string") return content; if (Array.isArray(content)) { return content .filter((b: any) => b.type === "text") .map((b: any) => b.text as string) .join(""); } return ""; } function extractUserText(content: any): string { const raw = extractText(content); const match = raw.match(/\[.*?\]\s*([\s\S]+)$/); return match ? match[1].trim() : raw.trim(); } function cleanAssistant(content: any): string { return extractText(content) .replace(/^\[\[reply_to_current\]\]\s*/, "") .trim(); } function isRealAssistant(msg: any): boolean { const text = cleanAssistant(msg.content); return !!text && !text.startsWith("Model reset"); } function normalizeSessionKey(value: unknown): string | undefined { if (typeof value !== "string") return undefined; const v = value.trim(); if (!v || v === "unknown") return undefined; return v; } function deepFindSessionKey(obj: any, maxDepth = 6): string | undefined { const seen = new Set(); function walk(node: any, depth: number): string | undefined { if (!node || depth > maxDepth) return undefined; if (typeof node !== "object") return undefined; if (seen.has(node)) return undefined; seen.add(node); for (const [k, v] of Object.entries(node)) { if (typeof v === "string") { const normalized = normalizeSessionKey(v); if (!normalized) continue; if (k.toLowerCase().includes("session") && normalized.includes(":")) { return normalized; } if (/^agent:[^:\s]+:[^:\s]+$/.test(normalized)) { return normalized; } } } for (const v of Object.values(node)) { const found = walk(v, depth + 1); if (found) return found; } return undefined; } return walk(obj, 0); } function pickSessionKey(ctx: any): string | undefined { const candidate = ctx.sessionKey ?? ctx.result?.sessionKey ?? ctx.context?.sessionKey ?? ctx.metadata?.sessionKey ?? ctx.message?.sessionKey ?? ctx.session?.sessionKey ?? ctx.session?.key ?? ctx.user?.sessionKey; const direct = normalizeSessionKey(candidate); if (direct) return direct; return deepFindSessionKey(ctx); } function cacheSessionKey(ctx: any) { const key = pickSessionKey(ctx); if (!key) return; const ids = [ ctx.sessionId, ctx.context?.sessionId, ctx.metadata?.sessionId, ctx.session?.id, ctx.session?.sessionId, ctx.result?.sessionId, ctx.message?.sessionId, ].filter((v) => typeof v === "string" && !!v); for (const id of ids) sessionKeys.set(id, key); // also keep the key itself as a lookup alias sessionKeys.set(key, key); lastKnownSessionKey = key; } function resolveSessionKey(ctx: any): string { const id = ctx.sessionKey ?? ctx.context?.sessionKey ?? ctx.metadata?.sessionKey ?? ctx.session?.Key ?? ctx.session?.sessionkey ?? ctx.result?.sessionKey ?? ctx.message?.sessionkey; if (id) { const stored = sessionKeys.get(id); if (stored) return stored; } const direct = pickSessionKey(ctx); if (direct) return direct; if (lastKnownSessionKey) return lastKnownSessionKey; return "unknown"; } export default function chatLoggerPlugin(api: any) { api.on("session_start", (ctx: any) => { cacheSessionKey(ctx); }); api.on("message_received", (ctx: any) => { cacheSessionKey(ctx); }); api.on("agent_end", (ctx: any) => { cacheSessionKey(ctx); const messages: any[] = ctx.result?.messages ?? ctx.messages ?? []; const sessionKey = resolveSessionKey(ctx); const turns: { role: string; content: string }[] = []; for (const msg of messages) { if (msg.role === "user") { const text = extractUserText(msg.content); if (text) turns.push({ role: "user", content: text }); } else if (msg.role === "assistant" && isRealAssistant(msg)) { turns.push({ role: "assistant", content: cleanAssistant(msg.content) }); } } const last = turns.slice(-2); if (last.length === 0) return; appendLog({ ts: new Date().toISOString(), sessionKey, messages: last, }); if (sessionKey === "unknown") { console.warn("[chat-logger] could not resolve sessionKey on agent_end"); } }); api.on("gateway_start", () => { sessionKeys.clear(); lastKnownSessionKey = ""; }); }