talking-stick 0.1.4 → 0.2.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.
@@ -0,0 +1,30 @@
1
+ # Receive Consumer Contract
2
+
3
+ `send_message` appends `message_sent` events to the room event log. `wait_for_events` is the canonical receive primitive. CLI consumers (`tt msg recv --wait`, `tt msg recv --follow`) and future harness-native consumers should share the same cursor and retry rules.
4
+
5
+ ## Delivery
6
+
7
+ - Delivery is at least once. Consumers must tolerate duplicates after restart.
8
+ - `event_seq` is monotonic per database and is the receive cursor.
9
+ - Consumers should persist the highest processed `event_seq` after each emitted batch.
10
+ - Directed messages are routing only. Any room member can read messages through `get_room_events` or `tt events --target any`.
11
+
12
+ ## Receive Modes
13
+
14
+ - Use `tt msg recv --follow --after <cursor>` when the harness can monitor stdout from a long-running child. Each output line is one `RoomEvent` JSON object.
15
+ - Use `tt msg recv --wait --after <cursor>` when the harness can only notice process completion. The process exits after the next matching batch or timeout; restart it with the latest processed cursor.
16
+ - If no `--after` is supplied in `--wait` or `--follow` mode, the CLI starts from the current event tail to avoid flooding a new consumer with history.
17
+ - A one-shot `tt msg recv --after <cursor>` is a non-blocking drain operation.
18
+
19
+ ## Filtering
20
+
21
+ - `target=self` receives direct messages to the caller plus broadcasts from other agents. It excludes the caller's own broadcasts.
22
+ - `target=any` receives all matching messages and is intended for audit/debug views.
23
+ - `--from <agent>` resolves a full `agent_id` or unambiguous active display name and is enforced server-side.
24
+
25
+ ## Consumer Responsibilities
26
+
27
+ - Keep `wait_for_turn` running separately. Message receive processes do not claim or grant the stick.
28
+ - Decide how to surface `delivery_hint=interrupt`; the server only records the hint.
29
+ - Dedupe on `event_id` if restart replay is possible.
30
+ - Treat message bodies as room-visible text, not private data.
@@ -0,0 +1,85 @@
1
+ # Talking Stick 0.2.0
2
+
3
+ Date: 2026-04-30
4
+
5
+ Minor release that adds **out-of-band messaging** between agents in a room. Two agents — typically the holder and a non-holder, or two non-holders — can now exchange short conversational messages without passing the stick. The protocol substrate is one new column on `room_events`; the surface is two MCP tools (`send_message`, `wait_for_events`) and three CLI commands (`tt msg send`, `tt msg recv`, `tt events --wait|--follow`).
6
+
7
+ The feature targets **Vignette H** from the design doc: holder + non-holder alternating short messages on a sub-question, paying ~80 tokens of body per round-trip instead of the ~600 tokens of structured-handoff scaffolding when the discussion would have otherwise required `pass_stick`/`release_stick` ping-pong.
8
+
9
+ ## Added
10
+
11
+ ### Out-of-band messaging
12
+
13
+ Three CLI commands. All wrap the same MCP/service primitives.
14
+
15
+ ```bash
16
+ tt msg send <recipient|room> "<body>" [--interrupt] [--stdin] [--path DIR]
17
+ tt msg recv [--wait|--follow] [--from agent] [--after N] [--target self|any|agent] [--path DIR]
18
+ tt events --wait|--follow [--event TYPE[,TYPE]] [--target self|any|agent]
19
+ ```
20
+
21
+ - `<recipient>` is a full `agent_id`, an unambiguous active display name (`codex`, `claude`), or the literal `room` for broadcast.
22
+ - `--interrupt` marks the message time-sensitive. The receiving harness or operator decides whether to act on it now; the protocol delivers, the consumer routes.
23
+ - `tt msg recv --follow` is a long-running tail (one JSON line per event) suited to harnesses that can monitor child stdout (Claude Code Monitor, terminals).
24
+ - `tt msg recv --wait` exits on the next matching batch — ideal for harnesses that can launch a background command and notice when it completes; restart with `--after <last_event_seq>` to resume.
25
+
26
+ The matching MCP tools are `send_message` (write) and `wait_for_events` (observer-safe long-poll). `get_room_events` now returns parsed `payload` for `message_sent` rows alongside the existing `handoff` field for legacy event types.
27
+
28
+ ### Observer-safe event long-poll
29
+
30
+ `wait_for_events` is non-mutating by contract. It does not call `touchMember`, `touchKnownMember`, `touchWaitingMember`, or `purgeExpiredIdleRooms`. The only read it performs at entry is `requireRoom` for fail-fast on a missing room. Non-holders can long-poll the event log freely without disturbing the `last_wait_at` / `last_seen_at` bookkeeping that drives turn fairness.
31
+
32
+ ### `getLatestEventSeq` cursor helper
33
+
34
+ `tt msg recv --wait|--follow` defaults to "start at now" — the highest `event_seq` in the room at startup time — so first-launch receivers don't replay history. Implemented as a single `SELECT MAX(event_seq) FROM room_events WHERE room_id = ?`, exposed on the service and commands layer. Operators wire `--after $LAST_SEQ` from their own bookkeeping when resuming after a crash; cursor persistence to disk is the harness's or plugin's responsibility per the receive-consumer contract.
35
+
36
+ ### Splice-at-1 parser repair for boolean flags after positionals
37
+
38
+ The CLI parser consumes the next non-`--` token as a flag's value. That meant `tt msg send codex --interrupt body` would parse `interrupt="body"` and leave `codex` as the only positional. The handler now repairs this case by splicing the consumed value at positional index 1 (after the recipient), so `tt msg send codex --interrupt "body"` produces `recipient=codex`, `body="body"`, `delivery_hint=interrupt`. The existing `normalizeBooleanFlag` helper unshifts to the front (correct for `tt notes add --stdin` etc.); this new repair handles the `<positional> <body>` shape without weakening the generic parser.
39
+
40
+ ### Receive-consumer contract
41
+
42
+ [`docs/receive-consumer-contract.md`](../receive-consumer-contract.md) documents the lifecycle expected of any receive consumer (CLI subprocess, future plugin, harness adapter): cursor persistence, replay coalescing on far-behind cursors, backpressure (drop-with-warning, never block the read loop), at-least-once delivery + dedupe on `event_id`, SIGTERM clean exit with the last cursor flushed to stderr.
43
+
44
+ ## Skill
45
+
46
+ The bundled skill at [`skills/talking-stick/SKILL.md`](../../skills/talking-stick/SKILL.md) gains a new §4.5 *Out-of-band messaging* section:
47
+
48
+ - send via `tt msg send <recipient> "<body>"` or MCP `send_message`
49
+ - receive via `tt msg recv --wait` or `--follow` depending on what the harness can observe
50
+ - when to message (conversational, ephemeral, between live processes) vs note (durable, resolvable artifacts) vs handoff (transfer of work)
51
+ - messages are routing not ACL — `to_agent_id` is delivery, not privacy
52
+ - messages do not grant the stick — paging the holder gets attention, not write authority
53
+ - a `tt msg recv` subprocess does not replace `wait_for_turn` — keep waiting for your turn in parallel
54
+
55
+ The skill also picks up a small note in §1 reminding harnesses that sibling `tt msg recv --wait` / `--follow` subprocesses may be running and should be left alone unless the operator says otherwise.
56
+
57
+ ## Migration
58
+
59
+ `room_events` gains a nullable `payload_json TEXT` column (migration #5). `ALTER TABLE ADD COLUMN` is O(1) on populated tables; existing rows back-fill to NULL; legacy event types continue to write NULL via the optional `payload?` parameter on `appendEvent`. No action required by operators on upgrade — the column is invisible to v0.1.x clients.
60
+
61
+ ## Design properties pinned by tests
62
+
63
+ - **Self-broadcast exclusion** for `target=self`: caller's own broadcasts (`to_agent_id IS NULL AND from_agent_id = caller`) are excluded from their default receive view; the audit path (`target=any`) still includes them. The SQL clause is `(event_type='message_sent' AND (to_agent_id = ? OR (to_agent_id IS NULL AND from_agent_id != ?)))` — pinned by tests 13a/13b/13c in `tests/oob-substrate.test.ts`.
64
+ - **Closed-room behavior** (deferred): `wait_for_events` on a `state='closed'` room returns empty after deadline; no short-circuit, no error. Pinned by test 19a so a future `close_room` PR has to opt in to changing it.
65
+ - **Body cap.** 4096 bytes UTF-8; rejected with typed `message_too_large`. No silent truncation.
66
+ - **Sender filter** (`from_agent_id`) applied server-side, so cursor advancement under `tt msg recv --from <agent>` is honest.
67
+ - **SIGTERM lifecycle** for `tt msg recv --follow` covered by a real subprocess test that spawns the CLI, sends a message via MCP, asserts the JSON line on stdout, sends SIGTERM, and verifies clean exit.
68
+
69
+ ## Verification
70
+
71
+ ```bash
72
+ npm run typecheck # clean
73
+ npm run build # clean
74
+ npm test # 263 passed (was 257 before fd67873)
75
+ tt --help | grep "tt msg" # tt msg send/recv visible
76
+ ```
77
+
78
+ End-to-end dogfood pre-release: claude (MCP) ↔ codex (MCP) ↔ codex (CLI) round-tripped 6 messages (events 668→675) in the live coordination room with zero `pass_stick`/`release_stick` calls during the chat. Both `target=self` (excludes own broadcast) and `target=any` (includes own broadcast) verified in production.
79
+
80
+ ## Plan and design
81
+
82
+ - [`docs/plans/out-of-band-signaling.md`](../plans/out-of-band-signaling.md) — converged design (commit 8069d84)
83
+ - [`docs/plans/out-of-band-signaling-implementation.md`](../plans/out-of-band-signaling-implementation.md) — file-by-file build sequence with R1/R2 review history
84
+ - [`docs/receive-consumer-contract.md`](../receive-consumer-contract.md) — lifecycle, cursor, replay, backpressure
85
+ - [`skills/talking-stick/SKILL.md`](../../skills/talking-stick/SKILL.md) §4.5 — when-to-message-vs-note-vs-handoff guidance
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "talking-stick",
3
- "version": "0.1.4",
3
+ "version": "0.2.0",
4
4
  "description": "MCP coordination server for path-scoped agent handoffs.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: talking-stick
3
- description: Use when working in a repo that coordinates multiple agent harnesses with Talking Stick (`tt` / `talking-stick`), or when the user asks you to avoid parallel work, wait your turn, pass structured handoffs, or coordinate with Claude, Codex, Gemini, or OpenCode in the same workspace. Also use when a workspace contains a `.talking-stick/` marker or when the MCP tools `list_rooms`, `join_path`, `leave_room`, `kick_member`, `wait_for_turn`, `heartbeat`, `release_stick`, `pass_stick`, `takeover_stick`, `get_room_state`, `get_room_events`, `add_note`, or `list_notes` are available.
3
+ description: Use when working in a repo that coordinates multiple agent harnesses with Talking Stick (`tt` / `talking-stick`), or when the user asks you to avoid parallel work, wait your turn, pass structured handoffs, or coordinate with Claude, Codex, Gemini, or OpenCode in the same workspace. Also use when a workspace contains a `.talking-stick/` marker or when the MCP tools `list_rooms`, `join_path`, `leave_room`, `kick_member`, `wait_for_turn`, `heartbeat`, `release_stick`, `pass_stick`, `takeover_stick`, `get_room_state`, `get_room_events`, `send_message`, `wait_for_events`, `add_note`, or `list_notes` are available.
4
4
  ---
5
5
 
6
6
  This skill teaches a harness how to behave in a Talking Stick workspace.
@@ -26,7 +26,9 @@ Do not use this skill for ordinary single-agent work in repos that are not using
26
26
 
27
27
  ### 1. Check that Talking Stick is actually available
28
28
 
29
- Prefer the Talking Stick MCP tools when they are available. If they are not available but the `tt` CLI is on `PATH`, use the CLI instead (`tt list`, `tt join`, `tt leave`, `tt kick`, `tt wait`, `tt state`, `tt release`, `tt pass`, `tt assign`, `tt take`). Do not treat missing MCP tools alone as proof that coordination is unavailable.
29
+ Prefer the Talking Stick MCP tools when they are available. If they are not available but the `tt` CLI is on `PATH`, use the CLI instead (`tt list`, `tt join`, `tt leave`, `tt kick`, `tt wait`, `tt state`, `tt release`, `tt pass`, `tt assign`, `tt take`, `tt msg`). Do not treat missing MCP tools alone as proof that coordination is unavailable.
30
+
31
+ Some workspaces may also have sibling receive processes running `tt msg recv --wait` or `tt msg recv --follow`; leave them alone unless the operator explicitly asks you to stop or restart them.
30
32
 
31
33
  If coordination is required and neither the MCP tools nor the `tt` CLI are available, say so briefly and ask the user whether they want to install or enable Talking Stick first. Do not pretend coordination is active.
32
34
 
@@ -95,6 +97,26 @@ The wait is for *active* non-mutating work, not idle sleep. Re-read the holder's
95
97
 
96
98
  When you do take the stick, first read the attached handoff and load any useful `artifacts[]`, then run `list_notes` once so you see what other members left for you. The owner's turn is the right place to act on a note, not to debate it with its author mid-turn.
97
99
 
100
+ ### 4.5 Out-of-band messaging
101
+
102
+ The talking stick guarantees single-writer authority over shared workspace state. It is not a chat protocol. For transient signaling -- paging the holder, asking a quick question, or broadcasting awareness -- use messages.
103
+
104
+ **Send.** `tt msg send <recipient> "<body>"` or MCP `send_message`. Recipient is a full `agent_id`, an unambiguous active display name, or the literal `room` for broadcast. `--interrupt` marks the message as time-sensitive; the receiver decides whether to act on it now.
105
+
106
+ **Receive.** Use the receive mode your harness can observe. If it can monitor stdout from a long-running child, run `tt msg recv --follow`; each incoming event lands as one JSON line. If it can only notice that a background command completed, run `tt msg recv --wait --after <last_event_seq>`; it exits on the next matching batch, then you start it again with the returned cursor. Restart with `--after <last_event_seq>` to resume.
107
+
108
+ **When to message vs note vs handoff.**
109
+
110
+ - **Message** when the exchange is conversational, ephemeral, and tied to processes that are currently online: design questions, "are you about to break X?", live coordination.
111
+ - **Note** (`tt notes add`) when the artifact should outlive the moment: a finding the next holder should consider at handoff, or an observation that survives process churn.
112
+ - **Handoff** (release/pass with a structured payload) when transferring work. Messages do not replace handoffs; they live alongside them.
113
+
114
+ **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 an ACL.
115
+
116
+ **Messages do not grant the stick.** A non-holder paging the holder does not gain write authority. The holder may act on the message immediately or defer until handoff.
117
+
118
+ **Stay in the wait loop in parallel.** A `tt msg recv --wait` or `--follow` subprocess does not replace `wait_for_turn`. Keep waiting for your turn; messages are a side channel.
119
+
98
120
  ### 5. While holding the stick
99
121
 
100
122
  If the task may run longer than a few minutes, heartbeat periodically.