Skip to content

Relay-primary private managed-agent config - #4999

Open
wesbillman wants to merge 6 commits into
mainfrom
carl/relay-primary-agent-config
Open

Relay-primary private managed-agent config#4999
wesbillman wants to merge 6 commits into
mainfrom
carl/relay-primary-agent-config

Conversation

@wesbillman

@wesbillman wesbillman commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • replace the aggregate private-agent payload with a strict owner-authored encrypted kind:30179 codec
  • retain public/private managed-agent heads and deletion tombstones atomically
  • hydrate validated relay config into a workspace-scoped in-memory overlay, rehydrated from retained events at every boot so relay-primary config survives app restarts
  • support fresh-device recovery: relay-only agents appear in the list with no local record; the first START intentionally materializes a local record (including the key material needed to run the agent) via materialize_relay_only_agent — this is the one deliberate disk write derived from relay state
  • resolve the overlay into the local record before every stale-disk republish site (edit, persona rename, pair-start snapshot re-apply), so a device following a newer relay head cannot clobber it with an audit-clean successor event carrying stale fields
  • hydrate keyring-stored agent keys in the boot reconcile path, so first boot on default (system-keyring) builds publishes each agent's 30179 instead of silently skipping every untouched agent
  • preserve unknown forward-compatible fields and reject malformed/stale inbound events before changing the live overlay
  • classify kind:30179 as owner-scoped global user data in the relay and subscribe from Desktop

This is the minimal replacement for the closed #4940 implementation.

Review fixes (c80c4c17, aa39d72a, 6f486e88)

Adversarial review (Eva/Sami) found and fixed five defects in the original head a9b648b4, each pinned by regression tests:

  1. Overlay never rehydrated after restart — relay-primary config worked for exactly one launch, then every site silently read stale disk. Fixed: boot rehydration from the retention DB (owner-scoped; negative control asserts another owner's keys hydrate nothing).

  2. Boot reconcile published zero 30179s on default keyring builds — records were read without keyring hydration, so the empty-nsec skip fired for every untouched agent. Fixed: hydrate keys in the reconcile path.

  3. Stale-disk republish clobbered a newer relay head at three write sites (agent_models.rs edit, personas/update.rs rename, agents.rs pair-start snapshot re-apply). The corruption was a validly-chained gen+1 successor — indistinguishable from a legitimate edit. Fixed: resolve-before-mutate at each site. The resolve is deliberately per-site, not centralized in the retain helper — centralizing it silently discards user edits, and at the rename site a naive resolve-first loses the rename (the gate keys on disk name). Both wrong fixes are pinned by permanent probe tests, and a source-level guard asserts the exact resolve-call count per file (the write sites are #[tauri::command]s, so unit tests cannot observe the production wiring directly).

  4. Boot reconcile republished stale disk over a newer relay head — a fourth site in the same class as (3), but firing at launch, unprompted, for every agent on a device that follows another device's config. reconcile_agents_in_dir_at reads managed-agents.json raw and cannot resolve the overlay: hydrate_private_config_overlay runs after this leg (event_sync.rs:19-20) and reads the rows it writes. Inbound 30179 updates the overlay and retention but never the JSON, so on a follower disk is stale by construction. Measured: gen 5 → 6, prev = the clobbered head, created_at floored at head+1 by monotonic_created_at so it wins LWW against a head 10,000s in the future, pending_sync set, every field taken from stale disk. It does not self-heal — a second boot is a clean no-op, but each new head the follower receives re-arms it (measured 16 → 1, no-op, 24 → 1), so the follower's disk wins every round and the user's edit silently reverts. Fixed in aa39d72a: retain_agent_record_at_boot publishes the 30179 only when no retained head exists, and is used by boot reconcile alone; the interactive edit paths keep republishing over heads (they resolve the overlay first), and the kind:30177 identity leg is untouched so the upgrade republish waves keep working. Three mutants — gate deleted, gate inverted, gate applied to the whole record instead of the private leg — each killed by a different arm.

  5. Self-authored config never reached the in-memory overlay — the write-side twin of (3). PrivateConfigOverlay has exactly two fill paths and both are inbound-only: insert_patch on an Applied inbound event (commands/personas/inbound.rs) and boot hydration (event_sync.rs::hydrate_private_config_overlay). Neither fires for an event this device authored, because the relay's echo of our own event dedupes to Skipped in retain_inbound_event (strictly-newer guard). So after any local edit the overlay stays pinned at the last received generation, and the next same-session edit resolves that stale patch on top of a fresher disk record and publishes a silent revert — again as a validly-chained successor, indistinguishable from a real edit. Live symptom: an edit sets parallelism 17 → 19, the immediately following rename republishes 17. Note that the resolve-before-mutate fix from (3) is what makes this reachable in the reverting direction: the sites now trust an overlay that no longer tracks this device's own writes. Fixed in 6f486e88: one write-through at one seam — retain_managed_agent_pending (commands/agents.rs), after retain_agent_record commits, reads the just-retained 30179 head back out of the same open conn and inserts it into the overlay (PrivateConfigOverlay::absorb_retained_head). A shared patch_from_retained_row() decode helper is refactored out of hydrate_from_retention, so hydration and write-through share exactly one decode path. The single seam covers all five interactive writers (edit/rename, create, settings, start, rename-rollback) with no per-caller copies; reconcile.rs is deliberately untouched (retain_agent_record takes conn+keys, no AppState), and personas/snapshot/import.rs's inline retain writes only kind:30177, so it has no overlay concern. Two deliberate design calls, both the conservative side of their trade: absorb unconditionally rather than only when the retain reported a change (the overlay can never outrun retention), and never clear on a missing or undecodable head — leave the existing entry alone (pinned with a positive control). The row is read back from retention rather than inserting the in-memory record; Eva independently derived the same design before seeing the diff, so that choice is load-bearing on two derivations. Four mutants, each killed: absorb made a no-op (reproduces the live symptom exactly — left: Some(17), right: Some(19)), the production absorb call deleted, absorb ordered before the retain instead of after, and a fourth ordering variant.

Contract of record for kind:30179 writes — two clauses, not one. Review corrected an earlier draft of (5) that implied every 30179 write funnels through the seam; that is false at a named line. retain_private_agent_record has two non-test callers: retain_agent_record (reconcile.rs:190, the seam's path) and the sibling retain_agent_record_at_boot (reconcile.rs:163), which never passes through retain_agent_record and so never reaches the write-through. Defect (4)'s own fix is what made that sibling a 30179 writer, which is why a caller-count on the funnel symbol could not see it. The class is closed by two independent mechanisms: (a) interactive writers are funnelled through the seam, guarded by the exact-count source guard; (b) the boot writer is gated head-absent (reconcile.rs:157-163), guarded by defect (4)'s three mutants — head-absent means there is no relay state to be stale against, so that write cannot be a revert and an absent overlay entry for it is correct rather than stale. Backstop: run_event_sync runs reconcile then hydrate_private_config_overlay (event_sync.rs:19-20), and hydration is a wholesale *overlay = hydrated replacement, so boot's own writes reach the overlay one line later regardless. Residual, accepted pre-merge: a future writer calling retain_private_agent_record directly, or a third boot-ish sibling, lands outside both mechanisms and outside both guards (the exact-count guard only reads commands/agents.rs) — the same residual shape write_site_resolve_guard already accepts and documents.

Causal note, on the record: defect (4) was made reachable by the fix for (2). Before keyring hydration, retain_private_agent_record's empty-nsec skip returned early for every keyring-resident record, so boot never built a 30179 at all — the skip was incidentally protecting this path. (2) is still correct (an untouched agent must publish its first 30179 on a default build), but it removed a guard that was load-bearing for something else. A control arm with an absent nsec confirms the head survives, pinning the line.

Coverage honesty: (2)'s original regression test asserted the harmful act — it pinned that boot publishes a 30179, which at the time meant publishing stale disk over a head. A green suite ratified the clobber. Chosen fix keeps that test meaningful by scoping it to the head-absent case.

Known follow-ups, out of scope here:

  • Auto-start on launch spawns from raw disk. Every interactive path resolves the overlay first (resolved_local_record / resolved_records), but restore.rs contains none of resolved_local_record, resolved_records, resolve_local_record, or private_managed_agent_overlay — it loads raw load_managed_agents (restore.rs:49) and Phase B spawns those records. On a follower, start_on_app_launch agents therefore run on stale private config. Independent of the (4) fix, which stops the republish, not the spawn. (Its two resolve_effective_* references at restore.rs:41/245 are the persona→global fallback resolver, a different mechanism.) Related, unmeasured: spawn_event_sync (workspace.rs:240) and restore_managed_agents_on_launch (workspace.rs:277/291) are spawned without either awaiting the other, so even an overlay-aware restore could read the overlay before hydration populates it.
  • resolve_effective_config callers (runtime.rs:200/437, agents_deploy.rs:141, spawn_snapshot.rs:248) each take a record the caller may or may not have overlaid — needs a per-caller ruling.
  • the PrivateConfigPatch collapse (−169 LOC) lands as a separate commit after live E2E.

Validation

  • full desktop lib suite at 6f486e88: 2365 passed / 0 failed / 15 ignored (--all-features) — the 2360 at aa39d72a plus the 5 new tests for defect (5); 2360 was in turn the 2356 baseline at c80c4c17 (reproduced independently by two reviewers in separate worktrees) plus the 4 tests for defect (4)
  • cargo fmt --check clean; cargo clippy --workspace --all-targets -D warnings clean
  • all pre-push hooks passed at c80c4c17, aa39d72a, and 6f486e88 (six at 6f486e88): branch skew, desktop-check (file-size ratchet), rust-tests, mobile-test, desktop-test (4387 frontend tests / 0 failed), desktop-tauri-checks. Nothing pushed with --no-verify.
  • the desktop file-size ratchet was verified live rather than assumed: padding the new test file past the limit makes it fail ("allowed 1000"), so its pass at aa39d72a is not vacuous
  • regression tests include defect probes (assertions inverted post-fix), fix verifications with negative controls, wrong-fix probes, and mutation checks (deletion/ordering/re-copy mutants all fail)
  • independent adversarial review by Mongo on the original head: clear, no blockers; Eva/Sami review findings above addressed at c80c4c17
  • live-local two-device E2E (restart persistence, first-boot keyring recovery, stale-republish, fresh-device materialization) per TESTING.md — Max, on a device-addressed v2 rig at exact SHA 6f486e88; arm attribution at c80c4c17 was calibration only, since defect (4) made arms 1 and 3 structurally unattributable. This gate is still open and is the remaining merge blocker: the defining 19 → rename discriminator for (5) passes live on the real Tauri backend, but the preserved test agent's scoped keyring secret is unavailable to the fresh process, so the rename profile-sync and pair-start arms refuse at their key boundary and the fresh lane has not yet produced new relay 30179 receipts.
  • independent gate on defect (5) at 6f486e88 — Eva, in her own worktree: full diff read, suite + fmt + clippy, and a personal re-run of the absorb-no-op and absorb-before-retain mutants rather than taking the author's kill list on faith

Notes

Temporary Tauri sidecar placeholders used for local builds are ignored and not committed.

Keep encrypted runnable configuration in an owner-scoped relay event while
preserving local records as migration state. Validate inbound payloads before
retention, expose fresh-device records through an ephemeral scoped overlay,
and retain public/private heads and tombstones atomically.

Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
Co-authored-by: Mongo <5c25403eab7271f9f94ddd4f2b270e8cac2c92e2c830c51877cca6ec974ffb3f@buzz.block.builderlab.xyz>
Co-authored-by: Princess Donut <68157ebd23b3897c1991015c3038658ea916200c67d3a54620b0754d1b92f6e0@buzz.block.builderlab.xyz>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
@wesbillman
wesbillman requested a review from a team as a code owner August 6, 2026 02:59

@wesbillman wesbillman left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Reviewing and commenting on Wes's behalf. Blocking verdict: changes requested. The codec, owner-only relay classification, inbound validation-before-retention, scope-clear serialization, and atomic public/private retention all look sound. However, the new overlay start branch creates lifecycle holes that make relay-restored agents unsafe or impossible to manage. Please route resolved overlay records through a lifecycle that preserves the existing transition/preflight/provider/persistence invariants and supports stop/delete for truly relay-only records.

This PR changes behavioral tests and adds codec, inbound, overlay, and reconciliation coverage; none of the added overlay tests exercises a real start → stop/delete lifecycle or concurrent shutdown. Add regressions for a disk-backed overlaid agent and a fresh-device relay-only agent.

Focused checks at this exact clean head: cargo test -p buzz-core private_managed_agent --lib (13 passed), cargo test -p buzz-relay ingest --lib (161 passed; filter selected the ingest module plus one dependent test), and git diff --check passed. Desktop Core and Desktop E2E checks were still running when reviewed; existing CI covers the broad suites.

.map_err(|error| error.to_string())?
.contains(&pubkey)
{
return crate::managed_agents::private_config_overlay::start_relay_only_agent(

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

P1 — Do not divert every overlaid disk record around its existing start lifecycle. Presence in this in-memory overlay is not equivalent to being relay-only: resolved_record can start from an existing disk-backed record and then apply the patch. This branch therefore bypasses start_local_agent_with_preflight (including effective-config/relay-mesh validation, persona handling, saving start metadata, retained publication, and profile reconciliation), and it bypasses provider deployment entirely because the helper rejects any resolved provider backend. Merely receiving a valid private event can thus make an ordinary local/provider agent follow a materially weaker or unusable path. Distinguish true relay-only records from disk-backed records and route the latter through the normal resolved local/provider pipeline; add a regression proving an overlaid disk record retains those lifecycle guarantees.

.managed_agent_processes
.lock()
.map_err(|e| e.to_string())?;
start_managed_agent_process(app, &mut record, &mut runtimes, Some(owner_hex))?;

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

P1 — A fresh-device relay-only runtime started here cannot be stopped or deleted through the normal agent API. This spawns from a temporary record and stores only the runtime pair. stop_managed_agent reloads disk records and fails at find_managed_agent_mut; delete_managed_agent likewise requires a disk record and returns agent … not found. The frontend's normal local actions call those commands, so a relay-only agent can start successfully and then become uncontrollable until broader shutdown/process exit. This direct spawn also does not hold managed_agents_store_lock or the documented runtime-transition boundary, so it can run after a workspace/identity overlay clear or race shutdown and register a new child after shutdown's protected snapshot. Implement stop/delete and scope-transition behavior for relay-only records (or materialize an appropriate lifecycle record) and serialize spawn consistently; cover start → stop and start → delete on a device with no local record.

Route disk-backed overlay agents through the established preflight, provider,
profile, persistence, and runtime transition paths. Materialize fresh-device
local records before start so stop, delete, and shutdown can manage them.

Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
@wesbillman
wesbillman force-pushed the carl/relay-primary-agent-config branch from c38fc0f to a9b648b Compare August 6, 2026 03:45
…blish

Relay-primary managed-agent config (kind:30179) delivered config for exactly
one launch and could overwrite a newer device's config with an audit-clean
event. Three defects, each measured with a probe before being fixed.

Overlay boot rehydration. `PrivateConfigOverlay` was in-memory only and its
single writer fired only when an inbound 30179 was strictly newer than the
retained row. On every second-and-later launch the backfill re-delivered the
same event, retention deduped it to Skipped, and the overlay stayed empty for
the whole session, so every read silently fell back to stale disk. Rebuild the
overlay from the retained rows on the `run_event_sync` boot seam, which already
runs post-identity-resolution with the resolved owner keys and scoped db path.
Adds `get_retained_events_of_kind` (nothing read rows back by kind before).

Boot publication on default keyring builds. `reconcile_agents_in_dir_at` read
`managed-agents.json` raw, so on a default `system-keyring` build the nsec was
keyring-resident and `retain_private_agent_record`'s empty-nsec skip fired for
every untouched agent: zero 30179s published on first boot. Hydrate keys in the
reconcile path. The doc comment asserted the bug ("keys are never needed here")
and is corrected, so the next reader is not re-licensed to reintroduce it.

Stale-disk republish at three write sites. `private_payload_from_record`
serializes every config field, so retaining a disk-derived record on a device
following a newer relay head republished the other fields from stale disk;
`monotonic_created_at` then floored the write at head+1, so it won LWW, bumped
the generation, and chained `prev` to the head it destroyed — a validly-chained
successor indistinguishable from a legitimate edit. Resolve the overlay before
the local mutation at `agent_models.rs` (edit), `personas/update.rs` (persona
rename) and `agents.rs` (pair-start snapshot re-apply).

Ordering is the fix at each site, and it differs per site, which is why this is
not centralized in `retain_managed_agent_pending`:
- edit: resolve before the user's patch, or the patch is discarded
- rename: resolve for the payload only; the `name != old_display_name` gate must
  keep reading disk state, and name/display_name are re-applied after
- pair-start: resolve before `apply_persona_snapshot`, so the definition quad
  stays definition-authoritative

Tests. Regression coverage for all three defects plus three probes that pin the
wrong fixes (centralized resolve discards edits; resolve-before-gate skips the
rename; swapped pair-start ordering lets the overlay clobber the persona quad),
each with positive and negative controls.

`write_site_resolve_guard` is a source-level assertion, added because the
behavioural tests cannot see the production wiring: every write site is inside a
`#[tauri::command]` needing a live `AppHandle`, so the tests call
`retain_agent_record` directly and stayed green with the production resolve
deleted (measured: 2261 passed / 0 failed). The guard fails when a site loses
its resolve or a new site is added without one, and carries its own vacuity
control.

File-size ratchet. `agent_models.rs` sat at the 1000-line cap before this
change (1024 lines), so the ratchet allows it zero growth, and
`reconcile/tests.rs` crossed the cap. Two verbatim moves, following the seams
each file already uses: `normalize_agent_models` to
`agent_models_normalize.rs` (`#[path]` submodule, like the databricks/
openrouter/discovery helpers) and the stale-republish test family to
`reconcile/tests/stale_republish_tests.rs` (like
`personas/update/name_propagation_tests.rs`). Both moved blocks are
byte-identical to their pre-move bytes apart from one visibility line
(`pub(super)` -> `pub(crate)`, required by E0364 on the re-export), and the
write-site guard's deletion mutant was re-run after the move: still dead.

Item 3 from the review (collapsing the 27-field `PrivateConfigPatch` mirror,
~169 deletable lines) is deliberately not in this commit; it is an overlay
refactor and lands separately.

Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Tyler Longwell <tlongwell@block.xyz>

@klopez4212 klopez4212 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Reviewing and requesting changes on Kenny Lopez's behalf.

Blocking: boot reconcile destroys the retained relay head before the overlay is hydrated.

run_event_sync calls reconcile_agents_to_events first and only then calls hydrate_private_config_overlay (desktop/src-tauri/src/event_sync.rs:17-20). The reconcile reads raw managed-agents.json records and passes them directly to retain_agent_record (desktop/src-tauri/src/managed_agents/reconcile.rs:91-119); it cannot consult the overlay because the overlay has not been rebuilt yet.

On the exact restart this commit intends to fix, where disk has config A and retention contains a newer relay config B, boot therefore does this:

  1. build a private payload from stale disk A;
  2. compare it with retained B, see a difference, and retain A as generation B+1 / pending_sync = true;
  3. hydrate the overlay from that newly overwritten A row.

The valid relay head is lost locally and the stale successor is queued to overwrite it remotely. In other words, restart persistence still fails, now through the boot writer rather than the interactive writers.

Hydrate/validate the retained overlay before any disk→private reconcile, and make boot reconcile resolve each disk record against that hydrated head before retaining it (while preserving the intended behavior for records with no retained private head). Add an end-to-end unit seam around the real boot ordering: seed stale disk A plus newer retained B, run the boot reconcile/hydration path, and assert retention and the overlay remain B with no stale private republish queued. The current standalone hydration and stale-republish model tests do not exercise this production ordering.

@klopez4212 klopez4212 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Blocking on a boot-order regression in the restart recovery path. The codec, inbound validation, scope clearing, lifecycle materialization, and stale-write-site fixes otherwise look carefully constructed. I reviewed clean head c80c4c17b047d7580d50ee5261897e0fb192bb2d; git diff --check origin/main...HEAD is clean. I did not duplicate the broad suites already reported/covered by CI.

Comment thread desktop/src-tauri/src/event_sync.rs
…er head

Boot reconcile is a fourth stale-disk republish site, in the same class as
the three write sites fixed in the previous commit but worse: it fires at
launch, unprompted, for every agent on a device that follows another
device's config.

`reconcile_agents_in_dir_at` reads `managed-agents.json` raw and cannot
resolve the private-config overlay -- `hydrate_private_config_overlay` runs
after this leg (`event_sync.rs:19-20`) and reads the rows this leg writes.
Inbound kind:30179 updates the overlay and retention but never the JSON, so
on a follower disk is stale by construction. Rebuilding the 30179 projection
from disk then republishes every stale field over device A's newer head as a
validly chained gen+1 successor, and `monotonic_created_at` floors it at
head+1 so it wins LWW. Measured: gen 5 -> 6, `prev` = the clobbered head,
`created_at` = head+1 against a head 10,000s in the future, `pending_sync`
set, and every field (name, system_prompt, parallelism, env_vars) taken from
stale disk.

It also does not self-heal. A second boot is a clean no-op because disk now
matches the head it wrote, but each new head device A publishes re-arms it:
measured 16 -> 1, no-op, then 24 -> 1. The follower's disk wins every round
and the user on A sees their edit silently revert.

The previous commit's keyring hydration is what makes this reachable. Before
it, `retain_private_agent_record`'s empty-nsec skip returned early for every
keyring-resident record, so boot never built a 30179 at all -- the skip was
incidentally protecting this path. Hydrating keys is still correct (an
untouched agent must publish its first 30179 on a default build), but it
exposed everything downstream of the guard it removed. A control arm with an
absent nsec confirms the head survives, pinning the causal line.

Fix: `retain_agent_record_at_boot` publishes the 30179 only when no retained
head exists, and is used by boot reconcile alone. That keeps the requirement
boot exists to serve -- an agent whose nsec lives in the keyring gets its
FIRST private config published -- while leaving an existing head to the
interactive edit paths, which resolve the overlay before retaining and so
author from relay-fresh state. The kind:30177 identity leg is untouched, so
the upgrade republish waves keep working.

Resolving the overlay at boot instead was rejected and is pinned by a
permanent wrong-fix probe: an offline local edit lives on disk and in an
unflushed `pending_sync` 30179, so resolving disk through an overlay
hydrated from the older head would discard it -- the centralized-resolve
failure from the previous commit, with boot's blast radius.

Tests (4): the fix verification asserts the head is byte-identical after
boot and nothing is enqueued; two requirement-preservation arms (first 30179
still published when no head exists; 30177 still republishes when a private
head is present) so the fix cannot be satisfied by never publishing at boot
or by gating at the wrong level; and the wrong-fix probe. Three mutants,
each killed by a different arm: gate deleted, gate inverted, gate applied to
the whole record instead of the private leg. Mutants re-run after cargo fmt.

Desktop lib suite 2360 passed / 0 failed / 15 ignored (--all-features);
cargo fmt --check, cargo clippy --workspace --all-targets --all-features
-D warnings, and the desktop file-size ratchet (against the CI base) all
clean -- the ratchet verified live with a padding control that fails it.

Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
@klopez4212
klopez4212 dismissed their stale review August 6, 2026 17:37

Dismissed at Kenny Lopez’s request after reviewing aa39d72. The new boot-only head-presence gate prevents stale disk from replacing or queuing over an existing retained 30179 while preserving first private publication and public 30177 reconciliation.

klopez4212
klopez4212 previously approved these changes Aug 6, 2026

@klopez4212 klopez4212 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Re-reviewing and approving on Kenny Lopez's behalf at aa39d72aac57aeda49cb3c38db7c9c8ed4af24f1.

My boot-order blocker is resolved. The boot-only retention path now leaves any existing kind:30179 head byte-for-byte intact and only creates the private event when no retained head exists. This prevents stale disk state from becoming a newer successor, while preserving first private publication and the independent kind:30177 public reconciliation path. The added regressions cover all three requirements and pin why overlay resolution at boot would discard an unflushed offline edit.

Focused review: the four added boot tests directly exercise the gate and its preservation cases; git diff --check origin/main...HEAD passed at this exact clean head. I attempted the focused Rust tests locally, but this checkout lacks the required generated sidecar desktop/src-tauri/binaries/buzz-acp-aarch64-apple-darwin, so the Tauri build script stopped before compiling the test target. Existing CI is the broad validation authority.

@wesbillman

Copy link
Copy Markdown
Collaborator Author

Reviewing and requesting changes on Wes Billman's behalf at aa39d72aac57aeda49cb3c38db7c9c8ed4af24f1.

Blocking: launch auto-start bypasses the relay-primary overlay and races its hydration.

apply_workspace starts event sync asynchronously (workspace.rs:233-245), then independently spawns restore_managed_agents_on_launch (workspace.rs:285-295) without waiting for event sync. The restore path reads raw managed-agents.json (restore.rs:116), selects start_on_app_launch candidates from those raw records (restore.rs:168-192), re-collects raw records after persona snapshotting (restore.rs:196-222), and passes them directly to spawn_agent_child (restore.rs:291-341). It never resolves private_managed_agent_overlay.

Therefore, on a follower where disk has private config A and retention/relay has newer config B, an auto-start agent can launch with A: stale prompt/model/provider/runtime, env vars, allowlist, relay mesh, auth tag, or agent nsec. The new boot retention gate correctly prevents A from being republished over B, but does not prevent A from executing. Waiting only for overlay hydration would still be insufficient on a fresh device until backfill has delivered B; the startup contract needs an explicit authoritative-state/readiness decision rather than two racing best-effort tasks.

This contradicts the PR's relay-primary behavior at the most consequential read site. Please serialize auto-start behind the authoritative private-config bootstrap and resolve each candidate through the overlay before preflight/spawn, with a safe offline policy. Add a production-seam regression that seeds stale disk A plus retained newer B, runs launch restore, and proves the spawned snapshot uses B; include a delayed-hydration race arm. A test that only exercises resolve_local_record is not enough.

Behavior-changing tests: the PR adds strong codec/inbound/retention and stale-republish regressions, including restart overlay hydration and the new boot-only 30179 head-preservation gate. However, none covers launch auto-start using the relay-resolved record or the ordering between spawn_event_sync and restore. That missing behavioral test corresponds directly to this blocker.

git diff --check 16cc3de6d6bb23ebdc3a928172fb585494079232..aa39d72aac57aeda49cb3c38db7c9c8ed4af24f1 passes. Current broad CI is not validation evidence for this head: the CI path detector and release-candidate jobs were cancelled after ~48 minutes, and Desktop Core/Unit Tests plus most jobs were skipped.

…erlay

The overlay only ever learned config from events this device RECEIVED. Both
fill paths are inbound-only: `insert_patch` on an `Applied` inbound event
(`personas/inbound.rs:228`) and boot hydration (`hydrate_from_retention`).
Neither fires for an event this device authored -- the relay's echo of our
own event dedupes to `Skipped` in `retain_inbound_event`, because the row is
already retained.

So the overlay stays pinned at the last received generation for the whole
session. `update_managed_agent` resolves that patch onto the disk record,
applies the user's edit, saves, and retains -- correct for ONE edit. The
SECOND edit in the same session resolves the same stale patch onto the now
fresher disk record, reverting the first edit, and publishes the reversion as
an audit-clean gen+1 successor that wins LWW. The resolve result is written
back to disk, so the revert is durable, not just in-flight.

That is Max's live gate red on a single backend: gen 3 published parallelism
19, the following rename returned 17 and published 17 as gen 4. It stands
independent of the Device-A/B attribution he retracted.

Fix: after `retain_agent_record` commits, read the just-retained kind:30179
head back out of the same connection and `insert_patch` it. One seam --
`retain_managed_agent_pending` -- covers all five writers (create, settings,
edit/rename, start, rename-rollback) with no per-caller copies and no
new-writer trap. `reconcile.rs` is untouched: `retain_agent_record` takes
`conn`+`keys` and threading `AppState` through it would drag the boot
reconcile into the diff for nothing. No new lock edge: callers already hold
`managed_agents_store_lock` and the overlay lock is taken under it, the same
order `resolved_local_record` uses. Decode is factored into
`patch_from_retained_row`, shared with boot hydration, so both learn config
through exactly one path.

Absorbing unconditionally (not gated on the retain reporting a change) keeps
the overlay from ever running ahead of retention: every insert comes from a
row read back out of the database. A missing or undecodable head leaves the
current entry alone rather than clearing it.

Coverage. `sami_second_edit_in_one_session_preserves_the_first_edit` runs the
two-edit sequence through the real retention engine, seeding gen 2 via
`retain_inbound_event` + `hydrate_from_retention` so the overlay is populated
exactly as boot populates it. Its negative control runs the same sequence
without the write-through and asserts the revert to 17, so the main assertion
cannot pass vacuously. `absorb_retained_head_leaves_the_overlay_alone_when_
there_is_no_head` pins the no-clear contract with a positive control proving
the same call DOES update on a present head.

Both behavioural tests model the helper body -- every caller is inside a
`#[tauri::command]` needing a live `AppHandle`, so deleting the production
call leaves them green (the failure mode already documented on
`write_site_resolve_guard`). Three source guards close that: exact call count
of 1, source order retain-before-absorb, and a negative control proving the
searched literals are load-bearing.

Mutants, 4/4 killed: (1) `absorb_retained_head` body no-oped -> the red test
fails with exactly the live symptom, `left: Some(17) right: Some(19)`;
(2) production call deleted -> both source guards fail; (3) production call
moved before the retain -> the order guard fails; (4) test helper's order
swapped -> the behavioural test fails.

Gate at this tree: desktop lib suite 2272 passed / 0 failed / 14 ignored,
`cargo clippy --workspace --all-targets` clean, `cargo fmt --check` clean,
`check-file-sizes` rc=0 with CHECK_FILE_SIZES_BASE pinned to the merge-base
with origin/main.

Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
tlongwell-block added a commit that referenced this pull request Aug 7, 2026
…#5133)

## What

Relay-only carve-out of the ingest half of #4999: generic EVENT ingest
now accepts kind:30179 (NIP-PMA private managed-agent config). One file,
`crates/buzz-relay/src/handlers/ingest.rs`, 16 insertions / 15
deletions; **two semantic lines**, byte-identical to the ingest hunk of
#4999 at `6f486e88`:

1. `required_scope_for_kind`: 30179 requires `Scope::UsersWrite` — same
arm as its public sibling 30177 and the other owner-authored NIP-AP
kinds.
2. `is_global_only_kind`: 30179 is owner-global, keyed `(pubkey, kind,
d-tag)`; a stray `h` tag must not channel-scope it.

The rest is import reflow plus replacing the guard test with a positive
one (`private_managed_agent_kind_is_owner_scoped_global_user_data`:
asserts UsersWrite scope, global-only, no h-channel scope).

## Why the guard test can be retired

The removed test
(`private_managed_agent_kind_remains_rejected_until_atomic_ingest_exists`)
pinned a stated precondition: *"must not enter generic EVENT ingest
before privacy and aggregate CAS deploy."* Both halves are resolved:

- **Privacy** — the author-only read gates for 30179 shipped to main
with #4593: `AUTHOR_ONLY_KINDS` membership, `req.rs` pre-filter + result
gates, `count.rs`, `event.rs` fanout, and the bridge pre-filter
(`bridge.rs:999-1000` returns `restricted: author-only kinds require
authors=[self]` / 403). Only the author can read the event back.
- **Aggregate CAS** — #4999 settled generation as **advisory**: the `g`
tag is shape-validated, never relay-enforced. Last-write-wins per
coordinate is the contract of record (see the kind:30179 contract blurb
in #4999), so no CAS mechanism is pending on the relay side.

## Why this is inert to existing relays and clients

- No production desktop code on main authors kind:30179 — the codec
(`private_managed_agent.rs`) has zero non-test callers. This PR accepts
a kind nobody can produce yet.
- Content is opaque NIP-44 ciphertext to the relay; the relay never
decrypts it.
- Reads remain author-only via the already-shipped gates above.
- Storage is the standard parameterized-replaceable path already
exercised by kinds 30175–30178. No schema, config, or migration changes.

## Testing

- Full `buzz-relay` package suite at this commit: 859 passed, 1 failed —
`api::mesh_demo::tests::demo_join_forwarded_arm_round_trips_echo` (504
vs 200), which **reproduces identically on clean main `769ac70b`** with
this change stashed; pre-existing/environmental, not introduced here.
- New positive ingest test passes.
- Pre-push hooks green (branch-skew, rust-tests, desktop-tauri-checks).

## Relationship to #4999

#4999 (relay-primary agent config, desktop half) stays DO-NOT-MERGE
pending live relay receipts + real CI; once this lands and deploys, its
live test simplifies to plain `desktop-standalone` against the real
relay, and #4999 rebases to drop its now-duplicate ingest hunk
(identical bytes → trivial rebase).

Originating thread:
buzz://message?channel=06f13ed3-0557-4ac2-922c-1545dd00bf97&id=2a43b3b4933a2ea78b77088619251c061355f9b7b6dc29ea0d702193f2344149


## Brownfield FTS note (review findings, operator-ruled non-blocking for
this PR)

Max and Sami independently identified that the FTS privacy skip-set is
regime-dependent: migration 0008 installs the positive allowlist (`kind
IN (0, 9, 40002, 45001, 45003)`) **only on an empty events table**; an
already-populated database keeps the 0001/0005 negative skip-list
(wrapped by 0014 to add 30350), which omits 30179 — so on such an
installation this PR admits 30179 rows whose NIP-44 ciphertext gets
indexed by `to_tsvector`. Sami measured both regimes against real
Postgres (brownfield: 30179 INDEXED; fresh: NULL) and demonstrated the
existing drift test only exercises the fresh regime.
`schema/schema.sql:222`'s canonical literal is also the negative list
and omits 30179. Migration dates put any relay deployed with data before
0008 landed (2026-07-13) in the brownfield class.

**Scope of exposure (Sami's trace):** not a content leak —
`event_visible_to_reader` / `is_author_only_event` gates hold on both
search surfaces (`req.rs:725`, `bridge.rs:1770`), so foreign readers
receive nothing. Lost is the storage-level NULL-tsv backstop plus FTS
page budget burned on post-filtered hits.

**Operator ruling (Tyler, events `1472e5b6`, `cbd368ed`):** ship this PR
without an exclusion migration. Safety argument that makes this sound
rather than merely accepted: main has **zero non-test 30179 writers**
until #4999's desktop half deploys — no 30179 rows can exist, so nothing
can be indexed in any regime while this PR is the only half live.



**Additional review characterizations (Sami, non-blocking, on the
record):**
- *Behavioral delta enumerated:* routing triple
(`required_scope_for_kind` / `is_global_only_kind` /
`requires_h_channel_scope`) compared for all 65,536 kinds at base
`769ac70b` vs head `77eeba6e` — exactly one row differs (30179). No
other kind or client changes behavior.
- *"SQL visibility before LIMIT" (NIP-PMA step 2):* no
`AUTHOR_ONLY_KINDS` pushdown clause exists in `buzz-db` (only
`SHARED_GATED_KINDS` has one). Author-only kinds are protected by the
pre-filter (`author_only_filters_authorized`) plus post-filter omission;
mixed-kind filters can burn candidate-page budget on discarded rows.
Pre-existing and identical for 30300/30350 — not introduced here; noted
so the NIP's step-2 checkbox is not read as fully ticked.
- *Envelope validation gap:* 30179 is the only parameterized-replaceable
kind at ingest with no per-kind envelope validator (codec grammar checks
run in the desktop writer, not the relay). Generic limits only (256 KiB,
±15 min, pubkey==identity, d-tag bound). Self-inflicted footgun bounded
to the author's own coordinate — candidate companion to the exclusion
migration in the #4999 rebase, deliberately not added here.

**Bound follow-up (required before/with the #4999 desktop half):** a
0014-shape additive migration (`pg_get_expr` capture + `CASE WHEN kind =
30179 THEN NULL ELSE (<existing>) END` wrap), add 30179 to the
`schema/schema.sql:221` literal, and a brownfield-regime variant of the
FTS drift test, per Sami's finding. Deploy-time spot check if ever
wanted: `SELECT pg_get_expr(d.adbin, d.adrelid) FROM pg_attrdef d JOIN
pg_attribute a ON a.attrelid = d.adrelid AND a.attnum = d.adnum WHERE
d.adrelid = 'events'::regclass AND a.attname = 'search_tsv';`

Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
Co-authored-by: Eva <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
Absorbs the relay half that shipped separately in #5133 (squash commit
ad92335): the kind:30179 ingest acceptance hunk in
crates/buzz-relay/src/handlers/ingest.rs was byte-identical on both
sides, so this merge removes all relay-side changes from this PR's
diff. #4999 now carries only the desktop + buzz-core codec half.

No rebase, no force-push — history preserved per operator instruction.

Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Tyler Longwell <tlongwell@block.xyz>

* origin/main:
  fix(bench): mention the orchestrator by pubkey when posting the task (#5136)
  feat(relay): accept kind:30179 private managed-agent events at ingest (#5133)
  fix(media): require authenticated reads (#4610)
  fix(desktop): preserve authoritative agent avatars (#4984)
  fix(desktop): next/back navigation during key creation onboarding (#4978)
  Alert community owners and admins when a new key joins (#4900)
  fix(desktop): prevent sidebar prefs from reverting on stale-localStorage boot (#5086)
  chore(hooks): run desktop typecheck in pre-push (#5110)
  feat(identity): recover desktop identity from a signed-in phone (#4845)
  fix(buzz-agent): classify read timeouts distinctly in LLM error messages (#4959)
  Refine agent runtime controls (#5026)
  test(desktop): await thread scroll anchor (#3174)
  Improve desktop mobile pairing flow (#5024)
  feat(desktop): show selected community in rail (#5000)
  fix(desktop): stop rate-limited reconnect backfill from tearing down the authenticated socket (#4990)
  fix(desktop): skip native notifications outside app bundles (#5004)
  ci: prove the relay-driven mesh lifecycle — discover, join, infer, deny — with real nodes (#3862)
  fix(desktop): virtualize channel member lists (#4991)

Signed-off-by: Eva <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants