dsh-trivium 0.4.20 → 0.4.22

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
@@ -55,6 +55,8 @@ Then restart `dsh web`.
55
55
 
56
56
  ## Updating
57
57
 
58
+ Before upgrading, stop DSH and back up the entire workspace `.dsh/trivium.tdb*` file group. Default mmap storage also uses `.vec`, `.wal`, `.text`, and other sidecars. Git JSONL omits local-only data and is not a complete backup.
59
+
58
60
  ```sh
59
61
  dsh plugin --profile web add dsh-trivium
60
62
  ```
@@ -138,7 +140,11 @@ Trivium is a memory kernel, not a journal, calendar, or chat companion. It store
138
140
 
139
141
  Nodes are `entity` / `preference` / `decision` / `experience`. Business edges are `about`, `decided`, `broke`, `fixed`.
140
142
 
141
- `ctx_find` returns L0 summaries plus edge paths (incoming as `<-label-id`). If the query names an existing entity, unexpired `about` / `decided` / `broke` / `fixed` neighbors come along even when the neighbor text does not contain the query. Decisions with `until` stay hidden after they expire, unless the query itself is asking about that deadline (e.g. “Friday”).
143
+ `ctx_find` returns L0 summaries plus edge paths (incoming as `<-label-id`). Entity-name queries also return `about` / `decided` / `broke` / `fixed` neighbors even when their text omits the query. Relevant expired decisions remain searchable as history, labeled `stale` and ranked lower. Set `include_stale: false` to exclude them. Automatic recall and the opening map still exclude expired decisions.
144
+
145
+ Dates in prose stay in prose. Set expiry in Settings or the separate `ctx_remember.until` field. Supported labels are `YYYY-MM-DD`, weekdays such as `Friday` / `周五`, `周末`, and `下周` (one week), resolved at the end of the UTC day. An unresolvable replacement label cannot retain the old `untilAt`. Clearing the label clears the timestamp; changing a relative deadline anchors it to the edit, while saving an unchanged label does not extend it.
146
+
147
+ 0.4.22 does not bulk-delete existing expiry fields. Find affected entries with `ctx_find` or Settings → **Show expired decisions**, clear `until`, and save. Later edits to dated prose will not recreate the expiry.
142
148
 
143
149
  ### Chips (memory whitelist)
144
150
 
@@ -183,9 +189,9 @@ Injection detail: autoRecall injects at most 3 L0 hits when the step has user te
183
189
 
184
190
  | Tool | What it does |
185
191
  |---|---|
186
- | `ctx_find` | Search; L0 summaries and graph paths (incoming `<-label-id`). Entity-name queries also return unexpired about/decided/broke/fixed neighbors. Expired `until` decisions stay out unless the query asks about the deadline |
192
+ | `ctx_find` | Text / optional vector / graph search; L0 summaries and paths. Expired history is labeled `stale` and ranked lower; `include_stale: false` excludes it |
187
193
  | `ctx_read` | Full text and incoming edges by id |
188
- | `ctx_remember` | Manual write |
194
+ | `ctx_remember` | Manual write; optional explicit `until` field for real expiry, never inferred from dates in prose |
189
195
  | `ctx_link` | Directed edge between two nodes |
190
196
 
191
197
  The kernel is still these four. Chips / session layer do not add a fifth tool.
@@ -245,6 +251,12 @@ Off by default. After you turn **Chips (memory whitelist)** on in Settings, the
245
251
 
246
252
  ## Changelog
247
253
 
254
+ **0.4.22** — Fix [issue #3](https://github.com/QWQcool/dsh-trivium/issues/3): expiry is explicit, clearing it persists, and relevant expired history is searchable with a `stale` label. Preserve native text/vector candidates; add multiword fallback, aliases, and two-character Chinese entity anchors. Upgrade TriviumDB to `0.8.11`. Add focused expiry/recall regressions and keep automatic recall restricted to unexpired memories.
255
+
256
+ The peer ranges now explicitly admit the supported DSH `0.1.5`, `0.2.0`, and `0.2.1` RC lines; a broad stable-version range alone does not include those prereleases under npm rules.
257
+
258
+ **0.4.21** — Host adaptation for `@deepseek-ai/dsh@0.2.x`. Session format v4 replaced the shared `kind: "plugin"` message source with producer-owned kinds: a third-party producer must write `kind: "plugin:dsh-trivium"` and drop the `plugin` field, or the whole turn fails with "format v4 message requires a producer-owned source kind". The plugin now picks the shape from the installed `dsh-llm` version, so 0.1.x keeps working. `agent/created` (0.1.6+) is handled next to `agent/session-start`, with a one-shot guard in case a host emits both. Peer range widened to `<0.3.0`. New `smoke-p14` pins both source shapes and the version switch. Four tools / short map / find paths unchanged.
259
+
248
260
  **0.4.20** — Storage bump to `triviumdb@0.8.9`. The napi surface is unchanged (`triviumdb.d.ts` and `index.js` hash identically to 0.8.8), so this is a kernel-only update: SM-L2 is now the default QuIVer query path, FIND planning no longer materializes discarded candidates, and the server edition ships an attributed Web Console. All 14 offline smoke scripts pass unchanged. Four tools / short map / find paths unchanged.
249
261
 
250
262
  **0.4.19** — Fixed the plugin's own UI rendering Simplified Chinese on an English host ([issue #2](https://github.com/QWQcool/dsh-trivium/issues/2)). The `locale` service is now reached through `ctx.inject(["locale"], …)`, so **follow host** no longer depends on composition order, and the plugin fallback is `en` — the host's own `FALLBACK_LOCALE`, and the bucket every other resolved host language lands in. The Settings card and the Chips tab only ever choose between zh and en, so a browser naming no registered language no longer gets a Chinese card. Four tools / short map / find paths unchanged.
@@ -310,7 +322,7 @@ Local source checkout: `npm install`, then `node scripts/link-dsh.mjs`, then res
310
322
  ## Release info
311
323
 
312
324
  - GitHub: https://github.com/QWQcool/dsh-trivium
313
- - npm: [`dsh-trivium@0.4.20`](https://www.npmjs.com/package/dsh-trivium)
325
+ - Package version: `0.4.22` · [npm](https://www.npmjs.com/package/dsh-trivium)
314
326
  - Tested host: `@deepseek-ai/dsh@0.1.5-rc.1` (also `0.1.1-rc.2` / `0.1.0-rc.8`)
315
327
  - License: MIT (depends on [TriviumDB](https://github.com/YoKONCy/TriviumDB), Apache-2.0)
316
328
 
package/README.zh-CN.md CHANGED
@@ -53,6 +53,8 @@ npm test # 可选:跑全部离线 smoke
53
53
 
54
54
  ## 更新
55
55
 
56
+ 升级前先停止 DSH,备份工作区 `.dsh/trivium.tdb*` 整个文件组。默认 mmap 模式还会使用 `.vec`、`.wal`、`.text` 等附属文件;Git JSONL 不含全部本机数据,不能代替完整备份。
57
+
56
58
  ```sh
57
59
  dsh plugin --profile web add dsh-trivium
58
60
  ```
@@ -134,7 +136,11 @@ Trivium 是记忆内核,不是日记、日历或聊天伴侣。它按**节点
134
136
 
135
137
  节点类型是 `entity` / `preference` / `decision` / `experience`。业务边是 `about`、`decided`、`broke`、`fixed`。
136
138
 
137
- `ctx_find` 返回 L0 摘要和图路径(入边写成 `<-label-id`)。查询点到已有实体名时,还会带上未过期的 about/decided/broke/fixed 邻居,即使邻居正文不含查询词。带 `until` 的过期决策默认不出现,除非查询本身在问期限(如「周五」)。
139
+ `ctx_find` 返回 L0 摘要和图路径(入边写成 `<-label-id`)。查询点到已有实体名时,还会带上 about/decided/broke/fixed 邻居,即使邻居正文不含查询词。相关的过期决策仍可作为历史返回,标记 `stale` 并降权;设置 `include_stale: false` 可排除过期条目。自动召回和开场短地图仍排除过期决策。
140
+
141
+ 正文中的日期只属于正文。有效期通过设置页或 `ctx_remember` 的独立 `until` 字段指定,支持 `YYYY-MM-DD`、`Friday` / `周五`、`周末`、`下周`,按 UTC 日末解释。无法解析的新标签不会沿用旧 `untilAt`。清空 `until` 会同时移除时间戳,修改为新的相对期限则从本次编辑时计算;仅保存同一期限不会延长它。
142
+
143
+ 0.4.22 不批量删除旧数据的期限。被误标的记忆可重新通过 `ctx_find` 查到,也可在设置里勾选「显示过期决策」找到,清空 `until` 并保存;之后编辑含日期的正文不会再自动生成期限。
138
144
 
139
145
  ### 芯片(记忆白名单)
140
146
 
@@ -179,9 +185,9 @@ Trivium 是记忆内核,不是日记、日历或聊天伴侣。它按**节点
179
185
 
180
186
  | 工具 | 说明 |
181
187
  |---|---|
182
- | `ctx_find` | 检索,返回 L0 摘要和图路径(含入边 `<-label-id`)。查询点到已有实体名时,还会带上未过期的 about/decided/broke/fixed 邻居。带 `until` 的过期决策默认不出现,除非查询本身在问期限(如「周五」) |
188
+ | `ctx_find` | 文本 / 可选向量 / 图关系检索;返回 L0 和路径。过期历史标记 `stale` 并降权;`include_stale: false` 排除过期条目 |
183
189
  | `ctx_read` | 按 id 读全文和入边 |
184
- | `ctx_remember` | 手动写入 |
190
+ | `ctx_remember` | 手动写入;可用独立 `until` 字段指定真实有效期,不从正文日期推断 |
185
191
  | `ctx_link` | 两点之间建有向边 |
186
192
 
187
193
  内核仍是这四个。芯片 / 会话层不加第五个工具。
@@ -241,6 +247,12 @@ Trivium 是记忆内核,不是日记、日历或聊天伴侣。它按**节点
241
247
 
242
248
  ## 更新说明
243
249
 
250
+ **0.4.22** — 修复 [issue #3](https://github.com/QWQcool/dsh-trivium/issues/3):期限只由显式字段设置,清空后不再重新生成,相关过期记忆可带 `stale` 标记检索。保留数据库原生文本 / 向量候选,增加中英文多词兜底、别名与两字中文实体锚点。TriviumDB 升至 `0.8.11`。新增期限与召回专项回归,自动召回仍排除过期记忆。
251
+
252
+ peer 范围显式接受已支持的 DSH `0.1.5`、`0.2.0`、`0.2.1` RC 版本线,修复 npm 普通稳定版范围不会自动接受这些预发布版本的问题。
253
+
254
+ **0.4.21** — 适配 `@deepseek-ai/dsh@0.2.x`。会话格式 v4 把共享的 `kind: "plugin"` 消息来源改成生产者自有的 kind:第三方生产者必须写 `kind: "plugin:dsh-trivium"` 且去掉 `plugin` 字段,否则整轮会以 "format v4 message requires a producer-owned source kind" 失败。插件按已安装的 `dsh-llm` 版本自动选择形状,0.1.x 仍走旧格式。新增对 `agent/created`(0.1.6+)的监听,与 `agent/session-start` 并存并做一次性去重。peer 范围放宽到 `<0.3.0`。新增 `smoke-p14` 钉住两种 source 形状与版本判定。四工具 / 短地图 / find 路径不变。
255
+
244
256
  **0.4.20** — 存储升到 `triviumdb@0.8.9`。napi 接口零变更(`triviumdb.d.ts` 与 `index.js` 与 0.8.8 哈希完全一致),属纯内核升级:QuIVer 默认走 SM-L2 查询路径,FIND 规划不再物化被丢弃的候选,服务端版本带上带署名的 Web Console。14 个离线冒烟脚本原样通过。四工具 / 短地图 / find 路径不变。
245
257
 
246
258
  **0.4.19** — 修复插件自身界面在英文宿主上渲染成简体中文([issue #2](https://github.com/QWQcool/dsh-trivium/issues/2))。`locale` 服务改经 `ctx.inject(["locale"], …)` 获取,「跟随宿主」不再依赖插件组合顺序;兜底语言改为 `en`——与宿主自己的 `FALLBACK_LOCALE` 一致,也是其它已解析宿主语言的归宿。设置卡与芯片标签只在 zh / en 之间选择,浏览器未命名任何注册语言时不再给出中文卡。四工具 / 短地图 / find 路径不变。
@@ -306,7 +318,7 @@ Trivium 是记忆内核,不是日记、日历或聊天伴侣。它按**节点
306
318
  ## 发布信息
307
319
 
308
320
  - GitHub: https://github.com/QWQcool/dsh-trivium
309
- - npm: [`dsh-trivium@0.4.20`](https://www.npmjs.com/package/dsh-trivium)
321
+ - 包版本:`0.4.22` · [npm](https://www.npmjs.com/package/dsh-trivium)
310
322
  - 测试宿主:`@deepseek-ai/dsh@0.1.5-rc.1`(兼 `0.1.1-rc.2` / `0.1.0-rc.8`)
311
323
  - License: MIT(依赖 [TriviumDB](https://github.com/YoKONCy/TriviumDB) 为 Apache-2.0)
312
324
 
package/lib/extract.js CHANGED
@@ -9,7 +9,7 @@ import {
9
9
  mergeInto,
10
10
  ensureLink,
11
11
  } from "./store.js";
12
- import { parseUntilFromText, resolveUntilAt } from "./until.js";
12
+ import { resolveUntilAt } from "./until.js";
13
13
 
14
14
  export const PREFERENCE_CUES =
15
15
  /记住|以后都|别再|不要再|从现在起|always do|from now on|never again|remember (?:this|that|to)\b/i;
@@ -114,10 +114,6 @@ export function textFromContent(content) {
114
114
  return parts.join("\n");
115
115
  }
116
116
 
117
- function untilOf(text) {
118
- return parseUntilFromText(text);
119
- }
120
-
121
117
  function isToolDriveTurn(text) {
122
118
  return TOOL_DRIVE_CUES.test(String(text || ""));
123
119
  }
@@ -256,7 +252,6 @@ export function ruleCandidates(turns) {
256
252
  type: "decision",
257
253
  name: object,
258
254
  text: clip(span, 240),
259
- until: untilOf(span),
260
255
  linkName: object,
261
256
  linkLabel: EDGE_LABELS.decided,
262
257
  quote: clip(span, 120),
@@ -377,9 +372,10 @@ export function buildExtractPrompt(turns) {
377
372
  const transcript = clip(turnsText(turns), 2400);
378
373
  return [
379
374
  "Extract durable memory candidates as JSON only.",
380
- 'Shape: {"candidates":[{"type":"preference|entity|decision|experience","name":"","text":"","linkName":"","linkLabel":"about|decided|broke|fixed","fail":"","fix":"","until":"","quote":""}]}',
375
+ 'Shape: {"candidates":[{"type":"preference|entity|decision|experience","name":"","text":"","linkName":"","linkLabel":"about|decided|broke|fixed","fail":"","fix":"","quote":""}]}',
381
376
  "Whitelist: 记住/以后都/别再 → preference; repeated proper names → entity; decision with an object (先别动/下周再/采用方案) → decision; tool fail then success in the same turn → experience.",
382
377
  "Do not write chitchat or one-off file edits as preference. Do not store secrets, tool dumps, or whole conversations.",
378
+ "Keep dates in the fact text. Do not infer expiry or emit until/untilAt; expiry is set explicitly through the memory tools or settings.",
383
379
  "If nothing matches, return {\"candidates\":[]}.",
384
380
  "",
385
381
  transcript,
@@ -481,7 +477,11 @@ export async function distill({ db, turns, sessionId, llmCall, log }) {
481
477
  if (typeof llmCall === "function") {
482
478
  try {
483
479
  const raw = await llmCall(buildExtractPrompt(usable));
484
- model = parseModelJson(raw);
480
+ // Automatic extraction cannot silently turn an event date into expiry,
481
+ // even when the model emits fields outside the requested schema.
482
+ model = parseModelJson(raw).map((candidate) => ({
483
+ ...candidate, until: undefined, untilAt: undefined,
484
+ }));
485
485
  } catch (err) {
486
486
  llmFailed = true;
487
487
  log?.warn?.(`[dsh-trivium] extract prompt failed: ${err.message}`);
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Host wire-format helpers, kept free of `@deepseek-ai/*` imports so the offline
3
+ * smoke suite can exercise them without a DSH install.
4
+ *
5
+ * Session format v4 (DSH 0.2.0+) replaced the shared `kind: "plugin"` message
6
+ * source with producer-owned kinds: a third-party producer is `plugin:<name>`
7
+ * and the `plugin` field is dropped. Writing the old shape into a v4 log fails
8
+ * the whole turn with "format v4 message requires a producer-owned source kind".
9
+ * 0.1.x only knows the old shape, so lib/index.js picks by host version.
10
+ */
11
+
12
+ /**
13
+ * Decide whether the host writes session format v4.
14
+ * @param version - `@deepseek-ai/dsh-llm` version, e.g. "0.1.5-rc.1" / "0.2.1-rc.2".
15
+ * @returns true for >= 0.2.0, and for an unreadable version (assume the current
16
+ * format rather than silently writing a shape the host rejects).
17
+ */
18
+ export function detectHostFormatV4(version) {
19
+ const v = String(version || "").trim();
20
+ if (!v) return true;
21
+ const [major, minor] = v.split(".").map((n) => parseInt(n, 10) || 0);
22
+ return major > 0 || minor >= 2;
23
+ }
24
+
25
+ /**
26
+ * Build the `source` for one plugin-authored message.
27
+ * @param hostFormatV4 - result of {@link detectHostFormatV4}.
28
+ * @param extra - additional source fields (form, sections, ...).
29
+ * @returns the source object the installed host accepts.
30
+ */
31
+ export function producerSourceFor(hostFormatV4, extra) {
32
+ return hostFormatV4
33
+ ? { kind: "plugin:dsh-trivium", ...extra }
34
+ : { kind: "plugin", plugin: "dsh-trivium", ...extra };
35
+ }
package/lib/index.js CHANGED
@@ -1,4 +1,6 @@
1
+ import { createRequire } from "node:module";
1
2
  import { BlockAssembler, createUserMessage } from "@deepseek-ai/dsh-llm";
3
+ import { detectHostFormatV4, producerSourceFor } from "./host-format.js";
2
4
  import { cwdOf, registerTools } from "./tools.js";
3
5
  import {
4
6
  buildShortMapReport,
@@ -29,6 +31,27 @@ import { syncSidecarOnOpen } from "./sidecar.js";
29
31
  export const name = "dsh-trivium";
30
32
  export const inject = ["tools"];
31
33
 
34
+ /*
35
+ * Session format v4 (DSH 0.2.0+) replaced the shared `kind: "plugin"` source
36
+ * with producer-owned kinds: a third-party producer is `plugin:<name>` and the
37
+ * `plugin` field is dropped. Writing the old shape into a v4 log fails the
38
+ * whole turn with "format v4 message requires a producer-owned source kind".
39
+ * 0.1.x only knows the old shape, so pick by host version.
40
+ */
41
+ function readHostLlmVersion() {
42
+ try {
43
+ return String(createRequire(import.meta.url)("@deepseek-ai/dsh-llm/package.json").version || "");
44
+ } catch {
45
+ return "";
46
+ }
47
+ }
48
+
49
+ export const HOST_FORMAT_V4 = detectHostFormatV4(readHostLlmVersion());
50
+
51
+ export function producerSource(extra) {
52
+ return producerSourceFor(HOST_FORMAT_V4, extra);
53
+ }
54
+
32
55
  const DEFAULTS = {
33
56
  autoRecall: false,
34
57
  anchorRecall: false,
@@ -127,7 +150,7 @@ function makeLlmCall(ctx, session, agent) {
127
150
  messages: [
128
151
  createUserMessage({
129
152
  content: [{ type: "text", text: prompt }],
130
- source: { kind: "plugin", plugin: "dsh-trivium" },
153
+ source: producerSource(),
131
154
  }),
132
155
  ],
133
156
  };
@@ -253,12 +276,10 @@ function injectShortMap(agent, db, budget) {
253
276
  agent.inject(
254
277
  createUserMessage({
255
278
  content: [{ type: "text", text: map.text }],
256
- source: {
257
- kind: "plugin",
258
- plugin: "dsh-trivium",
279
+ source: producerSource({
259
280
  form: "snapshot",
260
281
  sections: [{ name: "dsh-trivium", text: map.text }],
261
- },
282
+ }),
262
283
  }),
263
284
  );
264
285
  return map;
@@ -294,7 +315,19 @@ export function apply(ctx, config = {}) {
294
315
 
295
316
  registerTools(ctx, optionsOf);
296
317
 
297
- ctx.on("agent/session-start", async ({ agent }) => {
318
+ /*
319
+ * DSH 0.1.6 replaced the serial `agent/session-start` event with
320
+ * `agent/created` (payload `{ agent, source, signal? }`). 0.1.5 and older
321
+ * only emit session-start, and a host never emits both, but the started set
322
+ * keeps the four one-shot steps below from running twice if that changes.
323
+ */
324
+ const started = new Set();
325
+ const onAgentStart = async ({ agent }) => {
326
+ const startId = sessionKey(agent?.session);
327
+ if (startId) {
328
+ if (started.has(startId)) return;
329
+ started.add(startId);
330
+ }
298
331
  try {
299
332
  const ready = await ensureShortMap(agent, optionsOf());
300
333
  const cwd = ready?.cwd || cwdOf(agent);
@@ -340,7 +373,10 @@ export function apply(ctx, config = {}) {
340
373
  } catch (err) {
341
374
  log.warn?.(`[dsh-trivium] session-start skipped: ${err.message}`);
342
375
  }
343
- });
376
+ };
377
+
378
+ ctx.on("agent/created", onAgentStart);
379
+ ctx.on("agent/session-start", onAgentStart);
344
380
 
345
381
  ctx.on("session/event", (session, event) => {
346
382
  try {
@@ -457,10 +493,7 @@ export function apply(ctx, config = {}) {
457
493
  // No `form`: "pins" was never in DSH's ContextForm union, so the
458
494
  // client already rendered it as undeclared (opaque) context.
459
495
  // Omitting the field is the documented default for that.
460
- source: {
461
- kind: "plugin",
462
- plugin: "dsh-trivium",
463
- },
496
+ source: producerSource(),
464
497
  }),
465
498
  );
466
499
  }
@@ -482,6 +515,7 @@ export function apply(ctx, config = {}) {
482
515
  hits = searchNodes(db, query, {
483
516
  topK: 3,
484
517
  expandDepth: 1,
518
+ includeStale: false,
485
519
  queryVector: queryVector || undefined,
486
520
  }).slice(0, 3);
487
521
  }
@@ -498,11 +532,7 @@ export function apply(ctx, config = {}) {
498
532
  extra.push(
499
533
  createUserMessage({
500
534
  content: [{ type: "text", text }],
501
- source: {
502
- kind: "plugin",
503
- plugin: "dsh-trivium",
504
- form: "recall",
505
- },
535
+ source: producerSource({ form: "recall" }),
506
536
  }),
507
537
  );
508
538
  }
package/lib/markdown.js CHANGED
@@ -19,7 +19,6 @@ import {
19
19
  applyCandidates,
20
20
  firstEntityName,
21
21
  } from "./extract.js";
22
- import { parseUntilFromText } from "./until.js";
23
22
  import { listOutgoingBusiness } from "./store.js";
24
23
 
25
24
  export const EXPORT_MARKER = "dsh-trivium-export";
@@ -192,7 +191,6 @@ function classifyBullet(text, sectionEntity) {
192
191
  type: "decision",
193
192
  name,
194
193
  text: fact,
195
- until: parseUntilFromText(body),
196
194
  linkName: name,
197
195
  linkLabel: "decided",
198
196
  quote: clip(body, 120),
package/lib/pins.js CHANGED
@@ -7,7 +7,7 @@ import { archiveNode, deleteNode, suggestedNeighborIds } from "./store.js";
7
7
  import { applyCandidates, DECISION_CUES, EXPLICIT_LINE_CHARS, firstEntityName } from "./extract.js";
8
8
  import { clip } from "./schema.js";
9
9
  import { payloadLooksDirty, sanitizeForWrite, writeGateMessage } from "./hygiene.js";
10
- import { parseUntilFromText } from "./until.js";
10
+ import { isStalePayload } from "./until.js";
11
11
 
12
12
  export const PIN_TOKEN_BUDGET = 300;
13
13
  export const CHIP_TYPES = Object.freeze(["preference", "decision", "entity"]);
@@ -117,7 +117,8 @@ export function buildPinInject(db, sessionId, budget = PIN_TOKEN_BUDGET) {
117
117
  for (const id of ids) {
118
118
  const payload = db.get(Number(id))?.payload;
119
119
  if (!payload || payloadLooksDirty(payload)) continue;
120
- lines.push(`- [${payload.type}#${id}] ${l0Of(payload)}`);
120
+ const expiry = isStalePayload(payload) ? ` stale until=${payload.until || payload.untilAt}` : "";
121
+ lines.push(`- [${payload.type}#${id}${expiry}] ${l0Of(payload)}`);
121
122
  used.push(Number(id));
122
123
  }
123
124
  const full = lines.join("\n");
@@ -200,7 +201,6 @@ function candidateFromChipLine(body) {
200
201
  type: "decision",
201
202
  name,
202
203
  text: clip(text, EXPLICIT_LINE_CHARS),
203
- until: parseUntilFromText(text),
204
204
  linkName: name,
205
205
  linkLabel: "decided",
206
206
  quote: clip(text, 120),
package/lib/schema.js CHANGED
@@ -64,7 +64,7 @@ export function looksSecret(text) {
64
64
  }
65
65
 
66
66
  export function indexTextFor(payload) {
67
- const parts = [payload.type, payload.name, payload.text, payload.uri, payload.fail, payload.fix, payload.until]
67
+ const parts = [payload.type, payload.name, ...(Array.isArray(payload.aliases) ? payload.aliases : []), payload.text, payload.uri, payload.fail, payload.fix, payload.until]
68
68
  .filter(Boolean)
69
69
  .map(String);
70
70
  return parts.join(" ");
package/lib/sidecar.js CHANGED
@@ -333,7 +333,7 @@ async function applySidecar(db, parsed) {
333
333
  createdAt: row.createdAt || "",
334
334
  updatedAt: row.updatedAt || "",
335
335
  };
336
- if (!payload.until) delete payload.until;
336
+ // Keep the empty label as an explicit clear when reconciling a snapshot.
337
337
  if (!payload.untilAt) delete payload.untilAt;
338
338
  if (!payload.fail) delete payload.fail;
339
339
  if (!payload.fix) delete payload.fix;
package/lib/store.js CHANGED
@@ -12,7 +12,7 @@ import {
12
12
  } from "./sidecar.js";
13
13
  import { DIM, EDGE_LABELS, BUSINESS_EDGES, NODE_TYPES, indexTextFor, nowIso, weightOf, zeroVector } from "./schema.js";
14
14
  import { clipToTokenBudget, estimateTokens } from "./tokens.js";
15
- import { isStalePayload, parseUntilFromText, queryMentionsUntil, resolveUntilAt } from "./until.js";
15
+ import { isStalePayload, queryMentionsUntil, resolveUntilAt } from "./until.js";
16
16
  import { embedText, embeddingEnabled } from "./embed.js";
17
17
  import { payloadLooksDirty, sanitizeForWrite } from "./hygiene.js";
18
18
 
@@ -316,6 +316,7 @@ export function mergeInto(db, id, incoming) {
316
316
  incoming.name,
317
317
  old.name,
318
318
  ]).filter((a) => a !== (incoming.name || old.name));
319
+ const deadlineChanged = incoming.until !== undefined && String(incoming.until || "").trim() !== String(old.until || "").trim();
319
320
  const next = decorateUntil({
320
321
  ...old,
321
322
  ...incoming,
@@ -326,7 +327,11 @@ export function mergeInto(db, id, incoming) {
326
327
  updatedAt: nowIso(),
327
328
  status: old.status || "active",
328
329
  source: incoming.source || old.source,
329
- });
330
+ // An omitted expiry must not erase an existing one or leave an orphaned
331
+ // untilAt. A changed label must not inherit the previous absolute instant.
332
+ until: incoming.until === undefined ? old.until : incoming.until,
333
+ untilAt: incoming.untilAt || (deadlineChanged ? undefined : old.untilAt),
334
+ }, deadlineChanged ? nowIso() : old.createdAt);
330
335
  db.updatePayload(Number(id), next);
331
336
  indexNode(db, Number(id), next);
332
337
  persist(db);
@@ -430,14 +435,12 @@ export function updateNode(db, id, patch = {}) {
430
435
  if (Array.isArray(patch.aliases)) {
431
436
  next.aliases = uniqueStrings(patch.aliases).filter((a) => a !== next.name);
432
437
  }
438
+ let deadlineChanged = false;
433
439
  if (patch.until !== undefined) {
434
440
  const until = String(patch.until || "").trim();
435
- if (until) {
436
- next.until = until;
437
- const untilAt = resolveUntilAt(until, next.createdAt);
438
- if (untilAt) next.untilAt = untilAt;
439
- } else {
440
- delete next.until;
441
+ deadlineChanged = until !== String(old.until || "").trim();
442
+ next.until = until;
443
+ if (deadlineChanged || !until) {
441
444
  delete next.untilAt;
442
445
  }
443
446
  }
@@ -447,7 +450,7 @@ export function updateNode(db, id, patch = {}) {
447
450
  createdAt: old.createdAt,
448
451
  status: old.status || "active",
449
452
  updatedAt: nowIso(),
450
- });
453
+ }, deadlineChanged ? nowIso() : old.createdAt);
451
454
  const gate = sanitizeForWrite([body.name, body.text, body.fail, body.fix].filter(Boolean).join("\n"));
452
455
  if (!gate.ok && gate.reason !== "empty") {
453
456
  const err = new Error(gate.reason);
@@ -506,7 +509,7 @@ export function mergeNodes(db, keepId, dropId) {
506
509
  text: pa.text,
507
510
  aliases: [...(pa.aliases || []), ...(pb.aliases || []), pb.name],
508
511
  until: pa.until || pb.until,
509
- untilAt: pa.untilAt || pb.untilAt,
512
+ untilAt: pa.until ? pa.untilAt : pb.untilAt,
510
513
  source: pa.source || pb.source,
511
514
  });
512
515
  ensureLink(db, drop, keep, EDGE_LABELS.sameAs);
@@ -574,7 +577,8 @@ export function importGraph(db, graph) {
574
577
  let dest = findByUri(db, payload.uri);
575
578
  if (dest == null) dest = findMergeTarget(db, payload);
576
579
  if (dest != null) {
577
- mergeInto(db, dest, payload);
580
+ // An exported snapshot is authoritative, including a cleared deadline.
581
+ mergeInto(db, dest, { ...payload, until: payload.until ?? "" });
578
582
  idMap.set(oldId, dest);
579
583
  merged += 1;
580
584
  } else {
@@ -637,14 +641,18 @@ function pickMergedText(oldText, incomingText) {
637
641
  return b.length >= a.length ? b : a;
638
642
  }
639
643
 
640
- function decorateUntil(payload) {
644
+ function decorateUntil(payload, from = payload.createdAt) {
641
645
  const body = { ...payload };
642
- if (body.type === "decision" && !body.until) {
643
- const until = parseUntilFromText(body.text);
644
- if (until) body.until = until;
646
+ const until = String(body.until || "").trim();
647
+ if (!until) {
648
+ delete body.until;
649
+ delete body.untilAt;
650
+ return body;
645
651
  }
646
- if (body.until && !body.untilAt) {
647
- const untilAt = resolveUntilAt(body.until, body.createdAt);
652
+ body.until = until;
653
+ if (!body.untilAt || !Number.isFinite(Date.parse(body.untilAt))) {
654
+ delete body.untilAt;
655
+ const untilAt = resolveUntilAt(until, from);
648
656
  if (untilAt) body.untilAt = untilAt;
649
657
  }
650
658
  return body;
@@ -718,15 +726,30 @@ export async function backfillEmbeddings(db, { log } = {}) {
718
726
  return { ok: true, embedded, failed };
719
727
  }
720
728
 
721
- export function searchNodes(db, query, { topK = 8, expandDepth = 1, now, queryVector } = {}) {
729
+ export function searchNodes(db, query, { topK = 8, expandDepth = 1, now, queryVector, includeStale = true } = {}) {
722
730
  const text = String(query || "").trim();
723
731
  if (!text) return [];
724
732
  const at = now instanceof Date ? now : new Date();
725
733
  const primary = new Map();
726
- const consider = (id) => {
734
+ const terms = queryTerms(text);
735
+ const hasQueryVector = Array.isArray(queryVector) && queryVector.length === DIM &&
736
+ queryVector.every(Number.isFinite) && queryVector.some((value) => value !== 0);
737
+ const identifierQuery = /^[a-z0-9_]+(?:[-./:][a-z0-9_]+)+$/i.test(text);
738
+ const consider = (id, nativeScore = 0) => {
727
739
  const rec = hitRecord(db, id);
728
740
  if (!rec) return;
729
- const score = primaryScore(rec.payload, text, at);
741
+ // A native BM25/vector hit is already a retrieval candidate. Lexical
742
+ // evidence may boost it, but must never veto it with an exact-phrase gate.
743
+ const lexical = primaryScore(rec.payload, text, at, terms);
744
+ // Explicit keywords are additive in 0.8.x. A renamed node may still have
745
+ // an old native keyword: require some current token evidence for text-only
746
+ // hits, never the whole query phrase. Vector-only hits need no such overlap.
747
+ const currentText = indexTextFor(rec.payload).toLowerCase();
748
+ if (!hasQueryVector && lexical <= 0 && !terms.some((term) => currentText.includes(term))) return;
749
+ // Keep code identifiers atomic: a sparse hit for just "X" must not turn
750
+ // an exact X-Request-Id lookup into a hit on an unrelated header X fact.
751
+ if (identifierQuery && !hasQueryVector && lexical <= 0) return;
752
+ const score = Math.max(lexical, Number.isFinite(nativeScore) ? nativeScore : 0);
730
753
  if (score <= 0) return;
731
754
  rec.score = score;
732
755
  rec.query = text;
@@ -736,13 +759,16 @@ export function searchNodes(db, query, { topK = 8, expandDepth = 1, now, queryVe
736
759
  };
737
760
  try {
738
761
  const vec =
739
- Array.isArray(queryVector) && queryVector.length === DIM ? queryVector : zeroVector();
740
- const hits = db.searchHybrid(vec, text, Math.max(topK, 16), 0, 0.01, 0.2);
741
- for (const hit of hits || []) consider(hit.id);
762
+ hasQueryVector ? queryVector : zeroVector();
763
+ const hits = db.searchHybrid(vec, text, Math.max(topK, 16), 0, 0.01, 0.2, {
764
+ type: { $in: ["entity", "decision", "preference", "experience"] },
765
+ status: { $ne: "archived" },
766
+ });
767
+ for (const hit of hits || []) consider(hit.id, Number(hit.score));
742
768
  } catch {
743
769
  // fall through to keyword scan
744
770
  }
745
- for (const hit of keywordFallback(db, text, 64)) consider(hit.id);
771
+ for (const hit of keywordFallback(db, text, 64, terms)) consider(hit.id);
746
772
  for (const id of entityAnchorIds(db, text)) consider(id);
747
773
  if (queryMentionsUntil(text)) {
748
774
  for (const id of db.allNodeIds?.() || []) consider(id);
@@ -778,13 +804,14 @@ export function searchNodes(db, query, { topK = 8, expandDepth = 1, now, queryVe
778
804
  }
779
805
  const typeRank = { entity: 0, decision: 1, preference: 2, experience: 3 };
780
806
  return [...out.values()]
781
- .filter((hit) => !shouldHideStale(hit, text, at))
807
+ .filter((hit) => includeStale || !isStalePayload(hit.payload, at))
808
+ .map((hit) => ({ ...hit, includeStale }))
782
809
  .sort((a, b) => {
783
810
  const rb = rankBoost(db, b, text, at) - rankBoost(db, a, text, at);
784
811
  if (rb) return rb;
785
- const td = (typeRank[a.payload?.type] ?? 9) - (typeRank[b.payload?.type] ?? 9);
786
- if (td) return td;
787
- return (b.score || 0) - (a.score || 0);
812
+ const score = (b.score || 0) - (a.score || 0);
813
+ if (score) return score;
814
+ return (typeRank[a.payload?.type] ?? 9) - (typeRank[b.payload?.type] ?? 9);
788
815
  })
789
816
  .slice(0, topK);
790
817
  }
@@ -817,7 +844,7 @@ export function namedRecallHits(db, query, { topK = 8, now } = {}) {
817
844
  }
818
845
  }
819
846
  return [...out.values()]
820
- .filter((hit) => !shouldHideStale(hit, text, at))
847
+ .filter((hit) => !isStalePayload(hit.payload, at))
821
848
  .slice(0, topK);
822
849
  }
823
850
 
@@ -827,12 +854,6 @@ function hasBusinessEdge(db, hit) {
827
854
  return incomingBusinessEdges(db, hit.id).length > 0;
828
855
  }
829
856
 
830
- function shouldHideStale(hit, query, now) {
831
- if (hit.payload?.type !== "decision") return false;
832
- if (!isStalePayload(hit.payload, now)) return false;
833
- return !queryMentionsUntil(query, hit.payload);
834
- }
835
-
836
857
  function rankBoost(db, hit, query, now) {
837
858
  let n = 0;
838
859
  if (hasBusinessEdge(db, hit)) n += 4;
@@ -864,14 +885,18 @@ function hitRecord(db, id) {
864
885
 
865
886
  const MIN_ENTITY_ANCHOR = 3;
866
887
 
888
+ function longEnoughAnchor(label) {
889
+ return label.length >= MIN_ENTITY_ANCHOR || /^[\p{Script=Han}]{2}$/u.test(label);
890
+ }
891
+
867
892
  function entityAnchorLabels(payload) {
868
893
  return uniqueStrings([payload?.name, ...(payload?.aliases || [])]).filter(
869
- (s) => s.length >= MIN_ENTITY_ANCHOR,
894
+ longEnoughAnchor,
870
895
  );
871
896
  }
872
897
 
873
898
  function queryMentionsEntityName(queryLower, nameLower) {
874
- if (!queryLower || !nameLower || nameLower.length < MIN_ENTITY_ANCHOR) return false;
899
+ if (!queryLower || !nameLower || !longEnoughAnchor(nameLower)) return false;
875
900
  if (queryLower === nameLower) return true;
876
901
  if (/^[\x00-\x7f]+$/.test(nameLower)) {
877
902
  const escaped = nameLower.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -902,7 +927,31 @@ function entityAnchorIds(db, query) {
902
927
  return ids;
903
928
  }
904
929
 
905
- function primaryScore(payload, query, now) {
930
+ const wordSegmenter = new Intl.Segmenter("zh", { granularity: "word" });
931
+ const QUERY_STOP_WORDS = new Set(["的", "了", "是", "在", "有", "和", "与", "请", "什么", "哪些", "怎么", "如何", "一下", "这个", "我们", "the", "a", "an", "is", "are", "to", "of", "for", "and", "what", "how"]);
932
+
933
+ function queryTerms(query) {
934
+ const chunks = query.toLowerCase().match(/[\p{L}\p{N}_]+(?:[-./:][\p{L}\p{N}_]+)*/gu) || [];
935
+ return [...new Set(chunks.flatMap((chunk) => /\p{Script=Han}/u.test(chunk)
936
+ ? [...wordSegmenter.segment(chunk)].filter((part) => part.isWordLike).map((part) => part.segment)
937
+ : [chunk])
938
+ .filter((term) => !QUERY_STOP_WORDS.has(term) && (!/\p{Script=Han}/u.test(term) || term.length >= 2)))];
939
+ }
940
+
941
+ function lexicalScore(payload, query, terms = queryTerms(query)) {
942
+ const hay = indexTextFor(payload).toLowerCase();
943
+ if (hay.includes(query)) return 2;
944
+ if (!terms.length) return 0;
945
+ const matched = terms.filter((term) => {
946
+ if (/\p{Script=Han}/u.test(term)) return hay.includes(term);
947
+ const escaped = term.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
948
+ return new RegExp(`(?:^|[^a-z0-9_])${escaped}(?:$|[^a-z0-9_])`, "i").test(hay);
949
+ }).length;
950
+ const coverage = matched / terms.length;
951
+ return coverage >= 0.5 ? coverage : 0;
952
+ }
953
+
954
+ function primaryScore(payload, query, now, terms) {
906
955
  const q = String(query || "").trim().toLowerCase();
907
956
  if (!q) return 0;
908
957
  const name = String(payload?.name || "").toLowerCase();
@@ -912,7 +961,7 @@ function primaryScore(payload, query, now) {
912
961
  if (name === q) score = 4;
913
962
  else if (name.includes(q)) score = 3;
914
963
  else if (text.includes(q) || until.includes(q)) score = 2;
915
- else if (indexTextFor(payload || {}).toLowerCase().includes(q)) score = 2;
964
+ else score = lexicalScore(payload || {}, q, terms);
916
965
  if (
917
966
  payload?.type === "decision" &&
918
967
  isStalePayload(payload, now) &&
@@ -993,7 +1042,7 @@ function tqlBusinessNeighbors(db, id) {
993
1042
  }
994
1043
  }
995
1044
 
996
- function keywordFallback(db, text, topK) {
1045
+ function keywordFallback(db, text, topK, terms) {
997
1046
  const q = text.toLowerCase();
998
1047
  const ids = db.allNodeIds?.() || [];
999
1048
  const hits = [];
@@ -1007,17 +1056,16 @@ function keywordFallback(db, text, topK) {
1007
1056
  ) {
1008
1057
  continue;
1009
1058
  }
1010
- const hay = indexTextFor(node.payload || {}).toLowerCase();
1011
- if (!hay.includes(q)) continue;
1059
+ const score = lexicalScore(node.payload || {}, q, terms);
1060
+ if (!score) continue;
1012
1061
  hits.push({
1013
1062
  id,
1014
- score: 1,
1063
+ score,
1015
1064
  payload: node.payload,
1016
1065
  edges: node.edges || [],
1017
1066
  });
1018
- if (hits.length >= topK) break;
1019
1067
  }
1020
- return hits;
1068
+ return hits.sort((a, b) => b.score - a.score).slice(0, topK);
1021
1069
  }
1022
1070
 
1023
1071
  function nodeLabel(db, id) {
@@ -1054,8 +1102,7 @@ function incomingBusinessEdges(db, id, { query = "", now, hideStale = false } =
1054
1102
  if (!srcNode || srcNode.payload?.status === "archived") return;
1055
1103
  if (
1056
1104
  hideStale &&
1057
- isStalePayload(srcNode.payload, at) &&
1058
- !queryMentionsUntil(query, srcNode.payload)
1105
+ isStalePayload(srcNode.payload, at)
1059
1106
  ) {
1060
1107
  return;
1061
1108
  }
@@ -1109,6 +1156,7 @@ function edgePath(db, nodeId, edges, { query = "", now, hideStale = true } = {})
1109
1156
  const seenOut = new Set();
1110
1157
  for (const e of edges || []) {
1111
1158
  const targetId = e.targetId ?? e.target_id;
1159
+ if (hideStale && isStalePayload(db.get(Number(targetId))?.payload, now)) continue;
1112
1160
  const key = `${e.label}->${targetId}`;
1113
1161
  if (seenOut.has(key)) continue;
1114
1162
  seenOut.add(key);
@@ -1170,7 +1218,7 @@ export function formatHit(db, hit, opts = {}) {
1170
1218
  l0: l0.slice(0, 200),
1171
1219
  until: payload.until || "",
1172
1220
  stale,
1173
- path: edgePath(db, hit.id, node.edges || [], { query, now }),
1221
+ path: edgePath(db, hit.id, node.edges || [], { query, now, hideStale: hit.includeStale !== true }),
1174
1222
  };
1175
1223
  }
1176
1224
 
package/lib/tools.js CHANGED
@@ -59,10 +59,11 @@ export function registerTools(ctx, optionsOrFn) {
59
59
  textTool({
60
60
  name: "ctx_find",
61
61
  description:
62
- "Search local Trivium graph memory. If the query names a known entity, also returns its unexpired about/decided/broke/fixed neighbors. Returns L0 summaries plus edge paths (incoming as <-label-id). Call ctx_read for full text. Do not paste find results verbatim into later thinking.",
62
+ "Search local Trivium graph memory by keywords, meaning (when embeddings are enabled), and entity relationships. Relevant expired decisions remain searchable, marked stale and ranked lower; treat them as historical, not current instructions. Set include_stale=false for current facts only. Returns L0 summaries and edge paths. Call ctx_read for full text.",
63
63
  parameters: {
64
64
  query: { type: "string", required: true, description: "Search query." },
65
65
  top_k: { type: "integer", description: "Max hits, 1-20." },
66
+ include_stale: { type: "boolean", description: "Include relevant expired memories (default true). Expired hits are marked stale." },
66
67
  },
67
68
  async execute(args, exec) {
68
69
  try {
@@ -74,6 +75,7 @@ export function registerTools(ctx, optionsOrFn) {
74
75
  topK: k,
75
76
  expandDepth: options.expandDepth ?? 1,
76
77
  queryVector: queryVector || undefined,
78
+ includeStale: args.include_stale !== false,
77
79
  });
78
80
  if (!hits.length) return "No memory hits.";
79
81
  const grouped = [];
@@ -145,6 +147,7 @@ export function registerTools(ctx, optionsOrFn) {
145
147
  description: "entity | preference | decision | experience",
146
148
  },
147
149
  name: { type: "string", description: "Short name for entities." },
150
+ until: { type: "string", description: "Optional explicit expiry, only when the user states this fact expires: YYYY-MM-DD, weekday (Friday / 周五), or 下周. A date mentioned in the fact is not an expiry." },
148
151
  link_to: { type: "integer", description: "Optional existing node id to link." },
149
152
  link_label: { type: "string", description: "Edge label when link_to is set." },
150
153
  },
@@ -162,6 +165,7 @@ export function registerTools(ctx, optionsOrFn) {
162
165
  type,
163
166
  name: args.name || undefined,
164
167
  text: args.text,
168
+ ...(args.until !== undefined ? { until: args.until } : {}),
165
169
  uri: `ctx://${type}/${Date.now()}`,
166
170
  source: { sessionId: String(exec?.agent?.session?.header?.id || exec?.agent?.id || "") },
167
171
  };
package/lib/until.js CHANGED
@@ -10,19 +10,7 @@ const WEEKDAYS = {
10
10
  saturday: 6,
11
11
  };
12
12
 
13
- /** Pull a display `until` label out of a fact sentence. */
14
- export function parseUntilFromText(text) {
15
- const s = String(text || "");
16
- if (/下周/.test(s)) return "下周";
17
- const day = s.match(/\buntil\s+(Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday)\b/i);
18
- if (day) return day[1];
19
- const iso = s.match(/\b(\d{4}-\d{2}-\d{2})\b/);
20
- if (iso) return iso[1];
21
- const cn = s.match(/到?(\d{1,2}\s*月\d{1,2}\s*日|周五|周末)/);
22
- if (cn) return cn[1];
23
- return undefined;
24
- }
25
-
13
+ // Only resolve an explicit expiry field. Dates mentioned in prose are not expiry.
26
14
  export function resolveUntilAt(until, from = new Date()) {
27
15
  const raw = String(until || "").trim();
28
16
  if (!raw) return undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-trivium",
3
- "version": "0.4.20",
3
+ "version": "0.4.22",
4
4
  "description": "In-process graph memory for DeepSeek Harness, backed by TriviumDB.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -47,6 +47,8 @@
47
47
  "smoke-p11": "node scripts/smoke-p11.mjs",
48
48
  "smoke-p12": "node scripts/smoke-p12.mjs",
49
49
  "smoke-p13": "node scripts/smoke-p13.mjs",
50
+ "smoke-p14": "node scripts/smoke-p14.mjs",
51
+ "smoke-p15": "node scripts/smoke-p15.mjs",
50
52
  "preuninstall": "node ./lib/purge.js",
51
53
  "verify-live-p2": "node scripts/verify-live-p2.mjs"
52
54
  },
@@ -54,11 +56,11 @@
54
56
  "node": "^22.19.0 || >=24"
55
57
  },
56
58
  "peerDependencies": {
57
- "@deepseek-ai/dsh-llm": ">=0.1.0-rc.8 <0.1.1 || >=0.1.1-rc.2 <0.2.0",
58
- "@deepseek-ai/dsh-tools": ">=0.1.0-rc.8 <0.1.1 || >=0.1.1-rc.2 <0.2.0"
59
+ "@deepseek-ai/dsh-llm": ">=0.1.0-rc.8 <0.1.1 || >=0.1.1-rc.2 <0.3.0 || >=0.1.5-rc.1 <0.1.6 || >=0.2.0-rc.0 <0.2.1 || >=0.2.1-rc.0 <0.3.0",
60
+ "@deepseek-ai/dsh-tools": ">=0.1.0-rc.8 <0.1.1 || >=0.1.1-rc.2 <0.3.0 || >=0.1.5-rc.1 <0.1.6 || >=0.2.0-rc.0 <0.2.1 || >=0.2.1-rc.0 <0.3.0"
59
61
  },
60
62
  "dependencies": {
61
- "triviumdb": "^0.8.9"
63
+ "triviumdb": "^0.8.11"
62
64
  },
63
65
  "keywords": [
64
66
  "dsh-plugin",