GitHub repository slug and git ref selector syntax now has one owner:
fabro-types::repository defines GitHubRepositorySlug with a try_new
constructor and the is_valid_github_ref_selector predicate.
fabro-automation keeps its public type path as a re-export of the same
type and delegates its existing parser and ref validation to the shared
grammar, preserving its exact error variants and messages. Server
checkout and materialization code imports the type from its canonical
owner. No wire, API, or behavior change.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review asked twice whether `permit.send` can leave an agent Running with
no turn on its way. It cannot, and the reasoning is not local to the call,
so state it there.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
LockError::Task is only constructed while waiting for the refresh
sidecar lock, so "auth store lock" pointed at the wrong file.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The subagent event forwarder left its loop on any `recv` error, including
`Lagged`. A lagged broadcast receiver stays usable, so one transient lag
silenced the child for the rest of its life while the task completed
normally and shutdown joined it without noticing. Session reuse widens
that window from a single turn to the whole parent session.
Also borrow each result's output when rendering a parent notification
instead of cloning it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
AgentPermissions duplicated fabro_types::PermissionLevel: same variants,
same kebab-case wire form, same crate. PermissionLevel is strictly richer
(Hash, strum, clap::ValueEnum) and is already the with_replacement target
for the OpenAPI PermissionLevel schema, whose values are identical to the
AgentPermissions schema this branch deletes.
Delete AgentPermissions and type the [cli.exec.agent] permissions setting
as PermissionLevel. This drops the adapter match in `fabro exec` and the
`as AgentPermissionLevel` alias that existed only to tell the two names
apart. The TOML wire form is unchanged.
Removing run.agent.permissions also changed the serialized run spec, but
two fabro-cli inline snapshots still carried "permissions": null. They
failed on this branch and passed on main. Accept the updated snapshots.
Also tighten the removed-setting test to assert the exact unknown-field
message, rename its module to run_agent now that it covers more than
fabro_tools, and drop three doc references to the removed setting.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Review pass over the reuse change. No intended behavior changes.
- share one definition of the initial generation from fabro-types instead
of three copies across fabro-types, fabro-agent, and the supervisor
- give each child one SubAgentHandle instead of threading the supervisor's
state, callback, and notification sender through five functions, and
collapse the repeated signal-then-drain pairs into publish()
- move `reusable` inside SubAgentStatus::Finished so a closed agent can no
longer be marked reusable
- clear the lifecycle draining flag with an RAII guard, so one panicking
callback cannot silence every later lifecycle event
- tear down a session that failed to initialize right away rather than
holding it and its sandbox until the parent closes the agent
- look agents up through SupervisorState::agent/agent_mut instead of five
copies of the same not-found error
- drop the unreachable cleanup_started branch and the test-only emit_event
whose only caller was its own test
- render subagent starts from one ProgressEvent and one display method,
deriving the spawn/turn distinction from the generation
- set projected subagent status through one helper instead of four
identical reducer arms
- drive the generation-pinned wait test through spawn/send_input rather
than hand-writing private supervisor state
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Second pass, from the remaining review findings.
- Wrap the stall watchdog in a `StallWatchdog` type. The call site kept
two parallel `Option`s derived from the same condition and threaded out
an `Option<(CancellationToken, JoinHandle<()>)>`. `monitor_for_stall`
also took two same-typed `CancellationToken` params pointing opposite
directions, where swapping them compiles and yields a run that silently
never stalls.
- Rename `WorkflowAgentQuestionRuntime::stage_id` and
`PendingAgentQuestionBatch::stage_id` to `node_id`. They hold
`node.id`, and the previous commit put them two lines from
`stage_scope.stage_id()`, which returns a real `StageId`.
- Widen the two real-time interview tests. `node_timeout_excludes_
human_input_wait` allowed 20ms of active work against a 50ms budget,
which is tight enough to flake under parallel nextest load. The blocked
wait still outruns the timeout, so both still fail if the pause
regresses.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The staleness check treated "different from the token that failed" as
"usable". A long-lived process could read an entry a sibling rotated an
hour earlier, whose access token had since expired, install it, and
return Ok. The caller retries once and does not refresh again, so that
surfaced a 401. Require the stored token to be unexpired; an expired one
now falls through and rotates with the refresh token just read.
Also:
- Give the non-Unix `acquire_refresh_lock` a no-op passthrough, matching
the other lock helpers off Unix. Returning an error there broke
re-installing a stored dev token, which needs no lock because it never
writes.
- Rename `Client::refresh_lock` to `local_refresh_lock`. Two different
locks were sharing one word four lines apart.
- Gate `LockError::Task` on Unix, where its only construction site is.
- Give the concurrency test a no-proxy transport connector. Building
clients without one goes through `connect_target_transport`, which
does not disable proxy discovery, against localhost.
- Assert the rotated refresh token reaches the store, which is the
invariant behind single-use rotation.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The cross-process refresh lock added a third copy of the open-file,
try-lock, then block-on-contention sequence. Collapse all three into
one `open_locked_file` helper parameterized by `LockMode`, which
removes `open_lock_file` and `lock_error`.
Lock calls are now qualified as `FileExt` calls throughout, since
`std::fs::File` has inherent locking methods with different return
types that take precedence over trait methods.
Also give `acquire_refresh_lock` one signature on all platforms by
defining `RefreshLockGuard` for non-Unix targets too, instead of
returning `Result<(), _>` there and `Result<RefreshLockGuard, _>` on
Unix.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
One conflict, in StructuredOutputError::repair_message. main (#709)
added a `previous_error` parameter and richer validation-error
rendering; this branch had replaced the inline expectation match with
OutputSchemaKind::expectation().
Resolved by keeping both: main's new signature and section assembly,
calling schema.expectation() for the expectation text. The method
already supersedes main's inline match and carries this branch's intent
of embedding the resolved JSON Schema instead of naming it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Follow-up cleanup on the human-input timeout work.
- Drop the `unresolved_interviews` counter from `InterviewBlockState`. It
duplicated `blocked_stages`, which is non-empty exactly when the run is
blocked.
- Publish block state before emitting `run.blocked` / `run.unblocked` in
both directions, so a listener reading `subscribe()` from an event
callback never sees state that disagrees with the event. The watchdog
still gets a full fresh deadline because it restarts on the unblock
transition.
- Stop panicking in `InterviewBlockState::resolve`. It runs from `Drop`,
where a panic during unwind aborts the process.
- Replace the emitter's `activity_revision` watch channel with a
monotonic timestamp. `record_activity` runs on every agent stream
delta, and the channel woke the watchdog task and re-armed its timer
per event. The watchdog now samples `last_activity()` when its deadline
fires and re-arms only if the run was active, so the hot path is one
clock read and one relaxed store.
- Remove the now-unused `last_event_at()` and `epoch_millis()`.
- Collapse the duplicated blocked/unblocked `select!` arms in
`monitor_for_stall` and `timeout_excluding_interview_wait` into one
loop each, using a branch precondition to park the timer while blocked.
- Handle a dropped block-state sender in
`timeout_excluding_interview_wait` by falling back to a plain deadline
instead of panicking, which also removes a potential busy loop.
- Only compute `stage_id` when the node actually has a timeout.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Replace the two envelope-level tests with focused `ApiUsage` tests that
match the file's existing `token_counts_*` convention.
The streaming and non-streaming tests were the same test paid for twice:
`ApiResponse::usage` and `StreamChunk::usage` are both `Option<ApiUsage>`,
so the envelope cannot change the result. Envelope-level usage decoding is
already covered by `stream_chunk_usage_parsing`.
Also pin the precedence rule this change introduces — nested detail wins
over the flat spelling, and an empty `completion_tokens_details` still
falls back — and document it on `token_counts`. Revert the unrelated
`cost` doc edit that dropped the OpenRouter reference.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Both contract helpers dispatched on OutputSchemaKind, so they belong on
the type. Moves expectation() and agent_prompt() into an impl block and
drops the free functions.
Splits the combined agent test: assertions no longer run inside the
backend's run(), where a failure surfaces as a panic from execute().
Adds coverage for the Routing branch of the contract, which was
previously untested.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Resolve the model catalog table conflict in docs/public/core-concepts/models.mdx
by keeping both changes: this branch's `kimi` -> `moonshot` provider rename for
the Kimi rows, and main's new DeepSeek V4 rows.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The preamble removal changed the `stage.completed` payload, so the
`attach --json` inline snapshot no longer matched. Drop the stale
`current.preamble` line.
Reword the `context_values` doc row. `stage_context_values` only strips
runtime-only keys; it does not normalize artifact pointers to blob refs
the way `artifact::durable_context_snapshot` does, so calling it a
durable snapshot overstated it. Point readers at `checkpoint.completed`
for the durable projection.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two artifacts can share a filename, a retry, and an absent stage, in
which case the winner was whichever the object store listed first. Break
the tie on the serialized stage ID, which is the third key the artifacts
page sorts on. Compare the `node@visit` string rather than StageId's own
ordering: the page compares the string, so "unknown@2" beats
"unknown@10" there and now here too.
The spec said captures from the `start` and `exit` nodes are excluded,
but the exclusion is by handler type, so a node named `start` that does
real work keeps its artifacts. Say that instead.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Raise the replacement test's deadline to 20s. The server takes up to 1s to
notice the replacement and then bounds its own shutdown at 5s, so the old
5s deadline sat below the worst case and could fail a healthy server on a
loaded runner. A passing run still exits in about a second.
Reword the SHUTDOWN_TIMEOUT comment.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Addresses Copilot review feedback on the repeated-failure check.
serde_json runs with preserve_order, and jsonschema builds the
additionalProperties `unexpected` list by walking the instance in
document order. So the same leftover keys emitted in a different order
produced a different Vec and compared as a different problem, which
suppressed the "unchanged from your previous repair" nudge.
Sorting at capture also makes the MAX_UNEXPECTED_PROPERTIES truncation
pick the same subset every time instead of an order-dependent one, and
stabilizes the rendered message.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>