Skip to content

perf(assistant): bounded history loads + compaction trigger hysteresis - #219

Draft
juacker wants to merge 1 commit into
mainfrom
perf/compaction-bounded-loads-and-trigger
Draft

juacker wants to merge 1 commit into
mainfrom
perf/compaction-bounded-loads-and-trigger

Conversation

@juacker

@juacker juacker commented Sep 21, 2026

Copy link
Copy Markdown
Collaborator

Problem

Two compaction-related performance problems, confirmed by analysis of a real long session (12,064 messages / 64 MB):

  1. Full-session DB rescans every iteration. The engine loop re-loaded the entire session history from SQLite on every LLM iteration (engine.rs:222) — ~9 call sites did full scans. On the measured session that is 64 MB per iteration, ~6.4 GB of queries over 100 iterations.
  2. Compaction trigger thrash + drifting estimator. The size estimator under-counted (9.2% of tool messages exceeded its clamp), and the retained tail alone (16 messages × 8k ≈ 128k) could re-cross the old 120k threshold, so a fresh compaction could immediately re-fire.

Changes (single commit, 5 files, +926/−117)

A. Bounded loads (repository.rs)

  • New list_messages_after: keyset pagination anchored on (created_at, id) — loads only messages after a boundary row instead of the whole session.
  • (created_at, id) ORDER BY tiebreak in list_messages (fixes pre-existing ordering disagreement with list_messages_before).
  • latest_message_by_role: single-row reads for per-role lookups.
  • Engine loop, queued-message branch, local_agent (×3), and compact_session_history all converted to tail-only loads. Provider-visible history content/order is unchanged; divergence is confined to the intended tiebreak.

Insertion-faithful ordering (review hardening)

  • next_created_at enforces a monotonic per-session created_at on insert (max(requested_now, session_max + 1ms)) inside the insert transaction, so the (created_at, id) total order equals insertion order for all new rows. Without this, a tool result sharing a millisecond with its owning assistant message could sort before it and be silently orphaned by a compaction boundary. Historical rows are untouched.

B. Trigger rework (compaction.rs)

  • Trigger size computed from the actual rendered request payload (text + per-message overhead + system prompt + tools) — the old drifting estimator is removed.
  • Hysteresis: projected post-compaction size + a messages-since-last-compaction count gate, so a fresh compaction cannot immediately re-fire.
  • Threshold recalibrated 120k → 360k chars to match real payload sizing.
  • Forced /compact path unchanged.

Tests

  • Repository: 25 passed (incl. deterministic same-millisecond bump tests — session max pinned 10s into the future so only the bump can satisfy the assert).
  • Compaction: 32 passed (incl. new DB-backed load_provider_history tests: boundary exclusion, standing summary leading, stale-summary filtering, messages_since_compaction, never-compacted full history, cross-session fallback).
  • Engine: 40, local_agent: 44, 0 failures. cargo fmt + cargo clippy clean.

Review

Two independent review rounds; round-2 verdict production_quality. Known follow-up (accepted, non-gating): the BEGIN → SELECT MAX → INSERT bump uses deferred-BEGIN WAL semantics, so a concurrent writer commit between read and write fails with SQLITE_BUSY_SNAPSHOT (same class already ships on main in update_pending_queued_message); BEGIN IMMEDIATE or a single INSERT … SELECT MAX+1 would remove it if it ever bites.

Full analysis: reviews/compaction-analysis/current-approach.md in the workspace.

- repository: list_messages_after keyset query ((created_at, id) anchor);
  list_messages ORDER BY gains id tiebreak (same-timestamp rows could
  interleave); latest_message_by_role for single-row reads
- engine/local_agent: per-iteration tail-only loads replace full-session
  rescans; compaction window loads only messages since last source_to
- compaction: trigger sized from real provider payload instead of the
  drifting estimate; hysteresis (projected post-compaction size +
  messages-since-last-compaction gate) so a fresh compaction cannot
  immediately re-fire; forced /compact unchanged
- tests: keyset/tiebreak/newest-of-role repository tests; trigger
  hysteresis unit tests
@juacker
juacker force-pushed the perf/compaction-bounded-loads-and-trigger branch from bf695e2 to 7b40e53 Compare September 21, 2026 18:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant