@scotthuang/agent-knock-knock 0.12.11 → 0.12.13

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 (344) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/README.md +43 -30
  3. package/dist/src/approval-policy.d.ts +11 -2
  4. package/dist/src/approval-policy.js +33 -7
  5. package/dist/src/approval-policy.js.map +1 -1
  6. package/dist/src/callback-cli-adapter.d.ts +47 -0
  7. package/dist/src/callback-cli-adapter.js +156 -0
  8. package/dist/src/callback-cli-adapter.js.map +1 -0
  9. package/dist/src/callback-outbox-policy.d.ts +95 -0
  10. package/dist/src/callback-outbox-policy.js +149 -0
  11. package/dist/src/callback-outbox-policy.js.map +1 -0
  12. package/dist/src/callback-outbox-service.d.ts +247 -0
  13. package/dist/src/callback-outbox-service.js +1053 -0
  14. package/dist/src/callback-outbox-service.js.map +1 -0
  15. package/dist/src/callback-outbox-settlement.d.ts +54 -0
  16. package/dist/src/callback-outbox-settlement.js +241 -0
  17. package/dist/src/callback-outbox-settlement.js.map +1 -0
  18. package/dist/src/canonical-json.d.ts +1 -0
  19. package/dist/src/canonical-json.js +14 -0
  20. package/dist/src/canonical-json.js.map +1 -0
  21. package/dist/src/claude-lifecycle-compatibility.js +15 -0
  22. package/dist/src/claude-lifecycle-compatibility.js.map +1 -1
  23. package/dist/src/claude-local-transcript-provider.d.ts +82 -1
  24. package/dist/src/claude-local-transcript-provider.js +873 -23
  25. package/dist/src/claude-local-transcript-provider.js.map +1 -1
  26. package/dist/src/cli-command-runtime.d.ts +14 -0
  27. package/dist/src/cli-command-runtime.js +164 -0
  28. package/dist/src/cli-command-runtime.js.map +1 -0
  29. package/dist/src/cli-core.d.ts +4 -42
  30. package/dist/src/cli-core.js +958 -28684
  31. package/dist/src/cli-core.js.map +1 -1
  32. package/dist/src/cli-runtime-context.d.ts +57 -0
  33. package/dist/src/cli-runtime-context.js +118 -0
  34. package/dist/src/cli-runtime-context.js.map +1 -0
  35. package/dist/src/codex-lifecycle-compatibility.js +2 -1
  36. package/dist/src/codex-lifecycle-compatibility.js.map +1 -1
  37. package/dist/src/codex-local-session-provider.d.ts +18 -0
  38. package/dist/src/codex-local-session-provider.js +67 -0
  39. package/dist/src/codex-local-session-provider.js.map +1 -1
  40. package/dist/src/codex-session-provider.js +1 -3
  41. package/dist/src/codex-session-provider.js.map +1 -1
  42. package/dist/src/conversation-trace.d.ts +30 -0
  43. package/dist/src/conversation-trace.js +185 -0
  44. package/dist/src/conversation-trace.js.map +1 -0
  45. package/dist/src/deferred-foreground-application-service.d.ts +84 -0
  46. package/dist/src/deferred-foreground-application-service.js +742 -0
  47. package/dist/src/deferred-foreground-application-service.js.map +1 -0
  48. package/dist/src/deferred-foreground-authority-cli-adapter.d.ts +78 -0
  49. package/dist/src/deferred-foreground-authority-cli-adapter.js +795 -0
  50. package/dist/src/deferred-foreground-authority-cli-adapter.js.map +1 -0
  51. package/dist/src/deferred-foreground-boundary.d.ts +61 -0
  52. package/dist/src/deferred-foreground-boundary.js +2 -0
  53. package/dist/src/deferred-foreground-boundary.js.map +1 -0
  54. package/dist/src/deferred-foreground-capability.d.ts +4 -0
  55. package/dist/src/deferred-foreground-capability.js +184 -0
  56. package/dist/src/deferred-foreground-capability.js.map +1 -0
  57. package/dist/src/deferred-foreground-preparation-cli-adapter.d.ts +53 -0
  58. package/dist/src/deferred-foreground-preparation-cli-adapter.js +210 -0
  59. package/dist/src/deferred-foreground-preparation-cli-adapter.js.map +1 -0
  60. package/dist/src/deferred-foreground-preparation-service.d.ts +72 -0
  61. package/dist/src/deferred-foreground-preparation-service.js +179 -0
  62. package/dist/src/deferred-foreground-preparation-service.js.map +1 -0
  63. package/dist/src/deferred-foreground-recovery-cli-adapter.d.ts +139 -0
  64. package/dist/src/deferred-foreground-recovery-cli-adapter.js +1125 -0
  65. package/dist/src/deferred-foreground-recovery-cli-adapter.js.map +1 -0
  66. package/dist/src/deferred-foreground-recovery-service.d.ts +48 -0
  67. package/dist/src/deferred-foreground-recovery-service.js +91 -0
  68. package/dist/src/deferred-foreground-recovery-service.js.map +1 -0
  69. package/dist/src/deferred-foreground-transfer-policy.d.ts +2 -0
  70. package/dist/src/deferred-foreground-transfer-policy.js +10 -0
  71. package/dist/src/deferred-foreground-transfer-policy.js.map +1 -0
  72. package/dist/src/deferred-foreground-transfer.js +46 -105
  73. package/dist/src/deferred-foreground-transfer.js.map +1 -1
  74. package/dist/src/doctor-capabilities.d.ts +9 -0
  75. package/dist/src/doctor-capabilities.js +22 -0
  76. package/dist/src/doctor-capabilities.js.map +1 -1
  77. package/dist/src/durable-json-file.d.ts +22 -0
  78. package/dist/src/durable-json-file.js +127 -0
  79. package/dist/src/durable-json-file.js.map +1 -0
  80. package/dist/src/file-lock-cli-adapter.d.ts +43 -0
  81. package/dist/src/file-lock-cli-adapter.js +150 -0
  82. package/dist/src/file-lock-cli-adapter.js.map +1 -0
  83. package/dist/src/herdr-terminal-control-provider.js +1 -3
  84. package/dist/src/herdr-terminal-control-provider.js.map +1 -1
  85. package/dist/src/install-doctor-command-adapter.d.ts +17 -0
  86. package/dist/src/install-doctor-command-adapter.js +225 -0
  87. package/dist/src/install-doctor-command-adapter.js.map +1 -0
  88. package/dist/src/live-lifecycle-evidence.js +76 -46
  89. package/dist/src/live-lifecycle-evidence.js.map +1 -1
  90. package/dist/src/live-lifecycle-smoke.js +74 -50
  91. package/dist/src/live-lifecycle-smoke.js.map +1 -1
  92. package/dist/src/managed-session.d.ts +2 -0
  93. package/dist/src/managed-session.js +24 -0
  94. package/dist/src/managed-session.js.map +1 -1
  95. package/dist/src/managed-turn-recovery-service.d.ts +86 -0
  96. package/dist/src/managed-turn-recovery-service.js +145 -0
  97. package/dist/src/managed-turn-recovery-service.js.map +1 -0
  98. package/dist/src/mutation-transaction.d.ts +48 -0
  99. package/dist/src/mutation-transaction.js +73 -0
  100. package/dist/src/mutation-transaction.js.map +1 -0
  101. package/dist/src/native-thread-lifecycle-cli-adapter.d.ts +154 -0
  102. package/dist/src/native-thread-lifecycle-cli-adapter.js +786 -0
  103. package/dist/src/native-thread-lifecycle-cli-adapter.js.map +1 -0
  104. package/dist/src/native-thread-lifecycle-ledger-cli-adapter.d.ts +25 -0
  105. package/dist/src/native-thread-lifecycle-ledger-cli-adapter.js +50 -0
  106. package/dist/src/native-thread-lifecycle-ledger-cli-adapter.js.map +1 -0
  107. package/dist/src/native-thread-lifecycle-query-service.d.ts +75 -0
  108. package/dist/src/native-thread-lifecycle-query-service.js +297 -0
  109. package/dist/src/native-thread-lifecycle-query-service.js.map +1 -0
  110. package/dist/src/native-thread-lifecycle-recovery-adapter.d.ts +45 -0
  111. package/dist/src/native-thread-lifecycle-recovery-adapter.js +154 -0
  112. package/dist/src/native-thread-lifecycle-recovery-adapter.js.map +1 -0
  113. package/dist/src/native-thread-lifecycle-recovery-service.d.ts +211 -0
  114. package/dist/src/native-thread-lifecycle-recovery-service.js +1093 -0
  115. package/dist/src/native-thread-lifecycle-recovery-service.js.map +1 -0
  116. package/dist/src/native-thread-resume-snapshot-policy.d.ts +73 -0
  117. package/dist/src/native-thread-resume-snapshot-policy.js +218 -0
  118. package/dist/src/native-thread-resume-snapshot-policy.js.map +1 -0
  119. package/dist/src/native-thread-resume-snapshot.d.ts +3 -61
  120. package/dist/src/native-thread-resume-snapshot.js +2 -143
  121. package/dist/src/native-thread-resume-snapshot.js.map +1 -1
  122. package/dist/src/native-thread-transition-application.d.ts +147 -0
  123. package/dist/src/native-thread-transition-application.js +974 -0
  124. package/dist/src/native-thread-transition-application.js.map +1 -0
  125. package/dist/src/native-thread-transition-policy.d.ts +127 -0
  126. package/dist/src/native-thread-transition-policy.js +154 -0
  127. package/dist/src/native-thread-transition-policy.js.map +1 -0
  128. package/dist/src/native-thread-transition-resource-adapter.d.ts +12 -0
  129. package/dist/src/native-thread-transition-resource-adapter.js +44 -0
  130. package/dist/src/native-thread-transition-resource-adapter.js.map +1 -0
  131. package/dist/src/native-thread-transition-settlement-service.d.ts +94 -0
  132. package/dist/src/native-thread-transition-settlement-service.js +134 -0
  133. package/dist/src/native-thread-transition-settlement-service.js.map +1 -0
  134. package/dist/src/native-thread-transition-verification-adapter.d.ts +62 -0
  135. package/dist/src/native-thread-transition-verification-adapter.js +451 -0
  136. package/dist/src/native-thread-transition-verification-adapter.js.map +1 -0
  137. package/dist/src/openclaw-callback-transport.d.ts +69 -0
  138. package/dist/src/openclaw-callback-transport.js +583 -0
  139. package/dist/src/openclaw-callback-transport.js.map +1 -0
  140. package/dist/src/openclaw-plugin-callback-adapter.d.ts +1 -0
  141. package/dist/src/openclaw-plugin-callback-adapter.js +423 -0
  142. package/dist/src/openclaw-plugin-callback-adapter.js.map +1 -0
  143. package/dist/src/openclaw-plugin-command-adapter.d.ts +16 -0
  144. package/dist/src/openclaw-plugin-command-adapter.js +1868 -0
  145. package/dist/src/openclaw-plugin-command-adapter.js.map +1 -0
  146. package/dist/src/openclaw-plugin-helpers.d.ts +27 -4
  147. package/dist/src/openclaw-plugin-helpers.js +356 -81
  148. package/dist/src/openclaw-plugin-helpers.js.map +1 -1
  149. package/dist/src/openclaw-plugin-schemas.d.ts +358 -0
  150. package/dist/src/openclaw-plugin-schemas.js +366 -0
  151. package/dist/src/openclaw-plugin-schemas.js.map +1 -0
  152. package/dist/src/openclaw-plugin-supervisor.d.ts +6 -0
  153. package/dist/src/openclaw-plugin-supervisor.js +123 -0
  154. package/dist/src/openclaw-plugin-supervisor.js.map +1 -0
  155. package/dist/src/openclaw-plugin.js +7 -2212
  156. package/dist/src/openclaw-plugin.js.map +1 -1
  157. package/dist/src/openclaw-private-authority-offers.d.ts +22 -0
  158. package/dist/src/openclaw-private-authority-offers.js +177 -0
  159. package/dist/src/openclaw-private-authority-offers.js.map +1 -0
  160. package/dist/src/protocol.d.ts +5 -0
  161. package/dist/src/protocol.js +59 -3
  162. package/dist/src/protocol.js.map +1 -1
  163. package/dist/src/session-store.d.ts +1 -0
  164. package/dist/src/session-store.js +84 -106
  165. package/dist/src/session-store.js.map +1 -1
  166. package/dist/src/store.d.ts +1 -0
  167. package/dist/src/store.js +5 -2
  168. package/dist/src/store.js.map +1 -1
  169. package/dist/src/terminal-acceptance-application-service.d.ts +65 -0
  170. package/dist/src/terminal-acceptance-application-service.js +63 -0
  171. package/dist/src/terminal-acceptance-application-service.js.map +1 -0
  172. package/dist/src/terminal-acceptance-cli-adapter.d.ts +210 -0
  173. package/dist/src/terminal-acceptance-cli-adapter.js +957 -0
  174. package/dist/src/terminal-acceptance-cli-adapter.js.map +1 -0
  175. package/dist/src/terminal-action-projection.d.ts +309 -0
  176. package/dist/src/terminal-action-projection.js +376 -0
  177. package/dist/src/terminal-action-projection.js.map +1 -0
  178. package/dist/src/terminal-agent-bridge.d.ts +3 -0
  179. package/dist/src/terminal-agent-bridge.js +29 -8
  180. package/dist/src/terminal-agent-bridge.js.map +1 -1
  181. package/dist/src/terminal-authority-policy.d.ts +141 -0
  182. package/dist/src/terminal-authority-policy.js +367 -0
  183. package/dist/src/terminal-authority-policy.js.map +1 -0
  184. package/dist/src/terminal-binding-authority.d.ts +118 -0
  185. package/dist/src/terminal-binding-authority.js +245 -0
  186. package/dist/src/terminal-binding-authority.js.map +1 -0
  187. package/dist/src/terminal-binding-reconciliation-service.d.ts +75 -0
  188. package/dist/src/terminal-binding-reconciliation-service.js +73 -0
  189. package/dist/src/terminal-binding-reconciliation-service.js.map +1 -0
  190. package/dist/src/terminal-command-cli-adapter.d.ts +380 -0
  191. package/dist/src/terminal-command-cli-adapter.js +3288 -0
  192. package/dist/src/terminal-command-cli-adapter.js.map +1 -0
  193. package/dist/src/terminal-control-ref.d.ts +4 -0
  194. package/dist/src/terminal-control-ref.js +11 -8
  195. package/dist/src/terminal-control-ref.js.map +1 -1
  196. package/dist/src/terminal-delegate-cli-adapter.d.ts +41 -0
  197. package/dist/src/terminal-delegate-cli-adapter.js +242 -0
  198. package/dist/src/terminal-delegate-cli-adapter.js.map +1 -0
  199. package/dist/src/terminal-dispatch-abort.d.ts +22 -0
  200. package/dist/src/terminal-dispatch-abort.js +30 -0
  201. package/dist/src/terminal-dispatch-abort.js.map +1 -0
  202. package/dist/src/terminal-dispatch-application.d.ts +115 -0
  203. package/dist/src/terminal-dispatch-application.js +478 -0
  204. package/dist/src/terminal-dispatch-application.js.map +1 -0
  205. package/dist/src/terminal-dispatch-capability.d.ts +63 -0
  206. package/dist/src/terminal-dispatch-capability.js +133 -0
  207. package/dist/src/terminal-dispatch-capability.js.map +1 -0
  208. package/dist/src/terminal-dispatch-completion-cli-adapter.d.ts +22 -0
  209. package/dist/src/terminal-dispatch-completion-cli-adapter.js +96 -0
  210. package/dist/src/terminal-dispatch-completion-cli-adapter.js.map +1 -0
  211. package/dist/src/terminal-dispatch-composition.d.ts +79 -0
  212. package/dist/src/terminal-dispatch-composition.js +2 -0
  213. package/dist/src/terminal-dispatch-composition.js.map +1 -0
  214. package/dist/src/terminal-dispatch-execution.d.ts +290 -0
  215. package/dist/src/terminal-dispatch-execution.js +916 -0
  216. package/dist/src/terminal-dispatch-execution.js.map +1 -0
  217. package/dist/src/terminal-dispatch-ledger-codec.d.ts +110 -0
  218. package/dist/src/terminal-dispatch-ledger-codec.js +399 -0
  219. package/dist/src/terminal-dispatch-ledger-codec.js.map +1 -0
  220. package/dist/src/terminal-dispatch-policy.d.ts +93 -0
  221. package/dist/src/terminal-dispatch-policy.js +122 -0
  222. package/dist/src/terminal-dispatch-policy.js.map +1 -0
  223. package/dist/src/terminal-dispatch-presenter.d.ts +65 -0
  224. package/dist/src/terminal-dispatch-presenter.js +258 -0
  225. package/dist/src/terminal-dispatch-presenter.js.map +1 -0
  226. package/dist/src/terminal-dispatch-receipt.d.ts +79 -0
  227. package/dist/src/terminal-dispatch-receipt.js +314 -0
  228. package/dist/src/terminal-dispatch-receipt.js.map +1 -0
  229. package/dist/src/terminal-dispatch-recovery-cli-adapter.d.ts +132 -0
  230. package/dist/src/terminal-dispatch-recovery-cli-adapter.js +1330 -0
  231. package/dist/src/terminal-dispatch-recovery-cli-adapter.js.map +1 -0
  232. package/dist/src/terminal-dispatch-recovery-service.d.ts +271 -0
  233. package/dist/src/terminal-dispatch-recovery-service.js +374 -0
  234. package/dist/src/terminal-dispatch-recovery-service.js.map +1 -0
  235. package/dist/src/terminal-dispatch-repository-cli-adapter.d.ts +35 -0
  236. package/dist/src/terminal-dispatch-repository-cli-adapter.js +483 -0
  237. package/dist/src/terminal-dispatch-repository-cli-adapter.js.map +1 -0
  238. package/dist/src/terminal-handoff-application-service.d.ts +7 -0
  239. package/dist/src/terminal-handoff-application-service.js +18 -0
  240. package/dist/src/terminal-handoff-application-service.js.map +1 -0
  241. package/dist/src/terminal-handoff-cli-adapter.d.ts +229 -0
  242. package/dist/src/terminal-handoff-cli-adapter.js +1389 -0
  243. package/dist/src/terminal-handoff-cli-adapter.js.map +1 -0
  244. package/dist/src/terminal-handoff-facts.d.ts +12 -0
  245. package/dist/src/terminal-handoff-facts.js +16 -0
  246. package/dist/src/terminal-handoff-facts.js.map +1 -0
  247. package/dist/src/terminal-identity-authority-cli-adapter.d.ts +318 -0
  248. package/dist/src/terminal-identity-authority-cli-adapter.js +1072 -0
  249. package/dist/src/terminal-identity-authority-cli-adapter.js.map +1 -0
  250. package/dist/src/terminal-identity-authority-service.d.ts +48 -0
  251. package/dist/src/terminal-identity-authority-service.js +45 -0
  252. package/dist/src/terminal-identity-authority-service.js.map +1 -0
  253. package/dist/src/terminal-list-cli-adapter.d.ts +274 -0
  254. package/dist/src/terminal-list-cli-adapter.js +2444 -0
  255. package/dist/src/terminal-list-cli-adapter.js.map +1 -0
  256. package/dist/src/terminal-list-renderer.d.ts +34 -0
  257. package/dist/src/terminal-list-renderer.js +643 -0
  258. package/dist/src/terminal-list-renderer.js.map +1 -0
  259. package/dist/src/terminal-maintenance-cli-adapter.d.ts +102 -0
  260. package/dist/src/terminal-maintenance-cli-adapter.js +1059 -0
  261. package/dist/src/terminal-maintenance-cli-adapter.js.map +1 -0
  262. package/dist/src/terminal-monitor-application-service.d.ts +275 -0
  263. package/dist/src/terminal-monitor-application-service.js +982 -0
  264. package/dist/src/terminal-monitor-application-service.js.map +1 -0
  265. package/dist/src/terminal-monitor-cli-adapter.d.ts +130 -0
  266. package/dist/src/terminal-monitor-cli-adapter.js +442 -0
  267. package/dist/src/terminal-monitor-cli-adapter.js.map +1 -0
  268. package/dist/src/terminal-monitor-decision-policy.d.ts +98 -0
  269. package/dist/src/terminal-monitor-decision-policy.js +246 -0
  270. package/dist/src/terminal-monitor-decision-policy.js.map +1 -0
  271. package/dist/src/terminal-monitor-launch-plan.d.ts +45 -0
  272. package/dist/src/terminal-monitor-launch-plan.js +94 -0
  273. package/dist/src/terminal-monitor-launch-plan.js.map +1 -0
  274. package/dist/src/terminal-monitor-ownership-policy.d.ts +31 -0
  275. package/dist/src/terminal-monitor-ownership-policy.js +64 -0
  276. package/dist/src/terminal-monitor-ownership-policy.js.map +1 -0
  277. package/dist/src/terminal-monitor-poll-policy.d.ts +48 -0
  278. package/dist/src/terminal-monitor-poll-policy.js +67 -0
  279. package/dist/src/terminal-monitor-poll-policy.js.map +1 -0
  280. package/dist/src/terminal-monitor-reconciliation-eligibility.d.ts +53 -0
  281. package/dist/src/terminal-monitor-reconciliation-eligibility.js +114 -0
  282. package/dist/src/terminal-monitor-reconciliation-eligibility.js.map +1 -0
  283. package/dist/src/terminal-monitor-state-cli-adapter.d.ts +106 -0
  284. package/dist/src/terminal-monitor-state-cli-adapter.js +1492 -0
  285. package/dist/src/terminal-monitor-state-cli-adapter.js.map +1 -0
  286. package/dist/src/terminal-monitor-state-reconciliation-service.d.ts +87 -0
  287. package/dist/src/terminal-monitor-state-reconciliation-service.js +106 -0
  288. package/dist/src/terminal-monitor-state-reconciliation-service.js.map +1 -0
  289. package/dist/src/terminal-monitor-supervision-cli-adapter.d.ts +122 -0
  290. package/dist/src/terminal-monitor-supervision-cli-adapter.js +685 -0
  291. package/dist/src/terminal-monitor-supervision-cli-adapter.js.map +1 -0
  292. package/dist/src/terminal-process-facts.d.ts +2 -0
  293. package/dist/src/terminal-process-facts.js +19 -0
  294. package/dist/src/terminal-process-facts.js.map +1 -0
  295. package/dist/src/terminal-process-source.d.ts +6 -4
  296. package/dist/src/terminal-process-source.js +25 -20
  297. package/dist/src/terminal-process-source.js.map +1 -1
  298. package/dist/src/terminal-runtime-cli-adapter.d.ts +80 -0
  299. package/dist/src/terminal-runtime-cli-adapter.js +488 -0
  300. package/dist/src/terminal-runtime-cli-adapter.js.map +1 -0
  301. package/dist/src/terminal-scoped-approval-authority.d.ts +62 -0
  302. package/dist/src/terminal-scoped-approval-authority.js +281 -0
  303. package/dist/src/terminal-scoped-approval-authority.js.map +1 -0
  304. package/dist/src/terminal-selector-projection-service.d.ts +26 -0
  305. package/dist/src/terminal-selector-projection-service.js +81 -0
  306. package/dist/src/terminal-selector-projection-service.js.map +1 -0
  307. package/dist/src/terminal-status-cli-adapter.d.ts +126 -0
  308. package/dist/src/terminal-status-cli-adapter.js +577 -0
  309. package/dist/src/terminal-status-cli-adapter.js.map +1 -0
  310. package/dist/src/terminal-status-facts.d.ts +56 -0
  311. package/dist/src/terminal-status-facts.js +199 -0
  312. package/dist/src/terminal-status-facts.js.map +1 -0
  313. package/dist/src/terminal-submission-acceptance.d.ts +62 -145
  314. package/dist/src/terminal-submission-acceptance.js +555 -222
  315. package/dist/src/terminal-submission-acceptance.js.map +1 -1
  316. package/dist/src/terminal-submission-facts.d.ts +123 -0
  317. package/dist/src/terminal-submission-facts.js +205 -0
  318. package/dist/src/terminal-submission-facts.js.map +1 -0
  319. package/dist/src/terminal-turn-binding-authority-cli-adapter.d.ts +14 -0
  320. package/dist/src/terminal-turn-binding-authority-cli-adapter.js +90 -0
  321. package/dist/src/terminal-turn-binding-authority-cli-adapter.js.map +1 -0
  322. package/dist/src/terminal-watch-callback-cli-adapter.d.ts +20 -0
  323. package/dist/src/terminal-watch-callback-cli-adapter.js +55 -0
  324. package/dist/src/terminal-watch-callback-cli-adapter.js.map +1 -0
  325. package/dist/src/terminal-watch-cli-adapter.d.ts +64 -0
  326. package/dist/src/terminal-watch-cli-adapter.js +508 -0
  327. package/dist/src/terminal-watch-cli-adapter.js.map +1 -0
  328. package/dist/src/terminal-watch-service.d.ts +97 -0
  329. package/dist/src/terminal-watch-service.js +714 -0
  330. package/dist/src/terminal-watch-service.js.map +1 -0
  331. package/dist/src/terminal-watch-store.d.ts +149 -0
  332. package/dist/src/terminal-watch-store.js +861 -0
  333. package/dist/src/terminal-watch-store.js.map +1 -0
  334. package/dist/src/value-guards.d.ts +4 -0
  335. package/dist/src/value-guards.js +12 -0
  336. package/dist/src/value-guards.js.map +1 -0
  337. package/dist/src/verified-dead-agent-policy.d.ts +192 -0
  338. package/dist/src/verified-dead-agent-policy.js +262 -0
  339. package/dist/src/verified-dead-agent-policy.js.map +1 -0
  340. package/docs/quickstart-herdr.md +8 -0
  341. package/docs/quickstart-tmux.md +33 -6
  342. package/openclaw.plugin.json +11 -3
  343. package/package.json +3 -1
  344. package/templates/openclaw-skills/agent-knock-knock/SKILL.md +58 -40
@@ -0,0 +1,3288 @@
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
+ assertAutoApprovalCallbackRoute({
1634
+ options,
1635
+ conversation: loaded.conversation,
1636
+ statePath
1637
+ });
1638
+ const conversation = await migrateLegacyTerminalAgentIdentity({
1639
+ ...loaded,
1640
+ options
1641
+ });
1642
+ const nativeTakeover = isRecord(conversation.native_session_takeover)
1643
+ ? conversation.native_session_takeover
1644
+ : undefined;
1645
+ const terminalControl = terminalControlFromTakeover(nativeTakeover);
1646
+ if (!terminalControl) {
1647
+ throw new Error(`conversation ${conversation.conversation_id} is not controlled through a terminal`);
1648
+ }
1649
+ const autoApproved = options.autoApproved === true;
1650
+ const callbackAuthority = autoApprovalCallbackAuthorityFromOptions(options);
1651
+ if (!["waiting_for_agent", "waiting_for_openclaw"].includes(conversation.status) &&
1652
+ !(autoApproved && callbackAuthority)) {
1653
+ throw new Error(`cannot approve ${conversation.conversation_id}; conversation is ${conversation.status}`);
1654
+ }
1655
+ const executor = executorForConversation(conversation);
1656
+ const monitoredApproval = isRecord(nativeTakeover?.["terminal_bridge_approval"])
1657
+ ? nativeTakeover.terminal_bridge_approval
1658
+ : undefined;
1659
+ const suppliedExpectedFingerprint = stringValue(options.expectedApprovalFingerprint);
1660
+ const expectedFingerprint = suppliedExpectedFingerprint ??
1661
+ stringValue(monitoredApproval?.fingerprint);
1662
+ const claudeScreenApproval = executor.kind === "claude";
1663
+ if (claudeScreenApproval) {
1664
+ const monitoredState = isRecord(monitoredApproval?.approval_state)
1665
+ ? monitoredApproval.approval_state
1666
+ : undefined;
1667
+ const pendingDispatch = isRecord(nativeTakeover?.terminal_bridge_approval_dispatch)
1668
+ ? nativeTakeover.terminal_bridge_approval_dispatch
1669
+ : undefined;
1670
+ const lastApprovalFingerprint = stringValue(nativeTakeover?.terminal_bridge_last_approval_fingerprint);
1671
+ const lastApprovalMessageId = stringValue(nativeTakeover?.terminal_bridge_last_approval_message_id);
1672
+ const currentMessageId = stringValue(nativeTakeover?.terminal_bridge_message_id);
1673
+ const approvalResolvedAt = validTimestampMs(nativeTakeover?.terminal_bridge_approval_resolved_at);
1674
+ if (autoApproved &&
1675
+ callbackAuthority === undefined &&
1676
+ monitoredApproval === undefined &&
1677
+ pendingDispatch === undefined &&
1678
+ conversation.status === "waiting_for_agent" &&
1679
+ suppliedExpectedFingerprint !== undefined &&
1680
+ suppliedExpectedFingerprint === lastApprovalFingerprint &&
1681
+ lastApprovalMessageId !== undefined &&
1682
+ lastApprovalMessageId === currentMessageId &&
1683
+ approvalResolvedAt !== undefined) {
1684
+ const monitor = ensureTerminalBridgeMonitorAfterApproval({
1685
+ conversation,
1686
+ statePath,
1687
+ logPath,
1688
+ terminalControl,
1689
+ options,
1690
+ reason: "approval_already_resolved"
1691
+ });
1692
+ printJson({
1693
+ conversation,
1694
+ approved: false,
1695
+ already_approved: true,
1696
+ blocked: false,
1697
+ reason: "Claude screen approval fingerprint was already consumed",
1698
+ terminal_control: terminalControl,
1699
+ monitor_pid: monitor.monitorPid ?? null,
1700
+ monitor_handoff_pid: monitor.handoffWatchdog?.pid ?? null
1701
+ });
1702
+ return;
1703
+ }
1704
+ // A provenance-bound callback whose approval is already absent must reach
1705
+ // the terminal+state lock. Only the persisted callback message and the
1706
+ // locked consumed-approval receipt may classify it as an idempotent replay.
1707
+ if (callbackAuthority === undefined || monitoredApproval !== undefined) {
1708
+ const notifiedAt = validTimestampMs(monitoredApproval?.notified_at);
1709
+ if (conversation.status !== "waiting_for_openclaw" ||
1710
+ monitoredState?.decision_mode !== "keys" ||
1711
+ !stringValue(monitoredApproval?.fingerprint)) {
1712
+ printJson({
1713
+ conversation,
1714
+ approved: false,
1715
+ blocked: true,
1716
+ reason: "Claude screen approval requires a current managed-turn approval notification",
1717
+ terminal_control: terminalControl
1718
+ });
1719
+ return;
1720
+ }
1721
+ if (notifiedAt === undefined ||
1722
+ cliNowMs() - notifiedAt > CLAUDE_SCREEN_APPROVAL_TTL_MS) {
1723
+ printJson({
1724
+ conversation,
1725
+ approved: false,
1726
+ blocked: true,
1727
+ reason: "Claude screen approval expired; inspect and resolve the terminal manually",
1728
+ terminal_control: terminalControl
1729
+ });
1730
+ return;
1731
+ }
1732
+ if (!suppliedExpectedFingerprint ||
1733
+ expectedFingerprint !== monitoredApproval?.fingerprint) {
1734
+ printJson({
1735
+ conversation,
1736
+ approved: false,
1737
+ blocked: true,
1738
+ reason: "Claude screen approval requires the latest notified fingerprint",
1739
+ terminal_control: terminalControl
1740
+ });
1741
+ return;
1742
+ }
1743
+ if (pendingDispatch?.state === "reserved" &&
1744
+ pendingDispatch.terminal_bridge_message_id ===
1745
+ nativeTakeover?.terminal_bridge_message_id) {
1746
+ printJson({
1747
+ conversation,
1748
+ approved: false,
1749
+ blocked: true,
1750
+ reason: "a previous Claude approval dispatch has an uncertain outcome; inspect and resolve the terminal manually",
1751
+ terminal_control: terminalControl
1752
+ });
1753
+ return;
1754
+ }
1755
+ if (expectedFingerprint ===
1756
+ stringValue(nativeTakeover?.terminal_bridge_last_approval_fingerprint)) {
1757
+ printJson({
1758
+ conversation,
1759
+ approved: false,
1760
+ blocked: true,
1761
+ reason: "Claude screen approval fingerprint was already consumed",
1762
+ terminal_control: terminalControl
1763
+ });
1764
+ return;
1765
+ }
1766
+ }
1767
+ }
1768
+ return runManagedApprovalDispatch({
1769
+ options, conversation, statePath, logPath,
1770
+ nativeTakeover: nativeTakeover,
1771
+ terminalControl, executor, monitoredApproval, expectedFingerprint,
1772
+ autoApproved, claudeScreenApproval
1773
+ });
1774
+ }
1775
+ function approvalPolicyCandidateForInspection({ agent, currentTerminalControl, inspection, fingerprint }) {
1776
+ const evidence = inspection.approval.approvable
1777
+ ? inspection.approval.policyEvidence
1778
+ : undefined;
1779
+ return {
1780
+ agent,
1781
+ kind: evidence?.kind ?? inspection.approval.promptKind ?? "unknown",
1782
+ decisionMode: inspection.approval.approvable
1783
+ ? inspection.approval.action.mode ?? "keys"
1784
+ : undefined,
1785
+ command: evidence?.command ?? inspection.approval.command,
1786
+ cwd: evidence?.cwd ?? inspection.approval.cwd ?? currentTerminalControl.currentPath,
1787
+ fingerprint: fingerprint ?? "",
1788
+ terminalTarget: currentTerminalControl.target,
1789
+ ...(evidence?.source === "claude_transcript"
1790
+ ? {
1791
+ evidenceSource: "claude_transcript",
1792
+ evidenceFingerprint: evidence.evidenceFingerprint
1793
+ }
1794
+ : {})
1795
+ };
1796
+ }
1797
+ async function runManagedApprovalDispatch({ options, conversation, statePath, logPath, nativeTakeover, terminalControl, executor, monitoredApproval, expectedFingerprint, autoApproved, claudeScreenApproval }) {
1798
+ const policyRuleId = stringValue(options.policyRuleId);
1799
+ const policyFingerprint = stringValue(options.policyFingerprint);
1800
+ const autoApprovalPolicy = autoApproved
1801
+ ? parseJsonOption(options.autoApprovalPolicyJson, "--auto-approval-policy-json")
1802
+ : undefined;
1803
+ let executorPolicyDecision;
1804
+ const releaseTerminalLock = acquireTerminalBridgeSendLock(storeDirFromOptions(options), terminalControl, { timeoutMs: 30000 });
1805
+ let terminalLockReleased = false;
1806
+ const releaseApprovalTerminalLock = () => {
1807
+ if (!terminalLockReleased) {
1808
+ terminalLockReleased = true;
1809
+ releaseTerminalLock();
1810
+ }
1811
+ };
1812
+ let releaseStateLock;
1813
+ let approvalDispatchReserved = false;
1814
+ const releaseApprovalStateLock = () => {
1815
+ if (releaseStateLock) {
1816
+ const release = releaseStateLock;
1817
+ releaseStateLock = undefined;
1818
+ release();
1819
+ }
1820
+ };
1821
+ const writerStoreDir = pathsForConversationDir(path.dirname(statePath)).storeDir;
1822
+ const runApprovalWithStateLock = async () => {
1823
+ let approval;
1824
+ let lockedConversation = conversation;
1825
+ const currentConversation = loadState(statePath);
1826
+ const currentTakeover = isRecord(currentConversation.native_session_takeover)
1827
+ ? currentConversation.native_session_takeover
1828
+ : undefined;
1829
+ const currentControl = terminalControlFromTakeover(currentTakeover);
1830
+ const currentApproval = isRecord(currentTakeover?.terminal_bridge_approval)
1831
+ ? currentTakeover.terminal_bridge_approval
1832
+ : undefined;
1833
+ const callbackAuthorityState = assertAutoApprovalCallbackAuthority({
1834
+ options,
1835
+ conversation: currentConversation,
1836
+ statePath,
1837
+ takeover: currentTakeover,
1838
+ approval: currentApproval,
1839
+ expectedFingerprint
1840
+ });
1841
+ if (!currentControl ||
1842
+ !terminalControlsShareIncarnation(currentControl, terminalControl) ||
1843
+ (callbackAuthorityState !== "already_approved" &&
1844
+ (currentConversation.status !== conversation.status ||
1845
+ currentTakeover?.terminal_bridge_message_id !==
1846
+ nativeTakeover?.terminal_bridge_message_id ||
1847
+ (claudeScreenApproval &&
1848
+ currentApproval?.fingerprint !== monitoredApproval?.fingerprint)))) {
1849
+ throw new Error("approval state changed while waiting for terminal control; refresh status and retry");
1850
+ }
1851
+ lockedConversation = currentConversation;
1852
+ if (callbackAuthorityState === "already_approved") {
1853
+ releaseApprovalStateLock();
1854
+ printJson({
1855
+ conversation: lockedConversation,
1856
+ approved: false,
1857
+ already_approved: true,
1858
+ blocked: false,
1859
+ reason: "automatic approval callback was already handled",
1860
+ terminal_control: currentControl
1861
+ });
1862
+ return;
1863
+ }
1864
+ assertManagedTerminalDispatchOwner({
1865
+ storeDir: writerStoreDir,
1866
+ conversation: currentConversation,
1867
+ terminalControl: currentControl,
1868
+ action: "approve"
1869
+ });
1870
+ const currentRuntimeIdentity = terminalRuntimeIdentityForConversation(currentConversation, currentControl);
1871
+ approval = await createTerminalAgentBridge(options).approve(executor.kind, currentControl, {
1872
+ expectedFingerprint,
1873
+ scrollbackLines: Number(options.scrollbackLines ?? 120),
1874
+ runtime: currentRuntimeIdentity,
1875
+ managedRequest: terminalDurableRequestForConversation(currentConversation, currentControl),
1876
+ requiredDecisionMode: autoApproved && executor.kind === "claude" ? "keys" : undefined,
1877
+ authorize: autoApproved
1878
+ ? ({ agent, terminalControl: currentTerminalControl, inspection, fingerprint }) => {
1879
+ if (!autoApprovalPolicy) {
1880
+ return {
1881
+ approved: false,
1882
+ reason: "automatic approval requires an executor-side policy"
1883
+ };
1884
+ }
1885
+ const candidate = approvalPolicyCandidateForInspection({
1886
+ agent,
1887
+ currentTerminalControl,
1888
+ inspection,
1889
+ fingerprint
1890
+ });
1891
+ executorPolicyDecision = evaluateApprovalPolicy({
1892
+ policy: autoApprovalPolicy,
1893
+ candidate
1894
+ });
1895
+ if (executorPolicyDecision.action !== "approve") {
1896
+ return {
1897
+ approved: false,
1898
+ reason: `executor-side auto-approval policy rejected the current request: ${executorPolicyDecision.reason}`
1899
+ };
1900
+ }
1901
+ if (policyRuleId && executorPolicyDecision.ruleId !== policyRuleId) {
1902
+ return {
1903
+ approved: false,
1904
+ reason: "executor-side auto-approval rule changed before execution"
1905
+ };
1906
+ }
1907
+ if (policyFingerprint &&
1908
+ executorPolicyDecision.policyFingerprint !== policyFingerprint) {
1909
+ return {
1910
+ approved: false,
1911
+ reason: "executor-side auto-approval policy changed before execution"
1912
+ };
1913
+ }
1914
+ return { approved: true };
1915
+ }
1916
+ : undefined,
1917
+ beforeKeyDispatch: claudeScreenApproval
1918
+ ? ({ fingerprint, terminalControl: dispatchControl, inspection, keys }) => {
1919
+ if (autoApproved) {
1920
+ if (!autoApprovalPolicy) {
1921
+ throw new Error("automatic approval requires an executor-side policy before dispatch");
1922
+ }
1923
+ const freshPolicyDecision = evaluateApprovalPolicy({
1924
+ policy: autoApprovalPolicy,
1925
+ candidate: approvalPolicyCandidateForInspection({
1926
+ agent: executor.kind,
1927
+ currentTerminalControl: dispatchControl,
1928
+ inspection,
1929
+ fingerprint
1930
+ })
1931
+ });
1932
+ if (freshPolicyDecision.action !== "approve") {
1933
+ throw new Error(`executor-side auto-approval policy rejected the recaptured request: ${freshPolicyDecision.reason}`);
1934
+ }
1935
+ if (executorPolicyDecision?.ruleId &&
1936
+ freshPolicyDecision.ruleId !== executorPolicyDecision.ruleId) {
1937
+ throw new Error("executor-side auto-approval rule changed after recapture");
1938
+ }
1939
+ if (policyRuleId && freshPolicyDecision.ruleId !== policyRuleId) {
1940
+ throw new Error("executor-side auto-approval rule changed before dispatch");
1941
+ }
1942
+ if (policyFingerprint &&
1943
+ freshPolicyDecision.policyFingerprint !== policyFingerprint) {
1944
+ throw new Error("executor-side auto-approval policy changed before dispatch");
1945
+ }
1946
+ executorPolicyDecision = freshPolicyDecision;
1947
+ }
1948
+ if (approvalDispatchReserved) {
1949
+ throw new Error("Claude approval dispatch was already reserved");
1950
+ }
1951
+ approvalDispatchReserved = true;
1952
+ if (!releaseStateLock) {
1953
+ throw new Error("approval state lock was released before terminal dispatch");
1954
+ }
1955
+ const latestConversation = loadState(statePath);
1956
+ const latestTakeover = isRecord(latestConversation.native_session_takeover)
1957
+ ? latestConversation.native_session_takeover
1958
+ : undefined;
1959
+ const latestControl = terminalControlFromTakeover(latestTakeover);
1960
+ const latestApproval = isRecord(latestTakeover?.terminal_bridge_approval)
1961
+ ? latestTakeover.terminal_bridge_approval
1962
+ : undefined;
1963
+ const latestNotifiedAt = validTimestampMs(latestApproval?.notified_at);
1964
+ const latestApprovalState = isRecord(latestApproval?.approval_state)
1965
+ ? latestApproval.approval_state
1966
+ : undefined;
1967
+ const latestPolicyEvidence = isRecord(latestApprovalState?.policy_evidence)
1968
+ ? latestApprovalState.policy_evidence
1969
+ : undefined;
1970
+ const recapturedPolicyEvidence = inspection.approval.approvable
1971
+ ? inspection.approval.policyEvidence
1972
+ : undefined;
1973
+ const latestDispatch = isRecord(latestTakeover?.terminal_bridge_approval_dispatch)
1974
+ ? latestTakeover.terminal_bridge_approval_dispatch
1975
+ : undefined;
1976
+ if (!latestTakeover ||
1977
+ latestConversation.status !== "waiting_for_openclaw" ||
1978
+ latestTakeover.terminal_bridge_message_id !==
1979
+ nativeTakeover?.terminal_bridge_message_id ||
1980
+ latestApproval?.fingerprint !== fingerprint ||
1981
+ latestNotifiedAt === undefined ||
1982
+ cliNowMs() - latestNotifiedAt > CLAUDE_SCREEN_APPROVAL_TTL_MS ||
1983
+ expectedFingerprint !== fingerprint ||
1984
+ !terminalControlsShareIncarnation(latestControl, dispatchControl) ||
1985
+ (autoApproved &&
1986
+ (latestPolicyEvidence?.source !== "claude_transcript" ||
1987
+ latestPolicyEvidence.evidence_fingerprint !==
1988
+ recapturedPolicyEvidence?.evidenceFingerprint))) {
1989
+ throw new Error("approval state changed before terminal dispatch; refresh status and retry");
1990
+ }
1991
+ if (latestDispatch?.state === "reserved" &&
1992
+ latestDispatch.terminal_bridge_message_id ===
1993
+ latestTakeover.terminal_bridge_message_id) {
1994
+ throw new Error("a previous Claude approval dispatch has an uncertain outcome; inspect and resolve the terminal manually");
1995
+ }
1996
+ const reservedAt = cliNow().toISOString();
1997
+ const reservedConversation = {
1998
+ ...latestConversation,
1999
+ native_session_takeover: {
2000
+ ...latestTakeover,
2001
+ terminal_bridge_approval_dispatch: {
2002
+ state: "reserved",
2003
+ attempt_id: randomUUID(),
2004
+ fingerprint,
2005
+ keys,
2006
+ terminal_target: dispatchControl.target,
2007
+ terminal_bridge_message_id: latestTakeover.terminal_bridge_message_id,
2008
+ reserved_at: reservedAt
2009
+ }
2010
+ },
2011
+ updated_at: reservedAt
2012
+ };
2013
+ saveState(statePath, reservedConversation);
2014
+ lockedConversation = reservedConversation;
2015
+ }
2016
+ : undefined
2017
+ });
2018
+ const actualFingerprint = approval.fingerprint;
2019
+ const effectivePolicyRuleId = executorPolicyDecision?.ruleId ?? policyRuleId;
2020
+ const effectivePolicyFingerprint = executorPolicyDecision?.policyFingerprint ?? policyFingerprint;
2021
+ if (!approval.approved) {
2022
+ releaseApprovalStateLock();
2023
+ if (autoApproved) {
2024
+ appendEvent(logPath, {
2025
+ ts: cliNow().toISOString(),
2026
+ conversation_id: conversation.conversation_id,
2027
+ event: "terminal_auto_approval_decision",
2028
+ action: "rejected",
2029
+ reason: approval.reason,
2030
+ terminal_control: terminalControl,
2031
+ expected_fingerprint: expectedFingerprint,
2032
+ actual_fingerprint: actualFingerprint,
2033
+ policy_rule_id: effectivePolicyRuleId,
2034
+ policy_fingerprint: effectivePolicyFingerprint
2035
+ });
2036
+ }
2037
+ printJson({
2038
+ conversation,
2039
+ approved: false,
2040
+ blocked: approval.blocked,
2041
+ reason: approval.reason,
2042
+ terminal_control: terminalControl,
2043
+ expected_approval_fingerprint: expectedFingerprint,
2044
+ actual_approval_fingerprint: actualFingerprint,
2045
+ screen_excerpt: approval.screenExcerpt
2046
+ });
2047
+ return;
2048
+ }
2049
+ appendEvent(logPath, {
2050
+ ts: cliNow().toISOString(),
2051
+ conversation_id: conversation.conversation_id,
2052
+ event: "terminal_approval_send",
2053
+ terminal_control: terminalControl,
2054
+ key: approval.key,
2055
+ keys: approval.keys,
2056
+ label: approval.label,
2057
+ decision_mode: approval.decisionMode,
2058
+ request_id: approval.requestId,
2059
+ approval_fingerprint: actualFingerprint,
2060
+ auto_approved: autoApproved,
2061
+ policy_rule_id: effectivePolicyRuleId,
2062
+ policy_fingerprint: effectivePolicyFingerprint
2063
+ });
2064
+ if (autoApproved) {
2065
+ appendEvent(logPath, {
2066
+ ts: cliNow().toISOString(),
2067
+ conversation_id: conversation.conversation_id,
2068
+ event: "terminal_auto_approval_decision",
2069
+ action: "approved",
2070
+ terminal_control: terminalControl,
2071
+ approval_fingerprint: actualFingerprint,
2072
+ policy_rule_id: effectivePolicyRuleId,
2073
+ policy_fingerprint: effectivePolicyFingerprint
2074
+ });
2075
+ }
2076
+ runtimeLog("info", "terminal_approval_send", {
2077
+ conversation_id: conversation.conversation_id,
2078
+ terminal_target: terminalControl.target,
2079
+ key: approval.key,
2080
+ keys: approval.keys,
2081
+ label: approval.label,
2082
+ decision_mode: approval.decisionMode,
2083
+ request_id: approval.requestId,
2084
+ approval_fingerprint: actualFingerprint,
2085
+ auto_approved: autoApproved,
2086
+ policy_rule_id: effectivePolicyRuleId,
2087
+ policy_fingerprint: effectivePolicyFingerprint
2088
+ });
2089
+ const nativeTakeoverForUpdate = isRecord(lockedConversation.native_session_takeover)
2090
+ ? { ...lockedConversation.native_session_takeover }
2091
+ : {};
2092
+ const resolvedApproval = isRecord(nativeTakeoverForUpdate.terminal_bridge_approval)
2093
+ ? nativeTakeoverForUpdate.terminal_bridge_approval
2094
+ : undefined;
2095
+ const resolvedApprovalScreenDigest = stringValue(resolvedApproval?.screen_digest);
2096
+ const resolvedApprovalState = isRecord(resolvedApproval?.approval_state)
2097
+ ? resolvedApproval.approval_state
2098
+ : undefined;
2099
+ const resolvedTranscriptIdentity = claudeTranscriptApprovalIdentity(resolvedApprovalState);
2100
+ const approvalResolvedAt = cliNow().toISOString();
2101
+ const agentTimeoutMinutes = Number(options.agentTimeoutMinutes ??
2102
+ nativeTakeoverForUpdate.terminal_bridge_inactivity_timeout_minutes ??
2103
+ DEFAULT_AGENT_TIMEOUT_MINUTES);
2104
+ const agentHardTimeoutMinutes = positiveMinutes(options.agentHardTimeoutMinutes ??
2105
+ nativeTakeoverForUpdate.terminal_bridge_hard_timeout_minutes ??
2106
+ DEFAULT_AGENT_HARD_TIMEOUT_MINUTES, "--agent-hard-timeout-minutes");
2107
+ const nextNativeTakeover = {
2108
+ ...nativeTakeoverForUpdate,
2109
+ terminal_bridge_approval: undefined,
2110
+ terminal_bridge_approval_dispatch: undefined,
2111
+ terminal_bridge_approval_resolved_at: approvalResolvedAt,
2112
+ terminal_bridge_last_approval_fingerprint: actualFingerprint,
2113
+ terminal_bridge_last_approval_screen_digest: resolvedApprovalScreenDigest,
2114
+ terminal_bridge_last_approval_request_id: resolvedTranscriptIdentity?.requestId,
2115
+ terminal_bridge_last_approval_evidence_fingerprint: resolvedTranscriptIdentity?.evidenceFingerprint,
2116
+ terminal_bridge_last_approval_prompt_cleared_at: undefined,
2117
+ terminal_bridge_last_approval_at: approvalResolvedAt,
2118
+ terminal_bridge_last_approval_message_id: nativeTakeoverForUpdate.terminal_bridge_message_id,
2119
+ terminal_bridge_monitor_lock_version: monitorOwner.LOCK_VERSION,
2120
+ terminal_bridge_monitor_started_at: approvalResolvedAt,
2121
+ terminal_bridge_last_activity_at: approvalResolvedAt,
2122
+ terminal_bridge_last_activity_reason: "approval resolved",
2123
+ terminal_bridge_inactivity_timeout_minutes: agentTimeoutMinutes,
2124
+ terminal_bridge_hard_timeout_minutes: agentHardTimeoutMinutes,
2125
+ terminal_bridge_inactivity_deadline_at: deadlineAt(approvalResolvedAt, agentTimeoutMinutes),
2126
+ terminal_bridge_hard_deadline_at: deadlineAt(stringValue(nativeTakeoverForUpdate.terminal_bridge_started_at) ?? approvalResolvedAt, agentHardTimeoutMinutes)
2127
+ };
2128
+ delete nextNativeTakeover.terminal_bridge_approval;
2129
+ delete nextNativeTakeover.terminal_bridge_approval_dispatch;
2130
+ delete nextNativeTakeover.terminal_bridge_last_approval_prompt_cleared_at;
2131
+ const nextConversation = {
2132
+ ...lockedConversation,
2133
+ status: terminalBridgeEnabled(lockedConversation)
2134
+ ? "waiting_for_agent"
2135
+ : lockedConversation.status,
2136
+ native_session_takeover: nextNativeTakeover,
2137
+ updated_at: approvalResolvedAt
2138
+ };
2139
+ saveState(statePath, nextConversation);
2140
+ releaseApprovalStateLock();
2141
+ const bridgeMonitor = ensureTerminalBridgeMonitorAfterApproval({
2142
+ conversation: nextConversation,
2143
+ statePath,
2144
+ logPath,
2145
+ terminalControl,
2146
+ options
2147
+ });
2148
+ printJson({
2149
+ conversation: nextConversation,
2150
+ approved: true,
2151
+ terminal_control: terminalControl,
2152
+ key: approval.key,
2153
+ keys: approval.keys,
2154
+ label: approval.label,
2155
+ decision_mode: approval.decisionMode,
2156
+ request_id: approval.requestId,
2157
+ approval_fingerprint: actualFingerprint,
2158
+ auto_approved: autoApproved,
2159
+ policy_rule_id: effectivePolicyRuleId,
2160
+ policy_fingerprint: effectivePolicyFingerprint,
2161
+ monitor_pid: bridgeMonitor.monitorPid ?? null,
2162
+ monitor_handoff_pid: bridgeMonitor.handoffWatchdog?.pid ?? null
2163
+ });
2164
+ };
2165
+ try {
2166
+ return await withStoreWriterLeaseAsync(writerStoreDir, async () => {
2167
+ releaseStateLock = acquireFileLock(`${statePath}.lock`);
2168
+ try {
2169
+ return await runApprovalWithStateLock();
2170
+ }
2171
+ finally {
2172
+ releaseApprovalStateLock();
2173
+ }
2174
+ });
2175
+ }
2176
+ finally {
2177
+ try {
2178
+ releaseApprovalStateLock();
2179
+ }
2180
+ finally {
2181
+ releaseApprovalTerminalLock();
2182
+ }
2183
+ }
2184
+ }
2185
+ function assertAutoApprovalCallbackAuthority(input) {
2186
+ if (input.options.autoApproved !== true) {
2187
+ return "not_callback";
2188
+ }
2189
+ const expected = autoApprovalCallbackAuthorityFromOptions(input.options);
2190
+ if (!expected) {
2191
+ return "not_callback";
2192
+ }
2193
+ assertAutoApprovalCallbackRoute({
2194
+ options: input.options,
2195
+ conversation: input.conversation,
2196
+ statePath: input.statePath
2197
+ });
2198
+ const delivery = isRecord(input.conversation.callback_delivery)
2199
+ ? input.conversation.callback_delivery
2200
+ : undefined;
2201
+ const callbackMessage = isRecord(delivery?.message)
2202
+ ? delivery.message
2203
+ : undefined;
2204
+ const callbackMetadata = isRecord(callbackMessage?.metadata)
2205
+ ? callbackMessage.metadata
2206
+ : undefined;
2207
+ const callbackCandidate = isRecord(callbackMetadata?.approval_candidate)
2208
+ ? callbackMetadata.approval_candidate
2209
+ : undefined;
2210
+ const callbackTerminalStatus = isRecord(callbackMetadata?.terminal_status)
2211
+ ? callbackMetadata.terminal_status
2212
+ : undefined;
2213
+ const callbackApprovalState = isRecord(callbackTerminalStatus?.approval_state)
2214
+ ? callbackTerminalStatus.approval_state
2215
+ : undefined;
2216
+ const persistedApprovalState = isRecord(input.approval?.approval_state)
2217
+ ? input.approval.approval_state
2218
+ : undefined;
2219
+ const callbackFingerprints = [
2220
+ stringValue(callbackMetadata?.approval_fingerprint),
2221
+ stringValue(callbackCandidate?.fingerprint),
2222
+ stringValue(callbackApprovalState?.fingerprint)
2223
+ ];
2224
+ const commonAuthorityMismatch = delivery?.kind !== "approval_notification" ||
2225
+ stringValue(callbackMessage?.id) !== expected.messageId ||
2226
+ stringValue(callbackMessage?.conversation_id) !== expected.conversationId ||
2227
+ stringValue(callbackMessage?.session_id) !== expected.sessionId ||
2228
+ stringValue(callbackMessage?.turn_id) !== expected.turnId ||
2229
+ !input.expectedFingerprint ||
2230
+ !/^[a-f0-9]{64}$/u.test(input.expectedFingerprint) ||
2231
+ callbackFingerprints.some((value) => value !== input.expectedFingerprint) ||
2232
+ input.takeover?.terminal_bridge !== true;
2233
+ if (commonAuthorityMismatch) {
2234
+ throw new Error("automatic approval callback no longer matches the locked Turn state; refresh status and retry");
2235
+ }
2236
+ if (input.approval) {
2237
+ const currentFingerprints = [
2238
+ stringValue(input.approval.fingerprint),
2239
+ stringValue(persistedApprovalState?.fingerprint)
2240
+ ];
2241
+ if (input.conversation.status !== "waiting_for_openclaw" ||
2242
+ currentFingerprints.some((value) => value !== input.expectedFingerprint) ||
2243
+ stringValue(input.approval.callback_message_id) !== expected.messageId) {
2244
+ throw new Error("automatic approval callback no longer matches the locked Turn state; refresh status and retry");
2245
+ }
2246
+ return "current";
2247
+ }
2248
+ if (input.takeover?.terminal_bridge_approval !== undefined ||
2249
+ input.takeover?.terminal_bridge_approval_dispatch !== undefined ||
2250
+ !isPostApprovalCallbackReplayStatus(input.conversation.status) ||
2251
+ stringValue(input.takeover?.terminal_bridge_message_id) !==
2252
+ expected.messageId ||
2253
+ stringValue(input.takeover?.terminal_bridge_last_approval_message_id) !==
2254
+ expected.messageId ||
2255
+ stringValue(input.takeover?.terminal_bridge_last_approval_fingerprint) !==
2256
+ input.expectedFingerprint ||
2257
+ validTimestampMs(input.takeover?.terminal_bridge_approval_resolved_at) === undefined) {
2258
+ throw new Error("automatic approval callback no longer matches the locked Turn receipt; refresh status and retry");
2259
+ }
2260
+ return "already_approved";
2261
+ }
2262
+ function isPostApprovalCallbackReplayStatus(status) {
2263
+ return [
2264
+ "waiting_for_agent",
2265
+ "running",
2266
+ "idle",
2267
+ "stalled",
2268
+ "callback_pending",
2269
+ "callback_failed",
2270
+ "failed",
2271
+ "closed",
2272
+ "cancelled",
2273
+ "cancelling"
2274
+ ].includes(status);
2275
+ }
2276
+ function autoApprovalCallbackAuthorityFromOptions(options) {
2277
+ const candidate = {
2278
+ conversationId: stringValue(options.expectedCallbackConversationId),
2279
+ sessionId: stringValue(options.expectedCallbackSessionId),
2280
+ turnId: stringValue(options.expectedCallbackTurnId),
2281
+ messageId: stringValue(options.expectedCallbackMessageId),
2282
+ openclawSession: stringValue(options.expectedCallbackOpenclawSession)
2283
+ };
2284
+ const values = Object.values(candidate);
2285
+ if (values.every((value) => value === undefined)) {
2286
+ return undefined;
2287
+ }
2288
+ if (values.some((value) => value === undefined)) {
2289
+ throw new Error("automatic approval callback identity is incomplete; no approval key was sent");
2290
+ }
2291
+ return candidate;
2292
+ }
2293
+ function assertAutoApprovalCallbackRoute(input) {
2294
+ if (input.options.autoApproved !== true) {
2295
+ return;
2296
+ }
2297
+ const expected = autoApprovalCallbackAuthorityFromOptions(input.options);
2298
+ if (!expected) {
2299
+ return;
2300
+ }
2301
+ const storedOpenClawSession = stringValue(input.conversation.gateway_session ?? input.conversation.openclaw_session);
2302
+ if (input.conversation.conversation_id !== expected.conversationId ||
2303
+ sessionIdForConversation(input.conversation) !== expected.sessionId ||
2304
+ turnIdForConversation(input.conversation) !== expected.turnId ||
2305
+ !sameCanonicalStatePath(input.conversation.state_path, input.statePath) ||
2306
+ storedOpenClawSession !== expected.openclawSession) {
2307
+ throw new Error("automatic approval callback does not match the selected Turn state; no state was changed");
2308
+ }
2309
+ }
2310
+ async function runTerminalConversationApprove({ options, terminal }) {
2311
+ const { conversationId, agent, terminalControl, pid } = terminal;
2312
+ const storeDir = storeDirFromOptions(options);
2313
+ const releaseTerminalLock = acquireTerminalBridgeSendLock(storeDir, terminalControl, { timeoutMs: 30000 });
2314
+ try {
2315
+ if (agent === "claude") {
2316
+ printJson({
2317
+ conversation_id: conversationId,
2318
+ source: "terminal_control",
2319
+ approved: false,
2320
+ blocked: true,
2321
+ reason: "Claude screen approval requires `send --background` so AKK can bind it to an active managed turn",
2322
+ terminal_control: terminalControl
2323
+ });
2324
+ return;
2325
+ }
2326
+ const suppliedTerminalToken = stringValue(options.expectedTerminalToken);
2327
+ const initialResolution = await terminalListCliFacade.resolveTerminalScopedCodexApproval({
2328
+ options,
2329
+ terminal
2330
+ });
2331
+ if (initialResolution.state === "blocked") {
2332
+ throw new Error(initialResolution.reason);
2333
+ }
2334
+ if (initialResolution.state === "unmanaged" && suppliedTerminalToken) {
2335
+ throw new Error("--expected-terminal-token does not match an advertised terminal-scoped Codex approval");
2336
+ }
2337
+ if (initialResolution.state === "eligible" &&
2338
+ suppliedTerminalToken !== initialResolution.boundary.token) {
2339
+ throw new Error("terminal-scoped Codex approval token is missing or stale; refresh AKK list");
2340
+ }
2341
+ const runtime = {
2342
+ pid,
2343
+ cwd: terminalControl.currentPath,
2344
+ conversationId,
2345
+ terminalTarget: terminalControl.target
2346
+ };
2347
+ const approveCurrentPrompt = async (terminalScoped) => createTerminalAgentBridge(options).approve(agent, terminalControl, {
2348
+ expectedFingerprint: stringValue(options.expectedApprovalFingerprint),
2349
+ scrollbackLines: Number(options.scrollbackLines ?? 120),
2350
+ runtime,
2351
+ beforeKeyDispatch: terminalScoped
2352
+ ? async (context) => {
2353
+ const approvalSnapshot = terminalScopedCodexApprovalPromptSnapshot({
2354
+ approvable: true,
2355
+ fingerprint: context.fingerprint,
2356
+ keys: context.keys,
2357
+ decision_mode: context.inspection.approval.approvable
2358
+ ? context.inspection.approval.action.mode ?? "keys"
2359
+ : undefined,
2360
+ request_id: context.inspection.approval.approvable
2361
+ ? context.inspection.approval.action.requestId
2362
+ : undefined
2363
+ });
2364
+ const current = await terminalListCliFacade.resolveTerminalScopedCodexApproval({
2365
+ options,
2366
+ terminal,
2367
+ approvalSnapshot
2368
+ });
2369
+ if (current.state !== "eligible" ||
2370
+ current.boundary.token !== suppliedTerminalToken) {
2371
+ throw new Error(current.state === "blocked"
2372
+ ? current.reason
2373
+ : "terminal-scoped Codex approval authority changed before key dispatch");
2374
+ }
2375
+ }
2376
+ : undefined
2377
+ });
2378
+ const terminalScoped = initialResolution.state === "eligible";
2379
+ const approval = terminalScoped
2380
+ ? await withStoreWriterLeaseAsync(storeDir, async () => {
2381
+ const current = await terminalListCliFacade.resolveTerminalScopedCodexApproval({
2382
+ options,
2383
+ terminal
2384
+ });
2385
+ if (current.state !== "eligible" ||
2386
+ current.boundary.token !== suppliedTerminalToken) {
2387
+ throw new Error(current.state === "blocked"
2388
+ ? current.reason
2389
+ : "terminal-scoped Codex approval authority changed while waiting for Store control");
2390
+ }
2391
+ return approveCurrentPrompt(true);
2392
+ })
2393
+ : await approveCurrentPrompt(false);
2394
+ if (!approval.approved) {
2395
+ printJson({
2396
+ conversation_id: conversationId,
2397
+ source: "terminal_control",
2398
+ approved: false,
2399
+ blocked: approval.blocked,
2400
+ reason: approval.reason,
2401
+ terminal_control: terminalControl,
2402
+ screen_excerpt: approval.screenExcerpt
2403
+ });
2404
+ return;
2405
+ }
2406
+ runtimeLog("info", "terminal_approval_send", {
2407
+ conversation_id: conversationId,
2408
+ agent,
2409
+ terminal_target: terminalControl.target,
2410
+ key: approval.key,
2411
+ keys: approval.keys,
2412
+ label: approval.label,
2413
+ decision_mode: approval.decisionMode,
2414
+ request_id: approval.requestId
2415
+ });
2416
+ printJson({
2417
+ conversation_id: conversationId,
2418
+ source: "terminal_control",
2419
+ approved: true,
2420
+ terminal_control: terminalControl,
2421
+ key: approval.key,
2422
+ keys: approval.keys,
2423
+ label: approval.label,
2424
+ approval_fingerprint: approval.fingerprint,
2425
+ decision_mode: approval.decisionMode,
2426
+ request_id: approval.requestId,
2427
+ terminal_scoped: terminalScoped,
2428
+ ...(terminalScoped
2429
+ ? {
2430
+ durable_dispatch_receipt: false,
2431
+ uncertain_outcome_recovery: "refresh status and inspect the live prompt; do not retry blindly"
2432
+ }
2433
+ : {})
2434
+ });
2435
+ }
2436
+ finally {
2437
+ releaseTerminalLock();
2438
+ }
2439
+ }
2440
+ async function prepareTerminalControlSend(request) {
2441
+ const { transaction, options, conversation, nextConversation, executor, message, recordRawAttachmentAfterSend = false, allowedPreMaterializationIdentity, allowedAdditionalIdentities = [], verifiedEmptyCodexHandoff, deferredCodexForegroundBinding, continuingTurnResponse = false } = request;
2442
+ const bridge = terminalBridgeEnabled(conversation);
2443
+ const route = bindTerminalDispatchRoute(transaction.scopes, transaction.resources);
2444
+ const { terminalControl, storeDir: lockedStoreDir, statePath, logPath } = route;
2445
+ const terminalBridge = createTerminalAgentBridge(options);
2446
+ const execution = terminalDispatchExecution(options, terminalBridge);
2447
+ const bridgeStartedAt = cliNow().toISOString();
2448
+ const agentTimeoutMinutes = Number(options.agentTimeoutMinutes ?? DEFAULT_AGENT_TIMEOUT_MINUTES);
2449
+ const agentHardTimeoutMinutes = positiveMinutes(options.agentHardTimeoutMinutes ?? DEFAULT_AGENT_HARD_TIMEOUT_MINUTES, "--agent-hard-timeout-minutes");
2450
+ const terminalPayload = terminalSubmissionPayload(String(message.body ?? ""));
2451
+ const terminalRequestHash = required(terminalBridgeRequestFingerprint(terminalPayload), "terminal request hash is unavailable");
2452
+ const presentationContext = { message, executor, terminalControl };
2453
+ const presentationPorts = {
2454
+ write: printJson,
2455
+ budget: budgetAction,
2456
+ nextAction: openClawYieldNextAction,
2457
+ summarize: textSummary
2458
+ };
2459
+ let previousDispatchLedger = resolveTerminalDispatchLedgerPaneIncarnation(terminalControl, loadTerminalBridgeDispatchLedger(terminalControl));
2460
+ previousDispatchLedger = reconcilePreparedTerminalDispatchLedger(terminalControl, previousDispatchLedger);
2461
+ const previousDispatchLifecycle = terminalDispatchLedgerLooksLifecycle(previousDispatchLedger);
2462
+ const previousDispatchOwner = execution.preflightRequiresOwner(previousDispatchLedger, previousDispatchLifecycle)
2463
+ ? loadTerminalDispatchLedgerOwner(previousDispatchLedger)
2464
+ : undefined;
2465
+ const dispatchPreflight = execution.evaluatePreflight({
2466
+ ledger: previousDispatchLedger,
2467
+ owner: previousDispatchOwner,
2468
+ conversation,
2469
+ requestHash: terminalRequestHash,
2470
+ requestText: terminalPayload,
2471
+ messageId: message.id,
2472
+ terminalTarget: terminalControl.target,
2473
+ ledgerLifecycle: previousDispatchLifecycle,
2474
+ statePathMatches: Boolean(previousDispatchOwner &&
2475
+ sameCanonicalStatePath(previousDispatchLedger.state_path, statePath)),
2476
+ continuingTurnResponse
2477
+ });
2478
+ if (dispatchPreflight.action === "replay") {
2479
+ presentTerminalDispatchReplay(dispatchPreflight, presentationContext, presentationPorts);
2480
+ return;
2481
+ }
2482
+ if (bridge) {
2483
+ assertNoUnresolvedTerminalBridgeSubmission(lockedStoreDir, terminalControl, conversation.conversation_id, terminalPayload);
2484
+ }
2485
+ const sendTakeover = isRecord(conversation.native_session_takeover)
2486
+ ? conversation.native_session_takeover
2487
+ : undefined;
2488
+ const terminalAgentPid = Number(sendTakeover?.terminal_agent_pid);
2489
+ const expectedManagedNativeThreadId = stringValue(sendTakeover?.terminal_agent_expected_session_id) ?? stringValue(sendTakeover?.terminal_agent_session_id);
2490
+ const currentNativeIdentity = deferredCodexForegroundBinding
2491
+ ?.candidateAcceptanceAnchor
2492
+ ? undefined
2493
+ : await execution.resolveCurrentNativeIdentity({
2494
+ agent: executor.kind,
2495
+ pid: terminalAgentPid,
2496
+ cwd: terminalControl.currentPath,
2497
+ preferredSessionId: allowedPreMaterializationIdentity
2498
+ ? expectedManagedNativeThreadId
2499
+ : undefined,
2500
+ allowedCompanionIdentity: allowedPreMaterializationIdentity,
2501
+ allowedAdditionalIdentities
2502
+ });
2503
+ const virginRawAttach = Boolean(recordRawAttachmentAfterSend &&
2504
+ !stringValue(sendTakeover?.terminal_agent_session_id));
2505
+ const pendingManagedNativeBinding = Boolean(!stringValue(sendTakeover?.terminal_agent_session_id) &&
2506
+ stringValue(sendTakeover?.terminal_agent_expected_session_id));
2507
+ const needsPostSendNativeBinding = virginRawAttach || pendingManagedNativeBinding;
2508
+ if (needsPostSendNativeBinding) {
2509
+ if (currentNativeIdentity &&
2510
+ !nativeIdentityMatchesCodexPreMaterialization(currentNativeIdentity, allowedPreMaterializationIdentity)) {
2511
+ throw new Error("native agent session appeared while preparing an unmaterialized terminal binding; refresh list and retry");
2512
+ }
2513
+ }
2514
+ else {
2515
+ execution.assertTurnIdentity({
2516
+ conversation,
2517
+ currentIdentity: currentNativeIdentity,
2518
+ operation: "send to"
2519
+ });
2520
+ }
2521
+ const preSendRuntime = {
2522
+ ...(deferredCodexForegroundBinding?.candidateAcceptanceAnchor
2523
+ ? terminalRuntimeForLiveIdentity({
2524
+ terminal: deferredCodexForegroundBinding.terminal,
2525
+ physicalOnly: true
2526
+ })
2527
+ : terminalRuntimeIdentityForConversation(nextConversation, terminalControl)),
2528
+ allowedPreMaterializationNativeIdentity: allowedPreMaterializationIdentity,
2529
+ allowedAdditionalNativeIdentities: allowedAdditionalIdentities,
2530
+ messageId: message.id
2531
+ };
2532
+ let preSendScreenFingerprint;
2533
+ let codexRolloutAcceptanceAnchor;
2534
+ let claudeTranscriptAnchor;
2535
+ const claudeHome = executor.kind === "claude"
2536
+ ? path.resolve(expandHome(options.claudeHome) ?? defaultClaudeHome())
2537
+ : undefined;
2538
+ try {
2539
+ const status = await terminalBridge.status(executor.kind, terminalControl, {
2540
+ scrollbackLines: Number(options.scrollbackLines ?? 120),
2541
+ runtime: preSendRuntime
2542
+ });
2543
+ if (verifiedEmptyCodexHandoff || deferredCodexForegroundBinding) {
2544
+ if (executor.kind !== "codex" ||
2545
+ status.reachable !== true ||
2546
+ status.approval_state.blocked === true ||
2547
+ !["idle", "unknown"].includes(status.activity_state)) {
2548
+ throw new Error(`Codex deferred foreground send is not at a safe prompt ` +
2549
+ `(${status.activity_state}: ${status.activity_reason})`);
2550
+ }
2551
+ }
2552
+ else {
2553
+ assertSafeTerminalSend(executor.kind, status);
2554
+ }
2555
+ if (executor.kind === "codex" && needsPostSendNativeBinding) {
2556
+ if (pendingManagedNativeBinding || allowedPreMaterializationIdentity) {
2557
+ const expectedForegroundId = stringValue(sendTakeover?.terminal_agent_expected_session_id);
2558
+ const foreground = createRuntimeTerminalAgentRegistry(options)
2559
+ .require("codex")
2560
+ .observeThreadLifecycle?.({
2561
+ operation: { kind: "new_thread" },
2562
+ phase: "before",
2563
+ screen: status.screen.excerpt ?? ""
2564
+ });
2565
+ if (!expectedForegroundId ||
2566
+ foreground?.status !== "observed" ||
2567
+ foreground.nativeThreadId !== expectedForegroundId) {
2568
+ throw new Error("Codex foreground thread changed after the managed /status proof; " +
2569
+ "refresh list before sending");
2570
+ }
2571
+ }
2572
+ await assertCodexComposerReadyForAutomatedInput({
2573
+ options,
2574
+ terminalControl
2575
+ });
2576
+ }
2577
+ if (bridge) {
2578
+ preSendScreenFingerprint = stringValue(status.screen.digest) ??
2579
+ terminalBridgeScreenFingerprint(status.screen.excerpt);
2580
+ if (executor.kind === "codex") {
2581
+ codexRolloutAcceptanceAnchor = execution.captureCodexAcceptanceAnchor({
2582
+ currentIdentity: currentNativeIdentity,
2583
+ expectedNativeThreadId: expectedManagedNativeThreadId,
2584
+ boundProcessUuid: stringValue(sendTakeover?.terminal_agent_process_uuid),
2585
+ boundProcessBirth: stringValue(sendTakeover?.terminal_agent_process_birth),
2586
+ allowedPreMaterializationIdentity,
2587
+ needsPostSendNativeBinding,
2588
+ candidateSetAnchor: deferredCodexForegroundBinding?.candidateAcceptanceAnchor
2589
+ });
2590
+ }
2591
+ else {
2592
+ claudeTranscriptAnchor = captureClaudeTranscriptAnchor({
2593
+ sessionId: preSendRuntime.sessionId,
2594
+ cwd: preSendRuntime.cwd,
2595
+ pid: preSendRuntime.pid,
2596
+ claudeHome,
2597
+ agentRows: loadClaudeAgentRows(options)
2598
+ });
2599
+ if (!claudeTranscriptAnchor) {
2600
+ throw new Error("the completion monitor could not bind an owner-private Claude transcript boundary");
2601
+ }
2602
+ }
2603
+ }
2604
+ }
2605
+ catch (error) {
2606
+ throw new Error(`refusing to send to ${executor.display_name} without a verified idle terminal: ${error instanceof Error ? error.message : String(error)}`);
2607
+ }
2608
+ return {
2609
+ route, bridge, terminalControl, lockedStoreDir, statePath, logPath,
2610
+ terminalBridge, execution, bridgeStartedAt,
2611
+ agentTimeoutMinutes, agentHardTimeoutMinutes,
2612
+ terminalPayload, terminalRequestHash,
2613
+ presentationContext, presentationPorts, previousDispatchLedger,
2614
+ sendTakeover, terminalAgentPid, needsPostSendNativeBinding, preSendRuntime,
2615
+ preSendScreenFingerprint, codexRolloutAcceptanceAnchor,
2616
+ claudeTranscriptAnchor, claudeHome
2617
+ };
2618
+ }
2619
+ async function resolveTerminalDispatchSubmissionOwner(prepared, request, application, stagedConversation) {
2620
+ const { transaction, options, executor, allowedPreMaterializationIdentity, allowedAdditionalIdentities = [], deferredCodexForegroundBinding: deferredBinding } = request;
2621
+ const { execution, needsPostSendNativeBinding, codexRolloutAcceptanceAnchor: acceptanceAnchor, terminalControl, terminalAgentPid, sendTakeover, terminalRequestHash, route } = prepared;
2622
+ const deferredForegroundScope = deferredBinding
2623
+ ? bindDeferredForegroundApplicationScope(transaction.scopes, transaction.resources)
2624
+ : undefined;
2625
+ const ready = (conversation, pending = false) => ({
2626
+ conversation, deferredCandidateBindingPending: pending
2627
+ });
2628
+ if (!needsPostSendNativeBinding) {
2629
+ return ready(stagedConversation);
2630
+ }
2631
+ let boundIdentity;
2632
+ let boundConversation;
2633
+ let bindingError;
2634
+ try {
2635
+ if (deferredBinding &&
2636
+ cliEnv().AKK_TEST_ALLOW_SYNTHETIC_TERMINAL_ACCEPTANCE !== "1" &&
2637
+ ![2, 3].includes(Number(acceptanceAnchor?.version))) {
2638
+ throw new Error("deferred Codex foreground binding requires a virgin rollout acceptance anchor");
2639
+ }
2640
+ boundIdentity = await execution.pollNativeIdentity({
2641
+ executor: executor.kind,
2642
+ terminalControl,
2643
+ pid: terminalAgentPid,
2644
+ expectedSessionId: stringValue(sendTakeover?.terminal_agent_expected_session_id),
2645
+ allowedPreMaterializationIdentity,
2646
+ allowedAdditionalIdentities,
2647
+ ...(deferredBinding &&
2648
+ acceptanceAnchor &&
2649
+ [2, 3].includes(acceptanceAnchor.version)
2650
+ ? {
2651
+ requiredCodexAcceptance: {
2652
+ anchor: acceptanceAnchor,
2653
+ requestHash: terminalRequestHash
2654
+ },
2655
+ attempts: Math.max(1, Math.ceil(DEFAULT_TERMINAL_ACCEPTANCE_TIMEOUT_MS /
2656
+ DEFAULT_TERMINAL_ACCEPTANCE_POLL_INTERVAL_MS)),
2657
+ delayMs: DEFAULT_TERMINAL_ACCEPTANCE_POLL_INTERVAL_MS
2658
+ }
2659
+ : {})
2660
+ });
2661
+ }
2662
+ catch (error) {
2663
+ bindingError = error instanceof Error ? error.message : String(error);
2664
+ }
2665
+ if (boundIdentity) {
2666
+ try {
2667
+ if (acceptanceAnchor &&
2668
+ (boundIdentity.processUuid !==
2669
+ acceptanceAnchor.process_uuid ||
2670
+ boundIdentity.processBirth !==
2671
+ acceptanceAnchor.process_birth)) {
2672
+ throw new Error("Codex process incarnation changed while its native thread materialized");
2673
+ }
2674
+ const expectedNativeThreadId = stringValue(sendTakeover?.terminal_agent_expected_session_id);
2675
+ if (expectedNativeThreadId &&
2676
+ boundIdentity.sessionId !== expectedNativeThreadId) {
2677
+ throw new Error(`native agent created thread ${boundIdentity.sessionId}, expected ` +
2678
+ `${expectedNativeThreadId}`);
2679
+ }
2680
+ const resolvedBoundConversation = execution.withNativeIdentity(stagedConversation, boundIdentity);
2681
+ boundConversation = resolvedBoundConversation;
2682
+ const bindingStoreDir = managedSessionStoreDirForConversation(resolvedBoundConversation);
2683
+ const bindingStatePath = stringValue(resolvedBoundConversation.state_path);
2684
+ if (!bindingStoreDir || !bindingStatePath) {
2685
+ throw new Error("managed Session Store is unavailable before native identity commit");
2686
+ }
2687
+ const identityRoute = withExactTerminalDispatchRoute(route, {
2688
+ terminalControl,
2689
+ terminalKey: terminalBridgeRuntimeKey(terminalControl),
2690
+ storeDir: bindingStoreDir,
2691
+ statePath: bindingStatePath,
2692
+ logPath: path.join(path.dirname(bindingStatePath), "events.ndjson")
2693
+ }, (exactRoute) => exactRoute);
2694
+ if (deferredBinding) {
2695
+ await deferredForegroundApplication(options, deferredBinding.terminal).commit({
2696
+ scope: required(deferredForegroundScope, "deferred foreground mutation scope is unavailable"),
2697
+ boundary: deferredForegroundBoundaryProjection(deferredBinding),
2698
+ identity: boundIdentity,
2699
+ acceptedAt: cliNow().toISOString()
2700
+ });
2701
+ }
2702
+ else {
2703
+ await assertNativeThreadHasExclusiveOwnership({
2704
+ options,
2705
+ agent: executor.kind,
2706
+ currentPid: terminalAgentPid,
2707
+ nativeThreadId: boundIdentity.sessionId,
2708
+ storeDir: identityRoute.storeDir,
2709
+ terminalControl,
2710
+ excludedManagedSessionId: sessionIdForConversation(boundConversation)
2711
+ });
2712
+ persistManagedSessionNativeIdentity({
2713
+ conversation: boundConversation,
2714
+ terminalControl,
2715
+ identity: boundIdentity,
2716
+ storeDir: identityRoute.storeDir
2717
+ });
2718
+ }
2719
+ if (acceptanceAnchor?.version === 2 &&
2720
+ cliEnv().AKK_TEST_EXIT_AFTER_VIRGIN_SESSION_BINDING === "1") {
2721
+ cliExit(86);
2722
+ }
2723
+ // Persist the observed identity before the full Turn-vs-Session check:
2724
+ // the provisional Turn and Session initially share only a binding id.
2725
+ execution.assertTurnIdentity({
2726
+ conversation: boundConversation,
2727
+ currentIdentity: boundIdentity,
2728
+ operation: "bind"
2729
+ });
2730
+ }
2731
+ catch (error) {
2732
+ bindingError = error instanceof Error ? error.message : String(error);
2733
+ boundIdentity = undefined;
2734
+ boundConversation = undefined;
2735
+ }
2736
+ }
2737
+ if (!boundIdentity && deferredBinding) {
2738
+ try {
2739
+ const deferredScope = required(deferredForegroundScope, "deferred foreground mutation scope is unavailable");
2740
+ const transfer = deferredScope.loadTransfer(deferredBinding.transferId);
2741
+ if (["committed", "resolved"].includes(transfer.status)) {
2742
+ const recoveredTarget = await deferredForegroundApplication(options, deferredBinding.terminal).resolve({
2743
+ scope: deferredScope,
2744
+ boundary: deferredForegroundBoundaryProjection(deferredBinding)
2745
+ });
2746
+ const binding = recoveredTarget.binding;
2747
+ if (!binding?.native_thread_id ||
2748
+ !binding.native_process.process_uuid ||
2749
+ !binding.native_process.process_birth ||
2750
+ !isCompleteNativeRollout(binding.native_process.rollout)) {
2751
+ throw new Error("resolved deferred target lacks exact native identity");
2752
+ }
2753
+ const recoveredIdentity = {
2754
+ sessionId: binding.native_thread_id,
2755
+ processUuid: binding.native_process.process_uuid,
2756
+ processBirth: binding.native_process.process_birth,
2757
+ rollout: binding.native_process.rollout,
2758
+ evidence: binding.native_process.evidence
2759
+ };
2760
+ const recoveredConversation = execution.withNativeIdentity(stagedConversation, recoveredIdentity);
2761
+ execution.assertTurnIdentity({
2762
+ conversation: recoveredConversation,
2763
+ currentIdentity: recoveredIdentity,
2764
+ operation: "recover deferred foreground binding for"
2765
+ });
2766
+ boundIdentity = recoveredIdentity;
2767
+ boundConversation = recoveredConversation;
2768
+ }
2769
+ }
2770
+ catch (error) {
2771
+ bindingError = error instanceof Error ? error.message : String(error);
2772
+ boundIdentity = undefined;
2773
+ boundConversation = undefined;
2774
+ }
2775
+ }
2776
+ if (boundIdentity && boundConversation) {
2777
+ return ready(boundConversation);
2778
+ }
2779
+ if (deferredBinding &&
2780
+ acceptanceAnchor?.version === 3 &&
2781
+ bindingError === undefined) {
2782
+ return ready(stagedConversation, true);
2783
+ }
2784
+ const bindingReason = bindingError
2785
+ ? `AKK dispatched terminal input but could not verify the new native agent session: ${bindingError}`
2786
+ : "AKK dispatched terminal input but no exact native agent session appeared within the binding window";
2787
+ if (deferredBinding) {
2788
+ try {
2789
+ deferredForegroundApplication(options, deferredBinding.terminal).markUncertain({
2790
+ scope: required(deferredForegroundScope, "deferred foreground mutation scope is unavailable"),
2791
+ boundary: deferredForegroundBoundaryProjection(deferredBinding),
2792
+ reason: bindingReason
2793
+ });
2794
+ }
2795
+ catch (error) {
2796
+ runtimeLog("error", "deferred_codex_foreground_uncertain_persist_failed", {
2797
+ transfer_id: deferredBinding.transferId,
2798
+ error: error instanceof Error ? error.message : String(error)
2799
+ });
2800
+ }
2801
+ }
2802
+ return {
2803
+ conversation: application.applyIdentityFailure(cliNow().toISOString(), bindingReason, deferredBinding
2804
+ ? undefined
2805
+ : (current) => quarantineManagedSessionBinding({
2806
+ conversation: current,
2807
+ reason: bindingReason,
2808
+ storeDir: route.storeDir
2809
+ })),
2810
+ failureReason: bindingReason
2811
+ };
2812
+ }
2813
+ function deferredTerminalInputNotStartedAt(transfer, terminalInputNotStartedAt) {
2814
+ if (terminalInputNotStartedAt === undefined ||
2815
+ transfer.input_stage === "none") {
2816
+ return undefined;
2817
+ }
2818
+ if (transfer.status === "dispatch_started" &&
2819
+ transfer.input_stage === "dispatch_started") {
2820
+ return terminalInputNotStartedAt;
2821
+ }
2822
+ throw new Error(`deferred foreground transfer ${transfer.transfer_id} has ` +
2823
+ `${transfer.input_stage} input evidence and cannot accept a ` +
2824
+ "terminal-input-not-started result");
2825
+ }
2826
+ function terminalDispatchRollbackRepositories({ request, prepared, deferredForegroundScope, preTransportRollback }) {
2827
+ const { options, conversation, deferredCodexForegroundBinding } = request;
2828
+ let rollbackPreTransportAttach = preTransportRollback;
2829
+ const rollbackBeforeInput = (route) => {
2830
+ if (!rollbackPreTransportAttach) {
2831
+ return true;
2832
+ }
2833
+ const rollback = rollbackPreTransportAttach;
2834
+ rollbackPreTransportAttach = undefined;
2835
+ try {
2836
+ rollback(route);
2837
+ return true;
2838
+ }
2839
+ catch (error) {
2840
+ runtimeLog("error", "raw_attach_pre_transport_rollback_failed", {
2841
+ conversation_id: conversation.conversation_id,
2842
+ terminal_target: route.terminalControl.target,
2843
+ error: error instanceof Error ? error.message : String(error)
2844
+ });
2845
+ return false;
2846
+ }
2847
+ };
2848
+ const restoreDeferred = (route, terminalInputNotStartedAt) => {
2849
+ if (!deferredCodexForegroundBinding) {
2850
+ return false;
2851
+ }
2852
+ const deferredScope = required(deferredForegroundScope, "deferred foreground mutation scope is unavailable");
2853
+ const transfer = deferredScope.loadTransfer(deferredCodexForegroundBinding.transferId);
2854
+ if (terminalBridgeRuntimeKey(route.terminalControl) !==
2855
+ terminalBridgeRuntimeKey(deferredCodexForegroundBinding.terminal.terminalControl) ||
2856
+ (transfer.state_path !== undefined &&
2857
+ !sameCanonicalStatePath(transfer.state_path, route.statePath))) {
2858
+ throw new Error("deferred Codex pre-input abort escaped its exact mutation resources");
2859
+ }
2860
+ const durableTerminalInputNotStartedAt = deferredTerminalInputNotStartedAt(transfer, terminalInputNotStartedAt);
2861
+ deferredRecoveryAdapter.abortPreparedDeferredForegroundTurn(deferredForegroundRecoveryAdapterPorts(), {
2862
+ options,
2863
+ scope: deferredScope,
2864
+ storeDir: route.storeDir,
2865
+ terminal: {
2866
+ ...deferredCodexForegroundBinding.terminal,
2867
+ terminalControl: route.terminalControl
2868
+ },
2869
+ transfer,
2870
+ boundary: deferredCodexForegroundBinding,
2871
+ terminalInputNotStartedAt: durableTerminalInputNotStartedAt
2872
+ });
2873
+ // The dedicated abort already durably restores the Turn, ledger,
2874
+ // transfer, and Sessions. Never invoke the attach rollback twice.
2875
+ rollbackPreTransportAttach = undefined;
2876
+ return true;
2877
+ };
2878
+ return { rollbackBeforeInput, restoreDeferred };
2879
+ }
2880
+ function createTerminalDispatchRuntime(request, prepared, rollback) {
2881
+ const { transaction, conversation, nextConversation, executor, message, recordRawAttachmentAfterSend = false } = request;
2882
+ const { bridge, terminalControl, lockedStoreDir, statePath, logPath, bridgeStartedAt, agentTimeoutMinutes, agentHardTimeoutMinutes, terminalPayload, terminalRequestHash, previousDispatchLedger, preSendScreenFingerprint, codexRolloutAcceptanceAnchor, claudeTranscriptAnchor, claudeHome } = prepared;
2883
+ const terminalMessage = message;
2884
+ const bridgeConversation = bridge
2885
+ ? dispatchReceipt.withTerminalBridgeState({
2886
+ conversation: nextConversation,
2887
+ message: terminalMessage,
2888
+ requestText: terminalPayload,
2889
+ startedAt: bridgeStartedAt,
2890
+ agentTimeoutMinutes,
2891
+ agentHardTimeoutMinutes,
2892
+ monitorLockVersion: monitorOwner.LOCK_VERSION,
2893
+ preSendScreenFingerprint,
2894
+ codexRolloutAcceptanceAnchor,
2895
+ claudeTranscriptAnchor,
2896
+ claudeHome
2897
+ })
2898
+ : nextConversation;
2899
+ const preparedConversation = withTerminalBridgeSubmission({
2900
+ conversation: bridgeConversation,
2901
+ messageId: message.id,
2902
+ messageType: terminalMessage.type,
2903
+ messageBody: String(message.body),
2904
+ requestText: terminalPayload,
2905
+ status: "prepared",
2906
+ preparedAt: bridgeStartedAt
2907
+ });
2908
+ const previousGenerationId = stringValue(previousDispatchLedger?.generation_id) ??
2909
+ stringValue(previousDispatchLedger?.message_id);
2910
+ const progress = {
2911
+ stagedConversation: preparedConversation
2912
+ };
2913
+ let preparedMessageEvent;
2914
+ const recordPostTransportBookkeepingFailure = (phase, error) => {
2915
+ const warning = error instanceof Error ? error.message : String(error);
2916
+ progress.bookkeepingWarning ??= warning;
2917
+ runtimeLog("warn", "terminal_message_post_transport_bookkeeping_failed", {
2918
+ conversation_id: conversation.conversation_id,
2919
+ terminal_target: terminalControl.target,
2920
+ phase,
2921
+ error: warning
2922
+ });
2923
+ };
2924
+ const { applicationPorts } = bindTerminalDispatchCapabilities({
2925
+ scopes: transaction.scopes,
2926
+ resources: transaction.resources,
2927
+ repositories: terminalDispatchCapabilityRepositories({
2928
+ previousLedger: previousDispatchLedger,
2929
+ preparedMessageEvent: () => {
2930
+ if (!preparedMessageEvent) {
2931
+ throw new Error("prepared message event was not validated");
2932
+ }
2933
+ return preparedMessageEvent;
2934
+ },
2935
+ restoreDeferred: rollback.restoreDeferred,
2936
+ rollbackBeforeInput: rollback.rollbackBeforeInput
2937
+ }),
2938
+ local: {
2939
+ synchronizeStageProgress: (current, stage, at) => {
2940
+ progress.stagedConversation = current;
2941
+ progress.textInjectedAt = stage === "text_injected"
2942
+ ? at
2943
+ : progress.textInjectedAt;
2944
+ progress.enterDispatchedAt = stage === "enter_dispatched"
2945
+ ? at
2946
+ : progress.enterDispatchedAt;
2947
+ },
2948
+ audit: {
2949
+ log: runtimeLog,
2950
+ recordBookkeepingFailure: recordPostTransportBookkeepingFailure,
2951
+ recordPersistenceFailure: (phase, error, current) => {
2952
+ runtimeLog("error", phase, {
2953
+ conversation_id: current.conversation_id,
2954
+ terminal_target: terminalControl.target,
2955
+ error: error instanceof Error ? error.message : String(error)
2956
+ });
2957
+ }
2958
+ }
2959
+ }
2960
+ });
2961
+ const application = new dispatchApplication.TerminalDispatchApplication({
2962
+ originalConversation: conversation,
2963
+ preparedConversation,
2964
+ message: terminalMessage,
2965
+ executor,
2966
+ terminalControl,
2967
+ receiptTerminalControl: terminalControlFromTakeover(isRecord(preparedConversation.native_session_takeover)
2968
+ ? preparedConversation.native_session_takeover
2969
+ : undefined),
2970
+ requestText: terminalPayload,
2971
+ requestHash: terminalRequestHash,
2972
+ preparedAt: bridgeStartedAt,
2973
+ statePath,
2974
+ eventLogPath: logPath,
2975
+ previousGenerationId,
2976
+ dispatcherPid: cliPid(),
2977
+ storeDir: lockedStoreDir,
2978
+ recordRawAttachmentAfterSend,
2979
+ ledgerBindingFields: terminalBindingLedgerFields
2980
+ }, applicationPorts);
2981
+ application.persistPrepared();
2982
+ return {
2983
+ application,
2984
+ terminalMessage,
2985
+ progress,
2986
+ recordPostTransportBookkeepingFailure,
2987
+ validatePreparedMessageEvent: () => {
2988
+ preparedMessageEvent = messageEvent(terminalMessage);
2989
+ }
2990
+ };
2991
+ }
2992
+ function terminalDispatchTransportLifecycle({ request, prepared, application, deferredForegroundScope }) {
2993
+ const { options, observedHandoff, verifiedEmptyCodexHandoff, deferredCodexForegroundBinding } = request;
2994
+ return prepared.execution.transportLifecycle({
2995
+ ...(observedHandoff
2996
+ ? {
2997
+ observedHandoff: {
2998
+ verify: (requireEmptyComposer) => assertObservedHandoffTransportBoundary({
2999
+ options,
3000
+ terminal: observedHandoff.terminal,
3001
+ transition: observedHandoff.transition,
3002
+ requireEmptyComposer
3003
+ })
3004
+ }
3005
+ }
3006
+ : {}),
3007
+ ...(verifiedEmptyCodexHandoff
3008
+ ? {
3009
+ verifiedEmptyHandoff: {
3010
+ verify: (requireEmptyComposer) => assertVerifiedEmptyCodexTransportBoundary({
3011
+ options,
3012
+ boundary: verifiedEmptyCodexHandoff,
3013
+ requireEmptyComposer
3014
+ })
3015
+ }
3016
+ }
3017
+ : {}),
3018
+ ...(deferredCodexForegroundBinding
3019
+ ? {
3020
+ deferredBinding: {
3021
+ verify: (requireEmptyComposer) => assertDeferredCodexForegroundBindingBoundary({
3022
+ options,
3023
+ scope: required(deferredForegroundScope, "deferred foreground mutation scope is unavailable"),
3024
+ boundary: deferredCodexForegroundBinding,
3025
+ expectedSourceStatus: "transitioning",
3026
+ requireNoDispatch: false,
3027
+ requireEmptyComposer
3028
+ }),
3029
+ begin: (at) => deferredForegroundApplication(options, deferredCodexForegroundBinding.terminal).begin({
3030
+ scope: required(deferredForegroundScope, "deferred foreground mutation scope is unavailable"),
3031
+ boundary: deferredForegroundBoundaryProjection(deferredCodexForegroundBinding),
3032
+ at
3033
+ }),
3034
+ advance: (stage, at) => deferredForegroundApplication(options, deferredCodexForegroundBinding.terminal).advance({
3035
+ scope: required(deferredForegroundScope, "deferred foreground mutation scope is unavailable"),
3036
+ boundary: deferredForegroundBoundaryProjection(deferredCodexForegroundBinding),
3037
+ stage,
3038
+ at
3039
+ })
3040
+ }
3041
+ }
3042
+ : {}),
3043
+ recordStage: (stage, at, afterDurable) => application.recordTransportStage(stage, at, afterDurable)
3044
+ });
3045
+ }
3046
+ async function terminalDispatchAcceptance({ request, prepared, conversation }) {
3047
+ const { options, executor } = request;
3048
+ const { execution, terminalControl } = prepared;
3049
+ const timeoutMs = positiveMilliseconds(options.terminalAcceptanceTimeoutMs ??
3050
+ DEFAULT_TERMINAL_ACCEPTANCE_TIMEOUT_MS, "--terminal-acceptance-timeout-ms");
3051
+ return execution.pollAcceptance({
3052
+ executor: executor.kind,
3053
+ conversation,
3054
+ terminalControl,
3055
+ timeoutMs,
3056
+ pollIntervalMs: Math.max(10, Math.min(timeoutMs, Number(options.terminalAcceptancePollIntervalMs ??
3057
+ DEFAULT_TERMINAL_ACCEPTANCE_POLL_INTERVAL_MS))),
3058
+ scrollbackLines: Number(options.scrollbackLines ?? 240)
3059
+ });
3060
+ }
3061
+ function launchAcceptedTerminalMonitor({ request, prepared, conversation, acceptance, recordFailure }) {
3062
+ const { options } = request;
3063
+ const { bridge, statePath, logPath, terminalControl, agentTimeoutMinutes, agentHardTimeoutMinutes } = prepared;
3064
+ if (!bridge ||
3065
+ !(acceptance.outcome === "agent_accepted" ||
3066
+ acceptance.outcome === "pending_acceptance")) {
3067
+ return undefined;
3068
+ }
3069
+ try {
3070
+ const monitor = startTerminalBridgeMonitorForConversation({
3071
+ conversation,
3072
+ statePath,
3073
+ logPath,
3074
+ options
3075
+ });
3076
+ if (monitor) {
3077
+ appendEvent(logPath, {
3078
+ ts: cliNow().toISOString(),
3079
+ conversation_id: conversation.conversation_id,
3080
+ event: "terminal_bridge_monitor_launch",
3081
+ pid: monitor.pid ?? null,
3082
+ terminal_control: terminalControl,
3083
+ phase: acceptance.outcome,
3084
+ agent_timeout_minutes: agentTimeoutMinutes,
3085
+ agent_hard_timeout_minutes: agentHardTimeoutMinutes
3086
+ });
3087
+ }
3088
+ return monitor;
3089
+ }
3090
+ catch (error) {
3091
+ recordFailure("monitor_launch", error);
3092
+ return undefined;
3093
+ }
3094
+ }
3095
+ function presentTerminalDispatchTransportFailure({ error, request, prepared, application, progress, deferredForegroundScope, bridgeMonitor }) {
3096
+ const { options, executor, message, deferredCodexForegroundBinding } = request;
3097
+ const { lockedStoreDir, terminalControl, presentationContext, presentationPorts } = prepared;
3098
+ if (!progress.textInjectedAt &&
3099
+ error instanceof TerminalInputNotStartedError) {
3100
+ renderTerminalZeroInputAbort(application.recordZeroInputAbort({
3101
+ failureKind: "transport",
3102
+ error,
3103
+ abortedAt: cliNow().toISOString()
3104
+ }), presentationContext, presentationPorts, bridgeMonitor?.pid);
3105
+ return;
3106
+ }
3107
+ const uncertainAt = cliNow().toISOString();
3108
+ const errorMessage = error instanceof Error ? error.message : String(error);
3109
+ if (deferredCodexForegroundBinding) {
3110
+ try {
3111
+ deferredForegroundApplication(options, deferredCodexForegroundBinding.terminal).markUncertain({
3112
+ scope: required(deferredForegroundScope, "deferred foreground mutation scope is unavailable"),
3113
+ boundary: deferredForegroundBoundaryProjection(deferredCodexForegroundBinding),
3114
+ reason: errorMessage
3115
+ });
3116
+ }
3117
+ catch (transferError) {
3118
+ runtimeLog("error", "deferred_codex_foreground_uncertain_persist_failed", {
3119
+ transfer_id: deferredCodexForegroundBinding.transferId,
3120
+ error: transferError instanceof Error
3121
+ ? transferError.message
3122
+ : String(transferError)
3123
+ });
3124
+ }
3125
+ }
3126
+ const uncertainConversation = application.applyUncertain(uncertainAt, error);
3127
+ const stalledConversationIds = stallOtherTerminalBridgeConversationsForUncertainDispatch({
3128
+ storeDir: lockedStoreDir,
3129
+ terminalControl,
3130
+ currentConversationId: uncertainConversation.conversation_id,
3131
+ uncertainMessageId: message.id
3132
+ });
3133
+ runtimeLog("error", "terminal_message_submit_uncertain", {
3134
+ conversation_id: uncertainConversation.conversation_id,
3135
+ agent: executor.kind,
3136
+ terminal_target: terminalControl.target,
3137
+ error: errorMessage,
3138
+ do_not_retry: true,
3139
+ stalled_conversation_ids: stalledConversationIds
3140
+ });
3141
+ presentTerminalUncertain({
3142
+ conversation: uncertainConversation,
3143
+ stalledConversationIds,
3144
+ textInjected: progress.textInjectedAt !== undefined,
3145
+ enterDispatched: progress.enterDispatchedAt !== undefined,
3146
+ monitorPid: bridgeMonitor?.pid
3147
+ }, presentationContext, presentationPorts);
3148
+ }
3149
+ async function runTerminalDispatchTransport({ request, prepared, application, progress, recordPostTransportBookkeepingFailure, deferredForegroundScope }) {
3150
+ const { executor } = request;
3151
+ const { terminalBridge, terminalControl, terminalPayload, preSendRuntime, codexRolloutAcceptanceAnchor, presentationContext, presentationPorts } = prepared;
3152
+ let bridgeMonitor;
3153
+ try {
3154
+ const transportLifecycle = terminalDispatchTransportLifecycle({
3155
+ request,
3156
+ prepared,
3157
+ application,
3158
+ deferredForegroundScope
3159
+ });
3160
+ await terminalBridge.send(executor.kind, terminalControl, terminalPayload, { runtime: preSendRuntime, ...transportLifecycle });
3161
+ if (!progress.enterDispatchedAt) {
3162
+ throw new Error("terminal bridge returned without an enter_dispatched receipt");
3163
+ }
3164
+ if (codexRolloutAcceptanceAnchor?.version === 2 &&
3165
+ cliEnv().AKK_TEST_EXIT_AFTER_VIRGIN_ENTER_DISPATCHED === "1") {
3166
+ cliExit(86);
3167
+ }
3168
+ const submissionOwner = await resolveTerminalDispatchSubmissionOwner(prepared, request, application, progress.stagedConversation);
3169
+ if ("failureReason" in submissionOwner) {
3170
+ presentTerminalIdentityFailure(submissionOwner.conversation, submissionOwner.failureReason, presentationContext, presentationPorts, bridgeMonitor?.pid);
3171
+ return { outcome: "handled" };
3172
+ }
3173
+ const acceptance = submissionOwner.deferredCandidateBindingPending
3174
+ ? { outcome: "pending_acceptance" }
3175
+ : await terminalDispatchAcceptance({
3176
+ request,
3177
+ prepared,
3178
+ conversation: submissionOwner.conversation
3179
+ });
3180
+ const deliveredConversation = application.applyAcceptance(submissionOwner.conversation, acceptance, cliNow().toISOString()).conversation;
3181
+ bridgeMonitor = launchAcceptedTerminalMonitor({
3182
+ request,
3183
+ prepared,
3184
+ conversation: deliveredConversation,
3185
+ acceptance,
3186
+ recordFailure: recordPostTransportBookkeepingFailure
3187
+ });
3188
+ return {
3189
+ outcome: "completed",
3190
+ conversation: deliveredConversation,
3191
+ acceptance,
3192
+ bridgeMonitor
3193
+ };
3194
+ }
3195
+ catch (error) {
3196
+ presentTerminalDispatchTransportFailure({
3197
+ error,
3198
+ request,
3199
+ prepared,
3200
+ application,
3201
+ progress,
3202
+ deferredForegroundScope,
3203
+ bridgeMonitor
3204
+ });
3205
+ return { outcome: "handled" };
3206
+ }
3207
+ }
3208
+ async function runTerminalControlSend(request) {
3209
+ const { transaction, recordMessageAfterSend = false, onTerminalPreflightVerified, deferredCodexForegroundBinding } = request;
3210
+ const prepared = await prepareTerminalControlSend(request);
3211
+ if (!prepared) {
3212
+ return;
3213
+ }
3214
+ const { route, presentationContext, presentationPorts } = prepared;
3215
+ const deferredForegroundScope = deferredCodexForegroundBinding
3216
+ ? bindDeferredForegroundApplicationScope(transaction.scopes, transaction.resources)
3217
+ : undefined;
3218
+ // A newly discovered raw terminal may not have an authoritative Session
3219
+ // yet. Commit that Session only after every pre-input terminal and native
3220
+ // acceptance check has passed, but before the Turn or dispatch ledger can
3221
+ // become durable. This prevents a failed virgin attach from leaving a
3222
+ // zero-identity `bound` Session that fences every later control action.
3223
+ if (deferredCodexForegroundBinding) {
3224
+ deferredForegroundScope?.assertBoundary(deferredForegroundBoundaryProjection(deferredCodexForegroundBinding));
3225
+ }
3226
+ const preTransportRollback = await onTerminalPreflightVerified?.(route);
3227
+ const rollback = terminalDispatchRollbackRepositories({
3228
+ request,
3229
+ prepared,
3230
+ deferredForegroundScope,
3231
+ preTransportRollback: typeof preTransportRollback === "function"
3232
+ ? preTransportRollback
3233
+ : undefined
3234
+ });
3235
+ const dispatch = createTerminalDispatchRuntime(request, prepared, rollback);
3236
+ const { application, progress, recordPostTransportBookkeepingFailure, validatePreparedMessageEvent } = dispatch;
3237
+ try {
3238
+ // Preserve legacy argument-evaluation priority inside the setup catch:
3239
+ // validate once before raw-attach bookkeeping, then let the gated
3240
+ // repository consume only this cached immutable event.
3241
+ application.recordPreparedBookkeeping(recordMessageAfterSend, cliEnv().AKK_TEST_TERMINAL_SETUP_FAILURE === "1", recordMessageAfterSend
3242
+ ? validatePreparedMessageEvent
3243
+ : undefined);
3244
+ }
3245
+ catch (error) {
3246
+ renderTerminalZeroInputAbort(application.recordZeroInputAbort({
3247
+ failureKind: "setup",
3248
+ error,
3249
+ abortedAt: cliNow().toISOString(),
3250
+ injectStatePersistenceFailure: cliEnv().AKK_TEST_ABORTED_STATE_PERSISTENCE_FAILURE === "1"
3251
+ }), presentationContext, presentationPorts, undefined);
3252
+ return;
3253
+ }
3254
+ const transport = await runTerminalDispatchTransport({
3255
+ request,
3256
+ prepared,
3257
+ application,
3258
+ progress,
3259
+ recordPostTransportBookkeepingFailure,
3260
+ deferredForegroundScope
3261
+ });
3262
+ if (transport.outcome === "handled") {
3263
+ return;
3264
+ }
3265
+ const deliveredConversation = transport.conversation;
3266
+ const acceptanceResult = transport.acceptance;
3267
+ const bridgeMonitor = transport.bridgeMonitor;
3268
+ const nativeAccepted = acceptanceResult?.outcome === "agent_accepted";
3269
+ const postSubmissionWarning = application.recordPostSubmissionBookkeeping(deliveredConversation, nativeAccepted, () => cliNow().toISOString());
3270
+ if (postSubmissionWarning !== undefined) {
3271
+ progress.bookkeepingWarning = postSubmissionWarning;
3272
+ }
3273
+ presentTerminalCompleted({
3274
+ conversation: deliveredConversation,
3275
+ acceptance: acceptanceResult,
3276
+ monitorPid: bridgeMonitor?.pid,
3277
+ bookkeepingWarning: progress.bookkeepingWarning
3278
+ }, presentationContext, presentationPorts);
3279
+ }
3280
+ export function createTerminalCommandCliFacade(dependencies) {
3281
+ const call = (operation) => terminalCommandContext.run(dependencies, operation);
3282
+ return Object.freeze({
3283
+ runSend: (options) => call(() => runSend(options)),
3284
+ runRespond: (options) => call(() => runRespond(options)),
3285
+ runApprove: (options) => call(() => runApprove(options))
3286
+ });
3287
+ }
3288
+ //# sourceMappingURL=terminal-command-cli-adapter.js.map