docs(nips): add NIP-LE leader election draft

Documents the shared-identity multi-instance leader-election convention:
the one-prompter invariant, the local-filesystem lock contract, and
claim/hard-steal semantics. Lock is local-only and defines no wire format,
so this touches no relay protocol. References NIP-OA, NIP-RS, and NIP-AO
without amending them.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
This commit is contained in:
npub16v54tttfqacx9ycvc3k0ut0npj564ahcuajzy6qjvh57ntmsf4uq4806j2
2026-06-15 18:18:20 -04:00
parent 116486592d
commit 247e0a7127

114
docs/nips/NIP-LE.md Normal file
View File

@@ -0,0 +1,114 @@
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.
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):
```json
{
"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.
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.
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.