talking-stick 0.1.4 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,290 +1,369 @@
1
1
  # Out-of-Band Signaling Between Harnesses
2
2
 
3
- **Status:** Design proposalnot yet scheduled. Intended for cross-harness review (Codex + Claude Code).
3
+ **Status:** Amended designconverged on 2026-04-30 between `codex:5c11d1e8` and `claude:9610b1fe` under operator framing. Implementation gated on PR review.
4
4
  **Related:** [ambient-presence.md](../ambient-presence.md), [talking-stick-plan.md](../talking-stick-plan.md)
5
+ **Supersedes:** the original notes-with-severity proposal (commit `0e2bf58`, PR #12). Git history preserves the prior version.
5
6
 
6
- ## Purpose
7
+ ## What changed from the original
8
+
9
+ The original proposal piggybacked transient signaling on the existing `notes` table by adding `severity` (info/page) and `target_agent_id` columns, and made a per-harness `tt events --follow` watcher subprocess the canonical delivery mechanism. After re-reading the design under the operator's chat-style framing ("a message pipe for conversation as long as they keep a process running, send messages back and forth with single commands"), the amended design makes three structural changes:
10
+
11
+ 1. **Transient messages are not notes.** Notes stay durable, resolvable task artifacts, unchanged API. Messages are an event-only stream emitted directly into `room_events`, with no separate table and no impact on `list_notes`. The interrupt-class behavior lives on message events, not on notes.
12
+ 2. **The substrate is a generic event payload, not narrow per-event columns.** Migration 5 adds a single `room_events.payload_json TEXT NULL` column. Specific event types (`message_sent`, future `member_joined`, `member_left`) discriminate via `event_type` and read event-specific keys out of JSON while still using common columns like `from_agent_id`, `to_agent_id`, `reason`, and `created_at` where they already fit. Cheaper than four narrow columns; extensible without further migrations.
13
+ 3. **v1 ships a working agent-side path via the CLI; plugins are an optional future enhancement for richer harness-native UX.** The canonical v1 receive pattern is a long-running `tt msg recv --follow` child process whose stdout the harness, operator, or terminal can watch (Claude Code's Monitor today; tmux/terminal for humans; explicit between-turn polling where stdout-watch is unavailable). This repo ships the protocol substrate, the `tt msg` / `tt events --follow` CLI, the skill guidance, and a receive-consumer contract. Per-harness plugins (Claude Code, Codex, Gemini, OpenCode) are a *future* layer for harness owners that want ambient UX beyond what the CLI subprocess delivers; they live outside this repo and are not prerequisites for v1.
7
14
 
8
- The talking stick today enforces **in-band, single-speaker** coordination: the holder is the only participant whose work actually mutates the workspace, and other participants either wait or observe. That is correct for write authority. It is too restrictive for *signaling*.
15
+ ## Purpose
9
16
 
10
- There are real situations where a non-holder needs to reach the holder or needs to be reached *without* taking the stick:
17
+ The talking stick today enforces **in-band, single-speaker** coordination: the holder is the only participant whose work mutates the workspace; others wait or observe. That is correct for write authority. It is too restrictive for *signaling*.
11
18
 
12
- - The non-holder is watching the holder's work and notices a problem (wrong file, broken assumption, looming merge conflict). It should be able to say so without forcing a takeover.
13
- - A new participant joins the room mid-turn. The holder may want to greet, hand off, or just acknowledge. Today the holder finds out only when they next call `get_room_state`.
14
- - A participant leaves the room mid-task. The holder should know not to `pass_stick` to a harness that is no longer participating.
15
- - A holder finds an issue that another harness should address next. It should be able to page that harness before the formal handoff so the recipient is not surprised at claim time.
16
- - An operator drops a note ("we're scoping down — stop after the test passes"). The holder should see it before the next handoff boundary.
17
- - The watcher itself is an LLM ("guardian") spawned to keep the holder honest; its only job is to tail the room and raise its hand on specific conditions.
18
- - A release/pass event can be useful as an early wake-up signal for a waiting harness, even though `wait_for_turn` remains the authority that decides whether the harness may claim.
19
+ There are real situations where a non-holder needs to reach the holder or where two non-holders need to coordinate *without* taking the stick:
19
20
 
20
- This document proposes the smallest primitive set that lets harnesses exchange these signals over the existing room-event log, plus the harness-side glue (background watcher + stdout-line notification) that makes them feel ambient instead of poll-driven.
21
+ - A non-holder watching the holder's work notices a problem (wrong file, broken assumption, looming merge conflict). It should be able to say so without forcing a takeover.
22
+ - The holder wants a quick yes/no from a peer mid-edit and does not want to release the stick to ask.
23
+ - A new participant joins mid-turn. The holder may want to acknowledge, hand off, or just note the change in social shape.
24
+ - The operator drops a directive ("scope down — stop after the parser test passes"). The holder should see it before the next handoff boundary.
25
+ - Two agents want to discuss a design decision back and forth without paying the token cost of formal handoffs each round-trip.
21
26
 
22
- It is a layer on top of [ambient-presence.md](../ambient-presence.md). Where ambient-presence proposes `tt events --follow` as a one-way *observer* stream for waiting agents, this document extends the same stream to be the channel for *directed* signals into an active turn, and defines what those signals look like.
27
+ The original design framed the dominant case as *guardian-style page* (non-holder holder, single direction, interrupt-worthy). The chat-style framing makes it *bidirectional discussion* (holder non-holder, multiple round-trips, interrupt class is per-message, often not interrupt at all). Both cases sit on the same primitive; the amendment does not bifurcate them.
23
28
 
24
29
  ## Vision
25
30
 
26
- Vignette A guardian catches a wrong turn:
31
+ Vignettes A–G are adapted from the original; H is new and is the case the amendment most cares about. Presence-focused vignettes assume the optional presence-event stage has shipped.
32
+
33
+ **Vignette A — guardian catches a wrong turn:**
27
34
 
28
35
  1. Codex holds the stick, working on `src/auth/session.ts`.
29
- 2. Claude Code runs `tt events --follow` in the background under its Monitor tool. It is observer-only on the room.
30
- 3. A `note_added` event arrives with severity `page` plus a capped body preview: *"You're editing session.ts but the bug is in token.ts — see line 84."*
31
- 4. Claude Code's harness surfaces the line to the user, who can choose to interrupt Codex or let it self-correct on next read of room notes.
32
- 5. Codex finishes, calls `release_stick`, picks up the note via existing `list_notes`, acknowledges, hands off.
36
+ 2. Claude Code is acting as a non-holder guardian and sends `tt msg send codex --interrupt "You're editing session.ts but the bug is in token.ts see line 84."`
37
+ 3. Codex's receive process gets the `message_sent` event through `wait_for_events` with `target=self`.
38
+ 4. Codex may correct immediately or defer until the next natural read point. The message does not grant Claude write authority and does not require a takeover.
39
+ 5. At handoff, Codex can acknowledge the message in the structured `Handoff` if it affected the work.
33
40
 
34
- Vignette B — join awareness mid-turn:
41
+ **Vignette B — join awareness mid-turn:**
35
42
 
36
43
  1. Claude Code holds the stick on a long refactor.
37
- 2. A human runs `tt join` from a second terminal to observe.
38
- 3. A `member_joined` event arrives on Claude's background watcher.
39
- 4. Claude's watcher rule says: *member_joined is informational, not an interrupt — write it to the boundary buffer, not the loud Monitor stream.*
40
- 5. At next handoff prep, Claude reads the buffered events and notes "Wojtek joined two minutes ago" in the handoff body.
44
+ 2. A human runs `tt join` from a second terminal.
45
+ 3. A `member_joined` event lands on Claude's receive stream.
46
+ 4. Claude's consumer policy says: `member_joined` is informational, not an interrupt — write it to the boundary buffer.
47
+ 5. At handoff prep, Claude reads the buffered events and notes "Wojtek joined two minutes ago" in the handoff body.
41
48
 
42
- Vignette C — operator pages the active holder:
49
+ **Vignette C — operator pages the active holder:**
43
50
 
44
51
  1. The current holder is in the middle of a long edit.
45
- 2. The operator posts `tt notes add --severity page --target <holder> "Scope down: stop after the parser test passes."`
46
- 3. The holder's page channel emits one loud JSON line with the note id, author, severity, and capped preview.
47
- 4. The holder may act immediately or acknowledge at handoff. The page does not grant or revoke write authority.
52
+ 2. The operator runs `tt msg send <holder-id> --interrupt "Scope down: stop after the parser test passes."`
53
+ 3. The holder's receive process surfaces the message via the best available path, with the full body inline (no follow-up fetch).
54
+ 4. The holder may act immediately or acknowledge at handoff. The message does not grant or revoke write authority.
48
55
 
49
- Vignette D — a participant leaves before handoff:
56
+ **Vignette D — a participant leaves before handoff:**
50
57
 
51
- 1. Claude Code holds the stick and originally expected to pass the next turn to Gemini for review.
52
- 2. Gemini exits the room, or its membership is marked inactive after the implementation-defined debounce window.
53
- 3. Claude's buffer channel records `member_left` with `from_agent_id = gemini:...` and a reason.
54
- 4. At handoff prep, Claude sees the buffered leave event and does **not** `pass_stick` to Gemini. It either releases to the normal sequence or chooses a different active recipient with an explicit reason.
58
+ 1. Claude Code holds the stick and originally expected to pass to Gemini for review.
59
+ 2. Gemini exits the room or is GC'd inactive.
60
+ 3. Claude's receive process records `member_left` with `from_agent_id = gemini:...` and a reason.
61
+ 4. At handoff prep, Claude sees the buffered leave event and does not `pass_stick` to Gemini.
55
62
 
56
- Vignette E — the holder pages a future recipient:
63
+ **Vignette E — the holder pages a future recipient:**
57
64
 
58
- 1. Codex holds the stick and finds a regression that Claude should address after Codex finishes the current edit.
59
- 2. Codex posts `add_note` with `severity: "page"` and `target_agent_id = "claude:..."`: *"When I pass back, please start with tests/cli.test.ts; the install dry-run expectation is stale."*
60
- 3. Claude's page channel receives the note while Claude is still a non-holder. Claude may read and prepare, but still must not mutate the workspace until it owns the stick.
61
- 4. Codex later passes or releases with a handoff that references the same `note_id`, so the formal turn boundary and the earlier page line reconcile.
65
+ 1. Codex holds the stick and finds a regression Claude should address after.
66
+ 2. Codex calls `tt msg send claude --interrupt "When I pass back, please start with tests/cli.test.ts; the install dry-run expectation is stale."`
67
+ 3. Claude's receive process gets the message while Claude is still a non-holder. Claude may read and prepare, but still must not mutate the workspace until it owns the stick.
68
+ 4. Codex later releases or passes with a handoff that may reference the message body inline.
62
69
 
63
- Vignette F — a third harness joins an existing pair:
70
+ **Vignette F — a third harness joins an existing pair:**
64
71
 
65
72
  1. Codex and Claude have been alternating on a feature.
66
- 2. OpenCode joins the room to take over UI verification.
67
- 3. Both active watchers see `member_joined` in their buffer channels. It is not page-worthy by default, but it changes the social shape of the next handoff.
68
- 4. The current holder can mention the new participant in the next handoff, avoid hard-passing between only the original two harnesses, or explicitly pass to OpenCode if that is the right next owner.
73
+ 2. OpenCode joins the room.
74
+ 3. Both active members see `member_joined`. Default consumer policy treats it as informational, not interrupt-worthy. The current holder may mention OpenCode in the next handoff.
75
+
76
+ **Vignette G — handoff as an early wake-up signal (deferred future work):**
69
77
 
70
- Vignette G handoff as an early wake-up signal *(deferred future work, captured here for context):*
78
+ `wait_for_turn` remains the authority on stick availability; using events as advisory wakeup is gated on a separate wait-intent design.
71
79
 
72
- 1. Claude is waiting and, absent any other signal, would wake from its own scheduler in two minutes.
73
- 2. Codex releases the stick ten seconds later.
74
- 3. A future wait-helper sees the `release` event immediately and asks Claude to run a short `wait_for_turn` probe.
75
- 4. `wait_for_turn` still decides whether Claude may claim. The event line is only an advisory wake-up, and this optimization remains deferred until wait intent is modeled explicitly.
80
+ **Vignette H NEW bidirectional design chat without stick churn:**
81
+
82
+ 1. Codex holds the stick and is mid-implementation. He has a sharp question for Claude that is not blocking but matters.
83
+ 2. Codex: `tt msg send claude "are we tying force_new behavior to canonical_path uniqueness or to room_id uniqueness?"` a single CLI call, no flags beyond the recipient.
84
+ 3. Claude (non-holder, idle in his wait loop) gets the message via his receive process and answers: `tt msg send codex "canonical_path. There's a UNIQUE constraint on it; sequence_index is fairness state, not recency."`
85
+ 4. Three more round-trips of similar shape. Total: ~6 messages, no `pass_stick`, no `claim`, no formal handoff. Stick stays with Codex throughout.
86
+ 5. Codex resolves the question and continues the edit. The exchange is in the event log for audit, but did not occupy a turn boundary.
87
+
88
+ This is the case the amendment most wants to make ergonomic. Today, agents in this room (turns 73–76 of room `b94f3d80`) ping-pong the stick to have exactly this kind of discussion — paying ~600 tokens of handoff structure per round-trip for ~80 tokens of real exchange. Vignette H eliminates the ping-pong without weakening the ownership model.
76
89
 
77
90
  ## Scope
78
91
 
79
92
  In scope:
80
93
 
81
- - Extending the `RoomEvent` taxonomy so the existing `event_seq` log carries presence and notes, not just stick mutations.
82
- - Defining a "page" semantic on top of notes so harnesses can distinguish *interrupt-worthy* from *buffer-until-boundary*.
83
- - Specifying the stdout-line watcher contract that lets a harness convert events into harness-native notifications.
84
- - Quantifying the token cost of running such a watcher continuously.
94
+ - A single new event type `message_sent` carried over the existing `event_seq` log via a generic `payload_json` column.
95
+ - Optional follow-up event types: `member_joined`, `member_left`, and `note_added` — all using the same `payload_json` substrate. `note_added` is bookkeeping for consumers that want to follow notes without polling `list_notes`; it does not change notes semantics.
96
+ - A `delivery_hint: "normal" | "interrupt"` discriminator on `message_sent` payloads. `interrupt` is consumer policy, not a server guarantee the server delivers; the receiver decides whether and how to interrupt.
97
+ - `wait_for_events`: a non-mutating long-poll over the existing event stream with filters (`after_event_seq`, `event_type`, `target_agent_id`).
98
+ - `tt msg send` / `tt msg recv` ergonomic CLI wrappers around the underlying `send_message` / `wait_for_events` primitives.
99
+ - A documented receive-consumer contract so the long-running stdio process and future per-harness plugins share the same lifecycle, cursor, and backpressure rules.
100
+ - Skill prose updates explaining when to chat-message versus when to pass the stick.
85
101
 
86
102
  Explicitly out of scope:
87
103
 
88
- - Any new write authority for non-holders. Notes/pages do not grant the stick. Takeover remains the only way to seize write authority and is unchanged.
89
- - A second event log, second cursor concept, or second identity model. Everything reuses `event_seq`, `agent_id`, and the existing room-resolution rules.
90
- - Push transports (websockets, MCP resource subscriptions). Pull-based long-poll over SQLite is sufficient for v1; see *Tradeoffs*.
91
- - Any harness-specific notification format. Harness-detected runs get machine-readable JSON lines; plain human CLI runs get human-readable text by default. `--json` and `--text` remain explicit overrides.
92
- - Event-driven stick claiming. `wait_for_turn` remains the authoritative wait/claim path in v1; using the event stream to wake waiters is deferred until wait intent is modeled explicitly.
104
+ - Any new write authority for non-holders. Messages do not grant the stick. Takeover remains unchanged.
105
+ - Read receipts, acks, replies/threading, per-recipient mute, message resolution. v1 is one-shot fire-and-forget over the event log.
106
+ - Push transports (websockets, MCP resource subscriptions). Pull-based long-poll over SQLite is sufficient for v1.
107
+ - Any harness-specific notification format. Receive consumers use the same event payloads and decide their own UX.
108
+ - Server-side rate limiting. Deferred until real usage demonstrates need; v1 protections are body-size cap and harness-side replay coalescing.
109
+ - Event-driven stick claiming. `wait_for_turn` remains the authoritative wait/claim path until a separate wait-intent design exists.
110
+ - Treating `to_agent_id` as ACL/privacy. It is routing only; any room member can read any message.
93
111
 
94
112
  ## Architecture
95
113
 
96
- Four layers, building on what exists.
114
+ Six layers. Each is independently shippable; together they deliver the chat UX.
97
115
 
98
- ### Layer 1 — Extended event taxonomy
116
+ ### Layer 1 — Generic event payload
99
117
 
100
- Today `RoomEvent.event_type` is `"claim" | "release" | "pass" | "takeover" | "close"`. Notes (`addNote`) live in a separate `notes` table and emit nothing into `room_events`. Member join/leave does not emit events at all. That means the long-poll stream has nothing to say about anything except stick handoffs.
118
+ Today `room_events` has fixed columns: `event_seq`, `event_id`, `room_id`, `turn_id`, `event_type`, `from_agent_id`, `to_agent_id`, `handoff` (JSON), `reason`, `created_at`. Migration 5 adds:
101
119
 
102
- Proposed additions to `event_type`:
120
+ ```sql
121
+ ALTER TABLE room_events ADD COLUMN payload_json TEXT NULL;
122
+ ```
103
123
 
104
- | New event | Emitted when | Fields beyond the common ones |
105
- |-------------------|----------------------------------------------------|------------------------------------------------|
106
- | `member_joined` | `joinPath` adds a member or reactivates one | `to_agent_id` = joiner |
107
- | `member_left` | `leaveRoom` succeeds, or a member is GC'd inactive | `from_agent_id` = leaver, `reason` |
108
- | `note_added` | `addNote` succeeds | `note_id`, `severity`, `target_agent_id?`, `body_preview?` |
109
- | `note_resolved` | A future `resolve_note` (or implicit on takeover) | `note_id` |
124
+ The column is opaque JSON whose shape is discriminated by `event_type`. Existing event types (`claim`, `release`, `pass`, `takeover`, `close`, `kick`) write `NULL` and continue to use the typed columns they already populate. New event types put only event-specific fields in `payload_json`; routing and audit fields stay in the common columns when they fit. This keeps the migration trivial and avoids a parallel migration each time a new event type is added.
110
125
 
111
- Rationale for putting notes into the event log rather than inventing a parallel notes-stream:
126
+ ### Layer 2 Message events
112
127
 
113
- - Single cursor. Watchers already need `event_seq` to resume after disconnect; folding notes in means no second cursor and no race between two streams.
114
- - Replay parity. Rebuilding room state from the event log already requires reading every mutation; adding notes to that stream means a fresh observer can reconstruct "what does the holder need to know?" without a second query.
115
- - Audit shape. The event log is append-only and ordered. Notes already are too. The shapes match.
128
+ A new `event_type = "message_sent"`. Payload schema:
116
129
 
117
- The persisted `note_added` event carries metadata: `note_id`, `severity`, optional `target_agent_id`, and a capped `body_preview` for page delivery. The full body still lives in the `notes` table and is fetched via `list_notes`. This keeps persisted event payloads bounded while making a page line actionable without a second foreground tool call.
130
+ ```json
131
+ {
132
+ "body": "are we tying force_new to canonical_path or room_id?",
133
+ "delivery_hint": "normal"
134
+ }
135
+ ```
118
136
 
119
- ### Layer 2 — Note severity and targeting
137
+ Fields:
120
138
 
121
- Notes today are flat: any member can post one, the holder reads them at handoff boundaries. To support out-of-band signaling we add two optional fields on `AddNoteInput`:
139
+ - `body`: required string. Hard cap **4096 bytes** (UTF-8). Server rejects over-limit with typed error `message_too_large`. **No silent truncation.**
140
+ - `delivery_hint`: `"normal"` (default) or `"interrupt"`. Advisory only — the server delivers; the receiving consumer decides.
122
141
 
123
- ```ts
124
- interface AddNoteInput {
125
- agent_id: AgentId;
126
- room_id: string;
127
- body: string;
128
- turn_id?: number;
129
- severity?: "info" | "page"; // NEW — defaults to "info"
130
- target_agent_id?: AgentId; // NEW — null/undefined = whole room
131
- }
132
- ```
142
+ The event's existing `from_agent_id` column carries the sender; `to_agent_id` carries the recipient; `to_agent_id = NULL` means broadcast to the room. `event_id` doubles as the message id; `created_at` is the server-stamped timestamp. There is no separate `room_messages` table. Replay of historical chat uses the same `get_room_events` machinery already used for handoff replay.
143
+
144
+ `to_agent_id` is **not** a privacy boundary. Any room member reading the event log can see any message's body. Messages are routed, not encrypted. The skill must say so in plain words.
133
145
 
134
- Semantics:
146
+ ### Layer 3 — Notes (unchanged)
135
147
 
136
- - `severity: "info"` (default) buffer until the recipient's next safe boundary. Watchers should NOT interrupt the active turn for these.
137
- - `severity: "page"` — recipient's watcher SHOULD interrupt the active turn. Use sparingly. The protocol does not enforce attention; it provides the signal and lets the receiving harness decide.
138
- - `target_agent_id` — addresses a specific member. If absent, the note is room-wide. The current holder is implicitly a target for any unaddressed page.
148
+ Notes API and semantics are not modified by this plan. Notes remain durable, resolvable task artifacts written via `add_note` and read via `list_notes`. Notes are *not* the chat surface. Skill guidance must reinforce the split:
139
149
 
140
- The protocol does **not** define what "interrupt" means in any specific harness. That is each harness's call. The protocol guarantees only: the event arrives, the severity is preserved, and the cursor advances.
150
+ - **Note** when the holder needs to consider something at the next handoff boundary, or when the artifact should outlive the conversation.
151
+ - **Message** when the exchange is conversational, ephemeral, and tied to two or more processes that are currently online.
141
152
 
142
- ### Layer 3 `tt events --follow` as the harness channel
153
+ Optional v1.1: emit a `note_added` event on `add_note` so consumers watching a single event stream can surface notes without separately polling `list_notes`. Pure bookkeeping; no schema change to the notes table.
143
154
 
144
- Already proposed in [ambient-presence.md](../ambient-presence.md) §`tt events --follow`. We adopt it verbatim and extend it with the new event types. Restating the contract for completeness:
155
+ ### Layer 4 Generic wait primitive
156
+
157
+ `wait_for_events` is the canonical long-poll over the event log. Non-mutating: no `touchWaitingMember`, no claim path, no waiter-grace bookkeeping.
145
158
 
146
159
  ```
147
- tt events [path] --follow
148
- [--after <event_seq>]
149
- [--event <type[,type...]>]
150
- [--severity info|page]
151
- [--target self|any|<agent_id>]
152
- [--json|--text]
160
+ wait_for_events(
161
+ room_id,
162
+ after_event_seq?,
163
+ event_type?, // single string or array
164
+ target_agent_id?, // 'self' | 'any' | <agent_id>
165
+ max_wait_ms? // bounded by server policy; default follows the existing long-poll config
166
+ ) -> { events: RoomEvent[], cursor_event_seq }
153
167
  ```
154
168
 
155
- Stdout is line-oriented and flushed after each event. When the CLI detects one of the supported harness identities (`TT_HARNESS_AGENT_ID`, `CLAUDECODE`, `CODEX_THREAD_ID`, `GEMINI_CLI`, or `OPENCODE`), default stdout is one JSON object per line. When no harness is detected and the CLI falls back to a human identity, default stdout is human-readable text. `--json` forces JSON lines for scripts; `--text` forces human-readable text even from a harness. Stderr is diagnostics only. Exit on `SIGTERM`/`SIGHUP` with a final flush.
169
+ `target_agent_id` semantics for filter:
170
+
171
+ - `'self'` (default): include events whose `to_agent_id` matches the caller's resolved agent identity, plus message events with `to_agent_id = NULL` (broadcast). This is the chat default.
172
+ - `'any'`: no filter on recipient. Used by debuggers, humans tailing the room, or consumers that need full visibility.
173
+ - `<agent_id>`: events targeted at a specific other agent. Used by humans watching directed chat.
156
174
 
157
- The new `--severity` and `--target` flags filter `note_added` events specifically. A guardian-style harness uses two logical channels:
175
+ `wait_for_events` returns the next event(s) past the cursor, up to the wait deadline, then closes. Callers re-call with the new `cursor_event_seq` to continue. Unlike `wait_for_turn`, this primitive is **observer-safe**: it never mutates room state or queue position.
158
176
 
177
+ `send_message` is a thin write that appends a `message_sent` event. It runs in `withImmediateTransaction` to maintain `event_seq` ordering and emits exactly one event per call:
178
+
179
+ ```
180
+ send_message(
181
+ room_id,
182
+ body,
183
+ to_agent_id?,
184
+ delivery_hint?
185
+ ) -> { event_seq, event_id, created_at }
159
186
  ```
160
- # Page channel — loud. One line here means "interrupt the holder now."
161
- tt events --follow --event note_added --severity page --target self --json
162
187
 
163
- # Buffer channel quiet. Write to a local cursor/log and read at the next safe boundary.
164
- tt events --follow --event member_joined,member_left,note_added --severity info --target any --json
188
+ There is no `wait_for_message` primitive. `tt msg recv` and any higher-level message UX are wrappers around `wait_for_events` with `event_type=message_sent` filter. Keeping a single wait primitive avoids duplicate filter logic and a second cursor concept.
189
+
190
+ ### Layer 5 — CLI ergonomic surface
191
+
192
+ Three commands, all wrappers over Layers 2 and 4:
193
+
194
+ ```
195
+ tt msg send <recipient> "<body>" [--interrupt] [--room]
196
+ tt msg recv [--from <agent>] [--wait|--follow] [--after <event_seq>] [--json|--text]
197
+ tt events [--wait|--follow] [--event <type[,type...]>] [--after <event_seq>] [--target self|any|<agent_id>] [--json|--text]
165
198
  ```
166
199
 
167
- The distinction is not just severity in the JSON payload. Some harness glue, notably Claude Code's Monitor tool, treats *every stdout line* from a watched process as a conversation notification. Page output is suitable for that loud path. Buffer output is not; it should be drained into a local cursor/log and summarized by the foreground agent at handoff or another safe boundary.
200
+ Resolution rules:
168
201
 
169
- ### Wait pattern not changed by this plan
202
+ - `tt msg send <recipient>`: `<recipient>` may be a full `agent_id` (`codex:5c11d1e8`), a display name (`codex`), or the literal `room` for broadcast (`to_agent_id=null`). A bare display name resolves to the unique active member with that display name; ambiguity returns `ambiguous_recipient` listing candidates. No active member with that name returns `unknown_member`.
203
+ - `--interrupt`: sets `delivery_hint=interrupt`. Default is `normal`.
204
+ - `tt msg recv`: defaults to `target=self` (direct + broadcast). `--from <agent>` filters by sender. `--follow` long-polls until SIGTERM/SIGHUP, emitting one JSON-line event per stdout flush. `--wait` runs the same long-poll once, exits after the next matching batch, and is the portable path for harnesses that can observe process completion but not per-line output.
205
+ - `tt events --wait|--follow`: lower-level surface tailing arbitrary event types (not just messages). Output format follows identity (harness -> JSON lines, human -> text), per the `ambient-presence.md` contract.
170
206
 
171
- This proposal does **not** replace `wait_for_turn` with event-stream notifications for stick availability.
207
+ **`tt msg recv --follow` is the canonical v1 agent-side receive path.** Any harness that can spawn a long-lived child process and watch its stdout (Claude Code's Monitor today, future Codex/Gemini/OpenCode equivalents) integrates by spawning `tt msg recv --follow` and routing the JSON-line stream into its own context-injection pipeline. No plugin code is required; the harness's existing subprocess mechanism IS the integration. For harnesses without that mechanism, two fallback v1 paths exist: (a) the operator runs `tt msg recv --follow` in a tmux pane and human-relays the message, or (b) the agent calls `wait_for_events` between turns via MCP. `tt events --follow` is for humans, debuggers, and tooling that wants the full event stream rather than just messages.
172
208
 
173
- The current queue mechanics rely on `wait_for_turn` as both the claim authority and the wait-intent heartbeat. In the service today, `wait_for_turn` updates `last_wait_at`, and normal `release_stick` only reserves the stick for a candidate whose wait is recent according to `waiterGraceMs`. A participant that probes once and then sleeps only on event lines for minutes can therefore change the normal reservation behavior.
209
+ ### Layer 6 Receive-consumer contract
174
210
 
175
- The v1 skill should continue to teach direct `wait_for_turn` long-polls. A future event-driven wait helper can use the same event stream as an advisory wakeup channel, but it first needs an explicit wait-intent design, for example `waiting_since` / `wait_intent_expires_at` renewed by a helper. That follow-up helper would still run `wait_for_turn` to claim; event lines would be a bell, not the lock.
211
+ **v1 does not require any plugin work.** Layer 5's `tt msg recv --follow` subprocess pattern is the first receive consumer. A future plugin is just another consumer with richer routing. This layer documents the shared contract for both the CLI subprocess and later harness-native consumers lifecycle, cursor persistence, replay, and backpressure.
176
212
 
177
- ### Layer 4 — Harness-side: background process + stdout-line notification
213
+ The protocol guarantees: at-least-once event delivery via `wait_for_events`, with monotonic `event_seq` ordering and SIGTERM-clean wrappers. The CLI subprocess uses that primitive directly; plugins, when they exist, should do the same.
178
214
 
179
- The actual integration in Claude Code:
215
+ Consumer responsibilities:
180
216
 
181
- 1. Foreground agent starts the page channel in the background and attaches Monitor to that process only.
182
- 2. Foreground agent starts the buffer channel separately, without Monitor, writing JSON lines plus the last seen `event_seq` to a local cursor/log.
183
- 3. Page lines are injected into the conversation immediately. Buffer lines are read deliberately at handoff prep or another safe boundary.
184
- 4. The agent still uses `wait_for_turn` for turn ownership; these channels are notification surfaces only.
217
+ 1. **Receive loop.** Long-running task or subprocess that polls `wait_for_events` with `target=self`, persists `cursor_event_seq` across restarts, and routes incoming `message_sent` events to terminal output, app-level notification, or model-context injection.
218
+ 2. **App-level versus model-context routing.** The consumer decides per `delivery_hint`. `interrupt` may inject directly into the active conversation where the harness supports that (Claude Code's Monitor model already does this for `tt msg recv --follow` lines); `normal` may write to terminal output, a status indicator, a counter, or a deferred-injection buffer that the model drains explicitly.
219
+ 3. **Send invocation.** Expose a deliberate send path the model can invoke (skill, slash command, native tool). Should call the CLI or MCP tool, not reimplement the wire format.
220
+ 4. **Cursor recovery.** Persist last-seen `event_seq` to a known location (under `~/.local/share/talking-stick` or a harness-specific equivalent) so process restart resumes cleanly. The protocol's at-least-once guarantee means duplicates are possible across restarts; consumers dedupe on `event_id`. `tt msg recv --follow` supports `--after <event_seq>` for the same purpose.
221
+ 5. **Replay coalescing.** On reconnect with a far-behind cursor, the consumer should coalesce or summarize old messages rather than firehose them into the model context. Suggested policy: deliver the most recent N at full fidelity; older are summarized as a count.
222
+ 6. **Backpressure.** If the downstream (Monitor, terminal, IDE, etc.) blocks, the consumer's reading from `wait_for_events` should not. Drop-with-warning or buffer to disk; do not let pressure propagate to the protocol layer.
185
223
 
186
- Equivalents in other harnesses:
224
+ ### Delivery grades per harness (what to expect on day 1, no plugins required)
187
225
 
188
- - **Codex** spawn `tt events --follow` as a child process; map stdout lines to `attach` events on the active task. Same shape, different transport name.
189
- - **OpenCode / Gemini** — long-poll via shell subprocess; whatever the harness calls "background output" is the right hook.
190
- - **Plain shell (human operator)** `tt events --follow` in a tmux pane, with human-readable output by default; add `--json` only when piping to a script.
226
+ | Harness | v1 receive (no plugin) | v1 send | Plugin enhancement (later) |
227
+ |---------|------------------------|---------|----------------------------|
228
+ | **Claude Code** | mid-tool-loop injection via Monitor watching `tt msg recv --follow`; each JSON line becomes a system reminder | model-deliberate via skill prescription; CLI passthrough | richer routing (per-author mute, statusline, deferred-buffer for `normal`) |
229
+ | **Codex** | event delivery via operator-run `tt msg recv --follow` in a tmux pane, plus model-visible delivery only when Codex explicitly polls `wait_for_events` between turns | model-deliberate via tool call to MCP `send_message` or CLI passthrough | automatic mid-tool-loop injection if the harness eventually exposes a stdout-watch hook |
230
+ | **Gemini** | durable pipe via the same fallback paths as Codex | same as Codex | TBD with harness owner |
231
+ | **OpenCode** | durable pipe via the same fallback paths as Codex | same as Codex | TBD with harness owner |
232
+ | **Human (`tt msg recv --follow`)** | tmux pane / terminal output | bare CLI invocation | n/a |
191
233
 
192
- The protocol does not need to know which harness is on the other end. The contract is: line in, notification out.
234
+ The portable guarantee is: at-least-once event delivery with monotonic ordering. *Any* harness or operator that calls `wait_for_events` (directly, via MCP, or via the `tt msg recv --follow` subprocess) will eventually see every relevant event. The mid-tool-loop interrupt-class fast path is harness-by-harness; for v1, only Claude Code has it via Monitor + the CLI subprocess. Other harnesses get a functional pipe immediately, but model-visible delivery may be between-turn or human-relayed until those harnesses expose stdout-watch or plugin hooks.
193
235
 
194
- ## Token-cost analysis
236
+ ## Wait pattern — not changed by this plan
237
+
238
+ This proposal does not replace `wait_for_turn` with event-driven notifications for stick availability.
195
239
 
196
- Concrete numbers, since this was the explicit question.
240
+ `wait_for_turn` still updates `last_wait_at` and gates reservation behavior via `waiterGraceMs` (`src/service.ts:1689-1701`). A waiter that sleeps purely on `wait_for_events` for minutes can change reservation eligibility. The skill should continue to teach direct `wait_for_turn` long-polling for ownership; advisory event wakeups for waiters require explicit wait-intent state (e.g. `wait_intent_expires_at`), which is a separate design.
197
241
 
198
- **Idle cost: zero.** A backgrounded `tt events --follow` is a child process. It consumes no model tokens while running. The harness keeps a process handle, not a context-window slot.
242
+ ## Token-cost analysis
243
+
244
+ Concrete numbers, since this is the live concern.
199
245
 
200
- **Per-event cost: small and proportional.** Each page line that Monitor surfaces becomes a notification message in the conversation. A typical page line is on the order of 100–300 tokens with a capped body preview; each `member_joined` buffer line is under 80 tokens and should not enter the conversation until the agent chooses to summarize buffered context.
246
+ **Idle cost: zero.** A backgrounded `wait_for_events` long-poll consumes no model tokens while running.
201
247
 
202
- **Annual budget for a busy room:** at, say, 50 events per active hour (very high typical rooms see far fewer), that is ~5 000 tokens per hour of room activity surfaced into the holder's context. By comparison, a single `get_room_state` call already costs several hundred tokens, and most agents call it on every turn. The watcher is cheap.
248
+ **Per-event cost: small and proportional.** A typical `message_sent` event in the chat use case is 60–300 tokens of body. A receive process that injects into model context spends roughly that per message. A terminal-only receive process costs no model tokens until an operator or model deliberately drains it. Under the v1 CLI-only path on Claude Code's Monitor, every JSON line is a system reminder by default, so `normal` and `interrupt` differ only once a consumer adds routing policy; v1 Claude Code treats both as immediate.
203
249
 
204
- **Where it actually gets expensive:**
250
+ **Per-handoff replacement savings:** one skipped stick round-trip in our own dogfooding is ~600 tokens of handoff structure (`status`, `next_action`, `artifacts[]`, `open_questions[]`). A typical chat exchange of 4–6 messages totals 240–600 tokens of body. Net savings only materialize when the discussion is short enough to not justify a full handoff but long enough to span multiple round-trips. That's the sweet spot Vignette H targets.
205
251
 
206
- - If `note_added` events inline full bodies. Don't — keep full bodies in `list_notes`; page output gets only a capped preview.
207
- - If watchers don't filter. A holder doesn't need its own `claim` events echoed back. Filter via `--event` and `--target`.
208
- - If many idle agents all run watchers on the same room. The cost is per-agent-context, not per-room. With N agents, N watchers, N copies of each event in N contexts. Acceptable for small N (≤4 typical), worth revisiting if rooms grow.
209
- - If the watcher is replaced with a polling loop that calls `get_room_events` every few seconds. That defeats the design — the foreground agent burns tokens making the polling decisions. The watcher's whole point is to push that decision to a child process and only spend tokens on actual events.
252
+ **Where it gets expensive (still true from the original):**
210
253
 
211
- **On long-poll vs. push:** the watcher process can implement long-poll internally (block on SQLite for up to N seconds, emit on change, re-block). That makes the *process* efficient. But from the *foreground agent's* perspective, push and long-poll are identical both surface as a stdout line when something happens. So the choice is a server-side performance question, not a token question. v1 can use a 1-second SQLite poll inside `tt events --follow` and still cost zero foreground tokens between events.
254
+ - Receive processes inlining full bodies into context for `normal`-class messages without buffering. Mitigation under v1 CLI-only is best-effort: Monitor will inject every line; harnesses that want `normal`-vs-`interrupt` distinction need a smarter consumer (plugin, wrapper process, or operator-side filtering before injection).
255
+ - Receive processes not filtering. A holder doesn't need its own outbound messages echoed back. The CLI defaults to `target=self` which excludes the caller's own outbound, so this is mostly handled by the v1 default.
256
+ - Many idle agents all running receive subprocesses on the same room. Cost is per-agent context. With N agents, N receive contexts. Acceptable for small N (≤4 typical).
257
+ - Replay floods on subprocess/plugin restart. Use `--after <last_seen_event_seq>` to resume; the harness or plugin should coalesce per Layer 6 §5 if the gap is large.
212
258
 
213
259
  ## Concrete surface changes
214
260
 
215
261
  ### Service / DB
216
262
 
217
- 1. Add `member_joined`, `member_left`, `note_added`, `note_resolved` to the `event_type` enum in `src/types.ts`. The SQLite `room_events.event_type` column is already free text, but migration 5 should add nullable metadata columns for note events: `note_id`, `severity`, `target_agent_id`, and `body_preview`.
218
- 2. `joinPath`, `leaveRoom`, `addNote` all call `appendEvent(...)` in their respective transactions. They already run inside the same transaction as the state mutation, so atomicity is free.
219
- 3. Add optional `severity: "info" | "page"` and `target_agent_id` columns to the `notes` table. Default severity `info`. Existing rows back-fill to `info`, no `target`.
220
- 4. New service method `resolveNote({ agent_id, room_id, note_id })` that flips `resolved_at` / `resolved_by_agent_id` and emits `note_resolved`. Optional for v1 but cheap.
263
+ 1. Migration 5: `ALTER TABLE room_events ADD COLUMN payload_json TEXT NULL`. Existing rows back-fill to `NULL`.
264
+ 2. New service method `sendMessage({ agent_id, room_id, body, to_agent_id?, delivery_hint? })`. Validates body length (≤4096 bytes UTF-8), validates `to_agent_id` is null or a known room member id (loose check this is routing, not auth), appends one `message_sent` event with `payload_json`. Returns `{ event_seq, event_id, created_at }`.
265
+ 3. New service method `waitForEvents({ agent_id?, room_id, after_event_seq?, event_type?, target_agent_id?, max_wait_ms? })`. Non-mutating long-poll. Server policy bounds `max_wait_ms` using the same configurable long-poll defaults as the existing wait APIs; the design does not bake in a dogfooding timeout. Returns events past the cursor that match filters, plus a new `cursor_event_seq` for the next call.
266
+ 4. Optional v1.1: emit `member_joined` / `member_left` / `note_added` events alongside their primary actions. Each writes its small `payload_json` describing reason/turn/etc.
221
267
 
222
- ### CLI
223
-
224
- 1. `tt notes add --severity page --target <agent_id> "body"` — pass-through of new fields.
225
- 2. `tt events --follow [--after N] [--event T,...] [--severity ...] [--target ...] [--json|--text]` — per Layer 3. Default output follows detected identity: supported harness envs get JSON lines; human fallback gets readable text. `--target self` requires participant identity; observer-only shells must use `--target any` or an explicit agent id.
226
- 3. `tt notes resolve <note_id>` — wraps `resolveNote`. Optional for v1.
268
+ `waitForEvents` does **not** call `touchWaitingMember`. It is observer-safe by design. (This addresses Codex's blocker #1 from the original review at issue #11.)
227
269
 
228
270
  ### MCP
229
271
 
230
- 1. `add_note` tool gains optional `severity` and `target_agent_id` parameters.
231
- 2. `get_room_events` already accepts `after_event_seq` and `limit`; no signature change needed for the new event types they are additive on the discriminated union.
232
- 3. New MCP tool `resolve_note` optional for v1.
272
+ 1. New tool `send_message(room_id, body, to_agent_id?, delivery_hint?)` `{ event_seq, event_id, created_at }`.
273
+ 2. New tool `wait_for_events(room_id, after_event_seq?, event_type?, target_agent_id?, max_wait_ms?)` `{ events, cursor_event_seq }`. Resolves caller identity from MCP transport when `target_agent_id='self'`.
274
+ 3. `get_room_events` already exists and now returns events with their `payload_json` populated for new event types. No signature change.
275
+
276
+ ### CLI
277
+
278
+ 1. `tt msg send <recipient> <body> [--interrupt] [--room]` — resolves recipient, calls `send_message`. Passes `--room` for broadcast.
279
+ 2. `tt msg recv [--from <agent>] [--follow] [--after <event_seq>] [--json|--text]` — wraps `wait_for_events` with `event_type=message_sent` and optional sender filter. Default `target=self`.
280
+ 3. `tt events --follow [--event <type,...>] [--after <event_seq>] [--target self|any|<agent>] [--json|--text]` — generic event tailer. Output format follows identity per `ambient-presence.md`.
281
+
282
+ `tt msg recv --follow` is intended to be spawned as a long-lived child process by harnesses that watch subprocess stdout. SIGTERM and SIGHUP cause a final cursor flush and clean exit. The implementation should write each event as a single line of JSON (or human-readable text per identity), flushed immediately.
233
283
 
234
284
  ### Skill
235
285
 
236
- The shipped `skills/talking-stick/SKILL.md` gets a section: *"While you hold the stick, you may receive `note_added` events with severity `page`. Read the page preview, call `list_notes` if you need the full body, decide whether to act now or at the next handoff boundary, and resolve it when addressed."* Include the mirror instruction for non-holders: *"To get the holder's attention without taking the stick, use `add_note` with severity `page`."*
286
+ The shipped `skills/talking-stick/SKILL.md` gets a new top-level section between §4 (While waiting) and §5 (While holding the stick), titled **"§4.5 Out-of-band messaging"**. Drafting deferred to the implementation pass; bullet-level content:
237
287
 
238
- The skill's wait guidance should remain direct `wait_for_turn` long-polling. Event-stream wakeups for stick availability are future work and require explicit wait-intent state before they can replace the current polling cadence.
288
+ - Use `tt msg send` / `send_message` for transient conversational exchange. Use `add_note` for durable hints, observations, or pointers that should outlive the moment.
289
+ - `--interrupt` is for "the holder really should look at this now." Use it sparingly. The receiving harness or plugin decides what "interrupt" means; the protocol does not enforce attention.
290
+ - Messages are not private. Any room member can read any message via `get_room_events` or `tt events --follow --target any`. `to_agent_id` is routing, not ACL.
291
+ - Messages do not replace handoffs. `pass_stick` / `release_stick` with a structured `Handoff` is still required at turn boundaries. Use chat for *discussion*; use a handoff for *transfer of work*.
292
+ - The recommended v1 receive pattern is a `tt msg recv --follow` child process spawned by your harness (Claude Code) or operator (humans, Codex, Gemini, OpenCode in tmux). After releasing or passing, that subprocess keeps running and surfaces incoming messages between turns. Stay in the wait-for-turn loop in parallel.
293
+
294
+ ### Plugins *(future enhancement, not v1)*
295
+
296
+ This repo owns the protocol substrate, the CLI, the skill prose, and the **receive-consumer contract** (Layer 6 above, formalized as `docs/receive-consumer-contract.md` or equivalent in a future implementation pass).
297
+
298
+ **v1 ships and works without any plugin.** After the v1 CLI surface lands, all of the following are usable without harness-owner plugin work:
299
+
300
+ - **Claude Code**: spawn `tt msg recv --follow` and let Monitor inject each JSON line as a system reminder. The skill teaches the model to send via `tt msg send` (or the MCP `send_message` tool).
301
+ - **Codex / Gemini / OpenCode**: the operator runs `tt msg recv --follow` in a tmux pane and relays interrupt-class messages, OR the agent calls `wait_for_events` between turns via MCP. Both are v1 paths; neither requires harness-owner integration work.
302
+ - **Human**: `tt msg send/recv` and `tt events --follow` are sufficient with no further setup.
303
+
304
+ Per-harness plugins are a **post-v1** layer for harness owners that want richer ambient UX (statusline indicators, per-author mute, smarter routing of `normal` vs `interrupt`). When they ship, they live in their respective ecosystems:
305
+
306
+ - **Claude Code** plugin: ships in `claude-code-plugins` (or equivalent) when there's appetite for behavior beyond Monitor + CLI.
307
+ - **Codex** plugin: ships from the Codex extension surface when stdout-watch or equivalent injection becomes available.
308
+ - **Gemini, OpenCode** plugins: deferred to harness-owner work.
309
+
310
+ A reference Claude Code plugin is **not** bundled in the npm tarball at any stage, to avoid coupling protocol releases to a single harness's UX.
239
311
 
240
312
  ## Tradeoffs and open questions
241
313
 
242
- - **Why notes-with-severity instead of a separate `messages` primitive?** Notes already are durable, addressable, and resolvable. Adding two fields is cheaper than a parallel messaging table, and the harness-side UX is identical. The risk is conceptual creep: notes today are "things the holder should consider before handoff," and pages stretch that toward "things the holder must consider now." Worth naming explicitly so the skill reflects it.
243
- - **Should `member_joined` be page-able by default?** No. Joins are too frequent (humans `cd` and out, harnesses restart). Default to `info`. A specific guardian setup can choose to elevate joins by spawning a second `tt events --follow --event member_joined` stream and rendering it loudly.
244
- - **Heartbeat-stale and takeover-available as events.** Tempting the watcher could fire one line when the current holder goes stale and another when takeover unlocks. But these are derived states, not log entries; if we synthesize them into the event stream we either need a separate "synthetic events" cursor or we mix derived and persisted events on the same `event_seq`. Recommendation for v1: do not synthesize. Agents that care should continue to long-poll `wait_for_turn`; a future event-driven wait helper can handle derived deadlines after wait intent is modeled explicitly.
245
- - **Backpressure.** `tt events --follow` writes to stdout. If the harness Monitor stops draining (paused conversation, hit a tool error), the pipe will block. The watcher should use a small bounded write buffer and drop-with-warning rather than blocking forever; design parity with `tail -F`.
246
- - **Authentication of `target_agent_id`.** Anyone in the room can post a note targeted at anyone else. That matches the existing notes contract (any member can post). If we ever need permissioning, it is a separate concern from this design.
247
- - **Crash recovery.** Watcher process dies → harness restarts it with `--after <last_seen_event_seq>`. The harness must persist the last-seen seq across restarts; for Claude Code that means the agent writes it to a known location (a memory entry, or a `.talking-stick/` cursor file) before the watcher exits cleanly. Worth specifying in the skill.
248
- - **Multiple watchers per harness.** Layer 4 suggests two logical streams (page channel + buffer channel). That can be two child processes per agent, or one wrapper process that routes output to separate destinations. The important invariant is routing: page can be Monitor-injected; buffer should not be.
249
- - **Resolution semantics.** Does a `pass`/`release` auto-resolve outstanding pages? Probably not — the next holder may still need them. But we should mark them as "delivered to holder X at turn Y" so the page does not re-page on every turn. Either a `delivered_at` column on notes, or a per-turn dedup at the harness side. v1 recommendation: dedup at the harness side using `note_id`, no schema change.
314
+ - **Why event-only messages instead of a `room_messages` table?** Storage parity with handoff state, single cursor, and append-only ordering match the use case. A messages table would duplicate the indexing and create a second resumable stream. Cost: messages share retention with events; if events are GC'd, so are old messages. v1 retention for events is "forever" (matches today); revisit if the room grows hot enough to need TTL.
315
+ - **Why `delivery_hint` instead of `priority`/`severity`/`page`?** "Hint" makes the protocol's actual guarantee explicit: it's a routing suggestion to the consumer, not a server-enforced behavior. "Page" carries on-call connotations that overstate the contract. "Priority" implies a ladder; we only have two levels.
316
+ - **Why no rate limit?** Agents are not chatbots; the design-discussion traffic pattern is single-digit messages per minute, well below anything that would burst SQLite. If usage demonstrates need (e.g. a buggy agent loops on `send_message`), we can add a per-author cap as a typed `rate_limited` error in v1.x. Documented threshold for revisiting: 30 messages / author / minute sustained.
317
+ - **Why no read receipts / acks / threading?** Each adds a primitive and a column. None are needed for the stated use case; all can be added later non-breakingly if proven necessary.
318
+ - **Authentication of message targeting.** Any member can target any other; matches notes today. If permissioning is ever needed, it is a separate concern.
319
+ - **Crash recovery.** Receive subprocess (or plugin) dies → harness or operator restarts it with `--after <last_seen_event_seq>`. The CLI's `tt msg recv --follow` accepts that flag directly. Cursor persistence is the harness's or plugin's responsibility recommended path is `~/.local/share/talking-stick/cursor-<agent_id>.json` or a harness-specific equivalent. At-least-once is the contract; consumers dedupe on `event_id`.
320
+ - **Resolution semantics on chat.** No resolve concept for messages. The exchange is its own resolution; if action is required, it goes into a note (`add_note`) or a handoff. Notes still resolve.
321
+ - **Operator killability.** Each receive loop should be killable by SIGTERM with a final cursor flush. `tt msg recv --follow` should honor this; harness-side plugins should too.
250
322
 
251
323
  ## What this plan does NOT yet specify
252
324
 
253
- This document is a design proposal for review, not an implementation specification. The following decisions are deliberately deferred to post-review so that reviewer pushback can shape them. Treat each as a real gap an implementer would hit on day one:
254
-
255
- 1. **Exact migration 5 DDL.** The repo already has a `schema_migrations` runner in `src/db.ts`. Implementation still needs the exact `ALTER TABLE` sequence for `notes` and `room_events`, plus downgrade expectations for older binaries that see unknown event types.
256
- 2. **`member_left` trigger sites.** Explicit `leaveRoom` is straightforward. Inactivity/GC is not: members can become inactive through liveness checks and opportunistic cleanup. Implementation must decide which transitions emit a durable `member_left`, which reason strings are valid (`"left"`, `"inactive"`, `"gc"`), and how to avoid repeated leave/reactivate noise.
257
- 3. **`member_joined` debounce semantics.** `joinPath` upserts and touches members today, so harness reconnects, repeated `tt join` invocations, and idle CLI sessions can all retrigger the join path for the same `agent_id`. Implementation must define when a `member_joined` event is durable (first insert vs. inactive-after-meaningful-absence) and what reason strings distinguish first-join from reactivation. Without this, page-tier subscribers to joins get retrigger noise on every reconnect.
258
- 4. **Skill prose, in full.** The "Skill" subsection above paraphrases the addition. The actual shipped skill text is what every harness reads on every relevant turn, so it needs to be drafted, reviewed, and kept as tight as the rest of `skills/talking-stick/SKILL.md`.
259
- 5. **Page dedup persistence.** The plan recommends harness-side dedup of pages by `note_id` plus a cursor file for crash recovery. But the dedup set itself is in-memory; after a watcher restart the dedup set is empty, and a still-unresolved page can re-fire on the next event from the buffered tail. Either the cursor file must persist the dedup set too, or the server must offer a "since last delivery to <agent_id>" filter. Pick one before relying on dedup.
260
- 6. **Test plan.** Not enumerated. At minimum: schema migration on a populated v0.1.x database, event ordering when `addNote` and `release_stick` race, filter correctness on `tt events --follow` for each `--event` / `--severity` / `--target` combination, resume-after-cursor with new event types interleaved with existing ones, and behavior when a watcher's stdout is blocked.
325
+ Inputs to the implementation pass, not blockers for review:
261
326
 
262
- These are not blockers for the design discussion they are inputs to the implementation pass. The architectural decision to keep stick waiting on `wait_for_turn` is intentional; event-driven waiting belongs in a separate wait-intent design.
327
+ 1. **Exact migration 5 DDL and the `payload_json` schema sketch per event type.** Migration is one column; the discriminated payloads need a documented schema (probably a TypeScript discriminated union mirrored in `docs/event-payloads.md`).
328
+ 2. **`member_left` / `member_joined` trigger sites and reason strings.** As in the original — explicit `leaveRoom` is straightforward; inactivity GC and `joinPath`-on-reactivation need defined emission rules to avoid noise.
329
+ 3. **Recipient resolution corner cases for `tt msg send <name>`.** Display-name collisions across active members, short prefixes, exact-match on `agent_id`, broadcast typo guards.
330
+ 4. **Cursor persistence format for consumers.** Recommended: a per-agent cursor file under `~/.local/share/talking-stick`, written by the receive loop on each batch.
331
+ 5. **Skill prose, in full.** Drafted in the implementation pass; reviewed before merge.
332
+ 6. **Test plan.** At minimum: migration 5 on a populated v0.1.x DB; `payload_json` round-trip; `wait_for_events` filter correctness for each `event_type` × `target` combination; `wait_for_events` resume across cursor; `send_message` body cap rejection; `send_message` of a `message_sent` event interleaved with a `release_stick` event ordering; CLI recipient resolution (full id, display name, ambiguous, room-broadcast, unknown).
263
333
 
264
334
  ## Staged rollout
265
335
 
266
- 1. **Schema + service:** add new event types, emit on `joinPath`/`leaveRoom`/`addNote`. No CLI/MCP changes yet. Watchers that already follow the event log start seeing the new events immediately.
267
- 2. **`tt events --follow` extended:** add `--severity` and `--target` filters. CLI tests for filter shape and resume-after-cursor.
268
- 3. **`add_note` severity + targeting:** schema change to `notes`, plumbed through service, CLI, MCP. Skill updated.
269
- 4. **Skill rewrite:** holder-side and watcher-side guidance, including page-vs-buffer routing and the reminder that `wait_for_turn` remains the ownership path.
270
- 5. **Optional: `resolve_note` + `note_resolved` event.** Lets pages stop re-paging across handoffs without harness-side dedup.
271
- 6. **Optional: derived-event synthesis** (`takeover_available`, `lease_stale`) as a follow-up document if observer demand justifies it.
336
+ Each stage is independently shippable. **Stages 1–3 are v1; users have a fully working message pipe after stage 3 with no plugin work required.**
337
+
338
+ 1. **Substrate.** Migration 5 + service `sendMessage` / `waitForEvents` + tests. No CLI/MCP yet. Proves the wire format and the observer-safety claim.
339
+ 2. **MCP surface.** `send_message` and `wait_for_events` MCP tools. `get_room_events` returns `payload_json`. Agents using MCP-only harnesses (Codex, Gemini, OpenCode) can already send and explicitly poll messages between turns at this stage.
340
+ 3. **CLI surface.** `tt msg send` / `tt msg recv --follow` / `tt events --follow`. Recipient resolution. Skill update §4.5. Documentation pass. **End of v1.** Claude Code can now do mid-tool-loop chat by spawning `tt msg recv --follow` under Monitor; humans and other harnesses get the same durable pipe via terminal/tmux plus explicit between-turn polling.
341
+ 4. **Optional: presence event emission.** `member_joined` / `member_left` / `note_added` so consumers can follow presence and notes through the same stream. Decoupled — receivers not depending on these events keep working.
342
+ 5. **Optional: per-harness plugins.** The Layer 6 consumer contract is already load-bearing for `tt msg recv --follow`; plugins become relevant when harness owners want richer ambient UX. None required for v1.
343
+ 6. **Optional: per-recipient mute, message resolution, threading.** Only if real usage demonstrates need.
344
+ 7. **Optional: server-side rate limit.** Only if the documented threshold is hit.
272
345
 
273
346
  ## What we are not building
274
347
 
275
- - No new transport. No websockets, no MCP resource subscriptions in v1 (see [ambient-presence.md](../ambient-presence.md) "Out of scope").
276
- - No write authority changes. Pages are signals, not commands.
277
- - No automatic takeover on page. Takeover stays a deliberate act gated on `claim_expires_at`.
278
- - No harness-specific notification format. Harnesses consume JSON lines; humans get readable text by default.
279
- - No event-driven replacement for `wait_for_turn`. Stick availability remains governed by the existing wait/claim path until a separate wait-intent design exists.
348
+ - No write authority changes. Messages are signals, not commands.
349
+ - No automatic takeover on interrupt. Takeover stays gated on `claim_expires_at`.
350
+ - No harness-specific notification format. Consumers use the same events; harnesses decide UX.
351
+ - No event-driven replacement for `wait_for_turn`. Stick availability remains governed by the existing wait/claim path.
352
+ - No second event log or cursor. `event_seq` is the cursor; `payload_json` is the discriminator.
353
+ - No `to_agent_id` privacy. Routing only.
354
+ - No bundled per-harness plugin in the npm tarball for v1.
280
355
 
281
356
  ## Summary
282
357
 
283
- The talking stick already has the right primitives for *what* coordinates (rooms, leases, handoffs) and the right primitives for *who* coordinates (agent identity, membership). It is missing primitives for *what flows alongside the work in progress*. This proposal closes that gap with the minimum viable additions:
358
+ The amendment narrows the protocol surface and delivers a working chat path via the CLI subprocess pattern in v1. Per-harness plugins are a future enhancement, not a prerequisite. The stable substrate is:
359
+
360
+ - One new column (`room_events.payload_json`).
361
+ - One new event type (`message_sent`, with optional `member_joined` / `member_left` / `note_added` follow-ups).
362
+ - One new write tool (`send_message`).
363
+ - One new read tool (`wait_for_events`) — observer-safe, no waiter-grace mutation.
364
+ - Three CLI commands (`tt msg send`, `tt msg recv`, `tt events --follow`). `tt msg recv --follow` is the v1 canonical agent-side receive path: spawn it as a child process and watch its stdout.
365
+ - One receive-consumer contract document for the CLI subprocess and for harness owners who later want richer UX than the CLI subprocess pattern delivers.
284
366
 
285
- - Four new event types so the existing log carries presence and notes.
286
- - Two new fields on notes so non-holders can distinguish a hint from a page.
287
- - One existing CLI surface (`tt events --follow`) extended with two filter flags.
288
- - A documented harness pattern (page channel + quiet buffer channel) that costs zero idle tokens and proportional per-event tokens.
367
+ In v1 with no plugin: Claude Code chats mid-tool-loop via Monitor watching `tt msg recv --follow`. Codex, Gemini, OpenCode get the same message pipe immediately, but model-visible delivery is explicit polling or operator relay until those harnesses grow stdout-watch integration. Humans chat with bare CLI invocations. Plugins later add per-harness polish (statusline, mute, smarter routing) without changing the protocol.
289
368
 
290
- Everything else transports, write-authority changes, derived events, push channels is deferred until the simple version is in use and we know what is actually missing.
369
+ Vignette H is the case the design exists for: two agents talking through a question while one of them holds the stick, no formal handoff per round-trip, full audit trail in the event log. Everything else is in service of that.