From 7e26a2a261f5882dbc7a5dc7680d9bb9bfe11aa7 Mon Sep 17 00:00:00 2001 From: xcaliber Date: Wed, 4 Mar 2026 16:06:12 +0000 Subject: [PATCH] Changeset 0.22.8 (#150) --- docs/DESIGN-0.15.0.md | 584 --------- docs/DESIGN-0.15.1.md | 332 ----- docs/DESIGN-0.17.3.md | 1061 --------------- docs/DESIGN-0.18.0.md | 235 ---- docs/DESIGN-0.18.1.md | 244 ---- docs/DESIGN-0.19.0.md | 1151 ----------------- docs/DESIGN-0.19.1.md | 194 --- docs/DESIGN-0.19.2.md | 116 -- docs/ICD-API.md | 512 +++++++- docs/ICD-AUDIT.md | 4 +- scripts/db-validate.sh | 19 +- server/database/migrations/001_core.sql | 133 ++ .../database/migrations/001_v016_schema.sql | 884 ------------- server/database/migrations/002_teams.sql | 86 ++ .../migrations/002_v017_persona_kb.sql | 42 - server/database/migrations/003_providers.sql | 177 +++ .../migrations/003_v0173_note_links.sql | 35 - server/database/migrations/004_personas.sql | 113 ++ .../migrations/004_v0180_memories.sql | 85 -- server/database/migrations/005_channels.sql | 182 +++ .../migrations/005_v0180_memory_phase2.sql | 46 - server/database/migrations/006_knowledge.sql | 114 ++ server/database/migrations/007_notes.sql | 75 ++ .../migrations/007_v0200_notifications.sql | 17 - server/database/migrations/008_memory.sql | 82 ++ .../008_v0200_notification_prefs.sql | 18 - .../migrations/009_v0210_workspaces.sql | 74 -- server/database/migrations/009_workspaces.sql | 123 ++ ...06_v0190_projects.sql => 010_projects.sql} | 73 +- .../010_v0211_workspace_bindings.sql | 16 - .../database/migrations/011_notifications.sql | 41 + .../migrations/011_v0212_workspace_chunks.sql | 58 - server/database/migrations/012_extensions.sql | 44 + server/database/migrations/012_v0214_git.sql | 21 - server/database/migrations/013_files.sql | 65 + .../database/migrations/013_v0220_health.sql | 43 - .../database/migrations/014_audit_usage.sql | 58 + .../database/migrations/014_v0222_routing.sql | 31 - server/database/migrations/015_health.sql | 23 + .../migrations/015_v0224_health_enhance.sql | 33 - .../database/migrations/sqlite/001_core.sql | 98 ++ .../migrations/sqlite/001_embedding_patch.sql | 22 - .../migrations/sqlite/001_v017_schema.sql | 791 ----------- .../database/migrations/sqlite/002_teams.sql | 59 + .../sqlite/002_v0173_note_links.sql | 26 - .../migrations/sqlite/003_providers.sql | 106 ++ .../migrations/sqlite/003_v0180_memories.sql | 61 - .../migrations/sqlite/004_personas.sql | 57 + .../sqlite/004_v0180_memory_phase2.sql | 22 - .../migrations/sqlite/005_channels.sql | 120 ++ .../migrations/sqlite/005_v0190_projects.sql | 131 -- .../migrations/sqlite/006_knowledge.sql | 73 ++ .../database/migrations/sqlite/007_notes.sql | 34 + .../sqlite/007_v0200_notification_prefs.sql | 13 - .../database/migrations/sqlite/008_memory.sql | 34 + .../sqlite/008_v0210_workspaces.sql | 62 - .../sqlite/009_v0211_workspace_bindings.sql | 9 - .../migrations/sqlite/009_workspaces.sql | 66 + .../migrations/sqlite/010_projects.sql | 62 + .../sqlite/010_v0212_workspace_chunks.sql | 31 - ...otifications.sql => 011_notifications.sql} | 18 +- .../migrations/sqlite/011_v0214_git.sql | 19 - .../migrations/sqlite/012_extensions.sql | 30 + .../migrations/sqlite/012_v0220_health.sql | 35 - .../database/migrations/sqlite/013_files.sql | 61 + .../migrations/sqlite/013_v0222_routing.sql | 24 - .../migrations/sqlite/014_audit_usage.sql | 42 + ...0224_health_enhance.sql => 015_health.sql} | 9 +- server/database/testhelper.go | 6 +- server/extraction/queue.go | 44 +- server/extraction/queue_test.go | 10 +- server/handlers/channels.go | 4 +- server/handlers/completion.go | 50 +- server/handlers/{attachments.go => files.go} | 132 +- server/handlers/integration_test.go | 14 +- server/handlers/personas.go | 2 +- server/knowledge/ingest.go | 2 +- server/main.go | 32 +- server/models/models.go | 47 +- server/pages/pages.go | 6 +- server/pages/templates/admin/settings.html | 2 +- server/pages/templates/surfaces/chat.html | 38 +- server/pages/templates/surfaces/notes.html | 2 +- server/storage/pvc.go | 8 +- server/storage/pvc_test.go | 32 +- server/storage/s3.go | 6 +- server/storage/s3_test.go | 36 +- server/storage/storage.go | 4 +- server/store/interfaces.go | 25 +- server/store/postgres/attachment.go | 210 --- server/store/postgres/file.go | 251 ++++ server/store/postgres/stores.go | 2 +- server/store/sqlite/attachment.go | 213 --- server/store/sqlite/file.go | 256 ++++ server/store/sqlite/stores.go | 2 +- .../{attachment_recall.go => file_recall.go} | 68 +- src/css/persona-kb.css | 2 +- src/css/styles.css | 8 +- src/js/admin-handlers.js | 10 +- src/js/admin-scaffold.js | 4 - src/js/api.js | 72 +- src/js/app-state.js | 4 +- src/js/app.js | 64 +- src/js/chat.js | 28 +- src/js/{attachments.js => files.js} | 142 +- src/js/memory-ui.js | 2 +- src/js/pages.js | 1 - src/js/persona-kb.js | 4 +- src/js/settings-handlers.js | 18 +- src/js/tokens.js | 18 +- src/js/ui-admin.js | 20 +- src/js/ui-core.js | 6 +- src/js/ui-settings.js | 4 +- src/sw.js | 2 +- 114 files changed, 3700 insertions(+), 7572 deletions(-) delete mode 100644 docs/DESIGN-0.15.0.md delete mode 100644 docs/DESIGN-0.15.1.md delete mode 100644 docs/DESIGN-0.17.3.md delete mode 100644 docs/DESIGN-0.18.0.md delete mode 100644 docs/DESIGN-0.18.1.md delete mode 100644 docs/DESIGN-0.19.0.md delete mode 100644 docs/DESIGN-0.19.1.md delete mode 100644 docs/DESIGN-0.19.2.md create mode 100644 server/database/migrations/001_core.sql delete mode 100644 server/database/migrations/001_v016_schema.sql create mode 100644 server/database/migrations/002_teams.sql delete mode 100644 server/database/migrations/002_v017_persona_kb.sql create mode 100644 server/database/migrations/003_providers.sql delete mode 100644 server/database/migrations/003_v0173_note_links.sql create mode 100644 server/database/migrations/004_personas.sql delete mode 100644 server/database/migrations/004_v0180_memories.sql create mode 100644 server/database/migrations/005_channels.sql delete mode 100644 server/database/migrations/005_v0180_memory_phase2.sql create mode 100644 server/database/migrations/006_knowledge.sql create mode 100644 server/database/migrations/007_notes.sql delete mode 100644 server/database/migrations/007_v0200_notifications.sql create mode 100644 server/database/migrations/008_memory.sql delete mode 100644 server/database/migrations/008_v0200_notification_prefs.sql delete mode 100644 server/database/migrations/009_v0210_workspaces.sql create mode 100644 server/database/migrations/009_workspaces.sql rename server/database/migrations/{006_v0190_projects.sql => 010_projects.sql} (64%) delete mode 100644 server/database/migrations/010_v0211_workspace_bindings.sql create mode 100644 server/database/migrations/011_notifications.sql delete mode 100644 server/database/migrations/011_v0212_workspace_chunks.sql create mode 100644 server/database/migrations/012_extensions.sql delete mode 100644 server/database/migrations/012_v0214_git.sql create mode 100644 server/database/migrations/013_files.sql delete mode 100644 server/database/migrations/013_v0220_health.sql create mode 100644 server/database/migrations/014_audit_usage.sql delete mode 100644 server/database/migrations/014_v0222_routing.sql create mode 100644 server/database/migrations/015_health.sql delete mode 100644 server/database/migrations/015_v0224_health_enhance.sql create mode 100644 server/database/migrations/sqlite/001_core.sql delete mode 100644 server/database/migrations/sqlite/001_embedding_patch.sql delete mode 100644 server/database/migrations/sqlite/001_v017_schema.sql create mode 100644 server/database/migrations/sqlite/002_teams.sql delete mode 100644 server/database/migrations/sqlite/002_v0173_note_links.sql create mode 100644 server/database/migrations/sqlite/003_providers.sql delete mode 100644 server/database/migrations/sqlite/003_v0180_memories.sql create mode 100644 server/database/migrations/sqlite/004_personas.sql delete mode 100644 server/database/migrations/sqlite/004_v0180_memory_phase2.sql create mode 100644 server/database/migrations/sqlite/005_channels.sql delete mode 100644 server/database/migrations/sqlite/005_v0190_projects.sql create mode 100644 server/database/migrations/sqlite/006_knowledge.sql create mode 100644 server/database/migrations/sqlite/007_notes.sql delete mode 100644 server/database/migrations/sqlite/007_v0200_notification_prefs.sql create mode 100644 server/database/migrations/sqlite/008_memory.sql delete mode 100644 server/database/migrations/sqlite/008_v0210_workspaces.sql delete mode 100644 server/database/migrations/sqlite/009_v0211_workspace_bindings.sql create mode 100644 server/database/migrations/sqlite/009_workspaces.sql create mode 100644 server/database/migrations/sqlite/010_projects.sql delete mode 100644 server/database/migrations/sqlite/010_v0212_workspace_chunks.sql rename server/database/migrations/sqlite/{006_v0200_notifications.sql => 011_notifications.sql} (50%) delete mode 100644 server/database/migrations/sqlite/011_v0214_git.sql create mode 100644 server/database/migrations/sqlite/012_extensions.sql delete mode 100644 server/database/migrations/sqlite/012_v0220_health.sql create mode 100644 server/database/migrations/sqlite/013_files.sql delete mode 100644 server/database/migrations/sqlite/013_v0222_routing.sql create mode 100644 server/database/migrations/sqlite/014_audit_usage.sql rename server/database/migrations/sqlite/{014_v0224_health_enhance.sql => 015_health.sql} (57%) rename server/handlers/{attachments.go => files.go} (80%) delete mode 100644 server/store/postgres/attachment.go create mode 100644 server/store/postgres/file.go delete mode 100644 server/store/sqlite/attachment.go create mode 100644 server/store/sqlite/file.go rename server/tools/{attachment_recall.go => file_recall.go} (64%) rename src/js/{attachments.js => files.js} (87%) diff --git a/docs/DESIGN-0.15.0.md b/docs/DESIGN-0.15.0.md deleted file mode 100644 index b6c8e7a..0000000 --- a/docs/DESIGN-0.15.0.md +++ /dev/null @@ -1,584 +0,0 @@ -# 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, -// including system prompt overhead. -func EstimatePath(path []treepath.PathMessage, systemPromptLen int) int { - total := 0 - if systemPromptLen > 0 { - total += EstimateTokens(string(make([]byte, systemPromptLen))) + 4 - } - for _, m := range path { - total += EstimateTokens(m.Content) + 4 // +4 per-message overhead - } - return total -} -``` - -Not exact, but it's the same bar the user already sees in the UI. If we -need better accuracy later, swap in a tokenizer library without changing -the interface. - -### Context budget resolution - -The scanner needs `max_context` for whatever model the channel is using. -Resolution chain: - -1. Channel has a `model` field → look up `model_catalog` for capabilities -2. Channel has a `provider_config_id` → use that provider's catalog entry -3. Fallback: use the utility role's model context window as a proxy (it's - the model that would be doing the summarization anyway) -4. Hard fallback: 128K tokens (conservative default) - -```go -func (s *Scanner) getContextBudget(ctx context.Context, ch *models.Channel) int { - if ch.Model != "" { - if caps, err := s.stores.Catalog.GetCapabilities(ctx, ch.Model); err == nil { - if caps.MaxContext > 0 { - return caps.MaxContext - } - } - } - return DefaultContextBudget // 128000 -} -``` - ---- - -## 3. Background Scanner - -Follows the Ingester pattern from `knowledge/ingest.go`: goroutine pool -with semaphore, WaitGroup for graceful shutdown. - -### Scanner type - -```go -// compaction/scanner.go - -type Scanner struct { - service *Service - stores store.Stores - sem chan struct{} - wg sync.WaitGroup - stopCh chan struct{} - ticker *time.Ticker -} - -type ScannerConfig struct { - Enabled bool // global kill switch - Interval time.Duration // scan frequency (default: 5 minutes) - Concurrency int // max parallel compactions (default: 2) -} - -func NewScanner(svc *Service, stores store.Stores, cfg ScannerConfig) *Scanner -func (sc *Scanner) Start() -func (sc *Scanner) Stop() // signals stop + drains in-flight -``` - -### Scan loop - -``` -every : - if !globalEnabled(): continue - - candidates = findCandidates() - for each candidate: - sem.acquire() - go compact(candidate) -``` - -### Candidate query - -A single SQL query finds channels that need compaction: - -```sql -SELECT c.id, c.user_id, c.model, c.settings, - COUNT(m.id) as msg_count, - SUM(LENGTH(m.content)) as total_chars -FROM channels c -JOIN messages m ON m.channel_id = c.id - AND m.deleted_at IS NULL -WHERE c.deleted_at IS NULL - AND c.type = 'direct' -- only user conversations (not groups yet) - AND c.is_archived = false - -- Skip channels with very recent activity (let the user finish typing) - AND c.updated_at < NOW() - INTERVAL '2 minutes' - -- Only channels active in the last 7 days (don't compact stale channels) - AND c.updated_at > NOW() - INTERVAL '7 days' -GROUP BY c.id -HAVING COUNT(m.id) >= 10 -- minimum message count - AND SUM(LENGTH(m.content)) > 20000 -- rough: 20K chars ≈ 5K tokens minimum -ORDER BY c.updated_at DESC -LIMIT 50 -- batch size cap -``` - -This is a coarse filter. Each candidate then gets a precise check: - -```go -func (sc *Scanner) shouldCompact(ctx context.Context, ch *models.Channel) bool { - // 1. Check channel-level opt-out - if ch.Settings != nil { - if v, ok := ch.Settings["auto_compaction"]; ok && v == false { - return false - } - } - - // 2. Load active path for the channel owner - path, err := treepath.GetActivePath(ch.ID, ch.UserID) - if err != nil || len(path) < 10 { - return false - } - - // 3. Find post-summary messages only - startIdx := 0 - for i, m := range path { - if treepath.IsSummaryMessage(&m) { - startIdx = i + 1 - } - } - postSummary := path[startIdx:] - if len(postSummary) < 8 { - return false // not enough new messages since last summary - } - - // 4. Estimate tokens and check threshold - tokens := EstimatePath(postSummary, 0) - budget := sc.getContextBudget(ctx, ch) - threshold := sc.getThreshold(ch) // default 0.70 - - return float64(tokens) / float64(budget) >= threshold -} -``` - -### Threshold - -Default: **0.70** (70% of context window). This is intentionally lower -than the frontend's 75% warning bar — auto-compaction should fire before -the user sees a warning, not after. - -Configurable per-channel via `settings.compaction_threshold` (float) and -globally via `global_settings` key `auto_compaction_threshold`. - -### Rate limiting - -Auto-compaction calls use the utility role and are org-funded. They share -the existing `utility_rate_limit` budget but get a separate counter -prefix so admins can see auto vs manual usage: - -```go -entry := &models.UsageEntry{ - // ... - Role: &role, // "utility" - Metadata: models.JSONMap{ - "trigger": "auto", // distinguishes from manual - }, -} -``` - -If the global utility rate limit is hit, auto-compaction silently skips -the channel and retries next scan cycle. It never blocks or errors. - ---- - -## 4. Per-Channel Configuration - -Uses the existing `Channel.Settings` JSONB column. No migration needed. - -| Key | Type | Default | Description | -|-----|------|---------|-------------| -| `auto_compaction` | `bool` | `true` (from global) | Enable/disable for this channel | -| `compaction_threshold` | `float` | `0.70` (from global) | Context usage ratio to trigger | - -### Frontend: channel settings - -Add a "Compaction" section to the channel settings panel (the gear icon -in the chat header). Two controls: - -``` -┌─ Compaction ──────────────────────────────────┐ -│ Auto-compact [✓] │ -│ Threshold [70]% of context window │ -└───────────────────────────────────────────────┘ -``` - -The manual "Summarize & Continue" button remains in the context warning -bar and works regardless of auto-compaction settings. - ---- - -## 5. Admin Controls - -New `global_settings` keys: - -| Key | Type | Default | Description | -|-----|------|---------|-------------| -| `auto_compaction_enabled` | `bool` | `false` | Global kill switch. Ships **off** — admins opt in. | -| `auto_compaction_threshold` | `float` | `0.70` | Default threshold for channels without override | -| `auto_compaction_interval_minutes` | `int` | `5` | Scanner tick interval | -| `auto_compaction_concurrency` | `int` | `2` | Max parallel auto-compactions | -| `auto_compaction_cooldown_minutes` | `int` | `30` | Min time between auto-compactions of the same channel | - -### Admin UI - -Add an "Auto-Compaction" card to the existing admin AI settings panel -(alongside the utility rate limit and model roles): - -``` -┌─ Auto-Compaction ─────────────────────────────┐ -│ Enabled [✓] │ -│ Threshold [70]% │ -│ Scan interval [5] minutes │ -│ Concurrency [2] parallel │ -│ Cooldown [30] minutes per channel │ -└───────────────────────────────────────────────┘ -``` - ---- - -## 6. Cooldown + Dedup - -Prevent rapid re-compaction of the same channel: - -1. **Cooldown** — after compacting channel X, don't re-evaluate it for - `cooldown_minutes`. Track in-memory: - ```go - lastCompacted map[string]time.Time // channelID → timestamp - ``` - -2. **In-flight dedup** — if channel X is currently being compacted (sem - acquired, goroutine running), skip it in the current scan. Track via: - ```go - inFlight sync.Map // channelID → struct{} - ``` - -3. **Stacking** — the existing summary boundary logic already handles - multiple summaries correctly. Auto-compaction produces the same tree - nodes as manual, so stacking "just works." - ---- - -## 7. Lifecycle + Wiring - -### main.go changes - -```go -// After role resolver and stores setup: -compactionSvc := compaction.NewService(stores, roleResolver) -compactionScanner := compaction.NewScanner(compactionSvc, stores, compaction.ScannerConfig{ - Enabled: getGlobalBool(stores, "auto_compaction_enabled", false), - Interval: time.Duration(getGlobalInt(stores, "auto_compaction_interval_minutes", 5)) * time.Minute, - Concurrency: getGlobalInt(stores, "auto_compaction_concurrency", 2), -}) -compactionScanner.Start() -defer compactionScanner.Stop() // drain in-flight on shutdown - -// Summarize handler now wraps compaction service: -summarize := handlers.NewSummarizeHandler(stores, roleResolver, compactionSvc) -``` - -Follows the same pattern as `kbIngester` with `defer kbIngester.Wait()`. - -### Config reload - -The scanner re-reads global settings at the start of each scan cycle. -No restart required to change thresholds, intervals, or the kill switch. -If `auto_compaction_enabled` flips to `false`, the scanner skips all -work on the next tick. - ---- - -## 8. Observability - -### Logging - -``` -🔍 compaction: scanning 50 candidates -🔍 compaction: channel abc123 qualifies (78% of 128K context, 42 messages post-summary) -📝 compaction: compacting channel abc123 for user xyz (trigger=auto) -✅ compaction: channel abc123 done (42 messages → 847 chars, model=claude-sonnet-4-5-20250929) -⏭ compaction: channel def456 skipped (cooldown, 12m remaining) -⏭ compaction: channel ghi789 skipped (rate limit) -``` - -### Audit log - -Auto-compaction events are logged to `audit_log`: - -```go -stores.Audit.Log(ctx, &models.AuditEntry{ - Actor: "system:compaction", - Action: "compaction.auto", - Resource: "channel:" + channelID, - Details: models.JSONMap{ - "summarized_count": result.SummarizedCount, - "model": result.Model, - "trigger": "auto", - }, -}) -``` - -### Usage tracking - -All auto-compaction calls flow through the existing usage logging path -(same as manual summarization). The `metadata.trigger = "auto"` field -lets the admin usage dashboard distinguish auto from manual. - ---- - -## 9. Frontend Changes - -Minimal — the backend does the heavy lifting. - -### Auto-compaction indicator - -When loading a conversation that was auto-compacted, the frontend -already handles summary boundaries correctly (the collapsed history -toggle, the "X earlier messages summarized" bar). The only new UI is -a subtle indicator on the summary node: - -``` -▸ 42 earlier messages summarized (auto) -``` - -The `(auto)` label comes from `metadata.trigger` on the summary message. - -### Channel settings - -Add compaction controls to the channel settings panel (Section 4 above). -Saves to `PATCH /channels/:id` with `settings` field — no new endpoint. - -### Admin panel - -Add the auto-compaction card to the AI settings section (Section 5). -Saves via existing `PUT /admin/settings/:key` endpoints. - ---- - -## 10. Migration - -**No new migration required.** - -- Channel compaction settings → existing `Channel.Settings` JSONB -- Admin settings → existing `global_settings` table -- Summary messages → existing `messages` table with metadata -- Usage tracking → existing `usage_log` table -- Audit logging → existing `audit_log` table - ---- - -## 11. File Manifest - -``` -server/ - treepath/ ← NEW package - path.go (extracted from handlers/tree.go) - summary.go (isSummaryMessage + helpers) - siblings.go (sibling queries) - cursor.go (cursor + sibling index) - compaction/ ← NEW package - compaction.go (Service, Compact, CompactRequest/Result) - scanner.go (Scanner, scan loop, candidate query) - estimator.go (EstimateTokens, EstimatePath) - handlers/ - tree.go (updated: delegates to treepath) - summarize.go (updated: delegates to compaction.Service) - completion.go (updated: imports treepath) - main.go (updated: wire compaction scanner) -src/js/ - ui-core.js (updated: show "(auto)" on auto-compacted summaries) - ui-settings.js (updated: channel compaction controls) - ui-admin.js (updated: auto-compaction admin card) -``` - ---- - -## 12. Implementation Order - -**Phase 1: Extract** — Move tree helpers to `treepath`, core -summarization to `compaction.Service`. Refactor `SummarizeHandler` to -delegate. All existing tests must still pass. No new behavior. - -**Phase 2: Estimator + Scanner** — Add `estimator.go`, `scanner.go`, -candidate query, `shouldCompact` logic. Wire into `main.go` with -`Start()`/`Stop()`. Global settings for admin controls. Ship with -`auto_compaction_enabled` defaulting to `false`. - -**Phase 3: Frontend** — Channel compaction settings, admin panel card, -auto-compaction indicator on summary nodes. - -**Phase 4: Soak + tune** — Enable on a test instance, watch logs, tune -default threshold and cooldown. Adjust candidate query filters if scan -is too expensive. - ---- - -## 13. What This Intentionally Doesn't Do - -- **Token-accurate estimation** — chars/4 is good enough. A real - tokenizer adds a dependency for marginal accuracy gain. Revisit if - users report summaries firing too early or too late. - -- **Per-model prompt tuning** — the summarization prompt is one-size-fits - -all. Works fine across Claude/GPT/Llama. Revisit if quality varies - significantly across providers. - -- **Group channel compaction** — auto-compaction targets `direct` channels - only (single-owner). Group/channel types need multi-cursor awareness - (which user's path to compact?). Deferred to v0.19.0 multi-participant. - -- **Compaction snapshots for KB** — the roadmap mentions this. The - summary nodes we create here are already stored as messages with full - metadata. KB integration can query them later without changes to this - design. - -- **Event-driven triggers** — a post-completion hook that checks - immediately after each message would reduce latency vs. the ticker. - Worth adding later but the ticker is simpler to ship and debug. The - 2-minute activity gap in the candidate query achieves a similar effect - (don't compact mid-conversation). diff --git a/docs/DESIGN-0.15.1.md b/docs/DESIGN-0.15.1.md deleted file mode 100644 index e57d653..0000000 --- a/docs/DESIGN-0.15.1.md +++ /dev/null @@ -1,332 +0,0 @@ -# DESIGN-0.15.1 — Context Recall Tools - -## Overview - -After compaction (v0.15.0) or in long conversations, the model loses access -to earlier material — attachment content injected once at send time, and -message details compressed into summaries. These tools let the model -re-read source material on demand. - -Depends on: file handling (v0.12.0), tool framework (v0.11.0), compaction (v0.15.0). - -**Four work streams:** - -1. `attachment_recall` — re-read attachment content from storage -2. `conversation_search` — keyword search over channel messages -3. Token estimator improvements — attachment-aware context counting -4. KB auto-injection — automatic knowledge base context in system prompt - ---- - -## 1. `attachment_recall` Tool - -### Purpose - -Attachments are injected into context once (at the user message that -references them). On subsequent turns the model relies on its own response -as a natural summary. After compaction, that summary is gone. This tool -lets the model re-read any attachment in the current channel. - -### Two-action design - -Single tool, `action` parameter selects behavior: - -- **`list`** — returns metadata for all attachments in the channel - (id, filename, content_type, size, created_at). The model calls this - first to discover what's available. -- **`read`** — returns content for a specific attachment_id. - Documents → `extracted_text` from DB. Images → base64 data URI. - -### Security - -Channel-scoped: `attachment_id` must belong to `ExecutionContext.ChannelID`. -The store query is `GetByChannel(channelID)` so cross-channel access is -impossible by construction. - -### Dependencies - -Late registration (same pattern as `kb_search`): - -```go -func RegisterAttachmentRecall(stores store.Stores, objStore storage.ObjectStore) -``` - -Called from `main.go` after storage init. If `objStore` is nil (storage -not configured), the tool is **not registered** — it simply won't appear -in the tool list. - -### Implementation: `server/tools/attachment_recall.go` - -```go -type attachmentRecallTool struct { - stores store.Stores - objStore storage.ObjectStore -} - -func (t *attachmentRecallTool) Definition() ToolDef { - return ToolDef{ - Name: "attachment_recall", - DisplayName: "Attachments", - Category: "context", - Description: "Re-read file attachments from this conversation. " + - "Use action='list' to see available files, then action='read' " + - "with an attachment_id to get the content.", - Parameters: JSONSchema(map[string]interface{}{ - "action": PropEnum("Action to perform", "list", "read"), - "attachment_id": Prop("string", "Attachment ID (required for read)"), - }, []string{"action"}), - } -} -``` - -**`list` action returns:** -```json -{ - "attachments": [ - { - "id": "abc-123", - "filename": "report.pdf", - "content_type": "application/pdf", - "size_bytes": 45200, - "has_text": true, - "created_at": "2026-02-26T10:00:00Z" - } - ], - "count": 1 -} -``` - -**`read` action returns:** -```json -{ - "id": "abc-123", - "filename": "report.pdf", - "content_type": "application/pdf", - "content": "... extracted text ...", - "content_type_returned": "text" -} -``` - -For images: -```json -{ - "id": "def-456", - "filename": "diagram.png", - "content_type": "image/png", - "content": "data:image/png;base64,...", - "content_type_returned": "base64_image" -} -``` - -Image base64 is capped at 10MB to prevent context blow-up. Larger images -return an error suggesting the user re-upload. - ---- - -## 2. `conversation_search` Tool - -### Purpose - -After compaction, message details are compressed into summaries. If the -user asks about something the summary omitted, the model needs to search -the original messages. Also useful in long pre-compaction conversations -where the model's attention has drifted. - -### Design - -Keyword search (full-text) over messages in the current channel using -PostgreSQL's `plainto_tsquery`. No migration needed — `to_tsvector` is -computed on the fly. This is acceptable because: - -- Search is scoped to a single channel (bounded message count) -- Called infrequently (tool invocation, not every request) -- Channel messages rarely exceed ~10K rows - -If performance becomes an issue, a `search_vector tsvector` column with -GIN index can be added in a future migration. - -### Parameters - -```go -Parameters: JSONSchema(map[string]interface{}{ - "query": Prop("string", "Search keywords"), - "max_results": map[string]interface{}{ - "type": "integer", - "description": "Maximum results (1-20, default 5)", - }, - "role_filter": PropEnum("Filter by message role", "all", "user", "assistant"), -}, []string{"query"}) -``` - -### Returns - -```json -{ - "results": [ - { - "message_id": "...", - "role": "user", - "excerpt": "...highlighted excerpt...", - "timestamp": "2026-02-26T10:00:00Z", - "rank": 0.95 - } - ], - "query": "original query", - "total": 3, - "channel_id": "..." -} -``` - -Uses `ts_headline` for highlighted excerpts (same pattern as `note_search`). - -### Security - -Channel-scoped via `ExecutionContext.ChannelID`. Only searches messages -in the current channel. Soft-deleted messages (`deleted_at IS NOT NULL`) -are excluded. - -### Dependencies - -Late registration: - -```go -func RegisterConversationSearch(stores store.Stores) -``` - -No external dependencies beyond stores (uses `database.DB` directly for -the full-text query, same pattern as `note_search`). - ---- - -## 3. Token Estimator Improvements - -### Frontend (`tokens.js`) - -Current `estimateConversation` counts message text only. Enhancements: - -- **Staged attachments**: Before send, show estimated tokens for queued - attachments. Documents: `extracted_text.length / 4`. Images: flat - ~765 tokens per image (OpenAI's base tile estimate, reasonable cross-provider). -- **Sent attachments**: After send, the augmented content is already in - the message text (documents are inlined). Images were ephemeral - ContentParts — add their estimate to the message's token count. - -### Implementation - -Add to `Tokens`: - -```javascript -estimateAttachments(attachments) { - let total = 0; - for (const att of attachments) { - if (att.content_type?.startsWith('image/')) { - total += 765; // base tile estimate - } else if (att.extracted_text) { - total += this.estimate(att.extracted_text); - } else { - total += Math.ceil(att.size_bytes / 4); // rough fallback - } - } - return total; -} -``` - -Wire into `updateInputTokens()` to include staged attachment estimates. - ---- - -## 4. KB Auto-Injection - -### Purpose - -Currently the model must explicitly call `kb_search` to access knowledge -base content. For common patterns (channel linked to a KB, user asks a -question), automatic injection into the system prompt is more natural. - -### Design - -In the completion handler, after loading conversation history and before -building the provider request: - -1. Check if channel has linked KBs (or user has personal KBs) -2. Extract the user's latest message as the query -3. Run similarity search (top-K, K=3 default) -4. If results found AND context budget allows, prepend to system prompt - -### Context budget check - -```go -contextUsed := estimateTokens(systemPrompt + allMessages) -kbTokens := estimateTokens(kbContent) -if contextUsed + kbTokens > maxContext * 0.85 { - // Skip injection — context too full - log.Printf("⏭ KB auto-inject skipped: context %.0f%% full", pct*100) -} -``` - -### Channel settings - -```json -{ - "kb_injection": "auto" | "tool_only" | "both" | "off" -} -``` - -Default: `"auto"` — inject when KBs are linked. `"tool_only"` disables -auto-injection but keeps the `kb_search` tool available. `"both"` does -both. `"off"` disables all KB context. - -### System prompt format - -``` -[Knowledge Base Context] -The following information was retrieved from the user's knowledge bases -and may be relevant to this conversation: - ---- -Source: Technical Docs (chunk 3 of report.pdf, similarity: 0.87) -Content: ... - ---- -Source: FAQ (chunk 1 of faq.md, similarity: 0.82) -Content: ... -[End Knowledge Base Context] -``` - ---- - -## File Inventory - -### New files - -| File | Description | -|------|-------------| -| `server/tools/attachment_recall.go` | attachment_recall tool | -| `server/tools/conversation_search.go` | conversation_search tool | -| `docs/DESIGN-0.15.1.md` | This design doc | - -### Modified files - -| File | Change | -|------|--------| -| `server/main.go` | Register new tools | -| `server/handlers/completion.go` | KB auto-injection in completion flow | -| `src/js/tokens.js` | Attachment-aware token estimation | -| `docs/ROADMAP.md` | Mark v0.15.1 items | - -### No migration required - -- `conversation_search` uses on-the-fly `to_tsvector` (no new column) -- `attachment_recall` reads existing `extracted_text` column -- KB auto-injection uses existing `kb_search` infrastructure - ---- - -## Implementation Order - -1. **Phase 1**: `attachment_recall` tool + registration ✅ -2. **Phase 2**: `conversation_search` tool + registration ✅ -3. **Phase 3**: Token estimator improvements (frontend) ✅ -4. **Phase 4**: KB auto-injection (completion handler) — separate delivery, - requires adding embedder dependency to `CompletionHandler` and careful - latency budgeting (embedding query on every completion call) diff --git a/docs/DESIGN-0.17.3.md b/docs/DESIGN-0.17.3.md deleted file mode 100644 index 46ab2ff..0000000 --- a/docs/DESIGN-0.17.3.md +++ /dev/null @@ -1,1061 +0,0 @@ -# DESIGN: Notes Rich Text Editor, Obsidian-Style Linking & Knowledge Graph - -**Version:** v0.17.3 -**Status:** Draft -**Scope:** Upgrade the notes editor from a plain ` -``` -To: -```html -
-``` - -With graceful fallback: if `window.CM?.noteEditor` is undefined, create a -`