# 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]