@yeaft/webchat-agent 0.1.590 → 0.1.591

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/unify/web-bridge.js +94 -27
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yeaft/webchat-agent",
3
- "version": "0.1.590",
3
+ "version": "0.1.591",
4
4
  "description": "Remote agent for Yeaft WebChat — connects worker machines to the central server",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -138,22 +138,31 @@ function isPermissionErrorMsg(msg) {
138
138
  * Send a unify_output message carrying claude_output-format data.
139
139
  * The server forwards this as-is to the web client.
140
140
  * The frontend's handleUnifyOutput will dispatch via handleClaudeOutput.
141
+ *
142
+ * Optional `groupId` tags every emitted assistant/tool/user mirror with
143
+ * the originating group so the frontend can stamp arriving messages with
144
+ * the SEND-context group instead of the user's CURRENT filter (which can
145
+ * change while the reply is in flight). Without this, switching groups
146
+ * mid-reply lands the assistant turn in the wrong group.
141
147
  */
142
- function sendUnifyOutput(data) {
148
+ function sendUnifyOutput(data, groupId) {
143
149
  sendToServer({
144
150
  type: 'unify_output',
145
151
  conversationId: unifyConversationId,
152
+ ...(groupId ? { groupId } : {}),
146
153
  data,
147
154
  });
148
155
  }
149
156
 
150
157
  /**
151
158
  * Send a unify_output event (non-claude_output metadata).
159
+ * Optional `groupId` — see sendUnifyOutput for rationale.
152
160
  */
153
- function sendUnifyEvent(event) {
161
+ function sendUnifyEvent(event, groupId) {
154
162
  sendToServer({
155
163
  type: 'unify_output',
156
164
  conversationId: unifyConversationId,
165
+ ...(groupId ? { groupId } : {}),
157
166
  event,
158
167
  });
159
168
  }
@@ -775,6 +784,7 @@ export function parseThreadPrefix(text) {
775
784
  */
776
785
  function forwardPipelineEvent(ev, ctx) {
777
786
  if (!ev || typeof ev !== 'object') return false;
787
+ const gid = ctx && ctx.groupId;
778
788
  switch (ev.type) {
779
789
  case 'input_queue_updated':
780
790
  sendUnifyEvent({
@@ -784,7 +794,7 @@ function forwardPipelineEvent(ev, ctx) {
784
794
  routing: ev.routing,
785
795
  dispatched: ev.dispatched,
786
796
  head: ev.head,
787
- });
797
+ }, gid);
788
798
  return false;
789
799
  case 'routing_decision':
790
800
  sendUnifyEvent({
@@ -794,7 +804,7 @@ function forwardPipelineEvent(ev, ctx) {
794
804
  targetThreadId: ev.targetThreadId,
795
805
  source: ev.source,
796
806
  reason: ev.reason,
797
- });
807
+ }, gid);
798
808
  return false;
799
809
  case 'thread_list_updated':
800
810
  // Dispatcher built it already; just forward.
@@ -802,7 +812,7 @@ function forwardPipelineEvent(ev, ctx) {
802
812
  type: 'thread_list_updated',
803
813
  threads: ev.threads,
804
814
  currentThreadId: ev.currentThreadId,
805
- });
815
+ }, gid);
806
816
  return false;
807
817
  case 'engine_event':
808
818
  ctx.onEngineEvent(ev.event, ev.threadId);
@@ -827,6 +837,7 @@ function forwardPipelineEvent(ev, ctx) {
827
837
  */
828
838
  function handleEngineEvent(event, threadId, hctx) {
829
839
  hctx.resetQueryTimer();
840
+ const gid = hctx && hctx.groupId;
830
841
 
831
842
  // task-325b: translate Engine lifecycle events into a single
832
843
  // `thread_status` event for the frontend Working Status panel. These
@@ -851,11 +862,11 @@ function handleEngineEvent(event, threadId, hctx) {
851
862
  type: 'assistant',
852
863
  message: { content: [{ type: 'text', text: event.text }] },
853
864
  threadId,
854
- });
865
+ }, gid);
855
866
  break;
856
867
 
857
868
  case 'thinking_delta':
858
- sendUnifyEvent({ type: 'thinking_delta', text: event.text, threadId });
869
+ sendUnifyEvent({ type: 'thinking_delta', text: event.text, threadId }, gid);
859
870
  break;
860
871
 
861
872
  case 'tool_call':
@@ -874,7 +885,7 @@ function handleEngineEvent(event, threadId, hctx) {
874
885
  type: 'assistant',
875
886
  message: { content: [] },
876
887
  threadId,
877
- });
888
+ }, gid);
878
889
  sendUnifyOutput({
879
890
  type: 'assistant',
880
891
  message: {
@@ -886,7 +897,7 @@ function handleEngineEvent(event, threadId, hctx) {
886
897
  }],
887
898
  },
888
899
  threadId: event.threadId || threadId,
889
- });
900
+ }, gid);
890
901
  break;
891
902
 
892
903
  case 'tool_start':
@@ -895,7 +906,7 @@ function handleEngineEvent(event, threadId, hctx) {
895
906
  id: event.id,
896
907
  name: event.name,
897
908
  threadId: event.threadId || threadId,
898
- });
909
+ }, gid);
899
910
  break;
900
911
 
901
912
  case 'tool_end':
@@ -919,7 +930,7 @@ function handleEngineEvent(event, threadId, hctx) {
919
930
  is_error: event.isError || false,
920
931
  }],
921
932
  threadId: event.threadId || threadId,
922
- });
933
+ }, gid);
923
934
  if (THREAD_MUTATING_TOOLS.has(event.name)) {
924
935
  sendThreadListUpdate();
925
936
  }
@@ -937,7 +948,7 @@ function handleEngineEvent(event, threadId, hctx) {
937
948
  inputTokens: event.inputTokens,
938
949
  outputTokens: event.outputTokens,
939
950
  threadId,
940
- });
951
+ }, gid);
941
952
  break;
942
953
 
943
954
  case 'recall':
@@ -946,7 +957,7 @@ function handleEngineEvent(event, threadId, hctx) {
946
957
  entryCount: event.entryCount,
947
958
  cached: event.cached,
948
959
  threadId,
949
- });
960
+ }, gid);
950
961
  break;
951
962
 
952
963
  case 'consolidate':
@@ -962,7 +973,7 @@ function handleEngineEvent(event, threadId, hctx) {
962
973
  archivedCount: event.archivedCount,
963
974
  extractedCount: event.extractedCount,
964
975
  threadId,
965
- });
976
+ }, gid);
966
977
  break;
967
978
 
968
979
  case 'fallback':
@@ -972,7 +983,7 @@ function handleEngineEvent(event, threadId, hctx) {
972
983
  to: event.to,
973
984
  reason: event.reason,
974
985
  threadId,
975
- });
986
+ }, gid);
976
987
  break;
977
988
 
978
989
  case 'debug_turn':
@@ -992,7 +1003,7 @@ function handleEngineEvent(event, threadId, hctx) {
992
1003
  rawRequest: event.rawRequest,
993
1004
  rawResponse: event.rawResponse,
994
1005
  threadId,
995
- });
1006
+ }, gid);
996
1007
  break;
997
1008
 
998
1009
  case 'error': {
@@ -1012,7 +1023,7 @@ function handleEngineEvent(event, threadId, hctx) {
1012
1023
  }],
1013
1024
  },
1014
1025
  threadId,
1015
- });
1026
+ }, gid);
1016
1027
  }
1017
1028
  // Don't show subsequent permission errors.
1018
1029
  } else {
@@ -1022,7 +1033,7 @@ function handleEngineEvent(event, threadId, hctx) {
1022
1033
  content: [{ type: 'text', text: `⚠️ Error: ${errMsg}` }],
1023
1034
  },
1024
1035
  threadId,
1025
- });
1036
+ }, gid);
1026
1037
  }
1027
1038
  break;
1028
1039
  }
@@ -1089,6 +1100,53 @@ export async function handleUnifyGroupChat(msg) {
1089
1100
  return;
1090
1101
  }
1091
1102
 
1103
+ // Bug 2: When the user @-mentions a VP that exists in the library but is
1104
+ // not yet in the group's roster, auto-add it. This is the natural "invite"
1105
+ // gesture in group chat — failing here would punt to the legacy fallback
1106
+ // and surface the misleading "only Yeaft is in this conversation" error
1107
+ // even though the VP exists. We also ensure the group has a defaultVpId
1108
+ // when its roster is non-empty, so unaddressed messages route correctly.
1109
+ try {
1110
+ const meta = groupHandle.getMeta();
1111
+ const yeaftDir = ctx.CONFIG?.yeaftDir;
1112
+ const wantsAdd = mentions.filter(
1113
+ (m) => m && m !== 'all' && !meta.roster.includes(m)
1114
+ );
1115
+ if (wantsAdd.length && yeaftDir) {
1116
+ let mutated = false;
1117
+ for (const vpId of wantsAdd) {
1118
+ try {
1119
+ const vp = readVp(vpId);
1120
+ if (!vp) continue;
1121
+ addMember(yeaftDir, groupId, vpId);
1122
+ mutated = true;
1123
+ } catch { /* skip strangers */ }
1124
+ }
1125
+ if (mutated) {
1126
+ // Re-open with fresh meta so the coordinator sees the new roster.
1127
+ try { groupHandle.close && groupHandle.close(); } catch { /* best-effort */ }
1128
+ const { openGroup } = await import('./groups/group-store.js');
1129
+ const { join } = await import('node:path');
1130
+ groupHandle = openGroup(join(yeaftDir, 'groups'), groupId);
1131
+ sendGroupRosterChanged(groupHandle.getMeta());
1132
+ }
1133
+ }
1134
+ // Heal missing defaultVpId — pick roster[0] when one exists.
1135
+ const meta2 = groupHandle.getMeta();
1136
+ if (!meta2.defaultVpId && meta2.roster.length && yeaftDir) {
1137
+ try {
1138
+ setGroupDefaultVp(yeaftDir, groupId, meta2.roster[0]);
1139
+ try { groupHandle.close && groupHandle.close(); } catch { /* best-effort */ }
1140
+ const { openGroup } = await import('./groups/group-store.js');
1141
+ const { join } = await import('node:path');
1142
+ groupHandle = openGroup(join(yeaftDir, 'groups'), groupId);
1143
+ sendGroupRosterChanged(groupHandle.getMeta());
1144
+ } catch { /* best-effort */ }
1145
+ }
1146
+ } catch (err) {
1147
+ console.warn('[Unify] unify_group_chat: auto-roster heal failed', err?.message || err);
1148
+ }
1149
+
1092
1150
  // Adapter layer (PM red-line: do NOT modify coordinator to fit this
1093
1151
  // consumer). We drive `createCoordinator()` with a capturing `deliver`
1094
1152
  // callback, collect its dispatched/fallback report, then translate each
@@ -1238,6 +1296,10 @@ export async function handleUnifyChat(msg) {
1238
1296
  // branch and reach the dispatcher.submit() queryOpts.
1239
1297
  const vpId = typeof msg.vpId === 'string' && msg.vpId.trim() ? msg.vpId.trim() : null;
1240
1298
  const groupCoordinator = msg._groupCoordinator || null;
1299
+ // Bug 1: every event we emit during this query must carry the originating
1300
+ // groupId so the frontend stamps arriving messages with the SEND-context
1301
+ // group, not the user's CURRENT filter (which can change mid-reply).
1302
+ const groupId = typeof msg.groupId === 'string' && msg.groupId.trim() ? msg.groupId.trim() : null;
1241
1303
 
1242
1304
  // Deprecation warning — task-297 removed chat/work mode distinction
1243
1305
  if (mode !== undefined && mode !== null) {
@@ -1358,14 +1420,16 @@ export async function handleUnifyChat(msg) {
1358
1420
  routing: 0,
1359
1421
  dispatched: 0,
1360
1422
  head: { id: entry.id, status: entry.status, text: entry.text.slice(0, 80) },
1361
- });
1423
+ }, groupId);
1362
1424
 
1363
1425
  const pipelineCtx = {
1426
+ groupId,
1364
1427
  onEngineEvent: (event, threadId) => handleEngineEvent(event, threadId, {
1365
1428
  assistantTextParts,
1366
1429
  toolCallsAccum,
1367
1430
  toolResultsAccum,
1368
1431
  resetQueryTimer,
1432
+ groupId,
1369
1433
  }),
1370
1434
  onError: (err) => { throw err; },
1371
1435
  };
@@ -1427,12 +1491,12 @@ export async function handleUnifyChat(msg) {
1427
1491
  sendUnifyOutput({
1428
1492
  type: 'assistant',
1429
1493
  message: { content: [] },
1430
- });
1494
+ }, groupId);
1431
1495
  // Send result to clear processing state
1432
1496
  sendUnifyOutput({
1433
1497
  type: 'result',
1434
1498
  result_text: '',
1435
- });
1499
+ }, groupId);
1436
1500
 
1437
1501
  } finally {
1438
1502
  // Always clear the timeout guard
@@ -1451,7 +1515,7 @@ export async function handleUnifyChat(msg) {
1451
1515
  sendUnifyOutput({
1452
1516
  type: 'result',
1453
1517
  result_text: '',
1454
- });
1518
+ }, groupId);
1455
1519
  return;
1456
1520
  }
1457
1521
 
@@ -1469,7 +1533,7 @@ export async function handleUnifyChat(msg) {
1469
1533
  text: '⚠️ Cannot write to ~/.yeaft/ directory — some features (memory, history) are unavailable. Please check directory permissions: `chmod -R u+rw ~/.yeaft/`',
1470
1534
  }],
1471
1535
  },
1472
- });
1536
+ }, groupId);
1473
1537
  }
1474
1538
  } else {
1475
1539
  sendUnifyOutput({
@@ -1480,13 +1544,13 @@ export async function handleUnifyChat(msg) {
1480
1544
  text: `⚠️ Session error: ${err.message}`,
1481
1545
  }],
1482
1546
  },
1483
- });
1547
+ }, groupId);
1484
1548
  }
1485
1549
  // Still send result to clear processing state
1486
1550
  sendUnifyOutput({
1487
1551
  type: 'result',
1488
1552
  result_text: '',
1489
- });
1553
+ }, groupId);
1490
1554
  } finally {
1491
1555
  // task-320: only clear the per-thread slot if THIS controller is still
1492
1556
  // the registered one. If a newer message already overwrote it, leaving
@@ -2123,8 +2187,11 @@ export async function handleUnifyLoadHistory(msg) {
2123
2187
  // task-334m: replay groups snapshot so Sidebar Groups rebuilds on refresh.
2124
2188
  sendGroupSnapshotBroadcast();
2125
2189
 
2126
- const limit = msg.limit || 50;
2127
- const messages = session.conversationStore.loadRecent(limit);
2190
+ // Honor explicit limit:0 frontend uses it on Unify re-entry to refresh
2191
+ // metadata (model/status/group snapshot via the unconditional replay
2192
+ // above) without re-streaming the message history.
2193
+ const limit = (typeof msg.limit === 'number') ? msg.limit : 50;
2194
+ const messages = limit > 0 ? session.conversationStore.loadRecent(limit) : [];
2128
2195
  const compactSummary = session.conversationStore.readCompactSummary();
2129
2196
 
2130
2197
  // Send each message through standard claude_output rendering pipeline