# DESIGN-0.15.0 — Compaction ## Overview Automatic conversation compaction. A background service monitors channels for context pressure and triggers summarization via the utility model role, replacing the manual \"Summarize & Continue\" button as the primary compaction path. Manual summarization remains available as an explicit user action. Depends on: utility model role (v0.10.0), summarize handler (v0.10.2). **Design principle: don't reinvent the wheel.** The existing `SummarizeHandler` already has the full pipeline — path loading, summary boundary detection, prompt construction, role resolution, tree insertion, cursor update, usage logging. This design extracts that core logic into a shared `compaction` package and adds a background scanner on top. --- ## 1. Extract: `compaction` Package Move the reusable summarization pipeline out of `handlers/summarize.go` into `server/compaction/compaction.go`. The HTTP handler becomes a thin wrapper. ### Core type ```go package compaction // Service provides conversation compaction (summarization). // Used by both the HTTP handler (manual) and the background scanner (auto). type Service struct { stores store.Stores resolver *roles.Resolver } func NewService(stores store.Stores, resolver *roles.Resolver) *Service { return &Service{stores: stores, resolver: resolver} } ``` ### Extracted method: `Compact` The current `SummarizeHandler.Summarize` body becomes `Service.Compact`: ```go type CompactRequest struct { ChannelID string UserID string // channel owner TeamID *string // for role resolution Trigger string // \"manual\" | \"auto\" } type CompactResult struct { SummaryID string SummarizedCount int Model string UsedFallback bool Content string InputTokens int OutputTokens int } func (s *Service) Compact(ctx context.Context, req CompactRequest) (*CompactResult, error) ``` The method performs the same steps as today's handler: 1. Load active path via `getActivePath` (already exported within `handlers`) 2. Find summary boundary, collect messages to summarize 3. Guard: minimum 4 messages since last summary 4. Build summarization prompt 5. Call `resolver.Complete(ctx, RoleUtility, ...)` 6. Insert summary tree node with metadata 7. Update cursor 8. Log usage The only new metadata field is `\"trigger\"` (`\"manual\"` or `\"auto\"`) so the frontend can distinguish how the summary was created. ### Refactored handler `handlers/summarize.go` shrinks to: ```go func (h *SummarizeHandler) Summarize(c *gin.Context) { // ownership check, rate limit check (unchanged) // ... result, err := h.compaction.Compact(c.Request.Context(), compaction.CompactRequest{ ChannelID: channelID, UserID: userID, TeamID: teamID, Trigger: \"manual\", }) // return JSON response (unchanged) } ``` ### Tree helpers `getActivePath`, `isSummaryMessage`, `nextSiblingIndex`, `updateCursor` are currently unexported in `handlers/tree.go`. Two options: **Option A** — Export them from `handlers` and import in `compaction`. Clean but creates a dependency from `compaction` → `handlers`. **Option B** — Move tree helpers into a `treepath` (or similar) package imported by both `handlers` and `compaction`. Recommend **Option B** to keep the dependency graph clean: `handlers` → `compaction` → `treepath` ← `handlers`. The `treepath` package is pure data + SQL queries with no handler logic. ``` server/ treepath/ ← NEW: extracted from handlers/tree.go path.go (getActivePath, getPathToLeaf, getActiveLeaf) summary.go (isSummaryMessage, summary metadata helpers) siblings.go (getSiblingCount, getSiblings, findLeafFromMessage) cursor.go (updateCursor, nextSiblingIndex) compaction/ ← NEW compaction.go (Service, Compact) scanner.go (Scanner, background loop) estimator.go (token estimation) handlers/ tree.go → thin wrappers or deleted, imports treepath summarize.go → thin HTTP wrapper, delegates to compaction.Service completion.go → imports treepath for path building ``` --- ## 2. Server-Side Token Estimation The background scanner needs to evaluate context pressure without making an LLM call. The frontend already does this with a `chars / 4` heuristic (`tokens.js`). Replicate server-side: ```go // compaction/estimator.go // EstimateTokens returns a rough token count using the ~4 chars/token // heuristic. Matches the frontend Tokens.estimate() for consistency. func EstimateTokens(text string) int { return (len(text) + 3) / 4 } // EstimatePath returns total estimated tokens for a message path, ``` [Full content from read_file used in tool call, truncated here for brevity]