Repository navigation
Description
#954/#961 stopped the new permanent-failure card from offering Retry, but the button is still there — including for a failure that happened today on a build that has #961. The render is not right: two of the three failed-card surfaces never look at the flag at all, and #961 only fixed the third.
Evidence (reproduced on 2ab9e392)
One rule, three surfaces, two spellings:
| surface | file | what it checks |
|---|---|---|
| social dock | plugins/social/client.js:1979-1982 |
item.retry === false — correct, fixed by #961 |
| chat-bubble card | core/src/client/held-reply.js:52-54 (data-flybot="held-reply-actions") |
status === 'failed' only |
| dock card | core/src/client/flybot-slots.js:287 / :297 |
status === 'failed' only |
The flag does exist and is written correctly:
core/src/host/hooks.js:470—if (error?.noRetry === true) approval.noRetry = true, stored with the record.core/src/host/hooks.js:509—...(a.noRetry === true ? { retry: false } : {}), soheldRepliescarriesretry: false.
The two core cards simply never read it. What makes this easy to half-fix is that the same rule arrives under two field names: the dock gets the raw approval (noRetry: true), the bubble gets heldReplies (retry: false).
A second, smaller subset: a failure recorded by a build before #961 has no noRetry in the record, so no reader can derive it. That subset is unavoidable and is not the main defect — the main defect is that today's failures still get the button because the two core surfaces ignore the flag.
Expected
A failed card whose reason is terminal never draws Retry, on any of the three surfaces, whether the failure happened today or three builds ago. The decision "this can never succeed" is made in one place and both the writer and the view read it.
Hard requirement
One predicate, both spellings. canRetry(record) (or a normalisation at the producer so both noRetry and retry collapse into one shape before any renderer sees it) must be the single decision used by all three surfaces. Fixing held-reply.js while leaving flybot-slots.js alone — or the reverse — is a half fix and will be sent back.
For the pre-#961 records, derive the terminal answer from the stored error text at view time (the reasons are already sentences: "is no longer shared with you … Ask its owner for a new invite", bot_not_found). No state migration, and the derivation lives inside the same predicate, not a second copy of the string list.
Acceptance
- A new failure with a terminal reason (record has
noRetry: true) renders with no Retry on all three surfaces — bubble card, dock card, social dock. - The same is true for a record that reaches the view as
heldReplies(retry: false): both spellings take the same branch. - A persisted failed hold written before
#961(fixture:status: 'failed', error "… is no longer shared with you … Ask its owner for a new invite.", nonoRetry) renders with no Retry. - A failed hold with a retryable reason (429 / timeout / paused) still renders Retry on all three surfaces — the negative control.
- Only one copy of the terminal/retryable decision exists in the tree; no renderer re-implements it and no reason string is matched in two files.
- The approval view's shape does not change for other readers:
heldRepliesOfkeeps its fields, and no state migration is required.
Where
core/src/client/held-reply.js:52-54, core/src/client/flybot-slots.js:287/:297 (the actual fix), core/src/host/hooks.js:470/:509 (the flag and the predicate's home), plugins/social/client.js:1979-1982 (keeps working, may switch to the shared predicate). Any core/ file touched here must be mirrored byte-identically in patches/ds-bot/files/, and the mirror must be regenerated with hunk line numbers that a real git apply accepts.
Red proof
On 2ab9e392: a fixture that feeds both spellings of one terminal failure renders Retry in held-reply.js and in flybot-slots.js — two red — while a 429 record keeps Retry. Restoring only one of the two cards must leave exactly one red, which is how the half fix gets caught.
Dedupe
Not #954/#961: those changed what a new failure says and whether the social dock offers Retry; this is the other two surfaces plus the one-predicate requirement. Not #963 (looks) and not #955 (room hint text).
Activity
[-]Failed cards written before #961 keep a Retry that can never work: the render is right, the stored record has no flag[/-][+]A failed card that can never succeed still offers Retry: two core cards never read the flag, and #961 fixed only the social dock[/+]Birfy commentedon Oct 24, 2026
Claimed on
fix/968-can-retry@78ec20a5— PR #975.One predicate (
core/src/shared/held-retry.js,canRetry) now decides the Retry button on all three surfaces: the chat bubble, the dock card, and the social plugin's own dock. Both spellings (noRetry: trueon an approval,retry: falseon a hold) go through it, and a record written before the flag existed is read from its storederror/codeat render time — no migration.core/client.jsrebuilt,patches/ds-bot/regenerated.#968: name the shared retry rule in the four subsystem docs
Word a revoked share as one line, with no retry (ThinkFlowLab#974)
ThinkFlowLab#968: one predicate decides whether a failed card can ret…
Show one Share-with-a-friend row per Bot (ThinkFlowLab#976)
Narrow capability_not_granted to chat.public (ThinkFlowLab#1003)