dsh-tdai-memory 0.1.0 → 0.2.2

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
@@ -1,56 +1,96 @@
1
1
  # dsh-tdai-memory
2
2
 
3
- 把 **TencentDB Agent Memory**(腾讯云开源的四层记忆系统,原为 OpenClaw 插件)移植进 DeepSeek Harness。
3
+ [![中文文档](https://img.shields.io/badge/%E4%B8%AD%E6%96%87%E6%96%87%E6%A1%A3-blue)](README.zh.md)
4
4
 
5
- ## 能力
5
+ **GitHub**: [Scorp1o117/dsh-tdai-memory](https://github.com/Scorp1o117/dsh-tdai-memory) · **npm**: [dsh-tdai-memory](https://www.npmjs.com/package/dsh-tdai-memory)
6
6
 
7
- - **L0 对话捕获**:每轮对话(turn 结束、请求边界)自动写入原始对话(JSONL + SQLite + FTS + 向量)
8
- - **L1 结构化记忆**:后台管线用 LLM 从对话中提取事实/偏好/事件(persona / episodic / instruction),写入 `records/` + SQLite + FTS + 向量
9
- - **L2 场景 / L3 画像**:场景块与用户画像生成(管线自动调度)
10
- - **自动召回注入**:每次组装提示词时,按当前用户消息检索相关记忆 + 画像,作为动态上下文注入(模型"凭空想起")
11
- - **工具**:`tdai_memory_search`(L1 结构化检索)、`tdai_conversation_search`(L0 原文检索)
7
+ A port of **TencentDB Agent Memory** (Tencent Cloud's open-source four-layer
8
+ memory system, originally an OpenClaw plugin) into DeepSeek Harness.
12
9
 
13
- 数据目录复用现有 `~/.memory-tencentdb/memory-tdai`,**旧记忆无缝继承**。
10
+ ## Features
14
11
 
15
- ## 架构(移植方式)
12
+ - **L0 conversation capture**: every turn (turn end, request boundary) is
13
+ written to raw conversation storage (JSONL + SQLite + FTS + vectors)
14
+ - **L1 structured memory**: a background pipeline uses an LLM to extract
15
+ facts / preferences / events (persona / episodic / instruction) from
16
+ conversations, stored in `records/` + SQLite + FTS + vectors
17
+ - **L2 scenes / L3 persona**: scene blocks and user profile generation
18
+ (pipeline-scheduled)
19
+ - **Automatic recall injection**: on every prompt assembly, relevant memories
20
+ and the user profile are retrieved by the current user message and injected
21
+ as dynamic context (the model "just remembers")
22
+ - **Tools**: `tdai_memory_search` (L1 structured search),
23
+ `tdai_conversation_search` (L0 raw-text search)
16
24
 
17
- | 层 | 内容 |
25
+ The data directory reuses the existing `~/.memory-tencentdb/memory-tdai`, so
26
+ **previously accumulated memories carry over seamlessly**.
27
+
28
+ ## Architecture (porting approach)
29
+
30
+ | Layer | Content |
18
31
  |---|---|
19
- | 核心 | `tdai-memory-openclaw-plugin` 的 host-neutral 核心(`src/core`、`src/utils`),tsc 编译为 ESM(`dist-dsh/`),零改动 |
20
- | 宿主适配 | `StandaloneHostAdapter`(官方 standalone 模式,OpenAI 兼容直调) |
21
- | dsh 壳 | `index.js`:配置映射、`session/event`+`session/flush` 捕获、`agent.ctx` 上的 `system-prompt/assemble` waterfall 召回注入、工具注册、生命周期 |
22
- | 备用 | `recall-inject.js`:preset 行形态的召回注入(挂在 agent preset 内时用) |
32
+ | Core | The host-neutral core of `tdai-memory-openclaw-plugin` (`src/core`, `src/utils`), tsc-compiled to ESM (`dist-dsh/`), zero changes |
33
+ | Host adapter | `StandaloneHostAdapter` (official standalone mode, direct OpenAI-compatible calls) |
34
+ | dsh shell | `index.js`: config mapping, `session/event` + `session/flush` capture, `system-prompt/assemble` recall injection on `agent.ctx`, tool registration, lifecycle |
35
+ | Fallback | `recall-inject.js`: preset-row recall injection (used when mounted inside an agent preset) |
36
+
37
+ Hard-won wiring details:
38
+
39
+ - **Capture**: `session/flush` listener (await semantics; must complete before
40
+ headless exits); `turn/start` timestamps as the L0 cursor floor; turn-id dedup
41
+ - **Headless one-shot runs**: wait for `core.handleSessionEnd()` inside flush
42
+ (L1 extraction finishes before exit; otherwise the 5s shutdown timeout kills it)
43
+ - **Recall injection**: must be registered on **`agent.ctx`** (assembly runs in
44
+ the agent scope; root listeners never see it); attach one tick after
45
+ `session/created` by resolving the agent from the `agents` service
23
46
 
24
- 关键接线(都是踩坑换来的):
47
+ ## Configuration (profile patch + settings)
25
48
 
26
- - **捕获**:`session/flush` 监听器(await 语义,headless 退出前必完成);`turn/start` 时间戳做 L0 cursor 下限;turn id 去重
27
- - **headless 一次性任务**:flush 内再等 `core.handleSessionEnd()`(L1 提取跑完才退出;否则 5s 关闭超时会杀掉提取)
28
- - **召回注入**:必须注册在 **`agent.ctx`** 上(组装在 agent 作用域进行,root 监听器收不到);`session/created` 后延迟一 tick 从 `agents` 服务拿 agent
49
+ Configuration is **settings-namespace driven**: the profile patch is the base
50
+ layer, and the `tdai-memory:` section of `$DSH_HOME/settings.yaml` overrides it
51
+ (LLM/embedding keys live in settings.yaml). The **Web UI Settings → 记忆**
52
+ section edits every field (v0.2.0, write-only keys); TdaiCore is built at
53
+ startup, so changes apply **after a restart**.
29
54
 
30
- ## 配置(profile patch)
55
+ ```yaml
56
+ # $DSH_HOME/settings.yaml
57
+ tdai-memory:
58
+ llm:
59
+ apiKey: 'sk-...'
60
+ embedding:
61
+ apiKey: 'local-no-key'
62
+ ```
31
63
 
32
64
  ```yaml
65
+ # profile patch (base layer)
33
66
  - id: tdai-memory
34
- name: './plugins/dsh-tdai-memory/index.js'
67
+ name: 'dsh-tdai-memory'
35
68
  config:
36
69
  extraction:
37
70
  enabled: true
38
- enableDedup: false # dedup 的 LLM 输出解析不稳,先关
39
- llm: # L1/L2/L3 提取模型(OpenAI 兼容)
71
+ enableDedup: false # dedup LLM output parsing is flaky; off by default
72
+ llm: # L1/L2/L3 extraction model (OpenAI-compatible)
40
73
  baseUrl: 'https://opencode.ai/zen/go/v1'
41
- apiKey: 'sk-...'
42
- model: 'mimo-v2.5' # deepseek-v4-flash 对提取 JSON 输出不合格
43
- embedding: # 向量(OpenAI 兼容 /v1/embeddings)
74
+ model: 'mimo-v2.5' # deepseek-v4-flash produces invalid extraction JSON
75
+ embedding: # vectors (OpenAI-compatible /v1/embeddings)
44
76
  baseUrl: 'http://127.0.0.1:8088/v1'
45
- apiKey: 'local-no-key' # parseConfig 强制 apiKey 非空,本地端点可占位
46
77
  model: 'Qwen3-Embedding-0.6B'
47
78
  dimensions: 1024
48
79
  sendDimensions: false
49
80
  ```
50
81
 
51
- ## 已知取舍
82
+ ## Known trade-offs
83
+
84
+ - **Extraction model**: `mimo-v2.5` extracts correctly but takes 20-30s per
85
+ call (background execution, does not block the conversation);
86
+ `deepseek-v4-flash` is fast but its JSON output is non-compliant (extracts 0)
87
+ - **dedup**: LLM conflict-detection output parsing is unstable (once caused
88
+ stored=0); off by default; enable only with a more reliable model
89
+ - **L1 memory vectors**: written with storage (8088 embedding is fast); L0
90
+ vectors run as a background task, drained by `destroy()` on headless exit
91
+ - **Upgrades**: after pulling new upstream code, rerun
92
+ `npx tsc -p dsh-tsconfig.json` in the tdai project dir (output in `dist-dsh/`)
93
+
94
+ ## License
52
95
 
53
- - **提取模型**:`mimo-v2.5` 提取正确但单次 20-30s(后台执行,不阻塞对话);`deepseek-v4-flash` 快但 JSON 输出不合规(提取 0 条)
54
- - **dedup**:LLM 冲突检测输出解析不稳(曾导致 stored=0),默认关闭;开启需换更稳的模型
55
- - **L1 记忆向量**:随存储写入(8088 embedding 快);L0 向量走后台任务,headless 退出时由 `destroy()` drain
56
- - **升级**:上游拉新代码后,在 tdai 项目目录重跑 `npx tsc -p dsh-tsconfig.json`(产物在 `dist-dsh/`)
96
+ MIT
package/README.zh.md ADDED
@@ -0,0 +1,68 @@
1
+ # dsh-tdai-memory
2
+
3
+ **GitHub**: [Scorp1o117/dsh-tdai-memory](https://github.com/Scorp1o117/dsh-tdai-memory) · **npm**: [dsh-tdai-memory](https://www.npmjs.com/package/dsh-tdai-memory) · [English](README.md)
4
+
5
+ 把 **TencentDB Agent Memory**(腾讯云开源的四层记忆系统,原为 OpenClaw 插件)移植进 DeepSeek Harness。
6
+
7
+ ## 能力
8
+
9
+ - **L0 对话捕获**:每轮对话(turn 结束、请求边界)自动写入原始对话(JSONL + SQLite + FTS + 向量)
10
+ - **L1 结构化记忆**:后台管线用 LLM 从对话中提取事实/偏好/事件(persona / episodic / instruction),写入 `records/` + SQLite + FTS + 向量
11
+ - **L2 场景 / L3 画像**:场景块与用户画像生成(管线自动调度)
12
+ - **自动召回注入**:每次组装提示词时,按当前用户消息检索相关记忆 + 画像,作为动态上下文注入(模型"凭空想起")
13
+ - **工具**:`tdai_memory_search`(L1 结构化检索)、`tdai_conversation_search`(L0 原文检索)
14
+
15
+ 数据目录复用现有 `~/.memory-tencentdb/memory-tdai`,**旧记忆无缝继承**。
16
+
17
+ ## 架构(移植方式)
18
+
19
+ | 层 | 内容 |
20
+ |---|---|
21
+ | 核心 | `tdai-memory-openclaw-plugin` 的 host-neutral 核心(`src/core`、`src/utils`),tsc 编译为 ESM(`dist-dsh/`),零改动 |
22
+ | 宿主适配 | `StandaloneHostAdapter`(官方 standalone 模式,OpenAI 兼容直调) |
23
+ | dsh 壳 | `index.js`:配置映射、`session/event`+`session/flush` 捕获、`agent.ctx` 上的 `system-prompt/assemble` waterfall 召回注入、工具注册、生命周期 |
24
+ | 备用 | `recall-inject.js`:preset 行形态的召回注入(挂在 agent preset 内时用) |
25
+
26
+ 关键接线(都是踩坑换来的):
27
+
28
+ - **捕获**:`session/flush` 监听器(await 语义,headless 退出前必完成);`turn/start` 时间戳做 L0 cursor 下限;turn id 去重
29
+ - **headless 一次性任务**:flush 内再等 `core.handleSessionEnd()`(L1 提取跑完才退出;否则 5s 关闭超时会杀掉提取)
30
+ - **召回注入**:必须注册在 **`agent.ctx`** 上(组装在 agent 作用域进行,root 监听器收不到);`session/created` 后延迟一 tick 从 `agents` 服务拿 agent
31
+
32
+ ## 配置(profile patch + settings)
33
+
34
+ 配置以 **settings 命名空间**驱动:profile patch 作为 base 层,`$DSH_HOME/settings.yaml` 的 `tdai-memory:` 节覆盖(LLM/embedding 密钥已迁到 settings.yaml)。Web UI 设置 → 记忆 可编辑全部字段(v0.2.0,含写-only 密钥);TdaiCore 启动时构建,改动**重启后生效**。
35
+
36
+ ```yaml
37
+ # $DSH_HOME/settings.yaml
38
+ tdai-memory:
39
+ llm:
40
+ apiKey: 'sk-...'
41
+ embedding:
42
+ apiKey: 'local-no-key'
43
+ ```
44
+
45
+ ```yaml
46
+ # profile patch(base 层)
47
+ - id: tdai-memory
48
+ name: 'dsh-tdai-memory'
49
+ config:
50
+ extraction:
51
+ enabled: true
52
+ enableDedup: false # dedup 的 LLM 输出解析不稳,先关
53
+ llm: # L1/L2/L3 提取模型(OpenAI 兼容)
54
+ baseUrl: 'https://opencode.ai/zen/go/v1'
55
+ model: 'mimo-v2.5' # deepseek-v4-flash 对提取 JSON 输出不合格
56
+ embedding: # 向量(OpenAI 兼容 /v1/embeddings)
57
+ baseUrl: 'http://127.0.0.1:8088/v1'
58
+ model: 'Qwen3-Embedding-0.6B'
59
+ dimensions: 1024
60
+ sendDimensions: false
61
+ ```
62
+
63
+ ## 已知取舍
64
+
65
+ - **提取模型**:`mimo-v2.5` 提取正确但单次 20-30s(后台执行,不阻塞对话);`deepseek-v4-flash` 快但 JSON 输出不合规(提取 0 条)
66
+ - **dedup**:LLM 冲突检测输出解析不稳(曾导致 stored=0),默认关闭;开启需换更稳的模型
67
+ - **L1 记忆向量**:随存储写入(8088 embedding 快);L0 向量走后台任务,headless 退出时由 `destroy()` drain
68
+ - **升级**:上游拉新代码后,在 tdai 项目目录重跑 `npx tsc -p dsh-tsconfig.json`(产物在 `dist-dsh/`)
package/client.js ADDED
@@ -0,0 +1,303 @@
1
+ /**
2
+ * dsh-tdai-memory — browser half.
3
+ *
4
+ * A "记忆" section inside the Web UI settings page: edits the `tdai-memory`
5
+ * settings namespace (data dir, extraction LLM, embeddings, capture/extract/
6
+ * recall switches, tools) through the settings scope transport plus nested
7
+ * `settings.mutate` ops. TdaiCore is built at startup, so changes apply
8
+ * after a restart (noted in the UI).
9
+ *
10
+ * Hand-written ModuleLoader bundle — no build step required.
11
+ */
12
+ window.__ModuleLoader__.load({
13
+ id: "dsh-tdai-memory",
14
+ factory: (require) => {
15
+ var module = { exports: {} };
16
+ var exports = module.exports;
17
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
18
+ var react = require("react");
19
+ var h = react.createElement;
20
+
21
+ // ── CSS (theme tokens) ────────────────────────────────────────────────
22
+ var CSS = ".__tm_root{max-width:640px;display:flex;flex-direction:column;gap:10px}" +
23
+ ".__tm_group{font-size:13px;font-weight:700;color:var(--dsw-alias-label-primary);border-bottom:1px solid var(--dsw-alias-border-l2);padding-bottom:4px;margin:6px 0 2px}" +
24
+ ".__tm_field{display:flex;flex-direction:column;gap:4px}" +
25
+ ".__tm_label{font-size:12px;font-weight:600;color:var(--dsw-alias-label-primary);display:flex;align-items:center;gap:6px}" +
26
+ ".__tm_override{font-size:10px;color:var(--dsw-alias-state-business-primary);border:1px solid var(--dsw-alias-border-l2);border-radius:4px;padding:0 4px}" +
27
+ ".__tm_hint{font-size:11px;color:var(--dsw-alias-label-tertiary)}" +
28
+ ".__tm_input{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);font:inherit;color:var(--dsw-alias-label-primary);border-radius:8px;padding:6px 10px;font-size:13px;box-sizing:border-box;width:100%}" +
29
+ ".__tm_row{display:flex;align-items:center;gap:8px}" +
30
+ ".__tm_check{accent-color:var(--dsw-alias-state-business-primary)}" +
31
+ ".__tm_actions{display:flex;gap:8px;align-items:center;margin-top:4px}" +
32
+ ".__tm_btn{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-primary);border-radius:8px;padding:6px 14px;font:inherit;font-size:13px;cursor:pointer}" +
33
+ ".__tm_btn:hover:not(:disabled){border-color:var(--dsw-alias-state-business-primary)}" +
34
+ ".__tm_btn:disabled{opacity:.5;cursor:default}" +
35
+ ".__tm_btnPrimary{border-color:var(--dsw-alias-state-business-primary);background:var(--dsw-alias-state-business-primary);color:var(--dsw-alias-label-on-accent)}" +
36
+ ".__tm_status{font-size:12px;color:var(--dsw-alias-label-tertiary)}" +
37
+ ".__tm_error{font-size:12px;color:var(--dsw-alias-state-error-primary)}" +
38
+ ".__tm_unavailable{font-size:13px;color:var(--dsw-alias-label-tertiary)}";
39
+ var tagId = "dsh-tdai-memory/main.css";
40
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
41
+ var tag = document.createElement("style");
42
+ tag.dataset.plugin = "dsh-tdai-memory";
43
+ tag.dataset.pluginCss = tagId;
44
+ tag.textContent = CSS;
45
+ document.head.appendChild(tag);
46
+ }
47
+
48
+ // ── locale ────────────────────────────────────────────────────────────
49
+ var NS = "tdaiMemory";
50
+ var inject = ["slots", "locale", "settingsScope", "connection"];
51
+ var zh = {
52
+ nav: "记忆",
53
+ intro: "TDAI 记忆配置:L0 捕获 → L1 结构化提取 → 召回注入。密钥只写不读。TdaiCore 在启动时构建,修改后需重启生效。",
54
+ groupData: "数据",
55
+ groupLlm: "提取 LLM(L1/L2/L3)",
56
+ groupEmbedding: "向量 Embedding",
57
+ groupCapture: "捕获",
58
+ groupExtraction: "提取",
59
+ groupRecall: "召回",
60
+ groupTools: "工具",
61
+ secretHint: "留空保持当前密钥。",
62
+ save: "保存",
63
+ reset: "恢复默认",
64
+ saved: "已保存(重启后生效)",
65
+ saving: "保存中…",
66
+ error: "保存失败",
67
+ unavailable: "设置命名空间不可用(服务端未注册 tdai-memory 命名空间?)",
68
+ overridden: "已覆盖",
69
+ loading: "加载中…"
70
+ };
71
+ var en = {
72
+ nav: "Memory",
73
+ intro: "TDAI memory config: L0 capture → L1 extraction → recall injection. Keys are write-only. TdaiCore is built at startup; changes apply after a restart.",
74
+ groupData: "Data",
75
+ groupLlm: "Extraction LLM (L1/L2/L3)",
76
+ groupEmbedding: "Embeddings",
77
+ groupCapture: "Capture",
78
+ groupExtraction: "Extraction",
79
+ groupRecall: "Recall",
80
+ groupTools: "Tools",
81
+ secretHint: "Leave blank to keep the current key.",
82
+ save: "Save",
83
+ reset: "Reset",
84
+ saved: "Saved (applies after restart)",
85
+ saving: "Saving…",
86
+ error: "Save failed",
87
+ unavailable: "Settings namespace unavailable (tdai-memory namespace not registered server-side?)",
88
+ overridden: "overridden",
89
+ loading: "Loading…"
90
+ };
91
+
92
+ // ── field spec: dotted path + type + group ─────────────────────────────
93
+ var FIELDS = [
94
+ { path: ["dataDir"], label: "数据目录(空 = ~/.memory-tencentdb/memory-tdai)", type: "text", group: "groupData" },
95
+ { path: ["llm", "baseUrl"], label: "Base URL", type: "text", group: "groupLlm" },
96
+ { path: ["llm", "apiKey"], label: "API Key", type: "password", secret: true, group: "groupLlm" },
97
+ { path: ["llm", "model"], label: "模型", type: "text", group: "groupLlm" },
98
+ { path: ["llm", "maxTokens"], label: "最大输出 Tokens", type: "number", group: "groupLlm" },
99
+ { path: ["llm", "timeoutMs"], label: "超时(毫秒)", type: "number", group: "groupLlm" },
100
+ { path: ["embedding", "baseUrl"], label: "Base URL", type: "text", group: "groupEmbedding" },
101
+ { path: ["embedding", "apiKey"], label: "API Key", type: "password", secret: true, group: "groupEmbedding" },
102
+ { path: ["embedding", "model"], label: "模型", type: "text", group: "groupEmbedding" },
103
+ { path: ["embedding", "dimensions"], label: "向量维度", type: "number", group: "groupEmbedding" },
104
+ { path: ["embedding", "sendDimensions"], label: "请求带 dimensions 参数", type: "checkbox", group: "groupEmbedding" },
105
+ { path: ["captureEnabled"], label: "捕获对话(L0)", type: "checkbox", group: "groupCapture" },
106
+ { path: ["extraction", "enabled"], label: "结构化提取(L1)", type: "checkbox", group: "groupExtraction" },
107
+ { path: ["extraction", "enableDedup"], label: "冲突检测(额外 LLM 调用)", type: "checkbox", group: "groupExtraction" },
108
+ { path: ["recall", "enabled"], label: "召回注入", type: "checkbox", group: "groupRecall" },
109
+ { path: ["recall", "maxResults"], label: "最大召回条数", type: "number", group: "groupRecall" },
110
+ { path: ["recall", "scoreThreshold"], label: "相似度阈值", type: "number", group: "groupRecall" },
111
+ { path: ["recall", "timeoutMs"], label: "召回超时(毫秒)", type: "number", group: "groupRecall" },
112
+ { path: ["toolsEnabled"], label: "注册搜索工具(tdai_memory_search / tdai_conversation_search)", type: "checkbox", group: "groupTools" }
113
+ ];
114
+ FIELDS.forEach(function (f) { f.key = f.path.join("."); });
115
+
116
+ function getPath(obj, path) {
117
+ var cur = obj;
118
+ for (var i = 0; i < path.length; i += 1) {
119
+ if (cur === null || cur === void 0 || typeof cur !== "object") return void 0;
120
+ cur = cur[path[i]];
121
+ }
122
+ return cur;
123
+ }
124
+
125
+ function MemorySection(props) {
126
+ var t = props.t;
127
+ var scope = props.scope;
128
+ var api = props.api;
129
+ var snapshot = react.useSyncExternalStore(
130
+ function (cb) { return scope.subscribe(cb); },
131
+ function () { return scope.getSnapshot(); }
132
+ );
133
+ var ready = snapshot.status === "ready" && snapshot.value !== void 0;
134
+ var [draft, setDraft] = react.useState({});
135
+ var [busy, setBusy] = react.useState(false);
136
+ var [notice, setNotice] = react.useState(null);
137
+ var [error, setError] = react.useState(null);
138
+
139
+ react.useEffect(function () {
140
+ scope.load();
141
+ return function () { scope.dispose(); };
142
+ }, [scope]);
143
+ react.useEffect(function () {
144
+ if (ready) setDraft(Object.assign({}, valueToDraft(snapshot.value)));
145
+ // eslint-disable-next-line react-hooks/exhaustive-deps
146
+ }, [ready, snapshot.value]);
147
+
148
+ if (snapshot.status === "unavailable") {
149
+ return h("p", { className: "__tm_unavailable" }, t("unavailable"));
150
+ }
151
+ if (!ready) return h("p", { className: "__tm_status" }, t("loading"));
152
+
153
+ var value = snapshot.value;
154
+ var user = snapshot.user || {};
155
+
156
+ function fieldDraft(f) {
157
+ if (f.type === "checkbox") return draft[f.key] !== void 0 ? draft[f.key] : Boolean(getPath(value, f.path));
158
+ return draft[f.key] !== void 0 ? draft[f.key] : String(getPath(value, f.path) ?? "");
159
+ }
160
+ function setField(f, v) {
161
+ setDraft(function (prev) { var next = Object.assign({}, prev); next[f.key] = v; return next; });
162
+ setNotice(null);
163
+ setError(null);
164
+ }
165
+
166
+ function onSave() {
167
+ setBusy(true); setNotice(null); setError(null);
168
+ var ops = [];
169
+ for (var i = 0; i < FIELDS.length; i += 1) {
170
+ var f = FIELDS[i];
171
+ var d = fieldDraft(f);
172
+ var current = getPath(value, f.path);
173
+ if (f.type === "password") {
174
+ if (!d) continue; // blank keeps the current key
175
+ if (d === String(current ?? "")) continue;
176
+ ops.push({ op: "set", path: f.path, value: d });
177
+ continue;
178
+ }
179
+ if (f.type === "checkbox") {
180
+ if (Boolean(d) === Boolean(current)) continue;
181
+ ops.push(Boolean(d) ? { op: "set", path: f.path, value: true } : { op: "unset", path: f.path });
182
+ continue;
183
+ }
184
+ if (String(d) === String(current ?? "")) continue;
185
+ if (String(d).trim() === "" && getPath(user, f.path) === void 0) continue;
186
+ ops.push(String(d).trim() === "" ? { op: "unset", path: f.path } : { op: "set", path: f.path, value: f.type === "number" ? Number(d) : d });
187
+ }
188
+ if (ops.length === 0) { setBusy(false); setNotice(t("saved")); return; }
189
+ api.settings.mutate({
190
+ ns: "tdai-memory",
191
+ ops: ops,
192
+ ...snapshot.revision === void 0 ? {} : { expectedRevision: snapshot.revision }
193
+ }).then(function (response) {
194
+ setBusy(false);
195
+ if (!response.result.ok) {
196
+ var detail = response.result.error || {};
197
+ setError(t("error") + ": " + String(detail.message || detail.code || "unknown"));
198
+ return;
199
+ }
200
+ setNotice(t("saved"));
201
+ scope.load();
202
+ }).catch(function (e) {
203
+ setBusy(false); setError(t("error") + ": " + String(e && e.message || e));
204
+ });
205
+ }
206
+
207
+ function onReset() {
208
+ setBusy(true); setNotice(null); setError(null);
209
+ api.settings.mutate({
210
+ ns: "tdai-memory",
211
+ ops: FIELDS.map(function (f) { return { op: "unset", path: f.path }; }),
212
+ ...snapshot.revision === void 0 ? {} : { expectedRevision: snapshot.revision }
213
+ }).then(function (response) {
214
+ setBusy(false);
215
+ if (!response.result.ok) { setError(t("error")); return; }
216
+ setNotice(t("saved"));
217
+ scope.load();
218
+ }).catch(function (e) {
219
+ setBusy(false); setError(t("error") + ": " + String(e && e.message || e));
220
+ });
221
+ }
222
+
223
+ var nodes = [];
224
+ var lastGroup = null;
225
+ for (var i = 0; i < FIELDS.length; i += 1) {
226
+ var f = FIELDS[i];
227
+ if (f.group !== lastGroup) {
228
+ lastGroup = f.group;
229
+ nodes.push(h("div", { key: "g" + f.group, className: "__tm_group" }, t(f.group)));
230
+ }
231
+ var overridden = getPath(user, f.path) !== void 0;
232
+ if (f.type === "checkbox") {
233
+ nodes.push(h("label", { key: f.path.join("."), className: "__tm_field" },
234
+ h("span", { className: "__tm_row" },
235
+ h("input", { className: "__tm_check", type: "checkbox", checked: Boolean(fieldDraft(f)), onChange: function (e) { setField(f, e.target.checked); } }),
236
+ h("span", { className: "__tm_label" }, f.label),
237
+ overridden ? h("span", { className: "__tm_override" }, t("overridden")) : null
238
+ )
239
+ ));
240
+ continue;
241
+ }
242
+ nodes.push(h("label", { key: f.path.join("."), className: "__tm_field" },
243
+ h("span", { className: "__tm_label" },
244
+ f.label,
245
+ overridden ? h("span", { className: "__tm_override" }, t("overridden")) : null
246
+ ),
247
+ h("input", {
248
+ className: "__tm_input",
249
+ type: f.type === "password" ? "password" : f.type === "number" ? "number" : "text",
250
+ value: fieldDraft(f),
251
+ placeholder: f.type === "password" ? (overridden ? "••••••••" : t("secretHint")) : "",
252
+ onChange: function (e) { setField(f, e.target.value); }
253
+ }),
254
+ f.type === "password" ? h("span", { className: "__tm_hint" }, t("secretHint")) : null
255
+ ));
256
+ }
257
+
258
+ return h("div", { className: "__tm_root" },
259
+ h("p", { className: "__tm_hint", style: { margin: "0 0 4px" } }, t("intro")),
260
+ nodes,
261
+ h("div", { className: "__tm_actions" },
262
+ h("button", { type: "button", className: "__tm_btn __tm_btnPrimary", onClick: onSave, disabled: busy || !snapshot.writable }, t("save")),
263
+ h("button", { type: "button", className: "__tm_btn", onClick: onReset, disabled: busy || !snapshot.writable }, t("reset")),
264
+ notice ? h("span", { className: "__tm_status" }, notice) : null,
265
+ busy ? h("span", { className: "__tm_status" }, t("saving")) : null,
266
+ error ? h("span", { className: "__tm_error" }, error) : null
267
+ )
268
+ );
269
+ }
270
+
271
+ function valueToDraft(value) {
272
+ var out = {};
273
+ for (var i = 0; i < FIELDS.length; i += 1) {
274
+ var f = FIELDS[i];
275
+ out[f.key] = f.type === "checkbox" ? Boolean(getPath(value, f.path)) : String(getPath(value, f.path) ?? "");
276
+ }
277
+ return out;
278
+ }
279
+
280
+ // ── plugin ────────────────────────────────────────────────────────────
281
+ function apply(ctx) {
282
+ var t = ctx.locale.bind(NS);
283
+ ctx.effect(function () { return ctx.locale.register(NS, { zh: zh, en: en }); }, "dsh-tdai-memory: dictionaries");
284
+ var scope = ctx.settingsScope.bind({ namespace: "tdai-memory" });
285
+ var api = ctx.connection.api;
286
+ ctx.slots.inject("settings.section", function () {
287
+ return ctx.slots.register({
288
+ name: "settings.section",
289
+ id: "tdai-memory",
290
+ order: 26,
291
+ label: function () { return t("nav"); },
292
+ locale: NS
293
+ }, function (props) {
294
+ return h(MemorySection, Object.assign({}, props, { scope: scope, api: api }));
295
+ });
296
+ });
297
+ }
298
+
299
+ exports.apply = apply;
300
+ exports.inject = inject;
301
+ return module.exports;
302
+ }
303
+ });
package/index.js CHANGED
@@ -21,6 +21,7 @@ import os from "node:os";
21
21
  import { join } from "node:path";
22
22
  import z from "@deepseek-ai/schemastery";
23
23
  import { defineTool } from "@deepseek-ai/dsh-tools";
24
+ import { installSettingsSection, settingsNamespace } from "@deepseek-ai/dsh-settings";
24
25
  import { TdaiCore } from "./vendor/tdai/core/tdai-core.js";
25
26
  import { parseConfig } from "./vendor/tdai/config.js";
26
27
  import { StandaloneHostAdapter } from "./vendor/tdai/adapters/standalone/host-adapter.js";
@@ -29,6 +30,8 @@ import { StandaloneHostAdapter } from "./vendor/tdai/adapters/standalone/host-ad
29
30
  const name = "tdai-memory";
30
31
  /** Services this row injects. */
31
32
  const inject = ["sessions", "systemPrompt", "tools"];
33
+ /** Settings namespace owned by this plugin (Web UI settings section). */
34
+ const NS = settingsNamespace("tdai-memory");
32
35
 
33
36
  /** Runtime schema for the tdai-memory row. */
34
37
  const Config = z.object({
@@ -37,7 +40,7 @@ const Config = z.object({
37
40
  /** LLM used for L1/L2/L3 extraction (OpenAI-compatible). Falls back to TDAI_LLM_* env. */
38
41
  llm: z.object({
39
42
  baseUrl: z.string().default(""),
40
- apiKey: z.string().default(""),
43
+ apiKey: z.string().default("").role("secret"),
41
44
  model: z.string().default(""),
42
45
  maxTokens: z.number().default(4096),
43
46
  timeoutMs: z.number().default(120000),
@@ -45,7 +48,7 @@ const Config = z.object({
45
48
  /** Embedding endpoint (OpenAI-compatible /v1/embeddings). */
46
49
  embedding: z.object({
47
50
  baseUrl: z.string().default("http://127.0.0.1:8088/v1"),
48
- apiKey: z.string().default(""),
51
+ apiKey: z.string().default("").role("secret"),
49
52
  model: z.string().default("Qwen3-Embedding-0.6B"),
50
53
  dimensions: z.number().default(1024),
51
54
  sendDimensions: z.boolean().default(false),
@@ -81,64 +84,21 @@ function extractText(content) {
81
84
  }
82
85
 
83
86
  function apply(ctx, config) {
84
- const dataDir = config.dataDir || join(os.homedir(), ".memory-tencentdb", "memory-tdai");
85
-
86
- const tdaiConfig = parseConfig({
87
- timezone: "system",
88
- storeBackend: "sqlite",
89
- capture: { enabled: config.captureEnabled },
90
- extraction: { enabled: config.extraction.enabled, enableDedup: config.extraction.enableDedup },
91
- recall: { ...config.recall },
92
- embedding: {
93
- enabled: true,
94
- provider: "openai",
95
- baseUrl: config.embedding.baseUrl,
96
- apiKey: config.embedding.apiKey,
97
- model: config.embedding.model,
98
- dimensions: config.embedding.dimensions,
99
- sendDimensions: config.embedding.sendDimensions,
100
- timeoutMs: 10000,
101
- },
102
- llm: {
103
- enabled: true,
104
- baseUrl: config.llm.baseUrl || process.env.TDAI_LLM_BASE_URL || "",
105
- apiKey: config.llm.apiKey || process.env.TDAI_LLM_API_KEY || "",
106
- model: config.llm.model || process.env.TDAI_LLM_MODEL || "deepseek-v4-flash",
107
- maxTokens: config.llm.maxTokens,
108
- timeoutMs: config.llm.timeoutMs,
109
- },
110
- });
111
-
112
- // Verbose tdai-internal logging goes to the console only when explicitly
113
- // enabled; routine diagnostics use the dsh logger.
114
- const verbose = !!process.env.DSH_TDAI_DEBUG;
115
- const log = (level, m) => {
116
- if (verbose) console.error(`[tdai-memory] ${m}`);
117
- else if (level === "warn") ctx.logger.warn(`[tdai-memory] ${m}`);
118
- else if (level === "error") ctx.logger.error(`[tdai-memory] ${m}`);
119
- };
120
- const logger = {
121
- debug: (m) => log("debug", m),
122
- info: (m) => log("info", m),
123
- warn: (m) => log("warn", m),
124
- error: (m) => log("error", m),
125
- };
126
-
127
- const hostAdapter = new StandaloneHostAdapter({
128
- dataDir,
129
- llmConfig: {
130
- baseUrl: tdaiConfig.llm.baseUrl,
131
- apiKey: tdaiConfig.llm.apiKey,
132
- model: tdaiConfig.llm.model,
133
- maxTokens: tdaiConfig.llm.maxTokens,
134
- timeoutMs: tdaiConfig.llm.timeoutMs,
135
- },
136
- logger,
137
- defaultUserId: "dsh-user",
138
- platform: "dsh",
139
- });
140
-
141
- const core = new TdaiCore({ hostAdapter, config: tdaiConfig, instanceId: "dsh" });
87
+ // ── build-once state (rebuildable) ───────────────────────────────────────
88
+ // TdaiCore is built from the RESOLVED settings value (composition entry as
89
+ // base, `tdai-memory` settings section / settings.yaml as user layer), so
90
+ // keys stored in settings.yaml (llm.apiKey etc.) are honored. The settings
91
+ // inject callback that drives `installSettingsSection`'s setSource is
92
+ // scheduled by cordis as a plugin start — it ALWAYS runs after any 0ms
93
+ // timer — so the no-settings fallback below cannot be allowed to win the
94
+ // race. Instead of racing, we let the fallback build from the entry config
95
+ // first (guaranteeing a working core even without settings), and REBUILD
96
+ // from the resolved settings when the settings mount arrives. The fallback
97
+ // is a one-shot; the settings build always wins afterwards.
98
+ let core = null;
99
+ let tdaiConfig = null;
100
+ let dataDir = "";
101
+ let builtOnce = false;
142
102
 
143
103
  /** turn id of the last turn/end per session (capture trigger). */
144
104
  const lastTurnEnd = new Map();
@@ -157,6 +117,82 @@ function apply(ctx, config) {
157
117
  // long-lived process runs the pipeline in the background instead.
158
118
  const isOneShot = process.argv.some((arg) => arg === "headless");
159
119
 
120
+ // Verbose tdai-internal logging goes to the console only when explicitly
121
+ // enabled; routine diagnostics use the dsh logger.
122
+ const verbose = !!process.env.DSH_TDAI_DEBUG;
123
+ const log = (level, m) => {
124
+ if (verbose) console.error(`[tdai-memory] ${m}`);
125
+ else if (level === "warn") ctx.logger.warn(`[tdai-memory] ${m}`);
126
+ else if (level === "error") ctx.logger.error(`[tdai-memory] ${m}`);
127
+ };
128
+ const logger = {
129
+ debug: (m) => log("debug", m),
130
+ info: (m) => log("info", m),
131
+ warn: (m) => log("warn", m),
132
+ error: (m) => log("error", m),
133
+ };
134
+
135
+ // ── core construction (rebuildable) ──────────────────────────────────────
136
+ async function build(cfg) {
137
+ if (cfg?.llm?.apiKey) {
138
+ log("debug", `build: using settings config (apiKey=${cfg.llm.apiKey.slice(0, 6)}…)`);
139
+ }
140
+ // Tear down the previous core, if any (first build has none).
141
+ const previous = core;
142
+ if (previous) {
143
+ await previous.destroy().catch((error) => {
144
+ log("warn", `previous core destroy failed: ${String(error)}`);
145
+ });
146
+ }
147
+ dataDir = cfg.dataDir || join(os.homedir(), ".memory-tencentdb", "memory-tdai");
148
+ const nextConfig = parseConfig({
149
+ timezone: "system",
150
+ storeBackend: "sqlite",
151
+ capture: { enabled: cfg.captureEnabled },
152
+ extraction: { enabled: cfg.extraction.enabled, enableDedup: cfg.extraction.enableDedup },
153
+ recall: { ...cfg.recall },
154
+ embedding: {
155
+ enabled: true,
156
+ provider: "openai",
157
+ baseUrl: cfg.embedding.baseUrl,
158
+ apiKey: cfg.embedding.apiKey,
159
+ model: cfg.embedding.model,
160
+ dimensions: cfg.embedding.dimensions,
161
+ sendDimensions: cfg.embedding.sendDimensions,
162
+ timeoutMs: 10000,
163
+ },
164
+ llm: {
165
+ enabled: true,
166
+ baseUrl: cfg.llm.baseUrl || process.env.TDAI_LLM_BASE_URL || "",
167
+ apiKey: cfg.llm.apiKey || process.env.TDAI_LLM_API_KEY || "",
168
+ model: cfg.llm.model || process.env.TDAI_LLM_MODEL || "deepseek-v4-flash",
169
+ maxTokens: cfg.llm.maxTokens,
170
+ timeoutMs: cfg.llm.timeoutMs,
171
+ },
172
+ });
173
+ const nextHostAdapter = new StandaloneHostAdapter({
174
+ dataDir,
175
+ llmConfig: {
176
+ baseUrl: nextConfig.llm.baseUrl,
177
+ apiKey: nextConfig.llm.apiKey,
178
+ model: nextConfig.llm.model,
179
+ maxTokens: nextConfig.llm.maxTokens,
180
+ timeoutMs: nextConfig.llm.timeoutMs,
181
+ },
182
+ logger,
183
+ defaultUserId: "dsh-user",
184
+ platform: "dsh",
185
+ });
186
+ const nextCore = new TdaiCore({ hostAdapter: nextHostAdapter, config: nextConfig, instanceId: "dsh" });
187
+ // Publish before initialize so concurrent flush listeners already use the
188
+ // new core; initialize() only sets up stores/pipeline.
189
+ tdaiConfig = nextConfig;
190
+ core = nextCore;
191
+ await nextCore.initialize();
192
+ const key = nextConfig.llm.apiKey || "";
193
+ log("info", `initialized (dataDir=${dataDir}, llm=${nextConfig.llm.model}, apiKey=${key ? `${key.slice(0, 6)}…` : "<empty>"}, embedding=${nextConfig.embedding.model})`);
194
+ }
195
+
160
196
  // ── capture: triggered on session/flush (awaitable; runs before process
161
197
  // exit in headless, and at every request boundary in web) ─────────────
162
198
  ctx.on("session/event", (session, event) => {
@@ -177,7 +213,7 @@ function apply(ctx, config) {
177
213
  await captureTurn(session, startedAt);
178
214
  if (isOneShot) {
179
215
  // Run pending L1 extraction to completion before the process exits.
180
- await core.handleSessionEnd(session.id).catch((error) => {
216
+ await core?.handleSessionEnd(session.id).catch((error) => {
181
217
  ctx.logger.warn(`[tdai-memory] L1 flush failed: ${String(error)}`);
182
218
  });
183
219
  }
@@ -187,7 +223,7 @@ function apply(ctx, config) {
187
223
  });
188
224
 
189
225
  async function captureTurn(session, startedAt) {
190
- if (!tdaiConfig.capture.enabled) return;
226
+ if (!tdaiConfig?.capture.enabled) return;
191
227
  const derived = session.deriveMessages();
192
228
  const lastUserIdx = derived.map((m) => m.role).lastIndexOf("user");
193
229
  if (lastUserIdx < 0) return;
@@ -229,7 +265,7 @@ function apply(ctx, config) {
229
265
  if (!agent?.ctx) return;
230
266
  agent.ctx.on("system-prompt/assemble", async (assembly, context) => {
231
267
  try {
232
- if (!tdaiConfig.recall.enabled) return assembly;
268
+ if (!tdaiConfig?.recall.enabled) return assembly;
233
269
  const lastUser = [...session.deriveMessages()].reverse().find((m) => m.role === "user");
234
270
  if (!lastUser) return assembly;
235
271
  const text = extractText(lastUser.content);
@@ -304,25 +340,52 @@ function apply(ctx, config) {
304
340
  }));
305
341
  }
306
342
 
343
+ // ── settings-backed configuration ─────────────────────────────────────────
344
+ // The composition entry stays the base layer; a registered `tdai-memory`
345
+ // settings section overlays it (settings.yaml). `installSettingsSection`
346
+ // hands `setSource` a GETTER (`() => scope.get()`), not the config object —
347
+ // we must call it to obtain the resolved settings. The settings mount
348
+ // (cordis inject) always arrives after our fallback timer, so the fallback
349
+ // builds the core from the entry config first (one-shot) and the settings
350
+ // build REBUILDS the core with the resolved values (apiKey included).
351
+ installSettingsSection(ctx, NS, Config, config, {
352
+ setSource: (getter) => {
353
+ builtOnce = true;
354
+ void build(getter());
355
+ },
356
+ onChange: () => {
357
+ ctx.logger.warn("[tdai-memory] settings updated; restart to apply (TdaiCore is built at startup)");
358
+ },
359
+ });
360
+
361
+ // Fallback when no settings service ever mounts: build from the entry
362
+ // config. One-shot — if the settings mount arrives later, its build
363
+ // supersedes this one. Delayed past the 0ms window so headless/web boots
364
+ // are never blocked on this timer.
365
+ setTimeout(() => {
366
+ if (!builtOnce) {
367
+ builtOnce = true;
368
+ void build(config);
369
+ }
370
+ }, 500);
371
+
307
372
  // ── lifecycle ────────────────────────────────────────────────────────────
308
- ctx.effect(async () => {
309
- await core.initialize();
310
- ctx.logger.info(`[tdai-memory] initialized (dataDir=${dataDir}, llm=${tdaiConfig.llm.model}, embedding=${tdaiConfig.embedding.model})`);
373
+ ctx.effect(() => {
311
374
  return async () => {
312
375
  // Final flush: run pending L1 extraction for every session seen this
313
376
  // process and wait for it to finish (headless exits right after the
314
377
  // tree disposes, so this is what makes extraction complete in one-shot
315
378
  // runs). Unknown keys are tolerated as no-ops.
316
379
  for (const sessionKey of seenSessions) {
317
- await core.handleSessionEnd(sessionKey).catch((error) => {
380
+ await core?.handleSessionEnd(sessionKey).catch((error) => {
318
381
  ctx.logger.warn(`[tdai-memory] final L1 flush failed for ${sessionKey}: ${String(error)}`);
319
382
  });
320
383
  }
321
- await core.destroy().catch((error) => {
384
+ await core?.destroy().catch((error) => {
322
385
  ctx.logger.warn(`[tdai-memory] destroy failed: ${String(error)}`);
323
386
  });
324
387
  };
325
- }, "tdai-memory.initialize()");
388
+ }, "tdai-memory.cleanup()");
326
389
  }
327
390
 
328
391
  export { Config, apply, inject, name };
package/package.json CHANGED
@@ -1,53 +1,62 @@
1
- {
2
- "name": "dsh-tdai-memory",
3
- "version": "0.1.0",
4
- "description": "TencentDB Agent Memory for DeepSeek Harness: L0 conversation capture, L1 structured memory extraction, L2 scenes / L3 persona, automatic recall injection, and memory search tools. Uses the existing ~/.memory-tencentdb/memory-tdai data directory.",
5
- "type": "module",
6
- "main": "index.js",
7
- "exports": {
8
- ".": "./index.js"
9
- },
10
- "files": [
11
- "index.js",
12
- "recall-inject.js",
13
- "vendor",
14
- "README.md",
15
- "README.zh.md",
16
- "LICENSE",
17
- "THIRD-PARTY-NOTICES.md"
18
- ],
19
- "keywords": [
20
- "dsh",
21
- "dsh-plugin",
22
- "deepseek-harness",
23
- "memory",
24
- "tencentdb",
25
- "agent-memory",
26
- "rag",
27
- "vector-search",
28
- "persona"
29
- ],
30
- "license": "MIT",
31
- "engines": {
32
- "node": ">=22.18"
33
- },
34
- "repository": {
35
- "type": "git",
36
- "url": "git+https://github.com/Scorp1o117/dsh-tdai-memory.git"
37
- },
38
- "bugs": {
39
- "url": "https://github.com/Scorp1o117/dsh-tdai-memory/issues"
40
- },
41
- "dependencies": {
42
- "@ai-sdk/openai": "^3.0.53",
43
- "@deepseek-ai/dsh-tools": "^0.1.0-rc.6",
44
- "@deepseek-ai/schemastery": "^3.18.1",
45
- "@node-rs/jieba": "^2.0.1",
46
- "@tencentdb-agent-memory/tcvdb-text": "^0.1.1",
47
- "ai": "^6.0.164",
48
- "json5": "^2.2.3",
49
- "sqlite-vec": "0.1.7-alpha.2",
50
- "undici": "^8.1.0"
51
- }
52
- }
53
-
1
+ {
2
+ "name": "dsh-tdai-memory",
3
+ "version": "0.2.2",
4
+ "description": "DeepSeek Harness 的 TencentDB Agent Memory 移植:L0 对话捕获 → L1 结构化记忆提取 → L2 场景/L3 画像,自动召回注入 + 记忆/对话搜索工具;复用现有 ~/.memory-tencentdb/memory-tdai 数据;附 Web UI 设置栏。",
5
+ "type": "module",
6
+ "main": "index.js",
7
+ "exports": {
8
+ ".": "./index.js",
9
+ "./client": "./client.js",
10
+ "./package.json": "./package.json"
11
+ },
12
+ "dsh": {
13
+ "client": {
14
+ "platform": "web"
15
+ }
16
+ },
17
+ "homepage": "https://github.com/Scorp1o117/dsh-tdai-memory",
18
+ "files": [
19
+ "index.js",
20
+ "recall-inject.js",
21
+ "client.js",
22
+ "vendor",
23
+ "README.md",
24
+ "README.zh.md",
25
+ "LICENSE",
26
+ "THIRD-PARTY-NOTICES.md"
27
+ ],
28
+ "keywords": [
29
+ "dsh",
30
+ "dsh-plugin",
31
+ "deepseek-harness",
32
+ "memory",
33
+ "tencentdb",
34
+ "agent-memory",
35
+ "rag",
36
+ "vector-search",
37
+ "persona"
38
+ ],
39
+ "license": "MIT",
40
+ "engines": {
41
+ "node": "\u003e=22.18"
42
+ },
43
+ "repository": {
44
+ "type": "git",
45
+ "url": "git+https://github.com/Scorp1o117/dsh-tdai-memory.git"
46
+ },
47
+ "bugs": {
48
+ "url": "https://github.com/Scorp1o117/dsh-tdai-memory/issues"
49
+ },
50
+ "dependencies": {
51
+ "@ai-sdk/openai": "^3.0.53",
52
+ "@deepseek-ai/dsh-settings": "^0.1.0-rc.6",
53
+ "@deepseek-ai/dsh-tools": "^0.1.0-rc.6",
54
+ "@deepseek-ai/schemastery": "^3.18.1",
55
+ "@node-rs/jieba": "^2.0.1",
56
+ "@tencentdb-agent-memory/tcvdb-text": "^0.1.1",
57
+ "ai": "^6.0.164",
58
+ "json5": "^2.2.3",
59
+ "sqlite-vec": "0.1.7-alpha.2",
60
+ "undici": "^8.1.0"
61
+ }
62
+ }
@@ -338,8 +338,12 @@ export class CheckpointManager {
338
338
  const state = this.getRunnerState(cp, sessionKey);
339
339
  let afterTimestamp = state.last_captured_timestamp || 0;
340
340
  // Cold-start guard (same logic that was previously in auto-capture.ts)
341
+ // Subtract 1ms from the plugin-start floor: the very first turn may be
342
+ // flushed in the same millisecond the plugin started, and the L0 filter
343
+ // is strict `timestamp > cursor` — an identical timestamp would drop
344
+ // the first turn entirely.
341
345
  if (afterTimestamp === 0 && pluginStartTimestamp && pluginStartTimestamp > 0) {
342
- afterTimestamp = pluginStartTimestamp;
346
+ afterTimestamp = pluginStartTimestamp - 1;
343
347
  }
344
348
  const result = await fn(afterTimestamp);
345
349
  if (result) {