@leanandmean/coding-agent 0.74.1-scramjet.7 → 0.74.1-scramjet.70
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 +23 -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 +78 -15
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +1059 -203
- 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 +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 +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 +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 +24 -20
- 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 +901 -176
- 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 +2 -0
- 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 +31 -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 = [];
|
|
@@ -114,10 +165,22 @@ export class AgentSession {
|
|
|
114
165
|
// can apply per-section cache control.
|
|
115
166
|
_baseSystemPromptSections = [];
|
|
116
167
|
_baseSystemPromptOptions;
|
|
168
|
+
// SCRAMJET-DIVERGENCE: active-tool prompt rebuilds preserve the current top-level run's
|
|
169
|
+
// before_agent_start composition while new prompts and reloads establish a fresh composition (#524).
|
|
170
|
+
_runPromptComposition = { kind: "generated-base" };
|
|
117
171
|
constructor(config) {
|
|
118
172
|
this.agent = config.agent;
|
|
119
173
|
this.sessionManager = config.sessionManager;
|
|
120
174
|
this.settingsManager = config.settingsManager;
|
|
175
|
+
this.outputThroughputHistoryPath = config.outputThroughputHistoryPath;
|
|
176
|
+
this._outputThroughputHistory = new OutputThroughputHistoryStore(this.outputThroughputHistoryPath, (diagnostic) => {
|
|
177
|
+
this._extensionRunner.emitError({
|
|
178
|
+
extensionPath: "output-throughput-history",
|
|
179
|
+
event: diagnostic.operation,
|
|
180
|
+
error: `${diagnostic.message} (${diagnostic.path})`,
|
|
181
|
+
stack: diagnostic.cause instanceof Error ? diagnostic.cause.stack : undefined,
|
|
182
|
+
});
|
|
183
|
+
});
|
|
121
184
|
this._scopedModels = config.scopedModels ?? [];
|
|
122
185
|
this._resourceLoader = config.resourceLoader;
|
|
123
186
|
this._customTools = config.customTools ?? [];
|
|
@@ -161,8 +224,8 @@ export class AgentSession {
|
|
|
161
224
|
throw new Error(formatNoApiKeyFoundMessage(model.provider));
|
|
162
225
|
}
|
|
163
226
|
// SCRAMJET-DIVERGENCE: _drainAgentEventQueue ensures all queued async extension event
|
|
164
|
-
// processing has settled before proceeding. Used by beforeToolBatch (pre-extraction drain)
|
|
165
|
-
//
|
|
227
|
+
// processing has settled before proceeding. Used by beforeToolBatch (pre-extraction drain),
|
|
228
|
+
// beforeToolCall (per-tool drain), and the awaited turn_end listener barrier.
|
|
166
229
|
async _drainAgentEventQueue() {
|
|
167
230
|
await this._agentEventQueue;
|
|
168
231
|
}
|
|
@@ -241,46 +304,200 @@ export class AgentSession {
|
|
|
241
304
|
followUp: [...this._followUpMessages],
|
|
242
305
|
});
|
|
243
306
|
}
|
|
244
|
-
// Track last assistant message for auto-compaction check
|
|
245
|
-
_lastAssistantMessage = undefined;
|
|
246
307
|
/** Internal handler for agent events - shared by subscribe and reconnect */
|
|
247
308
|
_handleAgentEvent = (event) => {
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
this.
|
|
254
|
-
|
|
309
|
+
if (this._disposed)
|
|
310
|
+
return;
|
|
311
|
+
this._captureOutputThroughput(event);
|
|
312
|
+
const toolCost = this._captureToolCost(event);
|
|
313
|
+
const runSettlement = this._captureAgentRunSettlement(event);
|
|
314
|
+
const harnessAck = this._harnessPersistenceAckForEvent(event);
|
|
315
|
+
const processing = this._agentEventQueue.then(() => this._processAgentEventTracked(event, runSettlement, harnessAck, toolCost), () => this._processAgentEventTracked(event, runSettlement, harnessAck, toolCost));
|
|
316
|
+
this._agentEventQueue = processing;
|
|
317
|
+
if (harnessAck)
|
|
318
|
+
harnessAck.persistenceTail = processing;
|
|
255
319
|
// Keep queue alive if an event handler fails
|
|
256
|
-
|
|
320
|
+
processing.catch(() => { });
|
|
321
|
+
// SCRAMJET-DIVERGENCE: turn_end tool/prompt changes must settle before Agent snapshots
|
|
322
|
+
// live state for the next provider request (#524).
|
|
323
|
+
if (event.type === "turn_end")
|
|
324
|
+
return this._agentEventQueue;
|
|
257
325
|
};
|
|
258
|
-
|
|
259
|
-
|
|
326
|
+
// SCRAMJET-DIVERGENCE: scalar reports are captured before mutable events enter async hooks (#598).
|
|
327
|
+
_captureToolCost(event) {
|
|
328
|
+
if (event.type === "tool_execution_start") {
|
|
329
|
+
const execution = {};
|
|
330
|
+
this._toolCostExecutions.set(event.toolCallId, execution);
|
|
331
|
+
this._pendingToolCosts.add(execution);
|
|
260
332
|
return;
|
|
261
333
|
}
|
|
262
|
-
|
|
263
|
-
|
|
334
|
+
let toolCallId;
|
|
335
|
+
let reported;
|
|
336
|
+
const final = event.type === "message_end" && event.message.role === "toolResult";
|
|
337
|
+
if (event.type === "tool_execution_update" || event.type === "tool_execution_end") {
|
|
338
|
+
toolCallId = event.toolCallId;
|
|
339
|
+
reported = event.type === "tool_execution_update" ? event.partialResult?.cost : event.result?.cost;
|
|
340
|
+
}
|
|
341
|
+
else if (event.type === "message_end" && event.message.role === "toolResult") {
|
|
342
|
+
toolCallId = event.message.toolCallId;
|
|
343
|
+
reported = event.message.cost;
|
|
344
|
+
}
|
|
345
|
+
else {
|
|
264
346
|
return;
|
|
265
347
|
}
|
|
266
|
-
|
|
267
|
-
if (!
|
|
348
|
+
let execution = this._toolCostExecutions.get(toolCallId);
|
|
349
|
+
if (!execution && validRecordedCost(reported)) {
|
|
350
|
+
execution = {};
|
|
351
|
+
this._toolCostExecutions.set(toolCallId, execution);
|
|
352
|
+
this._pendingToolCosts.add(execution);
|
|
353
|
+
}
|
|
354
|
+
if (execution && validRecordedCost(reported))
|
|
355
|
+
execution.cost = reported;
|
|
356
|
+
if (final) {
|
|
357
|
+
this._toolCostExecutions.delete(toolCallId);
|
|
358
|
+
return execution;
|
|
359
|
+
}
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
// SCRAMJET-DIVERGENCE: Capture provider events synchronously and reset stale throughput lifecycles (#476).
|
|
363
|
+
_captureOutputThroughput(event) {
|
|
364
|
+
if (event.type === "message_start" && event.message.role === "assistant" && event.message.origin === "provider") {
|
|
365
|
+
this._outputThroughputGeneration = this._outputThroughputTracker.start({
|
|
366
|
+
provider: event.message.provider,
|
|
367
|
+
model: event.message.model,
|
|
368
|
+
});
|
|
268
369
|
return;
|
|
269
370
|
}
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
371
|
+
const generation = this._outputThroughputGeneration;
|
|
372
|
+
if (generation === undefined)
|
|
373
|
+
return;
|
|
374
|
+
if (event.type === "message_update" && event.message.role === "assistant") {
|
|
375
|
+
this._outputThroughputTracker.observe(generation, event.assistantMessageEvent);
|
|
376
|
+
}
|
|
377
|
+
else if (event.type === "message_end" && event.message.role === "assistant") {
|
|
378
|
+
const sample = this._outputThroughputTracker.finalize(generation, event.message);
|
|
379
|
+
this._outputThroughputGeneration = undefined;
|
|
380
|
+
if (sample) {
|
|
381
|
+
this._latestOutputThroughputSample = sample;
|
|
382
|
+
this._outputThroughputHistory.submit(sample);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
273
385
|
}
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
386
|
+
_resetOutputThroughput() {
|
|
387
|
+
this._outputThroughputTracker.reset();
|
|
388
|
+
this._outputThroughputGeneration = undefined;
|
|
389
|
+
}
|
|
390
|
+
// SCRAMJET-DIVERGENCE: every Agent run owns persistence state and a one-shot settlement token (#553).
|
|
391
|
+
_captureAgentRunSettlement(event) {
|
|
392
|
+
if (event.type === "agent_start") {
|
|
393
|
+
// A retry continuation's run adopts the chain its originating prompt awaits; the chain stays
|
|
394
|
+
// bound to the retry phase until this start consumes it, however late Agent emits it.
|
|
395
|
+
const chain = this._retry?.phase === "continuing" ? this._retry.chain : createDeferred();
|
|
396
|
+
this._unsettledRetryChains.add(chain);
|
|
397
|
+
const run = { ...createDeferred(), chain };
|
|
398
|
+
this._activeAgentRunSettlement = run;
|
|
399
|
+
this._unsettledAgentRuns.add(run);
|
|
400
|
+
const reservation = this._pendingAgentRunStart;
|
|
401
|
+
if (reservation) {
|
|
402
|
+
this._pendingAgentRunStart = undefined;
|
|
403
|
+
reservation.run = run;
|
|
404
|
+
resolveDeferred(reservation, run);
|
|
405
|
+
}
|
|
406
|
+
return run;
|
|
407
|
+
}
|
|
408
|
+
const run = this._activeAgentRunSettlement;
|
|
409
|
+
if (event.type === "agent_end" && this._activeAgentRunSettlement === run) {
|
|
410
|
+
this._activeAgentRunSettlement = undefined;
|
|
411
|
+
}
|
|
412
|
+
return run;
|
|
413
|
+
}
|
|
414
|
+
_settleAgentRun(run, error) {
|
|
415
|
+
if (settleDeferred(run, error))
|
|
416
|
+
this._unsettledAgentRuns.delete(run);
|
|
417
|
+
}
|
|
418
|
+
_settleRetryChain(chain, error) {
|
|
419
|
+
if (settleDeferred(chain, error))
|
|
420
|
+
this._unsettledRetryChains.delete(chain);
|
|
421
|
+
}
|
|
422
|
+
_completeAgentRun(run, error) {
|
|
423
|
+
this._settleAgentRun(run, error);
|
|
424
|
+
this._settleRetryChain(run.chain, error);
|
|
425
|
+
}
|
|
426
|
+
_reserveAgentRunStart() {
|
|
427
|
+
if (this._pendingAgentRunStart)
|
|
428
|
+
throw new Error("An Agent prompt is already waiting to start.");
|
|
429
|
+
const reservation = createDeferred();
|
|
430
|
+
this._pendingAgentRunStart = reservation;
|
|
431
|
+
return reservation;
|
|
432
|
+
}
|
|
433
|
+
_rejectAgentRunStart(reservation, error) {
|
|
434
|
+
if (this._pendingAgentRunStart === reservation)
|
|
435
|
+
this._pendingAgentRunStart = undefined;
|
|
436
|
+
rejectDeferred(reservation, error);
|
|
437
|
+
}
|
|
438
|
+
// SCRAMJET-DIVERGENCE: harness-tool persisted settlement (#341). Synchronous ownership keeps queued
|
|
439
|
+
// invocation events attributed after early failures; public settlement awaits execution and the final persistence tail.
|
|
440
|
+
async _processAgentEventTracked(event, run, harnessAck, toolCost) {
|
|
441
|
+
try {
|
|
442
|
+
await this._processAgentEvent(event, run, harnessAck !== undefined, toolCost);
|
|
443
|
+
}
|
|
444
|
+
catch (err) {
|
|
445
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
446
|
+
if (this._manualCompactionDrain)
|
|
447
|
+
this._manualCompactionDrain.firstError ??= error;
|
|
448
|
+
if (harnessAck) {
|
|
449
|
+
harnessAck.firstPersistenceError ??= error;
|
|
450
|
+
}
|
|
451
|
+
else if (run && !this._manualCompactionDrain) {
|
|
452
|
+
if (event.type === "message_end" && event.message.role === "assistant") {
|
|
453
|
+
run.assistantPersistenceError = error;
|
|
454
|
+
}
|
|
455
|
+
this._releaseRetryForFailedRun(error);
|
|
456
|
+
this._completeAgentRun(run, error);
|
|
457
|
+
}
|
|
458
|
+
throw err;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
_harnessPersistenceAckForEvent(event) {
|
|
462
|
+
let toolCallId;
|
|
463
|
+
switch (event.type) {
|
|
464
|
+
case "tool_execution_start":
|
|
465
|
+
case "tool_execution_update":
|
|
466
|
+
case "tool_execution_end":
|
|
467
|
+
toolCallId = event.toolCallId;
|
|
468
|
+
break;
|
|
469
|
+
case "message_start":
|
|
470
|
+
case "message_end": {
|
|
471
|
+
const message = event.message;
|
|
472
|
+
if (message.role === "toolResult") {
|
|
473
|
+
toolCallId = message.toolCallId;
|
|
474
|
+
}
|
|
475
|
+
else if (message.role === "assistant") {
|
|
476
|
+
for (const block of message.content) {
|
|
477
|
+
if (block.type === "toolCall" && this._harnessPersistenceAcks.has(block.id)) {
|
|
478
|
+
toolCallId = block.id;
|
|
479
|
+
break;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
break;
|
|
279
484
|
}
|
|
280
485
|
}
|
|
281
|
-
return undefined;
|
|
486
|
+
return toolCallId === undefined ? undefined : this._harnessPersistenceAcks.get(toolCallId);
|
|
282
487
|
}
|
|
283
|
-
async _processAgentEvent(event) {
|
|
488
|
+
async _processAgentEvent(event, run, fromHarnessInvocation = false, toolCost) {
|
|
489
|
+
if (this._disposed)
|
|
490
|
+
return;
|
|
491
|
+
// SCRAMJET-DIVERGENCE: queued lifecycle events must respect manual compaction ownership (#598).
|
|
492
|
+
if (this._manualCompactionDrain && event.type !== "message_end" && event.type !== "agent_end")
|
|
493
|
+
return;
|
|
494
|
+
if (this._manualCompactionDrain && event.type === "agent_end") {
|
|
495
|
+
if (this._retry)
|
|
496
|
+
this._finishCancelledRetry("cancelled_during_continuation");
|
|
497
|
+
if (run)
|
|
498
|
+
this._completeAgentRun(run, this._manualCompactionDrain.firstError);
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
284
501
|
// When a user message starts, check if it's from either queue and remove it BEFORE emitting
|
|
285
502
|
// This ensures the UI sees the updated queue state
|
|
286
503
|
if (event.type === "message_start" && event.message.role === "user") {
|
|
@@ -305,8 +522,22 @@ export class AgentSession {
|
|
|
305
522
|
}
|
|
306
523
|
// Emit to extensions first
|
|
307
524
|
await this._emitExtensionEvent(event);
|
|
525
|
+
if (event.type === "message_end" && event.message.role === "toolResult" && toolCost?.cost !== undefined) {
|
|
526
|
+
event.message.cost = toolCost.cost;
|
|
527
|
+
}
|
|
528
|
+
// An awaited hook may have acquired manual ownership while this event was in flight.
|
|
529
|
+
if (this._manualCompactionDrain && event.type !== "message_end") {
|
|
530
|
+
if (event.type === "agent_end" && run) {
|
|
531
|
+
if (this._retry)
|
|
532
|
+
this._finishCancelledRetry("cancelled_during_continuation");
|
|
533
|
+
this._completeAgentRun(run, this._manualCompactionDrain.firstError);
|
|
534
|
+
}
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
308
537
|
// Notify all listeners
|
|
309
538
|
this._emit(event);
|
|
539
|
+
if (this._disposed)
|
|
540
|
+
return;
|
|
310
541
|
// Handle session persistence
|
|
311
542
|
if (event.type === "message_end") {
|
|
312
543
|
// Check if this is a custom message from extensions
|
|
@@ -317,51 +548,132 @@ export class AgentSession {
|
|
|
317
548
|
else if (event.message.role === "user" ||
|
|
318
549
|
event.message.role === "assistant" ||
|
|
319
550
|
event.message.role === "toolResult") {
|
|
320
|
-
//
|
|
321
|
-
|
|
551
|
+
// SCRAMJET-DIVERGENCE: retry settlement classifies the exact JSON-safe assistant snapshot
|
|
552
|
+
// that crossed the persistence boundary, never a later-mutated live Agent message (#553).
|
|
553
|
+
const persistedMessage = event.message.role === "assistant"
|
|
554
|
+
? JSON.parse(JSON.stringify(event.message))
|
|
555
|
+
: event.message;
|
|
556
|
+
const messageEntryId = this.sessionManager.appendMessage(persistedMessage);
|
|
557
|
+
if (toolCost)
|
|
558
|
+
this._pendingToolCosts.delete(toolCost);
|
|
559
|
+
if (persistedMessage.role === "assistant" && !fromHarnessInvocation && run) {
|
|
560
|
+
run.persistedAssistantSnapshot = persistedMessage;
|
|
561
|
+
}
|
|
562
|
+
if (event.message.role === "user") {
|
|
563
|
+
const sessionEntries = this._inputSessionEntries.get(event.message);
|
|
564
|
+
this._inputSessionEntries.delete(event.message);
|
|
565
|
+
// This try/catch observes only failures thrown synchronously by
|
|
566
|
+
// appendCustomEntry. On the buffered first-turn path (before any
|
|
567
|
+
// assistant message) _persist stores the entry in memory with no I/O,
|
|
568
|
+
// so only metadata *serialization* failures surface here; the deferred
|
|
569
|
+
// disk write happens later in the unguarded appendMessage(assistant)
|
|
570
|
+
// flush, whose throw is swallowed by the _agentEventQueue.catch on the
|
|
571
|
+
// event-handler chain and degrades exact restoration silently.
|
|
572
|
+
for (const entry of sessionEntries ?? []) {
|
|
573
|
+
try {
|
|
574
|
+
this.sessionManager.appendCustomEntry(entry.customType, entry.data, messageEntryId);
|
|
575
|
+
}
|
|
576
|
+
catch (err) {
|
|
577
|
+
this._extensionRunner.emitError({
|
|
578
|
+
extensionPath: `session-entry:${entry.customType}`,
|
|
579
|
+
event: "session_entry_persistence",
|
|
580
|
+
error: `Failed to persist attached input metadata after its user message; exact input restoration may be unavailable: ${err instanceof Error ? err.message : String(err)}`,
|
|
581
|
+
stack: err instanceof Error ? err.stack : undefined,
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
}
|
|
322
586
|
}
|
|
323
587
|
// 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.
|
|
588
|
+
if (event.message.role === "assistant" &&
|
|
589
|
+
event.message.stopReason !== "error" &&
|
|
590
|
+
event.message.stopReason !== "aborted" &&
|
|
591
|
+
!fromHarnessInvocation &&
|
|
592
|
+
!this._manualCompactionDrain) {
|
|
593
|
+
this._overflowRecoveryAttempted = false;
|
|
594
|
+
if (this._retry)
|
|
595
|
+
this._retry = { ...this._retry, attempt: 0 };
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
if (event.type === "agent_end") {
|
|
599
|
+
if (this._disposed || !run)
|
|
600
|
+
return;
|
|
601
|
+
if (this._manualCompactionDrain) {
|
|
602
|
+
if (this._retry)
|
|
603
|
+
this._finishCancelledRetry("cancelled_during_continuation");
|
|
604
|
+
this._completeAgentRun(run, this._manualCompactionDrain.firstError);
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
if (run.assistantPersistenceError) {
|
|
608
|
+
const error = run.assistantPersistenceError;
|
|
609
|
+
run.assistantPersistenceError = undefined;
|
|
610
|
+
run.persistedAssistantSnapshot = undefined;
|
|
611
|
+
this._completeAgentRun(run, error);
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
const msg = run.persistedAssistantSnapshot;
|
|
615
|
+
run.persistedAssistantSnapshot = undefined;
|
|
616
|
+
if (!msg) {
|
|
617
|
+
this._releaseRetryForFailedRun(new Error("Retry continuation ended without an assistant message."));
|
|
618
|
+
this._completeAgentRun(run);
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
if (msg.stopReason === "aborted" && this._retry) {
|
|
622
|
+
this._finishCancelledRetry("cancelled_during_continuation");
|
|
623
|
+
this._completeAgentRun(run);
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
if (msg.stopReason !== "error") {
|
|
627
|
+
if (this._retry) {
|
|
628
|
+
const attempt = this._runRetryCount;
|
|
629
|
+
this._finishRetry({
|
|
630
|
+
schemaVersion: 1,
|
|
631
|
+
outcome: "succeeded",
|
|
632
|
+
attemptsCompleted: this._bounded(attempt),
|
|
633
|
+
cumulativeErrors: this._bounded(attempt),
|
|
634
|
+
}, { type: "auto_retry_end", success: true, attempt });
|
|
340
635
|
}
|
|
636
|
+
this._completeAgentRun(run);
|
|
637
|
+
await this._checkCompaction(msg);
|
|
638
|
+
return;
|
|
341
639
|
}
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
640
|
+
const classification = this._classifyRetry(msg);
|
|
641
|
+
if (classification.kind === "context_overflow") {
|
|
642
|
+
this._finishRetryWithoutSuccess({
|
|
643
|
+
schemaVersion: 1,
|
|
644
|
+
outcome: "not_attempted",
|
|
645
|
+
reason: "context_overflow_compaction",
|
|
646
|
+
evidence: "context_overflow",
|
|
647
|
+
}, msg);
|
|
648
|
+
this._completeAgentRun(run);
|
|
649
|
+
await this._checkCompaction(msg);
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
const settings = this.settingsManager.getRetrySettings();
|
|
653
|
+
if (!settings.enabled) {
|
|
654
|
+
this._finishRetryWithoutSuccess({
|
|
655
|
+
schemaVersion: 1,
|
|
656
|
+
outcome: "not_attempted",
|
|
657
|
+
reason: "retry_disabled",
|
|
658
|
+
evidence: classification.evidence,
|
|
659
|
+
}, msg);
|
|
660
|
+
this._completeAgentRun(run);
|
|
661
|
+
await this._checkCompaction(msg);
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
if (classification.kind === "retry") {
|
|
665
|
+
const didRetry = await this._handleRetryableError(msg, classification.evidence, run);
|
|
350
666
|
if (didRetry)
|
|
351
|
-
return;
|
|
667
|
+
return;
|
|
352
668
|
}
|
|
353
|
-
|
|
669
|
+
else {
|
|
670
|
+
const { kind: _kind, ...cause } = classification;
|
|
671
|
+
this._finishRetryWithoutSuccess({ schemaVersion: 1, outcome: "not_attempted", ...cause }, msg);
|
|
672
|
+
}
|
|
673
|
+
this._completeAgentRun(run);
|
|
354
674
|
await this._checkCompaction(msg);
|
|
355
675
|
}
|
|
356
676
|
}
|
|
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
677
|
/** Extract text content from a message */
|
|
366
678
|
_getUserMessageText(message) {
|
|
367
679
|
if (message.role !== "user")
|
|
@@ -510,8 +822,8 @@ export class AgentSession {
|
|
|
510
822
|
* Preserves all existing listeners.
|
|
511
823
|
*/
|
|
512
824
|
_reconnectToAgent() {
|
|
513
|
-
if (this._unsubscribeAgent)
|
|
514
|
-
return; // Already connected
|
|
825
|
+
if (this._disposed || this._unsubscribeAgent)
|
|
826
|
+
return; // Already connected or disposed
|
|
515
827
|
this._unsubscribeAgent = this.agent.subscribe(this._handleAgentEvent);
|
|
516
828
|
}
|
|
517
829
|
/**
|
|
@@ -519,7 +831,69 @@ export class AgentSession {
|
|
|
519
831
|
* Call this when completely done with the session.
|
|
520
832
|
*/
|
|
521
833
|
dispose() {
|
|
834
|
+
// SCRAMJET-DIVERGENCE: idempotent disposal revokes harness invocations and closes retry settlement (#341, #553).
|
|
835
|
+
// A second call must not re-reject already-settled acknowledgements or re-run teardown.
|
|
836
|
+
if (this._disposed)
|
|
837
|
+
return;
|
|
838
|
+
this._disposed = true;
|
|
839
|
+
this._toolCostExecutions.clear();
|
|
840
|
+
this._pendingToolCosts.clear();
|
|
841
|
+
this._resetOutputThroughput();
|
|
842
|
+
const retryDisposeError = new Error("AgentSession disposed before retry settlement completed.");
|
|
843
|
+
const unsettledRuns = [...this._unsettledAgentRuns];
|
|
844
|
+
const retry = this._retry;
|
|
845
|
+
const unclassifiedError = unsettledRuns.find((run) => run.persistedAssistantSnapshot?.stopReason === "error")?.persistedAssistantSnapshot;
|
|
846
|
+
if (retry)
|
|
847
|
+
this.agent.abort();
|
|
848
|
+
try {
|
|
849
|
+
if (retry) {
|
|
850
|
+
this._appendAutoRetryRecord({
|
|
851
|
+
schemaVersion: 1,
|
|
852
|
+
outcome: "cancelled",
|
|
853
|
+
reason: "session_disposed",
|
|
854
|
+
attempt: this._bounded(retry.attempt),
|
|
855
|
+
cumulativeErrors: this._bounded(this._runRetryCount),
|
|
856
|
+
});
|
|
857
|
+
}
|
|
858
|
+
else if (unclassifiedError) {
|
|
859
|
+
const classification = this._classifyRetry(unclassifiedError);
|
|
860
|
+
this._appendAutoRetryRecord({
|
|
861
|
+
schemaVersion: 1,
|
|
862
|
+
outcome: "not_attempted",
|
|
863
|
+
reason: "session_disposed",
|
|
864
|
+
evidence: classification.kind === "context_overflow" ? "context_overflow" : classification.evidence,
|
|
865
|
+
});
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
catch (error) {
|
|
869
|
+
this._reportRetryFailure(error);
|
|
870
|
+
}
|
|
871
|
+
if (retry?.phase === "backoff")
|
|
872
|
+
retry.controller.abort();
|
|
873
|
+
this._retry = undefined;
|
|
874
|
+
this._activeAgentRunSettlement = undefined;
|
|
875
|
+
const pendingAgentRunStart = this._pendingAgentRunStart;
|
|
876
|
+
if (pendingAgentRunStart)
|
|
877
|
+
this._rejectAgentRunStart(pendingAgentRunStart, retryDisposeError);
|
|
878
|
+
const pendingRetryContinuation = retry?.phase === "continuing" ? retry.chain : undefined;
|
|
522
879
|
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().");
|
|
880
|
+
// Reject pending persistence acknowledgements before revoking Agent-side invocations, so a caller
|
|
881
|
+
// awaiting invokeHarnessTool sees rejection rather than a promise that never settles. Already-
|
|
882
|
+
// running tool work may finish afterward; teardown revokes the old-session guarantee but cannot
|
|
883
|
+
// roll back arbitrary side effects.
|
|
884
|
+
const disposeError = new Error("AgentSession disposed before the harness tool invocation settled.");
|
|
885
|
+
for (const ack of this._harnessPersistenceAcks.values()) {
|
|
886
|
+
this._settleHarnessPersistenceAck(ack, disposeError);
|
|
887
|
+
}
|
|
888
|
+
this._harnessPersistenceAcks.clear();
|
|
889
|
+
this.agent.rejectUnsettledHarnessTools(disposeError);
|
|
890
|
+
const retryChains = new Set(this._unsettledRetryChains);
|
|
891
|
+
if (pendingRetryContinuation)
|
|
892
|
+
retryChains.add(pendingRetryContinuation);
|
|
893
|
+
for (const run of unsettledRuns)
|
|
894
|
+
this._settleAgentRun(run, retryDisposeError);
|
|
895
|
+
for (const chain of retryChains)
|
|
896
|
+
this._settleRetryChain(chain, retryDisposeError);
|
|
523
897
|
this._disconnectFromAgent();
|
|
524
898
|
this._eventListeners = [];
|
|
525
899
|
cleanupSessionResources(this.sessionId);
|
|
@@ -547,9 +921,51 @@ export class AgentSession {
|
|
|
547
921
|
get systemPrompt() {
|
|
548
922
|
return flattenSystemPrompt(this.agent.state.systemPrompt);
|
|
549
923
|
}
|
|
924
|
+
get liveOutputRate() {
|
|
925
|
+
const model = this.state.model;
|
|
926
|
+
const identity = this._outputThroughputTracker.activeIdentity;
|
|
927
|
+
if (!model || identity?.provider !== model.provider || identity.model !== model.id)
|
|
928
|
+
return undefined;
|
|
929
|
+
return this._outputThroughputTracker.liveRate();
|
|
930
|
+
}
|
|
931
|
+
get outputThroughputActive() {
|
|
932
|
+
return this._outputThroughputTracker.outputActive;
|
|
933
|
+
}
|
|
934
|
+
get outputThroughputGeneration() {
|
|
935
|
+
return this._outputThroughputTracker.generation;
|
|
936
|
+
}
|
|
937
|
+
get medianOutputRate() {
|
|
938
|
+
const model = this.state.model;
|
|
939
|
+
if (!model)
|
|
940
|
+
return undefined;
|
|
941
|
+
const history = this._outputThroughputHistory.snapshot();
|
|
942
|
+
const local = this._latestOutputThroughputSample;
|
|
943
|
+
if (local?.provider === model.provider &&
|
|
944
|
+
local.model === model.id &&
|
|
945
|
+
!history
|
|
946
|
+
.samples(model.provider, model.id)
|
|
947
|
+
.some((sample) => sample.provider === local.provider &&
|
|
948
|
+
sample.model === local.model &&
|
|
949
|
+
sample.responseModel === local.responseModel &&
|
|
950
|
+
sample.outputTokens === local.outputTokens &&
|
|
951
|
+
sample.durationMs === local.durationMs &&
|
|
952
|
+
sample.observedAt === local.observedAt)) {
|
|
953
|
+
history.add(local);
|
|
954
|
+
}
|
|
955
|
+
return history.median(model.provider, model.id);
|
|
956
|
+
}
|
|
957
|
+
get outputThroughputHistory() {
|
|
958
|
+
return this._outputThroughputHistory.snapshot();
|
|
959
|
+
}
|
|
960
|
+
refreshOutputThroughputHistory() {
|
|
961
|
+
return this._outputThroughputHistory.refresh();
|
|
962
|
+
}
|
|
963
|
+
flushOutputThroughputHistory() {
|
|
964
|
+
return this._outputThroughputHistory.flush();
|
|
965
|
+
}
|
|
550
966
|
/** Current retry attempt (0 if not retrying) */
|
|
551
967
|
get retryAttempt() {
|
|
552
|
-
return this.
|
|
968
|
+
return this._retry?.attempt ?? 0;
|
|
553
969
|
}
|
|
554
970
|
/**
|
|
555
971
|
* Get the names of currently active tools.
|
|
@@ -600,7 +1016,7 @@ export class AgentSession {
|
|
|
600
1016
|
// Rebuild base system prompt with new tool set
|
|
601
1017
|
this._rebuildSystemPrompt(validToolNames);
|
|
602
1018
|
}
|
|
603
|
-
// SCRAMJET-DIVERGENCE: harness-tool invocation (#244).
|
|
1019
|
+
// SCRAMJET-DIVERGENCE: harness-tool invocation (#244), persisted-settlement boundary (#341).
|
|
604
1020
|
/**
|
|
605
1021
|
* Execute a registered tool as a harness-originated call (not requested by the model).
|
|
606
1022
|
*
|
|
@@ -612,13 +1028,97 @@ export class AgentSession {
|
|
|
612
1028
|
* run/turn framing. The idle-vs-mid-run branch lives in `runHarnessTool` (keyed on the agent's
|
|
613
1029
|
* active run, which tracks `isStreaming`): idle calls execute immediately, mid-run calls queue
|
|
614
1030
|
* and drain before the next intra-run LLM call.
|
|
1031
|
+
*
|
|
1032
|
+
* **Persisted settlement (#341).** This is a strictly stronger boundary than
|
|
1033
|
+
* {@link Agent.runHarnessTool}: the returned promise resolves only after (1) the Agent-core
|
|
1034
|
+
* pipeline fulfills *and* (2) `_processAgentEvent` has persisted the matching tool-result
|
|
1035
|
+
* `message_end` via `SessionManager.appendMessage`. Because the event queue preserves order,
|
|
1036
|
+
* that also proves the preceding synthetic assistant tool-call message was persisted first. It
|
|
1037
|
+
* rejects on Agent-core failure, on failure to process/persist a matching event, on disposal
|
|
1038
|
+
* before settlement, on a post-disposal call, on an unknown tool, or on a duplicate explicit
|
|
1039
|
+
* tool-call id that is already pending. **Rejection means the promised pipeline did not complete —
|
|
1040
|
+
* it does not prove artifact absence** (Agent state and persistence may have partially or fully
|
|
1041
|
+
* completed before a late failure), so callers must not blindly retry. A tool's own `execute()`
|
|
1042
|
+
* error is *not* a rejection: it becomes a normal `isError: true` result and the promise resolves
|
|
1043
|
+
* after that error-result pipeline is persisted.
|
|
1044
|
+
*
|
|
1045
|
+
* **Reentrancy.** As with `runHarnessTool`, do not `await` this from work whose return is required
|
|
1046
|
+
* for the Agent to reach the drain point or for the event queue to persist the matching result.
|
|
615
1047
|
*/
|
|
616
1048
|
async invokeHarnessTool(name, args, options) {
|
|
1049
|
+
if (this._disposed) {
|
|
1050
|
+
throw new Error(`Cannot invoke harness tool "${name}": the session has been disposed.`);
|
|
1051
|
+
}
|
|
1052
|
+
// SCRAMJET-DIVERGENCE: reject new session work before allocating invocation ownership (#598).
|
|
1053
|
+
if (this._compactionAbortController) {
|
|
1054
|
+
throw new Error(`Cannot invoke harness tool "${name}": manual compaction is in progress. Retry after it settles.`);
|
|
1055
|
+
}
|
|
617
1056
|
const tool = this._toolRegistry.get(name);
|
|
618
1057
|
if (!tool) {
|
|
619
1058
|
throw new Error(`Cannot invoke harness tool "${name}": no tool with that name is registered.`);
|
|
620
1059
|
}
|
|
621
|
-
|
|
1060
|
+
const toolCallId = this._allocateHarnessToolCallId(options?.toolCallId);
|
|
1061
|
+
// Register before Agent execution can emit events. The coordinator retains attribution until Agent
|
|
1062
|
+
// execution and the invocation's final queued persistence task both settle.
|
|
1063
|
+
const ack = this._registerHarnessPersistenceAck(toolCallId);
|
|
1064
|
+
void this._completeHarnessToolInvocation(this.agent.runHarnessTool(tool, args, { toolCallId }), ack);
|
|
1065
|
+
try {
|
|
1066
|
+
await ack.promise;
|
|
1067
|
+
}
|
|
1068
|
+
finally {
|
|
1069
|
+
// Identity-checked cleanup: only drop the entry if it is still this invocation's ack (disposal
|
|
1070
|
+
// may have cleared the map, and a later same-id invocation must not have its entry removed).
|
|
1071
|
+
if (this._harnessPersistenceAcks.get(toolCallId) === ack) {
|
|
1072
|
+
this._harnessPersistenceAcks.delete(toolCallId);
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
// SCRAMJET-DIVERGENCE: harness-tool persisted-settlement (#341). Allocate the tool-call id used as
|
|
1077
|
+
// the acknowledgement key: reject a duplicate explicit id that is still pending, retry a generated
|
|
1078
|
+
// id on the (astronomically unlikely) collision with a pending one.
|
|
1079
|
+
_allocateHarnessToolCallId(explicit) {
|
|
1080
|
+
if (explicit !== undefined) {
|
|
1081
|
+
// Validate at the public trust boundary (invokeHarnessTool is on ExtensionAPI): an explicit
|
|
1082
|
+
// id flows straight into the synthetic assistant message and on to the provider, so it must
|
|
1083
|
+
// meet the provider-safe shape generateHarnessToolCallId guarantees (Anthropic's
|
|
1084
|
+
// ^[a-zA-Z0-9_-]+$, bounded length).
|
|
1085
|
+
if (!/^[a-zA-Z0-9_-]{1,64}$/.test(explicit)) {
|
|
1086
|
+
throw new Error(`Cannot invoke harness tool with tool-call id "${explicit}": ids must be 1-64 characters of [a-zA-Z0-9_-].`);
|
|
1087
|
+
}
|
|
1088
|
+
if (this._harnessPersistenceAcks.has(explicit)) {
|
|
1089
|
+
throw new Error(`Cannot invoke harness tool with tool-call id "${explicit}": an invocation with that id is already pending.`);
|
|
1090
|
+
}
|
|
1091
|
+
return explicit;
|
|
1092
|
+
}
|
|
1093
|
+
let id = generateHarnessToolCallId();
|
|
1094
|
+
while (this._harnessPersistenceAcks.has(id)) {
|
|
1095
|
+
id = generateHarnessToolCallId();
|
|
1096
|
+
}
|
|
1097
|
+
return id;
|
|
1098
|
+
}
|
|
1099
|
+
async _completeHarnessToolInvocation(execution, ack) {
|
|
1100
|
+
let executionError;
|
|
1101
|
+
try {
|
|
1102
|
+
await execution;
|
|
1103
|
+
}
|
|
1104
|
+
catch (error) {
|
|
1105
|
+
executionError = error instanceof Error ? error : new Error(String(error));
|
|
1106
|
+
}
|
|
1107
|
+
try {
|
|
1108
|
+
await ack.persistenceTail;
|
|
1109
|
+
}
|
|
1110
|
+
catch { }
|
|
1111
|
+
this._settleHarnessPersistenceAck(ack, ack.firstPersistenceError ?? executionError);
|
|
1112
|
+
}
|
|
1113
|
+
_settleHarnessPersistenceAck(ack, error) {
|
|
1114
|
+
settleDeferred(ack, error);
|
|
1115
|
+
}
|
|
1116
|
+
// SCRAMJET-DIVERGENCE: harness-tool persisted-settlement (#341). Register a deferred acknowledgement
|
|
1117
|
+
// keyed by tool-call id before any invocation event can be emitted.
|
|
1118
|
+
_registerHarnessPersistenceAck(toolCallId) {
|
|
1119
|
+
const ack = { ...createDeferred(), persistenceTail: Promise.resolve() };
|
|
1120
|
+
this._harnessPersistenceAcks.set(toolCallId, ack);
|
|
1121
|
+
return ack;
|
|
622
1122
|
}
|
|
623
1123
|
/** Whether compaction or branch summarization is currently running */
|
|
624
1124
|
get isCompacting() {
|
|
@@ -684,6 +1184,19 @@ export class AgentSession {
|
|
|
684
1184
|
}
|
|
685
1185
|
return Array.from(unique);
|
|
686
1186
|
}
|
|
1187
|
+
_applyRunPromptComposition() {
|
|
1188
|
+
switch (this._runPromptComposition.kind) {
|
|
1189
|
+
case "generated-base":
|
|
1190
|
+
this.agent.state.systemPrompt = this._baseSystemPromptSections.slice();
|
|
1191
|
+
break;
|
|
1192
|
+
case "contributed-sections":
|
|
1193
|
+
this.agent.state.systemPrompt = spliceContributedSections(this._baseSystemPromptSections, this._runPromptComposition.sections);
|
|
1194
|
+
break;
|
|
1195
|
+
case "authoritative-string":
|
|
1196
|
+
this.agent.state.systemPrompt = this._runPromptComposition.systemPrompt;
|
|
1197
|
+
break;
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
687
1200
|
/** Rebuild `_baseSystemPromptSections` from the current resources and tool set and apply it to agent state. */
|
|
688
1201
|
_rebuildSystemPrompt(toolNames) {
|
|
689
1202
|
const validToolNames = toolNames.filter((name) => this._toolRegistry.has(name));
|
|
@@ -704,6 +1217,7 @@ export class AgentSession {
|
|
|
704
1217
|
const appendSystemPrompt = loaderAppendSystemPrompt.length > 0 ? loaderAppendSystemPrompt.join("\n\n") : undefined;
|
|
705
1218
|
const loadedSkills = this._resourceLoader.getSkills().skills;
|
|
706
1219
|
const loadedContextFiles = this._resourceLoader.getAgentsFiles().agentsFiles;
|
|
1220
|
+
// SCRAMJET-DIVERGENCE: pass session file path for volatile environment fact (issue 348)
|
|
707
1221
|
this._baseSystemPromptOptions = {
|
|
708
1222
|
cwd: this._cwd,
|
|
709
1223
|
skills: loadedSkills,
|
|
@@ -713,9 +1227,10 @@ export class AgentSession {
|
|
|
713
1227
|
selectedTools: validToolNames,
|
|
714
1228
|
toolSnippets,
|
|
715
1229
|
promptGuidelines,
|
|
1230
|
+
sessionFile: this.sessionManager.getSessionFile(),
|
|
716
1231
|
};
|
|
717
1232
|
this._baseSystemPromptSections = buildSystemPromptSections(this._baseSystemPromptOptions);
|
|
718
|
-
this.
|
|
1233
|
+
this._applyRunPromptComposition();
|
|
719
1234
|
}
|
|
720
1235
|
// =========================================================================
|
|
721
1236
|
// Prompting
|
|
@@ -733,6 +1248,8 @@ export class AgentSession {
|
|
|
733
1248
|
const expandPromptTemplates = options?.expandPromptTemplates ?? true;
|
|
734
1249
|
const preflightResult = options?.preflightResult;
|
|
735
1250
|
let messages;
|
|
1251
|
+
let inputSessionEntries;
|
|
1252
|
+
let acceptInput;
|
|
736
1253
|
try {
|
|
737
1254
|
// Handle extension commands first (execute immediately, even during streaming)
|
|
738
1255
|
// Extension commands manage their own LLM interaction via pi.sendMessage()
|
|
@@ -744,6 +1261,7 @@ export class AgentSession {
|
|
|
744
1261
|
return;
|
|
745
1262
|
}
|
|
746
1263
|
}
|
|
1264
|
+
this._assertRetryNotOwningIdleAgent();
|
|
747
1265
|
// Emit input event for extension interception (before skill/template expansion)
|
|
748
1266
|
let currentText = text;
|
|
749
1267
|
let currentImages = options?.images;
|
|
@@ -756,6 +1274,8 @@ export class AgentSession {
|
|
|
756
1274
|
if (inputResult.action === "transform") {
|
|
757
1275
|
currentText = inputResult.text;
|
|
758
1276
|
currentImages = inputResult.images ?? currentImages;
|
|
1277
|
+
inputSessionEntries = inputResult.sessionEntries;
|
|
1278
|
+
acceptInput = inputResult.onAccepted;
|
|
759
1279
|
}
|
|
760
1280
|
}
|
|
761
1281
|
// Expand skill commands (/skill:name args) and prompt templates (/template args)
|
|
@@ -771,11 +1291,12 @@ export class AgentSession {
|
|
|
771
1291
|
"deliverAs when calling dispatchUserInput() or sendUserMessage() — to queue the message.");
|
|
772
1292
|
}
|
|
773
1293
|
if (options.streamingBehavior === "followUp") {
|
|
774
|
-
await this._queueFollowUp(expandedText, currentImages);
|
|
1294
|
+
await this._queueFollowUp(expandedText, currentImages, inputSessionEntries);
|
|
775
1295
|
}
|
|
776
1296
|
else {
|
|
777
|
-
await this._queueSteer(expandedText, currentImages);
|
|
1297
|
+
await this._queueSteer(expandedText, currentImages, inputSessionEntries);
|
|
778
1298
|
}
|
|
1299
|
+
acceptInput?.();
|
|
779
1300
|
preflightResult?.(true);
|
|
780
1301
|
return;
|
|
781
1302
|
}
|
|
@@ -797,7 +1318,7 @@ export class AgentSession {
|
|
|
797
1318
|
// Check if we need to compact before sending (catches aborted responses)
|
|
798
1319
|
const lastAssistant = this._findLastAssistantMessage();
|
|
799
1320
|
if (lastAssistant) {
|
|
800
|
-
await this._checkCompaction(lastAssistant,
|
|
1321
|
+
await this._checkCompaction(lastAssistant, "new_prompt");
|
|
801
1322
|
}
|
|
802
1323
|
// Build messages array (custom message if any, then user message)
|
|
803
1324
|
messages = [];
|
|
@@ -806,16 +1327,21 @@ export class AgentSession {
|
|
|
806
1327
|
if (currentImages) {
|
|
807
1328
|
userContent.push(...currentImages);
|
|
808
1329
|
}
|
|
809
|
-
|
|
1330
|
+
const userMessage = {
|
|
810
1331
|
role: "user",
|
|
811
1332
|
content: userContent,
|
|
812
1333
|
timestamp: Date.now(),
|
|
813
|
-
}
|
|
1334
|
+
};
|
|
1335
|
+
messages.push(userMessage);
|
|
1336
|
+
if (inputSessionEntries?.length) {
|
|
1337
|
+
this._inputSessionEntries.set(userMessage, inputSessionEntries);
|
|
1338
|
+
}
|
|
814
1339
|
// Inject any pending "nextTurn" messages as context alongside the user message
|
|
815
1340
|
for (const msg of this._pendingNextTurnMessages) {
|
|
816
1341
|
messages.push(msg);
|
|
817
1342
|
}
|
|
818
1343
|
this._pendingNextTurnMessages = [];
|
|
1344
|
+
this._runPromptComposition = { kind: "generated-base" };
|
|
819
1345
|
// Emit before_agent_start extension event
|
|
820
1346
|
const result = await this._extensionRunner.emitBeforeAgentStart(expandedText, currentImages, this._baseSystemPromptSections, this._baseSystemPromptOptions);
|
|
821
1347
|
// Add all custom messages from extensions
|
|
@@ -835,15 +1361,18 @@ export class AgentSession {
|
|
|
835
1361
|
// replacement is authoritative (even an empty string): it wins for the
|
|
836
1362
|
// whole prompt and any contributed sections are dropped for this turn.
|
|
837
1363
|
if (result?.systemPrompt !== undefined) {
|
|
838
|
-
this.
|
|
1364
|
+
this._runPromptComposition = {
|
|
1365
|
+
kind: "authoritative-string",
|
|
1366
|
+
systemPrompt: result.systemPrompt,
|
|
1367
|
+
};
|
|
839
1368
|
}
|
|
840
1369
|
else if (result?.systemPromptSections) {
|
|
841
|
-
this.
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
this.agent.state.systemPrompt = this._baseSystemPromptSections.slice();
|
|
1370
|
+
this._runPromptComposition = {
|
|
1371
|
+
kind: "contributed-sections",
|
|
1372
|
+
sections: result.systemPromptSections.map((section) => ({ ...section })),
|
|
1373
|
+
};
|
|
846
1374
|
}
|
|
1375
|
+
this._applyRunPromptComposition();
|
|
847
1376
|
}
|
|
848
1377
|
catch (error) {
|
|
849
1378
|
preflightResult?.(false);
|
|
@@ -852,9 +1381,9 @@ export class AgentSession {
|
|
|
852
1381
|
if (!messages) {
|
|
853
1382
|
return;
|
|
854
1383
|
}
|
|
1384
|
+
acceptInput?.();
|
|
855
1385
|
preflightResult?.(true);
|
|
856
|
-
await this.
|
|
857
|
-
await this.waitForRetry();
|
|
1386
|
+
await this._runAgentPrompt(messages);
|
|
858
1387
|
}
|
|
859
1388
|
/**
|
|
860
1389
|
* Try to execute an extension command. Returns true if command was found and executed.
|
|
@@ -951,34 +1480,42 @@ export class AgentSession {
|
|
|
951
1480
|
/**
|
|
952
1481
|
* Internal: Queue a steering message (already expanded, no extension command check).
|
|
953
1482
|
*/
|
|
954
|
-
async _queueSteer(text, images) {
|
|
1483
|
+
async _queueSteer(text, images, sessionEntries) {
|
|
955
1484
|
this._steeringMessages.push(text);
|
|
956
1485
|
this._emitQueueUpdate();
|
|
957
1486
|
const content = [{ type: "text", text }];
|
|
958
1487
|
if (images) {
|
|
959
1488
|
content.push(...images);
|
|
960
1489
|
}
|
|
961
|
-
|
|
1490
|
+
const message = {
|
|
962
1491
|
role: "user",
|
|
963
1492
|
content,
|
|
964
1493
|
timestamp: Date.now(),
|
|
965
|
-
}
|
|
1494
|
+
};
|
|
1495
|
+
if (sessionEntries?.length) {
|
|
1496
|
+
this._inputSessionEntries.set(message, sessionEntries);
|
|
1497
|
+
}
|
|
1498
|
+
this.agent.steer(message);
|
|
966
1499
|
}
|
|
967
1500
|
/**
|
|
968
1501
|
* Internal: Queue a follow-up message (already expanded, no extension command check).
|
|
969
1502
|
*/
|
|
970
|
-
async _queueFollowUp(text, images) {
|
|
1503
|
+
async _queueFollowUp(text, images, sessionEntries) {
|
|
971
1504
|
this._followUpMessages.push(text);
|
|
972
1505
|
this._emitQueueUpdate();
|
|
973
1506
|
const content = [{ type: "text", text }];
|
|
974
1507
|
if (images) {
|
|
975
1508
|
content.push(...images);
|
|
976
1509
|
}
|
|
977
|
-
|
|
1510
|
+
const message = {
|
|
978
1511
|
role: "user",
|
|
979
1512
|
content,
|
|
980
1513
|
timestamp: Date.now(),
|
|
981
|
-
}
|
|
1514
|
+
};
|
|
1515
|
+
if (sessionEntries?.length) {
|
|
1516
|
+
this._inputSessionEntries.set(message, sessionEntries);
|
|
1517
|
+
}
|
|
1518
|
+
this.agent.followUp(message);
|
|
982
1519
|
}
|
|
983
1520
|
/**
|
|
984
1521
|
* Throw an error if the text is an extension command.
|
|
@@ -1024,7 +1561,7 @@ export class AgentSession {
|
|
|
1024
1561
|
}
|
|
1025
1562
|
}
|
|
1026
1563
|
else if (options?.triggerTurn) {
|
|
1027
|
-
await this.
|
|
1564
|
+
await this._runAgentPrompt(appMessage);
|
|
1028
1565
|
}
|
|
1029
1566
|
else {
|
|
1030
1567
|
this.agent.state.messages.push(appMessage);
|
|
@@ -1121,6 +1658,7 @@ export class AgentSession {
|
|
|
1121
1658
|
*/
|
|
1122
1659
|
async abort() {
|
|
1123
1660
|
this.abortRetry();
|
|
1661
|
+
this._resetOutputThroughput();
|
|
1124
1662
|
this.agent.abort();
|
|
1125
1663
|
await this.agent.waitForIdle();
|
|
1126
1664
|
}
|
|
@@ -1308,11 +1846,32 @@ export class AgentSession {
|
|
|
1308
1846
|
* @param customInstructions Optional instructions for the compaction summary
|
|
1309
1847
|
*/
|
|
1310
1848
|
async compact(customInstructions) {
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
this.
|
|
1849
|
+
// SCRAMJET-DIVERGENCE: acquire one owner before awaits and drain terminal persistence (#598).
|
|
1850
|
+
if (this._disposed)
|
|
1851
|
+
throw new Error("Cannot compact a disposed session.");
|
|
1852
|
+
if (this.isCompacting)
|
|
1853
|
+
throw new Error("Compaction or branch summarization is already in progress. Retry after it settles.");
|
|
1854
|
+
if (this.agent.hasActiveTransientHarnessTools) {
|
|
1855
|
+
throw new Error("A harness tool is still executing outside an Agent run. Retry compaction after its persisted settlement.");
|
|
1856
|
+
}
|
|
1857
|
+
const controller = new AbortController();
|
|
1858
|
+
this._compactionAbortController = controller;
|
|
1859
|
+
const drain = {};
|
|
1860
|
+
this._manualCompactionDrain = drain;
|
|
1861
|
+
const runs = [...this._unsettledAgentRuns];
|
|
1315
1862
|
try {
|
|
1863
|
+
this._resetOutputThroughput();
|
|
1864
|
+
this._disconnectFromAgent();
|
|
1865
|
+
this._unsubscribeAgent = this.agent.subscribe((event) => this._handleAgentEvent(event));
|
|
1866
|
+
this._emit({ type: "compaction_start", reason: "manual" });
|
|
1867
|
+
await this.abort();
|
|
1868
|
+
await this._agentEventQueue;
|
|
1869
|
+
if (drain.firstError)
|
|
1870
|
+
throw drain.firstError;
|
|
1871
|
+
if (this._disposed)
|
|
1872
|
+
throw new Error("Session disposed during compaction.");
|
|
1873
|
+
if (controller.signal.aborted)
|
|
1874
|
+
throw new Error("Compaction cancelled");
|
|
1316
1875
|
if (!this.model) {
|
|
1317
1876
|
throw new Error(formatNoModelSelectedMessage());
|
|
1318
1877
|
}
|
|
@@ -1336,7 +1895,7 @@ export class AgentSession {
|
|
|
1336
1895
|
preparation,
|
|
1337
1896
|
branchEntries: pathEntries,
|
|
1338
1897
|
customInstructions,
|
|
1339
|
-
signal:
|
|
1898
|
+
signal: controller.signal,
|
|
1340
1899
|
}));
|
|
1341
1900
|
if (result?.cancel) {
|
|
1342
1901
|
throw new Error("Compaction cancelled");
|
|
@@ -1359,13 +1918,15 @@ export class AgentSession {
|
|
|
1359
1918
|
}
|
|
1360
1919
|
else {
|
|
1361
1920
|
// Generate compaction result
|
|
1362
|
-
const result = await compact(preparation, this.model, apiKey, headers, customInstructions,
|
|
1921
|
+
const result = await compact(preparation, this.model, apiKey, headers, customInstructions, controller.signal, this.thinkingLevel);
|
|
1363
1922
|
summary = result.summary;
|
|
1364
1923
|
firstKeptEntryId = result.firstKeptEntryId;
|
|
1365
1924
|
tokensBefore = result.tokensBefore;
|
|
1366
1925
|
details = result.details;
|
|
1367
1926
|
}
|
|
1368
|
-
if (this.
|
|
1927
|
+
if (this._disposed)
|
|
1928
|
+
throw new Error("Session disposed during compaction.");
|
|
1929
|
+
if (controller.signal.aborted) {
|
|
1369
1930
|
throw new Error("Compaction cancelled");
|
|
1370
1931
|
}
|
|
1371
1932
|
this.sessionManager.appendCompaction(summary, firstKeptEntryId, tokensBefore, details, fromExtension);
|
|
@@ -1397,8 +1958,13 @@ export class AgentSession {
|
|
|
1397
1958
|
return compactionResult;
|
|
1398
1959
|
}
|
|
1399
1960
|
catch (error) {
|
|
1400
|
-
const
|
|
1401
|
-
const
|
|
1961
|
+
const failure = drain.firstError ?? error;
|
|
1962
|
+
const settlementError = failure instanceof Error ? failure : new Error(String(failure));
|
|
1963
|
+
this._releaseRetryForFailedRun(settlementError);
|
|
1964
|
+
for (const run of runs)
|
|
1965
|
+
this._completeAgentRun(run, settlementError);
|
|
1966
|
+
const message = settlementError.message;
|
|
1967
|
+
const aborted = message === "Compaction cancelled" || settlementError.name === "AbortError";
|
|
1402
1968
|
this._emit({
|
|
1403
1969
|
type: "compaction_end",
|
|
1404
1970
|
reason: "manual",
|
|
@@ -1407,9 +1973,11 @@ export class AgentSession {
|
|
|
1407
1973
|
willRetry: false,
|
|
1408
1974
|
errorMessage: aborted ? undefined : `Compaction failed: ${message}`,
|
|
1409
1975
|
});
|
|
1410
|
-
throw
|
|
1976
|
+
throw failure;
|
|
1411
1977
|
}
|
|
1412
1978
|
finally {
|
|
1979
|
+
this._disconnectFromAgent();
|
|
1980
|
+
this._manualCompactionDrain = undefined;
|
|
1413
1981
|
this._compactionAbortController = undefined;
|
|
1414
1982
|
this._reconnectToAgent();
|
|
1415
1983
|
}
|
|
@@ -1436,15 +2004,44 @@ export class AgentSession {
|
|
|
1436
2004
|
* 2. Threshold: Context over threshold, compact, NO auto-retry (user continues manually)
|
|
1437
2005
|
*
|
|
1438
2006
|
* @param assistantMessage The assistant message to check
|
|
1439
|
-
* @param skipAbortedCheck If false, include aborted messages (for pre-prompt check). Default: true
|
|
1440
2007
|
*/
|
|
1441
|
-
async _checkCompaction(assistantMessage,
|
|
2008
|
+
async _checkCompaction(assistantMessage, trigger = "settlement") {
|
|
2009
|
+
if (this._manualCompactionDrain)
|
|
2010
|
+
return;
|
|
1442
2011
|
const settings = this.settingsManager.getCompactionSettings();
|
|
1443
2012
|
if (!settings.enabled)
|
|
1444
2013
|
return;
|
|
1445
|
-
//
|
|
1446
|
-
if (
|
|
2014
|
+
// SCRAMJET-DIVERGENCE: New prompts maintain context independently of failed-turn recovery.
|
|
2015
|
+
if (trigger === "settlement" && blocksFailureRecovery(assistantMessage))
|
|
2016
|
+
return;
|
|
2017
|
+
if (trigger === "new_prompt") {
|
|
2018
|
+
const boundary = getLatestCompactionEntry(this.sessionManager.getBranch());
|
|
2019
|
+
const messages = this.agent.state.messages;
|
|
2020
|
+
const reverseIndex = [...messages]
|
|
2021
|
+
.reverse()
|
|
2022
|
+
.findIndex((message) => message.role === "assistant" &&
|
|
2023
|
+
message.origin !== "harness" &&
|
|
2024
|
+
message.stopReason !== "error" &&
|
|
2025
|
+
message.stopReason !== "aborted" &&
|
|
2026
|
+
message.provider === this.model?.provider &&
|
|
2027
|
+
message.model === this.model?.id &&
|
|
2028
|
+
Number.isFinite(calculateContextTokens(message.usage)) &&
|
|
2029
|
+
calculateContextTokens(message.usage) >= 0 &&
|
|
2030
|
+
(!boundary || message.timestamp > new Date(boundary.timestamp).getTime()) &&
|
|
2031
|
+
[message.usage.input, message.usage.output, message.usage.cacheRead, message.usage.cacheWrite].every((n) => Number.isFinite(n) && n >= 0));
|
|
2032
|
+
if (reverseIndex < 0)
|
|
2033
|
+
return;
|
|
2034
|
+
const sourceIndex = messages.length - reverseIndex - 1;
|
|
2035
|
+
const source = messages[sourceIndex];
|
|
2036
|
+
const tokens = calculateContextTokens(source.usage) +
|
|
2037
|
+
messages.slice(sourceIndex + 1).reduce((total, message) => total + estimateTokens(message), 0);
|
|
2038
|
+
const window = this.model?.contextWindow ?? 0;
|
|
2039
|
+
if (shouldCompact(tokens, window, settings) ||
|
|
2040
|
+
(window > settings.reserveTokens && tokens > (this.model?.maxInputTokens ?? Infinity)))
|
|
2041
|
+
await this._runAutoCompaction("threshold", false);
|
|
1447
2042
|
return;
|
|
2043
|
+
}
|
|
2044
|
+
// SCRAMJET-DIVERGENCE: Total context and independent input limits are separate constraints.
|
|
1448
2045
|
const contextWindow = this.model?.contextWindow ?? 0;
|
|
1449
2046
|
// Skip overflow check if the message came from a different model.
|
|
1450
2047
|
// This handles the case where user switched from a smaller-context model (e.g. opus)
|
|
@@ -1460,7 +2057,8 @@ export class AgentSession {
|
|
|
1460
2057
|
return;
|
|
1461
2058
|
}
|
|
1462
2059
|
// Case 1: Overflow - LLM returned context overflow error
|
|
1463
|
-
if (sameModel &&
|
|
2060
|
+
if (sameModel &&
|
|
2061
|
+
isContextOverflow(assistantMessage, Math.min(contextWindow, this.model?.maxInputTokens ?? Infinity))) {
|
|
1464
2062
|
if (this._overflowRecoveryAttempted) {
|
|
1465
2063
|
this._emit({
|
|
1466
2064
|
type: "compaction_end",
|
|
@@ -1505,7 +2103,8 @@ export class AgentSession {
|
|
|
1505
2103
|
else {
|
|
1506
2104
|
contextTokens = calculateContextTokens(assistantMessage.usage);
|
|
1507
2105
|
}
|
|
1508
|
-
if (shouldCompact(contextTokens, contextWindow, settings)
|
|
2106
|
+
if (shouldCompact(contextTokens, contextWindow, settings) ||
|
|
2107
|
+
(contextWindow > settings.reserveTokens && contextTokens > (this.model?.maxInputTokens ?? Infinity))) {
|
|
1509
2108
|
await this._runAutoCompaction("threshold", false);
|
|
1510
2109
|
}
|
|
1511
2110
|
}
|
|
@@ -1514,9 +2113,10 @@ export class AgentSession {
|
|
|
1514
2113
|
*/
|
|
1515
2114
|
async _runAutoCompaction(reason, willRetry) {
|
|
1516
2115
|
const settings = this.settingsManager.getCompactionSettings();
|
|
1517
|
-
|
|
2116
|
+
// SCRAMJET-DIVERGENCE: reentrant subscribers must observe compaction ownership (#598).
|
|
1518
2117
|
this._autoCompactionAbortController = new AbortController();
|
|
1519
2118
|
try {
|
|
2119
|
+
this._emit({ type: "compaction_start", reason });
|
|
1520
2120
|
if (!this.model) {
|
|
1521
2121
|
this._emit({
|
|
1522
2122
|
type: "compaction_end",
|
|
@@ -1815,6 +2415,7 @@ export class AgentSession {
|
|
|
1815
2415
|
setThinkingLevel: (level) => this.setThinkingLevel(level),
|
|
1816
2416
|
}, {
|
|
1817
2417
|
getModel: () => this.model,
|
|
2418
|
+
getScopedModels: () => this.scopedModels,
|
|
1818
2419
|
isIdle: () => !this.isStreaming,
|
|
1819
2420
|
getSignal: () => this.agent.signal,
|
|
1820
2421
|
abort: () => this.abort(),
|
|
@@ -1860,7 +2461,13 @@ export class AgentSession {
|
|
|
1860
2461
|
definition,
|
|
1861
2462
|
sourceInfo: createSyntheticSourceInfo(`<sdk:${definition.name}>`, { source: "sdk" }),
|
|
1862
2463
|
})),
|
|
1863
|
-
]
|
|
2464
|
+
];
|
|
2465
|
+
// SCRAMJET-DIVERGENCE: historical interpretation follows definition collision winners, not execution allowlists (#598).
|
|
2466
|
+
this._historicalCostDefinitions = new Map(this._baseToolDefinitions);
|
|
2467
|
+
for (const tool of allCustomTools) {
|
|
2468
|
+
this._historicalCostDefinitions.set(tool.definition.name, tool.definition);
|
|
2469
|
+
}
|
|
2470
|
+
const allowedCustomTools = allCustomTools.filter((tool) => isAllowedTool(tool.definition.name));
|
|
1864
2471
|
const definitionRegistry = new Map(Array.from(this._baseToolDefinitions.entries())
|
|
1865
2472
|
.filter(([name]) => isAllowedTool(name))
|
|
1866
2473
|
.map(([name, definition]) => [
|
|
@@ -1870,7 +2477,7 @@ export class AgentSession {
|
|
|
1870
2477
|
sourceInfo: createSyntheticSourceInfo(`<builtin:${name}>`, { source: "builtin" }),
|
|
1871
2478
|
},
|
|
1872
2479
|
]));
|
|
1873
|
-
for (const tool of
|
|
2480
|
+
for (const tool of allowedCustomTools) {
|
|
1874
2481
|
definitionRegistry.set(tool.definition.name, {
|
|
1875
2482
|
definition: tool.definition,
|
|
1876
2483
|
sourceInfo: tool.sourceInfo,
|
|
@@ -1890,7 +2497,7 @@ export class AgentSession {
|
|
|
1890
2497
|
})
|
|
1891
2498
|
.filter((entry) => entry !== undefined));
|
|
1892
2499
|
const runner = this._extensionRunner;
|
|
1893
|
-
const wrappedExtensionTools = wrapRegisteredTools(
|
|
2500
|
+
const wrappedExtensionTools = wrapRegisteredTools(allowedCustomTools, runner);
|
|
1894
2501
|
const wrappedBuiltInTools = wrapRegisteredTools(Array.from(this._baseToolDefinitions.values())
|
|
1895
2502
|
.filter((definition) => isAllowedTool(definition.name))
|
|
1896
2503
|
.map((definition) => ({
|
|
@@ -1961,10 +2568,21 @@ export class AgentSession {
|
|
|
1961
2568
|
}
|
|
1962
2569
|
async reload() {
|
|
1963
2570
|
const previousFlagValues = this._extensionRunner.getFlagValues();
|
|
1964
|
-
|
|
2571
|
+
// SCRAMJET-DIVERGENCE: the Scramjet builtin is a required product component, so reload must validate
|
|
2572
|
+
// the replacement loader before any irreversible runtime mutation. settingsManager.reload() runs first —
|
|
2573
|
+
// it commits fresh settings before the loader can throw, but the re-read is idempotent, so a failed reload
|
|
2574
|
+
// leaves the runtime itself untouched. It also runs first
|
|
2575
|
+
// because the loader's path resolution reads live settings (the loader defers its own settings/source-map
|
|
2576
|
+
// commit until after the builtin init succeeds, so on this path the shared manager is intentionally read
|
|
2577
|
+
// twice). resourceLoader.reload() then re-runs the required
|
|
2578
|
+
// builtin and throws RequiredBuiltinInitError on failure — before session_shutdown is emitted and before
|
|
2579
|
+
// resetApiProviders(). A throwing builtin therefore leaves the current runner, resources, and providers
|
|
2580
|
+
// intact and fires no session_shutdown; only a validated candidate proceeds to teardown and rebuild.
|
|
1965
2581
|
await this.settingsManager.reload();
|
|
1966
|
-
resetApiProviders();
|
|
1967
2582
|
await this._resourceLoader.reload();
|
|
2583
|
+
await emitSessionShutdownEvent(this._extensionRunner, { type: "session_shutdown", reason: "reload" });
|
|
2584
|
+
resetApiProviders();
|
|
2585
|
+
this._runPromptComposition = { kind: "generated-base" };
|
|
1968
2586
|
this._buildRuntime({
|
|
1969
2587
|
activeToolNames: this.getActiveToolNames(),
|
|
1970
2588
|
flagValues: previousFlagValues,
|
|
@@ -1982,114 +2600,325 @@ export class AgentSession {
|
|
|
1982
2600
|
// =========================================================================
|
|
1983
2601
|
// Auto-Retry
|
|
1984
2602
|
// =========================================================================
|
|
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
|
|
2603
|
+
// SCRAMJET-DIVERGENCE: persisted post-extension snapshots authoritatively classify and settle retries (#553).
|
|
2604
|
+
_classifyRetry(message) {
|
|
2605
|
+
if ((message.origin === "harness" || message.stopReason === "aborted") && blocksFailureRecovery(message))
|
|
2606
|
+
return { kind: "do_not_retry", reason: "non_provider_failure", evidence: "none" };
|
|
1993
2607
|
const contextWindow = this.model?.contextWindow ?? 0;
|
|
1994
|
-
if (isContextOverflow(message, contextWindow))
|
|
1995
|
-
return
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
2608
|
+
if (isContextOverflow(message, Math.min(contextWindow, this.model?.maxInputTokens ?? Infinity))) {
|
|
2609
|
+
return { kind: "context_overflow" };
|
|
2610
|
+
}
|
|
2611
|
+
const structured = inspectFailureEvidence(message.diagnostics);
|
|
2612
|
+
if (structured.status === "valid") {
|
|
2613
|
+
if (structured.source !== "provider")
|
|
2614
|
+
return { kind: "do_not_retry", reason: "non_provider_failure", evidence: "none" };
|
|
2615
|
+
if (structured.suppression)
|
|
2616
|
+
return { kind: "do_not_retry", reason: structured.suppression, evidence: structured.family };
|
|
2617
|
+
if (structured.transient || ["missing_terminal_event", "missing_body"].includes(structured.category))
|
|
2618
|
+
return { kind: "retry", evidence: structured.family };
|
|
2619
|
+
return {
|
|
2620
|
+
kind: "do_not_retry",
|
|
2621
|
+
reason: ["unknown", "provider_error", "malformed_event"].includes(structured.category)
|
|
2622
|
+
? "structured_unknown"
|
|
2623
|
+
: "structured_non_transient",
|
|
2624
|
+
evidence: structured.family,
|
|
2625
|
+
};
|
|
2626
|
+
}
|
|
2627
|
+
if (structured.status === "conflicting")
|
|
2628
|
+
return { kind: "do_not_retry", reason: "invalid_failure_evidence", evidence: "none" };
|
|
2629
|
+
if (structured.status === "malformed") {
|
|
2630
|
+
return { kind: "do_not_retry", reason: "malformed_provider_diagnostic", evidence: "none" };
|
|
2631
|
+
}
|
|
2632
|
+
if (structured.status === "duplicate") {
|
|
2633
|
+
return { kind: "do_not_retry", reason: "duplicate_provider_diagnostic", evidence: "none" };
|
|
2634
|
+
}
|
|
2635
|
+
if (!message.errorMessage) {
|
|
2636
|
+
return { kind: "do_not_retry", reason: "missing_error_evidence", evidence: "none" };
|
|
2637
|
+
}
|
|
2638
|
+
if (this._isLegacyRetryableError(message.errorMessage))
|
|
2639
|
+
return { kind: "retry", evidence: "legacy_text" };
|
|
2640
|
+
return { kind: "do_not_retry", reason: "legacy_non_retryable", evidence: "legacy_text" };
|
|
2641
|
+
}
|
|
2642
|
+
_isLegacyRetryableError(errorMessage) {
|
|
2643
|
+
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);
|
|
2644
|
+
}
|
|
2645
|
+
_bounded(value) {
|
|
2646
|
+
return Math.min(65_535, Math.max(0, Number.isFinite(value) ? Math.trunc(value) : 65_535));
|
|
2647
|
+
}
|
|
2648
|
+
_appendAutoRetryRecord(record) {
|
|
2649
|
+
this.sessionManager.appendCustomEntry("coding-agent:auto-retry", record);
|
|
1999
2650
|
}
|
|
2000
2651
|
/**
|
|
2001
|
-
*
|
|
2002
|
-
*
|
|
2652
|
+
* Persist a terminal record, emit `auto_retry_end` when a retry phase was active, and clear the phase.
|
|
2653
|
+
* The phase is released before persistence is attempted so a persistence failure can never strand it.
|
|
2003
2654
|
*/
|
|
2004
|
-
|
|
2655
|
+
_finishRetry(record, end) {
|
|
2656
|
+
const retry = this._retry;
|
|
2657
|
+
this._retry = undefined;
|
|
2658
|
+
try {
|
|
2659
|
+
this._appendAutoRetryRecord(record);
|
|
2660
|
+
}
|
|
2661
|
+
catch (error) {
|
|
2662
|
+
if (retry && !this._manualCompactionDrain) {
|
|
2663
|
+
this._emit({
|
|
2664
|
+
type: "auto_retry_end",
|
|
2665
|
+
success: false,
|
|
2666
|
+
attempt: end?.attempt ?? retry.attempt,
|
|
2667
|
+
finalError: "Retry outcome persistence failed",
|
|
2668
|
+
});
|
|
2669
|
+
}
|
|
2670
|
+
throw this._reportRetryFailure(error);
|
|
2671
|
+
}
|
|
2672
|
+
if (retry && end && !this._manualCompactionDrain)
|
|
2673
|
+
this._emit(end);
|
|
2674
|
+
}
|
|
2675
|
+
_finishRetryWithoutSuccess(record, message) {
|
|
2676
|
+
const retry = this._retry;
|
|
2677
|
+
if (retry?.attempt === 0) {
|
|
2678
|
+
// The continuation already produced a successful message, so the retry recovered and this
|
|
2679
|
+
// later failure is a fresh, non-retried error; close the retry as succeeded first.
|
|
2680
|
+
const attempt = this._runRetryCount;
|
|
2681
|
+
this._finishRetry({
|
|
2682
|
+
schemaVersion: 1,
|
|
2683
|
+
outcome: "succeeded",
|
|
2684
|
+
attemptsCompleted: this._bounded(attempt),
|
|
2685
|
+
cumulativeErrors: this._bounded(attempt),
|
|
2686
|
+
}, { type: "auto_retry_end", success: true, attempt });
|
|
2687
|
+
this._finishRetry(record);
|
|
2688
|
+
return;
|
|
2689
|
+
}
|
|
2690
|
+
this._finishRetry(record, retry
|
|
2691
|
+
? { type: "auto_retry_end", success: false, attempt: retry.attempt, finalError: message.errorMessage }
|
|
2692
|
+
: undefined);
|
|
2693
|
+
}
|
|
2694
|
+
/** A run that ends without a classifiable snapshot cannot continue the retry; close it so nothing stays wedged. */
|
|
2695
|
+
_releaseRetryForFailedRun(error) {
|
|
2696
|
+
const retry = this._retry;
|
|
2697
|
+
if (!retry)
|
|
2698
|
+
return;
|
|
2699
|
+
try {
|
|
2700
|
+
this._finishRetry({
|
|
2701
|
+
schemaVersion: 1,
|
|
2702
|
+
outcome: "failed",
|
|
2703
|
+
reason: "run_failed",
|
|
2704
|
+
attempt: this._bounded(retry.attempt),
|
|
2705
|
+
cumulativeErrors: this._bounded(this._runRetryCount),
|
|
2706
|
+
}, { type: "auto_retry_end", success: false, attempt: retry.attempt, finalError: error.message });
|
|
2707
|
+
}
|
|
2708
|
+
catch {
|
|
2709
|
+
// Already reported and the phase is released; the run settles with its own error.
|
|
2710
|
+
}
|
|
2711
|
+
}
|
|
2712
|
+
_reportRetryFailure(error) {
|
|
2713
|
+
const normalized = error instanceof Error ? error : new Error(String(error));
|
|
2714
|
+
this._extensionRunner.emitError({
|
|
2715
|
+
extensionPath: "agent-session:auto-retry",
|
|
2716
|
+
event: "retry_settlement",
|
|
2717
|
+
error: normalized.message,
|
|
2718
|
+
stack: normalized.stack,
|
|
2719
|
+
});
|
|
2720
|
+
return normalized;
|
|
2721
|
+
}
|
|
2722
|
+
async _handleRetryableError(message, evidence, run) {
|
|
2005
2723
|
const settings = this.settingsManager.getRetrySettings();
|
|
2006
|
-
|
|
2007
|
-
|
|
2724
|
+
const attempt = (this._retry?.attempt ?? 0) + 1;
|
|
2725
|
+
const cumulativeCap = Math.min(Number.MAX_SAFE_INTEGER, settings.maxRetries * 2);
|
|
2726
|
+
if ((this._retry?.attempt ?? 0) >= settings.maxRetries) {
|
|
2727
|
+
const attemptsCompleted = attempt - 1;
|
|
2728
|
+
this._finishRetry({
|
|
2729
|
+
schemaVersion: 1,
|
|
2730
|
+
outcome: "exhausted",
|
|
2731
|
+
reason: "attempt_limit",
|
|
2732
|
+
attemptsCompleted: this._bounded(attemptsCompleted),
|
|
2733
|
+
maxAttempts: this._bounded(settings.maxRetries),
|
|
2734
|
+
cumulativeErrors: this._bounded(this._runRetryCount),
|
|
2735
|
+
}, { type: "auto_retry_end", success: false, attempt: attemptsCompleted, finalError: message.errorMessage });
|
|
2008
2736
|
return false;
|
|
2009
2737
|
}
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
this._emit({
|
|
2738
|
+
if (this._runRetryCount >= cumulativeCap) {
|
|
2739
|
+
const attemptsCompleted = this._runRetryCount;
|
|
2740
|
+
this._finishRetry({
|
|
2741
|
+
schemaVersion: 1,
|
|
2742
|
+
outcome: "exhausted",
|
|
2743
|
+
reason: "cumulative_limit",
|
|
2744
|
+
attemptsCompleted: this._bounded(attemptsCompleted),
|
|
2745
|
+
maxAttempts: this._bounded(settings.maxRetries),
|
|
2746
|
+
cumulativeErrors: this._bounded(this._runRetryCount + 1),
|
|
2747
|
+
}, {
|
|
2021
2748
|
type: "auto_retry_end",
|
|
2022
2749
|
success: false,
|
|
2023
|
-
attempt:
|
|
2024
|
-
finalError: message.errorMessage,
|
|
2750
|
+
attempt: attemptsCompleted,
|
|
2751
|
+
finalError: `Repeated retry failures (${attemptsCompleted} total attempts this prompt). ${message.errorMessage ?? ""}`.trim(),
|
|
2025
2752
|
});
|
|
2026
|
-
this._retryAttempt = 0;
|
|
2027
|
-
this._resolveRetry(); // Resolve so waitForRetry() completes
|
|
2028
2753
|
return false;
|
|
2029
2754
|
}
|
|
2030
|
-
|
|
2755
|
+
this._runRetryCount++;
|
|
2756
|
+
const delayMs = settings.baseDelayMs === 0
|
|
2757
|
+
? 0
|
|
2758
|
+
: Math.min(2_147_483_647, settings.baseDelayMs * 2 ** Math.min(31, attempt - 1));
|
|
2759
|
+
const previous = this._retry;
|
|
2760
|
+
this._retry = undefined;
|
|
2761
|
+
try {
|
|
2762
|
+
this._appendAutoRetryRecord({
|
|
2763
|
+
schemaVersion: 1,
|
|
2764
|
+
outcome: "scheduled",
|
|
2765
|
+
evidence,
|
|
2766
|
+
attempt: this._bounded(attempt),
|
|
2767
|
+
maxAttempts: this._bounded(settings.maxRetries),
|
|
2768
|
+
cumulativeErrors: this._bounded(this._runRetryCount),
|
|
2769
|
+
delayMs: this._bounded(delayMs),
|
|
2770
|
+
});
|
|
2771
|
+
}
|
|
2772
|
+
catch (error) {
|
|
2773
|
+
if (previous) {
|
|
2774
|
+
this._emit({
|
|
2775
|
+
type: "auto_retry_end",
|
|
2776
|
+
success: false,
|
|
2777
|
+
attempt: previous.attempt,
|
|
2778
|
+
finalError: "Retry outcome persistence failed",
|
|
2779
|
+
});
|
|
2780
|
+
}
|
|
2781
|
+
throw this._reportRetryFailure(error);
|
|
2782
|
+
}
|
|
2783
|
+
const controller = new AbortController();
|
|
2784
|
+
const backoff = { phase: "backoff", attempt, controller, run };
|
|
2785
|
+
this._retry = backoff;
|
|
2031
2786
|
this._emit({
|
|
2032
2787
|
type: "auto_retry_start",
|
|
2033
|
-
attempt
|
|
2788
|
+
attempt,
|
|
2034
2789
|
maxAttempts: settings.maxRetries,
|
|
2035
2790
|
delayMs,
|
|
2036
2791
|
errorMessage: message.errorMessage || "Unknown error",
|
|
2792
|
+
cumulativeErrors: this._runRetryCount,
|
|
2037
2793
|
});
|
|
2038
|
-
|
|
2794
|
+
if (controller.signal.aborted || this._disposed) {
|
|
2795
|
+
return this._finishCancelledRetry("cancelled_during_backoff", backoff);
|
|
2796
|
+
}
|
|
2039
2797
|
const messages = this.agent.state.messages;
|
|
2040
2798
|
if (messages.length > 0 && messages[messages.length - 1].role === "assistant") {
|
|
2041
2799
|
this.agent.state.messages = messages.slice(0, -1);
|
|
2042
2800
|
}
|
|
2043
|
-
// Wait with exponential backoff (abortable)
|
|
2044
|
-
this._retryAbortController = new AbortController();
|
|
2045
2801
|
try {
|
|
2046
|
-
await sleep(delayMs,
|
|
2802
|
+
await sleep(delayMs, controller.signal);
|
|
2803
|
+
await sleep(0, controller.signal);
|
|
2047
2804
|
}
|
|
2048
2805
|
catch {
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
this.
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
this.
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2806
|
+
return this._finishCancelledRetry("cancelled_during_backoff", backoff);
|
|
2807
|
+
}
|
|
2808
|
+
if (controller.signal.aborted || this._disposed) {
|
|
2809
|
+
return this._finishCancelledRetry("cancelled_during_backoff", backoff);
|
|
2810
|
+
}
|
|
2811
|
+
if (this._manualCompactionDrain)
|
|
2812
|
+
return this._finishCancelledRetry("cancelled_during_backoff", backoff);
|
|
2813
|
+
// Hand the chain to the continuation before `continue()`; the run that eventually emits
|
|
2814
|
+
// `agent_start` adopts it (see `_captureAgentRunSettlement`), however long Agent defers that start.
|
|
2815
|
+
const continuing = { phase: "continuing", attempt, chain: run.chain };
|
|
2816
|
+
this._retry = continuing;
|
|
2817
|
+
const continuation = this.agent.continue();
|
|
2818
|
+
this._settleAgentRun(run);
|
|
2819
|
+
continuation.catch((error) => {
|
|
2820
|
+
if (this._disposed || this._retry !== continuing)
|
|
2821
|
+
return;
|
|
2822
|
+
const continuationError = this._reportRetryFailure(error);
|
|
2823
|
+
let settlementError = continuationError;
|
|
2824
|
+
try {
|
|
2825
|
+
this._finishRetry({
|
|
2826
|
+
schemaVersion: 1,
|
|
2827
|
+
outcome: "failed",
|
|
2828
|
+
reason: "continuation_rejected",
|
|
2829
|
+
attempt: this._bounded(attempt),
|
|
2830
|
+
cumulativeErrors: this._bounded(this._runRetryCount),
|
|
2831
|
+
}, { type: "auto_retry_end", success: false, attempt, finalError: continuationError.message });
|
|
2832
|
+
}
|
|
2833
|
+
catch (persistenceError) {
|
|
2834
|
+
settlementError = new AggregateError([continuationError, persistenceError], "Retry continuation and outcome persistence both failed.", { cause: continuationError });
|
|
2835
|
+
}
|
|
2836
|
+
this._settleRetryChain(run.chain, settlementError);
|
|
2837
|
+
});
|
|
2069
2838
|
return true;
|
|
2070
2839
|
}
|
|
2840
|
+
_finishCancelledRetry(reason, expected) {
|
|
2841
|
+
if (this._disposed) {
|
|
2842
|
+
this._retry = undefined;
|
|
2843
|
+
throw new Error("AgentSession disposed before retry settlement completed.");
|
|
2844
|
+
}
|
|
2845
|
+
const retry = this._retry;
|
|
2846
|
+
if (expected && retry !== expected)
|
|
2847
|
+
return false;
|
|
2848
|
+
const attempt = retry?.attempt ?? 0;
|
|
2849
|
+
this._finishRetry({
|
|
2850
|
+
schemaVersion: 1,
|
|
2851
|
+
outcome: "cancelled",
|
|
2852
|
+
reason,
|
|
2853
|
+
attempt: this._bounded(attempt),
|
|
2854
|
+
cumulativeErrors: this._bounded(this._runRetryCount),
|
|
2855
|
+
}, { type: "auto_retry_end", success: false, attempt, finalError: "Retry cancelled" });
|
|
2856
|
+
return false;
|
|
2857
|
+
}
|
|
2071
2858
|
/**
|
|
2072
2859
|
* Cancel in-progress retry.
|
|
2073
2860
|
*/
|
|
2074
2861
|
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) {
|
|
2862
|
+
const retry = this._retry;
|
|
2863
|
+
if (!retry)
|
|
2085
2864
|
return;
|
|
2865
|
+
if (retry.phase === "backoff")
|
|
2866
|
+
retry.controller.abort();
|
|
2867
|
+
this.agent.abort();
|
|
2868
|
+
}
|
|
2869
|
+
/** Agent's own `activeRun` guard is false during backoff; the retry phase owns the idle Agent then. */
|
|
2870
|
+
_assertRetryNotOwningIdleAgent() {
|
|
2871
|
+
if (this._retry && !this.isStreaming) {
|
|
2872
|
+
throw new Error("Automatic retry is in progress. Cancel the retry before starting another prompt.");
|
|
2873
|
+
}
|
|
2874
|
+
}
|
|
2875
|
+
async _runAgentPrompt(messages) {
|
|
2876
|
+
this._assertRetryNotOwningIdleAgent();
|
|
2877
|
+
const start = this._reserveAgentRunStart();
|
|
2878
|
+
this._runRetryCount = 0;
|
|
2879
|
+
let promptFailure;
|
|
2880
|
+
try {
|
|
2881
|
+
await this.agent.prompt(messages);
|
|
2882
|
+
}
|
|
2883
|
+
catch (error) {
|
|
2884
|
+
promptFailure = { error };
|
|
2885
|
+
}
|
|
2886
|
+
if (!start.run && this._pendingAgentRunStart === start) {
|
|
2887
|
+
const startError = promptFailure
|
|
2888
|
+
? promptFailure.error instanceof Error
|
|
2889
|
+
? promptFailure.error
|
|
2890
|
+
: new Error(String(promptFailure.error))
|
|
2891
|
+
: new Error("Agent prompt completed before agent_start.");
|
|
2892
|
+
this._rejectAgentRunStart(start, startError);
|
|
2893
|
+
if (promptFailure)
|
|
2894
|
+
throw promptFailure.error;
|
|
2895
|
+
}
|
|
2896
|
+
let settlementFailure;
|
|
2897
|
+
try {
|
|
2898
|
+
const settlement = await start.promise;
|
|
2899
|
+
await this.waitForRetry(settlement);
|
|
2900
|
+
}
|
|
2901
|
+
catch (error) {
|
|
2902
|
+
settlementFailure = { error };
|
|
2903
|
+
}
|
|
2904
|
+
if (promptFailure && settlementFailure) {
|
|
2905
|
+
throw new AggregateError([promptFailure.error, settlementFailure.error], "Agent prompt and retry settlement both failed.", { cause: promptFailure.error });
|
|
2086
2906
|
}
|
|
2087
|
-
|
|
2907
|
+
if (promptFailure)
|
|
2908
|
+
throw promptFailure.error;
|
|
2909
|
+
if (settlementFailure)
|
|
2910
|
+
throw settlementFailure.error;
|
|
2911
|
+
}
|
|
2912
|
+
async waitForRetry(settlement) {
|
|
2913
|
+
if (!settlement)
|
|
2914
|
+
return;
|
|
2915
|
+
await settlement.promise;
|
|
2916
|
+
await settlement.chain.promise;
|
|
2088
2917
|
await this.agent.waitForIdle();
|
|
2089
2918
|
}
|
|
2090
2919
|
/** Whether auto-retry is currently in progress */
|
|
2091
2920
|
get isRetrying() {
|
|
2092
|
-
return this.
|
|
2921
|
+
return this._retry !== undefined;
|
|
2093
2922
|
}
|
|
2094
2923
|
/** Whether auto-retry is enabled */
|
|
2095
2924
|
get autoRetryEnabled() {
|
|
@@ -2309,7 +3138,7 @@ export class AgentSession {
|
|
|
2309
3138
|
if (targetEntry.type === "message" && targetEntry.message.role === "user") {
|
|
2310
3139
|
// User message: leaf = parent (null if root), text goes to editor
|
|
2311
3140
|
newLeafId = targetEntry.parentId;
|
|
2312
|
-
editorText =
|
|
3141
|
+
editorText = restoreScramjetCommandInvocation(targetEntry, this.sessionManager.getEntries());
|
|
2313
3142
|
}
|
|
2314
3143
|
else if (targetEntry.type === "custom_message") {
|
|
2315
3144
|
// Custom message: leaf = parent (null if root), text goes to editor
|
|
@@ -2373,29 +3202,56 @@ export class AgentSession {
|
|
|
2373
3202
|
*/
|
|
2374
3203
|
getUserMessagesForForking() {
|
|
2375
3204
|
const entries = this.sessionManager.getEntries();
|
|
3205
|
+
const commandStarts = indexScramjetCommandStarts(entries);
|
|
2376
3206
|
const result = [];
|
|
2377
3207
|
for (const entry of entries) {
|
|
2378
3208
|
if (entry.type !== "message")
|
|
2379
3209
|
continue;
|
|
2380
3210
|
if (entry.message.role !== "user")
|
|
2381
3211
|
continue;
|
|
2382
|
-
const text =
|
|
3212
|
+
const text = restoreScramjetCommandInvocation(entry, commandStarts);
|
|
2383
3213
|
if (text) {
|
|
2384
3214
|
result.push({ entryId: entry.id, text });
|
|
2385
3215
|
}
|
|
2386
3216
|
}
|
|
2387
3217
|
return result;
|
|
2388
3218
|
}
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
3219
|
+
// SCRAMJET-DIVERGENCE: whole-journal parent/tool costs plus reports awaiting successful append (#598).
|
|
3220
|
+
getRecordedSessionCost() {
|
|
3221
|
+
let total = 0;
|
|
3222
|
+
for (const entry of this.sessionManager.getEntries()) {
|
|
3223
|
+
if (entry.type !== "message")
|
|
3224
|
+
continue;
|
|
3225
|
+
const message = entry.message;
|
|
3226
|
+
if (message.role === "assistant") {
|
|
3227
|
+
if (validRecordedCost(message.usage?.cost?.total))
|
|
3228
|
+
total += message.usage.cost.total;
|
|
3229
|
+
}
|
|
3230
|
+
else if (message.role === "toolResult") {
|
|
3231
|
+
if (validRecordedCost(message.cost)) {
|
|
3232
|
+
total += message.cost;
|
|
3233
|
+
continue;
|
|
3234
|
+
}
|
|
3235
|
+
const definition = this._historicalCostDefinitions.get(message.toolName);
|
|
3236
|
+
if (!definition?.getHistoricalCost)
|
|
3237
|
+
continue;
|
|
3238
|
+
try {
|
|
3239
|
+
const cost = definition.getHistoricalCost(message.details);
|
|
3240
|
+
if (validRecordedCost(cost))
|
|
3241
|
+
total += cost;
|
|
3242
|
+
}
|
|
3243
|
+
catch (error) {
|
|
3244
|
+
this._extensionRunner.emitError({
|
|
3245
|
+
extensionPath: `tool:${message.toolName}`,
|
|
3246
|
+
event: "historical_cost",
|
|
3247
|
+
error: `Failed to interpret historical tool cost: ${error instanceof Error ? error.message : String(error)}`,
|
|
3248
|
+
});
|
|
3249
|
+
}
|
|
3250
|
+
}
|
|
2397
3251
|
}
|
|
2398
|
-
|
|
3252
|
+
for (const execution of this._pendingToolCosts)
|
|
3253
|
+
total += execution.cost ?? 0;
|
|
3254
|
+
return total;
|
|
2399
3255
|
}
|
|
2400
3256
|
/**
|
|
2401
3257
|
* Get session statistics.
|