@yeaft/webchat-agent 1.0.351 → 1.0.352
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/connection/message-router.js +29 -1
- package/index.js +2 -0
- package/local-runtime/server/handlers/agent-sync.js +14 -0
- package/local-runtime/server/handlers/client-misc.js +21 -0
- package/local-runtime/version.json +1 -1
- package/local-runtime/web/app.bundle.js +52 -33
- package/local-runtime/web/app.bundle.js.gz +0 -0
- package/local-runtime/web/index.html +2 -2
- package/local-runtime/web/style.bundle.css +1 -1
- package/local-runtime/web/style.bundle.css.gz +0 -0
- package/package.json +1 -1
- package/yeaft/config-api.js +53 -1
- package/yeaft/config.js +54 -0
- package/yeaft/conversation/history-index-worker.js +13 -10
- package/yeaft/conversation/internal-control.js +1 -0
- package/yeaft/debug-trace.js +164 -47
- package/yeaft/engine.js +318 -28
- package/yeaft/llm/adapter.js +38 -0
- package/yeaft/llm/anthropic.js +11 -8
- package/yeaft/llm/openai-responses.js +11 -8
- package/yeaft/llm/router.js +1 -1
- package/yeaft/perf-trace.js +156 -24
- package/yeaft/session.js +7 -0
- package/yeaft/sessions/session-crud.js +19 -4
- package/yeaft/sub-agent/runner.js +4 -0
- package/yeaft/tools/agent.js +4 -0
- package/yeaft/tools/ask-user.js +1 -0
- package/yeaft/tools/bash.js +4 -0
- package/yeaft/tools/create-work-item.js +3 -0
- package/yeaft/tools/file-read.js +1 -0
- package/yeaft/tools/glob.js +1 -0
- package/yeaft/tools/grep.js +1 -0
- package/yeaft/tools/history-search.js +74 -20
- package/yeaft/tools/js-repl.js +1 -0
- package/yeaft/tools/list-agents.js +1 -0
- package/yeaft/tools/list-dir.js +1 -0
- package/yeaft/tools/list-tasks.js +1 -0
- package/yeaft/tools/read-task-log.js +1 -0
- package/yeaft/tools/route-forward.js +4 -0
- package/yeaft/tools/send-message.js +3 -0
- package/yeaft/tools/types.js +8 -0
- package/yeaft/tools/wait-agent.js +1 -0
- package/yeaft/utf8.js +44 -0
- package/yeaft/web-bridge.js +6 -0
- package/yeaft/work-center/runner.js +1 -0
|
Binary file
|
package/package.json
CHANGED
package/yeaft/config-api.js
CHANGED
|
@@ -12,7 +12,7 @@ import { existsSync, readFileSync, writeFileSync } from 'fs';
|
|
|
12
12
|
import { join } from 'path';
|
|
13
13
|
import { DEFAULT_YEAFT_DIR } from './init.js';
|
|
14
14
|
import { normalizeProviderModels, parseModelRef, serializeModelForPersistence } from './models.js';
|
|
15
|
-
import { normaliseYeaftSection } from './config.js';
|
|
15
|
+
import { normaliseTelemetrySection, normaliseYeaftSection } from './config.js';
|
|
16
16
|
import { isGitHubCopilotProvider, serializeKnownProviderForPersistence } from './llm/known-providers.js';
|
|
17
17
|
|
|
18
18
|
/**
|
|
@@ -293,6 +293,58 @@ export function updateYeaftSettings(update, dir) {
|
|
|
293
293
|
return merged;
|
|
294
294
|
}
|
|
295
295
|
|
|
296
|
+
/**
|
|
297
|
+
* Read the bounded local telemetry settings.
|
|
298
|
+
*
|
|
299
|
+
* Raw provider exchanges are kept only for the debug trace and are bounded by
|
|
300
|
+
* bytes. This endpoint never returns secrets or trace payloads.
|
|
301
|
+
*
|
|
302
|
+
* @param {string} [dir]
|
|
303
|
+
* @returns {object | { error: string }}
|
|
304
|
+
*/
|
|
305
|
+
export function getTelemetrySettings(dir) {
|
|
306
|
+
const root = dir || process.env.YEAFT_DIR || DEFAULT_YEAFT_DIR;
|
|
307
|
+
const configPath = join(root, 'config.json');
|
|
308
|
+
if (!existsSync(configPath)) return normaliseTelemetrySection(null);
|
|
309
|
+
try {
|
|
310
|
+
const json = JSON.parse(readFileSync(configPath, 'utf8'));
|
|
311
|
+
return normaliseTelemetrySection(json.telemetry);
|
|
312
|
+
} catch (e) {
|
|
313
|
+
return { error: `Failed to read config.json: ${e.message}` };
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Update only the telemetry section of config.json.
|
|
319
|
+
*
|
|
320
|
+
* @param {object} update
|
|
321
|
+
* @param {string} [dir]
|
|
322
|
+
* @returns {object | { error: string }}
|
|
323
|
+
*/
|
|
324
|
+
export function updateTelemetrySettings(update, dir) {
|
|
325
|
+
if (!update || typeof update !== 'object' || Array.isArray(update)) {
|
|
326
|
+
return { error: 'update payload required' };
|
|
327
|
+
}
|
|
328
|
+
const allowed = new Set(['enabled', 'retentionDays', 'flushIntervalMs', 'maxQueueSize', 'rawExchangeMaxBytes', 'traceTextMaxBytes']);
|
|
329
|
+
if (Object.keys(update).some(key => !allowed.has(key))) {
|
|
330
|
+
return { error: 'unknown telemetry setting' };
|
|
331
|
+
}
|
|
332
|
+
const root = dir || process.env.YEAFT_DIR || DEFAULT_YEAFT_DIR;
|
|
333
|
+
const configPath = join(root, 'config.json');
|
|
334
|
+
const existing = readConfigJson(configPath);
|
|
335
|
+
const merged = normaliseTelemetrySection({
|
|
336
|
+
...(existing.telemetry && typeof existing.telemetry === 'object' ? existing.telemetry : {}),
|
|
337
|
+
...update,
|
|
338
|
+
});
|
|
339
|
+
existing.telemetry = merged;
|
|
340
|
+
try {
|
|
341
|
+
writeFileSync(configPath, JSON.stringify(existing, null, 2) + '\n', 'utf8');
|
|
342
|
+
} catch (e) {
|
|
343
|
+
return { error: `Failed to write config.json: ${e.message}` };
|
|
344
|
+
}
|
|
345
|
+
return merged;
|
|
346
|
+
}
|
|
347
|
+
|
|
296
348
|
// ─── Search settings (web-search backend selection + Tavily key) ────
|
|
297
349
|
|
|
298
350
|
/**
|
package/yeaft/config.js
CHANGED
|
@@ -57,6 +57,18 @@ const DEFAULTS = {
|
|
|
57
57
|
// block is injected). Hand-edited values are NOT clamped — we let
|
|
58
58
|
// power users opt into larger docs at their own context-window risk.
|
|
59
59
|
projectDocMaxBytes: 32 * 1024,
|
|
60
|
+
// ─── Performance telemetry ─────────────────────────────
|
|
61
|
+
// Telemetry is local, best-effort diagnostics. The agent buffers events and
|
|
62
|
+
// flushes them in batches; raw provider responses are bounded separately so
|
|
63
|
+
// a long SSE stream cannot grow the debug payload without limit.
|
|
64
|
+
telemetry: {
|
|
65
|
+
enabled: true,
|
|
66
|
+
retentionDays: 3,
|
|
67
|
+
flushIntervalMs: 1_000,
|
|
68
|
+
maxQueueSize: 5_000,
|
|
69
|
+
rawExchangeMaxBytes: 512 * 1024,
|
|
70
|
+
traceTextMaxBytes: 256 * 1024,
|
|
71
|
+
},
|
|
60
72
|
// ─── LLM retry policy ──────────────────────────────────────
|
|
61
73
|
// How the engine reacts when adapter.stream()/call() throws a
|
|
62
74
|
// retryable error (429 / 529 / 5xx / transport failure). Each field
|
|
@@ -280,6 +292,46 @@ export function clampYeaftField(v, field) {
|
|
|
280
292
|
return Math.min(hi, Math.max(lo, Math.floor(n)));
|
|
281
293
|
}
|
|
282
294
|
|
|
295
|
+
/**
|
|
296
|
+
* Normalize the local performance telemetry section.
|
|
297
|
+
*
|
|
298
|
+
* `debug` controls human-facing verbosity; this section controls bounded,
|
|
299
|
+
* local timing diagnostics. Unknown keys are dropped so hand-edited config
|
|
300
|
+
* cannot leak arbitrary values into the engine hot path.
|
|
301
|
+
*
|
|
302
|
+
* @param {unknown} raw
|
|
303
|
+
* @returns {{ enabled: boolean, retentionDays: number, flushIntervalMs: number, maxQueueSize: number, rawExchangeMaxBytes: number, traceTextMaxBytes: number }}
|
|
304
|
+
*/
|
|
305
|
+
export function normaliseTelemetrySection(raw) {
|
|
306
|
+
const defaults = DEFAULTS.telemetry;
|
|
307
|
+
const out = { ...defaults };
|
|
308
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return out;
|
|
309
|
+
const value = /** @type {Record<string, unknown>} */ (raw);
|
|
310
|
+
if (typeof value.enabled === 'boolean') out.enabled = value.enabled;
|
|
311
|
+
|
|
312
|
+
const normalizeInteger = (candidate, min, max, fallback) => {
|
|
313
|
+
const number = Number(candidate);
|
|
314
|
+
if (!Number.isFinite(number)) return fallback;
|
|
315
|
+
return Math.min(max, Math.max(min, Math.floor(number)));
|
|
316
|
+
};
|
|
317
|
+
out.retentionDays = normalizeInteger(value.retentionDays, 1, 3650, defaults.retentionDays);
|
|
318
|
+
out.flushIntervalMs = normalizeInteger(value.flushIntervalMs, 0, 60_000, defaults.flushIntervalMs);
|
|
319
|
+
out.maxQueueSize = normalizeInteger(value.maxQueueSize, 100, 50_000, defaults.maxQueueSize);
|
|
320
|
+
out.rawExchangeMaxBytes = normalizeInteger(
|
|
321
|
+
value.rawExchangeMaxBytes,
|
|
322
|
+
0,
|
|
323
|
+
4 * 1024 * 1024,
|
|
324
|
+
defaults.rawExchangeMaxBytes,
|
|
325
|
+
);
|
|
326
|
+
out.traceTextMaxBytes = normalizeInteger(
|
|
327
|
+
value.traceTextMaxBytes,
|
|
328
|
+
0,
|
|
329
|
+
4 * 1024 * 1024,
|
|
330
|
+
defaults.traceTextMaxBytes,
|
|
331
|
+
);
|
|
332
|
+
return out;
|
|
333
|
+
}
|
|
334
|
+
|
|
283
335
|
/**
|
|
284
336
|
* Build config from legacy config.md + .env + env vars.
|
|
285
337
|
* @deprecated — used only when config.json doesn't exist.
|
|
@@ -313,6 +365,7 @@ function loadLegacyConfig(dir, overrides) {
|
|
|
313
365
|
projectDocMaxBytes: overrides.projectDocMaxBytes ?? fileConfig.projectDocMaxBytes ?? DEFAULTS.projectDocMaxBytes,
|
|
314
366
|
// task-318: legacy path never had the `yeaft` section — defaults.
|
|
315
367
|
yeaft: normaliseYeaftSection(null),
|
|
368
|
+
telemetry: normaliseTelemetrySection(null),
|
|
316
369
|
providers: null,
|
|
317
370
|
primaryModel: null,
|
|
318
371
|
fastModel: null,
|
|
@@ -446,6 +499,7 @@ export function loadConfig(overrides = {}) {
|
|
|
446
499
|
// task-318: Yeaft runtime caps. `yeaft` is a nested section so we
|
|
447
500
|
// don't pollute the flat config namespace used by chat code.
|
|
448
501
|
yeaft: normaliseYeaftSection(jsonConfig.yeaft),
|
|
502
|
+
telemetry: normaliseTelemetrySection(jsonConfig.telemetry),
|
|
449
503
|
|
|
450
504
|
// Legacy fields (null when using config.json)
|
|
451
505
|
apiKey: overrides.apiKey || null,
|
|
@@ -325,8 +325,9 @@ function queryIndex(request) {
|
|
|
325
325
|
const boundary = pageBoundary(db, request, generation);
|
|
326
326
|
const senderKey = typeof request.senderKey === 'string' ? request.senderKey : '';
|
|
327
327
|
const normalized = normalizeLiteralSearch(String(request.query || '').trim());
|
|
328
|
-
const
|
|
329
|
-
const
|
|
328
|
+
const terms = normalized.split(/\s+/u).filter(Boolean);
|
|
329
|
+
const queryLength = codePoints(terms[0] || '').length;
|
|
330
|
+
const limit = Math.min(100, Math.max(1, Number(request.limit) || 20));
|
|
330
331
|
const matches = [];
|
|
331
332
|
let candidateRowsRead = 0;
|
|
332
333
|
let candidateBytesRead = 0;
|
|
@@ -344,12 +345,14 @@ function queryIndex(request) {
|
|
|
344
345
|
params.push(batchEndSeq, batchEndSeq, batchEntryId);
|
|
345
346
|
}
|
|
346
347
|
if (queryLength >= 3) {
|
|
347
|
-
const
|
|
348
|
+
const grams = terms
|
|
349
|
+
.filter(term => codePoints(term).length >= 3)
|
|
350
|
+
.map(term => codePoints(term).slice(0, 3).join(''));
|
|
348
351
|
sql += ' AND rowid IN (SELECT rowid FROM entry_fts WHERE entry_fts MATCH ?)';
|
|
349
|
-
params.push(`"${
|
|
352
|
+
params.push(grams.map(gram => `"${gram.replaceAll('"', '""')}"`).join(' AND '));
|
|
350
353
|
} else if (queryLength > 0) {
|
|
351
354
|
sql += ' AND yeaft_bloom_contains(short_bloom, ?) = 1';
|
|
352
|
-
params.push(
|
|
355
|
+
params.push(terms[0]);
|
|
353
356
|
}
|
|
354
357
|
if (senderKey === 'user') sql += " AND role='user'";
|
|
355
358
|
else if (senderKey.startsWith('vp:')) {
|
|
@@ -374,9 +377,9 @@ function queryIndex(request) {
|
|
|
374
377
|
candidateRowsRead += 1;
|
|
375
378
|
candidateBytesRead += bytes;
|
|
376
379
|
lastRow = row;
|
|
377
|
-
const
|
|
378
|
-
if (
|
|
379
|
-
matches.push({ row, matchIndex });
|
|
380
|
+
const matchIndexes = terms.map(term => findLiteralSearch(row.text_body, term));
|
|
381
|
+
if (matchIndexes.some(index => index < 0)) continue;
|
|
382
|
+
matches.push({ row, matchIndex: matchIndexes[0] || 0 });
|
|
380
383
|
if (matches.length > limit) break;
|
|
381
384
|
}
|
|
382
385
|
maxBatchRows = Math.max(maxBatchRows, batchRows);
|
|
@@ -395,10 +398,10 @@ function queryIndex(request) {
|
|
|
395
398
|
const result = parseEntry(row, generation);
|
|
396
399
|
const radius = 90;
|
|
397
400
|
const start = Math.max(0, matchIndex - radius);
|
|
398
|
-
const end = Math.min(row.text_body.length, matchIndex +
|
|
401
|
+
const end = Math.min(row.text_body.length, matchIndex + (terms[0] || '').length + radius);
|
|
399
402
|
return {
|
|
400
403
|
...result,
|
|
401
|
-
snippet:
|
|
404
|
+
snippet: terms.length > 0
|
|
402
405
|
? `${start > 0 ? '…' : ''}${row.text_body.slice(start, end)}${end < row.text_body.length ? '…' : ''}`
|
|
403
406
|
: row.text_body.slice(0, 180),
|
|
404
407
|
};
|
|
@@ -11,6 +11,7 @@ export function isInternalControlContent(content) {
|
|
|
11
11
|
if (typeof content !== 'string') return false;
|
|
12
12
|
const text = content.trimStart();
|
|
13
13
|
return text.startsWith('<task-result ')
|
|
14
|
+
|| text.startsWith('[system note] Async task completion for ')
|
|
14
15
|
|| /^\[system note\] You have called \S+ with the same arguments \d+ times\./.test(text);
|
|
15
16
|
}
|
|
16
17
|
|
package/yeaft/debug-trace.js
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
import { promises as fsp } from 'fs';
|
|
14
14
|
import { basename, dirname, extname, join } from 'path';
|
|
15
15
|
import { createHash, randomUUID } from 'crypto';
|
|
16
|
+
import { truncateUtf8Text } from './perf-trace.js';
|
|
16
17
|
|
|
17
18
|
const TRACE_VERSION = 3;
|
|
18
19
|
const REQUEST_RETENTION = 10;
|
|
@@ -33,11 +34,18 @@ const MAX_RAW_RESPONSE_BYTES = 64 * 1024;
|
|
|
33
34
|
const TRACE_APPEND_BATCH_MS = 100;
|
|
34
35
|
const EVENT_FLUSH_INTERVAL_MS = 30_000;
|
|
35
36
|
const MAX_SEARCH_PATTERN_CHARS = 300;
|
|
37
|
+
const DEFAULT_TRACE_TEXT_MAX_BYTES = 256 * 1024;
|
|
36
38
|
|
|
37
39
|
function isPlainObject(value) {
|
|
38
40
|
return value && typeof value === 'object' && !Array.isArray(value);
|
|
39
41
|
}
|
|
40
42
|
|
|
43
|
+
function normalizeTextMaxBytes(value, fallback = DEFAULT_TRACE_TEXT_MAX_BYTES) {
|
|
44
|
+
const parsed = Number(value);
|
|
45
|
+
if (!Number.isFinite(parsed)) return fallback;
|
|
46
|
+
return Math.min(4 * 1024 * 1024, Math.max(0, Math.floor(parsed)));
|
|
47
|
+
}
|
|
48
|
+
|
|
41
49
|
function safeDirComponent(value, fallback = 'unknown') {
|
|
42
50
|
const raw = String(value || '').trim();
|
|
43
51
|
if (!raw) return fallback;
|
|
@@ -149,10 +157,13 @@ function traceMatchesRegex(trace, regex) {
|
|
|
149
157
|
function truncateText(value, maxBytes = MAX_TEXT_BYTES) {
|
|
150
158
|
if (value == null) return value ?? null;
|
|
151
159
|
const str = String(value);
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
160
|
+
const budget = Math.max(0, Number(maxBytes) || 0);
|
|
161
|
+
if (Buffer.byteLength(str, 'utf8') <= budget) return str;
|
|
162
|
+
if (budget <= 0) return '';
|
|
163
|
+
const marker = `\n... [truncated to ${budget} bytes]`;
|
|
164
|
+
const markerBytes = Buffer.byteLength(marker, 'utf8');
|
|
165
|
+
if (markerBytes >= budget) return truncateUtf8Text(str, budget).value;
|
|
166
|
+
return `${truncateUtf8Text(str, budget - markerBytes).value}${marker}`;
|
|
156
167
|
}
|
|
157
168
|
|
|
158
169
|
function cloneJsonValue(value) {
|
|
@@ -165,12 +176,26 @@ function safeJsonValue(value, maxBytes = MAX_INLINE_VALUE_BYTES) {
|
|
|
165
176
|
if (value == null) return value;
|
|
166
177
|
try {
|
|
167
178
|
const json = JSON.stringify(value);
|
|
168
|
-
|
|
169
|
-
return
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
179
|
+
const originalBytes = Buffer.byteLength(json, 'utf8');
|
|
180
|
+
if (originalBytes <= maxBytes) return JSON.parse(json);
|
|
181
|
+
// A bounded raw-exchange sentinel already carries the useful preview.
|
|
182
|
+
// Preserve a re-bounded preview rather than reducing persisted history to
|
|
183
|
+
// metadata after an outer trace record crosses its storage budget.
|
|
184
|
+
if (value && typeof value === 'object' && value.__truncated === true
|
|
185
|
+
&& typeof value.preview === 'string') {
|
|
186
|
+
const previewBudget = Math.max(0, maxBytes - 512);
|
|
187
|
+
const preview = truncateText(value.preview, previewBudget);
|
|
188
|
+
return {
|
|
189
|
+
__truncated: true,
|
|
190
|
+
...(value.reason ? { reason: value.reason } : {}),
|
|
191
|
+
originalBytes: Number.isFinite(Number(value.originalBytes))
|
|
192
|
+
? Number(value.originalBytes)
|
|
193
|
+
: originalBytes,
|
|
194
|
+
maxBytes,
|
|
195
|
+
...(preview ? { preview } : {}),
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
return { __truncated: true, originalBytes, maxBytes };
|
|
174
199
|
} catch {
|
|
175
200
|
return null;
|
|
176
201
|
}
|
|
@@ -207,8 +232,11 @@ function jsonByteLength(value) {
|
|
|
207
232
|
}
|
|
208
233
|
|
|
209
234
|
function rawRequestSentinel(reason, value = null, maxBytes = MAX_RAW_REQUEST_BYTES) {
|
|
210
|
-
const
|
|
211
|
-
?
|
|
235
|
+
const previewSource = typeof value === 'string'
|
|
236
|
+
? value
|
|
237
|
+
: (() => { try { return JSON.stringify(value); } catch { return null; } })();
|
|
238
|
+
const preview = typeof previewSource === 'string'
|
|
239
|
+
? truncateText(previewSource, Math.min(64 * 1024, maxBytes))
|
|
212
240
|
: null;
|
|
213
241
|
const originalBytes = value == null ? null : jsonByteLength(value);
|
|
214
242
|
return {
|
|
@@ -228,25 +256,6 @@ function boundRawValue(value, reason = 'raw_request_budget') {
|
|
|
228
256
|
return rawRequestSentinel(reason, value);
|
|
229
257
|
}
|
|
230
258
|
|
|
231
|
-
function buildRawRequestBase(value) {
|
|
232
|
-
if (value == null) return null;
|
|
233
|
-
if (typeof value === 'string') return truncateText(value, MAX_RAW_REQUEST_BYTES);
|
|
234
|
-
if (!isPlainObject(value)) return boundRawValue(value);
|
|
235
|
-
const base = {};
|
|
236
|
-
for (const [key, item] of Object.entries(value)) {
|
|
237
|
-
if (key === 'body' && isPlainObject(item)) {
|
|
238
|
-
const body = {};
|
|
239
|
-
for (const [bodyKey, bodyValue] of Object.entries(item)) {
|
|
240
|
-
body[bodyKey] = boundRawValue(bodyValue, `raw_request_body_${bodyKey}_budget`);
|
|
241
|
-
}
|
|
242
|
-
base.body = body;
|
|
243
|
-
} else {
|
|
244
|
-
base[key] = boundRawValue(item, `raw_request_${key}_budget`);
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
return base;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
259
|
function buildRawMessagesDelta(previousMessages, nextMessages) {
|
|
251
260
|
if (!Array.isArray(nextMessages)) return null;
|
|
252
261
|
const priorMessages = Array.isArray(previousMessages) ? previousMessages : [];
|
|
@@ -373,6 +382,11 @@ export function applyRawRequestDelta(previous, delta) {
|
|
|
373
382
|
const existing = Array.isArray(body[messageKey]) ? body[messageKey] : [];
|
|
374
383
|
const from = Number.isFinite(Number(delta.body.messagesFrom)) ? Number(delta.body.messagesFrom) : existing.length;
|
|
375
384
|
body[messageKey] = existing.slice(0, from).concat(cloneJsonValue(delta.body.messagesAppend) || []);
|
|
385
|
+
} else if (Object.prototype.hasOwnProperty.call(delta.body, 'messagesAppend')) {
|
|
386
|
+
// A huge append is represented by a bounded raw-request sentinel, not
|
|
387
|
+
// an array. Preserve that sentinel rather than silently dropping the
|
|
388
|
+
// entire request body during hydration.
|
|
389
|
+
body[messageKey] = cloneJsonValue(delta.body.messagesAppend) ?? delta.body.messagesAppend;
|
|
376
390
|
}
|
|
377
391
|
next.body = body;
|
|
378
392
|
}
|
|
@@ -389,10 +403,82 @@ function messagesPrefixLength(prevMessages, nextMessages) {
|
|
|
389
403
|
return i;
|
|
390
404
|
}
|
|
391
405
|
|
|
392
|
-
function
|
|
406
|
+
function snapshotTruncation(path, maxBytes, originalBytes) {
|
|
407
|
+
return { __truncated: true, path, maxBytes, originalBytes };
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function boundedSnapshotString(value, maxBytes, path) {
|
|
411
|
+
const originalBytes = Buffer.byteLength(value, 'utf8');
|
|
412
|
+
if (originalBytes <= maxBytes) return value;
|
|
413
|
+
const marker = snapshotTruncation(path, maxBytes, originalBytes);
|
|
414
|
+
if (jsonByteLength(marker) > maxBytes) return null;
|
|
415
|
+
const markerText = `\n... [truncated to ${maxBytes} bytes]`;
|
|
416
|
+
const markerBytes = Buffer.byteLength(markerText, 'utf8');
|
|
417
|
+
if (markerBytes >= maxBytes) return marker;
|
|
418
|
+
return truncateUtf8Text(value, maxBytes - markerBytes).value + markerText;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function boundSnapshotValue(value, maxBytes, path = 'messages') {
|
|
422
|
+
if (maxBytes <= 0) return null;
|
|
423
|
+
const sourceBytes = jsonByteLength(value);
|
|
424
|
+
if (sourceBytes <= maxBytes) return cloneJsonValue(value);
|
|
425
|
+
const marker = snapshotTruncation(path, maxBytes, sourceBytes);
|
|
426
|
+
if (jsonByteLength(marker) > maxBytes) return null;
|
|
427
|
+
if (value == null || typeof value === 'number' || typeof value === 'boolean') return marker;
|
|
428
|
+
if (typeof value === 'string') return boundedSnapshotString(value, maxBytes, path);
|
|
429
|
+
|
|
430
|
+
// Account JSON framing incrementally. The old clone-and-stringify-prefix
|
|
431
|
+
// loop reserialized the full growing array/object per member and blocked
|
|
432
|
+
// DebugTrace.endTurn for tens of seconds on a large provider history.
|
|
433
|
+
if (Array.isArray(value)) {
|
|
434
|
+
const out = [];
|
|
435
|
+
let usedBytes = 2; // []
|
|
436
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
437
|
+
const separatorBytes = out.length > 0 ? 1 : 0;
|
|
438
|
+
const remaining = Math.max(0, maxBytes - usedBytes - separatorBytes);
|
|
439
|
+
const candidate = boundSnapshotValue(value[index], remaining, `${path}[${index}]`);
|
|
440
|
+
if (candidate == null) break;
|
|
441
|
+
let encoded;
|
|
442
|
+
try { encoded = JSON.stringify(candidate); } catch { break; }
|
|
443
|
+
const candidateBytes = Buffer.byteLength(encoded, 'utf8');
|
|
444
|
+
if (usedBytes + separatorBytes + candidateBytes > maxBytes) break;
|
|
445
|
+
out.push(candidate);
|
|
446
|
+
usedBytes += separatorBytes + candidateBytes;
|
|
447
|
+
}
|
|
448
|
+
return out.length > 0 ? out : marker;
|
|
449
|
+
}
|
|
450
|
+
if (typeof value === 'object') {
|
|
451
|
+
const out = {};
|
|
452
|
+
let usedBytes = 2; // {}
|
|
453
|
+
let count = 0;
|
|
454
|
+
for (const [key, item] of Object.entries(value)) {
|
|
455
|
+
let keyJson;
|
|
456
|
+
try { keyJson = JSON.stringify(key); } catch { break; }
|
|
457
|
+
const keyBytes = Buffer.byteLength(keyJson, 'utf8');
|
|
458
|
+
const separatorBytes = count > 0 ? 1 : 0;
|
|
459
|
+
const remaining = Math.max(0, maxBytes - usedBytes - separatorBytes - keyBytes - 1);
|
|
460
|
+
const candidate = boundSnapshotValue(item, remaining, `${path}.${key}`);
|
|
461
|
+
if (candidate == null) break;
|
|
462
|
+
let encoded;
|
|
463
|
+
try { encoded = JSON.stringify(candidate); } catch { break; }
|
|
464
|
+
const candidateBytes = Buffer.byteLength(encoded, 'utf8');
|
|
465
|
+
if (usedBytes + separatorBytes + keyBytes + 1 + candidateBytes > maxBytes) break;
|
|
466
|
+
out[key] = candidate;
|
|
467
|
+
usedBytes += separatorBytes + keyBytes + 1 + candidateBytes;
|
|
468
|
+
count += 1;
|
|
469
|
+
}
|
|
470
|
+
return count > 0 ? out : marker;
|
|
471
|
+
}
|
|
472
|
+
return marker;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function buildRequestSnapshot(info = {}, textMaxBytes = DEFAULT_TRACE_TEXT_MAX_BYTES) {
|
|
476
|
+
const messageBudget = normalizeTextMaxBytes(textMaxBytes);
|
|
393
477
|
return {
|
|
394
|
-
systemPrompt: truncateText(info.systemPrompt || '',
|
|
395
|
-
messages: Array.isArray(info.messages)
|
|
478
|
+
systemPrompt: truncateText(info.systemPrompt || '', messageBudget),
|
|
479
|
+
messages: Array.isArray(info.messages)
|
|
480
|
+
? boundSnapshotValue(info.messages, messageBudget, 'messages')
|
|
481
|
+
: [],
|
|
396
482
|
rawRequest: info.rawRequest ?? null,
|
|
397
483
|
};
|
|
398
484
|
}
|
|
@@ -438,7 +524,7 @@ function applyRequestDelta(previous, delta = {}) {
|
|
|
438
524
|
const nextBase = {
|
|
439
525
|
systemPrompt: delta.systemPrompt || '',
|
|
440
526
|
messages: Array.isArray(delta.messages) ? delta.messages : [],
|
|
441
|
-
rawRequest: null,
|
|
527
|
+
rawRequest: Object.prototype.hasOwnProperty.call(delta, 'rawRequest') ? delta.rawRequest : null,
|
|
442
528
|
};
|
|
443
529
|
if (Object.prototype.hasOwnProperty.call(delta, 'rawRequestDelta')) {
|
|
444
530
|
nextBase.rawRequest = applyRawRequestDelta(null, delta.rawRequestDelta);
|
|
@@ -627,9 +713,13 @@ function summarizeTrace(trace, detailsLoaded = false) {
|
|
|
627
713
|
function expandTrace(trace) {
|
|
628
714
|
const turnsById = new Map([[trace.requestId || trace.traceId, summarizeTrace(trace, true)]]);
|
|
629
715
|
let snapshot = null;
|
|
716
|
+
let rawRequest = trace?.baseRequest?.rawRequest ?? null;
|
|
630
717
|
const loops = [];
|
|
631
718
|
for (const loop of Array.isArray(trace?.loops) ? trace.loops : []) {
|
|
632
719
|
snapshot = applyRequestDelta(snapshot || trace.baseRequest || null, loop.requestDelta || {});
|
|
720
|
+
if (loop?.requestDelta && Object.prototype.hasOwnProperty.call(loop.requestDelta, 'rawRequestDelta')) {
|
|
721
|
+
rawRequest = reconstructDebugRawRequest(rawRequest, loop.requestDelta);
|
|
722
|
+
}
|
|
633
723
|
const usage = normalizeUsage(loop?.usage || {});
|
|
634
724
|
loops.push({
|
|
635
725
|
turnId: trace.requestId || trace.traceId,
|
|
@@ -645,7 +735,7 @@ function expandTrace(trace) {
|
|
|
645
735
|
ttfbMs: loop.ttfbMs || null,
|
|
646
736
|
stopReason: loop.stopReason || null,
|
|
647
737
|
at: loop.at || null,
|
|
648
|
-
rawRequest
|
|
738
|
+
rawRequest,
|
|
649
739
|
rawResponse: loop.rawResponse ?? null,
|
|
650
740
|
requestDelta: loop.requestDelta || {},
|
|
651
741
|
requestBase: trace.baseRequest || null,
|
|
@@ -659,8 +749,12 @@ function expandTrace(trace) {
|
|
|
659
749
|
|
|
660
750
|
function traceToLegacyRows(trace) {
|
|
661
751
|
let snapshot = null;
|
|
752
|
+
let rawRequest = trace?.baseRequest?.rawRequest ?? null;
|
|
662
753
|
return (Array.isArray(trace?.loops) ? trace.loops : []).map((loop) => {
|
|
663
754
|
snapshot = applyRequestDelta(snapshot || trace.baseRequest || null, loop.requestDelta || {});
|
|
755
|
+
if (loop?.requestDelta && Object.prototype.hasOwnProperty.call(loop.requestDelta, 'rawRequestDelta')) {
|
|
756
|
+
rawRequest = reconstructDebugRawRequest(rawRequest, loop.requestDelta);
|
|
757
|
+
}
|
|
664
758
|
const u = normalizeUsage(loop?.usage || {});
|
|
665
759
|
return {
|
|
666
760
|
id: loop.turnRowId || loop.loopInstanceId || randomUUID(),
|
|
@@ -686,7 +780,7 @@ function traceToLegacyRows(trace) {
|
|
|
686
780
|
tool_calls_json: JSON.stringify(loop.toolCalls || []),
|
|
687
781
|
usage_json: JSON.stringify(u),
|
|
688
782
|
ttfb_ms: loop.ttfbMs || null,
|
|
689
|
-
raw_request: null,
|
|
783
|
+
raw_request: rawRequest == null ? null : JSON.stringify(rawRequest),
|
|
690
784
|
raw_response: typeof loop.rawResponse === 'string' ? loop.rawResponse : JSON.stringify(loop.rawResponse ?? null),
|
|
691
785
|
user_prompt: trace.userPrompt || '',
|
|
692
786
|
};
|
|
@@ -864,6 +958,8 @@ export class DebugTrace {
|
|
|
864
958
|
* @type {Promise<void>}
|
|
865
959
|
*/
|
|
866
960
|
#flushChain = Promise.resolve();
|
|
961
|
+
/** @type {number} */
|
|
962
|
+
#textMaxBytes = DEFAULT_TRACE_TEXT_MAX_BYTES;
|
|
867
963
|
/** @type {boolean} */
|
|
868
964
|
#acceptingWrites = true;
|
|
869
965
|
|
|
@@ -871,15 +967,23 @@ export class DebugTrace {
|
|
|
871
967
|
* @param {string} tracePath — Back-compatible path. If it looks like a DB
|
|
872
968
|
* file, traces are stored in a sibling `debug/` directory.
|
|
873
969
|
*/
|
|
874
|
-
constructor(tracePath) {
|
|
970
|
+
constructor(tracePath, options = {}) {
|
|
875
971
|
const rootDir = fileTraceRoot(tracePath);
|
|
876
972
|
if (!rootDir) throw new Error('DebugTrace requires a storage path');
|
|
877
973
|
this.#rootDir = rootDir;
|
|
974
|
+
this.#textMaxBytes = normalizeTextMaxBytes(options?.textMaxBytes);
|
|
878
975
|
// Best-effort, fire-and-forget: atomicWriteText re-ensures the request
|
|
879
976
|
// subdir before every write, so a missed mkdir here is harmless.
|
|
880
977
|
ensureDir(rootDir).catch(() => {});
|
|
881
978
|
}
|
|
882
979
|
|
|
980
|
+
refreshConfig(config = {}) {
|
|
981
|
+
const nextValue = config && typeof config === 'object'
|
|
982
|
+
? config.traceTextMaxBytes
|
|
983
|
+
: config;
|
|
984
|
+
this.#textMaxBytes = normalizeTextMaxBytes(nextValue, this.#textMaxBytes);
|
|
985
|
+
}
|
|
986
|
+
|
|
883
987
|
startTurn({ traceId, messageId = null, mode = null, turnNumber = null, sessionId = null, vpId = null, threadId = null, userPrompt = null } = {}) {
|
|
884
988
|
if (!this.#acceptingWrites) return 'null';
|
|
885
989
|
const turnRowId = randomUUID();
|
|
@@ -911,13 +1015,21 @@ export class DebugTrace {
|
|
|
911
1015
|
const trace = this.#loadRequest(ctx.sessionId, ctx.requestKey);
|
|
912
1016
|
if (!trace) return;
|
|
913
1017
|
const loopNumber = ctx.loopNumber || Number(info.turnNumber || 0);
|
|
914
|
-
const snapshot = buildRequestSnapshot(
|
|
1018
|
+
const snapshot = buildRequestSnapshot({
|
|
1019
|
+
...info,
|
|
1020
|
+
// An explicit null describes this loop: no provider raw request was
|
|
1021
|
+
// captured. Do not inherit a previous loop's body and falsely attribute
|
|
1022
|
+
// it to a transport failure or another uncaptured attempt.
|
|
1023
|
+
rawRequest: Object.prototype.hasOwnProperty.call(info, 'rawRequest')
|
|
1024
|
+
? info.rawRequest
|
|
1025
|
+
: (trace.baseRequest?.rawRequest ?? null),
|
|
1026
|
+
}, this.#textMaxBytes);
|
|
915
1027
|
const previousSnapshot = trace._lastSnapshot || this.#reconstructLastSnapshot(trace);
|
|
916
1028
|
if (!trace.baseRequest) {
|
|
917
1029
|
trace.baseRequest = {
|
|
918
1030
|
systemPrompt: snapshot.systemPrompt,
|
|
919
1031
|
messages: Array.isArray(snapshot.messages) ? cloneJsonValue(snapshot.messages) : [],
|
|
920
|
-
rawRequest: null,
|
|
1032
|
+
rawRequest: snapshot.rawRequest ?? null,
|
|
921
1033
|
};
|
|
922
1034
|
}
|
|
923
1035
|
const loopIndex = (trace.loops || []).findIndex(l => l.turnRowId === turnId);
|
|
@@ -927,7 +1039,7 @@ export class DebugTrace {
|
|
|
927
1039
|
loopNumber,
|
|
928
1040
|
startedAt: trace.openedAt || Date.now(),
|
|
929
1041
|
model: info.model || null,
|
|
930
|
-
response: truncateText(info.responseText || '',
|
|
1042
|
+
response: truncateText(info.responseText || '', this.#textMaxBytes),
|
|
931
1043
|
toolCalls: cloneJsonValue(Array.isArray(info.toolCalls) ? info.toolCalls : []),
|
|
932
1044
|
usage: normalizeUsage(info.usage || {}, {
|
|
933
1045
|
inputTokens: info.inputTokens || 0,
|
|
@@ -940,8 +1052,12 @@ export class DebugTrace {
|
|
|
940
1052
|
stopReason: info.stopReason || null,
|
|
941
1053
|
at: Date.now(),
|
|
942
1054
|
rawResponse: typeof info.rawResponse === 'string'
|
|
943
|
-
? truncateText(info.rawResponse, MAX_RAW_RESPONSE_BYTES)
|
|
944
|
-
: safeJsonValue(info.rawResponse, MAX_RAW_RESPONSE_BYTES),
|
|
1055
|
+
? truncateText(info.rawResponse, Math.min(this.#textMaxBytes, MAX_RAW_RESPONSE_BYTES))
|
|
1056
|
+
: safeJsonValue(info.rawResponse, Math.min(this.#textMaxBytes, MAX_RAW_RESPONSE_BYTES)),
|
|
1057
|
+
// Raw request is canonical base-plus-delta data. Persisting the full
|
|
1058
|
+
// snapshot on every loop made a 512 KiB request consume N × 512 KiB for
|
|
1059
|
+
// an N-loop trace and kept the same duplication in the hydrated cache.
|
|
1060
|
+
rawRequest: null,
|
|
945
1061
|
requestDelta: buildRequestDelta(previousSnapshot, snapshot),
|
|
946
1062
|
};
|
|
947
1063
|
if (loopIndex >= 0) trace.loops[loopIndex] = loop;
|
|
@@ -968,7 +1084,7 @@ export class DebugTrace {
|
|
|
968
1084
|
toolName: toolName || '?',
|
|
969
1085
|
toolCallId,
|
|
970
1086
|
toolInput: truncateText(toolInput == null ? null : String(toolInput), MAX_TOOL_INPUT),
|
|
971
|
-
toolOutput: truncateText(toolOutput == null ? null : String(toolOutput),
|
|
1087
|
+
toolOutput: truncateText(toolOutput == null ? null : String(toolOutput), this.#textMaxBytes),
|
|
972
1088
|
durationMs: Number(durationMs || 0),
|
|
973
1089
|
isError: !!isError,
|
|
974
1090
|
createdAt: Date.now(),
|
|
@@ -1238,7 +1354,7 @@ export class DebugTrace {
|
|
|
1238
1354
|
sessionId: normalizedSessionId,
|
|
1239
1355
|
vpId: vpId || null,
|
|
1240
1356
|
threadId: threadId || null,
|
|
1241
|
-
userPrompt: truncateText(userPrompt || '',
|
|
1357
|
+
userPrompt: truncateText(userPrompt || '', this.#textMaxBytes),
|
|
1242
1358
|
openedAt: now,
|
|
1243
1359
|
closedAt: null,
|
|
1244
1360
|
updatedAt: now,
|
|
@@ -1618,13 +1734,14 @@ export class NullTrace {
|
|
|
1618
1734
|
async compact() { return { before: 0, after: 0 }; }
|
|
1619
1735
|
async purge() {}
|
|
1620
1736
|
async close() {}
|
|
1737
|
+
refreshConfig() {}
|
|
1621
1738
|
async flush() {}
|
|
1622
1739
|
async fetchTurnDebug() { return { loops: [], turns: [], dreamEvents: [] }; }
|
|
1623
1740
|
async fetchRecentDebugHistory() { return { loops: [], turns: [], dreamEvents: [] }; }
|
|
1624
1741
|
}
|
|
1625
1742
|
|
|
1626
|
-
export function createTrace({ enabled, dbPath, dirPath }) {
|
|
1743
|
+
export function createTrace({ enabled, dbPath, dirPath, textMaxBytes }) {
|
|
1627
1744
|
const path = dirPath || dbPath;
|
|
1628
1745
|
if (!enabled || !path) return new NullTrace();
|
|
1629
|
-
return new DebugTrace(path);
|
|
1746
|
+
return new DebugTrace(path, { textMaxBytes });
|
|
1630
1747
|
}
|