@kybernesis/brain-storage-sqlite 0.8.4 → 0.10.0
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/dist/hash.d.ts +9 -0
- package/dist/hash.d.ts.map +1 -0
- package/dist/hash.js +12 -0
- package/dist/hash.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +32 -0
- package/dist/index.js.map +1 -1
- package/dist/migrations/backup.d.ts +14 -0
- package/dist/migrations/backup.d.ts.map +1 -0
- package/dist/migrations/backup.js +20 -0
- package/dist/migrations/backup.js.map +1 -0
- package/dist/migrations/config.d.ts +14 -0
- package/dist/migrations/config.d.ts.map +1 -0
- package/dist/migrations/config.js +24 -0
- package/dist/migrations/config.js.map +1 -0
- package/dist/migrations/errors.d.ts +24 -0
- package/dist/migrations/errors.d.ts.map +1 -0
- package/dist/migrations/errors.js +45 -0
- package/dist/migrations/errors.js.map +1 -0
- package/dist/migrations/registry.d.ts +15 -0
- package/dist/migrations/registry.d.ts.map +1 -0
- package/dist/migrations/registry.js +34 -0
- package/dist/migrations/registry.js.map +1 -0
- package/dist/migrations/runner.d.ts +57 -0
- package/dist/migrations/runner.d.ts.map +1 -0
- package/dist/migrations/runner.js +141 -0
- package/dist/migrations/runner.js.map +1 -0
- package/dist/migrations/timeline/002-file-provenance.d.ts +24 -0
- package/dist/migrations/timeline/002-file-provenance.d.ts.map +1 -0
- package/dist/migrations/timeline/002-file-provenance.js +43 -0
- package/dist/migrations/timeline/002-file-provenance.js.map +1 -0
- package/dist/migrations/timeline/002-proof.d.ts +13 -0
- package/dist/migrations/timeline/002-proof.d.ts.map +1 -0
- package/dist/migrations/timeline/002-proof.js +16 -0
- package/dist/migrations/timeline/002-proof.js.map +1 -0
- package/dist/migrations/types.d.ts +52 -0
- package/dist/migrations/types.d.ts.map +1 -0
- package/dist/migrations/types.js +15 -0
- package/dist/migrations/types.js.map +1 -0
- package/dist/provider.d.ts +13 -3
- package/dist/provider.d.ts.map +1 -1
- package/dist/provider.js +9 -57
- package/dist/provider.js.map +1 -1
- package/dist/reconcile.d.ts +76 -0
- package/dist/reconcile.d.ts.map +1 -0
- package/dist/reconcile.js +120 -0
- package/dist/reconcile.js.map +1 -0
- package/dist/timeline.d.ts.map +1 -1
- package/dist/timeline.js +67 -10
- package/dist/timeline.js.map +1 -1
- package/dist/vec-lazy.d.ts +19 -0
- package/dist/vec-lazy.d.ts.map +1 -0
- package/dist/vec-lazy.js +76 -0
- package/dist/vec-lazy.js.map +1 -0
- package/package.json +5 -4
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spec B (docs/specs/file-provenance-and-reconcile.md): file provenance.
|
|
3
|
+
*
|
|
4
|
+
* Adds the mutable LOCATOR (`file_path`, relative path, type='file' rows only)
|
|
5
|
+
* and the VERSION fingerprint (`content_hash` = sha256 of raw file bytes) to
|
|
6
|
+
* timeline_events. Stable identity (random source_path / chunk origin_id) is
|
|
7
|
+
* deliberately untouched — separating identity from locator is what makes a
|
|
8
|
+
* rename a one-row UPDATE.
|
|
9
|
+
*
|
|
10
|
+
* The unique index is PARTIAL: conversation rows keep file_path NULL in any
|
|
11
|
+
* number; non-NULL paths are unique (the dual-key upsert rides on it — proven
|
|
12
|
+
* in partial-upsert.spike.test.ts on better-sqlite3 + libsql).
|
|
13
|
+
*
|
|
14
|
+
* Backfill stamps file_path from the legacy `title` ("… File: <relpath>") —
|
|
15
|
+
* in-DB only, no file reads; content_hash stays NULL on legacy rows (the first
|
|
16
|
+
* reconcile run stamps it lazily). DEDUPE-AWARE: legacy brains can hold
|
|
17
|
+
* duplicate rows for the same file (the pile-up bug Spec B fixes) — stamping
|
|
18
|
+
* all of them would violate the new unique index and roll back the whole
|
|
19
|
+
* migration, so only the NEWEST row (MAX(id)) per title gets the locator;
|
|
20
|
+
* older duplicates stay NULL and remain reachable by source_path.
|
|
21
|
+
*/
|
|
22
|
+
export const migration = {
|
|
23
|
+
id: '002-file-provenance',
|
|
24
|
+
db: 'timeline',
|
|
25
|
+
statements: [
|
|
26
|
+
`ALTER TABLE timeline_events ADD COLUMN file_path TEXT`,
|
|
27
|
+
`ALTER TABLE timeline_events ADD COLUMN content_hash TEXT`,
|
|
28
|
+
`CREATE UNIQUE INDEX IF NOT EXISTS idx_timeline_file_path
|
|
29
|
+
ON timeline_events(file_path) WHERE file_path IS NOT NULL`,
|
|
30
|
+
],
|
|
31
|
+
backfill(db) {
|
|
32
|
+
db.exec(`
|
|
33
|
+
UPDATE timeline_events
|
|
34
|
+
SET file_path = substr(title, instr(title, 'File: ') + 6)
|
|
35
|
+
WHERE id IN (
|
|
36
|
+
SELECT MAX(id) FROM timeline_events
|
|
37
|
+
WHERE type = 'file' AND file_path IS NULL AND title LIKE '%File: %'
|
|
38
|
+
GROUP BY title
|
|
39
|
+
)
|
|
40
|
+
`);
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
//# sourceMappingURL=002-file-provenance.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"002-file-provenance.js","sourceRoot":"","sources":["../../../src/migrations/timeline/002-file-provenance.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAIH,MAAM,CAAC,MAAM,SAAS,GAAc;IAClC,EAAE,EAAE,qBAAqB;IACzB,EAAE,EAAE,UAAU;IACd,UAAU,EAAE;QACV,uDAAuD;QACvD,0DAA0D;QAC1D;iEAC6D;KAC9D;IACD,QAAQ,CAAC,EAAE;QACT,EAAE,CAAC,IAAI,CAAC;;;;;;;;KAQP,CAAC,CAAC;IACL,CAAC;CACF,CAAC"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Proof migration (Spec A T8 / C9): a trivial additive column purely to
|
|
3
|
+
* exercise the runner end-to-end against a copy of a real brain.
|
|
4
|
+
*
|
|
5
|
+
* Deliberately NOT in the default registry — shipping a probe column to every
|
|
6
|
+
* consumer has no value. The real-brain regression test injects it via
|
|
7
|
+
* `opts.registry`; the first SHIPPED migration will be Spec B's provenance
|
|
8
|
+
* columns (which must also update the matching ensure*Schema CREATE body —
|
|
9
|
+
* see registry.ts).
|
|
10
|
+
*/
|
|
11
|
+
import type { Migration } from '../types.js';
|
|
12
|
+
export declare const proofMigration: Migration;
|
|
13
|
+
//# sourceMappingURL=002-proof.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"002-proof.d.ts","sourceRoot":"","sources":["../../../src/migrations/timeline/002-proof.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAE7C,eAAO,MAAM,cAAc,EAAE,SAI5B,CAAC"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Proof migration (Spec A T8 / C9): a trivial additive column purely to
|
|
3
|
+
* exercise the runner end-to-end against a copy of a real brain.
|
|
4
|
+
*
|
|
5
|
+
* Deliberately NOT in the default registry — shipping a probe column to every
|
|
6
|
+
* consumer has no value. The real-brain regression test injects it via
|
|
7
|
+
* `opts.registry`; the first SHIPPED migration will be Spec B's provenance
|
|
8
|
+
* columns (which must also update the matching ensure*Schema CREATE body —
|
|
9
|
+
* see registry.ts).
|
|
10
|
+
*/
|
|
11
|
+
export const proofMigration = {
|
|
12
|
+
id: '002-proof',
|
|
13
|
+
db: 'timeline',
|
|
14
|
+
statements: ['ALTER TABLE timeline_events ADD COLUMN _migration_probe TEXT'],
|
|
15
|
+
};
|
|
16
|
+
//# sourceMappingURL=002-proof.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"002-proof.js","sourceRoot":"","sources":["../../../src/migrations/timeline/002-proof.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAIH,MAAM,CAAC,MAAM,cAAc,GAAc;IACvC,EAAE,EAAE,WAAW;IACf,EAAE,EAAE,UAAU;IACd,UAAU,EAAE,CAAC,8DAA8D,CAAC;CAC7E,CAAC"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schema migration types (Spec A — docs/specs/migration-capability.md).
|
|
3
|
+
*
|
|
4
|
+
* A migration is a small, named, immutable-once-shipped module. Its DDL is
|
|
5
|
+
* DECLARATIVE (`statements`), not an imperative `up(db)` — the runner executes
|
|
6
|
+
* the statements in order. Declarative SQL keeps the checksum honest (we hash
|
|
7
|
+
* the normalized SQL, not JS source) and stays dialect-portable for a future
|
|
8
|
+
* libsql/Turso backend (Arcana).
|
|
9
|
+
*
|
|
10
|
+
* MVP constraint: statements must be ADDITIVE (`ALTER TABLE ... ADD COLUMN`,
|
|
11
|
+
* `CREATE TABLE/INDEX IF NOT EXISTS`) — no drop/rewrite. Table-rebuild
|
|
12
|
+
* migrations are out of MVP scope.
|
|
13
|
+
*/
|
|
14
|
+
import type Database from 'better-sqlite3';
|
|
15
|
+
/**
|
|
16
|
+
* The four brain DBs that carry migrations. Subset of `DbKind` in index.ts
|
|
17
|
+
* (same spelling — `entityGraph`, not `entity-graph`); `idempotency` is a
|
|
18
|
+
* cache-shaped DB with no migration line.
|
|
19
|
+
*/
|
|
20
|
+
export type MigrationKind = 'timeline' | 'entityGraph' | 'sleep' | 'vectors';
|
|
21
|
+
export interface Migration {
|
|
22
|
+
/** Ordered + named, e.g. '002-add-file-provenance'. NEVER edit after release (checksum guards it). */
|
|
23
|
+
readonly id: string;
|
|
24
|
+
/** Which DB file this migration belongs to (each carries its own _migrations table). */
|
|
25
|
+
readonly db: MigrationKind;
|
|
26
|
+
/** Plain-SQLite DDL, executed in order inside the migration transaction. Checksummed. */
|
|
27
|
+
readonly statements: readonly string[];
|
|
28
|
+
/**
|
|
29
|
+
* Marks the no-op baseline marker (one per kind, by convention `001-baseline`).
|
|
30
|
+
* On a LEGACY unversioned DB (sentinel table present, no `_migrations`), only
|
|
31
|
+
* baseline-flagged entries are stamped as applied without running.
|
|
32
|
+
*/
|
|
33
|
+
readonly baseline?: boolean;
|
|
34
|
+
/** Optional data backfill — idempotent, resumable; runs after `statements` in the same txn. */
|
|
35
|
+
backfill?(db: Database.Database): void;
|
|
36
|
+
}
|
|
37
|
+
export interface MigrationOptions {
|
|
38
|
+
/**
|
|
39
|
+
* 'apply' — advance schema (local consumers: KBDE / KyberBot via autoMigrate;
|
|
40
|
+
* Arcana's migrator job per-tenant).
|
|
41
|
+
* 'check' — never write; throw SchemaBehindError on pending (Arcana serving path).
|
|
42
|
+
* Refuse-when-ahead applies in BOTH modes.
|
|
43
|
+
*/
|
|
44
|
+
mode: 'apply' | 'check';
|
|
45
|
+
/**
|
|
46
|
+
* Invoked once before the first pending migration applies (apply mode only).
|
|
47
|
+
* Local: copyFileBackup (WAL checkpoint + `<name>.pre-<firstPendingId>.bak`).
|
|
48
|
+
* Cloud: snapshot hook or no-op.
|
|
49
|
+
*/
|
|
50
|
+
backup?: (db: Database.Database, firstPendingId: string) => void;
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/migrations/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,QAAQ,MAAM,gBAAgB,CAAC;AAE3C;;;;GAIG;AACH,MAAM,MAAM,aAAa,GAAG,UAAU,GAAG,aAAa,GAAG,OAAO,GAAG,SAAS,CAAC;AAE7E,MAAM,WAAW,SAAS;IACxB,sGAAsG;IACtG,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,wFAAwF;IACxF,QAAQ,CAAC,EAAE,EAAE,aAAa,CAAC;IAC3B,yFAAyF;IACzF,QAAQ,CAAC,UAAU,EAAE,SAAS,MAAM,EAAE,CAAC;IACvC;;;;OAIG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;IAC5B,+FAA+F;IAC/F,QAAQ,CAAC,CAAC,EAAE,EAAE,QAAQ,CAAC,QAAQ,GAAG,IAAI,CAAC;CACxC;AAED,MAAM,WAAW,gBAAgB;IAC/B;;;;;OAKG;IACH,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC;IACxB;;;;OAIG;IACH,MAAM,CAAC,EAAE,CAAC,EAAE,EAAE,QAAQ,CAAC,QAAQ,EAAE,cAAc,EAAE,MAAM,KAAK,IAAI,CAAC;CAClE"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schema migration types (Spec A — docs/specs/migration-capability.md).
|
|
3
|
+
*
|
|
4
|
+
* A migration is a small, named, immutable-once-shipped module. Its DDL is
|
|
5
|
+
* DECLARATIVE (`statements`), not an imperative `up(db)` — the runner executes
|
|
6
|
+
* the statements in order. Declarative SQL keeps the checksum honest (we hash
|
|
7
|
+
* the normalized SQL, not JS source) and stays dialect-portable for a future
|
|
8
|
+
* libsql/Turso backend (Arcana).
|
|
9
|
+
*
|
|
10
|
+
* MVP constraint: statements must be ADDITIVE (`ALTER TABLE ... ADD COLUMN`,
|
|
11
|
+
* `CREATE TABLE/INDEX IF NOT EXISTS`) — no drop/rewrite. Table-rebuild
|
|
12
|
+
* migrations are out of MVP scope.
|
|
13
|
+
*/
|
|
14
|
+
export {};
|
|
15
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/migrations/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG"}
|
package/dist/provider.d.ts
CHANGED
|
@@ -10,7 +10,17 @@
|
|
|
10
10
|
* corresponding brain-core module; behaviour is held constant.
|
|
11
11
|
*/
|
|
12
12
|
import type { StorageProvider } from '@kybernesis/brain-contracts';
|
|
13
|
-
|
|
14
|
-
export
|
|
15
|
-
export
|
|
13
|
+
import type { MigrationOptions } from './migrations/types.js';
|
|
14
|
+
export { resetLazyVectorStore } from './vec-lazy.js';
|
|
15
|
+
export interface SqliteStorageProviderOptions {
|
|
16
|
+
/**
|
|
17
|
+
* Migration trigger (Spec A) — consumer-chosen, off when omitted.
|
|
18
|
+
* `true` → local default `{ mode:'apply', backup: copyFileBackup }` (KBDE /
|
|
19
|
+
* KyberBot: self-heal on open). Cloud serving (Arcana) passes
|
|
20
|
+
* `{ mode:'check' }` — refuse ahead AND behind, never write; its migrator
|
|
21
|
+
* job calls `runMigrations` standalone per tenant.
|
|
22
|
+
*/
|
|
23
|
+
autoMigrate?: boolean | MigrationOptions;
|
|
24
|
+
}
|
|
25
|
+
export declare function createSqliteStorageProvider(options?: SqliteStorageProviderOptions): StorageProvider;
|
|
16
26
|
//# sourceMappingURL=provider.d.ts.map
|
package/dist/provider.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../src/provider.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../src/provider.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAC;AAEnE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAa9D,OAAO,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC;AAErD,MAAM,WAAW,4BAA4B;IAC3C;;;;;;OAMG;IACH,WAAW,CAAC,EAAE,OAAO,GAAG,gBAAgB,CAAC;CAC1C;AAED,wBAAgB,2BAA2B,CACzC,OAAO,GAAE,4BAAiC,GACzC,eAAe,CAoBjB"}
|
package/dist/provider.js
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* The SQL that lands here is moved verbatim (port-faithful) from the
|
|
10
10
|
* corresponding brain-core module; behaviour is held constant.
|
|
11
11
|
*/
|
|
12
|
+
import { setAutoMigrate } from './migrations/config.js';
|
|
12
13
|
import { sqliteFactStore } from './fact-store.js';
|
|
13
14
|
import { sqliteTimelineStore } from './timeline.js';
|
|
14
15
|
import { sqliteEntityGraphStore } from './entity-graph.js';
|
|
@@ -16,64 +17,15 @@ import { sqliteSleepStore } from './sleep-store.js';
|
|
|
16
17
|
import { sqliteSleepMaintenance } from './sleep-maintenance.js';
|
|
17
18
|
import { sqliteSearchQueries } from './search-queries.js';
|
|
18
19
|
import { sqliteFactRetrievalQueries } from './fact-retrieval-queries.js';
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
let _vecAttempted = false;
|
|
28
|
-
async function loadVecStore() {
|
|
29
|
-
if (_vecAttempted)
|
|
30
|
-
return _vecStore;
|
|
31
|
-
_vecAttempted = true;
|
|
32
|
-
try {
|
|
33
|
-
const mod = await import('@kybernesis/brain-storage-vec');
|
|
34
|
-
_vecStore = mod.sqliteVecStore;
|
|
20
|
+
// Lazy vector store lives in vec-lazy.ts (Spec B: timeline.ts also needs it
|
|
21
|
+
// for cluster cleanup, and importing provider.ts from timeline.ts would close
|
|
22
|
+
// the documented eval-order cycle). Re-exported here for API compatibility.
|
|
23
|
+
import { lazyVectorStore } from './vec-lazy.js';
|
|
24
|
+
export { resetLazyVectorStore } from './vec-lazy.js';
|
|
25
|
+
export function createSqliteStorageProvider(options = {}) {
|
|
26
|
+
if (options.autoMigrate !== undefined) {
|
|
27
|
+
setAutoMigrate(options.autoMigrate);
|
|
35
28
|
}
|
|
36
|
-
catch {
|
|
37
|
-
_vecStore = null;
|
|
38
|
-
}
|
|
39
|
-
return _vecStore;
|
|
40
|
-
}
|
|
41
|
-
/** Reset the lazy vec-store cache (test/utility). */
|
|
42
|
-
export function resetLazyVectorStore() {
|
|
43
|
-
_vecStore = null;
|
|
44
|
-
_vecAttempted = false;
|
|
45
|
-
}
|
|
46
|
-
const lazyVectorStore = {
|
|
47
|
-
async available(t) {
|
|
48
|
-
const s = await loadVecStore();
|
|
49
|
-
return s ? s.available(t) : false;
|
|
50
|
-
},
|
|
51
|
-
async hasOrigin(t, originId) {
|
|
52
|
-
const s = await loadVecStore();
|
|
53
|
-
if (!s)
|
|
54
|
-
return false;
|
|
55
|
-
return s.hasOrigin(t, originId);
|
|
56
|
-
},
|
|
57
|
-
async insertChunk(t, chunk) {
|
|
58
|
-
const s = await loadVecStore();
|
|
59
|
-
if (!s)
|
|
60
|
-
throw new Error('[brain-storage-sqlite] vector backend unavailable');
|
|
61
|
-
return s.insertChunk(t, chunk);
|
|
62
|
-
},
|
|
63
|
-
async search(t, embedding, limit) {
|
|
64
|
-
const s = await loadVecStore();
|
|
65
|
-
if (!s)
|
|
66
|
-
return [];
|
|
67
|
-
return s.search(t, embedding, limit);
|
|
68
|
-
},
|
|
69
|
-
async stats(t) {
|
|
70
|
-
const s = await loadVecStore();
|
|
71
|
-
if (!s)
|
|
72
|
-
return { count: 0, available: false };
|
|
73
|
-
return s.stats(t);
|
|
74
|
-
},
|
|
75
|
-
};
|
|
76
|
-
export function createSqliteStorageProvider() {
|
|
77
29
|
return {
|
|
78
30
|
repositories: {
|
|
79
31
|
timeline: sqliteTimelineStore,
|
package/dist/provider.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"provider.js","sourceRoot":"","sources":["../src/provider.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;
|
|
1
|
+
{"version":3,"file":"provider.js","sourceRoot":"","sources":["../src/provider.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAGH,OAAO,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAExD,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAClD,OAAO,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AACpD,OAAO,EAAE,sBAAsB,EAAE,MAAM,mBAAmB,CAAC;AAC3D,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,EAAE,sBAAsB,EAAE,MAAM,wBAAwB,CAAC;AAChE,OAAO,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAC1D,OAAO,EAAE,0BAA0B,EAAE,MAAM,6BAA6B,CAAC;AAEzE,4EAA4E;AAC5E,8EAA8E;AAC9E,4EAA4E;AAC5E,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC;AAarD,MAAM,UAAU,2BAA2B,CACzC,UAAwC,EAAE;IAE1C,IAAI,OAAO,CAAC,WAAW,KAAK,SAAS,EAAE,CAAC;QACtC,cAAc,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC;IACtC,CAAC;IACD,OAAO;QACL,YAAY,EAAE;YACZ,QAAQ,EAAE,mBAAmB;YAC7B,QAAQ,EAAE,sBAAsB;YAChC,KAAK,EAAE,eAAe;YACtB,KAAK,EAAE,gBAAgB;YACvB,OAAO,EAAE,eAAe;SACzB;QACD,gFAAgF;QAChF,MAAM,EAAE;YACN,MAAM,EAAE,mBAAmB,EAAE,KAAK;YAClC,aAAa,EAAE,0BAA0B,EAAE,KAAK;YAChD,KAAK,EAAE,sBAAsB,EAAE,OAAO;SAEvC;KACF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reconcile (Spec B) — diff the brain against the filesystem and converge.
|
|
3
|
+
* This is the capability that catches the 42-files class: files marked
|
|
4
|
+
* "synced" but silently absent from the brain.
|
|
5
|
+
*
|
|
6
|
+
* REPORT-ONLY by default: a plain run returns structured findings and makes no
|
|
7
|
+
* destructive change. The one write a report run performs is the documented
|
|
8
|
+
* lazy baseline (resolved decision #2): a legacy row with NULL content_hash
|
|
9
|
+
* whose file exists on disk gets the CURRENT file's hash stamped and is
|
|
10
|
+
* classified up-to-date — trusting the present file as baseline avoids a full
|
|
11
|
+
* re-embed of every legacy row.
|
|
12
|
+
*
|
|
13
|
+
* Classification:
|
|
14
|
+
* missing — on disk, no brain row (→ repair: re-ingest via callback)
|
|
15
|
+
* stale — both, hashes differ (→ repair: re-ingest via callback)
|
|
16
|
+
* orphaned — brain row, no file on disk (→ removeOrphaned: delete cluster)
|
|
17
|
+
* renamed — a missing path whose hash equals an orphaned row's hash
|
|
18
|
+
* (→ repair: ONE file_path UPDATE — cluster, enrichment, and
|
|
19
|
+
* embeddings all survive; no re-ingest, no re-embed)
|
|
20
|
+
*
|
|
21
|
+
* Rename-and-edit is NOT detected (hashes differ) — it degrades to the
|
|
22
|
+
* missing + orphaned pair, i.e. add + remove. Documented limitation.
|
|
23
|
+
*
|
|
24
|
+
* Re-ingest is delegated to `opts.reingest` because ingestion is brain-core's
|
|
25
|
+
* pipeline (LLM extraction + embedding) and the dependency points the other
|
|
26
|
+
* way. The consumer (KBDE/KyberBot sweep, Arcana job) wires storeConversation
|
|
27
|
+
* into it — see the spec's consumer-integration section.
|
|
28
|
+
*
|
|
29
|
+
* Filesystem truth = the walked roots; the watched-folders manifest is never
|
|
30
|
+
* consulted. Dotfiles and dot-directories are skipped.
|
|
31
|
+
*/
|
|
32
|
+
import type { TenantContext } from '@kybernesis/brain-contracts';
|
|
33
|
+
export interface ReconcileDiskFile {
|
|
34
|
+
file_path: string;
|
|
35
|
+
absolute_path: string;
|
|
36
|
+
content_hash: string;
|
|
37
|
+
}
|
|
38
|
+
export interface ReconcileReport {
|
|
39
|
+
missing: ReconcileDiskFile[];
|
|
40
|
+
stale: Array<ReconcileDiskFile & {
|
|
41
|
+
id: number;
|
|
42
|
+
brain_hash: string;
|
|
43
|
+
}>;
|
|
44
|
+
orphaned: Array<{
|
|
45
|
+
id: number;
|
|
46
|
+
file_path: string;
|
|
47
|
+
content_hash: string | null;
|
|
48
|
+
}>;
|
|
49
|
+
renamed: Array<{
|
|
50
|
+
id: number;
|
|
51
|
+
from: string;
|
|
52
|
+
to: string;
|
|
53
|
+
}>;
|
|
54
|
+
/** Legacy NULL-hash rows stamped with the current file's hash this run. */
|
|
55
|
+
baselined: number;
|
|
56
|
+
/** Files seen on disk across all roots. */
|
|
57
|
+
scanned: number;
|
|
58
|
+
/** Actions actually performed (empty unless repair/removeOrphaned). */
|
|
59
|
+
repaired: {
|
|
60
|
+
relabelled: number;
|
|
61
|
+
reingested: number;
|
|
62
|
+
removed: number;
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
export interface ReconcileOptions {
|
|
66
|
+
/** Filesystem roots to walk; brain file_paths are relative to one of these. */
|
|
67
|
+
roots: string[];
|
|
68
|
+
/** Apply renames (relabel) and re-ingest missing/stale via `reingest`. */
|
|
69
|
+
repair?: boolean;
|
|
70
|
+
/** Delete orphaned rows + their clusters. Destructive — explicitly opt-in. */
|
|
71
|
+
removeOrphaned?: boolean;
|
|
72
|
+
/** Consumer-provided ingest (brain-core storeConversation wired by the consumer). */
|
|
73
|
+
reingest?: (file: ReconcileDiskFile) => Promise<void>;
|
|
74
|
+
}
|
|
75
|
+
export declare function reconcile(t: TenantContext, opts: ReconcileOptions): Promise<ReconcileReport>;
|
|
76
|
+
//# sourceMappingURL=reconcile.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reconcile.d.ts","sourceRoot":"","sources":["../src/reconcile.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAIH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAKjE,MAAM,WAAW,iBAAiB;IAChC,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,iBAAiB,EAAE,CAAC;IAC7B,KAAK,EAAE,KAAK,CAAC,iBAAiB,GAAG;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACrE,QAAQ,EAAE,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAC;QAAC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC,CAAC;IAChF,OAAO,EAAE,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACzD,2EAA2E;IAC3E,SAAS,EAAE,MAAM,CAAC;IAClB,2CAA2C;IAC3C,OAAO,EAAE,MAAM,CAAC;IAChB,uEAAuE;IACvE,QAAQ,EAAE;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;CACvE;AAED,MAAM,WAAW,gBAAgB;IAC/B,+EAA+E;IAC/E,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,0EAA0E;IAC1E,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,8EAA8E;IAC9E,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,qFAAqF;IACrF,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,iBAAiB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACvD;AAkBD,wBAAsB,SAAS,CAAC,CAAC,EAAE,aAAa,EAAE,IAAI,EAAE,gBAAgB,GAAG,OAAO,CAAC,eAAe,CAAC,CAqElG"}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reconcile (Spec B) — diff the brain against the filesystem and converge.
|
|
3
|
+
* This is the capability that catches the 42-files class: files marked
|
|
4
|
+
* "synced" but silently absent from the brain.
|
|
5
|
+
*
|
|
6
|
+
* REPORT-ONLY by default: a plain run returns structured findings and makes no
|
|
7
|
+
* destructive change. The one write a report run performs is the documented
|
|
8
|
+
* lazy baseline (resolved decision #2): a legacy row with NULL content_hash
|
|
9
|
+
* whose file exists on disk gets the CURRENT file's hash stamped and is
|
|
10
|
+
* classified up-to-date — trusting the present file as baseline avoids a full
|
|
11
|
+
* re-embed of every legacy row.
|
|
12
|
+
*
|
|
13
|
+
* Classification:
|
|
14
|
+
* missing — on disk, no brain row (→ repair: re-ingest via callback)
|
|
15
|
+
* stale — both, hashes differ (→ repair: re-ingest via callback)
|
|
16
|
+
* orphaned — brain row, no file on disk (→ removeOrphaned: delete cluster)
|
|
17
|
+
* renamed — a missing path whose hash equals an orphaned row's hash
|
|
18
|
+
* (→ repair: ONE file_path UPDATE — cluster, enrichment, and
|
|
19
|
+
* embeddings all survive; no re-ingest, no re-embed)
|
|
20
|
+
*
|
|
21
|
+
* Rename-and-edit is NOT detected (hashes differ) — it degrades to the
|
|
22
|
+
* missing + orphaned pair, i.e. add + remove. Documented limitation.
|
|
23
|
+
*
|
|
24
|
+
* Re-ingest is delegated to `opts.reingest` because ingestion is brain-core's
|
|
25
|
+
* pipeline (LLM extraction + embedding) and the dependency points the other
|
|
26
|
+
* way. The consumer (KBDE/KyberBot sweep, Arcana job) wires storeConversation
|
|
27
|
+
* into it — see the spec's consumer-integration section.
|
|
28
|
+
*
|
|
29
|
+
* Filesystem truth = the walked roots; the watched-folders manifest is never
|
|
30
|
+
* consulted. Dotfiles and dot-directories are skipped.
|
|
31
|
+
*/
|
|
32
|
+
import { readdirSync, readFileSync } from 'node:fs';
|
|
33
|
+
import { join } from 'node:path';
|
|
34
|
+
import { getDb } from './index.js';
|
|
35
|
+
import { contentHash } from './hash.js';
|
|
36
|
+
import { sqliteTimelineStore } from './timeline.js';
|
|
37
|
+
function walk(dir, relBase, acc) {
|
|
38
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
39
|
+
if (entry.name.startsWith('.'))
|
|
40
|
+
continue;
|
|
41
|
+
const abs = join(dir, entry.name);
|
|
42
|
+
const rel = relBase ? `${relBase}/${entry.name}` : entry.name;
|
|
43
|
+
if (entry.isDirectory()) {
|
|
44
|
+
walk(abs, rel, acc);
|
|
45
|
+
}
|
|
46
|
+
else if (entry.isFile()) {
|
|
47
|
+
// first root wins on cross-root relpath collision
|
|
48
|
+
if (!acc.has(rel)) {
|
|
49
|
+
acc.set(rel, { file_path: rel, absolute_path: abs, content_hash: contentHash(readFileSync(abs)) });
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
export async function reconcile(t, opts) {
|
|
55
|
+
const onDisk = new Map();
|
|
56
|
+
for (const root of opts.roots)
|
|
57
|
+
walk(root, '', onDisk);
|
|
58
|
+
const brainRows = await sqliteTimelineStore.listAllFilePaths(t);
|
|
59
|
+
const db = getDb(t, 'timeline');
|
|
60
|
+
const stampHash = db.prepare(`UPDATE timeline_events SET content_hash = ? WHERE id = ?`);
|
|
61
|
+
let baselined = 0;
|
|
62
|
+
const stale = [];
|
|
63
|
+
const orphaned = [];
|
|
64
|
+
const brainPaths = new Set();
|
|
65
|
+
for (const row of brainRows) {
|
|
66
|
+
brainPaths.add(row.file_path);
|
|
67
|
+
const disk = onDisk.get(row.file_path);
|
|
68
|
+
if (!disk) {
|
|
69
|
+
orphaned.push(row);
|
|
70
|
+
}
|
|
71
|
+
else if (row.content_hash === null) {
|
|
72
|
+
stampHash.run(disk.content_hash, row.id); // lazy baseline: trust the current file
|
|
73
|
+
baselined++;
|
|
74
|
+
}
|
|
75
|
+
else if (row.content_hash !== disk.content_hash) {
|
|
76
|
+
stale.push({ ...disk, id: row.id, brain_hash: row.content_hash });
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
let missing = [...onDisk.values()].filter((f) => !brainPaths.has(f.file_path));
|
|
80
|
+
// Rename detection: a missing path whose content matches an orphaned row's
|
|
81
|
+
// hash is the same file under a new name — relocate the LOCATOR, keep the
|
|
82
|
+
// cluster. (Requires the orphaned row to carry a hash; legacy NULL-hash
|
|
83
|
+
// orphans can't match and stay orphaned.)
|
|
84
|
+
const renamed = [];
|
|
85
|
+
const orphanByHash = new Map();
|
|
86
|
+
for (const o of orphaned)
|
|
87
|
+
if (o.content_hash)
|
|
88
|
+
orphanByHash.set(o.content_hash, o);
|
|
89
|
+
missing = missing.filter((m) => {
|
|
90
|
+
const o = orphanByHash.get(m.content_hash);
|
|
91
|
+
if (!o)
|
|
92
|
+
return true;
|
|
93
|
+
renamed.push({ id: o.id, from: o.file_path, to: m.file_path });
|
|
94
|
+
orphanByHash.delete(m.content_hash);
|
|
95
|
+
orphaned.splice(orphaned.indexOf(o), 1);
|
|
96
|
+
return false;
|
|
97
|
+
});
|
|
98
|
+
const repaired = { relabelled: 0, reingested: 0, removed: 0 };
|
|
99
|
+
if (opts.repair) {
|
|
100
|
+
const relabel = db.prepare(`UPDATE timeline_events SET file_path = ? WHERE id = ?`);
|
|
101
|
+
for (const r of renamed) {
|
|
102
|
+
relabel.run(r.to, r.id);
|
|
103
|
+
repaired.relabelled++;
|
|
104
|
+
}
|
|
105
|
+
if (opts.reingest) {
|
|
106
|
+
for (const f of [...missing, ...stale]) {
|
|
107
|
+
await opts.reingest(f);
|
|
108
|
+
repaired.reingested++;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (opts.removeOrphaned) {
|
|
113
|
+
for (const o of orphaned) {
|
|
114
|
+
await sqliteTimelineStore.removeFileCluster(t, o.file_path, { removeEvent: true });
|
|
115
|
+
repaired.removed++;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return { missing, stale, orphaned, renamed, baselined, scanned: onDisk.size, repaired };
|
|
119
|
+
}
|
|
120
|
+
//# sourceMappingURL=reconcile.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reconcile.js","sourceRoot":"","sources":["../src/reconcile.ts"],"names":[],"mappings":"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"}
|
package/dist/timeline.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"timeline.d.ts","sourceRoot":"","sources":["../src/timeline.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAOV,kBAAkB,EACnB,MAAM,6BAA6B,CAAC;
|
|
1
|
+
{"version":3,"file":"timeline.d.ts","sourceRoot":"","sources":["../src/timeline.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAOV,kBAAkB,EACnB,MAAM,6BAA6B,CAAC;AA6DrC,iEAAiE;AACjE,wBAAgB,wBAAwB,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAG5D;AA2GD,eAAO,MAAM,mBAAmB,EAAE,kBAoRjC,CAAC"}
|
package/dist/timeline.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { getDb } from './index.js';
|
|
8
8
|
import { sanitizeFtsQuery } from './fts-sanitizer.js';
|
|
9
|
+
import { lazyVectorStore } from './vec-lazy.js';
|
|
9
10
|
function rowToEvent(row) {
|
|
10
11
|
return {
|
|
11
12
|
id: row.id,
|
|
@@ -29,6 +30,8 @@ function rowToEvent(row) {
|
|
|
29
30
|
classification: row.classification ?? undefined,
|
|
30
31
|
connection_id: row.connection_id ?? undefined,
|
|
31
32
|
source_did: row.source_did ?? undefined,
|
|
33
|
+
file_path: row.file_path ?? undefined,
|
|
34
|
+
content_hash: row.content_hash ?? undefined,
|
|
32
35
|
};
|
|
33
36
|
}
|
|
34
37
|
const initialized = new Set();
|
|
@@ -65,7 +68,9 @@ function ensureSchema(t) {
|
|
|
65
68
|
project_id TEXT,
|
|
66
69
|
classification TEXT,
|
|
67
70
|
connection_id TEXT,
|
|
68
|
-
source_did TEXT
|
|
71
|
+
source_did TEXT,
|
|
72
|
+
file_path TEXT,
|
|
73
|
+
content_hash TEXT
|
|
69
74
|
);
|
|
70
75
|
|
|
71
76
|
CREATE INDEX IF NOT EXISTS idx_timeline_timestamp ON timeline_events(timestamp);
|
|
@@ -77,6 +82,8 @@ function ensureSchema(t) {
|
|
|
77
82
|
CREATE INDEX IF NOT EXISTS idx_timeline_project ON timeline_events(project_id);
|
|
78
83
|
CREATE INDEX IF NOT EXISTS idx_timeline_classification ON timeline_events(classification);
|
|
79
84
|
CREATE INDEX IF NOT EXISTS idx_timeline_connection ON timeline_events(connection_id);
|
|
85
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_timeline_file_path
|
|
86
|
+
ON timeline_events(file_path) WHERE file_path IS NOT NULL;
|
|
80
87
|
|
|
81
88
|
CREATE VIRTUAL TABLE IF NOT EXISTS timeline_fts USING fts5(
|
|
82
89
|
title, summary, entities, topics, content='',
|
|
@@ -145,13 +152,13 @@ export const sqliteTimelineStore = {
|
|
|
145
152
|
async addEvent(t, event) {
|
|
146
153
|
ensureSchema(t);
|
|
147
154
|
const db = getDb(t, 'timeline');
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
+
// Dual-key upsert (Spec B): file rows converge on file_path (the mutable
|
|
156
|
+
// locator), conversation rows on source_path — exactly as before. In the
|
|
157
|
+
// file branch the row ADOPTS the new ingest's source_path (the fresh
|
|
158
|
+
// facts/chunks cluster lives under it; exact-match cleanup removes the old
|
|
159
|
+
// one), while the row id and enrichment columns (priority, tier, decay,
|
|
160
|
+
// pin, access, last_enriched) persist by omission from the SET list.
|
|
161
|
+
const sharedSet = `
|
|
155
162
|
type = excluded.type,
|
|
156
163
|
timestamp = excluded.timestamp,
|
|
157
164
|
end_timestamp = excluded.end_timestamp,
|
|
@@ -163,10 +170,60 @@ export const sqliteTimelineStore = {
|
|
|
163
170
|
tags_json = COALESCE(excluded.tags_json, timeline_events.tags_json),
|
|
164
171
|
classification = COALESCE(excluded.classification, timeline_events.classification),
|
|
165
172
|
connection_id = COALESCE(excluded.connection_id, timeline_events.connection_id),
|
|
166
|
-
source_did = COALESCE(excluded.source_did, timeline_events.source_did)
|
|
167
|
-
|
|
173
|
+
source_did = COALESCE(excluded.source_did, timeline_events.source_did)`;
|
|
174
|
+
const conflictClause = event.file_path
|
|
175
|
+
? `ON CONFLICT(file_path) WHERE file_path IS NOT NULL DO UPDATE SET
|
|
176
|
+
source_path = excluded.source_path,
|
|
177
|
+
content_hash = excluded.content_hash,${sharedSet}`
|
|
178
|
+
: `ON CONFLICT(source_path) DO UPDATE SET
|
|
179
|
+
file_path = COALESCE(excluded.file_path, timeline_events.file_path),
|
|
180
|
+
content_hash = COALESCE(excluded.content_hash, timeline_events.content_hash),${sharedSet}`;
|
|
181
|
+
const result = db.prepare(`
|
|
182
|
+
INSERT INTO timeline_events
|
|
183
|
+
(type, timestamp, end_timestamp, title, summary, source_path,
|
|
184
|
+
entities_json, topics_json,
|
|
185
|
+
project_id, tags_json, classification, connection_id, source_did,
|
|
186
|
+
file_path, content_hash)
|
|
187
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
188
|
+
${conflictClause}
|
|
189
|
+
`).run(event.type, event.timestamp, event.end_timestamp ?? null, event.title, event.summary ?? '', event.source_path, JSON.stringify(event.entities), JSON.stringify(event.topics), event.project_id ?? null, event.tags ? JSON.stringify(event.tags) : null, event.classification ?? null, event.connection_id ?? null, event.source_did ?? null, event.file_path ?? null, event.content_hash ?? null);
|
|
168
190
|
return result.lastInsertRowid;
|
|
169
191
|
},
|
|
192
|
+
async removeFileCluster(t, filePath, opts = {}) {
|
|
193
|
+
ensureSchema(t);
|
|
194
|
+
const db = getDb(t, 'timeline');
|
|
195
|
+
// EXACT match only — the substring approach this replaces cross-deleted
|
|
196
|
+
// notes.md when re-ingesting meeting-notes.md (baseline bug #14).
|
|
197
|
+
const row = db
|
|
198
|
+
.prepare(`SELECT id, source_path FROM timeline_events WHERE file_path = ?`)
|
|
199
|
+
.get(filePath);
|
|
200
|
+
if (!row)
|
|
201
|
+
return { removed: false, facts: 0, chunks: 0 };
|
|
202
|
+
// source_path = channel://<channel>/<conversationId> — the cluster key.
|
|
203
|
+
const conversationId = row.source_path.split('/').pop() ?? '';
|
|
204
|
+
const facts = conversationId
|
|
205
|
+
? db.prepare(`DELETE FROM facts WHERE source_conversation_id = ?`).run(conversationId).changes
|
|
206
|
+
: 0;
|
|
207
|
+
const chunks = conversationId
|
|
208
|
+
? await lazyVectorStore.deleteByOriginPrefix(t, `${conversationId}_seg_`)
|
|
209
|
+
: 0;
|
|
210
|
+
if (opts.removeEvent) {
|
|
211
|
+
db.prepare(`DELETE FROM timeline_events WHERE id = ?`).run(row.id);
|
|
212
|
+
}
|
|
213
|
+
return { removed: true, facts, chunks };
|
|
214
|
+
},
|
|
215
|
+
async listAllFilePaths(t, opts = {}) {
|
|
216
|
+
ensureSchema(t);
|
|
217
|
+
const db = getDb(t, 'timeline');
|
|
218
|
+
return db
|
|
219
|
+
.prepare(`
|
|
220
|
+
SELECT id, file_path, content_hash FROM timeline_events
|
|
221
|
+
WHERE type = 'file' AND file_path IS NOT NULL AND id > ?
|
|
222
|
+
ORDER BY id ASC
|
|
223
|
+
LIMIT ?
|
|
224
|
+
`)
|
|
225
|
+
.all(opts.afterId ?? 0, opts.limit ?? -1);
|
|
226
|
+
},
|
|
170
227
|
async removeEventByPath(t, sourcePath) {
|
|
171
228
|
ensureSchema(t);
|
|
172
229
|
const db = getDb(t, 'timeline');
|