`failed` means an agent has exhausted its own recovery and parks until a human
tells it to go again. That is the right design; the gate is per-agent and only
the addressed agent's is lifted, while in practice the user can only reach the
root. So the word never arrives for anything below it and a child that dies
stays dead for the rest of the scan.
A message from the user is that word for all of them. It sits beside the
budget-pause release directly above it, which already treats a user message as a
scan-wide signal with the same `exclude` for the agent being addressed.
Nothing else revives an agent, so there is no runaway to bound and no attempt
budget to keep: the person deciding whether to try again is the limit. Revival
also never asks how an agent died, only that it did.
This replaces an automatic revival keyed on a turn landing somewhere in the
scan, which needed a budget precisely because no human was in the loop, and
before that a check that recognised a provider's billing rejection by its
response body. `execution.py` is untouched again as a result.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Recovery was keyed on recognising a provider billing rejection by its response
body. That is a guess about one provider's prose: reword the message, add a
provider, and detection silently fails back to the original bug. It also had to
be right about the cause, when the cause was never the point.
The stranding is general. A failed agent is gated behind a user message and the
user can only realistically reach the root, so any child that dies stays dead for
the rest of the scan - however it died. Credit exhaustion was only conspicuous
because it took eighteen agents down at once.
So revive on evidence the scan works rather than on a theory of what broke: a
turn that lands anywhere buys every casualty another attempt, bounded by a
budget the agent refunds by landing a turn of its own. That budget is what a
cause analysis was standing in for, and it holds for causes nobody enumerated.
It joins the bounded-recovery limits already governing retries, compaction and
tool nudges, all of which have run by the time an agent reaches this state.
Reviving off live status rather than a marker set at failure time also removes a
race: a marker could be cleared by a recovery that overtook the agent recording
its own status, stranding it for good.
A billing 400 that LiteLLM re-wraps as a 500 mid-stream now spends the transient
retry budget before failing. That is the honest reading - a 500 is retryable by
contract, and only the prose said otherwise - and it costs bounded wall-clock,
no tokens, and self-heals through revival.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
An exhausted provider balance fails every in-flight agent within seconds, and
a failed agent is gated behind a user message. The user can only realistically
message the root, so the rest of the fleet stayed dead for the life of the scan
even after credits were topped up, and the header kept reporting a failure that
had already resolved.
Treat a billing rejection as its own class of failure:
- Detect it from the response body across the whole exception chain. The status
code is not a signal: the same rejection arrives as a 400 pre-stream and as a
LiteLLM-rewrapped 500 mid-stream, and the 500 shape was being replayed through
the full transient-retry budget on a failure no retry can fix.
- Tag the runtimes it stops, so the first call that succeeds again lifts their
human-wake gate and hands each agent a note explaining the gap. Their sessions
and tasks are intact throughout - only the gate kept them parked.
- Report it as one actionable sentence and one log line per agent rather than a
LiteLLM traceback per agent.
Unlatch the TUI alongside it. `scan_state` had no path back from "failed" and
`controller.error` was never cleared, so a resolved error stayed on screen for
the rest of a working scan; an agent's `error_message` was likewise write-only,
pinning it to a healthy agent. An unchanged error is now announced once rather
than once per retry, since a revived agent reads as "running" the instant it is
woken and again the instant it dies.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Treat literal 'null'/'none' strings as absent for optional tool args
Models routinely pass the literal string "null" or "none" instead of
omitting an optional argument. Taken at face value it becomes a filter
that matches nothing, so tools like list_notes / list_reports /
list_requests silently return no results.
Coerce such values to None in the central argument-coercion layer, but
only for parameters the schema allows to be null (or that are absent from
a declared "required" list), so required strings keep the literal value.
The list/filter helpers normalize the same values too, so a direct call
can't regress.
* Limit nullish coercion to query tools and keep literal tags
A literal "null"/"none" is only a mistake where the argument is a filter, so
gate the coercion on read-only query tools; a tool that writes keeps the value,
which stops update_note(content="none") from being read as "leave unchanged".
Stop dropping nullish entries from a notes tag filter too: tags are free-form,
so a literal "none" tag stays filterable and mixed tag queries keep every
branch.
* add a generic MCP client and a config for connecting MCP servers
* Add MCP docs and CLI polish: docs page, startup connect summary, --mcp-config flag, compact tool output
* Add MCP connection notes and per-run selection; clean up on cancel and dedupe names
* Show errored MCP tool calls as failed in the TUI
* Sanitize namespaced tool names so model APIs accept them
* Show MCP tool calls distinctly in the terminal and the run viewer
* Say what MCP servers are worth connecting for
* Correct the notes docstring to match how notes reach the agent
* keep the mcp tests from reading your shell's STRIX_MCP_* vars
* add extra-files plumbing so orchestrators can drop single files into the sandbox workspace
* reject extra-file paths that collide with a local source tree
* add --workspace-file so CLI users can place files in the sandbox workspace
* reject repeated and control-character workspace paths
* revalidate persisted workspace files when resuming a run
* drop the workspace-file size limit
* let an agent wait on what it already said
An agent that answers in plain text is nudged to call a tool, and the only tool
that hands control back takes a required message. So it says the same thing
twice: once as text the user has already read, once as the argument it had to
supply to stop. Seen on a run whose whole instruction was "hi" - a greeting, then
the same greeting again through respond_to_user.
message is optional now. The nudge arms the tool with the text that was
delivered and says not to repeat it, so an agent that has said its piece can park
on it with an empty call. Anything it does want to add it passes normally.
Parking still cannot leave the user on silence: an empty call is refused unless
something was actually said, and the arming is single use - execution clears it
as soon as a turn ends any other way.
The interactive prompt now also says to answer and stop in one respond_to_user
call, which is what avoids the nudge in the first place.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* drop the worked example from the interactive prompt
"the user greeted you, asked something you can answer outright, or you need a
decision" was the run I had been reading, written into a rule that holds
whatever the reason. The rule is that replying and stopping is one call; listing
occasions only invites the model to check whether this is one of them.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* drop the arming flag; an empty message just waits
Passing the delivered text from execution into the tool, and refusing an empty
call without it, was machinery guarding against an agent parking having said
nothing. That leaves the user looking at "waiting for your reply" with a cursor
in front of them - they type. It does not need a mechanism.
What is left is the default on message, and the nudge saying the text already
landed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* only offer waiting on words that were written
The nudge told every agent its text had already been delivered, but it fires
whenever a turn leaves the agent running, and a turn can end with no tool call
and no text at all - _final_output_preview has carried <none> and <empty>
branches all along. An agent that said nothing was being invited to wait on an
answer the user never received, leaving them at a bare prompt.
It now reads the turn: waiting on what was said is offered only when something
was, and otherwise the agent is told plainly that the user has read nothing and
to send its message.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* leave the continuation nudge alone
Rewording it meant asserting from the outside whether the agent had spoken, and
the nudge fires whenever a turn leaves the agent running - text or no text. The
agent knows which it did without being told, so the guidance belongs in its
prompt, where the condition is its own to read.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* say it in the nudge, where the agent is reading
An agent stranded by the nudge reasons off the nudge. Told only to call
respond_to_user, it supplies a message, and since it has just answered in plain
text that message is the same answer again. The system prompt saying otherwise
sits thousands of tokens earlier and loses.
The clause goes on the line the agent acts on: call respond_to_user, with no
message if it has already said it. That reads true whatever the turn did,
including one that produced no text, because the agent is the one who knows
which — nothing here has to work it out from the outside.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* correct the nudge the internal-turn test asserts
The test expected "ended the autonomous Strix run", which strix.core.execution
does not inject; it says "ended the autonomous run". The classifier was right and
the test was not, so the suite failed on main while the behaviour it guards was
fine.
The sentence is written inline in another module and copied by hand into the
classifier and again into the test, which is how it drifted. A second test now
reads it back out of that module's source, joining the adjacent string literals
its line wrapping leaves behind, and fails if either nudge is no longer injected
verbatim. Reworded one and it reports which nudge went missing and what a resumed
scan would do about it.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* read the nudges out of what the module can inject, not out of its text
Searching the source accepted the sentence anywhere in the file, so a stale copy
left behind in a comment would have kept the guard passing after the message it
guards had changed - the drift it exists to catch.
Parsing the module instead limits it to strings the code can actually inject.
Comments never reach the tree, docstrings are dropped as description rather than
behaviour, and adjacent literals are joined during parsing, which the line
wrapping needed and the regex was only approximating.
Checked by rewording the message and leaving the old wording in a comment: the
guard fails, where searching the text passed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* make the working-directory prompt answer the mouse
Its Confirm and Cancel were drawn as buttons and did nothing when clicked: the
modal mouse handler had a case for every dialog except this one, so a click fell
through and the scan sat waiting on an answer the user believed they had given.
Only the keyboard could answer it.
The prompt is docked in a corner rather than centered, so it also needs its own
bounds; the centered ones every other dialog uses would have put the buttons in
the wrong place. Those bounds now come from the same placement cornerOverlay
draws with.
Two returns that hand back the model alongside a call that mutates it are now
sequenced explicitly. They work, but only because the compiler happens to
evaluate the call first, and one of them is what puts the prompt back in the
composer when the mount is declined.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* skip the mount instead of abandoning the scan
Declining the working-directory prompt threw the whole launch away and dropped
back to the start screen, which is a lot to lose for answering one question
about one directory. The two answers are now about the directory alone: mount it,
or run without it. The prompt is the whole of the input either way.
The buttons say which is which - Mount and Skip rather than Confirm and Cancel -
and the prompt says what skipping costs.
A run with neither target nor directory is a real run, so two things follow it.
It can be resumed: its instruction is what drives it, and that is in the run
record. And it tells the agent plainly that it has neither, because an agent
given no scope goes looking for the one it assumes it was meant to have.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* fix(agents): collapse repeated waits queued inside one model turn
* fix(agents): state that one wait is enough in every prompt variant
---------
Co-authored-by: Ahmed Allam <ahmed39652003@gmail.com>
* fix(llm): cap the tool calls one assistant response may queue
* fix(llm): cap the subscription backend's responses too
---------
Co-authored-by: Ahmed Allam <ahmed39652003@gmail.com>
An exception escaping a non-interactive cycle re-raised before the status
handling, so a dying child stayed 'running' and its parent waited out the
timeout on a completion report the child could no longer send. Set the
terminal status and wake the parent on the way out too.
A tool call for a name Strix does not register raised ModelBehaviorError
from the SDK turn resolver, which nothing retries: the root agent's raise
tore down the whole scan and a sub-agent died before its status was set.
Opt into the SDK's tool_not_found_behavior="return_error_to_model" so the
unknown call comes back as a tool result and the agent self-corrects.
The setting landed in openai-agents 0.19.0, which requires openai>=2.45,
so both pins move.