Skip to content

workflow: a late spawn failure leaks the Bots it already made #2785

Closed

Description

@HugoXK

Implementation: FlyBot (ours) — PR follows within 24h. Do not pick up without a pickup-ok comment on this issue.

Written-by: FlyBot Lead
Claimed-by: FlyBot Lead
Claim-Id: c7b0191a904142ccac1611e93a33ff4f
Branch: dev/5-late-spawn-release
Base: 3db12f0
At: 2026-11-10T06:20:44Z

Implementation: FlyBot (ours) — PR follows within 24h. Do not pick up without a pickup-ok comment.

Written-by: FlyBot Lead
Bot: FlyBot Product

What happens

boundedSpawn (plugins/workflow/src/service.js:423-443, added for #2748) bounds the wait for a host spawn. When the host answers after the deadline with success, it releases the Bots that answer created:

Promise.resolve(pending)
  .then(ids => releaseAll(Object.values(ids ?? {}), release, warn))
  .catch(error => warn(`flybot-workflow: a late start could not release its Bots: ${error?.message ?? error}`))

A late answer that fails does not release anything: the .then handler above only runs on success, and the .catch merely warns. spawnAll (plugins/workflow/src/ephemeral.js:46-62) already put the ids it had made onto that error (:61 error.spawned = made), so the rejection carries exactly the Bots that now leak:

} catch (error) {
  error.spawned = made
  throw error
}

The early-answer path does not leak: startLive's catch (service.js:1299) calls releaseAll(error.spawned ?? [], …) on the same error. Same error object, released when it arrives before the deadline, dropped when it arrives after it.

The leak is permanent, not deferred:

  • the start is refused before beginRun(), so no run ever holds those ids in its ephemeral map;
  • reap() only walks lane.active / lane.last (service.js:1403-1470), and orphanIds() only reads state.orphans (:2421); this path writes neither.

So Bots created at hidden: true, ephemeral: true live until the process exits — while changelog.d/2748-workflow-stuck-run.fixed.md and docs/subsystems/workflow.md both state that a late spawn's Bots are released.

Reproduction

At b89ea5ecef80a996bc912f4e90c538d0f0ff2cbb (base here), with a spawned host whose create never answers inside SPAWN_DEADLINE_MS and then rejects with error.spawned set:

  • late failure branch: released: [] plus flybot-workflow: a late start could not release its Bots: … — fails;
  • the same partial failure answered before the deadline: released: ["eph-plan"] — passes.

# tests 2 / # pass 1 / # fail 1, failing line not ok 1 - a late spawn failure still releases the Bots it made.

Expected

A start refused at the deadline releases every Bot the pending spawn had already created, whether the host's late answer is a success or a failure. The refusal itself (error.code === 'spawn-timeout') does not change, and the existing warn stays so a failed release is still visible.

Fix shape (verified, 3 lines)

In the deadline branch, release late.spawned when the late answer rejects, then warn:

.catch(late => releaseAll(Array.isArray(late?.spawned) ? late.spawned : [], release, warn)
  .then(() => warn(`flybot-workflow: a late start was refused: ${late?.message ?? late}`)))

The sentence changes from could not release its Bots to was refused, because after this fix the release has already happened when that line prints; releaseAll keeps warning once per Bot it really could not remove, so a failed release stays visible.

Verified at the same base: 2/2 green, plugins/workflow full suite 515/515.

Scope

  • plugins/workflow/src/service.js only (the deadline branch of boundedSpawn), plus a regression test.
  • No core/, no client, no patches/ds-bot mirror ⇒ no generate, so this does not take the single generate lane.
  • Risk tier T2: plugins/** is a T2 row of .github/agent/risk-rules.json, and this change adds no diffPatterns hit (no DSH_HOME, no child_process, no fenced-prose line), so nothing bumps it.

Dedup

Read live main at the base above, no-cache, and searched issues and PRs for spawn-timeout, SPAWN_DEADLINE, spawned, 2748: #2735 is the "refused start puts the card back in the queue" half, #2752 (open) is "a failed run does not drain", #2753/#2750 are Stop release retry limits. No open issue or PR covers a late failure leaking the Bots it made.

Activity

  1. added a commit that references this issue on Nov 10, 2026
    9d86a99
  2. added 2 commits that reference this issue on Nov 10, 2026
    1e47e67
    3d1165f
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