AutoCompact.prepare_session runs on the turn hot path
(AgentLoop._compact_session) and read the persisted _last_summary metadata
with an unguarded meta['text'] and datetime.fromisoformat(meta['last_active']).
A _last_summary dict that was hand-edited or written by another version
(missing text/last_active, or a non-ISO last_active) raised KeyError/ValueError
out of the turn.
Sibling readers already tolerate the same data: estimate_session_prompt_tokens
uses .get('text') and _archive parses inside try/except. Mirror that tolerance:
skip when text is unusable, and fall back to the session's own updated_at (the
value the writer persists) when last_active is missing or unparseable, so the
archived summary is preserved instead of crashing the turn.
When an LLM response arrives with finish_reason='length' and has_tool_calls
but blank text content (e.g. the model spent its whole output budget on a
tool call whose closing tag was truncated), the runner dropped the tool
calls and then misrouted the blank response into the empty-response retry
branch. Retrying the same prompt cannot recover from output-budget
exhaustion, so every retry hit the same length ceiling and the turn ended
in the generic apology.
The length-recovery branch was gated on 'finish_reason == length and not
is_blank_text(clean)', so a blank-but-truncated turn could never reach it.
- The empty-response retry branch now excludes finish_reason == 'length'
(in addition to 'error').
- The length-recovery branch no longer requires non-blank content, so a
blank-but-truncated turn enters recovery and appends
build_length_recovery_message (which handles a blank tail safely).
Adds a regression test asserting the length-recovery path is taken; it
fails on the unfixed code and passes with the fix.
Fixes#5133
Before this change, idle compaction is triggered every 1 second
if the incoming message stream is idle.
When triggered, it enumerates all session files, loads and parses them,
and then checks their expiration.
This becomes too CPU-intensive, especially on low-power devices like Raspberry Pi.
It's unlikely that you actually need to compact every second over the long time.
This change adds a configurable throttling for idle-compaction.
Default behavior is unchanged.
Reset both the recovered segments and retry budget whenever injected input starts a new logical answer. Cover fatal tool-error boundaries and rename the prompt test module so pytest can collect the full suite.
* test(agent): use python3 in ExecTool workspace scope tests (fixes#5062)
* test(agent): use python on Windows and python3 on POSIX in ExecTool workspace scope tests (fixes#5062)
* test(agent): share Python command fixture
---------
Co-authored-by: chengyongru <chengyongru.ai@gmail.com>