openclaw-cortex-memory 0.1.0-Alpha.8 → 0.1.0
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 -0
- package/README.md +347 -299
- package/SIGNATURE.md +7 -0
- package/SKILL.md +96 -350
- package/dist/index.d.ts +93 -23
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1234 -1318
- package/dist/index.js.map +1 -1
- package/dist/openclaw.plugin.json +377 -18
- package/dist/src/dedup/three_stage_deduplicator.d.ts.map +1 -1
- package/dist/src/dedup/three_stage_deduplicator.js +13 -3
- package/dist/src/dedup/three_stage_deduplicator.js.map +1 -1
- package/dist/src/engine/memory_engine.d.ts +6 -1
- package/dist/src/engine/memory_engine.d.ts.map +1 -1
- package/dist/src/engine/ts_engine.d.ts +208 -0
- package/dist/src/engine/ts_engine.d.ts.map +1 -1
- package/dist/src/engine/ts_engine.js +1353 -84
- package/dist/src/engine/ts_engine.js.map +1 -1
- package/dist/src/engine/types.d.ts +27 -0
- package/dist/src/engine/types.d.ts.map +1 -1
- package/dist/src/graph/ontology.d.ts +87 -15
- package/dist/src/graph/ontology.d.ts.map +1 -1
- package/dist/src/graph/ontology.js +999 -12
- package/dist/src/graph/ontology.js.map +1 -1
- package/dist/src/net/http_post.d.ts +17 -0
- package/dist/src/net/http_post.d.ts.map +1 -0
- package/dist/src/net/http_post.js +56 -0
- package/dist/src/net/http_post.js.map +1 -0
- package/dist/src/quality/llm_output_validator.d.ts +65 -0
- package/dist/src/quality/llm_output_validator.d.ts.map +1 -0
- package/dist/src/quality/llm_output_validator.js +635 -0
- package/dist/src/quality/llm_output_validator.js.map +1 -0
- package/dist/src/reflect/reflector.d.ts.map +1 -1
- package/dist/src/reflect/reflector.js +296 -26
- package/dist/src/reflect/reflector.js.map +1 -1
- package/dist/src/rules/rule_store.d.ts.map +1 -1
- package/dist/src/rules/rule_store.js +75 -16
- package/dist/src/rules/rule_store.js.map +1 -1
- package/dist/src/session/session_end.d.ts +20 -42
- package/dist/src/session/session_end.d.ts.map +1 -1
- package/dist/src/session/session_end.js +31 -214
- package/dist/src/session/session_end.js.map +1 -1
- package/dist/src/store/archive_store.d.ts +52 -7
- package/dist/src/store/archive_store.d.ts.map +1 -1
- package/dist/src/store/archive_store.js +526 -96
- package/dist/src/store/archive_store.js.map +1 -1
- package/dist/src/store/embedding_utils.d.ts +32 -0
- package/dist/src/store/embedding_utils.d.ts.map +1 -0
- package/dist/src/store/embedding_utils.js +173 -0
- package/dist/src/store/embedding_utils.js.map +1 -0
- package/dist/src/store/graph_memory_store.d.ts +115 -0
- package/dist/src/store/graph_memory_store.d.ts.map +1 -0
- package/dist/src/store/graph_memory_store.js +1061 -0
- package/dist/src/store/graph_memory_store.js.map +1 -0
- package/dist/src/store/read_store.d.ts +95 -0
- package/dist/src/store/read_store.d.ts.map +1 -1
- package/dist/src/store/read_store.js +2108 -268
- package/dist/src/store/read_store.js.map +1 -1
- package/dist/src/store/vector_store.d.ts +15 -0
- package/dist/src/store/vector_store.d.ts.map +1 -1
- package/dist/src/store/vector_store.js +75 -1
- package/dist/src/store/vector_store.js.map +1 -1
- package/dist/src/store/write_store.d.ts +46 -0
- package/dist/src/store/write_store.d.ts.map +1 -1
- package/dist/src/store/write_store.js +399 -50
- package/dist/src/store/write_store.js.map +1 -1
- package/dist/src/sync/session_sync.d.ts +115 -2
- package/dist/src/sync/session_sync.d.ts.map +1 -1
- package/dist/src/sync/session_sync.js +2497 -44
- package/dist/src/sync/session_sync.js.map +1 -1
- package/dist/src/utils/runtime_env.d.ts +4 -0
- package/dist/src/utils/runtime_env.d.ts.map +1 -0
- package/dist/src/utils/runtime_env.js +51 -0
- package/dist/src/utils/runtime_env.js.map +1 -0
- package/dist/src/wiki/wiki_linter.d.ts +26 -0
- package/dist/src/wiki/wiki_linter.d.ts.map +1 -0
- package/dist/src/wiki/wiki_linter.js +339 -0
- package/dist/src/wiki/wiki_linter.js.map +1 -0
- package/dist/src/wiki/wiki_logger.d.ts +10 -0
- package/dist/src/wiki/wiki_logger.d.ts.map +1 -0
- package/dist/src/wiki/wiki_logger.js +78 -0
- package/dist/src/wiki/wiki_logger.js.map +1 -0
- package/dist/src/wiki/wiki_maintainer.d.ts +39 -0
- package/dist/src/wiki/wiki_maintainer.d.ts.map +1 -0
- package/dist/src/wiki/wiki_maintainer.js +38 -0
- package/dist/src/wiki/wiki_maintainer.js.map +1 -0
- package/dist/src/wiki/wiki_projector.d.ts +35 -0
- package/dist/src/wiki/wiki_projector.d.ts.map +1 -0
- package/dist/src/wiki/wiki_projector.js +1151 -0
- package/dist/src/wiki/wiki_projector.js.map +1 -0
- package/dist/src/wiki/wiki_queue.d.ts +29 -0
- package/dist/src/wiki/wiki_queue.d.ts.map +1 -0
- package/dist/src/wiki/wiki_queue.js +137 -0
- package/dist/src/wiki/wiki_queue.js.map +1 -0
- package/openclaw.plugin.json +377 -18
- package/package.json +52 -5
- package/schema/graph.schema.yaml +330 -0
- package/scripts/cli.js +80 -26
- package/scripts/repair-memory.js +321 -0
- package/scripts/uninstall.js +7 -1
- package/skills/cortex-memory/SKILL.md +83 -0
- package/skills/cortex-memory/references/agent-manual.md +127 -0
- package/skills/cortex-memory/references/configuration.md +109 -0
- package/skills/cortex-memory/references/publish-checklist.md +45 -0
- package/skills/cortex-memory/references/system-prompt-template.md +27 -0
- package/skills/cortex-memory/references/tools.md +191 -0
|
@@ -37,6 +37,10 @@ exports.createSessionSync = createSessionSync;
|
|
|
37
37
|
const crypto = __importStar(require("crypto"));
|
|
38
38
|
const fs = __importStar(require("fs"));
|
|
39
39
|
const path = __importStar(require("path"));
|
|
40
|
+
const http_post_1 = require("../net/http_post");
|
|
41
|
+
const ontology_1 = require("../graph/ontology");
|
|
42
|
+
const llm_output_validator_1 = require("../quality/llm_output_validator");
|
|
43
|
+
const runtime_env_1 = require("../utils/runtime_env");
|
|
40
44
|
function asRecord(value) {
|
|
41
45
|
if (typeof value === "object" && value !== null) {
|
|
42
46
|
return value;
|
|
@@ -51,26 +55,64 @@ function firstString(values) {
|
|
|
51
55
|
}
|
|
52
56
|
return undefined;
|
|
53
57
|
}
|
|
58
|
+
function extractTextFromContent(content) {
|
|
59
|
+
if (typeof content === "string" && content.trim()) {
|
|
60
|
+
return content.trim();
|
|
61
|
+
}
|
|
62
|
+
if (!Array.isArray(content)) {
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
const parts = [];
|
|
66
|
+
for (const item of content) {
|
|
67
|
+
if (typeof item === "string" && item.trim()) {
|
|
68
|
+
parts.push(item.trim());
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
const obj = asRecord(item);
|
|
72
|
+
if (!obj) {
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
const text = firstString([obj.text, obj.content, obj.summary, obj.message, obj.body]);
|
|
76
|
+
if (text) {
|
|
77
|
+
parts.push(text);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (parts.length === 0) {
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
return parts.join("\n");
|
|
84
|
+
}
|
|
85
|
+
function extractTextFromMessageRecord(record) {
|
|
86
|
+
const contentText = extractTextFromContent(record.content);
|
|
87
|
+
return firstString([contentText, record.text, record.summary, record.message, record.body]);
|
|
88
|
+
}
|
|
89
|
+
const SYNC_STATE_VERSION = "2";
|
|
90
|
+
function createDefaultState() {
|
|
91
|
+
return { version: SYNC_STATE_VERSION, files: {}, markdowns: {} };
|
|
92
|
+
}
|
|
54
93
|
function readState(filePath) {
|
|
55
94
|
try {
|
|
56
95
|
if (!fs.existsSync(filePath)) {
|
|
57
|
-
return
|
|
96
|
+
return createDefaultState();
|
|
58
97
|
}
|
|
59
98
|
const content = fs.readFileSync(filePath, "utf-8").trim();
|
|
60
99
|
if (!content) {
|
|
61
|
-
return
|
|
100
|
+
return createDefaultState();
|
|
62
101
|
}
|
|
63
102
|
const parsed = JSON.parse(content);
|
|
64
103
|
if (!parsed.files || typeof parsed.files !== "object") {
|
|
65
|
-
return
|
|
104
|
+
return createDefaultState();
|
|
66
105
|
}
|
|
67
106
|
if (!parsed.markdowns || typeof parsed.markdowns !== "object") {
|
|
68
107
|
parsed.markdowns = {};
|
|
69
108
|
}
|
|
109
|
+
parsed.version = typeof parsed.version === "string" && parsed.version.trim()
|
|
110
|
+
? parsed.version
|
|
111
|
+
: SYNC_STATE_VERSION;
|
|
70
112
|
return parsed;
|
|
71
113
|
}
|
|
72
114
|
catch {
|
|
73
|
-
return
|
|
115
|
+
return createDefaultState();
|
|
74
116
|
}
|
|
75
117
|
}
|
|
76
118
|
function writeState(filePath, state) {
|
|
@@ -78,9 +120,10 @@ function writeState(filePath, state) {
|
|
|
78
120
|
if (!fs.existsSync(dir)) {
|
|
79
121
|
fs.mkdirSync(dir, { recursive: true });
|
|
80
122
|
}
|
|
123
|
+
state.version = SYNC_STATE_VERSION;
|
|
81
124
|
fs.writeFileSync(filePath, JSON.stringify(state, null, 2), "utf-8");
|
|
82
125
|
}
|
|
83
|
-
function gatherSessionFiles(openclawBasePath, memoryRoot) {
|
|
126
|
+
function gatherSessionFiles(openclawBasePath, memoryRoot, includeLocalActiveInput) {
|
|
84
127
|
const results = new Set();
|
|
85
128
|
const openclawSessionsDir = path.join(openclawBasePath, "agents", "main", "sessions");
|
|
86
129
|
const localActiveFile = path.join(memoryRoot, "sessions", "active", "sessions.jsonl");
|
|
@@ -91,7 +134,7 @@ function gatherSessionFiles(openclawBasePath, memoryRoot) {
|
|
|
91
134
|
}
|
|
92
135
|
}
|
|
93
136
|
}
|
|
94
|
-
if (fs.existsSync(localActiveFile) && fs.statSync(localActiveFile).isFile()) {
|
|
137
|
+
if (includeLocalActiveInput && fs.existsSync(localActiveFile) && fs.statSync(localActiveFile).isFile()) {
|
|
95
138
|
results.add(localActiveFile);
|
|
96
139
|
}
|
|
97
140
|
return [...results];
|
|
@@ -114,19 +157,19 @@ function gatherDailySummaryFiles(openclawBasePath) {
|
|
|
114
157
|
return files;
|
|
115
158
|
}
|
|
116
159
|
function inferOpenclawBasePath(projectRoot) {
|
|
117
|
-
const configPath =
|
|
160
|
+
const configPath = (0, runtime_env_1.getEnvValue)("OPENCLAW_CONFIG_PATH");
|
|
118
161
|
if (configPath && fs.existsSync(configPath)) {
|
|
119
162
|
return path.dirname(configPath);
|
|
120
163
|
}
|
|
121
|
-
const stateDir =
|
|
164
|
+
const stateDir = (0, runtime_env_1.getEnvValue)("OPENCLAW_STATE_DIR");
|
|
122
165
|
if (stateDir && fs.existsSync(stateDir)) {
|
|
123
166
|
return stateDir;
|
|
124
167
|
}
|
|
125
|
-
const basePath =
|
|
168
|
+
const basePath = (0, runtime_env_1.getEnvValue)("OPENCLAW_BASE_PATH");
|
|
126
169
|
if (basePath && fs.existsSync(basePath)) {
|
|
127
170
|
return basePath;
|
|
128
171
|
}
|
|
129
|
-
const home =
|
|
172
|
+
const home = (0, runtime_env_1.getHomeDir)();
|
|
130
173
|
if (home) {
|
|
131
174
|
const defaultPath = path.join(home, ".openclaw");
|
|
132
175
|
if (fs.existsSync(defaultPath)) {
|
|
@@ -146,7 +189,7 @@ function extractMessages(record) {
|
|
|
146
189
|
const obj = asRecord(item);
|
|
147
190
|
if (!obj)
|
|
148
191
|
continue;
|
|
149
|
-
const text =
|
|
192
|
+
const text = extractTextFromMessageRecord(obj);
|
|
150
193
|
if (!text)
|
|
151
194
|
continue;
|
|
152
195
|
const role = firstString([obj.role, obj.senderRole, obj.fromRole]) || "unknown";
|
|
@@ -156,19 +199,31 @@ function extractMessages(record) {
|
|
|
156
199
|
return output;
|
|
157
200
|
}
|
|
158
201
|
}
|
|
159
|
-
const
|
|
202
|
+
const nestedMessage = asRecord(record.message);
|
|
203
|
+
if (nestedMessage) {
|
|
204
|
+
const text = extractTextFromMessageRecord(nestedMessage);
|
|
205
|
+
if (text) {
|
|
206
|
+
return [{ role: firstString([nestedMessage.role, record.role, record.senderRole, record.fromRole]) || "unknown", text }];
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
const text = extractTextFromMessageRecord(record);
|
|
160
210
|
if (text) {
|
|
161
211
|
return [{ role: firstString([record.role, record.senderRole, record.fromRole]) || "unknown", text }];
|
|
162
212
|
}
|
|
163
213
|
return [];
|
|
164
214
|
}
|
|
165
215
|
function getSessionId(record, fallbackSeed) {
|
|
216
|
+
const sessionObj = asRecord(record.session);
|
|
217
|
+
const type = firstString([record.type])?.toLowerCase();
|
|
218
|
+
const typeScopedId = type === "session" ? firstString([record.id]) : undefined;
|
|
166
219
|
return (firstString([
|
|
167
220
|
record.sessionId,
|
|
168
221
|
record.session_id,
|
|
169
222
|
record.conversationId,
|
|
170
223
|
record.conversation_id,
|
|
171
|
-
|
|
224
|
+
sessionObj?.id,
|
|
225
|
+
sessionObj?.sessionId,
|
|
226
|
+
typeScopedId,
|
|
172
227
|
]) || `sync:${fallbackSeed}`);
|
|
173
228
|
}
|
|
174
229
|
function parseDailySummary(content) {
|
|
@@ -198,10 +253,2358 @@ function parseDailySummary(content) {
|
|
|
198
253
|
}
|
|
199
254
|
return chunks.map(chunk => chunk.trim()).filter(chunk => chunk.length >= 10);
|
|
200
255
|
}
|
|
256
|
+
function normalizeBaseUrl(value) {
|
|
257
|
+
if (!value)
|
|
258
|
+
return "";
|
|
259
|
+
return value.endsWith("/") ? value.slice(0, -1) : value;
|
|
260
|
+
}
|
|
261
|
+
function resolveWriteCharLimit(value, minLimit, fallbackLimit) {
|
|
262
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
263
|
+
return Math.max(minLimit, Math.floor(value));
|
|
264
|
+
}
|
|
265
|
+
return fallbackLimit;
|
|
266
|
+
}
|
|
267
|
+
function tailByCharLimit(text, maxChars) {
|
|
268
|
+
const source = (text || "").trim();
|
|
269
|
+
if (!source)
|
|
270
|
+
return "";
|
|
271
|
+
if (!Number.isFinite(maxChars) || maxChars <= 0 || source.length <= maxChars) {
|
|
272
|
+
return source;
|
|
273
|
+
}
|
|
274
|
+
return source.slice(-Math.floor(maxChars)).trim();
|
|
275
|
+
}
|
|
276
|
+
function normalizeOneLineText(value) {
|
|
277
|
+
return String(value || "").replace(/\s+/g, " ").trim();
|
|
278
|
+
}
|
|
279
|
+
const LOW_INFORMATION_LINE = /^(ok|okay|got it|roger|noted|sure|thanks|thank you|received|copy that|understood|好的|收到|明白|了解|谢谢|感谢|可以|行|嗯|嗯嗯|没问题)(?:\b|$)/i;
|
|
280
|
+
const LOW_VALUE_ONLY_LINE = /^(ok|okay|got it|roger|noted|thanks|thank you|received|copy that|understood|sounds good|好的|收到|明白|了解|谢谢|感谢|可以|行|嗯|嗯嗯|没问题|辛苦了)[\s.!?,。!?、]*$/i;
|
|
281
|
+
const ACTIVE_VALUE_SIGNAL_PATTERN = /(decision|trade-?off|constraint|requirement|fix|error|exception|blocked|rollback|deploy|progress|milestone|action item|owner|next step|todo|deadline|eta|issue|bug|metric|latency|error rate|cost|url|link|path|file|config|parameter|version|commit|pr|ticket|决策|决定|取舍|约束|需求|要求|修复|错误|异常|阻塞|回滚|部署|进展|里程碑|行动项|负责人|下一步|待办|截止|问题|缺陷|指标|延迟|成本|链接|路径|文件|配置|参数|版本|提交|工单|测试|验证|通过|失败|成功|优化|导入|记忆|wiki)/i;
|
|
282
|
+
const ACTIVE_VALUE_EVIDENCE_PATTERN = /(https?:\/\/|www\.|[`#/:\\]|[A-Za-z]:\\|\/[A-Za-z0-9._\-\/]+|\b\d+(?:\.\d+)?%?\b|#\d{1,8})/;
|
|
283
|
+
const EVENT_SNIPPET_SIGNAL_PATTERN = /(decision|fix|error|exception|blocked|deploy|progress|action item|owner|resolved|depends|complete|completed|implemented|passed|accepted|approved|review|investigate|optimi[sz]e|决策|决定|修复|错误|异常|阻塞|部署|进展|行动项|负责人|解决|依赖|完成|已完成|实现|已实现|通过|验证|接受|确认|评审|检查|分析|优化|提高|改进|导入|生成)/i;
|
|
284
|
+
const USER_TASK_LINE_PATTERN = /(please|can you|need to|task|implement|fix|investigate|optimi[sz]e|deploy|enable|review|请|需要|帮我|麻烦|进入|开始|实现|修复|检查|分析|优化|部署|启用|评审|导入|生成|提高|改进|看一遍|处理|完成)/i;
|
|
285
|
+
const COMPLETION_LINE_PATTERN = /(done|completed|fixed|implemented|deployed|resolved|report|summary|finished|已完成|完成了|修复了|实现了|已修复|已实现|已部署|已通过|通过了|验证通过|构建通过|测试通过|总结|报告|处理完成)/i;
|
|
286
|
+
const ACCEPTANCE_LINE_PATTERN = /(approved|accepted|looks good|great|works|thank you|confirmed|ok|可以|好的|行|没问题|通过|接受|确认|效果可以|继续|谢谢|辛苦)/i;
|
|
287
|
+
function denoiseTranscriptForWrite(transcript) {
|
|
288
|
+
const raw = (transcript || "").trim();
|
|
289
|
+
if (!raw)
|
|
290
|
+
return "";
|
|
291
|
+
const output = [];
|
|
292
|
+
const seen = new Set();
|
|
293
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
294
|
+
const trimmed = line.trim();
|
|
295
|
+
if (!trimmed)
|
|
296
|
+
continue;
|
|
297
|
+
const content = trimmed.replace(/^\[[^\]]+\]\s*/, "").trim();
|
|
298
|
+
if (!content)
|
|
299
|
+
continue;
|
|
300
|
+
const hasSignal = /(https?:\/\/|www\.|[A-Za-z0-9._-]+\.[A-Za-z]{2,}|[`#/:\\]|@\w+|\b\d{2,}\b)/.test(content);
|
|
301
|
+
if (!hasSignal && LOW_INFORMATION_LINE.test(content)) {
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
const dedupKey = content.toLowerCase();
|
|
305
|
+
if (!hasSignal && seen.has(dedupKey)) {
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
seen.add(dedupKey);
|
|
309
|
+
output.push(trimmed);
|
|
310
|
+
}
|
|
311
|
+
return output.length > 0 ? output.join("\n") : raw;
|
|
312
|
+
}
|
|
313
|
+
function hasValuableActiveContent(text) {
|
|
314
|
+
const source = (text || "").trim();
|
|
315
|
+
if (!source) {
|
|
316
|
+
return false;
|
|
317
|
+
}
|
|
318
|
+
const normalized = source
|
|
319
|
+
.replace(/^\[[^\]]+\]\s*/, "")
|
|
320
|
+
.replace(/\s+/g, " ")
|
|
321
|
+
.trim();
|
|
322
|
+
if (!normalized) {
|
|
323
|
+
return false;
|
|
324
|
+
}
|
|
325
|
+
const hasSignal = ACTIVE_VALUE_SIGNAL_PATTERN.test(normalized);
|
|
326
|
+
const hasEvidence = ACTIVE_VALUE_EVIDENCE_PATTERN.test(normalized);
|
|
327
|
+
if (LOW_VALUE_ONLY_LINE.test(normalized) && !hasSignal && !hasEvidence) {
|
|
328
|
+
return false;
|
|
329
|
+
}
|
|
330
|
+
if (!hasSignal && !hasEvidence) {
|
|
331
|
+
return false;
|
|
332
|
+
}
|
|
333
|
+
if (normalized.length < 20 && !hasSignal) {
|
|
334
|
+
return false;
|
|
335
|
+
}
|
|
336
|
+
return true;
|
|
337
|
+
}
|
|
338
|
+
function limitSnippetText(text, maxChars = 8000) {
|
|
339
|
+
if (text.length <= maxChars) {
|
|
340
|
+
return text;
|
|
341
|
+
}
|
|
342
|
+
const headChars = Math.max(1000, Math.floor(maxChars * 0.45));
|
|
343
|
+
const tailChars = Math.max(1000, maxChars - headChars - 32);
|
|
344
|
+
return `${text.slice(0, headChars).trim()}\n[...snip...]\n${text.slice(-tailChars).trim()}`;
|
|
345
|
+
}
|
|
346
|
+
function buildEventSnippet(text) {
|
|
347
|
+
const lines = text
|
|
348
|
+
.split(/\r?\n/)
|
|
349
|
+
.map(line => line.trim())
|
|
350
|
+
.filter(Boolean)
|
|
351
|
+
.filter(line => line.length >= 8);
|
|
352
|
+
const selected = new Map();
|
|
353
|
+
const add = (index) => {
|
|
354
|
+
if (index >= 0 && index < lines.length) {
|
|
355
|
+
selected.set(index, lines[index]);
|
|
356
|
+
}
|
|
357
|
+
};
|
|
358
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
359
|
+
const line = lines[index];
|
|
360
|
+
if (EVENT_SNIPPET_SIGNAL_PATTERN.test(line)
|
|
361
|
+
|| ACTIVE_VALUE_EVIDENCE_PATTERN.test(line)
|
|
362
|
+
|| USER_TASK_LINE_PATTERN.test(line)
|
|
363
|
+
|| COMPLETION_LINE_PATTERN.test(line)
|
|
364
|
+
|| ACCEPTANCE_LINE_PATTERN.test(line)) {
|
|
365
|
+
add(index);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
for (let index = 0; index < Math.min(5, lines.length); index += 1) {
|
|
369
|
+
if (USER_TASK_LINE_PATTERN.test(lines[index]) || ACTIVE_VALUE_EVIDENCE_PATTERN.test(lines[index])) {
|
|
370
|
+
add(index);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
for (let index = Math.max(0, lines.length - 12); index < lines.length; index += 1) {
|
|
374
|
+
add(index);
|
|
375
|
+
}
|
|
376
|
+
if (selected.size === 0) {
|
|
377
|
+
lines.slice(-20).forEach((line, offset) => selected.set(lines.length - Math.min(20, lines.length) + offset, line));
|
|
378
|
+
}
|
|
379
|
+
const ordered = [...selected.entries()]
|
|
380
|
+
.sort((a, b) => a[0] - b[0])
|
|
381
|
+
.map(([, line]) => line);
|
|
382
|
+
const capped = ordered.length > 50
|
|
383
|
+
? [...ordered.slice(0, 5), ...ordered.slice(-45)]
|
|
384
|
+
: ordered;
|
|
385
|
+
return limitSnippetText(capped.join("\n"));
|
|
386
|
+
}
|
|
387
|
+
const TASK_INSTRUCTION_PATTERNS = [
|
|
388
|
+
/please|can you|need to|task|implement|fix|investigate|optimi[sz]e|deploy|enable|review/i,
|
|
389
|
+
/请|需要|帮我|麻烦|进入|开始|实现|修复|检查|分析|优化|部署|启用|评审|导入|生成|提高|改进|看一遍|处理|完成/,
|
|
390
|
+
];
|
|
391
|
+
const COMPLETION_REPORT_PATTERNS = [
|
|
392
|
+
/done|completed|fixed|implemented|deployed|resolved|report|summary|finished/i,
|
|
393
|
+
/已完成|完成了|修复了|实现了|已修复|已实现|已部署|已通过|通过了|验证通过|构建通过|测试通过|总结|报告|处理完成/,
|
|
394
|
+
];
|
|
395
|
+
const USER_ACCEPTANCE_PATTERNS = [
|
|
396
|
+
/approved|accepted|looks good|great|works|thank you|confirmed|ok/i,
|
|
397
|
+
/可以|好的|行|没问题|通过|接受|确认|效果可以|继续|谢谢|辛苦/,
|
|
398
|
+
];
|
|
399
|
+
const FAILURE_PATTERNS = [
|
|
400
|
+
/failed|error|exception|blocked|timeout|rollback|incident/i,
|
|
401
|
+
/失败|报错|错误|异常|阻塞|超时|回滚|事故|不通过|无法/,
|
|
402
|
+
];
|
|
403
|
+
const SUCCESS_PATTERNS = [
|
|
404
|
+
/success|completed|fixed|resolved|passed|stable|recovered|works/i,
|
|
405
|
+
/成功|完成|修复|解决|通过|稳定|恢复|可用|生效/,
|
|
406
|
+
];
|
|
407
|
+
const USER_ROLE_HINT = /^(user|human|customer|client|用户|人类)/i;
|
|
408
|
+
const AGENT_ROLE_HINT = /^(assistant|agent|ai|system|openclaw|claude|gpt|助手|助理|模型|codex)/i;
|
|
409
|
+
function matchesAnyPattern(text, patterns) {
|
|
410
|
+
return patterns.some(pattern => pattern.test(text));
|
|
411
|
+
}
|
|
412
|
+
function parseTranscriptLines(transcript) {
|
|
413
|
+
const lines = transcript
|
|
414
|
+
.split(/\r?\n/)
|
|
415
|
+
.map(line => line.trim())
|
|
416
|
+
.filter(Boolean);
|
|
417
|
+
return lines.map(line => {
|
|
418
|
+
const matched = line.match(/^\[([^\]]+)\]\s*(.*)$/);
|
|
419
|
+
if (!matched) {
|
|
420
|
+
return { role: "unknown", text: line };
|
|
421
|
+
}
|
|
422
|
+
return {
|
|
423
|
+
role: matched[1].trim().toLowerCase(),
|
|
424
|
+
text: (matched[2] || "").trim(),
|
|
425
|
+
};
|
|
426
|
+
}).filter(item => item.text.length > 0);
|
|
427
|
+
}
|
|
428
|
+
function summarizeForArchive(text, maxChars) {
|
|
429
|
+
void maxChars;
|
|
430
|
+
const normalized = text.replace(/\s+/g, " ").trim();
|
|
431
|
+
return normalized;
|
|
432
|
+
}
|
|
433
|
+
function evaluateTaskLifecycle(transcript) {
|
|
434
|
+
const parsed = parseTranscriptLines(transcript);
|
|
435
|
+
let taskText = "";
|
|
436
|
+
let reportText = "";
|
|
437
|
+
let acceptanceText = "";
|
|
438
|
+
let firstFailureIndex = -1;
|
|
439
|
+
let firstSuccessIndex = -1;
|
|
440
|
+
let hasTaskInstruction = false;
|
|
441
|
+
let hasCompletionReport = false;
|
|
442
|
+
let hasUserAcceptance = false;
|
|
443
|
+
let hasFailure = false;
|
|
444
|
+
let hasSuccess = false;
|
|
445
|
+
for (let i = 0; i < parsed.length; i += 1) {
|
|
446
|
+
const line = parsed[i];
|
|
447
|
+
const role = line.role;
|
|
448
|
+
const text = line.text;
|
|
449
|
+
const userLike = USER_ROLE_HINT.test(role) || role === "unknown";
|
|
450
|
+
const agentLike = AGENT_ROLE_HINT.test(role) || role === "unknown";
|
|
451
|
+
if (!hasTaskInstruction && userLike && matchesAnyPattern(text, TASK_INSTRUCTION_PATTERNS)) {
|
|
452
|
+
hasTaskInstruction = true;
|
|
453
|
+
taskText = text;
|
|
454
|
+
}
|
|
455
|
+
if (!hasCompletionReport && agentLike && matchesAnyPattern(text, COMPLETION_REPORT_PATTERNS)) {
|
|
456
|
+
hasCompletionReport = true;
|
|
457
|
+
reportText = text;
|
|
458
|
+
}
|
|
459
|
+
if (!hasUserAcceptance && userLike && matchesAnyPattern(text, USER_ACCEPTANCE_PATTERNS)) {
|
|
460
|
+
hasUserAcceptance = true;
|
|
461
|
+
acceptanceText = text;
|
|
462
|
+
}
|
|
463
|
+
if (matchesAnyPattern(text, FAILURE_PATTERNS)) {
|
|
464
|
+
hasFailure = true;
|
|
465
|
+
if (firstFailureIndex < 0) {
|
|
466
|
+
firstFailureIndex = i;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
if (matchesAnyPattern(text, SUCCESS_PATTERNS)) {
|
|
470
|
+
hasSuccess = true;
|
|
471
|
+
if (firstSuccessIndex < 0) {
|
|
472
|
+
firstSuccessIndex = i;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
if (!hasTaskInstruction) {
|
|
477
|
+
const fallback = parsed.find(item => matchesAnyPattern(item.text, TASK_INSTRUCTION_PATTERNS));
|
|
478
|
+
if (fallback) {
|
|
479
|
+
hasTaskInstruction = true;
|
|
480
|
+
taskText = fallback.text;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
if (!hasCompletionReport) {
|
|
484
|
+
const fallback = parsed.find(item => matchesAnyPattern(item.text, COMPLETION_REPORT_PATTERNS));
|
|
485
|
+
if (fallback) {
|
|
486
|
+
hasCompletionReport = true;
|
|
487
|
+
reportText = fallback.text;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
if (!hasUserAcceptance) {
|
|
491
|
+
const fallback = parsed.find(item => matchesAnyPattern(item.text, USER_ACCEPTANCE_PATTERNS));
|
|
492
|
+
if (fallback) {
|
|
493
|
+
hasUserAcceptance = true;
|
|
494
|
+
acceptanceText = fallback.text;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
const failThenSuccess = hasFailure && hasSuccess && firstFailureIndex >= 0 && firstSuccessIndex > firstFailureIndex;
|
|
498
|
+
return {
|
|
499
|
+
hasTaskInstruction,
|
|
500
|
+
hasCompletionReport,
|
|
501
|
+
hasUserAcceptance,
|
|
502
|
+
hasFailure,
|
|
503
|
+
hasSuccess,
|
|
504
|
+
failThenSuccess,
|
|
505
|
+
lifecycleComplete: hasTaskInstruction && hasCompletionReport && hasUserAcceptance,
|
|
506
|
+
taskText,
|
|
507
|
+
reportText,
|
|
508
|
+
acceptanceText,
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
function appendLifecycleArchiveDecision(decisions, transcript, logger) {
|
|
512
|
+
if (decisions.some(item => item.target_layer === "archive_event")) {
|
|
513
|
+
return decisions;
|
|
514
|
+
}
|
|
515
|
+
const lifecycle = evaluateTaskLifecycle(transcript);
|
|
516
|
+
if (!lifecycle.lifecycleComplete) {
|
|
517
|
+
return decisions;
|
|
518
|
+
}
|
|
519
|
+
const fallbackGraph = buildStablePersonalFactGraph(transcript) || buildLifecycleTaskGraph(lifecycle, transcript);
|
|
520
|
+
const summary = lifecycle.failThenSuccess
|
|
521
|
+
? "Task lifecycle closed: user request, failure iteration, final completion, and user acceptance."
|
|
522
|
+
: "Task lifecycle closed: user request, completion report, and user acceptance.";
|
|
523
|
+
const cause = lifecycle.taskText
|
|
524
|
+
? summarizeForArchive(lifecycle.taskText, 220)
|
|
525
|
+
: "User issued a concrete task request.";
|
|
526
|
+
const process = lifecycle.reportText
|
|
527
|
+
? summarizeForArchive(lifecycle.reportText, 320)
|
|
528
|
+
: "Agent executed the requested work and provided a completion report.";
|
|
529
|
+
const result = lifecycle.acceptanceText
|
|
530
|
+
? summarizeForArchive(lifecycle.acceptanceText, 220)
|
|
531
|
+
: "User acknowledged and accepted the delivery.";
|
|
532
|
+
const confidence = lifecycle.failThenSuccess ? 0.88 : 0.76;
|
|
533
|
+
const candidateDigest = crypto.createHash("sha1")
|
|
534
|
+
.update([summary, cause, process, result].join("\n"))
|
|
535
|
+
.digest("hex")
|
|
536
|
+
.slice(0, 12);
|
|
537
|
+
const fallbackDecision = {
|
|
538
|
+
candidate_id: `lifecycle_${candidateDigest}`,
|
|
539
|
+
target_layer: "archive_event",
|
|
540
|
+
candidate_text: buildLifecycleSourceSlice(lifecycle, transcript),
|
|
541
|
+
event: {
|
|
542
|
+
event_type: lifecycle.failThenSuccess ? "retrospective" : "milestone",
|
|
543
|
+
summary,
|
|
544
|
+
cause,
|
|
545
|
+
process,
|
|
546
|
+
result,
|
|
547
|
+
entities: fallbackGraph?.entities,
|
|
548
|
+
relations: fallbackGraph?.relations,
|
|
549
|
+
entity_types: fallbackGraph?.entity_types,
|
|
550
|
+
confidence: typeof fallbackGraph?.confidence === "number" ? fallbackGraph.confidence : confidence,
|
|
551
|
+
},
|
|
552
|
+
graph: fallbackGraph
|
|
553
|
+
? {
|
|
554
|
+
summary: ensureSummaryMentionsEntities(summary, fallbackGraph.entities),
|
|
555
|
+
entities: fallbackGraph.entities,
|
|
556
|
+
relations: fallbackGraph.relations,
|
|
557
|
+
entity_types: fallbackGraph.entity_types,
|
|
558
|
+
confidence: fallbackGraph.confidence,
|
|
559
|
+
}
|
|
560
|
+
: undefined,
|
|
561
|
+
reason: "lifecycle_archive_fallback",
|
|
562
|
+
};
|
|
563
|
+
logger.info(`sync_archive_fallback_applied reason=task_lifecycle_complete graph=${fallbackGraph ? "derived" : "none"}`);
|
|
564
|
+
return [...decisions, fallbackDecision];
|
|
565
|
+
}
|
|
566
|
+
const GRAPH_REWRITE_SCOPE_FIELDS = [
|
|
567
|
+
"summary",
|
|
568
|
+
"source_text_nav",
|
|
569
|
+
"entities",
|
|
570
|
+
"entity_types",
|
|
571
|
+
"relations",
|
|
572
|
+
"confidence",
|
|
573
|
+
];
|
|
574
|
+
const GRAPH_REWRITE_SCOPE_SET = new Set(GRAPH_REWRITE_SCOPE_FIELDS);
|
|
575
|
+
const WRITE_GATE_PROMPT_VERSION = "write-gate.v1.7.9";
|
|
576
|
+
const WRITE_GATE_STAGE_AB_PROMPT_VERSION = "write-gate.ab.v1.1.6";
|
|
577
|
+
const WRITE_GATE_STAGE_C_PROMPT_VERSION = "write-gate.c.v1.5.1";
|
|
578
|
+
const WRITE_GATE_STAGE_D_PROMPT_VERSION = "write-gate.d.v1.1.0";
|
|
579
|
+
const WRITE_GATE_GRAPH_REWRITE_PROMPT_VERSION = "write-gate.graph-rewrite.v1.1.0";
|
|
580
|
+
const WRITE_GATE_REGRESSION_SAMPLES = [
|
|
581
|
+
"Example A: \"Discussed three options today, no final decision yet\" => active_only",
|
|
582
|
+
"Example B: \"Decided to use plan B and completed rollout, error rate dropped to 0.2%\" => archive_event",
|
|
583
|
+
"Example C: \"ok received thanks\" => skip",
|
|
584
|
+
"Example D: \"请优化 openclaw-cortex-memory 的历史记忆导入质量;已修复并通过 npm run typecheck;用户确认可以\" => archive_event",
|
|
585
|
+
"Example E: \"收到,辛苦了\" => skip",
|
|
586
|
+
];
|
|
587
|
+
function buildActiveValuePromptHint(schema) {
|
|
588
|
+
const pick = (source, wanted) => {
|
|
589
|
+
const available = new Set(source.map(item => item.toLowerCase()));
|
|
590
|
+
return wanted.filter(item => available.has(item.toLowerCase()));
|
|
591
|
+
};
|
|
592
|
+
const eventSignals = pick(schema.eventTypes || [], [
|
|
593
|
+
"decision",
|
|
594
|
+
"issue",
|
|
595
|
+
"fix",
|
|
596
|
+
"constraint",
|
|
597
|
+
"requirement",
|
|
598
|
+
"blocker",
|
|
599
|
+
"dependency",
|
|
600
|
+
"action_item",
|
|
601
|
+
"follow_up",
|
|
602
|
+
"milestone",
|
|
603
|
+
"risk",
|
|
604
|
+
]);
|
|
605
|
+
const entitySignals = pick(schema.entityTypes || [], [
|
|
606
|
+
"Project",
|
|
607
|
+
"Task",
|
|
608
|
+
"Document",
|
|
609
|
+
"ConfigFile",
|
|
610
|
+
"Date",
|
|
611
|
+
"Person",
|
|
612
|
+
"Team",
|
|
613
|
+
"Resource",
|
|
614
|
+
]);
|
|
615
|
+
const aliasCanonicals = [
|
|
616
|
+
"Person",
|
|
617
|
+
"Resource",
|
|
618
|
+
"Document",
|
|
619
|
+
"ConfigFile",
|
|
620
|
+
"Project",
|
|
621
|
+
"Task",
|
|
622
|
+
"Team",
|
|
623
|
+
"Date",
|
|
624
|
+
"Issue",
|
|
625
|
+
"Fix",
|
|
626
|
+
"Milestone",
|
|
627
|
+
];
|
|
628
|
+
const aliasHints = aliasCanonicals
|
|
629
|
+
.map(canonical => {
|
|
630
|
+
const aliases = Array.isArray(schema.entityAliases?.[canonical])
|
|
631
|
+
? schema.entityAliases[canonical].slice(0, 4)
|
|
632
|
+
: [];
|
|
633
|
+
if (aliases.length === 0) {
|
|
634
|
+
return "";
|
|
635
|
+
}
|
|
636
|
+
return `${canonical}(${aliases.join("/")})`;
|
|
637
|
+
})
|
|
638
|
+
.filter(Boolean);
|
|
639
|
+
const eventAliases = pick(Object.keys(schema.eventTypeAliases || {}), [
|
|
640
|
+
"next_step",
|
|
641
|
+
"next_action",
|
|
642
|
+
"deadline",
|
|
643
|
+
"roadblock",
|
|
644
|
+
"bug",
|
|
645
|
+
"error",
|
|
646
|
+
"problem",
|
|
647
|
+
"workaround",
|
|
648
|
+
]);
|
|
649
|
+
return [
|
|
650
|
+
"Valuable active_only signals must be dictionary-grounded.",
|
|
651
|
+
`Use schema event types such as: ${eventSignals.join(", ")}.`,
|
|
652
|
+
`Use schema entity types such as: ${entitySignals.join(", ")}.`,
|
|
653
|
+
aliasHints.length > 0
|
|
654
|
+
? `Use schema entity aliases such as: ${aliasHints.join(", ")}.`
|
|
655
|
+
: "",
|
|
656
|
+
eventAliases.length > 0
|
|
657
|
+
? `Use schema event aliases such as: ${eventAliases.join(", ")}.`
|
|
658
|
+
: "",
|
|
659
|
+
].join(" ");
|
|
660
|
+
}
|
|
661
|
+
function buildEntityDictionaryPromptHint(schema) {
|
|
662
|
+
const aliases = Object.fromEntries(Object.entries(schema.entityAliases || {}).map(([canonical, values]) => [
|
|
663
|
+
canonical,
|
|
664
|
+
Array.isArray(values) ? values.slice(0, 8) : [],
|
|
665
|
+
]));
|
|
666
|
+
return `Entity dictionary (authoritative for concrete entities): ${JSON.stringify({
|
|
667
|
+
entity_types: schema.entityTypes || [],
|
|
668
|
+
entity_aliases: aliases,
|
|
669
|
+
})}`;
|
|
670
|
+
}
|
|
671
|
+
function buildStablePersonalFactGraph(text) {
|
|
672
|
+
const source = (text || "").trim();
|
|
673
|
+
if (!source)
|
|
674
|
+
return null;
|
|
675
|
+
const genericTokens = new Set([
|
|
676
|
+
"user", "person", "people", "system", "assistant", "agent",
|
|
677
|
+
]);
|
|
678
|
+
const normalizeToken = (value) => value.trim().toLowerCase();
|
|
679
|
+
const isConcreteName = (value) => {
|
|
680
|
+
const name = value.trim();
|
|
681
|
+
if (!name || name.length < 2)
|
|
682
|
+
return false;
|
|
683
|
+
if (genericTokens.has(normalizeToken(name)))
|
|
684
|
+
return false;
|
|
685
|
+
if (/^(wife|husband|spouse|child|kid|children)$/i.test(name))
|
|
686
|
+
return false;
|
|
687
|
+
return true;
|
|
688
|
+
};
|
|
689
|
+
const extractConcreteName = (candidate) => {
|
|
690
|
+
const cleaned = (candidate || "").trim().replace(/[.,;:!?]+$/g, "");
|
|
691
|
+
return isConcreteName(cleaned) ? cleaned : "";
|
|
692
|
+
};
|
|
693
|
+
const findSubjectName = () => {
|
|
694
|
+
const patterns = [
|
|
695
|
+
/([A-Za-z][A-Za-z0-9._-]{1,40})\s*'s\s*(wife|husband|spouse|child|kid|daughter|son)\b/i,
|
|
696
|
+
/\b([A-Z][a-zA-Z0-9._-]{1,40})\b/,
|
|
697
|
+
];
|
|
698
|
+
for (const pattern of patterns) {
|
|
699
|
+
const hit = source.match(pattern);
|
|
700
|
+
const candidate = hit ? extractConcreteName(hit[1] || "") : "";
|
|
701
|
+
if (candidate)
|
|
702
|
+
return candidate;
|
|
703
|
+
}
|
|
704
|
+
return "";
|
|
705
|
+
};
|
|
706
|
+
const subjectName = findSubjectName();
|
|
707
|
+
if (!subjectName)
|
|
708
|
+
return null;
|
|
709
|
+
const entities = new Set([subjectName]);
|
|
710
|
+
const entity_types = { [subjectName]: "Person" };
|
|
711
|
+
const relations = [];
|
|
712
|
+
const relationKeys = new Set();
|
|
713
|
+
const addRelation = (relation) => {
|
|
714
|
+
const relationKey = `${relation.source}|${relation.type}|${relation.target}`;
|
|
715
|
+
if (relationKeys.has(relationKey))
|
|
716
|
+
return;
|
|
717
|
+
relationKeys.add(relationKey);
|
|
718
|
+
relations.push(relation);
|
|
719
|
+
};
|
|
720
|
+
const spouseNameHit = source.match(/(?:wife|husband|spouse)(?:\s*(?:named|is|:|-)?\s*)?([A-Za-z][A-Za-z0-9._-]{1,40})/i);
|
|
721
|
+
const spouseName = spouseNameHit ? extractConcreteName(spouseNameHit[1] || "") : "";
|
|
722
|
+
if (spouseName) {
|
|
723
|
+
entities.add(spouseName);
|
|
724
|
+
entity_types[spouseName] = "FamilyMember";
|
|
725
|
+
addRelation({
|
|
726
|
+
source: subjectName,
|
|
727
|
+
target: spouseName,
|
|
728
|
+
type: "has_spouse",
|
|
729
|
+
evidence_span: spouseName,
|
|
730
|
+
context_chunk: source.slice(0, 160).trim(),
|
|
731
|
+
confidence: 0.9,
|
|
732
|
+
});
|
|
733
|
+
}
|
|
734
|
+
const childNameHit = source.match(/(?:child|kid|daughter|son)(?:\s*(?:named|is|:|-)?\s*)?([A-Za-z][A-Za-z0-9._-]{1,40})/i);
|
|
735
|
+
const childName = childNameHit ? extractConcreteName(childNameHit[1] || "") : "";
|
|
736
|
+
if (childName) {
|
|
737
|
+
entities.add(childName);
|
|
738
|
+
entity_types[childName] = "FamilyMember";
|
|
739
|
+
addRelation({
|
|
740
|
+
source: subjectName,
|
|
741
|
+
target: childName,
|
|
742
|
+
type: "has_child",
|
|
743
|
+
evidence_span: childName,
|
|
744
|
+
context_chunk: source.slice(0, 160).trim(),
|
|
745
|
+
confidence: 0.88,
|
|
746
|
+
});
|
|
747
|
+
}
|
|
748
|
+
const birthdayMatch = source.match(/birthday[^\n]*?(\d{4}-\d{2}-\d{2}|\d{1,2}[/-]\d{1,2})/i);
|
|
749
|
+
if (birthdayMatch && spouseName) {
|
|
750
|
+
const dateEntity = birthdayMatch[1];
|
|
751
|
+
entities.add(dateEntity);
|
|
752
|
+
entity_types[dateEntity] = "Date";
|
|
753
|
+
addRelation({
|
|
754
|
+
source: spouseName,
|
|
755
|
+
target: dateEntity,
|
|
756
|
+
type: "birthday_on",
|
|
757
|
+
evidence_span: birthdayMatch[1],
|
|
758
|
+
context_chunk: source.slice(0, 160).trim(),
|
|
759
|
+
confidence: 0.92,
|
|
760
|
+
});
|
|
761
|
+
}
|
|
762
|
+
if (birthdayMatch && childName) {
|
|
763
|
+
const dateEntity = birthdayMatch[1];
|
|
764
|
+
entities.add(dateEntity);
|
|
765
|
+
entity_types[dateEntity] = "Date";
|
|
766
|
+
addRelation({
|
|
767
|
+
source: childName,
|
|
768
|
+
target: dateEntity,
|
|
769
|
+
type: "birthday_on",
|
|
770
|
+
evidence_span: birthdayMatch[1],
|
|
771
|
+
context_chunk: source.slice(0, 160).trim(),
|
|
772
|
+
confidence: 0.9,
|
|
773
|
+
});
|
|
774
|
+
}
|
|
775
|
+
const anniversaryMatch = source.match(/anniversary[^\n]*?(\d{4}-\d{2}-\d{2}|\d{1,2}[/-]\d{1,2})/i);
|
|
776
|
+
if (anniversaryMatch) {
|
|
777
|
+
const dateEntity = anniversaryMatch[1];
|
|
778
|
+
entities.add(dateEntity);
|
|
779
|
+
entity_types[dateEntity] = "Date";
|
|
780
|
+
addRelation({
|
|
781
|
+
source: subjectName,
|
|
782
|
+
target: dateEntity,
|
|
783
|
+
type: "anniversary_on",
|
|
784
|
+
evidence_span: anniversaryMatch[1],
|
|
785
|
+
context_chunk: source.slice(0, 160).trim(),
|
|
786
|
+
confidence: 0.9,
|
|
787
|
+
});
|
|
788
|
+
}
|
|
789
|
+
if (relations.length === 0)
|
|
790
|
+
return null;
|
|
791
|
+
return {
|
|
792
|
+
entities: [...entities],
|
|
793
|
+
entity_types,
|
|
794
|
+
relations,
|
|
795
|
+
confidence: 0.9,
|
|
796
|
+
};
|
|
797
|
+
}
|
|
798
|
+
function cleanLifecycleEntityName(value) {
|
|
799
|
+
return (value || "")
|
|
800
|
+
.replace(/\s+/g, " ")
|
|
801
|
+
.replace(/^[`"'“”‘’\s]+|[`"'“”‘’\s]+$/g, "")
|
|
802
|
+
.replace(/[。.!?!?;;,,::]+$/g, "")
|
|
803
|
+
.trim();
|
|
804
|
+
}
|
|
805
|
+
function compactLifecycleTaskName(text) {
|
|
806
|
+
const source = cleanLifecycleEntityName(text);
|
|
807
|
+
if (!source)
|
|
808
|
+
return "";
|
|
809
|
+
const withoutPolitePrefix = source
|
|
810
|
+
.replace(/^(please|can you|could you|need to|task:?)\s+/i, "")
|
|
811
|
+
.replace(/^(请|需要|帮我|麻烦|进入|开始)\s*/u, "")
|
|
812
|
+
.trim();
|
|
813
|
+
const firstClause = (withoutPolitePrefix || source)
|
|
814
|
+
.split(/[。.!?!?;;\n]/)
|
|
815
|
+
.map(item => item.trim())
|
|
816
|
+
.find(Boolean) || source;
|
|
817
|
+
if (firstClause.length <= 90) {
|
|
818
|
+
return cleanLifecycleEntityName(firstClause);
|
|
819
|
+
}
|
|
820
|
+
return cleanLifecycleEntityName(firstClause.slice(0, 90));
|
|
821
|
+
}
|
|
822
|
+
function classifyLifecycleTarget(rawValue) {
|
|
823
|
+
const raw = cleanLifecycleEntityName(rawValue);
|
|
824
|
+
if (!raw || raw.length < 2)
|
|
825
|
+
return null;
|
|
826
|
+
const pathLike = /[A-Za-z]:\\|[\\/]/.test(raw);
|
|
827
|
+
const basename = pathLike
|
|
828
|
+
? cleanLifecycleEntityName(raw.split(/[\\/]/).filter(Boolean).pop() || raw)
|
|
829
|
+
: raw;
|
|
830
|
+
const name = cleanLifecycleEntityName(basename);
|
|
831
|
+
if (!name || /^(project|repo|repository|system|plugin|task|memory|wiki|项目|工程|仓库|插件|系统|任务)$/i.test(name)) {
|
|
832
|
+
return null;
|
|
833
|
+
}
|
|
834
|
+
if (/\.(json|jsonc|toml|ya?ml|ini|env|config)$/i.test(name)) {
|
|
835
|
+
return { name, type: "ConfigFile", evidence: raw, score: pathLike ? 95 : 85 };
|
|
836
|
+
}
|
|
837
|
+
if (/\.(ts|tsx|js|jsx|mjs|cjs|md|mdx|txt|py|go|rs|java|cs|gd)$/i.test(name)) {
|
|
838
|
+
return { name, type: "Document", evidence: raw, score: pathLike ? 90 : 80 };
|
|
839
|
+
}
|
|
840
|
+
if (/[A-Za-z][A-Za-z0-9]+(?:-[A-Za-z0-9]+)+/.test(name)) {
|
|
841
|
+
return { name, type: "Project", evidence: raw, score: 88 };
|
|
842
|
+
}
|
|
843
|
+
if (/openclaw|cortex|memory|wiki|plugin|gateway|worker|sdk/i.test(name)) {
|
|
844
|
+
return { name, type: "Project", evidence: raw, score: 76 };
|
|
845
|
+
}
|
|
846
|
+
if (/^[A-Za-z0-9_.:-]{3,80}$/.test(name)) {
|
|
847
|
+
return { name, type: "Resource", evidence: raw, score: 55 };
|
|
848
|
+
}
|
|
849
|
+
return null;
|
|
850
|
+
}
|
|
851
|
+
function pickLifecycleTarget(transcript) {
|
|
852
|
+
const source = transcript || "";
|
|
853
|
+
const candidates = [];
|
|
854
|
+
const addCandidate = (raw) => {
|
|
855
|
+
const classified = classifyLifecycleTarget(raw);
|
|
856
|
+
if (!classified)
|
|
857
|
+
return;
|
|
858
|
+
if (candidates.some(item => item.name.toLowerCase() === classified.name.toLowerCase())) {
|
|
859
|
+
return;
|
|
860
|
+
}
|
|
861
|
+
candidates.push(classified);
|
|
862
|
+
};
|
|
863
|
+
for (const match of source.matchAll(/`([^`]{2,180})`/g)) {
|
|
864
|
+
addCandidate(match[1] || "");
|
|
865
|
+
}
|
|
866
|
+
for (const match of source.matchAll(/[A-Za-z]:\\[^\s"'`<>]+|(?:\.{1,2}\/)?[A-Za-z0-9_.-]+(?:\/[A-Za-z0-9_.-]+)+/g)) {
|
|
867
|
+
addCandidate(match[0] || "");
|
|
868
|
+
}
|
|
869
|
+
for (const match of source.matchAll(/\b[A-Za-z][A-Za-z0-9]+(?:-[A-Za-z0-9]+){1,8}\b/g)) {
|
|
870
|
+
addCandidate(match[0] || "");
|
|
871
|
+
}
|
|
872
|
+
for (const match of source.matchAll(/(?:项目|工程|仓库|插件)\s*[`"“]?([A-Za-z0-9_.:-]{2,100})[`"”]?/g)) {
|
|
873
|
+
addCandidate(match[1] || "");
|
|
874
|
+
}
|
|
875
|
+
candidates.sort((a, b) => b.score - a.score);
|
|
876
|
+
const picked = candidates[0];
|
|
877
|
+
return picked ? { name: picked.name, type: picked.type, evidence: picked.evidence } : null;
|
|
878
|
+
}
|
|
879
|
+
function buildLifecycleSourceSlice(lifecycle, transcript) {
|
|
880
|
+
const parts = [lifecycle.taskText, lifecycle.reportText, lifecycle.acceptanceText]
|
|
881
|
+
.map(item => item.trim())
|
|
882
|
+
.filter(Boolean);
|
|
883
|
+
return parts.length > 0 ? parts.join("\n") : buildEventSnippet(transcript);
|
|
884
|
+
}
|
|
885
|
+
function ensureSummaryMentionsEntities(summary, entities) {
|
|
886
|
+
const normalizedSummary = summary || "";
|
|
887
|
+
const missing = (entities || [])
|
|
888
|
+
.map(item => item.trim())
|
|
889
|
+
.filter(Boolean)
|
|
890
|
+
.filter(entity => !normalizedSummary.toLowerCase().includes(entity.toLowerCase()));
|
|
891
|
+
if (missing.length === 0) {
|
|
892
|
+
return normalizedSummary;
|
|
893
|
+
}
|
|
894
|
+
return `${normalizedSummary} Entities: ${missing.join(", ")}.`;
|
|
895
|
+
}
|
|
896
|
+
function buildLifecycleTaskGraph(lifecycle, transcript) {
|
|
897
|
+
const taskName = compactLifecycleTaskName(lifecycle.taskText || lifecycle.reportText || "");
|
|
898
|
+
const target = pickLifecycleTarget(transcript);
|
|
899
|
+
if (!taskName || !target || taskName.toLowerCase() === target.name.toLowerCase()) {
|
|
900
|
+
return null;
|
|
901
|
+
}
|
|
902
|
+
const relationType = target.type === "Project" ? "belongs_to" : "references";
|
|
903
|
+
const contextChunk = limitSnippetText(buildLifecycleSourceSlice(lifecycle, transcript), 420);
|
|
904
|
+
const evidenceSpan = target.evidence || target.name;
|
|
905
|
+
return {
|
|
906
|
+
entities: [taskName, target.name],
|
|
907
|
+
entity_types: {
|
|
908
|
+
[taskName]: "Task",
|
|
909
|
+
[target.name]: target.type,
|
|
910
|
+
},
|
|
911
|
+
relations: [
|
|
912
|
+
{
|
|
913
|
+
source: taskName,
|
|
914
|
+
target: target.name,
|
|
915
|
+
type: relationType,
|
|
916
|
+
relation_origin: "canonical",
|
|
917
|
+
evidence_span: evidenceSpan,
|
|
918
|
+
context_chunk: contextChunk || transcript.slice(0, 420).trim(),
|
|
919
|
+
confidence: 0.72,
|
|
920
|
+
},
|
|
921
|
+
],
|
|
922
|
+
confidence: 0.72,
|
|
923
|
+
};
|
|
924
|
+
}
|
|
925
|
+
function parseArchiveEventPayload(value) {
|
|
926
|
+
if (!value || typeof value !== "object") {
|
|
927
|
+
return null;
|
|
928
|
+
}
|
|
929
|
+
const obj = value;
|
|
930
|
+
const eventType = typeof obj.event_type === "string" ? obj.event_type.trim() : "";
|
|
931
|
+
const summary = typeof obj.summary === "string" ? obj.summary.trim() : "";
|
|
932
|
+
const cause = typeof obj.cause === "string" ? obj.cause.trim() : "";
|
|
933
|
+
const process = typeof obj.process === "string" ? obj.process.trim() : "";
|
|
934
|
+
const result = typeof obj.result === "string" ? obj.result.trim() : "";
|
|
935
|
+
if (!eventType || !summary || !cause || !process || !result) {
|
|
936
|
+
return null;
|
|
937
|
+
}
|
|
938
|
+
const entities = Array.isArray(obj.entities)
|
|
939
|
+
? obj.entities.map(v => (typeof v === "string" ? v.trim() : "")).filter(Boolean)
|
|
940
|
+
: [];
|
|
941
|
+
const relations = Array.isArray(obj.relations)
|
|
942
|
+
? obj.relations
|
|
943
|
+
.map(valueItem => {
|
|
944
|
+
if (!valueItem || typeof valueItem !== "object")
|
|
945
|
+
return null;
|
|
946
|
+
const relation = valueItem;
|
|
947
|
+
const source = typeof relation.source === "string" ? relation.source.trim() : "";
|
|
948
|
+
const target = typeof relation.target === "string" ? relation.target.trim() : "";
|
|
949
|
+
const type = typeof relation.type === "string" ? relation.type.trim() : "";
|
|
950
|
+
const relationOrigin = typeof relation.relation_origin === "string" ? relation.relation_origin.trim() : "";
|
|
951
|
+
const relationDefinition = typeof relation.relation_definition === "string" ? relation.relation_definition.trim() : "";
|
|
952
|
+
const mappingHint = typeof relation.mapping_hint === "string" ? relation.mapping_hint.trim() : "";
|
|
953
|
+
const evidenceSpan = typeof relation.evidence_span === "string" ? relation.evidence_span.trim() : "";
|
|
954
|
+
const confidence = typeof relation.confidence === "number"
|
|
955
|
+
? Math.max(0, Math.min(1, relation.confidence))
|
|
956
|
+
: undefined;
|
|
957
|
+
if (!source || !target || !type)
|
|
958
|
+
return null;
|
|
959
|
+
return {
|
|
960
|
+
source,
|
|
961
|
+
target,
|
|
962
|
+
type,
|
|
963
|
+
relation_origin: relationOrigin || undefined,
|
|
964
|
+
relation_definition: relationDefinition || undefined,
|
|
965
|
+
mapping_hint: mappingHint || undefined,
|
|
966
|
+
evidence_span: evidenceSpan || undefined,
|
|
967
|
+
confidence,
|
|
968
|
+
};
|
|
969
|
+
})
|
|
970
|
+
.filter(Boolean)
|
|
971
|
+
: [];
|
|
972
|
+
const entity_types = typeof obj.entity_types === "object" && obj.entity_types !== null && !Array.isArray(obj.entity_types)
|
|
973
|
+
? Object.fromEntries(Object.entries(obj.entity_types)
|
|
974
|
+
.filter(([key, value]) => typeof key === "string" && key.trim().length > 0 && typeof value === "string" && value.trim().length > 0)
|
|
975
|
+
.map(([key, value]) => [key.trim(), value.trim()]))
|
|
976
|
+
: undefined;
|
|
977
|
+
return {
|
|
978
|
+
event_type: eventType,
|
|
979
|
+
summary,
|
|
980
|
+
cause,
|
|
981
|
+
process,
|
|
982
|
+
result,
|
|
983
|
+
entities,
|
|
984
|
+
entity_types,
|
|
985
|
+
relations,
|
|
986
|
+
confidence: typeof obj.confidence === "number" ? Math.max(0, Math.min(1, obj.confidence)) : 0.6,
|
|
987
|
+
};
|
|
988
|
+
}
|
|
989
|
+
function parseGraphPayload(value, options) {
|
|
990
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
991
|
+
return null;
|
|
992
|
+
}
|
|
993
|
+
const allowIncomplete = options?.allowIncomplete === true;
|
|
994
|
+
const obj = value;
|
|
995
|
+
const summary = typeof obj.summary === "string" ? obj.summary.trim() : "";
|
|
996
|
+
const sourceTextNavObj = asRecord(obj.source_text_nav);
|
|
997
|
+
const source_text_nav = sourceTextNavObj
|
|
998
|
+
? {
|
|
999
|
+
layer: typeof sourceTextNavObj.layer === "string" ? sourceTextNavObj.layer.trim() : undefined,
|
|
1000
|
+
session_id: typeof sourceTextNavObj.session_id === "string" ? sourceTextNavObj.session_id.trim() : undefined,
|
|
1001
|
+
source_file: typeof sourceTextNavObj.source_file === "string" ? sourceTextNavObj.source_file.trim() : undefined,
|
|
1002
|
+
source_memory_id: typeof sourceTextNavObj.source_memory_id === "string" ? sourceTextNavObj.source_memory_id.trim() : undefined,
|
|
1003
|
+
source_event_id: typeof sourceTextNavObj.source_event_id === "string" ? sourceTextNavObj.source_event_id.trim() : undefined,
|
|
1004
|
+
fulltext_anchor: typeof sourceTextNavObj.fulltext_anchor === "string" ? sourceTextNavObj.fulltext_anchor.trim() : undefined,
|
|
1005
|
+
}
|
|
1006
|
+
: undefined;
|
|
1007
|
+
const entities = Array.isArray(obj.entities)
|
|
1008
|
+
? obj.entities.map(v => (typeof v === "string" ? v.trim() : "")).filter(Boolean)
|
|
1009
|
+
: [];
|
|
1010
|
+
const entity_types = typeof obj.entity_types === "object" && obj.entity_types !== null && !Array.isArray(obj.entity_types)
|
|
1011
|
+
? Object.fromEntries(Object.entries(obj.entity_types)
|
|
1012
|
+
.filter(([key, val]) => typeof key === "string" && key.trim() && typeof val === "string" && val.trim())
|
|
1013
|
+
.map(([key, val]) => [key.trim(), val.trim()]))
|
|
1014
|
+
: undefined;
|
|
1015
|
+
const relations = Array.isArray(obj.relations)
|
|
1016
|
+
? obj.relations
|
|
1017
|
+
.map(item => {
|
|
1018
|
+
if (!item || typeof item !== "object")
|
|
1019
|
+
return null;
|
|
1020
|
+
const rel = item;
|
|
1021
|
+
const source = typeof rel.source === "string" ? rel.source.trim() : "";
|
|
1022
|
+
const target = typeof rel.target === "string" ? rel.target.trim() : "";
|
|
1023
|
+
const type = typeof rel.type === "string" && rel.type.trim() ? rel.type.trim() : "";
|
|
1024
|
+
if (!source || !target || !type)
|
|
1025
|
+
return null;
|
|
1026
|
+
const evidenceSpan = typeof rel.evidence_span === "string" ? rel.evidence_span.trim() : "";
|
|
1027
|
+
const confidence = typeof rel.confidence === "number"
|
|
1028
|
+
? Math.max(0, Math.min(1, rel.confidence))
|
|
1029
|
+
: undefined;
|
|
1030
|
+
const relationOrigin = typeof rel.relation_origin === "string" ? rel.relation_origin.trim() : "";
|
|
1031
|
+
const relationDefinition = typeof rel.relation_definition === "string" ? rel.relation_definition.trim() : "";
|
|
1032
|
+
const mappingHint = typeof rel.mapping_hint === "string" ? rel.mapping_hint.trim() : "";
|
|
1033
|
+
const contextChunk = typeof rel.context_chunk === "string" ? rel.context_chunk.trim() : "";
|
|
1034
|
+
if (!evidenceSpan || typeof confidence !== "number")
|
|
1035
|
+
return null;
|
|
1036
|
+
return {
|
|
1037
|
+
source,
|
|
1038
|
+
target,
|
|
1039
|
+
type,
|
|
1040
|
+
relation_origin: relationOrigin || undefined,
|
|
1041
|
+
relation_definition: relationDefinition || undefined,
|
|
1042
|
+
mapping_hint: mappingHint || undefined,
|
|
1043
|
+
evidence_span: evidenceSpan,
|
|
1044
|
+
context_chunk: contextChunk || undefined,
|
|
1045
|
+
confidence,
|
|
1046
|
+
};
|
|
1047
|
+
})
|
|
1048
|
+
.filter((item) => item !== null)
|
|
1049
|
+
: [];
|
|
1050
|
+
if (!allowIncomplete && (entities.length === 0 || relations.length === 0)) {
|
|
1051
|
+
return null;
|
|
1052
|
+
}
|
|
1053
|
+
const hasAnyField = Boolean(summary
|
|
1054
|
+
|| source_text_nav
|
|
1055
|
+
|| entities.length > 0
|
|
1056
|
+
|| relations.length > 0
|
|
1057
|
+
|| (entity_types && Object.keys(entity_types).length > 0)
|
|
1058
|
+
|| typeof obj.confidence === "number");
|
|
1059
|
+
if (!hasAnyField) {
|
|
1060
|
+
return null;
|
|
1061
|
+
}
|
|
1062
|
+
return {
|
|
1063
|
+
summary: summary || undefined,
|
|
1064
|
+
source_text_nav,
|
|
1065
|
+
entities: entities.length > 0 ? entities : undefined,
|
|
1066
|
+
entity_types: entity_types && Object.keys(entity_types).length > 0 ? entity_types : undefined,
|
|
1067
|
+
relations: relations.length > 0 ? relations : undefined,
|
|
1068
|
+
confidence: typeof obj.confidence === "number" ? Math.max(0, Math.min(1, obj.confidence)) : undefined,
|
|
1069
|
+
};
|
|
1070
|
+
}
|
|
1071
|
+
function toAppendableGraphPayload(payload) {
|
|
1072
|
+
if (!payload)
|
|
1073
|
+
return undefined;
|
|
1074
|
+
if (!Array.isArray(payload.entities) || payload.entities.length === 0)
|
|
1075
|
+
return undefined;
|
|
1076
|
+
if (!Array.isArray(payload.relations) || payload.relations.length === 0)
|
|
1077
|
+
return undefined;
|
|
1078
|
+
return {
|
|
1079
|
+
...payload,
|
|
1080
|
+
entities: payload.entities,
|
|
1081
|
+
relations: payload.relations,
|
|
1082
|
+
};
|
|
1083
|
+
}
|
|
1084
|
+
function mergeGraphPayload(base, patch) {
|
|
1085
|
+
if (!base && !patch)
|
|
1086
|
+
return undefined;
|
|
1087
|
+
if (!base)
|
|
1088
|
+
return toAppendableGraphPayload(patch);
|
|
1089
|
+
if (!patch)
|
|
1090
|
+
return toAppendableGraphPayload(base);
|
|
1091
|
+
const merged = {
|
|
1092
|
+
...base,
|
|
1093
|
+
...patch,
|
|
1094
|
+
summary: patch.summary || base.summary,
|
|
1095
|
+
source_text_nav: patch.source_text_nav || base.source_text_nav,
|
|
1096
|
+
entities: patch.entities && patch.entities.length > 0 ? patch.entities : base.entities,
|
|
1097
|
+
entity_types: patch.entity_types && Object.keys(patch.entity_types).length > 0 ? patch.entity_types : base.entity_types,
|
|
1098
|
+
relations: patch.relations && patch.relations.length > 0 ? patch.relations : base.relations,
|
|
1099
|
+
confidence: typeof patch.confidence === "number" ? patch.confidence : base.confidence,
|
|
1100
|
+
};
|
|
1101
|
+
return toAppendableGraphPayload(merged);
|
|
1102
|
+
}
|
|
1103
|
+
function normalizeRewriteScope(scope) {
|
|
1104
|
+
if (!Array.isArray(scope))
|
|
1105
|
+
return [];
|
|
1106
|
+
const output = [];
|
|
1107
|
+
const seen = new Set();
|
|
1108
|
+
for (const item of scope) {
|
|
1109
|
+
const key = typeof item === "string" ? item.trim() : "";
|
|
1110
|
+
if (!key || !GRAPH_REWRITE_SCOPE_SET.has(key) || seen.has(key)) {
|
|
1111
|
+
continue;
|
|
1112
|
+
}
|
|
1113
|
+
seen.add(key);
|
|
1114
|
+
output.push(key);
|
|
1115
|
+
}
|
|
1116
|
+
return output;
|
|
1117
|
+
}
|
|
1118
|
+
function normalizeSourceTextNavForCompare(value) {
|
|
1119
|
+
const nav = value || {};
|
|
1120
|
+
return JSON.stringify({
|
|
1121
|
+
layer: typeof nav.layer === "string" ? nav.layer.trim() : "",
|
|
1122
|
+
session_id: typeof nav.session_id === "string" ? nav.session_id.trim() : "",
|
|
1123
|
+
source_file: typeof nav.source_file === "string" ? nav.source_file.trim() : "",
|
|
1124
|
+
source_memory_id: typeof nav.source_memory_id === "string" ? nav.source_memory_id.trim() : "",
|
|
1125
|
+
source_event_id: typeof nav.source_event_id === "string" ? nav.source_event_id.trim() : "",
|
|
1126
|
+
fulltext_anchor: typeof nav.fulltext_anchor === "string" ? nav.fulltext_anchor.trim() : "",
|
|
1127
|
+
});
|
|
1128
|
+
}
|
|
1129
|
+
function normalizeEntitiesForCompare(value) {
|
|
1130
|
+
const entities = Array.isArray(value)
|
|
1131
|
+
? value.map(item => String(item || "").trim()).filter(Boolean)
|
|
1132
|
+
: [];
|
|
1133
|
+
entities.sort((a, b) => a.localeCompare(b, "en", { sensitivity: "base" }));
|
|
1134
|
+
return JSON.stringify(entities);
|
|
1135
|
+
}
|
|
1136
|
+
function normalizeEntityTypesForCompare(value) {
|
|
1137
|
+
const entries = Object.entries(value || {})
|
|
1138
|
+
.map(([entity, type]) => [String(entity || "").trim(), String(type || "").trim()])
|
|
1139
|
+
.filter(([entity, type]) => entity.length > 0 && type.length > 0)
|
|
1140
|
+
.sort((a, b) => {
|
|
1141
|
+
const keyA = `${a[0].toLowerCase()}|${a[1].toLowerCase()}`;
|
|
1142
|
+
const keyB = `${b[0].toLowerCase()}|${b[1].toLowerCase()}`;
|
|
1143
|
+
return keyA.localeCompare(keyB, "en", { sensitivity: "base" });
|
|
1144
|
+
});
|
|
1145
|
+
return JSON.stringify(entries);
|
|
1146
|
+
}
|
|
1147
|
+
function normalizeRelationsForCompare(value) {
|
|
1148
|
+
const rows = Array.isArray(value)
|
|
1149
|
+
? value.map(rel => ({
|
|
1150
|
+
source: String(rel.source || "").trim(),
|
|
1151
|
+
target: String(rel.target || "").trim(),
|
|
1152
|
+
type: String(rel.type || "").trim(),
|
|
1153
|
+
relation_origin: typeof rel.relation_origin === "string" ? rel.relation_origin.trim() : "",
|
|
1154
|
+
relation_definition: typeof rel.relation_definition === "string" ? rel.relation_definition.trim() : "",
|
|
1155
|
+
mapping_hint: typeof rel.mapping_hint === "string" ? rel.mapping_hint.trim() : "",
|
|
1156
|
+
evidence_span: typeof rel.evidence_span === "string" ? rel.evidence_span.trim() : "",
|
|
1157
|
+
context_chunk: typeof rel.context_chunk === "string" ? rel.context_chunk.trim() : "",
|
|
1158
|
+
confidence: typeof rel.confidence === "number" ? Number(rel.confidence.toFixed(6)) : null,
|
|
1159
|
+
}))
|
|
1160
|
+
: [];
|
|
1161
|
+
rows.sort((a, b) => JSON.stringify(a).localeCompare(JSON.stringify(b), "en", { sensitivity: "base" }));
|
|
1162
|
+
return JSON.stringify(rows);
|
|
1163
|
+
}
|
|
1164
|
+
function listChangedGraphFields(base, next) {
|
|
1165
|
+
const changed = [];
|
|
1166
|
+
if ((base?.summary || "").trim() !== (next?.summary || "").trim()) {
|
|
1167
|
+
changed.push("summary");
|
|
1168
|
+
}
|
|
1169
|
+
if (normalizeSourceTextNavForCompare(base?.source_text_nav) !== normalizeSourceTextNavForCompare(next?.source_text_nav)) {
|
|
1170
|
+
changed.push("source_text_nav");
|
|
1171
|
+
}
|
|
1172
|
+
if (normalizeEntitiesForCompare(base?.entities) !== normalizeEntitiesForCompare(next?.entities)) {
|
|
1173
|
+
changed.push("entities");
|
|
1174
|
+
}
|
|
1175
|
+
if (normalizeEntityTypesForCompare(base?.entity_types) !== normalizeEntityTypesForCompare(next?.entity_types)) {
|
|
1176
|
+
changed.push("entity_types");
|
|
1177
|
+
}
|
|
1178
|
+
if (normalizeRelationsForCompare(base?.relations) !== normalizeRelationsForCompare(next?.relations)) {
|
|
1179
|
+
changed.push("relations");
|
|
1180
|
+
}
|
|
1181
|
+
const baseConfidence = typeof base?.confidence === "number" ? Number(base.confidence.toFixed(6)) : null;
|
|
1182
|
+
const nextConfidence = typeof next?.confidence === "number" ? Number(next.confidence.toFixed(6)) : null;
|
|
1183
|
+
if (baseConfidence !== nextConfidence) {
|
|
1184
|
+
changed.push("confidence");
|
|
1185
|
+
}
|
|
1186
|
+
return changed;
|
|
1187
|
+
}
|
|
1188
|
+
function validateGraphRewriteCompleteness(payload, schema) {
|
|
1189
|
+
const validation = (0, llm_output_validator_1.validateGraphRewritePayload)(payload, { schema });
|
|
1190
|
+
return validation.valid ? [] : validation.errors;
|
|
1191
|
+
}
|
|
1192
|
+
function validateGraphRewriteResult(args) {
|
|
1193
|
+
const errors = validateGraphRewriteCompleteness(args.rewrittenPayload, args.schema);
|
|
1194
|
+
const changedFields = listChangedGraphFields(args.basePayload, args.rewrittenPayload);
|
|
1195
|
+
const scope = normalizeRewriteScope(args.rewriteScope);
|
|
1196
|
+
const implicitAllowedFields = new Set(["summary", "entities"]);
|
|
1197
|
+
if (Array.isArray(args.rewriteScope)) {
|
|
1198
|
+
const changedScopeControlled = changedFields.filter(field => !implicitAllowedFields.has(field));
|
|
1199
|
+
if (scope.length === 0 && changedScopeControlled.length > 0) {
|
|
1200
|
+
errors.push(`rewrite_scope_violation:empty_scope_changed:${changedScopeControlled.join(",")}`);
|
|
1201
|
+
}
|
|
1202
|
+
else if (scope.length > 0) {
|
|
1203
|
+
const scopeSet = new Set(scope);
|
|
1204
|
+
const outOfScope = changedFields.filter(field => !implicitAllowedFields.has(field) && !scopeSet.has(field));
|
|
1205
|
+
if (outOfScope.length > 0) {
|
|
1206
|
+
errors.push(`rewrite_scope_violation:${outOfScope.join(",")}`);
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
return {
|
|
1211
|
+
valid: errors.length === 0,
|
|
1212
|
+
errors,
|
|
1213
|
+
changedFields,
|
|
1214
|
+
};
|
|
1215
|
+
}
|
|
1216
|
+
function canonicalizeMergeEntityName(leftRaw, rightRaw) {
|
|
1217
|
+
const left = (leftRaw || "").trim();
|
|
1218
|
+
const right = (rightRaw || "").trim();
|
|
1219
|
+
if (!left)
|
|
1220
|
+
return right;
|
|
1221
|
+
if (!right)
|
|
1222
|
+
return left;
|
|
1223
|
+
const leftAscii = /^[\x00-\x7F]+$/.test(left);
|
|
1224
|
+
const rightAscii = /^[\x00-\x7F]+$/.test(right);
|
|
1225
|
+
if (leftAscii && !rightAscii)
|
|
1226
|
+
return right;
|
|
1227
|
+
if (!leftAscii && rightAscii)
|
|
1228
|
+
return left;
|
|
1229
|
+
return left.length >= right.length ? left : right;
|
|
1230
|
+
}
|
|
1231
|
+
function applyMergeHintToGraphPayload(args) {
|
|
1232
|
+
const warnings = [];
|
|
1233
|
+
const graphPayload = toAppendableGraphPayload(args.graphPayload);
|
|
1234
|
+
const mergeHint = args.mergeHint;
|
|
1235
|
+
if (!mergeHint) {
|
|
1236
|
+
return { graphPayload, warnings };
|
|
1237
|
+
}
|
|
1238
|
+
if (!graphPayload) {
|
|
1239
|
+
if (mergeHint.same_event === true) {
|
|
1240
|
+
warnings.push("same_event_merge_failed");
|
|
1241
|
+
}
|
|
1242
|
+
return { graphPayload, warnings };
|
|
1243
|
+
}
|
|
1244
|
+
let normalizedPayload = {
|
|
1245
|
+
...graphPayload,
|
|
1246
|
+
entities: Array.isArray(graphPayload.entities) ? [...graphPayload.entities] : [],
|
|
1247
|
+
entity_types: graphPayload.entity_types ? { ...graphPayload.entity_types } : {},
|
|
1248
|
+
relations: Array.isArray(graphPayload.relations) ? graphPayload.relations.map(item => ({ ...item })) : [],
|
|
1249
|
+
};
|
|
1250
|
+
const pairs = Array.isArray(mergeHint.same_entity_pairs) ? mergeHint.same_entity_pairs : [];
|
|
1251
|
+
if (pairs.length > 0) {
|
|
1252
|
+
const aliasToCanonical = new Map();
|
|
1253
|
+
const knownNames = new Set();
|
|
1254
|
+
for (const entity of normalizedPayload.entities || []) {
|
|
1255
|
+
knownNames.add(entity.trim().toLowerCase());
|
|
1256
|
+
}
|
|
1257
|
+
for (const relation of normalizedPayload.relations || []) {
|
|
1258
|
+
knownNames.add((relation.source || "").trim().toLowerCase());
|
|
1259
|
+
knownNames.add((relation.target || "").trim().toLowerCase());
|
|
1260
|
+
}
|
|
1261
|
+
let unresolvedPairCount = 0;
|
|
1262
|
+
for (const pair of pairs) {
|
|
1263
|
+
const left = (pair?.[0] || "").trim();
|
|
1264
|
+
const right = (pair?.[1] || "").trim();
|
|
1265
|
+
if (!left || !right)
|
|
1266
|
+
continue;
|
|
1267
|
+
const canonical = canonicalizeMergeEntityName(left, right);
|
|
1268
|
+
for (const alias of [left, right]) {
|
|
1269
|
+
aliasToCanonical.set(alias.toLowerCase(), canonical);
|
|
1270
|
+
}
|
|
1271
|
+
const hit = knownNames.has(left.toLowerCase()) || knownNames.has(right.toLowerCase());
|
|
1272
|
+
if (!hit) {
|
|
1273
|
+
unresolvedPairCount += 1;
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
const canonicalize = (value) => {
|
|
1277
|
+
const raw = (value || "").trim();
|
|
1278
|
+
if (!raw)
|
|
1279
|
+
return raw;
|
|
1280
|
+
return aliasToCanonical.get(raw.toLowerCase()) || raw;
|
|
1281
|
+
};
|
|
1282
|
+
const dedupedEntities = [];
|
|
1283
|
+
const entitySeen = new Set();
|
|
1284
|
+
for (const entity of normalizedPayload.entities || []) {
|
|
1285
|
+
const next = canonicalize(entity);
|
|
1286
|
+
const key = next.toLowerCase();
|
|
1287
|
+
if (!next || entitySeen.has(key))
|
|
1288
|
+
continue;
|
|
1289
|
+
entitySeen.add(key);
|
|
1290
|
+
dedupedEntities.push(next);
|
|
1291
|
+
}
|
|
1292
|
+
const mappedEntityTypes = {};
|
|
1293
|
+
for (const [name, type] of Object.entries(normalizedPayload.entity_types || {})) {
|
|
1294
|
+
const canonical = canonicalize(name);
|
|
1295
|
+
if (!canonical || !type)
|
|
1296
|
+
continue;
|
|
1297
|
+
if (!mappedEntityTypes[canonical]) {
|
|
1298
|
+
mappedEntityTypes[canonical] = type;
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
normalizedPayload = {
|
|
1302
|
+
...normalizedPayload,
|
|
1303
|
+
entities: dedupedEntities,
|
|
1304
|
+
entity_types: mappedEntityTypes,
|
|
1305
|
+
relations: (normalizedPayload.relations || []).map(relation => ({
|
|
1306
|
+
...relation,
|
|
1307
|
+
source: canonicalize(relation.source || ""),
|
|
1308
|
+
target: canonicalize(relation.target || ""),
|
|
1309
|
+
})),
|
|
1310
|
+
};
|
|
1311
|
+
if (unresolvedPairCount > 0) {
|
|
1312
|
+
warnings.push("same_entity_resolution_failed");
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
if (mergeHint.same_event === true) {
|
|
1316
|
+
const relationCount = Array.isArray(normalizedPayload.relations) ? normalizedPayload.relations.length : 0;
|
|
1317
|
+
if (relationCount === 0) {
|
|
1318
|
+
warnings.push("same_event_merge_failed");
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
return {
|
|
1322
|
+
graphPayload: toAppendableGraphPayload(normalizedPayload),
|
|
1323
|
+
warnings,
|
|
1324
|
+
};
|
|
1325
|
+
}
|
|
1326
|
+
function parseGateTargetLayer(value) {
|
|
1327
|
+
if (typeof value !== "string")
|
|
1328
|
+
return undefined;
|
|
1329
|
+
const layer = value.trim();
|
|
1330
|
+
if (layer === "active_only" || layer === "archive_event" || layer === "skip") {
|
|
1331
|
+
return layer;
|
|
1332
|
+
}
|
|
1333
|
+
return undefined;
|
|
1334
|
+
}
|
|
1335
|
+
function parseMergeHintPayload(value) {
|
|
1336
|
+
const hint = asRecord(value);
|
|
1337
|
+
if (!hint)
|
|
1338
|
+
return undefined;
|
|
1339
|
+
const candidateId = typeof hint.candidate_id === "string" ? hint.candidate_id.trim() : "";
|
|
1340
|
+
const sameEntityPairs = Array.isArray(hint.same_entity_pairs)
|
|
1341
|
+
? hint.same_entity_pairs
|
|
1342
|
+
.map(pair => {
|
|
1343
|
+
if (!Array.isArray(pair) || pair.length < 2)
|
|
1344
|
+
return null;
|
|
1345
|
+
const left = typeof pair[0] === "string" ? pair[0].trim() : "";
|
|
1346
|
+
const right = typeof pair[1] === "string" ? pair[1].trim() : "";
|
|
1347
|
+
if (!left || !right)
|
|
1348
|
+
return null;
|
|
1349
|
+
return [left, right];
|
|
1350
|
+
})
|
|
1351
|
+
.filter((item) => item !== null)
|
|
1352
|
+
: [];
|
|
1353
|
+
return {
|
|
1354
|
+
candidate_id: candidateId || undefined,
|
|
1355
|
+
same_event: hint.same_event === true,
|
|
1356
|
+
same_entity_pairs: sameEntityPairs.length > 0 ? sameEntityPairs : undefined,
|
|
1357
|
+
suggested_action: typeof hint.suggested_action === "string" ? hint.suggested_action.trim() : undefined,
|
|
1358
|
+
reason: typeof hint.reason === "string" ? hint.reason.trim() : undefined,
|
|
1359
|
+
};
|
|
1360
|
+
}
|
|
1361
|
+
function parseGraphRewritePlanPayload(value) {
|
|
1362
|
+
const plan = asRecord(value);
|
|
1363
|
+
if (!plan)
|
|
1364
|
+
return undefined;
|
|
1365
|
+
const rewriteRequired = plan.rewrite_required === true;
|
|
1366
|
+
const candidateId = typeof plan.candidate_id === "string" ? plan.candidate_id.trim() : "";
|
|
1367
|
+
const rewriteScopeValue = Array.isArray(plan.rewrite_scope) ? plan.rewrite_scope : null;
|
|
1368
|
+
const rewriteScopeProvided = Array.isArray(rewriteScopeValue);
|
|
1369
|
+
const rewriteScopeRaw = rewriteScopeProvided
|
|
1370
|
+
? rewriteScopeValue.map((item) => typeof item === "string" ? item.trim() : "").filter(Boolean)
|
|
1371
|
+
: [];
|
|
1372
|
+
const rewriteScope = normalizeRewriteScope(rewriteScopeRaw);
|
|
1373
|
+
const rewritePayloadRaw = asRecord(plan.graph_rewrite_payload) || asRecord(plan.graph_payload);
|
|
1374
|
+
const rewritePayload = parseGraphPayload(rewritePayloadRaw || undefined, { allowIncomplete: true }) || undefined;
|
|
1375
|
+
return {
|
|
1376
|
+
candidate_id: candidateId || undefined,
|
|
1377
|
+
rewrite_required: rewriteRequired,
|
|
1378
|
+
rewrite_reason: typeof plan.rewrite_reason === "string" ? plan.rewrite_reason.trim() : undefined,
|
|
1379
|
+
rewrite_scope: rewriteScopeProvided ? rewriteScope : undefined,
|
|
1380
|
+
graph_rewrite_payload: rewritePayload,
|
|
1381
|
+
};
|
|
1382
|
+
}
|
|
1383
|
+
function parseWritePlanDecisions(rootObj, logger, schema) {
|
|
1384
|
+
const writePlan = asRecord(rootObj.write_plan);
|
|
1385
|
+
if (!writePlan) {
|
|
1386
|
+
return [];
|
|
1387
|
+
}
|
|
1388
|
+
const trustedCandidateIds = new Set();
|
|
1389
|
+
const candidateRouteById = new Map();
|
|
1390
|
+
const candidateTextById = new Map();
|
|
1391
|
+
const candidateReasonById = new Map();
|
|
1392
|
+
const orderedCandidateIds = [];
|
|
1393
|
+
const candidates = Array.isArray(writePlan.candidates) ? writePlan.candidates : [];
|
|
1394
|
+
for (const candidateRaw of candidates) {
|
|
1395
|
+
const candidate = asRecord(candidateRaw);
|
|
1396
|
+
if (!candidate)
|
|
1397
|
+
continue;
|
|
1398
|
+
const candidateId = typeof candidate.candidate_id === "string" ? candidate.candidate_id.trim() : "";
|
|
1399
|
+
if (!candidateId)
|
|
1400
|
+
continue;
|
|
1401
|
+
trustedCandidateIds.add(candidateId);
|
|
1402
|
+
if (!orderedCandidateIds.includes(candidateId)) {
|
|
1403
|
+
orderedCandidateIds.push(candidateId);
|
|
1404
|
+
}
|
|
1405
|
+
const route = parseGateTargetLayer(candidate.route ?? candidate.target_layer);
|
|
1406
|
+
if (route) {
|
|
1407
|
+
candidateRouteById.set(candidateId, route);
|
|
1408
|
+
}
|
|
1409
|
+
const candidateText = firstString([candidate.normalized_text, candidate.span, candidate.candidate_text, candidate.text]) || "";
|
|
1410
|
+
if (candidateText) {
|
|
1411
|
+
candidateTextById.set(candidateId, candidateText);
|
|
1412
|
+
}
|
|
1413
|
+
const reason = typeof candidate.reason === "string" ? candidate.reason.trim() : "";
|
|
1414
|
+
if (reason) {
|
|
1415
|
+
candidateReasonById.set(candidateId, reason);
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
const activePayloadById = new Map();
|
|
1419
|
+
const activePayloads = Array.isArray(writePlan.active_payloads) ? writePlan.active_payloads : [];
|
|
1420
|
+
for (const payloadRaw of activePayloads) {
|
|
1421
|
+
const payload = asRecord(payloadRaw);
|
|
1422
|
+
if (!payload)
|
|
1423
|
+
continue;
|
|
1424
|
+
const candidateId = typeof payload.candidate_id === "string" ? payload.candidate_id.trim() : "";
|
|
1425
|
+
if (!candidateId)
|
|
1426
|
+
continue;
|
|
1427
|
+
trustedCandidateIds.add(candidateId);
|
|
1428
|
+
const sourceSlice = firstString([payload.source_slice, payload.sourceSlice]) || "";
|
|
1429
|
+
if (sourceSlice && !candidateTextById.has(candidateId)) {
|
|
1430
|
+
candidateTextById.set(candidateId, sourceSlice);
|
|
1431
|
+
}
|
|
1432
|
+
const activeSummary = normalizeOneLineText(firstString([payload.summary, payload.active_summary, payload.activeSummary]) || "");
|
|
1433
|
+
if (activeSummary || sourceSlice) {
|
|
1434
|
+
activePayloadById.set(candidateId, {
|
|
1435
|
+
summary: activeSummary,
|
|
1436
|
+
source_slice: sourceSlice.trim(),
|
|
1437
|
+
});
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
const archivePayloadById = new Map();
|
|
1441
|
+
const archivePayloads = Array.isArray(writePlan.archive_payloads) ? writePlan.archive_payloads : [];
|
|
1442
|
+
for (const payloadRaw of archivePayloads) {
|
|
1443
|
+
const payload = asRecord(payloadRaw);
|
|
1444
|
+
if (!payload)
|
|
1445
|
+
continue;
|
|
1446
|
+
const candidateId = typeof payload.candidate_id === "string" ? payload.candidate_id.trim() : "";
|
|
1447
|
+
if (!candidateId)
|
|
1448
|
+
continue;
|
|
1449
|
+
trustedCandidateIds.add(candidateId);
|
|
1450
|
+
const eventObj = asRecord(payload.event);
|
|
1451
|
+
const sourceSlice = firstString([
|
|
1452
|
+
payload.source_slice,
|
|
1453
|
+
eventObj?.source_slice,
|
|
1454
|
+
payload.source_span,
|
|
1455
|
+
eventObj?.source_span,
|
|
1456
|
+
payload.normalized_text,
|
|
1457
|
+
eventObj?.normalized_text,
|
|
1458
|
+
payload.span,
|
|
1459
|
+
eventObj?.span,
|
|
1460
|
+
payload.candidate_text,
|
|
1461
|
+
eventObj?.candidate_text,
|
|
1462
|
+
]) || "";
|
|
1463
|
+
if (sourceSlice && !candidateTextById.has(candidateId)) {
|
|
1464
|
+
candidateTextById.set(candidateId, sourceSlice);
|
|
1465
|
+
}
|
|
1466
|
+
const archiveValue = payload.event ?? payload;
|
|
1467
|
+
const eventValidation = (0, llm_output_validator_1.validateArchiveEvent)(archiveValue, { schema });
|
|
1468
|
+
if (!eventValidation.valid || !eventValidation.cleaned) {
|
|
1469
|
+
if (logger) {
|
|
1470
|
+
logger.warn(`quality_event_invalid candidate_id=${candidateId} errors=${eventValidation.errors.join("|")}`);
|
|
1471
|
+
}
|
|
1472
|
+
continue;
|
|
1473
|
+
}
|
|
1474
|
+
archivePayloadById.set(candidateId, {
|
|
1475
|
+
event_type: eventValidation.cleaned.event_type || "insight",
|
|
1476
|
+
summary: eventValidation.cleaned.summary,
|
|
1477
|
+
cause: eventValidation.cleaned.cause,
|
|
1478
|
+
process: eventValidation.cleaned.process,
|
|
1479
|
+
result: eventValidation.cleaned.result,
|
|
1480
|
+
entities: eventValidation.cleaned.entities,
|
|
1481
|
+
entity_types: eventValidation.cleaned.entity_types,
|
|
1482
|
+
relations: eventValidation.cleaned.relations,
|
|
1483
|
+
confidence: eventValidation.cleaned.confidence,
|
|
1484
|
+
});
|
|
1485
|
+
}
|
|
1486
|
+
const graphPayloadById = new Map();
|
|
1487
|
+
const graphPayloads = Array.isArray(writePlan.graph_payloads) ? writePlan.graph_payloads : [];
|
|
1488
|
+
for (const payloadRaw of graphPayloads) {
|
|
1489
|
+
const payload = asRecord(payloadRaw);
|
|
1490
|
+
if (!payload)
|
|
1491
|
+
continue;
|
|
1492
|
+
const candidateId = typeof payload.candidate_id === "string" ? payload.candidate_id.trim() : "";
|
|
1493
|
+
if (!candidateId)
|
|
1494
|
+
continue;
|
|
1495
|
+
trustedCandidateIds.add(candidateId);
|
|
1496
|
+
const sourceSlice = firstString([payload.source_slice, payload.source_span, payload.normalized_text, payload.span, payload.candidate_text]) || "";
|
|
1497
|
+
if (sourceSlice && !candidateTextById.has(candidateId)) {
|
|
1498
|
+
candidateTextById.set(candidateId, sourceSlice);
|
|
1499
|
+
}
|
|
1500
|
+
const graphValue = asRecord(payload.graph_payload) || asRecord(payload.graph) || payload;
|
|
1501
|
+
const graphPayload = toAppendableGraphPayload(parseGraphPayload(graphValue));
|
|
1502
|
+
if (!graphPayload)
|
|
1503
|
+
continue;
|
|
1504
|
+
graphPayloadById.set(candidateId, graphPayload);
|
|
1505
|
+
}
|
|
1506
|
+
const mergeHintById = new Map();
|
|
1507
|
+
const mergeHints = Array.isArray(writePlan.merge_hints) ? writePlan.merge_hints : [];
|
|
1508
|
+
for (const hintRaw of mergeHints) {
|
|
1509
|
+
const hint = parseMergeHintPayload(hintRaw);
|
|
1510
|
+
if (!hint?.candidate_id)
|
|
1511
|
+
continue;
|
|
1512
|
+
if (!trustedCandidateIds.has(hint.candidate_id)) {
|
|
1513
|
+
logger?.warn(`quality_gate_stage_d_unknown_candidate merge_hint candidate_id=${hint.candidate_id}`);
|
|
1514
|
+
continue;
|
|
1515
|
+
}
|
|
1516
|
+
mergeHintById.set(hint.candidate_id, hint);
|
|
1517
|
+
}
|
|
1518
|
+
const graphRewriteById = new Map();
|
|
1519
|
+
const graphRewriteItems = Array.isArray(writePlan.graph_rewrite) ? writePlan.graph_rewrite : [];
|
|
1520
|
+
for (const itemRaw of graphRewriteItems) {
|
|
1521
|
+
const item = parseGraphRewritePlanPayload(itemRaw);
|
|
1522
|
+
if (!item?.candidate_id)
|
|
1523
|
+
continue;
|
|
1524
|
+
if (!trustedCandidateIds.has(item.candidate_id)) {
|
|
1525
|
+
logger?.warn(`quality_gate_stage_d_unknown_candidate graph_rewrite candidate_id=${item.candidate_id}`);
|
|
1526
|
+
continue;
|
|
1527
|
+
}
|
|
1528
|
+
graphRewriteById.set(item.candidate_id, item);
|
|
1529
|
+
}
|
|
1530
|
+
const orderedIds = [];
|
|
1531
|
+
const seenIds = new Set();
|
|
1532
|
+
function pushId(id) {
|
|
1533
|
+
if (!id || seenIds.has(id))
|
|
1534
|
+
return;
|
|
1535
|
+
seenIds.add(id);
|
|
1536
|
+
orderedIds.push(id);
|
|
1537
|
+
}
|
|
1538
|
+
for (const id of orderedCandidateIds)
|
|
1539
|
+
pushId(id);
|
|
1540
|
+
for (const id of activePayloadById.keys())
|
|
1541
|
+
pushId(id);
|
|
1542
|
+
for (const id of archivePayloadById.keys())
|
|
1543
|
+
pushId(id);
|
|
1544
|
+
for (const id of graphPayloadById.keys())
|
|
1545
|
+
pushId(id);
|
|
1546
|
+
for (const id of mergeHintById.keys())
|
|
1547
|
+
pushId(id);
|
|
1548
|
+
for (const id of graphRewriteById.keys())
|
|
1549
|
+
pushId(id);
|
|
1550
|
+
const decisions = [];
|
|
1551
|
+
for (const candidateId of orderedIds) {
|
|
1552
|
+
let targetLayer = candidateRouteById.get(candidateId);
|
|
1553
|
+
if (!targetLayer) {
|
|
1554
|
+
if (archivePayloadById.has(candidateId)) {
|
|
1555
|
+
targetLayer = "archive_event";
|
|
1556
|
+
}
|
|
1557
|
+
else if (activePayloadById.has(candidateId)) {
|
|
1558
|
+
targetLayer = "active_only";
|
|
1559
|
+
}
|
|
1560
|
+
else {
|
|
1561
|
+
targetLayer = "skip";
|
|
1562
|
+
}
|
|
1563
|
+
}
|
|
1564
|
+
let event = archivePayloadById.get(candidateId);
|
|
1565
|
+
if (targetLayer === "archive_event" && !event) {
|
|
1566
|
+
if (logger) {
|
|
1567
|
+
logger.warn(`quality_gate_decisions_archive_missing candidate_id=${candidateId}`);
|
|
1568
|
+
}
|
|
1569
|
+
targetLayer = activePayloadById.has(candidateId) ? "active_only" : "skip";
|
|
1570
|
+
event = undefined;
|
|
1571
|
+
}
|
|
1572
|
+
const activePayload = activePayloadById.get(candidateId);
|
|
1573
|
+
const activeSummary = activePayload?.summary || "";
|
|
1574
|
+
const activeSourceSlice = activePayload?.source_slice || "";
|
|
1575
|
+
decisions.push({
|
|
1576
|
+
candidate_id: candidateId,
|
|
1577
|
+
candidate_text: candidateTextById.get(candidateId),
|
|
1578
|
+
target_layer: targetLayer,
|
|
1579
|
+
active_summary: activeSummary || undefined,
|
|
1580
|
+
active_source_slice: activeSourceSlice || undefined,
|
|
1581
|
+
event: targetLayer === "archive_event" ? event : undefined,
|
|
1582
|
+
graph: graphPayloadById.get(candidateId),
|
|
1583
|
+
merge_hint: mergeHintById.get(candidateId),
|
|
1584
|
+
graph_rewrite: graphRewriteById.get(candidateId),
|
|
1585
|
+
reason: candidateReasonById.get(candidateId) || (targetLayer === "skip" ? "llm_gate_skip" : ""),
|
|
1586
|
+
});
|
|
1587
|
+
}
|
|
1588
|
+
return decisions;
|
|
1589
|
+
}
|
|
1590
|
+
function parseLegacyRoutingDecisions(rootObj, logger, schema) {
|
|
1591
|
+
const output = [];
|
|
1592
|
+
const candidateTextById = new Map();
|
|
1593
|
+
if (Array.isArray(rootObj.candidate_events)) {
|
|
1594
|
+
for (const candidateRaw of rootObj.candidate_events) {
|
|
1595
|
+
if (!candidateRaw || typeof candidateRaw !== "object")
|
|
1596
|
+
continue;
|
|
1597
|
+
const candidate = candidateRaw;
|
|
1598
|
+
const candidateId = typeof candidate.candidate_id === "string" ? candidate.candidate_id.trim() : "";
|
|
1599
|
+
if (!candidateId)
|
|
1600
|
+
continue;
|
|
1601
|
+
const candidateText = firstString([candidate.normalized_text, candidate.span, candidate.text]) || "";
|
|
1602
|
+
if (candidateText) {
|
|
1603
|
+
candidateTextById.set(candidateId, candidateText);
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
const pushDecision = (obj, target) => {
|
|
1608
|
+
const candidateId = typeof obj.candidate_id === "string" ? obj.candidate_id.trim() : "";
|
|
1609
|
+
let event = null;
|
|
1610
|
+
if (target === "archive_event") {
|
|
1611
|
+
const eventValidation = (0, llm_output_validator_1.validateArchiveEvent)(obj.event || obj, { schema });
|
|
1612
|
+
if (eventValidation.valid && eventValidation.cleaned) {
|
|
1613
|
+
event = {
|
|
1614
|
+
event_type: eventValidation.cleaned.event_type || "insight",
|
|
1615
|
+
summary: eventValidation.cleaned.summary,
|
|
1616
|
+
cause: eventValidation.cleaned.cause,
|
|
1617
|
+
process: eventValidation.cleaned.process,
|
|
1618
|
+
result: eventValidation.cleaned.result,
|
|
1619
|
+
entities: eventValidation.cleaned.entities,
|
|
1620
|
+
entity_types: eventValidation.cleaned.entity_types,
|
|
1621
|
+
relations: eventValidation.cleaned.relations,
|
|
1622
|
+
confidence: eventValidation.cleaned.confidence,
|
|
1623
|
+
};
|
|
1624
|
+
}
|
|
1625
|
+
else {
|
|
1626
|
+
if (logger) {
|
|
1627
|
+
logger.warn(`quality_event_invalid errors=${eventValidation.errors.join("|")}`);
|
|
1628
|
+
}
|
|
1629
|
+
return;
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
output.push({
|
|
1633
|
+
candidate_id: candidateId,
|
|
1634
|
+
candidate_text: candidateTextById.get(candidateId) || undefined,
|
|
1635
|
+
target_layer: target,
|
|
1636
|
+
active_summary: normalizeOneLineText(typeof obj.summary === "string"
|
|
1637
|
+
? obj.summary
|
|
1638
|
+
: (typeof obj.active_summary === "string"
|
|
1639
|
+
? obj.active_summary
|
|
1640
|
+
: (typeof obj.active_text === "string" ? obj.active_text : ""))) || undefined,
|
|
1641
|
+
active_source_slice: firstString([
|
|
1642
|
+
obj.source_slice,
|
|
1643
|
+
obj.source_span,
|
|
1644
|
+
]) || undefined,
|
|
1645
|
+
event: event || undefined,
|
|
1646
|
+
graph: toAppendableGraphPayload(parseGraphPayload(obj.graph)),
|
|
1647
|
+
reason: typeof obj.reason === "string" ? obj.reason.trim() : "",
|
|
1648
|
+
});
|
|
1649
|
+
};
|
|
1650
|
+
const routingPlan = asRecord(rootObj.routing_plan);
|
|
1651
|
+
if (routingPlan) {
|
|
1652
|
+
const buckets = [
|
|
1653
|
+
{ key: "archive_event", items: routingPlan.archive_event },
|
|
1654
|
+
{ key: "active_only", items: routingPlan.active_only },
|
|
1655
|
+
{ key: "skip", items: routingPlan.skip },
|
|
1656
|
+
];
|
|
1657
|
+
for (const bucket of buckets) {
|
|
1658
|
+
if (!Array.isArray(bucket.items))
|
|
1659
|
+
continue;
|
|
1660
|
+
for (const item of bucket.items) {
|
|
1661
|
+
if (!item || typeof item !== "object")
|
|
1662
|
+
continue;
|
|
1663
|
+
pushDecision(item, bucket.key);
|
|
1664
|
+
}
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1667
|
+
else if (logger) {
|
|
1668
|
+
logger.warn("quality_gate_decisions_invalid missing_routing_plan");
|
|
1669
|
+
}
|
|
1670
|
+
return output;
|
|
1671
|
+
}
|
|
1672
|
+
function parseLlmGateDecisions(raw, logger, schema) {
|
|
1673
|
+
const validation = (0, llm_output_validator_1.validateLlmJsonOutput)(raw);
|
|
1674
|
+
if (!validation.valid) {
|
|
1675
|
+
if (logger) {
|
|
1676
|
+
logger.warn(`quality_gate_decisions_invalid errors=${validation.errors.join("|")}`);
|
|
1677
|
+
}
|
|
1678
|
+
return [];
|
|
1679
|
+
}
|
|
1680
|
+
if (validation.warnings.length > 0 && logger) {
|
|
1681
|
+
logger.debug(`quality_gate_decisions_warnings warnings=${validation.warnings.join("|")}`);
|
|
1682
|
+
}
|
|
1683
|
+
const root = validation.data;
|
|
1684
|
+
if (Array.isArray(root)) {
|
|
1685
|
+
if (logger) {
|
|
1686
|
+
logger.warn("quality_gate_decisions_invalid format=array_not_supported_require_write_plan");
|
|
1687
|
+
}
|
|
1688
|
+
return [];
|
|
1689
|
+
}
|
|
1690
|
+
const rootObj = asRecord(root);
|
|
1691
|
+
if (!rootObj) {
|
|
1692
|
+
if (logger) {
|
|
1693
|
+
logger.warn("quality_gate_decisions_invalid root_not_object");
|
|
1694
|
+
}
|
|
1695
|
+
return [];
|
|
1696
|
+
}
|
|
1697
|
+
const output = parseWritePlanDecisions(rootObj, logger, schema);
|
|
1698
|
+
const legacyOutput = output.length > 0 ? output : parseLegacyRoutingDecisions(rootObj, logger, schema);
|
|
1699
|
+
if (legacyOutput.length === 0 && logger) {
|
|
1700
|
+
logger.warn("quality_gate_decisions_empty");
|
|
1701
|
+
}
|
|
1702
|
+
const deduped = [];
|
|
1703
|
+
const seen = new Set();
|
|
1704
|
+
for (const item of legacyOutput) {
|
|
1705
|
+
const key = `${item.candidate_id || ""}|${item.target_layer}|${item.event?.summary || item.active_summary || item.reason || ""}`;
|
|
1706
|
+
if (seen.has(key))
|
|
1707
|
+
continue;
|
|
1708
|
+
seen.add(key);
|
|
1709
|
+
deduped.push(item);
|
|
1710
|
+
}
|
|
1711
|
+
return deduped;
|
|
1712
|
+
}
|
|
1713
|
+
function readWritePlanObject(rootObj) {
|
|
1714
|
+
return asRecord(rootObj.write_plan) || rootObj;
|
|
1715
|
+
}
|
|
1716
|
+
function readWritePlanArray(rootObj, key) {
|
|
1717
|
+
const writePlan = readWritePlanObject(rootObj);
|
|
1718
|
+
const value = writePlan[key];
|
|
1719
|
+
return Array.isArray(value) ? value : [];
|
|
1720
|
+
}
|
|
1721
|
+
async function requestWriteGateStage(args) {
|
|
1722
|
+
const body = {
|
|
1723
|
+
model: args.llm.model,
|
|
1724
|
+
temperature: 0.1,
|
|
1725
|
+
response_format: { type: "json_object" },
|
|
1726
|
+
messages: [
|
|
1727
|
+
{ role: "system", content: args.systemPrompt },
|
|
1728
|
+
{ role: "user", content: args.userLines.join("\n") },
|
|
1729
|
+
],
|
|
1730
|
+
};
|
|
1731
|
+
let lastError = null;
|
|
1732
|
+
const maxAttempts = typeof args.maxAttempts === "number" && args.maxAttempts > 0 ? args.maxAttempts : 3;
|
|
1733
|
+
const timeoutMs = typeof args.timeoutMs === "number" && args.timeoutMs >= 1000 ? args.timeoutMs : 25000;
|
|
1734
|
+
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
|
|
1735
|
+
let response;
|
|
1736
|
+
try {
|
|
1737
|
+
response = await (0, http_post_1.postJsonWithTimeout)({
|
|
1738
|
+
endpoint: args.endpoint,
|
|
1739
|
+
apiKey: args.llm.apiKey,
|
|
1740
|
+
body,
|
|
1741
|
+
timeoutMs,
|
|
1742
|
+
});
|
|
1743
|
+
}
|
|
1744
|
+
catch (error) {
|
|
1745
|
+
lastError = error;
|
|
1746
|
+
continue;
|
|
1747
|
+
}
|
|
1748
|
+
if (!response.ok) {
|
|
1749
|
+
lastError = new Error(response.status > 0 ? `sync_llm_${args.stage}_http_${response.status}` : (response.error || `sync_llm_${args.stage}_network_error`));
|
|
1750
|
+
continue;
|
|
1751
|
+
}
|
|
1752
|
+
try {
|
|
1753
|
+
const json = (response.json || {});
|
|
1754
|
+
const content = json?.choices?.[0]?.message?.content || "";
|
|
1755
|
+
if (!content.trim()) {
|
|
1756
|
+
lastError = new Error(`sync_llm_${args.stage}_empty`);
|
|
1757
|
+
continue;
|
|
1758
|
+
}
|
|
1759
|
+
const validation = (0, llm_output_validator_1.validateLlmJsonOutput)(content);
|
|
1760
|
+
if (!validation.valid) {
|
|
1761
|
+
lastError = new Error(`sync_llm_${args.stage}_invalid_json:${validation.errors.join("|")}`);
|
|
1762
|
+
continue;
|
|
1763
|
+
}
|
|
1764
|
+
const rootObj = asRecord(validation.data);
|
|
1765
|
+
if (!rootObj) {
|
|
1766
|
+
lastError = new Error(`sync_llm_${args.stage}_root_not_object`);
|
|
1767
|
+
continue;
|
|
1768
|
+
}
|
|
1769
|
+
if (validation.warnings.length > 0) {
|
|
1770
|
+
args.logger.debug(`sync_llm_${args.stage}_warnings warnings=${validation.warnings.join("|")}`);
|
|
1771
|
+
}
|
|
1772
|
+
return rootObj;
|
|
1773
|
+
}
|
|
1774
|
+
catch (error) {
|
|
1775
|
+
lastError = error;
|
|
1776
|
+
}
|
|
1777
|
+
}
|
|
1778
|
+
args.logger.warn(`Sync LLM stage=${args.stage} failed: ${String(lastError || "unknown")}`);
|
|
1779
|
+
return null;
|
|
1780
|
+
}
|
|
1781
|
+
async function extractGateDecisionsWithLlm(args) {
|
|
1782
|
+
const endpoint = args.llm.baseUrl.endsWith("/chat/completions")
|
|
1783
|
+
? args.llm.baseUrl
|
|
1784
|
+
: `${args.llm.baseUrl}/chat/completions`;
|
|
1785
|
+
const eventSnippet = buildEventSnippet(args.transcript);
|
|
1786
|
+
const activeValuePromptHint = buildActiveValuePromptHint(args.schema);
|
|
1787
|
+
const entityDictionaryPromptHint = buildEntityDictionaryPromptHint(args.schema);
|
|
1788
|
+
const stableGraphFactHint = `Stable graph facts are evidence-backed and dictionary-grounded entities/relations with reusable value. ${activeValuePromptHint}`;
|
|
1789
|
+
const stageAbRoot = await requestWriteGateStage({
|
|
1790
|
+
stage: "ab",
|
|
1791
|
+
llm: args.llm,
|
|
1792
|
+
endpoint,
|
|
1793
|
+
logger: args.logger,
|
|
1794
|
+
systemPrompt: "You are memory write-gate stage A+B router. Output JSON only.",
|
|
1795
|
+
userLines: [
|
|
1796
|
+
`prompt_version=${WRITE_GATE_STAGE_AB_PROMPT_VERSION}`,
|
|
1797
|
+
"Task: execute Stage A+B only (denoise + candidate split + route classification).",
|
|
1798
|
+
"Route classes: active_only | archive_event | skip.",
|
|
1799
|
+
"Rules:",
|
|
1800
|
+
"- Denoise first: remove pure acknowledgements/politeness/chitchat/repeated filler (for example: 好的/收到/谢谢/ok/got it/thanks) when they contain no task facts.",
|
|
1801
|
+
`- Keep factual evidence during denoise using dictionary-grounded signals. ${activeValuePromptHint}`,
|
|
1802
|
+
`- ${entityDictionaryPromptHint}`,
|
|
1803
|
+
"- Concrete entities must be source-grounded names or aliases from the dictionary above; reject generic placeholders (e.g., user/person/system/用户/系统/某项目/thing).",
|
|
1804
|
+
"- Semantic event split rule: one candidate = one principal event with a coherent subject + action/decision + object/outcome in the same phase.",
|
|
1805
|
+
"- Split into different candidates when topic/goal/subject changes, or when a new decision/outcome starts.",
|
|
1806
|
+
"- Merge sentences into one candidate when they describe the same event progression in one phase.",
|
|
1807
|
+
"- One candidate must represent one principal event only.",
|
|
1808
|
+
"- archive_event: reusable event with clear subject + action/decision + outcome/phase conclusion.",
|
|
1809
|
+
"- active_only: ongoing process context or temporary status without stable conclusion, but MUST contain valuable reusable information.",
|
|
1810
|
+
"- Active_only value criteria must follow the same dictionary-grounded signals above.",
|
|
1811
|
+
"- If candidate is only acknowledgement/politeness or has no valuable signal, route to skip.",
|
|
1812
|
+
"- skip: noise/repetition/chitchat/no clear business value.",
|
|
1813
|
+
"- Archive task-lifecycle by default when all three are present: user instruction -> agent completion report -> user acceptance.",
|
|
1814
|
+
"- If failure/iteration then eventual success exists, prefer archive_event.",
|
|
1815
|
+
"Candidate field definitions:",
|
|
1816
|
+
"- span: source text slice for this candidate (verbatim excerpt from transcript snippet, may contain minor noise).",
|
|
1817
|
+
"- normalized_text: denoised and normalized version of span for the same candidate; keep meaning unchanged.",
|
|
1818
|
+
"- When both are available, normalized_text is the canonical text field for downstream stages.",
|
|
1819
|
+
"Output schema:",
|
|
1820
|
+
"{\"write_plan\":{\"candidates\":[{\"candidate_id\":\"c1\",\"route\":\"archive_event\",\"span\":\"...\",\"normalized_text\":\"...\",\"reason\":\"...\"}]}}",
|
|
1821
|
+
...WRITE_GATE_REGRESSION_SAMPLES,
|
|
1822
|
+
"Output JSON only. Do NOT output active_payloads/archive_payloads/graph_payloads/merge_hints/graph_rewrite.",
|
|
1823
|
+
"",
|
|
1824
|
+
"[TRANSCRIPT_SNIPPET]",
|
|
1825
|
+
eventSnippet,
|
|
1826
|
+
"[/TRANSCRIPT_SNIPPET]",
|
|
1827
|
+
],
|
|
1828
|
+
});
|
|
1829
|
+
if (!stageAbRoot) {
|
|
1830
|
+
return [];
|
|
1831
|
+
}
|
|
1832
|
+
const stageAbCandidates = readWritePlanArray(stageAbRoot, "candidates");
|
|
1833
|
+
if (stageAbCandidates.length === 0) {
|
|
1834
|
+
args.logger.warn("quality_gate_stage_ab_empty_candidates");
|
|
1835
|
+
return [];
|
|
1836
|
+
}
|
|
1837
|
+
const stageCRoot = await requestWriteGateStage({
|
|
1838
|
+
stage: "c",
|
|
1839
|
+
llm: args.llm,
|
|
1840
|
+
endpoint,
|
|
1841
|
+
logger: args.logger,
|
|
1842
|
+
systemPrompt: "You are memory write-gate stage C payload builder. Output JSON only.",
|
|
1843
|
+
userLines: [
|
|
1844
|
+
`prompt_version=${WRITE_GATE_STAGE_C_PROMPT_VERSION}`,
|
|
1845
|
+
"Task: execute Stage C1/C2/C3 only.",
|
|
1846
|
+
"C1) Build active_payloads[] for active_only candidates.",
|
|
1847
|
+
"C2) Build archive_payloads[] for archive_event candidates.",
|
|
1848
|
+
"C3) Build graph_payloads[] for candidates with stable graph facts (independent from route).",
|
|
1849
|
+
"Keep candidate_id exactly equal to input candidates.",
|
|
1850
|
+
"- For archive_event route, C2 archive payload must include complete cause/process/result; if confidence < 0.35, prefer no archive payload for that candidate.",
|
|
1851
|
+
"- For every candidate routed to active_only or archive_event, C3 graph_payload is REQUIRED (non-optional).",
|
|
1852
|
+
"- For durable personal profile facts (family relation, birthday, anniversary, long-term schedule), C3 graph payload remains REQUIRED as a strict subset of the rule above.",
|
|
1853
|
+
"- Preserve key entities/relations/URLs/document paths/exact numbers/timepoints from source text; do NOT over-abstract placeholders.",
|
|
1854
|
+
`- ${stableGraphFactHint}`,
|
|
1855
|
+
`- ${entityDictionaryPromptHint}`,
|
|
1856
|
+
"- Concrete entities in C1/C2/C3 must follow the dictionary above and remain source-grounded; do not output generic placeholders.",
|
|
1857
|
+
"- Each C1/C2/C3 item must carry source_slice.",
|
|
1858
|
+
"- source_slice must come from the [CANDIDATES] object in this same request: use the denoised source segment text of the same candidate_id (prefer normalized_text, then span).",
|
|
1859
|
+
"- In [CANDIDATES], normalized_text means denoised canonical candidate text, and span means original source slice; keep source_slice semantically consistent with these fields.",
|
|
1860
|
+
"- source_slice is a trace field for executor write path and retrieval backtracking; keep it source-faithful (do not paraphrase, truncate, or invent text).",
|
|
1861
|
+
"C1 field requirements (active_payloads[] item):",
|
|
1862
|
+
"- candidate_id: required; must match one input candidate_id exactly.",
|
|
1863
|
+
"- source_slice: required; must be the denoised original text from this C1 candidate slice (full candidate text, DO NOT truncate or excerpt).",
|
|
1864
|
+
"- summary: required; preserve key information (cause, subject, object, and important entities grounded in the graph schema dictionary/aliases above), include stage result for process updates, and stay within 100 characters.",
|
|
1865
|
+
"C2 field requirements (archive_payloads[] item):",
|
|
1866
|
+
"- candidate_id: required; must match one input candidate_id exactly.",
|
|
1867
|
+
"- source_slice: required; must be the denoised original text from this C2 candidate slice.",
|
|
1868
|
+
"- event_type: required; must be dictionary-grounded to schema eventTypes/eventTypeAliases.",
|
|
1869
|
+
"- summary: required; must explain cause->process->result end-to-end and stay within 100 characters.",
|
|
1870
|
+
"- cause: required; explain why the event happened.",
|
|
1871
|
+
"- process: required; explain key execution/decision process.",
|
|
1872
|
+
"- result: required; explain final result/state.",
|
|
1873
|
+
"- entities: recommended; if present, use concrete entity names from source text.",
|
|
1874
|
+
"- entity_types: recommended; if entities are present, provide valid schema type for each entity.",
|
|
1875
|
+
"- relations: optional; if provided, each relation should be source-grounded and schema-compatible.",
|
|
1876
|
+
"- confidence: recommended numeric score in [0,1].",
|
|
1877
|
+
"C3 field requirements (graph_payloads[] item):",
|
|
1878
|
+
"- candidate_id: required; must match one input candidate_id exactly.",
|
|
1879
|
+
"- source_slice: required; must be the denoised original text from this candidate slice.",
|
|
1880
|
+
"- summary: required; must cover every entity listed in entities[] and explain key relations among entities.",
|
|
1881
|
+
"- source_text_nav: required trace object for source-location and replay; it links graph facts to the original memory/event for retrieval, citation, and debugging.",
|
|
1882
|
+
"- source_text_nav.layer: required; active_only or archive_event, and should be consistent with the candidate route/context.",
|
|
1883
|
+
"- source_text_nav.session_id: required; session id where this candidate comes from (copy from current routing context).",
|
|
1884
|
+
"- source_text_nav.source_file: required; source file identifier/path where this candidate comes from (copy from current routing context).",
|
|
1885
|
+
"- source_text_nav.source_memory_id: required; stable memory record id for this candidate in the source layer (use source_event_id when no separate memory id exists).",
|
|
1886
|
+
"- source_text_nav.source_event_id: required; stable event trace id for this candidate used for full-text backtracking.",
|
|
1887
|
+
"- entities: required array of concrete entities from source text.",
|
|
1888
|
+
"- entity_types: required map; every entity in entities[] must have a valid schema type.",
|
|
1889
|
+
"- relations: required array; each relation must be source-grounded and schema-compatible.",
|
|
1890
|
+
"- confidence: required number in [0,1].",
|
|
1891
|
+
"C3 additional requirements:",
|
|
1892
|
+
"- For archive_event candidates, extract richer source-grounded entities/relations from source_slice + cause/process/result when available (archive facts are higher-value).",
|
|
1893
|
+
"- Key-entity protection: entities explicitly mentioned in candidate span should not be dropped.",
|
|
1894
|
+
"- Normalize alias/cross-language references to one canonical entity when possible.",
|
|
1895
|
+
`- ${args.relationPromptHint}`,
|
|
1896
|
+
"- Every relation must include: source,target,type,relation_origin,evidence_span,context_chunk,confidence.",
|
|
1897
|
+
"- If relation_origin is llm_custom, relation_definition is required.",
|
|
1898
|
+
"Output structure (C1/C2/C3 are different item schemas):",
|
|
1899
|
+
"- write_plan.active_payloads[] item (C1): {candidate_id,source_slice,summary}",
|
|
1900
|
+
"- write_plan.archive_payloads[] item (C2): {candidate_id,source_slice,event_type,summary,cause,process,result,entities,entity_types,relations,confidence}",
|
|
1901
|
+
"- write_plan.graph_payloads[] item (C3): {candidate_id,source_slice,summary,source_text_nav,entities,entity_types,relations,confidence}",
|
|
1902
|
+
"Top-level output schema:",
|
|
1903
|
+
"{\"write_plan\":{\"active_payloads\":[{\"candidate_id\":\"c1\",\"source_slice\":\"...\",\"summary\":\"...\"}],\"archive_payloads\":[{\"candidate_id\":\"c2\",\"source_slice\":\"...\",\"event_type\":\"decision\",\"summary\":\"...\",\"cause\":\"...\",\"process\":\"...\",\"result\":\"...\",\"entities\":[\"openclaw-cortex-memory\"],\"entity_types\":{\"openclaw-cortex-memory\":\"Project\"},\"relations\":[],\"confidence\":0.82}],\"graph_payloads\":[{\"candidate_id\":\"c1\",\"source_slice\":\"...\",\"summary\":\"任务A depends_on 资源B。\",\"source_text_nav\":{\"layer\":\"active_only\",\"session_id\":\"s1\",\"source_file\":\"daily_summary:2026-04-03.md\",\"source_memory_id\":\"evt_1\",\"source_event_id\":\"evt_1\"},\"entities\":[\"任务A\",\"资源B\"],\"entity_types\":{\"任务A\":\"Task\",\"资源B\":\"Resource\"},\"relations\":[{\"source\":\"任务A\",\"target\":\"资源B\",\"type\":\"depends_on\",\"relation_origin\":\"canonical\",\"evidence_span\":\"任务A 依赖 资源B\",\"context_chunk\":\"用户说明任务A依赖资源B,需要后续处理。\",\"confidence\":0.9}],\"confidence\":0.8}]}}",
|
|
1904
|
+
"Output JSON only. Do NOT output merge_hints/graph_rewrite.",
|
|
1905
|
+
"",
|
|
1906
|
+
"[CANDIDATES]",
|
|
1907
|
+
JSON.stringify(stageAbCandidates),
|
|
1908
|
+
"[/CANDIDATES]",
|
|
1909
|
+
"",
|
|
1910
|
+
"[TRANSCRIPT_SNIPPET]",
|
|
1911
|
+
eventSnippet,
|
|
1912
|
+
"[/TRANSCRIPT_SNIPPET]",
|
|
1913
|
+
],
|
|
1914
|
+
});
|
|
1915
|
+
const stageCActivePayloads = stageCRoot ? readWritePlanArray(stageCRoot, "active_payloads") : [];
|
|
1916
|
+
const stageCArchivePayloads = stageCRoot ? readWritePlanArray(stageCRoot, "archive_payloads") : [];
|
|
1917
|
+
const stageCGraphPayloads = stageCRoot ? readWritePlanArray(stageCRoot, "graph_payloads") : [];
|
|
1918
|
+
const stageDRoot = await requestWriteGateStage({
|
|
1919
|
+
stage: "d",
|
|
1920
|
+
llm: args.llm,
|
|
1921
|
+
endpoint,
|
|
1922
|
+
logger: args.logger,
|
|
1923
|
+
systemPrompt: "You are memory write-gate stage D merge/rewrite planner. Output JSON only.",
|
|
1924
|
+
userLines: [
|
|
1925
|
+
`prompt_version=${WRITE_GATE_STAGE_D_PROMPT_VERSION}`,
|
|
1926
|
+
"Task: execute Stage D only (merge/conflict/rewrite planning).",
|
|
1927
|
+
"Output merge_hints[] and graph_rewrite[] only for executor consumption.",
|
|
1928
|
+
"Rules:",
|
|
1929
|
+
`- ${entityDictionaryPromptHint}`,
|
|
1930
|
+
`- Relation dictionary and mapping rule: ${args.relationPromptHint}`,
|
|
1931
|
+
`- Keep valuable factual evidence during merge/rewrite planning. ${activeValuePromptHint}`,
|
|
1932
|
+
"- Merge planning (how to merge):",
|
|
1933
|
+
"- For each candidate_id from Stage C graph_payloads, decide same_event and same_entity_pairs using source-grounded evidence.",
|
|
1934
|
+
"- same_event=true only when graph facts indicate continuation of the same principal event (same core entities + same relation cluster + same phase progression).",
|
|
1935
|
+
"- same_entity_pairs lists alias pairs that refer to the same concrete entity: [[alias,canonical_name],...].",
|
|
1936
|
+
"- canonical_name should prefer dictionary canonical/alias-backed concrete names; do not use generic placeholders.",
|
|
1937
|
+
"- If no alias merge is needed, same_entity_pairs should be empty or omitted.",
|
|
1938
|
+
"- Rewrite planning (how to rewrite):",
|
|
1939
|
+
"- rewrite_required=true only when post-merge synchronization is required for graph consistency.",
|
|
1940
|
+
"- Trigger rewrite when at least one is true: entity alias merge changes canonical names; relation mapping changes; summary no longer covers merged entities/relations; conflict resolution requires fact rewrite.",
|
|
1941
|
+
"- rewrite_scope must be a subset of: summary, source_text_nav, entities, entity_types, relations, confidence.",
|
|
1942
|
+
"- summary and entities are core fields and may be updated when rewrite_required=true even if not explicitly listed in rewrite_scope.",
|
|
1943
|
+
"- graph_rewrite_payload is optional patch payload; include only fields in rewrite_scope.",
|
|
1944
|
+
"- If graph_rewrite_payload.relations is provided, every relation must include source,target,type,relation_origin,evidence_span,context_chunk,confidence.",
|
|
1945
|
+
"- If rewrite is not needed: set rewrite_required=false, rewrite_scope=[], graph_rewrite_payload=null.",
|
|
1946
|
+
"- Coverage and consistency:",
|
|
1947
|
+
"- Keep candidate_id exactly equal to input candidate_id; do not invent candidate_id.",
|
|
1948
|
+
"- For every Stage C graph_payload candidate, output one merge_hints item and one graph_rewrite item.",
|
|
1949
|
+
"- Do not output active_payloads/archive_payloads/graph_payloads in this stage.",
|
|
1950
|
+
"Output schema:",
|
|
1951
|
+
"{\"write_plan\":{\"merge_hints\":[{\"candidate_id\":\"c1\",\"same_event\":false,\"same_entity_pairs\":[[\"Ava\",\"Ava\"]],\"suggested_action\":\"merge_aliases\",\"reason\":\"Alias pair refers to the same person\"}],\"graph_rewrite\":[{\"candidate_id\":\"c1\",\"rewrite_required\":true,\"rewrite_reason\":\"entity canonicalization changed summary and relations\",\"rewrite_scope\":[\"summary\",\"entities\",\"entity_types\",\"relations\"],\"graph_rewrite_payload\":{\"summary\":\"...\",\"entities\":[\"Ava\",\"Joe\"],\"entity_types\":{\"Ava\":\"Person\",\"Joe\":\"Person\"},\"relations\":[{\"source\":\"Joe\",\"target\":\"Ava\",\"type\":\"has_spouse\",\"relation_origin\":\"canonical\",\"evidence_span\":\"Joe is spouse of Ava\",\"context_chunk\":\"source snippet context\",\"confidence\":0.91}],\"confidence\":0.86}}]}}",
|
|
1952
|
+
"Output JSON only.",
|
|
1953
|
+
"",
|
|
1954
|
+
"[CANDIDATES]",
|
|
1955
|
+
JSON.stringify(stageAbCandidates),
|
|
1956
|
+
"[/CANDIDATES]",
|
|
1957
|
+
"",
|
|
1958
|
+
"[STAGE_C_PAYLOADS]",
|
|
1959
|
+
JSON.stringify({
|
|
1960
|
+
active_payloads: stageCActivePayloads,
|
|
1961
|
+
archive_payloads: stageCArchivePayloads,
|
|
1962
|
+
graph_payloads: stageCGraphPayloads,
|
|
1963
|
+
}),
|
|
1964
|
+
"[/STAGE_C_PAYLOADS]",
|
|
1965
|
+
"",
|
|
1966
|
+
"[TRANSCRIPT_SNIPPET]",
|
|
1967
|
+
eventSnippet,
|
|
1968
|
+
"[/TRANSCRIPT_SNIPPET]",
|
|
1969
|
+
],
|
|
1970
|
+
});
|
|
1971
|
+
const stageDMergeHints = stageDRoot ? readWritePlanArray(stageDRoot, "merge_hints") : [];
|
|
1972
|
+
const stageDGraphRewrite = stageDRoot ? readWritePlanArray(stageDRoot, "graph_rewrite") : [];
|
|
1973
|
+
const mergedPlan = {
|
|
1974
|
+
write_plan: {
|
|
1975
|
+
candidates: stageAbCandidates,
|
|
1976
|
+
active_payloads: stageCActivePayloads,
|
|
1977
|
+
archive_payloads: stageCArchivePayloads,
|
|
1978
|
+
graph_payloads: stageCGraphPayloads,
|
|
1979
|
+
merge_hints: stageDMergeHints,
|
|
1980
|
+
graph_rewrite: stageDGraphRewrite,
|
|
1981
|
+
},
|
|
1982
|
+
};
|
|
1983
|
+
const decisions = parseWritePlanDecisions(mergedPlan, args.logger, args.schema);
|
|
1984
|
+
if (decisions.length > 0) {
|
|
1985
|
+
return decisions;
|
|
1986
|
+
}
|
|
1987
|
+
args.logger.warn("quality_gate_decisions_empty stage_pipeline");
|
|
1988
|
+
return [];
|
|
1989
|
+
}
|
|
1990
|
+
async function rewriteGraphPayloadWithLlm(args) {
|
|
1991
|
+
const endpoint = args.llm.baseUrl.endsWith("/chat/completions")
|
|
1992
|
+
? args.llm.baseUrl
|
|
1993
|
+
: `${args.llm.baseUrl}/chat/completions`;
|
|
1994
|
+
const candidateText = (args.candidateText || "").trim();
|
|
1995
|
+
const sourceSnippet = candidateText || buildEventSnippet(args.transcript);
|
|
1996
|
+
const body = {
|
|
1997
|
+
model: args.llm.model,
|
|
1998
|
+
temperature: 0.1,
|
|
1999
|
+
response_format: { type: "json_object" },
|
|
2000
|
+
messages: [
|
|
2001
|
+
{ role: "system", content: "You are a graph payload rewrite engine. Output JSON only." },
|
|
2002
|
+
{
|
|
2003
|
+
role: "user",
|
|
2004
|
+
content: [
|
|
2005
|
+
`prompt_version=${WRITE_GATE_GRAPH_REWRITE_PROMPT_VERSION}`,
|
|
2006
|
+
"Task: rewrite graph payload only when synchronization is required after merge/conflict checks.",
|
|
2007
|
+
`rewrite_required=${args.rewritePlan?.rewrite_required === true ? "true" : "false"}`,
|
|
2008
|
+
`rewrite_reason=${args.rewritePlan?.rewrite_reason || ""}`,
|
|
2009
|
+
`rewrite_scope=${(args.rewritePlan?.rewrite_scope || []).join(",")}`,
|
|
2010
|
+
`merge_same_event=${args.mergeHint?.same_event === true ? "true" : "false"}`,
|
|
2011
|
+
`merge_same_entity_pairs=${JSON.stringify(args.mergeHint?.same_entity_pairs || [])}`,
|
|
2012
|
+
"Rewrite policy:",
|
|
2013
|
+
"- Return a FULL graph_payload object (not partial): include summary, source_text_nav, entities, entity_types, relations, confidence.",
|
|
2014
|
+
"- Apply semantic changes only for fields in rewrite_scope; for fields outside rewrite_scope, copy from CURRENT_GRAPH_PAYLOAD unless hard validation requires normalization.",
|
|
2015
|
+
"- Prefer merge-hint canonical entities for alias unification when same_entity_pairs is provided.",
|
|
2016
|
+
"- Do not invent new facts outside SOURCE_SNIPPET and CURRENT_GRAPH_PAYLOAD.",
|
|
2017
|
+
"- Keep factual evidence and valuable signals. "
|
|
2018
|
+
+ "Decisions/problems/fixes/constraints/requirements/names/deadlines/metrics/URLs/paths/document ids/config-version details should be preserved when present. "
|
|
2019
|
+
+ args.activeValuePromptHint,
|
|
2020
|
+
"Hard constraints:",
|
|
2021
|
+
`- ${args.entityDictionaryPromptHint}`,
|
|
2022
|
+
"- Keep entities concrete, dictionary-grounded names/aliases; no generic placeholders.",
|
|
2023
|
+
"- Keep relations source-grounded and evidence-backed.",
|
|
2024
|
+
"- Do not output related_to.",
|
|
2025
|
+
`- ${args.relationPromptHint}`,
|
|
2026
|
+
"Field contract (required):",
|
|
2027
|
+
"- graph_payload.summary: must cover all entities and key relations.",
|
|
2028
|
+
"- If source_text_nav.layer is active_only, summary must preserve key information (cause, subject, object and important entities grounded in the graph schema dictionary/aliases), include stage result for process updates, and stay within 100 characters.",
|
|
2029
|
+
"- summary should remain model-authored text; do not apply rule-based hard truncation after generation.",
|
|
2030
|
+
"- graph_payload.source_text_nav: required object with layer,session_id,source_file,source_memory_id,source_event_id; fulltext_anchor optional.",
|
|
2031
|
+
"- graph_payload.entities: required array of concrete entity names; deduplicated.",
|
|
2032
|
+
"- graph_payload.entity_types: required map; every entity in entities should have a type from dictionary.",
|
|
2033
|
+
"- graph_payload.relations: required array; each relation source/target should refer to entities in graph_payload.entities.",
|
|
2034
|
+
"- Every relation must include source,target,type,relation_origin,evidence_span,context_chunk,confidence.",
|
|
2035
|
+
"- relation_origin: canonical or llm_custom.",
|
|
2036
|
+
"- If relation_origin is llm_custom, relation_definition is required.",
|
|
2037
|
+
"- graph_payload.confidence: required number in [0,1], reflecting final payload certainty.",
|
|
2038
|
+
"Consistency checks before output:",
|
|
2039
|
+
"- Ensure summary mentions entities and does not contradict relations.",
|
|
2040
|
+
"- Ensure context_chunk/evidence_span comes from source text and supports the relation.",
|
|
2041
|
+
"- If rewrite_scope is empty, output CURRENT_GRAPH_PAYLOAD unchanged.",
|
|
2042
|
+
"Output schema:",
|
|
2043
|
+
"{\"graph_payload\":{\"summary\":\"...\",\"source_text_nav\":{\"layer\":\"archive_event\",\"session_id\":\"...\",\"source_file\":\"...\",\"source_memory_id\":\"...\",\"source_event_id\":\"...\"},\"entities\":[\"...\"],\"entity_types\":{\"...\":\"...\"},\"relations\":[{\"source\":\"...\",\"target\":\"...\",\"type\":\"...\",\"relation_origin\":\"canonical\",\"evidence_span\":\"...\",\"context_chunk\":\"...\",\"confidence\":0.9}],\"confidence\":0.8}}",
|
|
2044
|
+
"",
|
|
2045
|
+
"[CURRENT_GRAPH_PAYLOAD]",
|
|
2046
|
+
JSON.stringify(args.graphPayload),
|
|
2047
|
+
"[/CURRENT_GRAPH_PAYLOAD]",
|
|
2048
|
+
"",
|
|
2049
|
+
"[SOURCE_SNIPPET]",
|
|
2050
|
+
sourceSnippet,
|
|
2051
|
+
"[/SOURCE_SNIPPET]",
|
|
2052
|
+
].join("\n"),
|
|
2053
|
+
},
|
|
2054
|
+
],
|
|
2055
|
+
};
|
|
2056
|
+
let lastError = null;
|
|
2057
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
2058
|
+
let response;
|
|
2059
|
+
try {
|
|
2060
|
+
response = await (0, http_post_1.postJsonWithTimeout)({
|
|
2061
|
+
endpoint,
|
|
2062
|
+
apiKey: args.llm.apiKey,
|
|
2063
|
+
body,
|
|
2064
|
+
timeoutMs: 25000,
|
|
2065
|
+
});
|
|
2066
|
+
}
|
|
2067
|
+
catch (error) {
|
|
2068
|
+
lastError = error;
|
|
2069
|
+
continue;
|
|
2070
|
+
}
|
|
2071
|
+
if (!response.ok) {
|
|
2072
|
+
lastError = new Error(response.status > 0 ? `graph_rewrite_http_${response.status}` : (response.error || "graph_rewrite_network_error"));
|
|
2073
|
+
continue;
|
|
2074
|
+
}
|
|
2075
|
+
try {
|
|
2076
|
+
const json = (response.json || {});
|
|
2077
|
+
const content = json?.choices?.[0]?.message?.content || "";
|
|
2078
|
+
if (!content.trim()) {
|
|
2079
|
+
lastError = new Error("graph_rewrite_empty");
|
|
2080
|
+
continue;
|
|
2081
|
+
}
|
|
2082
|
+
const validation = (0, llm_output_validator_1.validateLlmJsonOutput)(content);
|
|
2083
|
+
if (!validation.valid) {
|
|
2084
|
+
lastError = new Error(`graph_rewrite_invalid_json:${validation.errors.join("|")}`);
|
|
2085
|
+
continue;
|
|
2086
|
+
}
|
|
2087
|
+
const rootObj = asRecord(validation.data);
|
|
2088
|
+
const candidatePayload = rootObj
|
|
2089
|
+
? (asRecord(rootObj.graph_payload) || asRecord(rootObj.graph) || rootObj)
|
|
2090
|
+
: null;
|
|
2091
|
+
const rewritten = toAppendableGraphPayload(parseGraphPayload(candidatePayload || undefined));
|
|
2092
|
+
if (rewritten) {
|
|
2093
|
+
const rewriteValidation = validateGraphRewriteResult({
|
|
2094
|
+
basePayload: args.graphPayload,
|
|
2095
|
+
rewrittenPayload: rewritten,
|
|
2096
|
+
rewriteScope: args.rewritePlan?.rewrite_scope,
|
|
2097
|
+
schema: args.schema,
|
|
2098
|
+
});
|
|
2099
|
+
if (!rewriteValidation.valid) {
|
|
2100
|
+
lastError = new Error(`graph_rewrite_invalid_payload:${rewriteValidation.errors.join("|")}`);
|
|
2101
|
+
continue;
|
|
2102
|
+
}
|
|
2103
|
+
return rewritten;
|
|
2104
|
+
}
|
|
2105
|
+
lastError = new Error("graph_rewrite_invalid_payload");
|
|
2106
|
+
}
|
|
2107
|
+
catch (error) {
|
|
2108
|
+
lastError = error;
|
|
2109
|
+
}
|
|
2110
|
+
}
|
|
2111
|
+
args.logger.warn(`graph_rewrite_failed reason=${String(lastError || "unknown")}`);
|
|
2112
|
+
return undefined;
|
|
2113
|
+
}
|
|
201
2114
|
function createSessionSync(options) {
|
|
202
2115
|
const memoryRoot = options.dbPath ? path.resolve(options.dbPath) : path.join(options.projectRoot, "data", "memory");
|
|
203
2116
|
const statePath = path.join(memoryRoot, ".sync_state.json");
|
|
204
2117
|
const openclawBasePath = inferOpenclawBasePath(options.projectRoot);
|
|
2118
|
+
const llmModel = options.llm?.model || "";
|
|
2119
|
+
const llmApiKey = options.llm?.apiKey || "";
|
|
2120
|
+
const llmBaseUrl = normalizeBaseUrl(options.llm?.baseURL || options.llm?.baseUrl);
|
|
2121
|
+
const requireLlmForWrite = options.requireLlmForWrite !== false;
|
|
2122
|
+
const includeLocalActiveInput = options.syncPolicy?.includeLocalActiveInput === true;
|
|
2123
|
+
const syncGraphQualityMode = options.syncPolicy?.graphQualityMode
|
|
2124
|
+
|| options.graphQualityMode
|
|
2125
|
+
|| "strict";
|
|
2126
|
+
const graphSchema = (0, ontology_1.loadGraphSchema)(options.projectRoot);
|
|
2127
|
+
const relationPromptHint = (0, ontology_1.buildRelationPromptHint)(graphSchema);
|
|
2128
|
+
const activeValuePromptHint = buildActiveValuePromptHint(graphSchema);
|
|
2129
|
+
const entityDictionaryPromptHint = buildEntityDictionaryPromptHint(graphSchema);
|
|
2130
|
+
options.logger.info(`sync_gate_prompt_version=${WRITE_GATE_PROMPT_VERSION}`);
|
|
2131
|
+
options.logger.info(`sync_gate_stage_versions=ab:${WRITE_GATE_STAGE_AB_PROMPT_VERSION},c:${WRITE_GATE_STAGE_C_PROMPT_VERSION},d:${WRITE_GATE_STAGE_D_PROMPT_VERSION},rw:${WRITE_GATE_GRAPH_REWRITE_PROMPT_VERSION}`);
|
|
2132
|
+
options.logger.info(`sync_include_local_active_input=${includeLocalActiveInput}`);
|
|
2133
|
+
options.logger.info(`sync_graph_quality_mode=${syncGraphQualityMode}`);
|
|
2134
|
+
if (!fs.existsSync(statePath)) {
|
|
2135
|
+
options.logger.warn("sync_state_missing: deleting state file triggers full re-import");
|
|
2136
|
+
}
|
|
2137
|
+
async function storeFromTranscript(args) {
|
|
2138
|
+
const skipReasons = {};
|
|
2139
|
+
const activeTextMaxChars = resolveWriteCharLimit(options.writePolicy?.activeTextMaxChars, 500, 200000);
|
|
2140
|
+
const archiveSourceTextMaxChars = resolveWriteCharLimit(options.writePolicy?.archiveSourceTextMaxChars, 1000, 500000);
|
|
2141
|
+
const normalizedTranscript = denoiseTranscriptForWrite(args.transcript);
|
|
2142
|
+
function bumpReason(reason) {
|
|
2143
|
+
const key = reason || "unknown";
|
|
2144
|
+
skipReasons[key] = (skipReasons[key] || 0) + 1;
|
|
2145
|
+
}
|
|
2146
|
+
if (!normalizedTranscript.trim()) {
|
|
2147
|
+
options.logger.info(`sync_skip reason=no_active_records session=${args.sessionId}`);
|
|
2148
|
+
bumpReason("no_active_records");
|
|
2149
|
+
return { imported: 0, skipped: 1, ok: true, llmDecisions: 0, activeOnly: 0, archiveEvent: 0, skipReasons };
|
|
2150
|
+
}
|
|
2151
|
+
if (!llmModel || !llmApiKey || !llmBaseUrl) {
|
|
2152
|
+
if (requireLlmForWrite) {
|
|
2153
|
+
options.logger.warn(`sync_skip reason=llm_not_configured session=${args.sessionId}`);
|
|
2154
|
+
bumpReason("llm_not_configured");
|
|
2155
|
+
return { imported: 0, skipped: 1, ok: false, llmDecisions: 0, activeOnly: 0, archiveEvent: 0, skipReasons };
|
|
2156
|
+
}
|
|
2157
|
+
options.logger.warn(`Sync gate degraded to active_only for ${args.sessionId}: llm_not_configured`);
|
|
2158
|
+
let fallbackWrite;
|
|
2159
|
+
try {
|
|
2160
|
+
fallbackWrite = await options.writeStore.writeMemory({
|
|
2161
|
+
text: tailByCharLimit(normalizedTranscript, activeTextMaxChars),
|
|
2162
|
+
sourceText: normalizedTranscript,
|
|
2163
|
+
role: "system",
|
|
2164
|
+
source: `sync_gate_fallback:${args.sourceFile}`,
|
|
2165
|
+
sessionId: args.sessionId,
|
|
2166
|
+
});
|
|
2167
|
+
}
|
|
2168
|
+
catch (error) {
|
|
2169
|
+
options.logger.warn(`sync_skip reason=active_only_fallback_exception session=${args.sessionId} error=${String(error)}`);
|
|
2170
|
+
bumpReason("active_only_fallback_exception");
|
|
2171
|
+
return { imported: 0, skipped: 1, ok: false, llmDecisions: 1, activeOnly: 0, archiveEvent: 0, skipReasons };
|
|
2172
|
+
}
|
|
2173
|
+
if (fallbackWrite.status === "ok") {
|
|
2174
|
+
return { imported: 1, skipped: 0, ok: true, llmDecisions: 1, activeOnly: 1, archiveEvent: 0, skipReasons };
|
|
2175
|
+
}
|
|
2176
|
+
bumpReason(fallbackWrite.reason || "active_only_fallback_failed");
|
|
2177
|
+
return { imported: 0, skipped: 1, ok: false, llmDecisions: 1, activeOnly: 0, archiveEvent: 0, skipReasons };
|
|
2178
|
+
}
|
|
2179
|
+
const decisions = await extractGateDecisionsWithLlm({
|
|
2180
|
+
llm: { model: llmModel, apiKey: llmApiKey, baseUrl: llmBaseUrl },
|
|
2181
|
+
transcript: normalizedTranscript,
|
|
2182
|
+
logger: options.logger,
|
|
2183
|
+
schema: graphSchema,
|
|
2184
|
+
relationPromptHint,
|
|
2185
|
+
});
|
|
2186
|
+
const routedDecisions = appendLifecycleArchiveDecision(decisions, normalizedTranscript, options.logger);
|
|
2187
|
+
const routeCounts = routedDecisions.reduce((acc, decision) => {
|
|
2188
|
+
acc[decision.target_layer] = (acc[decision.target_layer] || 0) + 1;
|
|
2189
|
+
return acc;
|
|
2190
|
+
}, { active_only: 0, archive_event: 0, skip: 0 });
|
|
2191
|
+
const sourceSlicePresent = routedDecisions.filter(decision => {
|
|
2192
|
+
if (decision.target_layer === "active_only") {
|
|
2193
|
+
return Boolean((decision.active_source_slice || "").trim());
|
|
2194
|
+
}
|
|
2195
|
+
if (decision.target_layer === "archive_event") {
|
|
2196
|
+
return Boolean((decision.candidate_text || "").trim());
|
|
2197
|
+
}
|
|
2198
|
+
return Boolean((decision.candidate_text || "").trim());
|
|
2199
|
+
}).length;
|
|
2200
|
+
const initialGraphPayloads = routedDecisions.filter(decision => Boolean(toAppendableGraphPayload(decision.graph))).length;
|
|
2201
|
+
if (routedDecisions.length === 0) {
|
|
2202
|
+
options.logger.info(`sync_skip reason=llm_extract_empty session=${args.sessionId}`);
|
|
2203
|
+
bumpReason("llm_extract_empty");
|
|
2204
|
+
return { imported: 0, skipped: 1, ok: true, llmDecisions: 0, activeOnly: 0, archiveEvent: 0, skipReasons };
|
|
2205
|
+
}
|
|
2206
|
+
let llmDecisions = 0;
|
|
2207
|
+
let imported = 0;
|
|
2208
|
+
let skipped = 0;
|
|
2209
|
+
let activeOnly = 0;
|
|
2210
|
+
let archiveEvent = 0;
|
|
2211
|
+
let activeAttempted = 0;
|
|
2212
|
+
let archiveAttempted = 0;
|
|
2213
|
+
let graphAttempted = 0;
|
|
2214
|
+
let graphStored = 0;
|
|
2215
|
+
let graphSkipped = 0;
|
|
2216
|
+
let graphRewriteRequested = 0;
|
|
2217
|
+
let graphRewriteTriggered = 0;
|
|
2218
|
+
let graphRewriteApplied = 0;
|
|
2219
|
+
let graphRewriteFailed = 0;
|
|
2220
|
+
const archiveInputs = [];
|
|
2221
|
+
function resolveGraphSummary(payload) {
|
|
2222
|
+
return typeof payload.summary === "string" ? payload.summary.trim() : "";
|
|
2223
|
+
}
|
|
2224
|
+
function buildAppendSourceTextNav(argsForAppend) {
|
|
2225
|
+
const nav = argsForAppend.graphPayload?.source_text_nav;
|
|
2226
|
+
const forceArchiveTrace = argsForAppend.sourceLayer === "archive_event" && !!argsForAppend.archiveEventId;
|
|
2227
|
+
const sourceEventId = forceArchiveTrace
|
|
2228
|
+
? (argsForAppend.archiveEventId || argsForAppend.sourceEventId)
|
|
2229
|
+
: ((nav?.source_event_id || "").trim() || argsForAppend.sourceEventId);
|
|
2230
|
+
const sourceMemoryId = forceArchiveTrace
|
|
2231
|
+
? (argsForAppend.archiveEventId || argsForAppend.sourceEventId)
|
|
2232
|
+
: ((nav?.source_memory_id || "").trim() || argsForAppend.archiveEventId || argsForAppend.sourceEventId);
|
|
2233
|
+
return {
|
|
2234
|
+
layer: forceArchiveTrace
|
|
2235
|
+
? "archive_event"
|
|
2236
|
+
: (nav?.layer === "archive_event" || nav?.layer === "active_only"
|
|
2237
|
+
? nav.layer
|
|
2238
|
+
: argsForAppend.sourceLayer),
|
|
2239
|
+
session_id: (nav?.session_id || "").trim() || args.sessionId,
|
|
2240
|
+
source_file: (nav?.source_file || "").trim() || args.sourceFile || "unknown",
|
|
2241
|
+
source_memory_id: sourceMemoryId,
|
|
2242
|
+
source_event_id: sourceEventId,
|
|
2243
|
+
fulltext_anchor: (nav?.fulltext_anchor || "").trim() || undefined,
|
|
2244
|
+
};
|
|
2245
|
+
}
|
|
2246
|
+
async function appendGraphPayload(argsForAppend) {
|
|
2247
|
+
if (!options.graphMemoryStore) {
|
|
2248
|
+
return;
|
|
2249
|
+
}
|
|
2250
|
+
const appendableGraph = toAppendableGraphPayload(argsForAppend.graphPayload);
|
|
2251
|
+
if (!appendableGraph) {
|
|
2252
|
+
return;
|
|
2253
|
+
}
|
|
2254
|
+
const summary = resolveGraphSummary(appendableGraph);
|
|
2255
|
+
if (!summary) {
|
|
2256
|
+
graphSkipped += 1;
|
|
2257
|
+
options.logger.info(`graph_skip_reason=missing_summary source_event_id=${argsForAppend.sourceEventId}`);
|
|
2258
|
+
return;
|
|
2259
|
+
}
|
|
2260
|
+
const sourceTextNav = buildAppendSourceTextNav({
|
|
2261
|
+
sourceEventId: argsForAppend.sourceEventId,
|
|
2262
|
+
sourceLayer: argsForAppend.sourceLayer,
|
|
2263
|
+
archiveEventId: argsForAppend.archiveEventId,
|
|
2264
|
+
graphPayload: appendableGraph,
|
|
2265
|
+
});
|
|
2266
|
+
graphAttempted += 1;
|
|
2267
|
+
let graphResult;
|
|
2268
|
+
try {
|
|
2269
|
+
graphResult = await options.graphMemoryStore.append({
|
|
2270
|
+
sourceEventId: argsForAppend.sourceEventId,
|
|
2271
|
+
sourceLayer: argsForAppend.sourceLayer,
|
|
2272
|
+
archiveEventId: argsForAppend.archiveEventId,
|
|
2273
|
+
sessionId: args.sessionId,
|
|
2274
|
+
sourceFile: args.sourceFile,
|
|
2275
|
+
source_text_nav: sourceTextNav,
|
|
2276
|
+
summary,
|
|
2277
|
+
eventType: argsForAppend.eventType || "insight",
|
|
2278
|
+
entities: appendableGraph.entities,
|
|
2279
|
+
entity_types: appendableGraph.entity_types,
|
|
2280
|
+
relations: appendableGraph.relations,
|
|
2281
|
+
gateSource: "sync",
|
|
2282
|
+
confidence: appendableGraph.confidence,
|
|
2283
|
+
sourceText: argsForAppend.sourceText,
|
|
2284
|
+
qualityMode: syncGraphQualityMode,
|
|
2285
|
+
});
|
|
2286
|
+
}
|
|
2287
|
+
catch (error) {
|
|
2288
|
+
graphSkipped += 1;
|
|
2289
|
+
options.logger.warn(`graph_append_exception source_event_id=${argsForAppend.sourceEventId} error=${String(error)}`);
|
|
2290
|
+
return;
|
|
2291
|
+
}
|
|
2292
|
+
if (!graphResult.success) {
|
|
2293
|
+
graphSkipped += 1;
|
|
2294
|
+
options.logger.info(`graph_skip_reason=${graphResult.reason} source_event_id=${argsForAppend.sourceEventId}`);
|
|
2295
|
+
return;
|
|
2296
|
+
}
|
|
2297
|
+
graphStored += 1;
|
|
2298
|
+
}
|
|
2299
|
+
function buildGraphOnlySourceEventId(argsForGraphOnly) {
|
|
2300
|
+
if (argsForGraphOnly.candidateId) {
|
|
2301
|
+
return `candidate:${args.sessionId}:${argsForGraphOnly.candidateId}`;
|
|
2302
|
+
}
|
|
2303
|
+
const relationFingerprint = (argsForGraphOnly.graphPayload?.relations || [])
|
|
2304
|
+
.map(rel => `${rel.source}|${rel.type}|${rel.target}|${rel.evidence_span || ""}`)
|
|
2305
|
+
.sort()
|
|
2306
|
+
.join("||");
|
|
2307
|
+
return `graph_only:${args.sessionId}:${crypto.createHash("sha1").update(relationFingerprint || argsForGraphOnly.fallbackText).digest("hex").slice(0, 16)}`;
|
|
2308
|
+
}
|
|
2309
|
+
for (const decision of routedDecisions) {
|
|
2310
|
+
llmDecisions += 1;
|
|
2311
|
+
const decisionActiveSummary = normalizeOneLineText(decision.active_summary || "");
|
|
2312
|
+
const decisionActiveSourceSlice = (decision.active_source_slice || "").trim();
|
|
2313
|
+
const fallbackGraphPayload = !decision.graph
|
|
2314
|
+
? buildStablePersonalFactGraph(decisionActiveSummary || decision.candidate_text || normalizedTranscript)
|
|
2315
|
+
: null;
|
|
2316
|
+
let graphPayload = mergeGraphPayload(toAppendableGraphPayload(decision.graph), toAppendableGraphPayload(fallbackGraphPayload || undefined));
|
|
2317
|
+
if (decisionActiveSummary && graphPayload && !normalizeOneLineText(graphPayload.summary || "")) {
|
|
2318
|
+
graphPayload = { ...graphPayload, summary: decisionActiveSummary };
|
|
2319
|
+
}
|
|
2320
|
+
const mergeReviewed = applyMergeHintToGraphPayload({
|
|
2321
|
+
graphPayload,
|
|
2322
|
+
mergeHint: decision.merge_hint,
|
|
2323
|
+
});
|
|
2324
|
+
graphPayload = mergeReviewed.graphPayload;
|
|
2325
|
+
for (const warning of mergeReviewed.warnings) {
|
|
2326
|
+
bumpReason(warning);
|
|
2327
|
+
options.logger.warn(`graph_merge_hint_warning reason=${warning} session=${args.sessionId} candidate_id=${decision.candidate_id || "unknown"}`);
|
|
2328
|
+
}
|
|
2329
|
+
if (decision.graph_rewrite?.rewrite_required) {
|
|
2330
|
+
graphRewriteRequested += 1;
|
|
2331
|
+
graphRewriteTriggered += 1;
|
|
2332
|
+
let rewriteApplied = false;
|
|
2333
|
+
const mergedRewritePayload = mergeGraphPayload(graphPayload, decision.graph_rewrite.graph_rewrite_payload);
|
|
2334
|
+
if (mergedRewritePayload) {
|
|
2335
|
+
const plannerRewriteValidation = validateGraphRewriteResult({
|
|
2336
|
+
basePayload: graphPayload,
|
|
2337
|
+
rewrittenPayload: mergedRewritePayload,
|
|
2338
|
+
rewriteScope: decision.graph_rewrite.rewrite_scope,
|
|
2339
|
+
schema: graphSchema,
|
|
2340
|
+
});
|
|
2341
|
+
if (plannerRewriteValidation.valid) {
|
|
2342
|
+
graphPayload = mergedRewritePayload;
|
|
2343
|
+
graphRewriteApplied += 1;
|
|
2344
|
+
rewriteApplied = true;
|
|
2345
|
+
}
|
|
2346
|
+
else {
|
|
2347
|
+
bumpReason("graph_rewrite_invalid_payload");
|
|
2348
|
+
options.logger.warn(`graph_rewrite_invalid source=planner session=${args.sessionId} candidate_id=${decision.candidate_id || "unknown"} errors=${plannerRewriteValidation.errors.join("|")}`);
|
|
2349
|
+
}
|
|
2350
|
+
}
|
|
2351
|
+
if (!rewriteApplied && graphPayload) {
|
|
2352
|
+
const rewritten = await rewriteGraphPayloadWithLlm({
|
|
2353
|
+
llm: { model: llmModel, apiKey: llmApiKey, baseUrl: llmBaseUrl },
|
|
2354
|
+
transcript: normalizedTranscript,
|
|
2355
|
+
candidateText: decision.candidate_text || decisionActiveSummary,
|
|
2356
|
+
graphPayload,
|
|
2357
|
+
rewritePlan: decision.graph_rewrite,
|
|
2358
|
+
mergeHint: decision.merge_hint,
|
|
2359
|
+
schema: graphSchema,
|
|
2360
|
+
relationPromptHint,
|
|
2361
|
+
activeValuePromptHint,
|
|
2362
|
+
entityDictionaryPromptHint,
|
|
2363
|
+
logger: options.logger,
|
|
2364
|
+
});
|
|
2365
|
+
if (rewritten) {
|
|
2366
|
+
graphPayload = rewritten;
|
|
2367
|
+
graphRewriteApplied += 1;
|
|
2368
|
+
rewriteApplied = true;
|
|
2369
|
+
}
|
|
2370
|
+
else {
|
|
2371
|
+
graphRewriteFailed += 1;
|
|
2372
|
+
}
|
|
2373
|
+
}
|
|
2374
|
+
if (!rewriteApplied && !graphPayload) {
|
|
2375
|
+
graphRewriteFailed += 1;
|
|
2376
|
+
}
|
|
2377
|
+
}
|
|
2378
|
+
if (decision.target_layer === "skip") {
|
|
2379
|
+
if (graphPayload) {
|
|
2380
|
+
const graphOnlySourceEventId = buildGraphOnlySourceEventId({
|
|
2381
|
+
candidateId: decision.candidate_id,
|
|
2382
|
+
graphPayload,
|
|
2383
|
+
fallbackText: normalizedTranscript,
|
|
2384
|
+
});
|
|
2385
|
+
await appendGraphPayload({
|
|
2386
|
+
sourceEventId: graphOnlySourceEventId,
|
|
2387
|
+
sourceLayer: "active_only",
|
|
2388
|
+
eventType: "insight",
|
|
2389
|
+
sourceText: normalizedTranscript,
|
|
2390
|
+
graphPayload,
|
|
2391
|
+
});
|
|
2392
|
+
}
|
|
2393
|
+
skipped += 1;
|
|
2394
|
+
bumpReason(decision.reason || "llm_gate_skip");
|
|
2395
|
+
continue;
|
|
2396
|
+
}
|
|
2397
|
+
if (decision.target_layer === "active_only") {
|
|
2398
|
+
activeAttempted += 1;
|
|
2399
|
+
const activeSummary = decisionActiveSummary
|
|
2400
|
+
|| normalizeOneLineText(graphPayload?.summary || "")
|
|
2401
|
+
|| normalizeOneLineText(decision.candidate_text || "");
|
|
2402
|
+
const activeText = tailByCharLimit(activeSummary, activeTextMaxChars);
|
|
2403
|
+
const activeSourceSlice = decisionActiveSourceSlice;
|
|
2404
|
+
if (activeSummary && graphPayload && !normalizeOneLineText(graphPayload.summary || "")) {
|
|
2405
|
+
graphPayload = { ...graphPayload, summary: activeSummary };
|
|
2406
|
+
}
|
|
2407
|
+
if (!activeText) {
|
|
2408
|
+
if (graphPayload) {
|
|
2409
|
+
const graphOnlySourceEventId = buildGraphOnlySourceEventId({
|
|
2410
|
+
candidateId: decision.candidate_id,
|
|
2411
|
+
graphPayload,
|
|
2412
|
+
fallbackText: normalizedTranscript,
|
|
2413
|
+
});
|
|
2414
|
+
await appendGraphPayload({
|
|
2415
|
+
sourceEventId: graphOnlySourceEventId,
|
|
2416
|
+
sourceLayer: "active_only",
|
|
2417
|
+
eventType: "insight",
|
|
2418
|
+
sourceText: decision.candidate_text || normalizedTranscript,
|
|
2419
|
+
graphPayload,
|
|
2420
|
+
});
|
|
2421
|
+
}
|
|
2422
|
+
skipped += 1;
|
|
2423
|
+
bumpReason("active_summary_missing");
|
|
2424
|
+
continue;
|
|
2425
|
+
}
|
|
2426
|
+
if (!activeSourceSlice) {
|
|
2427
|
+
skipped += 1;
|
|
2428
|
+
bumpReason("active_source_slice_missing");
|
|
2429
|
+
options.logger.warn(`sync_skip reason=active_source_slice_missing session=${args.sessionId} candidate_id=${decision.candidate_id || "unknown"}`);
|
|
2430
|
+
continue;
|
|
2431
|
+
}
|
|
2432
|
+
if (!hasValuableActiveContent(activeText)) {
|
|
2433
|
+
if (graphPayload) {
|
|
2434
|
+
const graphOnlySourceEventId = buildGraphOnlySourceEventId({
|
|
2435
|
+
candidateId: decision.candidate_id,
|
|
2436
|
+
graphPayload,
|
|
2437
|
+
fallbackText: activeText,
|
|
2438
|
+
});
|
|
2439
|
+
await appendGraphPayload({
|
|
2440
|
+
sourceEventId: graphOnlySourceEventId,
|
|
2441
|
+
sourceLayer: "active_only",
|
|
2442
|
+
eventType: "insight",
|
|
2443
|
+
sourceText: activeSourceSlice,
|
|
2444
|
+
graphPayload,
|
|
2445
|
+
});
|
|
2446
|
+
}
|
|
2447
|
+
skipped += 1;
|
|
2448
|
+
bumpReason("active_only_low_value");
|
|
2449
|
+
options.logger.info(`sync_skip reason=active_only_low_value session=${args.sessionId} candidate_id=${decision.candidate_id || "unknown"}`);
|
|
2450
|
+
continue;
|
|
2451
|
+
}
|
|
2452
|
+
if (!graphPayload) {
|
|
2453
|
+
skipped += 1;
|
|
2454
|
+
bumpReason("graph_payload_required_active");
|
|
2455
|
+
options.logger.warn(`sync_skip reason=graph_payload_required_active session=${args.sessionId} candidate_id=${decision.candidate_id || "unknown"}`);
|
|
2456
|
+
continue;
|
|
2457
|
+
}
|
|
2458
|
+
let writeResult;
|
|
2459
|
+
try {
|
|
2460
|
+
writeResult = await options.writeStore.writeMemory({
|
|
2461
|
+
text: activeText,
|
|
2462
|
+
summary: activeSummary || undefined,
|
|
2463
|
+
sourceText: activeSourceSlice,
|
|
2464
|
+
role: "system",
|
|
2465
|
+
source: `sync_gate_active:${args.sourceFile}`,
|
|
2466
|
+
sessionId: args.sessionId,
|
|
2467
|
+
});
|
|
2468
|
+
}
|
|
2469
|
+
catch (error) {
|
|
2470
|
+
skipped += 1;
|
|
2471
|
+
bumpReason("active_only_write_exception");
|
|
2472
|
+
options.logger.warn(`sync_skip reason=active_only_write_exception session=${args.sessionId} candidate_id=${decision.candidate_id || "unknown"} error=${String(error)}`);
|
|
2473
|
+
continue;
|
|
2474
|
+
}
|
|
2475
|
+
if (writeResult.status === "ok") {
|
|
2476
|
+
imported += 1;
|
|
2477
|
+
activeOnly += 1;
|
|
2478
|
+
if (graphPayload) {
|
|
2479
|
+
const relationFingerprint = (graphPayload.relations || [])
|
|
2480
|
+
.map(rel => `${rel.source}|${rel.type}|${rel.target}|${rel.evidence_span || ""}`)
|
|
2481
|
+
.sort()
|
|
2482
|
+
.join("||");
|
|
2483
|
+
const activeSourceEventId = `active:${args.sessionId}:${crypto.createHash("sha1").update(relationFingerprint || activeText).digest("hex").slice(0, 16)}`;
|
|
2484
|
+
await appendGraphPayload({
|
|
2485
|
+
sourceEventId: activeSourceEventId,
|
|
2486
|
+
sourceLayer: "active_only",
|
|
2487
|
+
eventType: "insight",
|
|
2488
|
+
sourceText: activeSourceSlice,
|
|
2489
|
+
graphPayload,
|
|
2490
|
+
});
|
|
2491
|
+
}
|
|
2492
|
+
}
|
|
2493
|
+
else {
|
|
2494
|
+
skipped += 1;
|
|
2495
|
+
bumpReason(writeResult.reason || "active_only_write_skipped");
|
|
2496
|
+
}
|
|
2497
|
+
continue;
|
|
2498
|
+
}
|
|
2499
|
+
if (decision.target_layer === "archive_event") {
|
|
2500
|
+
archiveAttempted += 1;
|
|
2501
|
+
const isLifecycleFallback = decision.reason === "lifecycle_archive_fallback";
|
|
2502
|
+
if (!decision.event) {
|
|
2503
|
+
skipped += 1;
|
|
2504
|
+
bumpReason("archive_event_missing_payload");
|
|
2505
|
+
continue;
|
|
2506
|
+
}
|
|
2507
|
+
const archiveFallbackGraph = toAppendableGraphPayload({
|
|
2508
|
+
summary: ensureSummaryMentionsEntities(resolveGraphSummary(graphPayload || {}) || decision.event.summary, decision.event.entities),
|
|
2509
|
+
entities: decision.event.entities,
|
|
2510
|
+
entity_types: decision.event.entity_types,
|
|
2511
|
+
relations: decision.event.relations,
|
|
2512
|
+
confidence: decision.event.confidence,
|
|
2513
|
+
});
|
|
2514
|
+
graphPayload = mergeGraphPayload(graphPayload, archiveFallbackGraph) || graphPayload || archiveFallbackGraph;
|
|
2515
|
+
if (!graphPayload && !isLifecycleFallback) {
|
|
2516
|
+
skipped += 1;
|
|
2517
|
+
bumpReason("graph_payload_required_archive");
|
|
2518
|
+
options.logger.warn(`sync_skip reason=graph_payload_required_archive session=${args.sessionId} candidate_id=${decision.candidate_id || "unknown"}`);
|
|
2519
|
+
continue;
|
|
2520
|
+
}
|
|
2521
|
+
const archiveSourceSlice = tailByCharLimit(denoiseTranscriptForWrite(decision.candidate_text || normalizedTranscript), archiveSourceTextMaxChars);
|
|
2522
|
+
archiveInputs.push({
|
|
2523
|
+
candidate_id: decision.candidate_id,
|
|
2524
|
+
graph_payload: graphPayload,
|
|
2525
|
+
event_type: decision.event.event_type,
|
|
2526
|
+
summary: decision.event.summary,
|
|
2527
|
+
cause: decision.event.cause,
|
|
2528
|
+
process: decision.event.process,
|
|
2529
|
+
result: decision.event.result,
|
|
2530
|
+
entities: decision.event.entities,
|
|
2531
|
+
relations: decision.event.relations,
|
|
2532
|
+
entity_types: decision.event.entity_types,
|
|
2533
|
+
confidence: decision.event.confidence,
|
|
2534
|
+
session_id: args.sessionId,
|
|
2535
|
+
source_file: args.sourceFile,
|
|
2536
|
+
source_text: archiveSourceSlice || tailByCharLimit(normalizedTranscript, archiveSourceTextMaxChars),
|
|
2537
|
+
source_event_id: decision.candidate_id
|
|
2538
|
+
? `candidate:${args.sessionId}:${decision.candidate_id}`
|
|
2539
|
+
: `candidate:${args.sessionId}:${crypto.createHash("sha1").update(decision.event.summary).digest("hex").slice(0, 16)}`,
|
|
2540
|
+
actor: "sync_llm_gate",
|
|
2541
|
+
});
|
|
2542
|
+
}
|
|
2543
|
+
}
|
|
2544
|
+
if (archiveInputs.length > 0) {
|
|
2545
|
+
let archivedSuccess = 0;
|
|
2546
|
+
let archivedSkipped = 0;
|
|
2547
|
+
for (const inputRecord of archiveInputs) {
|
|
2548
|
+
let archiveResult;
|
|
2549
|
+
try {
|
|
2550
|
+
archiveResult = await options.archiveStore.storeEvents([inputRecord]);
|
|
2551
|
+
}
|
|
2552
|
+
catch (error) {
|
|
2553
|
+
archivedSkipped += 1;
|
|
2554
|
+
skipped += 1;
|
|
2555
|
+
bumpReason("archive_store_exception");
|
|
2556
|
+
options.logger.warn(`sync_skip reason=archive_store_exception session=${args.sessionId} candidate_id=${inputRecord.candidate_id || "unknown"} error=${String(error)}`);
|
|
2557
|
+
continue;
|
|
2558
|
+
}
|
|
2559
|
+
imported += archiveResult.stored.length;
|
|
2560
|
+
skipped += archiveResult.skipped.length;
|
|
2561
|
+
archiveEvent += archiveResult.stored.length;
|
|
2562
|
+
archivedSuccess += archiveResult.stored.length;
|
|
2563
|
+
archivedSkipped += archiveResult.skipped.length;
|
|
2564
|
+
for (const skip of archiveResult.skipped) {
|
|
2565
|
+
bumpReason(skip.reason || "archive_store_skipped");
|
|
2566
|
+
}
|
|
2567
|
+
const archiveRecord = archiveResult.stored[0];
|
|
2568
|
+
if (!archiveRecord) {
|
|
2569
|
+
continue;
|
|
2570
|
+
}
|
|
2571
|
+
const archiveGraphPayload = inputRecord.graph_payload
|
|
2572
|
+
|| toAppendableGraphPayload({
|
|
2573
|
+
entities: inputRecord.entities,
|
|
2574
|
+
entity_types: inputRecord.entity_types,
|
|
2575
|
+
relations: inputRecord.relations,
|
|
2576
|
+
confidence: inputRecord.confidence,
|
|
2577
|
+
});
|
|
2578
|
+
await appendGraphPayload({
|
|
2579
|
+
// Graph trace points to persisted archive record id for stable lookup.
|
|
2580
|
+
sourceEventId: archiveRecord.id,
|
|
2581
|
+
sourceLayer: "archive_event",
|
|
2582
|
+
archiveEventId: archiveRecord.id,
|
|
2583
|
+
eventType: inputRecord.event_type,
|
|
2584
|
+
sourceText: inputRecord.source_text || normalizedTranscript,
|
|
2585
|
+
graphPayload: archiveGraphPayload,
|
|
2586
|
+
});
|
|
2587
|
+
}
|
|
2588
|
+
options.logger.info(`sync_archive_result session=${args.sessionId} archived_success=${archivedSuccess} skipped=${archivedSkipped}`);
|
|
2589
|
+
}
|
|
2590
|
+
options.logger.info(`sync_gate_result session=${args.sessionId} llm_decisions=${llmDecisions} active_only=${activeOnly} archive_event=${archiveEvent} skipped=${skipped}`);
|
|
2591
|
+
options.logger.info(`sync_gate_metrics session=${args.sessionId} active_attempted=${activeAttempted} archive_attempted=${archiveAttempted} graph_attempted=${graphAttempted} graph_stored=${graphStored} graph_skipped=${graphSkipped} graph_rewrite_requested=${graphRewriteRequested} graph_rewrite_triggered=${graphRewriteTriggered} graph_rewrite_applied=${graphRewriteApplied} graph_rewrite_failed=${graphRewriteFailed} skip_reason_kinds=${Object.keys(skipReasons).length}`);
|
|
2592
|
+
const topSkipReasons = Object.entries(skipReasons)
|
|
2593
|
+
.sort((a, b) => b[1] - a[1])
|
|
2594
|
+
.slice(0, 6)
|
|
2595
|
+
.map(([reason, count]) => `${reason}:${count}`)
|
|
2596
|
+
.join(",");
|
|
2597
|
+
options.logger.info(`sync_gate_quality_metrics session=${args.sessionId} llm_candidates=${decisions.length} routed_total=${routedDecisions.length} routed_active=${routeCounts.active_only || 0} routed_archive=${routeCounts.archive_event || 0} routed_skip=${routeCounts.skip || 0} source_slice_present=${sourceSlicePresent}/${routedDecisions.length} initial_graph_payloads=${initialGraphPayloads}/${routedDecisions.length} top_skip_reasons=${topSkipReasons || "none"}`);
|
|
2598
|
+
return {
|
|
2599
|
+
imported,
|
|
2600
|
+
skipped,
|
|
2601
|
+
ok: true,
|
|
2602
|
+
llmDecisions,
|
|
2603
|
+
activeOnly,
|
|
2604
|
+
archiveEvent,
|
|
2605
|
+
skipReasons,
|
|
2606
|
+
};
|
|
2607
|
+
}
|
|
205
2608
|
async function syncDailySummaries() {
|
|
206
2609
|
const files = gatherDailySummaryFiles(openclawBasePath);
|
|
207
2610
|
const state = readState(statePath);
|
|
@@ -211,6 +2614,10 @@ function createSessionSync(options) {
|
|
|
211
2614
|
let imported = 0;
|
|
212
2615
|
let skipped = 0;
|
|
213
2616
|
let filesProcessed = 0;
|
|
2617
|
+
let llmDecisions = 0;
|
|
2618
|
+
let activeOnly = 0;
|
|
2619
|
+
let archiveEvent = 0;
|
|
2620
|
+
const skipReasons = {};
|
|
214
2621
|
for (const filePath of files) {
|
|
215
2622
|
if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) {
|
|
216
2623
|
continue;
|
|
@@ -229,33 +2636,43 @@ function createSessionSync(options) {
|
|
|
229
2636
|
continue;
|
|
230
2637
|
}
|
|
231
2638
|
const summarySessionId = `daily_summary:${path.basename(filePath)}`;
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
2639
|
+
const transcript = chunks.join("\n");
|
|
2640
|
+
const result = await storeFromTranscript({
|
|
2641
|
+
sessionId: summarySessionId,
|
|
2642
|
+
sourceFile: `daily_summary_sync:${path.basename(filePath)}`,
|
|
2643
|
+
transcript,
|
|
2644
|
+
});
|
|
2645
|
+
imported += result.imported;
|
|
2646
|
+
skipped += result.skipped;
|
|
2647
|
+
llmDecisions += result.llmDecisions;
|
|
2648
|
+
activeOnly += result.activeOnly;
|
|
2649
|
+
archiveEvent += result.archiveEvent;
|
|
2650
|
+
for (const [key, count] of Object.entries(result.skipReasons)) {
|
|
2651
|
+
skipReasons[key] = (skipReasons[key] || 0) + count;
|
|
2652
|
+
}
|
|
2653
|
+
if (!result.ok) {
|
|
2654
|
+
continue;
|
|
245
2655
|
}
|
|
246
2656
|
state.markdowns[filePath] = { digest, importedAt: new Date().toISOString() };
|
|
247
2657
|
filesProcessed += 1;
|
|
248
2658
|
}
|
|
249
2659
|
writeState(statePath, state);
|
|
250
2660
|
options.logger.info(`TS daily summary sync completed: imported=${imported}, skipped=${skipped}, files=${filesProcessed}`);
|
|
251
|
-
return { imported, skipped, filesProcessed };
|
|
2661
|
+
return { imported, skipped, filesProcessed, llmDecisions, activeOnly, archiveEvent, skipReasons };
|
|
252
2662
|
}
|
|
253
2663
|
async function syncMemory() {
|
|
254
|
-
const files = gatherSessionFiles(openclawBasePath, memoryRoot);
|
|
2664
|
+
const files = gatherSessionFiles(openclawBasePath, memoryRoot, includeLocalActiveInput);
|
|
2665
|
+
if (files.length === 0) {
|
|
2666
|
+
options.logger.info("sync_skip reason=no_active_records");
|
|
2667
|
+
}
|
|
255
2668
|
const state = readState(statePath);
|
|
256
2669
|
let imported = 0;
|
|
257
2670
|
let skipped = 0;
|
|
258
2671
|
let filesProcessed = 0;
|
|
2672
|
+
let llmDecisions = 0;
|
|
2673
|
+
let activeOnly = 0;
|
|
2674
|
+
let archiveEvent = 0;
|
|
2675
|
+
const skipReasons = {};
|
|
259
2676
|
for (const filePath of files) {
|
|
260
2677
|
if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) {
|
|
261
2678
|
continue;
|
|
@@ -272,32 +2689,34 @@ function createSessionSync(options) {
|
|
|
272
2689
|
state.files[filePath] = { size: stat.size, lineCount: lines.length };
|
|
273
2690
|
continue;
|
|
274
2691
|
}
|
|
2692
|
+
const bySession = new Map();
|
|
2693
|
+
let fileHasFailure = false;
|
|
2694
|
+
const fileSessionSeed = path.basename(filePath, path.extname(filePath));
|
|
2695
|
+
let fileSessionId;
|
|
275
2696
|
for (let i = startIndex; i < lines.length; i++) {
|
|
276
2697
|
const line = lines[i].trim();
|
|
277
2698
|
if (!line)
|
|
278
2699
|
continue;
|
|
279
|
-
const hash = crypto.createHash("sha1").update(line).digest("hex").slice(0, 12);
|
|
280
2700
|
try {
|
|
281
2701
|
const record = JSON.parse(line);
|
|
2702
|
+
if (!fileSessionId) {
|
|
2703
|
+
const inferred = getSessionId(record, fileSessionSeed);
|
|
2704
|
+
if (inferred && inferred !== `sync:${fileSessionSeed}`) {
|
|
2705
|
+
fileSessionId = inferred;
|
|
2706
|
+
}
|
|
2707
|
+
}
|
|
282
2708
|
const messages = extractMessages(record);
|
|
283
2709
|
if (messages.length === 0) {
|
|
284
2710
|
skipped += 1;
|
|
285
2711
|
continue;
|
|
286
2712
|
}
|
|
287
|
-
const
|
|
2713
|
+
const fallbackSession = fileSessionId || fileSessionSeed;
|
|
2714
|
+
const sessionId = getSessionId(record, fallbackSession);
|
|
288
2715
|
for (const msg of messages) {
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
role: msg.role,
|
|
292
|
-
source: "sync",
|
|
293
|
-
sessionId,
|
|
294
|
-
});
|
|
295
|
-
if (result.status === "ok") {
|
|
296
|
-
imported += 1;
|
|
297
|
-
}
|
|
298
|
-
else {
|
|
299
|
-
skipped += 1;
|
|
2716
|
+
if (!bySession.has(sessionId)) {
|
|
2717
|
+
bySession.set(sessionId, []);
|
|
300
2718
|
}
|
|
2719
|
+
bySession.get(sessionId)?.push(`[${msg.role}] ${msg.text}`);
|
|
301
2720
|
}
|
|
302
2721
|
}
|
|
303
2722
|
catch (error) {
|
|
@@ -305,20 +2724,54 @@ function createSessionSync(options) {
|
|
|
305
2724
|
skipped += 1;
|
|
306
2725
|
}
|
|
307
2726
|
}
|
|
2727
|
+
for (const [sessionId, messages] of bySession.entries()) {
|
|
2728
|
+
const transcript = messages.join("\n");
|
|
2729
|
+
const result = await storeFromTranscript({
|
|
2730
|
+
sessionId,
|
|
2731
|
+
sourceFile: `sync:${path.basename(filePath)}`,
|
|
2732
|
+
transcript,
|
|
2733
|
+
});
|
|
2734
|
+
imported += result.imported;
|
|
2735
|
+
skipped += result.skipped;
|
|
2736
|
+
llmDecisions += result.llmDecisions;
|
|
2737
|
+
activeOnly += result.activeOnly;
|
|
2738
|
+
archiveEvent += result.archiveEvent;
|
|
2739
|
+
for (const [key, count] of Object.entries(result.skipReasons)) {
|
|
2740
|
+
skipReasons[key] = (skipReasons[key] || 0) + count;
|
|
2741
|
+
}
|
|
2742
|
+
if (!result.ok) {
|
|
2743
|
+
fileHasFailure = true;
|
|
2744
|
+
}
|
|
2745
|
+
}
|
|
308
2746
|
filesProcessed += 1;
|
|
309
|
-
|
|
2747
|
+
if (!fileHasFailure) {
|
|
2748
|
+
state.files[filePath] = { size: stat.size, lineCount: lines.length };
|
|
2749
|
+
}
|
|
310
2750
|
}
|
|
311
2751
|
writeState(statePath, state);
|
|
312
2752
|
const summary = await syncDailySummaries();
|
|
313
|
-
|
|
2753
|
+
llmDecisions += summary.llmDecisions;
|
|
2754
|
+
activeOnly += summary.activeOnly;
|
|
2755
|
+
archiveEvent += summary.archiveEvent;
|
|
2756
|
+
for (const [key, count] of Object.entries(summary.skipReasons)) {
|
|
2757
|
+
skipReasons[key] = (skipReasons[key] || 0) + count;
|
|
2758
|
+
}
|
|
2759
|
+
options.logger.info(`TS sync completed: imported=${imported}, skipped=${skipped}, files=${filesProcessed}, summaryImported=${summary.imported}, summarySkipped=${summary.skipped}, llmDecisions=${llmDecisions}, activeOnly=${activeOnly}, archiveEvent=${archiveEvent}`);
|
|
314
2760
|
return {
|
|
315
2761
|
imported,
|
|
316
2762
|
skipped,
|
|
317
2763
|
filesProcessed,
|
|
318
2764
|
summaryImported: summary.imported,
|
|
319
2765
|
summarySkipped: summary.skipped,
|
|
2766
|
+
llmDecisions,
|
|
2767
|
+
activeOnly,
|
|
2768
|
+
archiveEvent,
|
|
2769
|
+
skipReasons,
|
|
320
2770
|
};
|
|
321
2771
|
}
|
|
322
|
-
|
|
2772
|
+
async function routeTranscript(args) {
|
|
2773
|
+
return storeFromTranscript(args);
|
|
2774
|
+
}
|
|
2775
|
+
return { syncMemory, syncDailySummaries, routeTranscript };
|
|
323
2776
|
}
|
|
324
2777
|
//# sourceMappingURL=session_sync.js.map
|