Skip to content

Latest commit

 

History

History
730 lines (635 loc) · 47.8 KB

File metadata and controls

730 lines (635 loc) · 47.8 KB

Agents: current-Buzz compatibility V1

Scope

This document describes the read-only compatibility/mention slice. The Agents page also exposes native local controls for create/import, saved settings, mention-to-add/wake and bundled Start/Stop/Restart. The read-only compatibility view below remains the browser fallback; native management has its own handover and rollback contract.

V1 reuses the current Buzz library and mentions existing agents in channels and threads. No migration to relay-only storage. Creation, editing, add-existing membership, Save/recovery and all runner management are out of V1.

Implemented compatibility view

  • The live development broker (macOS and Linux) and packaged native host read the installed Buzz library at ~/Library/Application Support/xyz.block.buzz.app/agents/managed-agents.json (on Linux, $XDG_DATA_HOME/xyz.block.buzz.app/agents/managed-agents.json, defaulting to ~/.local/share). It does not search/merge the separate .dev library, read agent keys from Keychain, write the file, run migrations, or call old loaders with side effects.
  • Only definition ID/name, identity public key/name/definition link, and optional avatar artwork leave the host. Prompts, configuration, credentials and execution receipts are not projected. This is local library evidence, not verified ownership.
  • The main individual-agent grid is reserved for native local agents; teams follow it, with old/importable/relay inventory below. Browser-only hosts cannot establish current local custody: their read-only library lives in a collapsed Other agents section below teams, not in the individual-agent grid.
  • The compatibility library shows one compact row per exact identity. Identity details are available from its overflow button, not an exposed Public key link. Explicit profile links still supply artwork; names never join identities. Profiles with no linked identity appear separately; an archived identity does not become an empty profile.
  • Only distinct keys with the same displayed name need a short npub suffix. Names alone never create a profile group. Suffix collisions extend deterministically using the complete inventory, including identities hidden by archive filtering.
  • Missing archive evidence keeps identities visible. Archive filtering affects display only, never mention permission or runtime control.
  • One lazy host read per opening/Refresh; no polling or relay-directory startup scan. Concurrent host requests coalesce. Read caps: 8 MiB / 2000 records; malformed/missing files fail visibly without echoing their contents. The host projects from JSON, so private fields may transiently exist in host memory; there is no claim that JavaScript strings are zeroized. Browser reads time out at ten seconds and session disposal/cache/access/disconnect fences clear them.
  • The compatibility view uses only session.agentLibrary and session.archives. Unused relay ownership/configuration readers and native recovery code have been removed.
  • Avatars use saved library artwork, with initials on missing/failed images. Optional artwork accepts HTTPS without credentials or bounded raster data URLs, never file URLs or SVG data. Relay media uses the existing session media helper; CSP is unchanged. No extra profile scan or polling is added.

Source parity pinned at old Buzz b9392d9d78744df365f9276e1ffe8c1baa5ea903: desktop/src/features/agents/ui/AgentsView.tsx:221–253, lib/catalog.ts:3–12, ui/unifiedAgentGroups.ts:16–47, desktop/src-tauri/src/managed_agents/storage.rs:239–283, types.rs:180–209. Old load_personas can update builtins and write back; this adapter deliberately only reads the persisted post-fold library. It does not port builtin refresh, live runtime ordering or Teams.

Ready-to-try workflow / remaining acceptance

Use the public BUZZ_DEV_VIEWER pin and just desktop from the README; fixture/default startup cannot show the live local library. No private keys in environment files. Keep existing Buzz running: this app does not launch or supervise ACP.

  1. Compare Agents with installed Buzz's selected library; Refresh after changing that library in Buzz. Stop state should not remove cards. Duplicate display names must retain distinct exact keys.
  2. In a channel where the agent is already a member, select it from @ suggestions and send a short prompt; confirm the reply from the same key.
  3. Repeat in a thread and confirm the reply lands in that thread.

Browser fixtures cover library rendering/retry/session replacement and actual channel/thread composer publication, not a live ACP reply. Manual feedback reported a working live path, but a separately observed native/ACP trace, packaged acceptance and the final integration gate remain outstanding. Earlier envelope/fixture validation does not certify the later compatibility adapter.

Shared agent selection

session.agentChoices is the canonical read-only selection projection. Templates, mention pickers, the session agent chooser and their admission checks use it—not agentLibrary directly. Its general projection combines ready legacy identities and ready native identities in this exact community, deduplicated by public key. Templates use its separate templates projection: native identities when native controls exist, including authoritative empty/loading/error states without legacy fallback; legacy identities only on hosts without native controls. Template refresh never waits for an unused legacy inventory. Native process status is not selection eligibility; stopped/native-only agents remain selectable. agentLibrary remains the old-library compatibility/import source. Agents management and the shared display-name resolver keep their own distinct presentation contracts.

Do not build another agent inventory in a plugin. Retain the shared projection only while needed; explicit Refresh retries source failures. Retaining choices preserves ready evidence across menu remounts. Ordinary mentions subscribe to cached legacy hints without loading that library; session/template selectors ensure it on demand. Both use the app-owned native controller's idle-only ensure, not refresh-on-keystroke. The existing app controller owns native reads/processes, while the session projection adds no runner, polling, directory scan or signing authority. Session retirement revokes its candidates. A failed source contributes no stale candidates; another ready source can remain usable, with partial failures surfaced through Retry. status: ready means usable, not complete: automatic-recipient inference must honor complete, and automatic saved-template resolution must wait for required pending identity/roster evidence.

Archive visibility has one owner. The projection reads session.archives and exposes identities (every known agent, including archived ones, for facts about existing content) and selectable (known-archived agents removed; unknown state fails open; the viewer is never hidden). Every forward-looking agent chooser uses selectable, including the session agent picker and profile Add to channel. Session admission in workSessions.addAgents rejects every known-archived key, including parent-channel members; unknown archive state keeps the library and parent-member rules. Selectors demand the lazy archive read through useAgentChoices(session, includeLegacy, true), and the session picker shows an archive read failure with Retry. The same base rule, archiveHides, filters mention recipients, channel member invitations and member addition, and the Agents page and Agent Library. The Agents page keeps one documented exception: an archived identity with local controls stays listed so it can still be managed. Do not reimplement the rule per surface.

Agent artwork also has one owner: createAgentLibrary publishes snapshots where an identity without its own avatar shows its linked definition's avatar (inheritDefinitionAvatars). Surfaces read identity.avatar and do not repeat the lookup.

Action policy stays explicit: ordinary member mentions use the channel roster and hide known-archived identities without requiring verified non-archived evidence. Ordinary nonmember mentions also offer people from the selected community directory and eligible managed agents. Send asks before adding them; selection grants no access. A pasted mention of a known profile is offered the same way. Session invitations retain their existing rules, including legacy choices. Templates additionally require verified non-archived state (templateAgentChoices returns nothing until archive evidence is ready), and legacy-only choices need visible community membership. Saved keys are never rebound to a namesake. Template pickers and agent identity details display npubs, not raw hex keys. Save-as-template discloses an incomplete inferred lineup when its required inventory or roster evidence is partial; it never claims a full channel-membership copy. Shared choice visibility is not permission to grant access.

Regression sources: features/agents/choices.test.ts and bundled/channel-templates/agent-selection.test.tsx, plus existing chooser, composer and session-admission tests. These exercise shared selection and session admission, not native execution. Native/ACP acceptance and packaged validation remain separate gates; local hook and hosted CI results are recorded in the pull request.

Exact channel-member mentions

The shared channel summary now exposes exact members from its existing verified relay-authored kind-39002 roster, without a second directory or subscription. The bundled Mentions plugin offers Mention a member in both channel and thread composers through the shared conversation tool contract. The host retains recipient intent, inline editing and avatar removal even when the chooser plugin is disabled. The picker shows keys alongside names (namesakes remain separate), reads optional profiles only on demand, and keeps selected identity spans in scoped drafts. Typing a name alone does not notify anyone. Plain Space after a unique exact name selects its recipient; ambiguous names require Tab, Enter or click. Editing a selected span removes its notification intent. Native beforeinput ranges preserve untouched spans; missing range evidence, IME/history edits and collapsed deletions clear selections rather than guess. Even a same-text replacement drops the edited identity. Selected mentions appear as inline identity chips in the composer. Namesakes selected together receive visible key qualifiers; editing a selected span removes its notification intent. Chips remain available without the Mentions chooser.

Chooser rules

Two parts of the app decide who you can mention:

  • The Mentions plugin decides what the chooser shows. This covers both the toolbar picker and the inline @ list: who is listed, in what order, and when Space picks a name (src/bundled/mentions/).
  • The composer decides who a message can address. It checks every mention before it goes into the draft, whatever added it (mention-admission.ts). The plugin lists only people that this check accepts.

The composer accepts these people:

Where you write Who you can mention
Channel or forum Anyone. Before sending, the app asks what to do about people who are not in the channel.
DM Anyone. People outside the DM are named but not notified.
Session Session members, plus your agents when the session invites agents.
New DM, before it is created Only the people you chose for the DM.
Archived or read-only channel Nobody.

Everywhere, the composer refuses invalid keys and people known to be archived. You can always mention yourself. If archive state is unknown, the mention is allowed.

A mention notifies a member. It does not promise that an agent will answer, and choosing someone does not give them access or start an agent.

Search trims and lowercases the query. Members precede nonmembers, with humans and agents in each group. Within each group, matches against the visible resolved label come first: whole-name exact, name prefix, whole-word exact, then word prefix. Base names and known aliases are fallback matches in that same order. Only resolved names and real profile/agent names are searchable. Public keys (including unnamed identity fallbacks) are not completion matches. Arbitrary name substrings do not match. A hidden base-name match never promotes a weaker visible-label match. When visible-label match quality ties, base-name/alias match quality breaks the tie before recipient preferences. Among equal matches, agents with profile-reported ownership by the viewer come first, before humans and other agents. Agents that share a base name then form one block, placed at the first of their case-insensitive displayed labels (including disambiguating suffixes). Inside a block, explicit-choice recency, managed status, and already-known online/away status decide the order. Humans never join a block and sort first on an equal label. Remaining ties use the displayed label, then the full key. Each rule is a per-choice sort key, so the order is the same for any input order. Ownership comes from profile owner metadata, not a name or presence in the saved library. Recency stays in memory per session/destination and is bounded to 100 destinations and 100 recipients each. Neither ownership nor recency overrides membership or match quality.

An open query installs at most 50 keys. Their order and membership stay fixed until the query changes or the chooser reopens. Labels, insertion names and availability remain live. A removed/archived row stays disabled in place; Enter/Tab cannot fall through to sending. A member who becomes an outside invitation choice also stays disabled until reopening. Retry refreshes evidence, not the installed order. New arrivals need a changed query or reopening. Pending sources or missing profiles do not freeze a premature empty result. Local sources (members and agent choices) establish the list; the community directory never gates it. Directory people append below the rows already shown, so a late page never moves a visible row. While a new query waits or loads, still-matching people from the last settled page of the same chooser stay visible (one picker, or one inline @ token; inline completion remounts per keystroke, so the page is kept per session outside it) and the chooser shows "Searching community…". Uncached queries reach the network only after a 200 ms typing pause. Settled first pages are cached per session and query (100 queries); errors are not cached, and Retry reads the current query again. Chooser identity naming uses eligible candidates plus the current draft recipients, not every cached profile. Composer chips name their recipients among the destination's members plus the draft recipients.

Plain Space selects only a unique exact name/alias/label across the full uncapped candidate set, and only if that identity is displayed and still eligible. A known longer name beginning with that name plus a space prevents selection. Partial names, ambiguous names, modified Space, IME composition, code and protected literal ranges keep ordinary editing behavior. Selection rechecks available evidence and stores only {pubkey, name}; qualifiers are presentation, not wire data.

The composer rejects already-known archived recipients (never the viewer) at send entry. The next draft keeps only agents that the sent message notified. This is not an archive transaction: archive changes during enrollment, dispatch or retry are intentionally not covered. The existing relay membership/send/retry validator is unchanged.

After an accepted send, the next draft starts with the exact selected agent-name mentions, deduplicated by key. Agent classification uses already-cached profile hints or the shared agent-choice projection (legacy and native), not a new lookup or permission grant. Human recipients and plain typed names are not carried forward. The prefill is an ordinary scoped draft: channel/thread/account isolation, edits, removal, undo and delivery checks still apply. Settings → Messages → Remember mentioned agents defaults on and is saved on this device. Turning it off stops future prefills without changing the current draft; turning it back on does not restore old recipients. Session auto-recipient rules are unchanged. An outbox rejection preserves the original draft; acceptance is not proof of relay delivery or agent execution.

Refresh teams on Agents reloads the provider-owned catalog, including changes made in another window or device. After a save/delete revision conflict, close the dialog, refresh, and reopen the current team before retrying; stale drafts never silently overwrite a newer revision.

Saved teams from the Agents page are available in both mention choosers. A team is a shortcut, not a group identity: explicit selection inserts its saved agent keys as individual mentions in one undoable edit. Names never resolve membership. Typing a team name or Space alone does not select it. Team names also prevent Space from accidentally selecting a person with the same name or a prefix. Each query shows at most 20 matching teams, with up to four cached avatar thumbnails and a remainder count. Empty, oversized (more than 32 agents), changed or partially unavailable teams stay disabled; there is no partial expansion. Reopen or change the query to refresh changed team choices. The resulting draft must fit the existing 32-mention and message-length limits. Selection never adds members: ordinary channels retain invite-on-send consent, DMs retain reference-only outsiders, and session admission remains unchanged.

The picker supports Up/Down navigation, Enter selection and Escape dismissal.

session.messages.send/reply accepts up to 32 exact notification pubkeys and emits deduplicated p tags. In ordinary channels, Send pauses for selected nonmembers: Invite grants access only with permission and explicit consent, waits for confirmed membership, then sends notifications. Do nothing (or Send anyway without add permission) sends those identities as separate mention reference tags, without adding or notifying them. Close or Escape keeps the draft. Existing member mentions still notify in a mixed send. Reference keys are validated and bounded to 32. Selection itself never invites or starts anyone; confirmed outgoing notifications own agent wakeup. See local agent controls. DM and session admission paths remain separate. Current notification-recipient membership is checked at intent, before signing, and after signing before entering the transport publisher; retry/restored signed intent uses the same publisher check. Before each mention publication the session performs a bounded foreground finite read of this channel's kind-39002 roster under the captured relay author. It never joins an older in-flight read. Empty/failed/malformed evidence blocks dispatch; access, connection and cache changes fence the read. A newer observed removal beats an older response. Neither AUTH, route establishment nor the optional metadata-read status substitutes for this preflight. Failed/capacity-limited live routes can use the finite evidence while connected. One finite request per mention attempt is a safety cost, not additional startup work or polling.

A first known local rejection is failed/unsent. Finite membership preparation runs under the same outbox deadline but outside the dispatch phase: a timeout there is unsent and starts no confirmation reads. The final synchronous scope/membership check runs immediately before publisher entry, after all asynchronous preparation. A blocked retry preserves prior unknown/accepted delivery evidence and reports its retry error separately; the first dispatched attempt may already have delivered. These checks are client UX safety, not a substitute for relay authorization, a membership transaction, or the ACP listener's own admission rules. Network changes after transport dispatch remain possible. No ownership or running status is inferred from a member's name/profile.

Wire compatibility is kind 9 + h + exact p for notifications and mention for reference-only identities; direct replies also carry ["e", root, "", "reply"]. Existing buzz-acp owns mention admission, replay, channel membership, pool wake and harness execution. This slice adds no wake loop, process launcher, configuration save or agent invitation operation. The local library and archive display are not mention authorization.

Focused coverage: mentions.test.ts, MessageComposer.test.tsx, mention-draft.test.ts, broker sign/publish integration and the real React journey tests/browser/mentions.spec.mjs and mention-edit.spec.mjs (Chromium + WebKit, fixture identities). mentions-live.test.ts exercises the actual subscription → session → outbox path across reconnect, route failures, stale/failed preflight, restore, optional name failure and unknown delivery. Live ACP reply and packaged desktop acceptance are not established by these tests.

Relay-scoped archive display

session.archives is a lazy NIP-IA snapshot capability, independent of page/plugin lifetime. The dev broker passes archiveAuthority only when the community's NIP-11 advertises a valid explicit self; the legacy contact pubkey fallback continues to serve existing reads but cannot authenticate archive state. The host-supplied signed transport does not yet discover NIP-11 and therefore leaves this capability unavailable. No secret crosses the browser boundary.

One fresh background read requests kind 13535 from that exact authority, limit 1. The verified response must contain exactly one empty-content, protected snapshot within 2 MiB. Missing/failed/malformed/oversized evidence is unknown, never an empty active list. Invalid p keys are ignored and extra p elements have no semantics. A successful snapshot replaces the entire list; newer timestamps / lower-id ties win. A session-local ordering fence survives cache clear, without retaining old archive contents; it is not persistent rollback protection across sessions/devices. Access purge, disconnect, cache clear and disposal clear evidence and cancel work. No startup request, periodic poll, event-union seeding or history filtering is added.

This is finite evidence, not a live archive directory. My agents consumes it for display. As in base Buzz, mention autocomplete, the mention picker and member-add omit archived identities: fail-open while the snapshot is unknown, never hiding the viewer from themself, and never touching history or channel membership. not-archived means absent from the read snapshot, not online, owned, authorized or guaranteed current at a later write. Delta processing, native discovery and packaged/live acceptance remain separate.

The profile pane's Archive agent / Unarchive agent actions (base Buzz copy) send exact 9035/9036 requests (["-"], one p, optional auth) through dedicated broker sign/publish routes, never the outbox writer. The render guard and a fresh pre-sign check accept only the target itself (NIP-IA self request, no auth), a verified NIP-OA owner, or a relay owner/admin in the relay-signed 13534 roster; the relay re-verifies consent. An owner request copies the target's single live auth tag, verified against the target with kind= clauses ignored and created_at bounds checked against the request time. Success requires a fresh 13535 re-read showing the new state; a publish with an unknown outcome is reconciled by that re-read, and only a definitive relay rejection skips it. Rows are withheld while state is unknown; failed checks retry in the background and on window focus, as base Buzz does, and a mounted profile re-reads after a disconnect or cache reset.

Delete agent (base Buzz delete_managed_agent copy) is shown only to the verified NIP-OA owner, for an agent with exactly one native record in this community. Base Buzz removes the record first and queues the archive in its native retention store; this app has no such store, so the irreversible step runs last:

  1. An exact one-member 9001 (h, p, client-id) goes through the outbox for every channel whose relay-signed 39002 roster lists the agent, plus the viewer's loaded channels. Each channel's own fresh roster must then omit the agent; a roster the viewer cannot read leaves that channel unconfirmed.
  2. A fresh archive read runs; unless it already lists the identity, it is archived through the request path above and confirmed.
  3. agent_control_delete (shared with Agents → My agents) checks the record revision, stops the listener, then deletes this app's saved key before removing the record; a key-deletion failure fails the delete and leaves it retryable. Deployed remote records, which native refuses, get no Delete action.

Any failure before step 3 leaves the record and Delete in place for retry. Closing or retargeting the profile admits no new removal, archive or native request; work already dispatched settles, and a native removal in flight completes without closing whatever profile is shown next. Protocol source: old Buzz b9392d9 docs/nips/NIP-IA.md, especially relay identity, snapshot format and snapshot/delta consistency. Tests use the actual session and HTTP broker/verified transport, including corrupted signature rejection.

Current feedback round and deferred validation

Composer: grouped @/emoji controls on the left, icon-only send on the right, inline recipient mentions and compact avatar suggestions. No placeholder actions, rich-editor migration or changed notification semantics. Cards use squircle avatars, short labels and expandable exact keys; no running/ownership badge.

This feedback round passes typecheck, targeted Biome, 118 focused adapter/session/ envelope/composer checks, and the Agents + channel/thread mention journeys in Chromium and WebKit (4 browser checks). The agent fixture checks loaded artwork and the exact-key disclosure. Locked Cargo metadata resolves for Apple Silicon macOS after dependency pruning; no native compilation was run. Broader scan/native/package checks and independent compatibility-adapter review remain deferred until an agreed integration batch. Do not gate ordinary visual feedback on them. Broader agent architecture proposals are outside the V1 scope.

Raw Agent Activity plugin

Agent Activity is an independently toggleable bundled plugin. Compact avatar/name/status rows sit below messages and above the thread composer. The channel composer instead shows a collapsed Channel-wide activity summary with an agent count. Expand it to inspect all channel activity, including work in threads and unknown statuses; it does not imply another job is running in the channel conversation. Sidebar and thread-summary working dots are unchanged. Observer turns have no thread identity, so thread typing never hides channel telemetry for that agent, including simultaneous work. Channel navigation resets the disclosure; ordinary activity updates preserve its open state while activity remains. When the last evidence disappears, the disclosure unmounts and resets. Hover/focus on an agent row shows an owner-only summary; click, tap, Enter or Space opens that exact agent's channel activity in the right panel, including work in other threads. Optional names and avatars reuse shared background profile queries; key fragments distinguish identities without profiles.

Thread indicators consume the existing kind-20002 typing signal with the resolved NIP-10 root, not inferred observer turn IDs. The existing per-channel live route carries it; typing bypasses ordinary history, unread and persistent caches. Only identities observed on the current live owner-visible feed are recognized; restored history never establishes typing ownership. Typing before that first frame, or without telemetry publication, is deliberately omitted; public typing alone does not establish ownership.

Typing expires eight seconds after its signed timestamp (future clock skew is capped at receipt), swept by the existing one-second activity timer. Messages clear only the matching agent/channel/thread scope and suppress delayed typing for two seconds. Disconnect, channel-route failure, disable, access/cache clear and disposal drop typing evidence. A fresh observer frame does not refresh it.

The sidebar's quiet working dot has two independent sources. The plugin source is fresh channel observer turns or the channel-scoped typing above, and follows the plugin's lifecycle. Observer records have no thread identity, so its details remain explicitly channel-wide.

The display-only source is session.typing, not the plugin's typing evidence: typing by one of the viewer's own agents (the local library) anywhere in the channel, threads included. Typing remains a working signal when observer telemetry is explicitly disabled. Other people's agents, known only from a self-declared profile hint, are not shown. This store keeps working with the plugin off, rejects future timestamps instead of capping them, schedules its own expiry eight seconds after the signed timestamp and stays quiet for two seconds after the typer's message. It is display-only evidence, not ownership; while the plugin is off, the channel popover lists such an agent without a View activity action. A timeline thread summary shows the same dots from this source while one of the viewer's agents types in that thread; a thread with no replies yet has no summary to mark. No harness change, new subscription, directory or timer is added. The development broker loads subscription filters at startup: restart the existing dev server once to receive typing; frontend HMR alone is insufficient.

A known agent's profile View activity action remains available before the first frame or after a working chip disappears; people's profiles offer none. It preselects the exact identity and originating channel, not a thread. The known-agent check is display-only evidence, not an ownership badge, and the action may show a waiting state for identities with no published owner-visible telemetry. Shared agents and new activity-view permissions are out of scope.

The Channel selector filters raw entries and working-turn counts, or shows all channels including unscoped records. For a selected channel, batches are projected as individual matching children, with the original envelope ID retained; displayed child JSON is reserialized, not claimed byte-identical to the envelope. Unscoped children are omitted rather than inheriting the enclosing batch's channel. The all-channels diagnostic retains the exact raw envelope. Raw capture is unchanged. Channels owns contextual panel placement and closes it on channel/session or contribution changes; close returns focus to the originating control if retained.

The plugin's activation leases session.agentActivity for the live display; closing the panel does not release that demand. Disabling the plugin clears its live evidence, but does not control archive capture. The shared live connection carries one dedicated owner (#p=viewer) route for demanded observer/metrics kinds, with no #h or history limit. Live activity starts at actual dispatch/retry. When a kind-demand change replaces a metrics capture route, stored 44200 replay bridges the admission delay with a 60-second in-flight overlap, capped at four minutes at dispatch to leave headroom inside the five-minute ingest window. This replay floor clears at EOSE; later retries are live-only. Ephemeral 24200 cannot be recovered this way. Settings changes and plugin demand replace that route only when its combined kind demand changes. While capture is enabled, display-generation changes keep the wire and capture floor but advance a separate display freshness floor. Without capture, display resets still renew the live-only route. Neither lifecycle replaces the socket or chat globals. An uncertain control failure can reconnect the shared stream through its existing bounded recovery path.

The native identity host and development broker validate signatures, exact tags, recipient/key, freshness and size before ingest and host-only NIP-44 decryption. The renderer receives purpose-bound DTOs, not keys or a general decrypt API. Historical decoding accepts only rows owned by the host archive, not arbitrary renderer-supplied old envelopes. Native ingestion is renderer-delivered, host-validated, not host-captured: the main renderer owns the relay socket and supplies fresh envelopes through IPC. The native host does not independently prove relay delivery or that the signer belongs to the viewer. Buggy or malicious trusted renderer/plugin code can plant plausible history and evict genuine rows by filling the quota. This deliberately retains the existing trusted main-WebView model, not a hostile-plugin boundary; moving the relay connection or isolating plugins is outside this change. The development broker instead captures only from its own stream and rejects browser ingestion. Relay admission, not a name, local library entry or successful decryption, is the ownership authority on the normal delivery path. Saved rows are not ownership proof. These records never enter ordinary message history, unread reconciliation or channel caches.

Live RAM is limited to 200 envelopes / 2 MiB plaintext and 512 turn states, with visible trimming. Historical pages do not consume the live-record allowance. Disable, cache/access reset and session replacement clear live evidence. A batch with a recognized denied channel is hidden as a whole.

Host-owned saved activity

SQLite stores original signed, owner-encrypted envelopes, partitioned by normalized community endpoint and viewer public key. It stores no plaintext, decoded channel metadata, turn state or private keys; signed metadata (agent/recipient keys and event timestamps) remains visible on disk. The native database lives under the app-data directory in archive/events.sqlite3 (archive-debug for debug builds). The development broker uses ~/.buzz-foundation/dev-archive/events.sqlite3 on the broker's machine, not browser-local storage. Settings show the actual path.

The store implements exactly two owner-scoped policies, both enabled. Unused scope/value/kinds columns are removed by a transactional v1→v2 migration that preserves settings, revisions and ciphertext. General subscription matching and management wait for a caller beyond these two policies; the schema does not claim that capability:

  • Activity (24200): 30-day maximum age by default, configurable from 1–90 days (Settings offers 1, 7, 30 and 90); 512 MiB of serialized envelopes per viewer/community, with a 128 MiB per-agent limit.
  • Turn metrics (44200): independent capture, 90-day maximum age, 64 MiB per viewer/community and 16 MiB per agent. Metrics remain encrypted, unlike classic's plaintext metrics archive. No usage dashboard or arbitrary-subscription UI is added.

Quota totals choose the overflow; indexed oldest-first scans stop after enough bytes are found, rather than rescanning retained history. Oldest records are evicted when byte limits are reached, so retention is a maximum age, not a promise of a complete 30-day transcript. Budgets are separate for each account/community and kind, not a physical device-wide disk cap. SQLite indexes, WAL and other overhead use additional space. Expiry across inactive partitions is operation-driven and throttled to once a minute, not an OS background purge; reads also exclude expired rows. Pruning reclaims free pages incrementally and explicit clear attempts full free-page reclamation after the delete commits. Newer schema versions are rejected without downgrade.

Reopening/reloading reads 100-row keyset pages through the key-owning host. Historical decode revalidates signatures, exact tags, recipient and retention age, without weakening the live five-minute freshness gate. Bad rows are counted and skipped without trapping pagination. Live records win event-ID deduplication. Restored rows are display-only: they never create working turns or typing evidence. Channel-scoped rows need positively known local channel access, including every recognized batch child. Access changes immediately re-filter retained rows without rewinding pagination. Unknown/capped/suspended membership hides history rather than erasing it; positive access can reveal it again. Agent removal and sign-out do not erase ciphertext; another account cannot open that partition.

Settings → Agents → Saved agent activity has independent capture switches, activity retention and confirmed Clear activity history / Clear turn metrics actions for this account/community. Shortening retention requires confirmation. Turning capture off preserves saved rows. Clear advances a durable revision to fence stale writes and pending hydration; new traffic can be captured afterward. Another window's already-decoded display is not synchronously reconciled; reload it after clearing elsewhere. Developer cache clear also deletes this community's activity partition and reports deletion failure rather than claiming success. Storage/decoding errors, including metrics-save failures, are visible and do not stop live telemetry. An initial native settings-read failure retries at 1/2/4 seconds while settings remain unknown; disposal cancels retries and fences late responses. After exhaustion, open Settings or reconnect to retry. SQLite waits, maintenance and paging run off the async thread under only the archive mutex; the identity lock covers envelope validation/decryption, never storage. Viewer checks surround storage/decryption. The identity is immutable once ready today; a future in-process account switch needs a generation fence for admitted writes. Settings values and their CAS revision are read from one SQLite snapshot. No old-app import, general relay backfill, export or transcript redesign is included.

Working is fresh per-turn evidence, not process status. Batch children fold individually; session_resolved is activity, while turn_completed, turn_error and agent_panic end the agent/turn pair even with a null session ID. Silence beyond 30 seconds or disconnect makes work unknown, not stopped. Terminal state retains a monotonic evidence timestamp through clock rollback and bounded eviction. There is no agent-global sequence gate: producer sequences reset, skip and interleave.

Try with an existing owner account

Use the README's public-pin/Keychain setup and run bin/just web (or bin/just desktop). Open the printed Local URL, choose the agent's community and open a channel. Keep the existing Buzz runner active, with telemetry publication enabled on the agent, then give it work. This activity plugin does not start agents. App-managed agents now default to publishing on their next normal start; explicit BUZZ_ACP_RELAY_OBSERVER=false overrides for Pi/Goose remain honored. Existing running processes are not restarted by this change. No records may mean publishing is off, no new traffic, or an interrupted feed—not that an agent is idle.

For a contextual view, click the identity's avatar/mention in the channel, then View activity. It preselects that exact key and channel; Channel → All channels broadens the view. Alternatively, select an active agent above the channel or thread composer. Expand raw entries and close/reopen the panel. In Settings → Agents → Saved agent activity, verify both capture switches and the host path. Disable the Agent Activity plugin in Settings → Plugins, give the agent work, then re-enable: archive capture should continue independently, and restored rows must not claim the agent is working. Reload and reopen View activity; saved entries should remain. Use Load older activity when another page is available.

Turn Save agent activity off, give the agent work and verify live display still works without retaining that new activity across reload. Turn it back on. Cancel a retention-shortening confirmation and verify the original value remains. Confirm Clear activity history in Settings, reload, and verify old activity is gone while metrics remain independently retained. For native acceptance, quit/relaunch the desktop app as well as reloading its view.

The feed is best-effort telemetry: producers coalesce/batch and may elide oversized content, and storage budgets can evict rows early. It is not a complete ACP transcript. Both the development broker and packaged native identity host implement the archive; no runtime controller, recording export or old transcript renderer is included.

Evidence and remaining acceptance

Archive regression coverage lives in dev/archive.test.mjs, src-tauri/src/archive/tests.rs, src/features/archive/client.test.ts, the activity/session/native transport tests, and src/app/ArchiveSettings.test.tsx. The real Tauri IPC test exercises command ACL, persisted encrypted rows through two isolated processes, account/community admission and independent clear. The browser archive journey uses real broker SQLite and the actual plugin/Settings wiring, including ciphertext-at-rest, reload without working evidence and confirmed clear. These are synthetic identities/telemetry. Packaged GUI quit/relaunch, real-relay agent ownership admission, human acceptance and hosted cross-platform checks remain separate gates; consult the PR for results tied to its exact head. Historical validation below predates this archive rework and is not evidence for the new head.

dev/agent-observer.test.mjs, dev/relay-broker-live.test.mjs, and the activity/live service tests cover signed/encrypted WS → host decode → SSE → actual session, route generations, no chat reconciliation, terminal retention and stale controls. tests/browser/agent-activity.spec.mjs covers the actual plugin, raw HTML nonexecution, keyboard disclosures, agent selection, disable/re-enable and light/dark layouts at 1280 and 390 pixels in Chromium and WebKit. Live retry and plugin-launcher regression journeys also pass with the additional observer route. These automated checks use only ephemeral identities and synthetic upstream telemetry. The owner reported a successful live activity try on 2026-09-12 before the mainline merge; this is feedback evidence, not an independently captured trace.

The full just scan passed at 1183b2624485dc1e6a12e86cece22eaf7513591c after merging main's Markdown and Terminal changes: 35 Node integration tests, 1,054 Vitest tests, 16 plugin-manager Rust tests, 274 Chromium/WebKit browser checks (including measurements), 14 design-browser checks, 9 native Rust tests, formatting, types, builds and Clippy. Independent source review found no remaining merge-integration blocker. Channel-opening fixtures kept optional profiles held; warm click-to-visible samples were 16.8–19.2ms in Chromium and 50–67ms in WebKit, with no new head read, below the unchanged 100ms budget. These are local Apple Silicon fixture measurements, not a live-network SLA.

Packaged/native activity uses the native observer decoder; attended native/package acceptance and cross-platform CI remain separate from these local results.

Composer-entry feedback rounds

The first channel-only pass added a generic plugin accessory below the composer. On the uncommitted tree based on d5877002e2e58a601e1f46dd67697356e665164d, TypeScript, changed-file Biome, all 1,121 root Vitest tests and eight focused Chromium/WebKit activity journeys passed. This is historical feedback evidence, not validation of the current snapshot.

The 2026-09-16 round moves compact activity rows above both composers, adds exact-thread typing indicators and a quiet sidebar working dot, and preserves plugin-owned capture and revoked target-opening callbacks. Multiple turns are grouped by exact agent key. Stale observer evidence shows status unknown, not completed; expired typing and ended turns leave the rows. Profile access remains.

On the uncommitted tree based on a67102aa1201adfa47a03be7d668a62ac748c152, TypeScript, changed-file Biome, all 1,128 root Vitest tests (116 files), and all ten Chromium/WebKit activity journeys passed. The Chromium cold/warm channel-opening check also passed. Browser coverage includes exact-thread/sibling/channel isolation, sidebar semantics, channel-detail navigation, above-composer geometry, and light/dark 1280/390px layouts. Screenshots were inspected. Independent changed-path source review found no ownership, routing or lifecycle blocker; the subsequent future-timestamp expiry cap has a passing regression test.

Wes reported the live local workflow working and approved the tightened layout on 2026-09-18. Activity rows are borderless, avatars align with the composer's left edge, and the last row sits 4px above it. Working dots gently pulse unless reduced motion is requested; unknown status remains static. Browser regressions cover these presentation contracts in both engines.

The 2026-09-18 integration incorporates main 125b5ca while retaining its rich composer, routed-thread navigation, sidebar activity popover and public typing indicator. Public typing and owner-only activity remain independent projections; an owner's typing agent can appear in both. The receive path rechecks session access/generation after shared typing subscribers run, before admitting activity.

On 65213eb plus the resolved main integration, all 1,576 Vitest tests (150 files) passed. Both engines passed the shared typing/layout cases (22), then the activity, Buzz-link and thread-history cases (20) after fixing internal activity targets being swallowed by the broader buzz: navigation classifier. Existing activity cases failed before that fix in both engines. Accessory lifetime coverage now mounts real React in StrictMode rather than mocking hooks.

These are targeted integration checks, not a completed just scan. The earlier scan was interrupted during browser tests; broader hosted CI, DCO and required review remain separate gates. Packaged native activity now uses purpose-bound observer decoding; attended packaged and live-relay acceptance remain outstanding.

Shared identity names

Distinct agent keys with the same displayed name receive a short npub suffix, regardless of their profile links. Names are compared after trimming outer whitespace, with case preserved. Directory qualifiers use a middle-dot separator (Honey · 2abc). Unique displayed names have no suffix. These display labels are not serialized into mentions. The composer retains its existing inline-chip qualifiers for selected namesakes, independently of live directory labels. Collision checks include hidden library identities and ready native identities in the current community, using the same native/inventory/public-profile precedence. Name edits update the suffixes; they never merge identities or profile groups.

The Agents plugin supplies display names through the app-owned identity-name service. Each relay session binds its own view. A ready native record takes precedence only in its matching community; otherwise the ready legacy display inventory supplies the name, then the public profile. Plugin disable restores public-profile names. These labels never change identity keys, membership, credentials, or runtime admission. Profile panels, messages, mention choices, activity, conversation labels and new notifications consume this view. Mention parsing still uses signed identity evidence before resolving its visible label.

Additive community inventory

The active session reads the owner's kind-30175 profiles and kind-30177 identities from its accessible relay. It also retains the local library reader. The inventory joins exact public keys, not equal names; explicit profile references use the publisher's slug mapping only when local definitions do not collide. Local names and artwork win for matching keys. Native configuration still wins within its matching community. A failed source leaves the other source visible with a warning.

Discovery is not global coverage, verified membership, credentials, or execution status. Native cards keep their controls. Other known identities appear in a read-only section. Each card offers its own Import; the separate installation browser appears only for repair. No keys, config, memory, membership, or runtime state are changed by discovery.