Skip to content

feat(desktop): boot-time reconcile of managed agents to relay events#1601

Merged
wesbillman merged 3 commits into
mainfrom
pinky/boot-reconcile
Jul 7, 2026
Merged

feat(desktop): boot-time reconcile of managed agents to relay events#1601
wesbillman merged 3 commits into
mainfrom
pinky/boot-reconcile

Conversation

@wesbillman

Copy link
Copy Markdown
Collaborator

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.

  • New managed_agents/reconcile.rs: reconcile_agents_to_events / reconcile_agents_in_dir, mirroring migrate_personas_in_dir — per-coordinate content diff of the no-secrets agent_event_content projection vs the retained row at (30177, owner, agent_pubkey), monotonic created_at bump, retain with pending_sync=1. Keyless (empty-pubkey) records are skipped by construction.
  • Fail-loudly in storage.rs::load_managed_agents: on parse error, the malformed file is copied (not renamed) to managed-agents.json.invalid before returning Err — a hand-edit is never silently lost, even if a later save clobbers the store.
  • One additive call-site line at the end of 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

  • 9 new unit tests: fresh-retain-pending, no-churn-when-unchanged, edited-republish, excluded-field (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-test 1011 passed / 0 failed; just fmt-check, just desktop-tauri-fmt-check, workspace just clippy all green.

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 wpfleger96 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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.

Comment thread desktop/src-tauri/src/migration.rs Outdated
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);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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.

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.

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))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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.

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.

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) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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.

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.

Done in 381140f — reconcile_agents_to_events is now pub(crate).

wesbillman and others added 2 commits July 7, 2026 13:48
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>
@wesbillman wesbillman merged commit c5a541e into main Jul 7, 2026
25 checks passed
@wesbillman wesbillman deleted the pinky/boot-reconcile branch July 7, 2026 20:53
tellaho added a commit that referenced this pull request Jul 7, 2026
…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>
tlongwell-block pushed a commit that referenced this pull request Jul 7, 2026
* 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)
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