@yeaft/webchat-agent 1.0.477 → 1.0.479

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.477",
3
+ "version": "1.0.479",
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",
@@ -14,43 +14,22 @@ import { createReadStream, promises as fsp } from 'fs';
14
14
  import { basename, dirname, extname, join } from 'path';
15
15
  import { createHash, randomUUID } from 'crypto';
16
16
  import { createInterface } from 'readline';
17
- import { truncateUtf8Text } from './perf-trace.js';
18
17
 
19
18
  const TRACE_VERSION = 3;
20
19
  const REQUEST_RETENTION = 10;
21
20
  const MAX_HISTORY_LIMIT = 5;
22
21
  const MAX_DREAM_EVENTS = 100;
23
- const MAX_TEXT_BYTES = 1024 * 1024;
24
- const MAX_TOOL_INPUT = 10 * 1024;
25
- const MAX_INLINE_VALUE_BYTES = 1024 * 1024;
26
- const MAX_RAW_REQUEST_BYTES = 2 * 1024 * 1024;
27
- // Raw provider responses are diagnostic duplicates of the normalized response,
28
- // tool calls, usage and request delta already stored on each loop. Keeping up
29
- // to 1 MiB per loop made long tool turns produce 100+ MiB trace files. Every
30
- // buffered flush then JSON.stringify()'d and rewrote that entire file on the
31
- // agent's single event loop, so several active Sessions appeared deadlocked.
32
- // A 64 KiB prefix is enough to inspect provider envelopes without letting
33
- // always-on diagnostics dominate runtime latency.
34
- const MAX_RAW_RESPONSE_BYTES = 64 * 1024;
22
+ // Debug records are diagnostic source data. They deliberately do not inherit
23
+ // model-context or ordinary history budgets, and explicit detail transport is
24
+ // chunked instead of truncating persisted values.
35
25
  const TRACE_APPEND_BATCH_MS = 100;
36
26
  const EVENT_FLUSH_INTERVAL_MS = 30_000;
37
27
  const MAX_SEARCH_PATTERN_CHARS = 300;
38
- const DEFAULT_TRACE_TEXT_MAX_BYTES = 256 * 1024;
39
- // A turn detail is returned as one WebSocket message. Keep its UI projection
40
- // comfortably below the Agent connection's 8 MiB outbound queue ceiling while
41
- // preserving the canonical file-backed trace without any extra truncation.
42
- const DEBUG_DETAIL_WIRE_MAX_BYTES = 6 * 1024 * 1024;
43
28
 
44
29
  function isPlainObject(value) {
45
30
  return value && typeof value === 'object' && !Array.isArray(value);
46
31
  }
47
32
 
48
- function normalizeTextMaxBytes(value, fallback = DEFAULT_TRACE_TEXT_MAX_BYTES) {
49
- const parsed = Number(value);
50
- if (!Number.isFinite(parsed)) return fallback;
51
- return Math.min(4 * 1024 * 1024, Math.max(0, Math.floor(parsed)));
52
- }
53
-
54
33
  function safeDirComponent(value, fallback = 'unknown') {
55
34
  const raw = String(value || '').trim();
56
35
  if (!raw) return fallback;
@@ -169,53 +148,12 @@ function traceMatchesRegex(trace, regex) {
169
148
  }
170
149
  }
171
150
 
172
- function truncateText(value, maxBytes = MAX_TEXT_BYTES) {
173
- if (value == null) return value ?? null;
174
- const str = String(value);
175
- const budget = Math.max(0, Number(maxBytes) || 0);
176
- if (Buffer.byteLength(str, 'utf8') <= budget) return str;
177
- if (budget <= 0) return '';
178
- const marker = `\n... [truncated to ${budget} bytes]`;
179
- const markerBytes = Buffer.byteLength(marker, 'utf8');
180
- if (markerBytes >= budget) return truncateUtf8Text(str, budget).value;
181
- return `${truncateUtf8Text(str, budget - markerBytes).value}${marker}`;
182
- }
183
-
184
151
  function cloneJsonValue(value) {
185
152
  if (value == null) return value;
186
153
  try { return JSON.parse(JSON.stringify(value)); }
187
154
  catch { return null; }
188
155
  }
189
156
 
190
- function safeJsonValue(value, maxBytes = MAX_INLINE_VALUE_BYTES) {
191
- if (value == null) return value;
192
- try {
193
- const json = JSON.stringify(value);
194
- const originalBytes = Buffer.byteLength(json, 'utf8');
195
- if (originalBytes <= maxBytes) return JSON.parse(json);
196
- // A bounded raw-exchange sentinel already carries the useful preview.
197
- // Preserve a re-bounded preview rather than reducing persisted history to
198
- // metadata after an outer trace record crosses its storage budget.
199
- if (value && typeof value === 'object' && value.__truncated === true
200
- && typeof value.preview === 'string') {
201
- const previewBudget = Math.max(0, maxBytes - 512);
202
- const preview = truncateText(value.preview, previewBudget);
203
- return {
204
- __truncated: true,
205
- ...(value.reason ? { reason: value.reason } : {}),
206
- originalBytes: Number.isFinite(Number(value.originalBytes))
207
- ? Number(value.originalBytes)
208
- : originalBytes,
209
- maxBytes,
210
- ...(preview ? { preview } : {}),
211
- };
212
- }
213
- return { __truncated: true, originalBytes, maxBytes };
214
- } catch {
215
- return null;
216
- }
217
- }
218
-
219
157
  function normalizeUsage(usage = {}, fallback = {}) {
220
158
  const inputTokens = Number.isFinite(Number(usage?.inputTokens)) ? Number(usage.inputTokens) : Number(fallback.inputTokens || 0);
221
159
  const outputTokens = Number.isFinite(Number(usage?.outputTokens)) ? Number(usage.outputTokens) : Number(fallback.outputTokens || 0);
@@ -241,34 +179,9 @@ function stableEqual(a, b) {
241
179
  catch { return false; }
242
180
  }
243
181
 
244
- function jsonByteLength(value) {
245
- try { return Buffer.byteLength(JSON.stringify(value), 'utf8'); }
246
- catch { return Infinity; }
247
- }
248
-
249
- function rawRequestSentinel(reason, value = null, maxBytes = MAX_RAW_REQUEST_BYTES) {
250
- const previewSource = typeof value === 'string'
251
- ? value
252
- : (() => { try { return JSON.stringify(value); } catch { return null; } })();
253
- const preview = typeof previewSource === 'string'
254
- ? truncateText(previewSource, Math.min(64 * 1024, maxBytes))
255
- : null;
256
- const originalBytes = value == null ? null : jsonByteLength(value);
257
- return {
258
- __truncated: true,
259
- reason,
260
- ...(originalBytes != null ? { originalBytes } : {}),
261
- maxBytes,
262
- ...(preview ? { preview } : {}),
263
- };
264
- }
265
-
266
- function boundRawValue(value, reason = 'raw_request_budget') {
182
+ function cloneRawValue(value) {
267
183
  if (value == null) return value;
268
- const cloned = typeof value === 'string' ? value : cloneJsonValue(value);
269
- if (cloned == null) return null;
270
- if (jsonByteLength(cloned) <= MAX_RAW_REQUEST_BYTES) return cloned;
271
- return rawRequestSentinel(reason, value);
184
+ return typeof value === 'string' ? value : cloneJsonValue(value);
272
185
  }
273
186
 
274
187
  function buildRawMessagesDelta(previousMessages, nextMessages) {
@@ -276,9 +189,9 @@ function buildRawMessagesDelta(previousMessages, nextMessages) {
276
189
  const priorMessages = Array.isArray(previousMessages) ? previousMessages : [];
277
190
  const prefix = messagesPrefixLength(priorMessages, nextMessages);
278
191
  if (prefix === priorMessages.length && prefix <= nextMessages.length) {
279
- return { messagesFrom: prefix, messagesAppend: boundRawValue(nextMessages.slice(prefix), 'raw_request_messages_append_budget') };
192
+ return { messagesFrom: prefix, messagesAppend: cloneRawValue(nextMessages.slice(prefix)) };
280
193
  }
281
- return { messagesFrom: 0, messagesAppend: boundRawValue(nextMessages, 'raw_request_messages_append_budget') };
194
+ return { messagesFrom: 0, messagesAppend: cloneRawValue(nextMessages) };
282
195
  }
283
196
 
284
197
  function rawRequestMessageKey(body) {
@@ -290,18 +203,18 @@ function rawRequestMessageKey(body) {
290
203
 
291
204
  function buildInitialRawRequestDelta(value) {
292
205
  if (value == null) return null;
293
- if (typeof value === 'string') return { replacement: rawRequestSentinel('raw_request_string_replaced', value) };
294
- if (!isPlainObject(value)) return { replacement: boundRawValue(value) };
206
+ if (typeof value === 'string') return { replacement: value };
207
+ if (!isPlainObject(value)) return { replacement: cloneRawValue(value) };
295
208
  const delta = { set: {}, body: {} };
296
209
  for (const [key, item] of Object.entries(value)) {
297
210
  if (key === 'body') continue;
298
- delta.set[key] = boundRawValue(item, `raw_request_${key}_budget`);
211
+ delta.set[key] = cloneRawValue(item);
299
212
  }
300
213
  const body = isPlainObject(value.body) ? value.body : null;
301
214
  if (body) {
302
215
  for (const [key, bodyValue] of Object.entries(body)) {
303
216
  if (key === 'messages' || key === 'input') continue;
304
- delta.body[key] = boundRawValue(bodyValue, `raw_request_body_${key}_budget`);
217
+ delta.body[key] = cloneRawValue(bodyValue);
305
218
  }
306
219
  const messageKey = rawRequestMessageKey(body);
307
220
  if (messageKey) {
@@ -309,12 +222,11 @@ function buildInitialRawRequestDelta(value) {
309
222
  Object.assign(delta.body, buildRawMessagesDelta([], body[messageKey]) || {});
310
223
  }
311
224
  } else if (Object.prototype.hasOwnProperty.call(value, 'body')) {
312
- delta.set.body = rawRequestSentinel('raw_request_body_replaced');
225
+ delta.set.body = cloneRawValue(value.body);
313
226
  }
314
227
  if (Object.keys(delta.set).length === 0) delete delta.set;
315
228
  if (Object.keys(delta.body).length === 0) delete delta.body;
316
229
  if (!delta.set && !delta.body) return null;
317
- if (jsonByteLength(delta) > MAX_RAW_REQUEST_BYTES) return { replacement: rawRequestSentinel('raw_request_delta_budget') };
318
230
  return delta;
319
231
  }
320
232
 
@@ -332,16 +244,16 @@ function buildRawRequestDelta(previous, next) {
332
244
  const comparablePrevious = rawComparableRequest(previous);
333
245
  const comparableNext = rawComparableRequest(next);
334
246
  if (typeof comparablePrevious === 'string' || typeof comparableNext === 'string') {
335
- return comparablePrevious === comparableNext ? null : { replacement: rawRequestSentinel('raw_request_string_replaced', comparableNext) };
247
+ return comparablePrevious === comparableNext ? null : { replacement: cloneRawValue(comparableNext) };
336
248
  }
337
249
  if (!isPlainObject(comparablePrevious) || !isPlainObject(comparableNext)) {
338
- return stableEqual(comparablePrevious, comparableNext) ? null : { replacement: rawRequestSentinel('raw_request_replaced') };
250
+ return stableEqual(comparablePrevious, comparableNext) ? null : { replacement: cloneRawValue(comparableNext) };
339
251
  }
340
252
 
341
253
  const delta = { set: {}, body: {} };
342
254
  for (const key of Object.keys(comparableNext)) {
343
255
  if (key === 'body') continue;
344
- if (!stableEqual(comparablePrevious[key], comparableNext[key])) delta.set[key] = boundRawValue(comparableNext[key], `raw_request_${key}_budget`);
256
+ if (!stableEqual(comparablePrevious[key], comparableNext[key])) delta.set[key] = cloneRawValue(comparableNext[key]);
345
257
  }
346
258
  for (const key of Object.keys(comparablePrevious)) {
347
259
  if (key !== 'body' && !Object.prototype.hasOwnProperty.call(comparableNext, key)) delta.set[key] = null;
@@ -352,7 +264,7 @@ function buildRawRequestDelta(previous, next) {
352
264
  if (prevBody && nextBody) {
353
265
  for (const key of Object.keys(nextBody)) {
354
266
  if (key === 'messages' || key === 'input') continue;
355
- if (!stableEqual(prevBody[key], nextBody[key])) delta.body[key] = boundRawValue(nextBody[key], `raw_request_body_${key}_budget`);
267
+ if (!stableEqual(prevBody[key], nextBody[key])) delta.body[key] = cloneRawValue(nextBody[key]);
356
268
  }
357
269
  for (const key of Object.keys(prevBody)) {
358
270
  if (key !== 'messages' && key !== 'input' && !Object.prototype.hasOwnProperty.call(nextBody, key)) delta.body[key] = null;
@@ -362,15 +274,12 @@ function buildRawRequestDelta(previous, next) {
362
274
  const msgDelta = buildRawMessagesDelta(prevMessages, nextMessages);
363
275
  if (msgDelta) Object.assign(delta.body, msgDelta);
364
276
  } else if (!stableEqual(previous.body, next.body)) {
365
- delta.set.body = rawRequestSentinel('raw_request_body_replaced');
277
+ delta.set.body = cloneRawValue(next.body);
366
278
  }
367
279
 
368
280
  if (Object.keys(delta.set).length === 0) delete delta.set;
369
281
  if (Object.keys(delta.body).length === 0) delete delta.body;
370
282
  if (!delta.set && !delta.body) return null;
371
- if (jsonByteLength(delta) > MAX_RAW_REQUEST_BYTES) {
372
- return { replacement: rawRequestSentinel('raw_request_delta_budget') };
373
- }
374
283
  return delta;
375
284
  }
376
285
 
@@ -398,9 +307,7 @@ export function applyRawRequestDelta(previous, delta) {
398
307
  const from = Number.isFinite(Number(delta.body.messagesFrom)) ? Number(delta.body.messagesFrom) : existing.length;
399
308
  body[messageKey] = existing.slice(0, from).concat(cloneJsonValue(delta.body.messagesAppend) || []);
400
309
  } else if (Object.prototype.hasOwnProperty.call(delta.body, 'messagesAppend')) {
401
- // A huge append is represented by a bounded raw-request sentinel, not
402
- // an array. Preserve that sentinel rather than silently dropping the
403
- // entire request body during hydration.
310
+ // Preserve legacy non-array values written by older bounded traces.
404
311
  body[messageKey] = cloneJsonValue(delta.body.messagesAppend) ?? delta.body.messagesAppend;
405
312
  }
406
313
  next.body = body;
@@ -418,82 +325,10 @@ function messagesPrefixLength(prevMessages, nextMessages) {
418
325
  return i;
419
326
  }
420
327
 
421
- function snapshotTruncation(path, maxBytes, originalBytes) {
422
- return { __truncated: true, path, maxBytes, originalBytes };
423
- }
424
-
425
- function boundedSnapshotString(value, maxBytes, path) {
426
- const originalBytes = Buffer.byteLength(value, 'utf8');
427
- if (originalBytes <= maxBytes) return value;
428
- const marker = snapshotTruncation(path, maxBytes, originalBytes);
429
- if (jsonByteLength(marker) > maxBytes) return null;
430
- const markerText = `\n... [truncated to ${maxBytes} bytes]`;
431
- const markerBytes = Buffer.byteLength(markerText, 'utf8');
432
- if (markerBytes >= maxBytes) return marker;
433
- return truncateUtf8Text(value, maxBytes - markerBytes).value + markerText;
434
- }
435
-
436
- function boundSnapshotValue(value, maxBytes, path = 'messages') {
437
- if (maxBytes <= 0) return null;
438
- const sourceBytes = jsonByteLength(value);
439
- if (sourceBytes <= maxBytes) return cloneJsonValue(value);
440
- const marker = snapshotTruncation(path, maxBytes, sourceBytes);
441
- if (jsonByteLength(marker) > maxBytes) return null;
442
- if (value == null || typeof value === 'number' || typeof value === 'boolean') return marker;
443
- if (typeof value === 'string') return boundedSnapshotString(value, maxBytes, path);
444
-
445
- // Account JSON framing incrementally. The old clone-and-stringify-prefix
446
- // loop reserialized the full growing array/object per member and blocked
447
- // DebugTrace.endTurn for tens of seconds on a large provider history.
448
- if (Array.isArray(value)) {
449
- const out = [];
450
- let usedBytes = 2; // []
451
- for (let index = 0; index < value.length; index += 1) {
452
- const separatorBytes = out.length > 0 ? 1 : 0;
453
- const remaining = Math.max(0, maxBytes - usedBytes - separatorBytes);
454
- const candidate = boundSnapshotValue(value[index], remaining, `${path}[${index}]`);
455
- if (candidate == null) break;
456
- let encoded;
457
- try { encoded = JSON.stringify(candidate); } catch { break; }
458
- const candidateBytes = Buffer.byteLength(encoded, 'utf8');
459
- if (usedBytes + separatorBytes + candidateBytes > maxBytes) break;
460
- out.push(candidate);
461
- usedBytes += separatorBytes + candidateBytes;
462
- }
463
- return out.length > 0 ? out : marker;
464
- }
465
- if (typeof value === 'object') {
466
- const out = {};
467
- let usedBytes = 2; // {}
468
- let count = 0;
469
- for (const [key, item] of Object.entries(value)) {
470
- let keyJson;
471
- try { keyJson = JSON.stringify(key); } catch { break; }
472
- const keyBytes = Buffer.byteLength(keyJson, 'utf8');
473
- const separatorBytes = count > 0 ? 1 : 0;
474
- const remaining = Math.max(0, maxBytes - usedBytes - separatorBytes - keyBytes - 1);
475
- const candidate = boundSnapshotValue(item, remaining, `${path}.${key}`);
476
- if (candidate == null) break;
477
- let encoded;
478
- try { encoded = JSON.stringify(candidate); } catch { break; }
479
- const candidateBytes = Buffer.byteLength(encoded, 'utf8');
480
- if (usedBytes + separatorBytes + keyBytes + 1 + candidateBytes > maxBytes) break;
481
- out[key] = candidate;
482
- usedBytes += separatorBytes + keyBytes + 1 + candidateBytes;
483
- count += 1;
484
- }
485
- return count > 0 ? out : marker;
486
- }
487
- return marker;
488
- }
489
-
490
- function buildRequestSnapshot(info = {}, textMaxBytes = DEFAULT_TRACE_TEXT_MAX_BYTES) {
491
- const messageBudget = normalizeTextMaxBytes(textMaxBytes);
328
+ function buildRequestSnapshot(info = {}) {
492
329
  return {
493
- systemPrompt: truncateText(info.systemPrompt || '', messageBudget),
494
- messages: Array.isArray(info.messages)
495
- ? boundSnapshotValue(info.messages, messageBudget, 'messages')
496
- : [],
330
+ systemPrompt: String(info.systemPrompt || ''),
331
+ messages: Array.isArray(info.messages) ? cloneJsonValue(info.messages) : [],
497
332
  rawRequest: info.rawRequest ?? null,
498
333
  };
499
334
  }
@@ -812,167 +647,12 @@ function expandTrace(trace) {
812
647
  return { loops, turns: Array.from(turnsById.values()) };
813
648
  }
814
649
 
815
- function debugDetailTextSentinel(value, maxBytes, path, originalBytes = jsonByteLength(value)) {
816
- const text = value == null ? '' : String(value);
817
- const budget = Math.max(2, Math.floor(Number(maxBytes) || 0));
818
- if (originalBytes <= budget) return text;
819
- const marker = `\n... [wire truncated ${path}; original ${originalBytes} bytes]`;
820
- const rawBytes = Math.max(1, Buffer.byteLength(text, 'utf8'));
821
- const expansion = Math.max(1, originalBytes / rawBytes);
822
- const markerBytes = jsonByteLength(marker) - 2;
823
- let previewBytes = Math.max(0, Math.floor((budget - markerBytes - 2) / expansion));
824
- let projected = `${truncateUtf8Text(text, previewBytes).value}${marker}`;
825
- let projectedBytes = jsonByteLength(projected);
826
- if (projectedBytes > budget && previewBytes > 0) {
827
- previewBytes = Math.max(0, Math.floor(previewBytes * (budget / projectedBytes)) - 8);
828
- projected = `${truncateUtf8Text(text, previewBytes).value}${marker}`;
829
- projectedBytes = jsonByteLength(projected);
830
- }
831
- if (projectedBytes <= budget) return projected;
832
- if (jsonByteLength(marker) <= budget) return marker;
833
- return '';
834
- }
835
-
836
- function debugDetailJsonSentinel(value, maxBytes, path, originalBytes = jsonByteLength(value)) {
837
- if (value == null) return value;
838
- const budget = Math.max(0, Math.floor(Number(maxBytes) || 0));
839
- if (originalBytes <= budget) return value;
840
- const base = {
841
- __truncated: true,
842
- reason: 'debug_detail_wire_budget',
843
- path,
844
- originalBytes,
845
- maxBytes: budget,
846
- };
847
- const baseBytes = jsonByteLength(base);
848
- if (baseBytes > budget) return budget >= 4 ? null : '';
849
- let preview = '';
850
- try {
851
- const encoded = JSON.stringify(value);
852
- preview = debugDetailTextSentinel(
853
- encoded,
854
- Math.max(2, budget - baseBytes - 16),
855
- path,
856
- jsonByteLength(encoded),
857
- );
858
- } catch { /* metadata-only sentinel below */ }
859
- const projected = preview ? { ...base, preview } : base;
860
- return jsonByteLength(projected) <= budget ? projected : base;
861
- }
862
-
863
- function allocateDebugCandidateBudgets(candidates, totalBytes) {
864
- const sorted = [...candidates].sort((a, b) => a.originalBytes - b.originalBytes || a.path.localeCompare(b.path));
865
- let remainingBytes = Math.max(0, Math.floor(totalBytes));
866
- let remainingCount = sorted.length;
867
- for (let index = 0; index < sorted.length; index += 1) {
868
- const candidate = sorted[index];
869
- const fairShare = remainingCount > 0 ? Math.floor(remainingBytes / remainingCount) : 0;
870
- if (candidate.originalBytes <= fairShare) {
871
- candidate.budget = candidate.originalBytes;
872
- remainingBytes -= candidate.budget;
873
- remainingCount -= 1;
874
- continue;
875
- }
876
- for (let tail = index; tail < sorted.length; tail += 1) {
877
- const count = sorted.length - tail;
878
- const budget = count > 0 ? Math.floor(remainingBytes / count) : 0;
879
- sorted[tail].budget = budget;
880
- remainingBytes -= budget;
881
- }
882
- break;
883
- }
884
- }
885
-
886
- export function projectDebugDetailForWire(detail, maxBytes = DEBUG_DETAIL_WIRE_MAX_BYTES) {
887
- // `fetchTurnDebug()` hands us a freshly expanded response object; mutate that
888
- // disposable projection rather than cloning tens or hundreds of cumulative
889
- // request snapshots. Canonical file records and the request cache are separate.
890
- const wire = detail && typeof detail === 'object'
650
+ export function projectDebugDetailForWire(detail) {
651
+ // Backward-compatible export: debug detail is diagnostic source data and must
652
+ // remain lossless. The transport splits large JSON into bounded wire frames.
653
+ return detail && typeof detail === 'object'
891
654
  ? detail
892
655
  : { loops: [], turns: [], dreamEvents: [] };
893
- const payloadBudget = Math.max(0, maxBytes - 2048);
894
- const candidates = [];
895
- const addCandidate = (container, field, path) => {
896
- if (!container || container[field] == null) return;
897
- const value = container[field];
898
- candidates.push({
899
- container,
900
- field,
901
- path,
902
- value,
903
- originalBytes: jsonByteLength(value),
904
- budget: 0,
905
- });
906
- // Measure the non-candidate envelope exactly once without serializing every
907
- // cumulative request again after each replacement.
908
- container[field] = null;
909
- };
910
-
911
- const loopFields = ['rawRequest', 'rawResponse', 'messages', 'requestBase', 'requestDelta', 'toolCalls', 'response', 'systemPrompt'];
912
- for (const [loopIndex, loop] of (Array.isArray(wire.loops) ? wire.loops : []).entries()) {
913
- if (!loop || typeof loop !== 'object') continue;
914
- for (const field of loopFields) addCandidate(loop, field, `loops[${loopIndex}].${field}`);
915
- }
916
- for (const [turnIndex, turn] of (Array.isArray(wire.turns) ? wire.turns : []).entries()) {
917
- if (!turn || typeof turn !== 'object') continue;
918
- for (const field of ['userPrompt', 'memoryLoaded', 'memoryLoadedMeta', 'memoryAdjust']) {
919
- addCandidate(turn, field, `turns[${turnIndex}].${field}`);
920
- }
921
- for (const [toolIndex, tool] of (Array.isArray(turn.tools) ? turn.tools : []).entries()) {
922
- if (!tool || typeof tool !== 'object') continue;
923
- addCandidate(tool, 'toolInput', `turns[${turnIndex}].tools[${toolIndex}].toolInput`);
924
- addCandidate(tool, 'toolOutput', `turns[${turnIndex}].tools[${toolIndex}].toolOutput`);
925
- }
926
- }
927
- for (const [eventIndex] of (Array.isArray(wire.dreamEvents) ? wire.dreamEvents : []).entries()) {
928
- addCandidate(wire.dreamEvents, eventIndex, `dreamEvents[${eventIndex}]`);
929
- }
930
-
931
- const skeletonBytes = jsonByteLength(wire);
932
- const candidateBytes = candidates.reduce((sum, candidate) => sum + candidate.originalBytes, 0);
933
- const originalBytes = skeletonBytes - (4 * candidates.length) + candidateBytes;
934
- if (originalBytes <= payloadBudget) {
935
- for (const candidate of candidates) candidate.container[candidate.field] = candidate.value;
936
- return wire;
937
- }
938
- if (candidates.length === 0 || skeletonBytes > payloadBudget) {
939
- throw new Error(`Debug detail metadata exceeds the ${maxBytes}-byte wire budget`);
940
- }
941
-
942
- // Each candidate currently occupies JSON `null` (4 bytes) in the skeleton.
943
- // Water-fill the exact value budget: small fields stay complete, while large
944
- // cumulative requests and tool outputs receive fair, independently bounded
945
- // previews. This is O(total input bytes + candidates log candidates), with
946
- // one final whole-envelope serialization rather than one per candidate.
947
- const valueBudget = payloadBudget - skeletonBytes + (4 * candidates.length);
948
- allocateDebugCandidateBudgets(candidates, valueBudget);
949
- let truncatedFields = 0;
950
- for (const candidate of candidates) {
951
- if (candidate.originalBytes <= candidate.budget) {
952
- candidate.container[candidate.field] = candidate.value;
953
- continue;
954
- }
955
- const fieldBudget = candidate.budget;
956
- candidate.container[candidate.field] = typeof candidate.value === 'string'
957
- ? debugDetailTextSentinel(candidate.value, fieldBudget, candidate.path, candidate.originalBytes)
958
- : debugDetailJsonSentinel(candidate.value, fieldBudget, candidate.path, candidate.originalBytes);
959
- truncatedFields += 1;
960
- }
961
-
962
- wire.projection = {
963
- truncated: true,
964
- reason: 'debug_detail_wire_budget',
965
- maxBytes,
966
- truncatedFields,
967
- };
968
- const projectedBytesBase = jsonByteLength(wire);
969
- let projectedBytes = projectedBytesBase + Buffer.byteLength(`,\"projectedBytes\":${projectedBytesBase}`, 'utf8');
970
- projectedBytes = projectedBytesBase + Buffer.byteLength(`,\"projectedBytes\":${projectedBytes}`, 'utf8');
971
- wire.projection.projectedBytes = projectedBytes;
972
- if (projectedBytes > maxBytes) {
973
- throw new Error(`Debug detail projection still exceeds the ${maxBytes}-byte wire budget`);
974
- }
975
- return wire;
976
656
  }
977
657
 
978
658
  function traceToLegacyRows(trace) {
@@ -1192,8 +872,6 @@ export class DebugTrace {
1192
872
  * @type {Promise<void>}
1193
873
  */
1194
874
  #flushChain = Promise.resolve();
1195
- /** @type {number} */
1196
- #textMaxBytes = DEFAULT_TRACE_TEXT_MAX_BYTES;
1197
875
  /** @type {boolean} */
1198
876
  #acceptingWrites = true;
1199
877
 
@@ -1205,17 +883,16 @@ export class DebugTrace {
1205
883
  const rootDir = fileTraceRoot(tracePath);
1206
884
  if (!rootDir) throw new Error('DebugTrace requires a storage path');
1207
885
  this.#rootDir = rootDir;
1208
- this.#textMaxBytes = normalizeTextMaxBytes(options?.textMaxBytes);
886
+ // `textMaxBytes` remains an accepted constructor option for compatibility,
887
+ // but debug source data is intentionally lossless.
888
+ void options?.textMaxBytes;
1209
889
  // Best-effort, fire-and-forget: atomicWriteText re-ensures the request
1210
890
  // subdir before every write, so a missed mkdir here is harmless.
1211
891
  ensureDir(rootDir).catch(() => {});
1212
892
  }
1213
893
 
1214
- refreshConfig(config = {}) {
1215
- const nextValue = config && typeof config === 'object'
1216
- ? config.traceTextMaxBytes
1217
- : config;
1218
- this.#textMaxBytes = normalizeTextMaxBytes(nextValue, this.#textMaxBytes);
894
+ refreshConfig(_config = {}) {
895
+ // Compatibility no-op: debug trace content is no longer byte-truncated.
1219
896
  }
1220
897
 
1221
898
  startTurn({ traceId, messageId = null, mode = null, turnNumber = null, sessionId = null, vpId = null, threadId = null, userPrompt = null, memoryLoaded = null, memoryLoadedMeta = null } = {}) {
@@ -1259,7 +936,7 @@ export class DebugTrace {
1259
936
  rawRequest: Object.prototype.hasOwnProperty.call(info, 'rawRequest')
1260
937
  ? info.rawRequest
1261
938
  : (trace.baseRequest?.rawRequest ?? null),
1262
- }, this.#textMaxBytes);
939
+ });
1263
940
  const previousSnapshot = trace._lastSnapshot || this.#reconstructLastSnapshot(trace);
1264
941
  if (!trace.baseRequest) {
1265
942
  trace.baseRequest = {
@@ -1275,7 +952,7 @@ export class DebugTrace {
1275
952
  loopNumber,
1276
953
  startedAt: trace.openedAt || Date.now(),
1277
954
  model: info.model || null,
1278
- response: truncateText(info.responseText || '', this.#textMaxBytes),
955
+ response: String(info.responseText || ''),
1279
956
  toolCalls: cloneJsonValue(Array.isArray(info.toolCalls) ? info.toolCalls : []),
1280
957
  usage: normalizeUsage(info.usage || {}, {
1281
958
  inputTokens: info.inputTokens || 0,
@@ -1288,8 +965,8 @@ export class DebugTrace {
1288
965
  stopReason: info.stopReason || null,
1289
966
  at: Date.now(),
1290
967
  rawResponse: typeof info.rawResponse === 'string'
1291
- ? truncateText(info.rawResponse, Math.min(this.#textMaxBytes, MAX_RAW_RESPONSE_BYTES))
1292
- : safeJsonValue(info.rawResponse, Math.min(this.#textMaxBytes, MAX_RAW_RESPONSE_BYTES)),
968
+ ? info.rawResponse
969
+ : cloneJsonValue(info.rawResponse),
1293
970
  // Raw request is canonical base-plus-delta data. Persisting the full
1294
971
  // snapshot on every loop made a 512 KiB request consume N × 512 KiB for
1295
972
  // an N-loop trace and kept the same duplication in the hydrated cache.
@@ -1327,8 +1004,8 @@ export class DebugTrace {
1327
1004
  loopNumber: ctx.loopNumber || 0,
1328
1005
  toolName: toolName || '?',
1329
1006
  toolCallId,
1330
- toolInput: truncateText(toolInput == null ? null : String(toolInput), MAX_TOOL_INPUT),
1331
- toolOutput: truncateText(toolOutput == null ? null : String(toolOutput), this.#textMaxBytes),
1007
+ toolInput: toolInput == null ? null : String(toolInput),
1008
+ toolOutput: toolOutput == null ? null : String(toolOutput),
1332
1009
  durationMs: Number(durationMs || 0),
1333
1010
  isError: !!isError,
1334
1011
  suppressed: !!suppressed,
@@ -1350,7 +1027,7 @@ export class DebugTrace {
1350
1027
  id,
1351
1028
  traceId: traceId || String(eventType || 'event'),
1352
1029
  eventType: eventType || 'event',
1353
- eventData: safeJsonValue(eventData),
1030
+ eventData: cloneJsonValue(eventData),
1354
1031
  createdAt: Date.now(),
1355
1032
  });
1356
1033
  if (this.#events.length > MAX_DREAM_EVENTS) {
@@ -1433,7 +1110,7 @@ export class DebugTrace {
1433
1110
  const dreamEvents = this.#readDreamEvents({ sessionId: requestedSessionId, dreamLimit });
1434
1111
  if (!trace) return { loops: [], turns: [], dreamEvents, detailTurnId: requestedTurnId };
1435
1112
  const expanded = expandTrace(trace);
1436
- return projectDebugDetailForWire({ ...expanded, dreamEvents, detailTurnId: requestedTurnId });
1113
+ return { ...expanded, dreamEvents, detailTurnId: requestedTurnId };
1437
1114
  }
1438
1115
 
1439
1116
  async fetchRecentDebugHistory({ limit = MAX_HISTORY_LIMIT, dreamLimit = 5, sessionId = null, threadId = null, indexOnly = false, detailTurnId = null, search = '' } = {}) {
@@ -1643,7 +1320,7 @@ export class DebugTrace {
1643
1320
  sessionId: normalizedSessionId,
1644
1321
  vpId: vpId || null,
1645
1322
  threadId: threadId || null,
1646
- userPrompt: truncateText(userPrompt || '', this.#textMaxBytes),
1323
+ userPrompt: String(userPrompt || ''),
1647
1324
  memoryLoaded: Array.isArray(memoryLoaded) ? cloneJsonValue(memoryLoaded) : null,
1648
1325
  memoryLoadedMeta: memoryLoadedMeta && typeof memoryLoadedMeta === 'object'
1649
1326
  ? cloneJsonValue(memoryLoadedMeta)
package/yeaft/engine.js CHANGED
@@ -38,7 +38,7 @@ import { isVpForeign, readContent as readScopeContent } from './memory/store.js'
38
38
  import { ActiveMemorySet } from './memory/ams.js';
39
39
  import { cleanMemoryPromptText } from './memory/prompt-cleanup.js';
40
40
  import { isVpSeedBackfillStub } from './memory/seed-backfill.js';
41
- import { boundRawExchange, perfNowMs, recordAgentPerfTrace } from './perf-trace.js';
41
+ import { perfNowMs, recordAgentPerfTrace } from './perf-trace.js';
42
42
  // Default thread marker for legacy / non-group flows. Group VP runtime may
43
43
  // pass a real threadId per (sessionId, vpId, threadId) engine instance.
44
44
  const MAIN_THREAD_ID = 'main';
@@ -2622,17 +2622,24 @@ export class Engine {
2622
2622
  const thinkingBlocks = []; // task-327d: collected from adapter for round-trip
2623
2623
  let stopReason = 'end_turn';
2624
2624
  const totalUsage = { inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0, cacheInputDeltaTokens: 0 };
2625
- // task-344: capture bounded raw request / raw response for the debug
2626
- // panel. Both exchanges obey the live telemetry budget before they
2627
- // reach durable debug trace storage.
2625
+ // Raw provider exchange is diagnostic source data, not model context or
2626
+ // ordinary history. The legacy telemetry byte setting must not destroy it;
2627
+ // explicit debug detail transport is chunked instead.
2628
2628
  let rawRequest = null;
2629
2629
  let rawResponse = null;
2630
- const rawExchangeMaxBytes = Number.isFinite(Number(this.#config?.telemetry?.rawExchangeMaxBytes))
2631
- ? Math.max(0, Number(this.#config.telemetry.rawExchangeMaxBytes))
2632
- : 512 * 1024;
2630
+ const rawExchangeMaxBytes = Number.MAX_SAFE_INTEGER;
2631
+ const cloneRawExchange = (value) => {
2632
+ if (value == null || typeof value === 'string') return value;
2633
+ try { return JSON.parse(JSON.stringify(value)); }
2634
+ catch { return null; }
2635
+ };
2633
2636
  const captureRawExchange = (exchange) => {
2634
- if (exchange?.rawRequest) rawRequest = boundRawExchange(exchange.rawRequest, rawExchangeMaxBytes);
2635
- if (exchange?.rawResponse) rawResponse = boundRawExchange(exchange.rawResponse, rawExchangeMaxBytes);
2637
+ if (exchange && Object.prototype.hasOwnProperty.call(exchange, 'rawRequest')) {
2638
+ rawRequest = cloneRawExchange(exchange.rawRequest);
2639
+ }
2640
+ if (exchange && Object.prototype.hasOwnProperty.call(exchange, 'rawResponse')) {
2641
+ rawResponse = cloneRawExchange(exchange.rawResponse);
2642
+ }
2636
2643
  };
2637
2644
 
2638
2645
  // task-704b: resolve the live model's context window for this turn.