dsh-tdai-memory 0.2.14 → 0.3.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.
- package/README.md +15 -13
- package/README.zh.md +9 -7
- package/client.js +13 -0
- package/index.js +36 -14
- package/package.json +77 -78
- package/vendor/tdai/adapters/standalone/llm-runner.js +34 -6
- package/vendor/tdai/config.js +3 -0
- package/vendor/tdai/core/seed/seed-runtime.js +4 -0
- package/vendor/tdai/core/tdai-core.js +4 -0
- package/vendor/tdai/utils/session-header-fetch.js +88 -0
package/README.md
CHANGED
|
@@ -76,6 +76,8 @@ tdai-memory:
|
|
|
76
76
|
llm: # L1/L2/L3 extraction model (OpenAI-compatible)
|
|
77
77
|
baseUrl: 'https://opencode.ai/zen/go/v1'
|
|
78
78
|
model: 'mimo-v2.5' # deepseek-v4-flash produces invalid extraction JSON
|
|
79
|
+
sendSessionHeader: true # send x-opencode-session on LLM requests (required by OpenCode Go & similar gateways)
|
|
80
|
+
sessionId: '' # fixed session id; empty = persistent auto id under the data dir
|
|
79
81
|
embedding: # vectors (OpenAI-compatible /v1/embeddings)
|
|
80
82
|
baseUrl: 'http://127.0.0.1:8088/v1'
|
|
81
83
|
model: 'Qwen3-Embedding-0.6B'
|
|
@@ -106,19 +108,19 @@ settings page (记忆 / Memory) or directly in `settings.yaml` under
|
|
|
106
108
|
> - This plugin is a standard **profile bundle** (`dsh.bundle.patch`):
|
|
107
109
|
> `dsh plugin --profile web add dsh-tdai-memory` installs and mounts it in
|
|
108
110
|
> one step — no manual `cordis.patch.yml` edits needed.
|
|
109
|
-
> - DSH exposes the registered `tdai-memory` settings namespace directly; the
|
|
110
|
-
> plugin does not modify files in the host installation.
|
|
111
|
-
> - Settings changes apply **after a restart** (TdaiCore is built at startup).
|
|
112
|
-
> - Version 0.2.13 and newer require DSH `0.1.0-rc.7` or newer and are tested
|
|
113
|
-
> against `0.1.0-rc.7`, `0.1.0-rc.8`, and `0.1.1-rc.1`.
|
|
114
|
-
> - DSH `0.1.0-rc.6` users must pin `dsh-tdai-memory@0.2.11`, the last release
|
|
115
|
-
> carrying the legacy settings-allowlist compatibility patch.
|
|
116
|
-
|
|
117
|
-
`node-llama-cpp` is an optional peer used only by the fully local embedding
|
|
118
|
-
backend. It is intentionally not installed by default because its native build
|
|
119
|
-
requires explicit pnpm build approval. Remote OpenAI-compatible embeddings do
|
|
120
|
-
not need it. Users who select the local backend should install and approve
|
|
121
|
-
`node-llama-cpp` in the target DSH profile separately.
|
|
111
|
+
> - DSH exposes the registered `tdai-memory` settings namespace directly; the
|
|
112
|
+
> plugin does not modify files in the host installation.
|
|
113
|
+
> - Settings changes apply **after a restart** (TdaiCore is built at startup).
|
|
114
|
+
> - Version 0.2.13 and newer require DSH `0.1.0-rc.7` or newer and are tested
|
|
115
|
+
> against `0.1.0-rc.7`, `0.1.0-rc.8`, and `0.1.1-rc.1`.
|
|
116
|
+
> - DSH `0.1.0-rc.6` users must pin `dsh-tdai-memory@0.2.11`, the last release
|
|
117
|
+
> carrying the legacy settings-allowlist compatibility patch.
|
|
118
|
+
|
|
119
|
+
`node-llama-cpp` is an optional peer used only by the fully local embedding
|
|
120
|
+
backend. It is intentionally not installed by default because its native build
|
|
121
|
+
requires explicit pnpm build approval. Remote OpenAI-compatible embeddings do
|
|
122
|
+
not need it. Users who select the local backend should install and approve
|
|
123
|
+
`node-llama-cpp` in the target DSH profile separately.
|
|
122
124
|
|
|
123
125
|
## Known trade-offs
|
|
124
126
|
|
package/README.zh.md
CHANGED
|
@@ -16,13 +16,13 @@
|
|
|
16
16
|
- **自动召回注入**:每次组装提示词时,按当前用户消息检索相关记忆 + 画像,作为动态上下文注入(模型"凭空想起")
|
|
17
17
|
- **工具**:`tdai_memory_search`(L1 结构化检索)、`tdai_conversation_search`(L0 原文检索)
|
|
18
18
|
|
|
19
|
-
数据目录复用现有 `~/.memory-tencentdb/memory-tdai`,**旧记忆无缝继承**。
|
|
20
|
-
|
|
21
|
-
`node-llama-cpp` 仅供完全本地的 embedding 后端使用,现在声明为可选 peer,默认安装不会再触发原生构建脚本。使用 OpenAI 兼容远程 embedding 时不需要它;选择本地后端的用户需在目标 DSH profile 中单独安装并批准 `node-llama-cpp` 的构建。
|
|
22
|
-
|
|
23
|
-
从 `0.2.13` 起最低支持 DSH `0.1.0-rc.7`,已针对 `0.1.0-rc.7`、
|
|
24
|
-
`0.1.0-rc.8` 和 `0.1.1-rc.1` 测试。仍使用 DSH `0.1.0-rc.6` 的用户请锁定
|
|
25
|
-
`dsh-tdai-memory@0.2.11`;这是最后一个包含旧 settings 白名单兼容补丁的版本。
|
|
19
|
+
数据目录复用现有 `~/.memory-tencentdb/memory-tdai`,**旧记忆无缝继承**。
|
|
20
|
+
|
|
21
|
+
`node-llama-cpp` 仅供完全本地的 embedding 后端使用,现在声明为可选 peer,默认安装不会再触发原生构建脚本。使用 OpenAI 兼容远程 embedding 时不需要它;选择本地后端的用户需在目标 DSH profile 中单独安装并批准 `node-llama-cpp` 的构建。
|
|
22
|
+
|
|
23
|
+
从 `0.2.13` 起最低支持 DSH `0.1.0-rc.7`,已针对 `0.1.0-rc.7`、
|
|
24
|
+
`0.1.0-rc.8` 和 `0.1.1-rc.1` 测试。仍使用 DSH `0.1.0-rc.6` 的用户请锁定
|
|
25
|
+
`dsh-tdai-memory@0.2.11`;这是最后一个包含旧 settings 白名单兼容补丁的版本。
|
|
26
26
|
|
|
27
27
|
## 架构(移植方式)
|
|
28
28
|
|
|
@@ -63,6 +63,8 @@ tdai-memory:
|
|
|
63
63
|
llm: # L1/L2/L3 提取模型(OpenAI 兼容)
|
|
64
64
|
baseUrl: 'https://opencode.ai/zen/go/v1'
|
|
65
65
|
model: 'mimo-v2.5' # deepseek-v4-flash 对提取 JSON 输出不合格
|
|
66
|
+
sendSessionHeader: true # 给 LLM 请求带 x-opencode-session(OpenCode Go 等网关要求)
|
|
67
|
+
sessionId: '' # 固定会话 ID;留空 = 数据目录下自动持久化 ID(重启不变)
|
|
66
68
|
embedding: # 向量(OpenAI 兼容 /v1/embeddings)
|
|
67
69
|
baseUrl: 'http://127.0.0.1:8088/v1'
|
|
68
70
|
model: 'Qwen3-Embedding-0.6B'
|
package/client.js
CHANGED
|
@@ -64,6 +64,11 @@ window.__ModuleLoader__.load({
|
|
|
64
64
|
fieldLlmModel: "模型",
|
|
65
65
|
fieldLlmMaxTokens: "最大输出 Tokens",
|
|
66
66
|
fieldLlmTimeoutMs: "超时(毫秒)",
|
|
67
|
+
fieldLlmSendSessionHeader: "发送会话标识头(x-opencode-session)",
|
|
68
|
+
fieldLlmSessionHeaderName: "会话标识头名称",
|
|
69
|
+
fieldLlmSessionId: "固定会话 ID(留空 = 自动持久化)",
|
|
70
|
+
hintLlmSendSessionHeader: "OpenCode Go 等网关要求每个 LLM 请求带稳定的会话标识;请求不带可能报错(2026-09-06 起)。值:优先用固定 ID,否则在数据目录生成持久 ID(重启不变)。",
|
|
71
|
+
hintLlmSessionId: "后台抽取请求的固定会话标识;留空则自动生成并持久化到数据目录的 session-id 文件。",
|
|
67
72
|
fieldEmbeddingBaseUrl: "Base URL",
|
|
68
73
|
fieldEmbeddingApiKey: "API Key",
|
|
69
74
|
fieldEmbeddingModel: "模型",
|
|
@@ -103,6 +108,11 @@ window.__ModuleLoader__.load({
|
|
|
103
108
|
fieldLlmModel: "Model",
|
|
104
109
|
fieldLlmMaxTokens: "Max output tokens",
|
|
105
110
|
fieldLlmTimeoutMs: "Timeout (ms)",
|
|
111
|
+
fieldLlmSendSessionHeader: "Send session-id header (x-opencode-session)",
|
|
112
|
+
fieldLlmSessionHeaderName: "Session-id header name",
|
|
113
|
+
fieldLlmSessionId: "Fixed session id (empty = persistent auto)",
|
|
114
|
+
hintLlmSendSessionHeader: "OpenCode Go and similar gateways require a stable session id on every LLM request; requests without it may error (from 2026-09-06). Value: the fixed id below, else a persistent per-instance id stored under the data dir.",
|
|
115
|
+
hintLlmSessionId: "Fixed session id for background extraction requests; leave empty for a persistent auto id under the data dir (survives restarts).",
|
|
106
116
|
fieldEmbeddingBaseUrl: "Base URL",
|
|
107
117
|
fieldEmbeddingApiKey: "API Key",
|
|
108
118
|
fieldEmbeddingModel: "Model",
|
|
@@ -135,6 +145,9 @@ window.__ModuleLoader__.load({
|
|
|
135
145
|
{ path: ["llm", "model"], label: "fieldLlmModel", type: "text", group: "groupLlm" },
|
|
136
146
|
{ path: ["llm", "maxTokens"], label: "fieldLlmMaxTokens", type: "number", group: "groupLlm" },
|
|
137
147
|
{ path: ["llm", "timeoutMs"], label: "fieldLlmTimeoutMs", type: "number", group: "groupLlm" },
|
|
148
|
+
{ path: ["llm", "sendSessionHeader"], label: "fieldLlmSendSessionHeader", type: "checkbox", group: "groupLlm" },
|
|
149
|
+
{ path: ["llm", "sessionHeaderName"], label: "fieldLlmSessionHeaderName", type: "text", group: "groupLlm" },
|
|
150
|
+
{ path: ["llm", "sessionId"], label: "fieldLlmSessionId", type: "text", group: "groupLlm" },
|
|
138
151
|
{ path: ["embedding", "baseUrl"], label: "fieldEmbeddingBaseUrl", type: "text", group: "groupEmbedding" },
|
|
139
152
|
{ path: ["embedding", "apiKey"], label: "fieldEmbeddingApiKey", type: "password", secret: true, group: "groupEmbedding" },
|
|
140
153
|
{ path: ["embedding", "model"], label: "fieldEmbeddingModel", type: "text", group: "groupEmbedding" },
|
package/index.js
CHANGED
|
@@ -21,7 +21,6 @@ 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";
|
|
25
24
|
import { TdaiCore } from "./vendor/tdai/core/tdai-core.js";
|
|
26
25
|
import { parseConfig } from "./vendor/tdai/config.js";
|
|
27
26
|
import { StandaloneHostAdapter } from "./vendor/tdai/adapters/standalone/host-adapter.js";
|
|
@@ -31,7 +30,7 @@ const name = "tdai-memory";
|
|
|
31
30
|
/** Services this row injects. */
|
|
32
31
|
const inject = ["sessions", "systemPrompt", "tools"];
|
|
33
32
|
/** Settings namespace owned by this plugin (Web UI settings section). */
|
|
34
|
-
const NS =
|
|
33
|
+
const NS = "tdai-memory";
|
|
35
34
|
|
|
36
35
|
/** Runtime schema for the tdai-memory row. */
|
|
37
36
|
const Config = z.object({
|
|
@@ -44,6 +43,15 @@ const Config = z.object({
|
|
|
44
43
|
model: z.string().default(""),
|
|
45
44
|
maxTokens: z.number().default(4096),
|
|
46
45
|
timeoutMs: z.number().default(120000),
|
|
46
|
+
/** Send a stable session-id header on LLM requests. OpenCode Go and other
|
|
47
|
+
* gateways require `x-opencode-session` on every request; missing it may
|
|
48
|
+
* error after 2026-09-06. Value: `sessionId` below, else a persistent
|
|
49
|
+
* per-instance random id stored under the data dir. */
|
|
50
|
+
sendSessionHeader: z.boolean().default(true),
|
|
51
|
+
/** Header name carrying the session id. */
|
|
52
|
+
sessionHeaderName: z.string().default("x-opencode-session"),
|
|
53
|
+
/** Fixed session id for LLM requests; empty = persistent auto id. */
|
|
54
|
+
sessionId: z.string().default(""),
|
|
47
55
|
}),
|
|
48
56
|
/** Embedding endpoint (OpenAI-compatible /v1/embeddings). */
|
|
49
57
|
embedding: z.object({
|
|
@@ -183,6 +191,9 @@ function apply(ctx, config) {
|
|
|
183
191
|
model: cfg.llm.model || process.env.TDAI_LLM_MODEL || "deepseek-v4-flash",
|
|
184
192
|
maxTokens: cfg.llm.maxTokens,
|
|
185
193
|
timeoutMs: cfg.llm.timeoutMs,
|
|
194
|
+
sendSessionHeader: cfg.llm.sendSessionHeader,
|
|
195
|
+
sessionHeaderName: cfg.llm.sessionHeaderName,
|
|
196
|
+
sessionId: cfg.llm.sessionId,
|
|
186
197
|
},
|
|
187
198
|
});
|
|
188
199
|
const nextHostAdapter = new StandaloneHostAdapter({
|
|
@@ -193,6 +204,10 @@ function apply(ctx, config) {
|
|
|
193
204
|
model: nextConfig.llm.model,
|
|
194
205
|
maxTokens: nextConfig.llm.maxTokens,
|
|
195
206
|
timeoutMs: nextConfig.llm.timeoutMs,
|
|
207
|
+
sendSessionHeader: nextConfig.llm.sendSessionHeader,
|
|
208
|
+
sessionHeaderName: nextConfig.llm.sessionHeaderName,
|
|
209
|
+
sessionId: nextConfig.llm.sessionId,
|
|
210
|
+
dataDir,
|
|
196
211
|
},
|
|
197
212
|
logger,
|
|
198
213
|
defaultUserId: "dsh-user",
|
|
@@ -365,20 +380,27 @@ function apply(ctx, config) {
|
|
|
365
380
|
|
|
366
381
|
// ── settings-backed configuration ─────────────────────────────────────────
|
|
367
382
|
// The composition entry stays the base layer; a registered `tdai-memory`
|
|
368
|
-
// settings section overlays it (settings.yaml).
|
|
369
|
-
//
|
|
370
|
-
//
|
|
371
|
-
//
|
|
372
|
-
//
|
|
373
|
-
//
|
|
374
|
-
|
|
375
|
-
|
|
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.inject(["settings"], (sctx) => {
|
|
394
|
+
const scope = sctx.settings.register(NS, Config, { base: config });
|
|
395
|
+
const fire = () => {
|
|
376
396
|
builtOnce = true;
|
|
377
|
-
void build(
|
|
378
|
-
}
|
|
379
|
-
|
|
397
|
+
void build(scope.get());
|
|
398
|
+
};
|
|
399
|
+
sctx.effect(() => () => {});
|
|
400
|
+
fire();
|
|
401
|
+
scope.watch(() => {
|
|
380
402
|
ctx.logger.warn("[tdai-memory] settings updated; restart to apply (TdaiCore is built at startup)");
|
|
381
|
-
}
|
|
403
|
+
});
|
|
382
404
|
});
|
|
383
405
|
|
|
384
406
|
// Fallback when no settings service ever mounts: build from the entry
|
package/package.json
CHANGED
|
@@ -1,79 +1,78 @@
|
|
|
1
1
|
{
|
|
2
|
-
"name":
|
|
3
|
-
"version":
|
|
4
|
-
"description":
|
|
5
|
-
"type":
|
|
6
|
-
"main":
|
|
7
|
-
"exports":
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
"dsh":
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
"homepage":
|
|
21
|
-
"files":
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
"keywords":
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
"license":
|
|
44
|
-
"engines":
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
"repository":
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
"bugs":
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
"dependencies":
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
"peerDependencies":
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
"peerDependenciesMeta":
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
"scripts":
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
}
|
|
79
|
-
|
|
2
|
+
"name": "dsh-tdai-memory",
|
|
3
|
+
"version": "0.3.1",
|
|
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
|
+
"bundle": {
|
|
14
|
+
"patch": "./cordis.patch.yml"
|
|
15
|
+
},
|
|
16
|
+
"client": {
|
|
17
|
+
"platform": "web"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"homepage": "https://github.com/Scorp1o117/dsh-tdai-memory",
|
|
21
|
+
"files": [
|
|
22
|
+
"index.js",
|
|
23
|
+
"recall-inject.js",
|
|
24
|
+
"client.js",
|
|
25
|
+
"cordis.patch.yml",
|
|
26
|
+
"vendor",
|
|
27
|
+
"README.md",
|
|
28
|
+
"README.zh.md",
|
|
29
|
+
"LICENSE",
|
|
30
|
+
"THIRD-PARTY-NOTICES.md"
|
|
31
|
+
],
|
|
32
|
+
"keywords": [
|
|
33
|
+
"dsh",
|
|
34
|
+
"dsh-plugin",
|
|
35
|
+
"deepseek-harness",
|
|
36
|
+
"memory",
|
|
37
|
+
"tencentdb",
|
|
38
|
+
"agent-memory",
|
|
39
|
+
"rag",
|
|
40
|
+
"vector-search",
|
|
41
|
+
"persona"
|
|
42
|
+
],
|
|
43
|
+
"license": "MIT",
|
|
44
|
+
"engines": {
|
|
45
|
+
"node": ">=22.18"
|
|
46
|
+
},
|
|
47
|
+
"repository": {
|
|
48
|
+
"type": "git",
|
|
49
|
+
"url": "git+https://github.com/Scorp1o117/dsh-tdai-memory.git"
|
|
50
|
+
},
|
|
51
|
+
"bugs": {
|
|
52
|
+
"url": "https://github.com/Scorp1o117/dsh-tdai-memory/issues"
|
|
53
|
+
},
|
|
54
|
+
"dependencies": {
|
|
55
|
+
"@ai-sdk/openai": "^3.0.53",
|
|
56
|
+
"@deepseek-ai/schemastery": "^3.18.1",
|
|
57
|
+
"@node-rs/jieba": "^2.0.1",
|
|
58
|
+
"@tencentdb-agent-memory/tcvdb-text": "^0.1.1",
|
|
59
|
+
"ai": "^6.0.164",
|
|
60
|
+
"json5": "^2.2.3",
|
|
61
|
+
"sqlite-vec": "0.1.7-alpha.2",
|
|
62
|
+
"undici": "^8.1.0"
|
|
63
|
+
},
|
|
64
|
+
"peerDependencies": {
|
|
65
|
+
"react": "^18.2.0",
|
|
66
|
+
"@deepseek-ai/dsh-tools": "^0.1.0-rc.7 || ^0.1.1-rc.1 || ^0.1.2-rc.1 || ^0.1.5-rc.1",
|
|
67
|
+
"@deepseek-ai/dsh-settings": "^0.1.0-rc.7 || ^0.1.1-rc.1 || ^0.1.2-rc.1 || ^0.1.5-rc.1",
|
|
68
|
+
"node-llama-cpp": "^3.19.1"
|
|
69
|
+
},
|
|
70
|
+
"peerDependenciesMeta": {
|
|
71
|
+
"node-llama-cpp": {
|
|
72
|
+
"optional": true
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
"scripts": {
|
|
76
|
+
"test": "node tests/settings-scope-compat.test.mjs && node tests/install-contract.test.mjs"
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -21,6 +21,7 @@ import { generateText, tool, stepCountIs, jsonSchema } from "ai";
|
|
|
21
21
|
import { createOpenAI } from "@ai-sdk/openai";
|
|
22
22
|
import { report } from "../../core/report/reporter.js";
|
|
23
23
|
import { createNoThinkFetch } from "../../utils/no-think-fetch.js";
|
|
24
|
+
import { resolvePersistentSessionId, sessionHeaderOf } from "../../utils/session-header-fetch.js";
|
|
24
25
|
const TAG = "[memory-tdai] [standalone-runner]";
|
|
25
26
|
// Max iterations in the tool-call loop to prevent infinite loops
|
|
26
27
|
const MAX_TOOL_ITERATIONS = 20;
|
|
@@ -132,6 +133,7 @@ export class StandaloneLLMRunner {
|
|
|
132
133
|
enableTools;
|
|
133
134
|
logger;
|
|
134
135
|
customFetch;
|
|
136
|
+
sessionIdPromise;
|
|
135
137
|
constructor(opts) {
|
|
136
138
|
this.config = opts.config;
|
|
137
139
|
this.model = opts.model ?? opts.config.model;
|
|
@@ -140,6 +142,37 @@ export class StandaloneLLMRunner {
|
|
|
140
142
|
this.customFetch = opts.config.disableThinking
|
|
141
143
|
? createNoThinkFetch(opts.config.disableThinking)
|
|
142
144
|
: undefined;
|
|
145
|
+
// Resolve the persistent session id lazily (async, cached). Only needed
|
|
146
|
+
// when the session header is enabled and no fixed sessionId is set.
|
|
147
|
+
const { sendSessionHeader, sessionId, dataDir } = opts.config;
|
|
148
|
+
if (sendSessionHeader === false || (sessionId && String(sessionId).trim())) {
|
|
149
|
+
this.sessionIdPromise = Promise.resolve(String(sessionId ?? "").trim());
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
this.sessionIdPromise = resolvePersistentSessionId(dataDir);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
async buildProvider() {
|
|
156
|
+
const providerOptions = {
|
|
157
|
+
baseURL: this.config.baseUrl,
|
|
158
|
+
apiKey: this.config.apiKey,
|
|
159
|
+
compatibility: "compatible",
|
|
160
|
+
...(this.customFetch ? { fetch: this.customFetch } : {}),
|
|
161
|
+
};
|
|
162
|
+
// x-opencode-session: stable per-conversation id header.
|
|
163
|
+
if (this.config.sendSessionHeader !== false) {
|
|
164
|
+
const persistentId = await this.sessionIdPromise;
|
|
165
|
+
const header = sessionHeaderOf({
|
|
166
|
+
send: this.config.sendSessionHeader,
|
|
167
|
+
headerName: this.config.sessionHeaderName,
|
|
168
|
+
sessionId: this.config.sessionId,
|
|
169
|
+
persistentId,
|
|
170
|
+
});
|
|
171
|
+
if (header) {
|
|
172
|
+
providerOptions.headers = { ...(providerOptions.headers ?? {}), ...header };
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return createOpenAI(providerOptions);
|
|
143
176
|
}
|
|
144
177
|
async run(params) {
|
|
145
178
|
const runStartMs = Date.now();
|
|
@@ -151,12 +184,7 @@ export class StandaloneLLMRunner {
|
|
|
151
184
|
// Create OpenAI-compatible provider via AI SDK
|
|
152
185
|
// Use "compatible" mode to call /chat/completions (not Responses API),
|
|
153
186
|
// which works with all OpenAI-compatible backends (DeepSeek, Qwen, etc.)
|
|
154
|
-
const provider =
|
|
155
|
-
baseURL: this.config.baseUrl,
|
|
156
|
-
apiKey: this.config.apiKey,
|
|
157
|
-
compatibility: "compatible",
|
|
158
|
-
...(this.customFetch ? { fetch: this.customFetch } : {}),
|
|
159
|
-
});
|
|
187
|
+
const provider = await this.buildProvider();
|
|
160
188
|
// For pure text tasks like L1 extraction, avoid exposing any tools.
|
|
161
189
|
const tools = this.enableTools
|
|
162
190
|
? createSandboxedTools(workspaceDir, this.logger)
|
package/vendor/tdai/config.js
CHANGED
|
@@ -257,6 +257,9 @@ export function parseConfig(raw) {
|
|
|
257
257
|
maxTokens: num(llmGroup, "maxTokens") ?? 4096,
|
|
258
258
|
timeoutMs: num(llmGroup, "timeoutMs") ?? 120_000,
|
|
259
259
|
disableThinking: normalizeDisableThinking(boolOrStr(llmGroup, "disableThinking")),
|
|
260
|
+
sendSessionHeader: bool(llmGroup, "sendSessionHeader") ?? true,
|
|
261
|
+
sessionHeaderName: str(llmGroup, "sessionHeaderName") ?? "x-opencode-session",
|
|
262
|
+
sessionId: str(llmGroup, "sessionId") ?? "",
|
|
260
263
|
};
|
|
261
264
|
})(),
|
|
262
265
|
offload,
|
|
@@ -50,6 +50,10 @@ async function createSeedPipeline(opts) {
|
|
|
50
50
|
maxTokens: cfg.llm.maxTokens,
|
|
51
51
|
timeoutMs: cfg.llm.timeoutMs,
|
|
52
52
|
disableThinking: cfg.llm.disableThinking,
|
|
53
|
+
sendSessionHeader: cfg.llm.sendSessionHeader,
|
|
54
|
+
sessionHeaderName: cfg.llm.sessionHeaderName,
|
|
55
|
+
sessionId: cfg.llm.sessionId,
|
|
56
|
+
dataDir: outputDir,
|
|
53
57
|
},
|
|
54
58
|
logger,
|
|
55
59
|
});
|
|
@@ -359,6 +359,10 @@ export class TdaiCore {
|
|
|
359
359
|
maxTokens: this.cfg.llm.maxTokens,
|
|
360
360
|
timeoutMs: this.cfg.llm.timeoutMs,
|
|
361
361
|
disableThinking: this.cfg.llm.disableThinking,
|
|
362
|
+
sendSessionHeader: this.cfg.llm.sendSessionHeader,
|
|
363
|
+
sessionHeaderName: this.cfg.llm.sessionHeaderName,
|
|
364
|
+
sessionId: this.cfg.llm.sessionId,
|
|
365
|
+
dataDir: this.dataDir,
|
|
362
366
|
},
|
|
363
367
|
logger: this.logger,
|
|
364
368
|
});
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fetch wrapper that injects a stable session-id header (`x-opencode-session`)
|
|
3
|
+
* into outgoing LLM requests.
|
|
4
|
+
*
|
|
5
|
+
* OpenCode Go (and other OpenAI-compatible gateways) require every request to
|
|
6
|
+
* carry a stable per-conversation id; requests without it started erroring
|
|
7
|
+
* after 2026-09-06. The value is:
|
|
8
|
+
*
|
|
9
|
+
* 1. An explicit configured `sessionId`, when set;
|
|
10
|
+
* 2. Otherwise a persistent per-instance random id. The id is generated once
|
|
11
|
+
* and stored under the TDAI data dir (`<dataDir>/session-id`), so it
|
|
12
|
+
* survives restarts — one stable id per plugin instance, which is exactly
|
|
13
|
+
* what the gateway asks for when the client has no real conversation.
|
|
14
|
+
*
|
|
15
|
+
* Embedding and other non-chat traffic still gets the header (harmless, and
|
|
16
|
+
* gateways may require it on every request). When disabled, returns
|
|
17
|
+
* `globalThis.fetch` unchanged.
|
|
18
|
+
*/
|
|
19
|
+
import { randomUUID } from "node:crypto";
|
|
20
|
+
import { join } from "node:path";
|
|
21
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
22
|
+
|
|
23
|
+
const SESSION_ID_FILE = "session-id";
|
|
24
|
+
|
|
25
|
+
let cachedId = "";
|
|
26
|
+
/** Load-or-create a persistent per-instance session id under `dataDir`. */
|
|
27
|
+
export async function resolvePersistentSessionId(dataDir) {
|
|
28
|
+
if (cachedId)
|
|
29
|
+
return cachedId;
|
|
30
|
+
if (!dataDir) {
|
|
31
|
+
cachedId = `tdai-memory-${randomUUID()}`;
|
|
32
|
+
return cachedId;
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
const file = join(dataDir, SESSION_ID_FILE);
|
|
36
|
+
const existing = await readFile(file, "utf8").catch(() => "");
|
|
37
|
+
const trimmed = existing.trim();
|
|
38
|
+
if (trimmed) {
|
|
39
|
+
cachedId = trimmed;
|
|
40
|
+
return cachedId;
|
|
41
|
+
}
|
|
42
|
+
const fresh = `tdai-memory-${randomUUID()}`;
|
|
43
|
+
await mkdir(dataDir, { recursive: true });
|
|
44
|
+
await writeFile(file, fresh, "utf8");
|
|
45
|
+
cachedId = fresh;
|
|
46
|
+
return cachedId;
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
// Any persistence failure degrades to a per-process id.
|
|
50
|
+
cachedId = `tdai-memory-${randomUUID()}`;
|
|
51
|
+
return cachedId;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Build a session-id header value for a request.
|
|
57
|
+
* @param {object} opts { send, headerName, sessionId, persistentId }
|
|
58
|
+
* @returns {Record<string,string>|undefined} header object, or undefined when disabled.
|
|
59
|
+
*/
|
|
60
|
+
export function sessionHeaderOf({ send, headerName, sessionId, persistentId }) {
|
|
61
|
+
if (send === false)
|
|
62
|
+
return undefined;
|
|
63
|
+
const id = (sessionId && String(sessionId).trim()) || persistentId || "";
|
|
64
|
+
if (!id)
|
|
65
|
+
return undefined;
|
|
66
|
+
return { [headerName || "x-opencode-session"]: id };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Create a fetch wrapper that injects the session-id header into every request.
|
|
71
|
+
* @param {object} opts { send, headerName, sessionId, persistentId }
|
|
72
|
+
* @returns {typeof globalThis.fetch}
|
|
73
|
+
*/
|
|
74
|
+
export function createSessionHeaderFetch({ send, headerName, sessionId, persistentId }) {
|
|
75
|
+
if (send === false)
|
|
76
|
+
return globalThis.fetch;
|
|
77
|
+
const header = sessionHeaderOf({ send, headerName, sessionId, persistentId });
|
|
78
|
+
if (!header)
|
|
79
|
+
return globalThis.fetch;
|
|
80
|
+
return (async (input, init) => {
|
|
81
|
+
const next = init ? { ...init } : {};
|
|
82
|
+
next.headers = new Headers(init?.headers);
|
|
83
|
+
for (const [k, v] of Object.entries(header)) {
|
|
84
|
+
next.headers.set(k, v);
|
|
85
|
+
}
|
|
86
|
+
return globalThis.fetch(input, next);
|
|
87
|
+
});
|
|
88
|
+
}
|