@qduc/term2 0.22.1 → 0.23.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 (380) hide show
  1. package/dist/agent.d.ts +1 -0
  2. package/dist/agent.d.ts.map +1 -1
  3. package/dist/agent.js +18 -10
  4. package/dist/agent.js.map +1 -1
  5. package/dist/app.d.ts.map +1 -1
  6. package/dist/app.js +19 -1
  7. package/dist/app.js.map +1 -1
  8. package/dist/cli.js +26 -0
  9. package/dist/cli.js.map +1 -1
  10. package/dist/components/input/ApplicationInputSurface.js +1 -1
  11. package/dist/components/input/ApplicationInputSurface.js.map +1 -1
  12. package/dist/components/input/SettingsMenuSession.d.ts.map +1 -1
  13. package/dist/components/input/SettingsMenuSession.js +8 -5
  14. package/dist/components/input/SettingsMenuSession.js.map +1 -1
  15. package/dist/components/input/SubagentPoolMenuSession.d.ts +9 -0
  16. package/dist/components/input/SubagentPoolMenuSession.d.ts.map +1 -0
  17. package/dist/components/input/{MentorPoolMenuSession.js → SubagentPoolMenuSession.js} +17 -10
  18. package/dist/components/input/SubagentPoolMenuSession.js.map +1 -0
  19. package/dist/components/input/menu-controller.d.ts.map +1 -1
  20. package/dist/components/input/menu-controller.js +4 -3
  21. package/dist/components/input/menu-controller.js.map +1 -1
  22. package/dist/components/input/menu-registry.js +2 -2
  23. package/dist/components/input/menu-registry.js.map +1 -1
  24. package/dist/components/input/menu-types.d.ts +5 -1
  25. package/dist/components/input/menu-types.d.ts.map +1 -1
  26. package/dist/components/input/triggers.d.ts.map +1 -1
  27. package/dist/components/input/triggers.js +10 -7
  28. package/dist/components/input/triggers.js.map +1 -1
  29. package/dist/components/layout/Banner.d.ts.map +1 -1
  30. package/dist/components/layout/Banner.js +4 -1
  31. package/dist/components/layout/Banner.js.map +1 -1
  32. package/dist/components/layout/StatusBar.d.ts.map +1 -1
  33. package/dist/components/layout/StatusBar.js +10 -17
  34. package/dist/components/layout/StatusBar.js.map +1 -1
  35. package/dist/components/menu/SettingsSelectionMenu.d.ts.map +1 -1
  36. package/dist/components/menu/SettingsSelectionMenu.js +3 -1
  37. package/dist/components/menu/SettingsSelectionMenu.js.map +1 -1
  38. package/dist/components/menu/SubagentPoolSelectionMenu.d.ts +19 -0
  39. package/dist/components/menu/SubagentPoolSelectionMenu.d.ts.map +1 -0
  40. package/dist/components/menu/{MentorPoolSelectionMenu.js → SubagentPoolSelectionMenu.js} +29 -15
  41. package/dist/components/menu/SubagentPoolSelectionMenu.js.map +1 -0
  42. package/dist/context/InputContext.d.ts +1 -1
  43. package/dist/context/InputContext.d.ts.map +1 -1
  44. package/dist/context/InputContext.js +2 -2
  45. package/dist/context/InputContext.js.map +1 -1
  46. package/dist/contracts/conversation-items.d.ts +4 -0
  47. package/dist/contracts/conversation-items.d.ts.map +1 -1
  48. package/dist/contracts/provider-input.d.ts +2 -0
  49. package/dist/contracts/provider-input.d.ts.map +1 -1
  50. package/dist/contracts/provider-input.js.map +1 -1
  51. package/dist/gateway/assertion.d.ts +1 -1
  52. package/dist/gateway/assertion.d.ts.map +1 -1
  53. package/dist/gateway/contracts.d.ts +13 -3
  54. package/dist/gateway/contracts.d.ts.map +1 -1
  55. package/dist/gateway/contracts.js +56 -0
  56. package/dist/gateway/contracts.js.map +1 -1
  57. package/dist/gateway/gateway.d.ts +40 -1
  58. package/dist/gateway/gateway.d.ts.map +1 -1
  59. package/dist/gateway/gateway.js +1319 -76
  60. package/dist/gateway/gateway.js.map +1 -1
  61. package/dist/gateway/persistence/admission-persistence.d.ts +3 -3
  62. package/dist/gateway/persistence/admission-persistence.d.ts.map +1 -1
  63. package/dist/gateway/persistence/admission-persistence.js +25 -11
  64. package/dist/gateway/persistence/admission-persistence.js.map +1 -1
  65. package/dist/gateway/persistence/contracts.d.ts +5 -3
  66. package/dist/gateway/persistence/contracts.d.ts.map +1 -1
  67. package/dist/gateway/persistence/contracts.js +13 -0
  68. package/dist/gateway/persistence/contracts.js.map +1 -1
  69. package/dist/gateway/persistence/interaction-checkpoint.d.ts +5 -0
  70. package/dist/gateway/persistence/interaction-checkpoint.d.ts.map +1 -1
  71. package/dist/gateway/persistence/interaction-checkpoint.js +22 -0
  72. package/dist/gateway/persistence/interaction-checkpoint.js.map +1 -1
  73. package/dist/gateway/persistence/projection.d.ts.map +1 -1
  74. package/dist/gateway/persistence/projection.js +4 -1
  75. package/dist/gateway/persistence/projection.js.map +1 -1
  76. package/dist/gateway/persistence/session-index.d.ts +1 -1
  77. package/dist/gateway/persistence/session-index.d.ts.map +1 -1
  78. package/dist/gateway/persistence/session-index.js +2 -2
  79. package/dist/gateway/persistence/session-index.js.map +1 -1
  80. package/dist/gateway/persistence/session-snapshot.d.ts +32 -0
  81. package/dist/gateway/persistence/session-snapshot.d.ts.map +1 -0
  82. package/dist/gateway/persistence/session-snapshot.js +50 -0
  83. package/dist/gateway/persistence/session-snapshot.js.map +1 -0
  84. package/dist/gateway/runtime-factory.d.ts +37 -1
  85. package/dist/gateway/runtime-factory.d.ts.map +1 -1
  86. package/dist/gateway/runtime-factory.js +155 -24
  87. package/dist/gateway/runtime-factory.js.map +1 -1
  88. package/dist/gateway/safe-log.d.ts +1 -1
  89. package/dist/gateway/safe-log.d.ts.map +1 -1
  90. package/dist/gateway/safe-log.js +5 -46
  91. package/dist/gateway/safe-log.js.map +1 -1
  92. package/dist/gateway/serve-args.d.ts +53 -0
  93. package/dist/gateway/serve-args.d.ts.map +1 -0
  94. package/dist/gateway/serve-args.js +172 -0
  95. package/dist/gateway/serve-args.js.map +1 -0
  96. package/dist/gateway/serve.d.ts +79 -0
  97. package/dist/gateway/serve.d.ts.map +1 -0
  98. package/dist/gateway/serve.js +375 -0
  99. package/dist/gateway/serve.js.map +1 -0
  100. package/dist/gateway/server-session.d.ts +9 -0
  101. package/dist/gateway/server-session.d.ts.map +1 -1
  102. package/dist/gateway/server-session.js +142 -26
  103. package/dist/gateway/server-session.js.map +1 -1
  104. package/dist/gateway/server.d.ts.map +1 -1
  105. package/dist/gateway/server.js +13 -2
  106. package/dist/gateway/server.js.map +1 -1
  107. package/dist/hooks/use-subagent-pool-selection.d.ts +115 -0
  108. package/dist/hooks/use-subagent-pool-selection.d.ts.map +1 -0
  109. package/dist/hooks/{use-mentor-pool-selection.js → use-subagent-pool-selection.js} +110 -54
  110. package/dist/hooks/use-subagent-pool-selection.js.map +1 -0
  111. package/dist/lib/agent-client.d.ts +3 -0
  112. package/dist/lib/agent-client.d.ts.map +1 -1
  113. package/dist/lib/agent-client.js +7 -0
  114. package/dist/lib/agent-client.js.map +1 -1
  115. package/dist/lib/agent-configuration.d.ts +1 -0
  116. package/dist/lib/agent-configuration.d.ts.map +1 -1
  117. package/dist/lib/agent-configuration.js +3 -0
  118. package/dist/lib/agent-configuration.js.map +1 -1
  119. package/dist/lib/agent-factory.d.ts +2 -0
  120. package/dist/lib/agent-factory.d.ts.map +1 -1
  121. package/dist/lib/agent-factory.js +5 -0
  122. package/dist/lib/agent-factory.js.map +1 -1
  123. package/dist/lib/subagent-bridge.d.ts +5 -0
  124. package/dist/lib/subagent-bridge.d.ts.map +1 -1
  125. package/dist/lib/subagent-bridge.js +5 -0
  126. package/dist/lib/subagent-bridge.js.map +1 -1
  127. package/dist/lib/tool-selection-policy.d.ts +2 -0
  128. package/dist/lib/tool-selection-policy.d.ts.map +1 -1
  129. package/dist/lib/tool-selection-policy.js +6 -1
  130. package/dist/lib/tool-selection-policy.js.map +1 -1
  131. package/dist/prompts/explorer-delegation-contract.test.ts +1 -1
  132. package/dist/prompts/prompt-constructor.d.ts.map +1 -1
  133. package/dist/prompts/prompt-constructor.js +1 -0
  134. package/dist/prompts/prompt-constructor.js.map +1 -1
  135. package/dist/prompts/prompt-constructor.test.ts +3 -0
  136. package/dist/prompts/prompt-constructor.ts +1 -0
  137. package/dist/prompts/prompt-profiles.d.ts.map +1 -1
  138. package/dist/prompts/prompt-profiles.js +3 -2
  139. package/dist/prompts/prompt-profiles.js.map +1 -1
  140. package/dist/prompts/prompt-profiles.ts +4 -2
  141. package/dist/prompts/subagent-delegation.d.ts +2 -1
  142. package/dist/prompts/subagent-delegation.d.ts.map +1 -1
  143. package/dist/prompts/subagent-delegation.js +10 -5
  144. package/dist/prompts/subagent-delegation.js.map +1 -1
  145. package/dist/prompts/subagent-delegation.test.ts +10 -3
  146. package/dist/prompts/subagent-delegation.ts +16 -4
  147. package/dist/prompts/subagents/librarian.md +26 -21
  148. package/dist/prompts/subagents/reviewer.md +33 -0
  149. package/dist/services/agent-runtime/application-run-loop.d.ts.map +1 -1
  150. package/dist/services/agent-runtime/application-run-loop.js +10 -5
  151. package/dist/services/agent-runtime/application-run-loop.js.map +1 -1
  152. package/dist/services/agent-runtime/context-compaction/local-context-compactor.d.ts +9 -0
  153. package/dist/services/agent-runtime/context-compaction/local-context-compactor.d.ts.map +1 -1
  154. package/dist/services/agent-runtime/context-compaction/local-context-compactor.js +6 -0
  155. package/dist/services/agent-runtime/context-compaction/local-context-compactor.js.map +1 -1
  156. package/dist/services/agent-runtime/model-resolver.d.ts +6 -0
  157. package/dist/services/agent-runtime/model-resolver.d.ts.map +1 -1
  158. package/dist/services/agent-runtime/model-resolver.js +18 -4
  159. package/dist/services/agent-runtime/model-resolver.js.map +1 -1
  160. package/dist/services/approval/non-interactive-approval-policy.d.ts.map +1 -1
  161. package/dist/services/approval/non-interactive-approval-policy.js +2 -1
  162. package/dist/services/approval/non-interactive-approval-policy.js.map +1 -1
  163. package/dist/services/approval/shell-auto-approval-evaluator.js +2 -2
  164. package/dist/services/approval/shell-auto-approval-evaluator.js.map +1 -1
  165. package/dist/services/conversation/conversation-service.d.ts +28 -0
  166. package/dist/services/conversation/conversation-service.d.ts.map +1 -1
  167. package/dist/services/conversation/conversation-service.js +95 -27
  168. package/dist/services/conversation/conversation-service.js.map +1 -1
  169. package/dist/services/conversation/run-item-normalizer.d.ts.map +1 -1
  170. package/dist/services/conversation/run-item-normalizer.js +3 -0
  171. package/dist/services/conversation/run-item-normalizer.js.map +1 -1
  172. package/dist/services/conversation/session-browser.d.ts +5 -0
  173. package/dist/services/conversation/session-browser.d.ts.map +1 -1
  174. package/dist/services/conversation/session-browser.js +94 -31
  175. package/dist/services/conversation/session-browser.js.map +1 -1
  176. package/dist/services/conversation/session-index/session-index-database.d.ts +1 -0
  177. package/dist/services/conversation/session-index/session-index-database.d.ts.map +1 -1
  178. package/dist/services/conversation/session-index/session-index-database.js +6 -4
  179. package/dist/services/conversation/session-index/session-index-database.js.map +1 -1
  180. package/dist/services/conversation/session-index/session-index-schema.d.ts +1 -1
  181. package/dist/services/conversation/session-index/session-index-schema.d.ts.map +1 -1
  182. package/dist/services/conversation/session-index/session-index-schema.js +2 -1
  183. package/dist/services/conversation/session-index/session-index-schema.js.map +1 -1
  184. package/dist/services/conversation-agent-client.d.ts +3 -0
  185. package/dist/services/conversation-agent-client.d.ts.map +1 -1
  186. package/dist/services/memory/memory-capabilities.d.ts.map +1 -1
  187. package/dist/services/memory/memory-capabilities.js +2 -5
  188. package/dist/services/memory/memory-capabilities.js.map +1 -1
  189. package/dist/services/plan-mode-interceptor.js +2 -2
  190. package/dist/services/plan-mode-interceptor.js.map +1 -1
  191. package/dist/services/sandboxed-code-host/host-types.d.ts +12 -1
  192. package/dist/services/sandboxed-code-host/host-types.d.ts.map +1 -1
  193. package/dist/services/sandboxed-code-host/host-types.js.map +1 -1
  194. package/dist/services/sandboxed-code-host/host-worker.d.ts.map +1 -1
  195. package/dist/services/sandboxed-code-host/host-worker.js +146 -31
  196. package/dist/services/sandboxed-code-host/host-worker.js.map +1 -1
  197. package/dist/services/sandboxed-code-host/sandbox.d.ts +6 -1
  198. package/dist/services/sandboxed-code-host/sandbox.d.ts.map +1 -1
  199. package/dist/services/sandboxed-code-host/sandbox.js +5 -0
  200. package/dist/services/sandboxed-code-host/sandbox.js.map +1 -1
  201. package/dist/services/sandboxed-code-host/sandboxed-code-host.d.ts +3 -3
  202. package/dist/services/sandboxed-code-host/sandboxed-code-host.d.ts.map +1 -1
  203. package/dist/services/sandboxed-code-host/sandboxed-code-host.js +173 -22
  204. package/dist/services/sandboxed-code-host/sandboxed-code-host.js.map +1 -1
  205. package/dist/services/session/session-access-state.d.ts +5 -1
  206. package/dist/services/session/session-access-state.d.ts.map +1 -1
  207. package/dist/services/session/session-access-state.js +8 -1
  208. package/dist/services/session/session-access-state.js.map +1 -1
  209. package/dist/services/session/session-client-factory.d.ts +1 -0
  210. package/dist/services/session/session-client-factory.d.ts.map +1 -1
  211. package/dist/services/session/session-client-factory.js +1 -1
  212. package/dist/services/session/session-client-factory.js.map +1 -1
  213. package/dist/services/session/session-composition.d.ts +22 -4
  214. package/dist/services/session/session-composition.d.ts.map +1 -1
  215. package/dist/services/session/session-composition.js +9 -5
  216. package/dist/services/session/session-composition.js.map +1 -1
  217. package/dist/services/settings/ancillary-settings-migration.d.ts.map +1 -1
  218. package/dist/services/settings/ancillary-settings-migration.js +8 -1
  219. package/dist/services/settings/ancillary-settings-migration.js.map +1 -1
  220. package/dist/services/settings/settings-schema.d.ts +12 -40
  221. package/dist/services/settings/settings-schema.d.ts.map +1 -1
  222. package/dist/services/settings/settings-schema.js +27 -53
  223. package/dist/services/settings/settings-schema.js.map +1 -1
  224. package/dist/services/settings/settings-sources.d.ts +0 -3
  225. package/dist/services/settings/settings-sources.d.ts.map +1 -1
  226. package/dist/services/settings/settings-sources.js +0 -3
  227. package/dist/services/settings/settings-sources.js.map +1 -1
  228. package/dist/services/settings/settings-ui-metadata.d.ts.map +1 -1
  229. package/dist/services/settings/settings-ui-metadata.js +3 -3
  230. package/dist/services/settings/settings-ui-metadata.js.map +1 -1
  231. package/dist/services/settings/test-helpers/settings-consumer-inventory.d.ts +1 -1
  232. package/dist/services/settings/test-helpers/settings-consumer-inventory.d.ts.map +1 -1
  233. package/dist/services/settings/test-helpers/settings-consumer-inventory.js +5 -8
  234. package/dist/services/settings/test-helpers/settings-consumer-inventory.js.map +1 -1
  235. package/dist/services/subagents/nested-runner.d.ts +3 -0
  236. package/dist/services/subagents/nested-runner.d.ts.map +1 -1
  237. package/dist/services/subagents/nested-runner.js +31 -4
  238. package/dist/services/subagents/nested-runner.js.map +1 -1
  239. package/dist/services/subagents/role-loader.d.ts.map +1 -1
  240. package/dist/services/subagents/role-loader.js +7 -12
  241. package/dist/services/subagents/role-loader.js.map +1 -1
  242. package/dist/services/subagents/runtime.d.ts +3 -0
  243. package/dist/services/subagents/runtime.d.ts.map +1 -1
  244. package/dist/services/subagents/runtime.js +35 -3
  245. package/dist/services/subagents/runtime.js.map +1 -1
  246. package/dist/services/subagents/subagent-async-registry.d.ts +10 -1
  247. package/dist/services/subagents/subagent-async-registry.d.ts.map +1 -1
  248. package/dist/services/subagents/subagent-async-registry.js +21 -4
  249. package/dist/services/subagents/subagent-async-registry.js.map +1 -1
  250. package/dist/services/subagents/subagent-manager.d.ts +3 -0
  251. package/dist/services/subagents/subagent-manager.d.ts.map +1 -1
  252. package/dist/services/subagents/subagent-manager.js.map +1 -1
  253. package/dist/services/subagents/subagent-pool-config.d.ts +39 -0
  254. package/dist/services/subagents/subagent-pool-config.d.ts.map +1 -0
  255. package/dist/services/subagents/subagent-pool-config.js +84 -0
  256. package/dist/services/subagents/subagent-pool-config.js.map +1 -0
  257. package/dist/services/subagents/subagent-role-pool-selector.d.ts +29 -0
  258. package/dist/services/subagents/subagent-role-pool-selector.d.ts.map +1 -0
  259. package/dist/services/subagents/subagent-role-pool-selector.js +56 -0
  260. package/dist/services/subagents/subagent-role-pool-selector.js.map +1 -0
  261. package/dist/services/subagents/tool-policy.d.ts +13 -0
  262. package/dist/services/subagents/tool-policy.d.ts.map +1 -1
  263. package/dist/services/subagents/tool-policy.js +36 -4
  264. package/dist/services/subagents/tool-policy.js.map +1 -1
  265. package/dist/services/subagents/types.d.ts +1 -1
  266. package/dist/services/subagents/types.d.ts.map +1 -1
  267. package/dist/services/subagents/types.js +1 -1
  268. package/dist/services/subagents/types.js.map +1 -1
  269. package/dist/test-helpers/outside-sandbox-temp-dir.d.ts +2 -0
  270. package/dist/test-helpers/outside-sandbox-temp-dir.d.ts.map +1 -0
  271. package/dist/test-helpers/outside-sandbox-temp-dir.js +38 -0
  272. package/dist/test-helpers/outside-sandbox-temp-dir.js.map +1 -0
  273. package/dist/tools/agent/configure-task-check-in.d.ts +2 -0
  274. package/dist/tools/agent/configure-task-check-in.d.ts.map +1 -1
  275. package/dist/tools/agent/configure-task-check-in.js +3 -0
  276. package/dist/tools/agent/configure-task-check-in.js.map +1 -1
  277. package/dist/tools/agent/run-explorer.d.ts +6 -0
  278. package/dist/tools/agent/run-explorer.d.ts.map +1 -0
  279. package/dist/tools/agent/run-explorer.js +58 -0
  280. package/dist/tools/agent/run-explorer.js.map +1 -0
  281. package/dist/tools/agent/run-subagent-async.d.ts +2 -0
  282. package/dist/tools/agent/run-subagent-async.d.ts.map +1 -1
  283. package/dist/tools/agent/run-subagent-async.js +3 -3
  284. package/dist/tools/agent/run-subagent-async.js.map +1 -1
  285. package/dist/tools/agent/run-subagent.d.ts +2 -2
  286. package/dist/tools/agent/run-subagent.d.ts.map +1 -1
  287. package/dist/tools/agent/run-subagent.js +8 -6
  288. package/dist/tools/agent/run-subagent.js.map +1 -1
  289. package/dist/tools/file/apply-patch.d.ts.map +1 -1
  290. package/dist/tools/file/apply-patch.js +7 -2
  291. package/dist/tools/file/apply-patch.js.map +1 -1
  292. package/dist/tools/file/create-file.d.ts.map +1 -1
  293. package/dist/tools/file/create-file.js +4 -0
  294. package/dist/tools/file/create-file.js.map +1 -1
  295. package/dist/tools/file/glob.d.ts.map +1 -1
  296. package/dist/tools/file/glob.js +6 -0
  297. package/dist/tools/file/glob.js.map +1 -1
  298. package/dist/tools/file/search-replace.d.ts.map +1 -1
  299. package/dist/tools/file/search-replace.js +6 -2
  300. package/dist/tools/file/search-replace.js.map +1 -1
  301. package/dist/tools/format-helpers.d.ts +5 -0
  302. package/dist/tools/format-helpers.d.ts.map +1 -1
  303. package/dist/tools/format-helpers.js.map +1 -1
  304. package/dist/tools/memory/memory-tools.d.ts +0 -1
  305. package/dist/tools/memory/memory-tools.d.ts.map +1 -1
  306. package/dist/tools/memory/memory-tools.js +1 -116
  307. package/dist/tools/memory/memory-tools.js.map +1 -1
  308. package/dist/tools/scripted-return-contract.d.ts +24 -0
  309. package/dist/tools/scripted-return-contract.d.ts.map +1 -0
  310. package/dist/tools/scripted-return-contract.js +36 -0
  311. package/dist/tools/scripted-return-contract.js.map +1 -0
  312. package/dist/tools/session-browser/session-browser-tools.d.ts.map +1 -1
  313. package/dist/tools/session-browser/session-browser-tools.js +21 -8
  314. package/dist/tools/session-browser/session-browser-tools.js.map +1 -1
  315. package/dist/tools/system/run-code/index.d.ts +3 -0
  316. package/dist/tools/system/run-code/index.d.ts.map +1 -1
  317. package/dist/tools/system/run-code/index.js +3 -0
  318. package/dist/tools/system/run-code/index.js.map +1 -1
  319. package/dist/tools/system/run-code/run-code-execution.d.ts +56 -0
  320. package/dist/tools/system/run-code/run-code-execution.d.ts.map +1 -0
  321. package/dist/tools/system/run-code/run-code-execution.js +68 -0
  322. package/dist/tools/system/run-code/run-code-execution.js.map +1 -0
  323. package/dist/tools/system/run-code/run-code-runtime-contract.d.ts +20 -0
  324. package/dist/tools/system/run-code/run-code-runtime-contract.d.ts.map +1 -0
  325. package/dist/tools/system/run-code/run-code-runtime-contract.js +2 -0
  326. package/dist/tools/system/run-code/run-code-runtime-contract.js.map +1 -0
  327. package/dist/tools/system/run-code/run-code-runtime.d.ts +99 -0
  328. package/dist/tools/system/run-code/run-code-runtime.d.ts.map +1 -0
  329. package/dist/tools/system/run-code/run-code-runtime.js +719 -0
  330. package/dist/tools/system/run-code/run-code-runtime.js.map +1 -0
  331. package/dist/tools/system/run-code/run-code-telemetry.d.ts +12 -10
  332. package/dist/tools/system/run-code/run-code-telemetry.d.ts.map +1 -1
  333. package/dist/tools/system/run-code/run-code-telemetry.js +48 -72
  334. package/dist/tools/system/run-code/run-code-telemetry.js.map +1 -1
  335. package/dist/tools/system/run-code/run-code.d.ts +22 -59
  336. package/dist/tools/system/run-code/run-code.d.ts.map +1 -1
  337. package/dist/tools/system/run-code/run-code.js +118 -784
  338. package/dist/tools/system/run-code/run-code.js.map +1 -1
  339. package/dist/tools/system/run-code/tools-header.d.ts +16 -4
  340. package/dist/tools/system/run-code/tools-header.d.ts.map +1 -1
  341. package/dist/tools/system/run-code/tools-header.js +32 -16
  342. package/dist/tools/system/run-code/tools-header.js.map +1 -1
  343. package/dist/tools/system/shell.d.ts +3 -0
  344. package/dist/tools/system/shell.d.ts.map +1 -1
  345. package/dist/tools/system/shell.js +13 -3
  346. package/dist/tools/system/shell.js.map +1 -1
  347. package/dist/tools/types.d.ts +12 -4
  348. package/dist/tools/types.d.ts.map +1 -1
  349. package/dist/tools/types.js.map +1 -1
  350. package/dist/utils/ai/model-settings.d.ts +6 -0
  351. package/dist/utils/ai/model-settings.d.ts.map +1 -1
  352. package/dist/utils/ai/model-settings.js +7 -1
  353. package/dist/utils/ai/model-settings.js.map +1 -1
  354. package/dist/utils/ai/token-usage.d.ts +4 -5
  355. package/dist/utils/ai/token-usage.d.ts.map +1 -1
  356. package/dist/utils/ai/token-usage.js +3 -3
  357. package/dist/utils/ai/token-usage.js.map +1 -1
  358. package/dist/utils/conversation/conversation-utils.d.ts.map +1 -1
  359. package/dist/utils/conversation/conversation-utils.js +3 -0
  360. package/dist/utils/conversation/conversation-utils.js.map +1 -1
  361. package/dist/utils/settings-command.d.ts.map +1 -1
  362. package/dist/utils/settings-command.js +2 -1
  363. package/dist/utils/settings-command.js.map +1 -1
  364. package/dist/utils/shell/sandbox/sandbox-policy.d.ts +2 -0
  365. package/dist/utils/shell/sandbox/sandbox-policy.d.ts.map +1 -1
  366. package/dist/utils/shell/sandbox/sandbox-policy.js +1 -1
  367. package/dist/utils/shell/sandbox/sandbox-policy.js.map +1 -1
  368. package/dist/utils/streaming/streaming-session-factory.d.ts.map +1 -1
  369. package/dist/utils/streaming/streaming-session-factory.js +22 -22
  370. package/dist/utils/streaming/streaming-session-factory.js.map +1 -1
  371. package/package.json +1 -1
  372. package/dist/components/input/MentorPoolMenuSession.d.ts +0 -9
  373. package/dist/components/input/MentorPoolMenuSession.d.ts.map +0 -1
  374. package/dist/components/input/MentorPoolMenuSession.js.map +0 -1
  375. package/dist/components/menu/MentorPoolSelectionMenu.d.ts +0 -13
  376. package/dist/components/menu/MentorPoolSelectionMenu.d.ts.map +0 -1
  377. package/dist/components/menu/MentorPoolSelectionMenu.js.map +0 -1
  378. package/dist/hooks/use-mentor-pool-selection.d.ts +0 -105
  379. package/dist/hooks/use-mentor-pool-selection.d.ts.map +0 -1
  380. package/dist/hooks/use-mentor-pool-selection.js.map +0 -1
@@ -7,12 +7,17 @@ import { createSafeLogMetadata, GatewayAuditLog } from './safe-log.js';
7
7
  import { AssertionVerifier } from './assertion.js';
8
8
  import { SqliteReplayLedger } from './replay-ledger.js';
9
9
  import { GatewayServer } from './server.js';
10
+ import { RuntimeFactoryError } from './runtime-factory.js';
11
+ import { projectConversationMessage } from '../services/conversation/conversation-message-projection.js';
12
+ import { normalizeRunItem } from '../services/conversation/run-item-normalizer.js';
10
13
  import { FROZEN_AGENT_EVENT_TYPES, GatewayPersistenceError, TERMINAL_AGENT_EVENT_TYPES, } from './persistence/contracts.js';
11
- import { createSessionProjectionSource, } from './persistence/projection.js';
14
+ import { createSessionProjectionSource, hydrateConversationService, } from './persistence/projection.js';
12
15
  import { decideInteraction, InteractionProtocolError, projectPendingInteraction, validatePendingInteractionDto, } from './interaction-protocol.js';
13
- import { GatewayAdmissionPersistence } from './persistence/admission-persistence.js';
16
+ import { GatewayAdmissionPersistence, normalizedBodyHash } from './persistence/admission-persistence.js';
14
17
  import { GATEWAY_EVENT_HEARTBEAT_INTERVAL_MS, GATEWAY_EVENT_STREAM_CONTENT_TYPE, GATEWAY_EVENT_STREAM_HEARTBEAT, } from './server.js';
15
18
  import { ServerSession } from './server-session.js';
19
+ import { createSessionSettingsSnapshot } from './launcher-seam.js';
20
+ import { readSessionSnapshot, writeSessionSnapshot } from './persistence/session-snapshot.js';
16
21
  import { ServerSessionError } from './server-session.js';
17
22
  import { DynamicWorkspaceRegistry, DynamicWorkspaceRegistryError } from './dynamic-workspace-registry.js';
18
23
  import { accessSync, constants as fsConstants, realpathSync, statSync } from 'node:fs';
@@ -28,7 +33,30 @@ import { loginToCodex } from '../providers/codex-auth.js';
28
33
  import { loginToGrok } from '../providers/grok-auth.js';
29
34
  const MAX_BUFFERED_SSE_EVENTS = 256;
30
35
  const NONCRITICAL_RUNTIME_EVENT_TYPES = new Set(['text_delta', 'reasoning_delta']);
36
+ let malformedCommandMessageDrops = 0;
31
37
  const isCriticalRuntimeEvent = (event) => !NONCRITICAL_RUNTIME_EVENT_TYPES.has(event.type);
38
+ /**
39
+ * `retry-turn` replays the canonical transcript verbatim, so a retry is only
40
+ * meaningful while that transcript still ends on the user message of a turn
41
+ * that never produced output. Assistant output or a tool result after the last
42
+ * genuine user message means the turn committed something; synthetic user
43
+ * items (shell context, mode notices, local summaries) are not turns.
44
+ */
45
+ const hasFailedTurnToRetry = (history) => {
46
+ for (let index = history.length - 1; index >= 0; index -= 1) {
47
+ const item = history[index];
48
+ if (normalizeRunItem(item).some((normalized) => normalized.type === 'tool_result'))
49
+ return false;
50
+ const message = projectConversationMessage(item);
51
+ if (!message || message.isSynthetic)
52
+ continue;
53
+ if (message.role === 'user')
54
+ return true;
55
+ if (message.role === 'assistant')
56
+ return false;
57
+ }
58
+ return false;
59
+ };
32
60
  const INTERACTION_METRICS = [
33
61
  'interaction_presented',
34
62
  'interaction_updated',
@@ -91,6 +119,37 @@ const PUBLIC_EVENT_PAYLOAD_KEYS = new Set([
91
119
  'riskLevel',
92
120
  'deniedRead',
93
121
  'runBudgetEvidence',
122
+ 'agentId',
123
+ 'name',
124
+ 'task',
125
+ 'async',
126
+ 'toolCallId',
127
+ 'commandMessages',
128
+ 'status',
129
+ 'output',
130
+ 'error',
131
+ 'finalText',
132
+ 'finalTextTruncated',
133
+ 'toolsUsed',
134
+ 'count',
135
+ 'terminalCause',
136
+ 'provider',
137
+ 'attempt',
138
+ 'maxRetries',
139
+ 'errorMessage',
140
+ 'retryType',
141
+ 'errorKind',
142
+ 'delayMs',
143
+ 'retryAfterMs',
144
+ 'attempts',
145
+ 'maxAttempts',
146
+ 'canRetry',
147
+ 'sessionId',
148
+ 'inputTokensBefore',
149
+ 'inputTokensAfter',
150
+ 'durationMs',
151
+ 'strategy',
152
+ 'errorCategory',
94
153
  ]);
95
154
  const LOCAL_OWNER_PURPOSES = new Set([
96
155
  'settings_read',
@@ -188,8 +247,15 @@ export class Term2Gateway {
188
247
  #persisted = new Map();
189
248
  #admissions;
190
249
  #interactionBindings = new Map();
250
+ #childTurnIds = new Map();
251
+ #childRoles = new Map();
252
+ #lastTurnIds = new Map();
191
253
  #interactionCounters = new Map();
192
254
  #eventPersistenceTails = new Map();
255
+ #inflightCommands = new Map();
256
+ // Keyed by owner and session: an in-flight revival belongs to the owner who
257
+ // started it, and no other caller may inherit its result.
258
+ #sessionRestores = new Map();
193
259
  #shutdownInProgress = false;
194
260
  #shutdownPromise;
195
261
  #server;
@@ -234,6 +300,7 @@ export class Term2Gateway {
234
300
  otpTtlMs: config.pairing.otpTtlMs,
235
301
  maxAttempts: config.pairing.maxAttempts,
236
302
  trustStore: trustedStore,
303
+ ...(config.pairing.printOtp ? { printOtp: config.pairing.printOtp } : {}),
237
304
  })
238
305
  : undefined;
239
306
  this.#audit = new GatewayAuditLog(config.auditWriter);
@@ -373,6 +440,8 @@ export class Term2Gateway {
373
440
  return await this.#submitMessage(claims, input.body);
374
441
  if (claims.purpose === 'abort')
375
442
  return await this.#abortSession(claims, input.body);
443
+ if (claims.purpose === 'command_invoke')
444
+ return await this.#invokeCommand(claims, input.body, input.correlationId);
376
445
  if (claims.purpose === 'interaction_resolve')
377
446
  return await this.#resolveInteraction(claims, route.interactionId, input.body);
378
447
  if (claims.purpose === 'events_connect')
@@ -672,6 +741,7 @@ export class Term2Gateway {
672
741
  turnId,
673
742
  variant: dto.variant,
674
743
  dto,
744
+ owner: 'root',
675
745
  };
676
746
  this.#interactionBindings.set(sessionId, binding);
677
747
  this.#countInteraction('interaction_presented');
@@ -680,6 +750,56 @@ export class Term2Gateway {
680
750
  #countInteraction(metric) {
681
751
  this.#interactionCounters.set(metric, (this.#interactionCounters.get(metric) ?? 0) + 1);
682
752
  }
753
+ #backgroundInteractionBinding(sessionId, turnId, event, session) {
754
+ const publicInteractionId = crypto.randomUUID();
755
+ if (event.type === 'subagent_approval_required') {
756
+ const snapshot = session.service.backgroundSubagentApprovals.getSnapshot();
757
+ const entry = snapshot.current;
758
+ if (!entry)
759
+ return undefined;
760
+ const dto = projectPendingInteraction({
761
+ agentName: event.role,
762
+ toolName: entry.toolName,
763
+ callId: entry.toolCallId,
764
+ argumentsText: entry.argumentsText,
765
+ }, publicInteractionId, 1);
766
+ const binding = {
767
+ publicInteractionId,
768
+ expectedInteractionId: 0,
769
+ continuationGeneration: crypto.randomUUID(),
770
+ revision: 1,
771
+ turnId,
772
+ variant: dto.variant,
773
+ dto,
774
+ owner: 'background_approval',
775
+ childRunId: event.agentId,
776
+ childRole: event.role,
777
+ backgroundEntry: entry,
778
+ backgroundRevision: snapshot.revision,
779
+ };
780
+ this.#interactionBindings.set(sessionId, binding);
781
+ return binding;
782
+ }
783
+ const argumentsText = JSON.stringify({
784
+ questions: [{ question: event.question, options: [], is_multi_select: false }],
785
+ });
786
+ const dto = projectPendingInteraction({ agentName: event.role, toolName: 'ask_user', argumentsText }, publicInteractionId, 1);
787
+ const binding = {
788
+ publicInteractionId,
789
+ expectedInteractionId: 0,
790
+ continuationGeneration: crypto.randomUUID(),
791
+ revision: 1,
792
+ turnId,
793
+ variant: dto.variant,
794
+ dto,
795
+ owner: 'background_question',
796
+ childRunId: event.runId,
797
+ childMessageId: event.messageId,
798
+ childRole: event.role,
799
+ };
800
+ this.#interactionBindings.set(sessionId, binding);
801
+ return binding;
802
+ }
683
803
  async #ensurePersistedSession(ownerUserId, sessionId) {
684
804
  const existing = this.#persisted.get(sessionId);
685
805
  if (existing) {
@@ -754,10 +874,25 @@ export class Term2Gateway {
754
874
  return publicError(503, 'persistence_unavailable', 'gateway persistence unavailable', true);
755
875
  if (!isMessageBody(body))
756
876
  return publicError(400, 'validation_error', 'message request is invalid');
757
- const session = this.#sessions.get(claims.sessionId);
758
- const persisted = this.#persisted.get(claims.sessionId);
877
+ let session = this.#sessions.get(claims.sessionId);
878
+ let persisted = this.#persisted.get(claims.sessionId);
879
+ if (!(session instanceof ServerSession) || !persisted) {
880
+ // A session from a previous gateway process has no live runtime; revive
881
+ // it through the same factory path a fresh session uses.
882
+ const restored = await this.#ensureLiveRuntime(claims.sub, claims.sessionId);
883
+ if (restored && 'body' in restored)
884
+ return restored;
885
+ if (restored) {
886
+ session = restored.session;
887
+ persisted = restored.persisted;
888
+ }
889
+ }
759
890
  if (!(session instanceof ServerSession) || !persisted)
760
891
  return publicError(409, 'session_not_admitting', 'session is not admitting messages');
892
+ // Owner scoping holds on every path that reaches admission, live or
893
+ // restored: a session owned by someone else does not exist for this caller.
894
+ if (session.binding.ownerUserId !== claims.sub)
895
+ return publicError(404, 'not_found', 'session not found');
761
896
  const turnId = crypto.randomUUID();
762
897
  try {
763
898
  const result = await this.#admissions.admit({
@@ -850,6 +985,401 @@ export class Term2Gateway {
850
985
  };
851
986
  return publicError(409, 'session_not_admitting', 'session is interrupted');
852
987
  }
988
+ async #invokeCommand(claims, body, correlationId) {
989
+ if (!isCommandBody(body))
990
+ return publicError(400, 'validation_error', 'command request is invalid');
991
+ let session = this.#sessions.get(claims.sessionId);
992
+ if (!session || session.binding.ownerUserId !== claims.sub) {
993
+ // A restored session has no live runtime; commands revive it the same
994
+ // way message_submit does. Sessions unknown to this owner keep the 404.
995
+ const restored = await this.#ensureLiveRuntime(claims.sub, claims.sessionId);
996
+ if (restored && 'body' in restored)
997
+ return restored;
998
+ if (restored)
999
+ session = restored.session;
1000
+ }
1001
+ if (!session || session.binding.ownerUserId !== claims.sub) {
1002
+ return publicError(404, 'not_found', 'session not found');
1003
+ }
1004
+ if (!(session instanceof ServerSession)) {
1005
+ return publicError(409, 'session_not_admitting', 'session is not admitting commands');
1006
+ }
1007
+ if (session.status === 'running' || session.status === 'awaiting_interaction') {
1008
+ return publicError(409, 'session_busy', 'session is busy');
1009
+ }
1010
+ if (!COMMAND_ALLOWLIST.has(body.commandId)) {
1011
+ return publicError(422, 'command_not_allowed', 'command is not allowed');
1012
+ }
1013
+ await this.#audit.write(createSafeLogMetadata({
1014
+ operation: 'command_invoke',
1015
+ outcome: 'allowed',
1016
+ reasonCode: 'accepted',
1017
+ sessionId: session.sessionId,
1018
+ workspaceId: session.binding.workspaceId,
1019
+ grantVersion: session.binding.grantVersion,
1020
+ access: session.binding.access,
1021
+ ...(correlationId ? { correlationId } : {}),
1022
+ }));
1023
+ const inflightKey = `${claims.sub}:${session.sessionId}:${body.clientRequestId}`;
1024
+ const bodyHash = normalizedBodyHash(body);
1025
+ const inflight = this.#inflightCommands.get(inflightKey);
1026
+ if (inflight) {
1027
+ if (inflight.bodyHash !== bodyHash) {
1028
+ throw new GatewayPersistenceError('conflict', 'idempotency key has a different body');
1029
+ }
1030
+ const existing = await inflight.promise;
1031
+ return {
1032
+ ...existing,
1033
+ body: {
1034
+ ...existing.body,
1035
+ replayed: true,
1036
+ },
1037
+ };
1038
+ }
1039
+ if (this.#admissions) {
1040
+ const lookup = this.#admissions.lookup(claims.sub, session.sessionId, body.clientRequestId, body);
1041
+ if (lookup.kind === 'conflict') {
1042
+ throw new GatewayPersistenceError('conflict', 'idempotency key has a different body');
1043
+ }
1044
+ if (lookup.kind === 'replayed') {
1045
+ if (body.commandId === 'compact') {
1046
+ const parts = lookup.record.turnId.split(':');
1047
+ if (lookup.record.result === 'failed' || parts[1] === 'failed') {
1048
+ return publicError(409, 'compact_failed', 'compaction failed', false);
1049
+ }
1050
+ if (parts[1] === 'in_progress') {
1051
+ return publicError(409, 'compact_interrupted', 'compaction was interrupted', false);
1052
+ }
1053
+ if (parts[1] !== 'completed' && parts[1] !== 'not_reduced' && parts[1] !== 'nothing_to_retry') {
1054
+ return publicError(500, 'compact_invalid_state', 'invalid compaction record state', false);
1055
+ }
1056
+ const outcome = parts[1];
1057
+ const tokensBefore = parts[2] ? Number(parts[2]) : undefined;
1058
+ const tokensAfter = parts[3] ? Number(parts[3]) : undefined;
1059
+ return {
1060
+ status: 200,
1061
+ body: {
1062
+ commandId: body.commandId,
1063
+ outcome,
1064
+ ...(tokensBefore !== undefined && !Number.isNaN(tokensBefore) ? { tokensBefore } : {}),
1065
+ ...(tokensAfter !== undefined && !Number.isNaN(tokensAfter) ? { tokensAfter } : {}),
1066
+ replayed: true,
1067
+ },
1068
+ };
1069
+ }
1070
+ if (lookup.record.turnId === 'nothing_to_retry') {
1071
+ return {
1072
+ status: 200,
1073
+ body: {
1074
+ commandId: body.commandId,
1075
+ outcome: 'nothing_to_retry',
1076
+ replayed: true,
1077
+ },
1078
+ };
1079
+ }
1080
+ if (lookup.record.result === 'failed') {
1081
+ return publicError(409, 'retry_failed', 'retry failed', false);
1082
+ }
1083
+ return {
1084
+ status: 200,
1085
+ body: {
1086
+ commandId: body.commandId,
1087
+ outcome: 'accepted',
1088
+ turnId: lookup.record.turnId,
1089
+ replayed: true,
1090
+ },
1091
+ };
1092
+ }
1093
+ }
1094
+ const persisted = this.#persisted.get(claims.sessionId);
1095
+ if (this.#config.persistence && !persisted) {
1096
+ return publicError(503, 'persistence_unavailable', 'gateway persistence unavailable', true);
1097
+ }
1098
+ const inflightPromise = (async () => {
1099
+ try {
1100
+ if (body.commandId === 'compact') {
1101
+ if (this.#admissions) {
1102
+ this.#admissions.prepare({
1103
+ ownerUserId: claims.sub,
1104
+ sessionId: session.sessionId,
1105
+ clientRequestId: body.clientRequestId,
1106
+ body,
1107
+ turnId: 'compact:in_progress',
1108
+ });
1109
+ this.#config.persistence?.index.updateAdmission(claims.sub, session.sessionId, body.clientRequestId, {
1110
+ state: 'accepted',
1111
+ result: 'accepted',
1112
+ phase: 'committed',
1113
+ turnId: 'compact:in_progress',
1114
+ });
1115
+ }
1116
+ // The outcome is mapped from the typed compaction result, never from
1117
+ // result text, so the response agrees with the journaled compaction
1118
+ // events: failed compactions return { outcome: 'failed', reason } with
1119
+ // a bounded reason code, and 'nothing_to_retry' is reserved for
1120
+ // genuinely nothing to compact (contract 13 §8).
1121
+ let outcome;
1122
+ let tokensBefore;
1123
+ let tokensAfter;
1124
+ let failureReason;
1125
+ const compactTurnId = crypto.randomUUID();
1126
+ try {
1127
+ const result = await session.withCommandTurn(compactTurnId, () => session.service.compactContextDetailed());
1128
+ switch (result.kind) {
1129
+ case 'compacted': {
1130
+ tokensBefore = result.tokensBefore;
1131
+ tokensAfter = result.tokensAfter;
1132
+ // A compaction that grew the context is not a completed one; the
1133
+ // numbers come from the same estimator on both sides.
1134
+ outcome =
1135
+ tokensBefore !== undefined && tokensAfter !== undefined && tokensAfter >= tokensBefore
1136
+ ? 'not_reduced'
1137
+ : 'completed';
1138
+ break;
1139
+ }
1140
+ case 'nothing_to_compact':
1141
+ outcome = 'nothing_to_retry';
1142
+ break;
1143
+ case 'failed':
1144
+ outcome = 'failed';
1145
+ failureReason = result.reason;
1146
+ break;
1147
+ }
1148
+ if (this.#admissions) {
1149
+ const turnId = outcome === 'failed'
1150
+ ? `compact:failed:${failureReason}`
1151
+ : `compact:${outcome}:${tokensBefore ?? ''}:${tokensAfter ?? ''}`;
1152
+ this.#config.persistence?.index.updateAdmission(claims.sub, session.sessionId, body.clientRequestId, {
1153
+ state: 'terminal',
1154
+ result: outcome === 'failed' ? 'failed' : 'accepted',
1155
+ phase: 'committed',
1156
+ turnId,
1157
+ });
1158
+ }
1159
+ }
1160
+ catch (error) {
1161
+ if (this.#admissions) {
1162
+ try {
1163
+ this.#config.persistence?.index.updateAdmission(claims.sub, session.sessionId, body.clientRequestId, {
1164
+ state: 'terminal',
1165
+ result: 'failed',
1166
+ phase: 'committed',
1167
+ turnId: 'compact:failed',
1168
+ });
1169
+ }
1170
+ catch {
1171
+ // ignore
1172
+ }
1173
+ }
1174
+ throw error;
1175
+ }
1176
+ return {
1177
+ status: 200,
1178
+ body: {
1179
+ commandId: 'compact',
1180
+ outcome,
1181
+ ...(outcome === 'failed' ? { reason: failureReason } : {}),
1182
+ ...(tokensBefore !== undefined ? { tokensBefore } : {}),
1183
+ ...(tokensAfter !== undefined ? { tokensAfter } : {}),
1184
+ },
1185
+ };
1186
+ }
1187
+ if (body.commandId === 'retry-tool') {
1188
+ const lastTool = session.service.peekLastToolOutput();
1189
+ if (!lastTool) {
1190
+ if (this.#admissions) {
1191
+ this.#admissions.prepare({
1192
+ ownerUserId: claims.sub,
1193
+ sessionId: session.sessionId,
1194
+ clientRequestId: body.clientRequestId,
1195
+ body,
1196
+ turnId: 'nothing_to_retry',
1197
+ });
1198
+ this.#config.persistence?.index.updateAdmission(claims.sub, session.sessionId, body.clientRequestId, {
1199
+ state: 'terminal',
1200
+ result: 'accepted',
1201
+ phase: 'committed',
1202
+ turnId: 'nothing_to_retry',
1203
+ });
1204
+ }
1205
+ return {
1206
+ status: 200,
1207
+ body: {
1208
+ commandId: 'retry-tool',
1209
+ outcome: 'nothing_to_retry',
1210
+ },
1211
+ };
1212
+ }
1213
+ const turnId = crypto.randomUUID();
1214
+ if (this.#admissions && persisted) {
1215
+ const runtime = session.createRetryRuntime('retry-tool');
1216
+ try {
1217
+ const result = await this.#admissions.admit({
1218
+ ownerUserId: claims.sub,
1219
+ sessionId: session.sessionId,
1220
+ clientRequestId: body.clientRequestId,
1221
+ body,
1222
+ turnId,
1223
+ runtime,
1224
+ persistence: persisted.persistence.critical,
1225
+ acceptedEvent: {
1226
+ sessionId: session.sessionId,
1227
+ type: 'assistant_started',
1228
+ payload: { turnId, clientRequestId: body.clientRequestId },
1229
+ },
1230
+ });
1231
+ if (result.kind === 'rejected') {
1232
+ return result.reason === 'queue_full'
1233
+ ? publicError(429, 'queue_full', 'message queue is full', true)
1234
+ : publicError(409, 'session_not_admitting', 'session is not admitting commands');
1235
+ }
1236
+ return {
1237
+ status: 200,
1238
+ body: {
1239
+ commandId: 'retry-tool',
1240
+ outcome: 'accepted',
1241
+ turnId: result.record.turnId,
1242
+ ...(result.replayed ? { replayed: true } : {}),
1243
+ },
1244
+ };
1245
+ }
1246
+ catch (error) {
1247
+ try {
1248
+ await this.#enqueueEventPersistence(session.sessionId, async () => {
1249
+ const events = persisted.persistence.journal.events();
1250
+ const hasStarted = events.some((e) => e.type === 'assistant_started' && e.payload?.turnId === turnId);
1251
+ const hasTerminal = events.some((e) => (e.type === 'turn_completed' || e.type === 'turn_failed' || e.type === 'turn_aborted') &&
1252
+ e.payload?.turnId === turnId);
1253
+ if (hasStarted && !hasTerminal) {
1254
+ await persisted.persistence.critical.appendJournalCritical({
1255
+ sessionId: session.sessionId,
1256
+ type: 'turn_failed',
1257
+ payload: { turnId, outcome: 'failed', reason: 'runtime_error' },
1258
+ });
1259
+ }
1260
+ });
1261
+ }
1262
+ catch {
1263
+ // ignore secondary journal error
1264
+ }
1265
+ return this.#mapError(error);
1266
+ }
1267
+ }
1268
+ void session.service.retryLastToolOutput({ preferredMessageId: turnId }).catch(() => undefined);
1269
+ return {
1270
+ status: 200,
1271
+ body: {
1272
+ commandId: 'retry-tool',
1273
+ outcome: 'accepted',
1274
+ turnId,
1275
+ },
1276
+ };
1277
+ }
1278
+ if (body.commandId === 'retry-turn') {
1279
+ const state = session.service.exportState();
1280
+ if (!hasFailedTurnToRetry(state.history)) {
1281
+ if (this.#admissions) {
1282
+ this.#admissions.prepare({
1283
+ ownerUserId: claims.sub,
1284
+ sessionId: session.sessionId,
1285
+ clientRequestId: body.clientRequestId,
1286
+ body,
1287
+ turnId: 'nothing_to_retry',
1288
+ });
1289
+ this.#config.persistence?.index.updateAdmission(claims.sub, session.sessionId, body.clientRequestId, {
1290
+ state: 'terminal',
1291
+ result: 'accepted',
1292
+ phase: 'committed',
1293
+ turnId: 'nothing_to_retry',
1294
+ });
1295
+ }
1296
+ return {
1297
+ status: 200,
1298
+ body: {
1299
+ commandId: 'retry-turn',
1300
+ outcome: 'nothing_to_retry',
1301
+ },
1302
+ };
1303
+ }
1304
+ const turnId = crypto.randomUUID();
1305
+ if (this.#admissions && persisted) {
1306
+ const runtime = session.createRetryRuntime('retry-turn');
1307
+ try {
1308
+ const result = await this.#admissions.admit({
1309
+ ownerUserId: claims.sub,
1310
+ sessionId: session.sessionId,
1311
+ clientRequestId: body.clientRequestId,
1312
+ body,
1313
+ turnId,
1314
+ runtime,
1315
+ persistence: persisted.persistence.critical,
1316
+ acceptedEvent: {
1317
+ sessionId: session.sessionId,
1318
+ type: 'assistant_started',
1319
+ payload: { turnId, clientRequestId: body.clientRequestId },
1320
+ },
1321
+ });
1322
+ if (result.kind === 'rejected') {
1323
+ return result.reason === 'queue_full'
1324
+ ? publicError(429, 'queue_full', 'message queue is full', true)
1325
+ : publicError(409, 'session_not_admitting', 'session is not admitting commands');
1326
+ }
1327
+ return {
1328
+ status: 200,
1329
+ body: {
1330
+ commandId: 'retry-turn',
1331
+ outcome: 'accepted',
1332
+ turnId: result.record.turnId,
1333
+ ...(result.replayed ? { replayed: true } : {}),
1334
+ },
1335
+ };
1336
+ }
1337
+ catch (error) {
1338
+ try {
1339
+ await this.#enqueueEventPersistence(session.sessionId, async () => {
1340
+ const events = persisted.persistence.journal.events();
1341
+ const hasStarted = events.some((e) => e.type === 'assistant_started' && e.payload?.turnId === turnId);
1342
+ const hasTerminal = events.some((e) => (e.type === 'turn_completed' || e.type === 'turn_failed' || e.type === 'turn_aborted') &&
1343
+ e.payload?.turnId === turnId);
1344
+ if (hasStarted && !hasTerminal) {
1345
+ await persisted.persistence.critical.appendJournalCritical({
1346
+ sessionId: session.sessionId,
1347
+ type: 'turn_failed',
1348
+ payload: { turnId, outcome: 'failed', reason: 'runtime_error' },
1349
+ });
1350
+ }
1351
+ });
1352
+ }
1353
+ catch {
1354
+ // ignore secondary journal error
1355
+ }
1356
+ return this.#mapError(error);
1357
+ }
1358
+ }
1359
+ void session.service.retryLastFailedTurn({ preferredMessageId: turnId }).catch(() => undefined);
1360
+ return {
1361
+ status: 200,
1362
+ body: {
1363
+ commandId: 'retry-turn',
1364
+ outcome: 'accepted',
1365
+ turnId,
1366
+ },
1367
+ };
1368
+ }
1369
+ return publicError(422, 'command_not_allowed', 'command is not allowed');
1370
+ }
1371
+ finally {
1372
+ this.#inflightCommands.delete(inflightKey);
1373
+ }
1374
+ })();
1375
+ this.#inflightCommands.set(inflightKey, { promise: inflightPromise, bodyHash });
1376
+ try {
1377
+ return await inflightPromise;
1378
+ }
1379
+ catch (error) {
1380
+ return this.#mapError(error);
1381
+ }
1382
+ }
853
1383
  async #resolveInteraction(claims, interactionId, body) {
854
1384
  if (!isInteractionResolveRequest(body))
855
1385
  return publicError(400, 'validation_error', 'interaction request is invalid');
@@ -857,6 +1387,62 @@ export class Term2Gateway {
857
1387
  const session = this.#sessions.get(claims.sessionId);
858
1388
  const binding = this.#interactionBindings.get(claims.sessionId);
859
1389
  const snapshot = session instanceof ServerSession ? session.service.getPendingInteractionSnapshot() : null;
1390
+ if (session instanceof ServerSession &&
1391
+ binding?.publicInteractionId === interactionId &&
1392
+ binding.owner !== 'root') {
1393
+ if (body.revision !== binding.revision)
1394
+ return publicError(409, 'stale_interaction', 'interaction is stale');
1395
+ let decision;
1396
+ try {
1397
+ decision = decideInteraction(binding.dto, body);
1398
+ }
1399
+ catch (error) {
1400
+ if (error instanceof InteractionProtocolError)
1401
+ return publicError(400, 'validation_error', 'interaction decision is invalid');
1402
+ throw error;
1403
+ }
1404
+ let accepted = false;
1405
+ if (binding.owner === 'background_approval' &&
1406
+ binding.backgroundEntry &&
1407
+ binding.backgroundRevision !== undefined) {
1408
+ const current = session.service.backgroundSubagentApprovals.getSnapshot();
1409
+ if (current.revision !== binding.backgroundRevision || current.current?.runId !== binding.backgroundEntry.runId)
1410
+ return publicError(409, 'stale_interaction', 'interaction is stale');
1411
+ const result = session.service.backgroundSubagentApprovals.resolve({
1412
+ revision: binding.backgroundRevision,
1413
+ entry: binding.backgroundEntry,
1414
+ decision: { answer: decision.answer, rejectionReason: decision.rejectionReason },
1415
+ });
1416
+ accepted = result.kind === 'resolved';
1417
+ }
1418
+ else if (binding.owner === 'background_question' && binding.childRunId && binding.childMessageId) {
1419
+ accepted = session.service.answerBackgroundSubagentQuestion(binding.childRunId, binding.childMessageId, decision.approvalAnswer ?? decision.answer);
1420
+ }
1421
+ if (!accepted)
1422
+ return publicError(409, 'stale_interaction', 'interaction is stale');
1423
+ this.#countInteraction('interaction_resolved');
1424
+ await this.#enqueueEventPersistence(claims.sessionId, async () => {
1425
+ const persisted = this.#persisted.get(claims.sessionId);
1426
+ if (!persisted)
1427
+ throw new GatewayPersistenceError('not_found', 'session persistence is unavailable');
1428
+ persisted.persistence.interactionCheckpoint.clear();
1429
+ await persisted.persistence.journal.append({
1430
+ sessionId: claims.sessionId,
1431
+ type: 'interaction_resolved',
1432
+ payload: {
1433
+ turnId: binding.turnId,
1434
+ interactionId,
1435
+ outcome: decision.outcome,
1436
+ variant: binding.variant,
1437
+ },
1438
+ }, { durability: 'critical' });
1439
+ });
1440
+ this.#interactionBindings.delete(claims.sessionId);
1441
+ return {
1442
+ status: 202,
1443
+ body: { sessionId: claims.sessionId, turnId: binding.turnId, interactionId, accepted: true },
1444
+ };
1445
+ }
860
1446
  // A live runtime owns the current interaction. A public ID from an older
861
1447
  // presentation is stale even when the durable journal still says the
862
1448
  // interaction was recovered during startup.
@@ -1153,13 +1739,32 @@ export class Term2Gateway {
1153
1739
  return;
1154
1740
  const session = this.#sessions.get(sessionId);
1155
1741
  const snapshot = session instanceof ServerSession ? session.service.getPendingInteractionSnapshot() : null;
1156
- const binding = event.type === 'approval_required' && snapshot
1157
- ? this.#interactionBindingFor(sessionId, snapshot, context.turnId)
1742
+ const childBinding = session instanceof ServerSession &&
1743
+ (event.type === 'subagent_approval_required' || event.type === 'subagent_question')
1744
+ ? this.#backgroundInteractionBinding(sessionId, context.turnId, event, session)
1158
1745
  : undefined;
1746
+ const binding = childBinding ??
1747
+ ((event.type === 'approval_required' || event.type === 'subagent_approval_required') && snapshot
1748
+ ? this.#interactionBindingFor(sessionId, snapshot, context.turnId)
1749
+ : undefined);
1750
+ if (childBinding) {
1751
+ persisted.persistence.interactionCheckpoint.save({
1752
+ turnId: childBinding.turnId,
1753
+ interaction: childBinding.dto,
1754
+ revision: childBinding.revision,
1755
+ generation: childBinding.continuationGeneration,
1756
+ settleOnRecovery: 'child_interrupted',
1757
+ ...(childBinding.childRunId
1758
+ ? { child: { agentId: childBinding.childRunId, role: childBinding.childRole ?? 'subagent' } }
1759
+ : {}),
1760
+ });
1761
+ }
1159
1762
  // An ask_user answer can advance the live binding before the original
1160
1763
  // awaitable approval sink reaches this queue. Do not republish that
1161
1764
  // old presentation as a second approval after interaction_updated.
1162
- if (event.type === 'approval_required' && binding?.revision && binding.revision > 1)
1765
+ if ((event.type === 'approval_required' || event.type === 'subagent_approval_required') &&
1766
+ binding?.revision &&
1767
+ binding.revision > 1)
1163
1768
  return;
1164
1769
  const mapped = mapConversationEvent(event, context.turnId, sessionId, binding, snapshot ?? undefined);
1165
1770
  if (!mapped)
@@ -1231,6 +1836,8 @@ export class Term2Gateway {
1231
1836
  return publicError(403, 'workspace_forbidden', 'workspace access denied');
1232
1837
  }
1233
1838
  if (error instanceof GatewayPersistenceError) {
1839
+ if (error.code === 'snapshot_unwritable')
1840
+ return publicError(503, 'snapshot_unwritable', 'session snapshot could not be persisted', true);
1234
1841
  if (error.code === 'not_found' || error.code === 'owner_mismatch')
1235
1842
  return publicError(404, 'not_found', 'session not found');
1236
1843
  if (error.code === 'cursor_invalid')
@@ -1244,6 +1851,15 @@ export class Term2Gateway {
1244
1851
  return publicError(409, 'stale_interaction', 'interaction is stale');
1245
1852
  return publicError(409, 'session_not_admitting', 'session is not admitting');
1246
1853
  }
1854
+ if (error instanceof RuntimeFactoryError) {
1855
+ if (error.code === 'resource_limit')
1856
+ return publicError(429, 'resource_exhausted', 'session resource limit reached', true);
1857
+ if (error.code === 'provider_unavailable')
1858
+ return publicError(409, 'provider_unavailable', 'session provider is unavailable');
1859
+ if (error.code === 'invalid_binding')
1860
+ return publicError(409, 'session_not_admitting', 'session is not admitting');
1861
+ return publicError(503, 'gateway_unavailable', 'gateway unavailable', true);
1862
+ }
1247
1863
  return publicError(503, 'gateway_unavailable', 'gateway unavailable', true);
1248
1864
  }
1249
1865
  static create(config) {
@@ -1286,64 +1902,348 @@ export class Term2Gateway {
1286
1902
  throw new GatewayStartupError();
1287
1903
  const binding = this.#admission.admit(claims);
1288
1904
  if (this.#config.runtimeFactory) {
1289
- let persisted;
1290
- try {
1291
- persisted = await this.#config.persistence?.open(binding);
1292
- const session = await this.#config.runtimeFactory.create(binding, {
1293
- eventSink: (event, context) => {
1294
- const persistence = this.#persistConversationEvent(binding.sessionId, event, context);
1295
- if (isCriticalRuntimeEvent(event))
1296
- return persistence;
1297
- // Streaming deltas remain observationally non-blocking, but their
1298
- // persistence failure must be consumed so it cannot become an
1299
- // unhandled rejection. A later critical event observes the
1300
- // journal's failure latch and is awaited by the runtime.
1301
- void persistence.catch(() => undefined);
1302
- return undefined;
1905
+ const { session } = await this.#composeRuntimeBackedSession(binding, {
1906
+ auditOperation: 'session_create',
1907
+ ownedBinding: true,
1908
+ correlationId,
1909
+ });
1910
+ return session;
1911
+ }
1912
+ return this.#composeLegacyFixtureSession(claims, options, binding);
1913
+ }
1914
+ /**
1915
+ * Compose a live runtime-backed session through the RuntimeFactory: open (or
1916
+ * reuse) the persistence handle, create the runtime, then attach the shared
1917
+ * gateway wiring (dispose hooks, lifecycle registration, event and
1918
+ * background-subagent sinks). Fresh sessions and restart-restored sessions
1919
+ * take the same path; a restored session additionally hydrates its history
1920
+ * from the persisted transcript before the runtime becomes reachable.
1921
+ */
1922
+ async #composeRuntimeBackedSession(binding, options) {
1923
+ const runtimeFactory = this.#config.runtimeFactory;
1924
+ if (!runtimeFactory)
1925
+ throw new GatewayStartupError();
1926
+ const authority = runtimeFactory.settingsAuthority;
1927
+ // A restored session runs on its persisted snapshot; a fresh session
1928
+ // captures the launcher's current one. Either way the factory receives an
1929
+ // explicit snapshot so it can never silently re-read a changed default.
1930
+ const runtimeSnapshot = options.snapshot ??
1931
+ (authority
1932
+ ? createSessionSettingsSnapshot({
1933
+ settings: authority,
1934
+ effectiveToolPolicy: {
1935
+ allowWrite: this.#config.runtimeFactory?.allowWriteEnabled === true && binding.access === 'read_write',
1303
1936
  },
1304
- });
1305
- if (persisted) {
1306
- this.#persisted.set(binding.sessionId, persisted);
1307
- session.addDisposeHook(async () => {
1308
- await persisted.persistence.close();
1309
- this.#persisted.delete(binding.sessionId);
1310
- this.#interactionBindings.delete(binding.sessionId);
1311
- await this.#config.persistence?.close(binding.sessionId, this.#shutdownInProgress ? 'interrupted' : 'closed');
1937
+ })
1938
+ : undefined);
1939
+ let persisted = this.#persisted.get(binding.sessionId);
1940
+ let createdSession;
1941
+ try {
1942
+ if (!persisted) {
1943
+ persisted = await this.#config.persistence?.open(binding);
1944
+ }
1945
+ const session = await runtimeFactory.create(binding, {
1946
+ settingsSnapshot: runtimeSnapshot,
1947
+ eventSink: (event, context) => {
1948
+ if (context.turnId)
1949
+ this.#lastTurnIds.set(binding.sessionId, context.turnId);
1950
+ if (event.type === 'subagent_started' && event.agentId && context.turnId)
1951
+ this.#childTurnIds.set(event.agentId, context.turnId);
1952
+ if (event.type === 'subagent_approval_required')
1953
+ this.#childRoles.set(event.agentId, event.role);
1954
+ const persistence = this.#persistConversationEvent(binding.sessionId, event, context);
1955
+ if (isCriticalRuntimeEvent(event))
1956
+ return persistence;
1957
+ // Streaming deltas remain observationally non-blocking, but their
1958
+ // persistence failure must be consumed so it cannot become an
1959
+ // unhandled rejection. A later critical event observes the
1960
+ // journal's failure latch and is awaited by the runtime.
1961
+ void persistence.catch(() => undefined);
1962
+ return undefined;
1963
+ },
1964
+ });
1965
+ createdSession = session;
1966
+ options.hydrate?.(session);
1967
+ if (runtimeSnapshot && !options.snapshot && persisted) {
1968
+ // Session-creation durability: a session must not become reachable
1969
+ // unless its provider/model identity is durably recorded, or a restart
1970
+ // could silently substitute the launcher's current default. Failure
1971
+ // rolls the whole composition back in the catch below.
1972
+ try {
1973
+ await writeSessionSnapshot(persisted.persistence.directory, {
1974
+ schemaVersion: 1,
1975
+ providerId: runtimeSnapshot.providerId,
1976
+ modelId: runtimeSnapshot.modelId,
1977
+ reasoningEffort: runtimeSnapshot.reasoningEffort,
1312
1978
  });
1313
1979
  }
1314
- const release = this.#lifecycle.registerWorker({ close: () => session.dispose() });
1315
- session.addDisposeHook(() => {
1316
- release();
1317
- this.#admission.remove(binding.sessionId);
1318
- this.#sessions.delete(binding.sessionId);
1980
+ catch {
1981
+ throw new GatewayPersistenceError('snapshot_unwritable', 'session provider/model snapshot could not be persisted');
1982
+ }
1983
+ }
1984
+ if (persisted) {
1985
+ this.#persisted.set(binding.sessionId, persisted);
1986
+ session.addDisposeHook(async () => {
1987
+ await persisted.persistence.close();
1319
1988
  this.#persisted.delete(binding.sessionId);
1320
1989
  this.#interactionBindings.delete(binding.sessionId);
1990
+ this.#lastTurnIds.delete(binding.sessionId);
1991
+ await this.#config.persistence?.close(binding.sessionId, this.#shutdownInProgress ? 'interrupted' : 'closed');
1321
1992
  });
1322
- this.#sessions.set(binding.sessionId, session);
1323
- await this.#audit.write(createSafeLogMetadata({
1324
- operation: 'session_create',
1325
- outcome: 'allowed',
1326
- reasonCode: 'accepted',
1327
- sessionId: binding.sessionId,
1328
- workspaceId: binding.workspaceId,
1329
- grantVersion: binding.grantVersion,
1330
- access: binding.access,
1331
- ...(correlationId ? { correlationId } : {}),
1332
- }));
1333
- return session;
1334
1993
  }
1335
- catch (error) {
1994
+ const release = this.#lifecycle.registerWorker({ close: () => session.dispose() });
1995
+ session.addDisposeHook(() => {
1996
+ release();
1997
+ this.#admission.remove(binding.sessionId);
1998
+ this.#sessions.delete(binding.sessionId);
1999
+ this.#persisted.delete(binding.sessionId);
2000
+ this.#interactionBindings.delete(binding.sessionId);
2001
+ });
2002
+ this.#sessions.set(binding.sessionId, session);
2003
+ session.service.setBackgroundSubagentEventSink((event) => {
2004
+ const agentId = event.type === 'subagent_question'
2005
+ ? event.runId
2006
+ : event.type === 'subagent_completed'
2007
+ ? event.result.agentId
2008
+ : event.agentId;
2009
+ if (event.type === 'subagent_started' && event.agentId && session.activeTurnId) {
2010
+ this.#childTurnIds.set(event.agentId, session.activeTurnId);
2011
+ this.#childRoles.set(event.agentId, event.role);
2012
+ }
2013
+ const turnId = agentId
2014
+ ? this.#childTurnIds.get(agentId) ?? session.activeTurnId ?? this.#lastTurnIds.get(binding.sessionId)
2015
+ : session.activeTurnId ?? this.#lastTurnIds.get(binding.sessionId);
2016
+ if (turnId)
2017
+ void this.#persistConversationEvent(binding.sessionId, event, { turnId, discardedTurnIds: [] });
2018
+ });
2019
+ session.service.backgroundSubagentApprovals.subscribe(() => {
2020
+ const current = session.service.backgroundSubagentApprovals.getSnapshot().current;
2021
+ if (!current)
2022
+ return;
2023
+ const turnId = this.#childTurnIds.get(current.runId);
2024
+ if (!turnId)
2025
+ return;
2026
+ void this.#persistConversationEvent(binding.sessionId, {
2027
+ type: 'subagent_approval_required',
2028
+ agentId: current.runId,
2029
+ role: this.#childRoles.get(current.runId) ?? 'subagent',
2030
+ }, { turnId, discardedTurnIds: [] });
2031
+ });
2032
+ await this.#audit.write(createSafeLogMetadata({
2033
+ operation: options.auditOperation,
2034
+ outcome: 'allowed',
2035
+ reasonCode: 'accepted',
2036
+ sessionId: binding.sessionId,
2037
+ workspaceId: binding.workspaceId,
2038
+ grantVersion: binding.grantVersion,
2039
+ access: binding.access,
2040
+ ...(options.correlationId ? { correlationId: options.correlationId } : {}),
2041
+ }));
2042
+ return { session, persisted };
2043
+ }
2044
+ catch (error) {
2045
+ // The runtime was created but the composition failed (snapshot
2046
+ // durability, hydration, ...): dispose it before the persistence it
2047
+ // may still write to.
2048
+ if (createdSession) {
2049
+ try {
2050
+ await createdSession.dispose();
2051
+ }
2052
+ catch {
2053
+ // Preserve the original composition failure.
2054
+ }
2055
+ }
2056
+ // Roll the persistence handle back whoever opened it — composition
2057
+ // opened it, or #ensurePersistedSession installed it before the failed
2058
+ // restore. A failed restore must not leave an open handle in #persisted;
2059
+ // the read path and the next revival reopen on demand.
2060
+ if (persisted) {
1336
2061
  try {
1337
- await persisted?.persistence.close();
2062
+ await persisted.persistence.close();
1338
2063
  await this.#config.persistence?.close(binding.sessionId, 'interrupted');
1339
2064
  }
1340
2065
  catch {
1341
- // Preserve the admission failure; persistence keeps its evidence.
2066
+ // Preserve the original composition failure; persistence keeps its evidence.
1342
2067
  }
2068
+ this.#persisted.delete(binding.sessionId);
2069
+ }
2070
+ if (options.ownedBinding)
1343
2071
  this.#admission.remove(binding.sessionId);
2072
+ throw error;
2073
+ }
2074
+ }
2075
+ /**
2076
+ * Lazily revive a persisted session whose runtime died with a previous
2077
+ * gateway process. Returns the live runtime, a specific gateway error for a
2078
+ * refused revival (revoked grant, unavailable provider or model, resource
2079
+ * limits), or null when there is nothing this gateway can restore — callers
2080
+ * keep their legacy not-found/not-admitting behavior for null. Concurrent
2081
+ * first revivals of one session share a single creation promise.
2082
+ */
2083
+ async #ensureLiveRuntime(ownerUserId, sessionId, correlationId) {
2084
+ const existing = this.#sessions.get(sessionId);
2085
+ const existingPersisted = this.#persisted.get(sessionId);
2086
+ if (existing instanceof ServerSession) {
2087
+ // Owner scoping holds on every return path, live or restored: a session
2088
+ // owned by someone else does not exist for this caller.
2089
+ if (existing.binding.ownerUserId !== ownerUserId)
2090
+ return publicError(404, 'not_found', 'session not found');
2091
+ if (existingPersisted)
2092
+ return { session: existing, persisted: existingPersisted };
2093
+ }
2094
+ if (!this.#config.runtimeFactory || !this.#config.persistence)
2095
+ return null;
2096
+ // A persisted session owned by another caller is a 404 for this caller,
2097
+ // not the legacy fallback: the index is owner-scoped for a reason. This
2098
+ // also stops a foreign caller from latching onto an in-flight revival.
2099
+ let foreign;
2100
+ try {
2101
+ foreign = this.#config.persistence.index.get(sessionId);
2102
+ }
2103
+ catch {
2104
+ foreign = null;
2105
+ }
2106
+ if (foreign && foreign.ownerUserId !== ownerUserId)
2107
+ return publicError(404, 'not_found', 'session not found');
2108
+ const restoreKey = `${ownerUserId}\u0000${sessionId}`;
2109
+ const guardOwner = (result) => {
2110
+ if (result && 'session' in result && result.session.binding.ownerUserId !== ownerUserId)
2111
+ return publicError(404, 'not_found', 'session not found');
2112
+ return result;
2113
+ };
2114
+ const inflight = this.#sessionRestores.get(restoreKey);
2115
+ if (inflight)
2116
+ return inflight.then(guardOwner);
2117
+ const restore = this.#restoreRuntime(ownerUserId, sessionId, correlationId);
2118
+ this.#sessionRestores.set(restoreKey, restore);
2119
+ restore.then(() => {
2120
+ if (this.#sessionRestores.get(restoreKey) === restore)
2121
+ this.#sessionRestores.delete(restoreKey);
2122
+ }, () => {
2123
+ // A rejected revival must clear the single-flight entry too, or the
2124
+ // session would stay wedged forever; everyone awaiting the shared
2125
+ // promise already holds the rejection, so swallow it here.
2126
+ if (this.#sessionRestores.get(restoreKey) === restore)
2127
+ this.#sessionRestores.delete(restoreKey);
2128
+ });
2129
+ return restore.then(guardOwner);
2130
+ }
2131
+ async #restoreRuntime(ownerUserId, sessionId, correlationId) {
2132
+ try {
2133
+ const index = this.#config.persistence.index;
2134
+ let record;
2135
+ try {
2136
+ record = index.getForOwner(ownerUserId, sessionId);
2137
+ }
2138
+ catch (error) {
2139
+ if (error instanceof GatewayPersistenceError && error.code === 'not_found')
2140
+ return null;
1344
2141
  throw error;
1345
2142
  }
2143
+ if (record.status === 'closed')
2144
+ return null;
2145
+ const grantVersion = Number(record.grantVersion);
2146
+ if (!Number.isSafeInteger(grantVersion))
2147
+ throw new GatewayPersistenceError('readonly', 'session grant version is invalid');
2148
+ // The admission layer is the grant authority: manifest and restored
2149
+ // dynamic grants, enabled state, and the workspace boundary probe all
2150
+ // revalidate before any runtime exists. A revoked grant fails here with
2151
+ // a typed admission error instead of a generic not-found.
2152
+ const binding = this.#admission.restore(sessionId, ownerUserId, record.workspaceId, grantVersion);
2153
+ const persisted = await this.#ensurePersistedSession(ownerUserId, sessionId);
2154
+ const snapshot = await this.#restoredSessionSnapshot(persisted, binding);
2155
+ if (snapshot && 'body' in snapshot)
2156
+ return snapshot;
2157
+ const unavailable = await this.#sessionSnapshotUnavailable(snapshot);
2158
+ if (unavailable)
2159
+ return unavailable;
2160
+ if (this.#lifecycle.state !== 'running')
2161
+ return publicError(503, 'gateway_unavailable', 'gateway is not accepting sessions', true);
2162
+ const directory = this.#config.persistence?.layout.existingSessionPath(binding.ownerUserId, binding.workspaceId, binding.sessionId);
2163
+ const composed = await this.#composeRuntimeBackedSession(binding, {
2164
+ auditOperation: 'session_resume',
2165
+ ownedBinding: false,
2166
+ correlationId,
2167
+ snapshot,
2168
+ hydrate: (session) => {
2169
+ if (directory)
2170
+ hydrateConversationService(session.service, directory, binding.sessionId);
2171
+ // Revival is complete: the coarse index lifecycle returns to the
2172
+ // resting state a fresh session holds; live turn state stays in the
2173
+ // ServerSession. The interrupted turn stays interrupted in the
2174
+ // journal; the next turn is a normal turn.
2175
+ this.#config.persistence?.index.update(binding.sessionId, {
2176
+ status: 'idle',
2177
+ interruptedAt: null,
2178
+ recoveryWarning: null,
2179
+ });
2180
+ },
2181
+ });
2182
+ return { session: composed.session, persisted };
2183
+ }
2184
+ catch (error) {
2185
+ return this.#mapError(error);
2186
+ }
2187
+ }
2188
+ /**
2189
+ * The snapshot a restored session must come back on: the per-session
2190
+ * provider/model recorded when the session was created. Sessions created
2191
+ * before that record existed (or whose record is unreadable) fall back to
2192
+ * the launcher's current snapshot; the fallback is documented and is still
2193
+ * validated below, so it is never a silent substitution of an unavailable
2194
+ * provider or model. Only the production stack (settings authority) has a
2195
+ * snapshot to restore against; fixture gateways compose without one.
2196
+ */
2197
+ async #restoredSessionSnapshot(persisted, binding) {
2198
+ const settings = this.#config.runtimeFactory?.settingsAuthority;
2199
+ if (!settings)
2200
+ return undefined;
2201
+ const stored = await readSessionSnapshot(persisted.persistence.directory);
2202
+ // Absent is the documented pre-change compatibility state; a record that
2203
+ // exists but cannot be trusted is corruption and is refused — falling
2204
+ // back would silently change the session's provider/model identity.
2205
+ if (stored.state === 'corrupt')
2206
+ return publicError(500, 'session_snapshot_invalid', 'persisted session snapshot is unreadable or malformed');
2207
+ const allowWrite = this.#config.runtimeFactory?.allowWriteEnabled === true;
2208
+ const effectiveAllowWrite = allowWrite && binding.access === 'read_write';
2209
+ if (stored.state === 'absent')
2210
+ return createSessionSettingsSnapshot({ settings, effectiveToolPolicy: { allowWrite: effectiveAllowWrite } });
2211
+ return createSessionSettingsSnapshot({
2212
+ settings,
2213
+ providerId: stored.snapshot.providerId,
2214
+ modelId: stored.snapshot.modelId,
2215
+ reasoningEffort: stored.snapshot.reasoningEffort,
2216
+ effectiveToolPolicy: { allowWrite: effectiveAllowWrite },
2217
+ });
2218
+ }
2219
+ /**
2220
+ * A restored session replays into the runtime built from its snapshot.
2221
+ * Refuse instead of silently substituting when that snapshot's provider or
2222
+ * model is no longer available. Only the production stack (settings
2223
+ * authority + model catalog) can answer this; fixture gateways skip the
2224
+ * check.
2225
+ */
2226
+ async #sessionSnapshotUnavailable(snapshot) {
2227
+ const settings = this.#config.runtimeFactory?.settingsAuthority;
2228
+ const catalog = this.#modelCatalog;
2229
+ if (!settings || !catalog || !snapshot)
2230
+ return null;
2231
+ const providerId = String(snapshot.providerId);
2232
+ if (!getAvailableProviderIds(settings, getProviderIds()).includes(providerId))
2233
+ return publicError(409, 'provider_unavailable', 'session provider is no longer available');
2234
+ let models;
2235
+ try {
2236
+ ({ models } = await catalog.load(providerId));
2237
+ }
2238
+ catch {
2239
+ return publicError(503, 'model_catalog_unavailable', 'model catalog unavailable', true);
1346
2240
  }
2241
+ if (!models.some((model) => model.id === snapshot.modelId))
2242
+ return publicError(409, 'model_unavailable', 'session model is no longer available');
2243
+ return null;
2244
+ }
2245
+ /** Legacy fixture composition without the production runtime factory. */
2246
+ async #composeLegacyFixtureSession(_claims, options, binding, correlationId) {
1347
2247
  const providerProbe = this.#config.providerProbe;
1348
2248
  if (!providerProbe)
1349
2249
  throw new GatewayStartupError();
@@ -1378,6 +2278,17 @@ export class Term2Gateway {
1378
2278
  }));
1379
2279
  return session;
1380
2280
  }
2281
+ /**
2282
+ * Shutdown must not fail because its audit record could not be built or written, but
2283
+ * losing an audit record is not silent: it goes to the launcher-supplied diagnostics
2284
+ * sink when the launcher configured one.
2285
+ */
2286
+ #reportShutdownAuditFailure(error) {
2287
+ this.#config.logger?.warn('gateway shutdown audit failed', {
2288
+ errorCode: 'shutdown_audit_failed',
2289
+ errorMessage: error instanceof Error ? error.message : String(error),
2290
+ });
2291
+ }
1381
2292
  async shutdown(graceMs = 5_000) {
1382
2293
  if (this.#shutdownPromise)
1383
2294
  return this.#shutdownPromise;
@@ -1413,14 +2324,36 @@ export class Term2Gateway {
1413
2324
  if (this.#config.runtimeFactory && !(await waitBounded(this.#config.runtimeFactory.shutdown(remaining()))))
1414
2325
  markForced();
1415
2326
  this.#sessions.clear();
2327
+ // Close persistence handles that outlived their session: read-path opens
2328
+ // and compositions that failed before installing a dispose hook would
2329
+ // otherwise keep file descriptors and locks past shutdown.
2330
+ for (const [sessionId, persisted] of this.#persisted) {
2331
+ try {
2332
+ await waitBounded(persisted.persistence.close());
2333
+ await waitBounded(this.#config.persistence?.close(sessionId, 'interrupted') ?? Promise.resolve());
2334
+ }
2335
+ catch {
2336
+ // Shutdown proceeds; the interrupted status is recorded in the index.
2337
+ }
2338
+ }
2339
+ this.#persisted.clear();
1416
2340
  this.#replay.close();
1417
- // The audit record is bounded by the same deadline. A broken audit sink
1418
- // must not keep the local owner process alive indefinitely.
1419
- await waitBounded(this.#audit.write(createSafeLogMetadata({
1420
- operation: 'shutdown',
1421
- outcome: forced ? 'interrupted' : 'allowed',
1422
- reasonCode: forced ? 'forced_shutdown' : 'shutdown',
1423
- })));
2341
+ // The audit record is bounded by the same deadline. A broken audit sink must not
2342
+ // keep the local owner process alive indefinitely, and neither a synchronous
2343
+ // build failure nor a rejected write may turn a finished shutdown into a
2344
+ // rejection for the caller: the audit is best-effort, but never silent.
2345
+ try {
2346
+ await waitBounded(this.#audit
2347
+ .write(createSafeLogMetadata({
2348
+ operation: 'shutdown',
2349
+ outcome: forced ? 'interrupted' : 'allowed',
2350
+ reasonCode: forced ? 'forced_shutdown' : 'shutdown',
2351
+ }))
2352
+ .catch((error) => this.#reportShutdownAuditFailure(error)));
2353
+ }
2354
+ catch (error) {
2355
+ this.#reportShutdownAuditFailure(error);
2356
+ }
1424
2357
  })();
1425
2358
  return this.#shutdownPromise;
1426
2359
  }
@@ -1461,7 +2394,7 @@ function matchPrivateRoute(method, pathname) {
1461
2394
  return { purpose: 'session_create' };
1462
2395
  if (method === 'GET' && pathname === '/private/agent/v1/sessions')
1463
2396
  return { purpose: 'session_list' };
1464
- const match = pathname.match(/^\/private\/agent\/v1\/sessions\/([A-Za-z0-9_-]{1,256})(?:\/(messages|abort|events|config|interactions\/([A-Za-z0-9_-]{1,256})))?$/);
2397
+ const match = pathname.match(/^\/private\/agent\/v1\/sessions\/([A-Za-z0-9_-]{1,256})(?:\/(messages|abort|events|config|commands|interactions\/([A-Za-z0-9_-]{1,256})))?$/);
1465
2398
  if (!match)
1466
2399
  return null;
1467
2400
  const sessionId = match[1];
@@ -1475,6 +2408,8 @@ function matchPrivateRoute(method, pathname) {
1475
2408
  return { purpose: 'events_connect', sessionId };
1476
2409
  if (method === 'POST' && match[2] === 'config')
1477
2410
  return { purpose: 'session_update', sessionId };
2411
+ if (method === 'POST' && match[2] === 'commands')
2412
+ return { purpose: 'command_invoke', sessionId };
1478
2413
  if (method === 'POST' && match[2]?.startsWith('interactions/'))
1479
2414
  return { purpose: 'interaction_resolve', sessionId, interactionId: match[3] };
1480
2415
  return null;
@@ -1672,6 +2607,17 @@ function isTurnBody(body) {
1672
2607
  Object.keys(body).length === 1 &&
1673
2608
  isOpaqueId(body.turnId));
1674
2609
  }
2610
+ const COMMAND_ALLOWLIST = new Set(['compact', 'retry-tool', 'retry-turn']);
2611
+ function isCommandBody(body) {
2612
+ if (!body || typeof body !== 'object' || Array.isArray(body))
2613
+ return false;
2614
+ const value = body;
2615
+ return (Object.keys(value).length === 2 &&
2616
+ typeof value.commandId === 'string' &&
2617
+ value.commandId.length > 0 &&
2618
+ value.commandId.length <= 256 &&
2619
+ isOpaqueId(value.clientRequestId));
2620
+ }
1675
2621
  export function isInteractionResolveRequest(body) {
1676
2622
  if (!body || typeof body !== 'object' || Array.isArray(body))
1677
2623
  return false;
@@ -1721,7 +2667,9 @@ function isPublicEventEnvelope(event) {
1721
2667
  try {
1722
2668
  if (event.type === 'approval_required' ||
1723
2669
  event.type === 'interaction_updated' ||
1724
- event.type === 'interaction_recovered') {
2670
+ event.type === 'interaction_recovered' ||
2671
+ event.type === 'subagent_approval_required' ||
2672
+ event.type === 'subagent_question') {
1725
2673
  if (!event.payload.interaction ||
1726
2674
  typeof event.payload.interaction !== 'object' ||
1727
2675
  !validatePendingInteractionDto(event.payload.interaction))
@@ -1758,6 +2706,98 @@ function isPublicEventEnvelope(event) {
1758
2706
  function boundedText(value, max = 8_192) {
1759
2707
  return typeof value === 'string' ? value.slice(0, max) : '';
1760
2708
  }
2709
+ function safeCounter(value) {
2710
+ return typeof value === 'number' && Number.isFinite(value) && value >= 0
2711
+ ? Math.min(Math.floor(value), Number.MAX_SAFE_INTEGER)
2712
+ : 0;
2713
+ }
2714
+ function boundedInteger(value, max) {
2715
+ const number = safeCounter(value);
2716
+ return Math.min(number, max);
2717
+ }
2718
+ function normalizedUsagePayload(usage) {
2719
+ const inputTokens = safeCounter(usage.prompt_tokens);
2720
+ const outputTokens = safeCounter(usage.completion_tokens);
2721
+ return { inputTokens, outputTokens, totalTokens: safeCounter(usage.total_tokens) || inputTokens + outputTokens };
2722
+ }
2723
+ function commandStatus(value) {
2724
+ switch (value) {
2725
+ case 'pending':
2726
+ case 'running':
2727
+ return value;
2728
+ case 'completed':
2729
+ return 'completed';
2730
+ case 'failed':
2731
+ return 'failed';
2732
+ case 'cancelled':
2733
+ case 'interrupted':
2734
+ case 'aborted':
2735
+ return 'aborted';
2736
+ case 'backgrounded':
2737
+ return 'running';
2738
+ default:
2739
+ return null;
2740
+ }
2741
+ }
2742
+ function commandMessagePayload(input) {
2743
+ const message = input;
2744
+ const callId = typeof message.callId === 'string' ? message.callId : '';
2745
+ const toolName = typeof message.toolName === 'string' ? message.toolName : '';
2746
+ const status = commandStatus(message.status);
2747
+ if (!callId || !toolName || !status) {
2748
+ malformedCommandMessageDrops = Math.min(malformedCommandMessageDrops + 1, 1_000_000);
2749
+ return null;
2750
+ }
2751
+ return {
2752
+ callId: boundedText(callId, 256),
2753
+ toolName: boundedText(toolName, 256),
2754
+ status,
2755
+ ...(typeof message.output === 'string' ? { output: boundedText(message.output, 16_384) } : {}),
2756
+ ...(typeof message.failureReason === 'string' ? { error: boundedText(message.failureReason, 10_000) } : {}),
2757
+ message: {
2758
+ id: boundedText(message.id, 256),
2759
+ role: 'command',
2760
+ text: boundedText(message.command, 16_384),
2761
+ },
2762
+ };
2763
+ }
2764
+ export function publicEventProjectionCounters() {
2765
+ return Object.freeze({ malformedCommandMessageDrops });
2766
+ }
2767
+ function commandMessageCandidate(base, message, type) {
2768
+ const projected = commandMessagePayload(message);
2769
+ if (!projected)
2770
+ return null;
2771
+ return {
2772
+ ...base,
2773
+ type,
2774
+ payload: { ...base.payload, ...projected },
2775
+ };
2776
+ }
2777
+ function errorReason(kind) {
2778
+ switch (kind) {
2779
+ case 'provider':
2780
+ return 'provider_error';
2781
+ case 'network':
2782
+ return 'network_error';
2783
+ case 'rate_limit':
2784
+ return 'rate_limit';
2785
+ case 'authentication':
2786
+ return 'authentication_error';
2787
+ case 'cancelled':
2788
+ return 'cancelled';
2789
+ case 'validation':
2790
+ return 'validation_error';
2791
+ case 'compaction':
2792
+ return 'compaction_failed';
2793
+ case 'retry_exhausted':
2794
+ return 'retry_exhausted';
2795
+ case 'interaction_continuation_failed':
2796
+ return 'interaction_continuation_failed';
2797
+ default:
2798
+ return 'runtime_error';
2799
+ }
2800
+ }
1761
2801
  function interactionDtoFromApproval(approval, interactionId, revision) {
1762
2802
  return projectPendingInteraction(approval, interactionId, revision);
1763
2803
  }
@@ -1873,9 +2913,19 @@ function hasTerminalTurnEvent(events, turnId) {
1873
2913
  }
1874
2914
  function terminalTranscriptFact(event, turnId) {
1875
2915
  if (event.type === 'final') {
2916
+ const finalText = boundedText(event.finalText, 16_384);
1876
2917
  const items = event.turnItems ? [...event.turnItems] : [];
1877
- if (!items.some((item) => item.type === 'assistant_text')) {
1878
- items.push({ type: 'assistant_text', text: boundedText(event.finalText, 16_384) });
2918
+ const existingText = items.findIndex((item) => item.type === 'assistant_text');
2919
+ if (existingText >= 0) {
2920
+ // Derived turn items may carry an empty assistant_text entry when the
2921
+ // stream produced no text parts; the terminal finalText is the
2922
+ // authoritative text for the persisted turn.
2923
+ const existing = items[existingText];
2924
+ if (!existing.text && finalText)
2925
+ items[existingText] = { ...existing, text: finalText };
2926
+ }
2927
+ else if (finalText) {
2928
+ items.push({ type: 'assistant_text', text: finalText });
1879
2929
  }
1880
2930
  return { type: 'assistant_turn', turnId, turn: { items } };
1881
2931
  }
@@ -1888,7 +2938,7 @@ function terminalTranscriptFact(event, turnId) {
1888
2938
  }
1889
2939
  return null;
1890
2940
  }
1891
- function mapConversationEvent(event, turnId, sessionId, interactionBinding, interactionSnapshot) {
2941
+ export function mapConversationEvent(event, turnId, sessionId, interactionBinding, interactionSnapshot) {
1892
2942
  const base = { sessionId, payload: { turnId } };
1893
2943
  switch (event.type) {
1894
2944
  // The gateway-owned assistant_started admission marker is appended before
@@ -1905,18 +2955,7 @@ function mapConversationEvent(event, turnId, sessionId, interactionBinding, inte
1905
2955
  payload: { turnId, callId: event.toolCallId, toolName: boundedText(event.toolName, 256) },
1906
2956
  };
1907
2957
  case 'command_message':
1908
- return {
1909
- ...base,
1910
- type: 'command_message',
1911
- payload: {
1912
- turnId,
1913
- message: {
1914
- id: boundedText(event.message.id, 256),
1915
- role: boundedText(event.message.sender ?? event.message.role, 32),
1916
- text: boundedText(event.message.text, 16_384),
1917
- },
1918
- },
1919
- };
2958
+ return commandMessageCandidate(base, event.message, 'command_message');
1920
2959
  case 'approval_required': {
1921
2960
  if (!interactionBinding)
1922
2961
  return null;
@@ -1931,13 +2970,208 @@ function mapConversationEvent(event, turnId, sessionId, interactionBinding, inte
1931
2970
  },
1932
2971
  };
1933
2972
  }
1934
- case 'usage_update':
2973
+ case 'usage_update': {
2974
+ const inputTokens = safeCounter(event.usage.prompt_tokens);
2975
+ const outputTokens = safeCounter(event.usage.completion_tokens);
2976
+ const totalTokens = safeCounter(event.usage.total_tokens) || inputTokens + outputTokens;
1935
2977
  return {
1936
2978
  ...base,
1937
2979
  type: 'usage_update',
1938
2980
  payload: {
1939
2981
  turnId,
1940
- usage: { inputTokens: event.usage.prompt_tokens, outputTokens: event.usage.completion_tokens },
2982
+ inputTokens,
2983
+ outputTokens,
2984
+ totalTokens,
2985
+ usage: { inputTokens, outputTokens, totalTokens },
2986
+ },
2987
+ };
2988
+ }
2989
+ case 'retry':
2990
+ return {
2991
+ ...base,
2992
+ type: 'retry',
2993
+ payload: {
2994
+ turnId,
2995
+ ...(event.agentId ? { agentId: boundedText(event.agentId, 256) } : {}),
2996
+ toolName: boundedText(event.toolName, 256),
2997
+ attempt: boundedInteger(event.attempt, 512),
2998
+ maxRetries: boundedInteger(event.maxRetries, 512),
2999
+ errorMessage: boundedText(event.errorMessage),
3000
+ ...(event.retryType ? { retryType: event.retryType } : {}),
3001
+ ...(event.errorKind ? { errorKind: event.errorKind } : {}),
3002
+ ...(event.delayMs !== undefined ? { delayMs: boundedInteger(event.delayMs, 86_400_000) } : {}),
3003
+ ...(event.retryAfterMs !== undefined ? { retryAfterMs: boundedInteger(event.retryAfterMs, 86_400_000) } : {}),
3004
+ },
3005
+ };
3006
+ case 'retry_exhausted':
3007
+ return {
3008
+ ...base,
3009
+ type: 'retry_exhausted',
3010
+ payload: {
3011
+ turnId,
3012
+ ...(event.provider ? { provider: boundedText(event.provider, 256) } : {}),
3013
+ errorKind: event.errorKind,
3014
+ attempts: boundedInteger(event.attempts, 512),
3015
+ maxAttempts: boundedInteger(event.maxAttempts, 512),
3016
+ message: boundedText(event.message),
3017
+ canRetry: event.canRetry,
3018
+ },
3019
+ };
3020
+ case 'subagent_started':
3021
+ return {
3022
+ ...base,
3023
+ type: 'subagent_started',
3024
+ payload: {
3025
+ turnId,
3026
+ agentId: boundedText(event.agentId, 256),
3027
+ ...(event.name ? { name: boundedText(event.name, 256) } : {}),
3028
+ role: boundedText(event.role, 256),
3029
+ task: boundedText(event.task, 16_384),
3030
+ ...(event.async !== undefined ? { async: event.async } : {}),
3031
+ },
3032
+ };
3033
+ case 'subagent_tool_started':
3034
+ return {
3035
+ ...base,
3036
+ type: 'subagent_tool_started',
3037
+ payload: {
3038
+ turnId,
3039
+ agentId: boundedText(event.agentId, 256),
3040
+ role: boundedText(event.role, 256),
3041
+ toolCallId: boundedText(event.toolCallId, 256),
3042
+ toolName: boundedText(event.toolName, 256),
3043
+ ...(event.commandMessages
3044
+ ? {
3045
+ commandMessages: event.commandMessages
3046
+ .slice(0, 64)
3047
+ .map((message) => commandMessagePayload(message))
3048
+ .filter(Boolean),
3049
+ }
3050
+ : {}),
3051
+ },
3052
+ };
3053
+ case 'subagent_text_turn':
3054
+ return {
3055
+ ...base,
3056
+ type: 'subagent_text_turn',
3057
+ payload: {
3058
+ turnId,
3059
+ agentId: boundedText(event.agentId, 256),
3060
+ role: boundedText(event.role, 256),
3061
+ text: boundedText(event.text, 16_384),
3062
+ },
3063
+ };
3064
+ case 'subagent_command_message':
3065
+ return commandMessageCandidate({
3066
+ ...base,
3067
+ type: 'subagent_command_message',
3068
+ payload: {
3069
+ turnId,
3070
+ agentId: boundedText(event.agentId, 256),
3071
+ role: boundedText(event.role, 256),
3072
+ },
3073
+ }, event.message, 'subagent_command_message');
3074
+ case 'subagent_approval_required':
3075
+ if (!interactionBinding)
3076
+ return null;
3077
+ return {
3078
+ ...base,
3079
+ type: 'subagent_approval_required',
3080
+ payload: {
3081
+ turnId,
3082
+ agentId: boundedText(event.agentId, 256),
3083
+ role: boundedText(event.role, 256),
3084
+ interaction: interactionBinding.dto,
3085
+ },
3086
+ };
3087
+ case 'subagent_completed': {
3088
+ const result = event.result;
3089
+ return {
3090
+ ...base,
3091
+ type: 'subagent_completed',
3092
+ payload: {
3093
+ turnId,
3094
+ agentId: boundedText(result.agentId, 256),
3095
+ ...(result.name ? { name: boundedText(result.name, 256) } : {}),
3096
+ role: boundedText(result.role, 256),
3097
+ status: result.status,
3098
+ finalText: boundedText(result.finalText, 16_384),
3099
+ ...(result.finalTextTruncated !== undefined ? { finalTextTruncated: result.finalTextTruncated } : {}),
3100
+ toolsUsed: result.toolsUsed.slice(0, 64).map((tool) => ({
3101
+ toolName: boundedText(tool.toolName, 256),
3102
+ count: boundedInteger(tool.count, 1_000_000),
3103
+ })),
3104
+ ...(result.usage ? { usage: normalizedUsagePayload(result.usage) } : {}),
3105
+ ...(result.error ? { error: boundedText(result.error) } : {}),
3106
+ ...(result.terminalCause ? { terminalCause: boundedText(String(result.terminalCause), 128) } : {}),
3107
+ ...(event.async !== undefined ? { async: event.async } : {}),
3108
+ },
3109
+ };
3110
+ }
3111
+ case 'subagent_interrupted':
3112
+ return {
3113
+ ...base,
3114
+ type: 'subagent_interrupted',
3115
+ payload: {
3116
+ turnId,
3117
+ agentId: boundedText(event.agentId, 256),
3118
+ role: boundedText(event.role, 256),
3119
+ finalText: boundedText(event.finalText, 16_384),
3120
+ },
3121
+ };
3122
+ case 'subagent_question':
3123
+ return {
3124
+ ...base,
3125
+ type: 'subagent_question',
3126
+ payload: {
3127
+ turnId,
3128
+ agentId: boundedText(event.runId, 256),
3129
+ runId: boundedText(event.runId, 256),
3130
+ messageId: boundedText(event.messageId, 256),
3131
+ role: boundedText(event.role, 256),
3132
+ ...(event.name ? { name: boundedText(event.name, 256) } : {}),
3133
+ question: boundedText(event.question),
3134
+ async: true,
3135
+ ...(interactionBinding ? { interaction: interactionBinding.dto } : {}),
3136
+ },
3137
+ };
3138
+ case 'context_compaction_started':
3139
+ return {
3140
+ ...base,
3141
+ type: 'context_compaction_started',
3142
+ payload: {
3143
+ turnId,
3144
+ provider: boundedText(event.provider, 256),
3145
+ sessionId: boundedText(event.sessionId, 256),
3146
+ ...(event.inputTokensBefore !== undefined ? { inputTokensBefore: safeCounter(event.inputTokensBefore) } : {}),
3147
+ ...(event.strategy ? { strategy: event.strategy } : {}),
3148
+ },
3149
+ };
3150
+ case 'context_compaction_completed':
3151
+ return {
3152
+ ...base,
3153
+ type: 'context_compaction_completed',
3154
+ payload: {
3155
+ turnId,
3156
+ provider: boundedText(event.provider, 256),
3157
+ sessionId: boundedText(event.sessionId, 256),
3158
+ ...(event.inputTokensBefore !== undefined ? { inputTokensBefore: safeCounter(event.inputTokensBefore) } : {}),
3159
+ ...(event.inputTokensAfter !== undefined ? { inputTokensAfter: safeCounter(event.inputTokensAfter) } : {}),
3160
+ durationMs: boundedInteger(event.durationMs, 86_400_000),
3161
+ ...(event.strategy ? { strategy: event.strategy } : {}),
3162
+ },
3163
+ };
3164
+ case 'context_compaction_failed':
3165
+ return {
3166
+ ...base,
3167
+ type: 'context_compaction_failed',
3168
+ payload: {
3169
+ turnId,
3170
+ provider: boundedText(event.provider, 256),
3171
+ sessionId: boundedText(event.sessionId, 256),
3172
+ errorCategory: event.errorCategory,
3173
+ durationMs: boundedInteger(event.durationMs, 86_400_000),
3174
+ ...(event.strategy ? { strategy: event.strategy } : {}),
1941
3175
  },
1942
3176
  };
1943
3177
  case 'final':
@@ -1947,7 +3181,16 @@ function mapConversationEvent(event, turnId, sessionId, interactionBinding, inte
1947
3181
  payload: { turnId, outcome: 'completed', text: boundedText(event.finalText, 16_384) },
1948
3182
  };
1949
3183
  case 'error':
1950
- return { ...base, type: 'turn_failed', payload: { turnId, outcome: 'failed', reason: 'runtime_error' } };
3184
+ return {
3185
+ ...base,
3186
+ type: 'turn_failed',
3187
+ payload: {
3188
+ turnId,
3189
+ outcome: 'failed',
3190
+ reason: errorReason(event.kind),
3191
+ ...(event.finalText ? { finalText: boundedText(event.finalText, 16_384) } : {}),
3192
+ },
3193
+ };
1951
3194
  default:
1952
3195
  return null;
1953
3196
  }