Converging review found this at P1, two lenses independently, both with
probes against the installed query-core source. Reproduced here.
The prefetch marker records that a channel had a prefetch requested, not
which fetch is in flight. On a cold channel `prefetchQuery` does not start a
fetch at all — TanStack dedupes onto the request already running (query.js
honours `cancelRefetch` only when `state.data !== undefined`). So when the
hover dwell fires after the click, which is the ordinary case for a decisive
click inside the 100ms dwell, the marker labels the mounted timeline's own
fetch a "prefetch", and the post-subscribe refresh cancelled it.
Probe, real modules and a real QueryObserver:
no hover: { calls: 1, mountFetchAborted: false }
hover then click: { calls: 2, mountFetchAborted: true }
That is two sequential relay round trips with the skeleton held across both,
making a hovered channel slower to open than an unhovered one — on the exact
path this stack exists to speed up.
The refresh now waits for the in-flight fetch instead of cancelling it. The
gap it was closing still closes: the invalidate below re-reads the window
after the subscription exists, it just no longer discards the round trip the
user is watching. #6455's one-fetch contract for a cold channel with no
prefetch is unchanged.
The guard's own comment claimed a cold mount fetch "is still parked on the
hydration gate" and so could not be affected. That is false in steady state —
`channelHeadHydration` is an already-resolved promise after boot.
Adds the regression test; it fails on the cancelling version.
Signed-off-by: Max Lampert <maxwell@squareup.com>
A deliberate hover on a sidebar channel row (100ms dwell, so pointer
scrubbing never fires) warms that channel's message window, making the
subsequent click a cache hit instead of paying the relay fetch on the
switch path.
The window query options move into a shared factory so the prefetched
entry is byte-identical to what the mounted query reads; the prefetch
respects the window's staleTime, so hovering a fresh channel never
refetches. The mock bridge gains a head-fetch probe (cursorless window
reads) so the regression spec can assert dwell-fetches-once /
scrub-fetches-never.
Signed-off-by: Max Lampert <maxwell@squareup.com>
Two skeleton flashes made switching feel clunky:
- Every switch: the deferred timeline snapshot lags the live one for a
frame or two, and that gap painted the full skeleton to mask the
previous channel's rows — a visible skeleton flash on every switch.
The gap is a render-pipeline artifact, not a loading state: it now
renders plain background (new "blank" body surface). Blank blocks the
same behaviors as the skeleton (autoscroll, jumps, unread pill,
scrollback), only the visual differs. Cold loads keep the skeleton.
- Stale revisits: the loading latch reset on every switch, so re-entering
a channel past the messages staleTime refetched on mount with the
unsettled skeleton branch active — a skeleton over a fully cached
timeline for the whole relay round-trip. Channels that settled once
this session now render their cached rows stale-while-revalidate
(session registry, reset via resetCommunityState like every other
community-scoped singleton).
The revisit spec forces staleness by shifting Date.now past the 5-minute
window and asserts the skeleton never mounts over cached rows; the
switch-gap behavior is covered by the timelineSnapshot unit suite.
Signed-off-by: Max Lampert <maxwell@squareup.com>
Entering a channel whose thread-summary facepiles hydrate after first
paint (profiles resolving late) visibly shifted the timeline: every row
above a summary slid ~3px per summary a beat after content painted.
The summary button was inline-flex, so it participated in its wrapper's
line box via its baseline — and that baseline moves when the avatar's
content resolves (Radix fallback text appearing after its delay, or the
avatar image landing). Make the button a block-level flex row so the
wrapper height is padding + button height regardless of avatar state,
and render the facepile fallback immediately (fallbackDelayMs=0): the
participant starts with no image URL, so the default 200ms delay only
left a blank hole where the initials belonged.
Regression spec pins the wrapper height from first paint through
hydration and the root row's position after the facepile renders, with
the users-batch response deliberately delayed past first paint.
Signed-off-by: Max Lampert <maxwell@squareup.com>
The render-pending marker was moved onto MessageTimeline's always-mounted
wrapper for the switch tracer, which is gone. Tested whether the surviving
benchmark still needs it: reverted the file to main, rebuilt, re-ran all
three scenarios. Medians are unchanged within run-to-run noise —
general->deep-history 395.3 -> 394.3 baseline, 515.4 -> 520.4 at 10k
deep-history->general 173.3 -> 179.3 baseline, 314.9 -> 321.4 at 10k
general->projects 142.1 -> 141.7 baseline, 164.4 -> 169.8 at 10k
projects->general 171.0 -> 179.6 baseline, 282.8 -> 290.5 at 10k
— and membership scaling stays monotonic in every direction. The move was
not load-bearing.
Reverting it removes this PR's last write to the document-wide
data-render-pending namespace that review flagged as the one surviving piece
of the tracer's shape, and it removes an untestable risk: release-smoke.spec.ts
asserts that marker against a local relay, and those assertions cannot run
here (no relay), so a marker change in that file would have shipped unverified.
The PR no longer touches MessageTimeline at all.
Signed-off-by: Max Lampert <maxwell@squareup.com>
Blind review found the guard-refusal branch untested, and a refutation pass
disagreed. The refutation tested the wrong mutation: DELETING the
`allow()` call fails three ordering tests, so it looked covered. Consulting
the guard and discarding its answer passed all ten.
That is the branch that stops a channel switch while a thread edit is
unsaved, so a regression there would have shipped silently. Two tests now
pin it — refused, and refused-while-forced — and the ignore-the-answer
mutation fails both.
Also removes residue the same review found in the tracer teardown:
- a stranded `append_switch_perf_log` arm in the e2e mock bridge, for a
Tauri command deleted two commits ago
- MessageTimeline's marker comment, which justified its placement by a
tracer that no longer exists; its readers are now the perf benchmark and
the e2e readiness specs
- ChannelScreenLoadingFallback's `data-render-pending` wrapper, added
solely for the tracer. Measured benchmark switches are warm, so the lazy
pane never suspends there and nothing asserts the marker. Reverted to
main rather than left as an unowned production DOM node.
Signed-off-by: Max Lampert <maxwell@squareup.com>
The instrument was over half this PR and the source of nearly every review
finding, and the benchmark that actually validates the perf work does not
depend on it: member-heavy-switch.perf.ts times click -> ready itself with
performance.now() and DOM readiness, and never reads a tracer export.
Its remaining value was a dev console line, against a failure mode of
reporting a number that never happened — which is worse than reporting
nothing, because it sends someone chasing a regression that does not exist.
The live baselines it produced are already recorded in the PR description
and stand on their own; they justified the fixes, and the fixes are the
deliverable.
Removed: channelSwitchPerf.ts and its tests, useChannelSwitchTraceMarks,
switch-settle-after-paint.spec.ts, the fetch attribution in the window
queryFn, the trace hooks in commitGuardedNavigation/useAppNavigation, and
the community-init reset.
Kept, because the perf fixes build on them:
- member-heavy-switch.perf.ts plus the inflateChannelMembers bridge knob —
the high-membership benchmark
- useChannelTimelineLoading — the per-channel loading latch
- commitGuardedNavigation — the shared guard/no-op commit flow
- the Projects hydration marker the harness gates readiness on
- routing Pulse note actions through goChannel, so that DM entry goes
through the same navigation guard as every other one
Signed-off-by: Max Lampert <maxwell@squareup.com>
The instrument was accumulating defects faster than it was answering
questions. Nearly every review finding lived in a seam that existed only
because the tracer inferred more than it could observe: a starvation
heartbeat, a per-seam guard matrix, DOM-attribute sniffing with a commit
generation, click anchors threaded by hand through nine async call sites,
and an on-disk JSONL sink with rotation and byte caps. Each fix added a
seam, and the next review found the guard that seam was missing.
The perf work these numbers support matters more than the numbers, so the
instrument is now scoped to what it can measure without inference:
- click (or navigation) -> route commit -> settled paint, as a console line
plus User Timing marks/measures. That is what the Performance panel and
the perf harness read.
- the message-window fetch when it starts inside that interval.
- one drop rule per unmeasurable condition (hidden window, timeout,
superseded, left the surface), each printing why.
Removed: the Rust perf-log sink and its git-revision baking, the click-anchor
threading and every call site that existed only to carry it, the starvation
heartbeat, the roster-fetch attribution and its sequence machinery, and the
commit-generation burst gate.
Scope bounds are now stated in the module doc rather than inferred: callers
that await before navigating are measured from the navigation, a bounded
frame wait replaces the 5s render deadline, and a sample past that bound is
flagged rather than dropped.
Net: 2231 lines removed, 462 added.
Signed-off-by: Max Lampert <maxwell@squareup.com>
Cycle-1 adversarial review findings.
- The starvation heartbeat rescheduled forever for a trace that was never
settled, dropped, or replaced. It hung the unit suite (a test stubs rAF as
setTimeout, so the timer kept node's loop alive) and leaked a frame callback
per frame in production. Bounded to the trace's own liveness budget.
- `routeStillOnChannel` read `location.pathname`, but the app uses hash
history, so it answered false for every real channel route and degraded
route-exit handling to an unconditional abandon.
- Supersession and community reset discarded traces with no record, censoring
the impatient-second-click case — precisely the slow switches worth seeing.
`community-reset` was a declared reason that nothing emitted.
- Drop accounting had no test coverage at all; deleting it left the suite
green. Covered now, verified by reverting each guard.
- `settleChannelSwitchTrace` was not idempotent: a second call re-stamped the
fetch-attribution bound and spawned a duplicate wait chain.
- `anchorDiscarded` reached the console but never the record it documents.
- NewMessageScreen back-dated the anchor across the message publish, so send
latency was reported as switch latency.
- The canceled-fetch test restated the queryFn's reconcile-then-attribute
ordering instead of exercising it; inverting production left it green. The
ordering now lives in `reconcileAndAttributeChannelWindow`, which the test
drives directly.
Signed-off-by: Max Lampert <maxwell@squareup.com>
Three review findings against the tracer's honesty contract:
- The readiness -> paint frame was unguarded. `awaitDeferredCommit` checks
age, frame gap, and visibility on every frame it drives, but the one rAF
it queues before `record()` checked neither age nor gap, so a suspension
in that seam (App Nap fires no visibilitychange) recorded the whole
absence as a clean switch.
- DM actions await `open_dm` before `goChannel`, so that relay round-trip
sat outside the measurement; Pulse's note actions used raw `navigate` and
produced no measurement at all. Callers now capture the click anchor and
hand it to `goChannel`, and Pulse routes through `goChannel`.
- A `navigate()` rejection left the trace it opened active, so an untraced
re-entry could settle the failed attempt. Cancellation is by trace
identity, so a newer same-channel attempt is never erased.
Signed-off-by: Max Lampert <maxwell@squareup.com>
Blind-review round 18 (Opus xhigh) finding: 26ee8982b moved the
measure's origin to the input event's timeStamp but left the start mark
at handler-dispatch time, so the Performance panel showed the measure
beginning before its own start mark by the input delay — two different
switch durations from one instrument. The mark now carries
startTime: startedAt, pinned by asserting mark and measure share the
anchor.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Max Lampert <maxwell@squareup.com>
Blind-review round 17 (Fable high) finding: startedAt sampled
performance.now() at click-handler dispatch, silently excluding input
delay — a click queued behind a long task under-reported by the whole
queueing time, unbounded, in exactly the contention regime the tracer
exists to expose. begin() now anchors at the dispatching event's
timeStamp when one is present (window.event is set only during
synchronous dispatch, so stale timestamps cannot leak in from async
continuations; min() guards skewed clocks).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Max Lampert <maxwell@squareup.com>
Blind-review round 16 (Opus high) P3s:
- A same-task unmount-then-renavigate to the same channel left the exit
cleanup's scheduled abandon pending, and its microtask killed the
freshly opened trace (silent lost sample). beginChannelSwitchTrace
now revokes any pending abandon for its channel.
- The delayed-chunk spec's timing-budget guard hit expect(null) with a
raw matcher error when a truncated record had already cleared the
start mark — the one case its message exists for. Boolean form now
covers the null.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Max Lampert <maxwell@squareup.com>
Blind-review round 15 (Fable xhigh) finding: a clean measure recorded
behind the held chunk — the exact regression under test — clears the
start mark and nulls elapsedSinceClick, so the timing-budget assertion
failed first with a 'rerun, not a tracer bug' message stating the
opposite of the truth. The contract assertion now runs first.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Max Lampert <maxwell@squareup.com>
Blind-review round 14 (Opus medium) P3s:
- settleChannelSwitchTrace guarded window but then read document
(visibilityState, querySelector) unguarded — the document guard in
ensureVisibilityWatcher was dead protection. The settle entry gate now
covers both globals.
- The concurrent boundary test folded the rotated generation in behind
if-let, so a regression where rotation never fires under contention
would pass green with all 32 lines in the live file. The read is now
unconditional, and the boundary comment's arithmetic is corrected
(rotation triggers before the 23rd append).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Max Lampert <maxwell@squareup.com>
Blind-review round 13 (Sonnet xhigh) hardening: leavesChannelSurface
used a /channels/ prefix check, so sibling routes (forum posts) counted
as staying on the channel surface and kept a live trace alive. No
currently reachable path turns that into a wrong record — every
re-entry overwrites or drops the trace — but the invariant from
9b462328d should hold structurally, not incidentally. Anything other
than the exact message-view route now drops the active trace.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Max Lampert <maxwell@squareup.com>
Blind-review round 12 (Opus xhigh) findings, both reproduced:
- P1: the round-9 abort gate destructured { signal } in the members
queryFn — merely reading that getter sets React Query's
abortSignalConsumed, switching the roster query to cancel-and-revert
when its last observer unsubscribes mid-fetch. Interrupted switches
discarded rosters that previously landed in cache (the Tauri call
cannot be cancelled, so the work was paid and thrown away), and
A->B->A warm switches repaid a full roster fetch. Replaced with a
per-channel supersession token (openChannelMembersFetch) checked by
traceChannelMembersFetch — stale fetches stay out of the one-shot
slot without touching the signal. Sequences reset with community
state.
- P2: lastFrameAt started null, so the settle-entry -> first-frame
window skipped the starvation guard: a no-visibilitychange suspension
there recorded a truncated measure inflated by the whole stall
(bounded only by the 35s age guard). The gap clock is now seeded at
settle entry.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Max Lampert <maxwell@squareup.com>
Blind-review round 10 (Fable high) finding: the specs' settle polls
accepted a legitimately truncated measure — the tracer honestly hitting
its 5s render-wait deadline on a slow box — and then failed the painted-
rows assertion with a message blaming the tracer. Both tests now poll
for clean measures only and fail truncated-only runs with an explicit
harness-timing message.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Max Lampert <maxwell@squareup.com>
Blind-review round 9 (Opus high) findings:
- P2: the members queryFn attributed unconditionally, so a roster fetch
superseded by a live join/leave invalidation could claim the trace's
one-shot membersFetch slot with a stale count and duration. It now
checks signal.throwIfAborted() before attributing — same rule as the
window fetch's reconcile abort gate.
- P2: a permanently dead sink (unwritable log dir, stale directory at
the log path) was indistinguishable from no traced switches: console
lines kept flowing while every append failed into .catch(() => {}).
The first persistence failure now warns once.
- P3: during a suspension that fires no visibilitychange, the deferred
render-pending marker stays latched, so the settle wait recorded a
truncated measure inflated by the whole stall (bounded only by the
35s age guard). A single inter-frame gap beyond 3s — beyond any real
main-thread stall — now drops the sample.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Max Lampert <maxwell@squareup.com>
Blind-review round 7 (Opus medium) findings:
- P2: a trace begun while the channel route was still resolving never
mounted ChannelScreen, so no route-exit cleanup existed; leaving for
Home and history-backing into the channel within 30s settled the
stale trace with the time spent away. Any committed non-channel
navigation and any history traversal now drop the active trace at the
navigation layer (dropActiveChannelSwitchTrace), which covers traces
no component ever owned. Same-channel navigations keep the live
trace.
- The delayed-chunk spec now asserts no CLEAN measure while the chunk
is held (the tracer honestly emits a truncated one if its 5s deadline
passes) and guards its own timing budget explicitly so a slow CI box
fails with the real reason.
- The rotation-degradation test blocks rotation with a non-empty
directory instead of chmod, so it also holds when tests run as root
(containers), and now runs on Windows too.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Max Lampert <maxwell@squareup.com>
Blind-review round 6 (Sonnet xhigh) finding: a transient lock on the
rotated generation (AV/EDR or an editor, chiefly Windows) failed the
whole append — and since the live file stays oversized, every later
append re-entered the same failing branch, silently dropping every
record until the lock cleared. Rotation is now best-effort: on failure
the line appends unrotated and the size cap re-applies once a later
rotation succeeds. Unix regression locks the directory and asserts the
line survives.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Max Lampert <maxwell@squareup.com>
Blind-review round 5 (Fable medium) finding: a longtask trailing switch
N is delivered by the PerformanceObserver in a later task and landed in
switch N+1's freshly reset array, inflating its longtask lines. Drain
takeRecords() and discard before each sample's reset.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Max Lampert <maxwell@squareup.com>
Blind-review round 4 (Opus xhigh) findings:
- A not-pending frame landing past the wait deadline is rAF starvation
(system suspend, App Nap — no visibilitychange), not render time: a
fast settle followed by a stall recorded up to 34s as a clean switch
under the click-anchored 35s age guard. Such frames now drop.
- Visibility accounting now spans the whole trace: one module-level
visibilitychange watcher timestamps transitions, and any transition
since the click (not just during the settle wait) drops the trace —
a window minimized while the fetch was in flight no longer records
its absence as switch time. Replaces the per-settle listener.
- begin() now clears the previous switch's settled mark and measure
too, so a consumer polling the User Timing buffer mid-switch can
never read the prior switch's entries as the current one's.
- The goChannel comment no longer claims every channel navigation
funnels through it: Pulse startDm navigates to the channel route
directly and is untraced.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Max Lampert <maxwell@squareup.com>
Blind-review round 3 (Sonnet) finding: beginChannelSwitchTrace marked
unconditionally but only record() cleared, so every abandoned/dropped
trace (forum visits, route exits, hidden-window and frame-starved
drops) leaked a permanent buzz:channel-switch:start entry. Clearing
the previous start mark at begin bounds the buffer to one entry no
matter how the prior trace ended.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Max Lampert <maxwell@squareup.com>
Blind-review round 2 (Opus) findings:
- P1: rAF suspends in hidden windows, so a queued settle fired only on
the user's return and charged the whole absence to the switch as a
clean record (reproduced at runtime). Settle now drops when the window
is already hidden, poisons the wait on visibilitychange, and drops any
trace older than the entry timeout plus the render wait — nothing
legitimate can reach that age.
- The JSONL append is a single write_all: writeln! issues two write
syscalls and the lock is process-local while the log path is not, so a
second process sharing the log dir could interleave mid-line.
- The User Timing buffer keeps only the latest switch's mark/measure —
desktop sessions run for weeks and the buffer is never GC'd.
- The route-commit mark moved to a layout effect so it stamps commit
time, not first-paint time.
- Comment honesty: the member-heavy harness now states that mock-mode
IPC hands live objects (parse cost not exercised) and that the
Projects hydration marker guards cold/invalidated samples only; the
goChannel trace-anchor comment describes what the same-channel guard
actually skips; the Rust concurrency test's byte arithmetic corrected.
- Longtask sampling drains PerformanceObserver.takeRecords() at sample
time so a longtask ending just before the resolve frame isn't lost.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Max Lampert <maxwell@squareup.com>
Address the three findings from the 2026-08-24 re-review of #6455, plus
one finding from a follow-up adversarial review:
- The switch trace now opens inside commitGuardedNavigation, only after
the navigation guard accepts — a refused click can no longer leave an
orphan trace that a later history navigation would settle with
inflated time. The commit flow is extracted and dependency-injected;
new unit tests pin guard ordering and the refusal→history-back case.
- The perf-log 4 KiB cap now bounds bytes on disk: one byte is reserved
for the newline writeln! appends. New boundary test measures the file.
- The channel↔Projects benchmark gates readiness on a new
data-projects-hydrating marker (driven by the surface's query fan via
isLoading, so disabled queries never wedge it) instead of the shell
header, verifies member inflation on both inflated channels, uses a
prefixed ready-selector for general, and reports each switch direction
as its own median so a one-leg regression cannot hide.
- A settle wait that hits its 5s deadline while the render is still
pending now records with settleWaitTruncated instead of posing as an
honest settled paint — the >deadline tail is what the tracer exists
to expose. Also removes the dead ChannelSwitchFetchTrace type.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Max Lampert <maxwell@squareup.com>
Address the three findings from the 2026-08-24 review of #6455:
- The lazy ChannelPane's Suspense fallback now carries the
data-render-pending marker, so a switch trace can no longer settle
while the pane chunk is suspended. New Playwright regression holds
the chunk and asserts no measure lands until rows paint.
- Route-exit trace abandonment is deferred one microtask and canceled
by the effect re-setup, so StrictMode's dev-only effect replay no
longer kills a just-opened trace. New jsdom regressions run the real
react-dom dev replay; the settle spec also passes against the Vite
dev runtime.
- The perf-log 4 KiB cap is enforced on the final serialized line, and
BUZZ_PERF_LOG_LABEL is truncated to 128 bytes at a char boundary, so
a runaway label can neither inflate the sink nor kill every record.
Also extracts the timeline-loading latch + trace marks from
ChannelScreen into useChannelTimelineLoading (file-size ratchet).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Max Lampert <maxwell@squareup.com>
Add an inflateChannelMembers mock-bridge knob (channel name → target
member count; synthetic hex-pubkey members appended once, on first
channel read) and a member-heavy-switch perf spec that measures warm
switch wall time and longtasks for channel↔channel and
channel↔Projects at baseline / 2k / 10k members per channel, holding
message volume constant at 150 rows. Method mirrors
warm-switch-markdown.perf.ts: in-page click + rAF polling, 4x CPU
throttle, medians over 16 switches after an untimed warmup round-trip.
Run from desktop/:
pnpm build:e2e
npx playwright test --config=playwright.perf.config.ts member-heavy-switch.perf.ts
Signed-off-by: Max Lampert <maxwell@squareup.com>
Add a single-active-trace instrument for channel navigation: goChannel
opens the trace, ChannelScreen marks route commit and settles it after
the timeline loading latch clears (double rAF so the frame painted).
The two relay fetches that can sit on the switch path — the message
window and the member roster — are attributed to the trace when they
run during it; cache-served switches log "cache".
Each switch emits one [switch-perf] console line plus User Timing
marks/measures (buzz:channel-switch:*) so before/after comparisons work
identically in a dev build, the Performance panel, and Playwright perf
specs.
Signed-off-by: Max Lampert <maxwell@squareup.com>
## Summary
- inject bounded project-home identity and repository context into
managed agent sessions
- add project-aware CLI flows for creating projects, repositories,
issues, and related channels
- match project homes through the existing relay query surface, then
filter channel metadata client-side
This is Part 1 of the channel-first Projects stack. Part 2 contains
project creation and model foundations.
## Testing
- `cargo fmt --all -- --check`
- `cargo clippy -p buzz-cli -p buzz-acp --all-targets -- -D warnings`
- `cargo test -p buzz-cli -p buzz-acp` — 1,184 tests passed, 1 doc test
ignored
- full pre-push gate passed
## Post-Deploy Monitoring & Validation
- validate project-home agent context and project-aware CLI commands
against a staging relay
- healthy signals: project context matches the active channel, explicit
repo coordinates remain stable, and normal channels receive no project
block
- failure signals: cross-channel project context, duplicate project
creation, or commands targeting an unrelated repository; mitigate by
reverting this PR
---------
Signed-off-by: Thomas Petersen <thomasp@squareup.com>
Signed-off-by: Wrench <0eabe6ea5758c1e4c5b68cea4ac42b32c479072883cb28da8110b4a47c32b9a1@buzz.block.builderlab.xyz>
Co-authored-by: Wrench <0eabe6ea5758c1e4c5b68cea4ac42b32c479072883cb28da8110b4a47c32b9a1@buzz.block.builderlab.xyz>
## Why
PR #6503 bundled a broader context compiler with several prompt-shape
and telemetry changes, making it difficult to isolate whether paired
XML-like top-level boundaries improve agent behavior. This keeps the
follow-up experiment focused on framing only.
## What
- Replace top-level standing and per-turn bracket headers with paired
tags while preserving inner section content and event/message framing
- Carry only metadata already present in dynamic headers as attributes,
such as thread counts, truncation, event type, and batch count
- Preserve every section body verbatim so the model executes the exact
bytes reviewed in Desktop
- Keep routing metadata, event content, batching, and delivery behavior
unchanged
- Teach Desktop diagnostics to parse the new framing while retaining
compatibility with archived bracket-framed sessions; ambiguous paired
boundaries render as the complete captured prompt
## Risk Assessment
Medium — the change is limited to agent prompt framing and diagnostic
parsing, but it affects both standing context and every dynamic agent
turn. Bodies and delivery behavior remain unchanged, and compatibility
parsing remains for prior sessions.
## References
- Follow-up to #6503, scoped only to the paired-tag framing hypothesis
## Update — 2026-08-24
- Addressed review feedback by removing entity encoding/decoding from
the execution and diagnostic paths.
- Added regressions for literal closing tags, placeholders,
entity-looking text, ampersands, and authored boundary whitespace.
- Verified `cargo test -p buzz-acp` (805 unit + 9 integration), Desktop
tests (5,412 before the main sync; 5,439 in the pre-push gate), Desktop
typechecking, and Tauri clippy/tests.
- Live local relay startup and message ingestion passed. The live ACP
reply step is blocked locally because installed Goose 1.0.23 no longer
provides the documented `goose acp` command and no alternate ACP adapter
is installed.
---
**Update Aug 25, 12:13 EDT:** Extended paired tags to per-turn top-level
sections.
- Converted context, conversation/thread context, event batches, and
steer/interrupt boundaries without changing their inner bodies.
- Preserved existing header metadata as attributes only; added no new
prompt metadata.
- Verified 806 Buzz ACP unit tests plus 9 integration tests, 5,441
Desktop tests, Desktop typechecking and formatting, and all differential
pre-push lanes.
---
**Update Aug 25, 13:58 EDT:** Addressed framing review feedback.
- Updated operational base instructions to reference the emitted
`<context>` section and added a regression guard against stale
`[Context]` instructions.
- Restored batched steer/interrupt cardinality in Desktop diagnostic
titles with focused coverage for both paths.
- Verified 807 Buzz ACP unit tests plus 9 integration tests, 5,442
Desktop tests, Desktop typechecking and formatting, and every
differential pre-push lane including Tauri clippy/tests.
---
**Update Aug 25, 15:37 EDT:** Hardened legacy transcript compatibility.
- Semantic standing-context parsing now requires paired framing at the
start and complete input consumption.
- Historical bracket-framed personas keep literal balanced tag examples
in their original section; malformed or mixed current framing displays
as the complete captured prompt.
- Added regressions for the reported legacy persona case and trailing
unframed text; verified 60 focused parser tests, 5,444 full Desktop
tests, formatting, and typechecking.
---
**Update Aug 25, 16:31 EDT:** Preserved slash-command prompt block
boundaries.
- Desktop now sets aside a valid connector-facing slash-command block
only when the following block begins recognized paired framing, parses
the semantic blocks normally, and retains the command as visible Prompt
metadata.
- Added an end-to-end transcript regression using the production
three-block ACP shape, including the user bubble, author pubkey, event
ID, and grouped prompt context.
- Verified 113 combined transcript tests, 5,445 full Desktop tests,
formatting, typechecking, and all applicable pre-push lanes.
Generated with Codex
---------
Signed-off-by: Salman Mohammed <smohammed@squareup.com>
## Summary
- preserve string Nostr `sig` values in normal `messages get`, `messages
thread`, `messages search`, and `feed get` output
- omit absent or non-string signatures instead of fabricating empty
values
- keep compact output at exactly `{id, content, created_at}` and update
CLI/managed-agent contract docs
## Validation
- `cargo test -p buzz-cli --lib` (367 passed at final head
`a2498faa0e07f90efc0ab0165606ba3b048327c7`)
- release candidate live-relay matrix: 49/49 normal events independently
recomputed to their NIP-01 IDs and passed BIP-340 verification; all four
compact commands preserved exact three-key output and ordering
- `git diff origin/main...HEAD --check`
- independent adversarial review: clear, no findings
## Compatibility
The normal JSON change is additive for valid relay events. In-repo
consumers use tolerant field lookup. Compact output is unchanged and
locked by regressions for both message and feed formatting.
Generated by Carl, an automated contributor working with Wes.
---------
Signed-off-by: Carl <9d00794d3df50972eb8b615511783cab12a77a8fd5dd5edd58073ec73b54bd8b@buzz.block.builderlab.xyz>
Co-authored-by: Carl <9d00794d3df50972eb8b615511783cab12a77a8fd5dd5edd58073ec73b54bd8b@buzz.block.builderlab.xyz>
## Why
Finish the replaceable-event slice of [tracker
#2](https://github.com/TheSentinel454/buzz/issues/2) and [domain issue
#6](https://github.com/TheSentinel454/buzz/issues/6) without disturbing
the runtime/store boundary established by #6660 and #6668. PR #6700 has
merged; this PR now targets current main containing its replacement lock
and transaction observability.
## What
- Move `Db::replace_addressable_event`, its SQL, lock/transaction
instrumentation, and focused addressable/parameterized replacement tests
from `lib.rs` to `replaceable.rs`
- Preserve the transaction-required parameterized API, replacement
ordering, rollback semantics, mention indexing, and exactly one
datastore span per public operation
- Keep implementation and focused PostgreSQL tests co-located; follow-up
review removed the dedicated replaceable ownership source guard as low
value
## Stack
- Exact base: main at f249710331
- Exact head: codex/issue-6-finish-replaceable-store at
da018405cc
- Tracker: https://github.com/TheSentinel454/buzz/issues/2
- Domain: https://github.com/TheSentinel454/buzz/issues/6
- Test/span acceptance: https://github.com/TheSentinel454/buzz/issues/17
and https://github.com/TheSentinel454/buzz/issues/19
## Non-goals
- No SQL, schema, retry, timeout, lock ordering, transaction boundary,
or client-visible behavior changes
- No store traits, domain-handle redesign, `PgExecutor` migration, raw
pool accessor, new crate, or directory reorganization
- No changes to, retargeting of, or merge action on PR #6700
## Risk Assessment
Low. This is a mechanical ownership move with unchanged signatures and
SQL. The primary review risk is losing or nesting instrumentation,
covered by exact moved PostgreSQL tests and the cumulative
production-diff audit.
## Blox Verification
Author workstation: `buzz-tornquist-issue-2-store-stack` (`2046520`),
exact head `2de5444e14606ce6912d1bb932bc88bffa379a77`.
- `cargo fmt --all --check` — passed
- `cargo clippy -p buzz-db -p buzz-relay --all-targets -- -D warnings` —
passed
- `cargo test -p buzz-db --quiet` — 108 passed, 200 ignored; both
integration source guards passed
- Native PostgreSQL: `cargo test -p buzz-db replaceable::tests:: --
--ignored --test-threads=1` — 11 passed
- `cargo test -p buzz-relay --lib
api::mesh_demo::tests::demo_join_forwarded_arm_round_trips_echo --
--exact --test-threads=1` — passed
- Full serial relay suite was also exercised: 908 tests passed but the
existing load-sensitive mesh demo test returned 504 under suite load,
matching PR #6700's documented baseline; it passed in isolation and this
diff does not touch that subsystem
Independent exact-head Blox review: `buzz-tornquist-pr-6777-review`
(`2047422`) at `2de5444e14606ce6912d1bb932bc88bffa379a77`.
- Review findings — none (critical, important, or minor)
- `cargo fmt --all --check` and the ownership guard — passed
- `cargo clippy -p buzz-db -p buzz-relay --all-targets -- -D warnings` —
passed
- `cargo test -p buzz-db --quiet` — 108 passed, 200 ignored; source
guards passed
- Native PostgreSQL moved suite — 11 passed
- `cargo test -p buzz-relay --lib` — 909 passed, 48 ignored
Generated with Codex
## Superseded pre-comment restack verification
PR #6700 merged before publication completed. This layer was restacked
onto current main through the exact parent named above; the final
cumulative tip is 2ddcc8a29f95c8c8c9cb87bb6cf59335f2190fae. Cumulative
author gates passed: formatting and diff checks; buzz-db and buzz-relay
all-target clippy with -D warnings; DB lib 111 passed / 200 ignored;
ownership 22/22; observability 1/1; the full isolated PostgreSQL domain
matrix; and relay lib 910 passed / 49 ignored.
- Workstation: `buzz-tornquist-pr-6777-final-review` (`2057617`), fresh
shallow checkout
- Base: `f24971033178926153b49d320bd876d15d9cb2bf`
- Head: `ffbeaaf00810aa359ab85818ff4820f92263e45f`
- Findings: none
Reviewed `base..head` for the replaceable-store extraction. The
addressable replacement SQL, stale/duplicate outcomes, advisory-lock
ordering, transaction rollback/commit boundaries, mention-index
atomicity, and public `Db` signature are retained in `replaceable.rs`.
The PR #6700 transaction timer remains around the same logical
transaction through `TransactionTimer::observe`, and the public
operation has exactly one datastore span. Focused addressable and
parameterized tests moved with the implementation; the ownership guard
excludes duplicates from `lib.rs`.
Verification: format and diff checks passed; `buzz-db --all-targets`
clippy passed with `-D warnings`; DB lib tests passed (111 passed, 200
PostgreSQL tests ignored); ownership (1/1) and observability (1/1)
guards passed; all 11 replaceable PostgreSQL tests passed on native
PostgreSQL 17 with migrations 1-32 successful; relay lib test target
compiled successfully. Final worktree was detached at the exact head and
clean.
Complete evidence archive SHA-256:
`e37f92bbde4e2343196085095b3093b1bc78196be58bc86826325e0e1639d7ea`.
## Comment-addressed restack
Review follow-up removed the dedicated
`replaceable_store_has_single_ownership` source test as requested. No
production code changed.
- Exact base: `f24971033178926153b49d320bd876d15d9cb2bf`
- Exact head: `da018405cc83605362125c2d5e5a3f91492431ac`
- Final cumulative tip: `6fa2f104d42c6ba85bdf62e7ccb74ceaf4a84f67`
- Focused PR verification: formatting and diff checks passed; strict
`buzz-db`/`buzz-relay` Clippy passed; DB lib 111 passed / 200 ignored;
all 11 replaceable PostgreSQL tests passed.
- Cumulative Blox gate: formatting and diff checks; strict
`buzz-db`/`buzz-relay` Clippy; DB lib 111 passed / 200 ignored;
ownership 21/21; observability 1/1; every moved PostgreSQL test; relay
lib 910 passed / 49 ignored.
- Independent re-review at this exact head: no findings; fresh
exact-parent/head Blox review passed fmt/diff, strict `buzz-db` Clippy,
DB lib 111 passed / 200 ignored, manual one-owner/one-span replacement
checks, observability, 11 replaceable PostgreSQL tests, and relay
compilation.
---------
Signed-off-by: tornquist <tornquist@squareup.com>
## Why
Long community hostnames can make the native Community select exceed the
Admin feedback filter grid, pushing the Received and Status controls
outside the filter bar and creating horizontal overflow.
## What
- Allow filter labels and selects to shrink within their grid tracks
- Add a browser regression test covering an unusually long community
hostname
## Risk Assessment
Low — this is limited to Admin feedback filter sizing and preserves the
existing desktop and mobile grid layouts.
## Before
The Community select expands past the filter bar and pushes the Received
and Status controls outside it.

## After
Long community names remain contained, keeping all four controls inside
the filter bar.

## References
- Verified on Blox at the local checkout revision: Admin checks passed
and all 9 Playwright tests passed
Generated with Codex
## Summary
- Stop replaying the selected-agent pulse when a remembered agent
remains addressed across a send.
- Preserve the existing pulse for genuine selections and shake feedback
for failed sends.
### Related issue
Reported in Buzz:
buzz://message?channel=2745947d-f905-4f31-8a3f-fc4485d3c36c&id=0cffab376186be9cc4e7f29027352cc1c7941aead48b3fed948f36181f4a59af
### Testing
- `cd desktop && pnpm test` (5,556 passed)
- `cd desktop && pnpm typecheck`
- `cd desktop && pnpm check`
- `cd desktop && pnpm build:e2e`
- `cd desktop && pnpm exec playwright test
tests/e2e/persistent-agent-audience.spec.ts --grep 'always-mentioned
agents remain selected|failed always-mentioned send'` (2 passed)
This changes animation timing only; the regression is covered by
Playwright assertions on the pulse and shake versions, while a static
screenshot is unchanged.
Signed-off-by: Matt Toohey <contact@matttoohey.com>
## Summary
- Treat the sidebar temp-channel icon as a fallback behind working,
mute, numeric unread, and thread-unread indicators.
- Keep the header affordance and the sidebar temp icon when it is the
only row status.
- Extend the ephemeral-channel browser scenario across working, idle,
collapsed-sidebar, and unread states.
## Before / After
| Before: five temp icons compete with duration and unread states |
After: all five duration and unread states remain clear |
| --- | --- |
| 
| 
|
## Testing
- `node --import ./test-loader.mjs --experimental-strip-types --test
"src/**/*.test.mjs"` (5,556 passed)
- `tsc --noEmit -p desktop/tsconfig.json`
- `biome check desktop/src/features/sidebar/ui/SidebarSection.tsx
desktop/tests/e2e/channels.spec.ts`
- `vite build --mode e2e`
- Isolated Playwright run of `create ephemeral stream shows sidebar and
header affordances` (1 passed)
## Duplicate check
No matching open PR or issue found for `ephemeral sidebar icon`.
Signed-off-by: Jitter <d14dfe033ef0f809866f9f984de04821b0d900d7652fd85a54776ee40ca3a68f@buzz.block.builderlab.xyz>
Co-authored-by: Jitter <d14dfe033ef0f809866f9f984de04821b0d900d7652fd85a54776ee40ca3a68f@buzz.block.builderlab.xyz>
## Summary
Pasting a link while composer text is selected now turns that text into
a hyperlink instead of replacing it — the behavior people expect from
every other editor.
- `resolveExactLinkPaste` recognizes a clipboard payload that is exactly
one supported link (bare or `<…>`-wrapped `http(s)`, or a Buzz deep
link, canonicalized through the existing composer-link attribute
resolver) and returns the href to apply.
- The paste handler applies that href as a `link` mark over the current
selection, collapses the caret to the end of the newly linked range,
clears stored marks so subsequent typing isn't linked, and refocuses the
editor.
Edge cases handled:
- **No link mark available / selection can't carry marks** — falls
through to the existing replace-with-node path rather than silently
dropping the paste.
- **Selection contains a composer message-link chip** — keeps the old
replacement behavior instead of nesting a link over a chip.
- **Whitespace in the clipboard payload** — `unwrapExactHttpLink` now
rejects any text containing whitespace, so multi-line or "text plus URL"
pastes are not treated as an exact link.
- **Empty selection (caret paste)** — unchanged.
- **Idempotent re-paste** of an already-wrapped link is consumed rather
than doubling up.
## Testing
`desktop/src/features/messages/lib/composerMessageLinkNode.test.mjs`
gains coverage for exact-link resolution (per-shape accept/reject cases,
Buzz canonicalization) and for the paste handler: linking selected text,
canonicalizing Buzz links over a selection, idempotent wrapped-link
paste, fall-through when the selection can't carry marks, all-selection
collapse, chip-containing selections, and preserved caret-paste
behavior.
Full pre-push gate ran green on this branch: `file-size-check`,
`desktop-check`, `desktop-typecheck`, and `desktop-test` (5425 tests, 0
failures).
## Demo
Pasting an `https://` URL over selected composer text turns the selected
words into a hyperlink (instead of replacing them); text typed
immediately after is not linked:
https://github.com/user-attachments/assets/bbc93574-dd9d-4b1b-bad0-53307b6e887c
---------
Signed-off-by: Matt Toohey <contact@matttoohey.com>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <32a2e2c9d428ee08902cab75d956da2c1d235a22d4766b0dd4138bf6e2e5db1d@buzz.block.builderlab.xyz>
## Buzz Desktop release v0.5.20
- **Frozen main:** `8471049c430073474939336dfc6aa98272bc8762`
- **Reviewed candidate:** `95154bee4034ca7a40b33095c2ddbde8c9aa1614`
- **Previous desktop release:** `desktop-v0.5.19`
- **Proposed immutable tag:** `desktop-v0.5.20`
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>
## Summary
- Supersedes #1913 with a KLIPY-hosted URL implementation.
- Adds KLIPY GIF search and trending results to desktop message and
forum composers.
- Keeps selected GIFs hosted by KLIPY; Buzz stores only the external URL
and media metadata (no imeta tag, since relays only accept hash-backed
local `/media/` entries).
- Aligns the Emoji/GIF picker with Buzz's standard segmented control,
theme surfaces, and motion behavior.
## Relay-to-provider boundary
- The relay proxies KLIPY search/share so the `BUZZ_KLIPY_API_KEY` never
reaches the desktop; the key stays server-side behind a redacted `Debug`
impl.
- The dedicated GIF `reqwest` client sets `redirect::Policy::none()`.
Because the API key rides in the request path, following a provider
`3xx` could replay a key-bearing URL to an attacker-chosen host (an
SSRF/key-disclosure primitive). With redirects disabled, a `3xx` returns
as a non-success status that the handlers map to a generic `502`; the
`Location` target is never read or forwarded.
- Admission reuses the established NIP-98, membership, replay, and
per-pubkey rate-limit gates, with an upstream response-size cap and
allowlisting so KLIPY error bodies never cross the relay boundary.
## Accessibility
- Under `prefers-reduced-motion: reduce`, the picker grid renders a
static provider poster (a normalized `jpg` asset) instead of the
animated preview, or a named static placeholder when no poster is
available. It reacts to preference changes while mounted.
`no-preference` keeps the animated preview.
- Selected GIFs carry their title through `ImetaMedia`'s `displayLabel`,
so the composer thumbnail, preview dialog, editor, lightbox, and remove
control all derive one non-empty accessible name instead of an empty
`Attachment ` label. Ordinary hashed uploads keep their existing
hash-derived names.
---------
Signed-off-by: kenny lopez <klopez4212@gmail.com>
Signed-off-by: Kenny Lopez <klopez4212@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
Signed-off-by: Hayt <9e1c23a3fd83f61da34420e4e88ff1b16e45cafcc0cd9019eb07d4ecfa8ca9b0@buzz.block.builderlab.xyz>
Co-authored-by: Princess Donut <b238ea756dee4d98afa5883fc7f1de61eeabe65bf700e3a5a5a80db5e42e2c2b@buzz.block.builderlab.xyz>
Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: Hayt <9e1c23a3fd83f61da34420e4e88ff1b16e45cafcc0cd9019eb07d4ecfa8ca9b0@buzz.block.builderlab.xyz>
**Category:** fix
**User Impact:** Disabling automatic agent mentions now keeps one-time
agent mentions out of the next message draft.
**Problem:** A successfully sent inline agent mention was treated as
eligible for post-send restoration even when **Automatically mention
agents** was disabled, so the agent immediately reappeared in the
composer. **Solution:** Gate the send-success restoration path on the
live preference while leaving explicitly pinned agents and enabled
automatic mentions unchanged.
<details>
<summary>File changes</summary>
**desktop/src/features/messages/ui/MessageComposer.tsx**
Skips the automatic post-send mention restoration path when the
preference is disabled.
**desktop/tests/e2e/persistent-agent-audience.spec.ts**
Reproduces disabling the toggle before sending an inline agent mention
and verifies the next composer is empty while the outgoing mention still
reaches the agent.
</details>
## Reproduction steps
1. Enable **Automatically mention agents** in the composer mention
options.
2. Disable it again.
3. Compose and send `@Agent test` using the inline mention picker.
4. Confirm the message still mentions the agent, but the cleared
composer does not repopulate `@Agent`.
5. Re-enable the preference and repeat; confirm the mention is restored
for the next message.
## Testing
At `a179a2d00a6ac5cf0bc4a1c7c1ef3d5dfa9d0875`:
- Full desktop unit suite: 5,501 passed.
- Desktop check and typecheck passed in the pre-push hooks.
- Desktop file-size ratchet passed in the pre-push hooks.
- Full `persistent-agent-audience.spec.ts` Playwright file: 17 passed,
including preference off (empty composer), preference on (restored
mention), and the existing address-undo journey.
## Visual validation
| Automatic mentions off | Automatic mentions on |
| --- | --- |
| After sending a one-time mention, the outgoing message is preserved
and the next composer stays empty. | After sending, the agent remains
addressed and is restored in the next composer. |
| 
| 
|
<details>
<summary>Original regression</summary>
With automatic mentions disabled, sending `@Alia test` still repopulated
`@Alia` in the cleared composer.

</details>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Co-authored-by: Rizz <302abe414ca6e3134763d2539bfcf145aea2a63fe5f8455204ed602fd40cf381@buzz.block.builderlab.xyz>
The desktop release tooling hardcoded one contributor's personal
identity into every release candidate commit.
`scripts/prepare-desktop-release.sh` committed the candidate with a `git
-c user.name='Wes' -c user.email='wesbillman@users.noreply.github.com'`
override, and `scripts/desktop_release.py` `validate` required the
candidate author to be exactly `Wes
<wesbillman@users.noreply.github.com>` plus a matching `Signed-off-by`
trailer. That leaked from Wes's working setup into the validation
contract in #3568, so a release cut by any other operator was falsely
attributed to and signed off by Wes (as happened on #6828).
## Change
- `prepare-desktop-release.sh`: drop the `-c` identity overrides so `git
commit -s` uses the operator's own configured identity to author and
sign off the candidate. The automation `Co-authored-by` trailer is
unchanged.
- `desktop_release.py` `validate`: replace the exact-Wes checks with
structural ones — the commit author must be non-empty, the body must
contain a `Signed-off-by` trailer whose name and email match the commit
author (honest DCO), and the existing automation `Co-authored-by` regex
check stays. Failure messages remain specific.
- `test-desktop-release-candidate.sh`: the fixture candidate now commits
under the harness's own identity, and a new negative case rewrites the
author to a mismatched identity and asserts the validator rejects it.
Release authorization is bound to the merged PR via the GitHub API in
`scripts/verify-desktop-release-merge.sh`, never the commit author
field, so this does not weaken the trust model. `RELEASING.md` and
`.github/workflows/desktop-release-candidate.yml` reference no author
identity and need no change.
Verified locally: `scripts/test-desktop-release-candidate.sh` passes,
including the new sign-off/author-mismatch rejection.
---------
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
## Problem
Every `docker.yml` run on `main` has failed since
[#6781](https://github.com/block/buzz/pull/6781) merged. That PR added a
"Create deployment eligibility predicate" step to the `merge` job, which
runs `jq` against
`$GITHUB_WORKSPACE/scripts/create-deployment-eligibility-predicate.jq`.
But the `merge` job has no `actions/checkout` step — the workspace is
empty, so `jq` can't open the file and exits `2` (`jq: Could not open
... No such file or directory`). The `build` and `qualify` jobs each
check out the source; `merge` never needed one until this step was
added.
## Fix
- `.github/workflows/docker.yml`: add `actions/checkout` (same pinned
SHA as the other jobs, `df4cb1c` / v6.0.3) as the first step of the
`merge` job.
- `scripts/test-relay-image-eligibility-workflow.sh`: guard the
regression by asserting the `merge` job checks out the source before
building the predicate.
## Verification
CI cannot exercise the `merge` job on a pull request — the job is gated
`if: github.event_name != 'pull_request'`, so it only runs on push to
`main`. The proof is the root cause (missing checkout for a step that
reads a repo file) plus the eligibility workflow test:
`scripts/test-relay-image-eligibility-workflow.sh` passes with the fix
and fails with the new guard's specific message (`merge job must check
out the source before building the eligibility predicate`) when the
checkout is removed.
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
## Buzz Desktop release v0.5.19
- **Frozen main:** `ee6ca5fa28bce04dfecb6717de65b08a57f2ac47`
- **Reviewed candidate:** `ab691bcdaeccaa6698a2199beaba3f6e93daae81`
- **Previous desktop release:** `desktop-v0.5.18`
- **Proposed immutable tag:** `desktop-v0.5.19`
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: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Release Automation <release-automation@users.noreply.github.com>
Require `BUZZ_RELAY_PRIVATE_KEY` on every relay startup and remove the
shared public fallback key completely.
For local development, `just bootstrap` now generates a random key once
in the gitignored `.env` file. Re-running bootstrap preserves that key,
so relay-authored events retain the same identity across restarts. The
local relay recipes reload `.env`, while isolated CI and E2E launchers
pass explicit per-run keys.
Startup fails immediately when the key is missing or invalid, before
connecting to Postgres or Redis. Deployed relays continue to receive
their stable key from the existing chart-managed secret.
Validated with:
- `./scripts/test-ensure-local-relay-key.sh`
- `cargo test -p buzz-relay --bin buzz-relay`
- `cargo clippy -p buzz-relay --bin buzz-relay -- -D warnings`
- `cargo fmt --all -- --check`
- `shellcheck scripts/ensure-local-relay-key.sh
scripts/test-ensure-local-relay-key.sh`
Signed-off-by: Jordan Mecom <jm@squareup.com>
## Summary
- replace the generated Nest's unconditional human author/sign-off rules
with portable guidance that separates authorship, material
co-authorship, DCO certification, and cryptographic signing
- defer attribution to repository-local policy, forbid inferred or
guessed identities, and require inspection of every outgoing commit
- bump the Nest template version so existing installations refresh, with
regression coverage for fresh generation and upgrade preservation
### Related issue
None found. Related runtime identity work exists in #6177, but this PR
is intentionally limited to the generated Nest guidance and its refresh
behavior.
### Testing
- `bin/just desktop-tauri-clippy`
- `bin/just desktop-tauri-test`
- `bin/just file-size-check`
- `git diff --check`
- pre-push hooks: `push-head-scope`, `branch-skew`, `file-size-check`,
and `desktop-tauri-checks`
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <32a2e2c9d428ee08902cab75d956da2c1d235a22d4766b0dd4138bf6e2e5db1d@buzz.block.builderlab.xyz>
**Category:** fix
**User Impact:** Users can place `@Agent` mentions inline anywhere in a
message—even when that agent is also persistently addressed—so separate
agents can receive separate instructions in the same message.
**Problem:** Selecting an agent from the composer mention picker could
turn the selection into persistent addressing instead of leaving an
inline `@Agent` at the cursor. That made persistent addressing and
inline composition mutually exclusive: once the persistent behavior took
over, users lost the clear `@Agent A do X, @Agent B do Y` message
structure they previously had.
**Solution:** Make inline mentioning and persistent addressing
independent behaviors:
- Ordinary mention selections always insert `@Agent` inline at the
current cursor position.
- This remains true when the agent is already persistently addressed;
the persistent audience never blocks or replaces an inline mention.
- If **Automatically mention agents** is enabled, a successfully sent
inline mention may additionally make that agent persistent for later
messages. Existing saved preferences remain respected.
- The dedicated automatic-mention control and primary+Shift+Enter
shortcut continue to add or remove persistent addressing directly.
- For users without a saved preference, **Automatically mention agents**
now defaults off.
<details>
<summary>File changes</summary>
**desktop/src/features/messages/lib/autoPinMentionedAgentsPreference.test.mjs**
Updates preference coverage for the default-off behavior while
preserving explicit saved choices.
**desktop/src/features/messages/lib/autoPinMentionedAgentsPreference.ts**
Defaults automatic post-send persistence off when no valid preference
exists.
**desktop/src/features/messages/ui/useAgentAddressLockPicker.test.mjs**
Verifies ordinary picker selections insert inline mentions without
changing or pulsing the persistent audience, including when the selected
agent is already persistently addressed.
**desktop/src/features/messages/ui/useAgentAddressLockPicker.ts**
Keeps ordinary mention selection on the existing inline insertion path
while preserving the separate persistent-address controls and shortcut.
**desktop/tests/e2e/persistent-agent-audience.spec.ts**
Covers separately targeted inline instructions to two agents, signed
outgoing recipients, default-off persistence, and explicit opt-in
persistence scenarios.
</details>
## Reproduction steps
### Inline mentions without persistence
1. Open a channel with at least two available agents and leave
**Automatically mention agents** disabled.
2. Use the composer mention picker to insert the first agent, type an
instruction, then insert a second agent and type a different
instruction.
3. Confirm the draft reads like `@Agent A review this, @Agent B test
that` and neither agent appears in the persistent addressed-agent
controls.
4. Send the message and confirm both agents are recipients while neither
remains persistently addressed for the next draft.
### Inline mentions with persistence
1. Enable **Automatically mention agents**, then mention an agent inline
and send successfully.
2. Confirm that agent becomes persistently addressed for later messages.
3. In a new draft, select the same agent from the mention picker again.
4. Confirm a new inline `@Agent` is inserted at the cursor while the
agent remains persistently addressed.
5. Confirm the dedicated automatic-mention control and
primary+Shift+Enter shortcut can still add or remove persistent
addressing directly.
## Demo
- **Before:** Persistent addressing could consume an ordinary picker
selection, preventing users from placing that agent inline in the
message.
- **After:** Ordinary selection always produces an inline `@Agent`;
persistence is a separate optional behavior that can coexist with inline
mentions.
### Related issue
N/A — reported through the Buzz feature room.
### Testing
At `a4579c0a666f3644abe44271614186d7b9356bf5`:
- Post-push hooks passed desktop check, desktop typecheck, and the full
desktop unit suite.
- The two persistence-focused Playwright smoke scenarios passed after
explicitly opting into **Automatically mention agents**.
- The one-time multi-agent targeting and persistent-address shortcut
Playwright scenarios passed on the feature changes before the final
rebase; CI is validating the rebased branch.
---------
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Co-authored-by: Carl <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@buzz.block.builderlab.xyz>
## Why
Phase 1 of progressive delivery needs an explicit eligibility boundary
before deployment automation can safely consume relay artifacts. Today
the canonical Docker workflow can publish independently of the same-SHA
CI result, the relay does not expose its source/build identity, and the
Helm chart only renders mutable tags.
The existing `ghcr.io/block/buzz-staging-dev` workflow in #6709 remains
a manual pre-merge preview lane. It is deliberately not a canonical
promotion input.
## What changed
- Compile the full source SHA and GitHub Actions build identity into
canonical relay binaries and expose it at `/_status`.
- Gate creation of tagged multi-architecture manifests on the latest
exact-SHA `CI` push run succeeding on `main` or `release`.
- Preserve the signed SLSA provenance and add a signed, release-only
deployment-eligibility predicate with source, build run, qualifying CI
run, and exact compatible Buzz chart version.
- Keep failed or pending builds untagged and without an eligibility
predicate; preview-package artifacts are excluded by package and signer
contract.
- Add optional `image.digest` support to Buzz chart `0.1.8`, retaining
the existing tag fallback.
- Document verification and the intentional boundary between image/chart
compatibility and backwards-compatible database migrations.
## Risk and rollout
Canonical tag publication now waits for same-SHA CI and fails closed
when the newest matching run fails. Per-architecture blobs may remain
untagged after a failed qualification; they are not promotion inputs.
Debug manifests are also CI-gated but do not receive the release
eligibility predicate.
There are no migration or schema-orchestration changes. The companion
BuilderBot infrastructure PR will pin a separately verified canonical
main digest and expose a read-only running-deployment inspection
command. Signed eligibility is available only for artifacts published
after this workflow lands.
## Verification
- Blox: workflow contract fixtures, including stale-green rejection and
predicate structure
- Blox: `actionlint` for `ci.yml` and `docker.yml`
- Blox: 46 Helm unit tests, chart lint, and production/quickstart render
fixtures
- Blox: relay status unit test, workspace Rust formatting, and workspace
all-target clippy
- Live evidence: exact-SHA CI selector chose successful run
`32857345153` for source `9d1e4b257657f382d3111ce748f3da8d063b7671`
- Existing canonical digest provenance verified with `gh attestation
verify`
## References
- #6709 — developer-preview precursor; intentionally separate from
canonical eligibility
- `docs/deployment-identity.md`
*Generated with Codex*
---------
Signed-off-by: Luke Tornquist <tornquist@squareup.com>