@yeaft/webchat-agent 1.0.371 → 1.0.372
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/buffer.js +60 -46
- package/context.js +5 -1
- package/local-runtime/version.json +1 -1
- package/package.json +1 -1
- package/yeaft/debug-trace.js +93 -34
- package/yeaft/engine.js +14 -1
package/connection/buffer.js
CHANGED
|
@@ -2,7 +2,6 @@ import WebSocket from 'ws';
|
|
|
2
2
|
import ctx from '../context.js';
|
|
3
3
|
import { encrypt, decrypt, isEncrypted } from '../encryption.js';
|
|
4
4
|
|
|
5
|
-
// 需要在断连期间缓冲的消息类型(CLI / Session 输出相关的关键消息)
|
|
6
5
|
export const BUFFERABLE_TYPES = new Set([
|
|
7
6
|
'claude_output', 'yeaft_output', 'yeaft_session_output', 'session_output',
|
|
8
7
|
'yeaft_history_chunk',
|
|
@@ -10,43 +9,56 @@ export const BUFFERABLE_TYPES = new Set([
|
|
|
10
9
|
'session_id_update', 'compact_status', 'slash_commands_update',
|
|
11
10
|
'background_task_started', 'background_task_output',
|
|
12
11
|
'subagent_started', 'subagent_message', 'subagent_completed',
|
|
13
|
-
// Work Center broadcasts are projections over Agent-local SQLite. Buffering
|
|
14
|
-
// prevents a terminal transition from disappearing during a short reconnect;
|
|
15
|
-
// clients still refresh with `list` after reconnect for authoritative state.
|
|
16
12
|
'work_center_event'
|
|
17
13
|
]);
|
|
18
14
|
|
|
15
|
+
function messageBytes(msg) {
|
|
16
|
+
try { return Buffer.byteLength(JSON.stringify(msg), 'utf8'); }
|
|
17
|
+
catch { return 0; }
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const TERMINAL_TYPES = new Set(['turn_completed', 'conversation_closed']);
|
|
21
|
+
|
|
22
|
+
function removeBufferedAt(index) {
|
|
23
|
+
const [removed] = ctx.messageBuffer.splice(index, 1);
|
|
24
|
+
ctx.messageBufferBytes = Math.max(0, Number(ctx.messageBufferBytes || 0) - messageBytes(removed));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function removeOutboundAt(index, outcome = 'dropped') {
|
|
28
|
+
const [removed] = ctx.outboundSendQueue.splice(index, 1);
|
|
29
|
+
ctx.outboundSendQueueBytes = Math.max(0, Number(ctx.outboundSendQueueBytes || 0) - Number(removed?.bytes || 0));
|
|
30
|
+
removed?.resolve?.(outcome);
|
|
31
|
+
}
|
|
32
|
+
|
|
19
33
|
function bufferMessage(msg, reason) {
|
|
20
34
|
if (!BUFFERABLE_TYPES.has(msg.type)) {
|
|
21
35
|
console.warn(`[WS] Cannot send message, WebSocket not open: ${msg.type}`);
|
|
22
36
|
return 'dropped';
|
|
23
37
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
38
|
+
const bytes = messageBytes(msg);
|
|
39
|
+
const maxBytes = Math.max(1, Number(ctx.messageBufferMaxBytes) || 8 * 1024 * 1024);
|
|
40
|
+
if (bytes > maxBytes) {
|
|
41
|
+
console.warn(`[WS] Message exceeds disconnected buffer byte budget, dropping: ${msg.type}`);
|
|
42
|
+
return 'dropped';
|
|
28
43
|
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
ctx.messageBuffer.
|
|
34
|
-
|
|
35
|
-
|
|
44
|
+
while (ctx.messageBuffer.length > 0 && (
|
|
45
|
+
ctx.messageBuffer.length >= ctx.messageBufferMaxSize
|
|
46
|
+
|| Number(ctx.messageBufferBytes || 0) + bytes > maxBytes
|
|
47
|
+
)) {
|
|
48
|
+
const nonTerminal = ctx.messageBuffer.findIndex(m => !TERMINAL_TYPES.has(m.type));
|
|
49
|
+
if (nonTerminal < 0) break;
|
|
50
|
+
removeBufferedAt(nonTerminal);
|
|
36
51
|
}
|
|
37
|
-
|
|
38
|
-
|
|
52
|
+
if (ctx.messageBuffer.length >= ctx.messageBufferMaxSize
|
|
53
|
+
|| Number(ctx.messageBufferBytes || 0) + bytes > maxBytes) return 'dropped';
|
|
54
|
+
ctx.messageBuffer.push(msg);
|
|
55
|
+
ctx.messageBufferBytes = Number(ctx.messageBufferBytes || 0) + bytes;
|
|
56
|
+
console.log(`[WS] ${reason}, buffered: ${msg.type} (queue: ${ctx.messageBuffer.length})`);
|
|
57
|
+
return 'buffered';
|
|
39
58
|
}
|
|
40
59
|
|
|
41
60
|
async function sendNow(msg) {
|
|
42
|
-
if (!ctx.ws || ctx.ws.readyState !== WebSocket.OPEN)
|
|
43
|
-
return bufferMessage(msg, 'Disconnected');
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// feat-ws-plaintext-negotiation: encrypt only when the server has
|
|
47
|
-
// NOT advertised plaintext acceptance. Defaults to encrypted for
|
|
48
|
-
// back-compat with old servers; flipped to plaintext when the
|
|
49
|
-
// `registered` frame includes `acceptPlaintext: true`.
|
|
61
|
+
if (!ctx.ws || ctx.ws.readyState !== WebSocket.OPEN) return bufferMessage(msg, 'Disconnected');
|
|
50
62
|
if (ctx.serverEncryptionRequired && ctx.sessionKey) {
|
|
51
63
|
const encrypted = await encrypt(msg, ctx.sessionKey);
|
|
52
64
|
ctx.ws.send(JSON.stringify(encrypted));
|
|
@@ -63,6 +75,7 @@ function scheduleOutboundDrain() {
|
|
|
63
75
|
try {
|
|
64
76
|
while (ctx.outboundSendQueue.length > 0) {
|
|
65
77
|
const item = ctx.outboundSendQueue.shift();
|
|
78
|
+
ctx.outboundSendQueueBytes = Math.max(0, Number(ctx.outboundSendQueueBytes || 0) - Number(item?.bytes || 0));
|
|
66
79
|
const msg = item?.msg ?? item;
|
|
67
80
|
try {
|
|
68
81
|
const outcome = await sendNow(msg);
|
|
@@ -72,8 +85,6 @@ function scheduleOutboundDrain() {
|
|
|
72
85
|
const outcome = msg ? bufferMessage(msg, 'Send failed') : 'dropped';
|
|
73
86
|
item?.resolve?.(outcome);
|
|
74
87
|
}
|
|
75
|
-
// Yield between frames so ping/pong, inbound control messages and UI
|
|
76
|
-
// events cannot be starved by a reconnect flush or a burst of tool output.
|
|
77
88
|
await new Promise(resolve => setImmediate(resolve));
|
|
78
89
|
}
|
|
79
90
|
} finally {
|
|
@@ -83,42 +94,45 @@ function scheduleOutboundDrain() {
|
|
|
83
94
|
});
|
|
84
95
|
}
|
|
85
96
|
|
|
86
|
-
// Send message to server (with encryption if available)
|
|
87
|
-
// 断连时对关键消息类型进行缓冲,重连后自动 flush
|
|
88
97
|
export async function sendToServer(msg) {
|
|
89
|
-
if (!ctx.ws || ctx.ws.readyState !== WebSocket.OPEN)
|
|
90
|
-
|
|
98
|
+
if (!ctx.ws || ctx.ws.readyState !== WebSocket.OPEN) return bufferMessage(msg, 'Disconnected');
|
|
99
|
+
const bytes = messageBytes(msg);
|
|
100
|
+
const maxBytes = Math.max(1, Number(ctx.outboundSendQueueMaxBytes) || 8 * 1024 * 1024);
|
|
101
|
+
if (bytes > maxBytes) {
|
|
102
|
+
console.warn(`[WS] Outbound message exceeds byte budget, dropping: ${msg.type}`);
|
|
103
|
+
return 'dropped';
|
|
104
|
+
}
|
|
105
|
+
while (TERMINAL_TYPES.has(msg.type)
|
|
106
|
+
&& Number(ctx.outboundSendQueueBytes || 0) + bytes > maxBytes) {
|
|
107
|
+
const nonTerminal = ctx.outboundSendQueue.findIndex(item => !TERMINAL_TYPES.has(item?.msg?.type));
|
|
108
|
+
if (nonTerminal < 0) break;
|
|
109
|
+
removeOutboundAt(nonTerminal);
|
|
110
|
+
}
|
|
111
|
+
if (Number(ctx.outboundSendQueueBytes || 0) + bytes > maxBytes) {
|
|
112
|
+
console.warn(`[WS] Outbound queue byte budget exceeded, dropping: ${msg.type}`);
|
|
113
|
+
return 'dropped';
|
|
91
114
|
}
|
|
92
115
|
const promise = new Promise((resolve, reject) => {
|
|
93
|
-
ctx.outboundSendQueue.push({ msg, resolve, reject });
|
|
116
|
+
ctx.outboundSendQueue.push({ msg, bytes, resolve, reject });
|
|
117
|
+
ctx.outboundSendQueueBytes = Number(ctx.outboundSendQueueBytes || 0) + bytes;
|
|
94
118
|
});
|
|
95
119
|
scheduleOutboundDrain();
|
|
96
120
|
return promise;
|
|
97
121
|
}
|
|
98
122
|
|
|
99
|
-
// Flush 断连期间缓冲的消息
|
|
100
123
|
export async function flushMessageBuffer() {
|
|
101
124
|
if (ctx.messageBuffer.length === 0) return;
|
|
102
|
-
|
|
103
125
|
const buffered = ctx.messageBuffer.splice(0);
|
|
126
|
+
ctx.messageBufferBytes = 0;
|
|
104
127
|
console.log(`[WS] Flushing ${buffered.length} buffered messages...`);
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
await sendToServer(msg);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
console.log(`[WS] Flush queued`);
|
|
128
|
+
for (const msg of buffered) await sendToServer(msg);
|
|
129
|
+
console.log('[WS] Flush queued');
|
|
111
130
|
}
|
|
112
131
|
|
|
113
|
-
// Parse incoming message (decrypt if encrypted)
|
|
114
132
|
export async function parseMessage(data) {
|
|
115
133
|
try {
|
|
116
134
|
const parsed = JSON.parse(data.toString());
|
|
117
|
-
|
|
118
|
-
if (ctx.sessionKey && isEncrypted(parsed)) {
|
|
119
|
-
return await decrypt(parsed, ctx.sessionKey);
|
|
120
|
-
}
|
|
121
|
-
|
|
135
|
+
if (ctx.sessionKey && isEncrypted(parsed)) return await decrypt(parsed, ctx.sessionKey);
|
|
122
136
|
return parsed;
|
|
123
137
|
} catch (e) {
|
|
124
138
|
console.error('Failed to parse message:', e);
|
package/context.js
CHANGED
|
@@ -47,11 +47,15 @@ export default {
|
|
|
47
47
|
lastHeartbeatStallAt: 0,
|
|
48
48
|
lastHeartbeatStallMs: 0,
|
|
49
49
|
outboundSendQueue: [],
|
|
50
|
+
outboundSendQueueBytes: 0,
|
|
51
|
+
outboundSendQueueMaxBytes: 8 * 1024 * 1024,
|
|
50
52
|
outboundSendQueueActive: false,
|
|
51
53
|
assetOutbox: null,
|
|
52
54
|
// 断连期间的消息缓冲队列(重连后 flush)
|
|
53
55
|
messageBuffer: [],
|
|
54
|
-
|
|
56
|
+
messageBufferBytes: 0,
|
|
57
|
+
messageBufferMaxSize: 5000,
|
|
58
|
+
messageBufferMaxBytes: 8 * 1024 * 1024,
|
|
55
59
|
// 由 connection.js 注册的通信函数
|
|
56
60
|
sendToServer: null,
|
|
57
61
|
// 由 index.js 注册的配置保存函数
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":"1.0.
|
|
1
|
+
{"version":"1.0.372"}
|
package/package.json
CHANGED
package/yeaft/debug-trace.js
CHANGED
|
@@ -127,9 +127,19 @@ function regexHasUnsafeQuantifiedGroup(pattern) {
|
|
|
127
127
|
function buildTraceSearchDocument(trace) {
|
|
128
128
|
const loops = Array.isArray(trace?.loops) ? trace.loops : [];
|
|
129
129
|
const tools = Array.isArray(trace?.tools) ? trace.tools : [];
|
|
130
|
-
const toolNames =
|
|
131
|
-
|
|
132
|
-
|
|
130
|
+
const toolNames = [
|
|
131
|
+
...tools.map(t => t?.toolName || t?.name || '').filter(Boolean),
|
|
132
|
+
...(Array.isArray(trace?.toolNames) ? trace.toolNames : []),
|
|
133
|
+
].join(' ');
|
|
134
|
+
const loopModels = [
|
|
135
|
+
...loops.map(l => l?.model || '').filter(Boolean),
|
|
136
|
+
...(Array.isArray(trace?.loopModels) ? trace.loopModels : []),
|
|
137
|
+
].join(' ');
|
|
138
|
+
const stopReasons = [
|
|
139
|
+
...loops.map(l => l?.stopReason || '').filter(Boolean),
|
|
140
|
+
...(Array.isArray(trace?.stopReasons) ? trace.stopReasons : []),
|
|
141
|
+
trace?.finalStopReason || '',
|
|
142
|
+
].filter(Boolean).join(' ');
|
|
133
143
|
return [
|
|
134
144
|
trace?.requestId,
|
|
135
145
|
trace?.traceId,
|
|
@@ -596,7 +606,24 @@ function turnLocatorPath(rootDir, sessionId, turnId) {
|
|
|
596
606
|
}
|
|
597
607
|
|
|
598
608
|
function serializableTraceMeta(trace) {
|
|
599
|
-
const
|
|
609
|
+
const loops = Array.isArray(trace?.loops) ? trace.loops : [];
|
|
610
|
+
const tools = Array.isArray(trace?.tools) ? trace.tools : [];
|
|
611
|
+
const usage = loops.reduce((acc, loop) => {
|
|
612
|
+
const normalized = normalizeUsage(loop?.usage || {});
|
|
613
|
+
acc.totalMs += Number(loop?.latencyMs || 0);
|
|
614
|
+
acc.totalTokens += Number(normalized.totalTokens || 0);
|
|
615
|
+
acc.summaryInputTokens += Number(normalized.totalInputTokens || 0);
|
|
616
|
+
acc.summaryOutputTokens += Number(normalized.outputTokens || 0);
|
|
617
|
+
return acc;
|
|
618
|
+
}, { totalMs: 0, totalTokens: 0, summaryInputTokens: 0, summaryOutputTokens: 0 });
|
|
619
|
+
const meta = {
|
|
620
|
+
...trace,
|
|
621
|
+
loopCount: loops.length,
|
|
622
|
+
...usage,
|
|
623
|
+
loopModels: [...new Set(loops.map(loop => loop?.model).filter(Boolean))],
|
|
624
|
+
stopReasons: [...new Set(loops.map(loop => loop?.stopReason).filter(Boolean))],
|
|
625
|
+
toolNames: [...new Set(tools.map(tool => tool?.toolName || tool?.name).filter(Boolean))],
|
|
626
|
+
};
|
|
600
627
|
delete meta._lastSnapshot;
|
|
601
628
|
delete meta._persistedFormat;
|
|
602
629
|
delete meta._persistedRequestDir;
|
|
@@ -689,11 +716,11 @@ function summarizeTrace(trace, detailsLoaded = false) {
|
|
|
689
716
|
threadId: trace?.threadId || null,
|
|
690
717
|
openedAt: trace?.openedAt || 0,
|
|
691
718
|
closedAt: trace?.closedAt || null,
|
|
692
|
-
totalMs: usage.totalMs,
|
|
693
|
-
totalTokens: usage.totalTokens,
|
|
694
|
-
summaryInputTokens: usage.summaryInputTokens,
|
|
695
|
-
summaryOutputTokens: usage.summaryOutputTokens,
|
|
696
|
-
loopCount: loops.length,
|
|
719
|
+
totalMs: loops.length > 0 ? usage.totalMs : Number(trace?.totalMs || 0),
|
|
720
|
+
totalTokens: loops.length > 0 ? usage.totalTokens : Number(trace?.totalTokens || 0),
|
|
721
|
+
summaryInputTokens: loops.length > 0 ? usage.summaryInputTokens : Number(trace?.summaryInputTokens || 0),
|
|
722
|
+
summaryOutputTokens: loops.length > 0 ? usage.summaryOutputTokens : Number(trace?.summaryOutputTokens || 0),
|
|
723
|
+
loopCount: loops.length > 0 ? loops.length : Number(trace?.loopCount || 0),
|
|
697
724
|
memoryLoaded: null,
|
|
698
725
|
memoryAdjust: null,
|
|
699
726
|
tools: Array.isArray(trace?.tools) ? trace.tools.map(t => ({
|
|
@@ -1123,33 +1150,39 @@ export class DebugTrace {
|
|
|
1123
1150
|
}
|
|
1124
1151
|
|
|
1125
1152
|
async queryByMessage(messageId) {
|
|
1126
|
-
await this.#ensureHydrated();
|
|
1127
1153
|
await this.#drainWrites();
|
|
1128
|
-
const traces = this.#
|
|
1129
|
-
.filter(
|
|
1130
|
-
.map(({ trace }) => trace);
|
|
1154
|
+
const traces = (await readTraceSummaries(this.#rootDir)).map(item => item.trace)
|
|
1155
|
+
.filter(trace => trace.messageId === messageId);
|
|
1131
1156
|
return this.#expandLegacy(traces);
|
|
1132
1157
|
}
|
|
1133
1158
|
|
|
1134
1159
|
async queryByTrace(traceId) {
|
|
1135
|
-
await this.#ensureHydrated();
|
|
1136
1160
|
await this.#drainWrites();
|
|
1137
|
-
const traces = this.#
|
|
1138
|
-
.filter(
|
|
1139
|
-
.map(({ trace }) => trace);
|
|
1161
|
+
const traces = (await readTraceSummaries(this.#rootDir)).map(item => item.trace)
|
|
1162
|
+
.filter(trace => trace.traceId === traceId || trace.requestId === traceId);
|
|
1140
1163
|
return this.#expandLegacy(traces);
|
|
1141
1164
|
}
|
|
1142
1165
|
|
|
1143
1166
|
async queryRecent(limit = 20) {
|
|
1144
|
-
await this.#ensureHydrated();
|
|
1145
1167
|
await this.#drainWrites();
|
|
1146
1168
|
const lim = Math.max(1, Math.min(MAX_HISTORY_LIMIT, Number(limit) || MAX_HISTORY_LIMIT));
|
|
1147
|
-
return this.#
|
|
1169
|
+
return (await readTraceSummaries(this.#rootDir))
|
|
1148
1170
|
.slice(-lim)
|
|
1149
1171
|
.reverse()
|
|
1150
1172
|
.flatMap(({ trace }) => traceToLegacyRows(trace));
|
|
1151
1173
|
}
|
|
1152
1174
|
|
|
1175
|
+
finalizeQuery(traceId, { sessionId = null, stopReason = 'end_turn' } = {}) {
|
|
1176
|
+
for (const trace of this.#requestCache.values()) {
|
|
1177
|
+
if (trace.traceId !== traceId || (sessionId != null && trace.sessionId !== sessionId)) continue;
|
|
1178
|
+
trace.active = false;
|
|
1179
|
+
trace.closedAt ||= Date.now();
|
|
1180
|
+
trace.updatedAt = Date.now();
|
|
1181
|
+
trace.finalStopReason = stopReason;
|
|
1182
|
+
this.#appendTraceRecord(trace, 'finalize', { at: trace.updatedAt, stopReason }, { writeMeta: true, evictAfterWrite: true });
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1153
1186
|
async fetchTurnDebug({ sessionId, turnId, dreamLimit = 0 } = {}) {
|
|
1154
1187
|
const requestedSessionId = typeof sessionId === 'string' && sessionId ? sessionId : null;
|
|
1155
1188
|
const requestedTurnId = typeof turnId === 'string' && turnId ? turnId : null;
|
|
@@ -1166,10 +1199,7 @@ export class DebugTrace {
|
|
|
1166
1199
|
const locator = await readJson(turnLocatorPath(this.#rootDir, requestedSessionId, requestedTurnId));
|
|
1167
1200
|
if (locator?.requestKey && locator.sessionId === requestedSessionId && locator.requestId === requestedTurnId) {
|
|
1168
1201
|
const located = await readRequestDir(requestDirFor(this.#rootDir, requestedSessionId, locator.requestKey));
|
|
1169
|
-
if (traceMatchesIdentity(located, requestedSessionId, requestedTurnId))
|
|
1170
|
-
trace = located;
|
|
1171
|
-
this.#requestCache.set(trace.requestKey, trace);
|
|
1172
|
-
}
|
|
1202
|
+
if (traceMatchesIdentity(located, requestedSessionId, requestedTurnId)) trace = located;
|
|
1173
1203
|
}
|
|
1174
1204
|
}
|
|
1175
1205
|
if (!trace) {
|
|
@@ -1178,7 +1208,6 @@ export class DebugTrace {
|
|
|
1178
1208
|
for (const item of await readTraceSummaries(this.#rootDir, requestedSessionId)) {
|
|
1179
1209
|
if (traceMatchesIdentity(item.trace, requestedSessionId, requestedTurnId)) {
|
|
1180
1210
|
trace = item.trace;
|
|
1181
|
-
this.#requestCache.set(trace.requestKey, trace);
|
|
1182
1211
|
break;
|
|
1183
1212
|
}
|
|
1184
1213
|
}
|
|
@@ -1195,14 +1224,13 @@ export class DebugTrace {
|
|
|
1195
1224
|
const detail = await this.fetchTurnDebug({ sessionId, turnId: requestedDetailTurnId, dreamLimit });
|
|
1196
1225
|
return { ...detail, hasMore: false, limit: detail.loops.length, indexOnly: false };
|
|
1197
1226
|
}
|
|
1198
|
-
await this.#ensureHydrated();
|
|
1199
1227
|
await this.#drainWrites();
|
|
1200
1228
|
const lim = Math.max(1, Math.min(MAX_HISTORY_LIMIT, Number(limit) || MAX_HISTORY_LIMIT));
|
|
1201
1229
|
const searchRegex = requestedDetailTurnId ? null : compileTraceSearchRegex(search);
|
|
1202
|
-
const traces = this.#
|
|
1203
|
-
.
|
|
1204
|
-
.filter(
|
|
1205
|
-
.
|
|
1230
|
+
const traces = (await readTraceHeaders(this.#rootDir, sessionId))
|
|
1231
|
+
.map(({ trace }) => trace)
|
|
1232
|
+
.filter(trace => !threadId || trace.threadId === threadId)
|
|
1233
|
+
.filter(trace => requestedDetailTurnId || traceMatchesRegex(trace, searchRegex));
|
|
1206
1234
|
const dreamEvents = this.#readDreamEvents({ sessionId, dreamLimit });
|
|
1207
1235
|
if (requestedDetailTurnId) {
|
|
1208
1236
|
const trace = traces.find(t => t.requestId === requestedDetailTurnId || t.traceId === requestedDetailTurnId);
|
|
@@ -1214,14 +1242,22 @@ export class DebugTrace {
|
|
|
1214
1242
|
if (indexOnly) {
|
|
1215
1243
|
return {
|
|
1216
1244
|
loops: [],
|
|
1217
|
-
turns: selected.map(trace =>
|
|
1245
|
+
turns: selected.map(trace => ({
|
|
1246
|
+
...summarizeTrace(trace, false),
|
|
1247
|
+
loopCount: Number(trace.loopCount || 0),
|
|
1248
|
+
tools: [],
|
|
1249
|
+
})),
|
|
1218
1250
|
dreamEvents,
|
|
1219
1251
|
hasMore: traces.length > selected.length,
|
|
1220
1252
|
limit: lim,
|
|
1221
1253
|
indexOnly: true,
|
|
1222
1254
|
};
|
|
1223
1255
|
}
|
|
1224
|
-
const
|
|
1256
|
+
const selectedKeys = new Set(selected.map(trace => trace.requestKey));
|
|
1257
|
+
const detailed = (await readTraceSummaries(this.#rootDir, sessionId))
|
|
1258
|
+
.map(({ trace }) => trace)
|
|
1259
|
+
.filter(trace => selectedKeys.has(trace.requestKey));
|
|
1260
|
+
const expanded = detailed.reduce((acc, trace) => {
|
|
1225
1261
|
const item = expandTrace(trace);
|
|
1226
1262
|
acc.loops.push(...item.loops);
|
|
1227
1263
|
acc.turns.push(...item.turns);
|
|
@@ -1324,6 +1360,7 @@ export class DebugTrace {
|
|
|
1324
1360
|
const isUsableExisting = (t) => (
|
|
1325
1361
|
t?.sessionId === normalizedSessionId
|
|
1326
1362
|
&& t?.traceId === traceId
|
|
1363
|
+
&& t.active !== false
|
|
1327
1364
|
&& !(turnNumber === 1 && (t.loops || []).some(l => l.loopNumber === 1))
|
|
1328
1365
|
);
|
|
1329
1366
|
// Cache-only: the write path must NEVER touch disk (that was the O(N^2)
|
|
@@ -1374,6 +1411,23 @@ export class DebugTrace {
|
|
|
1374
1411
|
return this.#requestCache.get(requestKey) || null;
|
|
1375
1412
|
}
|
|
1376
1413
|
|
|
1414
|
+
async resumeTrace({ sessionId, turnId } = {}) {
|
|
1415
|
+
if (!sessionId || !turnId) return false;
|
|
1416
|
+
const locator = await readJson(turnLocatorPath(this.#rootDir, sessionId, turnId));
|
|
1417
|
+
let trace = null;
|
|
1418
|
+
if (locator?.requestKey && locator.sessionId === sessionId) {
|
|
1419
|
+
trace = await readRequestDir(requestDirFor(this.#rootDir, sessionId, locator.requestKey));
|
|
1420
|
+
}
|
|
1421
|
+
if (!traceMatchesIdentity(trace, sessionId, turnId)) {
|
|
1422
|
+
trace = (await readTraceSummaries(this.#rootDir, sessionId))
|
|
1423
|
+
.map(item => item.trace)
|
|
1424
|
+
.find(item => traceMatchesIdentity(item, sessionId, turnId)) || null;
|
|
1425
|
+
}
|
|
1426
|
+
if (!trace) return false;
|
|
1427
|
+
this.#requestCache.set(trace.requestKey, trace);
|
|
1428
|
+
return true;
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1377
1431
|
#traceSummaries(sessionId = null) {
|
|
1378
1432
|
// Cache-only: #ensureHydrated() has already merged every on-disk trace
|
|
1379
1433
|
// into #requestCache exactly once, so we never touch disk here. This is
|
|
@@ -1448,7 +1502,7 @@ export class DebugTrace {
|
|
|
1448
1502
|
return tracePathFor(this.#rootDir, trace.sessionId || null, trace.requestKey);
|
|
1449
1503
|
}
|
|
1450
1504
|
|
|
1451
|
-
#appendTraceRecord(trace, type, record, { writeMeta = false } = {}) {
|
|
1505
|
+
#appendTraceRecord(trace, type, record, { writeMeta = false, evictAfterWrite = false } = {}) {
|
|
1452
1506
|
if (!this.#acceptingWrites || !trace?.requestKey || !record) return;
|
|
1453
1507
|
this.#requestCache.set(trace.requestKey, trace);
|
|
1454
1508
|
const initialize = !this.#initializedRequestKeys.has(trace.requestKey);
|
|
@@ -1459,6 +1513,7 @@ export class DebugTrace {
|
|
|
1459
1513
|
record: cloneJsonValue(record),
|
|
1460
1514
|
initialize,
|
|
1461
1515
|
writeMeta: !!writeMeta,
|
|
1516
|
+
evictAfterWrite: !!evictAfterWrite,
|
|
1462
1517
|
});
|
|
1463
1518
|
if (writeMeta) {
|
|
1464
1519
|
this.#flushPending();
|
|
@@ -1498,15 +1553,16 @@ export class DebugTrace {
|
|
|
1498
1553
|
for (const entry of entries) {
|
|
1499
1554
|
if (!this.#requestCache.has(entry.trace.requestKey)) continue;
|
|
1500
1555
|
const requestDir = requestDirFor(this.#rootDir, entry.trace.sessionId || null, entry.trace.requestKey);
|
|
1501
|
-
const batch = batches.get(requestDir) || { trace: entry.trace, initialize: false, writeMeta: false, lines: [] };
|
|
1556
|
+
const batch = batches.get(requestDir) || { trace: entry.trace, initialize: false, writeMeta: false, evictAfterWrite: false, lines: [] };
|
|
1502
1557
|
batch.trace = entry.trace;
|
|
1503
1558
|
batch.initialize ||= entry.initialize;
|
|
1504
1559
|
batch.writeMeta ||= entry.writeMeta;
|
|
1560
|
+
batch.evictAfterWrite ||= entry.evictAfterWrite;
|
|
1505
1561
|
batch.lines.push(`${JSON.stringify({ type: entry.type, record: entry.record })}\n`);
|
|
1506
1562
|
batches.set(requestDir, batch);
|
|
1507
1563
|
}
|
|
1508
1564
|
for (const [requestDir, batch] of batches) {
|
|
1509
|
-
const { trace, initialize, writeMeta } = batch;
|
|
1565
|
+
const { trace, initialize, writeMeta, evictAfterWrite } = batch;
|
|
1510
1566
|
const lines = [...batch.lines];
|
|
1511
1567
|
const legacyRequestDir = trace._persistedFormat === 'legacy'
|
|
1512
1568
|
? trace._persistedRequestDir || null
|
|
@@ -1542,6 +1598,7 @@ export class DebugTrace {
|
|
|
1542
1598
|
if (legacyRequestDir && legacyRequestDir !== requestDir) {
|
|
1543
1599
|
await removeRequestDirIfIdentityMatches(legacyRequestDir, trace);
|
|
1544
1600
|
}
|
|
1601
|
+
if (evictAfterWrite) this.#requestCache.delete(trace.requestKey);
|
|
1545
1602
|
} catch (err) {
|
|
1546
1603
|
console.warn('[Yeaft] debug trace append failed:', err?.message || err);
|
|
1547
1604
|
}
|
|
@@ -1721,6 +1778,8 @@ export class DebugTrace {
|
|
|
1721
1778
|
export class NullTrace {
|
|
1722
1779
|
startTurn() { return 'null'; }
|
|
1723
1780
|
endTurn() {}
|
|
1781
|
+
finalizeQuery() {}
|
|
1782
|
+
async resumeTrace() { return false; }
|
|
1724
1783
|
logTool() { return 'null'; }
|
|
1725
1784
|
logEvent() { return 'null'; }
|
|
1726
1785
|
event() { return 'null'; }
|
package/yeaft/engine.js
CHANGED
|
@@ -1972,12 +1972,18 @@ export class Engine {
|
|
|
1972
1972
|
* @yields {EngineEvent}
|
|
1973
1973
|
*/
|
|
1974
1974
|
async *query(params = {}) {
|
|
1975
|
+
const queryTraceId = typeof params.vpTurnId === 'string' && params.vpTurnId
|
|
1976
|
+
? params.vpTurnId : null;
|
|
1975
1977
|
let terminalEmitted = false;
|
|
1978
|
+
let terminalStopReason = 'error';
|
|
1976
1979
|
let lastTurnNumber = 0;
|
|
1977
1980
|
try {
|
|
1978
1981
|
for await (const event of this.#queryLifecycle(params)) {
|
|
1979
1982
|
if (Number.isFinite(event?.turnNumber)) lastTurnNumber = event.turnNumber;
|
|
1980
|
-
if (event?.type === 'turn_end' && event.terminal === true)
|
|
1983
|
+
if (event?.type === 'turn_end' && event.terminal === true) {
|
|
1984
|
+
terminalEmitted = true;
|
|
1985
|
+
terminalStopReason = event.stopReason || terminalStopReason;
|
|
1986
|
+
}
|
|
1981
1987
|
yield event;
|
|
1982
1988
|
}
|
|
1983
1989
|
} catch (err) {
|
|
@@ -2001,6 +2007,13 @@ export class Engine {
|
|
|
2001
2007
|
// hook must not retroactively turn the completed answer into an error.
|
|
2002
2008
|
console.warn('[Engine] post-turn maintenance failed:', err?.message || err);
|
|
2003
2009
|
}
|
|
2010
|
+
} finally {
|
|
2011
|
+
if (queryTraceId && typeof this.#trace?.finalizeQuery === 'function') {
|
|
2012
|
+
this.#trace.finalizeQuery(queryTraceId, {
|
|
2013
|
+
sessionId: params.sessionId || null,
|
|
2014
|
+
stopReason: terminalEmitted ? terminalStopReason : 'interrupted',
|
|
2015
|
+
});
|
|
2016
|
+
}
|
|
2004
2017
|
}
|
|
2005
2018
|
}
|
|
2006
2019
|
|