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/CHANGES-0.15.0-phase2.md
2026-02-26 21:19:55 +00:00

187 lines
7.9 KiB
Markdown

# 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 `<section class="admin-section">` with
`🔐 Encryption`):
```html
<section class="settings-section">
<h3>Auto-Compaction</h3>
<label class="checkbox-label"><input type="checkbox" id="adminCompactionEnabled"> Enable automatic conversation compaction</label>
<p class="section-hint">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.</p>
<div id="compactionConfigFields" style="display:none;margin-top:8px">
<div class="form-row">
<div class="form-group">
<label>Threshold</label>
<div class="form-row" style="gap:4px;align-items:center">
<input type="number" id="adminCompactionThreshold" value="70" min="10" max="95" step="5" style="width:70px">
<span class="form-hint">% of context window</span>
</div>
</div>
<div class="form-group">
<label>Cooldown</label>
<div class="form-row" style="gap:4px;align-items:center">
<input type="number" id="adminCompactionCooldown" value="30" min="5" max="360" step="5" style="width:70px">
<span class="form-hint">minutes per channel</span>
</div>
</div>
</div>
</div>
</section>
```
---
### 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 `
<div class="message-summary" data-msg-id="${esc(msg.id || '')}">
<div class="summary-header">
<span>📝 Conversation summary (${count} messages, by ${esc(model)}${trigger})</span>
</div>
<div class="msg-text">${formatMessage(msg.content)}</div>
</div>`;
},
```
---
## 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