@yeaft/webchat-agent 1.0.575 → 1.0.577

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.
@@ -21,7 +21,6 @@ import { createFullRegistry } from './tools/index.js';
21
21
  import { existsSync, lstatSync } from 'node:fs';
22
22
  import { randomUUID } from 'node:crypto';
23
23
  import { DEFAULT_YEAFT_DIR } from './init.js';
24
- import { buildDreamOutputSnapshot } from './dream/output-snapshot.js';
25
24
  import { Engine } from './engine.js';
26
25
  import { loadSession } from './session.js';
27
26
  import { loadAgentMCPConfig, loadConfig, loadMCPConfig } from './config.js';
@@ -220,19 +219,6 @@ function applyLiveLanguage(language) {
220
219
  try { session?.engine?.setLanguage?.(language); } catch { /* best-effort */ }
221
220
  }
222
221
 
223
- /**
224
- * Apply an Agent-level Dream toggle to an already loaded runtime.
225
- * This must never bootstrap a Session: config.json is the authoritative commit,
226
- * while the live scheduler update is only a best-effort cache refresh.
227
- */
228
- export function setLiveDreamEnabled(enabled) {
229
- const next = enabled !== false;
230
- if (session?.config && typeof session.config === 'object') {
231
- session.config.dream = { ...(session.config.dream || {}), enabled: next };
232
- }
233
- session?.dreamScheduler?.setEnabled?.(next);
234
- }
235
-
236
222
  function modelRefIdentity(value) {
237
223
  const text = String(value || '');
238
224
  const slash = text.indexOf('/');
@@ -355,30 +341,6 @@ export function __testSetThreadClassifier(fn) {
355
341
  threadClassifier = typeof fn === 'function' ? fn : defaultClassifyThread;
356
342
  }
357
343
 
358
- /**
359
- * Tracks scoped-dream triggers that are currently inflight, keyed by
360
- * sessionId. Used by `handleYeaftDreamTrigger` to reject any overlapping
361
- * scoped trigger rather than racing the sink-wrapping logic against
362
- * itself.
363
- *
364
- * Cross-group overlap is rejected (not just same-group): under the
365
- * existing dream scheduler a second concurrent trigger silently shares
366
- * the first's inflight promise and dropped its own scope filter. So
367
- * "B during A's run" doesn't actually produce a separate scoped pass
368
- * for B — letting B install a second sink wrapper would only mis-stamp
369
- * A's events with B's sessionId. Reporting B as an explicit skipped
370
- * result is the honest answer; the user can re-click after A settles.
371
- * @type {Set<string>}
372
- */
373
- const inflightScopedDreamGroups = new Set();
374
-
375
- async function sendDreamSnapshotForSession(sessionId, extra = {}) {
376
- const snapshot = await buildDreamOutputSnapshot(session, sessionId);
377
- if (!snapshot) return null;
378
- sendSessionEvent({ type: 'yeaft_dream_snapshot', ...extra, snapshot }, { sessionId });
379
- return snapshot;
380
- }
381
-
382
344
  function scheduleYeaftLoadHistoryMetadataReplay(sessionId) {
383
345
  const replaySession = session;
384
346
  const replayConversationId = yeaftConversationId;
@@ -423,9 +385,6 @@ function scheduleYeaftLoadHistoryMetadataReplay(sessionId) {
423
385
  }, { sessionId });
424
386
  if (sessionId) replayPendingUserPrompts(sessionId);
425
387
  sendSessionSnapshotBroadcast();
426
- if (sessionId && session === replaySession) {
427
- sendDreamSnapshotForSession(sessionId, { trigger: 'load_history' }).catch(() => null);
428
- }
429
388
  try {
430
389
  getVpStatusBroker().broadcastSnapshot();
431
390
  } catch (err) {
@@ -1198,7 +1157,7 @@ export function broadcastLanguageChange(language) {
1198
1157
  applyLiveLanguage(language);
1199
1158
  }
1200
1159
 
1201
- /** Query timeout in ms — abort if LLM doesn't respond within this window */
1160
+ /** Provider inactivity windows (not a total-duration limit on an LLM call). */
1202
1161
  const QUERY_TIMEOUT_MS = 120_000;
1203
1162
  const HIGH_REASONING_QUERY_TIMEOUT_MS = 300_000;
1204
1163
  /** AskUser is human-paced but must never pin a VP turn forever. */
@@ -1209,6 +1168,41 @@ function isHighReasoningEffort(effort) {
1209
1168
  return value === 'high' || value === 'xhigh' || value === 'max' || value === 'ultra';
1210
1169
  }
1211
1170
 
1171
+ /**
1172
+ * Per-query provider inactivity window, not a total LLM call deadline.
1173
+ * Hidden deltas only refresh an armed timer; tool/retry waits stay paused.
1174
+ * stop() is irreversible, including on abort, so late events cannot revive it.
1175
+ * @param {{ signal: AbortSignal, timeoutMs: number, onTimeout: Function }} options
1176
+ */
1177
+ function createQueryWatchdog({ signal, timeoutMs, onTimeout }) {
1178
+ let timer = null;
1179
+ let stopped = signal.aborted;
1180
+ const pause = () => {
1181
+ if (timer) clearTimeout(timer);
1182
+ timer = null;
1183
+ };
1184
+ const stop = () => {
1185
+ stopped = true;
1186
+ pause();
1187
+ signal.removeEventListener('abort', stop);
1188
+ };
1189
+ const reset = () => {
1190
+ if (stopped || signal.aborted) return;
1191
+ if (timer) {
1192
+ timer.refresh();
1193
+ return;
1194
+ }
1195
+ timer = setTimeout(() => {
1196
+ stop();
1197
+ onTimeout();
1198
+ }, timeoutMs);
1199
+ };
1200
+ // No throttling or trailing heartbeat: the actual last delta owns idle time.
1201
+ const touch = () => { if (timer) reset(); };
1202
+ if (!stopped) signal.addEventListener('abort', stop, { once: true });
1203
+ return { reset, pause, touch, stop };
1204
+ }
1205
+
1212
1206
  function queryTimeoutMsForSessionConfig(config = null) {
1213
1207
  return isHighReasoningEffort(config?.modelEffort) ? HIGH_REASONING_QUERY_TIMEOUT_MS : QUERY_TIMEOUT_MS;
1214
1208
  }
@@ -3978,7 +3972,7 @@ function buildVpPersona(vpId) {
3978
3972
  }
3979
3973
 
3980
3974
  /**
3981
- * Install the dream pipeline progress sink and runtime settings bridge.
3975
+ * Install task delivery and the runtime settings compatibility bridge.
3982
3976
  * Thread scheduling is owned by the group VP runtime below, not by mutable
3983
3977
  * threadStore settings. The old threadStore setters are kept only as ignored
3984
3978
  * compatibility shims for older clients.
@@ -3998,59 +3992,6 @@ export function installYeaftRuntimeBridge(s) {
3998
3992
  });
3999
3993
  }
4000
3994
 
4001
- // Forward dream pipeline progress events to the web debug panel.
4002
- //
4003
- // Group-id stamping is NO LONGER done here. It used to be: this sink
4004
- // read a module-level `activeScopedDreamGroupId` that
4005
- // `handleYeaftDreamTrigger({sessionId})` parked before awaiting the
4006
- // scope-filtered pass. That created a race when two scoped triggers
4007
- // overlapped (auto-tick during a manual click; or two manual clicks
4008
- // for different groups): the second handler's `finally` could clear
4009
- // the module slot while the first run was still emitting events,
4010
- // dropping the stamp from the tail of the first pass. The new design:
4011
- // `handleYeaftDreamTrigger` wraps THIS sink for the lifetime of the
4012
- // trigger to inject `sessionId` per-call (see that function below). The
4013
- // base sink is intentionally a pure passthrough.
4014
- //
4015
- // Bug 2: also forward turn_open / turn_close / loop events emitted by
4016
- // the dream pipeline so the debug panel shows dream LLM API calls.
4017
- s._dreamProgressSink = (evt) => {
4018
- try {
4019
- if (evt.type === 'turn_open' || evt.type === 'turn_close' || evt.type === 'loop') {
4020
- const tag = evt && evt.sessionId ? { sessionId: evt.sessionId } : {};
4021
- sendSessionEvent(evt, tag);
4022
- } else {
4023
- const out = { type: 'dream_progress', ...evt };
4024
- const tag = evt && evt.sessionId ? { sessionId: evt.sessionId } : {};
4025
- sendSessionEvent(out, tag);
4026
- }
4027
- } catch { /* never let event delivery throw */ }
4028
- };
4029
-
4030
- // Auto dream runs are triggered by the scheduler / nudges, not by the
4031
- // manual `handleYeaftDreamTrigger` path. Without this terminal sink the UI
4032
- // only saw progress debug events and could not restore the final dream
4033
- // output after switching sessions. Manual runs keep using their explicit
4034
- // handler below to avoid duplicate terminal events.
4035
- s._dreamResultSink = async (result = {}) => {
4036
- if (result?.trigger !== 'auto') return;
4037
- const normalized = normalizeDreamResult(result);
4038
- const processed = Array.isArray(result.sessions)
4039
- ? result.sessions.filter(row => row && row.status === 'triaged' && row.sessionId)
4040
- : [];
4041
- for (const sessionRow of processed) {
4042
- const sessionId = sessionRow.sessionId;
4043
- const snapshot = await buildDreamOutputSnapshot(session, sessionId).catch(() => null);
4044
- sendToServer({
4045
- type: 'yeaft_dream_result',
4046
- sessionId,
4047
- ...result,
4048
- ...normalized,
4049
- snapshot,
4050
- });
4051
- }
4052
- };
4053
-
4054
3995
  ctx.yeaftRuntimeSettings = {
4055
3996
  // No multi-thread settings to surface anymore. Stub for back-compat
4056
3997
  // with message-router's update_yeaft_settings branch — assignments are
@@ -4181,15 +4122,25 @@ function queueStreamTextDelta(hctx, text, envelope) {
4181
4122
  * todos, debug cards, and persistence all share the same boundary.
4182
4123
  *
4183
4124
  * @param {object} event — engine event (text_delta / tool_call / …)
4184
- * @param {{assistantTextParts:string[], toolCallsAccum:Array, toolResultsAccum:Array, thinkingBlocksAccum?:Array, resetQueryTimer:Function, pauseQueryTimer?:Function, markEngineTerminal?:Function, sessionId?:string, vpId?:string, turnId?:string}} hctx
4125
+ * @param {{assistantTextParts:string[], toolCallsAccum:Array, toolResultsAccum:Array, thinkingBlocksAccum?:Array, resetQueryTimer:Function, pauseQueryTimer?:Function, touchQueryTimer?:Function, stopQueryTimer?:Function, markEngineTerminal?:Function, sessionId?:string, vpId?:string, turnId?:string}} hctx
4185
4126
  */
4186
4127
  export function __testHandleEngineEvent(event, hctx) {
4187
4128
  return handleEngineEvent(event, hctx);
4188
4129
  }
4189
4130
 
4190
4131
  function handleEngineEvent(event, hctx) {
4132
+ // Hidden progress has no wire/history/status projection and must not flush
4133
+ // pending visible text or resume a paused/terminal provider watchdog.
4134
+ if (event.type === 'provider_activity') {
4135
+ hctx.touchQueryTimer?.();
4136
+ return;
4137
+ }
4138
+ if ((event.type === 'text_delta' || event.type === 'thinking_delta')
4139
+ && (typeof event.text !== 'string' || event.text.length === 0)) return;
4191
4140
  const terminalTurnEnd = event.type === 'turn_end' && event.terminal === true;
4192
4141
  const managesQueryTimer = terminalTurnEnd
4142
+ || event.type === 'aborted'
4143
+ || event.type === 'usage'
4193
4144
  || event.type === 'tool_start'
4194
4145
  || event.type === 'tool_end'
4195
4146
  || event.type === 'async_task_wait_start'
@@ -4394,6 +4345,7 @@ function handleEngineEvent(event, hctx) {
4394
4345
  break;
4395
4346
 
4396
4347
  case 'aborted':
4348
+ (hctx.stopQueryTimer || hctx.pauseQueryTimer)?.();
4397
4349
  if (typeof hctx.markEngineTerminal === 'function') {
4398
4350
  hctx.markEngineTerminal('aborted', { reason: event.reason || 'external' });
4399
4351
  }
@@ -4403,8 +4355,8 @@ function handleEngineEvent(event, hctx) {
4403
4355
  // Most engine turn_end events are internal loop boundaries. Only the
4404
4356
  // explicit terminal event precedes post-turn persistence/maintenance;
4405
4357
  // stop the user-query silence watchdog before that best-effort work.
4406
- if (event.terminal && typeof hctx.pauseQueryTimer === 'function') {
4407
- hctx.pauseQueryTimer();
4358
+ if (event.terminal) {
4359
+ (hctx.stopQueryTimer || hctx.pauseQueryTimer)?.();
4408
4360
  }
4409
4361
  // A normal tool_use stop means "run tools, then call the adapter again",
4410
4362
  // so it must NOT end the VP's visible turn. An aborted turn is terminal
@@ -4562,17 +4514,6 @@ function handleEngineEvent(event, hctx) {
4562
4514
  }, envelope);
4563
4515
  break;
4564
4516
 
4565
- case 'dream_memory_loaded':
4566
- sendSessionEvent({
4567
- type: 'dream_memory_loaded',
4568
- turnId: event.turnId,
4569
- vpId: event.vpId || null,
4570
- sessionId: event.sessionId || null,
4571
- loadedInto: event.loadedInto || 'system_prompt.memory',
4572
- resident: Array.isArray(event.resident) ? event.resident : [],
4573
- }, envelope);
4574
- break;
4575
-
4576
4517
  case 'memory_adjust':
4577
4518
  sendSessionEvent({
4578
4519
  type: 'memory_adjust',
@@ -5368,7 +5309,6 @@ function startSessionLoadInBackground({ sessionId = null, sessionMeta = null, pe
5368
5309
  const hydrateStart = perfNowMs();
5369
5310
  setGroupHistory(sessionId, hydrateGroupHistory(sessionId));
5370
5311
  if (typeof traceDuration === 'function') traceDuration('history.hydrate_group_history', hydrateStart, { detail: { background: true } });
5371
- sendDreamSnapshotForSession(sessionId, { trigger: 'load_history' }).catch(() => null);
5372
5312
  }
5373
5313
  return loaded;
5374
5314
  })
@@ -5388,7 +5328,7 @@ function startSessionLoadInBackground({ sessionId = null, sessionMeta = null, pe
5388
5328
  /**
5389
5329
  * Wrap {@link runVpTurn} with a second-stage abort escalation.
5390
5330
  *
5391
- * The in-turn watchdog is activity based: every engine event resets its
5331
+ * The in-turn watchdog is activity based: content progress resets its
5392
5332
  * silence timer, and only a genuinely silent turn calls `vpAbort.abort()`.
5393
5333
  * This wrapper must therefore start its grace period from that abort signal,
5394
5334
  * not from turn enqueue. A fixed enqueue deadline incorrectly terminates
@@ -5613,25 +5553,16 @@ async function runVpTurn({ prompt, promptParts = null, sessionId, vpId, threadId
5613
5553
  };
5614
5554
 
5615
5555
  try {
5616
- if (session?.dreamScheduler) {
5617
- session.dreamScheduler.noteUserMessage();
5618
- }
5619
-
5620
- let queryTimer = null;
5621
5556
  const queryTimeoutMs = queryTimeoutMsForSession(sessionId);
5622
- const pauseQueryTimer = () => {
5623
- if (queryTimer) clearTimeout(queryTimer);
5624
- queryTimer = null;
5625
- };
5626
- const resetQueryTimer = () => {
5627
- pauseQueryTimer();
5628
- queryTimer = setTimeout(() => {
5629
- if (!vpAbort.signal.aborted) {
5630
- console.error(`[Yeaft] query timeout after ${queryTimeoutMs / 1000}s of silence — aborting VP ${vpId}`);
5631
- try { vpAbort.abort(); } catch { /* best-effort */ }
5632
- }
5633
- }, queryTimeoutMs);
5634
- };
5557
+ const watchdog = createQueryWatchdog({
5558
+ signal: vpAbort.signal,
5559
+ timeoutMs: queryTimeoutMs,
5560
+ onTimeout: () => {
5561
+ console.error(`[Yeaft] query timeout after ${queryTimeoutMs / 1000}s of silence — aborting VP ${vpId}`);
5562
+ try { vpAbort.abort(); } catch { /* best-effort */ }
5563
+ },
5564
+ });
5565
+ const { reset: resetQueryTimer, pause: pauseQueryTimer } = watchdog;
5635
5566
  resetQueryTimer();
5636
5567
 
5637
5568
  // Emit turn_start so frontend can create the message block.
@@ -5715,6 +5646,8 @@ async function runVpTurn({ prompt, promptParts = null, sessionId, vpId, threadId
5715
5646
  thinkingBlocksAccum,
5716
5647
  resetQueryTimer,
5717
5648
  pauseQueryTimer,
5649
+ touchQueryTimer: watchdog.touch,
5650
+ stopQueryTimer: watchdog.stop,
5718
5651
  sessionId,
5719
5652
  vpId,
5720
5653
  turnId,
@@ -5766,10 +5699,7 @@ async function runVpTurn({ prompt, promptParts = null, sessionId, vpId, threadId
5766
5699
  // Human think time is not engine silence. Pause the query watchdog,
5767
5700
  // but keep a separate bounded AskUser lifetime so an abandoned card
5768
5701
  // cannot pin the VP forever.
5769
- if (queryTimer) {
5770
- clearTimeout(queryTimer);
5771
- queryTimer = null;
5772
- }
5702
+ pauseQueryTimer();
5773
5703
  const pending = {
5774
5704
  resolve,
5775
5705
  reject,
@@ -5816,7 +5746,6 @@ async function runVpTurn({ prompt, promptParts = null, sessionId, vpId, threadId
5816
5746
  messageType: event?.type || null,
5817
5747
  });
5818
5748
  }
5819
- resetQueryTimer();
5820
5749
  handleEngineEvent(event, handlerCtx);
5821
5750
  }
5822
5751
  if (perfTraceId) {
@@ -5891,7 +5820,7 @@ async function runVpTurn({ prompt, promptParts = null, sessionId, vpId, threadId
5891
5820
  emitVpTurnEnd('end_turn');
5892
5821
  }
5893
5822
  } finally {
5894
- if (queryTimer) clearTimeout(queryTimer);
5823
+ watchdog.stop();
5895
5824
  }
5896
5825
  } catch (err) {
5897
5826
  if (escalationState?.escalated) return;
@@ -6526,195 +6455,17 @@ export function __testAppendTurnToSessionHistory(...args) {
6526
6455
  return appendTurnToSessionHistory(...args);
6527
6456
  }
6528
6457
 
6529
- /**
6530
- * Manual dream trigger.
6531
- *
6532
- * Two call shapes, both routed through this single handler:
6533
- *
6534
- * { type: 'yeaft_dream_trigger', vpId } — per-VP trigger (legacy
6535
- * VP-detail page button). Fires an unscoped dream pass; the result
6536
- * event is tagged with `vpId` so the per-VP store row updates.
6537
- *
6538
- * { type: 'yeaft_dream_trigger', sessionId } — per-GROUP trigger (new
6539
- * in v0.1.754 — added so users can manually kick dream for a group
6540
- * after seeing the Resident layer stuck on the bootstrap seed).
6541
- * Fires a scope-filtered pass via `triggerDreamForScopes(['sessions/X'])`
6542
- * so unrelated groups don't get processed; the result event is
6543
- * tagged with `sessionId` for the per-session UI row.
6544
- *
6545
- * Backwards-compat: when neither field is set, defaults to `vpId='default'`
6546
- * which matches the pre-v0.1.754 behavior.
6547
- */
6548
- function resolveDreamTriggerSessionId(msg = {}) {
6549
- return typeof msg.sessionId === 'string' && msg.sessionId
6550
- ? msg.sessionId
6551
- : (typeof msg.groupId === 'string' && msg.groupId ? msg.groupId : null);
6552
- }
6553
-
6554
- export function normalizeDreamResult(result) {
6555
- const sessions = Array.isArray(result?.sessions) ? result.sessions : [];
6556
- const targets = Array.isArray(result?.targets) ? result.targets : [];
6557
- const sessionsProcessed = sessions.filter(g => g && g.status === 'triaged').length;
6558
- const skippedSessions = sessions.filter(g => g && g.status === 'skipped');
6559
- const sessionsSkipped = skippedSessions.length;
6560
- const targetsApplied = targets.filter(t => t && t.status === 'done').length;
6561
- const targetErrors = targets
6562
- .filter(t => t && t.status === 'error')
6563
- .map(t => ({ target: t.target || null, error: t.error || 'unknown' }));
6564
- const hardError = result?.error || null;
6565
- const explicitSkipped = result?.skipped === true;
6566
- const skipped = !hardError && (explicitSkipped || (sessionsProcessed === 0 && targetsApplied === 0));
6567
- const skippedReason = skipped
6568
- ? (result?.skippedReason || skippedSessions[0]?.reason || 'no-targets-applied')
6569
- : null;
6570
- const trigger = result?.trigger || null;
6571
- const success = !hardError && targetErrors.length === 0 && !skipped && targetsApplied > 0;
6572
-
6573
- return {
6574
- success,
6575
- durationMs: Number.isFinite(Number(result?.durationMs)) ? Number(result.durationMs) : 0,
6576
- llmCallCount: Number.isFinite(Number(result?.llmCallCount)) ? Number(result.llmCallCount) : 0,
6577
- inputTokens: Number.isFinite(Number(result?.inputTokens)) ? Number(result.inputTokens) : 0,
6578
- outputTokens: Number.isFinite(Number(result?.outputTokens)) ? Number(result.outputTokens) : 0,
6579
- totalTokens: Number.isFinite(Number(result?.totalTokens)) ? Number(result.totalTokens) : 0,
6580
- metrics: result?.metrics || null,
6581
- passBreakdown: result?.passBreakdown || result?.metrics?.passBreakdown || null,
6582
- skipped,
6583
- skippedReason,
6584
- sessionsProcessed,
6585
- sessionsSkipped,
6586
- targetsApplied,
6587
- targetErrors,
6588
- entriesCreated: targetsApplied,
6589
- lastDreamAt: result?.startedAt || new Date().toISOString(),
6590
- trigger,
6591
- error: hardError || (targetErrors[0]?.error || null),
6592
- };
6593
- }
6594
-
6458
+ /** Reject old clients explicitly without loading a Session or touching Dream data. */
6595
6459
  export async function handleYeaftDreamTrigger(msg = {}) {
6596
- // Resolve tag up-front so EVERY outbound envelope (including the
6597
- // scheduler-uninitialised early-return below) carries `sessionId` /
6598
- // `vpId`. Without this the frontend's `applyDreamResult` couldn't
6599
- // route the error event back to the right row and the per-group
6600
- // "Run dream now" button would stay stuck on "Running…" forever
6601
- // (review feedback from PR #757).
6602
- const sessionId = resolveDreamTriggerSessionId(msg);
6603
- const vpId = !sessionId ? (msg.vpId || 'default') : null;
6604
- const tag = sessionId ? { sessionId } : { vpId };
6605
-
6606
- if (!session?.dreamScheduler) {
6607
- const error = 'Dream scheduler not initialized — session not loaded.';
6608
- sendToServer({
6609
- type: 'yeaft_dream_result',
6610
- ...tag,
6611
- ...normalizeDreamResult({ error }),
6612
- });
6613
- return;
6614
- }
6615
-
6616
- // Concurrent-trigger guard for scoped runs. Two scoped clicks (same
6617
- // group or different) overlapping the same inflight pass used to set
6618
- // the module-level sessionId slot, race the sink wrapping, and let the
6619
- // second `finally` restore the original sink while the first run was
6620
- // still emitting events. We now refuse scoped triggers while ANY dream
6621
- // pass is already running: a scoped manual click during an unscoped
6622
- // auto run must not install `_dreamActiveGroupId` or wrap the sink,
6623
- // otherwise auto-run events can be persisted under the clicked group.
6624
- // The scheduler also short-circuits the underlying run for same-group,
6625
- // and a different group's filter would have been silently dropped
6626
- // anyway (see dream/schedule.js inflight reuse), so the user-facing
6627
- // semantics are unchanged ("you already asked").
6628
- if (sessionId && (inflightScopedDreamGroups.size > 0 || session.dreamScheduler.isRunning)) {
6629
- const skippedResult = {
6630
- skipped: true,
6631
- skippedReason: 'already-running',
6632
- trigger: msg.manual === false ? 'auto' : 'manual',
6633
- };
6634
- sendToServer({
6635
- type: 'yeaft_dream_result',
6636
- ...tag,
6637
- ...skippedResult,
6638
- ...normalizeDreamResult(skippedResult),
6639
- });
6640
- return;
6641
- }
6642
-
6643
- // Per-call sink wrapper. For scoped runs we install a closure that
6644
- // injects this trigger's sessionId onto top-level events the runner
6645
- // emits without one (start/merge/done), then delegates to the
6646
- // original passthrough sink. The wrapper lives only for the lifetime
6647
- // of this trigger and is restored in `finally`; concurrent calls for
6648
- // OTHER sessionIds chain (last-installed wins) but each restoration
6649
- // unwinds back to its predecessor.
6650
- const originalSink = session?._dreamProgressSink;
6651
- if (sessionId) session._dreamActiveGroupId = sessionId;
6652
- if (sessionId && typeof originalSink === 'function') {
6653
- inflightScopedDreamGroups.add(sessionId);
6654
- session._dreamProgressSink = (evt) => {
6655
- try {
6656
- const stamped = evt && evt.sessionId
6657
- ? evt
6658
- : { ...evt, sessionId };
6659
- originalSink(stamped);
6660
- } catch { /* never let event delivery throw */ }
6661
- };
6662
- }
6663
-
6664
- try {
6665
- sendToServer({
6666
- type: 'yeaft_dream_status',
6667
- ...tag,
6668
- status: 'running',
6669
- });
6670
-
6671
- const result = sessionId
6672
- ? await session.dreamScheduler.triggerDreamForScopes([`sessions/${sessionId}`])
6673
- : await session.dreamScheduler.triggerDreamNow();
6674
-
6675
- const normalized = normalizeDreamResult(result);
6676
- const snapshot = sessionId
6677
- ? await buildDreamOutputSnapshot(session, sessionId).catch(() => null)
6678
- : null;
6679
-
6680
- // Spread `result` FIRST so normalized fields (success, skipped,
6681
- // skippedReason, sessionsProcessed, sessionsSkipped, targetsApplied,
6682
- // targetErrors, entriesCreated, lastDreamAt) authoritatively shadow
6683
- // anything the runner might grow
6684
- // with the same name. Today there is no collision (runner.js returns
6685
- // { groups, targets, startedAt, error?, skipped? }) but the failure
6686
- // mode of the alternative ordering is silent — review feedback from
6687
- // PR #743.
6688
- //
6689
- // This `yeaft_dream_result` envelope is the SOLE terminal signal for
6690
- // a dream pass. The chat-store projects it into BOTH `yeaftDreamLatest`
6691
- // (final tally row) AND `yeaftDreamEvents` (ring-buffer terminal
6692
- // marker), so we no longer mirror a synthetic `phase:'result'`
6693
- // dream_progress event — that mirror used to race the
6694
- // `yeaftDreamLatest` writer and flip the success row back to
6695
- // 'running' (Critical reviewer finding pre-merge).
6696
- sendToServer({
6697
- type: 'yeaft_dream_result',
6698
- ...tag,
6699
- ...result,
6700
- ...normalized,
6701
- ...(snapshot ? { snapshot } : {}),
6702
- });
6703
- } catch (err) {
6704
- const error = err?.message || String(err);
6705
- sendToServer({
6706
- type: 'yeaft_dream_result',
6707
- ...tag,
6708
- ...normalizeDreamResult({ error }),
6709
- });
6710
- } finally {
6711
- // Restore the original sink and release the per-group inflight lock.
6712
- if (sessionId && session?._dreamActiveGroupId === sessionId) session._dreamActiveGroupId = null;
6713
- if (sessionId && typeof originalSink === 'function') {
6714
- session._dreamProgressSink = originalSink;
6715
- inflightScopedDreamGroups.delete(sessionId);
6716
- }
6717
- }
6460
+ const sessionId = msg.sessionId || msg.groupId || null;
6461
+ sendToServer({
6462
+ type: 'yeaft_dream_result',
6463
+ ...(sessionId ? { sessionId } : { vpId: msg.vpId || 'default' }),
6464
+ success: false,
6465
+ skipped: true,
6466
+ skippedReason: 'disabled',
6467
+ error: 'Dream is disabled.',
6468
+ });
6718
6469
  }
6719
6470
 
6720
6471
  /**
@@ -6793,7 +6544,7 @@ export async function handleYeaftFetchToolStats(_msg = {}) {
6793
6544
  */
6794
6545
  export async function handleYeaftFetchDebugHistory(msg = {}) {
6795
6546
  const limit = Number.isFinite(msg?.limit) ? Number(msg.limit) : 10;
6796
- const dreamLimit = Number.isFinite(msg?.dreamLimit) ? Number(msg.dreamLimit) : 5;
6547
+ const dreamLimit = 0; // Older callers cannot request retired Dream history.
6797
6548
  const sessionId = typeof msg?.sessionId === 'string' && msg.sessionId ? msg.sessionId : null;
6798
6549
  const threadId = typeof msg?.threadId === 'string' && msg.threadId ? msg.threadId : null;
6799
6550
  const search = typeof msg?.search === 'string' ? msg.search.trim() : '';
@@ -6804,7 +6555,6 @@ export async function handleYeaftFetchDebugHistory(msg = {}) {
6804
6555
  const detailTurnId = typeof msg?.detailTurnId === 'string' && msg.detailTurnId ? msg.detailTurnId : null;
6805
6556
  let loops = [];
6806
6557
  let turns = [];
6807
- let dreamEvents = [];
6808
6558
  let projection = null;
6809
6559
  let hasMore = false;
6810
6560
  try {
@@ -6812,14 +6562,12 @@ export async function handleYeaftFetchDebugHistory(msg = {}) {
6812
6562
  const out = await session.trace.fetchTurnDebug({ sessionId, turnId: detailTurnId, dreamLimit });
6813
6563
  loops = Array.isArray(out?.loops) ? out.loops : [];
6814
6564
  turns = Array.isArray(out?.turns) ? out.turns : [];
6815
- dreamEvents = Array.isArray(out?.dreamEvents) ? out.dreamEvents : [];
6816
6565
  projection = out?.projection && typeof out.projection === 'object' ? out.projection : null;
6817
6566
  hasMore = false;
6818
6567
  } else if (session?.trace && typeof session.trace.fetchRecentDebugHistory === 'function') {
6819
6568
  const out = await session.trace.fetchRecentDebugHistory({ limit, dreamLimit, sessionId, threadId, indexOnly, detailTurnId, search });
6820
6569
  loops = Array.isArray(out?.loops) ? out.loops : [];
6821
6570
  turns = Array.isArray(out?.turns) ? out.turns : [];
6822
- dreamEvents = Array.isArray(out?.dreamEvents) ? out.dreamEvents : [];
6823
6571
  projection = out?.projection && typeof out.projection === 'object' ? out.projection : null;
6824
6572
  hasMore = !!out?.hasMore;
6825
6573
  }
@@ -6846,7 +6594,7 @@ export async function handleYeaftFetchDebugHistory(msg = {}) {
6846
6594
  type: 'yeaft_debug_history',
6847
6595
  loops,
6848
6596
  turns,
6849
- dreamEvents,
6597
+ dreamEvents: [],
6850
6598
  ...(projection ? { projection } : {}),
6851
6599
  requestId,
6852
6600
  requestKind,
@@ -7368,8 +7116,8 @@ export async function handleYeaftLoadHistory(msg) {
7368
7116
  }
7369
7117
  historyAlreadyReplayed = true;
7370
7118
 
7371
- // Full runtime boot can be expensive (memory FTS sync, skills, MCP, dream
7372
- // boot checks). It is not needed to render persisted history, so keep this
7119
+ // Full runtime boot can be expensive (skills and MCP discovery).
7120
+ // It is not needed to render persisted history, so keep this
7373
7121
  // request short and let message-send await the same single-flight boot when
7374
7122
  // the user actually submits a turn.
7375
7123
  startSessionLoadInBackground({ sessionId, sessionMeta: sessionMetaForRuntime, perfTraceId, traceDuration, tracePerf });
@@ -7394,7 +7142,7 @@ export async function handleYeaftLoadHistory(msg) {
7394
7142
  // Always replay session_ready so refresh / reconnect rebuilds UI state, but
7395
7143
  // never make the history response wait for bulky metadata snapshots. The
7396
7144
  // first visible chunk has already been sent above; defer metadata to the next
7397
- // tick so the browser can paint messages before VP/session/dream snapshots.
7145
+ // tick so the browser can paint messages before VP/session snapshots.
7398
7146
  if (session) scheduleYeaftLoadHistoryMetadataReplay(sessionId);
7399
7147
 
7400
7148
  if (historyAlreadyReplayed) {
@@ -8293,7 +8041,6 @@ export const __testHooks = {
8293
8041
  return getVpStatusBroker().transition(status);
8294
8042
  },
8295
8043
  decorateSessionsWithRuntimeState,
8296
- resolveDreamTriggerSessionId,
8297
8044
  async loadProjectRuntime(workDir) {
8298
8045
  return loadProjectRuntime(workDir);
8299
8046
  },
@@ -8360,6 +8107,7 @@ export const __testHooks = {
8360
8107
  projectRuntimeCount() {
8361
8108
  return projectRuntimes.size;
8362
8109
  },
8110
+ createQueryWatchdog,
8363
8111
  queryTimeoutMsForSessionConfig,
8364
8112
  queryTimeoutMsForSession,
8365
8113
  seedQueuedVpTurn({ sessionId = 'session-test', vpId = 'vp-test', threadId = 'main', turnId = 'turn-test' } = {}) {