dsh-tdai-memory 0.2.1 → 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/index.js CHANGED
@@ -84,74 +84,21 @@ function extractText(content) {
84
84
  }
85
85
 
86
86
  function apply(ctx, config) {
87
- // ── settings-backed configuration ─────────────────────────────────────────
88
- // TdaiCore is built once from the RESOLVED settings value (composition
89
- // entry as base, `tdai-memory` settings section / settings.yaml as user
90
- // layer), so keys stored in settings.yaml are honored. Changes apply after
91
- // a restart; the Web UI notes this. If no settings service ever mounts,
92
- // the composition entry builds the core unchanged.
93
- let started = false;
94
- const build = (cfg) => {
95
- if (started) return;
96
- started = true;
97
- const dataDir = cfg.dataDir || join(os.homedir(), ".memory-tencentdb", "memory-tdai");
98
-
99
- const tdaiConfig = parseConfig({
100
- timezone: "system",
101
- storeBackend: "sqlite",
102
- capture: { enabled: cfg.captureEnabled },
103
- extraction: { enabled: cfg.extraction.enabled, enableDedup: cfg.extraction.enableDedup },
104
- recall: { ...cfg.recall },
105
- embedding: {
106
- enabled: true,
107
- provider: "openai",
108
- baseUrl: cfg.embedding.baseUrl,
109
- apiKey: cfg.embedding.apiKey,
110
- model: cfg.embedding.model,
111
- dimensions: cfg.embedding.dimensions,
112
- sendDimensions: cfg.embedding.sendDimensions,
113
- timeoutMs: 10000,
114
- },
115
- llm: {
116
- enabled: true,
117
- baseUrl: cfg.llm.baseUrl || process.env.TDAI_LLM_BASE_URL || "",
118
- apiKey: cfg.llm.apiKey || process.env.TDAI_LLM_API_KEY || "",
119
- model: cfg.llm.model || process.env.TDAI_LLM_MODEL || "deepseek-v4-flash",
120
- maxTokens: cfg.llm.maxTokens,
121
- timeoutMs: cfg.llm.timeoutMs,
122
- },
123
- });
124
-
125
- // Verbose tdai-internal logging goes to the console only when explicitly
126
- // enabled; routine diagnostics use the dsh logger.
127
- const verbose = !!process.env.DSH_TDAI_DEBUG;
128
- const log = (level, m) => {
129
- if (verbose) console.error(`[tdai-memory] ${m}`);
130
- else if (level === "warn") ctx.logger.warn(`[tdai-memory] ${m}`);
131
- else if (level === "error") ctx.logger.error(`[tdai-memory] ${m}`);
132
- };
133
- const logger = {
134
- debug: (m) => log("debug", m),
135
- info: (m) => log("info", m),
136
- warn: (m) => log("warn", m),
137
- error: (m) => log("error", m),
138
- };
139
-
140
- const hostAdapter = new StandaloneHostAdapter({
141
- dataDir,
142
- llmConfig: {
143
- baseUrl: tdaiConfig.llm.baseUrl,
144
- apiKey: tdaiConfig.llm.apiKey,
145
- model: tdaiConfig.llm.model,
146
- maxTokens: tdaiConfig.llm.maxTokens,
147
- timeoutMs: tdaiConfig.llm.timeoutMs,
148
- },
149
- logger,
150
- defaultUserId: "dsh-user",
151
- platform: "dsh",
152
- });
153
-
154
- 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;
155
102
 
156
103
  /** turn id of the last turn/end per session (capture trigger). */
157
104
  const lastTurnEnd = new Map();
@@ -170,6 +117,82 @@ function apply(ctx, config) {
170
117
  // long-lived process runs the pipeline in the background instead.
171
118
  const isOneShot = process.argv.some((arg) => arg === "headless");
172
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
+
173
196
  // ── capture: triggered on session/flush (awaitable; runs before process
174
197
  // exit in headless, and at every request boundary in web) ─────────────
175
198
  ctx.on("session/event", (session, event) => {
@@ -190,7 +213,7 @@ function apply(ctx, config) {
190
213
  await captureTurn(session, startedAt);
191
214
  if (isOneShot) {
192
215
  // Run pending L1 extraction to completion before the process exits.
193
- await core.handleSessionEnd(session.id).catch((error) => {
216
+ await core?.handleSessionEnd(session.id).catch((error) => {
194
217
  ctx.logger.warn(`[tdai-memory] L1 flush failed: ${String(error)}`);
195
218
  });
196
219
  }
@@ -200,7 +223,7 @@ function apply(ctx, config) {
200
223
  });
201
224
 
202
225
  async function captureTurn(session, startedAt) {
203
- if (!tdaiConfig.capture.enabled) return;
226
+ if (!tdaiConfig?.capture.enabled) return;
204
227
  const derived = session.deriveMessages();
205
228
  const lastUserIdx = derived.map((m) => m.role).lastIndexOf("user");
206
229
  if (lastUserIdx < 0) return;
@@ -242,7 +265,7 @@ function apply(ctx, config) {
242
265
  if (!agent?.ctx) return;
243
266
  agent.ctx.on("system-prompt/assemble", async (assembly, context) => {
244
267
  try {
245
- if (!tdaiConfig.recall.enabled) return assembly;
268
+ if (!tdaiConfig?.recall.enabled) return assembly;
246
269
  const lastUser = [...session.deriveMessages()].reverse().find((m) => m.role === "user");
247
270
  if (!lastUser) return assembly;
248
271
  const text = extractText(lastUser.content);
@@ -319,43 +342,50 @@ function apply(ctx, config) {
319
342
 
320
343
  // ── settings-backed configuration ─────────────────────────────────────────
321
344
  // The composition entry stays the base layer; a registered `tdai-memory`
322
- // settings section overlays it (settings.yaml). TdaiCore is built once at
323
- // startup, so changes apply after a restart — the namespace registration is
324
- // what lets the Web UI surface and store them.
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).
325
351
  installSettingsSection(ctx, NS, Config, config, {
326
- setSource: (source) => {
327
- build(source);
352
+ setSource: (getter) => {
353
+ builtOnce = true;
354
+ void build(getter());
328
355
  },
329
356
  onChange: () => {
330
- if (started) ctx.logger.warn("[tdai-memory] settings updated; restart to apply (TdaiCore is built at startup)");
357
+ ctx.logger.warn("[tdai-memory] settings updated; restart to apply (TdaiCore is built at startup)");
331
358
  },
332
359
  });
333
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
+
334
372
  // ── lifecycle ────────────────────────────────────────────────────────────
335
- ctx.effect(async () => {
336
- await core.initialize();
337
- ctx.logger.info(`[tdai-memory] initialized (dataDir=${dataDir}, llm=${tdaiConfig.llm.model}, embedding=${tdaiConfig.embedding.model})`);
373
+ ctx.effect(() => {
338
374
  return async () => {
339
375
  // Final flush: run pending L1 extraction for every session seen this
340
376
  // process and wait for it to finish (headless exits right after the
341
377
  // tree disposes, so this is what makes extraction complete in one-shot
342
378
  // runs). Unknown keys are tolerated as no-ops.
343
379
  for (const sessionKey of seenSessions) {
344
- await core.handleSessionEnd(sessionKey).catch((error) => {
380
+ await core?.handleSessionEnd(sessionKey).catch((error) => {
345
381
  ctx.logger.warn(`[tdai-memory] final L1 flush failed for ${sessionKey}: ${String(error)}`);
346
382
  });
347
383
  }
348
- await core.destroy().catch((error) => {
384
+ await core?.destroy().catch((error) => {
349
385
  ctx.logger.warn(`[tdai-memory] destroy failed: ${String(error)}`);
350
386
  });
351
387
  };
352
- }, "tdai-memory.initialize()");
353
- }; // end build(cfg)
354
-
355
- // Fallback when no settings service ever mounts: build from the entry config.
356
- setTimeout(() => {
357
- if (!started) build(config);
358
- }, 0);
388
+ }, "tdai-memory.cleanup()");
359
389
  }
360
390
 
361
391
  export { Config, apply, inject, name };
package/package.json CHANGED
@@ -1,63 +1,62 @@
1
- {
2
- "name": "dsh-tdai-memory",
3
- "version": "0.2.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
- "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": ">=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
- }
63
-
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) {