feat(dev): supervise every runtime in headless mode - #2138
Conversation
|
Claude Security Review: no high-confidence findings. (run) |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## refactor #2138 +/- ##
============================================
+ Coverage 97.14% 97.16% +0.02%
============================================
Files 519 519
Lines 35492 35506 +14
============================================
+ Hits 34478 34499 +21
+ Misses 1014 1007 -7 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
AgentCore Harness Review
Verdict: Looks good
The change cleanly extends headless mode to supervise every runtime by reusing DevSupervisor instead of restricting to a single agent. I traced the new loop in src/handlers/project/dev/index.ts (lines 225–240) against DevSupervisor.launch/pump/events, and the phase/event ordering holds up:
entry.phase = "starting"is set synchronously inlaunch()before any await, so the initialPromise.allSettled(runtimes.map(start))guarantees no agent looks "idle" when the loop first iterates.- Failure events are pushed after
entry.phase = "failed", sosnapshot()on each iteration is consistent with what the user just saw rendered. - The
every(!starting && !running)+some(failed)predicate correctly avoids tearing down peers when one agent dies but others are still running, and only escalates toSilentCLIErrorwhen the whole cohort is down and at least one failed. - Single-runtime
--mode headlesswithout--agentnow falls into this path and exits gracefully when the lone runtime stops — a strict improvement over the previous hard-error. - Cleanup: since the throw only fires once no agent is still
starting/running, there are no orphan children whenSilentCLIErrorpropagates and the finally block closes the collector (verified bystate.closed === 1in the new test).
Test coverage looks appropriate: stayingRunner is a good addition and models a real dev server better than captureRunner for these cases, the removed --mode headless requires --agent and --port with several runtimes is rejected assertions are correctly folded into the parameterized cases / new tests, and the new tests exercise attributed output, per-runtime OTEL env rewrite, partial-failure survival, and total-failure exit.
One minor UX nit worth being aware of (not a blocker): when both agents fail nearly simultaneously and both failure events land in the supervisor queue before the outer loop drains it, the break can fire after the first yield and the second agent's failure line is never rendered. Users still get non-zero exit and the first failure message, but if you wanted symmetry you could drain remaining failed-phase events from supervisor.snapshot() before throwing. Fine to defer.
Nothing here that needs to change before merging.
| const starts = Promise.allSettled( | ||
| runtimes.map((runtime) => supervisor.start(runtime.name)), | ||
| ); | ||
| for await (const { agentName, event } of supervisor.events()) { |
There was a problem hiding this comment.
I think normal Ctrl-C gets reported as a crash in this supervised headless path. The production process runner rejects with the child signal's UserCancellationError, so the supervisor emits Agent 'orders' crashed: Operation cancelled by user before the command exits 130. Could the supervisor treat an error from an already-aborted child signal as a normal stop? A test where the runner throws input.signal.reason on abort would cover the production behavior better than stayingRunner, which returns cleanly.
There was a problem hiding this comment.
Good catch, fixed. The pump now treats an error from an already-aborted child signal as a stop, and the runner fakes throw the abort reason like the real runner so the SIGINT tests cover the production path. Verified end to end: Ctrl-C exits 130 with no crash line.
|
Claude Security Review: no high-confidence findings. (run) |
Hweinstock
left a comment
There was a problem hiding this comment.
lgtm, agree with Aidan's comment that we don't want to show user cancellations as errors
|
Stale |
--mode headless without --agent now runs all of the project's runtimes at once with attributed output, matching the supervised behavior browser mode gets from the Inspector. One agent crashing leaves the others running, and the command exits non-zero only when nothing is left running and something failed. --agent keeps the direct single-runtime path where a crash fails the command.
|
Claude Security Review: no high-confidence findings. (run) |
There was a problem hiding this comment.
AgentCore Harness Review
Verdict: Looks good
I traced the new headless multi‑agent loop in src/handlers/project/dev/index.ts (lines 205–237) against DevSupervisor.launch/pump/events and it holds together: entry.phase is set to "starting" synchronously in launch before any await, so the first pass of the for await never sees all-idle; failure status events are pushed after the phase flip, so snapshot() after each yielded event is consistent with what was just rendered; and the every(!starting && !running) + some(failed) predicate correctly avoids tearing peers down when only some agents die. The test additions (stayingRunner, per-runtime OTEL env assertions, partial-failure survival, total-failure exit, collector-close verification) are appropriate and exercise the interesting states without over-mocking.
The one real correctness issue — that a normal Ctrl‑C on a running cohort will land in pump's catch as a UserCancellationError, flip every phase to failed, render "Agent 'X' crashed: Operation cancelled by user", and throw SilentCLIError (exit 1) instead of falling through to controller.signal.throwIfAborted() (exit 130) — has already been flagged by @aidandaly24 and seconded by @Hweinstock, and hasn't been addressed on the current head (ba8cddf). Nothing new to add from me; that's the thing to fix before merging.
The process runner rejects with the abort reason on teardown, so Ctrl-C in supervised headless mode printed 'Agent crashed: Operation cancelled by user'. The pump now treats an error from an already-aborted child signal as a normal stop. The runner fakes throw the abort reason like the real runner, so the SIGINT tests exercise the production path.
ba8cddf to
2089652
Compare
|
Claude Security Review: no high-confidence findings. (run) |
What
--mode headlesswithout--agentnow supervises every runtime in the project, streaming attributed[name]output to the terminal, the same behavior browser mode gets from the Inspector. Follows the discussion on #2086: multi-agent terminal supervision (useful for A2A testing) was the interim behavior onrefactorbetween #2041 and #2086, and this restores it as a first-class mode instead of requiring--agent.--mode headless(no--agent): all runtimes start eagerly, output is[name]-attributed, one agent crashing leaves the others running, and the command exits non-zero only when nothing is left running and something failed.--mode headless --agent <name>: unchanged, the direct single-runtime path where a crash fails the command with a real exit code (scripts and CI rely on this).Verification
project dev --mode headlessstarted both agents eagerly on distinct ports with[bob]/[alice]attributed output, and SIGTERM tore both down.refactor(DataTable.tsx, usePagedList.tsx).