tinker-agent 2.0.0 → 2.2.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 +44 -1
- package/README.md +27 -2
- package/package.json +2 -1
- package/src/agent/context-meter.ts +2 -4
- package/src/agent/runtime-session.ts +9 -2
- package/src/agent/session-ledger.ts +12 -5
- package/src/agent/tool-result-content.ts +76 -0
- package/src/agent/types.ts +14 -2
- package/src/cli/config.ts +4 -0
- package/src/cli/model-profiles.ts +41 -2
- package/src/cli/public-config-contract.ts +30 -7
- package/src/cli/runner-dependencies.ts +5 -0
- package/src/cli/tui-memory.ts +1 -0
- package/src/cli/tui-runner.tsx +4 -0
- package/src/context/compiled-context-hash.ts +2 -1
- package/src/context/compiled-context-validator.ts +13 -4
- package/src/context/context-protocol-validator.ts +33 -2
- package/src/context/context-revision-compiler.ts +2 -1
- package/src/context/context-revision.ts +8 -2
- package/src/context/context-swap-renderer.ts +46 -12
- package/src/context/prefix-retirement-planner.ts +13 -9
- package/src/context/protocol-frame.ts +74 -7
- package/src/context/swap-planner.ts +19 -14
- package/src/events/observation-text-log.ts +1 -1
- package/src/events/stdout-event-printer.ts +6 -0
- package/src/image/image-asset-store.ts +32 -3
- package/src/memory/contracts.ts +63 -3
- package/src/memory/memory-coordinator.ts +319 -49
- package/src/memory/memory-extractor.ts +48 -48
- package/src/memory/memory-get-tool.ts +86 -0
- package/src/memory/memory-search-tool.ts +122 -33
- package/src/memory/memory-store.ts +227 -20
- package/src/model/fake-model-client.ts +129 -76
- package/src/model/model-client.ts +62 -11
- package/src/model/openai-chat-mapping.ts +2 -1
- package/src/model/openai-chat-model-client.ts +22 -10
- package/src/model/openai-model-utils.ts +61 -30
- package/src/model/openai-responses-mapping.ts +25 -1
- package/src/model/openai-responses-model-client.ts +27 -11
- package/src/model/token-estimator.ts +10 -0
- package/src/observation/observation-builder.ts +100 -25
- package/src/session/session-history-reader.ts +128 -5
- package/src/session/session-schema.ts +59 -9
- package/src/session/session-store.ts +343 -196
- package/src/tools/registry.ts +18 -0
- package/src/tools/types.ts +46 -0
- package/src/tools/view-image.ts +89 -0
- package/src/tools/wait.ts +85 -0
- package/src/tui/components/memory-browser.tsx +3 -0
- package/src/tui/components/prompt-input.tsx +48 -25
- package/src/tui/event-store.ts +61 -2
|
@@ -5,14 +5,16 @@ import path from "node:path";
|
|
|
5
5
|
import { Database } from "bun:sqlite";
|
|
6
6
|
import { createUuidV7, isCanonicalUuidV7 } from "../ids/uuid-v7";
|
|
7
7
|
import {
|
|
8
|
-
|
|
8
|
+
MAX_MEMORY_SUMMARY_BYTES,
|
|
9
9
|
MAX_MEMORY_TEXT_BYTES,
|
|
10
10
|
MEMORY_SCHEMA_VERSION,
|
|
11
11
|
MEMORY_SEARCH_LIMIT,
|
|
12
12
|
MemoryError,
|
|
13
13
|
type MemoryEmbeddingIdentity,
|
|
14
|
+
type MemoryFtsMatch,
|
|
14
15
|
type MemoryPaths,
|
|
15
16
|
type MemorySearchMatch,
|
|
17
|
+
type StoredMemoryRecord,
|
|
16
18
|
type StoredMemorySummary,
|
|
17
19
|
type MemoryWriteBatch,
|
|
18
20
|
type MemoryWriteResult,
|
|
@@ -34,6 +36,7 @@ const CREATE_MEMORY_META_SQL = `CREATE TABLE memory_meta (
|
|
|
34
36
|
const CREATE_MEMORIES_SQL = `CREATE TABLE memories (
|
|
35
37
|
memory_id TEXT PRIMARY KEY,
|
|
36
38
|
text TEXT NOT NULL,
|
|
39
|
+
summary TEXT NOT NULL DEFAULT '',
|
|
37
40
|
text_sha256 TEXT NOT NULL UNIQUE,
|
|
38
41
|
embedding BLOB NOT NULL,
|
|
39
42
|
source_workspace TEXT NOT NULL,
|
|
@@ -45,6 +48,16 @@ const CREATE_MEMORIES_SQL = `CREATE TABLE memories (
|
|
|
45
48
|
const CREATE_MEMORIES_INDEX_SQL = `CREATE INDEX memories_created_at
|
|
46
49
|
ON memories(created_at DESC)`;
|
|
47
50
|
|
|
51
|
+
const MEMORIES_FTS_TABLE = "memories_fts";
|
|
52
|
+
|
|
53
|
+
const CREATE_MEMORIES_FTS_SQL = `CREATE VIRTUAL TABLE memories_fts USING fts5(
|
|
54
|
+
text,
|
|
55
|
+
summary,
|
|
56
|
+
content='memories',
|
|
57
|
+
content_rowid='rowid',
|
|
58
|
+
tokenize='trigram'
|
|
59
|
+
)`;
|
|
60
|
+
|
|
48
61
|
const EXPECTED_SCHEMA = new Map([
|
|
49
62
|
["index:memories_created_at", CREATE_MEMORIES_INDEX_SQL],
|
|
50
63
|
["table:memories", CREATE_MEMORIES_SQL],
|
|
@@ -72,6 +85,7 @@ export function resolveMemoryPaths(homeRoot = os.homedir()): MemoryPaths {
|
|
|
72
85
|
export class MemoryStore {
|
|
73
86
|
readonly paths: MemoryPaths;
|
|
74
87
|
readonly dimensions: number;
|
|
88
|
+
readonly ftsAvailable: boolean;
|
|
75
89
|
private closed = false;
|
|
76
90
|
|
|
77
91
|
private constructor(
|
|
@@ -79,10 +93,12 @@ export class MemoryStore {
|
|
|
79
93
|
input: Required<Pick<OpenMemoryStoreInput, "clock" | "createMemoryId">> & {
|
|
80
94
|
readonly paths: MemoryPaths;
|
|
81
95
|
readonly embedding: MemoryEmbeddingIdentity;
|
|
96
|
+
readonly ftsAvailable: boolean;
|
|
82
97
|
},
|
|
83
98
|
) {
|
|
84
99
|
this.paths = input.paths;
|
|
85
100
|
this.dimensions = input.embedding.dimensions;
|
|
101
|
+
this.ftsAvailable = input.ftsAvailable;
|
|
86
102
|
this.clock = input.clock;
|
|
87
103
|
this.createMemoryId = input.createMemoryId;
|
|
88
104
|
}
|
|
@@ -120,7 +136,13 @@ export class MemoryStore {
|
|
|
120
136
|
safeIntegers: true,
|
|
121
137
|
});
|
|
122
138
|
configureDatabase(database, busyTimeoutMs);
|
|
123
|
-
initializeOrVerifySchema(database, input.embedding);
|
|
139
|
+
initializeOrVerifySchema(database, input.embedding, paths.database);
|
|
140
|
+
let ftsAvailable = true;
|
|
141
|
+
try {
|
|
142
|
+
ensureFtsIndex(database);
|
|
143
|
+
} catch {
|
|
144
|
+
ftsAvailable = false;
|
|
145
|
+
}
|
|
124
146
|
await secureCreatedAuxiliaryFile(`${paths.database}-wal`, walExisted);
|
|
125
147
|
await secureCreatedAuxiliaryFile(`${paths.database}-shm`, shmExisted);
|
|
126
148
|
await validatePrivateFile(paths.database);
|
|
@@ -130,6 +152,7 @@ export class MemoryStore {
|
|
|
130
152
|
embedding: Object.freeze({ ...input.embedding }),
|
|
131
153
|
clock: input.clock ?? (() => new Date().toISOString()),
|
|
132
154
|
createMemoryId: input.createMemoryId ?? createUuidV7,
|
|
155
|
+
ftsAvailable,
|
|
133
156
|
});
|
|
134
157
|
} catch (error) {
|
|
135
158
|
database?.close();
|
|
@@ -163,11 +186,16 @@ export class MemoryStore {
|
|
|
163
186
|
runImmediateTransaction(this.database, () => {
|
|
164
187
|
const insert = this.database.query(
|
|
165
188
|
`INSERT INTO memories (
|
|
166
|
-
memory_id, text, text_sha256, embedding, source_workspace,
|
|
189
|
+
memory_id, text, summary, text_sha256, embedding, source_workspace,
|
|
167
190
|
source_session_id, source_turn_id, created_at
|
|
168
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
191
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
169
192
|
ON CONFLICT(text_sha256) DO NOTHING`,
|
|
170
193
|
);
|
|
194
|
+
const ftsInsert = this.ftsAvailable
|
|
195
|
+
? this.database.query(
|
|
196
|
+
`INSERT INTO ${MEMORIES_FTS_TABLE}(rowid, text, summary) VALUES (?, ?, ?)`,
|
|
197
|
+
)
|
|
198
|
+
: undefined;
|
|
171
199
|
for (const candidate of input.candidates) {
|
|
172
200
|
const memoryId = this.createMemoryId();
|
|
173
201
|
if (!isCanonicalUuidV7(memoryId)) {
|
|
@@ -179,6 +207,7 @@ export class MemoryStore {
|
|
|
179
207
|
const result = insert.run(
|
|
180
208
|
memoryId,
|
|
181
209
|
candidate.text,
|
|
210
|
+
candidate.summary,
|
|
182
211
|
sha256(candidate.text),
|
|
183
212
|
encodeEmbedding(candidate.embedding),
|
|
184
213
|
input.workspaceRoot,
|
|
@@ -194,6 +223,7 @@ export class MemoryStore {
|
|
|
194
223
|
);
|
|
195
224
|
}
|
|
196
225
|
if (changes === 1) {
|
|
226
|
+
ftsInsert?.run(result.lastInsertRowid, candidate.text, candidate.summary);
|
|
197
227
|
inserted.push(
|
|
198
228
|
Object.freeze({
|
|
199
229
|
memoryId,
|
|
@@ -236,7 +266,8 @@ export class MemoryStore {
|
|
|
236
266
|
const matches: MemorySearchMatch[] = [];
|
|
237
267
|
const rows = this.database
|
|
238
268
|
.query(
|
|
239
|
-
`SELECT memory_id, text, embedding, source_workspace,
|
|
269
|
+
`SELECT memory_id, text, summary, embedding, source_workspace,
|
|
270
|
+
source_session_id, created_at
|
|
240
271
|
FROM memories`,
|
|
241
272
|
)
|
|
242
273
|
.iterate();
|
|
@@ -247,8 +278,10 @@ export class MemoryStore {
|
|
|
247
278
|
Object.freeze({
|
|
248
279
|
memoryId: sqlString(row.memory_id, "memory_id"),
|
|
249
280
|
text: sqlString(row.text, "memory text"),
|
|
281
|
+
summary: sqlSummary(row.summary, "memory summary"),
|
|
250
282
|
score: cosineFromNormalized(queryEmbedding, embedding),
|
|
251
283
|
sourceWorkspace: sqlString(row.source_workspace, "memory source_workspace"),
|
|
284
|
+
sourceSessionId: sqlString(row.source_session_id, "memory source_session_id"),
|
|
252
285
|
createdAt: requireUtcTimestamp(
|
|
253
286
|
sqlString(row.created_at, "memory created_at"),
|
|
254
287
|
"memory created_at",
|
|
@@ -265,12 +298,74 @@ export class MemoryStore {
|
|
|
265
298
|
return Object.freeze(matches.slice(0, limit));
|
|
266
299
|
}
|
|
267
300
|
|
|
301
|
+
searchFts(
|
|
302
|
+
keywords: readonly string[],
|
|
303
|
+
limit = MEMORY_SEARCH_LIMIT,
|
|
304
|
+
): readonly MemoryFtsMatch[] {
|
|
305
|
+
this.requireOpen();
|
|
306
|
+
if (!this.ftsAvailable) {
|
|
307
|
+
throw new MemoryError(
|
|
308
|
+
"memory_fts_unavailable",
|
|
309
|
+
"Global memory keyword index is unavailable.",
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
if (!Number.isSafeInteger(limit) || limit < 1) {
|
|
313
|
+
throw new MemoryError(
|
|
314
|
+
"memory_search_limit_invalid",
|
|
315
|
+
"Memory search limit must be a positive safe integer.",
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
const matchExpression = buildFtsMatchExpression(keywords);
|
|
319
|
+
if (matchExpression === null) {
|
|
320
|
+
return Object.freeze([]);
|
|
321
|
+
}
|
|
322
|
+
const matches: MemoryFtsMatch[] = [];
|
|
323
|
+
const rows = this.database
|
|
324
|
+
.query(
|
|
325
|
+
`SELECT m.memory_id, m.text, m.summary, m.source_workspace,
|
|
326
|
+
m.source_session_id, m.created_at,
|
|
327
|
+
bm25(${MEMORIES_FTS_TABLE}, 10.0, 1.0) AS bm25
|
|
328
|
+
FROM ${MEMORIES_FTS_TABLE}
|
|
329
|
+
JOIN memories AS m ON m.rowid = ${MEMORIES_FTS_TABLE}.rowid
|
|
330
|
+
WHERE ${MEMORIES_FTS_TABLE} MATCH ?
|
|
331
|
+
ORDER BY bm25 ASC, m.created_at DESC, m.memory_id ASC
|
|
332
|
+
LIMIT ?`,
|
|
333
|
+
)
|
|
334
|
+
.iterate(matchExpression, limit);
|
|
335
|
+
for (const rowValue of rows) {
|
|
336
|
+
const row = sqlRecord(rowValue, "memory fts row");
|
|
337
|
+
const bm25 = Number(row.bm25);
|
|
338
|
+
if (!Number.isFinite(bm25)) {
|
|
339
|
+
throw new MemoryError(
|
|
340
|
+
"memory_store_read_failed",
|
|
341
|
+
"Memory FTS bm25 score must be finite.",
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
matches.push(
|
|
345
|
+
Object.freeze({
|
|
346
|
+
memoryId: sqlString(row.memory_id, "memory_id"),
|
|
347
|
+
text: sqlString(row.text, "memory text"),
|
|
348
|
+
summary: sqlSummary(row.summary, "memory summary"),
|
|
349
|
+
bm25,
|
|
350
|
+
sourceWorkspace: sqlString(row.source_workspace, "memory source_workspace"),
|
|
351
|
+
sourceSessionId: sqlString(row.source_session_id, "memory source_session_id"),
|
|
352
|
+
createdAt: requireUtcTimestamp(
|
|
353
|
+
sqlString(row.created_at, "memory created_at"),
|
|
354
|
+
"memory created_at",
|
|
355
|
+
),
|
|
356
|
+
}),
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
return Object.freeze(matches);
|
|
360
|
+
}
|
|
361
|
+
|
|
268
362
|
listStoredMemories(): readonly StoredMemorySummary[] {
|
|
269
363
|
this.requireOpen();
|
|
270
364
|
const memories: StoredMemorySummary[] = [];
|
|
271
365
|
const rows = this.database
|
|
272
366
|
.query(
|
|
273
|
-
`SELECT memory_id, text, source_workspace,
|
|
367
|
+
`SELECT memory_id, text, summary, source_workspace, source_session_id,
|
|
368
|
+
created_at
|
|
274
369
|
FROM memories
|
|
275
370
|
ORDER BY created_at DESC, memory_id DESC`,
|
|
276
371
|
)
|
|
@@ -281,7 +376,9 @@ export class MemoryStore {
|
|
|
281
376
|
Object.freeze({
|
|
282
377
|
memoryId: sqlString(row.memory_id, "memory_id"),
|
|
283
378
|
text: sqlString(row.text, "memory text"),
|
|
379
|
+
summary: sqlSummary(row.summary, "memory summary"),
|
|
284
380
|
sourceWorkspace: sqlString(row.source_workspace, "memory source_workspace"),
|
|
381
|
+
sourceSessionId: sqlString(row.source_session_id, "memory source_session_id"),
|
|
285
382
|
createdAt: requireUtcTimestamp(
|
|
286
383
|
sqlString(row.created_at, "memory created_at"),
|
|
287
384
|
"memory created_at",
|
|
@@ -292,6 +389,34 @@ export class MemoryStore {
|
|
|
292
389
|
return Object.freeze(memories);
|
|
293
390
|
}
|
|
294
391
|
|
|
392
|
+
getById(memoryId: string): StoredMemoryRecord | undefined {
|
|
393
|
+
this.requireOpen();
|
|
394
|
+
const rowValue = this.database
|
|
395
|
+
.query(
|
|
396
|
+
`SELECT memory_id, text, summary, source_workspace, source_session_id,
|
|
397
|
+
source_turn_id, created_at
|
|
398
|
+
FROM memories
|
|
399
|
+
WHERE memory_id = ?`,
|
|
400
|
+
)
|
|
401
|
+
.get(memoryId);
|
|
402
|
+
if (rowValue === null) {
|
|
403
|
+
return undefined;
|
|
404
|
+
}
|
|
405
|
+
const row = sqlRecord(rowValue, "memory row");
|
|
406
|
+
return Object.freeze({
|
|
407
|
+
memoryId: sqlString(row.memory_id, "memory_id"),
|
|
408
|
+
text: sqlString(row.text, "memory text"),
|
|
409
|
+
summary: sqlSummary(row.summary, "memory summary"),
|
|
410
|
+
sourceWorkspace: sqlString(row.source_workspace, "memory source_workspace"),
|
|
411
|
+
sourceSessionId: sqlString(row.source_session_id, "memory source_session_id"),
|
|
412
|
+
sourceTurnId: sqlString(row.source_turn_id, "memory source_turn_id"),
|
|
413
|
+
createdAt: requireUtcTimestamp(
|
|
414
|
+
sqlString(row.created_at, "memory created_at"),
|
|
415
|
+
"memory created_at",
|
|
416
|
+
),
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
|
|
295
420
|
count(): number {
|
|
296
421
|
this.requireOpen();
|
|
297
422
|
const row = this.database.query("SELECT COUNT(*) AS count FROM memories").get();
|
|
@@ -350,6 +475,7 @@ function configureDatabase(database: Database, busyTimeoutMs: number): void {
|
|
|
350
475
|
function initializeOrVerifySchema(
|
|
351
476
|
database: Database,
|
|
352
477
|
embedding: MemoryEmbeddingIdentity,
|
|
478
|
+
databasePath: string,
|
|
353
479
|
): void {
|
|
354
480
|
let started = false;
|
|
355
481
|
try {
|
|
@@ -367,7 +493,7 @@ function initializeOrVerifySchema(
|
|
|
367
493
|
insert.run(key, value);
|
|
368
494
|
}
|
|
369
495
|
}
|
|
370
|
-
verifySchema(database, embedding);
|
|
496
|
+
verifySchema(database, embedding, databasePath);
|
|
371
497
|
database.exec("COMMIT");
|
|
372
498
|
started = false;
|
|
373
499
|
} catch (error) {
|
|
@@ -391,11 +517,15 @@ function initializeOrVerifySchema(
|
|
|
391
517
|
}
|
|
392
518
|
}
|
|
393
519
|
|
|
394
|
-
function verifySchema(
|
|
520
|
+
function verifySchema(
|
|
521
|
+
database: Database,
|
|
522
|
+
embedding: MemoryEmbeddingIdentity,
|
|
523
|
+
databasePath: string,
|
|
524
|
+
): void {
|
|
395
525
|
const objects = applicationSchemaObjects(database);
|
|
396
526
|
if (objects.length !== EXPECTED_SCHEMA.size) {
|
|
397
|
-
throw
|
|
398
|
-
|
|
527
|
+
throw unsupportedSchemaError(
|
|
528
|
+
databasePath,
|
|
399
529
|
"Global memory schema has unexpected objects.",
|
|
400
530
|
);
|
|
401
531
|
}
|
|
@@ -403,8 +533,8 @@ function verifySchema(database: Database, embedding: MemoryEmbeddingIdentity): v
|
|
|
403
533
|
const key = `${row.type}:${row.name}`;
|
|
404
534
|
const expected = EXPECTED_SCHEMA.get(key);
|
|
405
535
|
if (expected === undefined || normalizeSql(row.sql) !== normalizeSql(expected)) {
|
|
406
|
-
throw
|
|
407
|
-
|
|
536
|
+
throw unsupportedSchemaError(
|
|
537
|
+
databasePath,
|
|
408
538
|
`Global memory schema object ${key} is incompatible.`,
|
|
409
539
|
);
|
|
410
540
|
}
|
|
@@ -434,11 +564,16 @@ function verifySchema(database: Database, embedding: MemoryEmbeddingIdentity): v
|
|
|
434
564
|
const identityMismatch = identityKeys.some(
|
|
435
565
|
(key) => actual.has(key) && actual.get(key) !== expected.get(key),
|
|
436
566
|
);
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
567
|
+
if (identityMismatch) {
|
|
568
|
+
throw new MemoryError(
|
|
569
|
+
"memory_embedding_identity_mismatch",
|
|
570
|
+
"Configured embedding profile does not match the existing global memory database.",
|
|
571
|
+
);
|
|
572
|
+
}
|
|
573
|
+
throw unsupportedSchemaError(
|
|
574
|
+
databasePath,
|
|
575
|
+
"Global memory metadata does not match schema version " +
|
|
576
|
+
`${MEMORY_SCHEMA_VERSION}.`,
|
|
442
577
|
);
|
|
443
578
|
}
|
|
444
579
|
|
|
@@ -446,6 +581,15 @@ function verifySchema(database: Database, embedding: MemoryEmbeddingIdentity): v
|
|
|
446
581
|
expectedEmbeddingBlobBytes(storedDimensions);
|
|
447
582
|
}
|
|
448
583
|
|
|
584
|
+
function unsupportedSchemaError(databasePath: string, detail: string): MemoryError {
|
|
585
|
+
return new MemoryError(
|
|
586
|
+
"memory_schema_unsupported",
|
|
587
|
+
`${detail} Delete ${databasePath} (including ${databasePath}-wal and ` +
|
|
588
|
+
`${databasePath}-shm) and restart to finish the memory schema v` +
|
|
589
|
+
`${MEMORY_SCHEMA_VERSION} upgrade.`,
|
|
590
|
+
);
|
|
591
|
+
}
|
|
592
|
+
|
|
449
593
|
function applicationSchemaObjects(
|
|
450
594
|
database: Database,
|
|
451
595
|
): Array<{ readonly type: string; readonly name: string; readonly sql: string }> {
|
|
@@ -454,6 +598,7 @@ function applicationSchemaObjects(
|
|
|
454
598
|
`SELECT type, name, sql
|
|
455
599
|
FROM sqlite_schema
|
|
456
600
|
WHERE type IN ('table', 'index') AND name NOT LIKE 'sqlite_%'
|
|
601
|
+
AND name <> '${MEMORIES_FTS_TABLE}' AND name NOT LIKE '${MEMORIES_FTS_TABLE}\\_%' ESCAPE '\\'
|
|
457
602
|
ORDER BY type, name`,
|
|
458
603
|
)
|
|
459
604
|
.all()
|
|
@@ -479,6 +624,60 @@ function metadataEntries(
|
|
|
479
624
|
]);
|
|
480
625
|
}
|
|
481
626
|
|
|
627
|
+
function ensureFtsIndex(database: Database): void {
|
|
628
|
+
runImmediateTransaction(database, () => {
|
|
629
|
+
const rowValue = database
|
|
630
|
+
.query(
|
|
631
|
+
`SELECT sql FROM sqlite_schema
|
|
632
|
+
WHERE type = 'table' AND name = '${MEMORIES_FTS_TABLE}'`,
|
|
633
|
+
)
|
|
634
|
+
.get();
|
|
635
|
+
if (rowValue === null) {
|
|
636
|
+
database.exec(CREATE_MEMORIES_FTS_SQL);
|
|
637
|
+
backfillFtsIndex(database);
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
const row = sqlRecord(rowValue, "memories_fts schema");
|
|
641
|
+
const sql = sqlString(row.sql, "memories_fts SQL");
|
|
642
|
+
if (normalizeSql(sql) !== normalizeSql(CREATE_MEMORIES_FTS_SQL)) {
|
|
643
|
+
database.exec(`DROP TABLE ${MEMORIES_FTS_TABLE}`);
|
|
644
|
+
database.exec(CREATE_MEMORIES_FTS_SQL);
|
|
645
|
+
backfillFtsIndex(database);
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
const memoryCount = countRows(database, "memories");
|
|
649
|
+
const ftsCount = countRows(database, `${MEMORIES_FTS_TABLE}_docsize`);
|
|
650
|
+
if (memoryCount !== ftsCount) {
|
|
651
|
+
database.exec(
|
|
652
|
+
`INSERT INTO ${MEMORIES_FTS_TABLE}(${MEMORIES_FTS_TABLE}) VALUES('rebuild')`,
|
|
653
|
+
);
|
|
654
|
+
}
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
function backfillFtsIndex(database: Database): void {
|
|
659
|
+
database.exec(
|
|
660
|
+
`INSERT INTO ${MEMORIES_FTS_TABLE}(rowid, text, summary)
|
|
661
|
+
SELECT rowid, text, summary FROM memories`,
|
|
662
|
+
);
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
function countRows(database: Database, table: string): number {
|
|
666
|
+
const row = sqlRecord(
|
|
667
|
+
database.query(`SELECT COUNT(*) AS count FROM ${table}`).get(),
|
|
668
|
+
`${table} count`,
|
|
669
|
+
);
|
|
670
|
+
return Number(row.count);
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
export function buildFtsMatchExpression(keywords: readonly string[]): string | null {
|
|
674
|
+
const phrases = keywords
|
|
675
|
+
.map((keyword) => keyword.trim())
|
|
676
|
+
.filter((keyword) => [...keyword].length >= 3)
|
|
677
|
+
.map((keyword) => `"${keyword.replaceAll('"', '""')}"`);
|
|
678
|
+
return phrases.length === 0 ? null : phrases.join(" OR ");
|
|
679
|
+
}
|
|
680
|
+
|
|
482
681
|
function runImmediateTransaction(database: Database, operation: () => void): void {
|
|
483
682
|
let started = false;
|
|
484
683
|
try {
|
|
@@ -519,10 +718,10 @@ function validateWriteBatch(input: MemoryWriteBatch, dimensions: number): void {
|
|
|
519
718
|
"Memory source session and turn IDs must not be empty.",
|
|
520
719
|
);
|
|
521
720
|
}
|
|
522
|
-
if (input.candidates.length >
|
|
721
|
+
if (input.candidates.length > 1) {
|
|
523
722
|
throw new MemoryError(
|
|
524
723
|
"memory_write_invalid",
|
|
525
|
-
|
|
724
|
+
"A memory batch may contain at most 1 candidate.",
|
|
526
725
|
);
|
|
527
726
|
}
|
|
528
727
|
for (const candidate of input.candidates) {
|
|
@@ -530,11 +729,12 @@ function validateWriteBatch(input: MemoryWriteBatch, dimensions: number): void {
|
|
|
530
729
|
candidate.text.trim() !== candidate.text ||
|
|
531
730
|
Buffer.byteLength(candidate.text, "utf8") < 1 ||
|
|
532
731
|
Buffer.byteLength(candidate.text, "utf8") > MAX_MEMORY_TEXT_BYTES ||
|
|
732
|
+
Buffer.byteLength(candidate.summary, "utf8") > MAX_MEMORY_SUMMARY_BYTES ||
|
|
533
733
|
candidate.embedding.length !== dimensions
|
|
534
734
|
) {
|
|
535
735
|
throw new MemoryError(
|
|
536
736
|
"memory_write_invalid",
|
|
537
|
-
"Memory candidate text or embedding is invalid.",
|
|
737
|
+
"Memory candidate text, summary, or embedding is invalid.",
|
|
538
738
|
);
|
|
539
739
|
}
|
|
540
740
|
}
|
|
@@ -664,6 +864,13 @@ function sqlString(value: unknown, name: string): string {
|
|
|
664
864
|
return value;
|
|
665
865
|
}
|
|
666
866
|
|
|
867
|
+
function sqlSummary(value: unknown, name: string): string {
|
|
868
|
+
if (typeof value !== "string") {
|
|
869
|
+
throw new MemoryError("memory_store_read_failed", `${name} must be a string.`);
|
|
870
|
+
}
|
|
871
|
+
return value;
|
|
872
|
+
}
|
|
873
|
+
|
|
667
874
|
function requireUtcTimestamp(value: string, name: string): string {
|
|
668
875
|
if (!value.endsWith("Z") || Number.isNaN(Date.parse(value))) {
|
|
669
876
|
throw new MemoryError(
|