tinker-agent 2.11.0 → 2.12.0
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/CHANGELOG.md +19 -1
- package/README.md +31 -67
- package/package.json +1 -1
- package/src/agent/runtime-session-contracts.ts +2 -29
- package/src/agent/runtime-session.ts +13 -67
- package/src/cli/config.ts +0 -29
- package/src/cli/model-profiles.ts +0 -80
- package/src/cli/public-config-contract.ts +1 -82
- package/src/cli/tui-runner.tsx +2 -41
- package/src/events/observation-text-log.ts +7 -0
- package/src/memory/contracts.ts +0 -256
- package/src/memory/memory-create-tool.ts +3 -5
- package/src/memory/memory-files.ts +145 -0
- package/src/memory/memory-search-output.ts +23 -0
- package/src/memory/memory-search-tool.ts +79 -175
- package/src/memory/memory-search.ts +115 -0
- package/src/observation/observation-builder.ts +5 -1
- package/src/session/session-store-contracts.ts +0 -23
- package/src/session/session-store.ts +2 -103
- package/src/tools/registry.ts +15 -18
- package/src/tools/types.ts +12 -0
- package/src/tui/app.tsx +6 -4
- package/src/tui/event-store.ts +3 -1
- package/src/cli/tui-memory.ts +0 -69
- package/src/memory/embedding-client.ts +0 -105
- package/src/memory/memory-coordinator.ts +0 -1274
- package/src/memory/memory-delete-tool.ts +0 -88
- package/src/memory/memory-extractor.ts +0 -252
- package/src/memory/memory-get-tool.ts +0 -86
- package/src/memory/memory-log.ts +0 -88
- package/src/memory/memory-store.ts +0 -1133
- package/src/memory/memory-update-tool.ts +0 -142
- package/src/memory/vector.ts +0 -153
|
@@ -1,1274 +0,0 @@
|
|
|
1
|
-
import path from "node:path";
|
|
2
|
-
import type {
|
|
3
|
-
CompletedTurnHook,
|
|
4
|
-
CompletedTurnHookFailure,
|
|
5
|
-
CompletedTurnHookInput,
|
|
6
|
-
} from "../agent/runtime-session";
|
|
7
|
-
import { throwIfTurnCancelled } from "../agent/turn-cancellation";
|
|
8
|
-
import type { ToolCall } from "../agent/types";
|
|
9
|
-
import type { SessionId } from "../ids/runtime-id";
|
|
10
|
-
import type { ModelContextBudget } from "../model/model-context-profile";
|
|
11
|
-
import type { ModelClient } from "../model/model-client";
|
|
12
|
-
import type { CompletedTurnSnapshot } from "../session/session-store";
|
|
13
|
-
import type {
|
|
14
|
-
MemoryGetRawResult,
|
|
15
|
-
MemoryCreateRawResult,
|
|
16
|
-
MemoryDeleteRawResult,
|
|
17
|
-
MemorySearchRawResult,
|
|
18
|
-
MemoryUpdateRawResult,
|
|
19
|
-
ToolExecutor,
|
|
20
|
-
} from "../tools/types";
|
|
21
|
-
import {
|
|
22
|
-
boundedMemoryError,
|
|
23
|
-
MAX_SEARCH_RESULT_SUMMARY_BYTES,
|
|
24
|
-
MEMORY_CREATE_TOOL_NAME,
|
|
25
|
-
MEMORY_DELETE_TOOL_NAME,
|
|
26
|
-
MEMORY_EXTRACTION_QUEUE_CAPACITY,
|
|
27
|
-
memoryErrorCode,
|
|
28
|
-
MEMORY_GET_TOOL_NAME,
|
|
29
|
-
MEMORY_RECALL_CANDIDATE_LIMIT,
|
|
30
|
-
MEMORY_RRF_K,
|
|
31
|
-
MEMORY_SEARCH_DIAGNOSTIC_VECTOR_SCORES_LIMIT,
|
|
32
|
-
MEMORY_SEARCH_LIMIT,
|
|
33
|
-
MEMORY_SEARCH_TOOL_NAME,
|
|
34
|
-
MEMORY_UPDATE_TOOL_NAME,
|
|
35
|
-
MemoryError,
|
|
36
|
-
boundedMemoryErrorDetail,
|
|
37
|
-
truncateUtf8,
|
|
38
|
-
type MemoryEmbeddingConfig,
|
|
39
|
-
type MemoryExtractionDiagnostic,
|
|
40
|
-
type MemoryExtractionRejectedCounts,
|
|
41
|
-
type MemoryFtsMatch,
|
|
42
|
-
type MemoryGetDiagnostic,
|
|
43
|
-
type MemoryHybridMatch,
|
|
44
|
-
type MemoryMutationDiagnostic,
|
|
45
|
-
type MemoryPaths,
|
|
46
|
-
type MemoryRecallDegraded,
|
|
47
|
-
type MemoryRecallPath,
|
|
48
|
-
type MemorySearchDiagnostic,
|
|
49
|
-
type MemorySearchMatch,
|
|
50
|
-
type StoredMemorySummary,
|
|
51
|
-
} from "./contracts";
|
|
52
|
-
import {
|
|
53
|
-
OpenAICompatibleEmbeddingClient,
|
|
54
|
-
type MemoryEmbeddingClient,
|
|
55
|
-
} from "./embedding-client";
|
|
56
|
-
import {
|
|
57
|
-
MemoryExtractionOutputError,
|
|
58
|
-
MemoryExtractionRequestError,
|
|
59
|
-
MemoryExtractionSkippedError,
|
|
60
|
-
MemoryExtractor,
|
|
61
|
-
type MemoryExtractionResult,
|
|
62
|
-
} from "./memory-extractor";
|
|
63
|
-
import { ExtractedMemoryLog, MemoryLog } from "./memory-log";
|
|
64
|
-
import { createMemoryGetToolExecutor } from "./memory-get-tool";
|
|
65
|
-
import { createMemoryCreateToolExecutor } from "./memory-create-tool";
|
|
66
|
-
import { createMemoryDeleteToolExecutor } from "./memory-delete-tool";
|
|
67
|
-
import { createMemorySearchToolExecutor } from "./memory-search-tool";
|
|
68
|
-
import { createMemoryUpdateToolExecutor } from "./memory-update-tool";
|
|
69
|
-
import { MemoryStore, resolveMemoryPaths } from "./memory-store";
|
|
70
|
-
import { normalizeEmbedding } from "./vector";
|
|
71
|
-
|
|
72
|
-
type MemoryWorkerTask = {
|
|
73
|
-
readonly workspaceRoot: string;
|
|
74
|
-
readonly sessionId: SessionId;
|
|
75
|
-
readonly turnId: CompletedTurnHookInput["turnId"];
|
|
76
|
-
readonly extractionEvidenceText: string;
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
type ActiveMemoryTask = {
|
|
80
|
-
readonly task: MemoryWorkerTask;
|
|
81
|
-
readonly controller: AbortController;
|
|
82
|
-
completion?: Promise<void>;
|
|
83
|
-
};
|
|
84
|
-
|
|
85
|
-
type MemoryToolSource = {
|
|
86
|
-
readonly workspaceRoot: string;
|
|
87
|
-
readonly sessionId: SessionId;
|
|
88
|
-
};
|
|
89
|
-
|
|
90
|
-
const MEMORY_TOOL_NAMES: readonly string[] = Object.freeze([
|
|
91
|
-
MEMORY_SEARCH_TOOL_NAME,
|
|
92
|
-
MEMORY_GET_TOOL_NAME,
|
|
93
|
-
MEMORY_CREATE_TOOL_NAME,
|
|
94
|
-
MEMORY_UPDATE_TOOL_NAME,
|
|
95
|
-
MEMORY_DELETE_TOOL_NAME,
|
|
96
|
-
]);
|
|
97
|
-
|
|
98
|
-
export type CreateMemoryCoordinatorInput = {
|
|
99
|
-
readonly paths?: MemoryPaths;
|
|
100
|
-
readonly embedding: MemoryEmbeddingConfig;
|
|
101
|
-
readonly extractionContextBudget: ModelContextBudget;
|
|
102
|
-
readonly createExtractionClient: () => ModelClient;
|
|
103
|
-
readonly createEmbeddingClient?: () => MemoryEmbeddingClient;
|
|
104
|
-
readonly clock?: () => string;
|
|
105
|
-
};
|
|
106
|
-
|
|
107
|
-
export class MemoryCoordinator implements CompletedTurnHook {
|
|
108
|
-
private accepting = true;
|
|
109
|
-
private active?: ActiveMemoryTask;
|
|
110
|
-
private pending: MemoryWorkerTask[] = [];
|
|
111
|
-
|
|
112
|
-
private constructor(
|
|
113
|
-
private readonly store: MemoryStore,
|
|
114
|
-
private readonly extractor: MemoryExtractor,
|
|
115
|
-
private readonly embeddingClient: MemoryEmbeddingClient,
|
|
116
|
-
private readonly log: MemoryLog,
|
|
117
|
-
private readonly extractedLog: ExtractedMemoryLog,
|
|
118
|
-
private readonly embeddingDimensions: number,
|
|
119
|
-
private readonly clock: () => string,
|
|
120
|
-
) {}
|
|
121
|
-
|
|
122
|
-
static async create(input: CreateMemoryCoordinatorInput): Promise<MemoryCoordinator> {
|
|
123
|
-
const paths = input.paths ?? resolveMemoryPaths();
|
|
124
|
-
const log = new MemoryLog(paths.log);
|
|
125
|
-
const extractedLog = new ExtractedMemoryLog(paths.extractedLog);
|
|
126
|
-
const store = await MemoryStore.open({
|
|
127
|
-
paths,
|
|
128
|
-
embedding: input.embedding,
|
|
129
|
-
clock: input.clock,
|
|
130
|
-
});
|
|
131
|
-
try {
|
|
132
|
-
const extractionClient = input.createExtractionClient();
|
|
133
|
-
const embeddingClient =
|
|
134
|
-
input.createEmbeddingClient?.() ??
|
|
135
|
-
new OpenAICompatibleEmbeddingClient(input.embedding);
|
|
136
|
-
return new MemoryCoordinator(
|
|
137
|
-
store,
|
|
138
|
-
new MemoryExtractor(extractionClient, input.extractionContextBudget),
|
|
139
|
-
embeddingClient,
|
|
140
|
-
log,
|
|
141
|
-
extractedLog,
|
|
142
|
-
input.embedding.dimensions,
|
|
143
|
-
input.clock ?? (() => new Date().toISOString()),
|
|
144
|
-
);
|
|
145
|
-
} catch (error) {
|
|
146
|
-
store.close();
|
|
147
|
-
throw error;
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
enqueue(input: CompletedTurnHookInput): void {
|
|
152
|
-
if (!this.accepting) {
|
|
153
|
-
throw new MemoryError(
|
|
154
|
-
"completed_turn_enqueue_failed",
|
|
155
|
-
"Memory coordinator is shutting down.",
|
|
156
|
-
);
|
|
157
|
-
}
|
|
158
|
-
const extractionEvidenceText = buildExtractionEvidenceText(
|
|
159
|
-
input.workspaceRoot,
|
|
160
|
-
input.snapshot,
|
|
161
|
-
);
|
|
162
|
-
const task = Object.freeze({
|
|
163
|
-
workspaceRoot: input.workspaceRoot,
|
|
164
|
-
sessionId: input.sessionId,
|
|
165
|
-
turnId: input.turnId,
|
|
166
|
-
extractionEvidenceText,
|
|
167
|
-
});
|
|
168
|
-
if (this.active === undefined) {
|
|
169
|
-
this.start(task);
|
|
170
|
-
return;
|
|
171
|
-
}
|
|
172
|
-
if (this.pending.length >= MEMORY_EXTRACTION_QUEUE_CAPACITY) {
|
|
173
|
-
this.pending.shift();
|
|
174
|
-
}
|
|
175
|
-
this.pending.push(task);
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
recordFailure(input: CompletedTurnHookFailure): void {
|
|
179
|
-
void this.log.append(
|
|
180
|
-
extractionDiagnostic({
|
|
181
|
-
clock: this.clock,
|
|
182
|
-
outcome: "failed",
|
|
183
|
-
reason: input.reason,
|
|
184
|
-
workspace: input.workspaceRoot,
|
|
185
|
-
turnId: input.turnId,
|
|
186
|
-
ms: 0,
|
|
187
|
-
}),
|
|
188
|
-
);
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
createSearchToolExecutor(input: {
|
|
192
|
-
readonly workspaceRoot: string;
|
|
193
|
-
readonly sessionId: SessionId;
|
|
194
|
-
}): ToolExecutor {
|
|
195
|
-
return createMemorySearchToolExecutor({
|
|
196
|
-
search: (query, keywords, signal) => this.search(query, keywords, signal, input),
|
|
197
|
-
recordInvalidCall: (invalid) => this.recordInvalidSearch(invalid, input),
|
|
198
|
-
});
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
createGetToolExecutor(input: {
|
|
202
|
-
readonly workspaceRoot: string;
|
|
203
|
-
readonly sessionId: SessionId;
|
|
204
|
-
}): ToolExecutor {
|
|
205
|
-
return createMemoryGetToolExecutor({
|
|
206
|
-
get: (memoryId, signal) => this.get(memoryId, signal, input),
|
|
207
|
-
recordInvalidCall: () => this.recordInvalidGet(input),
|
|
208
|
-
});
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
createCreateToolExecutor(input: MemoryToolSource): ToolExecutor {
|
|
212
|
-
return createMemoryCreateToolExecutor({
|
|
213
|
-
create: (text, summary, call, signal) =>
|
|
214
|
-
this.createMemory(text, summary, call, signal, input),
|
|
215
|
-
recordInvalidCall: (call) => this.recordInvalidMutation("create", call, input),
|
|
216
|
-
});
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
createUpdateToolExecutor(input: MemoryToolSource): ToolExecutor {
|
|
220
|
-
return createMemoryUpdateToolExecutor({
|
|
221
|
-
update: (memoryId, text, summary, call, signal) =>
|
|
222
|
-
this.updateMemory(memoryId, text, summary, call, signal, input),
|
|
223
|
-
recordInvalidCall: (call) => this.recordInvalidMutation("update", call, input),
|
|
224
|
-
});
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
createDeleteToolExecutor(input: MemoryToolSource): ToolExecutor {
|
|
228
|
-
return createMemoryDeleteToolExecutor({
|
|
229
|
-
delete: (memoryId, call, signal) =>
|
|
230
|
-
this.deleteMemory(memoryId, call, signal, input),
|
|
231
|
-
recordInvalidCall: (call) => this.recordInvalidMutation("delete", call, input),
|
|
232
|
-
});
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
listStoredMemories(): readonly StoredMemorySummary[] {
|
|
236
|
-
return this.store.listStoredMemories();
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
dispose(): void {
|
|
240
|
-
if (!this.accepting) {
|
|
241
|
-
return;
|
|
242
|
-
}
|
|
243
|
-
this.accepting = false;
|
|
244
|
-
this.pending = [];
|
|
245
|
-
this.active?.controller.abort(
|
|
246
|
-
new MemoryError(
|
|
247
|
-
"memory_coordinator_disposed",
|
|
248
|
-
"Memory coordinator is shutting down.",
|
|
249
|
-
),
|
|
250
|
-
);
|
|
251
|
-
this.store.close();
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
private start(task: MemoryWorkerTask): void {
|
|
255
|
-
const active: ActiveMemoryTask = {
|
|
256
|
-
task,
|
|
257
|
-
controller: new AbortController(),
|
|
258
|
-
};
|
|
259
|
-
this.active = active;
|
|
260
|
-
active.completion = Promise.resolve()
|
|
261
|
-
.then(() => this.processTask(task, active.controller.signal))
|
|
262
|
-
.catch(() => undefined)
|
|
263
|
-
.finally(() => {
|
|
264
|
-
if (this.active !== active) {
|
|
265
|
-
return;
|
|
266
|
-
}
|
|
267
|
-
this.active = undefined;
|
|
268
|
-
const next = this.accepting ? this.pending.shift() : undefined;
|
|
269
|
-
if (next !== undefined) {
|
|
270
|
-
this.start(next);
|
|
271
|
-
}
|
|
272
|
-
});
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
private async processTask(
|
|
276
|
-
task: MemoryWorkerTask,
|
|
277
|
-
signal: AbortSignal,
|
|
278
|
-
): Promise<void> {
|
|
279
|
-
const startedAt = performance.now();
|
|
280
|
-
let inputTokens = 0;
|
|
281
|
-
let returned = 0;
|
|
282
|
-
let rejected = emptyRejectedCounts();
|
|
283
|
-
|
|
284
|
-
let extraction: MemoryExtractionResult;
|
|
285
|
-
try {
|
|
286
|
-
extraction = await this.extractor.extract(task.extractionEvidenceText, signal);
|
|
287
|
-
inputTokens = extraction.inputTokens;
|
|
288
|
-
returned = extraction.memory === null ? 0 : 1;
|
|
289
|
-
} catch (error) {
|
|
290
|
-
if (error instanceof MemoryExtractionSkippedError) {
|
|
291
|
-
inputTokens = error.inputTokens;
|
|
292
|
-
await this.log.append(
|
|
293
|
-
extractionDiagnostic({
|
|
294
|
-
clock: this.clock,
|
|
295
|
-
outcome: "skipped",
|
|
296
|
-
reason: error.code,
|
|
297
|
-
workspace: task.workspaceRoot,
|
|
298
|
-
turnId: task.turnId,
|
|
299
|
-
inputTokens,
|
|
300
|
-
ms: elapsedMs(startedAt),
|
|
301
|
-
detail: boundedMemoryErrorDetail(error),
|
|
302
|
-
}),
|
|
303
|
-
);
|
|
304
|
-
return;
|
|
305
|
-
}
|
|
306
|
-
if (error instanceof MemoryExtractionOutputError) {
|
|
307
|
-
inputTokens = error.inputTokens;
|
|
308
|
-
returned = error.returned;
|
|
309
|
-
rejected = Object.freeze({ ...rejected, invalid: Math.max(1, returned) });
|
|
310
|
-
} else if (error instanceof MemoryExtractionRequestError) {
|
|
311
|
-
inputTokens = error.inputTokens;
|
|
312
|
-
}
|
|
313
|
-
await this.log.append(
|
|
314
|
-
extractionDiagnostic({
|
|
315
|
-
clock: this.clock,
|
|
316
|
-
outcome: signal.aborted ? "skipped" : "failed",
|
|
317
|
-
reason: signal.aborted
|
|
318
|
-
? "extraction_cancelled"
|
|
319
|
-
: memoryErrorCode(error, "extraction_model_failed"),
|
|
320
|
-
workspace: task.workspaceRoot,
|
|
321
|
-
turnId: task.turnId,
|
|
322
|
-
inputTokens,
|
|
323
|
-
returned,
|
|
324
|
-
rejected,
|
|
325
|
-
ms: elapsedMs(startedAt),
|
|
326
|
-
detail: boundedMemoryErrorDetail(error),
|
|
327
|
-
}),
|
|
328
|
-
);
|
|
329
|
-
return;
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
const candidate = extraction.memory;
|
|
333
|
-
if (candidate === null) {
|
|
334
|
-
await this.log.append(
|
|
335
|
-
extractionDiagnostic({
|
|
336
|
-
clock: this.clock,
|
|
337
|
-
outcome: "ok",
|
|
338
|
-
reason: null,
|
|
339
|
-
workspace: task.workspaceRoot,
|
|
340
|
-
turnId: task.turnId,
|
|
341
|
-
inputTokens,
|
|
342
|
-
returned,
|
|
343
|
-
ms: elapsedMs(startedAt),
|
|
344
|
-
}),
|
|
345
|
-
);
|
|
346
|
-
return;
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
if (
|
|
350
|
-
containsSensitiveMemory(candidate.text) ||
|
|
351
|
-
containsSensitiveMemory(candidate.summary)
|
|
352
|
-
) {
|
|
353
|
-
rejected = Object.freeze({ ...rejected, secret: 1 });
|
|
354
|
-
await this.log.append(
|
|
355
|
-
extractionDiagnostic({
|
|
356
|
-
clock: this.clock,
|
|
357
|
-
outcome: "ok",
|
|
358
|
-
reason: null,
|
|
359
|
-
workspace: task.workspaceRoot,
|
|
360
|
-
turnId: task.turnId,
|
|
361
|
-
inputTokens,
|
|
362
|
-
returned,
|
|
363
|
-
rejected,
|
|
364
|
-
ms: elapsedMs(startedAt),
|
|
365
|
-
}),
|
|
366
|
-
);
|
|
367
|
-
return;
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
let embedding: Float32Array;
|
|
371
|
-
try {
|
|
372
|
-
const rawEmbeddings = await this.embeddingClient.embed([candidate.text], signal);
|
|
373
|
-
if (rawEmbeddings.length !== 1 || rawEmbeddings[0] === undefined) {
|
|
374
|
-
throw new MemoryError(
|
|
375
|
-
"memory_embedding_response_invalid",
|
|
376
|
-
"Embedding response count does not match the memory candidate count.",
|
|
377
|
-
);
|
|
378
|
-
}
|
|
379
|
-
embedding = normalizeEmbedding(rawEmbeddings[0], this.embeddingDimensions);
|
|
380
|
-
} catch (error) {
|
|
381
|
-
rejected = Object.freeze({
|
|
382
|
-
...rejected,
|
|
383
|
-
embedding: 1,
|
|
384
|
-
});
|
|
385
|
-
await this.log.append(
|
|
386
|
-
extractionDiagnostic({
|
|
387
|
-
clock: this.clock,
|
|
388
|
-
outcome: signal.aborted ? "skipped" : "failed",
|
|
389
|
-
reason: signal.aborted
|
|
390
|
-
? "extraction_cancelled"
|
|
391
|
-
: memoryErrorCode(error, "memory_embedding_failed"),
|
|
392
|
-
workspace: task.workspaceRoot,
|
|
393
|
-
turnId: task.turnId,
|
|
394
|
-
inputTokens,
|
|
395
|
-
returned,
|
|
396
|
-
rejected,
|
|
397
|
-
ms: elapsedMs(startedAt),
|
|
398
|
-
detail: boundedMemoryErrorDetail(error),
|
|
399
|
-
}),
|
|
400
|
-
);
|
|
401
|
-
return;
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
try {
|
|
405
|
-
const result = this.store.insertBatch({
|
|
406
|
-
workspaceRoot: task.workspaceRoot,
|
|
407
|
-
sessionId: task.sessionId,
|
|
408
|
-
turnId: task.turnId,
|
|
409
|
-
candidates: [
|
|
410
|
-
{
|
|
411
|
-
text: candidate.text,
|
|
412
|
-
summary: candidate.summary,
|
|
413
|
-
embedding,
|
|
414
|
-
},
|
|
415
|
-
],
|
|
416
|
-
});
|
|
417
|
-
rejected = Object.freeze({
|
|
418
|
-
...rejected,
|
|
419
|
-
duplicate: result.duplicate,
|
|
420
|
-
});
|
|
421
|
-
if (result.inserted.length > 0) {
|
|
422
|
-
await this.extractedLog.append({
|
|
423
|
-
at: result.inserted[0].createdAt,
|
|
424
|
-
workspace: task.workspaceRoot,
|
|
425
|
-
turnId: task.turnId,
|
|
426
|
-
memories: result.inserted,
|
|
427
|
-
});
|
|
428
|
-
}
|
|
429
|
-
await this.log.append(
|
|
430
|
-
extractionDiagnostic({
|
|
431
|
-
clock: this.clock,
|
|
432
|
-
outcome: "ok",
|
|
433
|
-
reason: null,
|
|
434
|
-
workspace: task.workspaceRoot,
|
|
435
|
-
turnId: task.turnId,
|
|
436
|
-
inputTokens,
|
|
437
|
-
returned,
|
|
438
|
-
written: result.written,
|
|
439
|
-
rejected,
|
|
440
|
-
ms: elapsedMs(startedAt),
|
|
441
|
-
}),
|
|
442
|
-
);
|
|
443
|
-
} catch (error) {
|
|
444
|
-
await this.log.append(
|
|
445
|
-
extractionDiagnostic({
|
|
446
|
-
clock: this.clock,
|
|
447
|
-
outcome: signal.aborted ? "skipped" : "failed",
|
|
448
|
-
reason: signal.aborted
|
|
449
|
-
? "extraction_cancelled"
|
|
450
|
-
: memoryErrorCode(error, "memory_write_failed"),
|
|
451
|
-
workspace: task.workspaceRoot,
|
|
452
|
-
turnId: task.turnId,
|
|
453
|
-
inputTokens,
|
|
454
|
-
returned,
|
|
455
|
-
rejected,
|
|
456
|
-
ms: elapsedMs(startedAt),
|
|
457
|
-
detail: boundedMemoryErrorDetail(error),
|
|
458
|
-
}),
|
|
459
|
-
);
|
|
460
|
-
}
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
private async createMemory(
|
|
464
|
-
text: string,
|
|
465
|
-
summary: string,
|
|
466
|
-
call: ToolCall,
|
|
467
|
-
signal: AbortSignal,
|
|
468
|
-
source: MemoryToolSource,
|
|
469
|
-
): Promise<MemoryCreateRawResult> {
|
|
470
|
-
const startedAt = performance.now();
|
|
471
|
-
try {
|
|
472
|
-
throwIfTurnCancelled(signal);
|
|
473
|
-
if (containsSensitiveMemory(text) || containsSensitiveMemory(summary)) {
|
|
474
|
-
await this.logMutation({
|
|
475
|
-
kind: "create",
|
|
476
|
-
outcome: "failed",
|
|
477
|
-
reason: "memory_sensitive_rejected",
|
|
478
|
-
source,
|
|
479
|
-
call,
|
|
480
|
-
memoryId: null,
|
|
481
|
-
startedAt,
|
|
482
|
-
});
|
|
483
|
-
return {
|
|
484
|
-
ok: false,
|
|
485
|
-
error:
|
|
486
|
-
"MemoryCreate rejected content that may contain sensitive information.",
|
|
487
|
-
};
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
let embedding: Float32Array;
|
|
491
|
-
try {
|
|
492
|
-
embedding = await this.embedMutationText(text, signal);
|
|
493
|
-
} catch (error) {
|
|
494
|
-
if (signal.aborted) {
|
|
495
|
-
throw error;
|
|
496
|
-
}
|
|
497
|
-
await this.logMutation({
|
|
498
|
-
kind: "create",
|
|
499
|
-
outcome: "failed",
|
|
500
|
-
reason: "memory_embedding_failed",
|
|
501
|
-
source,
|
|
502
|
-
call,
|
|
503
|
-
memoryId: null,
|
|
504
|
-
startedAt,
|
|
505
|
-
});
|
|
506
|
-
return {
|
|
507
|
-
ok: false,
|
|
508
|
-
error: "MemoryCreate could not generate a memory embedding.",
|
|
509
|
-
};
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
throwIfTurnCancelled(signal);
|
|
513
|
-
const result = this.store.insertBatch({
|
|
514
|
-
workspaceRoot: source.workspaceRoot,
|
|
515
|
-
sessionId: source.sessionId,
|
|
516
|
-
turnId: call.turnId,
|
|
517
|
-
candidates: [{ text, summary, embedding }],
|
|
518
|
-
});
|
|
519
|
-
const inserted = result.inserted[0];
|
|
520
|
-
const existing =
|
|
521
|
-
inserted === undefined ? this.store.getByTextHash(text) : undefined;
|
|
522
|
-
const resolved = inserted ?? existing;
|
|
523
|
-
if (resolved === undefined) {
|
|
524
|
-
throw new MemoryError(
|
|
525
|
-
"memory_write_failed",
|
|
526
|
-
"MemoryCreate did not produce a stored record.",
|
|
527
|
-
);
|
|
528
|
-
}
|
|
529
|
-
const memoryId = resolved.memoryId;
|
|
530
|
-
const createdAt = resolved.createdAt;
|
|
531
|
-
const status = inserted === undefined ? "already_exists" : "created";
|
|
532
|
-
await this.logMutation({
|
|
533
|
-
kind: "create",
|
|
534
|
-
outcome: "ok",
|
|
535
|
-
reason: null,
|
|
536
|
-
source,
|
|
537
|
-
call,
|
|
538
|
-
memoryId,
|
|
539
|
-
startedAt,
|
|
540
|
-
});
|
|
541
|
-
return { ok: true, status, memoryId, createdAt };
|
|
542
|
-
} catch (error) {
|
|
543
|
-
if (signal.aborted) {
|
|
544
|
-
await this.logMutation({
|
|
545
|
-
kind: "create",
|
|
546
|
-
outcome: "skipped",
|
|
547
|
-
reason: "memory_create_cancelled",
|
|
548
|
-
source,
|
|
549
|
-
call,
|
|
550
|
-
memoryId: null,
|
|
551
|
-
startedAt,
|
|
552
|
-
});
|
|
553
|
-
throw error;
|
|
554
|
-
}
|
|
555
|
-
await this.logMutation({
|
|
556
|
-
kind: "create",
|
|
557
|
-
outcome: "failed",
|
|
558
|
-
reason: memoryErrorCode(error, "memory_create_failed"),
|
|
559
|
-
source,
|
|
560
|
-
call,
|
|
561
|
-
memoryId: null,
|
|
562
|
-
startedAt,
|
|
563
|
-
});
|
|
564
|
-
return { ok: false, error: boundedMemoryError(error) };
|
|
565
|
-
}
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
private async updateMemory(
|
|
569
|
-
memoryId: string,
|
|
570
|
-
text: string,
|
|
571
|
-
summary: string,
|
|
572
|
-
call: ToolCall,
|
|
573
|
-
signal: AbortSignal,
|
|
574
|
-
source: MemoryToolSource,
|
|
575
|
-
): Promise<MemoryUpdateRawResult> {
|
|
576
|
-
const startedAt = performance.now();
|
|
577
|
-
try {
|
|
578
|
-
throwIfTurnCancelled(signal);
|
|
579
|
-
if (containsSensitiveMemory(text) || containsSensitiveMemory(summary)) {
|
|
580
|
-
await this.logMutation({
|
|
581
|
-
kind: "update",
|
|
582
|
-
outcome: "failed",
|
|
583
|
-
reason: "memory_sensitive_rejected",
|
|
584
|
-
source,
|
|
585
|
-
call,
|
|
586
|
-
memoryId,
|
|
587
|
-
startedAt,
|
|
588
|
-
});
|
|
589
|
-
return {
|
|
590
|
-
ok: false,
|
|
591
|
-
error:
|
|
592
|
-
"MemoryUpdate rejected content that may contain sensitive information.",
|
|
593
|
-
};
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
const target = this.store.getByIdForMutation(memoryId);
|
|
597
|
-
if (target === undefined) {
|
|
598
|
-
await this.logMutation({
|
|
599
|
-
kind: "update",
|
|
600
|
-
outcome: "failed",
|
|
601
|
-
reason: "memory_not_found",
|
|
602
|
-
source,
|
|
603
|
-
call,
|
|
604
|
-
memoryId,
|
|
605
|
-
startedAt,
|
|
606
|
-
});
|
|
607
|
-
return {
|
|
608
|
-
ok: false,
|
|
609
|
-
code: "memory_not_found",
|
|
610
|
-
error: `No global memory exists with id ${memoryId}.`,
|
|
611
|
-
};
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
let embedding = target.embedding;
|
|
615
|
-
if (text !== target.text) {
|
|
616
|
-
try {
|
|
617
|
-
embedding = await this.embedMutationText(text, signal);
|
|
618
|
-
} catch (error) {
|
|
619
|
-
if (signal.aborted) {
|
|
620
|
-
throw error;
|
|
621
|
-
}
|
|
622
|
-
await this.logMutation({
|
|
623
|
-
kind: "update",
|
|
624
|
-
outcome: "failed",
|
|
625
|
-
reason: "memory_embedding_failed",
|
|
626
|
-
source,
|
|
627
|
-
call,
|
|
628
|
-
memoryId,
|
|
629
|
-
startedAt,
|
|
630
|
-
});
|
|
631
|
-
return {
|
|
632
|
-
ok: false,
|
|
633
|
-
error: "MemoryUpdate could not generate a memory embedding.",
|
|
634
|
-
};
|
|
635
|
-
}
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
throwIfTurnCancelled(signal);
|
|
639
|
-
const result = this.store.updateMemory({
|
|
640
|
-
memoryId,
|
|
641
|
-
text,
|
|
642
|
-
summary,
|
|
643
|
-
embedding,
|
|
644
|
-
});
|
|
645
|
-
if (!result.ok && result.code === "memory_not_found") {
|
|
646
|
-
await this.logMutation({
|
|
647
|
-
kind: "update",
|
|
648
|
-
outcome: "failed",
|
|
649
|
-
reason: "memory_not_found",
|
|
650
|
-
source,
|
|
651
|
-
call,
|
|
652
|
-
memoryId,
|
|
653
|
-
startedAt,
|
|
654
|
-
});
|
|
655
|
-
return {
|
|
656
|
-
ok: false,
|
|
657
|
-
code: "memory_not_found",
|
|
658
|
-
error: `No global memory exists with id ${memoryId}.`,
|
|
659
|
-
};
|
|
660
|
-
}
|
|
661
|
-
if (!result.ok && result.code === "memory_duplicate") {
|
|
662
|
-
await this.logMutation({
|
|
663
|
-
kind: "update",
|
|
664
|
-
outcome: "failed",
|
|
665
|
-
reason: "memory_duplicate",
|
|
666
|
-
source,
|
|
667
|
-
call,
|
|
668
|
-
memoryId,
|
|
669
|
-
startedAt,
|
|
670
|
-
});
|
|
671
|
-
return {
|
|
672
|
-
ok: false,
|
|
673
|
-
code: "memory_duplicate",
|
|
674
|
-
conflictMemoryId: result.conflictMemoryId,
|
|
675
|
-
error: `Another global memory already has the replacement text (memoryId ${result.conflictMemoryId}).`,
|
|
676
|
-
};
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
await this.logMutation({
|
|
680
|
-
kind: "update",
|
|
681
|
-
outcome: "ok",
|
|
682
|
-
reason: null,
|
|
683
|
-
source,
|
|
684
|
-
call,
|
|
685
|
-
memoryId,
|
|
686
|
-
startedAt,
|
|
687
|
-
});
|
|
688
|
-
return { ok: true, status: "updated", memoryId };
|
|
689
|
-
} catch (error) {
|
|
690
|
-
if (signal.aborted) {
|
|
691
|
-
await this.logMutation({
|
|
692
|
-
kind: "update",
|
|
693
|
-
outcome: "skipped",
|
|
694
|
-
reason: "memory_update_cancelled",
|
|
695
|
-
source,
|
|
696
|
-
call,
|
|
697
|
-
memoryId,
|
|
698
|
-
startedAt,
|
|
699
|
-
});
|
|
700
|
-
throw error;
|
|
701
|
-
}
|
|
702
|
-
await this.logMutation({
|
|
703
|
-
kind: "update",
|
|
704
|
-
outcome: "failed",
|
|
705
|
-
reason: memoryErrorCode(error, "memory_update_failed"),
|
|
706
|
-
source,
|
|
707
|
-
call,
|
|
708
|
-
memoryId,
|
|
709
|
-
startedAt,
|
|
710
|
-
});
|
|
711
|
-
return { ok: false, error: boundedMemoryError(error) };
|
|
712
|
-
}
|
|
713
|
-
}
|
|
714
|
-
|
|
715
|
-
private async deleteMemory(
|
|
716
|
-
memoryId: string,
|
|
717
|
-
call: ToolCall,
|
|
718
|
-
signal: AbortSignal,
|
|
719
|
-
source: MemoryToolSource,
|
|
720
|
-
): Promise<MemoryDeleteRawResult> {
|
|
721
|
-
const startedAt = performance.now();
|
|
722
|
-
try {
|
|
723
|
-
throwIfTurnCancelled(signal);
|
|
724
|
-
const result = this.store.deleteMemory(memoryId);
|
|
725
|
-
if (!result.ok) {
|
|
726
|
-
await this.logMutation({
|
|
727
|
-
kind: "delete",
|
|
728
|
-
outcome: "failed",
|
|
729
|
-
reason: "memory_not_found",
|
|
730
|
-
source,
|
|
731
|
-
call,
|
|
732
|
-
memoryId,
|
|
733
|
-
startedAt,
|
|
734
|
-
});
|
|
735
|
-
return {
|
|
736
|
-
ok: false,
|
|
737
|
-
code: "memory_not_found",
|
|
738
|
-
error: `No global memory exists with id ${memoryId}.`,
|
|
739
|
-
};
|
|
740
|
-
}
|
|
741
|
-
await this.logMutation({
|
|
742
|
-
kind: "delete",
|
|
743
|
-
outcome: "ok",
|
|
744
|
-
reason: null,
|
|
745
|
-
source,
|
|
746
|
-
call,
|
|
747
|
-
memoryId,
|
|
748
|
-
startedAt,
|
|
749
|
-
});
|
|
750
|
-
return { ok: true, status: "deleted", memoryId };
|
|
751
|
-
} catch (error) {
|
|
752
|
-
if (signal.aborted) {
|
|
753
|
-
await this.logMutation({
|
|
754
|
-
kind: "delete",
|
|
755
|
-
outcome: "skipped",
|
|
756
|
-
reason: "memory_delete_cancelled",
|
|
757
|
-
source,
|
|
758
|
-
call,
|
|
759
|
-
memoryId,
|
|
760
|
-
startedAt,
|
|
761
|
-
});
|
|
762
|
-
throw error;
|
|
763
|
-
}
|
|
764
|
-
await this.logMutation({
|
|
765
|
-
kind: "delete",
|
|
766
|
-
outcome: "failed",
|
|
767
|
-
reason: memoryErrorCode(error, "memory_delete_failed"),
|
|
768
|
-
source,
|
|
769
|
-
call,
|
|
770
|
-
memoryId,
|
|
771
|
-
startedAt,
|
|
772
|
-
});
|
|
773
|
-
return { ok: false, error: boundedMemoryError(error) };
|
|
774
|
-
}
|
|
775
|
-
}
|
|
776
|
-
|
|
777
|
-
private async embedMutationText(
|
|
778
|
-
text: string,
|
|
779
|
-
signal: AbortSignal,
|
|
780
|
-
): Promise<Float32Array> {
|
|
781
|
-
const raw = await this.embeddingClient.embed([text], signal);
|
|
782
|
-
if (raw.length !== 1 || raw[0] === undefined) {
|
|
783
|
-
throw new MemoryError(
|
|
784
|
-
"memory_embedding_response_invalid",
|
|
785
|
-
"Embedding response did not contain the memory vector.",
|
|
786
|
-
);
|
|
787
|
-
}
|
|
788
|
-
return normalizeEmbedding(raw[0], this.embeddingDimensions);
|
|
789
|
-
}
|
|
790
|
-
|
|
791
|
-
private recordInvalidMutation(
|
|
792
|
-
kind: MemoryMutationDiagnostic["kind"],
|
|
793
|
-
call: ToolCall,
|
|
794
|
-
source: MemoryToolSource,
|
|
795
|
-
): Promise<void> {
|
|
796
|
-
return this.logMutation({
|
|
797
|
-
kind,
|
|
798
|
-
outcome: "failed",
|
|
799
|
-
reason: `memory_${kind}_args_invalid`,
|
|
800
|
-
source,
|
|
801
|
-
call,
|
|
802
|
-
memoryId: null,
|
|
803
|
-
startedAt: performance.now(),
|
|
804
|
-
});
|
|
805
|
-
}
|
|
806
|
-
|
|
807
|
-
private logMutation(input: {
|
|
808
|
-
readonly kind: MemoryMutationDiagnostic["kind"];
|
|
809
|
-
readonly outcome: MemoryMutationDiagnostic["outcome"];
|
|
810
|
-
readonly reason: string | null;
|
|
811
|
-
readonly source: MemoryToolSource;
|
|
812
|
-
readonly call: ToolCall;
|
|
813
|
-
readonly memoryId: string | null;
|
|
814
|
-
readonly startedAt: number;
|
|
815
|
-
}): Promise<void> {
|
|
816
|
-
return this.log.append(
|
|
817
|
-
mutationDiagnostic({
|
|
818
|
-
clock: this.clock,
|
|
819
|
-
kind: input.kind,
|
|
820
|
-
outcome: input.outcome,
|
|
821
|
-
reason: input.reason,
|
|
822
|
-
workspace: input.source.workspaceRoot,
|
|
823
|
-
sessionId: input.source.sessionId,
|
|
824
|
-
turnId: input.call.turnId,
|
|
825
|
-
toolCallId: input.call.toolCallId,
|
|
826
|
-
memoryId: input.memoryId,
|
|
827
|
-
ms: elapsedMs(input.startedAt),
|
|
828
|
-
}),
|
|
829
|
-
);
|
|
830
|
-
}
|
|
831
|
-
|
|
832
|
-
private async search(
|
|
833
|
-
query: string | null,
|
|
834
|
-
keywords: readonly string[],
|
|
835
|
-
signal: AbortSignal,
|
|
836
|
-
source: { readonly workspaceRoot: string; readonly sessionId: SessionId },
|
|
837
|
-
): Promise<MemorySearchRawResult> {
|
|
838
|
-
const startedAt = performance.now();
|
|
839
|
-
const queryBytes = query === null ? 0 : Buffer.byteLength(query, "utf8");
|
|
840
|
-
let degraded: MemoryRecallDegraded | null = null;
|
|
841
|
-
let vectorMatches: readonly MemorySearchMatch[] = [];
|
|
842
|
-
let ftsMatches: readonly MemoryFtsMatch[] = [];
|
|
843
|
-
try {
|
|
844
|
-
if (query !== null) {
|
|
845
|
-
try {
|
|
846
|
-
const raw = await this.embeddingClient.embed([query], signal);
|
|
847
|
-
if (raw.length !== 1 || raw[0] === undefined) {
|
|
848
|
-
throw new MemoryError(
|
|
849
|
-
"memory_embedding_response_invalid",
|
|
850
|
-
"Embedding response did not contain the query vector.",
|
|
851
|
-
);
|
|
852
|
-
}
|
|
853
|
-
const queryEmbedding = normalizeEmbedding(raw[0], this.embeddingDimensions);
|
|
854
|
-
vectorMatches = this.store.search(
|
|
855
|
-
queryEmbedding,
|
|
856
|
-
MEMORY_RECALL_CANDIDATE_LIMIT,
|
|
857
|
-
);
|
|
858
|
-
} catch (error) {
|
|
859
|
-
if (signal.aborted || keywords.length === 0) {
|
|
860
|
-
throw error;
|
|
861
|
-
}
|
|
862
|
-
degraded = "vector";
|
|
863
|
-
}
|
|
864
|
-
}
|
|
865
|
-
if (keywords.length > 0) {
|
|
866
|
-
try {
|
|
867
|
-
ftsMatches = this.store.searchFts(keywords, MEMORY_RECALL_CANDIDATE_LIMIT);
|
|
868
|
-
} catch (error) {
|
|
869
|
-
if (signal.aborted) {
|
|
870
|
-
throw error;
|
|
871
|
-
}
|
|
872
|
-
const vectorUsable = query !== null && degraded === null;
|
|
873
|
-
if (!vectorUsable) {
|
|
874
|
-
throw error;
|
|
875
|
-
}
|
|
876
|
-
degraded = "fts";
|
|
877
|
-
}
|
|
878
|
-
}
|
|
879
|
-
const fused = fuseMemoryRecall({ vector: vectorMatches, fts: ftsMatches }).slice(
|
|
880
|
-
0,
|
|
881
|
-
MEMORY_SEARCH_LIMIT,
|
|
882
|
-
);
|
|
883
|
-
await this.log.append(
|
|
884
|
-
searchDiagnostic({
|
|
885
|
-
clock: this.clock,
|
|
886
|
-
outcome: "ok",
|
|
887
|
-
reason: null,
|
|
888
|
-
workspace: source.workspaceRoot,
|
|
889
|
-
sessionId: source.sessionId,
|
|
890
|
-
queryBytes,
|
|
891
|
-
keywordCount: keywords.length,
|
|
892
|
-
returned: fused.length,
|
|
893
|
-
vectorReturned: vectorMatches.length,
|
|
894
|
-
ftsReturned: ftsMatches.length,
|
|
895
|
-
degraded,
|
|
896
|
-
scores: fused.map((match) => roundScore(match.score)),
|
|
897
|
-
vectorScores: vectorMatches
|
|
898
|
-
.slice(0, MEMORY_SEARCH_DIAGNOSTIC_VECTOR_SCORES_LIMIT)
|
|
899
|
-
.map((match) => roundScore(match.score)),
|
|
900
|
-
ms: elapsedMs(startedAt),
|
|
901
|
-
}),
|
|
902
|
-
);
|
|
903
|
-
return {
|
|
904
|
-
ok: true,
|
|
905
|
-
degraded,
|
|
906
|
-
matches: Object.freeze(
|
|
907
|
-
fused.map((match) =>
|
|
908
|
-
Object.freeze({
|
|
909
|
-
memoryId: match.memoryId,
|
|
910
|
-
text: match.text,
|
|
911
|
-
summary: truncateUtf8(match.summary, MAX_SEARCH_RESULT_SUMMARY_BYTES),
|
|
912
|
-
score: match.score,
|
|
913
|
-
via: match.via,
|
|
914
|
-
sourceWorkspace: match.sourceWorkspace,
|
|
915
|
-
sourceSessionId: match.sourceSessionId,
|
|
916
|
-
createdAt: match.createdAt,
|
|
917
|
-
}),
|
|
918
|
-
),
|
|
919
|
-
),
|
|
920
|
-
};
|
|
921
|
-
} catch (error) {
|
|
922
|
-
const reason = signal.aborted
|
|
923
|
-
? "memory_search_cancelled"
|
|
924
|
-
: memoryErrorCode(error, "memory_search_failed");
|
|
925
|
-
await this.log.append(
|
|
926
|
-
searchDiagnostic({
|
|
927
|
-
clock: this.clock,
|
|
928
|
-
outcome: signal.aborted ? "skipped" : "failed",
|
|
929
|
-
reason,
|
|
930
|
-
workspace: source.workspaceRoot,
|
|
931
|
-
sessionId: source.sessionId,
|
|
932
|
-
queryBytes,
|
|
933
|
-
keywordCount: keywords.length,
|
|
934
|
-
ms: elapsedMs(startedAt),
|
|
935
|
-
}),
|
|
936
|
-
);
|
|
937
|
-
if (signal.aborted) {
|
|
938
|
-
throw error;
|
|
939
|
-
}
|
|
940
|
-
return {
|
|
941
|
-
ok: false,
|
|
942
|
-
error: boundedMemoryError(error),
|
|
943
|
-
};
|
|
944
|
-
}
|
|
945
|
-
}
|
|
946
|
-
|
|
947
|
-
private recordInvalidSearch(
|
|
948
|
-
invalid: { readonly queryBytes: number; readonly keywordCount: number },
|
|
949
|
-
source: { readonly workspaceRoot: string; readonly sessionId: SessionId },
|
|
950
|
-
): Promise<void> {
|
|
951
|
-
return this.log.append(
|
|
952
|
-
searchDiagnostic({
|
|
953
|
-
clock: this.clock,
|
|
954
|
-
outcome: "failed",
|
|
955
|
-
reason: "memory_search_args_invalid",
|
|
956
|
-
workspace: source.workspaceRoot,
|
|
957
|
-
sessionId: source.sessionId,
|
|
958
|
-
queryBytes: invalid.queryBytes,
|
|
959
|
-
keywordCount: invalid.keywordCount,
|
|
960
|
-
ms: 0,
|
|
961
|
-
}),
|
|
962
|
-
);
|
|
963
|
-
}
|
|
964
|
-
|
|
965
|
-
private async get(
|
|
966
|
-
memoryId: string,
|
|
967
|
-
signal: AbortSignal,
|
|
968
|
-
source: { readonly workspaceRoot: string; readonly sessionId: SessionId },
|
|
969
|
-
): Promise<MemoryGetRawResult> {
|
|
970
|
-
const startedAt = performance.now();
|
|
971
|
-
try {
|
|
972
|
-
signal.throwIfAborted();
|
|
973
|
-
const record = this.store.getById(memoryId);
|
|
974
|
-
await this.log.append(
|
|
975
|
-
getDiagnostic({
|
|
976
|
-
clock: this.clock,
|
|
977
|
-
outcome: "ok",
|
|
978
|
-
reason: null,
|
|
979
|
-
workspace: source.workspaceRoot,
|
|
980
|
-
sessionId: source.sessionId,
|
|
981
|
-
found: record !== undefined,
|
|
982
|
-
ms: elapsedMs(startedAt),
|
|
983
|
-
}),
|
|
984
|
-
);
|
|
985
|
-
return {
|
|
986
|
-
ok: true,
|
|
987
|
-
memory:
|
|
988
|
-
record === undefined
|
|
989
|
-
? null
|
|
990
|
-
: Object.freeze({
|
|
991
|
-
memoryId: record.memoryId,
|
|
992
|
-
text: record.text,
|
|
993
|
-
summary: record.summary,
|
|
994
|
-
sourceWorkspace: record.sourceWorkspace,
|
|
995
|
-
sourceSessionId: record.sourceSessionId,
|
|
996
|
-
sourceTurnId: record.sourceTurnId,
|
|
997
|
-
createdAt: record.createdAt,
|
|
998
|
-
}),
|
|
999
|
-
};
|
|
1000
|
-
} catch (error) {
|
|
1001
|
-
if (signal.aborted) {
|
|
1002
|
-
throw error;
|
|
1003
|
-
}
|
|
1004
|
-
await this.log.append(
|
|
1005
|
-
getDiagnostic({
|
|
1006
|
-
clock: this.clock,
|
|
1007
|
-
outcome: "failed",
|
|
1008
|
-
reason: memoryErrorCode(error, "memory_get_failed"),
|
|
1009
|
-
workspace: source.workspaceRoot,
|
|
1010
|
-
sessionId: source.sessionId,
|
|
1011
|
-
found: false,
|
|
1012
|
-
ms: elapsedMs(startedAt),
|
|
1013
|
-
}),
|
|
1014
|
-
);
|
|
1015
|
-
return {
|
|
1016
|
-
ok: false,
|
|
1017
|
-
error: boundedMemoryError(error),
|
|
1018
|
-
};
|
|
1019
|
-
}
|
|
1020
|
-
}
|
|
1021
|
-
|
|
1022
|
-
private recordInvalidGet(source: {
|
|
1023
|
-
readonly workspaceRoot: string;
|
|
1024
|
-
readonly sessionId: SessionId;
|
|
1025
|
-
}): Promise<void> {
|
|
1026
|
-
return this.log.append(
|
|
1027
|
-
getDiagnostic({
|
|
1028
|
-
clock: this.clock,
|
|
1029
|
-
outcome: "failed",
|
|
1030
|
-
reason: "memory_get_args_invalid",
|
|
1031
|
-
workspace: source.workspaceRoot,
|
|
1032
|
-
sessionId: source.sessionId,
|
|
1033
|
-
found: false,
|
|
1034
|
-
ms: 0,
|
|
1035
|
-
}),
|
|
1036
|
-
);
|
|
1037
|
-
}
|
|
1038
|
-
}
|
|
1039
|
-
|
|
1040
|
-
export function buildExtractionEvidenceText(
|
|
1041
|
-
workspaceRoot: string,
|
|
1042
|
-
snapshot: CompletedTurnSnapshot,
|
|
1043
|
-
): string {
|
|
1044
|
-
if (!path.isAbsolute(workspaceRoot)) {
|
|
1045
|
-
throw new MemoryError(
|
|
1046
|
-
"completed_turn_enqueue_failed",
|
|
1047
|
-
"Completed turn workspace must be absolute.",
|
|
1048
|
-
);
|
|
1049
|
-
}
|
|
1050
|
-
const messages = snapshot.messages
|
|
1051
|
-
.filter(
|
|
1052
|
-
(message) => message.role !== "tool" || !MEMORY_TOOL_NAMES.includes(message.name),
|
|
1053
|
-
)
|
|
1054
|
-
.map((message) => ({ ...message }));
|
|
1055
|
-
return JSON.stringify(
|
|
1056
|
-
{
|
|
1057
|
-
workspaceRoot,
|
|
1058
|
-
messages,
|
|
1059
|
-
},
|
|
1060
|
-
null,
|
|
1061
|
-
2,
|
|
1062
|
-
);
|
|
1063
|
-
}
|
|
1064
|
-
|
|
1065
|
-
export function containsSensitiveMemory(text: string): boolean {
|
|
1066
|
-
return [
|
|
1067
|
-
/-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----/i,
|
|
1068
|
-
/\b(?:authorization\s*:\s*)?bearer\s+[a-z0-9._~+/=-]{12,}\b/i,
|
|
1069
|
-
/\b(?:sk-[a-z0-9_-]{12,}|gh[opusr]_[a-z0-9_]{20,}|github_pat_[a-z0-9_]{20,}|AKIA[0-9A-Z]{16}|AIza[0-9A-Za-z_-]{30,})\b/,
|
|
1070
|
-
/\b(?:cookie|session(?:_?token|id)?)\s*[:=]\s*["']?[^\s"',;]{8,}/i,
|
|
1071
|
-
/\b(?:password|passwd|pwd|secret|api[_-]?key|access[_-]?token|refresh[_-]?token)\s*[:=]\s*["']?[^\s"',;]{6,}/i,
|
|
1072
|
-
].some((pattern) => pattern.test(text));
|
|
1073
|
-
}
|
|
1074
|
-
|
|
1075
|
-
function extractionDiagnostic(input: {
|
|
1076
|
-
readonly clock: () => string;
|
|
1077
|
-
readonly outcome: MemoryExtractionDiagnostic["outcome"];
|
|
1078
|
-
readonly reason: string | null;
|
|
1079
|
-
readonly workspace: string;
|
|
1080
|
-
readonly turnId: string;
|
|
1081
|
-
readonly ms: number;
|
|
1082
|
-
readonly inputTokens?: number;
|
|
1083
|
-
readonly returned?: number;
|
|
1084
|
-
readonly written?: number;
|
|
1085
|
-
readonly rejected?: MemoryExtractionRejectedCounts;
|
|
1086
|
-
readonly detail?: string;
|
|
1087
|
-
}): MemoryExtractionDiagnostic {
|
|
1088
|
-
return Object.freeze({
|
|
1089
|
-
at: input.clock(),
|
|
1090
|
-
kind: "extraction",
|
|
1091
|
-
outcome: input.outcome,
|
|
1092
|
-
reason: input.reason,
|
|
1093
|
-
workspace: input.workspace,
|
|
1094
|
-
turnId: input.turnId,
|
|
1095
|
-
inputTokens: input.inputTokens ?? 0,
|
|
1096
|
-
returned: input.returned ?? 0,
|
|
1097
|
-
written: input.written ?? 0,
|
|
1098
|
-
rejected: input.rejected ?? emptyRejectedCounts(),
|
|
1099
|
-
ms: input.ms,
|
|
1100
|
-
...(input.detail === undefined ? {} : { detail: input.detail }),
|
|
1101
|
-
});
|
|
1102
|
-
}
|
|
1103
|
-
|
|
1104
|
-
function searchDiagnostic(input: {
|
|
1105
|
-
readonly clock: () => string;
|
|
1106
|
-
readonly outcome: MemorySearchDiagnostic["outcome"];
|
|
1107
|
-
readonly reason: string | null;
|
|
1108
|
-
readonly workspace: string;
|
|
1109
|
-
readonly sessionId: string;
|
|
1110
|
-
readonly queryBytes: number;
|
|
1111
|
-
readonly keywordCount: number;
|
|
1112
|
-
readonly returned?: number;
|
|
1113
|
-
readonly vectorReturned?: number;
|
|
1114
|
-
readonly ftsReturned?: number;
|
|
1115
|
-
readonly degraded?: MemoryRecallDegraded | null;
|
|
1116
|
-
readonly scores?: readonly number[];
|
|
1117
|
-
readonly vectorScores?: readonly number[];
|
|
1118
|
-
readonly ms: number;
|
|
1119
|
-
}): MemorySearchDiagnostic {
|
|
1120
|
-
return Object.freeze({
|
|
1121
|
-
at: input.clock(),
|
|
1122
|
-
kind: "search",
|
|
1123
|
-
outcome: input.outcome,
|
|
1124
|
-
reason: input.reason,
|
|
1125
|
-
workspace: input.workspace,
|
|
1126
|
-
sessionId: input.sessionId,
|
|
1127
|
-
queryBytes: input.queryBytes,
|
|
1128
|
-
keywordCount: input.keywordCount,
|
|
1129
|
-
returned: input.returned ?? 0,
|
|
1130
|
-
vectorReturned: input.vectorReturned ?? 0,
|
|
1131
|
-
ftsReturned: input.ftsReturned ?? 0,
|
|
1132
|
-
degraded: input.degraded ?? null,
|
|
1133
|
-
scores: Object.freeze([...(input.scores ?? [])]),
|
|
1134
|
-
vectorScores: Object.freeze([...(input.vectorScores ?? [])]),
|
|
1135
|
-
ms: input.ms,
|
|
1136
|
-
});
|
|
1137
|
-
}
|
|
1138
|
-
|
|
1139
|
-
function getDiagnostic(input: {
|
|
1140
|
-
readonly clock: () => string;
|
|
1141
|
-
readonly outcome: MemoryGetDiagnostic["outcome"];
|
|
1142
|
-
readonly reason: string | null;
|
|
1143
|
-
readonly workspace: string;
|
|
1144
|
-
readonly sessionId: string;
|
|
1145
|
-
readonly found: boolean;
|
|
1146
|
-
readonly ms: number;
|
|
1147
|
-
}): MemoryGetDiagnostic {
|
|
1148
|
-
return Object.freeze({
|
|
1149
|
-
at: input.clock(),
|
|
1150
|
-
kind: "get",
|
|
1151
|
-
outcome: input.outcome,
|
|
1152
|
-
reason: input.reason,
|
|
1153
|
-
workspace: input.workspace,
|
|
1154
|
-
sessionId: input.sessionId,
|
|
1155
|
-
found: input.found,
|
|
1156
|
-
ms: input.ms,
|
|
1157
|
-
});
|
|
1158
|
-
}
|
|
1159
|
-
|
|
1160
|
-
function mutationDiagnostic(input: {
|
|
1161
|
-
readonly clock: () => string;
|
|
1162
|
-
readonly kind: MemoryMutationDiagnostic["kind"];
|
|
1163
|
-
readonly outcome: MemoryMutationDiagnostic["outcome"];
|
|
1164
|
-
readonly reason: string | null;
|
|
1165
|
-
readonly workspace: string;
|
|
1166
|
-
readonly sessionId: string;
|
|
1167
|
-
readonly turnId: string;
|
|
1168
|
-
readonly toolCallId: string;
|
|
1169
|
-
readonly memoryId: string | null;
|
|
1170
|
-
readonly ms: number;
|
|
1171
|
-
}): MemoryMutationDiagnostic {
|
|
1172
|
-
return Object.freeze({
|
|
1173
|
-
at: input.clock(),
|
|
1174
|
-
kind: input.kind,
|
|
1175
|
-
outcome: input.outcome,
|
|
1176
|
-
reason: input.reason,
|
|
1177
|
-
workspace: input.workspace,
|
|
1178
|
-
sessionId: input.sessionId,
|
|
1179
|
-
turnId: input.turnId,
|
|
1180
|
-
toolCallId: input.toolCallId,
|
|
1181
|
-
memoryId: input.memoryId,
|
|
1182
|
-
ms: input.ms,
|
|
1183
|
-
});
|
|
1184
|
-
}
|
|
1185
|
-
|
|
1186
|
-
function emptyRejectedCounts(): MemoryExtractionRejectedCounts {
|
|
1187
|
-
return Object.freeze({
|
|
1188
|
-
duplicate: 0,
|
|
1189
|
-
secret: 0,
|
|
1190
|
-
invalid: 0,
|
|
1191
|
-
embedding: 0,
|
|
1192
|
-
});
|
|
1193
|
-
}
|
|
1194
|
-
|
|
1195
|
-
function elapsedMs(startedAt: number): number {
|
|
1196
|
-
return Math.round((performance.now() - startedAt) * 100) / 100;
|
|
1197
|
-
}
|
|
1198
|
-
|
|
1199
|
-
function roundScore(score: number): number {
|
|
1200
|
-
return Math.round(score * 1_000) / 1_000;
|
|
1201
|
-
}
|
|
1202
|
-
|
|
1203
|
-
export function fuseMemoryRecall(input: {
|
|
1204
|
-
readonly vector: readonly MemorySearchMatch[];
|
|
1205
|
-
readonly fts: readonly MemoryFtsMatch[];
|
|
1206
|
-
}): readonly MemoryHybridMatch[] {
|
|
1207
|
-
type Entry = {
|
|
1208
|
-
readonly memoryId: string;
|
|
1209
|
-
readonly text: string;
|
|
1210
|
-
readonly summary: string;
|
|
1211
|
-
readonly sourceWorkspace: string;
|
|
1212
|
-
readonly sourceSessionId: string;
|
|
1213
|
-
readonly createdAt: string;
|
|
1214
|
-
score: number;
|
|
1215
|
-
readonly via: Set<MemoryRecallPath>;
|
|
1216
|
-
};
|
|
1217
|
-
const entries = new Map<string, Entry>();
|
|
1218
|
-
const accumulate = (
|
|
1219
|
-
match: {
|
|
1220
|
-
readonly memoryId: string;
|
|
1221
|
-
readonly text: string;
|
|
1222
|
-
readonly summary: string;
|
|
1223
|
-
readonly sourceWorkspace: string;
|
|
1224
|
-
readonly sourceSessionId: string;
|
|
1225
|
-
readonly createdAt: string;
|
|
1226
|
-
},
|
|
1227
|
-
rank: number,
|
|
1228
|
-
path: MemoryRecallPath,
|
|
1229
|
-
): void => {
|
|
1230
|
-
const contribution = 1 / (MEMORY_RRF_K + rank);
|
|
1231
|
-
const existing = entries.get(match.memoryId);
|
|
1232
|
-
if (existing !== undefined) {
|
|
1233
|
-
existing.score += contribution;
|
|
1234
|
-
existing.via.add(path);
|
|
1235
|
-
return;
|
|
1236
|
-
}
|
|
1237
|
-
entries.set(match.memoryId, {
|
|
1238
|
-
memoryId: match.memoryId,
|
|
1239
|
-
text: match.text,
|
|
1240
|
-
summary: match.summary,
|
|
1241
|
-
sourceWorkspace: match.sourceWorkspace,
|
|
1242
|
-
sourceSessionId: match.sourceSessionId,
|
|
1243
|
-
createdAt: match.createdAt,
|
|
1244
|
-
score: contribution,
|
|
1245
|
-
via: new Set([path]),
|
|
1246
|
-
});
|
|
1247
|
-
};
|
|
1248
|
-
input.vector.forEach((match, index) => accumulate(match, index + 1, "vector"));
|
|
1249
|
-
input.fts.forEach((match, index) => accumulate(match, index + 1, "fts"));
|
|
1250
|
-
|
|
1251
|
-
return Object.freeze(
|
|
1252
|
-
[...entries.values()]
|
|
1253
|
-
.sort(
|
|
1254
|
-
(left, right) =>
|
|
1255
|
-
right.score - left.score ||
|
|
1256
|
-
right.createdAt.localeCompare(left.createdAt) ||
|
|
1257
|
-
left.memoryId.localeCompare(right.memoryId),
|
|
1258
|
-
)
|
|
1259
|
-
.map((entry) =>
|
|
1260
|
-
Object.freeze({
|
|
1261
|
-
memoryId: entry.memoryId,
|
|
1262
|
-
text: entry.text,
|
|
1263
|
-
summary: entry.summary,
|
|
1264
|
-
score: entry.score,
|
|
1265
|
-
via: Object.freeze(
|
|
1266
|
-
(["vector", "fts"] as const).filter((path) => entry.via.has(path)),
|
|
1267
|
-
),
|
|
1268
|
-
sourceWorkspace: entry.sourceWorkspace,
|
|
1269
|
-
sourceSessionId: entry.sourceSessionId,
|
|
1270
|
-
createdAt: entry.createdAt,
|
|
1271
|
-
}),
|
|
1272
|
-
),
|
|
1273
|
-
);
|
|
1274
|
-
}
|