@yeaft/webchat-agent 1.0.39 → 1.0.40

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": "1.0.39",
3
+ "version": "1.0.40",
4
4
  "description": "Remote worker agent for Yeaft Web Code Agent — connects the native Yeaft engine, CLI providers, and workbench tools",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -206,6 +206,7 @@ function serializeMessage(msg) {
206
206
  // routing can filter/replay by thread without rescanning JSON blobs.
207
207
  // Defaults to 'main' for legacy messages (see migrate-messages-threadid.js).
208
208
  fm.push(`threadId: ${msg.threadId || 'main'}`);
209
+ if (msg.turnId) fm.push(`turnId: ${msg.turnId}`);
209
210
  // task-313: when a thread is merged into another, the messages keep
210
211
  // their original thread id in `sourceThreadId` so the UI can still
211
212
  // render a small "#source" pill next to each bubble.
@@ -216,6 +217,7 @@ function serializeMessage(msg) {
216
217
  // shows an empty pane.
217
218
  if (msg.sessionId) fm.push(`sessionId: ${msg.sessionId}`);
218
219
  if (msg.chatId) fm.push(`chatId: ${msg.chatId}`);
220
+ if (msg.clientMessageId) fm.push(`clientMessageId: ${msg.clientMessageId}`);
219
221
  // Session attribution: when a VP authors an assistant turn (either
220
222
  // its own reply or a route_forward injection from another VP), stamp
221
223
  // the speaker so the UI can render the message on the correct VP track.
@@ -333,9 +335,11 @@ export function parseMessage(raw) {
333
335
  case 'isError': msg.isError = value === 'true'; break;
334
336
  case 'tokens_est': msg.tokens_est = parseInt(value, 10); break;
335
337
  case 'threadId': msg.threadId = value; break;
338
+ case 'turnId': msg.turnId = value; break;
336
339
  case 'sourceThreadId': msg.sourceThreadId = value; break;
337
340
  case 'sessionId': msg.sessionId = value; break;
338
341
  case 'chatId': msg.chatId = value; break;
342
+ case 'clientMessageId': msg.clientMessageId = value; break;
339
343
  case 'speakerVpId': msg.speakerVpId = value; break;
340
344
  case 'attachmentsB64':
341
345
  try {
package/yeaft/engine.js CHANGED
@@ -1597,7 +1597,7 @@ export class Engine {
1597
1597
  * string-prompt shape (no regression for existing callers).
1598
1598
  * @yields {EngineEvent}
1599
1599
  */
1600
- async *query({ prompt, promptParts = null, messages = [], signal, userEffort = null, scenario = 'chat', vpPersona, router, senderVpId, inboundEnvelope, taskId, taskMembers, sessionId, sessionMembers, sessionTopics = null, vpPlan, sessionAnnouncement, workDir, userAlreadyPersisted = false, getCurrentTodos = null, setCurrentTodos = null, threadId = MAIN_THREAD_ID, drainPendingUserMessages = null, collabToolPolicy = null } = {}) {
1600
+ async *query({ prompt, promptParts = null, messages = [], signal, userEffort = null, scenario = 'chat', vpPersona, router, senderVpId, inboundEnvelope, taskId, taskMembers, sessionId, sessionMembers, sessionTopics = null, vpPlan, sessionAnnouncement, workDir, userAlreadyPersisted = false, getCurrentTodos = null, setCurrentTodos = null, threadId = MAIN_THREAD_ID, vpTurnId = null, drainPendingUserMessages = null, collabToolPolicy = null } = {}) {
1601
1601
  if (!prompt || typeof prompt !== 'string' || !prompt.trim()) {
1602
1602
  yield {
1603
1603
  type: 'error',
@@ -2833,6 +2833,7 @@ export class Engine {
2833
2833
  // history replay can re-stamp them on reload.
2834
2834
  sessionId,
2835
2835
  threadId,
2836
+ turnId: vpTurnId || queryTurnId,
2836
2837
  vpId: this.#vpId,
2837
2838
  // Multi-VP fan-out (history-dedup): skip the user-row append
2838
2839
  // in stop-hooks when the orchestrator already wrote it once
@@ -65,6 +65,7 @@ export async function runStopHooks(context) {
65
65
  // history replay can route messages back into the originating group.
66
66
  sessionId,
67
67
  threadId,
68
+ turnId,
68
69
  // Group VP attribution: persist the engine-bound VP id on assistant/tool
69
70
  // rows so history replay can route replies back to the same visible VP.
70
71
  vpId,
@@ -167,6 +168,9 @@ export async function runStopHooks(context) {
167
168
  // Bug 6: stamp sessionId / threadId so replay can re-route by group.
168
169
  if (sessionId) record.sessionId = sessionId;
169
170
  if (threadId) record.threadId = threadId;
171
+ if (turnId && (msg.role === 'assistant' || msg.role === 'tool')) {
172
+ record.turnId = turnId;
173
+ }
170
174
  if (vpId && (msg.role === 'assistant' || msg.role === 'tool')) {
171
175
  record.speakerVpId = vpId;
172
176
  }
@@ -712,8 +712,9 @@ function projectPersistedToHistoryEntry(m) {
712
712
  const entry = { role: m.role, content: m.role === 'tool' ? m.content : __testNormalizePersistedVisibleContent(m.content) };
713
713
  if (m.id) entry.id = m.id;
714
714
  entry.threadId = m.threadId || m.turnId || 'main';
715
- entry.turnId = m.turnId || entry.threadId;
715
+ if (m.turnId) entry.turnId = m.turnId;
716
716
  if (m.sessionId) entry.sessionId = m.sessionId;
717
+ if (m.clientMessageId) entry.clientMessageId = m.clientMessageId;
717
718
  if (m.speakerVpId) entry.speakerVpId = m.speakerVpId;
718
719
  if (m.toolCallId) entry.toolCallId = m.toolCallId;
719
720
  if (Array.isArray(m.toolCalls) && m.toolCalls.length > 0) {
@@ -813,8 +814,9 @@ function projectVisibleHistoryChunkMessages(messages = []) {
813
814
  content: m.content,
814
815
  ts: m.ts || null,
815
816
  sessionId: m.sessionId || null,
817
+ ...(m.clientMessageId ? { clientMessageId: m.clientMessageId } : {}),
816
818
  threadId: m.threadId || m.turnId || 'main',
817
- turnId: m.turnId || m.threadId || 'main',
819
+ ...(m.turnId ? { turnId: m.turnId } : {}),
818
820
  ...(Array.isArray(m.attachments) && m.attachments.length > 0 ? { attachments: hydrateHistoryAttachmentPreviews(m.attachments) } : {}),
819
821
  ...(m.speakerVpId ? { speakerVpId: m.speakerVpId } : {}),
820
822
  ...(Number.isFinite(m.toolSummaryCount) && m.toolSummaryCount > 0
@@ -1392,6 +1394,7 @@ async function routeEnvelopeToVpThread(sessionId, vpId, envelope) {
1392
1394
  attachments: Array.isArray(envelope?.msg?.meta?.attachments) ? envelope.msg.meta.attachments : [],
1393
1395
  internal: isInternalAppend,
1394
1396
  ts: envelope?.msg?.ts || null,
1397
+ clientMessageId: envelope?.msg?.meta?.clientMessageId || null,
1395
1398
  });
1396
1399
  thread.updatedAt = Date.now();
1397
1400
  try {
@@ -1489,6 +1492,7 @@ function ensureDriverRunning(sessionId, vpId, threadId = 'main') {
1489
1492
  attachments: Array.isArray(meta.attachments) ? meta.attachments : [],
1490
1493
  internal: isInternal,
1491
1494
  ts: envelope?.msg?.ts || null,
1495
+ clientMessageId: meta.clientMessageId || null,
1492
1496
  });
1493
1497
  }
1494
1498
  } catch { /* never crash WS pipeline */ }
@@ -3152,6 +3156,7 @@ export async function handleYeaftSessionSend(msg) {
3152
3156
  mentions,
3153
3157
  // Persisted form (no base64) — safe for jsonl-log.
3154
3158
  attachments: persistedAttachments,
3159
+ clientMessageId: typeof msg.id === 'string' && msg.id ? msg.id : null,
3155
3160
  },
3156
3161
  // Live form — adapters need the base64 image blocks; runVpTurn
3157
3162
  // reads `_promptParts` off the envelope rather than going
@@ -3691,6 +3696,7 @@ async function runVpTurn({ prompt, promptParts = null, sessionId, vpId, threadId
3691
3696
  // would render the user's prompt N times.
3692
3697
  userAlreadyPersisted: true,
3693
3698
  threadId,
3699
+ vpTurnId: turnId,
3694
3700
  drainPendingUserMessages: () => {
3695
3701
  if (!thread || !Array.isArray(thread.pendingQueries) || thread.pendingQueries.length === 0) return [];
3696
3702
  return thread.pendingQueries.splice(0);
@@ -3945,11 +3951,11 @@ function appendTurnToSessionHistory(sessionId, threadId, vpId, prompts, assistan
3945
3951
  * refresh replay can render chips without leaking image source data into
3946
3952
  * the message body.
3947
3953
  *
3948
- * @param {{ msgId:string, text:string, sessionId:string, role?:string, speakerVpId?:string|null, attachments?:Array<object>, internal?:boolean, ts?:string|null }} args
3954
+ * @param {{ msgId:string, text:string, sessionId:string, role?:string, speakerVpId?:string|null, attachments?:Array<object>, internal?:boolean, ts?:string|null, clientMessageId?:string|null }} args
3949
3955
  * @returns {boolean} true if this call wrote the row, false if a prior
3950
3956
  * call already wrote it (dedup hit).
3951
3957
  */
3952
- function persistInboundMessageOnceByMsgId({ msgId, text, sessionId, threadId = 'main', role, speakerVpId, attachments, internal = false, ts = null }) {
3958
+ function persistInboundMessageOnceByMsgId({ msgId, text, sessionId, threadId = 'main', role, speakerVpId, attachments, internal = false, ts = null, clientMessageId = null }) {
3953
3959
  if (!session?.conversationStore) return false;
3954
3960
  // No msgId means no dedup key — caller is responsible for guarding.
3955
3961
  // Both call sites already do (`if (envMsgId && text)` and
@@ -3993,6 +3999,9 @@ function persistInboundMessageOnceByMsgId({ msgId, text, sessionId, threadId = '
3993
3999
  threadId: threadId || 'main',
3994
4000
  };
3995
4001
  if (sessionId) record.sessionId = sessionId;
4002
+ if (persistRole === 'user' && clientMessageId && typeof clientMessageId === 'string') {
4003
+ record.clientMessageId = clientMessageId;
4004
+ }
3996
4005
  // Stamp speakerVpId so the UI's loadHistory replay can route the row
3997
4006
  // to the correct VP block. Only meaningful when role='assistant'; for
3998
4007
  // a real user message we leave it unset (the UI's user track is