openclaw-cortex-memory 0.1.0-Alpha.30 → 0.1.0-Alpha.31
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 +40 -6
- package/SIGNATURE.md +7 -0
- package/SKILL.md +18 -3
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +148 -6
- package/dist/index.js.map +1 -1
- package/dist/openclaw.plugin.json +120 -4
- package/dist/src/engine/memory_engine.d.ts +5 -1
- package/dist/src/engine/memory_engine.d.ts.map +1 -1
- package/dist/src/engine/ts_engine.d.ts +116 -0
- package/dist/src/engine/ts_engine.d.ts.map +1 -1
- package/dist/src/engine/ts_engine.js +417 -102
- package/dist/src/engine/ts_engine.js.map +1 -1
- package/dist/src/engine/types.d.ts +17 -0
- package/dist/src/engine/types.d.ts.map +1 -1
- package/dist/src/graph/ontology.d.ts +23 -1
- package/dist/src/graph/ontology.d.ts.map +1 -1
- package/dist/src/graph/ontology.js +743 -70
- package/dist/src/graph/ontology.js.map +1 -1
- package/dist/src/quality/llm_output_validator.d.ts +20 -2
- package/dist/src/quality/llm_output_validator.d.ts.map +1 -1
- package/dist/src/quality/llm_output_validator.js +296 -41
- package/dist/src/quality/llm_output_validator.js.map +1 -1
- package/dist/src/store/archive_store.d.ts +8 -0
- package/dist/src/store/archive_store.d.ts.map +1 -1
- package/dist/src/store/archive_store.js +244 -84
- package/dist/src/store/archive_store.js.map +1 -1
- package/dist/src/store/graph_memory_store.d.ts +72 -2
- package/dist/src/store/graph_memory_store.d.ts.map +1 -1
- package/dist/src/store/graph_memory_store.js +723 -50
- package/dist/src/store/graph_memory_store.js.map +1 -1
- package/dist/src/store/read_store.d.ts +3 -0
- package/dist/src/store/read_store.d.ts.map +1 -1
- package/dist/src/store/read_store.js +1004 -209
- package/dist/src/store/read_store.js.map +1 -1
- package/dist/src/store/vector_store.d.ts +1 -0
- package/dist/src/store/vector_store.d.ts.map +1 -1
- package/dist/src/store/vector_store.js +1 -0
- package/dist/src/store/vector_store.js.map +1 -1
- package/dist/src/store/write_store.d.ts +2 -0
- package/dist/src/store/write_store.d.ts.map +1 -1
- package/dist/src/store/write_store.js +45 -3
- package/dist/src/store/write_store.js.map +1 -1
- package/dist/src/sync/session_sync.d.ts +20 -1
- package/dist/src/sync/session_sync.d.ts.map +1 -1
- package/dist/src/sync/session_sync.js +1810 -161
- package/dist/src/sync/session_sync.js.map +1 -1
- package/dist/src/wiki/wiki_linter.d.ts +25 -0
- package/dist/src/wiki/wiki_linter.d.ts.map +1 -0
- package/dist/src/wiki/wiki_linter.js +268 -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 +36 -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 +33 -0
- package/dist/src/wiki/wiki_projector.d.ts.map +1 -0
- package/dist/src/wiki/wiki_projector.js +633 -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 +120 -4
- package/package.json +7 -2
- package/schema/graph.schema.yaml +188 -33
- package/scripts/install.js +27 -0
- package/skills/cortex-memory/SKILL.md +49 -0
- package/skills/cortex-memory/references/agent-manual.md +115 -0
- package/skills/cortex-memory/references/configuration.md +92 -0
- package/skills/cortex-memory/references/publish-checklist.md +46 -0
- package/skills/cortex-memory/references/system-prompt-template.md +27 -0
- package/skills/cortex-memory/references/tools.md +181 -0
- package/skills/cortex-memory/scripts/smoke-check.ps1 +56 -0
|
@@ -38,6 +38,7 @@ const crypto = __importStar(require("crypto"));
|
|
|
38
38
|
const fs = __importStar(require("fs"));
|
|
39
39
|
const path = __importStar(require("path"));
|
|
40
40
|
const http_post_1 = require("../net/http_post");
|
|
41
|
+
const ontology_1 = require("../graph/ontology");
|
|
41
42
|
const llm_output_validator_1 = require("../quality/llm_output_validator");
|
|
42
43
|
const runtime_env_1 = require("../utils/runtime_env");
|
|
43
44
|
function asRecord(value) {
|
|
@@ -122,7 +123,7 @@ function writeState(filePath, state) {
|
|
|
122
123
|
state.version = SYNC_STATE_VERSION;
|
|
123
124
|
fs.writeFileSync(filePath, JSON.stringify(state, null, 2), "utf-8");
|
|
124
125
|
}
|
|
125
|
-
function gatherSessionFiles(openclawBasePath, memoryRoot) {
|
|
126
|
+
function gatherSessionFiles(openclawBasePath, memoryRoot, includeLocalActiveInput) {
|
|
126
127
|
const results = new Set();
|
|
127
128
|
const openclawSessionsDir = path.join(openclawBasePath, "agents", "main", "sessions");
|
|
128
129
|
const localActiveFile = path.join(memoryRoot, "sessions", "active", "sessions.jsonl");
|
|
@@ -133,7 +134,7 @@ function gatherSessionFiles(openclawBasePath, memoryRoot) {
|
|
|
133
134
|
}
|
|
134
135
|
}
|
|
135
136
|
}
|
|
136
|
-
if (fs.existsSync(localActiveFile) && fs.statSync(localActiveFile).isFile()) {
|
|
137
|
+
if (includeLocalActiveInput && fs.existsSync(localActiveFile) && fs.statSync(localActiveFile).isFile()) {
|
|
137
138
|
results.add(localActiveFile);
|
|
138
139
|
}
|
|
139
140
|
return [...results];
|
|
@@ -257,6 +258,76 @@ function normalizeBaseUrl(value) {
|
|
|
257
258
|
return "";
|
|
258
259
|
return value.endsWith("/") ? value.slice(0, -1) : value;
|
|
259
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
|
+
const LOW_INFORMATION_LINE = /^(ok|okay|got it|roger|noted|sure|thanks|thank you|received|copy that|understood)\b/i;
|
|
277
|
+
const LOW_VALUE_ONLY_LINE = /^(好的|收到|谢谢|辛苦|了解|明白|可以|没问题|嗯|哦|ok|okay|got it|roger|noted|thanks|thank you|copy that|understood|sounds good)[\s.!?,。!?]*$/i;
|
|
278
|
+
const ACTIVE_VALUE_SIGNAL_PATTERN = /(决定|结论|方案|权衡|风险|约束|需求|修复|报错|异常|阻塞|排查|回滚|上线|部署|进度|里程碑|行动项|待办|负责人|owner|next step|todo|deadline|eta|issue|bug|fix|decision|trade-?off|constraint|requirement|rollback|deploy|incident|metric|latency|error rate|cost|url|link|路径|文件|配置|参数|版本|commit|pr|ticket)/i;
|
|
279
|
+
const ACTIVE_VALUE_EVIDENCE_PATTERN = /(https?:\/\/|www\.|[`#/:\\]|[A-Za-z]:\\|\/[A-Za-z0-9._\-\/]+|\b\d+(?:\.\d+)?%?\b|#\d{1,8})/;
|
|
280
|
+
function denoiseTranscriptForWrite(transcript) {
|
|
281
|
+
const raw = (transcript || "").trim();
|
|
282
|
+
if (!raw)
|
|
283
|
+
return "";
|
|
284
|
+
const output = [];
|
|
285
|
+
const seen = new Set();
|
|
286
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
287
|
+
const trimmed = line.trim();
|
|
288
|
+
if (!trimmed)
|
|
289
|
+
continue;
|
|
290
|
+
const content = trimmed.replace(/^\[[^\]]+\]\s*/, "").trim();
|
|
291
|
+
if (!content)
|
|
292
|
+
continue;
|
|
293
|
+
const hasSignal = /(https?:\/\/|www\.|[A-Za-z0-9._-]+\.[A-Za-z]{2,}|[`#/:\\]|@\w+|\b\d{2,}\b)/.test(content);
|
|
294
|
+
if (!hasSignal && LOW_INFORMATION_LINE.test(content)) {
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
const dedupKey = content.toLowerCase();
|
|
298
|
+
if (!hasSignal && seen.has(dedupKey)) {
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
seen.add(dedupKey);
|
|
302
|
+
output.push(trimmed);
|
|
303
|
+
}
|
|
304
|
+
return output.length > 0 ? output.join("\n") : raw;
|
|
305
|
+
}
|
|
306
|
+
function hasValuableActiveContent(text) {
|
|
307
|
+
const source = (text || "").trim();
|
|
308
|
+
if (!source) {
|
|
309
|
+
return false;
|
|
310
|
+
}
|
|
311
|
+
const normalized = source
|
|
312
|
+
.replace(/^\[[^\]]+\]\s*/, "")
|
|
313
|
+
.replace(/\s+/g, " ")
|
|
314
|
+
.trim();
|
|
315
|
+
if (!normalized) {
|
|
316
|
+
return false;
|
|
317
|
+
}
|
|
318
|
+
const hasSignal = ACTIVE_VALUE_SIGNAL_PATTERN.test(normalized);
|
|
319
|
+
const hasEvidence = ACTIVE_VALUE_EVIDENCE_PATTERN.test(normalized);
|
|
320
|
+
if (LOW_VALUE_ONLY_LINE.test(normalized) && !hasSignal && !hasEvidence) {
|
|
321
|
+
return false;
|
|
322
|
+
}
|
|
323
|
+
if (!hasSignal && !hasEvidence) {
|
|
324
|
+
return false;
|
|
325
|
+
}
|
|
326
|
+
if (normalized.length < 20 && !hasSignal) {
|
|
327
|
+
return false;
|
|
328
|
+
}
|
|
329
|
+
return true;
|
|
330
|
+
}
|
|
260
331
|
function buildEventSnippet(text) {
|
|
261
332
|
const lines = text
|
|
262
333
|
.split(/\r?\n/)
|
|
@@ -268,12 +339,434 @@ function buildEventSnippet(text) {
|
|
|
268
339
|
const use = picked.length > 0 ? picked : lines.slice(-20);
|
|
269
340
|
return use.slice(-30).join("\n").slice(-8000);
|
|
270
341
|
}
|
|
271
|
-
const
|
|
342
|
+
const TASK_INSTRUCTION_PATTERNS = [
|
|
343
|
+
/请|帮我|麻烦|需要|任务|需求|实现|修复|排查|优化|上线|部署|整理|编写|启用|查看/i,
|
|
344
|
+
/please|can you|need to|task|implement|fix|investigate|optimi[sz]e|deploy|enable|review/i,
|
|
345
|
+
];
|
|
346
|
+
const COMPLETION_REPORT_PATTERNS = [
|
|
347
|
+
/已完成|完成了|处理完|搞定|已修复|修复了|已实现|已上线|已部署|结果|汇报|完成情况|报告/i,
|
|
348
|
+
/done|completed|fixed|implemented|deployed|resolved|report|summary|finished/i,
|
|
349
|
+
];
|
|
350
|
+
const USER_ACCEPTANCE_PATTERNS = [
|
|
351
|
+
/确认|认可|通过|验收|OK|可以|好的|收到|辛苦|谢谢|没问题|就这样/i,
|
|
352
|
+
/approved|accepted|looks good|great|works|thank you|confirmed/i,
|
|
353
|
+
];
|
|
354
|
+
const FAILURE_PATTERNS = [
|
|
355
|
+
/失败|报错|错误|异常|阻塞|卡住|不行|超时|回滚|故障/i,
|
|
356
|
+
/failed|error|exception|blocked|timeout|rollback|incident/i,
|
|
357
|
+
];
|
|
358
|
+
const SUCCESS_PATTERNS = [
|
|
359
|
+
/成功|完成|修复|解决|通过|已上线|稳定|正常|恢复/i,
|
|
360
|
+
/success|completed|fixed|resolved|passed|stable|recovered|works/i,
|
|
361
|
+
];
|
|
362
|
+
const USER_ROLE_HINT = /^(user|human|customer|client|用户|客户|需求方|老板)/i;
|
|
363
|
+
const AGENT_ROLE_HINT = /^(assistant|agent|ai|system|openclaw|claude|gpt|助手)/i;
|
|
364
|
+
function matchesAnyPattern(text, patterns) {
|
|
365
|
+
return patterns.some(pattern => pattern.test(text));
|
|
366
|
+
}
|
|
367
|
+
function parseTranscriptLines(transcript) {
|
|
368
|
+
const lines = transcript
|
|
369
|
+
.split(/\r?\n/)
|
|
370
|
+
.map(line => line.trim())
|
|
371
|
+
.filter(Boolean);
|
|
372
|
+
return lines.map(line => {
|
|
373
|
+
const matched = line.match(/^\[([^\]]+)\]\s*(.*)$/);
|
|
374
|
+
if (!matched) {
|
|
375
|
+
return { role: "unknown", text: line };
|
|
376
|
+
}
|
|
377
|
+
return {
|
|
378
|
+
role: matched[1].trim().toLowerCase(),
|
|
379
|
+
text: (matched[2] || "").trim(),
|
|
380
|
+
};
|
|
381
|
+
}).filter(item => item.text.length > 0);
|
|
382
|
+
}
|
|
383
|
+
function summarizeForArchive(text, maxChars) {
|
|
384
|
+
const normalized = text.replace(/\s+/g, " ").trim();
|
|
385
|
+
if (normalized.length <= maxChars) {
|
|
386
|
+
return normalized;
|
|
387
|
+
}
|
|
388
|
+
return `${normalized.slice(0, Math.max(40, maxChars - 1)).trim()}…`;
|
|
389
|
+
}
|
|
390
|
+
function evaluateTaskLifecycle(transcript) {
|
|
391
|
+
const parsed = parseTranscriptLines(transcript);
|
|
392
|
+
let taskText = "";
|
|
393
|
+
let reportText = "";
|
|
394
|
+
let acceptanceText = "";
|
|
395
|
+
let firstFailureIndex = -1;
|
|
396
|
+
let firstSuccessIndex = -1;
|
|
397
|
+
let hasTaskInstruction = false;
|
|
398
|
+
let hasCompletionReport = false;
|
|
399
|
+
let hasUserAcceptance = false;
|
|
400
|
+
let hasFailure = false;
|
|
401
|
+
let hasSuccess = false;
|
|
402
|
+
for (let i = 0; i < parsed.length; i += 1) {
|
|
403
|
+
const line = parsed[i];
|
|
404
|
+
const role = line.role;
|
|
405
|
+
const text = line.text;
|
|
406
|
+
const userLike = USER_ROLE_HINT.test(role) || role === "unknown";
|
|
407
|
+
const agentLike = AGENT_ROLE_HINT.test(role) || role === "unknown";
|
|
408
|
+
if (!hasTaskInstruction && userLike && matchesAnyPattern(text, TASK_INSTRUCTION_PATTERNS)) {
|
|
409
|
+
hasTaskInstruction = true;
|
|
410
|
+
taskText = text;
|
|
411
|
+
}
|
|
412
|
+
if (!hasCompletionReport && agentLike && matchesAnyPattern(text, COMPLETION_REPORT_PATTERNS)) {
|
|
413
|
+
hasCompletionReport = true;
|
|
414
|
+
reportText = text;
|
|
415
|
+
}
|
|
416
|
+
if (!hasUserAcceptance && userLike && matchesAnyPattern(text, USER_ACCEPTANCE_PATTERNS)) {
|
|
417
|
+
hasUserAcceptance = true;
|
|
418
|
+
acceptanceText = text;
|
|
419
|
+
}
|
|
420
|
+
if (matchesAnyPattern(text, FAILURE_PATTERNS)) {
|
|
421
|
+
hasFailure = true;
|
|
422
|
+
if (firstFailureIndex < 0) {
|
|
423
|
+
firstFailureIndex = i;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
if (matchesAnyPattern(text, SUCCESS_PATTERNS)) {
|
|
427
|
+
hasSuccess = true;
|
|
428
|
+
if (firstSuccessIndex < 0) {
|
|
429
|
+
firstSuccessIndex = i;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
if (!hasTaskInstruction) {
|
|
434
|
+
const fallback = parsed.find(item => matchesAnyPattern(item.text, TASK_INSTRUCTION_PATTERNS));
|
|
435
|
+
if (fallback) {
|
|
436
|
+
hasTaskInstruction = true;
|
|
437
|
+
taskText = fallback.text;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
if (!hasCompletionReport) {
|
|
441
|
+
const fallback = parsed.find(item => matchesAnyPattern(item.text, COMPLETION_REPORT_PATTERNS));
|
|
442
|
+
if (fallback) {
|
|
443
|
+
hasCompletionReport = true;
|
|
444
|
+
reportText = fallback.text;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
if (!hasUserAcceptance) {
|
|
448
|
+
const fallback = parsed.find(item => matchesAnyPattern(item.text, USER_ACCEPTANCE_PATTERNS));
|
|
449
|
+
if (fallback) {
|
|
450
|
+
hasUserAcceptance = true;
|
|
451
|
+
acceptanceText = fallback.text;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
const failThenSuccess = hasFailure && hasSuccess && firstFailureIndex >= 0 && firstSuccessIndex > firstFailureIndex;
|
|
455
|
+
return {
|
|
456
|
+
hasTaskInstruction,
|
|
457
|
+
hasCompletionReport,
|
|
458
|
+
hasUserAcceptance,
|
|
459
|
+
hasFailure,
|
|
460
|
+
hasSuccess,
|
|
461
|
+
failThenSuccess,
|
|
462
|
+
lifecycleComplete: hasTaskInstruction && hasCompletionReport && hasUserAcceptance,
|
|
463
|
+
taskText,
|
|
464
|
+
reportText,
|
|
465
|
+
acceptanceText,
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
function appendLifecycleArchiveDecision(decisions, transcript, logger) {
|
|
469
|
+
if (decisions.some(item => item.target_layer === "archive_event")) {
|
|
470
|
+
return decisions;
|
|
471
|
+
}
|
|
472
|
+
const lifecycle = evaluateTaskLifecycle(transcript);
|
|
473
|
+
if (!lifecycle.lifecycleComplete) {
|
|
474
|
+
return decisions;
|
|
475
|
+
}
|
|
476
|
+
const fallbackGraph = buildStablePersonalFactGraph(transcript);
|
|
477
|
+
if (!fallbackGraph) {
|
|
478
|
+
return decisions;
|
|
479
|
+
}
|
|
480
|
+
const summaryParts = [
|
|
481
|
+
"Task lifecycle archived: user issued a concrete task, agent reported completion, and user accepted the result.",
|
|
482
|
+
lifecycle.taskText ? `Task: ${summarizeForArchive(lifecycle.taskText, 180)}` : "",
|
|
483
|
+
lifecycle.reportText ? `Completion: ${summarizeForArchive(lifecycle.reportText, 220)}` : "",
|
|
484
|
+
lifecycle.acceptanceText ? `Acceptance: ${summarizeForArchive(lifecycle.acceptanceText, 160)}` : "",
|
|
485
|
+
lifecycle.failThenSuccess
|
|
486
|
+
? "The execution included setbacks/errors followed by successful completion, so the reusable experience value is high."
|
|
487
|
+
: "",
|
|
488
|
+
].filter(Boolean);
|
|
489
|
+
const summary = summaryParts.join(" ");
|
|
490
|
+
const cause = lifecycle.taskText
|
|
491
|
+
? summarizeForArchive(lifecycle.taskText, 220)
|
|
492
|
+
: "User issued a concrete task request.";
|
|
493
|
+
const process = lifecycle.reportText
|
|
494
|
+
? summarizeForArchive(lifecycle.reportText, 320)
|
|
495
|
+
: "Agent executed the requested work and provided a completion report.";
|
|
496
|
+
const result = lifecycle.acceptanceText
|
|
497
|
+
? summarizeForArchive(lifecycle.acceptanceText, 220)
|
|
498
|
+
: "User acknowledged and accepted the delivery.";
|
|
499
|
+
const confidence = lifecycle.failThenSuccess ? 0.88 : 0.76;
|
|
500
|
+
const fallbackDecision = {
|
|
501
|
+
candidate_id: `lifecycle_${crypto.createHash("sha1").update(summary).digest("hex").slice(0, 12)}`,
|
|
502
|
+
target_layer: "archive_event",
|
|
503
|
+
event: {
|
|
504
|
+
event_type: lifecycle.failThenSuccess ? "retrospective" : "milestone",
|
|
505
|
+
summary,
|
|
506
|
+
cause,
|
|
507
|
+
process,
|
|
508
|
+
result,
|
|
509
|
+
entities: fallbackGraph.entities,
|
|
510
|
+
relations: fallbackGraph.relations,
|
|
511
|
+
entity_types: fallbackGraph.entity_types,
|
|
512
|
+
outcome: lifecycle.acceptanceText
|
|
513
|
+
? summarizeForArchive(lifecycle.acceptanceText, 180)
|
|
514
|
+
: "User acknowledged and accepted the delivery.",
|
|
515
|
+
confidence: typeof fallbackGraph.confidence === "number" ? fallbackGraph.confidence : confidence,
|
|
516
|
+
},
|
|
517
|
+
reason: "lifecycle_archive_fallback",
|
|
518
|
+
};
|
|
519
|
+
logger.info("sync_archive_fallback_applied reason=task_lifecycle_complete");
|
|
520
|
+
return [...decisions, fallbackDecision];
|
|
521
|
+
}
|
|
522
|
+
const GRAPH_REWRITE_SCOPE_FIELDS = [
|
|
523
|
+
"summary",
|
|
524
|
+
"source_text_nav",
|
|
525
|
+
"entities",
|
|
526
|
+
"entity_types",
|
|
527
|
+
"relations",
|
|
528
|
+
"confidence",
|
|
529
|
+
];
|
|
530
|
+
const GRAPH_REWRITE_SCOPE_SET = new Set(GRAPH_REWRITE_SCOPE_FIELDS);
|
|
531
|
+
const WRITE_GATE_PROMPT_VERSION = "write-gate.v1.7.9";
|
|
532
|
+
const WRITE_GATE_STAGE_AB_PROMPT_VERSION = "write-gate.ab.v1.1.5";
|
|
533
|
+
const WRITE_GATE_STAGE_C_PROMPT_VERSION = "write-gate.c.v1.4.1";
|
|
534
|
+
const WRITE_GATE_STAGE_D_PROMPT_VERSION = "write-gate.d.v1.1.0";
|
|
535
|
+
const WRITE_GATE_GRAPH_REWRITE_PROMPT_VERSION = "write-gate.graph-rewrite.v1.1.0";
|
|
272
536
|
const WRITE_GATE_REGRESSION_SAMPLES = [
|
|
273
|
-
"
|
|
274
|
-
"
|
|
275
|
-
"
|
|
537
|
+
"样例A: “今天讨论了三种方案,尚未决策” => active_only",
|
|
538
|
+
"样例B: “决定采用B方案并完成上线,错误率下降到0.2%” => archive_event",
|
|
539
|
+
"样例C: “好的收到谢谢” => skip",
|
|
276
540
|
];
|
|
541
|
+
function buildActiveValuePromptHint(schema) {
|
|
542
|
+
const pick = (source, wanted) => {
|
|
543
|
+
const available = new Set(source.map(item => item.toLowerCase()));
|
|
544
|
+
return wanted.filter(item => available.has(item.toLowerCase()));
|
|
545
|
+
};
|
|
546
|
+
const eventSignals = pick(schema.eventTypes || [], [
|
|
547
|
+
"decision",
|
|
548
|
+
"issue",
|
|
549
|
+
"fix",
|
|
550
|
+
"constraint",
|
|
551
|
+
"requirement",
|
|
552
|
+
"blocker",
|
|
553
|
+
"dependency",
|
|
554
|
+
"action_item",
|
|
555
|
+
"follow_up",
|
|
556
|
+
"milestone",
|
|
557
|
+
"risk",
|
|
558
|
+
]);
|
|
559
|
+
const entitySignals = pick(schema.entityTypes || [], [
|
|
560
|
+
"Project",
|
|
561
|
+
"Task",
|
|
562
|
+
"Document",
|
|
563
|
+
"ConfigFile",
|
|
564
|
+
"Date",
|
|
565
|
+
"Person",
|
|
566
|
+
"Team",
|
|
567
|
+
"Resource",
|
|
568
|
+
]);
|
|
569
|
+
const aliasCanonicals = [
|
|
570
|
+
"Person",
|
|
571
|
+
"Resource",
|
|
572
|
+
"Document",
|
|
573
|
+
"ConfigFile",
|
|
574
|
+
"Project",
|
|
575
|
+
"Task",
|
|
576
|
+
"Team",
|
|
577
|
+
"Date",
|
|
578
|
+
"Issue",
|
|
579
|
+
"Fix",
|
|
580
|
+
"Milestone",
|
|
581
|
+
];
|
|
582
|
+
const aliasHints = aliasCanonicals
|
|
583
|
+
.map(canonical => {
|
|
584
|
+
const aliases = Array.isArray(schema.entityAliases?.[canonical])
|
|
585
|
+
? schema.entityAliases[canonical].slice(0, 4)
|
|
586
|
+
: [];
|
|
587
|
+
if (aliases.length === 0) {
|
|
588
|
+
return "";
|
|
589
|
+
}
|
|
590
|
+
return `${canonical}(${aliases.join("/")})`;
|
|
591
|
+
})
|
|
592
|
+
.filter(Boolean);
|
|
593
|
+
const eventAliases = pick(Object.keys(schema.eventTypeAliases || {}), [
|
|
594
|
+
"next_step",
|
|
595
|
+
"next_action",
|
|
596
|
+
"deadline",
|
|
597
|
+
"roadblock",
|
|
598
|
+
"bug",
|
|
599
|
+
"error",
|
|
600
|
+
"problem",
|
|
601
|
+
"workaround",
|
|
602
|
+
]);
|
|
603
|
+
return [
|
|
604
|
+
"Valuable active_only signals must be dictionary-grounded.",
|
|
605
|
+
`Use schema event types such as: ${eventSignals.join(", ")}.`,
|
|
606
|
+
`Use schema entity types such as: ${entitySignals.join(", ")}.`,
|
|
607
|
+
aliasHints.length > 0
|
|
608
|
+
? `Use schema entity aliases such as: ${aliasHints.join(", ")}.`
|
|
609
|
+
: "",
|
|
610
|
+
eventAliases.length > 0
|
|
611
|
+
? `Use schema event aliases such as: ${eventAliases.join(", ")}.`
|
|
612
|
+
: "",
|
|
613
|
+
].join(" ");
|
|
614
|
+
}
|
|
615
|
+
function buildEntityDictionaryPromptHint(schema) {
|
|
616
|
+
const aliases = Object.fromEntries(Object.entries(schema.entityAliases || {}).map(([canonical, values]) => [
|
|
617
|
+
canonical,
|
|
618
|
+
Array.isArray(values) ? values.slice(0, 8) : [],
|
|
619
|
+
]));
|
|
620
|
+
return `Entity dictionary (authoritative for concrete entities): ${JSON.stringify({
|
|
621
|
+
entity_types: schema.entityTypes || [],
|
|
622
|
+
entity_aliases: aliases,
|
|
623
|
+
})}`;
|
|
624
|
+
}
|
|
625
|
+
function buildStablePersonalFactGraph(text) {
|
|
626
|
+
const source = (text || "").trim();
|
|
627
|
+
if (!source)
|
|
628
|
+
return null;
|
|
629
|
+
const genericTokens = new Set([
|
|
630
|
+
"user",
|
|
631
|
+
"person",
|
|
632
|
+
"people",
|
|
633
|
+
"system",
|
|
634
|
+
"assistant",
|
|
635
|
+
"agent",
|
|
636
|
+
"用户",
|
|
637
|
+
"系统",
|
|
638
|
+
"助手",
|
|
639
|
+
"实体",
|
|
640
|
+
"问题",
|
|
641
|
+
"方案",
|
|
642
|
+
]);
|
|
643
|
+
const normalizeToken = (value) => value.trim().toLowerCase();
|
|
644
|
+
const isConcreteName = (value) => {
|
|
645
|
+
const name = value.trim();
|
|
646
|
+
if (!name || name.length < 2)
|
|
647
|
+
return false;
|
|
648
|
+
if (genericTokens.has(normalizeToken(name)))
|
|
649
|
+
return false;
|
|
650
|
+
if (/^(wife|husband|spouse|child|kid|children)$/i.test(name))
|
|
651
|
+
return false;
|
|
652
|
+
if (/^(妻子|丈夫|配偶|孩子|儿子|女儿)$/.test(name))
|
|
653
|
+
return false;
|
|
654
|
+
return true;
|
|
655
|
+
};
|
|
656
|
+
const extractConcreteName = (candidate) => {
|
|
657
|
+
const cleaned = (candidate || "").trim().replace(/[.,;:!?,。;:!?]+$/g, "");
|
|
658
|
+
return isConcreteName(cleaned) ? cleaned : "";
|
|
659
|
+
};
|
|
660
|
+
const findSubjectName = () => {
|
|
661
|
+
const patterns = [
|
|
662
|
+
/([A-Za-z][A-Za-z0-9._-]{1,40})\s*'s\s*(wife|husband|spouse|child|kid|daughter|son)\b/i,
|
|
663
|
+
/([\u4e00-\u9fff]{2,12})\s*的\s*(妻子|老婆|配偶|爱人|丈夫|老公|孩子|儿子|女儿)/,
|
|
664
|
+
/\b([A-Z][a-zA-Z0-9._-]{1,40})\b/,
|
|
665
|
+
];
|
|
666
|
+
for (const pattern of patterns) {
|
|
667
|
+
const hit = source.match(pattern);
|
|
668
|
+
const candidate = hit ? extractConcreteName(hit[1] || "") : "";
|
|
669
|
+
if (candidate) {
|
|
670
|
+
return candidate;
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
return "";
|
|
674
|
+
};
|
|
675
|
+
const subjectName = findSubjectName();
|
|
676
|
+
if (!subjectName) {
|
|
677
|
+
return null;
|
|
678
|
+
}
|
|
679
|
+
const entities = new Set([subjectName]);
|
|
680
|
+
const entity_types = { [subjectName]: "Person" };
|
|
681
|
+
const relations = [];
|
|
682
|
+
const relationKeys = new Set();
|
|
683
|
+
const addRelation = (relation) => {
|
|
684
|
+
const relationKey = `${relation.source}|${relation.type}|${relation.target}`;
|
|
685
|
+
if (relationKeys.has(relationKey)) {
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
relationKeys.add(relationKey);
|
|
689
|
+
relations.push(relation);
|
|
690
|
+
};
|
|
691
|
+
const spouseNameHit = source.match(/(妻子|老婆|配偶|爱人|丈夫|老公|wife|husband|spouse)(?:\s*(?:是|叫|named|:|-)\s*)?([A-Za-z][A-Za-z0-9._-]{1,40}|[\u4e00-\u9fff]{2,12})/i);
|
|
692
|
+
const spouseName = spouseNameHit ? extractConcreteName(spouseNameHit[2] || "") : "";
|
|
693
|
+
if (spouseName) {
|
|
694
|
+
entities.add(spouseName);
|
|
695
|
+
entity_types[spouseName] = "FamilyMember";
|
|
696
|
+
addRelation({
|
|
697
|
+
source: subjectName,
|
|
698
|
+
target: spouseName,
|
|
699
|
+
type: "has_spouse",
|
|
700
|
+
evidence_span: spouseName,
|
|
701
|
+
context_chunk: source.slice(0, 160).trim(),
|
|
702
|
+
confidence: 0.9,
|
|
703
|
+
});
|
|
704
|
+
}
|
|
705
|
+
const childNameHit = source.match(/(儿子|女儿|孩子|child|kid|daughter|son)(?:\s*(?:是|叫|named|:|-)\s*)?([A-Za-z][A-Za-z0-9._-]{1,40}|[\u4e00-\u9fff]{2,12})/i);
|
|
706
|
+
const childName = childNameHit ? extractConcreteName(childNameHit[2] || "") : "";
|
|
707
|
+
if (childName) {
|
|
708
|
+
entities.add(childName);
|
|
709
|
+
entity_types[childName] = "FamilyMember";
|
|
710
|
+
addRelation({
|
|
711
|
+
source: subjectName,
|
|
712
|
+
target: childName,
|
|
713
|
+
type: "has_child",
|
|
714
|
+
evidence_span: childName,
|
|
715
|
+
context_chunk: source.slice(0, 160).trim(),
|
|
716
|
+
confidence: 0.88,
|
|
717
|
+
});
|
|
718
|
+
}
|
|
719
|
+
const birthdayMatch = source.match(/(birthday|生日)[^,。;\n]*?(\d{4}-\d{2}-\d{2}|\d{1,2}月\d{1,2}日|\d{1,2}[/-]\d{1,2})/i);
|
|
720
|
+
if (birthdayMatch && spouseName) {
|
|
721
|
+
const dateEntity = birthdayMatch[2];
|
|
722
|
+
entities.add(dateEntity);
|
|
723
|
+
entity_types[dateEntity] = "Date";
|
|
724
|
+
addRelation({
|
|
725
|
+
source: spouseName,
|
|
726
|
+
target: dateEntity,
|
|
727
|
+
type: "birthday_on",
|
|
728
|
+
evidence_span: birthdayMatch[2],
|
|
729
|
+
context_chunk: source.slice(0, 160).trim(),
|
|
730
|
+
confidence: 0.92,
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
if (birthdayMatch && childName) {
|
|
734
|
+
const dateEntity = birthdayMatch[2];
|
|
735
|
+
entities.add(dateEntity);
|
|
736
|
+
entity_types[dateEntity] = "Date";
|
|
737
|
+
addRelation({
|
|
738
|
+
source: childName,
|
|
739
|
+
target: dateEntity,
|
|
740
|
+
type: "birthday_on",
|
|
741
|
+
evidence_span: birthdayMatch[2],
|
|
742
|
+
context_chunk: source.slice(0, 160).trim(),
|
|
743
|
+
confidence: 0.9,
|
|
744
|
+
});
|
|
745
|
+
}
|
|
746
|
+
const anniversaryMatch = source.match(/(纪念日|结婚纪念日|anniversary)[^,。;\n]*?(\d{4}-\d{2}-\d{2}|\d{1,2}月\d{1,2}日|\d{1,2}[/-]\d{1,2})/i);
|
|
747
|
+
if (anniversaryMatch) {
|
|
748
|
+
const dateEntity = anniversaryMatch[2];
|
|
749
|
+
entities.add(dateEntity);
|
|
750
|
+
entity_types[dateEntity] = "Date";
|
|
751
|
+
addRelation({
|
|
752
|
+
source: subjectName,
|
|
753
|
+
target: dateEntity,
|
|
754
|
+
type: "anniversary_on",
|
|
755
|
+
evidence_span: anniversaryMatch[2],
|
|
756
|
+
context_chunk: source.slice(0, 160).trim(),
|
|
757
|
+
confidence: 0.9,
|
|
758
|
+
});
|
|
759
|
+
}
|
|
760
|
+
if (relations.length === 0) {
|
|
761
|
+
return null;
|
|
762
|
+
}
|
|
763
|
+
return {
|
|
764
|
+
entities: [...entities],
|
|
765
|
+
entity_types,
|
|
766
|
+
relations,
|
|
767
|
+
confidence: 0.9,
|
|
768
|
+
};
|
|
769
|
+
}
|
|
277
770
|
function parseArchiveEventPayload(value) {
|
|
278
771
|
if (!value || typeof value !== "object") {
|
|
279
772
|
return null;
|
|
@@ -281,7 +774,10 @@ function parseArchiveEventPayload(value) {
|
|
|
281
774
|
const obj = value;
|
|
282
775
|
const eventType = typeof obj.event_type === "string" ? obj.event_type.trim() : "";
|
|
283
776
|
const summary = typeof obj.summary === "string" ? obj.summary.trim() : "";
|
|
284
|
-
|
|
777
|
+
const cause = typeof obj.cause === "string" ? obj.cause.trim() : "";
|
|
778
|
+
const process = typeof obj.process === "string" ? obj.process.trim() : "";
|
|
779
|
+
const result = typeof obj.result === "string" ? obj.result.trim() : "";
|
|
780
|
+
if (!eventType || !summary || !cause || !process || !result) {
|
|
285
781
|
return null;
|
|
286
782
|
}
|
|
287
783
|
const entities = Array.isArray(obj.entities)
|
|
@@ -295,14 +791,26 @@ function parseArchiveEventPayload(value) {
|
|
|
295
791
|
const relation = valueItem;
|
|
296
792
|
const source = typeof relation.source === "string" ? relation.source.trim() : "";
|
|
297
793
|
const target = typeof relation.target === "string" ? relation.target.trim() : "";
|
|
298
|
-
const type = typeof relation.type === "string" ? relation.type.trim() : "
|
|
794
|
+
const type = typeof relation.type === "string" ? relation.type.trim() : "";
|
|
795
|
+
const relationOrigin = typeof relation.relation_origin === "string" ? relation.relation_origin.trim() : "";
|
|
796
|
+
const relationDefinition = typeof relation.relation_definition === "string" ? relation.relation_definition.trim() : "";
|
|
797
|
+
const mappingHint = typeof relation.mapping_hint === "string" ? relation.mapping_hint.trim() : "";
|
|
299
798
|
const evidenceSpan = typeof relation.evidence_span === "string" ? relation.evidence_span.trim() : "";
|
|
300
799
|
const confidence = typeof relation.confidence === "number"
|
|
301
800
|
? Math.max(0, Math.min(1, relation.confidence))
|
|
302
801
|
: undefined;
|
|
303
|
-
if (!source || !target)
|
|
802
|
+
if (!source || !target || !type)
|
|
304
803
|
return null;
|
|
305
|
-
return {
|
|
804
|
+
return {
|
|
805
|
+
source,
|
|
806
|
+
target,
|
|
807
|
+
type,
|
|
808
|
+
relation_origin: relationOrigin || undefined,
|
|
809
|
+
relation_definition: relationDefinition || undefined,
|
|
810
|
+
mapping_hint: mappingHint || undefined,
|
|
811
|
+
evidence_span: evidenceSpan || undefined,
|
|
812
|
+
confidence,
|
|
813
|
+
};
|
|
306
814
|
})
|
|
307
815
|
.filter(Boolean)
|
|
308
816
|
: [];
|
|
@@ -314,6 +822,9 @@ function parseArchiveEventPayload(value) {
|
|
|
314
822
|
return {
|
|
315
823
|
event_type: eventType,
|
|
316
824
|
summary,
|
|
825
|
+
cause,
|
|
826
|
+
process,
|
|
827
|
+
result,
|
|
317
828
|
entities,
|
|
318
829
|
entity_types,
|
|
319
830
|
relations,
|
|
@@ -321,11 +832,24 @@ function parseArchiveEventPayload(value) {
|
|
|
321
832
|
confidence: typeof obj.confidence === "number" ? Math.max(0, Math.min(1, obj.confidence)) : 0.6,
|
|
322
833
|
};
|
|
323
834
|
}
|
|
324
|
-
function parseGraphPayload(value) {
|
|
835
|
+
function parseGraphPayload(value, options) {
|
|
325
836
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
326
837
|
return null;
|
|
327
838
|
}
|
|
839
|
+
const allowIncomplete = options?.allowIncomplete === true;
|
|
328
840
|
const obj = value;
|
|
841
|
+
const summary = typeof obj.summary === "string" ? obj.summary.trim() : "";
|
|
842
|
+
const sourceTextNavObj = asRecord(obj.source_text_nav);
|
|
843
|
+
const source_text_nav = sourceTextNavObj
|
|
844
|
+
? {
|
|
845
|
+
layer: typeof sourceTextNavObj.layer === "string" ? sourceTextNavObj.layer.trim() : undefined,
|
|
846
|
+
session_id: typeof sourceTextNavObj.session_id === "string" ? sourceTextNavObj.session_id.trim() : undefined,
|
|
847
|
+
source_file: typeof sourceTextNavObj.source_file === "string" ? sourceTextNavObj.source_file.trim() : undefined,
|
|
848
|
+
source_memory_id: typeof sourceTextNavObj.source_memory_id === "string" ? sourceTextNavObj.source_memory_id.trim() : undefined,
|
|
849
|
+
source_event_id: typeof sourceTextNavObj.source_event_id === "string" ? sourceTextNavObj.source_event_id.trim() : undefined,
|
|
850
|
+
fulltext_anchor: typeof sourceTextNavObj.fulltext_anchor === "string" ? sourceTextNavObj.fulltext_anchor.trim() : undefined,
|
|
851
|
+
}
|
|
852
|
+
: undefined;
|
|
329
853
|
const entities = Array.isArray(obj.entities)
|
|
330
854
|
? obj.entities.map(v => (typeof v === "string" ? v.trim() : "")).filter(Boolean)
|
|
331
855
|
: [];
|
|
@@ -342,55 +866,595 @@ function parseGraphPayload(value) {
|
|
|
342
866
|
const rel = item;
|
|
343
867
|
const source = typeof rel.source === "string" ? rel.source.trim() : "";
|
|
344
868
|
const target = typeof rel.target === "string" ? rel.target.trim() : "";
|
|
345
|
-
const type = typeof rel.type === "string" && rel.type.trim() ? rel.type.trim() : "
|
|
346
|
-
if (!source || !target)
|
|
869
|
+
const type = typeof rel.type === "string" && rel.type.trim() ? rel.type.trim() : "";
|
|
870
|
+
if (!source || !target || !type)
|
|
347
871
|
return null;
|
|
348
872
|
const evidenceSpan = typeof rel.evidence_span === "string" ? rel.evidence_span.trim() : "";
|
|
349
873
|
const confidence = typeof rel.confidence === "number"
|
|
350
874
|
? Math.max(0, Math.min(1, rel.confidence))
|
|
351
875
|
: undefined;
|
|
876
|
+
const relationOrigin = typeof rel.relation_origin === "string" ? rel.relation_origin.trim() : "";
|
|
877
|
+
const relationDefinition = typeof rel.relation_definition === "string" ? rel.relation_definition.trim() : "";
|
|
878
|
+
const mappingHint = typeof rel.mapping_hint === "string" ? rel.mapping_hint.trim() : "";
|
|
879
|
+
const contextChunk = typeof rel.context_chunk === "string" ? rel.context_chunk.trim() : "";
|
|
880
|
+
if (!evidenceSpan || typeof confidence !== "number")
|
|
881
|
+
return null;
|
|
352
882
|
return {
|
|
353
883
|
source,
|
|
354
884
|
target,
|
|
355
885
|
type,
|
|
356
|
-
|
|
357
|
-
|
|
886
|
+
relation_origin: relationOrigin || undefined,
|
|
887
|
+
relation_definition: relationDefinition || undefined,
|
|
888
|
+
mapping_hint: mappingHint || undefined,
|
|
889
|
+
evidence_span: evidenceSpan,
|
|
890
|
+
context_chunk: contextChunk || undefined,
|
|
891
|
+
confidence,
|
|
358
892
|
};
|
|
359
893
|
})
|
|
360
894
|
.filter((item) => item !== null)
|
|
361
895
|
: [];
|
|
362
|
-
if (entities.length === 0 || relations.length === 0) {
|
|
896
|
+
if (!allowIncomplete && (entities.length === 0 || relations.length === 0)) {
|
|
897
|
+
return null;
|
|
898
|
+
}
|
|
899
|
+
const hasAnyField = Boolean(summary
|
|
900
|
+
|| source_text_nav
|
|
901
|
+
|| entities.length > 0
|
|
902
|
+
|| relations.length > 0
|
|
903
|
+
|| (entity_types && Object.keys(entity_types).length > 0)
|
|
904
|
+
|| typeof obj.confidence === "number");
|
|
905
|
+
if (!hasAnyField) {
|
|
363
906
|
return null;
|
|
364
907
|
}
|
|
365
908
|
return {
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
909
|
+
summary: summary || undefined,
|
|
910
|
+
source_text_nav,
|
|
911
|
+
entities: entities.length > 0 ? entities : undefined,
|
|
912
|
+
entity_types: entity_types && Object.keys(entity_types).length > 0 ? entity_types : undefined,
|
|
913
|
+
relations: relations.length > 0 ? relations : undefined,
|
|
369
914
|
confidence: typeof obj.confidence === "number" ? Math.max(0, Math.min(1, obj.confidence)) : undefined,
|
|
370
915
|
};
|
|
371
916
|
}
|
|
372
|
-
function
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
917
|
+
function toAppendableGraphPayload(payload) {
|
|
918
|
+
if (!payload)
|
|
919
|
+
return undefined;
|
|
920
|
+
if (!Array.isArray(payload.entities) || payload.entities.length === 0)
|
|
921
|
+
return undefined;
|
|
922
|
+
if (!Array.isArray(payload.relations) || payload.relations.length === 0)
|
|
923
|
+
return undefined;
|
|
924
|
+
return {
|
|
925
|
+
...payload,
|
|
926
|
+
entities: payload.entities,
|
|
927
|
+
relations: payload.relations,
|
|
928
|
+
};
|
|
929
|
+
}
|
|
930
|
+
function mergeGraphPayload(base, patch) {
|
|
931
|
+
if (!base && !patch)
|
|
932
|
+
return undefined;
|
|
933
|
+
if (!base)
|
|
934
|
+
return toAppendableGraphPayload(patch);
|
|
935
|
+
if (!patch)
|
|
936
|
+
return toAppendableGraphPayload(base);
|
|
937
|
+
const merged = {
|
|
938
|
+
...base,
|
|
939
|
+
...patch,
|
|
940
|
+
summary: patch.summary || base.summary,
|
|
941
|
+
source_text_nav: patch.source_text_nav || base.source_text_nav,
|
|
942
|
+
entities: patch.entities && patch.entities.length > 0 ? patch.entities : base.entities,
|
|
943
|
+
entity_types: patch.entity_types && Object.keys(patch.entity_types).length > 0 ? patch.entity_types : base.entity_types,
|
|
944
|
+
relations: patch.relations && patch.relations.length > 0 ? patch.relations : base.relations,
|
|
945
|
+
confidence: typeof patch.confidence === "number" ? patch.confidence : base.confidence,
|
|
946
|
+
};
|
|
947
|
+
return toAppendableGraphPayload(merged);
|
|
948
|
+
}
|
|
949
|
+
function normalizeRewriteScope(scope) {
|
|
950
|
+
if (!Array.isArray(scope))
|
|
951
|
+
return [];
|
|
952
|
+
const output = [];
|
|
953
|
+
const seen = new Set();
|
|
954
|
+
for (const item of scope) {
|
|
955
|
+
const key = typeof item === "string" ? item.trim() : "";
|
|
956
|
+
if (!key || !GRAPH_REWRITE_SCOPE_SET.has(key) || seen.has(key)) {
|
|
957
|
+
continue;
|
|
958
|
+
}
|
|
959
|
+
seen.add(key);
|
|
960
|
+
output.push(key);
|
|
961
|
+
}
|
|
962
|
+
return output;
|
|
963
|
+
}
|
|
964
|
+
function normalizeSourceTextNavForCompare(value) {
|
|
965
|
+
const nav = value || {};
|
|
966
|
+
return JSON.stringify({
|
|
967
|
+
layer: typeof nav.layer === "string" ? nav.layer.trim() : "",
|
|
968
|
+
session_id: typeof nav.session_id === "string" ? nav.session_id.trim() : "",
|
|
969
|
+
source_file: typeof nav.source_file === "string" ? nav.source_file.trim() : "",
|
|
970
|
+
source_memory_id: typeof nav.source_memory_id === "string" ? nav.source_memory_id.trim() : "",
|
|
971
|
+
source_event_id: typeof nav.source_event_id === "string" ? nav.source_event_id.trim() : "",
|
|
972
|
+
fulltext_anchor: typeof nav.fulltext_anchor === "string" ? nav.fulltext_anchor.trim() : "",
|
|
973
|
+
});
|
|
974
|
+
}
|
|
975
|
+
function normalizeEntitiesForCompare(value) {
|
|
976
|
+
const entities = Array.isArray(value)
|
|
977
|
+
? value.map(item => String(item || "").trim()).filter(Boolean)
|
|
978
|
+
: [];
|
|
979
|
+
entities.sort((a, b) => a.localeCompare(b, "en", { sensitivity: "base" }));
|
|
980
|
+
return JSON.stringify(entities);
|
|
981
|
+
}
|
|
982
|
+
function normalizeEntityTypesForCompare(value) {
|
|
983
|
+
const entries = Object.entries(value || {})
|
|
984
|
+
.map(([entity, type]) => [String(entity || "").trim(), String(type || "").trim()])
|
|
985
|
+
.filter(([entity, type]) => entity.length > 0 && type.length > 0)
|
|
986
|
+
.sort((a, b) => {
|
|
987
|
+
const keyA = `${a[0].toLowerCase()}|${a[1].toLowerCase()}`;
|
|
988
|
+
const keyB = `${b[0].toLowerCase()}|${b[1].toLowerCase()}`;
|
|
989
|
+
return keyA.localeCompare(keyB, "en", { sensitivity: "base" });
|
|
990
|
+
});
|
|
991
|
+
return JSON.stringify(entries);
|
|
992
|
+
}
|
|
993
|
+
function normalizeRelationsForCompare(value) {
|
|
994
|
+
const rows = Array.isArray(value)
|
|
995
|
+
? value.map(rel => ({
|
|
996
|
+
source: String(rel.source || "").trim(),
|
|
997
|
+
target: String(rel.target || "").trim(),
|
|
998
|
+
type: String(rel.type || "").trim(),
|
|
999
|
+
relation_origin: typeof rel.relation_origin === "string" ? rel.relation_origin.trim() : "",
|
|
1000
|
+
relation_definition: typeof rel.relation_definition === "string" ? rel.relation_definition.trim() : "",
|
|
1001
|
+
mapping_hint: typeof rel.mapping_hint === "string" ? rel.mapping_hint.trim() : "",
|
|
1002
|
+
evidence_span: typeof rel.evidence_span === "string" ? rel.evidence_span.trim() : "",
|
|
1003
|
+
context_chunk: typeof rel.context_chunk === "string" ? rel.context_chunk.trim() : "",
|
|
1004
|
+
confidence: typeof rel.confidence === "number" ? Number(rel.confidence.toFixed(6)) : null,
|
|
1005
|
+
}))
|
|
1006
|
+
: [];
|
|
1007
|
+
rows.sort((a, b) => JSON.stringify(a).localeCompare(JSON.stringify(b), "en", { sensitivity: "base" }));
|
|
1008
|
+
return JSON.stringify(rows);
|
|
1009
|
+
}
|
|
1010
|
+
function listChangedGraphFields(base, next) {
|
|
1011
|
+
const changed = [];
|
|
1012
|
+
if ((base?.summary || "").trim() !== (next?.summary || "").trim()) {
|
|
1013
|
+
changed.push("summary");
|
|
1014
|
+
}
|
|
1015
|
+
if (normalizeSourceTextNavForCompare(base?.source_text_nav) !== normalizeSourceTextNavForCompare(next?.source_text_nav)) {
|
|
1016
|
+
changed.push("source_text_nav");
|
|
1017
|
+
}
|
|
1018
|
+
if (normalizeEntitiesForCompare(base?.entities) !== normalizeEntitiesForCompare(next?.entities)) {
|
|
1019
|
+
changed.push("entities");
|
|
1020
|
+
}
|
|
1021
|
+
if (normalizeEntityTypesForCompare(base?.entity_types) !== normalizeEntityTypesForCompare(next?.entity_types)) {
|
|
1022
|
+
changed.push("entity_types");
|
|
1023
|
+
}
|
|
1024
|
+
if (normalizeRelationsForCompare(base?.relations) !== normalizeRelationsForCompare(next?.relations)) {
|
|
1025
|
+
changed.push("relations");
|
|
1026
|
+
}
|
|
1027
|
+
const baseConfidence = typeof base?.confidence === "number" ? Number(base.confidence.toFixed(6)) : null;
|
|
1028
|
+
const nextConfidence = typeof next?.confidence === "number" ? Number(next.confidence.toFixed(6)) : null;
|
|
1029
|
+
if (baseConfidence !== nextConfidence) {
|
|
1030
|
+
changed.push("confidence");
|
|
1031
|
+
}
|
|
1032
|
+
return changed;
|
|
1033
|
+
}
|
|
1034
|
+
function validateGraphRewriteCompleteness(payload, schema) {
|
|
1035
|
+
const validation = (0, llm_output_validator_1.validateGraphRewritePayload)(payload, { schema });
|
|
1036
|
+
return validation.valid ? [] : validation.errors;
|
|
1037
|
+
}
|
|
1038
|
+
function validateGraphRewriteResult(args) {
|
|
1039
|
+
const errors = validateGraphRewriteCompleteness(args.rewrittenPayload, args.schema);
|
|
1040
|
+
const changedFields = listChangedGraphFields(args.basePayload, args.rewrittenPayload);
|
|
1041
|
+
const scope = normalizeRewriteScope(args.rewriteScope);
|
|
1042
|
+
if (Array.isArray(args.rewriteScope)) {
|
|
1043
|
+
if (scope.length === 0 && changedFields.length > 0) {
|
|
1044
|
+
errors.push(`rewrite_scope_violation:empty_scope_changed:${changedFields.join(",")}`);
|
|
1045
|
+
}
|
|
1046
|
+
else if (scope.length > 0) {
|
|
1047
|
+
const scopeSet = new Set(scope);
|
|
1048
|
+
const outOfScope = changedFields.filter(field => !scopeSet.has(field));
|
|
1049
|
+
if (outOfScope.length > 0) {
|
|
1050
|
+
errors.push(`rewrite_scope_violation:${outOfScope.join(",")}`);
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
return {
|
|
1055
|
+
valid: errors.length === 0,
|
|
1056
|
+
errors,
|
|
1057
|
+
changedFields,
|
|
1058
|
+
};
|
|
1059
|
+
}
|
|
1060
|
+
function canonicalizeMergeEntityName(leftRaw, rightRaw) {
|
|
1061
|
+
const left = (leftRaw || "").trim();
|
|
1062
|
+
const right = (rightRaw || "").trim();
|
|
1063
|
+
if (!left)
|
|
1064
|
+
return right;
|
|
1065
|
+
if (!right)
|
|
1066
|
+
return left;
|
|
1067
|
+
const leftAscii = /^[\x00-\x7F]+$/.test(left);
|
|
1068
|
+
const rightAscii = /^[\x00-\x7F]+$/.test(right);
|
|
1069
|
+
if (leftAscii && !rightAscii)
|
|
1070
|
+
return right;
|
|
1071
|
+
if (!leftAscii && rightAscii)
|
|
1072
|
+
return left;
|
|
1073
|
+
return left.length >= right.length ? left : right;
|
|
1074
|
+
}
|
|
1075
|
+
function applyMergeHintToGraphPayload(args) {
|
|
1076
|
+
const warnings = [];
|
|
1077
|
+
const graphPayload = toAppendableGraphPayload(args.graphPayload);
|
|
1078
|
+
const mergeHint = args.mergeHint;
|
|
1079
|
+
if (!mergeHint) {
|
|
1080
|
+
return { graphPayload, warnings };
|
|
1081
|
+
}
|
|
1082
|
+
if (!graphPayload) {
|
|
1083
|
+
if (mergeHint.same_event === true) {
|
|
1084
|
+
warnings.push("same_event_merge_failed");
|
|
1085
|
+
}
|
|
1086
|
+
return { graphPayload, warnings };
|
|
1087
|
+
}
|
|
1088
|
+
let normalizedPayload = {
|
|
1089
|
+
...graphPayload,
|
|
1090
|
+
entities: Array.isArray(graphPayload.entities) ? [...graphPayload.entities] : [],
|
|
1091
|
+
entity_types: graphPayload.entity_types ? { ...graphPayload.entity_types } : {},
|
|
1092
|
+
relations: Array.isArray(graphPayload.relations) ? graphPayload.relations.map(item => ({ ...item })) : [],
|
|
1093
|
+
};
|
|
1094
|
+
const pairs = Array.isArray(mergeHint.same_entity_pairs) ? mergeHint.same_entity_pairs : [];
|
|
1095
|
+
if (pairs.length > 0) {
|
|
1096
|
+
const aliasToCanonical = new Map();
|
|
1097
|
+
const knownNames = new Set();
|
|
1098
|
+
for (const entity of normalizedPayload.entities || []) {
|
|
1099
|
+
knownNames.add(entity.trim().toLowerCase());
|
|
1100
|
+
}
|
|
1101
|
+
for (const relation of normalizedPayload.relations || []) {
|
|
1102
|
+
knownNames.add((relation.source || "").trim().toLowerCase());
|
|
1103
|
+
knownNames.add((relation.target || "").trim().toLowerCase());
|
|
1104
|
+
}
|
|
1105
|
+
let unresolvedPairCount = 0;
|
|
1106
|
+
for (const pair of pairs) {
|
|
1107
|
+
const left = (pair?.[0] || "").trim();
|
|
1108
|
+
const right = (pair?.[1] || "").trim();
|
|
1109
|
+
if (!left || !right)
|
|
1110
|
+
continue;
|
|
1111
|
+
const canonical = canonicalizeMergeEntityName(left, right);
|
|
1112
|
+
for (const alias of [left, right]) {
|
|
1113
|
+
aliasToCanonical.set(alias.toLowerCase(), canonical);
|
|
1114
|
+
}
|
|
1115
|
+
const hit = knownNames.has(left.toLowerCase()) || knownNames.has(right.toLowerCase());
|
|
1116
|
+
if (!hit) {
|
|
1117
|
+
unresolvedPairCount += 1;
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
const canonicalize = (value) => {
|
|
1121
|
+
const raw = (value || "").trim();
|
|
1122
|
+
if (!raw)
|
|
1123
|
+
return raw;
|
|
1124
|
+
return aliasToCanonical.get(raw.toLowerCase()) || raw;
|
|
1125
|
+
};
|
|
1126
|
+
const dedupedEntities = [];
|
|
1127
|
+
const entitySeen = new Set();
|
|
1128
|
+
for (const entity of normalizedPayload.entities || []) {
|
|
1129
|
+
const next = canonicalize(entity);
|
|
1130
|
+
const key = next.toLowerCase();
|
|
1131
|
+
if (!next || entitySeen.has(key))
|
|
1132
|
+
continue;
|
|
1133
|
+
entitySeen.add(key);
|
|
1134
|
+
dedupedEntities.push(next);
|
|
1135
|
+
}
|
|
1136
|
+
const mappedEntityTypes = {};
|
|
1137
|
+
for (const [name, type] of Object.entries(normalizedPayload.entity_types || {})) {
|
|
1138
|
+
const canonical = canonicalize(name);
|
|
1139
|
+
if (!canonical || !type)
|
|
1140
|
+
continue;
|
|
1141
|
+
if (!mappedEntityTypes[canonical]) {
|
|
1142
|
+
mappedEntityTypes[canonical] = type;
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
normalizedPayload = {
|
|
1146
|
+
...normalizedPayload,
|
|
1147
|
+
entities: dedupedEntities,
|
|
1148
|
+
entity_types: mappedEntityTypes,
|
|
1149
|
+
relations: (normalizedPayload.relations || []).map(relation => ({
|
|
1150
|
+
...relation,
|
|
1151
|
+
source: canonicalize(relation.source || ""),
|
|
1152
|
+
target: canonicalize(relation.target || ""),
|
|
1153
|
+
})),
|
|
1154
|
+
};
|
|
1155
|
+
if (unresolvedPairCount > 0) {
|
|
1156
|
+
warnings.push("same_entity_resolution_failed");
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
if (mergeHint.same_event === true) {
|
|
1160
|
+
const relationCount = Array.isArray(normalizedPayload.relations) ? normalizedPayload.relations.length : 0;
|
|
1161
|
+
if (relationCount === 0) {
|
|
1162
|
+
warnings.push("same_event_merge_failed");
|
|
377
1163
|
}
|
|
1164
|
+
}
|
|
1165
|
+
return {
|
|
1166
|
+
graphPayload: toAppendableGraphPayload(normalizedPayload),
|
|
1167
|
+
warnings,
|
|
1168
|
+
};
|
|
1169
|
+
}
|
|
1170
|
+
function parseGateTargetLayer(value) {
|
|
1171
|
+
if (typeof value !== "string")
|
|
1172
|
+
return undefined;
|
|
1173
|
+
const layer = value.trim();
|
|
1174
|
+
if (layer === "active_only" || layer === "archive_event" || layer === "skip") {
|
|
1175
|
+
return layer;
|
|
1176
|
+
}
|
|
1177
|
+
return undefined;
|
|
1178
|
+
}
|
|
1179
|
+
function parseMergeHintPayload(value) {
|
|
1180
|
+
const hint = asRecord(value);
|
|
1181
|
+
if (!hint)
|
|
1182
|
+
return undefined;
|
|
1183
|
+
const candidateId = typeof hint.candidate_id === "string" ? hint.candidate_id.trim() : "";
|
|
1184
|
+
const sameEntityPairs = Array.isArray(hint.same_entity_pairs)
|
|
1185
|
+
? hint.same_entity_pairs
|
|
1186
|
+
.map(pair => {
|
|
1187
|
+
if (!Array.isArray(pair) || pair.length < 2)
|
|
1188
|
+
return null;
|
|
1189
|
+
const left = typeof pair[0] === "string" ? pair[0].trim() : "";
|
|
1190
|
+
const right = typeof pair[1] === "string" ? pair[1].trim() : "";
|
|
1191
|
+
if (!left || !right)
|
|
1192
|
+
return null;
|
|
1193
|
+
return [left, right];
|
|
1194
|
+
})
|
|
1195
|
+
.filter((item) => item !== null)
|
|
1196
|
+
: [];
|
|
1197
|
+
return {
|
|
1198
|
+
candidate_id: candidateId || undefined,
|
|
1199
|
+
same_event: hint.same_event === true,
|
|
1200
|
+
same_entity_pairs: sameEntityPairs.length > 0 ? sameEntityPairs : undefined,
|
|
1201
|
+
suggested_action: typeof hint.suggested_action === "string" ? hint.suggested_action.trim() : undefined,
|
|
1202
|
+
reason: typeof hint.reason === "string" ? hint.reason.trim() : undefined,
|
|
1203
|
+
};
|
|
1204
|
+
}
|
|
1205
|
+
function parseGraphRewritePlanPayload(value) {
|
|
1206
|
+
const plan = asRecord(value);
|
|
1207
|
+
if (!plan)
|
|
1208
|
+
return undefined;
|
|
1209
|
+
const rewriteRequired = plan.rewrite_required === true;
|
|
1210
|
+
const candidateId = typeof plan.candidate_id === "string" ? plan.candidate_id.trim() : "";
|
|
1211
|
+
const rewriteScopeValue = Array.isArray(plan.rewrite_scope) ? plan.rewrite_scope : null;
|
|
1212
|
+
const rewriteScopeProvided = Array.isArray(rewriteScopeValue);
|
|
1213
|
+
const rewriteScopeRaw = rewriteScopeProvided
|
|
1214
|
+
? rewriteScopeValue.map((item) => typeof item === "string" ? item.trim() : "").filter(Boolean)
|
|
1215
|
+
: [];
|
|
1216
|
+
const rewriteScope = normalizeRewriteScope(rewriteScopeRaw);
|
|
1217
|
+
const rewritePayloadRaw = asRecord(plan.graph_rewrite_payload) || asRecord(plan.graph_payload);
|
|
1218
|
+
const rewritePayload = parseGraphPayload(rewritePayloadRaw || undefined, { allowIncomplete: true }) || undefined;
|
|
1219
|
+
return {
|
|
1220
|
+
candidate_id: candidateId || undefined,
|
|
1221
|
+
rewrite_required: rewriteRequired,
|
|
1222
|
+
rewrite_reason: typeof plan.rewrite_reason === "string" ? plan.rewrite_reason.trim() : undefined,
|
|
1223
|
+
rewrite_scope: rewriteScopeProvided ? rewriteScope : undefined,
|
|
1224
|
+
graph_rewrite_payload: rewritePayload,
|
|
1225
|
+
};
|
|
1226
|
+
}
|
|
1227
|
+
function parseWritePlanDecisions(rootObj, logger, schema) {
|
|
1228
|
+
const writePlan = asRecord(rootObj.write_plan);
|
|
1229
|
+
if (!writePlan) {
|
|
378
1230
|
return [];
|
|
379
1231
|
}
|
|
380
|
-
|
|
381
|
-
|
|
1232
|
+
const trustedCandidateIds = new Set();
|
|
1233
|
+
const candidateRouteById = new Map();
|
|
1234
|
+
const candidateTextById = new Map();
|
|
1235
|
+
const candidateReasonById = new Map();
|
|
1236
|
+
const orderedCandidateIds = [];
|
|
1237
|
+
const candidates = Array.isArray(writePlan.candidates) ? writePlan.candidates : [];
|
|
1238
|
+
for (const candidateRaw of candidates) {
|
|
1239
|
+
const candidate = asRecord(candidateRaw);
|
|
1240
|
+
if (!candidate)
|
|
1241
|
+
continue;
|
|
1242
|
+
const candidateId = typeof candidate.candidate_id === "string" ? candidate.candidate_id.trim() : "";
|
|
1243
|
+
if (!candidateId)
|
|
1244
|
+
continue;
|
|
1245
|
+
trustedCandidateIds.add(candidateId);
|
|
1246
|
+
if (!orderedCandidateIds.includes(candidateId)) {
|
|
1247
|
+
orderedCandidateIds.push(candidateId);
|
|
1248
|
+
}
|
|
1249
|
+
const route = parseGateTargetLayer(candidate.route ?? candidate.target_layer);
|
|
1250
|
+
if (route) {
|
|
1251
|
+
candidateRouteById.set(candidateId, route);
|
|
1252
|
+
}
|
|
1253
|
+
const candidateText = firstString([candidate.normalized_text, candidate.span, candidate.candidate_text, candidate.text]) || "";
|
|
1254
|
+
if (candidateText) {
|
|
1255
|
+
candidateTextById.set(candidateId, candidateText);
|
|
1256
|
+
}
|
|
1257
|
+
const reason = typeof candidate.reason === "string" ? candidate.reason.trim() : "";
|
|
1258
|
+
if (reason) {
|
|
1259
|
+
candidateReasonById.set(candidateId, reason);
|
|
1260
|
+
}
|
|
382
1261
|
}
|
|
383
|
-
const
|
|
384
|
-
const
|
|
1262
|
+
const activePayloadById = new Map();
|
|
1263
|
+
const activePayloads = Array.isArray(writePlan.active_payloads) ? writePlan.active_payloads : [];
|
|
1264
|
+
for (const payloadRaw of activePayloads) {
|
|
1265
|
+
const payload = asRecord(payloadRaw);
|
|
1266
|
+
if (!payload)
|
|
1267
|
+
continue;
|
|
1268
|
+
const candidateId = typeof payload.candidate_id === "string" ? payload.candidate_id.trim() : "";
|
|
1269
|
+
if (!candidateId)
|
|
1270
|
+
continue;
|
|
1271
|
+
trustedCandidateIds.add(candidateId);
|
|
1272
|
+
const sourceSlice = firstString([payload.source_slice, payload.source_span, payload.normalized_text, payload.span, payload.candidate_text]) || "";
|
|
1273
|
+
if (sourceSlice && !candidateTextById.has(candidateId)) {
|
|
1274
|
+
candidateTextById.set(candidateId, sourceSlice);
|
|
1275
|
+
}
|
|
1276
|
+
const activeText = firstString([payload.active_text, payload.activeText, payload.text]) || "";
|
|
1277
|
+
if (activeText) {
|
|
1278
|
+
activePayloadById.set(candidateId, activeText);
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
const archivePayloadById = new Map();
|
|
1282
|
+
const archivePayloads = Array.isArray(writePlan.archive_payloads) ? writePlan.archive_payloads : [];
|
|
1283
|
+
for (const payloadRaw of archivePayloads) {
|
|
1284
|
+
const payload = asRecord(payloadRaw);
|
|
1285
|
+
if (!payload)
|
|
1286
|
+
continue;
|
|
1287
|
+
const candidateId = typeof payload.candidate_id === "string" ? payload.candidate_id.trim() : "";
|
|
1288
|
+
if (!candidateId)
|
|
1289
|
+
continue;
|
|
1290
|
+
trustedCandidateIds.add(candidateId);
|
|
1291
|
+
const eventObj = asRecord(payload.event);
|
|
1292
|
+
const sourceSlice = firstString([
|
|
1293
|
+
payload.source_slice,
|
|
1294
|
+
eventObj?.source_slice,
|
|
1295
|
+
payload.source_span,
|
|
1296
|
+
eventObj?.source_span,
|
|
1297
|
+
payload.normalized_text,
|
|
1298
|
+
eventObj?.normalized_text,
|
|
1299
|
+
payload.span,
|
|
1300
|
+
eventObj?.span,
|
|
1301
|
+
payload.candidate_text,
|
|
1302
|
+
eventObj?.candidate_text,
|
|
1303
|
+
]) || "";
|
|
1304
|
+
if (sourceSlice && !candidateTextById.has(candidateId)) {
|
|
1305
|
+
candidateTextById.set(candidateId, sourceSlice);
|
|
1306
|
+
}
|
|
1307
|
+
const archiveValue = payload.event ?? payload;
|
|
1308
|
+
const eventValidation = (0, llm_output_validator_1.validateArchiveEvent)(archiveValue, { schema });
|
|
1309
|
+
if (!eventValidation.valid || !eventValidation.cleaned) {
|
|
1310
|
+
if (logger) {
|
|
1311
|
+
logger.warn(`quality_event_invalid candidate_id=${candidateId} errors=${eventValidation.errors.join("|")}`);
|
|
1312
|
+
}
|
|
1313
|
+
continue;
|
|
1314
|
+
}
|
|
1315
|
+
archivePayloadById.set(candidateId, {
|
|
1316
|
+
event_type: eventValidation.cleaned.event_type || "insight",
|
|
1317
|
+
summary: eventValidation.cleaned.summary,
|
|
1318
|
+
cause: eventValidation.cleaned.cause,
|
|
1319
|
+
process: eventValidation.cleaned.process,
|
|
1320
|
+
result: eventValidation.cleaned.result,
|
|
1321
|
+
entities: eventValidation.cleaned.entities,
|
|
1322
|
+
entity_types: eventValidation.cleaned.entity_types,
|
|
1323
|
+
relations: eventValidation.cleaned.relations,
|
|
1324
|
+
outcome: eventValidation.cleaned.outcome || "",
|
|
1325
|
+
confidence: eventValidation.cleaned.confidence,
|
|
1326
|
+
});
|
|
1327
|
+
}
|
|
1328
|
+
const graphPayloadById = new Map();
|
|
1329
|
+
const graphPayloads = Array.isArray(writePlan.graph_payloads) ? writePlan.graph_payloads : [];
|
|
1330
|
+
for (const payloadRaw of graphPayloads) {
|
|
1331
|
+
const payload = asRecord(payloadRaw);
|
|
1332
|
+
if (!payload)
|
|
1333
|
+
continue;
|
|
1334
|
+
const candidateId = typeof payload.candidate_id === "string" ? payload.candidate_id.trim() : "";
|
|
1335
|
+
if (!candidateId)
|
|
1336
|
+
continue;
|
|
1337
|
+
trustedCandidateIds.add(candidateId);
|
|
1338
|
+
const sourceSlice = firstString([payload.source_slice, payload.source_span, payload.normalized_text, payload.span, payload.candidate_text]) || "";
|
|
1339
|
+
if (sourceSlice && !candidateTextById.has(candidateId)) {
|
|
1340
|
+
candidateTextById.set(candidateId, sourceSlice);
|
|
1341
|
+
}
|
|
1342
|
+
const graphValue = asRecord(payload.graph_payload) || asRecord(payload.graph) || payload;
|
|
1343
|
+
const graphPayload = toAppendableGraphPayload(parseGraphPayload(graphValue));
|
|
1344
|
+
if (!graphPayload)
|
|
1345
|
+
continue;
|
|
1346
|
+
graphPayloadById.set(candidateId, graphPayload);
|
|
1347
|
+
}
|
|
1348
|
+
const mergeHintById = new Map();
|
|
1349
|
+
const mergeHints = Array.isArray(writePlan.merge_hints) ? writePlan.merge_hints : [];
|
|
1350
|
+
for (const hintRaw of mergeHints) {
|
|
1351
|
+
const hint = parseMergeHintPayload(hintRaw);
|
|
1352
|
+
if (!hint?.candidate_id)
|
|
1353
|
+
continue;
|
|
1354
|
+
if (!trustedCandidateIds.has(hint.candidate_id)) {
|
|
1355
|
+
logger?.warn(`quality_gate_stage_d_unknown_candidate merge_hint candidate_id=${hint.candidate_id}`);
|
|
1356
|
+
continue;
|
|
1357
|
+
}
|
|
1358
|
+
mergeHintById.set(hint.candidate_id, hint);
|
|
1359
|
+
}
|
|
1360
|
+
const graphRewriteById = new Map();
|
|
1361
|
+
const graphRewriteItems = Array.isArray(writePlan.graph_rewrite) ? writePlan.graph_rewrite : [];
|
|
1362
|
+
for (const itemRaw of graphRewriteItems) {
|
|
1363
|
+
const item = parseGraphRewritePlanPayload(itemRaw);
|
|
1364
|
+
if (!item?.candidate_id)
|
|
1365
|
+
continue;
|
|
1366
|
+
if (!trustedCandidateIds.has(item.candidate_id)) {
|
|
1367
|
+
logger?.warn(`quality_gate_stage_d_unknown_candidate graph_rewrite candidate_id=${item.candidate_id}`);
|
|
1368
|
+
continue;
|
|
1369
|
+
}
|
|
1370
|
+
graphRewriteById.set(item.candidate_id, item);
|
|
1371
|
+
}
|
|
1372
|
+
const orderedIds = [];
|
|
1373
|
+
const seenIds = new Set();
|
|
1374
|
+
function pushId(id) {
|
|
1375
|
+
if (!id || seenIds.has(id))
|
|
1376
|
+
return;
|
|
1377
|
+
seenIds.add(id);
|
|
1378
|
+
orderedIds.push(id);
|
|
1379
|
+
}
|
|
1380
|
+
for (const id of orderedCandidateIds)
|
|
1381
|
+
pushId(id);
|
|
1382
|
+
for (const id of activePayloadById.keys())
|
|
1383
|
+
pushId(id);
|
|
1384
|
+
for (const id of archivePayloadById.keys())
|
|
1385
|
+
pushId(id);
|
|
1386
|
+
for (const id of graphPayloadById.keys())
|
|
1387
|
+
pushId(id);
|
|
1388
|
+
for (const id of mergeHintById.keys())
|
|
1389
|
+
pushId(id);
|
|
1390
|
+
for (const id of graphRewriteById.keys())
|
|
1391
|
+
pushId(id);
|
|
1392
|
+
const decisions = [];
|
|
1393
|
+
for (const candidateId of orderedIds) {
|
|
1394
|
+
let targetLayer = candidateRouteById.get(candidateId);
|
|
1395
|
+
if (!targetLayer) {
|
|
1396
|
+
if (archivePayloadById.has(candidateId)) {
|
|
1397
|
+
targetLayer = "archive_event";
|
|
1398
|
+
}
|
|
1399
|
+
else if (activePayloadById.has(candidateId)) {
|
|
1400
|
+
targetLayer = "active_only";
|
|
1401
|
+
}
|
|
1402
|
+
else {
|
|
1403
|
+
targetLayer = "skip";
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
let event = archivePayloadById.get(candidateId);
|
|
1407
|
+
if (targetLayer === "archive_event" && !event) {
|
|
1408
|
+
if (logger) {
|
|
1409
|
+
logger.warn(`quality_gate_decisions_archive_missing candidate_id=${candidateId}`);
|
|
1410
|
+
}
|
|
1411
|
+
targetLayer = activePayloadById.has(candidateId) ? "active_only" : "skip";
|
|
1412
|
+
event = undefined;
|
|
1413
|
+
}
|
|
1414
|
+
const activeText = activePayloadById.get(candidateId) || (targetLayer === "active_only" ? candidateTextById.get(candidateId) || "" : "");
|
|
1415
|
+
decisions.push({
|
|
1416
|
+
candidate_id: candidateId,
|
|
1417
|
+
candidate_text: candidateTextById.get(candidateId),
|
|
1418
|
+
target_layer: targetLayer,
|
|
1419
|
+
active_text: activeText,
|
|
1420
|
+
event: targetLayer === "archive_event" ? event : undefined,
|
|
1421
|
+
graph: graphPayloadById.get(candidateId),
|
|
1422
|
+
merge_hint: mergeHintById.get(candidateId),
|
|
1423
|
+
graph_rewrite: graphRewriteById.get(candidateId),
|
|
1424
|
+
reason: candidateReasonById.get(candidateId) || (targetLayer === "skip" ? "llm_gate_skip" : ""),
|
|
1425
|
+
});
|
|
1426
|
+
}
|
|
1427
|
+
return decisions;
|
|
1428
|
+
}
|
|
1429
|
+
function parseLegacyRoutingDecisions(rootObj, logger, schema) {
|
|
385
1430
|
const output = [];
|
|
1431
|
+
const candidateTextById = new Map();
|
|
1432
|
+
if (Array.isArray(rootObj.candidate_events)) {
|
|
1433
|
+
for (const candidateRaw of rootObj.candidate_events) {
|
|
1434
|
+
if (!candidateRaw || typeof candidateRaw !== "object")
|
|
1435
|
+
continue;
|
|
1436
|
+
const candidate = candidateRaw;
|
|
1437
|
+
const candidateId = typeof candidate.candidate_id === "string" ? candidate.candidate_id.trim() : "";
|
|
1438
|
+
if (!candidateId)
|
|
1439
|
+
continue;
|
|
1440
|
+
const candidateText = firstString([candidate.normalized_text, candidate.span, candidate.text]) || "";
|
|
1441
|
+
if (candidateText) {
|
|
1442
|
+
candidateTextById.set(candidateId, candidateText);
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
386
1446
|
const pushDecision = (obj, target) => {
|
|
1447
|
+
const candidateId = typeof obj.candidate_id === "string" ? obj.candidate_id.trim() : "";
|
|
387
1448
|
let event = null;
|
|
388
1449
|
if (target === "archive_event") {
|
|
389
|
-
const eventValidation = (0, llm_output_validator_1.validateArchiveEvent)(obj.event || obj);
|
|
1450
|
+
const eventValidation = (0, llm_output_validator_1.validateArchiveEvent)(obj.event || obj, { schema });
|
|
390
1451
|
if (eventValidation.valid && eventValidation.cleaned) {
|
|
391
1452
|
event = {
|
|
392
1453
|
event_type: eventValidation.cleaned.event_type || "insight",
|
|
393
1454
|
summary: eventValidation.cleaned.summary,
|
|
1455
|
+
cause: eventValidation.cleaned.cause,
|
|
1456
|
+
process: eventValidation.cleaned.process,
|
|
1457
|
+
result: eventValidation.cleaned.result,
|
|
394
1458
|
entities: eventValidation.cleaned.entities,
|
|
395
1459
|
entity_types: eventValidation.cleaned.entity_types,
|
|
396
1460
|
relations: eventValidation.cleaned.relations,
|
|
@@ -406,51 +1470,71 @@ function parseLlmGateDecisions(raw, logger) {
|
|
|
406
1470
|
}
|
|
407
1471
|
}
|
|
408
1472
|
output.push({
|
|
409
|
-
candidate_id:
|
|
1473
|
+
candidate_id: candidateId,
|
|
1474
|
+
candidate_text: candidateTextById.get(candidateId) || undefined,
|
|
410
1475
|
target_layer: target,
|
|
411
1476
|
active_text: typeof obj.active_text === "string" ? obj.active_text.trim() : "",
|
|
412
1477
|
event: event || undefined,
|
|
413
|
-
graph: parseGraphPayload(obj.graph)
|
|
1478
|
+
graph: toAppendableGraphPayload(parseGraphPayload(obj.graph)),
|
|
414
1479
|
reason: typeof obj.reason === "string" ? obj.reason.trim() : "",
|
|
415
1480
|
});
|
|
416
1481
|
};
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
{ key: "archive_event", items: routingPlan.archive_event },
|
|
430
|
-
{ key: "active_only", items: routingPlan.active_only },
|
|
431
|
-
{ key: "skip", items: routingPlan.skip },
|
|
432
|
-
];
|
|
433
|
-
for (const bucket of buckets) {
|
|
434
|
-
if (!Array.isArray(bucket.items))
|
|
1482
|
+
const routingPlan = asRecord(rootObj.routing_plan);
|
|
1483
|
+
if (routingPlan) {
|
|
1484
|
+
const buckets = [
|
|
1485
|
+
{ key: "archive_event", items: routingPlan.archive_event },
|
|
1486
|
+
{ key: "active_only", items: routingPlan.active_only },
|
|
1487
|
+
{ key: "skip", items: routingPlan.skip },
|
|
1488
|
+
];
|
|
1489
|
+
for (const bucket of buckets) {
|
|
1490
|
+
if (!Array.isArray(bucket.items))
|
|
1491
|
+
continue;
|
|
1492
|
+
for (const item of bucket.items) {
|
|
1493
|
+
if (!item || typeof item !== "object")
|
|
435
1494
|
continue;
|
|
436
|
-
|
|
437
|
-
if (!item || typeof item !== "object")
|
|
438
|
-
continue;
|
|
439
|
-
pushDecision(item, bucket.key);
|
|
440
|
-
}
|
|
1495
|
+
pushDecision(item, bucket.key);
|
|
441
1496
|
}
|
|
442
1497
|
}
|
|
443
|
-
|
|
444
|
-
|
|
1498
|
+
}
|
|
1499
|
+
else if (logger) {
|
|
1500
|
+
logger.warn("quality_gate_decisions_invalid missing_routing_plan");
|
|
1501
|
+
}
|
|
1502
|
+
return output;
|
|
1503
|
+
}
|
|
1504
|
+
function parseLlmGateDecisions(raw, logger, schema) {
|
|
1505
|
+
const validation = (0, llm_output_validator_1.validateLlmJsonOutput)(raw);
|
|
1506
|
+
if (!validation.valid) {
|
|
1507
|
+
if (logger) {
|
|
1508
|
+
logger.warn(`quality_gate_decisions_invalid errors=${validation.errors.join("|")}`);
|
|
1509
|
+
}
|
|
1510
|
+
return [];
|
|
1511
|
+
}
|
|
1512
|
+
if (validation.warnings.length > 0 && logger) {
|
|
1513
|
+
logger.debug(`quality_gate_decisions_warnings warnings=${validation.warnings.join("|")}`);
|
|
1514
|
+
}
|
|
1515
|
+
const root = validation.data;
|
|
1516
|
+
if (Array.isArray(root)) {
|
|
1517
|
+
if (logger) {
|
|
1518
|
+
logger.warn("quality_gate_decisions_invalid format=array_not_supported_require_write_plan");
|
|
445
1519
|
}
|
|
1520
|
+
return [];
|
|
1521
|
+
}
|
|
1522
|
+
const rootObj = asRecord(root);
|
|
1523
|
+
if (!rootObj) {
|
|
1524
|
+
if (logger) {
|
|
1525
|
+
logger.warn("quality_gate_decisions_invalid root_not_object");
|
|
1526
|
+
}
|
|
1527
|
+
return [];
|
|
446
1528
|
}
|
|
447
|
-
|
|
1529
|
+
const output = parseWritePlanDecisions(rootObj, logger, schema);
|
|
1530
|
+
const legacyOutput = output.length > 0 ? output : parseLegacyRoutingDecisions(rootObj, logger, schema);
|
|
1531
|
+
if (legacyOutput.length === 0 && logger) {
|
|
448
1532
|
logger.warn("quality_gate_decisions_empty");
|
|
449
1533
|
}
|
|
450
1534
|
const deduped = [];
|
|
451
1535
|
const seen = new Set();
|
|
452
|
-
for (const item of
|
|
453
|
-
const key = `${item.target_layer}|${item.event?.summary || item.active_text || item.reason || ""}`;
|
|
1536
|
+
for (const item of legacyOutput) {
|
|
1537
|
+
const key = `${item.candidate_id || ""}|${item.target_layer}|${item.event?.summary || item.active_text || item.reason || ""}`;
|
|
454
1538
|
if (seen.has(key))
|
|
455
1539
|
continue;
|
|
456
1540
|
seen.add(key);
|
|
@@ -458,75 +1542,390 @@ function parseLlmGateDecisions(raw, logger) {
|
|
|
458
1542
|
}
|
|
459
1543
|
return deduped;
|
|
460
1544
|
}
|
|
1545
|
+
function readWritePlanObject(rootObj) {
|
|
1546
|
+
return asRecord(rootObj.write_plan) || rootObj;
|
|
1547
|
+
}
|
|
1548
|
+
function readWritePlanArray(rootObj, key) {
|
|
1549
|
+
const writePlan = readWritePlanObject(rootObj);
|
|
1550
|
+
const value = writePlan[key];
|
|
1551
|
+
return Array.isArray(value) ? value : [];
|
|
1552
|
+
}
|
|
1553
|
+
async function requestWriteGateStage(args) {
|
|
1554
|
+
const body = {
|
|
1555
|
+
model: args.llm.model,
|
|
1556
|
+
temperature: 0.1,
|
|
1557
|
+
response_format: { type: "json_object" },
|
|
1558
|
+
messages: [
|
|
1559
|
+
{ role: "system", content: args.systemPrompt },
|
|
1560
|
+
{ role: "user", content: args.userLines.join("\n") },
|
|
1561
|
+
],
|
|
1562
|
+
};
|
|
1563
|
+
let lastError = null;
|
|
1564
|
+
const maxAttempts = typeof args.maxAttempts === "number" && args.maxAttempts > 0 ? args.maxAttempts : 3;
|
|
1565
|
+
const timeoutMs = typeof args.timeoutMs === "number" && args.timeoutMs >= 1000 ? args.timeoutMs : 25000;
|
|
1566
|
+
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
|
|
1567
|
+
let response;
|
|
1568
|
+
try {
|
|
1569
|
+
response = await (0, http_post_1.postJsonWithTimeout)({
|
|
1570
|
+
endpoint: args.endpoint,
|
|
1571
|
+
apiKey: args.llm.apiKey,
|
|
1572
|
+
body,
|
|
1573
|
+
timeoutMs,
|
|
1574
|
+
});
|
|
1575
|
+
}
|
|
1576
|
+
catch (error) {
|
|
1577
|
+
lastError = error;
|
|
1578
|
+
continue;
|
|
1579
|
+
}
|
|
1580
|
+
if (!response.ok) {
|
|
1581
|
+
lastError = new Error(response.status > 0 ? `sync_llm_${args.stage}_http_${response.status}` : (response.error || `sync_llm_${args.stage}_network_error`));
|
|
1582
|
+
continue;
|
|
1583
|
+
}
|
|
1584
|
+
try {
|
|
1585
|
+
const json = (response.json || {});
|
|
1586
|
+
const content = json?.choices?.[0]?.message?.content || "";
|
|
1587
|
+
if (!content.trim()) {
|
|
1588
|
+
lastError = new Error(`sync_llm_${args.stage}_empty`);
|
|
1589
|
+
continue;
|
|
1590
|
+
}
|
|
1591
|
+
const validation = (0, llm_output_validator_1.validateLlmJsonOutput)(content);
|
|
1592
|
+
if (!validation.valid) {
|
|
1593
|
+
lastError = new Error(`sync_llm_${args.stage}_invalid_json:${validation.errors.join("|")}`);
|
|
1594
|
+
continue;
|
|
1595
|
+
}
|
|
1596
|
+
const rootObj = asRecord(validation.data);
|
|
1597
|
+
if (!rootObj) {
|
|
1598
|
+
lastError = new Error(`sync_llm_${args.stage}_root_not_object`);
|
|
1599
|
+
continue;
|
|
1600
|
+
}
|
|
1601
|
+
if (validation.warnings.length > 0) {
|
|
1602
|
+
args.logger.debug(`sync_llm_${args.stage}_warnings warnings=${validation.warnings.join("|")}`);
|
|
1603
|
+
}
|
|
1604
|
+
return rootObj;
|
|
1605
|
+
}
|
|
1606
|
+
catch (error) {
|
|
1607
|
+
lastError = error;
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
args.logger.warn(`Sync LLM stage=${args.stage} failed: ${String(lastError || "unknown")}`);
|
|
1611
|
+
return null;
|
|
1612
|
+
}
|
|
461
1613
|
async function extractGateDecisionsWithLlm(args) {
|
|
462
1614
|
const endpoint = args.llm.baseUrl.endsWith("/chat/completions")
|
|
463
1615
|
? args.llm.baseUrl
|
|
464
1616
|
: `${args.llm.baseUrl}/chat/completions`;
|
|
1617
|
+
const eventSnippet = buildEventSnippet(args.transcript);
|
|
1618
|
+
const activeValuePromptHint = buildActiveValuePromptHint(args.schema);
|
|
1619
|
+
const entityDictionaryPromptHint = buildEntityDictionaryPromptHint(args.schema);
|
|
1620
|
+
const stableGraphFactHint = `Stable graph facts are evidence-backed and dictionary-grounded entities/relations with reusable value. ${activeValuePromptHint}`;
|
|
1621
|
+
const stageAbRoot = await requestWriteGateStage({
|
|
1622
|
+
stage: "ab",
|
|
1623
|
+
llm: args.llm,
|
|
1624
|
+
endpoint,
|
|
1625
|
+
logger: args.logger,
|
|
1626
|
+
systemPrompt: "You are memory write-gate stage A+B router. Output JSON only.",
|
|
1627
|
+
userLines: [
|
|
1628
|
+
`prompt_version=${WRITE_GATE_STAGE_AB_PROMPT_VERSION}`,
|
|
1629
|
+
"Task: execute Stage A+B only (denoise + candidate split + route classification).",
|
|
1630
|
+
"Route classes: active_only | archive_event | skip.",
|
|
1631
|
+
"Rules:",
|
|
1632
|
+
"- Denoise first: remove pure acknowledgements/politeness/chitchat/repeated filler (for example: 好的/收到/谢谢/ok/got it/thanks) when they contain no task facts.",
|
|
1633
|
+
`- Keep factual evidence during denoise using dictionary-grounded signals. ${activeValuePromptHint}`,
|
|
1634
|
+
`- ${entityDictionaryPromptHint}`,
|
|
1635
|
+
"- Concrete entities must be source-grounded names or aliases from the dictionary above; reject generic placeholders (e.g., user/person/system/问题/方案/实体/thing).",
|
|
1636
|
+
"- Semantic event split rule: one candidate = one principal event with a coherent subject + action/decision + object/outcome in the same phase.",
|
|
1637
|
+
"- Split into different candidates when topic/goal/subject changes, or when a new decision/outcome starts.",
|
|
1638
|
+
"- Merge sentences into one candidate when they describe the same event progression in one phase.",
|
|
1639
|
+
"- One candidate must represent one principal event only.",
|
|
1640
|
+
"- archive_event: reusable event with clear subject + action/decision + outcome/phase conclusion.",
|
|
1641
|
+
"- active_only: ongoing process context or temporary status without stable conclusion, but MUST contain valuable reusable information.",
|
|
1642
|
+
"- Active_only value criteria must follow the same dictionary-grounded signals above.",
|
|
1643
|
+
"- If candidate is only acknowledgement/politeness or has no valuable signal, route to skip.",
|
|
1644
|
+
"- skip: noise/repetition/chitchat/no clear business value.",
|
|
1645
|
+
"- Archive task-lifecycle by default when all three are present: user instruction -> agent completion report -> user acceptance.",
|
|
1646
|
+
"- If failure/iteration then eventual success exists, prefer archive_event.",
|
|
1647
|
+
"Output schema:",
|
|
1648
|
+
"{\"write_plan\":{\"candidates\":[{\"candidate_id\":\"c1\",\"route\":\"archive_event\",\"span\":\"...\",\"normalized_text\":\"...\",\"reason\":\"...\"}]}}",
|
|
1649
|
+
...WRITE_GATE_REGRESSION_SAMPLES,
|
|
1650
|
+
"Output JSON only. Do NOT output active_payloads/archive_payloads/graph_payloads/merge_hints/graph_rewrite.",
|
|
1651
|
+
"",
|
|
1652
|
+
"[TRANSCRIPT_SNIPPET]",
|
|
1653
|
+
eventSnippet,
|
|
1654
|
+
"[/TRANSCRIPT_SNIPPET]",
|
|
1655
|
+
],
|
|
1656
|
+
});
|
|
1657
|
+
if (!stageAbRoot) {
|
|
1658
|
+
return [];
|
|
1659
|
+
}
|
|
1660
|
+
const stageAbCandidates = readWritePlanArray(stageAbRoot, "candidates");
|
|
1661
|
+
if (stageAbCandidates.length === 0) {
|
|
1662
|
+
args.logger.warn("quality_gate_stage_ab_empty_candidates");
|
|
1663
|
+
return [];
|
|
1664
|
+
}
|
|
1665
|
+
const stageCRoot = await requestWriteGateStage({
|
|
1666
|
+
stage: "c",
|
|
1667
|
+
llm: args.llm,
|
|
1668
|
+
endpoint,
|
|
1669
|
+
logger: args.logger,
|
|
1670
|
+
systemPrompt: "You are memory write-gate stage C payload builder. Output JSON only.",
|
|
1671
|
+
userLines: [
|
|
1672
|
+
`prompt_version=${WRITE_GATE_STAGE_C_PROMPT_VERSION}`,
|
|
1673
|
+
"Task: execute Stage C1/C2/C3 only.",
|
|
1674
|
+
"C1) Build active_payloads[] for active_only candidates.",
|
|
1675
|
+
"C2) Build archive_payloads[] for archive_event candidates.",
|
|
1676
|
+
"C3) Build graph_payloads[] for candidates with stable graph facts (independent from route).",
|
|
1677
|
+
"Keep candidate_id exactly equal to input candidates.",
|
|
1678
|
+
"- For active_only route, C1 active_text is required and must contain valuable reusable information, not only acknowledgement/politeness.",
|
|
1679
|
+
"- For archive_event route, C2 archive payload must include complete cause/process/result; if confidence < 0.35, prefer no archive payload for that candidate.",
|
|
1680
|
+
"- Preserve key entities/relations/URLs/document paths/exact numbers/timepoints from source text; do NOT over-abstract placeholders.",
|
|
1681
|
+
`- ${stableGraphFactHint}`,
|
|
1682
|
+
`- ${entityDictionaryPromptHint}`,
|
|
1683
|
+
"- Concrete entities in C1/C2/C3 must follow the dictionary above and remain source-grounded; do not output generic placeholders.",
|
|
1684
|
+
"- Each C1/C2/C3 item must carry source_slice, which is the denoised segmented source text for that candidate from A+B.",
|
|
1685
|
+
"- source_slice is the canonical text passage for executor write path and traceability; do not invent text outside source.",
|
|
1686
|
+
"C1 field requirements (active_payloads[] item):",
|
|
1687
|
+
"- candidate_id: required; must match one input candidate_id exactly.",
|
|
1688
|
+
"- source_slice: required; denoised segmented source text for this candidate.",
|
|
1689
|
+
"- active_text: required; non-empty; must keep factual evidence and valuable progress context; must not be only acknowledgement/politeness.",
|
|
1690
|
+
"- active_text should preserve concrete entities, decisions, blockers/risks, next actions, owners, deadlines, metrics, URLs/paths/config/version details when present.",
|
|
1691
|
+
"C2 field requirements (archive_payloads[] item):",
|
|
1692
|
+
"- candidate_id: required; must match one input candidate_id exactly.",
|
|
1693
|
+
"- source_slice: required; denoised segmented source text for this candidate.",
|
|
1694
|
+
"- event_type: required; must be dictionary-grounded to schema eventTypes/eventTypeAliases.",
|
|
1695
|
+
"- summary: required; concise but complete, and should mention key entities in the event.",
|
|
1696
|
+
"- cause: required; explain why the event happened.",
|
|
1697
|
+
"- process: required; explain key execution/decision process.",
|
|
1698
|
+
"- result: required; explain final result/state.",
|
|
1699
|
+
"- entities: recommended; if present, use concrete entity names from source text.",
|
|
1700
|
+
"- entity_types: recommended; if entities are present, provide valid schema type for each entity.",
|
|
1701
|
+
"- relations: optional; if provided, each relation should be source-grounded and schema-compatible.",
|
|
1702
|
+
"- outcome: optional short conclusion.",
|
|
1703
|
+
"- confidence: recommended numeric score in [0,1].",
|
|
1704
|
+
"- For every candidate routed to active_only or archive_event, C3 graph_payload is REQUIRED (non-optional).",
|
|
1705
|
+
"- 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.",
|
|
1706
|
+
"C3 graph payload requirements (single authoritative definition):",
|
|
1707
|
+
"- Required fields: candidate_id, source_slice, summary, source_text_nav, entities[], entity_types, relations[], confidence.",
|
|
1708
|
+
"- summary must cover every entity listed in entities[].",
|
|
1709
|
+
"- source_text_nav must include: layer,session_id,source_file,source_memory_id,source_event_id.",
|
|
1710
|
+
"- entity_types must provide a valid type for every entity in entities[].",
|
|
1711
|
+
"- Key-entity protection: entities explicitly mentioned in candidate span should not be dropped.",
|
|
1712
|
+
"- Normalize alias/cross-language references to one canonical entity when possible.",
|
|
1713
|
+
`- ${args.relationPromptHint}`,
|
|
1714
|
+
"- Every relation must include: source,target,type,relation_origin,evidence_span,context_chunk,confidence.",
|
|
1715
|
+
"- If relation_origin is llm_custom, relation_definition is required.",
|
|
1716
|
+
"- If evidence_span/context_chunk/confidence is missing, do not output that relation.",
|
|
1717
|
+
"Output structure (C1/C2/C3 are different item schemas):",
|
|
1718
|
+
"- write_plan.active_payloads[] item (C1): {candidate_id,source_slice,active_text}",
|
|
1719
|
+
"- write_plan.archive_payloads[] item (C2): {candidate_id,source_slice,event_type,summary,cause,process,result,entities,entity_types,relations,outcome,confidence}",
|
|
1720
|
+
"- write_plan.graph_payloads[] item (C3): {candidate_id,source_slice,summary,source_text_nav,entities,entity_types,relations,confidence}",
|
|
1721
|
+
"Top-level output schema:",
|
|
1722
|
+
"{\"write_plan\":{\"active_payloads\":[{\"candidate_id\":\"c1\",\"source_slice\":\"...\",\"active_text\":\"...\"}],\"archive_payloads\":[{\"candidate_id\":\"c2\",\"source_slice\":\"...\",\"event_type\":\"decision\",\"summary\":\"...\",\"cause\":\"...\",\"process\":\"...\",\"result\":\"...\",\"entities\":[\"A\"],\"entity_types\":{\"A\":\"Project\"},\"relations\":[],\"outcome\":\"...\",\"confidence\":0.82}],\"graph_payloads\":[{\"candidate_id\":\"c1\",\"source_slice\":\"...\",\"summary\":\"...\",\"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\"],\"entity_types\":{\"A\":\"Project\"},\"relations\":[{\"source\":\"A\",\"target\":\"B\",\"type\":\"depends_on\",\"relation_origin\":\"canonical\",\"evidence_span\":\"A 依赖 B\",\"context_chunk\":\"原文上下文...\",\"confidence\":0.9}],\"confidence\":0.8}]}}",
|
|
1723
|
+
"Output JSON only. Do NOT output merge_hints/graph_rewrite.",
|
|
1724
|
+
"",
|
|
1725
|
+
"[CANDIDATES]",
|
|
1726
|
+
JSON.stringify(stageAbCandidates),
|
|
1727
|
+
"[/CANDIDATES]",
|
|
1728
|
+
"",
|
|
1729
|
+
"[TRANSCRIPT_SNIPPET]",
|
|
1730
|
+
eventSnippet,
|
|
1731
|
+
"[/TRANSCRIPT_SNIPPET]",
|
|
1732
|
+
],
|
|
1733
|
+
});
|
|
1734
|
+
const stageCActivePayloads = stageCRoot ? readWritePlanArray(stageCRoot, "active_payloads") : [];
|
|
1735
|
+
const stageCArchivePayloads = stageCRoot ? readWritePlanArray(stageCRoot, "archive_payloads") : [];
|
|
1736
|
+
const stageCGraphPayloads = stageCRoot ? readWritePlanArray(stageCRoot, "graph_payloads") : [];
|
|
1737
|
+
const stageDRoot = await requestWriteGateStage({
|
|
1738
|
+
stage: "d",
|
|
1739
|
+
llm: args.llm,
|
|
1740
|
+
endpoint,
|
|
1741
|
+
logger: args.logger,
|
|
1742
|
+
systemPrompt: "You are memory write-gate stage D merge/rewrite planner. Output JSON only.",
|
|
1743
|
+
userLines: [
|
|
1744
|
+
`prompt_version=${WRITE_GATE_STAGE_D_PROMPT_VERSION}`,
|
|
1745
|
+
"Task: execute Stage D only (merge/conflict/rewrite planning).",
|
|
1746
|
+
"Output merge_hints[] and graph_rewrite[] only for executor consumption.",
|
|
1747
|
+
"Rules:",
|
|
1748
|
+
`- ${entityDictionaryPromptHint}`,
|
|
1749
|
+
`- Relation dictionary and mapping rule: ${args.relationPromptHint}`,
|
|
1750
|
+
`- Keep valuable factual evidence during merge/rewrite planning. ${activeValuePromptHint}`,
|
|
1751
|
+
"- Merge planning (how to merge):",
|
|
1752
|
+
"- For each candidate_id from Stage C graph_payloads, decide same_event and same_entity_pairs using source-grounded evidence.",
|
|
1753
|
+
"- same_event=true only when graph facts indicate continuation of the same principal event (same core entities + same relation cluster + same phase progression).",
|
|
1754
|
+
"- same_entity_pairs lists alias pairs that refer to the same concrete entity: [[alias,canonical_name],...].",
|
|
1755
|
+
"- canonical_name should prefer dictionary canonical/alias-backed concrete names; do not use generic placeholders.",
|
|
1756
|
+
"- If no alias merge is needed, same_entity_pairs should be empty or omitted.",
|
|
1757
|
+
"- Rewrite planning (how to rewrite):",
|
|
1758
|
+
"- rewrite_required=true only when post-merge synchronization is required for graph consistency.",
|
|
1759
|
+
"- 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.",
|
|
1760
|
+
"- rewrite_scope must be a subset of: summary, source_text_nav, entities, entity_types, relations, confidence.",
|
|
1761
|
+
"- graph_rewrite_payload is optional patch payload; include only fields in rewrite_scope.",
|
|
1762
|
+
"- If graph_rewrite_payload.relations is provided, every relation must include source,target,type,relation_origin,evidence_span,context_chunk,confidence.",
|
|
1763
|
+
"- If rewrite is not needed: set rewrite_required=false, rewrite_scope=[], graph_rewrite_payload=null.",
|
|
1764
|
+
"- Coverage and consistency:",
|
|
1765
|
+
"- Keep candidate_id exactly equal to input candidate_id; do not invent candidate_id.",
|
|
1766
|
+
"- For every Stage C graph_payload candidate, output one merge_hints item and one graph_rewrite item.",
|
|
1767
|
+
"- Do not output active_payloads/archive_payloads/graph_payloads in this stage.",
|
|
1768
|
+
"Output schema:",
|
|
1769
|
+
"{\"write_plan\":{\"merge_hints\":[{\"candidate_id\":\"c1\",\"same_event\":false,\"same_entity_pairs\":[[\"Ava\",\"艾娃\"]],\"suggested_action\":\"merge_aliases\",\"reason\":\"Ava and 艾娃 refer to the same person in this candidate\"}],\"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\":[\"艾娃\",\"Joe\"],\"entity_types\":{\"艾娃\":\"Person\",\"Joe\":\"Person\"},\"relations\":[{\"source\":\"Joe\",\"target\":\"艾娃\",\"type\":\"has_spouse\",\"relation_origin\":\"canonical\",\"evidence_span\":\"Joe 与 艾娃是夫妻\",\"context_chunk\":\"原文上下文...\",\"confidence\":0.91}],\"confidence\":0.86}}]}}",
|
|
1770
|
+
"Output JSON only.",
|
|
1771
|
+
"",
|
|
1772
|
+
"[CANDIDATES]",
|
|
1773
|
+
JSON.stringify(stageAbCandidates),
|
|
1774
|
+
"[/CANDIDATES]",
|
|
1775
|
+
"",
|
|
1776
|
+
"[STAGE_C_PAYLOADS]",
|
|
1777
|
+
JSON.stringify({
|
|
1778
|
+
active_payloads: stageCActivePayloads,
|
|
1779
|
+
archive_payloads: stageCArchivePayloads,
|
|
1780
|
+
graph_payloads: stageCGraphPayloads,
|
|
1781
|
+
}),
|
|
1782
|
+
"[/STAGE_C_PAYLOADS]",
|
|
1783
|
+
"",
|
|
1784
|
+
"[TRANSCRIPT_SNIPPET]",
|
|
1785
|
+
eventSnippet,
|
|
1786
|
+
"[/TRANSCRIPT_SNIPPET]",
|
|
1787
|
+
],
|
|
1788
|
+
});
|
|
1789
|
+
const stageDMergeHints = stageDRoot ? readWritePlanArray(stageDRoot, "merge_hints") : [];
|
|
1790
|
+
const stageDGraphRewrite = stageDRoot ? readWritePlanArray(stageDRoot, "graph_rewrite") : [];
|
|
1791
|
+
const mergedPlan = {
|
|
1792
|
+
write_plan: {
|
|
1793
|
+
candidates: stageAbCandidates,
|
|
1794
|
+
active_payloads: stageCActivePayloads,
|
|
1795
|
+
archive_payloads: stageCArchivePayloads,
|
|
1796
|
+
graph_payloads: stageCGraphPayloads,
|
|
1797
|
+
merge_hints: stageDMergeHints,
|
|
1798
|
+
graph_rewrite: stageDGraphRewrite,
|
|
1799
|
+
},
|
|
1800
|
+
};
|
|
1801
|
+
const decisions = parseWritePlanDecisions(mergedPlan, args.logger, args.schema);
|
|
1802
|
+
if (decisions.length > 0) {
|
|
1803
|
+
return decisions;
|
|
1804
|
+
}
|
|
1805
|
+
args.logger.warn("quality_gate_decisions_empty stage_pipeline");
|
|
1806
|
+
return [];
|
|
1807
|
+
}
|
|
1808
|
+
async function rewriteGraphPayloadWithLlm(args) {
|
|
1809
|
+
const endpoint = args.llm.baseUrl.endsWith("/chat/completions")
|
|
1810
|
+
? args.llm.baseUrl
|
|
1811
|
+
: `${args.llm.baseUrl}/chat/completions`;
|
|
1812
|
+
const candidateText = (args.candidateText || "").trim();
|
|
1813
|
+
const sourceSnippet = candidateText || buildEventSnippet(args.transcript);
|
|
465
1814
|
const body = {
|
|
466
1815
|
model: args.llm.model,
|
|
467
1816
|
temperature: 0.1,
|
|
468
1817
|
response_format: { type: "json_object" },
|
|
469
1818
|
messages: [
|
|
470
|
-
{ role: "system", content: "You are a
|
|
1819
|
+
{ role: "system", content: "You are a graph payload rewrite engine. Output JSON only." },
|
|
471
1820
|
{
|
|
472
1821
|
role: "user",
|
|
473
1822
|
content: [
|
|
474
|
-
`prompt_version=${
|
|
475
|
-
"
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
"
|
|
480
|
-
|
|
481
|
-
"
|
|
482
|
-
"
|
|
483
|
-
"
|
|
484
|
-
"-
|
|
485
|
-
"-
|
|
486
|
-
"-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
"
|
|
490
|
-
|
|
491
|
-
"
|
|
492
|
-
"
|
|
493
|
-
"
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
"
|
|
1823
|
+
`prompt_version=${WRITE_GATE_GRAPH_REWRITE_PROMPT_VERSION}`,
|
|
1824
|
+
"Task: rewrite graph payload only when synchronization is required after merge/conflict checks.",
|
|
1825
|
+
`rewrite_required=${args.rewritePlan?.rewrite_required === true ? "true" : "false"}`,
|
|
1826
|
+
`rewrite_reason=${args.rewritePlan?.rewrite_reason || ""}`,
|
|
1827
|
+
`rewrite_scope=${(args.rewritePlan?.rewrite_scope || []).join(",")}`,
|
|
1828
|
+
`merge_same_event=${args.mergeHint?.same_event === true ? "true" : "false"}`,
|
|
1829
|
+
`merge_same_entity_pairs=${JSON.stringify(args.mergeHint?.same_entity_pairs || [])}`,
|
|
1830
|
+
"Rewrite policy:",
|
|
1831
|
+
"- Return a FULL graph_payload object (not partial): include summary, source_text_nav, entities, entity_types, relations, confidence.",
|
|
1832
|
+
"- Apply semantic changes only for fields in rewrite_scope; for fields outside rewrite_scope, copy from CURRENT_GRAPH_PAYLOAD unless hard validation requires normalization.",
|
|
1833
|
+
"- Prefer merge-hint canonical entities for alias unification when same_entity_pairs is provided.",
|
|
1834
|
+
"- Do not invent new facts outside SOURCE_SNIPPET and CURRENT_GRAPH_PAYLOAD.",
|
|
1835
|
+
"- Keep factual evidence and valuable signals. "
|
|
1836
|
+
+ "Decisions/problems/fixes/constraints/requirements/names/deadlines/metrics/URLs/paths/document ids/config-version details should be preserved when present. "
|
|
1837
|
+
+ args.activeValuePromptHint,
|
|
1838
|
+
"Hard constraints:",
|
|
1839
|
+
`- ${args.entityDictionaryPromptHint}`,
|
|
1840
|
+
"- Keep entities concrete, dictionary-grounded names/aliases; no generic placeholders.",
|
|
1841
|
+
"- Keep relations source-grounded and evidence-backed.",
|
|
1842
|
+
"- Do not output related_to.",
|
|
1843
|
+
`- ${args.relationPromptHint}`,
|
|
1844
|
+
"Field contract (required):",
|
|
1845
|
+
"- graph_payload.summary: concise stable fact summary; must cover all entities and key relations.",
|
|
1846
|
+
"- graph_payload.source_text_nav: required object with layer,session_id,source_file,source_memory_id,source_event_id; fulltext_anchor optional.",
|
|
1847
|
+
"- graph_payload.entities: required array of concrete entity names; deduplicated.",
|
|
1848
|
+
"- graph_payload.entity_types: required map; every entity in entities should have a type from dictionary.",
|
|
1849
|
+
"- graph_payload.relations: required array; each relation source/target should refer to entities in graph_payload.entities.",
|
|
1850
|
+
"- Every relation must include source,target,type,relation_origin,evidence_span,context_chunk,confidence.",
|
|
1851
|
+
"- relation_origin: canonical or llm_custom.",
|
|
1852
|
+
"- If relation_origin is llm_custom, relation_definition is required.",
|
|
1853
|
+
"- graph_payload.confidence: required number in [0,1], reflecting final payload certainty.",
|
|
1854
|
+
"Consistency checks before output:",
|
|
1855
|
+
"- Ensure summary mentions entities and does not contradict relations.",
|
|
1856
|
+
"- Ensure context_chunk/evidence_span comes from source text and supports the relation.",
|
|
1857
|
+
"- If rewrite_scope is empty, output CURRENT_GRAPH_PAYLOAD unchanged.",
|
|
1858
|
+
"Output schema:",
|
|
1859
|
+
"{\"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}}",
|
|
1860
|
+
"",
|
|
1861
|
+
"[CURRENT_GRAPH_PAYLOAD]",
|
|
1862
|
+
JSON.stringify(args.graphPayload),
|
|
1863
|
+
"[/CURRENT_GRAPH_PAYLOAD]",
|
|
497
1864
|
"",
|
|
498
|
-
|
|
1865
|
+
"[SOURCE_SNIPPET]",
|
|
1866
|
+
sourceSnippet,
|
|
1867
|
+
"[/SOURCE_SNIPPET]",
|
|
499
1868
|
].join("\n"),
|
|
500
1869
|
},
|
|
501
1870
|
],
|
|
502
1871
|
};
|
|
503
1872
|
let lastError = null;
|
|
504
|
-
for (let attempt = 0; attempt <
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
1873
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
1874
|
+
let response;
|
|
1875
|
+
try {
|
|
1876
|
+
response = await (0, http_post_1.postJsonWithTimeout)({
|
|
1877
|
+
endpoint,
|
|
1878
|
+
apiKey: args.llm.apiKey,
|
|
1879
|
+
body,
|
|
1880
|
+
timeoutMs: 25000,
|
|
1881
|
+
});
|
|
1882
|
+
}
|
|
1883
|
+
catch (error) {
|
|
1884
|
+
lastError = error;
|
|
1885
|
+
continue;
|
|
1886
|
+
}
|
|
511
1887
|
if (!response.ok) {
|
|
512
|
-
lastError = new Error(response.status > 0 ? `
|
|
1888
|
+
lastError = new Error(response.status > 0 ? `graph_rewrite_http_${response.status}` : (response.error || "graph_rewrite_network_error"));
|
|
513
1889
|
continue;
|
|
514
1890
|
}
|
|
515
1891
|
try {
|
|
516
1892
|
const json = (response.json || {});
|
|
517
1893
|
const content = json?.choices?.[0]?.message?.content || "";
|
|
518
1894
|
if (!content.trim()) {
|
|
519
|
-
lastError = new Error("
|
|
1895
|
+
lastError = new Error("graph_rewrite_empty");
|
|
520
1896
|
continue;
|
|
521
1897
|
}
|
|
522
|
-
|
|
1898
|
+
const validation = (0, llm_output_validator_1.validateLlmJsonOutput)(content);
|
|
1899
|
+
if (!validation.valid) {
|
|
1900
|
+
lastError = new Error(`graph_rewrite_invalid_json:${validation.errors.join("|")}`);
|
|
1901
|
+
continue;
|
|
1902
|
+
}
|
|
1903
|
+
const rootObj = asRecord(validation.data);
|
|
1904
|
+
const candidatePayload = rootObj
|
|
1905
|
+
? (asRecord(rootObj.graph_payload) || asRecord(rootObj.graph) || rootObj)
|
|
1906
|
+
: null;
|
|
1907
|
+
const rewritten = toAppendableGraphPayload(parseGraphPayload(candidatePayload || undefined));
|
|
1908
|
+
if (rewritten) {
|
|
1909
|
+
const rewriteValidation = validateGraphRewriteResult({
|
|
1910
|
+
basePayload: args.graphPayload,
|
|
1911
|
+
rewrittenPayload: rewritten,
|
|
1912
|
+
rewriteScope: args.rewritePlan?.rewrite_scope,
|
|
1913
|
+
schema: args.schema,
|
|
1914
|
+
});
|
|
1915
|
+
if (!rewriteValidation.valid) {
|
|
1916
|
+
lastError = new Error(`graph_rewrite_invalid_payload:${rewriteValidation.errors.join("|")}`);
|
|
1917
|
+
continue;
|
|
1918
|
+
}
|
|
1919
|
+
return rewritten;
|
|
1920
|
+
}
|
|
1921
|
+
lastError = new Error("graph_rewrite_invalid_payload");
|
|
523
1922
|
}
|
|
524
1923
|
catch (error) {
|
|
525
1924
|
lastError = error;
|
|
526
1925
|
}
|
|
527
1926
|
}
|
|
528
|
-
args.logger.warn(`
|
|
529
|
-
return
|
|
1927
|
+
args.logger.warn(`graph_rewrite_failed reason=${String(lastError || "unknown")}`);
|
|
1928
|
+
return undefined;
|
|
530
1929
|
}
|
|
531
1930
|
function createSessionSync(options) {
|
|
532
1931
|
const memoryRoot = options.dbPath ? path.resolve(options.dbPath) : path.join(options.projectRoot, "data", "memory");
|
|
@@ -536,23 +1935,27 @@ function createSessionSync(options) {
|
|
|
536
1935
|
const llmApiKey = options.llm?.apiKey || "";
|
|
537
1936
|
const llmBaseUrl = normalizeBaseUrl(options.llm?.baseURL || options.llm?.baseUrl);
|
|
538
1937
|
const requireLlmForWrite = options.requireLlmForWrite !== false;
|
|
1938
|
+
const includeLocalActiveInput = options.syncPolicy?.includeLocalActiveInput === true;
|
|
1939
|
+
const graphSchema = (0, ontology_1.loadGraphSchema)(options.projectRoot);
|
|
1940
|
+
const relationPromptHint = (0, ontology_1.buildRelationPromptHint)(graphSchema);
|
|
1941
|
+
const activeValuePromptHint = buildActiveValuePromptHint(graphSchema);
|
|
1942
|
+
const entityDictionaryPromptHint = buildEntityDictionaryPromptHint(graphSchema);
|
|
539
1943
|
options.logger.info(`sync_gate_prompt_version=${WRITE_GATE_PROMPT_VERSION}`);
|
|
1944
|
+
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}`);
|
|
1945
|
+
options.logger.info(`sync_include_local_active_input=${includeLocalActiveInput}`);
|
|
540
1946
|
if (!fs.existsSync(statePath)) {
|
|
541
1947
|
options.logger.warn("sync_state_missing: deleting state file triggers full re-import");
|
|
542
1948
|
}
|
|
543
1949
|
async function storeFromTranscript(args) {
|
|
544
1950
|
const skipReasons = {};
|
|
545
|
-
const activeTextMaxChars =
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
const archiveSourceTextMaxChars = typeof options.writePolicy?.archiveSourceTextMaxChars === "number"
|
|
549
|
-
? Math.max(1000, Math.min(30000, Math.floor(options.writePolicy.archiveSourceTextMaxChars)))
|
|
550
|
-
: 8000;
|
|
1951
|
+
const activeTextMaxChars = resolveWriteCharLimit(options.writePolicy?.activeTextMaxChars, 500, 200000);
|
|
1952
|
+
const archiveSourceTextMaxChars = resolveWriteCharLimit(options.writePolicy?.archiveSourceTextMaxChars, 1000, 500000);
|
|
1953
|
+
const normalizedTranscript = denoiseTranscriptForWrite(args.transcript);
|
|
551
1954
|
function bumpReason(reason) {
|
|
552
1955
|
const key = reason || "unknown";
|
|
553
1956
|
skipReasons[key] = (skipReasons[key] || 0) + 1;
|
|
554
1957
|
}
|
|
555
|
-
if (!
|
|
1958
|
+
if (!normalizedTranscript.trim()) {
|
|
556
1959
|
options.logger.info(`sync_skip reason=no_active_records session=${args.sessionId}`);
|
|
557
1960
|
bumpReason("no_active_records");
|
|
558
1961
|
return { imported: 0, skipped: 1, ok: true, llmDecisions: 0, activeOnly: 0, archiveEvent: 0, skipReasons };
|
|
@@ -564,12 +1967,20 @@ function createSessionSync(options) {
|
|
|
564
1967
|
return { imported: 0, skipped: 1, ok: false, llmDecisions: 0, activeOnly: 0, archiveEvent: 0, skipReasons };
|
|
565
1968
|
}
|
|
566
1969
|
options.logger.warn(`Sync gate degraded to active_only for ${args.sessionId}: llm_not_configured`);
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
1970
|
+
let fallbackWrite;
|
|
1971
|
+
try {
|
|
1972
|
+
fallbackWrite = await options.writeStore.writeMemory({
|
|
1973
|
+
text: tailByCharLimit(normalizedTranscript, activeTextMaxChars),
|
|
1974
|
+
role: "system",
|
|
1975
|
+
source: `sync_gate_fallback:${args.sourceFile}`,
|
|
1976
|
+
sessionId: args.sessionId,
|
|
1977
|
+
});
|
|
1978
|
+
}
|
|
1979
|
+
catch (error) {
|
|
1980
|
+
options.logger.warn(`sync_skip reason=active_only_fallback_exception session=${args.sessionId} error=${String(error)}`);
|
|
1981
|
+
bumpReason("active_only_fallback_exception");
|
|
1982
|
+
return { imported: 0, skipped: 1, ok: false, llmDecisions: 1, activeOnly: 0, archiveEvent: 0, skipReasons };
|
|
1983
|
+
}
|
|
573
1984
|
if (fallbackWrite.status === "ok") {
|
|
574
1985
|
return { imported: 1, skipped: 0, ok: true, llmDecisions: 1, activeOnly: 1, archiveEvent: 0, skipReasons };
|
|
575
1986
|
}
|
|
@@ -578,10 +1989,13 @@ function createSessionSync(options) {
|
|
|
578
1989
|
}
|
|
579
1990
|
const decisions = await extractGateDecisionsWithLlm({
|
|
580
1991
|
llm: { model: llmModel, apiKey: llmApiKey, baseUrl: llmBaseUrl },
|
|
581
|
-
transcript:
|
|
1992
|
+
transcript: normalizedTranscript,
|
|
582
1993
|
logger: options.logger,
|
|
1994
|
+
schema: graphSchema,
|
|
1995
|
+
relationPromptHint,
|
|
583
1996
|
});
|
|
584
|
-
|
|
1997
|
+
const routedDecisions = appendLifecycleArchiveDecision(decisions, normalizedTranscript, options.logger);
|
|
1998
|
+
if (routedDecisions.length === 0) {
|
|
585
1999
|
options.logger.info(`sync_skip reason=llm_extract_empty session=${args.sessionId}`);
|
|
586
2000
|
bumpReason("llm_extract_empty");
|
|
587
2001
|
return { imported: 0, skipped: 1, ok: true, llmDecisions: 0, activeOnly: 0, archiveEvent: 0, skipReasons };
|
|
@@ -596,58 +2010,273 @@ function createSessionSync(options) {
|
|
|
596
2010
|
let graphAttempted = 0;
|
|
597
2011
|
let graphStored = 0;
|
|
598
2012
|
let graphSkipped = 0;
|
|
2013
|
+
let graphRewriteRequested = 0;
|
|
2014
|
+
let graphRewriteTriggered = 0;
|
|
2015
|
+
let graphRewriteApplied = 0;
|
|
2016
|
+
let graphRewriteFailed = 0;
|
|
599
2017
|
const archiveInputs = [];
|
|
600
|
-
|
|
2018
|
+
function summaryMentionsAllEntities(summary, entities) {
|
|
2019
|
+
const normalizedSummary = summary.toLowerCase();
|
|
2020
|
+
return entities.every(entity => normalizedSummary.includes(entity.toLowerCase()));
|
|
2021
|
+
}
|
|
2022
|
+
function buildAppendSummary(payload) {
|
|
2023
|
+
const entities = Array.isArray(payload.entities) ? payload.entities : [];
|
|
2024
|
+
const relations = Array.isArray(payload.relations) ? payload.relations : [];
|
|
2025
|
+
const current = typeof payload.summary === "string" ? payload.summary.trim() : "";
|
|
2026
|
+
const relationText = relations
|
|
2027
|
+
.slice(0, 3)
|
|
2028
|
+
.map(item => `${item.source} ${item.type} ${item.target}`)
|
|
2029
|
+
.join("; ");
|
|
2030
|
+
const base = current || (relationText
|
|
2031
|
+
? `Graph extraction captured key relations: ${relationText}.`
|
|
2032
|
+
: "Graph extraction captured key entities and relations.");
|
|
2033
|
+
if (entities.length === 0 || summaryMentionsAllEntities(base, entities)) {
|
|
2034
|
+
return base;
|
|
2035
|
+
}
|
|
2036
|
+
return `${base} Entities: ${entities.join(", ")}.`;
|
|
2037
|
+
}
|
|
2038
|
+
function buildAppendSourceTextNav(argsForAppend) {
|
|
2039
|
+
const nav = argsForAppend.graphPayload?.source_text_nav;
|
|
2040
|
+
const forceArchiveTrace = argsForAppend.sourceLayer === "archive_event" && !!argsForAppend.archiveEventId;
|
|
2041
|
+
const sourceEventId = forceArchiveTrace
|
|
2042
|
+
? (argsForAppend.archiveEventId || argsForAppend.sourceEventId)
|
|
2043
|
+
: ((nav?.source_event_id || "").trim() || argsForAppend.sourceEventId);
|
|
2044
|
+
const sourceMemoryId = forceArchiveTrace
|
|
2045
|
+
? (argsForAppend.archiveEventId || argsForAppend.sourceEventId)
|
|
2046
|
+
: ((nav?.source_memory_id || "").trim() || argsForAppend.archiveEventId || argsForAppend.sourceEventId);
|
|
2047
|
+
return {
|
|
2048
|
+
layer: forceArchiveTrace
|
|
2049
|
+
? "archive_event"
|
|
2050
|
+
: (nav?.layer === "archive_event" || nav?.layer === "active_only"
|
|
2051
|
+
? nav.layer
|
|
2052
|
+
: argsForAppend.sourceLayer),
|
|
2053
|
+
session_id: (nav?.session_id || "").trim() || args.sessionId,
|
|
2054
|
+
source_file: (nav?.source_file || "").trim() || args.sourceFile || "unknown",
|
|
2055
|
+
source_memory_id: sourceMemoryId,
|
|
2056
|
+
source_event_id: sourceEventId,
|
|
2057
|
+
fulltext_anchor: (nav?.fulltext_anchor || "").trim() || undefined,
|
|
2058
|
+
};
|
|
2059
|
+
}
|
|
2060
|
+
async function appendGraphPayload(argsForAppend) {
|
|
2061
|
+
if (!options.graphMemoryStore) {
|
|
2062
|
+
return;
|
|
2063
|
+
}
|
|
2064
|
+
const appendableGraph = toAppendableGraphPayload(argsForAppend.graphPayload);
|
|
2065
|
+
if (!appendableGraph) {
|
|
2066
|
+
return;
|
|
2067
|
+
}
|
|
2068
|
+
const summary = buildAppendSummary(appendableGraph);
|
|
2069
|
+
const sourceTextNav = buildAppendSourceTextNav({
|
|
2070
|
+
sourceEventId: argsForAppend.sourceEventId,
|
|
2071
|
+
sourceLayer: argsForAppend.sourceLayer,
|
|
2072
|
+
archiveEventId: argsForAppend.archiveEventId,
|
|
2073
|
+
graphPayload: appendableGraph,
|
|
2074
|
+
});
|
|
2075
|
+
graphAttempted += 1;
|
|
2076
|
+
let graphResult;
|
|
2077
|
+
try {
|
|
2078
|
+
graphResult = await options.graphMemoryStore.append({
|
|
2079
|
+
sourceEventId: argsForAppend.sourceEventId,
|
|
2080
|
+
sourceLayer: argsForAppend.sourceLayer,
|
|
2081
|
+
archiveEventId: argsForAppend.archiveEventId,
|
|
2082
|
+
sessionId: args.sessionId,
|
|
2083
|
+
sourceFile: args.sourceFile,
|
|
2084
|
+
source_text_nav: sourceTextNav,
|
|
2085
|
+
summary,
|
|
2086
|
+
eventType: argsForAppend.eventType || "insight",
|
|
2087
|
+
entities: appendableGraph.entities,
|
|
2088
|
+
entity_types: appendableGraph.entity_types,
|
|
2089
|
+
relations: appendableGraph.relations,
|
|
2090
|
+
gateSource: "sync",
|
|
2091
|
+
confidence: appendableGraph.confidence,
|
|
2092
|
+
sourceText: argsForAppend.sourceText,
|
|
2093
|
+
});
|
|
2094
|
+
}
|
|
2095
|
+
catch (error) {
|
|
2096
|
+
graphSkipped += 1;
|
|
2097
|
+
options.logger.warn(`graph_append_exception source_event_id=${argsForAppend.sourceEventId} error=${String(error)}`);
|
|
2098
|
+
return;
|
|
2099
|
+
}
|
|
2100
|
+
if (!graphResult.success) {
|
|
2101
|
+
graphSkipped += 1;
|
|
2102
|
+
options.logger.info(`graph_skip_reason=${graphResult.reason} source_event_id=${argsForAppend.sourceEventId}`);
|
|
2103
|
+
return;
|
|
2104
|
+
}
|
|
2105
|
+
graphStored += 1;
|
|
2106
|
+
}
|
|
2107
|
+
function buildGraphOnlySourceEventId(argsForGraphOnly) {
|
|
2108
|
+
if (argsForGraphOnly.candidateId) {
|
|
2109
|
+
return `candidate:${args.sessionId}:${argsForGraphOnly.candidateId}`;
|
|
2110
|
+
}
|
|
2111
|
+
const relationFingerprint = (argsForGraphOnly.graphPayload?.relations || [])
|
|
2112
|
+
.map(rel => `${rel.source}|${rel.type}|${rel.target}|${rel.evidence_span || ""}`)
|
|
2113
|
+
.sort()
|
|
2114
|
+
.join("||");
|
|
2115
|
+
return `graph_only:${args.sessionId}:${crypto.createHash("sha1").update(relationFingerprint || argsForGraphOnly.fallbackText).digest("hex").slice(0, 16)}`;
|
|
2116
|
+
}
|
|
2117
|
+
for (const decision of routedDecisions) {
|
|
601
2118
|
llmDecisions += 1;
|
|
2119
|
+
const fallbackGraphPayload = !decision.graph
|
|
2120
|
+
? buildStablePersonalFactGraph(decision.active_text || decision.candidate_text || normalizedTranscript)
|
|
2121
|
+
: null;
|
|
2122
|
+
let graphPayload = mergeGraphPayload(toAppendableGraphPayload(decision.graph), toAppendableGraphPayload(fallbackGraphPayload || undefined));
|
|
2123
|
+
const mergeReviewed = applyMergeHintToGraphPayload({
|
|
2124
|
+
graphPayload,
|
|
2125
|
+
mergeHint: decision.merge_hint,
|
|
2126
|
+
});
|
|
2127
|
+
graphPayload = mergeReviewed.graphPayload;
|
|
2128
|
+
for (const warning of mergeReviewed.warnings) {
|
|
2129
|
+
bumpReason(warning);
|
|
2130
|
+
options.logger.warn(`graph_merge_hint_warning reason=${warning} session=${args.sessionId} candidate_id=${decision.candidate_id || "unknown"}`);
|
|
2131
|
+
}
|
|
2132
|
+
if (decision.graph_rewrite?.rewrite_required) {
|
|
2133
|
+
graphRewriteRequested += 1;
|
|
2134
|
+
graphRewriteTriggered += 1;
|
|
2135
|
+
let rewriteApplied = false;
|
|
2136
|
+
const mergedRewritePayload = mergeGraphPayload(graphPayload, decision.graph_rewrite.graph_rewrite_payload);
|
|
2137
|
+
if (mergedRewritePayload) {
|
|
2138
|
+
const plannerRewriteValidation = validateGraphRewriteResult({
|
|
2139
|
+
basePayload: graphPayload,
|
|
2140
|
+
rewrittenPayload: mergedRewritePayload,
|
|
2141
|
+
rewriteScope: decision.graph_rewrite.rewrite_scope,
|
|
2142
|
+
schema: graphSchema,
|
|
2143
|
+
});
|
|
2144
|
+
if (plannerRewriteValidation.valid) {
|
|
2145
|
+
graphPayload = mergedRewritePayload;
|
|
2146
|
+
graphRewriteApplied += 1;
|
|
2147
|
+
rewriteApplied = true;
|
|
2148
|
+
}
|
|
2149
|
+
else {
|
|
2150
|
+
bumpReason("graph_rewrite_invalid_payload");
|
|
2151
|
+
options.logger.warn(`graph_rewrite_invalid source=planner session=${args.sessionId} candidate_id=${decision.candidate_id || "unknown"} errors=${plannerRewriteValidation.errors.join("|")}`);
|
|
2152
|
+
}
|
|
2153
|
+
}
|
|
2154
|
+
if (!rewriteApplied && graphPayload) {
|
|
2155
|
+
const rewritten = await rewriteGraphPayloadWithLlm({
|
|
2156
|
+
llm: { model: llmModel, apiKey: llmApiKey, baseUrl: llmBaseUrl },
|
|
2157
|
+
transcript: normalizedTranscript,
|
|
2158
|
+
candidateText: decision.candidate_text || decision.active_text,
|
|
2159
|
+
graphPayload,
|
|
2160
|
+
rewritePlan: decision.graph_rewrite,
|
|
2161
|
+
mergeHint: decision.merge_hint,
|
|
2162
|
+
schema: graphSchema,
|
|
2163
|
+
relationPromptHint,
|
|
2164
|
+
activeValuePromptHint,
|
|
2165
|
+
entityDictionaryPromptHint,
|
|
2166
|
+
logger: options.logger,
|
|
2167
|
+
});
|
|
2168
|
+
if (rewritten) {
|
|
2169
|
+
graphPayload = rewritten;
|
|
2170
|
+
graphRewriteApplied += 1;
|
|
2171
|
+
rewriteApplied = true;
|
|
2172
|
+
}
|
|
2173
|
+
else {
|
|
2174
|
+
graphRewriteFailed += 1;
|
|
2175
|
+
}
|
|
2176
|
+
}
|
|
2177
|
+
if (!rewriteApplied && !graphPayload) {
|
|
2178
|
+
graphRewriteFailed += 1;
|
|
2179
|
+
}
|
|
2180
|
+
}
|
|
602
2181
|
if (decision.target_layer === "skip") {
|
|
2182
|
+
if (graphPayload) {
|
|
2183
|
+
const graphOnlySourceEventId = buildGraphOnlySourceEventId({
|
|
2184
|
+
candidateId: decision.candidate_id,
|
|
2185
|
+
graphPayload,
|
|
2186
|
+
fallbackText: normalizedTranscript,
|
|
2187
|
+
});
|
|
2188
|
+
await appendGraphPayload({
|
|
2189
|
+
sourceEventId: graphOnlySourceEventId,
|
|
2190
|
+
sourceLayer: "active_only",
|
|
2191
|
+
eventType: "insight",
|
|
2192
|
+
sourceText: normalizedTranscript,
|
|
2193
|
+
graphPayload,
|
|
2194
|
+
});
|
|
2195
|
+
}
|
|
603
2196
|
skipped += 1;
|
|
604
2197
|
bumpReason(decision.reason || "llm_gate_skip");
|
|
605
2198
|
continue;
|
|
606
2199
|
}
|
|
607
2200
|
if (decision.target_layer === "active_only") {
|
|
608
2201
|
activeAttempted += 1;
|
|
609
|
-
const
|
|
2202
|
+
const activeCandidateText = denoiseTranscriptForWrite(decision.active_text || normalizedTranscript);
|
|
2203
|
+
const activeText = tailByCharLimit(activeCandidateText, activeTextMaxChars);
|
|
610
2204
|
if (!activeText) {
|
|
2205
|
+
if (graphPayload) {
|
|
2206
|
+
const graphOnlySourceEventId = buildGraphOnlySourceEventId({
|
|
2207
|
+
candidateId: decision.candidate_id,
|
|
2208
|
+
graphPayload,
|
|
2209
|
+
fallbackText: normalizedTranscript,
|
|
2210
|
+
});
|
|
2211
|
+
await appendGraphPayload({
|
|
2212
|
+
sourceEventId: graphOnlySourceEventId,
|
|
2213
|
+
sourceLayer: "active_only",
|
|
2214
|
+
eventType: "insight",
|
|
2215
|
+
sourceText: normalizedTranscript,
|
|
2216
|
+
graphPayload,
|
|
2217
|
+
});
|
|
2218
|
+
}
|
|
611
2219
|
skipped += 1;
|
|
612
2220
|
bumpReason("active_only_empty");
|
|
613
2221
|
continue;
|
|
614
2222
|
}
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
2223
|
+
if (!hasValuableActiveContent(activeText)) {
|
|
2224
|
+
if (graphPayload) {
|
|
2225
|
+
const graphOnlySourceEventId = buildGraphOnlySourceEventId({
|
|
2226
|
+
candidateId: decision.candidate_id,
|
|
2227
|
+
graphPayload,
|
|
2228
|
+
fallbackText: activeText,
|
|
2229
|
+
});
|
|
2230
|
+
await appendGraphPayload({
|
|
2231
|
+
sourceEventId: graphOnlySourceEventId,
|
|
2232
|
+
sourceLayer: "active_only",
|
|
2233
|
+
eventType: "insight",
|
|
2234
|
+
sourceText: activeText,
|
|
2235
|
+
graphPayload,
|
|
2236
|
+
});
|
|
2237
|
+
}
|
|
2238
|
+
skipped += 1;
|
|
2239
|
+
bumpReason("active_only_low_value");
|
|
2240
|
+
options.logger.info(`sync_skip reason=active_only_low_value session=${args.sessionId} candidate_id=${decision.candidate_id || "unknown"}`);
|
|
2241
|
+
continue;
|
|
2242
|
+
}
|
|
2243
|
+
if (!graphPayload) {
|
|
2244
|
+
skipped += 1;
|
|
2245
|
+
bumpReason("graph_payload_required_active");
|
|
2246
|
+
options.logger.warn(`sync_skip reason=graph_payload_required_active session=${args.sessionId} candidate_id=${decision.candidate_id || "unknown"}`);
|
|
2247
|
+
continue;
|
|
2248
|
+
}
|
|
2249
|
+
let writeResult;
|
|
2250
|
+
try {
|
|
2251
|
+
writeResult = await options.writeStore.writeMemory({
|
|
2252
|
+
text: activeText,
|
|
2253
|
+
role: "system",
|
|
2254
|
+
source: `sync_gate_active:${args.sourceFile}`,
|
|
2255
|
+
sessionId: args.sessionId,
|
|
2256
|
+
});
|
|
2257
|
+
}
|
|
2258
|
+
catch (error) {
|
|
2259
|
+
skipped += 1;
|
|
2260
|
+
bumpReason("active_only_write_exception");
|
|
2261
|
+
options.logger.warn(`sync_skip reason=active_only_write_exception session=${args.sessionId} candidate_id=${decision.candidate_id || "unknown"} error=${String(error)}`);
|
|
2262
|
+
continue;
|
|
2263
|
+
}
|
|
621
2264
|
if (writeResult.status === "ok") {
|
|
622
2265
|
imported += 1;
|
|
623
2266
|
activeOnly += 1;
|
|
624
|
-
if (
|
|
625
|
-
|
|
626
|
-
const relationFingerprint = (decision.graph.relations || [])
|
|
2267
|
+
if (graphPayload) {
|
|
2268
|
+
const relationFingerprint = (graphPayload.relations || [])
|
|
627
2269
|
.map(rel => `${rel.source}|${rel.type}|${rel.target}|${rel.evidence_span || ""}`)
|
|
628
2270
|
.sort()
|
|
629
2271
|
.join("||");
|
|
630
2272
|
const activeSourceEventId = `active:${args.sessionId}:${crypto.createHash("sha1").update(relationFingerprint || activeText).digest("hex").slice(0, 16)}`;
|
|
631
|
-
|
|
2273
|
+
await appendGraphPayload({
|
|
632
2274
|
sourceEventId: activeSourceEventId,
|
|
633
2275
|
sourceLayer: "active_only",
|
|
634
|
-
sessionId: args.sessionId,
|
|
635
|
-
sourceFile: args.sourceFile,
|
|
636
2276
|
eventType: "insight",
|
|
637
|
-
entities: decision.graph.entities,
|
|
638
|
-
entity_types: decision.graph.entity_types,
|
|
639
|
-
relations: decision.graph.relations,
|
|
640
|
-
gateSource: "sync",
|
|
641
|
-
confidence: decision.graph.confidence,
|
|
642
2277
|
sourceText: activeText,
|
|
2278
|
+
graphPayload,
|
|
643
2279
|
});
|
|
644
|
-
if (!graphResult.success) {
|
|
645
|
-
graphSkipped += 1;
|
|
646
|
-
options.logger.info(`graph_skip_reason=${graphResult.reason} source_event_id=${activeSourceEventId}`);
|
|
647
|
-
}
|
|
648
|
-
else {
|
|
649
|
-
graphStored += 1;
|
|
650
|
-
}
|
|
651
2280
|
}
|
|
652
2281
|
}
|
|
653
2282
|
else {
|
|
@@ -663,9 +2292,29 @@ function createSessionSync(options) {
|
|
|
663
2292
|
bumpReason("archive_event_missing_payload");
|
|
664
2293
|
continue;
|
|
665
2294
|
}
|
|
2295
|
+
const archiveFallbackGraph = toAppendableGraphPayload({
|
|
2296
|
+
summary: decision.event.summary,
|
|
2297
|
+
entities: decision.event.entities,
|
|
2298
|
+
entity_types: decision.event.entity_types,
|
|
2299
|
+
relations: decision.event.relations,
|
|
2300
|
+
confidence: decision.event.confidence,
|
|
2301
|
+
});
|
|
2302
|
+
graphPayload = mergeGraphPayload(graphPayload, archiveFallbackGraph) || graphPayload || archiveFallbackGraph;
|
|
2303
|
+
if (!graphPayload) {
|
|
2304
|
+
skipped += 1;
|
|
2305
|
+
bumpReason("graph_payload_required_archive");
|
|
2306
|
+
options.logger.warn(`sync_skip reason=graph_payload_required_archive session=${args.sessionId} candidate_id=${decision.candidate_id || "unknown"}`);
|
|
2307
|
+
continue;
|
|
2308
|
+
}
|
|
2309
|
+
const archiveSourceSlice = tailByCharLimit(denoiseTranscriptForWrite(decision.candidate_text || normalizedTranscript), archiveSourceTextMaxChars);
|
|
666
2310
|
archiveInputs.push({
|
|
2311
|
+
candidate_id: decision.candidate_id,
|
|
2312
|
+
graph_payload: graphPayload,
|
|
667
2313
|
event_type: decision.event.event_type,
|
|
668
2314
|
summary: decision.event.summary,
|
|
2315
|
+
cause: decision.event.cause,
|
|
2316
|
+
process: decision.event.process,
|
|
2317
|
+
result: decision.event.result,
|
|
669
2318
|
entities: decision.event.entities,
|
|
670
2319
|
relations: decision.event.relations,
|
|
671
2320
|
entity_types: decision.event.entity_types,
|
|
@@ -673,7 +2322,7 @@ function createSessionSync(options) {
|
|
|
673
2322
|
confidence: decision.event.confidence,
|
|
674
2323
|
session_id: args.sessionId,
|
|
675
2324
|
source_file: args.sourceFile,
|
|
676
|
-
source_text:
|
|
2325
|
+
source_text: archiveSourceSlice || tailByCharLimit(normalizedTranscript, archiveSourceTextMaxChars),
|
|
677
2326
|
source_event_id: decision.candidate_id
|
|
678
2327
|
? `candidate:${args.sessionId}:${decision.candidate_id}`
|
|
679
2328
|
: `candidate:${args.sessionId}:${crypto.createHash("sha1").update(decision.event.summary).digest("hex").slice(0, 16)}`,
|
|
@@ -685,7 +2334,17 @@ function createSessionSync(options) {
|
|
|
685
2334
|
let archivedSuccess = 0;
|
|
686
2335
|
let archivedSkipped = 0;
|
|
687
2336
|
for (const inputRecord of archiveInputs) {
|
|
688
|
-
|
|
2337
|
+
let archiveResult;
|
|
2338
|
+
try {
|
|
2339
|
+
archiveResult = await options.archiveStore.storeEvents([inputRecord]);
|
|
2340
|
+
}
|
|
2341
|
+
catch (error) {
|
|
2342
|
+
archivedSkipped += 1;
|
|
2343
|
+
skipped += 1;
|
|
2344
|
+
bumpReason("archive_store_exception");
|
|
2345
|
+
options.logger.warn(`sync_skip reason=archive_store_exception session=${args.sessionId} candidate_id=${inputRecord.candidate_id || "unknown"} error=${String(error)}`);
|
|
2346
|
+
continue;
|
|
2347
|
+
}
|
|
689
2348
|
imported += archiveResult.stored.length;
|
|
690
2349
|
skipped += archiveResult.skipped.length;
|
|
691
2350
|
archiveEvent += archiveResult.stored.length;
|
|
@@ -698,37 +2357,27 @@ function createSessionSync(options) {
|
|
|
698
2357
|
if (!archiveRecord) {
|
|
699
2358
|
continue;
|
|
700
2359
|
}
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
2360
|
+
const archiveGraphPayload = inputRecord.graph_payload
|
|
2361
|
+
|| toAppendableGraphPayload({
|
|
2362
|
+
entities: inputRecord.entities,
|
|
2363
|
+
entity_types: inputRecord.entity_types,
|
|
2364
|
+
relations: inputRecord.relations,
|
|
2365
|
+
confidence: inputRecord.confidence,
|
|
2366
|
+
});
|
|
2367
|
+
await appendGraphPayload({
|
|
706
2368
|
// Graph trace points to persisted archive record id for stable lookup.
|
|
707
2369
|
sourceEventId: archiveRecord.id,
|
|
708
2370
|
sourceLayer: "archive_event",
|
|
709
2371
|
archiveEventId: archiveRecord.id,
|
|
710
|
-
sessionId: args.sessionId,
|
|
711
|
-
sourceFile: args.sourceFile,
|
|
712
2372
|
eventType: inputRecord.event_type,
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
relations: inputRecord.relations,
|
|
716
|
-
gateSource: "sync",
|
|
717
|
-
confidence: inputRecord.confidence,
|
|
718
|
-
sourceText: args.transcript,
|
|
2373
|
+
sourceText: inputRecord.source_text || normalizedTranscript,
|
|
2374
|
+
graphPayload: archiveGraphPayload,
|
|
719
2375
|
});
|
|
720
|
-
if (!graphResult.success) {
|
|
721
|
-
graphSkipped += 1;
|
|
722
|
-
options.logger.info(`graph_skip_reason=${graphResult.reason} source_event_id=${archiveRecord.id}`);
|
|
723
|
-
}
|
|
724
|
-
else {
|
|
725
|
-
graphStored += 1;
|
|
726
|
-
}
|
|
727
2376
|
}
|
|
728
2377
|
options.logger.info(`sync_archive_result session=${args.sessionId} archived_success=${archivedSuccess} skipped=${archivedSkipped}`);
|
|
729
2378
|
}
|
|
730
2379
|
options.logger.info(`sync_gate_result session=${args.sessionId} llm_decisions=${llmDecisions} active_only=${activeOnly} archive_event=${archiveEvent} skipped=${skipped}`);
|
|
731
|
-
options.logger.info(`sync_gate_metrics session=${args.sessionId} active_attempted=${activeAttempted} archive_attempted=${archiveAttempted} graph_attempted=${graphAttempted} graph_stored=${graphStored} graph_skipped=${graphSkipped} skip_reason_kinds=${Object.keys(skipReasons).length}`);
|
|
2380
|
+
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}`);
|
|
732
2381
|
return {
|
|
733
2382
|
imported,
|
|
734
2383
|
skipped,
|
|
@@ -795,7 +2444,7 @@ function createSessionSync(options) {
|
|
|
795
2444
|
return { imported, skipped, filesProcessed, llmDecisions, activeOnly, archiveEvent, skipReasons };
|
|
796
2445
|
}
|
|
797
2446
|
async function syncMemory() {
|
|
798
|
-
const files = gatherSessionFiles(openclawBasePath, memoryRoot);
|
|
2447
|
+
const files = gatherSessionFiles(openclawBasePath, memoryRoot, includeLocalActiveInput);
|
|
799
2448
|
if (files.length === 0) {
|
|
800
2449
|
options.logger.info("sync_skip reason=no_active_records");
|
|
801
2450
|
}
|