@yeaft/webchat-agent 0.1.931 → 0.1.932

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yeaft/webchat-agent",
3
- "version": "0.1.931",
3
+ "version": "0.1.932",
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,59 @@
1
+ /**
2
+ * dream/output-snapshot.js.
3
+ *
4
+ * Read-only projection of Dream-owned output files for UI observability.
5
+ * The Dream write path remains `runner -> apply -> memory/store`; callers use
6
+ * this module to load the current `memory.md` / `summary.md` contents for a
7
+ * single Yeaft session when emitting status or restoring a switched session.
8
+ */
9
+
10
+ import { join } from 'node:path';
11
+
12
+ import { readMemory, readSummary } from '../memory/store.js';
13
+ import { readGroupState } from './state.js';
14
+
15
+ export const DREAM_SNAPSHOT_TEXT_LIMIT = 6000;
16
+
17
+ export function truncateDreamText(value, limit = DREAM_SNAPSHOT_TEXT_LIMIT) {
18
+ const text = typeof value === 'string' ? value : '';
19
+ if (text.length <= limit) return { text, truncated: false };
20
+ return { text: text.slice(0, limit), truncated: true };
21
+ }
22
+
23
+ /**
24
+ * Build the loadable Dream output for one Yeaft session.
25
+ *
26
+ * @param {{ yeaftDir?: string|null }} sessionLike
27
+ * @param {string} sessionId
28
+ * @returns {Promise<object|null>}
29
+ */
30
+ export async function buildDreamOutputSnapshot(sessionLike, sessionId) {
31
+ if (!sessionId || !sessionLike?.yeaftDir) return null;
32
+ const scope = `group/${sessionId}`;
33
+ const memoryScope = { kind: 'group', id: sessionId };
34
+ const root = join(sessionLike.yeaftDir, 'memory');
35
+ const [memoryRaw, summaryRaw, state] = await Promise.all([
36
+ readMemory(memoryScope, { root }).catch(() => ''),
37
+ readSummary(memoryScope, { root }).catch(() => ''),
38
+ readGroupState(root, sessionId).catch(() => ({
39
+ lastDreamMessageId: null,
40
+ lastDreamAt: null,
41
+ messageCount: 0,
42
+ })),
43
+ ]);
44
+ const memory = truncateDreamText(memoryRaw);
45
+ const summary = truncateDreamText(summaryRaw);
46
+ return {
47
+ scope,
48
+ sessionId,
49
+ loadedAt: new Date().toISOString(),
50
+ lastDreamAt: state?.lastDreamAt || null,
51
+ lastDreamMessageId: state?.lastDreamMessageId || null,
52
+ messageCount: Number.isFinite(state?.messageCount) ? state.messageCount : 0,
53
+ hasOutput: !!(memoryRaw || summaryRaw),
54
+ memoryText: memory.text,
55
+ memoryTruncated: memory.truncated,
56
+ summaryText: summary.text,
57
+ summaryTruncated: summary.truncated,
58
+ };
59
+ }
@@ -338,7 +338,7 @@ export function createV2DreamScheduler(session) {
338
338
  ...buildRunDreamOpts(session, onProgress),
339
339
  manual: !!opts.manual,
340
340
  scopeFilter: Array.isArray(opts.scopeFilter) ? opts.scopeFilter : undefined,
341
- }).then((result) => {
341
+ }).then(async (result) => {
342
342
  // Bug 2: emit turn_close when the dream pass completes.
343
343
  result.trigger = opts.manual ? 'manual' : 'auto';
344
344
  if (session._dreamActiveGroupId && !result.sessionId) result.sessionId = session._dreamActiveGroupId;
@@ -370,6 +370,11 @@ export function createV2DreamScheduler(session) {
370
370
  if (typeof session._dreamProgressSink === 'function') {
371
371
  session._dreamProgressSink(turnClose);
372
372
  }
373
+ if (typeof session._dreamResultSink === 'function') {
374
+ try {
375
+ await session._dreamResultSink(result);
376
+ } catch { /* dream result visibility must never fail the scheduler */ }
377
+ }
373
378
  return result;
374
379
  });
375
380
  };
@@ -21,6 +21,7 @@
21
21
  import { join } from 'node:path';
22
22
  import { existsSync } from 'node:fs';
23
23
  import { randomUUID } from 'node:crypto';
24
+ import { buildDreamOutputSnapshot } from './dream/output-snapshot.js';
24
25
  import { Engine } from './engine.js';
25
26
  import { loadSession } from './session.js';
26
27
  import { loadConfig } from './config.js';
@@ -112,6 +113,14 @@ export function __testSetThreadClassifier(fn) {
112
113
  */
113
114
  const inflightScopedDreamGroups = new Set();
114
115
 
116
+ async function sendDreamSnapshotForSession(sessionId, extra = {}) {
117
+ const snapshot = await buildDreamOutputSnapshot(session, sessionId);
118
+ if (!snapshot) return null;
119
+ sendYeaftEvent({ type: 'yeaft_dream_snapshot', ...extra, snapshot }, { sessionId });
120
+ return snapshot;
121
+ }
122
+
123
+
115
124
  /**
116
125
  * Single in-flight AbortController for legacy 1:1 chat. A new 1:1 user message
117
126
  * cancels the prior round (if any).
@@ -1865,6 +1874,30 @@ export function installYeaftRuntimeBridge(s) {
1865
1874
  } catch { /* never let event delivery throw */ }
1866
1875
  };
1867
1876
 
1877
+ // Auto dream runs are triggered by the scheduler / nudges, not by the
1878
+ // manual `handleYeaftDreamTrigger` path. Without this terminal sink the UI
1879
+ // only saw progress debug events and could not restore the final dream
1880
+ // output after switching sessions. Manual runs keep using their explicit
1881
+ // handler below to avoid duplicate terminal events.
1882
+ s._dreamResultSink = async (result = {}) => {
1883
+ if (result?.trigger !== 'auto') return;
1884
+ const normalized = normalizeDreamResult(result);
1885
+ const processed = Array.isArray(result.groups)
1886
+ ? result.groups.filter(g => g && g.status === 'processed' && g.sessionId)
1887
+ : [];
1888
+ for (const group of processed) {
1889
+ const sessionId = group.sessionId;
1890
+ const snapshot = await buildDreamOutputSnapshot(session, sessionId).catch(() => null);
1891
+ sendToServer({
1892
+ type: 'yeaft_dream_result',
1893
+ sessionId,
1894
+ ...result,
1895
+ ...normalized,
1896
+ snapshot,
1897
+ });
1898
+ }
1899
+ };
1900
+
1868
1901
  // Wire the post-compact WS sink. Compactor is constructed in
1869
1902
  // session.js with a no-op sink; bridge owns `sendYeaftEvent` /
1870
1903
  // `yeaftConversationId`, so the sink is wired here once a session is
@@ -3594,6 +3627,9 @@ export async function handleYeaftDreamTrigger(msg = {}) {
3594
3627
  : await session.dreamScheduler.triggerDreamNow();
3595
3628
 
3596
3629
  const normalized = normalizeDreamResult(result);
3630
+ const snapshot = sessionId
3631
+ ? await buildDreamOutputSnapshot(session, sessionId).catch(() => null)
3632
+ : null;
3597
3633
 
3598
3634
  // Spread `result` FIRST so normalized fields (success, skipped,
3599
3635
  // skippedReason, groupsProcessed, groupsSkipped, targetsApplied,
@@ -3616,6 +3652,7 @@ export async function handleYeaftDreamTrigger(msg = {}) {
3616
3652
  ...tag,
3617
3653
  ...result,
3618
3654
  ...normalized,
3655
+ ...(snapshot ? { snapshot } : {}),
3619
3656
  });
3620
3657
  } catch (err) {
3621
3658
  const error = err?.message || String(err);
@@ -3829,6 +3866,9 @@ export async function handleYeaftLoadHistory(msg) {
3829
3866
  yeaftDir: ctx.CONFIG?.yeaftDir || null,
3830
3867
  });
3831
3868
  sendSessionSnapshotBroadcast();
3869
+ if (sessionId) {
3870
+ await sendDreamSnapshotForSession(sessionId, { trigger: 'load_history' }).catch(() => null);
3871
+ }
3832
3872
  // vp-status: replay the authoritative table on reconnect so a refreshed
3833
3873
  // frontend doesn't have to wait for the next transition to learn each
3834
3874
  // VP's current state.