akm-cli 0.9.0-beta.11 → 0.9.0-beta.26
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/CHANGELOG.md +163 -0
- package/dist/assets/prompts/consolidate-system.md +23 -0
- package/dist/assets/prompts/contradiction-judge.md +33 -0
- package/dist/assets/prompts/distill-knowledge-system.md +22 -0
- package/dist/assets/prompts/distill-lesson-system.md +36 -0
- package/dist/assets/prompts/extract-session.md +5 -1
- package/dist/assets/prompts/graph-extract-system.md +1 -0
- package/dist/assets/prompts/memory-infer-system.md +1 -0
- package/dist/assets/prompts/memory-infer-user.md +5 -0
- package/dist/assets/prompts/metadata-enhance-system.md +1 -0
- package/dist/assets/prompts/procedural-system.md +44 -0
- package/dist/assets/prompts/recombine-system.md +40 -0
- package/dist/assets/prompts/staleness-detect-system.md +6 -0
- package/dist/assets/prompts/validate-summary-judge.md +1 -0
- package/dist/assets/templates/html/health.html +25 -27
- package/dist/cli.js +2 -2
- package/dist/commands/agent/contribute-cli.js +16 -3
- package/dist/commands/feedback-cli.js +48 -44
- package/dist/commands/health/html-report.js +140 -16
- package/dist/commands/health.js +277 -1
- package/dist/commands/improve/calibration.js +161 -0
- package/dist/commands/improve/consolidate.js +595 -105
- package/dist/commands/improve/dedup.js +482 -0
- package/dist/commands/improve/distill.js +119 -64
- package/dist/commands/improve/encoding-salience.js +205 -0
- package/dist/commands/improve/extract-cli.js +115 -1
- package/dist/commands/improve/extract-prompt.js +32 -1
- package/dist/commands/improve/extract-watch.js +140 -0
- package/dist/commands/improve/extract.js +210 -30
- package/dist/commands/improve/feedback-valence.js +54 -0
- package/dist/commands/improve/homeostatic.js +467 -0
- package/dist/commands/improve/improve-auto-accept.js +80 -7
- package/dist/commands/improve/improve-profiles.js +8 -0
- package/dist/commands/improve/improve.js +991 -61
- package/dist/commands/improve/memory/memory-contradiction-detect.js +23 -28
- package/dist/commands/improve/outcome-loop.js +256 -0
- package/dist/commands/improve/proactive-maintenance.js +9 -35
- package/dist/commands/improve/procedural.js +409 -0
- package/dist/commands/improve/recombine.js +488 -0
- package/dist/commands/improve/reflect.js +20 -1
- package/dist/commands/improve/related-sessions.js +120 -0
- package/dist/commands/improve/salience.js +386 -0
- package/dist/commands/improve/triage.js +95 -0
- package/dist/commands/lint/agent-linter.js +19 -24
- package/dist/commands/lint/base-linter.js +173 -60
- package/dist/commands/lint/command-linter.js +19 -24
- package/dist/commands/lint/env-key-rules.js +34 -1
- package/dist/commands/lint/index.js +30 -13
- package/dist/commands/lint/memory-linter.js +1 -1
- package/dist/commands/lint/registry.js +5 -2
- package/dist/commands/lint/task-linter.js +3 -3
- package/dist/commands/lint/workflow-linter.js +26 -1
- package/dist/commands/proposal/validators/proposals.js +4 -0
- package/dist/commands/read/curate.js +284 -86
- package/dist/commands/read/search-cli.js +7 -0
- package/dist/commands/read/search.js +1 -0
- package/dist/commands/sources/installed-stashes.js +5 -1
- package/dist/core/asset/frontmatter.js +166 -167
- package/dist/core/asset/markdown.js +8 -0
- package/dist/core/config/config-schema.js +211 -3
- package/dist/core/config/config.js +2 -2
- package/dist/core/logs-db.js +4 -3
- package/dist/core/state-db.js +555 -29
- package/dist/indexer/db/db.js +250 -27
- package/dist/indexer/db/graph-db.js +81 -86
- package/dist/indexer/graph/graph-boost.js +51 -41
- package/dist/indexer/passes/memory-inference.js +10 -3
- package/dist/indexer/passes/staleness-detect.js +2 -5
- package/dist/indexer/search/db-search.js +15 -4
- package/dist/indexer/search/ranking.js +4 -0
- package/dist/integrations/harnesses/claude/session-log.js +10 -0
- package/dist/integrations/harnesses/opencode/session-log.js +9 -0
- package/dist/integrations/session-logs/index.js +16 -0
- package/dist/llm/embedder.js +27 -3
- package/dist/llm/embedders/local.js +66 -2
- package/dist/llm/graph-extract.js +2 -1
- package/dist/llm/memory-infer.js +4 -8
- package/dist/llm/metadata-enhance.js +9 -1
- package/dist/output/shapes/curate.js +14 -2
- package/dist/output/text/helpers.js +9 -0
- package/dist/runtime.js +25 -1
- package/dist/scripts/migrate-storage.js +1025 -567
- package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +435 -269
- package/dist/storage/sqlite-pragmas.js +146 -0
- package/dist/workflows/db.js +3 -4
- package/dist/workflows/validate-summary.js +2 -7
- package/docs/data-and-telemetry.md +1 -0
- package/package.json +5 -4
package/dist/indexer/db/db.js
CHANGED
|
@@ -12,13 +12,22 @@ import { warn } from "../../core/warn.js";
|
|
|
12
12
|
import { cosineSimilarity } from "../../llm/embedders/types.js";
|
|
13
13
|
import { sha256Hex } from "../../runtime.js";
|
|
14
14
|
import { openDatabase as openSqlite } from "../../storage/database.js";
|
|
15
|
+
import { applyStandardPragmas } from "../../storage/sqlite-pragmas.js";
|
|
15
16
|
import { buildSearchFields } from "../search/search-fields.js";
|
|
16
17
|
import { ensureUsageEventsSchema } from "../usage/usage-events.js";
|
|
17
18
|
import { backupDataDir, EMBEDDING_DIM_CHANGE_REASON } from "./db-backup.js";
|
|
18
19
|
// ── Constants ───────────────────────────────────────────────────────────────
|
|
20
|
+
// NOTE: do NOT bump DB_VERSION for graph-schema changes. A DB_VERSION mismatch
|
|
21
|
+
// triggers handleVersionUpgrade()'s NUCLEAR drop of the ENTIRE index — entries,
|
|
22
|
+
// embeddings, FTS, and the llm_enrichment_cache — forcing every user to re-embed
|
|
23
|
+
// their whole corpus on upgrade. The graph tables are derived and cheap to
|
|
24
|
+
// rebuild, so graph re-keying is migrated in a TARGETED, graph-only path
|
|
25
|
+
// (migrateGraphFilesSchema) that leaves entries + embeddings untouched.
|
|
19
26
|
export const DB_VERSION = 17;
|
|
20
27
|
export const EMBEDDING_DIM = 384;
|
|
21
|
-
|
|
28
|
+
// #624-P1: graph_files re-keyed to (stash_root, file_path, body_hash). Bumped 3→4
|
|
29
|
+
// as a marker; the actual migration is the targeted drop in migrateGraphFilesSchema.
|
|
30
|
+
export const GRAPH_SCHEMA_VERSION = 4;
|
|
22
31
|
// ── Database lifecycle ──────────────────────────────────────────────────────
|
|
23
32
|
export function openDatabase(dbPath, options) {
|
|
24
33
|
const resolvedPath = dbPath ?? getDbPath();
|
|
@@ -27,9 +36,7 @@ export function openDatabase(dbPath, options) {
|
|
|
27
36
|
fs.mkdirSync(dir, { recursive: true });
|
|
28
37
|
}
|
|
29
38
|
const db = openSqlite(resolvedPath);
|
|
30
|
-
db
|
|
31
|
-
db.exec("PRAGMA busy_timeout = 30000");
|
|
32
|
-
db.exec("PRAGMA foreign_keys = ON");
|
|
39
|
+
applyStandardPragmas(db, { dataDir: dir });
|
|
33
40
|
// Try to load sqlite-vec extension
|
|
34
41
|
loadVecExtension(db);
|
|
35
42
|
// Dim resolution: explicit option wins; otherwise consult the on-disk
|
|
@@ -67,10 +74,9 @@ function resolveConfiguredEmbeddingDim() {
|
|
|
67
74
|
}
|
|
68
75
|
export function openExistingDatabase(dbPath) {
|
|
69
76
|
const resolvedPath = dbPath ?? getDbPath();
|
|
77
|
+
const dir = path.dirname(resolvedPath);
|
|
70
78
|
const db = openSqlite(resolvedPath);
|
|
71
|
-
db
|
|
72
|
-
db.exec("PRAGMA busy_timeout = 30000");
|
|
73
|
-
db.exec("PRAGMA foreign_keys = ON");
|
|
79
|
+
applyStandardPragmas(db, { dataDir: dir });
|
|
74
80
|
// Existing-DB callers must not mutate schema or embedding metadata on open,
|
|
75
81
|
// but some paths still need write access to usage_events and other tables.
|
|
76
82
|
loadVecExtension(db);
|
|
@@ -299,15 +305,27 @@ function ensureSchema(db, embeddingDim, options) {
|
|
|
299
305
|
CREATE INDEX IF NOT EXISTS idx_llm_cache_updated
|
|
300
306
|
ON llm_enrichment_cache(updated_at);
|
|
301
307
|
`);
|
|
302
|
-
// Graph extraction tables — schema
|
|
308
|
+
// Graph extraction tables — schema v4 ((stash_root, file_path, body_hash) PK).
|
|
309
|
+
//
|
|
310
|
+
// graph_files is self-keyed on (stash_root, file_path, body_hash) and is NO
|
|
311
|
+
// LONGER tied to entries.id. This is the #624-P1 win: deleting and
|
|
312
|
+
// re-inserting an entries row during a reindex no longer cascade-wipes the
|
|
313
|
+
// extracted graph — as long as the file's body_hash is unchanged, the graph
|
|
314
|
+
// data survives. body_hash is part of the PK so a content change yields a
|
|
315
|
+
// distinct key; a UNIQUE index on (stash_root, file_path) still enforces
|
|
316
|
+
// exactly one graph_files row per path (delete-then-insert on a hash change).
|
|
303
317
|
//
|
|
304
|
-
//
|
|
305
|
-
//
|
|
306
|
-
//
|
|
307
|
-
// extractor's path-based upsert still works.
|
|
318
|
+
// graph_file_entities and graph_file_relations carry (stash_root, file_path,
|
|
319
|
+
// body_hash) and declare a composite FK -> graph_files ON DELETE CASCADE so
|
|
320
|
+
// child rows are removed when a graph_files row is replaced.
|
|
308
321
|
//
|
|
309
|
-
//
|
|
310
|
-
//
|
|
322
|
+
// #624-P1 targeted migration: an existing DB may still hold the OLD graph_files
|
|
323
|
+
// (entry_id PK). SQLite can't ALTER a primary key, so we RENAME the 3 graph
|
|
324
|
+
// tables aside (→ *_legacy) here — ONLY the graph tables, never the index/
|
|
325
|
+
// embeddings — then the CREATE block below builds the new shape, then
|
|
326
|
+
// migrateGraphDataFromLegacy() copies the data across so the graph is PRESERVED
|
|
327
|
+
// (not re-extracted).
|
|
328
|
+
migrateGraphFilesSchema(db);
|
|
311
329
|
db.exec(`
|
|
312
330
|
CREATE TABLE IF NOT EXISTS graph_meta (
|
|
313
331
|
stash_root TEXT PRIMARY KEY,
|
|
@@ -331,7 +349,6 @@ function ensureSchema(db, embeddingDim, options) {
|
|
|
331
349
|
);
|
|
332
350
|
|
|
333
351
|
CREATE TABLE IF NOT EXISTS graph_files (
|
|
334
|
-
entry_id INTEGER PRIMARY KEY REFERENCES entries(id) ON DELETE CASCADE,
|
|
335
352
|
stash_root TEXT NOT NULL,
|
|
336
353
|
file_path TEXT NOT NULL,
|
|
337
354
|
file_order INTEGER NOT NULL,
|
|
@@ -341,26 +358,34 @@ function ensureSchema(db, embeddingDim, options) {
|
|
|
341
358
|
status TEXT NOT NULL DEFAULT 'extracted',
|
|
342
359
|
reason TEXT,
|
|
343
360
|
extraction_run_id TEXT,
|
|
344
|
-
|
|
361
|
+
PRIMARY KEY (stash_root, file_path, body_hash)
|
|
345
362
|
);
|
|
346
363
|
|
|
364
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_graph_files_path
|
|
365
|
+
ON graph_files(stash_root, file_path);
|
|
366
|
+
|
|
347
367
|
CREATE INDEX IF NOT EXISTS idx_graph_files_stash_order
|
|
348
368
|
ON graph_files(stash_root, file_order);
|
|
349
369
|
|
|
350
370
|
CREATE TABLE IF NOT EXISTS graph_file_entities (
|
|
351
|
-
entry_id INTEGER NOT NULL REFERENCES graph_files(entry_id) ON DELETE CASCADE,
|
|
352
|
-
entity_order INTEGER NOT NULL,
|
|
353
371
|
stash_root TEXT NOT NULL,
|
|
372
|
+
file_path TEXT NOT NULL,
|
|
373
|
+
body_hash TEXT NOT NULL,
|
|
374
|
+
entity_order INTEGER NOT NULL,
|
|
354
375
|
entity_norm TEXT NOT NULL,
|
|
355
376
|
entity TEXT NOT NULL,
|
|
356
|
-
PRIMARY KEY (
|
|
377
|
+
PRIMARY KEY (stash_root, file_path, body_hash, entity_order),
|
|
378
|
+
FOREIGN KEY (stash_root, file_path, body_hash)
|
|
379
|
+
REFERENCES graph_files(stash_root, file_path, body_hash) ON DELETE CASCADE
|
|
357
380
|
);
|
|
358
381
|
|
|
359
382
|
CREATE INDEX IF NOT EXISTS idx_graph_file_entities_entity_norm
|
|
360
383
|
ON graph_file_entities(stash_root, entity_norm);
|
|
361
384
|
|
|
362
385
|
CREATE TABLE IF NOT EXISTS graph_file_relations (
|
|
363
|
-
|
|
386
|
+
stash_root TEXT NOT NULL,
|
|
387
|
+
file_path TEXT NOT NULL,
|
|
388
|
+
body_hash TEXT NOT NULL,
|
|
364
389
|
relation_order INTEGER NOT NULL,
|
|
365
390
|
from_entity_norm TEXT NOT NULL,
|
|
366
391
|
from_entity TEXT NOT NULL,
|
|
@@ -368,9 +393,15 @@ function ensureSchema(db, embeddingDim, options) {
|
|
|
368
393
|
to_entity TEXT NOT NULL,
|
|
369
394
|
relation_type TEXT,
|
|
370
395
|
confidence REAL,
|
|
371
|
-
PRIMARY KEY (
|
|
396
|
+
PRIMARY KEY (stash_root, file_path, body_hash, relation_order),
|
|
397
|
+
FOREIGN KEY (stash_root, file_path, body_hash)
|
|
398
|
+
REFERENCES graph_files(stash_root, file_path, body_hash) ON DELETE CASCADE
|
|
372
399
|
);
|
|
373
400
|
`);
|
|
401
|
+
// #624-P1 migration step 2: copy any renamed-aside legacy graph data into the
|
|
402
|
+
// new-shape tables (just created above), then drop the legacy tables. No-op
|
|
403
|
+
// unless migrateGraphFilesSchema renamed a legacy graph_files this open.
|
|
404
|
+
migrateGraphDataFromLegacy(db);
|
|
374
405
|
// FTS-dirty queue. Created here (not lazily on first upsert) so the
|
|
375
406
|
// per-entry write path doesn't issue a CREATE TABLE IF NOT EXISTS on
|
|
376
407
|
// every call — that DDL would fire thousands of times during a full
|
|
@@ -715,6 +746,110 @@ function ensureDerivedFromColumn(db) {
|
|
|
715
746
|
db.exec("CREATE INDEX IF NOT EXISTS idx_entries_derived_from ON entries(derived_from)");
|
|
716
747
|
}, "entries table may not exist on a brand-new DB before CREATE — caller is responsible");
|
|
717
748
|
}
|
|
749
|
+
/**
|
|
750
|
+
* Returns true when a table exists in the current database.
|
|
751
|
+
*/
|
|
752
|
+
function tableExists(db, name) {
|
|
753
|
+
const row = db.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name=? LIMIT 1").get(name);
|
|
754
|
+
return row !== undefined && row !== null;
|
|
755
|
+
}
|
|
756
|
+
/**
|
|
757
|
+
* #624-P1 targeted graph-schema migration — STEP 1 of 2 (rename).
|
|
758
|
+
*
|
|
759
|
+
* graph_files was re-keyed from `entry_id INTEGER PRIMARY KEY REFERENCES
|
|
760
|
+
* entries(id)` to a self-contained `(stash_root, file_path, body_hash)` PK.
|
|
761
|
+
* SQLite cannot ALTER a primary key, so an existing DB carrying the OLD shape
|
|
762
|
+
* has its 3 graph tables RENAMED to `*_legacy` here; ensureSchema's CREATE block
|
|
763
|
+
* then builds the new-shape tables, and {@link migrateGraphDataFromLegacy} COPIES
|
|
764
|
+
* the data across before dropping the legacy tables. The graph is preserved —
|
|
765
|
+
* NOT re-extracted (re-extraction is ~19s/file of LLM work).
|
|
766
|
+
*
|
|
767
|
+
* Crucially this is GRAPH-SCOPED: it touches ONLY the graph tables, never the
|
|
768
|
+
* index / embeddings / enrichment cache. So users keep their (expensive)
|
|
769
|
+
* embeddings instead of being forced into a full re-embed by a DB_VERSION bump.
|
|
770
|
+
*
|
|
771
|
+
* Detection: the old schema has an `entry_id` column on graph_files. Fresh DBs
|
|
772
|
+
* (no graph_files yet) and already-migrated DBs (no entry_id column) are no-ops.
|
|
773
|
+
* Idempotent.
|
|
774
|
+
*/
|
|
775
|
+
function migrateGraphFilesSchema(db) {
|
|
776
|
+
bestEffort(() => {
|
|
777
|
+
const cols = db.prepare("PRAGMA table_info(graph_files)").all();
|
|
778
|
+
const isLegacyShape = cols.some((c) => c.name === "entry_id");
|
|
779
|
+
if (!isLegacyShape)
|
|
780
|
+
return;
|
|
781
|
+
// A previous interrupted migration may have left *_legacy behind — drop those
|
|
782
|
+
// husks first so the rename below doesn't collide.
|
|
783
|
+
db.exec("DROP TABLE IF EXISTS graph_file_relations_legacy");
|
|
784
|
+
db.exec("DROP TABLE IF EXISTS graph_file_entities_legacy");
|
|
785
|
+
db.exec("DROP TABLE IF EXISTS graph_files_legacy");
|
|
786
|
+
// Rename the 3 entry_id-keyed tables aside. graph_meta is unchanged (stash_root
|
|
787
|
+
// key) so it is left in place. ALTER … RENAME auto-updates child FK refs in
|
|
788
|
+
// SQLite ≥3.25, which is fine — the legacy children are dropped after the copy.
|
|
789
|
+
db.exec("ALTER TABLE graph_files RENAME TO graph_files_legacy");
|
|
790
|
+
if (tableExists(db, "graph_file_entities")) {
|
|
791
|
+
db.exec("ALTER TABLE graph_file_entities RENAME TO graph_file_entities_legacy");
|
|
792
|
+
}
|
|
793
|
+
if (tableExists(db, "graph_file_relations")) {
|
|
794
|
+
db.exec("ALTER TABLE graph_file_relations RENAME TO graph_file_relations_legacy");
|
|
795
|
+
}
|
|
796
|
+
}, "graph_files may not exist on a brand-new DB before CREATE — caller is responsible");
|
|
797
|
+
}
|
|
798
|
+
/**
|
|
799
|
+
* #624-P1 targeted graph-schema migration — STEP 2 of 2 (copy + drop legacy).
|
|
800
|
+
*
|
|
801
|
+
* Runs AFTER the graph CREATE TABLE block, so the new-shape tables exist. Copies
|
|
802
|
+
* every legacy row into the re-keyed tables — the old tables already carry
|
|
803
|
+
* (stash_root, file_path, body_hash) next to entry_id, so the projection is a
|
|
804
|
+
* straight column copy (children JOIN back to graph_files_legacy to resolve the
|
|
805
|
+
* composite key from their entry_id). Then drops the `*_legacy` tables.
|
|
806
|
+
*
|
|
807
|
+
* Best-effort: a copy failure (e.g. a pre-body_hash legacy schema) is tolerated,
|
|
808
|
+
* and the legacy tables are dropped regardless so they never linger. Rows whose
|
|
809
|
+
* body_hash is null/empty can't form the new PK and are skipped (they re-extract).
|
|
810
|
+
*/
|
|
811
|
+
function migrateGraphDataFromLegacy(db) {
|
|
812
|
+
if (!tableExists(db, "graph_files_legacy"))
|
|
813
|
+
return;
|
|
814
|
+
let migratedFiles = 0;
|
|
815
|
+
bestEffort(() => {
|
|
816
|
+
db.transaction(() => {
|
|
817
|
+
const res = db
|
|
818
|
+
.prepare(`INSERT OR IGNORE INTO graph_files
|
|
819
|
+
(stash_root, file_path, body_hash, file_order, file_type, confidence, status, reason, extraction_run_id)
|
|
820
|
+
SELECT stash_root, file_path, body_hash, file_order, file_type, confidence, status, reason, extraction_run_id
|
|
821
|
+
FROM graph_files_legacy
|
|
822
|
+
WHERE body_hash IS NOT NULL AND body_hash != ''`)
|
|
823
|
+
.run();
|
|
824
|
+
migratedFiles = Number(res.changes);
|
|
825
|
+
if (tableExists(db, "graph_file_entities_legacy")) {
|
|
826
|
+
db.exec(`INSERT OR IGNORE INTO graph_file_entities
|
|
827
|
+
(stash_root, file_path, body_hash, entity_order, entity_norm, entity)
|
|
828
|
+
SELECT gf.stash_root, gf.file_path, gf.body_hash, e.entity_order, e.entity_norm, e.entity
|
|
829
|
+
FROM graph_file_entities_legacy e
|
|
830
|
+
JOIN graph_files_legacy gf ON gf.entry_id = e.entry_id
|
|
831
|
+
WHERE gf.body_hash IS NOT NULL AND gf.body_hash != ''`);
|
|
832
|
+
}
|
|
833
|
+
if (tableExists(db, "graph_file_relations_legacy")) {
|
|
834
|
+
db.exec(`INSERT OR IGNORE INTO graph_file_relations
|
|
835
|
+
(stash_root, file_path, body_hash, relation_order, from_entity_norm, from_entity, to_entity_norm, to_entity, relation_type, confidence)
|
|
836
|
+
SELECT gf.stash_root, gf.file_path, gf.body_hash, r.relation_order, r.from_entity_norm, r.from_entity, r.to_entity_norm, r.to_entity, r.relation_type, r.confidence
|
|
837
|
+
FROM graph_file_relations_legacy r
|
|
838
|
+
JOIN graph_files_legacy gf ON gf.entry_id = r.entry_id
|
|
839
|
+
WHERE gf.body_hash IS NOT NULL AND gf.body_hash != ''`);
|
|
840
|
+
}
|
|
841
|
+
})();
|
|
842
|
+
}, "graph data migration is best-effort; legacy tables are dropped regardless below");
|
|
843
|
+
// Always drop the legacy tables (children first), migrated or not.
|
|
844
|
+
bestEffort(() => {
|
|
845
|
+
db.exec("DROP TABLE IF EXISTS graph_file_relations_legacy");
|
|
846
|
+
db.exec("DROP TABLE IF EXISTS graph_file_entities_legacy");
|
|
847
|
+
db.exec("DROP TABLE IF EXISTS graph_files_legacy");
|
|
848
|
+
}, "drop legacy graph tables after migration");
|
|
849
|
+
if (migratedFiles > 0) {
|
|
850
|
+
warn(`[akm] graph index re-keyed (#624): migrated ${migratedFiles} extracted file(s) to the new schema — no re-extraction needed. Index + embeddings untouched.`);
|
|
851
|
+
}
|
|
852
|
+
}
|
|
718
853
|
/**
|
|
719
854
|
* Phase 5A / Advantage D5: look up the derived-memory child row whose
|
|
720
855
|
* `derived_from` column matches `parentRef` (e.g. `"memory:claude-prefs"`).
|
|
@@ -839,6 +974,38 @@ function deleteRelatedRows(db, ids) {
|
|
|
839
974
|
// Clean up usage events before deleting entries
|
|
840
975
|
bestEffort(() => db.prepare(`DELETE FROM usage_events WHERE entry_id IN (${placeholders})`).run(...chunk), "delete usage_events for entries");
|
|
841
976
|
}
|
|
977
|
+
// #624-P1: graph_files is NO LONGER keyed on entries.id, so deleting an
|
|
978
|
+
// entries row must NOT wipe the extracted graph (that is the whole point —
|
|
979
|
+
// the graph survives a reindex when body_hash is unchanged). We therefore do
|
|
980
|
+
// NOT delete graph_files here. We DO, however, recompute graph_meta counts
|
|
981
|
+
// for the stash roots touched by the deleted entries so the summary numbers
|
|
982
|
+
// stay consistent with the live child rows (the counts are derived, and the
|
|
983
|
+
// entries delete may have changed which files are considered/indexed).
|
|
984
|
+
//
|
|
985
|
+
// Resolve the affected stash roots from the entries rows BEFORE deletion.
|
|
986
|
+
const affectedStashRoots = new Set();
|
|
987
|
+
for (let i = 0; i < numericIds.length; i += SQLITE_CHUNK_SIZE) {
|
|
988
|
+
const chunk = numericIds.slice(i, i + SQLITE_CHUNK_SIZE);
|
|
989
|
+
const placeholders = chunk.map(() => "?").join(",");
|
|
990
|
+
bestEffort(() => {
|
|
991
|
+
const rows = db
|
|
992
|
+
.prepare(`SELECT DISTINCT stash_dir FROM entries WHERE id IN (${placeholders})`)
|
|
993
|
+
.all(...chunk);
|
|
994
|
+
for (const row of rows) {
|
|
995
|
+
if (row.stash_dir)
|
|
996
|
+
affectedStashRoots.add(row.stash_dir);
|
|
997
|
+
}
|
|
998
|
+
}, "resolve stash roots for graph_meta recompute");
|
|
999
|
+
}
|
|
1000
|
+
for (const stashRoot of affectedStashRoots) {
|
|
1001
|
+
bestEffort(() => db
|
|
1002
|
+
.prepare(`UPDATE graph_meta
|
|
1003
|
+
SET extracted_files = (SELECT COUNT(*) FROM graph_files WHERE stash_root = ?),
|
|
1004
|
+
entity_count = (SELECT COUNT(*) FROM graph_file_entities WHERE stash_root = ?),
|
|
1005
|
+
relation_count = (SELECT COUNT(*) FROM graph_file_relations WHERE stash_root = ?)
|
|
1006
|
+
WHERE stash_root = ?`)
|
|
1007
|
+
.run(stashRoot, stashRoot, stashRoot, stashRoot), "sync graph_meta counts after entries delete");
|
|
1008
|
+
}
|
|
842
1009
|
}
|
|
843
1010
|
/**
|
|
844
1011
|
* Delete entries by their primary key IDs, along with all related rows
|
|
@@ -1048,12 +1215,12 @@ function searchBlobVec(db, queryEmbedding, k) {
|
|
|
1048
1215
|
}
|
|
1049
1216
|
}
|
|
1050
1217
|
// ── FTS5 search ─────────────────────────────────────────────────────────────
|
|
1051
|
-
export function searchFts(db, query, limit, entryType) {
|
|
1218
|
+
export function searchFts(db, query, limit, entryType, excludeTypes) {
|
|
1052
1219
|
const ftsQuery = sanitizeFtsQuery(query);
|
|
1053
1220
|
if (!ftsQuery)
|
|
1054
1221
|
return [];
|
|
1055
1222
|
// Try the exact AND query first
|
|
1056
|
-
const exactResults = runFtsQuery(db, ftsQuery, limit, entryType);
|
|
1223
|
+
const exactResults = runFtsQuery(db, ftsQuery, limit, entryType, excludeTypes);
|
|
1057
1224
|
if (exactResults.length > 0)
|
|
1058
1225
|
return exactResults;
|
|
1059
1226
|
// Exact match returned zero results — try prefix fallback.
|
|
@@ -1063,7 +1230,7 @@ export function searchFts(db, query, limit, entryType) {
|
|
|
1063
1230
|
const prefixQuery = buildPrefixQuery(ftsQuery);
|
|
1064
1231
|
if (!prefixQuery)
|
|
1065
1232
|
return [];
|
|
1066
|
-
return runFtsQuery(db, prefixQuery, limit, entryType);
|
|
1233
|
+
return runFtsQuery(db, prefixQuery, limit, entryType, excludeTypes);
|
|
1067
1234
|
}
|
|
1068
1235
|
/**
|
|
1069
1236
|
* Build a prefix query from an FTS5 query string by appending `*` to each
|
|
@@ -1086,9 +1253,14 @@ function buildPrefixQuery(ftsQuery) {
|
|
|
1086
1253
|
return null;
|
|
1087
1254
|
return prefixTokens.join(" ");
|
|
1088
1255
|
}
|
|
1089
|
-
function runFtsQuery(db, ftsQuery, limit, entryType) {
|
|
1256
|
+
function runFtsQuery(db, ftsQuery, limit, entryType, excludeTypes) {
|
|
1090
1257
|
let sql;
|
|
1091
1258
|
let params;
|
|
1259
|
+
// #627 — exclude-type clause. Only applies on the untyped ('any') path; an
|
|
1260
|
+
// explicit include filter (entryType) already narrows to a single type, so
|
|
1261
|
+
// exclusion is redundant there. An empty list skips the clause entirely
|
|
1262
|
+
// (never emit `NOT IN ()`, which is a SQL error / always-false).
|
|
1263
|
+
const excludes = excludeTypes && excludeTypes.length > 0 ? excludeTypes : [];
|
|
1092
1264
|
// Join on integer entry_id directly (no CAST needed; we store integer)
|
|
1093
1265
|
// Use bm25() with per-column weights: entry_id(0), name(10), description(5), tags(3), hints(2), content(1)
|
|
1094
1266
|
if (entryType && entryType !== "any") {
|
|
@@ -1105,16 +1277,19 @@ function runFtsQuery(db, ftsQuery, limit, entryType) {
|
|
|
1105
1277
|
params = [ftsQuery, entryType, limit];
|
|
1106
1278
|
}
|
|
1107
1279
|
else {
|
|
1280
|
+
const excludeClause = excludes.length > 0 ? `AND e.entry_type NOT IN (${excludes.map(() => "?").join(", ")})` : "";
|
|
1108
1281
|
sql = `
|
|
1109
1282
|
SELECT e.id, e.file_path AS filePath, e.entry_json, e.search_text AS searchText,
|
|
1110
1283
|
bm25(entries_fts, 0, 10.0, 5.0, 3.0, 2.0, 1.0) AS bm25Score
|
|
1111
1284
|
FROM entries_fts f
|
|
1112
1285
|
JOIN entries e ON e.id = f.entry_id
|
|
1113
1286
|
WHERE entries_fts MATCH ?
|
|
1287
|
+
${excludeClause}
|
|
1114
1288
|
ORDER BY bm25Score, e.id ASC
|
|
1115
1289
|
LIMIT ?
|
|
1116
1290
|
`;
|
|
1117
|
-
|
|
1291
|
+
// Param order: MATCH, then the NOT IN values, then LIMIT.
|
|
1292
|
+
params = [ftsQuery, ...excludes, limit];
|
|
1118
1293
|
}
|
|
1119
1294
|
try {
|
|
1120
1295
|
const rows = db.prepare(sql).all(...params);
|
|
@@ -1183,14 +1358,21 @@ function parseEntryRows(rows, context) {
|
|
|
1183
1358
|
}
|
|
1184
1359
|
return entries;
|
|
1185
1360
|
}
|
|
1186
|
-
export function getAllEntries(db, entryType) {
|
|
1361
|
+
export function getAllEntries(db, entryType, excludeTypes) {
|
|
1187
1362
|
let sql;
|
|
1188
1363
|
let params;
|
|
1364
|
+
// #627 — exclude-type clause applies only on the untyped ('any') path. Empty
|
|
1365
|
+
// list skips the clause (never `NOT IN ()`).
|
|
1366
|
+
const excludes = excludeTypes && excludeTypes.length > 0 ? excludeTypes : [];
|
|
1189
1367
|
if (entryType && entryType !== "any") {
|
|
1190
1368
|
sql =
|
|
1191
1369
|
"SELECT id, entry_key, dir_path, file_path, stash_dir, entry_json, search_text FROM entries WHERE entry_type = ?";
|
|
1192
1370
|
params = [entryType];
|
|
1193
1371
|
}
|
|
1372
|
+
else if (excludes.length > 0) {
|
|
1373
|
+
sql = `SELECT id, entry_key, dir_path, file_path, stash_dir, entry_json, search_text FROM entries WHERE entry_type NOT IN (${excludes.map(() => "?").join(", ")})`;
|
|
1374
|
+
params = [...excludes];
|
|
1375
|
+
}
|
|
1194
1376
|
else {
|
|
1195
1377
|
sql = "SELECT id, entry_key, dir_path, file_path, stash_dir, entry_json, search_text FROM entries";
|
|
1196
1378
|
params = [];
|
|
@@ -1198,6 +1380,47 @@ export function getAllEntries(db, entryType) {
|
|
|
1198
1380
|
const rows = db.prepare(sql).all(...params);
|
|
1199
1381
|
return parseEntryRows(rows, "getAllEntries");
|
|
1200
1382
|
}
|
|
1383
|
+
/**
|
|
1384
|
+
* #609 — read graph entities (normalized) for a set of entry ids. Used by the
|
|
1385
|
+
* recombine pass to cluster memories by shared graph entity ("graph"
|
|
1386
|
+
* relatedness source). Returns a map of `entry_id -> entity_norm[]`. Entries
|
|
1387
|
+
* with no graph entities (graph extraction has not run, or the file produced
|
|
1388
|
+
* no entities) are simply absent from the map — callers must fail open
|
|
1389
|
+
* (fall back to tag relatedness) when the map is empty.
|
|
1390
|
+
*/
|
|
1391
|
+
export function getEntitiesByEntryIds(db, entryIds) {
|
|
1392
|
+
const result = new Map();
|
|
1393
|
+
if (entryIds.length === 0)
|
|
1394
|
+
return result;
|
|
1395
|
+
// #624-P1: graph_file_entities no longer carries entry_id. Re-derive the
|
|
1396
|
+
// entry_id -> entity_norm[] contract by JOINing through entries on
|
|
1397
|
+
// (stash_dir, file_path) -> graph_files. Chunk the IN(?) list because the
|
|
1398
|
+
// recombine pass can pass 10k+ entry ids (well over the SQLite param limit).
|
|
1399
|
+
for (let i = 0; i < entryIds.length; i += SQLITE_CHUNK_SIZE) {
|
|
1400
|
+
const chunk = entryIds.slice(i, i + SQLITE_CHUNK_SIZE);
|
|
1401
|
+
const placeholders = chunk.map(() => "?").join(", ");
|
|
1402
|
+
const rows = db
|
|
1403
|
+
.prepare(`SELECT e.id AS entry_id, gfe.entity_norm AS entity_norm
|
|
1404
|
+
FROM entries e
|
|
1405
|
+
JOIN graph_files gf
|
|
1406
|
+
ON gf.stash_root = e.stash_dir AND gf.file_path = e.file_path
|
|
1407
|
+
JOIN graph_file_entities gfe
|
|
1408
|
+
ON gfe.stash_root = gf.stash_root
|
|
1409
|
+
AND gfe.file_path = gf.file_path
|
|
1410
|
+
AND gfe.body_hash = gf.body_hash
|
|
1411
|
+
WHERE e.id IN (${placeholders})
|
|
1412
|
+
ORDER BY e.id, gfe.entity_order`)
|
|
1413
|
+
.all(...chunk);
|
|
1414
|
+
for (const row of rows) {
|
|
1415
|
+
const list = result.get(row.entry_id);
|
|
1416
|
+
if (list)
|
|
1417
|
+
list.push(row.entity_norm);
|
|
1418
|
+
else
|
|
1419
|
+
result.set(row.entry_id, [row.entity_norm]);
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
return result;
|
|
1423
|
+
}
|
|
1201
1424
|
export function findEntryIdByRef(db, ref) {
|
|
1202
1425
|
const parsed = parseAssetRef(ref);
|
|
1203
1426
|
const nameVariants = [parsed.name];
|