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 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 = settingsNamespace("tdai-memory");
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). `installSettingsSection`
369
- // hands `setSource` a GETTER (`() => scope.get()`), not the config object —
370
- // we must call it to obtain the resolved settings. The settings mount
371
- // (cordis inject) always arrives after our fallback timer, so the fallback
372
- // builds the core from the entry config first (one-shot) and the settings
373
- // build REBUILDS the core with the resolved values (apiKey included).
374
- installSettingsSection(ctx, NS, Config, config, {
375
- setSource: (getter) => {
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(getter());
378
- },
379
- onChange: () => {
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": "dsh-tdai-memory",
3
- "version": "0.2.14",
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": "\u003e=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",
67
- "@deepseek-ai/dsh-settings": "^0.1.0-rc.7 || ^0.1.1-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
- }
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 = createOpenAI({
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)
@@ -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
+ }