om-memory-system 5.9.0 → 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.
Files changed (93) hide show
  1. package/README.md +7 -11
  2. package/bin/omms-launch.mjs +8 -4
  3. package/dist/adapters/opencode/conversation.js +4 -2
  4. package/dist/adapters/opencode/memory-tool.d.ts +4 -0
  5. package/dist/adapters/opencode/memory-tool.js +53 -0
  6. package/dist/adapters/opencode/package-skills.d.ts +1 -1
  7. package/dist/adapters/opencode/package-skills.js +17 -15
  8. package/dist/adapters/pi/extension.js +7 -0
  9. package/dist/cli/memory-command.d.ts +2 -0
  10. package/dist/cli/memory-command.js +35 -9
  11. package/dist/cli/web-command.js +25 -9
  12. package/dist/cli/web-step-aside.d.ts +17 -0
  13. package/dist/cli/web-step-aside.js +34 -0
  14. package/dist/config-paths.d.ts +5 -0
  15. package/dist/config-paths.js +15 -0
  16. package/dist/config-template.d.ts +1 -1
  17. package/dist/config-template.js +8 -0
  18. package/dist/config.d.ts +51 -40
  19. package/dist/config.js +8 -15
  20. package/dist/core/capture.d.ts +2 -1
  21. package/dist/core/capture.js +8 -1
  22. package/dist/core/host.d.ts +12 -0
  23. package/dist/core/memorisation-operations.d.ts +33 -0
  24. package/dist/core/memorisation-operations.js +160 -0
  25. package/dist/core/memorisation-settings.d.ts +11 -0
  26. package/dist/core/memorisation-settings.js +48 -0
  27. package/dist/core/memorisation.d.ts +43 -0
  28. package/dist/core/memorisation.js +76 -0
  29. package/dist/core/memorise-turn.d.ts +11 -0
  30. package/dist/core/memorise-turn.js +64 -0
  31. package/dist/core/memory-operations.d.ts +6 -1
  32. package/dist/core/memory-operations.js +29 -0
  33. package/dist/core/memory-tool-text.js +1 -1
  34. package/dist/core/retrieval.d.ts +3 -2
  35. package/dist/core/retrieval.js +11 -4
  36. package/dist/core/tool-outcome.d.ts +6 -0
  37. package/dist/core/tool-outcome.js +31 -0
  38. package/dist/importer/claude-conversation.js +8 -1
  39. package/dist/importer/claude-turn-units.js +1 -0
  40. package/dist/importer/importer.js +3 -3
  41. package/dist/importer/opencode-reader.js +2 -1
  42. package/dist/importer/pi-conversation.js +11 -5
  43. package/dist/index.js +2 -42
  44. package/dist/services/api-handlers.js +13 -35
  45. package/dist/services/capture-diagnostics.d.ts +3 -2
  46. package/dist/services/capture-diagnostics.js +5 -1
  47. package/dist/services/cleanup-service.js +2 -1
  48. package/dist/services/client-session-read.d.ts +16 -0
  49. package/dist/services/client-session-read.js +32 -0
  50. package/dist/services/client.d.ts +6 -1
  51. package/dist/services/client.js +20 -36
  52. package/dist/services/deduplication-service.js +12 -1
  53. package/dist/services/global-config-writer.js +14 -1
  54. package/dist/services/linked-search.d.ts +15 -0
  55. package/dist/services/linked-search.js +23 -2
  56. package/dist/services/live-captured-entries.js +2 -1
  57. package/dist/services/memorisation-routes.d.ts +16 -0
  58. package/dist/services/memorisation-routes.js +94 -0
  59. package/dist/services/memorisation-store.d.ts +45 -0
  60. package/dist/services/memorisation-store.js +120 -0
  61. package/dist/services/memory-kind.d.ts +6 -0
  62. package/dist/services/memory-kind.js +6 -0
  63. package/dist/services/memory-portability-service.js +2 -1
  64. package/dist/services/memory-replace.d.ts +8 -0
  65. package/dist/services/memory-replace.js +26 -0
  66. package/dist/services/retrieval-rerank.d.ts +3 -1
  67. package/dist/services/retrieval-rerank.js +1 -1
  68. package/dist/services/settings-snapshot.js +6 -0
  69. package/dist/services/turso/exact-scan.d.ts +2 -1
  70. package/dist/services/turso/exact-scan.js +4 -3
  71. package/dist/services/turso/keyword-search.d.ts +2 -1
  72. package/dist/services/turso/keyword-search.js +3 -2
  73. package/dist/services/turso/vector-search.d.ts +10 -9
  74. package/dist/services/turso/vector-search.js +38 -95
  75. package/dist/services/turso/vector-writes.d.ts +8 -0
  76. package/dist/services/turso/vector-writes.js +80 -0
  77. package/dist/services/web-ensure.d.ts +1 -0
  78. package/dist/services/web-ensure.js +5 -1
  79. package/dist/services/web-memory-rows.d.ts +12 -0
  80. package/dist/services/web-memory-rows.js +58 -0
  81. package/dist/services/web-route-modules.js +5 -0
  82. package/dist/services/web-update.d.ts +6 -1
  83. package/dist/services/web-update.js +12 -1
  84. package/dist/v2/adapter.d.ts +13 -1
  85. package/dist/v2/adapter.js +7 -0
  86. package/dist/web/assets/index-CoYuPYCS.css +2 -0
  87. package/dist/web/assets/index-DQgI670D.js +171 -0
  88. package/dist/web/index.html +2 -2
  89. package/package.json +1 -1
  90. package/skills/omms-memory/SKILL.md +18 -0
  91. package/skills/omms-remember/SKILL.md +118 -0
  92. package/dist/web/assets/index-2BNRLGqe.css +0 -2
  93. package/dist/web/assets/index-Bhk1P5Z7.js +0 -170
@@ -1,6 +1,7 @@
1
1
  import { type MemoryReadOptions } from "./linked-search.js";
2
2
  import type { MemoryType } from "../types/index.js";
3
3
  import type { SearchResult } from "./turso/types.js";
4
+ import type { MemoryKind } from "./memory-kind.js";
4
5
  export type MemoryScope = "project" | "all-projects";
5
6
  export declare class LocalMemoryClient {
6
7
  private initPromise;
@@ -34,6 +35,10 @@ export declare class LocalMemoryClient {
34
35
  timing: number;
35
36
  }>;
36
37
  addMemory(content: string, containerTag: string, metadata?: {
38
+ /** A caller-chosen record ID, such as a memorisation `ms-` ID. Default: a generated `mem_` ID. */
39
+ id?: string;
40
+ /** Overwrite the row with `id` in one transaction, so a failed write keeps the old row. */
41
+ replaceExisting?: boolean;
37
42
  type?: MemoryType;
38
43
  source?: "manual" | "auto-capture" | "import" | "api";
39
44
  tags?: string[];
@@ -95,7 +100,7 @@ export declare class LocalMemoryClient {
95
100
  }): Promise<import("./shard-path-migration-service.js").PathMigrationResult>;
96
101
  exportMemories(currentDirectory: string, outputPath: string): Promise<import("./memory-portability-service.js").ExportMemoriesResult>;
97
102
  importMemories(currentDirectory: string, inputPath: string, dryRun?: boolean): Promise<import("./memory-portability-service.js").ImportMemoriesResult>;
98
- searchMemoriesBySessionID(sessionID: string, containerTag: string, limit?: number): Promise<{
103
+ searchMemoriesBySessionID(sessionID: string, containerTag: string, limit?: number, kind?: MemoryKind): Promise<{
99
104
  success: true;
100
105
  results: {
101
106
  id: any;
@@ -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: linkedSettingsFrom(CONFIG),
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 { scope, hash } = extractScopeFromContainerTag(containerTag);
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 tursoVectorSearch.getAllMemoriesWithExtractedVectors(db);
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
- const rows = await tursoVectorSearch.getAllMemories(db);
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)