# v0.15.0 Phase 2 — Changeset Notes ## Files included in this zip (new) ``` server/compaction/estimator.go NEW — EstimateTokens, EstimatePath, EstimatePathFromSummary server/compaction/estimator_test.go NEW — unit tests (no DB required) server/compaction/scanner.go NEW — Scanner, scan loop, candidate query, shouldCompact ``` ## Surgical edits needed in existing files ### 1. `server/main.go` Phase 1 already introduced `compactionSvc`. Now add the scanner below it (after the `compactionSvc` line, before route registration): ```go import "git.gobha.me/xcaliber/chat-switchboard/compaction" // already present from Phase 1 // ── Auto-Compaction Scanner ─────────── compactionScanner := compaction.NewScanner(compactionSvc, stores, compaction.ScannerConfig{ Interval: 5 * time.Minute, Concurrency: 2, }) compactionScanner.Start() defer compactionScanner.Stop() // drain in-flight compactions on shutdown ``` Place `defer compactionScanner.Stop()` near the existing `defer kbIngester.Wait()` line. **Note:** The scanner re-reads all config from `global_settings` each tick. The `ScannerConfig` struct only sets the ticker interval and semaphore size at startup. Everything else (enabled, threshold, cooldown) is dynamic. --- ### 2. `src/index.html` Add an auto-compaction section to the admin Settings tab. Insert **before** the Encryption section (before `
` with `🔐 Encryption`): ```html

Auto-Compaction

When enabled, long conversations are automatically summarized in the background using the utility model. Ships off by default — requires a utility model role to be configured.

``` --- ### 3. `src/js/ui-admin.js` — `loadAdminSettings()` Add these lines **after** the Web Search config loading block (after `document.getElementById('searxngConfigFields').style.display = ...`): ```javascript // Auto-Compaction (global_settings) const compactionCfg = getSetting('auto_compaction', {}) || {}; const compactionEnabled = document.getElementById('adminCompactionEnabled'); if (compactionEnabled) { compactionEnabled.checked = !!compactionCfg.enabled; document.getElementById('compactionConfigFields').style.display = compactionCfg.enabled ? '' : 'none'; } const compThreshold = document.getElementById('adminCompactionThreshold'); if (compThreshold) compThreshold.value = String(compactionCfg.threshold || 70); const compCooldown = document.getElementById('adminCompactionCooldown'); if (compCooldown) compCooldown.value = String(compactionCfg.cooldown || 30); ``` --- ### 4. `src/js/settings-handlers.js` — `handleSaveAdminSettings()` Add these lines **after** the `search_config` save block (before `UI.toast('Settings saved', 'success')`): ```javascript // Auto-Compaction config → global_settings const compactionEnabled = document.getElementById('adminCompactionEnabled')?.checked || false; const compactionThreshold = parseInt(document.getElementById('adminCompactionThreshold')?.value) || 70; const compactionCooldown = parseInt(document.getElementById('adminCompactionCooldown')?.value) || 30; await API.adminUpdateSetting('auto_compaction', { value: { enabled: compactionEnabled, threshold: compactionThreshold, cooldown: compactionCooldown, }}); // Persist individual keys for scanner (reads these independently) await API.adminUpdateSetting('auto_compaction_enabled', { value: compactionEnabled }); await API.adminUpdateSetting('auto_compaction_threshold', { value: compactionThreshold / 100 }); await API.adminUpdateSetting('auto_compaction_cooldown_minutes', { value: compactionCooldown }); ``` --- ### 5. `src/js/settings-handlers.js` — `_initSettingsListeners()` Add a toggle listener for the compaction checkbox, alongside the existing banner toggle. Add **after** the `adminBannerEnabled` change listener: ```javascript document.getElementById('adminCompactionEnabled')?.addEventListener('change', (e) => { document.getElementById('compactionConfigFields').style.display = e.target.checked ? '' : 'none'; }); ``` --- ### 6. `src/js/ui-core.js` — `_summaryHTML()` Show "(auto)" label on auto-triggered summaries. The `trigger` field is already in message metadata from Phase 1. **Replace** the `_summaryHTML` method: ```javascript _summaryHTML(msg) { const meta = msg.metadata || {}; const count = meta.summarized_count || '?'; const model = meta.utility_model || 'utility model'; const trigger = meta.trigger === 'auto' ? ' · auto' : ''; return `
📝 Conversation summary (${count} messages, by ${esc(model)}${trigger})
${formatMessage(msg.content)}
`; }, ``` --- ## How the scanner reads settings The scanner calls `GlobalConfig.Get()` **every tick** for these keys: | Key | Read by | How stored | |-----|---------|------------| | `auto_compaction_enabled` | `isEnabled()` | `{value: true/false}` | | `auto_compaction_threshold` | `getThreshold()` | `{value: 0.70}` (float 0-1) | | `auto_compaction_cooldown_minutes` | `getCooldownDuration()` | `{value: 30}` | The admin UI stores a combined `auto_compaction` key (for display) AND the individual keys (for scanner consumption). This avoids the scanner needing to parse a composite object. Interval and concurrency are set at startup via `ScannerConfig` and require a restart to change. This is intentional — they affect the goroutine pool size and ticker, which shouldn't be hot-swapped. ## Verification 1. `cd server && go test ./compaction/` — estimator tests pass (no DB) 2. `cd server && go build .` — compiles cleanly 3. Manual test: Admin → Settings → enable auto-compaction → Save → check server logs for `🔍 compaction scanner started` 4. Manual test: with utility role configured and a long conversation, verify auto-compaction fires after the threshold is exceeded 5. Verify cooldown: same channel should not re-compact within 30 minutes 6. Verify kill switch: disable auto-compaction → scanner stops processing on next tick