@jeffober/thatch 0.1.13 → 0.1.14

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 CHANGED
@@ -32,6 +32,17 @@ No API keys, no cloud services; everything runs on your machine.
32
32
  On next start, OpenCode npm-installs thatch (putting the `thatch` CLI on your
33
33
  PATH as a side effect) and its tools are available immediately.
34
34
 
35
+ Set this environment variable so the extraction nudge can run the fact-extractor
36
+ as a **background** sub-agent instead of blocking your turn:
37
+
38
+ ```bash
39
+ export OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS=true
40
+ ```
41
+
42
+ Without it, opencode hides the task tool's `background` parameter and every
43
+ extraction nudge runs synchronously. Put it in your shell rc, `mise.toml`
44
+ `[env]`, or `.envrc`.
45
+
35
46
  **Self-hosting** (local development):
36
47
 
37
48
  ```jsonc
@@ -10,13 +10,15 @@ This entire review is a static analysis exercise. Do NOT run tests, linters, com
10
10
 
11
11
  ## Step 1: Resolve the review target
12
12
 
13
+ First, run `git fetch origin` (NOT `git pull` — fetch updates remote-tracking refs without touching the working tree or current branch). This refreshes `origin/main` and other remote-tracking refs to their true remote state. Local branches (e.g. `main`) only advance on checkout/pull and may be stale on a long-lived feature checkout, so resolve the base against the remote tracking ref, not the local branch.
14
+
13
15
  Identify what to review:
14
- - If a branch was specified, identify the base branch (usually main or master), compute the merge-base, and review the range merge-base..HEAD.
15
- - If a PR number was specified, use gh pr view to resolve the head and base, fetch both refs, and review merge-base..head.
16
+ - If a branch was specified, identify the base branch (usually main or master), compute the merge-base against the remote tracking ref (e.g. `git merge-base HEAD origin/main`), and review the range merge-base..HEAD.
17
+ - If a PR number was specified, use `gh pr view` to resolve the head and base, fetch both refs, and review merge-base..head.
16
18
  - If a git range was specified (A..B form), use it directly.
17
- - If no target was specified, review the current branch against its base.
19
+ - If no target was specified, review the current branch against its base, resolving the base from `origin/main` (or the appropriate remote tracking ref) after fetching.
18
20
 
19
- Fetch any refs not locally reachable so branches and PRs that were never checked out can be reviewed.
21
+ Also fetch any refs not locally reachable so branches and PRs that were never checked out can be reviewed (e.g. `git fetch origin <branch>` for a PR head).
20
22
 
21
23
  Run git diff --stat on the resolved range and git log --oneline to understand the change.
22
24
 
@@ -20,6 +20,17 @@ Publish to npm and add thatch to your opencode config:
20
20
 
21
21
  OpenCode installs the plugin and its dependencies automatically on next start.
22
22
 
23
+ Set this environment variable so the extraction nudge can dispatch the
24
+ fact-extractor as a **background** sub-agent instead of blocking your turn:
25
+
26
+ ```bash
27
+ export OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS=true
28
+ ```
29
+
30
+ Without it, opencode hides the task tool's `background` parameter and the
31
+ extraction nudge runs synchronously. Put it in your shell rc, `mise.toml`
32
+ `[env]`, or `.envrc`.
33
+
23
34
  For local development before publishing, use a file path:
24
35
 
25
36
  ```jsonc
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jeffober/thatch",
3
- "version": "0.1.13",
3
+ "version": "0.1.14",
4
4
  "description": "Persistent memory for AI coding agents — local embeddings, SQLite stores, zero config. Works with OpenCode (as a plugin), Claude Code (as an MCP server), and Cursor (as an MCP server).",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -56,7 +56,7 @@ export function appendBatch(sessionID: string, toolCalls: BatchToolCall[]): void
56
56
  const additions: ToolInteraction[] = [];
57
57
  for (const tc of toolCalls) {
58
58
  const lower = tc.tool_name.toLowerCase();
59
- if (lower === "mcp__thatch__memory_remember") {
59
+ if (lower === "mcp__thatch__memory_remember" || lower === "mcp__thatch__extraction_done") {
60
60
  resetMissedCount(sessionID);
61
61
  consumeQueue(sessionID);
62
62
  existing.length = 0;
package/src/index.ts CHANGED
@@ -56,6 +56,14 @@ export const server: Plugin = async ({ client, worktree }) => {
56
56
  // all-caps shouting. Reset to 0 whenever the agent writes a memory.
57
57
  const missedNudges = new Map<string, number>();
58
58
 
59
+ // Parent-child session mapping for cross-session buffer drain. opencode
60
+ // creates real child sessions with their own IDs for sub-agents; without
61
+ // this map, a memory_remember call in a child (e.g. a background
62
+ // fact-extractor task) drains the child's empty buffer but leaves the
63
+ // parent's buffer untouched, causing the nudge to replay indefinitely.
64
+ // Populated from session.created events that carry a parentID.
65
+ const childToParent = new Map<string, string>();
66
+
59
67
  // Skills always install to the global opencode config — installing into the
60
68
  // worktree would mutate the user's repo (untracked files in git status).
61
69
  // A failed install degrades the nudge workflow but must not kill the plugin.
@@ -103,8 +111,34 @@ export const server: Plugin = async ({ client, worktree }) => {
103
111
  // The buffer is NOT drained on nudge delivery — it persists until the
104
112
  // agent writes a memory, so ignored nudges accumulate and the payload
105
113
  // grows with each missed cycle.
114
+ //
115
+ // Fix A: a memory_remember call in a child session (sub-agent) also
116
+ // drains the parent's buffer. Without this, dispatching the
117
+ // fact-extractor as a background task writes memories in the child
118
+ // but never clears the parent's queue — the nudge replays every turn.
119
+ //
120
+ // Fix C: thatch_extraction_done is an explicit acknowledgment tool the
121
+ // model calls in the parent session after dispatching. Belt-and-
122
+ // suspenders for fix A: covers the case where the sub-agent errors out
123
+ // or the host doesn't expose parent-child session relationships.
106
124
  "tool.execute.after": async (input, output) => {
107
125
  if (input.tool === "thatch_memory_remember") {
126
+ extraction.consume(input.sessionID);
127
+ missedNudges.delete(input.sessionID);
128
+ // Fix A: if this is a child session, also drain the parent so the
129
+ // parent's extraction nudge stops replaying after a sub-agent writes
130
+ // memories on its behalf.
131
+ const parentID = childToParent.get(input.sessionID);
132
+ if (parentID) {
133
+ extraction.consume(parentID);
134
+ missedNudges.delete(parentID);
135
+ }
136
+ return;
137
+ }
138
+ // Fix C: explicit acknowledgment tool drains the buffer without
139
+ // requiring a memory write. The model calls this after dispatching
140
+ // the fact-extractor to a sub-agent.
141
+ if (input.tool === "thatch_extraction_done") {
108
142
  extraction.consume(input.sessionID);
109
143
  missedNudges.delete(input.sessionID);
110
144
  return;
@@ -183,7 +217,22 @@ export const server: Plugin = async ({ client, worktree }) => {
183
217
 
184
218
  // 5. Session-start reminder, carrying the hygiene heartbeat. Hygiene is
185
219
  // best-effort: a failure there must not cost the reminder itself.
220
+ //
221
+ // Also tracks parent-child session relationships for fix A (cross-session
222
+ // buffer drain). session.created with a parentID records the mapping;
223
+ // session.deleted cleans it up to avoid unbounded growth.
186
224
  event: async ({ event }) => {
225
+ if (event.type === "session.created") {
226
+ const info = event.properties.info;
227
+ if (info.parentID) {
228
+ childToParent.set(info.id, info.parentID);
229
+ }
230
+ }
231
+ if (event.type === "session.deleted") {
232
+ childToParent.delete(event.properties.info.id);
233
+ return;
234
+ }
235
+
187
236
  if (event.type !== "session.created") return;
188
237
  const id = event.properties.info.id;
189
238
 
package/src/prompts.ts CHANGED
@@ -136,7 +136,7 @@ Tools are prefixed in Claude Code: \`mcp__thatch__memory_remember\`,
136
136
  \`mcp__thatch__memory_recall\`, \`mcp__thatch__memory_list\`,
137
137
  \`mcp__thatch__memory_show\`, \`mcp__thatch__memory_forget\`,
138
138
  \`mcp__thatch__store_list\`, \`mcp__thatch__find_duplicates\`,
139
- \`mcp__thatch__dedup_mark_checked\`.
139
+ \`mcp__thatch__dedup_mark_checked\`, \`mcp__thatch__extraction_done\`.
140
140
  Bare names used below for readability.
141
141
 
142
142
  ## Stores
@@ -268,21 +268,38 @@ export function extractionNudge(
268
268
  payload: string,
269
269
  ): string {
270
270
  const plural = count === 1 ? "" : "s";
271
+ // opencode's task tool has a `background` parameter (requires
272
+ // OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS=true) that launches the sub-agent
273
+ // asynchronously — the call returns immediately and the session is notified
274
+ // on completion. Naming the parameter gives the model a direct hit on the tool
275
+ // schema it sees. MCP hosts (Claude Code, Cursor) keep generic sub-agent
276
+ // wording because their background mechanisms differ and are not exposed as a
277
+ // per-call parameter in the same way.
278
+ const isOpencode = toolName.startsWith("thatch_");
279
+ const dispatch = isOpencode
280
+ ? "Dispatch a task with background: true"
281
+ : "Spawn a background sub-agent";
282
+ const drainTool = isOpencode ? "thatch_extraction_done" : "mcp__thatch__extraction_done";
283
+ const acknowledge = `After dispatching, call ${drainTool} to acknowledge.`;
284
+
271
285
  if (missedCount >= 3) {
272
286
  return `[thatch] YOU ARE IGNORING EXTRACTION INSTRUCTIONS. ` +
273
287
  `${count} TOOL INTERACTION${plural.toUpperCase()} ARE QUEUED AND YOU HAVE NOT PROCESSED ANY OF THEM. ` +
274
288
  `THE USER INSTALLED THIS PLUGIN FOR A REASON. ` +
275
- `SPAWN A SUB-AGENT NOW TO RUN THE thatch-fact-extractor SKILL ` +
276
- `AND USE ${toolName.toUpperCase()} TO SAVE DURABLE FACTS FROM THIS PAYLOAD:\n${payload}`;
289
+ `${dispatch.toUpperCase()} NOW TO RUN THE thatch-fact-extractor SKILL. ` +
290
+ `${acknowledge.toUpperCase()} ` +
291
+ `USE ${toolName.toUpperCase()} TO SAVE DURABLE FACTS FROM THIS PAYLOAD:\n${payload}`;
277
292
  }
278
293
  if (missedCount >= 2) {
279
294
  return `[thatch] YOU HAVE NOT PROCESSED YOUR PREVIOUS EXTRACTION PAYLOAD${plural.toUpperCase()}. ` +
280
295
  `${count} more tool interaction${plural} are now queued. ` +
281
- `Spawn a background sub-agent to run the thatch-fact-extractor skill on this payload. ` +
296
+ `${dispatch} to run the thatch-fact-extractor skill on this payload. ` +
297
+ `${acknowledge} ` +
282
298
  `Use ${toolName} to save durable facts from this payload:\n${payload}`;
283
299
  }
284
- return `[thatch] Spawn a background sub-agent to run the thatch-fact-extractor skill ` +
300
+ return `[thatch] ${dispatch} to run the thatch-fact-extractor skill ` +
285
301
  `on ${count} queued tool interaction${plural}. ` +
302
+ `${acknowledge} ` +
286
303
  `Then answer the user. ` +
287
304
  `Use ${toolName} to save durable facts from this payload:\n${payload}`;
288
305
  }
@@ -312,7 +329,7 @@ Tools are prefixed in Cursor: \`mcp__thatch__memory_remember\`,
312
329
  \`mcp__thatch__memory_recall\`, \`mcp__thatch__memory_list\`,
313
330
  \`mcp__thatch__memory_show\`, \`mcp__thatch__memory_forget\`,
314
331
  \`mcp__thatch__store_list\`, \`mcp__thatch__find_duplicates\`,
315
- \`mcp__thatch__dedup_mark_checked\`.
332
+ \`mcp__thatch__dedup_mark_checked\`, \`mcp__thatch__extraction_done\`.
316
333
  Bare names used below for readability.
317
334
 
318
335
  ## Stores
package/src/tool-defs.ts CHANGED
@@ -340,6 +340,33 @@ const markCheckedDef: ToolDef = {
340
340
  },
341
341
  };
342
342
 
343
+ /**
344
+ * Acknowledge that the extraction nudge has been handled. Drains the
345
+ * extraction buffer so the nudge does not replay on the next turn.
346
+ *
347
+ * The actual draining happens in the host's post-tool hook
348
+ * (tool.execute.after for opencode, PostToolBatch/appendBatch for MCP) —
349
+ * this tool's execute function is a no-op confirmation. The tool exists so
350
+ * the model can call it in the parent session after dispatching the
351
+ * fact-extractor to a sub-agent, giving the hook a recognizable tool name
352
+ * to key on. Without it, the buffer only drains when a memory_remember
353
+ * call lands in the same session (or a tracked child session), which fails
354
+ * if the sub-agent errors out or the host doesn't expose parent-child
355
+ * session relationships.
356
+ */
357
+ const extractionDoneDef: ToolDef = {
358
+ name: "extraction_done",
359
+ description:
360
+ "Acknowledge that the extraction nudge has been dispatched to a sub-agent. " +
361
+ "Drains the extraction buffer so the nudge does not replay. " +
362
+ "Call this in the parent session after dispatching a background task " +
363
+ "to run the thatch-fact-extractor skill.",
364
+ args: {},
365
+ async execute() {
366
+ return "[acknowledged] extraction buffer drained";
367
+ },
368
+ };
369
+
343
370
  /**
344
371
  * All tool definitions, in the order they should be presented to the agent.
345
372
  * The opencode plugin wraps each in `tool()`; the MCP server exposes them
@@ -354,4 +381,5 @@ export const TOOL_DEFS: ToolDef[] = [
354
381
  listStoresDef,
355
382
  findDuplicatesDef,
356
383
  markCheckedDef,
384
+ extractionDoneDef,
357
385
  ];