Structural cleanup of the durable pull request creation feature, from a
three-agent review (reuse, quality, efficiency) of the branch:
- Move the supervisor out of handler/ into server/pull_request_supervisor.rs,
collapse its double bookkeeping into one task-id map, and fold the five
copy-pasted failure arms into attempt_pull_request_creation.
- Tag pull_request.failed events with the creation id they resolve, so a
publish-stage failure can never fail an unrelated explicit creation. The
reducer gains PullRequestCreation::succeed/fail transition methods.
- Scan pending creations through a narrow projection-cache accessor instead
of materializing every run summary, raise the scan interval to 30s (notify
covers the live path), and cap retries for runs whose worker cannot even
record a failure.
- Answer "creation already pending" POSTs before taking the per-run create
lock, which a worker can hold for the whole creation.
- Replace the hand-rolled per-run lock map with fabro_store::KeyedMutex.
- Reuse cheap Arc'd projections (cached_run_projection) on the poll endpoint
and in the worker instead of deep-cloning run summaries and diffs.
- Merge ExistingPullRequest into fabro_github::CreatedPullRequest and
extract one reconcile_existing_pull_request helper for both call sites.
- Give the client poll loop a 15-minute deadline; document that Retry-After
and the poll interval are the same constant.
- Resolve a wedged pending creation (run already has a pull request) as a
durable failure instead of skipping it forever.
- Tests: shared wait_for_pull_request_creation helper, a pinned generation-
failure assertion, and a new pipeline test proving reconciliation adopts
an existing PR without an LLM call or create request.
Verified: cargo build --workspace, cargo nextest run --workspace (7,767
passed), nightly clippy -D warnings, fmt --check, insta (no pending), bun
typecheck in fabro-api-client.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Stage the pending task reminder as a Message and add
Message::to_llm_message so durable history and the round-staged turn
share one turn-to-wire conversion. Replace the one-off
BlockingAfterFirstOutputProvider with request capture and an
EventsThenPending variant on ScriptedStreamProvider, add a shared
make_session_with_provider_and_tools helper, and assert the reminder
tests against task_reminder::TASK_REMINDER_TEXT instead of a
substring.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The manifest builder's best-effort pre-run push converted every result
into a PreRunPushOutcome that was serialized into GitContext, expanded
into five OpenAPI union arms, and generated into API clients — but no
production path ever read it; every field read was a test.
Delete the concept while preserving the behavior:
- Drop the PreRunPushOutcome enum and GitContext.push_outcome from
fabro-types; GitContext keeps origin_url, branch, optional sha, and
dirty, which remain real execution inputs and provenance.
- Rename the manifest outcome builder to push_manifest_branch_best_effort,
a side-effect-only helper with the same decision rules: skip without an
origin, skip on configured-repository mismatch, skip when the branch is
already synced, otherwise push noninteractively and discard the result
without failing manifest creation or logging raw Git stderr.
- Prove the push through repository state instead of the deleted enum: a
branch ahead of a local bare origin is pushed during manifest build, a
mismatched configured repository is not, and a failing remote helper
still cannot fail manifest creation.
- Remove push_outcome from GitContext in OpenAPI, delete the five-arm
union schemas, and drop the fabro-api type replacement and re-export.
- Keep one regression proving historical run.created events with a nested
push_outcome still deserialize through ordinary unknown-field tolerance
and reserialize to the reduced shape. No migration or event rewrite.
Old JSON carrying the removed field stays readable. Newly generated
clients omit a field older servers required, so new-client-to-old-server
compatibility is intentionally not promised for this pre-1.0 contract.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Drop ManifestTarget.identifier (the raw token the user typed) and
ManifestGoal.path (the original goal-file path) from the OpenAPI
manifest schema, the Rust manifest builder, the regenerated Rust and
TypeScript client types, and every canonical test fixture. Neither
field had a production reader: the server selects the workflow by
target.path and consumes only the resolved goal type and text.
Target path, goal type/text, manifest versioning, and submitted-byte
persistence are unchanged. Old request bodies that still carry the
removed properties remain accepted through unknown-field tolerance,
pinned by a dedicated public-route regression test.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
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>
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>
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>