fix(scan): respawn-skip finalizes cancelled agents as `stopped`
When ``_respawn_subagents`` skipped an agent because it was in ``bus.stopping`` (the user clicked stop before the crash), the bus state was left untouched — status stayed ``running`` forever, so ``view_agent_graph`` and the TUI tree showed phantom agents that would never make progress. Now the skip path collects those agent ids and finalizes each as ``stopped`` outside the lock, which transitions status correctly, clears the ``stopping`` entry (``finalize`` already discards it), moves the live stats to ``stats_completed``, and triggers the post-finalize snapshot. A subsequent ``view_agent_graph`` shows the truth: the agent is stopped. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+23
-14
@@ -493,21 +493,30 @@ async def _respawn_subagents(
|
||||
TUI, but no task respawns.
|
||||
"""
|
||||
async with bus._lock:
|
||||
candidates = [
|
||||
(
|
||||
aid,
|
||||
bus.names.get(aid, aid),
|
||||
bus.parent_of.get(aid),
|
||||
dict(bus.metadata.get(aid, {})),
|
||||
)
|
||||
for aid, status in bus.statuses.items()
|
||||
if status in {"running", "waiting", "llm_failed"}
|
||||
and bus.parent_of.get(aid) is not None
|
||||
and aid != root_id
|
||||
# Honour a previously-issued graceful stop — the user clicked
|
||||
# "stop" before the crash; don't respawn what they cancelled.
|
||||
and aid not in bus.stopping
|
||||
# Snapshot the iteration view first so we can mutate via finalize
|
||||
# below without "dict changed during iteration" trouble.
|
||||
agents_snapshot = [
|
||||
(aid, status, dict(bus.metadata.get(aid, {}))) for aid, status in bus.statuses.items()
|
||||
]
|
||||
candidates: list[tuple[str, str, str | None, dict[str, Any]]] = []
|
||||
to_finalize_stopped: list[str] = []
|
||||
for aid, status, md in agents_snapshot:
|
||||
if status not in {"running", "waiting", "llm_failed"}:
|
||||
continue
|
||||
if bus.parent_of.get(aid) is None or aid == root_id:
|
||||
continue
|
||||
if aid in bus.stopping:
|
||||
# User clicked "stop" before the crash; don't respawn,
|
||||
# and reconcile the bus so its status truthfully reflects
|
||||
# "stopped" instead of staying "running" forever.
|
||||
to_finalize_stopped.append(aid)
|
||||
continue
|
||||
candidates.append((aid, bus.names.get(aid, aid), bus.parent_of.get(aid), md))
|
||||
|
||||
# Finalize outside the lock — ``bus.finalize`` acquires it itself.
|
||||
for aid in to_finalize_stopped:
|
||||
logger.info("respawn-skip %s: previously-cancelled, finalizing as stopped", aid)
|
||||
await bus.finalize(aid, "stopped")
|
||||
|
||||
for child_id, name, parent_id, md in candidates:
|
||||
try:
|
||||
|
||||
Reference in New Issue
Block a user