@yeaft/webchat-agent 0.1.607 → 0.1.609

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, handleUnifyGroupChat, handleUnifyModeSwitch, handleUnifyModelSwitch, resetUnifySession, handleUnifyLoadHistory, handleUnifyMergeThread, handleUnifyForkThread, handleUnifyAbortThread, handleUnifyAbortAll, handleUnifyVpSubscribe, handleUnifyVpCreate, handleUnifyVpUpdate, handleUnifyVpDelete, handleUnifyVpRead, handleUnifyTaskMessage, handleUnifyUserMemoryWrite, handleUnifyUserMemoryRemove, handleUnifyMemoryScopeList, handleUnifyMemoryQuery, handleUnifyMemoryTrace, handleUnifyFetchSummaryHistory, handleUnifyTaskCrud, 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, handleUnifyMemoryScopeList, handleUnifyMemoryQuery, handleUnifyMemoryTrace, handleUnifyFetchSummaryHistory, handleUnifyTaskCrud, handleUnifyListGroups, handleUnifyCreateGroup, handleUnifyRenameGroup, 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) {
@@ -479,6 +479,9 @@ export async function handleMessage(msg) {
479
479
  case 'unify_archive_group':
480
480
  handleUnifyArchiveGroup(msg);
481
481
  break;
482
+ case 'unify_delete_group':
483
+ handleUnifyDeleteGroup(msg);
484
+ break;
482
485
  case 'unify_add_member':
483
486
  handleUnifyAddMember(msg);
484
487
  break;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yeaft/webchat-agent",
3
- "version": "0.1.607",
3
+ "version": "0.1.609",
4
4
  "description": "Remote agent for Yeaft WebChat — connects worker machines to the central server",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -64,6 +64,11 @@ function serializeMessage(msg) {
64
64
  // their original thread id in `sourceThreadId` so the UI can still
65
65
  // render a small "#source" pill next to each bubble.
66
66
  if (msg.sourceThreadId) fm.push(`sourceThreadId: ${msg.sourceThreadId}`);
67
+ // Bug 6: persist groupId so history replay can stamp messages with the
68
+ // group they originated in. Without this, every replayed message lands
69
+ // in the default group and switching back to the originating group
70
+ // shows an empty pane.
71
+ if (msg.groupId) fm.push(`groupId: ${msg.groupId}`);
67
72
 
68
73
  // Token estimate
69
74
  const content = msg.content || '';
@@ -134,6 +139,7 @@ export function parseMessage(raw) {
134
139
  case 'tokens_est': msg.tokens_est = parseInt(value, 10); break;
135
140
  case 'threadId': msg.threadId = value; break;
136
141
  case 'sourceThreadId': msg.sourceThreadId = value; break;
142
+ case 'groupId': msg.groupId = value; break;
137
143
  // toolCalls are multi-line YAML — handled separately below
138
144
  }
139
145
  }
package/unify/engine.js CHANGED
@@ -476,7 +476,7 @@ export class Engine {
476
476
  * @param {string} assistantContent
477
477
  * @param {object[]} [toolCalls]
478
478
  */
479
- #persistMessages(userContent, assistantContent, toolCalls) {
479
+ #persistMessages(userContent, assistantContent, toolCalls, groupId) {
480
480
  if (!this.#conversationStore) return;
481
481
  if (this.#config._readOnly) return;
482
482
 
@@ -497,6 +497,8 @@ export class Engine {
497
497
  role: 'user',
498
498
  content: userContent,
499
499
  threadId,
500
+ // Bug 6: stamp groupId so history replay can route by group.
501
+ ...(groupId ? { groupId } : {}),
500
502
  });
501
503
 
502
504
  // Persist assistant message
@@ -505,6 +507,7 @@ export class Engine {
505
507
  content: assistantContent,
506
508
  model: this.#config.model,
507
509
  threadId,
510
+ ...(groupId ? { groupId } : {}),
508
511
  };
509
512
  if (toolCalls && toolCalls.length > 0) {
510
513
  assistantMsg.toolCalls = toolCalls;
@@ -1172,6 +1175,9 @@ export class Engine {
1172
1175
  primaryModel: this.#config.model,
1173
1176
  messages: conversationMessages,
1174
1177
  trace: this.#trace,
1178
+ // Bug 6: tag persisted messages with the originating group so
1179
+ // history replay can re-stamp them on reload.
1180
+ groupId,
1175
1181
  });
1176
1182
 
1177
1183
  if (hookResult.consolidated) {
@@ -1182,7 +1188,7 @@ export class Engine {
1182
1188
  }
1183
1189
  } else {
1184
1190
  // Legacy path (no yeaftDir → use old behavior)
1185
- this.#persistMessages(prompt, fullResponseText, assistantMsg.toolCalls);
1191
+ this.#persistMessages(prompt, fullResponseText, assistantMsg.toolCalls, groupId);
1186
1192
 
1187
1193
  const consolidated = await this.#maybeConsolidate();
1188
1194
  if (consolidated && consolidated.archivedCount > 0) {
@@ -32,7 +32,7 @@
32
32
  * 'reserved'/'invalid_vp_id'/... — bubbled from ids.js validators
33
33
  */
34
34
 
35
- import { existsSync, renameSync } from 'fs';
35
+ import { existsSync, renameSync, rmSync, readdirSync, statSync } from 'fs';
36
36
  import { randomBytes } from 'crypto';
37
37
  import { join } from 'path';
38
38
  import {
@@ -184,6 +184,74 @@ export function archiveGroup(yeaftDir, groupId) {
184
184
  return { groupId, archivedAs: dstDir };
185
185
  }
186
186
 
187
+ /**
188
+ * (A.3.b) Delete — physically remove the group directory and all its
189
+ * contents (group.json, messages/, tasks/, vps/). Irreversible.
190
+ *
191
+ * Bug 8 fix: replaces the soft-archive flow that left `.archived-*` dirs
192
+ * lying around in `~/.yeaft/groups/`. Per user request, "delete" means
193
+ * physical deletion, not rename.
194
+ *
195
+ * Also sweeps any sibling `.archived-*-<groupId>` dirs that were left
196
+ * behind by the previous soft-archive implementation, so a single
197
+ * delete cleans up legacy state too.
198
+ */
199
+ export function deleteGroup(yeaftDir, groupId) {
200
+ const root = groupsRoot(yeaftDir);
201
+ const srcDir = join(root, groupId);
202
+ const liveExists = existsSync(srcDir) && !!loadGroupMeta(srcDir);
203
+
204
+ // Collect any leftover soft-archive directories matching this groupId.
205
+ const legacyDirs = [];
206
+ if (existsSync(root)) {
207
+ for (const name of readdirSync(root)) {
208
+ if (!name.startsWith('.archived-')) continue;
209
+ // Soft-archive format: .archived-<ts>-<suffix>-<groupId>
210
+ if (!name.endsWith(`-${groupId}`)) continue;
211
+ const p = join(root, name);
212
+ try {
213
+ if (statSync(p).isDirectory()) legacyDirs.push(p);
214
+ } catch { /* skip */ }
215
+ }
216
+ }
217
+
218
+ if (!liveExists && legacyDirs.length === 0) {
219
+ throw new GroupCrudError('not_found', groupId);
220
+ }
221
+
222
+ if (liveExists) {
223
+ rmSync(srcDir, { recursive: true, force: true });
224
+ }
225
+ for (const dir of legacyDirs) {
226
+ rmSync(dir, { recursive: true, force: true });
227
+ }
228
+
229
+ return { groupId, deleted: true, legacyCleanedUp: legacyDirs.length };
230
+ }
231
+
232
+ /**
233
+ * Sweep any leftover `.archived-*` directories under groups/ that are
234
+ * orphans of the old soft-archive flow. Used at boot so users don't see
235
+ * ghost groups in subsequent loads. Returns the list of removed paths.
236
+ */
237
+ export function purgeArchivedGroups(yeaftDir) {
238
+ const root = groupsRoot(yeaftDir);
239
+ if (!existsSync(root)) return [];
240
+ const removed = [];
241
+ for (const name of readdirSync(root)) {
242
+ if (!name.startsWith('.archived-')) continue;
243
+ const p = join(root, name);
244
+ try {
245
+ if (!statSync(p).isDirectory()) continue;
246
+ } catch { continue; }
247
+ try {
248
+ rmSync(p, { recursive: true, force: true });
249
+ removed.push(p);
250
+ } catch { /* skip */ }
251
+ }
252
+ return removed;
253
+ }
254
+
187
255
  /**
188
256
  * (A.4) Add a VP to the group roster. Idempotent — no-op if already present.
189
257
  * Returns the new meta.
@@ -151,6 +151,8 @@ export function listGroups(groupsRoot) {
151
151
  if (!existsSync(groupsRoot)) return [];
152
152
  const out = [];
153
153
  for (const name of readdirSync(groupsRoot)) {
154
+ // Skip dotfiles and legacy soft-archive dirs (`.archived-*`).
155
+ if (name.startsWith('.')) continue;
154
156
  const p = join(groupsRoot, name);
155
157
  try {
156
158
  if (!statSync(p).isDirectory()) continue;
@@ -44,6 +44,8 @@ export {
44
44
  createGroupFromSpec,
45
45
  renameGroup,
46
46
  archiveGroup,
47
+ deleteGroup,
48
+ purgeArchivedGroups,
47
49
  addMember,
48
50
  removeMember,
49
51
  setGroupDefaultVp,
@@ -47,6 +47,10 @@ export async function runStopHooks(context) {
47
47
  messages = [],
48
48
  taskId,
49
49
  trace,
50
+ // Bug 6: groupId/threadId stamped on every persisted message so
51
+ // history replay can route messages back into the originating group.
52
+ groupId,
53
+ threadId,
50
54
  } = context;
51
55
 
52
56
  // Model name for persisted messages: use primaryModel if provided, else config.model
@@ -110,6 +114,9 @@ export async function runStopHooks(context) {
110
114
  record.toolCalls = msg.toolCalls;
111
115
  }
112
116
  if (msg.isError) record.isError = true;
117
+ // Bug 6: stamp groupId / threadId so replay can re-route by group.
118
+ if (groupId) record.groupId = groupId;
119
+ if (threadId) record.threadId = threadId;
113
120
  conversationStore.append(record);
114
121
  result.messagesPersisted++;
115
122
  }
@@ -40,6 +40,8 @@ import {
40
40
  createGroupFromSpec,
41
41
  renameGroup,
42
42
  archiveGroup,
43
+ deleteGroup,
44
+ purgeArchivedGroups,
43
45
  addMember,
44
46
  removeMember,
45
47
  setGroupDefaultVp,
@@ -455,6 +457,23 @@ export function handleUnifyArchiveGroup(msg) {
455
457
  }
456
458
  }
457
459
 
460
+ /**
461
+ * Bug 8: physical delete — removes the group dir and any legacy
462
+ * `.archived-*-<groupId>` siblings. Replies with op:'delete'.
463
+ */
464
+ export function handleUnifyDeleteGroup(msg) {
465
+ const requestId = msg && msg.requestId;
466
+ const groupId = msg && msg.groupId;
467
+ try {
468
+ const yeaftDir = ctx.CONFIG?.yeaftDir;
469
+ const result = deleteGroup(yeaftDir, groupId);
470
+ sendGroupCrudResult({ op: 'delete', requestId, ok: true, groupId: result.groupId });
471
+ sendGroupSnapshotBroadcast();
472
+ } catch (err) {
473
+ sendGroupCrudResult({ op: 'delete', requestId, ok: false, error: groupErrorPayload(err) });
474
+ }
475
+ }
476
+
458
477
  export function handleUnifyAddMember(msg) {
459
478
  const requestId = msg && msg.requestId;
460
479
  const groupId = msg && msg.groupId;
@@ -1370,6 +1389,21 @@ export async function handleUnifyChat(msg) {
1370
1389
  runAutoArchiveSweep(session);
1371
1390
  scheduleAutoArchive(session);
1372
1391
 
1392
+ // Bug 8: clean up any legacy `.archived-*` group directories left
1393
+ // behind by the previous soft-archive flow. This is a one-shot
1394
+ // boot-time sweep — physical deletes after this point are immediate.
1395
+ try {
1396
+ const yeaftDir = ctx.CONFIG?.yeaftDir;
1397
+ if (yeaftDir) {
1398
+ const removed = purgeArchivedGroups(yeaftDir);
1399
+ if (removed && removed.length > 0) {
1400
+ console.log(`[Unify] purged ${removed.length} legacy .archived group dir(s)`);
1401
+ }
1402
+ }
1403
+ } catch (err) {
1404
+ console.warn('[Unify] purgeArchivedGroups failed:', err?.message || err);
1405
+ }
1406
+
1373
1407
  // Create a stable conversationId for the Unify session
1374
1408
  unifyConversationId = `unify-${Date.now()}`;
1375
1409
 
@@ -2238,13 +2272,17 @@ export async function handleUnifyLoadHistory(msg) {
2238
2272
  // Send each message through standard claude_output rendering pipeline
2239
2273
  for (const m of messages) {
2240
2274
  if (m.role === 'user') {
2241
- sendUnifyOutput({ type: 'user', message: { content: m.content } });
2275
+ // Bug 6: forward groupId per message so the frontend re-stamps
2276
+ // replayed messages into their originating group instead of the
2277
+ // user's current filter (which would otherwise hide them when
2278
+ // switching groups).
2279
+ sendUnifyOutput({ type: 'user', message: { content: m.content } }, m.groupId || null);
2242
2280
  } else if (m.role === 'assistant') {
2243
2281
  sendUnifyOutput({
2244
2282
  type: 'assistant',
2245
2283
  message: { content: [{ type: 'text', text: m.content }] },
2246
- });
2247
- sendUnifyOutput({ type: 'result', result_text: '' });
2284
+ }, m.groupId || null);
2285
+ sendUnifyOutput({ type: 'result', result_text: '' }, m.groupId || null);
2248
2286
  }
2249
2287
  }
2250
2288