Reuse completed subagent sessions - #694
Conversation
|
Reviewer note Suggested review order:
The main invariants to check are:
The areas most worth extra scrutiny are shutdown races, parent-notification suppression across generations, and the state/follow-up lock ordering. The focused tests near the end of |
Review focusPlease focus review on these four questions:
Intentional semantics to validate:
The most relevant regression tests are |
There was a problem hiding this comment.
🟡 Not ready to approve
send_input can silently fail to dispatch a new-generation start command (dropped mpsc receiver) after already flipping the agent to Running, risking stuck waits/notifications.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.
Pull request overview
This PR enhances Fabro’s subagent lifecycle so completed child Sessions can be reused for later remediation work, adding explicit per-turn generation tracking and a new agent.sub.turn.started event to keep projections/CLI progress coherent without creating duplicate subagent rows.
Changes:
- Add a
generationconcept to subagent lifecycle events and introduceagent.sub.turn.startedto represent subsequent turns in the same child session. - Update event conversion, run projections, and CLI progress rendering to be generation-aware while preserving legacy defaults (generation = 1).
- Refactor the subagent supervisor to support queuing input, starting new generations for completed agents, and explicitly closing completed agents.
File summaries
| File | Description |
|---|---|
| lib/foundation/fabro-types/src/run_event/mod.rs | Adds agent.sub.turn.started event body and tests for generation defaults on legacy events. |
| lib/foundation/fabro-types/src/run_event/agent.rs | Adds generation fields (defaulting to 1 where needed) and defines AgentSubTurnStartedProps. |
| lib/components/fabro-workflow/src/handler/llm/api.rs | Updates test event construction to include generation. |
| lib/components/fabro-workflow/src/event/names.rs | Maps new SubAgentTurnStarted to agent.sub.turn.started and updates tests. |
| lib/components/fabro-workflow/src/event/convert.rs | Converts new/updated agent events into typed run event bodies with generation metadata. |
| lib/components/fabro-store/src/run_state.rs | Updates run projection reducer to handle AgentSubTurnStarted by returning subagents to Running. |
| lib/components/fabro-agent/src/types.rs | Adds generation to subagent lifecycle events, introduces SubAgentTurnStarted, and adds legacy serde default test. |
| lib/components/fabro-agent/src/subagent.rs | Implements session reuse via per-generation results, turn commands, lifecycle event draining, and explicit close semantics. |
| lib/components/fabro-agent/src/profiles/claude5_tools.rs | Updates tool descriptions to reflect that completed agents can be messaged/stopped; strengthens schema tests. |
| lib/components/fabro-agent/src/cli.rs | Prints generation metadata for subagent lifecycle events and adds output for turn start. |
| lib/apps/fabro-cli/src/commands/run/run_progress/stage_display.rs | Adds verbose UI line for subagent turn starts (includes generation). |
| lib/apps/fabro-cli/src/commands/run/run_progress/mod.rs | Wires new progress event through to stage display and updates snapshots/tests. |
| lib/apps/fabro-cli/src/commands/run/run_progress/event.rs | Adds ProgressEvent::SubagentTurnStarted and maps it from stored run events. |
Review details
- Files reviewed: 13/13 changed files
- Comments generated: 1
- Review effort level: Lite
We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.
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>
Simplification passRan a three-way review (reuse, quality, efficiency) over the branch diff and applied the findings in Duplication removed
Correctness and robustness
Dead code
Tests
Left alone, flagged for review
Also undocumented: the new event is missing from |
There was a problem hiding this comment.
🟡 Not ready to approve
send_input() can leave a reused subagent stuck in Running if the reserved command permit fails to send, causing waits/notifications to hang.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.
Review details
- Files reviewed: 14/14 changed files
- Comments generated: 1
- Review effort level: Lite
We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.
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>
Second pass: efficiency reviewThe efficiency reviewer's findings landed after the first pass. One was a real bug, fixed in Fixed
Confirmed clean by that review (recorded so it isn't re-litigated): no Additional items left for you
Not worth changing: two Verification for both commits: 7,680 workspace tests pass, |
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>
There was a problem hiding this comment.
🟡 Human review recommended
It changes core subagent lifecycle/concurrency behavior across multiple crates (agent supervision, event logging, projections, and CLI rendering) and warrants focused human review of edge cases and ordering guarantees.
Review details
Suppressed comments (1)
lib/components/fabro-agent/src/subagent.rs:846
suppress_parent_notificationcurrently clearspending_generationsbut keepsparent_notificationintact, so a latersend_inputon a completed/reusable agent will push a new generation intopending_generationsand automatic delivery will resume. If suppression is meant to be permanent for the agent (as the name suggests), this should probably setagent.parent_notification = None; if it’s meant to be per-generation, the naming/docs should make that explicit to avoid surprising duplicate notifications after reuse.
}
}
/// Stop automatic delivery for an agent whose result the parent retrieved
/// explicitly.
pub(crate) fn suppress_parent_notification(&self, agent_id: &str) {
let cleared = {
let mut state = self.state.lock().expect("subagent state lock poisoned");
- Files reviewed: 14/14 changed files
- Comments generated: 0 new
- Review effort level: Lite
We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.
There was a problem hiding this comment.
🟡 Human review recommended
It introduces substantial concurrency and lifecycle-state refactoring around reusable subagent sessions (generation-aware results, waits, notifications, and shutdown), which warrants final human verification.
Review details
Suppressed comments (1)
lib/apps/fabro-cli/src/commands/run/run_progress/event.rs:204
- With subagent session reuse, multiple completion events for the same
agent_idcan occur (one per generation), butProgressEvent::SubagentCompleteddoesn't carrygeneration, so verbose progress output can become ambiguous when events interleave across agents. Consider includinggenerationonSubagentCompleted(and printing it similarly toSubagentStarted) so each completion can be attributed to the correct turn.
SubagentCompleted {
stage_node_id: String,
agent_id: String,
success: bool,
turns_used: u64,
- Files reviewed: 14/14 changed files
- Comments generated: 0 new
- Review effort level: Lite
We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.
Summary
Sessionafter a successful task so the same subagent can receive later remediation workagent.sub.turn.startedand generation metadata while preserving generation 1 defaults for older stored eventsWhy
Review findings currently require fresh remediation agents after the original implementer finishes. Each replacement must reload the work order, plan, style guides, code, and review context. This change keeps the original child session and history available for follow-up work during the lifetime of the parent supervisor.
Concurrency behavior
The completion commit and
send_inputuse one state ordering. Input accepted before the completion commit stays in the current generation. Input accepted after that commit starts the next generation. Stored per-generation results prevent a later turn from hiding the result a waiter originally targeted.Scope
Session reuse lasts for the current parent supervisor lifetime. This change does not add cross-process subagent persistence or resume support.
Validation
cargo check --tests --workspacecargo +nightly-2026-04-14 fmt --check --allcargo +nightly-2026-04-14 clippy --workspace --all-targets --all-features -- -D warningsfabro-agent,fabro-types,fabro-workflow, andfabro-storefabro-clitests passed