dsh-tdai-memory 0.2.1 → 0.2.3

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
@@ -58,8 +58,9 @@ const Config = z.object({
58
58
  /** L1 extraction pipeline (facts from conversations). */
59
59
  extraction: z.object({
60
60
  enabled: z.boolean().default(true),
61
- /** Conflict detection before storing; extra LLM call, disable if flaky. */
62
- enableDedup: z.boolean().default(true),
61
+ /** Conflict detection before storing; extra LLM call, disable if flaky.
62
+ * Off by default: dedup LLM output parsing is flaky (README matches). */
63
+ enableDedup: z.boolean().default(false),
63
64
  }),
64
65
  /** Recall injection settings. */
65
66
  recall: z.object({
@@ -84,74 +85,21 @@ function extractText(content) {
84
85
  }
85
86
 
86
87
  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" });
88
+ // ── build-once state (rebuildable) ───────────────────────────────────────
89
+ // TdaiCore is built from the RESOLVED settings value (composition entry as
90
+ // base, `tdai-memory` settings section / settings.yaml as user layer), so
91
+ // keys stored in settings.yaml (llm.apiKey etc.) are honored. The settings
92
+ // inject callback that drives `installSettingsSection`'s setSource is
93
+ // scheduled by cordis as a plugin start — it ALWAYS runs after any 0ms
94
+ // timer — so the no-settings fallback below cannot be allowed to win the
95
+ // race. Instead of racing, we let the fallback build from the entry config
96
+ // first (guaranteeing a working core even without settings), and REBUILD
97
+ // from the resolved settings when the settings mount arrives. The fallback
98
+ // is a one-shot; the settings build always wins afterwards.
99
+ let core = null;
100
+ let tdaiConfig = null;
101
+ let dataDir = "";
102
+ let builtOnce = false;
155
103
 
156
104
  /** turn id of the last turn/end per session (capture trigger). */
157
105
  const lastTurnEnd = new Map();
@@ -170,6 +118,82 @@ function apply(ctx, config) {
170
118
  // long-lived process runs the pipeline in the background instead.
171
119
  const isOneShot = process.argv.some((arg) => arg === "headless");
172
120
 
121
+ // Verbose tdai-internal logging goes to the console only when explicitly
122
+ // enabled; routine diagnostics use the dsh logger.
123
+ const verbose = !!process.env.DSH_TDAI_DEBUG;
124
+ const log = (level, m) => {
125
+ if (verbose) console.error(`[tdai-memory] ${m}`);
126
+ else if (level === "warn") ctx.logger.warn(`[tdai-memory] ${m}`);
127
+ else if (level === "error") ctx.logger.error(`[tdai-memory] ${m}`);
128
+ };
129
+ const logger = {
130
+ debug: (m) => log("debug", m),
131
+ info: (m) => log("info", m),
132
+ warn: (m) => log("warn", m),
133
+ error: (m) => log("error", m),
134
+ };
135
+
136
+ // ── core construction (rebuildable) ──────────────────────────────────────
137
+ async function build(cfg) {
138
+ if (cfg?.llm?.apiKey) {
139
+ log("debug", `build: using settings config (apiKey=${cfg.llm.apiKey.slice(0, 6)}…)`);
140
+ }
141
+ // Tear down the previous core, if any (first build has none).
142
+ const previous = core;
143
+ if (previous) {
144
+ await previous.destroy().catch((error) => {
145
+ log("warn", `previous core destroy failed: ${String(error)}`);
146
+ });
147
+ }
148
+ dataDir = cfg.dataDir || join(os.homedir(), ".memory-tencentdb", "memory-tdai");
149
+ const nextConfig = parseConfig({
150
+ timezone: "system",
151
+ storeBackend: "sqlite",
152
+ capture: { enabled: cfg.captureEnabled },
153
+ extraction: { enabled: cfg.extraction.enabled, enableDedup: cfg.extraction.enableDedup },
154
+ recall: { ...cfg.recall },
155
+ embedding: {
156
+ enabled: true,
157
+ provider: "openai",
158
+ baseUrl: cfg.embedding.baseUrl,
159
+ apiKey: cfg.embedding.apiKey,
160
+ model: cfg.embedding.model,
161
+ dimensions: cfg.embedding.dimensions,
162
+ sendDimensions: cfg.embedding.sendDimensions,
163
+ timeoutMs: 10000,
164
+ },
165
+ llm: {
166
+ enabled: true,
167
+ baseUrl: cfg.llm.baseUrl || process.env.TDAI_LLM_BASE_URL || "",
168
+ apiKey: cfg.llm.apiKey || process.env.TDAI_LLM_API_KEY || "",
169
+ model: cfg.llm.model || process.env.TDAI_LLM_MODEL || "deepseek-v4-flash",
170
+ maxTokens: cfg.llm.maxTokens,
171
+ timeoutMs: cfg.llm.timeoutMs,
172
+ },
173
+ });
174
+ const nextHostAdapter = new StandaloneHostAdapter({
175
+ dataDir,
176
+ llmConfig: {
177
+ baseUrl: nextConfig.llm.baseUrl,
178
+ apiKey: nextConfig.llm.apiKey,
179
+ model: nextConfig.llm.model,
180
+ maxTokens: nextConfig.llm.maxTokens,
181
+ timeoutMs: nextConfig.llm.timeoutMs,
182
+ },
183
+ logger,
184
+ defaultUserId: "dsh-user",
185
+ platform: "dsh",
186
+ });
187
+ const nextCore = new TdaiCore({ hostAdapter: nextHostAdapter, config: nextConfig, instanceId: "dsh" });
188
+ // Publish before initialize so concurrent flush listeners already use the
189
+ // new core; initialize() only sets up stores/pipeline.
190
+ tdaiConfig = nextConfig;
191
+ core = nextCore;
192
+ await nextCore.initialize();
193
+ const key = nextConfig.llm.apiKey || "";
194
+ log("info", `initialized (dataDir=${dataDir}, llm=${nextConfig.llm.model}, apiKey=${key ? `${key.slice(0, 6)}…` : "<empty>"}, embedding=${nextConfig.embedding.model})`);
195
+ }
196
+
173
197
  // ── capture: triggered on session/flush (awaitable; runs before process
174
198
  // exit in headless, and at every request boundary in web) ─────────────
175
199
  ctx.on("session/event", (session, event) => {
@@ -190,7 +214,7 @@ function apply(ctx, config) {
190
214
  await captureTurn(session, startedAt);
191
215
  if (isOneShot) {
192
216
  // Run pending L1 extraction to completion before the process exits.
193
- await core.handleSessionEnd(session.id).catch((error) => {
217
+ await core?.handleSessionEnd(session.id).catch((error) => {
194
218
  ctx.logger.warn(`[tdai-memory] L1 flush failed: ${String(error)}`);
195
219
  });
196
220
  }
@@ -200,7 +224,7 @@ function apply(ctx, config) {
200
224
  });
201
225
 
202
226
  async function captureTurn(session, startedAt) {
203
- if (!tdaiConfig.capture.enabled) return;
227
+ if (!tdaiConfig?.capture.enabled) return;
204
228
  const derived = session.deriveMessages();
205
229
  const lastUserIdx = derived.map((m) => m.role).lastIndexOf("user");
206
230
  if (lastUserIdx < 0) return;
@@ -242,7 +266,7 @@ function apply(ctx, config) {
242
266
  if (!agent?.ctx) return;
243
267
  agent.ctx.on("system-prompt/assemble", async (assembly, context) => {
244
268
  try {
245
- if (!tdaiConfig.recall.enabled) return assembly;
269
+ if (!tdaiConfig?.recall.enabled) return assembly;
246
270
  const lastUser = [...session.deriveMessages()].reverse().find((m) => m.role === "user");
247
271
  if (!lastUser) return assembly;
248
272
  const text = extractText(lastUser.content);
@@ -319,43 +343,50 @@ function apply(ctx, config) {
319
343
 
320
344
  // ── settings-backed configuration ─────────────────────────────────────────
321
345
  // 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.
346
+ // settings section overlays it (settings.yaml). `installSettingsSection`
347
+ // hands `setSource` a GETTER (`() => scope.get()`), not the config object —
348
+ // we must call it to obtain the resolved settings. The settings mount
349
+ // (cordis inject) always arrives after our fallback timer, so the fallback
350
+ // builds the core from the entry config first (one-shot) and the settings
351
+ // build REBUILDS the core with the resolved values (apiKey included).
325
352
  installSettingsSection(ctx, NS, Config, config, {
326
- setSource: (source) => {
327
- build(source);
353
+ setSource: (getter) => {
354
+ builtOnce = true;
355
+ void build(getter());
328
356
  },
329
357
  onChange: () => {
330
- if (started) ctx.logger.warn("[tdai-memory] settings updated; restart to apply (TdaiCore is built at startup)");
358
+ ctx.logger.warn("[tdai-memory] settings updated; restart to apply (TdaiCore is built at startup)");
331
359
  },
332
360
  });
333
361
 
362
+ // Fallback when no settings service ever mounts: build from the entry
363
+ // config. One-shot — if the settings mount arrives later, its build
364
+ // supersedes this one. Delayed past the 0ms window so headless/web boots
365
+ // are never blocked on this timer.
366
+ setTimeout(() => {
367
+ if (!builtOnce) {
368
+ builtOnce = true;
369
+ void build(config);
370
+ }
371
+ }, 500);
372
+
334
373
  // ── 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})`);
374
+ ctx.effect(() => {
338
375
  return async () => {
339
376
  // Final flush: run pending L1 extraction for every session seen this
340
377
  // process and wait for it to finish (headless exits right after the
341
378
  // tree disposes, so this is what makes extraction complete in one-shot
342
379
  // runs). Unknown keys are tolerated as no-ops.
343
380
  for (const sessionKey of seenSessions) {
344
- await core.handleSessionEnd(sessionKey).catch((error) => {
381
+ await core?.handleSessionEnd(sessionKey).catch((error) => {
345
382
  ctx.logger.warn(`[tdai-memory] final L1 flush failed for ${sessionKey}: ${String(error)}`);
346
383
  });
347
384
  }
348
- await core.destroy().catch((error) => {
385
+ await core?.destroy().catch((error) => {
349
386
  ctx.logger.warn(`[tdai-memory] destroy failed: ${String(error)}`);
350
387
  });
351
388
  };
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);
389
+ }, "tdai-memory.cleanup()");
359
390
  }
360
391
 
361
392
  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.3",
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) {