@jeffober/thatch 0.1.17 → 0.1.18
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 +2 -2
- package/package.json +1 -1
- package/src/extract-queue.ts +0 -7
- package/src/extraction.ts +18 -5
- package/src/index.ts +33 -11
- package/src/mcp.ts +2 -1
package/README.md
CHANGED
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
Persistent memory and useful dev skills for AI coding agents that requires zero
|
|
8
8
|
manual config.
|
|
9
9
|
|
|
10
|
-
Works with **OpenCode** (as a plugin), **Claude Code** (as
|
|
11
|
-
**Cursor** (as a local `stdio` MCP server).
|
|
10
|
+
Works with **OpenCode** (as a plugin), **Claude Code** (as a local `stdio` MCP
|
|
11
|
+
server), and **Cursor** (as a local `stdio` MCP server).
|
|
12
12
|
|
|
13
13
|
Each session inherits the accumulated knowledge of every session before it —
|
|
14
14
|
project architecture, conventions, gotchas, user preferences — so your agent
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jeffober/thatch",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.18",
|
|
4
4
|
"description": "Persistent memory for AI coding agents — local embeddings, SQLite stores, zero config. Works with OpenCode (as a plugin), Claude Code (as an MCP server), and Cursor (as an MCP server).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
package/src/extract-queue.ts
CHANGED
|
@@ -99,13 +99,6 @@ export function consumeQueue(sessionID: string): void {
|
|
|
99
99
|
try { unlinkSync(queuePath(sessionID)); } catch { /* not present is fine */ }
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
-
/** Read and delete a session's queue. Returns the buffered interactions in order. */
|
|
103
|
-
export function flushQueue(sessionID: string): ToolInteraction[] {
|
|
104
|
-
const interactions = peekQueue(sessionID);
|
|
105
|
-
consumeQueue(sessionID);
|
|
106
|
-
return interactions;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
102
|
function readQueueFile(path: string): ToolInteraction[] {
|
|
110
103
|
if (!existsSync(path)) return [];
|
|
111
104
|
const text = readFileSync(path, "utf8").trim();
|
package/src/extraction.ts
CHANGED
|
@@ -110,11 +110,24 @@ export class ExtractionPipeline {
|
|
|
110
110
|
this.#buffers.delete(sessionID);
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
-
/**
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
113
|
+
/**
|
|
114
|
+
* Remove entries that were in the snapshot (by reference identity), keeping
|
|
115
|
+
* any that arrived after the snapshot was taken. Used by the child-parent
|
|
116
|
+
* drain: when a sub-agent writes a memory, only the entries present at
|
|
117
|
+
* dispatch time should be drained — not the entire buffer, which may now
|
|
118
|
+
* contain interleaved-turn entries the parent accumulated while the
|
|
119
|
+
* sub-agent was running.
|
|
120
|
+
*/
|
|
121
|
+
consumeSnapshot(sessionID: string, snapshot: ToolInteraction[]): void {
|
|
122
|
+
const buf = this.#buffers.get(sessionID);
|
|
123
|
+
if (!buf) return;
|
|
124
|
+
const old = new Set(snapshot);
|
|
125
|
+
const remaining = buf.filter((ix) => !old.has(ix));
|
|
126
|
+
if (remaining.length === 0) {
|
|
127
|
+
this.#buffers.delete(sessionID);
|
|
128
|
+
} else {
|
|
129
|
+
this.#buffers.set(sessionID, remaining);
|
|
130
|
+
}
|
|
118
131
|
}
|
|
119
132
|
|
|
120
133
|
pending(sessionID: string): boolean {
|
package/src/index.ts
CHANGED
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
extractionNudge,
|
|
13
13
|
type NudgeMatch,
|
|
14
14
|
} from "./prompts";
|
|
15
|
-
import { ExtractionPipeline } from "./extraction";
|
|
15
|
+
import { ExtractionPipeline, type ToolInteraction } from "./extraction";
|
|
16
16
|
import { installSkills, SHARED_SKILLS, OPENCODE_ONLY_SKILLS } from "./skills";
|
|
17
17
|
import { hygieneReport } from "./hygiene";
|
|
18
18
|
|
|
@@ -64,6 +64,14 @@ export const server: Plugin = async ({ client, worktree }) => {
|
|
|
64
64
|
// Populated from session.created events that carry a parentID.
|
|
65
65
|
const childToParent = new Map<string, string>();
|
|
66
66
|
|
|
67
|
+
// Snapshot of the parent's buffer at the moment each child was dispatched.
|
|
68
|
+
// When the child writes a memory, consumeSnapshot drains only these entries
|
|
69
|
+
// (by reference identity), preserving interleaved-turn entries that arrived
|
|
70
|
+
// while the sub-agent was running. Without this, the child's memory_remember
|
|
71
|
+
// would drain the parent's ENTIRE buffer — silently dropping facts from
|
|
72
|
+
// any tool calls the parent made concurrenly with the sub-agent.
|
|
73
|
+
const parentSnapshots = new Map<string, ToolInteraction[]>();
|
|
74
|
+
|
|
67
75
|
// Skills always install to the global opencode config — installing into the
|
|
68
76
|
// worktree would mutate the user's repo (untracked files in git status).
|
|
69
77
|
// A failed install degrades the nudge workflow but must not kill the plugin.
|
|
@@ -113,9 +121,11 @@ export const server: Plugin = async ({ client, worktree }) => {
|
|
|
113
121
|
// grows with each missed cycle.
|
|
114
122
|
//
|
|
115
123
|
// A memory_remember call in a child session (sub-agent) also drains
|
|
116
|
-
// the parent's buffer
|
|
117
|
-
//
|
|
118
|
-
//
|
|
124
|
+
// the parent's buffer — but only the entries that existed at dispatch
|
|
125
|
+
// time (the snapshot). Entries from interleaved turns survive so their
|
|
126
|
+
// facts aren't silently dropped. Without this, dispatching the
|
|
127
|
+
// fact-extractor as a background task writes memories in the child but
|
|
128
|
+
// never clears the parent's queue — the nudge replays every turn.
|
|
119
129
|
//
|
|
120
130
|
// thatch_extraction_done is an explicit acknowledgment tool the model
|
|
121
131
|
// calls in the parent session after dispatching. Covers the case where
|
|
@@ -125,12 +135,19 @@ export const server: Plugin = async ({ client, worktree }) => {
|
|
|
125
135
|
if (input.tool === "thatch_memory_remember") {
|
|
126
136
|
extraction.consume(input.sessionID);
|
|
127
137
|
missedNudges.delete(input.sessionID);
|
|
128
|
-
// If this is a child session,
|
|
129
|
-
//
|
|
130
|
-
//
|
|
138
|
+
// If this is a child session, drain only the parent's snapshot entries
|
|
139
|
+
// (those present at dispatch time). Interleaved-turn entries survive.
|
|
140
|
+
// If no snapshot was recorded (e.g. session.created event never
|
|
141
|
+
// arrived), fall back to draining the entire parent buffer.
|
|
131
142
|
const parentID = childToParent.get(input.sessionID);
|
|
132
143
|
if (parentID) {
|
|
133
|
-
|
|
144
|
+
const snapshot = parentSnapshots.get(input.sessionID);
|
|
145
|
+
if (snapshot) {
|
|
146
|
+
extraction.consumeSnapshot(parentID, snapshot);
|
|
147
|
+
parentSnapshots.delete(input.sessionID);
|
|
148
|
+
} else {
|
|
149
|
+
extraction.consume(parentID);
|
|
150
|
+
}
|
|
134
151
|
missedNudges.delete(parentID);
|
|
135
152
|
}
|
|
136
153
|
return;
|
|
@@ -218,18 +235,23 @@ export const server: Plugin = async ({ client, worktree }) => {
|
|
|
218
235
|
// 5. Session-start reminder, carrying the hygiene heartbeat. Hygiene is
|
|
219
236
|
// best-effort: a failure there must not cost the reminder itself.
|
|
220
237
|
//
|
|
221
|
-
// Also tracks parent-child session relationships for
|
|
222
|
-
//
|
|
223
|
-
//
|
|
238
|
+
// Also tracks parent-child session relationships for cross-session buffer
|
|
239
|
+
// drain. session.created with a parentID records the mapping AND
|
|
240
|
+
// snapshots the parent's current buffer — so the child's later
|
|
241
|
+
// memory_remember drains only those snapshot entries, not the parent's
|
|
242
|
+
// entire buffer (which may have grown from interleaved turns).
|
|
243
|
+
// session.deleted cleans both maps to avoid unbounded growth.
|
|
224
244
|
event: async ({ event }) => {
|
|
225
245
|
if (event.type === "session.created") {
|
|
226
246
|
const info = event.properties.info;
|
|
227
247
|
if (info.parentID) {
|
|
228
248
|
childToParent.set(info.id, info.parentID);
|
|
249
|
+
parentSnapshots.set(info.id, [...extraction.peek(info.parentID)]);
|
|
229
250
|
}
|
|
230
251
|
}
|
|
231
252
|
if (event.type === "session.deleted") {
|
|
232
253
|
childToParent.delete(event.properties.info.id);
|
|
254
|
+
parentSnapshots.delete(event.properties.info.id);
|
|
233
255
|
return;
|
|
234
256
|
}
|
|
235
257
|
|
package/src/mcp.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { detectRepo } from "./git";
|
|
|
5
5
|
import { checkSetup } from "./setup";
|
|
6
6
|
import { TOOL_DEFS, type CoreContext, type ToolDef } from "./tool-defs";
|
|
7
7
|
import { SidebandServer, sidebandSocketPath } from "./sideband";
|
|
8
|
+
import pkg from "../package.json";
|
|
8
9
|
|
|
9
10
|
// ---------------------------------------------------------------------------
|
|
10
11
|
// JSON-RPC 2.0 types
|
|
@@ -34,7 +35,7 @@ const INTERNAL_ERROR = -32603;
|
|
|
34
35
|
// ---------------------------------------------------------------------------
|
|
35
36
|
|
|
36
37
|
const SERVER_NAME = "thatch";
|
|
37
|
-
const SERVER_VERSION =
|
|
38
|
+
const SERVER_VERSION = pkg.version;
|
|
38
39
|
|
|
39
40
|
/**
|
|
40
41
|
* MCP capabilities declared in the initialize response. Thatch is a tools-only
|