mirror of
https://github.com/block/buzz.git
synced 2026-11-05 00:41:28 +00:00
4e4c7495bfd4bb61b8cb77327609d720785dc813
672 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
4e4c7495bf |
merge: mount terminal frontend on final runtime
* commit 'e1eaf88b0d7c9b2f9922ffea6a2cbd47682e746d': docs(terminal): cite the discriminating tail payload docs(terminal): say why tail_full cannot fire, not just that nothing calls it test(terminal): require the runtime to pump deferred work docs(terminal): say that the tail-depth signals have no consumer yet docs(terminal): repoint the links the slice_bytes deletion broke test(terminal): reject stop before child reap refactor(terminal): delete the slice-sizing function nothing calls test(terminal): make the decrease and RIS arms assert what they claim fix(terminal): retain the scrollback debt a shrink does not immediately repay fix(terminal): repair three defects the gate found in the work-bounded seam fix(terminal): raw-drain while child exits feat(terminal): bound the lock hold by weighted work, not by bytes test(terminal): close the review gaps in the cluster and snapshot contracts feat(terminal): give an attaching subscriber the screen as it stands feat(terminal): give the renderer each cluster's true column Signed-off-by: npub1mprnacetjua2xx3p5eddmhxyk6wv929ymm5py8kd2xfxurxahspqqlgyta <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@buzz.block.builderlab.xyz> Co-authored-by: npub1mprnacetjua2xx3p5eddmhxyk6wv929ymm5py8kd2xfxurxahspqqlgyta <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@buzz.block.builderlab.xyz> |
||
|
|
e1eaf88b0d |
docs(terminal): cite the discriminating tail payload
Co-authored-by: npub1jh9wn95s0472h86ahapupaf7m6kx4v9sx2n0atj2hltcfer8k06s5n3pyf <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@buzz.block.builderlab.xyz> Signed-off-by: npub1jh9wn95s0472h86ahapupaf7m6kx4v9sx2n0atj2hltcfer8k06s5n3pyf <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@buzz.block.builderlab.xyz> |
||
|
|
df19569e7c |
Merge final terminal backpressure documentation
Co-authored-by: npub1jh9wn95s0472h86ahapupaf7m6kx4v9sx2n0atj2hltcfer8k06s5n3pyf <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@buzz.block.builderlab.xyz> Signed-off-by: npub1jh9wn95s0472h86ahapupaf7m6kx4v9sx2n0atj2hltcfer8k06s5n3pyf <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@buzz.block.builderlab.xyz> * commit '232f56b67219613ddb1fd6628a73ae31bd1156a5': docs(terminal): say why tail_full cannot fire, not just that nothing calls it Signed-off-by: npub1jh9wn95s0472h86ahapupaf7m6kx4v9sx2n0atj2hltcfer8k06s5n3pyf <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@buzz.block.builderlab.xyz> |
||
|
|
232f56b672 |
docs(terminal): say why tail_full cannot fire, not just that nothing calls it
The previous wording said the signal has no consumer. That is the weaker half of the fact and the half that ages badly: a reader who deletes the pump loop learns the rest by wedging a terminal. Both measurements are now in the comment. With the loop, 1 MiB of pure RIS through 16 KiB reads leaves a tail high-water of exactly 0 -- the tail is empty between iterations, so the predicate is unreachable by construction. Without it, the same stream reaches TAIL_CAP at read 257. Reproduced here before quoting: 0 and 257 both hold on this tree. Wording is Sami's, who measured it first and made the argument that the signal and the loop are one fact from two sides. Delete the loop and this predicate stops being unreachable in the same instant it starts being needed. Co-authored-by: tlongwell-block <109685178+tlongwell-block@users.noreply.github.com> Signed-off-by: tlongwell-block <109685178+tlongwell-block@users.noreply.github.com> |
||
|
|
73d00c6013 |
Merge terminal scheduler documentation
Co-authored-by: npub1jh9wn95s0472h86ahapupaf7m6kx4v9sx2n0atj2hltcfer8k06s5n3pyf <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@buzz.block.builderlab.xyz> Signed-off-by: npub1jh9wn95s0472h86ahapupaf7m6kx4v9sx2n0atj2hltcfer8k06s5n3pyf <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@buzz.block.builderlab.xyz> * commit '9ca661ee8e684d720724673b69c8f8a110fb72b2': docs(terminal): say that the tail-depth signals have no consumer yet docs(terminal): repoint the links the slice_bytes deletion broke Signed-off-by: npub1jh9wn95s0472h86ahapupaf7m6kx4v9sx2n0atj2hltcfer8k06s5n3pyf <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@buzz.block.builderlab.xyz> # Conflicts: # desktop/src-tauri/crates/buzz-terminal/src/fences.rs # desktop/src-tauri/crates/buzz-terminal/src/lib.rs # desktop/src-tauri/crates/buzz-terminal/src/reader.rs |
||
|
|
5f416f7d25 |
test(terminal): require the runtime to pump deferred work
Co-authored-by: npub1jh9wn95s0472h86ahapupaf7m6kx4v9sx2n0atj2hltcfer8k06s5n3pyf <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@buzz.block.builderlab.xyz> Signed-off-by: npub1jh9wn95s0472h86ahapupaf7m6kx4v9sx2n0atj2hltcfer8k06s5n3pyf <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@buzz.block.builderlab.xyz> |
||
|
|
9ca661ee8e |
docs(terminal): say that the tail-depth signals have no consumer yet
`tail_full` and `tail_drained` are the queue bound, and nothing outside the tests calls them: the runtime reader pumps `drain` to completion after every read, so the tail cannot reach the cap and nobody needs to ask. That is fine and it is the design -- the signal was built for the reader that stops pumping -- but an exported, documented predicate with an empty call graph reads as wired to anyone who doesn't go looking. Stated in the doc comment instead, because an unused signal that looks connected is worse than one that says it isn't. Co-authored-by: tlongwell-block <109685178+tlongwell-block@users.noreply.github.com> Signed-off-by: tlongwell-block <109685178+tlongwell-block@users.noreply.github.com> |
||
|
|
651ef710b4 |
docs(terminal): repoint the links the slice_bytes deletion broke
Two intra-doc links still resolved to the deleted function, so `cargo doc` emitted unresolved-link warnings. Neither `cargo test` nor clippy sees those, which is why they survived a green gate -- the deletion was verified by the checks that run in CI and not by the one that reads the docs. The single remaining mention is deliberate: the paragraph in `fences.rs` explaining why the old function existed and why it is gone. Co-authored-by: tlongwell-block <109685178+tlongwell-block@users.noreply.github.com> Signed-off-by: tlongwell-block <109685178+tlongwell-block@users.noreply.github.com> |
||
|
|
3d4fc49df9 |
Merge terminal scheduler fixture correction
Co-authored-by: npub1jh9wn95s0472h86ahapupaf7m6kx4v9sx2n0atj2hltcfer8k06s5n3pyf <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@buzz.block.builderlab.xyz> Signed-off-by: npub1jh9wn95s0472h86ahapupaf7m6kx4v9sx2n0atj2hltcfer8k06s5n3pyf <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@buzz.block.builderlab.xyz> * commit 'd67b99a3dd611f0c239d9bce13552417acaba622': refactor(terminal): delete the slice-sizing function nothing calls Signed-off-by: npub1jh9wn95s0472h86ahapupaf7m6kx4v9sx2n0atj2hltcfer8k06s5n3pyf <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@buzz.block.builderlab.xyz> |
||
|
|
fe7d291f48 |
test(terminal): reject stop before child reap
Co-authored-by: npub1jh9wn95s0472h86ahapupaf7m6kx4v9sx2n0atj2hltcfer8k06s5n3pyf <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@buzz.block.builderlab.xyz> Signed-off-by: npub1jh9wn95s0472h86ahapupaf7m6kx4v9sx2n0atj2hltcfer8k06s5n3pyf <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@buzz.block.builderlab.xyz> |
||
|
|
d67b99a3dd |
refactor(terminal): delete the slice-sizing function nothing calls
`slice_bytes(columns, lines, scrollback)` stopped being the scheduler's function when slices became remaining-aware, and the fixtures went on asserting against it. The two disagreed exactly where the old floor bound -- reporting 4 bytes where `drain` used 1 -- so the preconditions guarding the decrease arm were describing behaviour that no longer existed. Not wrong at today's geometries, and no test would have noticed when it became wrong. It had zero callers outside the tests it misled. Deleted, with every assertion moved to `slice_bytes_remaining`, which is what the engine calls. One function, one answer. `MIN_SLICE` goes with it: the floor is 1 and lives in the function, because on a grid whose worst atom exceeds the whole budget no wider slice can promise to stop after the callback that crosses. A named constant that only appeared in a doc comment is a second authority waiting to disagree with the first. 69 tests, debug and release. Test-only in effect; the deleted item was unreachable from the engine. Co-authored-by: tlongwell-block <109685178+tlongwell-block@users.noreply.github.com> Signed-off-by: tlongwell-block <109685178+tlongwell-block@users.noreply.github.com> |
||
|
|
7a75b4e170 |
Merge terminal fixture repair
* commit '84b274d3c5da05257df95953b3f5115ead5420dc': test(terminal): make the decrease and RIS arms assert what they claim Signed-off-by: npub1jh9wn95s0472h86ahapupaf7m6kx4v9sx2n0atj2hltcfer8k06s5n3pyf <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@buzz.block.builderlab.xyz> |
||
|
|
84b274d3c5 |
test(terminal): make the decrease and RIS arms assert what they claim
Test-only. Five gaps, four of them found independently by two reviewers, which is the part worth noting: they are the kind that pass. The decrease arm captured `first_pending` and never asserted it, compared `first_units` to the literal `12` rather than the control's own field, and ran its geometry-persistence check on a *different* terminal -- one that had only ever grown. That last one is the sharp one. It would pass an implementation that retained the debt on shrink and dropped it on the next geometry resize, which is a real shape: `Feeder::resize` sees every resize, and only the scrollback branch is conditional. Verified by writing that mutant: the arm now fails `left: 80000, right: 4040000`, and it did not before. Two things had to be true for it to bite, and it was inert without either. It runs on the terminal that actually went shallow -> deep -> shallow, and the resize carries the *shallow* depth -- handing the debt's own value back in makes `max(debt, new)` and a plain assignment agree, so the arm cannot tell them apart. I found that second one by writing the mutant, watching it survive, and looking again. The RIS table collected all six arms before asserting but then asserted them in a loop, so a failure still printed one. Collecting stops an arm being skipped; comparing the vectors is what stops the failure being truncated. With the history term deleted the failure now shows all six rows and which ones moved, rather than the first mismatch. Every comparison in the decrease arm is now against the control's own field. A constant or geometry change has to move both sides together, or the fixture quietly starts asserting the arithmetic of the day it was written. Co-authored-by: tlongwell-block <109685178+tlongwell-block@users.noreply.github.com> Signed-off-by: tlongwell-block <109685178+tlongwell-block@users.noreply.github.com> |
||
|
|
03499c956e |
Merge Dawn's terminal scheduler repair
Co-authored-by: npub1jh9wn95s0472h86ahapupaf7m6kx4v9sx2n0atj2hltcfer8k06s5n3pyf <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@buzz.block.builderlab.xyz> Signed-off-by: npub1jh9wn95s0472h86ahapupaf7m6kx4v9sx2n0atj2hltcfer8k06s5n3pyf <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@buzz.block.builderlab.xyz> |
||
|
|
e1500806d3 |
fix(terminal): retain the scrollback debt a shrink does not immediately repay
The previous commit lowered the feeder's scrollback on a decrease, which reads as correct and is not: upstream frees history lazily. `Storage::shrink_lines` truncates only once the buffer exceeds the new length by `MAX_CACHE_SIZE`, so immediately after a shrink the grid still owns rows a reset has to walk. Pricing at the new depth charges for a grid that does not exist yet. So the feeder tracks a high-water mark instead of the current depth. It never falls, needs no clearing transition, and cannot go stale in the unsafe direction. The cost is real and stated: a session that shrinks its scrollback keeps paying the deep price for the rest of its life. The alternative is a bound that is wrong immediately after every shrink. The fixture that permitted this is the more important half. It asserted `shrunk_work >= fresh_shallow`, which a feeder that dropped the debt satisfies *by equality* -- "never had a debt" and "dropped the debt" are the same state, so any predicate anchored to a fresh control blesses both. Strictness on the pricing field is what separates them, and the pricing field is the only one that can: the scheduling observables separate only when the two depths land on different slice widths, and both are usually floored. The arm now asserts `completed_work > fresh_shallow` strictly, with the scheduling fields carrying their per-field directions -- `first_units` inverts, because a narrower slice retires fewer atoms per un-preemptable drain, which is the fence working. Also here, both from the same gate: * Monotonicity is swept per axis. A truncating `columns * lines` is a non-monotone middle on the columns axis alone, and a scrollback-only sweep cannot see it. * Seventeen more rows in the pricing table -- `scroll_down`, `insert_lines`, `put_tab`, forward tabs, both `clear_line` modes, both `clear_screen` modes, and the parameter extremes for each. The four that were missing are the four whose weights share a shape with ones already covered, which is exactly the argument for covering them separately rather than by analogy. Co-authored-by: tlongwell-block <109685178+tlongwell-block@users.noreply.github.com> Signed-off-by: tlongwell-block <109685178+tlongwell-block@users.noreply.github.com> |
||
|
|
1a2c86ebed |
fix(terminal): repair three defects the gate found in the work-bounded seam
All three were pre-registered predictions that the fixtures at
|
||
|
|
04e9d0dc7e |
fix(terminal): raw-drain while child exits
Co-authored-by: npub1jh9wn95s0472h86ahapupaf7m6kx4v9sx2n0atj2hltcfer8k06s5n3pyf <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@buzz.block.builderlab.xyz> Signed-off-by: npub1jh9wn95s0472h86ahapupaf7m6kx4v9sx2n0atj2hltcfer8k06s5n3pyf <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@buzz.block.builderlab.xyz> |
||
|
|
1a06fffddc |
Merge commit 'a957d25b02881bad31bb0d6ab6c9571545ba9bc8' into mari/tui-integration
* commit 'a957d25b02881bad31bb0d6ab6c9571545ba9bc8': feat(terminal): bound the lock hold by weighted work, not by bytes test(terminal): close the review gaps in the cluster and snapshot contracts feat(terminal): give an attaching subscriber the screen as it stands feat(terminal): give the renderer each cluster's true column Co-authored-by: npub1jh9wn95s0472h86ahapupaf7m6kx4v9sx2n0atj2hltcfer8k06s5n3pyf <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@buzz.block.builderlab.xyz> Signed-off-by: npub1jh9wn95s0472h86ahapupaf7m6kx4v9sx2n0atj2hltcfer8k06s5n3pyf <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@buzz.block.builderlab.xyz> # Conflicts: # desktop/src-tauri/crates/buzz-terminal/src/damage.rs # desktop/src-tauri/crates/buzz-terminal/tests/clusters.rs # desktop/src-tauri/crates/buzz-terminal/tests/snapshot.rs |
||
|
|
a957d25b02 |
feat(terminal): bound the lock hold by weighted work, not by bytes
Both existing fences meter bytes. That is right for memory and wrong for time: `ESC[m` and `ESC#8` are four bytes each, and the second rewrites every cell. A DECALN flood therefore reaches p95 65535us against a 4000us budget with **zero** F1 aborts -- the fence never fires, because nothing is buffered. Unfenced, one acquisition was observed at 22.1s. So add a third quantity. `Counting` wraps the handler and charges each callback what it touches: an O(cells) callback costs `columns * lines`, an O(1) callback costs 1. `Feeder` accepts bytes into a pending tail and parses only what one work budget affords, leaving the rest for the next call. That bounds the hold. It does not bound the queue, so the tail has a cap and a loud breach counter, and the reader is told to stop reading via a depth query rather than a pause flag the fence owns -- a flag is a state a reader can fail to clear, which is how a paused reader strands a child mid-teardown. The weights are read from `alacritty_terminal-0.26.0/src/term/mod.rs` and then checked against measurement, never fitted. Where source and measurement disagree the source wins and the slack is recorded in the table. Three findings from that reading are worth naming: * `delete_chars`/`insert_blank` cost `columns` for *every* N and are worst at N=1 -- the swap loop runs `columns - end` times, so cost falls as N rises. A sweep that varies only N reports them parameter-dependent *decreasing*, and the honest reading of that invites charging by N, which is backwards. * `move_backward_tabs` (CBT) assigns `col` inside the `if self.tabs[i]` test, so with tabstops cleared the cursor never moves, its `col == 0` exit is unreachable, and all N iterations rescan the row: `ESC[3g ESC[65535Z` is eight bytes for 82ms at 1600 columns. Its twin `move_forward_tabs` assigns outside the test and is fine. The wrapper stops it at the first fixed point -- permanent, because the scan depends only on the cursor -- which makes it O(columns) and, verified exhaustively over every tabstop subset of a 12-column grid, lands on the same column as the naive loop. * `reset_state` (RIS) resets both grids and walks the primary's scrollback, so two bytes can be worth more than the whole budget. Priced on *configured* depth, not `history_size()`, which observes only the active grid: a filled primary followed by `ESC[?1049h` reads as empty while the work is still paid. Slice size is derived from the densest atom the grid admits rather than chosen, so the bound holds on the first byte of a cold feeder. An earlier version sized slices from observed density and was strictly worse where it matters -- a fresh feeder has seen nothing, so its first slice is wide. `Terminal::feed` now returns whether a tail remains and the caller pumps `drain` between lock acquisitions. `feed_fully` is the whole-buffer form, deliberately a separate name because it reinstates the unbounded hold. Every fixture asserts exact counts, never `> 0` and never `<=` alone. Both directions are load-bearing: with the CBT break deleted the callback loops 65535 times while charging `work == 1`, so an upper bound passes over it, and the mutant lands on the same cursor column as the fix, so a semantic assertion passes too. 20 mutants dead in debug and release. Co-authored-by: tlongwell-block <109685178+tlongwell-block@users.noreply.github.com> Signed-off-by: tlongwell-block <109685178+tlongwell-block@users.noreply.github.com> |
||
|
|
7d56109ecb |
chore(desktop): keep app entrypoint within size budget
Co-authored-by: npub1mprnacetjua2xx3p5eddmhxyk6wv929ymm5py8kd2xfxurxahspqqlgyta <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@buzz.block.builderlab.xyz> Signed-off-by: npub1mprnacetjua2xx3p5eddmhxyk6wv929ymm5py8kd2xfxurxahspqqlgyta <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@buzz.block.builderlab.xyz> |
||
|
|
4bd3b03a3c |
fix(terminal): recover attach publication race
Co-authored-by: npub1jh9wn95s0472h86ahapupaf7m6kx4v9sx2n0atj2hltcfer8k06s5n3pyf <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@buzz.block.builderlab.xyz> Signed-off-by: npub1jh9wn95s0472h86ahapupaf7m6kx4v9sx2n0atj2hltcfer8k06s5n3pyf <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@buzz.block.builderlab.xyz> |
||
|
|
e887bdb2c4 |
test(terminal): close the review gaps in the cluster and snapshot contracts
Six review findings, all coverage on code that was already correct, plus one
enforcement change.
Clusters. The join guard has two halves and only one was exercised: every
fixture built a plain cluster after a marked one, none the reverse. Drop
`joinable` and a release build silently emits `Span { text: "xye\u{301}",
cluster_count: 3 }` -- four chars counted as three, so the consumer misplaces
everything after it. Also fixture the `u16::MAX` join refusal, which is live
rather than defensive: `Size.columns` is an unclamped `usize` with no caller
bounding it, so a 70000-column row reaches it and must split rather than wrap.
Promote the encoder's `debug_assert` to `assert!`. This is a wire invariant,
and a check that vanishes in release vanishes exactly where the corruption
ships: with the defect present, the promoted build fails at the producer
rather than leaving an undecodable span for the renderer to misdraw. Measured
at +5.9us on a 65us encode for 80x24 -- 0.04% of a 60Hz frame.
Snapshot. The four fixtures asserted the damage plane and nothing else, so a
capture that dropped the screen's last row, lied about the cursor, or stamped
a stale grid identity passed all of them. `visible_text` trims and filters
empty lines, which is what let the missing row disappear -- a helper written
for legibility concealing the defect it should have exposed. Assert the row
set rather than the text, the cursor against a real position and DECTCEM, the
whole `Viewport` rather than `columns` alone, and that a full-grid copy is
billed to the renderer's meter and not the reader's.
Findings from Sami's P2a and P2b reviews; enforcement call from Eva; encode
cost measured by Sami.
Co-authored-by: tlongwell-block <109685178+tlongwell-block@users.noreply.github.com>
Signed-off-by: tlongwell-block <109685178+tlongwell-block@users.noreply.github.com>
|
||
|
|
ba4ff04fea |
feat(desktop): wire terminal sessions to Tauri
Co-authored-by: npub1jh9wn95s0472h86ahapupaf7m6kx4v9sx2n0atj2hltcfer8k06s5n3pyf <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@buzz.block.builderlab.xyz> Signed-off-by: npub1jh9wn95s0472h86ahapupaf7m6kx4v9sx2n0atj2hltcfer8k06s5n3pyf <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@buzz.block.builderlab.xyz> |
||
|
|
1a213fe709 |
feat(terminal): give an attaching subscriber the screen as it stands
A subscriber that arrives mid-stream cannot start from `render()`. Damage describes what changed since someone last looked, so a newcomer is handed whatever happens next -- on a quiet terminal, the cursor's line alone -- painted onto a blank screen. Upstream's `mark_fully_damaged` is private, so an embedder cannot ask for a full frame that way either. `snapshot()` copies the whole visible viewport instead, marks the frame full so a reused encoder's dedup hashes realign, and stamps the geometry it was captured under. The delicate part is what it does not do. Damage is a single shared cursor across every subscriber, so a snapshot that consumed it would steal the incumbent renderer's pending rows: the newcomer's full frame would look perfect while the established renderer silently froze. `capture_all` therefore never calls `damage()` or `reset_damage()`, and their absence is the mechanism. Proving that needed more care than the interleaving alone. Because `Term::damage()` marks the cursor line on every call, an incumbent owed only the row it sits on gets that row back even when its damage was stolen -- so the first version of the fixture passed against a deliberately naive implementation. The committed fixture rewrites row 0 and parks the cursor on row 3, putting the owed row where the cursor cannot return it for free; it fails when snapshot consumes damage, and again when snapshot copies damaged rows only or omits the full flag. Co-authored-by: tlongwell-block <109685178+tlongwell-block@users.noreply.github.com> Signed-off-by: tlongwell-block <109685178+tlongwell-block@users.noreply.github.com> |
||
|
|
3a5c7d576e |
feat(terminal): give an attaching subscriber the screen as it stands
A subscriber that arrives mid-stream cannot start from `render()`. Damage describes what changed since someone last looked, so a newcomer is handed whatever happens next -- on a quiet terminal, the cursor's line alone -- painted onto a blank screen. Upstream's `mark_fully_damaged` is private, so an embedder cannot ask for a full frame that way either. `snapshot()` copies the whole visible viewport instead, marks the frame full so a reused encoder's dedup hashes realign, and stamps the geometry it was captured under. The delicate part is what it does not do. Damage is a single shared cursor across every subscriber, so a snapshot that consumed it would steal the incumbent renderer's pending rows: the newcomer's full frame would look perfect while the established renderer silently froze. `capture_all` therefore never calls `damage()` or `reset_damage()`, and their absence is the mechanism. Proving that needed more care than the interleaving alone. Because `Term::damage()` marks the cursor line on every call, an incumbent owed only the row it sits on gets that row back even when its damage was stolen -- so the first version of the fixture passed against a deliberately naive implementation. The committed fixture rewrites row 0 and parks the cursor on row 3, putting the owed row where the cursor cannot return it for free; it fails when snapshot consumes damage, and again when snapshot copies damaged rows only or omits the full flag. Co-authored-by: tlongwell-block <109685178+tlongwell-block@users.noreply.github.com> Signed-off-by: tlongwell-block <109685178+tlongwell-block@users.noreply.github.com> |
||
|
|
f3e7eb16c9 |
feat(terminal): give the renderer each cluster's true column
The span encoding shipped a concatenated string and a start column, which left the consumer to recover cell boundaries from the text. It cannot: a regional-indicator flag is two one-column cells and must split per codepoint, while a keycap is one cell holding three codepoints and must split per grapheme. Those rules are opposite, and the distinction lives in the grid, not in the string. Any split rule over the concatenated text is wrong for one of the two. So the span carries what the grid knows. `width` is the columns each cluster advances, uniform across a run by construction; `cluster_count` says how many clusters the text holds. Decoding is then arithmetic with no Unicode table anywhere: a count of 1 means the whole text is one cluster, otherwise cluster i is the i-th char at `column + i * width`. Geometry flags are also masked out of the style key. `WIDE_CHAR` and `WRAPLINE` are grid bookkeeping, not appearance, and while they sat in the key they broke runs as a side effect -- hiding the explicit width check behind a coincidence, and splitting the last column off every wrapped row into a span of its own. Row dedup still hashes the full flags, so nothing it needs to see is lost. Eight fixtures cover the contract, including the flag/keycap pair whose opposing rules make the case. Six mutations were run against them and each fails independently: forcing width to 1, dropping the width comparison, never breaking on zerowidth marks, removing WRAPLINE from the mask, removing the mask entirely, and never incrementing cluster_count. Co-authored-by: tlongwell-block <109685178+tlongwell-block@users.noreply.github.com> Signed-off-by: tlongwell-block <109685178+tlongwell-block@users.noreply.github.com> |
||
|
|
e78491767b |
feat(terminal): give the renderer each cluster's true column
The span encoding shipped a concatenated string and a start column, which left the consumer to recover cell boundaries from the text. It cannot: a regional-indicator flag is two one-column cells and must split per codepoint, while a keycap is one cell holding three codepoints and must split per grapheme. Those rules are opposite, and the distinction lives in the grid, not in the string. Any split rule over the concatenated text is wrong for one of the two. So the span carries what the grid knows. `width` is the columns each cluster advances, uniform across a run by construction; `cluster_count` says how many clusters the text holds. Decoding is then arithmetic with no Unicode table anywhere: a count of 1 means the whole text is one cluster, otherwise cluster i is the i-th char at `column + i * width`. Geometry flags are also masked out of the style key. `WIDE_CHAR` and `WRAPLINE` are grid bookkeeping, not appearance, and while they sat in the key they broke runs as a side effect -- hiding the explicit width check behind a coincidence, and splitting the last column off every wrapped row into a span of its own. Row dedup still hashes the full flags, so nothing it needs to see is lost. Eight fixtures cover the contract, including the flag/keycap pair whose opposing rules make the case. Six mutations were run against them and each fails independently: forcing width to 1, dropping the width comparison, never breaking on zerowidth marks, removing WRAPLINE from the mask, removing the mask entirely, and never incrementing cluster_count. Co-authored-by: tlongwell-block <109685178+tlongwell-block@users.noreply.github.com> Signed-off-by: tlongwell-block <109685178+tlongwell-block@users.noreply.github.com> |
||
|
|
c17f2d2c3d |
terminal: process-group lifecycle with drain-ordered teardown
Closing a tab must end the work the tab was doing. The child is a session leader (portable-pty calls setsid), so signalling its pid reaches the shell and leaves everything the shell started running, reparented to init and holding the pty slave. Signal the process group instead: SIGTERM, a bounded grace, then SIGKILL to the group whether or not the leader went quietly -- a polite leader does not imply an empty group. Three things this found that reading could not. waitpid(WNOHANG | WNOWAIT) returns EINVAL on Darwin. POSIX defines WNOWAIT only for waitid; Linux tolerates it, macOS does not. The failure was silent because `seen == pid` is false for -1 exactly as it is false for "still running" -- error and negative result collapsed into one branch. Every shutdown escalated to SIGKILL and the polite arm was dead code on the platform we develop on. Only an assertion on *which arm fired* could see it. A grandchild test that does not defeat the tty hangup proves nothing. Killing a session leader makes the kernel SIGHUP the foreground group, so the grandchild dies either way and a pid-only mutation survives. The fixture must ignore SIGHUP and busy-loop; then pid-only leaks and kill(-pgid) does not. A child blocked writing to an undrained master does not die promptly even on SIGKILL -- measured 606ms to reap, versus microseconds when drained. So the reader outlives termination and reap. shutdown_draining takes the reader by value, which makes the forbidden order a compile error rather than a comment: a joined reader has been consumed and cannot be passed. Mutation matrix, sources restored byte-identically after each: L1 delete SIGKILL escalation -> FAIL (orphan + grace arms) L2 kill(pid) not kill(-pgid) -> FAIL (orphan arm) L3 delete pre-reap try_wait -> FAIL (Killed, want AlreadyExited) L4 join reader before SIGTERM -> FAIL (10.01s, grace is 250ms) Fixtures built to ignore signals carry their own SIGKILL deadline. Without it a legitimate failure leaves a core spinning at PPID 1, and L1/L4 were detected by hanging -- a test that finds a bug by never finishing cannot be told from a broken one. The deadline sits far outside the observation window so it can never rescue a failing implementation. 2088 desktop + 3 mixer + 26 buzz-terminal + 5 fences + 4 resize green, clippy --workspace --all-targets -D warnings clean, fmt clean. Co-authored-by: tlongwell-block <109685178+tlongwell-block@users.noreply.github.com> Signed-off-by: tlongwell-block <109685178+tlongwell-block@users.noreply.github.com> |
||
|
|
f05ef3170f |
feat(desktop): enforce terminal frame publication credit
Co-authored-by: Mari <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@buzz.block.builderlab.xyz> Signed-off-by: tlongwell-block <109685178+tlongwell-block@users.noreply.github.com> |
||
|
|
c3c705583b |
feat(desktop): fence the terminal child's environment, PATH, and shell
A PTY child inherits its parent's environment by default, and Buzz's own process holds the user's nsec. `CommandBuilder::new()` pre-seeds its env map from `std::env::vars_os()` (`cmdbuilder.rs:218` -> `:74`), so the naive spawn hands a live shell 70 variables including `BUZZ_PRIVATE_KEY`: press the toggle, type `env`, read the signing key off the screen. Verified against a real PTY, not inferred. Three fences, each with the failure it exists to prevent: * Environment: `env_clear()` first, then rebuild from an allowlist. Order is load-bearing and the reverse fails silently -- clearing after populating yields an empty environment and no error -- so the gate asserts both directions. Allowlist rather than denylist because a denylist is only as current as the last person who remembered to extend it. * PATH: constructed, never inherited, never filtered. Buzz runs under Hermit activation, so an inherited PATH makes the user's `cargo` our pinned build toolchain -- on Linux, where no `path_helper` reorders it, at the front. The test seeds a uniquely-named executable into the parent PATH and proves the child cannot resolve it, rather than comparing PATH strings. * Shell: `$SHELL` -> passwd -> `/bin/sh`, each candidate validated as an executable regular file. `access(X_OK)` alone accepts a directory, and a `$SHELL` of `/tmp` produces a child that aborts with a Rust runtime panic while `get_shell()`, `spawn_command()`, and every layer above report success. Raw mode bits alone accept a file the caller cannot execute. The conjunction is the check. The resolved shell is injected as `SHELL`, not inherited: those differ in exactly the cases the fallback chain exists for. GUI context crosses a trust boundary. A channel name is attacker-controlled and lands in a variable shells interpolate into prompts, so `BUZZ_CHANNEL` is validated against a conservative character class and, on rejection, replaced by the channel UUID rather than stripped -- a stripped `$(evil)` becomes `evil`, which looks like a real channel. Nineteen fixtures, each shown failing under the mutation it exists to catch: deleting `env_clear`, reordering it to the end, inheriting PATH, denylisting instead of allowlisting, dropping either half of the executability predicate, inheriting `SHELL`, sniffing the shell's name for login flags, removing the passwd candidate, weakening the shared validator, skipping channel-name validation, stripping instead of substituting, dropping the length cap, letting the display name reach `BUZZ_CHANNEL_ID`, emitting an empty `BUZZ_THREAD_ID`, accepting `=` in a key, and adding a credential to the desktop crate's reserved list without covering it here. Co-authored-by: tlongwell-block <109685178+tlongwell-block@users.noreply.github.com> Signed-off-by: tlongwell-block <109685178+tlongwell-block@users.noreply.github.com> |
||
|
|
54cd502009 |
docs(desktop): record why G3's max bound cannot be widened
The p95 comment claimed 31x of headroom "lets this run on a loaded CI runner," and read as if it justified the whole gate. It justifies the quantile only. A p95 discards its worst samples by construction and so degrades gracefully under load; a max over 200 samples is one observation that any single scheduler preemption exceeds. No budget makes it robust, because that tail belongs to the scheduler. Measured on one 16-core host at the same commit: pass at load ~6, fail at ~31 (p95 65535 us, max 164889 us). Sami measured a run where p95 sat 4x inside budget while max alone blew at 38150 us -- a p95-only gate would have called a 38 ms stall green. That asymmetry is what makes the ignore-plus-native-release placement load-bearing rather than tidy, and it is the reason the repair for a flake here is to fix the host, never to raise the number: a hitch is a max-event, so widening the max is the one change that removes the only assert catching the user-visible failure. Co-authored-by: Dawn (sprout agent) <c6237ef84fa537c78dcee78efd2d4e59f728859c7f194da42ac51ededfa0be05@sprout-oss.stage.blox.sqprod.co> Signed-off-by: tlongwell-block <109685178+tlongwell-block@users.noreply.github.com> |
||
|
|
25fcb5b26d |
Merge branch 'dawn/tui-engine-core' of ../buzz-tui-dawn into tui-substrate-perci
* 'dawn/tui-engine-core' of ../buzz-tui-dawn: feat(desktop): make a frame's viewport comparable in one value Co-authored-by: tlongwell-block <109685178+tlongwell-block@users.noreply.github.com> Signed-off-by: tlongwell-block <109685178+tlongwell-block@users.noreply.github.com> |
||
|
|
105aadb22c |
feat(desktop): make a frame's viewport comparable in one value
The integration lane's core operation is "does this frame describe the grid I
am currently showing?" -- a frame captured before a resize can arrive after it,
because frame delivery and the resize call travel separate paths. Answering
that meant comparing three loose fields, and a consumer that checks two of the
three is wrong on exactly the resize that changed the one it skipped.
Group them into `Viewport { generation, columns, screen_lines }` so the
question is one equality, and return it from `resize` rather than making the
caller ask afterwards: across a transport a follow-up query races the next
resize, so its answer can describe a grid that has already been replaced. A
same-size resize stays inert and reports the unchanged generation.
Two mutants, both caught: returning the requested viewport instead of the
applied one, and stamping a frame at encode time instead of capture time.
The stamping fixture was tautological on its first draft -- it compared a
frame's viewport against a copy taken from that same frame, which the language
already guarantees. It now asserts capture-time values explicitly, and the
comment records why the obvious form proves nothing.
Co-authored-by: Dawn (sprout agent) <c6237ef84fa537c78dcee78efd2d4e59f728859c7f194da42ac51ededfa0be05@sprout-oss.stage.blox.sqprod.co>
Signed-off-by: tlongwell-block <109685178+tlongwell-block@users.noreply.github.com>
|
||
|
|
59648f0706 |
test(desktop): harden terminal engine gates
Co-authored-by: tlongwell-block <109685178+tlongwell-block@users.noreply.github.com> Signed-off-by: tlongwell-block <109685178+tlongwell-block@users.noreply.github.com> |
||
|
|
a094adce9a |
feat(desktop): add the resize seam to buzz-terminal
The engine had no resize entry point, so the integration lane was about to build a coalescing protocol against a `Frame` that could not say which viewport it described. A consumer could only infer that from message ordering, which is exactly the thing a resize in flight breaks. `Terminal::resize` takes one coalesced target size, never a stream: resize is superlinear in scrollback (2.5-4.7 ms for a single column change at 10k history) and runs while holding the terminal. Frames now carry `generation`, `columns` and `screen_lines`, so a full frame is self-describing and a grid discontinuity is detectable from the frame rather than assumed from ordering. Three fixtures, each shown failing under its mutation. One of them only exists because the first version of this test suite was vacuous: Deleting the encoder's dedup-cache clear left all the resize tests green. The width-changing fixture cannot catch it -- changing the width changes every cell, so the hashes differ and the rows are republished for the wrong reason. A height-only resize is the discriminator: the width is unchanged, so reflowed rows hash exactly as before and a stale cache suppresses them right after the consumer was told to discard what it had. Growing 10 -> 20 lines moves row 0 to row 1, and under the mutation the frame carries 13 of 20 rows -- seven rows blank on screen. With that arm added the mutant dies. Co-authored-by: Dawn (sprout agent) <c6237ef84fa537c78dcee78efd2d4e59f728859c7f194da42ac51ededfa0be05@sprout-oss.stage.blox.sqprod.co> Signed-off-by: tlongwell-block <109685178+tlongwell-block@users.noreply.github.com> |
||
|
|
7e7731b89b |
feat(desktop): add buzz-terminal engine crate
Scaffolds the terminal emulator behind the Buzz substrate: grid state, the parser and its two hardening fences, the closed listener set, and the damage encoding the renderer will consume. No PTY, no child process, no window -- those belong to the embedder, so this crate stays testable against byte fixtures on every platform. Three things ship together because each is load-bearing for the others: * The crate itself. * The explicit `members` key in the workspace manifest. With a bare `[workspace]`, `cargo test/check --workspace` expands to a set that excludes this crate, and its gates pass green-and-empty over a real defect. Verified as a four-arm matrix on this repo. * A file-size ratchet rule for `src-tauri/crates`. The ratchet's only Rust root was `src-tauri/src`, so the crate would be born outside the repo's one size discipline -- silently, since the check exits 0. Verified: a 1500-line file under `crates/` passes without the rule and fails with it, while the real crate passes. Two fences, both enforced on byte counts rather than a clock, because a clock makes the bound depend on machine speed: * F1 aborts a synchronized update once it buffers 64 KiB. * F2 rebuilds the parser once 256 KiB of parser-visible bytes are charged. F1 turned out to be dual-purpose. Measured under a flood, the reader's lock hold is p50 1 us while the renderer's *wait* for the same lock is p50 4245 us -- so the plan's original "reader hold under one frame" requirement passes trivially while the window is visibly stuck. The gate is now the renderer's acquire latency, and F1 is what holds it down: p95 127 us with the fence, 16383 us without. That gate also needs a fair lock. `std::sync::Mutex` lets the read loop barge, and the renderer completed 12 frames to the reader's 39893 acquisitions; `FairMutex` under the same flood gives 76 and 63 us. Six fixtures, each shown failing under the mutation it exists to catch: deleting F1, deleting F2, omitting flush accounting, charging raw input, and swapping the fair lock for a barging one. Co-authored-by: Dawn (sprout agent) <c6237ef84fa537c78dcee78efd2d4e59f728859c7f194da42ac51ededfa0be05@sprout-oss.stage.blox.sqprod.co> Signed-off-by: tlongwell-block <109685178+tlongwell-block@users.noreply.github.com> |
||
|
|
3ade48d503 |
fix(desktop): point Oh My Pi preset at omp.sh (#3516)
## Summary Points the Oh My Pi preset at the `omp.sh` installation page instead of the GitHub repository. The project serves its current installer from `omp.sh/install.sh`. ### Related issue Extracted from the maintainer request in #3111. I found no matching open pull request in a final duplicate check. ### Testing `https://omp.sh/` returned HTTP 200 with the installation page. `https://omp.sh/install.sh` resolved to the current installer and returned HTTP 200. `cargo test --manifest-path desktop/src-tauri/Cargo.toml preset_entry -- --nocapture` passed 5 tests. `just ci` passed. This changes metadata only, so screenshots do not apply. Signed-off-by: Shreyash Vengurlekar <262980978+kiranmagic7@users.noreply.github.com> Co-authored-by: Shreyash Vengurlekar <262980978+kiranmagic7@users.noreply.github.com> |
||
|
|
fa1a5b1a79 |
fix(mesh): stop restarting a busy or loading shared-compute node (#3909)
## Problem Sharing compute with a large model (e.g. `gemma-4-26B`) put the desktop app into a **restart loop**: toggle Share → app appears to "download" / stall → the whole app restarts → repeat. Small models (E4B) were unaffected, which made it look model-specific and flaky. It is not model-specific and not flaky. It is a **false-positive liveness check**. ## Root cause (proven by black-box measurement) A `serve` node's OpenAI ingress (`:9337`) serializes **all** HTTP — including the `/v1/models` liveness probe — behind the current in-flight inference. It is *also* HTTP-unresponsive during model load and package-layer download. In every one of those phases the node is alive and progressing, but it cannot answer an HTTP probe. Measured on a standalone `gemma-4-26B` node (randomized ~30k-token prompt, cache-miss): | during one ~30s inference | result | |---|---| | concurrent `GET /v1/models` | **27.0s**, then 200 | | concurrent small `/chat/completions` | **28.8s**, then 200 | | `tcp_connect(:9337)` throughout | **~0ms** | Both HTTP calls simply queued behind the turn; TCP kept accepting instantly. A probe with any timeout shorter than the turn reads the node as dead. Buzz then acted on that false "dead" reading in two places, **both restart paths added in #2823**: 1. **Ingress watchdog** — after 2 consecutive `/v1/models` timeouts, evicts the node; for a serve node eviction means `app.request_restart()`. Two dead probes landing inside a prefill window → restart loop. 2. **Start / restore paths** — on a `wait_for_mesh_inference` timeout, `stop()` the node and (fresh start) `request_restart()` the app "to guarantee cleanup" — even though the node was still loading weights or downloading layers. This is the exact line in the incident log: `started node failed inference readiness … Buzz is restarting`. ## Fix Treat a **bound TCP port as alive**. Death has exactly one unambiguous signal: a *closed* port. - **Watchdog** (`recovery.rs`): only `PortClosed` may evict. A bound-but-HTTP-unresponsive `Unhealthy` port is never evicted, at any probe streak or urgency. Closed-port eviction is unchanged. - **Start / restore** (`commands/mesh_llm.rs`): install the runtime **before** probing readiness (so it is always tracked by `AppState` and can never be orphaned — which is what the restart was guarding against), and on a readiness timeout **leave it warming up** instead of stopping/restarting. Launch-restoration stays disarmed until real inference is confirmed, so a genuinely broken start is retried next launch rather than silently disabling Share Compute. ### What this deliberately does *not* do Detecting a node that is bound-but-internally-wedged needs a liveness signal that bypasses the inference lock. There is none today, so this fix cannot distinguish "wedged" from "busy" and errs toward not restarting. That gap is a mesh-llm bug, filed upstream: **Mesh-LLM/mesh-llm#1126** (lock-free `/live`+`/ready` on the ingress). A follow-up here can consume it once it lands. ## Tests - Watchdog never evicts a bound/busy port at any probe streak or urgency (the regression). - Closed-port eviction still fires (dead listener still reclaimed). - Black-box: a listener that accepts TCP then stalls HTTP classifies as `Unhealthy`, not `PortClosed`. - **Mutation-proven**: reverting the eviction rule to the old count-based logic fails the busy-node test. `cargo test` (desktop, `--features mesh-llm`) green, fmt + clippy clean. ## Not covered here The intermittent nature means I could not force the live loop deterministically on a warm machine; the proof is the measured serialization + the mutation-proven unit/black-box tests. Live behaviour (app no longer restarts while a 26B node loads/serves) still merits a manual check before merge. --------- Signed-off-by: Michael Neale <michael.neale@gmail.com> Co-authored-by: Michael Neale <michael.neale@gmail.com> |
||
|
|
eb049ddf81 |
feat(desktop): Agent Trading Cards — mintable agent-snapshot card PNGs with optional NIP-44 lock (#3278)
## Agent Trading Cards "Create Agent Card" action in the agent panel that mints an AI-generated trading card PNG which **is** the agent: the card carries the `buzz_agent_snapshot` tEXt chunk and is drag-in importable like any snapshot PNG. ### What's in here - **Mint pipeline (Rust):** one OpenAI Responses call — `gpt-5.6-sol` as card designer with `gpt-image-2` via the `image_generation` tool (~2–3 min). New `mint_agent_card` / `save_agent_card` commands; preview with reroll; save or send as `.agent.png` with round-trip verification before any bytes leave the app. - **Snapshot/chunk work stays in Rust,** reusing the existing encoder/decoder seams (byte-compat golden vector proves the plain path is identical to the pre-envelope encoder for placeholder, PNG-injection, and JPEG-transcode paths). - **Locked cards (NIP-44):** optional `buzz-agent-snapshot-encrypted` envelope encrypted to the (owner, agent) pair. `parse_canonical_pubkey` performs lift-x curve validation before any API spend; wrong-key decrypt returns a fixed refusal; the plain decoder refuses locked cards. - **Guardrails:** 10 MiB ceiling on final bytes, memory structurally `none` in the snapshot, full-manifest import disclosure, API-key hygiene via env layering (record > persona > global > process), fail-early validation ordering (all key/lock/NIP-44-cap checks before Responses spend). - **Import side:** full-manifest disclosure dialog, locked-card import disclosure, bounded avatar fetch. ### Review Code reviewed by Wren across the full arc; final locked-card cross-review **APPROVED 9/9/9** at exactly this head (`64f819dc8`), with independent same-SHA verification: Rust lib 1,843/1,843, clippy `--all-targets -D warnings`, desktop file-size gate. ### Live-mint evidence (real API, shipping seams, this SHA) - **Plain (Honey):** 188s, 1500x2250, 5,101,503 bytes (< 10 MiB); decoded manifest == built manifest; memory=none. - **Locked (Fizz):** 176s, 4,670,184 bytes; owner-key and agent-key decrypt both verified via logical manifest compare; wrong-key refusal exact; plain decoder refuses. - **Live finding:** built-in agents' ~171 KB inline avatars exceed the NIP-44 65,535-byte plaintext cap and the fail-early guard fires before API spend — clean error path, noted as a UX follow-up for large-avatar agents choosing lock. Full evidence (cards + dialog screenshots) posted in the originating thread. --------- Signed-off-by: Tyler Longwell <tlongwell@block.xyz> Signed-off-by: npub12gtutshhh76rx0jx697f32f9tffd4hhp3hx58fp4x6u4uemkm7sqf8f757 <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@buzz.block.builderlab.xyz> Signed-off-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz> Co-authored-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz> Co-authored-by: Tyler Longwell <tlongwell@block.xyz> Co-authored-by: npub12gtutshhh76rx0jx697f32f9tffd4hhp3hx58fp4x6u4uemkm7sqf8f757 <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@buzz.block.builderlab.xyz> |
||
|
|
3a96acea09 |
chore(release): release Buzz Desktop version 0.5.3 (#3972)
## Buzz Desktop release v0.5.3 - **Frozen main:** `54c8ef30a9bb9c59a4415a8a7ee84c7c5454b48a` - **Reviewed candidate:** `d0c06978bbf494ded6fe1a55d69d810ae9b65863` - **Previous desktop release:** `v0.5.2` - **Proposed immutable tag:** `desktop-v0.5.3` This PR must be **squash merged** only after the Desktop Release Candidate check passes. The branch must remain based directly on current ; stale base, payload drift, incomplete notes, or an unauthorized merge produce no tag. The checked-in changelog accounts for every non-merge commit in the release range. Publication remains bound to the immutable candidate tag. Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Release Automation <release-automation@users.noreply.github.com> |
||
|
|
bb34bc4d98 |
Revert "chore(release): release Buzz Desktop version 0.5.3" (#3960)
Reverts block/buzz#3944 |
||
|
|
d12b3d6a79 |
chore(release): release Buzz Desktop version 0.5.3
Co-authored-by: Release Automation <release-automation@users.noreply.github.com> Signed-off-by: Wes <wesbillman@users.noreply.github.com> |
||
|
|
c104eecfb3 |
feat(desktop): import local Pocket voices (#3259)
## Context Pocket TTS currently offers bundled reference voices. People also need a local, private way to add a voice without sending audio to a cloud service. ## Summary Add a Pocket voice import flow to Voice settings. Buzz opens the native file picker, decodes common audio formats in the reusable `buzz-voice` crate, canonicalizes the selected audio, stores it under a content-derived identity in app data, selects it, and lets the user delete it later. ## Changes - Accept WAV, M4A, MP3, FLAC, OGG, and AIFF files between 2 and 30 seconds, including multichannel sources. - Decode and downmix accepted audio to canonical mono 32 kHz PCM16 WAV before hashing and storage. - Store imported voices behind stable `pocket:imported:<sha256>` identities and content-addressed files. - Keep absolute file paths inside the native process and expose only voice metadata to React. - Include imported voices in Pocket preview and live huddle playback. - Add Add voice and delete controls while preserving the bundled Pocket voice catalog. - Fall back to Mary when the selected imported voice is deleted. - Keep durable import, selection, and deletion successful when a live TTS worker acknowledgement is delayed. - Preserve bundled voices when optional import metadata is unreadable and keep failed deletion retryable. ## Related issue None found. ## Testing Production decoding was exercised with WAV, M4A with AAC, MP3, FLAC, OGG Vorbis, and AIFF fixtures. Each format canonicalized to mono 32 kHz PCM16 WAV. Manual validation in the combined daily-driver build covered native-picker import, Preview, live-huddle playback, deletion, and Mary fallback. ## Screenshots The Voice settings card preserves the bundled Pocket catalog and adds the local Add voice action.  ## Reviewer-reproducible examples Create common-format fixtures and run them through the production importer: ```bash . ./bin/activate-hermit fixtures="$(mktemp -d)" ffmpeg -hide_banner -loglevel error -f lavfi -i "sine=frequency=220:duration=3" -ac 2 -ar 44100 "$fixtures/voice.wav" ffmpeg -hide_banner -loglevel error -i "$fixtures/voice.wav" -c:a aac "$fixtures/voice.m4a" ffmpeg -hide_banner -loglevel error -i "$fixtures/voice.wav" "$fixtures/voice.mp3" ffmpeg -hide_banner -loglevel error -i "$fixtures/voice.wav" "$fixtures/voice.flac" ffmpeg -hide_banner -loglevel error -i "$fixtures/voice.wav" -c:a libvorbis "$fixtures/voice.ogg" ffmpeg -hide_banner -loglevel error -i "$fixtures/voice.wav" -c:a pcm_s16be "$fixtures/voice.aiff" BUZZ_VOICE_IMPORT_TEST_DIR="$fixtures" \ cargo test -p buzz-voice imports_common_audio_format_fixtures -- --ignored --nocapture ``` Exercise import persistence, synthesis, deletion, and bundled-voice fallback with an installed Pocket model: ```bash BUZZ_POCKET_MODEL_DIR=/path/to/pocket-model-bundle \ cargo test -p buzz-voice --test pocket_import_audio \ objective_import_synthesis_delete_and_mary_fallback \ -- --ignored --nocapture ``` Exercise the native-picker boundary, selection, preview dispatch, deletion, cancellation, and invalid-file states: ```bash cd desktop pnpm build:e2e pnpm exec playwright test tests/e2e/voice-settings.spec.ts --project=smoke ``` --------- Signed-off-by: John Tennant <jtennant@block.xyz> Signed-off-by: John Tennant <johnmatthewtennant@gmail.com> Signed-off-by: John Tennant <jtennant@squareup.com> Signed-off-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz> Signed-off-by: npub12gtutshhh76rx0jx697f32f9tffd4hhp3hx58fp4x6u4uemkm7sqf8f757 <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@buzz.block.builderlab.xyz> Co-authored-by: John Tennant <jtennant@block.xyz> Co-authored-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz> Co-authored-by: npub12gtutshhh76rx0jx697f32f9tffd4hhp3hx58fp4x6u4uemkm7sqf8f757 <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@buzz.block.builderlab.xyz> |
||
|
|
61ba9dfaa0 |
refactor(voice): extract reusable Pocket primitives + Pocket voice settings (relands #2467 + #3208) (#3910)
Relands **#2467** (extract `buzz-voice` crate) and **#3208** (Pocket
voice settings) onto main, after #3266 and #3180 merged.
## Why a fresh PR
The repo is squash-only with delete-branch-on-merge. Squashing #3266
deleted `jtennant/pocket-tts-2026-04`, which was #2467's base — GitHub
auto-closed #2467 and it cannot be reopened. Squash merges also sever
ancestry, so GitHub's natural merge-base reports phantom conflicts for
the whole remaining stack.
## Content provenance
- Byte-identical to the blessed `jt/buzz-voice-refactor` branch
(`93029c577`, tree `6729e0eff` — reviewed by Dawn (#2467) and Max
(#3208) at exact heads) **except** the three files where #3180 and #3208
genuinely interact.
- Three-file resolution (union of both sides):
- `huddle/mod.rs` — #3180's pipeline re-exports + #3208's
`agent_tts_routing` imports.
- `huddle/state.rs` — `reset_preserving_generation` preserves both
`huddle_generation` (#3180) and `tts_enabled` (#3208); test sets merged
into one `tests` module.
- `desktop/src/testing/e2eBridge.ts` — both switch arms kept; no
duplicate case labels.
## Verification at
|
||
|
|
4632c55041 |
feat(desktop): auto-enable huddle transcription for agents (#3180)
## Context Before this change, every huddle initialized with transcription off. Joining or adding an agent did not enable it, so the agent could not receive spoken conversation until a person clicked the transcript control. Starting a huddle from an agent DM could also omit that agent, and adding an agent who already belonged to the parent channel could attempt an unnecessary role change and show a warning. Agent detection uses authoritative huddle membership. A participant counts as an agent when the ephemeral membership identifies it with the `bot` role, or when the existing agent identity model identifies the participant in an agent DM. ## Summary Buzz now enables transcription once when the first authoritative agent is present. After that initial automatic action, explicit user control is authoritative: manual ON or OFF survives membership refreshes, reconnects, and UI remounts. Removing the last agent does not change the current transcription state. Agent-DM huddles enroll the agent automatically. Adding an agent who already belongs to the parent channel preserves the existing parent role and completes without a role-mutation warning. | Scenario | Before | With this change | | --- | --- | --- | | First authoritative agent joins or is hydrated | Transcription stays off | Transcription turns on once | | User explicitly turns transcription on or off | Manual control exists without an agent policy | The explicit choice suppresses later automatic changes | | Last agent leaves | No defined agent-presence behavior | The current transcription state remains unchanged | | Huddle starts from an agent DM | The agent can be omitted | The known agent is enrolled automatically | | Added agent already belongs to the parent channel | Buzz can attempt a role rewrite and warn | Existing parent membership and role are preserved | | Transcription is active | The control is not visually distinct | The control is highlighted and exposes `aria-pressed=true` | ## Changes - Derive agent presence from authoritative bot-role huddle membership and known agent-DM identity. - Apply the one-time auto-enable rule during create, join, membership hydration, reconnect, pipeline startup, and local agent addition. - Preserve explicit user state and use huddle-generation guards so stale asynchronous work cannot alter a replacement huddle. - Keep backend and React transcription state synchronized, with a visible and accessible active control. - Enroll known agent-DM participants and make parent-channel membership updates idempotent. - Cover hydration ordering, reconnects, remounts, explicit OFF, last-agent removal, DM enrollment, existing membership, and active styling. ## Related issue None found. ## Testing Manual validation in `pending-seed` confirmed the product contract: 1. Started a huddle from the owned, running Fizz agent DM. 2. Confirmed the authoritative roster contained the human and Fizz as an agent. 3. Confirmed transcription enabled without clicking the control: `Stop transcript`, `aria-pressed=true`, with the highlighted active background. 4. Turned transcription off and confirmed `Start transcript`, `aria-pressed=false` remained stable. 5. Removed Fizz while transcription was off and confirmed the state stayed off. 6. Left the huddle cleanly. ## Screenshots The same control has distinct active and inactive states.   ## Reviewer-reproducible examples From a fresh checkout: ```bash pnpm --dir desktop build:e2e pnpm --dir desktop exec playwright test tests/e2e/huddle-transcription.spec.ts --project=smoke pnpm --dir desktop exec playwright test tests/e2e/mentions.spec.ts --project=smoke --grep "system agent profile exposes owned agent actions|system agent avatar exposes owned agent actions|owned bot profile exposes message and huddle actions|owned agent mention profile exposes message and huddle actions" ``` The huddle scenario exercises initial authoritative hydration, exactly one automatic enable, explicit OFF persistence, unchanged state after last-agent removal, newer events winning over delayed hydration, agent-DM enrollment, and idempotent parent membership. It also asserts `aria-pressed` and distinct computed active styling. --------- Signed-off-by: John Tennant <jtennant@squareup.com> Signed-off-by: npub1jh9wn95s0472h86ahapupaf7m6kx4v9sx2n0atj2hltcfer8k06s5n3pyf <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@buzz.block.builderlab.xyz> Co-authored-by: npub1jh9wn95s0472h86ahapupaf7m6kx4v9sx2n0atj2hltcfer8k06s5n3pyf <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@buzz.block.builderlab.xyz> |
||
|
|
081f805d5e |
feat(agent): optional reply guard reminds a silent turn to publish (#3763)
## Why
A Buzz agent's assistant text and reasoning are never shown to anyone —
only what it posts through the CLI. A turn that runs fifteen tool calls
and never publishes is a silent failure: the requester waits on a result
that was produced and thrown away.
This adds an optional reminder at the end-of-turn gate, off by default.
Tyler asked for it in buzz-mesh; plan iterated to **9.5/10 with @Wren**
(Minimalness 9.7, Elegance 9.5, Correctness 9.3).
## What
`BUZZ_AGENT_REQUIRE_REPLY=1` (default off, per-agent opt-in). A turn
about to end with no recognized attempt to post gets a reminder and is
rerolled. **At most two, then the turn ends regardless** — the guard
catches accidental omission, it does not compel speech. The reminder
text explicitly licenses silence so it cannot fight the base prompt's
"silence is usually correct."
**This is not a new MCP hook.** `RunCtx::run` *is* the turn, so the two
per-turn locals need no plumbing, and every tool call already passes
through it with arguments visible. The objection is appended at the
existing `_Stop` gate and rides `push_hook_outputs_as_tool_results`, so
the model receives it as a lower-trust tool result with `{hook, server,
text}` attribution. No new trust path, no new lifecycle event, no
dev-mcp or CLI protocol change.
Earlier revisions of this plan needed four crates (a `_UserPromptSubmit`
hook, a marker file, a `buzz-cli` change, dev-mcp state). Tyler pointed
out the agent already knows both facts; that deleted all of it. Net
runtime change is ~35 lines in `agent.rs` + ~4 in `config.rs`.
### Recognition contract
A registered non-hook tool whose qualified name ends in `__shell`, whose
`command` argument contains `messages send` or `reactions add`.
- **The `__` separator is exact, not approximate.** Given `has()` +
`!is_hook()`, `ends_with("__shell")` is *provably equivalent* to a bare
name of `shell`: registration forbids `__` in server and bare names
(`mcp.rs:227,268`) and qnames are `{server}__{bare}`, so a trailing
`__shell` could only straddle the separator if the bare name began with
`_` — which `is_hook` excludes. Without the separator, `powershell` and
`noshell` would match.
- **Reads the structured `command` field**, not serialized arguments, so
a `description` that quotes a send cannot disarm the guard, and a
non-string `command` is rejected rather than coerced.
- **Detects an attempt, not a successful publish.** A failed send
already returns non-zero exit and error JSON — louder than this
reminder. The variable is named `buzz_reply_call_seen` so the code can't
pretend otherwise.
- **Checked after the per-turn tool-call cap**, since a discarded call
never ran.
- `messages send` also covers `messages send-diff`. Reactions count
because the base prompt directs agents to react rather than post a bare
acknowledgement.
**Known limits, both deliberate and documented:** a command assembled at
runtime (`$CMD`) or hidden in a wrapper script is missed; text that
merely quotes a send (`echo "buzz messages send"`) matches. Missing a
real post is the expensive direction and substring matching is the
forgiving one there. Neither edge is pinned by a test, so the matcher
stays free to improve.
### Budget
Reminders share `BUZZ_AGENT_STOP_MAX_REJECTIONS`, the existing outer cap
on every end-turn objection. Default 3 fits both; at 1 only one fits; at
0 the guard is off with the hooks. A round carrying both a hook
objection and a reminder costs one rejection and delivers both texts. An
independent budget would either violate that bound or need a second
arbitration rule.
## Prior art
- **#3467** (closed) built the same detector one layer up in `buzz-acp`
for a different remedy. None of its symbols are on main — this borrows
its permission to be coarse, but reads structured data that ACP didn't
have.
- **#3648** (open) detects turns with *no output at all*; a turn with
fifteen tool calls and no post counts as output there, so it does not
cover this case.
- **#3741** (merged) is mesh-only.
## Testing
**14 new tests.** 4 unit tests on the matcher; 10 integration tests
through the ACP wire harness: off by default, `=0` still off, opted-in
silent → exactly 2 reminders then `end_turn`, registered `fake__shell`
send → 0 reminders, hallucinated `fake__shell` → still reminded, publish
call truncated past the 64-call cap → still reminded, budget 1 → 1
reminder, budget 0 → off, combined `_Stop` hook objection + reminder →
one round both texts and after 2 reminders the hook objection continues
alone, unparseable `=true` → startup error naming the key.
**10 mutation checks, each breaking a specific named test** — neutralize
the nag cap, stop sharing the budget, neutralize `buzz_reply_call_seen`,
drop `has`/`is_hook`, ignore the flag, drop the `__`, drop `reactions
add`, read serialized args, move detection before truncation.
`tests/bin/fake_mcp.rs` gains `FAKE_MCP_SHELL_TOOL=1`: it previously
exposed no tool with a bare name of `shell`, so the satisfied-guard path
was untestable.
Full `cargo test -p buzz-agent` green at 9e0ae1f04; clippy `-D warnings`
and `cargo fmt --check` clean.
**Unrelated flake found:**
`cancelled_turn_with_usage_emits_notification_before_response`
(`tests/fake_llm.rs`) is timing-sensitive. Under 10 loaded cores it
fails **2/20 on this branch and 1/20 at unmodified
`origin/main@02be413b8`** — pre-existing, not caused by this change
(which is inert without the env var). Flagging so it isn't misattributed
to the next PR that's open when CI hits it.
## Docs
`crates/buzz-agent/README.md` is the primary home (env var, recognition
contract, limits, budget interaction). `docs/MCP_DRIVEN_HOOKS.md` gets a
short cross-reference explaining this is *not* a hook — otherwise
readers hunt for a `_ReplyGuard` tool that doesn't exist.
---------
Signed-off-by: tlongwell-block <109685178+tlongwell-block@users.noreply.github.com>
Signed-off-by: npub1mprnacetjua2xx3p5eddmhxyk6wv929ymm5py8kd2xfxurxahspqqlgyta <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@buzz.block.builderlab.xyz>
Co-authored-by: Dawn (sprout agent) <c6237ef84fa537c78dcee78efd2d4e59f728859c7f194da42ac51ededfa0be05@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: npub1mprnacetjua2xx3p5eddmhxyk6wv929ymm5py8kd2xfxurxahspqqlgyta <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@buzz.block.builderlab.xyz>
|
||
|
|
d48b0e0eec |
feat(desktop): upgrade Pocket TTS model (#3266)
## Context Buzz Desktop currently installs an older Pocket TTS model bundle. The current bundle changes the tokenizer, learned BOS input, recurrent-state contract, and prompt behavior, so updating download URLs alone is not compatible. ## Summary This PR upgrades Buzz Desktop to the current pinned Pocket TTS model. It preserves existing product behavior and the hard 50-token model-input limit while adding the required runtime support, verified acquisition, and crash-safe cache migration. ## Changes - Pins an immutable Pocket TTS revision, artifact names, exact byte sizes, SHA-256 checksums, Mary reference voice, and license. - Loads the bundle-matched SentencePiece tokenizer, learned BOS embedding, and bundle-declared recurrent states. - Uses one pinned Pocket TTS configuration; no precision or model-version selector is added. - Preserves the resident engine's exact `<= 50` token contract without changing Desktop segmentation policy. - Bumps the Pocket cache manifest to v4, verifies size and checksum before adoption, atomically swaps the cache, and recovers the last verified cache after interrupted installs, including an incomplete final directory. - Keeps acquisition, cache migration, worker adoption, and tests within the existing Desktop implementation. - Removes the obsolete model-quality harness, which was coupled to the superseded production prompt and model layout. ## Related issue None. ## Testing Manual listening completed on the exact Desktop build. The updated model improved speech quality and resolved the phrase-start and sample-onset artifacts. Reproducible integrity and model checks are below. ## Screenshots N/A. This changes model installation and speech synthesis, not a visual surface. ## Reviewer-reproducible examples ### Before and after model identity ```sh git show 35305bfc8fd456ca9a17caa1ddbfaabd87d46981:desktop/src-tauri/src/huddle/models.rs \ | grep -E 'sherpa-onnx-pocket-tts|TTS_MODEL_VERSION' git show 211d17c58567448fe7ac95c4fa0ad2b88378849a:desktop/src-tauri/src/huddle/pocket_models.rs \ | grep -E 'MODEL_REPOSITORY|MODEL_REVISION|MODEL_PRECISION|MAX_TOKENS' ``` The target branch identifies the January bundle. The PR branch identifies the immutable April revision, INT8 precision, and 50-token maximum. ### Deterministic runtime validation Use the pinned artifacts listed in `pocket_models.rs` and run the model-dependent Pocket tests with the model directory supplied by the test environment. The checked-in long-sentence fixture must preserve its expected 48 and 44 token split and produce non-silent PCM. ### Manual listening validation John listened to an untrimmed Pocket TTS onset-stress clip generated from the exact user-provided passage, with every sentence synthesized separately and identical 100 ms digital-silence boundaries. The clip used no leading period, onset trimming, gain adjustment, or loudness normalization. The updated model produced better-quality speech and resolved the start-of-sample artifacts. --------- Signed-off-by: John Tennant <jtennant@block.xyz> Signed-off-by: npub1mprnacetjua2xx3p5eddmhxyk6wv929ymm5py8kd2xfxurxahspqqlgyta <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@buzz.block.builderlab.xyz> Co-authored-by: John Tennant <jtennant@block.xyz> Co-authored-by: npub1mprnacetjua2xx3p5eddmhxyk6wv929ymm5py8kd2xfxurxahspqqlgyta <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@buzz.block.builderlab.xyz> |
||
|
|
468647a51f |
feat(desktop): locally stored NIP-49 encrypted key backup (#2937)
## Summary
Adds a locally stored **NIP-49 encrypted key backup** (`ncryptsec`) to
the desktop app, per the plan reviewed in buzz-development (Rev 3,
approved 9/10 by Wren; implementation also reviewed and approved 9/10).
**Two-artifact design — canonical bytes originate entirely in Rust:**
- `create_ncryptsec_backup` runs under the `identity_mutation` lock:
encrypt → decrypt-verify against the live pubkey → atomic `0o600` write
to `{app_data_dir}/identity.ncryptsec` → reread/byte-compare → return
the exact persisted bytes. The frontend never re-derives or re-encrypts.
- `save_ncryptsec_copy` writes a portable copy via the save dialog
(parse-gated, secret-file semantics) and never mutates canonical state.
- `generate_backup_passphrase`: 6 words from the EFF short wordlist via
`OsRng` (custom passphrases min 12 chars).
- Import accepts `ncryptsec1` with optional password; the raw-`nsec`
path is untouched. Different-pubkey import and sign-out wipe the
app-managed backup (post-commit, best-effort — a failed import can never
destroy the still-live identity's backup; regression-tested).
**Never-relay guarantee (egress guard + tripwires):**
- `egress_guard.rs` fail-closed at all 8 `/events` submission boundaries
(relay submit funnel, 3× `relay.rs`, huddle STT, both engram submitters,
native WS choke point), rejecting `ncryptsec1`/`NCRYPTSEC1` in text and
binary frames. Scope is deliberately ncryptsec-only: pairing
intentionally carries raw nsec inside its encrypted session.
- Site-granular `/events` inventory tripwire: per-file (`/events` count,
guard-call count) pairs; unlisted files expect zero. Mutation-style
tests prove a ninth site in an existing file, a removed guard, and a new
unlisted file all fail the scan.
- ncryptsec source-allowlist scans in **both** trees (Rust + TS).
**Frontend:** onboarding `BackupStep` is encrypted-by-default — the
default path never invokes `get_nsec` (e2e asserts the command log).
Raw-nsec export stays behind an explicit click with prior semantics.
Shared `EncryptedBackupCreator` powers onboarding + a new settings row;
the import form auto-switches to encrypted mode on `ncryptsec1` paste
(case-insensitive HRP).
**Open product call for @tlongwell-block:** onboarding default is
*encrypted* in this PR; flipping to raw-default is a small change either
way (documented in the plan).
Review history: plan Rev 3 and the implementation were both iterated
with Wren to 9/10 (two blockers from round 1 — import ordering,
inventory granularity — plus an uppercase-bech32 hardening gap, all
fixed in `dde37183e`). Thread: buzz-development.
### Related issue
Follow-up to the direction explored in #385 (NIP-PB, closed) — this
ships local NIP-49 (the standard) instead of a new NIP. No open
duplicate found.
### Testing
All at exactly `dde37183e` (same shell, HEAD verified):
- `cargo test` — 1680 passed / 0 failed / 14 ignored (includes a
deliberate ~70s log_n-18 NIP-49 round trip, spec vector, wrong-password,
NFKC, uppercase-vector decrypt, injection test per egress boundary,
inventory mutation tests, import-ordering regression tests)
- `cargo clippy --all-targets -- -D warnings` — clean; `cargo fmt
--check` — clean
- `pnpm typecheck` — clean; JS unit suite 3529/3529; biome (repo-pinned
2.4.16) clean
- Playwright `onboarding-backup` / `onboarding` /
`onboarding-agent-defaults` / `profile-nsec-reveal` — 86 passed, 1 known
avatar-reservation flake (passed on rerun; untouched by this diff).
`passThroughBackupStep` now exercises the encrypted default, so every
downstream onboarding spec covers the new path.
- Note: browser e2e fakes the crypto via the mock bridge (fixed
spec-vector blob); decryption correctness is proven in the Rust tests.
## Latest onboarding integration
The current head adds an additive `IdentityInfo.storage` field
(`ephemeral`, `system-keyring`, `local-file`, or `environment`) so
onboarding can accurately explain where the active identity is
protected. It surfaces storage metadata only—never key material—and
leaves the existing lost/keyring-locked recovery behavior intact.
---------
Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Co-authored-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Co-authored-by: npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w <52a228d6edf316ec6812ac3c9fc0d696ab59fc7954d77e7be31eedcddf91335b@buzz.block.builderlab.xyz>
|
||
|
|
36571f4adc |
fix(desktop): allow linux-only media items as dead code off-linux (#3811)
Local `desktop-tauri-clippy` fails on macOS with dead-code errors for `PROD_ORIGIN`, `DEV_ORIGIN`, and `is_trusted_media_origin`, which are only used inside `#[cfg(target_os = "linux")] enable_media_capture`. The items are intentionally platform-independent so unit tests run everywhere. Added `cfg_attr` allow attribute to suppress the warnings on non-Linux targets. Since [#3607](https://github.com/block/buzz/pull/3607), this affects all Rust developers on macOS. Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: d32955ad69077062930cc46cfe2df30ca9aaf6f8e76422681265e9e9af704d78 <d32955ad69077062930cc46cfe2df30ca9aaf6f8e76422681265e9e9af704d78@buzz.block.builderlab.xyz> |
||
|
|
114d40d9d3 |
feat(relay): gate kind 30178 team-catalog reads behind the shared tag (#3358)
Team catalog projections (`kind:30178`) embed every member's system
prompt, so they need the same read gate personas already have: only the
author sees an unshared event. The gate was hardcoded to `kind:30175` at
six read surfaces plus the SQL pushdown, so rather than adding a second
special case it becomes kind-generic over `SHARED_GATED_KINDS = {30175,
30178}`.
## Kind 30178
New parameterized-replaceable kind, addressed by `(pubkey_o, 30178,
team_id)`. It embeds sanitized member projections instead of referencing
`kind:30175` heads — a foreign reader of a shared team could not
otherwise hydrate members whose own persona events are unshared or, for
built-ins, absent entirely. `kind:30176`'s wire body is untouched, so
device sync keeps its contract.
## Kind-generic shared gate
`buzz_core::kind` replaces `is_persona_shared_kind` /
`is_unshared_persona_event` / `persona_event_is_shared` with
`SHARED_GATED_KINDS` and the kind-agnostic `is_shared_gated_kind` /
`is_unshared_gated_event` / `event_is_shared`. Every read surface
consults the set:
| Surface | File |
|---|---|
| REQ historical delivery + `ids` lookup |
`crates/buzz-relay/src/handlers/req.rs` |
| Live fan-out | `crates/buzz-relay/src/handlers/event.rs` |
| COUNT fallback | `crates/buzz-relay/src/handlers/count.rs` |
| NIP-98 HTTP `/query`, `/count`, `/search` |
`crates/buzz-relay/src/api/bridge.rs` |
| Pre-`LIMIT` SQL pushdown | `crates/buzz-db/src/event.rs` |
The SQL clause generalizes from `kind != 30175` to `kind NOT IN (...)`
bound from `SHARED_GATED_KINDS`, still applied before `ORDER BY … LIMIT`
so a page of newer private events cannot starve an older shared one off
the candidate set. `EventQuery::persona_reader` is renamed
`shared_gated_reader` and `needs_persona_filtering` to
`needs_shared_gate_filtering` to match.
Because the `buzz-core` rename has consumers outside the relay, the four
desktop call sites of `persona_event_is_shared` travel with it:
`desktop/src-tauri/src/commands/personas/pending.rs`,
`desktop/src-tauri/src/event_sync.rs`, and two in
`desktop/src-tauri/src/managed_agents/persona_events.rs`. Each call is
unchanged apart from the name — the persona `shared` projection behaves
exactly as before.
## Ingest validation
`validate_persona_envelope` splits into two reusable pieces —
`validate_shared_tag` (exactly-two-element `["shared","true"]`, at most
one occurrence) and `single_bounded_d_tag` (exactly one `d` tag,
non-empty, `<=64` chars, no ASCII control characters or whitespace).
`validate_team_catalog_envelope` composes both; personas additionally
keep the slug grammar `^[a-z0-9][a-z0-9_-]{0,63}$`.
`kind:30178` deliberately does **not** get the slug grammar. Team ids
are UUIDs or built-in identifiers such as `builtin-team:welcome`, and
the colon is not slug-legal; rewriting ids to fit would break NIP-33
addressing against the team's own `kind:30176` head. The non-empty and
exactly-one checks are load-bearing regardless — without them generic
NIP-33 storage maps a missing `d` onto `(pubkey_o, 30178, "")` and every
team overwrites its predecessor.
The exact two-element `shared` shape is enforced because the SQL
visibility clause is JSONB containment (`tags @>
'[["shared","true"]]'`), which would match a three-element superset such
as `["shared","true","extra"]`.
`kind:30178` is also added to the `Scope::UsersWrite` allowlist and to
`is_global_only_kind`, so a stray `h` tag cannot channel-scope an
owner-authored definition.
## Deferred
`kind:30176` is deliberately not a gate member. Its writers never emit
`shared`, so catalog opt-in semantics do not describe it — it needs
owner-private reads driven by an authenticated principal set, tracked as
a separate follow-up.
## Tests
- 19 new `ingest.rs` unit tests covering the 30178 envelope (UUID and
colon `d` tags, 64-char boundary, non-ASCII bound,
empty/valueless/duplicate/missing `d`, embedded newline, `shared`
false/three-element/duplicate, scope and global-only membership).
- Persona regressions for the valueless `["d"]` shapes, since the
`d`-tag helper is shared by both validators.
- Existing `kind.rs` gate tests generalized and extended to assert the
gate applies to 30178 as it does to 30175.
- New `crates/buzz-test-client/tests/e2e_team_catalog.rs`: 9 WS-level
tests over a live relay covering author reads of unshared heads, foreign
omission from REQ, `ids`-lookup denial, COUNT existence-leak, share and
unshare transitions, and the mixed-kind filter case.
- `.github/workflows/ci.yml` adds `--test e2e_team_catalog` to the Relay
E2E job so the new suite runs.
## Docs
`docs/nips/NIP-AP.md` gains a "Team catalog projection: kind:30178"
section and an "Ingest validation: kind:30178" subsection, records the
gate as kind-generic, documents 30178 deletion vs. unshare semantics,
and adds a security note that sharing a team exposes every member's
instructions even when that member's own `kind:30175` head is unshared.
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
|