@yeaft/webchat-agent 1.0.376 → 1.0.377
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/local-runtime/version.json +1 -1
- package/package.json +1 -1
- package/yeaft/debug-trace.js +115 -52
- package/yeaft/web-bridge.js +26 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":"1.0.
|
|
1
|
+
{"version":"1.0.377"}
|
package/package.json
CHANGED
package/yeaft/debug-trace.js
CHANGED
|
@@ -10,9 +10,10 @@
|
|
|
10
10
|
* best-effort: failures are logged and never allowed to stop the agent.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import { promises as fsp } from 'fs';
|
|
13
|
+
import { createReadStream, promises as fsp } from 'fs';
|
|
14
14
|
import { basename, dirname, extname, join } from 'path';
|
|
15
15
|
import { createHash, randomUUID } from 'crypto';
|
|
16
|
+
import { createInterface } from 'readline';
|
|
16
17
|
import { truncateUtf8Text } from './perf-trace.js';
|
|
17
18
|
|
|
18
19
|
const TRACE_VERSION = 3;
|
|
@@ -619,6 +620,7 @@ function serializableTraceMeta(trace) {
|
|
|
619
620
|
const meta = {
|
|
620
621
|
...trace,
|
|
621
622
|
loopCount: loops.length,
|
|
623
|
+
toolCount: tools.length,
|
|
622
624
|
...usage,
|
|
623
625
|
loopModels: [...new Set(loops.map(loop => loop?.model).filter(Boolean))],
|
|
624
626
|
stopReasons: [...new Set(loops.map(loop => loop?.stopReason).filter(Boolean))],
|
|
@@ -668,6 +670,33 @@ async function readJsonLines(filePath) {
|
|
|
668
670
|
return records;
|
|
669
671
|
}
|
|
670
672
|
|
|
673
|
+
async function countRequestEvents(requestDir) {
|
|
674
|
+
const meta = await readJson(requestMetaPath(requestDir));
|
|
675
|
+
if (!meta?.requestId) {
|
|
676
|
+
const legacy = await readJson(requestFilePath(requestDir));
|
|
677
|
+
return {
|
|
678
|
+
loopCount: Array.isArray(legacy?.loops) ? legacy.loops.length : 0,
|
|
679
|
+
toolCount: Array.isArray(legacy?.tools) ? legacy.tools.length : 0,
|
|
680
|
+
};
|
|
681
|
+
}
|
|
682
|
+
let loopCount = 0;
|
|
683
|
+
let toolCount = 0;
|
|
684
|
+
const stream = createReadStream(requestEventsPath(requestDir), { encoding: 'utf8' });
|
|
685
|
+
stream.on('error', () => {});
|
|
686
|
+
try {
|
|
687
|
+
const lines = createInterface({ input: stream, crlfDelay: Infinity });
|
|
688
|
+
for await (const line of lines) {
|
|
689
|
+
if (!line.trim()) continue;
|
|
690
|
+
try {
|
|
691
|
+
const event = JSON.parse(line);
|
|
692
|
+
if (event?.type === 'loop' && event.record) loopCount += 1;
|
|
693
|
+
else if (event?.type === 'tool' && event.record) toolCount += 1;
|
|
694
|
+
} catch { /* Ignore one torn final append. */ }
|
|
695
|
+
}
|
|
696
|
+
} catch { /* Missing/unreadable event file counts as empty. */ }
|
|
697
|
+
return { loopCount, toolCount };
|
|
698
|
+
}
|
|
699
|
+
|
|
671
700
|
async function readRequestDir(requestDir) {
|
|
672
701
|
const meta = await readJson(requestMetaPath(requestDir));
|
|
673
702
|
if (!meta?.requestId) {
|
|
@@ -894,19 +923,15 @@ async function removeRequestDirIfIdentityMatches(requestDir, expected) {
|
|
|
894
923
|
|
|
895
924
|
async function readTraceHeaders(rootDir, sessionId) {
|
|
896
925
|
const traces = [];
|
|
897
|
-
const requestDirs =
|
|
898
|
-
? await (async () => {
|
|
899
|
-
const dirs = [];
|
|
900
|
-
for (const entry of await readdirSafe(sessionRequestsDir(rootDir, null))) {
|
|
901
|
-
if (entry.isDirectory()) dirs.push(join(sessionRequestsDir(rootDir, null), entry.name));
|
|
902
|
-
}
|
|
903
|
-
return dirs;
|
|
904
|
-
})()
|
|
905
|
-
: await collectRequestDirs(rootDir, sessionId);
|
|
926
|
+
const requestDirs = await collectRequestDirs(rootDir, sessionId);
|
|
906
927
|
for (const requestDir of requestDirs) {
|
|
907
928
|
const meta = await readJson(requestMetaPath(requestDir));
|
|
908
|
-
const
|
|
909
|
-
if (!
|
|
929
|
+
const stored = meta?.requestId ? meta : await readJson(requestFilePath(requestDir));
|
|
930
|
+
if (!stored?.requestId || !stored?.requestKey) continue;
|
|
931
|
+
if (sessionId != null && stored.sessionId !== sessionId) continue;
|
|
932
|
+
const trace = meta?.requestId ? { ...meta } : serializableTraceMeta(stored);
|
|
933
|
+
trace._persistedFormat = meta?.requestId ? 'events' : 'legacy';
|
|
934
|
+
trace._persistedRequestDir = requestDir;
|
|
910
935
|
traces.push({
|
|
911
936
|
trace,
|
|
912
937
|
file: requestFilePath(requestDir),
|
|
@@ -917,6 +942,13 @@ async function readTraceHeaders(rootDir, sessionId) {
|
|
|
917
942
|
return traces.sort((a, b) => ((a.openedAt || 0) - (b.openedAt || 0)) || String(a.trace.requestKey).localeCompare(String(b.trace.requestKey)));
|
|
918
943
|
}
|
|
919
944
|
|
|
945
|
+
async function readHeaderDetail(rootDir, header) {
|
|
946
|
+
const requestDir = header?._persistedRequestDir
|
|
947
|
+
|| requestDirFor(rootDir, header?.sessionId || null, header?.requestKey);
|
|
948
|
+
const trace = await readRequestDir(requestDir);
|
|
949
|
+
return sameTraceIdentity(trace, header) ? trace : null;
|
|
950
|
+
}
|
|
951
|
+
|
|
920
952
|
async function countDirFiles(rootDir) {
|
|
921
953
|
let files = 0;
|
|
922
954
|
let bytes = 0;
|
|
@@ -947,6 +979,8 @@ export class DebugTrace {
|
|
|
947
979
|
#initializedRequestKeys = new Set();
|
|
948
980
|
/** @type {Set<string>} */
|
|
949
981
|
#reconciledRetentionSessions = new Set();
|
|
982
|
+
/** @type {Map<string, object>} Lightweight persisted metadata by request key. */
|
|
983
|
+
#diskHeaders = new Map();
|
|
950
984
|
/** @type {Map<string, Map<string, { trace: object, requestDirs: Set<string>, openedAt: number }>>} */
|
|
951
985
|
#retentionIndex = new Map();
|
|
952
986
|
/** @type {NodeJS.Timeout|null} */
|
|
@@ -971,9 +1005,9 @@ export class DebugTrace {
|
|
|
971
1005
|
/** @type {NodeJS.Timeout|null} */
|
|
972
1006
|
#eventFlushTimer = null;
|
|
973
1007
|
/**
|
|
974
|
-
* One-time hydrate guard. Reads/maintenance
|
|
975
|
-
*
|
|
976
|
-
*
|
|
1008
|
+
* One-time metadata hydrate guard. Reads/maintenance keep only bounded
|
|
1009
|
+
* request headers resident. Full loop/tool payloads are loaded for one
|
|
1010
|
+
* selected request at a time and are never installed in #requestCache.
|
|
977
1011
|
* @type {boolean}
|
|
978
1012
|
*/
|
|
979
1013
|
#hydrated = false;
|
|
@@ -1270,16 +1304,20 @@ export class DebugTrace {
|
|
|
1270
1304
|
await this.#ensureHydrated();
|
|
1271
1305
|
await this.#drainWrites();
|
|
1272
1306
|
const tools = [];
|
|
1273
|
-
for (const { trace } of this.#traceSummaries()) {
|
|
1307
|
+
for (const { trace: header } of this.#traceSummaries().slice().reverse()) {
|
|
1308
|
+
const trace = this.#requestCache.get(header.requestKey)
|
|
1309
|
+
|| await readHeaderDetail(this.#rootDir, header);
|
|
1310
|
+
if (!trace) continue;
|
|
1274
1311
|
for (const tool of Array.isArray(trace.tools) ? trace.tools : []) {
|
|
1275
1312
|
const row = traceToolToLegacy(trace, tool);
|
|
1276
1313
|
if (name && row.tool_name !== name) continue;
|
|
1277
1314
|
if (since && row.created_at < since) continue;
|
|
1278
1315
|
tools.push(row);
|
|
1279
1316
|
}
|
|
1317
|
+
tools.sort((a, b) => (b.created_at || 0) - (a.created_at || 0));
|
|
1318
|
+
if (tools.length > 100) tools.length = 100;
|
|
1280
1319
|
}
|
|
1281
|
-
tools
|
|
1282
|
-
return tools.slice(0, 100);
|
|
1320
|
+
return tools;
|
|
1283
1321
|
}
|
|
1284
1322
|
|
|
1285
1323
|
async search(keyword) {
|
|
@@ -1287,19 +1325,42 @@ export class DebugTrace {
|
|
|
1287
1325
|
await this.#drainWrites();
|
|
1288
1326
|
const needle = String(keyword || '').toLowerCase();
|
|
1289
1327
|
if (!needle) return [];
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
.
|
|
1293
|
-
|
|
1294
|
-
|
|
1328
|
+
const results = [];
|
|
1329
|
+
for (const { trace: header } of this.#traceSummaries().slice().reverse()) {
|
|
1330
|
+
const trace = this.#requestCache.get(header.requestKey)
|
|
1331
|
+
|| await readHeaderDetail(this.#rootDir, header);
|
|
1332
|
+
if (!trace || !JSON.stringify(trace).toLowerCase().includes(needle)) continue;
|
|
1333
|
+
results.push(...traceToLegacyRows(trace));
|
|
1334
|
+
if (results.length >= 50) break;
|
|
1335
|
+
}
|
|
1336
|
+
return results.slice(0, 50);
|
|
1295
1337
|
}
|
|
1296
1338
|
|
|
1297
1339
|
async stats() {
|
|
1298
1340
|
await this.#ensureHydrated();
|
|
1299
1341
|
await this.#drainWrites();
|
|
1300
1342
|
const traces = this.#traceSummaries().map(({ trace }) => trace);
|
|
1301
|
-
|
|
1302
|
-
|
|
1343
|
+
let turnCount = 0;
|
|
1344
|
+
let toolCount = 0;
|
|
1345
|
+
for (const header of traces) {
|
|
1346
|
+
const live = this.#requestCache.get(header.requestKey);
|
|
1347
|
+
if (live) {
|
|
1348
|
+
turnCount += Array.isArray(live.loops) ? live.loops.length : Number(live.loopCount || 0);
|
|
1349
|
+
toolCount += Array.isArray(live.tools) ? live.tools.length : Number(live.toolCount || 0);
|
|
1350
|
+
continue;
|
|
1351
|
+
}
|
|
1352
|
+
if (Number.isFinite(Number(header.loopCount)) && Number.isFinite(Number(header.toolCount))) {
|
|
1353
|
+
turnCount += Number(header.loopCount);
|
|
1354
|
+
toolCount += Number(header.toolCount);
|
|
1355
|
+
continue;
|
|
1356
|
+
}
|
|
1357
|
+
const counts = await countRequestEvents(
|
|
1358
|
+
header._persistedRequestDir
|
|
1359
|
+
|| requestDirFor(this.#rootDir, header.sessionId || null, header.requestKey),
|
|
1360
|
+
);
|
|
1361
|
+
turnCount += counts.loopCount;
|
|
1362
|
+
toolCount += counts.toolCount;
|
|
1363
|
+
}
|
|
1303
1364
|
const eventCount = this.#events.length;
|
|
1304
1365
|
const { bytes } = await countDirFiles(this.#rootDir);
|
|
1305
1366
|
return { turnCount, toolCount, eventCount, dbSizeBytes: bytes, fileSizeBytes: bytes, requestCount: traces.length };
|
|
@@ -1331,6 +1392,7 @@ export class DebugTrace {
|
|
|
1331
1392
|
await ensureDir(this.#rootDir);
|
|
1332
1393
|
this.#turnIndex.clear();
|
|
1333
1394
|
this.#requestCache.clear();
|
|
1395
|
+
this.#diskHeaders.clear();
|
|
1334
1396
|
this.#initializedRequestKeys.clear();
|
|
1335
1397
|
this.#reconciledRetentionSessions.clear();
|
|
1336
1398
|
this.#retentionIndex.clear();
|
|
@@ -1429,11 +1491,10 @@ export class DebugTrace {
|
|
|
1429
1491
|
}
|
|
1430
1492
|
|
|
1431
1493
|
#traceSummaries(sessionId = null) {
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
// what turns the old O(N^2) per-query rescan into an in-memory filter.
|
|
1494
|
+
const merged = new Map(this.#diskHeaders);
|
|
1495
|
+
for (const trace of this.#requestCache.values()) merged.set(trace.requestKey, trace);
|
|
1435
1496
|
const out = [];
|
|
1436
|
-
for (const trace of
|
|
1497
|
+
for (const trace of merged.values()) {
|
|
1437
1498
|
if (sessionId && trace.sessionId !== sessionId) continue;
|
|
1438
1499
|
if (!trace?.requestId || !trace?.requestKey) continue;
|
|
1439
1500
|
out.push({
|
|
@@ -1446,34 +1507,24 @@ export class DebugTrace {
|
|
|
1446
1507
|
}
|
|
1447
1508
|
|
|
1448
1509
|
/**
|
|
1449
|
-
* Load
|
|
1450
|
-
*
|
|
1451
|
-
*
|
|
1452
|
-
*
|
|
1453
|
-
*
|
|
1454
|
-
* Footprint trade-off (deliberate): this pins every session's full trace
|
|
1455
|
-
* payload (systemPrompt + cumulative messages + rawRequest, each already
|
|
1456
|
-
* byte-bounded) in #requestCache for the instance's lifetime — O(retention ×
|
|
1457
|
-
* total sessions) memory instead of the old O(N²) per-query CPU rescan that
|
|
1458
|
-
* stalled the event loop. Retention (10/session) and the byte caps keep it
|
|
1459
|
-
* bounded, and the web path uses a single module-level instance. Follow-up if
|
|
1460
|
-
* footprint ever bites: hydrate per queried sessionId, or keep only a
|
|
1461
|
-
* lightweight summary resident and lazy-load full payloads on detailTurnId.
|
|
1510
|
+
* Load lightweight persisted metadata once. Active requests stay in
|
|
1511
|
+
* #requestCache; completed payloads remain on disk and are lazy-loaded only
|
|
1512
|
+
* for detail/search/tool queries. This prevents retained debug history from
|
|
1513
|
+
* expanding into a process-lifetime multi-gigabyte object graph.
|
|
1462
1514
|
*/
|
|
1463
1515
|
async #ensureHydrated() {
|
|
1464
1516
|
if (this.#hydrated) return;
|
|
1465
1517
|
if (this.#hydratePromise) return this.#hydratePromise;
|
|
1466
1518
|
this.#hydratePromise = (async () => {
|
|
1467
|
-
const [
|
|
1468
|
-
|
|
1519
|
+
const [headers, storedEvents] = await Promise.all([
|
|
1520
|
+
readTraceHeaders(this.#rootDir, null),
|
|
1469
1521
|
readJson(join(this.#rootDir, 'events.json')),
|
|
1470
1522
|
]);
|
|
1471
|
-
for (const { trace } of
|
|
1472
|
-
if (!trace?.requestKey) continue;
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
}
|
|
1523
|
+
for (const { trace } of headers) {
|
|
1524
|
+
if (!trace?.requestKey || this.#requestCache.has(trace.requestKey)) continue;
|
|
1525
|
+
this.#diskHeaders.set(trace.requestKey, trace.active
|
|
1526
|
+
? { ...trace, active: false, interrupted: true }
|
|
1527
|
+
: trace);
|
|
1477
1528
|
}
|
|
1478
1529
|
if (Array.isArray(storedEvents)) this.#mergeStoredEvents(storedEvents);
|
|
1479
1530
|
this.#hydrated = true;
|
|
@@ -1592,13 +1643,23 @@ export class DebugTrace {
|
|
|
1592
1643
|
const eventPath = requestEventsPath(requestDir);
|
|
1593
1644
|
if (initialize) await prepareJsonlAppend(eventPath);
|
|
1594
1645
|
await fsp.appendFile(eventPath, lines.join(''), 'utf8');
|
|
1595
|
-
if (writeMeta)
|
|
1646
|
+
if (writeMeta) {
|
|
1647
|
+
const meta = serializableTraceMeta(trace);
|
|
1648
|
+
await atomicWriteText(metaPath, JSON.stringify(meta));
|
|
1649
|
+
this.#diskHeaders.set(trace.requestKey, meta);
|
|
1650
|
+
}
|
|
1596
1651
|
trace._persistedFormat = 'events';
|
|
1597
1652
|
trace._persistedRequestDir = requestDir;
|
|
1598
1653
|
if (legacyRequestDir && legacyRequestDir !== requestDir) {
|
|
1599
1654
|
await removeRequestDirIfIdentityMatches(legacyRequestDir, trace);
|
|
1600
1655
|
}
|
|
1601
|
-
if (evictAfterWrite)
|
|
1656
|
+
if (evictAfterWrite) {
|
|
1657
|
+
this.#requestCache.delete(trace.requestKey);
|
|
1658
|
+
this.#initializedRequestKeys.delete(trace.requestKey);
|
|
1659
|
+
for (const [turnId, ctx] of this.#turnIndex) {
|
|
1660
|
+
if (ctx.requestKey === trace.requestKey) this.#turnIndex.delete(turnId);
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1602
1663
|
} catch (err) {
|
|
1603
1664
|
console.warn('[Yeaft] debug trace append failed:', err?.message || err);
|
|
1604
1665
|
}
|
|
@@ -1694,6 +1755,7 @@ export class DebugTrace {
|
|
|
1694
1755
|
|
|
1695
1756
|
async #pruneAll(keep) {
|
|
1696
1757
|
const sessions = new Set();
|
|
1758
|
+
for (const trace of this.#diskHeaders.values()) sessions.add(trace.sessionId || null);
|
|
1697
1759
|
for (const trace of this.#requestCache.values()) sessions.add(trace.sessionId || null);
|
|
1698
1760
|
for (const sessionId of sessions) await this.#pruneSession(sessionId, keep);
|
|
1699
1761
|
}
|
|
@@ -1743,6 +1805,7 @@ export class DebugTrace {
|
|
|
1743
1805
|
const stale = pruneCandidates.slice(0, Math.max(0, traces.length - protectedItems.length - keep));
|
|
1744
1806
|
for (const item of stale) {
|
|
1745
1807
|
this.#requestCache.delete(item.trace.requestKey);
|
|
1808
|
+
this.#diskHeaders.delete(item.trace.requestKey);
|
|
1746
1809
|
this.#initializedRequestKeys.delete(item.trace.requestKey);
|
|
1747
1810
|
index.delete(item.trace.requestKey);
|
|
1748
1811
|
for (const requestDir of item.requestDirs) {
|
package/yeaft/web-bridge.js
CHANGED
|
@@ -954,7 +954,12 @@ function registerRoutePromise(msgId, promise) {
|
|
|
954
954
|
routePromisesByMsgId.set(msgId, set);
|
|
955
955
|
}
|
|
956
956
|
set.add(promise);
|
|
957
|
-
promise.finally(() =>
|
|
957
|
+
promise.finally(() => {
|
|
958
|
+
set.delete(promise);
|
|
959
|
+
if (set.size === 0 && routePromisesByMsgId.get(msgId) === set) {
|
|
960
|
+
routePromisesByMsgId.delete(msgId);
|
|
961
|
+
}
|
|
962
|
+
}).catch(() => {});
|
|
958
963
|
}
|
|
959
964
|
|
|
960
965
|
|
|
@@ -1859,7 +1864,12 @@ function getOrCreateVpEngine(sessionId, vpId, threadId = 'main') {
|
|
|
1859
1864
|
*/
|
|
1860
1865
|
function getOrCreateSessionContext(sessionId, sessionHandle) {
|
|
1861
1866
|
let entry = sessionContexts.get(sessionId);
|
|
1862
|
-
if (entry && entry.coord && entry.router)
|
|
1867
|
+
if (entry && entry.coord && entry.router) {
|
|
1868
|
+
if (sessionHandle && sessionHandle !== entry.sessionHandle) {
|
|
1869
|
+
try { sessionHandle.close?.(); } catch { /* best-effort unused handle cleanup */ }
|
|
1870
|
+
}
|
|
1871
|
+
return entry;
|
|
1872
|
+
}
|
|
1863
1873
|
// Either no entry, or a partial entry seeded by `getOrCreateSessionHistory`
|
|
1864
1874
|
// (no coord/router yet). Build the coord/router and merge into the
|
|
1865
1875
|
// existing record so the per-group history reference and hydration
|
|
@@ -7820,6 +7830,20 @@ export const __testHooks = {
|
|
|
7820
7830
|
async loadProjectRuntime(workDir) {
|
|
7821
7831
|
return loadProjectRuntime(workDir);
|
|
7822
7832
|
},
|
|
7833
|
+
registerRoutePromiseForTest(msgId, promise) {
|
|
7834
|
+
registerRoutePromise(msgId, promise);
|
|
7835
|
+
},
|
|
7836
|
+
routePromiseEntryCountForTest() {
|
|
7837
|
+
return routePromisesByMsgId.size;
|
|
7838
|
+
},
|
|
7839
|
+
getOrCreateSessionContextForTest(sessionId, sessionHandle) {
|
|
7840
|
+
return getOrCreateSessionContext(sessionId, sessionHandle);
|
|
7841
|
+
},
|
|
7842
|
+
clearSessionContextForTest(sessionId) {
|
|
7843
|
+
const entry = sessionContexts.get(sessionId);
|
|
7844
|
+
try { entry?.sessionHandle?.close?.(); } catch { /* best-effort test cleanup */ }
|
|
7845
|
+
sessionContexts.delete(sessionId);
|
|
7846
|
+
},
|
|
7823
7847
|
seedSessionContext(sessionId, meta) {
|
|
7824
7848
|
const group = {
|
|
7825
7849
|
getMeta() { return structuredClone(meta); },
|