Skip to content

A workflow window that takes over writes its stale copy over the dead window's records, and never reaps its run #2704

Closed

Description

@Birfy

Written-by: Chief

A second window on the same home starts read-only because the first one holds state.lock (plugins/workflow/index.js:127). When the first window dies, the second takes the lock (core/src/host/channel.js:318 → core/src/host/store.js:883-890) and readOnly() turns false — but nothing else changes: the service still holds the copy of workflows.json it loaded once at construction (plugins/workflow/src/service.js:224) and never re-reads it, and its reaper never runs again (index.js:136 → service.js:2113, which returns at once on a read-only host). Two consequences, both proven on main 71855d9d7b1ffabb3fca334452d753543558cfcb.

F1 — the takeover writes its stale copy over the dead window's newer records

Second window loads; first window records another task; first window is killed with -9; second window takes over and writes once.

/data/dsh/bot/workspace/audit46/shutdown.test.mjs, test F1 (real child processes, real state.lock, real kill -9):
F1 evidence stale: after kill -9, …; disk=["before"]; restart-reads="before" — the record the dead window wrote (after) is gone from disk and from the restart.
Reverse control in the same test: the same kill and takeover with one extra step — re-read the store before the first write (what core's store.mutate()/refresh() already does for its own state, #1032) keeps both: F1 reverse control: disk=["after","before"].

F2 — the dead window's run is never reaped, and the lane stays closed

F2 evidence: after kill -9, the dead run still {task:"task-of-dead-window", status:"running", active:1, queue:1}; 150 ms later identical — the takeover's assign() queues behind a run nobody is running, and it still counts against maxConcurrent. Reverse control: calling reap() once on the new writer recovers (task-after-reap starts).

So a killed window leaves the workflow lane permanently blocked for the surviving window: every later assignment waits, and nothing in this process arms the dead run's step deadline.

Why it matters

Two windows on one home is the normal way a user opens the app twice, and killing a window is what a crash looks like. The surviving window then (a) silently deletes whatever the dead one wrote after it loaded, and (b) reports a workflow as busy forever. Both are silent: no warning, no room line, and the store stays valid JSON throughout.

Expected

Becoming the writer is a state change, not a flag flip: the first write after readOnly() turns false re-reads workflows.json (or refuses until it has — the way core store.mutate() calls refresh()), and the takeover runs the load-time reaper once, so the dead window's running runs are failed-or-recovered instead of holding the lane. A test drives both with real child processes and a real kill -9.

Change scope

plugins/workflow/src/service.js (the write path and reap()), plugins/workflow/index.js (startup reap / takeover hook). docs/subsystems/workflow.md if it states the read-only rule.

Activity

  1. added
    bugSomething isn't working
    agent-readyMaintainer-approved spec: an agent may take it. Only maintainers set this; never in a template
    area:automationScheduled tasks and automation
    agent-workingAn agent has claimed this issue (the claim comment names the branch)
    on Nov 8, 2026
  2. self-assigned this
    on Nov 8, 2026
  3. Birfy commented on Nov 8, 2026

    @Birfy
    CollaboratorAuthor

    Written-by: Dev

    Claimed #2704 — branch dev/2704-workflow-takeover (tip 04ec1403b8024cb689f272f98ba9122887c69f80). The takeover now re-reads the store and reaps once before its first write.

  4. removed
    agent-readyMaintainer-approved spec: an agent may take it. Only maintainers set this; never in a template
    agent-workingAn agent has claimed this issue (the claim comment names the branch)
    on Nov 8, 2026
  5. added 2 commits that reference this issue on Nov 9, 2026
    651976f
    4c8ec30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:automationScheduled tasks and automationbugSomething isn't working

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions