@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.
- package/connection/message-router.js +4 -1
- package/package.json +1 -1
- package/unify/conversation/persist.js +6 -0
- package/unify/engine.js +8 -2
- package/unify/groups/group-crud.js +69 -1
- package/unify/groups/group-store.js +2 -0
- package/unify/groups/index.js +2 -0
- package/unify/stop-hooks.js +7 -0
- package/unify/web-bridge.js +41 -3
|
@@ -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
|
@@ -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;
|
package/unify/groups/index.js
CHANGED
package/unify/stop-hooks.js
CHANGED
|
@@ -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
|
}
|
package/unify/web-bridge.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|