Skip to content

feat(box): a box keeps attention items, so a plugin's snooze follows you between computers - #219

Draft
Stener1 wants to merge 4 commits into
cosscom:mainfrom
Stener1:bra-3274-attention-items
Draft

Stener1 wants to merge 4 commits into
cosscom:mainfrom
Stener1:bra-3274-attention-items

Conversation

@Stener1

@Stener1 Stener1 commented Nov 6, 2026 •

Copy link
Copy Markdown
Contributor

Summary

A plugin's storage is per computer, so a snooze or "resolved" a person sets on one laptop is news to their other one, and a plugin has nowhere on the box to keep what an agent asks. Each box now keeps a small list of attention items: asks an agent posts, and a person's marks on items a plugin builds from the box's own signals. Every app paired with the box reads the same list. ("Attention" is a working name: the box's inbox already means held prompts.)

sequenceDiagram
    participant A as Laptop A (plugin)
    participant Box as berthd (attention.json)
    participant B as Laptop B (plugin)
    A->>Box: PATCH /v1/attention/{key} {snoozed_until, revision}
    Box-->>A: item, revision+1
    Box-)B: attention.updated {key, revision, status}
    B->>Box: GET /v1/attention
    Note over Box: session stopped → settle at once (session.stopped observer)<br/>program exited → settle when two sweeps miss it (refreshTurns, 10 s)
Loading
 internal/box/
+├── attention.go      # the store: key + revision replace, settle, 14-day ageing, 1000-item cap
+├── attentionapi.go   # GET/POST /v1/attention, GET/PATCH /v1/attention/{key...}, events, the session.stopped observer
 ├── turns.go          # refreshTurns also sweeps attention (the backstop for exits)
 └── upgrade.go        # capability "attention"
 packages/plugin-sdk/src/
+│   BerthAttention     # berth.attention.list/get/post/update, AttentionItem types
 app/src/lib/
+└── attention.ts      # what plugins call, through the laptop agent
  • One key per ask. Posting to a key replaces the item there and counts its revision on, so an agent replaces its own ask instead of adding another; a post with a session and no key is session:<name>. A post or update that names a revision that has gone by is refused (409).
  • Marks. An update to a key with no item makes a mark there, so a plugin can snooze or resolve an item it builds itself (a waiting agent, a run's gate).
  • Settling. open → answered or closed. An open item about a session closes as session_ended when the session is stopped (at once), or when its program ends (once two sweeps, each with a listing taken after the item last changed, miss it).
  • Kept safe. A state file the box can't read refuses the call and is read again next time, never taken as empty; one that isn't JSON is set aside as attention.json.corrupt-<time>. A settle changes memory only after the file holds it.
  • Events say what changed, never an item's words. before:attention.post and before:attention.update gates can refuse.

Docs: reference/plugin-sdk#attention, the box routes in app-api, events and gates, and a changelog line. Based on 4ebd998 (the commit before #207); it merges clean onto main.

Evidence

A real box and two laptop agents paired with it, each with its own pairing code (.claude/skills/verify-berthd/scripts/attention-live.sh), driven in two browsers by scenarios/attention-two-laptops-live.mjs with a small test plugin. Result ok: true, no console errors on either laptop.

  • Before: a snooze kept in plugin storage stays on the laptop that made it. Laptop B's storage line reads nothing below.
    After: laptop B reads the snooze laptop A made, from the box.
Laptop A snoozes Laptop B reads it (its own storage: nothing)
A snoozed B reads the snooze
B resolves, A shows it with no reload (event) Session stopped: its open ask settles on both
A sees B's resolve B after the session ended

Steps the scenario asserts with literal text:

A: snooze session:att-shell     → "… open · snoozed until 2030-01-02T09:00:00Z · rev 2 · by app" (A and B)
B: storage line                 → "This computer's plugin storage: nothing"
B: resolve                      → "… closed (resolved) · … · rev 3" on B, then on A without a reload
PATCH from revision 2           → 409
DELETE session att-shell        → "acme/deploy-check · action · closed (session_ended) · … · rev 2 · by berth" on both
session "sleep 4" with an ask   → closed (session_ended) on both, from the sweep
GET /v1/attention via laptop B  → the same, read back through the second agent

Unit tests (go test -race ./internal/box -run 'Attention|AMark|ItemsSettle|AnAgentReplaces|ASweep|ASettle', 3× under -race): key and revision replace, marks, settle on session end, the two-miss rule and a post after the listing, ageing out, a restart, a file that can't be read or isn't JSON, a settle whose write fails. app/src/lib/attention.test.ts: paths are escaped (/, #), get is undefined for no item.

Checks run locally (fork PRs wait for a maintainer before CI): go vet ./..., go test -race ./... (only TestTheCatalogIsCapped fails, as on main on this machine: it counts the machine's own commands), the app's typecheck:plugins, check:*, test, build, tsc -p e2e, and the docs build. cargo check is untouched (no Rust change). Codex review and adversarial review ran once; their findings (a sweep against a stale listing, a short listing, a load error taken as empty, a settle not saved, a post to an exited session) are fixed in the last commit with tests.

Merge Danger

Door: two-way

Additive: a new state file (~/.berth/…/attention.json), new routes, a capability, new events and gates. Nothing existing reads or writes them. Reverting leaves an unused file behind.

Blast Radius: small

Only plugins that call berth.attention see it. The one shared path touched is refreshTurns, which now also sweeps the store (a map walk, a file write only when something settles), and a session.stopped observer that hands off to a goroutine. The plugin SDK gains attention on BerthPluginContext: a plugin written against it should check info(box).capabilities and that berth.attention exists, as documented.

🤖 Generated with Claude Code


Autofix with [code]smith
Open in [code]smith · Need help on this PR? Tag @codesmith-bot with what you need.

View guided diff

Stener1 and others added 3 commits November 6, 2026 12:39
…you between computers (BRA-3274)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ent, and simplify the store

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…, and never lose items on a failed read or write

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@vercel

vercel Bot commented Nov 6, 2026

Copy link
Copy Markdown

@Stener1 is attempting to deploy a commit to the seanbrydonio's projects Team on Vercel.

A member of the Team first needs to authorize it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
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.

1 participant