dsh-trivium 0.4.10 → 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/lib/server.js CHANGED
@@ -1,8 +1,10 @@
1
1
  import { existsSync } from "node:fs";
2
2
  import {
3
+ generateSidecarFromDb,
3
4
  archiveNode,
4
5
  backfillEmbeddings,
5
6
  dbPathFor,
7
+ deleteGitSidecars,
6
8
  deleteNode,
7
9
  exportGraph,
8
10
  getActiveCwd,
@@ -15,7 +17,7 @@ import {
15
17
  updateNode,
16
18
  } from "./store.js";
17
19
  import { embeddingPublicStatus } from "./embed.js";
18
- import { chipsEnabledOf, recallModeOf, sessionLayerEnabledOf, writeUiSettings } from "./settings.js";
20
+ import { chipsEnabledOf, configPanelOpenOf, gitSidecarEnabledOf, pluginLocaleOf, recallModeOf, sessionLayerEnabledOf, writeUiSettings } from "./settings.js";
19
21
  import { discoverWorkbuddyFiles, discoverExternalFiles, exportMarkdown, importWorkbuddy, importExternal } from "./markdown.js";
20
22
  import { checkNpmUpdate, installedVersion } from "./update.js";
21
23
  import { writeGateMessage } from "./hygiene.js";
@@ -116,6 +118,9 @@ async function handle(req, res, { liveOptions, status }) {
116
118
  extractEnabled: opts.extractEnabled !== false,
117
119
  chipsEnabled: chipsEnabledOf(opts),
118
120
  sessionLayerEnabled: opts.sessionLayerEnabled === true,
121
+ gitSidecarEnabled: gitSidecarEnabledOf(opts),
122
+ pluginLocale: pluginLocaleOf(opts),
123
+ configPanelOpen: configPanelOpenOf(opts),
119
124
  writeApproval: !!opts.writeApproval,
120
125
  mapTokenBudget: opts.mapTokenBudget || 400,
121
126
  workspaces: listOpenWorkspaces(),
@@ -138,6 +143,29 @@ async function handle(req, res, { liveOptions, status }) {
138
143
  return;
139
144
  }
140
145
 
146
+ if (path === "/api/dsh-trivium/sidecar/generate" && method === "POST") {
147
+ if (!gitSidecarEnabledOf(liveOptions())) {
148
+ sendJson(res, 400, { ok: false, message: "git sidecar off" });
149
+ return;
150
+ }
151
+ const { cwd, db } = await dbForRequest(url);
152
+ if (!db) {
153
+ sendJson(res, 400, { ok: false, message: "no workspace" });
154
+ return;
155
+ }
156
+ sendJson(res, 200, generateSidecarFromDb(cwd, db));
157
+ return;
158
+ }
159
+
160
+ if (path === "/api/dsh-trivium/sidecar/delete" && method === "POST") {
161
+ if (gitSidecarEnabledOf(liveOptions())) {
162
+ sendJson(res, 400, { ok: false, message: "turn sidecar off first" });
163
+ return;
164
+ }
165
+ sendJson(res, 200, deleteGitSidecars());
166
+ return;
167
+ }
168
+
141
169
  if (path === "/api/dsh-trivium/export" && method === "GET") {
142
170
  const { cwd, db } = await dbForRequest(url);
143
171
  if (!db) {
package/lib/settings.js CHANGED
@@ -82,6 +82,44 @@ export function sessionLayerEnabledOf(opts = {}) {
82
82
  return chipsEnabledOf(opts) && opts.sessionLayerEnabled === true;
83
83
  }
84
84
 
85
+ /** Git JSONL sidecar write. Missing key = off. Import still runs when this is off. */
86
+ export function gitSidecarEnabledOf(opts = {}) {
87
+ return opts.gitSidecarEnabled === true;
88
+ }
89
+
90
+ /** Plugin UI language. Missing key = follow the DSH host. */
91
+ export function pluginLocaleOf(opts = {}) {
92
+ const v = String(opts.pluginLocale || "").toLowerCase();
93
+ if (v === "zh" || v === "en" || v === "host") return v;
94
+ return "host";
95
+ }
96
+
97
+ /** Settings fold. Missing key = collapsed. */
98
+ export function configPanelOpenOf(opts = {}) {
99
+ return opts.configPanelOpen === true;
100
+ }
101
+
102
+ export function sidecarHashOf(cwd) {
103
+ const raw = readUiSettings().sidecarHashByCwd;
104
+ if (!raw || typeof raw !== "object") return "";
105
+ const key = resolve(String(cwd || ""));
106
+ return String(raw[key] || "");
107
+ }
108
+
109
+ export function writeSidecarHash(cwd, hash) {
110
+ const key = resolve(String(cwd || ""));
111
+ if (!process.env.DSH_TRIVIUM_SETTINGS && isInsideDir(tmpdir(), key)) {
112
+ return readUiSettings();
113
+ }
114
+ const prev = readUiSettings();
115
+ const sidecarHashByCwd = {
116
+ ...(prev.sidecarHashByCwd && typeof prev.sidecarHashByCwd === "object" ? prev.sidecarHashByCwd : {}),
117
+ };
118
+ if (!hash) delete sidecarHashByCwd[key];
119
+ else sidecarHashByCwd[key] = String(hash);
120
+ return persistSettings({ ...prev, sidecarHashByCwd });
121
+ }
122
+
85
123
  export function applyRecallMode(target, mode) {
86
124
  const next = mode === "auto" || mode === "anchor" ? mode : "off";
87
125
  target.recallMode = next;
@@ -110,6 +148,12 @@ export function writeUiSettings(patch) {
110
148
  if (typeof patch.extractEnabled === "boolean") next.extractEnabled = patch.extractEnabled;
111
149
  if (typeof patch.chipsEnabled === "boolean") next.chipsEnabled = patch.chipsEnabled;
112
150
  if (typeof patch.sessionLayerEnabled === "boolean") next.sessionLayerEnabled = patch.sessionLayerEnabled;
151
+ if (typeof patch.gitSidecarEnabled === "boolean") next.gitSidecarEnabled = patch.gitSidecarEnabled;
152
+ if (typeof patch.configPanelOpen === "boolean") next.configPanelOpen = patch.configPanelOpen;
153
+ if (typeof patch.pluginLocale === "string") {
154
+ const loc = String(patch.pluginLocale || "").toLowerCase();
155
+ next.pluginLocale = loc === "zh" || loc === "en" ? loc : "host";
156
+ }
113
157
  if (typeof patch.embeddingEnabled === "boolean") next.embeddingEnabled = patch.embeddingEnabled;
114
158
  if (typeof patch.embeddingUrl === "string") {
115
159
  const url = patch.embeddingUrl.trim();
@@ -126,6 +170,11 @@ export function writeUiSettings(patch) {
126
170
  }
127
171
  if (Array.isArray(patch.workspaces)) next.workspaces = patch.workspaces;
128
172
  else if (Array.isArray(prev.workspaces)) next.workspaces = prev.workspaces;
173
+ if (patch.sidecarHashByCwd && typeof patch.sidecarHashByCwd === "object") {
174
+ next.sidecarHashByCwd = patch.sidecarHashByCwd;
175
+ } else if (prev.sidecarHashByCwd && typeof prev.sidecarHashByCwd === "object") {
176
+ next.sidecarHashByCwd = prev.sidecarHashByCwd;
177
+ }
129
178
  return persistSettings(next);
130
179
  }
131
180
 
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
@@ -3,6 +3,13 @@ import { createRequire } from "node:module";
3
3
  import { join, resolve } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { rememberWorkspace } from "./settings.js";
6
+ import {
7
+ deleteKnownSidecars,
8
+ flushSidecarNow,
9
+ scheduleSidecarWrite,
10
+ syncSidecarOnOpen,
11
+ writeSidecarNow,
12
+ } from "./sidecar.js";
6
13
  import { DIM, EDGE_LABELS, BUSINESS_EDGES, NODE_TYPES, indexTextFor, nowIso, weightOf, zeroVector } from "./schema.js";
7
14
  import { clipToTokenBudget, estimateTokens } from "./tokens.js";
8
15
  import { isStalePayload, parseUntilFromText, queryMentionsUntil, resolveUntilAt } from "./until.js";
@@ -32,6 +39,31 @@ function loadEngine() {
32
39
 
33
40
  const dbs = new Map();
34
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
+ }
35
67
 
36
68
  export function dbPathFor(cwd) {
37
69
  return join(resolve(cwd), ".dsh", "trivium.tdb");
@@ -54,6 +86,19 @@ export function listOpenWorkspaces() {
54
86
  }));
55
87
  }
56
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
+
57
102
  export function nodeCountOf(db) {
58
103
  try {
59
104
  if (typeof db.nodeCount === "function") return db.nodeCount();
@@ -84,11 +129,21 @@ export async function openWorkspaceDb(cwd) {
84
129
  } catch {
85
130
  // listing is best-effort; opening the store still succeeds
86
131
  }
132
+ try {
133
+ await syncSidecarOnOpen(key, db);
134
+ } catch {
135
+ // sidecar is best-effort; the .tdb still opens
136
+ }
87
137
  return db;
88
138
  }
89
139
 
90
140
  export function closeAll() {
91
- for (const db of dbs.values()) {
141
+ for (const [cwd, db] of dbs) {
142
+ try {
143
+ flushSidecarNow(cwd, db);
144
+ } catch {
145
+ // ignore
146
+ }
92
147
  try {
93
148
  db.flush();
94
149
  db.close();
@@ -263,7 +318,7 @@ export function mergeInto(db, id, incoming) {
263
318
  });
264
319
  db.updatePayload(Number(id), next);
265
320
  indexNode(db, Number(id), next);
266
- db.flush();
321
+ persist(db);
267
322
  queueEmbed(db, Number(id), next);
268
323
  return Number(id);
269
324
  }
@@ -339,7 +394,7 @@ export function archiveNode(db, id) {
339
394
  };
340
395
  db.updatePayload(Number(id), payload);
341
396
  rebuildTextIndex(db);
342
- db.flush();
397
+ persist(db);
343
398
  return true;
344
399
  }
345
400
 
@@ -348,7 +403,7 @@ export function deleteNode(db, id) {
348
403
  if (!db.contains?.(n) && !db.get(n)) return false;
349
404
  db.delete(n);
350
405
  rebuildTextIndex(db);
351
- db.flush();
406
+ persist(db);
352
407
  return true;
353
408
  }
354
409
 
@@ -390,7 +445,7 @@ export function updateNode(db, id, patch = {}) {
390
445
  }
391
446
  db.updatePayload(nid, body);
392
447
  indexNode(db, nid, body);
393
- db.flush();
448
+ persist(db);
394
449
  queueEmbed(db, nid, body);
395
450
  return body;
396
451
  }
@@ -541,7 +596,7 @@ export function ensureLink(db, from, to, label) {
541
596
  if (!Number.isFinite(src) || !Number.isFinite(dst) || src === dst) return false;
542
597
  if (hasEdge(db, src, dst, label)) return false;
543
598
  db.link(src, dst, label, weightOf(label));
544
- db.flush();
599
+ persist(db);
545
600
  return true;
546
601
  }
547
602
 
@@ -554,7 +609,7 @@ export function dropLink(db, from, to, label) {
554
609
  try {
555
610
  if (label) db.unlink(src, dst, label);
556
611
  else db.unlink(src, dst);
557
- db.flush();
612
+ persist(db);
558
613
  return true;
559
614
  } catch {
560
615
  return false;
@@ -597,7 +652,7 @@ export function insertNode(db, payload, { linkToWorkspace = true } = {}) {
597
652
  const root = workspaceId(db);
598
653
  if (root != null) ensureLink(db, id, root, EDGE_LABELS.inWorkspace);
599
654
  }
600
- db.flush();
655
+ persist(db);
601
656
  queueEmbed(db, id, body);
602
657
  return id;
603
658
  }