om-memory-system 5.9.1 → 5.10.1
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 +7 -11
- package/dist/adapters/opencode/conversation.js +4 -2
- package/dist/adapters/opencode/memory-tool.d.ts +4 -0
- package/dist/adapters/opencode/memory-tool.js +53 -0
- package/dist/adapters/opencode/package-skills.d.ts +1 -1
- package/dist/adapters/opencode/package-skills.js +17 -15
- package/dist/adapters/pi/extension.js +7 -0
- package/dist/cli/memory-command.d.ts +2 -0
- package/dist/cli/memory-command.js +35 -9
- package/dist/config-paths.d.ts +5 -0
- package/dist/config-paths.js +15 -0
- package/dist/config-template.d.ts +1 -1
- package/dist/config-template.js +8 -0
- package/dist/config.d.ts +51 -40
- package/dist/config.js +8 -15
- package/dist/core/capture.d.ts +2 -1
- package/dist/core/capture.js +8 -1
- package/dist/core/host.d.ts +12 -0
- package/dist/core/memorisation-operations.d.ts +33 -0
- package/dist/core/memorisation-operations.js +160 -0
- package/dist/core/memorisation-settings.d.ts +11 -0
- package/dist/core/memorisation-settings.js +48 -0
- package/dist/core/memorisation.d.ts +43 -0
- package/dist/core/memorisation.js +76 -0
- package/dist/core/memorise-turn.d.ts +11 -0
- package/dist/core/memorise-turn.js +64 -0
- package/dist/core/memory-operations.d.ts +6 -1
- package/dist/core/memory-operations.js +29 -0
- package/dist/core/memory-tool-text.js +1 -1
- package/dist/core/retrieval.d.ts +3 -2
- package/dist/core/retrieval.js +11 -4
- package/dist/core/tool-outcome.d.ts +6 -0
- package/dist/core/tool-outcome.js +31 -0
- package/dist/importer/claude-conversation.js +8 -1
- package/dist/importer/claude-turn-units.js +1 -0
- package/dist/importer/importer.js +3 -3
- package/dist/importer/opencode-reader.js +2 -1
- package/dist/importer/pi-conversation.js +11 -5
- package/dist/index.js +2 -42
- package/dist/services/api-handlers.js +13 -35
- package/dist/services/capture-diagnostics.d.ts +3 -2
- package/dist/services/capture-diagnostics.js +5 -1
- package/dist/services/cleanup-service.js +2 -1
- package/dist/services/client-session-read.d.ts +16 -0
- package/dist/services/client-session-read.js +32 -0
- package/dist/services/client.d.ts +6 -1
- package/dist/services/client.js +20 -36
- package/dist/services/deduplication-service.js +12 -1
- package/dist/services/global-config-writer.js +14 -1
- package/dist/services/linked-search.d.ts +15 -0
- package/dist/services/linked-search.js +23 -2
- package/dist/services/live-captured-entries.js +2 -1
- package/dist/services/memorisation-routes.d.ts +16 -0
- package/dist/services/memorisation-routes.js +94 -0
- package/dist/services/memorisation-store.d.ts +45 -0
- package/dist/services/memorisation-store.js +120 -0
- package/dist/services/memory-kind.d.ts +6 -0
- package/dist/services/memory-kind.js +6 -0
- package/dist/services/memory-portability-service.js +2 -1
- package/dist/services/memory-replace.d.ts +8 -0
- package/dist/services/memory-replace.js +26 -0
- package/dist/services/retrieval-rerank.d.ts +3 -1
- package/dist/services/retrieval-rerank.js +1 -1
- package/dist/services/settings-snapshot.js +6 -0
- package/dist/services/turso/exact-scan.d.ts +2 -1
- package/dist/services/turso/exact-scan.js +4 -3
- package/dist/services/turso/keyword-search.d.ts +2 -1
- package/dist/services/turso/keyword-search.js +3 -2
- package/dist/services/turso/vector-search.d.ts +10 -9
- package/dist/services/turso/vector-search.js +38 -95
- package/dist/services/turso/vector-writes.d.ts +8 -0
- package/dist/services/turso/vector-writes.js +80 -0
- package/dist/services/web-ensure.d.ts +1 -0
- package/dist/services/web-ensure.js +5 -1
- package/dist/services/web-memory-rows.d.ts +12 -0
- package/dist/services/web-memory-rows.js +58 -0
- package/dist/services/web-route-modules.js +5 -0
- package/dist/v2/adapter.d.ts +13 -1
- package/dist/v2/adapter.js +7 -0
- package/dist/web/assets/{index-Be6Uh64d.css → index-CoYuPYCS.css} +1 -1
- package/dist/web/assets/index-DQgI670D.js +171 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/skills/omms-memory/SKILL.md +18 -0
- package/skills/omms-remember/SKILL.md +118 -0
- package/dist/web/assets/index-BX_zZiKa.js +0 -170
package/dist/services/client.js
CHANGED
|
@@ -11,6 +11,8 @@ import { findLinkedMemoryOwner, linkedRefusal, linkedSettingsFrom, listLinkedSpa
|
|
|
11
11
|
import { CONFIG } from "../config.js";
|
|
12
12
|
import { activeOwner } from "./active-space-access.js";
|
|
13
13
|
import { log } from "./logger.js";
|
|
14
|
+
import { replaceMemoryRow } from "./memory-replace.js";
|
|
15
|
+
import { searchMemoriesBySessionID as readSessionMemories } from "./client-session-read.js";
|
|
14
16
|
export class LocalMemoryClient {
|
|
15
17
|
initPromise = null;
|
|
16
18
|
isInitialized = false;
|
|
@@ -64,7 +66,7 @@ export class LocalMemoryClient {
|
|
|
64
66
|
// Capture the result count and the reranker settings at entry: initialize,
|
|
65
67
|
// embedding, and shard reads all await, and a config edit during them must
|
|
66
68
|
// not change this search's limit or reranker target.
|
|
67
|
-
const maxMemories = CONFIG.maxMemories;
|
|
69
|
+
const maxMemories = options.limit ?? CONFIG.maxMemories;
|
|
68
70
|
const keywordBoost = CONFIG.keywordBoost;
|
|
69
71
|
const reranker = rerankSettingsFrom(CONFIG);
|
|
70
72
|
// The reranker path below always uses the boost: it needs a full pool on the similarity scale.
|
|
@@ -96,13 +98,14 @@ export class LocalMemoryClient {
|
|
|
96
98
|
queryText: query,
|
|
97
99
|
maxMemories,
|
|
98
100
|
similarityThreshold: CONFIG.similarityThreshold,
|
|
101
|
+
kind: options.kind ?? "normal",
|
|
99
102
|
}, reranker, {
|
|
100
|
-
search: (shardList, vector, tag, limit, threshold, queryText) => tursoVectorSearch.searchAcrossShards(shardList, vector, tag, limit, threshold, queryText, keywordBoost),
|
|
103
|
+
search: (shardList, vector, tag, limit, threshold, queryText, kind) => tursoVectorSearch.searchAcrossShards(shardList, vector, tag, limit, threshold, queryText, keywordBoost, "boost", undefined, kind),
|
|
101
104
|
log: (message, meta) => log(message, meta),
|
|
102
105
|
}));
|
|
103
106
|
}
|
|
104
107
|
else {
|
|
105
|
-
({ results, warnings } = await tursoVectorSearch.searchAcrossShards(shards, queryVector, scope === "all-projects" ? "" : containerTag, maxMemories, CONFIG.similarityThreshold, query, keywordBoost, keywordMode));
|
|
108
|
+
({ results, warnings } = await tursoVectorSearch.searchAcrossShards(shards, queryVector, scope === "all-projects" ? "" : containerTag, maxMemories, CONFIG.similarityThreshold, query, keywordBoost, keywordMode, undefined, options.kind ?? "normal"));
|
|
106
109
|
}
|
|
107
110
|
// Linked spaces fill only the places the own search left (D6); the reranker never sees them.
|
|
108
111
|
if (options.includeLinked) {
|
|
@@ -113,7 +116,10 @@ export class LocalMemoryClient {
|
|
|
113
116
|
containerTag,
|
|
114
117
|
ownCount: results.length,
|
|
115
118
|
owner: activeOwner(),
|
|
116
|
-
settings:
|
|
119
|
+
settings: options.limit === undefined
|
|
120
|
+
? linkedSettingsFrom(CONFIG)
|
|
121
|
+
: { ...linkedSettingsFrom(CONFIG), maxMemories: options.limit },
|
|
122
|
+
kind: options.kind ?? "normal",
|
|
117
123
|
});
|
|
118
124
|
results = [...results, ...linked.results];
|
|
119
125
|
warnings = [...warnings, ...linked.warnings];
|
|
@@ -146,11 +152,11 @@ export class LocalMemoryClient {
|
|
|
146
152
|
});
|
|
147
153
|
}
|
|
148
154
|
const { scope, hash } = extractScopeFromContainerTag(containerTag);
|
|
149
|
-
return tursoShardManager.withScopeWriteLock(scope, hash, async () => {
|
|
155
|
+
return await tursoShardManager.withScopeWriteLock(scope, hash, async () => {
|
|
150
156
|
const shard = await tursoShardManager.getWriteShard(scope, hash);
|
|
151
|
-
const id = `mem_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`;
|
|
152
157
|
const now = Date.now();
|
|
153
|
-
const { displayName, userName, userEmail, projectPath, projectName, gitRepoUrl, type, tags: _tags, ...dynamicMetadata } = metadata || {};
|
|
158
|
+
const { id: customId, replaceExisting, displayName, userName, userEmail, projectPath, projectName, gitRepoUrl, type, tags: _tags, ...dynamicMetadata } = metadata || {};
|
|
159
|
+
const id = customId ?? `mem_${now}_${Math.random().toString(36).substring(2, 11)}`;
|
|
154
160
|
const record = {
|
|
155
161
|
id,
|
|
156
162
|
content,
|
|
@@ -169,6 +175,9 @@ export class LocalMemoryClient {
|
|
|
169
175
|
gitRepoUrl,
|
|
170
176
|
metadata: Object.keys(dynamicMetadata).length > 0 ? JSON.stringify(dynamicMetadata) : undefined,
|
|
171
177
|
};
|
|
178
|
+
if (replaceExisting && customId && (await replaceMemoryRow(record))) {
|
|
179
|
+
return { success: true, id };
|
|
180
|
+
}
|
|
172
181
|
const db = await tursoConnectionManager.getConnection(shard.dbPath);
|
|
173
182
|
await tursoVectorSearch.insertVector(db, record);
|
|
174
183
|
await tursoShardManager.incrementVectorCount(shard.id);
|
|
@@ -225,7 +234,7 @@ export class LocalMemoryClient {
|
|
|
225
234
|
const allMemories = [];
|
|
226
235
|
for (const shard of shards) {
|
|
227
236
|
const db = await tursoConnectionManager.getConnection(shard.dbPath);
|
|
228
|
-
const rows = await tursoVectorSearch.listMemories(db, scope === "all-projects" ? "" : containerTag, limit);
|
|
237
|
+
const rows = await tursoVectorSearch.listMemories(db, scope === "all-projects" ? "" : containerTag, limit, options.kind ?? "normal");
|
|
229
238
|
allMemories.push(...rows);
|
|
230
239
|
}
|
|
231
240
|
allMemories.sort((a, b) => Number(b.created_at) - Number(a.created_at));
|
|
@@ -240,6 +249,7 @@ export class LocalMemoryClient {
|
|
|
240
249
|
ownCount: memories.length,
|
|
241
250
|
owner: activeOwner(),
|
|
242
251
|
settings: linkedSettingsFrom(CONFIG),
|
|
252
|
+
kind: options.kind ?? "normal",
|
|
243
253
|
});
|
|
244
254
|
memories = [...memories, ...linked.memories];
|
|
245
255
|
}
|
|
@@ -315,36 +325,10 @@ export class LocalMemoryClient {
|
|
|
315
325
|
return { success: false, dryRun: Boolean(dryRun), error: errorMessage };
|
|
316
326
|
}
|
|
317
327
|
}
|
|
318
|
-
async searchMemoriesBySessionID(sessionID, containerTag, limit = 10) {
|
|
328
|
+
async searchMemoriesBySessionID(sessionID, containerTag, limit = 10, kind = "normal") {
|
|
319
329
|
try {
|
|
320
330
|
await this.initialize();
|
|
321
|
-
const
|
|
322
|
-
const shards = await tursoShardManager.getAllShards(scope, hash);
|
|
323
|
-
if (shards.length === 0) {
|
|
324
|
-
return { success: true, results: [], total: 0, timing: 0 };
|
|
325
|
-
}
|
|
326
|
-
const allMemories = [];
|
|
327
|
-
for (const shard of shards) {
|
|
328
|
-
const db = await tursoConnectionManager.getConnection(shard.dbPath);
|
|
329
|
-
const memories = await tursoVectorSearch.getMemoriesBySessionID(db, sessionID);
|
|
330
|
-
allMemories.push(...memories);
|
|
331
|
-
}
|
|
332
|
-
allMemories.sort((a, b) => Number(b.created_at) - Number(a.created_at));
|
|
333
|
-
const results = allMemories.slice(0, limit).map((row) => ({
|
|
334
|
-
id: row.id,
|
|
335
|
-
memory: row.content,
|
|
336
|
-
similarity: 1.0,
|
|
337
|
-
tags: row.tags || [],
|
|
338
|
-
metadata: row.metadata || {},
|
|
339
|
-
containerTag: row.container_tag,
|
|
340
|
-
displayName: row.display_name,
|
|
341
|
-
userName: row.user_name,
|
|
342
|
-
userEmail: row.user_email,
|
|
343
|
-
projectPath: row.project_path,
|
|
344
|
-
projectName: row.project_name,
|
|
345
|
-
gitRepoUrl: row.git_repo_url,
|
|
346
|
-
createdAt: row.created_at,
|
|
347
|
-
}));
|
|
331
|
+
const results = await readSessionMemories(sessionID, containerTag, limit, kind);
|
|
348
332
|
return { success: true, results, total: results.length, timing: 0 };
|
|
349
333
|
}
|
|
350
334
|
catch (error) {
|
|
@@ -6,6 +6,7 @@ import { CONFIG } from "../config.js";
|
|
|
6
6
|
import { log } from "./logger.js";
|
|
7
7
|
import { cosineSimilarity } from "../utils/math.js";
|
|
8
8
|
import { parseExtractedVector } from "./turso/vector-utils.js";
|
|
9
|
+
import { memoryKindPredicate } from "./memory-kind.js";
|
|
9
10
|
export class DeduplicationService {
|
|
10
11
|
isRunning = false;
|
|
11
12
|
async detectAndRemoveDuplicates() {
|
|
@@ -25,7 +26,17 @@ export class DeduplicationService {
|
|
|
25
26
|
const nearDuplicateGroups = [];
|
|
26
27
|
for (const shard of allShards) {
|
|
27
28
|
const db = await tursoConnectionManager.getConnection(shard.dbPath);
|
|
28
|
-
const memories = await
|
|
29
|
+
const memories = await db.all(`
|
|
30
|
+
SELECT
|
|
31
|
+
id,
|
|
32
|
+
content,
|
|
33
|
+
container_tag,
|
|
34
|
+
created_at,
|
|
35
|
+
vector_extract(vector) AS vector_json
|
|
36
|
+
FROM memories
|
|
37
|
+
WHERE ${memoryKindPredicate()}
|
|
38
|
+
ORDER BY created_at DESC
|
|
39
|
+
`);
|
|
29
40
|
const contentMap = new Map();
|
|
30
41
|
for (const memory of memories) {
|
|
31
42
|
const key = `${memory.container_tag}:${memory.content}`;
|
|
@@ -10,6 +10,7 @@ import { KEYWORD_MODES } from "../utils/keyword-boost.js";
|
|
|
10
10
|
import { getMemoryLimitRule } from "../utils/memory-limits.js";
|
|
11
11
|
import { OPENING_CONTEXT_QUERY_MAX_LENGTH } from "./injection-options-config.js";
|
|
12
12
|
import { RERANKER_TIMEOUT_MAX_MS, RERANKER_TIMEOUT_MIN_MS } from "./request-timeouts.js";
|
|
13
|
+
import { checkMemorisationSettingsEdits, memorisationSettingsFrom, } from "../core/memorisation-settings.js";
|
|
13
14
|
const keys = new Set([
|
|
14
15
|
"opencodeProvider",
|
|
15
16
|
"opencodeModel",
|
|
@@ -53,6 +54,9 @@ const keys = new Set([
|
|
|
53
54
|
"subagentMemory",
|
|
54
55
|
"compaction.enabled",
|
|
55
56
|
"compaction.memoryLimit",
|
|
57
|
+
"memorisationRepeat",
|
|
58
|
+
"memorisationMaxSize",
|
|
59
|
+
"memorisationSizeUnit",
|
|
56
60
|
]);
|
|
57
61
|
function isFolder(path) {
|
|
58
62
|
try {
|
|
@@ -112,6 +116,10 @@ function isValidEdit(key, value) {
|
|
|
112
116
|
// keywordMode: null removes the key, so the mode follows the default again.
|
|
113
117
|
if (key === "keywordMode")
|
|
114
118
|
return value === null || KEYWORD_MODES.includes(value);
|
|
119
|
+
if (key === "memorisationSizeUnit")
|
|
120
|
+
return value === "characters" || value === "tokens";
|
|
121
|
+
if (key === "memorisationRepeat" || key === "memorisationMaxSize")
|
|
122
|
+
return true;
|
|
115
123
|
// The opening context query: null removes it; an empty query means no opening search.
|
|
116
124
|
if (key === "openingContextQuery") {
|
|
117
125
|
return (value === null ||
|
|
@@ -270,6 +278,12 @@ options = {}) {
|
|
|
270
278
|
throw new ConfigConflictError();
|
|
271
279
|
}
|
|
272
280
|
let content = original.content;
|
|
281
|
+
const errors = [];
|
|
282
|
+
const current = parse(content, errors, { allowTrailingComma: true });
|
|
283
|
+
if (errors.length || !current || typeof current !== "object" || Array.isArray(current)) {
|
|
284
|
+
throw new Error("Config contains invalid JSONC");
|
|
285
|
+
}
|
|
286
|
+
checkMemorisationSettingsEdits(edits, memorisationSettingsFrom(current));
|
|
273
287
|
for (const [key, value] of Object.entries(edits)) {
|
|
274
288
|
// A nested identifier maps to its JSONC leaf path, so chatMessage.maxMemories
|
|
275
289
|
// and compaction.enabled write the leaf, never a literal dotted key.
|
|
@@ -286,7 +300,6 @@ options = {}) {
|
|
|
286
300
|
formattingOptions: { insertSpaces: true, tabSize: 2, eol: "\n" },
|
|
287
301
|
}));
|
|
288
302
|
}
|
|
289
|
-
const errors = [];
|
|
290
303
|
const parsed = parse(content, errors, { allowTrailingComma: true });
|
|
291
304
|
if (errors.length)
|
|
292
305
|
throw new Error("Config contains invalid JSONC");
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { SearchResult } from "./turso/types.js";
|
|
2
2
|
import type { KeywordMode } from "../utils/keyword-boost.js";
|
|
3
|
+
import type { MemoryKind } from "./memory-kind.js";
|
|
3
4
|
/**
|
|
4
5
|
* Linked-space reads (design D6): the active space may link other spaces and
|
|
5
6
|
* read their memories, read-only. This module holds every rule of that read:
|
|
@@ -15,6 +16,9 @@ export type LinkedScope = "project" | "all-projects";
|
|
|
15
16
|
/** Options shared by the search and list reads: `includeLinked` reads linked spaces too (D6). */
|
|
16
17
|
export interface MemoryReadOptions {
|
|
17
18
|
includeLinked?: boolean;
|
|
19
|
+
kind?: MemoryKind;
|
|
20
|
+
/** Search only: the result limit for this read, instead of `maxMemories`. */
|
|
21
|
+
limit?: number;
|
|
18
22
|
}
|
|
19
23
|
/** The settings a linked search needs; the caller snapshots them from CONFIG. */
|
|
20
24
|
export interface LinkedSearchSettings {
|
|
@@ -56,6 +60,7 @@ export interface ListedMemory {
|
|
|
56
60
|
type: string | undefined;
|
|
57
61
|
createdAt: string;
|
|
58
62
|
metadata: Record<string, unknown> | undefined;
|
|
63
|
+
tags: string[];
|
|
59
64
|
displayName: unknown;
|
|
60
65
|
userName: unknown;
|
|
61
66
|
userEmail: unknown;
|
|
@@ -77,6 +82,7 @@ export declare function searchLinkedSpaces(input: {
|
|
|
77
82
|
ownCount: number;
|
|
78
83
|
owner: LinkedOwner;
|
|
79
84
|
settings: LinkedSearchSettings;
|
|
85
|
+
kind?: MemoryKind;
|
|
80
86
|
}): Promise<{
|
|
81
87
|
results: LinkedSearchResult[];
|
|
82
88
|
warnings: string[];
|
|
@@ -89,6 +95,7 @@ export declare function listLinkedSpaces(input: {
|
|
|
89
95
|
ownCount: number;
|
|
90
96
|
owner: LinkedOwner;
|
|
91
97
|
settings: LinkedSearchSettings;
|
|
98
|
+
kind?: MemoryKind;
|
|
92
99
|
}): Promise<{
|
|
93
100
|
memories: ListedMemory[];
|
|
94
101
|
warnings: string[];
|
|
@@ -106,3 +113,11 @@ export declare function linkedRefusal(name: string): {
|
|
|
106
113
|
export declare function findLinkedMemoryOwner(memoryId: string, owner: LinkedOwner, settings: LinkedSearchSettings): Promise<{
|
|
107
114
|
name: string;
|
|
108
115
|
} | null>;
|
|
116
|
+
/**
|
|
117
|
+
* Reads one memory row by id from the linked spaces, with the space name. Read-only.
|
|
118
|
+
* A `containerTag` keeps only rows of that project; null accepts any project.
|
|
119
|
+
*/
|
|
120
|
+
export declare function readLinkedMemory(memoryId: string, owner: LinkedOwner, settings: LinkedSearchSettings, containerTag: string | null): Promise<{
|
|
121
|
+
name: string;
|
|
122
|
+
row: Record<string, unknown>;
|
|
123
|
+
} | null>;
|
|
@@ -54,6 +54,7 @@ export function mapListedMemory(row) {
|
|
|
54
54
|
type: row.type ? String(row.type) : undefined,
|
|
55
55
|
createdAt: safeToISOString(row.created_at),
|
|
56
56
|
metadata: safeJSONParse(row.metadata),
|
|
57
|
+
tags: typeof row.tags === "string" ? row.tags.split(",").filter(Boolean) : [],
|
|
57
58
|
displayName: row.display_name,
|
|
58
59
|
userName: row.user_name,
|
|
59
60
|
userEmail: row.user_email,
|
|
@@ -143,7 +144,7 @@ export async function searchLinkedSpaces(input) {
|
|
|
143
144
|
continue;
|
|
144
145
|
}
|
|
145
146
|
try {
|
|
146
|
-
const { results } = await tursoVectorSearch.searchAcrossShards(shards, input.queryVector, input.scope === "all-projects" ? "" : input.containerTag, room, input.settings.similarityThreshold, input.query, input.settings.keywordBoost, input.settings.keywordMode, (dbPath) => target.store.openShard(dbPath));
|
|
147
|
+
const { results } = await tursoVectorSearch.searchAcrossShards(shards, input.queryVector, input.scope === "all-projects" ? "" : input.containerTag, room, input.settings.similarityThreshold, input.query, input.settings.keywordBoost, input.settings.keywordMode, (dbPath) => target.store.openShard(dbPath), input.kind ?? "normal");
|
|
147
148
|
linked.push(...results.map((result) => ({ ...result, spaceName: target.name })));
|
|
148
149
|
}
|
|
149
150
|
catch (error) {
|
|
@@ -174,7 +175,7 @@ export async function listLinkedSpaces(input) {
|
|
|
174
175
|
try {
|
|
175
176
|
for (const shard of shards) {
|
|
176
177
|
const db = await target.store.openShard(shard.dbPath);
|
|
177
|
-
const rows = await tursoVectorSearch.listMemories(db, input.scope === "all-projects" ? "" : input.containerTag, room);
|
|
178
|
+
const rows = await tursoVectorSearch.listMemories(db, input.scope === "all-projects" ? "" : input.containerTag, room, input.kind ?? "normal");
|
|
178
179
|
linked.push(...rows.map((row) => ({ ...mapListedMemory(row), spaceName: target.name })));
|
|
179
180
|
}
|
|
180
181
|
}
|
|
@@ -217,3 +218,23 @@ export async function findLinkedMemoryOwner(memoryId, owner, settings) {
|
|
|
217
218
|
}
|
|
218
219
|
return null;
|
|
219
220
|
}
|
|
221
|
+
/**
|
|
222
|
+
* Reads one memory row by id from the linked spaces, with the space name. Read-only.
|
|
223
|
+
* A `containerTag` keeps only rows of that project; null accepts any project.
|
|
224
|
+
*/
|
|
225
|
+
export async function readLinkedMemory(memoryId, owner, settings, containerTag) {
|
|
226
|
+
const warnings = [];
|
|
227
|
+
for (const target of await linkedTargets(owner, settings, warnings)) {
|
|
228
|
+
const shards = [
|
|
229
|
+
...(await target.store.listShards("user", "")),
|
|
230
|
+
...(await target.store.listShards("project", "")),
|
|
231
|
+
];
|
|
232
|
+
for (const shard of shards) {
|
|
233
|
+
const db = await target.store.openShard(shard.dbPath);
|
|
234
|
+
const row = await db.get(`SELECT * FROM memories WHERE id = ? AND (? IS NULL OR container_tag = ?)`, [memoryId, containerTag, containerTag]);
|
|
235
|
+
if (row)
|
|
236
|
+
return { name: target.name, row };
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
@@ -3,6 +3,7 @@ import { join } from "node:path";
|
|
|
3
3
|
import { CONFIG } from "../config.js";
|
|
4
4
|
import { tursoConnectionManager } from "./turso/connection-manager.js";
|
|
5
5
|
import { tursoShardManager } from "./turso/shard-manager.js";
|
|
6
|
+
import { memoryKindPredicate } from "./memory-kind.js";
|
|
6
7
|
/** Read user and assistant IDs saved by live capture in one project session. */
|
|
7
8
|
export async function findLiveCapturedEntryIds(projectHash, host, sessionId) {
|
|
8
9
|
const entries = new Set();
|
|
@@ -11,7 +12,7 @@ export async function findLiveCapturedEntryIds(projectHash, host, sessionId) {
|
|
|
11
12
|
const shards = await tursoShardManager.getAllShards("project", projectHash);
|
|
12
13
|
for (const shard of shards) {
|
|
13
14
|
const db = await tursoConnectionManager.getConnection(shard.dbPath);
|
|
14
|
-
const rows = await db.all(`SELECT metadata FROM memories WHERE json_valid(metadata)
|
|
15
|
+
const rows = await db.all(`SELECT metadata FROM memories WHERE json_valid(metadata) AND ${memoryKindPredicate()}
|
|
15
16
|
AND json_extract(metadata, '$.host') = ?
|
|
16
17
|
AND json_extract(metadata, '$.hostSessionId') = ?
|
|
17
18
|
AND json_extract(metadata, '$.sourceType') != 'history-import'`, [host, sessionId]);
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { MemoryScope } from "./client.js";
|
|
2
|
+
import type { RouteResult } from "./chat/chat-routes.js";
|
|
3
|
+
import type { LinkedSearchSettings } from "./linked-search.js";
|
|
4
|
+
/** The CONFIG values recall needs. The route registration builds them, so this module has no CONFIG import. */
|
|
5
|
+
export interface MemorisationRouteConfig {
|
|
6
|
+
defaultScope: MemoryScope | undefined;
|
|
7
|
+
linked: LinkedSearchSettings;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Routes for the Memorisation page. Returns undefined for paths it does not own.
|
|
11
|
+
* `GET /api/memorisations?tags=a,b` lists, `GET /api/memorisations/recall?q=&tags=` recalls
|
|
12
|
+
* and `POST /api/memorisations/bulk-delete` deletes. The general `/api/memories` delete
|
|
13
|
+
* routes refuse memorisations by design, so this module owns the bulk delete. Memorisation
|
|
14
|
+
* text is returned to the page and never written to the log.
|
|
15
|
+
*/
|
|
16
|
+
export declare function routeMemorisations(path: string, method: string, url: URL, cwd: string, config: MemorisationRouteConfig, readBody?: () => Promise<any>): Promise<RouteResult | undefined>;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { isMemorisationId, parseTagList, visibleTags } from "../core/memorisation.js";
|
|
2
|
+
/** Enough for any project: a memorisation is capped in size, and the user saves each on purpose. */
|
|
3
|
+
const LIST_LIMIT = 200;
|
|
4
|
+
/** The most IDs one bulk delete takes. */
|
|
5
|
+
const BULK_DELETE_MAX = 500;
|
|
6
|
+
const ok = (data) => ({ status: 200, body: { success: true, data } });
|
|
7
|
+
const fail = (status, error) => ({
|
|
8
|
+
status,
|
|
9
|
+
body: { success: false, error },
|
|
10
|
+
});
|
|
11
|
+
/**
|
|
12
|
+
* Routes for the Memorisation page. Returns undefined for paths it does not own.
|
|
13
|
+
* `GET /api/memorisations?tags=a,b` lists, `GET /api/memorisations/recall?q=&tags=` recalls
|
|
14
|
+
* and `POST /api/memorisations/bulk-delete` deletes. The general `/api/memories` delete
|
|
15
|
+
* routes refuse memorisations by design, so this module owns the bulk delete. Memorisation
|
|
16
|
+
* text is returned to the page and never written to the log.
|
|
17
|
+
*/
|
|
18
|
+
export async function routeMemorisations(path, method, url, cwd, config, readBody) {
|
|
19
|
+
const wanted = parseTagList(url.searchParams.get("tags") ?? undefined);
|
|
20
|
+
if (method === "GET" && path === "/api/memorisations")
|
|
21
|
+
return listMemorisations(cwd, wanted);
|
|
22
|
+
if (method === "GET" && path === "/api/memorisations/recall") {
|
|
23
|
+
return recall(url.searchParams, wanted, cwd, config);
|
|
24
|
+
}
|
|
25
|
+
if (method === "POST" && path === "/api/memorisations/bulk-delete" && readBody) {
|
|
26
|
+
return bulkDelete(await readBody(), cwd, config);
|
|
27
|
+
}
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
async function listMemorisations(cwd, wanted) {
|
|
31
|
+
const [{ listMemorisationsByTags }, { getTags }] = await Promise.all([
|
|
32
|
+
import("./memorisation-store.js"),
|
|
33
|
+
import("./tags.js"),
|
|
34
|
+
]);
|
|
35
|
+
const result = await listMemorisationsByTags(getTags(cwd).project.tag, wanted, "project", LIST_LIMIT);
|
|
36
|
+
if (!result.success)
|
|
37
|
+
return fail(500, result.error);
|
|
38
|
+
const memorisations = result.memories.map((memory) => ({
|
|
39
|
+
id: memory.id,
|
|
40
|
+
content: memory.summary,
|
|
41
|
+
createdAt: memory.createdAt,
|
|
42
|
+
host: typeof memory.metadata?.host === "string" ? memory.metadata.host : undefined,
|
|
43
|
+
kind: typeof memory.metadata?.memorisation === "string" ? memory.metadata.memorisation : undefined,
|
|
44
|
+
space: memory.spaceName,
|
|
45
|
+
tags: visibleTags(memory.tags),
|
|
46
|
+
}));
|
|
47
|
+
return ok({ memorisations });
|
|
48
|
+
}
|
|
49
|
+
async function recall(params, wanted, cwd, config) {
|
|
50
|
+
const text = params.get("q")?.trim();
|
|
51
|
+
if (!text && wanted.length === 0)
|
|
52
|
+
return fail(400, "q or tags required: an ID, text or labels");
|
|
53
|
+
const limit = Number(params.get("limit"));
|
|
54
|
+
const [{ runRecall }, { getTags }] = await Promise.all([
|
|
55
|
+
import("../core/memorisation-operations.js"),
|
|
56
|
+
import("./tags.js"),
|
|
57
|
+
]);
|
|
58
|
+
const result = await runRecall({
|
|
59
|
+
query: text,
|
|
60
|
+
tags: wanted.join(","),
|
|
61
|
+
...(Number.isInteger(limit) && limit > 0 ? { limit } : {}),
|
|
62
|
+
}, getTags(cwd), config.defaultScope, config.linked);
|
|
63
|
+
if (result.success !== true) {
|
|
64
|
+
// An unknown ID is a missing record. Any other refusal is a server problem.
|
|
65
|
+
const error = String(result.error ?? "Recall failed");
|
|
66
|
+
return fail(error.startsWith("No memorisation has the ID") ? 404 : 500, error);
|
|
67
|
+
}
|
|
68
|
+
return ok({ count: result.count, matches: result.matches });
|
|
69
|
+
}
|
|
70
|
+
/** Deletes memorisations of this project from the active space. Linked spaces are never touched. */
|
|
71
|
+
async function bulkDelete(body, cwd, config) {
|
|
72
|
+
const ids = body?.ids;
|
|
73
|
+
if (!Array.isArray(ids) ||
|
|
74
|
+
ids.length < 1 ||
|
|
75
|
+
ids.length > BULK_DELETE_MAX ||
|
|
76
|
+
!ids.every((id) => typeof id === "string" && isMemorisationId(id))) {
|
|
77
|
+
return fail(400, `ids must be 1 to ${BULK_DELETE_MAX} memorisation IDs (ms-xxxxxx)`);
|
|
78
|
+
}
|
|
79
|
+
const [{ deleteMemorisation }, { getTags }] = await Promise.all([
|
|
80
|
+
import("./memorisation-store.js"),
|
|
81
|
+
import("./tags.js"),
|
|
82
|
+
]);
|
|
83
|
+
const containerTag = getTags(cwd).project.tag;
|
|
84
|
+
const deleted = [];
|
|
85
|
+
const skipped = [];
|
|
86
|
+
for (const id of new Set(ids)) {
|
|
87
|
+
const outcome = await deleteMemorisation(id, containerTag, config.linked);
|
|
88
|
+
if (outcome === "deleted")
|
|
89
|
+
deleted.push(id);
|
|
90
|
+
else
|
|
91
|
+
skipped.push({ id, reason: outcome });
|
|
92
|
+
}
|
|
93
|
+
return ok({ deleted, skipped });
|
|
94
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { type LinkedSearchSettings, type ListedMemory } from "./linked-search.js";
|
|
2
|
+
import type { MemoryScope } from "./client.js";
|
|
3
|
+
export interface StoredMemorisation {
|
|
4
|
+
id: string;
|
|
5
|
+
content: string;
|
|
6
|
+
createdAt: string;
|
|
7
|
+
/** The labels: the stored tags without the fixed memorisation tag. */
|
|
8
|
+
tags: string[];
|
|
9
|
+
/** Set when the record lives in a linked, read-only space. */
|
|
10
|
+
space?: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Looks a memorisation up by exact ID in the active space, then in the linked spaces. Only
|
|
14
|
+
* rows of the given project are returned, the same rule as text recall and list.
|
|
15
|
+
* The caller snapshots `linked` from CONFIG, so this module never imports it.
|
|
16
|
+
*/
|
|
17
|
+
export declare function getMemorisationById(id: string, containerTag: string, linked: LinkedSearchSettings): Promise<StoredMemorisation | null>;
|
|
18
|
+
/**
|
|
19
|
+
* True when any project in the active or a linked space already uses this memorisation ID.
|
|
20
|
+
* A new ID must be free everywhere, so this check ignores the project.
|
|
21
|
+
*/
|
|
22
|
+
export declare function memorisationIdTaken(id: string, linked: LinkedSearchSettings): Promise<boolean>;
|
|
23
|
+
/** The newest session snapshot of one host session in the project, or null. */
|
|
24
|
+
export declare function findSnapshotByHostSession(hostSessionId: string, containerTag: string): Promise<{
|
|
25
|
+
id: string;
|
|
26
|
+
} | null>;
|
|
27
|
+
/**
|
|
28
|
+
* The project's memorisations that carry every tag in `tags`, newest first, in the active
|
|
29
|
+
* space and its linked spaces. The tag filter runs before `limit`, so an old labelled
|
|
30
|
+
* row is not pushed out by newer rows without the label.
|
|
31
|
+
*/
|
|
32
|
+
export declare function listMemorisationsByTags(containerTag: string, tags: string[], scope: MemoryScope, limit: number): Promise<{
|
|
33
|
+
success: true;
|
|
34
|
+
memories: ListedMemory[];
|
|
35
|
+
} | {
|
|
36
|
+
success: false;
|
|
37
|
+
error: string;
|
|
38
|
+
}>;
|
|
39
|
+
export type MemorisationDeleteOutcome = "deleted" | "not-found" | "linked" | "not-memorisation";
|
|
40
|
+
/**
|
|
41
|
+
* Deletes one memorisation of this project from the active space. A row of another project,
|
|
42
|
+
* or no row, is "not-found". A row of another type is kept ("not-memorisation"). A row that
|
|
43
|
+
* lives only in a linked, read-only space is "linked" and is never touched.
|
|
44
|
+
*/
|
|
45
|
+
export declare function deleteMemorisation(id: string, containerTag: string, linked: LinkedSearchSettings): Promise<MemorisationDeleteOutcome>;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { hasAllTags, visibleTags } from "../core/memorisation.js";
|
|
2
|
+
import { activeOwner } from "./active-space-access.js";
|
|
3
|
+
import { readLinkedMemory } from "./linked-search.js";
|
|
4
|
+
import { extractScopeFromContainerTag } from "./memory-scope.js";
|
|
5
|
+
import { memoryClient } from "./client.js";
|
|
6
|
+
import { tursoConnectionManager } from "./turso/connection-manager.js";
|
|
7
|
+
import { tursoShardManager } from "./turso/shard-manager.js";
|
|
8
|
+
import { tursoVectorSearch } from "./turso/vector-search.js";
|
|
9
|
+
function toStored(row, space) {
|
|
10
|
+
const created = Number(row.created_at);
|
|
11
|
+
return {
|
|
12
|
+
id: String(row.id),
|
|
13
|
+
content: String(row.content),
|
|
14
|
+
createdAt: new Date(Number.isFinite(created) ? created : 0).toISOString(),
|
|
15
|
+
tags: visibleTags(typeof row.tags === "string" ? row.tags.split(",") : []),
|
|
16
|
+
...(space ? { space } : {}),
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
/** The active-space row with this ID, in any project, or null. */
|
|
20
|
+
async function findActiveRow(id) {
|
|
21
|
+
await memoryClient.ensureStorageReady();
|
|
22
|
+
const shards = [
|
|
23
|
+
...(await tursoShardManager.getAllShards("user", "")),
|
|
24
|
+
...(await tursoShardManager.getAllShards("project", "")),
|
|
25
|
+
];
|
|
26
|
+
for (const shard of shards) {
|
|
27
|
+
const db = await tursoConnectionManager.getConnection(shard.dbPath);
|
|
28
|
+
const row = await tursoVectorSearch.getMemoryById(db, id);
|
|
29
|
+
if (row)
|
|
30
|
+
return row;
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Looks a memorisation up by exact ID in the active space, then in the linked spaces. Only
|
|
36
|
+
* rows of the given project are returned, the same rule as text recall and list.
|
|
37
|
+
* The caller snapshots `linked` from CONFIG, so this module never imports it.
|
|
38
|
+
*/
|
|
39
|
+
export async function getMemorisationById(id, containerTag, linked) {
|
|
40
|
+
const row = await findActiveRow(id);
|
|
41
|
+
if (row?.container_tag === containerTag && row.type === "memorisation")
|
|
42
|
+
return toStored(row);
|
|
43
|
+
const found = await readLinkedMemory(id, activeOwner(), linked, containerTag);
|
|
44
|
+
return found?.row.type === "memorisation" ? toStored(found.row, found.name) : null;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* True when any project in the active or a linked space already uses this memorisation ID.
|
|
48
|
+
* A new ID must be free everywhere, so this check ignores the project.
|
|
49
|
+
*/
|
|
50
|
+
export async function memorisationIdTaken(id, linked) {
|
|
51
|
+
if ((await findActiveRow(id))?.type === "memorisation")
|
|
52
|
+
return true;
|
|
53
|
+
const found = await readLinkedMemory(id, activeOwner(), linked, null);
|
|
54
|
+
return found?.row.type === "memorisation";
|
|
55
|
+
}
|
|
56
|
+
/** The newest session snapshot of one host session in the project, or null. */
|
|
57
|
+
export async function findSnapshotByHostSession(hostSessionId, containerTag) {
|
|
58
|
+
await memoryClient.ensureStorageReady();
|
|
59
|
+
const { scope, hash } = extractScopeFromContainerTag(containerTag);
|
|
60
|
+
let newest = null;
|
|
61
|
+
for (const shard of await tursoShardManager.getAllShards(scope, hash)) {
|
|
62
|
+
const db = await tursoConnectionManager.getConnection(shard.dbPath);
|
|
63
|
+
const row = await db.get(`SELECT id, created_at FROM memories
|
|
64
|
+
WHERE type = 'memorisation'
|
|
65
|
+
AND container_tag = ?
|
|
66
|
+
AND json_extract(metadata, '$.memorisation') = 'snapshot'
|
|
67
|
+
AND json_extract(metadata, '$.hostSessionId') = ?
|
|
68
|
+
ORDER BY created_at DESC LIMIT 1`, [containerTag, hostSessionId]);
|
|
69
|
+
if (row && (!newest || Number(row.created_at) > newest.createdAt)) {
|
|
70
|
+
newest = { id: String(row.id), createdAt: Number(row.created_at) };
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return newest ? { id: newest.id } : null;
|
|
74
|
+
}
|
|
75
|
+
/** Far above any real project: a memorisation is capped in size and the user saves each on purpose. */
|
|
76
|
+
const READ_CEILING = 10_000;
|
|
77
|
+
/**
|
|
78
|
+
* The project's memorisations that carry every tag in `tags`, newest first, in the active
|
|
79
|
+
* space and its linked spaces. The tag filter runs before `limit`, so an old labelled
|
|
80
|
+
* row is not pushed out by newer rows without the label.
|
|
81
|
+
*/
|
|
82
|
+
export async function listMemorisationsByTags(containerTag, tags, scope, limit) {
|
|
83
|
+
const result = await memoryClient.listMemories(containerTag, READ_CEILING, scope, {
|
|
84
|
+
includeLinked: true,
|
|
85
|
+
kind: "memorisation",
|
|
86
|
+
});
|
|
87
|
+
if (!result.success)
|
|
88
|
+
return { success: false, error: result.error };
|
|
89
|
+
const memories = result.memories
|
|
90
|
+
.filter((memory) => hasAllTags(memory.tags, tags))
|
|
91
|
+
// Own rows come first from the client, then linked rows. Callers want one timeline.
|
|
92
|
+
.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())
|
|
93
|
+
.slice(0, limit);
|
|
94
|
+
return { success: true, memories };
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Deletes one memorisation of this project from the active space. A row of another project,
|
|
98
|
+
* or no row, is "not-found". A row of another type is kept ("not-memorisation"). A row that
|
|
99
|
+
* lives only in a linked, read-only space is "linked" and is never touched.
|
|
100
|
+
*/
|
|
101
|
+
export async function deleteMemorisation(id, containerTag, linked) {
|
|
102
|
+
await memoryClient.ensureStorageReady();
|
|
103
|
+
const shards = [
|
|
104
|
+
...(await tursoShardManager.getAllShards("user", "")),
|
|
105
|
+
...(await tursoShardManager.getAllShards("project", "")),
|
|
106
|
+
];
|
|
107
|
+
for (const shard of shards) {
|
|
108
|
+
const db = await tursoConnectionManager.getConnection(shard.dbPath);
|
|
109
|
+
const row = await tursoVectorSearch.getMemoryById(db, id);
|
|
110
|
+
if (!row || row.container_tag !== containerTag)
|
|
111
|
+
continue;
|
|
112
|
+
if (row.type !== "memorisation")
|
|
113
|
+
return "not-memorisation";
|
|
114
|
+
await tursoVectorSearch.deleteVector(db, id);
|
|
115
|
+
await tursoShardManager.decrementVectorCount(shard.id);
|
|
116
|
+
return "deleted";
|
|
117
|
+
}
|
|
118
|
+
const found = await readLinkedMemory(id, activeOwner(), linked, containerTag);
|
|
119
|
+
return found?.row.type === "memorisation" ? "linked" : "not-found";
|
|
120
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** The records an ordinary memory read includes. Memorisations need an explicit request. */
|
|
2
|
+
export type MemoryKind = "normal" | "memorisation";
|
|
3
|
+
/** Appends the record-kind predicate before the query's result limit. */
|
|
4
|
+
export declare function memoryKindPredicate(column?: string, kind?: MemoryKind): string;
|
|
5
|
+
/** Untagged normal memories eligible for AI tag backfill. */
|
|
6
|
+
export declare const UNTAGGED_NORMAL_PREDICATE: string;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Appends the record-kind predicate before the query's result limit. */
|
|
2
|
+
export function memoryKindPredicate(column = "type", kind = "normal") {
|
|
3
|
+
return kind === "memorisation" ? `${column} = 'memorisation'` : `${column} IS NOT 'memorisation'`;
|
|
4
|
+
}
|
|
5
|
+
/** Untagged normal memories eligible for AI tag backfill. */
|
|
6
|
+
export const UNTAGGED_NORMAL_PREDICATE = `(tags IS NULL OR tags = '') AND ${memoryKindPredicate()}`;
|
|
@@ -78,7 +78,8 @@ export class MemoryPortabilityService {
|
|
|
78
78
|
if (!existsSync(shard.dbPath))
|
|
79
79
|
continue;
|
|
80
80
|
const db = await tursoConnectionManager.getConnection(shard.dbPath);
|
|
81
|
-
|
|
81
|
+
// An export keeps every kind, memorisations included.
|
|
82
|
+
const rows = await tursoVectorSearch.getAllMemories(db, "all");
|
|
82
83
|
for (const row of rows) {
|
|
83
84
|
const exported = rowToExportedMemory(row);
|
|
84
85
|
if (exported)
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { MemoryRecord } from "./turso/types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Swaps the stored row that has `record.id` for `record`, in the shard that
|
|
4
|
+
* holds it, in one transaction: a failed insert rolls the delete back, so the
|
|
5
|
+
* old row survives. Returns false when no row has that ID. The caller embeds
|
|
6
|
+
* first, so an embedding failure never reaches this point.
|
|
7
|
+
*/
|
|
8
|
+
export declare function replaceMemoryRow(record: MemoryRecord): Promise<boolean>;
|