openclaw-scheduler 0.2.4 → 0.2.5
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/CHANGELOG.md +14 -0
- package/README.md +16 -6
- package/cli.js +13 -4
- package/dispatch/README.md +18 -3
- package/dispatch/completion.mjs +1035 -34
- package/dispatch/hooks.mjs +17 -5
- package/dispatch/index.mjs +573 -217
- package/dispatch/message-input.mjs +67 -0
- package/dispatch/watcher.mjs +110 -39
- package/dispatcher-strategies.js +121 -20
- package/gateway.js +32 -8
- package/index.d.ts +1 -0
- package/package.json +3 -1
- package/scripts/dispatch-cli-utils.mjs +53 -0
- package/scripts/inbox-watcher-guardrail.mjs +506 -0
package/dispatch/completion.mjs
CHANGED
|
@@ -1,5 +1,39 @@
|
|
|
1
|
-
const
|
|
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;
|
|
3
37
|
|
|
4
38
|
export function normalizeCompletionText(value) {
|
|
5
39
|
if (typeof value !== 'string') return null;
|
|
@@ -7,19 +41,700 @@ export function normalizeCompletionText(value) {
|
|
|
7
41
|
return trimmed ? trimmed : null;
|
|
8
42
|
}
|
|
9
43
|
|
|
10
|
-
|
|
44
|
+
const ANSI_ESCAPE_RE = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, 'g');
|
|
45
|
+
|
|
46
|
+
function stripAnsi(text) {
|
|
47
|
+
return text.replace(ANSI_ESCAPE_RE, '');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function isInternalTransportNoiseText(value) {
|
|
11
51
|
const text = normalizeCompletionText(value);
|
|
12
52
|
if (!text) return false;
|
|
13
53
|
|
|
14
|
-
const normalized = text.
|
|
54
|
+
const normalized = text.replace(/\s+/g, ' ').trim();
|
|
15
55
|
if (!normalized) return false;
|
|
16
|
-
if (
|
|
17
|
-
if (
|
|
56
|
+
if (INTERNAL_TRANSPORT_PREFIX_RE.test(normalized)) return true;
|
|
57
|
+
if (INTERNAL_DONE_PAYLOAD_RE.test(normalized)) return true;
|
|
58
|
+
if (INTERNAL_DONE_MESSAGE_RE.test(normalized)) return true;
|
|
59
|
+
|
|
60
|
+
return INTERNAL_TRANSPORT_PATTERNS.some((pattern) => pattern.test(normalized));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function cleanMarkdown(text) {
|
|
64
|
+
return stripAnsi(text)
|
|
65
|
+
.replace(/\r\n?/g, '\n')
|
|
66
|
+
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1')
|
|
67
|
+
.replace(/`([^`]+)`/g, '$1')
|
|
68
|
+
.replace(/\*\*([^*]+)\*\*/g, '$1')
|
|
69
|
+
.replace(/__([^_]+)__/g, '$1')
|
|
70
|
+
.replace(/^#{1,6}\s+/gm, '')
|
|
71
|
+
.replace(/^>\s?/gm, '');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function isGenericOrTrivial(text) {
|
|
75
|
+
const normalized = normalizeCompletionText(text)?.toLowerCase().replace(/\s+/g, ' ').trim();
|
|
76
|
+
if (!normalized) return true;
|
|
77
|
+
if (GENERIC_COMPLETION_TEXT_RE.test(normalized)) return true;
|
|
78
|
+
if (TRIVIAL_CHATTER_RE.test(normalized)) return true;
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function parseJsonCandidate(value) {
|
|
83
|
+
const trimmed = normalizeCompletionText(value);
|
|
84
|
+
if (!trimmed) return null;
|
|
85
|
+
try {
|
|
86
|
+
return JSON.parse(trimmed);
|
|
87
|
+
} catch {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function extractEmbeddedCompletionObject(text) {
|
|
93
|
+
const normalized = normalizeCompletionText(text);
|
|
94
|
+
if (!normalized) return null;
|
|
95
|
+
|
|
96
|
+
const candidates = [normalized];
|
|
97
|
+
const newlineJsonIdx = normalized.lastIndexOf('\n{');
|
|
98
|
+
if (newlineJsonIdx >= 0) candidates.push(normalized.slice(newlineJsonIdx + 1));
|
|
99
|
+
const firstBraceIdx = normalized.indexOf('{');
|
|
100
|
+
if (firstBraceIdx > 0) candidates.push(normalized.slice(firstBraceIdx));
|
|
101
|
+
|
|
102
|
+
const seen = new Set();
|
|
103
|
+
for (const candidate of candidates) {
|
|
104
|
+
const trimmed = candidate.trim();
|
|
105
|
+
if (!trimmed || seen.has(trimmed)) continue;
|
|
106
|
+
seen.add(trimmed);
|
|
107
|
+
const parsed = parseJsonCandidate(trimmed);
|
|
108
|
+
if (parsed !== null) return parsed;
|
|
109
|
+
}
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function gatherObjectTextCandidates(value, depth = 0, out = [], seen = new Set()) {
|
|
114
|
+
if (depth > 4 || value == null) return out;
|
|
115
|
+
|
|
116
|
+
if (typeof value === 'string') {
|
|
117
|
+
const text = normalizeCompletionText(value);
|
|
118
|
+
if (text && !seen.has(text)) {
|
|
119
|
+
seen.add(text);
|
|
120
|
+
out.push(text);
|
|
121
|
+
}
|
|
122
|
+
return out;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (Array.isArray(value)) {
|
|
126
|
+
for (const item of value.slice(0, 6)) {
|
|
127
|
+
gatherObjectTextCandidates(item, depth + 1, out, seen);
|
|
128
|
+
}
|
|
129
|
+
return out;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (typeof value !== 'object') return out;
|
|
133
|
+
|
|
134
|
+
const preferredKeys = ['deliveryText', 'summary', 'body', 'text', 'content', 'message', 'stdout', 'stderr'];
|
|
135
|
+
const nestedKeys = ['completion', 'result', 'response', 'data', 'payload'];
|
|
136
|
+
|
|
137
|
+
for (const key of preferredKeys) {
|
|
138
|
+
if (Object.hasOwn(value, key)) {
|
|
139
|
+
gatherObjectTextCandidates(value[key], depth + 1, out, seen);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
for (const key of nestedKeys) {
|
|
144
|
+
if (Object.hasOwn(value, key)) {
|
|
145
|
+
gatherObjectTextCandidates(value[key], depth + 1, out, seen);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
for (const [key, nestedValue] of Object.entries(value)) {
|
|
150
|
+
if (preferredKeys.includes(key) || nestedKeys.includes(key)) continue;
|
|
151
|
+
gatherObjectTextCandidates(nestedValue, depth + 1, out, seen);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return out;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function prepareLines(text) {
|
|
158
|
+
return cleanMarkdown(text)
|
|
159
|
+
.split('\n')
|
|
160
|
+
.map(line => line.replace(/\t/g, ' ').replace(/\s+/g, ' ').trim())
|
|
161
|
+
.filter(Boolean)
|
|
162
|
+
.filter(line => !/^```/.test(line))
|
|
163
|
+
.filter(line => !/^[`~=_-]{3,}$/.test(line))
|
|
164
|
+
.filter(line => ![...line].every(char => '{}[],'.includes(char)))
|
|
165
|
+
.filter(line => !STACK_TRACE_LINE_RE.test(line));
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function truncateText(text, maxChars = MAX_DELIVERY_CHARS) {
|
|
169
|
+
const normalized = normalizeCompletionText(text);
|
|
170
|
+
if (!normalized) return null;
|
|
171
|
+
if (normalized.length <= maxChars) return normalized;
|
|
172
|
+
return normalized.slice(0, Math.max(0, maxChars - 1)).trimEnd() + '…';
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function splitSentences(text) {
|
|
176
|
+
const normalized = normalizeCompletionText(text);
|
|
177
|
+
if (!normalized) return [];
|
|
178
|
+
return normalized.match(/[^.!?]+(?:[.!?]+|$)/g)?.map(part => part.trim()).filter(Boolean) || [];
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function asSentence(text) {
|
|
182
|
+
const normalized = normalizeCompletionText(text);
|
|
183
|
+
if (!normalized) return null;
|
|
184
|
+
return /[.!?]$/.test(normalized) ? normalized : `${normalized}.`;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function shortenFragment(text, maxChars = 110) {
|
|
188
|
+
const normalized = normalizeCompletionText(text);
|
|
189
|
+
if (!normalized) return null;
|
|
190
|
+
const cleaned = cleanMarkdown(normalized)
|
|
191
|
+
.replace(/\bhttps?:\/\/\S+/gi, '')
|
|
192
|
+
.replace(/\s+/g, ' ')
|
|
193
|
+
.trim();
|
|
194
|
+
if (!cleaned) return null;
|
|
195
|
+
return truncateText(cleaned, maxChars);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function lowerFirst(text) {
|
|
199
|
+
if (!text) return text;
|
|
200
|
+
return text.charAt(0).toLowerCase() + text.slice(1);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function upperFirst(text) {
|
|
204
|
+
if (!text) return text;
|
|
205
|
+
return text.charAt(0).toUpperCase() + text.slice(1);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function looksLikeRawPayloadText(text) {
|
|
209
|
+
const normalized = normalizeCompletionText(text);
|
|
210
|
+
if (!normalized) return false;
|
|
211
|
+
if (INTERNAL_DONE_PAYLOAD_RE.test(normalized)) return true;
|
|
212
|
+
if (isInternalTransportNoiseText(normalized)) return true;
|
|
213
|
+
return (/^[[{]/.test(normalized) && RAW_PAYLOAD_MARKERS_RE.test(normalized));
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function looksLikeGunbrokerReport(text) {
|
|
217
|
+
const normalized = normalizeCompletionText(text);
|
|
218
|
+
if (!normalized) return false;
|
|
219
|
+
return /deal scanner/i.test(normalized) || (/^Baseline:/mi.test(normalized) && /^#\d+/m.test(normalized));
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function parseGunbrokerItem(line) {
|
|
223
|
+
const normalized = normalizeCompletionText(line);
|
|
224
|
+
if (!normalized) return null;
|
|
225
|
+
const match = normalized.match(/^#(\d+)(?:\s+\S+)?\s*\|\s*([+-]\d+%)\s*[—-]\s*\$?([\d,]+)\s*\(([^)]*)\)/);
|
|
226
|
+
if (!match) return shortenFragment(normalized, 100);
|
|
227
|
+
const [, rank, edge, price, context] = match;
|
|
228
|
+
return `#${rank} ${edge} at $${price} (${context.trim()})`;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function summarizeGunbrokerReport(text) {
|
|
232
|
+
const lines = prepareLines(text);
|
|
233
|
+
if (!lines.length) return null;
|
|
234
|
+
|
|
235
|
+
const titleLine = lines.find(line => /deal scanner/i.test(line)) || null;
|
|
236
|
+
const baselineLine = lines.find(line => /^Baseline:/i.test(line)) || null;
|
|
237
|
+
const noDealsLine = lines.find(line => /no deals/i.test(line)) || null;
|
|
238
|
+
const items = lines
|
|
239
|
+
.filter(line => /^#\d+/.test(line))
|
|
240
|
+
.map(parseGunbrokerItem)
|
|
241
|
+
.filter(Boolean)
|
|
242
|
+
.slice(0, MAX_LIST_ITEMS);
|
|
243
|
+
|
|
244
|
+
const parts = [];
|
|
245
|
+
if (titleLine) parts.push(asSentence(titleLine.replace(/^[^\p{L}\p{N}#]+/gu, '')));
|
|
246
|
+
if (baselineLine) parts.push(asSentence(baselineLine));
|
|
247
|
+
if (noDealsLine && items.length === 0) parts.push(asSentence(noDealsLine));
|
|
248
|
+
if (items.length) parts.push(`Top deals: ${items.join('; ')}.`);
|
|
249
|
+
|
|
250
|
+
return truncateText(parts.filter(Boolean).join(' '), MAX_DELIVERY_CHARS);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function isItemLine(line) {
|
|
254
|
+
return /^(?:[-*•]\s+|\d+[.)]\s+|#\d+\b)/.test(line);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function summarizeStructuredText(text) {
|
|
258
|
+
if (looksLikeGunbrokerReport(text)) {
|
|
259
|
+
return summarizeGunbrokerReport(text);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const lines = prepareLines(text);
|
|
263
|
+
if (!lines.length) return null;
|
|
264
|
+
|
|
265
|
+
const itemLines = lines.filter(isItemLine);
|
|
266
|
+
const nonItemLines = lines.filter(line => !isItemLine(line));
|
|
267
|
+
|
|
268
|
+
if (itemLines.length > 0) {
|
|
269
|
+
const heading = nonItemLines[0] || null;
|
|
270
|
+
const context = nonItemLines.slice(1, 3).join(' ');
|
|
271
|
+
const highlights = itemLines
|
|
272
|
+
.slice(0, MAX_LIST_ITEMS)
|
|
273
|
+
.map(line => shortenFragment(line.replace(/^[-*•]\s+/, ''), 110))
|
|
274
|
+
.filter(Boolean);
|
|
275
|
+
|
|
276
|
+
const parts = [];
|
|
277
|
+
if (heading) parts.push(asSentence(heading));
|
|
278
|
+
if (context) parts.push(asSentence(truncateText(context, 180)));
|
|
279
|
+
if (highlights.length) parts.push(`Highlights: ${highlights.join('; ')}.`);
|
|
280
|
+
return truncateText(parts.filter(Boolean).join(' '), MAX_DELIVERY_CHARS);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const compact = truncateText(lines.slice(0, 4).join(' '), 260);
|
|
284
|
+
return compact ? asSentence(compact) : null;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function summarizeProse(text) {
|
|
288
|
+
const normalized = prepareLines(text).join(' ').replace(/\s+/g, ' ').trim();
|
|
289
|
+
if (!normalized || isGenericOrTrivial(normalized)) return null;
|
|
290
|
+
|
|
291
|
+
const sentences = splitSentences(normalized);
|
|
292
|
+
if (!sentences.length) return truncateText(normalized, MAX_DELIVERY_CHARS);
|
|
293
|
+
if (normalized.length <= MAX_DELIVERY_CHARS && sentences.length <= MAX_DELIVERY_SENTENCES) {
|
|
294
|
+
return normalized;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const kept = [];
|
|
298
|
+
let chars = 0;
|
|
299
|
+
for (const sentence of sentences) {
|
|
300
|
+
const next = kept.length ? chars + 1 + sentence.length : chars + sentence.length;
|
|
301
|
+
if (kept.length >= MAX_DELIVERY_SENTENCES || next > MAX_DELIVERY_CHARS) break;
|
|
302
|
+
kept.push(sentence);
|
|
303
|
+
chars = next;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (!kept.length) return truncateText(normalized, MAX_DELIVERY_CHARS);
|
|
307
|
+
return kept.join(' ');
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function countTechnicalKeywordHits(text) {
|
|
311
|
+
const normalized = normalizeCompletionText(text);
|
|
312
|
+
if (!normalized) return 0;
|
|
313
|
+
const matches = cleanMarkdown(normalized).match(TECHNICAL_KEYWORD_RE) || [];
|
|
314
|
+
return new Set(matches.map(match => match.toLowerCase())).size;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function looksTechnicalCompletionSummary(rawText, summarizedText = null) {
|
|
318
|
+
const raw = normalizeCompletionText(rawText);
|
|
319
|
+
if (!raw) return false;
|
|
320
|
+
|
|
321
|
+
const cleaned = cleanMarkdown(raw).replace(/\s+/g, ' ').trim();
|
|
322
|
+
if (!cleaned) return false;
|
|
323
|
+
|
|
324
|
+
const hasDelimiter = /[;|]/.test(cleaned) || /\n[-*#]/.test(raw);
|
|
325
|
+
const hardTechnical = TECHNICAL_COMMIT_PREFIX_RE.test(cleaned)
|
|
326
|
+
|| FILE_CONTEXT_PREFIX_RE.test(cleaned)
|
|
327
|
+
|| CODEISH_MARKER_RE.test(cleaned)
|
|
328
|
+
|| hasDelimiter;
|
|
329
|
+
|
|
330
|
+
let technicalScore = 0;
|
|
331
|
+
if (TECHNICAL_COMMIT_PREFIX_RE.test(cleaned)) technicalScore += 3;
|
|
332
|
+
if (FILE_CONTEXT_PREFIX_RE.test(cleaned)) technicalScore += 3;
|
|
333
|
+
if (CODEISH_MARKER_RE.test(cleaned)) technicalScore += 2;
|
|
334
|
+
if (hasDelimiter) technicalScore += 1;
|
|
335
|
+
technicalScore += Math.min(2, countTechnicalKeywordHits(cleaned));
|
|
336
|
+
if (/\b(?:summary_human|deliveryText|details_technical|sessionKey|idempotencyKey)\b/.test(cleaned)) {
|
|
337
|
+
technicalScore += 2;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
let humanScore = 0;
|
|
341
|
+
if (HUMAN_CUE_RE.test(cleaned)) humanScore += 2;
|
|
342
|
+
if (/\b(?:people|users?|operators?)\b/i.test(cleaned)) humanScore += 1;
|
|
343
|
+
if (/\b(?:now|because|so that|helps?|prevents?|avoids?|keeps?|lets?|allows?)\b/i.test(cleaned)) humanScore += 1;
|
|
344
|
+
if (summarizedText && summarizeProse(summarizedText) && !TECHNICAL_COMMIT_PREFIX_RE.test(cleaned) && !FILE_CONTEXT_PREFIX_RE.test(cleaned)) {
|
|
345
|
+
humanScore += 1;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if (!hardTechnical) {
|
|
349
|
+
return technicalScore >= 5 && technicalScore > humanScore + 2;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
return technicalScore >= 3 && technicalScore > humanScore + 1;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function replaceTechnicalPhrases(text) {
|
|
356
|
+
if (!text) return text;
|
|
357
|
+
return text
|
|
358
|
+
.replace(/\bsummary_human\b/gi, 'human summary')
|
|
359
|
+
.replace(/\bsummaryHuman\b/g, 'human summary')
|
|
360
|
+
.replace(/\bdeliveryText\b/g, 'delivery text')
|
|
361
|
+
.replace(/\bdetails_technical\b/gi, 'technical details')
|
|
362
|
+
.replace(/\bresolveCompletionDelivery\b/g, 'completion delivery logic')
|
|
363
|
+
.replace(/\bbuildTerminalCompletionPayload\b/g, 'completion payload builder')
|
|
364
|
+
.replace(/\bstructured completion summary\b/gi, 'the structured summary')
|
|
365
|
+
.replace(/\bstructured completion\b(?!\s+summary)\b/gi, 'structured completion data')
|
|
366
|
+
.replace(/\bcompletion delivery\b/gi, 'how the final completion message is delivered')
|
|
367
|
+
.replace(/\bdelivery path\b/gi, 'delivery flow')
|
|
368
|
+
.replace(/\bwatcher path\b/gi, 'completion watcher flow')
|
|
369
|
+
.replace(/\bwatcher\b/gi, 'completion watcher')
|
|
370
|
+
.replace(/\bstdout\b/gi, 'command output')
|
|
371
|
+
.replace(/\bstderr\b/gi, 'error output')
|
|
372
|
+
.replace(/\bpayload\b/gi, 'result payload')
|
|
373
|
+
.replace(/\braw transcript\b/gi, 'raw completion text')
|
|
374
|
+
.replace(/\bdouble[- ]delivery\b/gi, 'duplicate completion messages')
|
|
375
|
+
.replace(/\bsingle final user-facing completion\b/gi, 'one final completion message')
|
|
376
|
+
.replace(/\bsummary fallback\b/gi, 'fallback summary');
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function humanizeCamelToken(token) {
|
|
380
|
+
if (!/[a-z][A-Z]/.test(token) && !token.includes('_')) return token;
|
|
381
|
+
return token
|
|
382
|
+
.replace(/_/g, ' ')
|
|
383
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1 $2')
|
|
384
|
+
.toLowerCase();
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function cleanTechnicalFragment(text) {
|
|
388
|
+
const normalized = normalizeCompletionText(text);
|
|
389
|
+
if (!normalized) return null;
|
|
390
|
+
|
|
391
|
+
let cleaned = cleanMarkdown(normalized)
|
|
392
|
+
.replace(TECHNICAL_COMMIT_PREFIX_RE, '')
|
|
393
|
+
.replace(FILE_CONTEXT_PREFIX_RE, '')
|
|
394
|
+
.replace(/\b(?:[A-Za-z0-9_.-]+\/)+([A-Za-z0-9_.-]+\.(?:[cm]?[jt]sx?|json|md|py|sh|sql|ya?ml|toml))\b/g, '$1')
|
|
395
|
+
.replace(/\b([a-z][A-Za-z0-9_]*[A-Z][A-Za-z0-9_]*)\b/g, (_, token) => humanizeCamelToken(token))
|
|
396
|
+
.replace(/\(\)/g, '')
|
|
397
|
+
.replace(/^[:\-–—]+\s*/, '')
|
|
398
|
+
.replace(/^and\s+/i, '')
|
|
399
|
+
.replace(/^then\s+/i, '')
|
|
400
|
+
.replace(/\s+/g, ' ')
|
|
401
|
+
.trim();
|
|
402
|
+
|
|
403
|
+
cleaned = replaceTechnicalPhrases(cleaned)
|
|
404
|
+
.replace(/\s+,/g, ',')
|
|
405
|
+
.replace(/\s+\./g, '.')
|
|
406
|
+
.replace(/[;,:]+$/g, '')
|
|
407
|
+
.trim();
|
|
408
|
+
|
|
409
|
+
return cleaned || null;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function isTestOrValidationFragment(fragment) {
|
|
413
|
+
const cleaned = normalizeCompletionText(fragment);
|
|
414
|
+
if (!cleaned) return false;
|
|
415
|
+
return TEST_FRAGMENT_RE.test(cleaned)
|
|
416
|
+
&& !/\b(?:human(?:-|\s)?readable|summary|summaries|message|report|chat|duplicate|user-facing)\b/i.test(cleaned);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function toPastTenseFragment(fragment) {
|
|
420
|
+
let text = cleanTechnicalFragment(fragment);
|
|
421
|
+
if (!text) return null;
|
|
422
|
+
|
|
423
|
+
const rewrites = [
|
|
424
|
+
[/^clean up\b/i, 'cleaned up'],
|
|
425
|
+
[/^normalize\b/i, 'cleaned up'],
|
|
426
|
+
[/^humani[sz]e\b/i, 'made more readable'],
|
|
427
|
+
[/^rewrite\b/i, 'reworked'],
|
|
428
|
+
[/^refactor\b/i, 'refined'],
|
|
429
|
+
[/^fix\b/i, 'fixed'],
|
|
430
|
+
[/^update\b/i, 'updated'],
|
|
431
|
+
[/^adjust\b/i, 'adjusted'],
|
|
432
|
+
[/^change\b/i, 'updated'],
|
|
433
|
+
[/^improve\b/i, 'improved'],
|
|
434
|
+
[/^add\b/i, 'added'],
|
|
435
|
+
[/^introduce\b/i, 'introduced'],
|
|
436
|
+
[/^enable\b/i, 'enabled'],
|
|
437
|
+
[/^support\b/i, 'supported'],
|
|
438
|
+
[/^preserve\b/i, 'kept'],
|
|
439
|
+
[/^keep\b/i, 'kept'],
|
|
440
|
+
[/^retain\b/i, 'retained'],
|
|
441
|
+
[/^prevent\b/i, 'prevented'],
|
|
442
|
+
[/^avoid\b/i, 'avoided'],
|
|
443
|
+
[/^stop\b/i, 'stopped'],
|
|
444
|
+
[/^pass through\b/i, 'passed through'],
|
|
445
|
+
[/^leave\b/i, 'left'],
|
|
446
|
+
[/^prefer\b/i, 'preferred'],
|
|
447
|
+
[/^wire\b/i, 'wired up'],
|
|
448
|
+
[/^route\b/i, 'routed'],
|
|
449
|
+
[/^surface\b/i, 'surfaced'],
|
|
450
|
+
[/^append\b/i, 'appended'],
|
|
451
|
+
[/^format\b/i, 'formatted'],
|
|
452
|
+
[/^teach\b/i, 'taught'],
|
|
453
|
+
[/^ensure\b/i, 'ensured'],
|
|
454
|
+
[/^dedupe\b/i, 'deduplicated'],
|
|
455
|
+
];
|
|
456
|
+
|
|
457
|
+
for (const [pattern, replacement] of rewrites) {
|
|
458
|
+
if (pattern.test(text)) {
|
|
459
|
+
text = text.replace(pattern, replacement);
|
|
460
|
+
break;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
text = text.replace(/\s+/g, ' ').trim();
|
|
465
|
+
return text ? upperFirst(text) : null;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function extractTechnicalFragments(text) {
|
|
469
|
+
const normalized = normalizeCompletionText(text);
|
|
470
|
+
if (!normalized) return [];
|
|
471
|
+
|
|
472
|
+
const fragments = prepareLines(normalized)
|
|
473
|
+
.flatMap(line => line.split(/\s*;\s*|\s+\|\s+|\s+&&\s+|\s+->\s+|\s+=>\s+/))
|
|
474
|
+
.map(fragment => fragment.trim())
|
|
475
|
+
.filter(Boolean);
|
|
476
|
+
|
|
477
|
+
return fragments.map(cleanTechnicalFragment).filter(Boolean);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
function detectTechnicalThemes(rawText, fragments = []) {
|
|
481
|
+
const raw = cleanMarkdown(normalizeCompletionText(rawText) || '').toLowerCase();
|
|
482
|
+
const combined = [raw, fragments.join(' ').toLowerCase()].filter(Boolean).join(' ');
|
|
483
|
+
return {
|
|
484
|
+
completionFlow: /\b(?:completion|deliver(?:y|ed)?|notification|message|report|chat|summary|summaries|human(?:-|\s)?readable|plain english|user-facing)\b/.test(combined),
|
|
485
|
+
detailSeparation: /\b(?:technical details?|details_technical|debug|underneath|below|separate|separated|split|move|moved)\b/.test(combined),
|
|
486
|
+
duplicatePrevention: /\b(?:duplicate|double[- ]delivery|single final|dedupe|deduplicat|one clean)\b/.test(combined),
|
|
487
|
+
contextPreservation: /\b(?:structured|context|preserve|kept|retain|survive)\b/.test(combined),
|
|
488
|
+
reliability: RELIABILITY_SIGNAL_RE.test(combined),
|
|
489
|
+
addedBehavior: /\b(?:add|added|introduce|introduced|support|supported|enable|enabled)\b/.test(combined),
|
|
490
|
+
testOnly: fragments.length > 0 && fragments.every(isTestOrValidationFragment),
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function isPlainEnglishLeadText(text) {
|
|
495
|
+
const normalized = normalizeCompletionText(text);
|
|
496
|
+
if (!normalized) return false;
|
|
497
|
+
if (FILE_CONTEXT_PREFIX_RE.test(normalized)) return false;
|
|
498
|
+
if (CODEISH_MARKER_RE.test(normalized)) return false;
|
|
499
|
+
if (/\b(?:summary(?:_human|Human)?|deliveryText|details_technical|sessionKey|idempotencyKey|payload-precedence|raw summary|watcher path|completion delivery)\b/i.test(normalized)) {
|
|
500
|
+
return false;
|
|
501
|
+
}
|
|
502
|
+
return !looksTechnicalCompletionSummary(normalized, normalized);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function buildCompletionLeadFromThemes(themes) {
|
|
506
|
+
const sentences = [];
|
|
507
|
+
if (themes.duplicatePrevention) {
|
|
508
|
+
sentences.push('Final completion updates now arrive as one clean plain-English summary.');
|
|
509
|
+
sentences.push('That makes the result easier to scan and avoids noisy repeat messages.');
|
|
510
|
+
} else {
|
|
511
|
+
sentences.push('Final completion updates now start with a short plain-English summary.');
|
|
512
|
+
if (themes.contextPreservation) {
|
|
513
|
+
sentences.push('That makes the result easier to read without hiding the useful detail.');
|
|
514
|
+
} else {
|
|
515
|
+
sentences.push('That makes the result easier to scan without hiding the useful detail.');
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
sentences.push('Future runs should show the clean summary first, with technical details underneath when needed.');
|
|
519
|
+
return truncateText(sentences.join(' '), MAX_DELIVERY_CHARS);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function buildHumanizedTechnicalSummary(rawText, fallbackSummary) {
|
|
523
|
+
const cleanedRaw = normalizeCompletionText(rawText);
|
|
524
|
+
const fallback = normalizeCompletionText(fallbackSummary);
|
|
525
|
+
if (!cleanedRaw) return fallback;
|
|
526
|
+
|
|
527
|
+
const fragments = extractTechnicalFragments(cleanedRaw);
|
|
528
|
+
const primaryFragments = fragments.filter(fragment => !isTestOrValidationFragment(fragment));
|
|
529
|
+
const sourceFragments = primaryFragments.length > 0 ? primaryFragments : fragments;
|
|
530
|
+
const themes = detectTechnicalThemes(cleanedRaw, fragments);
|
|
531
|
+
|
|
532
|
+
if (themes.completionFlow) {
|
|
533
|
+
return buildCompletionLeadFromThemes(themes);
|
|
534
|
+
}
|
|
18
535
|
|
|
19
|
-
const
|
|
20
|
-
|
|
536
|
+
const chosen = sourceFragments
|
|
537
|
+
.slice(0, 2)
|
|
538
|
+
.map(fragment => toPastTenseFragment(fragment))
|
|
539
|
+
.filter(Boolean)
|
|
540
|
+
.filter(fragment => isPlainEnglishLeadText(fragment));
|
|
21
541
|
|
|
22
|
-
|
|
542
|
+
let actionSummary;
|
|
543
|
+
if (chosen.length === 1) {
|
|
544
|
+
actionSummary = asSentence(chosen[0]);
|
|
545
|
+
} else if (chosen.length >= 2) {
|
|
546
|
+
actionSummary = `${chosen[0]} and ${lowerFirst(chosen[1])}.`;
|
|
547
|
+
} else if (fallback && isPlainEnglishLeadText(fallback)) {
|
|
548
|
+
actionSummary = asSentence(fallback);
|
|
549
|
+
} else if (themes.testOnly) {
|
|
550
|
+
actionSummary = 'Added focused coverage for the weak spot.';
|
|
551
|
+
} else if (themes.reliability) {
|
|
552
|
+
actionSummary = 'The requested fix is in place.';
|
|
553
|
+
} else if (themes.addedBehavior) {
|
|
554
|
+
actionSummary = 'The requested behavior is now in place.';
|
|
555
|
+
} else {
|
|
556
|
+
actionSummary = 'The update is in place.';
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
const extras = [];
|
|
560
|
+
if (themes.testOnly) {
|
|
561
|
+
extras.push('That makes the behavior easier to trust.');
|
|
562
|
+
extras.push('Future regressions should get caught quickly.');
|
|
563
|
+
} else if (themes.reliability) {
|
|
564
|
+
extras.push('That should make the workflow more reliable.');
|
|
565
|
+
extras.push('Future runs should be less likely to hit the same problem.');
|
|
566
|
+
} else if (themes.addedBehavior) {
|
|
567
|
+
extras.push('That makes the new behavior available without extra follow-up.');
|
|
568
|
+
extras.push('Future runs should use it automatically.');
|
|
569
|
+
} else {
|
|
570
|
+
extras.push('That should make the result easier to work with.');
|
|
571
|
+
extras.push('Future runs should reflect the change automatically.');
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
return truncateText([actionSummary, ...extras].filter(Boolean).join(' '), MAX_DELIVERY_CHARS);
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
export function humanizeCompletionText(value) {
|
|
578
|
+
const raw = normalizeCompletionText(value);
|
|
579
|
+
if (!raw) return null;
|
|
580
|
+
|
|
581
|
+
const summarized = summarizeCompletionText(raw);
|
|
582
|
+
if (!summarized) return null;
|
|
583
|
+
if (!looksTechnicalCompletionSummary(raw, summarized)) return summarized;
|
|
584
|
+
|
|
585
|
+
return buildHumanizedTechnicalSummary(raw, summarized) || summarized;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
function summarizeChecklistTechnicalDetails(checklist, sha) {
|
|
589
|
+
const normalizedChecklist = cloneChecklist(checklist);
|
|
590
|
+
if (!normalizedChecklist) return null;
|
|
591
|
+
|
|
592
|
+
const parts = [];
|
|
593
|
+
if (normalizedChecklist.tests_passed === true) parts.push('tests passed');
|
|
594
|
+
if (normalizedChecklist.pushed === true) {
|
|
595
|
+
const short = shortSha(sha);
|
|
596
|
+
parts.push(short ? `pushed ${short}` : 'changes pushed');
|
|
597
|
+
} else if (sha) {
|
|
598
|
+
parts.push(`commit ${shortSha(sha)}`);
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
const extraTrueFlags = Object.entries(normalizedChecklist)
|
|
602
|
+
.filter(([key, flagValue]) => flagValue === true && !['work_complete', 'tests_passed', 'pushed'].includes(key))
|
|
603
|
+
.map(([key]) => key.replace(/_/g, ' '));
|
|
604
|
+
|
|
605
|
+
if (extraTrueFlags.length > 0) {
|
|
606
|
+
parts.push(`checks: ${extraTrueFlags.slice(0, 3).join(', ')}`);
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
return parts.length > 0 ? `Checks: ${parts.join('; ')}.` : null;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
function buildTechnicalDetailsText({ rawText, summaryText, completion } = {}) {
|
|
613
|
+
const raw = normalizeCompletionText(rawText);
|
|
614
|
+
const summary = normalizeCompletionText(summaryText);
|
|
615
|
+
const details = getCompletionTechnicalDetails(completion);
|
|
616
|
+
const parts = [];
|
|
617
|
+
|
|
618
|
+
const rawTechnical = raw && looksTechnicalCompletionSummary(raw, summary) && raw !== summary;
|
|
619
|
+
if (rawTechnical) {
|
|
620
|
+
parts.push(truncateText(cleanMarkdown(raw).replace(/\s+/g, ' ').trim(), 260));
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
let completionDetailsAreTechnical = false;
|
|
624
|
+
if (typeof details === 'string') {
|
|
625
|
+
const normalized = normalizeCompletionText(details);
|
|
626
|
+
completionDetailsAreTechnical = Boolean(normalized && looksTechnicalCompletionSummary(normalized, summary));
|
|
627
|
+
if (normalized
|
|
628
|
+
&& !isInternalTransportNoiseText(normalized)
|
|
629
|
+
&& (completionDetailsAreTechnical || rawTechnical)
|
|
630
|
+
&& (!rawTechnical || normalized !== raw)) {
|
|
631
|
+
parts.push(truncateText(normalized, 220));
|
|
632
|
+
}
|
|
633
|
+
} else if (details && typeof details === 'object') {
|
|
634
|
+
const rawSummary = normalizeCompletionText(details.raw_summary);
|
|
635
|
+
completionDetailsAreTechnical = Boolean(rawSummary && looksTechnicalCompletionSummary(rawSummary, summary));
|
|
636
|
+
if (rawSummary
|
|
637
|
+
&& !isInternalTransportNoiseText(rawSummary)
|
|
638
|
+
&& (completionDetailsAreTechnical || rawTechnical)
|
|
639
|
+
&& (!rawTechnical || rawSummary !== raw)) {
|
|
640
|
+
parts.push(truncateText(cleanMarkdown(rawSummary).replace(/\s+/g, ' ').trim(), 220));
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
const checklistDetails = summarizeChecklistTechnicalDetails(completion?.checklist, completion?.sha);
|
|
645
|
+
if (checklistDetails && (rawTechnical || completionDetailsAreTechnical)) parts.push(checklistDetails);
|
|
646
|
+
|
|
647
|
+
const unique = [];
|
|
648
|
+
const seen = new Set();
|
|
649
|
+
for (const part of parts) {
|
|
650
|
+
const normalized = normalizeCompletionText(part);
|
|
651
|
+
if (!normalized) continue;
|
|
652
|
+
const key = normalized.toLowerCase();
|
|
653
|
+
if (seen.has(key)) continue;
|
|
654
|
+
seen.add(key);
|
|
655
|
+
unique.push(normalized);
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
return unique;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
function composeDeliveryText(summaryText, technicalDetailsText = null) {
|
|
662
|
+
const summary = normalizeCompletionText(summaryText);
|
|
663
|
+
if (!summary) return null;
|
|
664
|
+
const technicalLines = Array.isArray(technicalDetailsText)
|
|
665
|
+
? technicalDetailsText.map(line => normalizeCompletionText(line)).filter(Boolean)
|
|
666
|
+
: [];
|
|
667
|
+
if (technicalLines.length > 0) {
|
|
668
|
+
return `${summary}\n\nTechnical details:\n- ${technicalLines.join('\n- ')}`;
|
|
669
|
+
}
|
|
670
|
+
const technical = normalizeCompletionText(technicalDetailsText);
|
|
671
|
+
return technical ? `${summary}\n\nTechnical details:\n- ${technical}` : summary;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
export function summarizeCompletionText(value, { skipEmbeddedObject = false } = {}) {
|
|
675
|
+
const raw = normalizeCompletionText(value);
|
|
676
|
+
if (!raw) return null;
|
|
677
|
+
|
|
678
|
+
if (!skipEmbeddedObject) {
|
|
679
|
+
const parsed = extractEmbeddedCompletionObject(raw);
|
|
680
|
+
if (parsed !== null) {
|
|
681
|
+
const candidates = gatherObjectTextCandidates(parsed);
|
|
682
|
+
for (const candidate of candidates) {
|
|
683
|
+
const summarized = summarizeCompletionText(candidate, { skipEmbeddedObject: true });
|
|
684
|
+
if (summarized) return summarized;
|
|
685
|
+
}
|
|
686
|
+
if (looksLikeRawPayloadText(raw)) return null;
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
if (looksLikeRawPayloadText(raw)) return null;
|
|
691
|
+
if (looksLikeGunbrokerReport(raw)) return summarizeGunbrokerReport(raw);
|
|
692
|
+
|
|
693
|
+
const prepared = prepareLines(raw);
|
|
694
|
+
const structured = prepared.length >= 4 || prepared.some(line => line.includes('|')) || prepared.filter(isItemLine).length >= 2;
|
|
695
|
+
if (structured) {
|
|
696
|
+
const summary = summarizeStructuredText(raw);
|
|
697
|
+
if (summary && !isGenericOrTrivial(summary)) return summary;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
return summarizeProse(raw);
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
export function isMeaningfulCompletionText(value) {
|
|
704
|
+
return Boolean(summarizeCompletionText(value));
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
function getCompletionSummaryHuman(completion) {
|
|
708
|
+
return normalizeCompletionText(completion?.summary_human ?? completion?.summaryHuman);
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
function getCompletionTechnicalDetails(completion) {
|
|
712
|
+
const details = completion?.details_technical ?? completion?.technicalDetails ?? null;
|
|
713
|
+
if (!details) return null;
|
|
714
|
+
if (typeof details === 'string') return normalizeCompletionText(details);
|
|
715
|
+
if (typeof details !== 'object' || Array.isArray(details)) return null;
|
|
716
|
+
return Object.keys(details).length > 0 ? details : null;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
export function hasCompletionSignal(completion) {
|
|
720
|
+
if (!completion || typeof completion !== 'object' || Array.isArray(completion)) return false;
|
|
721
|
+
if (getCompletionSummaryHuman(completion)) return true;
|
|
722
|
+
if (normalizeCompletionText(completion?.summary)) return true;
|
|
723
|
+
if (normalizeCompletionText(completion?.deliveryText)) return true;
|
|
724
|
+
if (normalizeCompletionText(completion?.sha)) return true;
|
|
725
|
+
if (getCompletionTechnicalDetails(completion)) return true;
|
|
726
|
+
return !!cloneChecklist(completion?.checklist);
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
function buildInternalNoiseFallback(noisyTexts) {
|
|
730
|
+
const joined = noisyTexts.join(' ').toLowerCase();
|
|
731
|
+
if (joined.includes('session went idle without calling done')) {
|
|
732
|
+
return 'The session ended without a final completion signal, so no reliable final report is available.';
|
|
733
|
+
}
|
|
734
|
+
if (joined.includes('session not found') || joined.includes('session never found')) {
|
|
735
|
+
return 'The session ended before a final report could be retrieved, so there is no user-facing completion message to deliver.';
|
|
736
|
+
}
|
|
737
|
+
return 'The session completed with internal transport status only and no user-facing completion report.';
|
|
23
738
|
}
|
|
24
739
|
|
|
25
740
|
function shortSha(sha) {
|
|
@@ -37,6 +752,194 @@ function cloneChecklist(checklist) {
|
|
|
37
752
|
}
|
|
38
753
|
}
|
|
39
754
|
|
|
755
|
+
function hasNegatedCommandContext(text, matchIndex) {
|
|
756
|
+
const before = text.slice(Math.max(0, matchIndex - 40), matchIndex);
|
|
757
|
+
return /\b(?:do\s+not|don't|dont|never)\s+(?:use|run|call|invoke)?\s*$/i.test(before)
|
|
758
|
+
|| /\bavoid\s+(?:using\s+)?$/i.test(before)
|
|
759
|
+
|| /\bwithout\s+(?:using\s+)?$/i.test(before);
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
export function taskRequiresGitSha(taskPrompt) {
|
|
763
|
+
if (!taskPrompt || typeof taskPrompt !== 'string') return false;
|
|
764
|
+
|
|
765
|
+
const commandPattern = /\bgit\s+(push|rebase|cherry-pick)\b|(?:^|\s)--force-with-lease\b|(?:^|\s)--force-push\b/ig;
|
|
766
|
+
let match;
|
|
767
|
+
while ((match = commandPattern.exec(taskPrompt)) !== null) {
|
|
768
|
+
if (!hasNegatedCommandContext(taskPrompt, match.index)) return true;
|
|
769
|
+
}
|
|
770
|
+
return false;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
export function inferTaskCompletionRequirements(taskPrompt) {
|
|
774
|
+
const text = typeof taskPrompt === 'string' ? taskPrompt : '';
|
|
775
|
+
const pushForbidden = PUSH_FORBIDDEN_RE.test(text);
|
|
776
|
+
const pushRequired = !pushForbidden && taskRequiresGitSha(text);
|
|
777
|
+
const testsApplicable = TEST_APPLICABILITY_RE.test(text) && !TEST_NEGATION_RE.test(text);
|
|
778
|
+
|
|
779
|
+
return {
|
|
780
|
+
pushRequired,
|
|
781
|
+
pushForbidden,
|
|
782
|
+
testsApplicable,
|
|
783
|
+
};
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
export function buildCompletionChecklistExample(taskPromptOrRequirements) {
|
|
787
|
+
const requirements = typeof taskPromptOrRequirements === 'string' || taskPromptOrRequirements == null
|
|
788
|
+
? inferTaskCompletionRequirements(taskPromptOrRequirements)
|
|
789
|
+
: taskPromptOrRequirements;
|
|
790
|
+
|
|
791
|
+
const example = { work_complete: true };
|
|
792
|
+
if (requirements?.testsApplicable) example.tests_passed = true;
|
|
793
|
+
if (requirements?.pushRequired) example.pushed = true;
|
|
794
|
+
return JSON.stringify(example);
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
export function buildCompletionSignalInstructions({ label, taskPrompt, doneScriptPath } = {}) {
|
|
798
|
+
const escapedLabel = String(label || '').replace(/'/g, "'\\''");
|
|
799
|
+
const requirements = inferTaskCompletionRequirements(taskPrompt);
|
|
800
|
+
const checklistExample = buildCompletionChecklistExample(requirements);
|
|
801
|
+
|
|
802
|
+
const readinessChecks = [
|
|
803
|
+
'All file edits are saved',
|
|
804
|
+
requirements.testsApplicable
|
|
805
|
+
? 'Any required tests / validation for this task have passed'
|
|
806
|
+
: 'Any required validation for this task is complete',
|
|
807
|
+
'All required API calls / external follow-up actions are done',
|
|
808
|
+
];
|
|
809
|
+
|
|
810
|
+
if (requirements.pushRequired) {
|
|
811
|
+
readinessChecks.push('Any required git push / history-editing step for this task is complete');
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
readinessChecks.push('You have verified the work is complete');
|
|
815
|
+
|
|
816
|
+
const lines = [];
|
|
817
|
+
lines.push('COMPLETION SIGNAL -- READ CAREFULLY:');
|
|
818
|
+
lines.push('');
|
|
819
|
+
lines.push('Only call this command after ALL of the following are true:');
|
|
820
|
+
readinessChecks.forEach((line, idx) => lines.push(` ${idx + 1}. ${line}`));
|
|
821
|
+
lines.push('');
|
|
822
|
+
lines.push('Call this as your ABSOLUTE FINAL action -- nothing else runs after this:');
|
|
823
|
+
lines.push(` node '${doneScriptPath}' done --label '${escapedLabel}' \\`);
|
|
824
|
+
lines.push(' --summary "<human-readable summary of what you actually did>" \\');
|
|
825
|
+
lines.push(` --checklist '${checklistExample}' \\`);
|
|
826
|
+
lines.push(' [--sha "<git commit SHA if applicable>"]');
|
|
827
|
+
lines.push('');
|
|
828
|
+
lines.push('Checklist rules:');
|
|
829
|
+
lines.push(' - work_complete MUST be true -- you are asserting you have finished ALL assigned work');
|
|
830
|
+
lines.push(' - Only include tests_passed if validation/testing was actually required for this task');
|
|
831
|
+
lines.push(' - Only include pushed:true if this task required a pushed git change');
|
|
832
|
+
|
|
833
|
+
if (requirements.pushForbidden) {
|
|
834
|
+
lines.push(' - This task explicitly says not to push -- do not wait on a push or set pushed:true before calling done');
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
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.');
|
|
838
|
+
lines.push('Do NOT call done while planning, reading files, or mid-task.');
|
|
839
|
+
|
|
840
|
+
if (requirements.pushRequired) {
|
|
841
|
+
lines.push('If the required push for this task has not happened yet, you are not done.');
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
return lines.join('\n');
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
function extractTextSegments(content) {
|
|
848
|
+
if (typeof content === 'string') return [content];
|
|
849
|
+
if (!content || typeof content !== 'object') return [];
|
|
850
|
+
|
|
851
|
+
if (Array.isArray(content)) {
|
|
852
|
+
return content.flatMap(part => {
|
|
853
|
+
if (typeof part === 'string') return [part];
|
|
854
|
+
if (part && typeof part.text === 'string') return [part.text];
|
|
855
|
+
return [];
|
|
856
|
+
});
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
if (typeof content.text === 'string') return [content.text];
|
|
860
|
+
return [];
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
function entryHasToolUse(entry) {
|
|
864
|
+
if (!entry || typeof entry !== 'object') return false;
|
|
865
|
+
if (entry.type === 'tool_use') return true;
|
|
866
|
+
if (!Array.isArray(entry.content)) return false;
|
|
867
|
+
return entry.content.some(part => part?.type === 'tool_use');
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
function entryHasToolResult(entry) {
|
|
871
|
+
if (!entry || typeof entry !== 'object') return false;
|
|
872
|
+
if (entry.type === 'tool_result') return true;
|
|
873
|
+
if (!Array.isArray(entry.content)) return false;
|
|
874
|
+
return entry.content.some(part => part?.type === 'tool_result');
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
function extractEntryText(entry) {
|
|
878
|
+
if (!entry || typeof entry !== 'object') return null;
|
|
879
|
+
return normalizeCompletionText(extractTextSegments(entry.content).join('').trim());
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
export function extractLastMeaningfulAssistantReplyFromEntries(entries) {
|
|
883
|
+
if (!Array.isArray(entries)) return null;
|
|
884
|
+
|
|
885
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
886
|
+
const entry = entries[i];
|
|
887
|
+
if (entry?.role !== 'assistant') continue;
|
|
888
|
+
if (entryHasToolUse(entry)) continue;
|
|
889
|
+
|
|
890
|
+
const text = extractEntryText(entry);
|
|
891
|
+
if (isMeaningfulCompletionText(text)) return text;
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
return null;
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
export function extractTerminalAssistantReplyFromEntries(entries) {
|
|
898
|
+
if (!Array.isArray(entries) || entries.length === 0) return null;
|
|
899
|
+
|
|
900
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
901
|
+
const entry = entries[i];
|
|
902
|
+
if (!entry || typeof entry !== 'object') continue;
|
|
903
|
+
|
|
904
|
+
if (entry.role === 'assistant') {
|
|
905
|
+
if (entryHasToolUse(entry)) return null;
|
|
906
|
+
|
|
907
|
+
const text = extractEntryText(entry);
|
|
908
|
+
if (!text) continue;
|
|
909
|
+
if (!isMeaningfulCompletionText(text)) return null;
|
|
910
|
+
|
|
911
|
+
const stopReason = entry.stop_reason ?? entry.stopReason ?? null;
|
|
912
|
+
return stopReason === 'end_turn' ? text : null;
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
if (entry.role === 'user') {
|
|
916
|
+
if (entryHasToolResult(entry)) return null;
|
|
917
|
+
return null;
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
return null;
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
function buildTechnicalDetails({ checklist, sha, rawSummary, summaryHuman } = {}) {
|
|
925
|
+
const details = {};
|
|
926
|
+
const normalizedChecklist = cloneChecklist(checklist);
|
|
927
|
+
const normalizedSha = normalizeCompletionText(sha);
|
|
928
|
+
const normalizedRawSummary = normalizeCompletionText(rawSummary);
|
|
929
|
+
const normalizedSummaryHuman = normalizeCompletionText(summaryHuman);
|
|
930
|
+
|
|
931
|
+
if (normalizedChecklist) details.checklist = normalizedChecklist;
|
|
932
|
+
if (normalizedSha) {
|
|
933
|
+
details.sha = normalizedSha;
|
|
934
|
+
details.sha_short = shortSha(normalizedSha);
|
|
935
|
+
}
|
|
936
|
+
if (normalizedRawSummary && normalizedRawSummary !== normalizedSummaryHuman) {
|
|
937
|
+
details.raw_summary = normalizedRawSummary;
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
return Object.keys(details).length > 0 ? details : null;
|
|
941
|
+
}
|
|
942
|
+
|
|
40
943
|
export function synthesizeCompletionReply({ checklist, sha } = {}) {
|
|
41
944
|
const normalizedChecklist = cloneChecklist(checklist);
|
|
42
945
|
const short = shortSha(sha);
|
|
@@ -56,7 +959,7 @@ export function synthesizeCompletionReply({ checklist, sha } = {}) {
|
|
|
56
959
|
|
|
57
960
|
const extraTrueFlags = normalizedChecklist
|
|
58
961
|
? Object.entries(normalizedChecklist)
|
|
59
|
-
.filter(([key,
|
|
962
|
+
.filter(([key, flagValue]) => flagValue === true && !['work_complete', 'tests_passed', 'pushed'].includes(key))
|
|
60
963
|
.map(([key]) => key.replace(/_/g, ' '))
|
|
61
964
|
: [];
|
|
62
965
|
|
|
@@ -72,64 +975,162 @@ export function buildTerminalCompletionPayload({ summary, checklist, sha } = {})
|
|
|
72
975
|
const rawSummary = normalizeCompletionText(summary);
|
|
73
976
|
const normalizedChecklist = cloneChecklist(checklist);
|
|
74
977
|
const normalizedSha = normalizeCompletionText(sha);
|
|
75
|
-
const
|
|
76
|
-
const synthesizedReply =
|
|
978
|
+
const normalizedSummary = humanizeCompletionText(rawSummary);
|
|
979
|
+
const synthesizedReply = normalizedSummary
|
|
77
980
|
? null
|
|
78
981
|
: synthesizeCompletionReply({ checklist: normalizedChecklist, sha: normalizedSha });
|
|
79
|
-
const
|
|
80
|
-
const
|
|
982
|
+
const summaryHuman = normalizedSummary || synthesizedReply || null;
|
|
983
|
+
const effectiveSummary = summaryHuman || rawSummary || null;
|
|
984
|
+
const deliveryText = summaryHuman || null;
|
|
985
|
+
const detailsTechnical = buildTechnicalDetails({
|
|
986
|
+
checklist: normalizedChecklist,
|
|
987
|
+
sha: normalizedSha,
|
|
988
|
+
rawSummary,
|
|
989
|
+
summaryHuman,
|
|
990
|
+
});
|
|
81
991
|
|
|
82
992
|
return {
|
|
83
|
-
version:
|
|
993
|
+
version: 2,
|
|
84
994
|
recordedAt: new Date().toISOString(),
|
|
995
|
+
summary_human: summaryHuman,
|
|
85
996
|
summary: effectiveSummary,
|
|
997
|
+
details_technical: detailsTechnical,
|
|
86
998
|
deliveryText,
|
|
87
|
-
prose,
|
|
999
|
+
prose: normalizedSummary,
|
|
88
1000
|
checklist: normalizedChecklist,
|
|
89
1001
|
sha: normalizedSha,
|
|
90
1002
|
debug: {
|
|
91
1003
|
rawSummary,
|
|
1004
|
+
normalizedSummary,
|
|
92
1005
|
synthesizedReply,
|
|
93
|
-
deliverySource:
|
|
1006
|
+
deliverySource: normalizedSummary ? 'summary_human' : synthesizedReply ? 'technical-synthesis' : 'none',
|
|
94
1007
|
},
|
|
95
1008
|
};
|
|
96
1009
|
}
|
|
97
1010
|
|
|
98
1011
|
export function resolveCompletionDelivery({ lastReply, completion, fallbackSummary } = {}) {
|
|
99
|
-
const
|
|
100
|
-
const
|
|
101
|
-
const
|
|
102
|
-
const
|
|
103
|
-
const
|
|
104
|
-
const meaningfulSummary = [completionSummary, fallback].find(isMeaningfulCompletionText) || null;
|
|
1012
|
+
const rawReply = normalizeCompletionText(lastReply);
|
|
1013
|
+
const rawCompletionSummaryHuman = getCompletionSummaryHuman(completion);
|
|
1014
|
+
const rawCompletionSummary = normalizeCompletionText(completion?.summary);
|
|
1015
|
+
const rawCompletionDelivery = normalizeCompletionText(completion?.deliveryText);
|
|
1016
|
+
const rawFallback = normalizeCompletionText(fallbackSummary);
|
|
105
1017
|
|
|
106
|
-
|
|
1018
|
+
const reply = humanizeCompletionText(lastReply);
|
|
1019
|
+
const completionSummaryHuman = humanizeCompletionText(rawCompletionSummaryHuman);
|
|
1020
|
+
const completionSummary = humanizeCompletionText(completion?.summary);
|
|
1021
|
+
const completionDelivery = humanizeCompletionText(completion?.deliveryText);
|
|
1022
|
+
const fallback = humanizeCompletionText(fallbackSummary);
|
|
1023
|
+
const synthesizedFromTechnical = synthesizeCompletionReply({
|
|
1024
|
+
checklist: completion?.checklist,
|
|
1025
|
+
sha: completion?.sha,
|
|
1026
|
+
});
|
|
1027
|
+
const completionDeliverySource = normalizeCompletionText(completion?.debug?.deliverySource);
|
|
1028
|
+
const preferredSummary = completionSummaryHuman || completionSummary || fallback || synthesizedFromTechnical || null;
|
|
1029
|
+
const authoritativeStructuredSummary = completionDeliverySource && completionDeliverySource !== 'technical-synthesis'
|
|
1030
|
+
? preferredSummary
|
|
1031
|
+
: null;
|
|
1032
|
+
const noisyTexts = [
|
|
1033
|
+
rawReply,
|
|
1034
|
+
rawCompletionSummaryHuman,
|
|
1035
|
+
rawCompletionDelivery,
|
|
1036
|
+
rawCompletionSummary,
|
|
1037
|
+
rawFallback,
|
|
1038
|
+
].filter(isInternalTransportNoiseText);
|
|
1039
|
+
const isDeliverableText = (rawText, summarizedText) => Boolean(summarizedText)
|
|
1040
|
+
&& !isInternalTransportNoiseText(summarizedText)
|
|
1041
|
+
&& (!rawText || !isInternalTransportNoiseText(rawText) || looksLikeRawPayloadText(rawText));
|
|
1042
|
+
const structuredCandidates = [
|
|
1043
|
+
{
|
|
1044
|
+
rawText: rawCompletionSummaryHuman,
|
|
1045
|
+
text: completionSummaryHuman,
|
|
1046
|
+
summary: completionSummaryHuman,
|
|
1047
|
+
source: completionDeliverySource === 'technical-synthesis' ? 'technical-synthesis' : 'summary_human',
|
|
1048
|
+
},
|
|
1049
|
+
{
|
|
1050
|
+
rawText: rawCompletionSummary,
|
|
1051
|
+
text: completionSummary,
|
|
1052
|
+
summary: completionSummary,
|
|
1053
|
+
source: completionDeliverySource === 'technical-synthesis' && completionSummary === synthesizedFromTechnical
|
|
1054
|
+
? 'technical-synthesis'
|
|
1055
|
+
: 'completion-summary',
|
|
1056
|
+
},
|
|
1057
|
+
{
|
|
1058
|
+
rawText: rawCompletionDelivery,
|
|
1059
|
+
text: completionDelivery,
|
|
1060
|
+
summary: completionSummaryHuman || completionSummary || completionDelivery,
|
|
1061
|
+
source: completionDeliverySource || 'completion-legacy',
|
|
1062
|
+
},
|
|
1063
|
+
];
|
|
1064
|
+
|
|
1065
|
+
for (const candidate of structuredCandidates.filter(candidate => candidate.source !== 'technical-synthesis')) {
|
|
1066
|
+
if (!isDeliverableText(candidate.rawText, candidate.text)) continue;
|
|
1067
|
+
const technicalDetailsText = buildTechnicalDetailsText({
|
|
1068
|
+
rawText: candidate.rawText,
|
|
1069
|
+
summaryText: candidate.text,
|
|
1070
|
+
completion,
|
|
1071
|
+
});
|
|
1072
|
+
return {
|
|
1073
|
+
deliveryText: composeDeliveryText(candidate.text, technicalDetailsText),
|
|
1074
|
+
summary: candidate.summary || candidate.text,
|
|
1075
|
+
source: candidate.source,
|
|
1076
|
+
};
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
if (isDeliverableText(rawReply, reply)) {
|
|
1080
|
+
const technicalDetailsText = buildTechnicalDetailsText({
|
|
1081
|
+
rawText: rawReply,
|
|
1082
|
+
summaryText: reply,
|
|
1083
|
+
completion,
|
|
1084
|
+
});
|
|
107
1085
|
return {
|
|
108
|
-
deliveryText: reply,
|
|
109
|
-
summary:
|
|
1086
|
+
deliveryText: composeDeliveryText(reply, technicalDetailsText),
|
|
1087
|
+
summary: authoritativeStructuredSummary || reply,
|
|
110
1088
|
source: 'lastReply',
|
|
111
1089
|
};
|
|
112
1090
|
}
|
|
113
1091
|
|
|
114
|
-
if (
|
|
1092
|
+
if (isDeliverableText(synthesizedFromTechnical, synthesizedFromTechnical)) {
|
|
1093
|
+
return {
|
|
1094
|
+
deliveryText: synthesizedFromTechnical,
|
|
1095
|
+
summary: synthesizedFromTechnical,
|
|
1096
|
+
source: 'technical-synthesis',
|
|
1097
|
+
};
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
for (const candidate of structuredCandidates.filter(candidate => candidate.source === 'technical-synthesis')) {
|
|
1101
|
+
if (!isDeliverableText(candidate.rawText, candidate.text)) continue;
|
|
1102
|
+
const technicalDetailsText = buildTechnicalDetailsText({
|
|
1103
|
+
rawText: candidate.rawText,
|
|
1104
|
+
summaryText: candidate.text,
|
|
1105
|
+
completion,
|
|
1106
|
+
});
|
|
1107
|
+
return {
|
|
1108
|
+
deliveryText: composeDeliveryText(candidate.text, technicalDetailsText),
|
|
1109
|
+
summary: candidate.summary || candidate.text,
|
|
1110
|
+
source: candidate.source,
|
|
1111
|
+
};
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
if (isDeliverableText(rawFallback, fallback)) {
|
|
115
1115
|
return {
|
|
116
|
-
deliveryText:
|
|
117
|
-
summary:
|
|
118
|
-
source:
|
|
1116
|
+
deliveryText: fallback,
|
|
1117
|
+
summary: fallback,
|
|
1118
|
+
source: 'summary',
|
|
119
1119
|
};
|
|
120
1120
|
}
|
|
121
1121
|
|
|
122
|
-
if (
|
|
1122
|
+
if (noisyTexts.length > 0) {
|
|
1123
|
+
const fallbackDelivery = buildInternalNoiseFallback(noisyTexts);
|
|
123
1124
|
return {
|
|
124
|
-
deliveryText:
|
|
125
|
-
summary:
|
|
126
|
-
source:
|
|
1125
|
+
deliveryText: fallbackDelivery,
|
|
1126
|
+
summary: fallbackDelivery,
|
|
1127
|
+
source: 'internal-noise',
|
|
127
1128
|
};
|
|
128
1129
|
}
|
|
129
1130
|
|
|
130
1131
|
return {
|
|
131
1132
|
deliveryText: null,
|
|
132
|
-
summary: preferredSummary || null,
|
|
1133
|
+
summary: authoritativeStructuredSummary || preferredSummary || completionDelivery || reply || null,
|
|
133
1134
|
source: 'none',
|
|
134
1135
|
};
|
|
135
1136
|
}
|