From 38b84b8163ad16b15115dbf35e9eb2dccebdb6eb Mon Sep 17 00:00:00 2001 From: "glm-5.1" Date: Tue, 21 Apr 2026 09:20:53 +0000 Subject: [PATCH] Add memory_compactions tool for browsing compaction checkpoints Queries compaction-type parts in the DB to find session compaction events, then retrieves the summary text from the adjacent assistant message. Presents compactions as navigable checkpoints with list and read modes. --- AGENTS.md | 12 ++++- docs/architecture.md | 1 + src/tools.ts | 116 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 128 insertions(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index e2e5bc5..064915b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -59,12 +59,13 @@ src/ | `experimental.chat.system.transform` | Inject context % used + advisory into system prompt | | `event` | Feed SSE events to ContextTracker | -### Tools (7) +### Tools (8) | Tool | Purpose | |------|---------| | `memory_context` | Current context window usage (% , tokens, model, status) | | `memory_compact` | Trigger compaction via `ctx.client.session.summarize()` | +| `memory_compactions` | List/read compaction checkpoints (summaries) for a session | | `memory_summary` | Quick counts: projects, sessions, messages, todos | | `memory_sessions` | List recent sessions, optionally filtered by project path | | `memory_messages` | Read messages from a specific session | @@ -99,6 +100,15 @@ usable = model.limit.input ? model.limit.input - reserved The `tokens.input` on the last assistant message approximates context size. We track against model context limit from config, falling back to 200k. +### Compaction Data in DB + +When compaction occurs, OpenCode creates: +1. A synthetic `user` message with a `compaction`-type part (`part.data = {type: "compaction", auto: true/false, overflow: true/false}`) +2. `message.data.summary = {diffs: [...]}` on the compaction message +3. The assistant message immediately after contains the actual summary text in a `text`-type part + +The `memory_compactions` tool queries for `compaction`-type parts and retrieves the adjacent summary text, presenting them as navigable checkpoints. + ### Write Operations All write operations (compaction triggering) go through the OpenCode client SDK (`ctx.client.session.summarize`). The plugin never writes to the database or any OpenCode files. diff --git a/docs/architecture.md b/docs/architecture.md index 1098e79..82452eb 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -82,6 +82,7 @@ All backed by read-only `bun:sqlite` queries to `${XDG_DATA_HOME:-$HOME/.local/s | Tool | Purpose | |------|---------| +| `memory_compactions` | List/read compaction checkpoints for a session | | `memory_summary` | Quick counts: projects, sessions, messages, todos | | `memory_sessions` | List recent sessions with metadata, sorted by update time | | `memory_messages` | Read messages from a specific session as markdown | diff --git a/src/tools.ts b/src/tools.ts index 4ecc6f5..708b72d 100644 --- a/src/tools.ts +++ b/src/tools.ts @@ -282,6 +282,122 @@ export const createTools = ( }, }), + memory_compactions: tool({ + description: + "List compaction events (checkpoints) for a session. Compactions are moments where the conversation was summarized to free context space. Use 'read' to get the full summary text of a specific compaction — these act as checkpoints showing what the agent considered important at that point in the session.", + args: { + sessionId: z.string().describe("Session ID to check compactions for."), + read: z + .number() + .optional() + .describe("Index (1-based) of a specific compaction to read the full summary text for."), + }, + async execute(args) { + try { + type CompactionMeta = { + compaction_msg_id: string; + compaction_time: number; + time: string; + is_auto: number; + overflow: number; + }; + + type SummaryRow = { + summary_text: string; + }; + + const compactions = runQuery( + ` + SELECT + cp_msg.id AS compaction_msg_id, + cp_msg.time_created AS compaction_time, + datetime(cp_msg.time_created/1000, 'unixepoch', 'localtime') AS time, + COALESCE(json_extract(cp_part.data, '$.auto'), 0) AS is_auto, + COALESCE(json_extract(cp_part.data, '$.overflow'), 0) AS overflow + FROM part cp_part + JOIN message cp_msg ON cp_msg.id = cp_part.message_id + WHERE cp_msg.session_id = $sessionId + AND json_extract(cp_part.data, '$.type') = 'compaction' + ORDER BY cp_msg.time_created ASC + `, + { $sessionId: args.sessionId }, + ); + + if (!compactions || compactions.length === 0) { + return "No compactions found for this session."; + } + + if (args.read !== undefined) { + const idx = args.read - 1; + if (idx < 0 || idx >= compactions.length) { + return `Invalid compaction index. Session has ${compactions.length} compaction(s). Use 1-${compactions.length}.`; + } + const comp = compactions[idx]; + const summaryRows = runQuery( + ` + SELECT json_extract(p.data, '$.text') AS summary_text + FROM message m + JOIN part p ON p.message_id = m.id + WHERE m.session_id = $sessionId + AND json_extract(m.data, '$.role') = 'assistant' + AND json_extract(p.data, '$.type') = 'text' + AND m.time_created > $compactionTime + ORDER BY m.time_created ASC + LIMIT 1 + `, + { $sessionId: args.sessionId, $compactionTime: comp.compaction_time }, + ); + const summaryText = + summaryRows && summaryRows.length > 0 && summaryRows[0].summary_text + ? summaryRows[0].summary_text + : "(no summary text found)"; + const header = [ + `# Compaction ${args.read}`, + `Time: ${comp.time}`, + `Auto: ${comp.is_auto ? "yes" : "no"}`, + `Overflow: ${comp.overflow ? "yes" : "no"}`, + "", + ].join("\n"); + return `${header}${summaryText}`; + } + + const lines = [`# Compactions (${compactions.length})\n`]; + lines.push("| # | Time | Auto | Summary |"); + lines.push("|---|------|------|---------|"); + + for (let i = 0; i < compactions.length; i++) { + const comp = compactions[i]; + const summaryRows = runQuery( + ` + SELECT substr(json_extract(p.data, '$.text'), 1, 150) AS summary_text + FROM message m + JOIN part p ON p.message_id = m.id + WHERE m.session_id = $sessionId + AND json_extract(m.data, '$.role') = 'assistant' + AND json_extract(p.data, '$.type') = 'text' + AND m.time_created > $compactionTime + ORDER BY m.time_created ASC + LIMIT 1 + `, + { $sessionId: args.sessionId, $compactionTime: comp.compaction_time }, + ); + const preview = summaryRows?.[0]?.summary_text + ? `${summaryRows[0].summary_text.replace(/\n/g, " ").substring(0, 60)}...` + : "(no summary)"; + lines.push(`| ${i + 1} | ${comp.time} | ${comp.is_auto ? "yes" : "no"} | ${preview} |`); + } + + lines.push( + "", + `Use memory_compactions with a "read" argument (1-${compactions.length}) to view the full summary for a specific compaction.`, + ); + return lines.join("\n"); + } catch (err) { + return `Failed to query compactions: ${err instanceof Error ? err.message : String(err)}`; + } + }, + }), + memory_plans: tool({ description: "List saved plan files from OpenCode's plans directory.", args: {