@yeaft/webchat-agent 0.1.564 → 0.1.567

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": "0.1.564",
3
+ "version": "0.1.567",
4
4
  "description": "Remote agent for Yeaft WebChat — connects worker machines to the central server",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -103,6 +103,58 @@ export class LLMAbortError extends Error {
103
103
 
104
104
  // ─── task-344: Raw payload redaction helper ────────────────────
105
105
 
106
+ /**
107
+ * task-344 follow-up (N2): cap raw payload size exposed via onRawExchange.
108
+ * Prevents the web debug store from linear-growing when a single turn
109
+ * sends / receives megabytes of content. 256 KiB per field per turn.
110
+ */
111
+ export const RAW_PAYLOAD_CAP_BYTES = 256 * 1024;
112
+
113
+ /**
114
+ * Truncate a string to at most `cap` bytes (UTF-8). When within cap the
115
+ * original is returned; otherwise a prefix with a trailing
116
+ * `…[truncated, original N bytes]` marker. Non-string inputs pass through.
117
+ *
118
+ * @param {string} s
119
+ * @param {number} [cap=RAW_PAYLOAD_CAP_BYTES]
120
+ * @returns {string}
121
+ */
122
+ export function capRawString(s, cap = RAW_PAYLOAD_CAP_BYTES) {
123
+ if (typeof s !== 'string') return s;
124
+ const encoder = new TextEncoder();
125
+ const fullBytes = encoder.encode(s);
126
+ if (fullBytes.length <= cap) return s;
127
+ const decoder = new TextDecoder('utf-8', { fatal: false });
128
+ const prefix = decoder.decode(fullBytes.slice(0, cap));
129
+ return `${prefix}…[truncated, original ${fullBytes.length} bytes]`;
130
+ }
131
+
132
+ /**
133
+ * Cap the `body` field of a rawRequest envelope. JSON-stringifies objects
134
+ * before sizing so an oversized `messages` array does not escape the cap.
135
+ * When truncation fires, body becomes a string (JSON prefix + marker);
136
+ * objects under cap stay objects.
137
+ *
138
+ * @param {{ url: string, method: string, headers: object, body: any }} req
139
+ * @param {number} [cap=RAW_PAYLOAD_CAP_BYTES]
140
+ * @returns {{ url: string, method: string, headers: object, body: any }}
141
+ */
142
+ export function capRawRequest(req, cap = RAW_PAYLOAD_CAP_BYTES) {
143
+ if (!req || typeof req !== 'object') return req;
144
+ let body = req.body;
145
+ if (body != null && typeof body !== 'string') {
146
+ let serialized;
147
+ try { serialized = JSON.stringify(body); }
148
+ catch { serialized = String(body); }
149
+ if (typeof serialized === 'string' && new TextEncoder().encode(serialized).length > cap) {
150
+ body = capRawString(serialized, cap);
151
+ }
152
+ } else if (typeof body === 'string') {
153
+ body = capRawString(body, cap);
154
+ }
155
+ return { url: req.url, method: req.method, headers: req.headers, body };
156
+ }
157
+
106
158
  /**
107
159
  * Redact sensitive headers (API keys / bearer tokens) from a raw request
108
160
  * shape before exposing it to debug UI. Always returns a NEW object — never
@@ -14,6 +14,9 @@ import {
14
14
  LLMServerError,
15
15
  LLMAbortError,
16
16
  redactRawRequest,
17
+ capRawRequest,
18
+ capRawString,
19
+ RAW_PAYLOAD_CAP_BYTES,
17
20
  } from './adapter.js';
18
21
  import {
19
22
  normalizeEffort,
@@ -182,7 +185,8 @@ export class AnthropicAdapter extends LLMAdapter {
182
185
  };
183
186
 
184
187
  // task-344: expose raw request (redacted) for debug panel.
185
- const rawRequest = redactRawRequest({ url, method: 'POST', headers, body });
188
+ // task-344 follow-up (N2): cap body to RAW_PAYLOAD_CAP_BYTES.
189
+ const rawRequest = capRawRequest(redactRawRequest({ url, method: 'POST', headers, body }));
186
190
 
187
191
  const response = await fetch(url, {
188
192
  method: 'POST',
@@ -203,7 +207,8 @@ export class AnthropicAdapter extends LLMAdapter {
203
207
  headers: response.headers && typeof response.headers.entries === 'function'
204
208
  ? Object.fromEntries(response.headers.entries())
205
209
  : {},
206
- body: errorBody,
210
+ // task-344 follow-up (N2): cap error body.
211
+ body: capRawString(errorBody),
207
212
  },
208
213
  });
209
214
  } catch { /* ignore */ }
@@ -219,7 +224,11 @@ export class AnthropicAdapter extends LLMAdapter {
219
224
  let currentToolName = null;
220
225
  let currentToolInput = '';
221
226
  // task-344: accumulate raw SSE body for debug exposure.
227
+ // task-344 follow-up (N2): cap growth — once past RAW_PAYLOAD_CAP_BYTES
228
+ // we freeze the captured body and record total-bytes-seen separately.
222
229
  let rawSseBody = '';
230
+ let rawSseTotalBytes = 0;
231
+ let rawSseCapped = false;
223
232
  const responseHeaders = response.headers && typeof response.headers.entries === 'function'
224
233
  ? Object.fromEntries(response.headers.entries())
225
234
  : {};
@@ -232,7 +241,20 @@ export class AnthropicAdapter extends LLMAdapter {
232
241
 
233
242
  const chunkText = decoder.decode(value, { stream: true });
234
243
  buffer += chunkText;
235
- rawSseBody += chunkText;
244
+ // task-344 follow-up (N2): size-capped capture.
245
+ rawSseTotalBytes += value.byteLength;
246
+ if (!rawSseCapped) {
247
+ if (rawSseTotalBytes <= RAW_PAYLOAD_CAP_BYTES) {
248
+ rawSseBody += chunkText;
249
+ } else {
250
+ // Append enough of this chunk to meet the cap, then freeze.
251
+ const remaining = RAW_PAYLOAD_CAP_BYTES - (rawSseTotalBytes - value.byteLength);
252
+ if (remaining > 0) {
253
+ rawSseBody += chunkText.slice(0, remaining);
254
+ }
255
+ rawSseCapped = true;
256
+ }
257
+ }
236
258
  const lines = buffer.split('\n');
237
259
  buffer = lines.pop() || ''; // Keep incomplete line
238
260
 
@@ -325,12 +347,16 @@ export class AnthropicAdapter extends LLMAdapter {
325
347
  // task-344: emit raw exchange after stream completes (or errors).
326
348
  if (onRawExchange) {
327
349
  try {
350
+ // task-344 follow-up (N2): append truncation marker when capped.
351
+ const finalBody = rawSseCapped
352
+ ? `${rawSseBody}…[truncated, original ${rawSseTotalBytes} bytes]`
353
+ : rawSseBody;
328
354
  onRawExchange({
329
355
  rawRequest,
330
356
  rawResponse: {
331
357
  status: responseStatus,
332
358
  headers: responseHeaders,
333
- body: rawSseBody,
359
+ body: finalBody,
334
360
  format: 'sse',
335
361
  },
336
362
  });
@@ -28,6 +28,9 @@ import {
28
28
  LLMServerError,
29
29
  LLMAbortError,
30
30
  redactRawRequest,
31
+ capRawRequest,
32
+ capRawString,
33
+ RAW_PAYLOAD_CAP_BYTES,
31
34
  } from './adapter.js';
32
35
  import {
33
36
  normalizeEffort,
@@ -239,7 +242,8 @@ export class ChatCompletionsAdapter extends LLMAdapter {
239
242
  };
240
243
 
241
244
  // task-344: expose raw request (redacted) for debug panel.
242
- const rawRequest = redactRawRequest({ url, method: 'POST', headers, body });
245
+ // task-344 follow-up (N2): cap body size.
246
+ const rawRequest = capRawRequest(redactRawRequest({ url, method: 'POST', headers, body }));
243
247
 
244
248
  const response = await fetch(url, {
245
249
  method: 'POST',
@@ -259,7 +263,8 @@ export class ChatCompletionsAdapter extends LLMAdapter {
259
263
  headers: response.headers && typeof response.headers.entries === 'function'
260
264
  ? Object.fromEntries(response.headers.entries())
261
265
  : {},
262
- body: errorBody,
266
+ // task-344 follow-up (N2): cap error body.
267
+ body: capRawString(errorBody),
263
268
  },
264
269
  });
265
270
  } catch { /* ignore */ }
@@ -272,7 +277,10 @@ export class ChatCompletionsAdapter extends LLMAdapter {
272
277
  const decoder = new TextDecoder();
273
278
  let buffer = '';
274
279
  // task-344: accumulate raw SSE body + headers/status for debug exposure.
280
+ // task-344 follow-up (N2): cap growth at RAW_PAYLOAD_CAP_BYTES.
275
281
  let rawSseBody = '';
282
+ let rawSseTotalBytes = 0;
283
+ let rawSseCapped = false;
276
284
  const responseHeaders = response.headers && typeof response.headers.entries === 'function'
277
285
  ? Object.fromEntries(response.headers.entries())
278
286
  : {};
@@ -290,7 +298,19 @@ export class ChatCompletionsAdapter extends LLMAdapter {
290
298
 
291
299
  const chunkText = decoder.decode(value, { stream: true });
292
300
  buffer += chunkText;
293
- rawSseBody += chunkText;
301
+ // task-344 follow-up (N2): size-capped capture.
302
+ rawSseTotalBytes += value.byteLength;
303
+ if (!rawSseCapped) {
304
+ if (rawSseTotalBytes <= RAW_PAYLOAD_CAP_BYTES) {
305
+ rawSseBody += chunkText;
306
+ } else {
307
+ const remaining = RAW_PAYLOAD_CAP_BYTES - (rawSseTotalBytes - value.byteLength);
308
+ if (remaining > 0) {
309
+ rawSseBody += chunkText.slice(0, remaining);
310
+ }
311
+ rawSseCapped = true;
312
+ }
313
+ }
294
314
  const lines = buffer.split('\n');
295
315
  buffer = lines.pop() || '';
296
316
 
@@ -377,12 +397,16 @@ export class ChatCompletionsAdapter extends LLMAdapter {
377
397
  // task-344: emit raw exchange after stream completes.
378
398
  if (onRawExchange) {
379
399
  try {
400
+ // task-344 follow-up (N2): append truncation marker when capped.
401
+ const finalBody = rawSseCapped
402
+ ? `${rawSseBody}…[truncated, original ${rawSseTotalBytes} bytes]`
403
+ : rawSseBody;
380
404
  onRawExchange({
381
405
  rawRequest,
382
406
  rawResponse: {
383
407
  status: responseStatus,
384
408
  headers: responseHeaders,
385
- body: rawSseBody,
409
+ body: finalBody,
386
410
  format: 'sse',
387
411
  },
388
412
  });