@scotthuang/agent-knock-knock 0.12.11 → 0.12.12

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 (322) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +5 -5
  3. package/dist/src/callback-cli-adapter.d.ts +47 -0
  4. package/dist/src/callback-cli-adapter.js +156 -0
  5. package/dist/src/callback-cli-adapter.js.map +1 -0
  6. package/dist/src/callback-outbox-policy.d.ts +95 -0
  7. package/dist/src/callback-outbox-policy.js +149 -0
  8. package/dist/src/callback-outbox-policy.js.map +1 -0
  9. package/dist/src/callback-outbox-service.d.ts +247 -0
  10. package/dist/src/callback-outbox-service.js +1053 -0
  11. package/dist/src/callback-outbox-service.js.map +1 -0
  12. package/dist/src/callback-outbox-settlement.d.ts +54 -0
  13. package/dist/src/callback-outbox-settlement.js +241 -0
  14. package/dist/src/callback-outbox-settlement.js.map +1 -0
  15. package/dist/src/canonical-json.d.ts +1 -0
  16. package/dist/src/canonical-json.js +14 -0
  17. package/dist/src/canonical-json.js.map +1 -0
  18. package/dist/src/claude-lifecycle-compatibility.js +15 -0
  19. package/dist/src/claude-lifecycle-compatibility.js.map +1 -1
  20. package/dist/src/claude-local-transcript-provider.d.ts +1 -1
  21. package/dist/src/claude-local-transcript-provider.js +19 -5
  22. package/dist/src/claude-local-transcript-provider.js.map +1 -1
  23. package/dist/src/cli-command-runtime.d.ts +14 -0
  24. package/dist/src/cli-command-runtime.js +164 -0
  25. package/dist/src/cli-command-runtime.js.map +1 -0
  26. package/dist/src/cli-core.d.ts +4 -42
  27. package/dist/src/cli-core.js +906 -28717
  28. package/dist/src/cli-core.js.map +1 -1
  29. package/dist/src/cli-runtime-context.d.ts +57 -0
  30. package/dist/src/cli-runtime-context.js +118 -0
  31. package/dist/src/cli-runtime-context.js.map +1 -0
  32. package/dist/src/codex-lifecycle-compatibility.js +2 -1
  33. package/dist/src/codex-lifecycle-compatibility.js.map +1 -1
  34. package/dist/src/codex-local-session-provider.d.ts +18 -0
  35. package/dist/src/codex-local-session-provider.js +67 -0
  36. package/dist/src/codex-local-session-provider.js.map +1 -1
  37. package/dist/src/codex-session-provider.js +1 -3
  38. package/dist/src/codex-session-provider.js.map +1 -1
  39. package/dist/src/conversation-trace.d.ts +30 -0
  40. package/dist/src/conversation-trace.js +185 -0
  41. package/dist/src/conversation-trace.js.map +1 -0
  42. package/dist/src/deferred-foreground-application-service.d.ts +84 -0
  43. package/dist/src/deferred-foreground-application-service.js +742 -0
  44. package/dist/src/deferred-foreground-application-service.js.map +1 -0
  45. package/dist/src/deferred-foreground-authority-cli-adapter.d.ts +78 -0
  46. package/dist/src/deferred-foreground-authority-cli-adapter.js +724 -0
  47. package/dist/src/deferred-foreground-authority-cli-adapter.js.map +1 -0
  48. package/dist/src/deferred-foreground-boundary.d.ts +61 -0
  49. package/dist/src/deferred-foreground-boundary.js +2 -0
  50. package/dist/src/deferred-foreground-boundary.js.map +1 -0
  51. package/dist/src/deferred-foreground-capability.d.ts +4 -0
  52. package/dist/src/deferred-foreground-capability.js +184 -0
  53. package/dist/src/deferred-foreground-capability.js.map +1 -0
  54. package/dist/src/deferred-foreground-preparation-cli-adapter.d.ts +53 -0
  55. package/dist/src/deferred-foreground-preparation-cli-adapter.js +210 -0
  56. package/dist/src/deferred-foreground-preparation-cli-adapter.js.map +1 -0
  57. package/dist/src/deferred-foreground-preparation-service.d.ts +72 -0
  58. package/dist/src/deferred-foreground-preparation-service.js +179 -0
  59. package/dist/src/deferred-foreground-preparation-service.js.map +1 -0
  60. package/dist/src/deferred-foreground-recovery-cli-adapter.d.ts +139 -0
  61. package/dist/src/deferred-foreground-recovery-cli-adapter.js +1125 -0
  62. package/dist/src/deferred-foreground-recovery-cli-adapter.js.map +1 -0
  63. package/dist/src/deferred-foreground-recovery-service.d.ts +48 -0
  64. package/dist/src/deferred-foreground-recovery-service.js +91 -0
  65. package/dist/src/deferred-foreground-recovery-service.js.map +1 -0
  66. package/dist/src/deferred-foreground-transfer-policy.d.ts +2 -0
  67. package/dist/src/deferred-foreground-transfer-policy.js +10 -0
  68. package/dist/src/deferred-foreground-transfer-policy.js.map +1 -0
  69. package/dist/src/deferred-foreground-transfer.js +43 -101
  70. package/dist/src/deferred-foreground-transfer.js.map +1 -1
  71. package/dist/src/doctor-capabilities.d.ts +9 -0
  72. package/dist/src/doctor-capabilities.js +22 -0
  73. package/dist/src/doctor-capabilities.js.map +1 -1
  74. package/dist/src/durable-json-file.d.ts +22 -0
  75. package/dist/src/durable-json-file.js +127 -0
  76. package/dist/src/durable-json-file.js.map +1 -0
  77. package/dist/src/file-lock-cli-adapter.d.ts +43 -0
  78. package/dist/src/file-lock-cli-adapter.js +150 -0
  79. package/dist/src/file-lock-cli-adapter.js.map +1 -0
  80. package/dist/src/herdr-terminal-control-provider.js +1 -3
  81. package/dist/src/herdr-terminal-control-provider.js.map +1 -1
  82. package/dist/src/install-doctor-command-adapter.d.ts +17 -0
  83. package/dist/src/install-doctor-command-adapter.js +225 -0
  84. package/dist/src/install-doctor-command-adapter.js.map +1 -0
  85. package/dist/src/live-lifecycle-evidence.js +76 -46
  86. package/dist/src/live-lifecycle-evidence.js.map +1 -1
  87. package/dist/src/live-lifecycle-smoke.js +74 -50
  88. package/dist/src/live-lifecycle-smoke.js.map +1 -1
  89. package/dist/src/managed-session.d.ts +2 -0
  90. package/dist/src/managed-session.js +24 -0
  91. package/dist/src/managed-session.js.map +1 -1
  92. package/dist/src/managed-turn-recovery-service.d.ts +86 -0
  93. package/dist/src/managed-turn-recovery-service.js +145 -0
  94. package/dist/src/managed-turn-recovery-service.js.map +1 -0
  95. package/dist/src/mutation-transaction.d.ts +48 -0
  96. package/dist/src/mutation-transaction.js +73 -0
  97. package/dist/src/mutation-transaction.js.map +1 -0
  98. package/dist/src/native-thread-lifecycle-cli-adapter.d.ts +154 -0
  99. package/dist/src/native-thread-lifecycle-cli-adapter.js +786 -0
  100. package/dist/src/native-thread-lifecycle-cli-adapter.js.map +1 -0
  101. package/dist/src/native-thread-lifecycle-ledger-cli-adapter.d.ts +25 -0
  102. package/dist/src/native-thread-lifecycle-ledger-cli-adapter.js +50 -0
  103. package/dist/src/native-thread-lifecycle-ledger-cli-adapter.js.map +1 -0
  104. package/dist/src/native-thread-lifecycle-query-service.d.ts +75 -0
  105. package/dist/src/native-thread-lifecycle-query-service.js +297 -0
  106. package/dist/src/native-thread-lifecycle-query-service.js.map +1 -0
  107. package/dist/src/native-thread-lifecycle-recovery-adapter.d.ts +45 -0
  108. package/dist/src/native-thread-lifecycle-recovery-adapter.js +154 -0
  109. package/dist/src/native-thread-lifecycle-recovery-adapter.js.map +1 -0
  110. package/dist/src/native-thread-lifecycle-recovery-service.d.ts +211 -0
  111. package/dist/src/native-thread-lifecycle-recovery-service.js +1093 -0
  112. package/dist/src/native-thread-lifecycle-recovery-service.js.map +1 -0
  113. package/dist/src/native-thread-resume-snapshot-policy.d.ts +73 -0
  114. package/dist/src/native-thread-resume-snapshot-policy.js +218 -0
  115. package/dist/src/native-thread-resume-snapshot-policy.js.map +1 -0
  116. package/dist/src/native-thread-resume-snapshot.d.ts +3 -61
  117. package/dist/src/native-thread-resume-snapshot.js +2 -143
  118. package/dist/src/native-thread-resume-snapshot.js.map +1 -1
  119. package/dist/src/native-thread-transition-application.d.ts +147 -0
  120. package/dist/src/native-thread-transition-application.js +974 -0
  121. package/dist/src/native-thread-transition-application.js.map +1 -0
  122. package/dist/src/native-thread-transition-policy.d.ts +127 -0
  123. package/dist/src/native-thread-transition-policy.js +154 -0
  124. package/dist/src/native-thread-transition-policy.js.map +1 -0
  125. package/dist/src/native-thread-transition-resource-adapter.d.ts +12 -0
  126. package/dist/src/native-thread-transition-resource-adapter.js +44 -0
  127. package/dist/src/native-thread-transition-resource-adapter.js.map +1 -0
  128. package/dist/src/native-thread-transition-settlement-service.d.ts +94 -0
  129. package/dist/src/native-thread-transition-settlement-service.js +134 -0
  130. package/dist/src/native-thread-transition-settlement-service.js.map +1 -0
  131. package/dist/src/native-thread-transition-verification-adapter.d.ts +62 -0
  132. package/dist/src/native-thread-transition-verification-adapter.js +451 -0
  133. package/dist/src/native-thread-transition-verification-adapter.js.map +1 -0
  134. package/dist/src/openclaw-callback-transport.d.ts +64 -0
  135. package/dist/src/openclaw-callback-transport.js +583 -0
  136. package/dist/src/openclaw-callback-transport.js.map +1 -0
  137. package/dist/src/openclaw-plugin-callback-adapter.d.ts +1 -0
  138. package/dist/src/openclaw-plugin-callback-adapter.js +341 -0
  139. package/dist/src/openclaw-plugin-callback-adapter.js.map +1 -0
  140. package/dist/src/openclaw-plugin-command-adapter.d.ts +16 -0
  141. package/dist/src/openclaw-plugin-command-adapter.js +1427 -0
  142. package/dist/src/openclaw-plugin-command-adapter.js.map +1 -0
  143. package/dist/src/openclaw-plugin-helpers.js +48 -38
  144. package/dist/src/openclaw-plugin-helpers.js.map +1 -1
  145. package/dist/src/openclaw-plugin-schemas.d.ts +382 -0
  146. package/dist/src/openclaw-plugin-schemas.js +394 -0
  147. package/dist/src/openclaw-plugin-schemas.js.map +1 -0
  148. package/dist/src/openclaw-plugin-supervisor.d.ts +6 -0
  149. package/dist/src/openclaw-plugin-supervisor.js +85 -0
  150. package/dist/src/openclaw-plugin-supervisor.js.map +1 -0
  151. package/dist/src/openclaw-plugin.js +7 -2212
  152. package/dist/src/openclaw-plugin.js.map +1 -1
  153. package/dist/src/protocol.d.ts +5 -0
  154. package/dist/src/protocol.js +59 -3
  155. package/dist/src/protocol.js.map +1 -1
  156. package/dist/src/session-store.d.ts +1 -0
  157. package/dist/src/session-store.js +84 -106
  158. package/dist/src/session-store.js.map +1 -1
  159. package/dist/src/store.js +2 -1
  160. package/dist/src/store.js.map +1 -1
  161. package/dist/src/terminal-acceptance-application-service.d.ts +65 -0
  162. package/dist/src/terminal-acceptance-application-service.js +63 -0
  163. package/dist/src/terminal-acceptance-application-service.js.map +1 -0
  164. package/dist/src/terminal-acceptance-cli-adapter.d.ts +210 -0
  165. package/dist/src/terminal-acceptance-cli-adapter.js +957 -0
  166. package/dist/src/terminal-acceptance-cli-adapter.js.map +1 -0
  167. package/dist/src/terminal-action-projection.d.ts +291 -0
  168. package/dist/src/terminal-action-projection.js +353 -0
  169. package/dist/src/terminal-action-projection.js.map +1 -0
  170. package/dist/src/terminal-agent-bridge.d.ts +3 -0
  171. package/dist/src/terminal-agent-bridge.js +29 -8
  172. package/dist/src/terminal-agent-bridge.js.map +1 -1
  173. package/dist/src/terminal-authority-policy.d.ts +141 -0
  174. package/dist/src/terminal-authority-policy.js +367 -0
  175. package/dist/src/terminal-authority-policy.js.map +1 -0
  176. package/dist/src/terminal-binding-authority.d.ts +118 -0
  177. package/dist/src/terminal-binding-authority.js +245 -0
  178. package/dist/src/terminal-binding-authority.js.map +1 -0
  179. package/dist/src/terminal-binding-reconciliation-service.d.ts +75 -0
  180. package/dist/src/terminal-binding-reconciliation-service.js +73 -0
  181. package/dist/src/terminal-binding-reconciliation-service.js.map +1 -0
  182. package/dist/src/terminal-command-cli-adapter.d.ts +375 -0
  183. package/dist/src/terminal-command-cli-adapter.js +3128 -0
  184. package/dist/src/terminal-command-cli-adapter.js.map +1 -0
  185. package/dist/src/terminal-control-ref.d.ts +4 -0
  186. package/dist/src/terminal-control-ref.js +11 -8
  187. package/dist/src/terminal-control-ref.js.map +1 -1
  188. package/dist/src/terminal-delegate-cli-adapter.d.ts +41 -0
  189. package/dist/src/terminal-delegate-cli-adapter.js +242 -0
  190. package/dist/src/terminal-delegate-cli-adapter.js.map +1 -0
  191. package/dist/src/terminal-dispatch-abort.d.ts +22 -0
  192. package/dist/src/terminal-dispatch-abort.js +30 -0
  193. package/dist/src/terminal-dispatch-abort.js.map +1 -0
  194. package/dist/src/terminal-dispatch-application.d.ts +115 -0
  195. package/dist/src/terminal-dispatch-application.js +478 -0
  196. package/dist/src/terminal-dispatch-application.js.map +1 -0
  197. package/dist/src/terminal-dispatch-capability.d.ts +63 -0
  198. package/dist/src/terminal-dispatch-capability.js +133 -0
  199. package/dist/src/terminal-dispatch-capability.js.map +1 -0
  200. package/dist/src/terminal-dispatch-completion-cli-adapter.d.ts +22 -0
  201. package/dist/src/terminal-dispatch-completion-cli-adapter.js +96 -0
  202. package/dist/src/terminal-dispatch-completion-cli-adapter.js.map +1 -0
  203. package/dist/src/terminal-dispatch-composition.d.ts +79 -0
  204. package/dist/src/terminal-dispatch-composition.js +2 -0
  205. package/dist/src/terminal-dispatch-composition.js.map +1 -0
  206. package/dist/src/terminal-dispatch-execution.d.ts +290 -0
  207. package/dist/src/terminal-dispatch-execution.js +916 -0
  208. package/dist/src/terminal-dispatch-execution.js.map +1 -0
  209. package/dist/src/terminal-dispatch-ledger-codec.d.ts +110 -0
  210. package/dist/src/terminal-dispatch-ledger-codec.js +399 -0
  211. package/dist/src/terminal-dispatch-ledger-codec.js.map +1 -0
  212. package/dist/src/terminal-dispatch-policy.d.ts +93 -0
  213. package/dist/src/terminal-dispatch-policy.js +122 -0
  214. package/dist/src/terminal-dispatch-policy.js.map +1 -0
  215. package/dist/src/terminal-dispatch-presenter.d.ts +65 -0
  216. package/dist/src/terminal-dispatch-presenter.js +258 -0
  217. package/dist/src/terminal-dispatch-presenter.js.map +1 -0
  218. package/dist/src/terminal-dispatch-receipt.d.ts +79 -0
  219. package/dist/src/terminal-dispatch-receipt.js +314 -0
  220. package/dist/src/terminal-dispatch-receipt.js.map +1 -0
  221. package/dist/src/terminal-dispatch-recovery-cli-adapter.d.ts +132 -0
  222. package/dist/src/terminal-dispatch-recovery-cli-adapter.js +1330 -0
  223. package/dist/src/terminal-dispatch-recovery-cli-adapter.js.map +1 -0
  224. package/dist/src/terminal-dispatch-recovery-service.d.ts +271 -0
  225. package/dist/src/terminal-dispatch-recovery-service.js +374 -0
  226. package/dist/src/terminal-dispatch-recovery-service.js.map +1 -0
  227. package/dist/src/terminal-dispatch-repository-cli-adapter.d.ts +35 -0
  228. package/dist/src/terminal-dispatch-repository-cli-adapter.js +257 -0
  229. package/dist/src/terminal-dispatch-repository-cli-adapter.js.map +1 -0
  230. package/dist/src/terminal-handoff-application-service.d.ts +7 -0
  231. package/dist/src/terminal-handoff-application-service.js +18 -0
  232. package/dist/src/terminal-handoff-application-service.js.map +1 -0
  233. package/dist/src/terminal-handoff-cli-adapter.d.ts +229 -0
  234. package/dist/src/terminal-handoff-cli-adapter.js +1389 -0
  235. package/dist/src/terminal-handoff-cli-adapter.js.map +1 -0
  236. package/dist/src/terminal-handoff-facts.d.ts +12 -0
  237. package/dist/src/terminal-handoff-facts.js +16 -0
  238. package/dist/src/terminal-handoff-facts.js.map +1 -0
  239. package/dist/src/terminal-identity-authority-cli-adapter.d.ts +318 -0
  240. package/dist/src/terminal-identity-authority-cli-adapter.js +1072 -0
  241. package/dist/src/terminal-identity-authority-cli-adapter.js.map +1 -0
  242. package/dist/src/terminal-identity-authority-service.d.ts +48 -0
  243. package/dist/src/terminal-identity-authority-service.js +45 -0
  244. package/dist/src/terminal-identity-authority-service.js.map +1 -0
  245. package/dist/src/terminal-list-cli-adapter.d.ts +250 -0
  246. package/dist/src/terminal-list-cli-adapter.js +2226 -0
  247. package/dist/src/terminal-list-cli-adapter.js.map +1 -0
  248. package/dist/src/terminal-list-renderer.d.ts +32 -0
  249. package/dist/src/terminal-list-renderer.js +568 -0
  250. package/dist/src/terminal-list-renderer.js.map +1 -0
  251. package/dist/src/terminal-maintenance-cli-adapter.d.ts +102 -0
  252. package/dist/src/terminal-maintenance-cli-adapter.js +1059 -0
  253. package/dist/src/terminal-maintenance-cli-adapter.js.map +1 -0
  254. package/dist/src/terminal-monitor-application-service.d.ts +275 -0
  255. package/dist/src/terminal-monitor-application-service.js +982 -0
  256. package/dist/src/terminal-monitor-application-service.js.map +1 -0
  257. package/dist/src/terminal-monitor-cli-adapter.d.ts +130 -0
  258. package/dist/src/terminal-monitor-cli-adapter.js +442 -0
  259. package/dist/src/terminal-monitor-cli-adapter.js.map +1 -0
  260. package/dist/src/terminal-monitor-decision-policy.d.ts +98 -0
  261. package/dist/src/terminal-monitor-decision-policy.js +246 -0
  262. package/dist/src/terminal-monitor-decision-policy.js.map +1 -0
  263. package/dist/src/terminal-monitor-launch-plan.d.ts +45 -0
  264. package/dist/src/terminal-monitor-launch-plan.js +94 -0
  265. package/dist/src/terminal-monitor-launch-plan.js.map +1 -0
  266. package/dist/src/terminal-monitor-ownership-policy.d.ts +31 -0
  267. package/dist/src/terminal-monitor-ownership-policy.js +64 -0
  268. package/dist/src/terminal-monitor-ownership-policy.js.map +1 -0
  269. package/dist/src/terminal-monitor-poll-policy.d.ts +48 -0
  270. package/dist/src/terminal-monitor-poll-policy.js +67 -0
  271. package/dist/src/terminal-monitor-poll-policy.js.map +1 -0
  272. package/dist/src/terminal-monitor-reconciliation-eligibility.d.ts +53 -0
  273. package/dist/src/terminal-monitor-reconciliation-eligibility.js +114 -0
  274. package/dist/src/terminal-monitor-reconciliation-eligibility.js.map +1 -0
  275. package/dist/src/terminal-monitor-state-cli-adapter.d.ts +106 -0
  276. package/dist/src/terminal-monitor-state-cli-adapter.js +1501 -0
  277. package/dist/src/terminal-monitor-state-cli-adapter.js.map +1 -0
  278. package/dist/src/terminal-monitor-state-reconciliation-service.d.ts +87 -0
  279. package/dist/src/terminal-monitor-state-reconciliation-service.js +106 -0
  280. package/dist/src/terminal-monitor-state-reconciliation-service.js.map +1 -0
  281. package/dist/src/terminal-monitor-supervision-cli-adapter.d.ts +122 -0
  282. package/dist/src/terminal-monitor-supervision-cli-adapter.js +685 -0
  283. package/dist/src/terminal-monitor-supervision-cli-adapter.js.map +1 -0
  284. package/dist/src/terminal-process-facts.d.ts +2 -0
  285. package/dist/src/terminal-process-facts.js +19 -0
  286. package/dist/src/terminal-process-facts.js.map +1 -0
  287. package/dist/src/terminal-process-source.d.ts +6 -4
  288. package/dist/src/terminal-process-source.js +25 -20
  289. package/dist/src/terminal-process-source.js.map +1 -1
  290. package/dist/src/terminal-runtime-cli-adapter.d.ts +80 -0
  291. package/dist/src/terminal-runtime-cli-adapter.js +488 -0
  292. package/dist/src/terminal-runtime-cli-adapter.js.map +1 -0
  293. package/dist/src/terminal-scoped-approval-authority.d.ts +62 -0
  294. package/dist/src/terminal-scoped-approval-authority.js +281 -0
  295. package/dist/src/terminal-scoped-approval-authority.js.map +1 -0
  296. package/dist/src/terminal-selector-projection-service.d.ts +26 -0
  297. package/dist/src/terminal-selector-projection-service.js +81 -0
  298. package/dist/src/terminal-selector-projection-service.js.map +1 -0
  299. package/dist/src/terminal-status-cli-adapter.d.ts +116 -0
  300. package/dist/src/terminal-status-cli-adapter.js +495 -0
  301. package/dist/src/terminal-status-cli-adapter.js.map +1 -0
  302. package/dist/src/terminal-status-facts.d.ts +56 -0
  303. package/dist/src/terminal-status-facts.js +199 -0
  304. package/dist/src/terminal-status-facts.js.map +1 -0
  305. package/dist/src/terminal-submission-acceptance.d.ts +6 -148
  306. package/dist/src/terminal-submission-acceptance.js +2 -229
  307. package/dist/src/terminal-submission-acceptance.js.map +1 -1
  308. package/dist/src/terminal-submission-facts.d.ts +123 -0
  309. package/dist/src/terminal-submission-facts.js +205 -0
  310. package/dist/src/terminal-submission-facts.js.map +1 -0
  311. package/dist/src/terminal-turn-binding-authority-cli-adapter.d.ts +14 -0
  312. package/dist/src/terminal-turn-binding-authority-cli-adapter.js +90 -0
  313. package/dist/src/terminal-turn-binding-authority-cli-adapter.js.map +1 -0
  314. package/dist/src/value-guards.d.ts +4 -0
  315. package/dist/src/value-guards.js +12 -0
  316. package/dist/src/value-guards.js.map +1 -0
  317. package/dist/src/verified-dead-agent-policy.d.ts +192 -0
  318. package/dist/src/verified-dead-agent-policy.js +262 -0
  319. package/dist/src/verified-dead-agent-policy.js.map +1 -0
  320. package/docs/quickstart-tmux.md +1 -1
  321. package/package.json +3 -1
  322. package/templates/openclaw-skills/agent-knock-knock/SKILL.md +1 -1
@@ -0,0 +1,3128 @@
1
+ // Raw CLI infrastructure for ordinary terminal send/respond/approval commands.
2
+ import { AsyncLocalStorage } from "node:async_hooks";
3
+ import { createHash, randomUUID } from "node:crypto";
4
+ import path from "node:path";
5
+ import { captureClaudeTranscriptAnchor, defaultClaudeHome } from "./claude-local-transcript-provider.js";
6
+ import { isRecord, nonBlankString as stringValue } from "./value-guards.js";
7
+ import { applyMessageToConversation, budgetAction, createMessage, effectiveTurnStatus, executorForConversation, isTerminalDispatchOwnerReleasedStatus, sessionIdForConversation, turnIdForConversation } from "./protocol.js";
8
+ import { readNdjsonLog } from "./transcript.js";
9
+ import { appendEvent, ensureDir, ensureStoreWritable, listConversations, loadState, messageEvent, pathsForConversationDir, saveState, withStoreWriterLeaseAsync } from "./store.js";
10
+ import { createManagedSessionId, managedSessionBindingToken } from "./managed-session.js";
11
+ import { loadManagedSession, saveManagedSession, tryLoadManagedSession } from "./session-store.js";
12
+ import { terminalControlEvidenceMatches } from "./terminal-control-ref.js";
13
+ import { TerminalInputNotStartedError } from "./terminal-agent-bridge.js";
14
+ import { evaluateApprovalPolicy } from "./approval-policy.js";
15
+ import { codexCompanionsPresentInOpenRootInventory, isCompleteNativeRollout, nativeIdentityMatchesCodexPreMaterialization, terminalControlsShareIncarnation } from "./terminal-authority-policy.js";
16
+ import { decideTerminalSendAuthority } from "./terminal-action-projection.js";
17
+ import { terminalScopedCodexApprovalPromptSnapshot } from "./terminal-scoped-approval-authority.js";
18
+ import { withCanonicalMutationLocks } from "./mutation-transaction.js";
19
+ import { assertTerminalDispatchRouteMatches, bindTerminalDispatchCapabilities, bindTerminalDispatchRoute, withExactTerminalDispatchRoute } from "./terminal-dispatch-capability.js";
20
+ import { claudeTranscriptApprovalIdentity, terminalMonitorDeadlineAt as deadlineAt, terminalMonitorScreenFingerprint as terminalBridgeScreenFingerprint, validTerminalMonitorTimestampMs as validTimestampMs } from "./terminal-monitor-decision-policy.js";
21
+ import * as monitorOwner from "./terminal-monitor-ownership-policy.js";
22
+ import { sameCanonicalStatePath, terminalDispatchLedgerLooksLifecycle, terminalDispatchReceiptHistory as terminalLedgerReceiptHistory } from "./terminal-dispatch-ledger-codec.js";
23
+ import * as dispatchApplication from "./terminal-dispatch-application.js";
24
+ import { bindDeferredForegroundApplicationScope, bindDeferredForegroundWriterScope } from "./deferred-foreground-capability.js";
25
+ import * as deferredRecoveryAdapter from "./deferred-foreground-recovery-cli-adapter.js";
26
+ import { deferredForegroundBoundaryProjection } from "./deferred-foreground-preparation-cli-adapter.js";
27
+ import { terminalSubmissionPayload } from "./terminal-dispatch-execution.js";
28
+ import { presentTerminalCompleted, presentTerminalDispatchReplay, presentTerminalIdentityFailure, presentTerminalUncertain, presentTerminalZeroInputAbort as renderTerminalZeroInputAbort } from "./terminal-dispatch-presenter.js";
29
+ import * as dispatchReceipt from "./terminal-dispatch-receipt.js";
30
+ import { expandHome, positiveMilliseconds, writeCliJson as printJson } from "./cli-command-runtime.js";
31
+ import { cliEnv, cliExit, cliNow, cliNowMs, cliPid, cliRuntimeLog as runtimeLog } from "./cli-runtime-context.js";
32
+ const terminalCommandContext = new AsyncLocalStorage();
33
+ function terminalCommandRuntime() {
34
+ const runtime = terminalCommandContext.getStore();
35
+ if (!runtime) {
36
+ throw new Error("Terminal command facade runtime is unavailable");
37
+ }
38
+ return runtime;
39
+ }
40
+ function rawPort(name) {
41
+ return ((...arguments_) => {
42
+ const operation = terminalCommandRuntime().ports[name];
43
+ if (typeof operation !== "function") {
44
+ throw new Error(`Terminal command port ${String(name)} is unavailable`);
45
+ }
46
+ return operation(...arguments_);
47
+ });
48
+ }
49
+ const acquireFileLock = rawPort("acquireFileLock");
50
+ const acquireTerminalBridgeSendLock = rawPort("acquireTerminalBridgeSendLock");
51
+ const assertCodexComposerReadyForAutomatedInput = rawPort("assertCodexComposerReadyForAutomatedInput");
52
+ const assertDeferredCodexForegroundBindingBoundary = rawPort("assertDeferredCodexForegroundBindingBoundary");
53
+ const assertExpectedHandoffTokenUsesExactTerminalSelector = rawPort("assertExpectedHandoffTokenUsesExactTerminalSelector");
54
+ const assertManagedSessionCanStartTurn = rawPort("assertManagedSessionCanStartTurn");
55
+ const assertManagedTerminalDispatchOwner = rawPort("assertManagedTerminalDispatchOwner");
56
+ const assertNativeAgentIdentityForTurn = rawPort("assertNativeAgentIdentityForTurn");
57
+ const assertNativeThreadHasExclusiveOwnership = rawPort("assertNativeThreadHasExclusiveOwnership");
58
+ const assertObservedHandoffTransportBoundary = rawPort("assertObservedHandoffTransportBoundary");
59
+ const assertSafeAbortedTerminalRetryBinding = rawPort("assertSafeAbortedTerminalRetryBinding");
60
+ const assertSafeTerminalSend = rawPort("assertSafeTerminalSend");
61
+ const assertVerifiedEmptyCodexTransportBoundary = rawPort("assertVerifiedEmptyCodexTransportBoundary");
62
+ const bindingMatchesLiveTerminal = rawPort("bindingMatchesLiveTerminal");
63
+ const codexAllowedCompanionSetForManagedSession = rawPort("codexAllowedCompanionSetForManagedSession");
64
+ const codexPreMaterializationIdentityForManagedSession = rawPort("codexPreMaterializationIdentityForManagedSession");
65
+ const createBoundManagedSession = rawPort("createBoundManagedSession");
66
+ const createManagedTerminalTurn = rawPort("createManagedTerminalTurn");
67
+ const createRuntimeTerminalAgentRegistry = rawPort("createRuntimeTerminalAgentRegistry");
68
+ const createTerminalAgentBridge = rawPort("createTerminalAgentBridge");
69
+ const deferredForegroundApplication = rawPort("deferredForegroundApplication");
70
+ const deferredForegroundRecoveryAdapterPorts = rawPort("deferredForegroundRecoveryAdapterPorts");
71
+ const ensureTerminalBridgeMonitorAfterApproval = rawPort("ensureTerminalBridgeMonitorAfterApproval");
72
+ const exactSafeAbortedRecoveredSessionMatches = rawPort("exactSafeAbortedRecoveredSessionMatches");
73
+ const inspectCodexOpenRootRolloutInventory = rawPort("inspectCodexOpenRootRolloutInventory");
74
+ const isDiscoverableTmuxConversation = rawPort("isDiscoverableTmuxConversation");
75
+ const loadClaudeAgentRows = rawPort("loadClaudeAgentRows");
76
+ const loadConversationFromOptions = rawPort("loadConversationFromOptions");
77
+ const loadTerminalBridgeDispatchLedger = rawPort("loadTerminalBridgeDispatchLedger");
78
+ const loadTerminalDispatchLedgerOwner = rawPort("loadTerminalDispatchLedgerOwner");
79
+ const logicalIdentityForManagedSession = rawPort("logicalIdentityForManagedSession");
80
+ const managedSessionStoreDirForConversation = rawPort("managedSessionStoreDirForConversation");
81
+ const managedTurnsForSession = rawPort("managedTurnsForSession");
82
+ const materializeCurrentManagedSession = rawPort("materializeCurrentManagedSession");
83
+ const maybeAdoptObservedExternalThread = rawPort("maybeAdoptObservedExternalThread");
84
+ const maybeDetachVerifiedEmptyCodexSource = rawPort("maybeDetachVerifiedEmptyCodexSource");
85
+ const migrateLegacyTerminalAgentIdentity = rawPort("migrateLegacyTerminalAgentIdentity");
86
+ const openClawYieldNextAction = rawPort("openClawYieldNextAction");
87
+ const observeCurrentNativeAgentSessionIdentity = rawPort("observeCurrentNativeAgentSessionIdentity");
88
+ const parseJsonOption = rawPort("parseJsonOption");
89
+ const persistManagedSessionNativeIdentity = rawPort("persistManagedSessionNativeIdentity");
90
+ const positiveMinutes = rawPort("positiveMinutes");
91
+ const prepareDeferredCodexForegroundBinding = rawPort("prepareDeferredCodexForegroundBinding");
92
+ const quarantineManagedSessionBinding = rawPort("quarantineManagedSessionBinding");
93
+ const reattachManagedSessionForNativeIdentity = rawPort("reattachManagedSessionForNativeIdentity");
94
+ const reconcilePreparedTerminalDispatchLedger = rawPort("reconcilePreparedTerminalDispatchLedger");
95
+ const refineManagedSessionNativeIdentity = rawPort("refineManagedSessionNativeIdentity");
96
+ const refineTerminalTurnEndpoint = rawPort("refineTerminalTurnEndpoint");
97
+ const required = rawPort("required");
98
+ const resolveCurrentNativeAgentSessionIdentity = rawPort("resolveCurrentNativeAgentSessionIdentity");
99
+ const resolveTerminalDispatchLedgerPaneIncarnation = rawPort("resolveTerminalDispatchLedgerPaneIncarnation");
100
+ const resolveTerminalConversationFromOptions = rawPort("resolveTerminalConversationFromOptions");
101
+ const stallOtherTerminalBridgeConversationsForUncertainDispatch = rawPort("stallOtherTerminalBridgeConversationsForUncertainDispatch");
102
+ const startTerminalBridgeMonitorForConversation = rawPort("startTerminalBridgeMonitorForConversation");
103
+ const storeDirFromOptions = rawPort("storeDirFromOptions");
104
+ const soleBoundManagedSessionClaimForTerminal = rawPort("soleBoundManagedSessionClaimForTerminal");
105
+ const terminalBindingLedgerFields = rawPort("terminalBindingLedgerFields");
106
+ const terminalBridgeEnabled = rawPort("terminalBridgeEnabled");
107
+ const terminalBridgeRequestFingerprint = rawPort("terminalBridgeRequestFingerprint");
108
+ const terminalBridgeRuntimeKey = rawPort("terminalBridgeRuntimeKey");
109
+ const terminalControlFromTakeover = rawPort("terminalControlFromTakeover");
110
+ const terminalDispatchCapabilityRepositories = rawPort("terminalDispatchCapabilityRepositories");
111
+ const terminalDispatchExecution = rawPort("terminalDispatchExecution");
112
+ const terminalDispatchRecordMatchesControl = rawPort("terminalDispatchRecordMatchesControl");
113
+ const terminalDurableRequestForConversation = rawPort("terminalDurableRequestForConversation");
114
+ const terminalRuntimeForLiveIdentity = rawPort("terminalRuntimeForLiveIdentity");
115
+ const terminalRuntimeIdentityForConversation = rawPort("terminalRuntimeIdentityForConversation");
116
+ const terminalWriterMutationLocks = rawPort("terminalWriterMutationLocks");
117
+ const textSummary = rawPort("textSummary");
118
+ const verifyCodexPendingManagedSendStatus = rawPort("verifyCodexPendingManagedSendStatus");
119
+ const withTerminalBridgeSubmission = rawPort("withTerminalBridgeSubmission");
120
+ const withTerminalDispatchStateScope = rawPort("withTerminalDispatchStateScope");
121
+ const mutationDispatchLedger = new Proxy({}, {
122
+ get: (_target, property) => terminalCommandRuntime().ports.mutationDispatchLedger[property]
123
+ });
124
+ const terminalListCliFacade = new Proxy({}, {
125
+ get: (_target, property) => terminalCommandRuntime().ports.terminalList[property]
126
+ });
127
+ const DEFAULT_AGENT_TIMEOUT_MINUTES = 60;
128
+ const DEFAULT_AGENT_HARD_TIMEOUT_MINUTES = 720;
129
+ const DEFAULT_TERMINAL_ACCEPTANCE_TIMEOUT_MS = 5000;
130
+ const DEFAULT_TERMINAL_ACCEPTANCE_POLL_INTERVAL_MS = 50;
131
+ const CLAUDE_SCREEN_APPROVAL_TTL_MS = 10 * 60 * 1000;
132
+ function replayReceiptBindingMismatch(receipt, expectation) {
133
+ return Boolean(!terminalDispatchRecordMatchesControl(receipt, expectation.terminalControl) ||
134
+ (stringValue(receipt.store_dir) !== undefined &&
135
+ path.resolve(String(receipt.store_dir)) !==
136
+ path.resolve(expectation.expectedStoreDir)) ||
137
+ (expectation.expectedSessionId &&
138
+ stringValue(receipt.session_id) !== undefined &&
139
+ stringValue(receipt.session_id) !== expectation.expectedSessionId) ||
140
+ (expectation.expectedTurnId &&
141
+ stringValue(receipt.turn_id) !== undefined &&
142
+ stringValue(receipt.turn_id) !== expectation.expectedTurnId) ||
143
+ (expectation.expectedStatePath &&
144
+ stringValue(receipt.state_path) !== undefined &&
145
+ !sameCanonicalStatePath(receipt.state_path, expectation.expectedStatePath)));
146
+ }
147
+ function replayReceiptPayloadMismatch(receipt, expectation, expectedMessageBodyHash) {
148
+ return ((stringValue(receipt.message_type) !== undefined &&
149
+ stringValue(receipt.message_type) !== expectation.expectedMessageType) ||
150
+ (stringValue(receipt.message_body_hash) !== undefined &&
151
+ stringValue(receipt.message_body_hash) !== expectedMessageBodyHash) ||
152
+ (stringValue(receipt.request_hash) !== undefined &&
153
+ stringValue(receipt.request_hash) !== expectation.requestHash) ||
154
+ (stringValue(expectation.options.openclawSession) !== undefined &&
155
+ stringValue(receipt.openclaw_session) !== undefined &&
156
+ stringValue(receipt.openclaw_session) !==
157
+ stringValue(expectation.options.openclawSession)));
158
+ }
159
+ function replayReceiptConflicts(receipt, expectation, expectedMessageBodyHash) {
160
+ return Boolean(receipt &&
161
+ !(receipt.status === "aborted" && receipt.safe_to_retry === true) &&
162
+ (replayReceiptBindingMismatch(receipt, expectation) ||
163
+ replayReceiptPayloadMismatch(receipt, expectation, expectedMessageBodyHash)));
164
+ }
165
+ function activeReplayLedgerBindingMismatch(ledger, expectation, storeDir, sessionId, statePath, matchesRecoveredSession) {
166
+ return Boolean(!terminalDispatchRecordMatchesControl(ledger, expectation.terminalControl) ||
167
+ (storeDir !== undefined &&
168
+ path.resolve(storeDir) !== path.resolve(expectation.expectedStoreDir)) ||
169
+ (expectation.expectedSessionId &&
170
+ sessionId !== undefined &&
171
+ sessionId !== expectation.expectedSessionId &&
172
+ !matchesRecoveredSession) ||
173
+ (expectation.expectedTurnId &&
174
+ stringValue(ledger.turn_id) !== undefined &&
175
+ stringValue(ledger.turn_id) !== expectation.expectedTurnId) ||
176
+ (expectation.expectedStatePath &&
177
+ statePath !== undefined &&
178
+ !sameCanonicalStatePath(statePath, expectation.expectedStatePath)));
179
+ }
180
+ function activeReplayLedgerPayloadMismatch(ledger, owner, expectation) {
181
+ return Boolean((stringValue(ledger.message_type) !== undefined &&
182
+ stringValue(ledger.message_type) !== expectation.expectedMessageType) ||
183
+ (stringValue(ledger.request_hash) !== undefined &&
184
+ stringValue(ledger.request_hash) !== expectation.requestHash) ||
185
+ (stringValue(expectation.options.openclawSession) !== undefined &&
186
+ owner !== undefined &&
187
+ owner.openclaw_session !==
188
+ stringValue(expectation.options.openclawSession)));
189
+ }
190
+ function activeReplayLedgerConflicts(input) {
191
+ return Boolean(input.ledger &&
192
+ input.ledgerMessageId === input.messageId &&
193
+ (activeReplayLedgerBindingMismatch(input.ledger, input.expectation, input.storeDir, input.sessionId, input.statePath, input.matchesRecoveredSession) ||
194
+ activeReplayLedgerPayloadMismatch(input.ledger, input.owner, input.expectation)));
195
+ }
196
+ function durableReceiptCannotDispatchAgain(receipt, ledger, ledgerMessageId, messageId) {
197
+ return Boolean(receipt &&
198
+ !(receipt.status === "aborted" && receipt.safe_to_retry === true) &&
199
+ (ledgerMessageId !== messageId ||
200
+ !["submitted", "enter_dispatched", "agent_accepted"].includes(String(receipt.status)) ||
201
+ !["submitted", "enter_dispatched", "agent_accepted"].includes(String(ledger?.status))));
202
+ }
203
+ function replayableReconciledLedger(ledger, expectation, messageId) {
204
+ if (!ledger ||
205
+ !["submitted", "enter_dispatched", "agent_accepted"].includes(String(ledger.status)) ||
206
+ !terminalDispatchRecordMatchesControl(ledger, expectation.terminalControl) ||
207
+ stringValue(ledger.message_id) !== messageId ||
208
+ (stringValue(ledger.message_type) !== undefined &&
209
+ stringValue(ledger.message_type) !== expectation.expectedMessageType) ||
210
+ stringValue(ledger.request_hash) !== expectation.requestHash) {
211
+ return undefined;
212
+ }
213
+ return ledger;
214
+ }
215
+ function activeReplayOwnerMismatch(input) {
216
+ const { owner, ledger, ledgerStoreDir, ownerStoreDir, expectation } = input;
217
+ return Boolean(!owner ||
218
+ isTerminalDispatchOwnerReleasedStatus(owner.status) ||
219
+ !ledgerStoreDir ||
220
+ !ownerStoreDir ||
221
+ path.resolve(ledgerStoreDir) !== path.resolve(expectation.expectedStoreDir) ||
222
+ path.resolve(ownerStoreDir) !== path.resolve(expectation.expectedStoreDir) ||
223
+ (expectation.expectedSessionId &&
224
+ sessionIdForConversation(owner) !== expectation.expectedSessionId) ||
225
+ (expectation.expectedTurnId &&
226
+ turnIdForConversation(owner) !== expectation.expectedTurnId) ||
227
+ (expectation.expectedStatePath &&
228
+ !sameCanonicalStatePath(ledger.state_path, expectation.expectedStatePath)) ||
229
+ (stringValue(expectation.options.openclawSession) &&
230
+ owner.openclaw_session !==
231
+ stringValue(expectation.options.openclawSession)));
232
+ }
233
+ function activeReplaySubmissionMismatch(input) {
234
+ return (stringValue(input.nativeTakeover?.terminal_bridge_message_id) !==
235
+ input.messageId ||
236
+ stringValue(input.submission?.message_id) !== input.messageId ||
237
+ (stringValue(input.submission?.message_type) !== undefined &&
238
+ stringValue(input.submission?.message_type) !==
239
+ input.expectation.expectedMessageType) ||
240
+ stringValue(input.submission?.request_hash) !== input.expectation.requestHash);
241
+ }
242
+ function replayLoggedMessageMismatch(loggedMessage, owner, expectedMessageType, requestText) {
243
+ return (loggedMessage.type !== expectedMessageType ||
244
+ loggedMessage.body !== requestText ||
245
+ loggedMessage.conversation_id !== owner.conversation_id ||
246
+ loggedMessage.session_id !== sessionIdForConversation(owner) ||
247
+ loggedMessage.turn_id !== turnIdForConversation(owner));
248
+ }
249
+ function replayExactActiveTerminalSubmission({ options, terminalControl, requestText, expectedStoreDir, expectedSessionId, expectedTurnId, expectedMessageType = "task", expectedStatePath }) {
250
+ const messageId = stringValue(options.messageId);
251
+ if (!messageId) {
252
+ return false;
253
+ }
254
+ const terminalPayload = terminalSubmissionPayload(requestText);
255
+ const requestHash = terminalBridgeRequestFingerprint(terminalPayload);
256
+ if (!requestHash) {
257
+ return false;
258
+ }
259
+ const loadedLedger = loadTerminalBridgeDispatchLedger(terminalControl);
260
+ const ledgerReceiptMatches = terminalLedgerReceiptHistory(loadedLedger)
261
+ .filter((receipt) => stringValue(receipt.message_id) === messageId);
262
+ if (ledgerReceiptMatches.length > 1) {
263
+ throw new Error(`terminal idempotency key ${messageId} has multiple terminal-ledger receipts`);
264
+ }
265
+ const ledgerReceipt = ledgerReceiptMatches[0];
266
+ const loadedLedgerMessageId = stringValue(loadedLedger?.message_id);
267
+ const loadedLedgerStoreDir = stringValue(loadedLedger?.store_dir);
268
+ const loadedLedgerStatePath = stringValue(loadedLedger?.state_path);
269
+ const loadedLedgerOwner = loadedLedger && loadedLedgerMessageId === messageId
270
+ ? loadTerminalDispatchLedgerOwner(loadedLedger)
271
+ : undefined;
272
+ const loadedLedgerSessionId = stringValue(loadedLedger?.session_id);
273
+ const loadedLedgerMatchesRecoveredSession = Boolean(expectedSessionId &&
274
+ loadedLedgerSessionId !== undefined &&
275
+ loadedLedgerSessionId !== expectedSessionId &&
276
+ loadedLedgerOwner &&
277
+ exactSafeAbortedRecoveredSessionMatches({
278
+ owner: loadedLedgerOwner,
279
+ storeDir: expectedStoreDir,
280
+ terminalControl,
281
+ messageId,
282
+ expectedSessionId
283
+ }));
284
+ const expectedMessageBodyHash = createHash("sha256")
285
+ .update(requestText)
286
+ .digest("hex");
287
+ const expectation = {
288
+ terminalControl,
289
+ expectedStoreDir,
290
+ expectedSessionId,
291
+ expectedTurnId,
292
+ expectedMessageType,
293
+ expectedStatePath,
294
+ requestHash,
295
+ options
296
+ };
297
+ if (replayReceiptConflicts(ledgerReceipt, expectation, expectedMessageBodyHash)) {
298
+ throw new Error(`terminal idempotency key ${messageId} does not match its durable ` +
299
+ "terminal receipt; no terminal input was sent");
300
+ }
301
+ if (activeReplayLedgerConflicts({
302
+ ledger: loadedLedger,
303
+ ledgerMessageId: loadedLedgerMessageId,
304
+ messageId,
305
+ owner: loadedLedgerOwner,
306
+ expectation,
307
+ storeDir: loadedLedgerStoreDir,
308
+ sessionId: loadedLedgerSessionId,
309
+ statePath: loadedLedgerStatePath,
310
+ matchesRecoveredSession: loadedLedgerMatchesRecoveredSession
311
+ })) {
312
+ throw new Error(`terminal idempotency key ${messageId} does not match its original ` +
313
+ "Store, Session, Turn, OpenClaw session, message, or terminal binding; " +
314
+ "no terminal input was sent");
315
+ }
316
+ if (replayExactStoredTerminalSubmission({
317
+ options,
318
+ terminalControl,
319
+ requestText,
320
+ requestHash,
321
+ messageId,
322
+ expectedStoreDir,
323
+ expectedSessionId,
324
+ expectedTurnId,
325
+ expectedMessageType,
326
+ expectedStatePath
327
+ })) {
328
+ return true;
329
+ }
330
+ if (durableReceiptCannotDispatchAgain(ledgerReceipt, loadedLedger, loadedLedgerMessageId, messageId)) {
331
+ throw new Error(`terminal idempotency key ${messageId} already has durable ` +
332
+ `${String(ledgerReceipt.status)} proof and cannot be dispatched again; ` +
333
+ "no terminal input was sent");
334
+ }
335
+ const incarnationLedger = resolveTerminalDispatchLedgerPaneIncarnation(terminalControl, loadedLedger);
336
+ const ledger = replayableReconciledLedger(reconcilePreparedTerminalDispatchLedger(terminalControl, incarnationLedger), expectation, messageId);
337
+ if (!ledger) {
338
+ return false;
339
+ }
340
+ const ownerCandidate = loadTerminalDispatchLedgerOwner(ledger);
341
+ const ledgerStoreDir = stringValue(ledger.store_dir);
342
+ const ownerStoreDir = ownerCandidate
343
+ ? managedSessionStoreDirForConversation(ownerCandidate)
344
+ : undefined;
345
+ if (activeReplayOwnerMismatch({
346
+ owner: ownerCandidate,
347
+ ledger,
348
+ ledgerStoreDir,
349
+ ownerStoreDir,
350
+ expectation
351
+ })) {
352
+ return false;
353
+ }
354
+ const owner = ownerCandidate;
355
+ const submission = terminalBridgeSubmission(owner);
356
+ const nativeTakeover = isRecord(owner.native_session_takeover)
357
+ ? owner.native_session_takeover
358
+ : undefined;
359
+ if (activeReplaySubmissionMismatch({
360
+ submission,
361
+ nativeTakeover,
362
+ messageId,
363
+ expectation
364
+ })) {
365
+ return false;
366
+ }
367
+ const executor = executorForConversation(owner);
368
+ const nativeThreadId = stringValue(owner.native_thread_id) ??
369
+ stringValue(nativeTakeover?.terminal_agent_session_id) ??
370
+ stringValue(nativeTakeover?.terminal_agent_expected_session_id) ??
371
+ sessionIdForConversation(owner);
372
+ const proofLevel = String(ledger.status);
373
+ const replayReceipt = dispatchReceipt.terminalSubmissionReplayReceipt({
374
+ proofLevel,
375
+ evidence: ledger.acceptance_evidence,
376
+ expected: {
377
+ source: executor.kind === "codex"
378
+ ? "codex_rollout"
379
+ : "claude_transcript",
380
+ nativeThreadId,
381
+ requestHash
382
+ }
383
+ });
384
+ const logPath = stringValue(ledger.event_log_path) ??
385
+ stringValue(owner.event_log_path);
386
+ let loggedMessage;
387
+ if (logPath) {
388
+ try {
389
+ loggedMessage = readNdjsonLog(logPath).find((event) => isRecord(event.message) && event.message.id === messageId)?.message;
390
+ }
391
+ catch {
392
+ loggedMessage = undefined;
393
+ }
394
+ }
395
+ if (isRecord(loggedMessage) &&
396
+ replayLoggedMessageMismatch(loggedMessage, owner, expectedMessageType, requestText)) {
397
+ return false;
398
+ }
399
+ const durableMessageType = stringValue(ledger.message_type) ??
400
+ stringValue(submission?.message_type) ??
401
+ (isRecord(loggedMessage) ? stringValue(loggedMessage.type) : undefined);
402
+ if (durableMessageType !== expectedMessageType) {
403
+ return false;
404
+ }
405
+ const replayedMessage = isRecord(loggedMessage)
406
+ ? loggedMessage
407
+ : createMessage({
408
+ conversation: owner,
409
+ id: messageId,
410
+ from: "openclaw",
411
+ to: executor.actor,
412
+ type: expectedMessageType,
413
+ body: requestText,
414
+ metadata: {
415
+ executor_kind: executor.kind,
416
+ executor_session: executor.session
417
+ }
418
+ });
419
+ const acceptanceInvalid = replayReceipt.submission_outcome === "uncertain";
420
+ printJson({
421
+ session_id: sessionIdForConversation(owner),
422
+ turn_id: turnIdForConversation(owner),
423
+ conversation: owner,
424
+ message: replayedMessage,
425
+ delivered: replayReceipt.delivered,
426
+ status: replayReceipt.status,
427
+ submission_outcome: replayReceipt.submission_outcome,
428
+ background: true,
429
+ callback_expected: !acceptanceInvalid && Boolean(owner.gateway_method ?? ledger.callback_expected),
430
+ terminal_control: terminalControl,
431
+ executor,
432
+ replayed: replayReceipt.replayed,
433
+ delivery_receipt: replayReceipt.delivery_receipt,
434
+ ...(replayReceipt.do_not_retry
435
+ ? { do_not_retry: replayReceipt.do_not_retry }
436
+ : {}),
437
+ reason: replayReceipt.delivered
438
+ ? "AKK replayed the durable native acceptance receipt and sent no additional terminal input."
439
+ : acceptanceInvalid
440
+ ? "AKK replayed an invalid native acceptance receipt as uncertain and sent no additional terminal input."
441
+ : "AKK replayed the original transport proof without upgrading it and sent no additional terminal input.",
442
+ openclaw_next_action: replayReceipt.delivered
443
+ ? openClawYieldNextAction({
444
+ conversationId: owner.conversation_id,
445
+ sessionId: sessionIdForConversation(owner),
446
+ turnId: turnIdForConversation(owner),
447
+ source: "terminal_control",
448
+ callbackExpected: Boolean(owner.gateway_method ?? ledger.callback_expected)
449
+ })
450
+ : {
451
+ action: "inspect",
452
+ conversation_id: owner.conversation_id,
453
+ session_id: sessionIdForConversation(owner),
454
+ turn_id: turnIdForConversation(owner),
455
+ do_not_retry: true,
456
+ reason: acceptanceInvalid
457
+ ? "Stored native acceptance evidence is invalid."
458
+ : "Only terminal transport is proven."
459
+ }
460
+ });
461
+ return true;
462
+ }
463
+ function storedReceiptTerminalBoundaryMismatch(input) {
464
+ return (!input.ownerStoreDir ||
465
+ !input.receiptStoreDir ||
466
+ path.resolve(input.ownerStoreDir) !== path.resolve(input.expectedStoreDir) ||
467
+ path.resolve(input.receiptStoreDir) !== path.resolve(input.expectedStoreDir) ||
468
+ !input.storedControl ||
469
+ !terminalControlsShareIncarnation(input.storedControl, input.terminalControl) ||
470
+ !terminalControlEvidenceMatches(input.receiptTerminalEvidence, input.terminalControl));
471
+ }
472
+ function storedReceiptAuthorityMismatch(input) {
473
+ return (input.receiptSessionId !== sessionIdForConversation(input.owner) ||
474
+ input.receiptTurnId !== turnIdForConversation(input.owner) ||
475
+ (input.expectedSessionId &&
476
+ input.receiptSessionId !== input.expectedSessionId &&
477
+ !input.receiptMatchesRecoveredSession) ||
478
+ (input.expectedTurnId && input.receiptTurnId !== input.expectedTurnId) ||
479
+ (input.expectedStatePath &&
480
+ !sameCanonicalStatePath(input.owner.state_path, input.expectedStatePath)) ||
481
+ (input.requestedOpenClawSession &&
482
+ input.receiptOpenClawSession !== input.requestedOpenClawSession) ||
483
+ stringValue(input.receipt.request_hash) !== input.requestHash ||
484
+ (stringValue(input.receipt.executor_kind) !== undefined &&
485
+ stringValue(input.receipt.executor_kind) !==
486
+ executorForConversation(input.owner).kind));
487
+ }
488
+ function validateStoredTerminalSubmissionMatch({ owner, receipt, options, terminalControl, requestText, requestHash, expectedStoreDir, expectedSessionId, expectedTurnId, expectedMessageType, expectedStatePath }) {
489
+ const messageId = required(stringValue(receipt.message_id), "stored terminal receipt message id is required");
490
+ const ownerStoreDir = managedSessionStoreDirForConversation(owner);
491
+ const nativeTakeover = isRecord(owner.native_session_takeover)
492
+ ? owner.native_session_takeover
493
+ : undefined;
494
+ const storedControl = terminalControlFromTakeover(nativeTakeover);
495
+ const receiptStoreDir = stringValue(receipt.store_dir) ?? ownerStoreDir;
496
+ const receiptSessionId = stringValue(receipt.session_id) ??
497
+ sessionIdForConversation(owner);
498
+ const receiptMatchesRecoveredSession = Boolean(expectedSessionId &&
499
+ receiptSessionId !== expectedSessionId &&
500
+ exactSafeAbortedRecoveredSessionMatches({
501
+ owner,
502
+ receipt,
503
+ storeDir: expectedStoreDir,
504
+ terminalControl,
505
+ messageId,
506
+ expectedSessionId
507
+ }));
508
+ const receiptTurnId = stringValue(receipt.turn_id) ??
509
+ turnIdForConversation(owner);
510
+ const receiptOpenClawSession = stringValue(receipt.openclaw_session) ??
511
+ owner.openclaw_session;
512
+ const receiptTerminalEvidence = receipt.terminal_endpoint !== undefined
513
+ ? receipt.terminal_endpoint
514
+ : {
515
+ kind: "tmux",
516
+ target: stringValue(receipt.terminal_target) ?? storedControl?.target,
517
+ socket_path: receipt.terminal_socket_path === null
518
+ ? null
519
+ : stringValue(receipt.terminal_socket_path) ??
520
+ storedControl?.socketPath ??
521
+ null,
522
+ pane_pid: Number(receipt.terminal_pane_pid ?? storedControl?.panePid)
523
+ };
524
+ const requestedOpenClawSession = stringValue(options.openclawSession);
525
+ if (storedReceiptTerminalBoundaryMismatch({
526
+ ownerStoreDir,
527
+ receiptStoreDir,
528
+ expectedStoreDir,
529
+ storedControl,
530
+ terminalControl,
531
+ receiptTerminalEvidence
532
+ }) ||
533
+ storedReceiptAuthorityMismatch({
534
+ owner,
535
+ receipt,
536
+ receiptSessionId,
537
+ receiptTurnId,
538
+ receiptOpenClawSession,
539
+ receiptMatchesRecoveredSession,
540
+ expectedSessionId,
541
+ expectedTurnId,
542
+ expectedStatePath,
543
+ requestedOpenClawSession,
544
+ requestHash
545
+ })) {
546
+ throw new Error(`terminal idempotency key ${messageId} does not match its original ` +
547
+ "Store, Session, Turn, OpenClaw session, or terminal binding; no terminal input was sent");
548
+ }
549
+ const logPath = stringValue(owner.event_log_path);
550
+ let loggedMessages = [];
551
+ if (logPath) {
552
+ try {
553
+ loggedMessages = readNdjsonLog(logPath)
554
+ .filter((event) => isRecord(event.message) && event.message.id === messageId)
555
+ .map((event) => event.message);
556
+ }
557
+ catch {
558
+ loggedMessages = [];
559
+ }
560
+ }
561
+ if (loggedMessages.length > 1) {
562
+ throw new Error(`terminal idempotency key ${messageId} has duplicate durable messages`);
563
+ }
564
+ const loggedMessage = loggedMessages[0];
565
+ if (isRecord(loggedMessage) &&
566
+ replayLoggedMessageMismatch(loggedMessage, owner, expectedMessageType, requestText)) {
567
+ throw new Error(`terminal idempotency key ${messageId} does not match its durable message; no terminal input was sent`);
568
+ }
569
+ const durableMessageType = stringValue(receipt.message_type) ??
570
+ (isRecord(loggedMessage) ? stringValue(loggedMessage.type) : undefined);
571
+ const durableMessageBodyHash = stringValue(receipt.message_body_hash) ??
572
+ (isRecord(loggedMessage) && typeof loggedMessage.body === "string"
573
+ ? createHash("sha256").update(loggedMessage.body).digest("hex")
574
+ : undefined);
575
+ const expectedMessageBodyHash = createHash("sha256")
576
+ .update(requestText)
577
+ .digest("hex");
578
+ if (durableMessageType !== expectedMessageType ||
579
+ durableMessageBodyHash !== expectedMessageBodyHash) {
580
+ throw new Error(`terminal idempotency key ${messageId} was already used for a different message; no terminal input was sent`);
581
+ }
582
+ return loggedMessage;
583
+ }
584
+ function replayExactStoredTerminalSubmission({ options, terminalControl, requestText, requestHash, messageId, expectedStoreDir, expectedSessionId, expectedTurnId, expectedMessageType, expectedStatePath }) {
585
+ const allMatches = listConversations(expectedStoreDir).flatMap((owner) => terminalBridgeSubmissionReceipts(owner)
586
+ .filter((receipt) => stringValue(receipt.message_id) === messageId)
587
+ .map((receipt) => ({ owner, receipt })));
588
+ const validatedMatches = allMatches.map((match) => ({
589
+ ...match,
590
+ loggedMessage: validateStoredTerminalSubmissionMatch({
591
+ ...match,
592
+ options,
593
+ terminalControl,
594
+ requestText,
595
+ requestHash,
596
+ expectedStoreDir,
597
+ expectedSessionId,
598
+ expectedTurnId,
599
+ expectedMessageType,
600
+ expectedStatePath
601
+ })
602
+ }));
603
+ const matches = validatedMatches.filter(({ receipt }) => !(receipt.status === "aborted" && receipt.safe_to_retry === true));
604
+ if (matches.length > 1) {
605
+ throw new Error(`terminal idempotency key ${messageId} has multiple durable receipts in one Store`);
606
+ }
607
+ const match = matches[0];
608
+ if (!match) {
609
+ for (const { owner, receipt } of validatedMatches) {
610
+ if (expectedSessionId) {
611
+ if (!exactSafeAbortedRecoveredSessionMatches({
612
+ owner,
613
+ receipt,
614
+ storeDir: expectedStoreDir,
615
+ terminalControl,
616
+ messageId,
617
+ expectedSessionId
618
+ })) {
619
+ throw new Error(`terminal idempotency key ${messageId} does not match its ` +
620
+ "restored retry Session; no terminal input was sent");
621
+ }
622
+ }
623
+ else {
624
+ assertSafeAbortedTerminalRetryBinding({
625
+ owner,
626
+ receipt,
627
+ storeDir: expectedStoreDir,
628
+ terminalControl,
629
+ messageId
630
+ });
631
+ }
632
+ }
633
+ // A prepared-stage failure with a restored ledger proves that tmux was
634
+ // untouched. The same exact id may therefore start a fresh attempt; all
635
+ // immutable boundaries above were still validated before allowing it.
636
+ return false;
637
+ }
638
+ const { owner, receipt, loggedMessage } = match;
639
+ const nativeTakeover = isRecord(owner.native_session_takeover)
640
+ ? owner.native_session_takeover
641
+ : undefined;
642
+ const currentSubmission = terminalBridgeSubmission(owner);
643
+ const isCurrentSubmission = stringValue(currentSubmission?.message_id) === messageId;
644
+ if (isCurrentSubmission &&
645
+ !isTerminalDispatchOwnerReleasedStatus(owner.status)) {
646
+ return false;
647
+ }
648
+ if (!["submitted", "enter_dispatched", "agent_accepted"].includes(String(receipt.status))) {
649
+ throw new Error(`terminal idempotency key ${messageId} has durable ${String(receipt.status)} ` +
650
+ "proof and must not be retried; no terminal input was sent");
651
+ }
652
+ const executor = executorForConversation(owner);
653
+ const nativeThreadId = stringValue(receipt.native_thread_id) ??
654
+ stringValue(owner.native_thread_id) ??
655
+ stringValue(nativeTakeover?.terminal_agent_session_id) ??
656
+ stringValue(nativeTakeover?.terminal_agent_expected_session_id) ??
657
+ sessionIdForConversation(owner);
658
+ const proofLevel = String(receipt.status);
659
+ const replayReceipt = dispatchReceipt.terminalSubmissionReplayReceipt({
660
+ proofLevel,
661
+ evidence: receipt.acceptance_evidence,
662
+ expected: {
663
+ source: executor.kind === "codex"
664
+ ? "codex_rollout"
665
+ : "claude_transcript",
666
+ nativeThreadId,
667
+ requestHash
668
+ }
669
+ });
670
+ const replayedMessage = isRecord(loggedMessage)
671
+ ? loggedMessage
672
+ : createMessage({
673
+ conversation: owner,
674
+ id: messageId,
675
+ from: "openclaw",
676
+ to: executor.actor,
677
+ type: expectedMessageType,
678
+ body: requestText,
679
+ metadata: {
680
+ executor_kind: executor.kind,
681
+ executor_session: executor.session
682
+ }
683
+ });
684
+ const acceptanceInvalid = replayReceipt.submission_outcome === "uncertain";
685
+ const callbackExpected = Boolean(owner.gateway_method);
686
+ printJson({
687
+ session_id: sessionIdForConversation(owner),
688
+ turn_id: turnIdForConversation(owner),
689
+ conversation: owner,
690
+ message: replayedMessage,
691
+ delivered: replayReceipt.delivered,
692
+ status: replayReceipt.status,
693
+ submission_outcome: replayReceipt.submission_outcome,
694
+ background: true,
695
+ callback_expected: !acceptanceInvalid && callbackExpected,
696
+ terminal_control: terminalControl,
697
+ executor,
698
+ replayed: true,
699
+ delivery_receipt: replayReceipt.delivery_receipt,
700
+ ...(replayReceipt.do_not_retry
701
+ ? { do_not_retry: replayReceipt.do_not_retry }
702
+ : {}),
703
+ reason: replayReceipt.delivered
704
+ ? "AKK replayed the stored durable native acceptance receipt and sent no additional terminal input."
705
+ : acceptanceInvalid
706
+ ? "AKK replayed an invalid stored native acceptance receipt as uncertain and sent no additional terminal input."
707
+ : "AKK replayed the stored transport proof without upgrading it and sent no additional terminal input.",
708
+ openclaw_next_action: replayReceipt.delivered
709
+ ? openClawYieldNextAction({
710
+ conversationId: owner.conversation_id,
711
+ sessionId: sessionIdForConversation(owner),
712
+ turnId: turnIdForConversation(owner),
713
+ source: "terminal_control",
714
+ callbackExpected
715
+ })
716
+ : {
717
+ action: "inspect",
718
+ conversation_id: owner.conversation_id,
719
+ session_id: sessionIdForConversation(owner),
720
+ turn_id: turnIdForConversation(owner),
721
+ do_not_retry: true,
722
+ reason: acceptanceInvalid
723
+ ? "Stored native acceptance evidence is invalid."
724
+ : "Only terminal transport is proven."
725
+ }
726
+ });
727
+ return true;
728
+ }
729
+ const terminalBridgeSubmission = dispatchReceipt.terminalBridgeSubmission;
730
+ const terminalBridgeSubmissionReceipts = dispatchReceipt.terminalBridgeSubmissionReceipts;
731
+ const unresolvedTerminalBridgeSubmission = dispatchReceipt.unresolvedTerminalBridgeSubmission;
732
+ function assertNoUnresolvedTerminalBridgeSubmission(storeDir, terminalControl, currentConversationId, requestText) {
733
+ const requestHash = terminalBridgeRequestFingerprint(requestText);
734
+ for (const candidate of listConversations(storeDir)) {
735
+ const submission = terminalBridgeSubmission(candidate);
736
+ if (candidate.conversation_id === currentConversationId ||
737
+ !submission ||
738
+ isTerminalDispatchOwnerReleasedStatus(effectiveTurnStatus(candidate)) ||
739
+ ![
740
+ "prepared",
741
+ "text_injected",
742
+ "enter_dispatched",
743
+ "agent_accepted",
744
+ "submitted",
745
+ "not_accepted",
746
+ "uncertain"
747
+ ].includes(String(submission.status))) {
748
+ continue;
749
+ }
750
+ if (["submitted", "agent_accepted"].includes(String(submission.status)) &&
751
+ stringValue(submission.request_hash) !== requestHash) {
752
+ continue;
753
+ }
754
+ const nativeTakeover = isRecord(candidate.native_session_takeover)
755
+ ? candidate.native_session_takeover
756
+ : undefined;
757
+ if (terminalControlsShareIncarnation(terminalControlFromTakeover(nativeTakeover), terminalControl)) {
758
+ throw new Error(`terminal ${terminalControl.target} has a conflicting ${String(submission.status)} ` +
759
+ `AKK submission in ${candidate.conversation_id}; inspect that conversation and pane, ` +
760
+ "then close it before retrying");
761
+ }
762
+ }
763
+ }
764
+ function prepareManagedSend({ options, statePath, logPath, messageBody, stateLockHeld = false, persist = true, rejectTerminalControl = false }) {
765
+ if (!stateLockHeld) {
766
+ const releaseLock = acquireFileLock(`${statePath}.lock`);
767
+ try {
768
+ return prepareManagedSend({
769
+ options,
770
+ statePath,
771
+ logPath,
772
+ messageBody,
773
+ stateLockHeld: true,
774
+ persist,
775
+ rejectTerminalControl
776
+ });
777
+ }
778
+ finally {
779
+ releaseLock();
780
+ }
781
+ }
782
+ const conversation = loadState(statePath);
783
+ if (conversation.status !== "waiting_for_openclaw" ||
784
+ options.type !== "answer") {
785
+ throw new Error(`cannot respond to turn ${turnIdForConversation(conversation)}; ` +
786
+ `turn is ${conversation.status}`);
787
+ }
788
+ const executor = executorForConversation(conversation);
789
+ const type = "answer";
790
+ const nativeTakeoverForSend = isRecord(conversation.native_session_takeover)
791
+ ? conversation.native_session_takeover
792
+ : undefined;
793
+ const unresolvedSubmission = unresolvedTerminalBridgeSubmission(conversation);
794
+ if (unresolvedSubmission) {
795
+ throw new Error(`cannot send to ${conversation.conversation_id}; its previous terminal submission is ` +
796
+ `${unresolvedSubmission.status}. Inspect the conversation and terminal pane, then close ` +
797
+ "the AKK conversation before creating a replacement task.");
798
+ }
799
+ if (rejectTerminalControl &&
800
+ terminalControlFromTakeover(nativeTakeoverForSend)) {
801
+ throw new Error("terminal control changed while waiting to send; refresh status and retry");
802
+ }
803
+ const message = createMessage({
804
+ conversation,
805
+ id: stringValue(options.messageId),
806
+ from: "openclaw",
807
+ to: executor.actor,
808
+ type,
809
+ body: messageBody,
810
+ metadata: {
811
+ executor_kind: executor.kind,
812
+ executor_session: executor.session
813
+ }
814
+ });
815
+ const nextConversation = {
816
+ ...applyMessageToConversation(conversation, message),
817
+ executor,
818
+ claude_session: executor.kind === "claude"
819
+ ? executor.session
820
+ : conversation.claude_session
821
+ };
822
+ if (persist) {
823
+ saveState(statePath, nextConversation);
824
+ appendEvent(logPath, messageEvent(message));
825
+ runtimeLog("info", "message_created", {
826
+ conversation_id: conversation.conversation_id,
827
+ agent: executor.kind,
828
+ executor_session: executor.session,
829
+ message_type: type,
830
+ state_path: statePath,
831
+ event_log_path: logPath,
832
+ message: textSummary(messageBody)
833
+ });
834
+ }
835
+ return {
836
+ conversation,
837
+ executor,
838
+ nativeTakeoverForSend,
839
+ message,
840
+ nextConversation
841
+ };
842
+ }
843
+ async function runSend(options) {
844
+ const messageBody = required(options.message ?? options.request, "--message is required");
845
+ // Ordinary send/respond owns Turn creation only. Reject native lifecycle
846
+ // slash commands before resolving or mutating any Session/Turn state so a
847
+ // caller cannot bypass lifecycle capabilities, CAS tokens, transition
848
+ // recovery, or binding-generation updates by writing directly to tmux.
849
+ terminalSubmissionPayload(messageBody);
850
+ if (options.agentHardTimeoutMinutes !== undefined) {
851
+ positiveMinutes(options.agentHardTimeoutMinutes, "--agent-hard-timeout-minutes");
852
+ }
853
+ if (options.respond === true) {
854
+ return runTurnResponse({ options, messageBody });
855
+ }
856
+ if ((options.type ?? "task") !== "task") {
857
+ throw new Error("ordinary send only accepts message type task; use respond --turn to answer an in-flight Turn");
858
+ }
859
+ const terminalConversation = await resolveTerminalConversationFromOptions(options);
860
+ if (terminalConversation) {
861
+ return runRawTerminalSend(options, messageBody, terminalConversation);
862
+ }
863
+ return runManagedSessionSend(options, messageBody);
864
+ }
865
+ function rawTerminalInitialAuthority({ options, terminal, storeDir }) {
866
+ const claimedSession = soleBoundManagedSessionClaimForTerminal(storeDir, terminal);
867
+ const suppliedExpectedTerminalToken = stringValue(options.expectedTerminalToken);
868
+ const implicitCodexCandidateAuthority = Boolean(terminal.agent === "codex" &&
869
+ suppliedExpectedTerminalToken === undefined &&
870
+ claimedSession?.status === "bound" &&
871
+ isCompleteNativeRollout(claimedSession.binding?.native_process.rollout));
872
+ const knownCodexCompanions = claimedSession
873
+ ? codexAllowedCompanionSetForManagedSession({
874
+ storeDir,
875
+ session: claimedSession
876
+ })
877
+ : { additional: [] };
878
+ return {
879
+ claimedSession,
880
+ suppliedExpectedTerminalToken,
881
+ implicitCodexCandidateAuthority,
882
+ knownCodexCompanions
883
+ };
884
+ }
885
+ function assertRawTerminalCandidateAuthority({ terminal, nativeIdentityObservation, deferredCodexCandidateInventory, implicitCodexCandidateAuthority }) {
886
+ if (implicitCodexCandidateAuthority &&
887
+ !deferredCodexCandidateInventory) {
888
+ throw new Error("rollout-backed Codex terminal send requires a fresh complete " +
889
+ "nonempty open-root inventory; refresh AKK list before sending");
890
+ }
891
+ if (nativeIdentityObservation.status === "unavailable" &&
892
+ !deferredCodexCandidateInventory) {
893
+ throw new Error(`native ${terminal.agent} identity observation is ` +
894
+ `unavailable: ${nativeIdentityObservation.reason}`);
895
+ }
896
+ }
897
+ async function runRawTerminalSend(options, messageBody, terminalConversation) {
898
+ // A token copied from list is authority for exactly the advertised full
899
+ // terminal selector. Reject aliases and implicit/no-selector resolution
900
+ // before taking locks or touching Store state.
901
+ assertExpectedHandoffTokenUsesExactTerminalSelector({
902
+ options,
903
+ terminal: terminalConversation
904
+ });
905
+ if (!options.background) {
906
+ throw new Error("raw terminal sends require --background so AKK can persist and monitor the submission safely");
907
+ }
908
+ const rawStoreDir = storeDirFromOptions(options);
909
+ await withCanonicalMutationLocks(terminalWriterMutationLocks(rawStoreDir, terminalConversation.terminalControl), async (scopes, resources) => {
910
+ await mutationDispatchLedger.beforeMutation(scopes, resources, options, terminalConversation);
911
+ // Upgrade legacy Stores before resolving their Session authority. The
912
+ // protocol migration, rather than mutable Turn recency, is the only code
913
+ // allowed to materialize Session records from existing Turns.
914
+ ensureStoreWritable(rawStoreDir);
915
+ if (replayExactActiveTerminalSubmission({
916
+ options,
917
+ terminalControl: terminalConversation.terminalControl,
918
+ requestText: String(messageBody),
919
+ expectedStoreDir: rawStoreDir,
920
+ expectedMessageType: "task"
921
+ })) {
922
+ return;
923
+ }
924
+ terminalListCliFacade.assertTerminalIncarnationCanStartTurn(rawStoreDir, terminalConversation.terminalControl);
925
+ const initialAuthority = rawTerminalInitialAuthority({
926
+ options,
927
+ terminal: terminalConversation,
928
+ storeDir: rawStoreDir
929
+ });
930
+ let { claimedSession, knownCodexCompanions } = initialAuthority;
931
+ const { suppliedExpectedTerminalToken, implicitCodexCandidateAuthority } = initialAuthority;
932
+ const nativeIdentityObservation = await observeCurrentNativeAgentSessionIdentity({
933
+ options,
934
+ agent: terminalConversation.agent,
935
+ pid: terminalConversation.pid,
936
+ cwd: terminalConversation.terminalControl.currentPath,
937
+ preferredSessionId: knownCodexCompanions.primary
938
+ ? claimedSession?.binding?.native_thread_id
939
+ : undefined,
940
+ allowedCompanionIdentity: knownCodexCompanions.primary,
941
+ allowedAdditionalIdentities: knownCodexCompanions.additional
942
+ });
943
+ let deferredCodexCandidateInventory;
944
+ if (terminalConversation.agent === "codex" &&
945
+ (suppliedExpectedTerminalToken || implicitCodexCandidateAuthority)) {
946
+ try {
947
+ const inventory = await inspectCodexOpenRootRolloutInventory({
948
+ options,
949
+ pid: terminalConversation.pid,
950
+ cwd: terminalConversation.terminalControl.currentPath
951
+ });
952
+ if (inventory.roots.length > 0) {
953
+ deferredCodexCandidateInventory = inventory;
954
+ }
955
+ }
956
+ catch (error) {
957
+ if (implicitCodexCandidateAuthority ||
958
+ nativeIdentityObservation.status === "unavailable") {
959
+ throw new Error(`native Codex foreground attribution requires a fresh complete ` +
960
+ `open-root inventory: ${error instanceof Error ? error.message : String(error)}`);
961
+ }
962
+ }
963
+ }
964
+ assertRawTerminalCandidateAuthority({
965
+ terminal: terminalConversation,
966
+ nativeIdentityObservation,
967
+ deferredCodexCandidateInventory,
968
+ implicitCodexCandidateAuthority
969
+ });
970
+ let currentNativeIdentity = nativeIdentityObservation.status === "resolved"
971
+ ? nativeIdentityObservation.identity
972
+ : undefined;
973
+ // A fresh nonempty inventory is the stronger physical authority for an
974
+ // implicit candidate send. Do not let an earlier verified-absent
975
+ // observation divert this path into the token-only empty handoff.
976
+ const verifiedEmptyHandoff = implicitCodexCandidateAuthority
977
+ ? undefined
978
+ : await maybeDetachVerifiedEmptyCodexSource({
979
+ options,
980
+ terminal: terminalConversation,
981
+ sourceSession: claimedSession,
982
+ observation: nativeIdentityObservation
983
+ });
984
+ if (verifiedEmptyHandoff) {
985
+ // The old rollout is conclusively closed. Never carry it forward as
986
+ // a pre-materialization companion for the new virgin Session.
987
+ claimedSession = undefined;
988
+ knownCodexCompanions = { additional: [] };
989
+ currentNativeIdentity = undefined;
990
+ }
991
+ const physicalNativeIdentityBeforeHandoff = currentNativeIdentity;
992
+ let handoff = await maybeAdoptObservedExternalThread({
993
+ options,
994
+ terminal: terminalConversation,
995
+ // A no-token raw send to an already managed rollout-backed Codex pane
996
+ // is an internal follow-current delegation, never a sole-root strict
997
+ // continuation or an external-handoff adoption. The dedicated v3
998
+ // transfer below owns attribution and lineage.
999
+ sourceSession: implicitCodexCandidateAuthority
1000
+ ? undefined
1001
+ : claimedSession,
1002
+ resolvedIdentity: currentNativeIdentity,
1003
+ storeDir: rawStoreDir
1004
+ });
1005
+ currentNativeIdentity =
1006
+ handoff.adopted &&
1007
+ terminalConversation.agent === "codex" &&
1008
+ !handoff.session?.binding?.native_process.rollout
1009
+ ? physicalNativeIdentityBeforeHandoff
1010
+ : handoff.identity;
1011
+ const deferredCodexForegroundBinding = !handoff.adopted
1012
+ ? await prepareDeferredCodexForegroundBinding({
1013
+ options,
1014
+ scope: bindDeferredForegroundWriterScope(scopes, resources),
1015
+ terminal: terminalConversation,
1016
+ sourceSession: claimedSession,
1017
+ observation: nativeIdentityObservation,
1018
+ candidateInventory: deferredCodexCandidateInventory,
1019
+ requestText: String(messageBody),
1020
+ allowImplicitFreshAuthority: implicitCodexCandidateAuthority
1021
+ })
1022
+ : undefined;
1023
+ const freshSendAuthority = decideTerminalSendAuthority({
1024
+ ownership: "conflict",
1025
+ verifiedEmpty: Boolean(verifiedEmptyHandoff),
1026
+ externalHandoff: handoff.adopted,
1027
+ deferred: Boolean(deferredCodexForegroundBinding)
1028
+ });
1029
+ if (freshSendAuthority.mode === "deferred" &&
1030
+ deferredCodexForegroundBinding) {
1031
+ claimedSession = undefined;
1032
+ knownCodexCompanions = { additional: [] };
1033
+ currentNativeIdentity = undefined;
1034
+ handoff = { identity: undefined, adopted: false };
1035
+ }
1036
+ else if ((suppliedExpectedTerminalToken || implicitCodexCandidateAuthority) &&
1037
+ freshSendAuthority.mode === "conflict") {
1038
+ throw new Error("the expected terminal token no longer authorizes the current " +
1039
+ "terminal context; refresh AKK list");
1040
+ }
1041
+ let managedSession = deferredCodexForegroundBinding
1042
+ ? undefined
1043
+ : handoff.session ??
1044
+ materializeCurrentManagedSession({
1045
+ options,
1046
+ terminal: terminalConversation,
1047
+ identity: currentNativeIdentity
1048
+ });
1049
+ if (!managedSession && currentNativeIdentity) {
1050
+ managedSession = await reattachManagedSessionForNativeIdentity({
1051
+ options,
1052
+ terminal: terminalConversation,
1053
+ identity: currentNativeIdentity,
1054
+ storeDir: rawStoreDir
1055
+ });
1056
+ }
1057
+ let pendingRawAttachSessionCreate;
1058
+ if (!managedSession) {
1059
+ if (currentNativeIdentity) {
1060
+ await assertNativeThreadHasExclusiveOwnership({
1061
+ options,
1062
+ agent: terminalConversation.agent,
1063
+ currentPid: terminalConversation.pid,
1064
+ nativeThreadId: currentNativeIdentity.sessionId,
1065
+ storeDir: rawStoreDir,
1066
+ terminalControl: terminalConversation.terminalControl
1067
+ });
1068
+ }
1069
+ managedSession = createBoundManagedSession({
1070
+ sessionId: deferredCodexForegroundBinding?.targetSessionId ??
1071
+ createManagedSessionId(),
1072
+ terminal: terminalConversation,
1073
+ identity: currentNativeIdentity,
1074
+ lineage: deferredCodexForegroundBinding
1075
+ ? {
1076
+ created_by: "attach",
1077
+ previous_session_id: deferredCodexForegroundBinding.sourceSessionId,
1078
+ transition_id: deferredCodexForegroundBinding.transferId
1079
+ }
1080
+ : { created_by: "attach" }
1081
+ });
1082
+ if (terminalConversation.agent === "codex" &&
1083
+ !currentNativeIdentity) {
1084
+ if (deferredCodexForegroundBinding) {
1085
+ managedSession = {
1086
+ ...managedSession,
1087
+ // Both endpoints stay fenced from older writers until the
1088
+ // dedicated transfer reaches its resolved receipt.
1089
+ status: "transitioning",
1090
+ last_transition_id: deferredCodexForegroundBinding.transferId
1091
+ };
1092
+ }
1093
+ pendingRawAttachSessionCreate = managedSession;
1094
+ }
1095
+ else {
1096
+ managedSession = saveManagedSession(rawStoreDir, managedSession, { expectedRevision: null });
1097
+ }
1098
+ }
1099
+ const logicalNativeIdentity = logicalIdentityForManagedSession({
1100
+ storeDir: rawStoreDir,
1101
+ session: managedSession,
1102
+ observedIdentity: currentNativeIdentity
1103
+ });
1104
+ const allowedPreMaterializationIdentity = codexPreMaterializationIdentityForManagedSession({
1105
+ storeDir: rawStoreDir,
1106
+ session: managedSession,
1107
+ observedIdentity: currentNativeIdentity
1108
+ }) ?? (currentNativeIdentity === undefined ||
1109
+ nativeIdentityMatchesCodexPreMaterialization(currentNativeIdentity, knownCodexCompanions.primary)
1110
+ ? knownCodexCompanions.primary
1111
+ : undefined);
1112
+ const allowedAdditionalIdentities = allowedPreMaterializationIdentity
1113
+ ? knownCodexCompanions.additional
1114
+ : [];
1115
+ const materializedNativeIdentity = currentNativeIdentity?.sessionId === logicalNativeIdentity?.sessionId
1116
+ ? currentNativeIdentity
1117
+ : undefined;
1118
+ if (!(handoff.adopted &&
1119
+ terminalConversation.agent === "codex" &&
1120
+ !managedSession.binding?.native_process.rollout)) {
1121
+ await verifyCodexPendingManagedSendStatus({
1122
+ options,
1123
+ terminal: terminalConversation,
1124
+ session: managedSession,
1125
+ logicalIdentity: logicalNativeIdentity,
1126
+ allowedPreMaterializationIdentity,
1127
+ allowedAdditionalIdentities
1128
+ });
1129
+ }
1130
+ const managedNativeThreadId = logicalNativeIdentity?.sessionId ??
1131
+ managedSession.binding?.native_thread_id;
1132
+ if (managedNativeThreadId) {
1133
+ await assertNativeThreadHasExclusiveOwnership({
1134
+ options,
1135
+ agent: terminalConversation.agent,
1136
+ currentPid: terminalConversation.pid,
1137
+ nativeThreadId: managedNativeThreadId,
1138
+ storeDir: rawStoreDir,
1139
+ terminalControl: terminalConversation.terminalControl,
1140
+ excludedManagedSessionId: managedSession.session_id
1141
+ });
1142
+ }
1143
+ managedSession = refineManagedSessionNativeIdentity({
1144
+ storeDir: rawStoreDir,
1145
+ session: managedSession,
1146
+ terminalControl: terminalConversation.terminalControl,
1147
+ identity: logicalNativeIdentity
1148
+ });
1149
+ const sessionTurns = managedTurnsForSession(rawStoreDir, managedSession.session_id);
1150
+ const reusableTurn = sessionTurns[0];
1151
+ assertManagedSessionCanStartTurn(sessionTurns);
1152
+ const managed = createManagedTerminalTurn({
1153
+ options,
1154
+ conversationId: terminalConversation.conversationId,
1155
+ agent: terminalConversation.agent,
1156
+ pid: terminalConversation.pid,
1157
+ messageBody,
1158
+ terminalControl: terminalConversation.terminalControl,
1159
+ previousTurn: reusableTurn,
1160
+ managedSession,
1161
+ nativeAgentIdentity: materializedNativeIdentity,
1162
+ deferredForegroundTransferId: deferredCodexForegroundBinding?.transferId
1163
+ });
1164
+ ensureStoreWritable(managed.conversation.store_dir);
1165
+ ensureDir(path.dirname(managed.statePath));
1166
+ await withTerminalDispatchStateScope(scopes, resources, managed.statePath, managed.logPath, async (dispatchScopes, dispatchResources) => {
1167
+ await runTerminalControlSend({
1168
+ transaction: {
1169
+ scopes: dispatchScopes,
1170
+ resources: dispatchResources
1171
+ },
1172
+ options,
1173
+ conversation: managed.conversation,
1174
+ nextConversation: managed.nextConversation,
1175
+ executor: managed.executor,
1176
+ message: managed.message,
1177
+ recordMessageAfterSend: true,
1178
+ recordRawAttachmentAfterSend: reusableTurn === undefined,
1179
+ onTerminalPreflightVerified: pendingRawAttachSessionCreate
1180
+ ? async (route) => {
1181
+ const exactRoute = withExactTerminalDispatchRoute(route, {
1182
+ terminalControl: terminalConversation.terminalControl,
1183
+ terminalKey: terminalBridgeRuntimeKey(terminalConversation.terminalControl),
1184
+ storeDir: rawStoreDir,
1185
+ statePath: managed.statePath,
1186
+ logPath: managed.logPath
1187
+ }, (boundRoute) => boundRoute);
1188
+ let createdSession;
1189
+ if (deferredCodexForegroundBinding) {
1190
+ const deferredScope = bindDeferredForegroundApplicationScope(dispatchScopes, dispatchResources);
1191
+ const reserved = await deferredForegroundApplication(options, deferredCodexForegroundBinding.terminal).reserve({
1192
+ scope: deferredScope,
1193
+ boundary: deferredForegroundBoundaryProjection(deferredCodexForegroundBinding),
1194
+ targetSession: pendingRawAttachSessionCreate,
1195
+ messageId: managed.message.id,
1196
+ turnId: turnIdForConversation(managed.conversation)
1197
+ });
1198
+ createdSession = reserved.createdSession;
1199
+ managedSession = createdSession;
1200
+ pendingRawAttachSessionCreate = undefined;
1201
+ return (rollbackRoute) => {
1202
+ assertTerminalDispatchRouteMatches(rollbackRoute, exactRoute);
1203
+ reserved.rollback(deferredScope);
1204
+ };
1205
+ }
1206
+ createdSession = saveManagedSession(exactRoute.storeDir, pendingRawAttachSessionCreate, {
1207
+ expectedRevision: null
1208
+ });
1209
+ managedSession = createdSession;
1210
+ pendingRawAttachSessionCreate = undefined;
1211
+ return (rollbackRoute) => {
1212
+ assertTerminalDispatchRouteMatches(rollbackRoute, route);
1213
+ const current = loadManagedSession(rollbackRoute.storeDir, createdSession.session_id);
1214
+ if (current.status !== "bound" ||
1215
+ current.revision !== createdSession.revision ||
1216
+ managedSessionBindingToken(current) !==
1217
+ managedSessionBindingToken(createdSession)) {
1218
+ throw new Error(`new raw-attach Session ${createdSession.session_id} changed before pre-transport rollback`);
1219
+ }
1220
+ const detachedAt = cliNow().toISOString();
1221
+ managedSession = saveManagedSession(rollbackRoute.storeDir, {
1222
+ ...current,
1223
+ status: "detached",
1224
+ detached_at: detachedAt,
1225
+ updated_at: detachedAt
1226
+ }, {
1227
+ expectedRevision: current.revision
1228
+ });
1229
+ };
1230
+ }
1231
+ : undefined,
1232
+ allowedPreMaterializationIdentity,
1233
+ allowedAdditionalIdentities,
1234
+ observedHandoff: handoff.adopted && handoff.transition
1235
+ ? {
1236
+ terminal: terminalConversation,
1237
+ transition: handoff.transition
1238
+ }
1239
+ : undefined,
1240
+ verifiedEmptyCodexHandoff: verifiedEmptyHandoff?.boundary,
1241
+ deferredCodexForegroundBinding
1242
+ });
1243
+ });
1244
+ });
1245
+ return;
1246
+ }
1247
+ async function runManagedSessionSend(options, messageBody) {
1248
+ if (stringValue(options.expectedTerminalToken)) {
1249
+ throw new Error("--expected-terminal-token cannot be used with a managed Session; " +
1250
+ "use the exact full terminal selector advertised by AKK list");
1251
+ }
1252
+ const sessionId = required(stringValue(options.session ?? options.conversation ?? options.conversationId), "--session is required for an ordinary managed send");
1253
+ const storeDir = storeDirFromOptions(options);
1254
+ // Read only enough legacy/Session authority to identify the physical
1255
+ // terminal. Store migration is deliberately deferred until the terminal
1256
+ // lock has reconciled any lifecycle fence.
1257
+ let initialSession = tryLoadManagedSession(storeDir, sessionId);
1258
+ let initialTurns = managedTurnsForSession(storeDir, sessionId);
1259
+ if (!initialSession) {
1260
+ const exactTurn = listConversations(storeDir)
1261
+ .filter(isDiscoverableTmuxConversation)
1262
+ .find((turn) => turnIdForConversation(turn) === sessionId ||
1263
+ turn.conversation_id === sessionId);
1264
+ if (exactTurn && sessionIdForConversation(exactTurn) !== sessionId) {
1265
+ throw new Error(`turn ${sessionId} is an execution identity, not an ordinary send target; ` +
1266
+ `send to session ${sessionIdForConversation(exactTurn)} instead`);
1267
+ }
1268
+ if (initialTurns.length === 0) {
1269
+ throw new Error(`managed session ${sessionId} was not found`);
1270
+ }
1271
+ }
1272
+ const legacyBindingTurn = initialTurns[0];
1273
+ const legacyTakeover = legacyBindingTurn && isRecord(legacyBindingTurn.native_session_takeover)
1274
+ ? legacyBindingTurn.native_session_takeover
1275
+ : undefined;
1276
+ const rawTerminalId = initialSession?.binding?.terminal_id ??
1277
+ stringValue(legacyTakeover?.native_session_id);
1278
+ if (!rawTerminalId) {
1279
+ throw new Error(`managed Session ${sessionId} has no authoritative terminal binding`);
1280
+ }
1281
+ const storedTerminalControl = initialSession?.binding?.terminal_control ??
1282
+ terminalControlFromTakeover(legacyTakeover);
1283
+ const storedAgent = initialSession?.agent ??
1284
+ (legacyBindingTurn
1285
+ ? executorForConversation(legacyBindingTurn).kind
1286
+ : undefined);
1287
+ const storedPid = initialSession?.binding?.native_process.pid ??
1288
+ Number(legacyTakeover?.terminal_agent_pid);
1289
+ const terminalBridge = createTerminalAgentBridge(options);
1290
+ const resolvedTerminal = storedTerminalControl && storedAgent &&
1291
+ Number.isSafeInteger(storedPid) && storedPid > 1
1292
+ ? await terminalBridge.resolveStoredTerminal(storedAgent, storedPid, storedTerminalControl, { pid: storedPid })
1293
+ : await terminalBridge.resolveConversationId(rawTerminalId);
1294
+ if (!resolvedTerminal) {
1295
+ throw new Error(`session ${sessionId} is not attached to a live terminal`);
1296
+ }
1297
+ await withCanonicalMutationLocks(terminalWriterMutationLocks(storeDir, resolvedTerminal.terminalControl), async (scopes, resources) => {
1298
+ const lockedStrictSession = tryLoadManagedSession(storeDir, sessionId);
1299
+ if (lockedStrictSession?.agent === "codex" &&
1300
+ lockedStrictSession.session_id === sessionId &&
1301
+ lockedStrictSession.status === "bound" &&
1302
+ isCompleteNativeRollout(lockedStrictSession.binding?.native_process.rollout)) {
1303
+ throw new Error(`Codex rollout-backed managed Session ${sessionId} cannot use a ` +
1304
+ "strict session_id send because an open rollout does not prove the " +
1305
+ "current TUI foreground thread. Refresh AKK list and use its exact " +
1306
+ "selector plus expected_terminal_token. No Turn was created and no " +
1307
+ "terminal input was sent.");
1308
+ }
1309
+ await mutationDispatchLedger.beforeMutation(scopes, resources, options, resolvedTerminal);
1310
+ // A protocol-1/2 Store materializes authoritative Session records as one
1311
+ // atomic migration. Once protocol 3 is active, a missing Session is corrupt
1312
+ // state and must not be reconstructed from whichever Turn looks newest.
1313
+ ensureStoreWritable(storeDir);
1314
+ if (replayExactActiveTerminalSubmission({
1315
+ options,
1316
+ terminalControl: resolvedTerminal.terminalControl,
1317
+ requestText: String(messageBody),
1318
+ expectedStoreDir: storeDir,
1319
+ expectedSessionId: sessionId,
1320
+ expectedMessageType: "task"
1321
+ })) {
1322
+ return;
1323
+ }
1324
+ terminalListCliFacade.assertTerminalIncarnationCanStartTurn(storeDir, resolvedTerminal.terminalControl);
1325
+ let currentSession = tryLoadManagedSession(storeDir, sessionId);
1326
+ let knownCodexCompanions = currentSession
1327
+ ? codexAllowedCompanionSetForManagedSession({
1328
+ storeDir,
1329
+ session: currentSession
1330
+ })
1331
+ : { additional: [] };
1332
+ if (currentSession?.agent === "codex" &&
1333
+ knownCodexCompanions.primary) {
1334
+ try {
1335
+ const inventory = await inspectCodexOpenRootRolloutInventory({
1336
+ options,
1337
+ pid: resolvedTerminal.pid,
1338
+ cwd: resolvedTerminal.terminalControl.currentPath
1339
+ });
1340
+ knownCodexCompanions = codexCompanionsPresentInOpenRootInventory(knownCodexCompanions, inventory);
1341
+ }
1342
+ catch {
1343
+ // Inventory proof is an optimization only. Preserve the existing
1344
+ // closed /status fence when exact open-root membership is unavailable.
1345
+ }
1346
+ }
1347
+ const lockedNativeIdentity = await resolveCurrentNativeAgentSessionIdentity({
1348
+ options,
1349
+ agent: resolvedTerminal.agent,
1350
+ pid: resolvedTerminal.pid,
1351
+ cwd: resolvedTerminal.terminalControl.currentPath,
1352
+ preferredSessionId: knownCodexCompanions.primary
1353
+ ? currentSession?.binding?.native_thread_id
1354
+ : undefined,
1355
+ allowedCompanionIdentity: knownCodexCompanions.primary,
1356
+ allowedAdditionalIdentities: knownCodexCompanions.additional
1357
+ });
1358
+ if (!currentSession) {
1359
+ currentSession = materializeCurrentManagedSession({
1360
+ options,
1361
+ terminal: resolvedTerminal,
1362
+ identity: lockedNativeIdentity
1363
+ });
1364
+ }
1365
+ if (!currentSession ||
1366
+ currentSession.session_id !== sessionId ||
1367
+ currentSession.status !== "bound" ||
1368
+ !currentSession.binding) {
1369
+ throw new Error(`managed Session ${sessionId} is no longer bound; refresh list and retry`);
1370
+ }
1371
+ const currentTurns = managedTurnsForSession(storeDir, sessionId);
1372
+ assertManagedSessionCanStartTurn(currentTurns);
1373
+ if (!bindingMatchesLiveTerminal(currentSession, resolvedTerminal, lockedNativeIdentity, storeDir)) {
1374
+ throw new Error("managed session identity changed while waiting to send; refresh list and retry");
1375
+ }
1376
+ const logicalLockedNativeIdentity = logicalIdentityForManagedSession({
1377
+ storeDir,
1378
+ session: currentSession,
1379
+ observedIdentity: lockedNativeIdentity
1380
+ });
1381
+ const allowedPreMaterializationIdentity = codexPreMaterializationIdentityForManagedSession({
1382
+ storeDir,
1383
+ session: currentSession,
1384
+ observedIdentity: lockedNativeIdentity
1385
+ }) ?? knownCodexCompanions.primary;
1386
+ const allowedAdditionalIdentities = knownCodexCompanions.additional;
1387
+ const materializedLockedNativeIdentity = lockedNativeIdentity?.sessionId === logicalLockedNativeIdentity?.sessionId
1388
+ ? lockedNativeIdentity
1389
+ : undefined;
1390
+ await verifyCodexPendingManagedSendStatus({
1391
+ options,
1392
+ terminal: resolvedTerminal,
1393
+ session: currentSession,
1394
+ logicalIdentity: logicalLockedNativeIdentity,
1395
+ allowedPreMaterializationIdentity,
1396
+ allowedAdditionalIdentities
1397
+ });
1398
+ const managedNativeThreadId = logicalLockedNativeIdentity?.sessionId ??
1399
+ currentSession.binding.native_thread_id;
1400
+ if (managedNativeThreadId) {
1401
+ await assertNativeThreadHasExclusiveOwnership({
1402
+ options,
1403
+ agent: resolvedTerminal.agent,
1404
+ currentPid: resolvedTerminal.pid,
1405
+ nativeThreadId: managedNativeThreadId,
1406
+ storeDir,
1407
+ terminalControl: resolvedTerminal.terminalControl,
1408
+ excludedManagedSessionId: currentSession.session_id
1409
+ });
1410
+ }
1411
+ currentSession = refineManagedSessionNativeIdentity({
1412
+ storeDir,
1413
+ session: currentSession,
1414
+ terminalControl: resolvedTerminal.terminalControl,
1415
+ identity: logicalLockedNativeIdentity
1416
+ });
1417
+ const managed = createManagedTerminalTurn({
1418
+ options,
1419
+ conversationId: resolvedTerminal.conversationId,
1420
+ agent: resolvedTerminal.agent,
1421
+ pid: resolvedTerminal.pid,
1422
+ messageBody,
1423
+ terminalControl: resolvedTerminal.terminalControl,
1424
+ previousTurn: currentTurns[0],
1425
+ managedSession: currentSession,
1426
+ nativeAgentIdentity: materializedLockedNativeIdentity
1427
+ });
1428
+ ensureStoreWritable(managed.conversation.store_dir);
1429
+ ensureDir(path.dirname(managed.statePath));
1430
+ await withTerminalDispatchStateScope(scopes, resources, managed.statePath, managed.logPath, async (dispatchScopes, dispatchResources) => {
1431
+ await runTerminalControlSend({
1432
+ transaction: {
1433
+ scopes: dispatchScopes,
1434
+ resources: dispatchResources
1435
+ },
1436
+ options,
1437
+ conversation: managed.conversation,
1438
+ nextConversation: managed.nextConversation,
1439
+ executor: managed.executor,
1440
+ message: managed.message,
1441
+ recordMessageAfterSend: true,
1442
+ allowedPreMaterializationIdentity,
1443
+ allowedAdditionalIdentities
1444
+ });
1445
+ });
1446
+ });
1447
+ }
1448
+ async function runRespond(options) {
1449
+ const turnId = required(stringValue(options.turn ?? options.conversation ?? options.conversationId), "--turn is required");
1450
+ return runSend({
1451
+ ...options,
1452
+ turn: turnId,
1453
+ conversation: turnId,
1454
+ session: undefined,
1455
+ type: "answer",
1456
+ respond: true
1457
+ });
1458
+ }
1459
+ async function runTurnResponse({ options, messageBody }) {
1460
+ const loaded = loadConversationFromOptions(options);
1461
+ const { statePath, logPath } = loaded;
1462
+ const initialConversation = loaded.conversation;
1463
+ const requestedOpenClawSession = stringValue(options.openclawSession);
1464
+ if (requestedOpenClawSession &&
1465
+ initialConversation.openclaw_session !== requestedOpenClawSession) {
1466
+ throw new Error(`turn ${turnIdForConversation(initialConversation)} belongs to a ` +
1467
+ "different OpenClaw session; no terminal input was sent");
1468
+ }
1469
+ const nativeTakeover = isRecord(initialConversation.native_session_takeover)
1470
+ ? initialConversation.native_session_takeover
1471
+ : undefined;
1472
+ const storedTerminalControl = terminalControlFromTakeover(nativeTakeover);
1473
+ const nativeTerminalId = stringValue(nativeTakeover?.native_session_id);
1474
+ if (!storedTerminalControl || !nativeTerminalId) {
1475
+ throw new Error(`turn ${turnIdForConversation(initialConversation)} is not attached to a live terminal`);
1476
+ }
1477
+ const storedPid = Number(nativeTakeover?.terminal_agent_pid);
1478
+ const storedAgent = executorForConversation(initialConversation).kind;
1479
+ const liveTerminal = Number.isSafeInteger(storedPid) && storedPid > 1
1480
+ ? await createTerminalAgentBridge(options).resolveStoredTerminal(storedAgent, storedPid, storedTerminalControl, terminalRuntimeIdentityForConversation(initialConversation, storedTerminalControl))
1481
+ : undefined;
1482
+ if (!liveTerminal ||
1483
+ liveTerminal.agent !== executorForConversation(initialConversation).kind ||
1484
+ !terminalControlsShareIncarnation(liveTerminal.terminalControl, storedTerminalControl)) {
1485
+ throw new Error(`turn ${turnIdForConversation(initialConversation)} is not attached to its expected live terminal`);
1486
+ }
1487
+ const terminalControl = liveTerminal.terminalControl;
1488
+ const responseStoreDir = pathsForConversationDir(path.dirname(statePath)).storeDir;
1489
+ await withCanonicalMutationLocks(terminalWriterMutationLocks(responseStoreDir, terminalControl), async (scopes, resources) => {
1490
+ // Legacy identity migration may write the Turn, so keep it inside the
1491
+ // canonical terminal -> Store writer -> state ordering as well.
1492
+ await migrateLegacyTerminalAgentIdentity({ ...loaded, options });
1493
+ await withTerminalDispatchStateScope(scopes, resources, statePath, logPath, async (dispatchScopes, dispatchResources) => {
1494
+ let lockedConversation = loadState(statePath);
1495
+ let lockedTakeover = isRecord(lockedConversation.native_session_takeover)
1496
+ ? lockedConversation.native_session_takeover
1497
+ : undefined;
1498
+ const lockedControl = terminalControlFromTakeover(lockedTakeover);
1499
+ if (stringValue(lockedTakeover?.native_session_id) !== nativeTerminalId ||
1500
+ executorForConversation(lockedConversation).kind !== liveTerminal.agent ||
1501
+ !lockedControl ||
1502
+ !terminalControlsShareIncarnation(lockedControl, terminalControl)) {
1503
+ throw new Error("terminal control changed while waiting to respond; refresh status and retry");
1504
+ }
1505
+ if (requestedOpenClawSession &&
1506
+ lockedConversation.openclaw_session !== requestedOpenClawSession) {
1507
+ throw new Error(`turn ${turnIdForConversation(lockedConversation)} belongs to a ` +
1508
+ "different OpenClaw session; no terminal input was sent");
1509
+ }
1510
+ lockedConversation = refineTerminalTurnEndpoint({
1511
+ conversation: lockedConversation,
1512
+ statePath,
1513
+ terminalControl
1514
+ });
1515
+ lockedTakeover = isRecord(lockedConversation.native_session_takeover)
1516
+ ? lockedConversation.native_session_takeover
1517
+ : undefined;
1518
+ if (replayExactActiveTerminalSubmission({
1519
+ options,
1520
+ terminalControl,
1521
+ requestText: String(messageBody),
1522
+ expectedStoreDir: responseStoreDir,
1523
+ expectedSessionId: sessionIdForConversation(lockedConversation),
1524
+ expectedTurnId: turnIdForConversation(lockedConversation),
1525
+ expectedMessageType: "answer",
1526
+ expectedStatePath: statePath
1527
+ })) {
1528
+ return;
1529
+ }
1530
+ if (lockedConversation.status !== "waiting_for_openclaw") {
1531
+ throw new Error(`cannot respond to turn ${turnIdForConversation(lockedConversation)}; ` +
1532
+ `turn is ${lockedConversation.status}, not waiting_for_openclaw`);
1533
+ }
1534
+ const prepared = prepareManagedSend({
1535
+ options: { ...options, type: "answer" },
1536
+ statePath,
1537
+ logPath,
1538
+ messageBody,
1539
+ stateLockHeld: true,
1540
+ persist: false
1541
+ });
1542
+ const preparedTakeover = isRecord(prepared.conversation.native_session_takeover)
1543
+ ? prepared.conversation.native_session_takeover
1544
+ : undefined;
1545
+ const terminalAgentPid = Number(preparedTakeover?.terminal_agent_pid);
1546
+ let responseManagedSession = tryLoadManagedSession(responseStoreDir, sessionIdForConversation(prepared.conversation));
1547
+ const responseCodexCompanions = prepared.executor.kind === "codex" && responseManagedSession
1548
+ ? codexAllowedCompanionSetForManagedSession({
1549
+ storeDir: responseStoreDir,
1550
+ session: responseManagedSession
1551
+ })
1552
+ : { additional: [] };
1553
+ const currentNativeIdentity = await resolveCurrentNativeAgentSessionIdentity({
1554
+ options,
1555
+ agent: prepared.executor.kind,
1556
+ pid: terminalAgentPid,
1557
+ cwd: terminalControl.currentPath,
1558
+ preferredSessionId: responseCodexCompanions.primary
1559
+ ? stringValue(preparedTakeover?.terminal_agent_session_id)
1560
+ : undefined,
1561
+ allowedCompanionIdentity: responseCodexCompanions.primary,
1562
+ allowedAdditionalIdentities: responseCodexCompanions.additional
1563
+ });
1564
+ assertNativeAgentIdentityForTurn({
1565
+ conversation: prepared.conversation,
1566
+ currentIdentity: currentNativeIdentity,
1567
+ operation: "respond to"
1568
+ });
1569
+ if (responseManagedSession) {
1570
+ const logicalResponseIdentity = logicalIdentityForManagedSession({
1571
+ storeDir: responseStoreDir,
1572
+ session: responseManagedSession,
1573
+ observedIdentity: currentNativeIdentity
1574
+ });
1575
+ responseManagedSession = refineManagedSessionNativeIdentity({
1576
+ storeDir: responseStoreDir,
1577
+ session: responseManagedSession,
1578
+ terminalControl,
1579
+ identity: logicalResponseIdentity
1580
+ });
1581
+ }
1582
+ const currentTerminalControl = terminalControlFromTakeover(prepared.nativeTakeoverForSend);
1583
+ if (!currentTerminalControl ||
1584
+ !terminalControlsShareIncarnation(currentTerminalControl, terminalControl)) {
1585
+ throw new Error("terminal control changed while waiting to respond; refresh status and retry");
1586
+ }
1587
+ const responseOptions = {
1588
+ ...options,
1589
+ type: "answer",
1590
+ agentTimeoutMinutes: options.agentTimeoutMinutes ??
1591
+ preparedTakeover?.terminal_bridge_inactivity_timeout_minutes ??
1592
+ DEFAULT_AGENT_TIMEOUT_MINUTES,
1593
+ agentHardTimeoutMinutes: options.agentHardTimeoutMinutes ??
1594
+ preparedTakeover?.terminal_bridge_hard_timeout_minutes ??
1595
+ DEFAULT_AGENT_HARD_TIMEOUT_MINUTES
1596
+ };
1597
+ await runTerminalControlSend({
1598
+ transaction: {
1599
+ scopes: dispatchScopes,
1600
+ resources: dispatchResources
1601
+ },
1602
+ options: responseOptions,
1603
+ conversation: prepared.conversation,
1604
+ nextConversation: prepared.nextConversation,
1605
+ executor: prepared.executor,
1606
+ message: prepared.message,
1607
+ recordMessageAfterSend: true,
1608
+ allowedPreMaterializationIdentity: responseCodexCompanions.primary,
1609
+ allowedAdditionalIdentities: responseCodexCompanions.additional,
1610
+ continuingTurnResponse: true
1611
+ });
1612
+ });
1613
+ });
1614
+ }
1615
+ async function runApprove(options) {
1616
+ const terminalConversation = await resolveTerminalConversationFromOptions(options);
1617
+ if (terminalConversation) {
1618
+ assertExpectedHandoffTokenUsesExactTerminalSelector({
1619
+ options,
1620
+ terminal: terminalConversation
1621
+ });
1622
+ if (options.autoApproved === true) {
1623
+ throw new Error("automatic approval requires an exact managed Turn and cannot use a raw terminal selector");
1624
+ }
1625
+ await runTerminalConversationApprove({
1626
+ options,
1627
+ terminal: terminalConversation
1628
+ });
1629
+ return;
1630
+ }
1631
+ const loaded = loadConversationFromOptions(options);
1632
+ const { statePath, logPath } = loaded;
1633
+ const conversation = await migrateLegacyTerminalAgentIdentity({
1634
+ ...loaded,
1635
+ options
1636
+ });
1637
+ const nativeTakeover = isRecord(conversation.native_session_takeover)
1638
+ ? conversation.native_session_takeover
1639
+ : undefined;
1640
+ const terminalControl = terminalControlFromTakeover(nativeTakeover);
1641
+ if (!terminalControl) {
1642
+ throw new Error(`conversation ${conversation.conversation_id} is not controlled through a terminal`);
1643
+ }
1644
+ if (!["waiting_for_agent", "waiting_for_openclaw"].includes(conversation.status)) {
1645
+ throw new Error(`cannot approve ${conversation.conversation_id}; conversation is ${conversation.status}`);
1646
+ }
1647
+ const executor = executorForConversation(conversation);
1648
+ const monitoredApproval = isRecord(nativeTakeover?.["terminal_bridge_approval"])
1649
+ ? nativeTakeover.terminal_bridge_approval
1650
+ : undefined;
1651
+ const suppliedExpectedFingerprint = stringValue(options.expectedApprovalFingerprint);
1652
+ const expectedFingerprint = suppliedExpectedFingerprint ??
1653
+ stringValue(monitoredApproval?.fingerprint);
1654
+ const autoApproved = options.autoApproved === true;
1655
+ const claudeScreenApproval = executor.kind === "claude";
1656
+ if (claudeScreenApproval) {
1657
+ const monitoredState = isRecord(monitoredApproval?.approval_state)
1658
+ ? monitoredApproval.approval_state
1659
+ : undefined;
1660
+ const pendingDispatch = isRecord(nativeTakeover?.terminal_bridge_approval_dispatch)
1661
+ ? nativeTakeover.terminal_bridge_approval_dispatch
1662
+ : undefined;
1663
+ const lastApprovalFingerprint = stringValue(nativeTakeover?.terminal_bridge_last_approval_fingerprint);
1664
+ const lastApprovalMessageId = stringValue(nativeTakeover?.terminal_bridge_last_approval_message_id);
1665
+ const currentMessageId = stringValue(nativeTakeover?.terminal_bridge_message_id);
1666
+ const approvalResolvedAt = validTimestampMs(nativeTakeover?.terminal_bridge_approval_resolved_at);
1667
+ if (autoApproved &&
1668
+ monitoredApproval === undefined &&
1669
+ pendingDispatch === undefined &&
1670
+ conversation.status === "waiting_for_agent" &&
1671
+ suppliedExpectedFingerprint !== undefined &&
1672
+ suppliedExpectedFingerprint === lastApprovalFingerprint &&
1673
+ lastApprovalMessageId !== undefined &&
1674
+ lastApprovalMessageId === currentMessageId &&
1675
+ approvalResolvedAt !== undefined) {
1676
+ const monitor = ensureTerminalBridgeMonitorAfterApproval({
1677
+ conversation,
1678
+ statePath,
1679
+ logPath,
1680
+ terminalControl,
1681
+ options,
1682
+ reason: "approval_already_resolved"
1683
+ });
1684
+ printJson({
1685
+ conversation,
1686
+ approved: false,
1687
+ already_approved: true,
1688
+ blocked: false,
1689
+ reason: "Claude screen approval fingerprint was already consumed",
1690
+ terminal_control: terminalControl,
1691
+ monitor_pid: monitor.monitorPid ?? null,
1692
+ monitor_handoff_pid: monitor.handoffWatchdog?.pid ?? null
1693
+ });
1694
+ return;
1695
+ }
1696
+ const notifiedAt = validTimestampMs(monitoredApproval?.notified_at);
1697
+ if (conversation.status !== "waiting_for_openclaw" ||
1698
+ monitoredState?.decision_mode !== "keys" ||
1699
+ !stringValue(monitoredApproval?.fingerprint)) {
1700
+ printJson({
1701
+ conversation,
1702
+ approved: false,
1703
+ blocked: true,
1704
+ reason: "Claude screen approval requires a current managed-turn approval notification",
1705
+ terminal_control: terminalControl
1706
+ });
1707
+ return;
1708
+ }
1709
+ if (notifiedAt === undefined ||
1710
+ cliNowMs() - notifiedAt > CLAUDE_SCREEN_APPROVAL_TTL_MS) {
1711
+ printJson({
1712
+ conversation,
1713
+ approved: false,
1714
+ blocked: true,
1715
+ reason: "Claude screen approval expired; inspect and resolve the terminal manually",
1716
+ terminal_control: terminalControl
1717
+ });
1718
+ return;
1719
+ }
1720
+ if (!suppliedExpectedFingerprint ||
1721
+ expectedFingerprint !== monitoredApproval?.fingerprint) {
1722
+ printJson({
1723
+ conversation,
1724
+ approved: false,
1725
+ blocked: true,
1726
+ reason: "Claude screen approval requires the latest notified fingerprint",
1727
+ terminal_control: terminalControl
1728
+ });
1729
+ return;
1730
+ }
1731
+ if (pendingDispatch?.state === "reserved" &&
1732
+ pendingDispatch.terminal_bridge_message_id ===
1733
+ nativeTakeover?.terminal_bridge_message_id) {
1734
+ printJson({
1735
+ conversation,
1736
+ approved: false,
1737
+ blocked: true,
1738
+ reason: "a previous Claude approval dispatch has an uncertain outcome; inspect and resolve the terminal manually",
1739
+ terminal_control: terminalControl
1740
+ });
1741
+ return;
1742
+ }
1743
+ if (expectedFingerprint ===
1744
+ stringValue(nativeTakeover?.terminal_bridge_last_approval_fingerprint)) {
1745
+ printJson({
1746
+ conversation,
1747
+ approved: false,
1748
+ blocked: true,
1749
+ reason: "Claude screen approval fingerprint was already consumed",
1750
+ terminal_control: terminalControl
1751
+ });
1752
+ return;
1753
+ }
1754
+ }
1755
+ return runManagedApprovalDispatch({
1756
+ options, conversation, statePath, logPath,
1757
+ nativeTakeover: nativeTakeover,
1758
+ terminalControl, executor, monitoredApproval, expectedFingerprint,
1759
+ autoApproved, claudeScreenApproval
1760
+ });
1761
+ }
1762
+ async function runManagedApprovalDispatch({ options, conversation, statePath, logPath, nativeTakeover, terminalControl, executor, monitoredApproval, expectedFingerprint, autoApproved, claudeScreenApproval }) {
1763
+ const policyRuleId = stringValue(options.policyRuleId);
1764
+ const policyFingerprint = stringValue(options.policyFingerprint);
1765
+ const autoApprovalPolicy = autoApproved
1766
+ ? parseJsonOption(options.autoApprovalPolicyJson, "--auto-approval-policy-json")
1767
+ : undefined;
1768
+ let executorPolicyDecision;
1769
+ const policyCandidateForInspection = ({ agent, currentTerminalControl, inspection, fingerprint }) => {
1770
+ const evidence = inspection.approval.approvable
1771
+ ? inspection.approval.policyEvidence
1772
+ : undefined;
1773
+ return {
1774
+ agent,
1775
+ kind: evidence?.kind ?? inspection.approval.promptKind ?? "unknown",
1776
+ decisionMode: inspection.approval.approvable
1777
+ ? inspection.approval.action.mode ?? "keys"
1778
+ : undefined,
1779
+ command: evidence?.command ?? inspection.approval.command,
1780
+ cwd: evidence?.cwd ?? inspection.approval.cwd ?? currentTerminalControl.currentPath,
1781
+ fingerprint: fingerprint ?? "",
1782
+ terminalTarget: currentTerminalControl.target,
1783
+ ...(evidence?.source === "claude_transcript"
1784
+ ? {
1785
+ evidenceSource: "claude_transcript",
1786
+ evidenceFingerprint: evidence.evidenceFingerprint
1787
+ }
1788
+ : {})
1789
+ };
1790
+ };
1791
+ const releaseTerminalLock = acquireTerminalBridgeSendLock(storeDirFromOptions(options), terminalControl, { timeoutMs: 30000 });
1792
+ let terminalLockReleased = false;
1793
+ const releaseApprovalTerminalLock = () => {
1794
+ if (!terminalLockReleased) {
1795
+ terminalLockReleased = true;
1796
+ releaseTerminalLock();
1797
+ }
1798
+ };
1799
+ let releaseStateLock;
1800
+ let approvalDispatchReserved = false;
1801
+ const releaseApprovalStateLock = () => {
1802
+ if (releaseStateLock) {
1803
+ const release = releaseStateLock;
1804
+ releaseStateLock = undefined;
1805
+ release();
1806
+ }
1807
+ };
1808
+ const writerStoreDir = pathsForConversationDir(path.dirname(statePath)).storeDir;
1809
+ const runApprovalWithStateLock = async () => {
1810
+ let approval;
1811
+ let lockedConversation = conversation;
1812
+ const currentConversation = loadState(statePath);
1813
+ const currentTakeover = isRecord(currentConversation.native_session_takeover)
1814
+ ? currentConversation.native_session_takeover
1815
+ : undefined;
1816
+ const currentControl = terminalControlFromTakeover(currentTakeover);
1817
+ const currentApproval = isRecord(currentTakeover?.terminal_bridge_approval)
1818
+ ? currentTakeover.terminal_bridge_approval
1819
+ : undefined;
1820
+ if (currentConversation.status !== conversation.status ||
1821
+ currentTakeover?.terminal_bridge_message_id !== nativeTakeover?.terminal_bridge_message_id ||
1822
+ !currentControl ||
1823
+ !terminalControlsShareIncarnation(currentControl, terminalControl) ||
1824
+ (claudeScreenApproval &&
1825
+ currentApproval?.fingerprint !== monitoredApproval?.fingerprint)) {
1826
+ throw new Error("approval state changed while waiting for terminal control; refresh status and retry");
1827
+ }
1828
+ assertManagedTerminalDispatchOwner({
1829
+ storeDir: writerStoreDir,
1830
+ conversation: currentConversation,
1831
+ terminalControl: currentControl,
1832
+ action: "approve"
1833
+ });
1834
+ lockedConversation = currentConversation;
1835
+ const currentRuntimeIdentity = terminalRuntimeIdentityForConversation(currentConversation, currentControl);
1836
+ approval = await createTerminalAgentBridge(options).approve(executor.kind, currentControl, {
1837
+ expectedFingerprint,
1838
+ scrollbackLines: Number(options.scrollbackLines ?? 120),
1839
+ runtime: currentRuntimeIdentity,
1840
+ managedRequest: terminalDurableRequestForConversation(currentConversation, currentControl),
1841
+ requiredDecisionMode: autoApproved && executor.kind === "claude" ? "keys" : undefined,
1842
+ authorize: autoApproved
1843
+ ? ({ agent, terminalControl: currentTerminalControl, inspection, fingerprint }) => {
1844
+ if (!autoApprovalPolicy) {
1845
+ return {
1846
+ approved: false,
1847
+ reason: "automatic approval requires an executor-side policy"
1848
+ };
1849
+ }
1850
+ const candidate = policyCandidateForInspection({
1851
+ agent,
1852
+ currentTerminalControl,
1853
+ inspection,
1854
+ fingerprint
1855
+ });
1856
+ executorPolicyDecision = evaluateApprovalPolicy({
1857
+ policy: autoApprovalPolicy,
1858
+ candidate
1859
+ });
1860
+ if (executorPolicyDecision.action !== "approve") {
1861
+ return {
1862
+ approved: false,
1863
+ reason: `executor-side auto-approval policy rejected the current request: ${executorPolicyDecision.reason}`
1864
+ };
1865
+ }
1866
+ if (policyRuleId && executorPolicyDecision.ruleId !== policyRuleId) {
1867
+ return {
1868
+ approved: false,
1869
+ reason: "executor-side auto-approval rule changed before execution"
1870
+ };
1871
+ }
1872
+ if (policyFingerprint &&
1873
+ executorPolicyDecision.policyFingerprint !== policyFingerprint) {
1874
+ return {
1875
+ approved: false,
1876
+ reason: "executor-side auto-approval policy changed before execution"
1877
+ };
1878
+ }
1879
+ return { approved: true };
1880
+ }
1881
+ : undefined,
1882
+ beforeKeyDispatch: claudeScreenApproval
1883
+ ? ({ fingerprint, terminalControl: dispatchControl, inspection, keys }) => {
1884
+ if (autoApproved) {
1885
+ if (!autoApprovalPolicy) {
1886
+ throw new Error("automatic approval requires an executor-side policy before dispatch");
1887
+ }
1888
+ const freshPolicyDecision = evaluateApprovalPolicy({
1889
+ policy: autoApprovalPolicy,
1890
+ candidate: policyCandidateForInspection({
1891
+ agent: executor.kind,
1892
+ currentTerminalControl: dispatchControl,
1893
+ inspection,
1894
+ fingerprint
1895
+ })
1896
+ });
1897
+ if (freshPolicyDecision.action !== "approve") {
1898
+ throw new Error(`executor-side auto-approval policy rejected the recaptured request: ${freshPolicyDecision.reason}`);
1899
+ }
1900
+ if (executorPolicyDecision?.ruleId &&
1901
+ freshPolicyDecision.ruleId !== executorPolicyDecision.ruleId) {
1902
+ throw new Error("executor-side auto-approval rule changed after recapture");
1903
+ }
1904
+ if (policyRuleId && freshPolicyDecision.ruleId !== policyRuleId) {
1905
+ throw new Error("executor-side auto-approval rule changed before dispatch");
1906
+ }
1907
+ if (policyFingerprint &&
1908
+ freshPolicyDecision.policyFingerprint !== policyFingerprint) {
1909
+ throw new Error("executor-side auto-approval policy changed before dispatch");
1910
+ }
1911
+ executorPolicyDecision = freshPolicyDecision;
1912
+ }
1913
+ if (approvalDispatchReserved) {
1914
+ throw new Error("Claude approval dispatch was already reserved");
1915
+ }
1916
+ approvalDispatchReserved = true;
1917
+ if (!releaseStateLock) {
1918
+ throw new Error("approval state lock was released before terminal dispatch");
1919
+ }
1920
+ const latestConversation = loadState(statePath);
1921
+ const latestTakeover = isRecord(latestConversation.native_session_takeover)
1922
+ ? latestConversation.native_session_takeover
1923
+ : undefined;
1924
+ const latestControl = terminalControlFromTakeover(latestTakeover);
1925
+ const latestApproval = isRecord(latestTakeover?.terminal_bridge_approval)
1926
+ ? latestTakeover.terminal_bridge_approval
1927
+ : undefined;
1928
+ const latestNotifiedAt = validTimestampMs(latestApproval?.notified_at);
1929
+ const latestApprovalState = isRecord(latestApproval?.approval_state)
1930
+ ? latestApproval.approval_state
1931
+ : undefined;
1932
+ const latestPolicyEvidence = isRecord(latestApprovalState?.policy_evidence)
1933
+ ? latestApprovalState.policy_evidence
1934
+ : undefined;
1935
+ const recapturedPolicyEvidence = inspection.approval.approvable
1936
+ ? inspection.approval.policyEvidence
1937
+ : undefined;
1938
+ const latestDispatch = isRecord(latestTakeover?.terminal_bridge_approval_dispatch)
1939
+ ? latestTakeover.terminal_bridge_approval_dispatch
1940
+ : undefined;
1941
+ if (!latestTakeover ||
1942
+ latestConversation.status !== "waiting_for_openclaw" ||
1943
+ latestTakeover.terminal_bridge_message_id !==
1944
+ nativeTakeover?.terminal_bridge_message_id ||
1945
+ latestApproval?.fingerprint !== fingerprint ||
1946
+ latestNotifiedAt === undefined ||
1947
+ cliNowMs() - latestNotifiedAt > CLAUDE_SCREEN_APPROVAL_TTL_MS ||
1948
+ expectedFingerprint !== fingerprint ||
1949
+ !terminalControlsShareIncarnation(latestControl, dispatchControl) ||
1950
+ (autoApproved &&
1951
+ (latestPolicyEvidence?.source !== "claude_transcript" ||
1952
+ latestPolicyEvidence.evidence_fingerprint !==
1953
+ recapturedPolicyEvidence?.evidenceFingerprint))) {
1954
+ throw new Error("approval state changed before terminal dispatch; refresh status and retry");
1955
+ }
1956
+ if (latestDispatch?.state === "reserved" &&
1957
+ latestDispatch.terminal_bridge_message_id ===
1958
+ latestTakeover.terminal_bridge_message_id) {
1959
+ throw new Error("a previous Claude approval dispatch has an uncertain outcome; inspect and resolve the terminal manually");
1960
+ }
1961
+ const reservedAt = cliNow().toISOString();
1962
+ const reservedConversation = {
1963
+ ...latestConversation,
1964
+ native_session_takeover: {
1965
+ ...latestTakeover,
1966
+ terminal_bridge_approval_dispatch: {
1967
+ state: "reserved",
1968
+ attempt_id: randomUUID(),
1969
+ fingerprint,
1970
+ keys,
1971
+ terminal_target: dispatchControl.target,
1972
+ terminal_bridge_message_id: latestTakeover.terminal_bridge_message_id,
1973
+ reserved_at: reservedAt
1974
+ }
1975
+ },
1976
+ updated_at: reservedAt
1977
+ };
1978
+ saveState(statePath, reservedConversation);
1979
+ lockedConversation = reservedConversation;
1980
+ }
1981
+ : undefined
1982
+ });
1983
+ const actualFingerprint = approval.fingerprint;
1984
+ const effectivePolicyRuleId = executorPolicyDecision?.ruleId ?? policyRuleId;
1985
+ const effectivePolicyFingerprint = executorPolicyDecision?.policyFingerprint ?? policyFingerprint;
1986
+ if (!approval.approved) {
1987
+ releaseApprovalStateLock();
1988
+ if (autoApproved) {
1989
+ appendEvent(logPath, {
1990
+ ts: cliNow().toISOString(),
1991
+ conversation_id: conversation.conversation_id,
1992
+ event: "terminal_auto_approval_decision",
1993
+ action: "rejected",
1994
+ reason: approval.reason,
1995
+ terminal_control: terminalControl,
1996
+ expected_fingerprint: expectedFingerprint,
1997
+ actual_fingerprint: actualFingerprint,
1998
+ policy_rule_id: effectivePolicyRuleId,
1999
+ policy_fingerprint: effectivePolicyFingerprint
2000
+ });
2001
+ }
2002
+ printJson({
2003
+ conversation,
2004
+ approved: false,
2005
+ blocked: approval.blocked,
2006
+ reason: approval.reason,
2007
+ terminal_control: terminalControl,
2008
+ expected_approval_fingerprint: expectedFingerprint,
2009
+ actual_approval_fingerprint: actualFingerprint,
2010
+ screen_excerpt: approval.screenExcerpt
2011
+ });
2012
+ return;
2013
+ }
2014
+ appendEvent(logPath, {
2015
+ ts: cliNow().toISOString(),
2016
+ conversation_id: conversation.conversation_id,
2017
+ event: "terminal_approval_send",
2018
+ terminal_control: terminalControl,
2019
+ key: approval.key,
2020
+ keys: approval.keys,
2021
+ label: approval.label,
2022
+ decision_mode: approval.decisionMode,
2023
+ request_id: approval.requestId,
2024
+ approval_fingerprint: actualFingerprint,
2025
+ auto_approved: autoApproved,
2026
+ policy_rule_id: effectivePolicyRuleId,
2027
+ policy_fingerprint: effectivePolicyFingerprint
2028
+ });
2029
+ if (autoApproved) {
2030
+ appendEvent(logPath, {
2031
+ ts: cliNow().toISOString(),
2032
+ conversation_id: conversation.conversation_id,
2033
+ event: "terminal_auto_approval_decision",
2034
+ action: "approved",
2035
+ terminal_control: terminalControl,
2036
+ approval_fingerprint: actualFingerprint,
2037
+ policy_rule_id: effectivePolicyRuleId,
2038
+ policy_fingerprint: effectivePolicyFingerprint
2039
+ });
2040
+ }
2041
+ runtimeLog("info", "terminal_approval_send", {
2042
+ conversation_id: conversation.conversation_id,
2043
+ terminal_target: terminalControl.target,
2044
+ key: approval.key,
2045
+ keys: approval.keys,
2046
+ label: approval.label,
2047
+ decision_mode: approval.decisionMode,
2048
+ request_id: approval.requestId,
2049
+ approval_fingerprint: actualFingerprint,
2050
+ auto_approved: autoApproved,
2051
+ policy_rule_id: effectivePolicyRuleId,
2052
+ policy_fingerprint: effectivePolicyFingerprint
2053
+ });
2054
+ const nativeTakeoverForUpdate = isRecord(lockedConversation.native_session_takeover)
2055
+ ? { ...lockedConversation.native_session_takeover }
2056
+ : {};
2057
+ const resolvedApproval = isRecord(nativeTakeoverForUpdate.terminal_bridge_approval)
2058
+ ? nativeTakeoverForUpdate.terminal_bridge_approval
2059
+ : undefined;
2060
+ const resolvedApprovalScreenDigest = stringValue(resolvedApproval?.screen_digest);
2061
+ const resolvedApprovalState = isRecord(resolvedApproval?.approval_state)
2062
+ ? resolvedApproval.approval_state
2063
+ : undefined;
2064
+ const resolvedTranscriptIdentity = claudeTranscriptApprovalIdentity(resolvedApprovalState);
2065
+ const approvalResolvedAt = cliNow().toISOString();
2066
+ const agentTimeoutMinutes = Number(options.agentTimeoutMinutes ??
2067
+ nativeTakeoverForUpdate.terminal_bridge_inactivity_timeout_minutes ??
2068
+ DEFAULT_AGENT_TIMEOUT_MINUTES);
2069
+ const agentHardTimeoutMinutes = positiveMinutes(options.agentHardTimeoutMinutes ??
2070
+ nativeTakeoverForUpdate.terminal_bridge_hard_timeout_minutes ??
2071
+ DEFAULT_AGENT_HARD_TIMEOUT_MINUTES, "--agent-hard-timeout-minutes");
2072
+ const nextNativeTakeover = {
2073
+ ...nativeTakeoverForUpdate,
2074
+ terminal_bridge_approval: undefined,
2075
+ terminal_bridge_approval_dispatch: undefined,
2076
+ terminal_bridge_approval_resolved_at: approvalResolvedAt,
2077
+ terminal_bridge_last_approval_fingerprint: actualFingerprint,
2078
+ terminal_bridge_last_approval_screen_digest: resolvedApprovalScreenDigest,
2079
+ terminal_bridge_last_approval_request_id: resolvedTranscriptIdentity?.requestId,
2080
+ terminal_bridge_last_approval_evidence_fingerprint: resolvedTranscriptIdentity?.evidenceFingerprint,
2081
+ terminal_bridge_last_approval_prompt_cleared_at: undefined,
2082
+ terminal_bridge_last_approval_at: approvalResolvedAt,
2083
+ terminal_bridge_last_approval_message_id: nativeTakeoverForUpdate.terminal_bridge_message_id,
2084
+ terminal_bridge_monitor_lock_version: monitorOwner.LOCK_VERSION,
2085
+ terminal_bridge_monitor_started_at: approvalResolvedAt,
2086
+ terminal_bridge_last_activity_at: approvalResolvedAt,
2087
+ terminal_bridge_last_activity_reason: "approval resolved",
2088
+ terminal_bridge_inactivity_timeout_minutes: agentTimeoutMinutes,
2089
+ terminal_bridge_hard_timeout_minutes: agentHardTimeoutMinutes,
2090
+ terminal_bridge_inactivity_deadline_at: deadlineAt(approvalResolvedAt, agentTimeoutMinutes),
2091
+ terminal_bridge_hard_deadline_at: deadlineAt(stringValue(nativeTakeoverForUpdate.terminal_bridge_started_at) ?? approvalResolvedAt, agentHardTimeoutMinutes)
2092
+ };
2093
+ delete nextNativeTakeover.terminal_bridge_approval;
2094
+ delete nextNativeTakeover.terminal_bridge_approval_dispatch;
2095
+ delete nextNativeTakeover.terminal_bridge_last_approval_prompt_cleared_at;
2096
+ const nextConversation = {
2097
+ ...lockedConversation,
2098
+ status: terminalBridgeEnabled(lockedConversation)
2099
+ ? "waiting_for_agent"
2100
+ : lockedConversation.status,
2101
+ native_session_takeover: nextNativeTakeover,
2102
+ updated_at: approvalResolvedAt
2103
+ };
2104
+ saveState(statePath, nextConversation);
2105
+ releaseApprovalStateLock();
2106
+ const bridgeMonitor = ensureTerminalBridgeMonitorAfterApproval({
2107
+ conversation: nextConversation,
2108
+ statePath,
2109
+ logPath,
2110
+ terminalControl,
2111
+ options
2112
+ });
2113
+ printJson({
2114
+ conversation: nextConversation,
2115
+ approved: true,
2116
+ terminal_control: terminalControl,
2117
+ key: approval.key,
2118
+ keys: approval.keys,
2119
+ label: approval.label,
2120
+ decision_mode: approval.decisionMode,
2121
+ request_id: approval.requestId,
2122
+ approval_fingerprint: actualFingerprint,
2123
+ auto_approved: autoApproved,
2124
+ policy_rule_id: effectivePolicyRuleId,
2125
+ policy_fingerprint: effectivePolicyFingerprint,
2126
+ monitor_pid: bridgeMonitor.monitorPid ?? null,
2127
+ monitor_handoff_pid: bridgeMonitor.handoffWatchdog?.pid ?? null
2128
+ });
2129
+ };
2130
+ try {
2131
+ return await withStoreWriterLeaseAsync(writerStoreDir, async () => {
2132
+ releaseStateLock = acquireFileLock(`${statePath}.lock`);
2133
+ try {
2134
+ return await runApprovalWithStateLock();
2135
+ }
2136
+ finally {
2137
+ releaseApprovalStateLock();
2138
+ }
2139
+ });
2140
+ }
2141
+ finally {
2142
+ try {
2143
+ releaseApprovalStateLock();
2144
+ }
2145
+ finally {
2146
+ releaseApprovalTerminalLock();
2147
+ }
2148
+ }
2149
+ }
2150
+ async function runTerminalConversationApprove({ options, terminal }) {
2151
+ const { conversationId, agent, terminalControl, pid } = terminal;
2152
+ const storeDir = storeDirFromOptions(options);
2153
+ const releaseTerminalLock = acquireTerminalBridgeSendLock(storeDir, terminalControl, { timeoutMs: 30000 });
2154
+ try {
2155
+ if (agent === "claude") {
2156
+ printJson({
2157
+ conversation_id: conversationId,
2158
+ source: "terminal_control",
2159
+ approved: false,
2160
+ blocked: true,
2161
+ reason: "Claude screen approval requires `send --background` so AKK can bind it to an active managed turn",
2162
+ terminal_control: terminalControl
2163
+ });
2164
+ return;
2165
+ }
2166
+ const suppliedTerminalToken = stringValue(options.expectedTerminalToken);
2167
+ const initialResolution = await terminalListCliFacade.resolveTerminalScopedCodexApproval({
2168
+ options,
2169
+ terminal
2170
+ });
2171
+ if (initialResolution.state === "blocked") {
2172
+ throw new Error(initialResolution.reason);
2173
+ }
2174
+ if (initialResolution.state === "unmanaged" && suppliedTerminalToken) {
2175
+ throw new Error("--expected-terminal-token does not match an advertised terminal-scoped Codex approval");
2176
+ }
2177
+ if (initialResolution.state === "eligible" &&
2178
+ suppliedTerminalToken !== initialResolution.boundary.token) {
2179
+ throw new Error("terminal-scoped Codex approval token is missing or stale; refresh AKK list");
2180
+ }
2181
+ const runtime = {
2182
+ pid,
2183
+ cwd: terminalControl.currentPath,
2184
+ conversationId,
2185
+ terminalTarget: terminalControl.target
2186
+ };
2187
+ const approveCurrentPrompt = async (terminalScoped) => createTerminalAgentBridge(options).approve(agent, terminalControl, {
2188
+ expectedFingerprint: stringValue(options.expectedApprovalFingerprint),
2189
+ scrollbackLines: Number(options.scrollbackLines ?? 120),
2190
+ runtime,
2191
+ beforeKeyDispatch: terminalScoped
2192
+ ? async (context) => {
2193
+ const approvalSnapshot = terminalScopedCodexApprovalPromptSnapshot({
2194
+ approvable: true,
2195
+ fingerprint: context.fingerprint,
2196
+ keys: context.keys,
2197
+ decision_mode: context.inspection.approval.approvable
2198
+ ? context.inspection.approval.action.mode ?? "keys"
2199
+ : undefined,
2200
+ request_id: context.inspection.approval.approvable
2201
+ ? context.inspection.approval.action.requestId
2202
+ : undefined
2203
+ });
2204
+ const current = await terminalListCliFacade.resolveTerminalScopedCodexApproval({
2205
+ options,
2206
+ terminal,
2207
+ approvalSnapshot
2208
+ });
2209
+ if (current.state !== "eligible" ||
2210
+ current.boundary.token !== suppliedTerminalToken) {
2211
+ throw new Error(current.state === "blocked"
2212
+ ? current.reason
2213
+ : "terminal-scoped Codex approval authority changed before key dispatch");
2214
+ }
2215
+ }
2216
+ : undefined
2217
+ });
2218
+ const terminalScoped = initialResolution.state === "eligible";
2219
+ const approval = terminalScoped
2220
+ ? await withStoreWriterLeaseAsync(storeDir, async () => {
2221
+ const current = await terminalListCliFacade.resolveTerminalScopedCodexApproval({
2222
+ options,
2223
+ terminal
2224
+ });
2225
+ if (current.state !== "eligible" ||
2226
+ current.boundary.token !== suppliedTerminalToken) {
2227
+ throw new Error(current.state === "blocked"
2228
+ ? current.reason
2229
+ : "terminal-scoped Codex approval authority changed while waiting for Store control");
2230
+ }
2231
+ return approveCurrentPrompt(true);
2232
+ })
2233
+ : await approveCurrentPrompt(false);
2234
+ if (!approval.approved) {
2235
+ printJson({
2236
+ conversation_id: conversationId,
2237
+ source: "terminal_control",
2238
+ approved: false,
2239
+ blocked: approval.blocked,
2240
+ reason: approval.reason,
2241
+ terminal_control: terminalControl,
2242
+ screen_excerpt: approval.screenExcerpt
2243
+ });
2244
+ return;
2245
+ }
2246
+ runtimeLog("info", "terminal_approval_send", {
2247
+ conversation_id: conversationId,
2248
+ agent,
2249
+ terminal_target: terminalControl.target,
2250
+ key: approval.key,
2251
+ keys: approval.keys,
2252
+ label: approval.label,
2253
+ decision_mode: approval.decisionMode,
2254
+ request_id: approval.requestId
2255
+ });
2256
+ printJson({
2257
+ conversation_id: conversationId,
2258
+ source: "terminal_control",
2259
+ approved: true,
2260
+ terminal_control: terminalControl,
2261
+ key: approval.key,
2262
+ keys: approval.keys,
2263
+ label: approval.label,
2264
+ approval_fingerprint: approval.fingerprint,
2265
+ decision_mode: approval.decisionMode,
2266
+ request_id: approval.requestId,
2267
+ terminal_scoped: terminalScoped,
2268
+ ...(terminalScoped
2269
+ ? {
2270
+ durable_dispatch_receipt: false,
2271
+ uncertain_outcome_recovery: "refresh status and inspect the live prompt; do not retry blindly"
2272
+ }
2273
+ : {})
2274
+ });
2275
+ }
2276
+ finally {
2277
+ releaseTerminalLock();
2278
+ }
2279
+ }
2280
+ async function prepareTerminalControlSend(request) {
2281
+ const { transaction, options, conversation, nextConversation, executor, message, recordRawAttachmentAfterSend = false, allowedPreMaterializationIdentity, allowedAdditionalIdentities = [], verifiedEmptyCodexHandoff, deferredCodexForegroundBinding, continuingTurnResponse = false } = request;
2282
+ const bridge = terminalBridgeEnabled(conversation);
2283
+ const route = bindTerminalDispatchRoute(transaction.scopes, transaction.resources);
2284
+ const { terminalControl, storeDir: lockedStoreDir, statePath, logPath } = route;
2285
+ const terminalBridge = createTerminalAgentBridge(options);
2286
+ const execution = terminalDispatchExecution(options, terminalBridge);
2287
+ const bridgeStartedAt = cliNow().toISOString();
2288
+ const agentTimeoutMinutes = Number(options.agentTimeoutMinutes ?? DEFAULT_AGENT_TIMEOUT_MINUTES);
2289
+ const agentHardTimeoutMinutes = positiveMinutes(options.agentHardTimeoutMinutes ?? DEFAULT_AGENT_HARD_TIMEOUT_MINUTES, "--agent-hard-timeout-minutes");
2290
+ const terminalPayload = terminalSubmissionPayload(String(message.body ?? ""));
2291
+ const terminalRequestHash = required(terminalBridgeRequestFingerprint(terminalPayload), "terminal request hash is unavailable");
2292
+ const presentationContext = { message, executor, terminalControl };
2293
+ const presentationPorts = {
2294
+ write: printJson,
2295
+ budget: budgetAction,
2296
+ nextAction: openClawYieldNextAction,
2297
+ summarize: textSummary
2298
+ };
2299
+ let previousDispatchLedger = resolveTerminalDispatchLedgerPaneIncarnation(terminalControl, loadTerminalBridgeDispatchLedger(terminalControl));
2300
+ previousDispatchLedger = reconcilePreparedTerminalDispatchLedger(terminalControl, previousDispatchLedger);
2301
+ const previousDispatchLifecycle = terminalDispatchLedgerLooksLifecycle(previousDispatchLedger);
2302
+ const previousDispatchOwner = execution.preflightRequiresOwner(previousDispatchLedger, previousDispatchLifecycle)
2303
+ ? loadTerminalDispatchLedgerOwner(previousDispatchLedger)
2304
+ : undefined;
2305
+ const dispatchPreflight = execution.evaluatePreflight({
2306
+ ledger: previousDispatchLedger,
2307
+ owner: previousDispatchOwner,
2308
+ conversation,
2309
+ requestHash: terminalRequestHash,
2310
+ requestText: terminalPayload,
2311
+ messageId: message.id,
2312
+ terminalTarget: terminalControl.target,
2313
+ ledgerLifecycle: previousDispatchLifecycle,
2314
+ statePathMatches: Boolean(previousDispatchOwner &&
2315
+ sameCanonicalStatePath(previousDispatchLedger.state_path, statePath)),
2316
+ continuingTurnResponse
2317
+ });
2318
+ if (dispatchPreflight.action === "replay") {
2319
+ presentTerminalDispatchReplay(dispatchPreflight, presentationContext, presentationPorts);
2320
+ return;
2321
+ }
2322
+ if (bridge) {
2323
+ assertNoUnresolvedTerminalBridgeSubmission(lockedStoreDir, terminalControl, conversation.conversation_id, terminalPayload);
2324
+ }
2325
+ const sendTakeover = isRecord(conversation.native_session_takeover)
2326
+ ? conversation.native_session_takeover
2327
+ : undefined;
2328
+ const terminalAgentPid = Number(sendTakeover?.terminal_agent_pid);
2329
+ const expectedManagedNativeThreadId = stringValue(sendTakeover?.terminal_agent_expected_session_id) ?? stringValue(sendTakeover?.terminal_agent_session_id);
2330
+ const currentNativeIdentity = deferredCodexForegroundBinding
2331
+ ?.candidateAcceptanceAnchor
2332
+ ? undefined
2333
+ : await execution.resolveCurrentNativeIdentity({
2334
+ agent: executor.kind,
2335
+ pid: terminalAgentPid,
2336
+ cwd: terminalControl.currentPath,
2337
+ preferredSessionId: allowedPreMaterializationIdentity
2338
+ ? expectedManagedNativeThreadId
2339
+ : undefined,
2340
+ allowedCompanionIdentity: allowedPreMaterializationIdentity,
2341
+ allowedAdditionalIdentities
2342
+ });
2343
+ const virginRawAttach = Boolean(recordRawAttachmentAfterSend &&
2344
+ !stringValue(sendTakeover?.terminal_agent_session_id));
2345
+ const pendingManagedNativeBinding = Boolean(!stringValue(sendTakeover?.terminal_agent_session_id) &&
2346
+ stringValue(sendTakeover?.terminal_agent_expected_session_id));
2347
+ const needsPostSendNativeBinding = virginRawAttach || pendingManagedNativeBinding;
2348
+ if (needsPostSendNativeBinding) {
2349
+ if (currentNativeIdentity &&
2350
+ !nativeIdentityMatchesCodexPreMaterialization(currentNativeIdentity, allowedPreMaterializationIdentity)) {
2351
+ throw new Error("native agent session appeared while preparing an unmaterialized terminal binding; refresh list and retry");
2352
+ }
2353
+ }
2354
+ else {
2355
+ execution.assertTurnIdentity({
2356
+ conversation,
2357
+ currentIdentity: currentNativeIdentity,
2358
+ operation: "send to"
2359
+ });
2360
+ }
2361
+ const preSendRuntime = {
2362
+ ...(deferredCodexForegroundBinding?.candidateAcceptanceAnchor
2363
+ ? terminalRuntimeForLiveIdentity({
2364
+ terminal: deferredCodexForegroundBinding.terminal,
2365
+ physicalOnly: true
2366
+ })
2367
+ : terminalRuntimeIdentityForConversation(nextConversation, terminalControl)),
2368
+ allowedPreMaterializationNativeIdentity: allowedPreMaterializationIdentity,
2369
+ allowedAdditionalNativeIdentities: allowedAdditionalIdentities,
2370
+ messageId: message.id
2371
+ };
2372
+ let preSendScreenFingerprint;
2373
+ let codexRolloutAcceptanceAnchor;
2374
+ let claudeTranscriptAnchor;
2375
+ const claudeHome = executor.kind === "claude"
2376
+ ? path.resolve(expandHome(options.claudeHome) ?? defaultClaudeHome())
2377
+ : undefined;
2378
+ try {
2379
+ const status = await terminalBridge.status(executor.kind, terminalControl, {
2380
+ scrollbackLines: Number(options.scrollbackLines ?? 120),
2381
+ runtime: preSendRuntime
2382
+ });
2383
+ if (verifiedEmptyCodexHandoff || deferredCodexForegroundBinding) {
2384
+ if (executor.kind !== "codex" ||
2385
+ status.reachable !== true ||
2386
+ status.approval_state.blocked === true ||
2387
+ !["idle", "unknown"].includes(status.activity_state)) {
2388
+ throw new Error(`Codex deferred foreground send is not at a safe prompt ` +
2389
+ `(${status.activity_state}: ${status.activity_reason})`);
2390
+ }
2391
+ }
2392
+ else {
2393
+ assertSafeTerminalSend(executor.kind, status);
2394
+ }
2395
+ if (executor.kind === "codex" && needsPostSendNativeBinding) {
2396
+ if (pendingManagedNativeBinding || allowedPreMaterializationIdentity) {
2397
+ const expectedForegroundId = stringValue(sendTakeover?.terminal_agent_expected_session_id);
2398
+ const foreground = createRuntimeTerminalAgentRegistry(options)
2399
+ .require("codex")
2400
+ .observeThreadLifecycle?.({
2401
+ operation: { kind: "new_thread" },
2402
+ phase: "before",
2403
+ screen: status.screen.excerpt ?? ""
2404
+ });
2405
+ if (!expectedForegroundId ||
2406
+ foreground?.status !== "observed" ||
2407
+ foreground.nativeThreadId !== expectedForegroundId) {
2408
+ throw new Error("Codex foreground thread changed after the managed /status proof; " +
2409
+ "refresh list before sending");
2410
+ }
2411
+ }
2412
+ await assertCodexComposerReadyForAutomatedInput({
2413
+ options,
2414
+ terminalControl
2415
+ });
2416
+ }
2417
+ if (bridge) {
2418
+ preSendScreenFingerprint = stringValue(status.screen.digest) ??
2419
+ terminalBridgeScreenFingerprint(status.screen.excerpt);
2420
+ if (executor.kind === "codex") {
2421
+ codexRolloutAcceptanceAnchor = execution.captureCodexAcceptanceAnchor({
2422
+ currentIdentity: currentNativeIdentity,
2423
+ expectedNativeThreadId: expectedManagedNativeThreadId,
2424
+ boundProcessUuid: stringValue(sendTakeover?.terminal_agent_process_uuid),
2425
+ boundProcessBirth: stringValue(sendTakeover?.terminal_agent_process_birth),
2426
+ allowedPreMaterializationIdentity,
2427
+ needsPostSendNativeBinding,
2428
+ candidateSetAnchor: deferredCodexForegroundBinding?.candidateAcceptanceAnchor
2429
+ });
2430
+ }
2431
+ else {
2432
+ claudeTranscriptAnchor = captureClaudeTranscriptAnchor({
2433
+ sessionId: preSendRuntime.sessionId,
2434
+ cwd: preSendRuntime.cwd,
2435
+ pid: preSendRuntime.pid,
2436
+ claudeHome,
2437
+ agentRows: loadClaudeAgentRows(options)
2438
+ });
2439
+ if (!claudeTranscriptAnchor) {
2440
+ throw new Error("the completion monitor could not bind an owner-private Claude transcript boundary");
2441
+ }
2442
+ }
2443
+ }
2444
+ }
2445
+ catch (error) {
2446
+ throw new Error(`refusing to send to ${executor.display_name} without a verified idle terminal: ${error instanceof Error ? error.message : String(error)}`);
2447
+ }
2448
+ return {
2449
+ route, bridge, terminalControl, lockedStoreDir, statePath, logPath,
2450
+ terminalBridge, execution, bridgeStartedAt,
2451
+ agentTimeoutMinutes, agentHardTimeoutMinutes,
2452
+ terminalPayload, terminalRequestHash,
2453
+ presentationContext, presentationPorts, previousDispatchLedger,
2454
+ sendTakeover, terminalAgentPid, needsPostSendNativeBinding, preSendRuntime,
2455
+ preSendScreenFingerprint, codexRolloutAcceptanceAnchor,
2456
+ claudeTranscriptAnchor, claudeHome
2457
+ };
2458
+ }
2459
+ async function resolveTerminalDispatchSubmissionOwner(prepared, request, application, stagedConversation) {
2460
+ const { transaction, options, executor, allowedPreMaterializationIdentity, allowedAdditionalIdentities = [], deferredCodexForegroundBinding: deferredBinding } = request;
2461
+ const { execution, needsPostSendNativeBinding, codexRolloutAcceptanceAnchor: acceptanceAnchor, terminalControl, terminalAgentPid, sendTakeover, terminalRequestHash, route } = prepared;
2462
+ const deferredForegroundScope = deferredBinding
2463
+ ? bindDeferredForegroundApplicationScope(transaction.scopes, transaction.resources)
2464
+ : undefined;
2465
+ const ready = (conversation, pending = false) => ({
2466
+ conversation, deferredCandidateBindingPending: pending
2467
+ });
2468
+ if (!needsPostSendNativeBinding) {
2469
+ return ready(stagedConversation);
2470
+ }
2471
+ let boundIdentity;
2472
+ let boundConversation;
2473
+ let bindingError;
2474
+ try {
2475
+ if (deferredBinding &&
2476
+ cliEnv().AKK_TEST_ALLOW_SYNTHETIC_TERMINAL_ACCEPTANCE !== "1" &&
2477
+ ![2, 3].includes(Number(acceptanceAnchor?.version))) {
2478
+ throw new Error("deferred Codex foreground binding requires a virgin rollout acceptance anchor");
2479
+ }
2480
+ boundIdentity = await execution.pollNativeIdentity({
2481
+ executor: executor.kind,
2482
+ terminalControl,
2483
+ pid: terminalAgentPid,
2484
+ expectedSessionId: stringValue(sendTakeover?.terminal_agent_expected_session_id),
2485
+ allowedPreMaterializationIdentity,
2486
+ allowedAdditionalIdentities,
2487
+ ...(deferredBinding &&
2488
+ acceptanceAnchor &&
2489
+ [2, 3].includes(acceptanceAnchor.version)
2490
+ ? {
2491
+ requiredCodexAcceptance: {
2492
+ anchor: acceptanceAnchor,
2493
+ requestHash: terminalRequestHash
2494
+ },
2495
+ attempts: Math.max(1, Math.ceil(DEFAULT_TERMINAL_ACCEPTANCE_TIMEOUT_MS /
2496
+ DEFAULT_TERMINAL_ACCEPTANCE_POLL_INTERVAL_MS)),
2497
+ delayMs: DEFAULT_TERMINAL_ACCEPTANCE_POLL_INTERVAL_MS
2498
+ }
2499
+ : {})
2500
+ });
2501
+ }
2502
+ catch (error) {
2503
+ bindingError = error instanceof Error ? error.message : String(error);
2504
+ }
2505
+ if (boundIdentity) {
2506
+ try {
2507
+ if (acceptanceAnchor &&
2508
+ (boundIdentity.processUuid !==
2509
+ acceptanceAnchor.process_uuid ||
2510
+ boundIdentity.processBirth !==
2511
+ acceptanceAnchor.process_birth)) {
2512
+ throw new Error("Codex process incarnation changed while its native thread materialized");
2513
+ }
2514
+ const expectedNativeThreadId = stringValue(sendTakeover?.terminal_agent_expected_session_id);
2515
+ if (expectedNativeThreadId &&
2516
+ boundIdentity.sessionId !== expectedNativeThreadId) {
2517
+ throw new Error(`native agent created thread ${boundIdentity.sessionId}, expected ` +
2518
+ `${expectedNativeThreadId}`);
2519
+ }
2520
+ const resolvedBoundConversation = execution.withNativeIdentity(stagedConversation, boundIdentity);
2521
+ boundConversation = resolvedBoundConversation;
2522
+ const bindingStoreDir = managedSessionStoreDirForConversation(resolvedBoundConversation);
2523
+ const bindingStatePath = stringValue(resolvedBoundConversation.state_path);
2524
+ if (!bindingStoreDir || !bindingStatePath) {
2525
+ throw new Error("managed Session Store is unavailable before native identity commit");
2526
+ }
2527
+ const identityRoute = withExactTerminalDispatchRoute(route, {
2528
+ terminalControl,
2529
+ terminalKey: terminalBridgeRuntimeKey(terminalControl),
2530
+ storeDir: bindingStoreDir,
2531
+ statePath: bindingStatePath,
2532
+ logPath: path.join(path.dirname(bindingStatePath), "events.ndjson")
2533
+ }, (exactRoute) => exactRoute);
2534
+ if (deferredBinding) {
2535
+ await deferredForegroundApplication(options, deferredBinding.terminal).commit({
2536
+ scope: required(deferredForegroundScope, "deferred foreground mutation scope is unavailable"),
2537
+ boundary: deferredForegroundBoundaryProjection(deferredBinding),
2538
+ identity: boundIdentity,
2539
+ acceptedAt: cliNow().toISOString()
2540
+ });
2541
+ }
2542
+ else {
2543
+ await assertNativeThreadHasExclusiveOwnership({
2544
+ options,
2545
+ agent: executor.kind,
2546
+ currentPid: terminalAgentPid,
2547
+ nativeThreadId: boundIdentity.sessionId,
2548
+ storeDir: identityRoute.storeDir,
2549
+ terminalControl,
2550
+ excludedManagedSessionId: sessionIdForConversation(boundConversation)
2551
+ });
2552
+ persistManagedSessionNativeIdentity({
2553
+ conversation: boundConversation,
2554
+ terminalControl,
2555
+ identity: boundIdentity,
2556
+ storeDir: identityRoute.storeDir
2557
+ });
2558
+ }
2559
+ if (acceptanceAnchor?.version === 2 &&
2560
+ cliEnv().AKK_TEST_EXIT_AFTER_VIRGIN_SESSION_BINDING === "1") {
2561
+ cliExit(86);
2562
+ }
2563
+ // Persist the observed identity before the full Turn-vs-Session check:
2564
+ // the provisional Turn and Session initially share only a binding id.
2565
+ execution.assertTurnIdentity({
2566
+ conversation: boundConversation,
2567
+ currentIdentity: boundIdentity,
2568
+ operation: "bind"
2569
+ });
2570
+ }
2571
+ catch (error) {
2572
+ bindingError = error instanceof Error ? error.message : String(error);
2573
+ boundIdentity = undefined;
2574
+ boundConversation = undefined;
2575
+ }
2576
+ }
2577
+ if (!boundIdentity && deferredBinding) {
2578
+ try {
2579
+ const deferredScope = required(deferredForegroundScope, "deferred foreground mutation scope is unavailable");
2580
+ const transfer = deferredScope.loadTransfer(deferredBinding.transferId);
2581
+ if (["committed", "resolved"].includes(transfer.status)) {
2582
+ const recoveredTarget = await deferredForegroundApplication(options, deferredBinding.terminal).resolve({
2583
+ scope: deferredScope,
2584
+ boundary: deferredForegroundBoundaryProjection(deferredBinding)
2585
+ });
2586
+ const binding = recoveredTarget.binding;
2587
+ if (!binding?.native_thread_id ||
2588
+ !binding.native_process.process_uuid ||
2589
+ !binding.native_process.process_birth ||
2590
+ !isCompleteNativeRollout(binding.native_process.rollout)) {
2591
+ throw new Error("resolved deferred target lacks exact native identity");
2592
+ }
2593
+ const recoveredIdentity = {
2594
+ sessionId: binding.native_thread_id,
2595
+ processUuid: binding.native_process.process_uuid,
2596
+ processBirth: binding.native_process.process_birth,
2597
+ rollout: binding.native_process.rollout,
2598
+ evidence: binding.native_process.evidence
2599
+ };
2600
+ const recoveredConversation = execution.withNativeIdentity(stagedConversation, recoveredIdentity);
2601
+ execution.assertTurnIdentity({
2602
+ conversation: recoveredConversation,
2603
+ currentIdentity: recoveredIdentity,
2604
+ operation: "recover deferred foreground binding for"
2605
+ });
2606
+ boundIdentity = recoveredIdentity;
2607
+ boundConversation = recoveredConversation;
2608
+ }
2609
+ }
2610
+ catch (error) {
2611
+ bindingError = error instanceof Error ? error.message : String(error);
2612
+ boundIdentity = undefined;
2613
+ boundConversation = undefined;
2614
+ }
2615
+ }
2616
+ if (boundIdentity && boundConversation) {
2617
+ return ready(boundConversation);
2618
+ }
2619
+ if (deferredBinding &&
2620
+ acceptanceAnchor?.version === 3 &&
2621
+ bindingError === undefined) {
2622
+ return ready(stagedConversation, true);
2623
+ }
2624
+ const bindingReason = bindingError
2625
+ ? `AKK dispatched terminal input but could not verify the new native agent session: ${bindingError}`
2626
+ : "AKK dispatched terminal input but no exact native agent session appeared within the binding window";
2627
+ if (deferredBinding) {
2628
+ try {
2629
+ deferredForegroundApplication(options, deferredBinding.terminal).markUncertain({
2630
+ scope: required(deferredForegroundScope, "deferred foreground mutation scope is unavailable"),
2631
+ boundary: deferredForegroundBoundaryProjection(deferredBinding),
2632
+ reason: bindingReason
2633
+ });
2634
+ }
2635
+ catch (error) {
2636
+ runtimeLog("error", "deferred_codex_foreground_uncertain_persist_failed", {
2637
+ transfer_id: deferredBinding.transferId,
2638
+ error: error instanceof Error ? error.message : String(error)
2639
+ });
2640
+ }
2641
+ }
2642
+ return {
2643
+ conversation: application.applyIdentityFailure(cliNow().toISOString(), bindingReason, deferredBinding
2644
+ ? undefined
2645
+ : (current) => quarantineManagedSessionBinding({
2646
+ conversation: current,
2647
+ reason: bindingReason,
2648
+ storeDir: route.storeDir
2649
+ })),
2650
+ failureReason: bindingReason
2651
+ };
2652
+ }
2653
+ function deferredTerminalInputNotStartedAt(transfer, terminalInputNotStartedAt) {
2654
+ if (terminalInputNotStartedAt === undefined ||
2655
+ transfer.input_stage === "none") {
2656
+ return undefined;
2657
+ }
2658
+ if (transfer.status === "dispatch_started" &&
2659
+ transfer.input_stage === "dispatch_started") {
2660
+ return terminalInputNotStartedAt;
2661
+ }
2662
+ throw new Error(`deferred foreground transfer ${transfer.transfer_id} has ` +
2663
+ `${transfer.input_stage} input evidence and cannot accept a ` +
2664
+ "terminal-input-not-started result");
2665
+ }
2666
+ function terminalDispatchRollbackRepositories({ request, prepared, deferredForegroundScope, preTransportRollback }) {
2667
+ const { options, conversation, deferredCodexForegroundBinding } = request;
2668
+ let rollbackPreTransportAttach = preTransportRollback;
2669
+ const rollbackBeforeInput = (route) => {
2670
+ if (!rollbackPreTransportAttach) {
2671
+ return true;
2672
+ }
2673
+ const rollback = rollbackPreTransportAttach;
2674
+ rollbackPreTransportAttach = undefined;
2675
+ try {
2676
+ rollback(route);
2677
+ return true;
2678
+ }
2679
+ catch (error) {
2680
+ runtimeLog("error", "raw_attach_pre_transport_rollback_failed", {
2681
+ conversation_id: conversation.conversation_id,
2682
+ terminal_target: route.terminalControl.target,
2683
+ error: error instanceof Error ? error.message : String(error)
2684
+ });
2685
+ return false;
2686
+ }
2687
+ };
2688
+ const restoreDeferred = (route, terminalInputNotStartedAt) => {
2689
+ if (!deferredCodexForegroundBinding) {
2690
+ return false;
2691
+ }
2692
+ const deferredScope = required(deferredForegroundScope, "deferred foreground mutation scope is unavailable");
2693
+ const transfer = deferredScope.loadTransfer(deferredCodexForegroundBinding.transferId);
2694
+ if (terminalBridgeRuntimeKey(route.terminalControl) !==
2695
+ terminalBridgeRuntimeKey(deferredCodexForegroundBinding.terminal.terminalControl) ||
2696
+ (transfer.state_path !== undefined &&
2697
+ !sameCanonicalStatePath(transfer.state_path, route.statePath))) {
2698
+ throw new Error("deferred Codex pre-input abort escaped its exact mutation resources");
2699
+ }
2700
+ const durableTerminalInputNotStartedAt = deferredTerminalInputNotStartedAt(transfer, terminalInputNotStartedAt);
2701
+ deferredRecoveryAdapter.abortPreparedDeferredForegroundTurn(deferredForegroundRecoveryAdapterPorts(), {
2702
+ options,
2703
+ scope: deferredScope,
2704
+ storeDir: route.storeDir,
2705
+ terminal: {
2706
+ ...deferredCodexForegroundBinding.terminal,
2707
+ terminalControl: route.terminalControl
2708
+ },
2709
+ transfer,
2710
+ boundary: deferredCodexForegroundBinding,
2711
+ terminalInputNotStartedAt: durableTerminalInputNotStartedAt
2712
+ });
2713
+ // The dedicated abort already durably restores the Turn, ledger,
2714
+ // transfer, and Sessions. Never invoke the attach rollback twice.
2715
+ rollbackPreTransportAttach = undefined;
2716
+ return true;
2717
+ };
2718
+ return { rollbackBeforeInput, restoreDeferred };
2719
+ }
2720
+ function createTerminalDispatchRuntime(request, prepared, rollback) {
2721
+ const { transaction, conversation, nextConversation, executor, message, recordRawAttachmentAfterSend = false } = request;
2722
+ const { bridge, terminalControl, lockedStoreDir, statePath, logPath, bridgeStartedAt, agentTimeoutMinutes, agentHardTimeoutMinutes, terminalPayload, terminalRequestHash, previousDispatchLedger, preSendScreenFingerprint, codexRolloutAcceptanceAnchor, claudeTranscriptAnchor, claudeHome } = prepared;
2723
+ const terminalMessage = message;
2724
+ const bridgeConversation = bridge
2725
+ ? dispatchReceipt.withTerminalBridgeState({
2726
+ conversation: nextConversation,
2727
+ message: terminalMessage,
2728
+ requestText: terminalPayload,
2729
+ startedAt: bridgeStartedAt,
2730
+ agentTimeoutMinutes,
2731
+ agentHardTimeoutMinutes,
2732
+ monitorLockVersion: monitorOwner.LOCK_VERSION,
2733
+ preSendScreenFingerprint,
2734
+ codexRolloutAcceptanceAnchor,
2735
+ claudeTranscriptAnchor,
2736
+ claudeHome
2737
+ })
2738
+ : nextConversation;
2739
+ const preparedConversation = withTerminalBridgeSubmission({
2740
+ conversation: bridgeConversation,
2741
+ messageId: message.id,
2742
+ messageType: terminalMessage.type,
2743
+ messageBody: String(message.body),
2744
+ requestText: terminalPayload,
2745
+ status: "prepared",
2746
+ preparedAt: bridgeStartedAt
2747
+ });
2748
+ const previousGenerationId = stringValue(previousDispatchLedger?.generation_id) ??
2749
+ stringValue(previousDispatchLedger?.message_id);
2750
+ const progress = {
2751
+ stagedConversation: preparedConversation
2752
+ };
2753
+ let preparedMessageEvent;
2754
+ const recordPostTransportBookkeepingFailure = (phase, error) => {
2755
+ const warning = error instanceof Error ? error.message : String(error);
2756
+ progress.bookkeepingWarning ??= warning;
2757
+ runtimeLog("warn", "terminal_message_post_transport_bookkeeping_failed", {
2758
+ conversation_id: conversation.conversation_id,
2759
+ terminal_target: terminalControl.target,
2760
+ phase,
2761
+ error: warning
2762
+ });
2763
+ };
2764
+ const { applicationPorts } = bindTerminalDispatchCapabilities({
2765
+ scopes: transaction.scopes,
2766
+ resources: transaction.resources,
2767
+ repositories: terminalDispatchCapabilityRepositories({
2768
+ previousLedger: previousDispatchLedger,
2769
+ preparedMessageEvent: () => {
2770
+ if (!preparedMessageEvent) {
2771
+ throw new Error("prepared message event was not validated");
2772
+ }
2773
+ return preparedMessageEvent;
2774
+ },
2775
+ restoreDeferred: rollback.restoreDeferred,
2776
+ rollbackBeforeInput: rollback.rollbackBeforeInput
2777
+ }),
2778
+ local: {
2779
+ synchronizeStageProgress: (current, stage, at) => {
2780
+ progress.stagedConversation = current;
2781
+ progress.textInjectedAt = stage === "text_injected"
2782
+ ? at
2783
+ : progress.textInjectedAt;
2784
+ progress.enterDispatchedAt = stage === "enter_dispatched"
2785
+ ? at
2786
+ : progress.enterDispatchedAt;
2787
+ },
2788
+ audit: {
2789
+ log: runtimeLog,
2790
+ recordBookkeepingFailure: recordPostTransportBookkeepingFailure,
2791
+ recordPersistenceFailure: (phase, error, current) => {
2792
+ runtimeLog("error", phase, {
2793
+ conversation_id: current.conversation_id,
2794
+ terminal_target: terminalControl.target,
2795
+ error: error instanceof Error ? error.message : String(error)
2796
+ });
2797
+ }
2798
+ }
2799
+ }
2800
+ });
2801
+ const application = new dispatchApplication.TerminalDispatchApplication({
2802
+ originalConversation: conversation,
2803
+ preparedConversation,
2804
+ message: terminalMessage,
2805
+ executor,
2806
+ terminalControl,
2807
+ receiptTerminalControl: terminalControlFromTakeover(isRecord(preparedConversation.native_session_takeover)
2808
+ ? preparedConversation.native_session_takeover
2809
+ : undefined),
2810
+ requestText: terminalPayload,
2811
+ requestHash: terminalRequestHash,
2812
+ preparedAt: bridgeStartedAt,
2813
+ statePath,
2814
+ eventLogPath: logPath,
2815
+ previousGenerationId,
2816
+ dispatcherPid: cliPid(),
2817
+ storeDir: lockedStoreDir,
2818
+ recordRawAttachmentAfterSend,
2819
+ ledgerBindingFields: terminalBindingLedgerFields
2820
+ }, applicationPorts);
2821
+ application.persistPrepared();
2822
+ return {
2823
+ application,
2824
+ terminalMessage,
2825
+ progress,
2826
+ recordPostTransportBookkeepingFailure,
2827
+ validatePreparedMessageEvent: () => {
2828
+ preparedMessageEvent = messageEvent(terminalMessage);
2829
+ }
2830
+ };
2831
+ }
2832
+ function terminalDispatchTransportLifecycle({ request, prepared, application, deferredForegroundScope }) {
2833
+ const { options, observedHandoff, verifiedEmptyCodexHandoff, deferredCodexForegroundBinding } = request;
2834
+ return prepared.execution.transportLifecycle({
2835
+ ...(observedHandoff
2836
+ ? {
2837
+ observedHandoff: {
2838
+ verify: (requireEmptyComposer) => assertObservedHandoffTransportBoundary({
2839
+ options,
2840
+ terminal: observedHandoff.terminal,
2841
+ transition: observedHandoff.transition,
2842
+ requireEmptyComposer
2843
+ })
2844
+ }
2845
+ }
2846
+ : {}),
2847
+ ...(verifiedEmptyCodexHandoff
2848
+ ? {
2849
+ verifiedEmptyHandoff: {
2850
+ verify: (requireEmptyComposer) => assertVerifiedEmptyCodexTransportBoundary({
2851
+ options,
2852
+ boundary: verifiedEmptyCodexHandoff,
2853
+ requireEmptyComposer
2854
+ })
2855
+ }
2856
+ }
2857
+ : {}),
2858
+ ...(deferredCodexForegroundBinding
2859
+ ? {
2860
+ deferredBinding: {
2861
+ verify: (requireEmptyComposer) => assertDeferredCodexForegroundBindingBoundary({
2862
+ options,
2863
+ scope: required(deferredForegroundScope, "deferred foreground mutation scope is unavailable"),
2864
+ boundary: deferredCodexForegroundBinding,
2865
+ expectedSourceStatus: "transitioning",
2866
+ requireNoDispatch: false,
2867
+ requireEmptyComposer
2868
+ }),
2869
+ begin: (at) => deferredForegroundApplication(options, deferredCodexForegroundBinding.terminal).begin({
2870
+ scope: required(deferredForegroundScope, "deferred foreground mutation scope is unavailable"),
2871
+ boundary: deferredForegroundBoundaryProjection(deferredCodexForegroundBinding),
2872
+ at
2873
+ }),
2874
+ advance: (stage, at) => deferredForegroundApplication(options, deferredCodexForegroundBinding.terminal).advance({
2875
+ scope: required(deferredForegroundScope, "deferred foreground mutation scope is unavailable"),
2876
+ boundary: deferredForegroundBoundaryProjection(deferredCodexForegroundBinding),
2877
+ stage,
2878
+ at
2879
+ })
2880
+ }
2881
+ }
2882
+ : {}),
2883
+ recordStage: (stage, at, afterDurable) => application.recordTransportStage(stage, at, afterDurable)
2884
+ });
2885
+ }
2886
+ async function terminalDispatchAcceptance({ request, prepared, conversation }) {
2887
+ const { options, executor } = request;
2888
+ const { execution, terminalControl } = prepared;
2889
+ const timeoutMs = positiveMilliseconds(options.terminalAcceptanceTimeoutMs ??
2890
+ DEFAULT_TERMINAL_ACCEPTANCE_TIMEOUT_MS, "--terminal-acceptance-timeout-ms");
2891
+ return execution.pollAcceptance({
2892
+ executor: executor.kind,
2893
+ conversation,
2894
+ terminalControl,
2895
+ timeoutMs,
2896
+ pollIntervalMs: Math.max(10, Math.min(timeoutMs, Number(options.terminalAcceptancePollIntervalMs ??
2897
+ DEFAULT_TERMINAL_ACCEPTANCE_POLL_INTERVAL_MS))),
2898
+ scrollbackLines: Number(options.scrollbackLines ?? 240)
2899
+ });
2900
+ }
2901
+ function launchAcceptedTerminalMonitor({ request, prepared, conversation, acceptance, recordFailure }) {
2902
+ const { options } = request;
2903
+ const { bridge, statePath, logPath, terminalControl, agentTimeoutMinutes, agentHardTimeoutMinutes } = prepared;
2904
+ if (!bridge ||
2905
+ !(acceptance.outcome === "agent_accepted" ||
2906
+ acceptance.outcome === "pending_acceptance")) {
2907
+ return undefined;
2908
+ }
2909
+ try {
2910
+ const monitor = startTerminalBridgeMonitorForConversation({
2911
+ conversation,
2912
+ statePath,
2913
+ logPath,
2914
+ options
2915
+ });
2916
+ if (monitor) {
2917
+ appendEvent(logPath, {
2918
+ ts: cliNow().toISOString(),
2919
+ conversation_id: conversation.conversation_id,
2920
+ event: "terminal_bridge_monitor_launch",
2921
+ pid: monitor.pid ?? null,
2922
+ terminal_control: terminalControl,
2923
+ phase: acceptance.outcome,
2924
+ agent_timeout_minutes: agentTimeoutMinutes,
2925
+ agent_hard_timeout_minutes: agentHardTimeoutMinutes
2926
+ });
2927
+ }
2928
+ return monitor;
2929
+ }
2930
+ catch (error) {
2931
+ recordFailure("monitor_launch", error);
2932
+ return undefined;
2933
+ }
2934
+ }
2935
+ function presentTerminalDispatchTransportFailure({ error, request, prepared, application, progress, deferredForegroundScope, bridgeMonitor }) {
2936
+ const { options, executor, message, deferredCodexForegroundBinding } = request;
2937
+ const { lockedStoreDir, terminalControl, presentationContext, presentationPorts } = prepared;
2938
+ if (!progress.textInjectedAt &&
2939
+ error instanceof TerminalInputNotStartedError) {
2940
+ renderTerminalZeroInputAbort(application.recordZeroInputAbort({
2941
+ failureKind: "transport",
2942
+ error,
2943
+ abortedAt: cliNow().toISOString()
2944
+ }), presentationContext, presentationPorts, bridgeMonitor?.pid);
2945
+ return;
2946
+ }
2947
+ const uncertainAt = cliNow().toISOString();
2948
+ const errorMessage = error instanceof Error ? error.message : String(error);
2949
+ if (deferredCodexForegroundBinding) {
2950
+ try {
2951
+ deferredForegroundApplication(options, deferredCodexForegroundBinding.terminal).markUncertain({
2952
+ scope: required(deferredForegroundScope, "deferred foreground mutation scope is unavailable"),
2953
+ boundary: deferredForegroundBoundaryProjection(deferredCodexForegroundBinding),
2954
+ reason: errorMessage
2955
+ });
2956
+ }
2957
+ catch (transferError) {
2958
+ runtimeLog("error", "deferred_codex_foreground_uncertain_persist_failed", {
2959
+ transfer_id: deferredCodexForegroundBinding.transferId,
2960
+ error: transferError instanceof Error
2961
+ ? transferError.message
2962
+ : String(transferError)
2963
+ });
2964
+ }
2965
+ }
2966
+ const uncertainConversation = application.applyUncertain(uncertainAt, error);
2967
+ const stalledConversationIds = stallOtherTerminalBridgeConversationsForUncertainDispatch({
2968
+ storeDir: lockedStoreDir,
2969
+ terminalControl,
2970
+ currentConversationId: uncertainConversation.conversation_id,
2971
+ uncertainMessageId: message.id
2972
+ });
2973
+ runtimeLog("error", "terminal_message_submit_uncertain", {
2974
+ conversation_id: uncertainConversation.conversation_id,
2975
+ agent: executor.kind,
2976
+ terminal_target: terminalControl.target,
2977
+ error: errorMessage,
2978
+ do_not_retry: true,
2979
+ stalled_conversation_ids: stalledConversationIds
2980
+ });
2981
+ presentTerminalUncertain({
2982
+ conversation: uncertainConversation,
2983
+ stalledConversationIds,
2984
+ textInjected: progress.textInjectedAt !== undefined,
2985
+ enterDispatched: progress.enterDispatchedAt !== undefined,
2986
+ monitorPid: bridgeMonitor?.pid
2987
+ }, presentationContext, presentationPorts);
2988
+ }
2989
+ async function runTerminalDispatchTransport({ request, prepared, application, progress, recordPostTransportBookkeepingFailure, deferredForegroundScope }) {
2990
+ const { executor } = request;
2991
+ const { terminalBridge, terminalControl, terminalPayload, preSendRuntime, codexRolloutAcceptanceAnchor, presentationContext, presentationPorts } = prepared;
2992
+ let bridgeMonitor;
2993
+ try {
2994
+ const transportLifecycle = terminalDispatchTransportLifecycle({
2995
+ request,
2996
+ prepared,
2997
+ application,
2998
+ deferredForegroundScope
2999
+ });
3000
+ await terminalBridge.send(executor.kind, terminalControl, terminalPayload, { runtime: preSendRuntime, ...transportLifecycle });
3001
+ if (!progress.enterDispatchedAt) {
3002
+ throw new Error("terminal bridge returned without an enter_dispatched receipt");
3003
+ }
3004
+ if (codexRolloutAcceptanceAnchor?.version === 2 &&
3005
+ cliEnv().AKK_TEST_EXIT_AFTER_VIRGIN_ENTER_DISPATCHED === "1") {
3006
+ cliExit(86);
3007
+ }
3008
+ const submissionOwner = await resolveTerminalDispatchSubmissionOwner(prepared, request, application, progress.stagedConversation);
3009
+ if ("failureReason" in submissionOwner) {
3010
+ presentTerminalIdentityFailure(submissionOwner.conversation, submissionOwner.failureReason, presentationContext, presentationPorts, bridgeMonitor?.pid);
3011
+ return { outcome: "handled" };
3012
+ }
3013
+ const acceptance = submissionOwner.deferredCandidateBindingPending
3014
+ ? { outcome: "pending_acceptance" }
3015
+ : await terminalDispatchAcceptance({
3016
+ request,
3017
+ prepared,
3018
+ conversation: submissionOwner.conversation
3019
+ });
3020
+ const deliveredConversation = application.applyAcceptance(submissionOwner.conversation, acceptance, cliNow().toISOString()).conversation;
3021
+ bridgeMonitor = launchAcceptedTerminalMonitor({
3022
+ request,
3023
+ prepared,
3024
+ conversation: deliveredConversation,
3025
+ acceptance,
3026
+ recordFailure: recordPostTransportBookkeepingFailure
3027
+ });
3028
+ return {
3029
+ outcome: "completed",
3030
+ conversation: deliveredConversation,
3031
+ acceptance,
3032
+ bridgeMonitor
3033
+ };
3034
+ }
3035
+ catch (error) {
3036
+ presentTerminalDispatchTransportFailure({
3037
+ error,
3038
+ request,
3039
+ prepared,
3040
+ application,
3041
+ progress,
3042
+ deferredForegroundScope,
3043
+ bridgeMonitor
3044
+ });
3045
+ return { outcome: "handled" };
3046
+ }
3047
+ }
3048
+ async function runTerminalControlSend(request) {
3049
+ const { transaction, recordMessageAfterSend = false, onTerminalPreflightVerified, deferredCodexForegroundBinding } = request;
3050
+ const prepared = await prepareTerminalControlSend(request);
3051
+ if (!prepared) {
3052
+ return;
3053
+ }
3054
+ const { route, presentationContext, presentationPorts } = prepared;
3055
+ const deferredForegroundScope = deferredCodexForegroundBinding
3056
+ ? bindDeferredForegroundApplicationScope(transaction.scopes, transaction.resources)
3057
+ : undefined;
3058
+ // A newly discovered raw terminal may not have an authoritative Session
3059
+ // yet. Commit that Session only after every pre-input terminal and native
3060
+ // acceptance check has passed, but before the Turn or dispatch ledger can
3061
+ // become durable. This prevents a failed virgin attach from leaving a
3062
+ // zero-identity `bound` Session that fences every later control action.
3063
+ if (deferredCodexForegroundBinding) {
3064
+ deferredForegroundScope?.assertBoundary(deferredForegroundBoundaryProjection(deferredCodexForegroundBinding));
3065
+ }
3066
+ const preTransportRollback = await onTerminalPreflightVerified?.(route);
3067
+ const rollback = terminalDispatchRollbackRepositories({
3068
+ request,
3069
+ prepared,
3070
+ deferredForegroundScope,
3071
+ preTransportRollback: typeof preTransportRollback === "function"
3072
+ ? preTransportRollback
3073
+ : undefined
3074
+ });
3075
+ const dispatch = createTerminalDispatchRuntime(request, prepared, rollback);
3076
+ const { application, progress, recordPostTransportBookkeepingFailure, validatePreparedMessageEvent } = dispatch;
3077
+ try {
3078
+ // Preserve legacy argument-evaluation priority inside the setup catch:
3079
+ // validate once before raw-attach bookkeeping, then let the gated
3080
+ // repository consume only this cached immutable event.
3081
+ application.recordPreparedBookkeeping(recordMessageAfterSend, cliEnv().AKK_TEST_TERMINAL_SETUP_FAILURE === "1", recordMessageAfterSend
3082
+ ? validatePreparedMessageEvent
3083
+ : undefined);
3084
+ }
3085
+ catch (error) {
3086
+ renderTerminalZeroInputAbort(application.recordZeroInputAbort({
3087
+ failureKind: "setup",
3088
+ error,
3089
+ abortedAt: cliNow().toISOString(),
3090
+ injectStatePersistenceFailure: cliEnv().AKK_TEST_ABORTED_STATE_PERSISTENCE_FAILURE === "1"
3091
+ }), presentationContext, presentationPorts, undefined);
3092
+ return;
3093
+ }
3094
+ const transport = await runTerminalDispatchTransport({
3095
+ request,
3096
+ prepared,
3097
+ application,
3098
+ progress,
3099
+ recordPostTransportBookkeepingFailure,
3100
+ deferredForegroundScope
3101
+ });
3102
+ if (transport.outcome === "handled") {
3103
+ return;
3104
+ }
3105
+ const deliveredConversation = transport.conversation;
3106
+ const acceptanceResult = transport.acceptance;
3107
+ const bridgeMonitor = transport.bridgeMonitor;
3108
+ const nativeAccepted = acceptanceResult?.outcome === "agent_accepted";
3109
+ const postSubmissionWarning = application.recordPostSubmissionBookkeeping(deliveredConversation, nativeAccepted, () => cliNow().toISOString());
3110
+ if (postSubmissionWarning !== undefined) {
3111
+ progress.bookkeepingWarning = postSubmissionWarning;
3112
+ }
3113
+ presentTerminalCompleted({
3114
+ conversation: deliveredConversation,
3115
+ acceptance: acceptanceResult,
3116
+ monitorPid: bridgeMonitor?.pid,
3117
+ bookkeepingWarning: progress.bookkeepingWarning
3118
+ }, presentationContext, presentationPorts);
3119
+ }
3120
+ export function createTerminalCommandCliFacade(dependencies) {
3121
+ const call = (operation) => terminalCommandContext.run(dependencies, operation);
3122
+ return Object.freeze({
3123
+ runSend: (options) => call(() => runSend(options)),
3124
+ runRespond: (options) => call(() => runRespond(options)),
3125
+ runApprove: (options) => call(() => runApprove(options))
3126
+ });
3127
+ }
3128
+ //# sourceMappingURL=terminal-command-cli-adapter.js.map