@jeffober/thatch 0.1.41 → 0.1.43
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 +28 -11
- package/artifacts/skills/thatch-clear-writing.md +77 -0
- package/artifacts/skills/thatch-coding-workflow.md +1 -0
- package/bin/release +4 -1
- package/bin/thatch +268 -55
- package/docs/dev/README.md +7 -4
- package/docs/dev/features/README.md +1 -1
- package/docs/dev/features/cli.md +31 -11
- package/docs/dev/features/cross-session-chat.md +141 -67
- package/docs/dev/features/multi-host.md +4 -3
- package/docs/dev/features/session-lifecycle.md +24 -1
- package/docs/dev/features/watchers.md +113 -23
- package/docs/dev/gotchas.md +9 -0
- package/docs/dev/mcp-parity.md +2 -1
- package/docs/dev/skills.md +1 -0
- package/docs/user/README.md +19 -3
- package/docs/user/cli.md +68 -14
- package/docs/user/cross-session-chat.md +80 -27
- package/docs/user/skills.md +1 -0
- package/docs/user/watchers.md +61 -16
- package/package.json +2 -1
- package/src/chat-names.ts +6 -7
- package/src/chat.ts +553 -152
- package/src/commands.ts +97 -0
- package/src/config.ts +34 -1
- package/src/db.ts +67 -7
- package/src/git.ts +22 -0
- package/src/index.ts +198 -25
- package/src/prompts.ts +142 -50
- package/src/skills.ts +1 -0
- package/src/tool-defs.ts +301 -98
- package/src/tools.ts +2 -0
- package/src/watchers.ts +358 -23
|
@@ -25,7 +25,7 @@ system, see [../skills.md](../skills.md). For non-obvious invariants, see
|
|
|
25
25
|
| [deduplication.md](deduplication.md) | Deduplication system (find pairs, verdict lifecycle) |
|
|
26
26
|
| [sideband.md](sideband.md) | Sideband IPC (Unix socket for MCP host hook processes) |
|
|
27
27
|
| [compaction-recovery.md](compaction-recovery.md) | Compaction recovery (opencode compaction hooks) |
|
|
28
|
-
| [session-lifecycle.md](session-lifecycle.md) | Session lifecycle management (opencode events, child tracking) |
|
|
28
|
+
| [session-lifecycle.md](session-lifecycle.md) | Session lifecycle management (opencode events, child tracking, wrap-up commands) |
|
|
29
29
|
| [watchers.md](watchers.md) | Event-driven notifications (in-memory registry, background poller, PR watching) |
|
|
30
30
|
| [cross-session-chat.md](cross-session-chat.md) | Cross-session chat (shared SQLite directory and inbox, per-process wake-up delivery) |
|
|
31
31
|
| [notifications.md](notifications.md) | Out-of-band notification (banner + voice) and the user config file |
|
package/docs/dev/features/cli.md
CHANGED
|
@@ -22,7 +22,7 @@ The thatch CLI is a Bun script at `bin/thatch`. It provides memory inspection, M
|
|
|
22
22
|
| `setup --claude [--cursor] [--global]` | none | `--claude`, `--cursor`, `--global` | none | Install config + instructions + hooks + skills |
|
|
23
23
|
| `session list/get/transcript/search` | per subcommand | `-s/--session`, `--id`, `--after`, `--before`, `--regex`, `--limit` | none | Read-only archaeology on the opencode session database (JSONL output) |
|
|
24
24
|
| `chat list` | none | none | none | Registered chat sessions: name, human-readable age, project, topic |
|
|
25
|
-
| `chat tail
|
|
25
|
+
| `chat tail` | none | `--once`, `--limit N\|all`, `--match RE` (repeatable), `--from NAME`, `--to NAME`, `--since DT`, `--until DT` | none | Follow cross-session chat: sent and read events. Backlog defaults to the last 20 messages; filters apply to follow mode too. |
|
|
26
26
|
| (unknown) | none | none | none | Print usage, exit 1 |
|
|
27
27
|
|
|
28
28
|
## Global behavior
|
|
@@ -46,16 +46,36 @@ Inherits stdio, exits with the child's exit code. Errors and exits 1 if none fou
|
|
|
46
46
|
|
|
47
47
|
Read-only window on the cross-session chat directory ([cross-session-chat.md](cross-session-chat.md)):
|
|
48
48
|
|
|
49
|
-
- `chat list` renders the roster
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
49
|
+
- `chat list` renders the roster as aligned columns under a header row
|
|
50
|
+
(NAME/AGE/STATUS/PROJECT/TOPIC), colorized only when stdout is a TTY
|
|
51
|
+
(`formatChatRoster()` in `bin/thatch`; pipes and QA runners get plain
|
|
52
|
+
text). Status reuses the `isStale()` staleness convention.
|
|
53
|
+
- `chat tail` follows the message stream as styled cards: label chips,
|
|
54
|
+
colored names, dimmed timestamps, topic annotations after names, and a
|
|
55
|
+
drawn rule between cards. On a TTY, sent bodies render as markdown via
|
|
56
|
+
an external renderer (`glow`, then `gum format`; plain fallback):
|
|
57
|
+
selection (`selectChatBodyRenderer()`) and body cleanup
|
|
58
|
+
(`cleanRenderedBody()`) live in `src/chat.ts`, unit-tested there;
|
|
59
|
+
`bin/thatch` owns the TTY/NO_COLOR gate, the once-per-process cache,
|
|
60
|
+
and the spawn. The card renderer is
|
|
61
|
+
`formatChatTailCard()` (optional `renderBody` hook; the read-event
|
|
62
|
+
clip is never rendered) and the event shaping is `chatTailDiff()` in
|
|
63
|
+
`src/chat.ts`, unit-tested there; the single-line `formatChatTailEvent()`
|
|
64
|
+
remains for tests and compact contexts. `--once` prints one snapshot
|
|
65
|
+
and exits, because a snapshot has no previous state to diff read
|
|
66
|
+
events against.
|
|
67
|
+
- The backlog renders only the last `CHAT_TAIL_DEFAULT_LIMIT` (20) messages;
|
|
68
|
+
`chatTailBacklog()` in `src/chat.ts` filters the feed, seeds the diff
|
|
69
|
+
state from every feed row (so the limit hides cards, not history, and a
|
|
70
|
+
mid-follow rename or unregister cannot resurface old rows as sent
|
|
71
|
+
events), and reports the elided count for
|
|
72
|
+
the CLI's stderr note. `filterChatTailRows()` ANDs body regexes
|
|
73
|
+
(`--match`, repeatable), rendered-name substrings (`--from`/`--to`,
|
|
74
|
+
case-insensitive), and a half-open `--since`/`--until` window on
|
|
75
|
+
`created_at`; the same filter applies to follow polls, so non-matching
|
|
76
|
+
messages stay invisible (their read events too). A `--until` in the past
|
|
77
|
+
exits after the backlog; one in the future follows until the window
|
|
78
|
+
closes. `parseChatTimeBound()` accepts `YYYY-MM-DD [HH:MM]` local time.
|
|
59
79
|
|
|
60
80
|
## Environment variables
|
|
61
81
|
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# Cross-Session Chat
|
|
2
2
|
|
|
3
3
|
Cross-session chat lets independent opencode sessions on one machine message
|
|
4
|
-
each other through thatch:
|
|
5
|
-
|
|
6
|
-
recipient is woken with a prompt when mail arrives, so
|
|
7
|
-
route through the human ("ask the other session
|
|
8
|
-
green" instead of "Jeff, go ask").
|
|
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
9
|
|
|
10
10
|
User-facing behavior is documented in
|
|
11
11
|
[docs/user/cross-session-chat.md](../../user/cross-session-chat.md).
|
|
@@ -14,8 +14,11 @@ User-facing behavior is documented in
|
|
|
14
14
|
|
|
15
15
|
- `thatch_chat_register` / `thatch_chat_list` / `thatch_chat_send` /
|
|
16
16
|
`thatch_chat_read` / `thatch_chat_unregister` / `thatch_chat_broadcast`
|
|
17
|
-
tools (shared across hosts; opencode injects identity, MCP hosts
|
|
18
|
-
|
|
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)
|
|
19
22
|
- A shared SQLite directory and inbox in thatch.db, writable by any opencode
|
|
20
23
|
process on the machine
|
|
21
24
|
- A per-process poller that heartbeats hosted sessions and delivers wake
|
|
@@ -54,40 +57,73 @@ outlives a process, so liveness needs a signal beyond process lifetime.
|
|
|
54
57
|
|
|
55
58
|
### Registration and identity
|
|
56
59
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
+
Registration also captures the checkout kind (`worktree` column):
|
|
74
|
+
`detectWorktreeKind` (src/git.ts) classifies the session's serving
|
|
75
|
+
directory once at registration - a `.git` file means a linked git
|
|
76
|
+
worktree, a `.git` directory means the project root, no `.git` reads as
|
|
77
|
+
undetected. The roster shows it as a `loc:` token so sessions coordinating
|
|
78
|
+
on a shared tree can tell who sits where.
|
|
79
|
+
|
|
80
|
+
`chat_register` (no arguments) is the explicit path: idempotent ensure for
|
|
81
|
+
opencode (identity is the host session ID, which the model cannot know or
|
|
82
|
+
forge), a fresh identity per conversation on MCP hosts, or a reclaim of an
|
|
83
|
+
existing name via `as`. Re-registering keeps the name. Unregistered
|
|
84
|
+
sessions are invisible and unmessageable in both directions. Only
|
|
85
|
+
top-level sessions should register - sub-agent children are ephemeral -
|
|
86
|
+
which the tool descriptions and system prompt state; there is no
|
|
87
|
+
mechanical barrier, a documented trust posture.
|
|
88
|
+
|
|
89
|
+
### Leave tombstones
|
|
90
|
+
|
|
91
|
+
Every exit path - `chat_unregister`, `session.deleted`, and a greenlit
|
|
92
|
+
`/thatch/exit` (the plugin unregisters the exiting session before
|
|
93
|
+
publishing `app.exit`, so the roster stops advertising a host that is
|
|
94
|
+
about to vanish; the exit template's checklist step asks the model to do
|
|
95
|
+
the same earlier) - writes a
|
|
96
|
+
`chat_leave_tombstones` row in the same transaction that deletes the
|
|
97
|
+
directory row. The tombstone is what makes a leave stick: both
|
|
98
|
+
registration paths consult it (the plugin's idle auto-registerer is
|
|
99
|
+
suppressed outright; the MCP hook's ensure-register prints the leave line
|
|
100
|
+
instead of rejoining), so a leave cannot be silently undone by the next
|
|
101
|
+
idle moment. The gate lives in `ChatStore.register` via `#tombstoneGate`
|
|
102
|
+
(src/chat.ts), which also closes the in-flight race: an auto-register
|
|
103
|
+
IIFE whose session was deleted during its title-fetch await finds the
|
|
104
|
+
tombstone and stops instead of resurrecting a dead session. An explicit
|
|
105
|
+
`chat_register` clears the tombstone before re-registering (fresh name -
|
|
106
|
+
assigned identities are never reused), and tombstones age out in the
|
|
107
|
+
hourly prune (7-day TTL; a resumed session after expiry simply
|
|
108
|
+
auto-registers under a fresh name).
|
|
109
|
+
|
|
110
|
+
Because names are system-minted and never recycled, the impersonation
|
|
111
|
+
surface shrinks to whatever the operator runs: every agent on this machine
|
|
78
112
|
is the operator's agent.
|
|
79
113
|
|
|
80
114
|
### Topics
|
|
81
115
|
|
|
82
|
-
The
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
116
|
+
The topic is the roster's "who is working on what" signal. For
|
|
117
|
+
auto-registered sessions it is the live session title, refreshed on every
|
|
118
|
+
idle event - the auto-titler usually lands a real title within a turn or
|
|
119
|
+
two, so the roster converges to the actual work with nobody filling in
|
|
120
|
+
forms. Legacy rows (from the claimed-names era) keep whatever topic was
|
|
121
|
+
set at their registration: free text, sanitized to one roster line
|
|
86
122
|
(whitespace runs collapse, 80-character cap, advisory - oversizing degrades
|
|
87
|
-
rather than errors).
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
123
|
+
rather than errors). There is no tool path to set a topic anymore - topics
|
|
124
|
+
are title-derived for everything the current code mints. Broadcast senders
|
|
125
|
+
benefit most: `chat_broadcast` answers "which of you is working on X?"
|
|
126
|
+
with the roster already in hand.
|
|
91
127
|
|
|
92
128
|
### Messages
|
|
93
129
|
|
|
@@ -114,7 +150,9 @@ A `setInterval` loop (default 30s) runs one cycle per process: heartbeat the
|
|
|
114
150
|
hosted sessions' `last_seen`, then deliver. A crashed process fires no
|
|
115
151
|
`session.deleted`, so `chat_list` marks sessions whose `last_seen` is older
|
|
116
152
|
than the staleness threshold (default 10 minutes) as stale rather than
|
|
117
|
-
hiding them - the operator decides what to do with ghosts.
|
|
153
|
+
hiding them - the operator decides what to do with ghosts. Each roster row
|
|
154
|
+
also renders a human-readable `last seen <age>` (shared `humanAge` helper,
|
|
155
|
+
src/chat.ts, same wording the CLI roster's AGE column uses). `session.deleted`
|
|
118
156
|
is the graceful-exit fast path that unregisters immediately.
|
|
119
157
|
|
|
120
158
|
### Delivery, re-nudge, and the rate cap
|
|
@@ -167,19 +205,39 @@ turn reads. The accepted edge: a real user message whose every visible
|
|
|
167
205
|
part starts with the prefix is skipped the same way, losing that one turn's
|
|
168
206
|
advisory nudges.
|
|
169
207
|
|
|
208
|
+
### The untrusted-content frame
|
|
209
|
+
|
|
210
|
+
Message bodies are other agents' text delivered verbatim into the reading
|
|
211
|
+
model's context - a prompt-injection surface by construction. The watcher
|
|
212
|
+
privacy model solves the same surface with pointer-only notifications, but
|
|
213
|
+
chat's payload IS the body, so `chat_read` frames the boundary instead:
|
|
214
|
+
`chatInboxFrame()` (src/prompts.ts) wraps the message lines in
|
|
215
|
+
begin/end fences with an explicit do-not-follow warning. The frame exists only in
|
|
216
|
+
the tool output - persisted rows are untouched - and pairs with the wake
|
|
217
|
+
nudge's anti-loop rule and the isChatEchoParts skip as the feature's three
|
|
218
|
+
injection-hygiene layers.
|
|
219
|
+
|
|
170
220
|
### CLI access
|
|
171
221
|
|
|
172
222
|
The user watches the conversation without an agent: `thatch chat list`
|
|
173
|
-
renders the roster
|
|
174
|
-
`
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
223
|
+
renders the roster as aligned columns under a header row (TTY-gated
|
|
224
|
+
color; `formatChatRoster()` in `bin/thatch`), and `thatch chat tail`
|
|
225
|
+
follows the message stream as cards, with sent bodies rendered as
|
|
226
|
+
markdown on a TTY via `glow`/`gum` (plain fallback, never in pipes).
|
|
227
|
+
The backlog renders only the
|
|
228
|
+
last `CHAT_TAIL_DEFAULT_LIMIT` (20) messages via `chatTailBacklog()`;
|
|
229
|
+
`filterChatTailRows()` narrows every feed snapshot (backlog and follow
|
|
230
|
+
polls alike) by body regexes, participant-name substrings, and a
|
|
231
|
+
half-open time window. The diff itself is `chatTailDiff()` (unit-tested
|
|
232
|
+
in `tests/chat.test.ts`): sent cards on first view, then new sends and
|
|
233
|
+
newly-read messages per poll, with the diff state seeded from the full
|
|
234
|
+
feed so neither the limit nor a mid-follow rename/unregister can
|
|
235
|
+
resurface old rows as sent events. `chat_messages.via_broadcast` marks
|
|
236
|
+
fan-out rows so the tail renders one `-> broadcast` card per recipient
|
|
237
|
+
instead of what would otherwise look like identical direct sends. The
|
|
238
|
+
CLI takes no chat write actions (the wake machinery owns those paths);
|
|
239
|
+
the only writes it can trigger are the schema migrations that run
|
|
240
|
+
whenever any thatch process opens the database.
|
|
183
241
|
|
|
184
242
|
## Interactions with other features
|
|
185
243
|
|
|
@@ -197,14 +255,23 @@ process opens the database.
|
|
|
197
255
|
non-thatch tool calls made during a chat notification turn are buffered
|
|
198
256
|
and extracted like any other turn's.
|
|
199
257
|
- Multi-host ([multi-host.md](multi-host.md)): the chat tools are shared -
|
|
200
|
-
MCP hosts
|
|
201
|
-
the
|
|
202
|
-
|
|
203
|
-
`
|
|
204
|
-
can start a turn in the client's conversation.
|
|
258
|
+
MCP hosts get their identity from the thatch hook, which ensure-registers
|
|
259
|
+
the conversation's stable session ID (`mcp_<hash of host session id>`)
|
|
260
|
+
and prints the assigned name plus unread count on every prompt; chat
|
|
261
|
+
tools take that name as `as`. Wake-up delivery remains opencode-only: no
|
|
262
|
+
MCP server can start a turn in the client's conversation.
|
|
205
263
|
|
|
206
264
|
## Multi-host delivery tiers
|
|
207
265
|
|
|
266
|
+
Chat works on every host, and can be disabled entirely: `chat.enabled:
|
|
267
|
+
false` in the thatch config makes every chat tool refuse (a `[disabled]`
|
|
268
|
+
reply naming the re-enable path), stops the poller at init, omits the
|
|
269
|
+
system prompt's chat section, and silences the hook lines. The tools
|
|
270
|
+
re-read the config per call, so a toggle applies immediately; the poller
|
|
271
|
+
gate is read once at init and needs a restart. The hook lines gate on the
|
|
272
|
+
same config, so a disabled feature never advertises itself through any
|
|
273
|
+
surface.
|
|
274
|
+
|
|
208
275
|
Chat works on every host. Earlier designs for the MCP path - a JSONL
|
|
209
276
|
temp-file inbox, a socket or daemon - were rejected: the DB already is the
|
|
210
277
|
inbox, and MCP hosts cannot hold connections across turns, so both
|
|
@@ -217,12 +284,15 @@ wake analog is the existing hook channel.
|
|
|
217
284
|
| claude code | yes | flush-tools hook line | no |
|
|
218
285
|
| cursor | yes | flush-tools hook line | no |
|
|
219
286
|
|
|
220
|
-
MCP identity is the
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
287
|
+
MCP identity is anchored by the host hook: `chatHookLine(sessionID)` in
|
|
288
|
+
`bin/thatch` ensure-registers `mcp_<sha256(host session id)[0:12]>` on
|
|
289
|
+
every prompt and prints the assigned name plus unread count, so the
|
|
290
|
+
identity is stable across the ephemeral conversations those harnesses run
|
|
291
|
+
(and a conversation without the hook gets a fresh per-conversation
|
|
292
|
+
identity from `chat_register`). `chat_status` is the quiet check
|
|
293
|
+
(registered flag + pending/total); the flush-tools hook line names the
|
|
294
|
+
caller's identity and mailbox, and is absent entirely when chat is
|
|
295
|
+
disabled - absence is silent by construction.
|
|
226
296
|
Staleness semantics differ by kind: an opencode row's stale age means the
|
|
227
297
|
process is gone (broadcast skips it), while an mcp row's age only means
|
|
228
298
|
"between turns" (broadcast always delivers).
|
|
@@ -231,19 +301,23 @@ process is gone (broadcast skips it), while an mcp row's age only means
|
|
|
231
301
|
|
|
232
302
|
Timing constants live in `src/chat.ts`: poll interval 30s, staleness
|
|
233
303
|
threshold 10 minutes, re-nudge window 15 minutes, nudge cap 6 per recipient
|
|
234
|
-
per hour. There are no environment overrides yet; add
|
|
235
|
-
`THATCH_WATCH_POLL_SECONDS` works if a user needs them.
|
|
304
|
+
per hour, auto-row TTL 7 days. There are no environment overrides yet; add
|
|
305
|
+
them the way `THATCH_WATCH_POLL_SECONDS` works if a user needs them.
|
|
236
306
|
|
|
237
307
|
## Source files
|
|
238
308
|
|
|
239
|
-
- `src/chat.ts` - ChatStore (directory + inbox SQL,
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
- `src/
|
|
244
|
-
|
|
309
|
+
- `src/chat.ts` - ChatStore (directory + inbox SQL, assigned-name draw:
|
|
310
|
+
slugify, counter bump, insert, prune, topic refresh), ChatPoller
|
|
311
|
+
(heartbeat, gated delivery, re-nudge, rate cap, hourly prune sweep),
|
|
312
|
+
staleness helper
|
|
313
|
+
- `src/chat-names.ts` - the static display-name pool (nomenclater style),
|
|
314
|
+
the fallback base for title-less registrations
|
|
315
|
+
- `src/db.ts` - the chat tables in schema init (including
|
|
316
|
+
chat_name_counters and the auto column), the NOCASE collation migration
|
|
317
|
+
and topic column migration, delegated methods
|
|
245
318
|
- `src/tool-defs.ts` - the chat tool definitions
|
|
246
|
-
- `src/index.ts` - poller construction, delivery closure, idle
|
|
319
|
+
- `src/index.ts` - poller construction, delivery closure, idle
|
|
320
|
+
auto-registration + topic refresh, idle flush,
|
|
247
321
|
session.deleted unregister, dispose, transcript echo in
|
|
248
322
|
tool.execute.after
|
|
249
323
|
- `src/prompts.ts` - `chatNotificationNudge()`, `chatEchoText()` /
|
|
@@ -21,7 +21,8 @@ host-agnostic tools plus the opencode-only tools, which the MCP server
|
|
|
21
21
|
filters out. The opencode-only set covers host-session capabilities MCP
|
|
22
22
|
hosts lack: `get_session_info`, `session_search`/`session_get` (no session
|
|
23
23
|
concept or session database), the `watch_create`/`watch_branch_create`/
|
|
24
|
-
`watch_list`/`watch_cancel` watcher tools (no
|
|
24
|
+
`watch_command_create`/`watch_list`/`watch_cancel` watcher tools (no
|
|
25
|
+
poller or proactive-prompt
|
|
25
26
|
channel), and the `chat_register`/`chat_list`/`chat_send`/`chat_read`/
|
|
26
27
|
`chat_unregister`/`chat_broadcast` chat tools (no session identity or
|
|
27
28
|
wake-up channel). The
|
|
@@ -188,7 +189,7 @@ behavior, with some features having no MCP counterpart.
|
|
|
188
189
|
| Tool batching | N/A (in-process) | PostToolBatch (batch) | postToolUse (per-tool) |
|
|
189
190
|
| `get_session_info` (session identity) | Yes | No | No |
|
|
190
191
|
| `session_search` / `session_get` (conversation archaeology) | Yes (tools) | CLI only (`thatch session ...`) | CLI only (`thatch session ...`) |
|
|
191
|
-
| `watch_*` (PR and
|
|
192
|
+
| `watch_*` (PR, branch, and command watchers) | Yes (tools) | No | No |
|
|
192
193
|
| `chat_*` (cross-session chat) | Yes | No | No |
|
|
193
194
|
|
|
194
195
|
For the full parity matrix, see [../mcp-parity.md](../mcp-parity.md).
|
|
@@ -239,4 +240,4 @@ the opencode-only skills require sub-agent dispatch or in-process hooks.
|
|
|
239
240
|
- The MCP server always exposes bare names. The `mcp__thatch__` prefix is applied by the MCP client, not thatch.
|
|
240
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.
|
|
241
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.
|
|
242
|
-
- opencode-only features (direct extraction, compaction recovery, code-review coordinator, background sub-agents, TUI toasts, `get_session_info`, `session_search`/`session_get`,
|
|
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.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Session Lifecycle Management (opencode)
|
|
2
2
|
|
|
3
|
-
opencode emits bus events for session lifecycle changes. Thatch subscribes to these events to manage the extraction pipeline, send session-start reminders,
|
|
3
|
+
opencode emits bus events for session lifecycle changes. Thatch subscribes to these events to manage the extraction pipeline, send session-start reminders, handle child session cleanup, and resolve wrap-up commands.
|
|
4
4
|
|
|
5
5
|
## What it does
|
|
6
6
|
|
|
@@ -11,6 +11,7 @@ opencode emits bus events for session lifecycle changes. Thatch subscribes to th
|
|
|
11
11
|
- Sub-agent lifecycle: task-dispatched sub-agents complete accepted entries but are not deleted
|
|
12
12
|
- Session error recovery: child errors requeue parent's accepted entries
|
|
13
13
|
- Session deletion recovery: child deletion requeues; parent deletion completes accepted entries
|
|
14
|
+
- Wrap-up commands: `/thatch/compact` and `/thatch/exit` arm a greenlight check resolved on the next idle
|
|
14
15
|
|
|
15
16
|
## How it works
|
|
16
17
|
|
|
@@ -56,9 +57,29 @@ When a task-dispatched sub-agent (not created by `triggerExtraction`) goes idle:
|
|
|
56
57
|
|
|
57
58
|
When a parent session (no parentID) goes idle:
|
|
58
59
|
|
|
60
|
+
- Wrap-up resolution first (below): a pending `/thatch/compact` or `/thatch/exit` resolves here, and a greenlit action returns early
|
|
59
61
|
- If not compacting, not already extracting, and buffer has pending interactions: `triggerExtraction(sessionID)`
|
|
60
62
|
- On failure: log error, clear `extracting` flag (nudge path takes over as fallback on next chat.message)
|
|
61
63
|
|
|
64
|
+
### Wrap-up commands (`/thatch/compact`, `/thatch/exit`)
|
|
65
|
+
|
|
66
|
+
User-invoked slash commands, shipped as command markdown synced by the plugin (see `src/commands.ts`). The template instructs the model to flush pending fact extraction (`thatch_get_extraction_payload` + `thatch_extraction_done`), finish promised memory writes, and surface unaddressed todos -- then end its response with a greenlight token (`THATCH_COMPACT_READY` / `THATCH_EXIT_READY`) only when safe to proceed. Text typed after the command is forwarded into the prompt: the templates substitute opencode's `$ARGUMENTS` ahead of the checklist, so `/thatch/exit nice work` reads as "nice work" followed by the wrap-up instructions.
|
|
67
|
+
|
|
68
|
+
1. `command.execute.before` arms the session in `pendingWrapUp` when the command runs
|
|
69
|
+
2. On the session's next idle, the plugin fetches the session's messages via the SDK client and checks the final assistant message's trailing text for the token (trimmed, exact match)
|
|
70
|
+
3. Token present: trigger the TUI action and return early -- compaction is starting (the checklist drained the buffer) or the process is exiting
|
|
71
|
+
- compact: `client.tui.executeCommand({ body: { command: "session_compact" } })`. The execute-command route only accepts legacy alias names; `session_compact` maps to the TUI's `session.compact` action, the same thing the built-in `/compact` runs
|
|
72
|
+
- exit: unregister the session from the chat directory first (a greenlit exit's host is about to vanish; other sessions must stop addressing mail to it), then `client.tui.publish({ body: { type: "tui.command.execute", properties: { command: "app.exit" } } })`. No exit alias exists, so the TUI keymap command is published directly. The exit template's checklist also asks the model to call `thatch_chat_unregister` as its last persistence step; the plugin-side unregister is the deterministic backstop. Compaction deliberately does NOT unregister - the session continues
|
|
73
|
+
4. Token absent: warning toast pointing at the blockers in the response, then fall through -- the model may have buffered tool interactions that still need extraction
|
|
74
|
+
|
|
75
|
+
### Command file install
|
|
76
|
+
|
|
77
|
+
`installOpencodeCommands` syncs the command markdown into
|
|
78
|
+
`$XDG_CONFIG_HOME/opencode/command/thatch/` on every plugin load, writing only
|
|
79
|
+
files whose on-disk content differs (template updates self-heal). opencode
|
|
80
|
+
loads config -- including command discovery -- before plugins, so a
|
|
81
|
+
first-ever install is invisible until the next server start.
|
|
82
|
+
|
|
62
83
|
### session.error (child session)
|
|
63
84
|
|
|
64
85
|
- `requeueAccepted(parentID)` -- move parent's accepted entries back to pending (the extractor never processed them)
|
|
@@ -73,6 +94,7 @@ When a parent session (no parentID) goes idle:
|
|
|
73
94
|
|
|
74
95
|
- `completeAccepted(id)` -- a deleted parent takes its accepted entries with it
|
|
75
96
|
- Clear `extracting` for the parent
|
|
97
|
+
- Clear `pendingWrapUp` for the parent
|
|
76
98
|
|
|
77
99
|
### session.compacted
|
|
78
100
|
|
|
@@ -87,6 +109,7 @@ When a parent session (no parentID) goes idle:
|
|
|
87
109
|
- **extracting**: `Set<parentID>` -- parent IDs with an active direct-extraction child (suppresses nudge)
|
|
88
110
|
- **extractionChildren**: `Set<childID>` -- distinguishes extraction children from task-dispatched sub-agents
|
|
89
111
|
- **compacting**: `Set<sessionID>` -- sessions currently being compacted (suppresses nudges)
|
|
112
|
+
- **pendingWrapUp**: `Map<sessionID, {token, kind}>` -- wrap-up commands awaiting their greenlight check; armed by `command.execute.before`, resolved on the next idle, cleared on session deletion
|
|
90
113
|
- **missedNudges**: `Map<sessionID, number>` -- extraction nudge escalation counter
|
|
91
114
|
|
|
92
115
|
## Interactions with other features
|