dsh-trivium 0.4.2 → 0.4.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/README.md CHANGED
@@ -54,7 +54,7 @@ dsh plugin --profile web add dsh-trivium
54
54
 
55
55
  - **跨会话** — 会话 A 记下「鉴权走 header X」,会话 B 调用 `ctx_find("鉴权")` 命中,并带上它连着谁(`about` / `decided` / `broke` / `fixed`)。
56
56
  - **默认安静** — 新会话只注入一张短地图(≤400 token)。模型要用再调工具,不会每一步灌满。
57
- - **会话图** — compaction 切一段就收成一个方框;点方框用 DSH 原生 fork 开分支;勾选的记忆芯片从当前这段的下一轮注入。
57
+ - **会话图** — compaction 切一段就收成一个方框;旧会话可点「更新检查点」补上已经发生过的压缩 / 分叉;勾选的记忆芯片从当前这段的下一轮注入。
58
58
  - **人能改错** — 会话图芯片条可新增、归档、删除;设置页管搜、改名 / 正文、合并、全局开关。
59
59
  - **抽取偏严** — 闲聊、一次性改文件、密钥**不会从对话自动入库**。芯片「新增」按你写的存(点一次整段一条)。
60
60
  - **可选 embedding** — 默认关。官方 DeepSeek 对话接口没有 embeddings;可填 OpenAI 兼容地址。
@@ -80,6 +80,7 @@ dsh plugin --profile web add dsh-trivium
80
80
 
81
81
  - 没压过上下文:只有一个「后续」方框。
82
82
  - 一次成功的 compaction 之后:左边历史检查点(摘要来自 DSH),右边仍是「后续」。
83
+ - 装插件之前已经压过 / 分过叉的旧会话:点「更新检查点」,从当前对话里的压缩标记和侧边栏分叉补方框(可重复点,不会双写)。没压过就仍只有「后续」——会话长、工具多不等于 DSH 已经压缩(默认约窗口 80% 才自动压)。
83
84
  - 方框自己不压缩。质量来自 DSH 已经做的摘要替换;fork 时不会再总结一遍。
84
85
  - 「改名」只改方框标题,不动 DSH 摘要。点方框可跳回对话里对应的检查点,或打开轨迹。
85
86
 
@@ -146,7 +147,7 @@ dsh plugin --profile web add dsh-trivium
146
147
  - 抽取会漏;脏数据可在芯片条归档 / 删除,或到设置里改、合并。
147
148
  - 同一个 `.tdb` 不要两个 Node 进程同时打开(正常只开一个 `dsh web` 即可)。
148
149
  - Markdown 导出是给人看的,不能再解析回去。WorkBuddy 导入是一次性,不是双向同步。
149
- - 会话图只投影 compaction 与 fork,不会按消息条数自己切方框。
150
+ - 会话图只投影 compaction 与 fork,不会按消息条数自己切方框。窗口没到 DSH 压缩线时,「更新检查点」也补不出历史方框。
150
151
  - 改插件设置后需要重启 `dsh web`。
151
152
  - 宿主升到 rc.8 后,DSH 自己的旧会话库可能打不开(官方 SQLite 格式不兼容)。工作区里的 `.tdb` 图记忆还在;对不上号的旧会话图方框可以当情节残留,不影响 `ctx_find`。
152
153
 
@@ -154,6 +155,10 @@ dsh plugin --profile web add dsh-trivium
154
155
 
155
156
  ## 更新说明
156
157
 
158
+ **0.4.4** — 依赖 `triviumdb@0.7.5`:入边、按 label 扩邻、按 label 删边走引擎 API,不再全表扫边。find / 会话图行为不变。
159
+
160
+ **0.4.3** — 会话图「更新检查点」:把当前对话里 DSH 已有的压缩标记和侧边栏 fork 补进图,给装插件之前的旧会话用。没压过的会话仍然只有「后续」。
161
+
157
162
  **0.4.2** — 会话图芯片条可新增(整段粘贴一条)、批量归档 / 删除。自动抽取仍偏严;芯片新增按你写的入库。
158
163
 
159
164
  **0.4.1** — 宿主钉到 `@deepseek-ai/dsh@0.1.0-rc.8`。功能与 0.4.0 相同。已在 rc.8 上确认:设置「Trivium 记忆」、新会话短地图、跨会话 `ctx_find`、会话图标签。
@@ -165,6 +170,6 @@ dsh plugin --profile web add dsh-trivium
165
170
  ## 发布信息
166
171
 
167
172
  - GitHub: https://github.com/QWQcool/dsh-trivium
168
- - npm: [`dsh-trivium@0.4.2`](https://www.npmjs.com/package/dsh-trivium)
173
+ - npm: [`dsh-trivium@0.4.4`](https://www.npmjs.com/package/dsh-trivium)
169
174
  - 测试宿主:`@deepseek-ai/dsh@0.1.0-rc.8`
170
- - License: MIT(依赖 [TriviumDB](https://github.com/TriviumDB/triviumdb) 为 Apache-2.0)
175
+ - License: MIT(依赖 [TriviumDB](https://github.com/YoKONCy/TriviumDB) 为 Apache-2.0)
package/lib/client.js CHANGED
@@ -980,6 +980,67 @@ window.__ModuleLoader__.load({
980
980
  return best;
981
981
  }
982
982
 
983
+ function collectChatCompactions(chat) {
984
+ const nodes = chat?.nodes;
985
+ if (!nodes || typeof nodes.values !== "function") return [];
986
+ const rows = [];
987
+ const seen = new Set();
988
+ for (const n of nodes.values()) {
989
+ const kind = n.kind;
990
+ const data = n.data || {};
991
+ let seq;
992
+ let summary = "";
993
+ let compactionId = "";
994
+ if (kind === "compaction") {
995
+ seq = Number(n.anchorSeq ?? data.seq);
996
+ summary = String(data.summary || "");
997
+ compactionId = String(data.compactionId || data.id || "");
998
+ } else if (kind === "manual-compaction") {
999
+ const landed = data.compaction;
1000
+ if (!landed) continue;
1001
+ seq = Number(landed.seq ?? n.anchorSeq ?? data.command?.seq);
1002
+ summary = String(landed.summary || "");
1003
+ compactionId = String(landed.compactionId || data.command?.commandId || "");
1004
+ } else continue;
1005
+ if (!Number.isFinite(seq) || seen.has(seq)) continue;
1006
+ seen.add(seq);
1007
+ rows.push({ atSeq: seq, compactionId, summary });
1008
+ }
1009
+ return rows.sort((a, b) => a.atSeq - b.atSeq);
1010
+ }
1011
+
1012
+ function parentIdOf(row) {
1013
+ return String(row?.parentSessionId || row?.parentId || "");
1014
+ }
1015
+
1016
+ function collectSessionForks(sessionId, byId, seedLengthOf) {
1017
+ const forks = [];
1018
+ const seen = new Set();
1019
+ const add = (childId, parentId) => {
1020
+ const child = String(childId || "");
1021
+ const parent = String(parentId || "");
1022
+ if (!child || !parent || seen.has(child + ":" + parent)) return;
1023
+ const row = byId[child];
1024
+ if (row?.origin === "subagent") return;
1025
+ seen.add(child + ":" + parent);
1026
+ const seed = typeof seedLengthOf === "function" ? seedLengthOf(child) : null;
1027
+ const n = Number(seed);
1028
+ forks.push({
1029
+ childSessionId: child,
1030
+ parentSessionId: parent,
1031
+ atSeq: Number.isFinite(n) && n > 0 ? n - 1 : undefined,
1032
+ });
1033
+ };
1034
+ const mine = String(sessionId || "");
1035
+ const rows = byId || {};
1036
+ add(mine, parentIdOf(rows[mine]));
1037
+ for (const id of Object.keys(rows)) {
1038
+ const parent = parentIdOf(rows[id]);
1039
+ if (parent && (parent === mine || id === mine)) add(id, parent);
1040
+ }
1041
+ return forks;
1042
+ }
1043
+
983
1044
  function scrollConversation({ key, bottom }) {
984
1045
  let n = 0;
985
1046
  const tick = () => {
@@ -1669,7 +1730,7 @@ window.__ModuleLoader__.load({
1669
1730
 
1670
1731
  let pendingSessionNav = null;
1671
1732
 
1672
- function SessionMapView({ sessionId, useSessions, useSession, fork, open, renameSession, actions }) {
1733
+ function SessionMapView({ sessionId, useSessions, useSession, fork, open, renameSession, actions, seedLengthOf }) {
1673
1734
  const canSetView = typeof actions?.setView === "function";
1674
1735
  const chat = useSession((s) => s.chat);
1675
1736
  const cwd = useSessions((s) => (sessionId == null ? "" : s.byId[sessionId]?.cwd || ""));
@@ -1701,6 +1762,7 @@ window.__ModuleLoader__.load({
1701
1762
  const [draftOpen, setDraftOpen] = react.useState(false);
1702
1763
  const [draftText, setDraftText] = react.useState("");
1703
1764
  const [forkDraft, setForkDraft] = react.useState(null);
1765
+ const [backfillNote, setBackfillNote] = react.useState("");
1704
1766
  const canvasRef = react.useRef(null);
1705
1767
  const dragRef = react.useRef(null);
1706
1768
  const userMovedRef = react.useRef(false);
@@ -1759,6 +1821,34 @@ window.__ModuleLoader__.load({
1759
1821
  }
1760
1822
  }, [qs, sessionId]);
1761
1823
 
1824
+ const syncCheckpoints = async () => {
1825
+ if (!sessionId || busy) return;
1826
+ setBusy(true);
1827
+ setError("");
1828
+ try {
1829
+ const compactations = collectChatCompactions(chat);
1830
+ const forks = collectSessionForks(sessionId, sessionById, seedLengthOf);
1831
+ const resp = await api("/api/dsh-trivium/map/backfill" + qs, {
1832
+ method: "POST",
1833
+ headers: { "content-type": "application/json" },
1834
+ body: JSON.stringify({ sessionId, compactations, forks }),
1835
+ });
1836
+ setNodes(resp.nodes || []);
1837
+ setEdges(resp.edges || []);
1838
+ const checks = Number(resp.checkpoints) || 0;
1839
+ const forkN = Number(resp.forks) || 0;
1840
+ if (!checks && !forkN) {
1841
+ setBackfillNote("没有发现压缩标记或分叉。没压过的会话本来就只有「后续」。");
1842
+ } else {
1843
+ setBackfillNote("已同步 " + checks + " 个检查点、" + forkN + " 条分叉。");
1844
+ }
1845
+ } catch (err) {
1846
+ setError(String(err.message || err));
1847
+ } finally {
1848
+ setBusy(false);
1849
+ }
1850
+ };
1851
+
1762
1852
  react.useEffect(() => {
1763
1853
  load();
1764
1854
  }, [load, lineageKey]);
@@ -2055,17 +2145,31 @@ window.__ModuleLoader__.load({
2055
2145
  jsx("div", {
2056
2146
  style: mapUi.hint,
2057
2147
  children:
2148
+ backfillNote ||
2058
2149
  "共 " +
2059
- chips.length +
2060
- " 条,已钉 " +
2061
- pinIds.length +
2062
- "。默认不勾选:不钉就不会按芯片白名单注入,但仍可能被开场短地图点名带上,模型也能 find 到。",
2150
+ chips.length +
2151
+ " 条,已钉 " +
2152
+ pinIds.length +
2153
+ "。默认不勾选:不钉就不会按芯片白名单注入,但仍可能被开场短地图点名带上,模型也能 find 到。",
2063
2154
  }),
2064
- jsx("button", {
2065
- type: "button",
2066
- style: mapUi.actionBtn,
2067
- onClick: () => setChipsOpen((v) => !v),
2068
- children: chipsOpen ? "收起" : "展开勾选",
2155
+ jsxs("div", {
2156
+ style: mapUi.chipsActions,
2157
+ children: [
2158
+ jsx("button", {
2159
+ type: "button",
2160
+ style: mapUi.actionBtn,
2161
+ disabled: busy,
2162
+ title: "读取当前对话里 DSH 已有的压缩标记和侧边栏分叉,生成检查点方框。给插件装上之前的旧会话用。",
2163
+ onClick: syncCheckpoints,
2164
+ children: busy ? "更新中…" : "更新检查点",
2165
+ }),
2166
+ jsx("button", {
2167
+ type: "button",
2168
+ style: mapUi.actionBtn,
2169
+ onClick: () => setChipsOpen((v) => !v),
2170
+ children: chipsOpen ? "收起" : "展开勾选",
2171
+ }),
2172
+ ],
2069
2173
  }),
2070
2174
  ],
2071
2175
  }),
@@ -2406,6 +2510,15 @@ window.__ModuleLoader__.load({
2406
2510
  inject: () => ({
2407
2511
  fork: (opts) => sessions.fork(opts),
2408
2512
  open: (id) => sessions.open(id),
2513
+ seedLengthOf: (id) => {
2514
+ try {
2515
+ const seed = sessions.binding?.(id)?.session?.header?.seedLength;
2516
+ const n = Number(seed);
2517
+ return Number.isFinite(n) && n > 0 ? n : null;
2518
+ } catch {
2519
+ return null;
2520
+ }
2521
+ },
2409
2522
  renameSession: async (id, title) => {
2410
2523
  const face = sessions.binding?.(id)?.session;
2411
2524
  if (!face || typeof face.rename !== "function") {
package/lib/episode.js CHANGED
@@ -203,6 +203,48 @@ export function syncForkLineage(db, { childSessionId, parentSessionId, atSeq } =
203
203
  return { cutId, childTail };
204
204
  }
205
205
 
206
+ /**
207
+ * Project DSH's already-landed compactations and fork list onto episode boxes.
208
+ * Idempotent: same atSeq / child reuses the existing node.
209
+ */
210
+ export function backfillSessionMap(db, { sessionId, compactations = [], forks = [] } = {}) {
211
+ const sid = String(sessionId || "");
212
+ let checkpoints = 0;
213
+ const seenSeq = new Set();
214
+ for (const raw of compactations || []) {
215
+ const atSeq = Number(raw.atSeq);
216
+ if (!sid || !Number.isFinite(atSeq) || seenSeq.has(atSeq)) continue;
217
+ seenSeq.add(atSeq);
218
+ const id = recordCheckpoint(db, {
219
+ sessionId: sid,
220
+ atSeq,
221
+ compactionId: raw.compactionId,
222
+ summary: raw.summary,
223
+ });
224
+ if (id != null) checkpoints += 1;
225
+ }
226
+ if (sid) ensureTail(db, sid);
227
+ let forkLinks = 0;
228
+ for (const raw of forks || []) {
229
+ const child = String(raw.childSessionId || "");
230
+ const parent = String(raw.parentSessionId || "");
231
+ if (!child || !parent) continue;
232
+ const atSeq = Number.isFinite(Number(raw.atSeq)) ? Number(raw.atSeq) : undefined;
233
+ const result = syncForkLineage(db, {
234
+ childSessionId: child,
235
+ parentSessionId: parent,
236
+ atSeq,
237
+ });
238
+ if (result?.childTail != null) forkLinks += 1;
239
+ }
240
+ return {
241
+ ok: true,
242
+ checkpoints,
243
+ forks: forkLinks,
244
+ ...sessionMapSnapshot(db, sid),
245
+ };
246
+ }
247
+
206
248
  export function sessionMapSnapshot(db, sessionId) {
207
249
  const sid = String(sessionId || "");
208
250
  const all = listEpisodeRecords(db);
package/lib/schema.js CHANGED
@@ -25,7 +25,7 @@ export const EDGE_LABELS = Object.freeze({
25
25
 
26
26
  export const BUSINESS_EDGES = Object.freeze(["about", "decided", "broke", "fixed"]);
27
27
 
28
- /** Weights written on link(). Engine expand currently ignores labels; we still persist them. */
28
+ /** Weights written on link(). Engine neighbors / expandLabels can whitelist labels; we still persist weights. */
29
29
  export const EDGE_WEIGHTS = Object.freeze({
30
30
  about: 1,
31
31
  decided: 1,
package/lib/server.js CHANGED
@@ -17,7 +17,7 @@ import {
17
17
  import { embeddingPublicStatus } from "./embed.js";
18
18
  import { recallModeOf, writeUiSettings } from "./settings.js";
19
19
  import { discoverWorkbuddyFiles, exportMarkdown, importWorkbuddy } from "./markdown.js";
20
- import { renameEpisode, sessionMapSnapshot } from "./episode.js";
20
+ import { backfillSessionMap, renameEpisode, sessionMapSnapshot } from "./episode.js";
21
21
  import { applyPinsUpdate, addChipsFromText, batchChipAction, listChips, readSessionPins } from "./pins.js";
22
22
 
23
23
  export { readUiSettings, writeUiSettings } from "./settings.js";
@@ -272,6 +272,23 @@ async function handle(req, res, { liveOptions, status }) {
272
272
  return;
273
273
  }
274
274
 
275
+ if (path === "/api/dsh-trivium/map/backfill" && method === "POST") {
276
+ const { cwd, db } = await dbForRequest(url);
277
+ if (!db) {
278
+ sendJson(res, 400, { ok: false, message: "no workspace" });
279
+ return;
280
+ }
281
+ const body = JSON.parse((await readBody(req)) || "{}");
282
+ const sessionId = body.sessionId || url.searchParams.get("sessionId") || "";
283
+ const result = backfillSessionMap(db, {
284
+ sessionId,
285
+ compactations: body.compactations,
286
+ forks: body.forks,
287
+ });
288
+ sendJson(res, 200, { ok: true, cwd, sessionId, ...result });
289
+ return;
290
+ }
291
+
275
292
  if (path === "/api/dsh-trivium/chips" && method === "GET") {
276
293
  const { cwd, db } = await dbForRequest(url);
277
294
  const q = url.searchParams.get("q") || "";
package/lib/store.js CHANGED
@@ -135,6 +135,42 @@ function normalizeTql(rows) {
135
135
  .filter(Boolean);
136
136
  }
137
137
 
138
+ function edgeTarget(e) {
139
+ return Number(e?.targetId ?? e?.target_id);
140
+ }
141
+
142
+ function edgeSource(e) {
143
+ return Number(e?.sourceId ?? e?.source_id ?? e?.fromId);
144
+ }
145
+
146
+ /** Native incoming edges when triviumdb >= 0.7.5; otherwise null so callers can scan. */
147
+ function nativeIncoming(db, id, label) {
148
+ if (typeof db.getIncomingEdges !== "function") return null;
149
+ try {
150
+ const rows = label
151
+ ? db.getIncomingEdges(Number(id), label)
152
+ : db.getIncomingEdges(Number(id));
153
+ return Array.isArray(rows) ? rows : [];
154
+ } catch {
155
+ return null;
156
+ }
157
+ }
158
+
159
+ function nativeLabeledNeighbors(db, id, labels) {
160
+ if (typeof db.neighbors !== "function") return null;
161
+ try {
162
+ const rows = db.neighbors(Number(id), 1, [...labels]);
163
+ return Array.isArray(rows) ? rows.map(Number).filter(Number.isFinite) : [];
164
+ } catch {
165
+ try {
166
+ const rows = db.neighbors(Number(id), 1);
167
+ return Array.isArray(rows) ? rows.map(Number).filter(Number.isFinite) : null;
168
+ } catch {
169
+ return null;
170
+ }
171
+ }
172
+ }
173
+
138
174
  function indexNode(db, id, payload) {
139
175
  const text = indexTextFor(payload);
140
176
  try {
@@ -364,12 +400,18 @@ export function mergeNodes(db, keepId, dropId) {
364
400
  if (pa.status === "archived" || pb.status === "archived") {
365
401
  return { ok: false, message: "archived" };
366
402
  }
367
- for (const other of db.allNodeIds?.() || []) {
368
- if (Number(other) === drop) continue;
369
- const n = db.get(other);
370
- for (const e of n?.edges || []) {
371
- if (Number(e.targetId ?? e.target_id) === drop) {
372
- ensureLink(db, other, keep, e.label);
403
+ const inbound = nativeIncoming(db, drop);
404
+ if (inbound) {
405
+ for (const e of inbound) {
406
+ const from = edgeSource(e);
407
+ if (Number.isFinite(from) && from !== keep) ensureLink(db, from, keep, e.label);
408
+ }
409
+ } else {
410
+ for (const other of db.allNodeIds?.() || []) {
411
+ if (Number(other) === drop) continue;
412
+ const n = db.get(other);
413
+ for (const e of n?.edges || []) {
414
+ if (edgeTarget(e) === drop) ensureLink(db, other, keep, e.label);
373
415
  }
374
416
  }
375
417
  }
@@ -402,7 +444,7 @@ export function exportGraph(db, cwd = "") {
402
444
  for (const e of node.edges || []) {
403
445
  edges.push({
404
446
  from: Number(id),
405
- to: Number(e.targetId ?? e.target_id),
447
+ to: edgeTarget(e),
406
448
  label: e.label || "",
407
449
  weight: e.weight ?? null,
408
450
  });
@@ -474,11 +516,11 @@ export function importGraph(db, graph) {
474
516
  export function hasEdge(db, from, to, label) {
475
517
  const node = db.get(Number(from));
476
518
  return (node?.edges || []).some(
477
- (e) => Number(e.targetId ?? e.target_id) === Number(to) && (!label || e.label === label),
519
+ (e) => edgeTarget(e) === Number(to) && (!label || e.label === label),
478
520
  );
479
521
  }
480
522
 
481
- /** Link once for a (from, to, label) triple. Engine unlink cannot drop a single label. */
523
+ /** Link once for a (from, to, label) triple. */
482
524
  export function ensureLink(db, from, to, label) {
483
525
  const src = Number(from);
484
526
  const dst = Number(to);
@@ -489,6 +531,22 @@ export function ensureLink(db, from, to, label) {
489
531
  return true;
490
532
  }
491
533
 
534
+ /** Drop one labeled edge when the engine supports unlink(src, dst, label). */
535
+ export function dropLink(db, from, to, label) {
536
+ const src = Number(from);
537
+ const dst = Number(to);
538
+ if (!Number.isFinite(src) || !Number.isFinite(dst)) return false;
539
+ if (!hasEdge(db, src, dst, label)) return false;
540
+ try {
541
+ if (label) db.unlink(src, dst, label);
542
+ else db.unlink(src, dst);
543
+ db.flush();
544
+ return true;
545
+ } catch {
546
+ return false;
547
+ }
548
+ }
549
+
492
550
  function pickMergedText(oldText, incomingText) {
493
551
  const a = String(oldText || "").trim();
494
552
  const b = String(incomingText || "").trim();
@@ -756,10 +814,9 @@ function entityAnchorIds(db, query) {
756
814
  const q = String(query || "").trim().toLowerCase();
757
815
  if (!q) return [];
758
816
  const ids = [];
759
- for (const id of db.allNodeIds?.() || []) {
760
- const node = db.get(id);
761
- if (node?.payload?.status === "archived" || node?.payload?.type === "episode") continue;
762
- if (entityAnchorScore(node?.payload, q) > 0) ids.push(Number(id));
817
+ for (const n of findByType(db, "entity")) {
818
+ if (n.payload?.status === "archived") continue;
819
+ if (entityAnchorScore(n.payload, q) > 0) ids.push(Number(n.id));
763
820
  }
764
821
  return ids;
765
822
  }
@@ -788,16 +845,27 @@ function primaryScore(payload, query, now) {
788
845
  function scanBusinessNeighbors(db, id) {
789
846
  const ids = new Set();
790
847
  const want = new Set(BUSINESS_EDGES);
791
- const node = db.get(Number(id));
792
- for (const e of node?.edges || []) {
793
- if (want.has(e.label)) ids.add(Number(e.targetId ?? e.target_id));
848
+ const nid = Number(id);
849
+ const labeled = nativeLabeledNeighbors(db, nid, BUSINESS_EDGES);
850
+ if (labeled) {
851
+ for (const other of labeled) ids.add(other);
852
+ } else {
853
+ const node = db.get(nid);
854
+ for (const e of node?.edges || []) {
855
+ if (want.has(e.label)) ids.add(edgeTarget(e));
856
+ }
794
857
  }
795
- for (const other of db.allNodeIds?.() || []) {
796
- if (Number(other) === Number(id)) continue;
797
- const n = db.get(other);
798
- for (const e of n?.edges || []) {
799
- if (want.has(e.label) && Number(e.targetId ?? e.target_id) === Number(id)) {
800
- ids.add(Number(other));
858
+ const inbound = nativeIncoming(db, nid);
859
+ if (inbound) {
860
+ for (const e of inbound) {
861
+ if (want.has(e.label)) ids.add(edgeSource(e));
862
+ }
863
+ } else {
864
+ for (const other of db.allNodeIds?.() || []) {
865
+ if (Number(other) === nid) continue;
866
+ const n = db.get(other);
867
+ for (const e of n?.edges || []) {
868
+ if (want.has(e.label) && edgeTarget(e) === nid) ids.add(Number(other));
801
869
  }
802
870
  }
803
871
  }
@@ -806,9 +874,11 @@ function scanBusinessNeighbors(db, id) {
806
874
 
807
875
  export function businessNeighborIds(db, id) {
808
876
  const ids = scanBusinessNeighbors(db, id);
809
- const viaTql = tqlBusinessNeighbors(db, id);
810
- if (viaTql) {
811
- for (const nid of viaTql) ids.add(Number(nid));
877
+ if (typeof db.getIncomingEdges !== "function") {
878
+ const viaTql = tqlBusinessNeighbors(db, id);
879
+ if (viaTql) {
880
+ for (const nid of viaTql) ids.add(Number(nid));
881
+ }
812
882
  }
813
883
  return [...ids].filter((n) => Number.isFinite(n));
814
884
  }
@@ -885,7 +955,7 @@ function nodeLabel(db, id) {
885
955
  function sameAsTarget(db, id) {
886
956
  const node = db.get(Number(id));
887
957
  for (const e of node?.edges || []) {
888
- if (e.label === EDGE_LABELS.sameAs) return Number(e.targetId ?? e.target_id);
958
+ if (e.label === EDGE_LABELS.sameAs) return edgeTarget(e);
889
959
  }
890
960
  return null;
891
961
  }
@@ -913,6 +983,11 @@ function incomingBusinessEdges(db, id, { query = "", now, hideStale = false } =
913
983
  seen.add(key);
914
984
  found.push({ fromId: src, label, payload: srcNode.payload });
915
985
  };
986
+ const inbound = nativeIncoming(db, nid);
987
+ if (inbound) {
988
+ for (const e of inbound) add(edgeSource(e), e.label);
989
+ return found;
990
+ }
916
991
  if (typeof db.tql === "function") {
917
992
  try {
918
993
  for (const label of BUSINESS_EDGES) {
@@ -928,9 +1003,7 @@ function incomingBusinessEdges(db, id, { query = "", now, hideStale = false } =
928
1003
  const n = db.get(other);
929
1004
  if (!n || n.payload?.status === "archived") continue;
930
1005
  for (const e of n.edges || []) {
931
- if (want.has(e.label) && Number(e.targetId ?? e.target_id) === nid) {
932
- add(other, e.label);
933
- }
1006
+ if (want.has(e.label) && edgeTarget(e) === nid) add(other, e.label);
934
1007
  }
935
1008
  }
936
1009
  return found;
@@ -976,7 +1049,7 @@ export function listOutgoingBusiness(db, id) {
976
1049
  const seen = new Set();
977
1050
  for (const e of node?.edges || []) {
978
1051
  if (!want.has(e.label)) continue;
979
- const tid = Number(e.targetId ?? e.target_id);
1052
+ const tid = edgeTarget(e);
980
1053
  if (!Number.isFinite(tid)) continue;
981
1054
  const key = `${e.label}:${tid}`;
982
1055
  if (seen.has(key)) continue;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-trivium",
3
- "version": "0.4.2",
3
+ "version": "0.4.4",
4
4
  "description": "In-process graph memory and session map for DeepSeek Harness, backed by TriviumDB.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -51,7 +51,7 @@
51
51
  "@deepseek-ai/dsh-tools": "0.1.0-rc.8"
52
52
  },
53
53
  "dependencies": {
54
- "triviumdb": "^0.7.4"
54
+ "triviumdb": "^0.7.5"
55
55
  },
56
56
  "keywords": [
57
57
  "dsh-plugin",