Repository navigation
feat(bestie): hourly check-ins from your Bestie - #2037
Conversation
43efb9f to
f688371
Compare
47c3bb1 to
0ae3227
Compare
wesbillman
left a comment
There was a problem hiding this comment.
🤖 Star Lord’s automated source review via Wes’s account, integrating independent Rocket, Mantis and Groot lanes.
Reviewed head 0ae322732ddc6cb87733dbd22e2ea9d2b204fbec against base f68837125121e8dfb1360086b40cc7b8fdf5d1dd. Two P2 findings are inline: failed enable leaves a live timer behind an unchecked switch, and a timer paused individually in Attention still appears to be running in Bestie.
Optional: exercise keyboard focus during pending saves and error/retry. docs/design-system.md documents read-only, focusable pending switches; this tab disables all switches during a save. Actual Base UI/browser focus behavior was not verified, so this is a validation suggestion, not an asserted focus-loss defect.
Scope: all five changed files and relevant save, timer, wake, DM and shared-control consumers; public description (no image attachments) and three commit messages. No concrete public-material disclosure finding. The existing Attention owner is reused and the change is bounded; the two state/recovery defects prevent a clean correctness verdict.
Source-only: no tests, builds, app launches or PR code execution. Rendered themes/layout, keyboard/assistive-technology behavior, native wake delivery and human acceptance remain unverified. One hosted-check snapshot shows the later CI-required failure with WebKit 2/14 failing, after an earlier successful run; annotations only report exit 1, so cause and relation to this patch are unestablished. Windows validation was skipped. Added test execution cost was not measured. This COMMENT is not approval or merge authorization.
| // Its timer runs only while the agent's Attention is on. Off leaves | ||
| // it on: the agent's other watches may need it. | ||
| ...(timer && { attentionEnabled: true }), | ||
| }); |
There was a problem hiding this comment.
🤖 P2 — Do not leave an enabled timer after the instruction save fails. This first save commits the hourly timer and enables the agent’s Attention before the following config save. A rejected/conflicting instruction revision leaves those effects in place, but featureOn reports unchecked because the Hourly upkeep section is absent. The scheduler (runner.ts:2012–2074) admits the saved timer independently of that section, so, with Watching on and destination membership confirmed, the failed enable can keep consuming tokens hourly. Clicking the unchecked switch retries On; it does not remove the live timer. Preserve the intended timer-first ordering by keeping staged work disabled until instructions are confirmed, or compensate the owned timer on failure and expose explicit partial-state cleanup. Do not enable unrelated watches until the operation succeeds. Cover instruction rejection and both retry/cancel recovery; reporting unchecked alone is not a safe recovery state.
There was a problem hiding this comment.
🤖 Fixed in 9479b07. If the instruction save fails, the timer and its Interest go back to how they were. Attention goes back off only if this save turned it on. If the undo fails too, the error says so and a Remove row takes the timer away.
| blocked && required && !checked | ||
| ? `Turn on ${required.title} first.` | ||
| : // Attention reads off while no plugin shows it, as when the | ||
| // Watching plugin is off, or while the agent's switch is off. |
There was a problem hiding this comment.
🤖 P2 — Reflect a timer paused individually in Attention. After a successful setup, pause bestie-hourly-check-in using the existing Attention tab. That writes the timer’s value.enabled = false while leaving its object and agent.attentionEnabled present/true (watching/Tab.tsx:439–452). Here featureOn checks only existence, and this paused branch checks only agent-wide Attention, so Bestie still shows Every hour checked with the normal hourly-running description although the scheduler skips the timer. Include the individual timer’s enabled state in the status and give usable rearm guidance. Preserve a way to remove a paused timer; simply treating it as absent can hide Off. Add coverage for pausing in Attention and returning to Bestie.
There was a problem hiding this comment.
🤖 Fixed in 9479b07. A timer paused in the Attention tab now reads "Paused in Attention. Turn Every hour off and on again to restart it." Off still removes it.
f688371 to
a9dc383
Compare
0ae3227 to
23475af
Compare
loganj
left a comment
There was a problem hiding this comment.
🤖 Larry and Auto: an independent adversarial review of 23475af. Source review only: we read the code and did not run the app or the tests. Labels: error, convention, preference, advice, question. Must-fix items say so.
Verdict: approve. The check-in posts only at the top level of the owner's DM, and only when warranted; other channels in mem/heartbeat are read, never posted to. The sleep test (one run after a five-hour jump, then one an hour later) is good. We first reviewed 0ae3227; the new head already fixes our main finding (a failed instructions save after the timer save left an hourly timer behind a switch that read off) and our paused-timer note.
| channel, | ||
| }; | ||
| } | ||
| // Attention first: if the instructions then fail to save, Off leaves |
There was a problem hiding this comment.
preference: the undo works, but it adds a compensating save, a second failure message and a Remove row. Reordering does the same with less: on the on path save the instructions first, then the timer. A section without a timer is inert and already reads as off, so a failed timer save needs no undo. Keep timer-first for off, where it is already correct.
23475af to
9479b07
Compare
ee46772 to
e10c4d5
Compare
9479b07 to
c2c4e76
Compare
|
🤖 This review note doesn't block this PR. It's tracked for one follow-up PR after the stack lands:
|
Your Bestie gets a **Bestie** tab on its agent page, and no other agent does. The tab says "This is your Bestie", has **Change** to pick another agent, and lists Bestie's settings, which later PRs in this stack fill in. **Why a tab:** Bestie's settings live in a plugin tab that shows only on the agent it applies to, and each setting is stored in the thing it changes (instructions, timers, channel membership). **What changes** - **Host:** a plugin's agent tab can pass an optional `when(agent)` filter, plus `subscribe` so the host re-checks it. That touches `features/agents/service.ts` and `tabs.tsx`. Tabs that set no filter, such as Watching, behave exactly as before. - **Choosing a Bestie:** the **Make my bestie** choice moves out of `AgentsPage.tsx` into `BestieChoice.tsx`, so the tab's **Change** reuses it, including the confirm-before-replace dialog. - **Saved Bestie:** `default-agent.ts` gains `savedAgentStore`, `useSavedAgent` for use outside React. With no window (node tests), it follows the account's choice only. - **Layout:** the tab is a size container, so its rows wrap to the agent side panel's width. **Try it** (`just dev`) 1. Open Agents, pick a local agent, then **⋯ → Make my bestie**. 2. Open that agent: it has a **Bestie** tab saying "This is your Bestie". Other agents have none. 3. Click **Change** and pick another agent. After the confirm dialog, the tab moves to the agent you picked. 4. Narrow the side panel: the rows wrap and nothing scrolls sideways. **Stack:** each PR is based on the one before it: **#2034 Bestie tab (this PR)** → #2035 Remembers you → #2036 private channels → #2037 check-ins → #2039 Former Bestie → #2041 Remove as Bestie → #2047 Choose your bestie. #2040 (recent notes in Claude Code prompts) is separate and based on main. **Overlap:** #1716 also changes `features/agents/service.ts` and `tabs.tsx`. Whichever lands second will rebase onto the other. 🤖 Generated with [Claude Code](https://claude.com/claude-code) buzz-review-completed --------- Signed-off-by: Riley Crane <[email protected]> Co-authored-by: Opus 5.5 Max <d9fc2aa400ad81a2f289921a1720811e5d922c43e079f37241d4be458d8d92aa@buzz.block.builderlab.xyz> Co-authored-by: Claude Opus 5.5 <[email protected]> Co-authored-by: eggboy <3986e5374b8c11a274f4a53fa9a68e1e18b53e712f8aa5218add2485bf5a2b09@buzz.block.builderlab.xyz>
The Bestie tab gets **Remembers you**: a switch that adds memory rules to the agent's instructions, and removes them when turned off. **Why it works this way:** the memory rules are a marked, versioned section of the agent's own instructions, so the owner can read and edit them, and later features can add sections the same way. **What changes** - **On** adds a section marked `<!-- buzz:bestie/memory v1 -->` … `<!-- /buzz:bestie/memory -->` to the instructions. **Off** removes it. No memory is deleted: core memory is still in every prompt, and the Memory tab still clears memory. - **The switch follows the markers.** It stays on through the owner's edits inside the section. Off keeps any text the owner put before or after the markers. - **Edited markers.** If a marker is edited, removed or copied, the row says the section was edited instead of showing a switch, so it never adds a second section. **Restore** puts the original back when the markers still show where the section is. - **Feature list.** `features.ts` holds the feature list. Memory is its first entry; check-ins (#2037) add one more. **Design decisions** - On and Off save the agent's instructions through `agents.save`; the next turn uses them. Rejected: a hidden section the host adds to every prompt. - An edited section blocks On rather than guessing. Rejected: rewriting the owner's edits. **Try it** (`just dev`, with a local agent that is your Bestie) 1. Open its **Bestie** tab and turn **Remembers you** on. Open **Instructions**: the marked memory section is at the end. 2. Type a line inside the section and save. The switch stays on. 3. Turn it off. The section is gone, and the text you wrote outside it stays. 4. Delete one marker in Instructions. The row says the section was edited, with **Restore**. Click **Restore** to put the section back. **Stack:** each PR is based on the one before it: #2034 Bestie tab → **#2035 Remembers you (this PR)** → #2036 private channels → #2037 check-ins → #2039 Former Bestie → #2041 Remove as Bestie → #2047 Choose your bestie. #2040 (recent notes in Claude Code prompts) is separate and based on main. **Overlap:** #1716 also changes `features/agents/service.ts` and `tabs.tsx`. Whichever lands second will rebase onto the other. 🤖 Generated with [Claude Code](https://claude.com/claude-code) buzz-review-completed --------- Signed-off-by: Riley Crane <[email protected]> Co-authored-by: eggboy <3986e5374b8c11a274f4a53fa9a68e1e18b53e712f8aa5218add2485bf5a2b09@buzz.block.builderlab.xyz> Co-authored-by: Claude Opus 5.5 <[email protected]>
e10c4d5 to
a0e2101
Compare
wesbillman
left a comment
There was a problem hiding this comment.
🤖 Star Lord’s automated source follow-up via Wes’s account, integrating independent Rocket, Mantis and Groot lanes.
The two earlier check-in reports are addressed in their original scenarios, but the rollback introduces one P2: failed setup while Watching is disabled can clear the agent’s previously enabled saved Attention switch. Details and a regression scenario are inline. The author’s deferred instruction-first ordering preference remains optional; this finding concerns preserving the existing saved setting.
Reviewed head c2c4e76c476d673849d528ae2cceb06bc4311d21 against base a0e210151e1e16735bbea1158fde0b51578ce262, focusing on the check-in repairs since 0ae322732ddc6cb87733dbd22e2ea9d2b204fbec and their current save/scheduler/UI consumers. Source-only: no tests, builds, app execution or CI assessment. Native wake delivery, performance, rendered keyboard focus through pending/error/retry and human acceptance remain unverified. Public description and conversation attachments were checked; no images were attached. This COMMENT is not approval or merge authorization.
| agent.attention[slug]?.value ?? null, | ||
| ]), | ||
| ), | ||
| ...(!agent.attentionEnabled && { attentionEnabled: false }), |
There was a problem hiding this comment.
🤖 P2 — Preserve the saved Attention switch during rollback. agent.attentionEnabled is an effective view value: record.attentionEnabled === true && this.attentionShown() (service.ts:4679–4680). With the agent’s saved switch already on but the Watching plugin off, it reads false. If the owner enables Every hour and its instruction save rejects/conflicts, this rollback writes attentionEnabled: false; changed() at service.ts:1282–1284 removes the saved true. Re-enabling Watching then leaves the agent’s other watches and timers stopped. This breaks the documented preservation of that switch while Watching is disabled (docs/agents.md:77, docs/plugin-architecture.md:336–340).
Base compensation on the actual saved state and what this operation changed, rather than the masked view value. Cover stored Attention=true → Watching off → failed check-in enable → Watching on, asserting the stored true survives and unrelated watches can resume. The current mocked false/true cases do not distinguish saved state from plugin masking. This path was traced from source, not executed.
The Bestie tab lists the private channels you share with Bestie. From
the tab you can add Bestie to one channel or to all your private
channels, and remove it from any of them.
**Why only private channels:** agents can already read public channels,
so only private channels need Bestie added.
**What changes**
- **Private channels Bestie can read:** each channel is listed with
**Remove**, which asks first, as the Members dialog does.
- **Add to a channel** (**Choose…**): a picker of your private channels
that don't have Bestie yet, most recently active first.
- **Add to all my private channels:** adds Bestie to every private
channel you're in that doesn't have it.
- Adds and removes are the same NIP-29 membership events the Members
dialog sends (kinds 9000/9001). After each one, the tab reads the roster
again to confirm. Remove and the config channels share one helper for
this, `administerMember` in `channel-members/administration.ts`.
- **Bestie joins as a guest**, so it reads the channel but can't post
there. `memberAdditions.add` takes an optional `{ role: "guest" }` for
this; every other addition keeps the role it had.
- **The browser host signs guest invitations**: its kind 9000 allowlist
now accepts `role=guest`, as well as no role or `role=member`. Owner,
admin and bot stay out of it.
- **An addition finishes only with the role it asked for** (shared with
the Members dialog and Outbox): an add already running, or a saved
receipt for the same channel and person, is reused only for the same
role. Otherwise the add stops with an error that says to retry or remove
the receipt in Outbox. No second write competes with it, and the receipt
stays. So a Members-dialog add no longer sends a new write past a saved
guest receipt, and Outbox **Retry** retries a guest addition as a guest.
- **Until the channel list has loaded in full**, the section says so
instead of saying Bestie is in none. A failed read shows **Retry**,
which reads the list again. The channels already known keep their rows,
and **Add to all** waits for the full list. A channel known only from a
cached list offers no Add or Remove until it is read again.
- **Keyboard focus stays in the section** when its control leaves: after
a Remove drops its row, or the last add takes **Choose…** and **Add to
all** with it, focus moves to the control now in its place, or to the
section heading. Focus you already moved elsewhere stays there.
- Each action has its own row, so in a narrow panel its button wraps
under the label.
- **Changing your Bestie stops an Add to all under way**, so the old
Bestie isn't added to the channels left.
**Try it** (`just dev`, with a local Bestie and two private channels it
isn't in)
1. Open the **Bestie** tab. Under **Private channels Bestie can read**,
click **Add to a channel → Choose…** and pick one channel. That channel
appears in the list, and its timeline shows that you added Bestie.
2. Click **Add to all**. The other channel is added too.
3. Click **Remove** on one channel and confirm. It leaves the list. With
the keyboard, focus moves to the next row.
**Stack:** each PR is based on the one before it: #2034 Bestie tab →
#2035 Remembers you → **#2036 private channels (this PR)** → #2037
check-ins → #2039 Former Bestie → #2041 Remove as Bestie → #2047 Choose
your bestie. #2040 (recent notes in Claude Code prompts) is separate and
based on main.
**Overlap:** #1716 also changes `features/agents/service.ts` and
`tabs.tsx`. Whichever lands second will rebase onto the other.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
buzz-review-completed
---------
Signed-off-by: Riley Crane <[email protected]>
Co-authored-by: eggboy <3986e5374b8c11a274f4a53fa9a68e1e18b53e712f8aa5218add2485bf5a2b09@buzz.block.builderlab.xyz>
Co-authored-by: Claude Opus 5.5 <[email protected]>
A Check-ins group under Remembers you has one row, Every hour, off by default and unavailable while memory is off. On adds the Hourly upkeep section to the agent's instructions, an Interest and an hourly Attention timer delivered to the owner's 1:1 DM with the agent, and turns the agent's Attention on, all in one save. Off, and turning memory off, remove the section and the timer and leave Attention as it is. The Bestie plugin now controls Attention, so the timer runs with Watching off. The upkeep text is the Hourly upkeep of prompt.ts, woken by the timer and using the local tools. Features can require another and carry a timer, so another check-in kind is one more entry in the feature list. A service test shows that an hourly timer runs once after the computer sleeps through several hours, then an hour later. Co-authored-by: eggboy <3986e5374b8c11a274f4a53fa9a68e1e18b53e712f8aa5218add2485bf5a2b09@buzz.block.builderlab.xyz> Co-Authored-By: Claude Opus 5.5 <[email protected]> Signed-off-by: Riley Crane <[email protected]>
Save the timer before the instructions, so a failed second save leaves a state that reads as off and toggles cleanly. Let one save run at a time across the tab's switches, keep a check-in left on without memory switchable off, and stop the upkeep steps from treating upkeep wakes as messages to ignore. Co-authored-by: eggboy <3986e5374b8c11a274f4a53fa9a68e1e18b53e712f8aa5218add2485bf5a2b09@buzz.block.builderlab.xyz> Co-Authored-By: Claude Opus 5.5 <[email protected]> Signed-off-by: Riley Crane <[email protected]>
Bestie no longer calls controlAttention(), which turned Attention on for every agent while Watching was off. Check-ins now run while the Watching plugin is on, and the row says it is paused while watches aren't running. Co-authored-by: eggboy <3986e5374b8c11a274f4a53fa9a68e1e18b53e712f8aa5218add2485bf5a2b09@buzz.block.builderlab.xyz> Co-Authored-By: Claude Opus 5.5 <[email protected]> Signed-off-by: Riley Crane <[email protected]>
…ttention Turning Every hour on saves the timer and turns on the agent's Attention before the instructions. If the instructions then failed to save, the timer kept running hourly behind a switch that read off. On now undoes what it saved: the timer and its Interest go back to how they were, and Attention goes back off only if this save turned it on. If that undo fails too, the error says so and a Remove row takes the timer away. A timer paused on its own in the Attention tab still read as running. The row now says it is paused there and how to restart it, and Off still removes it. Co-authored-by: eggboy <3986e5374b8c11a274f4a53fa9a68e1e18b53e712f8aa5218add2485bf5a2b09@buzz.block.builderlab.xyz> Co-Authored-By: Claude Opus 5.5 <[email protected]> Signed-off-by: Riley Crane <[email protected]>
…heck-in The hourly check-in now also reads what is new in the private channels Bestie was added to, tracked like the DM, so it reads along there and reports in the DM. Co-authored-by: eggboy <3986e5374b8c11a274f4a53fa9a68e1e18b53e712f8aa5218add2485bf5a2b09@buzz.block.builderlab.xyz> Co-Authored-By: Claude Opus 5.5 <[email protected]> Signed-off-by: Riley Crane <[email protected]>
c2c4e76 to
2121eb8
Compare
* commit '05fba2455': feat(agents): show each session's system prompt and dividers in activity (#1855) feat(bestie): hourly check-ins from your Bestie (#2037) Authorize local apps to use Buzz MCP tools (#2211) feat(bestie): choose the private channels Bestie can read (#2036) Polish message actions, spacing, and membership notices (#2258) test(unread): prevent startup catch-up in receipt reserve journey (#2274) test(search): wait for rejected join clearance to settle (#2278) test(reactions): provide stable emoji media for icon rendering (#2279) feat(profiles): reserve profile room for an open channel's roster (#2269) Signed-off-by: Daniel Rouleau <[email protected]> Co-authored-by: Codex <[email protected]> # Conflicts: # src/bundled/codex/runtime.ts
…ie tab (#2039) When you change your Bestie, the old one stops checking in, and its other settings stay where you can reach them in a **Former Bestie** tab. **Why:** only one agent should check in on you. A former Bestie's other settings (memory, channels) are still on, so they stay reachable in their own tab instead of becoming invisible. **What changes** - **Only your Bestie checks in.** Whenever the saved Bestie is known, every other local agent has its check-in section and timer removed. Its memory section and channels stay. A choice made on another device counts once this device next reads the account, once per connection. - **Former Bestie tab:** it shows "<name> is no longer your Bestie. These are still on.", the same switches, **Remove from all** for its private channels, and **Make my Bestie again**. The tab shows while the agent has a Bestie section or, if this device saw it as Bestie, while it is in one of your private channels. - **New Bestie starts remembering.** An agent this device makes Bestie starts with Remembers you on (the plugin hears `onBestieMade`). A choice synced from another device changes nothing, so memory turned off stays off. - **Tests:** a browser layout check (`tests/browser`) covers the Bestie and Former Bestie tabs at two panel widths and two text sizes. **Design decisions** - Cleanup acts only on a Bestie the account has saved. While the account is still being read, nothing is removed. - Turning the Bestie plugin off stops a cleanup already under way: it saves nothing more, not even the instructions after a timer save, and starts no further pass. - Former Bestie is a second tab (`buzz.bestie/former-bestie`), not a mode of the Bestie tab, so each tab's `when` stays simple. **Try it** (`just dev`, with two local agents, Ada as Bestie with Remembers you and Every hour on) 1. In Ada's **Bestie** tab, **Change** to Bea and confirm. 2. Ada now has a **Former Bestie** tab: Remembers you is still on, Every hour is off, and its timer is gone from Watching. 3. Bea has the **Bestie** tab, with Remembers you on. 4. In Ada's Former Bestie tab, click **Make my Bestie again** and confirm. Ada has the Bestie tab again. **Stack:** each PR is based on the one before it: #2034 Bestie tab → #2035 Remembers you → #2036 private channels → #2037 check-ins → **#2039 Former Bestie (this PR)** → #2041 Remove as Bestie → #2047 Choose your bestie. #2040 (recent notes in Claude Code prompts) is separate and based on main. **Overlap:** #1716 also changes `features/agents/service.ts` and `tabs.tsx`. Whichever lands second will rebase onto the other. 🤖 Generated with [Claude Code](https://claude.com/claude-code) buzz-review-completed --------- Signed-off-by: Riley Crane <[email protected]> Co-authored-by: eggboy <3986e5374b8c11a274f4a53fa9a68e1e18b53e712f8aa5218add2485bf5a2b09@buzz.block.builderlab.xyz> Co-authored-by: Claude Opus 5.5 <[email protected]>
…er a warning (#2041) You can **Remove** your Bestie, so the account saves none. Once the account is read to have saved none, check-ins stop on every local agent. A remote agent, hosted or on another device, can now be your Bestie from every place that offers one, after the same warning the empty-state chooser shows. Check-ins stop when you save no Bestie, just as they do when you change it (#2039). An account that never chose a Bestie changes nothing. > **The `main-agent` record** (kind 30078, `d=main-agent`, added in #1665). Saving `agent: null`, the native signer's admission of it, and the conditional clear after deletion are already on main (#2173). This PR changes only the reader: it returns `null` for a record of none and `undefined` when there is no record, where before it read both as no choice. It also writes a record of none on an account that has no record yet, though that matches the empty value, so other devices read it as a choice. The record shape is unchanged. **What changes** - **Remove as Bestie.** A row on the Bestie tab under Change, with **Remove**. It asks first: "Remove Ada as your Bestie?" The dialog's focus starts on Cancel. If the save fails, the error shows in the dialog and the Bestie stays. There is no Remove without an account (device-only session), because the composer would pick another agent at once. Remove uses main's `clearDefaultAgent`. - **Cleanup with no Bestie.** Once the account is read to have saved none, every local agent's check-in is stopped. An account with no record stops nothing. While the read is pending or has failed, or while a choice made here is still saving, nothing is stopped either. The Former Bestie tab follows the same rule. - **Remote agents can be Bestie.** Change, Agents → **Make my bestie** and the Former tab's **Make my Bestie again** offer every eligible agent, remote ones included. Before a remote agent is saved, they ask with the empty-state chooser's own confirmation, now one shared `RemoteBestieConfirm`: "Use Sky as your bestie? Some bestie features aren't available with remote agents." Cancel saves nothing. When a remote agent would replace a saved Bestie, the replacement dialog asks once and adds that sentence. A remote Bestie's switches show "Not available with remote agents yet.", and it doesn't start with Remembers you on. Check-in cleanup still stops only agents on this device, so changing from a local Bestie to a remote one still stops the local agent's check-ins. **Design decisions** - **A saved none is not the same as no record.** Only a saved none stops check-ins. An agent here may be Bestie in another community that shares its instructions, so an account that never chose one leaves them alone. The saved choice keeps this difference as `recorded()` next to `snapshot()`. Rejected: a new record field, because the record is shared with Solo and its reader rejects unknown keys. - **The choice is known (`known()`) only after a successful read made before any choice here, or after a successful save.** It is never known while a save is pending. This gates both the cleanup and the Former tab. Both are set by one `save()` wrapper around the account write, which `chooseDefaultAgent` and main's `clearDefaultAgent` use. - **One confirmation path for remote agents.** The empty-state chooser and the Bestie actions render the same component and string, so the warning can't drift. Rejected: two dialogs in a row when a remote agent replaces a Bestie. - **Switches off for remote agents.** A hosted agent's instructions live with its provider, and an agent on another device keeps its timers there, so a switch here would claim a change it can't make. <details><summary>Reader diff</summary> ```diff diff --git a/src/features/relay/native-sidebar.ts b/src/features/relay/native-sidebar.ts index 9a2ae20..c771725 100644 --- a/src/features/relay/native-sidebar.ts +++ b/src/features/relay/native-sidebar.ts @@ -150,7 +150,12 @@ export function nativeSidebar(transport: ReadTransport) { ): Promise<T> { const current = await head(coordinate, signal); const draft = prepare(current.payload, current.createdAt); - if (same(current.payload, draft.next)) return draft.result; + // A saved main agent of none matches the empty value but is still a + // record, unlike none at all, so it is written even then. + const unchanged = + same(current.payload, draft.next) && + (current.createdAt > 0 || coordinate !== "main-agent"); + if (unchanged) return draft.result; // Reject unsupported/over-budget writes at the existing projection boundary. if (coordinate === "main-agent") mainAgent(draft.next); else project(coordinate, draft.next); @@ -180,7 +185,10 @@ export function nativeSidebar(transport: ReadTransport) { decodeSidebarPreferences: decode, async readMainAgent(signal: AbortSignal) { const current = await head("main-agent", signal); - return current.createdAt ? mainAgent(current.payload) : undefined; + // A record of none reads as null, apart from no record at all. + return current.createdAt + ? (mainAgent(current.payload) ?? null) + : undefined; }, async writeMainAgent(agent: string | null, signal: AbortSignal) { if (agent !== null && !agentKey.test(agent)) diff --git a/src/features/relay/transport.ts b/src/features/relay/transport.ts index ff1b874..38fd1b8 100644 --- a/src/features/relay/transport.ts +++ b/src/features/relay/transport.ts @@ -165,8 +165,9 @@ export interface ReadTransport { readonly renameSidebarSection?: SidebarSectionRenameMutator; readonly writeSidebarStar?: SidebarStarMutator; /** Host-only private account record of the main agent the viewer chose: - * undefined when never saved or explicitly cleared. */ - readMainAgent?(signal: AbortSignal): Promise<string | undefined>; + * null when they saved none or it was cleared, undefined when there is no + * record. */ + readMainAgent?(signal: AbortSignal): Promise<string | null | undefined>; /** Null saves that this account has no main agent. */ writeMainAgent?(agent: string | null, signal: AbortSignal): Promise<void>; /** Clears only the expected choice and returns the resulting choice. */ ``` </details> **Try it** (`just dev`, local agent Ada as Bestie with Remembers you and Every hour on, and a hosted agent Sky if you have one) 1. In Ada's **Bestie** tab, click **Remove** under Remove as Bestie. The dialog opens with focus on Cancel. Click **Remove as Bestie**. 2. Ada's Bestie tab is gone and a **Former Bestie** tab takes its place. Remembers you is still on, Every hour is off, and the private channels are unchanged. 3. Click **Make my Bestie again**. Ada is Bestie again, with no confirmation, because there is no current Bestie to replace. 4. Open **Change** and pick Sky. One dialog asks to replace Ada and says some bestie features aren't available with remote agents. Switch. Sky's Bestie tab shows its switches as unavailable, and Ada's Every hour is off. 5. Remove Sky, then in Agents choose **Make my bestie** on Sky. The chooser's "Use Sky as your bestie?" dialog appears. Cancel leaves no Bestie. 6. Failure path: go offline, then Remove. The dialog says the removal couldn't be saved, and the Bestie stays. **Known gaps** - After Remove, the panel shows the agent's first tab rather than Former Bestie. - Another device keeps its old view of the Bestie until it next reads the account, once per connection. - With no Bestie, new chats in the composer fall back to an automatic pick on this device. That pick isn't saved to the account. - A remote agent made Bestie on another device of yours doesn't get Remembers you turned on there either; it is turned on only for a local agent, on the device that made it Bestie. **Stack:** each PR is based on the one before it: #2034 Bestie tab → #2035 Remembers you → #2036 private channels → #2037 check-ins → #2039 Former Bestie → **#2041 Remove as Bestie (this PR)**. #2040 (recent notes in Claude Code prompts) is separate and based on main. **Overlap:** #1716 also changes `features/agents/service.ts` and `tabs.tsx`. Whichever lands second will rebase onto the other. 🤖 Generated with [Claude Code](https://claude.com/claude-code) buzz-review-completed --------- Signed-off-by: Riley Crane <[email protected]> Co-authored-by: eggboy <3986e5374b8c11a274f4a53fa9a68e1e18b53e712f8aa5218add2485bf5a2b09@buzz.block.builderlab.xyz> Co-authored-by: Claude Opus 5.5 <[email protected]>
The Bestie tab lists the private channels you share with Bestie. From
the tab you can add Bestie to one channel or to all your private
channels, and remove it from any of them.
**Why only private channels:** agents can already read public channels,
so only private channels need Bestie added.
**What changes**
- **Private channels Bestie can read:** each channel is listed with
**Remove**, which asks first, as the Members dialog does.
- **Add to a channel** (**Choose…**): a picker of your private channels
that don't have Bestie yet, most recently active first.
- **Add to all my private channels:** adds Bestie to every private
channel you're in that doesn't have it.
- Adds and removes are the same NIP-29 membership events the Members
dialog sends (kinds 9000/9001). After each one, the tab reads the roster
again to confirm. Remove and the config channels share one helper for
this, `administerMember` in `channel-members/administration.ts`.
- **Bestie joins as a guest**, so it reads the channel but can't post
there. `memberAdditions.add` takes an optional `{ role: "guest" }` for
this; every other addition keeps the role it had.
- **The browser host signs guest invitations**: its kind 9000 allowlist
now accepts `role=guest`, as well as no role or `role=member`. Owner,
admin and bot stay out of it.
- **An addition finishes only with the role it asked for** (shared with
the Members dialog and Outbox): an add already running, or a saved
receipt for the same channel and person, is reused only for the same
role. Otherwise the add stops with an error that says to retry or remove
the receipt in Outbox. No second write competes with it, and the receipt
stays. So a Members-dialog add no longer sends a new write past a saved
guest receipt, and Outbox **Retry** retries a guest addition as a guest.
- **Until the channel list has loaded in full**, the section says so
instead of saying Bestie is in none. A failed read shows **Retry**,
which reads the list again. The channels already known keep their rows,
and **Add to all** waits for the full list. A channel known only from a
cached list offers no Add or Remove until it is read again.
- **Keyboard focus stays in the section** when its control leaves: after
a Remove drops its row, or the last add takes **Choose…** and **Add to
all** with it, focus moves to the control now in its place, or to the
section heading. Focus you already moved elsewhere stays there.
- Each action has its own row, so in a narrow panel its button wraps
under the label.
- **Changing your Bestie stops an Add to all under way**, so the old
Bestie isn't added to the channels left.
**Try it** (`just dev`, with a local Bestie and two private channels it
isn't in)
1. Open the **Bestie** tab. Under **Private channels Bestie can read**,
click **Add to a channel → Choose…** and pick one channel. That channel
appears in the list, and its timeline shows that you added Bestie.
2. Click **Add to all**. The other channel is added too.
3. Click **Remove** on one channel and confirm. It leaves the list. With
the keyboard, focus moves to the next row.
**Stack:** each PR is based on the one before it: block#2034 Bestie tab →
block#2035 Remembers you → **block#2036 private channels (this PR)** → block#2037
check-ins → block#2039 Former Bestie → block#2041 Remove as Bestie → block#2047 Choose
your bestie. block#2040 (recent notes in Claude Code prompts) is separate and
based on main.
**Overlap:** block#1716 also changes `features/agents/service.ts` and
`tabs.tsx`. Whichever lands second will rebase onto the other.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
buzz-review-completed
---------
Signed-off-by: Riley Crane <[email protected]>
Co-authored-by: eggboy <3986e5374b8c11a274f4a53fa9a68e1e18b53e712f8aa5218add2485bf5a2b09@buzz.block.builderlab.xyz>
Co-authored-by: Claude Opus 5.5 <[email protected]>
The Bestie tab gets **Check-ins → Every hour**. Turned on, it wakes your Bestie in your DM once an hour. **Why a local timer:** a Bestie on this device is woken by a timer on this device, using the same Attention timers as the Watching plugin. **What changes** - **Every hour** needs **Remembers you**. It is disabled while memory is off, and turning memory off turns it off too. - **On:** - Adds an Hourly upkeep section (`buzz:bestie/checkin.hourly`) to the instructions. Each check-in reads what's new in your DM and in the private channels Bestie was added to, and reports only in the DM. - Saves an hourly Attention timer, `watch/bestie-hourly-check-in`, through `agents.save`; the timer wakes Bestie in your DM. - Turns on the agent's Attention. - **Off** removes the section and the timer. It leaves Attention on, because the agent's other watches may need it. - The timer runs while Buzz is open and Watching is on. While it can't run, the row says **Paused**. A timer paused in the Attention tab, which can't resume it, says **Paused in Attention** and how to restart it; **Off** still works. After a sleep it runs once, not once for each missed hour. **Design decisions** - No `controlAttention()`. Turning Watching off still stops every agent's Attention, so Bestie doesn't take over a control the user can see. Rejected: having the plugin turn Attention on through `controlAttention()`. - The timer is saved first, then the instructions, so a failed save leaves no section without a timer. If the instructions fail to save, **On** undoes the timer, and turns Attention back off only if it turned it on. If that undo fails too, a row says the timer is still saved and offers **Remove**. **Try it** (`just dev`, local Bestie with Remembers you on) 1. Turn **Every hour** on. The Watching tab shows the timer `watch/bestie-hourly-check-in`, and Instructions has the Hourly upkeep section. 2. Turn **Remembers you** off. **Every hour** turns off and is disabled. 3. Turn memory back on, then check-ins on, then disable the Watching plugin. The row says it is paused. **Stack:** each PR is based on the one before it: block#2034 Bestie tab → block#2035 Remembers you → block#2036 private channels → **block#2037 check-ins (this PR)** → block#2039 Former Bestie → block#2041 Remove as Bestie → block#2047 Choose your bestie. block#2040 (recent notes in Claude Code prompts) is separate and based on main. **Overlap:** block#1716 also changes `features/agents/service.ts` and `tabs.tsx`. Whichever lands second will rebase onto the other. 🤖 Generated with [Claude Code](https://claude.com/claude-code) buzz-review-completed --------- Signed-off-by: Riley Crane <[email protected]> Co-authored-by: eggboy <3986e5374b8c11a274f4a53fa9a68e1e18b53e712f8aa5218add2485bf5a2b09@buzz.block.builderlab.xyz> Co-authored-by: Claude Opus 5.5 <[email protected]>
…ie tab (block#2039) When you change your Bestie, the old one stops checking in, and its other settings stay where you can reach them in a **Former Bestie** tab. **Why:** only one agent should check in on you. A former Bestie's other settings (memory, channels) are still on, so they stay reachable in their own tab instead of becoming invisible. **What changes** - **Only your Bestie checks in.** Whenever the saved Bestie is known, every other local agent has its check-in section and timer removed. Its memory section and channels stay. A choice made on another device counts once this device next reads the account, once per connection. - **Former Bestie tab:** it shows "<name> is no longer your Bestie. These are still on.", the same switches, **Remove from all** for its private channels, and **Make my Bestie again**. The tab shows while the agent has a Bestie section or, if this device saw it as Bestie, while it is in one of your private channels. - **New Bestie starts remembering.** An agent this device makes Bestie starts with Remembers you on (the plugin hears `onBestieMade`). A choice synced from another device changes nothing, so memory turned off stays off. - **Tests:** a browser layout check (`tests/browser`) covers the Bestie and Former Bestie tabs at two panel widths and two text sizes. **Design decisions** - Cleanup acts only on a Bestie the account has saved. While the account is still being read, nothing is removed. - Turning the Bestie plugin off stops a cleanup already under way: it saves nothing more, not even the instructions after a timer save, and starts no further pass. - Former Bestie is a second tab (`buzz.bestie/former-bestie`), not a mode of the Bestie tab, so each tab's `when` stays simple. **Try it** (`just dev`, with two local agents, Ada as Bestie with Remembers you and Every hour on) 1. In Ada's **Bestie** tab, **Change** to Bea and confirm. 2. Ada now has a **Former Bestie** tab: Remembers you is still on, Every hour is off, and its timer is gone from Watching. 3. Bea has the **Bestie** tab, with Remembers you on. 4. In Ada's Former Bestie tab, click **Make my Bestie again** and confirm. Ada has the Bestie tab again. **Stack:** each PR is based on the one before it: block#2034 Bestie tab → block#2035 Remembers you → block#2036 private channels → block#2037 check-ins → **block#2039 Former Bestie (this PR)** → block#2041 Remove as Bestie → block#2047 Choose your bestie. block#2040 (recent notes in Claude Code prompts) is separate and based on main. **Overlap:** block#1716 also changes `features/agents/service.ts` and `tabs.tsx`. Whichever lands second will rebase onto the other. 🤖 Generated with [Claude Code](https://claude.com/claude-code) buzz-review-completed --------- Signed-off-by: Riley Crane <[email protected]> Co-authored-by: eggboy <3986e5374b8c11a274f4a53fa9a68e1e18b53e712f8aa5218add2485bf5a2b09@buzz.block.builderlab.xyz> Co-authored-by: Claude Opus 5.5 <[email protected]>
The Bestie tab gets Check-ins → Every hour. Turned on, it wakes your Bestie in your DM once an hour.
Why a local timer: a Bestie on this device is woken by a timer on this device, using the same Attention timers as the Watching plugin.
What changes
buzz:bestie/checkin.hourly) to the instructions. Each check-in reads what's new in your DM and in the private channels Bestie was added to, and reports only in the DM.watch/bestie-hourly-check-in, throughagents.save; the timer wakes Bestie in your DM.Design decisions
controlAttention(). Turning Watching off still stops every agent's Attention, so Bestie doesn't take over a control the user can see. Rejected: having the plugin turn Attention on throughcontrolAttention().Try it (
just dev, local Bestie with Remembers you on)watch/bestie-hourly-check-in, and Instructions has the Hourly upkeep section.Stack: each PR is based on the one before it: #2034 Bestie tab → #2035 Remembers you → #2036 private channels → #2037 check-ins (this PR) → #2039 Former Bestie → #2041 Remove as Bestie → #2047 Choose your bestie. #2040 (recent notes in Claude Code prompts) is separate and based on main.
Overlap: #1716 also changes
features/agents/service.tsandtabs.tsx. Whichever lands second will rebase onto the other.🤖 Generated with Claude Code
buzz-review-completed