open-multi-agent-kit 0.93.0 → 0.94.1

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 (99) hide show
  1. package/CHANGELOG.md +29 -1
  2. package/README.md +3 -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/github-star-nudge.d.ts +15 -0
  8. package/dist/core/github-star-nudge.d.ts.map +1 -0
  9. package/dist/core/github-star-nudge.js +21 -0
  10. package/dist/core/github-star-nudge.js.map +1 -0
  11. package/dist/core/package-manager.d.ts.map +1 -1
  12. package/dist/core/package-manager.js +11 -1
  13. package/dist/core/package-manager.js.map +1 -1
  14. package/dist/core/sandbox/default-policy.d.ts +14 -0
  15. package/dist/core/sandbox/default-policy.d.ts.map +1 -0
  16. package/dist/core/sandbox/default-policy.js +58 -0
  17. package/dist/core/sandbox/default-policy.js.map +1 -0
  18. package/dist/core/sdk.d.ts.map +1 -1
  19. package/dist/core/sdk.js +1 -1
  20. package/dist/core/sdk.js.map +1 -1
  21. package/dist/core/session-bash-runtime.d.ts +62 -0
  22. package/dist/core/session-bash-runtime.d.ts.map +1 -0
  23. package/dist/core/session-bash-runtime.js +167 -0
  24. package/dist/core/session-bash-runtime.js.map +1 -0
  25. package/dist/core/session-bash-service.d.ts +61 -0
  26. package/dist/core/session-bash-service.d.ts.map +1 -0
  27. package/dist/core/session-bash-service.js +155 -0
  28. package/dist/core/session-bash-service.js.map +1 -0
  29. package/dist/core/session-compaction-service.d.ts +48 -0
  30. package/dist/core/session-compaction-service.d.ts.map +1 -0
  31. package/dist/core/session-compaction-service.js +250 -0
  32. package/dist/core/session-compaction-service.js.map +1 -0
  33. package/dist/core/settings-manager.d.ts +5 -0
  34. package/dist/core/settings-manager.d.ts.map +1 -1
  35. package/dist/core/settings-manager.js +9 -0
  36. package/dist/core/settings-manager.js.map +1 -1
  37. package/dist/core/skills-catalog-cache.d.ts +25 -0
  38. package/dist/core/skills-catalog-cache.d.ts.map +1 -0
  39. package/dist/core/skills-catalog-cache.js +123 -0
  40. package/dist/core/skills-catalog-cache.js.map +1 -0
  41. package/dist/core/skills.d.ts +6 -0
  42. package/dist/core/skills.d.ts.map +1 -1
  43. package/dist/core/skills.js +25 -4
  44. package/dist/core/skills.js.map +1 -1
  45. package/dist/core/slash-commands.d.ts.map +1 -1
  46. package/dist/core/slash-commands.js +4 -0
  47. package/dist/core/slash-commands.js.map +1 -1
  48. package/dist/core/system-prompt.d.ts.map +1 -1
  49. package/dist/core/system-prompt.js +1 -1
  50. package/dist/core/system-prompt.js.map +1 -1
  51. package/dist/core/tools/bash.d.ts +3 -0
  52. package/dist/core/tools/bash.d.ts.map +1 -1
  53. package/dist/core/tools/bash.js +3 -2
  54. package/dist/core/tools/bash.js.map +1 -1
  55. package/dist/core/tools/diagnostics.d.ts +29 -0
  56. package/dist/core/tools/diagnostics.d.ts.map +1 -0
  57. package/dist/core/tools/diagnostics.js +325 -0
  58. package/dist/core/tools/diagnostics.js.map +1 -0
  59. package/dist/core/tools/index.d.ts +2 -1
  60. package/dist/core/tools/index.d.ts.map +1 -1
  61. package/dist/core/tools/index.js +18 -1
  62. package/dist/core/tools/index.js.map +1 -1
  63. package/dist/core/verified-bash-adapter.d.ts +18 -1
  64. package/dist/core/verified-bash-adapter.d.ts.map +1 -1
  65. package/dist/core/verified-bash-adapter.js +38 -1
  66. package/dist/core/verified-bash-adapter.js.map +1 -1
  67. package/dist/core/verified-bash-runtime.d.ts +13 -0
  68. package/dist/core/verified-bash-runtime.d.ts.map +1 -0
  69. package/dist/core/verified-bash-runtime.js +81 -0
  70. package/dist/core/verified-bash-runtime.js.map +1 -0
  71. package/dist/index.d.ts +4 -2
  72. package/dist/index.d.ts.map +1 -1
  73. package/dist/index.js +4 -2
  74. package/dist/index.js.map +1 -1
  75. package/dist/modes/interactive/interactive-mode.d.ts +2 -0
  76. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  77. package/dist/modes/interactive/interactive-mode.js +55 -0
  78. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  79. package/dist/types/evidence.d.ts +1 -1
  80. package/dist/types/evidence.d.ts.map +1 -1
  81. package/dist/types/evidence.js.map +1 -1
  82. package/dist/utils/frontmatter.d.ts.map +1 -1
  83. package/dist/utils/frontmatter.js +31 -5
  84. package/dist/utils/frontmatter.js.map +1 -1
  85. package/docs/environment-variables.md +4 -0
  86. package/docs/sdk.md +10 -4
  87. package/docs/settings.md +11 -0
  88. package/docs/usage.md +1 -0
  89. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  90. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  91. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  92. package/examples/extensions/gondolin/package-lock.json +2 -2
  93. package/examples/extensions/gondolin/package.json +1 -1
  94. package/examples/extensions/sandbox/package-lock.json +2 -2
  95. package/examples/extensions/sandbox/package.json +1 -1
  96. package/examples/extensions/with-deps/package-lock.json +2 -2
  97. package/examples/extensions/with-deps/package.json +1 -1
  98. package/npm-shrinkwrap.json +15 -15
  99. package/package.json +5 -5
@@ -0,0 +1,167 @@
1
+ /**
2
+ * Session bash runtime: verified-evidence executor, default sandbox preflight,
3
+ * and git-aware workspace scope in one cohesive, lazily-initialized unit.
4
+ *
5
+ * Extracted from AgentSession so the session class stays a coordinator; every
6
+ * default-on behavior here keeps its opt-out:
7
+ * - `OMK_VERIFIED_BASH=0` disables the receipt-bound executor.
8
+ * - `OMK_BASH_SANDBOX=0` disables the default sandbox preflight.
9
+ */
10
+ import { join } from "node:path";
11
+ import { EvidenceReceiptStore } from "../guardrails/evidence-receipt-store.js";
12
+ import { VerifiedEvidenceExecutor } from "../guardrails/verified-executor.js";
13
+ import { stripAnsi } from "../utils/ansi.js";
14
+ import { getShellConfig, sanitizeBinaryOutput } from "../utils/shell.js";
15
+ import { detectSandboxBackend } from "./sandbox/backend.js";
16
+ import { createWorkspaceSandboxPolicy, resolveBashSandboxMode } from "./sandbox/default-policy.js";
17
+ import { executeVerifiedBash } from "./verified-bash-adapter.js";
18
+ import { isVerifiedBashEnabled, resolveSessionWorkspaceScope } from "./verified-bash-runtime.js";
19
+ export class SessionBashRuntime {
20
+ verifiedExecutor;
21
+ detectedBackend;
22
+ defaultSandboxPreflight;
23
+ sandboxModeOverride;
24
+ options;
25
+ constructor(options) {
26
+ this.options = options;
27
+ }
28
+ /**
29
+ * Interactive promotion: switch the default sandbox mode at runtime
30
+ * (audit ↔ enforce ↔ off) without restarting the session. `undefined`
31
+ * restores env-based resolution.
32
+ */
33
+ setSandboxMode(mode) {
34
+ this.sandboxModeOverride = mode;
35
+ this.defaultSandboxPreflight = undefined;
36
+ }
37
+ get sandboxMode() {
38
+ return this.sandboxModeOverride ?? resolveBashSandboxMode();
39
+ }
40
+ /** Lazy receipt executor bound to the session replay ledger (default-on path). */
41
+ verifiedEvidenceExecutor() {
42
+ const { replayLedger, replayGoalId, getSessionFile, cwd } = this.options;
43
+ if (!replayLedger || !replayGoalId || !isVerifiedBashEnabled())
44
+ return undefined;
45
+ if (!this.verifiedExecutor) {
46
+ const sessionFile = getSessionFile();
47
+ const evidenceRoot = sessionFile
48
+ ? `${sessionFile}.evidence`
49
+ : join(cwd, ".omk", "session-evidence", replayGoalId);
50
+ this.verifiedExecutor = new VerifiedEvidenceExecutor({
51
+ store: new EvidenceReceiptStore(join(evidenceRoot, "receipts")),
52
+ ledger: replayLedger,
53
+ });
54
+ }
55
+ return this.verifiedExecutor;
56
+ }
57
+ /** Configured preflight wins; otherwise the default audit/enforce preflight. */
58
+ sandboxPreflight(override) {
59
+ const preflight = override ?? this.options.configuredSandboxPreflight ?? this.getDefaultSandboxPreflight();
60
+ if (!preflight || preflight.policy.mode === "off" || preflight.backend) {
61
+ return preflight;
62
+ }
63
+ this.detectedBackend ??= detectSandboxBackend();
64
+ return { ...preflight, backend: this.detectedBackend };
65
+ }
66
+ /**
67
+ * Default-on sandbox preflight. `audit` (default) keeps the spawn unwrapped
68
+ * but reports every decision to the replay ledger; `enforce` activates the
69
+ * real OS backend and fails closed when none is available.
70
+ */
71
+ getDefaultSandboxPreflight() {
72
+ if (this.defaultSandboxPreflight !== undefined) {
73
+ return this.defaultSandboxPreflight ?? undefined;
74
+ }
75
+ const mode = this.sandboxMode;
76
+ if (mode === "off") {
77
+ this.defaultSandboxPreflight = null;
78
+ return undefined;
79
+ }
80
+ this.detectedBackend ??= detectSandboxBackend();
81
+ const detected = this.detectedBackend;
82
+ const backend = mode === "enforce"
83
+ ? detected
84
+ : { platform: detected.platform, backendAvailable: false, domainAllowlistAvailable: false };
85
+ // Root must cover the actual execution cwd (session manager), not just the
86
+ // session workspace: in-memory or RPC sessions can legitimately differ.
87
+ const policy = createWorkspaceSandboxPolicy(this.options.getExecutionCwd() || this.options.cwd, mode);
88
+ this.defaultSandboxPreflight = {
89
+ policy,
90
+ backend,
91
+ onSpawnDecision: (decision) => {
92
+ if (decision.rule === "sandbox.off")
93
+ return;
94
+ this.options.appendReplayEvent("sandbox_audit", {
95
+ mode,
96
+ rule: decision.rule,
97
+ reason: decision.reason,
98
+ wrapped: "wrapped" in decision ? decision.wrapped : false,
99
+ backendAvailable: backend.backendAvailable,
100
+ platform: backend.platform,
101
+ });
102
+ },
103
+ };
104
+ return this.defaultSandboxPreflight;
105
+ }
106
+ /** Git-aware workspace scope for verified bash receipts (TTL-cached). */
107
+ workspaceScope() {
108
+ return resolveSessionWorkspaceScope(this.options.cwd);
109
+ }
110
+ get goalId() {
111
+ return this.options.replayGoalId;
112
+ }
113
+ get laneId() {
114
+ return this.options.replayLaneId;
115
+ }
116
+ /**
117
+ * Receipt-bound bash execution. Returns `undefined` when the verified path
118
+ * is unavailable (no ledger / opted out) so the caller falls back to the
119
+ * legacy executor. Streams sanitized text through `onChunk` while the
120
+ * adapter owns the raw receipt bytes.
121
+ */
122
+ async executeVerified(request) {
123
+ const evidenceExecutor = this.verifiedEvidenceExecutor();
124
+ const goalId = this.options.replayGoalId;
125
+ if (!evidenceExecutor || !goalId)
126
+ return undefined;
127
+ const { shell } = getShellConfig(request.shellPath);
128
+ const command = request.resolvedCommand;
129
+ const claim = command.length > 200 ? `bash: ${command.slice(0, 197)}...` : `bash: ${command}`;
130
+ const outputChunks = [];
131
+ const decoder = new TextDecoder();
132
+ const execution = await executeVerifiedBash({
133
+ evidenceExecutor,
134
+ operations: request.operations,
135
+ goalId,
136
+ ...(this.options.replayLaneId !== undefined ? { laneId: this.options.replayLaneId } : {}),
137
+ claim,
138
+ shell,
139
+ script: command,
140
+ cwd: request.cwd,
141
+ timeoutMs: null,
142
+ workspaceScope: this.workspaceScope(),
143
+ executor: "bash-tool",
144
+ signal: request.signal,
145
+ onData: (data) => {
146
+ const text = sanitizeBinaryOutput(stripAnsi(decoder.decode(data, { stream: true }))).replace(/\r/g, "");
147
+ if (!text)
148
+ return;
149
+ outputChunks.push(text);
150
+ request.onChunk(text);
151
+ },
152
+ });
153
+ const status = execution.receipt.core.status;
154
+ // Preserve pre-adapter timeout throw semantics after the receipt is bound.
155
+ if (status === "timeout") {
156
+ throw new Error("timeout:0");
157
+ }
158
+ const cancelled = status === "aborted" || request.signal.aborted;
159
+ return {
160
+ output: outputChunks.join(""),
161
+ exitCode: cancelled ? undefined : (execution.receipt.core.exitCode ?? undefined),
162
+ cancelled,
163
+ truncated: false,
164
+ };
165
+ }
166
+ }
167
+ //# sourceMappingURL=session-bash-runtime.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-bash-runtime.js","sourceRoot":"","sources":["../../src/core/session-bash-runtime.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Session bash runtime: verified-evidence executor, default sandbox preflight,\n * and git-aware workspace scope in one cohesive, lazily-initialized unit.\n *\n * Extracted from AgentSession so the session class stays a coordinator; every\n * default-on behavior here keeps its opt-out:\n * - `OMK_VERIFIED_BASH=0` disables the receipt-bound executor.\n * - `OMK_BASH_SANDBOX=0` disables the default sandbox preflight.\n */\nimport { join } from \"node:path\";\nimport { EvidenceReceiptStore } from \"../guardrails/evidence-receipt-store.ts\";\nimport type { ReplayLedgerManager } from \"../guardrails/evidence-system.ts\";\nimport { VerifiedEvidenceExecutor } from \"../guardrails/verified-executor.ts\";\nimport type { ReplayEventType } from \"../types/evidence.ts\";\nimport { stripAnsi } from \"../utils/ansi.ts\";\nimport { getShellConfig, sanitizeBinaryOutput } from \"../utils/shell.ts\";\nimport type { BashResult } from \"./bash-executor.ts\";\nimport { detectSandboxBackend } from \"./sandbox/backend.ts\";\nimport { createWorkspaceSandboxPolicy, resolveBashSandboxMode } from \"./sandbox/default-policy.ts\";\nimport type { SandboxBackendStatus, SandboxMode } from \"./sandbox/policy.ts\";\nimport type { BashOperations, BashSandboxPreflight } from \"./tools/bash.ts\";\nimport { executeVerifiedBash } from \"./verified-bash-adapter.ts\";\nimport { isVerifiedBashEnabled, resolveSessionWorkspaceScope } from \"./verified-bash-runtime.ts\";\n\nexport interface SessionBashRuntimeOptions {\n\treadonly cwd: string;\n\t/** Actual execution cwd authority (session manager). */\n\treadonly getExecutionCwd: () => string;\n\t/** Session file anchor for the receipt store; undefined for ephemeral sessions. */\n\treadonly getSessionFile: () => string | undefined;\n\treadonly replayLedger: ReplayLedgerManager | undefined;\n\treadonly replayGoalId: string | undefined;\n\treadonly replayLaneId: string | undefined;\n\treadonly configuredSandboxPreflight: BashSandboxPreflight | undefined;\n\treadonly appendReplayEvent: (type: ReplayEventType, payload: unknown) => void;\n}\n\nexport class SessionBashRuntime {\n\tprivate verifiedExecutor: VerifiedEvidenceExecutor | undefined;\n\tprivate detectedBackend: SandboxBackendStatus | undefined;\n\tprivate defaultSandboxPreflight: BashSandboxPreflight | null | undefined;\n\tprivate sandboxModeOverride: SandboxMode | undefined;\n\tprivate readonly options: SessionBashRuntimeOptions;\n\n\tconstructor(options: SessionBashRuntimeOptions) {\n\t\tthis.options = options;\n\t}\n\n\t/**\n\t * Interactive promotion: switch the default sandbox mode at runtime\n\t * (audit ↔ enforce ↔ off) without restarting the session. `undefined`\n\t * restores env-based resolution.\n\t */\n\tsetSandboxMode(mode: SandboxMode | undefined): void {\n\t\tthis.sandboxModeOverride = mode;\n\t\tthis.defaultSandboxPreflight = undefined;\n\t}\n\n\tget sandboxMode(): SandboxMode {\n\t\treturn this.sandboxModeOverride ?? resolveBashSandboxMode();\n\t}\n\n\t/** Lazy receipt executor bound to the session replay ledger (default-on path). */\n\tverifiedEvidenceExecutor(): VerifiedEvidenceExecutor | undefined {\n\t\tconst { replayLedger, replayGoalId, getSessionFile, cwd } = this.options;\n\t\tif (!replayLedger || !replayGoalId || !isVerifiedBashEnabled()) return undefined;\n\t\tif (!this.verifiedExecutor) {\n\t\t\tconst sessionFile = getSessionFile();\n\t\t\tconst evidenceRoot = sessionFile\n\t\t\t\t? `${sessionFile}.evidence`\n\t\t\t\t: join(cwd, \".omk\", \"session-evidence\", replayGoalId);\n\t\t\tthis.verifiedExecutor = new VerifiedEvidenceExecutor({\n\t\t\t\tstore: new EvidenceReceiptStore(join(evidenceRoot, \"receipts\")),\n\t\t\t\tledger: replayLedger,\n\t\t\t});\n\t\t}\n\t\treturn this.verifiedExecutor;\n\t}\n\n\t/** Configured preflight wins; otherwise the default audit/enforce preflight. */\n\tsandboxPreflight(override?: BashSandboxPreflight): BashSandboxPreflight | undefined {\n\t\tconst preflight = override ?? this.options.configuredSandboxPreflight ?? this.getDefaultSandboxPreflight();\n\t\tif (!preflight || preflight.policy.mode === \"off\" || preflight.backend) {\n\t\t\treturn preflight;\n\t\t}\n\t\tthis.detectedBackend ??= detectSandboxBackend();\n\t\treturn { ...preflight, backend: this.detectedBackend };\n\t}\n\n\t/**\n\t * Default-on sandbox preflight. `audit` (default) keeps the spawn unwrapped\n\t * but reports every decision to the replay ledger; `enforce` activates the\n\t * real OS backend and fails closed when none is available.\n\t */\n\tprivate getDefaultSandboxPreflight(): BashSandboxPreflight | undefined {\n\t\tif (this.defaultSandboxPreflight !== undefined) {\n\t\t\treturn this.defaultSandboxPreflight ?? undefined;\n\t\t}\n\t\tconst mode = this.sandboxMode;\n\t\tif (mode === \"off\") {\n\t\t\tthis.defaultSandboxPreflight = null;\n\t\t\treturn undefined;\n\t\t}\n\t\tthis.detectedBackend ??= detectSandboxBackend();\n\t\tconst detected = this.detectedBackend;\n\t\tconst backend: SandboxBackendStatus =\n\t\t\tmode === \"enforce\"\n\t\t\t\t? detected\n\t\t\t\t: { platform: detected.platform, backendAvailable: false, domainAllowlistAvailable: false };\n\t\t// Root must cover the actual execution cwd (session manager), not just the\n\t\t// session workspace: in-memory or RPC sessions can legitimately differ.\n\t\tconst policy = createWorkspaceSandboxPolicy(this.options.getExecutionCwd() || this.options.cwd, mode);\n\t\tthis.defaultSandboxPreflight = {\n\t\t\tpolicy,\n\t\t\tbackend,\n\t\t\tonSpawnDecision: (decision) => {\n\t\t\t\tif (decision.rule === \"sandbox.off\") return;\n\t\t\t\tthis.options.appendReplayEvent(\"sandbox_audit\", {\n\t\t\t\t\tmode,\n\t\t\t\t\trule: decision.rule,\n\t\t\t\t\treason: decision.reason,\n\t\t\t\t\twrapped: \"wrapped\" in decision ? decision.wrapped : false,\n\t\t\t\t\tbackendAvailable: backend.backendAvailable,\n\t\t\t\t\tplatform: backend.platform,\n\t\t\t\t});\n\t\t\t},\n\t\t};\n\t\treturn this.defaultSandboxPreflight;\n\t}\n\n\t/** Git-aware workspace scope for verified bash receipts (TTL-cached). */\n\tworkspaceScope() {\n\t\treturn resolveSessionWorkspaceScope(this.options.cwd);\n\t}\n\n\tget goalId(): string | undefined {\n\t\treturn this.options.replayGoalId;\n\t}\n\n\tget laneId(): string | undefined {\n\t\treturn this.options.replayLaneId;\n\t}\n\n\t/**\n\t * Receipt-bound bash execution. Returns `undefined` when the verified path\n\t * is unavailable (no ledger / opted out) so the caller falls back to the\n\t * legacy executor. Streams sanitized text through `onChunk` while the\n\t * adapter owns the raw receipt bytes.\n\t */\n\tasync executeVerified(request: {\n\t\tresolvedCommand: string;\n\t\tcwd: string;\n\t\tshellPath: string | undefined;\n\t\toperations: BashOperations;\n\t\tsignal: AbortSignal;\n\t\tonChunk: (delta: string) => void;\n\t}): Promise<BashResult | undefined> {\n\t\tconst evidenceExecutor = this.verifiedEvidenceExecutor();\n\t\tconst goalId = this.options.replayGoalId;\n\t\tif (!evidenceExecutor || !goalId) return undefined;\n\t\tconst { shell } = getShellConfig(request.shellPath);\n\t\tconst command = request.resolvedCommand;\n\t\tconst claim = command.length > 200 ? `bash: ${command.slice(0, 197)}...` : `bash: ${command}`;\n\t\tconst outputChunks: string[] = [];\n\t\tconst decoder = new TextDecoder();\n\t\tconst execution = await executeVerifiedBash({\n\t\t\tevidenceExecutor,\n\t\t\toperations: request.operations,\n\t\t\tgoalId,\n\t\t\t...(this.options.replayLaneId !== undefined ? { laneId: this.options.replayLaneId } : {}),\n\t\t\tclaim,\n\t\t\tshell,\n\t\t\tscript: command,\n\t\t\tcwd: request.cwd,\n\t\t\ttimeoutMs: null,\n\t\t\tworkspaceScope: this.workspaceScope(),\n\t\t\texecutor: \"bash-tool\",\n\t\t\tsignal: request.signal,\n\t\t\tonData: (data) => {\n\t\t\t\tconst text = sanitizeBinaryOutput(stripAnsi(decoder.decode(data, { stream: true }))).replace(/\\r/g, \"\");\n\t\t\t\tif (!text) return;\n\t\t\t\toutputChunks.push(text);\n\t\t\t\trequest.onChunk(text);\n\t\t\t},\n\t\t});\n\t\tconst status = execution.receipt.core.status;\n\t\t// Preserve pre-adapter timeout throw semantics after the receipt is bound.\n\t\tif (status === \"timeout\") {\n\t\t\tthrow new Error(\"timeout:0\");\n\t\t}\n\t\tconst cancelled = status === \"aborted\" || request.signal.aborted;\n\t\treturn {\n\t\t\toutput: outputChunks.join(\"\"),\n\t\t\texitCode: cancelled ? undefined : (execution.receipt.core.exitCode ?? undefined),\n\t\t\tcancelled,\n\t\t\ttruncated: false,\n\t\t};\n\t}\n}\n"]}
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Session bash service: command execution, result recording, abort, and the
3
+ * streaming-deferral queue. Extracted from AgentSession so the session class
4
+ * coordinates lifecycle while this owns the bash surface end-to-end.
5
+ *
6
+ * Ordering contract (preserved from the session implementation):
7
+ * - results recorded while the agent is streaming are queued and flushed on
8
+ * turn end (`flushPending`) so tool_use/tool_result ordering never breaks;
9
+ * - `recordBashResult` is idempotent-safe to call from executeBash and from
10
+ * extensions that run bash themselves.
11
+ */
12
+ import { type BashResult } from "./bash-executor.ts";
13
+ import type { LoadoutAccessPolicy } from "./loadout-access-policy.ts";
14
+ import type { BashExecutionMessage } from "./messages.ts";
15
+ import type { SessionBashRuntime } from "./session-bash-runtime.ts";
16
+ import type { SettingsManager } from "./settings-manager.ts";
17
+ import { type BashOperations, type BashSandboxPreflight } from "./tools/bash.ts";
18
+ export interface ExecuteBashOptions {
19
+ excludeFromContext?: boolean;
20
+ /** Optional identifier echoed in `bash_execution_update` events (RPC request correlation). */
21
+ id?: string;
22
+ operations?: BashOperations;
23
+ safetyGate?: "headless";
24
+ /** Trusted internal/test override for local bash sandboxing. */
25
+ sandboxPolicy?: BashSandboxPreflight;
26
+ }
27
+ export interface SessionBashServiceDeps {
28
+ readonly settings: SettingsManager;
29
+ readonly runtime: SessionBashRuntime;
30
+ readonly loadoutPolicy: LoadoutAccessPolicy | undefined;
31
+ readonly getCwd: () => string;
32
+ readonly emit: (event: {
33
+ type: "bash_execution_update";
34
+ id?: string;
35
+ delta: string;
36
+ }) => void;
37
+ readonly isStreaming: () => boolean;
38
+ readonly pushMessage: (message: BashExecutionMessage) => void;
39
+ readonly appendMessage: (message: BashExecutionMessage) => void;
40
+ }
41
+ export declare class SessionBashService {
42
+ private abortController;
43
+ private pendingBashMessages;
44
+ private readonly deps;
45
+ constructor(deps: SessionBashServiceDeps);
46
+ executeBash(command: string, onChunk?: (chunk: string) => void, options?: ExecuteBashOptions): Promise<BashResult>;
47
+ /**
48
+ * Record a bash execution result in session history.
49
+ * Used by executeBash and by extensions that handle bash execution themselves.
50
+ */
51
+ recordBashResult(command: string, result: BashResult, options?: {
52
+ excludeFromContext?: boolean;
53
+ }): void;
54
+ /** Cancel running bash command. */
55
+ abortBash(): void;
56
+ get isBashRunning(): boolean;
57
+ get hasPendingBashMessages(): boolean;
58
+ /** Flush queued results after the agent turn completes (ordering contract). */
59
+ flushPending(): void;
60
+ }
61
+ //# sourceMappingURL=session-bash-service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-bash-service.d.ts","sourceRoot":"","sources":["../../src/core/session-bash-service.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,EAAE,KAAK,UAAU,EAA6B,MAAM,oBAAoB,CAAC;AAOhF,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AAEtE,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC;AAC1D,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AACpE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAC7D,OAAO,EAAE,KAAK,cAAc,EAAE,KAAK,oBAAoB,EAA6B,MAAM,iBAAiB,CAAC;AAE5G,MAAM,WAAW,kBAAkB;IAClC,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,8FAA8F;IAC9F,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,UAAU,CAAC,EAAE,cAAc,CAAC;IAC5B,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,gEAAgE;IAChE,aAAa,CAAC,EAAE,oBAAoB,CAAC;CACrC;AAED,MAAM,WAAW,sBAAsB;IACtC,QAAQ,CAAC,QAAQ,EAAE,eAAe,CAAC;IACnC,QAAQ,CAAC,OAAO,EAAE,kBAAkB,CAAC;IACrC,QAAQ,CAAC,aAAa,EAAE,mBAAmB,GAAG,SAAS,CAAC;IACxD,QAAQ,CAAC,MAAM,EAAE,MAAM,MAAM,CAAC;IAC9B,QAAQ,CAAC,IAAI,EAAE,CAAC,KAAK,EAAE;QAAE,IAAI,EAAE,uBAAuB,CAAC;QAAC,EAAE,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;IAC9F,QAAQ,CAAC,WAAW,EAAE,MAAM,OAAO,CAAC;IACpC,QAAQ,CAAC,WAAW,EAAE,CAAC,OAAO,EAAE,oBAAoB,KAAK,IAAI,CAAC;IAC9D,QAAQ,CAAC,aAAa,EAAE,CAAC,OAAO,EAAE,oBAAoB,KAAK,IAAI,CAAC;CAChE;AAUD,qBAAa,kBAAkB;IAC9B,OAAO,CAAC,eAAe,CAA8B;IACrD,OAAO,CAAC,mBAAmB,CAA8B;IACzD,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAyB;IAE9C,YAAY,IAAI,EAAE,sBAAsB,EAEvC;IAEK,WAAW,CAChB,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,EACjC,OAAO,CAAC,EAAE,kBAAkB,GAC1B,OAAO,CAAC,UAAU,CAAC,CAoFrB;IAED;;;OAGG;IACH,gBAAgB,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,OAAO,CAAC,EAAE;QAAE,kBAAkB,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,IAAI,CAoBtG;IAED,mCAAmC;IACnC,SAAS,IAAI,IAAI,CAEhB;IAED,IAAI,aAAa,IAAI,OAAO,CAE3B;IAED,IAAI,sBAAsB,IAAI,OAAO,CAEpC;IAED,+EAA+E;IAC/E,YAAY,IAAI,IAAI,CAOnB;CACD","sourcesContent":["/**\n * Session bash service: command execution, result recording, abort, and the\n * streaming-deferral queue. Extracted from AgentSession so the session class\n * coordinates lifecycle while this owns the bash surface end-to-end.\n *\n * Ordering contract (preserved from the session implementation):\n * - results recorded while the agent is streaming are queued and flushed on\n * turn end (`flushPending`) so tool_use/tool_result ordering never breaks;\n * - `recordBashResult` is idempotent-safe to call from executeBash and from\n * extensions that run bash themselves.\n */\nimport { type BashResult, executeBashWithOperations } from \"./bash-executor.ts\";\nimport { classifyShellCommand } from \"./command-safety.ts\";\nimport {\n\tbuildBlockedBashResult,\n\tevaluateCommandGate,\n\tisCommandSafetyAssumeYesEnabled,\n} from \"./extensions/builtin/command-safety-gate.ts\";\nimport type { LoadoutAccessPolicy } from \"./loadout-access-policy.ts\";\nimport { assertLoadoutAccess, decideLoadoutAccess } from \"./loadout-access-policy.ts\";\nimport type { BashExecutionMessage } from \"./messages.ts\";\nimport type { SessionBashRuntime } from \"./session-bash-runtime.ts\";\nimport type { SettingsManager } from \"./settings-manager.ts\";\nimport { type BashOperations, type BashSandboxPreflight, createLocalBashOperations } from \"./tools/bash.ts\";\n\nexport interface ExecuteBashOptions {\n\texcludeFromContext?: boolean;\n\t/** Optional identifier echoed in `bash_execution_update` events (RPC request correlation). */\n\tid?: string;\n\toperations?: BashOperations;\n\tsafetyGate?: \"headless\";\n\t/** Trusted internal/test override for local bash sandboxing. */\n\tsandboxPolicy?: BashSandboxPreflight;\n}\n\nexport interface SessionBashServiceDeps {\n\treadonly settings: SettingsManager;\n\treadonly runtime: SessionBashRuntime;\n\treadonly loadoutPolicy: LoadoutAccessPolicy | undefined;\n\treadonly getCwd: () => string;\n\treadonly emit: (event: { type: \"bash_execution_update\"; id?: string; delta: string }) => void;\n\treadonly isStreaming: () => boolean;\n\treadonly pushMessage: (message: BashExecutionMessage) => void;\n\treadonly appendMessage: (message: BashExecutionMessage) => void;\n}\n\nfunction isSandboxDeniedError(error: unknown): error is Error {\n\treturn error instanceof Error && error.message.startsWith(\"sandbox: shell denied\");\n}\n\nfunction buildSandboxDeniedBashResult(reason: string): BashResult {\n\treturn { output: reason, exitCode: 1, cancelled: false, truncated: false };\n}\n\nexport class SessionBashService {\n\tprivate abortController: AbortController | undefined;\n\tprivate pendingBashMessages: BashExecutionMessage[] = [];\n\tprivate readonly deps: SessionBashServiceDeps;\n\n\tconstructor(deps: SessionBashServiceDeps) {\n\t\tthis.deps = deps;\n\t}\n\n\tasync executeBash(\n\t\tcommand: string,\n\t\tonChunk?: (chunk: string) => void,\n\t\toptions?: ExecuteBashOptions,\n\t): Promise<BashResult> {\n\t\tconst { settings, runtime, loadoutPolicy } = this.deps;\n\t\tconst prefix = settings.getShellCommandPrefix();\n\t\tconst shellPath = settings.getShellPath();\n\t\tconst resolvedCommand = prefix ? `${prefix}\\n${command}` : command;\n\t\tif (loadoutPolicy) {\n\t\t\tassertLoadoutAccess((request) => decideLoadoutAccess(loadoutPolicy, request), {\n\t\t\t\toperation: \"execute\",\n\t\t\t\ttoolName: \"bash\",\n\t\t\t\tcommand: resolvedCommand,\n\t\t\t});\n\t\t}\n\n\t\t// Non-negotiable safety floor for headless callers (RPC bash): hard-deny\n\t\t// block-tier commands before any shell is spawned.\n\t\tif (options?.safetyGate === \"headless\") {\n\t\t\tconst floorVerdict = classifyShellCommand(command);\n\t\t\tif (floorVerdict.risk === \"block\") {\n\t\t\t\tthrow new Error(`OMK §0.1 safety floor blocked bash: [${floorVerdict.rule}] ${floorVerdict.reason}`);\n\t\t\t}\n\t\t}\n\n\t\t// Command-safety parity for non-interactive callers (RPC bash). Interactive\n\t\t// `!`/`!!` bash is gated earlier through the user_bash extension event, which\n\t\t// keeps its prompt-based approval semantics, so it does not pass safetyGate and\n\t\t// is never double-prompted here. confirm/block-tier verdicts deny headlessly;\n\t\t// the EFFECTIVE command (after the shell command prefix) is classified.\n\t\tif (options?.safetyGate === \"headless\") {\n\t\t\tconst decision = await evaluateCommandGate(resolvedCommand, {\n\t\t\t\thasUI: false,\n\t\t\t\theadlessConfirmPolicy: isCommandSafetyAssumeYesEnabled() ? \"allow\" : \"deny\",\n\t\t\t});\n\t\t\tif (decision?.deny) {\n\t\t\t\tconst blocked = buildBlockedBashResult(decision.reason);\n\t\t\t\tthis.recordBashResult(command, blocked, options);\n\t\t\t\treturn blocked;\n\t\t\t}\n\t\t}\n\n\t\tthis.abortController = new AbortController();\n\n\t\ttry {\n\t\t\ttry {\n\t\t\t\tconst operations =\n\t\t\t\t\toptions?.operations ??\n\t\t\t\t\tcreateLocalBashOperations({\n\t\t\t\t\t\tshellPath,\n\t\t\t\t\t\tsandboxPolicy: runtime.sandboxPreflight(options?.sandboxPolicy),\n\t\t\t\t\t});\n\t\t\t\tconst cwd = this.deps.getCwd();\n\t\t\t\tconst onChunkWrapped = (delta: string) => {\n\t\t\t\t\tonChunk?.(delta);\n\t\t\t\t\tthis.deps.emit({ type: \"bash_execution_update\", id: options?.id, delta });\n\t\t\t\t};\n\n\t\t\t\t// Default-on verified path when a session ledger exists; OMK_VERIFIED_BASH=0 rolls back.\n\t\t\t\tconst verified = await runtime.executeVerified({\n\t\t\t\t\tresolvedCommand,\n\t\t\t\t\tcwd,\n\t\t\t\t\tshellPath,\n\t\t\t\t\toperations,\n\t\t\t\t\tsignal: this.abortController.signal,\n\t\t\t\t\tonChunk: onChunkWrapped,\n\t\t\t\t});\n\t\t\t\tconst result: BashResult =\n\t\t\t\t\tverified ??\n\t\t\t\t\t(await executeBashWithOperations(resolvedCommand, cwd, operations, {\n\t\t\t\t\t\tonChunk: onChunkWrapped,\n\t\t\t\t\t\tsignal: this.abortController.signal,\n\t\t\t\t\t}));\n\n\t\t\t\tthis.recordBashResult(command, result, options);\n\t\t\t\treturn result;\n\t\t\t} catch (error) {\n\t\t\t\tif (!isSandboxDeniedError(error)) {\n\t\t\t\t\tthrow error;\n\t\t\t\t}\n\t\t\t\tconst blocked = buildSandboxDeniedBashResult(error.message);\n\t\t\t\tthis.recordBashResult(command, blocked, options);\n\t\t\t\treturn blocked;\n\t\t\t}\n\t\t} finally {\n\t\t\tthis.abortController = undefined;\n\t\t}\n\t}\n\n\t/**\n\t * Record a bash execution result in session history.\n\t * Used by executeBash and by extensions that handle bash execution themselves.\n\t */\n\trecordBashResult(command: string, result: BashResult, options?: { excludeFromContext?: boolean }): void {\n\t\tconst bashMessage: BashExecutionMessage = {\n\t\t\trole: \"bashExecution\",\n\t\t\tcommand,\n\t\t\toutput: result.output,\n\t\t\texitCode: result.exitCode,\n\t\t\tcancelled: result.cancelled,\n\t\t\ttruncated: result.truncated,\n\t\t\tfullOutputPath: result.fullOutputPath,\n\t\t\ttimestamp: Date.now(),\n\t\t\texcludeFromContext: options?.excludeFromContext,\n\t\t};\n\n\t\t// If agent is streaming, defer adding to avoid breaking tool_use/tool_result ordering\n\t\tif (this.deps.isStreaming()) {\n\t\t\tthis.pendingBashMessages.push(bashMessage);\n\t\t} else {\n\t\t\tthis.deps.pushMessage(bashMessage);\n\t\t\tthis.deps.appendMessage(bashMessage);\n\t\t}\n\t}\n\n\t/** Cancel running bash command. */\n\tabortBash(): void {\n\t\tthis.abortController?.abort();\n\t}\n\n\tget isBashRunning(): boolean {\n\t\treturn this.abortController !== undefined;\n\t}\n\n\tget hasPendingBashMessages(): boolean {\n\t\treturn this.pendingBashMessages.length > 0;\n\t}\n\n\t/** Flush queued results after the agent turn completes (ordering contract). */\n\tflushPending(): void {\n\t\tif (this.pendingBashMessages.length === 0) return;\n\t\tfor (const bashMessage of this.pendingBashMessages) {\n\t\t\tthis.deps.pushMessage(bashMessage);\n\t\t\tthis.deps.appendMessage(bashMessage);\n\t\t}\n\t\tthis.pendingBashMessages = [];\n\t}\n}\n"]}
@@ -0,0 +1,155 @@
1
+ /**
2
+ * Session bash service: command execution, result recording, abort, and the
3
+ * streaming-deferral queue. Extracted from AgentSession so the session class
4
+ * coordinates lifecycle while this owns the bash surface end-to-end.
5
+ *
6
+ * Ordering contract (preserved from the session implementation):
7
+ * - results recorded while the agent is streaming are queued and flushed on
8
+ * turn end (`flushPending`) so tool_use/tool_result ordering never breaks;
9
+ * - `recordBashResult` is idempotent-safe to call from executeBash and from
10
+ * extensions that run bash themselves.
11
+ */
12
+ import { executeBashWithOperations } from "./bash-executor.js";
13
+ import { classifyShellCommand } from "./command-safety.js";
14
+ import { buildBlockedBashResult, evaluateCommandGate, isCommandSafetyAssumeYesEnabled, } from "./extensions/builtin/command-safety-gate.js";
15
+ import { assertLoadoutAccess, decideLoadoutAccess } from "./loadout-access-policy.js";
16
+ import { createLocalBashOperations } from "./tools/bash.js";
17
+ function isSandboxDeniedError(error) {
18
+ return error instanceof Error && error.message.startsWith("sandbox: shell denied");
19
+ }
20
+ function buildSandboxDeniedBashResult(reason) {
21
+ return { output: reason, exitCode: 1, cancelled: false, truncated: false };
22
+ }
23
+ export class SessionBashService {
24
+ abortController;
25
+ pendingBashMessages = [];
26
+ deps;
27
+ constructor(deps) {
28
+ this.deps = deps;
29
+ }
30
+ async executeBash(command, onChunk, options) {
31
+ const { settings, runtime, loadoutPolicy } = this.deps;
32
+ const prefix = settings.getShellCommandPrefix();
33
+ const shellPath = settings.getShellPath();
34
+ const resolvedCommand = prefix ? `${prefix}\n${command}` : command;
35
+ if (loadoutPolicy) {
36
+ assertLoadoutAccess((request) => decideLoadoutAccess(loadoutPolicy, request), {
37
+ operation: "execute",
38
+ toolName: "bash",
39
+ command: resolvedCommand,
40
+ });
41
+ }
42
+ // Non-negotiable safety floor for headless callers (RPC bash): hard-deny
43
+ // block-tier commands before any shell is spawned.
44
+ if (options?.safetyGate === "headless") {
45
+ const floorVerdict = classifyShellCommand(command);
46
+ if (floorVerdict.risk === "block") {
47
+ throw new Error(`OMK §0.1 safety floor blocked bash: [${floorVerdict.rule}] ${floorVerdict.reason}`);
48
+ }
49
+ }
50
+ // Command-safety parity for non-interactive callers (RPC bash). Interactive
51
+ // `!`/`!!` bash is gated earlier through the user_bash extension event, which
52
+ // keeps its prompt-based approval semantics, so it does not pass safetyGate and
53
+ // is never double-prompted here. confirm/block-tier verdicts deny headlessly;
54
+ // the EFFECTIVE command (after the shell command prefix) is classified.
55
+ if (options?.safetyGate === "headless") {
56
+ const decision = await evaluateCommandGate(resolvedCommand, {
57
+ hasUI: false,
58
+ headlessConfirmPolicy: isCommandSafetyAssumeYesEnabled() ? "allow" : "deny",
59
+ });
60
+ if (decision?.deny) {
61
+ const blocked = buildBlockedBashResult(decision.reason);
62
+ this.recordBashResult(command, blocked, options);
63
+ return blocked;
64
+ }
65
+ }
66
+ this.abortController = new AbortController();
67
+ try {
68
+ try {
69
+ const operations = options?.operations ??
70
+ createLocalBashOperations({
71
+ shellPath,
72
+ sandboxPolicy: runtime.sandboxPreflight(options?.sandboxPolicy),
73
+ });
74
+ const cwd = this.deps.getCwd();
75
+ const onChunkWrapped = (delta) => {
76
+ onChunk?.(delta);
77
+ this.deps.emit({ type: "bash_execution_update", id: options?.id, delta });
78
+ };
79
+ // Default-on verified path when a session ledger exists; OMK_VERIFIED_BASH=0 rolls back.
80
+ const verified = await runtime.executeVerified({
81
+ resolvedCommand,
82
+ cwd,
83
+ shellPath,
84
+ operations,
85
+ signal: this.abortController.signal,
86
+ onChunk: onChunkWrapped,
87
+ });
88
+ const result = verified ??
89
+ (await executeBashWithOperations(resolvedCommand, cwd, operations, {
90
+ onChunk: onChunkWrapped,
91
+ signal: this.abortController.signal,
92
+ }));
93
+ this.recordBashResult(command, result, options);
94
+ return result;
95
+ }
96
+ catch (error) {
97
+ if (!isSandboxDeniedError(error)) {
98
+ throw error;
99
+ }
100
+ const blocked = buildSandboxDeniedBashResult(error.message);
101
+ this.recordBashResult(command, blocked, options);
102
+ return blocked;
103
+ }
104
+ }
105
+ finally {
106
+ this.abortController = undefined;
107
+ }
108
+ }
109
+ /**
110
+ * Record a bash execution result in session history.
111
+ * Used by executeBash and by extensions that handle bash execution themselves.
112
+ */
113
+ recordBashResult(command, result, options) {
114
+ const bashMessage = {
115
+ role: "bashExecution",
116
+ command,
117
+ output: result.output,
118
+ exitCode: result.exitCode,
119
+ cancelled: result.cancelled,
120
+ truncated: result.truncated,
121
+ fullOutputPath: result.fullOutputPath,
122
+ timestamp: Date.now(),
123
+ excludeFromContext: options?.excludeFromContext,
124
+ };
125
+ // If agent is streaming, defer adding to avoid breaking tool_use/tool_result ordering
126
+ if (this.deps.isStreaming()) {
127
+ this.pendingBashMessages.push(bashMessage);
128
+ }
129
+ else {
130
+ this.deps.pushMessage(bashMessage);
131
+ this.deps.appendMessage(bashMessage);
132
+ }
133
+ }
134
+ /** Cancel running bash command. */
135
+ abortBash() {
136
+ this.abortController?.abort();
137
+ }
138
+ get isBashRunning() {
139
+ return this.abortController !== undefined;
140
+ }
141
+ get hasPendingBashMessages() {
142
+ return this.pendingBashMessages.length > 0;
143
+ }
144
+ /** Flush queued results after the agent turn completes (ordering contract). */
145
+ flushPending() {
146
+ if (this.pendingBashMessages.length === 0)
147
+ return;
148
+ for (const bashMessage of this.pendingBashMessages) {
149
+ this.deps.pushMessage(bashMessage);
150
+ this.deps.appendMessage(bashMessage);
151
+ }
152
+ this.pendingBashMessages = [];
153
+ }
154
+ }
155
+ //# sourceMappingURL=session-bash-service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-bash-service.js","sourceRoot":"","sources":["../../src/core/session-bash-service.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Session bash service: command execution, result recording, abort, and the\n * streaming-deferral queue. Extracted from AgentSession so the session class\n * coordinates lifecycle while this owns the bash surface end-to-end.\n *\n * Ordering contract (preserved from the session implementation):\n * - results recorded while the agent is streaming are queued and flushed on\n * turn end (`flushPending`) so tool_use/tool_result ordering never breaks;\n * - `recordBashResult` is idempotent-safe to call from executeBash and from\n * extensions that run bash themselves.\n */\nimport { type BashResult, executeBashWithOperations } from \"./bash-executor.ts\";\nimport { classifyShellCommand } from \"./command-safety.ts\";\nimport {\n\tbuildBlockedBashResult,\n\tevaluateCommandGate,\n\tisCommandSafetyAssumeYesEnabled,\n} from \"./extensions/builtin/command-safety-gate.ts\";\nimport type { LoadoutAccessPolicy } from \"./loadout-access-policy.ts\";\nimport { assertLoadoutAccess, decideLoadoutAccess } from \"./loadout-access-policy.ts\";\nimport type { BashExecutionMessage } from \"./messages.ts\";\nimport type { SessionBashRuntime } from \"./session-bash-runtime.ts\";\nimport type { SettingsManager } from \"./settings-manager.ts\";\nimport { type BashOperations, type BashSandboxPreflight, createLocalBashOperations } from \"./tools/bash.ts\";\n\nexport interface ExecuteBashOptions {\n\texcludeFromContext?: boolean;\n\t/** Optional identifier echoed in `bash_execution_update` events (RPC request correlation). */\n\tid?: string;\n\toperations?: BashOperations;\n\tsafetyGate?: \"headless\";\n\t/** Trusted internal/test override for local bash sandboxing. */\n\tsandboxPolicy?: BashSandboxPreflight;\n}\n\nexport interface SessionBashServiceDeps {\n\treadonly settings: SettingsManager;\n\treadonly runtime: SessionBashRuntime;\n\treadonly loadoutPolicy: LoadoutAccessPolicy | undefined;\n\treadonly getCwd: () => string;\n\treadonly emit: (event: { type: \"bash_execution_update\"; id?: string; delta: string }) => void;\n\treadonly isStreaming: () => boolean;\n\treadonly pushMessage: (message: BashExecutionMessage) => void;\n\treadonly appendMessage: (message: BashExecutionMessage) => void;\n}\n\nfunction isSandboxDeniedError(error: unknown): error is Error {\n\treturn error instanceof Error && error.message.startsWith(\"sandbox: shell denied\");\n}\n\nfunction buildSandboxDeniedBashResult(reason: string): BashResult {\n\treturn { output: reason, exitCode: 1, cancelled: false, truncated: false };\n}\n\nexport class SessionBashService {\n\tprivate abortController: AbortController | undefined;\n\tprivate pendingBashMessages: BashExecutionMessage[] = [];\n\tprivate readonly deps: SessionBashServiceDeps;\n\n\tconstructor(deps: SessionBashServiceDeps) {\n\t\tthis.deps = deps;\n\t}\n\n\tasync executeBash(\n\t\tcommand: string,\n\t\tonChunk?: (chunk: string) => void,\n\t\toptions?: ExecuteBashOptions,\n\t): Promise<BashResult> {\n\t\tconst { settings, runtime, loadoutPolicy } = this.deps;\n\t\tconst prefix = settings.getShellCommandPrefix();\n\t\tconst shellPath = settings.getShellPath();\n\t\tconst resolvedCommand = prefix ? `${prefix}\\n${command}` : command;\n\t\tif (loadoutPolicy) {\n\t\t\tassertLoadoutAccess((request) => decideLoadoutAccess(loadoutPolicy, request), {\n\t\t\t\toperation: \"execute\",\n\t\t\t\ttoolName: \"bash\",\n\t\t\t\tcommand: resolvedCommand,\n\t\t\t});\n\t\t}\n\n\t\t// Non-negotiable safety floor for headless callers (RPC bash): hard-deny\n\t\t// block-tier commands before any shell is spawned.\n\t\tif (options?.safetyGate === \"headless\") {\n\t\t\tconst floorVerdict = classifyShellCommand(command);\n\t\t\tif (floorVerdict.risk === \"block\") {\n\t\t\t\tthrow new Error(`OMK §0.1 safety floor blocked bash: [${floorVerdict.rule}] ${floorVerdict.reason}`);\n\t\t\t}\n\t\t}\n\n\t\t// Command-safety parity for non-interactive callers (RPC bash). Interactive\n\t\t// `!`/`!!` bash is gated earlier through the user_bash extension event, which\n\t\t// keeps its prompt-based approval semantics, so it does not pass safetyGate and\n\t\t// is never double-prompted here. confirm/block-tier verdicts deny headlessly;\n\t\t// the EFFECTIVE command (after the shell command prefix) is classified.\n\t\tif (options?.safetyGate === \"headless\") {\n\t\t\tconst decision = await evaluateCommandGate(resolvedCommand, {\n\t\t\t\thasUI: false,\n\t\t\t\theadlessConfirmPolicy: isCommandSafetyAssumeYesEnabled() ? \"allow\" : \"deny\",\n\t\t\t});\n\t\t\tif (decision?.deny) {\n\t\t\t\tconst blocked = buildBlockedBashResult(decision.reason);\n\t\t\t\tthis.recordBashResult(command, blocked, options);\n\t\t\t\treturn blocked;\n\t\t\t}\n\t\t}\n\n\t\tthis.abortController = new AbortController();\n\n\t\ttry {\n\t\t\ttry {\n\t\t\t\tconst operations =\n\t\t\t\t\toptions?.operations ??\n\t\t\t\t\tcreateLocalBashOperations({\n\t\t\t\t\t\tshellPath,\n\t\t\t\t\t\tsandboxPolicy: runtime.sandboxPreflight(options?.sandboxPolicy),\n\t\t\t\t\t});\n\t\t\t\tconst cwd = this.deps.getCwd();\n\t\t\t\tconst onChunkWrapped = (delta: string) => {\n\t\t\t\t\tonChunk?.(delta);\n\t\t\t\t\tthis.deps.emit({ type: \"bash_execution_update\", id: options?.id, delta });\n\t\t\t\t};\n\n\t\t\t\t// Default-on verified path when a session ledger exists; OMK_VERIFIED_BASH=0 rolls back.\n\t\t\t\tconst verified = await runtime.executeVerified({\n\t\t\t\t\tresolvedCommand,\n\t\t\t\t\tcwd,\n\t\t\t\t\tshellPath,\n\t\t\t\t\toperations,\n\t\t\t\t\tsignal: this.abortController.signal,\n\t\t\t\t\tonChunk: onChunkWrapped,\n\t\t\t\t});\n\t\t\t\tconst result: BashResult =\n\t\t\t\t\tverified ??\n\t\t\t\t\t(await executeBashWithOperations(resolvedCommand, cwd, operations, {\n\t\t\t\t\t\tonChunk: onChunkWrapped,\n\t\t\t\t\t\tsignal: this.abortController.signal,\n\t\t\t\t\t}));\n\n\t\t\t\tthis.recordBashResult(command, result, options);\n\t\t\t\treturn result;\n\t\t\t} catch (error) {\n\t\t\t\tif (!isSandboxDeniedError(error)) {\n\t\t\t\t\tthrow error;\n\t\t\t\t}\n\t\t\t\tconst blocked = buildSandboxDeniedBashResult(error.message);\n\t\t\t\tthis.recordBashResult(command, blocked, options);\n\t\t\t\treturn blocked;\n\t\t\t}\n\t\t} finally {\n\t\t\tthis.abortController = undefined;\n\t\t}\n\t}\n\n\t/**\n\t * Record a bash execution result in session history.\n\t * Used by executeBash and by extensions that handle bash execution themselves.\n\t */\n\trecordBashResult(command: string, result: BashResult, options?: { excludeFromContext?: boolean }): void {\n\t\tconst bashMessage: BashExecutionMessage = {\n\t\t\trole: \"bashExecution\",\n\t\t\tcommand,\n\t\t\toutput: result.output,\n\t\t\texitCode: result.exitCode,\n\t\t\tcancelled: result.cancelled,\n\t\t\ttruncated: result.truncated,\n\t\t\tfullOutputPath: result.fullOutputPath,\n\t\t\ttimestamp: Date.now(),\n\t\t\texcludeFromContext: options?.excludeFromContext,\n\t\t};\n\n\t\t// If agent is streaming, defer adding to avoid breaking tool_use/tool_result ordering\n\t\tif (this.deps.isStreaming()) {\n\t\t\tthis.pendingBashMessages.push(bashMessage);\n\t\t} else {\n\t\t\tthis.deps.pushMessage(bashMessage);\n\t\t\tthis.deps.appendMessage(bashMessage);\n\t\t}\n\t}\n\n\t/** Cancel running bash command. */\n\tabortBash(): void {\n\t\tthis.abortController?.abort();\n\t}\n\n\tget isBashRunning(): boolean {\n\t\treturn this.abortController !== undefined;\n\t}\n\n\tget hasPendingBashMessages(): boolean {\n\t\treturn this.pendingBashMessages.length > 0;\n\t}\n\n\t/** Flush queued results after the agent turn completes (ordering contract). */\n\tflushPending(): void {\n\t\tif (this.pendingBashMessages.length === 0) return;\n\t\tfor (const bashMessage of this.pendingBashMessages) {\n\t\t\tthis.deps.pushMessage(bashMessage);\n\t\t\tthis.deps.appendMessage(bashMessage);\n\t\t}\n\t\tthis.pendingBashMessages = [];\n\t}\n}\n"]}
@@ -0,0 +1,48 @@
1
+ import type { Api, Message, Model } from "omk-ai";
2
+ import type { CompactionResult } from "./compaction/index.ts";
3
+ import { type CompactionBarrierResult, type CompactionEnvelope, type CompactionPreservedProvenanceInput, type CompactionSourceIdentity, type CompactionTransaction } from "./compaction/transaction.ts";
4
+ import type { SessionIntegrityReport } from "./session-integrity.ts";
5
+ import type { CompactionEntry, SessionEntry, SessionManager } from "./session-manager.ts";
6
+ export interface CapturedCompactionState {
7
+ readonly report: SessionIntegrityReport;
8
+ readonly branchEntries: readonly SessionEntry[];
9
+ readonly revision: CompactionTransaction["baseRevision"];
10
+ readonly source: CompactionSourceIdentity;
11
+ }
12
+ export interface BegunCompaction {
13
+ readonly capture: CapturedCompactionState;
14
+ readonly transaction: CompactionTransaction;
15
+ }
16
+ export interface CommittedCompaction {
17
+ readonly entry: CompactionEntry;
18
+ readonly envelope: CompactionEnvelope;
19
+ }
20
+ export interface SessionCompactionServiceDeps {
21
+ readonly sessionManager: SessionManager;
22
+ readonly pendingToolCallIds: () => ReadonlySet<string>;
23
+ readonly getUserMessageText: (message: Message) => string;
24
+ readonly cwd: string;
25
+ readonly invalidateContextBudget: () => void;
26
+ /** Refresh agent messages from the session manager after tail repair/commit. */
27
+ readonly refreshAgentMessages: () => void;
28
+ /** Hysteresis bookkeeping after a successful commit. */
29
+ readonly recordCommit: () => void;
30
+ }
31
+ export declare class SessionCompactionService {
32
+ private readonly deps;
33
+ constructor(deps: SessionCompactionServiceDeps);
34
+ captureState(): CapturedCompactionState;
35
+ private captureStateLocked;
36
+ barrierError(barrier: CompactionBarrierResult): Error;
37
+ evaluateBarrier(capture: CapturedCompactionState, includeMissingTailAsPending: boolean, excludedPendingIds?: ReadonlySet<string>): CompactionBarrierResult;
38
+ repairEmergencyTail(capture: CapturedCompactionState): {
39
+ readonly capture: CapturedCompactionState;
40
+ readonly repairedToolCallIds: ReadonlySet<string>;
41
+ };
42
+ priorCommittedSourceDigests(): string[];
43
+ beginTransaction(compactionModel: Model<Api>, emergency: boolean): BegunCompaction;
44
+ captureProvenance(capture: CapturedCompactionState): CompactionPreservedProvenanceInput;
45
+ detailsWithEnvelope(details: unknown, envelope: CompactionEnvelope): unknown;
46
+ commit(begun: BegunCompaction, result: CompactionResult, fromExtension: boolean): CommittedCompaction;
47
+ }
48
+ //# sourceMappingURL=session-compaction-service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-compaction-service.d.ts","sourceRoot":"","sources":["../../src/core/session-compaction-service.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,GAAG,EAAE,OAAO,EAAE,KAAK,EAAqB,MAAM,QAAQ,CAAC;AAErE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAC9D,OAAO,EACN,KAAK,uBAAuB,EAC5B,KAAK,kBAAkB,EACvB,KAAK,kCAAkC,EACvC,KAAK,wBAAwB,EAC7B,KAAK,qBAAqB,EAO1B,MAAM,6BAA6B,CAAC;AAErC,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,wBAAwB,CAAC;AAErE,OAAO,KAAK,EAAE,eAAe,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAE1F,MAAM,WAAW,uBAAuB;IACvC,QAAQ,CAAC,MAAM,EAAE,sBAAsB,CAAC;IACxC,QAAQ,CAAC,aAAa,EAAE,SAAS,YAAY,EAAE,CAAC;IAChD,QAAQ,CAAC,QAAQ,EAAE,qBAAqB,CAAC,cAAc,CAAC,CAAC;IACzD,QAAQ,CAAC,MAAM,EAAE,wBAAwB,CAAC;CAC1C;AAED,MAAM,WAAW,eAAe;IAC/B,QAAQ,CAAC,OAAO,EAAE,uBAAuB,CAAC;IAC1C,QAAQ,CAAC,WAAW,EAAE,qBAAqB,CAAC;CAC5C;AAED,MAAM,WAAW,mBAAmB;IACnC,QAAQ,CAAC,KAAK,EAAE,eAAe,CAAC;IAChC,QAAQ,CAAC,QAAQ,EAAE,kBAAkB,CAAC;CACtC;AAED,MAAM,WAAW,4BAA4B;IAC5C,QAAQ,CAAC,cAAc,EAAE,cAAc,CAAC;IACxC,QAAQ,CAAC,kBAAkB,EAAE,MAAM,WAAW,CAAC,MAAM,CAAC,CAAC;IACvD,QAAQ,CAAC,kBAAkB,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,MAAM,CAAC;IAC1D,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,uBAAuB,EAAE,MAAM,IAAI,CAAC;IAC7C,gFAAgF;IAChF,QAAQ,CAAC,oBAAoB,EAAE,MAAM,IAAI,CAAC;IAC1C,wDAAwD;IACxD,QAAQ,CAAC,YAAY,EAAE,MAAM,IAAI,CAAC;CAClC;AAED,qBAAa,wBAAwB;IACpC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAA+B;IAEpD,YAAY,IAAI,EAAE,4BAA4B,EAE7C;IAED,YAAY,IAAI,uBAAuB,CAEtC;IAED,OAAO,CAAC,kBAAkB;IAmD1B,YAAY,CAAC,OAAO,EAAE,uBAAuB,GAAG,KAAK,CAOpD;IAED,eAAe,CACd,OAAO,EAAE,uBAAuB,EAChC,2BAA2B,EAAE,OAAO,EACpC,kBAAkB,GAAE,WAAW,CAAC,MAAM,CAAa,GACjD,uBAAuB,CAQzB;IAED,mBAAmB,CAAC,OAAO,EAAE,uBAAuB,GAAG;QACtD,QAAQ,CAAC,OAAO,EAAE,uBAAuB,CAAC;QAC1C,QAAQ,CAAC,mBAAmB,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;KAClD,CA+BA;IAED,2BAA2B,IAAI,MAAM,EAAE,CAYtC;IAED,gBAAgB,CAAC,eAAe,EAAE,KAAK,CAAC,GAAG,CAAC,EAAE,SAAS,EAAE,OAAO,GAAG,eAAe,CAoBjF;IAED,iBAAiB,CAAC,OAAO,EAAE,uBAAuB,GAAG,kCAAkC,CA0CtF;IAED,mBAAmB,CAAC,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,kBAAkB,GAAG,OAAO,CAQ3E;IAED,MAAM,CAAC,KAAK,EAAE,eAAe,EAAE,MAAM,EAAE,gBAAgB,EAAE,aAAa,EAAE,OAAO,GAAG,mBAAmB,CAgDpG;CACD","sourcesContent":["/**\n * Session compaction service: capture / barrier / repair / transaction / commit.\n *\n * Extracted from AgentSession so the session keeps lifecycle orchestration\n * (abort controllers, extension events, auth, model resolution) while this\n * owns the integrity-critical compaction state machine. Every method preserves\n * the exact semantics of the original session implementation — the\n * compaction-transaction / session-integrity suites pin them.\n */\nimport { createHash, randomUUID } from \"node:crypto\";\nimport { existsSync, readFileSync } from \"node:fs\";\nimport { repairTranscriptIntegrity } from \"omk-agent-core\";\nimport type { Api, Message, Model, ToolResultMessage } from \"omk-ai\";\nimport { sanitizeBinaryOutput } from \"../utils/shell.ts\";\nimport type { CompactionResult } from \"./compaction/index.ts\";\nimport {\n\ttype CompactionBarrierResult,\n\ttype CompactionEnvelope,\n\ttype CompactionPreservedProvenanceInput,\n\ttype CompactionSourceIdentity,\n\ttype CompactionTransaction,\n\tcreateCompactionEnvelope,\n\tcreateCompactionSourceIdentity,\n\tcreateCompactionTransaction,\n\tdecideCompactionCommit,\n\tevaluateCompactionBarrier,\n\tvalidateCompactionEnvelope,\n} from \"./compaction/transaction.ts\";\nimport { redactSensitiveText } from \"./redaction.ts\";\nimport type { SessionIntegrityReport } from \"./session-integrity.ts\";\nimport { inspectSessionIntegrity } from \"./session-integrity.ts\";\nimport type { CompactionEntry, SessionEntry, SessionManager } from \"./session-manager.ts\";\n\nexport interface CapturedCompactionState {\n\treadonly report: SessionIntegrityReport;\n\treadonly branchEntries: readonly SessionEntry[];\n\treadonly revision: CompactionTransaction[\"baseRevision\"];\n\treadonly source: CompactionSourceIdentity;\n}\n\nexport interface BegunCompaction {\n\treadonly capture: CapturedCompactionState;\n\treadonly transaction: CompactionTransaction;\n}\n\nexport interface CommittedCompaction {\n\treadonly entry: CompactionEntry;\n\treadonly envelope: CompactionEnvelope;\n}\n\nexport interface SessionCompactionServiceDeps {\n\treadonly sessionManager: SessionManager;\n\treadonly pendingToolCallIds: () => ReadonlySet<string>;\n\treadonly getUserMessageText: (message: Message) => string;\n\treadonly cwd: string;\n\treadonly invalidateContextBudget: () => void;\n\t/** Refresh agent messages from the session manager after tail repair/commit. */\n\treadonly refreshAgentMessages: () => void;\n\t/** Hysteresis bookkeeping after a successful commit. */\n\treadonly recordCommit: () => void;\n}\n\nexport class SessionCompactionService {\n\tprivate readonly deps: SessionCompactionServiceDeps;\n\n\tconstructor(deps: SessionCompactionServiceDeps) {\n\t\tthis.deps = deps;\n\t}\n\n\tcaptureState(): CapturedCompactionState {\n\t\treturn this.deps.sessionManager.withCompactionCommitLock(() => this.captureStateLocked());\n\t}\n\n\tprivate captureStateLocked(): CapturedCompactionState {\n\t\tconst { sessionManager } = this.deps;\n\t\tconst sessionFile = sessionManager.getSessionFile();\n\t\tconst bytes =\n\t\t\tsessionFile && existsSync(sessionFile)\n\t\t\t\t? new Uint8Array(readFileSync(sessionFile))\n\t\t\t\t: new TextEncoder().encode(\n\t\t\t\t\t\t`${[sessionManager.getHeader(), ...sessionManager.getEntries()]\n\t\t\t\t\t\t\t.filter((entry) => entry !== null)\n\t\t\t\t\t\t\t.map((entry) => JSON.stringify(entry))\n\t\t\t\t\t\t\t.join(\"\\n\")}\\n`,\n\t\t\t\t\t);\n\t\tconst report = inspectSessionIntegrity(bytes, { activeLeafId: sessionManager.getLeafId() });\n\t\tconst branchEntries = report.activeBranch;\n\t\tlet latestCompactionIndex = -1;\n\t\tfor (let index = branchEntries.length - 1; index >= 0; index -= 1) {\n\t\t\tif (branchEntries[index]?.type === \"compaction\") {\n\t\t\t\tlatestCompactionIndex = index;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\tconst latestCompaction = branchEntries[latestCompactionIndex];\n\t\tconst firstKeptIndex =\n\t\t\tlatestCompaction?.type === \"compaction\"\n\t\t\t\t? branchEntries.findIndex((entry) => entry.id === latestCompaction.firstKeptEntryId)\n\t\t\t\t: -1;\n\t\tconst sourceEntries = branchEntries.slice(\n\t\t\tlatestCompactionIndex < 0 ? 0 : firstKeptIndex < 0 ? latestCompactionIndex : firstKeptIndex,\n\t\t);\n\t\tconst firstEntry = sourceEntries[0];\n\t\tconst lastEntry = sourceEntries.at(-1);\n\t\tif (!firstEntry || !lastEntry || report.activeLeafId === null) {\n\t\t\tconst barrier = evaluateCompactionBarrier(report, [...this.deps.pendingToolCallIds()]);\n\t\t\tif (barrier.status !== \"ready\") throw this.barrierError(barrier);\n\t\t\tthrow new Error(\"Nothing to compact: the active session branch is empty\");\n\t\t}\n\t\tconst revision = sessionManager.getDurableHeadToken();\n\t\tconst source = createCompactionSourceIdentity({\n\t\t\tsessionId: revision.sessionId,\n\t\t\tentryIds: sourceEntries.map((entry) => entry.id),\n\t\t\tfirstEntryId: firstEntry.id,\n\t\t\tlastEntryId: lastEntry.id,\n\t\t\tsourceSha256: createHash(\"sha256\")\n\t\t\t\t.update(sourceEntries.map((entry) => JSON.stringify(entry)).join(\"\\n\"), \"utf8\")\n\t\t\t\t.digest(\"hex\"),\n\t\t\tactiveLeafId: report.activeLeafId,\n\t\t\tmessageCount: report.activeMessages.length,\n\t\t});\n\t\treturn { report, branchEntries, revision, source };\n\t}\n\n\tbarrierError(barrier: CompactionBarrierResult): Error {\n\t\tif (barrier.status === \"defer\") {\n\t\t\treturn new Error(`Compaction deferred until the transcript closes (${barrier.reason})`);\n\t\t}\n\t\treturn new Error(\n\t\t\t`Compaction failed closed on transcript integrity (${barrier.reason}). Run the session doctor before retrying.`,\n\t\t);\n\t}\n\n\tevaluateBarrier(\n\t\tcapture: CapturedCompactionState,\n\t\tincludeMissingTailAsPending: boolean,\n\t\texcludedPendingIds: ReadonlySet<string> = new Set(),\n\t): CompactionBarrierResult {\n\t\tconst pending = new Set([...this.deps.pendingToolCallIds()].filter((id) => !excludedPendingIds.has(id)));\n\t\tif (includeMissingTailAsPending) {\n\t\t\tfor (const issue of capture.report.transcript?.issues ?? []) {\n\t\t\t\tif (issue.kind === \"missing_result\") pending.add(issue.toolCallId);\n\t\t\t}\n\t\t}\n\t\treturn evaluateCompactionBarrier(capture.report, [...pending]);\n\t}\n\n\trepairEmergencyTail(capture: CapturedCompactionState): {\n\t\treadonly capture: CapturedCompactionState;\n\t\treadonly repairedToolCallIds: ReadonlySet<string>;\n\t} {\n\t\tconst { sessionManager } = this.deps;\n\t\tconst barrier = this.evaluateBarrier(capture, true);\n\t\tif (barrier.status !== \"defer\" || barrier.reason !== \"missing_active_tail_results\") {\n\t\t\tif (barrier.status !== \"ready\") throw this.barrierError(barrier);\n\t\t\treturn { capture, repairedToolCallIds: new Set() };\n\t\t}\n\t\tconst repairedMessages = repairTranscriptIntegrity(\n\t\t\t[...capture.report.activeMessages],\n\t\t\t\"Tool result missing; synthesized to close an emergency compaction barrier\",\n\t\t);\n\t\tconst inserted = repairedMessages.slice(capture.report.activeMessages.length);\n\t\tconst repairedToolCallIds = new Set<string>();\n\t\tfor (const message of inserted) {\n\t\t\tif (message.role !== \"toolResult\") {\n\t\t\t\tthrow new Error(\"Emergency compaction repair produced a non-tool result\");\n\t\t\t}\n\t\t\tconst toolResult: ToolResultMessage = message;\n\t\t\trepairedToolCallIds.add(toolResult.toolCallId);\n\t\t\tsessionManager.appendMessage(toolResult);\n\t\t}\n\t\tsessionManager.appendCustomEntry(\"compaction_transcript_repaired\", {\n\t\t\tinsertedToolCallIds: [...repairedToolCallIds],\n\t\t\treason: \"emergency_compaction\",\n\t\t});\n\t\tthis.deps.invalidateContextBudget();\n\t\tconst closedCapture = this.captureState();\n\t\tconst closedBarrier = this.evaluateBarrier(closedCapture, false, repairedToolCallIds);\n\t\tif (closedBarrier.status !== \"ready\") throw this.barrierError(closedBarrier);\n\t\tthis.deps.refreshAgentMessages();\n\t\treturn { capture: closedCapture, repairedToolCallIds };\n\t}\n\n\tpriorCommittedSourceDigests(): string[] {\n\t\tconst digests: string[] = [];\n\t\tfor (const entry of this.deps.sessionManager.getEntries()) {\n\t\t\tif (entry.type !== \"compaction\" || typeof entry.details !== \"object\" || entry.details === null) continue;\n\t\t\tif (!Object.hasOwn(entry.details, \"compactionEnvelope\")) continue;\n\t\t\tconst envelope = validateCompactionEnvelope(Reflect.get(entry.details, \"compactionEnvelope\"));\n\t\t\tif (envelope.summary !== entry.summary) {\n\t\t\t\tthrow new Error(`Compaction entry ${entry.id} has invalid provenance. Run the session doctor.`);\n\t\t\t}\n\t\t\tdigests.push(envelope.source.sourceSha256);\n\t\t}\n\t\treturn digests;\n\t}\n\n\tbeginTransaction(compactionModel: Model<Api>, emergency: boolean): BegunCompaction {\n\t\tlet capture = this.captureState();\n\t\tif (emergency) {\n\t\t\tcapture = this.repairEmergencyTail(capture).capture;\n\t\t} else {\n\t\t\tconst barrier = this.evaluateBarrier(capture, false);\n\t\t\tif (barrier.status !== \"ready\") throw this.barrierError(barrier);\n\t\t}\n\t\tconst transaction = createCompactionTransaction({\n\t\t\ttransactionId: randomUUID(),\n\t\t\tbaseRevision: capture.revision,\n\t\t\tsource: capture.source,\n\t\t\tcreatedAt: new Date().toISOString(),\n\t\t\tmodel: { provider: compactionModel.provider, id: compactionModel.id },\n\t\t\tpreserved: this.captureProvenance(capture),\n\t\t});\n\t\tif (this.priorCommittedSourceDigests().includes(transaction.source.sourceSha256)) {\n\t\t\tthrow new Error(\"This exact compaction source was already compacted\");\n\t\t}\n\t\treturn { capture, transaction };\n\t}\n\n\tcaptureProvenance(capture: CapturedCompactionState): CompactionPreservedProvenanceInput {\n\t\tlet latestIntent = \"Continue the current session\";\n\t\tfor (let index = capture.report.activeMessages.length - 1; index >= 0; index -= 1) {\n\t\t\tconst message = capture.report.activeMessages[index];\n\t\t\tif (message?.role !== \"user\") continue;\n\t\t\tconst candidate = sanitizeBinaryOutput(redactSensitiveText(this.deps.getUserMessageText(message)).trim())\n\t\t\t\t.replace(/[\\u0000-\\u0008\\u000b\\u000c\\u000e-\\u001f\\u007f]/gu, \"\")\n\t\t\t\t.slice(0, 16_384);\n\t\t\tif (candidate.length > 0) latestIntent = candidate;\n\t\t\tbreak;\n\t\t}\n\t\tconst modelHistory = capture.branchEntries\n\t\t\t.flatMap((entry) => {\n\t\t\t\tif (entry.type === \"model_change\") {\n\t\t\t\t\treturn [{ entryId: entry.id, provider: entry.provider, modelId: entry.modelId }];\n\t\t\t\t}\n\t\t\t\tif (entry.type === \"message\" && entry.message.role === \"assistant\") {\n\t\t\t\t\treturn [{ entryId: entry.id, provider: entry.message.provider, modelId: entry.message.model }];\n\t\t\t\t}\n\t\t\t\treturn [];\n\t\t\t})\n\t\t\t.slice(-256);\n\t\tconst customEntryIds = (customType: string): string[] =>\n\t\t\tcapture.branchEntries\n\t\t\t\t.filter((entry) => entry.type === \"custom\" && entry.customType === customType)\n\t\t\t\t.map((entry) => entry.id);\n\t\treturn {\n\t\t\tlatestIntent,\n\t\t\topenTasks: [],\n\t\t\tlaneIds: customEntryIds(\"lane\"),\n\t\t\tacceptancePredicateIds: customEntryIds(\"acceptance_predicate\"),\n\t\t\tevidenceReceiptIds: customEntryIds(\"evidence_receipt\"),\n\t\t\tblockerReasons: [],\n\t\t\trepairEventIds: [\n\t\t\t\t...customEntryIds(\"transcript_repaired\"),\n\t\t\t\t...customEntryIds(\"compaction_transcript_repaired\"),\n\t\t\t],\n\t\t\tbranch: null,\n\t\t\tworktree: this.deps.cwd,\n\t\t\tmodelHistory,\n\t\t\tnextAction: latestIntent.slice(0, 4096) || \"Continue the current session\",\n\t\t};\n\t}\n\n\tdetailsWithEnvelope(details: unknown, envelope: CompactionEnvelope): unknown {\n\t\tif (typeof details === \"object\" && details !== null && !Array.isArray(details)) {\n\t\t\treturn { ...details, compactionEnvelope: envelope };\n\t\t}\n\t\treturn {\n\t\t\tcompactionEnvelope: envelope,\n\t\t\t...(details === undefined ? {} : { resultDetails: details }),\n\t\t};\n\t}\n\n\tcommit(begun: BegunCompaction, result: CompactionResult, fromExtension: boolean): CommittedCompaction {\n\t\tif (!begun.transaction.source.entryIds.includes(result.firstKeptEntryId)) {\n\t\t\tthrow new Error(\"Compaction first-kept entry is outside the captured source\");\n\t\t}\n\t\tconst committed = this.deps.sessionManager.withCompactionCommitLock(() => {\n\t\t\tconst current = this.captureState();\n\t\t\tconst barrier = this.evaluateBarrier(current, false);\n\t\t\tconst decision = decideCompactionCommit({\n\t\t\t\ttransaction: begun.transaction,\n\t\t\t\tcurrentRevision: current.revision,\n\t\t\t\tcurrentSource: current.source,\n\t\t\t\tbarrier,\n\t\t\t\tpriorCommittedSourceDigests: this.priorCommittedSourceDigests(),\n\t\t\t});\n\t\t\tswitch (decision.decision) {\n\t\t\t\tcase \"duplicate\":\n\t\t\t\t\tthrow new Error(\"This exact compaction source was already compacted\");\n\t\t\t\tcase \"stale\":\n\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t`Session changed during compaction (${decision.reason}); generated summary was discarded`,\n\t\t\t\t\t);\n\t\t\t\tcase \"defer\":\n\t\t\t\tcase \"fail_closed\":\n\t\t\t\t\tthrow this.barrierError(barrier);\n\t\t\t\tcase \"commit\": {\n\t\t\t\t\tconst envelope = createCompactionEnvelope({\n\t\t\t\t\t\ttransaction: begun.transaction,\n\t\t\t\t\t\tdecision,\n\t\t\t\t\t\tsummary: result.summary,\n\t\t\t\t\t\tsummarySha256: createHash(\"sha256\").update(result.summary, \"utf8\").digest(\"hex\"),\n\t\t\t\t\t});\n\t\t\t\t\tconst entryId = this.deps.sessionManager.appendCompaction(\n\t\t\t\t\t\tresult.summary,\n\t\t\t\t\t\tresult.firstKeptEntryId,\n\t\t\t\t\t\tresult.tokensBefore,\n\t\t\t\t\t\tthis.detailsWithEnvelope(result.details, envelope),\n\t\t\t\t\t\tfromExtension,\n\t\t\t\t\t);\n\t\t\t\t\tconst entry = this.deps.sessionManager.getEntry(entryId);\n\t\t\t\t\tif (!entry || entry.type !== \"compaction\") {\n\t\t\t\t\t\tthrow new Error(\"Compaction commit did not produce a compaction entry\");\n\t\t\t\t\t}\n\t\t\t\t\treturn { entry, envelope };\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\t\tthis.deps.recordCommit();\n\t\treturn committed;\n\t}\n}\n"]}