Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,20 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

## [Unreleased]
### Fixed

- `zeph-core`: `cached_prompt_tokens` was never recomputed after `rebuild_system_prompt`
rewrote `messages[0]` with the real, per-turn-filtered system prompt, leaving the counter
pinned at whatever value it held before the rebuild — on turn 0 this was the
construction-time seed derived from the entire unfiltered skill registry, which could
reach ~100K tokens regardless of what was actually sent to the LLM (#6332). This caused
the tool-loop context-budget early-stop check (`tier_loop.rs`) to potentially trigger a
premature "context window nearly full" stop on a brand-new session, and the TUI/channel
context estimate to show a misleading token count before the first LLM call.
`assemble_final_system_prompt` now calls `self.recompute_prompt_tokens()` immediately
after writing the rebuilt prompt into `messages[0]`, so the counter always reflects the
actual outgoing prompt on every turn.

### Testing

- **zeph-core**: added end-to-end coverage for `TurnSummary.tool_calls` / `TurnSummary.llm_requests`
Expand Down
1 change: 1 addition & 0 deletions crates/zeph-core/src/agent/context/assembly.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1065,6 +1065,7 @@ impl<C: Channel> Agent<C> {
if let Some(msg) = self.msg.messages.first_mut() {
msg.content = system_prompt;
}
self.recompute_prompt_tokens();
}

/// Rewrites `query` via a fast background-provider LLM call to improve skill-matching
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1063,6 +1063,45 @@ async fn rebuild_system_prompt_omits_memory_save_hint_when_tool_not_used() {
);
}

// --- #6332: cached_prompt_tokens must be recomputed after rebuild_system_prompt ---

/// #6332 regression: `rebuild_system_prompt` writes a freshly assembled system prompt into
/// `messages[0]` but must also recompute `cached_prompt_tokens` from the real message set —
/// otherwise the counter stays pinned at whatever stale value it held before the rebuild
/// (e.g. the construction-time seed), causing the turn-0 context-budget check and the
/// TUI/channel context estimate to see a value disconnected from the actual outgoing prompt.
#[tokio::test]
async fn rebuild_system_prompt_recomputes_cached_prompt_tokens() {
use zeph_skills::registry::SkillRegistry;
let provider = mock_provider(vec![]);
let channel = MockChannel::new(vec![]);
let registry = SkillRegistry::default();
let executor = MockToolExecutor::no_tools();

let mut agent = Agent::new(provider, channel, registry, None, 5, executor);
// Simulate a stale/mis-seeded counter (e.g. Defect 1's inflated construction-time
// estimate) that must NOT survive a rebuild.
agent.runtime.providers.cached_prompt_tokens = 100_106;

agent.rebuild_system_prompt("test query").await;

let actual: u64 = agent
.msg
.messages
.iter()
.map(|m| agent.runtime.metrics.token_counter.count_message_tokens(m) as u64)
.sum();

assert_eq!(
agent.runtime.providers.cached_prompt_tokens, actual,
"cached_prompt_tokens must reflect the just-rebuilt system prompt, not a stale seed"
);
assert_ne!(
agent.runtime.providers.cached_prompt_tokens, 100_106,
"the stale seed must have been overwritten by the recompute"
);
}

/// Verify that `maybe_proactive_compress` routes to `run_focus_auto_consolidation_pass`
/// when the strategy is `CompressionStrategy::Focus` and `should_proactively_compress`
/// returns `Some`.
Expand Down
Loading