Fix A: add remove_owner_p_kind / remove_save_subscription_kind
Closes the non-atomic toggle-OFF path (Wes note 1). The toggle handlers
in LocalArchiveSettingsCard were doing a TS-side read-modify-overwrite on
the shared owner_p row — on toggle-OFF they called deleteSaveSubscription
for the whole row, which would silently drop the *other* kind (24200 vs
44200) if subs state was stale.
- store.rs: add remove_owner_p_kind mirroring merge_owner_p_kinds — same
BEGIN IMMEDIATE → closure → COMMIT/ROLLBACK shape; reads current kinds,
removes the target kind, then DELETEs the row if the list becomes empty
or UPDATEs kinds to the reduced list otherwise.
- mod.rs: add remove_save_subscription_kind Tauri command delegating to
store::remove_owner_p_kind; registered in lib.rs alongside the merge cmd.
- tauriArchive.ts: add removeSaveSubscriptionKind wrapper calling the new
command + notifySubscriptionChange().
- LocalArchiveSettingsCard.tsx: rewrite handleObserverToggle and
handleMetricToggle to use atomic commands — toggle-ON calls
mergeSaveSubscriptionKinds(KIND), toggle-OFF calls
removeSaveSubscriptionKind(KIND). The TS-side read-modify-overwrite and
the whole-row deleteSaveSubscription branch are removed entirely. subs
dependency dropped from both useCallback dep arrays.
- store_tests (new file): 4 unit tests for remove_owner_p_kind: removes
one kind leaving the other, deletes row on last kind, no-op when row
absent, no-op when kind absent.
Fix B: split archive test modules (Wes note 3)
The check-file-sizes.mjs override for archive/mod.rs had ratcheted
1465→1705 across the PR series. Split both oversized test blocks:
- archive/mod_tests.rs: extracted #[cfg(test)] mod_tests module from
mod.rs (~1208 lines, test-only content). Wired via #[cfg(test)]
#[path = "mod_tests.rs"] mod mod_tests in mod.rs.
- archive/store_tests.rs: extracted #[cfg(test)] store_tests module from
store.rs (~732 lines, fits under 1000 — no override needed). Wired via
#[cfg(test)] #[path = "store_tests.rs"] mod store_tests in store.rs.
mod.rs is now 532 lines, store.rs is 599 lines — both under 1000. The
two mod.rs/store.rs overrides are replaced by a single mod_tests.rs
override (1208). All 934 Rust tests pass unchanged.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Replace DEFERRED unchecked_transaction() with BEGIN IMMEDIATE via
execute_batch so the write lock is acquired before the SELECT.
With DEFERRED, two concurrent first-run seed hooks (observer + metric)
both read the empty owner_p snapshot, then race to write; the loser
receives SQLITE_BUSY_SNAPSHOT which busy_timeout does not retry, leaving
one kind silently absent until app restart.
With IMMEDIATE, the second caller blocks at BEGIN until the first
commits, then reads the committed row and merges its kind in. Both
callers succeed and the final row contains [24200, 44200].
Also fixes the inaccurate doc-comment that claimed DEFERRED gave an
exclusive write lock for the read-modify-write (it did not), and
replaces the mislabeled sequential-single-connection test with a real
two-connection WAL regression test using std::thread + Barrier that
exercises the actual concurrent write path.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Two concurrent seed hooks (observer 24200 + metric 44200) each fire on
first internal-build run with no prior owner_p row. The old TS-side
read-then-write merge raced: each hook read [] before the other had
written, so last writer clobbered the first kind.
Add store::merge_owner_p_kinds: reads existing kinds JSON, unions in
new_kind, and writes back under a single SQLite unchecked_transaction.
Concurrent callers serialize on the SQLite write lock — last writer
always reads the first writer's committed row.
Add merge_save_subscription_kinds Tauri command in archive/mod.rs (same
module as the other subscription commands; registered in lib.rs).
Both seed hooks and their deps interfaces now call this single command
with just their own kind, replacing the list+merge+create pattern.
tauriArchive.ts gains a mergeSaveSubscriptionKinds() binding.
Tests added:
- store.rs: create-when-none, adds-kind-to-existing, idempotent,
concurrent-interleave (observer writes first, metric second, both
kinds survive)
- useAgentMetricArchiveSeed.test.mjs: updated to new deps interface,
adds test_concurrent_seeds_both_kinds_survive (Promise.all interleave)
- useObserverArchiveSeed.test.mjs: symmetric update to new interface
File-size overrides bumped: archive/mod.rs +30 (command), store.rs
+110 (new fn + 4 tests — first override for store.rs).
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Subscribe to all owned agents' turn-metric events (kind 44200) as a
special case of the #1442 local-save archive primitive. Mirrors the
observer-feed (24200) wiring but uses the persistent /query proof path
since the relay stores and #p-gates 44200 events.
Key design decisions:
- owner_p+44200 routes to the persistent bucket (relay is source of
truth); owner_p+24200 stays on the ephemeral path unchanged.
- Decrypt at ingest (NIP-44, agent→owner); store plaintext payload JSON
so token-usage calculators can read archive.db directly without the
owner key. Fail-closed: decrypt error → drop, never store ciphertext.
- owner_p filter arm added to plan_archive: {ids, #p, kinds}.
- Both seed hooks (observer + metric) now merge existing owner_p kinds
before upserting, preventing concurrent first-run seeds from clobbering
each other. Observer toggle likewise merges rather than stomping.
- Build-time flag BUZZ_BUILD_AGENT_METRIC_ARCHIVE_DEFAULT (presence
remap → BUZZ_DESKTOP_BUILD_AGENT_METRIC_ARCHIVE_DEFAULT=1); default
off for OSS, baked on for internal builds via buzz-releases.
- Separate toggle in LocalArchiveSettingsCard with its own enabled state.
- KIND_AGENT_TURN_METRIC = 44200 added to kinds.ts.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>