@yeaft/webchat-agent 0.1.820 → 0.1.821

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.820",
3
+ "version": "0.1.821",
4
4
  "description": "Remote agent for Yeaft WebChat — connects worker machines to the central server",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -107,6 +107,12 @@ function serializeMessage(msg) {
107
107
  // the speaker so the UI can render the message on the correct VP track.
108
108
  // For real user messages this is unset.
109
109
  if (msg.speakerVpId) fm.push(`speakerVpId: ${msg.speakerVpId}`);
110
+ if (Array.isArray(msg.attachments) && msg.attachments.length > 0) {
111
+ try {
112
+ const b64 = Buffer.from(JSON.stringify(msg.attachments)).toString('base64');
113
+ fm.push(`attachmentsB64: ${b64}`);
114
+ } catch { /* best-effort: attachments are UI metadata, not engine-critical */ }
115
+ }
110
116
  // Internal/synthetic rows must round-trip so refresh/history replay can
111
117
  // keep them out of the user-visible conversation. Reflection folding uses
112
118
  // `_reflection`; other engine-only rows may use one of the explicit flags.
@@ -213,6 +219,12 @@ export function parseMessage(raw) {
213
219
  case 'sourceThreadId': msg.sourceThreadId = value; break;
214
220
  case 'groupId': msg.groupId = value; break;
215
221
  case 'speakerVpId': msg.speakerVpId = value; break;
222
+ case 'attachmentsB64':
223
+ try {
224
+ const parsed = JSON.parse(Buffer.from(value, 'base64').toString('utf8'));
225
+ if (Array.isArray(parsed)) msg.attachments = parsed;
226
+ } catch { /* best-effort: ignore malformed attachment metadata */ }
227
+ break;
216
228
  case '_reflection': msg._reflection = value === 'true'; break;
217
229
  case 'internal': msg.internal = value === 'true'; break;
218
230
  case 'systemOnly': msg.systemOnly = value === 'true'; break;
@@ -364,11 +364,14 @@ export class DebugTrace {
364
364
  * fields the panel expects. JSON columns are parsed; truncated /
365
365
  * malformed payloads degrade to null instead of failing the call.
366
366
  *
367
- * @param {{ limit?: number, groupId?: string|null, threadId?: string|null }} [opts]
368
- * @returns {{ loops: object[], turns: object[] }}
367
+ * @param {{ limit?: number, dreamLimit?: number, groupId?: string|null, threadId?: string|null }} [opts]
368
+ * @returns {{ loops: object[], turns: object[], dreamEvents: object[] }}
369
369
  */
370
- fetchRecentDebugHistory({ limit = 100, groupId = null, threadId = null } = {}) {
370
+ fetchRecentDebugHistory({ limit = 100, dreamLimit = 5, groupId = null, threadId = null } = {}) {
371
371
  const lim = Math.max(1, Math.min(500, Number(limit) || 100));
372
+ const dreamLim = Number.isFinite(Number(dreamLimit))
373
+ ? Math.max(0, Math.min(50, Number(dreamLimit)))
374
+ : 5;
372
375
  const where = [];
373
376
  const args = [];
374
377
  if (groupId) { where.push('group_id = ?'); args.push(groupId); }
@@ -471,10 +474,30 @@ export class DebugTrace {
471
474
  isError: !!tool.is_error,
472
475
  });
473
476
  }
477
+ const dreamEvents = [];
478
+ if (dreamLim > 0) {
479
+ const eventRows = this.#db.prepare(`
480
+ SELECT * FROM trace_events WHERE event_type = 'dream_progress' ORDER BY created_at DESC LIMIT ?
481
+ `).all(Math.max(dreamLim * 5, dreamLim));
482
+ for (const er of eventRows) {
483
+ const data = parseJsonSafe(er.event_data) || {};
484
+ const evtGroupId = typeof data.groupId === 'string' && data.groupId ? data.groupId : null;
485
+ const target = typeof data.target === 'string' ? data.target : '';
486
+ if (groupId) {
487
+ const isBroadcast = !evtGroupId && !target;
488
+ const isThisGroup = evtGroupId === groupId || target === `group/${groupId}`;
489
+ if (!isBroadcast && !isThisGroup) continue;
490
+ }
491
+ dreamEvents.push({ type: 'dream_progress', ...data, at: er.created_at, ts: data.ts || er.created_at });
492
+ if (dreamEvents.length >= dreamLim) break;
493
+ }
494
+ dreamEvents.reverse();
495
+ }
496
+
474
497
  // Reverse to oldest-first so the panel's existing append-driven UI
475
498
  // renders in chronological order on hydration.
476
499
  loops.reverse();
477
- return { loops, turns: Array.from(turnsById.values()) };
500
+ return { loops, turns: Array.from(turnsById.values()), dreamEvents };
478
501
  }
479
502
 
480
503
  /**
@@ -624,7 +647,7 @@ export class NullTrace {
624
647
  cleanup() { return { deletedTurns: 0, deletedTools: 0, deletedEvents: 0 }; }
625
648
  purge() {}
626
649
  close() {}
627
- fetchRecentDebugHistory() { return { loops: [], turns: [] }; }
650
+ fetchRecentDebugHistory() { return { loops: [], turns: [], dreamEvents: [] }; }
628
651
  }
629
652
 
630
653
  /**
@@ -497,6 +497,56 @@ function makeGroupContextStub() {
497
497
  };
498
498
  }
499
499
 
500
+ /**
501
+ * Parse persisted content that may have been stringified from provider content
502
+ * blocks, then return only user-visible text. Image/file binary blocks are UI
503
+ * metadata and must never be rendered as bubble text; attachment chips ride in
504
+ * `attachments` instead.
505
+ *
506
+ * @param {unknown} content
507
+ * @returns {string}
508
+ */
509
+ export function __testNormalizePersistedVisibleContent(content) {
510
+ let value = content;
511
+ if (typeof value === 'string') {
512
+ const trimmed = value.trim();
513
+ if ((trimmed.startsWith('[') && trimmed.endsWith(']')) || (trimmed.startsWith('{') && trimmed.endsWith('}'))) {
514
+ try { value = JSON.parse(trimmed); } catch { value = content; }
515
+ }
516
+ }
517
+
518
+ if (Array.isArray(value)) {
519
+ return value
520
+ .map((part) => {
521
+ if (typeof part === 'string') return part;
522
+ if (!part || typeof part !== 'object') return '';
523
+ if (part.type === 'text' && typeof part.text === 'string') return part.text;
524
+ if (part.type === 'input_text' && typeof part.text === 'string') return part.text;
525
+ return '';
526
+ })
527
+ .join('')
528
+ .replace(/\n\n\[Uploaded files\][\s\S]*$/m, '')
529
+ .trim();
530
+ }
531
+
532
+ if (value && typeof value === 'object') {
533
+ if (typeof value.text === 'string') return value.text.trim();
534
+ if (typeof value.content === 'string') return value.content.trim();
535
+ return '';
536
+ }
537
+
538
+ return typeof value === 'string'
539
+ ? value.replace(/\n\n\[Uploaded files\][\s\S]*$/m, '').trim()
540
+ : '';
541
+ }
542
+
543
+ function isPersistedInternalMessage(m) {
544
+ if (!m) return true;
545
+ if (m._reflection || m.internal || m.systemOnly || m.systemOnlyMessage) return true;
546
+ if (m.kind === 'compact_summary' || m._compactSummary) return true;
547
+ return false;
548
+ }
549
+
500
550
  /**
501
551
  * Project a persisted message record into the in-memory history shape.
502
552
  * Accepts `role:'tool'` and preserves `toolCalls`/`toolCallId` so the
@@ -509,8 +559,8 @@ function makeGroupContextStub() {
509
559
  function projectPersistedToHistoryEntry(m) {
510
560
  if (!m) return null;
511
561
  if (m.role !== 'user' && m.role !== 'assistant' && m.role !== 'tool') return null;
512
- if (m._reflection || m.internal || m.systemOnly || m.systemOnlyMessage) return null;
513
- const entry = { role: m.role, content: m.content };
562
+ if (isPersistedInternalMessage(m)) return null;
563
+ const entry = { role: m.role, content: m.role === 'tool' ? m.content : __testNormalizePersistedVisibleContent(m.content) };
514
564
  if (m.id) entry.id = m.id;
515
565
  if (m.groupId) entry.groupId = m.groupId;
516
566
  if (m.speakerVpId) entry.speakerVpId = m.speakerVpId;
@@ -523,6 +573,9 @@ function projectPersistedToHistoryEntry(m) {
523
573
  }));
524
574
  }
525
575
  if (m.isError) entry.isError = true;
576
+ if (m.ts) entry.ts = m.ts;
577
+ if (Array.isArray(m.attachments) && m.attachments.length > 0) entry.attachments = m.attachments;
578
+ if ((entry.role === 'user' || entry.role === 'assistant') && !entry.content && !entry.attachments) return null;
526
579
  return entry;
527
580
  }
528
581
 
@@ -902,6 +955,7 @@ async function routeEnvelopeToVpThread(groupId, vpId, envelope) {
902
955
  threadId: thread.threadId,
903
956
  role: envelope?.msg?.meta?.injectedBy === 'route_forward' ? 'assistant' : 'user',
904
957
  speakerVpId: envelope?.msg?.meta?.senderVpId || envelope?.msg?.from || null,
958
+ attachments: Array.isArray(envelope?.msg?.meta?.attachments) ? envelope.msg.meta.attachments : [],
905
959
  });
906
960
  thread.updatedAt = Date.now();
907
961
  try {
@@ -987,6 +1041,7 @@ function ensureDriverRunning(groupId, vpId, threadId = 'main') {
987
1041
  threadId: thread.threadId,
988
1042
  role: isForward ? 'assistant' : 'user',
989
1043
  speakerVpId: senderVpId,
1044
+ attachments: Array.isArray(meta.attachments) ? meta.attachments : [],
990
1045
  });
991
1046
  }
992
1047
  } catch { /* never crash WS pipeline */ }
@@ -2857,17 +2912,17 @@ function appendTurnToGroupHistory(groupId, threadId, prompts, assistantTextParts
2857
2912
  * Best-effort: a write failure does NOT abort the turn — engines can
2858
2913
  * still run, and the next user message will trigger another append.
2859
2914
  *
2860
- * Note: we mirror `engine.#persistMessages`'s schema for the user row
2861
- * exactly (role/content/threadId/groupId), so existing parsers /
2862
- * loaders see no schema drift. Attachments are not part of the on-disk
2863
- * schema today (the engine never wrote them either) they live on the
2864
- * coordinator's jsonl-log under group meta.
2915
+ * Note: we mirror `engine.#persistMessages`'s core user-row fields
2916
+ * (role/content/threadId/groupId) so existing parsers keep working.
2917
+ * Attachment UI metadata is persisted separately (without base64) so
2918
+ * refresh replay can render chips without leaking image source data into
2919
+ * the message body.
2865
2920
  *
2866
- * @param {{ msgId:string, text:string, groupId:string, role?:string, speakerVpId?:string|null }} args
2921
+ * @param {{ msgId:string, text:string, groupId:string, role?:string, speakerVpId?:string|null, attachments?:Array<object> }} args
2867
2922
  * @returns {boolean} true if this call wrote the row, false if a prior
2868
2923
  * call already wrote it (dedup hit).
2869
2924
  */
2870
- function persistInboundMessageOnceByMsgId({ msgId, text, groupId, threadId = 'main', role, speakerVpId }) {
2925
+ function persistInboundMessageOnceByMsgId({ msgId, text, groupId, threadId = 'main', role, speakerVpId, attachments }) {
2871
2926
  if (!session?.conversationStore) return false;
2872
2927
  // No msgId means no dedup key — caller is responsible for guarding.
2873
2928
  // Both call sites already do (`if (envMsgId && text)` and
@@ -2918,6 +2973,9 @@ function persistInboundMessageOnceByMsgId({ msgId, text, groupId, threadId = 'ma
2918
2973
  if (persistRole === 'assistant' && speakerVpId && typeof speakerVpId === 'string') {
2919
2974
  record.speakerVpId = speakerVpId;
2920
2975
  }
2976
+ if (persistRole === 'user' && Array.isArray(attachments) && attachments.length > 0) {
2977
+ record.attachments = attachments;
2978
+ }
2921
2979
  session.conversationStore.append(record);
2922
2980
  return true;
2923
2981
  } catch (err) {
@@ -3357,21 +3415,25 @@ export async function handleUnifyFetchToolStats(_msg = {}) {
3357
3415
  */
3358
3416
  export async function handleUnifyFetchDebugHistory(msg = {}) {
3359
3417
  const limit = Number.isFinite(msg?.limit) ? Number(msg.limit) : 100;
3418
+ const dreamLimit = Number.isFinite(msg?.dreamLimit) ? Number(msg.dreamLimit) : 5;
3360
3419
  const groupId = typeof msg?.groupId === 'string' && msg.groupId ? msg.groupId : null;
3361
3420
  const threadId = typeof msg?.threadId === 'string' && msg.threadId ? msg.threadId : null;
3362
3421
  let loops = [];
3363
3422
  let turns = [];
3423
+ let dreamEvents = [];
3364
3424
  try {
3365
3425
  if (session?.trace && typeof session.trace.fetchRecentDebugHistory === 'function') {
3366
- const out = session.trace.fetchRecentDebugHistory({ limit, groupId, threadId });
3426
+ const out = session.trace.fetchRecentDebugHistory({ limit, dreamLimit, groupId, threadId });
3367
3427
  loops = Array.isArray(out?.loops) ? out.loops : [];
3368
3428
  turns = Array.isArray(out?.turns) ? out.turns : [];
3429
+ dreamEvents = Array.isArray(out?.dreamEvents) ? out.dreamEvents : [];
3369
3430
  }
3370
3431
  } catch (err) {
3371
3432
  sendToServer({
3372
3433
  type: 'unify_debug_history',
3373
3434
  loops: [],
3374
3435
  turns: [],
3436
+ dreamEvents: [],
3375
3437
  error: err && err.message ? err.message : String(err),
3376
3438
  });
3377
3439
  return;
@@ -3380,6 +3442,7 @@ export async function handleUnifyFetchDebugHistory(msg = {}) {
3380
3442
  type: 'unify_debug_history',
3381
3443
  loops,
3382
3444
  turns,
3445
+ dreamEvents,
3383
3446
  groupId,
3384
3447
  threadId,
3385
3448
  });
@@ -3493,7 +3556,15 @@ export async function handleUnifyLoadHistory(msg) {
3493
3556
 
3494
3557
  for (const entry of replayEntries) {
3495
3558
  if (entry.role === 'user') {
3496
- sendUnifyOutput({ type: 'user', message: { content: entry.content, id: entry.id || null }, ts: entry.ts || null }, { groupId: entry.groupId || null });
3559
+ sendUnifyOutput({
3560
+ type: 'user',
3561
+ message: {
3562
+ content: entry.content,
3563
+ id: entry.id || null,
3564
+ ...(Array.isArray(entry.attachments) && entry.attachments.length > 0 ? { attachments: entry.attachments } : {}),
3565
+ },
3566
+ ts: entry.ts || null,
3567
+ }, { groupId: entry.groupId || null });
3497
3568
  } else if (entry.role === 'assistant') {
3498
3569
  // speakerVpId rides on the envelope so the frontend can route this
3499
3570
  // replayed assistant text to the correct VP track. Without it, the
@@ -3584,6 +3655,7 @@ export async function handleUnifyLoadMoreHistory(msg) {
3584
3655
  role: m.role,
3585
3656
  content: m.content,
3586
3657
  groupId: m.groupId || null,
3658
+ ...(Array.isArray(m.attachments) && m.attachments.length > 0 ? { attachments: m.attachments } : {}),
3587
3659
  ...(m.speakerVpId ? { speakerVpId: m.speakerVpId } : {}),
3588
3660
  }));
3589
3661