Five defects found by auditing the agent stack, all of which let the harness act
confidently on a wrong picture of its own state.
1. A turn that errored discarded the record of tools that had already run. The CLI
hands run_turn a CLONE of the session; the turn appends the user message, each
assistant reply, and a tool_result per executed tool to that clone alone. The error
path dropped it while the side effects of those tools remained on disk, and the
recovery path then replayed the same user input against a session showing no sign
they had ever run — inviting a second execution of tools that are not idempotent.
The clone is now adopted before teardown, and recovery resumes the turn instead of
replaying it (new ConversationRuntime::resume_turn).
2. Every sizing guard was computed against the model NAME's context window. With a
local server behind a cloud model alias that is 200k against a much smaller
backend, which does not make the preflights inaccurate — it makes them unreachable.
CLAW_CONTEXT_WINDOW now declares the real window; model_token_limit honours it and
returns a limit for unrecognised models instead of None, max_tokens is capped to a
quarter of it, and the auto-compaction threshold derives from it (70%) rather than
defaulting to a 200k-window constant.
3. Whether to recover by discarding conversation history was decided by substring
matching on rendered error prose, including strings that ordinary transport and
decode failures also produce — so a dropped connection cost the session its
transcript. Classification now happens in the API layer, where the error still has
its type and the request is still in hand: typed overflow errors, plus ambiguous
transport failures only when the request actually filled the window. The two
copy-pasted detection blocks are gone.
4. The top-level agent loop had no iteration ceiling (usize::MAX) while spawned
subagents were bounded at 32. It now gets the same bound, overridable with
CLAW_MAX_TURN_ITERATIONS.
5. Auto-continue nudges were injected as USER turns, so harness-authored text was
persisted, re-sent on every later request, and folded into compaction summaries as
something the user had said. They are recorded as system turns now; the wire
payload is unchanged.
Also replaces the repository's own self-host section, which described a translator
and a serving stack no longer in the request path. That file is folded into the
system prompt verbatim, so it was briefing the model with an architecture that does
not exist.
(cherry picked from commit c8834cb4d1a3372eb70241429f641162a420297e)
A reply carrying no tool_use ended the turn unconditionally. Small local
models routinely narrate the next tool call in prose — "Now running the
EEC-V probe:" — and then emit EOS, so the announced work never happened
and the user was dropped back at the prompt. The same failure wears a
second hat mid-task: stopping to ask for a go-ahead on work already
requested.
The loop now inspects the final line of a tool-less reply. If it
announces a pending action, or asks a question after tools have already
run this turn, a short nudge is pushed as a user message and the loop
continues instead of breaking. The budget is bounded (default 3 per
turn, CLAW_MAX_STALL_NUDGES, 0 disables) so a model that really is
finished still gets to stop.
Prevention pairs with recovery: the "Doing tasks" system-prompt section
now tells the model to act in the turn it plans in.
Tests cover the auto-continue, the bound, a plain completion that must
NOT be nudged, the disable switch, and last-line-only detection. The
behavioural pair was mutation-checked: reverting the continue to a break
fails both.
(cherry picked from commit 9035fe6f4bf4a8d4cb70d7a3baf1f3748c757498)
Two related gaps let a single agentic turn exceed a small context window,
with no recovery path once it happened.
1. read_file's character ceiling was a hardcoded 64 KB (~16k tokens), sized
for a 200k window. Two reads in one turn reach ~48k tokens, which a 32k
backend rejects outright. Make it configurable via CLAW_MAX_READ_CHARS,
defaulting to the existing 64 KB so behavior is unchanged unless set.
Reads remain paged, not refused: callers still get truncated/nextOffset.
2. Compaction could not help, because it works at message granularity and an
agentic turn is five messages (user / assistant+tool_use / tool_result /
assistant+tool_use / tool_result). With preserve_recent_messages: 4 the
only droppable message is the short user prompt, so the two oversized
tool results were preserved verbatim by design.
Trim preserved tool-result payloads over 16 KB to a head/tail excerpt with
an explicit "[... N characters elided ...]" marker, so the model knows its
view is partial and can re-read. Small payloads pass through untouched.
Also apply trimming on the early-return path: a session can exceed the
window while holding fewer messages than preserve_recent_messages, where
should_compact is false and message-granular compaction has no move to
make. compact_now no longer gates on removed_message_count alone, which
would have computed the smaller session and then discarded it.
Also fixes a pre-existing flaky test: repl_executes_python_code resolves the
python runtime through PATH, which the PowerShell tests blank while they run.
It now takes the same env_lock, so it no longer intermittently fails with
"python runtime not found" on a machine that has python.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013bVvRYn7CQKR6hT2j1L6Cs
(cherry picked from commit b0d4d41e8132f8dc75717b1f74554d649639de10)
The usage-based compaction gate is post-hoc: it can only see the context
after a call returns and reports its counters. Between that measurement and
the next request the session can grow enormously - a reasoning model emitting
a long thinking block, then a tool result carrying a large file.
Observed live: a call measured at 17,858 prompt tokens generated 10,842 more,
a read_file added ~19k on top, and the next request reached the provider at
47,755 tokens and was rejected outright. No tuning of the post-hoc threshold
prevents that, because nothing measures the session in between.
Add a pre-send check driven by estimate_session_tokens, a local heuristic over
the transcript, so it costs no API call and works before any usage has been
recorded at all.
Note the limit of this: compaction preserves the 4 most recent messages, and
in an agentic tool-use loop those messages ARE the tool results carrying the
bulk. This check bounds gradual growth; it cannot rescue a session whose last
four messages already exceed the window.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013bVvRYn7CQKR6hT2j1L6Cs
(cherry picked from commit 26ab52d70fb17e49b74d523aecfc1da7f69a6d60)
`maybe_auto_compact` gated on `usage_tracker.cumulative_usage().input_tokens`,
which is wrong in two independent ways:
1. It ignores cached input. With prompt caching a warm call reports
`input_tokens: 2` and puts the real context in `cache_read_input_tokens`,
so the counter barely moved and auto-compaction never fired at all,
however large the conversation grew. Backends without prompt caching
report the whole prompt as `input_tokens`, so the bug was invisible there.
2. A sum across calls is not a context measurement, and it never decreases.
Once it did cross the threshold, every subsequent iteration compacted
again, permanently, even after compaction had shrunk the context.
Gate on the latest call's total input instead — input + cache_creation +
cache_read — which is the live context window size. It rises with the real
context and falls back below the threshold once compaction has done its job,
so no counter reset is needed.
`CLAUDE_CODE_AUTO_COMPACT_INPUT_TOKENS` now means what it reads like:
compact when the live context reaches N tokens. The default of 100_000 is
unchanged and still lands at 50% of a 200k window.
Adds `TokenUsage::total_input_tokens()` (saturating) and four tests: the
cached-usage shape that never fired, non-latching across three turns, and
two unit tests for the new helper.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013bVvRYn7CQKR6hT2j1L6Cs
(cherry picked from commit aebba4e77eddf585fc5b21123967dbc4ebb6e6a1)
Extend auto-compaction error detection to handle additional error patterns
from llama.cpp backends: 'Context size has been exceeded',
'exceed_context_size_error', 'exceeds the available context size'. Also
recover from reqwest 'error decoding response body' errors — some
llama.cpp instances return a non-SSE plaintext HTTP 500 on context overflow,
causing the SSE deserializer to fail.
Add dynamic threshold adaptation: parse server-reported context window
size from error messages (e.g., '(81920 tokens)') and set the auto-
compaction trigger at 70% of that value. This replaces the need for a
hardcoded threshold, adapting automatically to any backend's limits.
This patch was developed with assistance from OpenCode and local Qwen 3.6
API server.
Five interrelated fixes from parallel Hephaestus sessions:
1. fix(repl): display assistant text after spinner (#2981, #2982, #2937)
- Added final_assistant_text() call after run_turn spinner completes
- REPL now shows response text like run_prompt_json does
2. fix(compact): handle Thinking content blocks (#2985)
- Added ContentBlock::Thinking variant throughout compact summarizer
- Prevents panic when /compact encounters thinking blocks
3. fix(prompt): provider-aware model identity (#2822)
- New ModelFamilyIdentity enum (Claude vs Generic)
- Non-Anthropic models no longer say 'I am Claude'
- model_family_identity_for() detects provider and sets identity
4. fix(openai): preserve DeepSeek reasoning_content (#2821)
- Stream parser now captures reasoning_content from OpenAI-compat
- Emits ThinkingDelta/SignatureDelta events for reasoning models
- Thinking blocks included in conversation history for re-send
5. feat(runtime): Thinking block support across codebase
- AssistantEvent::Thinking variant in conversation.rs
- ContentBlock::Thinking in session serialization
- Thinking-aware compact summarization
- Tests for thinking block ordering and content
Closes#2981, #2982, #2937, #2985, #2822, #2821
ROADMAP #38 no longer reflects current main. The runtime already runs a
post-compaction session-health probe, but the backlog lacked explicit
regression proof. This change adds focused tests for the two important
behaviors: a broken tool surface aborts a compacted session with a targeted
error, while a freshly compacted empty session does not false-positive as
dead. With that proof in place, the roadmap item can be marked done.
Constraint: User required fresh cargo fmt/clippy/test evidence before closing any backlog item
Rejected: Leave #38 open because the implementation already existed | backlog stays stale and invites duplicate work
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Reopen#38 only with a fresh same-turn repro that bypasses the current health-probe gate
Tested: cargo fmt --all --check; cargo clippy --workspace --all-targets -- -D warnings; cargo test --workspace
Not-tested: No live long-running dogfood session replay beyond existing automated coverage
Parse --reasoning-effort <low|medium|high> in parse_args, thread through
CliAction::Prompt and CliAction::Repl, LiveCli::set_reasoning_effort(),
AnthropicRuntimeClient.reasoning_effort field, and MessageRequest.reasoning_effort.
Changes:
- parse_args: new --reasoning-effort / --reasoning-effort=VAL flag arms
- AnthropicRuntimeClient: new reasoning_effort field + set_reasoning_effort() method
- LiveCli: new set_reasoning_effort() that reaches through BuiltRuntime -> ConversationRuntime -> api_client_mut()
- runtime::ConversationRuntime: new pub api_client_mut() accessor
- MessageRequest construction: reasoning_effort: self.reasoning_effort.clone()
- run_repl(): accepts and applies reasoning_effort parameter
- parse_direct_slash_cli_action(): propagates reasoning_effort
All 156 CLI tests pass, all api tests pass, cargo fmt clean.
This adds crate-level and type-level Rustdoc to the runtime crate's core exported types so downstream crates and contributors can understand the session, prompt, permission, OAuth, usage, and tool I/O primitives without spelunking every implementation file.
Constraint: The docs pass needed to stay focused on public runtime types without changing behavior
Rejected: Add blanket docs to every public item in one sweep | larger churn than needed for a targeted docs pass
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: When exporting new runtime primitives from lib.rs, add a short Rustdoc summary in the defining module at the same time
Tested: cargo build --workspace; cargo test --workspace
Not-tested: rustdoc HTML rendering beyond doc-test coverage
Claw already had the core slash-command and git primitives, but the UX
still made users work to discover them, understand current workspace
state, and trust what `/commit` was about to do. This change tightens
that flow in the same places Codex-style CLIs do: command discovery,
live status, typo recovery, and commit preflight/output.
The REPL banner and `/help` now surface a clearer starter path, unknown
slash commands suggest likely matches, `/status` includes actionable git
state, and `/commit` explains what it is staging and committing before
and after the model writes the Lore message. I also cleared the
workspace's existing clippy blockers so the verification lane can stay
fully green.
Constraint: Improve UX inside the existing Rust CLI surfaces without adding new dependencies
Rejected: Add more slash commands first | discoverability and feedback were the bigger friction points
Rejected: Split verification lint fixes into a second commit | user requested one solid commit
Confidence: high
Scope-risk: moderate
Directive: Keep slash discoverability, status reporting, and commit reporting aligned so `/help`, `/status`, and `/commit` tell the same workflow story
Tested: cargo fmt --all; cargo clippy --workspace --all-targets -- -D warnings; cargo test --workspace
Not-tested: Manual interactive REPL session against live Anthropic/xAI endpoints
The runtime now auto-compacts completed conversations once cumulative input usage
crosses a configurable threshold, preserving recent context while surfacing an
explicit user notice. The CLI also publishes the requested ant-only slash
commands through the shared commands crate and main dispatch, using meaningful
local implementations for commit/PR/issue/teleport/debug workflows.
Constraint: Reuse the existing Rust compaction pipeline instead of introducing a new summarization stack
Constraint: No new dependencies or broad command-framework rewrite
Rejected: Implement API-driven compaction inside ConversationRuntime now | too much new plumbing for this delivery
Rejected: Expose new commands as parse-only stubs | would not satisfy the requested command availability
Confidence: medium
Scope-risk: moderate
Reversibility: clean
Directive: If runtime later gains true API-backed compaction, preserve the TurnSummary auto-compaction metadata shape so CLI call sites stay stable
Tested: cargo test; cargo build --release; cargo fmt --all; git diff --check; LSP diagnostics directory check
Not-tested: Live Anthropic-backed specialist command flows; gh-authenticated PR/issue creation in a real repo
This threads typed hook settings through runtime config, adds a shell-based hook runner, and executes PreToolUse/PostToolUse around each tool call in the conversation loop. The CLI now rebuilds runtimes with settings-derived hook configuration so user-defined Claude hook commands actually run before and after tools.
Constraint: Hook behavior needed to match Claude-style settings.json hooks without broad plugin/MCP parity work in this change
Rejected: Delay hook loading to the tool executor layer | would miss denied tool calls and duplicate runtime policy plumbing
Confidence: medium
Scope-risk: moderate
Reversibility: clean
Directive: Keep hook execution in the runtime loop so permission decisions and tool results remain wrapped by the same conversation semantics
Tested: cargo test; cargo build --release
Not-tested: Real user hook scripts outside the test harness; broader plugin/skills parity
This threads typed hook settings through runtime config, adds a shell-based hook runner, and executes PreToolUse/PostToolUse around each tool call in the conversation loop. The CLI now rebuilds runtimes with settings-derived hook configuration so user-defined Claw hook commands actually run before and after tools.
Constraint: Hook behavior needed to match Claw-style settings.json hooks without broad plugin/MCP parity work in this change
Rejected: Delay hook loading to the tool executor layer | would miss denied tool calls and duplicate runtime policy plumbing
Confidence: medium
Scope-risk: moderate
Reversibility: clean
Directive: Keep hook execution in the runtime loop so permission decisions and tool results remain wrapped by the same conversation semantics
Tested: cargo test; cargo build --release
Not-tested: Real user hook scripts outside the test harness; broader plugin/skills parity
Extended thinking needed to travel end-to-end through the API,
runtime, and CLI so the client can request a thinking budget,
preserve streamed reasoning blocks, and present them in a
collapsed text-first form. The implementation keeps thinking
strictly opt-in, adds a session-local toggle, and reuses the
existing flag/slash-command/reporting surfaces instead of
introducing a new UI layer.
Constraint: Existing non-thinking text/tool flows had to remain backward compatible by default
Constraint: Terminal UX needed a lightweight collapsed representation rather than an interactive TUI widget
Rejected: Heuristic CLI-only parsing of reasoning text | brittle against structured stream payloads
Rejected: Expanded raw thinking output by default | too noisy for normal assistant responses
Confidence: medium
Scope-risk: moderate
Reversibility: clean
Directive: Keep thinking blocks structurally separate from answer text unless the upstream API contract changes
Tested: cargo fmt --all; cargo clippy --workspace --all-targets -- -D warnings; cargo test -q
Not-tested: Live upstream thinking payloads against the production API contract