openclaw-scheduler 0.2.5 → 0.2.7
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/dispatch/README.md +16 -2
- package/dispatch/completion.mjs +297 -20
- package/dispatch/index.mjs +80 -57
- package/dispatch/liveness.mjs +61 -0
- package/dispatch/watcher.mjs +299 -17
- package/dispatcher-strategies.js +82 -10
- package/dispatcher.js +6 -1
- package/gateway.js +39 -0
- package/package.json +2 -1
package/dispatch/README.md
CHANGED
|
@@ -291,8 +291,8 @@ No manual token configuration needed on a standard OpenClaw install.
|
|
|
291
291
|
|
|
292
292
|
When `--deliver-to` is set, dispatch registers a **scheduler watcher job**
|
|
293
293
|
after dispatching the session. The watcher polls the session result every
|
|
294
|
-
minute until the agent
|
|
295
|
-
`handleDelivery` pipeline.
|
|
294
|
+
minute until the agent sends the structured `done` completion signal, then
|
|
295
|
+
delivers via the scheduler's `handleDelivery` pipeline.
|
|
296
296
|
|
|
297
297
|
```
|
|
298
298
|
dispatch enqueue --deliver-to <telegram-user-id>
|
|
@@ -316,6 +316,20 @@ dispatch enqueue --deliver-to <telegram-user-id>
|
|
|
316
316
|
Exit 1 with no output = retry on next cron tick (no spam — `announce-always`
|
|
317
317
|
only delivers when `output.trim()` is truthy).
|
|
318
318
|
|
|
319
|
+
Quiet sessions are treated conservatively. The watcher does not mark a running
|
|
320
|
+
job failed just because `sessions.json` or the JSONL transcript has been quiet
|
|
321
|
+
for 60 seconds. For high/xhigh reasoning work, the first idle result probe waits
|
|
322
|
+
at least 10 minutes, idle auto-resolution waits at least 20 minutes, and the hard
|
|
323
|
+
failure ceiling is longer than the requested task timeout. Missing or ambiguous
|
|
324
|
+
gateway/session liveness fails open to "still monitoring" until the hard timeout
|
|
325
|
+
window or a clear terminal error.
|
|
326
|
+
|
|
327
|
+
While a label is still `running`, a plain assistant reply is diagnostic only.
|
|
328
|
+
Successful final delivery requires the agent-side `done` signal and its
|
|
329
|
+
structured completion payload. If an older watcher records an error and the
|
|
330
|
+
worker later sends a valid `done`, the later completion is authoritative and the
|
|
331
|
+
stale error is cleared from the label.
|
|
332
|
+
|
|
319
333
|
### Progress check-ins from subagent sessions
|
|
320
334
|
|
|
321
335
|
Subagent sessions run without PATH access to the `openclaw` CLI, so
|
package/dispatch/completion.mjs
CHANGED
|
@@ -34,6 +34,13 @@ const TEST_FRAGMENT_RE = /\b(?:test|tests|spec|coverage|lint|typecheck|tsc|eslin
|
|
|
34
34
|
const TEST_APPLICABILITY_RE = /\b(?:test|tests|pytest|jest|vitest|mocha|cypress|playwright|npm\s+test|pnpm\s+test|yarn\s+test|cargo\s+test|go\s+test|rspec)\b/i;
|
|
35
35
|
const TEST_NEGATION_RE = /\b(?:do\s+not|don't|dont|never|skip|without|no)\s+(?:run\s+)?(?:the\s+)?tests?\b/i;
|
|
36
36
|
const PUSH_FORBIDDEN_RE = /\b(?:do\s+not|don't|dont|never|must\s+not|should\s+not)\s+(?:git\s+push|push)\b|\bno\s+push\b|\bwithout\s+pushing\b/i;
|
|
37
|
+
const EXPLICIT_TECHNICAL_MARKER_RE = /\b(?:Technically|Technical details)\s*:\s*/i;
|
|
38
|
+
const HUMAN_SUMMARY_SECTION_RE = /(?:^|\n)\s*(?:Human-readable summary|Human summary)\s*:\s*/i;
|
|
39
|
+
const TECHNICAL_DETAILS_SECTION_RE = /(?:^|\n)\s*(?:Technical details?|Details(?:_technical)?)\s*:\s*/i;
|
|
40
|
+
const HUMAN_SUMMARY_LABEL_RE = /^(?:human-readable summary|human summary)\s*:\s*/i;
|
|
41
|
+
const TECHNICAL_DETAILS_LABEL_RE = /^(?:technical details?|details(?:_technical)?)\s*:\s*/i;
|
|
42
|
+
const FINAL_REPORT_HEADING_RE = /^(?:#{1,6}\s*)?(?:root cause|files? changed|changes|validation|tests?(?: run| passed)?|sacrificial(?: delivery)?(?: result)?|deployment(?:\/live-runtime)?(?: step)?|live-runtime(?: step)?|result|results|summary|highlights?|notes?|follow[- ]ups?|next steps?|blockers?|implementation|what changed|verification)\s*:?$/i;
|
|
43
|
+
const FINAL_REPORT_CUE_RE = /\b(?:root cause|files? changed|tests? run|validation|sacrificial(?: delivery)?(?: result)?|deployment(?:\/live-runtime)?(?: step)?|live-runtime(?: step)?|final report|human-readable report|files changed|tests passed)\b/i;
|
|
37
44
|
|
|
38
45
|
export function normalizeCompletionText(value) {
|
|
39
46
|
if (typeof value !== 'string') return null;
|
|
@@ -71,6 +78,56 @@ function cleanMarkdown(text) {
|
|
|
71
78
|
.replace(/^>\s?/gm, '');
|
|
72
79
|
}
|
|
73
80
|
|
|
81
|
+
function normalizeReportLineEndings(text) {
|
|
82
|
+
const normalized = normalizeCompletionText(text);
|
|
83
|
+
if (!normalized) return null;
|
|
84
|
+
return stripAnsi(normalized)
|
|
85
|
+
.replace(/\r\n?/g, '\n')
|
|
86
|
+
.split('\n')
|
|
87
|
+
.map(line => line.replace(/[ \t]+$/g, ''))
|
|
88
|
+
.join('\n')
|
|
89
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
90
|
+
.trim();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function isLikelyHumanFinalReport(text) {
|
|
94
|
+
const normalized = normalizeReportLineEndings(text);
|
|
95
|
+
if (!normalized) return false;
|
|
96
|
+
if (isGenericOrTrivial(normalized)) return false;
|
|
97
|
+
if (isInternalTransportNoiseText(normalized)) return false;
|
|
98
|
+
if (looksLikeRawPayloadText(normalized)) return false;
|
|
99
|
+
if (looksLikeGunbrokerReport(normalized)) return false;
|
|
100
|
+
|
|
101
|
+
const rawLines = normalized
|
|
102
|
+
.split('\n')
|
|
103
|
+
.map(line => line.trim())
|
|
104
|
+
.filter(Boolean)
|
|
105
|
+
.filter(line => !/^```/.test(line));
|
|
106
|
+
if (rawLines.length < 3) return false;
|
|
107
|
+
|
|
108
|
+
const cleanedLines = rawLines.map(line => cleanMarkdown(line).replace(/\s+/g, ' ').trim());
|
|
109
|
+
const headingCount = cleanedLines.filter(line => FINAL_REPORT_HEADING_RE.test(line)).length;
|
|
110
|
+
const itemCount = rawLines.filter(isItemLine).length;
|
|
111
|
+
const hasCue = FINAL_REPORT_CUE_RE.test(normalized);
|
|
112
|
+
const hasSectionLabel = /^#{1,6}\s+\S|^[A-Za-z][A-Za-z0-9 /_-]{2,60}:$/m.test(normalized);
|
|
113
|
+
|
|
114
|
+
// This is the key path for real completion reports from agents: multiple
|
|
115
|
+
// human-readable sections plus bullets. Those reports are already the final
|
|
116
|
+
// answer and must not be collapsed into "Files changed: Validation: ...".
|
|
117
|
+
if (hasCue && headingCount >= 2 && (itemCount >= 1 || rawLines.length >= 5)) return true;
|
|
118
|
+
|
|
119
|
+
// Allow slightly shorter reports with an explicit root cause / validation shape.
|
|
120
|
+
if (hasCue && headingCount >= 1 && itemCount >= 2 && hasSectionLabel) return true;
|
|
121
|
+
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function getPassThroughHumanFinalReport(text) {
|
|
126
|
+
const normalized = normalizeReportLineEndings(text);
|
|
127
|
+
if (!normalized) return null;
|
|
128
|
+
return isLikelyHumanFinalReport(normalized) ? normalized : null;
|
|
129
|
+
}
|
|
130
|
+
|
|
74
131
|
function isGenericOrTrivial(text) {
|
|
75
132
|
const normalized = normalizeCompletionText(text)?.toLowerCase().replace(/\s+/g, ' ').trim();
|
|
76
133
|
if (!normalized) return true;
|
|
@@ -205,6 +262,80 @@ function upperFirst(text) {
|
|
|
205
262
|
return text.charAt(0).toUpperCase() + text.slice(1);
|
|
206
263
|
}
|
|
207
264
|
|
|
265
|
+
function extractExplicitTechnicalTail(text) {
|
|
266
|
+
const normalized = normalizeCompletionText(text);
|
|
267
|
+
if (!normalized) return null;
|
|
268
|
+
|
|
269
|
+
const match = EXPLICIT_TECHNICAL_MARKER_RE.exec(normalized);
|
|
270
|
+
if (!match || typeof match.index !== 'number' || match.index <= 0) return null;
|
|
271
|
+
|
|
272
|
+
const lead = normalizeCompletionText(normalized.slice(0, match.index));
|
|
273
|
+
const technicalTail = normalizeCompletionText(normalized.slice(match.index + match[0].length));
|
|
274
|
+
if (!lead || !technicalTail) return null;
|
|
275
|
+
|
|
276
|
+
return { lead, technicalTail };
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function extractStructuredSummarySections(text) {
|
|
280
|
+
const normalized = normalizeCompletionText(text);
|
|
281
|
+
if (!normalized) return null;
|
|
282
|
+
|
|
283
|
+
const cleaned = cleanMarkdown(normalized).replace(/\r\n?/g, '\n').trim();
|
|
284
|
+
if (!cleaned) return null;
|
|
285
|
+
|
|
286
|
+
const humanMatch = HUMAN_SUMMARY_SECTION_RE.exec(cleaned);
|
|
287
|
+
const technicalMatch = TECHNICAL_DETAILS_SECTION_RE.exec(cleaned);
|
|
288
|
+
if (!humanMatch && !technicalMatch) return null;
|
|
289
|
+
|
|
290
|
+
let summary = null;
|
|
291
|
+
let technical = null;
|
|
292
|
+
|
|
293
|
+
if (humanMatch) {
|
|
294
|
+
const summaryStart = humanMatch.index + humanMatch[0].length;
|
|
295
|
+
const summaryEnd = technicalMatch && technicalMatch.index > humanMatch.index
|
|
296
|
+
? technicalMatch.index
|
|
297
|
+
: cleaned.length;
|
|
298
|
+
summary = normalizeCompletionText(cleaned.slice(summaryStart, summaryEnd));
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (technicalMatch) {
|
|
302
|
+
const technicalStart = technicalMatch.index + technicalMatch[0].length;
|
|
303
|
+
technical = normalizeCompletionText(cleaned.slice(technicalStart));
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (!summary && !technical) return null;
|
|
307
|
+
return { summary, technical };
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function stripHumanSummaryLabel(text) {
|
|
311
|
+
const normalized = normalizeCompletionText(text);
|
|
312
|
+
if (!normalized) return null;
|
|
313
|
+
|
|
314
|
+
const sections = extractStructuredSummarySections(normalized);
|
|
315
|
+
if (sections?.summary) return normalizeCompletionText(sections.summary);
|
|
316
|
+
return normalizeCompletionText(normalized.replace(HUMAN_SUMMARY_LABEL_RE, ''));
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function normalizeTechnicalDetailLine(text) {
|
|
320
|
+
const normalized = normalizeCompletionText(text);
|
|
321
|
+
if (!normalized) return null;
|
|
322
|
+
|
|
323
|
+
const sections = extractStructuredSummarySections(normalized);
|
|
324
|
+
const source = normalizeCompletionText(sections?.technical || normalized);
|
|
325
|
+
if (!source) return null;
|
|
326
|
+
|
|
327
|
+
const lines = prepareLines(source)
|
|
328
|
+
.map(line => line
|
|
329
|
+
.replace(HUMAN_SUMMARY_LABEL_RE, '')
|
|
330
|
+
.replace(TECHNICAL_DETAILS_LABEL_RE, '')
|
|
331
|
+
.replace(/^[-*•]\s+/, '')
|
|
332
|
+
.trim())
|
|
333
|
+
.filter(Boolean);
|
|
334
|
+
|
|
335
|
+
const compact = normalizeCompletionText(lines.join(' '));
|
|
336
|
+
return compact || null;
|
|
337
|
+
}
|
|
338
|
+
|
|
208
339
|
function looksLikeRawPayloadText(text) {
|
|
209
340
|
const normalized = normalizeCompletionText(text);
|
|
210
341
|
if (!normalized) return false;
|
|
@@ -409,6 +540,107 @@ function cleanTechnicalFragment(text) {
|
|
|
409
540
|
return cleaned || null;
|
|
410
541
|
}
|
|
411
542
|
|
|
543
|
+
function cleanLeadForHumanSummary(text) {
|
|
544
|
+
const normalized = normalizeCompletionText(text);
|
|
545
|
+
if (!normalized) return null;
|
|
546
|
+
|
|
547
|
+
const cleaned = replaceTechnicalPhrases(
|
|
548
|
+
cleanMarkdown(normalized)
|
|
549
|
+
.replace(/\bsaved\s+[a-z0-9_]+(?:\/[a-z0-9_]+)+\b/gi, 'your saved progress')
|
|
550
|
+
.replace(/\b([a-z][A-Za-z0-9_]*[A-Z][A-Za-z0-9_]*)\b/g, (_, token) => humanizeCamelToken(token))
|
|
551
|
+
.replace(/\b([a-z0-9]+_[a-z0-9_]+)\b/g, token => token.replace(/_/g, ' ')),
|
|
552
|
+
)
|
|
553
|
+
.replace(/\brepeating the completed one\b/gi, 'repeating the one you already finished')
|
|
554
|
+
.replace(/\s+,/g, ',')
|
|
555
|
+
.replace(/\s+\./g, '.')
|
|
556
|
+
.replace(/\s+/g, ' ')
|
|
557
|
+
.trim();
|
|
558
|
+
|
|
559
|
+
return cleaned || null;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
function formatMixedTechnicalSubject(subject) {
|
|
563
|
+
let cleaned = normalizeCompletionText(subject);
|
|
564
|
+
if (!cleaned) return 'the missing data';
|
|
565
|
+
|
|
566
|
+
cleaned = replaceTechnicalPhrases(
|
|
567
|
+
cleanMarkdown(cleaned)
|
|
568
|
+
.replace(/\bhealth auto export\b/gi, 'source export')
|
|
569
|
+
.replace(/\bworkouts?\.json\b/gi, 'the export')
|
|
570
|
+
.replace(/\b([a-z][A-Za-z0-9_]*[A-Z][A-Za-z0-9_]*)\b/g, (_, token) => humanizeCamelToken(token))
|
|
571
|
+
.replace(/\b([a-z0-9]+_[a-z0-9_]+)\b/g, token => token.replace(/_/g, ' ')),
|
|
572
|
+
)
|
|
573
|
+
.replace(/^the\s+(?!missing\b)/i, '')
|
|
574
|
+
.replace(/\s+/g, ' ')
|
|
575
|
+
.trim();
|
|
576
|
+
|
|
577
|
+
if (!cleaned) return 'the missing data';
|
|
578
|
+
if (/^the\s+missing\b/i.test(cleaned)) return cleaned;
|
|
579
|
+
if (/^missing\b/i.test(cleaned)) return `the ${cleaned}`;
|
|
580
|
+
if (/^(?:the|your|this|that)\b/i.test(cleaned)) return cleaned;
|
|
581
|
+
return `the ${cleaned}`;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
function buildSourceSideHumanSentence(technicalTail) {
|
|
585
|
+
const normalized = cleanMarkdown(normalizeCompletionText(technicalTail) || '')
|
|
586
|
+
.replace(/\s+/g, ' ')
|
|
587
|
+
.trim();
|
|
588
|
+
if (!normalized) return null;
|
|
589
|
+
|
|
590
|
+
const hasSourceSide = /\bsource[- ]side\b/i.test(normalized);
|
|
591
|
+
const hasEmptySource = /\b(?:contains zero|count 0|empty|zero [a-z ]+ objects?|no [a-z ]+ to import)\b/i.test(normalized);
|
|
592
|
+
if (!hasSourceSide && !hasEmptySource) return null;
|
|
593
|
+
|
|
594
|
+
const subjectMatch = normalized.match(/\b(?:confirmed|found|verified|checked)\s+(the\s+missing\s+.+?)\s+is\s+source[- ]side\b/i)
|
|
595
|
+
|| normalized.match(/\b(the\s+missing\s+.+?)\s+is\s+source[- ]side\b/i)
|
|
596
|
+
|| normalized.match(/\b(?:confirmed|found|verified|checked)\s+(.+?)\s+is\s+source[- ]side\b/i)
|
|
597
|
+
|| normalized.match(/\b(.+?)\s+is\s+source[- ]side\b/i);
|
|
598
|
+
const subject = formatMixedTechnicalSubject(subjectMatch?.[1] || 'missing data');
|
|
599
|
+
|
|
600
|
+
if (hasEmptySource) {
|
|
601
|
+
return `I also checked ${subject}, and the source export is empty right now, so there isn't anything new to import yet.`;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
return `I also checked ${subject}, and this turned out to be a source-data issue rather than a new scheduler bug.`;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
function buildMixedLeadExpectation(text) {
|
|
608
|
+
const normalized = cleanMarkdown(normalizeCompletionText(text) || '').toLowerCase();
|
|
609
|
+
if (!normalized) return null;
|
|
610
|
+
if (/\b(?:next|should|will)\b/.test(normalized)) return null;
|
|
611
|
+
|
|
612
|
+
if (/\b(?:planner|plan|scheduled session|session|progression|workout)\b/.test(normalized)) {
|
|
613
|
+
return 'The next plan should reflect that automatically.';
|
|
614
|
+
}
|
|
615
|
+
if (/\b(?:completion|summary|delivery|message|report)\b/.test(normalized)) {
|
|
616
|
+
return 'The next finished job should show that automatically.';
|
|
617
|
+
}
|
|
618
|
+
if (/\b(?:import|sync)\b/.test(normalized)) {
|
|
619
|
+
return 'The next sync should reflect that automatically.';
|
|
620
|
+
}
|
|
621
|
+
return 'The next run should reflect that automatically.';
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
function buildHumanSummaryFromMixedTechnicalText(rawText) {
|
|
625
|
+
const split = extractExplicitTechnicalTail(rawText);
|
|
626
|
+
if (!split) return null;
|
|
627
|
+
|
|
628
|
+
const cleanedLead = cleanLeadForHumanSummary(split.lead);
|
|
629
|
+
const leadSummary = cleanedLead
|
|
630
|
+
? summarizeCompletionText(cleanedLead, { skipEmbeddedObject: true }) || summarizeProse(cleanedLead) || asSentence(cleanedLead)
|
|
631
|
+
: null;
|
|
632
|
+
if (!leadSummary) return null;
|
|
633
|
+
|
|
634
|
+
const sentences = [leadSummary];
|
|
635
|
+
const sourceSideSentence = buildSourceSideHumanSentence(split.technicalTail);
|
|
636
|
+
if (sourceSideSentence) sentences.push(sourceSideSentence);
|
|
637
|
+
|
|
638
|
+
const expectation = buildMixedLeadExpectation(sentences.join(' '));
|
|
639
|
+
if (expectation) sentences.push(expectation);
|
|
640
|
+
|
|
641
|
+
return truncateText(sentences.join(' '), MAX_DELIVERY_CHARS);
|
|
642
|
+
}
|
|
643
|
+
|
|
412
644
|
function isTestOrValidationFragment(fragment) {
|
|
413
645
|
const cleaned = normalizeCompletionText(fragment);
|
|
414
646
|
if (!cleaned) return false;
|
|
@@ -578,11 +810,21 @@ export function humanizeCompletionText(value) {
|
|
|
578
810
|
const raw = normalizeCompletionText(value);
|
|
579
811
|
if (!raw) return null;
|
|
580
812
|
|
|
581
|
-
const
|
|
813
|
+
const passThroughReport = getPassThroughHumanFinalReport(raw);
|
|
814
|
+
if (passThroughReport) return passThroughReport;
|
|
815
|
+
|
|
816
|
+
const structuredSections = extractStructuredSummarySections(raw);
|
|
817
|
+
const summarySource = normalizeCompletionText(structuredSections?.summary || raw);
|
|
818
|
+
if (!summarySource) return null;
|
|
819
|
+
|
|
820
|
+
const mixedSummary = buildHumanSummaryFromMixedTechnicalText(summarySource);
|
|
821
|
+
if (mixedSummary) return mixedSummary;
|
|
822
|
+
|
|
823
|
+
const summarized = summarizeCompletionText(summarySource);
|
|
582
824
|
if (!summarized) return null;
|
|
583
|
-
if (!looksTechnicalCompletionSummary(
|
|
825
|
+
if (!looksTechnicalCompletionSummary(summarySource, summarized)) return stripHumanSummaryLabel(summarized) || summarized;
|
|
584
826
|
|
|
585
|
-
return buildHumanizedTechnicalSummary(
|
|
827
|
+
return buildHumanizedTechnicalSummary(summarySource, summarized) || summarized;
|
|
586
828
|
}
|
|
587
829
|
|
|
588
830
|
function summarizeChecklistTechnicalDetails(checklist, sha) {
|
|
@@ -615,29 +857,47 @@ function buildTechnicalDetailsText({ rawText, summaryText, completion } = {}) {
|
|
|
615
857
|
const details = getCompletionTechnicalDetails(completion);
|
|
616
858
|
const parts = [];
|
|
617
859
|
|
|
618
|
-
const
|
|
860
|
+
const rawSections = extractStructuredSummarySections(raw);
|
|
861
|
+
const splitRaw = extractExplicitTechnicalTail(raw);
|
|
862
|
+
const rawTechnicalSource = normalizeTechnicalDetailLine(rawSections?.technical || splitRaw?.technicalTail || raw);
|
|
863
|
+
const rawHasExplicitTechnical = Boolean(rawSections?.technical || splitRaw?.technicalTail);
|
|
864
|
+
|
|
865
|
+
const rawTechnical = Boolean(
|
|
866
|
+
rawTechnicalSource
|
|
867
|
+
&& ((rawHasExplicitTechnical && rawTechnicalSource !== summary)
|
|
868
|
+
|| (looksTechnicalCompletionSummary(rawTechnicalSource, summary) && rawTechnicalSource !== summary)),
|
|
869
|
+
);
|
|
619
870
|
if (rawTechnical) {
|
|
620
|
-
parts.push(truncateText(
|
|
871
|
+
parts.push(truncateText(rawTechnicalSource, 260));
|
|
621
872
|
}
|
|
622
873
|
|
|
623
874
|
let completionDetailsAreTechnical = false;
|
|
624
875
|
if (typeof details === 'string') {
|
|
625
|
-
const
|
|
626
|
-
|
|
876
|
+
const detailSections = extractStructuredSummarySections(details);
|
|
877
|
+
const splitDetails = extractExplicitTechnicalTail(details);
|
|
878
|
+
const normalized = normalizeTechnicalDetailLine(detailSections?.technical || splitDetails?.technicalTail || details);
|
|
879
|
+
completionDetailsAreTechnical = Boolean(
|
|
880
|
+
normalized && (detailSections?.technical || splitDetails?.technicalTail || looksTechnicalCompletionSummary(normalized, summary)),
|
|
881
|
+
);
|
|
627
882
|
if (normalized
|
|
628
883
|
&& !isInternalTransportNoiseText(normalized)
|
|
629
884
|
&& (completionDetailsAreTechnical || rawTechnical)
|
|
630
|
-
&& (!rawTechnical || normalized !==
|
|
885
|
+
&& (!rawTechnical || normalized !== rawTechnicalSource)) {
|
|
631
886
|
parts.push(truncateText(normalized, 220));
|
|
632
887
|
}
|
|
633
888
|
} else if (details && typeof details === 'object') {
|
|
634
889
|
const rawSummary = normalizeCompletionText(details.raw_summary);
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
890
|
+
const detailSummarySections = extractStructuredSummarySections(rawSummary);
|
|
891
|
+
const splitDetailSummary = extractExplicitTechnicalTail(rawSummary);
|
|
892
|
+
const technicalSummary = normalizeTechnicalDetailLine(detailSummarySections?.technical || splitDetailSummary?.technicalTail || rawSummary);
|
|
893
|
+
completionDetailsAreTechnical = Boolean(
|
|
894
|
+
technicalSummary && (detailSummarySections?.technical || splitDetailSummary?.technicalTail || looksTechnicalCompletionSummary(technicalSummary, summary)),
|
|
895
|
+
);
|
|
896
|
+
if (technicalSummary
|
|
897
|
+
&& !isInternalTransportNoiseText(technicalSummary)
|
|
638
898
|
&& (completionDetailsAreTechnical || rawTechnical)
|
|
639
|
-
&& (!rawTechnical ||
|
|
640
|
-
parts.push(truncateText(
|
|
899
|
+
&& (!rawTechnical || technicalSummary !== rawTechnicalSource)) {
|
|
900
|
+
parts.push(truncateText(technicalSummary, 220));
|
|
641
901
|
}
|
|
642
902
|
}
|
|
643
903
|
|
|
@@ -647,7 +907,7 @@ function buildTechnicalDetailsText({ rawText, summaryText, completion } = {}) {
|
|
|
647
907
|
const unique = [];
|
|
648
908
|
const seen = new Set();
|
|
649
909
|
for (const part of parts) {
|
|
650
|
-
const normalized = normalizeCompletionText(part);
|
|
910
|
+
const normalized = normalizeTechnicalDetailLine(part) || normalizeCompletionText(part);
|
|
651
911
|
if (!normalized) continue;
|
|
652
912
|
const key = normalized.toLowerCase();
|
|
653
913
|
if (seen.has(key)) continue;
|
|
@@ -659,22 +919,39 @@ function buildTechnicalDetailsText({ rawText, summaryText, completion } = {}) {
|
|
|
659
919
|
}
|
|
660
920
|
|
|
661
921
|
function composeDeliveryText(summaryText, technicalDetailsText = null) {
|
|
662
|
-
const
|
|
922
|
+
const summarySections = extractStructuredSummarySections(summaryText);
|
|
923
|
+
const summary = stripHumanSummaryLabel(summarySections?.summary || summaryText);
|
|
663
924
|
if (!summary) return null;
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
925
|
+
|
|
926
|
+
const technicalCandidates = [];
|
|
927
|
+
if (summarySections?.technical) technicalCandidates.push(summarySections.technical);
|
|
928
|
+
if (Array.isArray(technicalDetailsText)) technicalCandidates.push(...technicalDetailsText);
|
|
929
|
+
else if (technicalDetailsText != null) technicalCandidates.push(technicalDetailsText);
|
|
930
|
+
|
|
931
|
+
const technicalLines = [];
|
|
932
|
+
const seen = new Set();
|
|
933
|
+
for (const candidate of technicalCandidates) {
|
|
934
|
+
const normalized = normalizeTechnicalDetailLine(candidate);
|
|
935
|
+
if (!normalized) continue;
|
|
936
|
+
const key = normalized.toLowerCase();
|
|
937
|
+
if (seen.has(key)) continue;
|
|
938
|
+
seen.add(key);
|
|
939
|
+
technicalLines.push(normalized);
|
|
940
|
+
}
|
|
941
|
+
|
|
667
942
|
if (technicalLines.length > 0) {
|
|
668
943
|
return `${summary}\n\nTechnical details:\n- ${technicalLines.join('\n- ')}`;
|
|
669
944
|
}
|
|
670
|
-
|
|
671
|
-
return technical ? `${summary}\n\nTechnical details:\n- ${technical}` : summary;
|
|
945
|
+
return summary;
|
|
672
946
|
}
|
|
673
947
|
|
|
674
948
|
export function summarizeCompletionText(value, { skipEmbeddedObject = false } = {}) {
|
|
675
949
|
const raw = normalizeCompletionText(value);
|
|
676
950
|
if (!raw) return null;
|
|
677
951
|
|
|
952
|
+
const passThroughReport = getPassThroughHumanFinalReport(raw);
|
|
953
|
+
if (passThroughReport) return passThroughReport;
|
|
954
|
+
|
|
678
955
|
if (!skipEmbeddedObject) {
|
|
679
956
|
const parsed = extractEmbeddedCompletionObject(raw);
|
|
680
957
|
if (parsed !== null) {
|