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 +16 -4
- package/README.zh-CN.md +16 -4
- package/lib/extract.js +8 -8
- package/lib/host-format.js +35 -0
- package/lib/index.js +46 -16
- package/lib/markdown.js +0 -2
- package/lib/pins.js +3 -3
- package/lib/schema.js +1 -1
- package/lib/sidecar.js +1 -1
- package/lib/store.js +96 -48
- package/lib/tools.js +5 -1
- package/lib/until.js +1 -13
- package/package.json +6 -4
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`).
|
|
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` |
|
|
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
|
-
-
|
|
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
|
|
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` |
|
|
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
|
-
-
|
|
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 {
|
|
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":"","
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
436
|
-
|
|
437
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
643
|
-
|
|
644
|
-
|
|
646
|
+
const until = String(body.until || "").trim();
|
|
647
|
+
if (!until) {
|
|
648
|
+
delete body.until;
|
|
649
|
+
delete body.untilAt;
|
|
650
|
+
return body;
|
|
645
651
|
}
|
|
646
|
-
|
|
647
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
740
|
-
const hits = db.searchHybrid(vec, text, Math.max(topK, 16), 0, 0.01, 0.2
|
|
741
|
-
|
|
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) => !
|
|
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
|
|
786
|
-
if (
|
|
787
|
-
return (
|
|
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) => !
|
|
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
|
-
|
|
894
|
+
longEnoughAnchor,
|
|
870
895
|
);
|
|
871
896
|
}
|
|
872
897
|
|
|
873
898
|
function queryMentionsEntityName(queryLower, nameLower) {
|
|
874
|
-
if (!queryLower || !nameLower || nameLower
|
|
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
|
-
|
|
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
|
|
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
|
|
1011
|
-
if (!
|
|
1059
|
+
const score = lexicalScore(node.payload || {}, q, terms);
|
|
1060
|
+
if (!score) continue;
|
|
1012
1061
|
hits.push({
|
|
1013
1062
|
id,
|
|
1014
|
-
score
|
|
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
|
|
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
|
-
|
|
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.
|
|
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.
|
|
63
|
+
"triviumdb": "^0.8.11"
|
|
62
64
|
},
|
|
63
65
|
"keywords": [
|
|
64
66
|
"dsh-plugin",
|