open-multi-agent-kit 0.93.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 (82) hide show
  1. package/CHANGELOG.md +23 -1
  2. package/README.md +2 -2
  3. package/dist/core/agent-session.d.ts +7 -14
  4. package/dist/core/agent-session.d.ts.map +1 -1
  5. package/dist/core/agent-session.js +78 -347
  6. package/dist/core/agent-session.js.map +1 -1
  7. package/dist/core/package-manager.d.ts.map +1 -1
  8. package/dist/core/package-manager.js +11 -1
  9. package/dist/core/package-manager.js.map +1 -1
  10. package/dist/core/sandbox/default-policy.d.ts +14 -0
  11. package/dist/core/sandbox/default-policy.d.ts.map +1 -0
  12. package/dist/core/sandbox/default-policy.js +58 -0
  13. package/dist/core/sandbox/default-policy.js.map +1 -0
  14. package/dist/core/sdk.d.ts.map +1 -1
  15. package/dist/core/sdk.js +1 -1
  16. package/dist/core/sdk.js.map +1 -1
  17. package/dist/core/session-bash-runtime.d.ts +62 -0
  18. package/dist/core/session-bash-runtime.d.ts.map +1 -0
  19. package/dist/core/session-bash-runtime.js +167 -0
  20. package/dist/core/session-bash-runtime.js.map +1 -0
  21. package/dist/core/session-bash-service.d.ts +61 -0
  22. package/dist/core/session-bash-service.d.ts.map +1 -0
  23. package/dist/core/session-bash-service.js +155 -0
  24. package/dist/core/session-bash-service.js.map +1 -0
  25. package/dist/core/session-compaction-service.d.ts +48 -0
  26. package/dist/core/session-compaction-service.d.ts.map +1 -0
  27. package/dist/core/session-compaction-service.js +250 -0
  28. package/dist/core/session-compaction-service.js.map +1 -0
  29. package/dist/core/skills-catalog-cache.d.ts +25 -0
  30. package/dist/core/skills-catalog-cache.d.ts.map +1 -0
  31. package/dist/core/skills-catalog-cache.js +123 -0
  32. package/dist/core/skills-catalog-cache.js.map +1 -0
  33. package/dist/core/skills.d.ts +6 -0
  34. package/dist/core/skills.d.ts.map +1 -1
  35. package/dist/core/skills.js +25 -4
  36. package/dist/core/skills.js.map +1 -1
  37. package/dist/core/system-prompt.d.ts.map +1 -1
  38. package/dist/core/system-prompt.js +1 -1
  39. package/dist/core/system-prompt.js.map +1 -1
  40. package/dist/core/tools/bash.d.ts +3 -0
  41. package/dist/core/tools/bash.d.ts.map +1 -1
  42. package/dist/core/tools/bash.js +3 -2
  43. package/dist/core/tools/bash.js.map +1 -1
  44. package/dist/core/tools/diagnostics.d.ts +29 -0
  45. package/dist/core/tools/diagnostics.d.ts.map +1 -0
  46. package/dist/core/tools/diagnostics.js +325 -0
  47. package/dist/core/tools/diagnostics.js.map +1 -0
  48. package/dist/core/tools/index.d.ts +2 -1
  49. package/dist/core/tools/index.d.ts.map +1 -1
  50. package/dist/core/tools/index.js +18 -1
  51. package/dist/core/tools/index.js.map +1 -1
  52. package/dist/core/verified-bash-adapter.d.ts +18 -1
  53. package/dist/core/verified-bash-adapter.d.ts.map +1 -1
  54. package/dist/core/verified-bash-adapter.js +38 -1
  55. package/dist/core/verified-bash-adapter.js.map +1 -1
  56. package/dist/core/verified-bash-runtime.d.ts +13 -0
  57. package/dist/core/verified-bash-runtime.d.ts.map +1 -0
  58. package/dist/core/verified-bash-runtime.js +81 -0
  59. package/dist/core/verified-bash-runtime.js.map +1 -0
  60. package/dist/index.d.ts +4 -2
  61. package/dist/index.d.ts.map +1 -1
  62. package/dist/index.js +4 -2
  63. package/dist/index.js.map +1 -1
  64. package/dist/types/evidence.d.ts +1 -1
  65. package/dist/types/evidence.d.ts.map +1 -1
  66. package/dist/types/evidence.js.map +1 -1
  67. package/dist/utils/frontmatter.d.ts.map +1 -1
  68. package/dist/utils/frontmatter.js +31 -5
  69. package/dist/utils/frontmatter.js.map +1 -1
  70. package/docs/environment-variables.md +4 -0
  71. package/docs/sdk.md +10 -4
  72. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  73. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  74. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  75. package/examples/extensions/gondolin/package-lock.json +2 -2
  76. package/examples/extensions/gondolin/package.json +1 -1
  77. package/examples/extensions/sandbox/package-lock.json +2 -2
  78. package/examples/extensions/sandbox/package.json +1 -1
  79. package/examples/extensions/with-deps/package-lock.json +2 -2
  80. package/examples/extensions/with-deps/package.json +1 -1
  81. package/npm-shrinkwrap.json +15 -15
  82. package/package.json +5 -5
@@ -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
  }
@@ -290,10 +277,8 @@ export class AgentSession {
290
277
  // not re-pick the same candidate (claude-opus-5 → grok → grok → grok loop fix).
291
278
  _refusedModels = new Set();
292
279
  // Bash execution state
293
- _bashAbortController = undefined;
294
- _pendingBashMessages = [];
295
- _configuredBashSandboxPreflight = undefined;
296
- _detectedBashSandboxBackend = undefined;
280
+ _bashService;
281
+ _compactionService;
297
282
  // Extension system
298
283
  _extensionRunner;
299
284
  _turnIndex = 0;
@@ -325,6 +310,7 @@ export class AgentSession {
325
310
  _replayLedger;
326
311
  _replayGoalId;
327
312
  _replayLaneId;
313
+ _bashRuntime;
328
314
  _transcriptRepair;
329
315
  _sessionRiskLevel = "normal";
330
316
  _workspaceMutationCount = 0;
@@ -363,7 +349,6 @@ export class AgentSession {
363
349
  this._excludedToolNames = config.excludedToolNames ? new Set(config.excludedToolNames) : undefined;
364
350
  this._baseToolsOverride = config.baseToolsOverride;
365
351
  this._loadoutAccessPolicy = config.loadoutAccessPolicy;
366
- this._configuredBashSandboxPreflight = config.bashSandboxPreflight;
367
352
  this._sessionStartEvent = config.sessionStartEvent ?? { type: "session_start", reason: "startup" };
368
353
  this._transcriptRepair = config.transcriptRepair;
369
354
  this._replayLedger = config.replayLedger;
@@ -372,6 +357,37 @@ export class AgentSession {
372
357
  if (this._replayLedger && this._replayGoalId !== this._replayLedger.getLedger().goalId) {
373
358
  throw new Error("AgentSession replayGoalId does not match the replay ledger goal id");
374
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
+ });
375
391
  const sessionFile = this.sessionManager.getSessionFile();
376
392
  this._runJournalStore = RunJournalStore.open({
377
393
  ...(sessionFile ? { journalPath: `${sessionFile}.runjournal` } : {}),
@@ -465,6 +481,10 @@ export class AgentSession {
465
481
  });
466
482
  this._replayLedger.persist();
467
483
  }
484
+ /** Lazy verified-bash executor bound to this session's replay ledger (default-on path). */
485
+ _getVerifiedEvidenceExecutor() {
486
+ return this._bashRuntime.verifiedEvidenceExecutor();
487
+ }
468
488
  /** Append one required durable audit record. Persistence failures propagate. */
469
489
  _appendRunJournalAudit(event, details) {
470
490
  this._runJournalStore.audit({
@@ -754,12 +774,7 @@ export class AgentSession {
754
774
  }
755
775
  }
756
776
  _getBashSandboxPreflight(override) {
757
- const preflight = override ?? this._configuredBashSandboxPreflight;
758
- if (!preflight || preflight.policy.mode === "off" || preflight.backend) {
759
- return preflight;
760
- }
761
- this._detectedBashSandboxBackend ??= detectSandboxBackend();
762
- return { ...preflight, backend: this._detectedBashSandboxBackend };
777
+ return this._bashRuntime.sandboxPreflight(override);
763
778
  }
764
779
  async _getRequiredRequestAuth(model) {
765
780
  const result = await this._modelRegistry.getApiKeyAndHeaders(model);
@@ -2361,230 +2376,11 @@ export class AgentSession {
2361
2376
  // =========================================================================
2362
2377
  // Compaction
2363
2378
  // =========================================================================
2364
- _captureCompactionState() {
2365
- return this.sessionManager.withCompactionCommitLock(() => this._captureCompactionStateLocked());
2366
- }
2367
- _captureCompactionStateLocked() {
2368
- const sessionFile = this.sessionManager.getSessionFile();
2369
- const bytes = sessionFile && existsSync(sessionFile)
2370
- ? new Uint8Array(readFileSync(sessionFile))
2371
- : new TextEncoder().encode(`${[this.sessionManager.getHeader(), ...this.sessionManager.getEntries()]
2372
- .filter((entry) => entry !== null)
2373
- .map((entry) => JSON.stringify(entry))
2374
- .join("\n")}\n`);
2375
- const report = inspectSessionIntegrity(bytes, { activeLeafId: this.sessionManager.getLeafId() });
2376
- const branchEntries = report.activeBranch;
2377
- let latestCompactionIndex = -1;
2378
- for (let index = branchEntries.length - 1; index >= 0; index -= 1) {
2379
- if (branchEntries[index]?.type === "compaction") {
2380
- latestCompactionIndex = index;
2381
- break;
2382
- }
2383
- }
2384
- const latestCompaction = branchEntries[latestCompactionIndex];
2385
- const firstKeptIndex = latestCompaction?.type === "compaction"
2386
- ? branchEntries.findIndex((entry) => entry.id === latestCompaction.firstKeptEntryId)
2387
- : -1;
2388
- const sourceEntries = branchEntries.slice(latestCompactionIndex < 0 ? 0 : firstKeptIndex < 0 ? latestCompactionIndex : firstKeptIndex);
2389
- const firstEntry = sourceEntries[0];
2390
- const lastEntry = sourceEntries.at(-1);
2391
- if (!firstEntry || !lastEntry || report.activeLeafId === null) {
2392
- const barrier = evaluateCompactionBarrier(report, [...this.agent.state.pendingToolCalls]);
2393
- if (barrier.status !== "ready")
2394
- throw this._barrierError(barrier);
2395
- throw new Error("Nothing to compact: the active session branch is empty");
2396
- }
2397
- const revision = this.sessionManager.getDurableHeadToken();
2398
- const source = createCompactionSourceIdentity({
2399
- sessionId: revision.sessionId,
2400
- entryIds: sourceEntries.map((entry) => entry.id),
2401
- firstEntryId: firstEntry.id,
2402
- lastEntryId: lastEntry.id,
2403
- sourceSha256: createHash("sha256")
2404
- .update(sourceEntries.map((entry) => JSON.stringify(entry)).join("\n"), "utf8")
2405
- .digest("hex"),
2406
- activeLeafId: report.activeLeafId,
2407
- messageCount: report.activeMessages.length,
2408
- });
2409
- return { report, branchEntries, revision, source };
2410
- }
2411
- _captureCompactionProvenance(capture) {
2412
- let latestIntent = "Continue the current session";
2413
- for (let index = capture.report.activeMessages.length - 1; index >= 0; index -= 1) {
2414
- const message = capture.report.activeMessages[index];
2415
- if (message?.role !== "user")
2416
- continue;
2417
- const candidate = sanitizeBinaryOutput(redactSensitiveText(this._getUserMessageText(message)).trim())
2418
- .replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/gu, "")
2419
- .slice(0, 16_384);
2420
- if (candidate.length > 0)
2421
- latestIntent = candidate;
2422
- break;
2423
- }
2424
- const modelHistory = capture.branchEntries
2425
- .flatMap((entry) => {
2426
- if (entry.type === "model_change") {
2427
- return [{ entryId: entry.id, provider: entry.provider, modelId: entry.modelId }];
2428
- }
2429
- if (entry.type === "message" && entry.message.role === "assistant") {
2430
- return [{ entryId: entry.id, provider: entry.message.provider, modelId: entry.message.model }];
2431
- }
2432
- return [];
2433
- })
2434
- .slice(-256);
2435
- const customEntryIds = (customType) => capture.branchEntries
2436
- .filter((entry) => entry.type === "custom" && entry.customType === customType)
2437
- .map((entry) => entry.id);
2438
- return {
2439
- latestIntent,
2440
- openTasks: [],
2441
- laneIds: customEntryIds("lane"),
2442
- acceptancePredicateIds: customEntryIds("acceptance_predicate"),
2443
- evidenceReceiptIds: customEntryIds("evidence_receipt"),
2444
- blockerReasons: [],
2445
- repairEventIds: [
2446
- ...customEntryIds("transcript_repaired"),
2447
- ...customEntryIds("compaction_transcript_repaired"),
2448
- ],
2449
- branch: null,
2450
- worktree: this._cwd,
2451
- modelHistory,
2452
- nextAction: latestIntent.slice(0, 4096) || "Continue the current session",
2453
- };
2454
- }
2455
- _barrierError(barrier) {
2456
- if (barrier.status === "defer") {
2457
- return new Error(`Compaction deferred until the transcript closes (${barrier.reason})`);
2458
- }
2459
- return new Error(`Compaction failed closed on transcript integrity (${barrier.reason}). Run the session doctor before retrying.`);
2460
- }
2461
- _evaluateCompactionBarrier(capture, includeMissingTailAsPending, excludedPendingIds = new Set()) {
2462
- const pending = new Set([...this.agent.state.pendingToolCalls].filter((toolCallId) => !excludedPendingIds.has(toolCallId)));
2463
- if (includeMissingTailAsPending) {
2464
- for (const issue of capture.report.transcript?.issues ?? []) {
2465
- if (issue.kind === "missing_result")
2466
- pending.add(issue.toolCallId);
2467
- }
2468
- }
2469
- return evaluateCompactionBarrier(capture.report, [...pending]);
2470
- }
2471
- _repairEmergencyCompactionTail(capture) {
2472
- const barrier = this._evaluateCompactionBarrier(capture, true);
2473
- if (barrier.status !== "defer" || barrier.reason !== "missing_active_tail_results") {
2474
- if (barrier.status !== "ready")
2475
- throw this._barrierError(barrier);
2476
- return { capture, repairedToolCallIds: new Set() };
2477
- }
2478
- const repairedMessages = repairTranscriptIntegrity([...capture.report.activeMessages], "Tool result missing; synthesized to close an emergency compaction barrier");
2479
- const inserted = repairedMessages.slice(capture.report.activeMessages.length);
2480
- const repairedToolCallIds = new Set();
2481
- for (const message of inserted) {
2482
- if (message.role !== "toolResult") {
2483
- throw new Error("Emergency compaction repair produced a non-tool result");
2484
- }
2485
- const toolResult = message;
2486
- repairedToolCallIds.add(toolResult.toolCallId);
2487
- this.sessionManager.appendMessage(toolResult);
2488
- }
2489
- this.sessionManager.appendCustomEntry("compaction_transcript_repaired", {
2490
- insertedToolCallIds: [...repairedToolCallIds],
2491
- reason: "emergency_compaction",
2492
- });
2493
- this._invalidateContextBudgetCache({ type: "transcriptRepair" });
2494
- const closedCapture = this._captureCompactionState();
2495
- const closedBarrier = this._evaluateCompactionBarrier(closedCapture, false, repairedToolCallIds);
2496
- if (closedBarrier.status !== "ready")
2497
- throw this._barrierError(closedBarrier);
2498
- this.agent.state.messages = this.sessionManager.buildSessionContext().messages;
2499
- return { capture: closedCapture, repairedToolCallIds };
2500
- }
2501
- _priorCommittedCompactionSourceDigests() {
2502
- const digests = [];
2503
- for (const entry of this.sessionManager.getEntries()) {
2504
- if (entry.type !== "compaction" || typeof entry.details !== "object" || entry.details === null)
2505
- continue;
2506
- if (!Object.hasOwn(entry.details, "compactionEnvelope"))
2507
- continue;
2508
- const envelope = validateCompactionEnvelope(Reflect.get(entry.details, "compactionEnvelope"));
2509
- if (envelope.summary !== entry.summary) {
2510
- throw new Error(`Compaction entry ${entry.id} has invalid provenance. Run the session doctor.`);
2511
- }
2512
- digests.push(envelope.source.sourceSha256);
2513
- }
2514
- return digests;
2515
- }
2516
2379
  _beginCompactionTransaction(compactionModel, emergency) {
2517
- let capture = this._captureCompactionState();
2518
- if (emergency) {
2519
- capture = this._repairEmergencyCompactionTail(capture).capture;
2520
- }
2521
- else {
2522
- const barrier = this._evaluateCompactionBarrier(capture, false);
2523
- if (barrier.status !== "ready")
2524
- throw this._barrierError(barrier);
2525
- }
2526
- const transaction = createCompactionTransaction({
2527
- transactionId: randomUUID(),
2528
- baseRevision: capture.revision,
2529
- source: capture.source,
2530
- createdAt: new Date().toISOString(),
2531
- model: { provider: compactionModel.provider, id: compactionModel.id },
2532
- preserved: this._captureCompactionProvenance(capture),
2533
- });
2534
- if (this._priorCommittedCompactionSourceDigests().includes(transaction.source.sourceSha256)) {
2535
- throw new Error("This exact compaction source was already compacted");
2536
- }
2537
- return { capture, transaction };
2538
- }
2539
- _detailsWithCompactionEnvelope(details, envelope) {
2540
- if (typeof details === "object" && details !== null && !Array.isArray(details)) {
2541
- return { ...details, compactionEnvelope: envelope };
2542
- }
2543
- return {
2544
- compactionEnvelope: envelope,
2545
- ...(details === undefined ? {} : { resultDetails: details }),
2546
- };
2380
+ return this._compactionService.beginTransaction(compactionModel, emergency);
2547
2381
  }
2548
2382
  _commitCompaction(begun, result, fromExtension) {
2549
- if (!begun.transaction.source.entryIds.includes(result.firstKeptEntryId)) {
2550
- throw new Error("Compaction first-kept entry is outside the captured source");
2551
- }
2552
- const committed = this.sessionManager.withCompactionCommitLock(() => {
2553
- const current = this._captureCompactionState();
2554
- const barrier = this._evaluateCompactionBarrier(current, false);
2555
- const decision = decideCompactionCommit({
2556
- transaction: begun.transaction,
2557
- currentRevision: current.revision,
2558
- currentSource: current.source,
2559
- barrier,
2560
- priorCommittedSourceDigests: this._priorCommittedCompactionSourceDigests(),
2561
- });
2562
- switch (decision.decision) {
2563
- case "duplicate":
2564
- throw new Error("This exact compaction source was already compacted");
2565
- case "stale":
2566
- throw new Error(`Session changed during compaction (${decision.reason}); generated summary was discarded`);
2567
- case "defer":
2568
- case "fail_closed":
2569
- throw this._barrierError(barrier);
2570
- case "commit": {
2571
- const envelope = createCompactionEnvelope({
2572
- transaction: begun.transaction,
2573
- decision,
2574
- summary: result.summary,
2575
- summarySha256: createHash("sha256").update(result.summary, "utf8").digest("hex"),
2576
- });
2577
- const entryId = this.sessionManager.appendCompaction(result.summary, result.firstKeptEntryId, result.tokensBefore, this._detailsWithCompactionEnvelope(result.details, envelope), fromExtension);
2578
- const entry = this.sessionManager.getEntry(entryId);
2579
- if (!entry || entry.type !== "compaction") {
2580
- throw new Error("Compaction commit did not produce a compaction entry");
2581
- }
2582
- return { entry, envelope };
2583
- }
2584
- }
2585
- });
2586
- this._recordCompactionCommitForHysteresis();
2587
- return committed;
2383
+ return this._compactionService.commit(begun, result, fromExtension);
2588
2384
  }
2589
2385
  _pendingToolResultReserve(settings) {
2590
2386
  const pendingIds = this.agent.state.pendingToolCalls;
@@ -3432,6 +3228,29 @@ export class AgentSession {
3432
3228
  commandPrefix: shellCommandPrefix,
3433
3229
  shellPath,
3434
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
+ })(),
3435
3254
  },
3436
3255
  edit: loadoutWriteOptions,
3437
3256
  write: loadoutWriteOptions,
@@ -3451,7 +3270,7 @@ export class AgentSession {
3451
3270
  this._applyExtensionBindings(this._extensionRunner);
3452
3271
  const defaultActiveToolNames = this._baseToolsOverride
3453
3272
  ? Object.keys(this._baseToolsOverride)
3454
- : ["read", "bash", "edit", "write"];
3273
+ : ["read", "bash", "edit", "write", "diagnostics"];
3455
3274
  const baseActiveToolNames = options.activeToolNames ?? defaultActiveToolNames;
3456
3275
  this._refreshToolRegistry({
3457
3276
  activeToolNames: baseActiveToolNames,
@@ -3685,123 +3504,35 @@ export class AgentSession {
3685
3504
  * @param options.sandboxPolicy Trusted sandbox preflight for local bash execution (never sourced from RPC payloads)
3686
3505
  */
3687
3506
  async executeBash(command, onChunk, options) {
3688
- // Apply command prefix if configured (e.g., "shopt -s expand_aliases" for alias support)
3689
- const prefix = this.settingsManager.getShellCommandPrefix();
3690
- const shellPath = this.settingsManager.getShellPath();
3691
- const resolvedCommand = prefix ? `${prefix}\n${command}` : command;
3692
- if (this._loadoutAccessPolicy) {
3693
- assertLoadoutAccess((request) => decideLoadoutAccess(this._loadoutAccessPolicy, request), { operation: "execute", toolName: "bash", command: resolvedCommand });
3694
- }
3695
- // Non-negotiable safety floor for headless callers (RPC bash): hard-deny
3696
- // block-tier commands before any shell is spawned.
3697
- if (options?.safetyGate === "headless") {
3698
- const floorVerdict = classifyShellCommand(command);
3699
- if (floorVerdict.risk === "block") {
3700
- throw new Error(`OMK §0.1 safety floor blocked bash: [${floorVerdict.rule}] ${floorVerdict.reason}`);
3701
- }
3702
- }
3703
- // Command-safety parity for non-interactive callers (RPC bash). Interactive
3704
- // `!`/`!!` bash is gated earlier through the user_bash extension event, which
3705
- // keeps its prompt-based approval semantics, so it does not pass safetyGate and
3706
- // is never double-prompted here. confirm/block-tier verdicts deny headlessly;
3707
- // the EFFECTIVE command (after the shell command prefix) is classified.
3708
- if (options?.safetyGate === "headless") {
3709
- const decision = await evaluateCommandGate(resolvedCommand, {
3710
- hasUI: false,
3711
- headlessConfirmPolicy: isCommandSafetyAssumeYesEnabled() ? "allow" : "deny",
3712
- });
3713
- if (decision?.deny) {
3714
- const blocked = buildBlockedBashResult(decision.reason);
3715
- this.recordBashResult(command, blocked, options);
3716
- return blocked;
3717
- }
3718
- }
3719
- this._bashAbortController = new AbortController();
3720
- try {
3721
- try {
3722
- const result = await executeBashWithOperations(resolvedCommand, this.sessionManager.getCwd(), options?.operations ??
3723
- createLocalBashOperations({
3724
- shellPath,
3725
- sandboxPolicy: this._getBashSandboxPreflight(options?.sandboxPolicy),
3726
- }), {
3727
- onChunk: (delta) => {
3728
- onChunk?.(delta);
3729
- this._emit({ type: "bash_execution_update", id: options?.id, delta });
3730
- },
3731
- signal: this._bashAbortController.signal,
3732
- });
3733
- this.recordBashResult(command, result, options);
3734
- return result;
3735
- }
3736
- catch (error) {
3737
- if (!isSandboxDeniedError(error)) {
3738
- throw error;
3739
- }
3740
- const blocked = buildSandboxDeniedBashResult(error.message);
3741
- this.recordBashResult(command, blocked, options);
3742
- return blocked;
3743
- }
3744
- }
3745
- finally {
3746
- this._bashAbortController = undefined;
3747
- }
3507
+ return this._bashService.executeBash(command, onChunk, options);
3748
3508
  }
3749
3509
  /**
3750
3510
  * Record a bash execution result in session history.
3751
3511
  * Used by executeBash and by extensions that handle bash execution themselves.
3752
3512
  */
3753
3513
  recordBashResult(command, result, options) {
3754
- const bashMessage = {
3755
- role: "bashExecution",
3756
- command,
3757
- output: result.output,
3758
- exitCode: result.exitCode,
3759
- cancelled: result.cancelled,
3760
- truncated: result.truncated,
3761
- fullOutputPath: result.fullOutputPath,
3762
- timestamp: Date.now(),
3763
- excludeFromContext: options?.excludeFromContext,
3764
- };
3765
- // If agent is streaming, defer adding to avoid breaking tool_use/tool_result ordering
3766
- if (this.isStreaming) {
3767
- // Queue for later - will be flushed on agent_end
3768
- this._pendingBashMessages.push(bashMessage);
3769
- }
3770
- else {
3771
- // Add to agent state immediately
3772
- this.agent.state.messages.push(bashMessage);
3773
- // Save to session
3774
- this.sessionManager.appendMessage(bashMessage);
3775
- }
3514
+ this._bashService.recordBashResult(command, result, options);
3776
3515
  }
3777
3516
  /**
3778
3517
  * Cancel running bash command.
3779
3518
  */
3780
3519
  abortBash() {
3781
- this._bashAbortController?.abort();
3520
+ this._bashService.abortBash();
3782
3521
  }
3783
3522
  /** Whether a bash command is currently running */
3784
3523
  get isBashRunning() {
3785
- return this._bashAbortController !== undefined;
3524
+ return this._bashService.isBashRunning;
3786
3525
  }
3787
3526
  /** Whether there are pending bash messages waiting to be flushed */
3788
3527
  get hasPendingBashMessages() {
3789
- return this._pendingBashMessages.length > 0;
3528
+ return this._bashService.hasPendingBashMessages;
3790
3529
  }
3791
3530
  /**
3792
3531
  * Flush pending bash messages to agent state and session.
3793
3532
  * Called after agent turn completes to maintain proper message ordering.
3794
3533
  */
3795
3534
  _flushPendingBashMessages() {
3796
- if (this._pendingBashMessages.length === 0)
3797
- return;
3798
- for (const bashMessage of this._pendingBashMessages) {
3799
- // Add to agent state
3800
- this.agent.state.messages.push(bashMessage);
3801
- // Save to session
3802
- this.sessionManager.appendMessage(bashMessage);
3803
- }
3804
- this._pendingBashMessages = [];
3535
+ this._bashService.flushPending();
3805
3536
  }
3806
3537
  // =========================================================================
3807
3538
  // Session Management