@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.
- package/.claude-plugin/marketplace.json +5 -3
- package/.claude-plugin/plugin.json +6 -4
- package/AGENTS.md +116 -0
- package/README.de.md +141 -48
- package/README.md +173 -48
- package/README.zh-TW.md +142 -48
- package/dashboard/dist/index.html +15 -14
- package/dist/cli/view-live.js +3 -3
- package/dist/core/analytics.d.ts +9 -0
- package/dist/core/analytics.d.ts.map +1 -1
- package/dist/core/analytics.js +36 -18
- package/dist/core/analytics.js.map +1 -1
- package/dist/core/auto-tagger.d.ts.map +1 -1
- package/dist/core/auto-tagger.js +4 -9
- package/dist/core/auto-tagger.js.map +1 -1
- package/dist/core/briefing.d.ts +8 -0
- package/dist/core/briefing.d.ts.map +1 -0
- package/dist/core/briefing.js +92 -0
- package/dist/core/briefing.js.map +1 -0
- package/dist/core/capture-flag.d.ts +5 -0
- package/dist/core/capture-flag.d.ts.map +1 -0
- package/dist/core/capture-flag.js +10 -0
- package/dist/core/capture-flag.js.map +1 -0
- package/dist/core/config.d.ts +0 -1
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js.map +1 -1
- package/dist/core/conflict-candidates.d.ts +20 -0
- package/dist/core/conflict-candidates.d.ts.map +1 -0
- package/dist/core/conflict-candidates.js +79 -0
- package/dist/core/conflict-candidates.js.map +1 -0
- package/dist/core/conflict-judge.d.ts +47 -0
- package/dist/core/conflict-judge.d.ts.map +1 -0
- package/dist/core/conflict-judge.js +189 -0
- package/dist/core/conflict-judge.js.map +1 -0
- package/dist/core/demo.d.ts.map +1 -1
- package/dist/core/demo.js +1 -1
- package/dist/core/demo.js.map +1 -1
- package/dist/core/digest-validator.d.ts.map +1 -1
- package/dist/core/digest-validator.js +3 -5
- package/dist/core/digest-validator.js.map +1 -1
- package/dist/core/doctor.d.ts +2 -0
- package/dist/core/doctor.d.ts.map +1 -1
- package/dist/core/doctor.js +59 -62
- package/dist/core/doctor.js.map +1 -1
- package/dist/core/dreamer.d.ts +5 -2
- package/dist/core/dreamer.d.ts.map +1 -1
- package/dist/core/dreamer.js +329 -25
- package/dist/core/dreamer.js.map +1 -1
- package/dist/core/embedder.d.ts +8 -4
- package/dist/core/embedder.d.ts.map +1 -1
- package/dist/core/embedder.js +82 -24
- package/dist/core/embedder.js.map +1 -1
- package/dist/core/failure-analyzer.d.ts.map +1 -1
- package/dist/core/failure-analyzer.js +7 -12
- package/dist/core/failure-analyzer.js.map +1 -1
- package/dist/core/graph.d.ts +12 -0
- package/dist/core/graph.d.ts.map +1 -1
- package/dist/core/graph.js +56 -1
- package/dist/core/graph.js.map +1 -1
- package/dist/core/guards.d.ts +20 -0
- package/dist/core/guards.d.ts.map +1 -0
- package/dist/core/guards.js +103 -0
- package/dist/core/guards.js.map +1 -0
- package/dist/core/install-channel.d.ts +1 -1
- package/dist/core/install-channel.d.ts.map +1 -1
- package/dist/core/install-channel.js +16 -5
- package/dist/core/install-channel.js.map +1 -1
- package/dist/core/install-hooks.d.ts +5 -0
- package/dist/core/install-hooks.d.ts.map +1 -1
- package/dist/core/install-hooks.js +0 -0
- package/dist/core/install-hooks.js.map +1 -1
- package/dist/core/json-utils.d.ts +1 -0
- package/dist/core/json-utils.d.ts.map +1 -1
- package/dist/core/json-utils.js +19 -10
- package/dist/core/json-utils.js.map +1 -1
- package/dist/core/kg-backfill.d.ts +5 -2
- package/dist/core/kg-backfill.d.ts.map +1 -1
- package/dist/core/kg-backfill.js +155 -5
- package/dist/core/kg-backfill.js.map +1 -1
- package/dist/core/lifecycle.d.ts.map +1 -1
- package/dist/core/lifecycle.js +14 -21
- package/dist/core/lifecycle.js.map +1 -1
- package/dist/core/memory-tool.d.ts.map +1 -1
- package/dist/core/memory-tool.js +4 -4
- package/dist/core/memory-tool.js.map +1 -1
- package/dist/core/operations.d.ts +13 -2
- package/dist/core/operations.d.ts.map +1 -1
- package/dist/core/operations.js +115 -28
- package/dist/core/operations.js.map +1 -1
- package/dist/core/prompt-safety.d.ts +1 -0
- package/dist/core/prompt-safety.d.ts.map +1 -1
- package/dist/core/prompt-safety.js +7 -0
- package/dist/core/prompt-safety.js.map +1 -1
- package/dist/core/schema-export.d.ts.map +1 -1
- package/dist/core/schema-export.js +31 -0
- package/dist/core/schema-export.js.map +1 -1
- package/dist/core/serializer.d.ts.map +1 -1
- package/dist/core/serializer.js +8 -0
- package/dist/core/serializer.js.map +1 -1
- package/dist/core/setup.d.ts +29 -0
- package/dist/core/setup.d.ts.map +1 -0
- package/dist/core/setup.js +127 -0
- package/dist/core/setup.js.map +1 -0
- package/dist/core/task-state-store.d.ts +17 -0
- package/dist/core/task-state-store.d.ts.map +1 -0
- package/dist/core/task-state-store.js +45 -0
- package/dist/core/task-state-store.js.map +1 -0
- package/dist/core/task-state.d.ts +19 -0
- package/dist/core/task-state.d.ts.map +1 -0
- package/dist/core/task-state.js +91 -0
- package/dist/core/task-state.js.map +1 -0
- package/dist/core/time-utils.d.ts +2 -0
- package/dist/core/time-utils.d.ts.map +1 -0
- package/dist/core/time-utils.js +14 -0
- package/dist/core/time-utils.js.map +1 -0
- package/dist/core/title.d.ts +5 -0
- package/dist/core/title.d.ts.map +1 -0
- package/dist/core/title.js +14 -0
- package/dist/core/title.js.map +1 -0
- package/dist/core/transcript-source.d.ts.map +1 -1
- package/dist/core/transcript-source.js +2 -3
- package/dist/core/transcript-source.js.map +1 -1
- package/dist/core/types.d.ts +5 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/why.d.ts +54 -0
- package/dist/core/why.d.ts.map +1 -0
- package/dist/core/why.js +168 -0
- package/dist/core/why.js.map +1 -0
- package/dist/core/work-topology.d.ts +36 -0
- package/dist/core/work-topology.d.ts.map +1 -0
- package/dist/core/work-topology.js +192 -0
- package/dist/core/work-topology.js.map +1 -0
- package/dist/db.d.ts +33 -11
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +307 -315
- package/dist/db.js.map +1 -1
- package/dist/knowledge-graph.d.ts +1 -0
- package/dist/knowledge-graph.d.ts.map +1 -1
- package/dist/knowledge-graph.js +50 -40
- package/dist/knowledge-graph.js.map +1 -1
- package/dist/skills-manifest.json +62 -22
- package/dist/storage/conflicts.d.ts.map +1 -1
- package/dist/storage/conflicts.js +2 -7
- package/dist/storage/conflicts.js.map +1 -1
- package/dist/storage/fts-index.d.ts +4 -2
- package/dist/storage/fts-index.d.ts.map +1 -1
- package/dist/storage/fts-index.js +16 -4
- package/dist/storage/fts-index.js.map +1 -1
- package/dist/storage/schema.d.ts +20 -0
- package/dist/storage/schema.d.ts.map +1 -0
- package/dist/storage/schema.js +274 -0
- package/dist/storage/schema.js.map +1 -0
- package/dist/storage/sqlite.d.ts.map +1 -1
- package/dist/storage/sqlite.js +1 -1
- package/dist/storage/sqlite.js.map +1 -1
- package/dist/transports/cli/cli.d.ts +1 -4
- package/dist/transports/cli/cli.d.ts.map +1 -1
- package/dist/transports/cli/cli.js +579 -66
- package/dist/transports/cli/cli.js.map +1 -1
- package/dist/transports/http/server.d.ts.map +1 -1
- package/dist/transports/http/server.js +242 -303
- package/dist/transports/http/server.js.map +1 -1
- package/dist/transports/mcp/handlers.d.ts +46 -0
- package/dist/transports/mcp/handlers.d.ts.map +1 -1
- package/dist/transports/mcp/handlers.js +59 -4
- package/dist/transports/mcp/handlers.js.map +1 -1
- package/dist/transports/schemas.d.ts +29 -10
- package/dist/transports/schemas.d.ts.map +1 -1
- package/dist/transports/schemas.js +33 -8
- package/dist/transports/schemas.js.map +1 -1
- package/hooks/hooks.json +10 -0
- package/llms-install.md +138 -0
- package/package.json +14 -9
- package/scripts/hooks/_generated/capture-flag.js +17 -0
- package/scripts/hooks/_generated/fts-index.js +16 -4
- package/scripts/hooks/_generated/guards.js +110 -0
- package/scripts/hooks/_generated/schema.js +281 -0
- package/scripts/hooks/_generated/sqlite.js +1 -1
- package/scripts/hooks/_generated/task-state.js +98 -0
- package/scripts/hooks/_generated/time-utils.js +21 -0
- package/scripts/hooks/_generated/title.js +21 -0
- package/scripts/hooks/_generated/work-topology.js +199 -0
- package/scripts/hooks/_shared.js +197 -480
- package/scripts/hooks/guard-check.js +76 -0
- package/scripts/hooks/post-commit.js +31 -1
- package/scripts/hooks/pre-compact.js +13 -1
- package/scripts/hooks/pre-edit-recall.js +158 -120
- package/scripts/hooks/session-start.js +169 -82
- package/scripts/hooks/session-summary.js +78 -90
- package/skills/memesh/SKILL.md +108 -76
- package/README.es.md +0 -467
- package/README.fr.md +0 -459
- package/README.ja.md +0 -467
- package/README.ko.md +0 -467
- package/README.pt.md +0 -459
- package/README.th.md +0 -460
- package/README.vi.md +0 -459
- package/README.zh-CN.md +0 -466
- package/dist/cli/view.d.ts +0 -3
- package/dist/cli/view.d.ts.map +0 -1
- package/dist/cli/view.js +0 -523
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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)
|