4.8 KiB
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
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:
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:
- Load active path via
getActivePath(already exported withinhandlers) - Find summary boundary, collect messages to summarize
- Guard: minimum 4 messages since last summary
- Build summarization prompt
- Call
resolver.Complete(ctx, RoleUtility, ...) - Insert summary tree node with metadata
- Update cursor
- 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:
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:
// 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]