Files
buzz/docs/nips/NIP-LE.md
npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 305cda5be1 test(buzz-acp): cover dispatch_pending leader gate, harden mutex, document fail-safe scope
The primary suppression surface — dispatch_pending — had no test; only
the heartbeat gate was covered, so a refactor of the shared leader gate
could silently ship a non-leader that still dispatches. Add the missing
leader/non-leader test pair.

Read-side mutex locks now read through poison rather than panic the event
loop, removing a latent footgun for when Phase 2 extends the critical
sections. Module-doc and NIP-LE now state that any IO error (not just an
absent lock) fails safe to leader, that this can produce a bounded ≤5s
transient duplicate during a concurrent rewrite, and that pid/claimed_at
are read-side-ignored.

Co-authored-by: Will Pfleger <wpfleger@block.xyz>
Signed-off-by: Will Pfleger <wpfleger@block.xyz>
2026-06-24 17:27:15 -04:00

6.0 KiB

NIP-LE

Leader Election (Shared-Identity Multi-Instance)

draft optional

This NIP defines a client-side, local-filesystem convention by which multiple instances running under a single shared agent identity elect exactly one prompter (leader), so that a mention fanned out to every instance produces a single agent response.

Motivation

When several Buzz client instances run with the same agent keypair, the relay correctly fans every event addressed to that key out to all connected instances (per NIP-01). Without coordination, every instance promotes the event to an agent prompt and every instance responds — duplicating work and producing duplicate replies under one identity.

This commonly arises in development: a packaged build (DMG) and one or more just staging builds from in-progress worktrees may run concurrently, all sharing the developer's agent identities.

This NIP defines a minimal coordination layer that elects a single prompter per agent identity without any relay-side logic and without defining a wire format.

Non-Goals

This NIP does not define any Nostr event kind or tag. This NIP does not define relay-side coordination. This NIP does not define a cancel mechanism — hard-steal reuses NIP-AO's cancel_turn control frame. This NIP does not define the durable election identity source; it requires only that the identity be per-window-unique (see The Lock Contract).

The Invariant

A single agent identity MAY have N subscribed instances but MUST have exactly one prompter (the leader).

Non-leader instances MUST subscribe to and render the full conversation — the message queue stays ungated so the UI displays all messages identically to the leader. Non-leader instances MUST suppress:

  • (a) the prompt/dispatch path — they do not promote queued events to agent prompts; and
  • (b) pre-dispatch side-effects — specifically the 👀 acknowledgement reaction, which fires at queue-acceptance time, before dispatch; and
  • (c) the autonomous heartbeat prompt path — the periodic self-prompt that, when enabled, has the agent act on the wire (send messages, approve workflows); non-leaders MUST NOT fire it, for the same duplicate-actor reason as (a).

Fail-safe: if no lock exists for an agent identity, every instance is a leader. This is the single-instance / solo-dev default and is byte-unchanged from behavior prior to this NIP — a solo developer has exactly one instance, which leads unconditionally.

The Lock Contract

The lock lives on the LOCAL filesystem at ~/.buzz/leader-locks/<agent-pubkey-hex>.lock. It is NOT a Nostr event and defines no wire format. This is why this NIP touches no relay protocol: leader election is entirely local to the machine running the instances.

The lock file contains a JSON object with the following shape (provisional — finalized against Phase 2 implementation):

{
  "instance_id": "<per-window-unique election id>",
  "pid": 12345,
  "claimed_at": "<iso8601 or unix timestamp>"
}
  • instance_id: the identity of the leading window. It MUST be a per-window-unique election identity. The Tauri bundle identifier is insufficient — it collides across same-class windows (for example a DMG build and a just staging dev build, or two worktree builds whose unique-icon generation fell back to the shared identifier), causing two windows to both match the lock and both lead. A correct election identity is unique per running window (for example a process-unique value derived at launch).
  • pid: the operating-system process id of the leading window.
  • claimed_at: when the current leader claimed the lock.

Only instance_id is significant to the read side; pid and claimed_at are informational, written by the Phase-2 acquire path and ignored when deciding leadership.

An instance is the leader for an agent identity iff a lock file exists and its instance_id equals that instance's own election identity. A malformed or unparseable lock file MUST fail safe to leader, preserving solo-dev behavior. An absent or unreadable lock file (permission error, or a partial read while another instance is mid-rewrite) likewise fails safe to leader. Because the read side is unguarded (acquire/steal flock is Phase 2), a concurrent rewrite read as malformed can briefly produce a duplicate responder; this self-heals on the next refresh (≤5s, see below) and is the intended trade — a transient duplicate beats silencing the only responder.

Leadership changes (claim, release, failover) take effect within a bounded refresh interval of 5 seconds: each instance re-reads its leader lock(s) every 5s, so a stale-cache window after a leadership change is capped at that interval. Phase 2 MUST NOT regress this bound.

Acquire and steal MUST be flock-guarded to close the read-check-write TOCTOU window between two instances racing to claim or steal the same lock.

Claim/Steal Semantics

Ownership is explicit and sticky to a window: a window claims an agent (via the agent sidebar menu) and remains its leader until the lock is released or stolen.

Hard-steal: claiming an agent in window B immediately aborts window A's in-flight turn. The abort reuses NIP-AO's cancel_turn control frame (kind 24200); this NIP does not define a separate cancel mechanism. See NIP-AO for the control-frame structure and authorization rules.

Relationship to other NIPs

NIP-LE references the following NIPs; it does not amend any of them.

  • NIP-OA (Owner Attestation) — owns the owner↔agent identity model (one owner authorizes one agent key) but is silent on what happens when that same agent key runs in N instances at once. NIP-LE fills that gap.
  • NIP-RS (Cross-Device Read State Sync) — precedent for same-user, multi-instance coordination. NIP-RS synchronizes data (read position) across instances; NIP-LE coordinates behavior (which instance prompts).
  • NIP-AO (Agent Observability) — provides the cancel_turn control frame (kind 24200) that NIP-LE's hard-steal reuses to abort a displaced window's in-flight turn.