tinker-agent 1.3.0 → 1.5.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 +39 -1
- package/README.md +271 -72
- package/bin/tinker.js +75 -25
- package/package.json +12 -3
- package/src/agent/runtime-session.ts +113 -15
- package/src/cli/command-line.ts +291 -0
- package/src/cli/config.ts +158 -262
- package/src/cli/index.ts +33 -21
- package/src/cli/main.ts +213 -0
- package/src/cli/model-profiles.ts +226 -72
- package/src/cli/output.ts +113 -0
- package/src/cli/package-metadata.ts +36 -0
- package/src/cli/prompt-source.ts +229 -0
- package/src/cli/public-cli-contract.ts +69 -0
- package/src/cli/public-config-contract.ts +732 -0
- package/src/cli/run-runner.ts +17 -12
- package/src/cli/runner-dependencies.ts +108 -0
- package/src/cli/tui-memory.ts +67 -0
- package/src/cli/tui-runner.tsx +79 -49
- package/src/context/context-policy.ts +2 -2
- package/src/events/stdout-event-printer.ts +1 -0
- package/src/mcp/mcp-manager.ts +2 -19
- package/src/mcp/mcp-tool-executor.ts +3 -4
- package/src/memory/contracts.ts +148 -0
- package/src/memory/embedding-client.ts +105 -0
- package/src/memory/memory-coordinator.ts +556 -0
- package/src/memory/memory-extractor.ts +231 -0
- package/src/memory/memory-log.ts +88 -0
- package/src/memory/memory-search-tool.ts +100 -0
- package/src/memory/memory-store.ts +687 -0
- package/src/memory/vector.ts +153 -0
- package/src/model/fake-model-client.ts +971 -3
- package/src/model/model-context-profile.ts +0 -30
- package/src/observation/observation-builder.ts +20 -0
- package/src/session/session-store.ts +123 -0
- package/src/tools/bash.ts +8 -25
- package/src/tools/grep.ts +9 -1
- package/src/tools/registry.ts +19 -1
- package/src/tools/ripgrep.ts +24 -27
- package/src/tools/types.ts +16 -0
- package/src/tools/web-fetch/index.ts +2 -15
- package/src/tui/app.tsx +72 -2
- package/src/tui/clipboard.ts +22 -0
- package/src/tui/components/footer.tsx +9 -4
- package/src/tui/components/memory-browser.tsx +151 -0
- package/src/tui/components/prompt-input.tsx +6 -3
- package/src/tui/event-store.ts +9 -2
- package/src/tui/slash-commands.ts +88 -24
- package/src/tui/workspace-file-search.ts +78 -71
|
@@ -0,0 +1,687 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { chmod, lstat, mkdir, open } from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { Database } from "bun:sqlite";
|
|
6
|
+
import { createUuidV7, isCanonicalUuidV7 } from "../ids/uuid-v7";
|
|
7
|
+
import {
|
|
8
|
+
MAX_MEMORIES_PER_TURN,
|
|
9
|
+
MAX_MEMORY_TEXT_BYTES,
|
|
10
|
+
MEMORY_SCHEMA_VERSION,
|
|
11
|
+
MEMORY_SEARCH_LIMIT,
|
|
12
|
+
MemoryError,
|
|
13
|
+
type MemoryEmbeddingIdentity,
|
|
14
|
+
type MemoryPaths,
|
|
15
|
+
type MemorySearchMatch,
|
|
16
|
+
type StoredMemorySummary,
|
|
17
|
+
type MemoryWriteBatch,
|
|
18
|
+
type MemoryWriteResult,
|
|
19
|
+
} from "./contracts";
|
|
20
|
+
import {
|
|
21
|
+
cosineFromNormalized,
|
|
22
|
+
decodeEmbedding,
|
|
23
|
+
encodeEmbedding,
|
|
24
|
+
expectedEmbeddingBlobBytes,
|
|
25
|
+
} from "./vector";
|
|
26
|
+
|
|
27
|
+
const DEFAULT_BUSY_TIMEOUT_MS = 5_000;
|
|
28
|
+
|
|
29
|
+
const CREATE_MEMORY_META_SQL = `CREATE TABLE memory_meta (
|
|
30
|
+
key TEXT PRIMARY KEY,
|
|
31
|
+
value TEXT NOT NULL
|
|
32
|
+
) STRICT`;
|
|
33
|
+
|
|
34
|
+
const CREATE_MEMORIES_SQL = `CREATE TABLE memories (
|
|
35
|
+
memory_id TEXT PRIMARY KEY,
|
|
36
|
+
text TEXT NOT NULL,
|
|
37
|
+
text_sha256 TEXT NOT NULL UNIQUE,
|
|
38
|
+
embedding BLOB NOT NULL,
|
|
39
|
+
source_workspace TEXT NOT NULL,
|
|
40
|
+
source_session_id TEXT NOT NULL,
|
|
41
|
+
source_turn_id TEXT NOT NULL,
|
|
42
|
+
created_at TEXT NOT NULL
|
|
43
|
+
) STRICT`;
|
|
44
|
+
|
|
45
|
+
const CREATE_MEMORIES_INDEX_SQL = `CREATE INDEX memories_created_at
|
|
46
|
+
ON memories(created_at DESC)`;
|
|
47
|
+
|
|
48
|
+
const EXPECTED_SCHEMA = new Map([
|
|
49
|
+
["index:memories_created_at", CREATE_MEMORIES_INDEX_SQL],
|
|
50
|
+
["table:memories", CREATE_MEMORIES_SQL],
|
|
51
|
+
["table:memory_meta", CREATE_MEMORY_META_SQL],
|
|
52
|
+
]);
|
|
53
|
+
|
|
54
|
+
export type OpenMemoryStoreInput = {
|
|
55
|
+
readonly paths?: MemoryPaths;
|
|
56
|
+
readonly embedding: MemoryEmbeddingIdentity;
|
|
57
|
+
readonly busyTimeoutMs?: number;
|
|
58
|
+
readonly clock?: () => string;
|
|
59
|
+
readonly createMemoryId?: () => string;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export function resolveMemoryPaths(homeRoot = os.homedir()): MemoryPaths {
|
|
63
|
+
const directory = path.join(homeRoot, ".tinker", "memory");
|
|
64
|
+
return Object.freeze({
|
|
65
|
+
directory,
|
|
66
|
+
database: path.join(directory, "memory.sqlite"),
|
|
67
|
+
log: path.join(directory, "memory-log.jsonl"),
|
|
68
|
+
extractedLog: path.join(directory, "extracted-memories.log"),
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export class MemoryStore {
|
|
73
|
+
readonly paths: MemoryPaths;
|
|
74
|
+
readonly dimensions: number;
|
|
75
|
+
private closed = false;
|
|
76
|
+
|
|
77
|
+
private constructor(
|
|
78
|
+
private readonly database: Database,
|
|
79
|
+
input: Required<Pick<OpenMemoryStoreInput, "clock" | "createMemoryId">> & {
|
|
80
|
+
readonly paths: MemoryPaths;
|
|
81
|
+
readonly embedding: MemoryEmbeddingIdentity;
|
|
82
|
+
},
|
|
83
|
+
) {
|
|
84
|
+
this.paths = input.paths;
|
|
85
|
+
this.dimensions = input.embedding.dimensions;
|
|
86
|
+
this.clock = input.clock;
|
|
87
|
+
this.createMemoryId = input.createMemoryId;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
private readonly clock: () => string;
|
|
91
|
+
private readonly createMemoryId: () => string;
|
|
92
|
+
|
|
93
|
+
static async open(input: OpenMemoryStoreInput): Promise<MemoryStore> {
|
|
94
|
+
const paths = input.paths ?? resolveMemoryPaths();
|
|
95
|
+
validateMemoryPaths(paths);
|
|
96
|
+
validateEmbeddingIdentity(input.embedding);
|
|
97
|
+
const busyTimeoutMs = input.busyTimeoutMs ?? DEFAULT_BUSY_TIMEOUT_MS;
|
|
98
|
+
if (!Number.isSafeInteger(busyTimeoutMs) || busyTimeoutMs < 1) {
|
|
99
|
+
throw new MemoryError(
|
|
100
|
+
"memory_store_config_invalid",
|
|
101
|
+
"Memory SQLite busy timeout must be a positive safe integer.",
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
await ensurePrivateDirectory(paths.directory);
|
|
106
|
+
await validatePrivateOptionalFile(paths.log);
|
|
107
|
+
await validatePrivateOptionalFile(paths.extractedLog);
|
|
108
|
+
await validatePrivateOptionalFile(`${paths.database}-wal`);
|
|
109
|
+
await validatePrivateOptionalFile(`${paths.database}-shm`);
|
|
110
|
+
await ensurePrivateFile(paths.database);
|
|
111
|
+
|
|
112
|
+
const walExisted = await pathExists(`${paths.database}-wal`);
|
|
113
|
+
const shmExisted = await pathExists(`${paths.database}-shm`);
|
|
114
|
+
let database: Database | undefined;
|
|
115
|
+
try {
|
|
116
|
+
database = new Database(paths.database, {
|
|
117
|
+
create: false,
|
|
118
|
+
readwrite: true,
|
|
119
|
+
strict: true,
|
|
120
|
+
safeIntegers: true,
|
|
121
|
+
});
|
|
122
|
+
configureDatabase(database, busyTimeoutMs);
|
|
123
|
+
initializeOrVerifySchema(database, input.embedding);
|
|
124
|
+
await secureCreatedAuxiliaryFile(`${paths.database}-wal`, walExisted);
|
|
125
|
+
await secureCreatedAuxiliaryFile(`${paths.database}-shm`, shmExisted);
|
|
126
|
+
await validatePrivateFile(paths.database);
|
|
127
|
+
await ensurePrivateFile(paths.extractedLog);
|
|
128
|
+
return new MemoryStore(database, {
|
|
129
|
+
paths,
|
|
130
|
+
embedding: Object.freeze({ ...input.embedding }),
|
|
131
|
+
clock: input.clock ?? (() => new Date().toISOString()),
|
|
132
|
+
createMemoryId: input.createMemoryId ?? createUuidV7,
|
|
133
|
+
});
|
|
134
|
+
} catch (error) {
|
|
135
|
+
database?.close();
|
|
136
|
+
if (error instanceof MemoryError) {
|
|
137
|
+
throw error;
|
|
138
|
+
}
|
|
139
|
+
const code =
|
|
140
|
+
sqliteCode(error) === "SQLITE_BUSY"
|
|
141
|
+
? "memory_store_busy"
|
|
142
|
+
: "memory_store_open_failed";
|
|
143
|
+
throw new MemoryError(code, "Global memory store could not be opened.", {
|
|
144
|
+
cause: error,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
insertBatch(input: MemoryWriteBatch): MemoryWriteResult {
|
|
150
|
+
this.requireOpen();
|
|
151
|
+
validateWriteBatch(input, this.dimensions);
|
|
152
|
+
if (input.candidates.length === 0) {
|
|
153
|
+
return Object.freeze({
|
|
154
|
+
written: 0,
|
|
155
|
+
duplicate: 0,
|
|
156
|
+
inserted: Object.freeze([]),
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const createdAt = this.clock();
|
|
161
|
+
requireUtcTimestamp(createdAt, "Memory creation timestamp");
|
|
162
|
+
const inserted: MemoryWriteResult["inserted"][number][] = [];
|
|
163
|
+
runImmediateTransaction(this.database, () => {
|
|
164
|
+
const insert = this.database.query(
|
|
165
|
+
`INSERT INTO memories (
|
|
166
|
+
memory_id, text, text_sha256, embedding, source_workspace,
|
|
167
|
+
source_session_id, source_turn_id, created_at
|
|
168
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
169
|
+
ON CONFLICT(text_sha256) DO NOTHING`,
|
|
170
|
+
);
|
|
171
|
+
for (const candidate of input.candidates) {
|
|
172
|
+
const memoryId = this.createMemoryId();
|
|
173
|
+
if (!isCanonicalUuidV7(memoryId)) {
|
|
174
|
+
throw new MemoryError(
|
|
175
|
+
"memory_write_failed",
|
|
176
|
+
"Memory ID factory did not produce a UUIDv7.",
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
const result = insert.run(
|
|
180
|
+
memoryId,
|
|
181
|
+
candidate.text,
|
|
182
|
+
sha256(candidate.text),
|
|
183
|
+
encodeEmbedding(candidate.embedding),
|
|
184
|
+
input.workspaceRoot,
|
|
185
|
+
input.sessionId,
|
|
186
|
+
input.turnId,
|
|
187
|
+
createdAt,
|
|
188
|
+
);
|
|
189
|
+
const changes = Number(result.changes);
|
|
190
|
+
if (changes !== 0 && changes !== 1) {
|
|
191
|
+
throw new MemoryError(
|
|
192
|
+
"memory_write_failed",
|
|
193
|
+
`Memory insert changed ${changes} rows.`,
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
if (changes === 1) {
|
|
197
|
+
inserted.push(
|
|
198
|
+
Object.freeze({
|
|
199
|
+
memoryId,
|
|
200
|
+
text: candidate.text,
|
|
201
|
+
createdAt,
|
|
202
|
+
}),
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
return Object.freeze({
|
|
208
|
+
written: inserted.length,
|
|
209
|
+
duplicate: input.candidates.length - inserted.length,
|
|
210
|
+
inserted: Object.freeze(inserted),
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
search(
|
|
215
|
+
queryEmbedding: Float32Array,
|
|
216
|
+
limit = MEMORY_SEARCH_LIMIT,
|
|
217
|
+
): readonly MemorySearchMatch[] {
|
|
218
|
+
this.requireOpen();
|
|
219
|
+
if (
|
|
220
|
+
queryEmbedding.length !== this.dimensions ||
|
|
221
|
+
[...queryEmbedding].some((value) => !Number.isFinite(value))
|
|
222
|
+
) {
|
|
223
|
+
throw new MemoryError(
|
|
224
|
+
"memory_embedding_dimensions_invalid",
|
|
225
|
+
"Query embedding does not match the memory embedding space.",
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
if (!Number.isSafeInteger(limit) || limit < 1) {
|
|
229
|
+
throw new MemoryError(
|
|
230
|
+
"memory_search_limit_invalid",
|
|
231
|
+
"Memory search limit must be a positive safe integer.",
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const expectedBlobBytes = expectedEmbeddingBlobBytes(this.dimensions);
|
|
236
|
+
const matches: MemorySearchMatch[] = [];
|
|
237
|
+
const rows = this.database
|
|
238
|
+
.query(
|
|
239
|
+
`SELECT memory_id, text, embedding, source_workspace, created_at
|
|
240
|
+
FROM memories`,
|
|
241
|
+
)
|
|
242
|
+
.iterate();
|
|
243
|
+
for (const rowValue of rows) {
|
|
244
|
+
const row = sqlRecord(rowValue, "memory row");
|
|
245
|
+
const embedding = decodeEmbedding(row.embedding, expectedBlobBytes);
|
|
246
|
+
matches.push(
|
|
247
|
+
Object.freeze({
|
|
248
|
+
memoryId: sqlString(row.memory_id, "memory_id"),
|
|
249
|
+
text: sqlString(row.text, "memory text"),
|
|
250
|
+
score: cosineFromNormalized(queryEmbedding, embedding),
|
|
251
|
+
sourceWorkspace: sqlString(row.source_workspace, "memory source_workspace"),
|
|
252
|
+
createdAt: requireUtcTimestamp(
|
|
253
|
+
sqlString(row.created_at, "memory created_at"),
|
|
254
|
+
"memory created_at",
|
|
255
|
+
),
|
|
256
|
+
}),
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
matches.sort(
|
|
260
|
+
(left, right) =>
|
|
261
|
+
right.score - left.score ||
|
|
262
|
+
right.createdAt.localeCompare(left.createdAt) ||
|
|
263
|
+
left.memoryId.localeCompare(right.memoryId),
|
|
264
|
+
);
|
|
265
|
+
return Object.freeze(matches.slice(0, limit));
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
listStoredMemories(): readonly StoredMemorySummary[] {
|
|
269
|
+
this.requireOpen();
|
|
270
|
+
const memories: StoredMemorySummary[] = [];
|
|
271
|
+
const rows = this.database
|
|
272
|
+
.query(
|
|
273
|
+
`SELECT memory_id, text, source_workspace, created_at
|
|
274
|
+
FROM memories
|
|
275
|
+
ORDER BY created_at DESC, memory_id DESC`,
|
|
276
|
+
)
|
|
277
|
+
.iterate();
|
|
278
|
+
for (const rowValue of rows) {
|
|
279
|
+
const row = sqlRecord(rowValue, "memory row");
|
|
280
|
+
memories.push(
|
|
281
|
+
Object.freeze({
|
|
282
|
+
memoryId: sqlString(row.memory_id, "memory_id"),
|
|
283
|
+
text: sqlString(row.text, "memory text"),
|
|
284
|
+
sourceWorkspace: sqlString(row.source_workspace, "memory source_workspace"),
|
|
285
|
+
createdAt: requireUtcTimestamp(
|
|
286
|
+
sqlString(row.created_at, "memory created_at"),
|
|
287
|
+
"memory created_at",
|
|
288
|
+
),
|
|
289
|
+
}),
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
return Object.freeze(memories);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
count(): number {
|
|
296
|
+
this.requireOpen();
|
|
297
|
+
const row = this.database.query("SELECT COUNT(*) AS count FROM memories").get();
|
|
298
|
+
const count = sqlRecord(row, "memory count").count;
|
|
299
|
+
const number = typeof count === "bigint" ? Number(count) : count;
|
|
300
|
+
if (!Number.isSafeInteger(number) || (number as number) < 0) {
|
|
301
|
+
throw new MemoryError("memory_store_read_failed", "Memory count is invalid.");
|
|
302
|
+
}
|
|
303
|
+
return number as number;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
close(): void {
|
|
307
|
+
if (this.closed) {
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
this.closed = true;
|
|
311
|
+
this.database.close();
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
private requireOpen(): void {
|
|
315
|
+
if (this.closed) {
|
|
316
|
+
throw new MemoryError("memory_store_closed", "Global memory store is closed.");
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function configureDatabase(database: Database, busyTimeoutMs: number): void {
|
|
322
|
+
database.exec(`PRAGMA busy_timeout = ${busyTimeoutMs}`);
|
|
323
|
+
const busyRow = sqlRecord(
|
|
324
|
+
database.query("PRAGMA busy_timeout").get(),
|
|
325
|
+
"busy_timeout",
|
|
326
|
+
);
|
|
327
|
+
if (!Object.values(busyRow).some((value) => Number(value) === busyTimeoutMs)) {
|
|
328
|
+
throw new MemoryError(
|
|
329
|
+
"memory_wal_unavailable",
|
|
330
|
+
"SQLite busy timeout could not be enabled.",
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const walRow = sqlRecord(
|
|
335
|
+
database.query("PRAGMA journal_mode = WAL").get(),
|
|
336
|
+
"journal_mode",
|
|
337
|
+
);
|
|
338
|
+
if (
|
|
339
|
+
!Object.values(walRow).some(
|
|
340
|
+
(value) => typeof value === "string" && value.toLowerCase() === "wal",
|
|
341
|
+
)
|
|
342
|
+
) {
|
|
343
|
+
throw new MemoryError(
|
|
344
|
+
"memory_wal_unavailable",
|
|
345
|
+
"SQLite WAL mode could not be enabled.",
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function initializeOrVerifySchema(
|
|
351
|
+
database: Database,
|
|
352
|
+
embedding: MemoryEmbeddingIdentity,
|
|
353
|
+
): void {
|
|
354
|
+
let started = false;
|
|
355
|
+
try {
|
|
356
|
+
database.exec("BEGIN IMMEDIATE");
|
|
357
|
+
started = true;
|
|
358
|
+
const objects = applicationSchemaObjects(database);
|
|
359
|
+
if (objects.length === 0) {
|
|
360
|
+
database.exec(CREATE_MEMORY_META_SQL);
|
|
361
|
+
database.exec(CREATE_MEMORIES_SQL);
|
|
362
|
+
database.exec(CREATE_MEMORIES_INDEX_SQL);
|
|
363
|
+
const insert = database.query(
|
|
364
|
+
"INSERT INTO memory_meta(key, value) VALUES (?, ?)",
|
|
365
|
+
);
|
|
366
|
+
for (const [key, value] of metadataEntries(embedding)) {
|
|
367
|
+
insert.run(key, value);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
verifySchema(database, embedding);
|
|
371
|
+
database.exec("COMMIT");
|
|
372
|
+
started = false;
|
|
373
|
+
} catch (error) {
|
|
374
|
+
if (started) {
|
|
375
|
+
try {
|
|
376
|
+
database.exec("ROLLBACK");
|
|
377
|
+
} catch {
|
|
378
|
+
// Preserve the initialization failure.
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
if (error instanceof MemoryError) {
|
|
382
|
+
throw error;
|
|
383
|
+
}
|
|
384
|
+
const code =
|
|
385
|
+
sqliteCode(error) === "SQLITE_BUSY"
|
|
386
|
+
? "memory_store_busy"
|
|
387
|
+
: "memory_schema_invalid";
|
|
388
|
+
throw new MemoryError(code, "Global memory schema initialization failed.", {
|
|
389
|
+
cause: error,
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function verifySchema(database: Database, embedding: MemoryEmbeddingIdentity): void {
|
|
395
|
+
const objects = applicationSchemaObjects(database);
|
|
396
|
+
if (objects.length !== EXPECTED_SCHEMA.size) {
|
|
397
|
+
throw new MemoryError(
|
|
398
|
+
"memory_schema_invalid",
|
|
399
|
+
"Global memory schema has unexpected objects.",
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
for (const row of objects) {
|
|
403
|
+
const key = `${row.type}:${row.name}`;
|
|
404
|
+
const expected = EXPECTED_SCHEMA.get(key);
|
|
405
|
+
if (expected === undefined || normalizeSql(row.sql) !== normalizeSql(expected)) {
|
|
406
|
+
throw new MemoryError(
|
|
407
|
+
"memory_schema_invalid",
|
|
408
|
+
`Global memory schema object ${key} is incompatible.`,
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const rows = database
|
|
414
|
+
.query("SELECT key, value FROM memory_meta ORDER BY key")
|
|
415
|
+
.all()
|
|
416
|
+
.map((value) => sqlRecord(value, "memory metadata"));
|
|
417
|
+
const actual = new Map(
|
|
418
|
+
rows.map((row) => [
|
|
419
|
+
sqlString(row.key, "memory metadata key"),
|
|
420
|
+
sqlString(row.value, "memory metadata value"),
|
|
421
|
+
]),
|
|
422
|
+
);
|
|
423
|
+
const expected = new Map(metadataEntries(embedding));
|
|
424
|
+
if (
|
|
425
|
+
actual.size !== expected.size ||
|
|
426
|
+
[...expected].some(([key, value]) => actual.get(key) !== value)
|
|
427
|
+
) {
|
|
428
|
+
const identityKeys = [
|
|
429
|
+
"embedding_profile",
|
|
430
|
+
"embedding_kind",
|
|
431
|
+
"embedding_model",
|
|
432
|
+
"embedding_dimensions",
|
|
433
|
+
];
|
|
434
|
+
const identityMismatch = identityKeys.some(
|
|
435
|
+
(key) => actual.has(key) && actual.get(key) !== expected.get(key),
|
|
436
|
+
);
|
|
437
|
+
throw new MemoryError(
|
|
438
|
+
identityMismatch ? "memory_embedding_identity_mismatch" : "memory_schema_invalid",
|
|
439
|
+
identityMismatch
|
|
440
|
+
? "Configured embedding profile does not match the existing global memory database."
|
|
441
|
+
: "Global memory metadata is invalid.",
|
|
442
|
+
);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const storedDimensions = Number(actual.get("embedding_dimensions"));
|
|
446
|
+
expectedEmbeddingBlobBytes(storedDimensions);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function applicationSchemaObjects(
|
|
450
|
+
database: Database,
|
|
451
|
+
): Array<{ readonly type: string; readonly name: string; readonly sql: string }> {
|
|
452
|
+
return database
|
|
453
|
+
.query(
|
|
454
|
+
`SELECT type, name, sql
|
|
455
|
+
FROM sqlite_schema
|
|
456
|
+
WHERE type IN ('table', 'index') AND name NOT LIKE 'sqlite_%'
|
|
457
|
+
ORDER BY type, name`,
|
|
458
|
+
)
|
|
459
|
+
.all()
|
|
460
|
+
.map((value) => {
|
|
461
|
+
const row = sqlRecord(value, "memory schema object");
|
|
462
|
+
return Object.freeze({
|
|
463
|
+
type: sqlString(row.type, "memory schema type"),
|
|
464
|
+
name: sqlString(row.name, "memory schema name"),
|
|
465
|
+
sql: sqlString(row.sql, "memory schema SQL"),
|
|
466
|
+
});
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function metadataEntries(
|
|
471
|
+
embedding: MemoryEmbeddingIdentity,
|
|
472
|
+
): readonly (readonly [string, string])[] {
|
|
473
|
+
return Object.freeze([
|
|
474
|
+
Object.freeze(["schema_version", String(MEMORY_SCHEMA_VERSION)] as const),
|
|
475
|
+
Object.freeze(["embedding_profile", embedding.name] as const),
|
|
476
|
+
Object.freeze(["embedding_kind", embedding.kind] as const),
|
|
477
|
+
Object.freeze(["embedding_model", embedding.model] as const),
|
|
478
|
+
Object.freeze(["embedding_dimensions", String(embedding.dimensions)] as const),
|
|
479
|
+
]);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function runImmediateTransaction(database: Database, operation: () => void): void {
|
|
483
|
+
let started = false;
|
|
484
|
+
try {
|
|
485
|
+
database.exec("BEGIN IMMEDIATE");
|
|
486
|
+
started = true;
|
|
487
|
+
operation();
|
|
488
|
+
database.exec("COMMIT");
|
|
489
|
+
started = false;
|
|
490
|
+
} catch (error) {
|
|
491
|
+
if (started) {
|
|
492
|
+
try {
|
|
493
|
+
database.exec("ROLLBACK");
|
|
494
|
+
} catch {
|
|
495
|
+
// Preserve the write failure.
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
if (error instanceof MemoryError) {
|
|
499
|
+
throw error;
|
|
500
|
+
}
|
|
501
|
+
throw new MemoryError(
|
|
502
|
+
sqliteCode(error) === "SQLITE_BUSY" ? "memory_store_busy" : "memory_write_failed",
|
|
503
|
+
"Global memory write transaction failed.",
|
|
504
|
+
{ cause: error },
|
|
505
|
+
);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function validateWriteBatch(input: MemoryWriteBatch, dimensions: number): void {
|
|
510
|
+
if (!path.isAbsolute(input.workspaceRoot)) {
|
|
511
|
+
throw new MemoryError(
|
|
512
|
+
"memory_write_invalid",
|
|
513
|
+
"Memory source workspace must be absolute.",
|
|
514
|
+
);
|
|
515
|
+
}
|
|
516
|
+
if (input.sessionId.trim() === "" || input.turnId.trim() === "") {
|
|
517
|
+
throw new MemoryError(
|
|
518
|
+
"memory_write_invalid",
|
|
519
|
+
"Memory source session and turn IDs must not be empty.",
|
|
520
|
+
);
|
|
521
|
+
}
|
|
522
|
+
if (input.candidates.length > MAX_MEMORIES_PER_TURN) {
|
|
523
|
+
throw new MemoryError(
|
|
524
|
+
"memory_write_invalid",
|
|
525
|
+
`A memory batch may contain at most ${MAX_MEMORIES_PER_TURN} candidates.`,
|
|
526
|
+
);
|
|
527
|
+
}
|
|
528
|
+
for (const candidate of input.candidates) {
|
|
529
|
+
if (
|
|
530
|
+
candidate.text.trim() !== candidate.text ||
|
|
531
|
+
Buffer.byteLength(candidate.text, "utf8") < 1 ||
|
|
532
|
+
Buffer.byteLength(candidate.text, "utf8") > MAX_MEMORY_TEXT_BYTES ||
|
|
533
|
+
candidate.embedding.length !== dimensions
|
|
534
|
+
) {
|
|
535
|
+
throw new MemoryError(
|
|
536
|
+
"memory_write_invalid",
|
|
537
|
+
"Memory candidate text or embedding is invalid.",
|
|
538
|
+
);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
function validateEmbeddingIdentity(identity: MemoryEmbeddingIdentity): void {
|
|
544
|
+
if (
|
|
545
|
+
identity.name.trim() === "" ||
|
|
546
|
+
identity.model.trim() === "" ||
|
|
547
|
+
identity.kind !== "openai-compatible" ||
|
|
548
|
+
!Number.isSafeInteger(identity.dimensions) ||
|
|
549
|
+
identity.dimensions < 1
|
|
550
|
+
) {
|
|
551
|
+
throw new MemoryError(
|
|
552
|
+
"memory_store_config_invalid",
|
|
553
|
+
"Memory embedding identity is invalid.",
|
|
554
|
+
);
|
|
555
|
+
}
|
|
556
|
+
expectedEmbeddingBlobBytes(identity.dimensions);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
function validateMemoryPaths(paths: MemoryPaths): void {
|
|
560
|
+
if (
|
|
561
|
+
!path.isAbsolute(paths.directory) ||
|
|
562
|
+
paths.database !== path.join(paths.directory, "memory.sqlite") ||
|
|
563
|
+
paths.log !== path.join(paths.directory, "memory-log.jsonl") ||
|
|
564
|
+
paths.extractedLog !== path.join(paths.directory, "extracted-memories.log")
|
|
565
|
+
) {
|
|
566
|
+
throw new MemoryError(
|
|
567
|
+
"memory_store_config_invalid",
|
|
568
|
+
"Global memory paths are invalid.",
|
|
569
|
+
);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
async function ensurePrivateDirectory(directory: string): Promise<void> {
|
|
574
|
+
const created = await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
575
|
+
if (created !== undefined) {
|
|
576
|
+
await chmod(directory, 0o700);
|
|
577
|
+
}
|
|
578
|
+
const state = await lstat(directory);
|
|
579
|
+
if (!state.isDirectory() || state.isSymbolicLink()) {
|
|
580
|
+
throw new MemoryError(
|
|
581
|
+
"memory_path_insecure",
|
|
582
|
+
"Global memory path is not a real directory.",
|
|
583
|
+
);
|
|
584
|
+
}
|
|
585
|
+
if ((state.mode & 0o777) !== 0o700) {
|
|
586
|
+
throw new MemoryError(
|
|
587
|
+
"memory_path_insecure",
|
|
588
|
+
"Global memory directory permissions must be 0700.",
|
|
589
|
+
);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
async function ensurePrivateFile(filePath: string): Promise<void> {
|
|
594
|
+
try {
|
|
595
|
+
const handle = await open(filePath, "wx", 0o600);
|
|
596
|
+
await handle.close();
|
|
597
|
+
} catch (error) {
|
|
598
|
+
if ((error as NodeJS.ErrnoException).code !== "EEXIST") {
|
|
599
|
+
throw error;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
await validatePrivateFile(filePath);
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
async function validatePrivateOptionalFile(filePath: string): Promise<void> {
|
|
606
|
+
if (await pathExists(filePath)) {
|
|
607
|
+
await validatePrivateFile(filePath);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
async function validatePrivateFile(filePath: string): Promise<void> {
|
|
612
|
+
const state = await lstat(filePath);
|
|
613
|
+
if (!state.isFile() || state.isSymbolicLink() || (state.mode & 0o777) !== 0o600) {
|
|
614
|
+
throw new MemoryError(
|
|
615
|
+
"memory_path_insecure",
|
|
616
|
+
`Global memory file permissions must be 0600: ${filePath}.`,
|
|
617
|
+
);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
async function secureCreatedAuxiliaryFile(
|
|
622
|
+
filePath: string,
|
|
623
|
+
existed: boolean,
|
|
624
|
+
): Promise<void> {
|
|
625
|
+
if (!(await pathExists(filePath))) {
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
if (!existed) {
|
|
629
|
+
await chmod(filePath, 0o600);
|
|
630
|
+
}
|
|
631
|
+
await validatePrivateFile(filePath);
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
async function pathExists(filePath: string): Promise<boolean> {
|
|
635
|
+
return lstat(filePath).then(
|
|
636
|
+
() => true,
|
|
637
|
+
(error: NodeJS.ErrnoException) => {
|
|
638
|
+
if (error.code === "ENOENT") {
|
|
639
|
+
return false;
|
|
640
|
+
}
|
|
641
|
+
throw error;
|
|
642
|
+
},
|
|
643
|
+
);
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
function normalizeSql(sql: string): string {
|
|
647
|
+
return sql.replaceAll(/\s+/g, " ").replace(/;$/, "").trim();
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
function sqlRecord(value: unknown, name: string): Record<string, unknown> {
|
|
651
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
652
|
+
throw new MemoryError("memory_store_read_failed", `${name} must be a SQLite row.`);
|
|
653
|
+
}
|
|
654
|
+
return value as Record<string, unknown>;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
function sqlString(value: unknown, name: string): string {
|
|
658
|
+
if (typeof value !== "string" || value === "") {
|
|
659
|
+
throw new MemoryError(
|
|
660
|
+
"memory_store_read_failed",
|
|
661
|
+
`${name} must be a non-empty string.`,
|
|
662
|
+
);
|
|
663
|
+
}
|
|
664
|
+
return value;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
function requireUtcTimestamp(value: string, name: string): string {
|
|
668
|
+
if (!value.endsWith("Z") || Number.isNaN(Date.parse(value))) {
|
|
669
|
+
throw new MemoryError(
|
|
670
|
+
"memory_store_read_failed",
|
|
671
|
+
`${name} must be a UTC ISO-8601 timestamp.`,
|
|
672
|
+
);
|
|
673
|
+
}
|
|
674
|
+
return value;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
function sqliteCode(error: unknown): string | undefined {
|
|
678
|
+
if (typeof error !== "object" || error === null || !("code" in error)) {
|
|
679
|
+
return undefined;
|
|
680
|
+
}
|
|
681
|
+
const code = (error as { code?: unknown }).code;
|
|
682
|
+
return typeof code === "string" ? code : undefined;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
function sha256(value: string): string {
|
|
686
|
+
return createHash("sha256").update(value, "utf8").digest("hex");
|
|
687
|
+
}
|