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 +122 -91
- package/package.json +62 -63
- package/vendor/tdai/utils/checkpoint.js +5 -1
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
|
-
|
|
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
|
-
// ──
|
|
88
|
-
// TdaiCore is built
|
|
89
|
-
//
|
|
90
|
-
//
|
|
91
|
-
//
|
|
92
|
-
//
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
|
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
|
|
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
|
|
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).
|
|
323
|
-
//
|
|
324
|
-
//
|
|
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: (
|
|
327
|
-
|
|
353
|
+
setSource: (getter) => {
|
|
354
|
+
builtOnce = true;
|
|
355
|
+
void build(getter());
|
|
328
356
|
},
|
|
329
357
|
onChange: () => {
|
|
330
|
-
|
|
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(
|
|
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
|
|
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
|
|
385
|
+
await core?.destroy().catch((error) => {
|
|
349
386
|
ctx.logger.warn(`[tdai-memory] destroy failed: ${String(error)}`);
|
|
350
387
|
});
|
|
351
388
|
};
|
|
352
|
-
}, "tdai-memory.
|
|
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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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) {
|