om-memory-system 5.5.0 → 5.6.0-next.216
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/adapters/pi/extension.js +5 -0
- package/dist/cli/space-command.d.ts +6 -5
- package/dist/cli/space-command.js +154 -13
- package/dist/cli/space-transfer-command.d.ts +25 -0
- package/dist/cli/space-transfer-command.js +79 -0
- package/dist/core/memory-operations.d.ts +12 -2
- package/dist/core/memory-operations.js +14 -4
- package/dist/core/recall-summary.js +7 -5
- package/dist/core/retrieval.js +5 -2
- package/dist/core/space-tool.d.ts +9 -4
- package/dist/core/space-tool.js +35 -3
- package/dist/services/active-space-access.d.ts +6 -0
- package/dist/services/active-space-access.js +8 -0
- package/dist/services/chat/chat-runtime.js +1 -1
- package/dist/services/client.d.ts +6 -16
- package/dist/services/client.js +54 -62
- package/dist/services/context.d.ts +2 -0
- package/dist/services/context.js +12 -1
- package/dist/services/linked-search.d.ts +108 -0
- package/dist/services/linked-search.js +219 -0
- package/dist/services/space-routes.d.ts +2 -0
- package/dist/services/space-routes.js +4 -3
- package/dist/services/space-transfer-routes.d.ts +10 -0
- package/dist/services/space-transfer-routes.js +344 -0
- package/dist/services/spaces/active-space.js +14 -7
- package/dist/services/spaces/delete.d.ts +2 -0
- package/dist/services/spaces/delete.js +4 -0
- package/dist/services/spaces/export-space.d.ts +60 -0
- package/dist/services/spaces/export-space.js +367 -0
- package/dist/services/spaces/identifier-scrub.d.ts +26 -0
- package/dist/services/spaces/identifier-scrub.js +40 -0
- package/dist/services/spaces/import-space.d.ts +85 -0
- package/dist/services/spaces/import-space.js +349 -0
- package/dist/services/spaces/operations.d.ts +57 -5
- package/dist/services/spaces/operations.js +161 -5
- package/dist/services/spaces/read-store-cache.d.ts +13 -0
- package/dist/services/spaces/read-store-cache.js +34 -0
- package/dist/services/spaces/registry-store.d.ts +4 -0
- package/dist/services/spaces/registry-store.js +15 -1
- package/dist/services/spaces/registry.d.ts +32 -0
- package/dist/services/spaces/registry.js +81 -7
- package/dist/services/spaces/space-store.d.ts +50 -0
- package/dist/services/spaces/space-store.js +72 -0
- package/dist/services/spaces/transfer-service.d.ts +41 -0
- package/dist/services/spaces/transfer-service.js +408 -0
- package/dist/services/spaces/transfer-types.d.ts +163 -0
- package/dist/services/spaces/transfer-types.js +10 -0
- package/dist/services/spaces/transfer.d.ts +31 -0
- package/dist/services/spaces/transfer.js +276 -0
- package/dist/services/turso/connection-manager.d.ts +4 -2
- package/dist/services/turso/connection-manager.js +11 -5
- package/dist/services/turso/cross-process-write-lock.d.ts +1 -1
- package/dist/services/turso/cross-process-write-lock.js +5 -5
- package/dist/services/turso/operation-lock.d.ts +2 -2
- package/dist/services/turso/operation-lock.js +5 -5
- package/dist/services/turso/shard-manager.d.ts +8 -0
- package/dist/services/turso/shard-manager.js +23 -22
- package/dist/services/turso/vector-search.d.ts +6 -2
- package/dist/services/turso/vector-search.js +8 -4
- package/dist/services/user-profile/user-profile-manager.d.ts +5 -1
- package/dist/services/user-profile/user-profile-manager.js +13 -10
- package/dist/services/web-route-modules.d.ts +5 -1
- package/dist/services/web-route-modules.js +4 -0
- package/dist/services/web-server.js +1 -1
- package/dist/shared/api/space-export-schemas.d.ts +159 -0
- package/dist/shared/api/space-export-schemas.js +72 -0
- package/dist/v2/adapter.d.ts +15 -0
- package/dist/v2/adapter.js +5 -0
- package/dist/web/assets/index-2BNRLGqe.css +2 -0
- package/dist/web/assets/index-C6WnDE6h.js +170 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/skills/omms-memory/SKILL.md +16 -14
- package/dist/web/assets/index-Dcvz3EXt.js +0 -170
- package/dist/web/assets/index-b8k5Tuei.css +0 -2
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The transfer service (task 3.5): it wires the pure engine (`transfer.ts`) to
|
|
3
|
+
* two space stores, resolves the spaces through the registry, refuses the
|
|
4
|
+
* transfers the spec forbids, and logs one record per outcome with counts,
|
|
5
|
+
* space ids and error codes only. The caller passes every setting in place
|
|
6
|
+
* of CONFIG, like `registry.ts` expects.
|
|
7
|
+
*/
|
|
8
|
+
import { existsSync } from "node:fs";
|
|
9
|
+
import { log } from "../logger.js";
|
|
10
|
+
import { tursoVectorSearch } from "../turso/vector-search.js";
|
|
11
|
+
import { parseExtractedVector } from "../turso/vector-utils.js";
|
|
12
|
+
import { openSpaceStore } from "./space-store.js";
|
|
13
|
+
import { findSpace, readRegistryFile, resolveSpaceFolder } from "./registry.js";
|
|
14
|
+
import { applyTransfer, previewTransfer } from "./transfer.js";
|
|
15
|
+
import { tagTextKey } from "./transfer-types.js";
|
|
16
|
+
const ID_CHUNK = 500;
|
|
17
|
+
/** The insert writes is_pinned 0, so a pinned row sets its flag with this. */
|
|
18
|
+
const PINNED_SQL = `UPDATE memories SET is_pinned = 1 WHERE id = ?`;
|
|
19
|
+
function refusal(code, error) {
|
|
20
|
+
return { ok: false, code, error };
|
|
21
|
+
}
|
|
22
|
+
function readAccessOf(store) {
|
|
23
|
+
return {
|
|
24
|
+
listShards: (scope) => store.mode === "write" ? store.shards.getAllShards(scope, "") : store.listShards(scope, ""),
|
|
25
|
+
open: (dbPath) => store.mode === "write" ? store.connections.getConnection(dbPath) : store.openShard(dbPath),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
/** Registered shards whose file still exists; a vanished file holds no rows. */
|
|
29
|
+
async function existingShards(access, scope) {
|
|
30
|
+
return (await access.listShards(scope)).filter((shard) => existsSync(shard.dbPath));
|
|
31
|
+
}
|
|
32
|
+
async function eachShard(access, use) {
|
|
33
|
+
for (const scope of ["user", "project"]) {
|
|
34
|
+
for (const shard of await existingShards(access, scope)) {
|
|
35
|
+
await use(await access.open(shard.dbPath), shard);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function optionalString(value) {
|
|
40
|
+
return value === null || value === undefined ? undefined : String(value);
|
|
41
|
+
}
|
|
42
|
+
function rowToTransferRow(raw, shard) {
|
|
43
|
+
return {
|
|
44
|
+
id: String(raw.id),
|
|
45
|
+
text: String(raw.content),
|
|
46
|
+
containerTag: String(raw.container_tag),
|
|
47
|
+
tags: optionalString(raw.tags),
|
|
48
|
+
type: optionalString(raw.type),
|
|
49
|
+
createdAt: Number(raw.created_at),
|
|
50
|
+
updatedAt: Number(raw.updated_at),
|
|
51
|
+
metadata: optionalString(raw.metadata),
|
|
52
|
+
isPinned: Number(raw.is_pinned) === 1,
|
|
53
|
+
projectName: optionalString(raw.project_name),
|
|
54
|
+
vector: parseExtractedVector(raw.vector_json) ?? undefined,
|
|
55
|
+
tagsVector: parseExtractedVector(raw.tags_vector_json) ?? undefined,
|
|
56
|
+
shardKey: String(shard.id),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
async function readRowsAcrossShards(access, filter) {
|
|
60
|
+
const tagClause = filter.projectTag === undefined ? "" : " AND container_tag = ?";
|
|
61
|
+
const args = [filter.since, filter.until];
|
|
62
|
+
if (filter.projectTag !== undefined)
|
|
63
|
+
args.push(filter.projectTag);
|
|
64
|
+
const rows = [];
|
|
65
|
+
await eachShard(access, async (db, shard) => {
|
|
66
|
+
const raw = await db.all(`
|
|
67
|
+
SELECT id, content, container_tag, tags, type, created_at, updated_at, metadata,
|
|
68
|
+
project_name, is_pinned,
|
|
69
|
+
vector_extract(vector) AS vector_json,
|
|
70
|
+
CASE WHEN tags_vector IS NULL THEN NULL ELSE vector_extract(tags_vector) END
|
|
71
|
+
AS tags_vector_json
|
|
72
|
+
FROM memories
|
|
73
|
+
WHERE created_at >= ? AND created_at <= ?${tagClause}
|
|
74
|
+
`, args);
|
|
75
|
+
for (const item of raw)
|
|
76
|
+
rows.push(rowToTransferRow(item, shard));
|
|
77
|
+
});
|
|
78
|
+
return rows;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* The settings a space's shards record. All shards must agree, or the vectors
|
|
82
|
+
* are not provably reusable and this reads as undefined.
|
|
83
|
+
*/
|
|
84
|
+
async function readEmbeddingInfo(access) {
|
|
85
|
+
let agreed;
|
|
86
|
+
for (const scope of ["user", "project"]) {
|
|
87
|
+
for (const shard of await existingShards(access, scope)) {
|
|
88
|
+
const db = await access.open(shard.dbPath);
|
|
89
|
+
const meta = await db.all(`SELECT key, value FROM shard_metadata WHERE key IN ('embedding_model', 'embedding_dimensions')`);
|
|
90
|
+
const model = meta.find((row) => String(row.key) === "embedding_model")?.value;
|
|
91
|
+
const dimensions = meta.find((row) => String(row.key) === "embedding_dimensions")?.value;
|
|
92
|
+
if (typeof model !== "string" || dimensions === undefined)
|
|
93
|
+
return undefined;
|
|
94
|
+
const entry = { model, dimensions: Number(dimensions) };
|
|
95
|
+
if (agreed && (agreed.model !== entry.model || agreed.dimensions !== entry.dimensions)) {
|
|
96
|
+
return undefined;
|
|
97
|
+
}
|
|
98
|
+
agreed ??= entry;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return agreed;
|
|
102
|
+
}
|
|
103
|
+
async function readAllIds(access) {
|
|
104
|
+
const ids = new Set();
|
|
105
|
+
await eachShard(access, async (db) => {
|
|
106
|
+
for (const row of await db.all(`SELECT id FROM memories`))
|
|
107
|
+
ids.add(String(row.id));
|
|
108
|
+
});
|
|
109
|
+
return ids;
|
|
110
|
+
}
|
|
111
|
+
async function findIdsIn(access, ids) {
|
|
112
|
+
const found = new Set();
|
|
113
|
+
await eachShard(access, async (db) => {
|
|
114
|
+
for (const chunk of chunkIds(ids)) {
|
|
115
|
+
const sql = `SELECT id FROM memories WHERE id IN (${chunk.map(() => "?").join(", ")})`;
|
|
116
|
+
for (const row of await db.all(sql, chunk))
|
|
117
|
+
found.add(String(row.id));
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
return found;
|
|
121
|
+
}
|
|
122
|
+
async function readAllTagTexts(access) {
|
|
123
|
+
const keys = new Set();
|
|
124
|
+
await eachShard(access, async (db) => {
|
|
125
|
+
for (const row of await db.all(`SELECT container_tag, content FROM memories`)) {
|
|
126
|
+
keys.add(tagTextKey(String(row.container_tag), String(row.content)));
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
return keys;
|
|
130
|
+
}
|
|
131
|
+
function chunkIds(ids) {
|
|
132
|
+
return Array.from({ length: Math.ceil(ids.length / ID_CHUNK) }, (_, index) => ids.slice(index * ID_CHUNK, (index + 1) * ID_CHUNK));
|
|
133
|
+
}
|
|
134
|
+
/** The source side of the engine: a read store, plus a write store for a move. */
|
|
135
|
+
class SpaceSourceReader {
|
|
136
|
+
access;
|
|
137
|
+
write;
|
|
138
|
+
constructor(access, write) {
|
|
139
|
+
this.access = access;
|
|
140
|
+
this.write = write;
|
|
141
|
+
}
|
|
142
|
+
readRows(filter) {
|
|
143
|
+
return readRowsAcrossShards(this.access, filter);
|
|
144
|
+
}
|
|
145
|
+
getEmbeddingInfo() {
|
|
146
|
+
return readEmbeddingInfo(this.access);
|
|
147
|
+
}
|
|
148
|
+
async getShardCounts(shardKeys) {
|
|
149
|
+
const write = this.requireWrite();
|
|
150
|
+
const counts = new Map();
|
|
151
|
+
for (const key of shardKeys) {
|
|
152
|
+
const shard = await write.shards.getShardById(Number(key));
|
|
153
|
+
counts.set(key, shard?.vectorCount ?? 0);
|
|
154
|
+
}
|
|
155
|
+
return counts;
|
|
156
|
+
}
|
|
157
|
+
async deleteFromShard(shardKey, ids) {
|
|
158
|
+
const write = this.requireWrite();
|
|
159
|
+
const shard = await write.shards.getShardById(Number(shardKey));
|
|
160
|
+
if (!shard)
|
|
161
|
+
throw new Error(`Source shard ${shardKey} is not registered.`);
|
|
162
|
+
await write.shards.withScopeWriteLock(shard.scope, shard.scopeHash, async () => {
|
|
163
|
+
const db = await write.connections.getConnection(shard.dbPath);
|
|
164
|
+
await db.transaction("write", async (tx) => {
|
|
165
|
+
for (const chunk of chunkIds(ids)) {
|
|
166
|
+
await tx.execute({
|
|
167
|
+
sql: `DELETE FROM memories WHERE id IN (${chunk.map(() => "?").join(", ")})`,
|
|
168
|
+
args: chunk,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
// A recount, not a delta, keeps the count right if another writer raced.
|
|
173
|
+
await write.shards.setVectorCount(shard.id, await tursoVectorSearch.countAllVectors(db));
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
async setShardCount(shardKey, _count) {
|
|
177
|
+
const write = this.requireWrite();
|
|
178
|
+
const shard = await write.shards.getShardById(Number(shardKey));
|
|
179
|
+
if (!shard)
|
|
180
|
+
return;
|
|
181
|
+
const db = await write.connections.getConnection(shard.dbPath);
|
|
182
|
+
await write.shards.setVectorCount(shard.id, await tursoVectorSearch.countAllVectors(db));
|
|
183
|
+
}
|
|
184
|
+
requireWrite() {
|
|
185
|
+
if (!this.write)
|
|
186
|
+
throw new Error("Only a move reaches this step; a copy source is read-only.");
|
|
187
|
+
return this.write;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
function toMemoryRecord(row) {
|
|
191
|
+
return {
|
|
192
|
+
id: row.id,
|
|
193
|
+
content: row.text,
|
|
194
|
+
vector: row.vector,
|
|
195
|
+
tagsVector: row.tagsVector,
|
|
196
|
+
containerTag: row.containerTag,
|
|
197
|
+
tags: row.tags,
|
|
198
|
+
type: row.type,
|
|
199
|
+
createdAt: row.createdAt,
|
|
200
|
+
updatedAt: row.updatedAt,
|
|
201
|
+
metadata: row.metadata,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
class SpaceTargetWriter {
|
|
205
|
+
store;
|
|
206
|
+
embedding;
|
|
207
|
+
shardInfo = new Map();
|
|
208
|
+
constructor(store, embedding) {
|
|
209
|
+
this.store = store;
|
|
210
|
+
this.embedding = embedding;
|
|
211
|
+
}
|
|
212
|
+
loadExistingIds() {
|
|
213
|
+
return readAllIds(readAccessOf(this.store));
|
|
214
|
+
}
|
|
215
|
+
loadExistingTagTexts() {
|
|
216
|
+
return readAllTagTexts(readAccessOf(this.store));
|
|
217
|
+
}
|
|
218
|
+
getEmbeddingSettings() {
|
|
219
|
+
return Promise.resolve(this.embedding);
|
|
220
|
+
}
|
|
221
|
+
async getWriteShard(scope, scopeHash) {
|
|
222
|
+
const shard = await this.store.shards.getWriteShard(scope, scopeHash);
|
|
223
|
+
this.shardInfo.set(String(shard.id), shard);
|
|
224
|
+
return { key: String(shard.id), vectorCount: shard.vectorCount };
|
|
225
|
+
}
|
|
226
|
+
async insertBatch(shard, rows) {
|
|
227
|
+
const info = this.offeredShard(shard.key);
|
|
228
|
+
await this.store.shards.withScopeWriteLock(info.scope, info.scopeHash, async () => {
|
|
229
|
+
const db = await this.store.connections.getConnection(info.dbPath);
|
|
230
|
+
await db.transaction("write", async (tx) => {
|
|
231
|
+
for (const row of rows) {
|
|
232
|
+
await tursoVectorSearch.insertVectorInTransaction(tx, toMemoryRecord(row));
|
|
233
|
+
// The insert writes is_pinned 0, so a pinned row sets its flag after.
|
|
234
|
+
if (row.isPinned)
|
|
235
|
+
await tx.execute({ sql: PINNED_SQL, args: [row.id] });
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
// A recount, not a delta, keeps the count right if another writer raced.
|
|
239
|
+
await this.store.shards.setVectorCount(info.id, await tursoVectorSearch.countAllVectors(db));
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
async setShardCount(shardKey, _count) {
|
|
243
|
+
const info = this.offeredShard(shardKey);
|
|
244
|
+
const db = await this.store.connections.getConnection(info.dbPath);
|
|
245
|
+
await this.store.shards.setVectorCount(info.id, await tursoVectorSearch.countAllVectors(db));
|
|
246
|
+
}
|
|
247
|
+
findExistingIds(ids) {
|
|
248
|
+
return findIdsIn(readAccessOf(this.store), ids);
|
|
249
|
+
}
|
|
250
|
+
offeredShard(shardKey) {
|
|
251
|
+
const info = this.shardInfo.get(shardKey);
|
|
252
|
+
if (!info)
|
|
253
|
+
throw new Error(`Target shard ${shardKey} was never offered for writing.`);
|
|
254
|
+
return info;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* The read-only target of a preview: reads from the read store, every write
|
|
259
|
+
* refused. Opening an empty target in write mode would create its metadata.db.
|
|
260
|
+
*/
|
|
261
|
+
class ReadOnlyTarget {
|
|
262
|
+
access;
|
|
263
|
+
constructor(access) {
|
|
264
|
+
this.access = access;
|
|
265
|
+
}
|
|
266
|
+
loadExistingIds() {
|
|
267
|
+
return readAllIds(this.access);
|
|
268
|
+
}
|
|
269
|
+
loadExistingTagTexts() {
|
|
270
|
+
return readAllTagTexts(this.access);
|
|
271
|
+
}
|
|
272
|
+
getEmbeddingSettings() {
|
|
273
|
+
return this.refuse();
|
|
274
|
+
}
|
|
275
|
+
getWriteShard() {
|
|
276
|
+
return this.refuse();
|
|
277
|
+
}
|
|
278
|
+
insertBatch() {
|
|
279
|
+
return this.refuse();
|
|
280
|
+
}
|
|
281
|
+
setShardCount() {
|
|
282
|
+
return this.refuse();
|
|
283
|
+
}
|
|
284
|
+
findExistingIds(ids) {
|
|
285
|
+
return findIdsIn(this.access, ids);
|
|
286
|
+
}
|
|
287
|
+
refuse = () => Promise.reject(new Error("A preview must not change the target."));
|
|
288
|
+
}
|
|
289
|
+
function defaultEmbed(text) {
|
|
290
|
+
return import("../embedding.js").then(({ embeddingService }) => embeddingService.embedWithTimeout(text, { task: "document" }));
|
|
291
|
+
}
|
|
292
|
+
/** Resolves both spaces and applies the refusals the spec demands, in order. */
|
|
293
|
+
function resolveSpaces(request, context) {
|
|
294
|
+
const read = readRegistryFile(context.files.registryPath);
|
|
295
|
+
if (read.problem)
|
|
296
|
+
return refusal("registry-problem", read.problem);
|
|
297
|
+
const source = findSpace(read.registry, request.from);
|
|
298
|
+
const target = findSpace(read.registry, request.to);
|
|
299
|
+
if (!source)
|
|
300
|
+
return refusal("unknown-space", `No space named "${request.from}".`);
|
|
301
|
+
if (!target)
|
|
302
|
+
return refusal("unknown-space", `No space named "${request.to}".`);
|
|
303
|
+
if (source.id === target.id) {
|
|
304
|
+
return refusal("same-space", `The source and the target must differ; both are "${source.name}".`);
|
|
305
|
+
}
|
|
306
|
+
if (source.archived) {
|
|
307
|
+
return refusal("archived-space", `The source space "${source.name}" is archived.`);
|
|
308
|
+
}
|
|
309
|
+
if (target.archived) {
|
|
310
|
+
return refusal("archived-space", `The target space "${target.name}" is archived.`);
|
|
311
|
+
}
|
|
312
|
+
return {
|
|
313
|
+
ok: true,
|
|
314
|
+
sourceId: source.id,
|
|
315
|
+
targetId: target.id,
|
|
316
|
+
sourceFolder: resolveSpaceFolder(source.id, context.paths),
|
|
317
|
+
targetFolder: resolveSpaceFolder(target.id, context.paths),
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
function errorName(error) {
|
|
321
|
+
return error instanceof Error ? error.name : "unknown error";
|
|
322
|
+
}
|
|
323
|
+
/** Previews a transfer without changing either space: read stores, no locks. */
|
|
324
|
+
export async function previewSpaceTransfer(request, context) {
|
|
325
|
+
const resolved = resolveSpaces(request, context);
|
|
326
|
+
if (!resolved.ok) {
|
|
327
|
+
logRefusal(request, resolved.code);
|
|
328
|
+
return resolved;
|
|
329
|
+
}
|
|
330
|
+
const ids = { from: resolved.sourceId, to: resolved.targetId };
|
|
331
|
+
const source = openSpaceStore(resolved.sourceFolder, "read");
|
|
332
|
+
const target = openSpaceStore(resolved.targetFolder, "read");
|
|
333
|
+
try {
|
|
334
|
+
const preview = await previewTransfer({
|
|
335
|
+
source: new SpaceSourceReader(readAccessOf(source)),
|
|
336
|
+
target: new ReadOnlyTarget(readAccessOf(target)),
|
|
337
|
+
filter: request.filter,
|
|
338
|
+
});
|
|
339
|
+
log("space transfer preview", {
|
|
340
|
+
...ids,
|
|
341
|
+
matched: preview.matched,
|
|
342
|
+
duplicates: preview.duplicates,
|
|
343
|
+
toCopy: preview.toCopy,
|
|
344
|
+
});
|
|
345
|
+
return { ok: true, preview };
|
|
346
|
+
}
|
|
347
|
+
catch (error) {
|
|
348
|
+
log("space transfer preview failed", { ...ids, error: errorName(error) }, "error");
|
|
349
|
+
throw error;
|
|
350
|
+
}
|
|
351
|
+
finally {
|
|
352
|
+
await source.close();
|
|
353
|
+
await target.close();
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* Runs a copy or a move. The target opens in write mode; the source opens in
|
|
358
|
+
* write mode only for a move. Engine failures land in `result.error`; the call
|
|
359
|
+
* throws only on an unexpected store failure, after logging its name.
|
|
360
|
+
*/
|
|
361
|
+
export async function applySpaceTransfer(request, context) {
|
|
362
|
+
const resolved = resolveSpaces(request, context);
|
|
363
|
+
if (!resolved.ok) {
|
|
364
|
+
logRefusal(request, resolved.code);
|
|
365
|
+
return resolved;
|
|
366
|
+
}
|
|
367
|
+
const ids = { from: resolved.sourceId, to: resolved.targetId };
|
|
368
|
+
const move = request.mode === "move";
|
|
369
|
+
const sourceStore = move
|
|
370
|
+
? openSpaceStore(resolved.sourceFolder, "write")
|
|
371
|
+
: openSpaceStore(resolved.sourceFolder, "read");
|
|
372
|
+
const targetStore = openSpaceStore(resolved.targetFolder, "write");
|
|
373
|
+
try {
|
|
374
|
+
const write = move ? sourceStore : undefined;
|
|
375
|
+
const source = new SpaceSourceReader(readAccessOf(sourceStore), write);
|
|
376
|
+
const result = await applyTransfer({
|
|
377
|
+
source,
|
|
378
|
+
target: new SpaceTargetWriter(targetStore, context.embedding),
|
|
379
|
+
filter: request.filter,
|
|
380
|
+
mode: request.mode,
|
|
381
|
+
maxVectorsPerShard: context.maxVectorsPerShard,
|
|
382
|
+
embed: context.embed ?? defaultEmbed,
|
|
383
|
+
...(request.previewMatched === undefined ? {} : { previewMatched: request.previewMatched }),
|
|
384
|
+
});
|
|
385
|
+
log("space transfer", {
|
|
386
|
+
...ids,
|
|
387
|
+
mode: request.mode,
|
|
388
|
+
ok: result.ok,
|
|
389
|
+
copied: result.copied,
|
|
390
|
+
skipped: result.skipped,
|
|
391
|
+
removed: result.removed,
|
|
392
|
+
changedFromPreview: result.changedFromPreview,
|
|
393
|
+
...(result.error ? { step: result.error.step } : {}),
|
|
394
|
+
});
|
|
395
|
+
return { ok: true, result };
|
|
396
|
+
}
|
|
397
|
+
catch (error) {
|
|
398
|
+
log("space transfer failed", { ...ids, mode: request.mode, error: errorName(error) }, "error");
|
|
399
|
+
throw error;
|
|
400
|
+
}
|
|
401
|
+
finally {
|
|
402
|
+
await sourceStore.close();
|
|
403
|
+
await targetStore.close();
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
function logRefusal(request, code) {
|
|
407
|
+
log("space transfer refused", { from: request.from, to: request.to, code }, "warn");
|
|
408
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Types for the space transfer engine (`transfer.ts`). The engine is pure: it
|
|
3
|
+
* works only through the `SourceReader` and `TargetWriter` interfaces below,
|
|
4
|
+
* never imports CONFIG and never opens a file. The wiring layer implements both
|
|
5
|
+
* interfaces against the space stores.
|
|
6
|
+
*/
|
|
7
|
+
export type TransferScope = "user" | "project";
|
|
8
|
+
export type TransferMode = "copy" | "move";
|
|
9
|
+
/**
|
|
10
|
+
* The embedding settings of a space or shard. A missing model means legacy
|
|
11
|
+
* shards that record none, which the engine treats as not provably compatible.
|
|
12
|
+
*/
|
|
13
|
+
export interface EmbeddingInfo {
|
|
14
|
+
model?: string;
|
|
15
|
+
dimensions: number;
|
|
16
|
+
}
|
|
17
|
+
/** One source row, shaped after the `memories` table of a shard. */
|
|
18
|
+
export interface TransferRow {
|
|
19
|
+
id: string;
|
|
20
|
+
/** Memory text (the `content` column). */
|
|
21
|
+
text: string;
|
|
22
|
+
/** Scope tag: `omms_project_<hash>` or `omms_user_<hash>`. */
|
|
23
|
+
containerTag: string;
|
|
24
|
+
/** Comma-separated tags, exactly as stored. */
|
|
25
|
+
tags?: string;
|
|
26
|
+
type?: string;
|
|
27
|
+
/** Creation time, epoch milliseconds. */
|
|
28
|
+
createdAt: number;
|
|
29
|
+
/** Last update time, epoch milliseconds. */
|
|
30
|
+
updatedAt: number;
|
|
31
|
+
/** Stored metadata as the raw JSON string, exactly as stored. */
|
|
32
|
+
metadata?: string;
|
|
33
|
+
isPinned: boolean;
|
|
34
|
+
/** The `project_name` column; drives the preview's project groups. */
|
|
35
|
+
projectName?: string;
|
|
36
|
+
/** The stored content vector, when the source shard can supply it. */
|
|
37
|
+
vector?: Float32Array;
|
|
38
|
+
/** The stored tags vector, when the row has one. */
|
|
39
|
+
tagsVector?: Float32Array;
|
|
40
|
+
/** The source shard the row was read from; drives a move's per-shard deletes. */
|
|
41
|
+
shardKey: string;
|
|
42
|
+
}
|
|
43
|
+
/** Selection filter. Both bounds on `created_at` are inclusive. */
|
|
44
|
+
export interface TransferFilter {
|
|
45
|
+
since: number;
|
|
46
|
+
until: number;
|
|
47
|
+
/** Only rows with this container tag. Omit for every project plus Personal. */
|
|
48
|
+
projectTag?: string;
|
|
49
|
+
}
|
|
50
|
+
/** One project group of a preview. User-scope rows group under "Personal". */
|
|
51
|
+
export interface TransferGroup {
|
|
52
|
+
name: string;
|
|
53
|
+
tag: string;
|
|
54
|
+
count: number;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* A preview example: the id and the text cut to 160 characters. For the screen
|
|
58
|
+
* only; callers must never log it.
|
|
59
|
+
*/
|
|
60
|
+
export interface TransferExample {
|
|
61
|
+
id: string;
|
|
62
|
+
text: string;
|
|
63
|
+
}
|
|
64
|
+
export interface TransferPreview {
|
|
65
|
+
/** Source rows that match the filter. */
|
|
66
|
+
matched: number;
|
|
67
|
+
/** Rows a copy would write: matched minus exact duplicates. */
|
|
68
|
+
toCopy: number;
|
|
69
|
+
/** Exact duplicates the target already holds. */
|
|
70
|
+
duplicates: number;
|
|
71
|
+
groups: TransferGroup[];
|
|
72
|
+
examples: TransferExample[];
|
|
73
|
+
}
|
|
74
|
+
/** A row ready to write: every stored field plus a usable content vector. */
|
|
75
|
+
export interface TransferWriteRow {
|
|
76
|
+
id: string;
|
|
77
|
+
text: string;
|
|
78
|
+
containerTag: string;
|
|
79
|
+
tags?: string;
|
|
80
|
+
type?: string;
|
|
81
|
+
createdAt: number;
|
|
82
|
+
updatedAt: number;
|
|
83
|
+
metadata?: string;
|
|
84
|
+
isPinned: boolean;
|
|
85
|
+
vector: Float32Array;
|
|
86
|
+
tagsVector?: Float32Array;
|
|
87
|
+
}
|
|
88
|
+
/** A shard the target offers for writes, with the rows it already holds. */
|
|
89
|
+
export interface TargetShard {
|
|
90
|
+
key: string;
|
|
91
|
+
vectorCount: number;
|
|
92
|
+
}
|
|
93
|
+
/** The key of a container tag and text pair, for the duplicate check. */
|
|
94
|
+
export declare function tagTextKey(tag: string, text: string): string;
|
|
95
|
+
/**
|
|
96
|
+
* Reads one space as the transfer source. Read methods must not change the
|
|
97
|
+
* space; the delete and count methods run only in a move's remove step.
|
|
98
|
+
*/
|
|
99
|
+
export interface SourceReader {
|
|
100
|
+
/**
|
|
101
|
+
* Read the source rows for a filter. The reader may pre-filter in SQL, but
|
|
102
|
+
* the engine applies the exact inclusive bounds and the project filter
|
|
103
|
+
* itself, so the reader must return at least every matching row. Rows whose
|
|
104
|
+
* container tag OMMS cannot parse are left alone by the engine.
|
|
105
|
+
*/
|
|
106
|
+
readRows(filter: TransferFilter): Promise<TransferRow[]>;
|
|
107
|
+
/**
|
|
108
|
+
* The model and dimensions recorded in the source shards. Undefined when the
|
|
109
|
+
* shards record no model; the engine then embeds again instead of reusing.
|
|
110
|
+
*/
|
|
111
|
+
getEmbeddingInfo(): Promise<EmbeddingInfo | undefined>;
|
|
112
|
+
/** Row counts of the given shards, used to fix counts after a move's deletes. */
|
|
113
|
+
getShardCounts(shardKeys: string[]): Promise<Map<string, number>>;
|
|
114
|
+
/** Delete the ids from one shard in a single transaction. */
|
|
115
|
+
deleteFromShard(shardKey: string, ids: string[]): Promise<void>;
|
|
116
|
+
/** Store a shard's vector count after the engine changed the shard. */
|
|
117
|
+
setShardCount(shardKey: string, count: number): Promise<void>;
|
|
118
|
+
}
|
|
119
|
+
/** Writes one space as the transfer target. */
|
|
120
|
+
export interface TargetWriter {
|
|
121
|
+
/** Every id the target holds, loaded once for the duplicate check. */
|
|
122
|
+
loadExistingIds(): Promise<Set<string>>;
|
|
123
|
+
/** Every container tag and text pair the target holds, as `tagTextKey` keys, loaded once. */
|
|
124
|
+
loadExistingTagTexts(): Promise<Set<string>>;
|
|
125
|
+
/** The target space's embedding settings. */
|
|
126
|
+
getEmbeddingSettings(): Promise<EmbeddingInfo>;
|
|
127
|
+
/**
|
|
128
|
+
* The shard that takes the next rows of one scope group, creating a shard
|
|
129
|
+
* when the scope has none. Must not return a shard with no room left under
|
|
130
|
+
* the capacity the engine was given.
|
|
131
|
+
*/
|
|
132
|
+
getWriteShard(scope: TransferScope, scopeHash: string): Promise<TargetShard>;
|
|
133
|
+
/** Insert the rows into the shard in one transaction: all rows or none. */
|
|
134
|
+
insertBatch(shard: TargetShard, rows: TransferWriteRow[]): Promise<void>;
|
|
135
|
+
/** Store the shard's vector count after the engine wrote to it. */
|
|
136
|
+
setShardCount(shardKey: string, count: number): Promise<void>;
|
|
137
|
+
/** Read back which of the ids the target holds; called once per move. */
|
|
138
|
+
findExistingIds(ids: string[]): Promise<Set<string>>;
|
|
139
|
+
}
|
|
140
|
+
export type TransferFailureStep = "select" | "write" | "check" | "remove";
|
|
141
|
+
/** A failed step. The message holds counts, shard keys and ids, never memory text. */
|
|
142
|
+
export interface TransferFailure {
|
|
143
|
+
step: TransferFailureStep;
|
|
144
|
+
message: string;
|
|
145
|
+
}
|
|
146
|
+
/** The outcome of an apply. Counts say how far the transfer got. */
|
|
147
|
+
export interface TransferApplyResult {
|
|
148
|
+
ok: boolean;
|
|
149
|
+
mode: TransferMode;
|
|
150
|
+
/** Rows written to the target before any failure. */
|
|
151
|
+
copied: number;
|
|
152
|
+
/** Exact duplicates skipped in the target's favour. */
|
|
153
|
+
skipped: number;
|
|
154
|
+
/** Rows deleted from the source (move only, always after a passed check). */
|
|
155
|
+
removed: number;
|
|
156
|
+
/** How many more (or fewer) rows matched at apply time than at preview time. */
|
|
157
|
+
changedFromPreview: number;
|
|
158
|
+
/** True when at least one row was embedded again instead of reusing its vector. */
|
|
159
|
+
embeddedAgain: boolean;
|
|
160
|
+
/** Copied ids the check could not find in the target (move only). */
|
|
161
|
+
missingIds: string[];
|
|
162
|
+
error?: TransferFailure;
|
|
163
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Types for the space transfer engine (`transfer.ts`). The engine is pure: it
|
|
3
|
+
* works only through the `SourceReader` and `TargetWriter` interfaces below,
|
|
4
|
+
* never imports CONFIG and never opens a file. The wiring layer implements both
|
|
5
|
+
* interfaces against the space stores.
|
|
6
|
+
*/
|
|
7
|
+
/** The key of a container tag and text pair, for the duplicate check. */
|
|
8
|
+
export function tagTextKey(tag, text) {
|
|
9
|
+
return `${tag}\u0000${text}`;
|
|
10
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { SourceReader, TargetWriter, TransferApplyResult, TransferFilter, TransferMode, TransferPreview } from "./transfer-types.js";
|
|
2
|
+
export * from "./transfer-types.js";
|
|
3
|
+
type EmbedFn = (text: string) => Promise<Float32Array>;
|
|
4
|
+
/**
|
|
5
|
+
* Preview a transfer: count, project groups, at most five examples (text cut
|
|
6
|
+
* to 160 characters) and the duplicate count. Reads only; never writes.
|
|
7
|
+
*/
|
|
8
|
+
export declare function previewTransfer(args: {
|
|
9
|
+
source: SourceReader;
|
|
10
|
+
target: TargetWriter;
|
|
11
|
+
filter: TransferFilter;
|
|
12
|
+
}): Promise<TransferPreview>;
|
|
13
|
+
export interface TransferApplyArgs {
|
|
14
|
+
source: SourceReader;
|
|
15
|
+
target: TargetWriter;
|
|
16
|
+
filter: TransferFilter;
|
|
17
|
+
mode: TransferMode;
|
|
18
|
+
/** Shard capacity; writes are split so no shard receives more rows than it has room for. */
|
|
19
|
+
maxVectorsPerShard: number;
|
|
20
|
+
/** Embeds one text with the target's model. Needed only when the stored vectors cannot be reused. */
|
|
21
|
+
embed?: EmbedFn;
|
|
22
|
+
/** The preview's matched count, so the apply can report how the source changed. */
|
|
23
|
+
previewMatched?: number;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Run a transfer: select and skip duplicates at apply time, write to the
|
|
27
|
+
* target, and for a move check every copied id before removing it from the
|
|
28
|
+
* source. Failures are reported in the result, never thrown: the counts say
|
|
29
|
+
* how far the transfer got.
|
|
30
|
+
*/
|
|
31
|
+
export declare function applyTransfer(args: TransferApplyArgs): Promise<TransferApplyResult>;
|