@jeffober/thatch 0.1.40 → 0.1.42
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 +27 -10
- package/artifacts/skills/thatch-code-review.md +4 -4
- package/artifacts/skills/thatch-review-synthesizer.md +8 -8
- package/artifacts/skills/{thatch-review-mentorship.md → thatch-review-technique.md} +9 -10
- package/bin/release +4 -1
- package/bin/thatch +318 -18
- package/docs/dev/README.md +29 -15
- package/docs/dev/features/README.md +1 -0
- package/docs/dev/features/cli.md +33 -4
- package/docs/dev/features/cross-session-chat.md +315 -0
- package/docs/dev/features/database.md +52 -4
- package/docs/dev/features/multi-host.md +18 -9
- package/docs/dev/features/setup.md +12 -5
- package/docs/dev/features/watchers.md +113 -23
- package/docs/dev/gotchas.md +7 -4
- package/docs/dev/mcp-parity.md +2 -2
- package/docs/dev/setup-and-hooks.md +5 -4
- package/docs/dev/skills.md +5 -4
- package/docs/user/README.md +25 -6
- package/docs/user/cli.md +78 -0
- package/docs/user/code-review.md +1 -1
- package/docs/user/cross-session-chat.md +150 -0
- package/docs/user/setup.md +16 -2
- package/docs/user/skills.md +7 -6
- package/docs/user/watchers.md +61 -16
- package/package.json +2 -1
- package/src/chat-names.ts +120 -0
- package/src/chat.ts +1210 -0
- package/src/config.ts +34 -1
- package/src/db.ts +227 -0
- package/src/index.ts +188 -17
- package/src/prompts.ts +218 -29
- package/src/setup.ts +83 -29
- package/src/skills.ts +47 -17
- package/src/tool-defs.ts +574 -31
- package/src/tools.ts +2 -0
- package/src/watchers.ts +358 -23
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
# Cross-Session Chat
|
|
2
|
+
|
|
3
|
+
Cross-session chat lets independent opencode sessions on one machine message
|
|
4
|
+
each other through thatch: sessions join a shared directory (automatically,
|
|
5
|
+
under assigned names), see each other in the list, and messages land in
|
|
6
|
+
inboxes. An idle recipient is woken with a prompt when mail arrives, so
|
|
7
|
+
coordination does not route through the human ("ask the other session
|
|
8
|
+
whether the release is green" instead of "Jeff, go ask").
|
|
9
|
+
|
|
10
|
+
User-facing behavior is documented in
|
|
11
|
+
[docs/user/cross-session-chat.md](../../user/cross-session-chat.md).
|
|
12
|
+
|
|
13
|
+
## What it does
|
|
14
|
+
|
|
15
|
+
- `thatch_chat_register` / `thatch_chat_list` / `thatch_chat_send` /
|
|
16
|
+
`thatch_chat_read` / `thatch_chat_unregister` / `thatch_chat_broadcast`
|
|
17
|
+
tools (shared across hosts; opencode injects identity, MCP hosts pass the
|
|
18
|
+
name their thatch hook printed as the `as` argument)
|
|
19
|
+
- Automatic registration: the plugin inserts a top-level session's
|
|
20
|
+
directory row on its first idle event, with an assigned never-reused
|
|
21
|
+
name (`chat.autoRegister: false` opts out)
|
|
22
|
+
- A shared SQLite directory and inbox in thatch.db, writable by any opencode
|
|
23
|
+
process on the machine
|
|
24
|
+
- A per-process poller that heartbeats hosted sessions and delivers wake
|
|
25
|
+
prompts via `client.session.promptAsync` with a synthetic part - the same
|
|
26
|
+
mechanism watchers use
|
|
27
|
+
- Anti-loop protection at two levels: prompt guidance (messages are
|
|
28
|
+
informational, no reflexive auto-reply) and a hard per-recipient nudge
|
|
29
|
+
rate cap
|
|
30
|
+
|
|
31
|
+
## The shared-state, local-delivery decision
|
|
32
|
+
|
|
33
|
+
Watchers keep their registry in plugin memory, never SQLite, because the
|
|
34
|
+
process that registered a watcher is the only one that can deliver its
|
|
35
|
+
events (see [watchers.md](watchers.md)). Chat cannot work that way: the
|
|
36
|
+
sender's process and the recipient's process are usually different, so the
|
|
37
|
+
state they coordinate through must be shared.
|
|
38
|
+
|
|
39
|
+
The split:
|
|
40
|
+
|
|
41
|
+
- **State is shared.** `chat_sessions` and `chat_messages` live in thatch.db.
|
|
42
|
+
Every opencode process on the machine reads and writes the same tables.
|
|
43
|
+
Concurrent access is the already-solved case - multiple sessions share the
|
|
44
|
+
DB for memories today, under WAL plus a busy timeout.
|
|
45
|
+
- **Delivery is local.** Each process polls the inbox for messages addressed
|
|
46
|
+
to sessions it hosts (those it has seen `session.status` events for) and
|
|
47
|
+
prompts them through its own SDK client. A sender never prompts a session
|
|
48
|
+
in another process.
|
|
49
|
+
|
|
50
|
+
This preserves the watcher rationale's core property - no process prompts a
|
|
51
|
+
session it does not host - so each message has exactly one possible
|
|
52
|
+
deliverer. There is no claiming logic, no double-delivery race, and no
|
|
53
|
+
orphaned delivery state. The cost is the mirror of watchers': chat state
|
|
54
|
+
outlives a process, so liveness needs a signal beyond process lifetime.
|
|
55
|
+
|
|
56
|
+
## How it works
|
|
57
|
+
|
|
58
|
+
### Registration and identity
|
|
59
|
+
|
|
60
|
+
Sessions are auto-registered by the plugin: the first `session.status`
|
|
61
|
+
idle event for a top-level session inserts its directory row (unless
|
|
62
|
+
`chat.autoRegister: false`). Names are assigned, never claimed:
|
|
63
|
+
`<slug>-<counter>` where the slug is the session title lowercased and
|
|
64
|
+
hyphenated (pool-draw slug when the title is still opencode's placeholder)
|
|
65
|
+
and the counter comes from a per-base counter row
|
|
66
|
+
(`chat_name_counters`, src/db.ts) that only ever increments - drawn
|
|
67
|
+
atomically via INSERT ... ON CONFLICT ... RETURNING. A name is therefore
|
|
68
|
+
minted exactly once per machine: pruning an auto-registered row (host
|
|
69
|
+
silent for CHAT_AUTO_TTL_DAYS = 7 days, swept hourly by the poller) can
|
|
70
|
+
never reissue its name, which is what makes the prune safe. The session's
|
|
71
|
+
live title rides the topic column, refreshed on every idle by
|
|
72
|
+
`refreshAutoTopic` - auto rows only; legacy rows keep their old model-set topics.
|
|
73
|
+
|
|
74
|
+
`chat_register` (no arguments) is the explicit path: idempotent ensure for
|
|
75
|
+
opencode (identity is the host session ID, which the model cannot know or
|
|
76
|
+
forge), a fresh identity per conversation on MCP hosts, or a reclaim of an
|
|
77
|
+
existing name via `as`. Re-registering keeps the name. Unregistered
|
|
78
|
+
sessions are invisible and unmessageable in both directions. Only
|
|
79
|
+
top-level sessions should register - sub-agent children are ephemeral -
|
|
80
|
+
which the tool descriptions and system prompt state; there is no
|
|
81
|
+
mechanical barrier, a documented trust posture.
|
|
82
|
+
|
|
83
|
+
### Leave tombstones
|
|
84
|
+
|
|
85
|
+
Every exit path - `chat_unregister` and `session.deleted` alike - writes a
|
|
86
|
+
`chat_leave_tombstones` row in the same transaction that deletes the
|
|
87
|
+
directory row. The tombstone is what makes a leave stick: both
|
|
88
|
+
registration paths consult it (the plugin's idle auto-registerer is
|
|
89
|
+
suppressed outright; the MCP hook's ensure-register prints the leave line
|
|
90
|
+
instead of rejoining), so a leave cannot be silently undone by the next
|
|
91
|
+
idle moment. The gate lives in `ChatStore.register` via `#tombstoneGate`
|
|
92
|
+
(src/chat.ts), which also closes the in-flight race: an auto-register
|
|
93
|
+
IIFE whose session was deleted during its title-fetch await finds the
|
|
94
|
+
tombstone and stops instead of resurrecting a dead session. An explicit
|
|
95
|
+
`chat_register` clears the tombstone before re-registering (fresh name -
|
|
96
|
+
assigned identities are never reused), and tombstones age out in the
|
|
97
|
+
hourly prune (7-day TTL; a resumed session after expiry simply
|
|
98
|
+
auto-registers under a fresh name).
|
|
99
|
+
|
|
100
|
+
Because names are system-minted and never recycled, the impersonation
|
|
101
|
+
surface shrinks to whatever the operator runs: every agent on this machine
|
|
102
|
+
is the operator's agent.
|
|
103
|
+
|
|
104
|
+
### Topics
|
|
105
|
+
|
|
106
|
+
The topic is the roster's "who is working on what" signal. For
|
|
107
|
+
auto-registered sessions it is the live session title, refreshed on every
|
|
108
|
+
idle event - the auto-titler usually lands a real title within a turn or
|
|
109
|
+
two, so the roster converges to the actual work with nobody filling in
|
|
110
|
+
forms. Legacy rows (from the claimed-names era) keep whatever topic was
|
|
111
|
+
set at their registration: free text, sanitized to one roster line
|
|
112
|
+
(whitespace runs collapse, 80-character cap, advisory - oversizing degrades
|
|
113
|
+
rather than errors). There is no tool path to set a topic anymore - topics
|
|
114
|
+
are title-derived for everything the current code mints. Broadcast senders
|
|
115
|
+
benefit most: `chat_broadcast` answers "which of you is working on X?"
|
|
116
|
+
with the roster already in hand.
|
|
117
|
+
|
|
118
|
+
### Messages
|
|
119
|
+
|
|
120
|
+
`chat_send` addresses a recipient by display name or session ID. Both
|
|
121
|
+
endpoints must be registered and distinct. `chat_read` drains the calling
|
|
122
|
+
session's inbox oldest-first and stamps rows read. Message history survives
|
|
123
|
+
unregistration; a departed sender degrades to an unknown name in the
|
|
124
|
+
reader's view (the endpoints are deliberately not foreign keys - leaving the
|
|
125
|
+
directory must not be blocked by history).
|
|
126
|
+
|
|
127
|
+
`chat_broadcast` posts one message to every other registered session at
|
|
128
|
+
once, one inbox row per recipient - the existing wake machinery (grouping,
|
|
129
|
+
gating, rate cap) treats each as ordinary mail. Stale sessions are skipped
|
|
130
|
+
and reported rather than messaged: a host that has stopped heartbeat-ing
|
|
131
|
+
will never read the mail, and dead mail to a dead process is just clutter.
|
|
132
|
+
The sender is excluded. It is a separate tool rather than a magic
|
|
133
|
+
`chat_send` recipient because "send to all" changes the behavior (fan-out,
|
|
134
|
+
stale skipping, no address resolution), and a function that changes
|
|
135
|
+
behavior drastically on a parameter value is two functions.
|
|
136
|
+
|
|
137
|
+
### Polling, heartbeat, staleness
|
|
138
|
+
|
|
139
|
+
A `setInterval` loop (default 30s) runs one cycle per process: heartbeat the
|
|
140
|
+
hosted sessions' `last_seen`, then deliver. A crashed process fires no
|
|
141
|
+
`session.deleted`, so `chat_list` marks sessions whose `last_seen` is older
|
|
142
|
+
than the staleness threshold (default 10 minutes) as stale rather than
|
|
143
|
+
hiding them - the operator decides what to do with ghosts. `session.deleted`
|
|
144
|
+
is the graceful-exit fast path that unregisters immediately.
|
|
145
|
+
|
|
146
|
+
### Delivery, re-nudge, and the rate cap
|
|
147
|
+
|
|
148
|
+
A message needs a wake prompt when it is unread and either never delivered
|
|
149
|
+
or delivered more than the re-nudge window ago (default 15 minutes).
|
|
150
|
+
Delivery is gated by the same `canDeliver` predicate watchers use - the
|
|
151
|
+
recipient must be idle and not compacting - and the idle event handler
|
|
152
|
+
flushes pending mail directly so it lands promptly instead of waiting for
|
|
153
|
+
the next cycle. Failed deliveries stay pending and retry. Only registered
|
|
154
|
+
recipients are ever selected: unregistering stops wake prompts for
|
|
155
|
+
kept-but-unread mail, which is the `chat_unregister` tool's promise.
|
|
156
|
+
Delivery is at-least-once - a crash after the wake prompt but before the
|
|
157
|
+
delivered_at stamp re-nudges the same batch on the next cycle.
|
|
158
|
+
|
|
159
|
+
One wake prompt covers all of a recipient's pending messages: sender names
|
|
160
|
+
and a count, pointer-only like watcher notifications, with the model calling
|
|
161
|
+
`chat_read` for content. On success the poller stamps `delivered_at`
|
|
162
|
+
(re-stamping on re-nudges resets the re-nudge timer).
|
|
163
|
+
|
|
164
|
+
The hard brake: at most a fixed number of wake prompts per recipient per
|
|
165
|
+
hour (default 6, in-memory on purpose - it is a loop guard, not accounting).
|
|
166
|
+
Without it, two agents politely acknowledging each other would ping-pong
|
|
167
|
+
forever, each wake triggering a reply that wakes the other.
|
|
168
|
+
|
|
169
|
+
### Transcript echo
|
|
170
|
+
|
|
171
|
+
Plugin tools render in the opencode TUI as muted one-line generic entries,
|
|
172
|
+
with the output block behind a default-off toggle - so without help, a chat
|
|
173
|
+
exchange is invisible to the human watching the session. The plugin's
|
|
174
|
+
`tool.execute.after` hook builds a short echo for the conversational events
|
|
175
|
+
(`chatEchoText()` in `src/prompts.ts`: register, send, read, broadcast) and
|
|
176
|
+
delivers it as a non-synthetic, `noReply` promptAsync part: rendered as a
|
|
177
|
+
visible bubble in the transcript, no model turn started (the server's
|
|
178
|
+
prompt path returns before the completion loop). Failed calls never echo;
|
|
179
|
+
`chat_list` and `chat_unregister` stay on the muted tool line. Broadcasts
|
|
180
|
+
echo their fan-out count with the clipped body.
|
|
181
|
+
|
|
182
|
+
The trade-off: non-synthetic is what makes the TUI render the part, and it
|
|
183
|
+
also means later turns see the echo in context - a small duplication of the
|
|
184
|
+
tool call it mirrors, accepted for visibility. Echo bodies are clipped
|
|
185
|
+
(send: the body; read: the formatted inbox; broadcast: the body) so a
|
|
186
|
+
bubble stays cheap. Echo delivery is fire-and-forget: a failure must never
|
|
187
|
+
fail the tool call it follows. Because the opencode server fires the
|
|
188
|
+
`chat.message` hook for every prompt part before the `noReply`
|
|
189
|
+
early-return, the plugin's `chat.message` handler skips messages whose
|
|
190
|
+
visible text is entirely `[chat]`-prefixed bubbles (`isChatEchoParts`) -
|
|
191
|
+
otherwise every echo would run the nudge machinery for a message no model
|
|
192
|
+
turn reads. The accepted edge: a real user message whose every visible
|
|
193
|
+
part starts with the prefix is skipped the same way, losing that one turn's
|
|
194
|
+
advisory nudges.
|
|
195
|
+
|
|
196
|
+
### The untrusted-content frame
|
|
197
|
+
|
|
198
|
+
Message bodies are other agents' text delivered verbatim into the reading
|
|
199
|
+
model's context - a prompt-injection surface by construction. The watcher
|
|
200
|
+
privacy model solves the same surface with pointer-only notifications, but
|
|
201
|
+
chat's payload IS the body, so `chat_read` frames the boundary instead:
|
|
202
|
+
`chatInboxFrame()` (src/prompts.ts) wraps the message lines in
|
|
203
|
+
begin/end fences with an explicit do-not-follow warning. The frame exists only in
|
|
204
|
+
the tool output - persisted rows are untouched - and pairs with the wake
|
|
205
|
+
nudge's anti-loop rule and the isChatEchoParts skip as the feature's three
|
|
206
|
+
injection-hygiene layers.
|
|
207
|
+
|
|
208
|
+
### CLI access
|
|
209
|
+
|
|
210
|
+
The user watches the conversation without an agent: `thatch chat list`
|
|
211
|
+
renders the roster with human-readable heartbeat ages, and `thatch chat
|
|
212
|
+
tail` follows the message stream as cards. The backlog renders only the
|
|
213
|
+
last `CHAT_TAIL_DEFAULT_LIMIT` (20) messages via `chatTailBacklog()`;
|
|
214
|
+
`filterChatTailRows()` narrows every feed snapshot (backlog and follow
|
|
215
|
+
polls alike) by body regexes, participant-name substrings, and a
|
|
216
|
+
half-open time window. The diff itself is `chatTailDiff()` (unit-tested
|
|
217
|
+
in `tests/chat.test.ts`): sent cards on first view, then new sends and
|
|
218
|
+
newly-read messages per poll, with the diff state seeded from the full
|
|
219
|
+
feed so neither the limit nor a mid-follow rename/unregister can
|
|
220
|
+
resurface old rows as sent events. `chat_messages.via_broadcast` marks
|
|
221
|
+
fan-out rows so the tail renders one `-> broadcast` card per recipient
|
|
222
|
+
instead of what would otherwise look like identical direct sends. The
|
|
223
|
+
CLI takes no chat write actions (the wake machinery owns those paths);
|
|
224
|
+
the only writes it can trigger are the schema migrations that run
|
|
225
|
+
whenever any thatch process opens the database.
|
|
226
|
+
|
|
227
|
+
## Interactions with other features
|
|
228
|
+
|
|
229
|
+
- Watchers ([watchers.md](watchers.md)): chat reuses the delivery gate
|
|
230
|
+
(`canDeliver` over `sessionStatus` + `compacting`), the idle-flush
|
|
231
|
+
pattern, and the synthetic-part + toast notification shape. The state
|
|
232
|
+
model is the deliberate opposite - shared SQLite vs process memory - and
|
|
233
|
+
each feature's doc explains why its side of the split is right for its
|
|
234
|
+
problem.
|
|
235
|
+
- Session lifecycle ([session-lifecycle.md](session-lifecycle.md)):
|
|
236
|
+
`session.status` feeds the delivery gate and the hosted-session set;
|
|
237
|
+
`session.deleted` unregisters.
|
|
238
|
+
- Extraction pipeline ([extraction.md](extraction.md)): chat tool calls
|
|
239
|
+
are `thatch_*` tools and excluded from buffering like all thatch tools;
|
|
240
|
+
non-thatch tool calls made during a chat notification turn are buffered
|
|
241
|
+
and extracted like any other turn's.
|
|
242
|
+
- Multi-host ([multi-host.md](multi-host.md)): the chat tools are shared -
|
|
243
|
+
MCP hosts get their identity from the thatch hook, which ensure-registers
|
|
244
|
+
the conversation's stable session ID (`mcp_<hash of host session id>`)
|
|
245
|
+
and prints the assigned name plus unread count on every prompt; chat
|
|
246
|
+
tools take that name as `as`. Wake-up delivery remains opencode-only: no
|
|
247
|
+
MCP server can start a turn in the client's conversation.
|
|
248
|
+
|
|
249
|
+
## Multi-host delivery tiers
|
|
250
|
+
|
|
251
|
+
Chat works on every host, and can be disabled entirely: `chat.enabled:
|
|
252
|
+
false` in the thatch config makes every chat tool refuse (a `[disabled]`
|
|
253
|
+
reply naming the re-enable path), stops the poller at init, omits the
|
|
254
|
+
system prompt's chat section, and silences the hook lines. The tools
|
|
255
|
+
re-read the config per call, so a toggle applies immediately; the poller
|
|
256
|
+
gate is read once at init and needs a restart. The hook lines gate on the
|
|
257
|
+
same config, so a disabled feature never advertises itself through any
|
|
258
|
+
surface.
|
|
259
|
+
|
|
260
|
+
Chat works on every host. Earlier designs for the MCP path - a JSONL
|
|
261
|
+
temp-file inbox, a socket or daemon - were rejected: the DB already is the
|
|
262
|
+
inbox, and MCP hosts cannot hold connections across turns, so both
|
|
263
|
+
degenerate to the turn-granularity polling the tools already provide. The
|
|
264
|
+
wake analog is the existing hook channel.
|
|
265
|
+
|
|
266
|
+
| Host | Send/receive | New-mail signal | Echo bubbles |
|
|
267
|
+
|-------------|--------------|------------------------------|--------------|
|
|
268
|
+
| opencode | yes | promptAsync wake (idle-gated)| yes |
|
|
269
|
+
| claude code | yes | flush-tools hook line | no |
|
|
270
|
+
| cursor | yes | flush-tools hook line | no |
|
|
271
|
+
|
|
272
|
+
MCP identity is anchored by the host hook: `chatHookLine(sessionID)` in
|
|
273
|
+
`bin/thatch` ensure-registers `mcp_<sha256(host session id)[0:12]>` on
|
|
274
|
+
every prompt and prints the assigned name plus unread count, so the
|
|
275
|
+
identity is stable across the ephemeral conversations those harnesses run
|
|
276
|
+
(and a conversation without the hook gets a fresh per-conversation
|
|
277
|
+
identity from `chat_register`). `chat_status` is the quiet check
|
|
278
|
+
(registered flag + pending/total); the flush-tools hook line names the
|
|
279
|
+
caller's identity and mailbox, and is absent entirely when chat is
|
|
280
|
+
disabled - absence is silent by construction.
|
|
281
|
+
Staleness semantics differ by kind: an opencode row's stale age means the
|
|
282
|
+
process is gone (broadcast skips it), while an mcp row's age only means
|
|
283
|
+
"between turns" (broadcast always delivers).
|
|
284
|
+
|
|
285
|
+
## Defaults
|
|
286
|
+
|
|
287
|
+
Timing constants live in `src/chat.ts`: poll interval 30s, staleness
|
|
288
|
+
threshold 10 minutes, re-nudge window 15 minutes, nudge cap 6 per recipient
|
|
289
|
+
per hour, auto-row TTL 7 days. There are no environment overrides yet; add
|
|
290
|
+
them the way `THATCH_WATCH_POLL_SECONDS` works if a user needs them.
|
|
291
|
+
|
|
292
|
+
## Source files
|
|
293
|
+
|
|
294
|
+
- `src/chat.ts` - ChatStore (directory + inbox SQL, assigned-name draw:
|
|
295
|
+
slugify, counter bump, insert, prune, topic refresh), ChatPoller
|
|
296
|
+
(heartbeat, gated delivery, re-nudge, rate cap, hourly prune sweep),
|
|
297
|
+
staleness helper
|
|
298
|
+
- `src/chat-names.ts` - the static display-name pool (nomenclater style),
|
|
299
|
+
the fallback base for title-less registrations
|
|
300
|
+
- `src/db.ts` - the chat tables in schema init (including
|
|
301
|
+
chat_name_counters and the auto column), the NOCASE collation migration
|
|
302
|
+
and topic column migration, delegated methods
|
|
303
|
+
- `src/tool-defs.ts` - the chat tool definitions
|
|
304
|
+
- `src/index.ts` - poller construction, delivery closure, idle
|
|
305
|
+
auto-registration + topic refresh, idle flush,
|
|
306
|
+
session.deleted unregister, dispose, transcript echo in
|
|
307
|
+
tool.execute.after
|
|
308
|
+
- `src/prompts.ts` - `chatNotificationNudge()`, `chatEchoText()` /
|
|
309
|
+
`isChatEchoParts()`, system prompt Cross-Session Chat section, MCP
|
|
310
|
+
absent-tools note
|
|
311
|
+
- `tests/chat.test.ts` - store, pool, migration, echo-text, and poller unit
|
|
312
|
+
tests (temp-dir SQLite, injected delivery)
|
|
313
|
+
- `tests/qa/auto/uc-097-chat.ts` - full lifecycle against a mocked poller
|
|
314
|
+
- `tests/qa/live/uc-098-chat-cross-session.ts` - two real sessions exchange
|
|
315
|
+
a message through the shared DB
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Database
|
|
2
2
|
|
|
3
|
-
Thatch stores all data in a single SQLite file. The schema is organized around
|
|
3
|
+
Thatch stores all data in a single SQLite file. The schema is organized around feature concerns: memory entries, the prediction engine, the behavior engine, and cross-session chat.
|
|
4
4
|
|
|
5
5
|
## Configuration
|
|
6
6
|
|
|
@@ -10,13 +10,14 @@ Thatch stores all data in a single SQLite file. The schema is organized around t
|
|
|
10
10
|
|
|
11
11
|
## Schema overview
|
|
12
12
|
|
|
13
|
-
The tables fall into
|
|
13
|
+
The tables fall into these groups:
|
|
14
14
|
|
|
15
15
|
| Group | Tables | Purpose |
|
|
16
16
|
|-------|--------|---------|
|
|
17
17
|
| Memory | `stores`, `entries`, `dedup_pairs` | Core memory CRUD + dedup verdicts |
|
|
18
18
|
| Prediction engine | `prediction_matchers`, `predictions`, `prediction_edges`, `prediction_provenance` | User decision model |
|
|
19
19
|
| Behavior engine | `behavior_matchers`, `behaviors`, `behavior_edges`, `behavior_provenance` | LLM self-discipline rules |
|
|
20
|
+
| Cross-session chat | `chat_sessions`, `chat_messages` | Session directory + message inbox (shared across opencode processes) |
|
|
20
21
|
|
|
21
22
|
The prediction and behavior engines share the same four-table shape (matchers, items, edges, provenance) with different table names. They are separate because the semantics differ: predictions model what the user wants; behaviors model what the LLM should do.
|
|
22
23
|
|
|
@@ -213,6 +214,52 @@ behavior_provenance(
|
|
|
213
214
|
|
|
214
215
|
- `signal`: "confirm" (ham), "disconfirm" (spam), or "codify".
|
|
215
216
|
|
|
217
|
+
## Cross-session chat tables
|
|
218
|
+
|
|
219
|
+
### chat_sessions
|
|
220
|
+
|
|
221
|
+
```sql
|
|
222
|
+
chat_sessions(
|
|
223
|
+
session_id TEXT PRIMARY KEY,
|
|
224
|
+
name TEXT NOT NULL UNIQUE COLLATE NOCASE,
|
|
225
|
+
topic TEXT,
|
|
226
|
+
project TEXT,
|
|
227
|
+
registered_at TEXT NOT NULL,
|
|
228
|
+
last_seen TEXT NOT NULL
|
|
229
|
+
)
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
- The machine-wide session directory. `last_seen` is the heartbeat: each
|
|
233
|
+
host process refreshes it for its own sessions every poll cycle, and a
|
|
234
|
+
stale value marks a crashed or closed process. Name uniqueness is
|
|
235
|
+
case-insensitive; databases whose chat_sessions table predates the NOCASE
|
|
236
|
+
constraint are rebuilt at schema init (first row wins per case group).
|
|
237
|
+
|
|
238
|
+
### chat_messages
|
|
239
|
+
|
|
240
|
+
```sql
|
|
241
|
+
chat_messages(
|
|
242
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
243
|
+
from_session TEXT NOT NULL,
|
|
244
|
+
to_session TEXT NOT NULL,
|
|
245
|
+
body TEXT NOT NULL,
|
|
246
|
+
created_at TEXT NOT NULL,
|
|
247
|
+
delivered_at TEXT,
|
|
248
|
+
read_at TEXT,
|
|
249
|
+
via_broadcast INTEGER NOT NULL DEFAULT 0
|
|
250
|
+
)
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
- `delivered_at` is the last wake-prompt stamp (restamped on re-nudges);
|
|
254
|
+
`read_at` is set when the recipient drains its inbox. `via_broadcast`
|
|
255
|
+
marks rows a chat_broadcast fan-out created (the CLI tail renders them
|
|
256
|
+
as `-> broadcast`). The endpoints are
|
|
257
|
+
plain columns, not foreign keys: unregistering a session must not be
|
|
258
|
+
blocked by message history, and a departed sender degrades to an unknown
|
|
259
|
+
name in the reader's view.
|
|
260
|
+
|
|
261
|
+
See [cross-session-chat.md](cross-session-chat.md) for the delivery model.
|
|
262
|
+
|
|
216
263
|
## Schema migration
|
|
217
264
|
|
|
218
265
|
`recall_count`, `last_recalled_at`, and `archived` are added to pre-existing databases by an idempotent column migration at init. The migration uses `PRAGMA table_info` to check for the column's existence, then `ALTER TABLE ADD COLUMN` if missing. This handles databases created before these columns existed.
|
|
@@ -235,8 +282,9 @@ Embeddings are raw Float32Array bytes stored as BLOBs. Serialization honors `byt
|
|
|
235
282
|
|
|
236
283
|
- Memory store ([memory-store.md](memory-store.md)): `entries` table is the core; `stores` table is the registry
|
|
237
284
|
- Deduplication ([deduplication.md](deduplication.md)): `dedup_pairs` table tracks reviewed pairs
|
|
238
|
-
- Prediction engine ([prediction-engine.md](prediction-engine.md)):
|
|
239
|
-
- Behavior engine ([behavior-engine.md](behavior-engine.md)):
|
|
285
|
+
- Prediction engine ([prediction-engine.md](prediction-engine.md)): the prediction tables
|
|
286
|
+
- Behavior engine ([behavior-engine.md](behavior-engine.md)): the behavior tables
|
|
287
|
+
- Cross-session chat ([cross-session-chat.md](cross-session-chat.md)): the chat tables, shared across opencode processes
|
|
240
288
|
- Hygiene ([hygiene.md](hygiene.md)): `staleEntryCount` uses `recall_count` and `last_recalled_at`; `branchesInStore` uses `branch` column
|
|
241
289
|
|
|
242
290
|
## Source files
|
|
@@ -16,11 +16,17 @@ Thatch supports three host agents from a single codebase:
|
|
|
16
16
|
- **Cursor MCP**---same stdio MCP server, `--json` hook output format,
|
|
17
17
|
`mcp__thatch__` tool prefix, system prompt in AGENTS.md
|
|
18
18
|
|
|
19
|
-
All three paths share the tool definitions from `src/tool-defs.ts`:
|
|
20
|
-
host-agnostic tools plus opencode-only tools
|
|
21
|
-
|
|
22
|
-
`
|
|
23
|
-
|
|
19
|
+
All three paths share the tool definitions from `src/tool-defs.ts`: the
|
|
20
|
+
host-agnostic tools plus the opencode-only tools, which the MCP server
|
|
21
|
+
filters out. The opencode-only set covers host-session capabilities MCP
|
|
22
|
+
hosts lack: `get_session_info`, `session_search`/`session_get` (no session
|
|
23
|
+
concept or session database), the `watch_create`/`watch_branch_create`/
|
|
24
|
+
`watch_command_create`/`watch_list`/`watch_cancel` watcher tools (no
|
|
25
|
+
poller or proactive-prompt
|
|
26
|
+
channel), and the `chat_register`/`chat_list`/`chat_send`/`chat_read`/
|
|
27
|
+
`chat_unregister`/`chat_broadcast` chat tools (no session identity or
|
|
28
|
+
wake-up channel). The
|
|
29
|
+
shared tools take args and a `CoreContext`
|
|
24
30
|
(`{ db, model, defaultStore }`) and return a string. The host-specific
|
|
25
31
|
wrapping---prefixing, transport, session hooks---lives outside the core.
|
|
26
32
|
|
|
@@ -74,9 +80,10 @@ at session start.
|
|
|
74
80
|
so the agent sees `mcp__thatch__memory_remember`. Thatch itself does not apply
|
|
75
81
|
this prefix.
|
|
76
82
|
|
|
77
|
-
**Skills**: installed to
|
|
78
|
-
|
|
79
|
-
|
|
83
|
+
**Skills**: installed to the scope's skills dir (repo `.claude/skills/` for
|
|
84
|
+
project-local setup, `$CLAUDE_CONFIG_DIR/skills/` for `--global`)---shared
|
|
85
|
+
skills only. The code-review coordinator skill is opencode-only because it
|
|
86
|
+
requires sub-agent dispatch, which Claude Code does not support.
|
|
80
87
|
|
|
81
88
|
**Session behavior**: driven by external CLI hook processes (`bin/thatch`).
|
|
82
89
|
The hooks are registered in `.claude/settings.json` during setup. Each hook
|
|
@@ -182,6 +189,8 @@ behavior, with some features having no MCP counterpart.
|
|
|
182
189
|
| Tool batching | N/A (in-process) | PostToolBatch (batch) | postToolUse (per-tool) |
|
|
183
190
|
| `get_session_info` (session identity) | Yes | No | No |
|
|
184
191
|
| `session_search` / `session_get` (conversation archaeology) | Yes (tools) | CLI only (`thatch session ...`) | CLI only (`thatch session ...`) |
|
|
192
|
+
| `watch_*` (PR, branch, and command watchers) | Yes (tools) | No | No |
|
|
193
|
+
| `chat_*` (cross-session chat) | Yes | No | No |
|
|
185
194
|
|
|
186
195
|
For the full parity matrix, see [../mcp-parity.md](../mcp-parity.md).
|
|
187
196
|
|
|
@@ -231,4 +240,4 @@ the opencode-only skills require sub-agent dispatch or in-process hooks.
|
|
|
231
240
|
- The MCP server always exposes bare names. The `mcp__thatch__` prefix is applied by the MCP client, not thatch.
|
|
232
241
|
- The three system prompt variants are independent string constants in `src/prompts.ts` with no shared template. Editing shared prose in one requires mirroring in the other two, or they drift.
|
|
233
242
|
- The tool list line in all three prompts must match `TOOL_DEFS` in `src/tool-defs.ts` (opencode's list includes opencode-only tools; the MCP lists exclude them). The historical failure mode: a tool is added to `TOOL_DEFS` but only two of three prompts are updated.
|
|
234
|
-
- opencode-only features (direct extraction, compaction recovery, code-review coordinator, background sub-agents, TUI toasts, `get_session_info`, `session_search`/`session_get`, PR
|
|
243
|
+
- opencode-only features (direct extraction, compaction recovery, code-review coordinator, background sub-agents, TUI toasts, `get_session_info`, `session_search`/`session_get`, all watchers (PR, branch, and command), cross-session chat wake-up) have no MCP counterpart. The `thatch session` CLI subcommands work anywhere - they only need an opencode.db file to read.
|
|
@@ -52,8 +52,9 @@ Four artifacts, all idempotent:
|
|
|
52
52
|
→ `thatch reminder`, `PostToolBatch` → `thatch buffer-batch`,
|
|
53
53
|
`UserPromptSubmit` → `thatch flush-tools`. Nested format:
|
|
54
54
|
`{hooks:{Event:{hooks:[{type,command}]}}}`.
|
|
55
|
-
4. **Skills** --- installs to
|
|
56
|
-
|
|
55
|
+
4. **Skills** --- installs to the scope's skills dir: the repo's
|
|
56
|
+
`.claude/skills/` for project-local, `$CLAUDE_CONFIG_DIR/skills/` for
|
|
57
|
+
`--global`. `SHARED_SKILLS` only --- the
|
|
57
58
|
code-review coordinator requires sub-agent dispatch, which Claude Code
|
|
58
59
|
does not support.
|
|
59
60
|
|
|
@@ -73,7 +74,9 @@ Four artifacts, same idempotent pattern:
|
|
|
73
74
|
`{version:1, hooks:{event:[{command}]}}`. Three hooks: `sessionStart`
|
|
74
75
|
→ `thatch reminder --json`, `postToolUse` → `thatch buffer-tool`,
|
|
75
76
|
`beforeSubmitPrompt` → `thatch flush-tools --json`.
|
|
76
|
-
4. **Skills** --- installs to
|
|
77
|
+
4. **Skills** --- installs to the scope's skills dir: the repo's
|
|
78
|
+
`.cursor/skills/` for project-local, `$CURSOR_CONFIG_DIR/skills/` for
|
|
79
|
+
`--global` (shared only).
|
|
77
80
|
|
|
78
81
|
### Marker system (`appendBlock`)
|
|
79
82
|
|
|
@@ -184,8 +187,12 @@ example, an older `thatch echo` hook is replaced with the current
|
|
|
184
187
|
1. **All operations are idempotent.** Re-running setup updates drifted content
|
|
185
188
|
without clobbering. Instructions use markers, hooks filter by `thatch`
|
|
186
189
|
string, MCP config preserves existing servers, skills diff before writing.
|
|
187
|
-
2. **Skills
|
|
188
|
-
|
|
190
|
+
2. **Skills follow the setup scope.** Project-local installs write skills to
|
|
191
|
+
the repo's `.claude/skills/` / `.cursor/skills/` (Claude Code and Cursor
|
|
192
|
+
both discover project skills there); `--global` writes to the user config
|
|
193
|
+
dirs. Setup reports the directory plus added/updated/removed/unchanged
|
|
194
|
+
counts, and notes (without touching) any thatch skills found in the
|
|
195
|
+
opposite scope.
|
|
189
196
|
3. **`appendBlock` leaves content alone if markers do not parse.** A start
|
|
190
197
|
marker without an end marker means the file was edited externally. Writing
|
|
191
198
|
would risk clobbering content after the start marker.
|