openclaw-scheduler 0.2.4 → 0.2.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,46 @@
1
- const GENERIC_COMPLETION_TEXT_RE = /^(?:completed(?:\s*\([^\n)]*\))?|done|ok|okay|success|successful|complete|all set|none|n\/?a)$/i;
1
+ const MAX_DELIVERY_SENTENCES = 5;
2
+ const MAX_DELIVERY_CHARS = 700;
3
+ const MAX_LIST_ITEMS = 3;
4
+
5
+ const GENERIC_COMPLETION_TEXT_RE = /^(?:completed(?:\s*\([^\n)]*\))?|done|ok|okay|success|successful|complete|all set|none|n\/?a)[.!?]*$/i;
2
6
  const TRIVIAL_CHATTER_RE = /^(?:hi|hello|hey|yo|sup|thanks|thank you|cool|nice|sure|yep|yeah|k|kk|roger|copy that)[.!?]*$/i;
7
+ const INTERNAL_DONE_PAYLOAD_RE = /"message"\s*:\s*"Label marked done via agent signal\."|"status"\s*:\s*"done"/i;
8
+ const INTERNAL_DONE_MESSAGE_RE = /^label marked done via agent signal\.[!?]*$/i;
9
+ const INTERNAL_TRANSPORT_PREFIX_RE = /^auto-resolved(?:\s+as\s+[a-z-]+)?\s*:/i;
10
+ const INTERNAL_TRANSPORT_PATTERNS = [
11
+ /^session not found in gateway store[.!?]*$/i,
12
+ /^session not found in sessions store[.!?]*$/i,
13
+ /^session never found(?:\s+--\s+spawn likely failed)?[.!?]*$/i,
14
+ /^the delivery watcher stopped before the task reached a terminal state[.!?]*$/i,
15
+ /^session went idle without calling done(?:\.\s*work may be incomplete\.)?(?:\s*\([^)]*\))?[.!?]*$/i,
16
+ ];
17
+ const RAW_PAYLOAD_MARKERS_RE = /"(?:ok|status|label|sessionKey|idempotencyKey|deliveryText|summary|message|checklist|stdout|stderr|tool|args|result|content)"\s*:/i;
18
+ const STACK_TRACE_LINE_RE = /^\s*at\s+\S+/;
19
+ const TECHNICAL_COMMIT_PREFIX_RE = /^(?:fix|feat|feature|chore|refactor|perf|docs|test|tests|build|ci|style|revert|hotfix)(?:\([^)]+\))?!?:\s*/i;
20
+ const FILE_CONTEXT_PREFIX_RE = /^(?:(?:[A-Za-z0-9_.-]+\/)*[A-Za-z0-9_.-]+\.(?:[cm]?[jt]sx?|json|md|py|sh|sql|ya?ml|toml)):\s*/i;
21
+ const CODEISH_MARKER_RE = /(?:`[^`]+`|--[a-z0-9-]+|\b[A-Z_]{3,}\b|\b[a-z0-9_]+\(\)|\b[a-z]+[A-Z][A-Za-z0-9_]+\b|\b(?:[A-Za-z0-9_.-]+\/)+[A-Za-z0-9_.-]+\b|\b[A-Za-z0-9_.-]+\.(?:[cm]?[jt]sx?|json|md|py|sh|sql|ya?ml|toml)\b)/;
22
+ const HUMAN_CUE_RE = /\b(?:now|so that|so\b|because|this\b|future runs?|expect|easier|readable|reliable|cleaner|people|users?|operators?|chat|helps?|lets?|allows?|prevents?|stops?|keeps?|avoids?)\b/i;
23
+ const TECHNICAL_KEYWORDS = [
24
+ 'api', 'artifact', 'assert', 'build', 'checklist', 'cli', 'commit', 'completion', 'config', 'context',
25
+ 'cron', 'db', 'delivery', 'dispatch', 'gateway', 'guard', 'hook', 'json', 'job', 'label', 'lint',
26
+ 'message', 'metadata', 'notify', 'output', 'path', 'payload', 'pipeline', 'post-office', 'queue', 'raw',
27
+ 'regex', 'report', 'retry', 'scheduler', 'session', 'sha', 'shell', 'spec', 'sql', 'stderr', 'stdout',
28
+ 'structured', 'summary', 'sync', 'test', 'tests', 'tool', 'transcript', 'transport', 'typecheck', 'watcher',
29
+ 'webhook', 'workflow'
30
+ ];
31
+ const TECHNICAL_KEYWORD_RE = new RegExp(`\\b(?:${TECHNICAL_KEYWORDS.join('|')})\\b`, 'gi');
32
+ const RELIABILITY_SIGNAL_RE = /\b(?:fix|guard|retry|timeout|error|fail|stuck|prevent|avoid|dedupe|deduplicat|preserve|ensure|handle|recover|reliab)\b/i;
33
+ const TEST_FRAGMENT_RE = /\b(?:test|tests|spec|coverage|lint|typecheck|tsc|eslint|oxlint|assert(?:ion)?s?)\b/i;
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
+ const TEST_NEGATION_RE = /\b(?:do\s+not|don't|dont|never|skip|without|no)\s+(?:run\s+)?(?:the\s+)?tests?\b/i;
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;
3
44
 
4
45
  export function normalizeCompletionText(value) {
5
46
  if (typeof value !== 'string') return null;
@@ -7,19 +48,970 @@ export function normalizeCompletionText(value) {
7
48
  return trimmed ? trimmed : null;
8
49
  }
9
50
 
10
- export function isMeaningfulCompletionText(value) {
51
+ const ANSI_ESCAPE_RE = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, 'g');
52
+
53
+ function stripAnsi(text) {
54
+ return text.replace(ANSI_ESCAPE_RE, '');
55
+ }
56
+
57
+ export function isInternalTransportNoiseText(value) {
11
58
  const text = normalizeCompletionText(value);
12
59
  if (!text) return false;
13
60
 
14
- const normalized = text.toLowerCase().replace(/\s+/g, ' ').trim();
61
+ const normalized = text.replace(/\s+/g, ' ').trim();
62
+ if (!normalized) return false;
63
+ if (INTERNAL_TRANSPORT_PREFIX_RE.test(normalized)) return true;
64
+ if (INTERNAL_DONE_PAYLOAD_RE.test(normalized)) return true;
65
+ if (INTERNAL_DONE_MESSAGE_RE.test(normalized)) return true;
66
+
67
+ return INTERNAL_TRANSPORT_PATTERNS.some((pattern) => pattern.test(normalized));
68
+ }
69
+
70
+ function cleanMarkdown(text) {
71
+ return stripAnsi(text)
72
+ .replace(/\r\n?/g, '\n')
73
+ .replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1')
74
+ .replace(/`([^`]+)`/g, '$1')
75
+ .replace(/\*\*([^*]+)\*\*/g, '$1')
76
+ .replace(/__([^_]+)__/g, '$1')
77
+ .replace(/^#{1,6}\s+/gm, '')
78
+ .replace(/^>\s?/gm, '');
79
+ }
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
+
131
+ function isGenericOrTrivial(text) {
132
+ const normalized = normalizeCompletionText(text)?.toLowerCase().replace(/\s+/g, ' ').trim();
133
+ if (!normalized) return true;
134
+ if (GENERIC_COMPLETION_TEXT_RE.test(normalized)) return true;
135
+ if (TRIVIAL_CHATTER_RE.test(normalized)) return true;
136
+ return false;
137
+ }
138
+
139
+ function parseJsonCandidate(value) {
140
+ const trimmed = normalizeCompletionText(value);
141
+ if (!trimmed) return null;
142
+ try {
143
+ return JSON.parse(trimmed);
144
+ } catch {
145
+ return null;
146
+ }
147
+ }
148
+
149
+ function extractEmbeddedCompletionObject(text) {
150
+ const normalized = normalizeCompletionText(text);
151
+ if (!normalized) return null;
152
+
153
+ const candidates = [normalized];
154
+ const newlineJsonIdx = normalized.lastIndexOf('\n{');
155
+ if (newlineJsonIdx >= 0) candidates.push(normalized.slice(newlineJsonIdx + 1));
156
+ const firstBraceIdx = normalized.indexOf('{');
157
+ if (firstBraceIdx > 0) candidates.push(normalized.slice(firstBraceIdx));
158
+
159
+ const seen = new Set();
160
+ for (const candidate of candidates) {
161
+ const trimmed = candidate.trim();
162
+ if (!trimmed || seen.has(trimmed)) continue;
163
+ seen.add(trimmed);
164
+ const parsed = parseJsonCandidate(trimmed);
165
+ if (parsed !== null) return parsed;
166
+ }
167
+ return null;
168
+ }
169
+
170
+ function gatherObjectTextCandidates(value, depth = 0, out = [], seen = new Set()) {
171
+ if (depth > 4 || value == null) return out;
172
+
173
+ if (typeof value === 'string') {
174
+ const text = normalizeCompletionText(value);
175
+ if (text && !seen.has(text)) {
176
+ seen.add(text);
177
+ out.push(text);
178
+ }
179
+ return out;
180
+ }
181
+
182
+ if (Array.isArray(value)) {
183
+ for (const item of value.slice(0, 6)) {
184
+ gatherObjectTextCandidates(item, depth + 1, out, seen);
185
+ }
186
+ return out;
187
+ }
188
+
189
+ if (typeof value !== 'object') return out;
190
+
191
+ const preferredKeys = ['deliveryText', 'summary', 'body', 'text', 'content', 'message', 'stdout', 'stderr'];
192
+ const nestedKeys = ['completion', 'result', 'response', 'data', 'payload'];
193
+
194
+ for (const key of preferredKeys) {
195
+ if (Object.hasOwn(value, key)) {
196
+ gatherObjectTextCandidates(value[key], depth + 1, out, seen);
197
+ }
198
+ }
199
+
200
+ for (const key of nestedKeys) {
201
+ if (Object.hasOwn(value, key)) {
202
+ gatherObjectTextCandidates(value[key], depth + 1, out, seen);
203
+ }
204
+ }
205
+
206
+ for (const [key, nestedValue] of Object.entries(value)) {
207
+ if (preferredKeys.includes(key) || nestedKeys.includes(key)) continue;
208
+ gatherObjectTextCandidates(nestedValue, depth + 1, out, seen);
209
+ }
210
+
211
+ return out;
212
+ }
213
+
214
+ function prepareLines(text) {
215
+ return cleanMarkdown(text)
216
+ .split('\n')
217
+ .map(line => line.replace(/\t/g, ' ').replace(/\s+/g, ' ').trim())
218
+ .filter(Boolean)
219
+ .filter(line => !/^```/.test(line))
220
+ .filter(line => !/^[`~=_-]{3,}$/.test(line))
221
+ .filter(line => ![...line].every(char => '{}[],'.includes(char)))
222
+ .filter(line => !STACK_TRACE_LINE_RE.test(line));
223
+ }
224
+
225
+ function truncateText(text, maxChars = MAX_DELIVERY_CHARS) {
226
+ const normalized = normalizeCompletionText(text);
227
+ if (!normalized) return null;
228
+ if (normalized.length <= maxChars) return normalized;
229
+ return normalized.slice(0, Math.max(0, maxChars - 1)).trimEnd() + '…';
230
+ }
231
+
232
+ function splitSentences(text) {
233
+ const normalized = normalizeCompletionText(text);
234
+ if (!normalized) return [];
235
+ return normalized.match(/[^.!?]+(?:[.!?]+|$)/g)?.map(part => part.trim()).filter(Boolean) || [];
236
+ }
237
+
238
+ function asSentence(text) {
239
+ const normalized = normalizeCompletionText(text);
240
+ if (!normalized) return null;
241
+ return /[.!?]$/.test(normalized) ? normalized : `${normalized}.`;
242
+ }
243
+
244
+ function shortenFragment(text, maxChars = 110) {
245
+ const normalized = normalizeCompletionText(text);
246
+ if (!normalized) return null;
247
+ const cleaned = cleanMarkdown(normalized)
248
+ .replace(/\bhttps?:\/\/\S+/gi, '')
249
+ .replace(/\s+/g, ' ')
250
+ .trim();
251
+ if (!cleaned) return null;
252
+ return truncateText(cleaned, maxChars);
253
+ }
254
+
255
+ function lowerFirst(text) {
256
+ if (!text) return text;
257
+ return text.charAt(0).toLowerCase() + text.slice(1);
258
+ }
259
+
260
+ function upperFirst(text) {
261
+ if (!text) return text;
262
+ return text.charAt(0).toUpperCase() + text.slice(1);
263
+ }
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
+
339
+ function looksLikeRawPayloadText(text) {
340
+ const normalized = normalizeCompletionText(text);
341
+ if (!normalized) return false;
342
+ if (INTERNAL_DONE_PAYLOAD_RE.test(normalized)) return true;
343
+ if (isInternalTransportNoiseText(normalized)) return true;
344
+ return (/^[[{]/.test(normalized) && RAW_PAYLOAD_MARKERS_RE.test(normalized));
345
+ }
346
+
347
+ function looksLikeGunbrokerReport(text) {
348
+ const normalized = normalizeCompletionText(text);
349
+ if (!normalized) return false;
350
+ return /deal scanner/i.test(normalized) || (/^Baseline:/mi.test(normalized) && /^#\d+/m.test(normalized));
351
+ }
352
+
353
+ function parseGunbrokerItem(line) {
354
+ const normalized = normalizeCompletionText(line);
355
+ if (!normalized) return null;
356
+ const match = normalized.match(/^#(\d+)(?:\s+\S+)?\s*\|\s*([+-]\d+%)\s*[—-]\s*\$?([\d,]+)\s*\(([^)]*)\)/);
357
+ if (!match) return shortenFragment(normalized, 100);
358
+ const [, rank, edge, price, context] = match;
359
+ return `#${rank} ${edge} at $${price} (${context.trim()})`;
360
+ }
361
+
362
+ function summarizeGunbrokerReport(text) {
363
+ const lines = prepareLines(text);
364
+ if (!lines.length) return null;
365
+
366
+ const titleLine = lines.find(line => /deal scanner/i.test(line)) || null;
367
+ const baselineLine = lines.find(line => /^Baseline:/i.test(line)) || null;
368
+ const noDealsLine = lines.find(line => /no deals/i.test(line)) || null;
369
+ const items = lines
370
+ .filter(line => /^#\d+/.test(line))
371
+ .map(parseGunbrokerItem)
372
+ .filter(Boolean)
373
+ .slice(0, MAX_LIST_ITEMS);
374
+
375
+ const parts = [];
376
+ if (titleLine) parts.push(asSentence(titleLine.replace(/^[^\p{L}\p{N}#]+/gu, '')));
377
+ if (baselineLine) parts.push(asSentence(baselineLine));
378
+ if (noDealsLine && items.length === 0) parts.push(asSentence(noDealsLine));
379
+ if (items.length) parts.push(`Top deals: ${items.join('; ')}.`);
380
+
381
+ return truncateText(parts.filter(Boolean).join(' '), MAX_DELIVERY_CHARS);
382
+ }
383
+
384
+ function isItemLine(line) {
385
+ return /^(?:[-*•]\s+|\d+[.)]\s+|#\d+\b)/.test(line);
386
+ }
387
+
388
+ function summarizeStructuredText(text) {
389
+ if (looksLikeGunbrokerReport(text)) {
390
+ return summarizeGunbrokerReport(text);
391
+ }
392
+
393
+ const lines = prepareLines(text);
394
+ if (!lines.length) return null;
395
+
396
+ const itemLines = lines.filter(isItemLine);
397
+ const nonItemLines = lines.filter(line => !isItemLine(line));
398
+
399
+ if (itemLines.length > 0) {
400
+ const heading = nonItemLines[0] || null;
401
+ const context = nonItemLines.slice(1, 3).join(' ');
402
+ const highlights = itemLines
403
+ .slice(0, MAX_LIST_ITEMS)
404
+ .map(line => shortenFragment(line.replace(/^[-*•]\s+/, ''), 110))
405
+ .filter(Boolean);
406
+
407
+ const parts = [];
408
+ if (heading) parts.push(asSentence(heading));
409
+ if (context) parts.push(asSentence(truncateText(context, 180)));
410
+ if (highlights.length) parts.push(`Highlights: ${highlights.join('; ')}.`);
411
+ return truncateText(parts.filter(Boolean).join(' '), MAX_DELIVERY_CHARS);
412
+ }
413
+
414
+ const compact = truncateText(lines.slice(0, 4).join(' '), 260);
415
+ return compact ? asSentence(compact) : null;
416
+ }
417
+
418
+ function summarizeProse(text) {
419
+ const normalized = prepareLines(text).join(' ').replace(/\s+/g, ' ').trim();
420
+ if (!normalized || isGenericOrTrivial(normalized)) return null;
421
+
422
+ const sentences = splitSentences(normalized);
423
+ if (!sentences.length) return truncateText(normalized, MAX_DELIVERY_CHARS);
424
+ if (normalized.length <= MAX_DELIVERY_CHARS && sentences.length <= MAX_DELIVERY_SENTENCES) {
425
+ return normalized;
426
+ }
427
+
428
+ const kept = [];
429
+ let chars = 0;
430
+ for (const sentence of sentences) {
431
+ const next = kept.length ? chars + 1 + sentence.length : chars + sentence.length;
432
+ if (kept.length >= MAX_DELIVERY_SENTENCES || next > MAX_DELIVERY_CHARS) break;
433
+ kept.push(sentence);
434
+ chars = next;
435
+ }
436
+
437
+ if (!kept.length) return truncateText(normalized, MAX_DELIVERY_CHARS);
438
+ return kept.join(' ');
439
+ }
440
+
441
+ function countTechnicalKeywordHits(text) {
442
+ const normalized = normalizeCompletionText(text);
443
+ if (!normalized) return 0;
444
+ const matches = cleanMarkdown(normalized).match(TECHNICAL_KEYWORD_RE) || [];
445
+ return new Set(matches.map(match => match.toLowerCase())).size;
446
+ }
447
+
448
+ function looksTechnicalCompletionSummary(rawText, summarizedText = null) {
449
+ const raw = normalizeCompletionText(rawText);
450
+ if (!raw) return false;
451
+
452
+ const cleaned = cleanMarkdown(raw).replace(/\s+/g, ' ').trim();
453
+ if (!cleaned) return false;
454
+
455
+ const hasDelimiter = /[;|]/.test(cleaned) || /\n[-*#]/.test(raw);
456
+ const hardTechnical = TECHNICAL_COMMIT_PREFIX_RE.test(cleaned)
457
+ || FILE_CONTEXT_PREFIX_RE.test(cleaned)
458
+ || CODEISH_MARKER_RE.test(cleaned)
459
+ || hasDelimiter;
460
+
461
+ let technicalScore = 0;
462
+ if (TECHNICAL_COMMIT_PREFIX_RE.test(cleaned)) technicalScore += 3;
463
+ if (FILE_CONTEXT_PREFIX_RE.test(cleaned)) technicalScore += 3;
464
+ if (CODEISH_MARKER_RE.test(cleaned)) technicalScore += 2;
465
+ if (hasDelimiter) technicalScore += 1;
466
+ technicalScore += Math.min(2, countTechnicalKeywordHits(cleaned));
467
+ if (/\b(?:summary_human|deliveryText|details_technical|sessionKey|idempotencyKey)\b/.test(cleaned)) {
468
+ technicalScore += 2;
469
+ }
470
+
471
+ let humanScore = 0;
472
+ if (HUMAN_CUE_RE.test(cleaned)) humanScore += 2;
473
+ if (/\b(?:people|users?|operators?)\b/i.test(cleaned)) humanScore += 1;
474
+ if (/\b(?:now|because|so that|helps?|prevents?|avoids?|keeps?|lets?|allows?)\b/i.test(cleaned)) humanScore += 1;
475
+ if (summarizedText && summarizeProse(summarizedText) && !TECHNICAL_COMMIT_PREFIX_RE.test(cleaned) && !FILE_CONTEXT_PREFIX_RE.test(cleaned)) {
476
+ humanScore += 1;
477
+ }
478
+
479
+ if (!hardTechnical) {
480
+ return technicalScore >= 5 && technicalScore > humanScore + 2;
481
+ }
482
+
483
+ return technicalScore >= 3 && technicalScore > humanScore + 1;
484
+ }
485
+
486
+ function replaceTechnicalPhrases(text) {
487
+ if (!text) return text;
488
+ return text
489
+ .replace(/\bsummary_human\b/gi, 'human summary')
490
+ .replace(/\bsummaryHuman\b/g, 'human summary')
491
+ .replace(/\bdeliveryText\b/g, 'delivery text')
492
+ .replace(/\bdetails_technical\b/gi, 'technical details')
493
+ .replace(/\bresolveCompletionDelivery\b/g, 'completion delivery logic')
494
+ .replace(/\bbuildTerminalCompletionPayload\b/g, 'completion payload builder')
495
+ .replace(/\bstructured completion summary\b/gi, 'the structured summary')
496
+ .replace(/\bstructured completion\b(?!\s+summary)\b/gi, 'structured completion data')
497
+ .replace(/\bcompletion delivery\b/gi, 'how the final completion message is delivered')
498
+ .replace(/\bdelivery path\b/gi, 'delivery flow')
499
+ .replace(/\bwatcher path\b/gi, 'completion watcher flow')
500
+ .replace(/\bwatcher\b/gi, 'completion watcher')
501
+ .replace(/\bstdout\b/gi, 'command output')
502
+ .replace(/\bstderr\b/gi, 'error output')
503
+ .replace(/\bpayload\b/gi, 'result payload')
504
+ .replace(/\braw transcript\b/gi, 'raw completion text')
505
+ .replace(/\bdouble[- ]delivery\b/gi, 'duplicate completion messages')
506
+ .replace(/\bsingle final user-facing completion\b/gi, 'one final completion message')
507
+ .replace(/\bsummary fallback\b/gi, 'fallback summary');
508
+ }
509
+
510
+ function humanizeCamelToken(token) {
511
+ if (!/[a-z][A-Z]/.test(token) && !token.includes('_')) return token;
512
+ return token
513
+ .replace(/_/g, ' ')
514
+ .replace(/([a-z0-9])([A-Z])/g, '$1 $2')
515
+ .toLowerCase();
516
+ }
517
+
518
+ function cleanTechnicalFragment(text) {
519
+ const normalized = normalizeCompletionText(text);
520
+ if (!normalized) return null;
521
+
522
+ let cleaned = cleanMarkdown(normalized)
523
+ .replace(TECHNICAL_COMMIT_PREFIX_RE, '')
524
+ .replace(FILE_CONTEXT_PREFIX_RE, '')
525
+ .replace(/\b(?:[A-Za-z0-9_.-]+\/)+([A-Za-z0-9_.-]+\.(?:[cm]?[jt]sx?|json|md|py|sh|sql|ya?ml|toml))\b/g, '$1')
526
+ .replace(/\b([a-z][A-Za-z0-9_]*[A-Z][A-Za-z0-9_]*)\b/g, (_, token) => humanizeCamelToken(token))
527
+ .replace(/\(\)/g, '')
528
+ .replace(/^[:\-–—]+\s*/, '')
529
+ .replace(/^and\s+/i, '')
530
+ .replace(/^then\s+/i, '')
531
+ .replace(/\s+/g, ' ')
532
+ .trim();
533
+
534
+ cleaned = replaceTechnicalPhrases(cleaned)
535
+ .replace(/\s+,/g, ',')
536
+ .replace(/\s+\./g, '.')
537
+ .replace(/[;,:]+$/g, '')
538
+ .trim();
539
+
540
+ return cleaned || null;
541
+ }
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
+
644
+ function isTestOrValidationFragment(fragment) {
645
+ const cleaned = normalizeCompletionText(fragment);
646
+ if (!cleaned) return false;
647
+ return TEST_FRAGMENT_RE.test(cleaned)
648
+ && !/\b(?:human(?:-|\s)?readable|summary|summaries|message|report|chat|duplicate|user-facing)\b/i.test(cleaned);
649
+ }
650
+
651
+ function toPastTenseFragment(fragment) {
652
+ let text = cleanTechnicalFragment(fragment);
653
+ if (!text) return null;
654
+
655
+ const rewrites = [
656
+ [/^clean up\b/i, 'cleaned up'],
657
+ [/^normalize\b/i, 'cleaned up'],
658
+ [/^humani[sz]e\b/i, 'made more readable'],
659
+ [/^rewrite\b/i, 'reworked'],
660
+ [/^refactor\b/i, 'refined'],
661
+ [/^fix\b/i, 'fixed'],
662
+ [/^update\b/i, 'updated'],
663
+ [/^adjust\b/i, 'adjusted'],
664
+ [/^change\b/i, 'updated'],
665
+ [/^improve\b/i, 'improved'],
666
+ [/^add\b/i, 'added'],
667
+ [/^introduce\b/i, 'introduced'],
668
+ [/^enable\b/i, 'enabled'],
669
+ [/^support\b/i, 'supported'],
670
+ [/^preserve\b/i, 'kept'],
671
+ [/^keep\b/i, 'kept'],
672
+ [/^retain\b/i, 'retained'],
673
+ [/^prevent\b/i, 'prevented'],
674
+ [/^avoid\b/i, 'avoided'],
675
+ [/^stop\b/i, 'stopped'],
676
+ [/^pass through\b/i, 'passed through'],
677
+ [/^leave\b/i, 'left'],
678
+ [/^prefer\b/i, 'preferred'],
679
+ [/^wire\b/i, 'wired up'],
680
+ [/^route\b/i, 'routed'],
681
+ [/^surface\b/i, 'surfaced'],
682
+ [/^append\b/i, 'appended'],
683
+ [/^format\b/i, 'formatted'],
684
+ [/^teach\b/i, 'taught'],
685
+ [/^ensure\b/i, 'ensured'],
686
+ [/^dedupe\b/i, 'deduplicated'],
687
+ ];
688
+
689
+ for (const [pattern, replacement] of rewrites) {
690
+ if (pattern.test(text)) {
691
+ text = text.replace(pattern, replacement);
692
+ break;
693
+ }
694
+ }
695
+
696
+ text = text.replace(/\s+/g, ' ').trim();
697
+ return text ? upperFirst(text) : null;
698
+ }
699
+
700
+ function extractTechnicalFragments(text) {
701
+ const normalized = normalizeCompletionText(text);
702
+ if (!normalized) return [];
703
+
704
+ const fragments = prepareLines(normalized)
705
+ .flatMap(line => line.split(/\s*;\s*|\s+\|\s+|\s+&&\s+|\s+->\s+|\s+=>\s+/))
706
+ .map(fragment => fragment.trim())
707
+ .filter(Boolean);
708
+
709
+ return fragments.map(cleanTechnicalFragment).filter(Boolean);
710
+ }
711
+
712
+ function detectTechnicalThemes(rawText, fragments = []) {
713
+ const raw = cleanMarkdown(normalizeCompletionText(rawText) || '').toLowerCase();
714
+ const combined = [raw, fragments.join(' ').toLowerCase()].filter(Boolean).join(' ');
715
+ return {
716
+ completionFlow: /\b(?:completion|deliver(?:y|ed)?|notification|message|report|chat|summary|summaries|human(?:-|\s)?readable|plain english|user-facing)\b/.test(combined),
717
+ detailSeparation: /\b(?:technical details?|details_technical|debug|underneath|below|separate|separated|split|move|moved)\b/.test(combined),
718
+ duplicatePrevention: /\b(?:duplicate|double[- ]delivery|single final|dedupe|deduplicat|one clean)\b/.test(combined),
719
+ contextPreservation: /\b(?:structured|context|preserve|kept|retain|survive)\b/.test(combined),
720
+ reliability: RELIABILITY_SIGNAL_RE.test(combined),
721
+ addedBehavior: /\b(?:add|added|introduce|introduced|support|supported|enable|enabled)\b/.test(combined),
722
+ testOnly: fragments.length > 0 && fragments.every(isTestOrValidationFragment),
723
+ };
724
+ }
725
+
726
+ function isPlainEnglishLeadText(text) {
727
+ const normalized = normalizeCompletionText(text);
15
728
  if (!normalized) return false;
16
- if (GENERIC_COMPLETION_TEXT_RE.test(normalized)) return false;
17
- if (TRIVIAL_CHATTER_RE.test(normalized)) return false;
729
+ if (FILE_CONTEXT_PREFIX_RE.test(normalized)) return false;
730
+ if (CODEISH_MARKER_RE.test(normalized)) return false;
731
+ if (/\b(?:summary(?:_human|Human)?|deliveryText|details_technical|sessionKey|idempotencyKey|payload-precedence|raw summary|watcher path|completion delivery)\b/i.test(normalized)) {
732
+ return false;
733
+ }
734
+ return !looksTechnicalCompletionSummary(normalized, normalized);
735
+ }
736
+
737
+ function buildCompletionLeadFromThemes(themes) {
738
+ const sentences = [];
739
+ if (themes.duplicatePrevention) {
740
+ sentences.push('Final completion updates now arrive as one clean plain-English summary.');
741
+ sentences.push('That makes the result easier to scan and avoids noisy repeat messages.');
742
+ } else {
743
+ sentences.push('Final completion updates now start with a short plain-English summary.');
744
+ if (themes.contextPreservation) {
745
+ sentences.push('That makes the result easier to read without hiding the useful detail.');
746
+ } else {
747
+ sentences.push('That makes the result easier to scan without hiding the useful detail.');
748
+ }
749
+ }
750
+ sentences.push('Future runs should show the clean summary first, with technical details underneath when needed.');
751
+ return truncateText(sentences.join(' '), MAX_DELIVERY_CHARS);
752
+ }
753
+
754
+ function buildHumanizedTechnicalSummary(rawText, fallbackSummary) {
755
+ const cleanedRaw = normalizeCompletionText(rawText);
756
+ const fallback = normalizeCompletionText(fallbackSummary);
757
+ if (!cleanedRaw) return fallback;
758
+
759
+ const fragments = extractTechnicalFragments(cleanedRaw);
760
+ const primaryFragments = fragments.filter(fragment => !isTestOrValidationFragment(fragment));
761
+ const sourceFragments = primaryFragments.length > 0 ? primaryFragments : fragments;
762
+ const themes = detectTechnicalThemes(cleanedRaw, fragments);
763
+
764
+ if (themes.completionFlow) {
765
+ return buildCompletionLeadFromThemes(themes);
766
+ }
767
+
768
+ const chosen = sourceFragments
769
+ .slice(0, 2)
770
+ .map(fragment => toPastTenseFragment(fragment))
771
+ .filter(Boolean)
772
+ .filter(fragment => isPlainEnglishLeadText(fragment));
773
+
774
+ let actionSummary;
775
+ if (chosen.length === 1) {
776
+ actionSummary = asSentence(chosen[0]);
777
+ } else if (chosen.length >= 2) {
778
+ actionSummary = `${chosen[0]} and ${lowerFirst(chosen[1])}.`;
779
+ } else if (fallback && isPlainEnglishLeadText(fallback)) {
780
+ actionSummary = asSentence(fallback);
781
+ } else if (themes.testOnly) {
782
+ actionSummary = 'Added focused coverage for the weak spot.';
783
+ } else if (themes.reliability) {
784
+ actionSummary = 'The requested fix is in place.';
785
+ } else if (themes.addedBehavior) {
786
+ actionSummary = 'The requested behavior is now in place.';
787
+ } else {
788
+ actionSummary = 'The update is in place.';
789
+ }
790
+
791
+ const extras = [];
792
+ if (themes.testOnly) {
793
+ extras.push('That makes the behavior easier to trust.');
794
+ extras.push('Future regressions should get caught quickly.');
795
+ } else if (themes.reliability) {
796
+ extras.push('That should make the workflow more reliable.');
797
+ extras.push('Future runs should be less likely to hit the same problem.');
798
+ } else if (themes.addedBehavior) {
799
+ extras.push('That makes the new behavior available without extra follow-up.');
800
+ extras.push('Future runs should use it automatically.');
801
+ } else {
802
+ extras.push('That should make the result easier to work with.');
803
+ extras.push('Future runs should reflect the change automatically.');
804
+ }
805
+
806
+ return truncateText([actionSummary, ...extras].filter(Boolean).join(' '), MAX_DELIVERY_CHARS);
807
+ }
808
+
809
+ export function humanizeCompletionText(value) {
810
+ const raw = normalizeCompletionText(value);
811
+ if (!raw) return null;
812
+
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);
824
+ if (!summarized) return null;
825
+ if (!looksTechnicalCompletionSummary(summarySource, summarized)) return stripHumanSummaryLabel(summarized) || summarized;
826
+
827
+ return buildHumanizedTechnicalSummary(summarySource, summarized) || summarized;
828
+ }
829
+
830
+ function summarizeChecklistTechnicalDetails(checklist, sha) {
831
+ const normalizedChecklist = cloneChecklist(checklist);
832
+ if (!normalizedChecklist) return null;
833
+
834
+ const parts = [];
835
+ if (normalizedChecklist.tests_passed === true) parts.push('tests passed');
836
+ if (normalizedChecklist.pushed === true) {
837
+ const short = shortSha(sha);
838
+ parts.push(short ? `pushed ${short}` : 'changes pushed');
839
+ } else if (sha) {
840
+ parts.push(`commit ${shortSha(sha)}`);
841
+ }
842
+
843
+ const extraTrueFlags = Object.entries(normalizedChecklist)
844
+ .filter(([key, flagValue]) => flagValue === true && !['work_complete', 'tests_passed', 'pushed'].includes(key))
845
+ .map(([key]) => key.replace(/_/g, ' '));
846
+
847
+ if (extraTrueFlags.length > 0) {
848
+ parts.push(`checks: ${extraTrueFlags.slice(0, 3).join(', ')}`);
849
+ }
850
+
851
+ return parts.length > 0 ? `Checks: ${parts.join('; ')}.` : null;
852
+ }
853
+
854
+ function buildTechnicalDetailsText({ rawText, summaryText, completion } = {}) {
855
+ const raw = normalizeCompletionText(rawText);
856
+ const summary = normalizeCompletionText(summaryText);
857
+ const details = getCompletionTechnicalDetails(completion);
858
+ const parts = [];
859
+
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
+ );
870
+ if (rawTechnical) {
871
+ parts.push(truncateText(rawTechnicalSource, 260));
872
+ }
873
+
874
+ let completionDetailsAreTechnical = false;
875
+ if (typeof details === 'string') {
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
+ );
882
+ if (normalized
883
+ && !isInternalTransportNoiseText(normalized)
884
+ && (completionDetailsAreTechnical || rawTechnical)
885
+ && (!rawTechnical || normalized !== rawTechnicalSource)) {
886
+ parts.push(truncateText(normalized, 220));
887
+ }
888
+ } else if (details && typeof details === 'object') {
889
+ const rawSummary = normalizeCompletionText(details.raw_summary);
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)
898
+ && (completionDetailsAreTechnical || rawTechnical)
899
+ && (!rawTechnical || technicalSummary !== rawTechnicalSource)) {
900
+ parts.push(truncateText(technicalSummary, 220));
901
+ }
902
+ }
903
+
904
+ const checklistDetails = summarizeChecklistTechnicalDetails(completion?.checklist, completion?.sha);
905
+ if (checklistDetails && (rawTechnical || completionDetailsAreTechnical)) parts.push(checklistDetails);
906
+
907
+ const unique = [];
908
+ const seen = new Set();
909
+ for (const part of parts) {
910
+ const normalized = normalizeTechnicalDetailLine(part) || normalizeCompletionText(part);
911
+ if (!normalized) continue;
912
+ const key = normalized.toLowerCase();
913
+ if (seen.has(key)) continue;
914
+ seen.add(key);
915
+ unique.push(normalized);
916
+ }
917
+
918
+ return unique;
919
+ }
920
+
921
+ function composeDeliveryText(summaryText, technicalDetailsText = null) {
922
+ const summarySections = extractStructuredSummarySections(summaryText);
923
+ const summary = stripHumanSummaryLabel(summarySections?.summary || summaryText);
924
+ if (!summary) return null;
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
+
942
+ if (technicalLines.length > 0) {
943
+ return `${summary}\n\nTechnical details:\n- ${technicalLines.join('\n- ')}`;
944
+ }
945
+ return summary;
946
+ }
947
+
948
+ export function summarizeCompletionText(value, { skipEmbeddedObject = false } = {}) {
949
+ const raw = normalizeCompletionText(value);
950
+ if (!raw) return null;
951
+
952
+ const passThroughReport = getPassThroughHumanFinalReport(raw);
953
+ if (passThroughReport) return passThroughReport;
954
+
955
+ if (!skipEmbeddedObject) {
956
+ const parsed = extractEmbeddedCompletionObject(raw);
957
+ if (parsed !== null) {
958
+ const candidates = gatherObjectTextCandidates(parsed);
959
+ for (const candidate of candidates) {
960
+ const summarized = summarizeCompletionText(candidate, { skipEmbeddedObject: true });
961
+ if (summarized) return summarized;
962
+ }
963
+ if (looksLikeRawPayloadText(raw)) return null;
964
+ }
965
+ }
966
+
967
+ if (looksLikeRawPayloadText(raw)) return null;
968
+ if (looksLikeGunbrokerReport(raw)) return summarizeGunbrokerReport(raw);
969
+
970
+ const prepared = prepareLines(raw);
971
+ const structured = prepared.length >= 4 || prepared.some(line => line.includes('|')) || prepared.filter(isItemLine).length >= 2;
972
+ if (structured) {
973
+ const summary = summarizeStructuredText(raw);
974
+ if (summary && !isGenericOrTrivial(summary)) return summary;
975
+ }
976
+
977
+ return summarizeProse(raw);
978
+ }
979
+
980
+ export function isMeaningfulCompletionText(value) {
981
+ return Boolean(summarizeCompletionText(value));
982
+ }
983
+
984
+ function getCompletionSummaryHuman(completion) {
985
+ return normalizeCompletionText(completion?.summary_human ?? completion?.summaryHuman);
986
+ }
987
+
988
+ function getCompletionTechnicalDetails(completion) {
989
+ const details = completion?.details_technical ?? completion?.technicalDetails ?? null;
990
+ if (!details) return null;
991
+ if (typeof details === 'string') return normalizeCompletionText(details);
992
+ if (typeof details !== 'object' || Array.isArray(details)) return null;
993
+ return Object.keys(details).length > 0 ? details : null;
994
+ }
18
995
 
19
- const words = normalized.split(/\s+/).filter(Boolean);
20
- if (words.length === 1) return false;
996
+ export function hasCompletionSignal(completion) {
997
+ if (!completion || typeof completion !== 'object' || Array.isArray(completion)) return false;
998
+ if (getCompletionSummaryHuman(completion)) return true;
999
+ if (normalizeCompletionText(completion?.summary)) return true;
1000
+ if (normalizeCompletionText(completion?.deliveryText)) return true;
1001
+ if (normalizeCompletionText(completion?.sha)) return true;
1002
+ if (getCompletionTechnicalDetails(completion)) return true;
1003
+ return !!cloneChecklist(completion?.checklist);
1004
+ }
21
1005
 
22
- return true;
1006
+ function buildInternalNoiseFallback(noisyTexts) {
1007
+ const joined = noisyTexts.join(' ').toLowerCase();
1008
+ if (joined.includes('session went idle without calling done')) {
1009
+ return 'The session ended without a final completion signal, so no reliable final report is available.';
1010
+ }
1011
+ if (joined.includes('session not found') || joined.includes('session never found')) {
1012
+ return 'The session ended before a final report could be retrieved, so there is no user-facing completion message to deliver.';
1013
+ }
1014
+ return 'The session completed with internal transport status only and no user-facing completion report.';
23
1015
  }
24
1016
 
25
1017
  function shortSha(sha) {
@@ -37,6 +1029,194 @@ function cloneChecklist(checklist) {
37
1029
  }
38
1030
  }
39
1031
 
1032
+ function hasNegatedCommandContext(text, matchIndex) {
1033
+ const before = text.slice(Math.max(0, matchIndex - 40), matchIndex);
1034
+ return /\b(?:do\s+not|don't|dont|never)\s+(?:use|run|call|invoke)?\s*$/i.test(before)
1035
+ || /\bavoid\s+(?:using\s+)?$/i.test(before)
1036
+ || /\bwithout\s+(?:using\s+)?$/i.test(before);
1037
+ }
1038
+
1039
+ export function taskRequiresGitSha(taskPrompt) {
1040
+ if (!taskPrompt || typeof taskPrompt !== 'string') return false;
1041
+
1042
+ const commandPattern = /\bgit\s+(push|rebase|cherry-pick)\b|(?:^|\s)--force-with-lease\b|(?:^|\s)--force-push\b/ig;
1043
+ let match;
1044
+ while ((match = commandPattern.exec(taskPrompt)) !== null) {
1045
+ if (!hasNegatedCommandContext(taskPrompt, match.index)) return true;
1046
+ }
1047
+ return false;
1048
+ }
1049
+
1050
+ export function inferTaskCompletionRequirements(taskPrompt) {
1051
+ const text = typeof taskPrompt === 'string' ? taskPrompt : '';
1052
+ const pushForbidden = PUSH_FORBIDDEN_RE.test(text);
1053
+ const pushRequired = !pushForbidden && taskRequiresGitSha(text);
1054
+ const testsApplicable = TEST_APPLICABILITY_RE.test(text) && !TEST_NEGATION_RE.test(text);
1055
+
1056
+ return {
1057
+ pushRequired,
1058
+ pushForbidden,
1059
+ testsApplicable,
1060
+ };
1061
+ }
1062
+
1063
+ export function buildCompletionChecklistExample(taskPromptOrRequirements) {
1064
+ const requirements = typeof taskPromptOrRequirements === 'string' || taskPromptOrRequirements == null
1065
+ ? inferTaskCompletionRequirements(taskPromptOrRequirements)
1066
+ : taskPromptOrRequirements;
1067
+
1068
+ const example = { work_complete: true };
1069
+ if (requirements?.testsApplicable) example.tests_passed = true;
1070
+ if (requirements?.pushRequired) example.pushed = true;
1071
+ return JSON.stringify(example);
1072
+ }
1073
+
1074
+ export function buildCompletionSignalInstructions({ label, taskPrompt, doneScriptPath } = {}) {
1075
+ const escapedLabel = String(label || '').replace(/'/g, "'\\''");
1076
+ const requirements = inferTaskCompletionRequirements(taskPrompt);
1077
+ const checklistExample = buildCompletionChecklistExample(requirements);
1078
+
1079
+ const readinessChecks = [
1080
+ 'All file edits are saved',
1081
+ requirements.testsApplicable
1082
+ ? 'Any required tests / validation for this task have passed'
1083
+ : 'Any required validation for this task is complete',
1084
+ 'All required API calls / external follow-up actions are done',
1085
+ ];
1086
+
1087
+ if (requirements.pushRequired) {
1088
+ readinessChecks.push('Any required git push / history-editing step for this task is complete');
1089
+ }
1090
+
1091
+ readinessChecks.push('You have verified the work is complete');
1092
+
1093
+ const lines = [];
1094
+ lines.push('COMPLETION SIGNAL -- READ CAREFULLY:');
1095
+ lines.push('');
1096
+ lines.push('Only call this command after ALL of the following are true:');
1097
+ readinessChecks.forEach((line, idx) => lines.push(` ${idx + 1}. ${line}`));
1098
+ lines.push('');
1099
+ lines.push('Call this as your ABSOLUTE FINAL action -- nothing else runs after this:');
1100
+ lines.push(` node '${doneScriptPath}' done --label '${escapedLabel}' \\`);
1101
+ lines.push(' --summary "<human-readable summary of what you actually did>" \\');
1102
+ lines.push(` --checklist '${checklistExample}' \\`);
1103
+ lines.push(' [--sha "<git commit SHA if applicable>"]');
1104
+ lines.push('');
1105
+ lines.push('Checklist rules:');
1106
+ lines.push(' - work_complete MUST be true -- you are asserting you have finished ALL assigned work');
1107
+ lines.push(' - Only include tests_passed if validation/testing was actually required for this task');
1108
+ lines.push(' - Only include pushed:true if this task required a pushed git change');
1109
+
1110
+ if (requirements.pushForbidden) {
1111
+ lines.push(' - This task explicitly says not to push -- do not wait on a push or set pushed:true before calling done');
1112
+ }
1113
+
1114
+ lines.push('If this task required a pushed git change, --sha is required and must be the actual pushed commit SHA. The done script will reject invented or placeholder SHAs.');
1115
+ lines.push('Do NOT call done while planning, reading files, or mid-task.');
1116
+
1117
+ if (requirements.pushRequired) {
1118
+ lines.push('If the required push for this task has not happened yet, you are not done.');
1119
+ }
1120
+
1121
+ return lines.join('\n');
1122
+ }
1123
+
1124
+ function extractTextSegments(content) {
1125
+ if (typeof content === 'string') return [content];
1126
+ if (!content || typeof content !== 'object') return [];
1127
+
1128
+ if (Array.isArray(content)) {
1129
+ return content.flatMap(part => {
1130
+ if (typeof part === 'string') return [part];
1131
+ if (part && typeof part.text === 'string') return [part.text];
1132
+ return [];
1133
+ });
1134
+ }
1135
+
1136
+ if (typeof content.text === 'string') return [content.text];
1137
+ return [];
1138
+ }
1139
+
1140
+ function entryHasToolUse(entry) {
1141
+ if (!entry || typeof entry !== 'object') return false;
1142
+ if (entry.type === 'tool_use') return true;
1143
+ if (!Array.isArray(entry.content)) return false;
1144
+ return entry.content.some(part => part?.type === 'tool_use');
1145
+ }
1146
+
1147
+ function entryHasToolResult(entry) {
1148
+ if (!entry || typeof entry !== 'object') return false;
1149
+ if (entry.type === 'tool_result') return true;
1150
+ if (!Array.isArray(entry.content)) return false;
1151
+ return entry.content.some(part => part?.type === 'tool_result');
1152
+ }
1153
+
1154
+ function extractEntryText(entry) {
1155
+ if (!entry || typeof entry !== 'object') return null;
1156
+ return normalizeCompletionText(extractTextSegments(entry.content).join('').trim());
1157
+ }
1158
+
1159
+ export function extractLastMeaningfulAssistantReplyFromEntries(entries) {
1160
+ if (!Array.isArray(entries)) return null;
1161
+
1162
+ for (let i = entries.length - 1; i >= 0; i--) {
1163
+ const entry = entries[i];
1164
+ if (entry?.role !== 'assistant') continue;
1165
+ if (entryHasToolUse(entry)) continue;
1166
+
1167
+ const text = extractEntryText(entry);
1168
+ if (isMeaningfulCompletionText(text)) return text;
1169
+ }
1170
+
1171
+ return null;
1172
+ }
1173
+
1174
+ export function extractTerminalAssistantReplyFromEntries(entries) {
1175
+ if (!Array.isArray(entries) || entries.length === 0) return null;
1176
+
1177
+ for (let i = entries.length - 1; i >= 0; i--) {
1178
+ const entry = entries[i];
1179
+ if (!entry || typeof entry !== 'object') continue;
1180
+
1181
+ if (entry.role === 'assistant') {
1182
+ if (entryHasToolUse(entry)) return null;
1183
+
1184
+ const text = extractEntryText(entry);
1185
+ if (!text) continue;
1186
+ if (!isMeaningfulCompletionText(text)) return null;
1187
+
1188
+ const stopReason = entry.stop_reason ?? entry.stopReason ?? null;
1189
+ return stopReason === 'end_turn' ? text : null;
1190
+ }
1191
+
1192
+ if (entry.role === 'user') {
1193
+ if (entryHasToolResult(entry)) return null;
1194
+ return null;
1195
+ }
1196
+ }
1197
+
1198
+ return null;
1199
+ }
1200
+
1201
+ function buildTechnicalDetails({ checklist, sha, rawSummary, summaryHuman } = {}) {
1202
+ const details = {};
1203
+ const normalizedChecklist = cloneChecklist(checklist);
1204
+ const normalizedSha = normalizeCompletionText(sha);
1205
+ const normalizedRawSummary = normalizeCompletionText(rawSummary);
1206
+ const normalizedSummaryHuman = normalizeCompletionText(summaryHuman);
1207
+
1208
+ if (normalizedChecklist) details.checklist = normalizedChecklist;
1209
+ if (normalizedSha) {
1210
+ details.sha = normalizedSha;
1211
+ details.sha_short = shortSha(normalizedSha);
1212
+ }
1213
+ if (normalizedRawSummary && normalizedRawSummary !== normalizedSummaryHuman) {
1214
+ details.raw_summary = normalizedRawSummary;
1215
+ }
1216
+
1217
+ return Object.keys(details).length > 0 ? details : null;
1218
+ }
1219
+
40
1220
  export function synthesizeCompletionReply({ checklist, sha } = {}) {
41
1221
  const normalizedChecklist = cloneChecklist(checklist);
42
1222
  const short = shortSha(sha);
@@ -56,7 +1236,7 @@ export function synthesizeCompletionReply({ checklist, sha } = {}) {
56
1236
 
57
1237
  const extraTrueFlags = normalizedChecklist
58
1238
  ? Object.entries(normalizedChecklist)
59
- .filter(([key, value]) => value === true && !['work_complete', 'tests_passed', 'pushed'].includes(key))
1239
+ .filter(([key, flagValue]) => flagValue === true && !['work_complete', 'tests_passed', 'pushed'].includes(key))
60
1240
  .map(([key]) => key.replace(/_/g, ' '))
61
1241
  : [];
62
1242
 
@@ -72,64 +1252,162 @@ export function buildTerminalCompletionPayload({ summary, checklist, sha } = {})
72
1252
  const rawSummary = normalizeCompletionText(summary);
73
1253
  const normalizedChecklist = cloneChecklist(checklist);
74
1254
  const normalizedSha = normalizeCompletionText(sha);
75
- const prose = isMeaningfulCompletionText(rawSummary) ? rawSummary : null;
76
- const synthesizedReply = prose
1255
+ const normalizedSummary = humanizeCompletionText(rawSummary);
1256
+ const synthesizedReply = normalizedSummary
77
1257
  ? null
78
1258
  : synthesizeCompletionReply({ checklist: normalizedChecklist, sha: normalizedSha });
79
- const effectiveSummary = prose || synthesizedReply || rawSummary || null;
80
- const deliveryText = prose || synthesizedReply || null;
1259
+ const summaryHuman = normalizedSummary || synthesizedReply || null;
1260
+ const effectiveSummary = summaryHuman || rawSummary || null;
1261
+ const deliveryText = summaryHuman || null;
1262
+ const detailsTechnical = buildTechnicalDetails({
1263
+ checklist: normalizedChecklist,
1264
+ sha: normalizedSha,
1265
+ rawSummary,
1266
+ summaryHuman,
1267
+ });
81
1268
 
82
1269
  return {
83
- version: 1,
1270
+ version: 2,
84
1271
  recordedAt: new Date().toISOString(),
1272
+ summary_human: summaryHuman,
85
1273
  summary: effectiveSummary,
1274
+ details_technical: detailsTechnical,
86
1275
  deliveryText,
87
- prose,
1276
+ prose: normalizedSummary,
88
1277
  checklist: normalizedChecklist,
89
1278
  sha: normalizedSha,
90
1279
  debug: {
91
1280
  rawSummary,
1281
+ normalizedSummary,
92
1282
  synthesizedReply,
93
- deliverySource: prose ? 'summary' : synthesizedReply ? 'synthesized' : 'none',
1283
+ deliverySource: normalizedSummary ? 'summary_human' : synthesizedReply ? 'technical-synthesis' : 'none',
94
1284
  },
95
1285
  };
96
1286
  }
97
1287
 
98
1288
  export function resolveCompletionDelivery({ lastReply, completion, fallbackSummary } = {}) {
99
- const reply = normalizeCompletionText(lastReply);
100
- const completionSummary = normalizeCompletionText(completion?.summary);
101
- const completionDelivery = normalizeCompletionText(completion?.deliveryText);
102
- const fallback = normalizeCompletionText(fallbackSummary);
103
- const preferredSummary = completionSummary || fallback;
104
- const meaningfulSummary = [completionSummary, fallback].find(isMeaningfulCompletionText) || null;
1289
+ const rawReply = normalizeCompletionText(lastReply);
1290
+ const rawCompletionSummaryHuman = getCompletionSummaryHuman(completion);
1291
+ const rawCompletionSummary = normalizeCompletionText(completion?.summary);
1292
+ const rawCompletionDelivery = normalizeCompletionText(completion?.deliveryText);
1293
+ const rawFallback = normalizeCompletionText(fallbackSummary);
105
1294
 
106
- if (isMeaningfulCompletionText(reply)) {
1295
+ const reply = humanizeCompletionText(lastReply);
1296
+ const completionSummaryHuman = humanizeCompletionText(rawCompletionSummaryHuman);
1297
+ const completionSummary = humanizeCompletionText(completion?.summary);
1298
+ const completionDelivery = humanizeCompletionText(completion?.deliveryText);
1299
+ const fallback = humanizeCompletionText(fallbackSummary);
1300
+ const synthesizedFromTechnical = synthesizeCompletionReply({
1301
+ checklist: completion?.checklist,
1302
+ sha: completion?.sha,
1303
+ });
1304
+ const completionDeliverySource = normalizeCompletionText(completion?.debug?.deliverySource);
1305
+ const preferredSummary = completionSummaryHuman || completionSummary || fallback || synthesizedFromTechnical || null;
1306
+ const authoritativeStructuredSummary = completionDeliverySource && completionDeliverySource !== 'technical-synthesis'
1307
+ ? preferredSummary
1308
+ : null;
1309
+ const noisyTexts = [
1310
+ rawReply,
1311
+ rawCompletionSummaryHuman,
1312
+ rawCompletionDelivery,
1313
+ rawCompletionSummary,
1314
+ rawFallback,
1315
+ ].filter(isInternalTransportNoiseText);
1316
+ const isDeliverableText = (rawText, summarizedText) => Boolean(summarizedText)
1317
+ && !isInternalTransportNoiseText(summarizedText)
1318
+ && (!rawText || !isInternalTransportNoiseText(rawText) || looksLikeRawPayloadText(rawText));
1319
+ const structuredCandidates = [
1320
+ {
1321
+ rawText: rawCompletionSummaryHuman,
1322
+ text: completionSummaryHuman,
1323
+ summary: completionSummaryHuman,
1324
+ source: completionDeliverySource === 'technical-synthesis' ? 'technical-synthesis' : 'summary_human',
1325
+ },
1326
+ {
1327
+ rawText: rawCompletionSummary,
1328
+ text: completionSummary,
1329
+ summary: completionSummary,
1330
+ source: completionDeliverySource === 'technical-synthesis' && completionSummary === synthesizedFromTechnical
1331
+ ? 'technical-synthesis'
1332
+ : 'completion-summary',
1333
+ },
1334
+ {
1335
+ rawText: rawCompletionDelivery,
1336
+ text: completionDelivery,
1337
+ summary: completionSummaryHuman || completionSummary || completionDelivery,
1338
+ source: completionDeliverySource || 'completion-legacy',
1339
+ },
1340
+ ];
1341
+
1342
+ for (const candidate of structuredCandidates.filter(candidate => candidate.source !== 'technical-synthesis')) {
1343
+ if (!isDeliverableText(candidate.rawText, candidate.text)) continue;
1344
+ const technicalDetailsText = buildTechnicalDetailsText({
1345
+ rawText: candidate.rawText,
1346
+ summaryText: candidate.text,
1347
+ completion,
1348
+ });
107
1349
  return {
108
- deliveryText: reply,
109
- summary: preferredSummary || reply.slice(0, 500),
1350
+ deliveryText: composeDeliveryText(candidate.text, technicalDetailsText),
1351
+ summary: candidate.summary || candidate.text,
1352
+ source: candidate.source,
1353
+ };
1354
+ }
1355
+
1356
+ if (isDeliverableText(rawReply, reply)) {
1357
+ const technicalDetailsText = buildTechnicalDetailsText({
1358
+ rawText: rawReply,
1359
+ summaryText: reply,
1360
+ completion,
1361
+ });
1362
+ return {
1363
+ deliveryText: composeDeliveryText(reply, technicalDetailsText),
1364
+ summary: authoritativeStructuredSummary || reply,
110
1365
  source: 'lastReply',
111
1366
  };
112
1367
  }
113
1368
 
114
- if (isMeaningfulCompletionText(completionDelivery)) {
1369
+ if (isDeliverableText(synthesizedFromTechnical, synthesizedFromTechnical)) {
1370
+ return {
1371
+ deliveryText: synthesizedFromTechnical,
1372
+ summary: synthesizedFromTechnical,
1373
+ source: 'technical-synthesis',
1374
+ };
1375
+ }
1376
+
1377
+ for (const candidate of structuredCandidates.filter(candidate => candidate.source === 'technical-synthesis')) {
1378
+ if (!isDeliverableText(candidate.rawText, candidate.text)) continue;
1379
+ const technicalDetailsText = buildTechnicalDetailsText({
1380
+ rawText: candidate.rawText,
1381
+ summaryText: candidate.text,
1382
+ completion,
1383
+ });
1384
+ return {
1385
+ deliveryText: composeDeliveryText(candidate.text, technicalDetailsText),
1386
+ summary: candidate.summary || candidate.text,
1387
+ source: candidate.source,
1388
+ };
1389
+ }
1390
+
1391
+ if (isDeliverableText(rawFallback, fallback)) {
115
1392
  return {
116
- deliveryText: completionDelivery,
117
- summary: completionSummary || completionDelivery,
118
- source: completion?.debug?.deliverySource || 'completion',
1393
+ deliveryText: fallback,
1394
+ summary: fallback,
1395
+ source: 'summary',
119
1396
  };
120
1397
  }
121
1398
 
122
- if (meaningfulSummary) {
1399
+ if (noisyTexts.length > 0) {
1400
+ const fallbackDelivery = buildInternalNoiseFallback(noisyTexts);
123
1401
  return {
124
- deliveryText: meaningfulSummary,
125
- summary: meaningfulSummary,
126
- source: completionSummary && meaningfulSummary === completionSummary ? 'completion-summary' : 'summary',
1402
+ deliveryText: fallbackDelivery,
1403
+ summary: fallbackDelivery,
1404
+ source: 'internal-noise',
127
1405
  };
128
1406
  }
129
1407
 
130
1408
  return {
131
1409
  deliveryText: null,
132
- summary: preferredSummary || null,
1410
+ summary: authoritativeStructuredSummary || preferredSummary || completionDelivery || reply || null,
133
1411
  source: 'none',
134
1412
  };
135
1413
  }