Skip to content

[upstream-sync] Merge block/buzz 39ce3dfc3..db7e84d4f (7 commits) - #10

Merged
adrienlacombe merged 9 commits into
mainfrom
upstream-sync-2026-07-31-b
Jul 31, 2026
Merged

[upstream-sync] Merge block/buzz 39ce3dfc3..db7e84d4f (7 commits)#10
adrienlacombe merged 9 commits into
mainfrom
upstream-sync-2026-07-31-b

Conversation

@adrienlacombe

Copy link
Copy Markdown
Owner

Daily upstream sync. Merge with a merge commit — not squash. A squash drops
the second parent, leaves the merge base stale, and every later sync re-resolves
the same conflicts from it. This fork has already been repaired by hand once for
exactly that (3ce7c8adc).

Range

39ce3dfc3..db7e84d4f — 7 commits. Clean merge, no conflicts.

Merge commit has 2 parents (b2a21fdc2 + db7e84d4f); git rev-list --count upstream/main ^HEAD is 0.

What changed upstream

Desktop — Pocket voice import (block#3259). New buzz-voice imported module
(730 lines) plus an audio test, Tauri import_pocket_voice /
delete_pocket_voice commands, tts_voice_import.rs, and VoiceSettingsCard UI
with a new e2e spec.

Release plumbing (block#3943, block#3965). prepare-desktop-release.yml deleted and
replaced by desktop-release-candidate.yml; desktop tagging made squash-safe
(target_sha now merge_commit_sha, not head.sha); new
desktop-release-bypass-authorized.jq filter with a fixture and mutation tests.

Version revert (block#3960, block#3944). The 0.5.3 desktop bump was reverted — the
desktop crate is back at 0.5.2. Not a fork action; nothing to reconcile.

Docs (block#2864). NIP-RS manual-unread override layer, plus a formal model under
docs/formal/nip-rs-unread/ (~3200 lines of Python model/mutation/exhaustive
checkers).

Conflicts

None — clean merge.

Per AGENTS.md, a clean merge is not a correct merge, so I read the diff at
every fork-local patch site. Upstream touched exactly two of them:

Site Upstream change Assessment
release.yml 1 line: the assemble-manifest "Upload latest.json to rolling release last" step dropped its env.already_published != 'true' gate Benign. Lands well away from every fork marker. All 15 FORK-LOCAL hunks intact — RELEASE_REPO guards, BASE/BUZZ_UPDATER_ENDPOINT deriving from github.repository, and the job-result asserts that replaced the unreachable -ge 3 count. Verified the fork's mutual-exclusion assert between the signed and unsigned macOS lanes still reads correctly against upstream's new surroundings
desktop/src-tauri/src/lib.rs 2 added command registrations in invoke_handler Benign. Nowhere near the fork's mod relay_allowlist; (line 32)

Spot-checked the untouched patches too: tauri.conf.json productName is still
BitcoinMarkets (version correctly follows upstream back to 0.5.2), the
Info.plist / xcconfig / gradle brand strings survive,
KIND_STARKNET_WALLET_BINDING is still 30900 with its
is_parameterized_replaceable assert, and both relay allowlists are in place.

migrations/ is unchanged — still 28 files, matching the migrations.len()
assertion. No new migration in this sync.

One fork patch went quietly no-op

Upstream's bump moved the root Cargo.lock mesh-llm pin to tag=v0.74.0
(e60b2fe4) — which is what desktop/src-tauri/Cargo.lock already pinned. The
two locks now name the same rev, so ci.yml's fork-local "resolve the rev from
the desktop lock, not the root one" patch resolves identically either way.

This is the failure mode AGENTS.md warns about: a patch that merges cleanly and
stops doing anything, whose comment now cites revs (v0.73.1 / f455d493) that
no longer appear in either lockfile — so the next sync could read it, fail to
find them, and delete the patch as obsolete.

Kept the patch, corrected the record (a43a4deb2, comments + the AGENTS.md
row only, no behaviour change). Desktop is excluded from the root workspace, so
the locks still resolve independently; the convergence is a coincidence of this
one bump, not upstream adopting the fix. The next bump moving one lock and not
the other re-breaks the root-lock version.

Verification

Run locally on macOS at a43a4deb2:

Gate Result
cargo fmt --all --check pass
cargo fmt --manifest-path desktop/src-tauri/Cargo.toml --all --check pass
cargo clippy --workspace --all-targets -- -D warnings pass
cargo clippy --manifest-path desktop/src-tauri/Cargo.toml --all-targets -- -D warnings pass
cargo metadata --locked pass — lockfiles resolve, no hand-editing needed
scripts/test-release-ref-contract.sh pass (exit 0)
scripts/test-mobile-worktree-overrides.sh pass — all brand-rename contract checks
just test-unit pass — 324 buzz-core, 275 buzz-cli, 94 buzz-db, 22 conformance, 15 push-gateway, 0 failures
pnpm check:px-text pass
pnpm exec tsc --noEmit pass

Two extra gates beyond the standard list, because upstream landed ~400 lines of
desktop TS: the px-text guard and the typecheck. Both clean.

test-release-ref-contract.sh is worth calling out — upstream tightened it in
this range. It now asserts the rolling-updater step is not gated by
already_published, and adds five mutation tests against the new bypass filter.
The fork's patched release.yml satisfies the new assertions unchanged.

Not run: flutter analyze — the Hermit Dart is older than
mobile/pubspec.yaml's constraint and fails to resolve, as documented. Relying
on CI. Low risk here: upstream touched no mobile files in this range.

Needs a human look

Nothing blocking. Three things worth knowing:

  1. New PR check. desktop-release-candidate.yml runs on every PR to main,
    including this one. Its only real step is gated on a version-bump/ head-ref
    prefix, so it no-ops here — but it is a new always-present check.
  2. auto-tag-on-release-pr-merge.yml changed upstream (head.sha
    merge_commit_sha). That workflow is disabled in this fork by repo
    setting, so no effect. Flagging only because the sync moved it.
  3. Two upstream commits carry no Signed-off-by (bb34bc4d9, 6e02e0a90).
    They are upstream's own and cannot be signed without rewriting their history,
    which would destroy the merge base. No DCO app is installed on this fork — the
    check listed in AGENTS.md belongs to block/buzz — and PR [upstream-sync] Sync upstream block/buzz → 2026-07-31 (23 commits) #9 merged the
    same way. Both fork-authored commits here are signed.

No wire-format changes. No kind renumbers. No migrations. No fork patch deleted.

johnmatthewtennant and others added 9 commits July 31, 2026 11:15
## 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.

![Pocket TTS voice
import](https://raw.githubusercontent.com/block/buzz/c03ba29060ca544c5ac3394c212f376651b386a3/pr-3259--pocket-voices.png)

## 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>
## Summary

- document `Prepare Desktop Release` as the canonical desktop release
entry point
- describe the frozen candidate, exact-head approval, and true
merge-commit contract
- document all platform outputs and complete release App/signing
configuration
- link the release runbook from the README
- allow stable reruns to repair the rolling updater manifest after the
versioned release has already published

## Release blocker

The live repository cannot currently complete this flow: repository
settings disable merge commits and the `main` ruleset allows only
squash, while `scripts/verify-desktop-release-merge.sh` requires a
two-parent merge whose second parent is the approved candidate. Those
settings must allow merge commits before a desktop release PR is merged.

## Validation

- `bash scripts/test-desktop-release-candidate.sh`
- `bash scripts/test-release-ref-contract.sh`
- `git diff --check`
- verified live repository merge settings, `main` ruleset, release tag
ruleset, Actions variable names, and secret names with GitHub API
- independent review by Princess Donut; incorporated all findings,
including the rolling-manifest retry gap and unsigned Windows labeling

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
Co-authored-by: Release Automation <release-automation@users.noreply.github.com>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
chore(release): release Buzz Desktop version 0.5.3
…rification model to NIP-RS (block#2864)

## Summary

Amends `docs/nips/NIP-RS.md` with the manual mark-as-unread override
layer and includes `docs/formal/nip-rs-unread/`, the bounded exhaustive
verification model that preceded and informed the spec.

All `ov_*` override state lives in exactly one coordinate per
installation. That single constraint is what makes the rest of the
amendment small: override state never moves between coordinates, so
there is no slot lifecycle to make crash-safe, and the only durability
obligation is carry-forward on `client_id` rotation.

## Spec changes (`docs/nips/NIP-RS.md`)

- **Non-Goals:** drop the stale line stating mark-as-unread is out of
scope; state the `ov_*` durability exception to the
best-effort/time-horizon model.
- **Reserved Namespace:** `ov_` stem and `esc:` escape marker reserved.
Escape on publish (prepend `esc:` to raw IDs beginning with `ov_` or
`esc:`), unescape on receive (strip exactly one `esc:`). Bijection, with
the pre-amendment backward-compat residual documented as a stated
limitation.
- **Content Validation:** override entries are collected and validated
as a complete logical group *before* any decoding, zero-filling,
merging, or canonicalizing. Only two wire shapes are accepted — a
complete live three-key group, or an `ov_c:`-only tombstone floor. Any
other shape rejects the whole group while retaining the frontier entry;
applying the generic per-entry discard rule first is prohibited.
- **`d` Tag:** `<slot-id>` is exactly 32 lowercase hexadecimal
characters, replacing "a random opaque string" of 1–64 ASCII characters.
The fixed shape lets a relay recognize a read-state coordinate
structurally from the `d` tag alone, without decrypting anything, and
apply per-coordinate protections to it — under the old wording a
conforming client could pick a shape that silently forfeits them.
Recognizable coordinates are also what let a relay replace superseded
versions outright rather than accumulating one retained row per publish,
which keeps the coordinate count a full-state load must enumerate near
one per installation. Every client designates one **primary** coordinate
with a stable `<slot-id>` for the installation's lifetime. All `ov_*`
entries, and the frontier entries of the contexts they belong to, MUST
live in the primary. Additional coordinates remain legal for frontier
volume but MUST NOT carry `ov_*`, which keeps them freely rewritable and
freely deletable.
- **`t` Tag:** described as a discoverability marker rather than a
guarantee of relay-side selectivity. A relay MAY apply tag constraints
after its result cap, and `kind:30078` is shared with unrelated
application data, so clients MUST apply the tag as a correctness filter
locally, MUST NOT infer completeness from a short result, and MUST omit
the tag entirely when performing a full-state load.
- **Fetching / Full-State Load:** clients implementing the override
layer MUST NOT apply a finite `since` filter — an encrypted payload
means a relay filter cannot select for override-bearing events, so any
event-level window can exclude the only coordinate holding a tombstone
floor. Removing `since` is not sufficient: relays MAY cap historical
results, MAY cap below the requested `limit`, and emit
end-of-stored-events after the capped query, so neither EOSE nor a short
page proves completeness. No test against the client's requested `limit`
can detect truncation either: the effective cap belongs to the relay, a
relay MAY cap below what was requested, and an advertised maximum limit
is not necessarily the limit enforced.

A full-state load is therefore enumerated on `{"kinds": [30078],
"authors": [<pubkey>], "limit": <n>}` with **no tag constraint**. A
relay MAY apply tag constraints only after its result cap and withhold
the events that fail them, so under a tag-constrained filter the
delivered count is not the count the cap selected — a delivered page can
be empty while older coordinates still exist below it, and `kind:30078`
is arbitrary application data whose `d` tag namespace is open to every
application that has written under the user's key. Omitting the tag
makes delivery observable; read-state selection moves client-side, where
the validation rules already place it.

Completeness is then established by enumeration on a strictly decreasing
cursor: collect a page, descend on the lowest `created_at` across all
delivered events, exhaust that second with a window pinned to it,
continue below it, and treat only an empty delivery as complete. Every
query carries the same explicit `limit` `n` with `n >= L`. Per-second
exhaustion is discharged by comparing the pinned window's delivery
against the largest delivery the relay has already demonstrated in the
same load, floored at `L = 2` so that the ordinary single-coordinate
installation can reach *complete* at all. The comparison fails safe: an
inconclusive window reports *cannot prove complete* rather than
*complete*, and that verdict is terminal for the load.

Because these are addressable events, a coordinate republished mid-load
moves *above* the descending cursor while its previous version stops
existing, so neither is reachable by any later query. A full-state load
is therefore fenced by a live subscription on the same tag-free filter,
established — defined as receipt of end-of-stored-events — before the
first enumeration query and held unbroken on the same connection for the
load's duration. Fence deliveries are collected like enumerated events
but do not contribute to the cursor or to the demonstrated-delivery
bound. Collection deduplicates coordinates on the full NIP-01
addressable ordering — greatest `created_at`, lowest event id on ties —
because an equal-timestamp replacement is legal and is the version the
relay retains. A lapsed or reconnected fence makes the load potentially
incomplete, and a client MUST NOT publish to its own coordinates during
its own load.

Five relay behaviours the *complete* verdict rests on are stated as
normative conformance preconditions rather than assumptions, because
none is verifiable from the responses a client receives: newest-first
prefix delivery with lowest-id tie-breaking (what NIP-01 already
specifies for `limit`), a non-decreasing effective cap within a load,
the floor `L`, push delivery on an open subscription, and a delivery
barrier ordering accepted matching events ahead of a query's
end-of-stored-events on the same connection. Conditioning *complete* on
positive proof of these instead would withdraw the override layer from
every client rather than from the non-conforming relays. A client MUST
NOT load against a relay it has evidence violates them, and MUST treat
any such load as potentially incomplete.

A load that is potentially incomplete, or that failed on any relay the
client publishes to, MUST NOT authorize canonical compaction, publishing
a canonicalized override blob, deleting or abandoning a coordinate, or
reporting a mark-read as successful; the client falls back to local
state.
- **Client-ID Rotation / Orphaned Blob Deletion:** rotation is the only
event that changes an override-bearing coordinate. Before deleting or
abandoning its previous primary, a client MUST republish the
componentwise `max()` of every register that primary holds — every
tombstone ceiling included — under its new primary, and MUST confirm
acceptance on **every relay** from which the old primary will be deleted
or allowed to lapse. Acceptance on one relay does not authorize deletion
on another. Frontier-only orphans are deletable unconditionally; an
unknown same-`client_id` coordinate is treated as a live carrier until
merged.
- **Live Subscription and Convergence:** the re-publish trigger and its
suppression are evaluated on canonicalized state, so a retained live
peer blob the client has already tombstoned cannot trigger an identical
write on every replay.
- **Manual-Unread Override Layer** (new section):
- **Wire encoding:** `ov_s:<ctx>`, `ov_c:<ctx>`, `ov_b:<ctx>` as uint32
siblings in the existing `contexts` map.
- **Merge rule:** componentwise `max()` per counter — no new wire merge
logic.
- **Liveness predicate:** `S > 0 AND F <= B AND S > C`, transcribed from
`model.py::override_set_b`.
- **Actions:** mark-unread bumps S and captures the effective frontier
as B; mark-read bumps C; a natural frontier advance past B deactivates a
stale set with no counter update. Every action requires a complete
full-state load. At the uint32 ceiling, wrapping and resetting are
prohibited: mark-unread is refused, and mark-read completes only if the
resulting state has `override_active == false` — otherwise it fails
visibly rather than reporting success over a still-live override.
- **Tombstone floor:** a dead ever-active register compacts to `RegB(0,
max(S,C), 0)` — a single `ov_c:` key. A virgin register is omitted
entirely. This blocks counter reuse and the resulting resurrection.
- **Mandatory canonical publication:** a protocol requirement, not an
optimization. Publishing raw dead registers lets two independently-dead
registers from different devices produce a live join.
- **Override group co-location rule:** a context's frontier entry and
all its `ov_*` siblings MUST travel in the same event, and that event
MUST be the primary coordinate. An override-bearing context therefore
has exactly one legal destination for its whole group; only
frontier-only groups may be distributed across additional coordinates.
Grouping is per logical context, never per key.
- **Unescape-before-group rule:** the frontier wire key MUST be
unescaped to its raw logical context ID before use as group identity.
Equal normative weight to atomic grouping.
- **Tie policy:** clear-wins is MUST. The tie verdict is not encoded on
the wire, so a selectable policy makes two conforming clients diverge
permanently on both the unread verdict and the canonical wire form.
- **Override State Durability:** `ov_*` entries are exempt from age
pruning and budget eviction permanently, and durability is defined over
retrievable logical state — the containing event must stay reachable and
the load must establish completeness, not merely retain keys. There is
no safe finite GC horizon.
- **Bounds and budget:** byte/key analysis at both small-counter and
uint32-maximum values. Confining `ov_*` to one blob makes its plaintext
budget a hard lifetime ceiling on ever-overridden contexts — roughly 600
tombstones at the worst-case ~54 bytes against 32 KiB, ~730 at the
common ~45 bytes, ~199 simultaneously live overrides at ~164 bytes. At
the ceiling a client MUST refuse mark-unread and MUST NOT split override
state, drop floors, or publish a truncated override set. Same policy
shape as counter exhaustion: visible failure, never silent degradation.
- **Verification artifact:** `docs/formal/nip-rs-unread/`. The model is
a broader predecessor of this NIP: its `split_blob_into_slots` permits
override groups in any slot, so verified atomicity covers every
arrangement this NIP allows, but the converse does not follow. The model
does not verify the single-primary rule, the completeness procedure, the
relay conformance requirements or the mutation fence, or carry-forward;
malformed-group wire validation is likewise normative but outside
verified scope.

- **Abstract / Non-Goals / Backwards Compatibility:** the absolute "no
relay-side logic" and "no relay behavior changes" claims are narrowed to
what remains true — no new event kind, no new wire message, no
relay-stored read-state logic — with the override layer's relay
conformance contract named as the exception. Frontier sync and clients
that skip the override layer are unaffected on any relay.

## Verification model (`docs/formal/nip-rs-unread/`)

Four Python files constituting a bounded exhaustive verification model
for the override layer's register algebra.

**What it does:** constructs a toy universe — 2–3 devices, 2 channels,
every action that can happen (mark-unread, mark-read, late/duplicate
syncs, app reinstall, storage compaction) — and brute-forces every
reachable ordering (14,258 BFS states; 672-point deep-history parameter
cube; 9-mutant harness over ~45,000 merge pairs). After each world-state
it asks: did all devices converge? Did any unread flag get resurrected
after being cleared, or vanish while live?

**What it found and fixed:**

1. **Killed candidate A.** The model produced a concrete kill sequence:
an old client that doesn't know about the new field rewrites its
read-state blob and silently erases unread flags. That witness is why
the spec uses candidate B (two counters that only count up, plus a
snapshot) instead.
2. **Candidate B passes everything.** All delivery orders converge; the
frontier high-water mark never regresses; duplicated/replayed syncs are
harmless; old clients can't destroy it; compaction never resurrects a
dead unread or drops a live one, including
cleanup-followed-by-weeks-late-stale-sync and
tombstone-landing-on-unrelated-live-state corner cases.
3. **Caught a second real bug late.** Two devices each publishing "this
unread is cleared" could, on merge, reactivate it. The fix (canonicalize
before publishing) is a mandatory rule in the spec; the model re-checks
it across ~45,000 merge pairs.

**Scope and caveats:** bounded to 2–3 devices and 2 channels. Can't
prove the infinite case. `NOTE.md` documents the exact verification
scope and the gap between the model's `split_blob_into_slots` generality
and the single-primary rule the spec adds on top.

**Why it's in the repo:** the spec asserts "verified by bounded
exhaustive model checking." Keeping the artifact in-repo means anyone
who later amends the merge/compaction rules can `python3 exhaustive.py
&& python3 mutation.py` (deterministic, exit 0) and confirm the
guarantees hold. Without it the spec claims a proof nobody can check.

## Diff scope

`docs/nips/NIP-RS.md` — spec amendment, zero product code.

`docs/formal/nip-rs-unread/{NOTE.md,model.py,exhaustive.py,mutation.py}`
— bounded exhaustive verification model, zero product code.
`.gitignore` — `__pycache__/` and `*.pyc` entries for the model
directory.

---------

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
## Summary
- validate desktop release candidates before merge and keep the
repository squash-only
- tag the squash commit only after proving frozen-base parent and
complete-tree identity with the validated PR head
- accept either an exact-head approval or the durable Default-ruleset
bypass record as release authorization
- remove the unusable App-backed preparation workflow; retain `just
release-desktop`

## Ruleset follow-up
After this PR merges, update Default ruleset `13596885` to:
- enable strict required status checks
- dismiss stale reviews on push and require approval after the last push
- require the integration-bound `Desktop Release Candidate` check

The next desktop release should be cut only after that settings update.

## Verification
At commit `d8c254db427eedbcffac1a6e078e90d1d0f5e151` with a clean
worktree:
- `scripts/test-release-ref-contract.sh`
- `scripts/test-desktop-release-candidate.sh`
- `bash -n scripts/verify-desktop-release-merge.sh
scripts/prepare-desktop-release.sh scripts/test-release-ref-contract.sh`
- `git diff --check`

The bypass test fixture is the captured rule-suite shape from real
squash merge PR block#2864 / suite `3520068134`.

---------

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
Range 39ce3df..db7e84d. Clean merge — no conflicts.

Upstream content:
- feat(desktop): import local Pocket voices (block#3259) — new buzz-voice
  `imported` module, Tauri `import_pocket_voice`/`delete_pocket_voice`
  commands, VoiceSettingsCard UI.
- fix(release): immutable desktop release operability (block#3943, block#3965) —
  prepare-desktop-release.yml replaced by desktop-release-candidate.yml,
  squash-safe tagging, new bypass-authorization jq filter and fixture.
- Revert of the 0.5.3 desktop version bump (block#3960, block#3944) — desktop is
  back at 0.5.2.
- docs(nips): NIP-RS manual-unread override layer plus a formal model
  under docs/formal/nip-rs-unread/ (block#2864).

Fork-local patch sites upstream touched, both benign:
- release.yml — one-line `if:` relaxation on the assemble-manifest
  latest.json upload, away from every fork marker. All 15 FORK-LOCAL
  hunks intact, including the job-result asserts that make the fork's
  auto-update manifest publishable.
- desktop/src-tauri/src/lib.rs — two command registrations, nowhere near
  the fork's `mod relay_allowlist;`.

Signed-off-by: adrienlacombe <6303520+adrienlacombe@users.noreply.github.com>
…atch

Upstream's 0.5.3 range bumped the root Cargo.lock's mesh-llm pin to
tag=v0.74.0 (e60b2fe4), which is what desktop/src-tauri/Cargo.lock already
pinned. The two locks now name the same rev, so ci.yml's fork-local "resolve
the rev from the desktop lock, not the root one" patch resolves identically
either way and is currently a no-op.

That is exactly the shape AGENTS.md warns about — a patch that merges cleanly
and quietly stops doing anything — so record it rather than let a future sync
read the stale v0.73.1/f455d493 revs, fail to find them, and conclude the
patch is obsolete.

Keep the patch. Desktop is excluded from the root workspace, so the two locks
resolve independently; the convergence is a coincidence of this one bump, not
upstream adopting the fix. The next bump that moves one lock and not the other
re-breaks the root-lock version, and the build step still fetches the desktop
manifest.

Comments and the AGENTS.md patch-table row only — no behaviour change.

Signed-off-by: adrienlacombe <6303520+adrienlacombe@users.noreply.github.com>
@adrienlacombe adrienlacombe added upstream-sync needs-human Sync stopped on a tripwire; a human must review and merge labels Jul 31, 2026
@adrienlacombe

Copy link
Copy Markdown
Owner Author

Not auto-merged — tripwire 3

The daily sync merges itself when every check is green and no tripwire fires.
One fired here:

3. A fork-local patch deleted, or a row changed in the AGENTS.md patch table.

I changed the ci.yml row. No patch was deleted and no code behaviour changed —
the edit is comments plus that one table row (a43a4deb2).

The decision a human is being asked to confirm: upstream's mesh-llm bump made
the root and desktop lockfiles pin the same rev, so ci.yml's fork-local
"read the desktop lock, not the root one" patch is currently a no-op. I judged
that it should be kept, not deleted — the two locks resolve independently
(desktop is outside the root workspace), so the convergence is a coincidence of
this one bump and the next asymmetric bump re-breaks the root-lock version.

The alternative reading is that a no-op patch is dead weight and should go. I do
not think so — deleting it trades a zero-cost dormant patch for a CI failure on a
fresh fork the next time the locks diverge — but that is a judgment about the
fork's shape, which is exactly what this tripwire reserves for a human.

Steps 6 and 8 of the runbook are consistent here rather than in tension: step 6
says to correct the table when a patch's rationale shifts, step 8 says a table
change gets human eyes before it lands.

Everything else about this sync is routine: clean merge, both parents intact, no
migrations, no kind renumber, no wire-format change, all local gates green.

Merge with a merge commit, not squash.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Daily upstream sync merging block/buzz range 39ce3dfc3..db7e84d4f into the fork, bringing in upstream desktop Pocket voice import support, updated desktop release plumbing (squash-merge based candidate verification + ruleset-bypass authorization), and new formal docs for NIP-RS unread modeling.

Changes:

  • Added Pocket voice import/delete support end-to-end (Rust Tauri commands + desktop UI + E2E mocks/tests) and introduced buzz-voice imported-voice library support.
  • Updated desktop release verification and contract tests to support squash-merge release candidates and authorized ruleset bypasses.
  • Added a formal Python model + mutation harness for NIP-RS manual-unread semantics, and minor repo hygiene/docs updates.

Reviewed changes

Copilot reviewed 32 out of 35 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
scripts/verify-desktop-release-merge.sh Updates desktop release merge verification for squash merges; adds ruleset-bypass authorization path.
scripts/test-release-ref-contract.sh Extends release contract assertions and adds bypass-filter fixture + mutation tests.
scripts/prepare-desktop-release.sh Updates release PR instructions to require squash merge and candidate check.
scripts/fixtures/desktop-release-rule-suite-bypass.json Adds fixture for GitHub ruleset bypass suite validation.
scripts/desktop-release-bypass-authorized.jq Adds jq predicate to validate a bypass came from the Default ruleset pull_request rule.
RELEASING.md Updates desktop release process documentation to match new candidate + squash-merge flow.
README.md Adds link to releasing documentation.
docs/formal/nip-rs-unread/mutation.py Adds mutation harness for candidate B unread model.
docs/formal/nip-rs-unread/model.py Adds bounded exhaustive model for two candidate unread encodings (A and B).
desktop/tests/helpers/bridge.ts Extends mock bridge options for Pocket voice import test outcomes.
desktop/tests/e2e/voice-settings.spec.ts Adds E2E coverage for importing/selecting/deleting Pocket voices and error/cancel handling.
desktop/src/testing/e2eBridge.ts Implements E2E mock handling for listing/importing/deleting Pocket voices.
desktop/src/features/settings/ui/VoiceSettingsCard.tsx Adds “Add voice” and delete flow UI for Pocket imported voices (with confirmation dialog).
desktop/src-tauri/src/lib.rs Registers new Tauri commands for importing and deleting Pocket voices.
desktop/src-tauri/src/huddle/tts_voice_transition.rs Updates voice-path resolution to support imported voices via absolute paths.
desktop/src-tauri/src/huddle/tts_voice_import.rs Adds native file picker adapter and library wiring for imported Pocket voices.
desktop/src-tauri/src/huddle/tts_settings.rs Extends voice registry to include imported voices; resolves imported voice file paths for runtime.
desktop/src-tauri/src/huddle/pipeline.rs Avoids repeated imported-voice resolution during hot-start polling; uses new voice reference resolver.
desktop/src-tauri/src/huddle/mod.rs Registers the new tts_voice_import module.
desktop/src-tauri/Cargo.lock Updates desktop lockfile for new buzz-voice dependencies (symphonia, sha2, etc.).
crates/buzz-voice/tests/pocket_import_audio.rs Adds ignored-on-by-default integration test covering import/synthesis/delete + fallback behavior.
crates/buzz-voice/src/lib.rs Exposes new imported module.
crates/buzz-voice/src/imported.rs Implements imported voice validation/canonicalization/storage (hashing, decoding, registry).
crates/buzz-voice/Cargo.toml Adds dependencies needed for import pipeline (atomic write, symphonia codecs, hashing, tempfile).
Cargo.lock Updates workspace lockfile for new buzz-voice deps.
AGENTS.md Updates fork-local patch note for ci.yml mesh-llm pin explanation (comment-only).
.gitignore Ignores Python cache files introduced by new formal model docs.
.github/workflows/release.yml Adjusts rolling latest.json upload gating logic.
.github/workflows/prepare-desktop-release.yml Removes deprecated workflow in favor of new desktop release candidate validation.
.github/workflows/desktop-release-candidate.yml Adds PR check that validates desktop release candidates on version-bump/* branches.
.github/workflows/ci.yml Updates fork-local comment explaining the mesh-llm rev resolution behavior.
.github/workflows/auto-tag-on-release-pr-merge.yml Updates tagging workflow inputs to use merge commit SHA and pass merger identity.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +258 to +269
def mutant_m5():
"""M5: values outside uint32 range must fail legacy sanitization."""
blob = {"v": 1, "client_id": "x", "contexts": {
"ov_s:c0": 4294967296,
"ov_c:c0": 0,
"ov_b:c0": 10,
}}
sanitized = legacy_sanitize_blob(blob)
if "ov_s:c0" in sanitized["contexts"]:
return []
return [("overflow-rejected", blob["contexts"]["ov_s:c0"],
sanitized["contexts"])]
Comment on lines +284 to +298
fn atomic_write_restricted(path: &Path, payload: &[u8]) -> Result<(), String> {
let resolved = fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf());
let mut file = AtomicWriteFile::open(&resolved)
.map_err(|error| format!("open {} for atomic write: {error}", resolved.display()))?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
file.set_permissions(fs::Permissions::from_mode(0o600))
.map_err(|error| format!("set {} permissions: {error}", resolved.display()))?;
}
file.write_all(payload)
.map_err(|error| format!("write {}: {error}", resolved.display()))?;
file.commit()
.map_err(|error| format!("commit {}: {error}", resolved.display()))
}
@github-actions
github-actions Bot requested a review from Copilot July 31, 2026 19:02
@github-actions

Copy link
Copy Markdown

CI is green — all 35 checks passed. Safe to review and merge.

Merge with a merge commit, not squash — the sync relies on upstream's individual commits staying in history so the next run can tell what is already merged (see .github/workflows/upstream-sync.md).

@adrienlacombe

Copy link
Copy Markdown
Owner Author

CI green — ready for a human to merge

32 pass, 3 skipping, 0 fail. Longest legs: Windows Rust 27m18s, Mobile 25m45s, Desktop E2E Relay 22m39s.

Skipped are the expected ones: Web (no web paths touched), and the two docker.yml publish jobs that only run on main.

Notably Desktop Release Candidate — upstream's new always-on PR check from block#3943 — passed in 15s, confirming it no-ops on a non-version-bump/ head ref as expected.

This PR is blocked only on the tripwire above, not on anything red. When you merge:

  • Use a merge commit (gh pr merge 10 --repo adrienlacombe/buzz --merge). Not --squash — it drops the second parent and strands the merge base.
  • Merging fires deploy-aws.yml via workflow_run off Docker image, which applies migrations and rolls the relay. This sync adds no migrations, so that step is a no-op here.
  • Relay was healthy (HTTP 200) before this PR; worth re-checking https://relay.bitcoinmarkets.app/health after the deploy settles.

@adrienlacombe
adrienlacombe merged commit 1c33fd3 into main Jul 31, 2026
38 checks passed

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 32 out of 35 changed files in this pull request and generated no new comments.

Suppressed comments (2)

docs/formal/nip-rs-unread/mutation.py:269

  • mutant_m5 is intended to fail only when legacy sanitization incorrectly accepts a uint32 overflow. The current condition is inverted and reports a violation when the overflow is correctly rejected.
def mutant_m5():
    """M5: values outside uint32 range must fail legacy sanitization."""
    blob = {"v": 1, "client_id": "x", "contexts": {
        "ov_s:c0": 4294967296,
        "ov_c:c0": 0,
        "ov_b:c0": 10,
    }}
    sanitized = legacy_sanitize_blob(blob)
    if "ov_s:c0" in sanitized["contexts"]:
        return []
    return [("overflow-rejected", blob["contexts"]["ov_s:c0"],
             sanitized["contexts"])]

docs/formal/nip-rs-unread/mutation.py:182

  • mutant_m3 returns a violation when the model correctly distinguishes SET vs CLEAR ties (S==C). The check is inverted: violations should be returned only when the two tie policies produce the same verdict.
def mutant_m3():
    """M3: tie policy is load-bearing — S==C must produce different verdicts.
    Not a DeviceB mutation; tests the model function directly."""
    reg = RegB(s=1, c=1, b=10)
    frontier = 10
    v_clear = override_set_b(reg, frontier, CLEAR)
    v_set = override_set_b(reg, frontier, SET)
    if v_clear == v_set:
        return []
    return [("tie-distinguishable", v_clear, v_set, reg, frontier)]

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs-human Sync stopped on a tripwire; a human must review and merge upstream-sync

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants