@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.
- package/connection/message-router.js +8 -22
- package/local-runtime/server/handlers/agent-output.js +12 -34
- package/local-runtime/server/handlers/agent-sync.js +4 -15
- package/local-runtime/server/handlers/client-conversation.js +7 -2
- package/local-runtime/server/handlers/client-misc.js +4 -6
- package/local-runtime/version.json +1 -1
- package/local-runtime/web/app.bundle.js +101 -298
- package/local-runtime/web/app.bundle.js.gz +0 -0
- package/local-runtime/web/index.html +2 -2
- package/local-runtime/web/style.bundle.css +1 -1
- package/local-runtime/web/style.bundle.css.gz +0 -0
- package/package.json +1 -1
- package/yeaft/cli-session-runner.js +2 -2
- package/yeaft/cli.js +1 -1
- package/yeaft/config.js +3 -1
- package/yeaft/debug-trace.js +12 -135
- package/yeaft/engine.js +6 -0
- package/yeaft/init.js +3 -22
- package/yeaft/llm/adapter.js +2 -1
- package/yeaft/llm/anthropic.js +13 -3
- package/yeaft/llm/openai-responses.js +9 -2
- package/yeaft/migrate/sessions.js +52 -47
- package/yeaft/session.js +15 -108
- package/yeaft/sessions/seed-default.js +0 -26
- package/yeaft/sessions/session-crud.js +6 -29
- package/yeaft/sub-agent/runner.js +9 -8
- package/yeaft/web-bridge.js +81 -333
- package/yeaft/work-center/runner.js +5 -98
package/yeaft/web-bridge.js
CHANGED
|
@@ -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
|
-
/**
|
|
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
|
|
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
|
|
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:
|
|
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
|
|
5623
|
-
|
|
5624
|
-
|
|
5625
|
-
|
|
5626
|
-
|
|
5627
|
-
|
|
5628
|
-
|
|
5629
|
-
|
|
5630
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
6597
|
-
|
|
6598
|
-
|
|
6599
|
-
|
|
6600
|
-
|
|
6601
|
-
|
|
6602
|
-
|
|
6603
|
-
|
|
6604
|
-
|
|
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 =
|
|
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 (
|
|
7372
|
-
//
|
|
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
|
|
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' } = {}) {
|