diff --git a/index.js b/index.js index 0d504d5..4f2a0c1 100644 --- a/index.js +++ b/index.js @@ -170,6 +170,68 @@ function cleanupMemoryDir(workspaceDir) { } catch {} } +/** + * Append a session summary to the daily session log. + * File: memory/sessions/YYYY-MM-DD.md (one per day, append-only) + */ +function writeSessionLog(workspaceDir, messages, config = {}) { + if (config.sessionLogging === false) return; + if (!messages || messages.length === 0) return; + + const now = new Date(); + const pad = (n) => String(n).padStart(2, "0"); + const dateStr = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}`; + const timeStr = `${pad(now.getHours())}:${pad(now.getMinutes())}`; + + const sessionsDir = path.join(workspaceDir, "memory", "sessions"); + fs.mkdirSync(sessionsDir, { recursive: true }); + + const logFile = path.join(sessionsDir, `${dateStr}.md`); + + // Extract first meaningful user message as topic + let topic = "(no topic)"; + for (const msg of messages) { + if (msg?.role !== "user") continue; + const text = typeof msg.content === "string" + ? msg.content + : Array.isArray(msg.content) + ? msg.content.filter(b => b?.type === "text").map(b => b.text).join("\n") + : ""; + const cleaned = text + .replace(/^Conversation info \(untrusted metadata\):[\s\S]*?\n\n/m, "") + .replace(/^Sender \(untrusted metadata\):[\s\S]*?\n\n/m, "") + .trim(); + if (cleaned.length > 10) { + topic = cleaned.split("\n")[0].slice(0, 120); + break; + } + } + + // Count messages by role + const userCount = messages.filter(m => m?.role === "user").length; + const assistantCount = messages.filter(m => m?.role === "assistant").length; + const totalTokens = estimateTokens( + messages.map(m => typeof m?.content === "string" ? m.content : "").join("") + ); + + // Build log entry + const entry = [ + `### ${timeStr}`, + `- **Topic:** ${topic}`, + `- **Messages:** ${userCount} user / ${assistantCount} assistant`, + `- **Est. tokens:** ~${totalTokens}`, + "", + ].join("\n"); + + // Append to daily log (create with header if new) + if (!fs.existsSync(logFile)) { + const header = `# Session Log — ${dateStr}\n\n`; + fs.writeFileSync(logFile, header + entry, "utf8"); + } else { + fs.appendFileSync(logFile, entry, "utf8"); + } +} + function extractStateFromMessages(messages) { if (!messages || messages.length === 0) return null; @@ -368,6 +430,9 @@ const plugin = { } } + // Write session log entry + writeSessionLog(ws, messages, config); + const existing = readFile(statePath); const newState = extractStateFromMessages(messages); if (!newState) { diff --git a/openclaw.plugin.json b/openclaw.plugin.json index 50688b2..0ac7ca3 100644 --- a/openclaw.plugin.json +++ b/openclaw.plugin.json @@ -2,7 +2,7 @@ "id": "memory-continuity", "name": "Memory Continuity", "description": "Preserves working state across /new, reset, compaction, and gateway restarts via a simple markdown checkpoint file.", - "version": "3.0.0", + "version": "3.1.0", "source": "https://github.com/dtzp555-max/memory-continuity", "configSchema": { "type": "object", @@ -32,6 +32,11 @@ "type": "array", "default": [], "description": "Regex patterns to ignore sessions (e.g. cron jobs, subagent noise). Matched against first user message." + }, + "sessionLogging": { + "type": "boolean", + "default": true, + "description": "Write session summaries to memory/sessions/ daily logs" } } }, @@ -55,6 +60,10 @@ "ignorePatterns": { "label": "Ignore patterns", "help": "Skip state extraction for sessions matching these patterns (regex)" + }, + "sessionLogging": { + "label": "Session logging", + "help": "Append session summaries to daily markdown logs" } } } \ No newline at end of file