@edihasaj/recall 0.5.2
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/LICENSE +21 -0
- package/README.md +409 -0
- package/dist/chunk-4CV4JOE5.js +27 -0
- package/dist/chunk-4CV4JOE5.js.map +1 -0
- package/dist/chunk-A5UIRZU6.js +469 -0
- package/dist/chunk-A5UIRZU6.js.map +1 -0
- package/dist/chunk-AYHFPCGY.js +964 -0
- package/dist/chunk-AYHFPCGY.js.map +1 -0
- package/dist/chunk-DNFKAHS6.js +204 -0
- package/dist/chunk-DNFKAHS6.js.map +1 -0
- package/dist/chunk-GC5XMBG4.js +551 -0
- package/dist/chunk-GC5XMBG4.js.map +1 -0
- package/dist/chunk-IILLSHLM.js +3021 -0
- package/dist/chunk-IILLSHLM.js.map +1 -0
- package/dist/chunk-LVQW6WHK.js +146 -0
- package/dist/chunk-LVQW6WHK.js.map +1 -0
- package/dist/chunk-LZ6PMQRX.js +955 -0
- package/dist/chunk-LZ6PMQRX.js.map +1 -0
- package/dist/chunk-PC43MBX5.js +2960 -0
- package/dist/chunk-PC43MBX5.js.map +1 -0
- package/dist/chunk-VEPXEHRZ.js +1763 -0
- package/dist/chunk-VEPXEHRZ.js.map +1 -0
- package/dist/cleanup-TVOX2S2S.js +28 -0
- package/dist/cleanup-TVOX2S2S.js.map +1 -0
- package/dist/cli.js +3425 -0
- package/dist/cli.js.map +1 -0
- package/dist/daemon.js +1298 -0
- package/dist/daemon.js.map +1 -0
- package/dist/dispatcher-UGMU6THT.js +15 -0
- package/dist/dispatcher-UGMU6THT.js.map +1 -0
- package/dist/keychain-5QG52ANO.js +22 -0
- package/dist/keychain-5QG52ANO.js.map +1 -0
- package/dist/mcp.js +21 -0
- package/dist/mcp.js.map +1 -0
- package/dist/quality-Z7LPMMBC.js +17 -0
- package/dist/quality-Z7LPMMBC.js.map +1 -0
- package/dist/sync-server.js +225 -0
- package/dist/sync-server.js.map +1 -0
- package/dist/tasks-UOLSPXJQ.js +61 -0
- package/dist/tasks-UOLSPXJQ.js.map +1 -0
- package/dist/usage-CY3V72YN.js +101 -0
- package/dist/usage-CY3V72YN.js.map +1 -0
- package/drizzle/0000_initial_create.sql +240 -0
- package/drizzle/0001_rich_liz_osborn.sql +21 -0
- package/drizzle/0002_unknown_spot.sql +18 -0
- package/drizzle/0003_red_wendigo.sql +19 -0
- package/drizzle/0004_early_carlie_cooper.sql +1 -0
- package/drizzle/0005_simple_emma_frost.sql +96 -0
- package/drizzle/0006_keen_mongoose.sql +2 -0
- package/drizzle/0007_flawless_maximus.sql +15 -0
- package/drizzle/meta/0000_snapshot.json +1630 -0
- package/drizzle/meta/0001_snapshot.json +1773 -0
- package/drizzle/meta/0002_snapshot.json +1891 -0
- package/drizzle/meta/0003_snapshot.json +2014 -0
- package/drizzle/meta/0004_snapshot.json +2022 -0
- package/drizzle/meta/0005_snapshot.json +2064 -0
- package/drizzle/meta/0006_snapshot.json +2078 -0
- package/drizzle/meta/0007_snapshot.json +2183 -0
- package/drizzle/meta/_journal.json +62 -0
- package/package.json +64 -0
- package/scripts/recall-claude +7 -0
- package/scripts/recall-codex +7 -0
- package/scripts/recall-session +71 -0
|
@@ -0,0 +1,3021 @@
|
|
|
1
|
+
import {
|
|
2
|
+
activityEvents,
|
|
3
|
+
auditTrail,
|
|
4
|
+
feedbackEvents,
|
|
5
|
+
historySnippets,
|
|
6
|
+
memories,
|
|
7
|
+
memoryEmbeddings,
|
|
8
|
+
memoryMaintenanceTasks
|
|
9
|
+
} from "./chunk-A5UIRZU6.js";
|
|
10
|
+
|
|
11
|
+
// src/maintenance/tasks.ts
|
|
12
|
+
import { and as and2, desc as desc2, eq as eq7, gt, inArray as inArray2, lt, or, isNull, sql as sql2 } from "drizzle-orm";
|
|
13
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
14
|
+
import { z as z2 } from "zod";
|
|
15
|
+
|
|
16
|
+
// src/embeddings/embeddings.ts
|
|
17
|
+
import { createHash } from "crypto";
|
|
18
|
+
import { eq as eq3 } from "drizzle-orm";
|
|
19
|
+
|
|
20
|
+
// src/types.ts
|
|
21
|
+
import { z } from "zod";
|
|
22
|
+
var MemoryStatus = z.enum([
|
|
23
|
+
"transient",
|
|
24
|
+
"candidate",
|
|
25
|
+
"active",
|
|
26
|
+
"rejected"
|
|
27
|
+
]);
|
|
28
|
+
var MemoryType = z.enum([
|
|
29
|
+
"rule",
|
|
30
|
+
"command",
|
|
31
|
+
"gotcha",
|
|
32
|
+
"decision",
|
|
33
|
+
"review_pattern"
|
|
34
|
+
]);
|
|
35
|
+
var MemoryScope = z.enum(["session", "path", "repo", "team", "global"]);
|
|
36
|
+
var MemorySource = z.enum([
|
|
37
|
+
"user_correction",
|
|
38
|
+
"user_reported_review",
|
|
39
|
+
"repo_scan",
|
|
40
|
+
"config_parse"
|
|
41
|
+
]);
|
|
42
|
+
var FeedbackOutcome = z.enum([
|
|
43
|
+
"followed",
|
|
44
|
+
"overridden",
|
|
45
|
+
"ignored",
|
|
46
|
+
"contradicted"
|
|
47
|
+
]);
|
|
48
|
+
var EvidenceEntry = z.discriminatedUnion("type", [
|
|
49
|
+
z.object({
|
|
50
|
+
type: z.literal("session_correction"),
|
|
51
|
+
session: z.string(),
|
|
52
|
+
timestamp: z.string(),
|
|
53
|
+
context: z.string().optional()
|
|
54
|
+
}),
|
|
55
|
+
z.object({
|
|
56
|
+
type: z.literal("review_feedback"),
|
|
57
|
+
reported_by_user: z.boolean(),
|
|
58
|
+
reviewer: z.string().optional(),
|
|
59
|
+
timestamp: z.string(),
|
|
60
|
+
context: z.string().optional()
|
|
61
|
+
}),
|
|
62
|
+
z.object({
|
|
63
|
+
type: z.literal("repo_scan"),
|
|
64
|
+
file: z.string(),
|
|
65
|
+
timestamp: z.string()
|
|
66
|
+
}),
|
|
67
|
+
z.object({
|
|
68
|
+
type: z.literal("repeated_correction"),
|
|
69
|
+
count: z.number(),
|
|
70
|
+
sessions: z.array(z.string()),
|
|
71
|
+
timestamp: z.string()
|
|
72
|
+
})
|
|
73
|
+
]);
|
|
74
|
+
var CaptureContextToolCall = z.object({
|
|
75
|
+
name: z.string(),
|
|
76
|
+
path: z.string().optional(),
|
|
77
|
+
exit_code: z.number().optional()
|
|
78
|
+
});
|
|
79
|
+
var CaptureContext = z.object({
|
|
80
|
+
prev_assistant_text: z.string().optional(),
|
|
81
|
+
recent_tool_calls: z.array(CaptureContextToolCall).max(5).optional(),
|
|
82
|
+
repo: z.string().nullable().optional(),
|
|
83
|
+
path: z.string().nullable().optional(),
|
|
84
|
+
agent: z.string().optional()
|
|
85
|
+
});
|
|
86
|
+
var MemoryItem = z.object({
|
|
87
|
+
id: z.string().uuid(),
|
|
88
|
+
type: MemoryType,
|
|
89
|
+
text: z.string(),
|
|
90
|
+
scope: MemoryScope,
|
|
91
|
+
path_scope: z.string().nullable(),
|
|
92
|
+
repo: z.string().nullable(),
|
|
93
|
+
status: MemoryStatus,
|
|
94
|
+
confidence: z.number().min(0).max(1),
|
|
95
|
+
source: MemorySource,
|
|
96
|
+
evidence: z.array(EvidenceEntry),
|
|
97
|
+
capture_context: CaptureContext.nullable(),
|
|
98
|
+
supersedes: z.string().uuid().nullable(),
|
|
99
|
+
created_at: z.string(),
|
|
100
|
+
updated_at: z.string(),
|
|
101
|
+
last_validated_at: z.string().nullable(),
|
|
102
|
+
last_injected_at: z.string().nullable(),
|
|
103
|
+
injection_count: z.number().int().nonnegative(),
|
|
104
|
+
override_count: z.number().int().nonnegative(),
|
|
105
|
+
repetition_count: z.number().int().nonnegative(),
|
|
106
|
+
auto_inject: z.boolean()
|
|
107
|
+
});
|
|
108
|
+
var FeedbackEvent = z.object({
|
|
109
|
+
id: z.string().uuid(),
|
|
110
|
+
memory_id: z.string().uuid(),
|
|
111
|
+
session_id: z.string(),
|
|
112
|
+
injected: z.boolean(),
|
|
113
|
+
outcome: FeedbackOutcome,
|
|
114
|
+
timestamp: z.string()
|
|
115
|
+
});
|
|
116
|
+
var MemoryInjection = z.object({
|
|
117
|
+
id: z.string().uuid(),
|
|
118
|
+
memory_id: z.string().uuid(),
|
|
119
|
+
session_id: z.string(),
|
|
120
|
+
repo: z.string().nullable(),
|
|
121
|
+
injected_at: z.string(),
|
|
122
|
+
outcome: FeedbackOutcome.nullable(),
|
|
123
|
+
outcome_at: z.string().nullable()
|
|
124
|
+
});
|
|
125
|
+
var MaintenanceTaskKind = z.enum([
|
|
126
|
+
"summarize_history",
|
|
127
|
+
"merge_duplicates",
|
|
128
|
+
"refine_candidate",
|
|
129
|
+
"summarize_session",
|
|
130
|
+
"synthesize_repo",
|
|
131
|
+
"verify_capture"
|
|
132
|
+
]);
|
|
133
|
+
var MaintenanceTaskStatus = z.enum([
|
|
134
|
+
"pending",
|
|
135
|
+
"claimed",
|
|
136
|
+
"submitted",
|
|
137
|
+
"completed",
|
|
138
|
+
"abandoned"
|
|
139
|
+
]);
|
|
140
|
+
var MaintenanceTask = z.object({
|
|
141
|
+
id: z.string().uuid(),
|
|
142
|
+
kind: MaintenanceTaskKind,
|
|
143
|
+
status: MaintenanceTaskStatus,
|
|
144
|
+
priority: z.number().int(),
|
|
145
|
+
repo: z.string().nullable(),
|
|
146
|
+
target_key: z.string(),
|
|
147
|
+
payload: z.record(z.string(), z.unknown()),
|
|
148
|
+
result: z.record(z.string(), z.unknown()).nullable(),
|
|
149
|
+
failure_reason: z.string().nullable(),
|
|
150
|
+
claimed_by: z.string().nullable(),
|
|
151
|
+
claimed_at: z.string().nullable(),
|
|
152
|
+
claim_expires_at: z.string().nullable(),
|
|
153
|
+
submitted_at: z.string().nullable(),
|
|
154
|
+
completed_at: z.string().nullable(),
|
|
155
|
+
created_at: z.string(),
|
|
156
|
+
attempts: z.number().int().nonnegative(),
|
|
157
|
+
max_attempts: z.number().int().positive()
|
|
158
|
+
});
|
|
159
|
+
var ACTIVITY_SOURCE_PATTERN = /^(cli|daemon|mcp|system|hook)(:[a-z0-9][a-z0-9._-]*)?$/;
|
|
160
|
+
var ActivitySource = z.string().regex(ACTIVITY_SOURCE_PATTERN);
|
|
161
|
+
function tagActivitySource(transport, agent) {
|
|
162
|
+
if (!agent) return transport;
|
|
163
|
+
const normalized = agent.trim().toLowerCase().replace(/[^a-z0-9._-]+/g, "-");
|
|
164
|
+
if (!normalized) return transport;
|
|
165
|
+
return `${transport}:${normalized}`;
|
|
166
|
+
}
|
|
167
|
+
var ActivityEventType = z.enum([
|
|
168
|
+
"compile",
|
|
169
|
+
"query",
|
|
170
|
+
"scan",
|
|
171
|
+
"correction",
|
|
172
|
+
"review",
|
|
173
|
+
"feedback",
|
|
174
|
+
"signal",
|
|
175
|
+
"session_start",
|
|
176
|
+
"session_event",
|
|
177
|
+
"session_end",
|
|
178
|
+
"tool_call"
|
|
179
|
+
]);
|
|
180
|
+
var ActivityEvent = z.object({
|
|
181
|
+
id: z.string().uuid(),
|
|
182
|
+
session_id: z.string().nullable(),
|
|
183
|
+
repo: z.string().nullable(),
|
|
184
|
+
path: z.string().nullable(),
|
|
185
|
+
source: ActivitySource,
|
|
186
|
+
event_type: ActivityEventType,
|
|
187
|
+
memory_ids: z.array(z.string().uuid()),
|
|
188
|
+
request: z.record(z.string(), z.unknown()),
|
|
189
|
+
result: z.record(z.string(), z.unknown()),
|
|
190
|
+
created_at: z.string()
|
|
191
|
+
});
|
|
192
|
+
var ActivityEventQuery = z.object({
|
|
193
|
+
repo: z.string().optional(),
|
|
194
|
+
session_id: z.string().optional(),
|
|
195
|
+
source: ActivitySource.optional(),
|
|
196
|
+
event_type: ActivityEventType.optional(),
|
|
197
|
+
since: z.string().optional(),
|
|
198
|
+
limit: z.number().int().positive().optional()
|
|
199
|
+
});
|
|
200
|
+
var HookCallEvent = z.enum([
|
|
201
|
+
"session_started",
|
|
202
|
+
"prompt_submitted",
|
|
203
|
+
"tool_invoked",
|
|
204
|
+
"session_ended"
|
|
205
|
+
]);
|
|
206
|
+
var HookCall = z.object({
|
|
207
|
+
id: z.string().uuid(),
|
|
208
|
+
event: HookCallEvent,
|
|
209
|
+
agent: z.string(),
|
|
210
|
+
duration_ms: z.number().int().nonnegative(),
|
|
211
|
+
ok: z.boolean(),
|
|
212
|
+
created_at: z.string()
|
|
213
|
+
});
|
|
214
|
+
var HookCallStatsQuery = z.object({
|
|
215
|
+
agent: z.string().optional(),
|
|
216
|
+
event: HookCallEvent.optional(),
|
|
217
|
+
limit: z.number().int().positive().optional()
|
|
218
|
+
});
|
|
219
|
+
var HookCallStatsRow = z.object({
|
|
220
|
+
event: HookCallEvent,
|
|
221
|
+
agent: z.string(),
|
|
222
|
+
total_calls: z.number().int().nonnegative(),
|
|
223
|
+
ok_calls: z.number().int().nonnegative(),
|
|
224
|
+
error_calls: z.number().int().nonnegative(),
|
|
225
|
+
avg_duration_ms: z.number().nonnegative(),
|
|
226
|
+
max_duration_ms: z.number().int().nonnegative(),
|
|
227
|
+
last_called_at: z.string()
|
|
228
|
+
});
|
|
229
|
+
var CONFIDENCE = {
|
|
230
|
+
/** Below this → transient, never stored durably */
|
|
231
|
+
TRANSIENT_MAX: 0.3,
|
|
232
|
+
/** Below this → candidate, stored but not injected */
|
|
233
|
+
CANDIDATE_MAX: 0.6,
|
|
234
|
+
/** At or above this → active, injected when scope matches */
|
|
235
|
+
ACTIVE_MIN: 0.6
|
|
236
|
+
};
|
|
237
|
+
var PROMOTION = {
|
|
238
|
+
/** User explicitly confirms */
|
|
239
|
+
EXPLICIT_CONFIRM: 0.8,
|
|
240
|
+
/** Same correction repeats */
|
|
241
|
+
REPEAT_CORRECTION: 0.2,
|
|
242
|
+
/** User-reported review feedback */
|
|
243
|
+
REVIEW_FEEDBACK: 0.3,
|
|
244
|
+
/** Passive gain per use without override */
|
|
245
|
+
PASSIVE_GAIN: 0.05
|
|
246
|
+
};
|
|
247
|
+
var CompilerConfig = z.object({
|
|
248
|
+
confidence_threshold: z.number().default(0.6),
|
|
249
|
+
max_lines: z.number().default(15),
|
|
250
|
+
max_commands: z.number().default(3),
|
|
251
|
+
max_gotchas: z.number().default(3),
|
|
252
|
+
max_history_snippets: z.number().default(2),
|
|
253
|
+
token_budget: z.number().default(2e3),
|
|
254
|
+
include_candidates: z.boolean().default(false)
|
|
255
|
+
});
|
|
256
|
+
var MemoryQuery = z.object({
|
|
257
|
+
repo: z.string().optional(),
|
|
258
|
+
path: z.string().optional(),
|
|
259
|
+
scope: MemoryScope.optional(),
|
|
260
|
+
type: MemoryType.optional(),
|
|
261
|
+
status: MemoryStatus.optional(),
|
|
262
|
+
min_confidence: z.number().optional(),
|
|
263
|
+
semantic_query: z.string().optional(),
|
|
264
|
+
auto_inject: z.boolean().optional(),
|
|
265
|
+
limit: z.number().int().positive().optional(),
|
|
266
|
+
offset: z.number().int().nonnegative().optional()
|
|
267
|
+
});
|
|
268
|
+
var SyncConfig = z.object({
|
|
269
|
+
remote_url: z.string().url(),
|
|
270
|
+
api_key: z.string(),
|
|
271
|
+
team_id: z.string().optional(),
|
|
272
|
+
auto_sync: z.boolean().default(false),
|
|
273
|
+
sync_interval_seconds: z.number().default(300)
|
|
274
|
+
});
|
|
275
|
+
var SyncDirection = z.enum(["push", "pull", "both"]);
|
|
276
|
+
var SyncResult = z.object({
|
|
277
|
+
pushed: z.number(),
|
|
278
|
+
pulled: z.number(),
|
|
279
|
+
conflicts: z.number(),
|
|
280
|
+
errors: z.array(z.string())
|
|
281
|
+
});
|
|
282
|
+
var TeamMember = z.object({
|
|
283
|
+
id: z.string().uuid(),
|
|
284
|
+
team_id: z.string().uuid(),
|
|
285
|
+
user_id: z.string(),
|
|
286
|
+
role: z.enum(["owner", "admin", "member"]),
|
|
287
|
+
joined_at: z.string()
|
|
288
|
+
});
|
|
289
|
+
var EmbeddingConfig = z.object({
|
|
290
|
+
provider: z.enum(["nomic", "multilingual-e5", "bge-small-en-v1.5"]).default("nomic"),
|
|
291
|
+
model: z.string().default("nomic-ai/nomic-embed-text-v1.5"),
|
|
292
|
+
dimensions: z.number().default(512),
|
|
293
|
+
version: z.string().default("v1"),
|
|
294
|
+
similarity_threshold: z.number().default(0.8)
|
|
295
|
+
});
|
|
296
|
+
var HistorySnippetKind = z.enum([
|
|
297
|
+
"session_summary",
|
|
298
|
+
"correction_summary",
|
|
299
|
+
"decision_summary",
|
|
300
|
+
"review_summary",
|
|
301
|
+
"compile_summary",
|
|
302
|
+
"repo_synthesis"
|
|
303
|
+
]);
|
|
304
|
+
var HistorySnippet = z.object({
|
|
305
|
+
id: z.string().uuid(),
|
|
306
|
+
repo: z.string().nullable(),
|
|
307
|
+
session_id: z.string().nullable(),
|
|
308
|
+
kind: HistorySnippetKind,
|
|
309
|
+
text: z.string(),
|
|
310
|
+
source_activity_ids: z.array(z.string().uuid()),
|
|
311
|
+
created_at: z.string(),
|
|
312
|
+
updated_at: z.string()
|
|
313
|
+
});
|
|
314
|
+
var EvalSession = z.object({
|
|
315
|
+
id: z.string().uuid(),
|
|
316
|
+
repo: z.string(),
|
|
317
|
+
started_at: z.string(),
|
|
318
|
+
ended_at: z.string().nullable(),
|
|
319
|
+
memories_injected: z.number(),
|
|
320
|
+
memories_followed: z.number(),
|
|
321
|
+
memories_overridden: z.number(),
|
|
322
|
+
user_corrections: z.number(),
|
|
323
|
+
test_passes: z.number(),
|
|
324
|
+
test_failures: z.number()
|
|
325
|
+
});
|
|
326
|
+
var MaintenanceEvalMetrics = z.object({
|
|
327
|
+
total_completed: z.number(),
|
|
328
|
+
total_abandoned: z.number(),
|
|
329
|
+
abandon_rate: z.number(),
|
|
330
|
+
mean_completion_ms: z.number().nullable(),
|
|
331
|
+
completed_by_kind: z.record(z.number()),
|
|
332
|
+
merge_precision: z.number().nullable(),
|
|
333
|
+
merge_rollbacks: z.number()
|
|
334
|
+
});
|
|
335
|
+
var EvalMetrics = z.object({
|
|
336
|
+
total_sessions: z.number(),
|
|
337
|
+
injection_rate: z.number(),
|
|
338
|
+
follow_rate: z.number(),
|
|
339
|
+
override_rate: z.number(),
|
|
340
|
+
correction_frequency: z.number(),
|
|
341
|
+
avg_confidence_at_injection: z.number(),
|
|
342
|
+
memory_effectiveness: z.number(),
|
|
343
|
+
maintenance: MaintenanceEvalMetrics.optional()
|
|
344
|
+
});
|
|
345
|
+
var RetrievalEvalCase = z.object({
|
|
346
|
+
name: z.string(),
|
|
347
|
+
repo: z.string(),
|
|
348
|
+
path: z.string().optional(),
|
|
349
|
+
query_text: z.string().default(""),
|
|
350
|
+
include_candidates: z.boolean().default(false),
|
|
351
|
+
confidence_threshold: z.number().optional(),
|
|
352
|
+
max_lines: z.number().optional(),
|
|
353
|
+
max_commands: z.number().optional(),
|
|
354
|
+
max_gotchas: z.number().optional(),
|
|
355
|
+
token_budget: z.number().optional(),
|
|
356
|
+
expected_all_texts: z.array(z.string()).default([]),
|
|
357
|
+
expected_any_texts: z.array(z.string()).default([]),
|
|
358
|
+
forbidden_texts: z.array(z.string()).default([]),
|
|
359
|
+
min_included: z.number().int().nonnegative().optional(),
|
|
360
|
+
max_included: z.number().int().nonnegative().optional()
|
|
361
|
+
});
|
|
362
|
+
var RetrievalEvalFile = z.object({
|
|
363
|
+
cases: z.array(RetrievalEvalCase)
|
|
364
|
+
});
|
|
365
|
+
var ImplicitSignal = z.object({
|
|
366
|
+
id: z.string().uuid(),
|
|
367
|
+
memory_id: z.string().uuid(),
|
|
368
|
+
session_id: z.string(),
|
|
369
|
+
signal_type: z.enum([
|
|
370
|
+
"test_pass",
|
|
371
|
+
"test_fail",
|
|
372
|
+
"file_unchanged",
|
|
373
|
+
"file_rewritten",
|
|
374
|
+
"task_accepted",
|
|
375
|
+
"task_rejected"
|
|
376
|
+
]),
|
|
377
|
+
timestamp: z.string(),
|
|
378
|
+
context: z.string().optional()
|
|
379
|
+
});
|
|
380
|
+
var RecallConfig = z.object({
|
|
381
|
+
sync: SyncConfig.optional(),
|
|
382
|
+
embeddings: EmbeddingConfig.optional()
|
|
383
|
+
});
|
|
384
|
+
var PolicyRule = z.object({
|
|
385
|
+
id: z.string().uuid(),
|
|
386
|
+
org_id: z.string(),
|
|
387
|
+
rule_type: z.enum([
|
|
388
|
+
"min_confidence",
|
|
389
|
+
"require_approval",
|
|
390
|
+
"allowed_sources",
|
|
391
|
+
"blocked_scopes",
|
|
392
|
+
"auto_approve_pattern",
|
|
393
|
+
"max_active_per_repo",
|
|
394
|
+
"require_evidence_count"
|
|
395
|
+
]),
|
|
396
|
+
config: z.record(z.unknown()),
|
|
397
|
+
enabled: z.boolean(),
|
|
398
|
+
created_at: z.string(),
|
|
399
|
+
updated_at: z.string()
|
|
400
|
+
});
|
|
401
|
+
var ApprovalStatus = z.enum(["pending", "approved", "denied"]);
|
|
402
|
+
var ApprovalRequest = z.object({
|
|
403
|
+
id: z.string().uuid(),
|
|
404
|
+
memory_id: z.string().uuid(),
|
|
405
|
+
org_id: z.string(),
|
|
406
|
+
requested_by: z.string(),
|
|
407
|
+
status: ApprovalStatus,
|
|
408
|
+
reviewed_by: z.string().nullable(),
|
|
409
|
+
reason: z.string().nullable(),
|
|
410
|
+
created_at: z.string(),
|
|
411
|
+
resolved_at: z.string().nullable()
|
|
412
|
+
});
|
|
413
|
+
var HealthScore = z.object({
|
|
414
|
+
memory_id: z.string().uuid(),
|
|
415
|
+
score: z.number().min(0).max(1),
|
|
416
|
+
confidence_component: z.number(),
|
|
417
|
+
freshness_component: z.number(),
|
|
418
|
+
follow_rate_component: z.number(),
|
|
419
|
+
signal_ratio_component: z.number(),
|
|
420
|
+
computed_at: z.string()
|
|
421
|
+
});
|
|
422
|
+
var Contradiction = z.object({
|
|
423
|
+
id: z.string().uuid(),
|
|
424
|
+
memory_a_id: z.string().uuid(),
|
|
425
|
+
memory_b_id: z.string().uuid(),
|
|
426
|
+
contradiction_type: z.enum([
|
|
427
|
+
"direct_negation",
|
|
428
|
+
"conflicting_rules",
|
|
429
|
+
"scope_overlap",
|
|
430
|
+
"superseded"
|
|
431
|
+
]),
|
|
432
|
+
severity: z.enum(["low", "medium", "high"]),
|
|
433
|
+
description: z.string(),
|
|
434
|
+
resolved: z.boolean(),
|
|
435
|
+
resolution: z.string().nullable(),
|
|
436
|
+
detected_at: z.string(),
|
|
437
|
+
resolved_at: z.string().nullable()
|
|
438
|
+
});
|
|
439
|
+
var PruneConfig = z.object({
|
|
440
|
+
repo: z.string().optional(),
|
|
441
|
+
stale_days: z.number().default(90),
|
|
442
|
+
rejected_retention_days: z.number().default(30),
|
|
443
|
+
transient_retention_days: z.number().default(7),
|
|
444
|
+
min_health_score: z.number().default(0.2),
|
|
445
|
+
dry_run: z.boolean().default(false)
|
|
446
|
+
});
|
|
447
|
+
var AuditAction = z.enum([
|
|
448
|
+
"created",
|
|
449
|
+
"promoted",
|
|
450
|
+
"demoted",
|
|
451
|
+
"rejected",
|
|
452
|
+
"confirmed",
|
|
453
|
+
"reactivated",
|
|
454
|
+
"edited",
|
|
455
|
+
"pruned",
|
|
456
|
+
"policy_applied",
|
|
457
|
+
"approval_requested",
|
|
458
|
+
"approval_resolved",
|
|
459
|
+
"contradiction_detected",
|
|
460
|
+
"contradiction_resolved",
|
|
461
|
+
"rolled_back"
|
|
462
|
+
]);
|
|
463
|
+
var AuditEntry = z.object({
|
|
464
|
+
id: z.string().uuid(),
|
|
465
|
+
memory_id: z.string().uuid(),
|
|
466
|
+
action: AuditAction,
|
|
467
|
+
actor: z.string(),
|
|
468
|
+
before_snapshot: z.string().nullable(),
|
|
469
|
+
after_snapshot: z.string().nullable(),
|
|
470
|
+
reason: z.string().nullable(),
|
|
471
|
+
timestamp: z.string()
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
// src/embeddings/cache.ts
|
|
475
|
+
import { existsSync, mkdirSync, readdirSync, statSync } from "fs";
|
|
476
|
+
import { homedir } from "os";
|
|
477
|
+
import { join } from "path";
|
|
478
|
+
function getEmbeddingCacheRoot() {
|
|
479
|
+
if (process.platform === "linux" && process.env.XDG_CACHE_HOME) {
|
|
480
|
+
return join(process.env.XDG_CACHE_HOME, "recall", "models");
|
|
481
|
+
}
|
|
482
|
+
return join(homedir(), ".recall", "models");
|
|
483
|
+
}
|
|
484
|
+
function getEmbeddingCachePath(config) {
|
|
485
|
+
return join(getEmbeddingCacheRoot(), config.provider, ...config.model.split("/"));
|
|
486
|
+
}
|
|
487
|
+
function ensureEmbeddingCachePath(config) {
|
|
488
|
+
const cachePath = getEmbeddingCachePath(config);
|
|
489
|
+
mkdirSync(cachePath, { recursive: true });
|
|
490
|
+
return cachePath;
|
|
491
|
+
}
|
|
492
|
+
function getDirectorySize(path) {
|
|
493
|
+
if (!existsSync(path)) return 0;
|
|
494
|
+
const stat = statSync(path);
|
|
495
|
+
if (!stat.isDirectory()) return stat.size;
|
|
496
|
+
let total = 0;
|
|
497
|
+
for (const entry of readdirSync(path, { withFileTypes: true })) {
|
|
498
|
+
total += getDirectorySize(join(path, entry.name));
|
|
499
|
+
}
|
|
500
|
+
return total;
|
|
501
|
+
}
|
|
502
|
+
function formatBytes(bytes) {
|
|
503
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
504
|
+
if (bytes < 1024 ** 2) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
505
|
+
if (bytes < 1024 ** 3) return `${(bytes / 1024 ** 2).toFixed(1)} MB`;
|
|
506
|
+
return `${(bytes / 1024 ** 3).toFixed(1)} GB`;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// src/embeddings/providers/bge-small-en-v1.5.ts
|
|
510
|
+
import { pipeline } from "@huggingface/transformers";
|
|
511
|
+
var BGE_MODEL = "Xenova/bge-small-en-v1.5";
|
|
512
|
+
var BGE_DIMENSIONS = 384;
|
|
513
|
+
var extractorPromise = null;
|
|
514
|
+
function getModel(config) {
|
|
515
|
+
return config.model || BGE_MODEL;
|
|
516
|
+
}
|
|
517
|
+
function getDimensions(config) {
|
|
518
|
+
const dimensions = config.dimensions || BGE_DIMENSIONS;
|
|
519
|
+
if (!Number.isInteger(dimensions) || dimensions <= 0) {
|
|
520
|
+
throw new Error(`Invalid bge-small-en-v1.5 embedding dimensions: ${dimensions}`);
|
|
521
|
+
}
|
|
522
|
+
if (dimensions > BGE_DIMENSIONS) {
|
|
523
|
+
throw new Error(`bge-small-en-v1.5 embeddings support at most ${BGE_DIMENSIONS} dimensions`);
|
|
524
|
+
}
|
|
525
|
+
return dimensions;
|
|
526
|
+
}
|
|
527
|
+
async function getExtractor(config) {
|
|
528
|
+
const cacheDir = ensureEmbeddingCachePath({
|
|
529
|
+
provider: config.provider,
|
|
530
|
+
model: getModel(config)
|
|
531
|
+
});
|
|
532
|
+
extractorPromise ??= pipeline("feature-extraction", getModel(config), {
|
|
533
|
+
cache_dir: cacheDir,
|
|
534
|
+
dtype: "q8"
|
|
535
|
+
});
|
|
536
|
+
return extractorPromise;
|
|
537
|
+
}
|
|
538
|
+
function tensorToEmbeddings(tensor) {
|
|
539
|
+
const [rows, columns] = tensor.dims.length === 1 ? [1, tensor.dims[0]] : tensor.dims;
|
|
540
|
+
if (!rows || !columns) {
|
|
541
|
+
throw new Error(`Unexpected bge-small-en-v1.5 tensor shape: [${tensor.dims.join(", ")}]`);
|
|
542
|
+
}
|
|
543
|
+
const embeddings = [];
|
|
544
|
+
for (let row = 0; row < rows; row++) {
|
|
545
|
+
const start = row * columns;
|
|
546
|
+
const end = start + columns;
|
|
547
|
+
embeddings.push(Float32Array.from(tensor.data.subarray(start, end)));
|
|
548
|
+
}
|
|
549
|
+
return embeddings;
|
|
550
|
+
}
|
|
551
|
+
async function embedTexts(texts, config) {
|
|
552
|
+
if (texts.length === 0) return [];
|
|
553
|
+
const extractor = await getExtractor(config);
|
|
554
|
+
const embeddings = await extractor(texts, {
|
|
555
|
+
pooling: "mean",
|
|
556
|
+
normalize: true
|
|
557
|
+
});
|
|
558
|
+
return tensorToEmbeddings(
|
|
559
|
+
embeddings.dims.at(-1) === getDimensions(config) ? embeddings : embeddings.slice(null, [0, getDimensions(config)]).normalize(2, -1)
|
|
560
|
+
);
|
|
561
|
+
}
|
|
562
|
+
function createBgeSmallEnV15Provider(config) {
|
|
563
|
+
return {
|
|
564
|
+
async embed(text, _purpose = "document") {
|
|
565
|
+
const [embedding] = await embedTexts([text], config);
|
|
566
|
+
return embedding;
|
|
567
|
+
},
|
|
568
|
+
async embedBatch(texts, _purpose = "document") {
|
|
569
|
+
return embedTexts(texts, config);
|
|
570
|
+
},
|
|
571
|
+
async prepare() {
|
|
572
|
+
await getExtractor(config);
|
|
573
|
+
},
|
|
574
|
+
metadata() {
|
|
575
|
+
const dims = getDimensions(config);
|
|
576
|
+
return {
|
|
577
|
+
model: getModel(config),
|
|
578
|
+
dimensions: dims,
|
|
579
|
+
canonical_dimensions: dims,
|
|
580
|
+
index_dimensions: dims,
|
|
581
|
+
version: config.version,
|
|
582
|
+
estimated_size_mb: 133
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// src/embeddings/providers/multilingual-e5.ts
|
|
589
|
+
import { pipeline as pipeline2 } from "@huggingface/transformers";
|
|
590
|
+
var MULTILINGUAL_E5_MODEL = "Xenova/multilingual-e5-small";
|
|
591
|
+
var MULTILINGUAL_E5_NATIVE_DIMENSIONS = 384;
|
|
592
|
+
var MULTILINGUAL_E5_PREFIXES = {
|
|
593
|
+
document: "passage: ",
|
|
594
|
+
query: "query: "
|
|
595
|
+
};
|
|
596
|
+
var extractorPromise2 = null;
|
|
597
|
+
function getModel2(config) {
|
|
598
|
+
return config.model || MULTILINGUAL_E5_MODEL;
|
|
599
|
+
}
|
|
600
|
+
function getDimensions2(config) {
|
|
601
|
+
const dimensions = config.dimensions || MULTILINGUAL_E5_NATIVE_DIMENSIONS;
|
|
602
|
+
if (!Number.isInteger(dimensions) || dimensions <= 0) {
|
|
603
|
+
throw new Error(`Invalid multilingual-e5 embedding dimensions: ${dimensions}`);
|
|
604
|
+
}
|
|
605
|
+
if (dimensions > MULTILINGUAL_E5_NATIVE_DIMENSIONS) {
|
|
606
|
+
throw new Error(`multilingual-e5 embeddings support at most ${MULTILINGUAL_E5_NATIVE_DIMENSIONS} dimensions`);
|
|
607
|
+
}
|
|
608
|
+
return dimensions;
|
|
609
|
+
}
|
|
610
|
+
function prefixTexts(texts, purpose) {
|
|
611
|
+
const prefix = MULTILINGUAL_E5_PREFIXES[purpose];
|
|
612
|
+
return texts.map((text) => `${prefix}${text}`);
|
|
613
|
+
}
|
|
614
|
+
async function getExtractor2(config) {
|
|
615
|
+
const cacheDir = ensureEmbeddingCachePath({
|
|
616
|
+
provider: config.provider,
|
|
617
|
+
model: getModel2(config)
|
|
618
|
+
});
|
|
619
|
+
extractorPromise2 ??= pipeline2("feature-extraction", getModel2(config), {
|
|
620
|
+
cache_dir: cacheDir,
|
|
621
|
+
dtype: "q8"
|
|
622
|
+
});
|
|
623
|
+
return extractorPromise2;
|
|
624
|
+
}
|
|
625
|
+
function tensorToEmbeddings2(tensor) {
|
|
626
|
+
const [rows, columns] = tensor.dims.length === 1 ? [1, tensor.dims[0]] : tensor.dims;
|
|
627
|
+
if (!rows || !columns) {
|
|
628
|
+
throw new Error(`Unexpected multilingual-e5 tensor shape: [${tensor.dims.join(", ")}]`);
|
|
629
|
+
}
|
|
630
|
+
const embeddings = [];
|
|
631
|
+
for (let row = 0; row < rows; row++) {
|
|
632
|
+
const start = row * columns;
|
|
633
|
+
const end = start + columns;
|
|
634
|
+
embeddings.push(Float32Array.from(tensor.data.subarray(start, end)));
|
|
635
|
+
}
|
|
636
|
+
return embeddings;
|
|
637
|
+
}
|
|
638
|
+
async function embedTexts2(texts, config, purpose) {
|
|
639
|
+
if (texts.length === 0) return [];
|
|
640
|
+
const extractor = await getExtractor2(config);
|
|
641
|
+
const embeddings = await extractor(prefixTexts(texts, purpose), {
|
|
642
|
+
pooling: "mean",
|
|
643
|
+
normalize: true
|
|
644
|
+
});
|
|
645
|
+
return tensorToEmbeddings2(
|
|
646
|
+
embeddings.dims.at(-1) === getDimensions2(config) ? embeddings : embeddings.slice(null, [0, getDimensions2(config)]).normalize(2, -1)
|
|
647
|
+
);
|
|
648
|
+
}
|
|
649
|
+
function createMultilingualE5Provider(config) {
|
|
650
|
+
return {
|
|
651
|
+
async embed(text, purpose = "document") {
|
|
652
|
+
const [embedding] = await embedTexts2([text], config, purpose);
|
|
653
|
+
return embedding;
|
|
654
|
+
},
|
|
655
|
+
async embedBatch(texts, purpose = "document") {
|
|
656
|
+
return embedTexts2(texts, config, purpose);
|
|
657
|
+
},
|
|
658
|
+
async prepare() {
|
|
659
|
+
await getExtractor2(config);
|
|
660
|
+
},
|
|
661
|
+
metadata() {
|
|
662
|
+
const dims = getDimensions2(config);
|
|
663
|
+
return {
|
|
664
|
+
model: getModel2(config),
|
|
665
|
+
dimensions: dims,
|
|
666
|
+
canonical_dimensions: dims,
|
|
667
|
+
index_dimensions: dims,
|
|
668
|
+
version: config.version,
|
|
669
|
+
task_prefix: `${MULTILINGUAL_E5_PREFIXES.document.trim()} | ${MULTILINGUAL_E5_PREFIXES.query.trim()}`,
|
|
670
|
+
estimated_size_mb: 113
|
|
671
|
+
};
|
|
672
|
+
}
|
|
673
|
+
};
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// src/embeddings/providers/nomic.ts
|
|
677
|
+
import { layer_norm, pipeline as pipeline3 } from "@huggingface/transformers";
|
|
678
|
+
var NOMIC_MODEL = "nomic-ai/nomic-embed-text-v1.5";
|
|
679
|
+
var NOMIC_NATIVE_DIMENSIONS = 768;
|
|
680
|
+
var NOMIC_PREFIXES = {
|
|
681
|
+
document: "search_document: ",
|
|
682
|
+
query: "search_query: "
|
|
683
|
+
};
|
|
684
|
+
var extractorPromise3 = null;
|
|
685
|
+
function getModel3(config) {
|
|
686
|
+
return config.model || NOMIC_MODEL;
|
|
687
|
+
}
|
|
688
|
+
function getDimensions3(config) {
|
|
689
|
+
const dimensions = config.dimensions || 512;
|
|
690
|
+
if (!Number.isInteger(dimensions) || dimensions <= 0) {
|
|
691
|
+
throw new Error(`Invalid nomic embedding dimensions: ${dimensions}`);
|
|
692
|
+
}
|
|
693
|
+
if (dimensions > NOMIC_NATIVE_DIMENSIONS) {
|
|
694
|
+
throw new Error(`Nomic embeddings support at most ${NOMIC_NATIVE_DIMENSIONS} dimensions`);
|
|
695
|
+
}
|
|
696
|
+
return dimensions;
|
|
697
|
+
}
|
|
698
|
+
function prefixTexts2(texts, purpose) {
|
|
699
|
+
const prefix = NOMIC_PREFIXES[purpose];
|
|
700
|
+
return texts.map((text) => `${prefix}${text}`);
|
|
701
|
+
}
|
|
702
|
+
async function getExtractor3(config) {
|
|
703
|
+
const cacheDir = ensureEmbeddingCachePath({
|
|
704
|
+
provider: config.provider,
|
|
705
|
+
model: getModel3(config)
|
|
706
|
+
});
|
|
707
|
+
extractorPromise3 ??= pipeline3("feature-extraction", getModel3(config), {
|
|
708
|
+
cache_dir: cacheDir,
|
|
709
|
+
dtype: "q8"
|
|
710
|
+
});
|
|
711
|
+
return extractorPromise3;
|
|
712
|
+
}
|
|
713
|
+
function tensorToEmbeddings3(tensor) {
|
|
714
|
+
const [rows, columns] = tensor.dims.length === 1 ? [1, tensor.dims[0]] : tensor.dims;
|
|
715
|
+
if (!rows || !columns) {
|
|
716
|
+
throw new Error(`Unexpected nomic tensor shape: [${tensor.dims.join(", ")}]`);
|
|
717
|
+
}
|
|
718
|
+
const embeddings = [];
|
|
719
|
+
for (let row = 0; row < rows; row++) {
|
|
720
|
+
const start = row * columns;
|
|
721
|
+
const end = start + columns;
|
|
722
|
+
embeddings.push(Float32Array.from(tensor.data.subarray(start, end)));
|
|
723
|
+
}
|
|
724
|
+
return embeddings;
|
|
725
|
+
}
|
|
726
|
+
async function embedTexts3(texts, config, purpose) {
|
|
727
|
+
if (texts.length === 0) return [];
|
|
728
|
+
const extractor = await getExtractor3(config);
|
|
729
|
+
const rawEmbeddings = await extractor(prefixTexts2(texts, purpose), {
|
|
730
|
+
pooling: "mean"
|
|
731
|
+
});
|
|
732
|
+
const nativeDimensions = rawEmbeddings.dims.at(-1);
|
|
733
|
+
if (!nativeDimensions) {
|
|
734
|
+
throw new Error("Nomic extractor returned an embedding tensor without dimensions");
|
|
735
|
+
}
|
|
736
|
+
const normalized = layer_norm(rawEmbeddings, [nativeDimensions]).normalize(2, -1);
|
|
737
|
+
return tensorToEmbeddings3(normalized);
|
|
738
|
+
}
|
|
739
|
+
function createNomicProvider(config) {
|
|
740
|
+
return {
|
|
741
|
+
async embed(text, purpose = "document") {
|
|
742
|
+
const [embedding] = await embedTexts3([text], config, purpose);
|
|
743
|
+
return embedding;
|
|
744
|
+
},
|
|
745
|
+
async embedBatch(texts, purpose = "document") {
|
|
746
|
+
return embedTexts3(texts, config, purpose);
|
|
747
|
+
},
|
|
748
|
+
async prepare() {
|
|
749
|
+
await getExtractor3(config);
|
|
750
|
+
},
|
|
751
|
+
metadata() {
|
|
752
|
+
const indexDimensions = getDimensions3(config);
|
|
753
|
+
return {
|
|
754
|
+
model: getModel3(config),
|
|
755
|
+
dimensions: indexDimensions,
|
|
756
|
+
canonical_dimensions: NOMIC_NATIVE_DIMENSIONS,
|
|
757
|
+
index_dimensions: indexDimensions,
|
|
758
|
+
version: config.version,
|
|
759
|
+
task_prefix: `${NOMIC_PREFIXES.document.trim()} | ${NOMIC_PREFIXES.query.trim()}`,
|
|
760
|
+
estimated_size_mb: 140
|
|
761
|
+
};
|
|
762
|
+
}
|
|
763
|
+
};
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
// src/embeddings/providers/index.ts
|
|
767
|
+
function resolveProvider(config) {
|
|
768
|
+
switch (config.provider) {
|
|
769
|
+
case "bge-small-en-v1.5":
|
|
770
|
+
return createBgeSmallEnV15Provider(config);
|
|
771
|
+
case "multilingual-e5":
|
|
772
|
+
return createMultilingualE5Provider(config);
|
|
773
|
+
case "nomic":
|
|
774
|
+
return createNomicProvider(config);
|
|
775
|
+
default:
|
|
776
|
+
throw new Error(`Unsupported embedding provider: ${config.provider}`);
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
// src/vector/sqlite-vec.ts
|
|
781
|
+
import * as sqliteVec from "sqlite-vec";
|
|
782
|
+
import { eq } from "drizzle-orm";
|
|
783
|
+
var VEC_MEMORY_INDEX = "vec_memory_index";
|
|
784
|
+
var loadedClients = /* @__PURE__ */ new WeakSet();
|
|
785
|
+
function getSqlite(db) {
|
|
786
|
+
return db.$client;
|
|
787
|
+
}
|
|
788
|
+
function hasMemoryVecIndex(db) {
|
|
789
|
+
return Boolean(
|
|
790
|
+
getSqlite(db).prepare("select 1 from sqlite_master where type = 'table' and name = ?").get(VEC_MEMORY_INDEX)
|
|
791
|
+
);
|
|
792
|
+
}
|
|
793
|
+
function ensureSqliteVecLoaded(db) {
|
|
794
|
+
const sqlite = getSqlite(db);
|
|
795
|
+
if (loadedClients.has(sqlite)) return;
|
|
796
|
+
sqliteVec.load(sqlite);
|
|
797
|
+
loadedClients.add(sqlite);
|
|
798
|
+
}
|
|
799
|
+
function getMemoryVecDimension(rows) {
|
|
800
|
+
const dimensions = [...new Set(rows.map((row) => row.index_dimensions))];
|
|
801
|
+
if (dimensions.length === 0) return null;
|
|
802
|
+
if (dimensions.length > 1) {
|
|
803
|
+
throw new Error(
|
|
804
|
+
`sqlite-vec index rebuild refused mixed memory embedding dimensions: ${dimensions.join(", ")}.`
|
|
805
|
+
);
|
|
806
|
+
}
|
|
807
|
+
return dimensions[0];
|
|
808
|
+
}
|
|
809
|
+
function ensureMemoryVecIndex(db, dimensions) {
|
|
810
|
+
ensureSqliteVecLoaded(db);
|
|
811
|
+
const sqlite = getSqlite(db);
|
|
812
|
+
const existing = sqlite.prepare("select sql from sqlite_master where type = 'table' and name = ?").get(VEC_MEMORY_INDEX);
|
|
813
|
+
const expectedDimension = `float[${dimensions}]`;
|
|
814
|
+
if (existing?.sql && !existing.sql.includes(expectedDimension)) {
|
|
815
|
+
throw new Error(
|
|
816
|
+
`sqlite-vec index dimension mismatch. Expected ${expectedDimension}. Run \`recall embeddings rebuild-index\`.`
|
|
817
|
+
);
|
|
818
|
+
}
|
|
819
|
+
sqlite.exec(`
|
|
820
|
+
create virtual table if not exists ${VEC_MEMORY_INDEX} using vec0(
|
|
821
|
+
embedding float[${dimensions}] distance_metric=cosine,
|
|
822
|
+
memory_id text,
|
|
823
|
+
repo text,
|
|
824
|
+
status text,
|
|
825
|
+
type text,
|
|
826
|
+
scope text
|
|
827
|
+
);
|
|
828
|
+
`);
|
|
829
|
+
}
|
|
830
|
+
function dropMemoryVecIndex(db) {
|
|
831
|
+
ensureSqliteVecLoaded(db);
|
|
832
|
+
getSqlite(db).exec(`drop table if exists ${VEC_MEMORY_INDEX};`);
|
|
833
|
+
}
|
|
834
|
+
function upsertMemoryVecRow(db, memory, embeddingRow) {
|
|
835
|
+
ensureMemoryVecIndex(db, embeddingRow.index_dimensions);
|
|
836
|
+
const sqlite = getSqlite(db);
|
|
837
|
+
sqlite.prepare(`delete from ${VEC_MEMORY_INDEX} where memory_id = ?`).run(memory.id);
|
|
838
|
+
sqlite.prepare(`
|
|
839
|
+
insert into ${VEC_MEMORY_INDEX} (
|
|
840
|
+
embedding,
|
|
841
|
+
memory_id,
|
|
842
|
+
repo,
|
|
843
|
+
status,
|
|
844
|
+
type,
|
|
845
|
+
scope
|
|
846
|
+
) values (?, ?, ?, ?, ?, ?)
|
|
847
|
+
`).run(
|
|
848
|
+
projectIndexBuffer(embeddingRow.embedding, embeddingRow.index_dimensions),
|
|
849
|
+
memory.id,
|
|
850
|
+
memory.repo ?? "",
|
|
851
|
+
memory.status,
|
|
852
|
+
memory.type,
|
|
853
|
+
memory.scope
|
|
854
|
+
);
|
|
855
|
+
}
|
|
856
|
+
function removeMemoryVecRow(db, memoryId, config) {
|
|
857
|
+
ensureSqliteVecLoaded(db);
|
|
858
|
+
if (!hasMemoryVecIndex(db)) return;
|
|
859
|
+
getSqlite(db).prepare(`delete from ${VEC_MEMORY_INDEX} where memory_id = ?`).run(memoryId);
|
|
860
|
+
}
|
|
861
|
+
function rebuildMemoryVecIndex(db, config, options = {}) {
|
|
862
|
+
const rows = db.select({
|
|
863
|
+
id: memories.id,
|
|
864
|
+
repo: memories.repo,
|
|
865
|
+
status: memories.status,
|
|
866
|
+
type: memories.type,
|
|
867
|
+
scope: memories.scope,
|
|
868
|
+
index_dimensions: memoryEmbeddings.index_dimensions,
|
|
869
|
+
embedding: memoryEmbeddings.embedding
|
|
870
|
+
}).from(memories).innerJoin(memoryEmbeddings, eq(memoryEmbeddings.memory_id, memories.id)).all().filter((row) => !options.repo || row.repo === options.repo);
|
|
871
|
+
const storedDimension = getMemoryVecDimension(rows);
|
|
872
|
+
const targetDimension = storedDimension ?? config.dimensions;
|
|
873
|
+
if (options.repo) {
|
|
874
|
+
if (rows.length > 0) {
|
|
875
|
+
ensureMemoryVecIndex(db, targetDimension);
|
|
876
|
+
}
|
|
877
|
+
if (!hasMemoryVecIndex(db)) return 0;
|
|
878
|
+
getSqlite(db).prepare(`delete from ${VEC_MEMORY_INDEX} where repo = ?`).run(options.repo);
|
|
879
|
+
if (rows.length === 0) return 0;
|
|
880
|
+
} else {
|
|
881
|
+
dropMemoryVecIndex(db);
|
|
882
|
+
ensureMemoryVecIndex(db, targetDimension);
|
|
883
|
+
}
|
|
884
|
+
const sqlite = getSqlite(db);
|
|
885
|
+
const stmt = sqlite.prepare(`
|
|
886
|
+
insert into ${VEC_MEMORY_INDEX} (
|
|
887
|
+
embedding,
|
|
888
|
+
memory_id,
|
|
889
|
+
repo,
|
|
890
|
+
status,
|
|
891
|
+
type,
|
|
892
|
+
scope
|
|
893
|
+
) values (?, ?, ?, ?, ?, ?)
|
|
894
|
+
`);
|
|
895
|
+
const insertMany = sqlite.transaction((batch) => {
|
|
896
|
+
for (const row of batch) {
|
|
897
|
+
stmt.run(
|
|
898
|
+
projectIndexBuffer(row.embedding, row.index_dimensions),
|
|
899
|
+
row.id,
|
|
900
|
+
row.repo ?? "",
|
|
901
|
+
row.status,
|
|
902
|
+
row.type,
|
|
903
|
+
row.scope
|
|
904
|
+
);
|
|
905
|
+
}
|
|
906
|
+
});
|
|
907
|
+
insertMany(rows);
|
|
908
|
+
return rows.length;
|
|
909
|
+
}
|
|
910
|
+
function projectIndexBuffer(buffer, indexDimensions) {
|
|
911
|
+
const embedding = new Float32Array(
|
|
912
|
+
buffer.buffer,
|
|
913
|
+
buffer.byteOffset,
|
|
914
|
+
buffer.byteLength / Float32Array.BYTES_PER_ELEMENT
|
|
915
|
+
);
|
|
916
|
+
if (embedding.length === indexDimensions) {
|
|
917
|
+
return buffer;
|
|
918
|
+
}
|
|
919
|
+
if (embedding.length < indexDimensions) {
|
|
920
|
+
throw new Error(`Canonical embedding width ${embedding.length} is smaller than index width ${indexDimensions}.`);
|
|
921
|
+
}
|
|
922
|
+
const sliced = embedding.slice(0, indexDimensions);
|
|
923
|
+
let norm = 0;
|
|
924
|
+
for (const value of sliced) norm += value * value;
|
|
925
|
+
const scale = Math.sqrt(norm) || 1;
|
|
926
|
+
for (let i = 0; i < sliced.length; i++) {
|
|
927
|
+
sliced[i] /= scale;
|
|
928
|
+
}
|
|
929
|
+
return Buffer.from(sliced.buffer, sliced.byteOffset, sliced.byteLength);
|
|
930
|
+
}
|
|
931
|
+
function verifyMemoryVecIndex(db, options = {}) {
|
|
932
|
+
ensureSqliteVecLoaded(db);
|
|
933
|
+
const sqlite = getSqlite(db);
|
|
934
|
+
const exists = sqlite.prepare("select 1 from sqlite_master where type = 'table' and name = ?").get(VEC_MEMORY_INDEX);
|
|
935
|
+
const expectedRows = db.select({
|
|
936
|
+
memory_id: memoryEmbeddings.memory_id,
|
|
937
|
+
repo: memories.repo
|
|
938
|
+
}).from(memoryEmbeddings).innerJoin(memories, eq(memories.id, memoryEmbeddings.memory_id)).all().filter((row) => !options.repo || row.repo === options.repo);
|
|
939
|
+
let indexed = 0;
|
|
940
|
+
if (exists) {
|
|
941
|
+
if (options.repo) {
|
|
942
|
+
const result = sqlite.prepare(`select count(*) as count from ${VEC_MEMORY_INDEX} where repo = ?`).get(options.repo);
|
|
943
|
+
indexed = result.count;
|
|
944
|
+
} else {
|
|
945
|
+
const result = sqlite.prepare(`select count(*) as count from ${VEC_MEMORY_INDEX}`).get();
|
|
946
|
+
indexed = result.count;
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
return {
|
|
950
|
+
expected: expectedRows.length,
|
|
951
|
+
indexed,
|
|
952
|
+
drift: expectedRows.length - indexed
|
|
953
|
+
};
|
|
954
|
+
}
|
|
955
|
+
function searchMemoryVecIndex(db, queryEmbedding, options = {}) {
|
|
956
|
+
ensureSqliteVecLoaded(db);
|
|
957
|
+
const sqlite = getSqlite(db);
|
|
958
|
+
const exists = sqlite.prepare("select 1 from sqlite_master where type = 'table' and name = ?").get(VEC_MEMORY_INDEX);
|
|
959
|
+
if (!exists) return [];
|
|
960
|
+
const limit = options.limit ?? 10;
|
|
961
|
+
if (options.repo) {
|
|
962
|
+
return sqlite.prepare(`
|
|
963
|
+
select memory_id, distance
|
|
964
|
+
from ${VEC_MEMORY_INDEX}
|
|
965
|
+
where embedding match ?
|
|
966
|
+
and k = ?
|
|
967
|
+
and repo = ?
|
|
968
|
+
order by distance
|
|
969
|
+
`).all(queryEmbedding, limit, options.repo);
|
|
970
|
+
}
|
|
971
|
+
return sqlite.prepare(`
|
|
972
|
+
select memory_id, distance
|
|
973
|
+
from ${VEC_MEMORY_INDEX}
|
|
974
|
+
where embedding match ?
|
|
975
|
+
and k = ?
|
|
976
|
+
order by distance
|
|
977
|
+
`).all(queryEmbedding, limit);
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
// src/vector/sqlite-fts.ts
|
|
981
|
+
import { eq as eq2 } from "drizzle-orm";
|
|
982
|
+
var FTS_MEMORY_INDEX = "fts_memory_index";
|
|
983
|
+
function getSqlite2(db) {
|
|
984
|
+
return db.$client;
|
|
985
|
+
}
|
|
986
|
+
function shouldIndexLexically(memory) {
|
|
987
|
+
return memory.status !== "rejected" && memory.status !== "transient";
|
|
988
|
+
}
|
|
989
|
+
function buildFtsQuery(query) {
|
|
990
|
+
const tokens = query.match(/[A-Za-z0-9_.:/-]+/g)?.map((token) => token.replace(/"/g, '""')).filter(Boolean) ?? [];
|
|
991
|
+
if (tokens.length === 0) return null;
|
|
992
|
+
return tokens.map((token) => `"${token}"`).join(" ");
|
|
993
|
+
}
|
|
994
|
+
function ensureMemoryFtsIndex(db) {
|
|
995
|
+
const sqlite = getSqlite2(db);
|
|
996
|
+
sqlite.exec(`
|
|
997
|
+
create virtual table if not exists ${FTS_MEMORY_INDEX} using fts5(
|
|
998
|
+
memory_id UNINDEXED,
|
|
999
|
+
text,
|
|
1000
|
+
repo UNINDEXED,
|
|
1001
|
+
status UNINDEXED,
|
|
1002
|
+
type UNINDEXED,
|
|
1003
|
+
scope UNINDEXED,
|
|
1004
|
+
path_scope UNINDEXED
|
|
1005
|
+
);
|
|
1006
|
+
`);
|
|
1007
|
+
}
|
|
1008
|
+
function dropMemoryFtsIndex(db) {
|
|
1009
|
+
getSqlite2(db).exec(`drop table if exists ${FTS_MEMORY_INDEX};`);
|
|
1010
|
+
}
|
|
1011
|
+
function removeMemoryFtsRow(db, memoryId) {
|
|
1012
|
+
const sqlite = getSqlite2(db);
|
|
1013
|
+
const exists = sqlite.prepare("select 1 from sqlite_master where type = 'table' and name = ?").get(FTS_MEMORY_INDEX);
|
|
1014
|
+
if (!exists) return;
|
|
1015
|
+
sqlite.prepare(`delete from ${FTS_MEMORY_INDEX} where memory_id = ?`).run(memoryId);
|
|
1016
|
+
}
|
|
1017
|
+
function upsertMemoryFtsRow(db, memory) {
|
|
1018
|
+
ensureMemoryFtsIndex(db);
|
|
1019
|
+
if (!shouldIndexLexically(memory)) {
|
|
1020
|
+
removeMemoryFtsRow(db, memory.id);
|
|
1021
|
+
return;
|
|
1022
|
+
}
|
|
1023
|
+
const sqlite = getSqlite2(db);
|
|
1024
|
+
sqlite.prepare(`delete from ${FTS_MEMORY_INDEX} where memory_id = ?`).run(memory.id);
|
|
1025
|
+
sqlite.prepare(`
|
|
1026
|
+
insert into ${FTS_MEMORY_INDEX} (
|
|
1027
|
+
memory_id,
|
|
1028
|
+
text,
|
|
1029
|
+
repo,
|
|
1030
|
+
status,
|
|
1031
|
+
type,
|
|
1032
|
+
scope,
|
|
1033
|
+
path_scope
|
|
1034
|
+
) values (?, ?, ?, ?, ?, ?, ?)
|
|
1035
|
+
`).run(
|
|
1036
|
+
memory.id,
|
|
1037
|
+
memory.text,
|
|
1038
|
+
memory.repo ?? "",
|
|
1039
|
+
memory.status,
|
|
1040
|
+
memory.type,
|
|
1041
|
+
memory.scope,
|
|
1042
|
+
memory.path_scope ?? ""
|
|
1043
|
+
);
|
|
1044
|
+
}
|
|
1045
|
+
function syncMemoryFtsIndex(db, memoryId) {
|
|
1046
|
+
const memory = db.select().from(memories).where(eq2(memories.id, memoryId)).get();
|
|
1047
|
+
if (!memory) {
|
|
1048
|
+
removeMemoryFtsRow(db, memoryId);
|
|
1049
|
+
return "removed";
|
|
1050
|
+
}
|
|
1051
|
+
upsertMemoryFtsRow(db, memory);
|
|
1052
|
+
return shouldIndexLexically(memory) ? "stored" : "removed";
|
|
1053
|
+
}
|
|
1054
|
+
function rebuildMemoryFtsIndex(db, options = {}) {
|
|
1055
|
+
if (options.repo) {
|
|
1056
|
+
ensureMemoryFtsIndex(db);
|
|
1057
|
+
getSqlite2(db).prepare(`delete from ${FTS_MEMORY_INDEX} where repo = ?`).run(options.repo);
|
|
1058
|
+
} else {
|
|
1059
|
+
dropMemoryFtsIndex(db);
|
|
1060
|
+
ensureMemoryFtsIndex(db);
|
|
1061
|
+
}
|
|
1062
|
+
const rows = db.select().from(memories).all().filter((row) => !options.repo || row.repo === options.repo).filter((row) => shouldIndexLexically(row));
|
|
1063
|
+
const sqlite = getSqlite2(db);
|
|
1064
|
+
const stmt = sqlite.prepare(`
|
|
1065
|
+
insert into ${FTS_MEMORY_INDEX} (
|
|
1066
|
+
memory_id,
|
|
1067
|
+
text,
|
|
1068
|
+
repo,
|
|
1069
|
+
status,
|
|
1070
|
+
type,
|
|
1071
|
+
scope,
|
|
1072
|
+
path_scope
|
|
1073
|
+
) values (?, ?, ?, ?, ?, ?, ?)
|
|
1074
|
+
`);
|
|
1075
|
+
const insertMany = sqlite.transaction((batch) => {
|
|
1076
|
+
for (const row of batch) {
|
|
1077
|
+
stmt.run(
|
|
1078
|
+
row.id,
|
|
1079
|
+
row.text,
|
|
1080
|
+
row.repo ?? "",
|
|
1081
|
+
row.status,
|
|
1082
|
+
row.type,
|
|
1083
|
+
row.scope,
|
|
1084
|
+
row.path_scope ?? ""
|
|
1085
|
+
);
|
|
1086
|
+
}
|
|
1087
|
+
});
|
|
1088
|
+
insertMany(rows);
|
|
1089
|
+
return rows.length;
|
|
1090
|
+
}
|
|
1091
|
+
function verifyMemoryFtsIndex(db, options = {}) {
|
|
1092
|
+
const sqlite = getSqlite2(db);
|
|
1093
|
+
const exists = sqlite.prepare("select 1 from sqlite_master where type = 'table' and name = ?").get(FTS_MEMORY_INDEX);
|
|
1094
|
+
const expected = db.select().from(memories).all().filter((row) => !options.repo || row.repo === options.repo).filter((row) => shouldIndexLexically(row)).length;
|
|
1095
|
+
let indexed = 0;
|
|
1096
|
+
if (exists) {
|
|
1097
|
+
if (options.repo) {
|
|
1098
|
+
const result = sqlite.prepare(`select count(*) as count from ${FTS_MEMORY_INDEX} where repo = ?`).get(options.repo);
|
|
1099
|
+
indexed = result.count;
|
|
1100
|
+
} else {
|
|
1101
|
+
const result = sqlite.prepare(`select count(*) as count from ${FTS_MEMORY_INDEX}`).get();
|
|
1102
|
+
indexed = result.count;
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
return {
|
|
1106
|
+
expected,
|
|
1107
|
+
indexed,
|
|
1108
|
+
drift: expected - indexed
|
|
1109
|
+
};
|
|
1110
|
+
}
|
|
1111
|
+
function searchMemoryFtsIndex(db, query, options = {}) {
|
|
1112
|
+
ensureMemoryFtsIndex(db);
|
|
1113
|
+
const sqlite = getSqlite2(db);
|
|
1114
|
+
const limit = options.limit ?? 10;
|
|
1115
|
+
const ftsQuery = buildFtsQuery(query);
|
|
1116
|
+
if (!ftsQuery) return [];
|
|
1117
|
+
if (options.repo) {
|
|
1118
|
+
return sqlite.prepare(`
|
|
1119
|
+
select memory_id, bm25(${FTS_MEMORY_INDEX}) as lexical_rank
|
|
1120
|
+
from ${FTS_MEMORY_INDEX}
|
|
1121
|
+
where ${FTS_MEMORY_INDEX} match ?
|
|
1122
|
+
and repo = ?
|
|
1123
|
+
order by lexical_rank
|
|
1124
|
+
limit ?
|
|
1125
|
+
`).all(ftsQuery, options.repo, limit);
|
|
1126
|
+
}
|
|
1127
|
+
return sqlite.prepare(`
|
|
1128
|
+
select memory_id, bm25(${FTS_MEMORY_INDEX}) as lexical_rank
|
|
1129
|
+
from ${FTS_MEMORY_INDEX}
|
|
1130
|
+
where ${FTS_MEMORY_INDEX} match ?
|
|
1131
|
+
order by lexical_rank
|
|
1132
|
+
limit ?
|
|
1133
|
+
`).all(ftsQuery, limit);
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
// src/embeddings/embeddings.ts
|
|
1137
|
+
var EMBEDDING_BATCH_SIZE = 100;
|
|
1138
|
+
var MIN_HYBRID_VECTOR_SIMILARITY = 0.7;
|
|
1139
|
+
var pendingEmbeddingJobs = /* @__PURE__ */ new Set();
|
|
1140
|
+
var EMBEDDING_DEFAULTS = {
|
|
1141
|
+
nomic: {
|
|
1142
|
+
model: "nomic-ai/nomic-embed-text-v1.5",
|
|
1143
|
+
dimensions: 512
|
|
1144
|
+
},
|
|
1145
|
+
"multilingual-e5": {
|
|
1146
|
+
model: "Xenova/multilingual-e5-small",
|
|
1147
|
+
dimensions: 384
|
|
1148
|
+
},
|
|
1149
|
+
"bge-small-en-v1.5": {
|
|
1150
|
+
model: "Xenova/bge-small-en-v1.5",
|
|
1151
|
+
dimensions: 384
|
|
1152
|
+
}
|
|
1153
|
+
};
|
|
1154
|
+
function loadEmbeddingConfigFromEnv() {
|
|
1155
|
+
if (process.env.RECALL_EMBEDDINGS_DISABLED === "true") return null;
|
|
1156
|
+
const provider = process.env.RECALL_EMBEDDING_PROVIDER === "multilingual-e5" ? "multilingual-e5" : "nomic";
|
|
1157
|
+
const defaults = EMBEDDING_DEFAULTS[provider];
|
|
1158
|
+
return {
|
|
1159
|
+
provider,
|
|
1160
|
+
model: process.env.RECALL_EMBEDDING_MODEL ?? defaults.model,
|
|
1161
|
+
dimensions: parseInt(process.env.RECALL_EMBEDDING_DIMS ?? `${defaults.dimensions}`, 10),
|
|
1162
|
+
version: process.env.RECALL_EMBEDDING_VERSION ?? "v1",
|
|
1163
|
+
similarity_threshold: parseFloat(process.env.RECALL_SIMILARITY_THRESHOLD ?? "0.8")
|
|
1164
|
+
};
|
|
1165
|
+
}
|
|
1166
|
+
function getEmbeddingModelInfo(config = loadEmbeddingConfigFromEnv()) {
|
|
1167
|
+
if (!config) return null;
|
|
1168
|
+
const provider = resolveProvider(config);
|
|
1169
|
+
const metadata = provider.metadata();
|
|
1170
|
+
const cachePath = getEmbeddingCachePath({
|
|
1171
|
+
provider: config.provider,
|
|
1172
|
+
model: metadata.model
|
|
1173
|
+
});
|
|
1174
|
+
const sizeBytes = getDirectorySize(cachePath);
|
|
1175
|
+
return {
|
|
1176
|
+
provider: config.provider,
|
|
1177
|
+
model: metadata.model,
|
|
1178
|
+
dimensions: metadata.dimensions,
|
|
1179
|
+
canonical_dimensions: metadata.canonical_dimensions,
|
|
1180
|
+
index_dimensions: metadata.index_dimensions,
|
|
1181
|
+
version: metadata.version,
|
|
1182
|
+
task_prefix: metadata.task_prefix,
|
|
1183
|
+
estimated_size_mb: metadata.estimated_size_mb,
|
|
1184
|
+
cache_path: cachePath,
|
|
1185
|
+
cached: sizeBytes > 0,
|
|
1186
|
+
size_bytes: sizeBytes,
|
|
1187
|
+
size_label: formatBytes(sizeBytes)
|
|
1188
|
+
};
|
|
1189
|
+
}
|
|
1190
|
+
async function ensureEmbeddingProviderReady(config = loadEmbeddingConfigFromEnv()) {
|
|
1191
|
+
if (!config) return null;
|
|
1192
|
+
const provider = resolveProvider(config);
|
|
1193
|
+
if (provider.prepare) {
|
|
1194
|
+
await provider.prepare();
|
|
1195
|
+
} else {
|
|
1196
|
+
await provider.embed("recall provider warmup", "document");
|
|
1197
|
+
}
|
|
1198
|
+
return getEmbeddingModelInfo(config);
|
|
1199
|
+
}
|
|
1200
|
+
function getEmbeddingVersion(config) {
|
|
1201
|
+
return config.version || `${config.provider}:${config.model}:${config.dimensions}`;
|
|
1202
|
+
}
|
|
1203
|
+
function hashMemoryText(text) {
|
|
1204
|
+
return createHash("sha256").update(text).digest("hex");
|
|
1205
|
+
}
|
|
1206
|
+
function shouldEmbedMemory(row) {
|
|
1207
|
+
if (row.status === "transient") return false;
|
|
1208
|
+
if (row.status === "rejected") {
|
|
1209
|
+
return row.source === "user_correction" || row.source === "user_reported_review";
|
|
1210
|
+
}
|
|
1211
|
+
return row.confidence >= CONFIDENCE.TRANSIENT_MAX;
|
|
1212
|
+
}
|
|
1213
|
+
function serializeEmbedding(embedding) {
|
|
1214
|
+
return Buffer.from(
|
|
1215
|
+
embedding.buffer,
|
|
1216
|
+
embedding.byteOffset,
|
|
1217
|
+
embedding.byteLength
|
|
1218
|
+
);
|
|
1219
|
+
}
|
|
1220
|
+
function deserializeEmbedding(buffer) {
|
|
1221
|
+
return new Float32Array(
|
|
1222
|
+
buffer.buffer,
|
|
1223
|
+
buffer.byteOffset,
|
|
1224
|
+
buffer.byteLength / Float32Array.BYTES_PER_ELEMENT
|
|
1225
|
+
);
|
|
1226
|
+
}
|
|
1227
|
+
function rowNeedsEmbeddingRefresh(row, existing, config) {
|
|
1228
|
+
const metadata = resolveProvider(config).metadata();
|
|
1229
|
+
if (!existing) return true;
|
|
1230
|
+
return existing.model !== config.model || existing.embedding_dimensions !== metadata.canonical_dimensions || existing.index_dimensions !== metadata.index_dimensions || existing.version !== getEmbeddingVersion(config) || existing.content_hash !== hashMemoryText(row.text);
|
|
1231
|
+
}
|
|
1232
|
+
function projectEmbeddingToIndex(embedding, indexDimensions) {
|
|
1233
|
+
if (embedding.length === indexDimensions) {
|
|
1234
|
+
return embedding;
|
|
1235
|
+
}
|
|
1236
|
+
if (embedding.length < indexDimensions) {
|
|
1237
|
+
throw new Error(`Embedding width ${embedding.length} is smaller than index width ${indexDimensions}.`);
|
|
1238
|
+
}
|
|
1239
|
+
const sliced = embedding.slice(0, indexDimensions);
|
|
1240
|
+
let norm = 0;
|
|
1241
|
+
for (const value of sliced) norm += value * value;
|
|
1242
|
+
const scale = Math.sqrt(norm) || 1;
|
|
1243
|
+
for (let i = 0; i < sliced.length; i++) {
|
|
1244
|
+
sliced[i] /= scale;
|
|
1245
|
+
}
|
|
1246
|
+
return sliced;
|
|
1247
|
+
}
|
|
1248
|
+
async function generateEmbedding(text, config, purpose = "document") {
|
|
1249
|
+
return resolveProvider(config).embed(text, purpose);
|
|
1250
|
+
}
|
|
1251
|
+
async function generateEmbeddings(texts, config, purpose = "document") {
|
|
1252
|
+
if (texts.length === 0) return [];
|
|
1253
|
+
return resolveProvider(config).embedBatch(texts, purpose);
|
|
1254
|
+
}
|
|
1255
|
+
function storeEmbedding(db, memoryId, text, embedding, config) {
|
|
1256
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1257
|
+
const metadata = resolveProvider(config).metadata();
|
|
1258
|
+
const payload = {
|
|
1259
|
+
memory_id: memoryId,
|
|
1260
|
+
model: config.model,
|
|
1261
|
+
embedding_dimensions: metadata.canonical_dimensions,
|
|
1262
|
+
index_dimensions: metadata.index_dimensions,
|
|
1263
|
+
version: getEmbeddingVersion(config),
|
|
1264
|
+
content_hash: hashMemoryText(text),
|
|
1265
|
+
updated_at: now,
|
|
1266
|
+
embedding: serializeEmbedding(embedding)
|
|
1267
|
+
};
|
|
1268
|
+
db.insert(memoryEmbeddings).values(payload).onConflictDoUpdate({
|
|
1269
|
+
target: memoryEmbeddings.memory_id,
|
|
1270
|
+
set: {
|
|
1271
|
+
model: payload.model,
|
|
1272
|
+
embedding_dimensions: payload.embedding_dimensions,
|
|
1273
|
+
index_dimensions: payload.index_dimensions,
|
|
1274
|
+
version: payload.version,
|
|
1275
|
+
content_hash: payload.content_hash,
|
|
1276
|
+
updated_at: payload.updated_at,
|
|
1277
|
+
embedding: payload.embedding
|
|
1278
|
+
}
|
|
1279
|
+
}).run();
|
|
1280
|
+
}
|
|
1281
|
+
function removeStoredEmbedding(db, memoryId) {
|
|
1282
|
+
const result = db.delete(memoryEmbeddings).where(eq3(memoryEmbeddings.memory_id, memoryId)).run();
|
|
1283
|
+
return result.changes > 0;
|
|
1284
|
+
}
|
|
1285
|
+
async function syncMemoryEmbedding(db, memoryId, config) {
|
|
1286
|
+
const memory = db.select().from(memories).where(eq3(memories.id, memoryId)).get();
|
|
1287
|
+
if (!memory || !shouldEmbedMemory(memory)) {
|
|
1288
|
+
removeStoredEmbedding(db, memoryId);
|
|
1289
|
+
removeMemoryVecRow(db, memoryId, config);
|
|
1290
|
+
return "removed";
|
|
1291
|
+
}
|
|
1292
|
+
const existing = db.select().from(memoryEmbeddings).where(eq3(memoryEmbeddings.memory_id, memoryId)).get();
|
|
1293
|
+
if (!rowNeedsEmbeddingRefresh(memory, existing, config)) {
|
|
1294
|
+
if (existing) {
|
|
1295
|
+
upsertMemoryVecRow(db, memory, existing);
|
|
1296
|
+
}
|
|
1297
|
+
return "skipped";
|
|
1298
|
+
}
|
|
1299
|
+
const embedding = await generateEmbedding(memory.text, config, "document");
|
|
1300
|
+
const stillExists = db.select({ id: memories.id }).from(memories).where(eq3(memories.id, memoryId)).get();
|
|
1301
|
+
if (!stillExists) {
|
|
1302
|
+
removeStoredEmbedding(db, memoryId);
|
|
1303
|
+
removeMemoryVecRow(db, memoryId, config);
|
|
1304
|
+
return "removed";
|
|
1305
|
+
}
|
|
1306
|
+
storeEmbedding(db, memory.id, memory.text, embedding, config);
|
|
1307
|
+
const refreshed = db.select().from(memoryEmbeddings).where(eq3(memoryEmbeddings.memory_id, memory.id)).get();
|
|
1308
|
+
if (!refreshed) {
|
|
1309
|
+
throw new Error(`Failed to reload embedding row for ${memory.id}`);
|
|
1310
|
+
}
|
|
1311
|
+
upsertMemoryVecRow(db, memory, refreshed);
|
|
1312
|
+
return existing ? "updated" : "stored";
|
|
1313
|
+
}
|
|
1314
|
+
function queueMemoryEmbeddingSync(db, memoryId, config = loadEmbeddingConfigFromEnv()) {
|
|
1315
|
+
syncMemoryFtsIndex(db, memoryId);
|
|
1316
|
+
if (!config) return null;
|
|
1317
|
+
const job = syncMemoryEmbedding(db, memoryId, config).then(() => void 0).catch((error) => {
|
|
1318
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1319
|
+
console.error(`[recall] embedding sync failed for ${memoryId.slice(0, 8)}: ${message}`);
|
|
1320
|
+
}).finally(() => {
|
|
1321
|
+
pendingEmbeddingJobs.delete(job);
|
|
1322
|
+
});
|
|
1323
|
+
pendingEmbeddingJobs.add(job);
|
|
1324
|
+
return job;
|
|
1325
|
+
}
|
|
1326
|
+
async function bootstrapEmbeddings(db, config, options = {}) {
|
|
1327
|
+
const rows = db.select().from(memories).all();
|
|
1328
|
+
const existingRows = db.select().from(memoryEmbeddings).all();
|
|
1329
|
+
const existingById = new Map(existingRows.map((row) => [row.memory_id, row]));
|
|
1330
|
+
const eligible = rows.filter((row) => {
|
|
1331
|
+
if (options.repo && row.repo !== options.repo) return false;
|
|
1332
|
+
return shouldEmbedMemory(row);
|
|
1333
|
+
});
|
|
1334
|
+
const pending = eligible.filter((row) => rowNeedsEmbeddingRefresh(row, existingById.get(row.id), config));
|
|
1335
|
+
for (const row of rows) {
|
|
1336
|
+
if (options.repo && row.repo !== options.repo) continue;
|
|
1337
|
+
if (!shouldEmbedMemory(row)) {
|
|
1338
|
+
removeStoredEmbedding(db, row.id);
|
|
1339
|
+
removeMemoryVecRow(db, row.id, config);
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
let total = 0;
|
|
1343
|
+
for (let i = 0; i < pending.length; i += EMBEDDING_BATCH_SIZE) {
|
|
1344
|
+
const batch = pending.slice(i, i + EMBEDDING_BATCH_SIZE);
|
|
1345
|
+
const embeddings = await generateEmbeddings(
|
|
1346
|
+
batch.map((row) => row.text),
|
|
1347
|
+
config,
|
|
1348
|
+
"document"
|
|
1349
|
+
);
|
|
1350
|
+
for (let j = 0; j < batch.length; j++) {
|
|
1351
|
+
storeEmbedding(db, batch[j].id, batch[j].text, embeddings[j], config);
|
|
1352
|
+
total++;
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
rebuildMemoryFtsIndex(db, options);
|
|
1356
|
+
rebuildMemoryVecIndex(db, config, options);
|
|
1357
|
+
return total;
|
|
1358
|
+
}
|
|
1359
|
+
function verifyEmbeddings(db, config, options = {}) {
|
|
1360
|
+
const rows = db.select().from(memories).all();
|
|
1361
|
+
const embeddingRows = db.select().from(memoryEmbeddings).all();
|
|
1362
|
+
const embeddingById = new Map(embeddingRows.map((row) => [row.memory_id, row]));
|
|
1363
|
+
let eligible = 0;
|
|
1364
|
+
let stale = 0;
|
|
1365
|
+
for (const row of rows) {
|
|
1366
|
+
if (options.repo && row.repo !== options.repo) continue;
|
|
1367
|
+
if (!shouldEmbedMemory(row)) continue;
|
|
1368
|
+
eligible++;
|
|
1369
|
+
if (rowNeedsEmbeddingRefresh(row, embeddingById.get(row.id), config)) {
|
|
1370
|
+
stale++;
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
const vec = verifyMemoryVecIndex(db, options);
|
|
1374
|
+
const fts = verifyMemoryFtsIndex(db, options);
|
|
1375
|
+
return {
|
|
1376
|
+
eligible,
|
|
1377
|
+
stored: embeddingRows.filter((row) => {
|
|
1378
|
+
if (!options.repo) return true;
|
|
1379
|
+
const memory = rows.find((item) => item.id === row.memory_id);
|
|
1380
|
+
return memory?.repo === options.repo;
|
|
1381
|
+
}).length,
|
|
1382
|
+
stale,
|
|
1383
|
+
indexed: vec.indexed,
|
|
1384
|
+
index_drift: vec.drift,
|
|
1385
|
+
lexical_indexed: fts.indexed,
|
|
1386
|
+
lexical_drift: fts.drift
|
|
1387
|
+
};
|
|
1388
|
+
}
|
|
1389
|
+
function rebuildEmbeddingIndex(db, config, options = {}) {
|
|
1390
|
+
const lexicalRows = rebuildMemoryFtsIndex(db, options);
|
|
1391
|
+
const vectorRows = config ? rebuildMemoryVecIndex(db, config, options) : 0;
|
|
1392
|
+
return {
|
|
1393
|
+
vector_rows: vectorRows,
|
|
1394
|
+
lexical_rows: lexicalRows
|
|
1395
|
+
};
|
|
1396
|
+
}
|
|
1397
|
+
function lexicalRankToScore(rank, position) {
|
|
1398
|
+
const safeRank = Number.isFinite(rank) ? Math.abs(rank) : position + 1;
|
|
1399
|
+
return 1 / (1 + safeRank + position);
|
|
1400
|
+
}
|
|
1401
|
+
async function hybridSearch(db, query, config, options = {}) {
|
|
1402
|
+
const limit = options.limit ?? 10;
|
|
1403
|
+
const minSimilarity = config ? Math.max(config.similarity_threshold, MIN_HYBRID_VECTOR_SIMILARITY) : null;
|
|
1404
|
+
const lexicalMatches = searchMemoryFtsIndex(db, query, {
|
|
1405
|
+
repo: options.repo,
|
|
1406
|
+
limit: Math.max(limit * 2, 20)
|
|
1407
|
+
});
|
|
1408
|
+
const semanticMatches = config ? searchMemoryVecIndex(
|
|
1409
|
+
db,
|
|
1410
|
+
projectEmbeddingToIndex(
|
|
1411
|
+
await generateEmbedding(query, config, "query"),
|
|
1412
|
+
resolveProvider(config).metadata().index_dimensions
|
|
1413
|
+
),
|
|
1414
|
+
{ repo: options.repo, limit: Math.max(limit * 2, 20) }
|
|
1415
|
+
) : [];
|
|
1416
|
+
const rowsById = new Map(
|
|
1417
|
+
db.select().from(memories).all().map((row) => [row.id, row])
|
|
1418
|
+
);
|
|
1419
|
+
const merged = /* @__PURE__ */ new Map();
|
|
1420
|
+
for (let i = 0; i < lexicalMatches.length; i++) {
|
|
1421
|
+
const match = lexicalMatches[i];
|
|
1422
|
+
const row = rowsById.get(match.memory_id);
|
|
1423
|
+
if (!row || !shouldEmbedMemory(row)) continue;
|
|
1424
|
+
const lexicalScore = lexicalRankToScore(match.lexical_rank, i);
|
|
1425
|
+
merged.set(match.memory_id, {
|
|
1426
|
+
memory: rowToMemory(row),
|
|
1427
|
+
similarity: 0,
|
|
1428
|
+
lexical_score: lexicalScore,
|
|
1429
|
+
score: lexicalScore * 0.35
|
|
1430
|
+
});
|
|
1431
|
+
}
|
|
1432
|
+
for (const match of semanticMatches) {
|
|
1433
|
+
const row = rowsById.get(match.memory_id);
|
|
1434
|
+
if (!row || !shouldEmbedMemory(row)) continue;
|
|
1435
|
+
const similarity = Math.max(0, 1 - match.distance);
|
|
1436
|
+
if (minSimilarity !== null && similarity < minSimilarity) continue;
|
|
1437
|
+
const existing = merged.get(match.memory_id);
|
|
1438
|
+
if (existing) {
|
|
1439
|
+
existing.similarity = similarity;
|
|
1440
|
+
existing.score = similarity * 0.65 + existing.lexical_score * 0.35;
|
|
1441
|
+
} else {
|
|
1442
|
+
merged.set(match.memory_id, {
|
|
1443
|
+
memory: rowToMemory(row),
|
|
1444
|
+
similarity,
|
|
1445
|
+
lexical_score: 0,
|
|
1446
|
+
score: similarity * 0.65
|
|
1447
|
+
});
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
return [...merged.values()].sort((a, b) => b.score - a.score).slice(0, limit);
|
|
1451
|
+
}
|
|
1452
|
+
function cosineSimilarity(a, b) {
|
|
1453
|
+
if (a.length !== b.length) return 0;
|
|
1454
|
+
let dot = 0;
|
|
1455
|
+
let normA = 0;
|
|
1456
|
+
let normB = 0;
|
|
1457
|
+
for (let i = 0; i < a.length; i++) {
|
|
1458
|
+
dot += a[i] * b[i];
|
|
1459
|
+
normA += a[i] * a[i];
|
|
1460
|
+
normB += b[i] * b[i];
|
|
1461
|
+
}
|
|
1462
|
+
const denom = Math.sqrt(normA) * Math.sqrt(normB);
|
|
1463
|
+
return denom === 0 ? 0 : dot / denom;
|
|
1464
|
+
}
|
|
1465
|
+
async function findSemanticDuplicates(db, text, config, threshold, options = {}) {
|
|
1466
|
+
const queryEmbedding = await generateEmbedding(text, config, "query");
|
|
1467
|
+
const dupThreshold = threshold ?? config.similarity_threshold;
|
|
1468
|
+
const rows = db.select().from(memories).all();
|
|
1469
|
+
const embeddingsById = new Map(
|
|
1470
|
+
db.select().from(memoryEmbeddings).all().map((row) => [row.memory_id, row])
|
|
1471
|
+
);
|
|
1472
|
+
const duplicates = [];
|
|
1473
|
+
for (const row of rows) {
|
|
1474
|
+
if (options.repo && row.repo !== options.repo) continue;
|
|
1475
|
+
if (options.type && row.type !== options.type) continue;
|
|
1476
|
+
if (row.status === "rejected") continue;
|
|
1477
|
+
if (!shouldEmbedMemory(row)) continue;
|
|
1478
|
+
const embeddingRow = embeddingsById.get(row.id);
|
|
1479
|
+
if (!embeddingRow?.embedding) continue;
|
|
1480
|
+
const similarity = cosineSimilarity(
|
|
1481
|
+
queryEmbedding,
|
|
1482
|
+
deserializeEmbedding(embeddingRow.embedding)
|
|
1483
|
+
);
|
|
1484
|
+
if (similarity >= dupThreshold) {
|
|
1485
|
+
duplicates.push({ id: row.id, text: row.text, similarity });
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
return duplicates.sort((a, b) => b.similarity - a.similarity).slice(0, options.limit ?? 10);
|
|
1489
|
+
}
|
|
1490
|
+
async function findSimilarRejectedExemplar(db, text, config, threshold) {
|
|
1491
|
+
const queryEmbedding = await generateEmbedding(text, config, "query");
|
|
1492
|
+
const rejectedRows = db.select().from(memories).where(eq3(memories.status, "rejected")).all().filter((row) => row.source === "user_correction" || row.source === "user_reported_review");
|
|
1493
|
+
if (rejectedRows.length === 0) return null;
|
|
1494
|
+
const embeddingsById = new Map(
|
|
1495
|
+
db.select().from(memoryEmbeddings).all().map((row) => [row.memory_id, row])
|
|
1496
|
+
);
|
|
1497
|
+
let best = null;
|
|
1498
|
+
for (const row of rejectedRows) {
|
|
1499
|
+
const embeddingRow = embeddingsById.get(row.id);
|
|
1500
|
+
if (!embeddingRow?.embedding) continue;
|
|
1501
|
+
const similarity = cosineSimilarity(
|
|
1502
|
+
queryEmbedding,
|
|
1503
|
+
deserializeEmbedding(embeddingRow.embedding)
|
|
1504
|
+
);
|
|
1505
|
+
if (similarity >= threshold && (!best || similarity > best.similarity)) {
|
|
1506
|
+
best = { id: row.id, text: row.text, similarity };
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
return best;
|
|
1510
|
+
}
|
|
1511
|
+
function rowToMemory(row) {
|
|
1512
|
+
const evidence = typeof row.evidence === "string" ? JSON.parse(row.evidence) : Array.isArray(row.evidence) ? row.evidence : [];
|
|
1513
|
+
const captureContext = typeof row.capture_context === "string" ? JSON.parse(row.capture_context) : row.capture_context ?? null;
|
|
1514
|
+
return {
|
|
1515
|
+
id: row.id,
|
|
1516
|
+
type: row.type,
|
|
1517
|
+
text: row.text,
|
|
1518
|
+
scope: row.scope,
|
|
1519
|
+
path_scope: row.path_scope,
|
|
1520
|
+
repo: row.repo,
|
|
1521
|
+
status: row.status,
|
|
1522
|
+
confidence: row.confidence,
|
|
1523
|
+
source: row.source,
|
|
1524
|
+
evidence,
|
|
1525
|
+
capture_context: captureContext,
|
|
1526
|
+
supersedes: row.supersedes,
|
|
1527
|
+
created_at: row.created_at,
|
|
1528
|
+
updated_at: row.updated_at,
|
|
1529
|
+
last_validated_at: row.last_validated_at,
|
|
1530
|
+
last_injected_at: row.last_injected_at,
|
|
1531
|
+
injection_count: row.injection_count,
|
|
1532
|
+
override_count: row.override_count,
|
|
1533
|
+
repetition_count: row.repetition_count,
|
|
1534
|
+
auto_inject: row.auto_inject
|
|
1535
|
+
};
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
// src/maintenance/appliers.ts
|
|
1539
|
+
import { eq as eq6 } from "drizzle-orm";
|
|
1540
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
1541
|
+
|
|
1542
|
+
// src/models/memory.ts
|
|
1543
|
+
import { eq as eq4, and, gte, inArray, like, sql } from "drizzle-orm";
|
|
1544
|
+
import { randomUUID } from "crypto";
|
|
1545
|
+
|
|
1546
|
+
// src/models/dedupe.ts
|
|
1547
|
+
function normalizeDedupeText(text) {
|
|
1548
|
+
return text.toLowerCase().replace(/\s+/g, " ").replace(/[\s.;:,!?`]+$/g, "").trim();
|
|
1549
|
+
}
|
|
1550
|
+
function memoryDedupeKey(input) {
|
|
1551
|
+
return [
|
|
1552
|
+
"memory",
|
|
1553
|
+
input.type,
|
|
1554
|
+
input.scope,
|
|
1555
|
+
input.repo ?? "",
|
|
1556
|
+
input.path_scope ?? "",
|
|
1557
|
+
normalizeDedupeText(input.text)
|
|
1558
|
+
].join("");
|
|
1559
|
+
}
|
|
1560
|
+
function historySnippetDedupeKey(input) {
|
|
1561
|
+
return [
|
|
1562
|
+
"history",
|
|
1563
|
+
input.repo ?? "",
|
|
1564
|
+
input.session_id ?? "",
|
|
1565
|
+
input.kind,
|
|
1566
|
+
normalizeDedupeText(input.text)
|
|
1567
|
+
].join("");
|
|
1568
|
+
}
|
|
1569
|
+
function activityEventDedupeKey(input) {
|
|
1570
|
+
if (!input.session_id) return null;
|
|
1571
|
+
return [
|
|
1572
|
+
"activity",
|
|
1573
|
+
input.session_id,
|
|
1574
|
+
input.repo ?? "",
|
|
1575
|
+
input.path ?? "",
|
|
1576
|
+
input.source,
|
|
1577
|
+
input.event_type,
|
|
1578
|
+
stableDedupeJson(stripVolatileFields(input.request ?? {})),
|
|
1579
|
+
stableDedupeJson(stripVolatileFields(input.result ?? {}))
|
|
1580
|
+
].join("");
|
|
1581
|
+
}
|
|
1582
|
+
function hookCallDedupeKey(input) {
|
|
1583
|
+
if (!input.session_id) return null;
|
|
1584
|
+
return [
|
|
1585
|
+
"hook",
|
|
1586
|
+
input.session_id,
|
|
1587
|
+
input.agent,
|
|
1588
|
+
input.event,
|
|
1589
|
+
input.ok ? "ok" : "error",
|
|
1590
|
+
stableDedupeJson(stripVolatileFields(input.payload ?? {}))
|
|
1591
|
+
].join("");
|
|
1592
|
+
}
|
|
1593
|
+
function stripVolatileFields(value) {
|
|
1594
|
+
if (Array.isArray(value)) {
|
|
1595
|
+
return value.map(stripVolatileFields);
|
|
1596
|
+
}
|
|
1597
|
+
if (!value || typeof value !== "object") {
|
|
1598
|
+
return value;
|
|
1599
|
+
}
|
|
1600
|
+
const out = {};
|
|
1601
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
1602
|
+
if (/_at$/u.test(key) || key === "timestamp") continue;
|
|
1603
|
+
if (entry === void 0) continue;
|
|
1604
|
+
out[key] = stripVolatileFields(entry);
|
|
1605
|
+
}
|
|
1606
|
+
return out;
|
|
1607
|
+
}
|
|
1608
|
+
function stableDedupeJson(value) {
|
|
1609
|
+
if (Array.isArray(value)) {
|
|
1610
|
+
return `[${value.map(stableDedupeJson).join(",")}]`;
|
|
1611
|
+
}
|
|
1612
|
+
if (!value || typeof value !== "object") {
|
|
1613
|
+
return JSON.stringify(value);
|
|
1614
|
+
}
|
|
1615
|
+
const record = value;
|
|
1616
|
+
return `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${stableDedupeJson(record[key])}`).join(",")}}`;
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
// src/models/memory.ts
|
|
1620
|
+
function statusFromConfidence(confidence) {
|
|
1621
|
+
if (confidence < CONFIDENCE.TRANSIENT_MAX) return "transient";
|
|
1622
|
+
if (confidence < CONFIDENCE.CANDIDATE_MAX) return "candidate";
|
|
1623
|
+
return "active";
|
|
1624
|
+
}
|
|
1625
|
+
function createMemory(db, input) {
|
|
1626
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1627
|
+
const id = randomUUID();
|
|
1628
|
+
const confidence = input.confidence ?? 0.35;
|
|
1629
|
+
const status = statusFromConfidence(confidence);
|
|
1630
|
+
const dedupeKey = input.dedupe === false ? null : memoryDedupeKey({
|
|
1631
|
+
type: input.type,
|
|
1632
|
+
scope: input.scope,
|
|
1633
|
+
repo: input.repo ?? null,
|
|
1634
|
+
path_scope: input.path_scope ?? null,
|
|
1635
|
+
text: input.text
|
|
1636
|
+
});
|
|
1637
|
+
if (dedupeKey) {
|
|
1638
|
+
const existing = db.select().from(memories).where(and(eq4(memories.dedupe_key, dedupeKey), sql`${memories.status} != 'rejected'`)).get();
|
|
1639
|
+
if (existing) return existing.id;
|
|
1640
|
+
}
|
|
1641
|
+
db.insert(memories).values({
|
|
1642
|
+
id,
|
|
1643
|
+
type: input.type,
|
|
1644
|
+
text: input.text,
|
|
1645
|
+
scope: input.scope,
|
|
1646
|
+
path_scope: input.path_scope ?? null,
|
|
1647
|
+
repo: input.repo ?? null,
|
|
1648
|
+
status,
|
|
1649
|
+
confidence,
|
|
1650
|
+
source: input.source,
|
|
1651
|
+
evidence: input.evidence ?? [],
|
|
1652
|
+
capture_context: input.capture_context ? input.capture_context : null,
|
|
1653
|
+
supersedes: input.supersedes ?? null,
|
|
1654
|
+
dedupe_key: dedupeKey,
|
|
1655
|
+
created_at: now,
|
|
1656
|
+
updated_at: now,
|
|
1657
|
+
last_validated_at: null,
|
|
1658
|
+
last_injected_at: null,
|
|
1659
|
+
injection_count: 0,
|
|
1660
|
+
override_count: 0,
|
|
1661
|
+
repetition_count: 0
|
|
1662
|
+
}).run();
|
|
1663
|
+
queueMemoryEmbeddingSync(db, id);
|
|
1664
|
+
return id;
|
|
1665
|
+
}
|
|
1666
|
+
function getMemory(db, id) {
|
|
1667
|
+
const row = db.select().from(memories).where(eq4(memories.id, id)).get();
|
|
1668
|
+
if (!row) return void 0;
|
|
1669
|
+
return rowToMemory2(row);
|
|
1670
|
+
}
|
|
1671
|
+
function queryMemories(db, query) {
|
|
1672
|
+
const conditions = [];
|
|
1673
|
+
if (query.repo) conditions.push(eq4(memories.repo, query.repo));
|
|
1674
|
+
if (query.status) conditions.push(eq4(memories.status, query.status));
|
|
1675
|
+
if (query.type) conditions.push(eq4(memories.type, query.type));
|
|
1676
|
+
if (query.scope) conditions.push(eq4(memories.scope, query.scope));
|
|
1677
|
+
if (query.min_confidence != null)
|
|
1678
|
+
conditions.push(gte(memories.confidence, query.min_confidence));
|
|
1679
|
+
if (query.path) conditions.push(like(memories.path_scope, `%${query.path}%`));
|
|
1680
|
+
if (query.auto_inject != null) conditions.push(eq4(memories.auto_inject, query.auto_inject));
|
|
1681
|
+
let statement = db.select().from(memories).$dynamic();
|
|
1682
|
+
if (conditions.length > 0) {
|
|
1683
|
+
statement = statement.where(and(...conditions));
|
|
1684
|
+
}
|
|
1685
|
+
if (query.offset != null) {
|
|
1686
|
+
statement = statement.offset(query.offset);
|
|
1687
|
+
}
|
|
1688
|
+
if (query.limit != null) {
|
|
1689
|
+
statement = statement.limit(query.limit);
|
|
1690
|
+
}
|
|
1691
|
+
const rows = statement.all();
|
|
1692
|
+
return rows.map(rowToMemory2);
|
|
1693
|
+
}
|
|
1694
|
+
function listMemories(db, repo, options = {}) {
|
|
1695
|
+
return queryMemories(db, {
|
|
1696
|
+
repo,
|
|
1697
|
+
limit: options.limit,
|
|
1698
|
+
offset: options.offset
|
|
1699
|
+
});
|
|
1700
|
+
}
|
|
1701
|
+
function listRepos(db) {
|
|
1702
|
+
return [...new Set(
|
|
1703
|
+
db.select({ repo: memories.repo }).from(memories).all().map((row) => row.repo).filter((repo) => Boolean(repo))
|
|
1704
|
+
)].sort();
|
|
1705
|
+
}
|
|
1706
|
+
function promoteMemory(db, id, reason, evidence) {
|
|
1707
|
+
const mem = getMemory(db, id);
|
|
1708
|
+
if (!mem) return false;
|
|
1709
|
+
if (mem.status === "rejected") return false;
|
|
1710
|
+
let newConfidence;
|
|
1711
|
+
if (reason === "explicit_confirm") {
|
|
1712
|
+
newConfidence = Math.max(mem.confidence, PROMOTION.EXPLICIT_CONFIRM);
|
|
1713
|
+
} else if (reason === "repeat_correction") {
|
|
1714
|
+
newConfidence = Math.min(1, mem.confidence + PROMOTION.REPEAT_CORRECTION);
|
|
1715
|
+
} else if (reason === "review_feedback") {
|
|
1716
|
+
newConfidence = Math.min(1, mem.confidence + PROMOTION.REVIEW_FEEDBACK);
|
|
1717
|
+
} else {
|
|
1718
|
+
newConfidence = Math.min(1, mem.confidence + PROMOTION.PASSIVE_GAIN);
|
|
1719
|
+
}
|
|
1720
|
+
const newStatus = statusFromConfidence(newConfidence);
|
|
1721
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1722
|
+
const newEvidence = evidence ? [...mem.evidence, evidence] : mem.evidence;
|
|
1723
|
+
db.update(memories).set({
|
|
1724
|
+
confidence: newConfidence,
|
|
1725
|
+
status: newStatus,
|
|
1726
|
+
evidence: newEvidence,
|
|
1727
|
+
updated_at: now,
|
|
1728
|
+
last_validated_at: now
|
|
1729
|
+
}).where(eq4(memories.id, id)).run();
|
|
1730
|
+
queueMemoryEmbeddingSync(db, id);
|
|
1731
|
+
return true;
|
|
1732
|
+
}
|
|
1733
|
+
function demoteMemory(db, id, reason) {
|
|
1734
|
+
const mem = getMemory(db, id);
|
|
1735
|
+
if (!mem) return false;
|
|
1736
|
+
const newConfidence = Math.max(0, mem.confidence - 0.3);
|
|
1737
|
+
const newStatus = statusFromConfidence(newConfidence);
|
|
1738
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1739
|
+
db.update(memories).set({
|
|
1740
|
+
confidence: newConfidence,
|
|
1741
|
+
status: newStatus === "transient" ? "candidate" : newStatus,
|
|
1742
|
+
// don't lose it completely
|
|
1743
|
+
updated_at: now
|
|
1744
|
+
}).where(eq4(memories.id, id)).run();
|
|
1745
|
+
queueMemoryEmbeddingSync(db, id);
|
|
1746
|
+
return true;
|
|
1747
|
+
}
|
|
1748
|
+
function demoteGlobalMemory(db, id, opts = {}) {
|
|
1749
|
+
const mem = getMemory(db, id);
|
|
1750
|
+
if (!mem) return { ok: false, reason: "not_found" };
|
|
1751
|
+
if (mem.scope !== "global") return { ok: false, reason: "not_global" };
|
|
1752
|
+
const targetRepo = opts.repo?.trim() || null;
|
|
1753
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1754
|
+
if (targetRepo) {
|
|
1755
|
+
const newDedupe = memoryDedupeKey({
|
|
1756
|
+
type: mem.type,
|
|
1757
|
+
scope: "repo",
|
|
1758
|
+
repo: targetRepo,
|
|
1759
|
+
path_scope: mem.path_scope,
|
|
1760
|
+
text: mem.text
|
|
1761
|
+
});
|
|
1762
|
+
db.update(memories).set({
|
|
1763
|
+
scope: "repo",
|
|
1764
|
+
repo: targetRepo,
|
|
1765
|
+
dedupe_key: newDedupe,
|
|
1766
|
+
updated_at: now
|
|
1767
|
+
}).where(eq4(memories.id, id)).run();
|
|
1768
|
+
} else {
|
|
1769
|
+
db.update(memories).set({
|
|
1770
|
+
status: "rejected",
|
|
1771
|
+
confidence: 0,
|
|
1772
|
+
dedupe_key: null,
|
|
1773
|
+
updated_at: now
|
|
1774
|
+
}).where(eq4(memories.id, id)).run();
|
|
1775
|
+
}
|
|
1776
|
+
queueMemoryEmbeddingSync(db, id);
|
|
1777
|
+
const updated = getMemory(db, id);
|
|
1778
|
+
return { ok: true, outcome: targetRepo ? "rescoped" : "rejected", memory: updated };
|
|
1779
|
+
}
|
|
1780
|
+
function rejectMemory(db, id) {
|
|
1781
|
+
const mem = getMemory(db, id);
|
|
1782
|
+
if (!mem) return false;
|
|
1783
|
+
db.update(memories).set({
|
|
1784
|
+
status: "rejected",
|
|
1785
|
+
confidence: 0,
|
|
1786
|
+
dedupe_key: null,
|
|
1787
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
1788
|
+
}).where(eq4(memories.id, id)).run();
|
|
1789
|
+
queueMemoryEmbeddingSync(db, id);
|
|
1790
|
+
return true;
|
|
1791
|
+
}
|
|
1792
|
+
function confirmMemory(db, id) {
|
|
1793
|
+
return promoteMemory(db, id, "explicit_confirm", {
|
|
1794
|
+
type: "session_correction",
|
|
1795
|
+
session: "cli",
|
|
1796
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1797
|
+
context: "user explicitly confirmed"
|
|
1798
|
+
});
|
|
1799
|
+
}
|
|
1800
|
+
function appendEvidence(db, id, evidence) {
|
|
1801
|
+
const mem = getMemory(db, id);
|
|
1802
|
+
if (!mem) return false;
|
|
1803
|
+
if (hasEquivalentEvidence(mem.evidence, evidence)) return true;
|
|
1804
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1805
|
+
db.update(memories).set({
|
|
1806
|
+
evidence: [...mem.evidence, evidence],
|
|
1807
|
+
updated_at: now,
|
|
1808
|
+
last_validated_at: now
|
|
1809
|
+
}).where(eq4(memories.id, id)).run();
|
|
1810
|
+
return true;
|
|
1811
|
+
}
|
|
1812
|
+
function updateMemoryCaptureContext(db, id, captureContext) {
|
|
1813
|
+
const mem = getMemory(db, id);
|
|
1814
|
+
if (!mem) return false;
|
|
1815
|
+
db.update(memories).set({
|
|
1816
|
+
capture_context: captureContext,
|
|
1817
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
1818
|
+
}).where(eq4(memories.id, id)).run();
|
|
1819
|
+
return true;
|
|
1820
|
+
}
|
|
1821
|
+
function incrementMemoryRepetition(db, id) {
|
|
1822
|
+
const mem = getMemory(db, id);
|
|
1823
|
+
if (!mem) return false;
|
|
1824
|
+
db.update(memories).set({
|
|
1825
|
+
repetition_count: sql`repetition_count + 1`,
|
|
1826
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
1827
|
+
}).where(eq4(memories.id, id)).run();
|
|
1828
|
+
return true;
|
|
1829
|
+
}
|
|
1830
|
+
function countDistinctCorrectionSessions(mem) {
|
|
1831
|
+
const sessions = /* @__PURE__ */ new Set();
|
|
1832
|
+
for (const entry of mem.evidence) {
|
|
1833
|
+
if (entry.type === "session_correction") {
|
|
1834
|
+
sessions.add(entry.session);
|
|
1835
|
+
} else if (entry.type === "repeated_correction") {
|
|
1836
|
+
for (const session of entry.sessions) {
|
|
1837
|
+
sessions.add(session);
|
|
1838
|
+
}
|
|
1839
|
+
}
|
|
1840
|
+
}
|
|
1841
|
+
return sessions.size;
|
|
1842
|
+
}
|
|
1843
|
+
function recordFeedback(db, memoryId, sessionId, injected, outcome) {
|
|
1844
|
+
const id = randomUUID();
|
|
1845
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1846
|
+
db.insert(feedbackEvents).values({
|
|
1847
|
+
id,
|
|
1848
|
+
memory_id: memoryId,
|
|
1849
|
+
session_id: sessionId,
|
|
1850
|
+
injected,
|
|
1851
|
+
outcome,
|
|
1852
|
+
timestamp: now
|
|
1853
|
+
}).run();
|
|
1854
|
+
const mem = getMemory(db, memoryId);
|
|
1855
|
+
if (!mem) return id;
|
|
1856
|
+
const delta = outcome === "followed" ? 0.05 : outcome === "overridden" ? -0.15 : outcome === "ignored" ? -0.02 : -0.25;
|
|
1857
|
+
const nextConfidence = Math.max(0, Math.min(1, mem.confidence + delta));
|
|
1858
|
+
const nextStatus = statusFromConfidence(nextConfidence);
|
|
1859
|
+
if (injected) {
|
|
1860
|
+
db.update(memories).set({
|
|
1861
|
+
confidence: nextConfidence,
|
|
1862
|
+
status: nextStatus === "transient" ? "candidate" : nextStatus,
|
|
1863
|
+
updated_at: now,
|
|
1864
|
+
last_injected_at: now,
|
|
1865
|
+
injection_count: sql`injection_count + 1`,
|
|
1866
|
+
...outcome === "overridden" || outcome === "contradicted" ? { override_count: sql`override_count + 1` } : {}
|
|
1867
|
+
}).where(eq4(memories.id, memoryId)).run();
|
|
1868
|
+
} else {
|
|
1869
|
+
db.update(memories).set({
|
|
1870
|
+
confidence: nextConfidence,
|
|
1871
|
+
status: nextStatus === "transient" ? "candidate" : nextStatus,
|
|
1872
|
+
updated_at: now
|
|
1873
|
+
}).where(eq4(memories.id, memoryId)).run();
|
|
1874
|
+
}
|
|
1875
|
+
return id;
|
|
1876
|
+
}
|
|
1877
|
+
function getMemoryFeedback(db, memoryId) {
|
|
1878
|
+
return db.select().from(feedbackEvents).where(eq4(feedbackEvents.memory_id, memoryId)).all();
|
|
1879
|
+
}
|
|
1880
|
+
function getMemoryFeedbackSummaries(db, memoryIds) {
|
|
1881
|
+
const empty = () => ({
|
|
1882
|
+
followed: 0,
|
|
1883
|
+
overridden: 0,
|
|
1884
|
+
contradicted: 0,
|
|
1885
|
+
ignored: 0,
|
|
1886
|
+
resolved: 0
|
|
1887
|
+
});
|
|
1888
|
+
const result = /* @__PURE__ */ new Map();
|
|
1889
|
+
if (memoryIds.length === 0) return result;
|
|
1890
|
+
for (const id of memoryIds) result.set(id, empty());
|
|
1891
|
+
const rows = db.select({
|
|
1892
|
+
memory_id: feedbackEvents.memory_id,
|
|
1893
|
+
outcome: feedbackEvents.outcome,
|
|
1894
|
+
count: sql`count(*)`.as("count")
|
|
1895
|
+
}).from(feedbackEvents).where(inArray(feedbackEvents.memory_id, [...memoryIds])).groupBy(feedbackEvents.memory_id, feedbackEvents.outcome).all();
|
|
1896
|
+
for (const row of rows) {
|
|
1897
|
+
const entry = result.get(row.memory_id) ?? empty();
|
|
1898
|
+
if (row.outcome === "followed") entry.followed += row.count;
|
|
1899
|
+
else if (row.outcome === "overridden") entry.overridden += row.count;
|
|
1900
|
+
else if (row.outcome === "contradicted") entry.contradicted += row.count;
|
|
1901
|
+
else if (row.outcome === "ignored") entry.ignored += row.count;
|
|
1902
|
+
if (row.outcome !== "ignored") entry.resolved += row.count;
|
|
1903
|
+
result.set(row.memory_id, entry);
|
|
1904
|
+
}
|
|
1905
|
+
return result;
|
|
1906
|
+
}
|
|
1907
|
+
var FEEDBACK_MATURITY = 5;
|
|
1908
|
+
function feedbackWeightedScore(confidence, summary) {
|
|
1909
|
+
const total = summary.resolved;
|
|
1910
|
+
const maturity = Math.min(total, FEEDBACK_MATURITY) / FEEDBACK_MATURITY;
|
|
1911
|
+
if (maturity === 0) return confidence;
|
|
1912
|
+
const positive = summary.followed;
|
|
1913
|
+
const negative = summary.overridden + 2 * summary.contradicted;
|
|
1914
|
+
const numerator = Math.max(0, positive - negative + 1);
|
|
1915
|
+
const denominator = Math.max(1, total + 2);
|
|
1916
|
+
const empirical = Math.min(1, numerator / denominator);
|
|
1917
|
+
return (1 - maturity) * confidence + maturity * empirical;
|
|
1918
|
+
}
|
|
1919
|
+
function rowToMemory2(row) {
|
|
1920
|
+
const evidence = typeof row.evidence === "string" ? JSON.parse(row.evidence) : Array.isArray(row.evidence) ? row.evidence : [];
|
|
1921
|
+
const captureContext = typeof row.capture_context === "string" ? JSON.parse(row.capture_context) : row.capture_context ?? null;
|
|
1922
|
+
return {
|
|
1923
|
+
id: row.id,
|
|
1924
|
+
type: row.type,
|
|
1925
|
+
text: row.text,
|
|
1926
|
+
scope: row.scope,
|
|
1927
|
+
path_scope: row.path_scope,
|
|
1928
|
+
repo: row.repo,
|
|
1929
|
+
status: row.status,
|
|
1930
|
+
confidence: row.confidence,
|
|
1931
|
+
source: row.source,
|
|
1932
|
+
evidence,
|
|
1933
|
+
capture_context: captureContext,
|
|
1934
|
+
supersedes: row.supersedes,
|
|
1935
|
+
created_at: row.created_at,
|
|
1936
|
+
updated_at: row.updated_at,
|
|
1937
|
+
last_validated_at: row.last_validated_at,
|
|
1938
|
+
last_injected_at: row.last_injected_at,
|
|
1939
|
+
injection_count: row.injection_count,
|
|
1940
|
+
override_count: row.override_count,
|
|
1941
|
+
repetition_count: row.repetition_count,
|
|
1942
|
+
auto_inject: row.auto_inject
|
|
1943
|
+
};
|
|
1944
|
+
}
|
|
1945
|
+
function hasEquivalentEvidence(existing, next) {
|
|
1946
|
+
return existing.some((entry) => {
|
|
1947
|
+
if (entry.type !== next.type) return false;
|
|
1948
|
+
if (entry.type === "session_correction" && next.type === "session_correction") {
|
|
1949
|
+
return entry.session === next.session;
|
|
1950
|
+
}
|
|
1951
|
+
if (entry.type === "review_feedback" && next.type === "review_feedback") {
|
|
1952
|
+
return entry.reviewer === next.reviewer && entry.context === next.context;
|
|
1953
|
+
}
|
|
1954
|
+
if (entry.type === "repo_scan" && next.type === "repo_scan") {
|
|
1955
|
+
return entry.file === next.file;
|
|
1956
|
+
}
|
|
1957
|
+
if (entry.type === "repeated_correction" && next.type === "repeated_correction") {
|
|
1958
|
+
return entry.sessions.join("|") === next.sessions.join("|");
|
|
1959
|
+
}
|
|
1960
|
+
return false;
|
|
1961
|
+
});
|
|
1962
|
+
}
|
|
1963
|
+
|
|
1964
|
+
// src/audit/trail.ts
|
|
1965
|
+
import { eq as eq5, desc } from "drizzle-orm";
|
|
1966
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
1967
|
+
function recordAudit(db, memoryId, action, actor, reason, beforeSnapshot, afterSnapshot) {
|
|
1968
|
+
const id = randomUUID2();
|
|
1969
|
+
if (afterSnapshot === void 0) {
|
|
1970
|
+
const mem = getMemory(db, memoryId);
|
|
1971
|
+
if (mem) {
|
|
1972
|
+
afterSnapshot = JSON.stringify(mem);
|
|
1973
|
+
}
|
|
1974
|
+
}
|
|
1975
|
+
db.insert(auditTrail).values({
|
|
1976
|
+
id,
|
|
1977
|
+
memory_id: memoryId,
|
|
1978
|
+
action,
|
|
1979
|
+
actor,
|
|
1980
|
+
before_snapshot: beforeSnapshot ?? null,
|
|
1981
|
+
after_snapshot: afterSnapshot ?? null,
|
|
1982
|
+
reason: reason ?? null,
|
|
1983
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1984
|
+
}).run();
|
|
1985
|
+
return id;
|
|
1986
|
+
}
|
|
1987
|
+
function recordAuditWithSnapshot(db, memoryId, action, actor, reason, before, after) {
|
|
1988
|
+
return recordAudit(
|
|
1989
|
+
db,
|
|
1990
|
+
memoryId,
|
|
1991
|
+
action,
|
|
1992
|
+
actor,
|
|
1993
|
+
reason,
|
|
1994
|
+
before ? JSON.stringify(before) : null,
|
|
1995
|
+
after ? JSON.stringify(after) : null
|
|
1996
|
+
);
|
|
1997
|
+
}
|
|
1998
|
+
function getAuditTrail(db, memoryId) {
|
|
1999
|
+
return db.select().from(auditTrail).where(eq5(auditTrail.memory_id, memoryId)).orderBy(desc(auditTrail.timestamp)).all().map(rowToAudit);
|
|
2000
|
+
}
|
|
2001
|
+
function getRecentAudit(db, limit = 50) {
|
|
2002
|
+
return db.select().from(auditTrail).orderBy(desc(auditTrail.timestamp)).limit(limit).all().map(rowToAudit);
|
|
2003
|
+
}
|
|
2004
|
+
function diffSnapshots(before, after) {
|
|
2005
|
+
if (!before || !after) return [];
|
|
2006
|
+
const a = JSON.parse(before);
|
|
2007
|
+
const b = JSON.parse(after);
|
|
2008
|
+
const diffs = [];
|
|
2009
|
+
const keys = /* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(b)]);
|
|
2010
|
+
for (const key of keys) {
|
|
2011
|
+
if (key === "evidence" || key === "embedding") continue;
|
|
2012
|
+
const va = JSON.stringify(a[key]);
|
|
2013
|
+
const vb = JSON.stringify(b[key]);
|
|
2014
|
+
if (va !== vb) {
|
|
2015
|
+
diffs.push({ field: key, before: a[key], after: b[key] });
|
|
2016
|
+
}
|
|
2017
|
+
}
|
|
2018
|
+
return diffs;
|
|
2019
|
+
}
|
|
2020
|
+
function rollbackMemory(db, memoryId, auditEntryId, actor) {
|
|
2021
|
+
const entry = db.select().from(auditTrail).where(eq5(auditTrail.id, auditEntryId)).get();
|
|
2022
|
+
if (!entry) return false;
|
|
2023
|
+
const snapshot = entry.before_snapshot;
|
|
2024
|
+
if (!snapshot) return false;
|
|
2025
|
+
const beforeRollback = getMemory(db, memoryId);
|
|
2026
|
+
const restored = JSON.parse(snapshot);
|
|
2027
|
+
db.update(memories).set({
|
|
2028
|
+
text: restored.text,
|
|
2029
|
+
type: restored.type,
|
|
2030
|
+
scope: restored.scope,
|
|
2031
|
+
path_scope: restored.path_scope,
|
|
2032
|
+
status: restored.status,
|
|
2033
|
+
confidence: restored.confidence,
|
|
2034
|
+
source: restored.source,
|
|
2035
|
+
evidence: restored.evidence,
|
|
2036
|
+
capture_context: restored.capture_context,
|
|
2037
|
+
repetition_count: restored.repetition_count,
|
|
2038
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
2039
|
+
}).where(eq5(memories.id, memoryId)).run();
|
|
2040
|
+
queueMemoryEmbeddingSync(db, memoryId);
|
|
2041
|
+
recordAuditWithSnapshot(
|
|
2042
|
+
db,
|
|
2043
|
+
memoryId,
|
|
2044
|
+
"rolled_back",
|
|
2045
|
+
actor,
|
|
2046
|
+
`Rolled back to state from ${entry.timestamp}`,
|
|
2047
|
+
beforeRollback ?? null,
|
|
2048
|
+
getMemory(db, memoryId) ?? null
|
|
2049
|
+
);
|
|
2050
|
+
return true;
|
|
2051
|
+
}
|
|
2052
|
+
function formatAuditTrail(entries) {
|
|
2053
|
+
if (entries.length === 0) return "No audit entries.";
|
|
2054
|
+
const lines = ["# Audit Trail", ""];
|
|
2055
|
+
for (const e of entries) {
|
|
2056
|
+
const diffs = diffSnapshots(e.before_snapshot, e.after_snapshot);
|
|
2057
|
+
const diffStr = diffs.length > 0 ? ` [${diffs.map((d) => `${d.field}: ${JSON.stringify(d.before)} \u2192 ${JSON.stringify(d.after)}`).join(", ")}]` : "";
|
|
2058
|
+
lines.push(
|
|
2059
|
+
`${e.timestamp.slice(0, 19)} ${e.action.padEnd(22)} by ${e.actor}${e.reason ? ` \u2014 ${e.reason}` : ""}${diffStr}`
|
|
2060
|
+
);
|
|
2061
|
+
}
|
|
2062
|
+
return lines.join("\n");
|
|
2063
|
+
}
|
|
2064
|
+
function rowToAudit(row) {
|
|
2065
|
+
return {
|
|
2066
|
+
id: row.id,
|
|
2067
|
+
memory_id: row.memory_id,
|
|
2068
|
+
action: row.action,
|
|
2069
|
+
actor: row.actor,
|
|
2070
|
+
before_snapshot: row.before_snapshot,
|
|
2071
|
+
after_snapshot: row.after_snapshot,
|
|
2072
|
+
reason: row.reason,
|
|
2073
|
+
timestamp: row.timestamp
|
|
2074
|
+
};
|
|
2075
|
+
}
|
|
2076
|
+
|
|
2077
|
+
// src/maintenance/appliers.ts
|
|
2078
|
+
var ApplyError = class extends Error {
|
|
2079
|
+
constructor(message, code) {
|
|
2080
|
+
super(message);
|
|
2081
|
+
this.code = code;
|
|
2082
|
+
this.name = "ApplyError";
|
|
2083
|
+
}
|
|
2084
|
+
code;
|
|
2085
|
+
};
|
|
2086
|
+
function applyRefineCandidate(db, task, result) {
|
|
2087
|
+
const memoryId = task.payload.memory_id;
|
|
2088
|
+
if (!memoryId) throw new ApplyError("payload missing memory_id", "invalid-state");
|
|
2089
|
+
const before = getMemory(db, memoryId);
|
|
2090
|
+
if (!before) throw new ApplyError(`memory ${memoryId} not found`, "target-missing");
|
|
2091
|
+
const actor = `maintenance:${task.claimed_by ?? "unknown"}`;
|
|
2092
|
+
if (result.verdict === "reject") {
|
|
2093
|
+
return rejectMemoryFromTask(db, task, memoryId, before, actor, result.rationale);
|
|
2094
|
+
}
|
|
2095
|
+
const changed = [];
|
|
2096
|
+
if (before.text !== result.refined_text) changed.push("text");
|
|
2097
|
+
if (before.scope !== result.scope) changed.push("scope");
|
|
2098
|
+
const newPathScope = result.path_scope ?? null;
|
|
2099
|
+
if (before.path_scope !== newPathScope) changed.push("path_scope");
|
|
2100
|
+
if (changed.length === 0) {
|
|
2101
|
+
return { audit_entry_id: null, target_id: memoryId, changed_fields: [] };
|
|
2102
|
+
}
|
|
2103
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2104
|
+
db.update(memories).set({
|
|
2105
|
+
text: result.refined_text,
|
|
2106
|
+
scope: result.scope,
|
|
2107
|
+
path_scope: newPathScope,
|
|
2108
|
+
updated_at: now,
|
|
2109
|
+
last_validated_at: now
|
|
2110
|
+
}).where(eq6(memories.id, memoryId)).run();
|
|
2111
|
+
queueMemoryEmbeddingSync(db, memoryId);
|
|
2112
|
+
const after = getMemory(db, memoryId);
|
|
2113
|
+
const reason = result.rationale ? `refined:${task.id}:${result.rationale.slice(0, 200)}` : `refined:${task.id}`;
|
|
2114
|
+
const auditId = recordAuditWithSnapshot(
|
|
2115
|
+
db,
|
|
2116
|
+
memoryId,
|
|
2117
|
+
"edited",
|
|
2118
|
+
actor,
|
|
2119
|
+
reason,
|
|
2120
|
+
before,
|
|
2121
|
+
after ?? null
|
|
2122
|
+
);
|
|
2123
|
+
return { audit_entry_id: auditId, target_id: memoryId, changed_fields: changed };
|
|
2124
|
+
}
|
|
2125
|
+
function applyVerifyCapture(db, task, result) {
|
|
2126
|
+
const memoryId = task.payload.memory_id;
|
|
2127
|
+
if (!memoryId) throw new ApplyError("payload missing memory_id", "invalid-state");
|
|
2128
|
+
const before = getMemory(db, memoryId);
|
|
2129
|
+
if (!before) throw new ApplyError(`memory ${memoryId} not found`, "target-missing");
|
|
2130
|
+
const actor = `maintenance:${task.claimed_by ?? "unknown"}`;
|
|
2131
|
+
if (result.verdict === "reject") {
|
|
2132
|
+
return rejectMemoryFromTask(db, task, memoryId, before, actor, result.reason);
|
|
2133
|
+
}
|
|
2134
|
+
if (result.verdict === "save") {
|
|
2135
|
+
return { audit_entry_id: null, target_id: memoryId, changed_fields: [] };
|
|
2136
|
+
}
|
|
2137
|
+
const newText = result.cleaned_text ?? before.text;
|
|
2138
|
+
const newScope = result.scope ?? before.scope;
|
|
2139
|
+
const newPathScope = result.path_scope ?? before.path_scope;
|
|
2140
|
+
const changed = [];
|
|
2141
|
+
if (before.text !== newText) changed.push("text");
|
|
2142
|
+
if (before.scope !== newScope) changed.push("scope");
|
|
2143
|
+
if (before.path_scope !== newPathScope) changed.push("path_scope");
|
|
2144
|
+
if (changed.length === 0) {
|
|
2145
|
+
return { audit_entry_id: null, target_id: memoryId, changed_fields: [] };
|
|
2146
|
+
}
|
|
2147
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2148
|
+
db.update(memories).set({
|
|
2149
|
+
text: newText,
|
|
2150
|
+
scope: newScope,
|
|
2151
|
+
path_scope: newPathScope,
|
|
2152
|
+
updated_at: now,
|
|
2153
|
+
last_validated_at: now
|
|
2154
|
+
}).where(eq6(memories.id, memoryId)).run();
|
|
2155
|
+
queueMemoryEmbeddingSync(db, memoryId);
|
|
2156
|
+
const after = getMemory(db, memoryId);
|
|
2157
|
+
const reason = result.reason ? `verify:rewrite:${task.id}:${result.reason.slice(0, 200)}` : `verify:rewrite:${task.id}`;
|
|
2158
|
+
const auditId = recordAuditWithSnapshot(
|
|
2159
|
+
db,
|
|
2160
|
+
memoryId,
|
|
2161
|
+
"edited",
|
|
2162
|
+
actor,
|
|
2163
|
+
reason,
|
|
2164
|
+
before,
|
|
2165
|
+
after ?? null
|
|
2166
|
+
);
|
|
2167
|
+
return { audit_entry_id: auditId, target_id: memoryId, changed_fields: changed };
|
|
2168
|
+
}
|
|
2169
|
+
function rejectMemoryFromTask(db, task, memoryId, before, actor, reasonText) {
|
|
2170
|
+
if (before.status === "rejected") {
|
|
2171
|
+
return { audit_entry_id: null, target_id: memoryId, changed_fields: [] };
|
|
2172
|
+
}
|
|
2173
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2174
|
+
db.update(memories).set({ status: "rejected", confidence: 0, dedupe_key: null, updated_at: now }).where(eq6(memories.id, memoryId)).run();
|
|
2175
|
+
queueMemoryEmbeddingSync(db, memoryId);
|
|
2176
|
+
const after = getMemory(db, memoryId);
|
|
2177
|
+
const reason = reasonText ? `${task.kind}:reject:${task.id}:${reasonText.slice(0, 200)}` : `${task.kind}:reject:${task.id}`;
|
|
2178
|
+
const auditId = recordAuditWithSnapshot(
|
|
2179
|
+
db,
|
|
2180
|
+
memoryId,
|
|
2181
|
+
"rejected",
|
|
2182
|
+
actor,
|
|
2183
|
+
reason,
|
|
2184
|
+
before,
|
|
2185
|
+
after ?? null
|
|
2186
|
+
);
|
|
2187
|
+
return { audit_entry_id: auditId, target_id: memoryId, changed_fields: ["status"] };
|
|
2188
|
+
}
|
|
2189
|
+
function applySummarizeHistory(db, task, result) {
|
|
2190
|
+
const snippetId = task.payload.snippet_id;
|
|
2191
|
+
if (!snippetId) throw new ApplyError("payload missing snippet_id", "invalid-state");
|
|
2192
|
+
const existing = db.select().from(historySnippets).where(eq6(historySnippets.id, snippetId)).get();
|
|
2193
|
+
if (!existing) throw new ApplyError(`history snippet ${snippetId} not found`, "target-missing");
|
|
2194
|
+
const changed = [];
|
|
2195
|
+
if (existing.text !== result.summary_text) changed.push("text");
|
|
2196
|
+
if (changed.length === 0) {
|
|
2197
|
+
return { audit_entry_id: null, target_id: snippetId, changed_fields: [] };
|
|
2198
|
+
}
|
|
2199
|
+
db.update(historySnippets).set({
|
|
2200
|
+
text: result.summary_text,
|
|
2201
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
2202
|
+
}).where(eq6(historySnippets.id, snippetId)).run();
|
|
2203
|
+
return { audit_entry_id: null, target_id: snippetId, changed_fields: changed };
|
|
2204
|
+
}
|
|
2205
|
+
function applyMergeDuplicates(db, task, result) {
|
|
2206
|
+
const payload = task.payload;
|
|
2207
|
+
const candidates = payload.candidates ?? [];
|
|
2208
|
+
if (candidates.length < 2) {
|
|
2209
|
+
throw new ApplyError("merge_duplicates payload needs \u22652 candidates", "invalid-state");
|
|
2210
|
+
}
|
|
2211
|
+
const winnerId = result.winner_id;
|
|
2212
|
+
if (!candidates.some((c) => c.id === winnerId)) {
|
|
2213
|
+
throw new ApplyError(`winner_id ${winnerId} not in candidates`, "invalid-state");
|
|
2214
|
+
}
|
|
2215
|
+
const winner = getMemory(db, winnerId);
|
|
2216
|
+
if (!winner) throw new ApplyError(`winner ${winnerId} not found`, "target-missing");
|
|
2217
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2218
|
+
const changed = [];
|
|
2219
|
+
const actor = `maintenance:${task.claimed_by ?? "unknown"}`;
|
|
2220
|
+
const nextText = result.winner_text ?? winner.text;
|
|
2221
|
+
const nextScope = result.winner_scope ?? winner.scope;
|
|
2222
|
+
const nextPathScope = result.winner_path_scope !== void 0 ? result.winner_path_scope ?? null : winner.path_scope;
|
|
2223
|
+
const winnerChanged = nextText !== winner.text || nextScope !== winner.scope || nextPathScope !== winner.path_scope;
|
|
2224
|
+
if (winnerChanged) {
|
|
2225
|
+
db.update(memories).set({
|
|
2226
|
+
text: nextText,
|
|
2227
|
+
scope: nextScope,
|
|
2228
|
+
path_scope: nextPathScope,
|
|
2229
|
+
updated_at: now,
|
|
2230
|
+
last_validated_at: now
|
|
2231
|
+
}).where(eq6(memories.id, winnerId)).run();
|
|
2232
|
+
queueMemoryEmbeddingSync(db, winnerId);
|
|
2233
|
+
const after = getMemory(db, winnerId);
|
|
2234
|
+
recordAuditWithSnapshot(
|
|
2235
|
+
db,
|
|
2236
|
+
winnerId,
|
|
2237
|
+
"edited",
|
|
2238
|
+
actor,
|
|
2239
|
+
`merged_winner:${task.id}`,
|
|
2240
|
+
winner,
|
|
2241
|
+
after ?? null
|
|
2242
|
+
);
|
|
2243
|
+
changed.push(`winner:${winnerId}`);
|
|
2244
|
+
}
|
|
2245
|
+
for (const cand of candidates) {
|
|
2246
|
+
if (cand.id === winnerId) continue;
|
|
2247
|
+
const loser = getMemory(db, cand.id);
|
|
2248
|
+
if (!loser) continue;
|
|
2249
|
+
if (loser.status === "rejected") continue;
|
|
2250
|
+
db.update(memories).set({
|
|
2251
|
+
status: "rejected",
|
|
2252
|
+
supersedes: winnerId,
|
|
2253
|
+
dedupe_key: null,
|
|
2254
|
+
updated_at: now
|
|
2255
|
+
}).where(eq6(memories.id, cand.id)).run();
|
|
2256
|
+
queueMemoryEmbeddingSync(db, cand.id);
|
|
2257
|
+
const afterLoser = getMemory(db, cand.id);
|
|
2258
|
+
recordAuditWithSnapshot(
|
|
2259
|
+
db,
|
|
2260
|
+
cand.id,
|
|
2261
|
+
"rejected",
|
|
2262
|
+
actor,
|
|
2263
|
+
`merged_into:${winnerId}:${task.id}`,
|
|
2264
|
+
loser,
|
|
2265
|
+
afterLoser ?? null
|
|
2266
|
+
);
|
|
2267
|
+
changed.push(`loser:${cand.id}`);
|
|
2268
|
+
}
|
|
2269
|
+
return {
|
|
2270
|
+
audit_entry_id: null,
|
|
2271
|
+
target_id: winnerId,
|
|
2272
|
+
changed_fields: changed
|
|
2273
|
+
};
|
|
2274
|
+
}
|
|
2275
|
+
function applySummarizeSession(db, task, result) {
|
|
2276
|
+
const payload = task.payload;
|
|
2277
|
+
const sessionId = payload.session_id;
|
|
2278
|
+
if (!sessionId) throw new ApplyError("payload missing session_id", "invalid-state");
|
|
2279
|
+
const id = randomUUID3();
|
|
2280
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2281
|
+
db.insert(historySnippets).values({
|
|
2282
|
+
id,
|
|
2283
|
+
repo: payload.repo ?? null,
|
|
2284
|
+
session_id: sessionId,
|
|
2285
|
+
kind: "session_summary",
|
|
2286
|
+
text: result.summary_text,
|
|
2287
|
+
source_activity_ids: payload.source_activity_ids ?? [],
|
|
2288
|
+
created_at: now,
|
|
2289
|
+
updated_at: now
|
|
2290
|
+
}).run();
|
|
2291
|
+
return { audit_entry_id: null, target_id: id, changed_fields: ["text"] };
|
|
2292
|
+
}
|
|
2293
|
+
function applySynthesizeRepo(db, task, result) {
|
|
2294
|
+
const payload = task.payload;
|
|
2295
|
+
const repo = payload.repo;
|
|
2296
|
+
if (!repo) throw new ApplyError("payload missing repo", "invalid-state");
|
|
2297
|
+
const id = randomUUID3();
|
|
2298
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2299
|
+
db.insert(historySnippets).values({
|
|
2300
|
+
id,
|
|
2301
|
+
repo,
|
|
2302
|
+
session_id: null,
|
|
2303
|
+
kind: "repo_synthesis",
|
|
2304
|
+
text: result.summary_text,
|
|
2305
|
+
source_activity_ids: [],
|
|
2306
|
+
created_at: now,
|
|
2307
|
+
updated_at: now
|
|
2308
|
+
}).run();
|
|
2309
|
+
return { audit_entry_id: null, target_id: id, changed_fields: ["text"] };
|
|
2310
|
+
}
|
|
2311
|
+
function applyTaskResult(db, task, result) {
|
|
2312
|
+
switch (task.kind) {
|
|
2313
|
+
case "verify_capture":
|
|
2314
|
+
return applyVerifyCapture(db, task, result);
|
|
2315
|
+
case "refine_candidate":
|
|
2316
|
+
return applyRefineCandidate(db, task, result);
|
|
2317
|
+
case "summarize_history":
|
|
2318
|
+
return applySummarizeHistory(db, task, result);
|
|
2319
|
+
case "merge_duplicates":
|
|
2320
|
+
return applyMergeDuplicates(db, task, result);
|
|
2321
|
+
case "summarize_session":
|
|
2322
|
+
return applySummarizeSession(db, task, result);
|
|
2323
|
+
case "synthesize_repo":
|
|
2324
|
+
return applySynthesizeRepo(db, task, result);
|
|
2325
|
+
default: {
|
|
2326
|
+
const never = task.kind;
|
|
2327
|
+
throw new ApplyError(`unknown kind ${never}`, "unsupported-kind");
|
|
2328
|
+
}
|
|
2329
|
+
}
|
|
2330
|
+
}
|
|
2331
|
+
|
|
2332
|
+
// src/maintenance/tasks.ts
|
|
2333
|
+
var OPEN_STATUSES = ["pending", "claimed", "submitted"];
|
|
2334
|
+
var ACTIVE_STATUSES = [...OPEN_STATUSES, "completed"];
|
|
2335
|
+
var DEFAULT_LEASE_SECONDS = 600;
|
|
2336
|
+
var DEFAULT_PRIORITIES = {
|
|
2337
|
+
verify_capture: 12,
|
|
2338
|
+
refine_candidate: 10,
|
|
2339
|
+
merge_duplicates: 8,
|
|
2340
|
+
summarize_history: 5,
|
|
2341
|
+
summarize_session: 3,
|
|
2342
|
+
synthesize_repo: 1
|
|
2343
|
+
};
|
|
2344
|
+
var DEFAULT_ENQUEUE_CONFIG = {
|
|
2345
|
+
max_pending: 50,
|
|
2346
|
+
max_per_kind: 10,
|
|
2347
|
+
refine_min_repetition: 1,
|
|
2348
|
+
summary_max_age_days: 7,
|
|
2349
|
+
merge_similarity_threshold: 0.9,
|
|
2350
|
+
session_min_activity_events: 5,
|
|
2351
|
+
repo_synthesis_min_memories: 20,
|
|
2352
|
+
repo_synthesis_refresh_days: 30
|
|
2353
|
+
};
|
|
2354
|
+
function rowToTask(row) {
|
|
2355
|
+
return {
|
|
2356
|
+
id: row.id,
|
|
2357
|
+
kind: row.kind,
|
|
2358
|
+
status: row.status,
|
|
2359
|
+
priority: row.priority,
|
|
2360
|
+
repo: row.repo,
|
|
2361
|
+
target_key: row.target_key,
|
|
2362
|
+
payload: row.payload ?? {},
|
|
2363
|
+
result: row.result ?? null,
|
|
2364
|
+
failure_reason: row.failure_reason,
|
|
2365
|
+
claimed_by: row.claimed_by,
|
|
2366
|
+
claimed_at: row.claimed_at,
|
|
2367
|
+
claim_expires_at: row.claim_expires_at,
|
|
2368
|
+
submitted_at: row.submitted_at,
|
|
2369
|
+
completed_at: row.completed_at,
|
|
2370
|
+
created_at: row.created_at,
|
|
2371
|
+
attempts: row.attempts,
|
|
2372
|
+
max_attempts: row.max_attempts
|
|
2373
|
+
};
|
|
2374
|
+
}
|
|
2375
|
+
function targetKey(kind, target) {
|
|
2376
|
+
return `${kind}:${target}`;
|
|
2377
|
+
}
|
|
2378
|
+
function hasActiveTaskForTarget(db, kind, target) {
|
|
2379
|
+
const row = db.select({ id: memoryMaintenanceTasks.id }).from(memoryMaintenanceTasks).where(and2(
|
|
2380
|
+
eq7(memoryMaintenanceTasks.kind, kind),
|
|
2381
|
+
eq7(memoryMaintenanceTasks.target_key, targetKey(kind, target)),
|
|
2382
|
+
inArray2(memoryMaintenanceTasks.status, ACTIVE_STATUSES)
|
|
2383
|
+
)).limit(1).get();
|
|
2384
|
+
return Boolean(row);
|
|
2385
|
+
}
|
|
2386
|
+
function insertTaskIdempotent(db, input) {
|
|
2387
|
+
if (hasActiveTaskForTarget(db, input.kind, input.target)) return null;
|
|
2388
|
+
const id = randomUUID4();
|
|
2389
|
+
db.insert(memoryMaintenanceTasks).values({
|
|
2390
|
+
id,
|
|
2391
|
+
kind: input.kind,
|
|
2392
|
+
status: "pending",
|
|
2393
|
+
priority: input.priority ?? DEFAULT_PRIORITIES[input.kind] ?? 0,
|
|
2394
|
+
repo: input.repo ?? null,
|
|
2395
|
+
target_key: targetKey(input.kind, input.target),
|
|
2396
|
+
payload: input.payload,
|
|
2397
|
+
result: null,
|
|
2398
|
+
failure_reason: null,
|
|
2399
|
+
claimed_by: null,
|
|
2400
|
+
claimed_at: null,
|
|
2401
|
+
claim_expires_at: null,
|
|
2402
|
+
submitted_at: null,
|
|
2403
|
+
completed_at: null,
|
|
2404
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2405
|
+
attempts: 0,
|
|
2406
|
+
max_attempts: input.max_attempts ?? 3
|
|
2407
|
+
}).run();
|
|
2408
|
+
return id;
|
|
2409
|
+
}
|
|
2410
|
+
function deleteTask(db, id) {
|
|
2411
|
+
const result = db.delete(memoryMaintenanceTasks).where(eq7(memoryMaintenanceTasks.id, id)).run();
|
|
2412
|
+
return result.changes > 0;
|
|
2413
|
+
}
|
|
2414
|
+
function getTaskStats(db) {
|
|
2415
|
+
const rows = db.select().from(memoryMaintenanceTasks).all();
|
|
2416
|
+
const by_status = { pending: 0, claimed: 0, submitted: 0, completed: 0, abandoned: 0 };
|
|
2417
|
+
const by_kind = { verify_capture: 0, refine_candidate: 0, merge_duplicates: 0, summarize_history: 0, summarize_session: 0, synthesize_repo: 0 };
|
|
2418
|
+
const by_kind_status = {};
|
|
2419
|
+
const dayAgo = new Date(Date.now() - 864e5).toISOString();
|
|
2420
|
+
let completed_last_24h = 0;
|
|
2421
|
+
let abandoned_last_24h = 0;
|
|
2422
|
+
let pending_oldest = null;
|
|
2423
|
+
let completionDurations = [];
|
|
2424
|
+
for (const row of rows) {
|
|
2425
|
+
by_status[row.status] += 1;
|
|
2426
|
+
by_kind[row.kind] += 1;
|
|
2427
|
+
const key = `${row.kind}:${row.status}`;
|
|
2428
|
+
by_kind_status[key] = (by_kind_status[key] ?? 0) + 1;
|
|
2429
|
+
if (row.status === "pending") {
|
|
2430
|
+
if (!pending_oldest || row.created_at < pending_oldest) pending_oldest = row.created_at;
|
|
2431
|
+
}
|
|
2432
|
+
if (row.completed_at && row.completed_at >= dayAgo) {
|
|
2433
|
+
if (row.status === "completed") completed_last_24h += 1;
|
|
2434
|
+
if (row.status === "abandoned") abandoned_last_24h += 1;
|
|
2435
|
+
}
|
|
2436
|
+
if (row.status === "completed" && row.completed_at) {
|
|
2437
|
+
const delta = new Date(row.completed_at).getTime() - new Date(row.created_at).getTime();
|
|
2438
|
+
if (Number.isFinite(delta) && delta >= 0) completionDurations.push(delta);
|
|
2439
|
+
}
|
|
2440
|
+
}
|
|
2441
|
+
const mean_completion_ms = completionDurations.length ? completionDurations.reduce((a, b) => a + b, 0) / completionDurations.length : null;
|
|
2442
|
+
return {
|
|
2443
|
+
total: rows.length,
|
|
2444
|
+
by_status,
|
|
2445
|
+
by_kind,
|
|
2446
|
+
by_kind_status,
|
|
2447
|
+
pending_oldest_created_at: pending_oldest,
|
|
2448
|
+
completed_last_24h,
|
|
2449
|
+
abandoned_last_24h,
|
|
2450
|
+
mean_completion_ms
|
|
2451
|
+
};
|
|
2452
|
+
}
|
|
2453
|
+
function getTask(db, id) {
|
|
2454
|
+
const row = db.select().from(memoryMaintenanceTasks).where(eq7(memoryMaintenanceTasks.id, id)).get();
|
|
2455
|
+
return row ? rowToTask(row) : void 0;
|
|
2456
|
+
}
|
|
2457
|
+
function listTasks(db, query = {}) {
|
|
2458
|
+
const conditions = [];
|
|
2459
|
+
if (query.status) {
|
|
2460
|
+
const statuses = Array.isArray(query.status) ? query.status : [query.status];
|
|
2461
|
+
conditions.push(inArray2(memoryMaintenanceTasks.status, statuses));
|
|
2462
|
+
}
|
|
2463
|
+
if (query.kinds?.length) {
|
|
2464
|
+
conditions.push(inArray2(memoryMaintenanceTasks.kind, query.kinds));
|
|
2465
|
+
}
|
|
2466
|
+
if (query.repo) {
|
|
2467
|
+
conditions.push(eq7(memoryMaintenanceTasks.repo, query.repo));
|
|
2468
|
+
}
|
|
2469
|
+
let stmt = db.select().from(memoryMaintenanceTasks).$dynamic();
|
|
2470
|
+
if (conditions.length) stmt = stmt.where(and2(...conditions));
|
|
2471
|
+
stmt = stmt.orderBy(sql2`${memoryMaintenanceTasks.priority} DESC`, memoryMaintenanceTasks.created_at).limit(query.limit ?? 50);
|
|
2472
|
+
return stmt.all().map(rowToTask);
|
|
2473
|
+
}
|
|
2474
|
+
function sweepExpiredLeases(db, now = /* @__PURE__ */ new Date()) {
|
|
2475
|
+
const nowIso = now.toISOString();
|
|
2476
|
+
const result = db.update(memoryMaintenanceTasks).set({
|
|
2477
|
+
status: "pending",
|
|
2478
|
+
claimed_by: null,
|
|
2479
|
+
claimed_at: null,
|
|
2480
|
+
claim_expires_at: null,
|
|
2481
|
+
attempts: sql2`${memoryMaintenanceTasks.attempts} + 1`
|
|
2482
|
+
}).where(and2(
|
|
2483
|
+
eq7(memoryMaintenanceTasks.status, "claimed"),
|
|
2484
|
+
lt(memoryMaintenanceTasks.claim_expires_at, nowIso)
|
|
2485
|
+
)).run();
|
|
2486
|
+
return result.changes;
|
|
2487
|
+
}
|
|
2488
|
+
function expireStalePendingTasks(db, maxAgeDays, now = /* @__PURE__ */ new Date()) {
|
|
2489
|
+
const cutoff = new Date(now.getTime() - maxAgeDays * 864e5).toISOString();
|
|
2490
|
+
const result = db.update(memoryMaintenanceTasks).set({
|
|
2491
|
+
status: "abandoned",
|
|
2492
|
+
failure_reason: "expired_no_dispatcher",
|
|
2493
|
+
completed_at: now.toISOString()
|
|
2494
|
+
}).where(and2(
|
|
2495
|
+
eq7(memoryMaintenanceTasks.status, "pending"),
|
|
2496
|
+
lt(memoryMaintenanceTasks.created_at, cutoff)
|
|
2497
|
+
)).run();
|
|
2498
|
+
return result.changes;
|
|
2499
|
+
}
|
|
2500
|
+
function abandonOverAttemptTasks(db) {
|
|
2501
|
+
const nowIso = (/* @__PURE__ */ new Date()).toISOString();
|
|
2502
|
+
const result = db.update(memoryMaintenanceTasks).set({
|
|
2503
|
+
status: "abandoned",
|
|
2504
|
+
failure_reason: "max_attempts_exceeded",
|
|
2505
|
+
completed_at: nowIso
|
|
2506
|
+
}).where(and2(
|
|
2507
|
+
inArray2(memoryMaintenanceTasks.status, ["pending", "claimed"]),
|
|
2508
|
+
sql2`${memoryMaintenanceTasks.attempts} >= ${memoryMaintenanceTasks.max_attempts}`
|
|
2509
|
+
)).run();
|
|
2510
|
+
return result.changes;
|
|
2511
|
+
}
|
|
2512
|
+
function applyBacklogCaps(db, config) {
|
|
2513
|
+
let dropped = 0;
|
|
2514
|
+
const overKindRows = db.select({
|
|
2515
|
+
kind: memoryMaintenanceTasks.kind,
|
|
2516
|
+
count: sql2`count(*)`.as("count")
|
|
2517
|
+
}).from(memoryMaintenanceTasks).where(eq7(memoryMaintenanceTasks.status, "pending")).groupBy(memoryMaintenanceTasks.kind).all();
|
|
2518
|
+
for (const { kind, count } of overKindRows) {
|
|
2519
|
+
if (count <= config.max_per_kind) continue;
|
|
2520
|
+
const toDrop = count - config.max_per_kind;
|
|
2521
|
+
dropped += dropLowestPriorityPending(db, toDrop, { kind });
|
|
2522
|
+
}
|
|
2523
|
+
const pendingCount = db.select({ n: sql2`count(*)` }).from(memoryMaintenanceTasks).where(eq7(memoryMaintenanceTasks.status, "pending")).get()?.n ?? 0;
|
|
2524
|
+
if (pendingCount > config.max_pending) {
|
|
2525
|
+
dropped += dropLowestPriorityPending(db, pendingCount - config.max_pending);
|
|
2526
|
+
}
|
|
2527
|
+
return dropped;
|
|
2528
|
+
}
|
|
2529
|
+
function dropLowestPriorityPending(db, limit, filter = {}) {
|
|
2530
|
+
const conditions = [eq7(memoryMaintenanceTasks.status, "pending")];
|
|
2531
|
+
if (filter.kind) conditions.push(eq7(memoryMaintenanceTasks.kind, filter.kind));
|
|
2532
|
+
const ids = db.select({ id: memoryMaintenanceTasks.id }).from(memoryMaintenanceTasks).where(and2(...conditions)).orderBy(memoryMaintenanceTasks.priority, sql2`${memoryMaintenanceTasks.created_at} DESC`).limit(limit).all().map((r) => r.id);
|
|
2533
|
+
if (!ids.length) return 0;
|
|
2534
|
+
const result = db.delete(memoryMaintenanceTasks).where(inArray2(memoryMaintenanceTasks.id, ids)).run();
|
|
2535
|
+
return result.changes;
|
|
2536
|
+
}
|
|
2537
|
+
function enqueueVerifyCapture(db, memory) {
|
|
2538
|
+
return insertTaskIdempotent(db, {
|
|
2539
|
+
kind: "verify_capture",
|
|
2540
|
+
target: memory.id,
|
|
2541
|
+
repo: memory.repo,
|
|
2542
|
+
payload: {
|
|
2543
|
+
memory_id: memory.id,
|
|
2544
|
+
text: memory.text,
|
|
2545
|
+
inferred_scope: memory.scope,
|
|
2546
|
+
inferred_path_scope: memory.path_scope,
|
|
2547
|
+
repo: memory.repo,
|
|
2548
|
+
capture_context: memory.capture_context ?? null
|
|
2549
|
+
}
|
|
2550
|
+
});
|
|
2551
|
+
}
|
|
2552
|
+
function produceRefineCandidateTasks(db, config) {
|
|
2553
|
+
const candidates = db.select().from(memories).where(and2(
|
|
2554
|
+
eq7(memories.status, "candidate"),
|
|
2555
|
+
or(eq7(memories.scope, "repo"), isNull(memories.path_scope))
|
|
2556
|
+
)).all();
|
|
2557
|
+
let enqueued = 0;
|
|
2558
|
+
for (const row of candidates) {
|
|
2559
|
+
if (row.repetition_count < config.refine_min_repetition) continue;
|
|
2560
|
+
if (!row.repo) continue;
|
|
2561
|
+
const id = insertTaskIdempotent(db, {
|
|
2562
|
+
kind: "refine_candidate",
|
|
2563
|
+
target: row.id,
|
|
2564
|
+
repo: row.repo,
|
|
2565
|
+
payload: {
|
|
2566
|
+
memory_id: row.id,
|
|
2567
|
+
text: row.text,
|
|
2568
|
+
current_scope: row.scope,
|
|
2569
|
+
current_path_scope: row.path_scope,
|
|
2570
|
+
repo: row.repo,
|
|
2571
|
+
capture_context: row.capture_context ?? null,
|
|
2572
|
+
repetition_count: row.repetition_count
|
|
2573
|
+
}
|
|
2574
|
+
});
|
|
2575
|
+
if (id) enqueued += 1;
|
|
2576
|
+
}
|
|
2577
|
+
return enqueued;
|
|
2578
|
+
}
|
|
2579
|
+
function produceSummarizeHistoryTasks(db, config) {
|
|
2580
|
+
const cutoff = new Date(Date.now() - config.summary_max_age_days * 864e5).toISOString();
|
|
2581
|
+
const snippets = db.select().from(historySnippets).where(sql2`${historySnippets.created_at} >= ${cutoff}`).all();
|
|
2582
|
+
let enqueued = 0;
|
|
2583
|
+
for (const snippet of snippets) {
|
|
2584
|
+
if (!snippetHasMeaningfulContent(snippet.text)) continue;
|
|
2585
|
+
const id = insertTaskIdempotent(db, {
|
|
2586
|
+
kind: "summarize_history",
|
|
2587
|
+
target: snippet.id,
|
|
2588
|
+
repo: snippet.repo ?? null,
|
|
2589
|
+
payload: {
|
|
2590
|
+
snippet_id: snippet.id,
|
|
2591
|
+
kind: snippet.kind,
|
|
2592
|
+
repo: snippet.repo,
|
|
2593
|
+
session_id: snippet.session_id,
|
|
2594
|
+
current_text: snippet.text,
|
|
2595
|
+
source_activity_ids: snippet.source_activity_ids
|
|
2596
|
+
}
|
|
2597
|
+
});
|
|
2598
|
+
if (id) enqueued += 1;
|
|
2599
|
+
}
|
|
2600
|
+
return enqueued;
|
|
2601
|
+
}
|
|
2602
|
+
function snippetHasMeaningfulContent(text) {
|
|
2603
|
+
const lines = text.split("\n").map((line) => line.trim()).filter(Boolean);
|
|
2604
|
+
if (lines.length === 0) return false;
|
|
2605
|
+
return lines.some(
|
|
2606
|
+
(line) => line.startsWith("Corrections:") || line.startsWith("Reviews:") || line.startsWith("Latest compile included") || line.startsWith("Prompts:")
|
|
2607
|
+
);
|
|
2608
|
+
}
|
|
2609
|
+
async function produceMergeDuplicateTasks(db, config) {
|
|
2610
|
+
const embeddingConfig = loadEmbeddingConfigFromEnv();
|
|
2611
|
+
if (!embeddingConfig) return 0;
|
|
2612
|
+
const activeMemories = db.select().from(memories).where(eq7(memories.status, "active")).all();
|
|
2613
|
+
const visited = /* @__PURE__ */ new Set();
|
|
2614
|
+
let enqueued = 0;
|
|
2615
|
+
for (const mem of activeMemories) {
|
|
2616
|
+
if (visited.has(mem.id)) continue;
|
|
2617
|
+
if (!mem.repo) continue;
|
|
2618
|
+
const threshold = mem.type === "command" ? Math.min(config.merge_similarity_threshold, 0.85) : config.merge_similarity_threshold;
|
|
2619
|
+
const duplicates = await findSemanticDuplicates(
|
|
2620
|
+
db,
|
|
2621
|
+
mem.text,
|
|
2622
|
+
embeddingConfig,
|
|
2623
|
+
threshold,
|
|
2624
|
+
{ repo: mem.repo, type: mem.type, limit: 10 }
|
|
2625
|
+
);
|
|
2626
|
+
const peers = duplicates.filter((d) => d.id !== mem.id);
|
|
2627
|
+
if (peers.length === 0) continue;
|
|
2628
|
+
const cluster = [mem.id, ...peers.map((p) => p.id)].sort();
|
|
2629
|
+
if (cluster[0] !== mem.id) {
|
|
2630
|
+
for (const id2 of cluster) visited.add(id2);
|
|
2631
|
+
continue;
|
|
2632
|
+
}
|
|
2633
|
+
const clusterRows = db.select().from(memories).where(inArray2(memories.id, cluster)).all();
|
|
2634
|
+
const candidates = clusterRows.map((row) => ({
|
|
2635
|
+
id: row.id,
|
|
2636
|
+
text: row.text,
|
|
2637
|
+
scope: row.scope,
|
|
2638
|
+
path_scope: row.path_scope,
|
|
2639
|
+
confidence: row.confidence
|
|
2640
|
+
}));
|
|
2641
|
+
const id = insertTaskIdempotent(db, {
|
|
2642
|
+
kind: "merge_duplicates",
|
|
2643
|
+
target: cluster[0],
|
|
2644
|
+
repo: mem.repo,
|
|
2645
|
+
payload: {
|
|
2646
|
+
repo: mem.repo,
|
|
2647
|
+
type: mem.type,
|
|
2648
|
+
candidates
|
|
2649
|
+
}
|
|
2650
|
+
});
|
|
2651
|
+
if (id) enqueued += 1;
|
|
2652
|
+
for (const memberId of cluster) visited.add(memberId);
|
|
2653
|
+
}
|
|
2654
|
+
return enqueued;
|
|
2655
|
+
}
|
|
2656
|
+
function produceSummarizeSessionTasks(db, config) {
|
|
2657
|
+
const cutoff = new Date(Date.now() - config.summary_max_age_days * 864e5).toISOString();
|
|
2658
|
+
const sessionEnds = db.select().from(activityEvents).where(and2(
|
|
2659
|
+
eq7(activityEvents.event_type, "session_end"),
|
|
2660
|
+
gt(activityEvents.created_at, cutoff)
|
|
2661
|
+
)).orderBy(desc2(activityEvents.created_at)).all();
|
|
2662
|
+
let enqueued = 0;
|
|
2663
|
+
for (const end of sessionEnds) {
|
|
2664
|
+
if (!end.session_id) continue;
|
|
2665
|
+
const events = db.select().from(activityEvents).where(eq7(activityEvents.session_id, end.session_id)).all();
|
|
2666
|
+
if (events.length < config.session_min_activity_events) continue;
|
|
2667
|
+
const existing = db.select().from(historySnippets).where(and2(
|
|
2668
|
+
eq7(historySnippets.session_id, end.session_id),
|
|
2669
|
+
eq7(historySnippets.kind, "session_summary")
|
|
2670
|
+
)).get();
|
|
2671
|
+
if (existing) continue;
|
|
2672
|
+
const repo = end.repo ?? events.find((e) => e.repo)?.repo ?? null;
|
|
2673
|
+
const eventTypes = [...new Set(events.map((e) => e.event_type))];
|
|
2674
|
+
const id = insertTaskIdempotent(db, {
|
|
2675
|
+
kind: "summarize_session",
|
|
2676
|
+
target: end.session_id,
|
|
2677
|
+
repo,
|
|
2678
|
+
payload: {
|
|
2679
|
+
session_id: end.session_id,
|
|
2680
|
+
repo,
|
|
2681
|
+
event_count: events.length,
|
|
2682
|
+
event_types: eventTypes,
|
|
2683
|
+
source_activity_ids: events.map((e) => e.id)
|
|
2684
|
+
}
|
|
2685
|
+
});
|
|
2686
|
+
if (id) enqueued += 1;
|
|
2687
|
+
}
|
|
2688
|
+
return enqueued;
|
|
2689
|
+
}
|
|
2690
|
+
function produceSynthesizeRepoTasks(db, config) {
|
|
2691
|
+
const rows = db.select({
|
|
2692
|
+
repo: memories.repo,
|
|
2693
|
+
count: sql2`count(*)`.as("count")
|
|
2694
|
+
}).from(memories).where(eq7(memories.status, "active")).groupBy(memories.repo).all();
|
|
2695
|
+
const cutoff = new Date(Date.now() - config.repo_synthesis_refresh_days * 864e5).toISOString();
|
|
2696
|
+
let enqueued = 0;
|
|
2697
|
+
for (const { repo, count } of rows) {
|
|
2698
|
+
if (!repo) continue;
|
|
2699
|
+
if (count < config.repo_synthesis_min_memories) continue;
|
|
2700
|
+
const recent = db.select().from(historySnippets).where(and2(
|
|
2701
|
+
eq7(historySnippets.repo, repo),
|
|
2702
|
+
eq7(historySnippets.kind, "repo_synthesis"),
|
|
2703
|
+
gt(historySnippets.updated_at, cutoff)
|
|
2704
|
+
)).get();
|
|
2705
|
+
if (recent) continue;
|
|
2706
|
+
const topMemories = db.select().from(memories).where(and2(
|
|
2707
|
+
eq7(memories.repo, repo),
|
|
2708
|
+
eq7(memories.status, "active")
|
|
2709
|
+
)).orderBy(desc2(memories.confidence)).limit(20).all().map((row) => ({
|
|
2710
|
+
id: row.id,
|
|
2711
|
+
text: row.text,
|
|
2712
|
+
type: row.type,
|
|
2713
|
+
scope: row.scope,
|
|
2714
|
+
confidence: row.confidence
|
|
2715
|
+
}));
|
|
2716
|
+
const id = insertTaskIdempotent(db, {
|
|
2717
|
+
kind: "synthesize_repo",
|
|
2718
|
+
target: repo,
|
|
2719
|
+
repo,
|
|
2720
|
+
payload: {
|
|
2721
|
+
repo,
|
|
2722
|
+
memory_count: count,
|
|
2723
|
+
top_memories: topMemories
|
|
2724
|
+
}
|
|
2725
|
+
});
|
|
2726
|
+
if (id) enqueued += 1;
|
|
2727
|
+
}
|
|
2728
|
+
return enqueued;
|
|
2729
|
+
}
|
|
2730
|
+
async function enqueueMaintenanceTasks(db, config = DEFAULT_ENQUEUE_CONFIG) {
|
|
2731
|
+
const expired = sweepExpiredLeases(db);
|
|
2732
|
+
abandonOverAttemptTasks(db);
|
|
2733
|
+
const expiredPending = expireStalePendingTasks(db, config.summary_max_age_days * 2);
|
|
2734
|
+
const counts = {};
|
|
2735
|
+
counts.refine_candidate = produceRefineCandidateTasks(db, config);
|
|
2736
|
+
counts.summarize_history = produceSummarizeHistoryTasks(db, config);
|
|
2737
|
+
counts.summarize_session = produceSummarizeSessionTasks(db, config);
|
|
2738
|
+
counts.synthesize_repo = produceSynthesizeRepoTasks(db, config);
|
|
2739
|
+
counts.merge_duplicates = await produceMergeDuplicateTasks(db, config);
|
|
2740
|
+
const dropped = applyBacklogCaps(db, config);
|
|
2741
|
+
const total = Object.values(counts).reduce((sum, n) => sum + (n ?? 0), 0);
|
|
2742
|
+
return {
|
|
2743
|
+
tasks_enqueued: total,
|
|
2744
|
+
per_kind: counts,
|
|
2745
|
+
expired_leases_swept: expired,
|
|
2746
|
+
dropped_over_cap: dropped,
|
|
2747
|
+
expired_pending_tasks: expiredPending
|
|
2748
|
+
};
|
|
2749
|
+
}
|
|
2750
|
+
var MemoryScope2 = z2.enum(["session", "path", "repo", "team", "global"]);
|
|
2751
|
+
var RefineCandidateResult = z2.object({
|
|
2752
|
+
refined_text: z2.string().min(1).max(4e3),
|
|
2753
|
+
scope: MemoryScope2,
|
|
2754
|
+
path_scope: z2.string().max(512).nullable().optional(),
|
|
2755
|
+
rationale: z2.string().max(2e3).optional(),
|
|
2756
|
+
// Optional verdict — when present, the LLM may also reject a re-captured
|
|
2757
|
+
// fragment instead of refining it. Backwards-compatible: omitted means
|
|
2758
|
+
// "rewrite" (legacy refine behavior).
|
|
2759
|
+
verdict: z2.enum(["rewrite", "reject"]).optional()
|
|
2760
|
+
});
|
|
2761
|
+
var VerifyCaptureResult = z2.object({
|
|
2762
|
+
verdict: z2.enum(["save", "rewrite", "reject"]),
|
|
2763
|
+
cleaned_text: z2.string().min(1).max(4e3).optional(),
|
|
2764
|
+
scope: MemoryScope2.optional(),
|
|
2765
|
+
path_scope: z2.string().max(512).nullable().optional(),
|
|
2766
|
+
is_destructive_risky: z2.boolean().optional(),
|
|
2767
|
+
reason: z2.string().max(2e3).optional()
|
|
2768
|
+
});
|
|
2769
|
+
var SummarizeHistoryResult = z2.object({
|
|
2770
|
+
summary_text: z2.string().min(1).max(4e3),
|
|
2771
|
+
tags: z2.array(z2.string().max(64)).max(20).optional()
|
|
2772
|
+
});
|
|
2773
|
+
var MergeDuplicatesResult = z2.object({
|
|
2774
|
+
winner_id: z2.string().uuid(),
|
|
2775
|
+
winner_text: z2.string().min(1).max(4e3).optional(),
|
|
2776
|
+
winner_scope: MemoryScope2.optional(),
|
|
2777
|
+
winner_path_scope: z2.string().max(512).nullable().optional(),
|
|
2778
|
+
rationale: z2.string().max(2e3).optional()
|
|
2779
|
+
});
|
|
2780
|
+
var SummarizeSessionResult = z2.object({
|
|
2781
|
+
summary_text: z2.string().min(1).max(4e3)
|
|
2782
|
+
});
|
|
2783
|
+
var SynthesizeRepoResult = z2.object({
|
|
2784
|
+
summary_text: z2.string().min(1).max(8e3)
|
|
2785
|
+
});
|
|
2786
|
+
var RESULT_SCHEMAS = {
|
|
2787
|
+
verify_capture: VerifyCaptureResult,
|
|
2788
|
+
refine_candidate: RefineCandidateResult,
|
|
2789
|
+
summarize_history: SummarizeHistoryResult,
|
|
2790
|
+
merge_duplicates: MergeDuplicatesResult,
|
|
2791
|
+
summarize_session: SummarizeSessionResult,
|
|
2792
|
+
synthesize_repo: SynthesizeRepoResult
|
|
2793
|
+
};
|
|
2794
|
+
function payloadSummary(payload) {
|
|
2795
|
+
const out = {};
|
|
2796
|
+
for (const [k, v] of Object.entries(payload)) {
|
|
2797
|
+
if (typeof v === "string") {
|
|
2798
|
+
out[k] = v.length > 160 ? `${v.slice(0, 157)}...` : v;
|
|
2799
|
+
} else if (Array.isArray(v)) {
|
|
2800
|
+
out[k] = `array(${v.length})`;
|
|
2801
|
+
} else {
|
|
2802
|
+
out[k] = v;
|
|
2803
|
+
}
|
|
2804
|
+
}
|
|
2805
|
+
return out;
|
|
2806
|
+
}
|
|
2807
|
+
function peekTasks(db, options = {}) {
|
|
2808
|
+
const tasks = listTasks(db, {
|
|
2809
|
+
status: "pending",
|
|
2810
|
+
repo: options.repo,
|
|
2811
|
+
kinds: options.kinds,
|
|
2812
|
+
limit: Math.min(options.limit ?? 3, 10)
|
|
2813
|
+
});
|
|
2814
|
+
return tasks.map((t) => ({
|
|
2815
|
+
id: t.id,
|
|
2816
|
+
kind: t.kind,
|
|
2817
|
+
priority: t.priority,
|
|
2818
|
+
repo: t.repo,
|
|
2819
|
+
created_at: t.created_at,
|
|
2820
|
+
payload_summary: payloadSummary(t.payload)
|
|
2821
|
+
}));
|
|
2822
|
+
}
|
|
2823
|
+
var TaskClaimConflictError = class extends Error {
|
|
2824
|
+
constructor(taskId, reason) {
|
|
2825
|
+
super(`Task ${taskId} cannot be claimed: ${reason}`);
|
|
2826
|
+
this.taskId = taskId;
|
|
2827
|
+
this.reason = reason;
|
|
2828
|
+
this.name = "TaskClaimConflictError";
|
|
2829
|
+
}
|
|
2830
|
+
taskId;
|
|
2831
|
+
reason;
|
|
2832
|
+
};
|
|
2833
|
+
function claimTask(db, taskId, agent, leaseSeconds = DEFAULT_LEASE_SECONDS) {
|
|
2834
|
+
const now = /* @__PURE__ */ new Date();
|
|
2835
|
+
const expiresAt = new Date(now.getTime() + leaseSeconds * 1e3).toISOString();
|
|
2836
|
+
const nowIso = now.toISOString();
|
|
2837
|
+
const result = db.update(memoryMaintenanceTasks).set({
|
|
2838
|
+
status: "claimed",
|
|
2839
|
+
claimed_by: agent,
|
|
2840
|
+
claimed_at: nowIso,
|
|
2841
|
+
claim_expires_at: expiresAt
|
|
2842
|
+
}).where(and2(
|
|
2843
|
+
eq7(memoryMaintenanceTasks.id, taskId),
|
|
2844
|
+
eq7(memoryMaintenanceTasks.status, "pending")
|
|
2845
|
+
)).run();
|
|
2846
|
+
if (result.changes === 0) {
|
|
2847
|
+
const existing = getTask(db, taskId);
|
|
2848
|
+
throw new TaskClaimConflictError(taskId, existing ? "not-pending" : "not-found");
|
|
2849
|
+
}
|
|
2850
|
+
const task = getTask(db, taskId);
|
|
2851
|
+
return { task, lease_expires_at: expiresAt };
|
|
2852
|
+
}
|
|
2853
|
+
function submitTask(db, taskId, agent, result) {
|
|
2854
|
+
const existing = getTask(db, taskId);
|
|
2855
|
+
if (!existing) return { status: "rejected", task_id: taskId, reason: "not-found", attempts: 0, abandoned: false };
|
|
2856
|
+
if (existing.status !== "claimed") {
|
|
2857
|
+
return { status: "rejected", task_id: taskId, reason: `not-claimed (status=${existing.status})`, attempts: existing.attempts, abandoned: false };
|
|
2858
|
+
}
|
|
2859
|
+
if (existing.claimed_by !== agent) {
|
|
2860
|
+
return { status: "rejected", task_id: taskId, reason: "not-claim-holder", attempts: existing.attempts, abandoned: false };
|
|
2861
|
+
}
|
|
2862
|
+
const schema = RESULT_SCHEMAS[existing.kind];
|
|
2863
|
+
const parsed = schema.safeParse(result);
|
|
2864
|
+
if (!parsed.success) {
|
|
2865
|
+
const attempts = existing.attempts + 1;
|
|
2866
|
+
const abandoned = attempts >= existing.max_attempts;
|
|
2867
|
+
const now2 = (/* @__PURE__ */ new Date()).toISOString();
|
|
2868
|
+
db.update(memoryMaintenanceTasks).set({
|
|
2869
|
+
status: abandoned ? "abandoned" : "pending",
|
|
2870
|
+
claimed_by: null,
|
|
2871
|
+
claimed_at: null,
|
|
2872
|
+
claim_expires_at: null,
|
|
2873
|
+
attempts,
|
|
2874
|
+
failure_reason: parsed.error.issues.map((i) => `${i.path.join(".")}:${i.message}`).join("; ").slice(0, 500),
|
|
2875
|
+
completed_at: abandoned ? now2 : null
|
|
2876
|
+
}).where(eq7(memoryMaintenanceTasks.id, taskId)).run();
|
|
2877
|
+
return {
|
|
2878
|
+
status: "rejected",
|
|
2879
|
+
task_id: taskId,
|
|
2880
|
+
reason: `validation-failed: ${parsed.error.issues[0]?.message ?? "shape mismatch"}`,
|
|
2881
|
+
attempts,
|
|
2882
|
+
abandoned
|
|
2883
|
+
};
|
|
2884
|
+
}
|
|
2885
|
+
let applyOutcome;
|
|
2886
|
+
try {
|
|
2887
|
+
applyOutcome = applyTaskResult(db, existing, parsed.data);
|
|
2888
|
+
} catch (err) {
|
|
2889
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2890
|
+
const attempts = existing.attempts + 1;
|
|
2891
|
+
const code = err instanceof ApplyError ? err.code : "apply-error";
|
|
2892
|
+
const abandoned = code === "target-missing" || code === "invalid-state" || code === "unsupported-kind" || attempts >= existing.max_attempts;
|
|
2893
|
+
const now2 = (/* @__PURE__ */ new Date()).toISOString();
|
|
2894
|
+
db.update(memoryMaintenanceTasks).set({
|
|
2895
|
+
status: abandoned ? "abandoned" : "pending",
|
|
2896
|
+
claimed_by: null,
|
|
2897
|
+
claimed_at: null,
|
|
2898
|
+
claim_expires_at: null,
|
|
2899
|
+
attempts,
|
|
2900
|
+
failure_reason: `apply-failed: ${message}`.slice(0, 500),
|
|
2901
|
+
completed_at: abandoned ? now2 : null
|
|
2902
|
+
}).where(eq7(memoryMaintenanceTasks.id, taskId)).run();
|
|
2903
|
+
return {
|
|
2904
|
+
status: "rejected",
|
|
2905
|
+
task_id: taskId,
|
|
2906
|
+
reason: `apply-failed: ${message}`,
|
|
2907
|
+
attempts,
|
|
2908
|
+
abandoned
|
|
2909
|
+
};
|
|
2910
|
+
}
|
|
2911
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2912
|
+
db.update(memoryMaintenanceTasks).set({
|
|
2913
|
+
status: "completed",
|
|
2914
|
+
result: parsed.data,
|
|
2915
|
+
submitted_at: now,
|
|
2916
|
+
completed_at: now,
|
|
2917
|
+
failure_reason: null
|
|
2918
|
+
}).where(eq7(memoryMaintenanceTasks.id, taskId)).run();
|
|
2919
|
+
return {
|
|
2920
|
+
status: "applied",
|
|
2921
|
+
task_id: taskId,
|
|
2922
|
+
kind: existing.kind,
|
|
2923
|
+
target_id: applyOutcome.target_id,
|
|
2924
|
+
changed_fields: applyOutcome.changed_fields,
|
|
2925
|
+
audit_entry_id: applyOutcome.audit_entry_id
|
|
2926
|
+
};
|
|
2927
|
+
}
|
|
2928
|
+
function releaseTask(db, taskId, agent, reason) {
|
|
2929
|
+
const existing = getTask(db, taskId);
|
|
2930
|
+
if (!existing) return { status: "not-found" };
|
|
2931
|
+
if (existing.status !== "claimed" || existing.claimed_by !== agent) {
|
|
2932
|
+
return { status: "not-claimed" };
|
|
2933
|
+
}
|
|
2934
|
+
db.update(memoryMaintenanceTasks).set({
|
|
2935
|
+
status: "pending",
|
|
2936
|
+
claimed_by: null,
|
|
2937
|
+
claimed_at: null,
|
|
2938
|
+
claim_expires_at: null,
|
|
2939
|
+
failure_reason: reason ? reason.slice(0, 500) : null
|
|
2940
|
+
}).where(eq7(memoryMaintenanceTasks.id, taskId)).run();
|
|
2941
|
+
return { status: "released" };
|
|
2942
|
+
}
|
|
2943
|
+
|
|
2944
|
+
export {
|
|
2945
|
+
getEmbeddingCacheRoot,
|
|
2946
|
+
memoryDedupeKey,
|
|
2947
|
+
historySnippetDedupeKey,
|
|
2948
|
+
activityEventDedupeKey,
|
|
2949
|
+
hookCallDedupeKey,
|
|
2950
|
+
tagActivitySource,
|
|
2951
|
+
CONFIDENCE,
|
|
2952
|
+
RetrievalEvalCase,
|
|
2953
|
+
RetrievalEvalFile,
|
|
2954
|
+
resolveProvider,
|
|
2955
|
+
loadEmbeddingConfigFromEnv,
|
|
2956
|
+
getEmbeddingModelInfo,
|
|
2957
|
+
ensureEmbeddingProviderReady,
|
|
2958
|
+
projectEmbeddingToIndex,
|
|
2959
|
+
generateEmbedding,
|
|
2960
|
+
generateEmbeddings,
|
|
2961
|
+
queueMemoryEmbeddingSync,
|
|
2962
|
+
bootstrapEmbeddings,
|
|
2963
|
+
verifyEmbeddings,
|
|
2964
|
+
rebuildEmbeddingIndex,
|
|
2965
|
+
hybridSearch,
|
|
2966
|
+
findSemanticDuplicates,
|
|
2967
|
+
findSimilarRejectedExemplar,
|
|
2968
|
+
statusFromConfidence,
|
|
2969
|
+
createMemory,
|
|
2970
|
+
getMemory,
|
|
2971
|
+
queryMemories,
|
|
2972
|
+
listMemories,
|
|
2973
|
+
listRepos,
|
|
2974
|
+
promoteMemory,
|
|
2975
|
+
demoteMemory,
|
|
2976
|
+
demoteGlobalMemory,
|
|
2977
|
+
rejectMemory,
|
|
2978
|
+
confirmMemory,
|
|
2979
|
+
appendEvidence,
|
|
2980
|
+
updateMemoryCaptureContext,
|
|
2981
|
+
incrementMemoryRepetition,
|
|
2982
|
+
countDistinctCorrectionSessions,
|
|
2983
|
+
recordFeedback,
|
|
2984
|
+
getMemoryFeedback,
|
|
2985
|
+
getMemoryFeedbackSummaries,
|
|
2986
|
+
feedbackWeightedScore,
|
|
2987
|
+
recordAudit,
|
|
2988
|
+
recordAuditWithSnapshot,
|
|
2989
|
+
getAuditTrail,
|
|
2990
|
+
getRecentAudit,
|
|
2991
|
+
rollbackMemory,
|
|
2992
|
+
formatAuditTrail,
|
|
2993
|
+
DEFAULT_LEASE_SECONDS,
|
|
2994
|
+
DEFAULT_PRIORITIES,
|
|
2995
|
+
DEFAULT_ENQUEUE_CONFIG,
|
|
2996
|
+
targetKey,
|
|
2997
|
+
hasActiveTaskForTarget,
|
|
2998
|
+
insertTaskIdempotent,
|
|
2999
|
+
deleteTask,
|
|
3000
|
+
getTaskStats,
|
|
3001
|
+
getTask,
|
|
3002
|
+
listTasks,
|
|
3003
|
+
sweepExpiredLeases,
|
|
3004
|
+
expireStalePendingTasks,
|
|
3005
|
+
abandonOverAttemptTasks,
|
|
3006
|
+
applyBacklogCaps,
|
|
3007
|
+
enqueueVerifyCapture,
|
|
3008
|
+
produceRefineCandidateTasks,
|
|
3009
|
+
produceSummarizeHistoryTasks,
|
|
3010
|
+
snippetHasMeaningfulContent,
|
|
3011
|
+
produceMergeDuplicateTasks,
|
|
3012
|
+
produceSummarizeSessionTasks,
|
|
3013
|
+
produceSynthesizeRepoTasks,
|
|
3014
|
+
enqueueMaintenanceTasks,
|
|
3015
|
+
peekTasks,
|
|
3016
|
+
TaskClaimConflictError,
|
|
3017
|
+
claimTask,
|
|
3018
|
+
submitTask,
|
|
3019
|
+
releaseTask
|
|
3020
|
+
};
|
|
3021
|
+
//# sourceMappingURL=chunk-IILLSHLM.js.map
|