talking-stick 0.1.3 → 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.
- package/README.md +32 -3
- package/dist/cli/event-stream.js +124 -0
- package/dist/cli/msg-commands.js +81 -0
- package/dist/cli/output.js +3 -1
- package/dist/cli/registry.js +11 -1
- package/dist/cli/room-commands.js +18 -3
- package/dist/commands.js +15 -0
- package/dist/config.js +3 -0
- package/dist/db.js +7 -0
- package/dist/mcp-server.js +32 -0
- package/dist/service.js +166 -5
- package/docs/plans/out-of-band-signaling-implementation.md +854 -0
- package/docs/plans/out-of-band-signaling.md +255 -176
- package/docs/receive-consumer-contract.md +30 -0
- package/docs/releases/0.1.4.md +68 -0
- package/docs/releases/0.2.0.md +85 -0
- package/docs/talking-stick-plan.md +1 -1
- package/package.json +1 -1
- package/skills/talking-stick/SKILL.md +25 -3
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
An MCP coordination server that lets multiple AI coding agents share a single workspace without stepping on each other. One agent holds the stick at a time; handoffs carry structured context so the next agent doesn't have to re-derive it.
|
|
4
4
|
|
|
5
|
-
**Version:** 0.
|
|
5
|
+
**Version:** 0.2.0. Multi-process-safe (SQLite WAL), liveness-aware, no daemon. Supports Claude Code, Codex CLI, Gemini CLI, and OpenCode out of the box. Two agents in the same room can also chat out-of-band — without passing the stick — via `tt msg send/recv` and the matching MCP tools.
|
|
6
6
|
|
|
7
7
|
## Quickstart
|
|
8
8
|
|
|
@@ -46,7 +46,7 @@ That's the whole workflow. They negotiate turns automatically, hand off structur
|
|
|
46
46
|
|
|
47
47
|
| Method | Command | Notes |
|
|
48
48
|
|---|---|---|
|
|
49
|
-
| **From npm** | `npm i -g talking-stick` | Published as `0.
|
|
49
|
+
| **From npm** | `npm i -g talking-stick` | Published as `0.2.0`. Requires Node ≥ 22. |
|
|
50
50
|
| **From GitHub** | `npm i -g github:mostlydev/talking-stick` | Tracks the `master` branch; builds on install via the `prepare` hook. |
|
|
51
51
|
| **From source** | `git clone … && npm install && npm link` | For contributors. |
|
|
52
52
|
|
|
@@ -97,10 +97,13 @@ heartbeat — prove liveness while holding the stick
|
|
|
97
97
|
release_stick — normal handoff to the next fair waiter, with structured Handoff
|
|
98
98
|
pass_stick — explicit handoff to a named agent
|
|
99
99
|
takeover_stick — deliberate claim when the prior holder is gone/stuck
|
|
100
|
+
kick_member — evict an idle member whose process is gone
|
|
100
101
|
get_room_state — authoritative state projection
|
|
101
102
|
get_room_events — audit log of turn transitions
|
|
102
103
|
add_note — leave an async observation for the current owner
|
|
103
104
|
list_notes — read notes left for the room
|
|
105
|
+
send_message — out-of-band chat into the room event log (direct or broadcast)
|
|
106
|
+
wait_for_events — observer-safe long-poll over the event log with type/target/sender filters
|
|
104
107
|
```
|
|
105
108
|
|
|
106
109
|
A workspace maps to a room — usually the `git` root or nearest project marker — so two agents `cd`'d anywhere under the same repo join the same room automatically.
|
|
@@ -118,6 +121,30 @@ While you wait your turn you may still need to flag something to the current own
|
|
|
118
121
|
- `list_notes` returns notes for the room; readers can paginate with `after_note_id` and opt into resolved entries with `include_resolved`.
|
|
119
122
|
- Notes are for observations and pointers, not for coordinating shared edits. Shared workspace changes still require holding the stick.
|
|
120
123
|
|
|
124
|
+
## Out-of-band messaging
|
|
125
|
+
|
|
126
|
+
The stick guarantees single-writer authority over shared workspace state. It is **not** a chat protocol. When two agents need to talk — design questions, "are you about to break X?", live coordination — use messages instead of churning the stick.
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
tt msg send <recipient|room> "<body>" [--interrupt] [--stdin]
|
|
130
|
+
tt msg recv [--wait|--follow] [--from agent] [--after N] [--target self|any|agent]
|
|
131
|
+
tt events --wait|--follow [--event TYPE[,TYPE]] [--target self|any|agent]
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
- `<recipient>` is a full `agent_id`, an unambiguous active display name (`codex`, `claude`), or the literal `room` for broadcast.
|
|
135
|
+
- `--interrupt` marks the message time-sensitive; receivers decide whether to act on it now.
|
|
136
|
+
- `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).
|
|
137
|
+
- `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.
|
|
138
|
+
- `wait_for_events` is observer-safe: it never mutates room state, so non-holders can use it freely without disturbing turn-fairness bookkeeping.
|
|
139
|
+
|
|
140
|
+
**When to message vs note vs handoff.**
|
|
141
|
+
|
|
142
|
+
- **Message** — conversational, ephemeral, between live processes. Six round-trips of "what about line 84?" cost about as much as one structured handoff and zero stick churn.
|
|
143
|
+
- **Note** (`tt notes add`) — durable, resolvable artifacts. Leave a note when the next holder should consider something at handoff, or when the observation should outlive the conversation.
|
|
144
|
+
- **Handoff** (`release_stick` / `pass_stick`) — transfer of work. Messages do not replace handoffs; they live alongside them.
|
|
145
|
+
|
|
146
|
+
**`to_agent_id` is routing, not ACL.** Any room member can read any message via `get_room_events` or `tt events --follow --target any`. Messages are not private. They also do not grant the stick — a non-holder paging the holder gets attention, not write authority.
|
|
147
|
+
|
|
121
148
|
## How installation works per harness
|
|
122
149
|
|
|
123
150
|
`tt install` installs both pieces a harness needs: the MCP server registration and the bundled `talking-stick` skill.
|
|
@@ -160,7 +187,9 @@ tt leave [path] # leave the room for p
|
|
|
160
187
|
tt wait [path] [--timeout 30s] # block until your turn
|
|
161
188
|
tt try [path] # non-blocking claim attempt
|
|
162
189
|
tt state [path] # full room state
|
|
163
|
-
tt events [path] [--after N] [--limit N]
|
|
190
|
+
tt events [path] [--after N] [--limit N] [--wait|--follow] [--event TYPE[,TYPE]] [--target self|any|agent] # room event log; --wait/--follow long-polls
|
|
191
|
+
tt msg send <recipient|room> <body...> [--interrupt] [--stdin] [--path DIR] # send an OOB message
|
|
192
|
+
tt msg recv [--wait|--follow] [--from agent] [--after N] [--target self|any|agent] [--path DIR] # receive OOB messages
|
|
164
193
|
tt release [path] --status TEXT --next-action TEXT # normal handoff
|
|
165
194
|
tt pass [path] --status TEXT --next-action TEXT # pass/end your turn
|
|
166
195
|
tt assign <target|next> [path] --status TEXT --next-action TEXT # explicit handoff
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { ProtocolError } from "../errors.js";
|
|
2
|
+
import { getStringOption, parseOptionalInteger, parseWaitTimeout } from "./parser.js";
|
|
3
|
+
import { shouldUseJson } from "./output.js";
|
|
4
|
+
export async function runEventStream(runtime, parsed, identity, roomId, options) {
|
|
5
|
+
const follow = parsed.options.has("follow");
|
|
6
|
+
const wait = parsed.options.has("wait");
|
|
7
|
+
if (follow && wait) {
|
|
8
|
+
throw new Error("Pass only one of --wait or --follow.");
|
|
9
|
+
}
|
|
10
|
+
const tailMode = follow || wait || options.force_tail_cursor;
|
|
11
|
+
const explicitAfter = parseOptionalInteger(parsed, "after");
|
|
12
|
+
const afterEventSeq = explicitAfter ??
|
|
13
|
+
(tailMode ? runtime.commands.getLatestEventSeq({ room_id: roomId }) : 0);
|
|
14
|
+
const targetAgentId = resolveTargetFilter(runtime, identity, roomId, getStringOption(parsed, "target") ?? options.default_target);
|
|
15
|
+
const fromAgentId = resolveOptionalAgentSelector(runtime, identity, roomId, getStringOption(parsed, "from"));
|
|
16
|
+
const waitInput = {
|
|
17
|
+
agent_id: identity.agent_id,
|
|
18
|
+
room_id: roomId,
|
|
19
|
+
after_event_seq: afterEventSeq,
|
|
20
|
+
event_type: options.event_type,
|
|
21
|
+
target_agent_id: targetAgentId,
|
|
22
|
+
from_agent_id: fromAgentId,
|
|
23
|
+
max_wait_ms: follow || wait ? parseWaitTimeout(parsed) : 0
|
|
24
|
+
};
|
|
25
|
+
if (!follow) {
|
|
26
|
+
const result = await runtime.commands.waitForEvents(waitInput);
|
|
27
|
+
writeEventLines(parsed, result.events);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
await followEvents(runtime, parsed, waitInput);
|
|
31
|
+
}
|
|
32
|
+
export function resolveOptionalAgentSelector(runtime, identity, roomId, raw) {
|
|
33
|
+
if (!raw) {
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
return resolveAgentSelector(runtime, identity, roomId, raw);
|
|
37
|
+
}
|
|
38
|
+
export function resolveAgentSelector(runtime, identity, roomId, raw) {
|
|
39
|
+
const members = runtime.commands.getRoomState({
|
|
40
|
+
room_id: roomId,
|
|
41
|
+
agent_id: identity.agent_id
|
|
42
|
+
}).members;
|
|
43
|
+
const exact = members.find((member) => member.agent_id === raw);
|
|
44
|
+
if (exact) {
|
|
45
|
+
return exact.agent_id;
|
|
46
|
+
}
|
|
47
|
+
const candidates = members.filter((member) => member.status === "active" && member.display_name === raw);
|
|
48
|
+
if (candidates.length === 1) {
|
|
49
|
+
return candidates[0].agent_id;
|
|
50
|
+
}
|
|
51
|
+
if (candidates.length > 1) {
|
|
52
|
+
throw new ProtocolError("ambiguous_recipient", `Multiple active room members match '${raw}'.`, { candidates: candidates.map((member) => member.agent_id) });
|
|
53
|
+
}
|
|
54
|
+
throw new ProtocolError("unknown_recipient", `No active room member matches '${raw}'.`);
|
|
55
|
+
}
|
|
56
|
+
export function parseEventTypeFilter(value) {
|
|
57
|
+
if (!value) {
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
const values = value
|
|
61
|
+
.split(",")
|
|
62
|
+
.map((item) => item.trim())
|
|
63
|
+
.filter(Boolean);
|
|
64
|
+
if (values.length === 0) {
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
return values.length === 1 ? values[0] : values;
|
|
68
|
+
}
|
|
69
|
+
export function writeEventLines(parsed, events) {
|
|
70
|
+
for (const event of events) {
|
|
71
|
+
const line = shouldUseJson(parsed)
|
|
72
|
+
? JSON.stringify(event)
|
|
73
|
+
: formatEventLine(event);
|
|
74
|
+
process.stdout.write(`${line}\n`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
export function formatEventLine(event) {
|
|
78
|
+
if (event.event_type === "message_sent") {
|
|
79
|
+
const target = event.to_agent_id ?? "room";
|
|
80
|
+
const hint = event.payload?.delivery_hint === "interrupt" ? " [interrupt]" : "";
|
|
81
|
+
return `[${event.created_at}] ${event.from_agent_id ?? "-"} -> ${target}${hint}: ${event.payload?.body ?? ""}`;
|
|
82
|
+
}
|
|
83
|
+
const reason = event.reason ? ` (${event.reason})` : "";
|
|
84
|
+
const target = event.from_agent_id && event.to_agent_id
|
|
85
|
+
? `${event.from_agent_id} -> ${event.to_agent_id}`
|
|
86
|
+
: event.to_agent_id
|
|
87
|
+
? `-> ${event.to_agent_id}`
|
|
88
|
+
: event.from_agent_id ?? "-";
|
|
89
|
+
return `[${event.created_at}] ${event.event_type} ${target}${reason}`;
|
|
90
|
+
}
|
|
91
|
+
async function followEvents(runtime, parsed, input) {
|
|
92
|
+
let cursor = input.after_event_seq ?? 0;
|
|
93
|
+
let shouldExit = false;
|
|
94
|
+
const markExit = () => {
|
|
95
|
+
shouldExit = true;
|
|
96
|
+
};
|
|
97
|
+
process.once("SIGTERM", markExit);
|
|
98
|
+
process.once("SIGHUP", markExit);
|
|
99
|
+
process.once("SIGINT", markExit);
|
|
100
|
+
try {
|
|
101
|
+
while (!shouldExit) {
|
|
102
|
+
const result = await runtime.commands.waitForEvents({
|
|
103
|
+
...input,
|
|
104
|
+
after_event_seq: cursor
|
|
105
|
+
});
|
|
106
|
+
if (result.events.length > 0) {
|
|
107
|
+
writeEventLines(parsed, result.events);
|
|
108
|
+
cursor = result.cursor_event_seq;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
finally {
|
|
113
|
+
process.off("SIGTERM", markExit);
|
|
114
|
+
process.off("SIGHUP", markExit);
|
|
115
|
+
process.off("SIGINT", markExit);
|
|
116
|
+
process.stderr.write(`cursor_event_seq=${cursor}\n`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
function resolveTargetFilter(runtime, identity, roomId, raw) {
|
|
120
|
+
if (raw === "self" || raw === "any") {
|
|
121
|
+
return raw;
|
|
122
|
+
}
|
|
123
|
+
return resolveAgentSelector(runtime, identity, roomId, raw);
|
|
124
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { deriveCliIdentity } from "./identity.js";
|
|
2
|
+
import { readAllStdin } from "./handoff.js";
|
|
3
|
+
import { hasOption } from "./parser.js";
|
|
4
|
+
import { printResult } from "./output.js";
|
|
5
|
+
import { resolveAgentSelector, runEventStream } from "./event-stream.js";
|
|
6
|
+
import { resolveSessionForNotes } from "./session.js";
|
|
7
|
+
export async function handleMsgCommand(runtime, parsed) {
|
|
8
|
+
const [subcommand, ...rest] = parsed.positionals;
|
|
9
|
+
if (!subcommand) {
|
|
10
|
+
throw new Error("Usage: tt msg <send|recv> [...]. See `tt --help` for details.");
|
|
11
|
+
}
|
|
12
|
+
const subParsed = {
|
|
13
|
+
name: `msg ${subcommand}`,
|
|
14
|
+
positionals: rest,
|
|
15
|
+
options: parsed.options
|
|
16
|
+
};
|
|
17
|
+
switch (subcommand) {
|
|
18
|
+
case "send":
|
|
19
|
+
await handleMsgSendCommand(runtime, subParsed);
|
|
20
|
+
return;
|
|
21
|
+
case "recv":
|
|
22
|
+
await handleMsgRecvCommand(runtime, subParsed);
|
|
23
|
+
return;
|
|
24
|
+
default:
|
|
25
|
+
throw new Error(`Unknown msg subcommand: ${subcommand}`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
async function handleMsgSendCommand(runtime, parsed) {
|
|
29
|
+
const identity = deriveCliIdentity(parsed);
|
|
30
|
+
const session = resolveSessionForNotes(runtime, parsed, identity);
|
|
31
|
+
const usesRoomFlag = hasOption(parsed, "room");
|
|
32
|
+
repairBooleanFlag(parsed, "room", 0);
|
|
33
|
+
repairBooleanFlag(parsed, "interrupt", usesRoomFlag ? 0 : 1);
|
|
34
|
+
const recipientSelector = usesRoomFlag ? "room" : parsed.positionals[0];
|
|
35
|
+
if (!recipientSelector) {
|
|
36
|
+
throw new Error("Usage: tt msg send <recipient|room> <body...> [--interrupt] [--stdin].");
|
|
37
|
+
}
|
|
38
|
+
const bodyStart = usesRoomFlag ? 0 : 1;
|
|
39
|
+
const positionalBody = parsed.positionals.slice(bodyStart).join(" ");
|
|
40
|
+
const body = positionalBody.length > 0
|
|
41
|
+
? positionalBody
|
|
42
|
+
: hasOption(parsed, "stdin")
|
|
43
|
+
? await readAllStdin()
|
|
44
|
+
: "";
|
|
45
|
+
if (body.length === 0) {
|
|
46
|
+
throw new Error("Message body is required (pass as a positional or use --stdin to read from stdin).");
|
|
47
|
+
}
|
|
48
|
+
const toAgentId = recipientSelector === "room"
|
|
49
|
+
? null
|
|
50
|
+
: resolveAgentSelector(runtime, identity, session.room_id, recipientSelector);
|
|
51
|
+
const result = runtime.commands.sendMessage(identity, {
|
|
52
|
+
room_id: session.room_id,
|
|
53
|
+
body,
|
|
54
|
+
to_agent_id: toAgentId,
|
|
55
|
+
delivery_hint: hasOption(parsed, "interrupt") ? "interrupt" : "normal"
|
|
56
|
+
});
|
|
57
|
+
printResult(parsed, result, () => {
|
|
58
|
+
const target = toAgentId ?? "room";
|
|
59
|
+
const hint = hasOption(parsed, "interrupt") ? " interrupt" : "";
|
|
60
|
+
return `Sent${hint} message ${shortEventId(result.event_id)} to ${target}.`;
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
async function handleMsgRecvCommand(runtime, parsed) {
|
|
64
|
+
const identity = deriveCliIdentity(parsed);
|
|
65
|
+
const session = resolveSessionForNotes(runtime, parsed, identity);
|
|
66
|
+
await runEventStream(runtime, parsed, identity, session.room_id, {
|
|
67
|
+
event_type: "message_sent",
|
|
68
|
+
default_target: "self",
|
|
69
|
+
force_tail_cursor: false
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
function repairBooleanFlag(parsed, key, insertAt) {
|
|
73
|
+
const value = parsed.options.get(key);
|
|
74
|
+
if (typeof value === "string") {
|
|
75
|
+
parsed.positionals.splice(insertAt, 0, value);
|
|
76
|
+
parsed.options.set(key, true);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
function shortEventId(eventId) {
|
|
80
|
+
return eventId.slice(0, 8);
|
|
81
|
+
}
|
package/dist/cli/output.js
CHANGED
|
@@ -126,7 +126,9 @@ Commands:
|
|
|
126
126
|
tt wait [path] [--timeout 30s]
|
|
127
127
|
tt try [path]
|
|
128
128
|
tt state [path]
|
|
129
|
-
tt events [path] [--after N] [--limit N]
|
|
129
|
+
tt events [path] [--after N] [--limit N] [--wait|--follow] [--event TYPE[,TYPE]] [--target self|any|agent]
|
|
130
|
+
tt msg send <recipient|room> <body...> [--interrupt] [--stdin] [--path DIR]
|
|
131
|
+
tt msg recv [--wait|--follow] [--from agent] [--after N] [--target self|any|agent] [--path DIR]
|
|
130
132
|
tt release [path] (--status TEXT --next-action TEXT | --stdin)
|
|
131
133
|
tt pass [path] (--status TEXT --next-action TEXT | --stdin)
|
|
132
134
|
tt assign <target|next> [path] (--status TEXT --next-action TEXT | --stdin)
|
package/dist/cli/registry.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { runStdioServer } from "../index.js";
|
|
2
2
|
import { runGuardCommand } from "./guardian.js";
|
|
3
3
|
import { runInstallCommand, runInstallSkillCommand, runSelfUpdateCommand, runUninstallCommand, runUninstallSkillCommand } from "./install-commands.js";
|
|
4
|
+
import { handleMsgCommand } from "./msg-commands.js";
|
|
4
5
|
import { handleNotesCommand } from "./notes-commands.js";
|
|
5
6
|
import { handleEventsCommand, handleJoinCommand, handleKickCommand, handleLeaveCommand, handleListCommand, handleStateCommand, handleWhoAmICommand } from "./room-commands.js";
|
|
6
7
|
import { handleAssignCommand, handlePassCommand, handleReleaseCommand, handleTakeCommand, handleWaitCommand } from "./turn-commands.js";
|
|
@@ -127,10 +128,19 @@ export const COMMAND_REGISTRY = [
|
|
|
127
128
|
needsRuntime: true,
|
|
128
129
|
startupMaintenance: true,
|
|
129
130
|
internal: false,
|
|
130
|
-
usage: "tt events [path] [--after N] [--limit N]",
|
|
131
|
+
usage: "tt events [path] [--after N] [--limit N] [--wait|--follow] [--event TYPE[,TYPE]] [--target self|any|agent]",
|
|
131
132
|
description: "Show room events.",
|
|
132
133
|
handler: ({ runtime, parsed }) => handleEventsCommand(requireRuntime(runtime), parsed)
|
|
133
134
|
},
|
|
135
|
+
{
|
|
136
|
+
name: "msg",
|
|
137
|
+
needsRuntime: true,
|
|
138
|
+
startupMaintenance: true,
|
|
139
|
+
internal: false,
|
|
140
|
+
usage: "tt msg <send|recv> [...]",
|
|
141
|
+
description: "Send or receive transient messages on a room's event stream.",
|
|
142
|
+
handler: ({ runtime, parsed }) => handleMsgCommand(requireRuntime(runtime), parsed)
|
|
143
|
+
},
|
|
134
144
|
{
|
|
135
145
|
name: "wait",
|
|
136
146
|
needsRuntime: true,
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { deriveCliIdentity, resolveCliIdentity } from "./identity.js";
|
|
2
2
|
import { removeCliSession, removeCliSessionsForRoom, resolveCliSessionPath } from "../index.js";
|
|
3
3
|
import { stopGuardian } from "./guardian.js";
|
|
4
|
-
import { getStringOption, hasOption, parseOptionalInteger } from "./parser.js";
|
|
4
|
+
import { getStringOption, hasOption, normalizeBooleanFlag, parseOptionalInteger } from "./parser.js";
|
|
5
5
|
import { formatRelativeTime, printResult } from "./output.js";
|
|
6
|
+
import { parseEventTypeFilter, runEventStream } from "./event-stream.js";
|
|
6
7
|
import { resolveSessionForReads, upsertSessionFromJoin } from "./session.js";
|
|
7
8
|
export function handleListCommand(runtime, parsed) {
|
|
8
9
|
const contextPath = parsed.positionals[0] ?? process.cwd();
|
|
@@ -31,7 +32,11 @@ export function handleJoinCommand(runtime, parsed) {
|
|
|
31
32
|
});
|
|
32
33
|
upsertSessionFromJoin(identity, joined);
|
|
33
34
|
printResult(parsed, joined, () => {
|
|
34
|
-
|
|
35
|
+
const lines = [`Joined ${joined.canonical_path} as ${joined.agent_id}`];
|
|
36
|
+
if (joined.warning) {
|
|
37
|
+
lines.push(`Warning: ${joined.warning}`);
|
|
38
|
+
}
|
|
39
|
+
return lines.join("\n");
|
|
35
40
|
});
|
|
36
41
|
}
|
|
37
42
|
export function handleLeaveCommand(runtime, parsed) {
|
|
@@ -122,9 +127,19 @@ export function handleStateCommand(runtime, parsed) {
|
|
|
122
127
|
return lines.join("\n");
|
|
123
128
|
});
|
|
124
129
|
}
|
|
125
|
-
export function handleEventsCommand(runtime, parsed) {
|
|
130
|
+
export async function handleEventsCommand(runtime, parsed) {
|
|
131
|
+
normalizeBooleanFlag(parsed, "wait");
|
|
132
|
+
normalizeBooleanFlag(parsed, "follow");
|
|
126
133
|
const identity = deriveCliIdentity(parsed);
|
|
127
134
|
const session = resolveSessionForReads(runtime, parsed, identity);
|
|
135
|
+
if (hasOption(parsed, "wait") || hasOption(parsed, "follow")) {
|
|
136
|
+
await runEventStream(runtime, parsed, identity, session.room_id, {
|
|
137
|
+
event_type: parseEventTypeFilter(getStringOption(parsed, "event")),
|
|
138
|
+
default_target: "any",
|
|
139
|
+
force_tail_cursor: false
|
|
140
|
+
});
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
128
143
|
const events = runtime.commands.getRoomEvents({
|
|
129
144
|
room_id: session.room_id,
|
|
130
145
|
agent_id: identity.agent_id,
|
package/dist/commands.js
CHANGED
|
@@ -82,6 +82,21 @@ export class TalkingStickCommands {
|
|
|
82
82
|
getRoomEvents(input) {
|
|
83
83
|
return this.service.getRoomEvents(input);
|
|
84
84
|
}
|
|
85
|
+
sendMessage(identity, input) {
|
|
86
|
+
return this.service.sendMessage({
|
|
87
|
+
agent_id: identity.agent_id,
|
|
88
|
+
room_id: input.room_id,
|
|
89
|
+
body: input.body,
|
|
90
|
+
to_agent_id: input.to_agent_id,
|
|
91
|
+
delivery_hint: input.delivery_hint
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
waitForEvents(input) {
|
|
95
|
+
return this.service.waitForEvents(input);
|
|
96
|
+
}
|
|
97
|
+
getLatestEventSeq(input) {
|
|
98
|
+
return this.service.getLatestEventSeq(input);
|
|
99
|
+
}
|
|
85
100
|
addNote(identity, input) {
|
|
86
101
|
return this.service.addNote({
|
|
87
102
|
agent_id: identity.agent_id,
|
package/dist/config.js
CHANGED
|
@@ -6,6 +6,9 @@ export const defaultPolicy = {
|
|
|
6
6
|
claimTtlMs: 20 * 60 * 1000,
|
|
7
7
|
waitForTurnMaxWaitMs: 30 * 1000,
|
|
8
8
|
waitForTurnPollMs: 250,
|
|
9
|
+
waitForEventsMaxWaitMs: 30 * 1000,
|
|
10
|
+
waitForEventsPollMs: 250,
|
|
11
|
+
waitForEventsBatchLimit: 100,
|
|
9
12
|
presenceTtlMs: 4 * 60 * 60 * 1000,
|
|
10
13
|
waiterGraceMs: 10 * 1000,
|
|
11
14
|
idleRoomTtlMs: 7 * 24 * 60 * 60 * 1000
|
package/dist/db.js
CHANGED
|
@@ -96,6 +96,13 @@ const migrations = [
|
|
|
96
96
|
up: `
|
|
97
97
|
ALTER TABLE room_members ADD COLUMN last_wait_at TEXT;
|
|
98
98
|
`
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
id: 5,
|
|
102
|
+
name: "room_events_payload_json",
|
|
103
|
+
up: `
|
|
104
|
+
ALTER TABLE room_events ADD COLUMN payload_json TEXT;
|
|
105
|
+
`
|
|
99
106
|
}
|
|
100
107
|
];
|
|
101
108
|
export function resolveDatabasePath(options = {}) {
|
package/dist/mcp-server.js
CHANGED
|
@@ -126,6 +126,38 @@ export function createMcpServer(service = new TalkingStickService()) {
|
|
|
126
126
|
...input,
|
|
127
127
|
agent_id: resolveConnectionIdentity(extra.sessionId).agent_id
|
|
128
128
|
})));
|
|
129
|
+
server.registerTool("send_message", {
|
|
130
|
+
title: "Send Message",
|
|
131
|
+
description: "Send a transient message into the room event log. Routes via to_agent_id; omit it for room broadcast.",
|
|
132
|
+
inputSchema: {
|
|
133
|
+
room_id: z.string().min(1),
|
|
134
|
+
body: z.string().min(1),
|
|
135
|
+
to_agent_id: z.string().min(1).optional(),
|
|
136
|
+
delivery_hint: z.enum(["normal", "interrupt"]).optional()
|
|
137
|
+
}
|
|
138
|
+
}, async (input, extra) => toolJson(() => commands.sendMessage(resolveConnectionIdentity(extra.sessionId), input)));
|
|
139
|
+
server.registerTool("wait_for_events", {
|
|
140
|
+
title: "Wait for Events",
|
|
141
|
+
description: "Long-poll the room event log past a cursor with optional event_type, target, and sender filters.",
|
|
142
|
+
inputSchema: {
|
|
143
|
+
room_id: z.string().min(1),
|
|
144
|
+
after_event_seq: z.number().int().nonnegative().optional(),
|
|
145
|
+
event_type: z
|
|
146
|
+
.union([z.string().min(1), z.array(z.string().min(1)).min(1)])
|
|
147
|
+
.optional(),
|
|
148
|
+
target_agent_id: z.string().min(1).optional(),
|
|
149
|
+
from_agent_id: z.string().min(1).optional(),
|
|
150
|
+
max_wait_ms: z.number().int().nonnegative().optional()
|
|
151
|
+
}
|
|
152
|
+
}, async (input, extra) => toolJson(() => commands.waitForEvents({
|
|
153
|
+
room_id: input.room_id,
|
|
154
|
+
after_event_seq: input.after_event_seq,
|
|
155
|
+
event_type: input.event_type,
|
|
156
|
+
target_agent_id: input.target_agent_id,
|
|
157
|
+
from_agent_id: input.from_agent_id,
|
|
158
|
+
max_wait_ms: input.max_wait_ms,
|
|
159
|
+
agent_id: resolveConnectionIdentity(extra.sessionId).agent_id
|
|
160
|
+
})));
|
|
129
161
|
server.registerTool("add_note", {
|
|
130
162
|
title: "Add Note",
|
|
131
163
|
description: "Leave an async note on a room. Any joined member can author; authoring refreshes presence.",
|