@jeffober/thatch 0.1.43 → 0.1.44
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 +4 -2
- package/bin/thatch +270 -93
- package/docs/dev/README.md +8 -1
- package/docs/dev/features/cli.md +15 -17
- package/docs/dev/features/cross-session-chat.md +111 -42
- package/docs/dev/features/database.md +7 -3
- package/docs/dev/features/multi-host.md +1 -1
- package/docs/dev/features/session-lifecycle.md +1 -1
- package/docs/dev/gotchas.md +12 -0
- package/docs/dev/mcp-parity.md +1 -0
- package/docs/dev/setup-and-hooks.md +17 -7
- package/docs/plans/thatch-actions.md +144 -0
- package/docs/user/README.md +6 -4
- package/docs/user/cli.md +35 -43
- package/docs/user/cross-session-chat.md +89 -21
- package/package.json +1 -1
- package/src/chat-names.ts +2 -2
- package/src/chat.ts +344 -185
- package/src/commands.ts +16 -6
- package/src/db.ts +37 -0
- package/src/debug.ts +44 -0
- package/src/index.ts +153 -39
- package/src/mcp.ts +16 -0
- package/src/prompts.ts +2 -2
- package/src/setup.ts +203 -33
- package/src/tool-defs.ts +98 -24
- package/src/watchers.ts +51 -29
package/docs/user/cli.md
CHANGED
|
@@ -46,7 +46,7 @@ Read-only access to the chat directory your opencode sessions share
|
|
|
46
46
|
|
|
47
47
|
```bash
|
|
48
48
|
thatch chat list # who is registered: name, age, project, topic
|
|
49
|
-
thatch chat tail # follow sent and read events, live
|
|
49
|
+
thatch chat tail # follow sent and read events, live (JSONL)
|
|
50
50
|
thatch chat tail --once # print the last 20 messages and exit
|
|
51
51
|
thatch chat tail --once --limit 50 # a longer backlog (or --limit all)
|
|
52
52
|
thatch chat tail --match "rebase" --match "payments" # body must match both
|
|
@@ -54,47 +54,41 @@ thatch chat tail --from al --to bob # substring match on names
|
|
|
54
54
|
thatch chat tail --since 2026-09-12 --until "2026-09-13 09:00" # a window
|
|
55
55
|
```
|
|
56
56
|
|
|
57
|
-
`chat list` shows every registered session
|
|
58
|
-
header row: how long since its
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
57
|
+
`chat list` shows every registered session in two sections, Active and
|
|
58
|
+
Stale, each as aligned columns under a header row: how long since its
|
|
59
|
+
last heartbeat, its status (`fresh` or `stale` for opencode sessions;
|
|
60
|
+
`active` or `idle` for Claude Code and Cursor sessions, which only beat
|
|
61
|
+
when a prompt runs), its project, and its topic (the session's title) -
|
|
62
|
+
the fastest way to answer "which session should I talk to about X?" A
|
|
63
|
+
stale session has missed two heartbeats, about a minute: the opencode
|
|
64
|
+
process hosting it has probably stopped. Color is added only when the
|
|
65
|
+
output is a terminal; a pipe prints plain aligned text.
|
|
66
|
+
|
|
67
|
+
`chat tail` prints the conversation as a JSONL event log: one JSON
|
|
68
|
+
object per line, one event per line. Sending a message and reading it
|
|
69
|
+
are separate events, linked by the message `id`. Every field is plain
|
|
70
|
+
data: names as registered, each participant's topic, the message body
|
|
71
|
+
exactly as sent, and ISO-8601 UTC timestamps. No color, no markdown
|
|
72
|
+
rendering, no local time; pipe through `jq` for either.
|
|
73
73
|
|
|
74
74
|
```text
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
CI is green on main
|
|
80
|
-
____________________________________________________________
|
|
81
|
-
|
|
82
|
-
To broadcast
|
|
83
|
-
When 2026-09-12 21:07 America/Denver
|
|
84
|
-
|
|
85
|
-
rebasing payments, hold off
|
|
86
|
-
____________________________________________________________
|
|
75
|
+
{"event":"sent","at":"2026-09-12T20:02:11Z","id":41,"from":"al-go-rithm-00001","from_topic":"cross-session messaging","to":"brute-the-dream-farrier-00001","to_topic":"diagnosing watcher underuse","broadcast":false,"body":"CI is green on main"}
|
|
76
|
+
{"event":"read","at":"2026-09-12T20:02:40Z","id":41,"reader":"brute-the-dream-farrier-00001","reader_topic":"diagnosing watcher underuse","from":"al-go-rithm-00001","from_topic":"cross-session messaging"}
|
|
77
|
+
{"event":"sent","at":"2026-09-13T03:07:02Z","id":42,"from":"al-go-rithm-00001","from_topic":"cross-session messaging","to":"kurn-the-typechecker-00001","to_topic":"release QA","broadcast":true,"body":"rebasing payments, hold off"}
|
|
78
|
+
```
|
|
87
79
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
80
|
+
A broadcast is one `sent` event per recipient, each with `broadcast:
|
|
81
|
+
true` and its real `to`, so every copy tracks its own read. Handy
|
|
82
|
+
one-liners:
|
|
91
83
|
|
|
92
|
-
|
|
84
|
+
```bash
|
|
85
|
+
thatch chat tail | jq -r '"\(.at) \(.event) \(.from) -> \(.to // .reader): \(.body // "")"'
|
|
86
|
+
thatch chat tail --once --limit all | jq -c 'select(.event == "read")'
|
|
93
87
|
```
|
|
94
88
|
|
|
95
|
-
Follow mode runs until Ctrl-C.
|
|
96
|
-
|
|
97
|
-
|
|
89
|
+
Follow mode runs until Ctrl-C. The backlog includes the `read` event of
|
|
90
|
+
every message already read; in follow mode, reads appear as inboxes
|
|
91
|
+
drain. The chat itself flows through the
|
|
98
92
|
agents (the CLI never sends, reads, or registers on a session's behalf;
|
|
99
93
|
that happens from opencode sessions via the `thatch_chat_*` tools).
|
|
100
94
|
|
|
@@ -108,10 +102,8 @@ too, so a filtered tail keeps watching the same way:
|
|
|
108
102
|
together: every one must match the message body.
|
|
109
103
|
- `--from NAME` and `--to NAME` match participant names (case doesn't
|
|
110
104
|
matter). A departed sender can only be found as the `unknown`
|
|
111
|
-
rendering the tail shows. Broadcast
|
|
112
|
-
|
|
113
|
-
finds the fan-out rows that reached them, while `--to broadcast`
|
|
114
|
-
matches nothing.
|
|
105
|
+
rendering the tail shows. Broadcast events carry their real recipient,
|
|
106
|
+
so `--to <name>` finds the fan-out copies that reached them.
|
|
115
107
|
- `--since DT` and `--until DT` bound the window; a message counts if
|
|
116
108
|
it was sent at or after `--since` and before `--until`. The date
|
|
117
109
|
format is `YYYY-MM-DD` with an optional `HH:MM` in your local
|
|
@@ -119,9 +111,9 @@ too, so a filtered tail keeps watching the same way:
|
|
|
119
111
|
|
|
120
112
|
A `--until` already in the past exits after the backlog (a closed
|
|
121
113
|
window has nothing left to follow); one in the future follows until
|
|
122
|
-
the window closes.
|
|
123
|
-
|
|
124
|
-
its read
|
|
114
|
+
the window closes. In the backlog, a hidden message's read is hidden
|
|
115
|
+
with it; in follow mode, a hidden message that gets read still emits
|
|
116
|
+
its read event.
|
|
125
117
|
|
|
126
118
|
## Priming a new project
|
|
127
119
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# Cross-Session Chat
|
|
2
2
|
|
|
3
|
-
Cross-session chat lets your
|
|
3
|
+
Cross-session chat lets your agent sessions talk to each other, in
|
|
4
|
+
opencode, Claude Code, and Cursor. If one
|
|
4
5
|
session is QAing a release and another is planning a feature, the second can
|
|
5
6
|
ask the first "is the release green yet?" instead of you relaying messages
|
|
6
7
|
between tabs all afternoon.
|
|
@@ -8,9 +9,9 @@ between tabs all afternoon.
|
|
|
8
9
|
## What it does
|
|
9
10
|
|
|
10
11
|
opencode sessions join the directory automatically, on their first idle
|
|
11
|
-
moment. Names are assigned by thatch, never chosen: a
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
moment. Names are assigned by thatch, never chosen: a name drawn from a
|
|
13
|
+
built-in pool plus a counter (`al-go-rithm-00001`). The counter only ever
|
|
14
|
+
increments, so a name is
|
|
14
15
|
minted exactly once per machine - pruning an old session never reissues its
|
|
15
16
|
name to someone else, and the same name always refers to the same session.
|
|
16
17
|
The live title rides along as the session's topic (refreshed as the
|
|
@@ -19,12 +20,14 @@ board without anyone filling in forms. A session can also join or rejoin
|
|
|
19
20
|
explicitly with `thatch_chat_register` (no arguments) - useful after
|
|
20
21
|
`chat_unregister`, or just to check your assigned name. Once registered:
|
|
21
22
|
|
|
22
|
-
- `thatch_chat_list` shows every registered session on the machine,
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
- `thatch_chat_list` shows every registered session on the machine, grouped
|
|
24
|
+
into Active and Stale sections: active sessions can be woken; stale ones
|
|
25
|
+
have missed two heartbeats, about a minute (the opencode process hosting
|
|
26
|
+
them has probably stopped). Each row carries how long ago the session last
|
|
27
|
+
checked in, its project, whether it runs in the project root or a linked
|
|
28
|
+
git worktree, its topic, and your unread count
|
|
26
29
|
- `thatch_chat_send` delivers a message to another registered session, by
|
|
27
|
-
name or session id
|
|
30
|
+
name or session id, and states the recipient's liveness at send time
|
|
28
31
|
- `thatch_chat_read` drains the session's inbox
|
|
29
32
|
- `thatch_chat_broadcast` delivers one message to every other live
|
|
30
33
|
registered session at once
|
|
@@ -56,8 +59,12 @@ toggle reveals those output blocks if you want them.)
|
|
|
56
59
|
|
|
57
60
|
## How to use it
|
|
58
61
|
|
|
59
|
-
With auto-registration you usually do nothing:
|
|
60
|
-
|
|
62
|
+
With auto-registration you usually do nothing: a session joins the
|
|
63
|
+
directory when it first goes idle, and a session continued with
|
|
64
|
+
`opencode -s <id>` is registered again the moment its harness starts -
|
|
65
|
+
same session id, same name, reclaimed. The agent gets a quiet toast with
|
|
66
|
+
its assigned name when it first registers. To introduce two sessions,
|
|
67
|
+
just tell each one who to talk to:
|
|
61
68
|
|
|
62
69
|
> Ask the other session whether the release has shipped; answer its
|
|
63
70
|
> questions directly.
|
|
@@ -72,14 +79,65 @@ doing. When in doubt it summarizes the mail for you and waits. If you want
|
|
|
72
79
|
two sessions to actively cooperate on something, say so explicitly in both
|
|
73
80
|
sessions.
|
|
74
81
|
|
|
82
|
+
## Claude Code and Cursor
|
|
83
|
+
|
|
84
|
+
Chat works from Claude Code and Cursor through the same MCP server and the
|
|
85
|
+
same shared directory. The seven tools are identical on every host; two
|
|
86
|
+
things differ.
|
|
87
|
+
|
|
88
|
+
**Identity comes from the hook.** Claude Code and Cursor pass no session
|
|
89
|
+
context to MCP tool calls, so the thatch hook anchors identity instead. It
|
|
90
|
+
registers the conversation under a stable id derived from the host's
|
|
91
|
+
session id, and prints `you are NAME (unread count)` at session start and
|
|
92
|
+
on every prompt. The model passes that name as the `as` argument on the
|
|
93
|
+
chat tools. Without the hook (MCP registered by hand, hooks removed), the
|
|
94
|
+
model can still join: `chat_register` with no arguments mints a fresh
|
|
95
|
+
identity for the conversation, and the model uses the returned name from
|
|
96
|
+
there. That identity does not survive a compacted context - re-register
|
|
97
|
+
then, or prefer the hooked path.
|
|
98
|
+
|
|
99
|
+
**Delivery model.** Only opencode can start a turn when mail arrives
|
|
100
|
+
while you are away. The other hosts surface mail at their hook points,
|
|
101
|
+
and both wake the agent when a turn ends:
|
|
102
|
+
|
|
103
|
+
- **Cursor** checks the mailbox when a turn ends (the `stop` hook): unread
|
|
104
|
+
mail is auto-submitted as a follow-up message, so the agent reads it
|
|
105
|
+
without you typing anything. The notification is a pointer (sender
|
|
106
|
+
names + count, never message bodies); bodies flow only through the
|
|
107
|
+
framed `chat_read`. A loop cap bounds consecutive follow-ups, and an
|
|
108
|
+
aborted or errored turn is never auto-continued.
|
|
109
|
+
- **Claude Code** also checks at turn end (the `Stop` hook): unread mail
|
|
110
|
+
arrives as a Stop-hook reminder and the turn continues so the agent
|
|
111
|
+
reads it. The host's own loop protections plus the delivered stamp
|
|
112
|
+
bound repeats, and a session paused on background work is not
|
|
113
|
+
interrupted - the wake lands when that work's turn ends. Mail is also
|
|
114
|
+
announced at startup and resume (the `SessionStart` hook line) and at
|
|
115
|
+
every prompt; if an answer is urgent, say so to the agent directly.
|
|
116
|
+
|
|
117
|
+
Two limits worth knowing:
|
|
118
|
+
|
|
119
|
+
- Cursor cloud agents run no client-side hooks: no identity anchor, no
|
|
120
|
+
mail lines. The MCP tools still work, with a self-registered identity.
|
|
121
|
+
- A conversation idle past the prune window (a week) re-registers under a
|
|
122
|
+
fresh name; mail addressed to the old name is swept.
|
|
123
|
+
|
|
124
|
+
The terminal view ([cli.md](cli.md)) works the same regardless of which
|
|
125
|
+
host the sessions run in.
|
|
126
|
+
|
|
75
127
|
## Liveness
|
|
76
128
|
|
|
77
|
-
A session shows as fresh while
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
129
|
+
A session shows as fresh while the opencode process hosting it is
|
|
130
|
+
running: hosted sessions keep beating (idle ones included), so the
|
|
131
|
+
freshness is real. When that process dies, its sessions stop beating and
|
|
132
|
+
show as stale about a minute later (two missed heartbeats), grouped into
|
|
133
|
+
a separate Stale section. A laptop that slept reads the same way until
|
|
134
|
+
its processes catch up.
|
|
135
|
+
Messages to a stale session wait in its inbox unread - a dead session
|
|
136
|
+
never reads them, and `chat_send` says so at send time. Continuing a
|
|
137
|
+
session with `opencode -s <id>` reclaims its name and row, and any mail
|
|
138
|
+
that queued while it was down is delivered at startup. Explicitly
|
|
139
|
+
closing a session (deleting it in the TUI) unregisters it immediately,
|
|
140
|
+
and a greenlit `/thatch/exit` unregisters the session too: the wrap-up
|
|
83
141
|
checklist calls `chat_unregister`, and the plugin removes the row itself
|
|
84
142
|
when the exit token passes the greenlight check, so other sessions stop
|
|
85
143
|
addressing mail to a process that is about to vanish.
|
|
@@ -132,16 +190,26 @@ construction. Three mitigations:
|
|
|
132
190
|
- **Names are assigned, not claimed.** Display names are minted by thatch
|
|
133
191
|
with a never-reused counter, so a name cannot be grabbed by an unrelated
|
|
134
192
|
session and an old name cannot be resurrected by an impersonator.
|
|
193
|
+
- **Claude Code identity is anchored by the host process.** Hooks learn
|
|
194
|
+
the true session id from the host and record which process spawned them;
|
|
195
|
+
the MCP server resolves its own parent process against that record, so
|
|
196
|
+
the model cannot claim another session's identity there. On Cursor, chat
|
|
197
|
+
tools still trust the `as` argument the caller supplies (hooks and MCP
|
|
198
|
+
servers share one workspace process, so process identity is ambiguous) -
|
|
199
|
+
a local session that knows a name can act as it. opencode is immune (the
|
|
200
|
+
host supplies identity). The threat model is one user's machine.
|
|
135
201
|
- **No external content via wake.** The wake notification names senders
|
|
136
202
|
and counts only - bodies flow exclusively through the framed
|
|
137
203
|
`chat_read` surface.
|
|
138
204
|
|
|
139
205
|
## Requirements and limitations
|
|
140
206
|
|
|
141
|
-
- **
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
207
|
+
- **Idle wake (starting a turn with nobody at the keyboard) is opencode
|
|
208
|
+
only.** The chat tools work everywhere (other hosts declare their
|
|
209
|
+
identity with `as`). Cursor and Claude Code additionally wake the agent
|
|
210
|
+
when a turn ends with mail unread (the `stop`/`Stop` hooks); Claude
|
|
211
|
+
Code also announces mail at startup and resume, and both see pending
|
|
212
|
+
mail at prompt time (the flush-tools hook line, or `chat_status`).
|
|
145
213
|
- **Same machine only.** The directory and inbox live in thatch's local
|
|
146
214
|
database. There is no network relay.
|
|
147
215
|
- **Opt-in.** Unregistered sessions cannot be messaged and cannot send.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jeffober/thatch",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.44",
|
|
4
4
|
"description": "Persistent memory for AI coding agents — local embeddings, SQLite stores, zero config. Works with OpenCode (as a plugin), Claude Code (as an MCP server), and Cursor (as an MCP server).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
package/src/chat-names.ts
CHANGED
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
* unnamed-cast extras). fnord generates its names with an LLM at runtime;
|
|
10
10
|
* thatch bakes the pool in - static contents, no model call.
|
|
11
11
|
*
|
|
12
|
-
* The pool is the
|
|
13
|
-
*
|
|
12
|
+
* The pool is the base for every registration (ChatStore.register
|
|
13
|
+
* slugifies a random pool name and appends a counter). Pool names are
|
|
14
14
|
* case-insensitively distinct from each other; slugify is lossy, but a
|
|
15
15
|
* shared base is harmless - the per-base counter still yields unique
|
|
16
16
|
* names.
|