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.
Files changed (86) hide show
  1. package/README.md +7 -11
  2. package/dist/adapters/opencode/conversation.js +4 -2
  3. package/dist/adapters/opencode/memory-tool.d.ts +4 -0
  4. package/dist/adapters/opencode/memory-tool.js +53 -0
  5. package/dist/adapters/opencode/package-skills.d.ts +1 -1
  6. package/dist/adapters/opencode/package-skills.js +17 -15
  7. package/dist/adapters/pi/extension.js +7 -0
  8. package/dist/cli/memory-command.d.ts +2 -0
  9. package/dist/cli/memory-command.js +35 -9
  10. package/dist/config-paths.d.ts +5 -0
  11. package/dist/config-paths.js +15 -0
  12. package/dist/config-template.d.ts +1 -1
  13. package/dist/config-template.js +8 -0
  14. package/dist/config.d.ts +51 -40
  15. package/dist/config.js +8 -15
  16. package/dist/core/capture.d.ts +2 -1
  17. package/dist/core/capture.js +8 -1
  18. package/dist/core/host.d.ts +12 -0
  19. package/dist/core/memorisation-operations.d.ts +33 -0
  20. package/dist/core/memorisation-operations.js +160 -0
  21. package/dist/core/memorisation-settings.d.ts +11 -0
  22. package/dist/core/memorisation-settings.js +48 -0
  23. package/dist/core/memorisation.d.ts +43 -0
  24. package/dist/core/memorisation.js +76 -0
  25. package/dist/core/memorise-turn.d.ts +11 -0
  26. package/dist/core/memorise-turn.js +64 -0
  27. package/dist/core/memory-operations.d.ts +6 -1
  28. package/dist/core/memory-operations.js +29 -0
  29. package/dist/core/memory-tool-text.js +1 -1
  30. package/dist/core/retrieval.d.ts +3 -2
  31. package/dist/core/retrieval.js +11 -4
  32. package/dist/core/tool-outcome.d.ts +6 -0
  33. package/dist/core/tool-outcome.js +31 -0
  34. package/dist/importer/claude-conversation.js +8 -1
  35. package/dist/importer/claude-turn-units.js +1 -0
  36. package/dist/importer/importer.js +3 -3
  37. package/dist/importer/opencode-reader.js +2 -1
  38. package/dist/importer/pi-conversation.js +11 -5
  39. package/dist/index.js +2 -42
  40. package/dist/services/api-handlers.js +13 -35
  41. package/dist/services/capture-diagnostics.d.ts +3 -2
  42. package/dist/services/capture-diagnostics.js +5 -1
  43. package/dist/services/cleanup-service.js +2 -1
  44. package/dist/services/client-session-read.d.ts +16 -0
  45. package/dist/services/client-session-read.js +32 -0
  46. package/dist/services/client.d.ts +6 -1
  47. package/dist/services/client.js +20 -36
  48. package/dist/services/deduplication-service.js +12 -1
  49. package/dist/services/global-config-writer.js +14 -1
  50. package/dist/services/linked-search.d.ts +15 -0
  51. package/dist/services/linked-search.js +23 -2
  52. package/dist/services/live-captured-entries.js +2 -1
  53. package/dist/services/memorisation-routes.d.ts +16 -0
  54. package/dist/services/memorisation-routes.js +94 -0
  55. package/dist/services/memorisation-store.d.ts +45 -0
  56. package/dist/services/memorisation-store.js +120 -0
  57. package/dist/services/memory-kind.d.ts +6 -0
  58. package/dist/services/memory-kind.js +6 -0
  59. package/dist/services/memory-portability-service.js +2 -1
  60. package/dist/services/memory-replace.d.ts +8 -0
  61. package/dist/services/memory-replace.js +26 -0
  62. package/dist/services/retrieval-rerank.d.ts +3 -1
  63. package/dist/services/retrieval-rerank.js +1 -1
  64. package/dist/services/settings-snapshot.js +6 -0
  65. package/dist/services/turso/exact-scan.d.ts +2 -1
  66. package/dist/services/turso/exact-scan.js +4 -3
  67. package/dist/services/turso/keyword-search.d.ts +2 -1
  68. package/dist/services/turso/keyword-search.js +3 -2
  69. package/dist/services/turso/vector-search.d.ts +10 -9
  70. package/dist/services/turso/vector-search.js +38 -95
  71. package/dist/services/turso/vector-writes.d.ts +8 -0
  72. package/dist/services/turso/vector-writes.js +80 -0
  73. package/dist/services/web-ensure.d.ts +1 -0
  74. package/dist/services/web-ensure.js +5 -1
  75. package/dist/services/web-memory-rows.d.ts +12 -0
  76. package/dist/services/web-memory-rows.js +58 -0
  77. package/dist/services/web-route-modules.js +5 -0
  78. package/dist/v2/adapter.d.ts +13 -1
  79. package/dist/v2/adapter.js +7 -0
  80. package/dist/web/assets/{index-Be6Uh64d.css → index-CoYuPYCS.css} +1 -1
  81. package/dist/web/assets/index-DQgI670D.js +171 -0
  82. package/dist/web/index.html +2 -2
  83. package/package.json +1 -1
  84. package/skills/omms-memory/SKILL.md +18 -0
  85. package/skills/omms-remember/SKILL.md +118 -0
  86. package/dist/web/assets/index-BX_zZiKa.js +0 -170
@@ -0,0 +1,26 @@
1
+ import { tursoConnectionManager } from "./turso/connection-manager.js";
2
+ import { tursoShardManager } from "./turso/shard-manager.js";
3
+ import { tursoVectorSearch } from "./turso/vector-search.js";
4
+ /**
5
+ * Swaps the stored row that has `record.id` for `record`, in the shard that
6
+ * holds it, in one transaction: a failed insert rolls the delete back, so the
7
+ * old row survives. Returns false when no row has that ID. The caller embeds
8
+ * first, so an embedding failure never reaches this point.
9
+ */
10
+ export async function replaceMemoryRow(record) {
11
+ const shards = [
12
+ ...(await tursoShardManager.getAllShards("user", "")),
13
+ ...(await tursoShardManager.getAllShards("project", "")),
14
+ ];
15
+ for (const shard of shards) {
16
+ const db = await tursoConnectionManager.getConnection(shard.dbPath);
17
+ if (!(await tursoVectorSearch.getMemoryById(db, record.id)))
18
+ continue;
19
+ await db.transaction("write", async (tx) => {
20
+ await tx.execute({ sql: `DELETE FROM memories WHERE id = ?`, args: [record.id] });
21
+ await tursoVectorSearch.insertVectorInTransaction(tx, record);
22
+ });
23
+ return true;
24
+ }
25
+ return false;
26
+ }
@@ -10,8 +10,9 @@
10
10
  */
11
11
  import type { SearchResult, ShardInfo } from "./turso/types.js";
12
12
  import { type RerankerSettings } from "./reranker.js";
13
+ import type { MemoryKind } from "./memory-kind.js";
13
14
  /** The shape of tursoVectorSearch.searchAcrossShards, injectable for tests. */
14
- export type SearchAcrossShards = (shards: ShardInfo[], queryVector: Float32Array, containerTag: string, limit: number, similarityThreshold: number, queryText?: string) => Promise<{
15
+ export type SearchAcrossShards = (shards: ShardInfo[], queryVector: Float32Array, containerTag: string, limit: number, similarityThreshold: number, queryText?: string, kind?: MemoryKind) => Promise<{
15
16
  results: SearchResult[];
16
17
  warnings: string[];
17
18
  }>;
@@ -22,6 +23,7 @@ export interface RerankedSearchOptions {
22
23
  queryText: string;
23
24
  maxMemories: number;
24
25
  similarityThreshold: number;
26
+ kind?: MemoryKind;
25
27
  }
26
28
  export interface RerankedSearchDeps {
27
29
  search: SearchAcrossShards;
@@ -9,7 +9,7 @@ import { rerankDocuments } from "./reranker.js";
9
9
  */
10
10
  export async function rerankedSearch(options, settings, deps) {
11
11
  const { search } = deps;
12
- const { results: candidates, warnings } = await search(options.shards, options.queryVector, options.containerTag, Math.max(options.maxMemories, RERANKER_POOL_SIZE), 0, options.queryText);
12
+ const { results: candidates, warnings } = await search(options.shards, options.queryVector, options.containerTag, Math.max(options.maxMemories, RERANKER_POOL_SIZE), 0, options.queryText, options.kind ?? "normal");
13
13
  // Nothing to reorder: skip the server round trip for an empty search.
14
14
  if (candidates.length === 0)
15
15
  return { results: [], warnings, reranked: false };
@@ -42,6 +42,9 @@ const editable = [
42
42
  "openingContextQuery",
43
43
  "subagentMemory",
44
44
  "compaction.enabled",
45
+ "memorisationRepeat",
46
+ "memorisationMaxSize",
47
+ "memorisationSizeUnit",
45
48
  ...MEMORY_LIMIT_SETTINGS,
46
49
  ];
47
50
  /**
@@ -73,6 +76,9 @@ const globalOnly = [
73
76
  "rerankerTimeoutMs",
74
77
  "skillLearningRedact",
75
78
  "skillFolders",
79
+ "memorisationRepeat",
80
+ "memorisationMaxSize",
81
+ "memorisationSizeUnit",
76
82
  ];
77
83
  function readSettingsFile(path) {
78
84
  if (!path)
@@ -1,6 +1,7 @@
1
1
  import type { TursoDb } from "./turso-db.js";
2
+ import { type MemoryKind } from "../memory-kind.js";
2
3
  /** Exact (non-indexed) nearest-neighbour scan, used when `vector_top_k` fails. */
3
- export declare function exactScanKind(db: TursoDb, queryJson: string, k: number, containerTag: string, columnName: string): Promise<Array<{
4
+ export declare function exactScanKind(db: TursoDb, queryJson: string, k: number, containerTag: string, columnName: string, kind?: MemoryKind): Promise<Array<{
4
5
  id: string;
5
6
  similarity: number;
6
7
  }>>;
@@ -1,18 +1,19 @@
1
1
  import { distanceToSimilarity } from "./vector-utils.js";
2
+ import { memoryKindPredicate } from "../memory-kind.js";
2
3
  /** Exact (non-indexed) nearest-neighbour scan, used when `vector_top_k` fails. */
3
- export async function exactScanKind(db, queryJson, k, containerTag, columnName) {
4
+ export async function exactScanKind(db, queryJson, k, containerTag, columnName, kind = "normal") {
4
5
  const rows = await db.all(containerTag === ""
5
6
  ? `
6
7
  SELECT m.id AS id, vector_distance_cos(m.${columnName}, vector32(?)) AS dist
7
8
  FROM memories m
8
- WHERE m.${columnName} IS NOT NULL
9
+ WHERE m.${columnName} IS NOT NULL AND ${memoryKindPredicate("m.type", kind)}
9
10
  ORDER BY dist ASC
10
11
  LIMIT ?
11
12
  `
12
13
  : `
13
14
  SELECT m.id AS id, vector_distance_cos(m.${columnName}, vector32(?)) AS dist
14
15
  FROM memories m
15
- WHERE m.${columnName} IS NOT NULL AND m.container_tag = ?
16
+ WHERE m.${columnName} IS NOT NULL AND m.container_tag = ? AND ${memoryKindPredicate("m.type", kind)}
16
17
  ORDER BY dist ASC
17
18
  LIMIT ?
18
19
  `, containerTag === "" ? [queryJson, k] : [queryJson, containerTag, k]);
@@ -1,5 +1,6 @@
1
1
  import type { KeywordMode } from "../../utils/keyword-boost.js";
2
2
  import type { TursoDb } from "./turso-db.js";
3
+ import { type MemoryKind } from "../memory-kind.js";
3
4
  /**
4
5
  * FTS5 query for the words of a prompt: lower-case terms from letters and
5
6
  * digits only ("ADR-023" gives `adr` and `023`), each in quotes, joined by OR.
@@ -27,7 +28,7 @@ export interface KeywordHits {
27
28
  * continues with vector and tag results. The log holds a code and an error
28
29
  * name only, never the prompt or memory text.
29
30
  */
30
- export declare function searchKeywords(db: TursoDb, queryText: string | undefined, containerTag: string, limit: number): Promise<KeywordHits | null>;
31
+ export declare function searchKeywords(db: TursoDb, queryText: string | undefined, containerTag: string, limit: number, kind?: MemoryKind): Promise<KeywordHits | null>;
31
32
  /**
32
33
  * `base` with the boost for memory `id`; `base` itself when it has no keyword hit
33
34
  * or the mode is `rrf` (rank fusion keeps each memory's own similarity).
@@ -4,6 +4,7 @@
4
4
  * arguments: do not import CONFIG here, many tests stub src/config.js.
5
5
  */
6
6
  import { log } from "../logger.js";
7
+ import { memoryKindPredicate } from "../memory-kind.js";
7
8
  const MAX_TERMS = 24;
8
9
  // Words that carry no search value. With the OR query below, one of them
9
10
  // would pull most of a shard in as candidates.
@@ -121,7 +122,7 @@ export async function ensureKeywordIndex(db) {
121
122
  * continues with vector and tag results. The log holds a code and an error
122
123
  * name only, never the prompt or memory text.
123
124
  */
124
- export async function searchKeywords(db, queryText, containerTag, limit) {
125
+ export async function searchKeywords(db, queryText, containerTag, limit, kind = "normal") {
125
126
  const match = buildKeywordQuery(queryText);
126
127
  if (!match)
127
128
  return null;
@@ -129,7 +130,7 @@ export async function searchKeywords(db, queryText, containerTag, limit) {
129
130
  await ensureKeywordIndex(db);
130
131
  const rows = await db.all(`SELECT m.id AS id, -bm25(memories_fts) AS score
131
132
  FROM memories_fts JOIN memories m ON m.rowid = memories_fts.rowid
132
- WHERE memories_fts MATCH ?${containerTag === "" ? "" : " AND m.container_tag = ?"}
133
+ WHERE memories_fts MATCH ? AND ${memoryKindPredicate("m.type", kind)}${containerTag === "" ? "" : " AND m.container_tag = ?"}
133
134
  ORDER BY score DESC LIMIT ?`, containerTag === "" ? [match, limit] : [match, containerTag, limit]);
134
135
  const scores = new Map(rows.map((row) => [String(row.id), Number(row.score)]));
135
136
  return { scores, best: Math.max(0, ...scores.values()) };
@@ -1,29 +1,30 @@
1
- import type { MemoryRecord, SearchResult, ShardInfo } from "./types.js";
1
+ import type { SearchResult, ShardInfo } from "./types.js";
2
2
  import type { KeywordMode } from "../../utils/keyword-boost.js";
3
3
  import type { TursoDb } from "./turso-db.js";
4
- import type { Transaction } from "@libsql/client";
4
+ import { type MemoryKind } from "../memory-kind.js";
5
5
  export declare class TursoVectorSearch {
6
- insertVectorInTransaction(tx: Transaction, record: MemoryRecord): Promise<void>;
7
- insertVector(db: TursoDb, record: MemoryRecord): Promise<void>;
6
+ insertVectorInTransaction: (tx: import("@libsql/client").Transaction, record: import("./types.js").MemoryRecord) => Promise<void>;
7
+ insertVector: (db: TursoDb, record: import("./types.js").MemoryRecord) => Promise<void>;
8
8
  searchInShard(shard: ShardInfo, queryVector: Float32Array, containerTag: string, limit: number, queryText?: string, keywordBoost?: number, keywordMode?: KeywordMode,
9
9
  /** Opens the shard database; linked-space searches pass a read-only opener. */
10
- openDb?: (dbPath: string) => Promise<TursoDb>): Promise<SearchResult[]>;
10
+ openDb?: (dbPath: string) => Promise<TursoDb>, kind?: MemoryKind): Promise<SearchResult[]>;
11
11
  private searchKind;
12
12
  searchAcrossShards(shards: ShardInfo[], queryVector: Float32Array, containerTag: string, limit: number, similarityThreshold: number, queryText?: string, keywordBoost?: number, keywordMode?: KeywordMode,
13
13
  /** Opens each shard database; linked-space searches pass a read-only opener. */
14
- openDb?: (dbPath: string) => Promise<TursoDb>): Promise<{
14
+ openDb?: (dbPath: string) => Promise<TursoDb>, kind?: MemoryKind): Promise<{
15
15
  results: SearchResult[];
16
16
  warnings: string[];
17
17
  }>;
18
18
  deleteVector(db: TursoDb, memoryId: string): Promise<void>;
19
19
  updateVector(db: TursoDb, memoryId: string, vector: Float32Array, tagsVector?: Float32Array, tags?: string): Promise<void>;
20
- listMemories(db: TursoDb, containerTag: string, limit: number): Promise<Record<string, unknown>[]>;
21
- getAllMemories(db: TursoDb): Promise<Record<string, unknown>[]>;
20
+ listMemories(db: TursoDb, containerTag: string, limit: number, kind?: MemoryKind): Promise<Record<string, unknown>[]>;
21
+ /** Every row of one kind. Pass "all" only for kind-neutral readers such as export. */
22
+ getAllMemories(db: TursoDb, kind?: MemoryKind | "all"): Promise<Record<string, unknown>[]>;
22
23
  getAllMemoriesWithExtractedVectors(db: TursoDb): Promise<Array<Record<string, unknown> & {
23
24
  vector_json: string | null;
24
25
  }>>;
25
26
  getMemoryById(db: TursoDb, memoryId: string): Promise<Record<string, unknown> | null>;
26
- getMemoriesBySessionID(db: TursoDb, sessionID: string): Promise<Record<string, unknown>[]>;
27
+ getMemoriesBySessionID(db: TursoDb, sessionID: string, kind?: MemoryKind): Promise<Record<string, unknown>[]>;
27
28
  countVectors(db: TursoDb, containerTag: string): Promise<number>;
28
29
  countAllVectors(db: TursoDb): Promise<number>;
29
30
  getDistinctTags(db: TursoDb): Promise<Record<string, unknown>[]>;
@@ -5,6 +5,8 @@ import { tagMatchBoost } from "./tag-match.js";
5
5
  import { exactScanKind } from "./exact-scan.js";
6
6
  import { boostedSimilarity, searchKeywords } from "./keyword-search.js";
7
7
  import { fuseShardResults } from "./rank-fusion.js";
8
+ import { tursoVectorWrites } from "./vector-writes.js";
9
+ import { memoryKindPredicate } from "../memory-kind.js";
8
10
  function parseMetadata(value) {
9
11
  if (!value || typeof value !== "string")
10
12
  return undefined;
@@ -16,99 +18,25 @@ function parseMetadata(value) {
16
18
  }
17
19
  }
18
20
  export class TursoVectorSearch {
19
- async insertVectorInTransaction(tx, record) {
20
- const contentVector = vectorToJson(record.vector);
21
- const commonArgs = [
22
- record.containerTag,
23
- record.tags || null,
24
- record.type || null,
25
- record.createdAt,
26
- record.updatedAt,
27
- record.metadata || null,
28
- record.displayName || null,
29
- record.userName || null,
30
- record.userEmail || null,
31
- record.projectPath || null,
32
- record.projectName || null,
33
- record.gitRepoUrl || null,
34
- ];
35
- if (record.tagsVector) {
36
- await tx.execute({
37
- sql: `
38
- INSERT INTO memories (
39
- id, content, vector, tags_vector, container_tag, tags, type, created_at, updated_at,
40
- metadata, display_name, user_name, user_email, project_path, project_name, git_repo_url
41
- ) VALUES (?, ?, vector32(?), vector32(?), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
42
- `,
43
- args: [
44
- record.id,
45
- record.content,
46
- contentVector,
47
- vectorToJson(record.tagsVector),
48
- ...commonArgs,
49
- ],
50
- });
51
- return;
52
- }
53
- await tx.execute({
54
- sql: `
55
- INSERT INTO memories (
56
- id, content, vector, tags_vector, container_tag, tags, type, created_at, updated_at,
57
- metadata, display_name, user_name, user_email, project_path, project_name, git_repo_url
58
- ) VALUES (?, ?, vector32(?), NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
59
- `,
60
- args: [record.id, record.content, contentVector, ...commonArgs],
61
- });
62
- }
63
- async insertVector(db, record) {
64
- const contentVector = vectorToJson(record.vector);
65
- const commonArgs = [
66
- record.containerTag,
67
- record.tags || null,
68
- record.type || null,
69
- record.createdAt,
70
- record.updatedAt,
71
- record.metadata || null,
72
- record.displayName || null,
73
- record.userName || null,
74
- record.userEmail || null,
75
- record.projectPath || null,
76
- record.projectName || null,
77
- record.gitRepoUrl || null,
78
- ];
79
- if (record.tagsVector) {
80
- await db.execute(`
81
- INSERT INTO memories (
82
- id, content, vector, tags_vector, container_tag, tags, type, created_at, updated_at,
83
- metadata, display_name, user_name, user_email, project_path, project_name, git_repo_url
84
- ) VALUES (?, ?, vector32(?), vector32(?), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
85
- `, [record.id, record.content, contentVector, vectorToJson(record.tagsVector), ...commonArgs]);
86
- return;
87
- }
88
- await db.execute(`
89
- INSERT INTO memories (
90
- id, content, vector, tags_vector, container_tag, tags, type, created_at, updated_at,
91
- metadata, display_name, user_name, user_email, project_path, project_name, git_repo_url
92
- ) VALUES (?, ?, vector32(?), NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
93
- `, [record.id, record.content, contentVector, ...commonArgs]);
94
- }
21
+ insertVectorInTransaction = tursoVectorWrites.insertVectorInTransaction.bind(tursoVectorWrites);
22
+ insertVector = tursoVectorWrites.insertVector.bind(tursoVectorWrites);
95
23
  async searchInShard(shard, queryVector, containerTag, limit, queryText, keywordBoost = 0, keywordMode = "boost",
96
24
  /** Opens the shard database; linked-space searches pass a read-only opener. */
97
- openDb = (dbPath) => tursoConnectionManager.getConnection(dbPath)) {
25
+ openDb = (dbPath) => tursoConnectionManager.getConnection(dbPath), kind = "normal") {
98
26
  const db = await openDb(shard.dbPath);
99
27
  const queryJson = vectorToJson(queryVector);
100
28
  // Over-fetch aggressively when filtering by container_tag after ANN,
101
29
  // because post-filtering can discard many DiskANN neighbors.
102
30
  const k = containerTag === "" ? Math.max(limit * 4, 32) : Math.max(limit * 16, 128);
103
- const contentResults = await this.searchKind(db, queryJson, k, containerTag, "memories_vec_idx", "vector");
104
- const tagsResults = await this.searchKind(db, queryJson, k, containerTag, "memories_tags_vec_idx", "tags_vector");
31
+ const contentResults = await this.searchKind(db, queryJson, k, containerTag, "memories_vec_idx", "vector", kind, limit);
32
+ const tagsResults = await this.searchKind(db, queryJson, k, containerTag, "memories_tags_vec_idx", "tags_vector", kind, limit);
105
33
  const candidateIds = new Set();
106
34
  for (const result of contentResults)
107
35
  candidateIds.add(result.id);
108
36
  for (const result of tagsResults)
109
37
  candidateIds.add(result.id);
110
38
  const kwHits = keywordBoost > 0
111
- ? await searchKeywords(db, queryText, containerTag, Math.max(limit * 4, 32))
39
+ ? await searchKeywords(db, queryText, containerTag, Math.max(limit * 4, 32), kind)
112
40
  : null;
113
41
  for (const id of kwHits?.scores.keys() ?? [])
114
42
  candidateIds.add(id);
@@ -128,7 +56,7 @@ export class TursoVectorSearch {
128
56
  THEN vector_distance_cos(tags_vector, vector32(?))
129
57
  ELSE NULL END AS tags_dist
130
58
  FROM memories
131
- WHERE id IN (${placeholders})
59
+ WHERE id IN (${placeholders}) AND ${memoryKindPredicate("type", kind)}
132
60
  `
133
61
  : `
134
62
  SELECT id, content, tags, type, created_at, metadata, container_tag,
@@ -139,7 +67,7 @@ export class TursoVectorSearch {
139
67
  THEN vector_distance_cos(tags_vector, vector32(?))
140
68
  ELSE NULL END AS tags_dist
141
69
  FROM memories
142
- WHERE id IN (${placeholders}) AND container_tag = ?
70
+ WHERE id IN (${placeholders}) AND container_tag = ? AND ${memoryKindPredicate("type", kind)}
143
71
  `, containerTag === ""
144
72
  ? [queryJson, queryJson, ...ids]
145
73
  : [queryJson, queryJson, ...ids, containerTag]);
@@ -151,7 +79,9 @@ export class TursoVectorSearch {
151
79
  const memoryTagsStr = String(row.tags || "");
152
80
  const exactMatchBoost = tagMatchBoost(queryText, memoryTagsStr);
153
81
  const finalTagsSim = Math.max(tagsSim, exactMatchBoost);
154
- const base = contentSim * 0.6 + finalTagsSim * 0.4;
82
+ // Every memorisation carries the same tag, so the tags half is near constant and
83
+ // pulls a close match down. Score memorisations by content alone.
84
+ const base = kind === "memorisation" ? contentSim : contentSim * 0.6 + finalTagsSim * 0.4;
155
85
  const similarity = boostedSimilarity(base, kwHits, String(row.id), keywordBoost, keywordMode);
156
86
  return {
157
87
  id: String(row.id),
@@ -177,21 +107,32 @@ export class TursoVectorSearch {
177
107
  const fusing = keywordMode === "rrf" && keywordBoost > 0;
178
108
  return fusing ? hydratedResults : hydratedResults.slice(0, Math.max(0, limit));
179
109
  }
180
- async searchKind(db, queryJson, k, containerTag, indexName, columnName) {
110
+ async searchKind(db, queryJson, k, containerTag, indexName, columnName, kind = "normal", wanted = 0) {
111
+ // The ANN index covers every row, and the kind filter runs after it. Normal rows
112
+ // can fill all k candidates and hide the few memorisations. Memorisations are few,
113
+ // so scan them exactly; the exact scan filters by kind in SQL.
114
+ if (kind === "memorisation") {
115
+ return exactScanKind(db, queryJson, k, containerTag, columnName, kind);
116
+ }
181
117
  try {
182
118
  const rows = await db.all(containerTag === ""
183
119
  ? `
184
120
  SELECT m.id AS id, vector_distance_cos(m.${columnName}, vector32(?)) AS dist
185
121
  FROM vector_top_k('${indexName}', vector32(?), ?) AS v
186
122
  CROSS JOIN memories m ON m.rowid = v.id
187
- WHERE m.${columnName} IS NOT NULL
123
+ WHERE m.${columnName} IS NOT NULL AND ${memoryKindPredicate("m.type", kind)}
188
124
  `
189
125
  : `
190
126
  SELECT m.id AS id, vector_distance_cos(m.${columnName}, vector32(?)) AS dist
191
127
  FROM vector_top_k('${indexName}', vector32(?), ?) AS v
192
128
  CROSS JOIN memories m ON m.rowid = v.id
193
- WHERE m.${columnName} IS NOT NULL AND m.container_tag = ?
129
+ WHERE m.${columnName} IS NOT NULL AND m.container_tag = ? AND ${memoryKindPredicate("m.type", kind)}
194
130
  `, containerTag === "" ? [queryJson, queryJson, k] : [queryJson, queryJson, k, containerTag]);
131
+ // Memorisations or other projects can fill the k candidates and the filters then
132
+ // drop them all. A short result is rare, so only then pay for the exact scan.
133
+ if (rows.length < wanted) {
134
+ return exactScanKind(db, queryJson, k, containerTag, columnName, kind);
135
+ }
195
136
  return rows.map((row) => ({
196
137
  id: String(row.id),
197
138
  similarity: distanceToSimilarity(Number(row.dist)),
@@ -202,16 +143,16 @@ export class TursoVectorSearch {
202
143
  indexName,
203
144
  error: String(error),
204
145
  }, "warn");
205
- return exactScanKind(db, queryJson, k, containerTag, columnName);
146
+ return exactScanKind(db, queryJson, k, containerTag, columnName, kind);
206
147
  }
207
148
  }
208
149
  async searchAcrossShards(shards, queryVector, containerTag, limit, similarityThreshold, queryText, keywordBoost = 0, keywordMode = "boost",
209
150
  /** Opens each shard database; linked-space searches pass a read-only opener. */
210
- openDb) {
151
+ openDb, kind = "normal") {
211
152
  const shardErrors = [];
212
153
  const shardPromises = shards.map(async (shard) => {
213
154
  try {
214
- return await this.searchInShard(shard, queryVector, containerTag, limit, queryText, keywordBoost, keywordMode, openDb);
155
+ return await this.searchInShard(shard, queryVector, containerTag, limit, queryText, keywordBoost, keywordMode, openDb, kind);
215
156
  }
216
157
  catch (error) {
217
158
  const message = error instanceof Error ? error.message : String(error);
@@ -264,22 +205,24 @@ export class TursoVectorSearch {
264
205
  await db.execute(`UPDATE memories SET vector = vector32(?), tags_vector = NULL WHERE id = ?`, [contentVector, memoryId]);
265
206
  }
266
207
  }
267
- async listMemories(db, containerTag, limit) {
208
+ async listMemories(db, containerTag, limit, kind = "normal") {
268
209
  return containerTag === ""
269
210
  ? db.all(`
270
211
  SELECT * FROM memories
212
+ WHERE ${memoryKindPredicate("type", kind)}
271
213
  ORDER BY created_at DESC
272
214
  LIMIT ?
273
215
  `, [limit])
274
216
  : db.all(`
275
217
  SELECT * FROM memories
276
- WHERE container_tag = ?
218
+ WHERE container_tag = ? AND ${memoryKindPredicate("type", kind)}
277
219
  ORDER BY created_at DESC
278
220
  LIMIT ?
279
221
  `, [containerTag, limit]);
280
222
  }
281
- async getAllMemories(db) {
282
- return db.all(`SELECT * FROM memories ORDER BY created_at DESC`);
223
+ /** Every row of one kind. Pass "all" only for kind-neutral readers such as export. */
224
+ async getAllMemories(db, kind = "normal") {
225
+ return db.all(`SELECT * FROM memories WHERE ${kind === "all" ? "1 = 1" : memoryKindPredicate("type", kind)} ORDER BY created_at DESC`);
283
226
  }
284
227
  async getAllMemoriesWithExtractedVectors(db) {
285
228
  return db.all(`
@@ -296,10 +239,10 @@ export class TursoVectorSearch {
296
239
  async getMemoryById(db, memoryId) {
297
240
  return db.get(`SELECT * FROM memories WHERE id = ?`, [memoryId]);
298
241
  }
299
- async getMemoriesBySessionID(db, sessionID) {
242
+ async getMemoriesBySessionID(db, sessionID, kind = "normal") {
300
243
  const rows = await db.all(`
301
244
  SELECT * FROM memories
302
- WHERE metadata LIKE ?
245
+ WHERE metadata LIKE ? AND ${memoryKindPredicate("type", kind)}
303
246
  ORDER BY created_at DESC
304
247
  `, [`%"sessionID":"${sessionID}"%`]);
305
248
  return rows.map((row) => ({
@@ -0,0 +1,8 @@
1
+ import type { MemoryRecord } from "./types.js";
2
+ import type { TursoDb } from "./turso-db.js";
3
+ import type { Transaction } from "@libsql/client";
4
+ export declare class TursoVectorWrites {
5
+ insertVectorInTransaction(tx: Transaction, record: MemoryRecord): Promise<void>;
6
+ insertVector(db: TursoDb, record: MemoryRecord): Promise<void>;
7
+ }
8
+ export declare const tursoVectorWrites: TursoVectorWrites;
@@ -0,0 +1,80 @@
1
+ import { vectorToJson } from "./vector-utils.js";
2
+ export class TursoVectorWrites {
3
+ async insertVectorInTransaction(tx, record) {
4
+ const contentVector = vectorToJson(record.vector);
5
+ const commonArgs = [
6
+ record.containerTag,
7
+ record.tags || null,
8
+ record.type || null,
9
+ record.createdAt,
10
+ record.updatedAt,
11
+ record.metadata || null,
12
+ record.displayName || null,
13
+ record.userName || null,
14
+ record.userEmail || null,
15
+ record.projectPath || null,
16
+ record.projectName || null,
17
+ record.gitRepoUrl || null,
18
+ ];
19
+ if (record.tagsVector) {
20
+ await tx.execute({
21
+ sql: `
22
+ INSERT INTO memories (
23
+ id, content, vector, tags_vector, container_tag, tags, type, created_at, updated_at,
24
+ metadata, display_name, user_name, user_email, project_path, project_name, git_repo_url
25
+ ) VALUES (?, ?, vector32(?), vector32(?), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
26
+ `,
27
+ args: [
28
+ record.id,
29
+ record.content,
30
+ contentVector,
31
+ vectorToJson(record.tagsVector),
32
+ ...commonArgs,
33
+ ],
34
+ });
35
+ return;
36
+ }
37
+ await tx.execute({
38
+ sql: `
39
+ INSERT INTO memories (
40
+ id, content, vector, tags_vector, container_tag, tags, type, created_at, updated_at,
41
+ metadata, display_name, user_name, user_email, project_path, project_name, git_repo_url
42
+ ) VALUES (?, ?, vector32(?), NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
43
+ `,
44
+ args: [record.id, record.content, contentVector, ...commonArgs],
45
+ });
46
+ }
47
+ async insertVector(db, record) {
48
+ const contentVector = vectorToJson(record.vector);
49
+ const commonArgs = [
50
+ record.containerTag,
51
+ record.tags || null,
52
+ record.type || null,
53
+ record.createdAt,
54
+ record.updatedAt,
55
+ record.metadata || null,
56
+ record.displayName || null,
57
+ record.userName || null,
58
+ record.userEmail || null,
59
+ record.projectPath || null,
60
+ record.projectName || null,
61
+ record.gitRepoUrl || null,
62
+ ];
63
+ if (record.tagsVector) {
64
+ await db.execute(`
65
+ INSERT INTO memories (
66
+ id, content, vector, tags_vector, container_tag, tags, type, created_at, updated_at,
67
+ metadata, display_name, user_name, user_email, project_path, project_name, git_repo_url
68
+ ) VALUES (?, ?, vector32(?), vector32(?), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
69
+ `, [record.id, record.content, contentVector, vectorToJson(record.tagsVector), ...commonArgs]);
70
+ return;
71
+ }
72
+ await db.execute(`
73
+ INSERT INTO memories (
74
+ id, content, vector, tags_vector, container_tag, tags, type, created_at, updated_at,
75
+ metadata, display_name, user_name, user_email, project_path, project_name, git_repo_url
76
+ ) VALUES (?, ?, vector32(?), NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
77
+ `, [record.id, record.content, contentVector, ...commonArgs]);
78
+ }
79
+ }
80
+ export const tursoVectorWrites = new TursoVectorWrites();
@@ -79,6 +79,7 @@ export declare function replaceRetrying(path: string, text: string, deps?: {
79
79
  mkdir?: (dir: string) => void;
80
80
  write?: (path: string, text: string) => void;
81
81
  rename?: (from: string, to: string) => void;
82
+ remove?: (path: string) => void;
82
83
  rewrites?: number;
83
84
  }, rewrites?: number): void;
84
85
  /** `renameSync` that retries while Windows reports the target busy. */
@@ -95,9 +95,12 @@ export function replaceRetrying(path, text, deps = {}, rewrites = deps.rewrites
95
95
  const mkdir = deps.mkdir ?? ((dir) => mkdirSync(dir, { recursive: true, mode: 0o700 }));
96
96
  const write = deps.write ?? ((file, body) => writeFileSync(file, body, { mode: 0o600 }));
97
97
  const rename = deps.rename ?? ((from, to) => renameRetrying(from, to));
98
+ const remove = deps.remove ?? ((file) => rmSync(file, { force: true }));
98
99
  mkdir(dirname(path));
99
- const temp = `${path}.${process.pid}.tmp`;
100
100
  for (let attempt = 0;; attempt++) {
101
+ // A new name for each attempt: the Bun rename fault can stick to one
102
+ // temp path, so a retry with the same name fails again (TDR-030).
103
+ const temp = `${path}.${process.pid}.${attempt}.tmp`;
101
104
  write(temp, text);
102
105
  try {
103
106
  rename(temp, path);
@@ -106,6 +109,7 @@ export function replaceRetrying(path, text, deps = {}, rewrites = deps.rewrites
106
109
  catch (error) {
107
110
  if (error.code !== "ENOENT" || attempt >= rewrites)
108
111
  throw error;
112
+ remove(temp);
109
113
  }
110
114
  }
111
115
  }
@@ -0,0 +1,12 @@
1
+ /** Why the Memories page cannot edit a memorisation. */
2
+ export declare const MEMORISATION_EDIT_REFUSAL = "A memorisation cannot be edited or bulk deleted from the Memories page. Delete it on the Memorisation page, or replace it by memorising again.";
3
+ /** Why the Memories page cannot create a memorisation. */
4
+ export declare const MEMORISATION_TYPE_REFUSAL = "The memorisation type is reserved. Save a memorisation with the memorise operation, not from the Memories page.";
5
+ /** Reads stored normal memory rows for one project tag, or for every project and user shard. */
6
+ export declare function loadMemoryRows(tag?: string): Promise<any[]>;
7
+ /** The IDs among `ids` whose stored row is a memorisation, across every project and user shard. */
8
+ export declare function memorisationIdsAmong(ids: string[]): Promise<string[]>;
9
+ /** The refusal for a web edit of memory `id` with `data`, or undefined when the edit is allowed. */
10
+ export declare function webUpdateRefusal(id: string, data: {
11
+ type?: string;
12
+ }): Promise<string | undefined>;
@@ -0,0 +1,58 @@
1
+ import { tursoShardManager } from "./turso/shard-manager.js";
2
+ import { tursoVectorSearch } from "./turso/vector-search.js";
3
+ import { tursoConnectionManager } from "./turso/connection-manager.js";
4
+ import { ensureTursoReady } from "./turso/ready.js";
5
+ import { extractScopeFromContainerTag } from "./memory-scope.js";
6
+ /** Why the Memories page cannot edit a memorisation. */
7
+ export const MEMORISATION_EDIT_REFUSAL = "A memorisation cannot be edited or bulk deleted from the Memories page. Delete it on the Memorisation page, or replace it by memorising again.";
8
+ /** Why the Memories page cannot create a memorisation. */
9
+ export const MEMORISATION_TYPE_REFUSAL = "The memorisation type is reserved. Save a memorisation with the memorise operation, not from the Memories page.";
10
+ /** Reads stored normal memory rows for one project tag, or for every project and user shard. */
11
+ export async function loadMemoryRows(tag) {
12
+ const allMemories = [];
13
+ if (tag) {
14
+ const { scope: tagScope, hash } = extractScopeFromContainerTag(tag);
15
+ const shards = await tursoShardManager.getAllShards(tagScope, hash);
16
+ for (const shard of shards) {
17
+ const db = await tursoConnectionManager.getConnection(shard.dbPath);
18
+ const memories = await tursoVectorSearch.listMemories(db, tag, 10000);
19
+ allMemories.push(...memories);
20
+ }
21
+ }
22
+ else {
23
+ // Walk both project and user shards, so user-scope memories are listed too.
24
+ // The container_tag check stays as defence in depth. Memorisations are not
25
+ // listed here: getAllMemories returns normal memories unless told otherwise.
26
+ const projectShards = await tursoShardManager.getAllShards("project", "");
27
+ const userShards = await tursoShardManager.getAllShards("user", "");
28
+ for (const shard of [...projectShards, ...userShards]) {
29
+ const db = await tursoConnectionManager.getConnection(shard.dbPath);
30
+ const memories = await tursoVectorSearch.getAllMemories(db);
31
+ allMemories.push(...memories.filter((m) => m.container_tag?.includes("_project_") || m.container_tag?.includes("_user_")));
32
+ }
33
+ }
34
+ return allMemories;
35
+ }
36
+ /** The IDs among `ids` whose stored row is a memorisation, across every project and user shard. */
37
+ export async function memorisationIdsAmong(ids) {
38
+ if (ids.length === 0)
39
+ return [];
40
+ await ensureTursoReady();
41
+ const shards = [
42
+ ...(await tursoShardManager.getAllShards("project", "")),
43
+ ...(await tursoShardManager.getAllShards("user", "")),
44
+ ];
45
+ const found = [];
46
+ for (const shard of shards) {
47
+ const db = await tursoConnectionManager.getConnection(shard.dbPath);
48
+ const rows = await db.all(`SELECT id FROM memories WHERE type = 'memorisation' AND id IN (${ids.map(() => "?").join(",")})`, ids);
49
+ found.push(...rows.map((row) => String(row.id)));
50
+ }
51
+ return found;
52
+ }
53
+ /** The refusal for a web edit of memory `id` with `data`, or undefined when the edit is allowed. */
54
+ export async function webUpdateRefusal(id, data) {
55
+ if (data.type === "memorisation")
56
+ return MEMORISATION_TYPE_REFUSAL;
57
+ return (await memorisationIdsAmong([id])).length > 0 ? MEMORISATION_EDIT_REFUSAL : undefined;
58
+ }
@@ -1,4 +1,6 @@
1
1
  import { routeChat } from "./chat/chat-routes.js";
2
+ import { linkedSettingsFrom } from "./linked-search.js";
3
+ import { routeMemorisations } from "./memorisation-routes.js";
2
4
  import { routeMemoryHosts } from "./memory-host-routes.js";
3
5
  import { routeProfileCleanup } from "./profile-cleanup-routes.js";
4
6
  import { routeSpaceTransfers } from "./space-transfer-routes.js";
@@ -31,5 +33,8 @@ export async function routeModules(path, method, url, req, ctx) {
31
33
  });
32
34
  if (spaces !== undefined)
33
35
  return spaces;
36
+ const memorisations = await routeMemorisations(path, method, url, ctx.cwd, { defaultScope: config.memory.defaultScope, linked: linkedSettingsFrom(config) }, readBody);
37
+ if (memorisations !== undefined)
38
+ return memorisations;
34
39
  return routeChat(path, method, readBody, config);
35
40
  }