dsh-tdai-memory 0.4.1 → 0.4.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.
Files changed (70) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +10 -0
  3. package/README.zh.md +9 -0
  4. package/THIRD-PARTY-NOTICES.md +20 -20
  5. package/client.js +119 -33
  6. package/cordis.patch.yml +8 -8
  7. package/index.js +11 -11
  8. package/locale/en.json +6 -0
  9. package/locale/zh.json +6 -0
  10. package/package.json +6 -3
  11. package/recall-inject.js +88 -86
  12. package/recall-runtime.js +42 -0
  13. package/vendor/tdai/adapters/index.js +15 -15
  14. package/vendor/tdai/adapters/openclaw/host-adapter.js +83 -83
  15. package/vendor/tdai/adapters/openclaw/index.js +5 -5
  16. package/vendor/tdai/adapters/openclaw/llm-runner.js +69 -69
  17. package/vendor/tdai/adapters/standalone/host-adapter.js +58 -58
  18. package/vendor/tdai/adapters/standalone/index.js +5 -5
  19. package/vendor/tdai/core/conversation/l0-recorder.js +420 -420
  20. package/vendor/tdai/core/hooks/auto-capture.js +236 -236
  21. package/vendor/tdai/core/hooks/auto-recall.js +640 -640
  22. package/vendor/tdai/core/index.js +8 -8
  23. package/vendor/tdai/core/persona/persona-generator.js +184 -184
  24. package/vendor/tdai/core/persona/persona-trigger.js +94 -94
  25. package/vendor/tdai/core/profile/profile-sync.js +200 -200
  26. package/vendor/tdai/core/prompts/l1-dedup.js +72 -72
  27. package/vendor/tdai/core/prompts/l1-extraction.js +34 -34
  28. package/vendor/tdai/core/prompts/persona-generation.js +35 -35
  29. package/vendor/tdai/core/prompts/scene-extraction.js +45 -45
  30. package/vendor/tdai/core/record/l1-dedup.js +318 -318
  31. package/vendor/tdai/core/record/l1-extractor.js +379 -379
  32. package/vendor/tdai/core/record/l1-reader.js +178 -178
  33. package/vendor/tdai/core/record/l1-writer.js +141 -141
  34. package/vendor/tdai/core/report/reporter.js +86 -86
  35. package/vendor/tdai/core/scene/filename-normalizer.js +161 -161
  36. package/vendor/tdai/core/scene/scene-extractor.js +410 -410
  37. package/vendor/tdai/core/scene/scene-format.js +53 -53
  38. package/vendor/tdai/core/scene/scene-index.js +82 -82
  39. package/vendor/tdai/core/scene/scene-navigation.js +57 -57
  40. package/vendor/tdai/core/seed/input.js +404 -404
  41. package/vendor/tdai/core/seed/types.js +9 -9
  42. package/vendor/tdai/core/store/bm25-client.js +117 -117
  43. package/vendor/tdai/core/store/bm25-local.js +68 -68
  44. package/vendor/tdai/core/store/embedding.js +632 -632
  45. package/vendor/tdai/core/store/factory.js +95 -95
  46. package/vendor/tdai/core/store/search-utils.js +54 -54
  47. package/vendor/tdai/core/store/sqlite.js +1632 -1632
  48. package/vendor/tdai/core/store/tcvdb-client.js +221 -221
  49. package/vendor/tdai/core/store/tcvdb.js +1047 -1047
  50. package/vendor/tdai/core/store/types.js +17 -17
  51. package/vendor/tdai/core/tools/conversation-search.js +199 -199
  52. package/vendor/tdai/core/tools/memory-search.js +207 -207
  53. package/vendor/tdai/core/types.js +12 -12
  54. package/vendor/tdai/utils/backup.js +189 -189
  55. package/vendor/tdai/utils/checkpoint.js +360 -360
  56. package/vendor/tdai/utils/clean-context-runner.js +472 -472
  57. package/vendor/tdai/utils/ensure-hook-policy.js +213 -213
  58. package/vendor/tdai/utils/env.js +13 -13
  59. package/vendor/tdai/utils/managed-timer.js +127 -127
  60. package/vendor/tdai/utils/manifest.js +91 -91
  61. package/vendor/tdai/utils/memory-cleaner.js +300 -300
  62. package/vendor/tdai/utils/no-think-fetch.js +109 -109
  63. package/vendor/tdai/utils/openclaw-state-dir.js +26 -26
  64. package/vendor/tdai/utils/pipeline-factory.js +517 -517
  65. package/vendor/tdai/utils/pipeline-manager.js +892 -892
  66. package/vendor/tdai/utils/sanitize.js +360 -360
  67. package/vendor/tdai/utils/serial-queue.js +103 -103
  68. package/vendor/tdai/utils/session-filter.js +88 -88
  69. package/vendor/tdai/utils/text-utils.js +29 -29
  70. package/vendor/tdai/utils/time.js +252 -252
package/LICENSE CHANGED
@@ -1,21 +1,21 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 Scorp1o117
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Scorp1o117
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,9 +1,13 @@
1
1
  # dsh-tdai-memory
2
2
 
3
+ The plugin follows the DSH language setting (Chinese and English in DSH 0.2.0-rc.2), including configuration, status messages and plugin-list metadata. Language-pack locales use the host fallback chain. Switching languages preserves unsaved settings; there is no separate plugin language selector.
4
+
3
5
  ## Configuration page (DSH 0.2.0-rc.2 and later)
4
6
 
5
7
  Open **Plugins → Installed → dsh-tdai-memory** from the homepage sidebar to configure and save this plugin. The page uses the official `plugins.bundle.config` interface, without a duplicate entry in global Settings. Web and Desktop share the page. This version requires DSH 0.2.0-rc.2 or a later 0.2.x host; existing configuration is retained.
6
8
 
9
+ Choose **Automatic memory** for capture, extraction, recall and search together; **Search existing memory only** disables capture, extraction and automatic recall while keeping search tools; **Pause memory features** disables all four. The main page contains the two model connections. Individual switches, storage paths and tuning remain under **Advanced**. Existing mixed switch values appear as **Custom combination** and are preserved. Select a mode, Save, then restart DSH for pipeline changes (the search-tool switch applies immediately). Only edited fields are written; blank keys and untouched advanced options remain unchanged.
10
+
7
11
  [![中文文档](https://img.shields.io/badge/%E4%B8%AD%E6%96%87%E6%96%87%E6%A1%A3-blue)](README.zh.md)
8
12
 
9
13
  **GitHub**: [Scorp1o117/dsh-tdai-memory](https://github.com/Scorp1o117/dsh-tdai-memory) · **npm**: [dsh-tdai-memory](https://www.npmjs.com/package/dsh-tdai-memory)
@@ -57,6 +61,12 @@ The data directory reuses the existing `~/.memory-tencentdb/memory-tdai`, so
57
61
  | dsh shell | `index.js`: config mapping, `session/event` + `session/flush` capture, `system-prompt/assemble` recall injection on `agent.ctx`, tool registration, lifecycle |
58
62
  | Fallback | `recall-inject.js`: preset-row recall injection (used when mounted inside an agent preset) |
59
63
 
64
+ Recall caches expire after 30 seconds and retain at most 128 sessions per
65
+ injection instance. Failed or timed-out recalls are retried on the next assembly.
66
+ The preset row also accepts `timeoutMs` (default 4000); reaching this deadline
67
+ continues prompt assembly without memory context. Deadline timers are cleared
68
+ when recall settles early. Underlying recall work may continue after the deadline.
69
+
60
70
  Hard-won wiring details:
61
71
 
62
72
  - **Capture**: `session/flush` listener (await semantics; must complete before
package/README.zh.md CHANGED
@@ -1,9 +1,13 @@
1
1
  # dsh-tdai-memory
2
2
 
3
+ 插件跟随 DSH 的语言设置(DSH 0.2.0-rc.2 内置中文和 English),配置页面、状态提示和插件列表名称/简介同步切换。扩展语言使用宿主的回退链。切换语言保留未保存的设置,无需单独选择插件语言。
4
+
3
5
  ## 配置入口(DSH 0.2.0-rc.2 起)
4
6
 
5
7
  在首页侧边栏打开 **插件 → 已安装 → dsh-tdai-memory**,直接在插件详情页配置并保存。配置页注册到官方的 `plugins.bundle.config` 接口;全局设置页不再重复显示配置入口。Web 与桌面版使用相同界面,本版要求 DSH 0.2.0-rc.2 或更新的 0.2.x 版本。现有配置无需迁移。
6
8
 
9
+ 选择 **自动记忆**,一次开启捕获、提取、召回和搜索;**仅搜索已有记忆** 关闭捕获、提取和自动召回,保留搜索工具;**暂停记忆功能** 关闭全部四项。主页面只展示两个模型的连接信息,独立开关、存储路径和调优参数保留在 **高级设置** 中。已有开关组合显示为 **自定义组合**,不会自动覆盖。选择模式后保存,并重启 DSH 使记忆流水线设置生效(搜索工具开关即时生效)。只写入编辑过的字段,密钥留空及未编辑的高级设置保持原值。
10
+
7
11
  **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)
8
12
 
9
13
  [![Enhancement Suite](https://img.shields.io/badge/part%20of-Enhancement%20Suite-3964fe)](https://github.com/Scorp1o117/dsh-enhancement-suite) [![npm](https://img.shields.io/npm/v/dsh-enhancement-suite)](https://www.npmjs.com/package/dsh-enhancement-suite)
@@ -52,6 +56,11 @@ dsh plugin --profile desktop add dsh-tdai-memory@0.3.6
52
56
  | dsh 壳 | `index.js`:配置映射、`session/event`+`session/flush` 捕获、`agent.ctx` 上的 `system-prompt/assemble` waterfall 召回注入、工具注册、生命周期 |
53
57
  | 备用 | `recall-inject.js`:preset 行形态的召回注入(挂在 agent preset 内时用) |
54
58
 
59
+ 召回缓存默认 30 秒过期,每个注入实例最多保留 128 个会话。失败或超时的召回
60
+ 会在下次组装时重试。preset 行还支持 `timeoutMs`(默认 4000),到达期限后
61
+ 不注入记忆并继续组装提示词;提前完成时会清除超时定时器。期限到达后,底层
62
+ 召回任务可能仍会继续运行。
63
+
55
64
  关键接线(都是踩坑换来的):
56
65
 
57
66
  - **捕获**:`session/flush` 监听器(await 语义,headless 退出前必完成);`turn/start` 时间戳做 L0 cursor 下限;turn id 去重
@@ -1,20 +1,20 @@
1
- # Third-party notices
2
-
3
- ## TencentDB Agent Memory core (vendored in `vendor/tdai`)
4
-
5
- `vendor/tdai` is a TypeScript-compiled copy of the host-neutral core of
6
- [TencentCloud/TencentDB-Agent-Memory](https://github.com/TencentCloud/TencentDB-Agent-Memory)
7
- (`tdai-memory-openclaw-plugin`, npm: `@tencentdb-agent-memory/memory-tencentdb`),
8
- compiled with `tsc -p dsh-tsconfig.json` (see the upstream repo for source).
9
- Its runtime dependencies are declared in this package's `dependencies`.
10
-
11
- MIT License — Copyright (c) 2026 TencentCloud
12
-
13
- ## Runtime dependencies
14
-
15
- - `ai` / `@ai-sdk/openai` — Vercel AI SDK (Apache-2.0)
16
- - `@node-rs/jieba` — Rust jieba bindings (MIT)
17
- - `sqlite-vec` — vector search extension for SQLite (Apache-2.0 / MIT)
18
- - `json5` — JSON5 parser (MIT)
19
- - `undici` — HTTP client (MIT)
20
- - `@tencentdb-agent-memory/tcvdb-text` — TencentDB text utilities (MIT)
1
+ # Third-party notices
2
+
3
+ ## TencentDB Agent Memory core (vendored in `vendor/tdai`)
4
+
5
+ `vendor/tdai` is a TypeScript-compiled copy of the host-neutral core of
6
+ [TencentCloud/TencentDB-Agent-Memory](https://github.com/TencentCloud/TencentDB-Agent-Memory)
7
+ (`tdai-memory-openclaw-plugin`, npm: `@tencentdb-agent-memory/memory-tencentdb`),
8
+ compiled with `tsc -p dsh-tsconfig.json` (see the upstream repo for source).
9
+ Its runtime dependencies are declared in this package's `dependencies`.
10
+
11
+ MIT License — Copyright (c) 2026 TencentCloud
12
+
13
+ ## Runtime dependencies
14
+
15
+ - `ai` / `@ai-sdk/openai` — Vercel AI SDK (Apache-2.0)
16
+ - `@node-rs/jieba` — Rust jieba bindings (MIT)
17
+ - `sqlite-vec` — vector search extension for SQLite (Apache-2.0 / MIT)
18
+ - `json5` — JSON5 parser (MIT)
19
+ - `undici` — HTTP client (MIT)
20
+ - `@tencentdb-agent-memory/tcvdb-text` — TencentDB text utilities (MIT)
package/client.js CHANGED
@@ -36,6 +36,35 @@ window.__ModuleLoader__.load({
36
36
  ".__tm_status{font-size:12px;color:var(--dsw-alias-label-tertiary)}" +
37
37
  ".__tm_error{font-size:12px;color:var(--dsw-alias-state-error-primary)}" +
38
38
  ".__tm_unavailable{font-size:13px;color:var(--dsw-alias-label-tertiary)}";
39
+ // Scoped flat controls retain native keyboard and form behavior.
40
+ CSS += `
41
+ .dsh-flat.__tm_root{width:100%;max-width:720px;gap:14px;font-size:13px;line-height:1.65;color:var(--dsw-alias-label-primary);--flat-accent:var(--dsw-alias-state-business-primary,#3964fe);--flat-border:var(--dsw-alias-border-l2,#dce2eb)}
42
+ .dsh-flat.__tm_root *{box-sizing:border-box;min-width:0}
43
+ .dsh-flat.__tm_root p{margin:0}
44
+ .dsh-flat.__tm_root label[class$="_field"]{gap:7px}
45
+ .dsh-flat.__tm_root [class$="_label"]{font-size:13px;font-weight:500}
46
+ .dsh-flat.__tm_root [class$="_hint"]{font-size:12px;line-height:1.65}
47
+ .dsh-flat.__tm_root input:not([type=checkbox]),.dsh-flat.__tm_root select,.dsh-flat.__tm_root textarea{width:100%;border:1px solid var(--flat-border);border-radius:6px;background:var(--dsw-alias-bg-layer-3);color:inherit;font:inherit;padding:9px 12px;min-height:40px;box-shadow:none;transition:border-color .15s}
48
+ .dsh-flat.__tm_root input:hover:not(:disabled),.dsh-flat.__tm_root select:hover:not(:disabled),.dsh-flat.__tm_root textarea:hover:not(:disabled){border-color:var(--dsw-alias-label-tertiary)}
49
+ .dsh-flat.__tm_root select{appearance:none;padding-right:34px;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath d='m2 2 4 4 4-4' fill='none' stroke='%23778091' stroke-width='1.5'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:right 12px center}
50
+ .dsh-flat.__tm_root input[type=checkbox]{appearance:none;flex:none;width:30px;height:18px;margin:0;border:1px solid var(--flat-border);border-radius:12px;background:var(--dsw-alias-bg-layer-2);position:relative;cursor:pointer;transition:background .15s,border-color .15s}
51
+ .dsh-flat.__tm_root input[type=checkbox]::before{content:"";position:absolute;left:2px;top:2px;width:12px;height:12px;border-radius:50%;background:var(--dsw-alias-label-secondary);transition:transform .15s}
52
+ .dsh-flat.__tm_root input[type=checkbox]:checked{background:var(--flat-accent);border-color:var(--flat-accent)}
53
+ .dsh-flat.__tm_root input[type=checkbox]:checked::before{transform:translateX(12px);background:#fff}
54
+ .dsh-flat.__tm_root :is(input,select,textarea,button,summary,a):focus-visible{outline:2px solid var(--flat-accent);outline-offset:3px}
55
+ .dsh-flat.__tm_root :is(input,select,textarea,button):disabled{opacity:.5;cursor:default}
56
+ .dsh-flat.__tm_root [class$="_actions"]{flex-wrap:wrap;gap:10px;margin-top:4px;padding-top:16px;border-top:1px solid var(--flat-border)}
57
+ .dsh-flat.__tm_root details{border-top:1px solid var(--flat-border);padding:0}
58
+ .dsh-flat.__tm_root summary{list-style:none;display:flex;align-items:center;justify-content:space-between;gap:12px;padding:14px 0;font-size:13px;font-weight:500;cursor:pointer;color:var(--dsw-alias-label-secondary)}
59
+ .dsh-flat.__tm_root summary::-webkit-details-marker{display:none}
60
+ .dsh-flat.__tm_root summary::after{content:"+";font-size:18px;font-weight:400;flex:none}
61
+ .dsh-flat.__tm_root details[open]>summary::after{content:"−"}
62
+ .dsh-flat.__tm_root details>div{padding-bottom:18px}
63
+ .dsh-flat.__tm_root .__tm_group{border:0;font-weight:600;font-size:14px;margin:8px 0 0;padding:0}
64
+ @media(prefers-reduced-motion:reduce){.dsh-flat.__tm_root *,.dsh-flat.__tm_root input[type=checkbox]::before{transition:none}}
65
+ .dsh-flat.__tm_root button{border-radius:6px;min-height:34px;padding:7px 14px;font:inherit;font-size:12px;box-shadow:none}
66
+ .dsh-flat.__tm_root :is(h2,h3){margin:0;font-size:14px;font-weight:600}
67
+ `;
39
68
  var tagId = "dsh-tdai-memory/main.css";
40
69
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
41
70
  var tag = document.createElement("style");
@@ -50,10 +79,18 @@ window.__ModuleLoader__.load({
50
79
  var inject = ["slots", "locale", "configForms"];
51
80
  var zh = {
52
81
  nav: "记忆",
53
- intro: "TDAI 记忆配置:L0 捕获 → L1 结构化提取 → 召回注入。密钥只写不读。TdaiCore 在启动时构建,修改后需重启生效。",
82
+ mode: "记忆模式",
83
+ modeAuto: "自动记忆(推荐)",
84
+ modeSearch: "仅搜索已有记忆",
85
+ modePaused: "暂停记忆功能",
86
+ modeCustom: "自定义组合",
87
+ modeHint: "自动:捕获、提取、召回和搜索全部开启;仅搜索:不记录新对话、不自动注入记忆;暂停:四项全部关闭。选择后点击保存。",
88
+ advanced: "高级设置:独立开关、存储与请求参数",
89
+ setupHint: "填写提取模型和向量模型的连接信息,再保存。已有自定义设置会保留;密钥留空不修改。",
90
+ intro: "记录对话、整理长期记忆,并在相关对话中自动召回。保存后请重启 DSH 生效;密钥只写不读。",
54
91
  groupData: "数据",
55
- groupLlm: "提取 LLM(L1/L2/L3)",
56
- groupEmbedding: "向量 Embedding",
92
+ groupLlm: "记忆整理模型",
93
+ groupEmbedding: "向量检索模型",
57
94
  groupCapture: "捕获",
58
95
  groupExtraction: "提取",
59
96
  groupRecall: "召回",
@@ -95,10 +132,18 @@ window.__ModuleLoader__.load({
95
132
  };
96
133
  var en = {
97
134
  nav: "Memory",
98
- intro: "TDAI memory config: L0 capture → L1 extraction → recall injection. Keys are write-only. TdaiCore is built at startup; changes apply after a restart.",
135
+ mode: "Memory mode",
136
+ modeAuto: "Automatic memory (recommended)",
137
+ modeSearch: "Search existing memory only",
138
+ modePaused: "Pause memory features",
139
+ modeCustom: "Custom combination",
140
+ modeHint: "Automatic enables capture, extraction, recall and search. Search only records no new conversations and injects no memory. Pause disables all four. Click Save to apply your selection.",
141
+ advanced: "Advanced: individual switches, storage and request options",
142
+ setupHint: "Enter the extraction and embedding connections, then save. Existing custom settings are preserved; blank keys stay unchanged.",
143
+ intro: "Record conversations, organize long-term memory and recall it in relevant chats. Restart DSH after saving to apply changes; keys are write-only.",
99
144
  groupData: "Data",
100
- groupLlm: "Extraction LLM (L1/L2/L3)",
101
- groupEmbedding: "Embeddings",
145
+ groupLlm: "Memory extraction model",
146
+ groupEmbedding: "Memory search embeddings",
102
147
  groupCapture: "Capture",
103
148
  groupExtraction: "Extraction",
104
149
  groupRecall: "Recall",
@@ -165,6 +210,9 @@ window.__ModuleLoader__.load({
165
210
  { path: ["toolsEnabled"], label: "fieldToolsEnabled", type: "checkbox", group: "groupTools" }
166
211
  ];
167
212
  FIELDS.forEach(function (f) { f.key = f.path.join("."); });
213
+ var BASIC_KEYS = ["llm.baseUrl", "llm.apiKey", "llm.model", "embedding.baseUrl", "embedding.apiKey", "embedding.model"];
214
+ var MODE_KEYS = ["captureEnabled", "extraction.enabled", "recall.enabled", "toolsEnabled"];
215
+ var MODES = { auto: [true, true, true, true], search: [false, false, false, true], paused: [false, false, false, false] };
168
216
 
169
217
  function getPath(obj, path) {
170
218
  var cur = obj;
@@ -176,6 +224,7 @@ window.__ModuleLoader__.load({
176
224
  }
177
225
 
178
226
  function MemorySection(props) {
227
+ useLocale(props.locale);
179
228
  var t = props.t;
180
229
  var scope = props.scope;
181
230
  var [snapshot, setSnapshot] = react.useState(function () { return scope.getSnapshot(); });
@@ -204,7 +253,7 @@ window.__ModuleLoader__.load({
204
253
  // call, so depending on snapshot.value would reset user input on every
205
254
  // render (typing appears dead).
206
255
  react.useEffect(function () {
207
- if (ready) setDraft(Object.assign({}, valueToDraft(snapshot.value)));
256
+ if (ready) setDraft({});
208
257
  // eslint-disable-next-line react-hooks/exhaustive-deps
209
258
  }, [ready]);
210
259
 
@@ -226,6 +275,23 @@ window.__ModuleLoader__.load({
226
275
  setNotice(null);
227
276
  setError(null);
228
277
  }
278
+ function memoryMode() {
279
+ var flags = MODE_KEYS.map(function (key) {
280
+ return Object.prototype.hasOwnProperty.call(draft, key) ? draft[key] : getPath(value, key.split(".")) !== false;
281
+ });
282
+ return Object.keys(MODES).find(function (mode) {
283
+ return MODES[mode].every(function (flag, i) { return flag === flags[i]; });
284
+ }) || "custom";
285
+ }
286
+ function setMode(mode) {
287
+ if (!MODES[mode] || busy || !snapshot.writable) return;
288
+ setDraft(function (prev) {
289
+ var next = Object.assign({}, prev);
290
+ MODE_KEYS.forEach(function (key, i) { next[key] = MODES[mode][i]; });
291
+ return next;
292
+ });
293
+ setNotice(null); setError(null);
294
+ }
229
295
 
230
296
  function onSave() {
231
297
  if (busy || !snapshot.writable) return;
@@ -233,6 +299,7 @@ window.__ModuleLoader__.load({
233
299
  var ops = [];
234
300
  for (var i = 0; i < FIELDS.length; i += 1) {
235
301
  var f = FIELDS[i];
302
+ if (!Object.prototype.hasOwnProperty.call(draft, f.key)) continue;
236
303
  var d = fieldDraft(f);
237
304
  var current = getPath(value, f.path);
238
305
  if (f.type === "password") {
@@ -250,7 +317,7 @@ window.__ModuleLoader__.load({
250
317
  if (String(d).trim() === "" && getPath(user, f.path) === void 0) continue;
251
318
  ops.push(String(d).trim() === "" ? { op: "unset", path: f.path } : { op: "set", path: f.path, value: f.type === "number" ? Number(d) : d });
252
319
  }
253
- if (ops.length === 0) { setBusy(false); setNotice(t("saved")); return; }
320
+ if (ops.length === 0) { setBusy(false); setNotice({ key: "saved" }); return; }
254
321
  commit(ops);
255
322
  }
256
323
 
@@ -269,36 +336,38 @@ window.__ModuleLoader__.load({
269
336
  var next = scope.getSnapshot();
270
337
  setSnapshot(next);
271
338
  setBusy(false);
272
- if (!ok) { setError(t("error") + ": " + t("notApplied")); return; }
273
- setNotice(t("saved"));
274
- if (next.value) setDraft(valueToDraft(next.value));
339
+ if (!ok) { setError({ key: "error", detailKey: "notApplied" }); return; }
340
+ setNotice({ key: "saved" });
341
+ if (next.value) setDraft({});
275
342
  }).catch(function (e) {
276
- setBusy(false); setError(t("error") + ": " + String(e && e.message || e));
343
+ setBusy(false); setError({ key: "error", detail: String(e && e.message || e) });
277
344
  });
278
345
  }
279
346
 
280
- var nodes = [];
281
- var lastGroup = null;
347
+ var nodes = [], advancedNodes = [];
348
+ var lastGroups = { basic: null, advanced: null };
282
349
  // forEach callback gives each handler its own `f` — a `for (var i)`
283
350
  // loop would share one `f` across every onChange closure, so typing
284
351
  // updated the LAST field's draft and the input appeared dead.
285
352
  FIELDS.forEach(function (f) {
286
- if (f.group !== lastGroup) {
287
- lastGroup = f.group;
288
- nodes.push(h("div", { key: "g" + f.group, className: "__tm_group" }, t(f.group)));
353
+ var section = BASIC_KEYS.indexOf(f.key) !== -1 ? "basic" : "advanced";
354
+ var target = section === "basic" ? nodes : advancedNodes;
355
+ if (f.group !== lastGroups[section]) {
356
+ lastGroups[section] = f.group;
357
+ target.push(h("div", { key: "g" + f.group, className: "__tm_group" }, t(f.group)));
289
358
  }
290
359
  var overridden = getPath(user, f.path) !== void 0;
291
360
  if (f.type === "checkbox") {
292
- nodes.push(h("label", { key: f.path.join("."), className: "__tm_field" },
361
+ target.push(h("label", { key: f.path.join("."), className: "__tm_field" },
293
362
  h("span", { className: "__tm_row" },
294
- h("input", { className: "__tm_check", type: "checkbox", checked: Boolean(fieldDraft(f)), onChange: function (e) { setField(f, e.target.checked); } }),
363
+ h("input", { className: "__tm_check", type: "checkbox", disabled: busy || !snapshot.writable, checked: Boolean(fieldDraft(f)), onChange: function (e) { setField(f, e.target.checked); } }),
295
364
  h("span", { className: "__tm_label" }, t(f.label)),
296
365
  overridden ? h("span", { className: "__tm_override" }, t("overridden")) : null
297
366
  )
298
367
  ));
299
368
  return;
300
369
  }
301
- nodes.push(h("label", { key: f.path.join("."), className: "__tm_field" },
370
+ target.push(h("label", { key: f.path.join("."), className: "__tm_field" },
302
371
  h("span", { className: "__tm_label" },
303
372
  t(f.label),
304
373
  overridden ? h("span", { className: "__tm_override" }, t("overridden")) : null
@@ -307,6 +376,7 @@ window.__ModuleLoader__.load({
307
376
  className: "__tm_input",
308
377
  type: f.type === "password" ? "password" : f.type === "number" ? "number" : "text",
309
378
  value: fieldDraft(f),
379
+ disabled: busy || !snapshot.writable,
310
380
  placeholder: f.type === "password" ? (overridden ? "••••••••" : t("secretHint")) : "",
311
381
  onChange: function (e) { setField(f, e.target.value); }
312
382
  }),
@@ -314,29 +384,45 @@ window.__ModuleLoader__.load({
314
384
  ));
315
385
  });
316
386
 
317
- return h("div", { className: "__tm_root" },
387
+ return h("div", { className: "__tm_root dsh-flat" },
318
388
  h("p", { className: "__tm_hint", style: { margin: "0 0 4px" } }, t("intro")),
389
+ h("label", { className: "__tm_field" },
390
+ h("span", { className: "__tm_label" }, t("mode")),
391
+ h("select", { className: "__tm_input", "aria-label": t("mode"), value: memoryMode(), disabled: busy || !snapshot.writable,
392
+ onChange: function (e) { setMode(e.target.value); } },
393
+ h("option", { value: "auto" }, t("modeAuto")), h("option", { value: "search" }, t("modeSearch")),
394
+ h("option", { value: "paused" }, t("modePaused")), h("option", { value: "custom", disabled: true }, t("modeCustom"))),
395
+ h("span", { className: "__tm_hint" }, t("modeHint"))),
396
+ h("p", { className: "__tm_hint" }, t("setupHint")),
319
397
  nodes,
398
+ h("details", null, h("summary", { style: { cursor: "pointer" } }, t("advanced")),
399
+ h("div", { style: { display: "flex", flexDirection: "column", gap: 10, paddingTop: 12 } }, advancedNodes)),
320
400
  h("div", { className: "__tm_actions" },
321
401
  h("button", { type: "button", className: "__tm_btn __tm_btnPrimary", onClick: onSave, disabled: busy || !snapshot.writable }, t("save")),
322
402
  h("button", { type: "button", className: "__tm_btn", onClick: onReset, disabled: busy || !snapshot.writable }, t("reset")),
323
- notice ? h("span", { className: "__tm_status" }, notice) : null,
403
+ notice ? h("span", { className: "__tm_status" }, messageText(t, notice)) : null,
324
404
  busy ? h("span", { className: "__tm_status" }, t("saving")) : null,
325
- error ? h("span", { className: "__tm_error" }, error) : null
405
+ error ? h("span", { className: "__tm_error" }, messageText(t, error)) : null
326
406
  )
327
407
  );
328
408
  }
329
409
 
330
- function valueToDraft(value) {
331
- var out = {};
332
- for (var i = 0; i < FIELDS.length; i += 1) {
333
- var f = FIELDS[i];
334
- out[f.key] = f.secret ? "" : f.type === "checkbox" ? Boolean(getPath(value, f.path)) : String(getPath(value, f.path) ?? "");
335
- }
336
- return out;
337
- }
338
-
339
410
  // ── plugin ────────────────────────────────────────────────────────────
411
+
412
+ // Follow the host language without remounting the form or losing drafts.
413
+ function useLocale(locale) {
414
+ var refresh = react.useState(0)[1];
415
+ react.useEffect(function () {
416
+ if (!locale || typeof locale.subscribe !== "function") return;
417
+ return locale.subscribe(function () { refresh(function (revision) { return revision + 1; }); });
418
+ }, [locale]);
419
+ }
420
+ // Keep translation keys in state so feedback follows later language changes.
421
+ function messageText(t, message) {
422
+ if (!message) return "";
423
+ return t(message.key) + (message.detailKey ? ": " + t(message.detailKey) : message.detail ? ": " + message.detail : "");
424
+ }
425
+
340
426
  function apply(ctx) {
341
427
  var t = ctx.locale.bind(NS);
342
428
  ctx.effect(function () { return ctx.locale.register(NS, { zh: zh, en: en }); }, "dsh-tdai-memory: dictionaries");
@@ -347,7 +433,7 @@ window.__ModuleLoader__.load({
347
433
  key: "dsh-tdai-memory",
348
434
  locale: NS
349
435
  }, function (props) {
350
- return h(MemorySection, Object.assign({}, props, { scope: scope, t: t }));
436
+ return h(MemorySection, Object.assign({}, props, { scope: scope, t: t, locale: ctx.locale }));
351
437
  });
352
438
  });
353
439
  }
package/cordis.patch.yml CHANGED
@@ -1,8 +1,8 @@
1
- # dsh-tdai-memory — bundle patch layer
2
- #
3
- # Declares the plugin as a Cordis loader entry, making the package a standard
4
- # profile bundle: `dsh plugin --profile <p> add dsh-tdai-memory` installs and
5
- # mounts it without manual cordis.patch.yml edits.
6
- - insert:
7
- - id: tdai-memory
8
- name: 'dsh-tdai-memory'
1
+ # dsh-tdai-memory — bundle patch layer
2
+ #
3
+ # Declares the plugin as a Cordis loader entry, making the package a standard
4
+ # profile bundle: `dsh plugin --profile <p> add dsh-tdai-memory` installs and
5
+ # mounts it without manual cordis.patch.yml edits.
6
+ - insert:
7
+ - id: tdai-memory
8
+ name: 'dsh-tdai-memory'
package/index.js CHANGED
@@ -24,6 +24,7 @@ import { defineTool } from "@deepseek-ai/dsh-tools";
24
24
  import { TdaiCore } from "./vendor/tdai/core/tdai-core.js";
25
25
  import { parseConfig } from "./vendor/tdai/config.js";
26
26
  import { StandaloneHostAdapter } from "./vendor/tdai/adapters/standalone/host-adapter.js";
27
+ import { createRecallCache, recallWithTimeout } from "./recall-runtime.js";
27
28
 
28
29
  /** Cordis plugin name. */
29
30
  const name = "tdai-memory";
@@ -145,8 +146,7 @@ function apply(ctx, config) {
145
146
  /** session keys seen this process (for final L1 flush on teardown). */
146
147
  const seenSessions = new Set();
147
148
  /** recall cache per session: same user text within TTL reuses the result. */
148
- const recallCache = new Map();
149
- const RECALL_CACHE_TTL_MS = 30_000;
149
+ const recallCache = createRecallCache();
150
150
 
151
151
  // One-shot mode (dsh --profile headless): the process exits right after
152
152
  // flush, so L1 must finish inside the flush listener. In web mode the
@@ -231,6 +231,7 @@ function apply(ctx, config) {
231
231
  // new core; initialize() only sets up stores/pipeline.
232
232
  tdaiConfig = nextConfig;
233
233
  core = nextCore;
234
+ recallCache.clear();
234
235
  await nextCore.initialize();
235
236
  const key = nextConfig.llm.apiKey || "";
236
237
  log("info", `initialized (dataDir=${dataDir}, llm=${nextConfig.llm.model}, apiKey=${key ? `${key.slice(0, 6)}…` : "<empty>"}, embedding=${nextConfig.embedding.model})`);
@@ -321,16 +322,15 @@ function apply(ctx, config) {
321
322
  if (!lastUser) return assembly;
322
323
  const text = extractText(lastUser.content);
323
324
  if (!text) return assembly;
324
- const now = Date.now();
325
- const cached = recallCache.get(session.id);
326
- if (cached && cached.text === text && now - cached.ts < RECALL_CACHE_TTL_MS) {
327
- return injectRecall(assembly, cached.result);
325
+ const cached = recallCache.get(session.id, text);
326
+ if (cached !== undefined) {
327
+ return injectRecall(assembly, cached);
328
328
  }
329
- const result = await Promise.race([
330
- core.handleBeforeRecall(text, session.id),
331
- new Promise((resolve) => setTimeout(() => resolve(null), tdaiConfig.recall.timeoutMs + 1000)),
332
- ]).catch(() => null);
333
- recallCache.set(session.id, { text, result: result ?? {}, ts: now });
329
+ const recallCore = core;
330
+ const result = await recallWithTimeout(
331
+ () => recallCore.handleBeforeRecall(text, session.id), tdaiConfig.recall.timeoutMs + 1000,
332
+ );
333
+ if (core === recallCore) recallCache.set(session.id, text, result);
334
334
  return injectRecall(assembly, result ?? {});
335
335
  } catch (error) {
336
336
  ctx.logger.warn(`[tdai-memory] recall failed: ${String(error)}`);
package/locale/en.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "Long-term memory",
4
+ "description": "Organize long-term memories from conversations and recall relevant context, with search-only and paused modes."
5
+ }
6
+ }
package/locale/zh.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "长期记忆",
4
+ "description": "自动整理对话中的长期记忆,并在相关对话中召回;支持仅搜索和暂停模式。"
5
+ }
6
+ }
package/package.json CHANGED
@@ -1,13 +1,14 @@
1
1
  {
2
2
  "name": "dsh-tdai-memory",
3
- "version": "0.4.1",
3
+ "version": "0.4.2",
4
4
  "description": "DeepSeek Harness 的 TencentDB Agent Memory 移植:L0 对话捕获 → L1 结构化记忆提取 → L2 场景/L3 画像,自动召回注入 + 记忆/对话搜索工具;复用现有 ~/.memory-tencentdb/memory-tdai 数据;附 Web UI 设置栏。",
5
5
  "type": "module",
6
6
  "main": "index.js",
7
7
  "exports": {
8
8
  ".": "./index.js",
9
9
  "./client": "./client.js",
10
- "./package.json": "./package.json"
10
+ "./package.json": "./package.json",
11
+ "./locale/*.json": "./locale/*.json"
11
12
  },
12
13
  "dsh": {
13
14
  "bundle": {
@@ -38,8 +39,10 @@
38
39
  },
39
40
  "homepage": "https://github.com/Scorp1o117/dsh-tdai-memory",
40
41
  "files": [
42
+ "locale",
41
43
  "index.js",
42
44
  "recall-inject.js",
45
+ "recall-runtime.js",
43
46
  "client.js",
44
47
  "cordis.patch.yml",
45
48
  "vendor",
@@ -92,6 +95,6 @@
92
95
  }
93
96
  },
94
97
  "scripts": {
95
- "test": "node tests/settings-scope-compat.test.mjs && node tests/install-contract.test.mjs && node --test tests/plugin-page.test.mjs tests/client-write-flow.test.mjs tests/tools-registration.test.mjs"
98
+ "test": "node tests/settings-scope-compat.test.mjs && node tests/install-contract.test.mjs && node --test tests/plugin-page.test.mjs tests/client-write-flow.test.mjs tests/tools-registration.test.mjs tests/recall-runtime.test.mjs && node --test tests/locale.test.mjs"
96
99
  }
97
100
  }