Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
f08b1b6
feat: add canvas version history with optimistic concurrency
Aug 25, 2026
27187bb
fix(canvas): enforce head advancement, tighten malformed tags, unclam…
Aug 25, 2026
0c529b0
fix(canvas): bound history limit and centralize v3 writer discipline …
Aug 25, 2026
5158994
fix(canvas): satisfy clippy int_plus_one in SDK test assertion
Aug 25, 2026
7e9c1b9
fix(canvas): validate SDK canvas inputs and harden CLI restore/query …
Aug 25, 2026
9f8b636
feat(desktop): canvas version history, restore, and conflict-checked …
Aug 25, 2026
442608f
fix(canvas): address canvas history review nits
Aug 25, 2026
f6a6837
refactor: move canvas concurrency check client-side, drop relay/DB en…
Aug 25, 2026
b5b825d
docs: describe canvas concurrency check as client-side advisory
Aug 25, 2026
3f43537
fix(canvas): apply writer discipline to CLI set and unify timestamp h…
Aug 25, 2026
5120935
test(buzz-cli): pin canvas set writer discipline at the command seam
Aug 25, 2026
efc06f8
feat(canvas): detect concurrent-write supersession client-side
Aug 26, 2026
8f8de67
test: make canvas skew boundary and CLI supersession tests deterministic
Aug 26, 2026
fb91c04
chore: run buzz-sdk unit tests in the authoritative test gate
Aug 26, 2026
b2759e6
fix(canvas): reset UI on channel switch; keep accepted writes durable…
Aug 27, 2026
55d005e
test(canvas): pin channel-switch reset and durable-verify branch to p…
Aug 27, 2026
a84d4a4
fix(desktop): invalidate canvas caches on superseded write
Aug 27, 2026
04b9a8b
fix(canvas): non-destructive unverified restore, transitive ancestry,…
Aug 27, 2026
3a44a7e
fix(desktop): reset rejected canvas save before new edit session
Aug 28, 2026
50f303b
fix(canvas): pin write-influencing reads to writer; confirm restore
Aug 28, 2026
1fada92
test(canvas): drive restore tests through the confirm dialog
Aug 28, 2026
9b931de
test(canvas): add causal shipping-seam evidence for writer-pin guard
Aug 28, 2026
0022f19
test(canvas): close display-inverse non-causality and CI gap (round 6…
Aug 28, 2026
8ddaeea
fix(canvas): gate ingress on eventId, lower future-skew ceiling to 60…
Aug 29, 2026
3622baf
test(canvas): close three causality gaps from round-7 (sheet wiring, …
Aug 29, 2026
c90fdc6
test(canvas): use interior +600 offset in ingest wiring tests to elim…
Aug 29, 2026
86ee1da
feat(canvas): add DB-level CAS for kind-40100 writes and wire through…
Aug 31, 2026
4198219
fix(canvas): add CAS ingest wiring test, CI steps, and trim event.rs
Aug 31, 2026
c04f54a
fix(canvas): close tombstone false-success, deterministic lock eviden…
Aug 31, 2026
129d032
fix(canvas): deterministic pg_locks waiter oracle and remove soft_del…
Aug 31, 2026
9e86d81
fix(canvas): keep accepted-write notice and cached content on refetch…
Sep 1, 2026
4ede94b
chore(canvas): remove debug call counters from refetch recovery test
Sep 1, 2026
20f9938
fix(relay): use test_support::database_url() in bridge tests after re…
Sep 1, 2026
9235a70
fix(canvas): freeze expectedRevision at restore dialog-open, not conf…
Sep 2, 2026
c77ea9f
fix(canvas): address Carl review 5096510707 and CI repair
Sep 3, 2026
b97197b
fix(canvas): move conflict_relay param doc into function doc block
Sep 3, 2026
c5c08cc
fix(canvas): address Thufir pass-2 findings on restore reconciliation…
Sep 3, 2026
eef6b43
fix(canvas): close three minor post-review cleanups
Sep 3, 2026
5e27e91
fix(canvas): separate persistence evidence from ancestry classificati…
Sep 3, 2026
9daada3
chore(desktop): rebase onto main β€” add react-day-picker, retain diff dep
Sep 3, 2026
d3be6bd
fix(canvas): remove spurious inline function from rebase conflict in …
Sep 3, 2026
a2a138f
test(canvas): cover existence-query safety guarantees in 409 reconcil…
Sep 3, 2026
ca647bd
chore(canvas): merge main into canvas-version-history
Sep 21, 2026
57c4b8b
Merge remote-tracking branch 'origin/main' into duncan/canvas-version…
Sep 21, 2026
83e9a2e
fix(relay): emit 409 for canvas CAS conflicts, not 400
Sep 22, 2026
5147bb3
Merge remote-tracking branch 'origin/main' into duncan/canvas-version…
Sep 22, 2026
b6c3b3e
fix(relay): log real response status in Rejected arm; pin log fidelity
Sep 22, 2026
ac21a64
Merge remote-tracking branch 'origin/main' into duncan/canvas-version…
Sep 22, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions Justfile
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,13 @@ test-unit:
cargo test -p buzz-auth --doc
cargo nextest run -p buzz-voice --lib
cargo nextest run -p buzz-cli
# buzz-sdk builder/validation unit tests: pure event-builder and input
# validation (e.g. the canvas writer-discipline/skew guard and the
# canvas_write_survived predicate), no infra. `--lib` runs all unit
# tests without the rustdoc dependency-resolution flake the full-package
# invocation hits. Enumerated explicitly because nothing in CI runs
# `cargo test --workspace` β€” membership buys clippy/check, not tests.
cargo nextest run -p buzz-sdk --lib
# buzz-acp owns the relay-to-agent trust boundary. Run its tests here so
# forged relay events cannot regain a path into agent routing unnoticed.
cargo nextest run -p buzz-acp
Expand Down
74 changes: 74 additions & 0 deletions PLANS/REPLICA_FULL_READ_ROUTING_DESIGN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
---
title: "Replica Full-Read Routing β€” Caller Classification"
tags: [relay, replica-routing, consistency]
status: active
created: 2026-08-28
---

# Replica Full-Read Routing β€” Caller Classification

`Db::query_events` always reads the **writer** pool. `Db::query_events_routed`
(and its `_bounded` / `count` / feed siblings) opt a read into **replica
routing**: the read may be served from a read replica when
`BUZZ_REPLICA_READ_MAX_AGE_MS` is set, subject to the soundness predicate
`RoutePredicate::for_query` derives from the query shape. The seam fails closed
to the writer at every error and is a genuine no-op until the budget is
configured (`crates/buzz-db/src/lib.rs`).

## Routing rule

**If a read's result influences a write or a permission decision, it reads from
the writer.** A replica can lag behind the caller's own just-committed write; a
read that gates the next write against that lag would make a wrong decision
(e.g. validate a save against a stale head, or miss the caller's own accepted
event during post-write verification and report a false conflict). Every such
read stays on `query_events` / writer.

Display reads β€” a page the user scrolls, a count on a badge, a history list β€”
tolerate bounded staleness and take the routed path.

Adding, removing, or reclassifying a caller **requires updating the table
below**; the `query_events_routed` doc-comment points here.

### Client-carried intent: the `consistency` extension field

Some write-influencing reads are issued by clients (Desktop, CLI) through the
HTTP `/query` bridge, and are **indistinguishable by query shape** from display
reads β€” a kind-40100 `limit:1` read serves both `get_canvas` (display) and a
canvas save's head precondition. The client therefore signals intent with the
`consistency` extension field on the raw filter:

- `"consistency": "strong"` β†’ the bridge serves that filter from the writer
(`query_events`), never a replica.
- absent β†’ the default routed path.
- any other value β†’ `400 Bad Request` (fails loud, never silently degrades).

The field only ever forces the **writer**, which is always the sound direction;
there is deliberately **no** inverse "force replica" value, so it cannot be
sprayed to bypass the replica-staleness guard on reads that should not. Parsed
in `crates/buzz-relay/src/api/bridge.rs` (`extract_consistency`).

## Caller classification table

Rows below are every `query_events_routed` / `query_events_routed_bounded`
call site at head, plus the writer-pinned canvas row this change adds. (The
`count` and feed routed families β€” `count_events_routed`,
`get_events_by_ids_routed`, `query_feed_*_routed`, and the `get_channel_window`
cursor/head reads β€” are all display or bounded-count surfaces on the routed
path; they carry their own soundness notes at their definitions in
`crates/buzz-db/src/lib.rs` and are out of scope for this table.)

| Caller / path label | Pool | Justification |
|---|---|---|
| `bridge_query` (default `/query` filter) | routed | Display reads over the HTTP bridge; bounded staleness acceptable. |
| `bridge_query` + `consistency: strong` | **writer** | Client-declared write-influencing read (canvas save precondition, restore precondition, post-write ancestry verification). |
| `req_historical` (WS REQ historical page) | routed | Display backfill of a subscription; per-row re-filter absorbs a briefly-stale row. |
| `bridge_thread_aux` (`AuxReader::Routed`, thread aux page) | routed | Thread reply hydration; display, post-verified against the fence wall. |
| `bridge_count_fallback` (`query_events_routed_bounded`) | routed (bounded arm) | COUNT fallback that materializes rows; bounded arm only, never covered. |
| `count_req_fallback` (`query_events_routed_bounded`) | routed (bounded arm) | WS COUNT fallback that materializes rows; bounded arm only. |

The **writer** row is the only write-influencing entry; every other caller is a
display or count surface that tolerates bounded staleness. Client canvas reads
that gate a write set `consistency: strong` (Desktop
`desktop/src-tauri/src/commands/canvas.rs`, CLI
`crates/buzz-cli/src/commands/channels.rs`) so they land on the writer row.
Loading
Loading