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.
Files changed (173) hide show
  1. package/CHANGELOG.md +38 -1
  2. package/README.md +2 -2
  3. package/dist/core/agent-session.d.ts +36 -14
  4. package/dist/core/agent-session.d.ts.map +1 -1
  5. package/dist/core/agent-session.js +155 -357
  6. package/dist/core/agent-session.js.map +1 -1
  7. package/dist/core/compaction/branch-summarization.d.ts +5 -1
  8. package/dist/core/compaction/branch-summarization.d.ts.map +1 -1
  9. package/dist/core/compaction/branch-summarization.js +5 -7
  10. package/dist/core/compaction/branch-summarization.js.map +1 -1
  11. package/dist/core/compaction/compaction.d.ts +12 -3
  12. package/dist/core/compaction/compaction.d.ts.map +1 -1
  13. package/dist/core/compaction/compaction.js +28 -15
  14. package/dist/core/compaction/compaction.js.map +1 -1
  15. package/dist/core/extensions/builtin/command-safety-gate.d.ts +8 -0
  16. package/dist/core/extensions/builtin/command-safety-gate.d.ts.map +1 -1
  17. package/dist/core/extensions/builtin/command-safety-gate.js +23 -0
  18. package/dist/core/extensions/builtin/command-safety-gate.js.map +1 -1
  19. package/dist/core/extensions/runner.d.ts.map +1 -1
  20. package/dist/core/extensions/runner.js +4 -0
  21. package/dist/core/extensions/runner.js.map +1 -1
  22. package/dist/core/extensions/types.d.ts +2 -0
  23. package/dist/core/extensions/types.d.ts.map +1 -1
  24. package/dist/core/extensions/types.js.map +1 -1
  25. package/dist/core/keybindings.d.ts +5 -0
  26. package/dist/core/keybindings.d.ts.map +1 -1
  27. package/dist/core/keybindings.js +5 -0
  28. package/dist/core/keybindings.js.map +1 -1
  29. package/dist/core/model-resolver.d.ts +41 -3
  30. package/dist/core/model-resolver.d.ts.map +1 -1
  31. package/dist/core/model-resolver.js +17 -9
  32. package/dist/core/model-resolver.js.map +1 -1
  33. package/dist/core/package-manager.d.ts.map +1 -1
  34. package/dist/core/package-manager.js +11 -1
  35. package/dist/core/package-manager.js.map +1 -1
  36. package/dist/core/provider-resilience.d.ts.map +1 -1
  37. package/dist/core/provider-resilience.js +3 -1
  38. package/dist/core/provider-resilience.js.map +1 -1
  39. package/dist/core/reasoning-router-v4-weights.d.ts.map +1 -1
  40. package/dist/core/reasoning-router-v4-weights.js +9 -3
  41. package/dist/core/reasoning-router-v4-weights.js.map +1 -1
  42. package/dist/core/reasoning-router-v4.d.ts.map +1 -1
  43. package/dist/core/reasoning-router-v4.js +94 -24
  44. package/dist/core/reasoning-router-v4.js.map +1 -1
  45. package/dist/core/resource-loader.d.ts.map +1 -1
  46. package/dist/core/resource-loader.js +11 -5
  47. package/dist/core/resource-loader.js.map +1 -1
  48. package/dist/core/sandbox/default-policy.d.ts +14 -0
  49. package/dist/core/sandbox/default-policy.d.ts.map +1 -0
  50. package/dist/core/sandbox/default-policy.js +58 -0
  51. package/dist/core/sandbox/default-policy.js.map +1 -0
  52. package/dist/core/sdk.d.ts.map +1 -1
  53. package/dist/core/sdk.js +1 -1
  54. package/dist/core/sdk.js.map +1 -1
  55. package/dist/core/session-bash-runtime.d.ts +62 -0
  56. package/dist/core/session-bash-runtime.d.ts.map +1 -0
  57. package/dist/core/session-bash-runtime.js +167 -0
  58. package/dist/core/session-bash-runtime.js.map +1 -0
  59. package/dist/core/session-bash-service.d.ts +61 -0
  60. package/dist/core/session-bash-service.d.ts.map +1 -0
  61. package/dist/core/session-bash-service.js +155 -0
  62. package/dist/core/session-bash-service.js.map +1 -0
  63. package/dist/core/session-compaction-service.d.ts +48 -0
  64. package/dist/core/session-compaction-service.d.ts.map +1 -0
  65. package/dist/core/session-compaction-service.js +250 -0
  66. package/dist/core/session-compaction-service.js.map +1 -0
  67. package/dist/core/session-file-compaction.d.ts.map +1 -1
  68. package/dist/core/session-file-compaction.js +39 -5
  69. package/dist/core/session-file-compaction.js.map +1 -1
  70. package/dist/core/session-termination.d.ts.map +1 -1
  71. package/dist/core/session-termination.js +1 -1
  72. package/dist/core/session-termination.js.map +1 -1
  73. package/dist/core/settings-manager.d.ts +3 -0
  74. package/dist/core/settings-manager.d.ts.map +1 -1
  75. package/dist/core/settings-manager.js +8 -0
  76. package/dist/core/settings-manager.js.map +1 -1
  77. package/dist/core/skills-catalog-cache.d.ts +25 -0
  78. package/dist/core/skills-catalog-cache.d.ts.map +1 -0
  79. package/dist/core/skills-catalog-cache.js +123 -0
  80. package/dist/core/skills-catalog-cache.js.map +1 -0
  81. package/dist/core/skills.d.ts +6 -0
  82. package/dist/core/skills.d.ts.map +1 -1
  83. package/dist/core/skills.js +25 -4
  84. package/dist/core/skills.js.map +1 -1
  85. package/dist/core/system-prompt.d.ts.map +1 -1
  86. package/dist/core/system-prompt.js +2 -2
  87. package/dist/core/system-prompt.js.map +1 -1
  88. package/dist/core/tools/bash.d.ts +5 -0
  89. package/dist/core/tools/bash.d.ts.map +1 -1
  90. package/dist/core/tools/bash.js +41 -13
  91. package/dist/core/tools/bash.js.map +1 -1
  92. package/dist/core/tools/diagnostics.d.ts +29 -0
  93. package/dist/core/tools/diagnostics.d.ts.map +1 -0
  94. package/dist/core/tools/diagnostics.js +325 -0
  95. package/dist/core/tools/diagnostics.js.map +1 -0
  96. package/dist/core/tools/index.d.ts +2 -1
  97. package/dist/core/tools/index.d.ts.map +1 -1
  98. package/dist/core/tools/index.js +18 -1
  99. package/dist/core/tools/index.js.map +1 -1
  100. package/dist/core/verified-bash-adapter.d.ts +18 -1
  101. package/dist/core/verified-bash-adapter.d.ts.map +1 -1
  102. package/dist/core/verified-bash-adapter.js +38 -1
  103. package/dist/core/verified-bash-adapter.js.map +1 -1
  104. package/dist/core/verified-bash-runtime.d.ts +13 -0
  105. package/dist/core/verified-bash-runtime.d.ts.map +1 -0
  106. package/dist/core/verified-bash-runtime.js +81 -0
  107. package/dist/core/verified-bash-runtime.js.map +1 -0
  108. package/dist/index.d.ts +4 -2
  109. package/dist/index.d.ts.map +1 -1
  110. package/dist/index.js +4 -2
  111. package/dist/index.js.map +1 -1
  112. package/dist/modes/interactive/components/control-panel-box.d.ts +27 -0
  113. package/dist/modes/interactive/components/control-panel-box.d.ts.map +1 -1
  114. package/dist/modes/interactive/components/control-panel-box.js +60 -8
  115. package/dist/modes/interactive/components/control-panel-box.js.map +1 -1
  116. package/dist/modes/interactive/components/control-panel-layout.d.ts.map +1 -1
  117. package/dist/modes/interactive/components/control-panel-layout.js +100 -45
  118. package/dist/modes/interactive/components/control-panel-layout.js.map +1 -1
  119. package/dist/modes/interactive/components/control-panel-runtime-status.d.ts +9 -0
  120. package/dist/modes/interactive/components/control-panel-runtime-status.d.ts.map +1 -1
  121. package/dist/modes/interactive/components/control-panel-runtime-status.js +10 -8
  122. package/dist/modes/interactive/components/control-panel-runtime-status.js.map +1 -1
  123. package/dist/modes/interactive/components/control-panel-sparkles.d.ts +5 -0
  124. package/dist/modes/interactive/components/control-panel-sparkles.d.ts.map +1 -1
  125. package/dist/modes/interactive/components/control-panel-sparkles.js +14 -9
  126. package/dist/modes/interactive/components/control-panel-sparkles.js.map +1 -1
  127. package/dist/modes/interactive/components/footer.d.ts +10 -0
  128. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  129. package/dist/modes/interactive/components/footer.js +3 -3
  130. package/dist/modes/interactive/components/footer.js.map +1 -1
  131. package/dist/modes/interactive/components/status-sidebar.d.ts +49 -0
  132. package/dist/modes/interactive/components/status-sidebar.d.ts.map +1 -0
  133. package/dist/modes/interactive/components/status-sidebar.js +253 -0
  134. package/dist/modes/interactive/components/status-sidebar.js.map +1 -0
  135. package/dist/modes/interactive/interactive-mode.d.ts +9 -0
  136. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  137. package/dist/modes/interactive/interactive-mode.js +94 -20
  138. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  139. package/dist/modes/interactive/theme/omk-neon-control.json +13 -13
  140. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  141. package/dist/modes/rpc/rpc-mode.js +1 -0
  142. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  143. package/dist/types/evidence.d.ts +1 -1
  144. package/dist/types/evidence.d.ts.map +1 -1
  145. package/dist/types/evidence.js.map +1 -1
  146. package/dist/utils/clipboard.d.ts.map +1 -1
  147. package/dist/utils/clipboard.js +19 -8
  148. package/dist/utils/clipboard.js.map +1 -1
  149. package/dist/utils/frontmatter.d.ts.map +1 -1
  150. package/dist/utils/frontmatter.js +31 -5
  151. package/dist/utils/frontmatter.js.map +1 -1
  152. package/docs/compaction.md +7 -0
  153. package/docs/docs.json +4 -0
  154. package/docs/environment-variables.md +94 -0
  155. package/docs/index.md +1 -0
  156. package/docs/rpc.md +5 -2
  157. package/docs/sdk.md +10 -4
  158. package/docs/settings.md +1 -0
  159. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  160. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  161. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  162. package/examples/extensions/gondolin/package-lock.json +2 -2
  163. package/examples/extensions/gondolin/package.json +1 -1
  164. package/examples/extensions/sandbox/package-lock.json +2 -2
  165. package/examples/extensions/sandbox/package.json +1 -1
  166. package/examples/extensions/subagent/agents.test.ts +86 -0
  167. package/examples/extensions/subagent/agents.ts +18 -4
  168. package/examples/extensions/subagent/capabilities.ts +15 -1
  169. package/examples/extensions/subagent/domain-profiles.ts +272 -0
  170. package/examples/extensions/with-deps/package-lock.json +2 -2
  171. package/examples/extensions/with-deps/package.json +1 -1
  172. package/npm-shrinkwrap.json +15 -15
  173. 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 { sanitizeBinaryOutput } from "../utils/shell.js";
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 { assertLoadoutAccess, decideLoadoutAccess } from "./loadout-access-policy.js";
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 { detectSandboxBackend } from "./sandbox/backend.js";
58
- import { inspectSessionIntegrity } from "./session-integrity.js";
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
- _bashAbortController = undefined;
291
- _pendingBashMessages = [];
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
- const preflight = override ?? this._configuredBashSandboxPreflight;
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
- let capture = this._captureCompactionState();
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
- if (!begun.transaction.source.entryIds.includes(result.firstKeptEntryId)) {
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
- const emitWillRetry = compactionEmitWillRetry(willRetry, this.agent.hasQueuedMessages());
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
- // Auto-compaction can complete while follow-up/steering/custom messages are waiting.
3064
- // Continue once so queued messages are delivered.
3065
- return this.agent.hasQueuedMessages();
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
- * Sticky safety models (e.g. claude-fable-5) often false-positive on coding turns.
3508
- * Retrying the same model repeats the refusal — switch via provider-resilience chain first.
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
- const currentId = message.model || current?.id || "";
3518
- const currentProvider = message.provider || current?.provider || "";
3519
- if (!isStickySafetyModel(currentId, currentProvider))
3520
- return undefined;
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 on Fable: switch model BEFORE delay so the retry is not another Fable refusal.
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
- // Apply command prefix if configured (e.g., "shopt -s expand_aliases" for alias support)
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
- const bashMessage = {
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._bashAbortController?.abort();
3520
+ this._bashService.abortBash();
3717
3521
  }
3718
3522
  /** Whether a bash command is currently running */
3719
3523
  get isBashRunning() {
3720
- return this._bashAbortController !== undefined;
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._pendingBashMessages.length > 0;
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
- if (this._pendingBashMessages.length === 0)
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 };