Skip to content

track_tasks commits the batch before save(): a refused store write leaves ghost cards in memory #2761

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: 0cbab9084d364946a62fcc0a24fb58d3
Branch: dev/1-track-tasks-rollback
Base: f2ef4df
At: 2026-11-10T00:42:08Z

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

Written-by: FlyBot Lead
Bot: FlyBot Product

rt.trackBotTasks (core/src/host/tasks.js:1123 at f2ef4dfa) commits the whole staged batch into state.tasks before it calls await save(), so a store write that is refused leaves cards in memory the store never took — and the next unrelated write persists them. #2597 fixed the other half (a batch refused by the list validation, before it reaches save()); this is the storage-refusal half.

What happens

At the end of a batch, :1299-1300 commit staged into board (State.tasks), :1301 calls announceClaim() (the room line), and :1302 is await save(). save() (core/src/host/store.js:999-1052) throws on every refusal — tempWritable (:1022), ELOCKLOST (:1028-1031), a failed stage/rename — and nothing restores the in-memory batch.

Probe (probe/tasks-refused-write.mjs, soft link state.json.<pid>.tmp → a file outside the store): track answers Not saved: …, but board already shows Ghost card while disk is []; after the link is moved away, one unrelated track_tasks writes both cards. The link target stays untouched (OUTSIDE\n), so the refusal happens before any byte lands.

Reachable on the ordinary path: tempWritable refusal, ELOCKLOST (multi-window is normal), ENOSPC/EACCES. track_tasks is the tool every team round uses.

Expected

  1. After a refused save, state.tasks is byte-identical to the call's start (the same byte-identical standard track_tasks creates the first card before refusing the second item of the batch #2597 set), and a remount reads no card from the batch.
  2. The claim line is not posted when the save is refused: postClaimLine is called zero times on refusal, exactly once on success.
  3. revision stays monotonic — the rollback does not rewind it.

Scope

core/src/host/tasks.js + core/test/track-tasks-batch-refusal.test.js. Five save points share the same helper:

  • main path :1299-1302 (announceClaim before :1302 await save());
  • claimQueued :1025-1035 (announceClaim at :1034);
  • claimHandoff :1067-1076 (announceClaim at :1075);
  • returnToProducer :1094-1095 (openRework mutates first);
  • releaseQueued :1115-1116 (requeue mutates first).

Implementation constraint: the batch is staged on staged = structuredClone(board) (:1163-1164), so the commit only moves references and key sets — a shallow { ...board } snapshot is enough; no deep clone. Restore = delete keys not in the snapshot, write back every old reference. announceClaim moves to after a successful save. Do not touch the client.

Tests that should exist (7, all red at f2ef4dfa)

  1. Main path refused: /Not saved/ rejects, JSON.stringify(rt.state.tasks) is unchanged, saves.length === 1.
  2. After the refusal, Object.keys(rt.state.tasks) is empty and a fresh trackBotTasks('d1', {}) answers [].
  3. Move the link away, write Unrelated card: state.json holds only that card.
  4. Claim-line contract: refused → postClaimLine zero calls; success → exactly one.
  5. claim: true refused: card still open, claimedBy unset, state.tasks unchanged, room line unchanged, postClaimLine zero calls.
  6. release refused: card still claimed and has no retryAt.
  7. rework refused: no child card, original card unchanged.

Dedupe

Open issues/PRs for this face: 0 (ghost card, rollback tasks, track_tasks refused, Not saved). #2597 and its PR #2602 are closed and covered only the validation-refusal half before save(); this is the store-refusal half on the same batch.

Not claimed: any UI change, and anything about track_tasks validation (that is #2597's, closed).

Activity

  1. added a commit that references this issue on Nov 10, 2026
    2ddec2d
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