Repository navigation
meetings: a decision is never written to disk, so a restart forgets it and a second decide doubles the card and the file #1774
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: 2d12e71e775c46b9a4ffd186b3c1ec2e
Branch: dev/3-meeting-persist
Base: 84385f2
At: 2026-10-30T04:34:48Z
What happens
A meeting is never written to disk. core/src/host/meetings.js mutates store() in memory and returns; the module contains no save() call at all, so every state transition is lost when the process restarts.
Reproduced on main 84385f2faa63a033cc9f7e5f69f5225bf7ca1408 with the real host and the real meeting tool, no failure injected:
open+ oneposition→ memory has 1 meeting, the on-diskstate.jsonhas 0; after a restart the meeting is gone entirely.open+ two positions +decide→ the decision file is written and one card is created, butstate.jsonstill saysstatus: "deliberating". After a restartmeeting readstarts withmeeting-… [deliberating], and a seconddecidesucceeds: the card count goes 1 → 2 anddecisions/gains a second…-2.md.
Root cause
store()[id] = meeting (:165, open), store()[meeting.id] = { ...meeting, pendingTaskIds } (:308) and store()[meeting.id] = closed (:345, the last statement of decide) are all memory writes. The only save() on the path belongs to task-create inside produceTasks (core/src/host/tasks.js:287), and that snapshot is taken before :345, so the archive on disk never learns that the meeting was decided.
The pendingTaskIds record added by #1765 is a memory write as well, so its retry protection holds across calls but not across a restart: after a restart the record is gone and the retry creates the cards again.
Expected
Every meeting write is durable before the call returns: open, position, block, resume, and both halves of decide (the pendingTaskIds record after the cards are created and before the decision file is written; the closed record after the file is archived). After a restart the meeting reads back [decided], a second decide is refused with meeting/closed, and no second card or second decision file is produced.
Scope
core/src/host/meetings.js—await rt.save()at those points.rt.save()is the existing convention in this host (schedules.js,tasks.js,project-assistant.js); no new API.core/test/meetings-persist.test.js— the red-first test.patches/ds-bot/0002-flybot-api-host.patchandpatches/ds-bot/files/src/host/meetings.js— regenerated, not hand-edited.changelog.d/…fixed.md.
Not in scope: an idempotency key for decide (a separate enhancement), any UI work, and the project-read wording note.
Acceptance
- Red-first on the base: decide →
ctx.dispose()→ a new runtime reads the meeting back asdecided, and a seconddecideis refused withmeeting/closed. - The same test proves the failure shape: after the restart, the card count and the number of files in
decisions/are both unchanged by the refused retry. open+positionsurvive a restart.- No regression: the existing meetings suites plus fix(host): a meeting archives its decision before it creates cards #1765's red-first stay green.
Duplicate check
Searched open and closed issues before opening: meeting persist → #1736 (closed, meeting records), #1763 (closed, orphan card on archive failure — a different face); meeting restart state → #1550 (proposal), #1720/#1732/#1730 (closed, task contract); meetings save disappear → 0 results. Open issues that mention meetings are #1767 (board display, already has a PR) and #1550 (proposal). Nothing covers persistence.
Activity
fix(host): write a meeting's state before the meeting tool answers (T…