dsh-tdai-memory 0.3.4 → 0.3.6

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.
Files changed (66) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +13 -4
  3. package/README.zh.md +13 -4
  4. package/THIRD-PARTY-NOTICES.md +20 -20
  5. package/cordis.patch.yml +8 -8
  6. package/index.js +13 -13
  7. package/package.json +9 -6
  8. package/recall-inject.js +86 -86
  9. package/vendor/tdai/adapters/index.js +15 -15
  10. package/vendor/tdai/adapters/openclaw/host-adapter.js +83 -83
  11. package/vendor/tdai/adapters/openclaw/index.js +5 -5
  12. package/vendor/tdai/adapters/openclaw/llm-runner.js +69 -69
  13. package/vendor/tdai/adapters/standalone/host-adapter.js +58 -58
  14. package/vendor/tdai/adapters/standalone/index.js +5 -5
  15. package/vendor/tdai/core/conversation/l0-recorder.js +420 -420
  16. package/vendor/tdai/core/hooks/auto-capture.js +236 -236
  17. package/vendor/tdai/core/hooks/auto-recall.js +640 -640
  18. package/vendor/tdai/core/index.js +8 -8
  19. package/vendor/tdai/core/persona/persona-generator.js +184 -184
  20. package/vendor/tdai/core/persona/persona-trigger.js +94 -94
  21. package/vendor/tdai/core/profile/profile-sync.js +200 -200
  22. package/vendor/tdai/core/prompts/l1-dedup.js +72 -72
  23. package/vendor/tdai/core/prompts/l1-extraction.js +34 -34
  24. package/vendor/tdai/core/prompts/persona-generation.js +35 -35
  25. package/vendor/tdai/core/prompts/scene-extraction.js +45 -45
  26. package/vendor/tdai/core/record/l1-dedup.js +318 -318
  27. package/vendor/tdai/core/record/l1-extractor.js +379 -379
  28. package/vendor/tdai/core/record/l1-reader.js +178 -178
  29. package/vendor/tdai/core/record/l1-writer.js +141 -141
  30. package/vendor/tdai/core/report/reporter.js +86 -86
  31. package/vendor/tdai/core/scene/filename-normalizer.js +161 -161
  32. package/vendor/tdai/core/scene/scene-extractor.js +410 -410
  33. package/vendor/tdai/core/scene/scene-format.js +53 -53
  34. package/vendor/tdai/core/scene/scene-index.js +82 -82
  35. package/vendor/tdai/core/scene/scene-navigation.js +57 -57
  36. package/vendor/tdai/core/seed/input.js +404 -404
  37. package/vendor/tdai/core/seed/types.js +9 -9
  38. package/vendor/tdai/core/store/bm25-client.js +117 -117
  39. package/vendor/tdai/core/store/bm25-local.js +68 -68
  40. package/vendor/tdai/core/store/embedding.js +632 -632
  41. package/vendor/tdai/core/store/factory.js +95 -95
  42. package/vendor/tdai/core/store/search-utils.js +54 -54
  43. package/vendor/tdai/core/store/sqlite.js +1632 -1632
  44. package/vendor/tdai/core/store/tcvdb-client.js +221 -221
  45. package/vendor/tdai/core/store/tcvdb.js +1047 -1047
  46. package/vendor/tdai/core/store/types.js +17 -17
  47. package/vendor/tdai/core/tools/conversation-search.js +199 -199
  48. package/vendor/tdai/core/tools/memory-search.js +207 -207
  49. package/vendor/tdai/core/types.js +12 -12
  50. package/vendor/tdai/utils/backup.js +189 -189
  51. package/vendor/tdai/utils/checkpoint.js +360 -360
  52. package/vendor/tdai/utils/clean-context-runner.js +472 -472
  53. package/vendor/tdai/utils/ensure-hook-policy.js +213 -213
  54. package/vendor/tdai/utils/env.js +13 -13
  55. package/vendor/tdai/utils/managed-timer.js +127 -127
  56. package/vendor/tdai/utils/manifest.js +91 -91
  57. package/vendor/tdai/utils/memory-cleaner.js +300 -300
  58. package/vendor/tdai/utils/no-think-fetch.js +109 -109
  59. package/vendor/tdai/utils/openclaw-state-dir.js +26 -26
  60. package/vendor/tdai/utils/pipeline-factory.js +517 -517
  61. package/vendor/tdai/utils/pipeline-manager.js +892 -892
  62. package/vendor/tdai/utils/sanitize.js +360 -360
  63. package/vendor/tdai/utils/serial-queue.js +103 -103
  64. package/vendor/tdai/utils/session-filter.js +88 -88
  65. package/vendor/tdai/utils/text-utils.js +29 -29
  66. 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
+ }