open-multi-agent-kit 0.92.0 → 0.94.0
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 +38 -1
- package/README.md +2 -2
- package/dist/core/agent-session.d.ts +36 -14
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +155 -357
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/compaction/branch-summarization.d.ts +5 -1
- package/dist/core/compaction/branch-summarization.d.ts.map +1 -1
- package/dist/core/compaction/branch-summarization.js +5 -7
- package/dist/core/compaction/branch-summarization.js.map +1 -1
- package/dist/core/compaction/compaction.d.ts +12 -3
- package/dist/core/compaction/compaction.d.ts.map +1 -1
- package/dist/core/compaction/compaction.js +28 -15
- package/dist/core/compaction/compaction.js.map +1 -1
- package/dist/core/extensions/builtin/command-safety-gate.d.ts +8 -0
- package/dist/core/extensions/builtin/command-safety-gate.d.ts.map +1 -1
- package/dist/core/extensions/builtin/command-safety-gate.js +23 -0
- package/dist/core/extensions/builtin/command-safety-gate.js.map +1 -1
- package/dist/core/extensions/runner.d.ts.map +1 -1
- package/dist/core/extensions/runner.js +4 -0
- package/dist/core/extensions/runner.js.map +1 -1
- package/dist/core/extensions/types.d.ts +2 -0
- 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 +5 -0
- package/dist/core/keybindings.d.ts.map +1 -1
- package/dist/core/keybindings.js +5 -0
- package/dist/core/keybindings.js.map +1 -1
- package/dist/core/model-resolver.d.ts +41 -3
- package/dist/core/model-resolver.d.ts.map +1 -1
- package/dist/core/model-resolver.js +17 -9
- package/dist/core/model-resolver.js.map +1 -1
- package/dist/core/package-manager.d.ts.map +1 -1
- package/dist/core/package-manager.js +11 -1
- package/dist/core/package-manager.js.map +1 -1
- package/dist/core/provider-resilience.d.ts.map +1 -1
- package/dist/core/provider-resilience.js +3 -1
- package/dist/core/provider-resilience.js.map +1 -1
- package/dist/core/reasoning-router-v4-weights.d.ts.map +1 -1
- package/dist/core/reasoning-router-v4-weights.js +9 -3
- package/dist/core/reasoning-router-v4-weights.js.map +1 -1
- package/dist/core/reasoning-router-v4.d.ts.map +1 -1
- package/dist/core/reasoning-router-v4.js +94 -24
- package/dist/core/reasoning-router-v4.js.map +1 -1
- package/dist/core/resource-loader.d.ts.map +1 -1
- package/dist/core/resource-loader.js +11 -5
- package/dist/core/resource-loader.js.map +1 -1
- package/dist/core/sandbox/default-policy.d.ts +14 -0
- package/dist/core/sandbox/default-policy.d.ts.map +1 -0
- package/dist/core/sandbox/default-policy.js +58 -0
- package/dist/core/sandbox/default-policy.js.map +1 -0
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +1 -1
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/session-bash-runtime.d.ts +62 -0
- package/dist/core/session-bash-runtime.d.ts.map +1 -0
- package/dist/core/session-bash-runtime.js +167 -0
- package/dist/core/session-bash-runtime.js.map +1 -0
- package/dist/core/session-bash-service.d.ts +61 -0
- package/dist/core/session-bash-service.d.ts.map +1 -0
- package/dist/core/session-bash-service.js +155 -0
- package/dist/core/session-bash-service.js.map +1 -0
- package/dist/core/session-compaction-service.d.ts +48 -0
- package/dist/core/session-compaction-service.d.ts.map +1 -0
- package/dist/core/session-compaction-service.js +250 -0
- package/dist/core/session-compaction-service.js.map +1 -0
- package/dist/core/session-file-compaction.d.ts.map +1 -1
- package/dist/core/session-file-compaction.js +39 -5
- package/dist/core/session-file-compaction.js.map +1 -1
- package/dist/core/session-termination.d.ts.map +1 -1
- package/dist/core/session-termination.js +1 -1
- package/dist/core/session-termination.js.map +1 -1
- package/dist/core/settings-manager.d.ts +3 -0
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +8 -0
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/skills-catalog-cache.d.ts +25 -0
- package/dist/core/skills-catalog-cache.d.ts.map +1 -0
- package/dist/core/skills-catalog-cache.js +123 -0
- package/dist/core/skills-catalog-cache.js.map +1 -0
- package/dist/core/skills.d.ts +6 -0
- package/dist/core/skills.d.ts.map +1 -1
- package/dist/core/skills.js +25 -4
- package/dist/core/skills.js.map +1 -1
- package/dist/core/system-prompt.d.ts.map +1 -1
- package/dist/core/system-prompt.js +2 -2
- package/dist/core/system-prompt.js.map +1 -1
- package/dist/core/tools/bash.d.ts +5 -0
- package/dist/core/tools/bash.d.ts.map +1 -1
- package/dist/core/tools/bash.js +41 -13
- package/dist/core/tools/bash.js.map +1 -1
- package/dist/core/tools/diagnostics.d.ts +29 -0
- package/dist/core/tools/diagnostics.d.ts.map +1 -0
- package/dist/core/tools/diagnostics.js +325 -0
- package/dist/core/tools/diagnostics.js.map +1 -0
- package/dist/core/tools/index.d.ts +2 -1
- package/dist/core/tools/index.d.ts.map +1 -1
- package/dist/core/tools/index.js +18 -1
- package/dist/core/tools/index.js.map +1 -1
- package/dist/core/verified-bash-adapter.d.ts +18 -1
- package/dist/core/verified-bash-adapter.d.ts.map +1 -1
- package/dist/core/verified-bash-adapter.js +38 -1
- package/dist/core/verified-bash-adapter.js.map +1 -1
- package/dist/core/verified-bash-runtime.d.ts +13 -0
- package/dist/core/verified-bash-runtime.d.ts.map +1 -0
- package/dist/core/verified-bash-runtime.js +81 -0
- package/dist/core/verified-bash-runtime.js.map +1 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -2
- package/dist/index.js.map +1 -1
- package/dist/modes/interactive/components/control-panel-box.d.ts +27 -0
- package/dist/modes/interactive/components/control-panel-box.d.ts.map +1 -1
- package/dist/modes/interactive/components/control-panel-box.js +60 -8
- package/dist/modes/interactive/components/control-panel-box.js.map +1 -1
- package/dist/modes/interactive/components/control-panel-layout.d.ts.map +1 -1
- package/dist/modes/interactive/components/control-panel-layout.js +100 -45
- package/dist/modes/interactive/components/control-panel-layout.js.map +1 -1
- package/dist/modes/interactive/components/control-panel-runtime-status.d.ts +9 -0
- package/dist/modes/interactive/components/control-panel-runtime-status.d.ts.map +1 -1
- package/dist/modes/interactive/components/control-panel-runtime-status.js +10 -8
- package/dist/modes/interactive/components/control-panel-runtime-status.js.map +1 -1
- package/dist/modes/interactive/components/control-panel-sparkles.d.ts +5 -0
- package/dist/modes/interactive/components/control-panel-sparkles.d.ts.map +1 -1
- package/dist/modes/interactive/components/control-panel-sparkles.js +14 -9
- package/dist/modes/interactive/components/control-panel-sparkles.js.map +1 -1
- package/dist/modes/interactive/components/footer.d.ts +10 -0
- package/dist/modes/interactive/components/footer.d.ts.map +1 -1
- package/dist/modes/interactive/components/footer.js +3 -3
- package/dist/modes/interactive/components/footer.js.map +1 -1
- package/dist/modes/interactive/components/status-sidebar.d.ts +49 -0
- package/dist/modes/interactive/components/status-sidebar.d.ts.map +1 -0
- package/dist/modes/interactive/components/status-sidebar.js +253 -0
- package/dist/modes/interactive/components/status-sidebar.js.map +1 -0
- package/dist/modes/interactive/interactive-mode.d.ts +9 -0
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +94 -20
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/interactive/theme/omk-neon-control.json +13 -13
- package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-mode.js +1 -0
- package/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/dist/types/evidence.d.ts +1 -1
- package/dist/types/evidence.d.ts.map +1 -1
- package/dist/types/evidence.js.map +1 -1
- package/dist/utils/clipboard.d.ts.map +1 -1
- package/dist/utils/clipboard.js +19 -8
- package/dist/utils/clipboard.js.map +1 -1
- package/dist/utils/frontmatter.d.ts.map +1 -1
- package/dist/utils/frontmatter.js +31 -5
- package/dist/utils/frontmatter.js.map +1 -1
- package/docs/compaction.md +7 -0
- package/docs/docs.json +4 -0
- package/docs/environment-variables.md +94 -0
- package/docs/index.md +1 -0
- package/docs/rpc.md +5 -2
- package/docs/sdk.md +10 -4
- package/docs/settings.md +1 -0
- package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/gondolin/package-lock.json +2 -2
- package/examples/extensions/gondolin/package.json +1 -1
- package/examples/extensions/sandbox/package-lock.json +2 -2
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/subagent/agents.test.ts +86 -0
- package/examples/extensions/subagent/agents.ts +18 -4
- package/examples/extensions/subagent/capabilities.ts +15 -1
- package/examples/extensions/subagent/domain-profiles.ts +272 -0
- package/examples/extensions/with-deps/package-lock.json +2 -2
- package/examples/extensions/with-deps/package.json +1 -1
- package/npm-shrinkwrap.json +15 -15
- package/package.json +10 -7
|
@@ -16,36 +16,31 @@ import { createHash, randomUUID } from "node:crypto";
|
|
|
16
16
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
17
17
|
import { basename, dirname } from "node:path";
|
|
18
18
|
import { isDeepStrictEqual } from "node:util";
|
|
19
|
-
import { repairTranscriptIntegrity, } from "omk-agent-core";
|
|
20
19
|
import { clampThinkingLevel, cleanupSessionResources, getSupportedThinkingLevels, isContextOverflow, modelsAreEqual, resetApiProviders, streamSimple, } from "omk-ai";
|
|
21
20
|
import { theme } from "../modes/interactive/theme/theme.js";
|
|
22
21
|
import { stripFrontmatter } from "../utils/frontmatter.js";
|
|
23
22
|
import { resolvePath } from "../utils/paths.js";
|
|
24
|
-
import {
|
|
23
|
+
import { getShellConfig } from "../utils/shell.js";
|
|
25
24
|
import { sleep } from "../utils/sleep.js";
|
|
26
25
|
import { createAdaptorchBridge } from "./adaptorch-bridge.js";
|
|
27
26
|
import { applyCategoryTimeoutDefaults, resolveAgentToolSettings, resolveToolTimeoutCategory, } from "./agent-tool-settings.js";
|
|
28
27
|
import { formatNoApiKeyFoundMessage, formatNoModelSelectedMessage } from "./auth-guidance.js";
|
|
29
28
|
import { parseBangInvocation } from "./bang-skill-invocation.js";
|
|
30
|
-
import { executeBashWithOperations } from "./bash-executor.js";
|
|
31
|
-
import { classifyShellCommand } from "./command-safety.js";
|
|
32
29
|
import { getCompactionHeadroomThreshold } from "./compaction/compaction.js";
|
|
33
30
|
import { createCompactionHysteresisConfig, createCompactionHysteresisState, stepCompactionHysteresis, } from "./compaction/hysteresis.js";
|
|
34
31
|
import { calculateContextTokens, collectEntriesForBranchSummary, compact, estimateContextTokens, estimateProjectedContextTokens, generateBranchSummary, prepareCompaction, resolveCompactionModel, } from "./compaction/index.js";
|
|
35
32
|
import { compactionEmitWillRetry } from "./compaction/resume-policy.js";
|
|
36
|
-
import { createCompactionEnvelope, createCompactionSourceIdentity, createCompactionTransaction, decideCompactionCommit, evaluateCompactionBarrier, validateCompactionEnvelope, } from "./compaction/transaction.js";
|
|
37
33
|
import { estimateToolResultReserve, } from "./context-budget-reserved-tokens.js";
|
|
38
34
|
import { applyContextCacheInvalidation, createContextCacheInvalidationSnapshot, } from "./context-budget-v2-cache-invalidation.js";
|
|
39
35
|
import { createMemoryContextBudgetCacheProviderV2 } from "./context-budget-v2-cache-provider.js";
|
|
40
36
|
import { DEFAULT_THINKING_LEVEL } from "./defaults.js";
|
|
41
37
|
import { exportSessionToHtml } from "./export-html/index.js";
|
|
42
38
|
import { createToolHtmlRenderer } from "./export-html/tool-renderer.js";
|
|
43
|
-
import { buildBlockedBashResult, evaluateCommandGate, isCommandSafetyAssumeYesEnabled, } from "./extensions/builtin/command-safety-gate.js";
|
|
44
39
|
import { ExtensionRunner, wrapRegisteredTools, } from "./extensions/index.js";
|
|
45
40
|
import { emitSessionShutdownEvent } from "./extensions/runner.js";
|
|
46
41
|
import { assertTextChatModelForCompletion } from "./grok-harness.js";
|
|
47
42
|
import { grokPlaybookAppendForProvider } from "./grok-playbook.js";
|
|
48
|
-
import {
|
|
43
|
+
import { decideLoadoutAccess } from "./loadout-access-policy.js";
|
|
49
44
|
import { findExactModelReferenceMatch } from "./model-resolver.js";
|
|
50
45
|
import { expandPromptTemplate } from "./prompt-templates.js";
|
|
51
46
|
import { isContentSafetyStopMessage, isStickySafetyModel, isTransientProviderErrorMessage, pickFailoverCandidate, resolveProviderResilience, stickySafetyBlockMessage, } from "./provider-resilience.js";
|
|
@@ -54,8 +49,9 @@ import { classifyTaskV4, resolveThinkingLevelV4WithUncertainty } from "./reasoni
|
|
|
54
49
|
import { redactSensitiveText } from "./redaction.js";
|
|
55
50
|
import { appendRouterFeedbackRecord, } from "./router-feedback-collector.js";
|
|
56
51
|
import { RunJournalStore } from "./run-journal-store.js";
|
|
57
|
-
import {
|
|
58
|
-
import {
|
|
52
|
+
import { SessionBashRuntime } from "./session-bash-runtime.js";
|
|
53
|
+
import { SessionBashService } from "./session-bash-service.js";
|
|
54
|
+
import { SessionCompactionService } from "./session-compaction-service.js";
|
|
59
55
|
import { CURRENT_SESSION_VERSION, getLatestCompactionEntry } from "./session-manager.js";
|
|
60
56
|
import { classifySessionTermination, } from "./session-termination.js";
|
|
61
57
|
import { createSyntheticSourceInfo } from "./source-info.js";
|
|
@@ -63,6 +59,8 @@ import { buildSystemPrompt } from "./system-prompt.js";
|
|
|
63
59
|
import { createLocalBashOperations } from "./tools/bash.js";
|
|
64
60
|
import { createAllToolDefinitions } from "./tools/index.js";
|
|
65
61
|
import { createToolDefinitionFromAgentTool } from "./tools/tool-definition-wrapper.js";
|
|
62
|
+
import { createVerifiedBashOperations } from "./verified-bash-adapter.js";
|
|
63
|
+
import { resolveSessionWorkspaceScope } from "./verified-bash-runtime.js";
|
|
66
64
|
/**
|
|
67
65
|
* Parse a skill block from message text.
|
|
68
66
|
* Returns null if the text doesn't contain a skill block.
|
|
@@ -120,17 +118,6 @@ function toolNameSetsEqual(left, right) {
|
|
|
120
118
|
return (normalizedLeft.length === normalizedRight.length &&
|
|
121
119
|
normalizedLeft.every((toolName, index) => toolName === normalizedRight[index]));
|
|
122
120
|
}
|
|
123
|
-
function isSandboxDeniedError(error) {
|
|
124
|
-
return error instanceof Error && error.message.startsWith("sandbox: shell denied");
|
|
125
|
-
}
|
|
126
|
-
function buildSandboxDeniedBashResult(reason) {
|
|
127
|
-
return {
|
|
128
|
-
output: reason,
|
|
129
|
-
exitCode: 1,
|
|
130
|
-
cancelled: false,
|
|
131
|
-
truncated: false,
|
|
132
|
-
};
|
|
133
|
-
}
|
|
134
121
|
function snapshotContractError(reason) {
|
|
135
122
|
return new Error(`Finalized message replacement must be a plain serializable snapshot: ${reason}`);
|
|
136
123
|
}
|
|
@@ -286,11 +273,12 @@ export class AgentSession {
|
|
|
286
273
|
// Retry state
|
|
287
274
|
_retryAbortController = undefined;
|
|
288
275
|
_retryAttempt = 0;
|
|
276
|
+
// v10.3-Ω: models that already refused/failed this turn — failover must advance,
|
|
277
|
+
// not re-pick the same candidate (claude-opus-5 → grok → grok → grok loop fix).
|
|
278
|
+
_refusedModels = new Set();
|
|
289
279
|
// Bash execution state
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
_configuredBashSandboxPreflight = undefined;
|
|
293
|
-
_detectedBashSandboxBackend = undefined;
|
|
280
|
+
_bashService;
|
|
281
|
+
_compactionService;
|
|
294
282
|
// Extension system
|
|
295
283
|
_extensionRunner;
|
|
296
284
|
_turnIndex = 0;
|
|
@@ -322,6 +310,7 @@ export class AgentSession {
|
|
|
322
310
|
_replayLedger;
|
|
323
311
|
_replayGoalId;
|
|
324
312
|
_replayLaneId;
|
|
313
|
+
_bashRuntime;
|
|
325
314
|
_transcriptRepair;
|
|
326
315
|
_sessionRiskLevel = "normal";
|
|
327
316
|
_workspaceMutationCount = 0;
|
|
@@ -360,7 +349,6 @@ export class AgentSession {
|
|
|
360
349
|
this._excludedToolNames = config.excludedToolNames ? new Set(config.excludedToolNames) : undefined;
|
|
361
350
|
this._baseToolsOverride = config.baseToolsOverride;
|
|
362
351
|
this._loadoutAccessPolicy = config.loadoutAccessPolicy;
|
|
363
|
-
this._configuredBashSandboxPreflight = config.bashSandboxPreflight;
|
|
364
352
|
this._sessionStartEvent = config.sessionStartEvent ?? { type: "session_start", reason: "startup" };
|
|
365
353
|
this._transcriptRepair = config.transcriptRepair;
|
|
366
354
|
this._replayLedger = config.replayLedger;
|
|
@@ -369,6 +357,37 @@ export class AgentSession {
|
|
|
369
357
|
if (this._replayLedger && this._replayGoalId !== this._replayLedger.getLedger().goalId) {
|
|
370
358
|
throw new Error("AgentSession replayGoalId does not match the replay ledger goal id");
|
|
371
359
|
}
|
|
360
|
+
this._bashRuntime = new SessionBashRuntime({
|
|
361
|
+
cwd: this._cwd,
|
|
362
|
+
getExecutionCwd: () => this.sessionManager.getCwd(),
|
|
363
|
+
getSessionFile: () => this.sessionManager.getSessionFile(),
|
|
364
|
+
replayLedger: this._replayLedger,
|
|
365
|
+
replayGoalId: this._replayGoalId,
|
|
366
|
+
replayLaneId: this._replayLaneId,
|
|
367
|
+
configuredSandboxPreflight: config.bashSandboxPreflight,
|
|
368
|
+
appendReplayEvent: (type, payload) => this._appendReplayEvent(type, payload),
|
|
369
|
+
});
|
|
370
|
+
this._bashService = new SessionBashService({
|
|
371
|
+
settings: this.settingsManager,
|
|
372
|
+
runtime: this._bashRuntime,
|
|
373
|
+
loadoutPolicy: this._loadoutAccessPolicy,
|
|
374
|
+
getCwd: () => this.sessionManager.getCwd(),
|
|
375
|
+
emit: (event) => this._emit(event),
|
|
376
|
+
isStreaming: () => this.isStreaming,
|
|
377
|
+
pushMessage: (message) => this.agent.state.messages.push(message),
|
|
378
|
+
appendMessage: (message) => this.sessionManager.appendMessage(message),
|
|
379
|
+
});
|
|
380
|
+
this._compactionService = new SessionCompactionService({
|
|
381
|
+
sessionManager: this.sessionManager,
|
|
382
|
+
pendingToolCallIds: () => this.agent.state.pendingToolCalls,
|
|
383
|
+
getUserMessageText: (message) => this._getUserMessageText(message),
|
|
384
|
+
cwd: this._cwd,
|
|
385
|
+
invalidateContextBudget: () => this._invalidateContextBudgetCache({ type: "transcriptRepair" }),
|
|
386
|
+
refreshAgentMessages: () => {
|
|
387
|
+
this.agent.state.messages = this.sessionManager.buildSessionContext().messages;
|
|
388
|
+
},
|
|
389
|
+
recordCommit: () => this._recordCompactionCommitForHysteresis(),
|
|
390
|
+
});
|
|
372
391
|
const sessionFile = this.sessionManager.getSessionFile();
|
|
373
392
|
this._runJournalStore = RunJournalStore.open({
|
|
374
393
|
...(sessionFile ? { journalPath: `${sessionFile}.runjournal` } : {}),
|
|
@@ -462,6 +481,10 @@ export class AgentSession {
|
|
|
462
481
|
});
|
|
463
482
|
this._replayLedger.persist();
|
|
464
483
|
}
|
|
484
|
+
/** Lazy verified-bash executor bound to this session's replay ledger (default-on path). */
|
|
485
|
+
_getVerifiedEvidenceExecutor() {
|
|
486
|
+
return this._bashRuntime.verifiedEvidenceExecutor();
|
|
487
|
+
}
|
|
465
488
|
/** Append one required durable audit record. Persistence failures propagate. */
|
|
466
489
|
_appendRunJournalAudit(event, details) {
|
|
467
490
|
this._runJournalStore.audit({
|
|
@@ -751,12 +774,7 @@ export class AgentSession {
|
|
|
751
774
|
}
|
|
752
775
|
}
|
|
753
776
|
_getBashSandboxPreflight(override) {
|
|
754
|
-
|
|
755
|
-
if (!preflight || preflight.policy.mode === "off" || preflight.backend) {
|
|
756
|
-
return preflight;
|
|
757
|
-
}
|
|
758
|
-
this._detectedBashSandboxBackend ??= detectSandboxBackend();
|
|
759
|
-
return { ...preflight, backend: this._detectedBashSandboxBackend };
|
|
777
|
+
return this._bashRuntime.sandboxPreflight(override);
|
|
760
778
|
}
|
|
761
779
|
async _getRequiredRequestAuth(model) {
|
|
762
780
|
const result = await this._modelRegistry.getApiKeyAndHeaders(model);
|
|
@@ -927,6 +945,7 @@ export class AgentSession {
|
|
|
927
945
|
attempt: this._retryAttempt,
|
|
928
946
|
});
|
|
929
947
|
this._retryAttempt = 0;
|
|
948
|
+
this._refusedModels.clear();
|
|
930
949
|
}
|
|
931
950
|
}
|
|
932
951
|
}
|
|
@@ -1463,6 +1482,7 @@ export class AgentSession {
|
|
|
1463
1482
|
finalError: msg.errorMessage,
|
|
1464
1483
|
});
|
|
1465
1484
|
this._retryAttempt = 0;
|
|
1485
|
+
this._refusedModels.clear();
|
|
1466
1486
|
}
|
|
1467
1487
|
if (await this._checkCompaction(msg)) {
|
|
1468
1488
|
return true;
|
|
@@ -2356,230 +2376,11 @@ export class AgentSession {
|
|
|
2356
2376
|
// =========================================================================
|
|
2357
2377
|
// Compaction
|
|
2358
2378
|
// =========================================================================
|
|
2359
|
-
_captureCompactionState() {
|
|
2360
|
-
return this.sessionManager.withCompactionCommitLock(() => this._captureCompactionStateLocked());
|
|
2361
|
-
}
|
|
2362
|
-
_captureCompactionStateLocked() {
|
|
2363
|
-
const sessionFile = this.sessionManager.getSessionFile();
|
|
2364
|
-
const bytes = sessionFile && existsSync(sessionFile)
|
|
2365
|
-
? new Uint8Array(readFileSync(sessionFile))
|
|
2366
|
-
: new TextEncoder().encode(`${[this.sessionManager.getHeader(), ...this.sessionManager.getEntries()]
|
|
2367
|
-
.filter((entry) => entry !== null)
|
|
2368
|
-
.map((entry) => JSON.stringify(entry))
|
|
2369
|
-
.join("\n")}\n`);
|
|
2370
|
-
const report = inspectSessionIntegrity(bytes, { activeLeafId: this.sessionManager.getLeafId() });
|
|
2371
|
-
const branchEntries = report.activeBranch;
|
|
2372
|
-
let latestCompactionIndex = -1;
|
|
2373
|
-
for (let index = branchEntries.length - 1; index >= 0; index -= 1) {
|
|
2374
|
-
if (branchEntries[index]?.type === "compaction") {
|
|
2375
|
-
latestCompactionIndex = index;
|
|
2376
|
-
break;
|
|
2377
|
-
}
|
|
2378
|
-
}
|
|
2379
|
-
const latestCompaction = branchEntries[latestCompactionIndex];
|
|
2380
|
-
const firstKeptIndex = latestCompaction?.type === "compaction"
|
|
2381
|
-
? branchEntries.findIndex((entry) => entry.id === latestCompaction.firstKeptEntryId)
|
|
2382
|
-
: -1;
|
|
2383
|
-
const sourceEntries = branchEntries.slice(latestCompactionIndex < 0 ? 0 : firstKeptIndex < 0 ? latestCompactionIndex : firstKeptIndex);
|
|
2384
|
-
const firstEntry = sourceEntries[0];
|
|
2385
|
-
const lastEntry = sourceEntries.at(-1);
|
|
2386
|
-
if (!firstEntry || !lastEntry || report.activeLeafId === null) {
|
|
2387
|
-
const barrier = evaluateCompactionBarrier(report, [...this.agent.state.pendingToolCalls]);
|
|
2388
|
-
if (barrier.status !== "ready")
|
|
2389
|
-
throw this._barrierError(barrier);
|
|
2390
|
-
throw new Error("Nothing to compact: the active session branch is empty");
|
|
2391
|
-
}
|
|
2392
|
-
const revision = this.sessionManager.getDurableHeadToken();
|
|
2393
|
-
const source = createCompactionSourceIdentity({
|
|
2394
|
-
sessionId: revision.sessionId,
|
|
2395
|
-
entryIds: sourceEntries.map((entry) => entry.id),
|
|
2396
|
-
firstEntryId: firstEntry.id,
|
|
2397
|
-
lastEntryId: lastEntry.id,
|
|
2398
|
-
sourceSha256: createHash("sha256")
|
|
2399
|
-
.update(sourceEntries.map((entry) => JSON.stringify(entry)).join("\n"), "utf8")
|
|
2400
|
-
.digest("hex"),
|
|
2401
|
-
activeLeafId: report.activeLeafId,
|
|
2402
|
-
messageCount: report.activeMessages.length,
|
|
2403
|
-
});
|
|
2404
|
-
return { report, branchEntries, revision, source };
|
|
2405
|
-
}
|
|
2406
|
-
_captureCompactionProvenance(capture) {
|
|
2407
|
-
let latestIntent = "Continue the current session";
|
|
2408
|
-
for (let index = capture.report.activeMessages.length - 1; index >= 0; index -= 1) {
|
|
2409
|
-
const message = capture.report.activeMessages[index];
|
|
2410
|
-
if (message?.role !== "user")
|
|
2411
|
-
continue;
|
|
2412
|
-
const candidate = sanitizeBinaryOutput(redactSensitiveText(this._getUserMessageText(message)).trim())
|
|
2413
|
-
.replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/gu, "")
|
|
2414
|
-
.slice(0, 16_384);
|
|
2415
|
-
if (candidate.length > 0)
|
|
2416
|
-
latestIntent = candidate;
|
|
2417
|
-
break;
|
|
2418
|
-
}
|
|
2419
|
-
const modelHistory = capture.branchEntries
|
|
2420
|
-
.flatMap((entry) => {
|
|
2421
|
-
if (entry.type === "model_change") {
|
|
2422
|
-
return [{ entryId: entry.id, provider: entry.provider, modelId: entry.modelId }];
|
|
2423
|
-
}
|
|
2424
|
-
if (entry.type === "message" && entry.message.role === "assistant") {
|
|
2425
|
-
return [{ entryId: entry.id, provider: entry.message.provider, modelId: entry.message.model }];
|
|
2426
|
-
}
|
|
2427
|
-
return [];
|
|
2428
|
-
})
|
|
2429
|
-
.slice(-256);
|
|
2430
|
-
const customEntryIds = (customType) => capture.branchEntries
|
|
2431
|
-
.filter((entry) => entry.type === "custom" && entry.customType === customType)
|
|
2432
|
-
.map((entry) => entry.id);
|
|
2433
|
-
return {
|
|
2434
|
-
latestIntent,
|
|
2435
|
-
openTasks: [],
|
|
2436
|
-
laneIds: customEntryIds("lane"),
|
|
2437
|
-
acceptancePredicateIds: customEntryIds("acceptance_predicate"),
|
|
2438
|
-
evidenceReceiptIds: customEntryIds("evidence_receipt"),
|
|
2439
|
-
blockerReasons: [],
|
|
2440
|
-
repairEventIds: [
|
|
2441
|
-
...customEntryIds("transcript_repaired"),
|
|
2442
|
-
...customEntryIds("compaction_transcript_repaired"),
|
|
2443
|
-
],
|
|
2444
|
-
branch: null,
|
|
2445
|
-
worktree: this._cwd,
|
|
2446
|
-
modelHistory,
|
|
2447
|
-
nextAction: latestIntent.slice(0, 4096) || "Continue the current session",
|
|
2448
|
-
};
|
|
2449
|
-
}
|
|
2450
|
-
_barrierError(barrier) {
|
|
2451
|
-
if (barrier.status === "defer") {
|
|
2452
|
-
return new Error(`Compaction deferred until the transcript closes (${barrier.reason})`);
|
|
2453
|
-
}
|
|
2454
|
-
return new Error(`Compaction failed closed on transcript integrity (${barrier.reason}). Run the session doctor before retrying.`);
|
|
2455
|
-
}
|
|
2456
|
-
_evaluateCompactionBarrier(capture, includeMissingTailAsPending, excludedPendingIds = new Set()) {
|
|
2457
|
-
const pending = new Set([...this.agent.state.pendingToolCalls].filter((toolCallId) => !excludedPendingIds.has(toolCallId)));
|
|
2458
|
-
if (includeMissingTailAsPending) {
|
|
2459
|
-
for (const issue of capture.report.transcript?.issues ?? []) {
|
|
2460
|
-
if (issue.kind === "missing_result")
|
|
2461
|
-
pending.add(issue.toolCallId);
|
|
2462
|
-
}
|
|
2463
|
-
}
|
|
2464
|
-
return evaluateCompactionBarrier(capture.report, [...pending]);
|
|
2465
|
-
}
|
|
2466
|
-
_repairEmergencyCompactionTail(capture) {
|
|
2467
|
-
const barrier = this._evaluateCompactionBarrier(capture, true);
|
|
2468
|
-
if (barrier.status !== "defer" || barrier.reason !== "missing_active_tail_results") {
|
|
2469
|
-
if (barrier.status !== "ready")
|
|
2470
|
-
throw this._barrierError(barrier);
|
|
2471
|
-
return { capture, repairedToolCallIds: new Set() };
|
|
2472
|
-
}
|
|
2473
|
-
const repairedMessages = repairTranscriptIntegrity([...capture.report.activeMessages], "Tool result missing; synthesized to close an emergency compaction barrier");
|
|
2474
|
-
const inserted = repairedMessages.slice(capture.report.activeMessages.length);
|
|
2475
|
-
const repairedToolCallIds = new Set();
|
|
2476
|
-
for (const message of inserted) {
|
|
2477
|
-
if (message.role !== "toolResult") {
|
|
2478
|
-
throw new Error("Emergency compaction repair produced a non-tool result");
|
|
2479
|
-
}
|
|
2480
|
-
const toolResult = message;
|
|
2481
|
-
repairedToolCallIds.add(toolResult.toolCallId);
|
|
2482
|
-
this.sessionManager.appendMessage(toolResult);
|
|
2483
|
-
}
|
|
2484
|
-
this.sessionManager.appendCustomEntry("compaction_transcript_repaired", {
|
|
2485
|
-
insertedToolCallIds: [...repairedToolCallIds],
|
|
2486
|
-
reason: "emergency_compaction",
|
|
2487
|
-
});
|
|
2488
|
-
this._invalidateContextBudgetCache({ type: "transcriptRepair" });
|
|
2489
|
-
const closedCapture = this._captureCompactionState();
|
|
2490
|
-
const closedBarrier = this._evaluateCompactionBarrier(closedCapture, false, repairedToolCallIds);
|
|
2491
|
-
if (closedBarrier.status !== "ready")
|
|
2492
|
-
throw this._barrierError(closedBarrier);
|
|
2493
|
-
this.agent.state.messages = this.sessionManager.buildSessionContext().messages;
|
|
2494
|
-
return { capture: closedCapture, repairedToolCallIds };
|
|
2495
|
-
}
|
|
2496
|
-
_priorCommittedCompactionSourceDigests() {
|
|
2497
|
-
const digests = [];
|
|
2498
|
-
for (const entry of this.sessionManager.getEntries()) {
|
|
2499
|
-
if (entry.type !== "compaction" || typeof entry.details !== "object" || entry.details === null)
|
|
2500
|
-
continue;
|
|
2501
|
-
if (!Object.hasOwn(entry.details, "compactionEnvelope"))
|
|
2502
|
-
continue;
|
|
2503
|
-
const envelope = validateCompactionEnvelope(Reflect.get(entry.details, "compactionEnvelope"));
|
|
2504
|
-
if (envelope.summary !== entry.summary) {
|
|
2505
|
-
throw new Error(`Compaction entry ${entry.id} has invalid provenance. Run the session doctor.`);
|
|
2506
|
-
}
|
|
2507
|
-
digests.push(envelope.source.sourceSha256);
|
|
2508
|
-
}
|
|
2509
|
-
return digests;
|
|
2510
|
-
}
|
|
2511
2379
|
_beginCompactionTransaction(compactionModel, emergency) {
|
|
2512
|
-
|
|
2513
|
-
if (emergency) {
|
|
2514
|
-
capture = this._repairEmergencyCompactionTail(capture).capture;
|
|
2515
|
-
}
|
|
2516
|
-
else {
|
|
2517
|
-
const barrier = this._evaluateCompactionBarrier(capture, false);
|
|
2518
|
-
if (barrier.status !== "ready")
|
|
2519
|
-
throw this._barrierError(barrier);
|
|
2520
|
-
}
|
|
2521
|
-
const transaction = createCompactionTransaction({
|
|
2522
|
-
transactionId: randomUUID(),
|
|
2523
|
-
baseRevision: capture.revision,
|
|
2524
|
-
source: capture.source,
|
|
2525
|
-
createdAt: new Date().toISOString(),
|
|
2526
|
-
model: { provider: compactionModel.provider, id: compactionModel.id },
|
|
2527
|
-
preserved: this._captureCompactionProvenance(capture),
|
|
2528
|
-
});
|
|
2529
|
-
if (this._priorCommittedCompactionSourceDigests().includes(transaction.source.sourceSha256)) {
|
|
2530
|
-
throw new Error("This exact compaction source was already compacted");
|
|
2531
|
-
}
|
|
2532
|
-
return { capture, transaction };
|
|
2533
|
-
}
|
|
2534
|
-
_detailsWithCompactionEnvelope(details, envelope) {
|
|
2535
|
-
if (typeof details === "object" && details !== null && !Array.isArray(details)) {
|
|
2536
|
-
return { ...details, compactionEnvelope: envelope };
|
|
2537
|
-
}
|
|
2538
|
-
return {
|
|
2539
|
-
compactionEnvelope: envelope,
|
|
2540
|
-
...(details === undefined ? {} : { resultDetails: details }),
|
|
2541
|
-
};
|
|
2380
|
+
return this._compactionService.beginTransaction(compactionModel, emergency);
|
|
2542
2381
|
}
|
|
2543
2382
|
_commitCompaction(begun, result, fromExtension) {
|
|
2544
|
-
|
|
2545
|
-
throw new Error("Compaction first-kept entry is outside the captured source");
|
|
2546
|
-
}
|
|
2547
|
-
const committed = this.sessionManager.withCompactionCommitLock(() => {
|
|
2548
|
-
const current = this._captureCompactionState();
|
|
2549
|
-
const barrier = this._evaluateCompactionBarrier(current, false);
|
|
2550
|
-
const decision = decideCompactionCommit({
|
|
2551
|
-
transaction: begun.transaction,
|
|
2552
|
-
currentRevision: current.revision,
|
|
2553
|
-
currentSource: current.source,
|
|
2554
|
-
barrier,
|
|
2555
|
-
priorCommittedSourceDigests: this._priorCommittedCompactionSourceDigests(),
|
|
2556
|
-
});
|
|
2557
|
-
switch (decision.decision) {
|
|
2558
|
-
case "duplicate":
|
|
2559
|
-
throw new Error("This exact compaction source was already compacted");
|
|
2560
|
-
case "stale":
|
|
2561
|
-
throw new Error(`Session changed during compaction (${decision.reason}); generated summary was discarded`);
|
|
2562
|
-
case "defer":
|
|
2563
|
-
case "fail_closed":
|
|
2564
|
-
throw this._barrierError(barrier);
|
|
2565
|
-
case "commit": {
|
|
2566
|
-
const envelope = createCompactionEnvelope({
|
|
2567
|
-
transaction: begun.transaction,
|
|
2568
|
-
decision,
|
|
2569
|
-
summary: result.summary,
|
|
2570
|
-
summarySha256: createHash("sha256").update(result.summary, "utf8").digest("hex"),
|
|
2571
|
-
});
|
|
2572
|
-
const entryId = this.sessionManager.appendCompaction(result.summary, result.firstKeptEntryId, result.tokensBefore, this._detailsWithCompactionEnvelope(result.details, envelope), fromExtension);
|
|
2573
|
-
const entry = this.sessionManager.getEntry(entryId);
|
|
2574
|
-
if (!entry || entry.type !== "compaction") {
|
|
2575
|
-
throw new Error("Compaction commit did not produce a compaction entry");
|
|
2576
|
-
}
|
|
2577
|
-
return { entry, envelope };
|
|
2578
|
-
}
|
|
2579
|
-
}
|
|
2580
|
-
});
|
|
2581
|
-
this._recordCompactionCommitForHysteresis();
|
|
2582
|
-
return committed;
|
|
2383
|
+
return this._compactionService.commit(begun, result, fromExtension);
|
|
2583
2384
|
}
|
|
2584
2385
|
_pendingToolResultReserve(settings) {
|
|
2585
2386
|
const pendingIds = this.agent.state.pendingToolCalls;
|
|
@@ -2743,7 +2544,7 @@ export class AgentSession {
|
|
|
2743
2544
|
}
|
|
2744
2545
|
else {
|
|
2745
2546
|
// Generate compaction result
|
|
2746
|
-
const result = await compact(preparation, compactionModel, apiKey, headers, customInstructions, this._compactionAbortController.signal, this.thinkingLevel, this.agent.streamFn);
|
|
2547
|
+
const result = await compact(preparation, compactionModel, apiKey, headers, customInstructions, this._compactionAbortController.signal, this.thinkingLevel, this.agent.streamFn, this.settingsManager.getRetrySettings(), this._summarizationRetryCallbacks({ source: "compaction", reason: "manual" }));
|
|
2747
2548
|
summary = result.summary;
|
|
2748
2549
|
firstKeptEntryId = result.firstKeptEntryId;
|
|
2749
2550
|
tokensBefore = result.tokensBefore;
|
|
@@ -3019,7 +2820,7 @@ export class AgentSession {
|
|
|
3019
2820
|
}
|
|
3020
2821
|
else {
|
|
3021
2822
|
// Generate compaction result
|
|
3022
|
-
const compactResult = await compact(preparation, compactionModel, apiKey, headers, undefined, this._autoCompactionAbortController.signal, this.thinkingLevel, this.agent.streamFn);
|
|
2823
|
+
const compactResult = await compact(preparation, compactionModel, apiKey, headers, undefined, this._autoCompactionAbortController.signal, this.thinkingLevel, this.agent.streamFn, this.settingsManager.getRetrySettings(), this._summarizationRetryCallbacks({ source: "compaction", reason }));
|
|
3023
2824
|
summary = compactResult.summary;
|
|
3024
2825
|
firstKeptEntryId = compactResult.firstKeptEntryId;
|
|
3025
2826
|
tokensBefore = compactResult.tokensBefore;
|
|
@@ -3050,7 +2851,21 @@ export class AgentSession {
|
|
|
3050
2851
|
fromExtension,
|
|
3051
2852
|
});
|
|
3052
2853
|
}
|
|
3053
|
-
|
|
2854
|
+
// Whether the agent loop will resume after this compaction. Shared by
|
|
2855
|
+
// the compaction_end event (so the TUI flushes its queued messages
|
|
2856
|
+
// correctly) and the return value below.
|
|
2857
|
+
const resumeMessages = this.agent.state.messages;
|
|
2858
|
+
let lastAssistantMsg;
|
|
2859
|
+
for (let i = resumeMessages.length - 1; i >= 0; i--) {
|
|
2860
|
+
const candidate = resumeMessages[i];
|
|
2861
|
+
if (candidate.role === "assistant") {
|
|
2862
|
+
lastAssistantMsg = candidate;
|
|
2863
|
+
break;
|
|
2864
|
+
}
|
|
2865
|
+
}
|
|
2866
|
+
const endedCleanly = lastAssistantMsg?.stopReason === "stop";
|
|
2867
|
+
const willResume = this.agent.hasQueuedMessages() || !endedCleanly;
|
|
2868
|
+
const emitWillRetry = compactionEmitWillRetry(willRetry, willResume);
|
|
3054
2869
|
this._emit({ type: "compaction_end", reason, result, aborted: false, willRetry: emitWillRetry });
|
|
3055
2870
|
if (willRetry) {
|
|
3056
2871
|
const messages = this.agent.state.messages;
|
|
@@ -3060,9 +2875,16 @@ export class AgentSession {
|
|
|
3060
2875
|
}
|
|
3061
2876
|
return true;
|
|
3062
2877
|
}
|
|
3063
|
-
//
|
|
3064
|
-
//
|
|
3065
|
-
|
|
2878
|
+
// Resume the agent loop after a successful auto-compaction unless the
|
|
2879
|
+
// agent had already finished cleanly. Previously this returned
|
|
2880
|
+
// `hasQueuedMessages()`, which made the agent stall right after the
|
|
2881
|
+
// purple "Auto-compacting..." indicator whenever no steer/follow-up
|
|
2882
|
+
// messages were queued — even when the last turn was cut off mid-task
|
|
2883
|
+
// (stopReason toolUse/length/error/aborted). Continue in that case so
|
|
2884
|
+
// the task runs to completion on the compacted context. Hysteresis
|
|
2885
|
+
// (disarmed until context drops below rearmRatio) prevents an immediate
|
|
2886
|
+
// re-trigger; a clean "stop" with nothing queued still ends the loop.
|
|
2887
|
+
return willResume;
|
|
3066
2888
|
}
|
|
3067
2889
|
catch (error) {
|
|
3068
2890
|
const errorMessage = error instanceof Error ? error.message : "compaction failed";
|
|
@@ -3406,6 +3228,29 @@ export class AgentSession {
|
|
|
3406
3228
|
commandPrefix: shellCommandPrefix,
|
|
3407
3229
|
shellPath,
|
|
3408
3230
|
...(loadoutAccessGuard ? { loadoutAccessGuard } : {}),
|
|
3231
|
+
...(() => {
|
|
3232
|
+
const sandboxPolicy = this._getBashSandboxPreflight();
|
|
3233
|
+
return sandboxPolicy ? { sandboxPolicy } : {};
|
|
3234
|
+
})(),
|
|
3235
|
+
...(() => {
|
|
3236
|
+
const evidenceExecutor = this._getVerifiedEvidenceExecutor();
|
|
3237
|
+
if (!evidenceExecutor || !this._replayGoalId)
|
|
3238
|
+
return {};
|
|
3239
|
+
const { shell } = getShellConfig(shellPath);
|
|
3240
|
+
const inner = createLocalBashOperations({
|
|
3241
|
+
...(shellPath !== undefined ? { shellPath } : {}),
|
|
3242
|
+
sandboxPolicy: this._getBashSandboxPreflight(),
|
|
3243
|
+
});
|
|
3244
|
+
return {
|
|
3245
|
+
operations: createVerifiedBashOperations(inner, {
|
|
3246
|
+
evidenceExecutor,
|
|
3247
|
+
goalId: this._replayGoalId,
|
|
3248
|
+
...(this._replayLaneId !== undefined ? { laneId: this._replayLaneId } : {}),
|
|
3249
|
+
shell,
|
|
3250
|
+
workspaceScope: resolveSessionWorkspaceScope(this._cwd),
|
|
3251
|
+
}),
|
|
3252
|
+
};
|
|
3253
|
+
})(),
|
|
3409
3254
|
},
|
|
3410
3255
|
edit: loadoutWriteOptions,
|
|
3411
3256
|
write: loadoutWriteOptions,
|
|
@@ -3425,7 +3270,7 @@ export class AgentSession {
|
|
|
3425
3270
|
this._applyExtensionBindings(this._extensionRunner);
|
|
3426
3271
|
const defaultActiveToolNames = this._baseToolsOverride
|
|
3427
3272
|
? Object.keys(this._baseToolsOverride)
|
|
3428
|
-
: ["read", "bash", "edit", "write"];
|
|
3273
|
+
: ["read", "bash", "edit", "write", "diagnostics"];
|
|
3429
3274
|
const baseActiveToolNames = options.activeToolNames ?? defaultActiveToolNames;
|
|
3430
3275
|
this._refreshToolRegistry({
|
|
3431
3276
|
activeToolNames: baseActiveToolNames,
|
|
@@ -3476,6 +3321,34 @@ export class AgentSession {
|
|
|
3476
3321
|
// Shared contract with provider-resilience.ts (Fable safety stop, K3 orphan tool_call_id, terminated).
|
|
3477
3322
|
return isTransientProviderErrorMessage(err);
|
|
3478
3323
|
}
|
|
3324
|
+
/**
|
|
3325
|
+
* Retry policy + callbacks shared by compaction and branch-summary summarization calls.
|
|
3326
|
+
* Uses the same `settings.retry` budget/backoff as agent-turn retries so a single transient
|
|
3327
|
+
* stream drop no longer fails the whole operation. `source` carries the context
|
|
3328
|
+
* the TUI needs to render the retry and recreate the underlying indicator.
|
|
3329
|
+
*/
|
|
3330
|
+
_summarizationRetryCallbacks(source) {
|
|
3331
|
+
return {
|
|
3332
|
+
onRetryScheduled: (attempt, maxAttempts, delayMs, errorMessage) => {
|
|
3333
|
+
this._emit({
|
|
3334
|
+
type: "summarization_retry_scheduled",
|
|
3335
|
+
attempt,
|
|
3336
|
+
maxAttempts,
|
|
3337
|
+
delayMs,
|
|
3338
|
+
errorMessage,
|
|
3339
|
+
});
|
|
3340
|
+
},
|
|
3341
|
+
onRetryAttemptStart: () => {
|
|
3342
|
+
this._emit({
|
|
3343
|
+
type: "summarization_retry_attempt_start",
|
|
3344
|
+
...source,
|
|
3345
|
+
});
|
|
3346
|
+
},
|
|
3347
|
+
onRetryFinished: () => {
|
|
3348
|
+
this._emit({ type: "summarization_retry_finished" });
|
|
3349
|
+
},
|
|
3350
|
+
};
|
|
3351
|
+
}
|
|
3479
3352
|
/**
|
|
3480
3353
|
* Prepare a retryable error for continuation with exponential backoff.
|
|
3481
3354
|
* @returns true if the caller should continue the agent, false otherwise
|
|
@@ -3504,8 +3377,9 @@ export class AgentSession {
|
|
|
3504
3377
|
await this.setModel(next);
|
|
3505
3378
|
}
|
|
3506
3379
|
/**
|
|
3507
|
-
*
|
|
3508
|
-
* Retrying the same model
|
|
3380
|
+
* Content/safety stops (stop_reason=refusal) are often false positives on coding turns
|
|
3381
|
+
* for Fable AND Claude Opus/Sonnet. Retrying the same model usually reprints the refusal —
|
|
3382
|
+
* switch via provider-resilience chain first. Failover targets still skip sticky models.
|
|
3509
3383
|
*/
|
|
3510
3384
|
async _maybeFailoverFromSafetyStop(message) {
|
|
3511
3385
|
const resilience = resolveProviderResilience(this.settingsManager.getProviderResilienceSettings());
|
|
@@ -3514,11 +3388,15 @@ export class AgentSession {
|
|
|
3514
3388
|
if (!isContentSafetyStopMessage(message.errorMessage))
|
|
3515
3389
|
return undefined;
|
|
3516
3390
|
const current = this.model;
|
|
3517
|
-
|
|
3518
|
-
|
|
3519
|
-
|
|
3520
|
-
|
|
3391
|
+
// v10.0-Ω: do NOT gate on isStickySafetyModel(source).
|
|
3392
|
+
// claude-opus-5 emits the same stop_reason=refusal FP; same-model retry is useless.
|
|
3393
|
+
// v10.3-Ω: mark the model that just refused so we never re-pick it this turn.
|
|
3394
|
+
if (current)
|
|
3395
|
+
this._refusedModels.add(`${current.provider}/${current.id}`);
|
|
3521
3396
|
const pick = pickFailoverCandidate(resilience.failoverCandidates, current ? { provider: current.provider, id: current.id } : undefined, (c) => {
|
|
3397
|
+
// Skip any model that already refused/failed this turn → advance the chain.
|
|
3398
|
+
if (this._refusedModels.has(`${c.provider}/${c.id}`))
|
|
3399
|
+
return false;
|
|
3522
3400
|
const next = this._modelRegistry.find(c.provider, c.id);
|
|
3523
3401
|
return !!next && this._modelRegistry.hasConfiguredAuth(next);
|
|
3524
3402
|
});
|
|
@@ -3532,6 +3410,8 @@ export class AgentSession {
|
|
|
3532
3410
|
return `${next.provider}/${next.id}`;
|
|
3533
3411
|
}
|
|
3534
3412
|
catch {
|
|
3413
|
+
// setModel failed (auth/guard) — blacklist so next retry advances further.
|
|
3414
|
+
this._refusedModels.add(`${pick.provider}/${pick.id}`);
|
|
3535
3415
|
return undefined;
|
|
3536
3416
|
}
|
|
3537
3417
|
}
|
|
@@ -3546,7 +3426,7 @@ export class AgentSession {
|
|
|
3546
3426
|
this._retryAttempt--;
|
|
3547
3427
|
return false;
|
|
3548
3428
|
}
|
|
3549
|
-
// Content/safety stop
|
|
3429
|
+
// Content/safety stop (Fable/Opus/Sonnet): switch model BEFORE delay so retry is not same-model refusal.
|
|
3550
3430
|
const failoverTo = await this._maybeFailoverFromSafetyStop(message);
|
|
3551
3431
|
// Safety stops are usually immediate false positives — short delay after failover, full backoff otherwise.
|
|
3552
3432
|
const delayMs = failoverTo
|
|
@@ -3576,6 +3456,7 @@ export class AgentSession {
|
|
|
3576
3456
|
// Aborted during sleep - emit end event so UI can clean up
|
|
3577
3457
|
const attempt = this._retryAttempt;
|
|
3578
3458
|
this._retryAttempt = 0;
|
|
3459
|
+
this._refusedModels.clear();
|
|
3579
3460
|
this._emit({
|
|
3580
3461
|
type: "auto_retry_end",
|
|
3581
3462
|
success: false,
|
|
@@ -3623,120 +3504,35 @@ export class AgentSession {
|
|
|
3623
3504
|
* @param options.sandboxPolicy Trusted sandbox preflight for local bash execution (never sourced from RPC payloads)
|
|
3624
3505
|
*/
|
|
3625
3506
|
async executeBash(command, onChunk, options) {
|
|
3626
|
-
|
|
3627
|
-
const prefix = this.settingsManager.getShellCommandPrefix();
|
|
3628
|
-
const shellPath = this.settingsManager.getShellPath();
|
|
3629
|
-
const resolvedCommand = prefix ? `${prefix}\n${command}` : command;
|
|
3630
|
-
if (this._loadoutAccessPolicy) {
|
|
3631
|
-
assertLoadoutAccess((request) => decideLoadoutAccess(this._loadoutAccessPolicy, request), { operation: "execute", toolName: "bash", command: resolvedCommand });
|
|
3632
|
-
}
|
|
3633
|
-
// Non-negotiable safety floor for headless callers (RPC bash): hard-deny
|
|
3634
|
-
// block-tier commands before any shell is spawned.
|
|
3635
|
-
if (options?.safetyGate === "headless") {
|
|
3636
|
-
const floorVerdict = classifyShellCommand(command);
|
|
3637
|
-
if (floorVerdict.risk === "block") {
|
|
3638
|
-
throw new Error(`OMK §0.1 safety floor blocked bash: [${floorVerdict.rule}] ${floorVerdict.reason}`);
|
|
3639
|
-
}
|
|
3640
|
-
}
|
|
3641
|
-
// Command-safety parity for non-interactive callers (RPC bash). Interactive
|
|
3642
|
-
// `!`/`!!` bash is gated earlier through the user_bash extension event, which
|
|
3643
|
-
// keeps its prompt-based approval semantics, so it does not pass safetyGate and
|
|
3644
|
-
// is never double-prompted here. confirm/block-tier verdicts deny headlessly;
|
|
3645
|
-
// the EFFECTIVE command (after the shell command prefix) is classified.
|
|
3646
|
-
if (options?.safetyGate === "headless") {
|
|
3647
|
-
const decision = await evaluateCommandGate(resolvedCommand, {
|
|
3648
|
-
hasUI: false,
|
|
3649
|
-
headlessConfirmPolicy: isCommandSafetyAssumeYesEnabled() ? "allow" : "deny",
|
|
3650
|
-
});
|
|
3651
|
-
if (decision?.deny) {
|
|
3652
|
-
const blocked = buildBlockedBashResult(decision.reason);
|
|
3653
|
-
this.recordBashResult(command, blocked, options);
|
|
3654
|
-
return blocked;
|
|
3655
|
-
}
|
|
3656
|
-
}
|
|
3657
|
-
this._bashAbortController = new AbortController();
|
|
3658
|
-
try {
|
|
3659
|
-
try {
|
|
3660
|
-
const result = await executeBashWithOperations(resolvedCommand, this.sessionManager.getCwd(), options?.operations ??
|
|
3661
|
-
createLocalBashOperations({
|
|
3662
|
-
shellPath,
|
|
3663
|
-
sandboxPolicy: this._getBashSandboxPreflight(options?.sandboxPolicy),
|
|
3664
|
-
}), {
|
|
3665
|
-
onChunk,
|
|
3666
|
-
signal: this._bashAbortController.signal,
|
|
3667
|
-
});
|
|
3668
|
-
this.recordBashResult(command, result, options);
|
|
3669
|
-
return result;
|
|
3670
|
-
}
|
|
3671
|
-
catch (error) {
|
|
3672
|
-
if (!isSandboxDeniedError(error)) {
|
|
3673
|
-
throw error;
|
|
3674
|
-
}
|
|
3675
|
-
const blocked = buildSandboxDeniedBashResult(error.message);
|
|
3676
|
-
this.recordBashResult(command, blocked, options);
|
|
3677
|
-
return blocked;
|
|
3678
|
-
}
|
|
3679
|
-
}
|
|
3680
|
-
finally {
|
|
3681
|
-
this._bashAbortController = undefined;
|
|
3682
|
-
}
|
|
3507
|
+
return this._bashService.executeBash(command, onChunk, options);
|
|
3683
3508
|
}
|
|
3684
3509
|
/**
|
|
3685
3510
|
* Record a bash execution result in session history.
|
|
3686
3511
|
* Used by executeBash and by extensions that handle bash execution themselves.
|
|
3687
3512
|
*/
|
|
3688
3513
|
recordBashResult(command, result, options) {
|
|
3689
|
-
|
|
3690
|
-
role: "bashExecution",
|
|
3691
|
-
command,
|
|
3692
|
-
output: result.output,
|
|
3693
|
-
exitCode: result.exitCode,
|
|
3694
|
-
cancelled: result.cancelled,
|
|
3695
|
-
truncated: result.truncated,
|
|
3696
|
-
fullOutputPath: result.fullOutputPath,
|
|
3697
|
-
timestamp: Date.now(),
|
|
3698
|
-
excludeFromContext: options?.excludeFromContext,
|
|
3699
|
-
};
|
|
3700
|
-
// If agent is streaming, defer adding to avoid breaking tool_use/tool_result ordering
|
|
3701
|
-
if (this.isStreaming) {
|
|
3702
|
-
// Queue for later - will be flushed on agent_end
|
|
3703
|
-
this._pendingBashMessages.push(bashMessage);
|
|
3704
|
-
}
|
|
3705
|
-
else {
|
|
3706
|
-
// Add to agent state immediately
|
|
3707
|
-
this.agent.state.messages.push(bashMessage);
|
|
3708
|
-
// Save to session
|
|
3709
|
-
this.sessionManager.appendMessage(bashMessage);
|
|
3710
|
-
}
|
|
3514
|
+
this._bashService.recordBashResult(command, result, options);
|
|
3711
3515
|
}
|
|
3712
3516
|
/**
|
|
3713
3517
|
* Cancel running bash command.
|
|
3714
3518
|
*/
|
|
3715
3519
|
abortBash() {
|
|
3716
|
-
this.
|
|
3520
|
+
this._bashService.abortBash();
|
|
3717
3521
|
}
|
|
3718
3522
|
/** Whether a bash command is currently running */
|
|
3719
3523
|
get isBashRunning() {
|
|
3720
|
-
return this.
|
|
3524
|
+
return this._bashService.isBashRunning;
|
|
3721
3525
|
}
|
|
3722
3526
|
/** Whether there are pending bash messages waiting to be flushed */
|
|
3723
3527
|
get hasPendingBashMessages() {
|
|
3724
|
-
return this.
|
|
3528
|
+
return this._bashService.hasPendingBashMessages;
|
|
3725
3529
|
}
|
|
3726
3530
|
/**
|
|
3727
3531
|
* Flush pending bash messages to agent state and session.
|
|
3728
3532
|
* Called after agent turn completes to maintain proper message ordering.
|
|
3729
3533
|
*/
|
|
3730
3534
|
_flushPendingBashMessages() {
|
|
3731
|
-
|
|
3732
|
-
return;
|
|
3733
|
-
for (const bashMessage of this._pendingBashMessages) {
|
|
3734
|
-
// Add to agent state
|
|
3735
|
-
this.agent.state.messages.push(bashMessage);
|
|
3736
|
-
// Save to session
|
|
3737
|
-
this.sessionManager.appendMessage(bashMessage);
|
|
3738
|
-
}
|
|
3739
|
-
this._pendingBashMessages = [];
|
|
3535
|
+
this._bashService.flushPending();
|
|
3740
3536
|
}
|
|
3741
3537
|
// =========================================================================
|
|
3742
3538
|
// Session Management
|
|
@@ -3838,6 +3634,8 @@ export class AgentSession {
|
|
|
3838
3634
|
replaceInstructions,
|
|
3839
3635
|
reserveTokens: branchSummarySettings.reserveTokens,
|
|
3840
3636
|
streamFn: this.agent.streamFn,
|
|
3637
|
+
retry: this.settingsManager.getRetrySettings(),
|
|
3638
|
+
callbacks: this._summarizationRetryCallbacks({ source: "branchSummary" }),
|
|
3841
3639
|
});
|
|
3842
3640
|
if (result.aborted) {
|
|
3843
3641
|
return { cancelled: true, aborted: true };
|