dsh-tdai-memory 0.4.2 → 0.4.4
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/LICENSE +21 -21
- package/README.md +8 -0
- package/README.zh.md +8 -0
- package/THIRD-PARTY-NOTICES.md +20 -20
- package/cordis.patch.yml +8 -8
- package/package.json +103 -100
- package/vendor/tdai/adapters/index.js +15 -15
- package/vendor/tdai/adapters/openclaw/host-adapter.js +83 -83
- package/vendor/tdai/adapters/openclaw/index.js +5 -5
- package/vendor/tdai/adapters/openclaw/llm-runner.js +69 -69
- package/vendor/tdai/adapters/standalone/host-adapter.js +58 -58
- package/vendor/tdai/adapters/standalone/index.js +5 -5
- package/vendor/tdai/core/conversation/l0-recorder.js +420 -420
- package/vendor/tdai/core/hooks/auto-capture.js +236 -236
- package/vendor/tdai/core/hooks/auto-recall.js +640 -640
- package/vendor/tdai/core/index.js +8 -8
- package/vendor/tdai/core/persona/persona-generator.js +184 -184
- package/vendor/tdai/core/persona/persona-trigger.js +94 -94
- package/vendor/tdai/core/profile/profile-sync.js +200 -200
- package/vendor/tdai/core/prompts/l1-dedup.js +72 -72
- package/vendor/tdai/core/prompts/l1-extraction.js +34 -34
- package/vendor/tdai/core/prompts/persona-generation.js +35 -35
- package/vendor/tdai/core/prompts/scene-extraction.js +45 -45
- package/vendor/tdai/core/record/l1-dedup.js +318 -318
- package/vendor/tdai/core/record/l1-extractor.js +379 -379
- package/vendor/tdai/core/record/l1-reader.js +178 -178
- package/vendor/tdai/core/record/l1-writer.js +141 -141
- package/vendor/tdai/core/report/reporter.js +86 -86
- package/vendor/tdai/core/scene/filename-normalizer.js +161 -161
- package/vendor/tdai/core/scene/scene-extractor.js +410 -410
- package/vendor/tdai/core/scene/scene-format.js +53 -53
- package/vendor/tdai/core/scene/scene-index.js +82 -82
- package/vendor/tdai/core/scene/scene-navigation.js +57 -57
- package/vendor/tdai/core/seed/input.js +404 -404
- package/vendor/tdai/core/seed/types.js +9 -9
- package/vendor/tdai/core/store/bm25-client.js +117 -117
- package/vendor/tdai/core/store/bm25-local.js +68 -68
- package/vendor/tdai/core/store/embedding.js +632 -632
- package/vendor/tdai/core/store/factory.js +95 -95
- package/vendor/tdai/core/store/search-utils.js +54 -54
- package/vendor/tdai/core/store/sqlite.js +1632 -1632
- package/vendor/tdai/core/store/tcvdb-client.js +221 -221
- package/vendor/tdai/core/store/tcvdb.js +1047 -1047
- package/vendor/tdai/core/store/types.js +17 -17
- package/vendor/tdai/core/tools/conversation-search.js +199 -199
- package/vendor/tdai/core/tools/memory-search.js +207 -207
- package/vendor/tdai/core/types.js +12 -12
- package/vendor/tdai/utils/backup.js +189 -189
- package/vendor/tdai/utils/checkpoint.js +360 -360
- package/vendor/tdai/utils/clean-context-runner.js +472 -472
- package/vendor/tdai/utils/ensure-hook-policy.js +213 -213
- package/vendor/tdai/utils/env.js +13 -13
- package/vendor/tdai/utils/managed-timer.js +127 -127
- package/vendor/tdai/utils/manifest.js +91 -91
- package/vendor/tdai/utils/memory-cleaner.js +300 -300
- package/vendor/tdai/utils/no-think-fetch.js +109 -109
- package/vendor/tdai/utils/openclaw-state-dir.js +26 -26
- package/vendor/tdai/utils/pipeline-factory.js +517 -517
- package/vendor/tdai/utils/pipeline-manager.js +892 -892
- package/vendor/tdai/utils/sanitize.js +360 -360
- package/vendor/tdai/utils/serial-queue.js +103 -103
- package/vendor/tdai/utils/session-filter.js +88 -88
- package/vendor/tdai/utils/text-utils.js +29 -29
- package/vendor/tdai/utils/time.js +252 -252
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* TDAI Core — barrel re-export for core types and service facade.
|
|
3
|
-
*
|
|
4
|
-
* This module exports ONLY the host-neutral interfaces and the TdaiCore facade.
|
|
5
|
-
* Host-specific adapters live in `../adapters/`.
|
|
6
|
-
*/
|
|
7
|
-
// TdaiCore service facade
|
|
8
|
-
export { TdaiCore } from "./tdai-core.js";
|
|
1
|
+
/**
|
|
2
|
+
* TDAI Core — barrel re-export for core types and service facade.
|
|
3
|
+
*
|
|
4
|
+
* This module exports ONLY the host-neutral interfaces and the TdaiCore facade.
|
|
5
|
+
* Host-specific adapters live in `../adapters/`.
|
|
6
|
+
*/
|
|
7
|
+
// TdaiCore service facade
|
|
8
|
+
export { TdaiCore } from "./tdai-core.js";
|
|
@@ -1,184 +1,184 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* PersonaGenerator: generates or updates user persona using the four-layer
|
|
3
|
-
* deep scan model via CleanContextRunner.
|
|
4
|
-
*/
|
|
5
|
-
import fs from "node:fs/promises";
|
|
6
|
-
import path from "node:path";
|
|
7
|
-
import { formatForLLM } from "../../utils/time.js";
|
|
8
|
-
import { CleanContextRunner } from "../../utils/clean-context-runner.js";
|
|
9
|
-
import { CheckpointManager } from "../../utils/checkpoint.js";
|
|
10
|
-
import { readSceneIndex } from "../scene/scene-index.js";
|
|
11
|
-
import { generateSceneNavigation, stripSceneNavigation } from "../scene/scene-navigation.js";
|
|
12
|
-
import { buildPersonaPrompt } from "../prompts/persona-generation.js";
|
|
13
|
-
import { BackupManager } from "../../utils/backup.js";
|
|
14
|
-
import { escapeXmlTags } from "../../utils/sanitize.js";
|
|
15
|
-
import { report } from "../report/reporter.js";
|
|
16
|
-
const TAG = "[memory-tdai] [persona]";
|
|
17
|
-
export class PersonaGenerator {
|
|
18
|
-
dataDir;
|
|
19
|
-
runner;
|
|
20
|
-
logger;
|
|
21
|
-
backupCount;
|
|
22
|
-
instanceId;
|
|
23
|
-
constructor(opts) {
|
|
24
|
-
this.dataDir = opts.dataDir;
|
|
25
|
-
this.logger = opts.logger;
|
|
26
|
-
this.backupCount = opts.backupCount ?? 3;
|
|
27
|
-
this.instanceId = opts.instanceId;
|
|
28
|
-
// Use injected LLMRunner if available, otherwise fall back to CleanContextRunner
|
|
29
|
-
this.runner = opts.llmRunner ?? new CleanContextRunner({
|
|
30
|
-
config: opts.config,
|
|
31
|
-
modelRef: opts.model,
|
|
32
|
-
enableTools: true,
|
|
33
|
-
logger: opts.logger,
|
|
34
|
-
});
|
|
35
|
-
this.logger?.debug?.(`${TAG} Generator created: model=${opts.model ?? "(default)"}, dataDir=${opts.dataDir}`);
|
|
36
|
-
}
|
|
37
|
-
/**
|
|
38
|
-
* Execute local persona generation without advancing checkpoint.
|
|
39
|
-
*/
|
|
40
|
-
async generateLocalPersona(triggerReason) {
|
|
41
|
-
const startMs = Date.now();
|
|
42
|
-
this.logger?.debug?.(`${TAG} Starting generation: reason="${triggerReason ?? "none"}"`);
|
|
43
|
-
const cpManager = new CheckpointManager(this.dataDir);
|
|
44
|
-
const cp = await cpManager.read();
|
|
45
|
-
this.logger?.debug?.(`${TAG} Checkpoint: total_processed=${cp.total_processed}, last_persona_at=${cp.last_persona_at}`);
|
|
46
|
-
const personaPath = path.join(this.dataDir, "persona.md");
|
|
47
|
-
// 1. Read existing persona (strip navigation)
|
|
48
|
-
let existingPersona;
|
|
49
|
-
try {
|
|
50
|
-
const raw = await fs.readFile(personaPath, "utf-8");
|
|
51
|
-
existingPersona = stripSceneNavigation(raw).trim() || undefined;
|
|
52
|
-
this.logger?.debug?.(`${TAG} Existing persona: ${existingPersona ? `${existingPersona.length} chars` : "empty"}`);
|
|
53
|
-
}
|
|
54
|
-
catch {
|
|
55
|
-
this.logger?.debug?.(`${TAG} No existing persona file`);
|
|
56
|
-
}
|
|
57
|
-
// 2. Load scene index + identify changed scenes
|
|
58
|
-
const index = await readSceneIndex(this.dataDir);
|
|
59
|
-
const changedScenes = index.filter((e) => {
|
|
60
|
-
if (!cp.last_persona_time)
|
|
61
|
-
return true;
|
|
62
|
-
const updatedMs = new Date(e.updated).getTime();
|
|
63
|
-
const personaMs = new Date(cp.last_persona_time).getTime();
|
|
64
|
-
// If either date is unparseable (NaN), treat as changed (conservative)
|
|
65
|
-
if (Number.isNaN(updatedMs) || Number.isNaN(personaMs))
|
|
66
|
-
return true;
|
|
67
|
-
return updatedMs > personaMs;
|
|
68
|
-
});
|
|
69
|
-
this.logger?.debug?.(`${TAG} Scene index: ${index.length} total, ${changedScenes.length} changed since last persona`);
|
|
70
|
-
// 3. Read changed scene contents (full raw content including META, matching Python reference)
|
|
71
|
-
const blocksDir = path.join(this.dataDir, "scene_blocks");
|
|
72
|
-
const changedSceneContents = [];
|
|
73
|
-
for (const entry of changedScenes) {
|
|
74
|
-
try {
|
|
75
|
-
const raw = await fs.readFile(path.join(blocksDir, entry.filename), "utf-8");
|
|
76
|
-
changedSceneContents.push(`### [${changedSceneContents.length + 1}] ${entry.filename}\n\n\`\`\`markdown\n${raw}\n\`\`\``);
|
|
77
|
-
}
|
|
78
|
-
catch {
|
|
79
|
-
this.logger?.warn(`${TAG} Could not read scene block: ${entry.filename}`);
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
if (changedSceneContents.length === 0 && existingPersona) {
|
|
83
|
-
this.logger?.debug?.(`${TAG} No scene changes and persona exists, skipping generation`);
|
|
84
|
-
return false;
|
|
85
|
-
}
|
|
86
|
-
// 4. Determine mode
|
|
87
|
-
const mode = existingPersona ? "incremental" : "first";
|
|
88
|
-
this.logger?.debug?.(`${TAG} Generation mode: ${mode}, ${changedSceneContents.length} scene blocks to process`);
|
|
89
|
-
// 5. Build changed scenes section with guidance (matching Python reference format)
|
|
90
|
-
let changedScenesContent;
|
|
91
|
-
if (changedSceneContents.length > 0) {
|
|
92
|
-
changedScenesContent =
|
|
93
|
-
`\n\n## 📄 变化场景完整内容\n\n` +
|
|
94
|
-
`*自上次 Persona 更新后,以下 ${changedSceneContents.length} 个场景发生了变化。工程已为你预加载完整内容:*\n\n` +
|
|
95
|
-
changedSceneContents.join("\n\n") +
|
|
96
|
-
`\n\n---\n\n` +
|
|
97
|
-
`⚠️ **重点分析变化场景**:上述场景是自上次更新后的**新增/修改内容**,请**重点分析**这些场景中的新信息。\n`;
|
|
98
|
-
}
|
|
99
|
-
else {
|
|
100
|
-
changedScenesContent = `\n\n⚠️ **无变化场景**:所有场景均已在上次 Persona 更新中分析过,本次可直接读取所有场景进行全局审视。\n`;
|
|
101
|
-
}
|
|
102
|
-
// 6. Build prompt
|
|
103
|
-
const { systemPrompt, userPrompt } = buildPersonaPrompt({
|
|
104
|
-
mode,
|
|
105
|
-
currentTime: formatForLLM(new Date()),
|
|
106
|
-
totalProcessed: cp.total_processed,
|
|
107
|
-
sceneCount: index.length,
|
|
108
|
-
changedSceneCount: changedScenes.length,
|
|
109
|
-
changedScenesContent,
|
|
110
|
-
existingPersona,
|
|
111
|
-
triggerInfo: triggerReason,
|
|
112
|
-
personaFilePath: personaPath,
|
|
113
|
-
checkpointPath: path.join(this.dataDir, ".metadata", "recall_checkpoint.json"),
|
|
114
|
-
});
|
|
115
|
-
// 7. Backup before LLM run (LLM writes persona.md via tools)
|
|
116
|
-
const bm = new BackupManager(path.join(this.dataDir, ".backup"));
|
|
117
|
-
await bm.backupFile(personaPath, "persona", `offset${cp.total_processed}`, this.backupCount);
|
|
118
|
-
// 8. Run LLM agent (sandboxed to dataDir, tools enabled — LLM writes persona.md directly)
|
|
119
|
-
try {
|
|
120
|
-
this.logger?.debug?.(`${TAG} Calling LLM for persona generation (timeout=180s, tools=enabled, workspaceDir=${this.dataDir})...`);
|
|
121
|
-
await this.runner.run({
|
|
122
|
-
systemPrompt,
|
|
123
|
-
prompt: userPrompt,
|
|
124
|
-
taskId: "persona-generation",
|
|
125
|
-
timeoutMs: 180_000,
|
|
126
|
-
// maxTokens omitted → core uses the resolved model's maxTokens from catalog
|
|
127
|
-
workspaceDir: this.dataDir,
|
|
128
|
-
});
|
|
129
|
-
this.logger?.debug?.(`${TAG} LLM runner completed`);
|
|
130
|
-
}
|
|
131
|
-
catch (err) {
|
|
132
|
-
const elapsedMs = Date.now() - startMs;
|
|
133
|
-
this.logger?.error(`${TAG} Persona generation failed after ${elapsedMs}ms: ${err instanceof Error ? err.stack ?? err.message : String(err)}`);
|
|
134
|
-
return false;
|
|
135
|
-
}
|
|
136
|
-
// 9. Read LLM-written persona.md and apply post-processing
|
|
137
|
-
let personaText;
|
|
138
|
-
try {
|
|
139
|
-
personaText = await fs.readFile(personaPath, "utf-8");
|
|
140
|
-
}
|
|
141
|
-
catch {
|
|
142
|
-
// LLM failed to write persona.md — treat as failure
|
|
143
|
-
this.logger?.error(`${TAG} LLM did not write persona.md — file not found after runner completed`);
|
|
144
|
-
return false;
|
|
145
|
-
}
|
|
146
|
-
// 10. Strip any navigation the LLM might have added + sanitize for safe injection
|
|
147
|
-
personaText = escapeXmlTags(stripSceneNavigation(personaText).trim());
|
|
148
|
-
if (!personaText) {
|
|
149
|
-
this.logger?.error(`${TAG} LLM wrote empty persona.md — skipping`);
|
|
150
|
-
return false;
|
|
151
|
-
}
|
|
152
|
-
// 11. Append fresh scene navigation and write final content
|
|
153
|
-
const nav = generateSceneNavigation(index);
|
|
154
|
-
const finalContent = nav ? `${personaText}\n\n${nav}\n` : personaText;
|
|
155
|
-
await fs.writeFile(personaPath, finalContent, "utf-8");
|
|
156
|
-
const elapsedMs = Date.now() - startMs;
|
|
157
|
-
this.logger?.info(`${TAG} Persona written (${finalContent.length} chars) in ${elapsedMs}ms`);
|
|
158
|
-
// ── l3_persona_generation metric ──
|
|
159
|
-
if (this.instanceId && this.logger) {
|
|
160
|
-
report("l3_persona_generation", {
|
|
161
|
-
triggerReason: triggerReason ?? "unknown",
|
|
162
|
-
mode: existingPersona ? "incremental" : "initial",
|
|
163
|
-
newPersonaContent: personaText,
|
|
164
|
-
newPersonaLength: personaText.length,
|
|
165
|
-
totalDurationMs: elapsedMs,
|
|
166
|
-
success: true,
|
|
167
|
-
error: null,
|
|
168
|
-
});
|
|
169
|
-
}
|
|
170
|
-
return true;
|
|
171
|
-
}
|
|
172
|
-
/**
|
|
173
|
-
* Backward-compatible wrapper: local generation + checkpoint advance.
|
|
174
|
-
*/
|
|
175
|
-
async generate(triggerReason) {
|
|
176
|
-
const updated = await this.generateLocalPersona(triggerReason);
|
|
177
|
-
if (!updated)
|
|
178
|
-
return false;
|
|
179
|
-
const cpManager = new CheckpointManager(this.dataDir);
|
|
180
|
-
const cp = await cpManager.read();
|
|
181
|
-
await cpManager.markPersonaGenerated(cp.total_processed);
|
|
182
|
-
return true;
|
|
183
|
-
}
|
|
184
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* PersonaGenerator: generates or updates user persona using the four-layer
|
|
3
|
+
* deep scan model via CleanContextRunner.
|
|
4
|
+
*/
|
|
5
|
+
import fs from "node:fs/promises";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { formatForLLM } from "../../utils/time.js";
|
|
8
|
+
import { CleanContextRunner } from "../../utils/clean-context-runner.js";
|
|
9
|
+
import { CheckpointManager } from "../../utils/checkpoint.js";
|
|
10
|
+
import { readSceneIndex } from "../scene/scene-index.js";
|
|
11
|
+
import { generateSceneNavigation, stripSceneNavigation } from "../scene/scene-navigation.js";
|
|
12
|
+
import { buildPersonaPrompt } from "../prompts/persona-generation.js";
|
|
13
|
+
import { BackupManager } from "../../utils/backup.js";
|
|
14
|
+
import { escapeXmlTags } from "../../utils/sanitize.js";
|
|
15
|
+
import { report } from "../report/reporter.js";
|
|
16
|
+
const TAG = "[memory-tdai] [persona]";
|
|
17
|
+
export class PersonaGenerator {
|
|
18
|
+
dataDir;
|
|
19
|
+
runner;
|
|
20
|
+
logger;
|
|
21
|
+
backupCount;
|
|
22
|
+
instanceId;
|
|
23
|
+
constructor(opts) {
|
|
24
|
+
this.dataDir = opts.dataDir;
|
|
25
|
+
this.logger = opts.logger;
|
|
26
|
+
this.backupCount = opts.backupCount ?? 3;
|
|
27
|
+
this.instanceId = opts.instanceId;
|
|
28
|
+
// Use injected LLMRunner if available, otherwise fall back to CleanContextRunner
|
|
29
|
+
this.runner = opts.llmRunner ?? new CleanContextRunner({
|
|
30
|
+
config: opts.config,
|
|
31
|
+
modelRef: opts.model,
|
|
32
|
+
enableTools: true,
|
|
33
|
+
logger: opts.logger,
|
|
34
|
+
});
|
|
35
|
+
this.logger?.debug?.(`${TAG} Generator created: model=${opts.model ?? "(default)"}, dataDir=${opts.dataDir}`);
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Execute local persona generation without advancing checkpoint.
|
|
39
|
+
*/
|
|
40
|
+
async generateLocalPersona(triggerReason) {
|
|
41
|
+
const startMs = Date.now();
|
|
42
|
+
this.logger?.debug?.(`${TAG} Starting generation: reason="${triggerReason ?? "none"}"`);
|
|
43
|
+
const cpManager = new CheckpointManager(this.dataDir);
|
|
44
|
+
const cp = await cpManager.read();
|
|
45
|
+
this.logger?.debug?.(`${TAG} Checkpoint: total_processed=${cp.total_processed}, last_persona_at=${cp.last_persona_at}`);
|
|
46
|
+
const personaPath = path.join(this.dataDir, "persona.md");
|
|
47
|
+
// 1. Read existing persona (strip navigation)
|
|
48
|
+
let existingPersona;
|
|
49
|
+
try {
|
|
50
|
+
const raw = await fs.readFile(personaPath, "utf-8");
|
|
51
|
+
existingPersona = stripSceneNavigation(raw).trim() || undefined;
|
|
52
|
+
this.logger?.debug?.(`${TAG} Existing persona: ${existingPersona ? `${existingPersona.length} chars` : "empty"}`);
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
this.logger?.debug?.(`${TAG} No existing persona file`);
|
|
56
|
+
}
|
|
57
|
+
// 2. Load scene index + identify changed scenes
|
|
58
|
+
const index = await readSceneIndex(this.dataDir);
|
|
59
|
+
const changedScenes = index.filter((e) => {
|
|
60
|
+
if (!cp.last_persona_time)
|
|
61
|
+
return true;
|
|
62
|
+
const updatedMs = new Date(e.updated).getTime();
|
|
63
|
+
const personaMs = new Date(cp.last_persona_time).getTime();
|
|
64
|
+
// If either date is unparseable (NaN), treat as changed (conservative)
|
|
65
|
+
if (Number.isNaN(updatedMs) || Number.isNaN(personaMs))
|
|
66
|
+
return true;
|
|
67
|
+
return updatedMs > personaMs;
|
|
68
|
+
});
|
|
69
|
+
this.logger?.debug?.(`${TAG} Scene index: ${index.length} total, ${changedScenes.length} changed since last persona`);
|
|
70
|
+
// 3. Read changed scene contents (full raw content including META, matching Python reference)
|
|
71
|
+
const blocksDir = path.join(this.dataDir, "scene_blocks");
|
|
72
|
+
const changedSceneContents = [];
|
|
73
|
+
for (const entry of changedScenes) {
|
|
74
|
+
try {
|
|
75
|
+
const raw = await fs.readFile(path.join(blocksDir, entry.filename), "utf-8");
|
|
76
|
+
changedSceneContents.push(`### [${changedSceneContents.length + 1}] ${entry.filename}\n\n\`\`\`markdown\n${raw}\n\`\`\``);
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
this.logger?.warn(`${TAG} Could not read scene block: ${entry.filename}`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (changedSceneContents.length === 0 && existingPersona) {
|
|
83
|
+
this.logger?.debug?.(`${TAG} No scene changes and persona exists, skipping generation`);
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
// 4. Determine mode
|
|
87
|
+
const mode = existingPersona ? "incremental" : "first";
|
|
88
|
+
this.logger?.debug?.(`${TAG} Generation mode: ${mode}, ${changedSceneContents.length} scene blocks to process`);
|
|
89
|
+
// 5. Build changed scenes section with guidance (matching Python reference format)
|
|
90
|
+
let changedScenesContent;
|
|
91
|
+
if (changedSceneContents.length > 0) {
|
|
92
|
+
changedScenesContent =
|
|
93
|
+
`\n\n## 📄 变化场景完整内容\n\n` +
|
|
94
|
+
`*自上次 Persona 更新后,以下 ${changedSceneContents.length} 个场景发生了变化。工程已为你预加载完整内容:*\n\n` +
|
|
95
|
+
changedSceneContents.join("\n\n") +
|
|
96
|
+
`\n\n---\n\n` +
|
|
97
|
+
`⚠️ **重点分析变化场景**:上述场景是自上次更新后的**新增/修改内容**,请**重点分析**这些场景中的新信息。\n`;
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
changedScenesContent = `\n\n⚠️ **无变化场景**:所有场景均已在上次 Persona 更新中分析过,本次可直接读取所有场景进行全局审视。\n`;
|
|
101
|
+
}
|
|
102
|
+
// 6. Build prompt
|
|
103
|
+
const { systemPrompt, userPrompt } = buildPersonaPrompt({
|
|
104
|
+
mode,
|
|
105
|
+
currentTime: formatForLLM(new Date()),
|
|
106
|
+
totalProcessed: cp.total_processed,
|
|
107
|
+
sceneCount: index.length,
|
|
108
|
+
changedSceneCount: changedScenes.length,
|
|
109
|
+
changedScenesContent,
|
|
110
|
+
existingPersona,
|
|
111
|
+
triggerInfo: triggerReason,
|
|
112
|
+
personaFilePath: personaPath,
|
|
113
|
+
checkpointPath: path.join(this.dataDir, ".metadata", "recall_checkpoint.json"),
|
|
114
|
+
});
|
|
115
|
+
// 7. Backup before LLM run (LLM writes persona.md via tools)
|
|
116
|
+
const bm = new BackupManager(path.join(this.dataDir, ".backup"));
|
|
117
|
+
await bm.backupFile(personaPath, "persona", `offset${cp.total_processed}`, this.backupCount);
|
|
118
|
+
// 8. Run LLM agent (sandboxed to dataDir, tools enabled — LLM writes persona.md directly)
|
|
119
|
+
try {
|
|
120
|
+
this.logger?.debug?.(`${TAG} Calling LLM for persona generation (timeout=180s, tools=enabled, workspaceDir=${this.dataDir})...`);
|
|
121
|
+
await this.runner.run({
|
|
122
|
+
systemPrompt,
|
|
123
|
+
prompt: userPrompt,
|
|
124
|
+
taskId: "persona-generation",
|
|
125
|
+
timeoutMs: 180_000,
|
|
126
|
+
// maxTokens omitted → core uses the resolved model's maxTokens from catalog
|
|
127
|
+
workspaceDir: this.dataDir,
|
|
128
|
+
});
|
|
129
|
+
this.logger?.debug?.(`${TAG} LLM runner completed`);
|
|
130
|
+
}
|
|
131
|
+
catch (err) {
|
|
132
|
+
const elapsedMs = Date.now() - startMs;
|
|
133
|
+
this.logger?.error(`${TAG} Persona generation failed after ${elapsedMs}ms: ${err instanceof Error ? err.stack ?? err.message : String(err)}`);
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
// 9. Read LLM-written persona.md and apply post-processing
|
|
137
|
+
let personaText;
|
|
138
|
+
try {
|
|
139
|
+
personaText = await fs.readFile(personaPath, "utf-8");
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
// LLM failed to write persona.md — treat as failure
|
|
143
|
+
this.logger?.error(`${TAG} LLM did not write persona.md — file not found after runner completed`);
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
// 10. Strip any navigation the LLM might have added + sanitize for safe injection
|
|
147
|
+
personaText = escapeXmlTags(stripSceneNavigation(personaText).trim());
|
|
148
|
+
if (!personaText) {
|
|
149
|
+
this.logger?.error(`${TAG} LLM wrote empty persona.md — skipping`);
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
// 11. Append fresh scene navigation and write final content
|
|
153
|
+
const nav = generateSceneNavigation(index);
|
|
154
|
+
const finalContent = nav ? `${personaText}\n\n${nav}\n` : personaText;
|
|
155
|
+
await fs.writeFile(personaPath, finalContent, "utf-8");
|
|
156
|
+
const elapsedMs = Date.now() - startMs;
|
|
157
|
+
this.logger?.info(`${TAG} Persona written (${finalContent.length} chars) in ${elapsedMs}ms`);
|
|
158
|
+
// ── l3_persona_generation metric ──
|
|
159
|
+
if (this.instanceId && this.logger) {
|
|
160
|
+
report("l3_persona_generation", {
|
|
161
|
+
triggerReason: triggerReason ?? "unknown",
|
|
162
|
+
mode: existingPersona ? "incremental" : "initial",
|
|
163
|
+
newPersonaContent: personaText,
|
|
164
|
+
newPersonaLength: personaText.length,
|
|
165
|
+
totalDurationMs: elapsedMs,
|
|
166
|
+
success: true,
|
|
167
|
+
error: null,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Backward-compatible wrapper: local generation + checkpoint advance.
|
|
174
|
+
*/
|
|
175
|
+
async generate(triggerReason) {
|
|
176
|
+
const updated = await this.generateLocalPersona(triggerReason);
|
|
177
|
+
if (!updated)
|
|
178
|
+
return false;
|
|
179
|
+
const cpManager = new CheckpointManager(this.dataDir);
|
|
180
|
+
const cp = await cpManager.read();
|
|
181
|
+
await cpManager.markPersonaGenerated(cp.total_processed);
|
|
182
|
+
return true;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
@@ -1,94 +1,94 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* PersonaTrigger: determines whether to trigger persona generation.
|
|
3
|
-
* Implements the 5 trigger conditions from the legacy system.
|
|
4
|
-
*/
|
|
5
|
-
import fs from "node:fs/promises";
|
|
6
|
-
import path from "node:path";
|
|
7
|
-
import { CheckpointManager } from "../../utils/checkpoint.js";
|
|
8
|
-
import { stripSceneNavigation } from "../scene/scene-navigation.js";
|
|
9
|
-
const TAG = "[memory-tdai] [trigger]";
|
|
10
|
-
export class PersonaTrigger {
|
|
11
|
-
dataDir;
|
|
12
|
-
interval;
|
|
13
|
-
logger;
|
|
14
|
-
constructor(opts) {
|
|
15
|
-
this.dataDir = opts.dataDir;
|
|
16
|
-
this.interval = opts.interval;
|
|
17
|
-
this.logger = opts.logger;
|
|
18
|
-
}
|
|
19
|
-
async shouldGenerate() {
|
|
20
|
-
const cpManager = new CheckpointManager(this.dataDir);
|
|
21
|
-
const cp = await cpManager.read();
|
|
22
|
-
this.logger?.debug?.(`${TAG} Evaluating: total_processed=${cp.total_processed}, last_persona_at=${cp.last_persona_at}, memories_since=${cp.memories_since_last_persona}, scenes=${cp.scenes_processed}`);
|
|
23
|
-
// Priority 1: Agent explicitly requested persona update
|
|
24
|
-
if (cp.request_persona_update) {
|
|
25
|
-
const result = {
|
|
26
|
-
should: true,
|
|
27
|
-
reason: `主动请求: ${cp.persona_update_reason || "Agent 请求更新"}`,
|
|
28
|
-
};
|
|
29
|
-
this.logger?.debug?.(`${TAG} Trigger P1 (explicit request): ${result.reason}`);
|
|
30
|
-
return result;
|
|
31
|
-
}
|
|
32
|
-
// Priority 2: Cold start — first extraction done, no persona yet, has scene files
|
|
33
|
-
if (cp.scenes_processed > 0 &&
|
|
34
|
-
cp.last_persona_at === 0 &&
|
|
35
|
-
(await this.hasSceneFiles())) {
|
|
36
|
-
const result = { should: true, reason: "首次冷启动:首次提取完成且有场景文件" };
|
|
37
|
-
this.logger?.debug?.(`${TAG} Trigger P2 (cold start): scenes_processed=${cp.scenes_processed}, total_processed=${cp.total_processed}`);
|
|
38
|
-
return result;
|
|
39
|
-
}
|
|
40
|
-
// Priority 2.5: Recovery — persona was generated before but persona.md body
|
|
41
|
-
// is now empty (corrupted/missing). Regenerate to restore.
|
|
42
|
-
if (cp.last_persona_at > 0 &&
|
|
43
|
-
(await this.hasSceneFiles()) &&
|
|
44
|
-
!(await this.hasPersonaBody())) {
|
|
45
|
-
const result = { should: true, reason: "恢复:persona.md 正文丢失或为空,需要重新生成" };
|
|
46
|
-
this.logger?.debug?.(`${TAG} Trigger P2.5 (recovery): last_persona_at=${cp.last_persona_at}, persona body missing`);
|
|
47
|
-
return result;
|
|
48
|
-
}
|
|
49
|
-
// Priority 3: First scene block extraction
|
|
50
|
-
if (cp.scenes_processed === 1 && cp.memories_since_last_persona > 0) {
|
|
51
|
-
const result = { should: true, reason: "首次 Scene Block 提取完成" };
|
|
52
|
-
this.logger?.debug?.(`${TAG} Trigger P3 (first scene): scenes_processed=${cp.scenes_processed}`);
|
|
53
|
-
return result;
|
|
54
|
-
}
|
|
55
|
-
// Priority 4: Reached threshold
|
|
56
|
-
if (cp.memories_since_last_persona >= this.interval) {
|
|
57
|
-
const result = {
|
|
58
|
-
should: true,
|
|
59
|
-
reason: `达到阈值: ${cp.memories_since_last_persona} >= ${this.interval}`,
|
|
60
|
-
};
|
|
61
|
-
this.logger?.debug?.(`${TAG} Trigger P4 (threshold): ${result.reason}`);
|
|
62
|
-
return result;
|
|
63
|
-
}
|
|
64
|
-
this.logger?.debug?.(`${TAG} No trigger conditions met`);
|
|
65
|
-
return { should: false, reason: "" };
|
|
66
|
-
}
|
|
67
|
-
async hasSceneFiles() {
|
|
68
|
-
const blocksDir = path.join(this.dataDir, "scene_blocks");
|
|
69
|
-
try {
|
|
70
|
-
const files = await fs.readdir(blocksDir);
|
|
71
|
-
const hasFiles = files.some((f) => f.endsWith(".md"));
|
|
72
|
-
return hasFiles;
|
|
73
|
-
}
|
|
74
|
-
catch {
|
|
75
|
-
return false;
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
/**
|
|
79
|
-
* Check whether persona.md has a non-empty body (excluding scene navigation).
|
|
80
|
-
* Returns false if the file doesn't exist, is empty, or only contains
|
|
81
|
-
* scene navigation (no actual persona content).
|
|
82
|
-
*/
|
|
83
|
-
async hasPersonaBody() {
|
|
84
|
-
const personaPath = path.join(this.dataDir, "persona.md");
|
|
85
|
-
try {
|
|
86
|
-
const raw = await fs.readFile(personaPath, "utf-8");
|
|
87
|
-
const body = stripSceneNavigation(raw).trim();
|
|
88
|
-
return body.length > 0;
|
|
89
|
-
}
|
|
90
|
-
catch {
|
|
91
|
-
return false;
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* PersonaTrigger: determines whether to trigger persona generation.
|
|
3
|
+
* Implements the 5 trigger conditions from the legacy system.
|
|
4
|
+
*/
|
|
5
|
+
import fs from "node:fs/promises";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { CheckpointManager } from "../../utils/checkpoint.js";
|
|
8
|
+
import { stripSceneNavigation } from "../scene/scene-navigation.js";
|
|
9
|
+
const TAG = "[memory-tdai] [trigger]";
|
|
10
|
+
export class PersonaTrigger {
|
|
11
|
+
dataDir;
|
|
12
|
+
interval;
|
|
13
|
+
logger;
|
|
14
|
+
constructor(opts) {
|
|
15
|
+
this.dataDir = opts.dataDir;
|
|
16
|
+
this.interval = opts.interval;
|
|
17
|
+
this.logger = opts.logger;
|
|
18
|
+
}
|
|
19
|
+
async shouldGenerate() {
|
|
20
|
+
const cpManager = new CheckpointManager(this.dataDir);
|
|
21
|
+
const cp = await cpManager.read();
|
|
22
|
+
this.logger?.debug?.(`${TAG} Evaluating: total_processed=${cp.total_processed}, last_persona_at=${cp.last_persona_at}, memories_since=${cp.memories_since_last_persona}, scenes=${cp.scenes_processed}`);
|
|
23
|
+
// Priority 1: Agent explicitly requested persona update
|
|
24
|
+
if (cp.request_persona_update) {
|
|
25
|
+
const result = {
|
|
26
|
+
should: true,
|
|
27
|
+
reason: `主动请求: ${cp.persona_update_reason || "Agent 请求更新"}`,
|
|
28
|
+
};
|
|
29
|
+
this.logger?.debug?.(`${TAG} Trigger P1 (explicit request): ${result.reason}`);
|
|
30
|
+
return result;
|
|
31
|
+
}
|
|
32
|
+
// Priority 2: Cold start — first extraction done, no persona yet, has scene files
|
|
33
|
+
if (cp.scenes_processed > 0 &&
|
|
34
|
+
cp.last_persona_at === 0 &&
|
|
35
|
+
(await this.hasSceneFiles())) {
|
|
36
|
+
const result = { should: true, reason: "首次冷启动:首次提取完成且有场景文件" };
|
|
37
|
+
this.logger?.debug?.(`${TAG} Trigger P2 (cold start): scenes_processed=${cp.scenes_processed}, total_processed=${cp.total_processed}`);
|
|
38
|
+
return result;
|
|
39
|
+
}
|
|
40
|
+
// Priority 2.5: Recovery — persona was generated before but persona.md body
|
|
41
|
+
// is now empty (corrupted/missing). Regenerate to restore.
|
|
42
|
+
if (cp.last_persona_at > 0 &&
|
|
43
|
+
(await this.hasSceneFiles()) &&
|
|
44
|
+
!(await this.hasPersonaBody())) {
|
|
45
|
+
const result = { should: true, reason: "恢复:persona.md 正文丢失或为空,需要重新生成" };
|
|
46
|
+
this.logger?.debug?.(`${TAG} Trigger P2.5 (recovery): last_persona_at=${cp.last_persona_at}, persona body missing`);
|
|
47
|
+
return result;
|
|
48
|
+
}
|
|
49
|
+
// Priority 3: First scene block extraction
|
|
50
|
+
if (cp.scenes_processed === 1 && cp.memories_since_last_persona > 0) {
|
|
51
|
+
const result = { should: true, reason: "首次 Scene Block 提取完成" };
|
|
52
|
+
this.logger?.debug?.(`${TAG} Trigger P3 (first scene): scenes_processed=${cp.scenes_processed}`);
|
|
53
|
+
return result;
|
|
54
|
+
}
|
|
55
|
+
// Priority 4: Reached threshold
|
|
56
|
+
if (cp.memories_since_last_persona >= this.interval) {
|
|
57
|
+
const result = {
|
|
58
|
+
should: true,
|
|
59
|
+
reason: `达到阈值: ${cp.memories_since_last_persona} >= ${this.interval}`,
|
|
60
|
+
};
|
|
61
|
+
this.logger?.debug?.(`${TAG} Trigger P4 (threshold): ${result.reason}`);
|
|
62
|
+
return result;
|
|
63
|
+
}
|
|
64
|
+
this.logger?.debug?.(`${TAG} No trigger conditions met`);
|
|
65
|
+
return { should: false, reason: "" };
|
|
66
|
+
}
|
|
67
|
+
async hasSceneFiles() {
|
|
68
|
+
const blocksDir = path.join(this.dataDir, "scene_blocks");
|
|
69
|
+
try {
|
|
70
|
+
const files = await fs.readdir(blocksDir);
|
|
71
|
+
const hasFiles = files.some((f) => f.endsWith(".md"));
|
|
72
|
+
return hasFiles;
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Check whether persona.md has a non-empty body (excluding scene navigation).
|
|
80
|
+
* Returns false if the file doesn't exist, is empty, or only contains
|
|
81
|
+
* scene navigation (no actual persona content).
|
|
82
|
+
*/
|
|
83
|
+
async hasPersonaBody() {
|
|
84
|
+
const personaPath = path.join(this.dataDir, "persona.md");
|
|
85
|
+
try {
|
|
86
|
+
const raw = await fs.readFile(personaPath, "utf-8");
|
|
87
|
+
const body = stripSceneNavigation(raw).trim();
|
|
88
|
+
return body.length > 0;
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|