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 +71 -31
- package/README.zh.md +68 -0
- package/client.js +303 -0
- package/index.js +132 -69
- package/package.json +62 -53
- package/vendor/tdai/utils/checkpoint.js +5 -1
package/README.md
CHANGED
|
@@ -1,56 +1,96 @@
|
|
|
1
1
|
# dsh-tdai-memory
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](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
|
-
|
|
8
|
-
|
|
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
|
-
|
|
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
|
-
|
|
|
20
|
-
|
|
|
21
|
-
| dsh
|
|
22
|
-
|
|
|
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
|
-
|
|
27
|
-
-
|
|
28
|
-
|
|
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
|
-
|
|
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: '
|
|
67
|
+
name: 'dsh-tdai-memory'
|
|
35
68
|
config:
|
|
36
69
|
extraction:
|
|
37
70
|
enabled: true
|
|
38
|
-
enableDedup: false # dedup
|
|
39
|
-
llm: # L1/L2/L3
|
|
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
|
-
|
|
42
|
-
|
|
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
|
-
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
384
|
+
await core?.destroy().catch((error) => {
|
|
322
385
|
ctx.logger.warn(`[tdai-memory] destroy failed: ${String(error)}`);
|
|
323
386
|
});
|
|
324
387
|
};
|
|
325
|
-
}, "tdai-memory.
|
|
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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
"
|
|
11
|
-
|
|
12
|
-
"
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
"
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
"
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
"
|
|
40
|
-
|
|
41
|
-
"
|
|
42
|
-
|
|
43
|
-
"
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
"
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
"
|
|
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) {
|