dsh-tdai-memory 0.3.2 → 0.3.4
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/LICENSE +21 -21
- package/README.md +15 -19
- package/README.zh.md +7 -1
- package/THIRD-PARTY-NOTICES.md +20 -20
- package/client.js +2 -2
- package/cordis.patch.yml +8 -8
- package/index.js +13 -22
- package/package.json +94 -78
- package/recall-inject.js +86 -86
- package/vendor/tdai/adapters/index.js +15 -15
- package/vendor/tdai/adapters/openclaw/host-adapter.js +83 -83
- package/vendor/tdai/adapters/openclaw/index.js +5 -5
- package/vendor/tdai/adapters/openclaw/llm-runner.js +69 -69
- package/vendor/tdai/adapters/standalone/host-adapter.js +58 -58
- package/vendor/tdai/adapters/standalone/index.js +5 -5
- package/vendor/tdai/core/conversation/l0-recorder.js +420 -420
- package/vendor/tdai/core/hooks/auto-capture.js +236 -236
- package/vendor/tdai/core/hooks/auto-recall.js +640 -640
- package/vendor/tdai/core/index.js +8 -8
- package/vendor/tdai/core/persona/persona-generator.js +184 -184
- package/vendor/tdai/core/persona/persona-trigger.js +94 -94
- package/vendor/tdai/core/profile/profile-sync.js +200 -200
- package/vendor/tdai/core/prompts/l1-dedup.js +72 -72
- package/vendor/tdai/core/prompts/l1-extraction.js +34 -34
- package/vendor/tdai/core/prompts/persona-generation.js +35 -35
- package/vendor/tdai/core/prompts/scene-extraction.js +45 -45
- package/vendor/tdai/core/record/l1-dedup.js +318 -318
- package/vendor/tdai/core/record/l1-extractor.js +379 -379
- package/vendor/tdai/core/record/l1-reader.js +178 -178
- package/vendor/tdai/core/record/l1-writer.js +141 -141
- package/vendor/tdai/core/report/reporter.js +86 -86
- package/vendor/tdai/core/scene/filename-normalizer.js +161 -161
- package/vendor/tdai/core/scene/scene-extractor.js +410 -410
- package/vendor/tdai/core/scene/scene-format.js +53 -53
- package/vendor/tdai/core/scene/scene-index.js +82 -82
- package/vendor/tdai/core/scene/scene-navigation.js +57 -57
- package/vendor/tdai/core/seed/input.js +404 -404
- package/vendor/tdai/core/seed/types.js +9 -9
- package/vendor/tdai/core/store/bm25-client.js +117 -117
- package/vendor/tdai/core/store/bm25-local.js +68 -68
- package/vendor/tdai/core/store/embedding.js +632 -632
- package/vendor/tdai/core/store/factory.js +95 -95
- package/vendor/tdai/core/store/search-utils.js +54 -54
- package/vendor/tdai/core/store/sqlite.js +1632 -1632
- package/vendor/tdai/core/store/tcvdb-client.js +221 -221
- package/vendor/tdai/core/store/tcvdb.js +1047 -1047
- package/vendor/tdai/core/store/types.js +17 -17
- package/vendor/tdai/core/tools/conversation-search.js +199 -199
- package/vendor/tdai/core/tools/memory-search.js +207 -207
- package/vendor/tdai/core/types.js +12 -12
- package/vendor/tdai/utils/backup.js +189 -189
- package/vendor/tdai/utils/checkpoint.js +360 -360
- package/vendor/tdai/utils/clean-context-runner.js +472 -472
- package/vendor/tdai/utils/ensure-hook-policy.js +213 -213
- package/vendor/tdai/utils/env.js +13 -13
- package/vendor/tdai/utils/managed-timer.js +127 -127
- package/vendor/tdai/utils/manifest.js +91 -91
- package/vendor/tdai/utils/memory-cleaner.js +300 -300
- package/vendor/tdai/utils/no-think-fetch.js +109 -109
- package/vendor/tdai/utils/openclaw-state-dir.js +26 -26
- package/vendor/tdai/utils/pipeline-factory.js +517 -517
- package/vendor/tdai/utils/pipeline-manager.js +892 -892
- package/vendor/tdai/utils/sanitize.js +360 -360
- package/vendor/tdai/utils/serial-queue.js +103 -103
- package/vendor/tdai/utils/session-filter.js +88 -88
- package/vendor/tdai/utils/text-utils.js +29 -29
- package/vendor/tdai/utils/time.js +252 -252
|
@@ -1,127 +1,127 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ManagedTimer: a named, lifecycle-managed wrapper around setTimeout.
|
|
3
|
-
*
|
|
4
|
-
* Eliminates repetitive clear→set→fire→clean patterns by providing:
|
|
5
|
-
* - `schedule(delayMs, cb)` — cancel any pending timer, set a new one
|
|
6
|
-
* - `scheduleAt(epochMs, cb)` — schedule by absolute time point
|
|
7
|
-
* - `tryAdvanceTo(epochMs, cb)` — only reschedule if new time is *earlier*
|
|
8
|
-
* - `cancel()` — cancel without triggering
|
|
9
|
-
* - `flush()` — trigger immediately (for graceful shutdown)
|
|
10
|
-
* - `pending` — whether a timer is waiting
|
|
11
|
-
*
|
|
12
|
-
* The optional `isDestroyed` guard prevents firing after the owner is torn down.
|
|
13
|
-
*/
|
|
14
|
-
export class ManagedTimer {
|
|
15
|
-
name;
|
|
16
|
-
isDestroyed;
|
|
17
|
-
handle = null;
|
|
18
|
-
callback = null;
|
|
19
|
-
/** Absolute epoch-ms when the current timer is scheduled to fire. */
|
|
20
|
-
scheduledAt = 0;
|
|
21
|
-
constructor(
|
|
22
|
-
/** Human-readable name for logging. */
|
|
23
|
-
name,
|
|
24
|
-
/** If provided, checked before firing — skips callback when true. */
|
|
25
|
-
isDestroyed) {
|
|
26
|
-
this.name = name;
|
|
27
|
-
this.isDestroyed = isDestroyed;
|
|
28
|
-
}
|
|
29
|
-
// ── Core operations ──────────────────────────────────
|
|
30
|
-
/**
|
|
31
|
-
* Cancel any pending timer and schedule a new one after `delayMs`.
|
|
32
|
-
* The callback fires once; the timer auto-clears after firing.
|
|
33
|
-
*/
|
|
34
|
-
schedule(delayMs, callback) {
|
|
35
|
-
this.cancelInternal();
|
|
36
|
-
this.callback = callback;
|
|
37
|
-
this.scheduledAt = Date.now() + delayMs;
|
|
38
|
-
this.handle = setTimeout(() => this.fire(), delayMs);
|
|
39
|
-
// Don't let pipeline timers keep the process alive in CLI mode.
|
|
40
|
-
// In gateway mode the server listener holds the event loop anyway.
|
|
41
|
-
this.handle.unref();
|
|
42
|
-
}
|
|
43
|
-
/**
|
|
44
|
-
* Cancel any pending timer and schedule to fire at an absolute epoch-ms.
|
|
45
|
-
* If `epochMs` is in the past, fires on next tick (delay = 0).
|
|
46
|
-
*/
|
|
47
|
-
scheduleAt(epochMs, callback) {
|
|
48
|
-
this.cancelInternal();
|
|
49
|
-
this.callback = callback;
|
|
50
|
-
this.scheduledAt = epochMs;
|
|
51
|
-
const delay = Math.max(0, epochMs - Date.now());
|
|
52
|
-
this.handle = setTimeout(() => this.fire(), delay);
|
|
53
|
-
this.handle.unref();
|
|
54
|
-
}
|
|
55
|
-
/**
|
|
56
|
-
* Only reschedule if `epochMs` is *earlier* than the current scheduled time.
|
|
57
|
-
* This implements the "downward-only" timer pattern (L2 scheduling).
|
|
58
|
-
* If no timer is pending, behaves like `scheduleAt()`.
|
|
59
|
-
*
|
|
60
|
-
* @returns true if the timer was actually advanced (or newly set).
|
|
61
|
-
*/
|
|
62
|
-
tryAdvanceTo(epochMs, callback) {
|
|
63
|
-
if (this.handle === null) {
|
|
64
|
-
// No pending timer → set it
|
|
65
|
-
this.scheduleAt(epochMs, callback);
|
|
66
|
-
return true;
|
|
67
|
-
}
|
|
68
|
-
if (epochMs < this.scheduledAt) {
|
|
69
|
-
// New time is earlier → reschedule
|
|
70
|
-
this.scheduleAt(epochMs, callback);
|
|
71
|
-
return true;
|
|
72
|
-
}
|
|
73
|
-
// Current timer is already earlier or equal → keep it
|
|
74
|
-
return false;
|
|
75
|
-
}
|
|
76
|
-
/**
|
|
77
|
-
* Cancel the pending timer without triggering the callback.
|
|
78
|
-
*/
|
|
79
|
-
cancel() {
|
|
80
|
-
this.cancelInternal();
|
|
81
|
-
}
|
|
82
|
-
/**
|
|
83
|
-
* Immediately trigger the callback (if pending) and clear the timer.
|
|
84
|
-
* Used for graceful shutdown to flush pending work.
|
|
85
|
-
*
|
|
86
|
-
* Note: Unlike `fire()`, this method intentionally does NOT check `isDestroyed`.
|
|
87
|
-
* This is by design — during shutdown, `destroy()` sets `destroyed = true` first,
|
|
88
|
-
* then calls `flush()` to drain pending work. The `isDestroyed` guard only applies
|
|
89
|
-
* to natural timer expiration via `fire()`, not to explicit shutdown flushes.
|
|
90
|
-
*/
|
|
91
|
-
flush() {
|
|
92
|
-
if (this.handle === null)
|
|
93
|
-
return;
|
|
94
|
-
const cb = this.callback;
|
|
95
|
-
this.cancelInternal();
|
|
96
|
-
if (cb)
|
|
97
|
-
cb();
|
|
98
|
-
}
|
|
99
|
-
// ── Accessors ────────────────────────────────────────
|
|
100
|
-
/** Whether a timer is currently pending. */
|
|
101
|
-
get pending() {
|
|
102
|
-
return this.handle !== null;
|
|
103
|
-
}
|
|
104
|
-
/** The epoch-ms when the current timer is scheduled to fire (0 if none). */
|
|
105
|
-
get scheduledTime() {
|
|
106
|
-
return this.handle !== null ? this.scheduledAt : 0;
|
|
107
|
-
}
|
|
108
|
-
// ── Internals ────────────────────────────────────────
|
|
109
|
-
fire() {
|
|
110
|
-
const cb = this.callback;
|
|
111
|
-
this.handle = null;
|
|
112
|
-
this.callback = null;
|
|
113
|
-
this.scheduledAt = 0;
|
|
114
|
-
if (this.isDestroyed?.())
|
|
115
|
-
return;
|
|
116
|
-
if (cb)
|
|
117
|
-
cb();
|
|
118
|
-
}
|
|
119
|
-
cancelInternal() {
|
|
120
|
-
if (this.handle !== null) {
|
|
121
|
-
clearTimeout(this.handle);
|
|
122
|
-
this.handle = null;
|
|
123
|
-
}
|
|
124
|
-
this.callback = null;
|
|
125
|
-
this.scheduledAt = 0;
|
|
126
|
-
}
|
|
127
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* ManagedTimer: a named, lifecycle-managed wrapper around setTimeout.
|
|
3
|
+
*
|
|
4
|
+
* Eliminates repetitive clear→set→fire→clean patterns by providing:
|
|
5
|
+
* - `schedule(delayMs, cb)` — cancel any pending timer, set a new one
|
|
6
|
+
* - `scheduleAt(epochMs, cb)` — schedule by absolute time point
|
|
7
|
+
* - `tryAdvanceTo(epochMs, cb)` — only reschedule if new time is *earlier*
|
|
8
|
+
* - `cancel()` — cancel without triggering
|
|
9
|
+
* - `flush()` — trigger immediately (for graceful shutdown)
|
|
10
|
+
* - `pending` — whether a timer is waiting
|
|
11
|
+
*
|
|
12
|
+
* The optional `isDestroyed` guard prevents firing after the owner is torn down.
|
|
13
|
+
*/
|
|
14
|
+
export class ManagedTimer {
|
|
15
|
+
name;
|
|
16
|
+
isDestroyed;
|
|
17
|
+
handle = null;
|
|
18
|
+
callback = null;
|
|
19
|
+
/** Absolute epoch-ms when the current timer is scheduled to fire. */
|
|
20
|
+
scheduledAt = 0;
|
|
21
|
+
constructor(
|
|
22
|
+
/** Human-readable name for logging. */
|
|
23
|
+
name,
|
|
24
|
+
/** If provided, checked before firing — skips callback when true. */
|
|
25
|
+
isDestroyed) {
|
|
26
|
+
this.name = name;
|
|
27
|
+
this.isDestroyed = isDestroyed;
|
|
28
|
+
}
|
|
29
|
+
// ── Core operations ──────────────────────────────────
|
|
30
|
+
/**
|
|
31
|
+
* Cancel any pending timer and schedule a new one after `delayMs`.
|
|
32
|
+
* The callback fires once; the timer auto-clears after firing.
|
|
33
|
+
*/
|
|
34
|
+
schedule(delayMs, callback) {
|
|
35
|
+
this.cancelInternal();
|
|
36
|
+
this.callback = callback;
|
|
37
|
+
this.scheduledAt = Date.now() + delayMs;
|
|
38
|
+
this.handle = setTimeout(() => this.fire(), delayMs);
|
|
39
|
+
// Don't let pipeline timers keep the process alive in CLI mode.
|
|
40
|
+
// In gateway mode the server listener holds the event loop anyway.
|
|
41
|
+
this.handle.unref();
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Cancel any pending timer and schedule to fire at an absolute epoch-ms.
|
|
45
|
+
* If `epochMs` is in the past, fires on next tick (delay = 0).
|
|
46
|
+
*/
|
|
47
|
+
scheduleAt(epochMs, callback) {
|
|
48
|
+
this.cancelInternal();
|
|
49
|
+
this.callback = callback;
|
|
50
|
+
this.scheduledAt = epochMs;
|
|
51
|
+
const delay = Math.max(0, epochMs - Date.now());
|
|
52
|
+
this.handle = setTimeout(() => this.fire(), delay);
|
|
53
|
+
this.handle.unref();
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Only reschedule if `epochMs` is *earlier* than the current scheduled time.
|
|
57
|
+
* This implements the "downward-only" timer pattern (L2 scheduling).
|
|
58
|
+
* If no timer is pending, behaves like `scheduleAt()`.
|
|
59
|
+
*
|
|
60
|
+
* @returns true if the timer was actually advanced (or newly set).
|
|
61
|
+
*/
|
|
62
|
+
tryAdvanceTo(epochMs, callback) {
|
|
63
|
+
if (this.handle === null) {
|
|
64
|
+
// No pending timer → set it
|
|
65
|
+
this.scheduleAt(epochMs, callback);
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
if (epochMs < this.scheduledAt) {
|
|
69
|
+
// New time is earlier → reschedule
|
|
70
|
+
this.scheduleAt(epochMs, callback);
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
// Current timer is already earlier or equal → keep it
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Cancel the pending timer without triggering the callback.
|
|
78
|
+
*/
|
|
79
|
+
cancel() {
|
|
80
|
+
this.cancelInternal();
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Immediately trigger the callback (if pending) and clear the timer.
|
|
84
|
+
* Used for graceful shutdown to flush pending work.
|
|
85
|
+
*
|
|
86
|
+
* Note: Unlike `fire()`, this method intentionally does NOT check `isDestroyed`.
|
|
87
|
+
* This is by design — during shutdown, `destroy()` sets `destroyed = true` first,
|
|
88
|
+
* then calls `flush()` to drain pending work. The `isDestroyed` guard only applies
|
|
89
|
+
* to natural timer expiration via `fire()`, not to explicit shutdown flushes.
|
|
90
|
+
*/
|
|
91
|
+
flush() {
|
|
92
|
+
if (this.handle === null)
|
|
93
|
+
return;
|
|
94
|
+
const cb = this.callback;
|
|
95
|
+
this.cancelInternal();
|
|
96
|
+
if (cb)
|
|
97
|
+
cb();
|
|
98
|
+
}
|
|
99
|
+
// ── Accessors ────────────────────────────────────────
|
|
100
|
+
/** Whether a timer is currently pending. */
|
|
101
|
+
get pending() {
|
|
102
|
+
return this.handle !== null;
|
|
103
|
+
}
|
|
104
|
+
/** The epoch-ms when the current timer is scheduled to fire (0 if none). */
|
|
105
|
+
get scheduledTime() {
|
|
106
|
+
return this.handle !== null ? this.scheduledAt : 0;
|
|
107
|
+
}
|
|
108
|
+
// ── Internals ────────────────────────────────────────
|
|
109
|
+
fire() {
|
|
110
|
+
const cb = this.callback;
|
|
111
|
+
this.handle = null;
|
|
112
|
+
this.callback = null;
|
|
113
|
+
this.scheduledAt = 0;
|
|
114
|
+
if (this.isDestroyed?.())
|
|
115
|
+
return;
|
|
116
|
+
if (cb)
|
|
117
|
+
cb();
|
|
118
|
+
}
|
|
119
|
+
cancelInternal() {
|
|
120
|
+
if (this.handle !== null) {
|
|
121
|
+
clearTimeout(this.handle);
|
|
122
|
+
this.handle = null;
|
|
123
|
+
}
|
|
124
|
+
this.callback = null;
|
|
125
|
+
this.scheduledAt = 0;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
@@ -1,91 +1,91 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Manifest — self-describing metadata for a memory-tdai data directory.
|
|
3
|
-
*
|
|
4
|
-
* Lives at `<dataDir>/.metadata/manifest.json`.
|
|
5
|
-
*
|
|
6
|
-
* - **store**: written once on first successful store init; never overwritten.
|
|
7
|
-
* On subsequent starts the current config is compared against the persisted
|
|
8
|
-
* store binding — mismatches are logged at debug level (informational only).
|
|
9
|
-
* - **seed**: written once when a seed run completes; null for live-runtime dirs.
|
|
10
|
-
*
|
|
11
|
-
* This file is informational / read-only from the user's perspective.
|
|
12
|
-
* The plugin reads it on startup for consistency checks.
|
|
13
|
-
*/
|
|
14
|
-
import fs from "node:fs";
|
|
15
|
-
import path from "node:path";
|
|
16
|
-
// ============================
|
|
17
|
-
// Paths
|
|
18
|
-
// ============================
|
|
19
|
-
const METADATA_DIR = ".metadata";
|
|
20
|
-
const MANIFEST_FILE = "manifest.json";
|
|
21
|
-
export function manifestPath(dataDir) {
|
|
22
|
-
return path.join(dataDir, METADATA_DIR, MANIFEST_FILE);
|
|
23
|
-
}
|
|
24
|
-
// ============================
|
|
25
|
-
// Read / Write
|
|
26
|
-
// ============================
|
|
27
|
-
/**
|
|
28
|
-
* Read an existing manifest from disk. Returns `null` if not found or unparseable.
|
|
29
|
-
*/
|
|
30
|
-
export function readManifest(dataDir) {
|
|
31
|
-
const p = manifestPath(dataDir);
|
|
32
|
-
try {
|
|
33
|
-
if (!fs.existsSync(p))
|
|
34
|
-
return null;
|
|
35
|
-
const raw = fs.readFileSync(p, "utf-8");
|
|
36
|
-
return JSON.parse(raw);
|
|
37
|
-
}
|
|
38
|
-
catch {
|
|
39
|
-
return null;
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
/**
|
|
43
|
-
* Write a manifest to disk (creates `.metadata/` if needed).
|
|
44
|
-
*/
|
|
45
|
-
export function writeManifest(dataDir, manifest) {
|
|
46
|
-
const dir = path.join(dataDir, METADATA_DIR);
|
|
47
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
48
|
-
fs.writeFileSync(manifestPath(dataDir), JSON.stringify(manifest, null, 2) + "\n", "utf-8");
|
|
49
|
-
}
|
|
50
|
-
/**
|
|
51
|
-
* Build a ManifestStoreInfo from the current store config snapshot.
|
|
52
|
-
*/
|
|
53
|
-
export function buildStoreInfo(snapshot) {
|
|
54
|
-
const info = { type: snapshot.type };
|
|
55
|
-
if (snapshot.type === "sqlite") {
|
|
56
|
-
info.sqlite = { path: snapshot.sqlitePath ?? "vectors.db" };
|
|
57
|
-
}
|
|
58
|
-
else {
|
|
59
|
-
info.tcvdb = {
|
|
60
|
-
url: snapshot.tcvdbUrl,
|
|
61
|
-
database: snapshot.tcvdbDatabase,
|
|
62
|
-
alias: snapshot.tcvdbAlias || undefined,
|
|
63
|
-
};
|
|
64
|
-
}
|
|
65
|
-
return info;
|
|
66
|
-
}
|
|
67
|
-
/**
|
|
68
|
-
* Compare the persisted store binding against the current config.
|
|
69
|
-
* Returns a list of human-readable mismatch descriptions (empty = all good).
|
|
70
|
-
*/
|
|
71
|
-
export function diffStoreBinding(persisted, current) {
|
|
72
|
-
const diffs = [];
|
|
73
|
-
if (persisted.type !== current.type) {
|
|
74
|
-
diffs.push(`store type changed: ${persisted.type} → ${current.type}`);
|
|
75
|
-
return diffs; // no point comparing fields across different types
|
|
76
|
-
}
|
|
77
|
-
if (persisted.type === "sqlite" && current.type === "sqlite") {
|
|
78
|
-
if (persisted.sqlite?.path !== current.sqlite?.path) {
|
|
79
|
-
diffs.push(`sqlite path changed: ${persisted.sqlite?.path} → ${current.sqlite?.path}`);
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
if (persisted.type === "tcvdb" && current.type === "tcvdb") {
|
|
83
|
-
if (persisted.tcvdb?.url !== current.tcvdb?.url) {
|
|
84
|
-
diffs.push(`tcvdb url changed: ${persisted.tcvdb?.url} → ${current.tcvdb?.url}`);
|
|
85
|
-
}
|
|
86
|
-
if (persisted.tcvdb?.database !== current.tcvdb?.database) {
|
|
87
|
-
diffs.push(`tcvdb database changed: ${persisted.tcvdb?.database} → ${current.tcvdb?.database}`);
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
return diffs;
|
|
91
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Manifest — self-describing metadata for a memory-tdai data directory.
|
|
3
|
+
*
|
|
4
|
+
* Lives at `<dataDir>/.metadata/manifest.json`.
|
|
5
|
+
*
|
|
6
|
+
* - **store**: written once on first successful store init; never overwritten.
|
|
7
|
+
* On subsequent starts the current config is compared against the persisted
|
|
8
|
+
* store binding — mismatches are logged at debug level (informational only).
|
|
9
|
+
* - **seed**: written once when a seed run completes; null for live-runtime dirs.
|
|
10
|
+
*
|
|
11
|
+
* This file is informational / read-only from the user's perspective.
|
|
12
|
+
* The plugin reads it on startup for consistency checks.
|
|
13
|
+
*/
|
|
14
|
+
import fs from "node:fs";
|
|
15
|
+
import path from "node:path";
|
|
16
|
+
// ============================
|
|
17
|
+
// Paths
|
|
18
|
+
// ============================
|
|
19
|
+
const METADATA_DIR = ".metadata";
|
|
20
|
+
const MANIFEST_FILE = "manifest.json";
|
|
21
|
+
export function manifestPath(dataDir) {
|
|
22
|
+
return path.join(dataDir, METADATA_DIR, MANIFEST_FILE);
|
|
23
|
+
}
|
|
24
|
+
// ============================
|
|
25
|
+
// Read / Write
|
|
26
|
+
// ============================
|
|
27
|
+
/**
|
|
28
|
+
* Read an existing manifest from disk. Returns `null` if not found or unparseable.
|
|
29
|
+
*/
|
|
30
|
+
export function readManifest(dataDir) {
|
|
31
|
+
const p = manifestPath(dataDir);
|
|
32
|
+
try {
|
|
33
|
+
if (!fs.existsSync(p))
|
|
34
|
+
return null;
|
|
35
|
+
const raw = fs.readFileSync(p, "utf-8");
|
|
36
|
+
return JSON.parse(raw);
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Write a manifest to disk (creates `.metadata/` if needed).
|
|
44
|
+
*/
|
|
45
|
+
export function writeManifest(dataDir, manifest) {
|
|
46
|
+
const dir = path.join(dataDir, METADATA_DIR);
|
|
47
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
48
|
+
fs.writeFileSync(manifestPath(dataDir), JSON.stringify(manifest, null, 2) + "\n", "utf-8");
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Build a ManifestStoreInfo from the current store config snapshot.
|
|
52
|
+
*/
|
|
53
|
+
export function buildStoreInfo(snapshot) {
|
|
54
|
+
const info = { type: snapshot.type };
|
|
55
|
+
if (snapshot.type === "sqlite") {
|
|
56
|
+
info.sqlite = { path: snapshot.sqlitePath ?? "vectors.db" };
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
info.tcvdb = {
|
|
60
|
+
url: snapshot.tcvdbUrl,
|
|
61
|
+
database: snapshot.tcvdbDatabase,
|
|
62
|
+
alias: snapshot.tcvdbAlias || undefined,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
return info;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Compare the persisted store binding against the current config.
|
|
69
|
+
* Returns a list of human-readable mismatch descriptions (empty = all good).
|
|
70
|
+
*/
|
|
71
|
+
export function diffStoreBinding(persisted, current) {
|
|
72
|
+
const diffs = [];
|
|
73
|
+
if (persisted.type !== current.type) {
|
|
74
|
+
diffs.push(`store type changed: ${persisted.type} → ${current.type}`);
|
|
75
|
+
return diffs; // no point comparing fields across different types
|
|
76
|
+
}
|
|
77
|
+
if (persisted.type === "sqlite" && current.type === "sqlite") {
|
|
78
|
+
if (persisted.sqlite?.path !== current.sqlite?.path) {
|
|
79
|
+
diffs.push(`sqlite path changed: ${persisted.sqlite?.path} → ${current.sqlite?.path}`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (persisted.type === "tcvdb" && current.type === "tcvdb") {
|
|
83
|
+
if (persisted.tcvdb?.url !== current.tcvdb?.url) {
|
|
84
|
+
diffs.push(`tcvdb url changed: ${persisted.tcvdb?.url} → ${current.tcvdb?.url}`);
|
|
85
|
+
}
|
|
86
|
+
if (persisted.tcvdb?.database !== current.tcvdb?.database) {
|
|
87
|
+
diffs.push(`tcvdb database changed: ${persisted.tcvdb?.database} → ${current.tcvdb?.database}`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return diffs;
|
|
91
|
+
}
|