Skip to content

fix(relay): one disconnect notice on the main surface; passive reads recover on their own - #3181

Open
matt2e wants to merge 21 commits into
mainfrom
disconnected
Open

matt2e wants to merge 21 commits into
mainfrom
disconnected

Conversation

@matt2e

@matt2e matt2e commented Nov 10, 2026

Copy link
Copy Markdown
Contributor

Why

During a relay outage the app reports the problem many times over. The recovery toast appears, and the sidebar, pickers, panels and pages each add their own inline error and Retry button, often with raw text such as ReadError: Relay request failed (500) or TypeError: Load failed. Several of those errors stay after the relay comes back until the user reloads. Offline mode has no app-level notice at all, and a broker that is unreachable at launch is shown as an access denial.

What

On the main surface, a connectivity failure now produces one message: the toast "Buzz is having trouble connecting / Retrying automatically." Passive reads (anything the app loads by itself, including what a picker, panel or dialog loads on open) hide their own disconnect error and Retry, join session recovery, and fill in on their own once the relay answers. Nothing needs a click or a reload.

Unchanged by design:

  • User actions (send, save, delete, a typed search) keep their own errors.
  • Denials, other 4xx and malformed relay responses are not disconnects and still show where they happen.
  • Settings, onboarding and pairing are outside the main surface and are untouched.

Surfaces covered: the connection itself (offline mode, a failing connect, launch with an unreachable broker, a visit that times out during an outage), the sidebar (hidden conversations, Solo workspaces, recent activity), the channel view (older messages), thread/profile/members panels, the composer pickers (emoji, GIF, mentions, recipients), Activity, Shared notes, the Agents marketplace, the ⌘K lists, Drafts & sent, Workflows and Todos.

How

  • feat(relay): RelayRecovery.register(source) lets any read join session recovery while it fails. useRecoverableRead (React) and createRecoverableRead (stores) return quiet; while quiet, a surface hides its own error and Retry. The rule is documented at the top of src/features/relay/recoverable-read.ts.
  • Several stores kept only an error string or rethrew a plain Error, which dropped the transient classification so recovery never retried them. Those stores now carry an optional transient flag.
  • ConnectionNotice is mounted once in App.tsx (no longer in the sidebar). It shows the toast after 30 s when either session recovery's notice is due or the connection itself is erroring/retrying or serving cached data.
  • The identity lookup retries transient failures on the recovery backoff, so an unreachable broker reads as "connecting" rather than "denied".
  • A visit that times out while recovery is recovering keeps its loading view and reopens itself when the relay is ready.

Native-only surfaces (Reminders, Huddles) have equivalent fixes on stream branches but are held back from this PR until they can be recorded natively.

Testing

Not run locally; relying on CI. Unit tests were updated/added for the behaviour changes. Every change below has a side-by-side recording: Before is ba3455d52, After is this branch, both driven by the same fault timeline in WebKit. Long waits are fast-forwarded (badge shown).

Whole app (merged branch)

Full relay outage with a reload, visiting the mention picker, #general, Activity and the members dialog. Before stacks inline errors and Retry buttons under the toast; After shows loading states and the toast only.

01-full-outage-tour.mp4

Same tour without a reload. After restore, Before stays stuck; After fills in on its own.

01b-full-outage-tour-no-reload.mp4

Offline mode after a reload. Before shows "Offline · Showing saved conversations.", the ⌘K note and "TypeError: Load failed", each with Retry connection, and no toast. After shows only the toast and recovers on its own.

03-cached-offline-tour.mp4

Opening #general during an outage. Before gets stuck on "This page isn't available". After keeps loading, shows the toast and opens #general by itself.

05-visit-during-outage.mp4

Members dialog during an outage. Before keeps "Some agents could not load." and "Archived identities could not be checked." after restore; After shows neither.

02-members-dialog-outage.mp4

Solo workspaces with a 503 on the channel-kit read. Before keeps "ReadError: Relay request failed (503)" + Retry workspaces; After never shows them.

04-solo-workspaces-retry.mp4
Connection, offline mode and launch (7 videos)

Offline mode. Before: the "Offline" row with Retry. After: only the toast at ~30 s.

01-cached-offline-notice.mp4

Full outage: one toast, cleared on restore (confirms a single mount).

02-relay-outage-notice.mp4

Broker unreachable at launch. Before: "You don't have access to this", never recovers. After: "Connecting…", the toast, then the community loads.

03-broker-unreachable-at-launch.mp4

Connect failure with nothing cached. Before: the "Can't connect" card and no toast. After: loading and the toast, then reopens by itself.

04-no-cache-connect-failure.mp4

Offline mode on Workflows, Bestie, pinned Bestie and the Activity preview. Before: "TypeError: Load failed" + Retry connection on each. After: "Connecting…" and the one toast.

05-cached-pages-tour.mp4

A malformed session response (not a disconnect). Both sides keep their notes and Retry; no toast. Confirms non-connectivity errors are untouched.

06-cached-nonretrying-error-keeps-retry.mp4

Fresh profile with a failing connection, open Agents, return to Messages. Before: a second "Can't connect" card. After: loading and the toast.

07-visit-settled-no-second-card.mp4
Sidebar (4 videos)

Full outage with a reload. Before: hidden-conversations and workspaces errors; the workspaces error sticks after restore. After: only the toast.

01-full-outage-whole-sidebar.mp4

503 on the hidden-conversations read. Before keeps the error and Retry; After shows nothing inline.

02-hidden-conversations-503.mp4

503 on the preferences and templates reads. Neither side errors; these were already gated.

03-preferences-503.mp4

Recent-activity read held. Before: the "Couldn't refresh recent activity" toast with Retry. After: only the recovery toast.

04-recent-activity-hold.mp4
Channel view (3 videos)

Older messages during an outage. Before: "Retry older messages". After: "Loading older…", then the page loads itself after restore.

03-older-messages-outage.mp4

An uncached channel with its window read failing. Both sides stay on loading and fill in; the new owner is a safeguard.

01-uncached-channel-window-503.mp4

A cached channel through a 30+ s outage. No strip or edge error on either side; a new message arrives after restore.

02-cached-channel-outage-restore.mp4
Thread, profile and members panels (3 videos)

An uncached thread. Before: "This thread didn't load." + Try again, stuck. After: loads, toast, fills in.

01-thread-uncached.mp4

A profile with the kind 0 read failing. Before: key-only panel with Retry profile. After: "Loading profile…", then name and bio.

02-profile-kind0.mp4

Members dialog with the member-list read failing. Before: "Relay request failed (503)" twice and "The member list could not load." After: cached list, then access details fill in.

03-members-dialog.mp4
Composer pickers (6 videos)

Emoji picker with the catalog read held. Before: "ReadError: Relay read timed out" + Retry emoji, even after the fault clears. After: Unicode grid, toast, recovers.

01-emoji-picker-held-read.mp4

Mention picker with a 503 on agent and archive reads. Before: textless "Retry agent list" and "Retry archive information", never clears. After: neither.

02-mention-picker-503.mp4

Mention picker left open through an outage. Before keeps "Retry community search" and three people. After fills in five people on its own.

03-mention-picker-outage.mp4

Legacy teams row with a 503. Before: "Could not load legacy teams." + Retry. After: no row error. (The Solo error here is fixed by the sidebar commits.)

04-mention-picker-legacy-teams.mp4

New DM recipient picker. Before: "Could not load people." + Retry and no toast. After: placeholder rows, toast, then eight people.

05-recipient-picker-people.mp4

Emoji : completion and GIF availability. Before: "Community emoji unavailable…" and "Could not check GIF availability." + Retry. After: neither; the GIF check answers once the relay returns.

06-emoji-completion-and-gif-availability.mp4
Activity, Shared notes, Agents and ⌘K (6 videos)

Activity with a 503 on the mentions feed. Before: "Relay request failed (503)" + Retry activity, stuck. After: neither.

01-activity-mentions-503.mp4

Activity through a 45 s outage. Before: "Showing retained activity. Refresh" prompts kept after restore. After: only the toast.

02-activity-outage.mp4

Activity preview card on hover. Before: error + Retry, stuck. After: "Checking recent activity…", then the summary.

06-inbox-preview-503.mp4

Shared notes with the agent inventory failing. Before: "Relay inventory unavailable…" + Retry agents. After: "Loading your agents…", then fills in.

03-shared-notes-inventory-503.mp4

Agents marketplace tab. Before: "Couldn't load shared agents." + Try again, stuck. After: fills in by itself.

04-agents-marketplace-503.mp4

⌘K lists on open. Before: "Couldn't load all channels." + Retry channels. After: "Loading channels…", then the list.

05-command-k-channels-503.mp4
Drafts & sent, Workflows, Todos (3 videos)

Drafts & sent. Before keeps "Sent messages could not be loaded." + Retry after the fault clears. After: toast, clears itself.

01-drafts-sent-503.mp4

Workflows. Before stays on "Workflow discovery paused" + Retry. After: "Reading workflows…", toast, then the page.

02-workflows-discovery-503.mp4

Todos panel. Before stuck on "ReadError: Relay request failed (503)". After: "Loading todos…", toast, then "No todos yet".

03-todos-canvas-503.mp4

🤖 Generated with Claude Code

matt2e and others added 21 commits November 10, 2026 16:22
Add RelayRecovery.register for dynamic sources, createRecoverableRead for
stores and useRecoverableRead for React surfaces, so a passive read's
connectivity failure feeds the one recovery notice instead of its own error.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Matt Toohey <[email protected]>
The Solo groups read rethrew the kit's error as a plain Error, so a 5xx or
timeout lost its connectivity classification: the store never flagged it
transient, session recovery treated it as settled and never retried, and
"Retry workspaces" stayed until a reload. Keep the kit's verdict as a
ReadError, and hide the workspaces error and both Retry workspaces controls
while the failure is transient.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Signed-off-by: Matt Toohey <[email protected]>
…-activity reads

Visibility exhausted its quiet retries without a transient flag, so its
recovery source never retried and the sidebar showed its own error. Flag a
connectivity failure transient and hide the inline error and Retry. Recent
activity gets the same: report transient failures, register a recovery source
that re-reads the roster, and hide its own toast so only the one recovery
notice shows.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Signed-off-by: Matt Toohey <[email protected]>
…sion recovery

An older page that fails for connectivity now joins session recovery, which
pages again on its own: the timeline edge shows "Loading older…" instead of
the reason and Retry, and the offline "load older once Buzz reconnects" copy
is gone. A first window read that fails for connectivity does the same in the
channel body. The window snapshot gains `transient` to say so.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Matt Toohey <[email protected]>
Opening a thread starts its read by itself. A connectivity failure of
the thread read, an older page or the selected message no longer shows
"This thread didn’t load." or Try again; recovery retries it and the
thread fills in. Unavailable and not-found variants are unchanged.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Matt Toohey <[email protected]>
The profile panel's kind 0 read, owner evidence read and channel list
no longer show "Could not load this profile.", Retry profile or Retry
channels for a connectivity failure; recovery retries and fills them in.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Matt Toohey <[email protected]>
Opening Manage members starts the roster, names, roles, owners and
general access reads by itself. A connectivity failure of any of them
no longer shows its own message, Retry member data or Reload access;
recovery retries them and the dialog fills in.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Matt Toohey <[email protected]>
…recovery notice

Opening the emoji, mention and recipient pickers loads catalogs, agent
inventory, archive evidence and people. A connectivity failure in those
passive loads no longer shows the picker's own error text or Retry; each
joins session recovery through useRecoverableRead and fills in when the
relay answers. Stores report `transient` (emoji directory, identity
archives, agent library and choices) so the pickers can classify without
parsing strings. The emoji directory stores the error message rather than
String(error), so no raw "ReadError:" prefix appears, including in the
send-time preparation error, which keeps its error and Retry.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Signed-off-by: Matt Toohey <[email protected]>
The Sent view and the draft preview showed their own "could not be loaded" error
and Retry on a 503 or timeout. Observed views now flag connectivity failures
(additive `transient` on EventViewSnapshot); the Sent view joins session
recovery with useRecoverableRead and the preview defers to head recovery.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Signed-off-by: Matt Toohey <[email protected]>
Workflow discovery ("Workflow discovery paused" + Retry), the per-channel
configuration and run-history reads, and the channel list ("Retry channels")
showed their own error on a 503 or timeout. Workflow views and the channel list
now flag connectivity failures (additive `transient`); the page joins session
recovery with useRecoverableRead, reads as loading meanwhile and refills on its
own when the connection returns.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Signed-off-by: Matt Toohey <[email protected]>
The Todos panel showed "ReadError: Relay request failed (503)" and "Couldn't
load todos. Try Refresh." for a failed Canvas read, plus its own "Channel
members unavailable" and "Retry users" for the channel list and profile reads.
It now joins session recovery for those passive reads, reads as loading
meanwhile and fills in on its own. Save errors and denials are unchanged.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Signed-off-by: Matt Toohey <[email protected]>
The socket-interruption snapshot is a deliberate editor message that retains
the draft (covered by the workflows journey), so it is not flagged transient.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Signed-off-by: Matt Toohey <[email protected]>
…ures to the recovery notice

The agent inventory and the archive list load when the members dialog opens.
Under a connectivity failure they showed "Some agents could not load." and
"Archived identities could not be checked." beside Retry until a reload.
Read them with useRecoverableRead like the rest of the dialog, so they hide
while quiet and recovery retries them and clears the messages on its own.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Signed-off-by: Matt Toohey <[email protected]>
…overy

The inbox, mention feed and reaction feed snapshots now say when their
error is a connectivity failure (transient). The Activity page and its
sidebar preview register those reads with session recovery and hide
their own error text, Retry activity and the Refresh prompts while
recovery retries, so only the recovery notice reports the outage.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Matt Toohey <[email protected]>
…o session recovery

The agent library snapshot and the merged inventory carry transient when
the relay's own read failed for connectivity. Shared notes hides the
inventory error and Retry agents (and the notes errors) for it, and the
Agents marketplace hides Try again; both retry through recovery and fill
in when the relay answers.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Matt Toohey <[email protected]>
…ssion recovery

The joined-conversation list and the channel directory that load when
Command-K opens no longer show Couldn't load... or Retry channels for a
connectivity failure; recovery retries them and the recovery notice is
the one report.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Matt Toohey <[email protected]>
Mount the recovery toast at the app level so cached/offline mode and a
retrying connect show "Buzz is having trouble connecting" after the same
30 s quiet delay, once, and clear when connected. A visit that failed only
because the connection is retrying keeps loading instead of adding a second
"Can't connect" card. The sidebar's Offline strip with Retry stays only for
a failure nothing retries.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Signed-off-by: Matt Toohey <[email protected]>
…ce pages

Search, Activity, Shared notes, Workflows, Bestie and Channels
no longer show their own offline copy, raw error or Retry while the
connection retries by itself. Saved-conversation search results stay. A
failure nothing retries keeps its own state.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Signed-off-by: Matt Toohey <[email protected]>
When the identity read fails for a reason that clears by itself, retry it on
the recovery backoff and on online/focus, read the connection as failing and
retrying, and open the shell instead of claiming the account has no access.
A wrong answer from the broker is still not retried.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Signed-off-by: Matt Toohey <[email protected]>
…ading and reopen it

A visit opened while the session is live but its reads are failing (a full
relay outage after load) timed out into "This page isn't available" with
Try again, replacing the page and any open dialog, and never reopened when
the relay returned. Session recovery already retries those reads and the one
notice reports them, so treat "recovery is retrying" like a reconnecting
connection: the timed-out visit keeps its loading view and reopens once, by
itself, when recovery settles and the connection is ready. Denied, removed
and unknown visits, host-decided failures and Settings keep their cards.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Signed-off-by: Matt Toohey <[email protected]>
@matt2e
matt2e requested a review from a team as a code owner November 10, 2026 05:25

@kalvinnchau kalvinnchau left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Required typecheck and new recovery tests fail at this head. Three concrete fixes below.

key={createdRun ?? ""}
capability={capability}
workflow={history}
recovery={session.recovery}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 [P2] Preserve the optional-session contract. session is RelaySession | undefined, so this dereference fails typecheck (TS18048) and crashes existing session-less component callers with Cannot read properties of undefined (reading 'recovery'). It reproduces in 51 WorkflowLanding tests. Use session?.recovery, as the other call site does. Production WorkflowsPage supplies a session; the demonstrated defect is the optional component contract and required build gate.

* the notice timer, retryNow and retryAll like a static source. The returned
* function removes it and re-evaluates, so a failed source that goes away
* cannot keep the notice up. Registering twice adds it once. */
register(source: RecoverySource): () => void;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 [P2] Migrate the typed recovery doubles when requiring register. Nine LiveStatus.test.tsx implementations (lines 59, 332, 341, 361, 396, 453, 516, 581 and 649) still omit this method and produce TS2741. All six JavaScript CI shards stop at typecheck before Vitest, and local typecheck reproduces it. Update those implementations, using the real recovery owner where practical.

loadingOlder: loadingOlder || accepted,
historyLimited,
error,
...(transient ? { transient: true as const } : {}),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 [P2] Forward transient state into the rendered window. The fixture adds this flag to its store snapshot, but render() separately constructs the ChannelTimeline window at lines 323–332 without it. Both new older-page recovery cases consequently retain Relay request failed (503) and fail at line 2113. Pass the snapshot or transient flag to the actual component so these tests exercise the intended recovery state.

kazemek21 added a commit that referenced this pull request Nov 10, 2026
## TL;DR
Captured explicit reads now check the current saved manual-unread
journal inside the replacing transaction, so a newer local
choice—including one saved by another window—is not erased. Retries
retain their original cut; the additive
`unread.prepareThreadRead(channelId, rootId)` captures only retained
incoming replies without reading the root or advancing the channel.

The fence also preserves earlier same-window queued manual actions,
authorizing their revisions only after a successful save whose prior
same-key intent is also captured. An earlier queued save cannot mask an
intervening unread choice from another window. It leaves reading-lease
ownership, passive observations, remote-marker ordering, journal format
and relay protocol unchanged.

Mark all now reserves one shared saved revision cut before queued
writes. Each predicate retains only the revision, not a full journal
copy.

### Validation
- Shared-cut correction: pinned Linux Blox run of both complete owner
files passed 383 tests (174 read-state; 209 unread). The per-sweep
capture-budget and capture-failure regressions fail on the prior
implementation; older queued-save controls pass. No new browser cases.
- Linux Blox, pinned tools: both owner files passed 366 tests on the
initial head. After review fixes, `pnpm exec vitest run
src/features/relay/unread.test.ts` passed all 206 tests. The final
queue-ordering fix passes all 174 tests in
`src/features/relay/read-state.test.ts`, typecheck and the commit/push
hooks.
- Pre-fix channel regressions: six failures; bypassing the new
transaction fences makes eleven targeted concurrency regressions fail.
Replacing canonical thread selection with parent-edge traversal makes
the narrow-thread regression fail. Restoring the implementation passes
both owner files.
- Three successful-save interleavings fail before the final fix and pass
afterward, with three matching controls: channel, thread and message
marks overwritten by an older queued save after another window marks
unread.
- Eight additional regressions fail before the review fixes and pass
afterward: root/reply deletion before or after persistence, and shared
sidebar hints with absent, present or cross-window forced-message
overlays.
- Touched-file Biome, `pnpm typecheck`, `pnpm author:build`, strict
generated-SDK consumer compilation and repository commit/push hooks.
- Deterministic fixture gates cover newer channel/thread/message marks,
second-window saves at the transaction boundary, queued ordering, retry
cuts, later arrivals, independent root/thread markers and
access/lifecycle expiry. No browser tests were added or removed.

Generated type-only SDK declarations expose the additive method through
the existing `UnreadCapability` export; existing channel and message
APIs remain compatible. Capture requires initialized read-state storage.
The thread API does not discover history; receipts remain device-local.
A narrow read leaves the shared sidebar hint until channel open
reconciles it, since that hint has no subtree ownership. Its shared
message-force fence is conservative: a newer mark elsewhere in the same
channel can also reject the save. This does not establish native runtime
behavior, identical remote-tuple ordering or atomicity through an
operation's eventual return.

Related independent work remains with #2060 (quiet Solo/Inbox), #2610
(selected-message UI), #3181 (passive recovery), and #2939/#1666/#2956
(notification/audience behavior). This change is limited to the explicit
mutation/preparation seam.

Fixes NEXUS-4426

[Initiative
status](https://g2.sqprod.co/apps/kylez-buzz-improvements-status)

*Posted by Codex (gpt-6.1-sol) on behalf of kylez.*

Signed-off-by: Kyle Zemek <[email protected]>
Co-authored-by: Codex <[email protected]>

@wesbillman wesbillman left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Star Lord’s team source review, with independent Rocket, Mantis and Groot lanes and Star Lord integration across the 89 changed files.

Reviewed head f6b89e95d105f95aabad44a439ba0fe90c0421ec against target/merge-base 157bbb464c529a42cf16be077079441b1cac7fd9.

Four new P2 findings are attached inline:

  • Automatic recovery after a failed Todos Refresh can discard newer local work or invalidate an active save.
  • Typed recipient searches lose their own error and Retry feedback.
  • A transient profile-retry failure can remove the focused Retry control without transferring focus.
  • Non-transient activity HTTP 4xx responses enter indefinite quiet recovery and produce a misleading connection notice.

We also confirmed the source paths behind the existing optional WorkflowLanding session, required recovery.register test doubles, and timeline transient fixture findings. Those are not duplicated inline.

Coverage: Rocket reviewed recovery/session/store lifetimes and classification; Mantis reviewed identity, agents, member administration, workflows, drafts, inbox, todos and notes; Groot reviewed consumer recovery, search/pickers and focus; Star Lord reviewed app/navigation/sidebar integration, cross-lane findings and supplemental test diffs. Relevant design, navigation, relay and vision contracts informed the review.

Source-only limitations: no tests, typecheck, builds or apps were run, and no live behavior was reproduced. A single hosted-check snapshot showed required CI failing at this head; we did not establish the cause of every failing job. The PR description’s 38 video attachments remain uninspected. Test-source inspection does not establish runtime correctness or approval.

const { quiet: loadQuiet } = useRecoverableRead(
recovery,
{
status: busy === "load" ? "loading" : loadFailure ? "error" : "ready",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 [P2] Preserve newer todo edits when retrying a failed refresh

After an already-loaded panel’s Refresh fails transiently, loaded stays true and busy clears, so the person can type a new todo while recovery backs off. This callback then calls load() without the dirty/input confirmation used by the Refresh button. On success, load rebuilds the draft from the mount-time saved/savedDirty values (lines 250–270), clearing that newly typed input and potentially replacing newer unsaved content. It can also increment operation.current during an active save, invalidating its completion. Limit automatic recovery to the initial, not-yet-loaded read; keep a loaded panel’s explicit Refresh failures visible and under the existing confirmation/write guards. This also preserves the design guide’s requirement to merge refresh results into the latest draft.

? "loading"
: "ready",
error: directory.error || undefined,
transient: directory.transient,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 [P2] Keep typed recipient searches out of passive recovery

usePeople(session, query) also executes the text typed into “Message recipients”, but this registration treats every query as passive. If a typed search fails transiently, it hides both its alert and Retry; directory.loading is false between attempts, while directory.error also suppresses “No matching people”, so an uncached query leaves an empty chooser until the next automatic retry. This violates the stated contract that typed searches keep their own errors. Restrict this registration to the empty-query listing, as MentionPicker does with directory.passive, and retain the existing error/Retry for typed queries.

Comment on lines +805 to +808
{((!profile && shown !== "loading") ||
(profile?.verified && shown === "error") ||
retrying ||
ownership.failed) && (
(ownership.failed && !ownerRead.quiet)) && (

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 [P2] Transfer focus before quieting a failed profile retry

A keyboard user can focus “Retry profile” after an empty profile result or a non-transient failure. If that retry then fails transiently, the rejection handler clears retrying and shown becomes loading; with no owner error, this condition removes the focused button. The success handler transfers focus to the stable region, but the transient-failure handler does not, leaving focus on the document body even when a later automatic retry succeeds. Preserve the design guide’s focused-retry contract by handing focus to the region before hiding this control (without stealing focus if the person already moved), and cover this failure-to-recovery transition.

// Batches that landed before the failure still apply.
values = merge(ids.slice(0, offset));
status = "error";
transient = isTransientFailure(error);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 [P2] Keep permanent activity HTTP errors out of indefinite recovery

The browser activity transport throws httpReadError(result.status) (transport.ts:1295), which maps non-authentication 4xx such as 400/404/422 to ReadError("unavailable"). isTransientFailure returns true for all such errors regardless of status. With the newly registered activity source, a rejected or unsupported activity request now retries indefinitely, hides the activity warning in ChannelInventory, and eventually shows a misleading connection notice, even while other relay reads work. Honor the promised non-transient-4xx boundary when setting this recovery flag; retain retries for 408/429/5xx and transport outages. This does not require changing the existing bounded edge-error retry policy used by retryRead.

kazemek21 added a commit that referenced this pull request Nov 13, 2026
## TL;DR

Let the optional Undo from `markCapturedMessagesRead` restore ordinary
unread messages, not just saved reminders. Restore only that action’s
local receipts and cleared visit overlays, without rewinding synced
channel or thread prefixes.

Full captured inverses carry `undoScope: "captured-action"` alongside
`undo`. Untagged older handles may restore saved reminders only, so
callers can check coverage without host-version allowlists.

The handle expires after any later durable journal write or loss of
captured access/lifecycle evidence. This conservative boundary preserves
newer reads and manual unread intent, including writes that leave the
journal revision unchanged. Failed inverse transactions remain
retryable; repeat Undo rejects.

Synthetic regressions cover exact IDs across channels, already-read and
own messages, forced overlays, later and overlapping actions, storage
failure, and access/deletion/visit/session changes. They also verify
that restored ordinary unread can auto-read normally rather than
becoming a sticky manual reminder. Deterministic commit-return cases
failed before the fix and pass afterward: a change to one captured row
no longer suppresses a valid overlay on another. No browser journeys
were added or removed. Native app and live-relay behavior have not been
exercised.

Related work touches the same owner or documentation:
[#3340](#3340),
[#3181](#3181),
[#3009](#3009), and
[#3434](#3434). This change does
not alter their divider, disconnect, row-receipt, or layout behavior.

Generated with Codex (gpt-6.1-sol) on behalf of kylez.

Signed-off-by: Kyle Zemek <[email protected]>
Co-authored-by: Codex <[email protected]>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants