feat(desktop): boot-time reconcile of managed agents to relay events#1601
Conversation
run_event_sync reconciled personas (30175) and teams (30176) into the retention store at boot, but managed agents (30177) were only enqueued on the interactive save path — a record hand-edited on disk between launches, or a save whose publish was missed, silently diverged from the relay. Add the missing leg, mirroring migrate_personas_in_dir: per-coordinate content diff of the opt-IN projection, monotonic created_at bump, retain with pending_sync for the existing flush loop. Best-effort file-authoring contract: no watcher (boot-time pickup only), no deletion reconcile (a record absent from managed-agents.json never tombstones its relay row), and a malformed store fails loudly — preserved as managed-agents.json.invalid and the parse error propagates instead of being silently clobbered by the next save. Co-authored-by: Pinky <44b8e82baa6e0e254e0208d68f335c283c94e7b78dd1fa10d5a49d3f13dd0435@sprout-oss.stage.blox.sqprod.co> Signed-off-by: Wes <wesbillman@users.noreply.github.com>
wpfleger96
left a comment
There was a problem hiding this comment.
Two dedicated reviews were run on this PR (boot-time reconcile) and #1602 (restart badge) in parallel; findings were then re-verified at source before posting. The reconcile logic is well-scoped and faithfully mirrors the persona/team sibling path — deletion-reconcile is genuinely absent (no tombstone path), keyless-skip is by-construction, the .invalid backup survives later saves, the monotonic bump handles future-dated heads, and the no-secrets projection is structurally enforced (no keyring hydration in the reconcile path). All 9 tests pin the right invariants.
Requesting changes on one mechanical CI blocker (file-size gate); the rest is optional. Inline comments below.
| pub fn run_event_sync(app: &tauri::AppHandle, owner_keys: &nostr::Keys) { | ||
| migrate_personas_to_events(app, owner_keys); | ||
| migrate_teams_to_events(app, owner_keys); | ||
| crate::managed_agents::reconcile::reconcile_agents_to_events(app, owner_keys); |
There was a problem hiding this comment.
CI blocker (file-size gate). With this added call plus the doc-block update above, migration.rs is now 1576 lines (wc -l), which check-file-sizes.mjs counts as 1577 via split(/\r?\n/).length (trailing newline → +1). That trips the 1575 cap at desktop/scripts/check-file-sizes.mjs:160 and fails desktop-check.
Fix (author's choice): bump the exception to ["src-tauri/src/migration.rs", 1577], or trim 2 lines here — e.g. drop the new third bullet in the run_event_sync doc block, since migrate_teams_to_events already documents the ordering guarantee. The gate bump is the lighter touch.
There was a problem hiding this comment.
Fixed in ce4e453 by splitting the boot-time event sync out of migration.rs into a new event_sync.rs (plus event_sync_tests.rs / event_sync_team_events_tests.rs) rather than bumping the gate — migration.rs is now 1310 lines and the size limits were ratcheted DOWN (migration.rs 1575→1310, migration_tests.rs 1410→1110). Test parity verified by name: 1025 = 1025, identical test names before/after.
| // The published content is the opt-IN projection JSON, independent of | ||
| // signing and created_at — compare it before building an event so an | ||
| // unchanged agent is a true no-op. | ||
| let content = serde_json::to_string(&agent_event_content(record)) |
There was a problem hiding this comment.
Latent divergence risk (non-blocking). This serializes agent_event_content(record) independently, and build_agent_event (agent_events.rs:92) does the same serde_json::to_string a few lines down when it builds the event. They're byte-identical today, so the retained content and the raw_event's embedded content match. But they're two independent serializations: if build_agent_event ever changes how it serializes (field reordering, a wrapper, different serde options), the retained content compared here would silently diverge from what's actually inside raw_event — every agent would re-publish every boot on a false content mismatch. Consider building the event first and extracting event.content for both the comparison and the retention row (the interactive retain_managed_agent_pending path already does this), so there's a single source of truth for the serialized content.
There was a problem hiding this comment.
Fixed in 381140f: reconcile now builds the event first and compares event.content, so the comparison and the retained row share one serialization (mirrors migrate_personas_in_dir). The independent agent_event_content serialization + import are gone. Content is timestamp-independent, so an unchanged agent is still a true no-op despite the monotonic bump.
| /// Reconcile `managed-agents.json` into kind:30177 events in the retention | ||
| /// store. Boot-time entry point, called from `migration::run_event_sync` | ||
| /// after the persona and team legs. | ||
| pub fn reconcile_agents_to_events(app: &tauri::AppHandle, keys: &nostr::Keys) { |
There was a problem hiding this comment.
Nit: reconcile_agents_to_events is pub, but its only caller is run_event_sync in migration.rs within the same crate (the sibling reconcile_agents_in_dir below is already pub(crate)). Could tighten to pub(crate) to match. Not harmful.
There was a problem hiding this comment.
Done in 381140f — reconcile_agents_to_events is now pub(crate).
migration.rs crossed the file-size guard (1577 > 1575) when the managed-agent reconcile call site landed. Rather than bump the limit, extract the post-identity event-sync leg (run_event_sync + migrate_personas_to_events/_in_dir + migrate_teams_to_events/_in_dir) into a new event_sync.rs with its tests, and ratchet the guard down: migration.rs 1575 -> 1310, migration_tests.rs 1410 -> 1110. Pure code move — no behavior change; the full test set is unchanged (verified by name against the pre-split list). Co-authored-by: Pinky <44b8e82baa6e0e254e0208d68f335c283c94e7b78dd1fa10d5a49d3f13dd0435@sprout-oss.stage.blox.sqprod.co> Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Review feedback on #1601: reconcile_agents_in_dir serialized the projection independently of build_agent_event to compare against the retained row — a silent divergence risk if build_agent_event ever changed its serialization (every agent would republish every boot). Build the event first and compare its content, mirroring migrate_personas_in_dir. Content is timestamp- independent, so an unchanged agent remains a true no-op. Also narrows reconcile_agents_to_events to pub(crate). Co-authored-by: Pinky <44b8e82baa6e0e254e0208d68f335c283c94e7b78dd1fa10d5a49d3f13dd0435@sprout-oss.stage.blox.sqprod.co> Signed-off-by: Wes <wesbillman@users.noreply.github.com>
…ivity * origin/main: feat(desktop): restart-required badge from spawn-time config hash (#1602) feat(desktop): boot-time reconcile of managed agents to relay events (#1601) feat(desktop): canonical <PubKey> component — hover to view/copy full keys, owner "you" labels (#1589) Co-authored-by: Taylor Ho <taylorkmho@gmail.com> Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
* origin/main: docs(readme): add Getting started section routing install paths (#1606) fix(desktop): restrict shared-agent sync to dev data dirs (#1597) feat(desktop): restart-required badge from spawn-time config hash (#1602) feat(desktop): boot-time reconcile of managed agents to relay events (#1601) feat(desktop): canonical <PubKey> component — hover to view/copy full keys, owner "you" labels (#1589) fix(desktop): hydrate reactions for Inbox context messages (#1596) fix: cleanup old screenshots that my agents committed (#1598) chore(release): release Buzz Desktop version 0.3.46 (#1585) fix(desktop): preserve agent model/provider when persona snapshot fields are blank (#1583) feat(acp,desktop): identify and reap stale agent harness processes (#1582) feat(desktop): active-draft badge, send-from-drafts confirm dialog, thread-deleted state (#1581) fix(desktop): treat baked build env vars as satisfying required agent config (#1580) feat(desktop): add "Copy image" to image right-click context menu (#1579) fix(nest): use buzz-dev symlink name for dev builds (#1587) fix(composer): address image-editor follow-up nits on #1491 (#1565) fix(desktop): render black static boot screen (#1570) feat(agents): group activity tool bursts (#1571) feat(desktop): aggregated overview rail, commit detail page, and full breadcrumbs (#1573) fix(desktop): fetch profiles for reaction actors and thread-reply authors (#1550)
Summary
Managed agents (kind 30177) were the one record type without boot-time disk↔relay reconcile — personas (30175) and teams (30176) already republish hand-edited disk state at startup via
run_event_sync, but managed-agent publishes only happened on the in-app save path. This adds the missing leg, plus fail-loudly handling for malformed stores.managed_agents/reconcile.rs:reconcile_agents_to_events/reconcile_agents_in_dir, mirroringmigrate_personas_in_dir— per-coordinate content diff of the no-secretsagent_event_contentprojection vs the retained row at(30177, owner, agent_pubkey), monotoniccreated_atbump, retain withpending_sync=1. Keyless (empty-pubkey) records are skipped by construction.storage.rs::load_managed_agents: on parse error, the malformed file is copied (not renamed) tomanaged-agents.json.invalidbefore returningErr— a hand-edit is never silently lost, even if a later save clobbers the store.run_event_sync(migration.rs), after the persona/team sync.Deliberate non-goals: no file watcher; no deletion reconcile (a truncated/partial JSON must never tombstone relay records — pinned by test); no schema changes.
Testing
env_vars) no-op, missing-record-never-tombstoned, keyless-skip, malformed →.invalid+ error, missing-store no-op, monotonic bump past a future-dated head.just desktop-tauri-test1011 passed / 0 failed;just fmt-check,just desktop-tauri-fmt-check, workspacejust clippyall green.