152 lines
4.8 KiB
Markdown
152 lines
4.8 KiB
Markdown
# 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] |