Repository navigation
fix(workflow): a run Bot's id is dropped before the release on saveGraph() and delete() #1621
Description
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: b3f2a61dc1cf4e86a2aa4e6f7a550262
Branch: dev/1621-workflow-orphan-release
Base: 63f07ef
At: 2026-10-29T02:06:04Z
Implementation: FlyBot (ours) — PR follows within 24h. Do not pick up without a pickup-ok comment on this issue.
Written-by: FlyBot Lead
What happens
#1616 (PR #1617, merged 3dc44ec9) fixed three sites where a run Bot's ephemeral id was deleted before release() ran, so a failing release left nothing behind and the Bot became a hidden orphan. Two sites in plugins/workflow/src/service.js still drop the id first (line numbers on live main 3dc44ec9787b5782d2d89ac4fea9df3e4b2d508e):
saveGraph()(:1398-1411) collectsreleasing, thendelete run.ephemeral/delete run.handedat:1399-1402, thenwrite()at:1403, and only afterwardsawait releaseAll(releasing, release, warn)at:1408-1411— with the return value ignored. Trigger: an edit of the graph that removes a node a run is currently on; the run ends and lands inlane.last.delete()(whole-workflow delete,:950-968) collects ids fromlane.activeonly, callsawait releaseAll(releasing, release, warn)at:967ignoring failure, thenfinish()dropsstate.lanes[...],state.runs[...]and the logs (:953,:954,:968). A failure branch has nowhere to keep the id. It also never retries the ids#1616'skeepUnreleased()parked onlane.last, because those are not inlane.active.
Reproduction
Inject a spawn that yields one id per node and a release that throws host call failed for a chosen id, then:
- start a two-node run, edit the graph through
saveGraph()removing the node the run is on → the persisted run record has noephemeralleft, the release warning is the only trace, and a reloadreap()retries nothing; delete({ workflowId })on a workflow whose Bot will not go →lanes/runs/logsare gone, the id appears nowhere in the store, reload retries nothing;- let a run end normally so
#1616keeps the failed id onlane.last, then delete the workflow →releasingis built fromlane.activeonly, so that id is never even attempted.
All three leave a hidden run Bot: it is not in the sidebar, Search or Settings → Bots, list_bots/bot.list skip it, the Session is not reclaimed, and the name stays reserved by findBot (core-host.md:337, plugin-runtime.md:68).
Expected behaviour
Deleting a workflow must not be blocked by one failed release — the user asked for a delete, that action must still complete. But the id must have a home instead of being dropped:
saveGraph(): release first, then forget the ids that were released; ids whose release failed stay on the archived run via the existingkeepUnreleased(run, failed)so the next load'sreap()retries them.delete():releasingislane.active ∪ lane.last(deduped); ids whose release failed are recorded in a persistedstate.orphansbeforefinish()removes the lane;reap()retriesorphanson load and drops each entry once it succeeds.orphanslives instateand is persisted, not in a memory variable; entries are deduped by id and capped (50); one failingdelete()call registers an id once.orphansis preserved bymigrateRuns(), so an older store that is upgraded keeps its pending ids.
Acceptance
- A test pins the full chain: delete a lane while
releasethrows → the delete still succeeds, the id lands inorphans→ after a reloadreap()retries it and succeeds →orphansis empty. reap()is idempotent: a successful retry removes the entry, no unbounded growth, no duplicate entries for the same id.saveGraph()keeps the failed id on the archived run (same mechanism as#1616).releaseRun(),reset()anddispose()keep their current behaviour;#1476's double-release guard (forgetReleased) is untouched.
Scope
plugins/workflow/src/service.js: the two sites above, plusorphanIds()/noteOrphans()helpers, thereap()retry andmigrateRuns().- One test file (
plugins/workflow/test/dispose.test.js) and a changelog entry;docs/subsystems/workflow.mdonly if the documented contract needs theorphanssentence. - Not in scope:
startLive()'s spawn-failure path (service.js:795,:801) — that run record never exists, it is a separate decision. No UI changes in this issue.
Activity
HugoXK commentedon Oct 29, 2026
Heartbeat
Written-by: FlyBot Lead
Still-working: b3f2a61dc1cf4e86a2aa4e6f7a550262
At: 2026-10-29T02:46:54Z
HugoXK commentedon Oct 29, 2026
Release
Written-by: FlyBot Lead
Released: b3f2a61dc1cf4e86a2aa4e6f7a550262
Reason: merged-as-fa5ff751
At: 2026-10-29T02:53:29Z