dsh-tdai-memory 0.4.0 → 0.4.1

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 (66) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +34 -26
  3. package/README.zh.md +19 -18
  4. package/THIRD-PARTY-NOTICES.md +20 -20
  5. package/cordis.patch.yml +8 -8
  6. package/index.js +157 -76
  7. package/package.json +97 -97
  8. package/recall-inject.js +86 -86
  9. package/vendor/tdai/adapters/index.js +15 -15
  10. package/vendor/tdai/adapters/openclaw/host-adapter.js +83 -83
  11. package/vendor/tdai/adapters/openclaw/index.js +5 -5
  12. package/vendor/tdai/adapters/openclaw/llm-runner.js +69 -69
  13. package/vendor/tdai/adapters/standalone/host-adapter.js +58 -58
  14. package/vendor/tdai/adapters/standalone/index.js +5 -5
  15. package/vendor/tdai/core/conversation/l0-recorder.js +420 -420
  16. package/vendor/tdai/core/hooks/auto-capture.js +236 -236
  17. package/vendor/tdai/core/hooks/auto-recall.js +640 -640
  18. package/vendor/tdai/core/index.js +8 -8
  19. package/vendor/tdai/core/persona/persona-generator.js +184 -184
  20. package/vendor/tdai/core/persona/persona-trigger.js +94 -94
  21. package/vendor/tdai/core/profile/profile-sync.js +200 -200
  22. package/vendor/tdai/core/prompts/l1-dedup.js +72 -72
  23. package/vendor/tdai/core/prompts/l1-extraction.js +34 -34
  24. package/vendor/tdai/core/prompts/persona-generation.js +35 -35
  25. package/vendor/tdai/core/prompts/scene-extraction.js +45 -45
  26. package/vendor/tdai/core/record/l1-dedup.js +318 -318
  27. package/vendor/tdai/core/record/l1-extractor.js +379 -379
  28. package/vendor/tdai/core/record/l1-reader.js +178 -178
  29. package/vendor/tdai/core/record/l1-writer.js +141 -141
  30. package/vendor/tdai/core/report/reporter.js +86 -86
  31. package/vendor/tdai/core/scene/filename-normalizer.js +161 -161
  32. package/vendor/tdai/core/scene/scene-extractor.js +410 -410
  33. package/vendor/tdai/core/scene/scene-format.js +53 -53
  34. package/vendor/tdai/core/scene/scene-index.js +82 -82
  35. package/vendor/tdai/core/scene/scene-navigation.js +57 -57
  36. package/vendor/tdai/core/seed/input.js +404 -404
  37. package/vendor/tdai/core/seed/types.js +9 -9
  38. package/vendor/tdai/core/store/bm25-client.js +117 -117
  39. package/vendor/tdai/core/store/bm25-local.js +68 -68
  40. package/vendor/tdai/core/store/embedding.js +632 -632
  41. package/vendor/tdai/core/store/factory.js +95 -95
  42. package/vendor/tdai/core/store/search-utils.js +54 -54
  43. package/vendor/tdai/core/store/sqlite.js +1632 -1632
  44. package/vendor/tdai/core/store/tcvdb-client.js +221 -221
  45. package/vendor/tdai/core/store/tcvdb.js +1047 -1047
  46. package/vendor/tdai/core/store/types.js +17 -17
  47. package/vendor/tdai/core/tools/conversation-search.js +199 -199
  48. package/vendor/tdai/core/tools/memory-search.js +207 -207
  49. package/vendor/tdai/core/types.js +12 -12
  50. package/vendor/tdai/utils/backup.js +189 -189
  51. package/vendor/tdai/utils/checkpoint.js +360 -360
  52. package/vendor/tdai/utils/clean-context-runner.js +472 -472
  53. package/vendor/tdai/utils/ensure-hook-policy.js +213 -213
  54. package/vendor/tdai/utils/env.js +13 -13
  55. package/vendor/tdai/utils/managed-timer.js +127 -127
  56. package/vendor/tdai/utils/manifest.js +91 -91
  57. package/vendor/tdai/utils/memory-cleaner.js +300 -300
  58. package/vendor/tdai/utils/no-think-fetch.js +109 -109
  59. package/vendor/tdai/utils/openclaw-state-dir.js +26 -26
  60. package/vendor/tdai/utils/pipeline-factory.js +517 -517
  61. package/vendor/tdai/utils/pipeline-manager.js +892 -892
  62. package/vendor/tdai/utils/sanitize.js +360 -360
  63. package/vendor/tdai/utils/serial-queue.js +103 -103
  64. package/vendor/tdai/utils/session-filter.js +88 -88
  65. package/vendor/tdai/utils/text-utils.js +29 -29
  66. 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
@@ -15,22 +15,22 @@ Part of the [DeepSeek Harness Enhancement Suite](https://github.com/Scorp1o117/d
15
15
  A port of **TencentDB Agent Memory** (Tencent Cloud's open-source four-layer
16
16
  memory system, originally an OpenClaw plugin) into DeepSeek Harness.
17
17
 
18
- ## Compatibility (v0.3.6)
19
-
20
- Verified with DSH `0.1.7-rc.2` (Web) and `0.2.0-rc.2` (Desktop runtime) in isolated profiles. The Desktop app uses its own `desktop` profile. Other DSH prereleases remain unverified.
21
-
22
- ## Desktop install
23
-
24
- Use the Desktop-installed `dsh` command (Application → Manage dsh Command), or the app’s Plugins page. Then install into the Desktop profile:
25
-
26
- ```powershell
27
- dsh plugin --profile desktop add dsh-tdai-memory@0.3.6
28
- ```
29
-
30
- Restart the Desktop app to load the client bundle. Desktop keeps its profile under `$DSH_HOME/profiles/desktop`.
31
-
32
-
33
- ## Features
18
+ ## Compatibility (v0.4.1)
19
+
20
+ Verified with DSH `0.1.7-rc.2` (Web) and `0.2.0-rc.2` (Desktop runtime) in isolated profiles. The Desktop app uses its own `desktop` profile. Other DSH prereleases remain unverified.
21
+
22
+ ## Desktop install
23
+
24
+ Use the Desktop-installed `dsh` command (Application → Manage dsh Command), or the app’s Plugins page. Then install into the Desktop profile:
25
+
26
+ ```powershell
27
+ dsh plugin --profile desktop add dsh-tdai-memory@0.3.6
28
+ ```
29
+
30
+ Restart the Desktop app to load the client bundle. Desktop keeps its profile under `$DSH_HOME/profiles/desktop`.
31
+
32
+
33
+ ## Features
34
34
 
35
35
  - **L0 conversation capture**: every turn (turn end, request boundary) is
36
36
  written to raw conversation storage (JSONL + SQLite + FTS + vectors)
@@ -69,14 +69,14 @@ Hard-won wiring details:
69
69
 
70
70
  ## Configuration (profile patch + settings)
71
71
 
72
- Configuration is stored in the `tdai-memory` entry of the active Profile patch.
73
- On first launch, DSH imports the old `$DSH_HOME/settings.yaml` values into that
74
- entry. The **Web UI Settings → 记忆**
72
+ Configuration is stored in the `tdai-memory` entry of the active Profile patch.
73
+ On first launch, DSH imports the old `$DSH_HOME/settings.yaml` values into that
74
+ entry. The **Web UI Settings → 记忆**
75
75
  section edits every field (v0.2.0, write-only keys); TdaiCore is built at
76
76
  startup, so changes apply **after a restart**.
77
77
 
78
- ```yaml
79
- # $DSH_HOME/profiles/web/cordis.patch.yml
78
+ ```yaml
79
+ # $DSH_HOME/profiles/web/cordis.patch.yml
80
80
  - id: tdai-memory
81
81
  name: 'dsh-tdai-memory'
82
82
  config:
@@ -107,11 +107,11 @@ then mount it in `$DSH_HOME/profiles/web/cordis.patch.yml`:
107
107
  - insert:
108
108
  - id: tdai-memory
109
109
  name: 'dsh-tdai-memory'
110
- config: {} # fill through Settings → 记忆
110
+ config: {} # fill through Settings → 记忆
111
111
  ```
112
112
 
113
- and restart `dsh web`. LLM/embedding API keys can be set in the Web UI
114
- settings page (记忆 / Memory) or in the `tdai-memory` Profile patch entry.
113
+ and restart `dsh web`. LLM/embedding API keys can be set in the Web UI
114
+ settings page (记忆 / Memory) or in the `tdai-memory` Profile patch entry.
115
115
 
116
116
  > **Note for users**
117
117
  > - This plugin is a standard **profile bundle** (`dsh.bundle.patch`):
@@ -140,6 +140,14 @@ not need it. Users who select the local backend should install and approve
140
140
  stored=0); off by default; enable only with a more reliable model
141
141
  - **L1 memory vectors**: written with storage (8088 embedding is fast); L0
142
142
  vectors run as a background task, drained by `destroy()` on headless exit
143
+ - **Search tools** (`toolsEnabled`, on by default): the switch lives in the
144
+ Plugins page and applies in place — the row config is `volatile`, so the
145
+ plugin re-syncs its registration without a restart. If both tools are
146
+ missing, look for the plugin's
147
+ `[tdai-memory] tools registered: tdai_memory_search, tdai_conversation_search`
148
+ log line (v0.4.1+). Before v0.4.1 it read `config.toolsEnabled` off the
149
+ Volatile wrapper cordis hands to `apply` — a property that never exists — so
150
+ the failure was completely silent (issue #3)
143
151
  - **Upgrades**: after pulling new upstream code, rerun
144
152
  `npx tsc -p dsh-tsconfig.json` in the tdai project dir (output in `dist-dsh/`)
145
153
 
@@ -147,5 +155,5 @@ not need it. Users who select the local backend should install and approve
147
155
 
148
156
  MIT
149
157
 
150
-
151
- Saves use the configuration form API. Rejected writes retain the draft and show an error. Changing a model preserves credentials, and disabling a default-on option explicitly stores `false`.
158
+
159
+ Saves use the configuration form API. Rejected writes retain the draft and show an error. Changing a model preserves credentials, and disabling a default-on option explicitly stores `false`.
package/README.zh.md CHANGED
@@ -12,22 +12,22 @@
12
12
 
13
13
  把 **TencentDB Agent Memory**(腾讯云开源的四层记忆系统,原为 OpenClaw 插件)移植进 DeepSeek Harness。
14
14
 
15
- ## 兼容性(v0.3.6)
16
-
17
- 已在 DSH `0.1.7-rc.2`(Web)与 `0.2.0-rc.2`(桌面端运行时)的一次性 Profile 验证。桌面端使用独立的 `desktop` Profile;其他预发布版本暂未验证。
18
-
19
- ## 桌面端安装
20
-
21
- 在桌面端的“插件”页面安装,或使用桌面端“应用 → 管理 dsh 命令”注册的命令:
22
-
23
- ```powershell
24
- dsh plugin --profile desktop add dsh-tdai-memory@0.3.6
25
- ```
26
-
27
- 重启桌面端以加载客户端插件。配置位于 `$DSH_HOME/profiles/desktop`。
28
-
29
-
30
- ## 能力
15
+ ## 兼容性(v0.4.1)
16
+
17
+ 已在 DSH `0.1.7-rc.2`(Web)与 `0.2.0-rc.2`(桌面端运行时)的一次性 Profile 验证。桌面端使用独立的 `desktop` Profile;其他预发布版本暂未验证。
18
+
19
+ ## 桌面端安装
20
+
21
+ 在桌面端的“插件”页面安装,或使用桌面端“应用 → 管理 dsh 命令”注册的命令:
22
+
23
+ ```powershell
24
+ dsh plugin --profile desktop add dsh-tdai-memory@0.3.6
25
+ ```
26
+
27
+ 重启桌面端以加载客户端插件。配置位于 `$DSH_HOME/profiles/desktop`。
28
+
29
+
30
+ ## 能力
31
31
 
32
32
  - **L0 对话捕获**:每轮对话(turn 结束、请求边界)自动写入原始对话(JSONL + SQLite + FTS + 向量)
33
33
  - **L1 结构化记忆**:后台管线用 LLM 从对话中提取事实/偏好/事件(persona / episodic / instruction),写入 `records/` + SQLite + FTS + 向量
@@ -96,7 +96,8 @@ tdai-memory:
96
96
  - **提取模型**:`mimo-v2.5` 提取正确但单次 20-30s(后台执行,不阻塞对话);`deepseek-v4-flash` 快但 JSON 输出不合规(提取 0 条)
97
97
  - **dedup**:LLM 冲突检测输出解析不稳(曾导致 stored=0),默认关闭;开启需换更稳的模型
98
98
  - **L1 记忆向量**:随存储写入(8088 embedding 快);L0 向量走后台任务,headless 退出时由 `destroy()` drain
99
+ - **搜索工具**(`toolsEnabled`,默认开启):开关在插件页,切换后**原地生效**(行配置是 `volatile`,无需重启即重新注册)。若两个工具完全缺失,可查插件日志 `[tdai-memory] tools registered: …`(v0.4.1 起);v0.4.1 之前它读的是 cordis 交给 `apply` 的 Volatile 包装对象上的 `config.toolsEnabled` —— 那个属性永远不存在,因此整个失败过程完全静默(issue #3)
99
100
  - **升级**:上游拉新代码后,在 tdai 项目目录重跑 `npx tsc -p dsh-tsconfig.json`(产物在 `dist-dsh/`)
100
101
 
101
-
102
- 保存使用配置表单 API;宿主拒绝写入时保留草稿并提示错误。只修改模型不会覆盖密钥,关闭默认开启的选项会显式保存 `false`。
102
+
103
+ 保存使用配置表单 API;宿主拒绝写入时保留草稿并提示错误。只修改模型不会覆盖密钥,关闭默认开启的选项会显式保存 `false`。
@@ -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/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
@@ -37,7 +37,7 @@ const Config = z.object({
37
37
  /** TDAI data directory; default: ~/.memory-tencentdb/memory-tdai. */
38
38
  dataDir: z.string().default(""),
39
39
  /** LLM used for L1/L2/L3 extraction (OpenAI-compatible). Falls back to TDAI_LLM_* env. */
40
- llm: z.object({
40
+ llm: z.object({
41
41
  baseUrl: z.string().default(""),
42
42
  apiKey: z.string().default("").role("secret"),
43
43
  model: z.string().default(""),
@@ -52,34 +52,34 @@ const Config = z.object({
52
52
  sessionHeaderName: z.string().default("x-opencode-session"),
53
53
  /** Fixed session id for LLM requests; empty = persistent auto id. */
54
54
  sessionId: z.string().default(""),
55
- }).default({}),
55
+ }).default({}),
56
56
  /** Embedding endpoint (OpenAI-compatible /v1/embeddings). */
57
- embedding: z.object({
57
+ embedding: z.object({
58
58
  baseUrl: z.string().default("http://127.0.0.1:8088/v1"),
59
59
  apiKey: z.string().default("").role("secret"),
60
60
  model: z.string().default("Qwen3-Embedding-0.6B"),
61
61
  dimensions: z.number().default(1024),
62
62
  sendDimensions: z.boolean().default(false),
63
- }).default({}),
63
+ }).default({}),
64
64
  /** Capture conversations into L0. */
65
65
  captureEnabled: z.boolean().default(true),
66
66
  /** L1 extraction pipeline (facts from conversations). */
67
- extraction: z.object({
67
+ extraction: z.object({
68
68
  enabled: z.boolean().default(true),
69
69
  /** Conflict detection before storing; extra LLM call, disable if flaky.
70
70
  * Off by default: dedup LLM output parsing is flaky (README matches). */
71
71
  enableDedup: z.boolean().default(false),
72
- }).default({}),
72
+ }).default({}),
73
73
  /** Recall injection settings. */
74
- recall: z.object({
74
+ recall: z.object({
75
75
  enabled: z.boolean().default(true),
76
76
  maxResults: z.number().default(5),
77
77
  scoreThreshold: z.number().default(0.3),
78
78
  timeoutMs: z.number().default(3000),
79
- }).default({}),
79
+ }).default({}),
80
80
  /** Register the two search tools. */
81
81
  toolsEnabled: z.boolean().default(true),
82
- }).volatile();
82
+ }).volatile();
83
83
 
84
84
  /**
85
85
  * Whether a derived message is real human input.
@@ -106,18 +106,31 @@ function extractText(content) {
106
106
  .trim();
107
107
  }
108
108
 
109
+ /**
110
+ * Resolve the config value handed to `apply`.
111
+ *
112
+ * The row schema ends in `.volatile()` (hot-reloadable config). Config
113
+ * validation runs through the standard-schema path, which returns the
114
+ * validated object wrapped in a cosmokit `Volatile`: a frozen `{ get() }`
115
+ * reference whose OWN properties are always `undefined`. Reading
116
+ * `config.toolsEnabled` off it can therefore never be true — the tools guard
117
+ * did exactly that and silently stopped registering the search tools in
118
+ * v0.3.4 (issue #3). Read config through this helper only.
119
+ *
120
+ * The wrapper is a live reference (`updateVolatile` swaps its inner value), so
121
+ * calling this at use time — not once at startup — sees volatile edits.
122
+ */
123
+ function runtimeConfigOf(config) {
124
+ return typeof config?.get === "function" ? config.get() : config;
125
+ }
126
+
109
127
  function apply(ctx, config) {
110
128
  // ── build-once state (rebuildable) ───────────────────────────────────────
111
- // TdaiCore is built from the RESOLVED settings value (composition entry as
112
- // base, `tdai-memory` settings section / settings.yaml as user layer), so
113
- // keys stored in settings.yaml (llm.apiKey etc.) are honored. The settings
114
- // inject callback that drives `installSettingsSection`'s setSource is
115
- // scheduled by cordis as a plugin start — it ALWAYS runs after any 0ms
116
- // timer — so the no-settings fallback below cannot be allowed to win the
117
- // race. Instead of racing, we let the fallback build from the entry config
118
- // first (guaranteeing a working core even without settings), and REBUILD
119
- // from the resolved settings when the settings mount arrives. The fallback
120
- // is a one-shot; the settings build always wins afterwards.
129
+ // TdaiCore is built once from the resolved row config: the composition entry
130
+ // as base with the Plugins-page values merged in by the host (settings.yaml is
131
+ // no longer a config source since v0.4.0 — see client.js `configForms`). The
132
+ // build is deferred past the 0ms window so web/headless boots are never
133
+ // blocked on TdaiCore's stores and embedding client.
121
134
  let core = null;
122
135
  let tdaiConfig = null;
123
136
  let dataDir = "";
@@ -339,69 +352,137 @@ function apply(ctx, config) {
339
352
  }
340
353
 
341
354
  // ── tools ────────────────────────────────────────────────────────────────
342
- if (config.toolsEnabled) {
343
- ctx.tools.register(defineTool({
344
- name: "tdai_memory_search",
345
- description:
346
- "Search structured long-term memories (facts, preferences, and experiences extracted from past conversations). " +
347
- "Use this to recall what the user told you or what happened in earlier sessions.",
348
- parameters: {
349
- query: { type: "string", required: true, description: "The memory to search for." },
350
- limit: { type: "integer", description: "Maximum results (default 5)." },
351
- },
352
- output: {
353
- schema: { type: "string" },
354
- render: (_args, value) => [{ type: "text", text: value }],
355
- },
356
- async execute(args) {
357
- const result = await core.searchMemories({ query: args.query, limit: args.limit ?? 5 });
358
- return result.text;
359
- },
360
- }));
361
- ctx.tools.register(defineTool({
362
- name: "tdai_conversation_search",
363
- description:
364
- "Search raw past conversation logs (L0) by keyword. Use this to look up exact quotes or details " +
365
- "that structured memory search does not cover.",
366
- parameters: {
367
- query: { type: "string", required: true, description: "Text to find in past conversations." },
368
- limit: { type: "integer", description: "Maximum results (default 5)." },
369
- },
370
- output: {
371
- schema: { type: "string" },
372
- render: (_args, value) => [{ type: "text", text: value }],
373
- },
374
- async execute(args) {
375
- const result = await core.searchConversations({ query: args.query, limit: args.limit ?? 5 });
376
- return result.text;
377
- },
378
- }));
355
+ // `toolsEnabled` defaults to true (schema `.default(true)`), so only an
356
+ // explicit `false` turns the two search tools off. Read through
357
+ // `runtimeConfigOf` — the volatile row config has no plain properties (#3).
358
+ let toolsRegistered = false;
359
+ let toolsDeferred = false;
360
+ let toolsDisposers = [];
361
+
362
+ /** The core, or a clear error while the first build is still in flight. */
363
+ function requireCore() {
364
+ if (!core) throw new Error("tdai-memory is still initializing; retry this call in a moment");
365
+ return core;
366
+ }
367
+
368
+ function registerTools(tools) {
369
+ if (toolsRegistered) return;
370
+ if (typeof tools?.register !== "function") {
371
+ ctx.logger.warn("[tdai-memory] tools service does not expose register(); search tools stay unavailable");
372
+ return;
373
+ }
374
+ toolsRegistered = true;
375
+ toolsDisposers = [
376
+ tools.register(defineTool({
377
+ name: "tdai_memory_search",
378
+ description:
379
+ "Search structured long-term memories (facts, preferences, and experiences extracted from past conversations). " +
380
+ "Use this to recall what the user told you or what happened in earlier sessions.",
381
+ parameters: {
382
+ query: { type: "string", required: true, description: "The memory to search for." },
383
+ limit: { type: "integer", description: "Maximum results (default 5)." },
384
+ },
385
+ output: {
386
+ schema: { type: "string" },
387
+ render: (_args, value) => [{ type: "text", text: value }],
388
+ },
389
+ async execute(args) {
390
+ const result = await requireCore().searchMemories({ query: args.query, limit: args.limit ?? 5 });
391
+ return result.text;
392
+ },
393
+ })),
394
+ tools.register(defineTool({
395
+ name: "tdai_conversation_search",
396
+ description:
397
+ "Search raw past conversation logs (L0) by keyword. Use this to look up exact quotes or details " +
398
+ "that structured memory search does not cover.",
399
+ parameters: {
400
+ query: { type: "string", required: true, description: "Text to find in past conversations." },
401
+ limit: { type: "integer", description: "Maximum results (default 5)." },
402
+ },
403
+ output: {
404
+ schema: { type: "string" },
405
+ render: (_args, value) => [{ type: "text", text: value }],
406
+ },
407
+ async execute(args) {
408
+ const result = await requireCore().searchConversations({ query: args.query, limit: args.limit ?? 5 });
409
+ return result.text;
410
+ },
411
+ })),
412
+ ];
413
+ ctx.logger.info("[tdai-memory] tools registered: tdai_memory_search, tdai_conversation_search");
414
+ }
415
+
416
+ function unregisterTools() {
417
+ for (const dispose of toolsDisposers.splice(0)) {
418
+ try {
419
+ dispose();
420
+ } catch (error) {
421
+ ctx.logger.warn(`[tdai-memory] tool dispose failed: ${String(error)}`);
422
+ }
423
+ }
424
+ toolsRegistered = false;
425
+ ctx.logger.info("[tdai-memory] tools unregistered (toolsEnabled=false)");
379
426
  }
380
427
 
381
- // ── settings-backed configuration ─────────────────────────────────────────
382
- // The composition entry stays the base layer; a registered `tdai-memory`
383
- // settings section overlays it (settings.yaml). The source is a GETTER
384
- // (`() => scope.get()`), not the config object — we must call it to obtain
385
- // the resolved settings. The settings mount (cordis inject) always arrives
386
- // after our fallback timer, so the fallback builds the core from the entry
387
- // config first (one-shot) and the settings build REBUILDS the core with the
388
- // resolved values (apiKey included).
389
- // Compat shim: dsh-settings 0.1.2-rc.1 removed the module-level
390
- // `installSettingsSection` export (the provider now lives at ctx.settings).
391
- // Inline the same logic via ctx.inject(["settings"]) — works on both
392
- // 0.1.1 (module export wrapper) and 0.1.2 (ctx.settings) hosts.
393
- ctx.on("settings/document-updated", (id) => {
394
- if (id === NS) ctx.logger.warn("[tdai-memory] settings updated; restart to apply (TdaiCore is built at startup)");
395
- });
428
+ /** Align tool registration with the live `toolsEnabled` value. */
429
+ function syncTools() {
430
+ if (runtimeConfigOf(config)?.toolsEnabled === false) {
431
+ if (toolsRegistered) unregisterTools();
432
+ return;
433
+ }
434
+ if (toolsRegistered || toolsDeferred) return;
435
+ // `tools` is in this plugin's `inject`, so the service is normally live
436
+ // here; `ctx.inject` is the fallback for hosts that mount it later. The old
437
+ // one-shot `ctx.tools` property read registered nothing and said nothing.
438
+ const tools = ctx.tools ?? ctx.get?.("tools");
439
+ if (tools === undefined) {
440
+ toolsDeferred = true;
441
+ ctx.logger.warn("[tdai-memory] tools service not ready; deferring search-tool registration");
442
+ ctx.inject(["tools"], (toolCtx) => {
443
+ toolsDeferred = false;
444
+ const deferred = toolCtx.tools ?? toolCtx.get?.("tools");
445
+ if (deferred === undefined) {
446
+ ctx.logger.warn("[tdai-memory] tools service still unavailable; search tools stay unavailable");
447
+ return;
448
+ }
449
+ registerTools(deferred);
450
+ });
451
+ return;
452
+ }
453
+ registerTools(tools);
454
+ }
455
+
456
+ // `toolsEnabled` is a volatile config path, so flipping it (Plugins page)
457
+ // updates the running fiber in place instead of remounting this plugin: the
458
+ // loader reports that as `loader/volatile-update`, and this keeps the
459
+ // registration in sync with the new value.
460
+ ctx.on("loader/volatile-update", () => {
461
+ try {
462
+ syncTools();
463
+ } catch (error) {
464
+ ctx.logger.warn(`[tdai-memory] tool sync failed: ${String(error)}`);
465
+ }
466
+ });
467
+
468
+ syncTools();
469
+
470
+ // ── configuration changes ─────────────────────────────────────────────────
471
+ // Configuration lives in the Plugins page (client.js → configForms), i.e. in
472
+ // this row's own config. Because the row schema is `.volatile()`, an edit is
473
+ // applied to the running fiber in place — the loader reports it as
474
+ // `loader/volatile-update` (the tools above re-sync on it) — while TdaiCore
475
+ // is built at startup, so an edit that must reach the core needs a plugin
476
+ // reload. Legacy settings documents are only reported here.
477
+ ctx.on("settings/document-updated", (id) => {
478
+ if (id === NS) ctx.logger.warn("[tdai-memory] settings updated; restart to apply (TdaiCore is built at startup)");
479
+ });
396
480
 
397
- // Fallback when no settings service ever mounts: build from the entry
398
- // config. One-shot — if the settings mount arrives later, its build
399
- // supersedes this one. Delayed past the 0ms window so headless/web boots
400
- // are never blocked on this timer.
481
+ // Deferred first build (see the note at the top of apply).
401
482
  setTimeout(() => {
402
483
  if (!builtOnce) {
403
484
  builtOnce = true;
404
- void build(typeof config.get === "function" ? config.get() : config);
485
+ void build(runtimeConfigOf(config));
405
486
  }
406
487
  }, 500);
407
488