@pcircle/memesh 4.5.1 → 4.6.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 (202) hide show
  1. package/.claude-plugin/marketplace.json +5 -3
  2. package/.claude-plugin/plugin.json +6 -4
  3. package/AGENTS.md +116 -0
  4. package/README.de.md +141 -48
  5. package/README.md +173 -48
  6. package/README.zh-TW.md +142 -48
  7. package/dashboard/dist/index.html +15 -14
  8. package/dist/cli/view-live.js +3 -3
  9. package/dist/core/analytics.d.ts +9 -0
  10. package/dist/core/analytics.d.ts.map +1 -1
  11. package/dist/core/analytics.js +36 -18
  12. package/dist/core/analytics.js.map +1 -1
  13. package/dist/core/auto-tagger.d.ts.map +1 -1
  14. package/dist/core/auto-tagger.js +4 -9
  15. package/dist/core/auto-tagger.js.map +1 -1
  16. package/dist/core/briefing.d.ts +8 -0
  17. package/dist/core/briefing.d.ts.map +1 -0
  18. package/dist/core/briefing.js +92 -0
  19. package/dist/core/briefing.js.map +1 -0
  20. package/dist/core/capture-flag.d.ts +5 -0
  21. package/dist/core/capture-flag.d.ts.map +1 -0
  22. package/dist/core/capture-flag.js +10 -0
  23. package/dist/core/capture-flag.js.map +1 -0
  24. package/dist/core/config.d.ts +0 -1
  25. package/dist/core/config.d.ts.map +1 -1
  26. package/dist/core/config.js.map +1 -1
  27. package/dist/core/conflict-candidates.d.ts +20 -0
  28. package/dist/core/conflict-candidates.d.ts.map +1 -0
  29. package/dist/core/conflict-candidates.js +79 -0
  30. package/dist/core/conflict-candidates.js.map +1 -0
  31. package/dist/core/conflict-judge.d.ts +47 -0
  32. package/dist/core/conflict-judge.d.ts.map +1 -0
  33. package/dist/core/conflict-judge.js +189 -0
  34. package/dist/core/conflict-judge.js.map +1 -0
  35. package/dist/core/demo.d.ts.map +1 -1
  36. package/dist/core/demo.js +1 -1
  37. package/dist/core/demo.js.map +1 -1
  38. package/dist/core/digest-validator.d.ts.map +1 -1
  39. package/dist/core/digest-validator.js +3 -5
  40. package/dist/core/digest-validator.js.map +1 -1
  41. package/dist/core/doctor.d.ts +2 -0
  42. package/dist/core/doctor.d.ts.map +1 -1
  43. package/dist/core/doctor.js +59 -62
  44. package/dist/core/doctor.js.map +1 -1
  45. package/dist/core/dreamer.d.ts +5 -2
  46. package/dist/core/dreamer.d.ts.map +1 -1
  47. package/dist/core/dreamer.js +329 -25
  48. package/dist/core/dreamer.js.map +1 -1
  49. package/dist/core/embedder.d.ts +8 -4
  50. package/dist/core/embedder.d.ts.map +1 -1
  51. package/dist/core/embedder.js +82 -24
  52. package/dist/core/embedder.js.map +1 -1
  53. package/dist/core/failure-analyzer.d.ts.map +1 -1
  54. package/dist/core/failure-analyzer.js +7 -12
  55. package/dist/core/failure-analyzer.js.map +1 -1
  56. package/dist/core/graph.d.ts +12 -0
  57. package/dist/core/graph.d.ts.map +1 -1
  58. package/dist/core/graph.js +56 -1
  59. package/dist/core/graph.js.map +1 -1
  60. package/dist/core/guards.d.ts +20 -0
  61. package/dist/core/guards.d.ts.map +1 -0
  62. package/dist/core/guards.js +103 -0
  63. package/dist/core/guards.js.map +1 -0
  64. package/dist/core/install-channel.d.ts +1 -1
  65. package/dist/core/install-channel.d.ts.map +1 -1
  66. package/dist/core/install-channel.js +16 -5
  67. package/dist/core/install-channel.js.map +1 -1
  68. package/dist/core/install-hooks.d.ts +5 -0
  69. package/dist/core/install-hooks.d.ts.map +1 -1
  70. package/dist/core/install-hooks.js +0 -0
  71. package/dist/core/install-hooks.js.map +1 -1
  72. package/dist/core/json-utils.d.ts +1 -0
  73. package/dist/core/json-utils.d.ts.map +1 -1
  74. package/dist/core/json-utils.js +19 -10
  75. package/dist/core/json-utils.js.map +1 -1
  76. package/dist/core/kg-backfill.d.ts +5 -2
  77. package/dist/core/kg-backfill.d.ts.map +1 -1
  78. package/dist/core/kg-backfill.js +155 -5
  79. package/dist/core/kg-backfill.js.map +1 -1
  80. package/dist/core/lifecycle.d.ts.map +1 -1
  81. package/dist/core/lifecycle.js +14 -21
  82. package/dist/core/lifecycle.js.map +1 -1
  83. package/dist/core/memory-tool.d.ts.map +1 -1
  84. package/dist/core/memory-tool.js +4 -4
  85. package/dist/core/memory-tool.js.map +1 -1
  86. package/dist/core/operations.d.ts +13 -2
  87. package/dist/core/operations.d.ts.map +1 -1
  88. package/dist/core/operations.js +115 -28
  89. package/dist/core/operations.js.map +1 -1
  90. package/dist/core/prompt-safety.d.ts +1 -0
  91. package/dist/core/prompt-safety.d.ts.map +1 -1
  92. package/dist/core/prompt-safety.js +7 -0
  93. package/dist/core/prompt-safety.js.map +1 -1
  94. package/dist/core/schema-export.d.ts.map +1 -1
  95. package/dist/core/schema-export.js +31 -0
  96. package/dist/core/schema-export.js.map +1 -1
  97. package/dist/core/serializer.d.ts.map +1 -1
  98. package/dist/core/serializer.js +8 -0
  99. package/dist/core/serializer.js.map +1 -1
  100. package/dist/core/setup.d.ts +29 -0
  101. package/dist/core/setup.d.ts.map +1 -0
  102. package/dist/core/setup.js +127 -0
  103. package/dist/core/setup.js.map +1 -0
  104. package/dist/core/task-state-store.d.ts +17 -0
  105. package/dist/core/task-state-store.d.ts.map +1 -0
  106. package/dist/core/task-state-store.js +45 -0
  107. package/dist/core/task-state-store.js.map +1 -0
  108. package/dist/core/task-state.d.ts +19 -0
  109. package/dist/core/task-state.d.ts.map +1 -0
  110. package/dist/core/task-state.js +91 -0
  111. package/dist/core/task-state.js.map +1 -0
  112. package/dist/core/time-utils.d.ts +2 -0
  113. package/dist/core/time-utils.d.ts.map +1 -0
  114. package/dist/core/time-utils.js +14 -0
  115. package/dist/core/time-utils.js.map +1 -0
  116. package/dist/core/title.d.ts +5 -0
  117. package/dist/core/title.d.ts.map +1 -0
  118. package/dist/core/title.js +14 -0
  119. package/dist/core/title.js.map +1 -0
  120. package/dist/core/transcript-source.d.ts.map +1 -1
  121. package/dist/core/transcript-source.js +2 -3
  122. package/dist/core/transcript-source.js.map +1 -1
  123. package/dist/core/types.d.ts +5 -0
  124. package/dist/core/types.d.ts.map +1 -1
  125. package/dist/core/why.d.ts +54 -0
  126. package/dist/core/why.d.ts.map +1 -0
  127. package/dist/core/why.js +168 -0
  128. package/dist/core/why.js.map +1 -0
  129. package/dist/core/work-topology.d.ts +36 -0
  130. package/dist/core/work-topology.d.ts.map +1 -0
  131. package/dist/core/work-topology.js +192 -0
  132. package/dist/core/work-topology.js.map +1 -0
  133. package/dist/db.d.ts +33 -11
  134. package/dist/db.d.ts.map +1 -1
  135. package/dist/db.js +307 -315
  136. package/dist/db.js.map +1 -1
  137. package/dist/knowledge-graph.d.ts +1 -0
  138. package/dist/knowledge-graph.d.ts.map +1 -1
  139. package/dist/knowledge-graph.js +50 -40
  140. package/dist/knowledge-graph.js.map +1 -1
  141. package/dist/skills-manifest.json +62 -22
  142. package/dist/storage/conflicts.d.ts.map +1 -1
  143. package/dist/storage/conflicts.js +2 -7
  144. package/dist/storage/conflicts.js.map +1 -1
  145. package/dist/storage/fts-index.d.ts +4 -2
  146. package/dist/storage/fts-index.d.ts.map +1 -1
  147. package/dist/storage/fts-index.js +16 -4
  148. package/dist/storage/fts-index.js.map +1 -1
  149. package/dist/storage/schema.d.ts +20 -0
  150. package/dist/storage/schema.d.ts.map +1 -0
  151. package/dist/storage/schema.js +274 -0
  152. package/dist/storage/schema.js.map +1 -0
  153. package/dist/storage/sqlite.d.ts.map +1 -1
  154. package/dist/storage/sqlite.js +1 -1
  155. package/dist/storage/sqlite.js.map +1 -1
  156. package/dist/transports/cli/cli.d.ts +1 -4
  157. package/dist/transports/cli/cli.d.ts.map +1 -1
  158. package/dist/transports/cli/cli.js +579 -66
  159. package/dist/transports/cli/cli.js.map +1 -1
  160. package/dist/transports/http/server.d.ts.map +1 -1
  161. package/dist/transports/http/server.js +242 -303
  162. package/dist/transports/http/server.js.map +1 -1
  163. package/dist/transports/mcp/handlers.d.ts +46 -0
  164. package/dist/transports/mcp/handlers.d.ts.map +1 -1
  165. package/dist/transports/mcp/handlers.js +59 -4
  166. package/dist/transports/mcp/handlers.js.map +1 -1
  167. package/dist/transports/schemas.d.ts +29 -10
  168. package/dist/transports/schemas.d.ts.map +1 -1
  169. package/dist/transports/schemas.js +33 -8
  170. package/dist/transports/schemas.js.map +1 -1
  171. package/hooks/hooks.json +10 -0
  172. package/llms-install.md +138 -0
  173. package/package.json +14 -9
  174. package/scripts/hooks/_generated/capture-flag.js +17 -0
  175. package/scripts/hooks/_generated/fts-index.js +16 -4
  176. package/scripts/hooks/_generated/guards.js +110 -0
  177. package/scripts/hooks/_generated/schema.js +281 -0
  178. package/scripts/hooks/_generated/sqlite.js +1 -1
  179. package/scripts/hooks/_generated/task-state.js +98 -0
  180. package/scripts/hooks/_generated/time-utils.js +21 -0
  181. package/scripts/hooks/_generated/title.js +21 -0
  182. package/scripts/hooks/_generated/work-topology.js +199 -0
  183. package/scripts/hooks/_shared.js +197 -480
  184. package/scripts/hooks/guard-check.js +76 -0
  185. package/scripts/hooks/post-commit.js +31 -1
  186. package/scripts/hooks/pre-compact.js +13 -1
  187. package/scripts/hooks/pre-edit-recall.js +158 -120
  188. package/scripts/hooks/session-start.js +169 -82
  189. package/scripts/hooks/session-summary.js +78 -90
  190. package/skills/memesh/SKILL.md +108 -76
  191. package/README.es.md +0 -467
  192. package/README.fr.md +0 -459
  193. package/README.ja.md +0 -467
  194. package/README.ko.md +0 -467
  195. package/README.pt.md +0 -459
  196. package/README.th.md +0 -460
  197. package/README.vi.md +0 -459
  198. package/README.zh-CN.md +0 -466
  199. package/dist/cli/view.d.ts +0 -3
  200. package/dist/cli/view.d.ts.map +0 -1
  201. package/dist/cli/view.js +0 -523
  202. package/dist/cli/view.js.map +0 -1
package/dist/db.js CHANGED
@@ -6,176 +6,12 @@ import { runAutoDecay } from './core/lifecycle.js';
6
6
  import { resolveEmbeddingDimension } from './core/config.js';
7
7
  import { computeSignalScore } from './core/signal-scorer.js';
8
8
  import { getDbPath } from './core/paths.js';
9
- import { insertFtsRow } from './storage/fts-index.js';
9
+ import { insertFtsRow, joinIndexedObservations, removeFromFts } from './storage/fts-index.js';
10
+ import { SCHEMA_SQL, FTS_SQL, safeAlter, migrateEntitiesSchema, ensureTagsUniqueIndex, ensureHookRunsSince, ensureFtsSegmentation, rebuildFtsIndex, runOnceMigration, FTS_SEGMENTATION_VERSION, } from './storage/schema.js';
11
+ export { runOnceMigration, FTS_SEGMENTATION_VERSION };
12
+ import { truncateTitle, isBoilerplateObservation } from './core/title.js';
10
13
  let db = null;
11
- const SCHEMA_SQL = `
12
- CREATE TABLE IF NOT EXISTS entities (
13
- id INTEGER PRIMARY KEY AUTOINCREMENT,
14
- name TEXT NOT NULL UNIQUE,
15
- type TEXT NOT NULL,
16
- created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
17
- metadata JSON
18
- );
19
-
20
- CREATE TABLE IF NOT EXISTS observations (
21
- id INTEGER PRIMARY KEY AUTOINCREMENT,
22
- entity_id INTEGER NOT NULL,
23
- content TEXT NOT NULL,
24
- created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
25
- FOREIGN KEY (entity_id) REFERENCES entities(id) ON DELETE CASCADE
26
- );
27
-
28
- CREATE TABLE IF NOT EXISTS relations (
29
- id INTEGER PRIMARY KEY AUTOINCREMENT,
30
- from_entity_id INTEGER NOT NULL,
31
- to_entity_id INTEGER NOT NULL,
32
- relation_type TEXT NOT NULL,
33
- metadata JSON,
34
- created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
35
- FOREIGN KEY (from_entity_id) REFERENCES entities(id) ON DELETE CASCADE,
36
- FOREIGN KEY (to_entity_id) REFERENCES entities(id) ON DELETE CASCADE,
37
- UNIQUE(from_entity_id, to_entity_id, relation_type)
38
- );
39
-
40
- CREATE TABLE IF NOT EXISTS tags (
41
- id INTEGER PRIMARY KEY AUTOINCREMENT,
42
- entity_id INTEGER NOT NULL,
43
- tag TEXT NOT NULL,
44
- FOREIGN KEY (entity_id) REFERENCES entities(id) ON DELETE CASCADE
45
- );
46
-
47
- CREATE INDEX IF NOT EXISTS idx_tags_entity ON tags(entity_id);
48
- CREATE INDEX IF NOT EXISTS idx_tags_tag ON tags(tag);
49
- -- The tags dedup DELETE + idx_tags_entity_tag_unique creation live in
50
- -- ensureTagsUniqueIndex(), AFTER this exec — the DELETE is a one-time
51
- -- migration, and a DML statement in this string made every open start a
52
- -- write transaction even when it deleted nothing (same reader-breaking
53
- -- pattern as the hook_runs_since note below).
54
- CREATE INDEX IF NOT EXISTS idx_observations_entity ON observations(entity_id);
55
- CREATE INDEX IF NOT EXISTS idx_relations_from ON relations(from_entity_id);
56
- CREATE INDEX IF NOT EXISTS idx_relations_to ON relations(to_entity_id);
57
- CREATE INDEX IF NOT EXISTS idx_entities_type_created ON entities(type, created_at);
58
-
59
- -- Migration markers and small bits of persistent state (index segmentation
60
- -- version, embedding dimension, pending-reindex flags, backfill markers).
61
- --
62
- -- This used to be created ad hoc by each helper that needed it — four inline
63
- -- CREATE TABLE IF NOT EXISTS copies in src/db.ts, none of them visible to
64
- -- scripts/check-schema-drift.mjs, which only extracts SCHEMA_SQL and FTS_SQL.
65
- -- A column added to one copy would not have been caught. It also meant the
66
- -- hook-side schema had no metadata table at all, so hooks could not
67
- -- participate in migrations even in principle.
68
- CREATE TABLE IF NOT EXISTS memesh_metadata (
69
- key TEXT PRIMARY KEY,
70
- value TEXT NOT NULL
71
- );
72
-
73
- -- Proof that a capture hook actually RAN. Nothing else in this schema can
74
- -- give it: every other signal is "a row was written", and "the hook ran and
75
- -- found nothing worth saving" is the healthy case that produces no row at
76
- -- all. So a quiet day and a dead capture loop were byte-identical in the
77
- -- database, and the one doctor message that had to cover both cried wolf on
78
- -- the first and stayed silent on the second.
79
- --
80
- -- Written by the three hooks that hold a read-write handle (Stop, PreCompact,
81
- -- PostToolUse), each calling recordHookRun() at its own SUCCESSFUL exit —
82
- -- after capture, not at open. Stamping at open certified the wrong thing: a
83
- -- hook that opened the database and then died mid-capture looked alive for a
84
- -- day. "Successful" is precise: a completed capture, or a well-formed
85
- -- payload the hook correctly decided not to capture (dedup, low-signal
86
- -- session). A malformed payload (schema-flip shapes) and a write that did
87
- -- not land both leave no stamp — either would make the heartbeat mask the
88
- -- exact dropout it exists to expose. The recall-side hooks open read-only
89
- -- and deliberately do not appear here: their liveness answers a different
90
- -- question, and giving them a write handle would put a lock acquisition on
91
- -- the SessionStart hot path.
92
- --
93
- -- One row per hook, upserted. It does not grow.
94
- CREATE TABLE IF NOT EXISTS hook_runs (
95
- hook TEXT PRIMARY KEY,
96
- last_run_at TIMESTAMP NOT NULL,
97
- run_count INTEGER NOT NULL DEFAULT 0
98
- );
99
-
100
- -- The 'hook_runs_since' metadata key (when this database first became able
101
- -- to record hook runs) is stamped by ensureHookRunsSince() AFTER this exec,
102
- -- NOT here. It used to be an INSERT OR IGNORE in this string, and that made
103
- -- every open — including opens that only ever read — start a write
104
- -- transaction: an INSERT statement takes the WAL writer lock even when
105
- -- OR IGNORE ends up changing nothing. Two states regressed from "reads work"
106
- -- to "open throws": a peer holding the writer lock past busy_timeout, and a
107
- -- read-only database FILE (measured: "attempt to write a readonly database"
108
- -- killed recall along with capture). The helper SELECTs first and writes
109
- -- only when the key is genuinely absent — once per database lifetime.
110
- `;
111
- const FTS_SQL = `
112
- CREATE VIRTUAL TABLE IF NOT EXISTS entities_fts USING fts5(
113
- name, observations, content='',
114
- tokenize='unicode61 remove_diacritics 1'
115
- );
116
-
117
- -- Term -> document-count view over the index above. Stores nothing of its own;
118
- -- it exists so search() can drop query terms that appear in most of the corpus,
119
- -- which are the ones BM25 already scores near zero. See dropUbiquitousTerms().
120
- CREATE VIRTUAL TABLE IF NOT EXISTS fts_vocab USING fts5vocab(entities_fts, 'row');
121
- `;
122
- function ensureTagsUniqueIndex(handle) {
123
- try {
124
- const present = handle
125
- .prepare("SELECT 1 FROM sqlite_master WHERE type = 'index' AND name = 'idx_tags_entity_tag_unique'")
126
- .get();
127
- if (present)
128
- return;
129
- handle.exec('BEGIN IMMEDIATE; ' +
130
- 'DELETE FROM tags WHERE id NOT IN (SELECT MIN(id) FROM tags GROUP BY entity_id, tag); ' +
131
- 'CREATE UNIQUE INDEX IF NOT EXISTS idx_tags_entity_tag_unique ON tags(entity_id, tag); ' +
132
- 'COMMIT;');
133
- }
134
- catch (err) {
135
- try {
136
- handle.exec('ROLLBACK');
137
- }
138
- catch { }
139
- try {
140
- process.stderr.write(`MeMesh: could not create the tags unique index (${err instanceof Error ? err.message : String(err)}). ` +
141
- `Reads are unaffected; the next open retries the dedup and index together.\n`);
142
- }
143
- catch { }
144
- }
145
- }
146
- function ensureHookRunsSince(handle) {
147
- try {
148
- const row = handle
149
- .prepare("SELECT value FROM memesh_metadata WHERE key = 'hook_runs_since'")
150
- .get();
151
- if (row) {
152
- const m = /^(\d{4})-(\d{2})-(\d{2})[ T](\d{2}):(\d{2}):(\d{2})$/.exec(row.value ?? '');
153
- if (m) {
154
- const then = Date.UTC(+m[1], +m[2] - 1, +m[3], +m[4], +m[5], +m[6]);
155
- const d = new Date(then);
156
- const intact = Number.isFinite(then)
157
- && d.getUTCFullYear() === +m[1] && d.getUTCMonth() === +m[2] - 1 && d.getUTCDate() === +m[3]
158
- && d.getUTCHours() === +m[4] && d.getUTCMinutes() === +m[5] && d.getUTCSeconds() === +m[6];
159
- if (intact && then <= Date.now() + 5 * 60 * 1000)
160
- return;
161
- }
162
- handle
163
- .prepare("UPDATE memesh_metadata SET value = datetime('now') WHERE key = 'hook_runs_since'")
164
- .run();
165
- return;
166
- }
167
- handle
168
- .prepare("INSERT OR IGNORE INTO memesh_metadata (key, value) VALUES ('hook_runs_since', datetime('now'))")
169
- .run();
170
- }
171
- catch (err) {
172
- try {
173
- process.stderr.write(`MeMesh: could not stamp hook_runs_since (${err instanceof Error ? err.message : String(err)}). ` +
174
- `Reads are unaffected; doctor's hook-activity tracking starts once the database is writable.\n`);
175
- }
176
- catch { }
177
- }
178
- }
14
+ let dimensionMismatchNoticed = false;
179
15
  export function openDatabase(dbPath) {
180
16
  if (db)
181
17
  return db;
@@ -236,41 +72,13 @@ function migrateToCurrentSchema(db, resolvedPath) {
236
72
  }
237
73
  catch { }
238
74
  }
239
- const safeAlter = (sql) => {
240
- try {
241
- db.exec(sql);
242
- }
243
- catch (e) {
244
- const msg = e instanceof Error ? e.message : String(e);
245
- if (!/duplicate column name/i.test(msg))
246
- throw e;
247
- }
248
- };
249
- const columns = db.prepare("PRAGMA table_info(entities)").all();
250
- if (!columns.some((c) => c.name === 'status')) {
251
- safeAlter("ALTER TABLE entities ADD COLUMN status TEXT NOT NULL DEFAULT 'active'");
252
- db.exec("CREATE INDEX IF NOT EXISTS idx_entities_status ON entities(status)");
253
- }
254
- const scoringCols = db.prepare("PRAGMA table_info(entities)").all();
255
- if (!scoringCols.some((c) => c.name === 'access_count')) {
256
- safeAlter("ALTER TABLE entities ADD COLUMN access_count INTEGER DEFAULT 0");
257
- safeAlter("ALTER TABLE entities ADD COLUMN last_accessed_at TIMESTAMP");
258
- safeAlter("ALTER TABLE entities ADD COLUMN confidence REAL DEFAULT 1.0");
259
- safeAlter("ALTER TABLE entities ADD COLUMN valid_from TIMESTAMP");
260
- safeAlter("ALTER TABLE entities ADD COLUMN valid_until TIMESTAMP");
261
- }
262
- if (!scoringCols.some((c) => c.name === 'namespace')) {
263
- safeAlter("ALTER TABLE entities ADD COLUMN namespace TEXT DEFAULT 'personal'");
264
- db.exec("CREATE INDEX IF NOT EXISTS idx_entities_namespace ON entities(namespace)");
265
- }
266
- const recallCols = db.prepare("PRAGMA table_info(entities)").all();
267
- if (!recallCols.some((c) => c.name === 'recall_hits')) {
268
- safeAlter("ALTER TABLE entities ADD COLUMN recall_hits INTEGER DEFAULT 0");
269
- safeAlter("ALTER TABLE entities ADD COLUMN recall_misses INTEGER DEFAULT 0");
270
- }
75
+ migrateEntitiesSchema(db);
271
76
  runAutoDecay(db);
272
77
  backfillSignalScores(db);
78
+ backfillTitles(db);
79
+ backfillAcceptedProposalTrust(db);
273
80
  ensureDreamProposalsTable(db);
81
+ ensureConflictJudgedPairsTable(db);
274
82
  ensureLlmTelemetryTable(db);
275
83
  runAutoTelemetryPrune(db);
276
84
  ensureFtsSegmentation(db);
@@ -293,82 +101,6 @@ function migrateToCurrentSchema(db, resolvedPath) {
293
101
  ensureVecTable(db, resolvedPath, targetDim, dimensionKnown);
294
102
  }
295
103
  }
296
- export const FTS_SEGMENTATION_VERSION = 3;
297
- const MIGRATION_RETRY_BACKOFF_MS = 24 * 60 * 60 * 1000;
298
- function isTransientDbError(err) {
299
- const code = err?.code ?? '';
300
- const msg = err?.message ?? '';
301
- return (/SQLITE_BUSY|SQLITE_LOCKED|SQLITE_PROTOCOL/.test(code) ||
302
- /database is locked|database table is locked|locking protocol/i.test(msg));
303
- }
304
- export function runOnceMigration(db, opts) {
305
- const { key, version, describe, migrate } = opts;
306
- const attemptKey = `${key}_last_attempt`;
307
- const readMarker = (k) => db.prepare('SELECT value FROM memesh_metadata WHERE key = ?').get(k)?.value;
308
- const stored = readMarker(key);
309
- if (stored && parseInt(stored, 10) >= version)
310
- return false;
311
- const lastAttempt = readMarker(attemptKey);
312
- if (lastAttempt && Date.now() - parseInt(lastAttempt, 10) < MIGRATION_RETRY_BACKOFF_MS) {
313
- return false;
314
- }
315
- try {
316
- db.transaction(() => {
317
- const current = readMarker(key);
318
- if (current && parseInt(current, 10) >= version)
319
- return;
320
- migrate(db, current ? parseInt(current, 10) : 0);
321
- db.prepare('INSERT OR REPLACE INTO memesh_metadata (key, value) VALUES (?, ?)').run(key, String(version));
322
- db.prepare('DELETE FROM memesh_metadata WHERE key = ?').run(attemptKey);
323
- }).immediate();
324
- return true;
325
- }
326
- catch (err) {
327
- if (isTransientDbError(err)) {
328
- process.stderr.write(`MeMesh: ${describe} deferred (${err instanceof Error ? err.message : String(err)}). ` +
329
- `Another process holds the database; it will run on the next start.\n`);
330
- return false;
331
- }
332
- try {
333
- db.prepare('INSERT OR REPLACE INTO memesh_metadata (key, value) VALUES (?, ?)').run(attemptKey, String(Date.now()));
334
- }
335
- catch { }
336
- process.stderr.write(`MeMesh: ${describe} failed (${err instanceof Error ? err.message : String(err)}). ` +
337
- `Your memories are unaffected — this rebuilds a derived index. ` +
338
- `It will retry in 24h, or run 'memesh reindex --fts' to retry now.\n`);
339
- return false;
340
- }
341
- }
342
- function ensureFtsSegmentation(db) {
343
- runOnceMigration(db, {
344
- key: 'fts_segmentation_version',
345
- version: FTS_SEGMENTATION_VERSION,
346
- describe: 'search index rebuild',
347
- migrate: rebuildFtsIndex,
348
- });
349
- }
350
- const FTS_REBUILD_PAGE_SIZE = 500;
351
- function rebuildFtsIndex(db) {
352
- db.exec("INSERT INTO entities_fts (entities_fts) VALUES('delete-all')");
353
- const page = db.prepare(`SELECT e.id, e.name, COALESCE(group_concat(o.content, ' '), '') AS obs
354
- FROM entities e
355
- LEFT JOIN observations o ON o.entity_id = e.id
356
- WHERE e.status = 'active' AND e.id > ?
357
- GROUP BY e.id
358
- ORDER BY e.id
359
- LIMIT ?`);
360
- let afterId = 0;
361
- for (;;) {
362
- const rows = page.all(afterId, FTS_REBUILD_PAGE_SIZE);
363
- if (rows.length === 0)
364
- break;
365
- for (const row of rows)
366
- insertFtsRow(db, row.id, row.name, row.obs);
367
- afterId = rows[rows.length - 1].id;
368
- if (rows.length < FTS_REBUILD_PAGE_SIZE)
369
- break;
370
- }
371
- }
372
104
  export function reindexFts() {
373
105
  const database = getDatabase();
374
106
  database.transaction(() => {
@@ -383,20 +115,7 @@ export function reindexFts() {
383
115
  .get();
384
116
  return { entities: c };
385
117
  }
386
- let vectorRebuildConsentFor = null;
387
- export async function allowVectorIndexRebuild(dbPath, canRefill) {
388
- if (!(await canRefill()))
389
- return false;
390
- vectorRebuildConsentFor = path.resolve(dbPath);
391
- return true;
392
- }
393
- function consumeVectorRebuildConsent(resolvedPath) {
394
- const granted = vectorRebuildConsentFor;
395
- vectorRebuildConsentFor = null;
396
- return granted !== null && granted === path.resolve(resolvedPath);
397
- }
398
118
  function ensureVecTable(db, resolvedPath, targetDim, dimensionKnown = true) {
399
- const rebuildConsented = consumeVectorRebuildConsent(resolvedPath);
400
119
  const storedDim = db.prepare("SELECT value FROM memesh_metadata WHERE key = 'embedding_dimension'").get();
401
120
  const currentDim = storedDim ? parseInt(storedDim.value, 10) : 0;
402
121
  const vecExists = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='entities_vec'").get();
@@ -409,25 +128,21 @@ function ensureVecTable(db, resolvedPath, targetDim, dimensionKnown = true) {
409
128
  `Fix ~/.memesh/config.json to change embedders.\n`);
410
129
  return;
411
130
  }
412
- if (vecExists &&
413
- currentDim !== 0 &&
414
- currentDim !== targetDim &&
415
- !rebuildConsented) {
416
- process.stderr.write(`MeMesh: this database records ${currentDim}-dim embeddings but the current ` +
417
- `configuration asks for ${targetDim}. Keeping the existing vector index rather ` +
418
- `than rebuilding it, because rebuilding deletes every stored vector. ` +
419
- `If the configuration is wrong, fix it. If you meant to switch embedders, run ` +
420
- `'memesh reindex --vectors' to rebuild the index at ${targetDim} and regenerate.\n`);
131
+ if (vecExists && currentDim !== 0 && currentDim !== targetDim) {
132
+ if (!dimensionMismatchNoticed) {
133
+ process.stderr.write(`MeMesh: this database records ${currentDim}-dim embeddings but the current ` +
134
+ `configuration asks for ${targetDim}. Nothing is deleted — the index is kept ` +
135
+ `so a rebuild can resume but semantic search is OFF until the rebuild ` +
136
+ `finishes, because a ${targetDim}-dim query cannot be matched against a ` +
137
+ `${currentDim}-dim index; recall is on keyword search alone meanwhile. ` +
138
+ `Run 'memesh reindex' to build the ${targetDim}-dim index alongside it and ` +
139
+ `switch over once it is complete.\n`);
140
+ dimensionMismatchNoticed = true;
141
+ }
142
+ markReindexOwed(currentDim, targetDim, 'dimension-change', db);
421
143
  return;
422
144
  }
423
145
  db.transaction(() => {
424
- if (vecExists) {
425
- process.stderr.write(`MeMesh: Embedding dimension changed (${currentDim} → ${targetDim}). Rebuilding vector index.\n` +
426
- `MeMesh: Old embeddings deleted. Run 'memesh reindex' to regenerate vectors for all entities.\n` +
427
- `MeMesh: Without reindex, only newly accessed entities will be embedded.\n`);
428
- db.exec('DROP TABLE entities_vec');
429
- db.prepare("INSERT OR REPLACE INTO memesh_metadata (key, value) VALUES ('pending_reindex', ?)").run(JSON.stringify({ from: currentDim, to: targetDim, droppedAt: new Date().toISOString() }));
430
- }
431
146
  db.exec(`
432
147
  CREATE VIRTUAL TABLE IF NOT EXISTS entities_vec USING vec0(
433
148
  embedding float[${targetDim}]
@@ -436,17 +151,164 @@ function ensureVecTable(db, resolvedPath, targetDim, dimensionKnown = true) {
436
151
  db.prepare("INSERT OR REPLACE INTO memesh_metadata (key, value) VALUES ('embedding_dimension', ?)").run(String(targetDim));
437
152
  }).immediate();
438
153
  }
439
- export function getPendingReindexInfo() {
154
+ export const GENERATION_TABLE = 'entities_vec_next';
155
+ const GENERATION_HASH_TABLE = 'entities_vec_next_source';
156
+ const GENERATION_KEY = 'vector_generation';
157
+ function tableExists(conn, name) {
158
+ return conn.prepare("SELECT COUNT(*) AS c FROM sqlite_master WHERE type IN ('table','view') AND name = ?").get(name).c > 0;
159
+ }
160
+ function assertVectorWidth(dimension) {
161
+ if (!Number.isInteger(dimension) || dimension <= 0 || dimension > 65536) {
162
+ throw new Error(`Refusing to build a vector index at width ${String(dimension)}.`);
163
+ }
164
+ }
165
+ export function readVectorGeneration() {
166
+ if (!db)
167
+ return { state: 'none' };
168
+ try {
169
+ const row = db.prepare('SELECT value FROM memesh_metadata WHERE key = ?').get(GENERATION_KEY);
170
+ if (!row)
171
+ return { state: 'none' };
172
+ const parsed = JSON.parse(row.value);
173
+ if (typeof parsed.dimension !== 'number' || typeof parsed.provider !== 'string') {
174
+ return { state: 'unreadable', detail: 'the marker is missing its dimension or provider' };
175
+ }
176
+ return {
177
+ state: 'open',
178
+ info: {
179
+ dimension: parsed.dimension,
180
+ provider: parsed.provider,
181
+ startedAt: String(parsed.startedAt ?? ''),
182
+ },
183
+ };
184
+ }
185
+ catch (err) {
186
+ return { state: 'unreadable', detail: err instanceof Error ? err.message : String(err) };
187
+ }
188
+ }
189
+ export function generationRowIds() {
190
+ const out = new Set();
440
191
  if (!db)
192
+ return out;
193
+ if (!tableExists(db, GENERATION_TABLE))
194
+ return out;
195
+ const rows = db.prepare(`SELECT rowid AS id FROM ${GENERATION_TABLE}`).all();
196
+ for (const r of rows)
197
+ out.add(Number(r.id));
198
+ return out;
199
+ }
200
+ export function beginVectorGeneration(dimension, provider) {
201
+ assertVectorWidth(dimension);
202
+ const conn = getDatabase();
203
+ const read = readVectorGeneration();
204
+ const stagingExists = tableExists(conn, GENERATION_TABLE);
205
+ if (read.state === 'unreadable' && stagingExists) {
206
+ throw new Error('A half-built vector index is present but its marker cannot be read '
207
+ + `(${read.detail}). Resuming it risks mixing vectors from two different `
208
+ + 'embedding spaces, and discarding it throws away embeddings a previous run '
209
+ + 'already produced, so neither is done automatically. Run '
210
+ + '`memesh reindex --discard-generation` to throw the half-built index away '
211
+ + 'and start clean.');
212
+ }
213
+ const existing = read.state === 'open' ? read.info : null;
214
+ const compatible = stagingExists && existing !== null
215
+ && existing.dimension === dimension && existing.provider === provider;
216
+ if (stagingExists && !compatible) {
217
+ discardVectorGeneration();
218
+ }
219
+ const startedAt = compatible && existing ? existing.startedAt : new Date().toISOString();
220
+ conn.transaction(() => {
221
+ conn.exec(`CREATE VIRTUAL TABLE IF NOT EXISTS ${GENERATION_TABLE} USING vec0(embedding float[${dimension}])`);
222
+ conn.exec(`CREATE TABLE IF NOT EXISTS ${GENERATION_HASH_TABLE} (`
223
+ + 'rowid_ref INTEGER PRIMARY KEY, text_hash TEXT NOT NULL)');
224
+ conn.prepare('INSERT OR REPLACE INTO memesh_metadata (key, value) VALUES (?, ?)').run(GENERATION_KEY, JSON.stringify({ dimension, provider, startedAt }));
225
+ }).immediate();
226
+ return { resumed: compatible };
227
+ }
228
+ export function generationRowHashes() {
229
+ const out = new Map();
230
+ if (!db)
231
+ return out;
232
+ if (!tableExists(db, GENERATION_HASH_TABLE))
233
+ return out;
234
+ const rows = db.prepare(`SELECT rowid_ref AS id, text_hash AS h FROM ${GENERATION_HASH_TABLE}`).all();
235
+ for (const r of rows)
236
+ out.set(Number(r.id), r.h);
237
+ return out;
238
+ }
239
+ export function recordGenerationRow(entityId, textHash) {
240
+ const conn = getDatabase();
241
+ conn.prepare(`INSERT OR REPLACE INTO ${GENERATION_HASH_TABLE} (rowid_ref, text_hash) VALUES (?, ?)`).run(BigInt(entityId), textHash);
242
+ }
243
+ export function discardVectorGeneration() {
244
+ const conn = getDatabase();
245
+ conn.transaction(() => {
246
+ conn.exec(`DROP TABLE IF EXISTS ${GENERATION_TABLE}`);
247
+ conn.exec(`DROP TABLE IF EXISTS ${GENERATION_HASH_TABLE}`);
248
+ conn.prepare('DELETE FROM memesh_metadata WHERE key = ?').run(GENERATION_KEY);
249
+ }).immediate();
250
+ }
251
+ export function swapVectorGeneration(dimension) {
252
+ assertVectorWidth(dimension);
253
+ const conn = getDatabase();
254
+ conn.transaction(() => {
255
+ conn.exec(`DELETE FROM ${GENERATION_TABLE} WHERE rowid NOT IN `
256
+ + `(SELECT id FROM entities WHERE status = 'active')`);
257
+ const storedDim = Number(conn.prepare("SELECT value FROM memesh_metadata WHERE key = 'embedding_dimension'").get()?.value ?? 0);
258
+ if (storedDim === dimension) {
259
+ conn.exec(`INSERT INTO ${GENERATION_TABLE} (rowid, embedding) `
260
+ + `SELECT v.rowid, v.embedding FROM entities_vec v `
261
+ + `WHERE v.rowid IN (SELECT id FROM entities WHERE status = 'active') `
262
+ + `AND v.rowid NOT IN (SELECT rowid FROM ${GENERATION_TABLE})`);
263
+ }
264
+ const staged = conn.prepare(`SELECT COUNT(*) AS c FROM ${GENERATION_TABLE}`).get().c;
265
+ conn.exec('DROP TABLE IF EXISTS entities_vec');
266
+ conn.exec(`CREATE VIRTUAL TABLE entities_vec USING vec0(embedding float[${dimension}])`);
267
+ conn.exec(`INSERT INTO entities_vec (rowid, embedding) SELECT rowid, embedding FROM ${GENERATION_TABLE}`);
268
+ const installed = conn.prepare('SELECT COUNT(*) AS c FROM entities_vec').get().c;
269
+ if (installed !== staged) {
270
+ throw new Error(`Vector index swap copied ${installed} of ${staged} staged rows; refusing to publish a short index.`);
271
+ }
272
+ conn.exec(`DROP TABLE ${GENERATION_TABLE}`);
273
+ conn.exec(`DROP TABLE IF EXISTS ${GENERATION_HASH_TABLE}`);
274
+ conn.prepare('INSERT OR REPLACE INTO memesh_metadata (key, value) VALUES (?, ?)').run('embedding_dimension', String(dimension));
275
+ conn.prepare('DELETE FROM memesh_metadata WHERE key = ?').run(GENERATION_KEY);
276
+ }).immediate();
277
+ }
278
+ export function getStoredEmbeddingDimension() {
279
+ if (!db)
280
+ return 0;
281
+ const row = db.prepare("SELECT value FROM memesh_metadata WHERE key = 'embedding_dimension'").get();
282
+ return row ? Number(row.value) || 0 : 0;
283
+ }
284
+ export function getPendingReindexInfo() {
285
+ return readPendingReindex(db);
286
+ }
287
+ function readPendingReindex(conn) {
288
+ if (!conn)
441
289
  return null;
442
290
  try {
443
- const row = db.prepare("SELECT value FROM memesh_metadata WHERE key = 'pending_reindex'").get();
291
+ const row = conn.prepare("SELECT value FROM memesh_metadata WHERE key = 'pending_reindex'").get();
444
292
  return row ? JSON.parse(row.value) : null;
445
293
  }
446
294
  catch {
447
295
  return null;
448
296
  }
449
297
  }
298
+ export function markReindexOwed(from, to, reason, conn = db) {
299
+ if (!conn)
300
+ return;
301
+ const existing = readPendingReindex(conn);
302
+ if (existing && existing.from === from && existing.to === to && existing.reason === reason) {
303
+ return;
304
+ }
305
+ conn.prepare("INSERT OR REPLACE INTO memesh_metadata (key, value) VALUES ('pending_reindex', ?)").run(JSON.stringify({
306
+ from,
307
+ to,
308
+ reason,
309
+ noticedAt: existing?.noticedAt ?? new Date().toISOString(),
310
+ }));
311
+ }
450
312
  export function clearPendingReindexFlag() {
451
313
  if (!db)
452
314
  return;
@@ -472,14 +334,21 @@ function ensureDreamProposalsTable(db) {
472
334
  `);
473
335
  const dpCols = db.prepare("PRAGMA table_info(dream_proposals)").all();
474
336
  if (!dpCols.some((c) => c.name === 'source_kind')) {
475
- try {
476
- db.exec("ALTER TABLE dream_proposals ADD COLUMN source_kind TEXT NOT NULL DEFAULT 'entities'");
477
- }
478
- catch (err) {
479
- if (!String(err.message).includes('duplicate column name'))
480
- throw err;
481
- }
337
+ safeAlter(db, "ALTER TABLE dream_proposals ADD COLUMN source_kind TEXT NOT NULL DEFAULT 'entities'");
482
338
  }
339
+ if (!dpCols.some((c) => c.name === 'kind')) {
340
+ safeAlter(db, "ALTER TABLE dream_proposals ADD COLUMN kind TEXT NOT NULL DEFAULT 'digest'");
341
+ }
342
+ }
343
+ function ensureConflictJudgedPairsTable(db) {
344
+ db.exec(`
345
+ CREATE TABLE IF NOT EXISTS conflict_judged_pairs (
346
+ pair_key TEXT PRIMARY KEY,
347
+ verdict TEXT NOT NULL,
348
+ proposal_id INTEGER,
349
+ judged_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
350
+ );
351
+ `);
483
352
  }
484
353
  function ensureLlmTelemetryTable(db) {
485
354
  db.exec(`
@@ -574,11 +443,134 @@ function backfillSignalScores(db) {
574
443
  });
575
444
  tx();
576
445
  }
446
+ function deriveHeuristicTitle(type, observations) {
447
+ if (observations.length === 0)
448
+ return null;
449
+ if (type === 'lesson_learned' || type === 'lesson' || type === 'mistake') {
450
+ const errObs = observations.find((o) => /^Error:\s*/.test(o.trim()));
451
+ if (errObs) {
452
+ const firstLine = errObs.trim().replace(/^Error:\s*/, '').split('\n')[0].trim();
453
+ if (firstLine)
454
+ return truncateTitle(firstLine);
455
+ }
456
+ }
457
+ if (type === 'commit') {
458
+ const first = observations[0]?.split('\n')[0].trim();
459
+ if (first && !/^(Branch|Diff stats):/.test(first))
460
+ return truncateTitle(first);
461
+ }
462
+ const nonTrivial = observations.filter((o) => o.length > 30 && !isBoilerplateObservation(o));
463
+ const pool = nonTrivial.length > 0 ? nonTrivial : observations;
464
+ const best = pool.slice(0, 3).reduce((a, b) => (b.length > a.length ? b : a), pool[0]);
465
+ const firstLine = best?.split('\n')[0].trim();
466
+ return firstLine ? truncateTitle(firstLine) : null;
467
+ }
468
+ function backfillAcceptedProposalTrust(db) {
469
+ const MARKER = 'accepted_proposal_trust_v1';
470
+ if (db.prepare('SELECT value FROM memesh_metadata WHERE key = ?').get(MARKER))
471
+ return;
472
+ const stamp = (cleared, skipped) => db.prepare('INSERT OR REPLACE INTO memesh_metadata (key, value) VALUES (?, ?)')
473
+ .run(MARKER, JSON.stringify({ at: new Date().toISOString(), cleared, skipped }));
474
+ let rows;
475
+ try {
476
+ rows = db.prepare(`SELECT id, metadata FROM entities
477
+ WHERE metadata IS NOT NULL
478
+ AND json_valid(metadata)
479
+ AND json_extract(metadata, '$.trust') = 'untrusted'
480
+ AND json_extract(metadata, '$.proposal_id') IS NOT NULL`).all();
481
+ }
482
+ catch {
483
+ return;
484
+ }
485
+ if (rows.length === 0) {
486
+ stamp(0, 0);
487
+ return;
488
+ }
489
+ const updateStmt = db.prepare('UPDATE entities SET metadata = ? WHERE id = ?');
490
+ const tx = db.transaction(() => {
491
+ let cleared = 0;
492
+ let skipped = 0;
493
+ for (const row of rows) {
494
+ let metadata;
495
+ try {
496
+ const parsed = JSON.parse(row.metadata ?? '{}');
497
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
498
+ skipped++;
499
+ continue;
500
+ }
501
+ metadata = parsed;
502
+ }
503
+ catch {
504
+ skipped++;
505
+ continue;
506
+ }
507
+ delete metadata.trust;
508
+ updateStmt.run(JSON.stringify(metadata), row.id);
509
+ cleared++;
510
+ }
511
+ stamp(cleared, skipped);
512
+ });
513
+ tx();
514
+ }
515
+ function backfillTitles(db) {
516
+ const MARKER = 'title_backfill_v1';
517
+ const done = db.prepare('SELECT value FROM memesh_metadata WHERE key = ?').get(MARKER);
518
+ if (done)
519
+ return;
520
+ const rows = db.prepare('SELECT id, name, type, status, metadata FROM entities WHERE title IS NULL').all();
521
+ const stamp = (titled, skipped) => db.prepare('INSERT OR REPLACE INTO memesh_metadata (key, value) VALUES (?, ?)').run(MARKER, JSON.stringify({ at: new Date().toISOString(), titled, skipped }));
522
+ if (rows.length === 0) {
523
+ stamp(0, 0);
524
+ return;
525
+ }
526
+ const obsStmt = db.prepare('SELECT content FROM observations WHERE entity_id = ? ORDER BY id');
527
+ const updateStmt = db.prepare('UPDATE entities SET title = ?, metadata = ? WHERE id = ?');
528
+ const tx = db.transaction(() => {
529
+ let titled = 0;
530
+ let skipped = 0;
531
+ for (const row of rows) {
532
+ let metadata;
533
+ if (row.metadata) {
534
+ try {
535
+ metadata = JSON.parse(row.metadata);
536
+ }
537
+ catch {
538
+ skipped++;
539
+ continue;
540
+ }
541
+ if (typeof metadata !== 'object' || metadata === null || Array.isArray(metadata)) {
542
+ skipped++;
543
+ continue;
544
+ }
545
+ }
546
+ else {
547
+ metadata = {};
548
+ }
549
+ const observations = obsStmt.all(row.id).map(o => o.content);
550
+ const title = deriveHeuristicTitle(row.type, observations);
551
+ if (!title) {
552
+ skipped++;
553
+ continue;
554
+ }
555
+ metadata.title_source = 'heuristic';
556
+ updateStmt.run(title, JSON.stringify(metadata), row.id);
557
+ if (row.status === 'active') {
558
+ const obsText = joinIndexedObservations(observations);
559
+ removeFromFts(db, row.id, row.name, obsText);
560
+ insertFtsRow(db, row.id, row.name, obsText, title);
561
+ }
562
+ titled++;
563
+ }
564
+ db.prepare('INSERT OR REPLACE INTO memesh_metadata (key, value) VALUES (?, ?)').run(MARKER, JSON.stringify({ at: new Date().toISOString(), titled, skipped }));
565
+ });
566
+ tx();
567
+ }
577
568
  export function closeDatabase() {
578
569
  if (db) {
579
570
  db.close();
580
571
  db = null;
581
572
  }
573
+ dimensionMismatchNoticed = false;
582
574
  }
583
575
  export function getDatabase() {
584
576
  if (!db)