镜像站点 · 本页由第三方 GitHub 只读镜像提供,非 GitHub 官方站点,不接受任何登录或凭据输入。前往 github.com
Skip to content

feat(claude-code): start sessions with the agent's recent notes - #2040

Merged
rileycrane merged 2 commits into
mainfrom
rileycrane/bestie-l2b-recent-notes
Nov 2, 2026
Merged

rileycrane merged 2 commits into
mainfrom
rileycrane/bestie-l2b-recent-notes

Conversation

@rileycrane

@rileycrane rileycrane commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

A Claude Code agent now starts each session knowing what it wrote down today and yesterday. Its prompt carries those daily notes (mem/notes/<date>) next to core memory.

It is based on main and is independent of the Bestie tab stack (#2034 onward).

What changes

  • features/agent-harness/core-memory.ts gains a helper next to core memory. It reads yesterday's and today's notes by slug and keeps the newest 8 KB of each.
  • The Claude Code runtime adds a <recent-notes> block after core memory, once per process start.
  • A missing, blank or unread note adds nothing, and a start waits at most 2 seconds for the notes. After that it cancels the reads, so they don't hold up other agents' memory reads. A read another caller still waits for keeps running.
  • Dates follow the device's time zone, with UTC as the fallback.

Trade-offs

  • An agent's process start now does one relay read. Before, it did none: launch-boundary.test.ts loosens that invariant in three tests.
  • A long-running process keeps the notes it read at start until it restarts, including across midnight.
  • Claude Code only. Codex, goose, Pi and hosted agents are not covered.

Try it (just dev, with a local Claude Code agent)

  1. Ask the agent to save a note for today (mem/notes/<today>), for example "I like short answers".
  2. Restart the agent (or Buzz) and start a new chat. Ask it what today's note says. It answers without reading memory first.
  3. An agent with no daily notes behaves as before.

🤖 Generated with Claude Code

buzz-review-completed

A Claude Code session's prompt now carries yesterday's and today's
mem/notes/<date> after core memory, read by slug once per process start
and the newest 8 KB of each. A missing, blank or unread note adds
nothing, and a start waits at most 2 seconds for them.

Co-authored-by: eggboy <3986e5374b8c11a274f4a53fa9a68e1e18b53e712f8aa5218add2485bf5a2b09@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Riley Crane <rileycrane@squareup.com>

@kazemek21 kazemek21 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Kyle reviewed this PR and the Nexus review-tracker notes and approved with the non-blocking comment below.

Posted by Codex (gpt-6.1-sol) on behalf of kylez

// slow read starts it without them.
const [memory, notes] = await Promise.all([
this.core(agent),
recentNotesSection((slug) =>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[P2] Release startup note reads when their budget expires

The outer timeout stops startup waiting after two seconds, but this.memories() still permits the underlying read to run for ten seconds. Since owner-view memory reads share a serialized queue, a stalled startup note read can make another agent’s memory read time out after this session has already started. I reproduced a 2.5-second core read succeeding without these startup reads and timing out with them. Could the startup budget also cancel the underlying note work, without cancelling unrelated shared readers?

Posted by Codex (gpt-6.1-sol) on behalf of kylez

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

🤖 Fixed in f222439. The startup timeout now aborts the notes read itself. A read shared by several callers is cancelled only when its last caller stops waiting.

@wesbillman wesbillman left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🤖 Star Lord’s automated source review via Wes’s GitHub account, with independent Rocket, Mantis and Groot coverage.

The existing P2 about releasing startup note reads remains applicable at this head. The two-second wrapper stops waiting, but the underlying read can occupy the shared serialized memory queue for ten seconds. A different agent with no cached core can exhaust its five-second startup budget behind that optional work and start without its memory. Please address the existing comment’s cancellation/queue-isolation recommendation while preserving unrelated shared readers. I’m referencing that thread rather than adding a duplicate inline finding. No additional actionable defects found in the six-file PR diff.

Reviewed head 0b66b4b44e2275c6d6f8493fe63da0463889ac83, target base b5747efe32a7fc6df97714f8dd70dfe9a9fff907, merge-base 336e8a147dedf12034301ccaa57d471f119dd906. Inspected launch callers, memory isolation/lifetime, date and UTF-8 handling, changed test coverage, public description and commit material (no attached images).

Source-only: no tests, builds, app launches or live Claude/relay workflows executed. The hosted-check snapshot shows CI required passing; Windows native validation and browser flake check were skipped. Real startup latency, native acceptance and incremental test cost remain unverified. This is a non-blocking COMMENT review, not approval or merge authorization.

A session start waited at most two seconds for the agent's recent notes,
but the reads behind them kept running for up to ten, holding the shared
memory read queue. Another agent's memory read, queued behind them, could
time out even though the session had already started.

The start now passes its two-second budget into the read. Shared reads stop
once every caller waiting on them has stopped, so a stalled notes read
frees its place in the queue, while a read other callers still wait for is
left running.

Co-authored-by: eggboy <3986e5374b8c11a274f4a53fa9a68e1e18b53e712f8aa5218add2485bf5a2b09@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Riley Crane <rileycrane@squareup.com>
@rileycrane
rileycrane merged commit 036fc36 into main Nov 2, 2026
77 of 79 checks passed
@rileycrane
rileycrane deleted the rileycrane/bestie-l2b-recent-notes branch November 2, 2026 23:01
rileycrane added a commit that referenced this pull request Nov 2, 2026
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 <rileycrane@squareup.com>
Co-authored-by: Opus 5.5 Max <d9fc2aa400ad81a2f289921a1720811e5d922c43e079f37241d4be458d8d92aa@buzz.block.builderlab.xyz>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: eggboy <3986e5374b8c11a274f4a53fa9a68e1e18b53e712f8aa5218add2485bf5a2b09@buzz.block.builderlab.xyz>
dr0uleau added a commit that referenced this pull request Nov 2, 2026
Co-authored-by: Codex <noreply@openai.com>

* origin/main: (26 commits)
  AIDA-2336: Show when worktree folders are required (#2165)
  AIDA-2326: Reveal full emoji completion names (#2172)
  feat(agent-command): let local agents discover advertised plugin tools (#2110)
  feat(bestie): a Bestie tab on your Bestie's agent page (#2034)
  fix(relay): notify verified profile changes once per replay (#2209)
  feat(shortcuts): open Shortcuts settings with Mod+/ (#2022)
  Re-send live channel subscriptions on access grants, not every minute (#2207)
  perf(profiles): count verified namesakes once per publish (#2159)
  feat(claude-code): start sessions with the agent's recent notes (#2040)
  fix(relay): skip status, emoji, read-state and reminder re-reads a continuous replay covers (#2204)
  Support agent credentials through the macOS debug helper (#2174)
  refactor: centralize relay search debounce and require two characters (#2200)
  fix(relay): drop unused profile exemptions from config and agent reads (#2202)
  fix(relay): skip full profile and roster re-reads after a continuous reconnect (#2203)
  Notify when Solo chat agents finish (#2011)
  feat(mcp): start a Solo chat with channel_create (#2128)
  fix(relay): keep profiles when channel access ends (#2187)
  Wait for thread readiness before keyboard input (#2181)
  fix(builderlab): unify the development sign-in (#2109)
  feat(host): add byte requests, private saves and POST dispatch protection (#2084)
  ...

Signed-off-by: Daniel Rouleau <drouleau@squareup.com>
rileycrane added a commit that referenced this pull request Nov 2, 2026
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 <rileycrane@squareup.com>
Co-authored-by: eggboy <3986e5374b8c11a274f4a53fa9a68e1e18b53e712f8aa5218add2485bf5a2b09@buzz.block.builderlab.xyz>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
rileycrane added a commit that referenced this pull request Nov 3, 2026
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 <rileycrane@squareup.com>
Co-authored-by: eggboy <3986e5374b8c11a274f4a53fa9a68e1e18b53e712f8aa5218add2485bf5a2b09@buzz.block.builderlab.xyz>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
rileycrane added a commit that referenced this pull request Nov 3, 2026
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: #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.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 <rileycrane@squareup.com>
Co-authored-by: eggboy <3986e5374b8c11a274f4a53fa9a68e1e18b53e712f8aa5218add2485bf5a2b09@buzz.block.builderlab.xyz>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
rileycrane added a commit that referenced this pull request Nov 3, 2026
…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 <rileycrane@squareup.com>
Co-authored-by: eggboy <3986e5374b8c11a274f4a53fa9a68e1e18b53e712f8aa5218add2485bf5a2b09@buzz.block.builderlab.xyz>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
rileycrane added a commit that referenced this pull request Nov 4, 2026
…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 <rileycrane@squareup.com>
Co-authored-by: eggboy <3986e5374b8c11a274f4a53fa9a68e1e18b53e712f8aa5218add2485bf5a2b09@buzz.block.builderlab.xyz>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
arividar pushed a commit to minniai/minni-buzz-app that referenced this pull request Nov 4, 2026
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 <rileycrane@squareup.com>
Co-authored-by: eggboy <3986e5374b8c11a274f4a53fa9a68e1e18b53e712f8aa5218add2485bf5a2b09@buzz.block.builderlab.xyz>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
arividar pushed a commit to minniai/minni-buzz-app that referenced this pull request Nov 4, 2026
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 <rileycrane@squareup.com>
Co-authored-by: eggboy <3986e5374b8c11a274f4a53fa9a68e1e18b53e712f8aa5218add2485bf5a2b09@buzz.block.builderlab.xyz>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
arividar pushed a commit to minniai/minni-buzz-app that referenced this pull request Nov 4, 2026
…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 <rileycrane@squareup.com>
Co-authored-by: eggboy <3986e5374b8c11a274f4a53fa9a68e1e18b53e712f8aa5218add2485bf5a2b09@buzz.block.builderlab.xyz>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
shellz-n-stuff added a commit that referenced this pull request Nov 4, 2026
Based on main. The base fix
[#2044](#2044) and Pi's adapter
[#1877](#1877) have merged.
Companion plugin:
[squareup/buzz-plugin-security#2](squareup/buzz-plugin-security#2).

Protected Claude agents now pause native input and tool calls for the
agent's bound policy. Security reviews that use a Claude model run
tool-free and return a structured verdict. Both run Claude's pinned
release, and the host runs its own verified copy of it. Security reviews
also sign in apart from `~/.claude`, with their own Claude folder and
token.

### What this changes

- **Pinned, host-verified runtime.** The plugin declares Claude 2.1.305
as two macOS artifact processes, `claude-protected-arm64` and
`claude-protected-x64`.
- Each has the release's SHA-256 and the path `~/Library/Application
Support/Buzz/claude-code/2.1.305/claude`. The host expands `~/` under
the user's home.
- For a contained spawn, the host hashes the bytes from one no-follow
descriptor and copies them into its write-denied artifact store, then
runs that copy. A changed file in the download folder doesn't start, and
swapping it after the check changes nothing.
- The plugin picks the process from `uname -sm`. A wrong answer only
names a pin whose checksum then fails.
- Setup checks the pin by starting the host's copy contained, with the
same read-only Claude root as the real launch, and then stopping it. It
never runs the download folder's file outside the sandbox. A pruned
2.1.305 folder doesn't trigger a download while the store copy is valid.
- If the host refuses its copy, setup installs the pinned release
through `claude.sh` and checks again the same way. A failed install step
reports "could not be installed"; a copy the host still refuses surfaces
the host's own reason. The host, not `claude.sh`, verifies and starts
protected Claude. `claude.sh` only installs the pinned release on
request, checking its SHA-256, and keeps 2.1.305 when it prunes.
- A caller waiting for that download stops waiting when its own signal
aborts, and the shared download carries on for later callers.
- **Protected sessions.** Hooks pause native input, tool actions and raw
output for the policy. The native watchdog (350 s) stops a stalled
callback before the CLI's 600 s hook timeout.
- Exact tool arguments and unknown evidence are kept, and prompt and
history representations map to stable message sources.
  - Sessions retire before protection changes.
- An entry made before `sync`, by `session`, `run` or `runOnce`, is
already marked protected, so the "no live policy binding" backstop
applies to it.
- `AgentSessions` tracks every process it starts, from the request to
its exit, including a spare it took. `shutdown()` kills each one and
waits for it, even one still starting. A pool that has shut down starts
nothing.
- A session subscriber that throws is logged and can't stop the others,
so it can no longer break a shutdown before every process is killed and
waited for.
- A failed retirement is dropped rather than kept, so the agent can
start again.
- When the host's hook guard stops a worker for an invalid or oversized
frame, it writes the reason to stderr, so the turn's error names it.
- After a guarded worker exits, the host still delivers its last output,
such as its final `result` or error. Only the guard's timers stop, and
input to an exited worker is still refused.
- **Security reviews.** Reviews run the pinned copy, contained and with
no tools. Trusted instructions are kept apart from evidence. The output
schema goes to native `--json-schema`, and the adapter requires
`structured_output`, so formatted prose can't pass as a verdict.
- Reviews, like protected agents, pass `--setting-sources ""`, so Claude
reads no user, project or local settings file. A contained worker can't
redirect reviews through `env` (such as `ANTHROPIC_BASE_URL`) or
`apiKeyHelper` in `~/.claude/settings.json`.
- **Own folder and sign-in** (Alex's idea of giving reviewers
credentials that contained workers can't reach). Reviews run with
`CLAUDE_CONFIG_DIR` set to `~/Library/Application
Support/Buzz/claude-code-review-home`, and sign in with a `claude
setup-token` token in `CLAUDE_CODE_OAUTH_TOKEN`. So a review reads
nothing from `~/.claude`: no `.credentials.json` and no cached
`remote-settings.json`. The companion creates that folder at 0700 and
lists it in `hiddenPaths`, so contained workers can neither read nor
write it.
- **Setting it up.** The person runs `claude setup-token` in Terminal
and pastes the token under **Sign in for reviews** in **Settings → Agent
setup → Harness defaults → Claude → Edit configuration**. A new
`claude.sh review-token` step reads it from stdin and prints only fixed
lines, so the token never appears in argv, step output or the setup log.
It refuses a folder that is a link or not a folder, and writes `token`
at 0600 through an exclusive temp file and `mv -h`. A `review` step
reads the folder and token back for the reviewer, skips a token that is
a link, and off macOS fails with "Claude security reviews need macOS".
Closing the dialog during a save cancels it, as Cancel does.
- **Fail closed.** With no saved token, a review fails before the
download or any model starts. Clear auth failures
(`authentication_error`, or an expired or revoked OAuth token) map to
one message that also says a Claude subscription is needed.
- **Env hygiene.** The review env nulls `ANTHROPIC_AUTH_TOKEN`,
`ANTHROPIC_BASE_URL`, `CLAUDE_CODE_API_BASE_URL`, `CLAUDE_CODE_SIMPLE`,
`ANTHROPIC_UNIX_SOCKET`, `CLAUDE_CODE_REMOTE_SESSION_ID` and the
`CLAUDE_CODE_USE_*` routes.
- A review's 60 s budget now covers reading its sign-in and waiting for
the pinned release's first download. A longer download fails the review
closed and carries on for later reviews.
- **File reads.** Buzz file tools and the transcript paths a worker
chooses use the app-bundled reader under the captured policy. It accepts
`~/…`, passes the filename as its own argument, and never falls back to
an ordinary reader.
- **Codex launcher fixes that are reachable from here.** `codex.sh`
`run` and `adopt` already run uncontained at this point in the stack, so
two link-following fixes land here rather than in #1879:
- The sign-in handover moves Buzz's sign-in file onto the shared
`auth.json` in one rename (`mv -f -h` on macOS). It no longer reads or
writes through a link, so a worker can't use it to overwrite or copy out
a protected file. A user's own link at `~/.codex/auth.json` is now
replaced by a regular file, as `codex login` does; the Codex README says
so.
- `adopt` skips a saved thread that is a link, creates its copy as a new
file (`set -C`, keeping the time with `touch -r`), and on macOS
publishes it with `ln -P`. A link planted at the temporary name makes
adopt stop without touching the target, and the thread starts new.
- **CI.** `install-protected-claude-test.mjs` checks that both pins
exist in the plugin manifest and match the release manifest before it
checks the download.

### Known gaps and tradeoffs

- Protected agents and Claude-model reviews need a release build of the
macOS desktop app. In dev builds and on other platforms they fail
closed.
- Claude-model reviews sign in only with their own `claude setup-token`
token. That needs a Claude Pro, Max, Team or Enterprise subscription and
lasts a year. API-key, gateway, Bedrock and Vertex setups can't run
Claude-model reviews; choose another review model.
- **Existing users must sign in for reviews once.** Until the review
sign-in is saved, and after it expires or is revoked, Claude-model
reviews fail closed.
- **Release coupling.** Ship the companion change
(squareup/buzz-plugin-security#2) in the same Buzz release that carries
both this PR and [#839](#839). The
companion lists protected paths that may not exist yet, and a host
without #839 refuses a protected launch when a listed path is missing.
Without the companion change, the review folder is not hidden from
contained workers.
- **Process environment.** While a review runs, another process of the
user can read the review token from that review's process environment.
Whether contained agents can read the pinned binary's environment this
way (`KERN_PROCARGS2`) has not been checked. Closing this needs a new
host channel for the token, so it is documented instead.
- **Protected runs still use `~/.claude`.** The review folder applies
only to security reviews. Another contained worker can plant a sign-in,
or cached managed settings that carry hooks and env, in `~/.claude`, and
they reach a protected run's checks. Transcripts in `~/.claude/projects`
can also be swapped between a run's checks or between the CLI's
`--resume` load and Buzz's read for the review. Moving protected runs to
a Buzz-owned folder would also move their sign-in, cached managed
settings and session history, so it is a separate change. Both gaps are
in `agent-protection.md`.
- The review env's null list was read from 2.1.312's strings and is not
yet checked against 2.1.305.
- Review model choices come from the Claude in use, but reviews run
2.1.305. An alias such as `opus` resolves inside 2.1.305, and a model
newer than it may be unknown to it.
- A symlinked home directory, or any symlinked component of the artifact
path, fails closed. Reinstalling doesn't help, because the install
writes to the same path.
- A spare still being handed over when a pool shuts down is killed as
soon as it arrives, but not waited for. Spares serve only unprotected
launches.
- The first protected launch copies the release into the artifact store:
about 273 MB on Apple silicon, 284 MB on Intel. Each review still hashes
the release twice (the contained check, then the launch).
- Older Buzz builds prune 2.1.305 from the shared download folder, so
switching between builds can download it again.
- Pi's protected runtime does not verify its runtime bytes at launch: it
runs the shared host file and Node, made read-only for the worker,
without hashing or copying them as this PR now does for Claude. Fixing
it is a follow-up.
- Planting a program on `PATH` stays outside the documented boundary.
- A tool result Claude echoes over the 4 MiB frame cap, such as a large
PDF or image, stops the turn closed, now with a reason.
- Codex `adopt` keeps small residuals that bash can't close: a rollout
swapped for a link between the check and the read can still be copied,
linked folders along the path can place a new `rollout-*.jsonl`
elsewhere, and noclobber still writes through a link to a FIFO or
device. The handover guards only the last path part, so a linked
`~/.codex` still receives `auth.json` in the folder it points to.
- **Size.** The Claude review sign-in is about 195 production lines,
against a checkpoint budget of about 55. The author chose to keep it as
is.
- Follow-ups not in this PR, tracked in [the follow-up
issue](#2334) with the
protected-run `CLAUDE_CONFIG_DIR` and Pi's runtime bytes:
- a per-hook TypeScript deadline below the native 350 s, which would
make a stalled review's error clearer;
- removing the duplication in `protectedEvents`, and the O(n) source
checks per hook;
- a wall-clock hook deadline. Tokio's `Instant` stops during macOS
sleep. If the CLI's hook timer doesn't stop, a laptop that sleeps
through a pending `PostToolUse` review could let the CLI time out first,
and that hook fails open;
- caching the artifact snapshot's validation in the host, so a review
doesn't hash the release twice;
- carrying the Buzz destination explicitly in natively gated tool
reviews, across Claude, Codex and Goose;
- the shared 256 KiB action cap, which a long protected Claude
conversation can reach.

### Validation

Restacked onto main `066e34f12` with the round-2 fixes: now `4564d2b68`
(stack tip `b85084bff`). Main's saved session prompts
([#1855](#1855)) overlapped the
Claude runtime's session store: both are kept, the store keyed by
protection state and main's `started` prompt save. At the stack tip
(`b85084bff`): `pnpm check` (Biome, `tsc`, design checks), the
integration suite (248 passed, 1 skipped), the full Vitest run (16,072
passed, 22 skipped), `cargo test -p buzzodz-plugins` (64), `cargo test
--lib` in `src-tauri` (841 passed, 2 ignored), Clippy (`-D warnings`)
and `cargo fmt --check` pass. One `src-tauri` test was left out:
`window_state::tests::onboarding_snapshot_is_kept_until_next_launch_restore`
crashes (SIGSEGV) on main as well (seen at `7cb3cb2c1`), and this stack
doesn't change `window_state`. The round-2 checks below ran on the
round-2 changes over `0caf2fd59`, before the restack.

Round 2, on the changes over `0caf2fd59`:

- `vitest run src/bundled/claude-code`: 316 passed, 4 skipped. `vitest
run src/bundled/codex`: 297 passed, 2 skipped. `tsc --noEmit` and `biome
check` on the changed files are clean. `bash -n` and `shellcheck -s
bash` are clean for `claude.sh`; `codex.sh` has only the existing
SC2174.
- `cargo test --lib host_process`: 34 passed. Clippy (`-D warnings`) and
`cargo fmt --check` are clean.
- **New tests, each seen failing on the previous code:**
- the pinned check starts the copy contained, never the download
folder's file;
- a cancelled wait for the pinned copy ends, during the check or the
download, while one download serves the next caller;
  - a review's 60 s budget fires during a held install;
  - a failed install step is reported without checking the copy again;
- a subscriber that throws during retirement no longer stops the worker
being killed;
- a guarded worker's last frame reaches the page after it exits, and the
guard drains an exited worker's output but refuses its input;
- a review without its own sign-in fails closed before any model; a
review signs in with its own folder and token, over other sign-ins; only
a refused sign-in maps to the sign-in message; the save step gets the
token on stdin alone;
- the review sign-in field saves through stdin, keeps its result,
cancels a save from the footer and when the dialog closes, and keeps the
field and Save focusable while a save is pending;
- `claude.sh` saves the review sign-in privately, refuses a linked or
non-folder home and a non-Claude token without printing it, and reads no
sign-in through a link;
- `codex.sh` never writes a handed-over sign-in through a link to a file
or into a linked folder; adopt never copies a linked thread, never
writes through a planted temporary link, and never hard-links what a
swapped-in link points to.
- **CI gap.** The `claude-sh` and `codex-sh` shell tests that depend on
macOS are skipped elsewhere. CI's Vitest job runs on Ubuntu, so CI does
not run them; they ran only locally on macOS.
- **UI check.** Before the last polish, the real Claude configuration
dialog body ran in Chromium through the browser test server: both
themes, 390 px and 1280 px, root text at 200%, keyboard and pointer.
That check found focus loss while a save was pending, and a dialog close
that didn't cancel a save. Both are fixed and covered by the tests
above, but the fixed version was not rendered again.
- **Agent review:** independent reviews of the round-2 changes scored
them 9 or higher with no blockers.

Earlier rounds, at `29828e405` (stack tip `6b514fff4`), on main
`6dc462a2d`, with later rebases onto main `61a67d28f`:

- At this head, `vitest run src/bundled/claude-code`: 281 passed, 4
skipped, and `tsc --noEmit` is clean. At the stack tip, the Claude,
Codex, Goose and Pi plugins, agent harness, agent protection, agents,
Buzz MCP, Builderlab and scripts: 3,000 passed, 18 skipped.
- At the stack tip: `cargo test --lib host_process`: 33 passed. `cargo
test -p buzzodz-plugins`: 64 passed. Clippy (`-D warnings`) and `cargo
fmt --check` are clean.
- `biome check` and `tsc --noEmit` are clean at the stack tip.
- After the rebase onto `61a67d28f`, one conflict: main's recent-notes
read ([#2040](#2040)) now runs
after the protection check at session launch; in protected sessions the
notes reach the policy with the rest of the system prompt. The agent,
Buzz MCP, relay, Builderlab, Inbox, Channels and scripts suites (6,882
passed, 22 skipped), `tsc --noEmit`, `cargo test --lib host_process`
(33), `cargo test -p buzzodz-plugins` (64), Clippy (`-D warnings`) and
`cargo fmt --check` passed there.
- **New tests, each seen failing on the previous code:**
  - a review runs the pinned copy, contained;
- a review installs the pin first, and fails without starting a worker
when that install fails;
- shutdown waits for processes still starting and for a spare it took,
and starts nothing for a conversation still launching;
  - the backstop refuses a protected entry made before `sync`;
  - an agent runs again after its retirement failed.
- **Other new tests:** a review passes `--setting-sources ""`; the hook
guard's stderr reason and its quiet ordinary exit; `~/` artifact and
protection paths; the `uname` mapping for both Mac architectures,
refusing Linux; the manifest pins matching `PROTECTED_CLAUDE_ROOT` and
`PROTECTED_CLAUDE_VERSION`; `claude.sh` keeping 2.1.305 when it switches
past it.
- **Agent review:** an independent review of the earlier round's fixes
found no blockers. An independent review of the settings change scored
it 9/10 with no blockers.

### Not yet done

- No packaged-app run of the sandbox and the artifact copy. Dev builds
refuse contained spawns.
- The CI pin check runs only in hosted CI. I didn't download the release
locally.
- **Deferred live checks.** No pinned Claude 2.1.305 binary or real
Claude account was run for the review sign-in. Not yet checked:
- that `CLAUDE_SECURESTORAGE_CONFIG_DIR` and the keychain don't supply
another sign-in;
- a review with a fresh, empty `CLAUDE_CONFIG_DIR` (trust and onboarding
skip, and Claude writing into that folder);
- `CLAUDE_CODE_OAUTH_TOKEN` taking precedence over stored sign-ins, and
its 401 behavior;
- the real `claude setup-token` output, and the real error texts for an
expired, revoked or non-subscription token;
- whether remote managed settings are fetched into the review folder
under an env token.
- Not checked live: a Claude-model review under the flag with hostile
settings files.
- The companion creating and hiding the review folder was not run
against this build in the app.
- The Codex launcher fixes were checked only in fake homes, not against
a real Codex.
- The review sign-in field in the native app: both themes, narrow and
wide widths, enlarged text, keyboard and pointer.
- Human testing.

<!-- buzz-security-flow:start -->
### Code flow

![[3/5] Add protection to the Claude harness plugin — illustrative code
flow](https://github.com/raw/block/buzz-app/f36b5077959f8c565487457947a479525ff4d05f/docs/assets/agent-protection/claude.png)

Claude holds native input, tool actions and raw output. Its 350-second
native watchdog stops stalled callbacks before the 600-second CLI
timeout.

Illustrative flow; checks shown enabled. Source:
[claude.ts](https://github.com/block/buzz-app/blob/4564d2b68/src/bundled/claude-code/claude.ts),
[claude_hooks.rs](https://github.com/block/buzz-app/blob/4564d2b68/src-tauri/src/host_process/claude_hooks.rs).
<!-- buzz-security-flow:end -->

Review and merge bottom to top:

1. [[1/5] Keep protected launches on the startup Agents
root](#2044) (merged)
2. [[2/5] Pi harness
integration](#1877) (merged)
3. [[3/5] Claude harness
integration](#1878) (this PR)
4. [[4/5] Codex harness
integration](#1879)
5. [[5/5] Goose harness
integration](#839)

Above these: [#2088](#2088) (exact
history records) and
[#2105](#2105) (denial reasons for
the owner).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Signed-off-by: Alex Rosenzweig <arosenzweig@squareup.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Codex <codex@openai.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants