Skip to content

broker: add storage read/write and live-signal actions to the contract - #6922

Open
jrobotham-square wants to merge 2 commits into
mainfrom
jrobotham/broker-storage-rw
Open

broker: add storage read/write and live-signal actions to the contract#6922
jrobotham-square wants to merge 2 commits into
mainfrom
jrobotham/broker-storage-rw

Conversation

@jrobotham-square

@jrobotham-square jrobotham-square commented Aug 27, 2026

Copy link
Copy Markdown

What

Extends the broker action contract (buzz-sdk::broker) with six actions in two groups. Four were named as deferred by #6742 (memory read/write, presence.set, typing.set); the other two, observer.emit and liveness.ping, are net-new — the trajectory and keepalive planes the contract never enumerated.

Storage — memory read/write (deferred by #6742):

action args outcome
storage.get { slug } { value? }value absent = no record (not an error)
storage.put { slug, value } EventPublished

Live signals — the ephemeral signals a running agent emits so an owner and channel can see it work:

action args outcome source
presence.set { status } EventPublished deferred by #6742
typing.set { channelId } EventPublished deferred by #6742
observer.emit { frames: [{ kind, payload }] } { accepted } (batch receipt) net-new
liveness.ping { channelId, turnId } EventPublished net-new

All six mirror the existing nine in shape, validation, strict-wire rules, and test coverage. The four signal actions are best-effort — a host that doesn't offer one refuses it and the agent carries on.

Why

A keyless agent routes everything through the broker host and holds no relay connection, so anything a local agent emits directly to the relay needs an action or it silently can't happen. #6742 shipped the nine core actions and named four deferrals; this PR:

  • graduates the deferred storage + presence/typingstorage.address could address an encrypted-memory record but not read or write one (a keyless runtime was amnesiac across wakes), and presence/typing are how an owner and channel see an agent is alive and composing (the presence dot, the typing bubble);
  • adds observer.emit and liveness.ping, which broker: define the agent-to-broker action contract #6742 didn't enumerate — the observer/trajectory stream in Buzz Desktop and a turn keepalive. These fell out of the keyless-observability discussion: without them a keyless agent runs invisibly, with no way to surface its tool calls or prove a long turn is still alive.

Decisions baked in (all up for discussion)

  • Host does all the crypto and Nostr work — owner pubkey, NIP-44 encryption, signatures, kinds, and the observer batching/pacing are host-derived; the agent supplies only semantic content. Same separation as the rest of the contract.
  • observer.emit is a batch — trajectory is high-volume, so frames are sent per call and the host re-batches/paces. Each frame is { kind, payload } where payload is opaque (a serialized frame body the host encrypts verbatim, never parses); kind stays top-level so a host can apply per-kind policy without decrypting. The outcome is a batch receipt { accepted }, since re-batched frames have no stable per-frame id.
  • Ephemeral, not persisted — these frames exist only for a live watcher; persistence, if ever wanted, is a separate system.
  • liveness.ping is its own action despite overlapping observer.emit on the wire (a keepalive could be one more frame). Keeping it separate lets a host attach meaning — resetting a stall watchdog — rather than only forwarding it. Flagged in the module docs; happy to collapse it if preferred.
  • typing.set has no stop counterpart — the indicator is ephemeral and lapses on its own; a client signals by re-sending and stops by falling silent. Thread-scoped typing is a purely-additive later refinement.
  • presence.set reuses buzz_core::presence::PresenceStatus (online/away/offline) rather than minting a parallel enum.
  • (storage half) slug-addressed, missing record → absent value, no patch/rm — a client composes those from read-modify-write, as profile.set already does.

Open questions for review

  1. liveness.ping vs observer.emit — worth a first-class action, or fold it in?
  2. Observer boundsMAX_OBSERVER_FRAMES = 256 per call, MAX_OBSERVER_FRAME_BYTES = 64 KiB per frame. Reasonable for trajectory volume?
  3. storage.address fate — with slug-addressed get/put, does address still earn its place (existence checks / coordinates without a payload)? Left in place either way.

Scope / sequencing

Contract crate only — self-contained and mergeable on its own (nothing on main matches these enums exhaustively outside buzz-sdk). When the keyless client branch rebases onto this, buzz-cli's exhaustive matches gain the six variants — the forcing function to implement them over the host seam — which is where the client's memory refusals and disabled observer path get removed.

Two commits, reviewable independently: storage.get/storage.put, then the four signals.

Verification

  • cargo test -p buzz-sdk — 302 passed. The self-enforcing coverage, round-trip, null-injection, canonicalization, and exact-schema tests all extend to the new actions automatically.
  • cargo clippy -p buzz-sdk --all-targets -- -D warnings — clean.
  • cargo fmt -p buzz-sdk -- --check — clean.
  • cargo check --workspace — clean.

🤖 Generated with Claude Code

Memory read/write graduates from a deferred operation into v1. A keyless agent
could address an encrypted-memory record (storage.address) but not read or
write one, so a keyless runtime stays amnesiac across wakes.

Both actions are slug-addressed and mirror the existing nine:
- storage.get { slug } -> { value? }   (value absent = no record, not an error)
- storage.put { slug, value } -> EventPublished

The host derives the address, encrypts on put, and decrypts on get, so the
secret never leaves the key holder -- the same reason storage.address routes
through the interface. value is non-empty and bounded by MAX_CONTENT_BYTES. No
patch/rm: a client composes those from read-modify-write, as profile.set
already does for partial updates.

storage.address is left in place; whether get/put make it redundant is an open
question for review.

Contract crate only. buzz-cli's exhaustive matches gain the two variants when
the keyless-client branch rebases onto this.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Joel Robotham <jrobotham@squareup.com>
@jrobotham-square
jrobotham-square requested a review from a team as a code owner August 27, 2026 06:45
@jrobotham-square
jrobotham-square marked this pull request as draft August 27, 2026 06:50
@jrobotham-square jrobotham-square changed the title broker: add storage.get and storage.put to the action contract broker: add storage read/write and live-signal actions to the contract Aug 27, 2026
…ontract

Give a keyless agent parity with a local one for the ephemeral signals a
running agent emits so an owner and channel can see it work. presence.set and
typing.set were named as deferred by #6742; observer.emit and liveness.ping are
net-new -- the trajectory and keepalive planes the contract never enumerated
but a keyless agent needs just as much once it holds no relay connection.

Four best-effort actions, all following the existing contract shape (strict
wire, one spelling of every identity, no member names its own subject):

- presence.set  -> status only (reuses buzz_core PresenceStatus)
- typing.set    -> channelId only; ephemeral, no stop counterpart
- observer.emit -> a batch of frames, each { kind, payload }; payload is opaque
                   and encrypted host-side, and the outcome is a batch receipt
                   since re-batched frames have no stable per-frame id
- liveness.ping -> { channelId, turnId }; distinct from an observer frame so a
                   host can attach meaning (reset a stall watchdog), not just
                   forward it

The host still derives owner, key, encryption, and all Nostr metadata; the agent
supplies only content. observer.emit and liveness.ping overlap on the wire --
flagged in the module docs so a reviewer can collapse liveness.ping if preferred.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Joel Robotham <jrobotham@squareup.com>
@jrobotham-square
jrobotham-square force-pushed the jrobotham/broker-storage-rw branch from ee63980 to b25a202 Compare August 27, 2026 07:28
@jrobotham-square
jrobotham-square marked this pull request as ready for review August 27, 2026 07:33
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