@leanandmean/coding-agent 0.74.1-scramjet.6 → 0.74.1-scramjet.60
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 +21 -9
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +1 -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 +1 -0
- package/dist/core/agent-session-services.d.ts.map +1 -1
- package/dist/core/agent-session-services.js +2 -0
- package/dist/core/agent-session-services.js.map +1 -1
- package/dist/core/agent-session.d.ts +86 -16
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +865 -181
- 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/loader.d.ts.map +1 -1
- package/dist/core/extensions/loader.js +7 -0
- package/dist/core/extensions/loader.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 +162 -3
- package/dist/core/extensions/runner.js.map +1 -1
- package/dist/core/extensions/types.d.ts +110 -6
- 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 +4 -0
- package/dist/core/model-registry.d.ts.map +1 -1
- package/dist/core/model-registry.js +179 -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 +2 -0
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +61 -10
- 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 +18 -6
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +84 -16
- 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 +16 -0
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +38 -10
- 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 +22 -18
- package/dist/modes/interactive/components/footer.js.map +1 -1
- package/dist/modes/interactive/components/model-selector.d.ts +3 -1
- package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/model-selector.js +9 -3
- 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 +876 -175
- 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 +9 -3
- package/docs/custom-provider.md +13 -4
- package/docs/extensions.md +139 -21
- package/docs/keybindings.md +25 -1
- package/docs/models.md +34 -2
- package/docs/packages.md +2 -0
- package/docs/prompt-templates.md +2 -0
- package/docs/quickstart.md +1 -4
- package/docs/rpc.md +8 -6
- package/docs/sdk.md +22 -3
- package/docs/session-format.md +2 -1
- package/docs/settings.md +13 -3
- 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,7 +14,11 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
16
16
|
import { basename, dirname, resolve } from "node:path";
|
|
17
|
+
// SCRAMJET-DIVERGENCE: provider-safe harness tool-call id generation for the persisted-settlement
|
|
18
|
+
// acknowledgement key (#341).
|
|
19
|
+
import { generateHarnessToolCallId } from "@leanandmean/agent";
|
|
17
20
|
import { clampThinkingLevel, cleanupSessionResources, flattenSystemPrompt, getSupportedThinkingLevels, isContextOverflow, modelsAreEqual, resetApiProviders, } from "@leanandmean/ai";
|
|
21
|
+
import { validateResponsesProviderFailure } from "@leanandmean/ai/openai-responses";
|
|
18
22
|
import { theme } from "../modes/interactive/theme/theme.js";
|
|
19
23
|
import { stripFrontmatter } from "../utils/frontmatter.js";
|
|
20
24
|
import { sleep } from "../utils/sleep.js";
|
|
@@ -26,7 +30,10 @@ import { exportSessionToHtml } from "./export-html/index.js";
|
|
|
26
30
|
import { createToolHtmlRenderer } from "./export-html/tool-renderer.js";
|
|
27
31
|
import { ExtensionRunner, wrapRegisteredTools, } from "./extensions/index.js";
|
|
28
32
|
import { emitSessionShutdownEvent, spliceContributedSections } from "./extensions/runner.js";
|
|
33
|
+
import { OutputThroughputTracker } from "./output-throughput.js";
|
|
34
|
+
import { OutputThroughputHistoryStore } from "./output-throughput-history.js";
|
|
29
35
|
import { expandPromptTemplate } from "./prompt-templates.js";
|
|
36
|
+
import { indexScramjetCommandStarts, restoreScramjetCommandInvocation } from "./scramjet-command-parser.js";
|
|
30
37
|
import { CURRENT_SESSION_VERSION, getLatestCompactionEntry } from "./session-manager.js";
|
|
31
38
|
import { createSyntheticSourceInfo } from "./source-info.js";
|
|
32
39
|
import { buildSystemPromptSections } from "./system-prompt.js";
|
|
@@ -53,24 +60,59 @@ export function parseSkillBlock(text) {
|
|
|
53
60
|
// ============================================================================
|
|
54
61
|
/** Standard thinking levels */
|
|
55
62
|
const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high"];
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
63
|
+
function createDeferred() {
|
|
64
|
+
let resolve;
|
|
65
|
+
let reject;
|
|
66
|
+
const promise = new Promise((res, rej) => {
|
|
67
|
+
resolve = res;
|
|
68
|
+
reject = rej;
|
|
69
|
+
});
|
|
70
|
+
promise.catch(() => { });
|
|
71
|
+
return { promise, resolve, reject, settled: false };
|
|
72
|
+
}
|
|
73
|
+
function resolveDeferred(deferred, value) {
|
|
74
|
+
if (deferred.settled)
|
|
75
|
+
return false;
|
|
76
|
+
deferred.settled = true;
|
|
77
|
+
deferred.resolve(value);
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
function rejectDeferred(deferred, error) {
|
|
81
|
+
if (deferred.settled)
|
|
82
|
+
return false;
|
|
83
|
+
deferred.settled = true;
|
|
84
|
+
deferred.reject(error);
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
function settleDeferred(deferred, error) {
|
|
88
|
+
return error ? rejectDeferred(deferred, error) : resolveDeferred(deferred, undefined);
|
|
89
|
+
}
|
|
59
90
|
export class AgentSession {
|
|
60
91
|
agent;
|
|
61
92
|
sessionManager;
|
|
62
93
|
settingsManager;
|
|
94
|
+
outputThroughputHistoryPath;
|
|
63
95
|
_scopedModels;
|
|
64
96
|
// Event subscription state
|
|
65
97
|
_unsubscribeAgent;
|
|
66
98
|
_eventListeners = [];
|
|
67
99
|
_agentEventQueue = Promise.resolve();
|
|
100
|
+
_outputThroughputTracker = new OutputThroughputTracker();
|
|
101
|
+
_outputThroughputHistory;
|
|
102
|
+
_outputThroughputGeneration;
|
|
103
|
+
_latestOutputThroughputSample;
|
|
104
|
+
// SCRAMJET-DIVERGENCE: harness-tool persisted-settlement state (#341). `_disposed` gates post-
|
|
105
|
+
// teardown invocations; the map keys each in-flight invocation's tool-call id to its persistence
|
|
106
|
+
// acknowledgement so `_processAgentEvent` can settle it after the matching tool-result is persisted.
|
|
107
|
+
_disposed = false;
|
|
108
|
+
_harnessPersistenceAcks = new Map();
|
|
68
109
|
/** Tracks pending steering messages for UI display. Removed when delivered. */
|
|
69
110
|
_steeringMessages = [];
|
|
70
111
|
/** Tracks pending follow-up messages for UI display. Removed when delivered. */
|
|
71
112
|
_followUpMessages = [];
|
|
72
113
|
/** Messages queued to be included with the next user prompt as context ("asides"). */
|
|
73
114
|
_pendingNextTurnMessages = [];
|
|
115
|
+
_inputSessionEntries = new WeakMap();
|
|
74
116
|
// Compaction state
|
|
75
117
|
_compactionAbortController = undefined;
|
|
76
118
|
_autoCompactionAbortController = undefined;
|
|
@@ -78,10 +120,12 @@ export class AgentSession {
|
|
|
78
120
|
// Branch summarization state
|
|
79
121
|
_branchSummaryAbortController = undefined;
|
|
80
122
|
// Retry state
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
123
|
+
_retry = undefined;
|
|
124
|
+
_runRetryCount = 0;
|
|
125
|
+
_activeAgentRunSettlement = undefined;
|
|
126
|
+
_pendingAgentRunStart = undefined;
|
|
127
|
+
_unsettledAgentRuns = new Set();
|
|
128
|
+
_unsettledRetryChains = new Set();
|
|
85
129
|
// Bash execution state
|
|
86
130
|
_bashAbortController = undefined;
|
|
87
131
|
_pendingBashMessages = [];
|
|
@@ -114,10 +158,22 @@ export class AgentSession {
|
|
|
114
158
|
// can apply per-section cache control.
|
|
115
159
|
_baseSystemPromptSections = [];
|
|
116
160
|
_baseSystemPromptOptions;
|
|
161
|
+
// SCRAMJET-DIVERGENCE: active-tool prompt rebuilds preserve the current top-level run's
|
|
162
|
+
// before_agent_start composition while new prompts and reloads establish a fresh composition (#524).
|
|
163
|
+
_runPromptComposition = { kind: "generated-base" };
|
|
117
164
|
constructor(config) {
|
|
118
165
|
this.agent = config.agent;
|
|
119
166
|
this.sessionManager = config.sessionManager;
|
|
120
167
|
this.settingsManager = config.settingsManager;
|
|
168
|
+
this.outputThroughputHistoryPath = config.outputThroughputHistoryPath;
|
|
169
|
+
this._outputThroughputHistory = new OutputThroughputHistoryStore(this.outputThroughputHistoryPath, (diagnostic) => {
|
|
170
|
+
this._extensionRunner.emitError({
|
|
171
|
+
extensionPath: "output-throughput-history",
|
|
172
|
+
event: diagnostic.operation,
|
|
173
|
+
error: `${diagnostic.message} (${diagnostic.path})`,
|
|
174
|
+
stack: diagnostic.cause instanceof Error ? diagnostic.cause.stack : undefined,
|
|
175
|
+
});
|
|
176
|
+
});
|
|
121
177
|
this._scopedModels = config.scopedModels ?? [];
|
|
122
178
|
this._resourceLoader = config.resourceLoader;
|
|
123
179
|
this._customTools = config.customTools ?? [];
|
|
@@ -161,8 +217,8 @@ export class AgentSession {
|
|
|
161
217
|
throw new Error(formatNoApiKeyFoundMessage(model.provider));
|
|
162
218
|
}
|
|
163
219
|
// SCRAMJET-DIVERGENCE: _drainAgentEventQueue ensures all queued async extension event
|
|
164
|
-
// processing has settled before proceeding. Used by beforeToolBatch (pre-extraction drain)
|
|
165
|
-
//
|
|
220
|
+
// processing has settled before proceeding. Used by beforeToolBatch (pre-extraction drain),
|
|
221
|
+
// beforeToolCall (per-tool drain), and the awaited turn_end listener barrier.
|
|
166
222
|
async _drainAgentEventQueue() {
|
|
167
223
|
await this._agentEventQueue;
|
|
168
224
|
}
|
|
@@ -241,46 +297,147 @@ export class AgentSession {
|
|
|
241
297
|
followUp: [...this._followUpMessages],
|
|
242
298
|
});
|
|
243
299
|
}
|
|
244
|
-
// Track last assistant message for auto-compaction check
|
|
245
|
-
_lastAssistantMessage = undefined;
|
|
246
300
|
/** Internal handler for agent events - shared by subscribe and reconnect */
|
|
247
301
|
_handleAgentEvent = (event) => {
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
302
|
+
this._captureOutputThroughput(event);
|
|
303
|
+
const runSettlement = this._captureAgentRunSettlement(event);
|
|
304
|
+
const harnessAck = this._harnessPersistenceAckForEvent(event);
|
|
305
|
+
const processing = this._agentEventQueue.then(() => this._processAgentEventTracked(event, runSettlement, harnessAck), () => this._processAgentEventTracked(event, runSettlement, harnessAck));
|
|
306
|
+
this._agentEventQueue = processing;
|
|
307
|
+
if (harnessAck)
|
|
308
|
+
harnessAck.persistenceTail = processing;
|
|
255
309
|
// Keep queue alive if an event handler fails
|
|
256
|
-
|
|
310
|
+
processing.catch(() => { });
|
|
311
|
+
// SCRAMJET-DIVERGENCE: turn_end tool/prompt changes must settle before Agent snapshots
|
|
312
|
+
// live state for the next provider request (#524).
|
|
313
|
+
if (event.type === "turn_end")
|
|
314
|
+
return this._agentEventQueue;
|
|
257
315
|
};
|
|
258
|
-
|
|
259
|
-
|
|
316
|
+
// SCRAMJET-DIVERGENCE: Capture provider events synchronously and reset stale throughput lifecycles (#476).
|
|
317
|
+
_captureOutputThroughput(event) {
|
|
318
|
+
if (event.type === "message_start" && event.message.role === "assistant" && event.message.origin === "provider") {
|
|
319
|
+
this._outputThroughputGeneration = this._outputThroughputTracker.start({
|
|
320
|
+
provider: event.message.provider,
|
|
321
|
+
model: event.message.model,
|
|
322
|
+
});
|
|
260
323
|
return;
|
|
261
324
|
}
|
|
262
|
-
const
|
|
263
|
-
if (
|
|
325
|
+
const generation = this._outputThroughputGeneration;
|
|
326
|
+
if (generation === undefined)
|
|
264
327
|
return;
|
|
328
|
+
if (event.type === "message_update" && event.message.role === "assistant") {
|
|
329
|
+
this._outputThroughputTracker.observe(generation, event.assistantMessageEvent);
|
|
265
330
|
}
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
331
|
+
else if (event.type === "message_end" && event.message.role === "assistant") {
|
|
332
|
+
const sample = this._outputThroughputTracker.finalize(generation, event.message);
|
|
333
|
+
this._outputThroughputGeneration = undefined;
|
|
334
|
+
if (sample) {
|
|
335
|
+
this._latestOutputThroughputSample = sample;
|
|
336
|
+
this._outputThroughputHistory.submit(sample);
|
|
337
|
+
}
|
|
269
338
|
}
|
|
270
|
-
this._retryPromise = new Promise((resolve) => {
|
|
271
|
-
this._retryResolve = resolve;
|
|
272
|
-
});
|
|
273
339
|
}
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
340
|
+
_resetOutputThroughput() {
|
|
341
|
+
this._outputThroughputTracker.reset();
|
|
342
|
+
this._outputThroughputGeneration = undefined;
|
|
343
|
+
}
|
|
344
|
+
// SCRAMJET-DIVERGENCE: every Agent run owns persistence state and a one-shot settlement token (#553).
|
|
345
|
+
_captureAgentRunSettlement(event) {
|
|
346
|
+
if (event.type === "agent_start") {
|
|
347
|
+
// A retry continuation's run adopts the chain its originating prompt awaits; the chain stays
|
|
348
|
+
// bound to the retry phase until this start consumes it, however late Agent emits it.
|
|
349
|
+
const chain = this._retry?.phase === "continuing" ? this._retry.chain : createDeferred();
|
|
350
|
+
this._unsettledRetryChains.add(chain);
|
|
351
|
+
const run = { ...createDeferred(), chain };
|
|
352
|
+
this._activeAgentRunSettlement = run;
|
|
353
|
+
this._unsettledAgentRuns.add(run);
|
|
354
|
+
const reservation = this._pendingAgentRunStart;
|
|
355
|
+
if (reservation) {
|
|
356
|
+
this._pendingAgentRunStart = undefined;
|
|
357
|
+
reservation.run = run;
|
|
358
|
+
resolveDeferred(reservation, run);
|
|
359
|
+
}
|
|
360
|
+
return run;
|
|
361
|
+
}
|
|
362
|
+
const run = this._activeAgentRunSettlement;
|
|
363
|
+
if (event.type === "agent_end" && this._activeAgentRunSettlement === run) {
|
|
364
|
+
this._activeAgentRunSettlement = undefined;
|
|
365
|
+
}
|
|
366
|
+
return run;
|
|
367
|
+
}
|
|
368
|
+
_settleAgentRun(run, error) {
|
|
369
|
+
if (settleDeferred(run, error))
|
|
370
|
+
this._unsettledAgentRuns.delete(run);
|
|
371
|
+
}
|
|
372
|
+
_settleRetryChain(chain, error) {
|
|
373
|
+
if (settleDeferred(chain, error))
|
|
374
|
+
this._unsettledRetryChains.delete(chain);
|
|
375
|
+
}
|
|
376
|
+
_completeAgentRun(run, error) {
|
|
377
|
+
this._settleAgentRun(run, error);
|
|
378
|
+
this._settleRetryChain(run.chain, error);
|
|
379
|
+
}
|
|
380
|
+
_reserveAgentRunStart() {
|
|
381
|
+
if (this._pendingAgentRunStart)
|
|
382
|
+
throw new Error("An Agent prompt is already waiting to start.");
|
|
383
|
+
const reservation = createDeferred();
|
|
384
|
+
this._pendingAgentRunStart = reservation;
|
|
385
|
+
return reservation;
|
|
386
|
+
}
|
|
387
|
+
_rejectAgentRunStart(reservation, error) {
|
|
388
|
+
if (this._pendingAgentRunStart === reservation)
|
|
389
|
+
this._pendingAgentRunStart = undefined;
|
|
390
|
+
rejectDeferred(reservation, error);
|
|
391
|
+
}
|
|
392
|
+
// SCRAMJET-DIVERGENCE: harness-tool persisted settlement (#341). Synchronous ownership keeps queued
|
|
393
|
+
// invocation events attributed after early failures; public settlement awaits execution and the final persistence tail.
|
|
394
|
+
async _processAgentEventTracked(event, run, harnessAck) {
|
|
395
|
+
try {
|
|
396
|
+
await this._processAgentEvent(event, run, harnessAck !== undefined);
|
|
397
|
+
}
|
|
398
|
+
catch (err) {
|
|
399
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
400
|
+
if (harnessAck) {
|
|
401
|
+
harnessAck.firstPersistenceError ??= error;
|
|
402
|
+
}
|
|
403
|
+
else if (run) {
|
|
404
|
+
if (event.type === "message_end" && event.message.role === "assistant") {
|
|
405
|
+
run.assistantPersistenceError = error;
|
|
406
|
+
}
|
|
407
|
+
this._releaseRetryForFailedRun(error);
|
|
408
|
+
this._completeAgentRun(run, error);
|
|
409
|
+
}
|
|
410
|
+
throw err;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
_harnessPersistenceAckForEvent(event) {
|
|
414
|
+
let toolCallId;
|
|
415
|
+
switch (event.type) {
|
|
416
|
+
case "tool_execution_start":
|
|
417
|
+
case "tool_execution_update":
|
|
418
|
+
case "tool_execution_end":
|
|
419
|
+
toolCallId = event.toolCallId;
|
|
420
|
+
break;
|
|
421
|
+
case "message_start":
|
|
422
|
+
case "message_end": {
|
|
423
|
+
const message = event.message;
|
|
424
|
+
if (message.role === "toolResult") {
|
|
425
|
+
toolCallId = message.toolCallId;
|
|
426
|
+
}
|
|
427
|
+
else if (message.role === "assistant") {
|
|
428
|
+
for (const block of message.content) {
|
|
429
|
+
if (block.type === "toolCall" && this._harnessPersistenceAcks.has(block.id)) {
|
|
430
|
+
toolCallId = block.id;
|
|
431
|
+
break;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
break;
|
|
279
436
|
}
|
|
280
437
|
}
|
|
281
|
-
return undefined;
|
|
438
|
+
return toolCallId === undefined ? undefined : this._harnessPersistenceAcks.get(toolCallId);
|
|
282
439
|
}
|
|
283
|
-
async _processAgentEvent(event) {
|
|
440
|
+
async _processAgentEvent(event, run, fromHarnessInvocation = false) {
|
|
284
441
|
// When a user message starts, check if it's from either queue and remove it BEFORE emitting
|
|
285
442
|
// This ensures the UI sees the updated queue state
|
|
286
443
|
if (event.type === "message_start" && event.message.role === "user") {
|
|
@@ -307,6 +464,8 @@ export class AgentSession {
|
|
|
307
464
|
await this._emitExtensionEvent(event);
|
|
308
465
|
// Notify all listeners
|
|
309
466
|
this._emit(event);
|
|
467
|
+
if (this._disposed)
|
|
468
|
+
return;
|
|
310
469
|
// Handle session persistence
|
|
311
470
|
if (event.type === "message_end") {
|
|
312
471
|
// Check if this is a custom message from extensions
|
|
@@ -317,51 +476,123 @@ export class AgentSession {
|
|
|
317
476
|
else if (event.message.role === "user" ||
|
|
318
477
|
event.message.role === "assistant" ||
|
|
319
478
|
event.message.role === "toolResult") {
|
|
320
|
-
//
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
if (assistantMsg.stopReason !== "error") {
|
|
329
|
-
this._overflowRecoveryAttempted = false;
|
|
479
|
+
// SCRAMJET-DIVERGENCE: retry settlement classifies the exact JSON-safe assistant snapshot
|
|
480
|
+
// that crossed the persistence boundary, never a later-mutated live Agent message (#553).
|
|
481
|
+
const persistedMessage = event.message.role === "assistant"
|
|
482
|
+
? JSON.parse(JSON.stringify(event.message))
|
|
483
|
+
: event.message;
|
|
484
|
+
const messageEntryId = this.sessionManager.appendMessage(persistedMessage);
|
|
485
|
+
if (persistedMessage.role === "assistant" && !fromHarnessInvocation && run) {
|
|
486
|
+
run.persistedAssistantSnapshot = persistedMessage;
|
|
330
487
|
}
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
488
|
+
if (event.message.role === "user") {
|
|
489
|
+
const sessionEntries = this._inputSessionEntries.get(event.message);
|
|
490
|
+
this._inputSessionEntries.delete(event.message);
|
|
491
|
+
// This try/catch observes only failures thrown synchronously by
|
|
492
|
+
// appendCustomEntry. On the buffered first-turn path (before any
|
|
493
|
+
// assistant message) _persist stores the entry in memory with no I/O,
|
|
494
|
+
// so only metadata *serialization* failures surface here; the deferred
|
|
495
|
+
// disk write happens later in the unguarded appendMessage(assistant)
|
|
496
|
+
// flush, whose throw is swallowed by the _agentEventQueue.catch on the
|
|
497
|
+
// event-handler chain and degrades exact restoration silently.
|
|
498
|
+
for (const entry of sessionEntries ?? []) {
|
|
499
|
+
try {
|
|
500
|
+
this.sessionManager.appendCustomEntry(entry.customType, entry.data, messageEntryId);
|
|
501
|
+
}
|
|
502
|
+
catch (err) {
|
|
503
|
+
this._extensionRunner.emitError({
|
|
504
|
+
extensionPath: `session-entry:${entry.customType}`,
|
|
505
|
+
event: "session_entry_persistence",
|
|
506
|
+
error: `Failed to persist attached input metadata after its user message; exact input restoration may be unavailable: ${err instanceof Error ? err.message : String(err)}`,
|
|
507
|
+
stack: err instanceof Error ? err.stack : undefined,
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
}
|
|
340
511
|
}
|
|
341
512
|
}
|
|
513
|
+
// Other message types (bashExecution, compactionSummary, branchSummary) are persisted elsewhere
|
|
514
|
+
if (event.message.role === "assistant" &&
|
|
515
|
+
event.message.stopReason !== "error" &&
|
|
516
|
+
event.message.stopReason !== "aborted" &&
|
|
517
|
+
!fromHarnessInvocation) {
|
|
518
|
+
this._overflowRecoveryAttempted = false;
|
|
519
|
+
if (this._retry)
|
|
520
|
+
this._retry = { ...this._retry, attempt: 0 };
|
|
521
|
+
}
|
|
342
522
|
}
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
523
|
+
if (event.type === "agent_end") {
|
|
524
|
+
if (this._disposed || !run)
|
|
525
|
+
return;
|
|
526
|
+
if (run.assistantPersistenceError) {
|
|
527
|
+
const error = run.assistantPersistenceError;
|
|
528
|
+
run.assistantPersistenceError = undefined;
|
|
529
|
+
run.persistedAssistantSnapshot = undefined;
|
|
530
|
+
this._completeAgentRun(run, error);
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
const msg = run.persistedAssistantSnapshot;
|
|
534
|
+
run.persistedAssistantSnapshot = undefined;
|
|
535
|
+
if (!msg) {
|
|
536
|
+
this._releaseRetryForFailedRun(new Error("Retry continuation ended without an assistant message."));
|
|
537
|
+
this._completeAgentRun(run);
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
if (msg.stopReason === "aborted" && this._retry) {
|
|
541
|
+
this._finishCancelledRetry("cancelled_during_continuation");
|
|
542
|
+
this._completeAgentRun(run);
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
if (msg.stopReason !== "error") {
|
|
546
|
+
if (this._retry) {
|
|
547
|
+
const attempt = this._runRetryCount;
|
|
548
|
+
this._finishRetry({
|
|
549
|
+
schemaVersion: 1,
|
|
550
|
+
outcome: "succeeded",
|
|
551
|
+
attemptsCompleted: this._bounded(attempt),
|
|
552
|
+
cumulativeErrors: this._bounded(attempt),
|
|
553
|
+
}, { type: "auto_retry_end", success: true, attempt });
|
|
554
|
+
}
|
|
555
|
+
this._completeAgentRun(run);
|
|
556
|
+
await this._checkCompaction(msg);
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
const classification = this._classifyRetry(msg);
|
|
560
|
+
if (classification.kind === "context_overflow") {
|
|
561
|
+
this._finishRetryWithoutSuccess({
|
|
562
|
+
schemaVersion: 1,
|
|
563
|
+
outcome: "not_attempted",
|
|
564
|
+
reason: "context_overflow_compaction",
|
|
565
|
+
evidence: "context_overflow",
|
|
566
|
+
}, msg);
|
|
567
|
+
this._completeAgentRun(run);
|
|
568
|
+
await this._checkCompaction(msg);
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
const settings = this.settingsManager.getRetrySettings();
|
|
572
|
+
if (!settings.enabled) {
|
|
573
|
+
this._finishRetryWithoutSuccess({
|
|
574
|
+
schemaVersion: 1,
|
|
575
|
+
outcome: "not_attempted",
|
|
576
|
+
reason: "retry_disabled",
|
|
577
|
+
evidence: classification.evidence,
|
|
578
|
+
}, msg);
|
|
579
|
+
this._completeAgentRun(run);
|
|
580
|
+
await this._checkCompaction(msg);
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
if (classification.kind === "retry") {
|
|
584
|
+
const didRetry = await this._handleRetryableError(msg, classification.evidence, run);
|
|
350
585
|
if (didRetry)
|
|
351
|
-
return;
|
|
586
|
+
return;
|
|
352
587
|
}
|
|
353
|
-
|
|
588
|
+
else {
|
|
589
|
+
const { kind: _kind, ...cause } = classification;
|
|
590
|
+
this._finishRetryWithoutSuccess({ schemaVersion: 1, outcome: "not_attempted", ...cause }, msg);
|
|
591
|
+
}
|
|
592
|
+
this._completeAgentRun(run);
|
|
354
593
|
await this._checkCompaction(msg);
|
|
355
594
|
}
|
|
356
595
|
}
|
|
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
596
|
/** Extract text content from a message */
|
|
366
597
|
_getUserMessageText(message) {
|
|
367
598
|
if (message.role !== "user")
|
|
@@ -519,7 +750,67 @@ export class AgentSession {
|
|
|
519
750
|
* Call this when completely done with the session.
|
|
520
751
|
*/
|
|
521
752
|
dispose() {
|
|
753
|
+
// SCRAMJET-DIVERGENCE: idempotent disposal revokes harness invocations and closes retry settlement (#341, #553).
|
|
754
|
+
// A second call must not re-reject already-settled acknowledgements or re-run teardown.
|
|
755
|
+
if (this._disposed)
|
|
756
|
+
return;
|
|
757
|
+
this._disposed = true;
|
|
758
|
+
this._resetOutputThroughput();
|
|
759
|
+
const retryDisposeError = new Error("AgentSession disposed before retry settlement completed.");
|
|
760
|
+
const unsettledRuns = [...this._unsettledAgentRuns];
|
|
761
|
+
const retry = this._retry;
|
|
762
|
+
const unclassifiedError = unsettledRuns.find((run) => run.persistedAssistantSnapshot?.stopReason === "error")?.persistedAssistantSnapshot;
|
|
763
|
+
if (retry)
|
|
764
|
+
this.agent.abort();
|
|
765
|
+
try {
|
|
766
|
+
if (retry) {
|
|
767
|
+
this._appendAutoRetryRecord({
|
|
768
|
+
schemaVersion: 1,
|
|
769
|
+
outcome: "cancelled",
|
|
770
|
+
reason: "session_disposed",
|
|
771
|
+
attempt: this._bounded(retry.attempt),
|
|
772
|
+
cumulativeErrors: this._bounded(this._runRetryCount),
|
|
773
|
+
});
|
|
774
|
+
}
|
|
775
|
+
else if (unclassifiedError) {
|
|
776
|
+
const classification = this._classifyRetry(unclassifiedError);
|
|
777
|
+
this._appendAutoRetryRecord({
|
|
778
|
+
schemaVersion: 1,
|
|
779
|
+
outcome: "not_attempted",
|
|
780
|
+
reason: "session_disposed",
|
|
781
|
+
evidence: classification.kind === "context_overflow" ? "context_overflow" : classification.evidence,
|
|
782
|
+
});
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
catch (error) {
|
|
786
|
+
this._reportRetryFailure(error);
|
|
787
|
+
}
|
|
788
|
+
if (retry?.phase === "backoff")
|
|
789
|
+
retry.controller.abort();
|
|
790
|
+
this._retry = undefined;
|
|
791
|
+
this._activeAgentRunSettlement = undefined;
|
|
792
|
+
const pendingAgentRunStart = this._pendingAgentRunStart;
|
|
793
|
+
if (pendingAgentRunStart)
|
|
794
|
+
this._rejectAgentRunStart(pendingAgentRunStart, retryDisposeError);
|
|
795
|
+
const pendingRetryContinuation = retry?.phase === "continuing" ? retry.chain : undefined;
|
|
522
796
|
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().");
|
|
797
|
+
// Reject pending persistence acknowledgements before revoking Agent-side invocations, so a caller
|
|
798
|
+
// awaiting invokeHarnessTool sees rejection rather than a promise that never settles. Already-
|
|
799
|
+
// running tool work may finish afterward; teardown revokes the old-session guarantee but cannot
|
|
800
|
+
// roll back arbitrary side effects.
|
|
801
|
+
const disposeError = new Error("AgentSession disposed before the harness tool invocation settled.");
|
|
802
|
+
for (const ack of this._harnessPersistenceAcks.values()) {
|
|
803
|
+
this._settleHarnessPersistenceAck(ack, disposeError);
|
|
804
|
+
}
|
|
805
|
+
this._harnessPersistenceAcks.clear();
|
|
806
|
+
this.agent.rejectUnsettledHarnessTools(disposeError);
|
|
807
|
+
const retryChains = new Set(this._unsettledRetryChains);
|
|
808
|
+
if (pendingRetryContinuation)
|
|
809
|
+
retryChains.add(pendingRetryContinuation);
|
|
810
|
+
for (const run of unsettledRuns)
|
|
811
|
+
this._settleAgentRun(run, retryDisposeError);
|
|
812
|
+
for (const chain of retryChains)
|
|
813
|
+
this._settleRetryChain(chain, retryDisposeError);
|
|
523
814
|
this._disconnectFromAgent();
|
|
524
815
|
this._eventListeners = [];
|
|
525
816
|
cleanupSessionResources(this.sessionId);
|
|
@@ -547,9 +838,51 @@ export class AgentSession {
|
|
|
547
838
|
get systemPrompt() {
|
|
548
839
|
return flattenSystemPrompt(this.agent.state.systemPrompt);
|
|
549
840
|
}
|
|
841
|
+
get liveOutputRate() {
|
|
842
|
+
const model = this.state.model;
|
|
843
|
+
const identity = this._outputThroughputTracker.activeIdentity;
|
|
844
|
+
if (!model || identity?.provider !== model.provider || identity.model !== model.id)
|
|
845
|
+
return undefined;
|
|
846
|
+
return this._outputThroughputTracker.liveRate();
|
|
847
|
+
}
|
|
848
|
+
get outputThroughputActive() {
|
|
849
|
+
return this._outputThroughputTracker.outputActive;
|
|
850
|
+
}
|
|
851
|
+
get outputThroughputGeneration() {
|
|
852
|
+
return this._outputThroughputTracker.generation;
|
|
853
|
+
}
|
|
854
|
+
get medianOutputRate() {
|
|
855
|
+
const model = this.state.model;
|
|
856
|
+
if (!model)
|
|
857
|
+
return undefined;
|
|
858
|
+
const history = this._outputThroughputHistory.snapshot();
|
|
859
|
+
const local = this._latestOutputThroughputSample;
|
|
860
|
+
if (local?.provider === model.provider &&
|
|
861
|
+
local.model === model.id &&
|
|
862
|
+
!history
|
|
863
|
+
.samples(model.provider, model.id)
|
|
864
|
+
.some((sample) => sample.provider === local.provider &&
|
|
865
|
+
sample.model === local.model &&
|
|
866
|
+
sample.responseModel === local.responseModel &&
|
|
867
|
+
sample.outputTokens === local.outputTokens &&
|
|
868
|
+
sample.durationMs === local.durationMs &&
|
|
869
|
+
sample.observedAt === local.observedAt)) {
|
|
870
|
+
history.add(local);
|
|
871
|
+
}
|
|
872
|
+
return history.median(model.provider, model.id);
|
|
873
|
+
}
|
|
874
|
+
get outputThroughputHistory() {
|
|
875
|
+
return this._outputThroughputHistory.snapshot();
|
|
876
|
+
}
|
|
877
|
+
refreshOutputThroughputHistory() {
|
|
878
|
+
return this._outputThroughputHistory.refresh();
|
|
879
|
+
}
|
|
880
|
+
flushOutputThroughputHistory() {
|
|
881
|
+
return this._outputThroughputHistory.flush();
|
|
882
|
+
}
|
|
550
883
|
/** Current retry attempt (0 if not retrying) */
|
|
551
884
|
get retryAttempt() {
|
|
552
|
-
return this.
|
|
885
|
+
return this._retry?.attempt ?? 0;
|
|
553
886
|
}
|
|
554
887
|
/**
|
|
555
888
|
* Get the names of currently active tools.
|
|
@@ -582,6 +915,14 @@ export class AgentSession {
|
|
|
582
915
|
const tools = [];
|
|
583
916
|
const validToolNames = [];
|
|
584
917
|
for (const name of toolNames) {
|
|
918
|
+
// SCRAMJET-DIVERGENCE: harness-only tools are never placed in the provider-visible tool
|
|
919
|
+
// set, even when explicitly requested (#244). This is the single choke point for
|
|
920
|
+
// `agent.state.tools`, so filtering here structurally guarantees a harness-only tool can
|
|
921
|
+
// never masquerade as a model-callable one — while it stays resolvable in `_toolRegistry`
|
|
922
|
+
// for `invokeHarnessTool`.
|
|
923
|
+
if (this._toolDefinitions.get(name)?.definition.activation === "harness-only") {
|
|
924
|
+
continue;
|
|
925
|
+
}
|
|
585
926
|
const tool = this._toolRegistry.get(name);
|
|
586
927
|
if (tool) {
|
|
587
928
|
tools.push(tool);
|
|
@@ -592,6 +933,106 @@ export class AgentSession {
|
|
|
592
933
|
// Rebuild base system prompt with new tool set
|
|
593
934
|
this._rebuildSystemPrompt(validToolNames);
|
|
594
935
|
}
|
|
936
|
+
// SCRAMJET-DIVERGENCE: harness-tool invocation (#244), persisted-settlement boundary (#341).
|
|
937
|
+
/**
|
|
938
|
+
* Execute a registered tool as a harness-originated call (not requested by the model).
|
|
939
|
+
*
|
|
940
|
+
* Resolves the wrapped tool from the full registry — including `"harness-only"` tools that are
|
|
941
|
+
* never provider-visible — so its `execute` still receives the extension context and its
|
|
942
|
+
* `tool_call`/`tool_result` hooks still fire. Delegates to {@link Agent.runHarnessTool}, which
|
|
943
|
+
* runs the call through the real prepare/execute/finalize pipeline: identical live
|
|
944
|
+
* `tool_execution_*`/message events, persisted session entries, and extension hooks, but no
|
|
945
|
+
* run/turn framing. The idle-vs-mid-run branch lives in `runHarnessTool` (keyed on the agent's
|
|
946
|
+
* active run, which tracks `isStreaming`): idle calls execute immediately, mid-run calls queue
|
|
947
|
+
* and drain before the next intra-run LLM call.
|
|
948
|
+
*
|
|
949
|
+
* **Persisted settlement (#341).** This is a strictly stronger boundary than
|
|
950
|
+
* {@link Agent.runHarnessTool}: the returned promise resolves only after (1) the Agent-core
|
|
951
|
+
* pipeline fulfills *and* (2) `_processAgentEvent` has persisted the matching tool-result
|
|
952
|
+
* `message_end` via `SessionManager.appendMessage`. Because the event queue preserves order,
|
|
953
|
+
* that also proves the preceding synthetic assistant tool-call message was persisted first. It
|
|
954
|
+
* rejects on Agent-core failure, on failure to process/persist a matching event, on disposal
|
|
955
|
+
* before settlement, on a post-disposal call, on an unknown tool, or on a duplicate explicit
|
|
956
|
+
* tool-call id that is already pending. **Rejection means the promised pipeline did not complete —
|
|
957
|
+
* it does not prove artifact absence** (Agent state and persistence may have partially or fully
|
|
958
|
+
* completed before a late failure), so callers must not blindly retry. A tool's own `execute()`
|
|
959
|
+
* error is *not* a rejection: it becomes a normal `isError: true` result and the promise resolves
|
|
960
|
+
* after that error-result pipeline is persisted.
|
|
961
|
+
*
|
|
962
|
+
* **Reentrancy.** As with `runHarnessTool`, do not `await` this from work whose return is required
|
|
963
|
+
* for the Agent to reach the drain point or for the event queue to persist the matching result.
|
|
964
|
+
*/
|
|
965
|
+
async invokeHarnessTool(name, args, options) {
|
|
966
|
+
if (this._disposed) {
|
|
967
|
+
throw new Error(`Cannot invoke harness tool "${name}": the session has been disposed.`);
|
|
968
|
+
}
|
|
969
|
+
const tool = this._toolRegistry.get(name);
|
|
970
|
+
if (!tool) {
|
|
971
|
+
throw new Error(`Cannot invoke harness tool "${name}": no tool with that name is registered.`);
|
|
972
|
+
}
|
|
973
|
+
const toolCallId = this._allocateHarnessToolCallId(options?.toolCallId);
|
|
974
|
+
// Register before Agent execution can emit events. The coordinator retains attribution until Agent
|
|
975
|
+
// execution and the invocation's final queued persistence task both settle.
|
|
976
|
+
const ack = this._registerHarnessPersistenceAck(toolCallId);
|
|
977
|
+
void this._completeHarnessToolInvocation(this.agent.runHarnessTool(tool, args, { toolCallId }), ack);
|
|
978
|
+
try {
|
|
979
|
+
await ack.promise;
|
|
980
|
+
}
|
|
981
|
+
finally {
|
|
982
|
+
// Identity-checked cleanup: only drop the entry if it is still this invocation's ack (disposal
|
|
983
|
+
// may have cleared the map, and a later same-id invocation must not have its entry removed).
|
|
984
|
+
if (this._harnessPersistenceAcks.get(toolCallId) === ack) {
|
|
985
|
+
this._harnessPersistenceAcks.delete(toolCallId);
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
// SCRAMJET-DIVERGENCE: harness-tool persisted-settlement (#341). Allocate the tool-call id used as
|
|
990
|
+
// the acknowledgement key: reject a duplicate explicit id that is still pending, retry a generated
|
|
991
|
+
// id on the (astronomically unlikely) collision with a pending one.
|
|
992
|
+
_allocateHarnessToolCallId(explicit) {
|
|
993
|
+
if (explicit !== undefined) {
|
|
994
|
+
// Validate at the public trust boundary (invokeHarnessTool is on ExtensionAPI): an explicit
|
|
995
|
+
// id flows straight into the synthetic assistant message and on to the provider, so it must
|
|
996
|
+
// meet the provider-safe shape generateHarnessToolCallId guarantees (Anthropic's
|
|
997
|
+
// ^[a-zA-Z0-9_-]+$, bounded length).
|
|
998
|
+
if (!/^[a-zA-Z0-9_-]{1,64}$/.test(explicit)) {
|
|
999
|
+
throw new Error(`Cannot invoke harness tool with tool-call id "${explicit}": ids must be 1-64 characters of [a-zA-Z0-9_-].`);
|
|
1000
|
+
}
|
|
1001
|
+
if (this._harnessPersistenceAcks.has(explicit)) {
|
|
1002
|
+
throw new Error(`Cannot invoke harness tool with tool-call id "${explicit}": an invocation with that id is already pending.`);
|
|
1003
|
+
}
|
|
1004
|
+
return explicit;
|
|
1005
|
+
}
|
|
1006
|
+
let id = generateHarnessToolCallId();
|
|
1007
|
+
while (this._harnessPersistenceAcks.has(id)) {
|
|
1008
|
+
id = generateHarnessToolCallId();
|
|
1009
|
+
}
|
|
1010
|
+
return id;
|
|
1011
|
+
}
|
|
1012
|
+
async _completeHarnessToolInvocation(execution, ack) {
|
|
1013
|
+
let executionError;
|
|
1014
|
+
try {
|
|
1015
|
+
await execution;
|
|
1016
|
+
}
|
|
1017
|
+
catch (error) {
|
|
1018
|
+
executionError = error instanceof Error ? error : new Error(String(error));
|
|
1019
|
+
}
|
|
1020
|
+
try {
|
|
1021
|
+
await ack.persistenceTail;
|
|
1022
|
+
}
|
|
1023
|
+
catch { }
|
|
1024
|
+
this._settleHarnessPersistenceAck(ack, ack.firstPersistenceError ?? executionError);
|
|
1025
|
+
}
|
|
1026
|
+
_settleHarnessPersistenceAck(ack, error) {
|
|
1027
|
+
settleDeferred(ack, error);
|
|
1028
|
+
}
|
|
1029
|
+
// SCRAMJET-DIVERGENCE: harness-tool persisted-settlement (#341). Register a deferred acknowledgement
|
|
1030
|
+
// keyed by tool-call id before any invocation event can be emitted.
|
|
1031
|
+
_registerHarnessPersistenceAck(toolCallId) {
|
|
1032
|
+
const ack = { ...createDeferred(), persistenceTail: Promise.resolve() };
|
|
1033
|
+
this._harnessPersistenceAcks.set(toolCallId, ack);
|
|
1034
|
+
return ack;
|
|
1035
|
+
}
|
|
595
1036
|
/** Whether compaction or branch summarization is currently running */
|
|
596
1037
|
get isCompacting() {
|
|
597
1038
|
return (this._autoCompactionAbortController !== undefined ||
|
|
@@ -656,6 +1097,19 @@ export class AgentSession {
|
|
|
656
1097
|
}
|
|
657
1098
|
return Array.from(unique);
|
|
658
1099
|
}
|
|
1100
|
+
_applyRunPromptComposition() {
|
|
1101
|
+
switch (this._runPromptComposition.kind) {
|
|
1102
|
+
case "generated-base":
|
|
1103
|
+
this.agent.state.systemPrompt = this._baseSystemPromptSections.slice();
|
|
1104
|
+
break;
|
|
1105
|
+
case "contributed-sections":
|
|
1106
|
+
this.agent.state.systemPrompt = spliceContributedSections(this._baseSystemPromptSections, this._runPromptComposition.sections);
|
|
1107
|
+
break;
|
|
1108
|
+
case "authoritative-string":
|
|
1109
|
+
this.agent.state.systemPrompt = this._runPromptComposition.systemPrompt;
|
|
1110
|
+
break;
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
659
1113
|
/** Rebuild `_baseSystemPromptSections` from the current resources and tool set and apply it to agent state. */
|
|
660
1114
|
_rebuildSystemPrompt(toolNames) {
|
|
661
1115
|
const validToolNames = toolNames.filter((name) => this._toolRegistry.has(name));
|
|
@@ -676,6 +1130,7 @@ export class AgentSession {
|
|
|
676
1130
|
const appendSystemPrompt = loaderAppendSystemPrompt.length > 0 ? loaderAppendSystemPrompt.join("\n\n") : undefined;
|
|
677
1131
|
const loadedSkills = this._resourceLoader.getSkills().skills;
|
|
678
1132
|
const loadedContextFiles = this._resourceLoader.getAgentsFiles().agentsFiles;
|
|
1133
|
+
// SCRAMJET-DIVERGENCE: pass session file path for volatile environment fact (issue 348)
|
|
679
1134
|
this._baseSystemPromptOptions = {
|
|
680
1135
|
cwd: this._cwd,
|
|
681
1136
|
skills: loadedSkills,
|
|
@@ -685,9 +1140,10 @@ export class AgentSession {
|
|
|
685
1140
|
selectedTools: validToolNames,
|
|
686
1141
|
toolSnippets,
|
|
687
1142
|
promptGuidelines,
|
|
1143
|
+
sessionFile: this.sessionManager.getSessionFile(),
|
|
688
1144
|
};
|
|
689
1145
|
this._baseSystemPromptSections = buildSystemPromptSections(this._baseSystemPromptOptions);
|
|
690
|
-
this.
|
|
1146
|
+
this._applyRunPromptComposition();
|
|
691
1147
|
}
|
|
692
1148
|
// =========================================================================
|
|
693
1149
|
// Prompting
|
|
@@ -705,6 +1161,8 @@ export class AgentSession {
|
|
|
705
1161
|
const expandPromptTemplates = options?.expandPromptTemplates ?? true;
|
|
706
1162
|
const preflightResult = options?.preflightResult;
|
|
707
1163
|
let messages;
|
|
1164
|
+
let inputSessionEntries;
|
|
1165
|
+
let acceptInput;
|
|
708
1166
|
try {
|
|
709
1167
|
// Handle extension commands first (execute immediately, even during streaming)
|
|
710
1168
|
// Extension commands manage their own LLM interaction via pi.sendMessage()
|
|
@@ -716,6 +1174,7 @@ export class AgentSession {
|
|
|
716
1174
|
return;
|
|
717
1175
|
}
|
|
718
1176
|
}
|
|
1177
|
+
this._assertRetryNotOwningIdleAgent();
|
|
719
1178
|
// Emit input event for extension interception (before skill/template expansion)
|
|
720
1179
|
let currentText = text;
|
|
721
1180
|
let currentImages = options?.images;
|
|
@@ -728,6 +1187,8 @@ export class AgentSession {
|
|
|
728
1187
|
if (inputResult.action === "transform") {
|
|
729
1188
|
currentText = inputResult.text;
|
|
730
1189
|
currentImages = inputResult.images ?? currentImages;
|
|
1190
|
+
inputSessionEntries = inputResult.sessionEntries;
|
|
1191
|
+
acceptInput = inputResult.onAccepted;
|
|
731
1192
|
}
|
|
732
1193
|
}
|
|
733
1194
|
// Expand skill commands (/skill:name args) and prompt templates (/template args)
|
|
@@ -743,11 +1204,12 @@ export class AgentSession {
|
|
|
743
1204
|
"deliverAs when calling dispatchUserInput() or sendUserMessage() — to queue the message.");
|
|
744
1205
|
}
|
|
745
1206
|
if (options.streamingBehavior === "followUp") {
|
|
746
|
-
await this._queueFollowUp(expandedText, currentImages);
|
|
1207
|
+
await this._queueFollowUp(expandedText, currentImages, inputSessionEntries);
|
|
747
1208
|
}
|
|
748
1209
|
else {
|
|
749
|
-
await this._queueSteer(expandedText, currentImages);
|
|
1210
|
+
await this._queueSteer(expandedText, currentImages, inputSessionEntries);
|
|
750
1211
|
}
|
|
1212
|
+
acceptInput?.();
|
|
751
1213
|
preflightResult?.(true);
|
|
752
1214
|
return;
|
|
753
1215
|
}
|
|
@@ -778,16 +1240,21 @@ export class AgentSession {
|
|
|
778
1240
|
if (currentImages) {
|
|
779
1241
|
userContent.push(...currentImages);
|
|
780
1242
|
}
|
|
781
|
-
|
|
1243
|
+
const userMessage = {
|
|
782
1244
|
role: "user",
|
|
783
1245
|
content: userContent,
|
|
784
1246
|
timestamp: Date.now(),
|
|
785
|
-
}
|
|
1247
|
+
};
|
|
1248
|
+
messages.push(userMessage);
|
|
1249
|
+
if (inputSessionEntries?.length) {
|
|
1250
|
+
this._inputSessionEntries.set(userMessage, inputSessionEntries);
|
|
1251
|
+
}
|
|
786
1252
|
// Inject any pending "nextTurn" messages as context alongside the user message
|
|
787
1253
|
for (const msg of this._pendingNextTurnMessages) {
|
|
788
1254
|
messages.push(msg);
|
|
789
1255
|
}
|
|
790
1256
|
this._pendingNextTurnMessages = [];
|
|
1257
|
+
this._runPromptComposition = { kind: "generated-base" };
|
|
791
1258
|
// Emit before_agent_start extension event
|
|
792
1259
|
const result = await this._extensionRunner.emitBeforeAgentStart(expandedText, currentImages, this._baseSystemPromptSections, this._baseSystemPromptOptions);
|
|
793
1260
|
// Add all custom messages from extensions
|
|
@@ -807,15 +1274,18 @@ export class AgentSession {
|
|
|
807
1274
|
// replacement is authoritative (even an empty string): it wins for the
|
|
808
1275
|
// whole prompt and any contributed sections are dropped for this turn.
|
|
809
1276
|
if (result?.systemPrompt !== undefined) {
|
|
810
|
-
this.
|
|
1277
|
+
this._runPromptComposition = {
|
|
1278
|
+
kind: "authoritative-string",
|
|
1279
|
+
systemPrompt: result.systemPrompt,
|
|
1280
|
+
};
|
|
811
1281
|
}
|
|
812
1282
|
else if (result?.systemPromptSections) {
|
|
813
|
-
this.
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
this.agent.state.systemPrompt = this._baseSystemPromptSections.slice();
|
|
1283
|
+
this._runPromptComposition = {
|
|
1284
|
+
kind: "contributed-sections",
|
|
1285
|
+
sections: result.systemPromptSections.map((section) => ({ ...section })),
|
|
1286
|
+
};
|
|
818
1287
|
}
|
|
1288
|
+
this._applyRunPromptComposition();
|
|
819
1289
|
}
|
|
820
1290
|
catch (error) {
|
|
821
1291
|
preflightResult?.(false);
|
|
@@ -824,9 +1294,9 @@ export class AgentSession {
|
|
|
824
1294
|
if (!messages) {
|
|
825
1295
|
return;
|
|
826
1296
|
}
|
|
1297
|
+
acceptInput?.();
|
|
827
1298
|
preflightResult?.(true);
|
|
828
|
-
await this.
|
|
829
|
-
await this.waitForRetry();
|
|
1299
|
+
await this._runAgentPrompt(messages);
|
|
830
1300
|
}
|
|
831
1301
|
/**
|
|
832
1302
|
* Try to execute an extension command. Returns true if command was found and executed.
|
|
@@ -923,34 +1393,42 @@ export class AgentSession {
|
|
|
923
1393
|
/**
|
|
924
1394
|
* Internal: Queue a steering message (already expanded, no extension command check).
|
|
925
1395
|
*/
|
|
926
|
-
async _queueSteer(text, images) {
|
|
1396
|
+
async _queueSteer(text, images, sessionEntries) {
|
|
927
1397
|
this._steeringMessages.push(text);
|
|
928
1398
|
this._emitQueueUpdate();
|
|
929
1399
|
const content = [{ type: "text", text }];
|
|
930
1400
|
if (images) {
|
|
931
1401
|
content.push(...images);
|
|
932
1402
|
}
|
|
933
|
-
|
|
1403
|
+
const message = {
|
|
934
1404
|
role: "user",
|
|
935
1405
|
content,
|
|
936
1406
|
timestamp: Date.now(),
|
|
937
|
-
}
|
|
1407
|
+
};
|
|
1408
|
+
if (sessionEntries?.length) {
|
|
1409
|
+
this._inputSessionEntries.set(message, sessionEntries);
|
|
1410
|
+
}
|
|
1411
|
+
this.agent.steer(message);
|
|
938
1412
|
}
|
|
939
1413
|
/**
|
|
940
1414
|
* Internal: Queue a follow-up message (already expanded, no extension command check).
|
|
941
1415
|
*/
|
|
942
|
-
async _queueFollowUp(text, images) {
|
|
1416
|
+
async _queueFollowUp(text, images, sessionEntries) {
|
|
943
1417
|
this._followUpMessages.push(text);
|
|
944
1418
|
this._emitQueueUpdate();
|
|
945
1419
|
const content = [{ type: "text", text }];
|
|
946
1420
|
if (images) {
|
|
947
1421
|
content.push(...images);
|
|
948
1422
|
}
|
|
949
|
-
|
|
1423
|
+
const message = {
|
|
950
1424
|
role: "user",
|
|
951
1425
|
content,
|
|
952
1426
|
timestamp: Date.now(),
|
|
953
|
-
}
|
|
1427
|
+
};
|
|
1428
|
+
if (sessionEntries?.length) {
|
|
1429
|
+
this._inputSessionEntries.set(message, sessionEntries);
|
|
1430
|
+
}
|
|
1431
|
+
this.agent.followUp(message);
|
|
954
1432
|
}
|
|
955
1433
|
/**
|
|
956
1434
|
* Throw an error if the text is an extension command.
|
|
@@ -996,7 +1474,7 @@ export class AgentSession {
|
|
|
996
1474
|
}
|
|
997
1475
|
}
|
|
998
1476
|
else if (options?.triggerTurn) {
|
|
999
|
-
await this.
|
|
1477
|
+
await this._runAgentPrompt(appMessage);
|
|
1000
1478
|
}
|
|
1001
1479
|
else {
|
|
1002
1480
|
this.agent.state.messages.push(appMessage);
|
|
@@ -1093,6 +1571,7 @@ export class AgentSession {
|
|
|
1093
1571
|
*/
|
|
1094
1572
|
async abort() {
|
|
1095
1573
|
this.abortRetry();
|
|
1574
|
+
this._resetOutputThroughput();
|
|
1096
1575
|
this.agent.abort();
|
|
1097
1576
|
await this.agent.waitForIdle();
|
|
1098
1577
|
}
|
|
@@ -1280,6 +1759,7 @@ export class AgentSession {
|
|
|
1280
1759
|
* @param customInstructions Optional instructions for the compaction summary
|
|
1281
1760
|
*/
|
|
1282
1761
|
async compact(customInstructions) {
|
|
1762
|
+
this._resetOutputThroughput();
|
|
1283
1763
|
this._disconnectFromAgent();
|
|
1284
1764
|
await this.abort();
|
|
1285
1765
|
this._compactionAbortController = new AbortController();
|
|
@@ -1417,6 +1897,7 @@ export class AgentSession {
|
|
|
1417
1897
|
// Skip if message was aborted (user cancelled) - unless skipAbortedCheck is false
|
|
1418
1898
|
if (skipAbortedCheck && assistantMessage.stopReason === "aborted")
|
|
1419
1899
|
return;
|
|
1900
|
+
// SCRAMJET-DIVERGENCE: Total context and independent input limits are separate constraints.
|
|
1420
1901
|
const contextWindow = this.model?.contextWindow ?? 0;
|
|
1421
1902
|
// Skip overflow check if the message came from a different model.
|
|
1422
1903
|
// This handles the case where user switched from a smaller-context model (e.g. opus)
|
|
@@ -1432,7 +1913,8 @@ export class AgentSession {
|
|
|
1432
1913
|
return;
|
|
1433
1914
|
}
|
|
1434
1915
|
// Case 1: Overflow - LLM returned context overflow error
|
|
1435
|
-
if (sameModel &&
|
|
1916
|
+
if (sameModel &&
|
|
1917
|
+
isContextOverflow(assistantMessage, Math.min(contextWindow, this.model?.maxInputTokens ?? Infinity))) {
|
|
1436
1918
|
if (this._overflowRecoveryAttempted) {
|
|
1437
1919
|
this._emit({
|
|
1438
1920
|
type: "compaction_end",
|
|
@@ -1477,7 +1959,8 @@ export class AgentSession {
|
|
|
1477
1959
|
else {
|
|
1478
1960
|
contextTokens = calculateContextTokens(assistantMessage.usage);
|
|
1479
1961
|
}
|
|
1480
|
-
if (shouldCompact(contextTokens, contextWindow, settings)
|
|
1962
|
+
if (shouldCompact(contextTokens, contextWindow, settings) ||
|
|
1963
|
+
(contextWindow > settings.reserveTokens && contextTokens > (this.model?.maxInputTokens ?? Infinity))) {
|
|
1481
1964
|
await this._runAutoCompaction("threshold", false);
|
|
1482
1965
|
}
|
|
1483
1966
|
}
|
|
@@ -1773,6 +2256,8 @@ export class AgentSession {
|
|
|
1773
2256
|
getActiveTools: () => this.getActiveToolNames(),
|
|
1774
2257
|
getAllTools: () => this.getAllTools(),
|
|
1775
2258
|
setActiveTools: (toolNames) => this.setActiveToolsByName(toolNames),
|
|
2259
|
+
// SCRAMJET-DIVERGENCE: harness-tool invocation (#244).
|
|
2260
|
+
invokeHarnessTool: (name, args, options) => this.invokeHarnessTool(name, args, options),
|
|
1776
2261
|
refreshTools: () => this._refreshToolRegistry(),
|
|
1777
2262
|
getCommands,
|
|
1778
2263
|
setModel: async (model) => {
|
|
@@ -1785,6 +2270,7 @@ export class AgentSession {
|
|
|
1785
2270
|
setThinkingLevel: (level) => this.setThinkingLevel(level),
|
|
1786
2271
|
}, {
|
|
1787
2272
|
getModel: () => this.model,
|
|
2273
|
+
getScopedModels: () => this.scopedModels,
|
|
1788
2274
|
isIdle: () => !this.isStreaming,
|
|
1789
2275
|
getSignal: () => this.agent.signal,
|
|
1790
2276
|
abort: () => this.abort(),
|
|
@@ -1931,10 +2417,21 @@ export class AgentSession {
|
|
|
1931
2417
|
}
|
|
1932
2418
|
async reload() {
|
|
1933
2419
|
const previousFlagValues = this._extensionRunner.getFlagValues();
|
|
1934
|
-
|
|
2420
|
+
// SCRAMJET-DIVERGENCE: the Scramjet builtin is a required product component, so reload must validate
|
|
2421
|
+
// the replacement loader before any irreversible runtime mutation. settingsManager.reload() runs first —
|
|
2422
|
+
// it commits fresh settings before the loader can throw, but the re-read is idempotent, so a failed reload
|
|
2423
|
+
// leaves the runtime itself untouched. It also runs first
|
|
2424
|
+
// because the loader's path resolution reads live settings (the loader defers its own settings/source-map
|
|
2425
|
+
// commit until after the builtin init succeeds, so on this path the shared manager is intentionally read
|
|
2426
|
+
// twice). resourceLoader.reload() then re-runs the required
|
|
2427
|
+
// builtin and throws RequiredBuiltinInitError on failure — before session_shutdown is emitted and before
|
|
2428
|
+
// resetApiProviders(). A throwing builtin therefore leaves the current runner, resources, and providers
|
|
2429
|
+
// intact and fires no session_shutdown; only a validated candidate proceeds to teardown and rebuild.
|
|
1935
2430
|
await this.settingsManager.reload();
|
|
1936
|
-
resetApiProviders();
|
|
1937
2431
|
await this._resourceLoader.reload();
|
|
2432
|
+
await emitSessionShutdownEvent(this._extensionRunner, { type: "session_shutdown", reason: "reload" });
|
|
2433
|
+
resetApiProviders();
|
|
2434
|
+
this._runPromptComposition = { kind: "generated-base" };
|
|
1938
2435
|
this._buildRuntime({
|
|
1939
2436
|
activeToolNames: this.getActiveToolNames(),
|
|
1940
2437
|
flagValues: previousFlagValues,
|
|
@@ -1952,114 +2449,311 @@ export class AgentSession {
|
|
|
1952
2449
|
// =========================================================================
|
|
1953
2450
|
// Auto-Retry
|
|
1954
2451
|
// =========================================================================
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
* Context overflow errors are NOT retryable (handled by compaction instead).
|
|
1958
|
-
*/
|
|
1959
|
-
_isRetryableError(message) {
|
|
1960
|
-
if (message.stopReason !== "error" || !message.errorMessage)
|
|
1961
|
-
return false;
|
|
1962
|
-
// Context overflow is handled by compaction, not retry
|
|
2452
|
+
// SCRAMJET-DIVERGENCE: persisted post-extension snapshots authoritatively classify and settle retries (#553).
|
|
2453
|
+
_classifyRetry(message) {
|
|
1963
2454
|
const contextWindow = this.model?.contextWindow ?? 0;
|
|
1964
|
-
if (isContextOverflow(message, contextWindow))
|
|
1965
|
-
return
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
2455
|
+
if (isContextOverflow(message, Math.min(contextWindow, this.model?.maxInputTokens ?? Infinity))) {
|
|
2456
|
+
return { kind: "context_overflow" };
|
|
2457
|
+
}
|
|
2458
|
+
const structured = validateResponsesProviderFailure(message.diagnostics);
|
|
2459
|
+
if (structured.status === "valid") {
|
|
2460
|
+
if (structured.retryDisposition === "transient")
|
|
2461
|
+
return { kind: "retry", evidence: "provider_failure" };
|
|
2462
|
+
return {
|
|
2463
|
+
kind: "do_not_retry",
|
|
2464
|
+
reason: structured.retryDisposition === "non_transient" ? "structured_non_transient" : "structured_unknown",
|
|
2465
|
+
evidence: "provider_failure",
|
|
2466
|
+
};
|
|
2467
|
+
}
|
|
2468
|
+
if (structured.status === "malformed") {
|
|
2469
|
+
return { kind: "do_not_retry", reason: "malformed_provider_diagnostic", evidence: "none" };
|
|
2470
|
+
}
|
|
2471
|
+
if (structured.status === "duplicate") {
|
|
2472
|
+
return { kind: "do_not_retry", reason: "duplicate_provider_diagnostic", evidence: "none" };
|
|
2473
|
+
}
|
|
2474
|
+
if (!message.errorMessage) {
|
|
2475
|
+
return { kind: "do_not_retry", reason: "missing_error_evidence", evidence: "none" };
|
|
2476
|
+
}
|
|
2477
|
+
if (this._isLegacyRetryableError(message.errorMessage))
|
|
2478
|
+
return { kind: "retry", evidence: "legacy_text" };
|
|
2479
|
+
return { kind: "do_not_retry", reason: "legacy_non_retryable", evidence: "legacy_text" };
|
|
2480
|
+
}
|
|
2481
|
+
_isLegacyRetryableError(errorMessage) {
|
|
2482
|
+
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);
|
|
2483
|
+
}
|
|
2484
|
+
_bounded(value) {
|
|
2485
|
+
return Math.min(65_535, Math.max(0, Number.isFinite(value) ? Math.trunc(value) : 65_535));
|
|
2486
|
+
}
|
|
2487
|
+
_appendAutoRetryRecord(record) {
|
|
2488
|
+
this.sessionManager.appendCustomEntry("coding-agent:auto-retry", record);
|
|
1969
2489
|
}
|
|
1970
2490
|
/**
|
|
1971
|
-
*
|
|
1972
|
-
*
|
|
2491
|
+
* Persist a terminal record, emit `auto_retry_end` when a retry phase was active, and clear the phase.
|
|
2492
|
+
* The phase is released before persistence is attempted so a persistence failure can never strand it.
|
|
1973
2493
|
*/
|
|
1974
|
-
|
|
2494
|
+
_finishRetry(record, end) {
|
|
2495
|
+
const retry = this._retry;
|
|
2496
|
+
this._retry = undefined;
|
|
2497
|
+
try {
|
|
2498
|
+
this._appendAutoRetryRecord(record);
|
|
2499
|
+
}
|
|
2500
|
+
catch (error) {
|
|
2501
|
+
if (retry) {
|
|
2502
|
+
this._emit({
|
|
2503
|
+
type: "auto_retry_end",
|
|
2504
|
+
success: false,
|
|
2505
|
+
attempt: end?.attempt ?? retry.attempt,
|
|
2506
|
+
finalError: "Retry outcome persistence failed",
|
|
2507
|
+
});
|
|
2508
|
+
}
|
|
2509
|
+
throw this._reportRetryFailure(error);
|
|
2510
|
+
}
|
|
2511
|
+
if (retry && end)
|
|
2512
|
+
this._emit(end);
|
|
2513
|
+
}
|
|
2514
|
+
_finishRetryWithoutSuccess(record, message) {
|
|
2515
|
+
const retry = this._retry;
|
|
2516
|
+
if (retry?.attempt === 0) {
|
|
2517
|
+
// The continuation already produced a successful message, so the retry recovered and this
|
|
2518
|
+
// later failure is a fresh, non-retried error; close the retry as succeeded first.
|
|
2519
|
+
const attempt = this._runRetryCount;
|
|
2520
|
+
this._finishRetry({
|
|
2521
|
+
schemaVersion: 1,
|
|
2522
|
+
outcome: "succeeded",
|
|
2523
|
+
attemptsCompleted: this._bounded(attempt),
|
|
2524
|
+
cumulativeErrors: this._bounded(attempt),
|
|
2525
|
+
}, { type: "auto_retry_end", success: true, attempt });
|
|
2526
|
+
this._finishRetry(record);
|
|
2527
|
+
return;
|
|
2528
|
+
}
|
|
2529
|
+
this._finishRetry(record, retry
|
|
2530
|
+
? { type: "auto_retry_end", success: false, attempt: retry.attempt, finalError: message.errorMessage }
|
|
2531
|
+
: undefined);
|
|
2532
|
+
}
|
|
2533
|
+
/** A run that ends without a classifiable snapshot cannot continue the retry; close it so nothing stays wedged. */
|
|
2534
|
+
_releaseRetryForFailedRun(error) {
|
|
2535
|
+
const retry = this._retry;
|
|
2536
|
+
if (!retry)
|
|
2537
|
+
return;
|
|
2538
|
+
try {
|
|
2539
|
+
this._finishRetry({
|
|
2540
|
+
schemaVersion: 1,
|
|
2541
|
+
outcome: "failed",
|
|
2542
|
+
reason: "run_failed",
|
|
2543
|
+
attempt: this._bounded(retry.attempt),
|
|
2544
|
+
cumulativeErrors: this._bounded(this._runRetryCount),
|
|
2545
|
+
}, { type: "auto_retry_end", success: false, attempt: retry.attempt, finalError: error.message });
|
|
2546
|
+
}
|
|
2547
|
+
catch {
|
|
2548
|
+
// Already reported and the phase is released; the run settles with its own error.
|
|
2549
|
+
}
|
|
2550
|
+
}
|
|
2551
|
+
_reportRetryFailure(error) {
|
|
2552
|
+
const normalized = error instanceof Error ? error : new Error(String(error));
|
|
2553
|
+
this._extensionRunner.emitError({
|
|
2554
|
+
extensionPath: "agent-session:auto-retry",
|
|
2555
|
+
event: "retry_settlement",
|
|
2556
|
+
error: normalized.message,
|
|
2557
|
+
stack: normalized.stack,
|
|
2558
|
+
});
|
|
2559
|
+
return normalized;
|
|
2560
|
+
}
|
|
2561
|
+
async _handleRetryableError(message, evidence, run) {
|
|
1975
2562
|
const settings = this.settingsManager.getRetrySettings();
|
|
1976
|
-
|
|
1977
|
-
|
|
2563
|
+
const attempt = (this._retry?.attempt ?? 0) + 1;
|
|
2564
|
+
this._runRetryCount++;
|
|
2565
|
+
const cumulativeCap = settings.maxRetries * 2;
|
|
2566
|
+
if (attempt > settings.maxRetries) {
|
|
2567
|
+
const attemptsCompleted = attempt - 1;
|
|
2568
|
+
this._finishRetry({
|
|
2569
|
+
schemaVersion: 1,
|
|
2570
|
+
outcome: "exhausted",
|
|
2571
|
+
reason: "attempt_limit",
|
|
2572
|
+
attemptsCompleted: this._bounded(attemptsCompleted),
|
|
2573
|
+
maxAttempts: this._bounded(settings.maxRetries),
|
|
2574
|
+
cumulativeErrors: this._bounded(this._runRetryCount),
|
|
2575
|
+
}, { type: "auto_retry_end", success: false, attempt: attemptsCompleted, finalError: message.errorMessage });
|
|
1978
2576
|
return false;
|
|
1979
2577
|
}
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
this._emit({
|
|
2578
|
+
if (this._runRetryCount > cumulativeCap) {
|
|
2579
|
+
const attemptsCompleted = this._runRetryCount - 1;
|
|
2580
|
+
this._finishRetry({
|
|
2581
|
+
schemaVersion: 1,
|
|
2582
|
+
outcome: "exhausted",
|
|
2583
|
+
reason: "cumulative_limit",
|
|
2584
|
+
attemptsCompleted: this._bounded(attemptsCompleted),
|
|
2585
|
+
maxAttempts: this._bounded(settings.maxRetries),
|
|
2586
|
+
cumulativeErrors: this._bounded(this._runRetryCount),
|
|
2587
|
+
}, {
|
|
1991
2588
|
type: "auto_retry_end",
|
|
1992
2589
|
success: false,
|
|
1993
|
-
attempt:
|
|
1994
|
-
finalError: message.errorMessage,
|
|
2590
|
+
attempt: attemptsCompleted,
|
|
2591
|
+
finalError: `Repeated retry failures (${attemptsCompleted} total attempts this prompt). ${message.errorMessage ?? ""}`.trim(),
|
|
1995
2592
|
});
|
|
1996
|
-
this._retryAttempt = 0;
|
|
1997
|
-
this._resolveRetry(); // Resolve so waitForRetry() completes
|
|
1998
2593
|
return false;
|
|
1999
2594
|
}
|
|
2000
|
-
const delayMs = settings.baseDelayMs * 2 ** (
|
|
2595
|
+
const delayMs = settings.baseDelayMs * 2 ** (attempt - 1);
|
|
2596
|
+
const previous = this._retry;
|
|
2597
|
+
this._retry = undefined;
|
|
2598
|
+
try {
|
|
2599
|
+
this._appendAutoRetryRecord({
|
|
2600
|
+
schemaVersion: 1,
|
|
2601
|
+
outcome: "scheduled",
|
|
2602
|
+
evidence,
|
|
2603
|
+
attempt: this._bounded(attempt),
|
|
2604
|
+
maxAttempts: this._bounded(settings.maxRetries),
|
|
2605
|
+
cumulativeErrors: this._bounded(this._runRetryCount),
|
|
2606
|
+
delayMs: this._bounded(delayMs),
|
|
2607
|
+
});
|
|
2608
|
+
}
|
|
2609
|
+
catch (error) {
|
|
2610
|
+
if (previous) {
|
|
2611
|
+
this._emit({
|
|
2612
|
+
type: "auto_retry_end",
|
|
2613
|
+
success: false,
|
|
2614
|
+
attempt: previous.attempt,
|
|
2615
|
+
finalError: "Retry outcome persistence failed",
|
|
2616
|
+
});
|
|
2617
|
+
}
|
|
2618
|
+
throw this._reportRetryFailure(error);
|
|
2619
|
+
}
|
|
2620
|
+
const controller = new AbortController();
|
|
2621
|
+
const backoff = { phase: "backoff", attempt, controller, run };
|
|
2622
|
+
this._retry = backoff;
|
|
2001
2623
|
this._emit({
|
|
2002
2624
|
type: "auto_retry_start",
|
|
2003
|
-
attempt
|
|
2625
|
+
attempt,
|
|
2004
2626
|
maxAttempts: settings.maxRetries,
|
|
2005
2627
|
delayMs,
|
|
2006
2628
|
errorMessage: message.errorMessage || "Unknown error",
|
|
2629
|
+
cumulativeErrors: this._runRetryCount,
|
|
2007
2630
|
});
|
|
2008
|
-
|
|
2631
|
+
if (controller.signal.aborted || this._disposed) {
|
|
2632
|
+
return this._finishCancelledRetry("cancelled_during_backoff", backoff);
|
|
2633
|
+
}
|
|
2009
2634
|
const messages = this.agent.state.messages;
|
|
2010
2635
|
if (messages.length > 0 && messages[messages.length - 1].role === "assistant") {
|
|
2011
2636
|
this.agent.state.messages = messages.slice(0, -1);
|
|
2012
2637
|
}
|
|
2013
|
-
// Wait with exponential backoff (abortable)
|
|
2014
|
-
this._retryAbortController = new AbortController();
|
|
2015
2638
|
try {
|
|
2016
|
-
await sleep(delayMs,
|
|
2639
|
+
await sleep(delayMs, controller.signal);
|
|
2640
|
+
await sleep(0, controller.signal);
|
|
2017
2641
|
}
|
|
2018
2642
|
catch {
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
this.
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2643
|
+
return this._finishCancelledRetry("cancelled_during_backoff", backoff);
|
|
2644
|
+
}
|
|
2645
|
+
if (controller.signal.aborted || this._disposed) {
|
|
2646
|
+
return this._finishCancelledRetry("cancelled_during_backoff", backoff);
|
|
2647
|
+
}
|
|
2648
|
+
// Hand the chain to the continuation before `continue()`; the run that eventually emits
|
|
2649
|
+
// `agent_start` adopts it (see `_captureAgentRunSettlement`), however long Agent defers that start.
|
|
2650
|
+
const continuing = { phase: "continuing", attempt, chain: run.chain };
|
|
2651
|
+
this._retry = continuing;
|
|
2652
|
+
const continuation = this.agent.continue();
|
|
2653
|
+
this._settleAgentRun(run);
|
|
2654
|
+
continuation.catch((error) => {
|
|
2655
|
+
if (this._disposed || this._retry !== continuing)
|
|
2656
|
+
return;
|
|
2657
|
+
const continuationError = this._reportRetryFailure(error);
|
|
2658
|
+
let settlementError = continuationError;
|
|
2659
|
+
try {
|
|
2660
|
+
this._finishRetry({
|
|
2661
|
+
schemaVersion: 1,
|
|
2662
|
+
outcome: "failed",
|
|
2663
|
+
reason: "continuation_rejected",
|
|
2664
|
+
attempt: this._bounded(attempt),
|
|
2665
|
+
cumulativeErrors: this._bounded(this._runRetryCount),
|
|
2666
|
+
}, { type: "auto_retry_end", success: false, attempt, finalError: continuationError.message });
|
|
2667
|
+
}
|
|
2668
|
+
catch (persistenceError) {
|
|
2669
|
+
settlementError = new AggregateError([continuationError, persistenceError], "Retry continuation and outcome persistence both failed.", { cause: continuationError });
|
|
2670
|
+
}
|
|
2671
|
+
this._settleRetryChain(run.chain, settlementError);
|
|
2672
|
+
});
|
|
2039
2673
|
return true;
|
|
2040
2674
|
}
|
|
2675
|
+
_finishCancelledRetry(reason, expected) {
|
|
2676
|
+
if (this._disposed) {
|
|
2677
|
+
this._retry = undefined;
|
|
2678
|
+
throw new Error("AgentSession disposed before retry settlement completed.");
|
|
2679
|
+
}
|
|
2680
|
+
const retry = this._retry;
|
|
2681
|
+
if (expected && retry !== expected)
|
|
2682
|
+
return false;
|
|
2683
|
+
const attempt = retry?.attempt ?? 0;
|
|
2684
|
+
this._finishRetry({
|
|
2685
|
+
schemaVersion: 1,
|
|
2686
|
+
outcome: "cancelled",
|
|
2687
|
+
reason,
|
|
2688
|
+
attempt: this._bounded(attempt),
|
|
2689
|
+
cumulativeErrors: this._bounded(this._runRetryCount),
|
|
2690
|
+
}, { type: "auto_retry_end", success: false, attempt, finalError: "Retry cancelled" });
|
|
2691
|
+
return false;
|
|
2692
|
+
}
|
|
2041
2693
|
/**
|
|
2042
2694
|
* Cancel in-progress retry.
|
|
2043
2695
|
*/
|
|
2044
2696
|
abortRetry() {
|
|
2045
|
-
this.
|
|
2046
|
-
|
|
2047
|
-
this._resolveRetry();
|
|
2048
|
-
}
|
|
2049
|
-
/**
|
|
2050
|
-
* Wait for any in-progress retry to complete.
|
|
2051
|
-
* Returns immediately if no retry is in progress.
|
|
2052
|
-
*/
|
|
2053
|
-
async waitForRetry() {
|
|
2054
|
-
if (!this._retryPromise) {
|
|
2697
|
+
const retry = this._retry;
|
|
2698
|
+
if (!retry)
|
|
2055
2699
|
return;
|
|
2700
|
+
if (retry.phase === "backoff")
|
|
2701
|
+
retry.controller.abort();
|
|
2702
|
+
this.agent.abort();
|
|
2703
|
+
}
|
|
2704
|
+
/** Agent's own `activeRun` guard is false during backoff; the retry phase owns the idle Agent then. */
|
|
2705
|
+
_assertRetryNotOwningIdleAgent() {
|
|
2706
|
+
if (this._retry && !this.isStreaming) {
|
|
2707
|
+
throw new Error("Automatic retry is in progress. Cancel the retry before starting another prompt.");
|
|
2708
|
+
}
|
|
2709
|
+
}
|
|
2710
|
+
async _runAgentPrompt(messages) {
|
|
2711
|
+
this._assertRetryNotOwningIdleAgent();
|
|
2712
|
+
const start = this._reserveAgentRunStart();
|
|
2713
|
+
this._runRetryCount = 0;
|
|
2714
|
+
let promptFailure;
|
|
2715
|
+
try {
|
|
2716
|
+
await this.agent.prompt(messages);
|
|
2056
2717
|
}
|
|
2057
|
-
|
|
2718
|
+
catch (error) {
|
|
2719
|
+
promptFailure = { error };
|
|
2720
|
+
}
|
|
2721
|
+
if (!start.run && this._pendingAgentRunStart === start) {
|
|
2722
|
+
const startError = promptFailure
|
|
2723
|
+
? promptFailure.error instanceof Error
|
|
2724
|
+
? promptFailure.error
|
|
2725
|
+
: new Error(String(promptFailure.error))
|
|
2726
|
+
: new Error("Agent prompt completed before agent_start.");
|
|
2727
|
+
this._rejectAgentRunStart(start, startError);
|
|
2728
|
+
if (promptFailure)
|
|
2729
|
+
throw promptFailure.error;
|
|
2730
|
+
}
|
|
2731
|
+
let settlementFailure;
|
|
2732
|
+
try {
|
|
2733
|
+
const settlement = await start.promise;
|
|
2734
|
+
await this.waitForRetry(settlement);
|
|
2735
|
+
}
|
|
2736
|
+
catch (error) {
|
|
2737
|
+
settlementFailure = { error };
|
|
2738
|
+
}
|
|
2739
|
+
if (promptFailure && settlementFailure) {
|
|
2740
|
+
throw new AggregateError([promptFailure.error, settlementFailure.error], "Agent prompt and retry settlement both failed.", { cause: promptFailure.error });
|
|
2741
|
+
}
|
|
2742
|
+
if (promptFailure)
|
|
2743
|
+
throw promptFailure.error;
|
|
2744
|
+
if (settlementFailure)
|
|
2745
|
+
throw settlementFailure.error;
|
|
2746
|
+
}
|
|
2747
|
+
async waitForRetry(settlement) {
|
|
2748
|
+
if (!settlement)
|
|
2749
|
+
return;
|
|
2750
|
+
await settlement.promise;
|
|
2751
|
+
await settlement.chain.promise;
|
|
2058
2752
|
await this.agent.waitForIdle();
|
|
2059
2753
|
}
|
|
2060
2754
|
/** Whether auto-retry is currently in progress */
|
|
2061
2755
|
get isRetrying() {
|
|
2062
|
-
return this.
|
|
2756
|
+
return this._retry !== undefined;
|
|
2063
2757
|
}
|
|
2064
2758
|
/** Whether auto-retry is enabled */
|
|
2065
2759
|
get autoRetryEnabled() {
|
|
@@ -2279,7 +2973,7 @@ export class AgentSession {
|
|
|
2279
2973
|
if (targetEntry.type === "message" && targetEntry.message.role === "user") {
|
|
2280
2974
|
// User message: leaf = parent (null if root), text goes to editor
|
|
2281
2975
|
newLeafId = targetEntry.parentId;
|
|
2282
|
-
editorText =
|
|
2976
|
+
editorText = restoreScramjetCommandInvocation(targetEntry, this.sessionManager.getEntries());
|
|
2283
2977
|
}
|
|
2284
2978
|
else if (targetEntry.type === "custom_message") {
|
|
2285
2979
|
// Custom message: leaf = parent (null if root), text goes to editor
|
|
@@ -2343,30 +3037,20 @@ export class AgentSession {
|
|
|
2343
3037
|
*/
|
|
2344
3038
|
getUserMessagesForForking() {
|
|
2345
3039
|
const entries = this.sessionManager.getEntries();
|
|
3040
|
+
const commandStarts = indexScramjetCommandStarts(entries);
|
|
2346
3041
|
const result = [];
|
|
2347
3042
|
for (const entry of entries) {
|
|
2348
3043
|
if (entry.type !== "message")
|
|
2349
3044
|
continue;
|
|
2350
3045
|
if (entry.message.role !== "user")
|
|
2351
3046
|
continue;
|
|
2352
|
-
const text =
|
|
3047
|
+
const text = restoreScramjetCommandInvocation(entry, commandStarts);
|
|
2353
3048
|
if (text) {
|
|
2354
3049
|
result.push({ entryId: entry.id, text });
|
|
2355
3050
|
}
|
|
2356
3051
|
}
|
|
2357
3052
|
return result;
|
|
2358
3053
|
}
|
|
2359
|
-
_extractUserMessageText(content) {
|
|
2360
|
-
if (typeof content === "string")
|
|
2361
|
-
return content;
|
|
2362
|
-
if (Array.isArray(content)) {
|
|
2363
|
-
return content
|
|
2364
|
-
.filter((c) => c.type === "text")
|
|
2365
|
-
.map((c) => c.text)
|
|
2366
|
-
.join("");
|
|
2367
|
-
}
|
|
2368
|
-
return "";
|
|
2369
|
-
}
|
|
2370
3054
|
/**
|
|
2371
3055
|
* Get session statistics.
|
|
2372
3056
|
*/
|