Repository navigation
Conversation
Expose signed, authenticated relay scheduling through the native and browser host boundaries. Preserve exact message bindings and stable retries within the active session. Co-authored-by: Codex <[email protected]> Signed-off-by: sqnico <[email protected]>
Schedule from the composer and manage the queue in Drafts & sent. Keep searches scoped to each tab and preserve local drafts through failures and navigation. Co-authored-by: Codex <[email protected]> Signed-off-by: sqnico <[email protected]>
Refresh remote schedules when management opens and preserve mention recipients through safe editing. Reuse uncertain send-now requests and renew them after a definite rejection. Co-authored-by: Codex <[email protected]> Signed-off-by: sqnico <[email protected]>
Preserve composer chips when scheduled delivery cannot encode their metadata. Keep the draft intact after refusal and verify that an ordinary send still signs the chips. Co-authored-by: Codex <[email protected]> Signed-off-by: sqnico <[email protected]>
aa81f6c to
7c1b002
Compare
|
Still working through the community contribution model for UX changes so can't provide a review timeline as of now. Additional context can be found here: |
joahg
left a comment
There was a problem hiding this comment.
🤖 Comment only. This account isn't approving UI changes, so a design or core-team reviewer is needed. The maintainer note on this PR, that the UX contribution model is still being worked out, also applies. No P0/P1 found; worst is P2.
FOUNDATION: src/features/relay/session.ts has the marker ("One relay session owns reads, local intent, delivery and shared views"). I'm asking a maintainer to sign off on that file. The change moves the createMessages(...) call into messageCommands ahead of the session object and adds scheduledMessages. I checked that every binding it captures is declared above the new position, so there's no TDZ risk.
Relay without scheduling: Solid. buzz-scheduled-messages-v1 is read from NIP-11 supported_extensions in both the native and broker transports, matching what block/buzz#9335 advertises. When it's absent, session.scheduledMessages is undefined, so the composer shows the plain Send button and there's no Scheduled tab. The Rust allowlist only adds POST /scheduled-messages. Support is read once at connect, so a relay that toggles the flag needs a reconnect. P3.
Signing at schedule time: Yes. The client signs the exact kind-9 event with created_at = sendAt (future) and the relay stores it. Content and the p, e, imeta and emoji tags are therefore frozen. The client checks write access only at schedule time. Per the relay PR, membership and ingest checks re-run at delivery, so a removed member's message should land as failed rather than post. A late delivery keeps its past timestamp. Edits re-sign client-side, so mention recipients can't change (the UI makes those read-only) and a text edit can leave emoji tags stale. P3.
Edit and cancel:
- Revision checks, send-now re-signing on a 400, and save-to-drafts (local save first, then cancel, with a clear message if cancel is unconfirmed) are handled well.
- Editing text on a row whose time has passed fails with "Choose a future time within 120 days." (inline). P2.
- A lost create response followed by an identical new schedule reuses the old UUID and dedupes. Rare, P3.
Timezone and DST: All local-time via Date. parseScheduleTime round-trips, so a nonexistent spring-forward time is rejected rather than shifted, but the message is the generic future-time error. The ambiguous fall-back hour silently picks the first occurrence. Tests only use Nov 10, with no DST fixtures. P3.
Other: The list verifies signatures and viewer ownership, renders content and error as text, and caps responses. The client's 356 matches the relay's 256+100. The relay's P2s (bad row aborting the sweep, internal errors retrying forever, rows kept forever) are relay-side. The client surfaces failed rows but gives no per-row reason beyond relay text. The Drafts & sent plugin is opt-in, so someone can schedule a message without a way to manage it. At 3.8k lines across 38 files, the scope checkpoint and diff budget in AGENTS.md may warrant splitting service and host from UI.
CI is green apart from the skipped Windows job. I ran nothing locally. Not reviewed: the Rust native signing path beyond the allowlist, browser-host broker tests, CSS and accessibility, GIF and chat-card handling in the scheduled path, and the full composer diff beyond the send path.
| if (connected && !liveWasConnected) recovery?.retryNow(liveRecovery); | ||
| liveWasConnected = connected; | ||
| }); | ||
| const messageCommands = createMessages( |
There was a problem hiding this comment.
🤖 FOUNDATION file (session.ts, line 3): needs a maintainer's sign-off. The change moves createMessages(...) ahead of session and adds scheduledMessages; I found no TDZ problem.
| ); | ||
| // Preserve attachment and thread bindings. Mentioned messages may only | ||
| // change time here; the full composer owns recipient editing. | ||
| const event = await signed(input, { |
There was a problem hiding this comment.
🤖 Editing text on a row whose time has passed (failed, overdue, or due while the dialog is open) fails here: signed() requires sendAt > now, so the user sees "Choose a future time within 120 days." for a text edit. Consider a clearer message or prompting for a new time.
| type Template = Pick<EventTemplate, "kind" | "content" | "tags">; | ||
| const uuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/; | ||
|
|
||
| class ScheduledRequestError extends Error { |
There was a problem hiding this comment.
🤖 Every non-409 status (400, 403, 404, 5xx) becomes "Could not reach scheduled delivery", so someone who lost access can't tell that from an outage. Consider mapping 4xx to the relay's reason.
…es (#3442) ## Why Two browser journeys fail intermittently in CI for the same underlying reason. Both jump the page clock far enough to expire the live stream's heartbeat, and the stream restart that follows was not accounted for. | Test | Sightings | |---|---| | `agent-outcomes.spec.mjs`, "a remote failure shares its expandable history and preserves its message anchor" | WebKit 1/18, Browser journeys: [10 Nov, main](https://github.com/block/buzz-app/actions/runs/42923245792/job/130206998334), [10 Nov, PR](https://github.com/block/buzz-app/actions/runs/42920092542/job/130193154955), [11 Nov, PR](https://github.com/block/buzz-app/actions/runs/43110752944/job/130826317616), [11 Nov, main](https://github.com/block/buzz-app/actions/runs/42952461094/job/130300041605) | | `relay-recovery.spec.mjs`, "relay reads recover automatically after the network returns" | WebKit 5/18, Browser journeys: [10 Nov, PR, passed on re-run](https://github.com/block/buzz-app/actions/runs/42918607668/job/130187892464) | ## Root cause The live broker stream arms a 45 s heartbeat on the page clock. The broker's 15 s keepalive runs on real time. A page-clock jump longer than 45 s therefore always expires the heartbeat, and the page aborts and restarts its stream. `presence.spec.mjs` already records this hazard. **`agent-outcomes`** pauses the page clock, freezes wall time, and runs 120 s of browser time. Two failures follow from the restart: - In three of the four jobs the test published its signed replies before the restarted stream had re-issued a channel REQ. The policy relay rejected the publish with "event must traverse an explicit production channel REQ". In each trace the publish came about 0.9 s after the clock resumed, and the new stream opened 0.2 to 0.4 s later. - In the fourth job (11 Nov, main) the 120 s jump took 85.6 s of real time instead of the usual 4 to 12 s. The still-frozen wall clock drifted more than 60 s from the broker, so read-state signing answered 400. **`relay-recovery`** fast-forwards the page clock 30 s twice. The second jump expires the heartbeat. In the same jump, session recovery's scheduled retry posts `/stream-retry` with the old stream id. The broker has already handled the stream close and answers 404 "Live stream no longer available". The app drops the error for a superseded generation, but the browser still logs "Failed to load resource … 404". The fixture only excused retired-stream 404s for `/stream-observer` and `/stream-priority`, so the unexplained-console-error check failed the test. The trace shows the fast-forward, the 404 ten milliseconds later, and the new stream 0.7 s after that. Neither is a product defect. The restart only happens under a fake-clock jump, and the app already ignores the late control response. ## Fix - **`agent-outcomes.spec.mjs`**: re-anchor the page wall clock to real time right after resuming, so signed read-state events follow the broker's clock again. Then poll for the primary/alpha route before publishing, mirroring the readiness wait the test already does at startup. - **`fixture.mjs`**: widen the retired-stream 404 match to the six broker control routes that answer 404 once a stream is retired: observer, priority, retry, interests, artifact-sync and artifact-sync-close. The exact `retiredStreams.has(streamId)` guard stays, so a 404 for a live or unknown stream still fails. Presence controls answer 200 for a missing stream and stay out of the list. No timeouts, retries or assertions were relaxed. No browser cases were added or removed. ## Evidence Local runs on Linux, WebKit, `bin/pnpm test:browser:ci --project webkit --no-deps --workers=1 --repeat-each=8 --reporter=list`. | Scope | Result | |---|---| | `agent-outcomes.spec.mjs -g "a remote failure shares"` | 8/8 passed, 17.8 to 20.2 s each | | `relay-recovery.spec.mjs relay-refusal.spec.mjs -g "relay reads recover automatically\|an upload is the first request"` | 16/16 passed | ## Not reproduced, not run - The late `/stream-retry` 404 did not reproduce locally. No retired controls were recorded in the local evidence, so the widened fixture branch is only exercised under CI timing. The Flake check lane on this PR is the real test of it. - The unchanged `agent-outcomes` test also passed 8/8 locally, with two or three stream restarts during each jump. The local run guards against regression rather than reproducing the CI ordering. - The read-state re-anchor addresses a failure seen once, when the jump ran unusually slowly. That slowness was not reproduced. - Chromium was not run for these two changes. - No human has tested this yet, so `buzz-review-completed` is not set. ## Overlap with open PRs Several open PRs also edit `tests/browser/fixture.mjs` (for example #3332, #3239, #3186 and #3093). None touches the retired-stream match, and a trial merge of each against this change is clean. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Signed-off-by: mtoohey <[email protected]> Signed-off-by: Matt Toohey <[email protected]> Co-authored-by: Claude Fable 5.1 <[email protected]>
Why
People need to write a message now and deliver it later, including while Buzz is closed. The modern app has no composer scheduling flow or scheduled queue in Drafts & sent.
What
How
The existing message builder prepares formatting, mentions, uploaded attachments, and thread references. The host signs the exact future event and uses one fixed authenticated relay endpoint. A session-owned service verifies queued signatures and preserves revisions and retry IDs.
The sidebar and page share one queue read, subscription, and timer. Scheduled revalidates on entry and shares a jittered 15-minute backstop, including when the queue is empty. Overdue messages and failed reads retry with bounded backoff. The UI reuses Dialog, Calendar, Combobox, Menu, SearchField, and the split-button recipe.
The first commit adds the service and host boundary; the second adds the composer and management UI. No standalone scheduling plugin is needed.
Risk and limitations
Testing
The running modified macOS app was exercised against an isolated local HTTPS relay during UI iteration. The feature flow was checked in Chromium and WebKit. The branch is rebased onto main at
df88b83, retaining signed composer-chip metadata, draft diagnostics, and annotators. The chip-send regression now also covers a refused schedule followed by an ordinary send; the original signing assertions are retained. The author confirmed successful testing on November 10, 2026, before the review fixes below. Those fixes need a new human acceptance pass.pnpm exec playwright test --config tests/browser/playwright.config.mjs scheduled-messages.spec.mjs drafts-in-progress.spec.mjs --project chromium --project webkit --no-deps: representative flows pass in both engines, including refusal/retry, draft preservation/reload, cross-year layout at 100%/150%, Finnish time selection, queue edits, rescheduling, search, and save-to-drafts. Later focused reruns checked the locale fix and visible menu capture.BUZZ_SCHEDULED_TEST_RELAY=<isolated loopback relay> pnpm exec vitest run src/features/relay/scheduled-messages.live.test.ts: passed with verified local HTTPS; delivery completed after the scheduling session closed.An existing team-mentions status test now controls its directory-search debounce instead of racing a real timer under suite load. Assertions and timeouts are unchanged.
Focused coverage checks signature/owner boundaries, stable retries, stale revisions, newer composer drafts, delayed queue refresh, tab isolation, and refusal of unsafe draft conversion. All mandatory pre-commit and pre-push checks passed without bypasses, including the related test suite, design guards, and native Clippy.
Browser cases added: 5 scenarios per engine. They prove real picker focus/input behavior, rendered geometry, and composer/page wiring. State matrices stay in unit tests. A locale parsing probe reproduced the English-parser failure; canonical-time picker selections now pass in both engines; existing browser cases were retained.
Review fixes: the signed HTTPS relay test now also passes exact create retries after delivery and send-now retries without a second revision. Independent review of the fixes found no remaining blockers. The existing 10 browser checks passed in Chromium and WebKit.
Post-rebase validation passed: mandatory TypeScript/related-unit, design-system, and native Clippy push gates; the existing scheduling/draft journeys also passed all 10 checks across Chromium and WebKit.
Screenshots use fictional local fixtures and the actual shared UI components.
Schedule a message
Manage scheduled messages
Bigger picture
Companion relay: block/buzz#9335. Deploy its migration and reservation-aware probes with scheduling off before enabling delivery. This PR uses a branch pushed directly to the upstream repository. Human acceptance of the review fixes and current hosted CI remain pending.
Replaces the fork-based app PR #3207. Review fixes address queue revalidation and safe mention editing. The original review discussion remains available there.
Generated with Codex