@yeaft/webchat-agent 1.0.173 → 1.0.175

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yeaft/webchat-agent",
3
- "version": "1.0.173",
3
+ "version": "1.0.175",
4
4
  "description": "Remote worker agent for Yeaft Web Code Agent — connects the native Yeaft engine, CLI providers, and workbench tools",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -18,14 +18,19 @@ let shuttingDown = false;
18
18
  let shutdownPromise = null;
19
19
  let serviceFactory = null;
20
20
 
21
- const BROWSER_DETAIL_OPS = new Set(['get', 'create', 'update', 'start', 'cancel', 'guide', 'retry']);
21
+ const BROWSER_DETAIL_OPS = new Set(['get', 'create', 'update', 'start', 'cancel', 'action_input', 'guide', 'retry']);
22
+ const BROWSER_ACTION_DEBUG_OPS = new Set(['get_action_messages', 'get_action_requests', 'get_action_request']);
22
23
  // `files` is an internal server-to-Agent field. The browser relay rejects any
23
24
  // client-supplied value and only emits files resolved from owned upload ids.
24
25
  const BROWSER_FILE_FIELDS = Object.freeze({
25
26
  create: [
26
27
  'title', 'goal', 'acceptanceCriteria', 'workItemType', 'workDir', 'reuseMemory', 'files', 'start',
27
28
  ],
29
+ action_input: ['id', 'text', 'actionId', 'revision', 'files'],
28
30
  guide: ['id', 'guidance', 'actionId', 'revision', 'files'],
31
+ get_action_messages: ['id', 'actionId', 'cursor', 'limit'],
32
+ get_action_requests: ['id', 'actionId'],
33
+ get_action_request: ['id', 'actionId', 'runId', 'requestId'],
29
34
  });
30
35
 
31
36
  function browserFilePayload(op, value) {
@@ -100,6 +105,7 @@ async function createDefaultService() {
100
105
  },
101
106
  policyProvider: async () => readWorkCenterSettings(yeaftDir),
102
107
  attachmentRoot: join(yeaftDir, 'work-center', 'attachments'),
108
+ yeaftDir,
103
109
  actionWorktreeRoot: join(yeaftDir, 'work-center', 'worktrees'),
104
110
  registry: defaultRegistry,
105
111
  store: null,
@@ -184,7 +190,7 @@ export async function handleWorkCenterRequest(msg) {
184
190
  const workCenter = await ensureWorkCenter();
185
191
  const payload = Object.hasOwn(BROWSER_FILE_FIELDS, op)
186
192
  ? browserFilePayload(op, msg.payload)
187
- : (msg.payload || {});
193
+ : (BROWSER_ACTION_DEBUG_OPS.has(op) ? browserFilePayload(op, msg.payload) : (msg.payload || {}));
188
194
  data = await workCenter.handle(op, payload);
189
195
  }
190
196
  if (BROWSER_DETAIL_OPS.has(op)) data = projectWorkItemDetail(data);
@@ -210,16 +210,54 @@ export class WorkflowController {
210
210
  instruction: actionInstruction(step, workItem, context, renderSessionContextSnapshot(workItem.sessionContext)),
211
211
  maxAttempts: previous.maxAttempts || 2,
212
212
  };
213
- }, input.attachments);
213
+ }, input.attachments, input.addedAttachments);
214
214
  if (!detail) throw new Error(`WorkItem not found: ${id}`);
215
215
  return detail;
216
216
  }
217
217
 
218
+ input(id, input = {}) {
219
+ const text = typeof input.text === 'string' ? input.text.trim().slice(0, 8_000) : '';
220
+ const addedAttachmentCount = Math.max(0, Number(input.addedAttachmentCount) || 0);
221
+ if (!text && addedAttachmentCount === 0) throw new Error('Action input or attachments are required');
222
+ const workItem = this.store.getWorkItem(id);
223
+ if (!workItem) throw new Error(`WorkItem not found: ${id}`);
224
+ if (['ready', 'running'].includes(workItem.status)) {
225
+ if (workItem.currentActionId !== input.actionId || workItem.revision !== input.revision) {
226
+ throw new Error('Action changed before input was applied; refresh and try again');
227
+ }
228
+ return this.guide(id, {
229
+ guidance: text,
230
+ actionId: input.actionId,
231
+ revision: input.revision,
232
+ addedAttachmentCount,
233
+ addedAttachments: input.addedAttachments,
234
+ attachments: input.attachments,
235
+ });
236
+ }
237
+ if (!['waiting', 'needs_attention'].includes(workItem.status)) {
238
+ throw new Error(`WorkItem in ${workItem.status} cannot accept Action input`);
239
+ }
240
+ if (workItem.currentActionId !== input.actionId || workItem.revision !== input.revision) {
241
+ throw new Error('Action changed before input was applied; refresh and try again');
242
+ }
243
+ return this.retry(id, {
244
+ answer: text,
245
+ addedAttachmentCount,
246
+ expected: { actionId: input.actionId, revision: input.revision },
247
+ attachments: input.attachments,
248
+ inputEvent: {
249
+ text: text || `The user added ${addedAttachmentCount} attachment(s) as additional context for this Action.`,
250
+ attachments: input.addedAttachments,
251
+ },
252
+ });
253
+ }
254
+
218
255
  retry(id, input = {}) {
219
256
  const answer = typeof input.answer === 'string' ? input.answer.trim().slice(0, 8_000) : '';
257
+ const addedAttachmentCount = Math.max(0, Number(input.addedAttachmentCount) || 0);
220
258
  const detail = this.store.retryWorkItemAtomic(id, (workItem, previous, previousRun) => {
221
- if (workItem.status === 'waiting' && !answer) {
222
- throw new Error('answer is required to resume a waiting WorkItem');
259
+ if (workItem.status === 'waiting' && !answer && addedAttachmentCount === 0) {
260
+ throw new Error('answer or attachments are required to resume a waiting WorkItem');
223
261
  }
224
262
  const step = previous
225
263
  ? {
@@ -244,7 +282,9 @@ export class WorkflowController {
244
282
  summary: previousRun.summary || '',
245
283
  evidence: normalizeEvidence(previousRun.evidence),
246
284
  waitingReason: previousRun.waitingReason || null,
247
- answer: answer || null,
285
+ answer: answer || (addedAttachmentCount > 0
286
+ ? `The user added ${addedAttachmentCount} attachment(s) as additional context for this Action.`
287
+ : null),
248
288
  });
249
289
  }
250
290
  return {
@@ -253,6 +293,10 @@ export class WorkflowController {
253
293
  instruction: actionInstruction(step, workItem, context, renderSessionContextSnapshot(workItem.sessionContext)),
254
294
  maxAttempts: previous?.maxAttempts || 2,
255
295
  };
296
+ }, {
297
+ expected: input.expected || null,
298
+ attachments: input.attachments,
299
+ inputEvent: input.inputEvent || null,
256
300
  });
257
301
  if (!detail) throw new Error(`WorkItem not found: ${id}`);
258
302
  return detail;
@@ -0,0 +1,541 @@
1
+ const MAX_DEBUG_STRING_BYTES = 64 * 1024;
2
+ export const MAX_ACTION_REQUEST_DETAIL_BYTES = 256 * 1024;
3
+ const MAX_ACTION_REQUEST_INPUT_BYTES = MAX_ACTION_REQUEST_DETAIL_BYTES;
4
+ const MAX_ACTION_REQUEST_INPUT_LOOPS = 16;
5
+ const MAX_ACTION_REQUEST_INPUT_TOOLS = MAX_ACTION_REQUEST_INPUT_LOOPS * 128;
6
+ const MAX_DEBUG_ARRAY_ITEMS = 128;
7
+ const SENSITIVE_NAMES = new Set([
8
+ 'apikey', 'xapikey', 'token', 'accesstoken', 'refreshtoken', 'idtoken', 'clientsecret',
9
+ 'secret', 'signature', 'sig', 'credential', 'password', 'passwd',
10
+ 'authorization', 'proxyauthorization', 'auth', 'code', 'cookie', 'setcookie',
11
+ ]);
12
+ const BINARY_DATA_TYPES = new Set(['base64', 'redactedthinking', 'redacted_thinking']);
13
+ const MAX_SENSITIVE_NAME_LENGTH = Math.max(...[...SENSITIVE_NAMES].map(name => name.length));
14
+ const MAX_SENSITIVE_NAME_SOURCE_CHARS = 256;
15
+ const MAX_QUOTED_SECRET_VALUE_CHARS = 64 * 1024;
16
+ const MAX_URL_NAME_DECODE_STEPS = 3;
17
+
18
+ function byteLength(value) {
19
+ return Buffer.byteLength(String(value || ''), 'utf8');
20
+ }
21
+
22
+ function jsonByteLength(value) {
23
+ return Buffer.byteLength(JSON.stringify(value), 'utf8');
24
+ }
25
+
26
+ function truncateUtf8(value, maxBytes = MAX_DEBUG_STRING_BYTES) {
27
+ const bytes = Buffer.from(String(value || ''), 'utf8');
28
+ if (bytes.length <= maxBytes) return bytes.toString('utf8');
29
+ const marker = '\n[truncated to browser debug budget]';
30
+ const contentBytes = Math.max(0, maxBytes - byteLength(marker));
31
+ let end = Math.min(contentBytes, bytes.length);
32
+ while (end > 0 && (bytes[end] & 0xc0) === 0x80) end -= 1;
33
+ return `${bytes.subarray(0, end).toString('utf8')}${marker}`;
34
+ }
35
+
36
+ function normalizeSensitiveName(name) {
37
+ return String(name || '').toLowerCase().replace(/[^a-z0-9]/g, '');
38
+ }
39
+
40
+ function isSensitiveName(name) {
41
+ return SENSITIVE_NAMES.has(normalizeSensitiveName(name));
42
+ }
43
+
44
+ function decodeQuotedName(value, quote) {
45
+ if (quote === "'") return value;
46
+ try {
47
+ return JSON.parse(`"${value}"`);
48
+ } catch {
49
+ return null;
50
+ }
51
+ }
52
+
53
+ function quotedNameBeforeOperator(text, operatorIndex) {
54
+ let end = operatorIndex;
55
+ while (end > 0 && /\s/.test(text[end - 1])) end -= 1;
56
+ const quote = text[end - 1];
57
+ if (quote !== '"' && quote !== "'") return undefined;
58
+ const earliest = Math.max(0, end - MAX_SENSITIVE_NAME_SOURCE_CHARS);
59
+ for (let start = end - 2; start >= earliest; start -= 1) {
60
+ if (text[start] !== quote) continue;
61
+ let slashCount = 0;
62
+ for (let cursor = start - 1; cursor >= earliest && text[cursor] === '\\'; cursor -= 1) {
63
+ slashCount += 1;
64
+ }
65
+ if (slashCount % 2 === 0) {
66
+ return decodeQuotedName(text.slice(start + 1, end - 1), quote);
67
+ }
68
+ }
69
+ return null;
70
+ }
71
+
72
+ function candidateStart(text, end, colonOperator) {
73
+ let start = end;
74
+ const boundary = colonOperator
75
+ ? /[\r\n,:=;{}\[\]()"'?&]/
76
+ : /[\r\n,=;{}\[\]()"'?&]/;
77
+ while (start > 0 && !boundary.test(text[start - 1])) start -= 1;
78
+ return start;
79
+ }
80
+
81
+ function hasSensitiveSuffix(text, start, end) {
82
+ let reversed = '';
83
+ const earliest = Math.max(start, end - MAX_SENSITIVE_NAME_SOURCE_CHARS);
84
+ for (let index = end - 1; index >= earliest; index -= 1) {
85
+ const character = text[index].toLowerCase();
86
+ if (/[a-z0-9]/.test(character)) {
87
+ reversed += character;
88
+ if (reversed.length > MAX_SENSITIVE_NAME_LENGTH) return false;
89
+ continue;
90
+ }
91
+ if (!reversed) continue;
92
+ const normalized = [...reversed].reverse().join('');
93
+ if (/[\s:&?;,={}\[\]()"']/.test(character)) {
94
+ if (SENSITIVE_NAMES.has(normalized)) return true;
95
+ if (!/[\s:]/.test(character)) return false;
96
+ }
97
+ }
98
+ return SENSITIVE_NAMES.has([...reversed].reverse().join(''));
99
+ }
100
+
101
+ function assignmentHasSensitiveName(text, operatorIndex, operator) {
102
+ let end = operatorIndex;
103
+ while (end > 0 && /\s/.test(text[end - 1])) end -= 1;
104
+ if (end === 0) return false;
105
+ const quotedName = quotedNameBeforeOperator(text, operatorIndex);
106
+ if (quotedName !== undefined) return quotedName == null || isSensitiveName(quotedName);
107
+ if (operator === '=') return hasSensitiveSuffix(text, 0, end);
108
+ const start = candidateStart(text, end, true);
109
+ const candidate = text.slice(start, end).trim();
110
+ if (/(?:^|\s)status\s+code$/i.test(candidate)) return false;
111
+ return isSensitiveName(candidate) || hasSensitiveSuffix(text, start, end);
112
+ }
113
+
114
+ function sensitiveAssignmentOperators(value, allowColon = true) {
115
+ const text = String(value || '');
116
+ const operators = [];
117
+ for (let index = 0; index < text.length; index += 1) {
118
+ const operator = text[index];
119
+ if (operator !== '=' && operator !== ':') continue;
120
+ if (operator === ':') {
121
+ if (!allowColon) continue;
122
+ let valueStart = index + 1;
123
+ const valueLimit = Math.min(text.length, index + MAX_SENSITIVE_NAME_SOURCE_CHARS);
124
+ while (valueStart < valueLimit && /[\t ]/.test(text[valueStart])) valueStart += 1;
125
+ const quotedName = quotedNameBeforeOperator(text, index);
126
+ const hasStructuredValue = valueStart > index + 1 || /["']/.test(text[valueStart] || '');
127
+ if (quotedName === undefined && !hasStructuredValue) {
128
+ let end = index;
129
+ const earliest = Math.max(0, index - MAX_SENSITIVE_NAME_SOURCE_CHARS);
130
+ while (end > earliest && /\s/.test(text[end - 1])) end -= 1;
131
+ const start = candidateStart(text, end, true);
132
+ if (start > 0 && text[start - 1] === ':') continue;
133
+ }
134
+ }
135
+ if (assignmentHasSensitiveName(text, index, operator)) operators.push(index);
136
+ }
137
+ return operators;
138
+ }
139
+
140
+ function containsSensitiveFieldSyntax(value) {
141
+ return sensitiveAssignmentOperators(value).length > 0;
142
+ }
143
+
144
+ function assignmentUsesWholeLogicalValue(text, operatorIndex) {
145
+ let end = operatorIndex;
146
+ while (end > 0 && /\s/.test(text[end - 1])) end -= 1;
147
+ const start = candidateStart(text, end, text[operatorIndex] === ':');
148
+ const normalized = normalizeSensitiveName(text.slice(start, end));
149
+ return ['authorization', 'proxyauthorization', 'cookie', 'setcookie']
150
+ .some(name => normalized.endsWith(name));
151
+ }
152
+
153
+ function assignmentValueReplacement(text, operatorIndex) {
154
+ let start = operatorIndex + 1;
155
+ const valueLimit = Math.min(text.length, operatorIndex + MAX_SENSITIVE_NAME_SOURCE_CHARS);
156
+ while (start < valueLimit && /[\t ]/.test(text[start])) start += 1;
157
+ if (start === valueLimit && start < text.length) {
158
+ return { start: operatorIndex + 1, end: text.length, value: '***' };
159
+ }
160
+ if (start >= text.length || /[\r\n]/.test(text[start])) return null;
161
+ if (assignmentUsesWholeLogicalValue(text, operatorIndex)) {
162
+ let end = start;
163
+ while (end < text.length && !/[;\r\n]/.test(text[end])) end += 1;
164
+ return { start, end, value: '***' };
165
+ }
166
+ const quote = text[start];
167
+ if (quote === '"' || quote === "'") {
168
+ let end = start + 1;
169
+ const quoteLimit = Math.min(text.length, start + MAX_QUOTED_SECRET_VALUE_CHARS);
170
+ while (end < quoteLimit && !/[\r\n]/.test(text[end])) {
171
+ if (text[end] === quote && text[end - 1] !== '\\') {
172
+ return { start, end: end + 1, value: `${quote}***${quote}` };
173
+ }
174
+ end += 1;
175
+ }
176
+ return { start, end, value: `${quote}***${quote}` };
177
+ }
178
+ let end = start;
179
+ while (end < text.length && !/[\s,;=&}\]]/.test(text[end])) end += 1;
180
+ return end > start ? { start, end, value: '***' } : null;
181
+ }
182
+
183
+ function redactSensitiveAssignments(value, allowColon = true) {
184
+ const text = String(value || '');
185
+ const replacements = sensitiveAssignmentOperators(text, allowColon)
186
+ .map(index => assignmentValueReplacement(text, index))
187
+ .filter(Boolean);
188
+ const merged = [];
189
+ for (const replacement of replacements) {
190
+ const previous = merged.at(-1);
191
+ if (!previous || replacement.start >= previous.end) {
192
+ merged.push({ ...replacement });
193
+ continue;
194
+ }
195
+ if (replacement.end <= previous.end) continue;
196
+ previous.end = replacement.end;
197
+ previous.value = '***';
198
+ }
199
+ if (merged.length === 0) return text;
200
+ const parts = [];
201
+ let cursor = 0;
202
+ for (const replacement of merged) {
203
+ parts.push(text.slice(cursor, replacement.start), replacement.value);
204
+ cursor = replacement.end;
205
+ }
206
+ parts.push(text.slice(cursor));
207
+ return parts.join('');
208
+ }
209
+
210
+ function isSensitiveUrlName(name) {
211
+ let decoded = String(name || '');
212
+ for (let step = 0; step < MAX_URL_NAME_DECODE_STEPS; step += 1) {
213
+ if (isSensitiveName(decoded)) return true;
214
+ let next;
215
+ try {
216
+ next = decodeURIComponent(decoded.replace(/\+/g, ' '));
217
+ } catch {
218
+ return true;
219
+ }
220
+ if (next === decoded) return false;
221
+ decoded = next;
222
+ }
223
+ return isSensitiveName(decoded) || decoded.includes('%');
224
+ }
225
+
226
+ function boundedDebugInputBytes(value, maxBytes) {
227
+ let bytes = 0;
228
+ const seen = new Set();
229
+ const stack = [value];
230
+ while (stack.length > 0 && bytes <= maxBytes) {
231
+ const item = stack.pop();
232
+ if (item == null) {
233
+ bytes += 4;
234
+ continue;
235
+ }
236
+ if (typeof item === 'string') {
237
+ if (item.length > maxBytes - bytes) return maxBytes + 1;
238
+ bytes += byteLength(item);
239
+ continue;
240
+ }
241
+ if (typeof item !== 'object') {
242
+ bytes += byteLength(item);
243
+ continue;
244
+ }
245
+ if (seen.has(item)) continue;
246
+ seen.add(item);
247
+ if (Array.isArray(item)) {
248
+ const length = Math.min(item.length, MAX_DEBUG_ARRAY_ITEMS);
249
+ if (item.length > length) bytes += 32;
250
+ for (let index = length - 1; index >= 0; index -= 1) stack.push(item[index]);
251
+ continue;
252
+ }
253
+ for (const key in item) {
254
+ if (!Object.hasOwn(item, key)) continue;
255
+ bytes += byteLength(key);
256
+ if (bytes > maxBytes) break;
257
+ stack.push(item[key]);
258
+ }
259
+ }
260
+ return bytes > maxBytes ? maxBytes + 1 : bytes;
261
+ }
262
+
263
+ export function limitActionRequestDebugInput(loopValues, toolValues) {
264
+ const sourceLoops = Array.isArray(loopValues) ? loopValues : [];
265
+ const sourceTools = Array.isArray(toolValues) ? toolValues : [];
266
+ const candidates = sourceLoops.slice(-MAX_ACTION_REQUEST_INPUT_LOOPS);
267
+ const candidateNumbers = new Set(candidates.map(loop => Number(loop?.loopNumber) || 0));
268
+ const toolsByLoop = new Map();
269
+ for (const tool of sourceTools.slice(-MAX_ACTION_REQUEST_INPUT_TOOLS)) {
270
+ const loopNumber = Number(tool?.loopNumber) || 0;
271
+ if (!candidateNumbers.has(loopNumber)) continue;
272
+ const tools = toolsByLoop.get(loopNumber) || [];
273
+ tools.push(tool);
274
+ toolsByLoop.set(loopNumber, tools);
275
+ }
276
+
277
+ let remainingBytes = MAX_ACTION_REQUEST_INPUT_BYTES;
278
+ const loops = [];
279
+ const tools = [];
280
+ for (let index = candidates.length - 1; index >= 0; index -= 1) {
281
+ const loop = candidates[index];
282
+ const loopTools = toolsByLoop.get(Number(loop?.loopNumber) || 0) || [];
283
+ const inputBytes = boundedDebugInputBytes({ loop, tools: loopTools }, remainingBytes);
284
+ if (inputBytes > remainingBytes) continue;
285
+ loops.unshift(loop);
286
+ tools.unshift(...loopTools);
287
+ remainingBytes -= inputBytes;
288
+ }
289
+ return {
290
+ loops,
291
+ tools,
292
+ omittedLoopCount: sourceLoops.length - loops.length,
293
+ };
294
+ }
295
+
296
+ function sanitizeUrlQueryNames(value) {
297
+ return String(value || '').replace(
298
+ /([?&])([^=&#]+)=([^&#]*)/g,
299
+ (match, prefix, name) => (isSensitiveUrlName(name) ? `${prefix}${name}=***` : match),
300
+ );
301
+ }
302
+
303
+ export function sanitizeDebugUrl(value) {
304
+ const text = sanitizeUrlQueryNames(truncateUtf8(value, MAX_DEBUG_STRING_BYTES));
305
+ try {
306
+ const url = new URL(text);
307
+ url.username = '';
308
+ url.password = '';
309
+ return truncateUtf8(url.toString());
310
+ } catch {
311
+ return truncateUtf8(text.replace(/\/\/[^/@\s]+@/g, '//'));
312
+ }
313
+ }
314
+
315
+ function redactSensitiveHeaderLines(value) {
316
+ return String(value || '').replace(
317
+ /(^|\r\n|\r|\n)([\t ]*)([^:\r\n]{1,256})([\t ]*:[\t ]*)([^\r\n]*)/g,
318
+ (match, boundary, whitespace, name, operator) => (isSensitiveName(name)
319
+ ? `${boundary}${whitespace}${name}${operator}***`
320
+ : match),
321
+ );
322
+ }
323
+
324
+ export function sanitizeDiagnosticText(value, maxBytes = 8 * 1024) {
325
+ let text = truncateUtf8(value, maxBytes);
326
+ text = text.replace(/https?:\/\/[^\s"'<>]+/gi, match => sanitizeDebugUrl(match));
327
+ text = redactSensitiveHeaderLines(text);
328
+ text = redactSensitiveAssignments(text);
329
+ text = text.replace(/\b(Bearer)\s+[^\s,;}\]]+/gi, '$1 ***');
330
+ return truncateUtf8(text, maxBytes);
331
+ }
332
+
333
+ function omittedBinary(value) {
334
+ return `[binary data omitted: ${byteLength(value)} bytes]`;
335
+ }
336
+
337
+ function sanitizeSseMetadataLine(line) {
338
+ const match = String(line || '').match(/^(\s*(?:event|id|retry)?\s*:)(.*)$/i);
339
+ if (!match) return sanitizeDiagnosticText(line, MAX_DEBUG_STRING_BYTES);
340
+ let value = truncateUtf8(match[2], MAX_DEBUG_STRING_BYTES);
341
+ value = value.replace(/https?:\/\/[^\s"'<>]+/gi, url => sanitizeDebugUrl(url));
342
+ value = redactSensitiveAssignments(value, false);
343
+ const colonValue = value.match(/^(\s*)([^:=\s]+)(\s+|\s*:\s+)(.+)$/);
344
+ if (colonValue && isSensitiveName(colonValue[2])) {
345
+ value = `${colonValue[1]}${colonValue[2]}${colonValue[3]}***`;
346
+ }
347
+ value = value.replace(/\b(Bearer)\s+[^\s,;}\]]+/gi, '$1 ***');
348
+ return truncateUtf8(`${match[1]}${value}`, MAX_DEBUG_STRING_BYTES);
349
+ }
350
+
351
+ function sanitizeSseEvent(event, seen) {
352
+ const lines = event.split(/\r?\n/);
353
+ const dataIndexes = [];
354
+ const dataParts = [];
355
+ for (const [index, line] of lines.entries()) {
356
+ const match = line.match(/^\s*data:\s?(.*)$/);
357
+ if (!match) continue;
358
+ dataIndexes.push(index);
359
+ dataParts.push(match[1]);
360
+ }
361
+ if (dataParts.length === 0) {
362
+ return lines.map(line => sanitizeSseMetadataLine(line)).join('\n');
363
+ }
364
+
365
+ const data = dataParts.join('\n');
366
+ let sanitized = data;
367
+ if (data !== '[DONE]') {
368
+ try {
369
+ sanitized = JSON.stringify(sanitizeDebugValue(JSON.parse(data), null, '', seen));
370
+ } catch {
371
+ sanitized = containsSensitiveFieldSyntax(data)
372
+ ? '[redacted SSE event: malformed sensitive data]'
373
+ : sanitizeDiagnosticText(data, MAX_DEBUG_STRING_BYTES);
374
+ }
375
+ }
376
+
377
+ const firstDataIndex = dataIndexes[0];
378
+ const dataIndexSet = new Set(dataIndexes);
379
+ const sanitizedDataLines = String(sanitized).split('\n').map(line => `data: ${line}`).join('\n');
380
+ const sanitizedLines = [];
381
+ for (const [index, line] of lines.entries()) {
382
+ if (dataIndexSet.has(index)) {
383
+ if (index === firstDataIndex) sanitizedLines.push(sanitizedDataLines);
384
+ continue;
385
+ }
386
+ sanitizedLines.push(sanitizeSseMetadataLine(line));
387
+ }
388
+ return sanitizedLines.join('\n');
389
+ }
390
+
391
+ function sanitizeSseBody(value, seen) {
392
+ const text = truncateUtf8(value, MAX_DEBUG_STRING_BYTES);
393
+ const trailingSeparator = /(?:\r?\n){2}$/.test(text) ? '\n\n' : '';
394
+ const events = text.split(/(?:\r?\n){2}/);
395
+ if (events.at(-1) === '') events.pop();
396
+ return truncateUtf8(`${events.map(event => sanitizeSseEvent(event, seen)).join('\n\n')}${trailingSeparator}`);
397
+ }
398
+
399
+ function sanitizeHeaderEntries(entries, seen) {
400
+ return entries.map(([name, headerValue]) => [
401
+ String(name || ''),
402
+ isSensitiveName(name) ? '***' : sanitizeDebugValue(headerValue, null, String(name || ''), seen),
403
+ ]);
404
+ }
405
+
406
+ function sanitizeHeaders(value, seen, raw = false) {
407
+ if (typeof value === 'string') return sanitizeDiagnosticText(value, MAX_DEBUG_STRING_BYTES);
408
+ if (!value || typeof value !== 'object') return sanitizeDebugValue(value, null, '', seen);
409
+ if (!Array.isArray(value)) {
410
+ return Object.fromEntries(sanitizeHeaderEntries(Object.entries(value), seen));
411
+ }
412
+ const items = value.slice(0, MAX_DEBUG_ARRAY_ITEMS);
413
+ if (raw || items.every(item => !Array.isArray(item))) {
414
+ const out = [];
415
+ for (let index = 0; index < items.length; index += 2) {
416
+ const name = items[index];
417
+ out.push(name);
418
+ if (index + 1 < items.length) {
419
+ out.push(isSensitiveName(name)
420
+ ? '***'
421
+ : sanitizeDebugValue(items[index + 1], null, String(name || ''), seen));
422
+ }
423
+ }
424
+ if (value.length > items.length) out.push(`[${value.length - items.length} additional items omitted]`);
425
+ return out;
426
+ }
427
+ const out = items.map(item => {
428
+ if (!Array.isArray(item) || item.length < 2) return sanitizeDebugValue(item, value, '', seen);
429
+ const [name, headerValue, ...rest] = item;
430
+ return [name, isSensitiveName(name) ? '***' : sanitizeDebugValue(headerValue, null, String(name || ''), seen),
431
+ ...rest.map(valueItem => sanitizeDebugValue(valueItem, item, '', seen))];
432
+ });
433
+ if (value.length > items.length) out.push(`[${value.length - items.length} additional items omitted]`);
434
+ return out;
435
+ }
436
+
437
+ export function sanitizeDebugValue(value, parent = null, key = '', seen = new WeakSet()) {
438
+ if (key && isSensitiveName(key)) return '***';
439
+ if (value == null || typeof value === 'number' || typeof value === 'boolean') return value;
440
+ if (typeof value === 'string') {
441
+ const text = truncateUtf8(value, MAX_DEBUG_STRING_BYTES);
442
+ if (key === 'url') return sanitizeDebugUrl(text);
443
+ if (text.startsWith('data:') && text.includes(';base64,')) return omittedBinary(value);
444
+ const parentType = String(parent?.type || '').toLowerCase().replace(/[^a-z0-9_]/g, '');
445
+ if (key === 'data' && BINARY_DATA_TYPES.has(parentType)) return omittedBinary(value);
446
+ if (key === 'body' && /^[\s\r\n]*[\[{]/.test(text)) {
447
+ try {
448
+ return truncateUtf8(JSON.stringify(sanitizeDebugValue(JSON.parse(text), null, '', seen)));
449
+ } catch {
450
+ if (containsSensitiveFieldSyntax(text)) {
451
+ return '[redacted malformed JSON: sensitive data]';
452
+ }
453
+ }
454
+ }
455
+ return sanitizeDiagnosticText(text, MAX_DEBUG_STRING_BYTES);
456
+ }
457
+ if (typeof value !== 'object') return truncateUtf8(String(value));
458
+ if (seen.has(value)) return '[circular value omitted]';
459
+ seen.add(value);
460
+ try {
461
+ if (Array.isArray(value)) {
462
+ const items = value.slice(0, MAX_DEBUG_ARRAY_ITEMS)
463
+ .map(item => sanitizeDebugValue(item, value, '', seen));
464
+ if (value.length > items.length) {
465
+ items.push(`[${value.length - items.length} additional items omitted]`);
466
+ }
467
+ return items;
468
+ }
469
+ const out = {};
470
+ for (const [childKey, item] of Object.entries(value)) {
471
+ if (childKey === 'headers' || childKey === 'rawHeaders') {
472
+ out[childKey] = sanitizeHeaders(item, seen, childKey === 'rawHeaders');
473
+ } else if (childKey === 'body' && value.format === 'sse' && typeof item === 'string') {
474
+ out.body = sanitizeSseBody(item, seen);
475
+ } else {
476
+ out[childKey] = sanitizeDebugValue(item, value, childKey, seen);
477
+ }
478
+ }
479
+ return out;
480
+ } finally {
481
+ seen.delete(value);
482
+ }
483
+ }
484
+
485
+ export function enforceActionRequestDetailBudget(detail, omittedLoopCount = 0) {
486
+ if (omittedLoopCount > 0 && detail?.request) {
487
+ detail.request.truncated = true;
488
+ detail.request.omittedLoopCount = omittedLoopCount;
489
+ }
490
+ if (!detail?.request || jsonByteLength(detail) <= MAX_ACTION_REQUEST_DETAIL_BYTES) return detail;
491
+ detail.request.truncated = true;
492
+ const loops = Array.isArray(detail.request.loops) ? detail.request.loops : [];
493
+ const stages = [
494
+ loop => { loop.rawResponse = null; },
495
+ loop => { loop.rawRequest = null; },
496
+ loop => {
497
+ for (const tool of Array.isArray(loop.tools) ? loop.tools : []) {
498
+ tool.output = null;
499
+ tool.input = null;
500
+ }
501
+ },
502
+ loop => { loop.messages = []; },
503
+ loop => { loop.systemPrompt = ''; },
504
+ loop => { loop.response = ''; },
505
+ ];
506
+ for (const stage of stages) {
507
+ for (const loop of loops) stage(loop);
508
+ if (jsonByteLength(detail) <= MAX_ACTION_REQUEST_DETAIL_BYTES) return detail;
509
+ }
510
+ while (loops.length > 1 && jsonByteLength(detail) > MAX_ACTION_REQUEST_DETAIL_BYTES) {
511
+ loops.shift();
512
+ detail.request.omittedLoopCount = (detail.request.omittedLoopCount || 0) + 1;
513
+ }
514
+ if (jsonByteLength(detail) <= MAX_ACTION_REQUEST_DETAIL_BYTES) return detail;
515
+ detail.request.omittedLoopCount = (detail.request.omittedLoopCount || 0) + loops.length;
516
+ detail.request.loops = [];
517
+ if (jsonByteLength(detail) <= MAX_ACTION_REQUEST_DETAIL_BYTES) return detail;
518
+ const request = detail.request;
519
+ const metadata = value => truncateUtf8(value, 4 * 1024);
520
+ return {
521
+ actionId: metadata(detail.actionId),
522
+ request: {
523
+ id: metadata(request.id),
524
+ runId: metadata(request.runId),
525
+ status: metadata(request.status),
526
+ model: metadata(request.model),
527
+ vp: request.vp ? {
528
+ id: metadata(request.vp.id),
529
+ name: metadata(request.vp.name),
530
+ } : null,
531
+ openedAt: request.openedAt,
532
+ closedAt: request.closedAt,
533
+ loopCount: request.loopCount,
534
+ totalMs: request.totalMs,
535
+ totalTokens: request.totalTokens,
536
+ loops: [],
537
+ truncated: true,
538
+ omittedLoopCount: detail.request.omittedLoopCount,
539
+ },
540
+ };
541
+ }