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

7.9 KiB

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):

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):

                <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.jsloadAdminSettings()

Add these lines after the Web Search config loading block (after document.getElementById('searxngConfigFields').style.display = ...):

            // 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.jshandleSaveAdminSettings()

Add these lines after the search_config save block (before UI.toast('Settings saved', 'success')):

        // 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:

    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:

    _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