@yeaft/webchat-agent 1.0.507 → 1.0.509
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 +1 -1
- package/local-runtime/server/handlers/agent-sync.js +1 -0
- package/local-runtime/server/handlers/client-conversation.js +38 -2
- package/local-runtime/server/handlers/client-misc.js +1 -1
- package/local-runtime/server/handlers/client-workbench.js +7 -1
- package/local-runtime/server/workbench-route.js +11 -4
- package/local-runtime/version.json +1 -1
- package/local-runtime/web/app.bundle.js +212 -99
- 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/config-api.js +59 -6
- package/yeaft/config.js +3 -3
- package/yeaft/conversation/history-index-worker.js +3 -2
- package/yeaft/conversation/history-index.js +6 -4
- package/yeaft/conversation/persist.js +37 -0
- package/yeaft/conversation/recall-relevance.js +44 -11
- package/yeaft/engine.js +38 -16
- package/yeaft/history-window.js +48 -53
- package/yeaft/prompts.js +3 -0
- package/yeaft/session.js +16 -45
- package/yeaft/sub-agent/runner.js +6 -4
- package/yeaft/web-bridge.js +130 -39
package/yeaft/history-window.js
CHANGED
|
@@ -27,6 +27,7 @@ export const DEFAULT_RUNTIME_CACHE_TURN_CAP = 25;
|
|
|
27
27
|
export const DEFAULT_RUNTIME_CACHE_TOKEN_BUDGET = 32768;
|
|
28
28
|
export const DEFAULT_RUNTIME_CACHE_MESSAGE_CAP = 256;
|
|
29
29
|
|
|
30
|
+
const MINIMUM_RECENT_PROVIDER_TURNS = 5;
|
|
30
31
|
const IMAGE_PART_TOKEN_COST = 1024;
|
|
31
32
|
const DOCUMENT_PART_TOKEN_COST = 2048;
|
|
32
33
|
const CONTENT_PART_FRAME_TOKENS = 2;
|
|
@@ -565,10 +566,11 @@ function truncateToolResultsForModel(messages, options = {}) {
|
|
|
565
566
|
|
|
566
567
|
const HISTORY_SOURCE_INDEX = Symbol('historySourceIndex');
|
|
567
568
|
|
|
568
|
-
function withHistorySourceIndexes(messages) {
|
|
569
|
+
function withHistorySourceIndexes(messages, offset = 0) {
|
|
569
570
|
return messages.map((message, index) => (
|
|
570
571
|
message && typeof message === 'object'
|
|
571
|
-
? { ...message, [HISTORY_SOURCE_INDEX]:
|
|
572
|
+
? { ...message, [HISTORY_SOURCE_INDEX]: Number.isInteger(message[HISTORY_SOURCE_INDEX])
|
|
573
|
+
? message[HISTORY_SOURCE_INDEX] : offset + index }
|
|
572
574
|
: message
|
|
573
575
|
));
|
|
574
576
|
}
|
|
@@ -795,12 +797,13 @@ function bucketTextMessages(messages) {
|
|
|
795
797
|
}
|
|
796
798
|
|
|
797
799
|
function bucketTurn(messages, index, supplied = {}) {
|
|
798
|
-
const
|
|
799
|
-
const
|
|
800
|
-
const
|
|
800
|
+
const indexedMessages = withHistorySourceIndexes(messages, index ?? 0);
|
|
801
|
+
const user = indexedMessages.find(bucketUserBoundary);
|
|
802
|
+
const sourceIds = [...new Set(indexedMessages.flatMap(bucketSourceIds))];
|
|
803
|
+
const keys = [...new Set(indexedMessages.filter(bucketUserBoundary).flatMap(bucketUserKeys))];
|
|
801
804
|
if (supplied.id) keys.push(`turn:${supplied.id}`);
|
|
802
805
|
if (keys.length === 0 && index != null) keys.push(`snapshot:${index}`);
|
|
803
|
-
const text = bucketTextMessages(
|
|
806
|
+
const text = bucketTextMessages(indexedMessages);
|
|
804
807
|
return {
|
|
805
808
|
...supplied,
|
|
806
809
|
id: supplied.id || keys[0] || `snapshot:${index}`,
|
|
@@ -808,7 +811,7 @@ function bucketTurn(messages, index, supplied = {}) {
|
|
|
808
811
|
sourceIds,
|
|
809
812
|
userSeq: Number.isFinite(supplied.userSeq) ? supplied.userSeq : bucketSequence(user),
|
|
810
813
|
index,
|
|
811
|
-
messages,
|
|
814
|
+
messages: indexedMessages,
|
|
812
815
|
text,
|
|
813
816
|
tokens: estimateMessagesTokens(text),
|
|
814
817
|
};
|
|
@@ -893,19 +896,21 @@ function describeBucket(turns, messages = turns.flatMap(turn => turn.text)) {
|
|
|
893
896
|
|
|
894
897
|
/**
|
|
895
898
|
* Recompute provider history from untrimmed candidates; never mutate/cache the
|
|
896
|
-
* result in the transcript. Past human
|
|
897
|
-
*
|
|
899
|
+
* result in the transcript. Past human turn boundaries are retained when the
|
|
900
|
+
* configured recent window fits; tools are optional enrichment, newest first.
|
|
898
901
|
*
|
|
899
902
|
* The active turn is outside both buckets and consumes the global budget first.
|
|
900
|
-
* Its opening user row is protected
|
|
901
|
-
*
|
|
902
|
-
*
|
|
903
|
+
* Its opening user row is protected. Recent text stays complete: reduce the
|
|
904
|
+
* oldest end of the suffix down to five turns (or all available for a new
|
|
905
|
+
* Session); fail closed if that floor cannot fit. Related recall only uses
|
|
906
|
+
* remaining budget and stays optional and complete. External recall must have
|
|
907
|
+
* comparable userSeq/source identities to establish
|
|
903
908
|
* that it predates recent/current history; unknown chronology fails closed.
|
|
904
909
|
*
|
|
905
910
|
* @param {Array<object>} snapshot Untrimmed history plus the active execution.
|
|
906
911
|
* @param {{ prompt?: string, relatedTurns?: Array<object>, recentTurnCap?: number,
|
|
907
912
|
* relatedTurnCap?: number, messageTokenBudget?: number, maxMessageCount?: number,
|
|
908
|
-
* language?: string, currentTurnStartIndex?: number,
|
|
913
|
+
* keepToolTurns?: number, language?: string, currentTurnStartIndex?: number,
|
|
909
914
|
* scoreTurn?: (turn: object, prompt: string) => (number|object) }} [options]
|
|
910
915
|
* @returns {{messages: Array<object>, meta: object}}
|
|
911
916
|
*/
|
|
@@ -914,7 +919,8 @@ export function buildHistoryBuckets(snapshot, options = {}) {
|
|
|
914
919
|
const tokenBudget = bucketCap(options.messageTokenBudget, DEFAULT_MESSAGE_TOKEN_BUDGET);
|
|
915
920
|
const messageCap = bucketCap(options.maxMessageCount, DEFAULT_RUNTIME_CACHE_MESSAGE_CAP);
|
|
916
921
|
const recentCap = bucketCap(options.recentTurnCap, 20);
|
|
917
|
-
const relatedCap = bucketCap(options.relatedTurnCap,
|
|
922
|
+
const relatedCap = bucketCap(options.relatedTurnCap, 5, 5);
|
|
923
|
+
const keepToolTurns = bucketCap(options.keepToolTurns, DEFAULT_KEEP_TOOL_TURNS);
|
|
918
924
|
const allTurns = splitBucketTurns(source);
|
|
919
925
|
const currentStart = Number.isInteger(options.currentTurnStartIndex)
|
|
920
926
|
? Math.max(0, Math.min(source.length, options.currentTurnStartIndex))
|
|
@@ -1017,28 +1023,26 @@ export function buildHistoryBuckets(snapshot, options = {}) {
|
|
|
1017
1023
|
// past-turn boundary is still a safe fence; never guess from text/time.
|
|
1018
1024
|
|| (turn.index == null && currentIdentity.userSeq == null && past.length > 0
|
|
1019
1025
|
&& bucketBefore(turn, past.at(-1)))));
|
|
1020
|
-
//
|
|
1021
|
-
//
|
|
1022
|
-
const
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
const
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
return selected;
|
|
1026
|
+
// Recent text has first claim on history budget. Drop whole oldest turns,
|
|
1027
|
+
// never reserve space for recall or truncate text to manufacture turn counts.
|
|
1028
|
+
const recent = [];
|
|
1029
|
+
let recentTokens = 0;
|
|
1030
|
+
let recentRows = 0;
|
|
1031
|
+
const recentCandidates = recentCap > 0 ? past.slice(-recentCap) : [];
|
|
1032
|
+
for (let index = recentCandidates.length - 1; index >= 0; index -= 1) {
|
|
1033
|
+
const turn = recentCandidates[index];
|
|
1034
|
+
if (recentTokens + turn.tokens > availableTokens
|
|
1035
|
+
|| recentRows + turn.text.length > availableRows) break;
|
|
1036
|
+
recent.unshift(turn);
|
|
1037
|
+
recentTokens += turn.tokens;
|
|
1038
|
+
recentRows += turn.text.length;
|
|
1039
|
+
}
|
|
1040
|
+
const minimumRecent = Math.min(MINIMUM_RECENT_PROVIDER_TURNS, recentCandidates.length);
|
|
1041
|
+
if (recent.length < minimumRecent) {
|
|
1042
|
+
const error = new Error(`Context budget cannot retain ${minimumRecent} complete recent history turns`);
|
|
1043
|
+
error.code = 'HISTORY_RECENT_BUDGET_EXCEEDED';
|
|
1044
|
+
throw error;
|
|
1040
1045
|
}
|
|
1041
|
-
let recent = selectRecent(availableTokens - reserve, availableRows - reservedRows);
|
|
1042
1046
|
let remainingTokens = availableTokens - recent.reduce((total, turn) => total + turn.tokens, 0);
|
|
1043
1047
|
let remainingRows = availableRows - recent.reduce((total, turn) => total + turn.text.length, 0);
|
|
1044
1048
|
const related = [];
|
|
@@ -1056,29 +1060,19 @@ export function buildHistoryBuckets(snapshot, options = {}) {
|
|
|
1056
1060
|
remainingTokens -= turn.tokens;
|
|
1057
1061
|
remainingRows -= turn.text.length;
|
|
1058
1062
|
}
|
|
1059
|
-
if (!related.length) recent = selectRecent(availableTokens);
|
|
1060
|
-
else {
|
|
1061
|
-
// Pay the actual related cost, not the provisional reserve. Expand the
|
|
1062
|
-
// recent suffix only while every related turn remains strictly older.
|
|
1063
|
-
const expanded = selectRecent(
|
|
1064
|
-
availableTokens - related.reduce((sum, turn) => sum + turn.tokens, 0),
|
|
1065
|
-
availableRows - related.reduce((sum, turn) => sum + turn.text.length, 0),
|
|
1066
|
-
);
|
|
1067
|
-
while (expanded.length > recent.length && related.some(turn => (
|
|
1068
|
-
!bucketBefore(turn, expanded[0]) || bucketOverlap(turn, expanded[0])
|
|
1069
|
-
))) expanded.shift();
|
|
1070
|
-
if (expanded.length > recent.length) recent = expanded;
|
|
1071
|
-
}
|
|
1072
1063
|
related.sort((a, b) => bucketBefore(a, b) ? -1 : bucketBefore(b, a) ? 1 : 0);
|
|
1073
1064
|
|
|
1074
1065
|
const relatedMessages = related.flatMap(turn => turn.text);
|
|
1075
|
-
const
|
|
1076
|
-
.filter(isVisibleConversationRow));
|
|
1077
|
-
const recentBaseline = bucketTextMessages(recentText);
|
|
1066
|
+
const recentBaseline = recent.flatMap(turn => turn.text);
|
|
1078
1067
|
// Enrich only after both complete-text buckets and the active turn are paid.
|
|
1068
|
+
// Tool protocol is useful only for immediate continuity; unlike visible text,
|
|
1069
|
+
// it never reaches farther back than the configured recent tool window.
|
|
1070
|
+
const recentToolSource = keepToolTurns > 0
|
|
1071
|
+
? recent.slice(-keepToolTurns).flatMap(turn => turn.messages).filter(isVisibleConversationRow)
|
|
1072
|
+
: [];
|
|
1079
1073
|
const recentMessages = withoutHistorySourceIndexes(addOptionalRecentToolPairs(
|
|
1080
1074
|
recentBaseline,
|
|
1081
|
-
truncateToolResultsForModel(
|
|
1075
|
+
truncateToolResultsForModel(recentToolSource, { language: options.language }),
|
|
1082
1076
|
{
|
|
1083
1077
|
messageTokenBudget: availableTokens - estimateMessagesTokens(relatedMessages),
|
|
1084
1078
|
maxMessageCount: availableRows - relatedMessages.length,
|
|
@@ -1103,7 +1097,8 @@ export function buildHistoryBuckets(snapshot, options = {}) {
|
|
|
1103
1097
|
budget: {
|
|
1104
1098
|
messageTokenBudget: tokenBudget, maxMessageCount: messageCap,
|
|
1105
1099
|
recentTurnCap: recentCap, relatedTurnCap: relatedCap,
|
|
1106
|
-
|
|
1100
|
+
minimumRecentTurns: minimumRecent,
|
|
1101
|
+
relatedReservedTokens: 0, availableHistoryTokens: availableTokens,
|
|
1107
1102
|
usedTokens: estimateMessagesTokens(messages), usedMessages: messages.length,
|
|
1108
1103
|
},
|
|
1109
1104
|
dropped: {
|
package/yeaft/prompts.js
CHANGED
|
@@ -532,6 +532,8 @@ function renderVpPersona(vpPersona, lang, effectiveLang = 'en') {
|
|
|
532
532
|
const name = selectVpPersonaName(vpPersona, effectiveLang);
|
|
533
533
|
if (!name) return '';
|
|
534
534
|
const body = selectVpPersonaBody(vpPersona, effectiveLang);
|
|
535
|
+
const runtimePreamble = typeof vpPersona.runtimePreamble === 'string'
|
|
536
|
+
? vpPersona.runtimePreamble.trim() : '';
|
|
535
537
|
|
|
536
538
|
// Persona is the IDENTITY layer (not an overlay). Do not prepend a
|
|
537
539
|
// generic assistant identity here: the VP soul body is the source of truth.
|
|
@@ -540,6 +542,7 @@ function renderVpPersona(vpPersona, lang, effectiveLang = 'en') {
|
|
|
540
542
|
const soulHeading = effectiveLang === 'zh' ? '## 灵魂' : '## Soul';
|
|
541
543
|
const lines = [`# ${name}`, '', soulHeading];
|
|
542
544
|
if (body) lines.push('', body);
|
|
545
|
+
if (runtimePreamble) lines.push('', runtimePreamble);
|
|
543
546
|
return lines.join('\n');
|
|
544
547
|
}
|
|
545
548
|
|
package/yeaft/session.js
CHANGED
|
@@ -49,7 +49,8 @@ import { ensureDefaultSessionIfEmpty, migrateRegisteredWorkDirSessions } from '.
|
|
|
49
49
|
import { seedDefaultVps } from './vp/seed-defaults.js';
|
|
50
50
|
import { topUpDefaultVps } from './vp/seed-topup.js';
|
|
51
51
|
import { archiveLegacyScopes } from './memory/seed-backfill.js';
|
|
52
|
-
|
|
52
|
+
// Dream scheduler wiring is intentionally not imported while the runtime path is disabled.
|
|
53
|
+
// import { createV2DreamScheduler, bootInitEmptyGroups, bootCatchUpStaleDream } from './dream/session-wiring.js';
|
|
53
54
|
import { isWorkCenterEnabled } from './work-center/feature.js';
|
|
54
55
|
import { openSegmentIndex } from './memory/index-db.js';
|
|
55
56
|
import { syncAll as syncSegmentIndex } from './memory/segment-sync.js';
|
|
@@ -473,50 +474,20 @@ export async function loadSession(options = {}) {
|
|
|
473
474
|
});
|
|
474
475
|
|
|
475
476
|
|
|
476
|
-
// ─── 9a.
|
|
477
|
-
//
|
|
478
|
-
//
|
|
479
|
-
//
|
|
480
|
-
//
|
|
481
|
-
//
|
|
482
|
-
//
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
const dreamScheduler = createV2DreamScheduler(partialSession);
|
|
491
|
-
|
|
492
|
-
// task-710: kick a dream pass at boot for any group that has user
|
|
493
|
-
// messages but zero memory segments in the FTS index. Without this a
|
|
494
|
-
// freshly opened agent had to wait an hour (or for the nudge counter
|
|
495
|
-
// to cross 50) before the first segment landed and recall could find
|
|
496
|
-
// anything. Fire-and-forget; failure logs at debug only.
|
|
497
|
-
if (memoryIndex && !config._readOnly) {
|
|
498
|
-
bootInitEmptyGroups({
|
|
499
|
-
yeaftDir,
|
|
500
|
-
memoryIndex,
|
|
501
|
-
dreamScheduler,
|
|
502
|
-
config,
|
|
503
|
-
}).catch(() => { /* best-effort boot init */ });
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
// fix/dream-cadence-and-ui-trigger: stale-cadence catch-up. If the
|
|
507
|
-
// newest per-group lastDreamAt across all groups is older than
|
|
508
|
-
// DREAM_INTERVAL_HOURS (or absent and there's user traffic), fire a
|
|
509
|
-
// single non-manual tick now. Independent of the interval timer —
|
|
510
|
-
// necessary because production observed 12 days between scheduled
|
|
511
|
-
// ticks (the unref'd interval did not fire reliably on long-lived
|
|
512
|
-
// server processes).
|
|
513
|
-
if (!config._readOnly) {
|
|
514
|
-
bootCatchUpStaleDream({
|
|
515
|
-
yeaftDir,
|
|
516
|
-
dreamScheduler,
|
|
517
|
-
config,
|
|
518
|
-
}).catch(() => { /* best-effort catch-up */ });
|
|
519
|
-
}
|
|
477
|
+
// ─── 9a. Dream runtime temporarily disabled ────────────
|
|
478
|
+
// Message history now supplies turn context. Keep the Dream implementation
|
|
479
|
+
// and persisted data intact, but do not create a scheduler or run boot-time
|
|
480
|
+
// initialization/catch-up while the replacement is evaluated.
|
|
481
|
+
//
|
|
482
|
+
// const partialSession = { yeaftDir, adapter, config, engine, trace };
|
|
483
|
+
// const dreamScheduler = createV2DreamScheduler(partialSession);
|
|
484
|
+
// if (memoryIndex && !config._readOnly) {
|
|
485
|
+
// bootInitEmptyGroups({ yeaftDir, memoryIndex, dreamScheduler, config }).catch(() => {});
|
|
486
|
+
// }
|
|
487
|
+
// if (!config._readOnly) {
|
|
488
|
+
// bootCatchUpStaleDream({ yeaftDir, dreamScheduler, config }).catch(() => {});
|
|
489
|
+
// }
|
|
490
|
+
const dreamScheduler = null;
|
|
520
491
|
|
|
521
492
|
// H2.f.5 retired the old session-level thread engine registry, input queue,
|
|
522
493
|
// and dispatcher. The session exposes a default `engine`; PR #797 keeps
|
|
@@ -221,10 +221,12 @@ export function startSubAgent(agent, deps = {}) {
|
|
|
221
221
|
deps.parentVpPersona && typeof deps.parentVpPersona === 'object'
|
|
222
222
|
? { ...deps.parentVpPersona }
|
|
223
223
|
: {};
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
224
|
+
// Keep the spawned-agent contract outside the inherited VP soul. Stock
|
|
225
|
+
// souls can contain bilingual section markers, and persona rendering selects
|
|
226
|
+
// one language section before provider dispatch; appending the preamble to
|
|
227
|
+
// that source can therefore discard the child identity and mission. The
|
|
228
|
+
// prompt renderer appends this runtime-only block after soul selection.
|
|
229
|
+
baseVpPersona.runtimePreamble = preamble.trim();
|
|
228
230
|
if (!baseVpPersona.displayName || !String(baseVpPersona.displayName).trim()) {
|
|
229
231
|
baseVpPersona.displayName = `${deps.parentName || 'Parent'}/${agent.name || 'sub-agent'}`;
|
|
230
232
|
}
|
package/yeaft/web-bridge.js
CHANGED
|
@@ -24,6 +24,7 @@ import { buildDreamOutputSnapshot } from './dream/output-snapshot.js';
|
|
|
24
24
|
import { Engine } from './engine.js';
|
|
25
25
|
import { loadSession } from './session.js';
|
|
26
26
|
import { loadAgentMCPConfig, loadConfig, loadMCPConfig } from './config.js';
|
|
27
|
+
import { resolveMaxOutputTokens } from './models.js';
|
|
27
28
|
import {
|
|
28
29
|
createManagedProjectSkill,
|
|
29
30
|
createManagedSkill,
|
|
@@ -160,7 +161,8 @@ const SKILL_RELOAD_INTERVAL_MS = 2_000;
|
|
|
160
161
|
* loads the Session.
|
|
161
162
|
* @type {Map<string, {
|
|
162
163
|
* resolve:Function,
|
|
163
|
-
* reject
|
|
164
|
+
* reject:Function,
|
|
165
|
+
* conversationId:string|null,
|
|
164
166
|
* sessionId:string,
|
|
165
167
|
* vpId:string,
|
|
166
168
|
* threadId:string,
|
|
@@ -176,6 +178,9 @@ const SKILL_RELOAD_INTERVAL_MS = 2_000;
|
|
|
176
178
|
* onAbort?:Function,
|
|
177
179
|
* }>} */
|
|
178
180
|
const pendingUserPrompts = new Map();
|
|
181
|
+
// Instance-local retry receipts, never persisted with Session runtime data.
|
|
182
|
+
const terminalUserPrompts = new Map();
|
|
183
|
+
const MAX_TERMINAL_USER_PROMPTS = 256;
|
|
179
184
|
|
|
180
185
|
/** @type {import('./session.js').Session | null} */
|
|
181
186
|
let session = null;
|
|
@@ -2238,7 +2243,9 @@ async function routeEnvelopeToVpThread(sessionId, vpId, envelope) {
|
|
|
2238
2243
|
});
|
|
2239
2244
|
}
|
|
2240
2245
|
|
|
2241
|
-
|
|
2246
|
+
// An append shares the running query's request config. A quick send owns
|
|
2247
|
+
// a separate query, so retain its envelope in this thread's normal inbox.
|
|
2248
|
+
if (related && !envelope?._turnConfig) {
|
|
2242
2249
|
const content = promptParts || prompt;
|
|
2243
2250
|
const injectedBy = envelope?.msg?.meta?.injectedBy;
|
|
2244
2251
|
const isInternalAppend = injectedBy === 'route_forward' || injectedBy === 'task_result';
|
|
@@ -3036,10 +3043,10 @@ function mergedStatusForProjectRuntime(runtime, ownerSession = session) {
|
|
|
3036
3043
|
}
|
|
3037
3044
|
|
|
3038
3045
|
/** Send a Yeaft Session metadata event over the legacy-compatible envelope. */
|
|
3039
|
-
function sendSessionEvent(event, { sessionId, chatId, vpId, turnId, threadId, requestId, requestClientId, perfTraceId } = {}) {
|
|
3046
|
+
function sendSessionEvent(event, { sessionId, chatId, vpId, turnId, threadId, requestId, requestClientId, perfTraceId, conversationId = yeaftConversationId } = {}) {
|
|
3040
3047
|
sendToServer({
|
|
3041
3048
|
type: 'yeaft_output',
|
|
3042
|
-
conversationId
|
|
3049
|
+
conversationId,
|
|
3043
3050
|
...(perfTraceId ? { perfTraceId } : {}),
|
|
3044
3051
|
...(requestId ? { requestId } : {}),
|
|
3045
3052
|
...(requestClientId ? { _requestClientId: requestClientId } : {}),
|
|
@@ -3085,26 +3092,52 @@ function replayPendingUserPrompts(sessionId) {
|
|
|
3085
3092
|
}
|
|
3086
3093
|
}
|
|
3087
3094
|
|
|
3088
|
-
function
|
|
3095
|
+
function registerPendingUserPrompt(requestId, pending) {
|
|
3096
|
+
pending.onAbort = () => settlePendingUserPrompt(requestId, pending, { aborted: true });
|
|
3097
|
+
pending.timer = setTimeout(() => {
|
|
3098
|
+
settlePendingUserPrompt(requestId, pending, { timedOut: true });
|
|
3099
|
+
}, Math.max(0, pending.expiresAt - Date.now()));
|
|
3100
|
+
pending.timer.unref?.();
|
|
3101
|
+
pendingUserPrompts.set(requestId, pending);
|
|
3102
|
+
if (pending.signal?.aborted) pending.onAbort();
|
|
3103
|
+
else pending.signal?.addEventListener('abort', pending.onAbort, { once: true });
|
|
3104
|
+
}
|
|
3105
|
+
|
|
3106
|
+
function settlePendingUserPrompt(requestId, pending, { answers = null, timedOut = false, aborted = false } = {}) {
|
|
3089
3107
|
if (pendingUserPrompts.get(requestId) !== pending) return false;
|
|
3090
3108
|
pendingUserPrompts.delete(requestId);
|
|
3091
3109
|
if (pending.timer) clearTimeout(pending.timer);
|
|
3092
3110
|
if (pending.signal && pending.onAbort) {
|
|
3093
3111
|
try { pending.signal.removeEventListener('abort', pending.onAbort); } catch { /* ignore */ }
|
|
3094
3112
|
}
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
3113
|
+
if (!aborted) {
|
|
3114
|
+
try { pending.resumeQueryTimer?.(); } catch { /* best-effort */ }
|
|
3115
|
+
}
|
|
3116
|
+
const expired = timedOut || aborted;
|
|
3117
|
+
const event = {
|
|
3118
|
+
type: expired ? 'ask_user_expired' : 'ask_user_answered',
|
|
3098
3119
|
requestId,
|
|
3099
3120
|
toolCallId: pending.toolCallId || null,
|
|
3100
|
-
...(
|
|
3101
|
-
}
|
|
3121
|
+
...(expired ? { expiredAt: Date.now() } : { answers: answers || {} }),
|
|
3122
|
+
};
|
|
3123
|
+
const identity = {
|
|
3124
|
+
requestId,
|
|
3125
|
+
toolCallId: pending.toolCallId,
|
|
3126
|
+
conversationId: pending.conversationId,
|
|
3102
3127
|
sessionId: pending.sessionId,
|
|
3103
3128
|
vpId: pending.vpId,
|
|
3104
3129
|
threadId: pending.threadId,
|
|
3105
3130
|
turnId: pending.turnId,
|
|
3106
|
-
}
|
|
3107
|
-
|
|
3131
|
+
};
|
|
3132
|
+
terminalUserPrompts.set(requestId, { identity, event });
|
|
3133
|
+
while (terminalUserPrompts.size > MAX_TERMINAL_USER_PROMPTS) {
|
|
3134
|
+
terminalUserPrompts.delete(terminalUserPrompts.keys().next().value);
|
|
3135
|
+
}
|
|
3136
|
+
// Like prompt replay, live settlement belongs to the current projection,
|
|
3137
|
+
// not the conversation generation captured when the question was created.
|
|
3138
|
+
sendSessionEvent(event, { ...identity, conversationId: yeaftConversationId || pending.conversationId });
|
|
3139
|
+
if (aborted) pending.reject(new Error('aborted'));
|
|
3140
|
+
else pending.resolve(timedOut ? { __yeaftTimedOut: true } : (answers || {}));
|
|
3108
3141
|
return true;
|
|
3109
3142
|
}
|
|
3110
3143
|
|
|
@@ -4744,6 +4777,27 @@ function handleEngineEvent(event, hctx) {
|
|
|
4744
4777
|
* - No legacy "no-session" fallback paths — they were the source of the
|
|
4745
4778
|
* router_unavailable bug fixed in v0.1.671.
|
|
4746
4779
|
*/
|
|
4780
|
+
/** Validate a one-message override against the loaded Agent catalog, never Session config. */
|
|
4781
|
+
export function validateQuickSend(value, config) {
|
|
4782
|
+
if (value === undefined) return null;
|
|
4783
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
4784
|
+
throw new Error('quickSend must be an object');
|
|
4785
|
+
}
|
|
4786
|
+
const { model, effort, maxOutputTokens } = value;
|
|
4787
|
+
const entry = typeof model === 'string' && model.includes('/')
|
|
4788
|
+
? config?.availableModels?.find(candidate => candidate.ref === model)
|
|
4789
|
+
: null;
|
|
4790
|
+
if (!entry) throw new Error('quickSend.model must name an available provider/model');
|
|
4791
|
+
if (effort !== null && (typeof effort !== 'string' || !entry.effortOptions?.includes(effort))) {
|
|
4792
|
+
throw new Error('quickSend.effort is not supported by the selected model');
|
|
4793
|
+
}
|
|
4794
|
+
if (maxOutputTokens !== null && (!Number.isSafeInteger(maxOutputTokens) || maxOutputTokens < 1
|
|
4795
|
+
|| maxOutputTokens > resolveMaxOutputTokens(entry.id, { modelInfo: entry }))) {
|
|
4796
|
+
throw new Error('quickSend.maxOutputTokens must be null or a positive integer within the model output limit');
|
|
4797
|
+
}
|
|
4798
|
+
return { model, effort, maxOutputTokens };
|
|
4799
|
+
}
|
|
4800
|
+
|
|
4747
4801
|
async function runYeaftSessionSend(msg) {
|
|
4748
4802
|
if (!msg || typeof msg !== 'object') return;
|
|
4749
4803
|
const { text } = msg;
|
|
@@ -4800,6 +4854,20 @@ async function runYeaftSessionSend(msg) {
|
|
|
4800
4854
|
return;
|
|
4801
4855
|
}
|
|
4802
4856
|
|
|
4857
|
+
// Reject malformed overrides before boot, roster changes, attachment writes,
|
|
4858
|
+
// or coordinator persistence. The error includes the client id for retry.
|
|
4859
|
+
let turnConfig = null;
|
|
4860
|
+
try {
|
|
4861
|
+
if (msg.quickSend !== undefined) {
|
|
4862
|
+
turnConfig = validateQuickSend(msg.quickSend, loadConfig({ dir: yeaftDir }));
|
|
4863
|
+
}
|
|
4864
|
+
} catch (err) {
|
|
4865
|
+
sendSessionOutputFrame({ type: 'error', code: 'invalid_quick_send',
|
|
4866
|
+
message: err.message, clientMessageId: msg.id || null }, { sessionId });
|
|
4867
|
+
sendSessionOutputFrame({ type: 'result', result_text: '', is_error: true }, { sessionId });
|
|
4868
|
+
return;
|
|
4869
|
+
}
|
|
4870
|
+
|
|
4803
4871
|
// Open the user-level session metadata first so project runtime loading can
|
|
4804
4872
|
// use the stored workDir without blocking the user-message hot path. If
|
|
4805
4873
|
// metadata is missing we still boot the agent runtime for a useful error
|
|
@@ -4981,6 +5049,7 @@ async function runYeaftSessionSend(msg) {
|
|
|
4981
5049
|
// Live form — adapters need the base64 image blocks; runVpTurn
|
|
4982
5050
|
// reads `_promptParts` off the envelope rather than going
|
|
4983
5051
|
// back to disk on every fan-out target. NOT persisted.
|
|
5052
|
+
_turnConfig: turnConfig,
|
|
4984
5053
|
_promptParts: attachmentBundle.promptParts,
|
|
4985
5054
|
_promptSuffix: attachmentBundle.promptSuffix,
|
|
4986
5055
|
_perfTraceId: perfTraceId,
|
|
@@ -5166,6 +5235,12 @@ export function buildVpQueryOpts({ vpId, sessionCoordinator, sessionId, envelope
|
|
|
5166
5235
|
// envelope inside router.forward.
|
|
5167
5236
|
if (envelope && typeof envelope === 'object') {
|
|
5168
5237
|
out.inboundEnvelope = envelope;
|
|
5238
|
+
// Only direct delivery of this user message inherits quick-send settings.
|
|
5239
|
+
// RouteForward and background reentry keep the recipient's own defaults.
|
|
5240
|
+
if (envelope._turnConfig && !envelope.msg?.meta?.injectedBy
|
|
5241
|
+
&& (envelope.msg?.role === 'user' || envelope.msg?.from === 'user')) {
|
|
5242
|
+
out.turnConfig = { ...envelope._turnConfig };
|
|
5243
|
+
}
|
|
5169
5244
|
}
|
|
5170
5245
|
// TodoWrite per-thread isolation. Bind closures that read/write a slot
|
|
5171
5246
|
// keyed by `${sessionId}::${vpId}::${threadId}` so concurrent threads for
|
|
@@ -5720,6 +5795,7 @@ async function runVpTurn({ prompt, promptParts = null, sessionId, vpId, threadId
|
|
|
5720
5795
|
const pending = {
|
|
5721
5796
|
resolve,
|
|
5722
5797
|
reject,
|
|
5798
|
+
conversationId: yeaftConversationId,
|
|
5723
5799
|
sessionId,
|
|
5724
5800
|
vpId,
|
|
5725
5801
|
threadId,
|
|
@@ -5734,20 +5810,8 @@ async function runVpTurn({ prompt, promptParts = null, sessionId, vpId, threadId
|
|
|
5734
5810
|
signal,
|
|
5735
5811
|
onAbort: null,
|
|
5736
5812
|
};
|
|
5737
|
-
|
|
5738
|
-
|
|
5739
|
-
pendingUserPrompts.delete(requestId);
|
|
5740
|
-
if (pending.timer) clearTimeout(pending.timer);
|
|
5741
|
-
reject(new Error('aborted'));
|
|
5742
|
-
};
|
|
5743
|
-
pending.onAbort = onAbort;
|
|
5744
|
-
pending.timer = setTimeout(() => {
|
|
5745
|
-
settlePendingUserPrompt(requestId, pending, { timedOut: true });
|
|
5746
|
-
}, ASK_USER_TIMEOUT_MS);
|
|
5747
|
-
if (typeof pending.timer.unref === 'function') pending.timer.unref();
|
|
5748
|
-
pendingUserPrompts.set(requestId, pending);
|
|
5749
|
-
signal.addEventListener('abort', onAbort, { once: true });
|
|
5750
|
-
sendPendingUserPrompt(requestId, pending);
|
|
5813
|
+
registerPendingUserPrompt(requestId, pending);
|
|
5814
|
+
if (pendingUserPrompts.has(requestId)) sendPendingUserPrompt(requestId, pending);
|
|
5751
5815
|
}),
|
|
5752
5816
|
threadId,
|
|
5753
5817
|
vpTurnId: turnId,
|
|
@@ -6829,16 +6893,42 @@ export async function handleYeaftFetchDebugHistory(msg = {}) {
|
|
|
6829
6893
|
}
|
|
6830
6894
|
|
|
6831
6895
|
/** Resolve a pending Yeaft AskUser prompt from the web UI. */
|
|
6832
|
-
export function handleYeaftAskUserAnswer(msg) {
|
|
6896
|
+
export function handleYeaftAskUserAnswer(msg = {}) {
|
|
6833
6897
|
const requestId = typeof msg?.requestId === 'string' ? msg.requestId : '';
|
|
6898
|
+
const rejectAnswer = reason => {
|
|
6899
|
+
// Echo only submitted identity: never expose the actual pending scope.
|
|
6900
|
+
sendSessionEvent({
|
|
6901
|
+
type: 'ask_user_answer_rejected', requestId,
|
|
6902
|
+
toolCallId: msg?.toolCallId || null, reason,
|
|
6903
|
+
}, {
|
|
6904
|
+
requestId, requestClientId: msg?._requestClientId,
|
|
6905
|
+
conversationId: msg?.conversationId ?? null,
|
|
6906
|
+
sessionId: msg?.sessionId, vpId: msg?.vpId,
|
|
6907
|
+
turnId: msg?.turnId, threadId: msg?.threadId,
|
|
6908
|
+
});
|
|
6909
|
+
return false;
|
|
6910
|
+
};
|
|
6834
6911
|
const pending = pendingUserPrompts.get(requestId);
|
|
6835
|
-
|
|
6836
|
-
|
|
6837
|
-
if (
|
|
6838
|
-
|
|
6839
|
-
|
|
6840
|
-
|
|
6841
|
-
|
|
6912
|
+
const terminal = terminalUserPrompts.get(requestId);
|
|
6913
|
+
const identity = pending || terminal?.identity;
|
|
6914
|
+
if (!identity) return rejectAnswer('unavailable');
|
|
6915
|
+
// Retain legacy optional identity matching. conversationId is a projection
|
|
6916
|
+
// generation, not Session ownership; it can change after a reconnect.
|
|
6917
|
+
for (const field of ['sessionId', 'vpId', 'turnId', 'threadId', 'toolCallId']) {
|
|
6918
|
+
if (msg[field] && msg[field] !== identity[field]) return rejectAnswer('identity_mismatch');
|
|
6919
|
+
}
|
|
6920
|
+
if (!pending) {
|
|
6921
|
+
sendSessionEvent(terminal.event, {
|
|
6922
|
+
...terminal.identity,
|
|
6923
|
+
conversationId: msg.conversationId ?? terminal.identity.conversationId,
|
|
6924
|
+
requestClientId: msg._requestClientId,
|
|
6925
|
+
});
|
|
6926
|
+
return terminal.event.type === 'ask_user_answered' ? true : rejectAnswer('unavailable');
|
|
6927
|
+
}
|
|
6928
|
+
if (pending.expiresAt <= Date.now()) {
|
|
6929
|
+
settlePendingUserPrompt(requestId, pending, { timedOut: true });
|
|
6930
|
+
return rejectAnswer('unavailable');
|
|
6931
|
+
}
|
|
6842
6932
|
return settlePendingUserPrompt(requestId, pending, { answers: msg.answers || {} });
|
|
6843
6933
|
}
|
|
6844
6934
|
|
|
@@ -8211,11 +8301,10 @@ export const __testHooks = {
|
|
|
8211
8301
|
vpAborts.clear();
|
|
8212
8302
|
vpInboxes.clear();
|
|
8213
8303
|
},
|
|
8214
|
-
seedPendingUserPrompt({ requestId = 'ask-test', sessionId = 'session-test', vpId = 'vp-test', threadId = 'main', turnId = 'turn-test', toolCallId = 'call-test', question = 'Continue?', options = [], createdAt = Date.now(), expiresAt = Date.now() + ASK_USER_TIMEOUT_MS } = {}) {
|
|
8215
|
-
|
|
8216
|
-
|
|
8217
|
-
|
|
8218
|
-
return promise;
|
|
8304
|
+
seedPendingUserPrompt({ requestId = 'ask-test', sessionId = 'session-test', vpId = 'vp-test', threadId = 'main', turnId = 'turn-test', toolCallId = 'call-test', conversationId = yeaftConversationId, question = 'Continue?', options = [], createdAt = Date.now(), expiresAt = Date.now() + ASK_USER_TIMEOUT_MS, signal = null } = {}) {
|
|
8305
|
+
return new Promise((resolve, reject) => {
|
|
8306
|
+
registerPendingUserPrompt(requestId, { resolve, reject, conversationId, sessionId, vpId, threadId, turnId, toolCallId, question, options, createdAt, expiresAt, signal });
|
|
8307
|
+
});
|
|
8219
8308
|
},
|
|
8220
8309
|
replayPendingUserPrompts,
|
|
8221
8310
|
settlePendingUserPromptForTest(requestId, opts = {}) {
|
|
@@ -8225,8 +8314,10 @@ export const __testHooks = {
|
|
|
8225
8314
|
resetPendingUserPrompts() {
|
|
8226
8315
|
for (const pending of pendingUserPrompts.values()) {
|
|
8227
8316
|
if (pending.timer) clearTimeout(pending.timer);
|
|
8317
|
+
if (pending.onAbort) pending.signal?.removeEventListener('abort', pending.onAbort);
|
|
8228
8318
|
}
|
|
8229
8319
|
pendingUserPrompts.clear();
|
|
8320
|
+
terminalUserPrompts.clear();
|
|
8230
8321
|
},
|
|
8231
8322
|
resetVpStatusBroker() {
|
|
8232
8323
|
if (vpStatusBroker) vpStatusBroker.reset();
|