@gajae-code/coding-agent 0.10.2 → 0.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (476) hide show
  1. package/CHANGELOG.md +58 -2
  2. package/dist/types/append-only-mode.d.ts +1 -1
  3. package/dist/types/async/job-manager.d.ts +46 -7
  4. package/dist/types/cli/args.d.ts +4 -5
  5. package/dist/types/cli/daemon-cli.d.ts +12 -1
  6. package/dist/types/cli/notify-cli.d.ts +21 -1
  7. package/dist/types/cli.d.ts +3 -0
  8. package/dist/types/commands/daemon.d.ts +39 -0
  9. package/dist/types/commands/launch.d.ts +3 -0
  10. package/dist/types/commands/notify.d.ts +28 -1
  11. package/dist/types/commands/sdk.d.ts +33 -0
  12. package/dist/types/config/atomic-yaml-patch.d.ts +85 -0
  13. package/dist/types/config/model-profile-activation.d.ts +35 -15
  14. package/dist/types/config/model-profiles.d.ts +11 -5
  15. package/dist/types/config/model-registry.d.ts +3 -3
  16. package/dist/types/config/model-resolver.d.ts +39 -6
  17. package/dist/types/config/model-selector-value.d.ts +17 -0
  18. package/dist/types/config/models-config-schema.d.ts +10 -6
  19. package/dist/types/config/settings-schema.d.ts +133 -6
  20. package/dist/types/config/settings.d.ts +63 -35
  21. package/dist/types/coordinator-mcp/model-preset.d.ts +7 -6
  22. package/dist/types/coordinator-mcp/server.d.ts +6 -49
  23. package/dist/types/daemon/builtin.d.ts +6 -5
  24. package/dist/types/daemon/control-types.d.ts +1 -1
  25. package/dist/types/discovery/mcp-json.d.ts +12 -1
  26. package/dist/types/extensibility/custom-tools/types.d.ts +1 -1
  27. package/dist/types/extensibility/extensions/compact-handler.d.ts +4 -2
  28. package/dist/types/extensibility/extensions/get-commands-handler.d.ts +2 -2
  29. package/dist/types/extensibility/extensions/loader.d.ts +10 -1
  30. package/dist/types/extensibility/extensions/runner.d.ts +3 -3
  31. package/dist/types/extensibility/extensions/types.d.ts +141 -16
  32. package/dist/types/extensibility/hooks/types.d.ts +1 -1
  33. package/dist/types/gjc-runtime/launch-tmux.d.ts +3 -2
  34. package/dist/types/gjc-runtime/psmux-detect.d.ts +3 -0
  35. package/dist/types/gjc-runtime/session-state-sidecar.d.ts +3 -0
  36. package/dist/types/gjc-runtime/team-runtime.d.ts +3 -4
  37. package/dist/types/gjc-runtime/tmux-owner-isolation-cli.d.ts +13 -0
  38. package/dist/types/gjc-runtime/tmux-sessions.d.ts +4 -1
  39. package/dist/types/gjc-runtime/ultragoal-receipt-freshness.d.ts +11 -0
  40. package/dist/types/gjc-runtime/windows-powershell-command.d.ts +9 -0
  41. package/dist/types/harness-control-plane/operate.d.ts +5 -5
  42. package/dist/types/harness-control-plane/owner.d.ts +18 -4
  43. package/dist/types/harness-control-plane/receipts.d.ts +0 -1
  44. package/dist/types/harness-control-plane/sdk-transport.d.ts +43 -0
  45. package/dist/types/harness-control-plane/session-transport.d.ts +48 -0
  46. package/dist/types/harness-control-plane/types.d.ts +3 -4
  47. package/dist/types/lsp/index.d.ts +1 -1
  48. package/dist/types/main.d.ts +32 -38
  49. package/dist/types/modes/acp/acp-agent.d.ts +224 -53
  50. package/dist/types/modes/acp/acp-client-bridge.d.ts +1 -0
  51. package/dist/types/modes/acp/acp-mode.d.ts +6 -15
  52. package/dist/types/modes/acp/permission-mode.d.ts +4 -0
  53. package/dist/types/modes/acp/startup-options.d.ts +6 -0
  54. package/dist/types/modes/components/agent-dashboard.d.ts +3 -0
  55. package/dist/types/modes/components/model-selector.d.ts +1 -0
  56. package/dist/types/modes/components/notifications-settings-editor.d.ts +135 -0
  57. package/dist/types/modes/components/settings-selector.d.ts +6 -4
  58. package/dist/types/modes/components/welcome.d.ts +1 -1
  59. package/dist/types/modes/controllers/command-controller.d.ts +2 -2
  60. package/dist/types/modes/controllers/extension-ui-controller.d.ts +1 -0
  61. package/dist/types/modes/controllers/selector-controller.d.ts +35 -1
  62. package/dist/types/modes/index.d.ts +0 -4
  63. package/dist/types/modes/interactive-mode.d.ts +5 -2
  64. package/dist/types/modes/runtime-init.d.ts +2 -2
  65. package/dist/types/modes/shared/agent-wire/approval-gate.d.ts +6 -9
  66. package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +4 -6
  67. package/dist/types/modes/shared/agent-wire/event-contract.d.ts +6 -6
  68. package/dist/types/modes/shared/agent-wire/event-envelope.d.ts +4 -9
  69. package/dist/types/modes/shared/agent-wire/event-observation.d.ts +1 -1
  70. package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +170 -30
  71. package/dist/types/modes/shared/agent-wire/workflow-gate-schema.d.ts +7 -7
  72. package/dist/types/modes/shared/agent-wire/workflow-gate-types.d.ts +105 -0
  73. package/dist/types/modes/types.d.ts +7 -2
  74. package/dist/types/runtime-mcp/client.d.ts +2 -0
  75. package/dist/types/runtime-mcp/config.d.ts +5 -1
  76. package/dist/types/runtime-mcp/manager.d.ts +8 -1
  77. package/dist/types/runtime-mcp/types.d.ts +4 -0
  78. package/dist/types/sdk/acp/adapter.d.ts +67 -0
  79. package/dist/types/sdk/acp/index.d.ts +2 -0
  80. package/dist/types/sdk/broker/broker.d.ts +53 -0
  81. package/dist/types/sdk/broker/discovery.d.ts +30 -0
  82. package/dist/types/sdk/broker/ensure.d.ts +42 -0
  83. package/dist/types/sdk/broker/identity.d.ts +3 -0
  84. package/dist/types/sdk/broker/index.d.ts +8 -0
  85. package/dist/types/sdk/broker/lifecycle-ledger.d.ts +95 -0
  86. package/dist/types/sdk/broker/lifecycle.d.ts +85 -0
  87. package/dist/types/sdk/broker/process-incarnation.d.ts +14 -0
  88. package/dist/types/sdk/broker/runtime.d.ts +33 -0
  89. package/dist/types/sdk/broker/session-index.d.ts +57 -0
  90. package/dist/types/sdk/broker/state-version.d.ts +8 -0
  91. package/dist/types/sdk/broker/transport.d.ts +9 -0
  92. package/dist/types/sdk/bus/chat-command-policy.d.ts +49 -0
  93. package/dist/types/sdk/bus/chat-daemon-cli.d.ts +21 -0
  94. package/dist/types/sdk/bus/chat-daemon-control.d.ts +111 -0
  95. package/dist/types/sdk/bus/chat-daemon-runtime.d.ts +77 -0
  96. package/dist/types/sdk/bus/chat-effect-journal.d.ts +84 -0
  97. package/dist/types/{notifications → sdk/bus}/config-commands.d.ts +7 -2
  98. package/dist/types/sdk/bus/config.d.ts +274 -0
  99. package/dist/types/sdk/bus/conversation-store.d.ts +72 -0
  100. package/dist/types/sdk/bus/discord-conversation.d.ts +58 -0
  101. package/dist/types/sdk/bus/discord-daemon.d.ts +50 -0
  102. package/dist/types/sdk/bus/discord-live-provider.d.ts +70 -0
  103. package/dist/types/sdk/bus/discord-provider.d.ts +85 -0
  104. package/dist/types/{notifications → sdk/bus}/html-format.d.ts +2 -2
  105. package/dist/types/{notifications → sdk/bus}/index.d.ts +113 -13
  106. package/dist/types/{notifications → sdk/bus}/lifecycle-control-runtime.d.ts +4 -2
  107. package/dist/types/{notifications → sdk/bus}/lifecycle-orchestrator.d.ts +18 -21
  108. package/dist/types/{notifications → sdk/bus}/managed-daemon.d.ts +1 -1
  109. package/dist/types/sdk/bus/native-runtime-compatibility.d.ts +13 -0
  110. package/dist/types/sdk/bus/notification-orchestration.d.ts +170 -0
  111. package/dist/types/{notifications → sdk/bus}/notification-service.d.ts +7 -1
  112. package/dist/types/sdk/bus/session-control.d.ts +83 -0
  113. package/dist/types/sdk/bus/slack-conversation.d.ts +56 -0
  114. package/dist/types/sdk/bus/slack-daemon.d.ts +60 -0
  115. package/dist/types/sdk/bus/slack-live-provider.d.ts +59 -0
  116. package/dist/types/sdk/bus/slack-provider.d.ts +55 -0
  117. package/dist/types/{notifications → sdk/bus}/telegram-cli.d.ts +4 -4
  118. package/dist/types/{notifications → sdk/bus}/telegram-daemon-cli.d.ts +14 -5
  119. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +12 -0
  120. package/dist/types/{notifications → sdk/bus}/telegram-daemon-control.d.ts +2 -2
  121. package/dist/types/{notifications → sdk/bus}/telegram-daemon.d.ts +78 -17
  122. package/dist/types/{notifications → sdk/bus}/telegram-reference.d.ts +2 -2
  123. package/dist/types/sdk/bus/telegram-setup.d.ts +128 -0
  124. package/dist/types/{notifications → sdk/bus}/topic-registry.d.ts +11 -0
  125. package/dist/types/sdk/cli/index.d.ts +1 -0
  126. package/dist/types/sdk/cli/session-cli.d.ts +19 -0
  127. package/dist/types/sdk/client/client.d.ts +1 -0
  128. package/dist/types/sdk/client/discovery.d.ts +27 -0
  129. package/dist/types/sdk/client/index.d.ts +2 -0
  130. package/dist/types/sdk/host/control/dispatch.d.ts +34 -0
  131. package/dist/types/sdk/host/control/index.d.ts +2 -0
  132. package/dist/types/sdk/host/control/operations.d.ts +65 -0
  133. package/dist/types/sdk/host/events.d.ts +35 -0
  134. package/dist/types/sdk/host/host.d.ts +35 -0
  135. package/dist/types/sdk/host/index.d.ts +4 -0
  136. package/dist/types/sdk/host/query/cursor.d.ts +62 -0
  137. package/dist/types/sdk/host/query/handlers.d.ts +70 -0
  138. package/dist/types/sdk/host/query/index.d.ts +3 -0
  139. package/dist/types/sdk/host/query/revision-store.d.ts +60 -0
  140. package/dist/types/sdk/host/reverse-leases.d.ts +45 -0
  141. package/dist/types/sdk/host/types.d.ts +32 -0
  142. package/dist/types/sdk/index.d.ts +6 -0
  143. package/dist/types/sdk/lifecycle-session.d.ts +14 -0
  144. package/dist/types/sdk/mcp/index.d.ts +1 -0
  145. package/dist/types/sdk/mcp/server.d.ts +37 -0
  146. package/dist/types/sdk/models.d.ts +44 -0
  147. package/dist/types/sdk/protocol/adapter-validation.d.ts +14 -0
  148. package/dist/types/sdk/protocol/operation-registry.d.ts +24 -0
  149. package/dist/types/{sdk.d.ts → sdk/session.d.ts} +44 -38
  150. package/dist/types/sdk/startup-capability.d.ts +50 -0
  151. package/dist/types/session/agent-session.d.ts +114 -21
  152. package/dist/types/session/default-model-selection.d.ts +25 -0
  153. package/dist/types/session/fallback-chain-controller.d.ts +52 -0
  154. package/dist/types/session/session-manager.d.ts +58 -1
  155. package/dist/types/setup/credential-auto-import.d.ts +36 -6
  156. package/dist/types/tools/ask-answer-registry.d.ts +1 -1
  157. package/dist/types/tools/browser/benchmark/detector-report.d.ts +63 -0
  158. package/dist/types/tools/browser/benchmark/run-suite.d.ts +18 -0
  159. package/dist/types/tools/browser/launch.d.ts +22 -0
  160. package/dist/types/tools/browser/profile-discovery.d.ts +31 -0
  161. package/dist/types/tools/browser/profile-posture.d.ts +37 -0
  162. package/dist/types/tools/browser/profile-reuse.d.ts +39 -0
  163. package/dist/types/tools/browser/profile-warmup.d.ts +39 -0
  164. package/dist/types/tools/browser/registry.d.ts +19 -0
  165. package/dist/types/tools/browser/tab-protocol.d.ts +4 -0
  166. package/dist/types/tools/index.d.ts +8 -8
  167. package/dist/types/tools/subagent-render.d.ts +1 -0
  168. package/dist/types/tools/subagent.d.ts +6 -1
  169. package/package.json +34 -13
  170. package/scripts/build-sdk-package-smoke.ts +99 -0
  171. package/scripts/capture-notifications-settings-showcase.ts +435 -0
  172. package/scripts/compile-args.ts +4 -2
  173. package/scripts/g005-daemon-smoke.ts +4 -5
  174. package/scripts/g011-daemon-path-smoke.ts +5 -5
  175. package/scripts/generate-sdk-adapter-parity-manifest.ts +163 -0
  176. package/scripts/generate-sdk-operation-inventory.ts +867 -0
  177. package/scripts/generate-telegram-baseline-manifest.ts +128 -0
  178. package/scripts/manifest-schema.ts +124 -0
  179. package/scripts/run-test-manifest.ts +153 -0
  180. package/scripts/run-threat-model-check.ts +157 -0
  181. package/scripts/verify-gjc-sdk-canonicalization.ts +3467 -0
  182. package/scripts/verify-gjc-sdk-rename.ts +90 -0
  183. package/src/append-only-mode.ts +1 -1
  184. package/src/async/job-manager.ts +406 -63
  185. package/src/cli/args.ts +43 -12
  186. package/src/cli/auth-gateway-cli.ts +2 -2
  187. package/src/cli/config-cli.ts +2 -1
  188. package/src/cli/daemon-cli.ts +63 -14
  189. package/src/cli/list-models.ts +1 -1
  190. package/src/cli/notify-cli.ts +288 -286
  191. package/src/cli.ts +21 -2
  192. package/src/commands/daemon.ts +50 -6
  193. package/src/commands/harness.ts +93 -36
  194. package/src/commands/launch.ts +5 -2
  195. package/src/commands/mcp-serve.ts +19 -12
  196. package/src/commands/notify.ts +36 -3
  197. package/src/commands/sdk.ts +426 -0
  198. package/src/commit/agentic/agent.ts +1 -0
  199. package/src/config/atomic-yaml-patch.ts +423 -0
  200. package/src/config/model-profile-activation.ts +240 -89
  201. package/src/config/model-profiles.ts +18 -15
  202. package/src/config/model-registry.ts +51 -26
  203. package/src/config/model-resolver.ts +138 -51
  204. package/src/config/model-selector-value.ts +50 -0
  205. package/src/config/models-config-schema.ts +14 -21
  206. package/src/config/settings-schema.ts +128 -10
  207. package/src/config/settings.ts +1120 -158
  208. package/src/coordinator-mcp/model-preset.ts +74 -20
  209. package/src/coordinator-mcp/server.ts +1597 -3052
  210. package/src/daemon/builtin.ts +8 -5
  211. package/src/daemon/control-types.ts +1 -1
  212. package/src/defaults/gjc/skills/team/SKILL.md +4 -4
  213. package/src/discovery/mcp-json.ts +327 -20
  214. package/src/extensibility/custom-tools/types.ts +1 -1
  215. package/src/extensibility/extensions/compact-handler.ts +3 -3
  216. package/src/extensibility/extensions/get-commands-handler.ts +2 -2
  217. package/src/extensibility/extensions/loader.ts +69 -5
  218. package/src/extensibility/extensions/runner.ts +112 -4
  219. package/src/extensibility/extensions/types.ts +176 -17
  220. package/src/extensibility/hooks/types.ts +1 -1
  221. package/src/gjc-runtime/launch-tmux.ts +170 -86
  222. package/src/gjc-runtime/psmux-detect.ts +82 -4
  223. package/src/gjc-runtime/session-state-sidecar.ts +41 -31
  224. package/src/gjc-runtime/team-runtime.ts +54 -35
  225. package/src/gjc-runtime/tmux-owner-isolation-cli.ts +53 -17
  226. package/src/gjc-runtime/tmux-owner-isolation.ts +1 -1
  227. package/src/gjc-runtime/tmux-sessions.ts +19 -13
  228. package/src/gjc-runtime/ultragoal-guard.ts +108 -14
  229. package/src/gjc-runtime/ultragoal-receipt-freshness.ts +102 -1
  230. package/src/gjc-runtime/ultragoal-runtime.ts +227 -11
  231. package/src/gjc-runtime/windows-powershell-command.ts +58 -0
  232. package/src/harness-control-plane/operate.ts +9 -9
  233. package/src/harness-control-plane/owner.ts +283 -39
  234. package/src/harness-control-plane/receipts.ts +0 -1
  235. package/src/harness-control-plane/sdk-transport.ts +395 -0
  236. package/src/harness-control-plane/session-transport.ts +94 -0
  237. package/src/harness-control-plane/types.ts +1 -1
  238. package/src/internal-urls/docs-index.generated.ts +34 -35
  239. package/src/lsp/client.ts +42 -6
  240. package/src/lsp/index.ts +2 -2
  241. package/src/main.ts +299 -204
  242. package/src/modes/DESIGN.md +258 -0
  243. package/src/modes/acp/acp-agent.ts +1228 -2612
  244. package/src/modes/acp/acp-client-bridge.ts +2 -20
  245. package/src/modes/acp/acp-event-mapper.ts +18 -2
  246. package/src/modes/acp/acp-mode.ts +8 -36
  247. package/src/modes/acp/permission-mode.ts +25 -0
  248. package/src/modes/acp/startup-options.ts +6 -0
  249. package/src/modes/components/agent-dashboard.ts +37 -17
  250. package/src/modes/components/model-selector.ts +93 -35
  251. package/src/modes/components/notifications-settings-editor.ts +1379 -0
  252. package/src/modes/components/settings-selector.ts +60 -11
  253. package/src/modes/components/welcome.ts +4 -4
  254. package/src/modes/controllers/command-controller.ts +10 -17
  255. package/src/modes/controllers/event-controller.ts +11 -16
  256. package/src/modes/controllers/extension-ui-controller.ts +396 -28
  257. package/src/modes/controllers/selector-controller.ts +674 -36
  258. package/src/modes/index.ts +0 -22
  259. package/src/modes/interactive-mode.ts +87 -36
  260. package/src/modes/runtime-init.ts +298 -6
  261. package/src/modes/shared/agent-wire/approval-gate.ts +9 -11
  262. package/src/modes/shared/agent-wire/deep-interview-gate.ts +10 -11
  263. package/src/modes/shared/agent-wire/event-contract.ts +7 -8
  264. package/src/modes/shared/agent-wire/event-envelope.ts +9 -17
  265. package/src/modes/shared/agent-wire/event-observation.ts +16 -7
  266. package/src/modes/shared/agent-wire/workflow-gate-broker.ts +1258 -87
  267. package/src/modes/shared/agent-wire/workflow-gate-schema.ts +15 -15
  268. package/src/modes/shared/agent-wire/workflow-gate-types.ts +113 -0
  269. package/src/modes/types.ts +7 -2
  270. package/src/runtime-mcp/client.ts +94 -19
  271. package/src/runtime-mcp/config.ts +27 -15
  272. package/src/runtime-mcp/manager.ts +343 -105
  273. package/src/runtime-mcp/oauth-flow.ts +42 -12
  274. package/src/runtime-mcp/transports/http.ts +49 -19
  275. package/src/runtime-mcp/transports/stdio.ts +47 -28
  276. package/src/runtime-mcp/types.ts +10 -0
  277. package/src/sdk/acp/adapter.ts +418 -0
  278. package/src/sdk/acp/index.ts +2 -0
  279. package/src/sdk/broker/broker.ts +642 -0
  280. package/src/sdk/broker/discovery.ts +113 -0
  281. package/src/sdk/broker/ensure.ts +307 -0
  282. package/src/sdk/broker/identity.ts +42 -0
  283. package/src/sdk/broker/index.ts +8 -0
  284. package/src/sdk/broker/internal-source-marker-2178.txt +1 -0
  285. package/src/sdk/broker/internal-source.bunfig.toml +1 -0
  286. package/src/sdk/broker/lifecycle-ledger.ts +286 -0
  287. package/src/sdk/broker/lifecycle.ts +2157 -0
  288. package/src/sdk/broker/process-incarnation.ts +159 -0
  289. package/src/sdk/broker/runtime.ts +164 -0
  290. package/src/sdk/broker/session-index.ts +318 -0
  291. package/src/sdk/broker/state-version.ts +23 -0
  292. package/src/sdk/broker/transport.ts +125 -0
  293. package/src/sdk/bus/chat-command-policy.ts +107 -0
  294. package/src/sdk/bus/chat-daemon-cli.ts +214 -0
  295. package/src/sdk/bus/chat-daemon-control.ts +713 -0
  296. package/src/sdk/bus/chat-daemon-runtime.ts +526 -0
  297. package/src/sdk/bus/chat-effect-journal.ts +355 -0
  298. package/src/{notifications → sdk/bus}/config-commands.ts +39 -10
  299. package/src/sdk/bus/config.ts +433 -0
  300. package/src/sdk/bus/conversation-store.ts +391 -0
  301. package/src/sdk/bus/discord-conversation.ts +85 -0
  302. package/src/sdk/bus/discord-daemon.ts +2292 -0
  303. package/src/sdk/bus/discord-live-provider.ts +521 -0
  304. package/src/sdk/bus/discord-provider.ts +56 -0
  305. package/src/{notifications → sdk/bus}/html-format.ts +2 -2
  306. package/src/sdk/bus/index.ts +3979 -0
  307. package/src/{notifications → sdk/bus}/lifecycle-commands.ts +2 -0
  308. package/src/{notifications → sdk/bus}/lifecycle-control-runtime.ts +225 -196
  309. package/src/{notifications → sdk/bus}/lifecycle-orchestrator.ts +161 -74
  310. package/src/{notifications → sdk/bus}/managed-daemon.ts +2 -2
  311. package/src/sdk/bus/native-runtime-compatibility.ts +42 -0
  312. package/src/sdk/bus/natives-augment.d.ts +38 -0
  313. package/src/sdk/bus/notification-orchestration.ts +535 -0
  314. package/src/{notifications → sdk/bus}/notification-service.ts +106 -12
  315. package/src/sdk/bus/session-control.ts +333 -0
  316. package/src/sdk/bus/slack-conversation.ts +74 -0
  317. package/src/sdk/bus/slack-daemon.ts +2144 -0
  318. package/src/sdk/bus/slack-live-provider.ts +380 -0
  319. package/src/sdk/bus/slack-provider.ts +76 -0
  320. package/src/{notifications → sdk/bus}/telegram-cli.ts +8 -8
  321. package/src/sdk/bus/telegram-daemon-cli.ts +330 -0
  322. package/src/sdk/bus/telegram-daemon-contract.ts +14 -0
  323. package/src/{notifications → sdk/bus}/telegram-daemon-control.ts +4 -4
  324. package/src/{notifications → sdk/bus}/telegram-daemon.ts +603 -63
  325. package/src/{notifications → sdk/bus}/telegram-reference.ts +2 -2
  326. package/src/sdk/bus/telegram-setup.ts +663 -0
  327. package/src/{notifications → sdk/bus}/topic-registry.ts +32 -0
  328. package/src/sdk/cli/index.ts +1 -0
  329. package/src/sdk/cli/session-cli.ts +275 -0
  330. package/src/sdk/client/client.ts +1 -0
  331. package/src/sdk/client/discovery.ts +108 -0
  332. package/src/sdk/client/index.ts +2 -0
  333. package/src/sdk/host/control/dispatch.ts +367 -0
  334. package/src/sdk/host/control/index.ts +10 -0
  335. package/src/sdk/host/control/operations.ts +63 -0
  336. package/src/sdk/host/events.ts +91 -0
  337. package/src/sdk/host/host.ts +296 -0
  338. package/src/sdk/host/index.ts +4 -0
  339. package/src/sdk/host/query/cursor.ts +189 -0
  340. package/src/sdk/host/query/handlers.ts +533 -0
  341. package/src/sdk/host/query/index.ts +3 -0
  342. package/src/sdk/host/query/revision-store.ts +1126 -0
  343. package/src/sdk/host/reverse-leases.ts +325 -0
  344. package/src/sdk/host/types.ts +29 -0
  345. package/src/sdk/index.ts +13 -0
  346. package/src/sdk/lifecycle-session.ts +39 -0
  347. package/src/sdk/mcp/index.ts +1 -0
  348. package/src/sdk/mcp/server.ts +332 -0
  349. package/src/sdk/models.ts +168 -0
  350. package/src/sdk/protocol/adapter-validation.ts +48 -0
  351. package/src/sdk/protocol/operation-inventory.generated.json +5654 -0
  352. package/src/sdk/protocol/operation-registry.ts +312 -0
  353. package/src/{sdk.ts → sdk/session.ts} +339 -212
  354. package/src/sdk/startup-capability.ts +189 -0
  355. package/src/session/agent-session.ts +2650 -944
  356. package/src/session/default-model-selection.ts +93 -0
  357. package/src/session/fallback-chain-controller.ts +136 -0
  358. package/src/session/session-manager.ts +438 -11
  359. package/src/session/session-storage.ts +8 -1
  360. package/src/setup/credential-auto-import.ts +290 -35
  361. package/src/setup/hermes/templates/operator-instructions.v1.md +3 -5
  362. package/src/slash-commands/builtin-registry.ts +2 -1
  363. package/src/task/executor.ts +62 -13
  364. package/src/task/index.ts +26 -16
  365. package/src/tools/ask-answer-registry.ts +1 -1
  366. package/src/tools/ask.ts +13 -8
  367. package/src/tools/browser/STEALTH-THIRD-PARTY-NOTICES.md +26 -0
  368. package/src/tools/browser/benchmark/detector-report.ts +157 -0
  369. package/src/tools/browser/benchmark/run-suite.ts +39 -0
  370. package/src/tools/browser/launch.ts +124 -19
  371. package/src/tools/browser/profile-discovery.ts +63 -0
  372. package/src/tools/browser/profile-posture.ts +59 -0
  373. package/src/tools/browser/profile-reuse.ts +102 -0
  374. package/src/tools/browser/profile-warmup.ts +109 -0
  375. package/src/tools/browser/registry.ts +87 -11
  376. package/src/tools/browser/tab-protocol.ts +1 -0
  377. package/src/tools/browser/tab-supervisor.ts +1 -0
  378. package/src/tools/browser/tab-worker.ts +1 -1
  379. package/src/tools/browser.ts +6 -0
  380. package/src/tools/index.ts +8 -8
  381. package/src/tools/puppeteer/14_stealth_permissions.txt +57 -0
  382. package/src/tools/puppeteer/15_stealth_webrtc.txt +60 -0
  383. package/src/tools/subagent-render.ts +19 -8
  384. package/src/tools/subagent.ts +84 -32
  385. package/src/tools/telegram-send.ts +2 -2
  386. package/vendor/insane-search/MANIFEST.json +2 -1
  387. package/vendor/insane-search/engine/bias_check.py +4 -1
  388. package/vendor/insane-search/engine/phase0.py +26 -1
  389. package/dist/types/harness-control-plane/rpc-adapter.d.ts +0 -69
  390. package/dist/types/modes/bridge/auth.d.ts +0 -12
  391. package/dist/types/modes/bridge/bridge-client-bridge.d.ts +0 -9
  392. package/dist/types/modes/bridge/bridge-mode.d.ts +0 -47
  393. package/dist/types/modes/bridge/bridge-ui-context.d.ts +0 -88
  394. package/dist/types/modes/bridge/event-stream.d.ts +0 -8
  395. package/dist/types/modes/rpc/host-tools.d.ts +0 -1
  396. package/dist/types/modes/rpc/host-uris.d.ts +0 -1
  397. package/dist/types/modes/rpc/rpc-client.d.ts +0 -299
  398. package/dist/types/modes/rpc/rpc-mode.d.ts +0 -86
  399. package/dist/types/modes/rpc/rpc-socket-security.d.ts +0 -10
  400. package/dist/types/modes/rpc/rpc-types.d.ts +0 -810
  401. package/dist/types/modes/shared/agent-wire/command-contract.d.ts +0 -18
  402. package/dist/types/modes/shared/agent-wire/command-dispatch.d.ts +0 -37
  403. package/dist/types/modes/shared/agent-wire/command-validation.d.ts +0 -2
  404. package/dist/types/modes/shared/agent-wire/handshake.d.ts +0 -56
  405. package/dist/types/modes/shared/agent-wire/host-tool-bridge.d.ts +0 -16
  406. package/dist/types/modes/shared/agent-wire/host-uri-bridge.d.ts +0 -17
  407. package/dist/types/modes/shared/agent-wire/protocol.d.ts +0 -25
  408. package/dist/types/modes/shared/agent-wire/responses.d.ts +0 -4
  409. package/dist/types/modes/shared/agent-wire/scopes.d.ts +0 -18
  410. package/dist/types/modes/shared/agent-wire/session-registry.d.ts +0 -25
  411. package/dist/types/modes/shared/agent-wire/ui-request-broker.d.ts +0 -42
  412. package/dist/types/modes/shared/agent-wire/ui-result.d.ts +0 -27
  413. package/dist/types/modes/shared/agent-wire/unattended-action-policy.d.ts +0 -29
  414. package/dist/types/modes/shared/agent-wire/unattended-audit.d.ts +0 -68
  415. package/dist/types/modes/shared/agent-wire/unattended-run-controller.d.ts +0 -166
  416. package/dist/types/modes/shared/agent-wire/unattended-session.d.ts +0 -127
  417. package/dist/types/notifications/config.d.ts +0 -132
  418. package/src/harness-control-plane/rpc-adapter.ts +0 -290
  419. package/src/modes/bridge/auth.ts +0 -41
  420. package/src/modes/bridge/bridge-client-bridge.ts +0 -47
  421. package/src/modes/bridge/bridge-mode.ts +0 -675
  422. package/src/modes/bridge/bridge-ui-context.ts +0 -200
  423. package/src/modes/bridge/event-stream.ts +0 -70
  424. package/src/modes/rpc/host-tools.ts +0 -1
  425. package/src/modes/rpc/host-uris.ts +0 -1
  426. package/src/modes/rpc/rpc-client.ts +0 -1166
  427. package/src/modes/rpc/rpc-mode.ts +0 -1029
  428. package/src/modes/rpc/rpc-socket-security.ts +0 -115
  429. package/src/modes/rpc/rpc-types.ts +0 -637
  430. package/src/modes/shared/agent-wire/command-contract.ts +0 -18
  431. package/src/modes/shared/agent-wire/command-dispatch.ts +0 -498
  432. package/src/modes/shared/agent-wire/command-validation.ts +0 -181
  433. package/src/modes/shared/agent-wire/handshake.ts +0 -157
  434. package/src/modes/shared/agent-wire/host-tool-bridge.ts +0 -197
  435. package/src/modes/shared/agent-wire/host-uri-bridge.ts +0 -236
  436. package/src/modes/shared/agent-wire/protocol.ts +0 -46
  437. package/src/modes/shared/agent-wire/responses.ts +0 -17
  438. package/src/modes/shared/agent-wire/scopes.ts +0 -94
  439. package/src/modes/shared/agent-wire/session-registry.ts +0 -109
  440. package/src/modes/shared/agent-wire/ui-request-broker.ts +0 -150
  441. package/src/modes/shared/agent-wire/ui-result.ts +0 -48
  442. package/src/modes/shared/agent-wire/unattended-action-policy.ts +0 -365
  443. package/src/modes/shared/agent-wire/unattended-audit.ts +0 -176
  444. package/src/modes/shared/agent-wire/unattended-run-controller.ts +0 -426
  445. package/src/modes/shared/agent-wire/unattended-session.ts +0 -528
  446. package/src/notifications/config.ts +0 -219
  447. package/src/notifications/index.ts +0 -1835
  448. package/src/notifications/telegram-daemon-cli.ts +0 -243
  449. package/src/tools/puppeteer/09_stealth_locale.txt +0 -46
  450. /package/dist/types/{notifications → sdk/bus}/attachment-registry.d.ts +0 -0
  451. /package/dist/types/{notifications → sdk/bus}/chat-adapters.d.ts +0 -0
  452. /package/dist/types/{notifications → sdk/bus}/daemon-paths.d.ts +0 -0
  453. /package/dist/types/{notifications → sdk/bus}/engine.d.ts +0 -0
  454. /package/dist/types/{notifications → sdk/bus}/helpers.d.ts +0 -0
  455. /package/dist/types/{notifications → sdk/bus}/lifecycle-commands.d.ts +0 -0
  456. /package/dist/types/{notifications → sdk/bus}/operator-runtime.d.ts +0 -0
  457. /package/dist/types/{notifications → sdk/bus}/rate-limit-pool.d.ts +0 -0
  458. /package/dist/types/{notifications → sdk/bus}/recent-activity.d.ts +0 -0
  459. /package/dist/types/{notifications → sdk/bus}/reply-sent-store.d.ts +0 -0
  460. /package/dist/types/{notifications → sdk/bus}/rich-draft.d.ts +0 -0
  461. /package/dist/types/{notifications → sdk/bus}/rich-render.d.ts +0 -0
  462. /package/dist/types/{notifications → sdk/bus}/threaded-inbound.d.ts +0 -0
  463. /package/dist/types/{notifications → sdk/bus}/threaded-render.d.ts +0 -0
  464. /package/src/{notifications → sdk/bus}/attachment-registry.ts +0 -0
  465. /package/src/{notifications → sdk/bus}/chat-adapters.ts +0 -0
  466. /package/src/{notifications → sdk/bus}/daemon-paths.ts +0 -0
  467. /package/src/{notifications → sdk/bus}/engine.ts +0 -0
  468. /package/src/{notifications → sdk/bus}/helpers.ts +0 -0
  469. /package/src/{notifications → sdk/bus}/operator-runtime.ts +0 -0
  470. /package/src/{notifications → sdk/bus}/rate-limit-pool.ts +0 -0
  471. /package/src/{notifications → sdk/bus}/recent-activity.ts +0 -0
  472. /package/src/{notifications → sdk/bus}/reply-sent-store.ts +0 -0
  473. /package/src/{notifications → sdk/bus}/rich-draft.ts +0 -0
  474. /package/src/{notifications → sdk/bus}/rich-render.ts +0 -0
  475. /package/src/{notifications → sdk/bus}/threaded-inbound.ts +0 -0
  476. /package/src/{notifications → sdk/bus}/threaded-render.ts +0 -0
@@ -14,25 +14,27 @@
14
14
  import { closeSync, fsyncSync, mkdirSync, openSync, readFileSync, renameSync, writeFileSync } from "node:fs";
15
15
  import * as path from "node:path";
16
16
  import type { AskSelectedAckOutcome } from "../../../tools";
17
- import type {
18
- RpcWorkflowGate,
19
- RpcWorkflowGateContext,
20
- RpcWorkflowGateKind,
21
- RpcWorkflowGateOption,
22
- RpcWorkflowGateResolution,
23
- RpcWorkflowGateResponse,
24
- RpcWorkflowGateValidationError,
25
- RpcWorkflowStage,
26
- } from "../../rpc/rpc-types";
27
- import { RESERVED_WORKFLOW_STAGES } from "../../rpc/rpc-types";
28
17
  import { classifyAskGateDisposition } from "./deep-interview-gate";
29
18
  import { answerHashOf, canonicalJson, compileGateSchema, schemaHash, validateGateAnswer } from "./workflow-gate-schema";
19
+ import type {
20
+ WorkflowGate,
21
+ WorkflowGateContext,
22
+ WorkflowGateDiagnostic,
23
+ WorkflowGateKind,
24
+ WorkflowGateOption,
25
+ WorkflowGateQueryRecord,
26
+ WorkflowGateResolution,
27
+ WorkflowGateResponse,
28
+ WorkflowGateValidationError,
29
+ WorkflowStage,
30
+ } from "./workflow-gate-types";
31
+ import { RESERVED_WORKFLOW_STAGES } from "./workflow-gate-types";
30
32
 
31
33
  export type PersistedSemanticDisposition = "commit" | "resolve_without_commit";
32
34
  export type PersistedResolutionOrigin =
33
- | { kind: "generic"; channel: "rpc" | "bridge" | "other" }
34
- | { kind: "telegram_notification"; interactionActionId: string }
35
- | { kind: "legacy_unknown" };
35
+ | { kind: "generic"; channel: "sdk" | "other" }
36
+ | { kind: "telegram_notification"; interactionActionId: string };
37
+
36
38
  export type PersistedAckPolicy =
37
39
  | { kind: "none"; reason: "non_telegram" | "semantic_noncommit" | "legacy_unproven" }
38
40
  | {
@@ -51,35 +53,457 @@ export interface GateResolutionOptions {
51
53
  beforeAdvance?: () => Promise<void>;
52
54
  }
53
55
 
54
- const V1_STAGES: readonly RpcWorkflowStage[] = ["deep-interview", "ralplan", "ultragoal"];
56
+ export type WorkflowGateTerminalProof = "retired" | "already_terminal" | "not_published";
57
+
58
+ export interface WorkflowGateTerminalController {
59
+ completeGateInteractions(gateId: string): WorkflowGateTerminalProof | Promise<WorkflowGateTerminalProof>;
60
+ cancelGateInteractions(gateId: string, reason: string): void | Promise<void>;
61
+ }
62
+
63
+ export interface NotificationGateResolutionOptions {
64
+ interactionActionId: string;
65
+ replyReceiptId: string;
66
+ answerJson: string;
67
+ idempotencyKey?: string;
68
+ requestSelectedAck(input: {
69
+ replyReceiptId: string;
70
+ actionId: string;
71
+ commitKey: string;
72
+ daemonDeadlineAt: number;
73
+ hostTimeoutMs: number;
74
+ }): Promise<AskSelectedAckOutcome>;
75
+ resolveClaim(): void;
76
+ closeClaimInvalid(reason: string): void;
77
+ }
78
+
79
+ export interface AskSelectedAckRecoveryParticipant {
80
+ requestRecoveredAskSelectedAck(input: {
81
+ sessionId: string;
82
+ actionId: string;
83
+ commitKey: string;
84
+ deadlineAt: number;
85
+ hostTimeoutMs: number;
86
+ }): Promise<AskSelectedAckOutcome>;
87
+ }
88
+
89
+ /** SDK-native surface for emitting a workflow gate and awaiting its answer. */
90
+ export interface WorkflowGateEmitter {
91
+ isUnattended(): boolean;
92
+ emitGate(input: OpenGateInput): Promise<unknown>;
93
+ onGateEmitted?(listener: (gate: WorkflowGate) => void): () => void;
94
+ resolveGate?(response: WorkflowGateResponse): Promise<WorkflowGateResolution>;
95
+ resolveGateFromNotification?(
96
+ response: WorkflowGateResponse,
97
+ options: NotificationGateResolutionOptions,
98
+ ): Promise<WorkflowGateResolution>;
99
+ registerGateTerminalController?(controller: WorkflowGateTerminalController): () => void;
100
+ /** Records the exact terminalization proof established by direct control preparation. */
101
+ prepareTerminalization?(gateId: string, proof: WorkflowGateTerminalProof): boolean;
102
+ /** Discards a prepared proof when the gate remains pending after a rejected direct response. */
103
+ clearPreparedTerminalization?(gateId: string): void;
104
+ listPendingGates?(): WorkflowGate[];
105
+ listGateDiagnostics?(): WorkflowGateDiagnostic[];
106
+ listWorkflowGateQueryRecords?(): WorkflowGateQueryRecord[];
107
+ quarantineGate?(gateId: string): void;
108
+ setAckRecoveryParticipant?(participant: AskSelectedAckRecoveryParticipant | null): void;
109
+ /** Explicit host hook for same-process accepted-but-unadvanced recovery. */
110
+ recoverAcceptedGates?(): Promise<string[]>;
111
+ /** Permanently revoke all in-process continuation authority and reject their waiters. */
112
+ fence?(): void;
113
+ /** Temporarily make this emitter unavailable while a session transition is reversible. */
114
+ suspend?(): void;
115
+ resume?(): void;
116
+ }
117
+
118
+ /**
119
+ * SDK-native workflow-gate emitter backed by a durable broker/store pair.
120
+ *
121
+ * Answers are resolved by the broker, so validation and idempotency have one
122
+ * authority. Listeners replay durable pending gates when attached, allowing an
123
+ * SDK host that starts after a gate was opened to discover it.
124
+ */
125
+ export class BrokerWorkflowGateEmitter implements WorkflowGateEmitter {
126
+ readonly #listeners = new Set<(gate: WorkflowGate) => void>();
127
+ readonly #waiters = new Map<string, { resolve(answer: unknown): void; reject(error: Error): void }>();
128
+ readonly #broker: WorkflowGateBroker;
129
+ #terminalController: WorkflowGateTerminalController | undefined;
130
+ #recoveryParticipant: AskSelectedAckRecoveryParticipant | undefined;
131
+ #recoveryPromise: Promise<void> | undefined;
132
+ readonly #participantReady = Promise.withResolvers<void>();
133
+ #recoveryGraceTimer: NodeJS.Timeout | undefined;
134
+ #recoveryGraceWake: (() => void) | undefined;
135
+ #recoveryTimer: NodeJS.Timeout | undefined;
136
+ #recoveryAttempts = 0;
137
+ #fenced = false;
138
+ #suspended = false;
139
+ readonly #runId: string;
140
+ readonly #store: GateStore;
141
+ readonly #emitterHooks: Pick<BrokerHooks, "advance">;
142
+
143
+ constructor(runId: string, store: GateStore, emitterHooks: Pick<BrokerHooks, "advance"> = {}) {
144
+ this.#runId = runId;
145
+ this.#store = store;
146
+ this.#emitterHooks = emitterHooks;
147
+ this.#broker = new WorkflowGateBroker(runId, store, {
148
+ emit: gate => {
149
+ for (const listener of this.#listeners) listener(gate);
150
+ },
151
+ advance: (gate, answer) => this.#emitterHooks.advance?.(gate, answer),
152
+ completeAccepted: record => this.#completeAccepted(record),
153
+ finalizeAccepted: record => this.#finalizeAccepted(record),
154
+ terminalizeAccepted: record => this.#terminalizeAccepted(record),
155
+ });
156
+ }
157
+
158
+ isUnattended(): boolean {
159
+ return true;
160
+ }
161
+
162
+ emitGate(input: OpenGateInput): Promise<unknown> {
163
+ if (this.#fenced || this.#suspended) return Promise.reject(new Error("workflow gate emitter is unavailable"));
164
+ const waiter = Promise.withResolvers<unknown>();
165
+ const gate = this.#broker.openGate(input, {
166
+ activate: opened => this.#waiters.set(opened.gate_id, waiter),
167
+ isLive: gateId => this.#waiters.has(gateId),
168
+ release: gateId => {
169
+ const activeWaiter = this.#waiters.get(gateId);
170
+ if (!activeWaiter) return;
171
+ this.#waiters.delete(gateId);
172
+ activeWaiter.reject(new Error(`workflow gate ${gateId} continuation was fenced`));
173
+ },
174
+ });
175
+ // A listener may answer synchronously while the broker emits. The durable
176
+ // record is the source of truth in that race; never strand its promise.
177
+ const persisted = this.#store.get(gate.gate_id);
178
+ return persisted?.status === "accepted" ? Promise.resolve(persisted.answer) : waiter.promise;
179
+ }
180
+
181
+ onGateEmitted(listener: (gate: WorkflowGate) => void): () => void {
182
+ this.#listeners.add(listener);
183
+ for (const gate of this.listPendingGates()) listener(gate);
184
+ return () => this.#listeners.delete(listener);
185
+ }
186
+
187
+ resolveGate(response: WorkflowGateResponse): Promise<WorkflowGateResolution> {
188
+ if (this.#fenced || this.#suspended)
189
+ return Promise.reject(new WorkflowGateBrokerError("unknown_gate", `workflow gate emitter is unavailable`));
190
+ return this.#broker.resolve(response).catch(error => {
191
+ if (!this.#fenced && !this.#suspended) this.#scheduleRecovery();
192
+ throw error;
193
+ });
194
+ }
195
+ prepareTerminalization(gateId: string, proof: WorkflowGateTerminalProof): boolean {
196
+ return this.#broker.prepareTerminalization(gateId, proof);
197
+ }
198
+ clearPreparedTerminalization(gateId: string): void {
199
+ this.#broker.clearPreparedTerminalization(gateId);
200
+ }
201
+
202
+ listPendingGates(): WorkflowGate[] {
203
+ return this.#suspended ? [] : this.#broker.listPendingGates();
204
+ }
205
+
206
+ listGateDiagnostics(): WorkflowGateDiagnostic[] {
207
+ return this.#broker.listGateDiagnostics();
208
+ }
209
+
210
+ listWorkflowGateQueryRecords(): WorkflowGateQueryRecord[] {
211
+ return this.#broker.listWorkflowGateQueryRecords();
212
+ }
213
+
214
+ suspend(): void {
215
+ if (!this.#fenced) this.#suspended = true;
216
+ }
217
+
218
+ resume(): void {
219
+ if (!this.#fenced) this.#suspended = false;
220
+ }
221
+
222
+ fence(): void {
223
+ if (this.#fenced) return;
224
+ this.#fenced = true;
225
+ this.#suspended = false;
226
+ if (this.#recoveryTimer) clearTimeout(this.#recoveryTimer);
227
+ this.#recoveryTimer = undefined;
228
+ if (this.#recoveryGraceTimer) clearTimeout(this.#recoveryGraceTimer);
229
+ this.#recoveryGraceTimer = undefined;
230
+ this.#recoveryGraceWake?.();
231
+ this.#recoveryGraceWake = undefined;
232
+ for (const [gateId, waiter] of this.#waiters) {
233
+ this.#waiters.delete(gateId);
234
+ waiter.reject(new Error(`workflow gate ${gateId} continuation was fenced`));
235
+ }
236
+ this.#broker.fenceContinuations();
237
+ }
238
+
239
+ quarantineGate(gateId: string): void {
240
+ const waiter = this.#waiters.get(gateId);
241
+ if (waiter) {
242
+ this.#waiters.delete(gateId);
243
+ waiter.reject(new Error(`workflow gate ${gateId} continuation was fenced`));
244
+ }
245
+ this.#broker.quarantineGate(gateId);
246
+ }
247
+
248
+ registerGateTerminalController(controller: WorkflowGateTerminalController): () => void {
249
+ if (this.#terminalController && this.#terminalController !== controller)
250
+ throw new Error("a workflow gate terminal controller is already registered");
251
+ this.#terminalController = controller;
252
+ return () => {
253
+ if (this.#terminalController === controller) this.#terminalController = undefined;
254
+ };
255
+ }
256
+
257
+ setAckRecoveryParticipant(participant: AskSelectedAckRecoveryParticipant | null): void {
258
+ this.#recoveryParticipant = participant ?? undefined;
259
+ if (participant) this.#participantReady.resolve();
260
+ void this.#startRecoveryOnce();
261
+ }
262
+ async recoverAcceptedGates(): Promise<string[]> {
263
+ const recovered = await this.#broker.recover();
264
+ if (recovered.length > 0 || this.#broker.hasRecoverableAcceptedGate()) {
265
+ this.#scheduleRecovery();
266
+ } else {
267
+ this.#recoveryAttempts = 0;
268
+ }
269
+ return recovered;
270
+ }
271
+
272
+ async resolveGateFromNotification(
273
+ response: WorkflowGateResponse,
274
+ options: NotificationGateResolutionOptions,
275
+ ): Promise<WorkflowGateResolution> {
276
+ let claimSettled = false;
277
+ const resolveClaim = () => {
278
+ if (claimSettled) return;
279
+ claimSettled = true;
280
+ options.resolveClaim();
281
+ };
282
+ const closeClaimInvalid = (reason: string) => {
283
+ if (claimSettled) return;
284
+ claimSettled = true;
285
+ options.closeClaimInvalid(reason);
286
+ };
287
+ try {
288
+ JSON.parse(options.answerJson);
289
+ } catch {
290
+ closeClaimInvalid("invalid_structured_answer");
291
+ return {
292
+ gate_id: response.gate_id,
293
+ status: "rejected",
294
+ answer_hash: "",
295
+ resolved_at: new Date().toISOString(),
296
+ error: this.#broker.validationError(response.gate_id, "json_parse", "invalid structured answer"),
297
+ };
298
+ }
299
+ let semanticDisposition: PersistedSemanticDisposition;
300
+ try {
301
+ semanticDisposition = this.#broker.classifyDisposition(response.gate_id, response.answer);
302
+ } catch (error) {
303
+ const message = error instanceof Error ? error.message : "invalid answer";
304
+ closeClaimInvalid(message);
305
+ return {
306
+ gate_id: response.gate_id,
307
+ status: "rejected",
308
+ answer_hash: "",
309
+ resolved_at: new Date().toISOString(),
310
+ error: this.#broker.validationError(response.gate_id, "semantic_disposition", message),
311
+ };
312
+ }
313
+ const commitKey = `${response.gate_id}:${options.idempotencyKey ?? options.replyReceiptId}`;
314
+ const ackPolicy: PersistedAckPolicy =
315
+ semanticDisposition === "commit"
316
+ ? {
317
+ kind: "telegram_selected_v1",
318
+ commitKey,
319
+ actionId: options.interactionActionId,
320
+ state: "pending",
321
+ updatedAt: new Date().toISOString(),
322
+ }
323
+ : { kind: "none", reason: "semantic_noncommit" };
324
+ try {
325
+ const resolution = await this.#broker.resolve(response, {
326
+ resolutionOrigin: { kind: "telegram_notification", interactionActionId: options.interactionActionId },
327
+ ackPolicy,
328
+ semanticDisposition,
329
+ beforeAdvance: async () => {
330
+ if (ackPolicy.kind === "telegram_selected_v1") {
331
+ this.#broker.updateAckPolicy(response.gate_id, {
332
+ ...ackPolicy,
333
+ state: "attempt_started",
334
+ updatedAt: new Date().toISOString(),
335
+ });
336
+ let outcome: AskSelectedAckOutcome;
337
+ try {
338
+ outcome = await options.requestSelectedAck({
339
+ replyReceiptId: options.replyReceiptId,
340
+ actionId: options.interactionActionId,
341
+ commitKey,
342
+ daemonDeadlineAt: Date.now() + 8_000,
343
+ hostTimeoutMs: 10_000,
344
+ });
345
+ } catch {
346
+ outcome = { status: "unknown", reason: "host_timeout" };
347
+ }
348
+ this.#broker.updateAckPolicy(response.gate_id, {
349
+ ...ackPolicy,
350
+ state: outcome.status,
351
+ outcome,
352
+ updatedAt: new Date().toISOString(),
353
+ });
354
+ }
355
+ resolveClaim();
356
+ },
357
+ });
358
+ if (resolution.status === "accepted") resolveClaim();
359
+ else closeClaimInvalid(resolution.error?.code ?? "invalid_answer");
360
+ return resolution;
361
+ } catch (error) {
362
+ closeClaimInvalid(error instanceof Error ? error.message : "invalid_answer");
363
+ this.#scheduleRecovery();
364
+ throw error;
365
+ }
366
+ }
367
+
368
+ async #completeAccepted(record: PersistedGate): Promise<void> {
369
+ const waiter = this.#waiters.get(record.gate.gate_id);
370
+ if (!waiter) throw new Error(`workflow gate ${record.gate.gate_id} lost its continuation owner`);
371
+ waiter.resolve(record.answer);
372
+ }
373
+
374
+ async #terminalizeAccepted(record: PersistedGate): Promise<WorkflowGateTerminalProof> {
375
+ const controller = this.#terminalController;
376
+ if (!controller) {
377
+ this.quarantineGate(record.gate.gate_id);
378
+ throw new Error(`workflow gate ${record.gate.gate_id} has no terminal controller`);
379
+ }
380
+ try {
381
+ const proof = await controller.completeGateInteractions(record.gate.gate_id);
382
+ if (proof !== "retired" && proof !== "already_terminal" && proof !== "not_published")
383
+ throw new Error(`workflow gate ${record.gate.gate_id} terminalization returned no exact proof`);
384
+ return proof;
385
+ } catch (error) {
386
+ try {
387
+ await controller.cancelGateInteractions(
388
+ record.gate.gate_id,
389
+ "workflow gate presentation terminalization failed",
390
+ );
391
+ } finally {
392
+ this.quarantineGate(record.gate.gate_id);
393
+ }
394
+ throw error;
395
+ }
396
+ }
397
+
398
+ async #finalizeAccepted(record: PersistedGate): Promise<void> {
399
+ const policy = record.ackPolicy;
400
+ if (policy?.kind === "telegram_selected_v1" && policy.state === "pending") {
401
+ let outcome: AskSelectedAckOutcome;
402
+ const participant = this.#recoveryParticipant;
403
+ if (!participant) outcome = { status: "failed", reason: "no_participant" };
404
+ else {
405
+ this.#broker.updateAckPolicy(record.gate.gate_id, {
406
+ ...policy,
407
+ state: "attempt_started",
408
+ updatedAt: new Date().toISOString(),
409
+ });
410
+ try {
411
+ outcome = await participant.requestRecoveredAskSelectedAck({
412
+ sessionId: this.#runId,
413
+ actionId: policy.actionId,
414
+ commitKey: policy.commitKey,
415
+ deadlineAt: Date.now() + 8_000,
416
+ hostTimeoutMs: 10_000,
417
+ });
418
+ } catch {
419
+ outcome = { status: "unknown", reason: "host_timeout" };
420
+ }
421
+ }
422
+ this.#broker.updateAckPolicy(record.gate.gate_id, {
423
+ ...policy,
424
+ state: outcome.status,
425
+ outcome,
426
+ updatedAt: new Date().toISOString(),
427
+ });
428
+ }
429
+ if (policy?.kind === "telegram_selected_v1" && policy.state === "attempt_started") {
430
+ this.#broker.updateAckPolicy(record.gate.gate_id, {
431
+ ...policy,
432
+ state: "unknown",
433
+ outcome: { status: "unknown", reason: "shutdown" },
434
+ updatedAt: new Date().toISOString(),
435
+ });
436
+ }
437
+ }
438
+
439
+ #scheduleRecovery(): void {
440
+ if (this.#recoveryTimer || this.#recoveryAttempts >= 3) return;
441
+ const delay = 50 * 2 ** this.#recoveryAttempts++;
442
+ this.#recoveryTimer = setTimeout(() => {
443
+ this.#recoveryTimer = undefined;
444
+ void this.recoverAcceptedGates().catch(() => {
445
+ this.#scheduleRecovery();
446
+ });
447
+ }, delay);
448
+ }
449
+
450
+ async #startRecoveryOnce(options: { participantGraceMs?: number } = {}): Promise<void> {
451
+ if (this.#fenced) return;
452
+ if (this.#recoveryPromise) return this.#recoveryPromise;
453
+ this.#recoveryPromise = (async () => {
454
+ if (!this.#recoveryParticipant) {
455
+ const grace = Promise.withResolvers<void>();
456
+ this.#recoveryGraceWake = grace.resolve;
457
+ const graceMs = options.participantGraceMs ?? 2_000;
458
+ this.#recoveryGraceTimer = setTimeout(grace.resolve, graceMs);
459
+ await Promise.race([this.#participantReady.promise, grace.promise]);
460
+ if (this.#recoveryGraceTimer) clearTimeout(this.#recoveryGraceTimer);
461
+ this.#recoveryGraceTimer = undefined;
462
+ this.#recoveryGraceWake = undefined;
463
+ }
464
+ if (!this.#fenced) await this.recoverAcceptedGates();
465
+ })();
466
+ return this.#recoveryPromise;
467
+ }
468
+ }
469
+
470
+ const V1_STAGES: readonly WorkflowStage[] = ["deep-interview", "ralplan", "ultragoal"];
55
471
 
56
472
  export interface PersistedGate {
57
- gate: RpcWorkflowGate;
58
- status: "pending" | "accepted";
473
+ gate: WorkflowGate;
474
+ status: "pending" | "accepted" | "quarantined";
475
+ ownerInstanceId?: string;
59
476
  idempotencyKey?: string;
60
477
  responseHash?: string;
61
- /** Raw accepted answer, retained so a crashed advance can be replayed. */
478
+ /** Raw accepted answer, retained so a same-process advance can be replayed. */
62
479
  answer?: unknown;
63
- resolution?: RpcWorkflowGateResolution;
480
+ resolution?: WorkflowGateResolution;
64
481
  advanced: boolean;
482
+ /** Exact presentation terminalization completed before workflow advancement. */
483
+ terminalized?: boolean;
484
+ /** Exact presentation terminalization proof recorded before workflow advancement. */
485
+ terminalProof?: WorkflowGateTerminalProof;
65
486
  semanticDisposition?: PersistedSemanticDisposition;
66
487
  resolutionOrigin?: PersistedResolutionOrigin;
67
488
  ackPolicy?: PersistedAckPolicy;
489
+ lifecycle?: WorkflowGateDiagnostic["lifecycle"];
68
490
  }
69
491
 
70
492
  export interface GateStore {
71
- nextSeq(stage: RpcWorkflowStage): number;
493
+ nextSeq(stage: WorkflowStage): number;
72
494
  put(record: PersistedGate): void;
73
495
  get(gateId: string): PersistedGate | undefined;
74
- /** All persisted gate records (used for crash recovery). */
496
+ /** Atomically quarantines records owned by a prior runtime before exposure. */
497
+ beginRuntimeInstance(instanceId: string): void;
498
+ /** All persisted gate records (used for same-process recovery and Q12). */
75
499
  list(): PersistedGate[];
76
500
  }
77
501
 
78
502
  export class MemoryGateStore implements GateStore {
79
- private counters = new Map<RpcWorkflowStage, number>();
503
+ private counters = new Map<WorkflowStage, number>();
80
504
  private gates = new Map<string, PersistedGate>();
81
-
82
- nextSeq(stage: RpcWorkflowStage): number {
505
+ private runtimeInstanceId: string | undefined;
506
+ nextSeq(stage: WorkflowStage): number {
83
507
  const next = (this.counters.get(stage) ?? 0) + 1;
84
508
  this.counters.set(stage, next);
85
509
  return next;
@@ -91,21 +515,459 @@ export class MemoryGateStore implements GateStore {
91
515
  const r = this.gates.get(gateId);
92
516
  return r ? structuredClone(r) : undefined;
93
517
  }
518
+ beginRuntimeInstance(instanceId: string): void {
519
+ if (this.runtimeInstanceId === instanceId) return;
520
+ const priorInstanceId = this.runtimeInstanceId;
521
+ const now = new Date().toISOString();
522
+ for (const [gateId, record] of this.gates) {
523
+ if (record.ownerInstanceId !== priorInstanceId) continue;
524
+ if (record.status === "pending") {
525
+ this.gates.set(gateId, {
526
+ ...record,
527
+ status: "quarantined",
528
+ lifecycle: { state: "quarantined", reason: "orphaned_after_process_restart", quarantinedAt: now },
529
+ });
530
+ } else if (record.status === "accepted" && !record.advanced) {
531
+ this.gates.set(gateId, {
532
+ ...record,
533
+ status: "quarantined",
534
+ lifecycle: {
535
+ state: "quarantined",
536
+ reason: "accepted_unadvanced_after_process_restart",
537
+ quarantinedAt: now,
538
+ },
539
+ });
540
+ }
541
+ }
542
+ this.runtimeInstanceId = instanceId;
543
+ }
94
544
  list(): PersistedGate[] {
95
545
  return [...this.gates.values()].map(r => structuredClone(r));
96
546
  }
97
547
  }
98
548
 
99
549
  interface FileState {
100
- counters: Record<string, number>;
550
+ version: 1;
551
+ counters: Partial<Record<WorkflowStage, number>>;
101
552
  gates: Record<string, PersistedGate>;
553
+ runtimeInstanceId?: string;
554
+ }
555
+
556
+ const FILE_GATE_STORE_VERSION = 1;
557
+
558
+ function isObject(value: unknown): value is Record<string, unknown> {
559
+ return typeof value === "object" && value !== null && !Array.isArray(value);
560
+ }
561
+
562
+ function isNonEmptyString(value: unknown): value is string {
563
+ return typeof value === "string" && value.length > 0;
564
+ }
565
+
566
+ function isIsoDate(value: unknown): value is string {
567
+ return typeof value === "string" && Number.isFinite(Date.parse(value));
568
+ }
569
+
570
+ function invalidFileState(message: string): never {
571
+ throw new Error(`invalid gate store at ${message}`);
572
+ }
573
+
574
+ function migrateVersionlessV0SemanticDisposition(
575
+ value: unknown,
576
+ filePath: string,
577
+ ): PersistedSemanticDisposition | undefined {
578
+ if (value === undefined) return undefined;
579
+ if (value === "commit" || value === "resolve_without_commit") return value;
580
+ invalidFileState(filePath);
581
+ }
582
+
583
+ function migrateVersionlessV0ResolutionOrigin(value: unknown, filePath: string): PersistedResolutionOrigin | undefined {
584
+ if (value === undefined) return undefined;
585
+ if (!isObject(value)) invalidFileState(filePath);
586
+ if (value.kind === "telegram_notification" && isNonEmptyString(value.interactionActionId))
587
+ return { kind: "telegram_notification", interactionActionId: value.interactionActionId };
588
+ if (value.kind === "legacy_unknown") return { kind: "generic", channel: "other" };
589
+ if (value.kind !== "generic" || typeof value.channel !== "string") invalidFileState(filePath);
590
+ if (value.channel === "sdk" || value.channel === "other") return { kind: "generic", channel: value.channel };
591
+ if (value.channel === "rpc") return { kind: "generic", channel: "sdk" };
592
+ if (value.channel === "bridge") return { kind: "generic", channel: "other" };
593
+ invalidFileState(filePath);
594
+ }
595
+
596
+ function migrateVersionlessV0AckPolicy(value: unknown, filePath: string): PersistedAckPolicy | undefined {
597
+ if (value === undefined) return undefined;
598
+ if (!isObject(value)) invalidFileState(filePath);
599
+ if (
600
+ value.kind === "none" &&
601
+ (value.reason === "non_telegram" || value.reason === "semantic_noncommit" || value.reason === "legacy_unproven")
602
+ )
603
+ return { kind: "none", reason: value.reason };
604
+ if (
605
+ value.kind !== "telegram_selected_v1" ||
606
+ !isNonEmptyString(value.commitKey) ||
607
+ !isNonEmptyString(value.actionId) ||
608
+ !isIsoDate(value.updatedAt) ||
609
+ !(["pending", "attempt_started", "delivered", "failed", "unknown"] as const).includes(value.state as never)
610
+ )
611
+ invalidFileState(filePath);
612
+ if (value.outcome === undefined)
613
+ return {
614
+ kind: "telegram_selected_v1",
615
+ commitKey: value.commitKey,
616
+ actionId: value.actionId,
617
+ state: value.state as "pending" | "attempt_started" | "delivered" | "failed" | "unknown",
618
+ updatedAt: value.updatedAt,
619
+ };
620
+ if (!isObject(value.outcome)) invalidFileState(filePath);
621
+ if (
622
+ value.outcome.status === "delivered" &&
623
+ typeof value.outcome.messageId === "number" &&
624
+ Number.isSafeInteger(value.outcome.messageId)
625
+ )
626
+ return {
627
+ kind: "telegram_selected_v1",
628
+ commitKey: value.commitKey,
629
+ actionId: value.actionId,
630
+ state: value.state as "pending" | "attempt_started" | "delivered" | "failed" | "unknown",
631
+ outcome: { status: "delivered", messageId: value.outcome.messageId },
632
+ updatedAt: value.updatedAt,
633
+ };
634
+ if (
635
+ value.outcome.status === "failed" &&
636
+ (
637
+ [
638
+ "unsupported",
639
+ "no_participant",
640
+ "ambiguous_participant",
641
+ "route_missing",
642
+ "expired",
643
+ "cancelled",
644
+ "telegram_rejected",
645
+ "session_closed",
646
+ ] as const
647
+ ).includes(value.outcome.reason as never)
648
+ )
649
+ return {
650
+ kind: "telegram_selected_v1",
651
+ commitKey: value.commitKey,
652
+ actionId: value.actionId,
653
+ state: value.state as "pending" | "attempt_started" | "delivered" | "failed" | "unknown",
654
+ outcome: {
655
+ status: "failed",
656
+ reason: value.outcome.reason as
657
+ | "unsupported"
658
+ | "no_participant"
659
+ | "ambiguous_participant"
660
+ | "route_missing"
661
+ | "expired"
662
+ | "cancelled"
663
+ | "telegram_rejected"
664
+ | "session_closed",
665
+ },
666
+ updatedAt: value.updatedAt,
667
+ };
668
+ if (
669
+ value.outcome.status === "unknown" &&
670
+ (["transport_ambiguous", "origin_disconnected", "host_timeout", "shutdown"] as const).includes(
671
+ value.outcome.reason as never,
672
+ )
673
+ )
674
+ return {
675
+ kind: "telegram_selected_v1",
676
+ commitKey: value.commitKey,
677
+ actionId: value.actionId,
678
+ state: value.state as "pending" | "attempt_started" | "delivered" | "failed" | "unknown",
679
+ outcome: {
680
+ status: "unknown",
681
+ reason: value.outcome.reason as "transport_ambiguous" | "origin_disconnected" | "host_timeout" | "shutdown",
682
+ },
683
+ updatedAt: value.updatedAt,
684
+ };
685
+ invalidFileState(filePath);
686
+ }
687
+
688
+ function migrateVersionlessV0AcceptedRecord(legacy: Record<string, unknown>, filePath: string): PersistedGate {
689
+ if (typeof legacy.advanced !== "boolean") invalidFileState(filePath);
690
+ const semanticDisposition = migrateVersionlessV0SemanticDisposition(legacy.semanticDisposition, filePath);
691
+ const resolutionOrigin = migrateVersionlessV0ResolutionOrigin(legacy.resolutionOrigin, filePath);
692
+ const ackPolicy = migrateVersionlessV0AckPolicy(legacy.ackPolicy, filePath);
693
+ return {
694
+ ...(legacy as unknown as PersistedGate),
695
+ ...(semanticDisposition === undefined ? {} : { semanticDisposition }),
696
+ ...(resolutionOrigin === undefined ? {} : { resolutionOrigin }),
697
+ ...(ackPolicy === undefined ? {} : { ackPolicy }),
698
+ };
699
+ }
700
+
701
+ function migrateVersionlessV0(value: unknown, filePath: string): FileState {
702
+ if (!isObject(value) || Object.hasOwn(value, "version")) invalidFileState(filePath);
703
+ if (Object.keys(value).some(key => key !== "counters" && key !== "gates" && key !== "runtimeInstanceId"))
704
+ invalidFileState(filePath);
705
+ if (!isObject(value.counters) || !isObject(value.gates)) invalidFileState(filePath);
706
+ const gates: Record<string, PersistedGate> = {};
707
+ for (const [gateId, legacy] of Object.entries(value.gates)) {
708
+ if (!isObject(legacy) || !isObject(legacy.gate) || legacy.ownerInstanceId !== undefined)
709
+ invalidFileState(filePath);
710
+ const status = legacy.status ?? "pending";
711
+ if (status !== "pending" && status !== "accepted") invalidFileState(filePath);
712
+ if (status === "pending") {
713
+ gates[gateId] = {
714
+ gate: legacy.gate as unknown as WorkflowGate,
715
+ status: "quarantined",
716
+ ownerInstanceId: "legacy-v0",
717
+ advanced: false,
718
+ lifecycle: {
719
+ state: "quarantined",
720
+ reason: "orphaned_after_process_restart",
721
+ quarantinedAt: new Date().toISOString(),
722
+ },
723
+ };
724
+ continue;
725
+ }
726
+ const accepted = migrateVersionlessV0AcceptedRecord(legacy, filePath);
727
+ if (accepted.advanced) {
728
+ gates[gateId] = { ...accepted, ownerInstanceId: "legacy-v0" };
729
+ continue;
730
+ }
731
+ gates[gateId] = {
732
+ ...accepted,
733
+ status: "quarantined",
734
+ ownerInstanceId: "legacy-v0",
735
+ advanced: false,
736
+ lifecycle: {
737
+ state: "quarantined",
738
+ reason: "accepted_unadvanced_after_process_restart",
739
+ quarantinedAt: new Date().toISOString(),
740
+ },
741
+ };
742
+ }
743
+ const migrated: FileState = {
744
+ version: FILE_GATE_STORE_VERSION,
745
+ counters: value.counters as Partial<Record<WorkflowStage, number>>,
746
+ gates,
747
+ ...(value.runtimeInstanceId === undefined ? {} : { runtimeInstanceId: value.runtimeInstanceId as string }),
748
+ };
749
+ assertFileState(migrated, filePath);
750
+ return migrated;
751
+ }
752
+
753
+ function assertFileState(value: unknown, filePath: string): asserts value is FileState {
754
+ if (
755
+ !isObject(value) ||
756
+ value.version !== FILE_GATE_STORE_VERSION ||
757
+ !isObject(value.counters) ||
758
+ !isObject(value.gates)
759
+ )
760
+ invalidFileState(filePath);
761
+ for (const [stage, counter] of Object.entries(value.counters)) {
762
+ if (
763
+ !V1_STAGES.includes(stage as WorkflowStage) ||
764
+ typeof counter !== "number" ||
765
+ !Number.isSafeInteger(counter) ||
766
+ counter < 0
767
+ )
768
+ invalidFileState(filePath);
769
+ }
770
+ if (value.runtimeInstanceId !== undefined && !isNonEmptyString(value.runtimeInstanceId)) invalidFileState(filePath);
771
+ for (const [gateId, record] of Object.entries(value.gates))
772
+ assertPersistedGate(record, gateId, value.counters, filePath);
773
+ for (const [gateId, record] of Object.entries(value.gates)) {
774
+ if (
775
+ !isObject(record) ||
776
+ record.status !== "quarantined" ||
777
+ !isObject(record.lifecycle) ||
778
+ !isNonEmptyString(record.lifecycle.supersededByGateId)
779
+ )
780
+ continue;
781
+ if (
782
+ record.lifecycle.supersededByGateId === gateId ||
783
+ !Object.hasOwn(value.gates, record.lifecycle.supersededByGateId)
784
+ )
785
+ invalidFileState(filePath);
786
+ }
787
+ }
788
+
789
+ function assertPersistedGate(
790
+ value: unknown,
791
+ gateId: string,
792
+ counters: Record<string, unknown>,
793
+ filePath: string,
794
+ ): asserts value is PersistedGate {
795
+ if (
796
+ !isObject(value) ||
797
+ !isObject(value.gate) ||
798
+ value.gate.gate_id !== gateId ||
799
+ !isNonEmptyString(value.ownerInstanceId)
800
+ )
801
+ invalidFileState(filePath);
802
+ const gate = value.gate;
803
+ const stage = gate.stage as WorkflowStage;
804
+ const schema = gate.schema as WorkflowGate["schema"];
805
+ if (
806
+ gate.type !== "workflow_gate" ||
807
+ !isNonEmptyString(gate.gate_id) ||
808
+ !V1_STAGES.includes(stage) ||
809
+ !["question", "approval", "execution"].includes(gate.kind as string) ||
810
+ !isObject(gate.schema) ||
811
+ !isNonEmptyString(gate.schema_hash) ||
812
+ !isObject(gate.context) ||
813
+ !isIsoDate(gate.created_at) ||
814
+ gate.required !== true
815
+ )
816
+ invalidFileState(filePath);
817
+ const id = new RegExp(`^wg_[A-Za-z0-9]+_${gate.stage}_(\\d{6,})$`).exec(gate.gate_id);
818
+ if (
819
+ !id ||
820
+ typeof counters[stage] !== "number" ||
821
+ Number(id[1]) > counters[stage] ||
822
+ gate.schema_hash !== schemaHash(schema)
823
+ )
824
+ invalidFileState(filePath);
825
+ try {
826
+ compileGateSchema(schema);
827
+ } catch {
828
+ invalidFileState(filePath);
829
+ }
830
+ if (
831
+ gate.options !== undefined &&
832
+ (!Array.isArray(gate.options) ||
833
+ gate.options.some(option => !isObject(option) || !isNonEmptyString(option.label)))
834
+ )
835
+ invalidFileState(filePath);
836
+ if (value.status === "pending") {
837
+ if (
838
+ value.advanced !== false ||
839
+ value.terminalized !== undefined ||
840
+ value.terminalProof !== undefined ||
841
+ value.lifecycle !== undefined ||
842
+ value.resolution !== undefined ||
843
+ value.answer !== undefined ||
844
+ value.idempotencyKey !== undefined ||
845
+ value.responseHash !== undefined ||
846
+ value.semanticDisposition !== undefined ||
847
+ value.resolutionOrigin !== undefined ||
848
+ value.ackPolicy !== undefined
849
+ )
850
+ invalidFileState(filePath);
851
+ return;
852
+ }
853
+ if (value.status === "accepted") {
854
+ if (
855
+ typeof value.advanced !== "boolean" ||
856
+ (value.terminalized !== undefined && typeof value.terminalized !== "boolean") ||
857
+ (value.terminalProof !== undefined &&
858
+ value.terminalProof !== "retired" &&
859
+ value.terminalProof !== "already_terminal" &&
860
+ value.terminalProof !== "not_published") ||
861
+ (value.terminalized === true && value.terminalProof === undefined) ||
862
+ (value.terminalized !== true && value.terminalProof !== undefined) ||
863
+ !Object.hasOwn(value, "answer") ||
864
+ (value.idempotencyKey !== undefined && !isNonEmptyString(value.idempotencyKey)) ||
865
+ !isNonEmptyString(value.responseHash) ||
866
+ value.responseHash !== answerHashOf({ gate_id: gateId, answer: value.answer }) ||
867
+ !isObject(value.resolution) ||
868
+ value.resolution.gate_id !== gateId ||
869
+ value.resolution.status !== "accepted" ||
870
+ value.resolution.answer_hash !== answerHashOf(value.answer) ||
871
+ !isIsoDate(value.resolution.resolved_at) ||
872
+ value.resolution.error !== undefined ||
873
+ value.lifecycle !== undefined
874
+ )
875
+ invalidFileState(filePath);
876
+ return;
877
+ }
878
+ if (
879
+ value.status !== "quarantined" ||
880
+ value.advanced !== false ||
881
+ !isObject(value.lifecycle) ||
882
+ Object.keys(value.lifecycle).some(
883
+ key => key !== "state" && key !== "reason" && key !== "quarantinedAt" && key !== "supersededByGateId",
884
+ ) ||
885
+ value.lifecycle.state !== "quarantined" ||
886
+ ![
887
+ "orphaned_after_process_restart",
888
+ "accepted_unadvanced_after_process_restart",
889
+ "continuation_owner_lost",
890
+ "opened_without_continuation",
891
+ "finalization_failed",
892
+ "advance_failed",
893
+ ].includes(value.lifecycle.reason as string) ||
894
+ !isIsoDate(value.lifecycle.quarantinedAt) ||
895
+ (value.lifecycle.supersededByGateId !== undefined && !isNonEmptyString(value.lifecycle.supersededByGateId))
896
+ )
897
+ invalidFileState(filePath);
898
+ const hasAcceptedState =
899
+ Object.hasOwn(value, "answer") ||
900
+ value.terminalized !== undefined ||
901
+ value.terminalProof !== undefined ||
902
+ value.idempotencyKey !== undefined ||
903
+ value.responseHash !== undefined ||
904
+ value.resolution !== undefined ||
905
+ value.semanticDisposition !== undefined ||
906
+ value.resolutionOrigin !== undefined ||
907
+ value.ackPolicy !== undefined;
908
+ if (value.lifecycle.reason === "accepted_unadvanced_after_process_restart" || hasAcceptedState) {
909
+ if (
910
+ !Object.hasOwn(value, "answer") ||
911
+ (value.terminalized !== undefined && typeof value.terminalized !== "boolean") ||
912
+ (value.terminalProof !== undefined &&
913
+ value.terminalProof !== "retired" &&
914
+ value.terminalProof !== "already_terminal" &&
915
+ value.terminalProof !== "not_published") ||
916
+ (value.terminalized === true && value.terminalProof === undefined) ||
917
+ (value.terminalized !== true && value.terminalProof !== undefined) ||
918
+ (value.idempotencyKey !== undefined && !isNonEmptyString(value.idempotencyKey)) ||
919
+ !isNonEmptyString(value.responseHash) ||
920
+ value.responseHash !== answerHashOf({ gate_id: gateId, answer: value.answer }) ||
921
+ !isObject(value.resolution) ||
922
+ value.resolution.gate_id !== gateId ||
923
+ value.resolution.status !== "accepted" ||
924
+ value.resolution.answer_hash !== answerHashOf(value.answer) ||
925
+ !isIsoDate(value.resolution.resolved_at) ||
926
+ value.resolution.error !== undefined
927
+ )
928
+ invalidFileState(filePath);
929
+ return;
930
+ }
931
+ if (
932
+ value.lifecycle.reason === "accepted_unadvanced_after_process_restart" ||
933
+ value.lifecycle.reason === "finalization_failed" ||
934
+ value.lifecycle.reason === "advance_failed"
935
+ )
936
+ invalidFileState(filePath);
937
+ }
938
+
939
+ export function isUnsupportedWindowsDirectorySyncError(
940
+ error: unknown,
941
+ platform: NodeJS.Platform = process.platform,
942
+ ): boolean {
943
+ if (platform !== "win32") return false;
944
+ const code = (error as NodeJS.ErrnoException | undefined)?.code;
945
+ // Bun reports EPERM when fsyncSync is applied to a Windows directory handle.
946
+ return code === "EINVAL" || code === "ENOTSUP" || code === "EOPNOTSUPP" || code === "EPERM";
947
+ }
948
+
949
+ /** A write may have reached `rename` but not the directory fsync durability barrier. */
950
+ export class GateStoreWriteError extends Error {
951
+ constructor(
952
+ readonly certainty: "not_committed" | "uncertain",
953
+ cause: unknown,
954
+ ) {
955
+ super(`gate store write ${certainty}: ${cause instanceof Error ? cause.message : String(cause)}`);
956
+ this.name = "GateStoreWriteError";
957
+ }
102
958
  }
103
959
 
104
960
  /** Crash-durable JSON-file backed store. Writes the full state on every mutation. */
105
961
  export class FileGateStore implements GateStore {
106
962
  private state: FileState;
107
- constructor(private readonly filePath: string) {
108
- mkdirSync(path.dirname(filePath), { recursive: true });
963
+ private certaintyUnknown = false;
964
+ constructor(
965
+ private readonly filePath: string,
966
+ private readonly sync: (fd: number) => void = fsyncSync,
967
+ ) {
968
+ // Load eagerly so a corrupt store fails closed at construction, but defer
969
+ // directory creation to the first write so constructing a store under a
970
+ // non-writable cwd (e.g. a session that never emits a gate) never throws.
109
971
  this.state = this.load();
110
972
  }
111
973
  private load(): FileState {
@@ -113,14 +975,26 @@ export class FileGateStore implements GateStore {
113
975
  try {
114
976
  raw = readFileSync(this.filePath, "utf8");
115
977
  } catch (err) {
116
- if ((err as NodeJS.ErrnoException).code === "ENOENT") return { counters: {}, gates: {} };
978
+ if ((err as NodeJS.ErrnoException).code === "ENOENT")
979
+ return { version: FILE_GATE_STORE_VERSION, counters: {}, gates: {} };
117
980
  throw err;
118
981
  }
982
+ let parsed: unknown;
119
983
  try {
120
- return JSON.parse(raw) as FileState;
984
+ parsed = JSON.parse(raw);
985
+ if (isObject(parsed) && !Object.hasOwn(parsed, "version")) {
986
+ const migrated = migrateVersionlessV0(parsed, this.filePath);
987
+ // Persist the v1 envelope before beginRuntimeInstance can quarantine or
988
+ // expose any record. This preserves all v0 records atomically.
989
+ this.flushState(migrated);
990
+ return migrated;
991
+ }
992
+ assertFileState(parsed, this.filePath);
993
+ return parsed;
121
994
  } catch (err) {
122
- // Fail closed: a corrupt state file must not be silently reset (that would
123
- // drop counters/gates and risk gate-id reuse). Quarantine + throw.
995
+ if (err instanceof GateStoreWriteError) throw err;
996
+ // Fail closed: invalid state must not be exposed or mutated because it can
997
+ // drop counters/gates and risk gate-id reuse.
124
998
  const quarantine = `${this.filePath}.corrupt-${Date.now()}`;
125
999
  try {
126
1000
  renameSync(this.filePath, quarantine);
@@ -128,43 +1002,105 @@ export class FileGateStore implements GateStore {
128
1002
  /* best-effort quarantine */
129
1003
  }
130
1004
  throw new Error(
131
- `corrupt gate store at ${this.filePath} (quarantined to ${quarantine}): ${(err as Error).message}`,
1005
+ `corrupt gate store at ${this.filePath} (quarantined to ${quarantine}): ${err instanceof Error ? err.message : String(err)}`,
132
1006
  );
133
1007
  }
134
1008
  }
135
- private flush(): void {
136
- // Atomic write: serialize to a temp file, fsync, then rename over the target.
1009
+ private flushState(next: FileState): void {
1010
+ mkdirSync(path.dirname(this.filePath), { recursive: true });
137
1011
  const tmp = `${this.filePath}.tmp-${process.pid}-${Date.now()}`;
138
- const fd = openSync(tmp, "w");
1012
+ let renamed = false;
139
1013
  try {
140
- writeFileSync(fd, JSON.stringify(this.state, null, 2));
141
- fsyncSync(fd);
142
- } finally {
143
- closeSync(fd);
1014
+ const fd = openSync(tmp, "w");
1015
+ try {
1016
+ writeFileSync(fd, JSON.stringify(next, null, 2));
1017
+ this.sync(fd);
1018
+ } finally {
1019
+ closeSync(fd);
1020
+ }
1021
+ renameSync(tmp, this.filePath);
1022
+ renamed = true;
1023
+ const parentFd = openSync(path.dirname(this.filePath), "r");
1024
+ try {
1025
+ try {
1026
+ this.sync(parentFd);
1027
+ } catch (error) {
1028
+ if (!isUnsupportedWindowsDirectorySyncError(error)) throw error;
1029
+ }
1030
+ } finally {
1031
+ closeSync(parentFd);
1032
+ }
1033
+ } catch (error) {
1034
+ if (renamed) {
1035
+ this.certaintyUnknown = true;
1036
+ try {
1037
+ this.state = this.load();
1038
+ } catch {
1039
+ // The original uncertain error is the authority; later operations fail closed.
1040
+ }
1041
+ }
1042
+ throw new GateStoreWriteError(renamed ? "uncertain" : "not_committed", error);
144
1043
  }
145
- renameSync(tmp, this.filePath);
146
1044
  }
147
- nextSeq(stage: RpcWorkflowStage): number {
1045
+ private commit(next: FileState): void {
1046
+ this.flushState(next);
1047
+ this.state = next;
1048
+ }
1049
+ private reconcileCertainty(): void {
1050
+ if (!this.certaintyUnknown) return;
1051
+ this.state = this.load();
1052
+ this.certaintyUnknown = false;
1053
+ }
1054
+ nextSeq(stage: WorkflowStage): number {
1055
+ this.reconcileCertainty();
148
1056
  const next = (this.state.counters[stage] ?? 0) + 1;
149
- this.state.counters[stage] = next;
150
- this.flush();
1057
+ const candidate = structuredClone(this.state);
1058
+ candidate.counters[stage] = next;
1059
+ this.commit(candidate);
151
1060
  return next;
152
1061
  }
153
1062
  put(record: PersistedGate): void {
154
- this.state.gates[record.gate.gate_id] = record;
155
- this.flush();
1063
+ this.reconcileCertainty();
1064
+ const candidate = structuredClone(this.state);
1065
+ candidate.gates[record.gate.gate_id] = structuredClone(record);
1066
+ this.commit(candidate);
156
1067
  }
157
1068
  get(gateId: string): PersistedGate | undefined {
1069
+ this.reconcileCertainty();
158
1070
  const r = this.state.gates[gateId];
159
- return r ? (JSON.parse(JSON.stringify(r)) as PersistedGate) : undefined;
1071
+ return r ? structuredClone(r) : undefined;
1072
+ }
1073
+ beginRuntimeInstance(instanceId: string): void {
1074
+ this.reconcileCertainty();
1075
+ if (this.state.runtimeInstanceId === instanceId) return;
1076
+ const priorInstanceId = this.state.runtimeInstanceId;
1077
+ const next = structuredClone(this.state);
1078
+ const now = new Date().toISOString();
1079
+ for (const record of Object.values(next.gates)) {
1080
+ if (record.ownerInstanceId !== priorInstanceId) continue;
1081
+ if (record.status === "pending") {
1082
+ record.status = "quarantined";
1083
+ record.lifecycle = { state: "quarantined", reason: "orphaned_after_process_restart", quarantinedAt: now };
1084
+ } else if (record.status === "accepted" && !record.advanced) {
1085
+ record.status = "quarantined";
1086
+ record.lifecycle = {
1087
+ state: "quarantined",
1088
+ reason: "accepted_unadvanced_after_process_restart",
1089
+ quarantinedAt: now,
1090
+ };
1091
+ }
1092
+ }
1093
+ next.runtimeInstanceId = instanceId;
1094
+ this.commit(next);
160
1095
  }
161
1096
  list(): PersistedGate[] {
162
- return Object.values(this.state.gates).map(r => JSON.parse(JSON.stringify(r)) as PersistedGate);
1097
+ this.reconcileCertainty();
1098
+ return Object.values(this.state.gates).map(r => structuredClone(r));
163
1099
  }
164
1100
  }
165
1101
 
166
1102
  export type GateAuditEvent =
167
- | { event: "gate_emitted"; gate_id: string; stage: RpcWorkflowStage; kind: RpcWorkflowGateKind }
1103
+ | { event: "gate_emitted"; gate_id: string; stage: WorkflowStage; kind: WorkflowGateKind }
168
1104
  | { event: "gate_response_accepted"; gate_id: string; answer_hash: string }
169
1105
  | { event: "gate_response_rejected"; gate_id: string; answer_hash: string }
170
1106
  | { event: "gate_response_idempotent_replay"; gate_id: string }
@@ -175,26 +1111,44 @@ export type GateAuditEvent =
175
1111
 
176
1112
  export interface BrokerHooks {
177
1113
  /** Called once when a pending gate has been persisted and should be emitted. */
178
- emit?(gate: RpcWorkflowGate): void;
1114
+ emit?(gate: WorkflowGate): void;
179
1115
  /**
180
1116
  * Invoked to advance the workflow after an accepted resolution is durably
181
1117
  * committed. MUST be idempotent keyed by `gate.gate_id`: `recover()` replays
182
1118
  * it for any gate left `accepted` but not `advanced` by a crash.
183
1119
  */
184
- advance?(gate: RpcWorkflowGate, answer: unknown): void | Promise<void>;
185
- /** Runs after durable acceptance and before advance, including crash recovery. */
1120
+ advance?(gate: WorkflowGate, answer: unknown): void | Promise<void>;
1121
+ /** Runs after durable acceptance and before presentation terminalization, including crash recovery. */
186
1122
  finalizeAccepted?(record: PersistedGate): Promise<void>;
187
-
1123
+ /**
1124
+ * Terminalizes the exact presentation before `advance` can run and returns
1125
+ * proof for the durable record. `not_published` explicitly proves that no
1126
+ * presentation existed. On failure it MUST revoke authority or throw.
1127
+ */
1128
+ terminalizeAccepted?(record: PersistedGate): WorkflowGateTerminalProof | Promise<WorkflowGateTerminalProof>;
1129
+ /** Runs only after a successful advance has been durably marked advanced. */
1130
+ completeAccepted?(record: PersistedGate): void | Promise<void>;
188
1131
  /** Append-only audit sink. */
189
1132
  audit?(event: GateAuditEvent): void;
190
1133
  }
191
1134
 
1135
+ /** Live in-process continuation authority for one emitted gate. */
1136
+ export interface GateContinuation {
1137
+ activate(gate: WorkflowGate): void;
1138
+ isLive(gateId: string): boolean;
1139
+ release?(gateId: string): void;
1140
+ /** Explicitly declares that this direct continuation owns no presentation. */
1141
+ terminalProof?: "not_published";
1142
+ }
1143
+
192
1144
  export interface OpenGateInput {
193
- stage: RpcWorkflowStage;
194
- kind: RpcWorkflowGateKind;
195
- schema: RpcWorkflowGate["schema"];
196
- options?: RpcWorkflowGateOption[];
197
- context?: RpcWorkflowGateContext;
1145
+ stage: WorkflowStage;
1146
+ kind: WorkflowGateKind;
1147
+ schema: WorkflowGate["schema"];
1148
+ options?: WorkflowGateOption[];
1149
+ context?: WorkflowGateContext;
1150
+ /** Optional diagnostic gate replaced by this newly persisted gate. */
1151
+ supersedesGateId?: string;
198
1152
  }
199
1153
 
200
1154
  export class WorkflowGateBrokerError extends Error {
@@ -210,6 +1164,8 @@ export class WorkflowGateBrokerError extends Error {
210
1164
  export class WorkflowGateBroker {
211
1165
  private readonly recovering = new Set<string>();
212
1166
  private readonly gateLocks = new Map<string, Promise<void>>();
1167
+ private readonly continuations = new Map<string, GateContinuation>();
1168
+ readonly #terminalProofs = new Map<string, WorkflowGateTerminalProof>();
213
1169
 
214
1170
  private async withGateLock<T>(gateId: string, operation: () => Promise<T>): Promise<T> {
215
1171
  const previous = this.gateLocks.get(gateId) ?? Promise.resolve();
@@ -229,14 +1185,17 @@ export class WorkflowGateBroker {
229
1185
  private readonly runId: string,
230
1186
  private readonly store: GateStore,
231
1187
  private readonly hooks: BrokerHooks = {},
232
- ) {}
1188
+ private readonly instanceId = crypto.randomUUID(),
1189
+ ) {
1190
+ this.store.beginRuntimeInstance(this.instanceId);
1191
+ }
233
1192
 
234
1193
  classifyDisposition(gateId: string, answer: unknown): PersistedSemanticDisposition {
235
1194
  const record = this.store.get(gateId);
236
1195
  if (!record) throw new WorkflowGateBrokerError("unknown_gate", `no pending gate ${gateId}`);
237
1196
  return classifyAskGateDisposition(record.gate, answer);
238
1197
  }
239
- validationError(gateId: string, keyword: string, message: string): RpcWorkflowGateValidationError {
1198
+ validationError(gateId: string, keyword: string, message: string): WorkflowGateValidationError {
240
1199
  const record = this.store.get(gateId);
241
1200
  return {
242
1201
  code: "invalid_workflow_gate_answer",
@@ -253,12 +1212,144 @@ export class WorkflowGateBroker {
253
1212
  this.store.put({ ...record, ackPolicy });
254
1213
  }
255
1214
 
1215
+ async #terminalize(record: PersistedGate): Promise<void> {
1216
+ const proof = this.#terminalProofs.get(record.gate.gate_id) ?? (await this.hooks.terminalizeAccepted?.(record));
1217
+ this.#terminalProofs.delete(record.gate.gate_id);
1218
+ if (proof !== "retired" && proof !== "already_terminal" && proof !== "not_published")
1219
+ throw new Error(`workflow gate ${record.gate.gate_id} has no terminalization proof`);
1220
+ const terminalized = this.store.get(record.gate.gate_id);
1221
+ if (terminalized?.status !== "accepted" || terminalized.advanced)
1222
+ throw new WorkflowGateBrokerError(
1223
+ "unknown_gate",
1224
+ `accepted gate ${record.gate.gate_id} disappeared before advance`,
1225
+ );
1226
+ this.store.put({ ...terminalized, terminalized: true, terminalProof: proof });
1227
+ }
1228
+
256
1229
  private runShort(): string {
257
1230
  return this.runId.replace(/[^a-zA-Z0-9]/g, "").slice(-8) || "run";
258
1231
  }
259
1232
 
1233
+ private hasLiveContinuation(gateId: string): boolean {
1234
+ return this.continuations.get(gateId)?.isLive(gateId) === true;
1235
+ }
1236
+
1237
+ hasRecoverableAcceptedGate(): boolean {
1238
+ return this.store
1239
+ .list()
1240
+ .some(
1241
+ record =>
1242
+ record.status === "accepted" &&
1243
+ !record.advanced &&
1244
+ record.ownerInstanceId === this.instanceId &&
1245
+ this.hasLiveContinuation(record.gate.gate_id),
1246
+ );
1247
+ }
1248
+ prepareTerminalization(gateId: string, proof: WorkflowGateTerminalProof): boolean {
1249
+ const record = this.store.get(gateId);
1250
+ if (
1251
+ record?.status !== "pending" ||
1252
+ record.ownerInstanceId !== this.instanceId ||
1253
+ !this.hasLiveContinuation(gateId)
1254
+ )
1255
+ return false;
1256
+ this.#terminalProofs.set(gateId, proof);
1257
+ return true;
1258
+ }
1259
+ clearPreparedTerminalization(gateId: string): void {
1260
+ this.#terminalProofs.delete(gateId);
1261
+ }
1262
+
1263
+ /**
1264
+ * Fences a gate after an uncertain control outcome by deliberately releasing
1265
+ * this runtime's continuation authority. The durable diagnostic is retained
1266
+ * for recovery; it is never republished as a pending gate.
1267
+ */
1268
+ quarantineGate(gateId: string): void {
1269
+ this.loseContinuation(gateId);
1270
+ }
1271
+
1272
+ /** Revoke every live continuation, including accepted-but-unadvanced recovery authority. */
1273
+ fenceContinuations(): void {
1274
+ for (const gateId of [...this.continuations.keys()]) this.loseContinuation(gateId);
1275
+ }
1276
+
1277
+ /** Fence a cancelled/lost continuation before it can be exposed or advanced. */
1278
+ loseContinuation(gateId: string): void {
1279
+ const continuation = this.continuations.get(gateId);
1280
+ continuation?.release?.(gateId);
1281
+ this.continuations.delete(gateId);
1282
+ this.#terminalProofs.delete(gateId);
1283
+ const record = this.store.get(gateId);
1284
+ if (
1285
+ record?.ownerInstanceId !== this.instanceId ||
1286
+ record.status === "quarantined" ||
1287
+ (record.status === "accepted" && record.advanced)
1288
+ )
1289
+ return;
1290
+ this.store.put({
1291
+ ...record,
1292
+ status: "quarantined",
1293
+ advanced: false,
1294
+ lifecycle: {
1295
+ state: "quarantined",
1296
+ reason: "continuation_owner_lost",
1297
+ quarantinedAt: new Date().toISOString(),
1298
+ },
1299
+ });
1300
+ }
1301
+
1302
+ listPendingGates(): WorkflowGate[] {
1303
+ return this.store
1304
+ .list()
1305
+ .filter(
1306
+ record =>
1307
+ record.status === "pending" &&
1308
+ record.ownerInstanceId === this.instanceId &&
1309
+ this.hasLiveContinuation(record.gate.gate_id),
1310
+ )
1311
+ .sort(compareGates)
1312
+ .map(record => record.gate);
1313
+ }
1314
+
1315
+ listGateDiagnostics(): WorkflowGateDiagnostic[] {
1316
+ return this.store
1317
+ .list()
1318
+ .filter(
1319
+ (
1320
+ record,
1321
+ ): record is PersistedGate & { status: "quarantined"; lifecycle: WorkflowGateDiagnostic["lifecycle"] } =>
1322
+ record.status === "quarantined" && record.lifecycle !== undefined,
1323
+ )
1324
+ .sort(
1325
+ (left, right) =>
1326
+ left.lifecycle.quarantinedAt.localeCompare(right.lifecycle.quarantinedAt) ||
1327
+ left.gate.gate_id.localeCompare(right.gate.gate_id),
1328
+ )
1329
+ .map(record => ({
1330
+ ...record.gate,
1331
+ id: `diagnostic:${record.gate.gate_id}`,
1332
+ tag: "quarantined",
1333
+ lifecycle: record.lifecycle,
1334
+ }));
1335
+ }
1336
+
1337
+ listWorkflowGateQueryRecords(): WorkflowGateQueryRecord[] {
1338
+ const pending: WorkflowGateQueryRecord[] = this.store
1339
+ .list()
1340
+ .filter(
1341
+ record =>
1342
+ record.status === "pending" &&
1343
+ record.ownerInstanceId === this.instanceId &&
1344
+ this.hasLiveContinuation(record.gate.gate_id),
1345
+ )
1346
+ .sort(compareGates)
1347
+ .map(record => ({ ...record.gate, id: `pending:${record.gate.gate_id}`, tag: "pending" }));
1348
+ return [...pending, ...this.listGateDiagnostics()];
1349
+ }
1350
+
260
1351
  /** Open and emit a gate. The pending record is persisted BEFORE emission. */
261
- openGate(input: OpenGateInput): RpcWorkflowGate {
1352
+ openGate(input: OpenGateInput, continuation?: GateContinuation): WorkflowGate {
262
1353
  if (RESERVED_WORKFLOW_STAGES.includes(input.stage) || !V1_STAGES.includes(input.stage)) {
263
1354
  throw new WorkflowGateBrokerError(
264
1355
  "invalid_workflow_stage",
@@ -269,7 +1360,7 @@ export class WorkflowGateBroker {
269
1360
  compileGateSchema(input.schema);
270
1361
  const seq = this.store.nextSeq(input.stage).toString().padStart(6, "0");
271
1362
  const gateId = `wg_${this.runShort()}_${input.stage}_${seq}`;
272
- const gate: RpcWorkflowGate = {
1363
+ const gate: WorkflowGate = {
273
1364
  type: "workflow_gate",
274
1365
  gate_id: gateId,
275
1366
  stage: input.stage,
@@ -281,8 +1372,30 @@ export class WorkflowGateBroker {
281
1372
  created_at: new Date().toISOString(),
282
1373
  required: true,
283
1374
  };
284
- // Persist pending BEFORE emit so a crash never loses an emitted gate.
285
- this.store.put({ gate, status: "pending", advanced: false });
1375
+ if (!continuation) {
1376
+ this.store.put({
1377
+ gate,
1378
+ status: "quarantined",
1379
+ ownerInstanceId: this.instanceId,
1380
+ advanced: false,
1381
+ lifecycle: {
1382
+ state: "quarantined",
1383
+ reason: "opened_without_continuation",
1384
+ quarantinedAt: new Date().toISOString(),
1385
+ },
1386
+ });
1387
+ return gate;
1388
+ }
1389
+ this.store.put({ gate, status: "pending", ownerInstanceId: this.instanceId, advanced: false });
1390
+ continuation.activate(gate);
1391
+ if (continuation.terminalProof === "not_published") this.#terminalProofs.set(gateId, "not_published");
1392
+ this.continuations.set(gateId, continuation);
1393
+ if (input.supersedesGateId) {
1394
+ const superseded = this.store.get(input.supersedesGateId);
1395
+ if (superseded?.status === "quarantined" && superseded.lifecycle) {
1396
+ this.store.put({ ...superseded, lifecycle: { ...superseded.lifecycle, supersededByGateId: gateId } });
1397
+ }
1398
+ }
286
1399
  this.hooks.emit?.(gate);
287
1400
  this.hooks.audit?.({ event: "gate_emitted", gate_id: gateId, stage: gate.stage, kind: gate.kind });
288
1401
  return gate;
@@ -290,33 +1403,41 @@ export class WorkflowGateBroker {
290
1403
 
291
1404
  /**
292
1405
  * Resolve a gate with an answer. Validates against the advertised schema.
293
- * On success the resolution is persisted BEFORE `advance` is invoked exactly
294
- * once. Invalid answers leave the gate pending (per #315 acceptance).
1406
+ * Accepted answers are persisted first, then their presentation is terminalized,
1407
+ * then the workflow advances and commits `advanced:true` exactly once. Invalid
1408
+ * answers leave the gate pending (per #315 acceptance).
295
1409
  */
296
- async resolve(
297
- response: RpcWorkflowGateResponse,
298
- options: GateResolutionOptions = {},
299
- ): Promise<RpcWorkflowGateResolution> {
300
- return this.withGateLock(response.gate_id, () => this.resolveUnlocked(response, options));
1410
+ async resolve(response: WorkflowGateResponse, options: GateResolutionOptions = {}): Promise<WorkflowGateResolution> {
1411
+ try {
1412
+ return await this.withGateLock(response.gate_id, () => this.resolveUnlocked(response, options));
1413
+ } catch (error) {
1414
+ if (error instanceof GateStoreWriteError && error.certainty === "uncertain") {
1415
+ try {
1416
+ this.quarantineGate(response.gate_id);
1417
+ } catch {
1418
+ // Continuation authority was still released; preserve the uncertain write error.
1419
+ }
1420
+ }
1421
+ throw error;
1422
+ }
301
1423
  }
302
1424
 
303
1425
  private async resolveUnlocked(
304
- response: RpcWorkflowGateResponse,
1426
+ response: WorkflowGateResponse,
305
1427
  options: GateResolutionOptions = {},
306
- ): Promise<RpcWorkflowGateResolution> {
1428
+ ): Promise<WorkflowGateResolution> {
307
1429
  const record = this.store.get(response.gate_id);
308
- if (!record) {
1430
+ if (!record || record.status === "quarantined") {
309
1431
  this.hooks.audit?.({ event: "gate_response_unknown_gate", gate_id: response.gate_id });
310
- throw new WorkflowGateBrokerError("unknown_gate", `no pending gate ${response.gate_id}`);
1432
+ throw new WorkflowGateBrokerError("unknown_gate", `no live pending gate ${response.gate_id}`);
311
1433
  }
312
1434
  const responseHash = answerHashOf({ gate_id: response.gate_id, answer: response.answer });
313
-
314
1435
  if (record.status === "accepted") {
315
1436
  const sameBody = record.responseHash === responseHash;
316
1437
  const sameKey = record.idempotencyKey === response.idempotency_key;
317
1438
  if (response.idempotency_key !== undefined && sameKey && sameBody) {
318
1439
  this.hooks.audit?.({ event: "gate_response_idempotent_replay", gate_id: response.gate_id });
319
- return record.resolution as RpcWorkflowGateResolution;
1440
+ return record.resolution as WorkflowGateResolution;
320
1441
  }
321
1442
  if (response.idempotency_key !== undefined && sameKey && !sameBody) {
322
1443
  this.hooks.audit?.({ event: "gate_response_idempotency_conflict", gate_id: response.gate_id });
@@ -328,6 +1449,10 @@ export class WorkflowGateBroker {
328
1449
  this.hooks.audit?.({ event: "gate_response_already_resolved", gate_id: response.gate_id });
329
1450
  throw new WorkflowGateBrokerError("already_resolved", `already_resolved: gate ${response.gate_id}`);
330
1451
  }
1452
+ if (record.ownerInstanceId !== this.instanceId || !this.hasLiveContinuation(response.gate_id)) {
1453
+ this.hooks.audit?.({ event: "gate_response_unknown_gate", gate_id: response.gate_id });
1454
+ throw new WorkflowGateBrokerError("unknown_gate", `no live pending gate ${response.gate_id}`);
1455
+ }
331
1456
 
332
1457
  const compiled = compileGateSchema(record.gate.schema);
333
1458
  const validationError = validateGateAnswer(compiled, response.gate_id, response.answer);
@@ -346,12 +1471,12 @@ export class WorkflowGateBroker {
346
1471
 
347
1472
  const semanticDisposition =
348
1473
  options.semanticDisposition ?? classifyAskGateDisposition(record.gate, response.answer);
349
- const resolutionOrigin = options.resolutionOrigin ?? { kind: "generic", channel: "rpc" as const };
1474
+ const resolutionOrigin = options.resolutionOrigin ?? { kind: "generic", channel: "sdk" as const };
350
1475
  const ackPolicy =
351
1476
  options.ackPolicy ??
352
1477
  ({ kind: "none", reason: semanticDisposition === "commit" ? "non_telegram" : "semantic_noncommit" } as const);
353
1478
 
354
- const resolution: RpcWorkflowGateResolution = {
1479
+ const resolution: WorkflowGateResolution = {
355
1480
  gate_id: response.gate_id,
356
1481
  status: "accepted",
357
1482
  answer_hash: answerHash,
@@ -363,11 +1488,13 @@ export class WorkflowGateBroker {
363
1488
  this.store.put({
364
1489
  gate: record.gate,
365
1490
  status: "accepted",
1491
+ ownerInstanceId: record.ownerInstanceId,
366
1492
  idempotencyKey: response.idempotency_key,
367
1493
  responseHash,
368
1494
  answer: response.answer,
369
1495
  resolution,
370
1496
  advanced: false,
1497
+ terminalized: false,
371
1498
  semanticDisposition,
372
1499
  resolutionOrigin,
373
1500
  ackPolicy,
@@ -379,19 +1506,32 @@ export class WorkflowGateBroker {
379
1506
  if (finalized?.status !== "accepted") {
380
1507
  throw new WorkflowGateBrokerError(
381
1508
  "unknown_gate",
382
- `accepted gate ${response.gate_id} disappeared before advance`,
1509
+ `accepted gate ${response.gate_id} disappeared before terminalization`,
383
1510
  );
384
1511
  }
385
1512
  if (finalized.advanced) return finalized.resolution ?? resolution;
386
- await this.hooks.advance?.(finalized.gate, finalized.answer);
387
- const latest = this.store.get(response.gate_id);
388
- if (latest?.status !== "accepted") {
1513
+ if (!finalized.terminalized) await this.#terminalize(finalized);
1514
+ const terminalized = this.store.get(response.gate_id);
1515
+ if (terminalized?.status !== "accepted" || terminalized.advanced) {
389
1516
  throw new WorkflowGateBrokerError(
390
1517
  "unknown_gate",
391
- `accepted gate ${response.gate_id} disappeared after advance`,
1518
+ `accepted gate ${response.gate_id} disappeared before advance`,
392
1519
  );
393
1520
  }
1521
+ if (!this.hooks.advance || !this.hasLiveContinuation(response.gate_id)) {
1522
+ throw new WorkflowGateBrokerError("unknown_gate", `no live continuation for gate ${response.gate_id}`);
1523
+ }
1524
+ await this.hooks.advance(terminalized.gate, terminalized.answer);
1525
+ const latest = this.store.get(response.gate_id);
1526
+ if (latest?.status !== "accepted" || !this.hasLiveContinuation(response.gate_id))
1527
+ throw new WorkflowGateBrokerError(
1528
+ "unknown_gate",
1529
+ `accepted gate ${response.gate_id} lost its continuation before advance completed`,
1530
+ );
394
1531
  this.store.put({ ...latest, advanced: true });
1532
+ await this.hooks.completeAccepted?.(this.store.get(response.gate_id) as PersistedGate);
1533
+ this.continuations.get(response.gate_id)?.release?.(response.gate_id);
1534
+ this.continuations.delete(response.gate_id);
395
1535
  return resolution;
396
1536
  }
397
1537
 
@@ -404,7 +1544,14 @@ export class WorkflowGateBroker {
404
1544
  async recover(): Promise<string[]> {
405
1545
  const recovered: string[] = [];
406
1546
  for (const listed of this.store.list()) {
407
- if (listed.status !== "accepted" || listed.advanced || this.recovering.has(listed.gate.gate_id)) continue;
1547
+ if (
1548
+ listed.status !== "accepted" ||
1549
+ listed.advanced ||
1550
+ listed.ownerInstanceId !== this.instanceId ||
1551
+ !this.hasLiveContinuation(listed.gate.gate_id) ||
1552
+ this.recovering.has(listed.gate.gate_id)
1553
+ )
1554
+ continue;
408
1555
  this.recovering.add(listed.gate.gate_id);
409
1556
  try {
410
1557
  await this.withGateLock(listed.gate.gate_id, async () => {
@@ -412,11 +1559,29 @@ export class WorkflowGateBroker {
412
1559
  if (rec?.status !== "accepted" || rec.advanced) return;
413
1560
  await this.hooks.finalizeAccepted?.(rec);
414
1561
  const finalized = this.store.get(listed.gate.gate_id);
415
- if (finalized?.status !== "accepted" || finalized.advanced) return;
416
- await this.hooks.advance?.(finalized.gate, finalized.answer);
1562
+ if (
1563
+ finalized?.status !== "accepted" ||
1564
+ finalized.advanced ||
1565
+ !this.hasLiveContinuation(listed.gate.gate_id)
1566
+ )
1567
+ return;
1568
+ if (!finalized.terminalized) await this.#terminalize(finalized);
1569
+ const terminalized = this.store.get(listed.gate.gate_id);
1570
+ if (
1571
+ terminalized?.status !== "accepted" ||
1572
+ terminalized.advanced ||
1573
+ !this.hooks.advance ||
1574
+ !this.hasLiveContinuation(listed.gate.gate_id)
1575
+ )
1576
+ return;
1577
+ await this.hooks.advance(terminalized.gate, terminalized.answer);
417
1578
  const latest = this.store.get(listed.gate.gate_id);
418
- if (latest?.status !== "accepted" || latest.advanced) return;
1579
+ if (latest?.status !== "accepted" || latest.advanced || !this.hasLiveContinuation(listed.gate.gate_id))
1580
+ return;
419
1581
  this.store.put({ ...latest, advanced: true });
1582
+ await this.hooks.completeAccepted?.(this.store.get(latest.gate.gate_id) as PersistedGate);
1583
+ this.continuations.get(latest.gate.gate_id)?.release?.(latest.gate.gate_id);
1584
+ this.continuations.delete(latest.gate.gate_id);
420
1585
  this.hooks.audit?.({ event: "gate_advance_recovered", gate_id: latest.gate.gate_id });
421
1586
  recovered.push(latest.gate.gate_id);
422
1587
  });
@@ -432,3 +1597,9 @@ export class WorkflowGateBroker {
432
1597
  return canonicalJson(value);
433
1598
  }
434
1599
  }
1600
+
1601
+ function compareGates(left: PersistedGate, right: PersistedGate): number {
1602
+ return (
1603
+ left.gate.created_at.localeCompare(right.gate.created_at) || left.gate.gate_id.localeCompare(right.gate.gate_id)
1604
+ );
1605
+ }