@jeffober/thatch 0.1.17 → 0.1.19
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 +56 -2
- package/bin/thatch +73 -7
- package/docs/in-progress/prediction-engine.md +404 -0
- package/docs/qa/README.md +33 -1
- package/docs/qa/use-cases/README.md +4 -0
- package/docs/qa/use-cases/UC-021-prediction-autofire.md +21 -0
- package/docs/qa/use-cases/UC-022-prediction-dedup.md +19 -0
- package/docs/qa/use-cases/UC-023-prediction-confidence-model.md +23 -0
- package/docs/qa/use-cases/UC-024-prediction-delete.md +20 -0
- package/package.json +1 -1
- package/src/db.ts +480 -0
- package/src/extract-queue.ts +0 -7
- package/src/extraction.ts +18 -5
- package/src/index.ts +86 -28
- package/src/mcp.ts +2 -1
- package/src/prompts.ts +89 -5
- package/src/sideband.ts +51 -13
- package/src/tool-defs.ts +203 -0
package/src/index.ts
CHANGED
|
@@ -10,9 +10,10 @@ import {
|
|
|
10
10
|
sessionStartReminder,
|
|
11
11
|
recallNudge,
|
|
12
12
|
extractionNudge,
|
|
13
|
+
predictionNudge,
|
|
13
14
|
type NudgeMatch,
|
|
14
15
|
} from "./prompts";
|
|
15
|
-
import { ExtractionPipeline } from "./extraction";
|
|
16
|
+
import { ExtractionPipeline, type ToolInteraction } from "./extraction";
|
|
16
17
|
import { installSkills, SHARED_SKILLS, OPENCODE_ONLY_SKILLS } from "./skills";
|
|
17
18
|
import { hygieneReport } from "./hygiene";
|
|
18
19
|
|
|
@@ -29,6 +30,11 @@ const RECALL_THRESHOLD = parseFloat(process.env.THATCH_RECALL_THRESHOLD ?? "0.55
|
|
|
29
30
|
// like "yes" or "ok" match too broadly to be useful.
|
|
30
31
|
const MIN_PROMPT_LEN = 10;
|
|
31
32
|
|
|
33
|
+
// Minimum cosine score for the prediction auto-fire. Lower than
|
|
34
|
+
// RECALL_THRESHOLD because "this situation matches a known pattern" is
|
|
35
|
+
// a weaker signal than "this prompt relates to a stored memory."
|
|
36
|
+
const PREDICTION_THRESHOLD = parseFloat(process.env.THATCH_PREDICTION_THRESHOLD ?? "0.45");
|
|
37
|
+
|
|
32
38
|
export const server: Plugin = async ({ client, worktree }) => {
|
|
33
39
|
// The opencode server's cwd is wherever the server happened to start;
|
|
34
40
|
// `worktree` is the project this plugin instance actually serves.
|
|
@@ -64,6 +70,14 @@ export const server: Plugin = async ({ client, worktree }) => {
|
|
|
64
70
|
// Populated from session.created events that carry a parentID.
|
|
65
71
|
const childToParent = new Map<string, string>();
|
|
66
72
|
|
|
73
|
+
// Snapshot of the parent's buffer at the moment each child was dispatched.
|
|
74
|
+
// When the child writes a memory, consumeSnapshot drains only these entries
|
|
75
|
+
// (by reference identity), preserving interleaved-turn entries that arrived
|
|
76
|
+
// while the sub-agent was running. Without this, the child's memory_remember
|
|
77
|
+
// would drain the parent's ENTIRE buffer — silently dropping facts from
|
|
78
|
+
// any tool calls the parent made concurrently with the sub-agent.
|
|
79
|
+
const parentSnapshots = new Map<string, ToolInteraction[]>();
|
|
80
|
+
|
|
67
81
|
// Skills always install to the global opencode config — installing into the
|
|
68
82
|
// worktree would mutate the user's repo (untracked files in git status).
|
|
69
83
|
// A failed install degrades the nudge workflow but must not kill the plugin.
|
|
@@ -113,9 +127,11 @@ export const server: Plugin = async ({ client, worktree }) => {
|
|
|
113
127
|
// grows with each missed cycle.
|
|
114
128
|
//
|
|
115
129
|
// A memory_remember call in a child session (sub-agent) also drains
|
|
116
|
-
// the parent's buffer
|
|
117
|
-
//
|
|
118
|
-
//
|
|
130
|
+
// the parent's buffer — but only the entries that existed at dispatch
|
|
131
|
+
// time (the snapshot). Entries from interleaved turns survive so their
|
|
132
|
+
// facts aren't silently dropped. Without this, dispatching the
|
|
133
|
+
// fact-extractor as a background task writes memories in the child but
|
|
134
|
+
// never clears the parent's queue — the nudge replays every turn.
|
|
119
135
|
//
|
|
120
136
|
// thatch_extraction_done is an explicit acknowledgment tool the model
|
|
121
137
|
// calls in the parent session after dispatching. Covers the case where
|
|
@@ -125,12 +141,19 @@ export const server: Plugin = async ({ client, worktree }) => {
|
|
|
125
141
|
if (input.tool === "thatch_memory_remember") {
|
|
126
142
|
extraction.consume(input.sessionID);
|
|
127
143
|
missedNudges.delete(input.sessionID);
|
|
128
|
-
// If this is a child session,
|
|
129
|
-
//
|
|
130
|
-
//
|
|
144
|
+
// If this is a child session, drain only the parent's snapshot entries
|
|
145
|
+
// (those present at dispatch time). Interleaved-turn entries survive.
|
|
146
|
+
// If no snapshot was recorded (e.g. session.created event never
|
|
147
|
+
// arrived), fall back to draining the entire parent buffer.
|
|
131
148
|
const parentID = childToParent.get(input.sessionID);
|
|
132
149
|
if (parentID) {
|
|
133
|
-
|
|
150
|
+
const snapshot = parentSnapshots.get(input.sessionID);
|
|
151
|
+
if (snapshot) {
|
|
152
|
+
extraction.consumeSnapshot(parentID, snapshot);
|
|
153
|
+
parentSnapshots.delete(input.sessionID);
|
|
154
|
+
} else {
|
|
155
|
+
extraction.consume(parentID);
|
|
156
|
+
}
|
|
134
157
|
missedNudges.delete(parentID);
|
|
135
158
|
}
|
|
136
159
|
return;
|
|
@@ -156,9 +179,14 @@ export const server: Plugin = async ({ client, worktree }) => {
|
|
|
156
179
|
// 4. Per-message nudge — two priority tiers:
|
|
157
180
|
// a. Extraction nudge: prior tool interactions are queued for fact
|
|
158
181
|
// extraction (carries the JSON payload for thatch-fact-extractor).
|
|
159
|
-
// b. Recall nudge:
|
|
160
|
-
//
|
|
161
|
-
//
|
|
182
|
+
// b. Recall + prediction nudge: when no extraction is pending, embed
|
|
183
|
+
// the user's prompt (shared embedding for both) and:
|
|
184
|
+
// - search memories by cosine (recall nudge)
|
|
185
|
+
// - search matchers by cosine, score predictions (prediction fire)
|
|
186
|
+
// Both fire independently; either, both, or neither may inject.
|
|
187
|
+
// The auto-fire reuses the embedding already computed for recall,
|
|
188
|
+
// adding only one more cosine scan against the matchers table.
|
|
189
|
+
// No extra model calls.
|
|
162
190
|
//
|
|
163
191
|
// Skipped during compaction: the agent can't call tools while generating
|
|
164
192
|
// a summary, so a nudge that says "use thatch_memory_recall" triggers a
|
|
@@ -195,41 +223,71 @@ export const server: Plugin = async ({ client, worktree }) => {
|
|
|
195
223
|
if (promptText.length < MIN_PROMPT_LEN) return;
|
|
196
224
|
|
|
197
225
|
const embedding = await model.queryEmbed(promptText);
|
|
198
|
-
const results = db.search([repo, "global"], embedding, { limit: 5 });
|
|
199
|
-
const matches: NudgeMatch[] = results
|
|
200
|
-
.filter((r) => r._score >= RECALL_THRESHOLD)
|
|
201
|
-
.map((r) => ({ label: r.label, score: Math.round(r._score * 1000) / 1000 }));
|
|
202
226
|
|
|
203
|
-
|
|
227
|
+
// Recall nudge: separate try/catch so a memory-search failure
|
|
228
|
+
// does not block the prediction fire (they share only the embedding).
|
|
229
|
+
try {
|
|
230
|
+
const results = db.search([repo, "global"], embedding, { limit: 5 });
|
|
231
|
+
const matches: NudgeMatch[] = results
|
|
232
|
+
.filter((r) => r._score >= RECALL_THRESHOLD)
|
|
233
|
+
.map((r) => ({ label: r.label, score: Math.round(r._score * 1000) / 1000 }));
|
|
204
234
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
235
|
+
if (matches.length > 0) {
|
|
236
|
+
output.parts.push({
|
|
237
|
+
id: `prt_thatch_${Math.random().toString(36).slice(2)}`,
|
|
238
|
+
sessionID: input.sessionID,
|
|
239
|
+
messageID: input.messageID ?? output.message.id,
|
|
240
|
+
type: "text",
|
|
241
|
+
text: recallNudge(matches),
|
|
242
|
+
synthetic: true,
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
} catch (err) {
|
|
246
|
+
console.error(`[thatch] recall nudge failed: ${err}`);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Prediction fire: independent of recall; a failure here does
|
|
250
|
+
// not affect the recall nudge that may have already been pushed.
|
|
251
|
+
try {
|
|
252
|
+
const predItems = db.scorePredictionNudge([repo, "global"], embedding, PREDICTION_THRESHOLD);
|
|
253
|
+
if (predItems.length > 0) {
|
|
254
|
+
output.parts.push({
|
|
255
|
+
id: `prt_thatch_${Math.random().toString(36).slice(2)}`,
|
|
256
|
+
sessionID: input.sessionID,
|
|
257
|
+
messageID: input.messageID ?? output.message.id,
|
|
258
|
+
type: "text",
|
|
259
|
+
text: predictionNudge(predItems),
|
|
260
|
+
synthetic: true,
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
} catch (err) {
|
|
264
|
+
console.error(`[thatch] prediction nudge failed: ${err}`);
|
|
265
|
+
}
|
|
213
266
|
} catch (err) {
|
|
214
|
-
console.error(`[thatch]
|
|
267
|
+
console.error(`[thatch] nudge hook failed: ${err}`);
|
|
215
268
|
}
|
|
216
269
|
},
|
|
217
270
|
|
|
218
271
|
// 5. Session-start reminder, carrying the hygiene heartbeat. Hygiene is
|
|
219
272
|
// best-effort: a failure there must not cost the reminder itself.
|
|
220
273
|
//
|
|
221
|
-
// Also tracks parent-child session relationships for
|
|
222
|
-
//
|
|
223
|
-
//
|
|
274
|
+
// Also tracks parent-child session relationships for cross-session buffer
|
|
275
|
+
// drain. session.created with a parentID records the mapping AND
|
|
276
|
+
// snapshots the parent's current buffer — so the child's later
|
|
277
|
+
// memory_remember drains only those snapshot entries, not the parent's
|
|
278
|
+
// entire buffer (which may have grown from interleaved turns).
|
|
279
|
+
// session.deleted cleans both maps to avoid unbounded growth.
|
|
224
280
|
event: async ({ event }) => {
|
|
225
281
|
if (event.type === "session.created") {
|
|
226
282
|
const info = event.properties.info;
|
|
227
283
|
if (info.parentID) {
|
|
228
284
|
childToParent.set(info.id, info.parentID);
|
|
285
|
+
parentSnapshots.set(info.id, [...extraction.peek(info.parentID)]);
|
|
229
286
|
}
|
|
230
287
|
}
|
|
231
288
|
if (event.type === "session.deleted") {
|
|
232
289
|
childToParent.delete(event.properties.info.id);
|
|
290
|
+
parentSnapshots.delete(event.properties.info.id);
|
|
233
291
|
return;
|
|
234
292
|
}
|
|
235
293
|
|
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
|
package/src/prompts.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { PredictionNudgeItem } from "./db";
|
|
2
|
+
|
|
1
3
|
export function systemPrompt(repo: string): string {
|
|
2
4
|
return `# Persistence
|
|
3
5
|
|
|
@@ -7,7 +9,9 @@ knowledge so future sessions can build on what you've already learned.
|
|
|
7
9
|
Tools: thatch_memory_remember, thatch_memory_recall, thatch_memory_list,
|
|
8
10
|
thatch_memory_show, thatch_memory_forget, thatch_store_list,
|
|
9
11
|
thatch_find_duplicates, thatch_dedup_mark_checked,
|
|
10
|
-
thatch_extraction_done
|
|
12
|
+
thatch_extraction_done,
|
|
13
|
+
thatch_prediction_query, thatch_prediction_update, thatch_prediction_list,
|
|
14
|
+
thatch_prediction_delete
|
|
11
15
|
|
|
12
16
|
## Stores
|
|
13
17
|
|
|
@@ -60,6 +64,27 @@ whether you've discovered knowledge worth persisting. Use thatch_memory_recall
|
|
|
60
64
|
to check for duplicates, then thatch_memory_remember for new findings. Then
|
|
61
65
|
deliver your response.
|
|
62
66
|
|
|
67
|
+
## User Decision Model
|
|
68
|
+
|
|
69
|
+
A statistical model of the user's decision-making preferences is maintained by
|
|
70
|
+
the prediction engine. Scored predictions may appear above as "User decision
|
|
71
|
+
model" context; these are learned patterns about what the user tends to
|
|
72
|
+
prefer in specific situations.
|
|
73
|
+
|
|
74
|
+
When facing a judgment call about scope, appropriateness, or methodology:
|
|
75
|
+
- If a prediction is strong (high confidence, sufficient evidence), follow it
|
|
76
|
+
- If predictions conflict or confidence is thin, surface the uncertainty
|
|
77
|
+
naturally: "I think you usually prefer X here, but I'm not sure; what do
|
|
78
|
+
you want?"
|
|
79
|
+
- When the user responds to a surfaced prediction, use thatch_prediction_update
|
|
80
|
+
to reinforce or weaken the model
|
|
81
|
+
|
|
82
|
+
When the user corrects you, answers your question, or provides a clear signal
|
|
83
|
+
about their preferences:
|
|
84
|
+
1. Use thatch_prediction_query to check for existing matchers and predictions
|
|
85
|
+
2. Use thatch_prediction_update to create, reinforce, or weaken a prediction
|
|
86
|
+
3. Use thatch_prediction_delete to remove a prediction created in error
|
|
87
|
+
|
|
63
88
|
## What to Store
|
|
64
89
|
|
|
65
90
|
- **Global store**: user observations, agent personality, system environment
|
|
@@ -150,8 +175,9 @@ Tools are prefixed in Claude Code: \`mcp__thatch__memory_remember\`,
|
|
|
150
175
|
\`mcp__thatch__memory_recall\`, \`mcp__thatch__memory_list\`,
|
|
151
176
|
\`mcp__thatch__memory_show\`, \`mcp__thatch__memory_forget\`,
|
|
152
177
|
\`mcp__thatch__store_list\`, \`mcp__thatch__find_duplicates\`,
|
|
153
|
-
\`mcp__thatch__dedup_mark_checked\`, \`mcp__thatch__extraction_done
|
|
154
|
-
|
|
178
|
+
\`mcp__thatch__dedup_mark_checked\`, \`mcp__thatch__extraction_done\`,
|
|
179
|
+
\`mcp__thatch__prediction_query\`, \`mcp__thatch__prediction_update\`,
|
|
180
|
+
\`mcp__thatch__prediction_list\`, \`mcp__thatch__prediction_delete\`. Bare names used below for readability.
|
|
155
181
|
|
|
156
182
|
## Stores
|
|
157
183
|
|
|
@@ -204,6 +230,27 @@ whether you've discovered knowledge worth persisting. Use memory_recall to
|
|
|
204
230
|
check for duplicates, then memory_remember for new findings. Then deliver
|
|
205
231
|
your response.
|
|
206
232
|
|
|
233
|
+
## User Decision Model
|
|
234
|
+
|
|
235
|
+
A statistical model of the user's decision-making preferences is maintained by
|
|
236
|
+
the prediction engine. Scored predictions may appear above as "User decision
|
|
237
|
+
model" context; these are learned patterns about what the user tends to
|
|
238
|
+
prefer in specific situations.
|
|
239
|
+
|
|
240
|
+
When facing a judgment call about scope, appropriateness, or methodology:
|
|
241
|
+
- If a prediction is strong (high confidence, sufficient evidence), follow it
|
|
242
|
+
- If predictions conflict or confidence is thin, surface the uncertainty
|
|
243
|
+
naturally: "I think you usually prefer X here, but I'm not sure; what do
|
|
244
|
+
you want?"
|
|
245
|
+
- When the user responds to a surfaced prediction, use prediction_update
|
|
246
|
+
to reinforce or weaken the model
|
|
247
|
+
|
|
248
|
+
When the user corrects you, answers your question, or provides a clear signal
|
|
249
|
+
about their preferences:
|
|
250
|
+
1. Use prediction_query to check for existing matchers and predictions
|
|
251
|
+
2. Use prediction_update to create, reinforce, or weaken a prediction
|
|
252
|
+
3. Use prediction_delete to remove a prediction created in error
|
|
253
|
+
|
|
207
254
|
## What to Store
|
|
208
255
|
|
|
209
256
|
- **Global store**: user observations, agent personality, system environment
|
|
@@ -356,8 +403,9 @@ Tools are prefixed in Cursor: \`mcp__thatch__memory_remember\`,
|
|
|
356
403
|
\`mcp__thatch__memory_recall\`, \`mcp__thatch__memory_list\`,
|
|
357
404
|
\`mcp__thatch__memory_show\`, \`mcp__thatch__memory_forget\`,
|
|
358
405
|
\`mcp__thatch__store_list\`, \`mcp__thatch__find_duplicates\`,
|
|
359
|
-
\`mcp__thatch__dedup_mark_checked\`, \`mcp__thatch__extraction_done
|
|
360
|
-
|
|
406
|
+
\`mcp__thatch__dedup_mark_checked\`, \`mcp__thatch__extraction_done\`,
|
|
407
|
+
\`mcp__thatch__prediction_query\`, \`mcp__thatch__prediction_update\`,
|
|
408
|
+
\`mcp__thatch__prediction_list\`, \`mcp__thatch__prediction_delete\`. Bare names used below for readability.
|
|
361
409
|
|
|
362
410
|
## Stores
|
|
363
411
|
|
|
@@ -410,6 +458,27 @@ whether you've discovered knowledge worth persisting. Use memory_recall to
|
|
|
410
458
|
check for duplicates, then memory_remember for new findings. Then deliver
|
|
411
459
|
your response.
|
|
412
460
|
|
|
461
|
+
## User Decision Model
|
|
462
|
+
|
|
463
|
+
A statistical model of the user's decision-making preferences is maintained by
|
|
464
|
+
the prediction engine. Scored predictions may appear above as "User decision
|
|
465
|
+
model" context; these are learned patterns about what the user tends to
|
|
466
|
+
prefer in specific situations.
|
|
467
|
+
|
|
468
|
+
When facing a judgment call about scope, appropriateness, or methodology:
|
|
469
|
+
- If a prediction is strong (high confidence, sufficient evidence), follow it
|
|
470
|
+
- If predictions conflict or confidence is thin, surface the uncertainty
|
|
471
|
+
naturally: "I think you usually prefer X here, but I'm not sure; what do
|
|
472
|
+
you want?"
|
|
473
|
+
- When the user responds to a surfaced prediction, use prediction_update
|
|
474
|
+
to reinforce or weaken the model
|
|
475
|
+
|
|
476
|
+
When the user corrects you, answers your question, or provides a clear signal
|
|
477
|
+
about their preferences:
|
|
478
|
+
1. Use prediction_query to check for existing matchers and predictions
|
|
479
|
+
2. Use prediction_update to create, reinforce, or weaken a prediction
|
|
480
|
+
3. Use prediction_delete to remove a prediction created in error
|
|
481
|
+
|
|
413
482
|
## What to Store
|
|
414
483
|
|
|
415
484
|
- **Global store**: user observations, agent personality, system environment
|
|
@@ -495,3 +564,18 @@ function formatRecallNudge(matches: NudgeMatch[], toolName: string): string {
|
|
|
495
564
|
const etc = n > 2 ? ", etc." : "";
|
|
496
565
|
return `[thatch] ${n} ${word} ${verb} to this prompt (${labels}${etc}). Use ${toolName} before responding.`;
|
|
497
566
|
}
|
|
567
|
+
|
|
568
|
+
// ---------------------------------------------------------------------------
|
|
569
|
+
// Prediction nudge: auto-injected when matchers fire for the user's prompt.
|
|
570
|
+
// Descriptive, not directive: the system prompt instructions govern how to
|
|
571
|
+
// act on the scored predictions (follow strong, surface weak/competing,
|
|
572
|
+
// update on user response).
|
|
573
|
+
// ---------------------------------------------------------------------------
|
|
574
|
+
|
|
575
|
+
export function predictionNudge(items: PredictionNudgeItem[]): string {
|
|
576
|
+
const lines = items.map((p) => {
|
|
577
|
+
const verb = p.evidence_count === 0 ? "you may prefer" : "you tend to";
|
|
578
|
+
return `- [${p.confidence.toFixed(2)} conf, ${p.evidence_count} tests] When ${p.matcher_description}: ${verb} ${p.statement}`;
|
|
579
|
+
});
|
|
580
|
+
return `[thatch] User decision model\n${lines.join("\n")}`;
|
|
581
|
+
}
|
package/src/sideband.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { unlinkSync } from "node:fs";
|
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { createServer, connect, type Server, type Socket } from "node:net";
|
|
6
|
-
import type { ThatchDB } from "./db";
|
|
6
|
+
import type { ThatchDB, PredictionNudgeItem } from "./db";
|
|
7
7
|
import type { EmbeddingModel } from "./embeddings";
|
|
8
8
|
|
|
9
9
|
/**
|
|
@@ -24,7 +24,7 @@ export interface SidebandMatch {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
interface MatchRequest {
|
|
27
|
-
method: "match";
|
|
27
|
+
method: "match" | "predictions";
|
|
28
28
|
text: string;
|
|
29
29
|
stores: string[];
|
|
30
30
|
threshold: number;
|
|
@@ -34,6 +34,7 @@ interface MatchRequest {
|
|
|
34
34
|
interface MatchResponse {
|
|
35
35
|
ok: boolean;
|
|
36
36
|
matches?: SidebandMatch[];
|
|
37
|
+
predictions?: PredictionNudgeItem[];
|
|
37
38
|
error?: string;
|
|
38
39
|
}
|
|
39
40
|
|
|
@@ -115,13 +116,22 @@ export class SidebandServer {
|
|
|
115
116
|
return;
|
|
116
117
|
}
|
|
117
118
|
|
|
118
|
-
if (req.method !== "match") {
|
|
119
|
+
if (req.method !== "match" && req.method !== "predictions") {
|
|
119
120
|
this.#respond(socket, { ok: false, error: `Unknown method: ${req.method}` });
|
|
120
121
|
return;
|
|
121
122
|
}
|
|
122
123
|
|
|
123
124
|
try {
|
|
124
125
|
const embedding = await this.#model.queryEmbed(req.text);
|
|
126
|
+
|
|
127
|
+
if (req.method === "predictions") {
|
|
128
|
+
const predictions = this.#db.scorePredictionNudge(
|
|
129
|
+
req.stores, embedding, req.threshold, 5,
|
|
130
|
+
);
|
|
131
|
+
this.#respond(socket, { ok: true, predictions });
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
125
135
|
const results = this.#db.search(req.stores, embedding, { limit: req.limit });
|
|
126
136
|
const matches: SidebandMatch[] = results
|
|
127
137
|
.filter((r) => r._score >= req.threshold)
|
|
@@ -156,16 +166,45 @@ export function sidebandMatch(
|
|
|
156
166
|
limit: number,
|
|
157
167
|
timeoutMs = 2000,
|
|
158
168
|
): Promise<SidebandMatch[] | null> {
|
|
169
|
+
return sidebandRequest("match", socketPath, text, stores, threshold, limit, timeoutMs)
|
|
170
|
+
.then((res) => res?.matches ?? null);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Connect to the sideband server and request scored predictions for a prompt.
|
|
175
|
+
* Returns null on any failure; callers treat null as "skip the prediction nudge."
|
|
176
|
+
*/
|
|
177
|
+
export function sidebandPredictions(
|
|
178
|
+
socketPath: string,
|
|
179
|
+
text: string,
|
|
180
|
+
stores: string[],
|
|
181
|
+
threshold: number,
|
|
182
|
+
limit: number,
|
|
183
|
+
timeoutMs = 2000,
|
|
184
|
+
): Promise<PredictionNudgeItem[] | null> {
|
|
185
|
+
return sidebandRequest("predictions", socketPath, text, stores, threshold, limit, timeoutMs)
|
|
186
|
+
.then((res) => res?.predictions ?? null);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function sidebandRequest(
|
|
190
|
+
method: "match" | "predictions",
|
|
191
|
+
socketPath: string,
|
|
192
|
+
text: string,
|
|
193
|
+
stores: string[],
|
|
194
|
+
threshold: number,
|
|
195
|
+
limit: number,
|
|
196
|
+
timeoutMs: number,
|
|
197
|
+
): Promise<MatchResponse | null> {
|
|
159
198
|
return new Promise((resolve) => {
|
|
160
199
|
let settled = false;
|
|
161
|
-
const done = (val:
|
|
200
|
+
const done = (val: MatchResponse | null) => {
|
|
162
201
|
if (settled) return;
|
|
163
202
|
settled = true;
|
|
164
203
|
resolve(val);
|
|
165
204
|
};
|
|
166
205
|
|
|
167
206
|
const socket = connect(socketPath);
|
|
168
|
-
const req = JSON.stringify({ method
|
|
207
|
+
const req = JSON.stringify({ method, text, stores, threshold, limit }) + "\n";
|
|
169
208
|
let buf = "";
|
|
170
209
|
|
|
171
210
|
const timer = setTimeout(() => {
|
|
@@ -173,14 +212,13 @@ export function sidebandMatch(
|
|
|
173
212
|
done(null);
|
|
174
213
|
}, timeoutMs);
|
|
175
214
|
|
|
176
|
-
socket.on("error", () => {
|
|
215
|
+
socket.on("error", (err: NodeJS.ErrnoException) => {
|
|
177
216
|
clearTimeout(timer);
|
|
178
|
-
//
|
|
179
|
-
//
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
// Already gone or never existed.
|
|
217
|
+
// Only remove the socket file if it's a stale-file situation
|
|
218
|
+
// (ECONNREFUSED). Other errors (timeout, server error) should
|
|
219
|
+
// not delete a live server's socket.
|
|
220
|
+
if (err.code === "ECONNREFUSED" || err.code === "ENOENT") {
|
|
221
|
+
try { unlinkSync(socketPath); } catch { /* already gone */ }
|
|
184
222
|
}
|
|
185
223
|
done(null);
|
|
186
224
|
});
|
|
@@ -197,7 +235,7 @@ export function sidebandMatch(
|
|
|
197
235
|
clearTimeout(timer);
|
|
198
236
|
try {
|
|
199
237
|
const res = JSON.parse(buf.slice(0, nl)) as MatchResponse;
|
|
200
|
-
done(res
|
|
238
|
+
done(res);
|
|
201
239
|
} catch {
|
|
202
240
|
done(null);
|
|
203
241
|
}
|