`NativeTool` documents itself as "an identity, not a name" whose canonical
form is fabro's own vocabulary, with harness names layered on as aliases:
`to_string = "read_file", serialize = "Read"`.
The four Claude 5 subagent tools inverted that. `ClaudeAgent` declared
`to_string = "Agent"`, making the Anthropic wire name the identity and
leaving `name(ToolVocabulary::Fabro)` returning `"Agent"` -- and pairing a
provider-specific variant name with a generic wire name. It also meant the
`Claude5` arm listed none of them: they fell through to
`canonical_name()` and were correct only by accident.
Rename to `BackgroundAgent` / `AgentOutput` / `StopAgent` / `MessageAgent`
with fabro canonical names, keep the harness names as `serialize` aliases
so `from_any_name` still resolves them, and name them explicitly in the
`Claude5` vocabulary arm. Also map `Grep`/`Glob` there: that arm describes
the vocabulary rather than the profile's registry, and if either were ever
registered it would otherwise reach the harness lowercased.
Records why these are separate identities from
`spawn_agent`/`wait`/`close_agent`/`send_input` rather than aliases of
them, since the capabilities genuinely differ.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Task tools scope their list by `root_session_id` -- `Session` documents
this as "a subagent session inherits its parent's `root_session_id` so
todo tools that scope by root (Anthropic tasks) share one list across all
subagents" -- so a root and its children address one logical list.
`build()` runs once per session, though, and `AnthropicProfile`
constructed its own `TodoRuntime` inside that call. Root and child
therefore resolved the same `list_id` through different runtimes: both ID
counters started at zero, so both emitted `todo.created` with id `1` for
the same list, and `TodoListProjection::upsert` matches on id -- the
child's task replaced the parent's in the persisted projection. `TaskGet`
and `TaskList` read the local runtime, so neither session could see the
other's tasks either.
The previous commit's shared runtime fixed this for Claude 5 only,
because `build()` passed dependencies positionally and adding a fourth
argument would have meant touching all six call sites. It grew a second
constructor for Claude 5 instead, leaving the other five on a signature
that could not carry the runtime.
Bundle them into `ProfileDeps` so every profile takes the same
`(model, &deps)`. The duplicate constructor is gone, Anthropic shares the
runtime by construction rather than by opting in, and a future dependency
reaches all six profiles or none.
The existing Claude 5 sharing test is generalized and now also runs for
Anthropic; it fails against a per-profile runtime.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`ParentNotificationHub` kept a second `Mutex` and `watch` channel holding
a copy of each child's terminal result -- data `SubAgent.status` already
owns as `SubAgentStatus::Finished`, and which is never evicted, since
nothing removes entries from `SupervisorState.agents`.
Two of the three bugs fixed in the previous commit were ordering bugs in
the coupling between those two structures: suppress-vs-commit in
`begin_shutdown`, and register-vs-publish in `spawn_inner`. Both were
fixed by ordering the steps correctly. Keeping the registration beside
the status it is delivered with makes that whole class unrepresentable
instead:
- Registration is now a field on the `SubAgent` literal `spawn_inner`
already builds, under the lock that publishes it. There is no window
between publishing an agent and registering its notification.
- Suppression on shutdown happens inside the critical section that
decides the shutdown, after the status transition commits, so a
rejected shutdown cannot discard a result the parent is owed.
- `next_parent_notification_batch` scans agents for a live registration
whose status is `Finished`, and ignores `Closing`/`Closed` outright --
so a shutdown racing delivery can no longer park the parent on a result
that will never arrive, even if suppression were missed.
`spawn_result_monitor` no longer takes the hub; it bumps a single
`watch` counter after committing the status it already commits. Batch
order was the queue's insertion order, so `SubAgent` carries a
`spawn_seq` to keep delivery oldest-first.
Tests move from exercising the hub directly to the supervisor API, and
cover spawn-order batching and the shutdown-races-delivery case that the
old shape could not express.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three correctness fixes in the Claude 5 background-agent path, plus
cleanups from a reuse/quality/efficiency review pass.
Fixes:
- Background-agent output was run through skill expansion. A child that
wrote a bare path ("cleaned up /tmp") failed the whole parent turn with
`Unknown skill: /tmp`, and a child whose output happened to name a real
skill had its report replaced by that skill's template. Synthesized
harness turns now skip expansion; only text the user typed can invoke a
skill.
- `begin_shutdown` suppressed the pending notification before deciding
whether a shutdown would happen. Stopping an agent that had just
finished rejected the stop *and* discarded the result the parent was
owed. Suppression now happens only once shutdown is committed.
- `spawn_inner` registered the notification after publishing the agent in
`state.agents`, so a concurrent `shutdown_all` in that window left a
pending entry the monitor never completes, and the parent's drain loop
would never see the queue as drained. Registration now precedes
publication.
- `TaskOutput.timeout` was declared `number` but parsed with `as_u64`, so
a schema-valid `30000.0` failed at runtime.
- Update the fabro-server alias test for the `sonnet` alias moving to
Claude Sonnet 5.
Cleanups:
- The supervisor renders the notification turn; `Session` no longer knows
the envelope format.
- Replace six near-identical prompt snapshots with a property test over
all eight conditional combinations, keeping the default and
all-conditionals snapshots for wording.
- Collapse `TodoRuntime`'s two mutexes into one.
- Read the prompt vocabulary from the registry instead of hardcoding it.
- Drop internal vocabulary from the `SendMessage` tool description.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Codex drives the GPT-5.6 models with a much narrower tool set than the
other OpenAI models: a shell, `apply_patch`, and `update_plan`. It has no
file-read, file-write, grep, glob, or fetch tool at all -- reading and
searching go through the shell, and every write goes through
`apply_patch`. Offering 5.6 fabro's extra tools advertises affordances its
instructions never mention, so this adds a profile that registers only
what Codex does.
The profile is selected per model via `agent_profile = "gpt56"` on the six
5.6 rows (three each on `openai` and `openrouter`), following the existing
Kimi-over-a-gateway pattern. Every other model on those providers keeps
its provider default, with no code branch and no version sniffing.
- `ToolVocabulary::Codex` renames `shell` to `shell_command`; a strum
alias keeps `from_any_name` resolving it to `NativeTool::Shell`, so
permissions, categories, and telemetry still key on the canonical name.
- `shell_command` gains `workdir`, passed to the `cwd` argument
`execute_shell_command` already accepted, with Codex's "always set
`workdir`, do not `cd`" guidance.
- `prompts/gpt56.md.j2` is adapted from Codex's 5.6 `base_instructions`,
which are byte-identical across Sol, Terra, and Luna. A header comment
records provenance and the departures fabro's harness forces.
This is an alignment-only pass: it matches Codex's tool contract while
keeping direct tool calls. Codex actually drives 5.6 in code mode, with a
single `exec` tool taking JavaScript and every other tool reached through
a `tools` object inside a V8 isolate. That is deliberately out of scope.
Luna's `multi_agent_version: v1` (vs v2 on Sol and Terra) is also out of
scope. It only changes the sub-agent tool set, which fabro registers from
the caller rather than the profile, and fabro's current set matches
neither version exactly.
Two server cancel-timing tests are adjusted. `gpt-5.6-sol` is the
`openai` provider's default model, so runs that name no model now build a
3-tool profile instead of an 8-tool one and reach their first stage
sooner. `full_http_lifecycle_cancel` asserted `status.kind == "blocked"`
at the instant of cancel, which the worker is free to change the moment it
is signaled; it now accepts either live state, matching the tolerance its
own comment already documents for `pending_control`.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The stage-summary preamble rendered per-stage token usage for every
completed LLM stage: "Model: kimi-k3, 92.6k tokens in / 41.1k out" at
compact fidelity and "Tokens: N in / N out" at summary:high. Agents read
that as their own remaining budget.
In run 01KYCM3EG4KMCVRDYNV93PZWBV an implementation stage stopped after 2
of 9 units, reasoning "We have around 100k tokens, but time constraints
are an issue" and recording the rest as halted "within the available
execution window". The 92.6k it saw was the preceding plan stage's
billing telemetry, the only token quantity anywhere in its context. It
had used 11% of a 1,050,000-token window and 0.8% of a 24h stage timeout,
and no harness limit was near.
These counts have no task value to the agent: they describe a different
model's usage on an earlier stage, they are stale by one stage, and
nothing in the preamble distinguishes them from a budget. Keep the model
id and files touched, which carry provenance the agent can act on.
Both tests that asserted the counts now assert their absence, so the
regression is caught rather than re-snapshotted.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Extend the Daytona Bash probe to cover the streaming toolbox-session transport in addition to the direct process exec. The two build different requests, so passing one is not evidence for the other: the `exec` regression fixed in #636 left every streaming command stalling until its timeout while the lifecycle probe reported a healthy sandbox. The session probe reuses the streaming path's own command construction and completion wait, so a transport that suppresses Daytona's exit-code bookkeeping fails at the lifecycle boundary with a remediation that names the wrapper-shell contract.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
server-secrets-strategy.md described only two credential mechanisms — bootstrap
ServerSecrets and vault-only optional integrations — and stated its most
restrictive rule in terms of "server runtime", which is ambiguous now that every
run is a server process plus a worker. It omitted the third mechanism actually
used by operator-configured integrations: settings-declared credentials in
InterpString fields, resolved at consumption time from {{ env.NAME }} or
{{ secrets.NAME }}, as LLM provider extra_headers already does.
Add a "Which process resolves what" table keyed on resolving process and timing,
a "Settings-declared credentials" section with the extra_headers precedent, and a
mechanism table at the head of "Adding A New Server Secret". Replace "server
runtime" with per-process statements, and describe where CredentialResolver's
process-env fallback is actually live.
Also correct six docs that told operators to export provider keys for "standalone
local runs". There is no CLI-local run execution: runs always execute in a worker
whose environment is cleared and repopulated from WORKER_ENV_ALLOWLIST, which
excludes provider API keys. Those instructions could not have worked.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
agent.message already carries a `reasoning` property with the model's own
summary and its verbatim trace, and the generated client already types it.
The web app just never read it.
Read it onto the assistant turn and render it in the details panel, after
the message and before the metrics. A trace can run thousands of characters,
so leading with one would push the message the user clicked on below the
fold. Text over 280 characters collapses to a preview with a "Show all"
toggle, matching ChatUserCard's disclosure pattern.
Providers disclose one field or the other or both, so a trace with no
summary is labeled just "Reasoning" rather than "Reasoning trace" — that is
the common Anthropic thinking case, and the bare label reads better when
there is nothing to contrast it with. Both fields render as preformatted
text: reasoning is raw model output, not authored Markdown, and parsing it
would eat the line breaks that are part of what it says.
Adding the field to the assistant turn broke six existing toEqual fixtures
that assert whole turn objects; they now expect `reasoning: null`.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The prompt bubble is `w-fit max-w-[85%]`, so its width is measured
intrinsically and only then clamped. `items-start` left the inner content
wrapper intrinsically sized too, so it resolved against the available space
from before the clamp — the full column width — and kept that measurement
after the bubble shrank. The text laid out at 100% of the column while the
background painted at 85%, spilling out the right side.
Give the wrapper `w-full` so it fills the bubble's resolved width instead of
measuring itself. Short prompts still hug their content: a percentage-width
child contributes its content size during intrinsic sizing, so the bubble
measures the same and only the final wrap width changes. The expand button
keeps hugging its label as a separate flex child.
Also break long words in the collapsed preview. That is a separate overflow
path: the preview is raw prompt text under `whitespace-pre-wrap`, where an
unbreakable path or URL would spill even at the correct width.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A text-free agent message marks the boundary between two batches of tool
calls, so it stays in the turn stream to keep those batches as separate
"N tool calls" chips. But it rendered an empty prose div, which still took
a slot in the gap-4 column and doubled the vertical space between the chips
on either side of it.
Render nothing for those turns instead. The final assistant turn still
renders when it carries a token/duration footer, even with no text, so the
completed-stage metrics are unchanged.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Chat is the more useful first view for agent stages, so open there instead
of Thread. Only agent stages offer "chat" in availableTabs; every other
renderer already falls back to "primary", so this leaves Logs/Q&A/Decision
and the rest unchanged.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The "Model request · waiting on <model>" readout sat directly above the
Chat/Thread/Debug toolbar and appeared and disappeared as requests opened
and closed, shifting the toolbar underneath it.
Drops the StageInferenceIndicator component and everything that existed
only to feed it: the inference/runSettled prop threading through
RunStages, and StageActivity's watchdogTimedOut field. The watchdog.timeout
event now falls through to the same ignore path it always would have, since
it was never in STAGE_ACTIVITY_EVENT_TYPES.
The run-events invalidations for watchdog.timeout and agent.llm.* stay:
they still refresh stage events for the Debug tab and run state for the
insights sidebar.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Run the streaming Bash wrapper as a child of Daytona's session shell so the provider can resume its bookkeeping and persist the command exit code. Add a regression test that exercises the sourced-command contract and preserves a nonzero exit status.