@yeaft/webchat-agent 0.1.823 → 0.1.825

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yeaft/webchat-agent",
3
- "version": "0.1.823",
3
+ "version": "0.1.825",
4
4
  "description": "Remote agent for Yeaft WebChat — connects worker machines to the central server",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -125,6 +125,38 @@ function truncate(str, max) {
125
125
  return str.slice(0, max) + '... [truncated]';
126
126
  }
127
127
 
128
+ function truncatedJsonSentinel(originalBytes) {
129
+ return {
130
+ __truncated: true,
131
+ originalBytes,
132
+ maxBytes: MAX_LOOP_PAYLOAD,
133
+ };
134
+ }
135
+
136
+ function truncateJsonValue(value) {
137
+ if (value == null) return value;
138
+ if (typeof value === 'string') return truncate(value, MAX_LOOP_PAYLOAD);
139
+ try {
140
+ const s = JSON.stringify(value);
141
+ if (s.length <= MAX_LOOP_PAYLOAD) return value;
142
+ return truncatedJsonSentinel(s.length);
143
+ } catch {
144
+ return null;
145
+ }
146
+ }
147
+
148
+ function boundDreamEventData(eventType, eventData) {
149
+ if (eventType !== 'dream_loop' || !eventData || typeof eventData !== 'object') return eventData;
150
+ return {
151
+ ...eventData,
152
+ systemPrompt: truncateJsonValue(eventData.systemPrompt),
153
+ messages: truncateJsonValue(eventData.messages),
154
+ response: truncateJsonValue(eventData.response),
155
+ rawRequest: truncateJsonValue(eventData.rawRequest),
156
+ rawResponse: truncateJsonValue(eventData.rawResponse),
157
+ };
158
+ }
159
+
128
160
  /**
129
161
  * DebugTrace — SQLite-backed debug trace.
130
162
  */
@@ -229,11 +261,7 @@ export class DebugTrace {
229
261
  try {
230
262
  const s = JSON.stringify(v);
231
263
  if (s.length <= MAX_LOOP_PAYLOAD) return s;
232
- return JSON.stringify({
233
- __truncated: true,
234
- originalBytes: s.length,
235
- maxBytes: MAX_LOOP_PAYLOAD,
236
- });
264
+ return JSON.stringify(truncatedJsonSentinel(s.length));
237
265
  } catch { return null; }
238
266
  };
239
267
  // For raw request/response, accept either a pre-stringified blob
@@ -304,7 +332,8 @@ export class DebugTrace {
304
332
  logEvent({ traceId, eventType, eventData = null }) {
305
333
  const id = randomUUID();
306
334
  const now = Date.now();
307
- const data = eventData != null ? JSON.stringify(eventData) : null;
335
+ const boundedData = boundDreamEventData(eventType, eventData);
336
+ const data = boundedData != null ? JSON.stringify(boundedData) : null;
308
337
  this.#prepare('insertEvent', `
309
338
  INSERT INTO trace_events (id, trace_id, event_type, event_data, created_at)
310
339
  VALUES (?, ?, ?, ?, ?)
@@ -312,6 +341,19 @@ export class DebugTrace {
312
341
  return id;
313
342
  }
314
343
 
344
+ /**
345
+ * Compatibility helper used by older engine/dream call sites.
346
+ * @param {string} eventType
347
+ * @param {unknown} eventData
348
+ * @returns {string}
349
+ */
350
+ event(eventType, eventData = null) {
351
+ const traceId = (eventData && typeof eventData === 'object' && (eventData.turnId || eventData.runId))
352
+ ? String(eventData.turnId || eventData.runId)
353
+ : String(eventType || 'event');
354
+ return this.logEvent({ traceId, eventType, eventData });
355
+ }
356
+
315
357
  // ─── Read API ────────────────────────────────────────────────
316
358
 
317
359
  /**
@@ -477,7 +519,9 @@ export class DebugTrace {
477
519
  const dreamEvents = [];
478
520
  if (dreamLim > 0) {
479
521
  const eventRows = this.#db.prepare(`
480
- SELECT * FROM trace_events WHERE event_type = 'dream_progress' ORDER BY created_at DESC LIMIT ?
522
+ SELECT * FROM trace_events
523
+ WHERE event_type IN ('dream_progress', 'dream_loop', 'dream_turn_open', 'dream_turn_close', 'dream_run')
524
+ ORDER BY created_at DESC, rowid DESC LIMIT ?
481
525
  `).all(Math.max(dreamLim * 5, dreamLim));
482
526
  for (const er of eventRows) {
483
527
  const data = parseJsonSafe(er.event_data) || {};
@@ -488,7 +532,12 @@ export class DebugTrace {
488
532
  const isThisGroup = evtGroupId === groupId || target === `group/${groupId}`;
489
533
  if (!isBroadcast && !isThisGroup) continue;
490
534
  }
491
- dreamEvents.push({ type: 'dream_progress', ...data, at: er.created_at, ts: data.ts || er.created_at });
535
+ dreamEvents.push({
536
+ type: data.type || (er.event_type === 'dream_progress' ? 'dream_progress' : er.event_type),
537
+ ...data,
538
+ at: er.created_at,
539
+ ts: data.ts || data.at || er.created_at,
540
+ });
492
541
  if (dreamEvents.length >= dreamLim) break;
493
542
  }
494
543
  dreamEvents.reverse();
@@ -638,6 +687,7 @@ export class NullTrace {
638
687
  endTurn() {}
639
688
  logTool() { return 'null'; }
640
689
  logEvent() { return 'null'; }
690
+ event() { return 'null'; }
641
691
  queryByMessage() { return { turns: [], tools: [], events: [] }; }
642
692
  queryByTrace() { return { turns: [], tools: [], events: [] }; }
643
693
  queryRecent() { return []; }
@@ -128,29 +128,128 @@ function makeLlm(session) {
128
128
  });
129
129
 
130
130
  // Emit complete loop event (request + response) to the debug panel.
131
+ const latencyMs = Date.now() - startedMs;
132
+ const usage = normalizeDreamUsage(r?.usage || {});
133
+ if (!session._dreamMetrics) session._dreamMetrics = createDreamMetrics({ turnId });
134
+ session._dreamMetrics.llmCallCount += 1;
135
+ session._dreamMetrics.inputTokens += usage.inputTokens;
136
+ session._dreamMetrics.outputTokens += usage.outputTokens;
137
+ session._dreamMetrics.totalTokens += usage.totalTokens;
138
+ session._dreamMetrics.byPass[pass] = session._dreamMetrics.byPass[pass] || createDreamPassMetrics();
139
+ session._dreamMetrics.byPass[pass].llmCallCount += 1;
140
+ session._dreamMetrics.byPass[pass].inputTokens += usage.inputTokens;
141
+ session._dreamMetrics.byPass[pass].outputTokens += usage.outputTokens;
142
+ session._dreamMetrics.byPass[pass].totalTokens += usage.totalTokens;
143
+ session._dreamMetrics.byPass[pass].durationMs += latencyMs;
144
+ const loopEvent = stampDreamScope(session, {
145
+ type: 'loop',
146
+ turnId,
147
+ loopNumber,
148
+ pass,
149
+ model: model || 'unknown',
150
+ systemPrompt: effectiveSystem,
151
+ messages: [{ role: 'user', content: prompt }],
152
+ response: typeof r?.text === 'string' ? r.text : '',
153
+ toolCalls: [],
154
+ usage,
155
+ latencyMs,
156
+ ttfbMs: null,
157
+ stopReason: 'end_turn',
158
+ rawRequest: null,
159
+ rawResponse: null,
160
+ });
161
+ persistDreamTrace(session, 'dream_loop', loopEvent);
131
162
  if (typeof session._dreamProgressSink === 'function') {
132
- session._dreamProgressSink({
133
- type: 'loop',
134
- turnId,
135
- loopNumber,
136
- model: model || 'unknown',
137
- systemPrompt: effectiveSystem,
138
- messages: [{ role: 'user', content: prompt }],
139
- response: typeof r?.text === 'string' ? r.text : '',
140
- toolCalls: [],
141
- usage: r?.usage || { inputTokens: 0, outputTokens: 0, totalTokens: 0 },
142
- latencyMs: Date.now() - startedMs,
143
- ttfbMs: null,
144
- stopReason: 'end_turn',
145
- rawRequest: null,
146
- rawResponse: null,
147
- });
163
+ session._dreamProgressSink(loopEvent);
148
164
  }
149
165
 
150
166
  return (r && r.text) ? r.text : '';
151
167
  };
152
168
  }
153
169
 
170
+
171
+ function stampDreamScope(session, evt) {
172
+ if (!evt || typeof evt !== 'object') return evt;
173
+ if (evt.groupId || !session?._dreamActiveGroupId) return evt;
174
+ return { ...evt, groupId: session._dreamActiveGroupId };
175
+ }
176
+
177
+ function finiteNumber(v) {
178
+ const n = Number(v);
179
+ return Number.isFinite(n) && n > 0 ? Math.floor(n) : 0;
180
+ }
181
+
182
+ function normalizeDreamUsage(usage = {}) {
183
+ const inputTokens = finiteNumber(
184
+ usage.inputTokens ?? usage.input_tokens ?? usage.promptTokens ?? usage.prompt_tokens,
185
+ );
186
+ const outputTokens = finiteNumber(
187
+ usage.outputTokens ?? usage.output_tokens ?? usage.completionTokens ?? usage.completion_tokens,
188
+ );
189
+ const explicitTotal = finiteNumber(usage.totalTokens ?? usage.total_tokens);
190
+ return {
191
+ inputTokens,
192
+ outputTokens,
193
+ totalTokens: explicitTotal || inputTokens + outputTokens,
194
+ };
195
+ }
196
+
197
+ function createDreamPassMetrics() {
198
+ return { llmCallCount: 0, inputTokens: 0, outputTokens: 0, totalTokens: 0, durationMs: 0 };
199
+ }
200
+
201
+ function createDreamMetrics({ turnId, startedAt = Date.now() } = {}) {
202
+ return {
203
+ turnId: turnId || 'dream',
204
+ startedAt,
205
+ durationMs: 0,
206
+ llmCallCount: 0,
207
+ inputTokens: 0,
208
+ outputTokens: 0,
209
+ totalTokens: 0,
210
+ byPass: {},
211
+ };
212
+ }
213
+
214
+ function finalizeDreamMetrics(metrics, durationMs) {
215
+ const m = metrics || createDreamMetrics();
216
+ return {
217
+ turnId: m.turnId || 'dream',
218
+ startedAt: m.startedAt || null,
219
+ durationMs: finiteNumber(durationMs),
220
+ llmCallCount: finiteNumber(m.llmCallCount),
221
+ inputTokens: finiteNumber(m.inputTokens),
222
+ outputTokens: finiteNumber(m.outputTokens),
223
+ totalTokens: finiteNumber(m.totalTokens),
224
+ passBreakdown: Object.fromEntries(Object.entries(m.byPass || {}).map(([pass, rec]) => [pass, {
225
+ llmCallCount: finiteNumber(rec.llmCallCount),
226
+ inputTokens: finiteNumber(rec.inputTokens),
227
+ outputTokens: finiteNumber(rec.outputTokens),
228
+ totalTokens: finiteNumber(rec.totalTokens),
229
+ durationMs: finiteNumber(rec.durationMs),
230
+ }])),
231
+ };
232
+ }
233
+
234
+ function summarizeDreamResult(result = {}) {
235
+ return {
236
+ groups: Array.isArray(result.groups) ? result.groups.length : 0,
237
+ targets: Array.isArray(result.targets) ? result.targets.length : 0,
238
+ error: result.error || null,
239
+ skipped: !!result.skipped,
240
+ skippedReason: result.skippedReason || null,
241
+ };
242
+ }
243
+
244
+ function persistDreamTrace(session, eventType, eventData) {
245
+ try {
246
+ if (typeof session?.trace?.event === 'function') session.trace.event(eventType, eventData);
247
+ else if (typeof session?.trace?.logEvent === 'function') {
248
+ session.trace.logEvent({ traceId: String(eventData?.turnId || eventData?.runId || eventType), eventType, eventData });
249
+ }
250
+ } catch { /* trace must never break dream */ }
251
+ }
252
+
154
253
  /**
155
254
  * Create a v2 dream scheduler bound to a session and wire its progress
156
255
  * events into the engine's sub-agent event sink (web-bridge translates
@@ -162,20 +261,21 @@ function makeLlm(session) {
162
261
  export function createV2DreamScheduler(session) {
163
262
  const onProgress = (evt) => {
164
263
  try {
264
+ const stampedEvt = stampDreamScope(session, evt);
165
265
  const sink = session.engine?.subAgentEventSink || null;
166
266
  // We don't have a sub-agent id; emit on the engine's standard
167
267
  // event channel instead. session.engine exposes setSubAgentEventSink
168
268
  // for nested events; for top-level dream, we route through trace.
169
269
  if (typeof session.trace?.event === 'function') {
170
- session.trace.event('dream_progress', evt);
270
+ session.trace.event('dream_progress', stampedEvt);
171
271
  }
172
272
  if (typeof session._dreamProgressSink === 'function') {
173
- session._dreamProgressSink(evt);
273
+ session._dreamProgressSink(stampedEvt);
174
274
  }
175
275
  // Best-effort console for debug builds.
176
276
  if (session.config?.debug) {
177
277
  // eslint-disable-next-line no-console
178
- console.log('[dream-v2]', evt);
278
+ console.log('[dream-v2]', stampedEvt);
179
279
  }
180
280
  } catch { /* never let progress reporting kill the run */ }
181
281
  };
@@ -190,16 +290,19 @@ export function createV2DreamScheduler(session) {
190
290
  const startedAt = Date.now();
191
291
  session._dreamTurnId = turnId;
192
292
  session._dreamLoopCounter = 0;
293
+ session._dreamMetrics = createDreamMetrics({ turnId, startedAt });
193
294
 
295
+ const turnOpen = stampDreamScope(session, {
296
+ type: 'turn_open',
297
+ turnId,
298
+ userPrompt: '[dream] automatic memory consolidation',
299
+ vpId: null,
300
+ groupId: null,
301
+ at: startedAt,
302
+ });
303
+ persistDreamTrace(session, 'dream_turn_open', turnOpen);
194
304
  if (typeof session._dreamProgressSink === 'function') {
195
- session._dreamProgressSink({
196
- type: 'turn_open',
197
- turnId,
198
- userPrompt: '[dream] automatic memory consolidation',
199
- vpId: null,
200
- groupId: null,
201
- at: startedAt,
202
- });
305
+ session._dreamProgressSink(turnOpen);
203
306
  }
204
307
 
205
308
  return runDream({
@@ -208,14 +311,35 @@ export function createV2DreamScheduler(session) {
208
311
  scopeFilter: Array.isArray(opts.scopeFilter) ? opts.scopeFilter : undefined,
209
312
  }).then((result) => {
210
313
  // Bug 2: emit turn_close when the dream pass completes.
314
+ result.trigger = opts.manual ? 'manual' : 'auto';
315
+ if (session._dreamActiveGroupId && !result.groupId) result.groupId = session._dreamActiveGroupId;
316
+ const metrics = finalizeDreamMetrics(session._dreamMetrics, Date.now() - startedAt);
317
+ result.metrics = metrics;
318
+ result.durationMs = metrics.durationMs;
319
+ result.llmCallCount = metrics.llmCallCount;
320
+ result.inputTokens = metrics.inputTokens;
321
+ result.outputTokens = metrics.outputTokens;
322
+ result.totalTokens = metrics.totalTokens;
323
+ result.passBreakdown = metrics.passBreakdown;
324
+ const turnClose = stampDreamScope(session, {
325
+ type: 'turn_close',
326
+ turnId,
327
+ totalMs: metrics.durationMs,
328
+ totalTokens: metrics.totalTokens,
329
+ loopCount: metrics.llmCallCount,
330
+ metrics,
331
+ });
332
+ persistDreamTrace(session, 'dream_turn_close', turnClose);
333
+ persistDreamTrace(session, 'dream_run', stampDreamScope(session, {
334
+ type: 'dream_run',
335
+ turnId,
336
+ phase: 'result',
337
+ status: result?.error ? 'error' : 'done',
338
+ metrics,
339
+ resultSummary: summarizeDreamResult(result),
340
+ }));
211
341
  if (typeof session._dreamProgressSink === 'function') {
212
- session._dreamProgressSink({
213
- type: 'turn_close',
214
- turnId,
215
- totalMs: Date.now() - startedAt,
216
- totalTokens: 0,
217
- loopCount: session._dreamLoopCounter || 0,
218
- });
342
+ session._dreamProgressSink(turnClose);
219
343
  }
220
344
  return result;
221
345
  });
@@ -3222,6 +3222,13 @@ export function normalizeDreamResult(result) {
3222
3222
 
3223
3223
  return {
3224
3224
  success,
3225
+ durationMs: Number.isFinite(Number(result?.durationMs)) ? Number(result.durationMs) : 0,
3226
+ llmCallCount: Number.isFinite(Number(result?.llmCallCount)) ? Number(result.llmCallCount) : 0,
3227
+ inputTokens: Number.isFinite(Number(result?.inputTokens)) ? Number(result.inputTokens) : 0,
3228
+ outputTokens: Number.isFinite(Number(result?.outputTokens)) ? Number(result.outputTokens) : 0,
3229
+ totalTokens: Number.isFinite(Number(result?.totalTokens)) ? Number(result.totalTokens) : 0,
3230
+ metrics: result?.metrics || null,
3231
+ passBreakdown: result?.passBreakdown || result?.metrics?.passBreakdown || null,
3225
3232
  skipped,
3226
3233
  skippedReason,
3227
3234
  groupsProcessed,
@@ -3260,13 +3267,15 @@ export async function handleUnifyDreamTrigger(msg = {}) {
3260
3267
  // group or different) overlapping the same inflight pass used to set
3261
3268
  // the module-level groupId slot, race the sink wrapping, and let the
3262
3269
  // second `finally` restore the original sink while the first run was
3263
- // still emitting events. We now refuse any second scoped trigger
3264
- // while ANY scoped pass is inflight the scheduler already
3265
- // short-circuits the underlying run for same-group, and a different
3266
- // group's filter would have been silently dropped anyway (see
3267
- // dream-v2/schedule.js inflight reuse), so the user-facing semantics
3268
- // are unchanged ("you already asked").
3269
- if (groupId && inflightScopedDreamGroups.size > 0) {
3270
+ // still emitting events. We now refuse scoped triggers while ANY dream
3271
+ // pass is already running: a scoped manual click during an unscoped
3272
+ // auto run must not install `_dreamActiveGroupId` or wrap the sink,
3273
+ // otherwise auto-run events can be persisted under the clicked group.
3274
+ // The scheduler also short-circuits the underlying run for same-group,
3275
+ // and a different group's filter would have been silently dropped
3276
+ // anyway (see dream-v2/schedule.js inflight reuse), so the user-facing
3277
+ // semantics are unchanged ("you already asked").
3278
+ if (groupId && (inflightScopedDreamGroups.size > 0 || session.dreamScheduler.isRunning)) {
3270
3279
  const skippedResult = {
3271
3280
  skipped: true,
3272
3281
  skippedReason: 'already-running',
@@ -3289,6 +3298,7 @@ export async function handleUnifyDreamTrigger(msg = {}) {
3289
3298
  // OTHER groupIds chain (last-installed wins) but each restoration
3290
3299
  // unwinds back to its predecessor.
3291
3300
  const originalSink = session?._dreamProgressSink;
3301
+ if (groupId) session._dreamActiveGroupId = groupId;
3292
3302
  if (groupId && typeof originalSink === 'function') {
3293
3303
  inflightScopedDreamGroups.add(groupId);
3294
3304
  session._dreamProgressSink = (evt) => {
@@ -3345,6 +3355,7 @@ export async function handleUnifyDreamTrigger(msg = {}) {
3345
3355
  });
3346
3356
  } finally {
3347
3357
  // Restore the original sink and release the per-group inflight lock.
3358
+ if (groupId && session?._dreamActiveGroupId === groupId) session._dreamActiveGroupId = null;
3348
3359
  if (groupId && typeof originalSink === 'function') {
3349
3360
  session._dreamProgressSink = originalSink;
3350
3361
  inflightScopedDreamGroups.delete(groupId);