@yeaft/webchat-agent 1.0.640 → 1.0.642
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 +4 -1
- package/index.js +1 -1
- package/local-runtime/server/handlers/agent-output.js +7 -5
- package/local-runtime/server/handlers/client-conversation.js +47 -0
- package/local-runtime/version.json +1 -1
- package/local-runtime/web/app.bundle.js +158 -119
- 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/conversation/persist.js +272 -24
- package/yeaft/conversation/session-history.js +336 -0
- package/yeaft/engine.js +88 -573
- package/yeaft/history-window.js +132 -5
- package/yeaft/routing/router.js +4 -0
- package/yeaft/sessions/index.js +1 -0
- package/yeaft/sessions/session-crud.js +68 -3
- package/yeaft/tools/agent.js +4 -1
- package/yeaft/web-bridge.js +255 -54
package/yeaft/engine.js
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* Reference: yeaft-yeaft-implementation-plan.md §3.1, §4 (Phase 2)
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
|
-
import { randomUUID
|
|
20
|
+
import { randomUUID } from 'crypto';
|
|
21
21
|
import { QueryProgress, progressReminder } from './query-progress.js';
|
|
22
22
|
import { createWebResearchState } from './web-research.js';
|
|
23
23
|
import { promises as fsp } from 'fs';
|
|
@@ -55,12 +55,6 @@ import { perfNowMs, recordAgentPerfTrace } from './perf-trace.js';
|
|
|
55
55
|
// pass a real threadId per (sessionId, vpId, threadId) engine instance.
|
|
56
56
|
const MAIN_THREAD_ID = 'main';
|
|
57
57
|
|
|
58
|
-
function foldingMessageKey(message) {
|
|
59
|
-
return message?._persistedMessageId || message?.id || createHash('sha256').update(JSON.stringify([
|
|
60
|
-
message?.role, message?.toolCallId, message?.toolCalls, message?.content,
|
|
61
|
-
])).digest('hex');
|
|
62
|
-
}
|
|
63
|
-
|
|
64
58
|
import { pickEffort, parseEffortPrefix, snapshotEffortDecision } from './effort.js';
|
|
65
59
|
import { bindProviderState } from './llm/provider-state.js';
|
|
66
60
|
import {
|
|
@@ -71,8 +65,7 @@ import {
|
|
|
71
65
|
removePostCompactIfSource,
|
|
72
66
|
savePostCompact,
|
|
73
67
|
} from './post-compact.js';
|
|
74
|
-
import {
|
|
75
|
-
import { lookupModelLimitSync } from './llm/models-dev.js';
|
|
68
|
+
import { getModelInfo, normalizeEffort, parseModelRef, resolveContextWindow, resolveMaxOutputTokens } from './models.js';
|
|
76
69
|
import { attachRouterPlan, extractPriorPlan, stripMetaForWire } from './router/continuity.js';
|
|
77
70
|
import { resolveThinking } from './router/thinking.js';
|
|
78
71
|
import { approxTokens, computeBudget } from './memory/budget.js';
|
|
@@ -84,18 +77,11 @@ import { createPluginSkillManager } from './plugins.js';
|
|
|
84
77
|
import { extractDisplayImages, stripDisplayImageData } from './image-assets.js';
|
|
85
78
|
import { acknowledgePendingNotifications, formatNotificationsForPrompt, peekPendingNotifications } from './sub-agent/notifications.js';
|
|
86
79
|
import {
|
|
87
|
-
TOOL_LOOP_REFLECTION_INTERVAL,
|
|
88
|
-
TURN_SUMMARY_THRESHOLD,
|
|
89
80
|
DUP_TOOL_THRESHOLD,
|
|
90
81
|
ExecLog,
|
|
91
82
|
buildEntry as buildExecLogEntry,
|
|
92
83
|
argsHashOf,
|
|
93
|
-
runT1Reflection,
|
|
94
|
-
runT2Reflection,
|
|
95
|
-
buildFallbackStub,
|
|
96
|
-
collapseRangeToReflection,
|
|
97
84
|
buildDuplicateReminder,
|
|
98
|
-
extractToolPairsFromRange,
|
|
99
85
|
} from './tool-folding/index.js';
|
|
100
86
|
|
|
101
87
|
/**
|
|
@@ -465,47 +451,6 @@ export function estimateProviderInputBreakdown({
|
|
|
465
451
|
};
|
|
466
452
|
}
|
|
467
453
|
|
|
468
|
-
export const GROUP_CONTEXT_PRESSURE_RATIO = 0.8;
|
|
469
|
-
|
|
470
|
-
export function shouldAllowGroupReflection({
|
|
471
|
-
system = '',
|
|
472
|
-
messages = [],
|
|
473
|
-
model = null,
|
|
474
|
-
config = {},
|
|
475
|
-
sessionId = null,
|
|
476
|
-
} = {}) {
|
|
477
|
-
if (!sessionId) {
|
|
478
|
-
return {
|
|
479
|
-
allowed: true,
|
|
480
|
-
tokenEstimate: estimateMessagesTokens(system, messages),
|
|
481
|
-
threshold: 0,
|
|
482
|
-
contextWindow: null,
|
|
483
|
-
ratio: GROUP_CONTEXT_PRESSURE_RATIO,
|
|
484
|
-
usedFallbackContextWindow: false,
|
|
485
|
-
};
|
|
486
|
-
}
|
|
487
|
-
const contextWindow = resolveContextWindow(model, config);
|
|
488
|
-
// Telemetry: did the resolver hit either of its top non-default rungs?
|
|
489
|
-
// Used by `usedFallbackContextWindow` below — if neither models.dev nor
|
|
490
|
-
// the global config provided a number, we fell through to DEFAULT and
|
|
491
|
-
// callers may want to surface that to the user.
|
|
492
|
-
const hasModelsDevContext = !!lookupModelLimitSync(model, resolveModel(model)?.provider || null)?.context;
|
|
493
|
-
const hasConfigContext = Number.isFinite(config?.maxContextTokens) && config.maxContextTokens > 0;
|
|
494
|
-
const threshold = Math.floor(contextWindow * GROUP_CONTEXT_PRESSURE_RATIO);
|
|
495
|
-
const tokenEstimate = estimateMessagesTokens(system, messages);
|
|
496
|
-
const overThreshold = tokenEstimate >= threshold;
|
|
497
|
-
return {
|
|
498
|
-
// Group send defaults to no reflection. Trust the model until context
|
|
499
|
-
// pressure says we are near the model window.
|
|
500
|
-
allowed: overThreshold,
|
|
501
|
-
tokenEstimate,
|
|
502
|
-
threshold,
|
|
503
|
-
contextWindow,
|
|
504
|
-
ratio: GROUP_CONTEXT_PRESSURE_RATIO,
|
|
505
|
-
usedFallbackContextWindow: !hasModelsDevContext && !hasConfigContext && contextWindow === DEFAULT_CONTEXT_WINDOW,
|
|
506
|
-
};
|
|
507
|
-
}
|
|
508
|
-
|
|
509
454
|
// ─── Engine Events (superset of adapter events) ──────────────────
|
|
510
455
|
|
|
511
456
|
/**
|
|
@@ -769,24 +714,8 @@ export class Engine {
|
|
|
769
714
|
*/
|
|
770
715
|
#currentAbortCtrl = null;
|
|
771
716
|
|
|
772
|
-
/**
|
|
773
|
-
* PR-L — V7 Tool History Reflection state. Owned per Engine instance.
|
|
774
|
-
*
|
|
775
|
-
* • `#execLog` — append-only log of every tool execution, used for
|
|
776
|
-
* fallback-stub generation and duplicate-call detection. Persists
|
|
777
|
-
* to <yeaftDir>/tool-log/<traceId>/<turnIdx>.jsonl when yeaftDir
|
|
778
|
-
* is set; in-memory only otherwise.
|
|
779
|
-
* • `#pendingT2` — Map<turnNumber, { promise, loopRange, count, ... }>
|
|
780
|
-
* keyed by the turn number that triggered T2. The next query() call
|
|
781
|
-
* non-blocking-checks this map; if the promise has resolved, the
|
|
782
|
-
* prior turn's history is rewritten with the reflection. If still
|
|
783
|
-
* pending, the engine falls back to the exec-log stub.
|
|
784
|
-
* • `#reflectedTurns` — Set<turnNumber>; ensures T1 fires at most
|
|
785
|
-
* once per reflection interval measured in tool loops.
|
|
786
|
-
*/
|
|
717
|
+
/** Append-only tool execution diagnostics and query-local duplicate lookup. */
|
|
787
718
|
#execLog = null;
|
|
788
|
-
#pendingT2 = new Map();
|
|
789
|
-
#reflectedTurns = new Set();
|
|
790
719
|
#__queryCounter = 0;
|
|
791
720
|
|
|
792
721
|
/** Derived post-response summaries, keyed by Session/VP/thread scope. */
|
|
@@ -1446,6 +1375,7 @@ export class Engine {
|
|
|
1446
1375
|
effortDecision: snapshotEffortDecision(vpCtx?.effortDecision),
|
|
1447
1376
|
requestIdentity: vpCtx?.requestIdentity,
|
|
1448
1377
|
queryTurnId: vpCtx?.queryTurnId,
|
|
1378
|
+
causalRootId: vpCtx?.causalRootId || null,
|
|
1449
1379
|
webResearch: vpCtx?.webResearch,
|
|
1450
1380
|
historyBeforeSeq: vpCtx?.historyBeforeSeq,
|
|
1451
1381
|
yeaftDir: this.#yeaftDir,
|
|
@@ -1683,17 +1613,6 @@ export class Engine {
|
|
|
1683
1613
|
return this.#conversationStore.append(this.#conversationRecord(message, context), { strict: context.strict === true });
|
|
1684
1614
|
}
|
|
1685
1615
|
|
|
1686
|
-
#persistFoldedRange(foldedMessages, reflection, context = {}) {
|
|
1687
|
-
if (!this.#canPersistConversation() || typeof this.#conversationStore?.foldMessages !== 'function') return null;
|
|
1688
|
-
const persistedRows = (foldedMessages || [])
|
|
1689
|
-
.map(message => message?._persistedMessageId || message?.id)
|
|
1690
|
-
.filter(id => typeof id === 'string' && id)
|
|
1691
|
-
.map(id => ({ id }));
|
|
1692
|
-
if (persistedRows.length === 0) return null;
|
|
1693
|
-
const record = this.#conversationRecord(reflection, context);
|
|
1694
|
-
return this.#conversationStore.foldMessages(persistedRows, record);
|
|
1695
|
-
}
|
|
1696
|
-
|
|
1697
1616
|
#formatTaskResultUpdateContent(content) {
|
|
1698
1617
|
if (typeof content === 'string') return content;
|
|
1699
1618
|
try { return JSON.stringify(content); } catch { return String(content); }
|
|
@@ -1713,10 +1632,9 @@ export class Engine {
|
|
|
1713
1632
|
msg && msg.role === 'tool' && msg.toolCallId === update.toolCallId
|
|
1714
1633
|
));
|
|
1715
1634
|
if (!toolMsg) {
|
|
1716
|
-
//
|
|
1717
|
-
// completed.
|
|
1718
|
-
//
|
|
1719
|
-
// provider transcript. This is engine control context, not a fresh
|
|
1635
|
+
// Legacy histories may have folded the original tool row before this
|
|
1636
|
+
// task completed. A late completion must not recreate the hidden tool arc
|
|
1637
|
+
// on disk or in the provider transcript. This is engine control context, not a fresh
|
|
1720
1638
|
// user-authored message; ConversationStore keeps it off the visible
|
|
1721
1639
|
// transcript while retaining it for the next provider boundary.
|
|
1722
1640
|
const contextContent = truncateToolResultIfNeeded(appendText, {
|
|
@@ -1734,6 +1652,7 @@ export class Engine {
|
|
|
1734
1652
|
const persistedContinuation = this.#persistConversationMessage(continuation, {
|
|
1735
1653
|
sessionId: update.sessionId || this.#sessionId,
|
|
1736
1654
|
turnId: update.turnId || null,
|
|
1655
|
+
causalRootId: update.causalRootId || null,
|
|
1737
1656
|
});
|
|
1738
1657
|
if (persistedContinuation?.id) continuation._persistedMessageId = persistedContinuation.id;
|
|
1739
1658
|
conversationMessages.push(continuation);
|
|
@@ -2535,64 +2454,10 @@ export class Engine {
|
|
|
2535
2454
|
},
|
|
2536
2455
|
];
|
|
2537
2456
|
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
messages: conversationMessages,
|
|
2541
|
-
model: this.#config.model,
|
|
2542
|
-
config: this.#config,
|
|
2543
|
-
sessionId,
|
|
2544
|
-
});
|
|
2545
|
-
const groupReflectionAllowed = groupReflectionGate.allowed === true;
|
|
2546
|
-
if (sessionId && groupReflectionGate?.usedFallbackContextWindow) {
|
|
2547
|
-
this.#trace.log?.('group_context_window_fallback', {
|
|
2548
|
-
sessionId,
|
|
2549
|
-
model: this.#config.model,
|
|
2550
|
-
contextWindow: groupReflectionGate.contextWindow,
|
|
2551
|
-
threshold: groupReflectionGate.threshold,
|
|
2552
|
-
});
|
|
2553
|
-
}
|
|
2554
|
-
|
|
2555
|
-
// PR-L: T2 carry-forward. If a previous query()'s end-of-turn
|
|
2556
|
-
// reflection has resolved, rewrite that turn's range in
|
|
2557
|
-
// `conversationMessages` to a single assistant reflection message.
|
|
2558
|
-
// If still pending, fall back to the exec-log stub — non-blocking,
|
|
2559
|
-
// never wait. This runs BEFORE the first adapter.stream so the
|
|
2560
|
-
// upcoming call sees the rewritten history. Group send defaults to no
|
|
2561
|
-
// reflection; only high context pressure (>=80% of model window)
|
|
2562
|
-
// enables the carry-forward rewrite.
|
|
2563
|
-
if (groupReflectionAllowed) {
|
|
2564
|
-
yield* this.#applyPendingT2Reflections(conversationMessages, prompt, {
|
|
2565
|
-
sessionId: runtimeSessionId,
|
|
2566
|
-
model: this.#config.model,
|
|
2567
|
-
});
|
|
2568
|
-
}
|
|
2569
|
-
|
|
2570
|
-
// PR-L: track this query()'s tool-arc for reflection.
|
|
2571
|
-
// `turnStartIdx` is where the current user message lives; the arc
|
|
2572
|
-
// we may collapse spans (arcStartIdx .. last assistant/tool).
|
|
2573
|
-
//
|
|
2574
|
-
// Periodic T1 fires every 30 tool loops, not every 30 calls. A single
|
|
2575
|
-
// provider batch can contain many parallel calls but is still one loop.
|
|
2576
|
-
// track:
|
|
2577
|
-
// • `lastT1AtLoopCount` — tool-loop snapshot at the last T1
|
|
2578
|
-
// ATTEMPT (success OR error). Trigger when
|
|
2579
|
-
// `completedToolLoops - lastT1AtLoopCount >= interval`.
|
|
2580
|
-
// • `arcStartIdx` — first index of the current (uncollapsed)
|
|
2581
|
-
// tool arc. Initialised to turnStartIdx + 1; reset after each
|
|
2582
|
-
// successful T1 collapse to `conversationMessages.length`
|
|
2583
|
-
// (i.e. the slot the next assistant message will land in).
|
|
2584
|
-
// • `t1CollapsesDone` — count of T1 firings that ACTUALLY
|
|
2585
|
-
// rewrote history. Distinct from `lastT1AtLoopCount` because
|
|
2586
|
-
// the catch block bumps the latter to back off after a
|
|
2587
|
-
// transient reflector error WITHOUT having collapsed
|
|
2588
|
-
// anything. The T2 schedule check below is gated on this
|
|
2589
|
-
// counter (==0 means "no T1 ever rewrote the arc, T2 may
|
|
2590
|
-
// fall back at end_turn").
|
|
2457
|
+
// Current-turn protocol stays on the live tape. Only the disposable
|
|
2458
|
+
// provider copy is compacted when its actual model-window budget is full.
|
|
2591
2459
|
const turnStartIdx = conversationMessages.length - 1;
|
|
2592
2460
|
let queryToolCount = 0;
|
|
2593
|
-
let lastT1AtLoopCount = 0;
|
|
2594
|
-
let arcStartIdx = turnStartIdx + 1;
|
|
2595
|
-
let t1CollapsesDone = 0;
|
|
2596
2461
|
// Duplicate policy is scoped to one user query. Only successful, real
|
|
2597
2462
|
// executions increment these counters; errors and cache reuse do not.
|
|
2598
2463
|
const queryDuplicateCounts = new Map();
|
|
@@ -3063,7 +2928,7 @@ export class Engine {
|
|
|
3063
2928
|
));
|
|
3064
2929
|
const toolSchemaTokens = toolDefs.length > 0
|
|
3065
2930
|
? approxTokens(JSON.stringify(toolDefs)) : 0;
|
|
3066
|
-
const
|
|
2931
|
+
const fitOptions = {
|
|
3067
2932
|
contextWindow: Math.max(1, Math.floor(currentContextWindow * providerContextScale)),
|
|
3068
2933
|
systemTokens: estimateMessagesTokens(systemPrompt, []),
|
|
3069
2934
|
toolSchemaTokens,
|
|
@@ -3071,9 +2936,50 @@ export class Engine {
|
|
|
3071
2936
|
historyMessageCount,
|
|
3072
2937
|
historyTokenBudget: historyBudget,
|
|
3073
2938
|
language: requestConfig.language,
|
|
2939
|
+
};
|
|
2940
|
+
let fittedRequest = fitProviderRequestToContext(wireMessages, fitOptions);
|
|
2941
|
+
// A task can complete on an old tool row. Sampling or evicting that row
|
|
2942
|
+
// must not acknowledge unseen evidence. Re-inject only missing pending
|
|
2943
|
+
// deliveries as fresh provider-only notes, then check the final copy.
|
|
2944
|
+
const taskDeliveries = Array.from(this.#pendingAsyncTaskConfirmIds).flatMap(taskId => {
|
|
2945
|
+
const delivery = this.#acceptedAsyncTaskResults.get(taskId);
|
|
2946
|
+
if (!delivery) return [];
|
|
2947
|
+
const text = truncateToolResultIfNeeded(this.#formatTaskResultUpdateContent(delivery.content), {
|
|
2948
|
+
toolName: 'async task result', language: requestConfig.language,
|
|
2949
|
+
});
|
|
2950
|
+
return [{ taskId, text }];
|
|
3074
2951
|
});
|
|
2952
|
+
const includesDelivery = (messages, text) => messages.some(message => (
|
|
2953
|
+
this.#formatTaskResultUpdateContent(message.content).includes(text)
|
|
2954
|
+
));
|
|
2955
|
+
const missingDeliveries = taskDeliveries.filter(({ text }) => !includesDelivery(fittedRequest.messages, text));
|
|
2956
|
+
if (missingDeliveries.length > 0) {
|
|
2957
|
+
fittedRequest = fitProviderRequestToContext([...wireMessages, ...missingDeliveries.map(({ text }) => ({
|
|
2958
|
+
role: 'user', content: `[system note] Async task completion:\n${text}`,
|
|
2959
|
+
}))], fitOptions);
|
|
2960
|
+
}
|
|
3075
2961
|
wireMessages = fittedRequest.messages;
|
|
2962
|
+
const deliveredTaskIds = taskDeliveries.filter(({ text }) => includesDelivery(wireMessages, text))
|
|
2963
|
+
.map(({ taskId }) => taskId);
|
|
3076
2964
|
historyMessageCount = fittedRequest.meta.historyMessagesAfter;
|
|
2965
|
+
if (fittedRequest.meta.compactedToolResults > 0) {
|
|
2966
|
+
// Compaction invalidates file-read hints, not execution/reuse safety
|
|
2967
|
+
// ledgers. The complete result remains in transcript/debug storage.
|
|
2968
|
+
fileReadObservations.clear();
|
|
2969
|
+
recordAgentPerfTrace(requestConfig, {
|
|
2970
|
+
traceId: perfTraceId || queryTurnId,
|
|
2971
|
+
phase: 'tool_result_compact',
|
|
2972
|
+
sessionId: runtimeSessionId,
|
|
2973
|
+
vpId: queryVpId,
|
|
2974
|
+
turnId: queryTurnId,
|
|
2975
|
+
threadId: runtimeThreadId,
|
|
2976
|
+
detail: {
|
|
2977
|
+
model: currentModel,
|
|
2978
|
+
recoveryAttempt: contextOverflowRecoveryAttempts,
|
|
2979
|
+
...fittedRequest.meta,
|
|
2980
|
+
},
|
|
2981
|
+
});
|
|
2982
|
+
}
|
|
3077
2983
|
if (fittedRequest.meta.droppedHistoryMessages > 0
|
|
3078
2984
|
|| fittedRequest.meta.droppedCurrentMessages > 0
|
|
3079
2985
|
|| providerContextScale < 1) {
|
|
@@ -3086,49 +2992,6 @@ export class Engine {
|
|
|
3086
2992
|
});
|
|
3087
2993
|
}
|
|
3088
2994
|
|
|
3089
|
-
// task-704b: pre-flight total-token guard. Even with the per-tool
|
|
3090
|
-
// cap (registry.js: 10% of contextWindow per result), N tool
|
|
3091
|
-
// results plus history can still breach the wire limit before we
|
|
3092
|
-
// ever call adapter.stream(). Estimate the total token cost; if
|
|
3093
|
-
// it exceeds PREFLIGHT_RATIO of the live context window, run an
|
|
3094
|
-
// emergency archive sweep with `turnAgeMin: 0` so even
|
|
3095
|
-
// current-turn-but-not-this-call bulky results get stubbed. The
|
|
3096
|
-
// normal sweep above only stubs results older than 5 user turns
|
|
3097
|
-
// — that's the wrong cadence when the *current* turn already has
|
|
3098
|
-
// 4 large grep results.
|
|
3099
|
-
//
|
|
3100
|
-
// PREFLIGHT_RATIO = 0.85 leaves ~15% of the window for the model's
|
|
3101
|
-
// own output tokens + tools metadata + light future history.
|
|
3102
|
-
// The estimator (`estimateMessagesTokens`) is approxTokens
|
|
3103
|
-
// (char/4 with CJK weighting) — good enough for a guard rail; a
|
|
3104
|
-
// real tokenizer would be exact but adds a heavy dep.
|
|
3105
|
-
if (scenario !== 'work-item' && this.#yeaftDir && (this.#config?.archive?.toolResults !== false)) {
|
|
3106
|
-
const PREFLIGHT_RATIO = 0.85;
|
|
3107
|
-
const threshold = Math.floor(currentContextWindow * PREFLIGHT_RATIO);
|
|
3108
|
-
const estimate = estimateMessagesTokens(systemPrompt, wireMessages);
|
|
3109
|
-
if (estimate > threshold) {
|
|
3110
|
-
try {
|
|
3111
|
-
const sweep = await archiveToolResults({
|
|
3112
|
-
root: `${this.#yeaftDir}/memory`,
|
|
3113
|
-
scopeDir: 'user',
|
|
3114
|
-
messages: wireMessages,
|
|
3115
|
-
turnAgeMin: 0,
|
|
3116
|
-
lengthMin: this.#config?.archive?.lengthMin ?? 2000,
|
|
3117
|
-
});
|
|
3118
|
-
wireMessages = sweep.nextMessages;
|
|
3119
|
-
if (sweep.archivedCount > 0) {
|
|
3120
|
-
this.#trace.log?.('preflight_sweep', {
|
|
3121
|
-
archivedCount: sweep.archivedCount,
|
|
3122
|
-
archivedBytes: sweep.archivedBytes,
|
|
3123
|
-
estimateBefore: estimate,
|
|
3124
|
-
threshold,
|
|
3125
|
-
contextWindow: currentContextWindow,
|
|
3126
|
-
});
|
|
3127
|
-
}
|
|
3128
|
-
} catch { /* best-effort */ }
|
|
3129
|
-
}
|
|
3130
|
-
}
|
|
3131
|
-
|
|
3132
2995
|
// Freeze diagnostics only after every provider-only trim/sweep has
|
|
3133
2996
|
// completed, so debug describes the exact request copy rather than the
|
|
3134
2997
|
// durable transcript or an earlier preflight candidate.
|
|
@@ -3224,7 +3087,7 @@ export class Engine {
|
|
|
3224
3087
|
// is not delivery: fetch may remain pending and then be aborted before
|
|
3225
3088
|
// the provider processes anything. Ack only after a normal stream end
|
|
3226
3089
|
// that included the provider's terminal stop event.
|
|
3227
|
-
const requestAsyncTaskIds =
|
|
3090
|
+
const requestAsyncTaskIds = deliveredTaskIds;
|
|
3228
3091
|
let sawProviderStop = false;
|
|
3229
3092
|
for await (const event of providerStream) {
|
|
3230
3093
|
// task-325a (abort-stop fix): per-event abort short-circuit.
|
|
@@ -4210,80 +4073,8 @@ export class Engine {
|
|
|
4210
4073
|
yield { type: 'turn_end', turnNumber, stopReason, threadId, terminal: true };
|
|
4211
4074
|
|
|
4212
4075
|
// Message durability is handled incrementally before this terminal
|
|
4213
|
-
// branch.
|
|
4214
|
-
|
|
4215
|
-
// PR-L: T2 end-of-turn (asynchronous) reflection. Fires when the
|
|
4216
|
-
// total tool count for this query() exceeds TURN_SUMMARY_THRESHOLD
|
|
4217
|
-
// (8) AND no T1 has actually rewritten the arc yet. Kicks off the
|
|
4218
|
-
// primary-model call without await; the next query()'s
|
|
4219
|
-
// `#applyPendingT2Reflections` carries the result forward.
|
|
4220
|
-
//
|
|
4221
|
-
// Periodic-T1 fix: gate on `t1CollapsesDone === 0`, NOT
|
|
4222
|
-
// `lastT1AtLoopCount === 0`. The catch block of T1 advances
|
|
4223
|
-
// `lastT1AtLoopCount` after a reflector error to avoid
|
|
4224
|
-
// tight-loop retries — but no collapse happened, so T2 should
|
|
4225
|
-
// still be allowed to fall back at end_turn. Fowler-review
|
|
4226
|
-
// critical finding.
|
|
4227
|
-
if (groupReflectionAllowed && queryToolCount > TURN_SUMMARY_THRESHOLD && t1CollapsesDone === 0) {
|
|
4228
|
-
const arcStart = turnStartIdx + 1;
|
|
4229
|
-
const arcEnd = conversationMessages.length - 1;
|
|
4230
|
-
if (arcEnd > arcStart) {
|
|
4231
|
-
const { pairs, assistantText } = extractToolPairsFromRange(
|
|
4232
|
-
conversationMessages, arcStart, arcEnd,
|
|
4233
|
-
);
|
|
4234
|
-
yield {
|
|
4235
|
-
type: 'reflection',
|
|
4236
|
-
turnId: queryTurnId,
|
|
4237
|
-
loopNumber: turnNumber,
|
|
4238
|
-
trigger: 't2',
|
|
4239
|
-
status: 'pending',
|
|
4240
|
-
loopRange: [arcStart, arcEnd],
|
|
4241
|
-
toolCount: pairs.length,
|
|
4242
|
-
};
|
|
4243
|
-
const promise = runT2Reflection({
|
|
4244
|
-
adapter: this.#adapter,
|
|
4245
|
-
model: this.#config.model,
|
|
4246
|
-
originalUserMsg: prompt,
|
|
4247
|
-
toolPairs: pairs,
|
|
4248
|
-
assistantText,
|
|
4249
|
-
language: this.#config.language,
|
|
4250
|
-
signal,
|
|
4251
|
-
onComplete: diagnostic => this.#trace.log?.('tool_reflection', {
|
|
4252
|
-
sessionId: runtimeSessionId, turnId: queryTurnId,
|
|
4253
|
-
trigger: 't2', model: this.#config.model, ...diagnostic,
|
|
4254
|
-
}),
|
|
4255
|
-
});
|
|
4256
|
-
// Detach: never await. The promise outlives this query() and
|
|
4257
|
-
// the next call will pick it up (or use the fallback stub if
|
|
4258
|
-
// it hasn't resolved by then).
|
|
4259
|
-
// PR-L follow-up: latch a synchronously-readable ready flag
|
|
4260
|
-
// and result on the info record so `#applyPendingT2Reflections`
|
|
4261
|
-
// can decide ready-vs-pending without racing microtasks.
|
|
4262
|
-
const info = {
|
|
4263
|
-
promise,
|
|
4264
|
-
loopRange: [arcStart, arcEnd],
|
|
4265
|
-
// Absolute indexes do not survive history-window reconstruction.
|
|
4266
|
-
arcKeys: conversationMessages.slice(arcStart, arcEnd + 1).map(foldingMessageKey),
|
|
4267
|
-
arcRows: conversationMessages.slice(arcStart, arcEnd + 1).map(message => ({
|
|
4268
|
-
role: message.role, id: message.id, _persistedMessageId: message._persistedMessageId,
|
|
4269
|
-
toolCallId: message.toolCallId, toolCalls: message.toolCalls?.map(call => ({ id: call.id })),
|
|
4270
|
-
})),
|
|
4271
|
-
count: pairs.length,
|
|
4272
|
-
originalUserMsg: prompt,
|
|
4273
|
-
originatingTurnId: queryTurnId,
|
|
4274
|
-
causalRootId,
|
|
4275
|
-
executionOrigin,
|
|
4276
|
-
ready: false,
|
|
4277
|
-
result: null,
|
|
4278
|
-
error: null,
|
|
4279
|
-
};
|
|
4280
|
-
promise.then(
|
|
4281
|
-
(v) => { info.ready = true; info.result = v; },
|
|
4282
|
-
(err) => { info.ready = true; info.error = err; },
|
|
4283
|
-
);
|
|
4284
|
-
this.#pendingT2.set(queryNumber, info);
|
|
4285
|
-
}
|
|
4286
|
-
}
|
|
4076
|
+
// branch. No reflection or durable history fold runs here; the separate
|
|
4077
|
+
// post-response compact is scheduled only after turn_close delivery.
|
|
4287
4078
|
|
|
4288
4079
|
break;
|
|
4289
4080
|
}
|
|
@@ -4301,6 +4092,7 @@ export class Engine {
|
|
|
4301
4092
|
effortDecision: snapshotEffortDecision(requestEffortDecision),
|
|
4302
4093
|
requestIdentity,
|
|
4303
4094
|
queryTurnId,
|
|
4095
|
+
causalRootId,
|
|
4304
4096
|
webResearch,
|
|
4305
4097
|
historyBeforeSeq: Number.isFinite(persistedQueryUser?.seq) ? persistedQueryUser.seq : parseSeqFromId(persistedQueryUser?.id),
|
|
4306
4098
|
router,
|
|
@@ -4413,7 +4205,6 @@ export class Engine {
|
|
|
4413
4205
|
// break out of the outer while-loop cleanly once the current
|
|
4414
4206
|
// tool batch finishes reporting.
|
|
4415
4207
|
let abortedDuringTools = false;
|
|
4416
|
-
let batchHasUnseenImages = false;
|
|
4417
4208
|
/** @type {string[]} */
|
|
4418
4209
|
const pendingDupReminders = [];
|
|
4419
4210
|
let terminateAfterDuplicateBatch = false;
|
|
@@ -4946,7 +4737,6 @@ export class Engine {
|
|
|
4946
4737
|
toolName: tc.name,
|
|
4947
4738
|
language: this.#config?.language,
|
|
4948
4739
|
});
|
|
4949
|
-
if (modelImages.length > 0) batchHasUnseenImages = true;
|
|
4950
4740
|
const toolMessage = {
|
|
4951
4741
|
role: 'tool',
|
|
4952
4742
|
toolCallId: tc.id,
|
|
@@ -5088,10 +4878,8 @@ export class Engine {
|
|
|
5088
4878
|
// clean wire shape. Used by `route_forward` to hand off control
|
|
5089
4879
|
// to other VPs without continuing to generate.
|
|
5090
4880
|
//
|
|
5091
|
-
//
|
|
5092
|
-
//
|
|
5093
|
-
// multi-iteration tool loops) and BEFORE the abortedDuringTools
|
|
5094
|
-
// check (so a clean handoff doesn't get reported as 'aborted').
|
|
4881
|
+
// A clean handoff must precede the abortedDuringTools check so it
|
|
4882
|
+
// does not get reported as 'aborted'.
|
|
5095
4883
|
if (endTurnRequested && !toolBatchBarrier) {
|
|
5096
4884
|
if (pendingSubAgentNotifs.length > 0) {
|
|
5097
4885
|
acknowledgePendingNotifications(notifScope, pendingSubAgentNotifs.map(n => n.id));
|
|
@@ -5110,180 +4898,6 @@ export class Engine {
|
|
|
5110
4898
|
break;
|
|
5111
4899
|
}
|
|
5112
4900
|
|
|
5113
|
-
// PR-L: T1 in-turn (synchronous) reflection. Fires every 30 completed
|
|
5114
|
-
// tool loops. A provider response containing many parallel tool calls is
|
|
5115
|
-
// one loop, not many. Generates a markdown reflection
|
|
5116
|
-
// over the assistant+tool arc since the last T1 firing (or
|
|
5117
|
-
// since the user prompt for the first batch) and rewrites that
|
|
5118
|
-
// range to a SINGLE synthetic user message before the next
|
|
5119
|
-
// adapter.stream() runs.
|
|
5120
|
-
//
|
|
5121
|
-
// Loop semantics:
|
|
5122
|
-
// - First interval: arcStartIdx = turnStartIdx + 1.
|
|
5123
|
-
// - Each subsequent interval: arcStartIdx is updated to the slot
|
|
5124
|
-
// right after the just-inserted reflection message; fires
|
|
5125
|
-
// again whenever 30 more tool loops have completed.
|
|
5126
|
-
// - The dedup Set key includes the loop count so each interval
|
|
5127
|
-
// within the same query gets a distinct entry — without
|
|
5128
|
-
// this the second batch would be silently skipped.
|
|
5129
|
-
const completedToolLoops = toolLoopTurns + 1;
|
|
5130
|
-
const t1BatchDue = completedToolLoops - lastT1AtLoopCount
|
|
5131
|
-
>= TOOL_LOOP_REFLECTION_INTERVAL;
|
|
5132
|
-
// Text-only reflection cannot consume visual input. Defer folding until
|
|
5133
|
-
// the primary provider has received this image batch at least once.
|
|
5134
|
-
if (t1BatchDue && !batchHasUnseenImages && !toolBatchBarrier
|
|
5135
|
-
&& !abortedDuringTools && !signal?.aborted) {
|
|
5136
|
-
const t1DedupKey = `${queryNumber}:t1-loop:${completedToolLoops}`;
|
|
5137
|
-
if (this.#reflectedTurns.has(t1DedupKey)) {
|
|
5138
|
-
// Defensive: should never hit since t1BatchDue gates re-entry
|
|
5139
|
-
// and queryNumber namespaces queries. Kept as belt-and-
|
|
5140
|
-
// suspenders against any future external mutation of the
|
|
5141
|
-
// cursor (or a re-entrant query() that this code doesn't
|
|
5142
|
-
// anticipate).
|
|
5143
|
-
} else {
|
|
5144
|
-
this.#reflectedTurns.add(t1DedupKey);
|
|
5145
|
-
const batchStart = arcStartIdx;
|
|
5146
|
-
const batchEnd = conversationMessages.length - 1;
|
|
5147
|
-
try {
|
|
5148
|
-
const { pairs, assistantText } = extractToolPairsFromRange(
|
|
5149
|
-
conversationMessages, batchStart, batchEnd,
|
|
5150
|
-
);
|
|
5151
|
-
yield {
|
|
5152
|
-
type: 'reflection',
|
|
5153
|
-
turnId: queryTurnId,
|
|
5154
|
-
threadId,
|
|
5155
|
-
loopNumber: turnNumber,
|
|
5156
|
-
trigger: 't1',
|
|
5157
|
-
status: 'pending',
|
|
5158
|
-
loopRange: [batchStart, batchEnd],
|
|
5159
|
-
toolCount: pairs.length,
|
|
5160
|
-
};
|
|
5161
|
-
// Observe only this synchronous query-owned call. Do not tap the
|
|
5162
|
-
// shared accounting adapter (also used by children), or feed these
|
|
5163
|
-
// costs into the main loop's token/dispatch budget. Capture raw usage
|
|
5164
|
-
// before reflection diagnostics normalize away cache-inclusion flags.
|
|
5165
|
-
const reflectionAdapter = typeof this.#adapter.call === 'function' ? {
|
|
5166
|
-
call: async params => {
|
|
5167
|
-
let dispatches = 0;
|
|
5168
|
-
let result;
|
|
5169
|
-
let failure;
|
|
5170
|
-
try {
|
|
5171
|
-
result = await this.#adapter.call({
|
|
5172
|
-
...params,
|
|
5173
|
-
onRequestStart: (...args) => {
|
|
5174
|
-
dispatches++;
|
|
5175
|
-
queryProgress.requestStarted();
|
|
5176
|
-
params.onRequestStart?.(...args);
|
|
5177
|
-
},
|
|
5178
|
-
});
|
|
5179
|
-
return result;
|
|
5180
|
-
} catch (error) {
|
|
5181
|
-
failure = error;
|
|
5182
|
-
throw error;
|
|
5183
|
-
} finally {
|
|
5184
|
-
// Native adapters report every dispatch, including a refreshed
|
|
5185
|
-
// credential retry. Legacy adapters without the hook count once.
|
|
5186
|
-
if (dispatches === 0) queryProgress.requestStarted();
|
|
5187
|
-
const usage = result?.usage || failure?.usage;
|
|
5188
|
-
if (usage) queryProgress.usageObserved(usage);
|
|
5189
|
-
}
|
|
5190
|
-
},
|
|
5191
|
-
} : this.#adapter;
|
|
5192
|
-
const { content, durationMs } = await runT1Reflection({
|
|
5193
|
-
adapter: reflectionAdapter,
|
|
5194
|
-
model: this.#config.model,
|
|
5195
|
-
originalUserMsg: prompt,
|
|
5196
|
-
toolPairs: pairs,
|
|
5197
|
-
assistantText,
|
|
5198
|
-
language: this.#config.language,
|
|
5199
|
-
signal,
|
|
5200
|
-
onComplete: diagnostic => this.#trace.log?.('tool_reflection', {
|
|
5201
|
-
sessionId: runtimeSessionId, turnId: queryTurnId,
|
|
5202
|
-
trigger: 't1', model: this.#config.model, ...diagnostic,
|
|
5203
|
-
}),
|
|
5204
|
-
});
|
|
5205
|
-
const { messages: next, reflection: reflectionMessage, foldedMessages } = collapseRangeToReflection(
|
|
5206
|
-
conversationMessages, batchStart, batchEnd, content,
|
|
5207
|
-
);
|
|
5208
|
-
const durableRowsInRange = foldedMessages
|
|
5209
|
-
.some(message => message?._persistedMessageId || message?.id);
|
|
5210
|
-
const persistedReflection = this.#persistFoldedRange(
|
|
5211
|
-
foldedMessages,
|
|
5212
|
-
reflectionMessage,
|
|
5213
|
-
{ sessionId: runtimeSessionId, model: currentModel, executionOrigin },
|
|
5214
|
-
);
|
|
5215
|
-
if (durableRowsInRange && !persistedReflection) {
|
|
5216
|
-
throw new Error('T1 reflection could not publish its durable range replacement');
|
|
5217
|
-
}
|
|
5218
|
-
if (persistedReflection?.id) reflectionMessage._persistedMessageId = persistedReflection.id;
|
|
5219
|
-
// Raw tool rows inside the replacement range are now tombstoned in
|
|
5220
|
-
// durable history. Late async completions must follow the folded
|
|
5221
|
-
// continuation path rather than appending to those stale rows.
|
|
5222
|
-
const foldedToolCallIds = conversationMessages
|
|
5223
|
-
.slice(batchStart, batchEnd + 1)
|
|
5224
|
-
.filter(message => message?.role === 'tool' && message.toolCallId)
|
|
5225
|
-
.map(message => message.toolCallId);
|
|
5226
|
-
for (const toolCallId of foldedToolCallIds) {
|
|
5227
|
-
this.#persistedToolMessages.delete(toolCallId);
|
|
5228
|
-
}
|
|
5229
|
-
conversationMessages.length = 0;
|
|
5230
|
-
for (const m of next) conversationMessages.push(m);
|
|
5231
|
-
// The preserved users precede the reflection. The next arc starts
|
|
5232
|
-
// immediately after it, i.e. at conversationMessages.length
|
|
5233
|
-
// (the next assistant message will land here).
|
|
5234
|
-
arcStartIdx = conversationMessages.length;
|
|
5235
|
-
// Read hints must not imply raw ranges remain in model context.
|
|
5236
|
-
fileReadObservations.clear();
|
|
5237
|
-
lastT1AtLoopCount = completedToolLoops;
|
|
5238
|
-
// Bump the success counter — used by the T2 schedule check
|
|
5239
|
-
// to decide whether T2 still has work to do at end_turn.
|
|
5240
|
-
// Distinct from lastT1AtLoopCount which the catch block
|
|
5241
|
-
// also bumps (but without rewriting history).
|
|
5242
|
-
t1CollapsesDone += 1;
|
|
5243
|
-
yield {
|
|
5244
|
-
type: 'reflection',
|
|
5245
|
-
turnId: queryTurnId,
|
|
5246
|
-
threadId,
|
|
5247
|
-
loopNumber: turnNumber,
|
|
5248
|
-
trigger: 't1',
|
|
5249
|
-
// PR-L bug fix: keep the same loopRange as the `pending` event
|
|
5250
|
-
// so the frontend key stays stable across pending → ready and
|
|
5251
|
-
// the spinner card is replaced in place (no orphan).
|
|
5252
|
-
status: 'ready',
|
|
5253
|
-
loopRange: [batchStart, batchEnd],
|
|
5254
|
-
toolCount: pairs.length,
|
|
5255
|
-
content,
|
|
5256
|
-
durationMs,
|
|
5257
|
-
};
|
|
5258
|
-
} catch (err) {
|
|
5259
|
-
// Best-effort. On failure leave history unchanged so the loop
|
|
5260
|
-
// continues normally — never block the turn.
|
|
5261
|
-
yield {
|
|
5262
|
-
type: 'reflection',
|
|
5263
|
-
turnId: queryTurnId,
|
|
5264
|
-
threadId,
|
|
5265
|
-
loopNumber: turnNumber,
|
|
5266
|
-
trigger: 't1',
|
|
5267
|
-
status: 'error',
|
|
5268
|
-
error: err && err.message || String(err),
|
|
5269
|
-
};
|
|
5270
|
-
// Advance lastT1AtLoopCount past this interval so we don't
|
|
5271
|
-
// tight-loop on a hiccuping reflector. The next attempt is
|
|
5272
|
-
// another 30 tool loops from now, not immediately. arcStartIdx is
|
|
5273
|
-
// left alone because history wasn't rewritten — the tail still
|
|
5274
|
-
// begins where it did. The trade-off: the next batch's
|
|
5275
|
-
// reflection will cover the tools that just failed too,
|
|
5276
|
-
// which is fine (they're still in conversationMessages).
|
|
5277
|
-
//
|
|
5278
|
-
// We do NOT bump t1CollapsesDone — see the variable's
|
|
5279
|
-
// declaration comment. This keeps the T2 fallback path live
|
|
5280
|
-
// when every T1 attempt has errored.
|
|
5281
|
-
lastT1AtLoopCount = completedToolLoops;
|
|
5282
|
-
}
|
|
5283
|
-
yield queryProgress.snapshot(progressIdentity);
|
|
5284
|
-
}
|
|
5285
|
-
}
|
|
5286
|
-
|
|
5287
4901
|
// task-325a: if abort fired between tools, converge now — emit
|
|
5288
4902
|
// the typed `aborted` event + a final turn_end with stopReason
|
|
5289
4903
|
// 'aborted' instead of looping back to a new adapter call.
|
|
@@ -5455,109 +5069,6 @@ export class Engine {
|
|
|
5455
5069
|
/** @returns {import('./mcp.js').MCPManager|null} */
|
|
5456
5070
|
get mcpManager() { return this.#mcpManager; }
|
|
5457
5071
|
|
|
5458
|
-
/**
|
|
5459
|
-
* PR-L — V7 Tool History Reflection helpers.
|
|
5460
|
-
*
|
|
5461
|
-
* `#applyPendingT2Reflections` is called at the start of every
|
|
5462
|
-
* `#runQuery` to carry forward any prior turn's async reflection. It is
|
|
5463
|
-
* a generator so it can yield reflection events to the engine consumer.
|
|
5464
|
-
* Non-blocking: never awaits a pending promise.
|
|
5465
|
-
*
|
|
5466
|
-
* @param {Array} conversationMessages
|
|
5467
|
-
* @param {string} originalUserMsg
|
|
5468
|
-
* @param {{sessionId?: string, model?: string}} context
|
|
5469
|
-
*/
|
|
5470
|
-
async *#applyPendingT2Reflections(conversationMessages, originalUserMsg, context = {}) {
|
|
5471
|
-
if (this.#pendingT2.size === 0) return;
|
|
5472
|
-
// Drain in insertion order (Map preserves it). We process all entries
|
|
5473
|
-
// because the user could send multiple prompts back-to-back before
|
|
5474
|
-
// the engine resumes — each historical turn gets its rewrite.
|
|
5475
|
-
const drained = [...this.#pendingT2.entries()];
|
|
5476
|
-
this.#pendingT2.clear();
|
|
5477
|
-
|
|
5478
|
-
for (const [turnNumber, info] of drained) {
|
|
5479
|
-
const keys = info.arcKeys;
|
|
5480
|
-
if (!Array.isArray(keys) || keys.length === 0) continue;
|
|
5481
|
-
const currentKeys = conversationMessages.map(foldingMessageKey);
|
|
5482
|
-
const startIdx = currentKeys.findIndex((key, index) => key === keys[0]
|
|
5483
|
-
&& keys.every((expected, offset) => currentKeys[index + offset] === expected));
|
|
5484
|
-
const endIdx = startIdx + keys.length - 1;
|
|
5485
|
-
const rangePresent = startIdx >= 0;
|
|
5486
|
-
|
|
5487
|
-
// PR-L follow-up: deterministic readiness check. The info record
|
|
5488
|
-
// carries `ready / result / error` flags that are flipped from the
|
|
5489
|
-
// promise's then/catch handler; reading them here is purely
|
|
5490
|
-
// synchronous bookkeeping — no microtask race.
|
|
5491
|
-
let content;
|
|
5492
|
-
let trigger;
|
|
5493
|
-
let durationMs = 0;
|
|
5494
|
-
if (!info.ready) {
|
|
5495
|
-
// Still in flight — fall back to the exec-log stub. Detach the
|
|
5496
|
-
// unresolved promise so we don't leak it (handlers above already
|
|
5497
|
-
// swallow rejection by routing into info.error).
|
|
5498
|
-
const entries = this.#execLog ? this.#execLog.readTurn(turnNumber) : [];
|
|
5499
|
-
content = buildFallbackStub({ execLogEntries: entries, originalUserMsg: info.originalUserMsg || originalUserMsg });
|
|
5500
|
-
trigger = 't2-fallback';
|
|
5501
|
-
} else if (info.error) {
|
|
5502
|
-
// Promise rejected — leave history unchanged, no event.
|
|
5503
|
-
continue;
|
|
5504
|
-
} else if (info.result && typeof info.result.content === 'string' && info.result.content) {
|
|
5505
|
-
content = info.result.content;
|
|
5506
|
-
trigger = 't2';
|
|
5507
|
-
durationMs = info.result.durationMs || 0;
|
|
5508
|
-
} else {
|
|
5509
|
-
// Resolved but with no usable content — defensively skip.
|
|
5510
|
-
continue;
|
|
5511
|
-
}
|
|
5512
|
-
|
|
5513
|
-
// Rewrite history and publish the same logical replacement to disk.
|
|
5514
|
-
const { messages: next, reflection: reflectionMessage, foldedMessages } = rangePresent
|
|
5515
|
-
? collapseRangeToReflection(conversationMessages, startIdx, endIdx, content)
|
|
5516
|
-
: collapseRangeToReflection(info.arcRows, 0, info.arcRows.length - 1, content);
|
|
5517
|
-
const durableRowsInRange = foldedMessages
|
|
5518
|
-
.some(message => message?._persistedMessageId || message?.id);
|
|
5519
|
-
const persistedReflection = this.#persistFoldedRange(
|
|
5520
|
-
foldedMessages,
|
|
5521
|
-
reflectionMessage,
|
|
5522
|
-
{
|
|
5523
|
-
...context,
|
|
5524
|
-
causalRootId: info.causalRootId || null,
|
|
5525
|
-
executionOrigin: info.executionOrigin === 'route_forward' ? 'route_forward' : null,
|
|
5526
|
-
},
|
|
5527
|
-
);
|
|
5528
|
-
if (durableRowsInRange && !persistedReflection) continue;
|
|
5529
|
-
if (persistedReflection?.id) reflectionMessage._persistedMessageId = persistedReflection.id;
|
|
5530
|
-
// T2 replaces this arc in durable and in-memory history just like T1.
|
|
5531
|
-
// Forget its raw result handles before a late async task can append to a
|
|
5532
|
-
// tombstoned tool row at the next provider boundary.
|
|
5533
|
-
const foldedToolCallIds = foldedMessages
|
|
5534
|
-
.filter(message => message?.role === 'tool' && message.toolCallId)
|
|
5535
|
-
.map(message => message.toolCallId);
|
|
5536
|
-
for (const toolCallId of foldedToolCallIds) {
|
|
5537
|
-
this.#persistedToolMessages.delete(toolCallId);
|
|
5538
|
-
}
|
|
5539
|
-
// History may have evicted the old arc. Publish only its original
|
|
5540
|
-
// durable row IDs; never replace unrelated rows at stale indexes.
|
|
5541
|
-
if (rangePresent) {
|
|
5542
|
-
conversationMessages.length = 0;
|
|
5543
|
-
for (const m of next) conversationMessages.push(m);
|
|
5544
|
-
}
|
|
5545
|
-
|
|
5546
|
-
yield {
|
|
5547
|
-
type: 'reflection',
|
|
5548
|
-
turnId: info.originatingTurnId || null,
|
|
5549
|
-
trigger,
|
|
5550
|
-
status: 'ready',
|
|
5551
|
-
// Wire identity must match the original pending card, even if the
|
|
5552
|
-
// history window shifted or evicted its internal replacement range.
|
|
5553
|
-
loopRange: info.loopRange,
|
|
5554
|
-
toolCount: info.count || 0,
|
|
5555
|
-
content,
|
|
5556
|
-
durationMs,
|
|
5557
|
-
};
|
|
5558
|
-
}
|
|
5559
|
-
}
|
|
5560
|
-
|
|
5561
5072
|
/**
|
|
5562
5073
|
* PR-L — read-only accessor for tests.
|
|
5563
5074
|
*/
|
|
@@ -5687,7 +5198,7 @@ export class Engine {
|
|
|
5687
5198
|
*
|
|
5688
5199
|
* @param {string} taskId
|
|
5689
5200
|
* @param {string|Array} content — pre-formatted task result body
|
|
5690
|
-
* @param {{ preview?: string, sessionId?: string, vpId?: string, threadId?: string, taskKind?: string, taskStatus?: string, turnId?: string }} [opts]
|
|
5201
|
+
* @param {{ preview?: string, sessionId?: string, vpId?: string, threadId?: string, taskKind?: string, taskStatus?: string, turnId?: string, causalRootId?: string }} [opts]
|
|
5691
5202
|
* @returns {boolean}
|
|
5692
5203
|
*/
|
|
5693
5204
|
notifyAsyncTaskCompleted(taskId, content, opts = {}) {
|
|
@@ -5712,10 +5223,9 @@ export class Engine {
|
|
|
5712
5223
|
? opts.preview
|
|
5713
5224
|
: (typeof content === 'string' ? content.slice(0, 200) : '[task result]');
|
|
5714
5225
|
const meta = this.#asyncTaskToolMeta.get(taskId) || {};
|
|
5715
|
-
// A completion
|
|
5716
|
-
//
|
|
5717
|
-
//
|
|
5718
|
-
// a raw tool row after the reflection and invalidate the fold.
|
|
5226
|
+
// A completion updates its original tool row when available. Legacy folded
|
|
5227
|
+
// histories instead receive a continuation note; provider-only compaction
|
|
5228
|
+
// never clears this durable handle or changes result ownership.
|
|
5719
5229
|
const hasLiveToolRow = typeof meta.toolCallId === 'string'
|
|
5720
5230
|
&& meta.toolCallId
|
|
5721
5231
|
&& Boolean(this.#persistedToolMessages.get(meta.toolCallId));
|
|
@@ -5728,6 +5238,11 @@ export class Engine {
|
|
|
5728
5238
|
taskKind: opts.taskKind,
|
|
5729
5239
|
taskStatus: opts.taskStatus,
|
|
5730
5240
|
turnId: opts.turnId || meta.turnId || null,
|
|
5241
|
+
// Registration owns provenance even when newer human input arrives before
|
|
5242
|
+
// completion. Retain it until provider acceptance or undelivered rescue.
|
|
5243
|
+
causalRootId: meta.causalRootId
|
|
5244
|
+
|| (typeof opts.causalRootId === 'string' ? opts.causalRootId.trim() : null)
|
|
5245
|
+
|| this.#currentCausalRootId || null,
|
|
5731
5246
|
};
|
|
5732
5247
|
this.#acceptedAsyncTaskResults.set(taskId, delivery);
|
|
5733
5248
|
this.#asyncTaskToolMeta.delete(taskId);
|
|
@@ -5741,9 +5256,9 @@ export class Engine {
|
|
|
5741
5256
|
sessionId: delivery.sessionId,
|
|
5742
5257
|
vpId: delivery.vpId,
|
|
5743
5258
|
turnId: delivery.turnId,
|
|
5744
|
-
|
|
5745
|
-
// durable
|
|
5746
|
-
//
|
|
5259
|
+
causalRootId: delivery.causalRootId,
|
|
5260
|
+
// Capture durable row ownership at completion time, independently of
|
|
5261
|
+
// any provider-only compacted request copy.
|
|
5747
5262
|
folded: !hasLiveToolRow,
|
|
5748
5263
|
});
|
|
5749
5264
|
} else {
|
|
@@ -5762,7 +5277,7 @@ export class Engine {
|
|
|
5762
5277
|
* Register a result-producing async task as belonging to the current query.
|
|
5763
5278
|
* Called by tools such as SpawnAgent via `toolCtx.registerAsyncTask`.
|
|
5764
5279
|
* @param {string} taskId
|
|
5765
|
-
* @param {{ id?: string, name?: string, threadId?: string, toolCallId?: string, toolName?: string, sessionId?: string, vpId?: string, turnId?: string }} [meta]
|
|
5280
|
+
* @param {{ id?: string, name?: string, threadId?: string, toolCallId?: string, toolName?: string, sessionId?: string, vpId?: string, turnId?: string, causalRootId?: string }} [meta]
|
|
5766
5281
|
* @returns {void}
|
|
5767
5282
|
*/
|
|
5768
5283
|
#registerAsyncTask(taskId, meta = {}) {
|
|
@@ -5771,23 +5286,23 @@ export class Engine {
|
|
|
5771
5286
|
const toolCallId = typeof meta.toolCallId === 'string' && meta.toolCallId
|
|
5772
5287
|
? meta.toolCallId
|
|
5773
5288
|
: (typeof meta.id === 'string' && meta.id ? meta.id : null);
|
|
5774
|
-
|
|
5775
|
-
|
|
5776
|
-
|
|
5777
|
-
|
|
5778
|
-
|
|
5779
|
-
|
|
5780
|
-
|
|
5781
|
-
|
|
5782
|
-
|
|
5783
|
-
|
|
5784
|
-
|
|
5785
|
-
|
|
5786
|
-
|
|
5787
|
-
|
|
5788
|
-
|
|
5789
|
-
|
|
5790
|
-
}
|
|
5289
|
+
this.#asyncTaskToolMeta.set(taskId, {
|
|
5290
|
+
toolCallId,
|
|
5291
|
+
toolName: typeof meta.toolName === 'string' && meta.toolName ? meta.toolName : (typeof meta.name === 'string' ? meta.name : undefined),
|
|
5292
|
+
threadId: typeof meta.threadId === 'string' && meta.threadId ? meta.threadId : undefined,
|
|
5293
|
+
sessionId: typeof meta.sessionId === 'string' && meta.sessionId
|
|
5294
|
+
? meta.sessionId
|
|
5295
|
+
: (this.#sessionId || null),
|
|
5296
|
+
vpId: typeof meta.vpId === 'string' && meta.vpId ? meta.vpId : (this.#vpId || null),
|
|
5297
|
+
// query() exposes the wire turn id while the task is registered.
|
|
5298
|
+
// Keep provenance even for legacy missing/folded tool rows.
|
|
5299
|
+
turnId: typeof meta.turnId === 'string' && meta.turnId
|
|
5300
|
+
? meta.turnId
|
|
5301
|
+
: (this.#currentQueryTurnId || null),
|
|
5302
|
+
causalRootId: typeof meta.causalRootId === 'string' && meta.causalRootId.trim()
|
|
5303
|
+
? meta.causalRootId.trim()
|
|
5304
|
+
: (this.#currentCausalRootId || null),
|
|
5305
|
+
});
|
|
5791
5306
|
try { this.#asyncTaskCoordinator?.onRegister?.(taskId, this); } catch { /* coord must not throw into tools */ }
|
|
5792
5307
|
}
|
|
5793
5308
|
|