Skip to content

feat(core): context compression — Focus Agent, SWE-Pruner/COMI, SideQuest (#1850, #1851, #1885)#1900

Merged
bug-ops merged 4 commits intomainfrom
context-compression
Mar 16, 2026
Merged

feat(core): context compression — Focus Agent, SWE-Pruner/COMI, SideQuest (#1850, #1851, #1885)#1900
bug-ops merged 4 commits intomainfrom
context-compression

Conversation

@bug-ops
Copy link
Owner

@bug-ops bug-ops commented Mar 15, 2026

Summary

Critic gap fixes

ID Fix
S1 compact_context() and prune_tool_outputs() skip focus_pinned messages
S2 TF-IDF weighted Jaccard with Rust/shell stop-word list in compaction_strategy.rs
S3 SideQuest disabled when pruning_strategy != Reactive
S4 complete_focus uses UUID marker; returns error to LLM if marker not found
S5 Task goal cache invalidated by last-user-message hash, not by compaction events

New files

  • crates/zeph-core/src/agent/focus.rs — Focus Agent state, tool definitions, Knowledge block management
  • crates/zeph-core/src/agent/sidequest.rs — SideQuest cursor tracking and eviction logic
  • crates/zeph-core/src/agent/compaction_strategy.rs — TF-IDF scoring and MIG pruning (feature-gated)

Configuration

[agent.focus]
enabled = false
compression_interval = 5
min_messages_per_focus = 3

[memory.sidequest]
enabled = false
interval_turns = 4
max_eviction_ratio = 0.5
max_cursors = 30
min_cursor_tokens = 50

[memory.compression]
pruning_strategy = "reactive"  # reactive | task_aware | mig | task_aware_mig

Test plan

  • cargo +nightly fmt --check passes
  • cargo clippy --workspace --features full -- -D warnings passes (0 warnings)
  • cargo nextest run --workspace --features full --lib --bins — 6027/6027 passed (+35 new unit tests)
  • Synced with origin/main (fast-forward, no conflicts)

…uest (#1850, #1851, #1885)

Adds three context compression subsystems behind the `context-compression` feature flag.

Focus Agent (#1850):
- Native tools `start_focus(scope)` and `complete_focus(summary)` bracket a task window
- `complete_focus` truncates history to the UUID-based checkpoint, synthesises a pinned
  Knowledge block from the summary that survives all compaction (S1, S4 fixes)
- `/focus status` slash command; `FocusConfig` in `[agent.focus]`

SWE-Pruner / COMI (#1851):
- `PruningStrategy` enum: Reactive | TaskAware | Mig | TaskAwareMig
- TF-IDF–weighted Jaccard similarity with Rust/shell stop-word list (S2)
- MIG = relevance − redundancy; blocks with negative MIG evicted first
- Task goal cached by last-user-message hash; re-computed only on hash change (S5)
- Configured via `[memory.compression] pruning_strategy`

SideQuest (#1885):
- `SidequestState` tracks up to `max_cursors` largest tool outputs
- Every `interval_turns` user turns a 5-second LLM call selects stale outputs
- Eviction capped by `max_eviction_ratio`; guards: focus-active, compaction-cooldown,
  pinned-message protection, JSON-parse fallback, cursor size/token filters (S3)
- `/sidequest status` slash command; `SidequestConfig` in `[memory.sidequest]`

Test count: 6027 (+35 new unit tests across sidequest, compaction_strategy, focus modules).
@github-actions github-actions bot added documentation Improvements or additions to documentation llm zeph-llm crate (Ollama, Claude) memory zeph-memory crate (SQLite) rust Rust code changes core zeph-core crate dependencies Dependency updates config Configuration file changes enhancement New feature or request size/XL Extra large PR (500+ lines) labels Mar 15, 2026
@bug-ops bug-ops enabled auto-merge (squash) March 16, 2026 00:21
bug-ops added 2 commits March 16, 2026 01:21
#1900)

Correctness:
- C1: set compacted_this_turn=true in complete_focus and maybe_sidequest_eviction
- C2: reset compacted_this_turn BEFORE sidequest tick so it sees a clean slate
- S4: extract prune_tool_outputs_oldest_first() to break infinite recursion in scored fallback
- S5: checkpoint message now has focus_pinned=true so compaction cannot destroy it

Security:
- SEC-CC-01: append_knowledge() enforces max_knowledge_tokens via FIFO eviction
- SEC-CC-02: build_eviction_prompt() prefixes untrusted content boundary
- SEC-CC-03: focus summary sanitized as WebScrape (ExternalUntrusted) not ToolResult

Performance:
- PERF-1: maybe_sidequest_eviction() is now non-async, two-phase: apply last turn's
  JoinHandle result via now_or_never(), spawn next turn's LLM call in background

Tests (16 new):
- T-CRIT-01/02/03: pinned message skipping, handle_focus_tool, scored pruning
- T-HIGH-01/02/03: guard conditions, rebuild_cursors, tf_weighted_similarity scoring
- T-MED-01/03: config validation for SidequestConfig/FocusConfig, feature-gate guard
- SEC-CC-01/02 tests: token cap eviction, untrusted boundary in prompt

Minor:
- S1: tracing::warn when sidequest + non-Reactive strategy both enabled
- S3: rename weighted_similarity -> tf_weighted_similarity (no IDF component)
- default.toml: align commented defaults with FocusConfig field defaults
@bug-ops
Copy link
Owner Author

bug-ops commented Mar 16, 2026

Code review complete (rust-code-reviewer agent). All validator findings verified in code:

  • C1/C2 (double-compaction guard): confirmed at native.rs:1576, mod.rs:4146, reset at mod.rs:3135
  • S4 (infinite recursion): prune_tool_outputs_oldest_first() extracted at summarization.rs:788
  • S5 (checkpoint focus_pinned): confirmed at native.rs:1508
  • PERF-1 (SideQuest blocking): two-phase now_or_never() + tokio::spawn at mod.rs:4134/4188
  • SEC-CC-01/02/03: all confirmed
  • SPDX headers, feature gates, tracing, doc comments, cargo deny — all pass
  • Tests: 6043/6043 passed, +16 new tests

Follow-up issue #1904 created for deferred integration points (CLI flags --focus/--sidequest, --init wizard steps, debug dump, TUI spinners).

Ready to merge.

- Add explicit `use std::collections::HashMap` in compaction_strategy tests
  so LSP resolves the type without requiring the context-compression feature
- Gate FocusState struct, impl, and KNOWLEDGE_BLOCK_PREFIX with
  cfg_attr(not(feature = "context-compression"), allow(dead_code)) so
  clippy passes without the feature flag
- Gate is_active with allow(clippy::unused_self) since &self is required
  for API consistency even when the non-feature branch returns a constant
- Gate ToolOutputCursor struct and SidequestState struct/impl with the
  same cfg_attr pattern for the same reason
- Add cfg_attr(not(feature = "context-compression"), allow(unused_mut))
  on tool_defs binding in native.rs — mut is only needed when the feature
  is enabled for focus_tool_definitions injection
@bug-ops bug-ops merged commit dacdab7 into main Mar 16, 2026
20 checks passed
@bug-ops bug-ops deleted the context-compression branch March 16, 2026 00:56
@bug-ops
Copy link
Owner Author

bug-ops commented Mar 16, 2026

Fix pushed (460065a): added #\![cfg_attr(not(feature = "context-compression"), allow(dead_code))] at module level in focus.rs and sidequest.rs. The #[cfg_attr] on struct/impl blocks doesn't propagate to individual fields and methods, causing dead_code errors in ide/server/chat bundles (which exclude context-compression). Module-level attr covers all items.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

config Configuration file changes core zeph-core crate dependencies Dependency updates documentation Improvements or additions to documentation enhancement New feature or request llm zeph-llm crate (Ollama, Claude) memory zeph-memory crate (SQLite) rust Rust code changes size/XL Extra large PR (500+ lines)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant