@yeaft/webchat-agent 0.1.546 → 0.1.547

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 } 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yeaft/webchat-agent",
3
- "version": "0.1.546",
3
+ "version": "0.1.547",
4
4
  "description": "Remote agent for Yeaft WebChat — connects worker machines to the central server",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -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,
@@ -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.