@yeaft/webchat-agent 0.1.555 → 0.1.556

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.
@@ -36,7 +36,7 @@ import { sendToServer, flushMessageBuffer } from './buffer.js';
36
36
  import { handleRestartAgent, handleUpgradeAgent } from './upgrade.js';
37
37
  import { loadMcpServers, updateMcpConfig } from '../mcp.js';
38
38
  import { getLlmConfig, updateLlmConfig, getUnifySettings, updateUnifySettings } from '../unify/config-api.js';
39
- import { handleUnifyChat, handleUnifyModeSwitch, handleUnifyModelSwitch, resetUnifySession, handleUnifyLoadHistory, handleUnifyMergeThread, handleUnifyForkThread, handleUnifyAbortThread, handleUnifyAbortAll, handleUnifyVpSubscribe, handleUnifyVpCreate, handleUnifyVpUpdate, handleUnifyVpDelete, handleUnifyVpRead, handleUnifyTaskMessage, handleUnifyUserMemoryWrite, handleUnifyUserMemoryRemove, handleUnifyListGroups, handleUnifyCreateGroup, handleUnifyRenameGroup, handleUnifyArchiveGroup, handleUnifyAddMember, handleUnifyRemoveMember, handleUnifySetDefaultVp, handleUnifyDreamTrigger } from '../unify/web-bridge.js';
39
+ import { handleUnifyChat, handleUnifyGroupChat, handleUnifyModeSwitch, handleUnifyModelSwitch, resetUnifySession, handleUnifyLoadHistory, handleUnifyMergeThread, handleUnifyForkThread, handleUnifyAbortThread, handleUnifyAbortAll, handleUnifyVpSubscribe, handleUnifyVpCreate, handleUnifyVpUpdate, handleUnifyVpDelete, handleUnifyVpRead, handleUnifyTaskMessage, handleUnifyUserMemoryWrite, handleUnifyUserMemoryRemove, handleUnifyListGroups, handleUnifyCreateGroup, handleUnifyRenameGroup, handleUnifyArchiveGroup, handleUnifyAddMember, handleUnifyRemoveMember, handleUnifySetDefaultVp, handleUnifyDreamTrigger } from '../unify/web-bridge.js';
40
40
 
41
41
  export async function handleMessage(msg) {
42
42
  switch (msg.type) {
@@ -360,6 +360,11 @@ export async function handleMessage(msg) {
360
360
  await handleUnifyChat(msg);
361
361
  break;
362
362
 
363
+ // task-338-F4: Unify group-chat dispatch via GroupCoordinator.
364
+ case 'unify_group_chat':
365
+ await handleUnifyGroupChat(msg);
366
+ break;
367
+
363
368
  case 'unify_load_history':
364
369
  await handleUnifyLoadHistory(msg);
365
370
  break;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yeaft/webchat-agent",
3
- "version": "0.1.555",
3
+ "version": "0.1.556",
4
4
  "description": "Remote agent for Yeaft WebChat — connects worker machines to the central server",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -145,7 +145,7 @@ function _fanout(evt) {
145
145
  }
146
146
 
147
147
  function ensureLoader(registry = defaultRegistry) {
148
- if (_loaderStarted) return _loader;
148
+ if (_loaderStarted) return { loader: _loader, fresh: false };
149
149
  _loaderStarted = true;
150
150
  try {
151
151
  _loader = new VpLoader({
@@ -158,7 +158,7 @@ function ensureLoader(registry = defaultRegistry) {
158
158
  // Hot-reload optional; subscribe still returns whatever scan loaded.
159
159
  _loader = null;
160
160
  }
161
- return _loader;
161
+ return { loader: _loader, fresh: true };
162
162
  }
163
163
 
164
164
  /**
@@ -209,7 +209,19 @@ export function buildVpSnapshot(registry = defaultRegistry) {
209
209
  * @returns {() => void} unsubscribe fn
210
210
  */
211
211
  export function handleVpSubscribe(sendUnifyEvent, registry = defaultRegistry) {
212
- ensureLoader(registry);
212
+ const { loader, fresh } = ensureLoader(registry);
213
+ // task-338-F2: replay semantics. The loader's own start() already scans
214
+ // on first creation, so on a `fresh` loader we skip the extra rescan (it
215
+ // would be redundant work and, more importantly, tests that seed the
216
+ // registry BEFORE subscribing would see their seeded entries wiped by
217
+ // a rescan against an unrelated DEFAULT_VP_LIB_DIR). On subsequent
218
+ // subscribes (page reload, reconnect, second web client), rescanNow()
219
+ // refreshes the registry so every client gets a snapshot that reflects
220
+ // current disk — catching the case where the FS watcher missed an
221
+ // event or VPs were added between the initial scan and this subscribe.
222
+ if (!fresh && loader && typeof loader.rescanNow === 'function') {
223
+ try { loader.rescanNow(); } catch { /* never crash subscribe on rescan */ }
224
+ }
213
225
  _subscribers.add(sendUnifyEvent);
214
226
  try {
215
227
  sendUnifyEvent(buildVpSnapshot(registry));
@@ -940,6 +940,106 @@ function handleEngineEvent(event, threadId, hctx) {
940
940
  }
941
941
  }
942
942
 
943
+ /**
944
+ * task-338-F4: Handle a unify_group_chat message from the web UI.
945
+ *
946
+ * Routes user text through the group coordinator's dispatch contract:
947
+ * 1. @-mentions → each mentioned vpId (intersected with group roster)
948
+ * 2. no mention → group.defaultVpId
949
+ * 3. no default VP → fallback to legacy single-agent handleUnifyChat
950
+ *
951
+ * Emits a `group_message` event tagged with `vpId` per dispatched target so
952
+ * frontend can render VP-scoped feedback. Does NOT itself run the engine —
953
+ * for each resolved target, it delegates into handleUnifyChat (which owns
954
+ * the Dispatcher + AbortController + timeout plumbing).
955
+ *
956
+ * Message shape: { type:'unify_group_chat', groupId, text, mentions? }
957
+ *
958
+ * @param {{groupId:string, text:string, mentions?:string[], agentId?:string, userId?:string, username?:string}} msg
959
+ */
960
+ export async function handleUnifyGroupChat(msg) {
961
+ if (!msg || typeof msg !== 'object') return;
962
+ const { groupId, text } = msg;
963
+ if (!text?.trim()) return;
964
+ const mentions = Array.isArray(msg.mentions) ? msg.mentions : [];
965
+
966
+ // Resolve the group meta. If the group is missing / not found, fall back
967
+ // to legacy single-agent behavior so the send never silently drops.
968
+ let meta = null;
969
+ try {
970
+ const yeaftDir = ctx.CONFIG?.yeaftDir;
971
+ if (yeaftDir && groupId) {
972
+ const { openGroup, loadGroupMeta } = await import('./groups/group-store.js');
973
+ const { join } = await import('node:path');
974
+ const { existsSync } = await import('node:fs');
975
+ const root = join(yeaftDir, 'groups');
976
+ const dir = join(root, groupId);
977
+ if (existsSync(dir) && loadGroupMeta(dir)) {
978
+ const handle = openGroup(root, groupId);
979
+ meta = handle.getMeta();
980
+ }
981
+ }
982
+ } catch (err) {
983
+ console.warn('[Unify] unify_group_chat: group resolve failed', err?.message || err);
984
+ }
985
+
986
+ // Resolve target vpIds.
987
+ const roster = Array.isArray(meta?.roster) ? meta.roster : [];
988
+ let targets = [];
989
+ if (mentions.length > 0) {
990
+ // Intersect mentions with roster. Unknown mentions are ignored (not
991
+ // dispatched) — coordinator §6 "not_in_roster" semantics.
992
+ targets = mentions.filter((v) => roster.includes(v));
993
+ }
994
+ if (targets.length === 0 && meta?.defaultVpId && roster.includes(meta.defaultVpId)) {
995
+ targets = [meta.defaultVpId];
996
+ }
997
+
998
+ // Fallback: no target resolvable → behave as the legacy single-agent path
999
+ // so that the user's text still lands on the Engine.
1000
+ if (targets.length === 0) {
1001
+ await handleUnifyChat({
1002
+ ...msg,
1003
+ prompt: text,
1004
+ });
1005
+ return;
1006
+ }
1007
+
1008
+ // Tag outbound events with vpId so the UI can attribute them per VP. One
1009
+ // `group_message` event per target (mirrors coordinator.deliver() calls).
1010
+ for (const vpId of targets) {
1011
+ try {
1012
+ sendUnifyEvent({
1013
+ type: 'group_message',
1014
+ groupId,
1015
+ vpId,
1016
+ text,
1017
+ mentions,
1018
+ trigger: mentions.includes(vpId) ? 'mention' : 'fallback',
1019
+ ts: Date.now(),
1020
+ });
1021
+ } catch { /* never crash WS pipeline */ }
1022
+ }
1023
+
1024
+ // For each target, dispatch into the engine via handleUnifyChat. We prepend
1025
+ // an `@vp-<id>` tag on the prompt so the Dispatcher can route per-VP when
1026
+ // VP-bound Engine wiring lands (task-338-F2/F5); today handleUnifyChat
1027
+ // still runs a single shared Engine, which preserves legacy behaviour.
1028
+ for (const vpId of targets) {
1029
+ const scoped = `@vp-${vpId} ${text}`;
1030
+ try {
1031
+ await handleUnifyChat({
1032
+ ...msg,
1033
+ prompt: scoped,
1034
+ groupId,
1035
+ vpId,
1036
+ });
1037
+ } catch (err) {
1038
+ console.warn('[Unify] unify_group_chat: per-vp dispatch failed', vpId, err?.message || err);
1039
+ }
1040
+ }
1041
+ }
1042
+
943
1043
  /**
944
1044
  * Handle a unify_chat message from the web UI.
945
1045
  *