@leanandmean/coding-agent 0.74.1-scramjet.7 → 0.74.1-scramjet.71
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/CHANGELOG.md +32 -0
- package/LICENSE +202 -0
- package/README.md +37 -9
- package/dist/cli/args.d.ts +2 -0
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +45 -1
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/initial-message.d.ts +1 -0
- package/dist/cli/initial-message.d.ts.map +1 -1
- package/dist/cli/initial-message.js +3 -0
- package/dist/cli/initial-message.js.map +1 -1
- package/dist/cli/list-models.d.ts +2 -0
- package/dist/cli/list-models.d.ts.map +1 -1
- package/dist/cli/list-models.js +4 -1
- package/dist/cli/list-models.js.map +1 -1
- package/dist/config.d.ts +9 -2
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +33 -22
- package/dist/config.js.map +1 -1
- package/dist/core/agent-session-runtime.d.ts +12 -3
- package/dist/core/agent-session-runtime.d.ts.map +1 -1
- package/dist/core/agent-session-runtime.js +64 -38
- package/dist/core/agent-session-runtime.js.map +1 -1
- package/dist/core/agent-session-services.d.ts +2 -0
- package/dist/core/agent-session-services.d.ts.map +1 -1
- package/dist/core/agent-session-services.js +3 -0
- package/dist/core/agent-session-services.js.map +1 -1
- package/dist/core/agent-session.d.ts +84 -17
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +1082 -209
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/compaction/branch-summarization.d.ts.map +1 -1
- package/dist/core/compaction/branch-summarization.js +21 -17
- package/dist/core/compaction/branch-summarization.js.map +1 -1
- package/dist/core/compaction/compaction.d.ts +3 -5
- package/dist/core/compaction/compaction.d.ts.map +1 -1
- package/dist/core/compaction/compaction.js +43 -10
- package/dist/core/compaction/compaction.js.map +1 -1
- package/dist/core/exec.d.ts +12 -0
- package/dist/core/exec.d.ts.map +1 -1
- package/dist/core/exec.js +74 -28
- package/dist/core/exec.js.map +1 -1
- package/dist/core/export-html/template.js +4 -0
- package/dist/core/extensions/index.d.ts +1 -1
- package/dist/core/extensions/index.d.ts.map +1 -1
- package/dist/core/extensions/index.js.map +1 -1
- package/dist/core/extensions/runner.d.ts +9 -5
- package/dist/core/extensions/runner.d.ts.map +1 -1
- package/dist/core/extensions/runner.js +160 -3
- package/dist/core/extensions/runner.js.map +1 -1
- package/dist/core/extensions/types.d.ts +85 -8
- package/dist/core/extensions/types.d.ts.map +1 -1
- package/dist/core/extensions/types.js.map +1 -1
- package/dist/core/keybindings.d.ts +8 -0
- package/dist/core/keybindings.d.ts.map +1 -1
- package/dist/core/model-registry.d.ts +103 -0
- package/dist/core/model-registry.d.ts.map +1 -1
- package/dist/core/model-registry.js +272 -28
- package/dist/core/model-registry.js.map +1 -1
- package/dist/core/model-resolver.d.ts.map +1 -1
- package/dist/core/model-resolver.js +24 -6
- package/dist/core/model-resolver.js.map +1 -1
- package/dist/core/output-throughput-history.d.ts +25 -0
- package/dist/core/output-throughput-history.d.ts.map +1 -0
- package/dist/core/output-throughput-history.js +240 -0
- package/dist/core/output-throughput-history.js.map +1 -0
- package/dist/core/output-throughput.d.ts +43 -0
- package/dist/core/output-throughput.d.ts.map +1 -0
- package/dist/core/output-throughput.js +161 -0
- package/dist/core/output-throughput.js.map +1 -0
- package/dist/core/resource-loader.d.ts +6 -0
- package/dist/core/resource-loader.d.ts.map +1 -1
- package/dist/core/resource-loader.js +76 -21
- package/dist/core/resource-loader.js.map +1 -1
- package/dist/core/scramjet-command-parser.d.ts +4 -0
- package/dist/core/scramjet-command-parser.d.ts.map +1 -1
- package/dist/core/scramjet-command-parser.js +72 -1
- package/dist/core/scramjet-command-parser.js.map +1 -1
- package/dist/core/sdk.d.ts +4 -0
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +70 -11
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/session-manager.d.ts +11 -3
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +46 -11
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/settings-manager.d.ts +19 -6
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +131 -17
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/skills.d.ts.map +1 -1
- package/dist/core/skills.js +6 -2
- package/dist/core/skills.js.map +1 -1
- package/dist/core/system-prompt.d.ts +2 -0
- package/dist/core/system-prompt.d.ts.map +1 -1
- package/dist/core/system-prompt.js +5 -2
- package/dist/core/system-prompt.js.map +1 -1
- package/dist/core/tools/bash.d.ts.map +1 -1
- package/dist/core/tools/bash.js +4 -3
- package/dist/core/tools/bash.js.map +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/main.d.ts +119 -0
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +113 -11
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/components/armin.d.ts +4 -2
- package/dist/modes/interactive/components/armin.d.ts.map +1 -1
- package/dist/modes/interactive/components/armin.js +14 -8
- package/dist/modes/interactive/components/armin.js.map +1 -1
- package/dist/modes/interactive/components/assistant-message.d.ts +3 -1
- package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/assistant-message.js +19 -8
- package/dist/modes/interactive/components/assistant-message.js.map +1 -1
- package/dist/modes/interactive/components/daxnuts.d.ts +4 -1
- package/dist/modes/interactive/components/daxnuts.d.ts.map +1 -1
- package/dist/modes/interactive/components/daxnuts.js +14 -6
- package/dist/modes/interactive/components/daxnuts.js.map +1 -1
- package/dist/modes/interactive/components/extension-editor.d.ts.map +1 -1
- package/dist/modes/interactive/components/extension-editor.js +5 -3
- package/dist/modes/interactive/components/extension-editor.js.map +1 -1
- package/dist/modes/interactive/components/extension-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/extension-selector.js +4 -3
- package/dist/modes/interactive/components/extension-selector.js.map +1 -1
- package/dist/modes/interactive/components/footer.d.ts +4 -0
- package/dist/modes/interactive/components/footer.d.ts.map +1 -1
- package/dist/modes/interactive/components/footer.js +24 -20
- package/dist/modes/interactive/components/footer.js.map +1 -1
- package/dist/modes/interactive/components/model-selector.d.ts +3 -3
- package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/model-selector.js +10 -7
- package/dist/modes/interactive/components/model-selector.js.map +1 -1
- package/dist/modes/interactive/components/scoped-models-selector.d.ts +3 -1
- package/dist/modes/interactive/components/scoped-models-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/scoped-models-selector.js +8 -2
- package/dist/modes/interactive/components/scoped-models-selector.js.map +1 -1
- package/dist/modes/interactive/components/settings-selector.d.ts +17 -3
- package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/settings-selector.js +87 -16
- package/dist/modes/interactive/components/settings-selector.js.map +1 -1
- package/dist/modes/interactive/components/thinking-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/thinking-selector.js +2 -1
- package/dist/modes/interactive/components/thinking-selector.js.map +1 -1
- package/dist/modes/interactive/components/tool-execution.d.ts +19 -1
- package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
- package/dist/modes/interactive/components/tool-execution.js +129 -36
- package/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/dist/modes/interactive/components/user-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/user-message.js +3 -2
- package/dist/modes/interactive/components/user-message.js.map +1 -1
- package/dist/modes/interactive/components/visual-truncate.d.ts.map +1 -1
- package/dist/modes/interactive/components/visual-truncate.js +2 -2
- package/dist/modes/interactive/components/visual-truncate.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +37 -1
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +902 -177
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/interactive/spellcheck.d.ts +26 -0
- package/dist/modes/interactive/spellcheck.d.ts.map +1 -0
- package/dist/modes/interactive/spellcheck.js +144 -0
- package/dist/modes/interactive/spellcheck.js.map +1 -0
- package/dist/modes/interactive/theme/{dark.json → pi-dark.json} +3 -2
- package/dist/modes/interactive/theme/{light.json → pi-light.json} +27 -26
- package/dist/modes/interactive/theme/theme-schema.json +6 -1
- package/dist/modes/interactive/theme/theme.d.ts +24 -4
- package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
- package/dist/modes/interactive/theme/theme.js +123 -22
- package/dist/modes/interactive/theme/theme.js.map +1 -1
- package/dist/modes/print-mode.d.ts.map +1 -1
- package/dist/modes/print-mode.js +5 -0
- package/dist/modes/print-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-client.d.ts +3 -1
- package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-client.js.map +1 -1
- package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-mode.js +6 -1
- package/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/dist/package-manager-cli.d.ts.map +1 -1
- package/dist/package-manager-cli.js +33 -14
- package/dist/package-manager-cli.js.map +1 -1
- package/dist/utils/clipboard-native.d.ts +1 -0
- package/dist/utils/clipboard-native.d.ts.map +1 -1
- package/dist/utils/clipboard-native.js.map +1 -1
- package/dist/utils/clipboard.d.ts +6 -0
- package/dist/utils/clipboard.d.ts.map +1 -1
- package/dist/utils/clipboard.js +278 -10
- package/dist/utils/clipboard.js.map +1 -1
- package/dist/utils/tools-manager.d.ts +1 -1
- package/dist/utils/tools-manager.d.ts.map +1 -1
- package/dist/utils/tools-manager.js +7 -6
- package/dist/utils/tools-manager.js.map +1 -1
- package/dist/utils/version-check.d.ts +17 -5
- package/dist/utils/version-check.d.ts.map +1 -1
- package/dist/utils/version-check.js +88 -21
- package/dist/utils/version-check.js.map +1 -1
- package/docs/compaction.md +21 -3
- package/docs/custom-provider.md +33 -4
- package/docs/extensions.md +150 -24
- package/docs/json.md +8 -0
- package/docs/keybindings.md +25 -1
- package/docs/models.md +103 -4
- package/docs/packages.md +2 -0
- package/docs/prompt-templates.md +2 -0
- package/docs/quickstart.md +1 -4
- package/docs/rpc.md +16 -6
- package/docs/sdk.md +48 -3
- package/docs/session-format.md +5 -1
- package/docs/settings.md +19 -5
- package/docs/skills.md +7 -5
- package/docs/terminal-setup.md +79 -2
- package/docs/terminal-verification.md +71 -0
- package/docs/themes.md +63 -10
- package/docs/tui.md +120 -25
- package/docs/usage.md +6 -4
- package/examples/extensions/border-status-editor.ts +2 -3
- package/examples/extensions/doom-overlay/doom-component.ts +3 -3
- package/examples/extensions/mac-system-theme.ts +3 -2
- package/examples/extensions/modal-editor.ts +13 -8
- package/examples/extensions/overlay-qa-tests.ts +22 -9
- package/examples/extensions/overlay-test.ts +5 -3
- package/examples/extensions/snake.ts +3 -1
- package/examples/extensions/space-invaders.ts +6 -3
- package/examples/extensions/subagent/index.ts +20 -17
- package/package.json +69 -58
|
@@ -14,19 +14,25 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
16
16
|
import { basename, dirname, resolve } from "node:path";
|
|
17
|
-
|
|
17
|
+
// SCRAMJET-DIVERGENCE: provider-safe harness tool-call id generation for the persisted-settlement
|
|
18
|
+
// acknowledgement key (#341).
|
|
19
|
+
import { generateHarnessToolCallId } from "@leanandmean/agent";
|
|
20
|
+
import { blocksFailureRecovery, clampThinkingLevel, cleanupSessionResources, flattenSystemPrompt, getSupportedThinkingLevels, inspectFailureEvidence, isContextOverflow, modelsAreEqual, resetApiProviders, } from "@leanandmean/ai";
|
|
18
21
|
import { theme } from "../modes/interactive/theme/theme.js";
|
|
19
22
|
import { stripFrontmatter } from "../utils/frontmatter.js";
|
|
20
23
|
import { sleep } from "../utils/sleep.js";
|
|
21
24
|
import { formatNoApiKeyFoundMessage, formatNoModelSelectedMessage } from "./auth-guidance.js";
|
|
22
25
|
import { executeBashWithOperations } from "./bash-executor.js";
|
|
23
|
-
import { calculateContextTokens, collectEntriesForBranchSummary, compact, estimateContextTokens, generateBranchSummary, prepareCompaction, shouldCompact, } from "./compaction/index.js";
|
|
26
|
+
import { calculateContextTokens, collectEntriesForBranchSummary, compact, estimateContextTokens, estimateTokens, generateBranchSummary, prepareCompaction, shouldCompact, } from "./compaction/index.js";
|
|
24
27
|
import { DEFAULT_THINKING_LEVEL } from "./defaults.js";
|
|
25
28
|
import { exportSessionToHtml } from "./export-html/index.js";
|
|
26
29
|
import { createToolHtmlRenderer } from "./export-html/tool-renderer.js";
|
|
27
30
|
import { ExtensionRunner, wrapRegisteredTools, } from "./extensions/index.js";
|
|
28
31
|
import { emitSessionShutdownEvent, spliceContributedSections } from "./extensions/runner.js";
|
|
32
|
+
import { OutputThroughputTracker } from "./output-throughput.js";
|
|
33
|
+
import { OutputThroughputHistoryStore } from "./output-throughput-history.js";
|
|
29
34
|
import { expandPromptTemplate } from "./prompt-templates.js";
|
|
35
|
+
import { indexScramjetCommandStarts, restoreScramjetCommandInvocation } from "./scramjet-command-parser.js";
|
|
30
36
|
import { CURRENT_SESSION_VERSION, getLatestCompactionEntry } from "./session-manager.js";
|
|
31
37
|
import { createSyntheticSourceInfo } from "./source-info.js";
|
|
32
38
|
import { buildSystemPromptSections } from "./system-prompt.js";
|
|
@@ -53,35 +59,80 @@ export function parseSkillBlock(text) {
|
|
|
53
59
|
// ============================================================================
|
|
54
60
|
/** Standard thinking levels */
|
|
55
61
|
const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high"];
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
62
|
+
function createDeferred() {
|
|
63
|
+
let resolve;
|
|
64
|
+
let reject;
|
|
65
|
+
const promise = new Promise((res, rej) => {
|
|
66
|
+
resolve = res;
|
|
67
|
+
reject = rej;
|
|
68
|
+
});
|
|
69
|
+
promise.catch(() => { });
|
|
70
|
+
return { promise, resolve, reject, settled: false };
|
|
71
|
+
}
|
|
72
|
+
function resolveDeferred(deferred, value) {
|
|
73
|
+
if (deferred.settled)
|
|
74
|
+
return false;
|
|
75
|
+
deferred.settled = true;
|
|
76
|
+
deferred.resolve(value);
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
function rejectDeferred(deferred, error) {
|
|
80
|
+
if (deferred.settled)
|
|
81
|
+
return false;
|
|
82
|
+
deferred.settled = true;
|
|
83
|
+
deferred.reject(error);
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
function settleDeferred(deferred, error) {
|
|
87
|
+
return error ? rejectDeferred(deferred, error) : resolveDeferred(deferred, undefined);
|
|
88
|
+
}
|
|
89
|
+
function validRecordedCost(value) {
|
|
90
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0;
|
|
91
|
+
}
|
|
59
92
|
export class AgentSession {
|
|
60
93
|
agent;
|
|
61
94
|
sessionManager;
|
|
62
95
|
settingsManager;
|
|
96
|
+
outputThroughputHistoryPath;
|
|
63
97
|
_scopedModels;
|
|
64
98
|
// Event subscription state
|
|
65
99
|
_unsubscribeAgent;
|
|
66
100
|
_eventListeners = [];
|
|
67
101
|
_agentEventQueue = Promise.resolve();
|
|
102
|
+
// SCRAMJET-DIVERGENCE: execution ownership survives final-event queue latency until append succeeds (#598).
|
|
103
|
+
_toolCostExecutions = new Map();
|
|
104
|
+
_pendingToolCosts = new Set();
|
|
105
|
+
_historicalCostDefinitions = new Map();
|
|
106
|
+
_outputThroughputTracker = new OutputThroughputTracker();
|
|
107
|
+
_outputThroughputHistory;
|
|
108
|
+
_outputThroughputGeneration;
|
|
109
|
+
_latestOutputThroughputSample;
|
|
110
|
+
// SCRAMJET-DIVERGENCE: harness-tool persisted-settlement state (#341). `_disposed` gates post-
|
|
111
|
+
// teardown invocations; the map keys each in-flight invocation's tool-call id to its persistence
|
|
112
|
+
// acknowledgement so `_processAgentEvent` can settle it after the matching tool-result is persisted.
|
|
113
|
+
_disposed = false;
|
|
114
|
+
_harnessPersistenceAcks = new Map();
|
|
68
115
|
/** Tracks pending steering messages for UI display. Removed when delivered. */
|
|
69
116
|
_steeringMessages = [];
|
|
70
117
|
/** Tracks pending follow-up messages for UI display. Removed when delivered. */
|
|
71
118
|
_followUpMessages = [];
|
|
72
119
|
/** Messages queued to be included with the next user prompt as context ("asides"). */
|
|
73
120
|
_pendingNextTurnMessages = [];
|
|
121
|
+
_inputSessionEntries = new WeakMap();
|
|
74
122
|
// Compaction state
|
|
75
123
|
_compactionAbortController = undefined;
|
|
124
|
+
_manualCompactionDrain;
|
|
76
125
|
_autoCompactionAbortController = undefined;
|
|
77
126
|
_overflowRecoveryAttempted = false;
|
|
78
127
|
// Branch summarization state
|
|
79
128
|
_branchSummaryAbortController = undefined;
|
|
80
129
|
// Retry state
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
130
|
+
_retry = undefined;
|
|
131
|
+
_runRetryCount = 0;
|
|
132
|
+
_activeAgentRunSettlement = undefined;
|
|
133
|
+
_pendingAgentRunStart = undefined;
|
|
134
|
+
_unsettledAgentRuns = new Set();
|
|
135
|
+
_unsettledRetryChains = new Set();
|
|
85
136
|
// Bash execution state
|
|
86
137
|
_bashAbortController = undefined;
|
|
87
138
|
_pendingBashMessages = [];
|
|
@@ -104,6 +155,9 @@ export class AgentSession {
|
|
|
104
155
|
_extensionErrorUnsubscriber;
|
|
105
156
|
// Model registry for API key resolution
|
|
106
157
|
_modelRegistry;
|
|
158
|
+
// SCRAMJET-DIVERGENCE: Ephemeral routes retain live preferences and journal changes without changing shared defaults.
|
|
159
|
+
_persistModelPreferences;
|
|
160
|
+
_sessionThinkingLevelPreference;
|
|
107
161
|
// Tool registry for extension getTools/setTools
|
|
108
162
|
_toolRegistry = new Map();
|
|
109
163
|
_toolDefinitions = new Map();
|
|
@@ -114,15 +168,28 @@ export class AgentSession {
|
|
|
114
168
|
// can apply per-section cache control.
|
|
115
169
|
_baseSystemPromptSections = [];
|
|
116
170
|
_baseSystemPromptOptions;
|
|
171
|
+
// SCRAMJET-DIVERGENCE: active-tool prompt rebuilds preserve the current top-level run's
|
|
172
|
+
// before_agent_start composition while new prompts and reloads establish a fresh composition (#524).
|
|
173
|
+
_runPromptComposition = { kind: "generated-base" };
|
|
117
174
|
constructor(config) {
|
|
118
175
|
this.agent = config.agent;
|
|
119
176
|
this.sessionManager = config.sessionManager;
|
|
120
177
|
this.settingsManager = config.settingsManager;
|
|
178
|
+
this.outputThroughputHistoryPath = config.outputThroughputHistoryPath;
|
|
179
|
+
this._outputThroughputHistory = new OutputThroughputHistoryStore(this.outputThroughputHistoryPath, (diagnostic) => {
|
|
180
|
+
this._extensionRunner.emitError({
|
|
181
|
+
extensionPath: "output-throughput-history",
|
|
182
|
+
event: diagnostic.operation,
|
|
183
|
+
error: `${diagnostic.message} (${diagnostic.path})`,
|
|
184
|
+
stack: diagnostic.cause instanceof Error ? diagnostic.cause.stack : undefined,
|
|
185
|
+
});
|
|
186
|
+
});
|
|
121
187
|
this._scopedModels = config.scopedModels ?? [];
|
|
122
188
|
this._resourceLoader = config.resourceLoader;
|
|
123
189
|
this._customTools = config.customTools ?? [];
|
|
124
190
|
this._cwd = config.cwd;
|
|
125
191
|
this._modelRegistry = config.modelRegistry;
|
|
192
|
+
this._persistModelPreferences = config.persistModelPreferences ?? true;
|
|
126
193
|
this._extensionRunnerRef = config.extensionRunnerRef;
|
|
127
194
|
this._initialActiveToolNames = config.initialActiveToolNames;
|
|
128
195
|
this._allowedToolNames = config.allowedToolNames ? new Set(config.allowedToolNames) : undefined;
|
|
@@ -161,8 +228,8 @@ export class AgentSession {
|
|
|
161
228
|
throw new Error(formatNoApiKeyFoundMessage(model.provider));
|
|
162
229
|
}
|
|
163
230
|
// SCRAMJET-DIVERGENCE: _drainAgentEventQueue ensures all queued async extension event
|
|
164
|
-
// processing has settled before proceeding. Used by beforeToolBatch (pre-extraction drain)
|
|
165
|
-
//
|
|
231
|
+
// processing has settled before proceeding. Used by beforeToolBatch (pre-extraction drain),
|
|
232
|
+
// beforeToolCall (per-tool drain), and the awaited turn_end listener barrier.
|
|
166
233
|
async _drainAgentEventQueue() {
|
|
167
234
|
await this._agentEventQueue;
|
|
168
235
|
}
|
|
@@ -241,46 +308,200 @@ export class AgentSession {
|
|
|
241
308
|
followUp: [...this._followUpMessages],
|
|
242
309
|
});
|
|
243
310
|
}
|
|
244
|
-
// Track last assistant message for auto-compaction check
|
|
245
|
-
_lastAssistantMessage = undefined;
|
|
246
311
|
/** Internal handler for agent events - shared by subscribe and reconnect */
|
|
247
312
|
_handleAgentEvent = (event) => {
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
this.
|
|
254
|
-
|
|
313
|
+
if (this._disposed)
|
|
314
|
+
return;
|
|
315
|
+
this._captureOutputThroughput(event);
|
|
316
|
+
const toolCost = this._captureToolCost(event);
|
|
317
|
+
const runSettlement = this._captureAgentRunSettlement(event);
|
|
318
|
+
const harnessAck = this._harnessPersistenceAckForEvent(event);
|
|
319
|
+
const processing = this._agentEventQueue.then(() => this._processAgentEventTracked(event, runSettlement, harnessAck, toolCost), () => this._processAgentEventTracked(event, runSettlement, harnessAck, toolCost));
|
|
320
|
+
this._agentEventQueue = processing;
|
|
321
|
+
if (harnessAck)
|
|
322
|
+
harnessAck.persistenceTail = processing;
|
|
255
323
|
// Keep queue alive if an event handler fails
|
|
256
|
-
|
|
324
|
+
processing.catch(() => { });
|
|
325
|
+
// SCRAMJET-DIVERGENCE: turn_end tool/prompt changes must settle before Agent snapshots
|
|
326
|
+
// live state for the next provider request (#524).
|
|
327
|
+
if (event.type === "turn_end")
|
|
328
|
+
return this._agentEventQueue;
|
|
257
329
|
};
|
|
258
|
-
|
|
259
|
-
|
|
330
|
+
// SCRAMJET-DIVERGENCE: scalar reports are captured before mutable events enter async hooks (#598).
|
|
331
|
+
_captureToolCost(event) {
|
|
332
|
+
if (event.type === "tool_execution_start") {
|
|
333
|
+
const execution = {};
|
|
334
|
+
this._toolCostExecutions.set(event.toolCallId, execution);
|
|
335
|
+
this._pendingToolCosts.add(execution);
|
|
260
336
|
return;
|
|
261
337
|
}
|
|
262
|
-
|
|
263
|
-
|
|
338
|
+
let toolCallId;
|
|
339
|
+
let reported;
|
|
340
|
+
const final = event.type === "message_end" && event.message.role === "toolResult";
|
|
341
|
+
if (event.type === "tool_execution_update" || event.type === "tool_execution_end") {
|
|
342
|
+
toolCallId = event.toolCallId;
|
|
343
|
+
reported = event.type === "tool_execution_update" ? event.partialResult?.cost : event.result?.cost;
|
|
344
|
+
}
|
|
345
|
+
else if (event.type === "message_end" && event.message.role === "toolResult") {
|
|
346
|
+
toolCallId = event.message.toolCallId;
|
|
347
|
+
reported = event.message.cost;
|
|
348
|
+
}
|
|
349
|
+
else {
|
|
264
350
|
return;
|
|
265
351
|
}
|
|
266
|
-
|
|
267
|
-
if (!
|
|
352
|
+
let execution = this._toolCostExecutions.get(toolCallId);
|
|
353
|
+
if (!execution && validRecordedCost(reported)) {
|
|
354
|
+
execution = {};
|
|
355
|
+
this._toolCostExecutions.set(toolCallId, execution);
|
|
356
|
+
this._pendingToolCosts.add(execution);
|
|
357
|
+
}
|
|
358
|
+
if (execution && validRecordedCost(reported))
|
|
359
|
+
execution.cost = reported;
|
|
360
|
+
if (final) {
|
|
361
|
+
this._toolCostExecutions.delete(toolCallId);
|
|
362
|
+
return execution;
|
|
363
|
+
}
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
// SCRAMJET-DIVERGENCE: Capture provider events synchronously and reset stale throughput lifecycles (#476).
|
|
367
|
+
_captureOutputThroughput(event) {
|
|
368
|
+
if (event.type === "message_start" && event.message.role === "assistant" && event.message.origin === "provider") {
|
|
369
|
+
this._outputThroughputGeneration = this._outputThroughputTracker.start({
|
|
370
|
+
provider: event.message.provider,
|
|
371
|
+
model: event.message.model,
|
|
372
|
+
});
|
|
268
373
|
return;
|
|
269
374
|
}
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
375
|
+
const generation = this._outputThroughputGeneration;
|
|
376
|
+
if (generation === undefined)
|
|
377
|
+
return;
|
|
378
|
+
if (event.type === "message_update" && event.message.role === "assistant") {
|
|
379
|
+
this._outputThroughputTracker.observe(generation, event.assistantMessageEvent);
|
|
380
|
+
}
|
|
381
|
+
else if (event.type === "message_end" && event.message.role === "assistant") {
|
|
382
|
+
const sample = this._outputThroughputTracker.finalize(generation, event.message);
|
|
383
|
+
this._outputThroughputGeneration = undefined;
|
|
384
|
+
if (sample) {
|
|
385
|
+
this._latestOutputThroughputSample = sample;
|
|
386
|
+
this._outputThroughputHistory.submit(sample);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
273
389
|
}
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
390
|
+
_resetOutputThroughput() {
|
|
391
|
+
this._outputThroughputTracker.reset();
|
|
392
|
+
this._outputThroughputGeneration = undefined;
|
|
393
|
+
}
|
|
394
|
+
// SCRAMJET-DIVERGENCE: every Agent run owns persistence state and a one-shot settlement token (#553).
|
|
395
|
+
_captureAgentRunSettlement(event) {
|
|
396
|
+
if (event.type === "agent_start") {
|
|
397
|
+
// A retry continuation's run adopts the chain its originating prompt awaits; the chain stays
|
|
398
|
+
// bound to the retry phase until this start consumes it, however late Agent emits it.
|
|
399
|
+
const chain = this._retry?.phase === "continuing" ? this._retry.chain : createDeferred();
|
|
400
|
+
this._unsettledRetryChains.add(chain);
|
|
401
|
+
const run = { ...createDeferred(), chain };
|
|
402
|
+
this._activeAgentRunSettlement = run;
|
|
403
|
+
this._unsettledAgentRuns.add(run);
|
|
404
|
+
const reservation = this._pendingAgentRunStart;
|
|
405
|
+
if (reservation) {
|
|
406
|
+
this._pendingAgentRunStart = undefined;
|
|
407
|
+
reservation.run = run;
|
|
408
|
+
resolveDeferred(reservation, run);
|
|
409
|
+
}
|
|
410
|
+
return run;
|
|
411
|
+
}
|
|
412
|
+
const run = this._activeAgentRunSettlement;
|
|
413
|
+
if (event.type === "agent_end" && this._activeAgentRunSettlement === run) {
|
|
414
|
+
this._activeAgentRunSettlement = undefined;
|
|
415
|
+
}
|
|
416
|
+
return run;
|
|
417
|
+
}
|
|
418
|
+
_settleAgentRun(run, error) {
|
|
419
|
+
if (settleDeferred(run, error))
|
|
420
|
+
this._unsettledAgentRuns.delete(run);
|
|
421
|
+
}
|
|
422
|
+
_settleRetryChain(chain, error) {
|
|
423
|
+
if (settleDeferred(chain, error))
|
|
424
|
+
this._unsettledRetryChains.delete(chain);
|
|
425
|
+
}
|
|
426
|
+
_completeAgentRun(run, error) {
|
|
427
|
+
this._settleAgentRun(run, error);
|
|
428
|
+
this._settleRetryChain(run.chain, error);
|
|
429
|
+
}
|
|
430
|
+
_reserveAgentRunStart() {
|
|
431
|
+
if (this._pendingAgentRunStart)
|
|
432
|
+
throw new Error("An Agent prompt is already waiting to start.");
|
|
433
|
+
const reservation = createDeferred();
|
|
434
|
+
this._pendingAgentRunStart = reservation;
|
|
435
|
+
return reservation;
|
|
436
|
+
}
|
|
437
|
+
_rejectAgentRunStart(reservation, error) {
|
|
438
|
+
if (this._pendingAgentRunStart === reservation)
|
|
439
|
+
this._pendingAgentRunStart = undefined;
|
|
440
|
+
rejectDeferred(reservation, error);
|
|
441
|
+
}
|
|
442
|
+
// SCRAMJET-DIVERGENCE: harness-tool persisted settlement (#341). Synchronous ownership keeps queued
|
|
443
|
+
// invocation events attributed after early failures; public settlement awaits execution and the final persistence tail.
|
|
444
|
+
async _processAgentEventTracked(event, run, harnessAck, toolCost) {
|
|
445
|
+
try {
|
|
446
|
+
await this._processAgentEvent(event, run, harnessAck !== undefined, toolCost);
|
|
447
|
+
}
|
|
448
|
+
catch (err) {
|
|
449
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
450
|
+
if (this._manualCompactionDrain)
|
|
451
|
+
this._manualCompactionDrain.firstError ??= error;
|
|
452
|
+
if (harnessAck) {
|
|
453
|
+
harnessAck.firstPersistenceError ??= error;
|
|
454
|
+
}
|
|
455
|
+
else if (run && !this._manualCompactionDrain) {
|
|
456
|
+
if (event.type === "message_end" && event.message.role === "assistant") {
|
|
457
|
+
run.assistantPersistenceError = error;
|
|
458
|
+
}
|
|
459
|
+
this._releaseRetryForFailedRun(error);
|
|
460
|
+
this._completeAgentRun(run, error);
|
|
461
|
+
}
|
|
462
|
+
throw err;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
_harnessPersistenceAckForEvent(event) {
|
|
466
|
+
let toolCallId;
|
|
467
|
+
switch (event.type) {
|
|
468
|
+
case "tool_execution_start":
|
|
469
|
+
case "tool_execution_update":
|
|
470
|
+
case "tool_execution_end":
|
|
471
|
+
toolCallId = event.toolCallId;
|
|
472
|
+
break;
|
|
473
|
+
case "message_start":
|
|
474
|
+
case "message_end": {
|
|
475
|
+
const message = event.message;
|
|
476
|
+
if (message.role === "toolResult") {
|
|
477
|
+
toolCallId = message.toolCallId;
|
|
478
|
+
}
|
|
479
|
+
else if (message.role === "assistant") {
|
|
480
|
+
for (const block of message.content) {
|
|
481
|
+
if (block.type === "toolCall" && this._harnessPersistenceAcks.has(block.id)) {
|
|
482
|
+
toolCallId = block.id;
|
|
483
|
+
break;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
break;
|
|
279
488
|
}
|
|
280
489
|
}
|
|
281
|
-
return undefined;
|
|
490
|
+
return toolCallId === undefined ? undefined : this._harnessPersistenceAcks.get(toolCallId);
|
|
282
491
|
}
|
|
283
|
-
async _processAgentEvent(event) {
|
|
492
|
+
async _processAgentEvent(event, run, fromHarnessInvocation = false, toolCost) {
|
|
493
|
+
if (this._disposed)
|
|
494
|
+
return;
|
|
495
|
+
// SCRAMJET-DIVERGENCE: queued lifecycle events must respect manual compaction ownership (#598).
|
|
496
|
+
if (this._manualCompactionDrain && event.type !== "message_end" && event.type !== "agent_end")
|
|
497
|
+
return;
|
|
498
|
+
if (this._manualCompactionDrain && event.type === "agent_end") {
|
|
499
|
+
if (this._retry)
|
|
500
|
+
this._finishCancelledRetry("cancelled_during_continuation");
|
|
501
|
+
if (run)
|
|
502
|
+
this._completeAgentRun(run, this._manualCompactionDrain.firstError);
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
284
505
|
// When a user message starts, check if it's from either queue and remove it BEFORE emitting
|
|
285
506
|
// This ensures the UI sees the updated queue state
|
|
286
507
|
if (event.type === "message_start" && event.message.role === "user") {
|
|
@@ -305,8 +526,22 @@ export class AgentSession {
|
|
|
305
526
|
}
|
|
306
527
|
// Emit to extensions first
|
|
307
528
|
await this._emitExtensionEvent(event);
|
|
529
|
+
if (event.type === "message_end" && event.message.role === "toolResult" && toolCost?.cost !== undefined) {
|
|
530
|
+
event.message.cost = toolCost.cost;
|
|
531
|
+
}
|
|
532
|
+
// An awaited hook may have acquired manual ownership while this event was in flight.
|
|
533
|
+
if (this._manualCompactionDrain && event.type !== "message_end") {
|
|
534
|
+
if (event.type === "agent_end" && run) {
|
|
535
|
+
if (this._retry)
|
|
536
|
+
this._finishCancelledRetry("cancelled_during_continuation");
|
|
537
|
+
this._completeAgentRun(run, this._manualCompactionDrain.firstError);
|
|
538
|
+
}
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
308
541
|
// Notify all listeners
|
|
309
542
|
this._emit(event);
|
|
543
|
+
if (this._disposed)
|
|
544
|
+
return;
|
|
310
545
|
// Handle session persistence
|
|
311
546
|
if (event.type === "message_end") {
|
|
312
547
|
// Check if this is a custom message from extensions
|
|
@@ -317,51 +552,132 @@ export class AgentSession {
|
|
|
317
552
|
else if (event.message.role === "user" ||
|
|
318
553
|
event.message.role === "assistant" ||
|
|
319
554
|
event.message.role === "toolResult") {
|
|
320
|
-
//
|
|
321
|
-
|
|
555
|
+
// SCRAMJET-DIVERGENCE: retry settlement classifies the exact JSON-safe assistant snapshot
|
|
556
|
+
// that crossed the persistence boundary, never a later-mutated live Agent message (#553).
|
|
557
|
+
const persistedMessage = event.message.role === "assistant"
|
|
558
|
+
? JSON.parse(JSON.stringify(event.message))
|
|
559
|
+
: event.message;
|
|
560
|
+
const messageEntryId = this.sessionManager.appendMessage(persistedMessage);
|
|
561
|
+
if (toolCost)
|
|
562
|
+
this._pendingToolCosts.delete(toolCost);
|
|
563
|
+
if (persistedMessage.role === "assistant" && !fromHarnessInvocation && run) {
|
|
564
|
+
run.persistedAssistantSnapshot = persistedMessage;
|
|
565
|
+
}
|
|
566
|
+
if (event.message.role === "user") {
|
|
567
|
+
const sessionEntries = this._inputSessionEntries.get(event.message);
|
|
568
|
+
this._inputSessionEntries.delete(event.message);
|
|
569
|
+
// This try/catch observes only failures thrown synchronously by
|
|
570
|
+
// appendCustomEntry. On the buffered first-turn path (before any
|
|
571
|
+
// assistant message) _persist stores the entry in memory with no I/O,
|
|
572
|
+
// so only metadata *serialization* failures surface here; the deferred
|
|
573
|
+
// disk write happens later in the unguarded appendMessage(assistant)
|
|
574
|
+
// flush, whose throw is swallowed by the _agentEventQueue.catch on the
|
|
575
|
+
// event-handler chain and degrades exact restoration silently.
|
|
576
|
+
for (const entry of sessionEntries ?? []) {
|
|
577
|
+
try {
|
|
578
|
+
this.sessionManager.appendCustomEntry(entry.customType, entry.data, messageEntryId);
|
|
579
|
+
}
|
|
580
|
+
catch (err) {
|
|
581
|
+
this._extensionRunner.emitError({
|
|
582
|
+
extensionPath: `session-entry:${entry.customType}`,
|
|
583
|
+
event: "session_entry_persistence",
|
|
584
|
+
error: `Failed to persist attached input metadata after its user message; exact input restoration may be unavailable: ${err instanceof Error ? err.message : String(err)}`,
|
|
585
|
+
stack: err instanceof Error ? err.stack : undefined,
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
}
|
|
322
590
|
}
|
|
323
591
|
// Other message types (bashExecution, compactionSummary, branchSummary) are persisted elsewhere
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
this.
|
|
592
|
+
if (event.message.role === "assistant" &&
|
|
593
|
+
event.message.stopReason !== "error" &&
|
|
594
|
+
event.message.stopReason !== "aborted" &&
|
|
595
|
+
!fromHarnessInvocation &&
|
|
596
|
+
!this._manualCompactionDrain) {
|
|
597
|
+
this._overflowRecoveryAttempted = false;
|
|
598
|
+
if (this._retry)
|
|
599
|
+
this._retry = { ...this._retry, attempt: 0 };
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
if (event.type === "agent_end") {
|
|
603
|
+
if (this._disposed || !run)
|
|
604
|
+
return;
|
|
605
|
+
if (this._manualCompactionDrain) {
|
|
606
|
+
if (this._retry)
|
|
607
|
+
this._finishCancelledRetry("cancelled_during_continuation");
|
|
608
|
+
this._completeAgentRun(run, this._manualCompactionDrain.firstError);
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
if (run.assistantPersistenceError) {
|
|
612
|
+
const error = run.assistantPersistenceError;
|
|
613
|
+
run.assistantPersistenceError = undefined;
|
|
614
|
+
run.persistedAssistantSnapshot = undefined;
|
|
615
|
+
this._completeAgentRun(run, error);
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
const msg = run.persistedAssistantSnapshot;
|
|
619
|
+
run.persistedAssistantSnapshot = undefined;
|
|
620
|
+
if (!msg) {
|
|
621
|
+
this._releaseRetryForFailedRun(new Error("Retry continuation ended without an assistant message."));
|
|
622
|
+
this._completeAgentRun(run);
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
if (msg.stopReason === "aborted" && this._retry) {
|
|
626
|
+
this._finishCancelledRetry("cancelled_during_continuation");
|
|
627
|
+
this._completeAgentRun(run);
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
if (msg.stopReason !== "error") {
|
|
631
|
+
if (this._retry) {
|
|
632
|
+
const attempt = this._runRetryCount;
|
|
633
|
+
this._finishRetry({
|
|
634
|
+
schemaVersion: 1,
|
|
635
|
+
outcome: "succeeded",
|
|
636
|
+
attemptsCompleted: this._bounded(attempt),
|
|
637
|
+
cumulativeErrors: this._bounded(attempt),
|
|
638
|
+
}, { type: "auto_retry_end", success: true, attempt });
|
|
340
639
|
}
|
|
640
|
+
this._completeAgentRun(run);
|
|
641
|
+
await this._checkCompaction(msg);
|
|
642
|
+
return;
|
|
341
643
|
}
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
644
|
+
const classification = this._classifyRetry(msg);
|
|
645
|
+
if (classification.kind === "context_overflow") {
|
|
646
|
+
this._finishRetryWithoutSuccess({
|
|
647
|
+
schemaVersion: 1,
|
|
648
|
+
outcome: "not_attempted",
|
|
649
|
+
reason: "context_overflow_compaction",
|
|
650
|
+
evidence: "context_overflow",
|
|
651
|
+
}, msg);
|
|
652
|
+
this._completeAgentRun(run);
|
|
653
|
+
await this._checkCompaction(msg);
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
const settings = this.settingsManager.getRetrySettings();
|
|
657
|
+
if (!settings.enabled) {
|
|
658
|
+
this._finishRetryWithoutSuccess({
|
|
659
|
+
schemaVersion: 1,
|
|
660
|
+
outcome: "not_attempted",
|
|
661
|
+
reason: "retry_disabled",
|
|
662
|
+
evidence: classification.evidence,
|
|
663
|
+
}, msg);
|
|
664
|
+
this._completeAgentRun(run);
|
|
665
|
+
await this._checkCompaction(msg);
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
if (classification.kind === "retry") {
|
|
669
|
+
const didRetry = await this._handleRetryableError(msg, classification.evidence, run);
|
|
350
670
|
if (didRetry)
|
|
351
|
-
return;
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
else {
|
|
674
|
+
const { kind: _kind, ...cause } = classification;
|
|
675
|
+
this._finishRetryWithoutSuccess({ schemaVersion: 1, outcome: "not_attempted", ...cause }, msg);
|
|
352
676
|
}
|
|
353
|
-
this.
|
|
677
|
+
this._completeAgentRun(run);
|
|
354
678
|
await this._checkCompaction(msg);
|
|
355
679
|
}
|
|
356
680
|
}
|
|
357
|
-
/** Resolve the pending retry promise */
|
|
358
|
-
_resolveRetry() {
|
|
359
|
-
if (this._retryResolve) {
|
|
360
|
-
this._retryResolve();
|
|
361
|
-
this._retryResolve = undefined;
|
|
362
|
-
this._retryPromise = undefined;
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
681
|
/** Extract text content from a message */
|
|
366
682
|
_getUserMessageText(message) {
|
|
367
683
|
if (message.role !== "user")
|
|
@@ -510,8 +826,8 @@ export class AgentSession {
|
|
|
510
826
|
* Preserves all existing listeners.
|
|
511
827
|
*/
|
|
512
828
|
_reconnectToAgent() {
|
|
513
|
-
if (this._unsubscribeAgent)
|
|
514
|
-
return; // Already connected
|
|
829
|
+
if (this._disposed || this._unsubscribeAgent)
|
|
830
|
+
return; // Already connected or disposed
|
|
515
831
|
this._unsubscribeAgent = this.agent.subscribe(this._handleAgentEvent);
|
|
516
832
|
}
|
|
517
833
|
/**
|
|
@@ -519,7 +835,69 @@ export class AgentSession {
|
|
|
519
835
|
* Call this when completely done with the session.
|
|
520
836
|
*/
|
|
521
837
|
dispose() {
|
|
838
|
+
// SCRAMJET-DIVERGENCE: idempotent disposal revokes harness invocations and closes retry settlement (#341, #553).
|
|
839
|
+
// A second call must not re-reject already-settled acknowledgements or re-run teardown.
|
|
840
|
+
if (this._disposed)
|
|
841
|
+
return;
|
|
842
|
+
this._disposed = true;
|
|
843
|
+
this._toolCostExecutions.clear();
|
|
844
|
+
this._pendingToolCosts.clear();
|
|
845
|
+
this._resetOutputThroughput();
|
|
846
|
+
const retryDisposeError = new Error("AgentSession disposed before retry settlement completed.");
|
|
847
|
+
const unsettledRuns = [...this._unsettledAgentRuns];
|
|
848
|
+
const retry = this._retry;
|
|
849
|
+
const unclassifiedError = unsettledRuns.find((run) => run.persistedAssistantSnapshot?.stopReason === "error")?.persistedAssistantSnapshot;
|
|
850
|
+
if (retry)
|
|
851
|
+
this.agent.abort();
|
|
852
|
+
try {
|
|
853
|
+
if (retry) {
|
|
854
|
+
this._appendAutoRetryRecord({
|
|
855
|
+
schemaVersion: 1,
|
|
856
|
+
outcome: "cancelled",
|
|
857
|
+
reason: "session_disposed",
|
|
858
|
+
attempt: this._bounded(retry.attempt),
|
|
859
|
+
cumulativeErrors: this._bounded(this._runRetryCount),
|
|
860
|
+
});
|
|
861
|
+
}
|
|
862
|
+
else if (unclassifiedError) {
|
|
863
|
+
const classification = this._classifyRetry(unclassifiedError);
|
|
864
|
+
this._appendAutoRetryRecord({
|
|
865
|
+
schemaVersion: 1,
|
|
866
|
+
outcome: "not_attempted",
|
|
867
|
+
reason: "session_disposed",
|
|
868
|
+
evidence: classification.kind === "context_overflow" ? "context_overflow" : classification.evidence,
|
|
869
|
+
});
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
catch (error) {
|
|
873
|
+
this._reportRetryFailure(error);
|
|
874
|
+
}
|
|
875
|
+
if (retry?.phase === "backoff")
|
|
876
|
+
retry.controller.abort();
|
|
877
|
+
this._retry = undefined;
|
|
878
|
+
this._activeAgentRunSettlement = undefined;
|
|
879
|
+
const pendingAgentRunStart = this._pendingAgentRunStart;
|
|
880
|
+
if (pendingAgentRunStart)
|
|
881
|
+
this._rejectAgentRunStart(pendingAgentRunStart, retryDisposeError);
|
|
882
|
+
const pendingRetryContinuation = retry?.phase === "continuing" ? retry.chain : undefined;
|
|
522
883
|
this._extensionRunner.invalidate("This extension ctx is stale after session replacement or reload. Do not use a captured pi or command ctx after ctx.newSession(), ctx.fork(), ctx.switchSession(), or ctx.reload(). For newSession, fork, and switchSession, move post-replacement work into withSession and use the ctx passed to withSession. For reload, do not use the old ctx after await ctx.reload().");
|
|
884
|
+
// Reject pending persistence acknowledgements before revoking Agent-side invocations, so a caller
|
|
885
|
+
// awaiting invokeHarnessTool sees rejection rather than a promise that never settles. Already-
|
|
886
|
+
// running tool work may finish afterward; teardown revokes the old-session guarantee but cannot
|
|
887
|
+
// roll back arbitrary side effects.
|
|
888
|
+
const disposeError = new Error("AgentSession disposed before the harness tool invocation settled.");
|
|
889
|
+
for (const ack of this._harnessPersistenceAcks.values()) {
|
|
890
|
+
this._settleHarnessPersistenceAck(ack, disposeError);
|
|
891
|
+
}
|
|
892
|
+
this._harnessPersistenceAcks.clear();
|
|
893
|
+
this.agent.rejectUnsettledHarnessTools(disposeError);
|
|
894
|
+
const retryChains = new Set(this._unsettledRetryChains);
|
|
895
|
+
if (pendingRetryContinuation)
|
|
896
|
+
retryChains.add(pendingRetryContinuation);
|
|
897
|
+
for (const run of unsettledRuns)
|
|
898
|
+
this._settleAgentRun(run, retryDisposeError);
|
|
899
|
+
for (const chain of retryChains)
|
|
900
|
+
this._settleRetryChain(chain, retryDisposeError);
|
|
523
901
|
this._disconnectFromAgent();
|
|
524
902
|
this._eventListeners = [];
|
|
525
903
|
cleanupSessionResources(this.sessionId);
|
|
@@ -547,9 +925,51 @@ export class AgentSession {
|
|
|
547
925
|
get systemPrompt() {
|
|
548
926
|
return flattenSystemPrompt(this.agent.state.systemPrompt);
|
|
549
927
|
}
|
|
928
|
+
get liveOutputRate() {
|
|
929
|
+
const model = this.state.model;
|
|
930
|
+
const identity = this._outputThroughputTracker.activeIdentity;
|
|
931
|
+
if (!model || identity?.provider !== model.provider || identity.model !== model.id)
|
|
932
|
+
return undefined;
|
|
933
|
+
return this._outputThroughputTracker.liveRate();
|
|
934
|
+
}
|
|
935
|
+
get outputThroughputActive() {
|
|
936
|
+
return this._outputThroughputTracker.outputActive;
|
|
937
|
+
}
|
|
938
|
+
get outputThroughputGeneration() {
|
|
939
|
+
return this._outputThroughputTracker.generation;
|
|
940
|
+
}
|
|
941
|
+
get medianOutputRate() {
|
|
942
|
+
const model = this.state.model;
|
|
943
|
+
if (!model)
|
|
944
|
+
return undefined;
|
|
945
|
+
const history = this._outputThroughputHistory.snapshot();
|
|
946
|
+
const local = this._latestOutputThroughputSample;
|
|
947
|
+
if (local?.provider === model.provider &&
|
|
948
|
+
local.model === model.id &&
|
|
949
|
+
!history
|
|
950
|
+
.samples(model.provider, model.id)
|
|
951
|
+
.some((sample) => sample.provider === local.provider &&
|
|
952
|
+
sample.model === local.model &&
|
|
953
|
+
sample.responseModel === local.responseModel &&
|
|
954
|
+
sample.outputTokens === local.outputTokens &&
|
|
955
|
+
sample.durationMs === local.durationMs &&
|
|
956
|
+
sample.observedAt === local.observedAt)) {
|
|
957
|
+
history.add(local);
|
|
958
|
+
}
|
|
959
|
+
return history.median(model.provider, model.id);
|
|
960
|
+
}
|
|
961
|
+
get outputThroughputHistory() {
|
|
962
|
+
return this._outputThroughputHistory.snapshot();
|
|
963
|
+
}
|
|
964
|
+
refreshOutputThroughputHistory() {
|
|
965
|
+
return this._outputThroughputHistory.refresh();
|
|
966
|
+
}
|
|
967
|
+
flushOutputThroughputHistory() {
|
|
968
|
+
return this._outputThroughputHistory.flush();
|
|
969
|
+
}
|
|
550
970
|
/** Current retry attempt (0 if not retrying) */
|
|
551
971
|
get retryAttempt() {
|
|
552
|
-
return this.
|
|
972
|
+
return this._retry?.attempt ?? 0;
|
|
553
973
|
}
|
|
554
974
|
/**
|
|
555
975
|
* Get the names of currently active tools.
|
|
@@ -600,7 +1020,7 @@ export class AgentSession {
|
|
|
600
1020
|
// Rebuild base system prompt with new tool set
|
|
601
1021
|
this._rebuildSystemPrompt(validToolNames);
|
|
602
1022
|
}
|
|
603
|
-
// SCRAMJET-DIVERGENCE: harness-tool invocation (#244).
|
|
1023
|
+
// SCRAMJET-DIVERGENCE: harness-tool invocation (#244), persisted-settlement boundary (#341).
|
|
604
1024
|
/**
|
|
605
1025
|
* Execute a registered tool as a harness-originated call (not requested by the model).
|
|
606
1026
|
*
|
|
@@ -612,13 +1032,97 @@ export class AgentSession {
|
|
|
612
1032
|
* run/turn framing. The idle-vs-mid-run branch lives in `runHarnessTool` (keyed on the agent's
|
|
613
1033
|
* active run, which tracks `isStreaming`): idle calls execute immediately, mid-run calls queue
|
|
614
1034
|
* and drain before the next intra-run LLM call.
|
|
1035
|
+
*
|
|
1036
|
+
* **Persisted settlement (#341).** This is a strictly stronger boundary than
|
|
1037
|
+
* {@link Agent.runHarnessTool}: the returned promise resolves only after (1) the Agent-core
|
|
1038
|
+
* pipeline fulfills *and* (2) `_processAgentEvent` has persisted the matching tool-result
|
|
1039
|
+
* `message_end` via `SessionManager.appendMessage`. Because the event queue preserves order,
|
|
1040
|
+
* that also proves the preceding synthetic assistant tool-call message was persisted first. It
|
|
1041
|
+
* rejects on Agent-core failure, on failure to process/persist a matching event, on disposal
|
|
1042
|
+
* before settlement, on a post-disposal call, on an unknown tool, or on a duplicate explicit
|
|
1043
|
+
* tool-call id that is already pending. **Rejection means the promised pipeline did not complete —
|
|
1044
|
+
* it does not prove artifact absence** (Agent state and persistence may have partially or fully
|
|
1045
|
+
* completed before a late failure), so callers must not blindly retry. A tool's own `execute()`
|
|
1046
|
+
* error is *not* a rejection: it becomes a normal `isError: true` result and the promise resolves
|
|
1047
|
+
* after that error-result pipeline is persisted.
|
|
1048
|
+
*
|
|
1049
|
+
* **Reentrancy.** As with `runHarnessTool`, do not `await` this from work whose return is required
|
|
1050
|
+
* for the Agent to reach the drain point or for the event queue to persist the matching result.
|
|
615
1051
|
*/
|
|
616
1052
|
async invokeHarnessTool(name, args, options) {
|
|
1053
|
+
if (this._disposed) {
|
|
1054
|
+
throw new Error(`Cannot invoke harness tool "${name}": the session has been disposed.`);
|
|
1055
|
+
}
|
|
1056
|
+
// SCRAMJET-DIVERGENCE: reject new session work before allocating invocation ownership (#598).
|
|
1057
|
+
if (this._compactionAbortController) {
|
|
1058
|
+
throw new Error(`Cannot invoke harness tool "${name}": manual compaction is in progress. Retry after it settles.`);
|
|
1059
|
+
}
|
|
617
1060
|
const tool = this._toolRegistry.get(name);
|
|
618
1061
|
if (!tool) {
|
|
619
1062
|
throw new Error(`Cannot invoke harness tool "${name}": no tool with that name is registered.`);
|
|
620
1063
|
}
|
|
621
|
-
|
|
1064
|
+
const toolCallId = this._allocateHarnessToolCallId(options?.toolCallId);
|
|
1065
|
+
// Register before Agent execution can emit events. The coordinator retains attribution until Agent
|
|
1066
|
+
// execution and the invocation's final queued persistence task both settle.
|
|
1067
|
+
const ack = this._registerHarnessPersistenceAck(toolCallId);
|
|
1068
|
+
void this._completeHarnessToolInvocation(this.agent.runHarnessTool(tool, args, { toolCallId }), ack);
|
|
1069
|
+
try {
|
|
1070
|
+
await ack.promise;
|
|
1071
|
+
}
|
|
1072
|
+
finally {
|
|
1073
|
+
// Identity-checked cleanup: only drop the entry if it is still this invocation's ack (disposal
|
|
1074
|
+
// may have cleared the map, and a later same-id invocation must not have its entry removed).
|
|
1075
|
+
if (this._harnessPersistenceAcks.get(toolCallId) === ack) {
|
|
1076
|
+
this._harnessPersistenceAcks.delete(toolCallId);
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
// SCRAMJET-DIVERGENCE: harness-tool persisted-settlement (#341). Allocate the tool-call id used as
|
|
1081
|
+
// the acknowledgement key: reject a duplicate explicit id that is still pending, retry a generated
|
|
1082
|
+
// id on the (astronomically unlikely) collision with a pending one.
|
|
1083
|
+
_allocateHarnessToolCallId(explicit) {
|
|
1084
|
+
if (explicit !== undefined) {
|
|
1085
|
+
// Validate at the public trust boundary (invokeHarnessTool is on ExtensionAPI): an explicit
|
|
1086
|
+
// id flows straight into the synthetic assistant message and on to the provider, so it must
|
|
1087
|
+
// meet the provider-safe shape generateHarnessToolCallId guarantees (Anthropic's
|
|
1088
|
+
// ^[a-zA-Z0-9_-]+$, bounded length).
|
|
1089
|
+
if (!/^[a-zA-Z0-9_-]{1,64}$/.test(explicit)) {
|
|
1090
|
+
throw new Error(`Cannot invoke harness tool with tool-call id "${explicit}": ids must be 1-64 characters of [a-zA-Z0-9_-].`);
|
|
1091
|
+
}
|
|
1092
|
+
if (this._harnessPersistenceAcks.has(explicit)) {
|
|
1093
|
+
throw new Error(`Cannot invoke harness tool with tool-call id "${explicit}": an invocation with that id is already pending.`);
|
|
1094
|
+
}
|
|
1095
|
+
return explicit;
|
|
1096
|
+
}
|
|
1097
|
+
let id = generateHarnessToolCallId();
|
|
1098
|
+
while (this._harnessPersistenceAcks.has(id)) {
|
|
1099
|
+
id = generateHarnessToolCallId();
|
|
1100
|
+
}
|
|
1101
|
+
return id;
|
|
1102
|
+
}
|
|
1103
|
+
async _completeHarnessToolInvocation(execution, ack) {
|
|
1104
|
+
let executionError;
|
|
1105
|
+
try {
|
|
1106
|
+
await execution;
|
|
1107
|
+
}
|
|
1108
|
+
catch (error) {
|
|
1109
|
+
executionError = error instanceof Error ? error : new Error(String(error));
|
|
1110
|
+
}
|
|
1111
|
+
try {
|
|
1112
|
+
await ack.persistenceTail;
|
|
1113
|
+
}
|
|
1114
|
+
catch { }
|
|
1115
|
+
this._settleHarnessPersistenceAck(ack, ack.firstPersistenceError ?? executionError);
|
|
1116
|
+
}
|
|
1117
|
+
_settleHarnessPersistenceAck(ack, error) {
|
|
1118
|
+
settleDeferred(ack, error);
|
|
1119
|
+
}
|
|
1120
|
+
// SCRAMJET-DIVERGENCE: harness-tool persisted-settlement (#341). Register a deferred acknowledgement
|
|
1121
|
+
// keyed by tool-call id before any invocation event can be emitted.
|
|
1122
|
+
_registerHarnessPersistenceAck(toolCallId) {
|
|
1123
|
+
const ack = { ...createDeferred(), persistenceTail: Promise.resolve() };
|
|
1124
|
+
this._harnessPersistenceAcks.set(toolCallId, ack);
|
|
1125
|
+
return ack;
|
|
622
1126
|
}
|
|
623
1127
|
/** Whether compaction or branch summarization is currently running */
|
|
624
1128
|
get isCompacting() {
|
|
@@ -684,6 +1188,19 @@ export class AgentSession {
|
|
|
684
1188
|
}
|
|
685
1189
|
return Array.from(unique);
|
|
686
1190
|
}
|
|
1191
|
+
_applyRunPromptComposition() {
|
|
1192
|
+
switch (this._runPromptComposition.kind) {
|
|
1193
|
+
case "generated-base":
|
|
1194
|
+
this.agent.state.systemPrompt = this._baseSystemPromptSections.slice();
|
|
1195
|
+
break;
|
|
1196
|
+
case "contributed-sections":
|
|
1197
|
+
this.agent.state.systemPrompt = spliceContributedSections(this._baseSystemPromptSections, this._runPromptComposition.sections);
|
|
1198
|
+
break;
|
|
1199
|
+
case "authoritative-string":
|
|
1200
|
+
this.agent.state.systemPrompt = this._runPromptComposition.systemPrompt;
|
|
1201
|
+
break;
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
687
1204
|
/** Rebuild `_baseSystemPromptSections` from the current resources and tool set and apply it to agent state. */
|
|
688
1205
|
_rebuildSystemPrompt(toolNames) {
|
|
689
1206
|
const validToolNames = toolNames.filter((name) => this._toolRegistry.has(name));
|
|
@@ -704,6 +1221,7 @@ export class AgentSession {
|
|
|
704
1221
|
const appendSystemPrompt = loaderAppendSystemPrompt.length > 0 ? loaderAppendSystemPrompt.join("\n\n") : undefined;
|
|
705
1222
|
const loadedSkills = this._resourceLoader.getSkills().skills;
|
|
706
1223
|
const loadedContextFiles = this._resourceLoader.getAgentsFiles().agentsFiles;
|
|
1224
|
+
// SCRAMJET-DIVERGENCE: pass session file path for volatile environment fact (issue 348)
|
|
707
1225
|
this._baseSystemPromptOptions = {
|
|
708
1226
|
cwd: this._cwd,
|
|
709
1227
|
skills: loadedSkills,
|
|
@@ -713,9 +1231,10 @@ export class AgentSession {
|
|
|
713
1231
|
selectedTools: validToolNames,
|
|
714
1232
|
toolSnippets,
|
|
715
1233
|
promptGuidelines,
|
|
1234
|
+
sessionFile: this.sessionManager.getSessionFile(),
|
|
716
1235
|
};
|
|
717
1236
|
this._baseSystemPromptSections = buildSystemPromptSections(this._baseSystemPromptOptions);
|
|
718
|
-
this.
|
|
1237
|
+
this._applyRunPromptComposition();
|
|
719
1238
|
}
|
|
720
1239
|
// =========================================================================
|
|
721
1240
|
// Prompting
|
|
@@ -733,6 +1252,8 @@ export class AgentSession {
|
|
|
733
1252
|
const expandPromptTemplates = options?.expandPromptTemplates ?? true;
|
|
734
1253
|
const preflightResult = options?.preflightResult;
|
|
735
1254
|
let messages;
|
|
1255
|
+
let inputSessionEntries;
|
|
1256
|
+
let acceptInput;
|
|
736
1257
|
try {
|
|
737
1258
|
// Handle extension commands first (execute immediately, even during streaming)
|
|
738
1259
|
// Extension commands manage their own LLM interaction via pi.sendMessage()
|
|
@@ -744,6 +1265,7 @@ export class AgentSession {
|
|
|
744
1265
|
return;
|
|
745
1266
|
}
|
|
746
1267
|
}
|
|
1268
|
+
this._assertRetryNotOwningIdleAgent();
|
|
747
1269
|
// Emit input event for extension interception (before skill/template expansion)
|
|
748
1270
|
let currentText = text;
|
|
749
1271
|
let currentImages = options?.images;
|
|
@@ -756,6 +1278,8 @@ export class AgentSession {
|
|
|
756
1278
|
if (inputResult.action === "transform") {
|
|
757
1279
|
currentText = inputResult.text;
|
|
758
1280
|
currentImages = inputResult.images ?? currentImages;
|
|
1281
|
+
inputSessionEntries = inputResult.sessionEntries;
|
|
1282
|
+
acceptInput = inputResult.onAccepted;
|
|
759
1283
|
}
|
|
760
1284
|
}
|
|
761
1285
|
// Expand skill commands (/skill:name args) and prompt templates (/template args)
|
|
@@ -771,11 +1295,12 @@ export class AgentSession {
|
|
|
771
1295
|
"deliverAs when calling dispatchUserInput() or sendUserMessage() — to queue the message.");
|
|
772
1296
|
}
|
|
773
1297
|
if (options.streamingBehavior === "followUp") {
|
|
774
|
-
await this._queueFollowUp(expandedText, currentImages);
|
|
1298
|
+
await this._queueFollowUp(expandedText, currentImages, inputSessionEntries);
|
|
775
1299
|
}
|
|
776
1300
|
else {
|
|
777
|
-
await this._queueSteer(expandedText, currentImages);
|
|
1301
|
+
await this._queueSteer(expandedText, currentImages, inputSessionEntries);
|
|
778
1302
|
}
|
|
1303
|
+
acceptInput?.();
|
|
779
1304
|
preflightResult?.(true);
|
|
780
1305
|
return;
|
|
781
1306
|
}
|
|
@@ -797,7 +1322,7 @@ export class AgentSession {
|
|
|
797
1322
|
// Check if we need to compact before sending (catches aborted responses)
|
|
798
1323
|
const lastAssistant = this._findLastAssistantMessage();
|
|
799
1324
|
if (lastAssistant) {
|
|
800
|
-
await this._checkCompaction(lastAssistant,
|
|
1325
|
+
await this._checkCompaction(lastAssistant, "new_prompt");
|
|
801
1326
|
}
|
|
802
1327
|
// Build messages array (custom message if any, then user message)
|
|
803
1328
|
messages = [];
|
|
@@ -806,16 +1331,21 @@ export class AgentSession {
|
|
|
806
1331
|
if (currentImages) {
|
|
807
1332
|
userContent.push(...currentImages);
|
|
808
1333
|
}
|
|
809
|
-
|
|
1334
|
+
const userMessage = {
|
|
810
1335
|
role: "user",
|
|
811
1336
|
content: userContent,
|
|
812
1337
|
timestamp: Date.now(),
|
|
813
|
-
}
|
|
1338
|
+
};
|
|
1339
|
+
messages.push(userMessage);
|
|
1340
|
+
if (inputSessionEntries?.length) {
|
|
1341
|
+
this._inputSessionEntries.set(userMessage, inputSessionEntries);
|
|
1342
|
+
}
|
|
814
1343
|
// Inject any pending "nextTurn" messages as context alongside the user message
|
|
815
1344
|
for (const msg of this._pendingNextTurnMessages) {
|
|
816
1345
|
messages.push(msg);
|
|
817
1346
|
}
|
|
818
1347
|
this._pendingNextTurnMessages = [];
|
|
1348
|
+
this._runPromptComposition = { kind: "generated-base" };
|
|
819
1349
|
// Emit before_agent_start extension event
|
|
820
1350
|
const result = await this._extensionRunner.emitBeforeAgentStart(expandedText, currentImages, this._baseSystemPromptSections, this._baseSystemPromptOptions);
|
|
821
1351
|
// Add all custom messages from extensions
|
|
@@ -835,15 +1365,18 @@ export class AgentSession {
|
|
|
835
1365
|
// replacement is authoritative (even an empty string): it wins for the
|
|
836
1366
|
// whole prompt and any contributed sections are dropped for this turn.
|
|
837
1367
|
if (result?.systemPrompt !== undefined) {
|
|
838
|
-
this.
|
|
1368
|
+
this._runPromptComposition = {
|
|
1369
|
+
kind: "authoritative-string",
|
|
1370
|
+
systemPrompt: result.systemPrompt,
|
|
1371
|
+
};
|
|
839
1372
|
}
|
|
840
1373
|
else if (result?.systemPromptSections) {
|
|
841
|
-
this.
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
this.agent.state.systemPrompt = this._baseSystemPromptSections.slice();
|
|
1374
|
+
this._runPromptComposition = {
|
|
1375
|
+
kind: "contributed-sections",
|
|
1376
|
+
sections: result.systemPromptSections.map((section) => ({ ...section })),
|
|
1377
|
+
};
|
|
846
1378
|
}
|
|
1379
|
+
this._applyRunPromptComposition();
|
|
847
1380
|
}
|
|
848
1381
|
catch (error) {
|
|
849
1382
|
preflightResult?.(false);
|
|
@@ -852,9 +1385,9 @@ export class AgentSession {
|
|
|
852
1385
|
if (!messages) {
|
|
853
1386
|
return;
|
|
854
1387
|
}
|
|
1388
|
+
acceptInput?.();
|
|
855
1389
|
preflightResult?.(true);
|
|
856
|
-
await this.
|
|
857
|
-
await this.waitForRetry();
|
|
1390
|
+
await this._runAgentPrompt(messages);
|
|
858
1391
|
}
|
|
859
1392
|
/**
|
|
860
1393
|
* Try to execute an extension command. Returns true if command was found and executed.
|
|
@@ -951,34 +1484,42 @@ export class AgentSession {
|
|
|
951
1484
|
/**
|
|
952
1485
|
* Internal: Queue a steering message (already expanded, no extension command check).
|
|
953
1486
|
*/
|
|
954
|
-
async _queueSteer(text, images) {
|
|
1487
|
+
async _queueSteer(text, images, sessionEntries) {
|
|
955
1488
|
this._steeringMessages.push(text);
|
|
956
1489
|
this._emitQueueUpdate();
|
|
957
1490
|
const content = [{ type: "text", text }];
|
|
958
1491
|
if (images) {
|
|
959
1492
|
content.push(...images);
|
|
960
1493
|
}
|
|
961
|
-
|
|
1494
|
+
const message = {
|
|
962
1495
|
role: "user",
|
|
963
1496
|
content,
|
|
964
1497
|
timestamp: Date.now(),
|
|
965
|
-
}
|
|
1498
|
+
};
|
|
1499
|
+
if (sessionEntries?.length) {
|
|
1500
|
+
this._inputSessionEntries.set(message, sessionEntries);
|
|
1501
|
+
}
|
|
1502
|
+
this.agent.steer(message);
|
|
966
1503
|
}
|
|
967
1504
|
/**
|
|
968
1505
|
* Internal: Queue a follow-up message (already expanded, no extension command check).
|
|
969
1506
|
*/
|
|
970
|
-
async _queueFollowUp(text, images) {
|
|
1507
|
+
async _queueFollowUp(text, images, sessionEntries) {
|
|
971
1508
|
this._followUpMessages.push(text);
|
|
972
1509
|
this._emitQueueUpdate();
|
|
973
1510
|
const content = [{ type: "text", text }];
|
|
974
1511
|
if (images) {
|
|
975
1512
|
content.push(...images);
|
|
976
1513
|
}
|
|
977
|
-
|
|
1514
|
+
const message = {
|
|
978
1515
|
role: "user",
|
|
979
1516
|
content,
|
|
980
1517
|
timestamp: Date.now(),
|
|
981
|
-
}
|
|
1518
|
+
};
|
|
1519
|
+
if (sessionEntries?.length) {
|
|
1520
|
+
this._inputSessionEntries.set(message, sessionEntries);
|
|
1521
|
+
}
|
|
1522
|
+
this.agent.followUp(message);
|
|
982
1523
|
}
|
|
983
1524
|
/**
|
|
984
1525
|
* Throw an error if the text is an extension command.
|
|
@@ -1024,7 +1565,7 @@ export class AgentSession {
|
|
|
1024
1565
|
}
|
|
1025
1566
|
}
|
|
1026
1567
|
else if (options?.triggerTurn) {
|
|
1027
|
-
await this.
|
|
1568
|
+
await this._runAgentPrompt(appMessage);
|
|
1028
1569
|
}
|
|
1029
1570
|
else {
|
|
1030
1571
|
this.agent.state.messages.push(appMessage);
|
|
@@ -1121,6 +1662,7 @@ export class AgentSession {
|
|
|
1121
1662
|
*/
|
|
1122
1663
|
async abort() {
|
|
1123
1664
|
this.abortRetry();
|
|
1665
|
+
this._resetOutputThroughput();
|
|
1124
1666
|
this.agent.abort();
|
|
1125
1667
|
await this.agent.waitForIdle();
|
|
1126
1668
|
}
|
|
@@ -1139,7 +1681,7 @@ export class AgentSession {
|
|
|
1139
1681
|
}
|
|
1140
1682
|
/**
|
|
1141
1683
|
* Set model directly.
|
|
1142
|
-
* Validates
|
|
1684
|
+
* Validates auth, journals selection, and optionally saves global defaults.
|
|
1143
1685
|
* @throws Error if no auth is configured for the model
|
|
1144
1686
|
*/
|
|
1145
1687
|
async setModel(model) {
|
|
@@ -1150,7 +1692,8 @@ export class AgentSession {
|
|
|
1150
1692
|
const thinkingLevel = this._getThinkingLevelForModelSwitch();
|
|
1151
1693
|
this.agent.state.model = model;
|
|
1152
1694
|
this.sessionManager.appendModelChange(model.provider, model.id);
|
|
1153
|
-
this.
|
|
1695
|
+
if (this._persistModelPreferences)
|
|
1696
|
+
this.settingsManager.setDefaultModelAndProvider(model.provider, model.id);
|
|
1154
1697
|
// Re-clamp thinking level for new model's capabilities
|
|
1155
1698
|
this.setThinkingLevel(thinkingLevel);
|
|
1156
1699
|
await this._emitModelSelect(model, previousModel, "set");
|
|
@@ -1182,7 +1725,9 @@ export class AgentSession {
|
|
|
1182
1725
|
// Apply model
|
|
1183
1726
|
this.agent.state.model = next.model;
|
|
1184
1727
|
this.sessionManager.appendModelChange(next.model.provider, next.model.id);
|
|
1185
|
-
this.
|
|
1728
|
+
if (this._persistModelPreferences) {
|
|
1729
|
+
this.settingsManager.setDefaultModelAndProvider(next.model.provider, next.model.id);
|
|
1730
|
+
}
|
|
1186
1731
|
// Apply thinking level.
|
|
1187
1732
|
// - Explicit scoped model thinking level overrides current session level
|
|
1188
1733
|
// - Undefined scoped model thinking level inherits the current session preference
|
|
@@ -1205,7 +1750,9 @@ export class AgentSession {
|
|
|
1205
1750
|
const thinkingLevel = this._getThinkingLevelForModelSwitch();
|
|
1206
1751
|
this.agent.state.model = nextModel;
|
|
1207
1752
|
this.sessionManager.appendModelChange(nextModel.provider, nextModel.id);
|
|
1208
|
-
this.
|
|
1753
|
+
if (this._persistModelPreferences) {
|
|
1754
|
+
this.settingsManager.setDefaultModelAndProvider(nextModel.provider, nextModel.id);
|
|
1755
|
+
}
|
|
1209
1756
|
// Re-clamp thinking level for new model's capabilities
|
|
1210
1757
|
this.setThinkingLevel(thinkingLevel);
|
|
1211
1758
|
await this._emitModelSelect(nextModel, currentModel, "cycle");
|
|
@@ -1217,7 +1764,7 @@ export class AgentSession {
|
|
|
1217
1764
|
/**
|
|
1218
1765
|
* Set thinking level.
|
|
1219
1766
|
* Clamps to model capabilities based on available thinking levels.
|
|
1220
|
-
*
|
|
1767
|
+
* Journals actual changes and optionally saves global defaults.
|
|
1221
1768
|
*/
|
|
1222
1769
|
setThinkingLevel(level) {
|
|
1223
1770
|
const availableLevels = this.getAvailableThinkingLevels();
|
|
@@ -1229,7 +1776,12 @@ export class AgentSession {
|
|
|
1229
1776
|
if (isChanging) {
|
|
1230
1777
|
this.sessionManager.appendThinkingLevelChange(effectiveLevel);
|
|
1231
1778
|
if (this.supportsThinking() || effectiveLevel !== "off") {
|
|
1232
|
-
this.
|
|
1779
|
+
if (this._persistModelPreferences) {
|
|
1780
|
+
this.settingsManager.setDefaultThinkingLevel(effectiveLevel);
|
|
1781
|
+
}
|
|
1782
|
+
else {
|
|
1783
|
+
this._sessionThinkingLevelPreference = effectiveLevel;
|
|
1784
|
+
}
|
|
1233
1785
|
}
|
|
1234
1786
|
this._emit({ type: "thinking_level_changed", level: effectiveLevel });
|
|
1235
1787
|
void this._extensionRunner.emit({
|
|
@@ -1273,6 +1825,9 @@ export class AgentSession {
|
|
|
1273
1825
|
return explicitLevel;
|
|
1274
1826
|
}
|
|
1275
1827
|
if (!this.supportsThinking()) {
|
|
1828
|
+
if (!this._persistModelPreferences && this._sessionThinkingLevelPreference !== undefined) {
|
|
1829
|
+
return this._sessionThinkingLevelPreference;
|
|
1830
|
+
}
|
|
1276
1831
|
return this.settingsManager.getDefaultThinkingLevel() ?? DEFAULT_THINKING_LEVEL;
|
|
1277
1832
|
}
|
|
1278
1833
|
return this.thinkingLevel;
|
|
@@ -1308,11 +1863,32 @@ export class AgentSession {
|
|
|
1308
1863
|
* @param customInstructions Optional instructions for the compaction summary
|
|
1309
1864
|
*/
|
|
1310
1865
|
async compact(customInstructions) {
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
this.
|
|
1866
|
+
// SCRAMJET-DIVERGENCE: acquire one owner before awaits and drain terminal persistence (#598).
|
|
1867
|
+
if (this._disposed)
|
|
1868
|
+
throw new Error("Cannot compact a disposed session.");
|
|
1869
|
+
if (this.isCompacting)
|
|
1870
|
+
throw new Error("Compaction or branch summarization is already in progress. Retry after it settles.");
|
|
1871
|
+
if (this.agent.hasActiveTransientHarnessTools) {
|
|
1872
|
+
throw new Error("A harness tool is still executing outside an Agent run. Retry compaction after its persisted settlement.");
|
|
1873
|
+
}
|
|
1874
|
+
const controller = new AbortController();
|
|
1875
|
+
this._compactionAbortController = controller;
|
|
1876
|
+
const drain = {};
|
|
1877
|
+
this._manualCompactionDrain = drain;
|
|
1878
|
+
const runs = [...this._unsettledAgentRuns];
|
|
1315
1879
|
try {
|
|
1880
|
+
this._resetOutputThroughput();
|
|
1881
|
+
this._disconnectFromAgent();
|
|
1882
|
+
this._unsubscribeAgent = this.agent.subscribe((event) => this._handleAgentEvent(event));
|
|
1883
|
+
this._emit({ type: "compaction_start", reason: "manual" });
|
|
1884
|
+
await this.abort();
|
|
1885
|
+
await this._agentEventQueue;
|
|
1886
|
+
if (drain.firstError)
|
|
1887
|
+
throw drain.firstError;
|
|
1888
|
+
if (this._disposed)
|
|
1889
|
+
throw new Error("Session disposed during compaction.");
|
|
1890
|
+
if (controller.signal.aborted)
|
|
1891
|
+
throw new Error("Compaction cancelled");
|
|
1316
1892
|
if (!this.model) {
|
|
1317
1893
|
throw new Error(formatNoModelSelectedMessage());
|
|
1318
1894
|
}
|
|
@@ -1336,7 +1912,7 @@ export class AgentSession {
|
|
|
1336
1912
|
preparation,
|
|
1337
1913
|
branchEntries: pathEntries,
|
|
1338
1914
|
customInstructions,
|
|
1339
|
-
signal:
|
|
1915
|
+
signal: controller.signal,
|
|
1340
1916
|
}));
|
|
1341
1917
|
if (result?.cancel) {
|
|
1342
1918
|
throw new Error("Compaction cancelled");
|
|
@@ -1359,13 +1935,15 @@ export class AgentSession {
|
|
|
1359
1935
|
}
|
|
1360
1936
|
else {
|
|
1361
1937
|
// Generate compaction result
|
|
1362
|
-
const result = await compact(preparation, this.model, apiKey, headers, customInstructions,
|
|
1938
|
+
const result = await compact(preparation, this.model, apiKey, headers, customInstructions, controller.signal, this.thinkingLevel);
|
|
1363
1939
|
summary = result.summary;
|
|
1364
1940
|
firstKeptEntryId = result.firstKeptEntryId;
|
|
1365
1941
|
tokensBefore = result.tokensBefore;
|
|
1366
1942
|
details = result.details;
|
|
1367
1943
|
}
|
|
1368
|
-
if (this.
|
|
1944
|
+
if (this._disposed)
|
|
1945
|
+
throw new Error("Session disposed during compaction.");
|
|
1946
|
+
if (controller.signal.aborted) {
|
|
1369
1947
|
throw new Error("Compaction cancelled");
|
|
1370
1948
|
}
|
|
1371
1949
|
this.sessionManager.appendCompaction(summary, firstKeptEntryId, tokensBefore, details, fromExtension);
|
|
@@ -1397,8 +1975,13 @@ export class AgentSession {
|
|
|
1397
1975
|
return compactionResult;
|
|
1398
1976
|
}
|
|
1399
1977
|
catch (error) {
|
|
1400
|
-
const
|
|
1401
|
-
const
|
|
1978
|
+
const failure = drain.firstError ?? error;
|
|
1979
|
+
const settlementError = failure instanceof Error ? failure : new Error(String(failure));
|
|
1980
|
+
this._releaseRetryForFailedRun(settlementError);
|
|
1981
|
+
for (const run of runs)
|
|
1982
|
+
this._completeAgentRun(run, settlementError);
|
|
1983
|
+
const message = settlementError.message;
|
|
1984
|
+
const aborted = message === "Compaction cancelled" || settlementError.name === "AbortError";
|
|
1402
1985
|
this._emit({
|
|
1403
1986
|
type: "compaction_end",
|
|
1404
1987
|
reason: "manual",
|
|
@@ -1407,9 +1990,11 @@ export class AgentSession {
|
|
|
1407
1990
|
willRetry: false,
|
|
1408
1991
|
errorMessage: aborted ? undefined : `Compaction failed: ${message}`,
|
|
1409
1992
|
});
|
|
1410
|
-
throw
|
|
1993
|
+
throw failure;
|
|
1411
1994
|
}
|
|
1412
1995
|
finally {
|
|
1996
|
+
this._disconnectFromAgent();
|
|
1997
|
+
this._manualCompactionDrain = undefined;
|
|
1413
1998
|
this._compactionAbortController = undefined;
|
|
1414
1999
|
this._reconnectToAgent();
|
|
1415
2000
|
}
|
|
@@ -1436,15 +2021,44 @@ export class AgentSession {
|
|
|
1436
2021
|
* 2. Threshold: Context over threshold, compact, NO auto-retry (user continues manually)
|
|
1437
2022
|
*
|
|
1438
2023
|
* @param assistantMessage The assistant message to check
|
|
1439
|
-
* @param skipAbortedCheck If false, include aborted messages (for pre-prompt check). Default: true
|
|
1440
2024
|
*/
|
|
1441
|
-
async _checkCompaction(assistantMessage,
|
|
2025
|
+
async _checkCompaction(assistantMessage, trigger = "settlement") {
|
|
2026
|
+
if (this._manualCompactionDrain)
|
|
2027
|
+
return;
|
|
1442
2028
|
const settings = this.settingsManager.getCompactionSettings();
|
|
1443
2029
|
if (!settings.enabled)
|
|
1444
2030
|
return;
|
|
1445
|
-
//
|
|
1446
|
-
if (
|
|
2031
|
+
// SCRAMJET-DIVERGENCE: New prompts maintain context independently of failed-turn recovery.
|
|
2032
|
+
if (trigger === "settlement" && blocksFailureRecovery(assistantMessage))
|
|
2033
|
+
return;
|
|
2034
|
+
if (trigger === "new_prompt") {
|
|
2035
|
+
const boundary = getLatestCompactionEntry(this.sessionManager.getBranch());
|
|
2036
|
+
const messages = this.agent.state.messages;
|
|
2037
|
+
const reverseIndex = [...messages]
|
|
2038
|
+
.reverse()
|
|
2039
|
+
.findIndex((message) => message.role === "assistant" &&
|
|
2040
|
+
message.origin !== "harness" &&
|
|
2041
|
+
message.stopReason !== "error" &&
|
|
2042
|
+
message.stopReason !== "aborted" &&
|
|
2043
|
+
message.provider === this.model?.provider &&
|
|
2044
|
+
message.model === this.model?.id &&
|
|
2045
|
+
Number.isFinite(calculateContextTokens(message.usage)) &&
|
|
2046
|
+
calculateContextTokens(message.usage) >= 0 &&
|
|
2047
|
+
(!boundary || message.timestamp > new Date(boundary.timestamp).getTime()) &&
|
|
2048
|
+
[message.usage.input, message.usage.output, message.usage.cacheRead, message.usage.cacheWrite].every((n) => Number.isFinite(n) && n >= 0));
|
|
2049
|
+
if (reverseIndex < 0)
|
|
2050
|
+
return;
|
|
2051
|
+
const sourceIndex = messages.length - reverseIndex - 1;
|
|
2052
|
+
const source = messages[sourceIndex];
|
|
2053
|
+
const tokens = calculateContextTokens(source.usage) +
|
|
2054
|
+
messages.slice(sourceIndex + 1).reduce((total, message) => total + estimateTokens(message), 0);
|
|
2055
|
+
const window = this.model?.contextWindow ?? 0;
|
|
2056
|
+
if (shouldCompact(tokens, window, settings) ||
|
|
2057
|
+
(window > settings.reserveTokens && tokens > (this.model?.maxInputTokens ?? Infinity)))
|
|
2058
|
+
await this._runAutoCompaction("threshold", false);
|
|
1447
2059
|
return;
|
|
2060
|
+
}
|
|
2061
|
+
// SCRAMJET-DIVERGENCE: Total context and independent input limits are separate constraints.
|
|
1448
2062
|
const contextWindow = this.model?.contextWindow ?? 0;
|
|
1449
2063
|
// Skip overflow check if the message came from a different model.
|
|
1450
2064
|
// This handles the case where user switched from a smaller-context model (e.g. opus)
|
|
@@ -1460,7 +2074,8 @@ export class AgentSession {
|
|
|
1460
2074
|
return;
|
|
1461
2075
|
}
|
|
1462
2076
|
// Case 1: Overflow - LLM returned context overflow error
|
|
1463
|
-
if (sameModel &&
|
|
2077
|
+
if (sameModel &&
|
|
2078
|
+
isContextOverflow(assistantMessage, Math.min(contextWindow, this.model?.maxInputTokens ?? Infinity))) {
|
|
1464
2079
|
if (this._overflowRecoveryAttempted) {
|
|
1465
2080
|
this._emit({
|
|
1466
2081
|
type: "compaction_end",
|
|
@@ -1505,7 +2120,8 @@ export class AgentSession {
|
|
|
1505
2120
|
else {
|
|
1506
2121
|
contextTokens = calculateContextTokens(assistantMessage.usage);
|
|
1507
2122
|
}
|
|
1508
|
-
if (shouldCompact(contextTokens, contextWindow, settings)
|
|
2123
|
+
if (shouldCompact(contextTokens, contextWindow, settings) ||
|
|
2124
|
+
(contextWindow > settings.reserveTokens && contextTokens > (this.model?.maxInputTokens ?? Infinity))) {
|
|
1509
2125
|
await this._runAutoCompaction("threshold", false);
|
|
1510
2126
|
}
|
|
1511
2127
|
}
|
|
@@ -1514,9 +2130,10 @@ export class AgentSession {
|
|
|
1514
2130
|
*/
|
|
1515
2131
|
async _runAutoCompaction(reason, willRetry) {
|
|
1516
2132
|
const settings = this.settingsManager.getCompactionSettings();
|
|
1517
|
-
|
|
2133
|
+
// SCRAMJET-DIVERGENCE: reentrant subscribers must observe compaction ownership (#598).
|
|
1518
2134
|
this._autoCompactionAbortController = new AbortController();
|
|
1519
2135
|
try {
|
|
2136
|
+
this._emit({ type: "compaction_start", reason });
|
|
1520
2137
|
if (!this.model) {
|
|
1521
2138
|
this._emit({
|
|
1522
2139
|
type: "compaction_end",
|
|
@@ -1815,6 +2432,7 @@ export class AgentSession {
|
|
|
1815
2432
|
setThinkingLevel: (level) => this.setThinkingLevel(level),
|
|
1816
2433
|
}, {
|
|
1817
2434
|
getModel: () => this.model,
|
|
2435
|
+
getScopedModels: () => this.scopedModels,
|
|
1818
2436
|
isIdle: () => !this.isStreaming,
|
|
1819
2437
|
getSignal: () => this.agent.signal,
|
|
1820
2438
|
abort: () => this.abort(),
|
|
@@ -1860,7 +2478,13 @@ export class AgentSession {
|
|
|
1860
2478
|
definition,
|
|
1861
2479
|
sourceInfo: createSyntheticSourceInfo(`<sdk:${definition.name}>`, { source: "sdk" }),
|
|
1862
2480
|
})),
|
|
1863
|
-
]
|
|
2481
|
+
];
|
|
2482
|
+
// SCRAMJET-DIVERGENCE: historical interpretation follows definition collision winners, not execution allowlists (#598).
|
|
2483
|
+
this._historicalCostDefinitions = new Map(this._baseToolDefinitions);
|
|
2484
|
+
for (const tool of allCustomTools) {
|
|
2485
|
+
this._historicalCostDefinitions.set(tool.definition.name, tool.definition);
|
|
2486
|
+
}
|
|
2487
|
+
const allowedCustomTools = allCustomTools.filter((tool) => isAllowedTool(tool.definition.name));
|
|
1864
2488
|
const definitionRegistry = new Map(Array.from(this._baseToolDefinitions.entries())
|
|
1865
2489
|
.filter(([name]) => isAllowedTool(name))
|
|
1866
2490
|
.map(([name, definition]) => [
|
|
@@ -1870,7 +2494,7 @@ export class AgentSession {
|
|
|
1870
2494
|
sourceInfo: createSyntheticSourceInfo(`<builtin:${name}>`, { source: "builtin" }),
|
|
1871
2495
|
},
|
|
1872
2496
|
]));
|
|
1873
|
-
for (const tool of
|
|
2497
|
+
for (const tool of allowedCustomTools) {
|
|
1874
2498
|
definitionRegistry.set(tool.definition.name, {
|
|
1875
2499
|
definition: tool.definition,
|
|
1876
2500
|
sourceInfo: tool.sourceInfo,
|
|
@@ -1890,7 +2514,7 @@ export class AgentSession {
|
|
|
1890
2514
|
})
|
|
1891
2515
|
.filter((entry) => entry !== undefined));
|
|
1892
2516
|
const runner = this._extensionRunner;
|
|
1893
|
-
const wrappedExtensionTools = wrapRegisteredTools(
|
|
2517
|
+
const wrappedExtensionTools = wrapRegisteredTools(allowedCustomTools, runner);
|
|
1894
2518
|
const wrappedBuiltInTools = wrapRegisteredTools(Array.from(this._baseToolDefinitions.values())
|
|
1895
2519
|
.filter((definition) => isAllowedTool(definition.name))
|
|
1896
2520
|
.map((definition) => ({
|
|
@@ -1961,10 +2585,21 @@ export class AgentSession {
|
|
|
1961
2585
|
}
|
|
1962
2586
|
async reload() {
|
|
1963
2587
|
const previousFlagValues = this._extensionRunner.getFlagValues();
|
|
1964
|
-
|
|
2588
|
+
// SCRAMJET-DIVERGENCE: the Scramjet builtin is a required product component, so reload must validate
|
|
2589
|
+
// the replacement loader before any irreversible runtime mutation. settingsManager.reload() runs first —
|
|
2590
|
+
// it commits fresh settings before the loader can throw, but the re-read is idempotent, so a failed reload
|
|
2591
|
+
// leaves the runtime itself untouched. It also runs first
|
|
2592
|
+
// because the loader's path resolution reads live settings (the loader defers its own settings/source-map
|
|
2593
|
+
// commit until after the builtin init succeeds, so on this path the shared manager is intentionally read
|
|
2594
|
+
// twice). resourceLoader.reload() then re-runs the required
|
|
2595
|
+
// builtin and throws RequiredBuiltinInitError on failure — before session_shutdown is emitted and before
|
|
2596
|
+
// resetApiProviders(). A throwing builtin therefore leaves the current runner, resources, and providers
|
|
2597
|
+
// intact and fires no session_shutdown; only a validated candidate proceeds to teardown and rebuild.
|
|
1965
2598
|
await this.settingsManager.reload();
|
|
1966
|
-
resetApiProviders();
|
|
1967
2599
|
await this._resourceLoader.reload();
|
|
2600
|
+
await emitSessionShutdownEvent(this._extensionRunner, { type: "session_shutdown", reason: "reload" });
|
|
2601
|
+
resetApiProviders();
|
|
2602
|
+
this._runPromptComposition = { kind: "generated-base" };
|
|
1968
2603
|
this._buildRuntime({
|
|
1969
2604
|
activeToolNames: this.getActiveToolNames(),
|
|
1970
2605
|
flagValues: previousFlagValues,
|
|
@@ -1982,114 +2617,325 @@ export class AgentSession {
|
|
|
1982
2617
|
// =========================================================================
|
|
1983
2618
|
// Auto-Retry
|
|
1984
2619
|
// =========================================================================
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
_isRetryableError(message) {
|
|
1990
|
-
if (message.stopReason !== "error" || !message.errorMessage)
|
|
1991
|
-
return false;
|
|
1992
|
-
// Context overflow is handled by compaction, not retry
|
|
2620
|
+
// SCRAMJET-DIVERGENCE: persisted post-extension snapshots authoritatively classify and settle retries (#553).
|
|
2621
|
+
_classifyRetry(message) {
|
|
2622
|
+
if ((message.origin === "harness" || message.stopReason === "aborted") && blocksFailureRecovery(message))
|
|
2623
|
+
return { kind: "do_not_retry", reason: "non_provider_failure", evidence: "none" };
|
|
1993
2624
|
const contextWindow = this.model?.contextWindow ?? 0;
|
|
1994
|
-
if (isContextOverflow(message, contextWindow))
|
|
1995
|
-
return
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
2625
|
+
if (isContextOverflow(message, Math.min(contextWindow, this.model?.maxInputTokens ?? Infinity))) {
|
|
2626
|
+
return { kind: "context_overflow" };
|
|
2627
|
+
}
|
|
2628
|
+
const structured = inspectFailureEvidence(message.diagnostics);
|
|
2629
|
+
if (structured.status === "valid") {
|
|
2630
|
+
if (structured.source !== "provider")
|
|
2631
|
+
return { kind: "do_not_retry", reason: "non_provider_failure", evidence: "none" };
|
|
2632
|
+
if (structured.suppression)
|
|
2633
|
+
return { kind: "do_not_retry", reason: structured.suppression, evidence: structured.family };
|
|
2634
|
+
if (structured.transient || ["missing_terminal_event", "missing_body"].includes(structured.category))
|
|
2635
|
+
return { kind: "retry", evidence: structured.family };
|
|
2636
|
+
return {
|
|
2637
|
+
kind: "do_not_retry",
|
|
2638
|
+
reason: ["unknown", "provider_error", "malformed_event"].includes(structured.category)
|
|
2639
|
+
? "structured_unknown"
|
|
2640
|
+
: "structured_non_transient",
|
|
2641
|
+
evidence: structured.family,
|
|
2642
|
+
};
|
|
2643
|
+
}
|
|
2644
|
+
if (structured.status === "conflicting")
|
|
2645
|
+
return { kind: "do_not_retry", reason: "invalid_failure_evidence", evidence: "none" };
|
|
2646
|
+
if (structured.status === "malformed") {
|
|
2647
|
+
return { kind: "do_not_retry", reason: "malformed_provider_diagnostic", evidence: "none" };
|
|
2648
|
+
}
|
|
2649
|
+
if (structured.status === "duplicate") {
|
|
2650
|
+
return { kind: "do_not_retry", reason: "duplicate_provider_diagnostic", evidence: "none" };
|
|
2651
|
+
}
|
|
2652
|
+
if (!message.errorMessage) {
|
|
2653
|
+
return { kind: "do_not_retry", reason: "missing_error_evidence", evidence: "none" };
|
|
2654
|
+
}
|
|
2655
|
+
if (this._isLegacyRetryableError(message.errorMessage))
|
|
2656
|
+
return { kind: "retry", evidence: "legacy_text" };
|
|
2657
|
+
return { kind: "do_not_retry", reason: "legacy_non_retryable", evidence: "legacy_text" };
|
|
2658
|
+
}
|
|
2659
|
+
_isLegacyRetryableError(errorMessage) {
|
|
2660
|
+
return /overloaded|provider.?returned.?error|rate.?limit|too many requests|429|500|502|503|504|service.?unavailable|server.?error|internal.?error|network.?error|connection.?error|connection.?refused|connection.?lost|websocket.?closed|websocket.?error|other side closed|fetch failed|upstream.?connect|reset before headers|socket hang up|ended without|stream ended before message_stop|http2 request did not get a response|timed? out|timeout|terminated|retry delay/i.test(errorMessage);
|
|
2661
|
+
}
|
|
2662
|
+
_bounded(value) {
|
|
2663
|
+
return Math.min(65_535, Math.max(0, Number.isFinite(value) ? Math.trunc(value) : 65_535));
|
|
2664
|
+
}
|
|
2665
|
+
_appendAutoRetryRecord(record) {
|
|
2666
|
+
this.sessionManager.appendCustomEntry("coding-agent:auto-retry", record);
|
|
1999
2667
|
}
|
|
2000
2668
|
/**
|
|
2001
|
-
*
|
|
2002
|
-
*
|
|
2669
|
+
* Persist a terminal record, emit `auto_retry_end` when a retry phase was active, and clear the phase.
|
|
2670
|
+
* The phase is released before persistence is attempted so a persistence failure can never strand it.
|
|
2003
2671
|
*/
|
|
2004
|
-
|
|
2672
|
+
_finishRetry(record, end) {
|
|
2673
|
+
const retry = this._retry;
|
|
2674
|
+
this._retry = undefined;
|
|
2675
|
+
try {
|
|
2676
|
+
this._appendAutoRetryRecord(record);
|
|
2677
|
+
}
|
|
2678
|
+
catch (error) {
|
|
2679
|
+
if (retry && !this._manualCompactionDrain) {
|
|
2680
|
+
this._emit({
|
|
2681
|
+
type: "auto_retry_end",
|
|
2682
|
+
success: false,
|
|
2683
|
+
attempt: end?.attempt ?? retry.attempt,
|
|
2684
|
+
finalError: "Retry outcome persistence failed",
|
|
2685
|
+
});
|
|
2686
|
+
}
|
|
2687
|
+
throw this._reportRetryFailure(error);
|
|
2688
|
+
}
|
|
2689
|
+
if (retry && end && !this._manualCompactionDrain)
|
|
2690
|
+
this._emit(end);
|
|
2691
|
+
}
|
|
2692
|
+
_finishRetryWithoutSuccess(record, message) {
|
|
2693
|
+
const retry = this._retry;
|
|
2694
|
+
if (retry?.attempt === 0) {
|
|
2695
|
+
// The continuation already produced a successful message, so the retry recovered and this
|
|
2696
|
+
// later failure is a fresh, non-retried error; close the retry as succeeded first.
|
|
2697
|
+
const attempt = this._runRetryCount;
|
|
2698
|
+
this._finishRetry({
|
|
2699
|
+
schemaVersion: 1,
|
|
2700
|
+
outcome: "succeeded",
|
|
2701
|
+
attemptsCompleted: this._bounded(attempt),
|
|
2702
|
+
cumulativeErrors: this._bounded(attempt),
|
|
2703
|
+
}, { type: "auto_retry_end", success: true, attempt });
|
|
2704
|
+
this._finishRetry(record);
|
|
2705
|
+
return;
|
|
2706
|
+
}
|
|
2707
|
+
this._finishRetry(record, retry
|
|
2708
|
+
? { type: "auto_retry_end", success: false, attempt: retry.attempt, finalError: message.errorMessage }
|
|
2709
|
+
: undefined);
|
|
2710
|
+
}
|
|
2711
|
+
/** A run that ends without a classifiable snapshot cannot continue the retry; close it so nothing stays wedged. */
|
|
2712
|
+
_releaseRetryForFailedRun(error) {
|
|
2713
|
+
const retry = this._retry;
|
|
2714
|
+
if (!retry)
|
|
2715
|
+
return;
|
|
2716
|
+
try {
|
|
2717
|
+
this._finishRetry({
|
|
2718
|
+
schemaVersion: 1,
|
|
2719
|
+
outcome: "failed",
|
|
2720
|
+
reason: "run_failed",
|
|
2721
|
+
attempt: this._bounded(retry.attempt),
|
|
2722
|
+
cumulativeErrors: this._bounded(this._runRetryCount),
|
|
2723
|
+
}, { type: "auto_retry_end", success: false, attempt: retry.attempt, finalError: error.message });
|
|
2724
|
+
}
|
|
2725
|
+
catch {
|
|
2726
|
+
// Already reported and the phase is released; the run settles with its own error.
|
|
2727
|
+
}
|
|
2728
|
+
}
|
|
2729
|
+
_reportRetryFailure(error) {
|
|
2730
|
+
const normalized = error instanceof Error ? error : new Error(String(error));
|
|
2731
|
+
this._extensionRunner.emitError({
|
|
2732
|
+
extensionPath: "agent-session:auto-retry",
|
|
2733
|
+
event: "retry_settlement",
|
|
2734
|
+
error: normalized.message,
|
|
2735
|
+
stack: normalized.stack,
|
|
2736
|
+
});
|
|
2737
|
+
return normalized;
|
|
2738
|
+
}
|
|
2739
|
+
async _handleRetryableError(message, evidence, run) {
|
|
2005
2740
|
const settings = this.settingsManager.getRetrySettings();
|
|
2006
|
-
|
|
2007
|
-
|
|
2741
|
+
const attempt = (this._retry?.attempt ?? 0) + 1;
|
|
2742
|
+
const cumulativeCap = Math.min(Number.MAX_SAFE_INTEGER, settings.maxRetries * 2);
|
|
2743
|
+
if ((this._retry?.attempt ?? 0) >= settings.maxRetries) {
|
|
2744
|
+
const attemptsCompleted = attempt - 1;
|
|
2745
|
+
this._finishRetry({
|
|
2746
|
+
schemaVersion: 1,
|
|
2747
|
+
outcome: "exhausted",
|
|
2748
|
+
reason: "attempt_limit",
|
|
2749
|
+
attemptsCompleted: this._bounded(attemptsCompleted),
|
|
2750
|
+
maxAttempts: this._bounded(settings.maxRetries),
|
|
2751
|
+
cumulativeErrors: this._bounded(this._runRetryCount),
|
|
2752
|
+
}, { type: "auto_retry_end", success: false, attempt: attemptsCompleted, finalError: message.errorMessage });
|
|
2008
2753
|
return false;
|
|
2009
2754
|
}
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
this._emit({
|
|
2755
|
+
if (this._runRetryCount >= cumulativeCap) {
|
|
2756
|
+
const attemptsCompleted = this._runRetryCount;
|
|
2757
|
+
this._finishRetry({
|
|
2758
|
+
schemaVersion: 1,
|
|
2759
|
+
outcome: "exhausted",
|
|
2760
|
+
reason: "cumulative_limit",
|
|
2761
|
+
attemptsCompleted: this._bounded(attemptsCompleted),
|
|
2762
|
+
maxAttempts: this._bounded(settings.maxRetries),
|
|
2763
|
+
cumulativeErrors: this._bounded(this._runRetryCount + 1),
|
|
2764
|
+
}, {
|
|
2021
2765
|
type: "auto_retry_end",
|
|
2022
2766
|
success: false,
|
|
2023
|
-
attempt:
|
|
2024
|
-
finalError: message.errorMessage,
|
|
2767
|
+
attempt: attemptsCompleted,
|
|
2768
|
+
finalError: `Repeated retry failures (${attemptsCompleted} total attempts this prompt). ${message.errorMessage ?? ""}`.trim(),
|
|
2025
2769
|
});
|
|
2026
|
-
this._retryAttempt = 0;
|
|
2027
|
-
this._resolveRetry(); // Resolve so waitForRetry() completes
|
|
2028
2770
|
return false;
|
|
2029
2771
|
}
|
|
2030
|
-
|
|
2772
|
+
this._runRetryCount++;
|
|
2773
|
+
const delayMs = settings.baseDelayMs === 0
|
|
2774
|
+
? 0
|
|
2775
|
+
: Math.min(2_147_483_647, settings.baseDelayMs * 2 ** Math.min(31, attempt - 1));
|
|
2776
|
+
const previous = this._retry;
|
|
2777
|
+
this._retry = undefined;
|
|
2778
|
+
try {
|
|
2779
|
+
this._appendAutoRetryRecord({
|
|
2780
|
+
schemaVersion: 1,
|
|
2781
|
+
outcome: "scheduled",
|
|
2782
|
+
evidence,
|
|
2783
|
+
attempt: this._bounded(attempt),
|
|
2784
|
+
maxAttempts: this._bounded(settings.maxRetries),
|
|
2785
|
+
cumulativeErrors: this._bounded(this._runRetryCount),
|
|
2786
|
+
delayMs: this._bounded(delayMs),
|
|
2787
|
+
});
|
|
2788
|
+
}
|
|
2789
|
+
catch (error) {
|
|
2790
|
+
if (previous) {
|
|
2791
|
+
this._emit({
|
|
2792
|
+
type: "auto_retry_end",
|
|
2793
|
+
success: false,
|
|
2794
|
+
attempt: previous.attempt,
|
|
2795
|
+
finalError: "Retry outcome persistence failed",
|
|
2796
|
+
});
|
|
2797
|
+
}
|
|
2798
|
+
throw this._reportRetryFailure(error);
|
|
2799
|
+
}
|
|
2800
|
+
const controller = new AbortController();
|
|
2801
|
+
const backoff = { phase: "backoff", attempt, controller, run };
|
|
2802
|
+
this._retry = backoff;
|
|
2031
2803
|
this._emit({
|
|
2032
2804
|
type: "auto_retry_start",
|
|
2033
|
-
attempt
|
|
2805
|
+
attempt,
|
|
2034
2806
|
maxAttempts: settings.maxRetries,
|
|
2035
2807
|
delayMs,
|
|
2036
2808
|
errorMessage: message.errorMessage || "Unknown error",
|
|
2809
|
+
cumulativeErrors: this._runRetryCount,
|
|
2037
2810
|
});
|
|
2038
|
-
|
|
2811
|
+
if (controller.signal.aborted || this._disposed) {
|
|
2812
|
+
return this._finishCancelledRetry("cancelled_during_backoff", backoff);
|
|
2813
|
+
}
|
|
2039
2814
|
const messages = this.agent.state.messages;
|
|
2040
2815
|
if (messages.length > 0 && messages[messages.length - 1].role === "assistant") {
|
|
2041
2816
|
this.agent.state.messages = messages.slice(0, -1);
|
|
2042
2817
|
}
|
|
2043
|
-
// Wait with exponential backoff (abortable)
|
|
2044
|
-
this._retryAbortController = new AbortController();
|
|
2045
2818
|
try {
|
|
2046
|
-
await sleep(delayMs,
|
|
2819
|
+
await sleep(delayMs, controller.signal);
|
|
2820
|
+
await sleep(0, controller.signal);
|
|
2047
2821
|
}
|
|
2048
2822
|
catch {
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
this.
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
this.
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2823
|
+
return this._finishCancelledRetry("cancelled_during_backoff", backoff);
|
|
2824
|
+
}
|
|
2825
|
+
if (controller.signal.aborted || this._disposed) {
|
|
2826
|
+
return this._finishCancelledRetry("cancelled_during_backoff", backoff);
|
|
2827
|
+
}
|
|
2828
|
+
if (this._manualCompactionDrain)
|
|
2829
|
+
return this._finishCancelledRetry("cancelled_during_backoff", backoff);
|
|
2830
|
+
// Hand the chain to the continuation before `continue()`; the run that eventually emits
|
|
2831
|
+
// `agent_start` adopts it (see `_captureAgentRunSettlement`), however long Agent defers that start.
|
|
2832
|
+
const continuing = { phase: "continuing", attempt, chain: run.chain };
|
|
2833
|
+
this._retry = continuing;
|
|
2834
|
+
const continuation = this.agent.continue();
|
|
2835
|
+
this._settleAgentRun(run);
|
|
2836
|
+
continuation.catch((error) => {
|
|
2837
|
+
if (this._disposed || this._retry !== continuing)
|
|
2838
|
+
return;
|
|
2839
|
+
const continuationError = this._reportRetryFailure(error);
|
|
2840
|
+
let settlementError = continuationError;
|
|
2841
|
+
try {
|
|
2842
|
+
this._finishRetry({
|
|
2843
|
+
schemaVersion: 1,
|
|
2844
|
+
outcome: "failed",
|
|
2845
|
+
reason: "continuation_rejected",
|
|
2846
|
+
attempt: this._bounded(attempt),
|
|
2847
|
+
cumulativeErrors: this._bounded(this._runRetryCount),
|
|
2848
|
+
}, { type: "auto_retry_end", success: false, attempt, finalError: continuationError.message });
|
|
2849
|
+
}
|
|
2850
|
+
catch (persistenceError) {
|
|
2851
|
+
settlementError = new AggregateError([continuationError, persistenceError], "Retry continuation and outcome persistence both failed.", { cause: continuationError });
|
|
2852
|
+
}
|
|
2853
|
+
this._settleRetryChain(run.chain, settlementError);
|
|
2854
|
+
});
|
|
2069
2855
|
return true;
|
|
2070
2856
|
}
|
|
2857
|
+
_finishCancelledRetry(reason, expected) {
|
|
2858
|
+
if (this._disposed) {
|
|
2859
|
+
this._retry = undefined;
|
|
2860
|
+
throw new Error("AgentSession disposed before retry settlement completed.");
|
|
2861
|
+
}
|
|
2862
|
+
const retry = this._retry;
|
|
2863
|
+
if (expected && retry !== expected)
|
|
2864
|
+
return false;
|
|
2865
|
+
const attempt = retry?.attempt ?? 0;
|
|
2866
|
+
this._finishRetry({
|
|
2867
|
+
schemaVersion: 1,
|
|
2868
|
+
outcome: "cancelled",
|
|
2869
|
+
reason,
|
|
2870
|
+
attempt: this._bounded(attempt),
|
|
2871
|
+
cumulativeErrors: this._bounded(this._runRetryCount),
|
|
2872
|
+
}, { type: "auto_retry_end", success: false, attempt, finalError: "Retry cancelled" });
|
|
2873
|
+
return false;
|
|
2874
|
+
}
|
|
2071
2875
|
/**
|
|
2072
2876
|
* Cancel in-progress retry.
|
|
2073
2877
|
*/
|
|
2074
2878
|
abortRetry() {
|
|
2075
|
-
this.
|
|
2076
|
-
|
|
2077
|
-
this._resolveRetry();
|
|
2078
|
-
}
|
|
2079
|
-
/**
|
|
2080
|
-
* Wait for any in-progress retry to complete.
|
|
2081
|
-
* Returns immediately if no retry is in progress.
|
|
2082
|
-
*/
|
|
2083
|
-
async waitForRetry() {
|
|
2084
|
-
if (!this._retryPromise) {
|
|
2879
|
+
const retry = this._retry;
|
|
2880
|
+
if (!retry)
|
|
2085
2881
|
return;
|
|
2882
|
+
if (retry.phase === "backoff")
|
|
2883
|
+
retry.controller.abort();
|
|
2884
|
+
this.agent.abort();
|
|
2885
|
+
}
|
|
2886
|
+
/** Agent's own `activeRun` guard is false during backoff; the retry phase owns the idle Agent then. */
|
|
2887
|
+
_assertRetryNotOwningIdleAgent() {
|
|
2888
|
+
if (this._retry && !this.isStreaming) {
|
|
2889
|
+
throw new Error("Automatic retry is in progress. Cancel the retry before starting another prompt.");
|
|
2890
|
+
}
|
|
2891
|
+
}
|
|
2892
|
+
async _runAgentPrompt(messages) {
|
|
2893
|
+
this._assertRetryNotOwningIdleAgent();
|
|
2894
|
+
const start = this._reserveAgentRunStart();
|
|
2895
|
+
this._runRetryCount = 0;
|
|
2896
|
+
let promptFailure;
|
|
2897
|
+
try {
|
|
2898
|
+
await this.agent.prompt(messages);
|
|
2899
|
+
}
|
|
2900
|
+
catch (error) {
|
|
2901
|
+
promptFailure = { error };
|
|
2902
|
+
}
|
|
2903
|
+
if (!start.run && this._pendingAgentRunStart === start) {
|
|
2904
|
+
const startError = promptFailure
|
|
2905
|
+
? promptFailure.error instanceof Error
|
|
2906
|
+
? promptFailure.error
|
|
2907
|
+
: new Error(String(promptFailure.error))
|
|
2908
|
+
: new Error("Agent prompt completed before agent_start.");
|
|
2909
|
+
this._rejectAgentRunStart(start, startError);
|
|
2910
|
+
if (promptFailure)
|
|
2911
|
+
throw promptFailure.error;
|
|
2912
|
+
}
|
|
2913
|
+
let settlementFailure;
|
|
2914
|
+
try {
|
|
2915
|
+
const settlement = await start.promise;
|
|
2916
|
+
await this.waitForRetry(settlement);
|
|
2917
|
+
}
|
|
2918
|
+
catch (error) {
|
|
2919
|
+
settlementFailure = { error };
|
|
2086
2920
|
}
|
|
2087
|
-
|
|
2921
|
+
if (promptFailure && settlementFailure) {
|
|
2922
|
+
throw new AggregateError([promptFailure.error, settlementFailure.error], "Agent prompt and retry settlement both failed.", { cause: promptFailure.error });
|
|
2923
|
+
}
|
|
2924
|
+
if (promptFailure)
|
|
2925
|
+
throw promptFailure.error;
|
|
2926
|
+
if (settlementFailure)
|
|
2927
|
+
throw settlementFailure.error;
|
|
2928
|
+
}
|
|
2929
|
+
async waitForRetry(settlement) {
|
|
2930
|
+
if (!settlement)
|
|
2931
|
+
return;
|
|
2932
|
+
await settlement.promise;
|
|
2933
|
+
await settlement.chain.promise;
|
|
2088
2934
|
await this.agent.waitForIdle();
|
|
2089
2935
|
}
|
|
2090
2936
|
/** Whether auto-retry is currently in progress */
|
|
2091
2937
|
get isRetrying() {
|
|
2092
|
-
return this.
|
|
2938
|
+
return this._retry !== undefined;
|
|
2093
2939
|
}
|
|
2094
2940
|
/** Whether auto-retry is enabled */
|
|
2095
2941
|
get autoRetryEnabled() {
|
|
@@ -2309,7 +3155,7 @@ export class AgentSession {
|
|
|
2309
3155
|
if (targetEntry.type === "message" && targetEntry.message.role === "user") {
|
|
2310
3156
|
// User message: leaf = parent (null if root), text goes to editor
|
|
2311
3157
|
newLeafId = targetEntry.parentId;
|
|
2312
|
-
editorText =
|
|
3158
|
+
editorText = restoreScramjetCommandInvocation(targetEntry, this.sessionManager.getEntries());
|
|
2313
3159
|
}
|
|
2314
3160
|
else if (targetEntry.type === "custom_message") {
|
|
2315
3161
|
// Custom message: leaf = parent (null if root), text goes to editor
|
|
@@ -2373,29 +3219,56 @@ export class AgentSession {
|
|
|
2373
3219
|
*/
|
|
2374
3220
|
getUserMessagesForForking() {
|
|
2375
3221
|
const entries = this.sessionManager.getEntries();
|
|
3222
|
+
const commandStarts = indexScramjetCommandStarts(entries);
|
|
2376
3223
|
const result = [];
|
|
2377
3224
|
for (const entry of entries) {
|
|
2378
3225
|
if (entry.type !== "message")
|
|
2379
3226
|
continue;
|
|
2380
3227
|
if (entry.message.role !== "user")
|
|
2381
3228
|
continue;
|
|
2382
|
-
const text =
|
|
3229
|
+
const text = restoreScramjetCommandInvocation(entry, commandStarts);
|
|
2383
3230
|
if (text) {
|
|
2384
3231
|
result.push({ entryId: entry.id, text });
|
|
2385
3232
|
}
|
|
2386
3233
|
}
|
|
2387
3234
|
return result;
|
|
2388
3235
|
}
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
3236
|
+
// SCRAMJET-DIVERGENCE: whole-journal parent/tool costs plus reports awaiting successful append (#598).
|
|
3237
|
+
getRecordedSessionCost() {
|
|
3238
|
+
let total = 0;
|
|
3239
|
+
for (const entry of this.sessionManager.getEntries()) {
|
|
3240
|
+
if (entry.type !== "message")
|
|
3241
|
+
continue;
|
|
3242
|
+
const message = entry.message;
|
|
3243
|
+
if (message.role === "assistant") {
|
|
3244
|
+
if (validRecordedCost(message.usage?.cost?.total))
|
|
3245
|
+
total += message.usage.cost.total;
|
|
3246
|
+
}
|
|
3247
|
+
else if (message.role === "toolResult") {
|
|
3248
|
+
if (validRecordedCost(message.cost)) {
|
|
3249
|
+
total += message.cost;
|
|
3250
|
+
continue;
|
|
3251
|
+
}
|
|
3252
|
+
const definition = this._historicalCostDefinitions.get(message.toolName);
|
|
3253
|
+
if (!definition?.getHistoricalCost)
|
|
3254
|
+
continue;
|
|
3255
|
+
try {
|
|
3256
|
+
const cost = definition.getHistoricalCost(message.details);
|
|
3257
|
+
if (validRecordedCost(cost))
|
|
3258
|
+
total += cost;
|
|
3259
|
+
}
|
|
3260
|
+
catch (error) {
|
|
3261
|
+
this._extensionRunner.emitError({
|
|
3262
|
+
extensionPath: `tool:${message.toolName}`,
|
|
3263
|
+
event: "historical_cost",
|
|
3264
|
+
error: `Failed to interpret historical tool cost: ${error instanceof Error ? error.message : String(error)}`,
|
|
3265
|
+
});
|
|
3266
|
+
}
|
|
3267
|
+
}
|
|
2397
3268
|
}
|
|
2398
|
-
|
|
3269
|
+
for (const execution of this._pendingToolCosts)
|
|
3270
|
+
total += execution.cost ?? 0;
|
|
3271
|
+
return total;
|
|
2399
3272
|
}
|
|
2400
3273
|
/**
|
|
2401
3274
|
* Get session statistics.
|