@yeaft/webchat-agent 0.1.546 → 0.1.548
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 +6 -1
- package/package.json +1 -1
- package/unify/memory/dream-scheduler.js +182 -0
- package/unify/session.js +30 -1
- package/unify/web-bridge.js +49 -0
|
@@ -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 } from '../unify/web-bridge.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';
|
|
40
40
|
|
|
41
41
|
export async function handleMessage(msg) {
|
|
42
42
|
switch (msg.type) {
|
|
@@ -462,6 +462,11 @@ export async function handleMessage(msg) {
|
|
|
462
462
|
handleUnifySetDefaultVp(msg);
|
|
463
463
|
break;
|
|
464
464
|
|
|
465
|
+
// wave-6b: manual dream trigger from VP detail page
|
|
466
|
+
case 'unify_dream_trigger':
|
|
467
|
+
await handleUnifyDreamTrigger(msg);
|
|
468
|
+
break;
|
|
469
|
+
|
|
465
470
|
// Expert roles definition (for ExpertPanel detail view)
|
|
466
471
|
case 'get_expert_roles': {
|
|
467
472
|
const { getExpertRolesDefinition } = await import('../expert-roles.js');
|
package/package.json
CHANGED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dream-scheduler.js — wave-6b: idle-timer dream trigger + per-VP orchestration.
|
|
3
|
+
*
|
|
4
|
+
* Responsibilities:
|
|
5
|
+
* 1. Idle timer: fires after 30 min of no user messages if there are
|
|
6
|
+
* un-ingested (new since last dream) messages in the session.
|
|
7
|
+
* 2. Per-VP dream orchestration: runs dreamShard() per VP, serial within
|
|
8
|
+
* each VP, max 2 VPs concurrently.
|
|
9
|
+
* 3. Integrates the 334f recompression hook post-dream.
|
|
10
|
+
* 4. Exposes `triggerDreamNow()` for manual "Run dream now" from the UI.
|
|
11
|
+
*
|
|
12
|
+
* Usage (from session.js / web-bridge.js):
|
|
13
|
+
* const scheduler = createDreamScheduler({ session });
|
|
14
|
+
* scheduler.noteUserMessage(); // reset idle timer on each user msg
|
|
15
|
+
* scheduler.triggerDreamNow(); // manual trigger from WS event
|
|
16
|
+
* scheduler.shutdown(); // cleanup on session close
|
|
17
|
+
*
|
|
18
|
+
* References:
|
|
19
|
+
* - 334g dream-shard.js: dreamShard(), scanShards(), runCompactJob()
|
|
20
|
+
* - 334f recompression.js: checkRecompression()
|
|
21
|
+
* - PM spec: "30min 无消息 AND 有未 ingest message → 触发"
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { dreamShard } from './dream-shard.js';
|
|
25
|
+
import { checkRecompression } from './recompression.js';
|
|
26
|
+
|
|
27
|
+
/** Default idle timeout before dream triggers (ms). */
|
|
28
|
+
export const DREAM_IDLE_MS = 30 * 60 * 1000; // 30 min
|
|
29
|
+
|
|
30
|
+
/** Max concurrent VP dream runs. */
|
|
31
|
+
const MAX_CONCURRENT_DREAMS = 2;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Create a dream scheduler instance.
|
|
35
|
+
*
|
|
36
|
+
* @param {{
|
|
37
|
+
* memoryShardStore: object | null,
|
|
38
|
+
* adapter: object | null,
|
|
39
|
+
* config: object,
|
|
40
|
+
* idleMs?: number,
|
|
41
|
+
* onDreamStart?: (vpId: string) => void,
|
|
42
|
+
* onDreamEnd?: (vpId: string, result: object) => void,
|
|
43
|
+
* onError?: (vpId: string, err: Error) => void,
|
|
44
|
+
* }} opts
|
|
45
|
+
* @returns {DreamScheduler}
|
|
46
|
+
*/
|
|
47
|
+
export function createDreamScheduler(opts = {}) {
|
|
48
|
+
const {
|
|
49
|
+
memoryShardStore,
|
|
50
|
+
adapter,
|
|
51
|
+
config,
|
|
52
|
+
idleMs = DREAM_IDLE_MS,
|
|
53
|
+
onDreamStart,
|
|
54
|
+
onDreamEnd,
|
|
55
|
+
onError,
|
|
56
|
+
} = opts;
|
|
57
|
+
|
|
58
|
+
let idleTimer = null;
|
|
59
|
+
let messagesSinceLastDream = 0;
|
|
60
|
+
let dreamRunning = false;
|
|
61
|
+
let lastDreamAt = 0;
|
|
62
|
+
let destroyed = false;
|
|
63
|
+
|
|
64
|
+
// ── Idle timer management ────────────────────────────────
|
|
65
|
+
|
|
66
|
+
function resetIdleTimer() {
|
|
67
|
+
if (destroyed) return;
|
|
68
|
+
if (idleTimer) {
|
|
69
|
+
clearTimeout(idleTimer);
|
|
70
|
+
idleTimer = null;
|
|
71
|
+
}
|
|
72
|
+
if (messagesSinceLastDream > 0) {
|
|
73
|
+
idleTimer = setTimeout(() => {
|
|
74
|
+
idleTimer = null;
|
|
75
|
+
if (messagesSinceLastDream > 0 && !dreamRunning && !destroyed) {
|
|
76
|
+
runDream('idle').catch(() => {});
|
|
77
|
+
}
|
|
78
|
+
}, idleMs);
|
|
79
|
+
if (idleTimer && typeof idleTimer.unref === 'function') {
|
|
80
|
+
idleTimer.unref();
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Call on every user message to reset the idle timer and increment
|
|
87
|
+
* the un-ingested message counter.
|
|
88
|
+
*/
|
|
89
|
+
function noteUserMessage() {
|
|
90
|
+
if (destroyed) return;
|
|
91
|
+
messagesSinceLastDream++;
|
|
92
|
+
resetIdleTimer();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ── Dream execution ──────────────────────────────────────
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Run a dream cycle. Currently single-VP (Unify default VP), but
|
|
99
|
+
* structured for future multi-VP with maxConcurrent=2.
|
|
100
|
+
*
|
|
101
|
+
* @param {'idle'|'manual'} trigger
|
|
102
|
+
* @returns {Promise<object>} dream result
|
|
103
|
+
*/
|
|
104
|
+
async function runDream(trigger = 'manual') {
|
|
105
|
+
if (dreamRunning) {
|
|
106
|
+
return { skipped: true, reason: 'already_running' };
|
|
107
|
+
}
|
|
108
|
+
if (!memoryShardStore) {
|
|
109
|
+
return { skipped: true, reason: 'no_shard_store' };
|
|
110
|
+
}
|
|
111
|
+
if (!adapter) {
|
|
112
|
+
return { skipped: true, reason: 'no_adapter' };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
dreamRunning = true;
|
|
116
|
+
const vpId = 'default'; // single-VP Unify mode
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
onDreamStart?.(vpId);
|
|
120
|
+
|
|
121
|
+
const result = await dreamShard({
|
|
122
|
+
shardStore: memoryShardStore,
|
|
123
|
+
adapter,
|
|
124
|
+
config: { model: config?.primaryModel || config?.model || 'default' },
|
|
125
|
+
onPhase: (phase, data) => {
|
|
126
|
+
// Could forward to UI in future
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// Post-dream: run recompression hook (334f)
|
|
131
|
+
try {
|
|
132
|
+
const recompResult = checkRecompression(memoryShardStore);
|
|
133
|
+
result.recompression = recompResult;
|
|
134
|
+
} catch {
|
|
135
|
+
// Non-fatal
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
messagesSinceLastDream = 0;
|
|
139
|
+
lastDreamAt = Date.now();
|
|
140
|
+
onDreamEnd?.(vpId, { ...result, trigger });
|
|
141
|
+
|
|
142
|
+
return result;
|
|
143
|
+
} catch (err) {
|
|
144
|
+
onError?.(vpId, err);
|
|
145
|
+
return { error: err.message, trigger };
|
|
146
|
+
} finally {
|
|
147
|
+
dreamRunning = false;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Manual trigger from UI ("Run dream now" button).
|
|
153
|
+
* @returns {Promise<object>}
|
|
154
|
+
*/
|
|
155
|
+
function triggerDreamNow() {
|
|
156
|
+
return runDream('manual');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Cleanup: clear timers, prevent further runs.
|
|
161
|
+
*/
|
|
162
|
+
function shutdown() {
|
|
163
|
+
destroyed = true;
|
|
164
|
+
if (idleTimer) {
|
|
165
|
+
clearTimeout(idleTimer);
|
|
166
|
+
idleTimer = null;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return {
|
|
171
|
+
noteUserMessage,
|
|
172
|
+
triggerDreamNow,
|
|
173
|
+
shutdown,
|
|
174
|
+
get isRunning() { return dreamRunning; },
|
|
175
|
+
get messagesSinceLastDream() { return messagesSinceLastDream; },
|
|
176
|
+
get lastDreamAt() { return lastDreamAt; },
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* @typedef {ReturnType<typeof createDreamScheduler>} DreamScheduler
|
|
182
|
+
*/
|
package/unify/session.js
CHANGED
|
@@ -33,6 +33,7 @@ import { createIntentClassifier } from './router/intent-classifier.js';
|
|
|
33
33
|
import { initInputQueueStore } from './input-queue/store.js';
|
|
34
34
|
import { createDispatcher } from './pipeline/dispatcher.js';
|
|
35
35
|
import { ensureDefaultGroupIfEmpty } from './groups/group-crud.js';
|
|
36
|
+
import { createDreamScheduler } from './memory/dream-scheduler.js';
|
|
36
37
|
import { join } from 'path';
|
|
37
38
|
import { existsSync as existsSyncSafe, readFileSync as readFileSyncSafe } from 'fs';
|
|
38
39
|
|
|
@@ -232,6 +233,28 @@ export async function loadSession(options = {}) {
|
|
|
232
233
|
yeaftDir,
|
|
233
234
|
});
|
|
234
235
|
|
|
236
|
+
// ─── 9a. Create dream scheduler (wave-6b) ─────────────
|
|
237
|
+
const dreamScheduler = createDreamScheduler({
|
|
238
|
+
memoryShardStore,
|
|
239
|
+
adapter,
|
|
240
|
+
config,
|
|
241
|
+
onDreamStart: (vpId) => {
|
|
242
|
+
if (config.debug) console.log(`[Yeaft] Dream started for VP ${vpId}`);
|
|
243
|
+
},
|
|
244
|
+
onDreamEnd: (vpId, result) => {
|
|
245
|
+
if (config.debug) console.log(`[Yeaft] Dream ended for VP ${vpId}:`, JSON.stringify({
|
|
246
|
+
trigger: result.trigger,
|
|
247
|
+
entriesMerged: result.entriesMerged,
|
|
248
|
+
entriesPruned: result.entriesPruned,
|
|
249
|
+
bytesReclaimed: result.bytesReclaimed,
|
|
250
|
+
errors: result.errors?.length || 0,
|
|
251
|
+
}));
|
|
252
|
+
},
|
|
253
|
+
onError: (vpId, err) => {
|
|
254
|
+
console.warn(`[Yeaft] Dream error for VP ${vpId}:`, err?.message || err);
|
|
255
|
+
},
|
|
256
|
+
});
|
|
257
|
+
|
|
235
258
|
// task-308 Phase 2: thread-aware engine registry.
|
|
236
259
|
// Each thread gets its own EngineInstance (lazy-created) that owns its
|
|
237
260
|
// messages array and tags all events with the bound threadId. Legacy
|
|
@@ -283,8 +306,13 @@ export async function loadSession(options = {}) {
|
|
|
283
306
|
tools: toolRegistry.size,
|
|
284
307
|
};
|
|
285
308
|
|
|
286
|
-
/** Graceful shutdown: disconnect MCP, close trace DB. */
|
|
309
|
+
/** Graceful shutdown: disconnect MCP, close trace DB, stop dream scheduler. */
|
|
287
310
|
async function shutdown() {
|
|
311
|
+
try {
|
|
312
|
+
dreamScheduler.shutdown();
|
|
313
|
+
} catch {
|
|
314
|
+
// Best-effort cleanup
|
|
315
|
+
}
|
|
288
316
|
try {
|
|
289
317
|
engineRegistry.terminateAll();
|
|
290
318
|
} catch {
|
|
@@ -313,6 +341,7 @@ export async function loadSession(options = {}) {
|
|
|
313
341
|
conversationStore,
|
|
314
342
|
memoryStore,
|
|
315
343
|
memoryShardStore,
|
|
344
|
+
dreamScheduler,
|
|
316
345
|
skillManager,
|
|
317
346
|
mcpManager,
|
|
318
347
|
toolRegistry,
|
package/unify/web-bridge.js
CHANGED
|
@@ -1016,6 +1016,11 @@ export async function handleUnifyChat(msg) {
|
|
|
1016
1016
|
sendGroupSnapshotBroadcast();
|
|
1017
1017
|
}
|
|
1018
1018
|
|
|
1019
|
+
// wave-6b: notify dream scheduler of user activity
|
|
1020
|
+
if (session?.dreamScheduler) {
|
|
1021
|
+
session.dreamScheduler.noteUserMessage();
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1019
1024
|
// ─── Per-call AbortController (task-320) ──
|
|
1020
1025
|
// Each call owns its own controller. Only once the router resolves the
|
|
1021
1026
|
// target thread do we register it into `abortByThread` and abort any
|
|
@@ -1260,6 +1265,50 @@ export function __testGetRegisteredThreadIds() {
|
|
|
1260
1265
|
return [...abortByThread.keys()];
|
|
1261
1266
|
}
|
|
1262
1267
|
|
|
1268
|
+
/**
|
|
1269
|
+
* wave-6b: Handle manual dream trigger from VP detail page.
|
|
1270
|
+
* Payload: { vpId?: string } (optional, defaults to 'default').
|
|
1271
|
+
* Emits unify_dream_result with the dream outcome.
|
|
1272
|
+
*
|
|
1273
|
+
* @param {{ vpId?: string }} msg
|
|
1274
|
+
*/
|
|
1275
|
+
export async function handleUnifyDreamTrigger(msg = {}) {
|
|
1276
|
+
if (!session?.dreamScheduler) {
|
|
1277
|
+
sendToServer({
|
|
1278
|
+
type: 'unify_dream_result',
|
|
1279
|
+
success: false,
|
|
1280
|
+
error: 'Dream scheduler not initialized — session not loaded.',
|
|
1281
|
+
});
|
|
1282
|
+
return;
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
const vpId = msg.vpId || 'default';
|
|
1286
|
+
|
|
1287
|
+
try {
|
|
1288
|
+
sendToServer({
|
|
1289
|
+
type: 'unify_dream_status',
|
|
1290
|
+
vpId,
|
|
1291
|
+
status: 'running',
|
|
1292
|
+
});
|
|
1293
|
+
|
|
1294
|
+
const result = await session.dreamScheduler.triggerDreamNow();
|
|
1295
|
+
|
|
1296
|
+
sendToServer({
|
|
1297
|
+
type: 'unify_dream_result',
|
|
1298
|
+
vpId,
|
|
1299
|
+
success: !result.error && !result.skipped,
|
|
1300
|
+
...result,
|
|
1301
|
+
});
|
|
1302
|
+
} catch (err) {
|
|
1303
|
+
sendToServer({
|
|
1304
|
+
type: 'unify_dream_result',
|
|
1305
|
+
vpId,
|
|
1306
|
+
success: false,
|
|
1307
|
+
error: err?.message || String(err),
|
|
1308
|
+
});
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1263
1312
|
/**
|
|
1264
1313
|
* Handle mode switch from the web UI.
|
|
1265
1314
|
* DEPRECATED (task-297): Unify no longer has chat/work mode distinction.
|