open-multi-agent-kit 0.92.0 → 0.94.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (173) hide show
  1. package/CHANGELOG.md +38 -1
  2. package/README.md +2 -2
  3. package/dist/core/agent-session.d.ts +36 -14
  4. package/dist/core/agent-session.d.ts.map +1 -1
  5. package/dist/core/agent-session.js +155 -357
  6. package/dist/core/agent-session.js.map +1 -1
  7. package/dist/core/compaction/branch-summarization.d.ts +5 -1
  8. package/dist/core/compaction/branch-summarization.d.ts.map +1 -1
  9. package/dist/core/compaction/branch-summarization.js +5 -7
  10. package/dist/core/compaction/branch-summarization.js.map +1 -1
  11. package/dist/core/compaction/compaction.d.ts +12 -3
  12. package/dist/core/compaction/compaction.d.ts.map +1 -1
  13. package/dist/core/compaction/compaction.js +28 -15
  14. package/dist/core/compaction/compaction.js.map +1 -1
  15. package/dist/core/extensions/builtin/command-safety-gate.d.ts +8 -0
  16. package/dist/core/extensions/builtin/command-safety-gate.d.ts.map +1 -1
  17. package/dist/core/extensions/builtin/command-safety-gate.js +23 -0
  18. package/dist/core/extensions/builtin/command-safety-gate.js.map +1 -1
  19. package/dist/core/extensions/runner.d.ts.map +1 -1
  20. package/dist/core/extensions/runner.js +4 -0
  21. package/dist/core/extensions/runner.js.map +1 -1
  22. package/dist/core/extensions/types.d.ts +2 -0
  23. package/dist/core/extensions/types.d.ts.map +1 -1
  24. package/dist/core/extensions/types.js.map +1 -1
  25. package/dist/core/keybindings.d.ts +5 -0
  26. package/dist/core/keybindings.d.ts.map +1 -1
  27. package/dist/core/keybindings.js +5 -0
  28. package/dist/core/keybindings.js.map +1 -1
  29. package/dist/core/model-resolver.d.ts +41 -3
  30. package/dist/core/model-resolver.d.ts.map +1 -1
  31. package/dist/core/model-resolver.js +17 -9
  32. package/dist/core/model-resolver.js.map +1 -1
  33. package/dist/core/package-manager.d.ts.map +1 -1
  34. package/dist/core/package-manager.js +11 -1
  35. package/dist/core/package-manager.js.map +1 -1
  36. package/dist/core/provider-resilience.d.ts.map +1 -1
  37. package/dist/core/provider-resilience.js +3 -1
  38. package/dist/core/provider-resilience.js.map +1 -1
  39. package/dist/core/reasoning-router-v4-weights.d.ts.map +1 -1
  40. package/dist/core/reasoning-router-v4-weights.js +9 -3
  41. package/dist/core/reasoning-router-v4-weights.js.map +1 -1
  42. package/dist/core/reasoning-router-v4.d.ts.map +1 -1
  43. package/dist/core/reasoning-router-v4.js +94 -24
  44. package/dist/core/reasoning-router-v4.js.map +1 -1
  45. package/dist/core/resource-loader.d.ts.map +1 -1
  46. package/dist/core/resource-loader.js +11 -5
  47. package/dist/core/resource-loader.js.map +1 -1
  48. package/dist/core/sandbox/default-policy.d.ts +14 -0
  49. package/dist/core/sandbox/default-policy.d.ts.map +1 -0
  50. package/dist/core/sandbox/default-policy.js +58 -0
  51. package/dist/core/sandbox/default-policy.js.map +1 -0
  52. package/dist/core/sdk.d.ts.map +1 -1
  53. package/dist/core/sdk.js +1 -1
  54. package/dist/core/sdk.js.map +1 -1
  55. package/dist/core/session-bash-runtime.d.ts +62 -0
  56. package/dist/core/session-bash-runtime.d.ts.map +1 -0
  57. package/dist/core/session-bash-runtime.js +167 -0
  58. package/dist/core/session-bash-runtime.js.map +1 -0
  59. package/dist/core/session-bash-service.d.ts +61 -0
  60. package/dist/core/session-bash-service.d.ts.map +1 -0
  61. package/dist/core/session-bash-service.js +155 -0
  62. package/dist/core/session-bash-service.js.map +1 -0
  63. package/dist/core/session-compaction-service.d.ts +48 -0
  64. package/dist/core/session-compaction-service.d.ts.map +1 -0
  65. package/dist/core/session-compaction-service.js +250 -0
  66. package/dist/core/session-compaction-service.js.map +1 -0
  67. package/dist/core/session-file-compaction.d.ts.map +1 -1
  68. package/dist/core/session-file-compaction.js +39 -5
  69. package/dist/core/session-file-compaction.js.map +1 -1
  70. package/dist/core/session-termination.d.ts.map +1 -1
  71. package/dist/core/session-termination.js +1 -1
  72. package/dist/core/session-termination.js.map +1 -1
  73. package/dist/core/settings-manager.d.ts +3 -0
  74. package/dist/core/settings-manager.d.ts.map +1 -1
  75. package/dist/core/settings-manager.js +8 -0
  76. package/dist/core/settings-manager.js.map +1 -1
  77. package/dist/core/skills-catalog-cache.d.ts +25 -0
  78. package/dist/core/skills-catalog-cache.d.ts.map +1 -0
  79. package/dist/core/skills-catalog-cache.js +123 -0
  80. package/dist/core/skills-catalog-cache.js.map +1 -0
  81. package/dist/core/skills.d.ts +6 -0
  82. package/dist/core/skills.d.ts.map +1 -1
  83. package/dist/core/skills.js +25 -4
  84. package/dist/core/skills.js.map +1 -1
  85. package/dist/core/system-prompt.d.ts.map +1 -1
  86. package/dist/core/system-prompt.js +2 -2
  87. package/dist/core/system-prompt.js.map +1 -1
  88. package/dist/core/tools/bash.d.ts +5 -0
  89. package/dist/core/tools/bash.d.ts.map +1 -1
  90. package/dist/core/tools/bash.js +41 -13
  91. package/dist/core/tools/bash.js.map +1 -1
  92. package/dist/core/tools/diagnostics.d.ts +29 -0
  93. package/dist/core/tools/diagnostics.d.ts.map +1 -0
  94. package/dist/core/tools/diagnostics.js +325 -0
  95. package/dist/core/tools/diagnostics.js.map +1 -0
  96. package/dist/core/tools/index.d.ts +2 -1
  97. package/dist/core/tools/index.d.ts.map +1 -1
  98. package/dist/core/tools/index.js +18 -1
  99. package/dist/core/tools/index.js.map +1 -1
  100. package/dist/core/verified-bash-adapter.d.ts +18 -1
  101. package/dist/core/verified-bash-adapter.d.ts.map +1 -1
  102. package/dist/core/verified-bash-adapter.js +38 -1
  103. package/dist/core/verified-bash-adapter.js.map +1 -1
  104. package/dist/core/verified-bash-runtime.d.ts +13 -0
  105. package/dist/core/verified-bash-runtime.d.ts.map +1 -0
  106. package/dist/core/verified-bash-runtime.js +81 -0
  107. package/dist/core/verified-bash-runtime.js.map +1 -0
  108. package/dist/index.d.ts +4 -2
  109. package/dist/index.d.ts.map +1 -1
  110. package/dist/index.js +4 -2
  111. package/dist/index.js.map +1 -1
  112. package/dist/modes/interactive/components/control-panel-box.d.ts +27 -0
  113. package/dist/modes/interactive/components/control-panel-box.d.ts.map +1 -1
  114. package/dist/modes/interactive/components/control-panel-box.js +60 -8
  115. package/dist/modes/interactive/components/control-panel-box.js.map +1 -1
  116. package/dist/modes/interactive/components/control-panel-layout.d.ts.map +1 -1
  117. package/dist/modes/interactive/components/control-panel-layout.js +100 -45
  118. package/dist/modes/interactive/components/control-panel-layout.js.map +1 -1
  119. package/dist/modes/interactive/components/control-panel-runtime-status.d.ts +9 -0
  120. package/dist/modes/interactive/components/control-panel-runtime-status.d.ts.map +1 -1
  121. package/dist/modes/interactive/components/control-panel-runtime-status.js +10 -8
  122. package/dist/modes/interactive/components/control-panel-runtime-status.js.map +1 -1
  123. package/dist/modes/interactive/components/control-panel-sparkles.d.ts +5 -0
  124. package/dist/modes/interactive/components/control-panel-sparkles.d.ts.map +1 -1
  125. package/dist/modes/interactive/components/control-panel-sparkles.js +14 -9
  126. package/dist/modes/interactive/components/control-panel-sparkles.js.map +1 -1
  127. package/dist/modes/interactive/components/footer.d.ts +10 -0
  128. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  129. package/dist/modes/interactive/components/footer.js +3 -3
  130. package/dist/modes/interactive/components/footer.js.map +1 -1
  131. package/dist/modes/interactive/components/status-sidebar.d.ts +49 -0
  132. package/dist/modes/interactive/components/status-sidebar.d.ts.map +1 -0
  133. package/dist/modes/interactive/components/status-sidebar.js +253 -0
  134. package/dist/modes/interactive/components/status-sidebar.js.map +1 -0
  135. package/dist/modes/interactive/interactive-mode.d.ts +9 -0
  136. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  137. package/dist/modes/interactive/interactive-mode.js +94 -20
  138. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  139. package/dist/modes/interactive/theme/omk-neon-control.json +13 -13
  140. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  141. package/dist/modes/rpc/rpc-mode.js +1 -0
  142. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  143. package/dist/types/evidence.d.ts +1 -1
  144. package/dist/types/evidence.d.ts.map +1 -1
  145. package/dist/types/evidence.js.map +1 -1
  146. package/dist/utils/clipboard.d.ts.map +1 -1
  147. package/dist/utils/clipboard.js +19 -8
  148. package/dist/utils/clipboard.js.map +1 -1
  149. package/dist/utils/frontmatter.d.ts.map +1 -1
  150. package/dist/utils/frontmatter.js +31 -5
  151. package/dist/utils/frontmatter.js.map +1 -1
  152. package/docs/compaction.md +7 -0
  153. package/docs/docs.json +4 -0
  154. package/docs/environment-variables.md +94 -0
  155. package/docs/index.md +1 -0
  156. package/docs/rpc.md +5 -2
  157. package/docs/sdk.md +10 -4
  158. package/docs/settings.md +1 -0
  159. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  160. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  161. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  162. package/examples/extensions/gondolin/package-lock.json +2 -2
  163. package/examples/extensions/gondolin/package.json +1 -1
  164. package/examples/extensions/sandbox/package-lock.json +2 -2
  165. package/examples/extensions/sandbox/package.json +1 -1
  166. package/examples/extensions/subagent/agents.test.ts +86 -0
  167. package/examples/extensions/subagent/agents.ts +18 -4
  168. package/examples/extensions/subagent/capabilities.ts +15 -1
  169. package/examples/extensions/subagent/domain-profiles.ts +272 -0
  170. package/examples/extensions/with-deps/package-lock.json +2 -2
  171. package/examples/extensions/with-deps/package.json +1 -1
  172. package/npm-shrinkwrap.json +15 -15
  173. package/package.json +10 -7
@@ -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":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,EAAmB,yBAAyB,EAAE,MAAM,oBAAoB,CAAC;AAChF,OAAO,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAC3D,OAAO,EACN,sBAAsB,EACtB,mBAAmB,EACnB,+BAA+B,GAC/B,MAAM,6CAA6C,CAAC;AAErD,OAAO,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AAItF,OAAO,EAAkD,yBAAyB,EAAE,MAAM,iBAAiB,CAAC;AAuB5G,SAAS,oBAAoB,CAAC,KAAc,EAAkB;IAC7D,OAAO,KAAK,YAAY,KAAK,IAAI,KAAK,CAAC,OAAO,CAAC,UAAU,CAAC,uBAAuB,CAAC,CAAC;AAAA,CACnF;AAED,SAAS,4BAA4B,CAAC,MAAc,EAAc;IACjE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,EAAE,SAAS,EAAE,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC;AAAA,CAC3E;AAED,MAAM,OAAO,kBAAkB;IACtB,eAAe,CAA8B;IAC7C,mBAAmB,GAA2B,EAAE,CAAC;IACxC,IAAI,CAAyB;IAE9C,YAAY,IAA4B,EAAE;QACzC,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;IAAA,CACjB;IAED,KAAK,CAAC,WAAW,CAChB,OAAe,EACf,OAAiC,EACjC,OAA4B,EACN;QACtB,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,aAAa,EAAE,GAAG,IAAI,CAAC,IAAI,CAAC;QACvD,MAAM,MAAM,GAAG,QAAQ,CAAC,qBAAqB,EAAE,CAAC;QAChD,MAAM,SAAS,GAAG,QAAQ,CAAC,YAAY,EAAE,CAAC;QAC1C,MAAM,eAAe,GAAG,MAAM,CAAC,CAAC,CAAC,GAAG,MAAM,KAAK,OAAO,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC;QACnE,IAAI,aAAa,EAAE,CAAC;YACnB,mBAAmB,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,mBAAmB,CAAC,aAAa,EAAE,OAAO,CAAC,EAAE;gBAC7E,SAAS,EAAE,SAAS;gBACpB,QAAQ,EAAE,MAAM;gBAChB,OAAO,EAAE,eAAe;aACxB,CAAC,CAAC;QACJ,CAAC;QAED,yEAAyE;QACzE,mDAAmD;QACnD,IAAI,OAAO,EAAE,UAAU,KAAK,UAAU,EAAE,CAAC;YACxC,MAAM,YAAY,GAAG,oBAAoB,CAAC,OAAO,CAAC,CAAC;YACnD,IAAI,YAAY,CAAC,IAAI,KAAK,OAAO,EAAE,CAAC;gBACnC,MAAM,IAAI,KAAK,CAAC,yCAAwC,YAAY,CAAC,IAAI,KAAK,YAAY,CAAC,MAAM,EAAE,CAAC,CAAC;YACtG,CAAC;QACF,CAAC;QAED,4EAA4E;QAC5E,8EAA8E;QAC9E,gFAAgF;QAChF,8EAA8E;QAC9E,wEAAwE;QACxE,IAAI,OAAO,EAAE,UAAU,KAAK,UAAU,EAAE,CAAC;YACxC,MAAM,QAAQ,GAAG,MAAM,mBAAmB,CAAC,eAAe,EAAE;gBAC3D,KAAK,EAAE,KAAK;gBACZ,qBAAqB,EAAE,+BAA+B,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM;aAC3E,CAAC,CAAC;YACH,IAAI,QAAQ,EAAE,IAAI,EAAE,CAAC;gBACpB,MAAM,OAAO,GAAG,sBAAsB,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;gBACxD,IAAI,CAAC,gBAAgB,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC;gBACjD,OAAO,OAAO,CAAC;YAChB,CAAC;QACF,CAAC;QAED,IAAI,CAAC,eAAe,GAAG,IAAI,eAAe,EAAE,CAAC;QAE7C,IAAI,CAAC;YACJ,IAAI,CAAC;gBACJ,MAAM,UAAU,GACf,OAAO,EAAE,UAAU;oBACnB,yBAAyB,CAAC;wBACzB,SAAS;wBACT,aAAa,EAAE,OAAO,CAAC,gBAAgB,CAAC,OAAO,EAAE,aAAa,CAAC;qBAC/D,CAAC,CAAC;gBACJ,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC;gBAC/B,MAAM,cAAc,GAAG,CAAC,KAAa,EAAE,EAAE,CAAC;oBACzC,OAAO,EAAE,CAAC,KAAK,CAAC,CAAC;oBACjB,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,uBAAuB,EAAE,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC;gBAAA,CAC1E,CAAC;gBAEF,yFAAyF;gBACzF,MAAM,QAAQ,GAAG,MAAM,OAAO,CAAC,eAAe,CAAC;oBAC9C,eAAe;oBACf,GAAG;oBACH,SAAS;oBACT,UAAU;oBACV,MAAM,EAAE,IAAI,CAAC,eAAe,CAAC,MAAM;oBACnC,OAAO,EAAE,cAAc;iBACvB,CAAC,CAAC;gBACH,MAAM,MAAM,GACX,QAAQ;oBACR,CAAC,MAAM,yBAAyB,CAAC,eAAe,EAAE,GAAG,EAAE,UAAU,EAAE;wBAClE,OAAO,EAAE,cAAc;wBACvB,MAAM,EAAE,IAAI,CAAC,eAAe,CAAC,MAAM;qBACnC,CAAC,CAAC,CAAC;gBAEL,IAAI,CAAC,gBAAgB,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC;gBAChD,OAAO,MAAM,CAAC;YACf,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBAChB,IAAI,CAAC,oBAAoB,CAAC,KAAK,CAAC,EAAE,CAAC;oBAClC,MAAM,KAAK,CAAC;gBACb,CAAC;gBACD,MAAM,OAAO,GAAG,4BAA4B,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;gBAC5D,IAAI,CAAC,gBAAgB,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC;gBACjD,OAAO,OAAO,CAAC;YAChB,CAAC;QACF,CAAC;gBAAS,CAAC;YACV,IAAI,CAAC,eAAe,GAAG,SAAS,CAAC;QAClC,CAAC;IAAA,CACD;IAED;;;OAGG;IACH,gBAAgB,CAAC,OAAe,EAAE,MAAkB,EAAE,OAA0C,EAAQ;QACvG,MAAM,WAAW,GAAyB;YACzC,IAAI,EAAE,eAAe;YACrB,OAAO;YACP,MAAM,EAAE,MAAM,CAAC,MAAM;YACrB,QAAQ,EAAE,MAAM,CAAC,QAAQ;YACzB,SAAS,EAAE,MAAM,CAAC,SAAS;YAC3B,SAAS,EAAE,MAAM,CAAC,SAAS;YAC3B,cAAc,EAAE,MAAM,CAAC,cAAc;YACrC,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE;YACrB,kBAAkB,EAAE,OAAO,EAAE,kBAAkB;SAC/C,CAAC;QAEF,sFAAsF;QACtF,IAAI,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,EAAE,CAAC;YAC7B,IAAI,CAAC,mBAAmB,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;QAC5C,CAAC;aAAM,CAAC;YACP,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,WAAW,CAAC,CAAC;YACnC,IAAI,CAAC,IAAI,CAAC,aAAa,CAAC,WAAW,CAAC,CAAC;QACtC,CAAC;IAAA,CACD;IAED,mCAAmC;IACnC,SAAS,GAAS;QACjB,IAAI,CAAC,eAAe,EAAE,KAAK,EAAE,CAAC;IAAA,CAC9B;IAED,IAAI,aAAa,GAAY;QAC5B,OAAO,IAAI,CAAC,eAAe,KAAK,SAAS,CAAC;IAAA,CAC1C;IAED,IAAI,sBAAsB,GAAY;QACrC,OAAO,IAAI,CAAC,mBAAmB,CAAC,MAAM,GAAG,CAAC,CAAC;IAAA,CAC3C;IAED,+EAA+E;IAC/E,YAAY,GAAS;QACpB,IAAI,IAAI,CAAC,mBAAmB,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO;QAClD,KAAK,MAAM,WAAW,IAAI,IAAI,CAAC,mBAAmB,EAAE,CAAC;YACpD,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,WAAW,CAAC,CAAC;YACnC,IAAI,CAAC,IAAI,CAAC,aAAa,CAAC,WAAW,CAAC,CAAC;QACtC,CAAC;QACD,IAAI,CAAC,mBAAmB,GAAG,EAAE,CAAC;IAAA,CAC9B;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,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"]}
@@ -0,0 +1,250 @@
1
+ /**
2
+ * Session compaction service: capture / barrier / repair / transaction / commit.
3
+ *
4
+ * Extracted from AgentSession so the session keeps lifecycle orchestration
5
+ * (abort controllers, extension events, auth, model resolution) while this
6
+ * owns the integrity-critical compaction state machine. Every method preserves
7
+ * the exact semantics of the original session implementation — the
8
+ * compaction-transaction / session-integrity suites pin them.
9
+ */
10
+ import { createHash, randomUUID } from "node:crypto";
11
+ import { existsSync, readFileSync } from "node:fs";
12
+ import { repairTranscriptIntegrity } from "omk-agent-core";
13
+ import { sanitizeBinaryOutput } from "../utils/shell.js";
14
+ import { createCompactionEnvelope, createCompactionSourceIdentity, createCompactionTransaction, decideCompactionCommit, evaluateCompactionBarrier, validateCompactionEnvelope, } from "./compaction/transaction.js";
15
+ import { redactSensitiveText } from "./redaction.js";
16
+ import { inspectSessionIntegrity } from "./session-integrity.js";
17
+ export class SessionCompactionService {
18
+ deps;
19
+ constructor(deps) {
20
+ this.deps = deps;
21
+ }
22
+ captureState() {
23
+ return this.deps.sessionManager.withCompactionCommitLock(() => this.captureStateLocked());
24
+ }
25
+ captureStateLocked() {
26
+ const { sessionManager } = this.deps;
27
+ const sessionFile = sessionManager.getSessionFile();
28
+ const bytes = sessionFile && existsSync(sessionFile)
29
+ ? new Uint8Array(readFileSync(sessionFile))
30
+ : new TextEncoder().encode(`${[sessionManager.getHeader(), ...sessionManager.getEntries()]
31
+ .filter((entry) => entry !== null)
32
+ .map((entry) => JSON.stringify(entry))
33
+ .join("\n")}\n`);
34
+ const report = inspectSessionIntegrity(bytes, { activeLeafId: sessionManager.getLeafId() });
35
+ const branchEntries = report.activeBranch;
36
+ let latestCompactionIndex = -1;
37
+ for (let index = branchEntries.length - 1; index >= 0; index -= 1) {
38
+ if (branchEntries[index]?.type === "compaction") {
39
+ latestCompactionIndex = index;
40
+ break;
41
+ }
42
+ }
43
+ const latestCompaction = branchEntries[latestCompactionIndex];
44
+ const firstKeptIndex = latestCompaction?.type === "compaction"
45
+ ? branchEntries.findIndex((entry) => entry.id === latestCompaction.firstKeptEntryId)
46
+ : -1;
47
+ const sourceEntries = branchEntries.slice(latestCompactionIndex < 0 ? 0 : firstKeptIndex < 0 ? latestCompactionIndex : firstKeptIndex);
48
+ const firstEntry = sourceEntries[0];
49
+ const lastEntry = sourceEntries.at(-1);
50
+ if (!firstEntry || !lastEntry || report.activeLeafId === null) {
51
+ const barrier = evaluateCompactionBarrier(report, [...this.deps.pendingToolCallIds()]);
52
+ if (barrier.status !== "ready")
53
+ throw this.barrierError(barrier);
54
+ throw new Error("Nothing to compact: the active session branch is empty");
55
+ }
56
+ const revision = sessionManager.getDurableHeadToken();
57
+ const source = createCompactionSourceIdentity({
58
+ sessionId: revision.sessionId,
59
+ entryIds: sourceEntries.map((entry) => entry.id),
60
+ firstEntryId: firstEntry.id,
61
+ lastEntryId: lastEntry.id,
62
+ sourceSha256: createHash("sha256")
63
+ .update(sourceEntries.map((entry) => JSON.stringify(entry)).join("\n"), "utf8")
64
+ .digest("hex"),
65
+ activeLeafId: report.activeLeafId,
66
+ messageCount: report.activeMessages.length,
67
+ });
68
+ return { report, branchEntries, revision, source };
69
+ }
70
+ barrierError(barrier) {
71
+ if (barrier.status === "defer") {
72
+ return new Error(`Compaction deferred until the transcript closes (${barrier.reason})`);
73
+ }
74
+ return new Error(`Compaction failed closed on transcript integrity (${barrier.reason}). Run the session doctor before retrying.`);
75
+ }
76
+ evaluateBarrier(capture, includeMissingTailAsPending, excludedPendingIds = new Set()) {
77
+ const pending = new Set([...this.deps.pendingToolCallIds()].filter((id) => !excludedPendingIds.has(id)));
78
+ if (includeMissingTailAsPending) {
79
+ for (const issue of capture.report.transcript?.issues ?? []) {
80
+ if (issue.kind === "missing_result")
81
+ pending.add(issue.toolCallId);
82
+ }
83
+ }
84
+ return evaluateCompactionBarrier(capture.report, [...pending]);
85
+ }
86
+ repairEmergencyTail(capture) {
87
+ const { sessionManager } = this.deps;
88
+ const barrier = this.evaluateBarrier(capture, true);
89
+ if (barrier.status !== "defer" || barrier.reason !== "missing_active_tail_results") {
90
+ if (barrier.status !== "ready")
91
+ throw this.barrierError(barrier);
92
+ return { capture, repairedToolCallIds: new Set() };
93
+ }
94
+ const repairedMessages = repairTranscriptIntegrity([...capture.report.activeMessages], "Tool result missing; synthesized to close an emergency compaction barrier");
95
+ const inserted = repairedMessages.slice(capture.report.activeMessages.length);
96
+ const repairedToolCallIds = new Set();
97
+ for (const message of inserted) {
98
+ if (message.role !== "toolResult") {
99
+ throw new Error("Emergency compaction repair produced a non-tool result");
100
+ }
101
+ const toolResult = message;
102
+ repairedToolCallIds.add(toolResult.toolCallId);
103
+ sessionManager.appendMessage(toolResult);
104
+ }
105
+ sessionManager.appendCustomEntry("compaction_transcript_repaired", {
106
+ insertedToolCallIds: [...repairedToolCallIds],
107
+ reason: "emergency_compaction",
108
+ });
109
+ this.deps.invalidateContextBudget();
110
+ const closedCapture = this.captureState();
111
+ const closedBarrier = this.evaluateBarrier(closedCapture, false, repairedToolCallIds);
112
+ if (closedBarrier.status !== "ready")
113
+ throw this.barrierError(closedBarrier);
114
+ this.deps.refreshAgentMessages();
115
+ return { capture: closedCapture, repairedToolCallIds };
116
+ }
117
+ priorCommittedSourceDigests() {
118
+ const digests = [];
119
+ for (const entry of this.deps.sessionManager.getEntries()) {
120
+ if (entry.type !== "compaction" || typeof entry.details !== "object" || entry.details === null)
121
+ continue;
122
+ if (!Object.hasOwn(entry.details, "compactionEnvelope"))
123
+ continue;
124
+ const envelope = validateCompactionEnvelope(Reflect.get(entry.details, "compactionEnvelope"));
125
+ if (envelope.summary !== entry.summary) {
126
+ throw new Error(`Compaction entry ${entry.id} has invalid provenance. Run the session doctor.`);
127
+ }
128
+ digests.push(envelope.source.sourceSha256);
129
+ }
130
+ return digests;
131
+ }
132
+ beginTransaction(compactionModel, emergency) {
133
+ let capture = this.captureState();
134
+ if (emergency) {
135
+ capture = this.repairEmergencyTail(capture).capture;
136
+ }
137
+ else {
138
+ const barrier = this.evaluateBarrier(capture, false);
139
+ if (barrier.status !== "ready")
140
+ throw this.barrierError(barrier);
141
+ }
142
+ const transaction = createCompactionTransaction({
143
+ transactionId: randomUUID(),
144
+ baseRevision: capture.revision,
145
+ source: capture.source,
146
+ createdAt: new Date().toISOString(),
147
+ model: { provider: compactionModel.provider, id: compactionModel.id },
148
+ preserved: this.captureProvenance(capture),
149
+ });
150
+ if (this.priorCommittedSourceDigests().includes(transaction.source.sourceSha256)) {
151
+ throw new Error("This exact compaction source was already compacted");
152
+ }
153
+ return { capture, transaction };
154
+ }
155
+ captureProvenance(capture) {
156
+ let latestIntent = "Continue the current session";
157
+ for (let index = capture.report.activeMessages.length - 1; index >= 0; index -= 1) {
158
+ const message = capture.report.activeMessages[index];
159
+ if (message?.role !== "user")
160
+ continue;
161
+ const candidate = sanitizeBinaryOutput(redactSensitiveText(this.deps.getUserMessageText(message)).trim())
162
+ .replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/gu, "")
163
+ .slice(0, 16_384);
164
+ if (candidate.length > 0)
165
+ latestIntent = candidate;
166
+ break;
167
+ }
168
+ const modelHistory = capture.branchEntries
169
+ .flatMap((entry) => {
170
+ if (entry.type === "model_change") {
171
+ return [{ entryId: entry.id, provider: entry.provider, modelId: entry.modelId }];
172
+ }
173
+ if (entry.type === "message" && entry.message.role === "assistant") {
174
+ return [{ entryId: entry.id, provider: entry.message.provider, modelId: entry.message.model }];
175
+ }
176
+ return [];
177
+ })
178
+ .slice(-256);
179
+ const customEntryIds = (customType) => capture.branchEntries
180
+ .filter((entry) => entry.type === "custom" && entry.customType === customType)
181
+ .map((entry) => entry.id);
182
+ return {
183
+ latestIntent,
184
+ openTasks: [],
185
+ laneIds: customEntryIds("lane"),
186
+ acceptancePredicateIds: customEntryIds("acceptance_predicate"),
187
+ evidenceReceiptIds: customEntryIds("evidence_receipt"),
188
+ blockerReasons: [],
189
+ repairEventIds: [
190
+ ...customEntryIds("transcript_repaired"),
191
+ ...customEntryIds("compaction_transcript_repaired"),
192
+ ],
193
+ branch: null,
194
+ worktree: this.deps.cwd,
195
+ modelHistory,
196
+ nextAction: latestIntent.slice(0, 4096) || "Continue the current session",
197
+ };
198
+ }
199
+ detailsWithEnvelope(details, envelope) {
200
+ if (typeof details === "object" && details !== null && !Array.isArray(details)) {
201
+ return { ...details, compactionEnvelope: envelope };
202
+ }
203
+ return {
204
+ compactionEnvelope: envelope,
205
+ ...(details === undefined ? {} : { resultDetails: details }),
206
+ };
207
+ }
208
+ commit(begun, result, fromExtension) {
209
+ if (!begun.transaction.source.entryIds.includes(result.firstKeptEntryId)) {
210
+ throw new Error("Compaction first-kept entry is outside the captured source");
211
+ }
212
+ const committed = this.deps.sessionManager.withCompactionCommitLock(() => {
213
+ const current = this.captureState();
214
+ const barrier = this.evaluateBarrier(current, false);
215
+ const decision = decideCompactionCommit({
216
+ transaction: begun.transaction,
217
+ currentRevision: current.revision,
218
+ currentSource: current.source,
219
+ barrier,
220
+ priorCommittedSourceDigests: this.priorCommittedSourceDigests(),
221
+ });
222
+ switch (decision.decision) {
223
+ case "duplicate":
224
+ throw new Error("This exact compaction source was already compacted");
225
+ case "stale":
226
+ throw new Error(`Session changed during compaction (${decision.reason}); generated summary was discarded`);
227
+ case "defer":
228
+ case "fail_closed":
229
+ throw this.barrierError(barrier);
230
+ case "commit": {
231
+ const envelope = createCompactionEnvelope({
232
+ transaction: begun.transaction,
233
+ decision,
234
+ summary: result.summary,
235
+ summarySha256: createHash("sha256").update(result.summary, "utf8").digest("hex"),
236
+ });
237
+ const entryId = this.deps.sessionManager.appendCompaction(result.summary, result.firstKeptEntryId, result.tokensBefore, this.detailsWithEnvelope(result.details, envelope), fromExtension);
238
+ const entry = this.deps.sessionManager.getEntry(entryId);
239
+ if (!entry || entry.type !== "compaction") {
240
+ throw new Error("Compaction commit did not produce a compaction entry");
241
+ }
242
+ return { entry, envelope };
243
+ }
244
+ }
245
+ });
246
+ this.deps.recordCommit();
247
+ return committed;
248
+ }
249
+ }
250
+ //# sourceMappingURL=session-compaction-service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-compaction-service.js","sourceRoot":"","sources":["../../src/core/session-compaction-service.ts"],"names":[],"mappings":"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","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"]}
@@ -1 +1 @@
1
- {"version":3,"file":"session-file-compaction.d.ts","sourceRoot":"","sources":["../../src/core/session-file-compaction.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAmB,SAAS,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AA4EpG,kFAAkF;AAClF,wBAAgB,oCAAoC,CACnD,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,SAAS,SAAS,EAAE,EAC7B,SAAS,EAAE,MAAM,GACf,IAAI,CAyCN;AAED,oFAAoF;AACpF,wBAAgB,iCAAiC,CAChD,MAAM,EAAE,aAAa,EACrB,OAAO,EAAE,SAAS,YAAY,EAAE,GAC9B,YAAY,EAAE,CAkDhB","sourcesContent":["import { createHash } from \"node:crypto\";\nimport { readFileSync } from \"node:fs\";\nimport {\n\ttype CompactionEnvelope,\n\tcreateSessionRevisionToken,\n\tvalidateCompactionEnvelope,\n} from \"./compaction/transaction.ts\";\nimport type { CompactionEntry, FileEntry, SessionEntry, SessionHeader } from \"./session-manager.ts\";\n\nfunction envelopeFromEntry(entry: CompactionEntry): CompactionEnvelope | null {\n\tif (typeof entry.details !== \"object\" || entry.details === null || Array.isArray(entry.details)) return null;\n\tif (!Object.hasOwn(entry.details, \"compactionEnvelope\")) return null;\n\treturn validateCompactionEnvelope(Reflect.get(entry.details, \"compactionEnvelope\"));\n}\n\nfunction branchFromPrefix(prefix: readonly FileEntry[], leafId: string): SessionEntry[] {\n\tconst byId = new Map(\n\t\tprefix.filter((entry): entry is SessionEntry => entry.type !== \"session\").map((entry) => [entry.id, entry]),\n\t);\n\tconst branch: SessionEntry[] = [];\n\tconst seen = new Set<string>();\n\tlet current = byId.get(leafId);\n\twhile (current) {\n\t\tif (seen.has(current.id)) throw new Error(`Compaction source at ${leafId} contains a cycle`);\n\t\tseen.add(current.id);\n\t\tbranch.unshift(current);\n\t\tcurrent = current.parentId === null ? undefined : byId.get(current.parentId);\n\t}\n\tif (branch.at(-1)?.id !== leafId)\n\t\tthrow new Error(`Compaction source leaf ${leafId} is outside the persisted prefix`);\n\treturn branch;\n}\n\nfunction sourceSha256(entries: readonly SessionEntry[]): string {\n\treturn createHash(\"sha256\")\n\t\t.update(entries.map((entry) => JSON.stringify(entry)).join(\"\\n\"), \"utf8\")\n\t\t.digest(\"hex\");\n}\n\nfunction serializedPrefix(prefix: readonly FileEntry[]): string {\n\treturn `${prefix.map((entry) => JSON.stringify(entry)).join(\"\\n\")}\\n`;\n}\n\nfunction prefixRevision(header: SessionHeader, prefix: readonly FileEntry[], leafId: string) {\n\tconst bytes = serializedPrefix(prefix);\n\tconst last = prefix.at(-1);\n\treturn createSessionRevisionToken({\n\t\tsessionId: header.id,\n\t\tcompleteBytes: Buffer.byteLength(bytes),\n\t\trecordCount: prefix.length,\n\t\tleafId,\n\t\tlastEntryId: last?.type === \"session\" ? null : (last?.id ?? null),\n\t\tcompletePrefixSha256: createHash(\"sha256\").update(bytes, \"utf8\").digest(\"hex\"),\n\t});\n}\n\nfunction sameOrderedIds(left: readonly string[], right: readonly SessionEntry[]): boolean {\n\treturn left.length === right.length && left.every((id, index) => id === right[index]?.id);\n}\n\nfunction assertSourceMatches(\n\tentry: CompactionEntry,\n\tenvelope: CompactionEnvelope,\n\tsource: readonly SessionEntry[],\n): void {\n\tif (\n\t\tenvelope.source.sessionId === \"\" ||\n\t\t!sameOrderedIds(envelope.source.entryIds, source) ||\n\t\tenvelope.source.sourceSha256 !== sourceSha256(source) ||\n\t\tenvelope.source.activeLeafId !== entry.parentId\n\t) {\n\t\tthrow new Error(`Invalid compaction envelope source in entry ${entry.id}. Run the session doctor.`);\n\t}\n}\n\nfunction persistedRecordStarts(bytes: Uint8Array): number[] {\n\tconst starts = [0];\n\tfor (let index = 0; index < bytes.byteLength; index += 1) {\n\t\tif (bytes[index] === 0x0a && index + 1 < bytes.byteLength) starts.push(index + 1);\n\t}\n\treturn starts;\n}\n\n/** Recompute persisted compaction provenance instead of trusting its envelope. */\nexport function validatePersistedCompactionEnvelopes(\n\tpath: string,\n\tentries: readonly FileEntry[],\n\tsessionId: string,\n): void {\n\tconst header = entries[0];\n\tif (!header || header.type !== \"session\" || header.id !== sessionId) throw new Error(\"Invalid session header\");\n\tconst bytes = readFileSync(path);\n\tconst starts = persistedRecordStarts(bytes);\n\tfor (let index = 0; index < entries.length; index += 1) {\n\t\tconst entry = entries[index];\n\t\tif (!entry || entry.type !== \"compaction\") continue;\n\t\tconst envelope = envelopeFromEntry(entry);\n\t\tif (!envelope) continue;\n\t\tif (entry.parentId === null) throw new Error(`Compaction entry ${entry.id} has no source leaf`);\n\t\tconst prefix = entries.slice(0, index);\n\t\tconst source = branchFromPrefix(prefix, entry.parentId);\n\t\tconst start = starts[index];\n\t\tif (start === undefined) throw new Error(`Compaction entry ${entry.id} has no persisted prefix`);\n\t\tconst last = prefix.at(-1);\n\t\tconst expectedRevision = createSessionRevisionToken({\n\t\t\tsessionId,\n\t\t\tcompleteBytes: start,\n\t\t\trecordCount: index,\n\t\t\tleafId: entry.parentId,\n\t\t\tlastEntryId: last?.type === \"session\" ? null : (last?.id ?? null),\n\t\t\tcompletePrefixSha256: createHash(\"sha256\").update(bytes.subarray(0, start)).digest(\"hex\"),\n\t\t});\n\t\tconst base = envelope.baseRevision;\n\t\tconst summarySha256 = createHash(\"sha256\").update(entry.summary, \"utf8\").digest(\"hex\");\n\t\tif (\n\t\t\tbase.sessionId !== expectedRevision.sessionId ||\n\t\t\tbase.completeBytes !== expectedRevision.completeBytes ||\n\t\t\tbase.recordCount !== expectedRevision.recordCount ||\n\t\t\tbase.leafId !== expectedRevision.leafId ||\n\t\t\tbase.lastEntryId !== expectedRevision.lastEntryId ||\n\t\t\tbase.completePrefixSha256 !== expectedRevision.completePrefixSha256 ||\n\t\t\tenvelope.source.sessionId !== sessionId ||\n\t\t\tenvelope.summary !== entry.summary ||\n\t\t\tenvelope.summarySha256 !== summarySha256\n\t\t) {\n\t\t\tthrow new Error(`Invalid compaction envelope binding in entry ${entry.id}. Run the session doctor.`);\n\t\t}\n\t\tassertSourceMatches(entry, envelope, source);\n\t}\n}\n\n/** Copy one branch while rebinding nested compaction provenance in source order. */\nexport function rebindBranchedCompactionEnvelopes(\n\theader: SessionHeader,\n\tentries: readonly SessionEntry[],\n): SessionEntry[] {\n\tconst referencedLabels = new Set<string>();\n\tfor (const entry of entries) {\n\t\tif (entry.type !== \"compaction\") continue;\n\t\tfor (const id of envelopeFromEntry(entry)?.source.entryIds ?? []) referencedLabels.add(id);\n\t}\n\tconst originalPrefix: FileEntry[] = [];\n\tconst copiedPrefix: FileEntry[] = [header];\n\tconst copied: SessionEntry[] = [];\n\tfor (const entry of entries) {\n\t\tlet copy = entry;\n\t\tif (entry.type === \"compaction\") {\n\t\t\tconst details = entry.details;\n\t\t\tconst envelope = envelopeFromEntry(entry);\n\t\t\tif (envelope) {\n\t\t\t\tif (typeof details !== \"object\" || details === null || Array.isArray(details))\n\t\t\t\t\tthrow new Error(`Compaction entry ${entry.id} has invalid details`);\n\t\t\t\tif (entry.parentId === null) throw new Error(`Compaction entry ${entry.id} has no source leaf`);\n\t\t\t\tassertSourceMatches(entry, envelope, branchFromPrefix(originalPrefix, entry.parentId));\n\t\t\t\tconst source = branchFromPrefix(copiedPrefix, entry.parentId);\n\t\t\t\tconst first = source[0];\n\t\t\t\tconst last = source.at(-1);\n\t\t\t\tif (!first || !last) throw new Error(`Compaction entry ${entry.id} has no source entries`);\n\t\t\t\tcopy = {\n\t\t\t\t\t...entry,\n\t\t\t\t\tdetails: {\n\t\t\t\t\t\t...details,\n\t\t\t\t\t\tcompactionEnvelope: validateCompactionEnvelope({\n\t\t\t\t\t\t\t...envelope,\n\t\t\t\t\t\t\tbaseRevision: prefixRevision(header, copiedPrefix, entry.parentId),\n\t\t\t\t\t\t\tsource: {\n\t\t\t\t\t\t\t\t...envelope.source,\n\t\t\t\t\t\t\t\tsessionId: header.id,\n\t\t\t\t\t\t\t\tentryIds: source.map((candidate) => candidate.id),\n\t\t\t\t\t\t\t\tfirstEntryId: first.id,\n\t\t\t\t\t\t\t\tlastEntryId: last.id,\n\t\t\t\t\t\t\t\tsourceSha256: sourceSha256(source),\n\t\t\t\t\t\t\t\tactiveLeafId: entry.parentId,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t}),\n\t\t\t\t\t},\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\t\toriginalPrefix.push(entry);\n\t\tif (entry.type === \"label\" && !referencedLabels.has(entry.id)) continue;\n\t\tcopied.push(copy);\n\t\tcopiedPrefix.push(copy);\n\t}\n\treturn copied;\n}\n"]}
1
+ {"version":3,"file":"session-file-compaction.d.ts","sourceRoot":"","sources":["../../src/core/session-file-compaction.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAmB,SAAS,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAwGpG,kFAAkF;AAClF,wBAAgB,oCAAoC,CACnD,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,SAAS,SAAS,EAAE,EAC7B,SAAS,EAAE,MAAM,GACf,IAAI,CAiDN;AAED,oFAAoF;AACpF,wBAAgB,iCAAiC,CAChD,MAAM,EAAE,aAAa,EACrB,OAAO,EAAE,SAAS,YAAY,EAAE,GAC9B,YAAY,EAAE,CAkDhB","sourcesContent":["import { createHash } from \"node:crypto\";\nimport { readFileSync } from \"node:fs\";\nimport {\n\ttype CompactionEnvelope,\n\tcreateSessionRevisionToken,\n\tvalidateCompactionEnvelope,\n} from \"./compaction/transaction.ts\";\nimport type { CompactionEntry, FileEntry, SessionEntry, SessionHeader } from \"./session-manager.ts\";\n\nfunction envelopeFromEntry(entry: CompactionEntry): CompactionEnvelope | null {\n\tif (typeof entry.details !== \"object\" || entry.details === null || Array.isArray(entry.details)) return null;\n\tif (!Object.hasOwn(entry.details, \"compactionEnvelope\")) return null;\n\treturn validateCompactionEnvelope(Reflect.get(entry.details, \"compactionEnvelope\"));\n}\n\nfunction branchFromPrefix(prefix: readonly FileEntry[], leafId: string): SessionEntry[] {\n\tconst byId = new Map(\n\t\tprefix.filter((entry): entry is SessionEntry => entry.type !== \"session\").map((entry) => [entry.id, entry]),\n\t);\n\tconst branch: SessionEntry[] = [];\n\tconst seen = new Set<string>();\n\tlet current = byId.get(leafId);\n\twhile (current) {\n\t\tif (seen.has(current.id)) throw new Error(`Compaction source at ${leafId} contains a cycle`);\n\t\tseen.add(current.id);\n\t\tbranch.unshift(current);\n\t\tcurrent = current.parentId === null ? undefined : byId.get(current.parentId);\n\t}\n\tif (branch.at(-1)?.id !== leafId)\n\t\tthrow new Error(`Compaction source leaf ${leafId} is outside the persisted prefix`);\n\treturn branch;\n}\n\nfunction sourceSha256(entries: readonly SessionEntry[]): string {\n\treturn createHash(\"sha256\")\n\t\t.update(entries.map((entry) => JSON.stringify(entry)).join(\"\\n\"), \"utf8\")\n\t\t.digest(\"hex\");\n}\n\nfunction serializedPrefix(prefix: readonly FileEntry[]): string {\n\treturn `${prefix.map((entry) => JSON.stringify(entry)).join(\"\\n\")}\\n`;\n}\n\nfunction prefixRevision(header: SessionHeader, prefix: readonly FileEntry[], leafId: string) {\n\tconst bytes = serializedPrefix(prefix);\n\tconst last = prefix.at(-1);\n\treturn createSessionRevisionToken({\n\t\tsessionId: header.id,\n\t\tcompleteBytes: Buffer.byteLength(bytes),\n\t\trecordCount: prefix.length,\n\t\tleafId,\n\t\tlastEntryId: last?.type === \"session\" ? null : (last?.id ?? null),\n\t\tcompletePrefixSha256: createHash(\"sha256\").update(bytes, \"utf8\").digest(\"hex\"),\n\t});\n}\n\nfunction sameOrderedIds(left: readonly string[], right: readonly SessionEntry[]): boolean {\n\treturn left.length === right.length && left.every((id, index) => id === right[index]?.id);\n}\n\nfunction assertSourceMatches(\n\tentry: CompactionEntry,\n\tenvelope: CompactionEnvelope,\n\tsource: readonly SessionEntry[],\n): void {\n\tif (!matchesSource(entry, envelope, source)) {\n\t\tthrow new Error(`Invalid compaction envelope source in entry ${entry.id}. Run the session doctor.`);\n\t}\n}\n\nfunction matchesSource(entry: CompactionEntry, envelope: CompactionEnvelope, source: readonly SessionEntry[]): boolean {\n\treturn (\n\t\tenvelope.source.sessionId !== \"\" &&\n\t\tsameOrderedIds(envelope.source.entryIds, source) &&\n\t\tenvelope.source.sourceSha256 === sourceSha256(source) &&\n\t\tenvelope.source.activeLeafId === entry.parentId\n\t);\n}\n\n/**\n * Writer-mirrored source window. The compaction writer (agent-session.ts) attests\n * the slice of the active branch from the previous compaction's firstKeptEntryId\n * onward — the summary covers the kept window plus earlier summaries, not the\n * full parent branch. For the second and later compactions this window is\n * strictly smaller than the full branch, so validating persisted envelopes\n * against the full branch alone makes every twice-compacted session unopenable.\n */\nfunction windowedSourceFromPrefix(prefix: readonly FileEntry[], leafId: string): SessionEntry[] {\n\tconst branch = branchFromPrefix(prefix, leafId);\n\tlet latestCompactionIndex = -1;\n\tfor (let index = branch.length - 1; index >= 0; index -= 1) {\n\t\tif (branch[index]?.type === \"compaction\") {\n\t\t\tlatestCompactionIndex = index;\n\t\t\tbreak;\n\t\t}\n\t}\n\tif (latestCompactionIndex < 0) return branch;\n\tconst latestCompaction = branch[latestCompactionIndex];\n\tif (latestCompaction?.type !== \"compaction\") return branch;\n\tconst firstKeptIndex = branch.findIndex((entry) => entry.id === latestCompaction.firstKeptEntryId);\n\treturn branch.slice(firstKeptIndex < 0 ? latestCompactionIndex : firstKeptIndex);\n}\n\nfunction persistedRecordStarts(bytes: Uint8Array): number[] {\n\tconst starts = [0];\n\tfor (let index = 0; index < bytes.byteLength; index += 1) {\n\t\tif (bytes[index] === 0x0a && index + 1 < bytes.byteLength) starts.push(index + 1);\n\t}\n\treturn starts;\n}\n\n/** Recompute persisted compaction provenance instead of trusting its envelope. */\nexport function validatePersistedCompactionEnvelopes(\n\tpath: string,\n\tentries: readonly FileEntry[],\n\tsessionId: string,\n): void {\n\tconst header = entries[0];\n\tif (!header || header.type !== \"session\" || header.id !== sessionId) throw new Error(\"Invalid session header\");\n\tconst bytes = readFileSync(path);\n\tconst starts = persistedRecordStarts(bytes);\n\tfor (let index = 0; index < entries.length; index += 1) {\n\t\tconst entry = entries[index];\n\t\tif (!entry || entry.type !== \"compaction\") continue;\n\t\tconst envelope = envelopeFromEntry(entry);\n\t\tif (!envelope) continue;\n\t\tif (entry.parentId === null) throw new Error(`Compaction entry ${entry.id} has no source leaf`);\n\t\tconst prefix = entries.slice(0, index);\n\t\tconst source = branchFromPrefix(prefix, entry.parentId);\n\t\tconst start = starts[index];\n\t\tif (start === undefined) throw new Error(`Compaction entry ${entry.id} has no persisted prefix`);\n\t\tconst last = prefix.at(-1);\n\t\tconst expectedRevision = createSessionRevisionToken({\n\t\t\tsessionId,\n\t\t\tcompleteBytes: start,\n\t\t\trecordCount: index,\n\t\t\tleafId: entry.parentId,\n\t\t\tlastEntryId: last?.type === \"session\" ? null : (last?.id ?? null),\n\t\t\tcompletePrefixSha256: createHash(\"sha256\").update(bytes.subarray(0, start)).digest(\"hex\"),\n\t\t});\n\t\tconst base = envelope.baseRevision;\n\t\tconst summarySha256 = createHash(\"sha256\").update(entry.summary, \"utf8\").digest(\"hex\");\n\t\tif (\n\t\t\tbase.sessionId !== expectedRevision.sessionId ||\n\t\t\tbase.completeBytes !== expectedRevision.completeBytes ||\n\t\t\tbase.recordCount !== expectedRevision.recordCount ||\n\t\t\tbase.leafId !== expectedRevision.leafId ||\n\t\t\tbase.lastEntryId !== expectedRevision.lastEntryId ||\n\t\t\tbase.completePrefixSha256 !== expectedRevision.completePrefixSha256 ||\n\t\t\tenvelope.source.sessionId !== sessionId ||\n\t\t\tenvelope.summary !== entry.summary ||\n\t\t\tenvelope.summarySha256 !== summarySha256\n\t\t) {\n\t\t\tthrow new Error(`Invalid compaction envelope binding in entry ${entry.id}. Run the session doctor.`);\n\t\t}\n\t\t// The writer attests either the full parent branch or its kept-window\n\t\t// slice since the previous compaction (both are exact matches, so\n\t\t// tamper-evidence is preserved; see windowedSourceFromPrefix).\n\t\tif (\n\t\t\t!matchesSource(entry, envelope, source) &&\n\t\t\t!matchesSource(entry, envelope, windowedSourceFromPrefix(prefix, entry.parentId))\n\t\t) {\n\t\t\tthrow new Error(`Invalid compaction envelope source in entry ${entry.id}. Run the session doctor.`);\n\t\t}\n\t}\n}\n\n/** Copy one branch while rebinding nested compaction provenance in source order. */\nexport function rebindBranchedCompactionEnvelopes(\n\theader: SessionHeader,\n\tentries: readonly SessionEntry[],\n): SessionEntry[] {\n\tconst referencedLabels = new Set<string>();\n\tfor (const entry of entries) {\n\t\tif (entry.type !== \"compaction\") continue;\n\t\tfor (const id of envelopeFromEntry(entry)?.source.entryIds ?? []) referencedLabels.add(id);\n\t}\n\tconst originalPrefix: FileEntry[] = [];\n\tconst copiedPrefix: FileEntry[] = [header];\n\tconst copied: SessionEntry[] = [];\n\tfor (const entry of entries) {\n\t\tlet copy = entry;\n\t\tif (entry.type === \"compaction\") {\n\t\t\tconst details = entry.details;\n\t\t\tconst envelope = envelopeFromEntry(entry);\n\t\t\tif (envelope) {\n\t\t\t\tif (typeof details !== \"object\" || details === null || Array.isArray(details))\n\t\t\t\t\tthrow new Error(`Compaction entry ${entry.id} has invalid details`);\n\t\t\t\tif (entry.parentId === null) throw new Error(`Compaction entry ${entry.id} has no source leaf`);\n\t\t\t\tassertSourceMatches(entry, envelope, branchFromPrefix(originalPrefix, entry.parentId));\n\t\t\t\tconst source = branchFromPrefix(copiedPrefix, entry.parentId);\n\t\t\t\tconst first = source[0];\n\t\t\t\tconst last = source.at(-1);\n\t\t\t\tif (!first || !last) throw new Error(`Compaction entry ${entry.id} has no source entries`);\n\t\t\t\tcopy = {\n\t\t\t\t\t...entry,\n\t\t\t\t\tdetails: {\n\t\t\t\t\t\t...details,\n\t\t\t\t\t\tcompactionEnvelope: validateCompactionEnvelope({\n\t\t\t\t\t\t\t...envelope,\n\t\t\t\t\t\t\tbaseRevision: prefixRevision(header, copiedPrefix, entry.parentId),\n\t\t\t\t\t\t\tsource: {\n\t\t\t\t\t\t\t\t...envelope.source,\n\t\t\t\t\t\t\t\tsessionId: header.id,\n\t\t\t\t\t\t\t\tentryIds: source.map((candidate) => candidate.id),\n\t\t\t\t\t\t\t\tfirstEntryId: first.id,\n\t\t\t\t\t\t\t\tlastEntryId: last.id,\n\t\t\t\t\t\t\t\tsourceSha256: sourceSha256(source),\n\t\t\t\t\t\t\t\tactiveLeafId: entry.parentId,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t}),\n\t\t\t\t\t},\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\t\toriginalPrefix.push(entry);\n\t\tif (entry.type === \"label\" && !referencedLabels.has(entry.id)) continue;\n\t\tcopied.push(copy);\n\t\tcopiedPrefix.push(copy);\n\t}\n\treturn copied;\n}\n"]}