This repository has been archived on 2026-04-03. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
core/docs/archive/DESIGN-0.15.0.md

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:

  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:

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 compactionhandlers.

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: handlerscompactiontreepathhandlers. 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]