dsh-trivium 0.4.9 → 0.4.13
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/README.md +76 -44
- package/README.zh-CN.md +77 -47
- package/lib/client.js +374 -45
- package/lib/index.js +9 -0
- package/lib/purge.js +122 -0
- package/lib/server.js +29 -1
- package/lib/settings.js +106 -9
- package/lib/sidecar.js +422 -0
- package/lib/store.js +69 -8
- package/package.json +4 -1
package/lib/sidecar.js
ADDED
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
/** Git-friendly JSONL sidecar next to trivium.tdb. No embeddings, episodes, or chip pins. */
|
|
2
|
+
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { dirname, join, resolve } from "node:path";
|
|
6
|
+
import { BUSINESS_EDGES } from "./schema.js";
|
|
7
|
+
import {
|
|
8
|
+
gitSidecarEnabledOf,
|
|
9
|
+
listedWorkspaces,
|
|
10
|
+
readUiSettings,
|
|
11
|
+
sidecarHashOf,
|
|
12
|
+
writeSidecarHash,
|
|
13
|
+
} from "./settings.js";
|
|
14
|
+
|
|
15
|
+
export const SIDECAR_FORMAT = "dsh-trivium-jsonl";
|
|
16
|
+
export const SIDECAR_VERSION = 1;
|
|
17
|
+
export const SIDECAR_DEBOUNCE_MS = 1500;
|
|
18
|
+
export const BUSINESS_NODE_TYPES = Object.freeze(["entity", "preference", "decision", "experience"]);
|
|
19
|
+
|
|
20
|
+
const timers = new Map();
|
|
21
|
+
const memoryHash = new Map();
|
|
22
|
+
|
|
23
|
+
export function sidecarPath(cwd) {
|
|
24
|
+
return join(resolve(cwd || ""), ".dsh", "trivium.jsonl");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function hashText(text) {
|
|
28
|
+
return createHash("sha256").update(String(text || ""), "utf8").digest("hex");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function isBusinessType(type) {
|
|
32
|
+
return BUSINESS_NODE_TYPES.includes(String(type || ""));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function payloadOf(db, id) {
|
|
36
|
+
return db.get(Number(id))?.payload || {};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function edgeTarget(e) {
|
|
40
|
+
return Number(e?.targetId ?? e?.target_id);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function mintUri(payload) {
|
|
44
|
+
const h = createHash("sha256")
|
|
45
|
+
.update(
|
|
46
|
+
[payload.type || "", payload.name || "", payload.text || "", payload.createdAt || ""].join("\0"),
|
|
47
|
+
"utf8",
|
|
48
|
+
)
|
|
49
|
+
.digest("hex")
|
|
50
|
+
.slice(0, 16);
|
|
51
|
+
return `ctx://${payload.type}/${h}`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function nodeRecord(payload) {
|
|
55
|
+
const rec = {
|
|
56
|
+
op: "node",
|
|
57
|
+
uri: payload.uri,
|
|
58
|
+
type: payload.type,
|
|
59
|
+
name: payload.name || "",
|
|
60
|
+
text: payload.text || "",
|
|
61
|
+
status: payload.status || "active",
|
|
62
|
+
};
|
|
63
|
+
const aliases = Array.isArray(payload.aliases)
|
|
64
|
+
? payload.aliases.map((a) => String(a || "").trim()).filter(Boolean)
|
|
65
|
+
: [];
|
|
66
|
+
if (aliases.length) rec.aliases = aliases;
|
|
67
|
+
if (payload.until) rec.until = payload.until;
|
|
68
|
+
if (payload.untilAt) rec.untilAt = payload.untilAt;
|
|
69
|
+
if (payload.fail) rec.fail = payload.fail;
|
|
70
|
+
if (payload.fix) rec.fix = payload.fix;
|
|
71
|
+
if (payload.createdAt) rec.createdAt = payload.createdAt;
|
|
72
|
+
if (payload.updatedAt) rec.updatedAt = payload.updatedAt;
|
|
73
|
+
return rec;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Assign stable uris on business nodes that still lack one. Flushes the db; does not schedule a sidecar write. */
|
|
77
|
+
export function ensureBusinessUris(db) {
|
|
78
|
+
let changed = 0;
|
|
79
|
+
const used = new Set();
|
|
80
|
+
for (const id of db.allNodeIds?.() || []) {
|
|
81
|
+
const payload = payloadOf(db, id);
|
|
82
|
+
if (!isBusinessType(payload.type)) continue;
|
|
83
|
+
const uri = String(payload.uri || "").trim();
|
|
84
|
+
if (uri) used.add(uri);
|
|
85
|
+
}
|
|
86
|
+
for (const id of db.allNodeIds?.() || []) {
|
|
87
|
+
const payload = payloadOf(db, id);
|
|
88
|
+
if (!isBusinessType(payload.type) || payload.status === "archived") continue;
|
|
89
|
+
if (String(payload.uri || "").trim()) continue;
|
|
90
|
+
let uri = mintUri(payload);
|
|
91
|
+
let n = 0;
|
|
92
|
+
while (used.has(uri)) {
|
|
93
|
+
n += 1;
|
|
94
|
+
uri = `${mintUri(payload)}-${n}`;
|
|
95
|
+
}
|
|
96
|
+
used.add(uri);
|
|
97
|
+
db.updatePayload(Number(id), { ...payload, uri });
|
|
98
|
+
changed += 1;
|
|
99
|
+
}
|
|
100
|
+
if (changed) db.flush();
|
|
101
|
+
return changed;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function serializeGraph(db) {
|
|
105
|
+
const nodes = [];
|
|
106
|
+
const uriOf = new Map();
|
|
107
|
+
for (const id of db.allNodeIds?.() || []) {
|
|
108
|
+
const payload = payloadOf(db, id);
|
|
109
|
+
if (!isBusinessType(payload.type) || payload.status === "archived") continue;
|
|
110
|
+
const uri = String(payload.uri || "").trim();
|
|
111
|
+
if (!uri) continue;
|
|
112
|
+
uriOf.set(Number(id), uri);
|
|
113
|
+
nodes.push(nodeRecord(payload));
|
|
114
|
+
}
|
|
115
|
+
nodes.sort((a, b) => String(a.uri).localeCompare(String(b.uri)));
|
|
116
|
+
const edges = [];
|
|
117
|
+
const seen = new Set();
|
|
118
|
+
const want = new Set(BUSINESS_EDGES);
|
|
119
|
+
for (const id of db.allNodeIds?.() || []) {
|
|
120
|
+
const fromUri = uriOf.get(Number(id));
|
|
121
|
+
if (!fromUri) continue;
|
|
122
|
+
const node = db.get(Number(id));
|
|
123
|
+
for (const e of node?.edges || []) {
|
|
124
|
+
const label = e.label || "";
|
|
125
|
+
if (!want.has(label)) continue;
|
|
126
|
+
const toUri = uriOf.get(edgeTarget(e));
|
|
127
|
+
if (!toUri || toUri === fromUri) continue;
|
|
128
|
+
const key = `${fromUri}\0${toUri}\0${label}`;
|
|
129
|
+
if (seen.has(key)) continue;
|
|
130
|
+
seen.add(key);
|
|
131
|
+
edges.push({ op: "edge", from: fromUri, to: toUri, type: label });
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
edges.sort((a, b) => {
|
|
135
|
+
const x = String(a.from).localeCompare(String(b.from));
|
|
136
|
+
if (x) return x;
|
|
137
|
+
const y = String(a.to).localeCompare(String(b.to));
|
|
138
|
+
if (y) return y;
|
|
139
|
+
return String(a.type).localeCompare(String(b.type));
|
|
140
|
+
});
|
|
141
|
+
const header = { v: SIDECAR_VERSION, format: SIDECAR_FORMAT };
|
|
142
|
+
return [header, ...nodes, ...edges].map((row) => JSON.stringify(row)).join("\n") + "\n";
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function parseSidecar(text) {
|
|
146
|
+
const lines = String(text || "").split(/\r?\n/);
|
|
147
|
+
const nodes = [];
|
|
148
|
+
const edges = [];
|
|
149
|
+
let header = null;
|
|
150
|
+
for (const raw of lines) {
|
|
151
|
+
const line = raw.trim();
|
|
152
|
+
if (!line) continue;
|
|
153
|
+
let row;
|
|
154
|
+
try {
|
|
155
|
+
row = JSON.parse(line);
|
|
156
|
+
} catch {
|
|
157
|
+
return { ok: false, message: "invalid jsonl" };
|
|
158
|
+
}
|
|
159
|
+
if (!row || typeof row !== "object") return { ok: false, message: "invalid jsonl" };
|
|
160
|
+
if (row.format === SIDECAR_FORMAT || (row.v && !row.op && !row.uri)) {
|
|
161
|
+
if (header) return { ok: false, message: "duplicate header" };
|
|
162
|
+
header = row;
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
const op = row.op || (row.uri && row.type ? "node" : row.from && row.to ? "edge" : "");
|
|
166
|
+
if (op === "node") {
|
|
167
|
+
if (!isBusinessType(row.type) || !String(row.uri || "").trim()) {
|
|
168
|
+
return { ok: false, message: "invalid node" };
|
|
169
|
+
}
|
|
170
|
+
nodes.push(row);
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
if (op === "edge") {
|
|
174
|
+
const label = row.type || row.label || "";
|
|
175
|
+
if (!BUSINESS_EDGES.includes(label) || !row.from || !row.to) {
|
|
176
|
+
return { ok: false, message: "invalid edge" };
|
|
177
|
+
}
|
|
178
|
+
edges.push({ from: row.from, to: row.to, type: label });
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
return { ok: false, message: "unknown jsonl row" };
|
|
182
|
+
}
|
|
183
|
+
if (!header || header.format !== SIDECAR_FORMAT) {
|
|
184
|
+
return { ok: false, message: "missing sidecar header" };
|
|
185
|
+
}
|
|
186
|
+
return { ok: true, header, nodes, edges };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function readSidecarFile(cwd) {
|
|
190
|
+
const file = sidecarPath(cwd);
|
|
191
|
+
if (!existsSync(file)) return null;
|
|
192
|
+
try {
|
|
193
|
+
return readFileSync(file, "utf8");
|
|
194
|
+
} catch {
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function atomicWrite(file, text) {
|
|
200
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
201
|
+
const tmp = `${file}.tmp`;
|
|
202
|
+
writeFileSync(tmp, text, "utf8");
|
|
203
|
+
try {
|
|
204
|
+
renameSync(tmp, file);
|
|
205
|
+
} catch {
|
|
206
|
+
try {
|
|
207
|
+
unlinkSync(file);
|
|
208
|
+
} catch {
|
|
209
|
+
// dest may not exist
|
|
210
|
+
}
|
|
211
|
+
renameSync(tmp, file);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function rememberHash(cwd, hash) {
|
|
216
|
+
const key = resolve(cwd);
|
|
217
|
+
memoryHash.set(key, hash);
|
|
218
|
+
try {
|
|
219
|
+
writeSidecarHash(key, hash);
|
|
220
|
+
} catch {
|
|
221
|
+
// home settings write is best-effort
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function knownHash(cwd) {
|
|
226
|
+
const key = resolve(cwd);
|
|
227
|
+
return memoryHash.get(key) || sidecarHashOf(key) || "";
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function unlinkIfExists(path) {
|
|
231
|
+
try {
|
|
232
|
+
unlinkSync(path);
|
|
233
|
+
return true;
|
|
234
|
+
} catch (err) {
|
|
235
|
+
if (err && err.code === "ENOENT") return false;
|
|
236
|
+
throw err;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function clearSidecarState(cwd) {
|
|
241
|
+
const key = resolve(cwd || "");
|
|
242
|
+
const prev = timers.get(key);
|
|
243
|
+
if (prev) {
|
|
244
|
+
clearTimeout(prev);
|
|
245
|
+
timers.delete(key);
|
|
246
|
+
}
|
|
247
|
+
memoryHash.delete(key);
|
|
248
|
+
try {
|
|
249
|
+
writeSidecarHash(key, "");
|
|
250
|
+
} catch {
|
|
251
|
+
// home settings write is best-effort
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export function deleteSidecarAt(cwd) {
|
|
256
|
+
const key = resolve(cwd || "");
|
|
257
|
+
if (!key) return { path: "", deleted: false };
|
|
258
|
+
clearSidecarState(key);
|
|
259
|
+
const file = sidecarPath(key);
|
|
260
|
+
const deletedFile = unlinkIfExists(file);
|
|
261
|
+
const deletedTmp = unlinkIfExists(`${file}.tmp`);
|
|
262
|
+
return { path: file, deleted: deletedFile || deletedTmp };
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export function deleteKnownSidecars(extraCwds = []) {
|
|
266
|
+
const seen = new Set();
|
|
267
|
+
const files = [];
|
|
268
|
+
const add = (cwd) => {
|
|
269
|
+
const key = String(cwd || "").trim();
|
|
270
|
+
if (!key) return;
|
|
271
|
+
const resolved = resolve(key);
|
|
272
|
+
if (seen.has(resolved)) return;
|
|
273
|
+
seen.add(resolved);
|
|
274
|
+
const result = deleteSidecarAt(resolved);
|
|
275
|
+
if (result.deleted) files.push(result.path);
|
|
276
|
+
};
|
|
277
|
+
try {
|
|
278
|
+
for (const cwd of listedWorkspaces()) add(cwd);
|
|
279
|
+
} catch {
|
|
280
|
+
// listing is best-effort
|
|
281
|
+
}
|
|
282
|
+
for (const cwd of extraCwds) add(cwd);
|
|
283
|
+
return files;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
export function writeSidecarNow(cwd, db, { force = false } = {}) {
|
|
287
|
+
if (!cwd || !db) return null;
|
|
288
|
+
if (!gitSidecarEnabledOf(readUiSettings())) return null;
|
|
289
|
+
ensureBusinessUris(db);
|
|
290
|
+
const text = serializeGraph(db);
|
|
291
|
+
const hash = hashText(text);
|
|
292
|
+
const file = sidecarPath(cwd);
|
|
293
|
+
if (!force && hash === knownHash(cwd) && existsSync(file)) {
|
|
294
|
+
return { path: file, hash, wrote: false };
|
|
295
|
+
}
|
|
296
|
+
atomicWrite(file, text);
|
|
297
|
+
rememberHash(cwd, hash);
|
|
298
|
+
return { path: file, hash, wrote: true };
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
export function scheduleSidecarWrite(cwd, db) {
|
|
302
|
+
if (!cwd || !db) return;
|
|
303
|
+
if (!gitSidecarEnabledOf(readUiSettings())) return;
|
|
304
|
+
const key = resolve(cwd);
|
|
305
|
+
const prev = timers.get(key);
|
|
306
|
+
if (prev) clearTimeout(prev);
|
|
307
|
+
const timer = setTimeout(() => {
|
|
308
|
+
timers.delete(key);
|
|
309
|
+
try {
|
|
310
|
+
writeSidecarNow(key, db);
|
|
311
|
+
} catch {
|
|
312
|
+
// disk errors are non-fatal
|
|
313
|
+
}
|
|
314
|
+
}, SIDECAR_DEBOUNCE_MS);
|
|
315
|
+
timers.set(key, timer);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
export function flushSidecarNow(cwd, db) {
|
|
319
|
+
const key = resolve(cwd || "");
|
|
320
|
+
const prev = timers.get(key);
|
|
321
|
+
if (prev) {
|
|
322
|
+
clearTimeout(prev);
|
|
323
|
+
timers.delete(key);
|
|
324
|
+
}
|
|
325
|
+
if (!cwd || !db) return null;
|
|
326
|
+
return writeSidecarNow(key, db);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
async function applySidecar(db, parsed) {
|
|
330
|
+
const store = await import("./store.js");
|
|
331
|
+
const uriSet = new Set();
|
|
332
|
+
store.runWithoutSidecar(() => {
|
|
333
|
+
const idByUri = new Map();
|
|
334
|
+
for (const row of parsed.nodes) {
|
|
335
|
+
const uri = String(row.uri || "").trim();
|
|
336
|
+
uriSet.add(uri);
|
|
337
|
+
const payload = {
|
|
338
|
+
type: row.type,
|
|
339
|
+
name: row.name || "",
|
|
340
|
+
text: row.text || "",
|
|
341
|
+
uri,
|
|
342
|
+
status: row.status && row.status !== "archived" ? row.status : "active",
|
|
343
|
+
aliases: Array.isArray(row.aliases) ? row.aliases : [],
|
|
344
|
+
until: row.until || "",
|
|
345
|
+
untilAt: row.untilAt || "",
|
|
346
|
+
fail: row.fail || "",
|
|
347
|
+
fix: row.fix || "",
|
|
348
|
+
createdAt: row.createdAt || "",
|
|
349
|
+
updatedAt: row.updatedAt || "",
|
|
350
|
+
};
|
|
351
|
+
if (!payload.until) delete payload.until;
|
|
352
|
+
if (!payload.untilAt) delete payload.untilAt;
|
|
353
|
+
if (!payload.fail) delete payload.fail;
|
|
354
|
+
if (!payload.fix) delete payload.fix;
|
|
355
|
+
let dest = store.findByUri(db, uri);
|
|
356
|
+
if (dest == null) dest = store.findMergeTarget(db, payload);
|
|
357
|
+
if (dest != null) {
|
|
358
|
+
store.mergeInto(db, dest, payload);
|
|
359
|
+
idByUri.set(uri, dest);
|
|
360
|
+
} else {
|
|
361
|
+
idByUri.set(uri, store.insertNode(db, payload));
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
const wantEdges = new Set();
|
|
365
|
+
for (const e of parsed.edges) {
|
|
366
|
+
const from = idByUri.get(e.from) ?? store.findByUri(db, e.from);
|
|
367
|
+
const to = idByUri.get(e.to) ?? store.findByUri(db, e.to);
|
|
368
|
+
if (from == null || to == null || from === to) continue;
|
|
369
|
+
store.ensureLink(db, from, to, e.type);
|
|
370
|
+
wantEdges.add(`${e.from}\0${e.to}\0${e.type}`);
|
|
371
|
+
}
|
|
372
|
+
const uriById = new Map();
|
|
373
|
+
for (const id of db.allNodeIds?.() || []) {
|
|
374
|
+
const payload = payloadOf(db, id);
|
|
375
|
+
if (!isBusinessType(payload.type)) continue;
|
|
376
|
+
const uri = String(payload.uri || "").trim();
|
|
377
|
+
if (uri) uriById.set(Number(id), uri);
|
|
378
|
+
}
|
|
379
|
+
for (const id of db.allNodeIds?.() || []) {
|
|
380
|
+
const fromUri = uriById.get(Number(id));
|
|
381
|
+
if (!fromUri) continue;
|
|
382
|
+
const node = db.get(Number(id));
|
|
383
|
+
for (const e of node?.edges || []) {
|
|
384
|
+
const label = e.label || "";
|
|
385
|
+
if (!BUSINESS_EDGES.includes(label)) continue;
|
|
386
|
+
const toUri = uriById.get(edgeTarget(e));
|
|
387
|
+
if (!toUri) continue;
|
|
388
|
+
if (!wantEdges.has(`${fromUri}\0${toUri}\0${label}`)) {
|
|
389
|
+
store.dropLink(db, id, edgeTarget(e), label);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
for (const id of db.allNodeIds?.() || []) {
|
|
394
|
+
const payload = payloadOf(db, id);
|
|
395
|
+
if (!isBusinessType(payload.type) || payload.status === "archived") continue;
|
|
396
|
+
const uri = String(payload.uri || "").trim();
|
|
397
|
+
if (!uri || !uriSet.has(uri)) store.archiveNode(db, id);
|
|
398
|
+
}
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Import when the sidecar changed outside this process (clone / git pull).
|
|
404
|
+
* Write is skipped when the switch is off; import still runs.
|
|
405
|
+
*/
|
|
406
|
+
export async function syncSidecarOnOpen(cwd, db) {
|
|
407
|
+
if (!cwd || !db) return { action: "skip" };
|
|
408
|
+
const key = resolve(cwd);
|
|
409
|
+
const text = readSidecarFile(key);
|
|
410
|
+
if (text == null) {
|
|
411
|
+
if (!gitSidecarEnabledOf(readUiSettings())) return { action: "skip" };
|
|
412
|
+
const wrote = writeSidecarNow(key, db);
|
|
413
|
+
return { action: "bootstrap-write", hash: wrote?.hash || "" };
|
|
414
|
+
}
|
|
415
|
+
const fileHash = hashText(text);
|
|
416
|
+
if (fileHash === knownHash(key)) return { action: "skip" };
|
|
417
|
+
const parsed = parseSidecar(text);
|
|
418
|
+
if (!parsed.ok) return { action: "skip-invalid", message: parsed.message };
|
|
419
|
+
await applySidecar(db, parsed);
|
|
420
|
+
rememberHash(key, fileHash);
|
|
421
|
+
return { action: "import", hash: fileHash, nodes: parsed.nodes.length, edges: parsed.edges.length };
|
|
422
|
+
}
|
package/lib/store.js
CHANGED
|
@@ -2,6 +2,14 @@ import { mkdirSync } from "node:fs";
|
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
import { join, resolve } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { rememberWorkspace } from "./settings.js";
|
|
6
|
+
import {
|
|
7
|
+
deleteKnownSidecars,
|
|
8
|
+
flushSidecarNow,
|
|
9
|
+
scheduleSidecarWrite,
|
|
10
|
+
syncSidecarOnOpen,
|
|
11
|
+
writeSidecarNow,
|
|
12
|
+
} from "./sidecar.js";
|
|
5
13
|
import { DIM, EDGE_LABELS, BUSINESS_EDGES, NODE_TYPES, indexTextFor, nowIso, weightOf, zeroVector } from "./schema.js";
|
|
6
14
|
import { clipToTokenBudget, estimateTokens } from "./tokens.js";
|
|
7
15
|
import { isStalePayload, parseUntilFromText, queryMentionsUntil, resolveUntilAt } from "./until.js";
|
|
@@ -31,6 +39,31 @@ function loadEngine() {
|
|
|
31
39
|
|
|
32
40
|
const dbs = new Map();
|
|
33
41
|
let activeCwd = null;
|
|
42
|
+
let sidecarPaused = false;
|
|
43
|
+
|
|
44
|
+
export function cwdForDb(db) {
|
|
45
|
+
for (const [cwd, inst] of dbs) {
|
|
46
|
+
if (inst === db) return cwd;
|
|
47
|
+
}
|
|
48
|
+
return "";
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function runWithoutSidecar(fn) {
|
|
52
|
+
const prev = sidecarPaused;
|
|
53
|
+
sidecarPaused = true;
|
|
54
|
+
try {
|
|
55
|
+
return fn();
|
|
56
|
+
} finally {
|
|
57
|
+
sidecarPaused = prev;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function persist(db) {
|
|
62
|
+
db.flush();
|
|
63
|
+
if (sidecarPaused) return;
|
|
64
|
+
const cwd = cwdForDb(db);
|
|
65
|
+
if (cwd) scheduleSidecarWrite(cwd, db);
|
|
66
|
+
}
|
|
34
67
|
|
|
35
68
|
export function dbPathFor(cwd) {
|
|
36
69
|
return join(resolve(cwd), ".dsh", "trivium.tdb");
|
|
@@ -53,6 +86,19 @@ export function listOpenWorkspaces() {
|
|
|
53
86
|
}));
|
|
54
87
|
}
|
|
55
88
|
|
|
89
|
+
/** Replace jsonl from the current .tdb. Overwrite in place (same path) so git diffs lines. */
|
|
90
|
+
export function generateSidecarFromDb(cwd, db) {
|
|
91
|
+
const wrote = writeSidecarNow(cwd, db, { force: true });
|
|
92
|
+
if (!wrote) return { ok: false, message: "git sidecar off" };
|
|
93
|
+
return { ok: true, action: "generate", cwd, path: wrote.path, wrote: wrote.wrote };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Delete known workspace trivium.jsonl files. .tdb stays. */
|
|
97
|
+
export function deleteGitSidecars() {
|
|
98
|
+
const files = deleteKnownSidecars([...dbs.keys(), activeCwd].filter(Boolean));
|
|
99
|
+
return { ok: true, action: "delete", files };
|
|
100
|
+
}
|
|
101
|
+
|
|
56
102
|
export function nodeCountOf(db) {
|
|
57
103
|
try {
|
|
58
104
|
if (typeof db.nodeCount === "function") return db.nodeCount();
|
|
@@ -78,11 +124,26 @@ export async function openWorkspaceDb(cwd) {
|
|
|
78
124
|
ensureWorkspaceRoot(db, cwd);
|
|
79
125
|
rebuildTextIndex(db);
|
|
80
126
|
dbs.set(key, db);
|
|
127
|
+
try {
|
|
128
|
+
rememberWorkspace(key);
|
|
129
|
+
} catch {
|
|
130
|
+
// listing is best-effort; opening the store still succeeds
|
|
131
|
+
}
|
|
132
|
+
try {
|
|
133
|
+
await syncSidecarOnOpen(key, db);
|
|
134
|
+
} catch {
|
|
135
|
+
// sidecar is best-effort; the .tdb still opens
|
|
136
|
+
}
|
|
81
137
|
return db;
|
|
82
138
|
}
|
|
83
139
|
|
|
84
140
|
export function closeAll() {
|
|
85
|
-
for (const db of dbs
|
|
141
|
+
for (const [cwd, db] of dbs) {
|
|
142
|
+
try {
|
|
143
|
+
flushSidecarNow(cwd, db);
|
|
144
|
+
} catch {
|
|
145
|
+
// ignore
|
|
146
|
+
}
|
|
86
147
|
try {
|
|
87
148
|
db.flush();
|
|
88
149
|
db.close();
|
|
@@ -257,7 +318,7 @@ export function mergeInto(db, id, incoming) {
|
|
|
257
318
|
});
|
|
258
319
|
db.updatePayload(Number(id), next);
|
|
259
320
|
indexNode(db, Number(id), next);
|
|
260
|
-
db
|
|
321
|
+
persist(db);
|
|
261
322
|
queueEmbed(db, Number(id), next);
|
|
262
323
|
return Number(id);
|
|
263
324
|
}
|
|
@@ -333,7 +394,7 @@ export function archiveNode(db, id) {
|
|
|
333
394
|
};
|
|
334
395
|
db.updatePayload(Number(id), payload);
|
|
335
396
|
rebuildTextIndex(db);
|
|
336
|
-
db
|
|
397
|
+
persist(db);
|
|
337
398
|
return true;
|
|
338
399
|
}
|
|
339
400
|
|
|
@@ -342,7 +403,7 @@ export function deleteNode(db, id) {
|
|
|
342
403
|
if (!db.contains?.(n) && !db.get(n)) return false;
|
|
343
404
|
db.delete(n);
|
|
344
405
|
rebuildTextIndex(db);
|
|
345
|
-
db
|
|
406
|
+
persist(db);
|
|
346
407
|
return true;
|
|
347
408
|
}
|
|
348
409
|
|
|
@@ -384,7 +445,7 @@ export function updateNode(db, id, patch = {}) {
|
|
|
384
445
|
}
|
|
385
446
|
db.updatePayload(nid, body);
|
|
386
447
|
indexNode(db, nid, body);
|
|
387
|
-
db
|
|
448
|
+
persist(db);
|
|
388
449
|
queueEmbed(db, nid, body);
|
|
389
450
|
return body;
|
|
390
451
|
}
|
|
@@ -535,7 +596,7 @@ export function ensureLink(db, from, to, label) {
|
|
|
535
596
|
if (!Number.isFinite(src) || !Number.isFinite(dst) || src === dst) return false;
|
|
536
597
|
if (hasEdge(db, src, dst, label)) return false;
|
|
537
598
|
db.link(src, dst, label, weightOf(label));
|
|
538
|
-
db
|
|
599
|
+
persist(db);
|
|
539
600
|
return true;
|
|
540
601
|
}
|
|
541
602
|
|
|
@@ -548,7 +609,7 @@ export function dropLink(db, from, to, label) {
|
|
|
548
609
|
try {
|
|
549
610
|
if (label) db.unlink(src, dst, label);
|
|
550
611
|
else db.unlink(src, dst);
|
|
551
|
-
db
|
|
612
|
+
persist(db);
|
|
552
613
|
return true;
|
|
553
614
|
} catch {
|
|
554
615
|
return false;
|
|
@@ -591,7 +652,7 @@ export function insertNode(db, payload, { linkToWorkspace = true } = {}) {
|
|
|
591
652
|
const root = workspaceId(db);
|
|
592
653
|
if (root != null) ensureLink(db, id, root, EDGE_LABELS.inWorkspace);
|
|
593
654
|
}
|
|
594
|
-
db
|
|
655
|
+
persist(db);
|
|
595
656
|
queueEmbed(db, id, body);
|
|
596
657
|
return id;
|
|
597
658
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-trivium",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.13",
|
|
4
4
|
"description": "In-process graph memory for DeepSeek Harness, backed by TriviumDB.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -42,6 +42,9 @@
|
|
|
42
42
|
"smoke-p7": "node scripts/smoke-p7.mjs",
|
|
43
43
|
"smoke-p8": "node scripts/smoke-p8.mjs",
|
|
44
44
|
"smoke-p10": "node scripts/smoke-p10.mjs",
|
|
45
|
+
"smoke-p11": "node scripts/smoke-p11.mjs",
|
|
46
|
+
"smoke-p12": "node scripts/smoke-p12.mjs",
|
|
47
|
+
"preuninstall": "node ./lib/purge.js",
|
|
45
48
|
"verify-live-p2": "node scripts/verify-live-p2.mjs"
|
|
46
49
|
},
|
|
47
50
|
"engines": {
|