mirror of
https://github.com/block/buzz.git
synced 2026-10-29 21:41:33 +00:00
195298d7e4fe2e2cb8575f10f7eb48fb5af952af
2635 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
195298d7e4 |
Fix accumulator message builder compatibility
Signed-off-by: Sol <478bb5a31222ea2b28a3d1afb8b1d598940628f19c2a87efc3c4b822299eeec6@buzz.block.builderlab.xyz> |
||
|
|
9b0e325fdd |
accumulator: channel exclusion — persistent flag, sync skip, broad-scope query omission
Excluded channels keep their mirrored events (existing folds and artifact
provenance stay truthful) but receive no live subscription, backfill, or
gap-fill. Broad (authors-only) selections omit them inside the one shared
signal_query, so preview, the events browser, and fold plan/coverage/run
cannot disagree; explicitly naming an excluded channel or a thread anchor
inside it still queries the stale mirror. Re-including resets the backfill
cursor to now so the paged walk repairs the gap by construction.
PUT /channels/{id}/excluded toggles the flag (404 unknown) and nudges the
sync loop into an immediate resync; GET /channels now carries excluded on
every row. Discovery upserts never touch the flag. Legacy mirrors gain the
column via idempotent migration.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Riley Crane <rileycrane@squareup.com>
|
||
|
|
6c89661ca9 |
accumulator: tag-filtered selections + tagged artifact publication
Selections gain a tags clause: exact-match [name, value] pairs checked against the event's own tag list, verbatim and case-sensitive. Pairs sharing a name OR together; distinct names AND together (NIP-01 filter semantics). Tags only narrow — they never grant scope. The predicate is applied inside the single signal query, reading straight from the stored raw event JSON, so preview, the events browser, and fold plans can never disagree about what matches. Legacy selections load unchanged and keep their cache identity. Publish gains caller-supplied tags, stamped on the outgoing message verbatim so a downstream fold can select the published artifact back by channel + tags — the machine-readable recipe edge. Protocol tag names (h, e, p, auth) are refused: each is load-bearing on the relay and stamping one would spoof scope, threading, notifications, or ownership. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Riley Crane <rileycrane@squareup.com> |
||
|
|
0308c7fd43 |
accumulator: replay missed membership notifications via persisted watermark
A channel created or joined while the daemon was backfilling, reconnecting, or briefly down never appeared until a process restart: the live-membership subscription (44100/44101, which are persisted and p-gated) anchored at "now", taken only after discovery + channel backfill + the profile backfill — so a notification emitted in that window was never queried again. The daemon now persists a membership watermark (meta table), advanced when discovery completes and past each handled notification. The subscription anchors at the watermark, so missed notifications replay on subscribe and trigger the existing clean resync; replays terminate because every resync's discovery re-advances the watermark. One subscription per channel is unchanged (a resync is a fresh connection). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Riley Crane <rileycrane@squareup.com> |
||
|
|
2e5602a442 |
accumulator: thread selections — anchor + all descendant replies
Selection gains a threads clause: 64-hex event ids, each selecting itself
plus every descendant reply at any depth, resolved by a recursive CTE over
the mirror's new parent column (NIP-10 e-tag linkage). channels and threads
union as scope; authors/kinds/window intersect on top. Legacy mirrors gain
the column in place, backfilled once from the raw JSON already on disk — no
relay re-fetch. GET /events/{id} now returns parent + thread_root for
pickers. One-off comparison recipe documented in the README.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Riley Crane <rileycrane@squareup.com>
|
||
|
|
2a4638444f |
accumulator: budget for effective runner context
Signed-off-by: Sol <478bb5a31222ea2b28a3d1afb8b1d598940628f19c2a87efc3c4b822299eeec6@buzz.block.builderlab.xyz> |
||
|
|
7d95007cc3 |
accumulator: size prompts conservatively for runner limits
Signed-off-by: Sol <478bb5a31222ea2b28a3d1afb8b1d598940628f19c2a87efc3c4b822299eeec6@buzz.block.builderlab.xyz> |
||
|
|
8c1a29e0f2 |
accumulator: explicit traversal order, model-aware token budget, coverage state
Riley's briefing-flow mandate (event 9b7fa66c), three changes:
1. Order is an explicit policy, not hidden global behavior. FoldSpec
gains `order: oldest-first | newest-first` (serde default
oldest-first; legacy specs load), overridable per preflight/run body.
oldest-first walks the backlog forward — repeated bootstrap runs
cover earliest → latest with no skipped or duplicate shown_ids —
and is the default because it never leaves temporal holes;
newest-first keeps the freshest evidence when the budget binds
(catch-up). The transcript the model sees is chronological either
way (truncation marker sits where the missing events are). Order is
deliberately absent from the cached-run comparison: it decides which
uncovered signals fold first, not what an artifact contains.
2. The flat 120k-char planning ceiling is replaced by a model-aware
budget: window − reserved output (16,384 tok) − safety margin
(8,192 tok), with instructions, engine guidance, prior artifact and
the source-id list charged before the transcript gets the remainder;
1M-char emergency cap; unknown models fall back to the old
conservative ceiling instead of guessing. MAX_SHOWN_PER_RUN raised
250 → 1,000 and demoted to an emergency provenance guard; the
40k-byte prior guard stays. Every ready plan reports its actual
limiting constraint (none | token-budget | event-cap) plus the full
budget breakdown.
3. Explicit completion state: coverage {processed, pending, complete}
on every preflight variant and on GET /folds/{name} (complete only
ever true for a fully covered frozen selection). artifact summaries
gain the fold name so any row deep-links to fold/version.
Also: README documents that follow/participated-in defaults are the
client's to resolve (GET /channels + authors:[me]); acceptance test
bootstrap_walks_the_backlog_then_increments_without_replay pins the
whole flow — multi-pass oldest-first bootstrap, truthful per-run
boundary reporting, then one new event folds with zero replay.
Gates: cargo test 84/84, clippy -D warnings, fmt, ui tsc + build.
Live smoke on a scratch mirror: a real 361-pending fold now plans 358
events in ONE run at ~175k est tokens with 25k headroom (was ~65
events near the 30k-token ceiling), limit reported token-budget;
newest-first override changes the chunk; legacy spec surfaces
order oldest-first.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Fizz <fizz@agents.buzz>
|
||
|
|
3741229235 |
accumulator: three-noun algebra — selection absorbs when, publish verb, schema deleted
Design of record (Riley's refinement): Selection = frozen-or-live
description of a signal set (who x what x WHEN — since/until_exclusive
live on the selection itself; pinned end = frozen, open = live). Fold =
name + selection + model + instructions, a factory for artifacts. A
frozen fold runs until covered then is cached forever; a live fold is
never done. Rollup machinery stays deleted: publish an artifact back
into a channel and it becomes a signal a later fold can select.
- selection: since/until_exclusive fields; canonicalize rejects empty
windows; resolve_window(clamp, now) — request bounds can only narrow
(runs stay pinned to priced preflight windows; frozen selections never
read outside their freeze). Dead code deleted (compile_filters,
describe).
- spec/artifact/run: schema label deleted end to end — the output is
verbatim, a label describing its shape was dead weight. Legacy JSON
with the field still loads (unknown-field tolerance, test-covered);
config_matches = model + prompt hash + selection.
- daemon/folds: plan/preflight/run take a WindowClamp instead of a
resolved window; default_window deleted. New test: frozen fold
completes then stays cached forever even under a wide clamp.
- daemon/publish (new): Publisher trait + RelayPublisher — short-lived
authenticated connection per publish, same identity as the mirror.
- http: POST /folds/{name}/artifacts/{version}/publish with a taint
guard (chain-union channels must equal the target unless
allow_cross_channel is passed deliberately); preview/select/preflight/
run resolve through the selection's own window; PutFoldBody loses
schema. FakePublisher tests for footer, guard, and 404.
- ui/api.ts: type-sync only (schema fields dropped, Selection window
fields added) — client work is Sol's.
- crate README: the three nouns, honesty invariants, run recipe, API
table.
Gates: cargo test -p buzz-accumulator 73/73, clippy -D warnings clean,
fmt clean, ui tsc + build clean. Live-smoked on a scratch mirror:
live window ends at now+1, wide clamp cannot widen a frozen fold
(window == freeze exactly), empty window 422s, cross-channel publish
refuses with the deliberate-override hint.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Fizz <fizz@agents.buzz>
|
||
|
|
0722066d73 |
refactor(accumulator): the response IS the artifact — free-form output, transparent runs
The artifact model now matches the mental model: instructions + context → model → response, persisted verbatim. Any shape the task asks for. - Delete schema.rs and validate.rs entirely: no output contract, no splice, no refusal path. complete_run is infallible; Error::Nonconforming and RunOutcome::Refused are gone. Provenance (shown_ids, coverage, truncated) stays engine-computed from the plan, never parsed from output. - spec: schema becomes a provenance label (freeform@v1 default; old chains keep channel-digest@v1, no migration). New DEFAULT_INSTRUCTIONS. - run: model_input = TASK (user instructions) + short engine guidance (context explained, citations render as links) + CONTEXT; prior version rides as '--- PRIOR VERSION (name vN) ---'. - preflight gains include_input: the Ready plan can carry the exact string the model would receive — the behind-the-curtain seam. - UI: fold cards always show their instructions; the plan box explains incremental coverage and offers the exact model input behind a disclosure; artifact header explains what 'covers' means (this run's new events) and that versions build on the prior; refused branch removed. Gates: cargo test -p buzz-accumulator 67/67, clippy -D warnings, fmt; ui tsc --noEmit + vite build clean. Live-verified on scratch daemon :4641 (preflight include_input round-trip; legacy channel-digest@v1 artifact still loads). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Riley Crane <rileycrane@squareup.com> |
||
|
|
5c0fc2e78a |
refactor(accumulator): citations are links, not a validated contract
Provenance was always engine-owned and deterministic — shown_ids and the coverage window come from the plan, never from model output. The citation-subset refusal was a second layer on top whose failure mode (refuse the whole paid run) fired on model formatting slips: rewriting prior Log entries non-verbatim, or citing an event id quoted inside a message's text. Both hit in dogfood. validate_output now checks only the H1 section structure, which the append-splice and the reader depend on. Citations stay in the prompt as guidance and render as verifiable links; a wrong id is a visible dead chip instead of a lost run. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Riley Crane <rileycrane@squareup.com> |
||
|
|
936fc522b3 |
fix(accumulator): output contract travels with every run; real markdown in the lab
Custom fold instructions replaced the entire prompt, including the
structural rules validate_output() enforces — so any task-only prompt
('what is this project about?') was a guaranteed refusal. The schema's
contract is now rendered from ArtifactSchema::contract_prompt() and
composed into every model input after the instructions; instructions
are task focus only. Verified live: the exact instructions that refused
now fold with conforming sections and citations.
Lab: replace the hand-rolled artifact renderer (which left **bold** and
Signed-off-by: Riley Crane <rileycrane@squareup.com>
--- literal) with react-markdown + remark-gfm, citation chips preserved
via a #cite: link rewrite; styles for hr/blockquote/pre/table.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Riley Crane <rileycrane@squareup.com>
|
||
|
|
e2065667da |
fix(accumulator): lab tolerates omitted selection arrays; add error boundary
The daemon skip-serializes empty Selection vecs, so an authors-only fold spec arrives with no channels key — FoldCard crashed reading .length. Normalize every Selection at the API boundary and wrap the app in an error boundary so future render errors show the message instead of a white screen. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Riley Crane <rileycrane@squareup.com> |
||
|
|
e82d63ad58 |
fix(accumulator): lab degrades honestly against a pre-micro-batch daemon
An older daemon build omits buckets from /select/preview and 404s /select/events; the Explore pane crashed on buckets.length instead of degrading. Now: - Preview.buckets is optional in the client type, guarded at use. - A 404 from /select/events renders an actionable note (rebuild the daemon) instead of an unhandled query error. - Inline SVG favicon silences the /favicon.ico 404 console noise. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Riley Crane <rileycrane@squareup.com> |
||
|
|
c9c4fd941e |
feat(accumulator): vite+React lab UI at crates/buzz-accumulator/ui
The ergonomics lab for the exploration client: a standalone SPA that talks to the daemon through vite's dev proxy (/api → 127.0.0.1:4640), so the daemon stays loopback-pure and zero-config. One screen, three panes: - Selection composer (channels/authors/kinds + window presets), with URL-shareable state (?channels=…&since=… and ?fold=…&v=…). - Explore: live zero-spend preview (count/chars/est tokens), the daily rhythm strip, and a paged browser of the matching events. - Folds board: create-from-current-selection, and per-fold cards with the priced loop — preflight ($0) → run pinned to the exact priced window → outcome (refusals salvage the model output visibly). - Artifact reader: Working Context / Log rendering with [event:id] citation chips that pin the source message into an evidence rail, plus a version scrubber. Deliberately its own pnpm workspace root: not in the repo workspace, invisible to just ci. Run: cd crates/buzz-accumulator/ui && pnpm install && pnpm dev (daemon running on :4640). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Riley Crane <rileycrane@squareup.com> |
||
|
|
6baffde9df |
feat(accumulator): daemon micro-batch for the exploration client
Three small additions the lab UI builds on, plus one fix:
- GET /events/{id}: resolve a mirrored event (author name included) —
the citation-chip endpoint.
- POST /select/events: the actual matching events, paged oldest-first
with a (created_at, id) keyset cursor; limit clamped to 1..=500.
- /select/preview now returns sparse UTC-day buckets (the rhythm strip).
- FoldSpec gains a free-form client-owned `meta` field, persisted
verbatim, never read by the engine, and deliberately absent from the
cached-run comparison — clients stash strategy metadata there without
invalidating chains. Legacy spec JSON without the field still loads.
- fix: profiles backfill now pages to the beginning (bounded at 40
pages) instead of taking one newest-first page, which let recent
ephemeral agent profiles crowd out long-standing human names.
Live-verified: 16.5k profiles mirrored; author names resolve.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Riley Crane <rileycrane@squareup.com>
|
||
|
|
839c4b637b |
refactor(accumulator): cut the relay-kinds + CLI path; daemon is the only door
Riley-approved parsimony pass now that the standalone daemon supersedes the relay-event path: - Remove kinds 30640/4640 end to end: kind.rs constants/allowlists/ asserts, ingest.rs scope arms, migration 0041 + schema.sql FTS exclusion + migrator expectations (back to 40), and the entire 'buzz folds' CLI group with its buzz-accumulator dependency. The branch now touches ONLY crates/buzz-accumulator plus the workspace manifests - nothing for the team relay to redeploy. - Estimator keeps tokens, loses dollars: est_cost_usd and the curated PRICING table are gone (a 3-row USD table ignoring output tokens is false precision that goes silently stale); estimate_tokens + window fit stay so the UI can still gate a priced Run button. - Strict citation grammar: a bracket is a citation iff its content is exactly one id; the multiple-of-64 hex-run splitting is deleted and a jammed multi-id bracket is now refused loudly. - Collapse the one-entry schema registry: BUILTIN_SCHEMAS/builtin() and the duplicate unknown-schema checks in plan_run/complete_run are gone; FoldSpec::validate is the single seam and the schema string field stays as the versioning hook. - Doc honesty for the SQLite reality: MAX_PRIOR_OUTPUT_BYTES is a model-input budget (value unchanged), artifacts persist locally instead of as NIP-44 relay events, and the daemon docs state the HTTP API is loopback-only by design - /run spends money, so any non-loopback bind must add auth first. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Riley Crane <rileycrane@squareup.com> |
||
|
|
6aa6e37b01 |
fix(accumulator): honest status on resumed runs
A restart with a completed mirror skipped backfill without telling the status registry, so /status reported backfill_complete=false forever and newest_ts stayed null until a live event happened to land. Done channels now mark themselves at startup, and live subscriptions seed newest_ts from the mirror. Verified live: restart on a warm mirror reports backfill_complete=true and per-channel recency within seconds, zero pages re-fetched. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Riley Crane <rileycrane@squareup.com> |
||
|
|
e987232711 |
fix(accumulator): survive relay CLOSED acks; report backfill progress in status
Live run against the team relay surfaced three gaps: - The relay acks a client CLOSE with an empty-message CLOSED. Backfill pages reused one sub id per channel, so a late ack for page N matched page N+1's REQ and the sync treated it as a refusal - permanent reconnect loop, live tail never established. One-shot sub ids are now connection-unique (per-page / per-chunk), an empty-message CLOSED on the current sub settles it instead of erroring, and the live loop ignores CLOSED frames for subscriptions it does not own. - Access revoked mid-backfill would bail and hard-loop the reconnect; it now deactivates the channel and moves on, same as the live path. - newest_ts was only set by the live tail; backfill pages now report it, so /status shows real progress while history downloads. Also stops clap from leaking the internal Config doc comment into --help long output. Verified live: 19/19 channels discovered, backfilled (7529 events) and live-tailed against wss://buzz.block.builderlab.xyz with zero reconnects; the previously fatal CLOSED ack now logs and is ignored. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Riley Crane <rileycrane@squareup.com> |
||
|
|
639a6905d9 |
feat(accumulator): standalone headless daemon behind a feature gate
One crate, runnable on its own: cargo run -p buzz-accumulator starts a
tokio daemon that authenticates to the relay (NIP-42), discovers every
channel the key is a member of (39002 -> 39000, archived dropped),
backfills each channel into a local SQLite mirror with a persisted
per-channel cursor, then live-tails new events - reconnecting with
capped backoff, and treating membership changes (44100/44101) as a
clean resync. Profiles (kind 0) mirror too so fold prompts get names.
A loopback HTTP API (default 127.0.0.1:4640) exposes the machinery for
the future standalone UI: GET /status (connection, backfill and live
progress per channel), GET /channels, POST /select/preview, fold CRUD
under /folds/{name}, zero-spend /preflight, /run, and artifact
history. Artifacts persist under a (fold, version) primary key, so the
version fence is atomic; a losing concurrent run salvages its paid-for
model output in the response instead of forking the chain. Runs are
serialized per fold (409 when busy).
Identity is a single seam (BUZZ_PRIVATE_KEY or --key-file) so swapping
in an editable identity later touches one function. The relay is
pinned to the team relay; the ambient BUZZ_RELAY_URL is deliberately
ignored (dev shells export a sandbox value) - override only via
--relay / BUZZ_ACCUMULATOR_RELAY_URL.
The whole daemon sits behind a default-on 'daemon' cargo feature; the
workspace dependency uses default-features = false, so buzz-cli and
the engine consumers keep the pure zero-I/O core.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Riley Crane <rileycrane@squareup.com>
|
||
|
|
15dfe9f3e8 |
revert(accumulator): drop desktop Settings tab + Tauri exec seam
The standalone daemon (buzz-fold) plus a future standalone UI supersede the in-app control surface; the branch no longer touches desktop/. Also drops the 'accumulator' preview flag entry. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Riley Crane <rileycrane@squareup.com> |
||
|
|
6c8b1c865a |
fix(accumulator): harden engine, CLI, and desktop seam after adversarial review
Engine (buzz-accumulator): - Citations are always judged: a run shown no signals may cite nothing, so fabricated citations can no longer ride config-only reruns. - Hex runs split into event ids only at exact multiples of 64 chars; a valid-id prefix with hex garbage appended now fails loudly. - Oversized transcript lines keep a >=256-char tail or stay pending; a sliver can no longer seal an event as covered. - Runs cap shown signals (250) and stall when the standing artifact exceeds a 40k-byte publish ceiling, keeping the NIP-44 65,535-byte plaintext cap a priced-before-spend stall instead of a post-spend publish failure. Empty selections with no prior stall too. - Artifacts carry their selection and the chain-wide channel union; the cached-run shortcut now misses on selection edits. - The subprocess runner leads a process group and group-kills on timeout, and pipe drains are joined with a deadline so a grandchild cannot orphan a billed model call or wedge the run. - Selections canonicalize lowercase and validate UUID/pubkey/kind shapes; the unused freeform@v1 schema is gone. CLI (buzz folds): - share checks the artifact's channel provenance, not the live spec: history folded from another channel can never be laundered into a single-channel share by editing the selection. - Artifact events publish without plaintext tags (fold names private). - Signal fetches are exhaustive within the window and error on the --limit bound instead of silently keeping only the newest page. - run salvages the paid-for model output to stdout on every post-spend failure (refusal, size ceiling, version fence, publish), and aborts before forking the chain if a concurrent run published. - estimate reports the priced window so run can replay it exactly; the redundant --schema flag is gone. Desktop: - run_folds_cli scrubs ambient BUZZ_AUTH_TAG/BUZZ_API_TOKEN/ NOSTR_PRIVATE_KEY, runs from home, leads a process group, and uses 900s/120s timeouts with headroom over the CLI's own. - The settings card pins Run to the preflight's priced window and re-locks Run when a run fails; dead kind consts removed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Riley Crane <rileycrane@squareup.com> |
||
|
|
84e86f085f |
feat(accumulator): desktop Accumulator settings tab behind preview flag
Adds an 'accumulator' preview feature (off by default) that reveals a Settings > Personal > Accumulator control plane. The panel is a thin shell over the bundled buzz CLI: a new run_folds_cli Tauri command executes 'buzz folds <verb> ...' as the desktop identity against the active workspace relay, with the verb allow-listed to the folds group, bounded args, drained pipes, and a hard timeout (longer for 'run', which invokes a model). Nonzero exits return as data so the UI can show the CLI's own error message. The panel lists folds, prices a run via the zero-spend preflight (Run only unlocks after Preflight prices the exact pending set), shows the latest artifact, and points at the CLI for creation and automation - fold state itself never lives in the desktop. Mirrors kinds 30640/4640 into shared/constants/kinds.ts and stubs run_folds_cli in the e2e mock bridge. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Riley Crane <rileycrane@squareup.com> |
||
|
|
167d45d946 |
feat(accumulator): buzz folds CLI — set/list/get/delete/estimate/run/artifact/share
Registers the accumulator control plane in buzz-cli. All state lives on
the relay as the caller's own signed events: fold specs are kind-30640
addressable heads (d = fold name, LWW via monotonic created_at) and
artifact versions are immutable kind-4640 events, both NIP-44 encrypted
to self. delete publishes a {"deleted":true} tombstone envelope since
addressable events cannot be removed from an append-only store.
estimate and run share one zero-spend preflight: load spec + full
artifact chain (bounded exhaustive read so the covered-signal union can
never silently truncate), fetch pending signals one filter per channel,
and plan via buzz-accumulator. Nonconforming model output is refused
and nothing persists. share is taint-gated: an artifact may only be
republished into the single channel its selection reads from.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Riley Crane <rileycrane@squareup.com>
|
||
|
|
62f0d68761 |
feat(accumulator): fold engine crate + fold event kinds (30640, 4640)
A1 of the accumulator experiment: incrementally maintained, provenance- carrying artifacts over the event stream (signal / selection / fold / artifact), shipped as a packaged experiment. - New pure-logic workspace crate buzz-accumulator: selection-to-filter compilation with deterministic materialization, budgeted transcript rendering, zero-spend estimates (unknown model => honest None), plan_run/complete_run around a FoldRunner seam (subprocess claude -p v1 impl with sentinel/exit-code/timeout hardening), output-contract validation (exact H1 sections, cited within shown ids, >=1 citation, refuse => nothing persists), and the engine splice that retains append-section history by construction. 46 unit tests port the honesty invariants: coverage never seals unread, stall-don't-seal, prior citations never satisfy a new run. - New kinds in buzz-core: 30640 fold spec (parameterized replaceable, d = fold name) and 4640 artifact version (regular/immutable), both AUTHOR_ONLY_KINDS with NIP-44 content encrypted to self. - Relay: UsersWrite scope arms + global-only (never channel-scoped). - FTS privacy: both kinds excluded from search_tsv in schema.sql and brownfield migration 0041 (same shape as 0014/0033); embedded migrator tripwire extended. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Riley Crane <rileycrane@squareup.com> |
||
|
|
d595806fc3 |
fix(desktop): authorize remote mentions at publication (#7124)
🤖
## Summary
In Buzz Desktop, you could own an agent running on another device but be
unable to mention it in a channel where it had not yet joined: it was
filtered out before you could invite it. A selected agent could also
disappear from the message's recipients when permissions changed. This
lets you select an eligible agent in the existing **@ menu**, invite it
from the message composer, and send to that agent—or see an error and
keep your draft rather than silently sending without it.
#### Where the experience changes
| Screen / control | Before → after |
| --- | --- |
| A channel's **Message #…** composer, or a message's **Reply in thread
to …** composer | Type `@` (or use the existing @ button), choose your
agent, write the message and press **Send message**. An owned agent not
yet in the channel can now reach the existing **“Mention people outside
this channel?”** dialog when its response settings allow you to address
it. |
| That dialog's **Invite** button | Previously the membership
requirement could block the agent before the invitation. Now Invite
checks permission to add it, adds it as an agent member of the
**channel** (not just the thread), then rechecks membership and response
permission before sending the waiting message. An agent already in the
channel needs no invitation. |
| Existing direct message, or the new-message screen with the **To:**
field | A mention is checked against the conversation the message will
actually enter, including a newly created direct message, rather than
the old or not-yet-created destination. This does not add an Invite
control to direct messages. |
| Editing a message / sending attachments | The selected agent remains
part of the send or edit attempt through attachment upload and the final
permission check. Lost permission produces a visible error instead of
dropping that recipient. |
**Invite is not the only chat choice.** The existing **Do nothing**
button sends the message *without inviting or notifying the nonmembers*;
their names remain references in the text. Where you cannot invite, that
choice is labelled **Send anyway**. To abandon the send instead, dismiss
the dialog with Escape. Invitation actions are disabled while
preparation is pending, preventing duplicate clicks.
**Leaving and returning must not resurrect a cancelled send.** Switching
threads or leaving the composer cancels its pending invitation, even if
you return to the same thread. Cancellation before dispatch sends no
message; an accepted membership change cannot be automatically undone.
An ordinary send without a pending invitation remains bound to its
original destination rather than following you into another
conversation.
**Failed sends must not overwrite your next draft.** If you leave a
thread, return and replace or deliberately clear its draft while an
older send is pending, the older failure cannot restore deleted text,
recipients or files; success cannot erase the newer draft—even if its
text is identical. An untouched draft cleared automatically for sending
remains recoverable on failure. This protection also covers reopening
the composer and starting a newer send.
The channel timeline also keeps its existing **new-messages / Jump to
latest** button available when newer messages are waiting to be
displayed. For example, after sharing a reply to the channel and closing
the thread panel, you can click the catch-up button to reveal buffered
messages. Closing the thread does **not** guarantee the shared row
appears automatically or force you away from reading history.
### Related issue
Built on [#7122](https://github.com/block/buzz/pull/7122), base branch
`split/owned-agent-discovery`, which lets Desktop find and verify owned
agents independently of this device. Current integration head:
`1144465d00273cf74b7c22544ae5a3299bd98560`, built on exact published
root `3a56d17824522580fe04cae463b54f4c7ba66021`. Root #7122 has its own
CI and security gates; this PR must not land ahead of that dependency.
Finding an agent is not channel membership, online status or a promise
of a reply. This PR changes what the existing message controls can do
with those agents; it adds no profile, presence, cloud marker or remote
start/stop UI.
Standalone forum post/reply **Invite / Cancel** is added separately in
[#7125](https://github.com/block/buzz/pull/7125); here those composers
only gain visible authorization errors. Same-name selection/binding
fixes ([#7133](https://github.com/block/buzz/pull/7133)) and mention
spacing ([#7128](https://github.com/block/buzz/pull/7128)) are not
included.
Extracted from [#7114](https://github.com/block/buzz/pull/7114)
(historical source `98fe33ec`). [Behavior and draft-recovery
contract](
|
||
|
|
ce9decb235 |
fix(acp): rename system tag to agent-instructions (#7332)
## Summary Rename the paired persona prompt boundary from `<system>` to `<agent-instructions>` in both modern and legacy ACP delivery paths. Update Desktop diagnostics to label the new boundary as Agent Instructions while keeping archived `<system>` captures readable. `system` is confusing (esp with system role) and agent instructions is what shows up in the UI today <img width="767" height="298" alt="Screenshot 2026-09-04 at 11 37 54 AM" src="https://github.com/user-attachments/assets/4e34ac06-db50-461e-a555-ce17885b0031" /> ### Related issue Follow-up to #6701. No duplicate open issue or PR found. ### Testing - `cargo test -p buzz-acp` - `cargo clippy -p buzz-acp --all-targets -- -D warnings` - `node --import ./desktop/test-loader.mjs --experimental-strip-types --test desktop/src/features/agents/ui/agentSessionTranscriptHelpers.test.mjs desktop/src/features/agents/ui/agentSessionTranscript.test.mjs` - `pnpm --dir desktop typecheck` - Pre-push hook: 6,237 Desktop tests, 3,164 Tauri tests, and all 13 Rust unit-test lanes passed Generated with Codex Signed-off-by: Salman Mohammed <smohammed@squareup.com> |
||
|
|
ee883d73fb |
fix(desktop): bind duplicate mention selections to exact recipients (#7133)
## Summary Selecting two people or agents named Scout could replace the first recipient with the second even though the message still looked right. This binds each selection to its exact identity: the first keeps `@Scout`, and a conflicting selection gets `@Scout (<full public key>)`. Removing one no longer removes or redirects the other. - Reuse the existing **@ suggestion list**; team selection and automatic agent addressing reserve and reuse distinct labels too. Typing an ambiguous name manually shows an instruction to use the picker and preserves the draft without publishing—in chat, edits and standalone forums. - Share literal mention matching across recipient extraction, removal, display and editing so a shorter name cannot claim another recipient's longer or qualified label. - Rebuild authored references when a message is edited, and use the latest authorized snapshot when reopening or forwarding. Rendering/editing can recover qualified identities only from the message's recorded references, not from a key typed into its body. Unresolvable historical names remain literal rather than guessed recipients. - Wrap full-key labels within narrow/zoomed layouts while keeping their complete accessible label and ordinary mention icons. Edit activation waits for the action menu's focus cleanup before focusing the editor. ### Related issue Targets `main`; mention spacing (#7128) is already merged. Split from #7114. This is independent of the #7124 → #7125 remote-invitation stack and does not expand agent eligibility or invitation permissions. The separate Enter-selection suffix issue remains tracked in #7253. ### Testing The desktop unit suite and focused mock-Chromium checks passed on the published integration candidate, including pending-paste selection, edit/forwarding, copy and narrow-layout cases; formatting, types and frontend builds passed. On `0b3b18c0`, 64 focused trust/paste/selection unit tests and six mock-Chromium tests passed with zero browser retries, including actual timeline chip copy → fresh channel paste → send and mismatched-key rejection; TypeScript, changed-file Biome and an isolated E2E build passed. The broader browser run had copy failures before the focused repairs and is not claimed as wholly green. See [live CI](https://github.com/block/buzz/pull/7133/checks) for current-head results. No full local `just ci` pass, native/live-relay or cross-browser validation is claimed. To try it: select two same-name recipients, remove one, send, then edit/reopen and forward; only the intended identities should remain. Type an ambiguous name without choosing a suggestion and check that sending retains the draft with an error. Inspect full-key labels in a narrow window at 150% text size, and open Edit and type immediately.   *Earlier mock-browser captures, not current-head runtime proof. No new before-state capture; screenshots alone do not prove recipient delivery.* **Clipboard trust:** generated full-key-qualified mentions now retain their exact recipient after copy/paste when the full key matches the clipboard record and community directory/profile state independently vouches for the base alias, including numeric collision suffixes. A qualifier alone does not establish trust. Arbitrary historical labels still cannot always be reconstructed. --------- Signed-off-by: Logan Johnson <loganj@squareup.com> Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> Co-authored-by: Bad Janet <150b20bdf6130418df9239dd1bd082c71612c8d653b47c277200365b9be215dc@buzz> |
||
|
|
cd02b693aa |
refactor(relay): extract NIP-29 membership authorization (#7285)
## Why NIP-29 membership authorization for kinds 9000, 9001, and 9022 was embedded in a large async handler, requiring Postgres and Redis to test and repeating the last-owner rule across five call sites. This addresses [TheSentinel454/buzz#24](https://github.com/TheSentinel454/buzz/issues/24) without adding the broader state/repository abstraction suggested there because HTTP and WebSocket writes already share ingestion. ## What - Extract pure, typed membership authorization decisions while leaving database reads and mutations in `validate_admin_event`. - Preserve existing client-visible errors and independent database last-owner safeguards. - Collapse five last-owner policy restatements into one predicate and share the identical self-departure policy. - Include the relay decision modules in `just test-unit`, so these tests execute in CI. ## Risk Assessment Medium-low. This touches production relay authorization, but intentionally preserves wire behavior and database defense in depth; exhaustive decision tables and relay-backed tests cover the affected paths. ## Simplification This removes repeated policy from the orchestration path and makes the rule set directly testable without introducing a repository trait or second transport path. ## Verification Verified at `15255a090797f85874921120003c645962fefaed`: - `just test-unit` — all 10 package summaries passed. An initial run hit two unrelated timing-sensitive `buzz-acp` failures; the complete retry passed 905/905 in that package. - `cargo fmt --all -- --check` - `cargo clippy --workspace --all-targets -- -D warnings` - `just file-size-check` — 10/10 policy tests passed. - `just security-review-check` — 13/13 tests passed. - Push preflight — `push-head-scope`, `branch-skew`, `file-size-check`, `rust-tests`, and `desktop-tauri-checks` passed. - Blox Postgres relay lane — 84/84 at the byte-identical source patch; full live `e2e_relay` behavior matched the clean base (45 pass and the same pre-existing kind:9002 failure on each). Generated with Claude Code --------- Signed-off-by: tornquist <tornquist@squareup.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: Codex <noreply@openai.com> |
||
|
|
01bacb8df3 |
chore(release): release Buzz Desktop version 0.5.22 (#7308)
## Buzz Desktop release v0.5.22 - **Frozen main:** `75f101d8b4f5b4b26f9891b73b87c67fedac25a0` - **Reviewed candidate:** `9ceb1f79bbc21785a0a075c40aecb3c058b1ea15` - **Previous desktop release:** `desktop-v0.5.20` - **Proposed immutable tag:** `desktop-v0.5.22` This PR may be **squash merged** after the Desktop Release Candidate check and all protected-branch checks pass. Merging authorizes publication of the exact reviewed candidate; later or unrelated changes on `main` cannot alter it. The checked-in changelog accounts for every non-merge commit in the release range. The Desktop tag points to the reviewed candidate commit, not the later squash commit. Publication remains bound to that immutable candidate tag. Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: Release Automation <release-automation@users.noreply.github.com> |
||
|
|
b1f6b7ef77 |
feat(desktop): preserve mentions across copy and paste (#7228)
Copying a message out of the timeline lost the mention. The rendered chip drops the `@` for display, so the clipboard carried "John Smith" — two ordinary words no composer could bind back to a pubkey. Pasting into another channel produced dead text, and sending it tagged nobody. ## What changed Every Buzz copy now writes two clipboard flavors in one transaction: - **`text/plain`** — readable anywhere, sigils restored, no pubkeys. This is what TextEdit, Slack, and every other external app receive. - **`text/html`** — the same content with each mention wrapped in a span carrying `data-mention-pubkey` / `-label` / `-kind`. On paste, the composer harvests those records, registers each `name → pubkey` with the existing mention machinery, and inserts the content: the chip re-lights and the send path recovers the identity the author tagged. A marker attribute records what the plain flavor holds, so a Markdown copy pastes through the text pipeline and a rendered copy through the HTML one. **Covered surfaces:** timeline selection copy, thread-panel selection copy, forum post/reply selection copy, "Copy message", and composer copy/cut — plus paste in both the channel and forum composers. ## Trust boundary Clipboard HTML is untrusted, and the branch treats it that way: - Records are capped (50), labels bounded (200 chars), and a pubkey must be 64 hex before it can become a `p` tag. - **Only mentions the paste actually shows are registered.** An empty `<span data-mention-pubkey=… -label="Jane Doe">` would otherwise rebind that display name for the rest of the composer session, so a later hand-written @Jane Doe would chip-light convincingly against the attacker's key. Each branch registers only the records whose label appears in the text *it* inserts, matched with `getMentionOffsets` — the same matcher the send-time extractor uses. - **The visibility gate reads only what ProseMirror will insert.** `DOMParser` hard-drops `script`, `style`, `title`, `noscript`, `object`, and `head` content, so `visible<style>@Jane Doe</style>` beside an empty chip span used to smuggle a binding past the gate. Those elements are stripped before either output is derived. - **A partial chip never gains a sigil.** A selection crossing a chip boundary falls through to the browser's default copy, which serializes the full identity attributes around a slice of the text — pasting that invented "@Smith" out of "John Smith". Paste now leaves a fragment as plain text, tolerating only what a whole chip picks up in transit (restored sigil, author casing, U+00A0 swaps, and the label cap's own ellipsis). Both clipboard sides share one `matchChipTextToLabel` helper, in the module that owns the label attributes, so copy and paste cannot drift on what counts as a whole chip. ## Notes - Mention matching reuses `getMentionOffsets`, so code spans and fences are excluded and the longest display name wins. - The plain flavor inlines chip boxes before reading `innerText`; a chip is a flex container, so the browser's own copy split "@John Smith" onto its own line. - `MarkdownMention` and `MacEmacsTextShortcuts` are extracted verbatim from `markdown.tsx` and `useRichTextEditor.ts` to keep both files under the size gate. ## Testing - ~40 unit tests over the flavor builder/parser, the visibility filter, the ignored-tag sweep, and the chip-match verdicts. - A Playwright spec (`desktop/tests/e2e/mention-clipboard.spec.ts`) driving real copy/cut/paste DOM events: timeline and "Copy message" of a multi-word non-member mention pasted into another channel and sent with the original pubkey in its `p` tag; the forum copy → forum reply round trip; composer copy/cut; plain flavor asserted to contain no 64-hex string; the boundary-crossing drag; and the hidden-record and `<style>` smuggling vectors. - Every regression test is bound to a production seam and fails with its guard removed. - `just ci` / pre-push lanes green (5927 desktop unit tests, typecheck, lint, file-size gate). --------- Signed-off-by: Matt Toohey <contact@matttoohey.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
4b0744d7f3 | test(desktop): await Bestie drag and profile hover endpoints (#7294) | ||
|
|
2ac0aa1dd1 | Collapse contiguous join messages (#7262) | ||
|
|
75f101d8b4 |
chore(release): release Buzz Desktop version 0.5.21 (#7301)
## Buzz Desktop release v0.5.21 - **Frozen main:** `88687876f7808a2fd742b7eb2e4b9f87d999ad8d` - **Reviewed candidate:** `47e4e0347fb4142df55b03e0e3dc537fe33a5d62` - **Previous desktop release:** `desktop-v0.5.20` - **Proposed immutable tag:** `desktop-v0.5.21` This PR may be **squash merged** after the Desktop Release Candidate check and all protected-branch checks pass. Merging authorizes publication of the exact reviewed candidate; later or unrelated changes on `main` cannot alter it. The checked-in changelog accounts for every non-merge commit in the release range. The Desktop tag points to the reviewed candidate commit, not the later squash commit. Publication remains bound to that immutable candidate tag. Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: Release Automation <release-automation@users.noreply.github.com> |
||
|
|
8e7b275cb0 |
fix(scripts): copy global-agent-config.json in buzz-adopt-prod-agents (#7303)
## What Extend `scripts/buzz-adopt-prod-agents.sh` to copy `agents/global-agent-config.json` from the prod app-data store to dev as part of the existing atomic bundle commit, and document what the script cannot copy (baked build-time defaults). ## Why `global-agent-config.json` holds the owner's global agent defaults (`env_vars`, `provider`, `model`, `preferred_runtime`). Previously the script carried the dev copy across verbatim — it was absent from the bundle drop list and not in `RECORD_FILES`. After a `just reset`, the dev build inherited stale global defaults (or none at all) instead of mirroring prod, inconsistent with how all other bundle files are handled. The "WHAT THIS DOES NOT DO" header lacked any mention of baked build-time defaults, which are a separate (and common) source of prod/dev parity confusion. ## Changes - **`RECORD_FILES`** — add `global-agent-config.json`. The file has no volatile fields (`GlobalAgentConfig` struct = `env_vars`/`provider`/`model`/`preferred_runtime`), so it joins the verbatim-copy path already used by `teams.json` — no new transform. - **Stage-seed block** — add `global-agent-config.json` to the `rm -rf` drop list so the prod overlay replaces it cleanly; remove its stale mention in the "carried verbatim" comment. - **Docs/comments** — update "4-file bundle" → "5-file bundle" in the DMG concurrent-run warning and the step 2 comment; list the file explicitly in the step 2 header comment. - **"WHAT THIS DOES NOT DO" header** — add a bullet explaining that baked build-time agent defaults (`BUZZ_BUILD_BUZZ_AGENT_PROVIDER` / `BUZZ_BUILD_AGENT_ENV` via `option_env!` in `build.rs`) are compiled into the binary, not stored in app-data. Includes the correct `just production` invocation pattern and a `.env` warning. - **Preflight symlink checks and dry-run output** iterate `RECORD_FILES` and pick up the new entry automatically — no other changes needed. Verified: `bash -n` clean, `shellcheck` clean, `--dry-run --force` shows `[dry-run] stage global-agent-config.json (0600)` in the expected position. --------- Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> |
||
|
|
88687876f7 |
feat(relay): add early startup lifecycle logs (#7258)
## Why Early relay failures can currently appear as a container restart without a trustworthy in-process account of whether crypto, structured logging, configuration, relay identity, or the metrics listener failed. Most of those steps happen before the Prometheus exporter exists, so their chronology belongs in logs rather than metrics. Implements the logs-only early-startup slice of #7238. Post-bind Prometheus exporter supervision is tracked separately in #7284. ## What changed - create a process lifecycle recorder before the Tokio runtime and emit a fixed, versioned JSON schema directly to stderr; - record started and exactly one terminal event for `crypto_init`, `tracing_init`, `config_load`, `key_load`, `metrics_bind`, and the aggregate `process_telemetry` phase; - keep every status and reason bounded and suppress raw errors that could contain credentials, keys, URLs, or other secrets; - return typed metrics-install errors so `metrics_bind` can be classified without logging raw values, while preserving the existing public `metrics::install` API; - document the logs-only evidence contract and add real child-process regressions for success and failure paths. This PR adds **no startup metric families** and no dashboard contract. Existing application metrics remain unchanged. ## Verification Exact head: `8faf7526822a119efa035e58b2b3c59aa67fc81d` - `cargo fmt --all -- --check` - `cargo clippy -p buzz-relay --all-targets -- -D warnings` - relay binary target: 13 passed, 1 PostgreSQL-only test ignored - real relay child-process lifecycle target: 9 passed - full relay package library target: 1,023 passed, 89 ignored; the same six media tests failed at `crates/buzz-relay/src/api/media.rs:1145` with `Sqlx(PoolTimedOut)` because local PostgreSQL is unavailable - three independent exact-head reviews found no correctness, security, compatibility, lifecycle-accounting, logs-only-scope, or test-adequacy finding All exact-head GitHub CI gates are green, including lint, unit tests, PostgreSQL, relay/backend/desktop integration, both Linux server cross-compiles, Windows/macOS builds, and security checks. ## Staging verification - exact multi-architecture image: `dev-sha-8faf7526822a119efa035e58b2b3c59aa67fc81d-run-33708188952-1` - immutable manifest: `sha256:26cad28266a6bb0b0e7081eb6091d374e5489f8bb78c475a4a65737dee86cc67` - image workflow: https://github.com/block/buzz/actions/runs/33708188952 - focused staging deployment: https://github.com/squareup/builderbot-platform-core-infrastructure/pull/314 - replacement ReplicaSet `buzz-d68764bc7` has two Ready pods with zero restarts - Datadog received one complete, contiguous sequence 1-12 from each pod; both end with `process_telemetry/terminal/succeeded` at 3 ms - queries scoped to the replacement ReplicaSet return no data for the removed `buzz_startup_phase_terminal` or `buzz_startup_phase_duration_seconds` families The experimental Row 7 was removed from the Buzz Startup & Rollout Safety dashboard. This logs-only PR deliberately adds no replacement dashboard row. --- **Update Sep 3, 12:26 ET:** Clarified the review boundary: this PR does not close the broader #7238. Later exporter-task termination is pre-existing runtime behavior and is now explicitly tracked in #7284; no production code or staged image changed in this update. Generated with Codex Signed-off-by: Ravneet Arora <rarora@squareup.com> |
||
|
|
752cbfc037 |
docs(nip-fi): add Blossom kind-24242 media possession-proof exception (#7278)
Adds the media/Blossom possession-proof exception section to
\`docs/nips/NIP-FI.md\`.
## What this changes
Encodes the condition set confirmed by Thufir's security review
(2026-09-03) as normative MUSTs in NIP-FI. Kind-24242 Blossom auth
events are admitted as the NIP-FI pairing possession proof for **media
routes only** — a named, bounded exception with an explicit precedent
fence against expansion.
### Scope fence
Kind-24242 proofs are valid only on:
| Proof type (`t` tag) | Valid route | Method |
|---|---|---|
| `upload` | `PUT /upload` (and temporary alias `PUT /media/upload`
until removed) | PUT |
| `get` | `GET /media/{hash…}`, `HEAD /media/{hash…}` | GET, HEAD |
All other protected routes MUST reject kind-24242 proofs.
### Upload proofs
Exactly one `x` tag over consumed body bytes; temporal check precedes
body consumption.
### Read proofs (the relaxation Will approved)
Host-wide MAY: no `x` required. Exactly one `server` tag matching the
resolved tenant host is a MUST. Optional `x` must match parent hash if
present.
Named residual (verbatim in spec): within at most 60 seconds from
minting (plus 5s future-skew), a captured full header set allows reading
any media blob on exactly one tenant host — read-only,
membership-checked, revocable, not state-changing, not cross-tenant.
### Freshness (Thufir option 2)
- `created_at <= now + 5s` (bounded future skew)
- `now - created_at <= 60s`
- Exactly one `expiration`, valid at admission, satisfying `expiration
<= created_at + 60s`
### Transport/cardinality
Exactly one each of `Authorization` (Nostr scheme), `t`, `expiration`,
`server`; `x` at most once; reject any duplicate, malformed, or
conflicting instance.
### Per-request pairing
Full assertion verification, exact key equality between assertion
`nostr_pubkey` and kind-24242 signer, deny-map enforcement on every
request. Stub gap named.
### Compliance note
PR #7264 implementation is explicitly non-compliant until the bounded
hardening task lands (named gaps: multi-tag acceptance, 3600s window,
optional `server`).
## Behavioral oracle
`FI-TRACE-HTTP-INGRESS` extended to cover kind-24242 admission and
denial cases.
## Scope
Docs-only. No code changes. The code hardening is a separate follow-on
task.
---------
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
|
||
|
|
6cf514ed90 |
fix(desktop): wrap message tables within the available pane (#7279)
## Summary - Let Markdown tables use the available message width instead of their maximum-content width, inheriting the renderer's existing `wrap-anywhere` handling for long tokens and links. - Top-align header cells as well as body cells. Give cells a `min-w-24` readability floor so short labels do not collapse into one-letter columns; retain the existing table-local scrollbar when many columns genuinely cannot fit. - Preserve semantic table markup, links, inline code, and surrounding message layout. Add three browser regressions through the real message renderer (channel, narrow thread, and many-column overflow). ### Related issue Fixes #5313. No matching open table-readability PR found. Owner-authorized task channel: `819b36d5-7371-4ed0-bf9b-d461d723779e` Source: buzz://message?channel=819b36d5-7371-4ed0-bf9b-d461d723779e&id=3bf2d1bd5df28fcb481e71159dc1be2b3888918b05f67e270354fbfea65a8c90 ### Testing Original production candidate: `4427f2e6f9ef85a01a267fe9eabd1cf13cacfb78`; base: `7a9a5233d9d755e715be0c585cf7850e935d28cf`. - `pnpm -C desktop check` — passed. - `pnpm -C desktop test` — 6,110 passed, none skipped. - `pnpm -C desktop typecheck` — passed. - `BUZZ_PROTECTED_BUILD_OUTPUT=<isolated-production-directory> pnpm -C desktop build` — passed. - `pnpm -C desktop build:e2e --outDir <isolated-candidate-directory>` — passed. - `just file-size-check` and `git diff --check` — passed. - Chromium / mock Tauri bridge: all 3 new Playwright tests passed against the fixed candidate build. Both wrapping tests fail on the unchanged base build, while the overflow-fallback test passes. Each browser run used a unique output directory and a non-reused local server pinned to its build directory. - At 883px channel message width: table scroll width **1,395 → 883px**. At 292px thread message width: **1,395 → 292px**. All cells top-aligned; long tokens/URLs wrap, link destinations and code text remain intact, short labels stay on one line, and document width remains 1,280px. - Focused fresh-frame review checked actual before/after screenshots and the complete diff. It caught one-letter label wrapping in an early width-only candidate; the final cell-width floor and regression assertion address that finding. Limitations: native Tauri/WebKit and relay-backed integration were not exercised for this CSS-only delta. Repository-wide `just ci` was attempted but exceeded the local command budget during unrelated Rust compilation; it is **not** reported green. The full affected TypeScript package gates above passed on the exact candidate. Production/E2E builds retain existing chunk-size and mixed static/dynamic import warnings. Before/after screenshots are posted below using the repository's screenshot publication script. No merge, production installation, or runtime restart is requested. ### CI-driven test-only follow-up Current head: `925d964b6cf31ba704d74baf73769110774b2789`. Smoke shard 3 exposed an older test in `messaging.spec.ts:457` that still required three columns of ordinary prose to overflow horizontally ([failure log](https://github.com/block/buzz/actions/runs/33771325404/job/100702242706)). This expectation contradicts the intended wrapping change. Updated its name and assertion to require containment; the separate many-column local-scroll test is unchanged. No production code or harness settings changed in this follow-up. - Affected browser validation: **4/4 passed** (updated existing prose/narrow table case plus the three new channel/thread/overflow cases). - Reused the immutable `4427f2e6` E2E build on a fresh non-reused server; production source is unchanged at the current head. The package/build checks above remain evidence for that unchanged production tree, not a claim of rerunning the full suite at the new SHA. - Targeted Biome check, file-size gate, and `git diff --check` passed. - Current-head CI: https://github.com/block/buzz/actions/runs/33773490446 - Prior unrelated Bestie baseline failure and reproduction: https://github.com/block/buzz/pull/7279#issuecomment-5528077181 - Security authorization and review must target the **current** head, not the old screenshot/build SHA. A Block organization member must comment exactly `@buzz-security-review 925d964b6cf31ba704d74baf73769110774b2789`. --------- Signed-off-by: Logan Johnson <loganj@squareup.com> |
||
|
|
7a9a5233d9 |
🤖 fix(desktop): harden smoke E2E tests against Bestie overlay and toast timing (#7270)
## What Two remaining E2E hardening fixes from the Desktop Smoke flake pattern introduced by `ac5a18697` (Bestie — added `VITE_BUZZ_BESTIE=1` to `.env.e2e` and mounted `BestieGlobalOverlay` globally). The toast/DM-retry fix landed independently in main via #7127 (`input.hover()` + bounded `toHaveCount` wait); that hunk is dropped from this PR. ## Fixes ### `agent-control-regressions.spec.ts:240` — Stop does not accept an unconfirmed or foreign-channel result **Cause:** Playwright 1.60.0's `page.clock.install()` fakes all timers including `requestAnimationFrame`. The test called it before opening the settings menu. With RAF frozen, the `DropdownMenuContent`'s `zoom-in-95 duration-150` CSS enter-animation never advances — Playwright's stability check observes a continuously-changing bounding box until the 30s test timeout. **Fix:** Re-sequence so the menu is opened on real time first. After `openAgentActivity`, open the trigger, assert visibility/enabled, call `waitForAnimations(page)` to settle the enter-animation (real `setTimeout`, no fake clock installed yet), then install the clock. The `fastForward(8_001)` correlation timeout still works because it's scheduled after the clock is active. Pointer actionability preserved — normal `stop.click()` (no `force`) fails with pointer-interception under a covering surface. ### `message-feedback-snapshots.spec.ts:97` — profile hover uses the channel hover surface **Cause:** `channel.hover()` triggers a CSS `transition-colors` animation. With the Bestie `LayoutGroup` mounted, `evaluate()` captures a mid-transition background value that never matches the profile card's settled token. **Fix:** `waitForAnimations(page)` after `channel.hover()` and before reading `channelHoverColor`. ## Evidence - Target specs pass: stop-turn 8/8, profile-hover passes. - Full `agent-control-regressions.spec.ts` (7 tests) green. - `just desktop-typecheck` clean at pushed head `0fbfe2a9f`. - No production code changed. Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: Hayt <9e1c23a3fd83f61da34420e4e88ff1b16e45cafcc0cd9019eb07d4ecfa8ca9b0@buzz.block.builderlab.xyz> |
||
|
|
c6ca9d94d9 |
Show status and huddle indicators beside names (#7112)
## Summary - show status and huddle emoji beside names in DMs and message rows - provide accessible tooltips, fallback status emoji, and profile-menu icon replacement - add the desktop status editor with preset durations, a ShadCN calendar, and a capped half-hour time menu ## Validation - desktop checks, typecheck, and file-size guard - 5,802 desktop tests - focused Playwright coverage (3 passed) - E2E build and native Builderlab staging verification Updated visual snapshots are attached in the PR comments. --------- Signed-off-by: kenny lopez <klopez4212@gmail.com> Signed-off-by: Princess Donut <b238ea756dee4d98afa5883fc7f1de61eeabe65bf700e3a5a5a80db5e42e2c2b@buzz.block.builderlab.xyz> Co-authored-by: Princess Donut <b238ea756dee4d98afa5883fc7f1de61eeabe65bf700e3a5a5a80db5e42e2c2b@buzz.block.builderlab.xyz> |
||
|
|
6f5ec4a731 |
Add mobile voice notes (#7121)
## Summary - add iOS and Android voice-note recording and preview directly in the mobile composer - add waveform playback, scrubbing, speed controls, haptics, and one-shot playback in chat - package recordings in a canonical H.264/AAC MP4 envelope on both platforms so existing relays accept them - preserve the shared composer interaction and attachment-card treatment across mobile platforms Mobile counterpart to #6978. ## Testing - `just ci` - `just mobile-check` - `just mobile-test` (2,026 tests) - Android debug build compiled, installed, launched, and Voice note verified in the attachment menu on Pixel 10 - signed iOS device build installed on iPhone --------- Signed-off-by: kenny lopez <klopez4212@gmail.com> Signed-off-by: Princess Donut <b238ea756dee4d98afa5883fc7f1de61eeabe65bf700e3a5a5a80db5e42e2c2b@buzz.block.builderlab.xyz> Signed-off-by: Kenny Lopez <klopez4212@gmail.com> Signed-off-by: Carl <3c4caeafb646d23867f1c4832e68211d77e2561946171625f75c3ce1a3f2670f@buzz.block.builderlab.xyz> Co-authored-by: Princess Donut <b238ea756dee4d98afa5883fc7f1de61eeabe65bf700e3a5a5a80db5e42e2c2b@buzz.block.builderlab.xyz> Co-authored-by: Carl <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@buzz.block.builderlab.xyz> Co-authored-by: Carl <3c4caeafb646d23867f1c4832e68211d77e2561946171625f75c3ce1a3f2670f@buzz.block.builderlab.xyz> |
||
|
|
40220d5618 |
perf(desktop): publish mention sends before waking agents (#7154)
## Summary Cuts perceived agent-mention send latency by publishing the message first and waking the agent afterwards, instead of blocking the send on a synchronous agent start/deploy round-trip. A send that mentions a stopped or undeployed managed agent now shows the message immediately; the wake runs fire-and-forget after the relay accepts the publish. The already-running-agent send also gets faster via revalidation dedupe and NIP-11 caching. ## Changes ### Publish-first agent wake - Wakes for mentioned managed agents are collected during send preparation and flushed fire-and-forget only after `await send(...)` resolves. No start can fire — and no "your message was sent" toast can appear — for a message the relay never accepted; every abort path (cancel, readiness error, publish rejection, dismissed non-member prompt) simply drops the queue. Persona-create wakes ride the pending draft behind the non-member prompt for the same reason. - Each wake is bound to the tenant scope captured at send time: the new `useDetachedAgentStart` hook passes `expectedRelayUrl` + `expectedSignerPubkey` with every start, so a wake that outlives a community switch fails closed at the backend instead of spawning against the new tenant. A wake whose scope has not resolved yet (identity query still loading, blank stored relay URL) is refused with a recoverable toast rather than fired unscoped. - In-flight wakes are deduped through a module-level map keyed by `(relay URL, pubkey)` — the same tenant pair the backend keys on — so two quick sends or two composers cannot double-spawn a cold agent during the seconds-long start window. Entries are deliberately retained across community switches (the key *is* the tenant scope, so a retained entry can never affect another community, and clearing it let an A→B→A round trip deploy a provider agent twice) and self-clean when the start settles. - Wake-failure toasts are fenced to the community they fired in via a module-level scope mirror: a start that settles after a community switch logs instead of rendering community A's failure over community B's UI, and an A→B→A return re-delivers the warning where it is actionable. - Membership attach and access-policy writes stay synchronous, so the harness's first kind-39002 read still sees the channel. ### Replay floor - The send timestamp travels with the wake as `BUZZ_ACP_REPLAY_FLOOR`, threaded through both local spawns (`spawn_agent_child`) and provider deploys (`deploy_to_provider` injects it into `launch.policy_env`), so the harness's startup watermark replays back past the just-published triggering message no matter how long the spawn takes. `buzz-acp` clamps the floor to `[now − 15min, now]`. - The floor is captured at enqueue time, not flush time — the flush runs post-publish, so a flush-time stamp could exceed the message's `created_at` and skip the very message the floor exists to cover. - On local spawns the caller's floor is asserted *after* the user env layering (and the ambient parent-process value is stripped unconditionally), so a saved persona/global/agent env entry cannot shadow this send's floor — mirroring the shadow-strip the provider path applies to `launch.env`. Both halves share one `REPLAY_FLOOR_ENV_VAR` const. ### Send-path latency reductions (already-running agents) - Mention revalidation is deduped: the publish-boundary pass reuses the pre-side-effect authorization pass unless an awaited round-trip actually separated the two (background upload, link-preview settlement, DM expansion, a real access-policy/membership write, or active-huddle enrollment). This preserves the #5681 authorization boundary while making the common send single-pass. - NIP-11 `self` lookups are cached per relay URL for 5 minutes. Only verified values are cached — non-2xx and malformed responses stay retryable — and URL keying keeps community switches from serving another relay's identity. - `applyReusableAgentAccessPolicy` now reports its relay write explicitly (`{ agent, wrote }`) instead of signalling through object identity, so the revalidation trigger above is load-bearing by construction. ### File splits Four files crossed the repository file-size ratchet during this work; one cohesive unit was extracted from each rather than raising a ceiling — `runtime/setup_payload.rs`, `commands/agents_create_fields.rs`, `app_state_accessors.rs`, and `useEnsureAgentMentionsReady.ts`. The ratchet is green at the tip. ### Review follow-ups The three concrete findings from the first review round are fixed at the tip: the pre-publish wake and its false "your message was sent" toast (fixed by queueing wakes behind the publish), the stale cross-community failure toast (fixed by the scope-mirror fence), and the A→B→A duplicate provider deploy (fixed by retaining the tenant-keyed in-flight entries across switches). The fast-path admission-staleness point is answered in the review thread: deferred paths already re-validate at the publish boundary, and the remaining fast-path window is milliseconds against an irreducible network-transit race. Mid-branch send-perf instrumentation was added to attribute the residual spinner latency and reverted once that analysis concluded — it is net-zero in this diff. ### Deferred follow-ups Durable mention catch-up via `event_mentions` (option 2 step 3) and backend deploy-epoch coalescing for the wake paths that do not funnel through `useDetachedAgentStart` (Agents-panel Start, restore, inbound-persona deploys) are intentionally left for separate changes. ## Testing - `cargo test --lib` on desktop/src-tauri: 3054 passed; clippy `-D warnings` + fmt clean - Desktop unit tests: 5856 passed (the 5 failures are the pre-existing `inboxReopenNavigation` / `useRetainedProjectGitViews` baseline, present on origin/main); `tsc --noEmit` and biome clean - Full mentions (87), channels (89), and community-rail (25) Playwright smoke suites against `pnpm build:e2e` bundles, with 3× stress reruns of each new spec - The load-bearing regression specs were confirmed red on the pre-fix code: publish-failure → zero starts and no false toast, the dedupe hold (1 call vs 2), the fail-closed scope refusal, the rail-switch toast fence, and the A→B→A retention spec (1 deploy vs 2) - New unit coverage pins the queue contract (enqueue-time floors, attach-seam queueing), the scope capture and verbatim relay-URL handoff, the dedupe map's keying and settle-then-repermit behavior, the unscoped refusal, the toast-scope mirror, the `{ agent, wrote }` contract, and the replay-floor env layering on both spawn paths 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Signed-off-by: Matt Toohey <contact@matttoohey.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
5073e075d7 |
fix(desktop): unify owned-agent cloud provenance markers (#7129)
Unify presentation-only cloud provenance across agent identity surfaces. Keep successful local-inventory and verified-ownership gates; preserve main availability and mention spacing behavior. Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> Signed-off-by: Logan Johnson <loganj@squareup.com> |
||
|
|
d5a73b9f35 |
fix(desktop): derive agent availability from relay presence (#7127)
🤖 ## Requested rebase published — |
||
|
|
44d19f59a5 |
fix(desktop): preserve spacing after multi-word mentions (#7128)
🤖
## Summary
In Buzz Desktop, choosing a multi-word name and immediately continuing a
sentence could swallow the space after the mention: `Hey @Alice
Chenhello`. This keeps the separator, so the same action produces `Hey
@Alice Chen hello` without moving the caret or repairing the name by
hand.
The editor recognizes the complete selected label, including its
internal spaces, and settles the autocomplete caret after the trailing
separator. Deliberately moving left or clicking inside the label still
lets you edit there; this is not a rule that forces every caret to the
end of a mention.
### Related issue
Independent base: `main`. Child:
[#7133](https://github.com/block/buzz/pull/7133), whose disambiguated
labels also contain spaces. Extracted from
[#7114](https://github.com/block/buzz/pull/7114), retained as historical
source (`98fe33ec`).
[Behavior
contract](
|
||
|
|
c328202cb0 |
docs(nip-fi): document Git smart-HTTP credential exemption (#7268)
Amend NIP-FI HTTP ingress with an explicit Git smart-HTTP credential-helper exemption. The exception covers method binding, endpoint-URL binding, and the `payload` tag requirement for `info/refs`, `git-upload-pack`, and `git-receive-pack`, while preserving per-request NIP-FI assertion, key pairing, and deny-map enforcement. The spec records Git's credential-protocol limitation, the required compensating controls, and the rule that this exception is limited to these endpoints and is superseded by per-request signing. Related: [PR #7264](https://github.com/block/buzz/pull/7264) Signed-off-by: Will Pfleger <pfleger.will@gmail.com> |
||
|
|
47d068e210 |
feat(cli): add buzz gifs command group and NIP-30 emoji tags on messages (#7259)
## What Two new agent-facing capabilities in `buzz-cli`: ### 1. `buzz gifs` command group (agent KLIPY picker path) Agents can now search and share GIFs via the relay's authenticated KLIPY proxy without holding a provider credential. ```bash buzz gifs search # trending GIFs buzz gifs search --query "celebration" # search GIFs buzz gifs share --slug <slug> # report selection to provider Recents ``` Output is a JSON array of GIF objects. Paste the `cdn_url` field directly into `buzz messages send --content` — sending a GIF is a plain message containing the CDN URL, no special send-path handling. **Implementation details:** - Gates on NIP-11 `supported_extensions` containing `buzz-gif` and `gif.provider == "klipy"` - Uses relay-relative paths from the NIP-11 `gif` descriptor — no hardcoded paths; safe-path validation mirrors `desktop/src/features/gifs/api.ts` - New `post_json_authed` helper in `BuzzClient` handles NIP-98-signed JSON POSTs and 204 No Content responses - `customer_id` derived as `SHA-256(secret_key_bytes || '\0' || relay_url_bytes)[..16]` → 32 hex chars: stable, relay-scoped, not computable from public data, no storage needed - `locale` defaults to `$LANG` (stripped of encoding suffix) or `en_US` ### 2. NIP-30 custom emoji tags on outgoing messages `buzz messages send` now automatically attaches `["emoji", shortcode, url]` tags for any `:shortcode:` patterns in the content that resolve in the workspace palette — identical to the desktop composer behavior. ```bash buzz messages send --channel <uuid> --content "hello 👋 everyone 🎉" # → event carries ["emoji", "wave", "..."] and ["emoji", "tada", "..."] tags ``` **Implementation details:** - Hand-rolled single-pass scanner (no new dependency) implementing `:([a-z0-9_-]+):` case-insensitively with canonical lowercase output — mirrors `desktop/src/shared/lib/customEmojiTags.ts` exactly - Zero extra relay round-trips when content contains no `:` character; one `query` when candidates exist but none match - Palette fetch reuses the existing `union_custom_emoji` logic from `commands/emoji.rs` - `build_message` in `buzz-sdk` gains a new `emoji_tags: &[Vec<String>]` parameter (additive — all existing callers pass `&[]`); NIP-30 tag attachment lives in the SDK alongside `imeta` tags - MCP send path (`buzz-acp`) continues to pass `&[]` and is not affected; the MCP gap is noted in a comment ## Files changed | Crate | File | Change | |-------|------|--------| | `buzz-cli` | `src/commands/gifs.rs` | New — search + share handlers, NIP-11 gating, tests | | `buzz-cli` | `src/commands/mod.rs` | `pub mod gifs` | | `buzz-cli` | `src/lib.rs` | `Gifs(GifsCmd)` variant, dispatch arm, inventory test update | | `buzz-cli` | `src/client.rs` | `post_json_authed` helper | | `buzz-cli` | `src/commands/emoji.rs` | `scan_shortcodes` + `resolve_emoji_tags_for_content` + tests | | `buzz-cli` | `src/commands/messages.rs` | Emoji scan + tag injection in `cmd_send_message` + seam tests | | `buzz-cli` | `README.md` | `buzz gifs` section + emoji-in-messages note | | `buzz-sdk` | `src/builders.rs` | `build_message` gains `emoji_tags` param + tests | | `buzz-acp` | `src/pool.rs` | Update `build_message` call site (`&[]`) | | `buzz-acp` | `src/setup_mode.rs` | Update `build_message` call site (`&[]`) | | `countdown-bot` | `src/main.rs` | Update `build_message` call site (`&[]`) | Relates to: https://buzz.block.builderlab.xyz — buzz-team channel thread on agent GIF/emoji support --------- Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> |
||
|
|
ac5a18697c |
feat(desktop): add persistent Bestie experience (#7223)
## Summary - add a persistent, community-scoped Bestie designation for local managed agents - surface the designated agent in the sidebar, agent library, profile actions, message toolbar, and draggable floating shortcut - bloom the floating shortcut into a lightweight compact composer that reuses the normal DM timeline, reactions, presence, and send behavior - support message handoff with a bounded snapshot and a full Buzz thread link so the agent can retrieve the complete conversation ## UX details - the floating avatar and expanded panel stay above app chrome and drag as one aligned surface - closing the expanded panel returns it to its top-right anchor - each mini-composer opening starts visually fresh while messages sent during that opening remain conversational - the designated Bestie's duplicate DM entry is hidden from the regular DM list - Bestie actions are suppressed inside the mini timeline to avoid recursive handoff ## Reliability and maintainability - preserve existing retention database paths across upgrades - serialize assignment and deletion, clearing matching assignments across community scopes before an agent is removed - fence async conversation resolution against workspace and assignment changes - validate stale assignments against existing local agents before hiding DMs - share lightweight assignment state across agent cards and keep protected-feature behavior out of the shared timeline API ## Testing - `pnpm --dir desktop test` — 5,886 tests passed - `pnpm --dir desktop exec tsc --noEmit` - `pnpm --dir desktop exec biome check ...` - `cargo clippy --manifest-path desktop/src-tauri/Cargo.toml --all-targets -- -D warnings` - focused native retention and Bestie assignment/command tests — 34 passed - `VITE_BUZZ_BESTIE=1 pnpm --dir desktop build:e2e` - `VITE_BUZZ_BESTIE=1 pnpm --dir desktop exec playwright test --project=smoke tests/e2e/bestie.spec.ts` - pre-commit and differential pre-push hooks ## Rollout The UI remains gated by the `bestie` build feature. Screenshots covering setup, empty, assigned, floating, and message-handoff states are included in the PR discussion. --------- Signed-off-by: Arjun Mahanti <arjun@squareup.com> Co-authored-by: Codex <noreply@openai.com> |
||
|
|
6f6093243e |
fix(desktop): harden profile batch and thread-reply fetches against relay slowness (#7188)
Three client gaps turn transient relay failures into permanent UI degradation. Under a slow or rate-limited relay: 1. A cold channel's profile batch exhausts its single retry and leaves raw npubs + broken mention chips until the user manually kicks the channel. 2. A thread opened from a notification trusts a successful-but-empty reply read as authoritative and never retries. 3. A rate-limited `CLOSED` on a history subscription immediately rejects the caller rather than retrying after the rate-limit window. All three are addressed without changing global query defaults or the happy-path behavior. ## Changes **Fix 1 — cold profile batch resilience** (`useUsersBatchQuery`, `desktop/src/features/profile/hooks.ts`) Override `retry: 3` with exponential backoff and error-gated `refetchOnWindowFocus: (query) => query.state.status === "error"`, scoped to this query only. The global defaults (`retry: 1`, `refetchOnWindowFocus: false`) are intentional for other queries and are unchanged. After the retry budget exhausts, a window-focus event (e.g. channel-switch) recovers the query automatically — but only when it is already in an error state, preventing unnecessary refetches for successful batches. **Fix 2 — stale-empty thread reads** (`useThreadReplies.ts`, `ChannelScreen.tsx`) Add optional `expectedEventId` parameter. When a completed paged fetch does not contain the expected event, throw `ThreadExpectedEventMissingError` so React Query's built-in retry machinery handles it rather than caching an authoritative empty. `ChannelScreen` passes `threadScrollTargetId` (the notification-linked reply ID) as `expectedEventId`. When notification routing changes `expectedEventId` while the same thread root is already mounted (same query key), an explicit `invalidateQueries` in a `useEffect` triggers a fresh validation pass. The `useEffect` is declared after `useQuery` so TanStack's internal options-update effect installs the new `queryFn` closure first; the refetch therefore uses the current `expectedEventId` rather than the previous null closure. For the cold-start race (target arrives before the first page returns), the effect detects `fetchStatus === "fetching" && status === "pending"` and calls `cancelQueries().then(invalidateQueries)` so the obsolete in-flight response cannot settle as authoritative before the new target's validation closure is active. The query-fn tracks consecutive fetch attempts per target. On attempt 3, it adds the target to `exhaustedTargetsRef` before calling `loadThreadReplies`. `loadThreadReplies` sees the target in the exhausted set and returns the fetched replies directly rather than throwing — the terminal attempt always resolves to success. No re-entrant scheduling: the resolution is synchronous inside the query function itself. Deleted/moderated targets never lock the thread in a terminal error surface. **Fix 3 — CLOSED recovery for history subscriptions** (`relayClosedRecovery.ts`, `relayClientSession.ts`, `relayClientShared.ts`, `relayGateBoundary.ts`) On a rate-limited `CLOSED` the subscription previously rejected the caller immediately. Store `filter` and `timeoutMs` on `HistorySubscription`, then on rate-limited `CLOSED` re-register under a fresh `subId` and defer `sendReq` until the rate-limit window clears — matching the live-sub recovery design already present in `relayClosedRecovery.ts`. Bounded to 3 attempts; exhausted retries reject immediately so callers are never left waiting indefinitely. A new op-timeout guards the retry REQ against a non-responding relay; when the op-timeout fires it sends `CLOSE` for the rotated `subId` (matching the behavior of the original timeout path) so the relay releases the slot rather than counting it against the per-connection cap. ## Tests - `relayClosedRecovery.test.mjs`: behavioral fake-clock tests for history-sub retry, 3-attempt exhaustion, op-timeout CLOSE send + late-EOSE non-regression, rejecting-`closeSubscription` swallowed without unhandled rejection, wiring source assertion (fails if `relayClientSession.ts` drops the `closeSubscription` callback) — 18 tests - `useThreadReplies.test.mjs`: `loadThreadReplies` unit tests (throw/exhaustion-guard); behavioral hook tests via real `QueryClientProvider` + `renderHook`: exhaustion-resolves-to-data, settled null→target change retries on missing-target page and lands target data (fails if `invalidateQueries` is removed OR if `useQuery` is moved after the `useEffect`), cold-fetch cancel-then-invalidate (gated fetcher — released after rerender, stale empty discarded, replacement fetch settles with target); ChannelScreen wiring source assertion — 9 tests - `profileBatchResilience.test.mjs`: source assertions for `retry: 3`, `retryDelay`, error-gated `refetchOnWindowFocus`, and unchanged global defaults — 2 tests --------- Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> |
||
|
|
187df22252 |
docs(nip-fi): adopt deny-until-TTL and extend enforcement to HTTP ingress (#7254)
Spec revision following two decisions: Option B admin deny (2026-09-02) and the HTTP ingress ruling (2026-09-02). Revises `docs/nips/NIP-FI.md` only. Follows #7214 (merged spec v2). ## What changed ### Admin disconnect: session-only → deny-until-TTL The disconnect operation proceeds in two steps, in order: 1. Insert a memory-resident deny entry keyed by `(iss, target_pubkey)` with absolute expiry `until` — atomically combined with the `(iss, jti)` replay-identity reservation as one all-or-nothing mutation. If the deny set is at capacity (per-issuer bound), the relay rejects `503`; neither the jti nor the deny entry is recorded, and the caller may safely retry the same signed command. 2. Close all live WebSocket connections for the target pubkey, synchronously. The single atomic admission mutation (jti reservation + deny-entry insertion) lives inside `VerifyCommandJwt` step 7, after all pure authorization checks. The endpoint only closes sessions on success. This ordering ensures a capacity failure leaves no state behind and makes the retry-safe 503 contract implementable. The deny set is RAM-cache only — no durable storage, no schema changes. The same operational posture as the JWKS snapshot. A relay restart MAY forget active entries; the issuer SHOULD re-push still-active deny entries on observed restart (same publish/cache pattern as JWKS). If the issuer stops issuing assertions and re-push completes before any expired-entry reconnection attempt, residual exposure after restart is bounded by `max(0, min(exp, iat + maximum_assertion_age) - now)`. If the issuer continues issuing or re-push does not complete in time, that formula does not apply and access may continue beyond it. **`until` claim:** Required on the disconnect command JWT. Because an assertion accepted at the future-skew boundary (`iat <= now + skew`) remains valid until `iat + maximum_assertion_age`, the latest possible authority deadline is `now + skew + maximum_assertion_age`. The relay enforces `until <= now + skew + maximum_assertion_age`. A value above this ceiling rejects `400`; a past `until` still closes live sessions — absent an active same-key entry it creates no future denial, while an active entry remains unchanged under the merge rule. **Capacity and eviction (per-issuer):** The relay MUST bound the deny set size **per issuer**. Capacity exhaustion under one issuer MUST NOT cause rejection of another issuer's commands; the `503` capacity check is evaluated against the command's own issuer bound. Implementations MUST evict only expired entries; when an issuer's partition is at capacity and all entries are still active, the relay MUST reject the new command `503` without removing any existing entry. There is no LRU eviction of active denies. **Issuer-global deny:** The deny entry applies to admission across all communities served by the relay under that issuer. Identity-level revocation is intentionally not community-partial. **Cross-replica propagation:** In a deployment with multiple relay processes, the deployment MUST propagate both the session-close and the deny entry to every process serving admissions for the issuer's communities. The mechanism is deployment-defined (e.g. the existing inter-process message bus, same posture as JWKS convergence). Propagation is asynchronous with no protocol-level completion bound. The issuer re-push duty is the recovery path for lost propagation, exactly as for relay restart. **Response shape:** A successful disconnect responds `{"disconnected": true}` regardless of how many sessions were closed. No session count is returned; a count would aggregate activity across communities and constitute an information leak. **Admission procedure:** Step 5 registers the session's proven `k` before the deny-set check (new step 6) — ensuring any connection that straddles a concurrent disconnect is caught by one side or the other. `FI-TRACE-DENY-SET` oracle covers the per-issuer capacity rule and the straddling termination requirement. ### HTTP ingress enforcement Without explicit enforcement, a protected HTTP surface (bridge, invites, media, git) with NIP-98-only authorization allows a principal holding an active key to mint fresh NIP-98 events indefinitely — NIP-98 proves key possession only, not identity. Without assertion verification there is no expiry bound; the key remains valid for as long as it is accepted. **Pairing rule:** in enforce mode, a protected HTTP request MUST carry both: ``` Authorization: Nostr <base64-NIP-98-event> Nostr-Federated-Identity: Bearer <compact-JWS> ``` The NIP-98 pubkey MUST equal the assertion `nostr_pubkey` claim. Missing, mismatched, or invalid evidence of either kind denies, fail closed. **Verification:** reuses `VerifyAssertion` unchanged — offline, same JWKS, same claim requirements, same denial classes. **Per-request:** HTTP is sessionless; every request re-verifies. No session lifetime, no cached admission. The cumulative residual bound applies per request. **Deny-set applicability:** the deny-until-TTL entry introduced above is consulted per HTTP request identically to WebSocket admission. **Protected surface:** deployment-configured set of routes, fail-closed default (unclassifiable routes treated as protected). No normative route names in the spec. `FI-TRACE-HTTP-INGRESS` oracle added. Security considerations updated with HTTP ingress bypass analysis. NIP-98 source reference added. ### Other changes - `authorization_denied` rejection table row updated to "active deny-set entry for pubkey". - Discovery: `maximum_residual_upstream_revocation_seconds` remains `null` — the deny-until-TTL model is best-effort RAM state and provides no unconditional finite revocation bound. - Rejection and privacy: explicit sentence for HTTP denial path. - Client-attached transport: opening sentence generalized to cover both WebSocket and HTTP. ## Scope Single file: `docs/nips/NIP-FI.md`. No code changes. References #7214. Channel: buzz-enterprise-identity-spec-v2 (#a6fe0b1c-987a-43c5-a974-71ee36678d78). --------- Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> |