Skip to content

orchestrate: kill-tree skips running agents under handed-off subplanner branches, diverging from crawl and from its own help text #641

Description

@rahul05ranjan

What happens

The crawl and kill-tree commands walk the same orchestrate tree with two parallel implementations in scripts/cli/util.ts, and they disagree on which subplanner branches to descend into:

  • crawlBranch (used by crawl) recurses into a subplanner branch when its status is running or handed-off:

    orchestrate/skills/orchestrate/scripts/cli/util.ts:100-110

    if (
      t.type === "subplanner" &&
      (t.status === "running" || t.status === "handed-off")
    ) {
      crawlBranch(...);
    }
  • collectRunningAgentsInTree (used by kill-tree / filterVictimsToSubtree) only recurses when the status is running:

    orchestrate/skills/orchestrate/scripts/cli/util.ts:174

    if (t.type === "subplanner" && t.status === "running") {
      collectRunningAgentsInTree(...);
    }

The two claims that they agree:

  • cli/util.ts:123 — doc comment on collectRunningAgentsInTree: "Walk the tree the same way crawl does and collect every running task's (agentId, runId)"
  • cli/forensics.ts:84 — kill-tree help text: "Cancel every running cloud agent across an orchestrate tree. Walks state.json like crawl, collects running (agentId, runId) pairs, and cancels each via the SDK."

Walkthrough

Consider a tree where a subplanner's own cloud run has ended (its row in the parent's state.json is handed-off), but the state.json committed on the subplanner's branch (orch/<slug>/<name>) still contains rows with status: "running" and non-null agentId/runId — e.g. a verifier/respawn it kicked off before returning, or workers it spawned that were still in flight when its run stopped:

  1. bun cli.ts crawl <repo> <root-branch> <root-slug> descends into orch/<slug>/<name> (the handed-off case matches) and lists those running agents.
  2. bun cli.ts kill-tree <repo> <root-branch> <root-slug> -y calls collectRunningAgentsInTree, which does not descend into handed-off subplanner branches, so those (agentId, runId) pairs are never collected.
  3. If those are the only running agents in the tree, kill-tree prints:
    nothing to kill — no running agents found in the tree.
    and exits without cancelling anything — while crawl on the same tree displays running agents one command earlier.

--agent-id <id> scoping is affected the same way: filterVictimsToSubtree only walks parent links among already-collected victims, so a victim in a handed-off subplanner branch is unreachable even when its parentAgentId chain points at the given root.

Expected vs actual

  • Expected (per both doc comments and the kill-tree help text): kill-tree sees and cancels every running cloud agent that crawl shows.
  • Actual: running agents under a handed-off subplanner branch are invisible to kill-tree; orphaned cloud agents keep running (and keep consuming quota) after the operator used the command whose purpose is to stop them.

Evidence summary

  • Divergent recursion condition: cli/util.ts:105 vs cli/util.ts:174.
  • Parity claims contradicted by code: cli/util.ts:123, cli/forensics.ts:84.
  • collectRunningAgentsInTree / filterVictimsToSubtree have no tests (no references under scripts/__tests__/).

Suggested fix

Make collectRunningAgentsInTree recurse on t.status === "running" || t.status === "handed-off" to match crawlBranch, or (if skipping handed-off is intentional for some reason) update both doc comments and the kill-tree help text to say which agents it can miss. A regression test mirroring a handed-off subplanner branch containing a running worker would pin this down.

Activity

  1. deanrie commented on Oct 16, 2026

    @deanrie

    Hey @rahul05ranjan, confirmed on main, both snippets are as you quote them and the help text does promise parity. Fix is up as #647: crawlBranch and collectRunningAgentsInTree now share one descendsIntoSubplanner predicate (running or handed-off subplanner), so kill-tree follows the same branches crawl does and cannot drift again; a test builds a two-branch repo with a handed-off subplanner and a running worker under it and checks both walkers see the worker (it fails on main, 0 collected). Whether skipping handed-off branches was ever intentional is the orchestrate owners' call, and the PR says so; if it was, the helper is the single place to encode it and the help text should change with it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions