Skip to content

Bench: a card no longer asked is edited closed instead of deleted - #150

Merged
QuicksilverSlick merged 4 commits into
mainfrom
fix/bench-old-cards
Nov 10, 2026
Merged

QuicksilverSlick merged 4 commits into
mainfrom
fix/bench-old-cards

Conversation

@QuicksilverSlick

Copy link
Copy Markdown
Owner

Factory-Task: bench/old-cards-edited
Built-by: claude-code/claude-opus-5-5
Claude-Account: russelledeming@gmail.com
Undo: revert the merge. Older builds ignore the new fromPost key in state.json. Posts already edited keep their first line and missing seeds, and a card kept instead of deleted stays in the feed.

Needs a desktop launch check, because it touches desktop/. The code it changes is Bench service code, which does not run in a debug build, so the launch check only shows the app still starts. The first real test is the installed app after the update (see the phone steps).

The receipts PR (fix/bench-old-receipts) is stacked on this one and does the close-out receipts. It is separate because together they pass the lane's ~600 lines. When both ride one train, one launch check covers both.

Russell accepted one trade on 9 Nov 2026 (decision 6, yes). A retired card now stays for good in the phone's Search and Activity lists, reading as it was first posted. In return, an old post says on its first line that a tap doesn't count, and a tap after a socket gap gets an answer instead of vanishing with nothing said. See "Search and Activity" under Known limits. Reverting this PR alone restores the deletes. I did not try that revert with the stacked PR on top.

What changes

Follow-up to #125, for the path it left alone.

  • No stale card is deleted. An unanswered, untouched re-drop, or a card no longer asked, used to be deleted. It now stays. It is edited from its own text as posted, with the first line "Replaced by a newer card. A tap here doesn't count." or "No longer asked. A tap here doesn't count." Its seeds come off, and a tap in the next 24 h gets one reply, as fix(bench): a replaced card says so, and a tap on it is told it didn't count #125 does for a kept card.
  • It can't fail a tick. Publish (b) no longer calls the relay at all. It records the old post and moves on, and replaced.rs does the rest behind live work. lastError stops the kit's approvals, so failures go on the board under "Old cards". A failed delete or close there used to set lastError.
  • A retired card in a thread ends an older card's root-reply window only if something is on it, as on main. One nobody touched leaves that window alone, so a reply naming only the root still reaches the card still open there.

One visible side effect: an old post is now edited after its new card goes up, not before, so live work goes first.

Not changed: CARD_FORMAT, the kit's findCard, the text the kit parses and WATCH_MAX. The orphan path, which needs lost state, still deletes an orphan nobody touched. Its one change is that a post edited closed is never adopted as the live card (see "Repost storm").

What the block#782 search shows

His screenshots do not show a deleted card staying on the phone. A search cannot show one.

  • Search reads the relay, not a local store (mobile/lib/features/search/search_provider.dart). The relay drops deleted events twice: deleted_at IS NULL in the search query and again when it loads the hits. This is the fork's relay code. Block's hosted relay was not read.
  • Search never shows an edit. It asks for kinds 9, 40002, 45001 and 45003, and an edit is kind 40003. So a preview that doesn't start "Replaced by" says nothing about whether the post was edited.

Each row lines up with a post the Bench never deleted. The screenshots show no ids, so I matched them by title, heading and time.

Row on the phone Post Posted (CT) In state
27 Oct, "Escalated: Release the badge swap" 8848ebead096 27 Oct 23:51 kept
3 days ago, "Back to you" 51d8c27105e0 31 Oct 20:57 kept, no answer recorded (inferred)
3 days ago, "Release the refreshed leaderboard" 9bc794e294f4 1 Nov 12:30 kept
2 days ago, "Back to you: Release refreshed lead..." daa8272a0b23 1 Nov 18:18 the item's card, a receipt with 4 seeds
"as of 12:04" board be8c7de7da99 4 Nov 12:04:33 the live board

#125 has never fired in production. kept has held 8 since 1 Nov 20:10 CT, before #125 merged at 22:33, and replaced is empty in every backup since. The three kept block#782 posts predate it and were not backfilled, so they still have their pills.

The channel view differs, and that is where a deleted card can stay. It shows edits and pills. A fresh open reads the relay, and a kind 5 that arrives live removes the post. But a card deleted while the socket was down stays on screen until the app restarts: _init merges the rows it holds with the new page, and the page's deletion markers only cover rows it returns. A tap there is refused ("reaction target event not found"), and the unawaited addReaction shows nothing. This is why the delete path goes, at the cost of a few more dead posts. I read this in the fork's mobile/. I did not run it (no Flutter on this machine) and did not read Block's build.

The lost tap on block#782 is not settled. State records no deletes, so I cannot rule out a deleted block#782 card: the Orchestrator's writes at 01:31 and 01:49 UTC on 1 Nov could have posted one that went at 01:57. But a tap on a kept post with its pills fits with no delete at all. 51d8c27105e0 looks like that post. At 00:15 UTC on 2 Nov, 9bc794e294f4 had no previous, so it carried no earlier answer. That only says 51d8c27105e0 closed with none recorded if b37fa9531d0a was not a block#782 card. kept lists it between the two, it appears in no sampled state, and carry_card can lose an earlier answer across a close-out. Phone step 1 tests this: a highlighted pill settles it, and no highlight leaves it open.

A phone fix can't reach him from this repo. The fix is about 15 lines plus tests: catch the refusal at the 4 addReaction call sites and say so, and drop rows the new page no longer returns. But his phone runs Block's Play build, last confirmed by him on 16 Sep and noted again on 29 Oct, and the fork has never released a phone app. So it needs Block to ship it, or a sideloaded build with its own signing and app id. Filing it upstream needs his OK.

How to verify on the phone

Steps 2 and 3 take under a minute each. Step 1 may take longer.

  1. Settle fix(desktop): sync persona pack directory across worktree instances block/buzz#782, any time. Search can't take you there: a hit opens the channel at its newest rows, not at the post. So open #bench and scroll back to the evening of 31 Oct, to the "Back to you" post from 20:57 CT. About 45 cards were posted since, so this may need a second page and may take more than a minute. State points to no answer recorded on it, if b37fa9531d0a was not a fix(desktop): sync persona pack directory across worktree instances block/buzz#782 card. So if a number pill there is highlighted as yours, that is the lost tap, and it landed on a kept post. If none is, the tap went somewhere else and a deleted card stays possible.
  2. After the update, have a session drop a fresh test card into ~/.dreamforge/bench/inbox/<writer>/, then drop it again reworded. Leave the first one untapped. In #bench, in the channel rather than search, the old post should stay. Its first line should say "Replaced by a newer card. A tap here doesn't count." and its pills should be gone.
  3. Add a number to that old post from the reaction picker. Within seconds there should be an inbox mention and a "1 reply" link saying "That tap didn't count" and linking the live card. Nothing should be recorded on the live card.

Budgets

Limit Value When it fills
Writes per tick (shared, unchanged) 4 per 2 s tick, 120/min Live cards go first and old posts wait. Nothing is dropped.
Lookups for old posts (unchanged) 1 per tick, 30/min The rest wait. Past 30 failing at once, the rest wait for those to go (Known limits). Publish (b)'s up to 4 lookups a tick are gone.
Reads for taps (unchanged) 1 batched query per tick
Old posts read for taps (WATCH_MAX, unchanged) 100, about 3 times the busiest day (28 drops on 1 Nov) The oldest go unread, and the board says how many and what to do.
Old post's window 24 h No reply to a tap after that. One that never updated is let go then, and the board says so until that point.
  • One old post costs: 1 lookup and 1 edit, then 1 lookup and up to 4 removals. One posted in a thread adds 1 lookup, to see what else is on it. A reply adds 1 lookup and 1 post.
  • After lost state only: 1 lookup per recovered orphan asked again, to see whether it was edited closed.
  • Worst burst: every card asked again at once. At 40 cards (MAX_CARDS + OVER_CAP) with 4 options each, that is 200 writes for the new cards (50 ticks), then 200 for the old posts at 2 ticks each (80 ticks, or 3 ticks each for one in a thread): about 4 minutes. The test runs 5 cards against the same budgets and checks every tick stays inside them.
  • Every ceiling at once is still the relay's 300/min: poll 120, read 30, lookups 30, posts 120.

Repost storm: none found. Publish (b) decides staleness from the card's hash against its item, and editing an old post changes neither. An old post is pushed to kept before the card is forgotten, so a relaunch never adopts it (an_old_post_never_brings_a_repost, which also covers state lost). With kept lost too and the same Drop asked again, a post edited closed stays an old post and a new card goes up (lost_state_never_adopts_a_post_edited_closed). One outside cause remains: a roles.json that flaps every tick would repost every tick, as it does today, but the old posts would now stay. Writes stay inside the 4 a tick. I saw no sign of this.

Known limits

  • The 8 cards kept before fix(bench): a replaced card says so, and a tap on it is told it didn't count #125 keep their pills, including three of the fix(desktop): sync persona pack directory across worktree instances block/buzz#782 posts. State holds only their ids, and what they show includes a later edit. A small follow-up can read their newest edit from the relay.
  • Search and Activity on the phone keep a retired card for good, as first posted. The edit can't reach either list. Search asks for kinds 9, 40002, 45001 and 45003, and Activity asks for 9, 40002, 1, 45001 and 45003 by #p and shows the event's own content. An edit is kind 40003. So each untouched re-ask adds a Search row, and an Activity row if the card pinged him (ping_owner tags pending needs-you cards). The row reads as the original card, including "Tap one number below to answer", with no "Replaced by" line. On main, deleting an untouched card removed it from both, since the relay's search drops deleted rows. Search is where the fix(desktop): sync persona pack directory across worktree instances block/buzz#782 question started, and this change only adds rows there. I read the kinds and the content in the fork's mobile/ and at Block's tags mobile-v0.20.0-rc.1 and mobile-v0.27.0-rc.15, and did not run any of it. The cost is slow, at the rate below. This is the trade he accepted on 9 Nov 2026: a few stale-looking rows in those lists, against a tap that vanishes after a socket gap.
  • More dead posts in #bench. A retired card now stays, at a few a day by nightly sampling (6 in 13 days, a lower bound, since a card posted and deleted within a day doesn't show). The phone pages 50 rows at a time, so a live card can sit further down. A CARD_FORMAT bump would leave one old post per open card.
  • An edit that fails for 24 h is let go. The board says so until then. Past 30 posts failing at once, the old posts' one lookup a tick is all spent on those 30, so the rest wait behind them untried until the 30 go, and the board's "Would not update" counts only the 30. Each waiting post gets one try after that, then goes. Sized for a relay fault: Block's relay took 95 of 95 receipt edits in production.
  • Block's phone is unmeasured, as in fix(bench): a replaced card says so, and a tap on it is told it didn't count #125. If it keeps the pills, the first line and the reply still hold.
  • Named answerers, as in fix(bench): a replaced card says so, and a tap on it is told it didn't count #125: only the owner's keys are read, so Craig tapping an old post gets no reply.
  • Rebuild limit, not new: at 5000 writer kind 9s (10 pages of 500) a relaunch's rebuild is cut and sets lastError for that tick. This change adds a few a day to that count.

What was exercised, and how

All of this ran on this Windows machine. The real app, Block's relay and Block's phone were not run. The relay in the Rust tests is the harness's fake one.

Rebased on 9 Nov 2026 onto main at 5e1fce0cb, with no conflict. The five commits it picked up touch only Cargo.lock, deny.toml and crates/buzz-huddle-bridge, nothing in the Bench. After the second review's fixes, also on 9 Nov, I re-ran the Bench tests, the whole cargo test --lib, cargo fmt --check, clippy, check-file-sizes and git merge-tree at this head. The other results below are from before those fixes.

  • Rust: cargo test --lib bench:: gave 220 passed, 0 failed, 2 ignored at this head. Main before the rebase gave 212 passed, 0 failed, 2 ignored. The whole cargo test --lib at this head gave 3518 passed, 0 failed, 16 ignored. The mesh-llm and compiled-flags lanes were not run; Linux CI covers them.
  • Rust checks: cargo fmt --check exits 0. cargo clippy -p buzz-desktop --lib --tests exits 0 with nothing under src/bench. check-file-sizes exits 0.
  • Mutations: I took out 11 guards one at a time, and each now fails a test: failed posts going at their window, recording the old post, keeping it from recover, owing its edit, the budget check, the lookup cap, a missing post counting as "not found", charging the edit to the budget, its thread root in kept_at, its root for the reply, and #[serde(default)] on the new key. Two survived at first (the lookup cap and charging the edit), so the tests got sharper and I re-ran those two. A third showed a check in idle() was redundant, so it came out. The first ran on this commit. The other 10 ran on the stack's combined tree, not on this commit alone. I also tried a one-word break in place of the return the tick takes when lookups run out: it frees failed posts too, but drops an owed post past its window with no try, for example after the Bench was off for a day, so past_its_window_a_post_still_gets_its_try_when_lookups_run_out fails on it. After the second review, 6 more at this head, each failing a test: held taken out of (b)'s guard; a retired post in a thread always adding its kept_at entry, never adding it, or counting its own closing edit as something on it; adopting a post edited closed; and treating any edit as a closing one.
  • Desktop: no JS or test vector changed. pnpm test gave 7201 + 193 passed, 0 failed.
  • Kit: not touched and its tests not re-run. It reads state.items, and this adds one key under replaced.
  • Merges: git merge-tree is clean against main at 849bd8bbb, five commits past this branch's base. On 9 Nov gh pr list shows no other open PR touching the Bench.
  • Not done: the desktop launch check.

Review

One adversarial review has run on this branch. It found three things, all in this body now: Search and Activity (Known limits), failed posts outliving their 24 h when lookups ran out (fixed, with a test), and phone step 1 and one time in the table (corrected).

A second, independent review covered both branches on 9 Nov 2026 and found three things here. None needs a decision.

  • A retired card nobody touched ended an older card's root-reply window for good, a regression from main. Fixed (a_retired_card_nobody_touched_ends_no_older_cards_window).
  • With state lost, the same Drop asked again could adopt a post edited closed as the live card. Fixed (lost_state_never_adopts_a_post_edited_closed).
  • Nothing tested held in (b)'s guard. The code was right, and a test now pins it (a_cut_owner_page_leaves_a_stale_format_card_up).

Size: 599 changed lines, 186 of code and 413 of tests.

🤖 Generated with Claude Code

QuicksilverSlick and others added 4 commits November 9, 2026 13:52
A phone that lost its socket across a delete keeps showing the card, the
relay refuses a tap on it and the phone's code ignores the refusal, so
the tap vanishes with nothing said. An untouched re-drop or a card no
longer asked now stays, as a kept one does: its first line says a tap
doesn't count, its seeds come off and a tap on it gets one reply.

Publish (b) no longer calls the relay, so nothing there can fail a tick.
The edit, its lookup and any failure are the old posts' tick's, on the
same budgets, and on the board under "Old cards".

Claude-Account: russelledeming@gmail.com
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every card retired in a thread added a kept_at entry, including untouched
ones main deleted and left no entry for. An older card still open in that
thread then stopped taking replies that name only the root, for good,
with nothing said.

A retired post in a thread now reads what is on it once it is edited, and
adds the entry only if there is something besides its seeds and its own
closing edit, as main did. It costs one lookup more, on the old posts'
one a tick, and only for a post in a thread.

Claude-Account: russelledeming@gmail.com
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Retired posts now stay on the relay with their bench:card marker. With
state lost and the same Drop asked again, publish (c) adopted one as the
live card: a seedless post saying a tap doesn't count, whose tap would
then be read as the answer.

An orphan edited closed now goes to kept like any orphan with something
on it, and a new card goes up. One lookup per orphan, after lost state
only.

Claude-Account: russelledeming@gmail.com
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Nothing failed with `held` taken out of publish (b)'s guard: a re-worded
drop waits in the inbox before it gets there, so only a card whose format
moved reaches it. Without the guard that card is retired for good with a
tap on it unread.

Claude-Account: russelledeming@gmail.com
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@QuicksilverSlick QuicksilverSlick added the lane:ready Builder is done: green, agent-reviewed, exercised. The steward puts it in the next train. label Nov 9, 2026
@QuicksilverSlick
QuicksilverSlick merged commit 750cb8d into main Nov 10, 2026
123 of 126 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

lane:ready Builder is done: green, agent-reviewed, exercised. The steward puts it in the next train.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant