From b26821ead97b986f886065b20d3dbde8283daa64 Mon Sep 17 00:00:00 2001 From: Adam Malczewski Date: Wed, 3 Jun 2026 01:26:16 +0900 Subject: feat(compaction): add UI-driven conversation compaction MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summarize a conversation's older "head" into a structured anchored Markdown summary while preserving the most recent turns verbatim, shrinking context size while keeping the information needed to continue coherently. Triggered by a "Compact conversation" button in Chat Settings (not an agent tool). Approach informed by OpenCode's session/compaction.ts: - Ported SUMMARY_TEMPLATE (Goal / Constraints / Progress / Key Decisions / Next Steps / Critical Context / Relevant Files) and the anchored-summary buildPrompt (re-summarizes a prior summary when present). - Ported the TOOL_OUTPUT_MAX_CHARS (2000) cap on tool results in the summary request. - Simplified tail selection to a fixed recent-turn count (DEFAULT_TAIL_TURNS=2) instead of OpenCode's token-budget splitTurn. core: - New src/compaction/ module (pure, DB-free): template, prompt builder, head/tail selection, transcript renderer with tool-output capping, prior summary extraction. Generic over ChatMessage so callers keep turnId/seq. - db/chunks.ts: rekeyChunks(from,to) relocates a tab's full history to a backup tab (reversible — nothing is deleted). - AgentEvent: compaction-started / -complete / -error variants. api: - AgentManager.compactTab(tempTabId, sourceTabId): side-effect-free resolveConnection() for the compactor model (configured compaction_model_*, else the source tab's own key+model), one-shot tool-less summary generation via a transient Agent, then relocate full history to a fresh backup tab and re-seed the canonical source id with [summary turn + preserved tail]. Source tab is locked (messages queue) during the run; queue drains afterward. - Routes: POST /tabs/:id/compact, GET/PUT /tabs/settings/compaction-model. frontend: - "Compact conversation" button in ModelSelector (Chat Settings), between Working Directory and the agent toggle; idle-gated. - Compaction-model key+model selector in Settings, beside the title model. - Transient placeholder tab shows a large, non-faded "Please wait, compacting conversation…" screen; closing it cancels. Source input locked while running. - Handle compaction-* events: reload compacted source, insert backup tab, refocus source, discard placeholder. tests: core compaction unit tests, rekeyChunks DB test, AgentManager.compactTab orchestration tests, and compaction route tests. All green (713 tests), biome clean, all typechecks pass, frontend builds. --- packages/core/src/chunks/append.ts | 3 + packages/core/src/compaction/index.ts | 245 ++++++++++++++++++++++ packages/core/src/db/chunks.ts | 19 ++ packages/core/src/index.ts | 16 ++ packages/core/src/types/index.ts | 24 ++- packages/core/tests/compaction/compaction.test.ts | 194 +++++++++++++++++ packages/core/tests/db/rekey-chunks.db.test.ts | 129 ++++++++++++ 7 files changed, 629 insertions(+), 1 deletion(-) create mode 100644 packages/core/src/compaction/index.ts create mode 100644 packages/core/tests/compaction/compaction.test.ts create mode 100644 packages/core/tests/db/rekey-chunks.db.test.ts (limited to 'packages/core') diff --git a/packages/core/src/chunks/append.ts b/packages/core/src/chunks/append.ts index 4bc6b5f..4ca6fe1 100644 --- a/packages/core/src/chunks/append.ts +++ b/packages/core/src/chunks/append.ts @@ -210,6 +210,9 @@ export function appendEventToChunks(chunks: Chunk[], event: AgentEvent): void { case "message-queued": case "message-consumed": case "message-cancelled": + case "compaction-started": + case "compaction-complete": + case "compaction-error": return; default: { diff --git a/packages/core/src/compaction/index.ts b/packages/core/src/compaction/index.ts new file mode 100644 index 0000000..663ccb2 --- /dev/null +++ b/packages/core/src/compaction/index.ts @@ -0,0 +1,245 @@ +// Conversation compaction — summarize the older "head" of a conversation into +// a structured anchor while preserving the most recent turns verbatim. +// +// Ported from opencode's `session/compaction.ts` (SUMMARY_TEMPLATE, the +// anchored-summary `buildPrompt`, and the `TOOL_OUTPUT_MAX_CHARS` cap). The +// turn-budget `splitTurn` tail selection is intentionally simplified to a fixed +// recent-turn count (`DEFAULT_TAIL_TURNS`) — Dispatch keeps the last N turns +// verbatim rather than computing a token budget. +// +// This module is pure and DB-free so it can be unit-tested in isolation and +// shared by the API orchestrator. + +import type { ChatMessage } from "../types/index.js"; + +/** Number of trailing turns kept verbatim (opencode's DEFAULT_TAIL_TURNS). */ +export const DEFAULT_TAIL_TURNS = 2; + +/** Max characters of a single tool result fed into the summary request. */ +export const TOOL_OUTPUT_MAX_CHARS = 2_000; + +/** + * Marker prefixing the seeded summary turn in a compacted conversation. Lets a + * subsequent compaction detect a prior summary and re-summarize (anchor) it + * instead of treating it as ordinary conversation. + */ +export const SUMMARY_MARKER = "[CONVERSATION SUMMARY]"; + +/** + * Structured Markdown template the summary must follow. Ported verbatim from + * opencode's `SUMMARY_TEMPLATE`. + */ +export const SUMMARY_TEMPLATE = `Output exactly the Markdown structure shown inside