@jeffober/thatch 0.2.1 → 0.2.3
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/artifacts/skills/thatch-plan-refinement.md +3 -2
- package/artifacts/skills/thatch-refine-infra.md +66 -0
- package/artifacts/skills/thatch-refine-oss-contribution.md +68 -0
- package/artifacts/skills/thatch-refine-personal.md +61 -0
- package/artifacts/skills/thatch-refine-shared-lib.md +69 -0
- package/artifacts/skills/thatch-refine-spike.md +53 -0
- package/artifacts/skills/thatch-refine-team-app.md +69 -0
- package/artifacts/skills/thatch-refine.md +84 -0
- package/bin/thatch +9 -5
- package/docs/dev/README.md +21 -25
- package/docs/dev/features/cli.md +1 -0
- package/docs/dev/features/extraction.md +120 -81
- package/docs/dev/features/nudge-pipeline.md +44 -39
- package/docs/dev/features/session-lifecycle.md +34 -20
- package/docs/dev/features/watchers.md +9 -0
- package/docs/dev/gotchas.md +33 -26
- package/docs/dev/mcp-parity.md +17 -18
- package/docs/dev/setup-and-hooks.md +1 -1
- package/docs/dev/skills.md +8 -1
- package/docs/user/README.md +7 -0
- package/docs/user/commands.md +8 -0
- package/docs/user/skills.md +12 -0
- package/docs/user/watchers.md +6 -0
- package/package.json +1 -1
- package/src/chat.ts +1 -1
- package/src/commands.ts +23 -3
- package/src/extract-queue.ts +16 -1
- package/src/extraction.ts +4 -3
- package/src/opencode/v2.ts +4 -3
- package/src/prompts.ts +26 -33
- package/src/runtime.ts +119 -106
- package/src/skills.ts +7 -0
- package/src/tool-defs.ts +39 -31
- package/src/watchers.ts +31 -0
|
@@ -15,19 +15,23 @@ directly for conversation-derived knowledge.
|
|
|
15
15
|
|
|
16
16
|
- Buffers every non-`thatch_*`, non-`skill`, non-`task` tool call for later
|
|
17
17
|
extraction
|
|
18
|
-
- Two paths: **direct extraction** via a child session (
|
|
19
|
-
**nudge-based extraction** (
|
|
20
|
-
|
|
21
|
-
-
|
|
22
|
-
|
|
18
|
+
- Two paths: **direct extraction** via a plugin-created child session (the
|
|
19
|
+
only opencode path) and **nudge-based extraction** (MCP hosts only)
|
|
20
|
+
- The MCP extraction nudge escalates: polite (0–1 missed), insistent (2),
|
|
21
|
+
all-caps shouting (3+). There is deliberately no opencode nudge: the
|
|
22
|
+
model-driven handshake it required raced its own state machine (acks
|
|
23
|
+
before fetches, no-claim completions wiping in-flight sets, mis-targeted
|
|
24
|
+
session ids making every transition a silent no-op) and was removed —
|
|
25
|
+
see "Key invariants"
|
|
23
26
|
- `thatch_get_extraction_payload` fetches queued interactions as JSON,
|
|
24
|
-
keeping the full payload out of the main session's context window
|
|
25
|
-
|
|
27
|
+
keeping the full payload out of the main session's context window; the
|
|
28
|
+
fetch is also the **delivery record** (it claims the entries for that
|
|
29
|
+
fetcher)
|
|
30
|
+
- `thatch_extraction_done` completes only the calling child's claimed
|
|
31
|
+
delivery — never the whole accepted set
|
|
26
32
|
- AMQP-style buffer lifecycle for opencode: pending → accepted → completed,
|
|
27
33
|
with requeue on failure
|
|
28
34
|
- File-backed JSONL queue for MCP hosts (no cross-call state)
|
|
29
|
-
- Parent-child session drain: the child drains the parent's snapshot,
|
|
30
|
-
preserving interleaved-turn entries added after the snapshot was taken
|
|
31
35
|
|
|
32
36
|
## How it works
|
|
33
37
|
|
|
@@ -71,57 +75,70 @@ calls are buffered for later extraction.
|
|
|
71
75
|
- Filters the same tools as opencode (`mcp__thatch__memory_remember`,
|
|
72
76
|
`mcp__thatch__extraction_done`, `mcp__thatch__*`, `skill`, `task`, `agent`)
|
|
73
77
|
|
|
74
|
-
### Direct extraction (opencode,
|
|
78
|
+
### Direct extraction (opencode, the only opencode path)
|
|
75
79
|
|
|
76
80
|
When a parent session goes idle (`session.status` idle event) with pending
|
|
77
|
-
buffer interactions
|
|
81
|
+
buffer interactions — after `requeueStaleAccepted()` has returned any
|
|
82
|
+
timed-out accepted entries to pending:
|
|
78
83
|
|
|
79
84
|
1. `triggerExtraction` adds the parent ID to the `extracting` set — this
|
|
80
|
-
|
|
85
|
+
gates re-triggering while a child runs
|
|
81
86
|
2. Peeks the buffer to count pending interactions
|
|
82
87
|
3. Creates a child session via
|
|
83
88
|
`client.session.create({ parentID, title: "thatch-extraction" })`
|
|
84
89
|
4. The `session.created` event fires, populating `childToParent` and
|
|
85
|
-
`parentSnapshots` (a snapshot of the full pending buffer at dispatch
|
|
90
|
+
`parentSnapshots` (a snapshot of the full pending buffer at dispatch
|
|
91
|
+
time — journal-recovery data only; claims subsumed the snapshot drain)
|
|
86
92
|
5. Adds the child ID to the `extractionChildren` set
|
|
87
|
-
6. Prompts the child with `extractionDirectPrompt(count, sessionID)`
|
|
93
|
+
6. Prompts the child with `extractionDirectPrompt(count, sessionID)` — the
|
|
94
|
+
plugin interpolates the parent's session ID into the prompt, so no
|
|
95
|
+
model ever has to copy one
|
|
88
96
|
7. If background sub-agents are enabled
|
|
89
97
|
(`OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS`): `promptAsync`. Otherwise:
|
|
90
98
|
fire-and-forget `prompt`
|
|
91
99
|
8. On prompt failure: `cleanupChild` removes the child from all maps and
|
|
92
|
-
deletes the child session
|
|
100
|
+
deletes the child session. The entries stay pending; the next idle
|
|
101
|
+
retries (there is no nudge fallback)
|
|
93
102
|
|
|
94
103
|
The child session then:
|
|
95
104
|
|
|
96
|
-
- Calls `thatch_get_extraction_payload`
|
|
105
|
+
- Calls `thatch_get_extraction_payload` — the fetch **claims** the entries
|
|
106
|
+
for this fetcher (claim = accept + delivery record)
|
|
97
107
|
- Runs the [thatch-fact-extractor](../skills.md) skill
|
|
98
108
|
- Writes memories via `thatch_memory_remember`
|
|
99
109
|
- Goes idle
|
|
100
110
|
|
|
101
111
|
On child idle (`session.status` idle event, extraction child):
|
|
102
112
|
|
|
103
|
-
1.
|
|
104
|
-
|
|
105
|
-
|
|
113
|
+
1. `completeClaimed(childID)` — consumes only the entries this child
|
|
114
|
+
claimed via its fetch. A no-claim idle is a no-op: a child that never
|
|
115
|
+
fetched processed nothing, and its idle signal must not drop entries
|
|
116
|
+
another extractor holds
|
|
106
117
|
2. Fires a toast with extraction metrics (only if memories were actually
|
|
107
118
|
written)
|
|
108
|
-
3.
|
|
109
|
-
4.
|
|
110
|
-
5.
|
|
111
|
-
6. Deletes the child session
|
|
119
|
+
3. Cleans up all maps
|
|
120
|
+
4. `consume(childID)` — drains the child's own buffer
|
|
121
|
+
5. Deletes the child session
|
|
112
122
|
|
|
113
|
-
|
|
123
|
+
If the extraction child is deleted before completing,
|
|
124
|
+
`requeueClaimed`/`requeueAccepted` returns what it held to pending. If its
|
|
125
|
+
completion signal never comes at all, `requeueStaleAccepted` (15 min)
|
|
126
|
+
returns the accepted entries to pending — the next idle re-extracts them.
|
|
114
127
|
|
|
115
|
-
|
|
116
|
-
|
|
128
|
+
### Nudge-based extraction (MCP hosts only)
|
|
129
|
+
|
|
130
|
+
Claude Code and Cursor have no SDK client to create child sessions and no
|
|
131
|
+
plugin lifecycle (each hook invocation is a fresh process), so extraction
|
|
132
|
+
there is still model-driven. When the buffer has pending interactions, the
|
|
133
|
+
next user prompt (`UserPromptSubmit`/`beforeSubmitPrompt`) gets an
|
|
134
|
+
extraction nudge telling the agent to spawn a sub-agent for the
|
|
135
|
+
fact-extractor skill.
|
|
117
136
|
|
|
118
|
-
- The next user message (`chat.message` for opencode,
|
|
119
|
-
`UserPromptSubmit`/`beforeSubmitPrompt` for MCP) gets an extraction nudge
|
|
120
137
|
- The nudge carries the session ID and fetch tool name — not the full
|
|
121
138
|
payload — so the sub-agent calls `thatch_get_extraction_payload` to
|
|
122
139
|
retrieve the interactions as a tool response
|
|
123
140
|
|
|
124
|
-
**Nudge escalation** (via the
|
|
141
|
+
**Nudge escalation** (via the file-backed missed counter):
|
|
125
142
|
|
|
126
143
|
| Missed count | Tone |
|
|
127
144
|
|--------------|------|
|
|
@@ -129,26 +146,29 @@ the `extracting` set) **and** the buffer has pending interactions:
|
|
|
129
146
|
| 2 | Insistent |
|
|
130
147
|
| 3+ | All-caps shouting |
|
|
131
148
|
|
|
132
|
-
The
|
|
133
|
-
`
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
149
|
+
The file-backed queue persists until the extractor completes
|
|
150
|
+
(`extraction_done` with the parent's `session_id`) or the parent writes a
|
|
151
|
+
memory itself. The parent's dispatch-time ack resets the escalation
|
|
152
|
+
counter but does NOT drain the queue — a drain at ack time deleted the
|
|
153
|
+
queue before the sub-agent fetched (the accept-before-fetch loss; the
|
|
154
|
+
opencode path removed the handshake for the same failure class).
|
|
137
155
|
|
|
138
156
|
### Buffer lifecycle (opencode, AMQP-style)
|
|
139
157
|
|
|
140
158
|
Buffered interactions move through four states in `ExtractionPipeline`
|
|
141
159
|
(`src/extraction.ts`):
|
|
142
160
|
|
|
143
|
-
- **pending** — interactions in the ring buffer. The
|
|
144
|
-
`
|
|
145
|
-
- **accepted** — moved from pending by `accept()
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
161
|
+
- **pending** — interactions in the ring buffer. The idle trigger
|
|
162
|
+
(`triggerExtraction`) fires on them; there is no model-facing nudge.
|
|
163
|
+
- **accepted** — moved from pending by `accept()` (a parent's legacy ack,
|
|
164
|
+
or a fetcher's claim). Entries are held, not dropped, and are still
|
|
165
|
+
served by the payload provider.
|
|
166
|
+
- **completed** — dropped by `completeClaimed()` (the fetcher's completion
|
|
167
|
+
signal) or `completeAccepted()` (the parent session being deleted —
|
|
168
|
+
nothing exists to replay into).
|
|
169
|
+
- **requeued** — moved back to pending by `requeueClaimed()` /
|
|
170
|
+
`requeueAccepted()` (child error or deletion before completing) or
|
|
171
|
+
`requeueStaleAccepted()` (15-minute completion timeout).
|
|
152
172
|
|
|
153
173
|
Key methods:
|
|
154
174
|
|
|
@@ -157,36 +177,42 @@ Key methods:
|
|
|
157
177
|
| `push()` | Add interaction to pending buffer (capped at 20) |
|
|
158
178
|
| `peek()` | Read without clearing |
|
|
159
179
|
| `consume()` | Delete the session's pending buffer (called on memory write) |
|
|
160
|
-
| `accept()` | Move pending to accepted —
|
|
161
|
-
| `
|
|
162
|
-
| `
|
|
163
|
-
| `
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
`
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
`
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
+
| `accept()` | Move pending to accepted — hold entries (non-destructive) |
|
|
181
|
+
| `claim()` | Accept + record WHICH fetcher received the entries — the delivery record |
|
|
182
|
+
| `completeClaimed()` | Drop only the completing fetcher's claimed entries |
|
|
183
|
+
| `completeAccepted()` | Drop accepted entries — used on parent deletion |
|
|
184
|
+
| `requeueClaimed()` / `requeueAccepted()` | Move held entries back to pending — extractor died |
|
|
185
|
+
| `requeueStaleAccepted()` | Requeue accepted entries held longer than 15 min |
|
|
186
|
+
|
|
187
|
+
### Parent-child delivery (claim semantics)
|
|
188
|
+
|
|
189
|
+
The payload fetch is the delivery record:
|
|
190
|
+
|
|
191
|
+
1. `thatch_get_extraction_payload` with a fetcher identity calls
|
|
192
|
+
`claim(parentID, fetcherID)`: the pending buffer is accepted and the
|
|
193
|
+
fetcher's claim records exactly which entries it received
|
|
194
|
+
2. The fetcher's completion signal (`extraction_done` or
|
|
195
|
+
`memory_remember` from that child) calls `completeClaimed(fetcherID)` —
|
|
196
|
+
consuming only the claimed entries, by reference identity
|
|
197
|
+
3. A completion from a fetcher with no claim is a **no-op** — it processed
|
|
198
|
+
nothing, so it must not drop entries another extractor holds (a sibling
|
|
199
|
+
sub-agent going idle, or a mis-ordered ack, once wiped the whole
|
|
200
|
+
accepted set this way — the accept-then-racing-completion loss)
|
|
201
|
+
4. Entries that arrived after the fetch stay pending; the next idle
|
|
202
|
+
re-extracts them
|
|
180
203
|
|
|
181
204
|
### MCP file-backed queue drain
|
|
182
205
|
|
|
183
206
|
- `drainExtractionQueue(sessionID)` calls `resetMissedCount` +
|
|
184
207
|
`consumeQueue` (deletes the JSONL file)
|
|
185
208
|
- Triggered by: `thatch_extraction_done` called with the parent's
|
|
186
|
-
`session_id
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
consumes the queue inline
|
|
209
|
+
`session_id` (the extractor's completion), or `thatch_memory_remember`
|
|
210
|
+
called by the parent itself
|
|
211
|
+
- `appendBatch` in `extract-queue.ts` self-detects `memory_remember` and
|
|
212
|
+
resets the counter + consumes the queue inline. It detects
|
|
213
|
+
`extraction_done` too, but only resets the counter — draining at the
|
|
214
|
+
parent's dispatch-time ack deleted the queue before the sub-agent
|
|
215
|
+
fetched (the accept-before-fetch loss)
|
|
190
216
|
|
|
191
217
|
### The `extraction_done` tool
|
|
192
218
|
|
|
@@ -197,6 +223,10 @@ continues working while the child extracts.
|
|
|
197
223
|
- The real state transitions happen in the host's post-tool hook
|
|
198
224
|
(`tool.execute.after` for opencode, `PostToolBatch`/`appendBatch` for MCP)
|
|
199
225
|
- The tool exists primarily so the model has a recognizable name to key on
|
|
226
|
+
- On opencode there is no parent ack any more: a parent-side
|
|
227
|
+
`extraction_done` is tolerated as a non-destructive accept (entries
|
|
228
|
+
held, not dropped) for backward compatibility with an in-flight older
|
|
229
|
+
nudge
|
|
200
230
|
|
|
201
231
|
### The `get_extraction_payload` tool
|
|
202
232
|
|
|
@@ -226,14 +256,15 @@ continues working while the child extracts.
|
|
|
226
256
|
- [Memory store](memory-store.md) — extraction writes memories via
|
|
227
257
|
`thatch_memory_remember`
|
|
228
258
|
- [Nudge pipeline](nudge-pipeline.md) — the extraction nudge is tier 1
|
|
229
|
-
(highest priority, returns early)
|
|
259
|
+
(highest priority, returns early) on MCP hosts; opencode's
|
|
260
|
+
`chat.message` runs the recall tier only
|
|
230
261
|
- [Session lifecycle](session-lifecycle.md) — direct extraction is triggered
|
|
231
262
|
by `session.status` idle; child lifecycle is managed by the event handler
|
|
232
263
|
- [Skills](../skills.md) — the thatch-fact-extractor skill is dispatched to
|
|
233
264
|
child sessions
|
|
234
265
|
- [Multi-host](multi-host.md) — in-memory ring buffer for opencode,
|
|
235
266
|
file-backed queue for MCP hosts
|
|
236
|
-
- [Compaction recovery](compaction-recovery.md) —
|
|
267
|
+
- [Compaction recovery](compaction-recovery.md) — nudges are
|
|
237
268
|
suppressed during compaction (tools are blocked)
|
|
238
269
|
|
|
239
270
|
## Source files
|
|
@@ -242,25 +273,33 @@ continues working while the child extracts.
|
|
|
242
273
|
|------|----------------|
|
|
243
274
|
| `src/extraction.ts` | In-memory ring buffer (`ExtractionPipeline`), shared payload builders (`buildExtractionPayload`, `deriveTitle`, `summarizeArgs`) |
|
|
244
275
|
| `src/extract-queue.ts` | File-backed JSONL queue for MCP hosts |
|
|
245
|
-
| `src/runtime.ts` | opencode hooks: `tool.execute.after`, `session.status` idle, `session.created`, `session.error`, `session.deleted`, `chat.message` (
|
|
276
|
+
| `src/runtime.ts` | opencode hooks: `tool.execute.after`, `session.status` idle (direct extraction trigger), `session.created`, `session.error`, `session.deleted`, `chat.message` (recall/prediction/behavior nudges; no extraction nudge) -- shared by the v1/v2 adapters |
|
|
246
277
|
| `bin/thatch` | `buffer-batch`, `buffer-tool`, `flush-tools` subcommands |
|
|
247
278
|
| `src/prompts.ts` | `extractionNudge` (with escalation), `extractionDirectPrompt` |
|
|
248
279
|
|
|
249
280
|
## Key invariants
|
|
250
281
|
|
|
251
|
-
1. **Tool filtering is absolute.** `thatch_*`, `skill`,
|
|
252
|
-
never buffered. Buffering them would echo the store
|
|
253
|
-
a feedback loop.
|
|
254
|
-
2. **
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
282
|
+
1. **Tool filtering is absolute.** `thatch_*`, `skill`, `task`, and
|
|
283
|
+
`subagent` tools are never buffered. Buffering them would echo the store
|
|
284
|
+
into itself or create a feedback loop.
|
|
285
|
+
2. **Extraction never depends on model cooperation on opencode.** The
|
|
286
|
+
plugin creates the extraction child, interpolates the session ID, and
|
|
287
|
+
drives the lifecycle from events. The old model-driven handshake (model
|
|
288
|
+
dispatches, parent acks, child copies the ID) raced its own state
|
|
289
|
+
machine — the September 2026 dispatch-loop report (accept-before-fetch
|
|
290
|
+
loss + never-terminating re-nudge) — and was removed. The opencode
|
|
291
|
+
`chat.message` hook computes no extraction nudge.
|
|
292
|
+
3. **Completion is claim-scoped.** Only a fetcher that recorded a claim
|
|
293
|
+
(via `get_extraction_payload`) can consume entries, and only the entries
|
|
294
|
+
it received. No-claim completions are no-ops; no transition drops
|
|
295
|
+
entries another extractor holds. The stale reaper bounds any orphan
|
|
296
|
+
linger at 15 minutes.
|
|
259
297
|
4. **`tool.execute.after` is a plugin hook, not a bus event.** The event bus
|
|
260
298
|
has no such event. Moving the buffering logic into the `event` handler
|
|
261
299
|
silently never fires it.
|
|
262
300
|
5. **MCP host hooks must be silent.** `PostToolBatch`/`postToolUse` produce
|
|
263
301
|
no stdout — only `flush-tools` prints. Any stdout delays the agent loop.
|
|
264
|
-
6. **The
|
|
265
|
-
|
|
266
|
-
|
|
302
|
+
6. **The MCP queue is durable until the extractor completes.** The
|
|
303
|
+
parent's dispatch-time ack must not drain it (see the invariant above);
|
|
304
|
+
the drain happens at the extractor's completion or the parent's own
|
|
305
|
+
memory write.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Unless noted, `client.*` names below are the opencode v1 mapping; the v2 equivalents and degrades are in [opencode-plugin.md](opencode-plugin.md).
|
|
4
4
|
|
|
5
|
-
Runs on every user message.
|
|
5
|
+
Runs on every user message. Priority tiers share one embedding
|
|
6
6
|
computation. The pipeline injects *synthetic parts*---text the model sees in
|
|
7
7
|
the conversation but the user does not see in the TUI. This is the inverse of
|
|
8
8
|
toast notifications, which are TUI-visible but model-invisible.
|
|
@@ -10,16 +10,18 @@ toast notifications, which are TUI-visible but model-invisible.
|
|
|
10
10
|
The nudge pipeline is thatch's core innovation. It is not a passive
|
|
11
11
|
store-and-retrieve system. It is a nudge layer that changes agent behavior by
|
|
12
12
|
injecting context at the right moment. See [extraction.md](extraction.md) for
|
|
13
|
-
the extraction
|
|
13
|
+
the extraction pipeline (plugin-driven on opencode; nudged on MCP hosts only),
|
|
14
|
+
[prediction-engine.md](prediction-engine.md) for the
|
|
14
15
|
prediction auto-fire, and [behavior-engine.md](behavior-engine.md) for the
|
|
15
16
|
behavior auto-fire.
|
|
16
17
|
|
|
17
18
|
## What it does
|
|
18
19
|
|
|
19
|
-
On every user message, thatch runs through
|
|
20
|
+
On every user message, thatch runs through the priority tiers:
|
|
20
21
|
|
|
21
|
-
1. **Extraction nudge
|
|
22
|
-
|
|
22
|
+
1. **Extraction nudge** (MCP hosts only)---if tool interactions are queued,
|
|
23
|
+
tell the agent to dispatch an extractor. There is deliberately no
|
|
24
|
+
opencode extraction nudge: extraction is plugin-driven at session idle.
|
|
23
25
|
2. **Recall nudge**---semantically search stored memories for context
|
|
24
26
|
relevant to the user's prompt.
|
|
25
27
|
3. **Prediction nudge**---surface user decision-model predictions relevant to
|
|
@@ -28,14 +30,16 @@ On every user message, thatch runs through four priority tiers:
|
|
|
28
30
|
current situation.
|
|
29
31
|
|
|
30
32
|
Tiers 2--4 fire independently and share one embedding computation. Tier 1
|
|
31
|
-
|
|
33
|
+
(extraction) exists only on the MCP path.
|
|
32
34
|
|
|
33
35
|
## How it works
|
|
34
36
|
|
|
35
|
-
Two host paths deliver the
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
Two host paths deliver the tiers through different mechanisms. The opencode
|
|
38
|
+
path runs in-process with direct event hooks and has THREE tiers (recall,
|
|
39
|
+
prediction, behavior - extraction is plugin-driven at session idle, not
|
|
40
|
+
nudged). The MCP path runs via external CLI hook processes that communicate
|
|
41
|
+
with the long-lived MCP server through the sideband socket and keeps all
|
|
42
|
+
four.
|
|
39
43
|
|
|
40
44
|
### opencode path (`src/runtime.ts`, `onChatMessage` - v1 `chat.message` hook, v2 `session.hook("prompt")`)
|
|
41
45
|
|
|
@@ -53,28 +57,25 @@ See [compaction-recovery.md](compaction-recovery.md).
|
|
|
53
57
|
|
|
54
58
|
If the session is a child in `childToParent` but not in `extractionChildren`
|
|
55
59
|
(a task-dispatched sub-agent), all nudge tiers are skipped and the hook
|
|
56
|
-
returns
|
|
60
|
+
returns early. Dispatched sub-agents may have restricted tool lists
|
|
57
61
|
that exclude the thatch tools, and even where the tools exist the child is
|
|
58
62
|
driven by its dispatch prompt, so a nudge only produces "No tool named"
|
|
59
|
-
error rounds or noise.
|
|
60
|
-
the guard - it is session-independent maintenance. See
|
|
63
|
+
error rounds or noise. See
|
|
61
64
|
[session-lifecycle.md](session-lifecycle.md) for the `extractionChildren`
|
|
62
65
|
distinction.
|
|
63
66
|
|
|
64
|
-
#### Tier 1---Extraction nudge
|
|
67
|
+
#### Tier 1---Extraction nudge: REMOVED from opencode
|
|
65
68
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
-
|
|
73
|
-
|
|
74
|
-
-
|
|
75
|
-
|
|
76
|
-
See [extraction.md](extraction.md) for the full extraction pipeline, including
|
|
77
|
-
the accept/complete/requeue lifecycle and the `extracting` set.
|
|
69
|
+
There is deliberately no extraction nudge on the opencode path. The old
|
|
70
|
+
model-driven handshake (nudge → model dispatches a sub-agent → parent acks
|
|
71
|
+
with `thatch_extraction_done` → child fetches by a model-copied session ID)
|
|
72
|
+
raced its own state machine: the parent's ack accepted the buffer before
|
|
73
|
+
the child fetched, a no-claim completion wiped the accepted set, and
|
|
74
|
+
mis-targeted IDs made every transition a silent no-op - so the same queued
|
|
75
|
+
count re-fired forever while buffered interactions were silently lost (the
|
|
76
|
+
September 2026 dispatch-loop report, both defects). Extraction on opencode
|
|
77
|
+
is now plugin-driven end-to-end (`triggerExtraction` at session idle); see
|
|
78
|
+
[extraction.md](extraction.md). The MCP path keeps its nudge - see below.
|
|
78
79
|
|
|
79
80
|
#### Tier 2---Recall nudge
|
|
80
81
|
|
|
@@ -127,8 +128,7 @@ and the self-discipline rule data model.
|
|
|
127
128
|
#### Tier independence
|
|
128
129
|
|
|
129
130
|
Tiers 2, 3, and 4 all fire independently. Any subset may inject. All share
|
|
130
|
-
one embedding computation
|
|
131
|
-
extraction tier returns early, and is reused across three cosine scans against
|
|
131
|
+
one embedding computation, reused across three cosine scans against
|
|
132
132
|
different tables.
|
|
133
133
|
|
|
134
134
|
### MCP path (`bin/thatch`, `flush-tools` subcommand)
|
|
@@ -156,10 +156,14 @@ Three priority tiers, first match wins:
|
|
|
156
156
|
#### Tier 1---Extraction nudge
|
|
157
157
|
|
|
158
158
|
- `peekQueue(sessionID)` peeks the file-backed JSONL queue without draining
|
|
159
|
-
it. The queue persists until the
|
|
160
|
-
|
|
159
|
+
it. The queue persists until the extractor completes
|
|
160
|
+
(`extraction_done` with the parent's session id) or the agent calls
|
|
161
|
+
`memory_remember`, which triggers `consumeQueue`. The parent's
|
|
162
|
+
dispatch-time ack resets the escalation counter but does NOT drain - a
|
|
163
|
+
drain at ack time deleted the queue before the sub-agent fetched (the
|
|
164
|
+
accept-before-fetch loss).
|
|
161
165
|
- If interactions are pending: increments the missed count, prints
|
|
162
|
-
`extractionNudge(count, missed,
|
|
166
|
+
`extractionNudge(count, missed, sessionID)`.
|
|
163
167
|
- Breaks. Tiers 2--3 are skipped.
|
|
164
168
|
|
|
165
169
|
See [extraction.md](extraction.md) for the file-backed queue and the
|
|
@@ -253,9 +257,10 @@ TUI display. The opencode framework's history serializer filters on
|
|
|
253
257
|
|
|
254
258
|
## Interactions with other features
|
|
255
259
|
|
|
256
|
-
- **Extraction pipeline** ([extraction.md](extraction.md)): tier 1
|
|
257
|
-
extraction nudge.
|
|
258
|
-
extraction is
|
|
260
|
+
- **Extraction pipeline** ([extraction.md](extraction.md)): the MCP tier 1
|
|
261
|
+
is the extraction nudge. On opencode there is no extraction
|
|
262
|
+
nudge - extraction is plugin-driven at session idle, so `chat.message`
|
|
263
|
+
runs the recall/prediction/behavior tiers only.
|
|
259
264
|
- **Memory store** ([memory-store.md](memory-store.md)): tier 2 searches
|
|
260
265
|
stored memories via `db.search()`. This path records no telemetry---the
|
|
261
266
|
agent has not read the memories yet.
|
|
@@ -272,21 +277,21 @@ TUI display. The opencode framework's history serializer filters on
|
|
|
272
277
|
the nudge pipeline is suppressed during compaction. The compaction guard
|
|
273
278
|
clears stale flags when compaction fails.
|
|
274
279
|
- **Session lifecycle** ([session-lifecycle.md](session-lifecycle.md)):
|
|
275
|
-
direct extraction (`triggerExtraction`)
|
|
276
|
-
|
|
280
|
+
direct extraction (`triggerExtraction`) is triggered by session idle;
|
|
281
|
+
there is no extraction tier to suppress on opencode.
|
|
277
282
|
|
|
278
283
|
## Source files
|
|
279
284
|
|
|
280
285
|
| File | Role |
|
|
281
286
|
|------|------|
|
|
282
|
-
| `src/runtime.ts` | opencode: per-message nudges (
|
|
283
|
-
| `bin/thatch` | MCP: `flush-tools` and `flush-predictions` subcommands |
|
|
287
|
+
| `src/runtime.ts` | opencode: per-message nudges (recall/prediction/behavior tiers; no extraction nudge), compaction guard |
|
|
288
|
+
| `bin/thatch` | MCP: `flush-tools` and `flush-predictions` subcommands (all 4 tiers) |
|
|
284
289
|
| `src/sideband.ts` | MCP: sideband client helpers (`sidebandMatch`, `sidebandPredictions`, `sidebandBehaviors`) |
|
|
285
290
|
| `src/prompts.ts` | All nudge formatting functions (`recallNudge`, `claudeRecallNudge`, `predictionNudge`, `behaviorNudge`, `extractionNudge`, `claudeWriteNudge`) |
|
|
286
291
|
|
|
287
292
|
## Key invariants
|
|
288
293
|
|
|
289
|
-
- Tier 1 (extraction) returns early. Tiers 2-4 are skipped when tier 1 fires.
|
|
294
|
+
- Tier 1 (extraction, MCP only) returns early. Tiers 2-4 are skipped when tier 1 fires.
|
|
290
295
|
- Tiers 2-4 share one embedding computation. One embed, three cosine scans against different tables.
|
|
291
296
|
- Each tier is wrapped in its own try/catch. A failure in one tier does not block the others.
|
|
292
297
|
- The recall nudge uses `db.search()`, not `db.recall()`, to avoid inflating telemetry. The agent has not read the memories yet.
|
|
@@ -27,7 +27,10 @@ Subscribes to all session bus events. Dispatches based on `event.type`.
|
|
|
27
27
|
2. Snapshot the parent's current pending buffer: `parentSnapshots.set(childID, [...extraction.peek(parentID)])`
|
|
28
28
|
3. Return early -- child sessions don't get the session-start reminder
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
The snapshot is journal-recovery data only (restored child bookkeeping
|
|
31
|
+
after a reload); live delivery is claim-based: the child's payload fetch
|
|
32
|
+
records exactly which entries it received, and its completion signal
|
|
33
|
+
consumes only those.
|
|
31
34
|
|
|
32
35
|
### session.created without parentID (top-level session)
|
|
33
36
|
|
|
@@ -39,20 +42,22 @@ This snapshot is used by `consumeSnapshot` for snapshot-aware drain: when the ch
|
|
|
39
42
|
|
|
40
43
|
When a child created by `triggerExtraction` goes idle:
|
|
41
44
|
|
|
42
|
-
1.
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
45
|
+
1. `completeClaimed(childID)` -- consume only the entries this child
|
|
46
|
+
claimed via its payload fetch. A no-claim idle is a no-op: a child that
|
|
47
|
+
never fetched processed nothing, and its idle signal must not drop
|
|
48
|
+
entries another extractor holds. Held entries with no completer are
|
|
49
|
+
bounded by the 15-minute stale reaper
|
|
50
|
+
2. Fire a toast with extraction metrics (new/updated/deleted counts) -- only if memories were actually written
|
|
51
|
+
3. Clean up all maps: `extracting`, `childToParent`, `parentSnapshots`, `childMetrics`, `extractionChildren`
|
|
52
|
+
4. `consume(childID)` -- drain child's own buffer
|
|
53
|
+
5. Delete the child session via `client.session.delete`
|
|
49
54
|
|
|
50
55
|
### session.status idle -- task-dispatched sub-agent
|
|
51
56
|
|
|
52
57
|
When a task-dispatched sub-agent (not created by `triggerExtraction`) goes idle:
|
|
53
58
|
|
|
54
|
-
- `
|
|
55
|
-
-
|
|
59
|
+
- `completeClaimed(childID)` -- complete only what THIS child claimed via
|
|
60
|
+
its payload fetch (a no-claim idle consumes nothing)
|
|
56
61
|
- Does NOT drain the buffer or delete the session -- the task tool that dispatched the sub-agent needs to read its output
|
|
57
62
|
|
|
58
63
|
### session.status idle -- parent session
|
|
@@ -60,8 +65,9 @@ When a task-dispatched sub-agent (not created by `triggerExtraction`) goes idle:
|
|
|
60
65
|
When a parent session (no parentID) goes idle:
|
|
61
66
|
|
|
62
67
|
- Wrap-up resolution first (below): a pending `/thatch/compact` or `/thatch/exit` resolves here, and a greenlit action returns early
|
|
68
|
+
- `requeueStaleAccepted()` first: accepted entries whose completion signal never came (15 min) return to pending
|
|
63
69
|
- If not compacting, not already extracting, and buffer has pending interactions: `triggerExtraction(sessionID)`
|
|
64
|
-
- On failure: log error, clear `extracting` flag (
|
|
70
|
+
- On failure: log error, clear `extracting` flag (the next idle retries -- there is no model-facing nudge fallback)
|
|
65
71
|
|
|
66
72
|
### Wrap-up commands (`/thatch/compact`, `/thatch/exit`)
|
|
67
73
|
|
|
@@ -84,12 +90,21 @@ first-ever install is invisible until the next server start.
|
|
|
84
90
|
|
|
85
91
|
### session.error (child session)
|
|
86
92
|
|
|
87
|
-
- `
|
|
93
|
+
- `requeueClaimed(childID)` if it fetched (the delivery was never
|
|
94
|
+
processed), else `requeueAccepted(parentID)` -- the error is a terminal
|
|
95
|
+
death signal, so a claim-less child falls back to whole-set requeue for
|
|
96
|
+
immediate recovery
|
|
97
|
+
- Clear `extracting` for the parent (the next idle re-triggers)
|
|
88
98
|
- Clean up all maps for the child
|
|
89
99
|
|
|
90
100
|
### session.deleted -- child
|
|
91
101
|
|
|
92
|
-
- `
|
|
102
|
+
- Extraction child: `requeueClaimed(childID)`, else
|
|
103
|
+
`requeueAccepted(parentID)` (never processed)
|
|
104
|
+
- Task-kind child: `requeueClaimed(childID)` only -- task children are
|
|
105
|
+
deleted ROUTINELY, so a claim-less task child's deletion must not requeue
|
|
106
|
+
the whole accepted set (it would yank an in-flight extractor's payload
|
|
107
|
+
back to pending for duplicate extraction)
|
|
93
108
|
- Clean up all maps for the child
|
|
94
109
|
|
|
95
110
|
### session.deleted -- parent
|
|
@@ -106,21 +121,20 @@ first-ever install is invisible until the next server start.
|
|
|
106
121
|
### Internal state maps
|
|
107
122
|
|
|
108
123
|
- **childToParent**: `Map<childID, parentID>` -- maps child sessions to their parents
|
|
109
|
-
- **parentSnapshots**: `Map<childID, Interaction[]>` -- snapshot of parent's buffer at child creation time
|
|
124
|
+
- **parentSnapshots**: `Map<childID, Interaction[]>` -- snapshot of parent's buffer at child creation time (journal recovery only; live delivery is claim-based)
|
|
110
125
|
- **childMetrics**: `Map<childID, {new, updated, deleted}>` -- extraction metrics per child
|
|
111
|
-
- **extracting**: `Set<parentID>` -- parent IDs with an active direct-extraction child (
|
|
126
|
+
- **extracting**: `Set<parentID>` -- parent IDs with an active direct-extraction child (gates re-triggering)
|
|
112
127
|
- **extractionChildren**: `Set<childID>` -- distinguishes extraction children from task-dispatched sub-agents
|
|
113
128
|
- **compacting**: `Set<sessionID>` -- sessions currently being compacted (suppresses nudges)
|
|
114
129
|
- **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
|
|
115
|
-
- **missedNudges**: `Map<sessionID, number>` -- extraction nudge escalation counter
|
|
116
130
|
|
|
117
131
|
## Interactions with other features
|
|
118
132
|
|
|
119
133
|
- Extraction pipeline ([extraction.md](extraction.md)): direct extraction is triggered by `session.status idle`; child lifecycle managed here
|
|
120
|
-
- Nudge pipeline ([nudge-pipeline.md](nudge-pipeline.md)): `
|
|
134
|
+
- Nudge pipeline ([nudge-pipeline.md](nudge-pipeline.md)): `compacting` set suppresses all nudges; task-dispatched sub-agents (in `childToParent`, not in `extractionChildren`) suppress all tiers
|
|
121
135
|
- Hygiene ([hygiene.md](hygiene.md)): hygiene report runs at `session.created` for top-level sessions
|
|
122
136
|
- Compaction recovery ([compaction-recovery.md](compaction-recovery.md)): `session.compacted` event clears the compacting flag
|
|
123
|
-
- Memory store ([memory-store.md](memory-store.md)): child sessions write memories via `memory_remember`, which
|
|
137
|
+
- Memory store ([memory-store.md](memory-store.md)): child sessions write memories via `memory_remember`, which completes their claimed delivery via `tool.execute.after`
|
|
124
138
|
|
|
125
139
|
## Source files
|
|
126
140
|
|
|
@@ -130,7 +144,7 @@ first-ever install is invisible until the next server start.
|
|
|
130
144
|
## Key invariants
|
|
131
145
|
|
|
132
146
|
- Child sessions don't get the session-start reminder (early return in `session.created` with parentID).
|
|
133
|
-
-
|
|
147
|
+
- Delivery is claim-based: the payload fetch records what the fetcher received; only the fetcher's completion consumes it. A no-claim completion is a no-op, never a whole-set drop.
|
|
134
148
|
- Extraction children are deleted after going idle; task-dispatched sub-agents are NOT deleted (task tool reads output).
|
|
135
|
-
- Child errors requeue the
|
|
149
|
+
- Child errors requeue what the child held (never processed). Child deletion also requeues.
|
|
136
150
|
- Parent deletion completes accepted entries (takes them with it).
|
|
@@ -73,6 +73,15 @@ gh setup fails registration with a real error instead of producing a
|
|
|
73
73
|
watcher that never fires. Watch creation is also gated on a cached
|
|
74
74
|
`gh --version` availability check.
|
|
75
75
|
|
|
76
|
+
CI events (`pr_ci`, `branch_ci`) fire only on a running -> completed
|
|
77
|
+
check-run transition, so a watcher registered after a fast pipeline has
|
|
78
|
+
already finished holds completed runs in its baseline and can never
|
|
79
|
+
notify for that head. When every check run on the head is already
|
|
80
|
+
completed at registration, the tool response says so, with the
|
|
81
|
+
conclusion breakdown (`describeBaselineCheckRuns`), so the caller reads
|
|
82
|
+
the result instead of waiting on a notification that cannot come. The
|
|
83
|
+
rule for catching a push's CI: register the watcher before pushing.
|
|
84
|
+
|
|
76
85
|
### Polling
|
|
77
86
|
|
|
78
87
|
A `setInterval` loop (unref'd - it never keeps the process alive)
|