Repository navigation
feat(relay): add incremental artifact reads - #8523
Conversation
Signed-off-by: Daniel Rouleau <drouleau@squareup.com>
Signed-off-by: Daniel Rouleau <drouleau@squareup.com>
…pshots * origin/main: feat(relay): add announce channels with a join_role setting (#8143) Signed-off-by: Daniel Rouleau <drouleau@squareup.com>
Signed-off-by: Daniel Rouleau <drouleau@squareup.com>
Signed-off-by: Daniel Rouleau <drouleau@squareup.com>
Signed-off-by: Daniel Rouleau <drouleau@squareup.com>
Signed-off-by: Daniel Rouleau <drouleau@squareup.com>
…pshots * origin/main: fix(db): bound every statement and the #p mention scan (#8461) Signed-off-by: Daniel Rouleau <drouleau@squareup.com>
🔐 Codex Security Review
|
wesbillman
left a comment
There was a problem hiding this comment.
🤖 Star Lord’s automated source review via Wes’s account.
One P2 correctness finding is inline: a concurrent first artifact can escape the conditional policy reset when its channel becomes private or is deleted. No code changes were applied.
Reviewed head 6a52195bd9a387e4871e3689b5c942ae42a5d597 against base/merge-base 2f2f6e2774fb0899d2a927f5e5b1987603765f31, with independent Rocket (systems/database), Mantis (correctness/security/concurrency), and Groot (protocol/user flows) lanes plus my integration.
Source-only review; no tests, builds, SQL or app execution. A single CI snapshot showed 50 successful, 28 skipped and 1 in-progress checks. Production query costs, migrations/rollback, client recovery and the documented 120-second stamp-to-visibility assumption remain unverified. Matching TTL artifacts intentionally return 422; clients must account for that permanent-channel-only scope.
Optional documentation clarification: spell out applying 0068 after its index prebuild, then 0069, before building the 0070 indexes; the current runbook jumps from the 0068 prebuild to 0069. Public PR description inspected; no attached images or actionable disclosure found.
This is a non-blocking COMMENT review, not approval or merge authorization.
Signed-off-by: Daniel Rouleau <drouleau@squareup.com>
…pshots * origin/main: fix(db): keep pgschema re-apply from dropping runtime partitions (#8487) Signed-off-by: Daniel Rouleau <drouleau@squareup.com>
Signed-off-by: Daniel Rouleau <drouleau@squareup.com>
wesbillman
left a comment
There was a problem hiding this comment.
🤖 Star Lord’s automated source follow-up via Wes’s account.
The prior P2 is fixed: actual visibility/TTL changes and successful channel deletion now advance the policy stamp even when the channel has no artifacts. The first-artifact interleaving test covers both transitions, and the deployment instructions now include 0068 application and migration-ledger recording.
One P2 remains inline, independently confirmed from the new security-review evidence: a continuation can expose an inaccessible destination revision ID. This is separate from the repaired policy-stamp race.
Reviewed head 357da40b3941efd083ea053fab56dbef3cce619c against base/merge-base 347ce88d5dd8bc631c073f1947fea6b75d5a43c0, with independent Rocket (systems/database), Mantis (correctness/security), and Groot (protocol/user flows) lanes plus my integration. Range-diff preserves all six previously reviewed feature patches; this pass covers the follow-up changes and the newly evidenced cursor issue.
Source only: no tests, builds, SQL or apps executed. One CI snapshot showed 29 successful, 21 skipped and 5 in-progress checks. Runtime interleavings, rollout/rollback, reset costs, client recovery and the 120-second visibility assumption remain unverified. Public description inspected; no attached images or actionable disclosure there. Non-blocking COMMENT review; no approval or merge authorization.
Signed-off-by: Daniel Rouleau <drouleau@squareup.com>
wesbillman
left a comment
There was a problem hiding this comment.
🤖 Star Lord’s automated source follow-up via Wes’s account.
The prior P2 cursor disclosure is fixed. Move-source continuation positions now contain artifact IDs instead of inaccessible destination revision IDs; DISTINCT preserves unique equal-timestamp positions. The current-ID read retains the caller’s query predicates and intersects them with the requested artifact IDs.
No new actionable findings in the follow-up from 357da40b3941efd083ea053fab56dbef3cce619c to reviewed head 4019c1df8d266701de685f9c714f747c1781967a, against base/merge-base 347ce88d5dd8bc631c073f1947fea6b75d5a43c0. Rocket, Mantis, and Groot independently reviewed systems/pagination, correctness/security, and product/protocol behavior; Star Lord integrated the results. The public description and changed commit/source material had no actionable disclosure finding; the description contains no attached images.
Source only: no tests, SQL, builds, or app workflows were executed. The new tests exercise the production HTTP router with fixture identity headers and direct database ingestion; this review does not establish signed-HTTP authentication coverage. CI snapshot: 43 successful checks, 28 skipped, 3 still running. Query-plan cost of DISTINCT, production latency/concurrency, client recovery and native accessibility/focus behavior remain unverified. The documented disabled-by-default rollout gate and 120-second overlap assumption remain applicable. This COMMENT is not approval or merge authorization.
Signed-off-by: Daniel Rouleau <drouleau@squareup.com>
Signed-off-by: Daniel Rouleau <drouleau@squareup.com>
Signed-off-by: Daniel Rouleau <drouleau@squareup.com>
ngoodger-block
left a comment
There was a problem hiding this comment.
🤖 Reviewed the sync core (timestamps, paging, move/grant/removal paths, lock ordering) and it looks sound; two issues inline. One operational risk: every channel delete (unconditionally) or visibility/unlisted/TTL change resets every client in the community to a full re-bootstrap, and with the per-process Semaphore(4) + try_acquire the resulting burst mostly gets a 503 with no Retry-After. Worth adding client backoff or a Retry-After before fleet enablement.
Signed-off-by: Daniel Rouleau <drouleau@squareup.com>
ngoodger-block
left a comment
There was a problem hiding this comment.
🤖 Both inline comments are fixed in f3d6ac8 (TTL channels are excluded from candidates; #h now limits candidates before pagination, and the initial read skips deleted heads). Two non-blocking notes: on populated databases, 0076/0078 will fail unless the indexes are built concurrently before deploy (relevant if BUZZ_AUTO_MIGRATE is on), and before turning on BUZZ_ARTIFACT_INCREMENTAL_ENABLED across the fleet, please add jitter to the client's fixed 60s capacity backoff (or have the relay send Retry-After) so community-wide resets don't retry in lockstep against Semaphore(4).
#8523 added Incremental reads with a link to Sync. Add the reverse link, and state the difference: incremental reads do not prove a complete set. Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> Signed-off-by: Logan Johnson <loganj@squareup.com>
…vent-guards * origin/main: (29 commits) Restyle community invitations (#8728) feat(relay): add incremental artifact reads (#8523) fix(mobile): show agent DM typing and balance reply bubble padding (#8731) perf(relay): route author- and #p-scoped global subscriptions by selector (#8666) fix(desktop): fix the product races behind four E2E flakes (#8680) test(scripts): stop file-size fixtures leaking git repos and fsmonitor daemons (#8727) test(mobile): keep late callbacks and fixed sleeps from failing tests (#8678) fix(mobile): resolve iOS pushes on a fresh connection and retry a stale one (#8722) ci(loadgen): publish buzz-loadgen as ghcr.io/block/buzz-loadgen (#8717) test(relay): fix main's build after the test-state helper rename (#8718) fix(db): keep lease releases durable and deflake PostgreSQL lane tests (#8676) test: dedupe collect_until_eose by event id and uncap two shell tests (#8677) ci: cache the Tauri apt .debs and fit a cold Windows Rust run in its cap (#8679) test(desktop): gate flaky E2E specs on the state they assume (#8681) test(desktop): settle composer edits and press hover-only row actions by keyboard (#8682) test(desktop): measure inline video controls against their player (#8683) relay: open the search pool lazily so a dead reader can't block boot (#8662) fix(mobile): address #8650 second-pass review (#8691) Revert "fix(mobile): push every message in your own Solo chats (#8615)" (#8690) fix: don't notify workflow owners through their attribution tag (#8694) ... Signed-off-by: Ravneet Arora <rarora@squareup.com>
🤖 ## Summary Clients that show a list of artifacts (tasks, config entries and similar records) had no way to get the complete list and then stay current. They could only page through query results, and a page is not proof that the list is complete. This PR adds **sync**. A client sends one subscription and gets every matching artifact as one consistent snapshot, then a signal that the set is complete. After that it gets the current version of each artifact that changes. When the relay cannot guarantee an exact copy, it closes the subscription with a `resync` reason, so the client never misses a change without knowing it. It also adds a **head lookup**. It returns the current version of named artifacts, deleted ones included, so that "create or restore" flows can retry safely. This implements the Sync section of NIP-AR (the artifact protocol spec), which is the first commit of this PR. ## Details - **Request.** A normal `REQ` whose only filter has `"artifact": "current"` and at least one channel in `#h`. This is the same current-state filter as HTTP `/query`; on a `REQ` it becomes a sync, because a `REQ` already means "stored set, `EOSE`, then live". The client does not page: the relay streams the whole set until `EOSE`, so `limit` and `offset` are refused. Other artifact views (`history`, `head`) stay HTTP-only, as on main. Other clients and other `REQ` shapes do not change. - **Initial set.** The relay first registers for live changes and waits until Redis confirms that registration. Then it reads every matching current version in pages ordered by artifact ID, all inside one read-only `REPEATABLE READ` transaction. So all pages come from the same database snapshot. `EOSE` comes only after a complete read. A failed read, an unreadable channel, or a read that takes too long ends with `CLOSED` and no `EOSE`. - **Live changes.** Fan-out never sends the published event to a sync. It only records which artifact changed. The sync then reads that artifact's current version and sends it, one artifact at a time. So a client never gets an older version after a newer one. Changes that arrive during the initial read are held and sent after `EOSE`. Changes are signalled per channel, so a sync skips an artifact whose `type` or `d` does not match its filter: those never change, so it was never in the set. A board that syncs three record types in the same channels no longer gets each change three times. - **When the relay sends `resync`.** The Redis link drops, reconnects, or skips a message it cannot decode. Local fan-out lags. The connection's send queue is full. A moderator redacts an event in the channel. The reader's channel access changes (remove, leave, metadata edit, channel delete). An access change ends the whole sync, also when the sync names other channels. A sync cannot drop one channel and still claim an exact set. - **Publish retry.** When a Redis publish fails for an event kind that syncs use, the relay retries it in the background until it succeeds. - **Relay info (NIP-11).** The relay now lists `sync` (`max_events: 4096`), adds the `head` mode, and sets `websocket_query_extensions: true`. - **No migration.** Sync reads the existing `artifact_heads` table. - **Next to incremental reads (#8523).** #8523 added a separate HTTP protocol, `artifacts.incremental` / `"artifact":"incremental"`. It keeps a saved store up to date after a disconnect, but it does not prove a complete set. The two protocols use different NIP-11 keys and filter values, so they do not collide. They share one copy of the current-state access rule: `artifact_query::predicates`, which #8523 reads through, now applies the view join and calls `push_scope`, and the sync snapshot and head lookup call `push_scope` too. - **A failed access lookup in fan-out resyncs.** The access gate fails closed when a visibility or membership lookup fails. For a sync, that would drop a change with no signal, so the gate also reports those recipients and each one gets `resync: access check failed`. ### Known cost To find out which redactions remove an artifact that a sync covers, the relay would need an extra database read. So every redaction in a synced channel resyncs that channel's syncs, and so does every channel metadata edit. Each resync reads the set again, up to 4,096 artifacts. This is correct, but it adds load to channels where people often delete chat messages. We can narrow it later if it becomes a problem. ### Related issue None found. The design was reviewed as NIP-AR Sync r28. ### Testing - New Postgres tests: keyset pages see one snapshot, and an artifact created after the snapshot opens is in no page; the head view includes deletions; a private or unknown channel is refused. - New relay end-to-end tests on real Postgres and Redis, using the real `REQ` handler, ingest and fan-out: initial set in order, then `EOSE`, then current versions for an update and a delete; a chat message causes no frame; `CLOSE` stops the sync. Other tests: removing the reader from one channel resyncs the whole sync; an unreadable or unknown channel gets `CLOSED restricted` with no `EOSE`; a node-wide loss resyncs. - Unit tests for the parser, request routing, fan-out diversion, the held-change limit, and access-change signals that must arrive before the access filter. - Lifecycle tests from an adversarial review: every frame is checked against the subscription owner, also when a `CLOSE` or a replacing `REQ` arrives while the frame waits for queue room or while a live head read is in progress. A resync recorded while `EOSE` waits ends the sync with no `EOSE`. The held list refuses a change before it stores it (4096 entries, 4 MiB of held removals). - End to end: a relay container built from this branch, with the buzz-app #1523 browser build and the Work plugin. An edit from another client showed in the open board 4 to 25 ms after the write returned, with no refresh. - `cargo clippy --all-targets -D warnings` is clean for the four changed crates. All `buzz-relay` lib tests pass except `api::mesh_demo::tests::demo_join_forwarded_arm_round_trips_echo`. That test returns 504 on this machine. It is a mesh networking test, and this PR does not touch that code. --------- Signed-off-by: Logan Johnson <loganj@squareup.com> Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…e-agent-audience * origin/main: test(mobile): check the title fade's animation, wait 10 s for the alert (#8741) test(db): drop scratch databases and roles even when a test fails (#8747) test: stop desktop, buzz-agent and mobile tests leaking temp files (#8748) test(desktop): keep unit tests away from the real ~/.buzz nest (#8749) feat(mobile): show conversation activity by the composer, as buzz-app does (#8765) feat(mobile): show your agents working in a thread on its root message (#8754) Clarify mobile community onboarding and saved identity selection (#8729) Add Buzz background and flying bees to invitations (#8763) Polish mobile navigation and restore community switch animation (#8732) feat(loadgen): add a hot-thread stressor and teardown (#8746) feat(relay): NIP-AR sync subscriptions (#8522) fix(mobile): retain invite keys and reject incomplete onboarding (#8753) Restyle community invitations (#8728) feat(relay): add incremental artifact reads (#8523) fix(mobile): show agent DM typing and balance reply bubble padding (#8731) Signed-off-by: John Tennant <jtennant@squareup.com>
Units 6-9 of the 23-30 Oct desktop Playwright flake audit. All are test-only. origin/main (1eb67bb, now including #8680) hasn't touched these specs since fae3f03. No open PR touches the tests changed here: #6134 (conflicting since 10-19) only swaps page.goto calls in three of these files, #8187's hunk starts just after workflow-local-controls:151, #5778 (draft, conflicting) creates projects-v3-screenshots from an older base, and #8622, #7452, #7812, #8244 and #3342 edit other tests in the same files. Unit 6: measuring before an animation finishes onboarding.spec.ts:1487, "hosted community address line stays within the card for a long name" Run: #8715 (Integration shard 1), which doesn't touch onboarding. Signature: the centring check at :1545 expects <= 2 and receives 5.13. Cause: "Connect and continue" ends in showPage("owned") (WelcomeSetup.tsx:323), which mounts an OnboardingSlideTransition. Its content slides in from translateX(48px) over 650 ms (components.css:521, :676). fill() doesn't wait for stability, and the surface, input and suffix boxes come from three boundingBox round trips, so they can be read at different points of the slide. Change: waitForAnimations(page) after the fill, before measuring. onboarding.spec.ts:2289, "connected first-community profile keeps navigation inside the card and balances the avatar editor" Runs: main 40746677244 and 40925172215, and #8666 (Integration shard 1). Signature: the spacing check at :2485 expects <= 4 and receives 4.255. Cause: the avatar dialog opens with MODAL_CONTENT_MOTION_CLASS, a 200 ms zoom-in-95 (modalMotion.ts:5), and has a 250 ms height transition. dialogBox is read in one round trip and the preview and editor boxes in a later one, so a scale change between them skews the gap comparison. Change: waitForAnimations(page) once the dialog is visible. projects-v3-screenshots.spec.ts:280, "projects v3 workspace screenshot states", failing in the expectInsetSection call at :893 Runs: main 39643531998, 40759209995 and 40969909906 (Smoke shard 8), each passing on retry. Signature: the header's right inset at :326 expects 16 and receives 8. Four more runs fail the menu edge check at :328 with fractional values (expected 984, received 975.6875): 39622053330, 39641311877, 40281448810 and 40856574572. Cause: switching the context rail back from chat to repository narrows it from the thread panel's 380 px to the repository's 280 px (useProjectPanelWidths.ts:5, :17), animated by transition-[width] duration-200 (ProjectContextRail.tsx:31). The workspace widens with it, and expectInsetSection reads the workspace, header and menu boxes in three separate round trips. The fractional widths are mid-transition values. Change: waitForAnimations(page) before that call, the only one that follows a rail width change. Unit 7: fill() racing a caret move the app makes a frame later mention-recipients.spec.ts:376, "editing to a longer typed member drops the original shorter reference", and :97, "ambiguous added mention blocks editing before clearing the draft" Runs: main 40969965981 (Smoke shard 5, a run with every earlier fix) for :376. 40969858676 for :97, before the fixes. Signature: the edit_message poll at :428 receives null. The error context shows the edited message as "@scout hello @scout Jones hello": fill() appended. Cause: entering edit mode sets the content, then focuses its end in a requestAnimationFrame (MessageComposer.tsx:413). toHaveText passes as soon as the content lands, before that frame. If the frame lands between fill()'s select-all and its typing, the selection collapses and the text is appended. #8682's replaceComposerText handles this, but these two call sites (:115 and :403) were missed. Change: replaceComposerText at both. workflow-local-controls.spec.ts:151, "inserts template variables with keyboard control and restores the caret" Runs: main 40926393897 and 40957616146 (before the fixes), and #8239, which changes no desktop files. Signature: :194 expects "Keep {{auth" and receives "Hello {{trigger.text}}Keep {{auth". Cause: selectVariable restores the caret in a requestAnimationFrame (WorkflowTemplateTextarea.tsx:66). Setting the controlled value has already put the caret at the end, so the selectionStart poll passes before that frame runs. The frame then collapses fill()'s select-all. Change: after the poll, wait two animation frames, then fill. This is a frame barrier, not a timer. The app queued its frame before the evaluate, so it runs first. Unit 8: clicks that hit the wrong element profile.spec.ts:1128, "renders agent profile ingress subviews from the Playwright mock bridge", failing at :1543 Run: #8523, which changes no desktop files. Its base (ef4130d) already includes #8681. Signature: archiveAgentRow.click() times out because three stacked Sonner success toasts (data-expanded="true") intercept the pointer. Cause: the test's Stop and Start steps raise success toasts (useAgentLifecycleActions.ts:51, :61). Sonner stacks them bottom right, over the profile panel's action rows, and pauses their dismissal while the pointer is over them. #8681 fixed this test's motion window, and fixed the toast problem only in agent-availability:602. Change: press the five action rows (:1528-:1547) through a local pressActionRow helper: toBeEnabled, focus, Enter, as agent-availability.spec.ts:744-745 does. toBeEnabled keeps the wait click() gave, since these rows are disabled while a delete or archive is pending. config-bridge-screenshots.spec.ts:314, "07 — profile side panel — Configuration section" Runs: main 40967147375 (before the fixes), and #8235, which changes no desktop files. Signature: "Model" isn't visible within 10 s at :352. The error context shows nadia's panel ("Not managed on this device"), not Charlie's. Cause: the spec clicked .last() of the rows with a [data-testid^="message-avatar-"] descendant. That matches only UserAvatar's -image and -fallback children. Radix renders the image only once it loads, and each fallback only after its own 200 ms delayMs timer (UserAvatar.tsx:67, :119). Neither avatar in #agents has a URL, so the matching set fills in row by row as the timers fire. Resolved in between, .last() is nadia's row (seeded at -85 s), just above Charlie's "reporting in." row (-80 s). The note blamed a misplaced click; the code points at locator resolution instead. Change: pick Charlie's row by its text, as profile.spec.ts does for Charlie's other #agents message. Then check the panel's name row reads charlie before opening Runtime, so a wrong panel fails there and says so. Unit 9: the createWorkflow copies (preventive) navigation.spec.ts:153, "direct workflow detail links close back to workflows" Runs: 7 failures on main, the last at 29 Oct 16:12Z, none after #8681. Signature: the Edit workflow dialog has no "Trigger: Message Posted" at :176. Runs: 14 first-attempt failures in Smoke shard 7, all passing on retry; on main 39827234118, 40718018673, 40789486699, 40802051064, 40840949170, 40891643205 and 40909291143. Cause: the same race #8681 fixed in workflows.spec's createWorkflow. Picking a channel opens the trigger pane (WorkflowDialog.tsx:702), and for Message Posted the conditions editor expands "Message text" by default (WorkflowTriggerConditions.tsx:157), rendering an input with that label. After "Send Message", AnimatePresence mode="wait" (WorkflowFormBuilder.tsx:907) keeps the trigger body mounted through its exit and holds the step body back. An unscoped getByLabel("Message text") can fill the trigger's filter. The step then has no text, which yamlToFormState rejects (workflowFormTypes.ts:348), so the Edit dialog opens in YAML mode (WorkflowDialog.tsx:88). That is the signature the earlier audit recorded for navigation:153. Change: wait for the step inspector's "Action" header and "Step details" control, then fill inside the inspector, as workflows.spec does. workflows.spec.ts:716 ("captures the built editor at desktop and narrow widths", fill at :726) and :820 ("pane routes use stable IDs and Form/YAML changes stay synchronized", fill at :831) pick a channel and then add a Send Message step the same way, with no failures in the window. So the wait moves into one addSendMessageStep helper in tests/helpers/workflows.ts, rather than four copies. Both specs' createWorkflow and those two tests call it. A change to the inspector's structure then needs one fix. Left out: workflow-reaction-picker.spec.ts:203-204. The note listed it, but its trigger is Reaction Added, whose condition fields are "Reaction emoji", "Author" and "Message" (workflowConditionExpression.ts:40). No trigger control there is labelled "Message text", so the unscoped fill can only reach the step's textarea. Verification Not run locally: Playwright bogs this machine down, and the Flake Check lane will repeat the touched specs. pnpm typecheck and pnpm check pass. biome's 3 warnings are in untouched files. An ad hoc tsc ran over the eight touched specs, using the desktop tsconfig with only those files, and over HEAD's copies of them. Both gave the same errors: onboarding 16, navigation 7, profile 7, workflow-local-controls 7, projects-v3-screenshots 2, and none in mention-recipients, config-bridge-screenshots or workflows. The messages are identical once line numbers are stripped, and all are pre-existing (no Node types, import.meta.env, MockBridgeOptions drift and the like). Once the helper moved to tests/helpers/workflows.ts, a rerun over navigation, workflows and the helper gave navigation's same 7 and none in the other two. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Matt Toohey <contact@matttoohey.com>
…kspace (#8744) ## Summary Units 6–9 and 14 of the 23–30 Oct desktop Playwright flake audit, in two commits. The specs now settle before they measure or act, and remote-owned-mentions clears its draft reliably. Everything is test-only. This is one of six PRs split from the reviewed `flakey-tests` branch (siblings below). ### Unit 6: measuring before an animation finishes Each spec now calls `waitForAnimations(page)` before measuring. - `onboarding.spec.ts:1487` (#8715, Integration shard 1; the centring check at :1545 receives 5.13): `showPage("owned")` slides content in from `translateX(48px)` over 650 ms. The spec reads its three boxes in separate `boundingBox` round trips, so they can come from different points of the slide. - `onboarding.spec.ts:2289` (main 40746677244 and 40925172215, and #8666; the spacing check at :2485 receives 4.255): the avatar dialog has a 200 ms zoom-in and a 250 ms height transition. - `projects-v3-screenshots.spec.ts:280`, at :893 (main 39643531998, 40759209995 and 40969909906, plus four runs with fractional menu-edge values): the context rail narrows from 380 to 280 px over 200 ms. ### Unit 7: `fill()` racing a caret move the app makes a frame later - `mention-recipients.spec.ts:376` and `:97` (main 40969965981 and 40969858676): edit mode focuses the end of the composer in a `requestAnimationFrame`. If that frame lands mid-`fill()`, the edit is appended instead of replacing the text. **Change:** use #8682's `replaceComposerText` at both call sites. - `workflow-local-controls.spec.ts:151` (main 40926393897 and 40957616146, and #8239): `selectVariable` restores the caret in a `requestAnimationFrame` after the spec's poll has passed. **Change:** wait two animation frames before filling. ### Unit 8: clicks that hit the wrong element - `profile.spec.ts:1128`, at :1543 (#8523): stacked Sonner success toasts intercept the pointer. **Change:** a local `pressActionRow` helper presses the five action rows with `toBeEnabled`, focus and Enter. - `config-bridge-screenshots.spec.ts:314` (main 40967147375, and #8235): `.last()` resolved while avatar fallbacks were still filling in on 200 ms timers, so it opened nadia's panel instead of Charlie's. **Change:** pick Charlie's row by its text, and check the panel's name before opening Runtime. ### Unit 9: the `createWorkflow` copies (preventive) - `navigation.spec.ts:153` (7 failures on main, none since #8681; 14 first-attempt failures in Smoke shard 7): an unscoped `getByLabel("Message text")` can fill the trigger's filter while `AnimatePresence` keeps it mounted. **Change:** wait for the step inspector, then fill inside it, as `workflows.spec` already does. The wait now lives in one `addSendMessageStep` helper in `tests/helpers/workflows.ts`, which `navigation.spec.ts` and `workflows.spec.ts` (`createWorkflow`, :716, :820) all call. - `workflow-reaction-picker.spec.ts:203-204` is left out. Its Reaction Added trigger has no control labelled "Message text". ### Unit 14 (second commit): `remote-owned-mentions.spec.ts:566` - **Run:** main 40998438103 (Smoke shard 9, job 123877347301). It passed on retry and failed 1 of the 6 attempts it got in the window. `sourceRecord()` at :673 returned thread A's draft instead of null. - **Cause:** on ProseMirror, `fill("")` selects through the DOM, then presses Delete. If ProseMirror hasn't seen the selection yet, it blocks the Delete. Navigating to B then saves A's live text as its draft. This is not a product bug: had the editor really been empty, the cleanup would have cleared A's draft. - **Change:** delete the text with one Backspace per character from the end, then wait for the composer to read empty. The guard assertions are unchanged. ### Related issue None found. Sibling #8743 also edits `navigation.spec.ts`, in a separate hunk. The two merge cleanly in either order. No open PR touches the tests changed here. #6134, #8187, #5778, #8622, #7452, #7812, #8244 and #3342 edit other tests in the same files. ### Testing Not run locally, because Playwright bogs this machine down. Flake Check will repeat the touched specs. `pnpm typecheck` and `pnpm check` pass, and biome's 3 warnings are in untouched files. Ad hoc `tsc` runs over the touched specs and the helper found the same pre-existing errors as on their `main` copies. Pushed with `git push --no-verify`, because the pre-push suites fail when the laptop is under load. ### Readiness An agent review of the combined `flakey-tests` branch (b432453af) found no blockers. `buzz-review-completed` is not added, because the human test step (AGENTS.md "Before opening a PR", step 3) hasn't happened yet. ### Siblings - #8739 ci: Hermit fetch deadline and sccache fallback - #8740 test(relay): thread_window aux-page hold and `api::media` Postgres tests - #8741 test(mobile): RunnerTests title fade and alert wait - #8742 fix(desktop): deep-linked message hold (product change) - #8743 test(desktop): four specs racing scroll settles and focus 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Signed-off-by: Matt Toohey <contact@matttoohey.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
## Why Saved task records need updates after restart without a full download on each sync. ## What Keep each local collection current through the relay's incremental reads. Resume from the last saved checkpoint with a 120-second overlap. ## How One window owns each collection. It saves paged changes and current-ID reads in order. Explicit removals preserve saved bodies and preferences. Channel removal commits at most 100 saved records at a time. Failed pages cannot advance progress. Access recovery revalidates saved IDs. Incremental reads use `artifacts.incremental` and `artifact: "incremental"`. The existing WebSocket service keeps `artifacts.sync` for a complete set followed by live updates. The two capabilities work independently. Long retries wait 60–90 seconds so owners do not all retry at the same fixed interval. ## Risk The overlap assumes bounded database delay. Incremental reads cover permanent channels. Native end-to-end sync needs the compatible relay and client together. ## Testing At `09ee7d13d`, native macOS tests use the real Buzz relay. A host collection saves bootstrap data, resumes from its checkpoint after app restart, and reads a later update after a live hint. Channel removal returns an explicit `remove`; the saved body and pinned preference remain, with unavailable status. The relay rejects an invalid initial checkpoint with HTTP 400. The client makes one fresh pass. An injected failure of that fresh pass preserves the prior checkpoint, body, and preference. A real policy change returns HTTP 409 with `policy_changed`. Recovery loads 101 records in pages of 100 and one, then rechecks saved IDs in batches of 100 and one. The two delta reads take 156 ms and 50 ms. All 101 records remain saved, with a checkpoint and no refresh error. This verifies the page boundary; large-scale performance remains unmeasured. Human acceptance passes in the native test harness: Publish test update reaches the saved collection and preserves the pinned preference. The operator supplies the PASS screenshot. The harness uses the real host service and signed reader. It does not establish adoption by a production plugin. Chromium and WebKit also pass the two ownership-transfer cases with real IndexedDB and Web Locks. Command: `bin/pnpm exec playwright test --config tests/browser/playwright.config.mjs tests/browser/artifact-sync.spec.mjs --project chromium --project webkit --no-deps`. buzz-review-completed ## Bigger picture The storage prerequisite, [PR 1501](#1501), is merged into `main`. The relay prerequisite is [PR 8523](block/buzz#8523). The client preserves the WebSocket service from [PR 1523](#1523). Generated with Codex --------- Signed-off-by: Daniel Rouleau <drouleau@squareup.com> Co-authored-by: Codex <noreply@openai.com>
) ## Problem Migrations `0076`, `0078` and `0081` raise an exception on any table with at least one row unless its index was prebuilt with `CREATE INDEX CONCURRENTLY`. A relay that runs migrations at startup (`BUZZ_AUTO_MIGRATE`, on by default in the Helm chart) and has data in those tables therefore crash-loops on upgrade: ``` migration error: while executing migration 81: error returned from database: prebuild idx_nip_fi_profile_heads_names concurrently; see docs/profile-search.md ``` `0081` (#9019) hits every auto-migrating relay that has published NIP-FI verified profiles. `0076`/`0078` (#8523) hit any relay with artifact data or channel members. Each of these tables is small enough on such relays that a plain index build takes milliseconds. The root cause is `docs/database.md` Rule 6. It told authors that an index on a populated table MUST be built concurrently by an operator ahead of time, so migrations were written to refuse populated tables. With auto-migration that refusal is a crash loop, not a safe stop. ## Fix The migrations are already applied on some databases, so they can't be edited: sqlx would reject the changed checksums. Instead, `run_migrations_locked` builds their missing indexes first. For each entry in `INLINE_INDEX_BUILDS` (`0076`, `0078`, `0081`), it runs migrations through the version before it, then builds any missing index. Each build is a plain `CREATE INDEX` in its own transaction, under the same 1s `lock_timeout` and 5s `statement_timeout` the migrations use. That is the bounded inline build `0049` already does: - **Small populated databases** upgrade the same way empty ones do. - **Large databases** still fail closed. Only a lock or statement timeout (`55P03` / `57014`) gets the "prebuild concurrently" hint, pointing at that migration's own doc. Other errors pass through unchanged. The blocked migration is not recorded. - **Prebuilt indexes** are found with `to_regclass` and skipped before `CREATE INDEX`, so deployments that prebuilt concurrently never request the writer-conflicting lock. - **Invalid prebuilt indexes** are left for the migration to reject, as before. `CONCURRENTLY` is not used in this path. The runner holds the migration advisory lock, and another replica blocked on that lock holds a snapshot that a concurrent build would wait on. ### Rules - `docs/database.md` Rule 6: with `BUZZ_AUTO_MIGRATE` on, every migration must apply its own change on a populated database and must never refuse one because it has rows. A new index is built inline under the migration's budgets. Only deployments that apply migrations out of band (auto-migration off) prebuild concurrently. - Root `AGENTS.md` (step 4) and `migrations/AGENTS.md` repeat the never-refuse rule. - `docs/artifact-sync-deployment.md` and `docs/profile-search.md` describe the startup behavior first and the concurrent prebuild as the out-of-band path. ## Tests - `populated_database_auto_migrates_through_every_migration` (Postgres) migrates to `0075`, seeds artifact rows, and checks four phases: 1. **Lock held:** while a writer lock is held, the `0076` build fails closed. 2. **Index prebuilt:** with the `0076` index prebuilt, the build is skipped. 3. **Partial `0078`:** a blocked `0078` build keeps the builds before it committed. 4. **Verified profile seeded:** with a verified profile head inserted before `0081`, a final startup succeeds. It asserts that the applied versions equal every embedded migration and that all six indexes are valid, ready and live. - `frozen_prebuild_guards_have_inline_builds` (no database) scans `migrations/`. It fails if any migration refuses a populated table (`RAISE EXCEPTION 'prebuild …`) without an inline build, and if a new guard is added. - `migration_execution_cannot_bypass_schema_destruction_lock` allows exactly one production `run_to`, inside `run_migrations_locked`. ## What got simpler - Auto-migrating relays no longer need a manual, operator-only prebuild step for small deployments. That restores the chart README's contract that `helm upgrade` is the whole upgrade procedure. - Rule 6 no longer steers authors toward refusing populated tables, and a test enforces that. - The change adds one table-driven step for three frozen migrations and no new configuration. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Signed-off-by: Luke Tornquist <tornquist@squareup.com> Co-authored-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Summary
Buzz clients need to recover task changes after a disconnect. This adds incremental reads of changed artifacts with database modification timestamps and a 120-second overlap. Each result contains the current revision or an explicit removal. A separate read of known IDs confirms accepted edits and restores access state.
Pages use a timestamp and a stable ID to handle equal timestamps. Access grants include older records. Access policy changes require discovery and revalidation of saved IDs. The reader does not load a list of all readable channels. Ordinary artifact writes update their own rows.
Incremental reads and WebSocket Sync
This HTTP protocol maintains an eventually consistent local store. It uses
artifacts.incrementaland"artifact":"incremental". It does not prove a complete set at one database snapshot. Absence never proves deletion or loss of access.WebSocket Sync serves a separate use case: a complete set at
EOSE, followed by live updates. It ownsartifacts.sync. Both protocols can coexist. The second PR to merge must reuse the shared access helper inartifact_query.rswhen it resolves the overlapping edits.The gate is
BUZZ_ARTIFACT_INCREMENTAL_ENABLED. Errors use theincremental_prefix. Unlisted open channels require membership or an explicit#hscope. Listing changes reset incremental progress. Incremental reads exclude TTL artifacts. Known-ID reads returnremovefor them. The#hscope limits candidates before pagination. Initial reads omit deleted heads. Later reads retain deletion removals.Why the new timestamps are needed
created_atcomes from the event signer.received_atrecords event arrival. Neither advances when access changes or an event is redacted. Sync needs database timestamps for those changes:artifact_heads.modified_atartifact_revisions.modified_atchannel_members.sync_modified_atcommunities.artifact_policy_modified_atThe database sets these timestamps with
clock_timestamp(). The 120-second overlap covers the accepted delay between timestamp assignment and commit visibility.BUZZ_ARTIFACT_INCREMENTAL_ENABLEDdefaults to off. Apply migrations0076–0078and upgrade all writers before enablement. The 120-second delay bound is an operating assumption. Deployment steps cover index prebuilds and rollback.Related issue
No duplicate found. Related work: read permissions, statement limits, and atomic channel settings.
Testing
Signed HTTP E2E passes on
f3d6ac852e7ce4e2cc53bd891cef87832a868d72after CI passes. It includes main throughef4130da4. A standalone local relay uses a fresh PostgreSQL 17 database, Redis, and signed NIP-98 HTTP requests. All 78 migrations apply through relay startup. All behavioral writes and reads use HTTP.The tests cover create, edit, delta reads, known-ID recovery, private moves, access grants, removals, rejoins, visibility resets, redaction, and deletion. Unlisted tests cover hidden broad discovery, named scope, known-ID reads, member access, and policy resets. They also cover bootstrap across two pages, overlap replay, and 101 move removals across pages. Decoded cursors contain no private revision IDs. With the incremental gate off,
artifacts.incrementalis absent and a signed incremental request returns503 incremental_unavailable. Unsigned requests are rejected. Signed tests also cover TTL exclusion, current-ID removals for TTL artifacts, moves into TTL channels, deleted initial heads, and scoped pages with exactly 100 and 3 upserts.A separate local scale test on
babcdb766reads 10,050 tasks across 60,002 memberships. It uses seeded fixtures and a second relay state with the same signing key. The initial read takes 8.31 seconds over 101 pages with 4,923-byte events. A read with no changes takes 5.70 milliseconds. The candidate keeps the same database query. These are local measurements, not production limits. Cold pages repeat candidate scans and access checks. Writer load needs validation before fleet enablement.cargo test -p buzz-relay sync_downloads_ten_thousand_tasks_with_sixty_thousand_memberships -- --ignored --nocaptureThe behavioral run uses
cargo build -p buzz-relay --bin buzz-relay, thencargo run -p buzz-relay --example artifact_incremental_final_oct30_f3. The temporary harness drives the built relay and is removed after the run. The worktree is clean at the tested SHA. It does not exercise a native client. Git and S3 behavior are outside this validation.The protocol defines progress, removals, limits, and recovery. Relay verification covers the signed HTTP path. Native client verification belongs to the client PR.
Generated with Codex