@yeaft/webchat-agent 0.1.689 → 0.1.691

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, getSearchSettings, updateSearchSettings, fetchTavilyUsage } from '../unify/config-api.js';
39
- import { handleUnifyGroupChat, handleUnifyModeSwitch, handleUnifyModelSwitch, resetUnifySession, handleUnifyLoadHistory, handleUnifyAbortThread, handleUnifyAbortAll, handleUnifyAbortTurn, handleUnifyVpSubscribe, handleUnifyVpCreate, handleUnifyVpUpdate, handleUnifyVpDelete, handleUnifyVpRead, handleUnifyFeatureMessage, handleUnifyFetchSummaryHistory, handleUnifyFeatureCrud, handleUnifyListGroups, handleUnifyCreateGroup, handleUnifyRenameGroup, handleUnifyArchiveGroup, handleUnifyDeleteGroup, handleUnifyAddMember, handleUnifyRemoveMember, handleUnifySetDefaultVp, handleUnifyDreamTrigger } from '../unify/web-bridge.js';
39
+ import { handleUnifyGroupChat, handleUnifyModeSwitch, handleUnifyModelSwitch, resetUnifySession, handleUnifyLoadHistory, handleUnifyAbortThread, handleUnifyAbortAll, handleUnifyAbortTurn, handleUnifyVpSubscribe, handleUnifyVpCreate, handleUnifyVpUpdate, handleUnifyVpDelete, handleUnifyVpRead, handleUnifyFeatureMessage, handleUnifyFetchSummaryHistory, handleUnifyFeatureCrud, handleUnifyListGroups, handleUnifyCreateGroup, handleUnifyRenameGroup, handleUnifyUpdateGroup, handleUnifyArchiveGroup, handleUnifyDeleteGroup, handleUnifyAddMember, handleUnifyRemoveMember, handleUnifySetDefaultVp, handleUnifyDreamTrigger } from '../unify/web-bridge.js';
40
40
 
41
41
  export async function handleMessage(msg) {
42
42
  switch (msg.type) {
@@ -468,6 +468,9 @@ export async function handleMessage(msg) {
468
468
  case 'unify_rename_group':
469
469
  handleUnifyRenameGroup(msg);
470
470
  break;
471
+ case 'unify_update_group':
472
+ handleUnifyUpdateGroup(msg);
473
+ break;
471
474
  case 'unify_archive_group':
472
475
  handleUnifyArchiveGroup(msg);
473
476
  break;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yeaft/webchat-agent",
3
- "version": "0.1.689",
3
+ "version": "0.1.691",
4
4
  "description": "Remote agent for Yeaft WebChat — connects worker machines to the central server",
5
5
  "main": "index.js",
6
6
  "type": "module",
package/unify/engine.js CHANGED
@@ -563,7 +563,7 @@ export class Engine {
563
563
  * @param {{user?:string, group?:string, vp?:string}} [summaries]
564
564
  * @returns {string}
565
565
  */
566
- #buildSystemPrompt(memory, compactSummary, prompt, memoryInjection, userProfile, vpPersona, summaries) {
566
+ #buildSystemPrompt(memory, compactSummary, prompt, memoryInjection, userProfile, vpPersona, summaries, groupAnnouncement) {
567
567
  // Get relevant skill content if SkillManager is wired
568
568
  let skillContent = '';
569
569
  if (this.#skillManager && prompt) {
@@ -585,6 +585,7 @@ export class Engine {
585
585
  userProfile,
586
586
  vpPersona,
587
587
  summaries,
588
+ groupAnnouncement,
588
589
  // Worker-shape harness is descriptive metadata for human inspection;
589
590
  // production prompts skip it to save tokens. Re-enable via env when
590
591
  // diagnosing prompt structure issues.
@@ -859,7 +860,7 @@ export class Engine {
859
860
  * SCENARIO_EFFORT. Unknown values fall through to 'high'.
860
861
  * @yields {EngineEvent}
861
862
  */
862
- async *query({ prompt, messages = [], signal, userEffort = null, scenario = 'chat', vpPersona, router, senderVpId, inboundEnvelope, taskId, taskMembers, groupId, vpPlan } = {}) {
863
+ async *query({ prompt, messages = [], signal, userEffort = null, scenario = 'chat', vpPersona, router, senderVpId, inboundEnvelope, taskId, taskMembers, groupId, vpPlan, groupAnnouncement } = {}) {
863
864
  if (!prompt || typeof prompt !== 'string' || !prompt.trim()) {
864
865
  yield {
865
866
  type: 'error',
@@ -910,7 +911,7 @@ export class Engine {
910
911
  const runSignal = abortCtrl.signal;
911
912
 
912
913
  try {
913
- yield* this.#runQuery({ prompt: effectivePrompt, messages, signal: runSignal, userEffort: effectiveUserEffort, scenario, vpPersona, router, senderVpId, inboundEnvelope, taskId, taskMembers, groupId, vpPlan });
914
+ yield* this.#runQuery({ prompt: effectivePrompt, messages, signal: runSignal, userEffort: effectiveUserEffort, scenario, vpPersona, router, senderVpId, inboundEnvelope, taskId, taskMembers, groupId, vpPlan, groupAnnouncement });
914
915
  } finally {
915
916
  if (signal) {
916
917
  try { signal.removeEventListener('abort', onExternalAbort); } catch { /* ignore */ }
@@ -928,7 +929,7 @@ export class Engine {
928
929
  * in a try/finally without indenting the whole loop.
929
930
  * @private
930
931
  */
931
- async *#runQuery({ prompt, messages, signal, userEffort = null, scenario = 'chat', vpPersona, router, senderVpId, inboundEnvelope, taskId, taskMembers, groupId, vpPlan }) {
932
+ async *#runQuery({ prompt, messages, signal, userEffort = null, scenario = 'chat', vpPersona, router, senderVpId, inboundEnvelope, taskId, taskMembers, groupId, vpPlan, groupAnnouncement }) {
932
933
 
933
934
  // ─── Pre-query: FTS5 Memory Recall + Compact Summary ──
934
935
  // Memory feed comes from two places:
@@ -1004,7 +1005,7 @@ export class Engine {
1004
1005
  : amsContext.snapshotBlock;
1005
1006
  }
1006
1007
 
1007
- const systemPrompt = this.#buildSystemPrompt(undefined, compactSummary, prompt, memoryInjection, userProfile, vpPersona, summaries);
1008
+ const systemPrompt = this.#buildSystemPrompt(undefined, compactSummary, prompt, memoryInjection, userProfile, vpPersona, summaries, groupAnnouncement);
1008
1009
 
1009
1010
  // Build conversation: existing messages + new user message
1010
1011
  const conversationMessages = [
@@ -162,6 +162,26 @@ export function renameGroup(yeaftDir, groupId, newName) {
162
162
  return next;
163
163
  }
164
164
 
165
+ /**
166
+ * (A.2.b) Update announcement — group-wide system-prompt prefix shared by
167
+ * every VP in the group (CLAUDE.md-style). Empty/whitespace clears it.
168
+ *
169
+ * `text` must be a string. Trimmed before persist so leading/trailing
170
+ * whitespace doesn't pollute the prompt.
171
+ */
172
+ export function updateGroupAnnouncement(yeaftDir, groupId, text) {
173
+ if (typeof text !== 'string') {
174
+ throw new GroupCrudError('invalid_announcement', groupId);
175
+ }
176
+ const announcement = text.trim();
177
+ const handle = requireGroup(yeaftDir, groupId);
178
+ const meta = handle.getMeta();
179
+ handle.saveMeta({ ...meta, announcement });
180
+ const next = handle.getMeta();
181
+ handle.close();
182
+ return next;
183
+ }
184
+
165
185
  /**
166
186
  * (A.3) Archive — renames the dir to `.archived-<ts>-<id>`. Directory
167
187
  * prefix `.` keeps `listGroups` from picking it up (readdirSync filter in
@@ -126,6 +126,7 @@ export function createGroup(groupsRoot, spec) {
126
126
  name: spec.name || spec.id,
127
127
  roster,
128
128
  defaultVpId: spec.defaultVpId || null,
129
+ announcement: typeof spec.announcement === 'string' ? spec.announcement : '',
129
130
  createdAt: spec.createdAt || new Date().toISOString(),
130
131
  };
131
132
  h.saveMeta(meta);
@@ -140,6 +141,9 @@ export function loadGroupMeta(dir) {
140
141
  const raw = readFileSync(path, 'utf8');
141
142
  const parsed = JSON.parse(raw);
142
143
  validateMeta(parsed);
144
+ // Legacy groups created before the announcement field was added are
145
+ // forward-compat: missing field reads back as empty string.
146
+ if (typeof parsed.announcement !== 'string') parsed.announcement = '';
143
147
  return parsed;
144
148
  } catch {
145
149
  return null;
@@ -173,6 +177,9 @@ function validateMeta(meta) {
173
177
  if (meta.defaultVpId != null && typeof meta.defaultVpId !== 'string') {
174
178
  throw new Error('group.defaultVpId must be string|null');
175
179
  }
180
+ if (meta.announcement != null && typeof meta.announcement !== 'string') {
181
+ throw new Error('group.announcement must be string');
182
+ }
176
183
  }
177
184
 
178
185
  /**
package/unify/prompts.js CHANGED
@@ -283,6 +283,7 @@ export function buildSystemPrompt({
283
283
  coreMemory,
284
284
  memoryTraceAvailable = false,
285
285
  vpPersona,
286
+ groupAnnouncement = '',
286
287
  } = {}) {
287
288
  // Fallback to English for unknown languages
288
289
  const lang = PROMPTS[language] || PROMPTS.en;
@@ -310,6 +311,15 @@ export function buildSystemPrompt({
310
311
  }
311
312
  }
312
313
 
314
+ // ─── 1.5 Group Announcement (CLAUDE.md-style shared prefix) ───
315
+ // When a group has set an announcement, every VP in the group sees it
316
+ // near the top of the system prompt — before tools, memory, mode-specific
317
+ // instructions. Empty/whitespace = no block emitted.
318
+ const annText = (typeof groupAnnouncement === 'string') ? groupAnnouncement.trim() : '';
319
+ if (annText) {
320
+ parts.push(`[Group Announcement]\n${annText}`);
321
+ }
322
+
313
323
  // ─── 2. Date Metadata ──────────────────────────────────
314
324
  parts.push(lang.date(new Date().toISOString().split('T')[0]));
315
325
 
@@ -33,6 +33,7 @@ import {
33
33
  GroupCrudError,
34
34
  createGroupFromSpec,
35
35
  renameGroup,
36
+ updateGroupAnnouncement,
36
37
  archiveGroup,
37
38
  deleteGroup,
38
39
  purgeArchivedGroups,
@@ -318,6 +319,45 @@ export function handleUnifyRenameGroup(msg) {
318
319
  }
319
320
  }
320
321
 
322
+ /**
323
+ * `unify_update_group` — generalised group meta patch. Currently accepts
324
+ * `name` and `announcement` keys. Empty patch is rejected; an empty/
325
+ * whitespace-only `name` is also rejected up front rather than letting
326
+ * `renameGroup` raise a less-specific error deeper in the call stack.
327
+ *
328
+ * Partial-success contract: when a single patch contains BOTH `name` and
329
+ * `announcement`, the rename is committed first; if the announcement
330
+ * write throws, the rename has already persisted on disk and the client
331
+ * receives `ok:false` for the announcement error — i.e. the WS op is not
332
+ * atomic. Today's UI binds Save buttons per pane in `GroupSettingsModal`
333
+ * so this is theoretical; readers extending the patch shape should know
334
+ * the contract permits half-commits.
335
+ */
336
+ export function handleUnifyUpdateGroup(msg) {
337
+ const requestId = msg && msg.requestId;
338
+ const groupId = msg && msg.groupId;
339
+ const patch = (msg && msg.patch && typeof msg.patch === 'object') ? msg.patch : null;
340
+ try {
341
+ const hasName = patch && typeof patch.name === 'string' && patch.name.trim().length > 0;
342
+ const hasAnnouncement = patch && typeof patch.announcement === 'string';
343
+ if (!patch || (!hasName && !hasAnnouncement)) {
344
+ throw new GroupCrudError('invalid_patch', groupId);
345
+ }
346
+ const yeaftDir = ctx.CONFIG?.yeaftDir;
347
+ let group = null;
348
+ if (hasName) {
349
+ group = renameGroup(yeaftDir, groupId, patch.name);
350
+ }
351
+ if (hasAnnouncement) {
352
+ group = updateGroupAnnouncement(yeaftDir, groupId, patch.announcement);
353
+ }
354
+ sendGroupCrudResult({ op: 'update', requestId, ok: true, group });
355
+ sendGroupSnapshotBroadcast();
356
+ } catch (err) {
357
+ sendGroupCrudResult({ op: 'update', requestId, ok: false, error: groupErrorPayload(err) });
358
+ }
359
+ }
360
+
321
361
  export function handleUnifyArchiveGroup(msg) {
322
362
  const requestId = msg && msg.requestId;
323
363
  const groupId = msg && msg.groupId;
@@ -912,16 +952,24 @@ export async function handleUnifyGroupChat(msg) {
912
952
  * Build the per-query VP context for the Engine.
913
953
  */
914
954
  export function buildVpQueryOpts({ vpId, groupCoordinator, groupId }) {
955
+ // Read the group meta once and reuse for both defaultVpId fallback and
956
+ // announcement injection. Each .getMeta() reload reads + parses the
957
+ // group.json file, so calling it twice per turn is wasteful — and
958
+ // (more importantly) opens a window where a concurrent group edit
959
+ // could land between the two reads, giving the engine a defaultVpId
960
+ // from one snapshot and an announcement from a newer one.
961
+ let groupMeta = null;
962
+ try {
963
+ groupMeta = groupCoordinator && groupCoordinator.group
964
+ && typeof groupCoordinator.group.getMeta === 'function'
965
+ ? groupCoordinator.group.getMeta() : null;
966
+ } catch { /* coordinator inspection is best-effort */ }
967
+
915
968
  let resolvedVpId = vpId;
916
969
  if (!resolvedVpId) {
917
- try {
918
- const meta = groupCoordinator && groupCoordinator.group
919
- && typeof groupCoordinator.group.getMeta === 'function'
920
- ? groupCoordinator.group.getMeta() : null;
921
- if (meta && typeof meta.defaultVpId === 'string' && meta.defaultVpId) {
922
- resolvedVpId = meta.defaultVpId;
923
- }
924
- } catch { /* coordinator inspection is best-effort */ }
970
+ if (groupMeta && typeof groupMeta.defaultVpId === 'string' && groupMeta.defaultVpId) {
971
+ resolvedVpId = groupMeta.defaultVpId;
972
+ }
925
973
  }
926
974
  if (!resolvedVpId) {
927
975
  const cfgDefault = session?.config?.defaultVpId;
@@ -943,6 +991,12 @@ export function buildVpQueryOpts({ vpId, groupCoordinator, groupId }) {
943
991
  if (typeof groupId === 'string' && groupId.trim()) {
944
992
  out.groupId = groupId.trim();
945
993
  }
994
+ // task-334-group-editor: surface the group announcement to the engine so
995
+ // buildWorkerPrompt can inject it as a CLAUDE.md-style shared prefix.
996
+ // Empty/missing reads as '' and prompts.js skips the section.
997
+ if (groupMeta && typeof groupMeta.announcement === 'string') {
998
+ out.groupAnnouncement = groupMeta.announcement;
999
+ }
946
1000
  try {
947
1001
  const vp = readVp(resolvedVpId);
948
1002
  if (vp) {