@scotthuang/agent-knock-knock 0.13.2 → 0.13.4

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 (278) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/README.md +27 -6
  3. package/dist/src/callback-outbox-policy.js +27 -6
  4. package/dist/src/callback-outbox-policy.js.map +1 -1
  5. package/dist/src/callback-outbox-service.d.ts +4 -30
  6. package/dist/src/callback-outbox-service.js +21 -154
  7. package/dist/src/callback-outbox-service.js.map +1 -1
  8. package/dist/src/callback-outbox-settlement.js +13 -8
  9. package/dist/src/callback-outbox-settlement.js.map +1 -1
  10. package/dist/src/callback-route-service.d.ts +46 -0
  11. package/dist/src/callback-route-service.js +155 -0
  12. package/dist/src/callback-route-service.js.map +1 -0
  13. package/dist/src/claude-terminal-agent-adapter.js +4 -0
  14. package/dist/src/claude-terminal-agent-adapter.js.map +1 -1
  15. package/dist/src/cli-core.js +9 -1
  16. package/dist/src/cli-core.js.map +1 -1
  17. package/dist/src/codex-process-incarnation.d.ts +8 -0
  18. package/dist/src/codex-process-incarnation.js +36 -0
  19. package/dist/src/codex-process-incarnation.js.map +1 -0
  20. package/dist/src/codex-terminal-agent-adapter.js +4 -0
  21. package/dist/src/codex-terminal-agent-adapter.js.map +1 -1
  22. package/dist/src/deferred-foreground-conflict-policy.d.ts +22 -0
  23. package/dist/src/deferred-foreground-conflict-policy.js +52 -0
  24. package/dist/src/deferred-foreground-conflict-policy.js.map +1 -0
  25. package/dist/src/durable-notification-kernel.d.ts +94 -0
  26. package/dist/src/durable-notification-kernel.js +68 -0
  27. package/dist/src/durable-notification-kernel.js.map +1 -0
  28. package/dist/src/herdr-socket-transport.d.ts +43 -0
  29. package/dist/src/herdr-socket-transport.js +170 -0
  30. package/dist/src/herdr-socket-transport.js.map +1 -0
  31. package/dist/src/herdr-terminal-control-provider.d.ts +2 -45
  32. package/dist/src/herdr-terminal-control-provider.js +8 -173
  33. package/dist/src/herdr-terminal-control-provider.js.map +1 -1
  34. package/dist/src/host-adapter-capabilities.d.ts +35 -0
  35. package/dist/src/host-adapter-capabilities.js +146 -0
  36. package/dist/src/host-adapter-capabilities.js.map +1 -0
  37. package/dist/src/host-adapter.d.ts +9 -3
  38. package/dist/src/host-adapter.js +33 -9
  39. package/dist/src/host-adapter.js.map +1 -1
  40. package/dist/src/host-bridge-tools.d.ts +3 -4
  41. package/dist/src/host-bridge-tools.js +49 -92
  42. package/dist/src/host-bridge-tools.js.map +1 -1
  43. package/dist/src/host-bridge.js +5 -5
  44. package/dist/src/host-bridge.js.map +1 -1
  45. package/dist/src/host-monitor-reconciliation.d.ts +6 -0
  46. package/dist/src/host-monitor-reconciliation.js +96 -0
  47. package/dist/src/host-monitor-reconciliation.js.map +1 -0
  48. package/dist/src/managed-session.js +2 -18
  49. package/dist/src/managed-session.js.map +1 -1
  50. package/dist/src/native-thread-lifecycle-cli-adapter.d.ts +13 -0
  51. package/dist/src/native-thread-lifecycle-cli-adapter.js +635 -3
  52. package/dist/src/native-thread-lifecycle-cli-adapter.js.map +1 -1
  53. package/dist/src/openclaw-list-projection.d.ts +2 -0
  54. package/dist/src/openclaw-list-projection.js +3 -0
  55. package/dist/src/openclaw-list-projection.js.map +1 -0
  56. package/dist/src/openclaw-model-facing-field-policy.d.ts +2 -0
  57. package/dist/src/openclaw-model-facing-field-policy.js +3 -0
  58. package/dist/src/openclaw-model-facing-field-policy.js.map +1 -0
  59. package/dist/src/openclaw-plugin-command-adapter.d.ts +6 -29
  60. package/dist/src/openclaw-plugin-command-adapter.js +7 -2370
  61. package/dist/src/openclaw-plugin-command-adapter.js.map +1 -1
  62. package/dist/src/openclaw-plugin-helpers.d.ts +2 -121
  63. package/dist/src/openclaw-plugin-helpers.js +2 -1164
  64. package/dist/src/openclaw-plugin-helpers.js.map +1 -1
  65. package/dist/src/openclaw-plugin-schemas.d.ts +2 -510
  66. package/dist/src/openclaw-plugin-schemas.js +2 -519
  67. package/dist/src/openclaw-plugin-schemas.js.map +1 -1
  68. package/dist/src/openclaw-plugin-supervisor.d.ts +4 -5
  69. package/dist/src/openclaw-plugin-supervisor.js +5 -94
  70. package/dist/src/openclaw-plugin-supervisor.js.map +1 -1
  71. package/dist/src/openclaw-plugin-value-helpers.d.ts +2 -0
  72. package/dist/src/openclaw-plugin-value-helpers.js +3 -0
  73. package/dist/src/openclaw-plugin-value-helpers.js.map +1 -0
  74. package/dist/src/openclaw-private-authority-offers.d.ts +4 -39
  75. package/dist/src/openclaw-private-authority-offers.js +3 -258
  76. package/dist/src/openclaw-private-authority-offers.js.map +1 -1
  77. package/dist/src/semantic-private-authority-offers.d.ts +41 -0
  78. package/dist/src/semantic-private-authority-offers.js +261 -0
  79. package/dist/src/semantic-private-authority-offers.js.map +1 -0
  80. package/dist/src/semantic-tool-arguments.d.ts +9 -0
  81. package/dist/src/semantic-tool-arguments.js +64 -0
  82. package/dist/src/semantic-tool-arguments.js.map +1 -0
  83. package/dist/src/semantic-tool-catalog.d.ts +48 -0
  84. package/dist/src/semantic-tool-catalog.js +105 -0
  85. package/dist/src/semantic-tool-catalog.js.map +1 -0
  86. package/dist/src/semantic-tool-command-helpers.d.ts +145 -0
  87. package/dist/src/semantic-tool-command-helpers.js +1272 -0
  88. package/dist/src/semantic-tool-command-helpers.js.map +1 -0
  89. package/dist/src/semantic-tool-list-projection.d.ts +10 -0
  90. package/dist/src/semantic-tool-list-projection.js +497 -0
  91. package/dist/src/semantic-tool-list-projection.js.map +1 -0
  92. package/dist/src/semantic-tool-model-facing-field-policy.d.ts +3 -0
  93. package/dist/src/semantic-tool-model-facing-field-policy.js +66 -0
  94. package/dist/src/semantic-tool-model-facing-field-policy.js.map +1 -0
  95. package/dist/src/semantic-tool-presentation.d.ts +37 -0
  96. package/dist/src/semantic-tool-presentation.js +680 -0
  97. package/dist/src/semantic-tool-presentation.js.map +1 -0
  98. package/dist/src/semantic-tool-private-authority.d.ts +39 -0
  99. package/dist/src/semantic-tool-private-authority.js +602 -0
  100. package/dist/src/semantic-tool-private-authority.js.map +1 -0
  101. package/dist/src/semantic-tool-relay.d.ts +16 -0
  102. package/dist/src/semantic-tool-relay.js +175 -0
  103. package/dist/src/semantic-tool-relay.js.map +1 -0
  104. package/dist/src/semantic-tool-runtime.d.ts +10 -0
  105. package/dist/src/semantic-tool-runtime.js +1157 -0
  106. package/dist/src/semantic-tool-runtime.js.map +1 -0
  107. package/dist/src/semantic-tool-schemas.d.ts +563 -0
  108. package/dist/src/semantic-tool-schemas.js +573 -0
  109. package/dist/src/semantic-tool-schemas.js.map +1 -0
  110. package/dist/src/semantic-tool-value-helpers.d.ts +5 -0
  111. package/dist/src/semantic-tool-value-helpers.js +27 -0
  112. package/dist/src/semantic-tool-value-helpers.js.map +1 -0
  113. package/dist/src/store-event-presentation.d.ts +17 -0
  114. package/dist/src/store-event-presentation.js +33 -0
  115. package/dist/src/store-event-presentation.js.map +1 -0
  116. package/dist/src/store.d.ts +3 -16
  117. package/dist/src/store.js +2 -30
  118. package/dist/src/store.js.map +1 -1
  119. package/dist/src/terminal-action-projection.d.ts +3 -17
  120. package/dist/src/terminal-action-projection.js +13 -41
  121. package/dist/src/terminal-action-projection.js.map +1 -1
  122. package/dist/src/terminal-agent-adapter.d.ts +7 -0
  123. package/dist/src/terminal-agent-adapter.js +8 -0
  124. package/dist/src/terminal-agent-adapter.js.map +1 -1
  125. package/dist/src/terminal-agent-bridge.d.ts +35 -332
  126. package/dist/src/terminal-agent-bridge.js +293 -2798
  127. package/dist/src/terminal-agent-bridge.js.map +1 -1
  128. package/dist/src/terminal-approval-cli-adapter.d.ts +17 -0
  129. package/dist/src/terminal-approval-cli-adapter.js +921 -0
  130. package/dist/src/terminal-approval-cli-adapter.js.map +1 -0
  131. package/dist/src/terminal-command-cli-adapter.d.ts +2 -429
  132. package/dist/src/terminal-command-cli-adapter.js +344 -6061
  133. package/dist/src/terminal-command-cli-adapter.js.map +1 -1
  134. package/dist/src/terminal-command-cli-ports.d.ts +443 -0
  135. package/dist/src/terminal-command-cli-ports.js +2 -0
  136. package/dist/src/terminal-command-cli-ports.js.map +1 -0
  137. package/dist/src/terminal-command-dispatch-preparation.d.ts +81 -0
  138. package/dist/src/terminal-command-dispatch-preparation.js +209 -0
  139. package/dist/src/terminal-command-dispatch-preparation.js.map +1 -0
  140. package/dist/src/terminal-command-dispatch-transport.d.ts +35 -0
  141. package/dist/src/terminal-command-dispatch-transport.js +859 -0
  142. package/dist/src/terminal-command-dispatch-transport.js.map +1 -0
  143. package/dist/src/terminal-command-foreground-proof.d.ts +32 -0
  144. package/dist/src/terminal-command-foreground-proof.js +72 -0
  145. package/dist/src/terminal-command-foreground-proof.js.map +1 -0
  146. package/dist/src/terminal-command-send-preflight.d.ts +41 -0
  147. package/dist/src/terminal-command-send-preflight.js +86 -0
  148. package/dist/src/terminal-command-send-preflight.js.map +1 -0
  149. package/dist/src/terminal-composer-classifier.d.ts +55 -0
  150. package/dist/src/terminal-composer-classifier.js +638 -0
  151. package/dist/src/terminal-composer-classifier.js.map +1 -0
  152. package/dist/src/terminal-control-ref.d.ts +23 -0
  153. package/dist/src/terminal-control-ref.js +32 -0
  154. package/dist/src/terminal-control-ref.js.map +1 -1
  155. package/dist/src/terminal-dispatch-recovery-authority.d.ts +56 -0
  156. package/dist/src/terminal-dispatch-recovery-authority.js +251 -0
  157. package/dist/src/terminal-dispatch-recovery-authority.js.map +1 -0
  158. package/dist/src/terminal-dispatch-recovery-cli-adapter.d.ts +1 -9
  159. package/dist/src/terminal-dispatch-recovery-cli-adapter.js +5 -240
  160. package/dist/src/terminal-dispatch-recovery-cli-adapter.js.map +1 -1
  161. package/dist/src/terminal-handoff-cli-adapter.d.ts +1 -11
  162. package/dist/src/terminal-handoff-cli-adapter.js +2 -39
  163. package/dist/src/terminal-handoff-cli-adapter.js.map +1 -1
  164. package/dist/src/terminal-human-explicit-send-cli-adapter.d.ts +25 -0
  165. package/dist/src/terminal-human-explicit-send-cli-adapter.js +903 -0
  166. package/dist/src/terminal-human-explicit-send-cli-adapter.js.map +1 -0
  167. package/dist/src/terminal-identity-authority-cli-adapter.d.ts +1 -6
  168. package/dist/src/terminal-identity-authority-cli-adapter.js +3 -33
  169. package/dist/src/terminal-identity-authority-cli-adapter.js.map +1 -1
  170. package/dist/src/terminal-interaction-cli-adapter.js +209 -210
  171. package/dist/src/terminal-interaction-cli-adapter.js.map +1 -1
  172. package/dist/src/terminal-interaction-core.d.ts +0 -2
  173. package/dist/src/terminal-interaction-core.js +4 -3
  174. package/dist/src/terminal-interaction-core.js.map +1 -1
  175. package/dist/src/terminal-interaction-protocol.d.ts +21 -0
  176. package/dist/src/terminal-interaction-protocol.js +74 -0
  177. package/dist/src/terminal-interaction-protocol.js.map +1 -1
  178. package/dist/src/terminal-interaction-response-bridge.d.ts +126 -0
  179. package/dist/src/terminal-interaction-response-bridge.js +579 -0
  180. package/dist/src/terminal-interaction-response-bridge.js.map +1 -0
  181. package/dist/src/terminal-interaction-response-transaction.d.ts +44 -0
  182. package/dist/src/terminal-interaction-response-transaction.js +56 -0
  183. package/dist/src/terminal-interaction-response-transaction.js.map +1 -0
  184. package/dist/src/terminal-list-action-policy.d.ts +43 -0
  185. package/dist/src/terminal-list-action-policy.js +164 -0
  186. package/dist/src/terminal-list-action-policy.js.map +1 -0
  187. package/dist/src/terminal-list-cli-adapter.d.ts +6 -36
  188. package/dist/src/terminal-list-cli-adapter.js +314 -1711
  189. package/dist/src/terminal-list-cli-adapter.js.map +1 -1
  190. package/dist/src/terminal-list-facts.d.ts +129 -0
  191. package/dist/src/terminal-list-facts.js +179 -0
  192. package/dist/src/terminal-list-facts.js.map +1 -0
  193. package/dist/src/terminal-list-inventory.d.ts +59 -0
  194. package/dist/src/terminal-list-inventory.js +194 -0
  195. package/dist/src/terminal-list-inventory.js.map +1 -0
  196. package/dist/src/terminal-list-ownership-service.d.ts +118 -0
  197. package/dist/src/terminal-list-ownership-service.js +1003 -0
  198. package/dist/src/terminal-list-ownership-service.js.map +1 -0
  199. package/dist/src/terminal-list-projection-service.d.ts +49 -0
  200. package/dist/src/terminal-list-projection-service.js +407 -0
  201. package/dist/src/terminal-list-projection-service.js.map +1 -0
  202. package/dist/src/terminal-list-renderer.d.ts +12 -11
  203. package/dist/src/terminal-list-renderer.js +237 -104
  204. package/dist/src/terminal-list-renderer.js.map +1 -1
  205. package/dist/src/terminal-managed-send-cli-adapter.d.ts +33 -0
  206. package/dist/src/terminal-managed-send-cli-adapter.js +994 -0
  207. package/dist/src/terminal-managed-send-cli-adapter.js.map +1 -0
  208. package/dist/src/terminal-managed-turn-list-action-policy.d.ts +47 -0
  209. package/dist/src/terminal-managed-turn-list-action-policy.js +64 -0
  210. package/dist/src/terminal-managed-turn-list-action-policy.js.map +1 -0
  211. package/dist/src/terminal-model-control-action-policy.d.ts +60 -0
  212. package/dist/src/terminal-model-control-action-policy.js +96 -0
  213. package/dist/src/terminal-model-control-action-policy.js.map +1 -0
  214. package/dist/src/terminal-model-control-availability.d.ts +68 -0
  215. package/dist/src/terminal-model-control-availability.js +157 -0
  216. package/dist/src/terminal-model-control-availability.js.map +1 -0
  217. package/dist/src/terminal-model-control-bridge.d.ts +48 -0
  218. package/dist/src/terminal-model-control-bridge.js +259 -0
  219. package/dist/src/terminal-model-control-bridge.js.map +1 -0
  220. package/dist/src/terminal-model-control-profile.d.ts +55 -0
  221. package/dist/src/terminal-model-control-profile.js +137 -0
  222. package/dist/src/terminal-model-control-profile.js.map +1 -0
  223. package/dist/src/terminal-model-control-subject.d.ts +34 -0
  224. package/dist/src/terminal-model-control-subject.js +58 -0
  225. package/dist/src/terminal-model-control-subject.js.map +1 -0
  226. package/dist/src/terminal-model-control-transaction.d.ts +19 -0
  227. package/dist/src/terminal-model-control-transaction.js +39 -0
  228. package/dist/src/terminal-model-control-transaction.js.map +1 -0
  229. package/dist/src/terminal-model-control.d.ts +331 -0
  230. package/dist/src/terminal-model-control.js +1686 -0
  231. package/dist/src/terminal-model-control.js.map +1 -0
  232. package/dist/src/terminal-monitor-application-service.js +50 -17
  233. package/dist/src/terminal-monitor-application-service.js.map +1 -1
  234. package/dist/src/terminal-native-inspection-bridge.d.ts +211 -0
  235. package/dist/src/terminal-native-inspection-bridge.js +1127 -0
  236. package/dist/src/terminal-native-inspection-bridge.js.map +1 -0
  237. package/dist/src/terminal-runtime-cli-adapter.d.ts +2 -0
  238. package/dist/src/terminal-runtime-cli-adapter.js +59 -17
  239. package/dist/src/terminal-runtime-cli-adapter.js.map +1 -1
  240. package/dist/src/terminal-submission-retry-application.d.ts +24 -0
  241. package/dist/src/terminal-submission-retry-application.js +402 -0
  242. package/dist/src/terminal-submission-retry-application.js.map +1 -0
  243. package/dist/src/terminal-submission-retry-ports.d.ts +30 -0
  244. package/dist/src/terminal-submission-retry-ports.js +2 -0
  245. package/dist/src/terminal-submission-retry-ports.js.map +1 -0
  246. package/dist/src/terminal-submission-retry-reconciliation.d.ts +236 -0
  247. package/dist/src/terminal-submission-retry-reconciliation.js +1130 -0
  248. package/dist/src/terminal-submission-retry-reconciliation.js.map +1 -0
  249. package/dist/src/terminal-submission-retry-transport.d.ts +58 -0
  250. package/dist/src/terminal-submission-retry-transport.js +675 -0
  251. package/dist/src/terminal-submission-retry-transport.js.map +1 -0
  252. package/dist/src/terminal-text-submission-bridge.d.ts +200 -0
  253. package/dist/src/terminal-text-submission-bridge.js +632 -0
  254. package/dist/src/terminal-text-submission-bridge.js.map +1 -0
  255. package/dist/src/terminal-user-explicit-send-policy.d.ts +29 -0
  256. package/dist/src/terminal-user-explicit-send-policy.js +46 -0
  257. package/dist/src/terminal-user-explicit-send-policy.js.map +1 -0
  258. package/dist/src/terminal-watch-cli-adapter.js +91 -143
  259. package/dist/src/terminal-watch-cli-adapter.js.map +1 -1
  260. package/dist/src/terminal-watch-codec.d.ts +9 -0
  261. package/dist/src/terminal-watch-codec.js +1051 -0
  262. package/dist/src/terminal-watch-codec.js.map +1 -0
  263. package/dist/src/terminal-watch-interaction-response-store.d.ts +19 -0
  264. package/dist/src/terminal-watch-interaction-response-store.js +87 -0
  265. package/dist/src/terminal-watch-interaction-response-store.js.map +1 -0
  266. package/dist/src/terminal-watch-record.d.ts +319 -0
  267. package/dist/src/terminal-watch-record.js +742 -0
  268. package/dist/src/terminal-watch-record.js.map +1 -0
  269. package/dist/src/terminal-watch-service.js +48 -27
  270. package/dist/src/terminal-watch-service.js.map +1 -1
  271. package/dist/src/terminal-watch-store.d.ts +3 -279
  272. package/dist/src/terminal-watch-store.js +5 -1779
  273. package/dist/src/terminal-watch-store.js.map +1 -1
  274. package/docs/quickstart-herdr.md +1 -1
  275. package/docs/quickstart-tmux.md +27 -2
  276. package/openclaw.plugin.json +13 -1
  277. package/package.json +7 -2
  278. package/templates/openclaw-skills/agent-knock-knock/SKILL.md +54 -11
@@ -1,351 +1,18 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { formatTerminalConversationId, parseTerminalConversationId, terminalControlCapabilitiesForAdapter, terminalApprovalActionForDecision, terminalApprovalChoices } from "./terminal-agent-adapter.js";
3
3
  import { enrichActiveProcessesWithTerminalControl, TerminalControlInputNotSentError } from "./terminal-control-provider.js";
4
- import { hasCanonicalTerminalEndpoint, sameTerminalControlIncarnation, terminalControlEvidenceMatches, terminalEndpointFromControlRef, terminalEndpointIdentityFromEvidence, terminalEndpointIdentityKey } from "./terminal-control-ref.js";
5
- import { CLAUDE_INJECTED_PASTE_FRAME_PROFILE, claudeInjectedPasteProofEligible, revalidateClaudeComposerProof, settleClaudeInjectedPasteComposer } from "./claude-injected-paste-proof.js";
6
- import { TERMINAL_INTERACTION_SCHEMA, TERMINAL_INTERACTION_VERSION, validateAnyTerminalInteractionResponse, validateTerminalInteractionProjection } from "./terminal-interaction-protocol.js";
7
- import { captureTerminalInteraction } from "./terminal-interaction-core.js";
8
- // Verified Codex profiles through 0.154.0 keep Enter in paste/newline mode for
9
- // 120ms after burst input. Cross that boundary rather than landing on it, and
10
- // also require observable composer stability instead of treating this delay
11
- // alone as acceptance.
12
- const CODEX_PASTE_ENTER_SETTLE_MS = 121;
13
- const CODEX_MULTILINE_SETTLE_POLL_MS = 30;
14
- // A capture can itself take longer than the old two-second deadline on a busy
15
- // tmux server. Give repaint discovery a bounded five-second window and, once
16
- // an exact frame has been observed, always allow the required stable/final
17
- // captures to complete instead of letting the first I/O consume their budget.
18
- const CODEX_MULTILINE_SETTLE_TIMEOUT_MS = 5_000;
19
- const CODEX_MULTILINE_STABLE_CAPTURES = 2;
20
- const CODEX_EXACT_CANDIDATE_GRACE_CAPTURES = 8;
21
- const CODEX_MULTILINE_SETTLE_SCROLLBACK_LINES = 240;
22
- const CODEX_COMPOSER_MARKER = /^[›»](?:\s|$)/u;
23
- const CODEX_COMPOSER_FOOTER = /^(?:gpt-[\w.-]+(?:\s|$)|[-\w.]+ default ·)/u;
24
- const CODEX_COMPLETE_COMPOSER_FOOTER = /^(?:gpt-[\w.-]+(?:\s+\S+)?|[-\w.]+ default)\s+·\s+\S.*$/u;
25
- // Keep every exact slash-completion shape closed per behavior profile so a
26
- // version adding another matching command cannot silently become an authorized
27
- // native command surface.
28
- const CODEX_NATIVE_STATUS_POPUP_BY_PROFILE = {
29
- "codex-tui-0.146.0": [
30
- " /status show current session configuration and token usage"
31
- ],
32
- "codex-tui-0.146.1": [
33
- " /status show current session configuration and token usage"
34
- ],
35
- "codex-tui-0.147.0": [
36
- " /status show current session configuration and token usage",
37
- " /statusline configure which items appear in the status line"
38
- ],
39
- "codex-tui-0.148.0": [
40
- " /status show current session configuration and token usage",
41
- " /statusline configure which items appear in the status line"
42
- ],
43
- "codex-tui-0.149.1": [
44
- " /status show current session configuration and token usage",
45
- " /statusline configure which items appear in the status line"
46
- ],
47
- "codex-tui-0.150.1": [
48
- " /status show current session configuration and token usage",
49
- " /statusline configure which items appear in the status line"
50
- ],
51
- "codex-tui-0.151.0": [
52
- " /status show current session configuration and token usage",
53
- " /statusline configure which items appear in the status line"
54
- ],
55
- "codex-tui-0.153.0": [
56
- " /status show current session configuration and token usage",
57
- " /statusline configure which items appear in the status line"
58
- ],
59
- "codex-tui-0.153.4": [
60
- " /status show current session configuration and token usage",
61
- " /statusline configure which items appear in the status line"
62
- ],
63
- "codex-tui-0.154.0": [
64
- " /status show current session configuration and token usage",
65
- " /statusline configure which items appear in the status line"
66
- ],
67
- "codex-tui-generic-v1": [
68
- " /status show current session configuration and token usage",
69
- " /statusline configure which items appear in the status line"
70
- ]
71
- };
72
- // Codex's verified `/status` profiles are exercised against its canonical
73
- // 80-column status surface. Narrower layouts can truncate the 36-character
74
- // Session UUID after a dynamically sized label column. Exact provider geometry
75
- // is required before input; ANSI visible-buffer width is only a conservative
76
- // fallback diagnostic and never upgrades unknown geometry to safe.
77
- const CODEX_NATIVE_STATUS_MIN_VIEWPORT_BY_PROFILE = {
78
- "codex-tui-0.146.0": 80,
79
- "codex-tui-0.146.1": 80,
80
- "codex-tui-0.147.0": 80,
81
- "codex-tui-0.148.0": 80,
82
- "codex-tui-0.149.1": 80,
83
- "codex-tui-0.150.1": 80,
84
- "codex-tui-0.151.0": 80,
85
- "codex-tui-0.153.0": 80,
86
- "codex-tui-0.153.4": 80,
87
- "codex-tui-0.154.0": 80,
88
- "codex-tui-generic-v1": 80
89
- };
90
- const CLAUDE_NATIVE_STATUS_POPUP_BY_PROFILE = {
91
- "claude-code-2.1.218-native-status": [
92
- "/status Show Claude Code status including version, model, account, API connectivity, and tool statuses",
93
- "/statusline Set up Claude Code's status line UI",
94
- "/ide Manage IDE integrations and show status",
95
- "/usage Show session cost, plan usage, and activity stats"
96
- ],
97
- "claude-code-2.1.226-native-status": [
98
- "/status Show Claude Code status including version, model, account, API connectivity, and tool statuses",
99
- "/statusline Set up Claude Code's status line UI",
100
- "/ide Manage IDE integrations and show status",
101
- "/usage Show session cost, plan usage, and activity stats"
102
- ],
103
- "claude-code-2.1.237-native-status": [
104
- "/status Show Claude Code status including version, model, account, API connectivity, and tool statuses",
105
- "/statusline Set up Claude Code's status line UI",
106
- "/ide Manage IDE integrations and show status",
107
- "/usage Show session cost, plan usage, and activity stats"
108
- ],
109
- "claude-code-2.1.251-native-status": [
110
- "/status Show Claude Code status including version, model, account, API connectivity, and tool statuses",
111
- "/statusline Set up Claude Code's status line UI",
112
- "/ide Manage IDE integrations and show status",
113
- "/usage Show session cost, plan usage, and activity stats"
114
- ],
115
- "claude-code-2.1.259-native-status": [
116
- "/status Show Claude Code status including version, model, account, API connectivity, and tool statuses",
117
- "/statusline Set up Claude Code's status line UI",
118
- "/ide Manage IDE integrations and show status",
119
- "/usage Show session cost, plan usage, and activity stats"
120
- ],
121
- "claude-code-2.1.263-native-status": [
122
- "/status Show Claude Code status including version, model, account, API connectivity, and tool statuses",
123
- "/statusline Set up Claude Code's status line UI",
124
- "/ide Manage IDE integrations and show status",
125
- "/usage Show session cost, plan usage, and activity stats"
126
- ],
127
- "claude-code-2.1.266-native-status": [
128
- "/status Show Claude Code status including version, model, account, API connectivity, and tool statuses",
129
- "/statusline Set up Claude Code's status line UI",
130
- "/ide Manage IDE integrations and show status",
131
- "/usage Show session cost, plan usage, and activity stats"
132
- ],
133
- "claude-code-2.1.267-native-status": [
134
- "/status Show Claude Code status including version, model, account, API connectivity, and tool statuses",
135
- "/statusline Set up Claude Code's status line UI",
136
- "/ide Manage IDE integrations and show status",
137
- "/usage Show session cost, plan usage, and activity stats"
138
- ],
139
- "claude-code-unverified-native-status-v1": [
140
- "/status Show Claude Code status including version, model, account, API connectivity, and tool statuses",
141
- "/statusline Set up Claude Code's status line UI",
142
- "/ide Manage IDE integrations and show status",
143
- "/usage Show session cost, plan usage, and activity stats"
144
- ]
145
- };
146
- const CLAUDE_NATIVE_STATUS_SETTLE_BY_PROFILE = {
147
- "claude-code-2.1.218-native-status": {
148
- minimumStableMs: 80,
149
- maximumSettleMs: 2_000
150
- },
151
- "claude-code-2.1.226-native-status": {
152
- minimumStableMs: 80,
153
- maximumSettleMs: 5_000
154
- },
155
- "claude-code-2.1.237-native-status": {
156
- minimumStableMs: 80,
157
- maximumSettleMs: 5_000
158
- },
159
- "claude-code-2.1.251-native-status": {
160
- minimumStableMs: 80,
161
- maximumSettleMs: 5_000
162
- },
163
- "claude-code-2.1.259-native-status": {
164
- minimumStableMs: 80,
165
- maximumSettleMs: 5_000
166
- },
167
- "claude-code-2.1.263-native-status": {
168
- minimumStableMs: 80,
169
- maximumSettleMs: 5_000
170
- },
171
- "claude-code-2.1.266-native-status": {
172
- minimumStableMs: 80,
173
- maximumSettleMs: 5_000
174
- },
175
- "claude-code-2.1.267-native-status": {
176
- minimumStableMs: 80,
177
- maximumSettleMs: 5_000
178
- },
179
- "claude-code-unverified-native-status-v1": {
180
- minimumStableMs: 80,
181
- maximumSettleMs: 5_000
182
- }
183
- };
184
- const CODEX_LARGE_PASTE_CHAR_THRESHOLD = 1_000;
185
- const TERMINAL_INTERACTION_TTL_MS = 10 * 60 * 1_000;
186
- const TERMINAL_INTERACTION_IDENTIFIER_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/u;
187
- const TERMINAL_INTERACTION_FINGERPRINT_PATTERN = /^[0-9a-f]{64}$/u;
188
- /** A terminal send failed at a boundary that proves input never started. */
189
- export class TerminalInputNotStartedError extends Error {
190
- code = "AKK_TERMINAL_INPUT_NOT_STARTED";
191
- constructor(message, options = {}) {
192
- super(message, options);
193
- this.name = "TerminalInputNotStartedError";
194
- }
195
- }
196
- /**
197
- * Text is already in the terminal composer, but this bridge invocation proved
198
- * that it never called the Enter transport boundary.
199
- *
200
- * Callers may persist this as narrow recovery evidence. It says nothing about
201
- * whether the draft remains exact or whether a later human action submitted
202
- * it; those facts must be re-observed under the terminal lock.
203
- */
204
- export class TerminalEnterDispatchNotAttemptedError extends Error {
205
- code = "AKK_TERMINAL_ENTER_DISPATCH_NOT_ATTEMPTED";
206
- stage = "enter_not_attempted";
207
- constructor(message, options = {}) {
208
- super(message, options);
209
- this.name = "TerminalEnterDispatchNotAttemptedError";
210
- }
211
- }
212
- /**
213
- * The caller's one-shot Enter reservation was invoked, so this recovery
214
- * attempt is permanently consumed even though C-m was not proven dispatched.
215
- */
216
- export class TerminalEnterDispatchReservedError extends Error {
217
- code = "AKK_TERMINAL_ENTER_DISPATCH_RESERVED";
218
- stage = "enter_reserved";
219
- doNotRetry = true;
220
- constructor(message, options = {}) {
221
- super(message, options);
222
- this.name = "TerminalEnterDispatchReservedError";
223
- }
224
- }
225
- /**
226
- * A fail-closed native-inspection submission result.
227
- *
228
- * Only `not_started` proves that a caller may safely retry. Once text has
229
- * reached the composer, or Enter has been attempted, automated retries could
230
- * duplicate input or execute a different command after terminal drift.
231
- */
232
- export class NativeInspectionSubmissionError extends Error {
233
- stage;
234
- code = "AKK_NATIVE_INSPECTION_SUBMISSION_FAILED";
235
- doNotRetry;
236
- constructor(stage, message, options = {}) {
237
- super(message, options);
238
- this.stage = stage;
239
- this.name = "NativeInspectionSubmissionError";
240
- this.doNotRetry = stage !== "not_started";
241
- this.diagnostic = options.diagnostic;
242
- }
243
- diagnostic;
244
- }
245
- class NativeInspectionDiagnosticError extends Error {
246
- diagnostic;
247
- constructor(diagnostic, message, options = {}) {
248
- super(message, options);
249
- this.diagnostic = diagnostic;
250
- this.name = "NativeInspectionDiagnosticError";
251
- }
252
- }
253
- /** A verified modal could not be dismissed across one exact key attempt. */
254
- export class NativeInspectionDismissalError extends Error {
255
- code = "AKK_NATIVE_INSPECTION_DISMISSAL_FAILED";
256
- doNotRetry = true;
257
- constructor(message, options = {}) {
258
- super(message, options);
259
- this.name = "NativeInspectionDismissalError";
260
- }
261
- }
262
- /**
263
- * A response reservation or terminal input may already have happened. The
264
- * same interaction must never be dispatched automatically after this error.
265
- */
266
- export class TerminalInteractionDispatchReservedError extends Error {
267
- stage;
268
- code = "AKK_TERMINAL_INTERACTION_DISPATCH_RESERVED";
269
- doNotRetry = true;
270
- constructor(stage, message, options = {}) {
271
- super(message, options);
272
- this.stage = stage;
273
- this.name = "TerminalInteractionDispatchReservedError";
274
- }
275
- }
276
- /**
277
- * A durable response reservation exists, but the bridge proved that no
278
- * terminal text or key delivery was attempted. The caller must discard the
279
- * one-shot offer and obtain a fresh status projection before trying again.
280
- */
281
- export class TerminalInteractionInputNotStartedError extends Error {
282
- code = "AKK_TERMINAL_INTERACTION_INPUT_NOT_STARTED";
283
- stage = "input_not_started";
284
- doNotRetry = true;
285
- requiresFreshOffer = true;
286
- constructor(message, options = {}) {
287
- super(message, options);
288
- this.name = "TerminalInteractionInputNotStartedError";
289
- }
290
- }
291
- function terminalSendComposerRequirements(agent, multiline, options) {
292
- const exactEmptyRetryAuthority = options.requireExactEmptyComposerAfterBeforeText;
293
- const requireExactEmptyComposerBeforeText = options.requireExactEmptyComposerBeforeText === true;
294
- const userExplicitEnterAfterTextWithoutComposerVeto = options.userExplicitEnterAfterTextWithoutComposerVeto === true;
295
- if (userExplicitEnterAfterTextWithoutComposerVeto &&
296
- (agent !== "codex" || !requireExactEmptyComposerBeforeText)) {
297
- throw new TerminalInputNotStartedError("composer-veto-free Enter requires Codex and an exact-empty pre-text user-explicit boundary");
298
- }
299
- const requireExactComposer = !userExplicitEnterAfterTextWithoutComposerVeto &&
300
- ((agent === "codex" && multiline) ||
301
- options.requireExactComposerBeforeEnter === true ||
302
- requireExactEmptyComposerBeforeText ||
303
- exactEmptyRetryAuthority !== undefined);
304
- if (exactEmptyRetryAuthority !== undefined &&
305
- (agent !== "codex" ||
306
- options.beforeText === undefined ||
307
- !/^[0-9a-f]{64}$/u.test(exactEmptyRetryAuthority.preliminaryComposerDigest))) {
308
- throw new TerminalInputNotStartedError("exact-empty replacement requires Codex, a reservation hook, and a preliminary composer digest");
309
- }
310
- return {
311
- exactEmptyRetryAuthority,
312
- requireExactEmptyComposerBeforeText,
313
- requireExactComposer
314
- };
315
- }
316
- async function recordTerminalSendStage(options, event, preserveUserInput, previousError) {
317
- try {
318
- await options.onTransportStage?.(event);
319
- return previousError;
320
- }
321
- catch (error) {
322
- if (!preserveUserInput)
323
- throw error;
324
- return previousError ?? error;
325
- }
326
- }
327
- async function runTerminalSendBeforeEnter(options, context, preserveUserInput, previousError) {
328
- try {
329
- await options.beforeEnter?.(context);
330
- return previousError;
331
- }
332
- catch (error) {
333
- if (!preserveUserInput)
334
- throw error;
335
- return previousError ?? error;
336
- }
337
- }
338
- async function waitForUserExplicitCodexEnter(input) {
339
- if (!input.enabled)
340
- return;
341
- await input.sleep(Math.max(0, CODEX_PASTE_ENTER_SETTLE_MS -
342
- (input.nowMs() - input.textInjectedAt)));
343
- }
344
- function throwPostTextHookFailureAfterUserEnter(enabled, error) {
345
- if (!enabled || error === undefined)
346
- return;
347
- throw new TerminalEnterDispatchReservedError("user-explicit Codex Enter was dispatched but post-text bookkeeping failed; do not retry", { cause: error });
348
- }
4
+ import { hasCanonicalTerminalEndpoint, sameTerminalControlIncarnation, terminalEndpointFromControlRef, terminalEndpointIdentityKey } from "./terminal-control-ref.js";
5
+ import { terminalInteractionPublicCompatibilityProjection } from "./terminal-interaction-protocol.js";
6
+ import { captureTerminalInteractionRuntimeOffer, exactCurrentModelControlSurface, TerminalInteractionResponseBridge } from "./terminal-interaction-response-bridge.js";
7
+ import { discoverTerminalModelOptions, inspectTerminalModelControlResidual, repairTerminalModelControlResidual, switchTerminalModel, terminalModelControlPlanConforms } from "./terminal-model-control.js";
8
+ import { codexActiveWriterViewerVisible, codexBlockingModalVisible, currentCodexComposerCapture, exactClaudeComposerCapture, exactClaudeInjectedPastePlaceholderCapture, exactClaudeModelControlComposerCapture, exactTerminalComposerCapture, inspectCodexAsyncQuestionInputMode } from "./terminal-composer-classifier.js";
9
+ import { assertTerminalMutationCapabilities, isExactClaudeIdleComposer, stripTerminalEscapeSequences, TerminalNativeInspectionBridge } from "./terminal-native-inspection-bridge.js";
10
+ import { createTerminalModelControlPorts } from "./terminal-model-control-bridge.js";
11
+ import { CODEX_MULTILINE_SETTLE_POLL_MS, CODEX_MULTILINE_SETTLE_SCROLLBACK_LINES, CODEX_PASTE_ENTER_SETTLE_MS, TerminalEnterDispatchNotAttemptedError, TerminalEnterDispatchReservedError, TerminalInputNotStartedError, TerminalTextSubmissionBridge } from "./terminal-text-submission-bridge.js";
12
+ export { TerminalEnterDispatchNotAttemptedError, TerminalEnterDispatchReservedError, TerminalInputNotStartedError } from "./terminal-text-submission-bridge.js";
13
+ export { captureTerminalInteractionRuntimeOffer, TerminalInteractionDispatchReservedError, TerminalInteractionInputNotStartedError } from "./terminal-interaction-response-bridge.js";
14
+ export { inspectCodexAsyncQuestionInputMode } from "./terminal-composer-classifier.js";
15
+ export { exactCodexReadyStyledComposerCapture, isExactClaudeIdleComposer, isExactClaudeNativeInspectionIdleComposer, NativeInspectionDismissalError, NativeInspectionSubmissionError } from "./terminal-native-inspection-bridge.js";
349
16
  function terminalApprovalPreflight(input) {
350
17
  const { adapter, inspection, decision } = input;
351
18
  if (!inspection.approval.approvable) {
@@ -647,274 +314,227 @@ export class TerminalAgentBridge {
647
314
  };
648
315
  }
649
316
  }
650
- async send(agent, terminalControl, text, options = {}) {
317
+ async modelOptions(agent, terminalControl, agentVersion, plan, options = {}) {
651
318
  const adapter = this.registry.require(agent);
652
- const multiline = /[\r\n]/u.test(text.trimEnd());
653
- const { exactEmptyRetryAuthority, requireExactEmptyComposerBeforeText, requireExactComposer } = terminalSendComposerRequirements(adapter.agent, multiline, options);
654
- try {
655
- assertTerminalMutationCapabilities({
656
- provider: this.terminalProvider,
657
- terminal: this.terminalProvider.endpoint(terminalControl),
658
- semantic: [
659
- "send_keys",
660
- ...(requireExactComposer || exactEmptyRetryAuthority
661
- ? ["screen_status"]
662
- : [])
663
- ],
664
- transport: [
665
- "stable_resource_resolution",
666
- "text_delivery",
667
- "key_delivery",
668
- ...(requireExactComposer || exactEmptyRetryAuthority
669
- ? ["screen_capture"]
670
- : [])
671
- ]
672
- });
673
- }
674
- catch (error) {
675
- throw new TerminalInputNotStartedError(error instanceof Error ? error.message : String(error), { cause: error });
676
- }
677
- const normalized = text.trimEnd();
678
- if (!normalized) {
679
- throw new TerminalInputNotStartedError("terminal message is empty");
680
- }
681
- let verifiedForText;
682
- try {
683
- verifiedForText = await this.verifyTerminalIdentity(adapter.agent, terminalControl, options.runtime);
684
- }
685
- catch (error) {
686
- throw new TerminalInputNotStartedError(error instanceof Error ? error.message : String(error), { cause: error });
687
- }
688
- if (!verifiedForText.capabilities.includes("send_keys")) {
689
- throw new TerminalInputNotStartedError(`${adapter.displayName} terminal input capability changed before injection`);
690
- }
691
- let composerVerifiedImmediatelyBeforeText = false;
692
- if (exactEmptyRetryAuthority) {
693
- try {
694
- // Invoking this retry-only hook consumes the durable attempt. The
695
- // independently identity-fenced recapture closes the human-edit race
696
- // before any physical text delivery.
697
- await options.beforeText?.({
698
- agent: adapter.agent,
699
- terminalControl: verifiedForText,
700
- multiline,
701
- text: normalized
702
- });
703
- const finalEmpty = await this.captureCodexComposerSnapshot(adapter, verifiedForText, normalized, options.runtime);
704
- if (finalEmpty.state !== "exact_empty" ||
705
- finalEmpty.digest !==
706
- exactEmptyRetryAuthority.preliminaryComposerDigest ||
707
- !sameTerminalControlIdentity(verifiedForText, finalEmpty.terminalControl)) {
708
- const reason = "reason" in finalEmpty
709
- ? finalEmpty.reason
710
- : `Codex composer is ${finalEmpty.state}`;
711
- throw new Error(`reserved Codex replacement lost its exact-empty composer: ${reason}`);
712
- }
713
- verifiedForText = finalEmpty.terminalControl;
714
- }
715
- catch (error) {
716
- throw new TerminalEnterDispatchReservedError(error instanceof Error ? error.message : String(error), { cause: error });
717
- }
718
- }
719
- else {
720
- try {
721
- await options.beforeText?.({
722
- agent: adapter.agent,
723
- terminalControl: verifiedForText,
724
- multiline,
725
- text: normalized
726
- });
727
- }
728
- catch (error) {
729
- throw new TerminalInputNotStartedError(error instanceof Error ? error.message : String(error), { cause: error });
730
- }
731
- if (requireExactEmptyComposerBeforeText) {
732
- try {
733
- verifiedForText = await this.verifyExactEmptyComposerBeforeText(adapter, verifiedForText, normalized, options.runtime, options.allowWorkingComposerForUserExplicit === true);
734
- composerVerifiedImmediatelyBeforeText = true;
735
- }
736
- catch (error) {
737
- throw new TerminalInputNotStartedError(error instanceof Error ? error.message : String(error), { cause: error });
738
- }
739
- }
740
- }
741
- if (!composerVerifiedImmediatelyBeforeText) {
742
- try {
743
- const reverifiedForText = await this.verifyTerminalIdentity(adapter.agent, verifiedForText, options.runtime);
744
- if (!sameTerminalControlIdentity(verifiedForText, reverifiedForText)) {
745
- throw new Error("terminal identity changed after the final pre-text check");
746
- }
747
- verifiedForText = reverifiedForText;
748
- }
749
- catch (error) {
750
- const message = error instanceof Error ? error.message : String(error);
751
- if (exactEmptyRetryAuthority) {
752
- throw new TerminalEnterDispatchReservedError(message, {
753
- cause: error
754
- });
755
- }
756
- throw new TerminalInputNotStartedError(message, { cause: error });
757
- }
758
- }
759
- try {
760
- await this.terminalProvider.sendText(this.terminalProvider.endpoint(verifiedForText), normalized);
761
- }
762
- catch (error) {
763
- if (exactEmptyRetryAuthority) {
764
- throw new TerminalEnterDispatchReservedError("reserved Codex replacement text delivery outcome is uncertain; do not retry", { cause: error });
765
- }
766
- if (error instanceof TerminalControlInputNotSentError) {
767
- throw new TerminalInputNotStartedError(error.message, {
768
- cause: error
769
- });
770
- }
771
- throw error;
772
- }
773
- const textInjectedAt = this.nowMs();
774
- const preserveUserInput = options.userExplicitEnterAfterTextWithoutComposerVeto === true;
775
- const allowClaudeInjectedPastePlaceholder = claudeInjectedPasteProofEligible({
776
- agent: adapter.agent,
777
- multiline,
778
- composerVerifiedImmediatelyBeforeText,
779
- terminalControl: verifiedForText
319
+ assertTerminalModelControlPlan(adapter, terminalControl, agentVersion, plan);
320
+ assertTerminalMutationCapabilities({
321
+ provider: this.terminalProvider,
322
+ terminal: this.terminalProvider.endpoint(terminalControl),
323
+ semantic: ["send_keys", "screen_status"],
324
+ transport: [
325
+ "stable_resource_resolution",
326
+ "screen_capture",
327
+ "ansi_capture",
328
+ "text_delivery",
329
+ "key_delivery"
330
+ ]
780
331
  });
781
- let postTextHookError;
782
- let verifiedForEnter;
783
- let claudeComposerProof;
784
- try {
785
- postTextHookError = await recordTerminalSendStage(options, {
786
- stage: "text_injected",
787
- agent: adapter.agent,
788
- terminalControl: verifiedForText,
789
- multiline
790
- }, preserveUserInput);
791
- // Text and Enter are separate transport operations. Autonomous sends
792
- // retain their exact Composer proof. A user-explicit managed Codex Send
793
- // crosses only the native paste-suppression window, then keeps terminal
794
- // identity as its final physical boundary.
795
- if (adapter.agent === "codex" && requireExactComposer) {
796
- verifiedForEnter = await this.settleCodexMultilineComposer(adapter, terminalControl, normalized, options.runtime, {
797
- allowOpaqueLargePastePlaceholder: exactEmptyRetryAuthority === undefined,
798
- allowWorkingComposer: options.allowWorkingComposerForUserExplicit === true
799
- });
800
- }
801
- else if (allowClaudeInjectedPastePlaceholder) {
802
- claudeComposerProof = await settleClaudeInjectedPasteComposer({
803
- terminalControl: verifiedForText,
804
- expectedText: normalized,
805
- runtime: options.runtime,
806
- allowWorkingComposer: options.allowWorkingComposerForUserExplicit === true,
807
- ports: this.claudeInjectedPasteProofPorts(adapter)
808
- });
809
- verifiedForEnter = claudeComposerProof.terminalControl;
810
- }
811
- else if (requireExactComposer) {
812
- verifiedForEnter = await this.verifyExactComposerBeforeEnter(adapter, terminalControl, normalized, options.runtime, options.allowWorkingComposerForUserExplicit === true);
813
- }
814
- else {
815
- await waitForUserExplicitCodexEnter({
816
- enabled: preserveUserInput,
817
- textInjectedAt,
818
- nowMs: this.nowMs,
819
- sleep: this.sleep
820
- });
821
- verifiedForEnter = await this.verifyTerminalIdentity(adapter.agent, terminalControl, options.runtime);
822
- }
823
- postTextHookError = await runTerminalSendBeforeEnter(options, {
824
- agent: adapter.agent,
825
- terminalControl: verifiedForEnter,
826
- multiline,
827
- text: normalized
828
- }, preserveUserInput, postTextHookError);
829
- // `beforeEnter` may await Store and terminal identity fences. The human
830
- // can still edit the composer or change the native thread while that
831
- // callback is pending, so recapture before attempting Enter.
832
- if (requireExactComposer) {
833
- if (adapter.agent === "codex") {
834
- verifiedForEnter = await this.settleCodexMultilineComposer(adapter, terminalControl, normalized, options.runtime, {
835
- allowOpaqueLargePastePlaceholder: exactEmptyRetryAuthority === undefined,
836
- allowWorkingComposer: options.allowWorkingComposerForUserExplicit === true
837
- });
838
- }
839
- else if (allowClaudeInjectedPastePlaceholder && claudeComposerProof) {
840
- verifiedForEnter = await revalidateClaudeComposerProof({
841
- proof: claudeComposerProof,
842
- expectedText: normalized,
843
- runtime: options.runtime,
844
- allowWorkingComposer: options.allowWorkingComposerForUserExplicit === true,
845
- ports: this.claudeInjectedPasteProofPorts(adapter)
846
- });
847
- }
848
- else {
849
- verifiedForEnter = await this.verifyExactComposerBeforeEnter(adapter, terminalControl, normalized, options.runtime, options.allowWorkingComposerForUserExplicit === true);
850
- }
851
- }
852
- else if (options.beforeEnter) {
853
- verifiedForEnter = await this.verifyTerminalIdentity(adapter.agent, terminalControl, options.runtime);
854
- }
332
+ const result = await discoverTerminalModelOptions({
333
+ agent,
334
+ agentVersion,
335
+ plan,
336
+ terminalControl,
337
+ ports: this.modelControlPorts(adapter, plan, options),
338
+ initialResidual: options.initialResidual
339
+ });
340
+ return {
341
+ terminalControl: result.terminalControl,
342
+ catalog: result.catalog
343
+ };
344
+ }
345
+ async setModel(agent, terminalControl, agentVersion, plan, expectedCatalogFingerprint, request, options = {}) {
346
+ const adapter = this.registry.require(agent);
347
+ assertTerminalModelControlPlan(adapter, terminalControl, agentVersion, plan);
348
+ assertTerminalMutationCapabilities({
349
+ provider: this.terminalProvider,
350
+ terminal: this.terminalProvider.endpoint(terminalControl),
351
+ semantic: ["send_keys", "screen_status"],
352
+ transport: [
353
+ "stable_resource_resolution",
354
+ "screen_capture",
355
+ "ansi_capture",
356
+ "text_delivery",
357
+ "key_delivery"
358
+ ]
359
+ });
360
+ const result = await switchTerminalModel({
361
+ agent,
362
+ agentVersion,
363
+ plan,
364
+ terminalControl,
365
+ expectedCatalogFingerprint,
366
+ request,
367
+ ports: this.modelControlPorts(adapter, plan, options)
368
+ });
369
+ return {
370
+ ...result,
371
+ terminalControl: result.terminalControl
372
+ };
373
+ }
374
+ async inspectModelControlResidual(agent, terminalControl, agentVersion, plan, options = {}) {
375
+ const adapter = this.registry.require(agent);
376
+ assertTerminalModelControlPlan(adapter, terminalControl, agentVersion, plan);
377
+ assertTerminalMutationCapabilities({
378
+ provider: this.terminalProvider,
379
+ terminal: this.terminalProvider.endpoint(terminalControl),
380
+ semantic: ["screen_status"],
381
+ transport: ["stable_resource_resolution", "screen_capture", "ansi_capture"]
382
+ });
383
+ return inspectTerminalModelControlResidual({
384
+ plan,
385
+ terminalControl,
386
+ ports: this.modelControlPorts(adapter, plan, options)
387
+ });
388
+ }
389
+ async repairModelControlResidual(agent, terminalControl, agentVersion, plan, expectedResidualFingerprint, options = {}) {
390
+ const adapter = this.registry.require(agent);
391
+ assertTerminalModelControlPlan(adapter, terminalControl, agentVersion, plan);
392
+ assertTerminalMutationCapabilities({
393
+ provider: this.terminalProvider,
394
+ terminal: this.terminalProvider.endpoint(terminalControl),
395
+ semantic: ["send_keys", "screen_status"],
396
+ transport: [
397
+ "stable_resource_resolution",
398
+ "screen_capture",
399
+ "ansi_capture",
400
+ "key_delivery"
401
+ ]
402
+ });
403
+ const result = await repairTerminalModelControlResidual({
404
+ plan,
405
+ terminalControl,
406
+ expectedResidualFingerprint,
407
+ ports: this.modelControlPorts(adapter, plan, options)
408
+ });
409
+ return {
410
+ ...result,
411
+ terminalControl: result.terminalControl
412
+ };
413
+ }
414
+ modelControlPorts(adapter, plan, options) {
415
+ if (!options.beforeInput) {
416
+ throw new Error("terminal model control requires current-snapshot authority before every input");
855
417
  }
856
- catch (error) {
857
- if (exactEmptyRetryAuthority) {
858
- if (error instanceof TerminalEnterDispatchReservedError) {
859
- throw error;
860
- }
861
- throw new TerminalEnterDispatchReservedError(error instanceof Error ? error.message : String(error), { cause: error });
418
+ return createTerminalModelControlPorts({
419
+ adapter,
420
+ plan,
421
+ runtime: options.runtime,
422
+ beforeInput: options.beforeInput,
423
+ loadCodexCatalog: options.loadCodexCatalog,
424
+ runtimePorts: {
425
+ verifyTerminalIdentity: (agent, control, runtime) => this.verifyTerminalIdentity(agent, control, runtime),
426
+ captureStyled: (control) => this.terminalProvider.capture(this.terminalProvider.endpoint(control), { scrollbackLines: 160, preserveEscapes: true }),
427
+ sendText: (control, text) => this.terminalProvider.sendText(this.terminalProvider.endpoint(control), text),
428
+ sendKeys: (control, keys) => this.terminalProvider.sendKeys(this.terminalProvider.endpoint(control), keys),
429
+ sleep: this.sleep
430
+ },
431
+ classifiers: {
432
+ stripEscapes: stripTerminalEscapeSequences,
433
+ sameIdentity: sameTerminalControlIdentity,
434
+ currentCodexComposer: currentCodexComposerCapture,
435
+ inspectCodexAsyncQuestionInputMode,
436
+ codexActiveWriterViewerVisible,
437
+ isExactClaudeIdleComposer,
438
+ exactClaudeModelControlComposer: exactClaudeModelControlComposerCapture,
439
+ exactTerminalComposer: exactTerminalComposerCapture
862
440
  }
863
- if (error instanceof TerminalEnterDispatchNotAttemptedError) {
864
- throw error;
441
+ });
442
+ }
443
+ textSubmissionBridge() {
444
+ return new TerminalTextSubmissionBridge({
445
+ runtime: {
446
+ preflight: (terminalControl, operation) => {
447
+ const terminal = this.terminalProvider.endpoint(terminalControl);
448
+ switch (operation) {
449
+ case "send":
450
+ assertTerminalMutationCapabilities({
451
+ provider: this.terminalProvider,
452
+ terminal,
453
+ semantic: ["send_keys"],
454
+ transport: [
455
+ "stable_resource_resolution",
456
+ "text_delivery",
457
+ "key_delivery"
458
+ ]
459
+ });
460
+ return;
461
+ case "send_with_composer":
462
+ assertTerminalMutationCapabilities({
463
+ provider: this.terminalProvider,
464
+ terminal,
465
+ semantic: ["send_keys", "screen_status"],
466
+ transport: [
467
+ "stable_resource_resolution",
468
+ "text_delivery",
469
+ "key_delivery",
470
+ "screen_capture"
471
+ ]
472
+ });
473
+ return;
474
+ case "observe_composer":
475
+ assertTerminalMutationCapabilities({
476
+ provider: this.terminalProvider,
477
+ terminal,
478
+ semantic: ["screen_status"],
479
+ transport: ["stable_resource_resolution", "screen_capture"]
480
+ });
481
+ }
482
+ },
483
+ verifyIdentity: (agent, control, runtime) => this.verifyTerminalIdentity(agent, control, runtime),
484
+ captureInspection: (adapter, control, options) => this.captureInspection(adapter, control, options),
485
+ captureStyled: (control) => this.terminalProvider.capture(this.terminalProvider.endpoint(control), {
486
+ scrollbackLines: CODEX_MULTILINE_SETTLE_SCROLLBACK_LINES,
487
+ preserveEscapes: true
488
+ }),
489
+ deliverText: (control, text) => this.terminalProvider.sendText(this.terminalProvider.endpoint(control), text),
490
+ dispatchEnter: (control) => this.terminalProvider.sendKeys(this.terminalProvider.endpoint(control), ["C-m"]),
491
+ nowMs: () => this.nowMs(),
492
+ sleep: (milliseconds) => this.sleep(milliseconds)
493
+ },
494
+ classifiers: {
495
+ sameIdentity: sameTerminalControlIdentity,
496
+ stripEscapes: stripTerminalEscapeSequences,
497
+ codexBlockingModalVisible,
498
+ inspectCodexAsyncQuestionInputMode,
499
+ currentCodexComposer: currentCodexComposerCapture,
500
+ exactTerminalComposer: exactTerminalComposerCapture,
501
+ exactClaudeComposer: exactClaudeComposerCapture,
502
+ exactClaudeInjectedPastePlaceholder: exactClaudeInjectedPastePlaceholderCapture
865
503
  }
866
- throw new TerminalEnterDispatchNotAttemptedError(error instanceof Error ? error.message : String(error), { cause: error });
867
- }
868
- // Do not include this transport call (or anything after it) in the typed
869
- // pre-Enter proof: a thrown key-delivery error may still have emitted C-m.
870
- try {
871
- await this.terminalProvider.sendKeys(this.terminalProvider.endpoint(verifiedForEnter), ["C-m"]);
872
- postTextHookError = await recordTerminalSendStage(options, {
873
- stage: "enter_dispatched",
874
- agent: adapter.agent,
875
- terminalControl: verifiedForEnter,
876
- multiline
877
- }, preserveUserInput, postTextHookError);
878
- throwPostTextHookFailureAfterUserEnter(preserveUserInput, postTextHookError);
879
- }
880
- catch (error) {
881
- if (exactEmptyRetryAuthority) {
882
- throw new TerminalEnterDispatchReservedError("reserved Codex Enter dispatch outcome is uncertain; do not retry", { cause: error });
504
+ });
505
+ }
506
+ interactionResponseBridge() {
507
+ return new TerminalInteractionResponseBridge(this.terminalProvider, {
508
+ captureInspection: (agent, control, runtime, scrollbackLines) => this.captureInspection(this.registry.require(agent), control, {
509
+ runtime,
510
+ scrollbackLines
511
+ }),
512
+ verifyIdentity: (agent, control, runtime) => this.verifyTerminalIdentity(agent, control, runtime),
513
+ now: () => this.now(),
514
+ sleep: (milliseconds) => this.sleep(milliseconds)
515
+ });
516
+ }
517
+ nativeInspectionBridge() {
518
+ return new TerminalNativeInspectionBridge({
519
+ registry: this.registry,
520
+ terminalProvider: this.terminalProvider,
521
+ runtime: {
522
+ captureInspection: (adapter, control, options) => this.captureInspection(adapter, control, options),
523
+ verifyIdentity: (agent, control, runtime) => this.verifyTerminalIdentity(agent, control, runtime),
524
+ statusFromInspection: (adapter, control, inspection, options) => statusFromInspection(adapter, control, inspection, options),
525
+ nowMs: () => this.nowMs(),
526
+ sleep: (milliseconds) => this.sleep(milliseconds)
883
527
  }
884
- throw error;
885
- }
886
- return {
887
- stage: "enter_dispatched",
888
- agent: adapter.agent,
889
- terminalControl: verifiedForEnter,
890
- multiline
891
- };
528
+ });
529
+ }
530
+ async send(agent, terminalControl, text, options = {}) {
531
+ const adapter = this.registry.require(agent);
532
+ return this.textSubmissionBridge().send(adapter, terminalControl, text, options);
892
533
  }
893
534
  /** Observe only the current, live Codex composer without exposing its text. */
894
535
  async observeCodexComposer(terminalControl, expectedText, options = {}) {
895
536
  const adapter = this.registry.require("codex");
896
- const normalized = expectedText.trimEnd();
897
- if (!normalized) {
898
- return {
899
- state: "unavailable",
900
- reason: "expected Codex draft is empty"
901
- };
902
- }
903
- try {
904
- assertTerminalMutationCapabilities({
905
- provider: this.terminalProvider,
906
- terminal: this.terminalProvider.endpoint(terminalControl),
907
- semantic: ["screen_status"],
908
- transport: ["stable_resource_resolution", "screen_capture"]
909
- });
910
- }
911
- catch (error) {
912
- return {
913
- state: "unavailable",
914
- reason: error instanceof Error ? error.message : String(error)
915
- };
916
- }
917
- return this.settleCodexComposerObservation(adapter, terminalControl, normalized, options.runtime, { minimumStableMs: CODEX_MULTILINE_SETTLE_POLL_MS });
537
+ return this.textSubmissionBridge().observeCodexComposer(adapter, terminalControl, expectedText, options.runtime);
918
538
  }
919
539
  /**
920
540
  * Honor one explicit user Send against the current Codex terminal. The
@@ -1102,6 +722,7 @@ export class TerminalAgentBridge {
1102
722
  */
1103
723
  async submitExactCodexDraft(terminalControl, expectedText, options) {
1104
724
  const adapter = this.registry.require("codex");
725
+ const textSubmission = this.textSubmissionBridge();
1105
726
  const normalized = expectedText.trimEnd();
1106
727
  if (!normalized) {
1107
728
  throw new TerminalEnterDispatchNotAttemptedError("expected Codex draft is empty");
@@ -1118,7 +739,7 @@ export class TerminalAgentBridge {
1118
739
  "key_delivery"
1119
740
  ]
1120
741
  });
1121
- const observation = await this.settleCodexComposerObservation(adapter, terminalControl, normalized, options.runtime, {
742
+ const observation = await textSubmission.settleCodexComposerObservation(adapter, terminalControl, normalized, options.runtime, {
1122
743
  minimumStableMs: CODEX_PASTE_ENTER_SETTLE_MS,
1123
744
  requiredState: "exact_draft"
1124
745
  });
@@ -1146,7 +767,7 @@ export class TerminalAgentBridge {
1146
767
  text: normalized,
1147
768
  composerDigest: preliminaryObservation.digest
1148
769
  });
1149
- const recaptured = await this.captureCodexComposerSnapshot(adapter, preliminaryObservation.terminalControl, normalized, options.runtime);
770
+ const recaptured = await textSubmission.captureCodexComposerSnapshot(adapter, preliminaryObservation.terminalControl, normalized, options.runtime);
1150
771
  if (recaptured.state !== "exact_draft" ||
1151
772
  recaptured.digest !== preliminaryObservation.digest ||
1152
773
  !sameTerminalControlIdentity(preliminaryObservation.terminalControl, recaptured.terminalControl)) {
@@ -1175,66 +796,6 @@ export class TerminalAgentBridge {
1175
796
  enterCount: 1
1176
797
  };
1177
798
  }
1178
- async verifyExactComposerBeforeEnter(adapter, terminalControl, expectedText, runtime, allowWorkingComposer = false) {
1179
- const captured = await this.captureInspection(adapter, terminalControl, {
1180
- runtime,
1181
- scrollbackLines: CODEX_MULTILINE_SETTLE_SCROLLBACK_LINES
1182
- });
1183
- const exactComposer = exactTerminalComposerCapture(adapter.agent, captured.screen, expectedText);
1184
- if (captured.inspection.approval.blocked ||
1185
- !(["idle", "unknown"].includes(captured.inspection.activity.state) ||
1186
- (allowWorkingComposer &&
1187
- captured.inspection.activity.state === "working")) ||
1188
- !exactComposer) {
1189
- throw new Error(`${adapter.displayName} composer was not exact and idle immediately before Enter`);
1190
- }
1191
- const verifiedImmediatelyBeforeEnter = await this.verifyTerminalIdentity(adapter.agent, captured.terminalControl, runtime);
1192
- if (!sameTerminalControlIdentity(captured.terminalControl, verifiedImmediatelyBeforeEnter)) {
1193
- throw new Error("terminal control identity changed after the final exact composer capture");
1194
- }
1195
- return verifiedImmediatelyBeforeEnter;
1196
- }
1197
- claudeInjectedPasteProofPorts(adapter) {
1198
- return {
1199
- nowMs: this.nowMs,
1200
- sleep: this.sleep,
1201
- capture: (terminalControl, runtime) => this.captureInspection(adapter, terminalControl, {
1202
- runtime,
1203
- scrollbackLines: CODEX_MULTILINE_SETTLE_SCROLLBACK_LINES
1204
- }),
1205
- verifyIdentity: (terminalControl, runtime) => this.verifyTerminalIdentity(adapter.agent, terminalControl, runtime),
1206
- exactDraft: exactClaudeComposerCapture,
1207
- exactInjectedPastePlaceholder: exactClaudeInjectedPastePlaceholderCapture
1208
- };
1209
- }
1210
- async verifyExactEmptyComposerBeforeText(adapter, terminalControl, expectedText, runtime, allowWorkingComposer = false) {
1211
- if (adapter.agent === "codex") {
1212
- const snapshot = await this.captureCodexComposerSnapshot(adapter, terminalControl, expectedText, runtime);
1213
- if (snapshot.state !== "exact_empty") {
1214
- const reason = "reason" in snapshot
1215
- ? snapshot.reason
1216
- : `Codex composer is ${snapshot.state}`;
1217
- throw new Error(`Codex composer was not exactly empty immediately before text: ${reason}`);
1218
- }
1219
- return snapshot.terminalControl;
1220
- }
1221
- const captured = await this.captureInspection(adapter, terminalControl, {
1222
- runtime,
1223
- scrollbackLines: CODEX_MULTILINE_SETTLE_SCROLLBACK_LINES
1224
- });
1225
- if (captured.inspection.approval.blocked ||
1226
- !(["idle", "unknown"].includes(captured.inspection.activity.state) ||
1227
- (allowWorkingComposer &&
1228
- captured.inspection.activity.state === "working")) ||
1229
- !exactTerminalComposerCapture(adapter.agent, captured.screen, "")) {
1230
- throw new Error(`${adapter.displayName} composer was not exactly empty immediately before text`);
1231
- }
1232
- const verified = await this.verifyTerminalIdentity(adapter.agent, captured.terminalControl, runtime);
1233
- if (!sameTerminalControlIdentity(captured.terminalControl, verified)) {
1234
- throw new Error("terminal control identity changed after the final empty composer capture");
1235
- }
1236
- return verified;
1237
- }
1238
799
  /**
1239
800
  * Submit one closed, adapter-owned native inspection command.
1240
801
  *
@@ -1243,182 +804,19 @@ export class TerminalAgentBridge {
1243
804
  * draft untouched instead of issuing the legacy best-effort C-u cleanup.
1244
805
  */
1245
806
  async submitNativeInspection(agent, terminalControl, plan, options = {}) {
1246
- return this.submitClosedNativeInspection(agent, terminalControl, plan, options, { requireCodexReadyComposer: agent === "codex" });
807
+ return this.nativeInspectionBridge().submitNativeInspection(agent, terminalControl, plan, options);
1247
808
  }
1248
809
  /**
1249
810
  * Submit Codex's closed, version-profiled `/status` probe.
1250
811
  *
1251
812
  * Unlike the generic native-inspection entry point, callers provide only
1252
- * the detected Codex version, never a command or plan. The bridge proves an
1253
- * exact empty (or fully dim replace-on-type) ANSI composer before injecting
1254
- * text, then crosses Codex's paste suppression window under the same exact
1255
- * composer and terminal-identity fences used by native inspection.
813
+ * the detected Codex version, never a command or plan.
1256
814
  */
1257
815
  async submitCodexStatusProbe(terminalControl, agentVersion, options = {}) {
1258
- const adapter = this.registry.require("codex");
1259
- let plan;
1260
- try {
1261
- const capability = adapter.probeNativeInspection?.(agentVersion);
1262
- if (capability?.status !== "supported" ||
1263
- capability.statusInspection !== true) {
1264
- throw new NativeInspectionDiagnosticError("unsupported_profile", capability?.reason ??
1265
- `Codex ${agentVersion} has no closed /status behavior profile`);
1266
- }
1267
- const planned = adapter.planNativeInspection?.({ kind: "status" }, capability);
1268
- if (!planned) {
1269
- throw new NativeInspectionDiagnosticError("unsupported_profile", `Codex ${agentVersion} did not produce a closed /status plan`);
1270
- }
1271
- plan = planned;
1272
- }
1273
- catch (error) {
1274
- throw nativeInspectionSubmissionError("not_started", error, "unsupported_profile");
1275
- }
1276
- const result = await this.submitClosedNativeInspection("codex", terminalControl, plan, options, { requireCodexReadyComposer: true });
1277
- if (!result.preTextScreenDigest) {
1278
- throw new Error("Codex /status pre-text composer evidence is missing");
1279
- }
1280
- return {
1281
- ...result,
1282
- agent: "codex",
1283
- preTextScreenDigest: result.preTextScreenDigest,
1284
- observationBaselineDigest: bareDigestFromNativeInspectionScreenFingerprint(result.preEnterScreenDigest),
1285
- observationScrollbackLines: CODEX_MULTILINE_SETTLE_SCROLLBACK_LINES
1286
- };
1287
- }
1288
- async submitClosedNativeInspection(agent, terminalControl, plan, options, safety = {}) {
1289
- const adapter = this.registry.require(agent);
1290
- try {
1291
- assertClosedStatusInspectionPlan(adapter, terminalControl, plan);
1292
- assertTerminalMutationCapabilities({
1293
- provider: this.terminalProvider,
1294
- terminal: this.terminalProvider.endpoint(terminalControl),
1295
- semantic: ["send_keys", "screen_status"],
1296
- transport: [
1297
- "stable_resource_resolution",
1298
- "screen_capture",
1299
- "text_delivery",
1300
- "key_delivery",
1301
- ...(safety.requireCodexReadyComposer
1302
- ? ["ansi_capture"]
1303
- : [])
1304
- ]
1305
- });
1306
- }
1307
- catch (error) {
1308
- throw nativeInspectionSubmissionError("not_started", error, "capability_unavailable");
1309
- }
1310
- let verifiedForText;
1311
- try {
1312
- verifiedForText = await this.verifyTerminalIdentity(adapter.agent, terminalControl, options.runtime);
1313
- }
1314
- catch (error) {
1315
- throw nativeInspectionSubmissionError("not_started", error, "identity_unverified");
1316
- }
1317
- let preTextScreenDigest;
1318
- if (safety.requireCodexReadyComposer) {
1319
- try {
1320
- const ready = await this.captureCodexReadyComposer(adapter, verifiedForText, plan, options.runtime);
1321
- verifiedForText = ready.terminalControl;
1322
- preTextScreenDigest = ready.screenDigest;
1323
- }
1324
- catch (error) {
1325
- throw nativeInspectionSubmissionError("not_started", error, "composer_not_ready");
1326
- }
1327
- }
1328
- try {
1329
- if (!verifiedForText.capabilities.includes("send_keys")) {
1330
- throw new TerminalControlInputNotSentError(`${adapter.displayName} native inspection input capability changed`);
1331
- }
1332
- await this.terminalProvider.sendText(this.terminalProvider.endpoint(verifiedForText), plan.command);
1333
- }
1334
- catch (error) {
1335
- if (error instanceof TerminalControlInputNotSentError) {
1336
- throw nativeInspectionSubmissionError("not_started", error, "text_delivery_unproven");
1337
- }
1338
- // The transport cannot prove whether an untyped failure happened before
1339
- // or after tmux accepted the literal input. Fail closed as injected.
1340
- throw nativeInspectionSubmissionError("text_injected", error, "text_delivery_unproven");
1341
- }
1342
- let settled;
1343
- try {
1344
- settled = await this.settleNativeInspectionComposer(adapter, terminalControl, plan, options.runtime);
1345
- await options.beforeEnter?.({
1346
- agent: adapter.agent,
1347
- terminalControl: settled.terminalControl,
1348
- plan,
1349
- preEnterScreenDigest: settled.screenDigest,
1350
- materialization: settled.materialization
1351
- });
1352
- if (safety.requireCodexReadyComposer) {
1353
- settled = {
1354
- ...settled,
1355
- terminalControl: await this.assertFinalCodexStatusViewport(settled.terminalControl, plan, options.runtime)
1356
- };
1357
- }
1358
- // Viewport inspection may await provider I/O and the PTY may receive
1359
- // human input while that proof is in flight. Keep the exact composer
1360
- // capture as the final substantive asynchronous evidence before the
1361
- // single Enter attempt.
1362
- settled = await this.revalidateNativeInspectionComposer(adapter, settled.terminalControl, plan, settled.materialization, options.runtime);
1363
- }
1364
- catch (error) {
1365
- throw nativeInspectionSubmissionError("text_injected", error, "composer_not_exact");
1366
- }
1367
- try {
1368
- // Exactly one Enter attempt. Any error is submission-uncertain and must
1369
- // never trigger a fallback executable or a blind second Enter.
1370
- await this.terminalProvider.sendKeys(this.terminalProvider.endpoint(settled.terminalControl), ["C-m"]);
1371
- }
1372
- catch (error) {
1373
- throw nativeInspectionSubmissionError("enter_uncertain", error, "enter_uncertain");
1374
- }
1375
- return {
1376
- stage: "enter_dispatched",
1377
- agent: adapter.agent,
1378
- terminalControl: settled.terminalControl,
1379
- command: plan.command,
1380
- behaviorProfile: plan.behaviorProfile,
1381
- preEnterScreenDigest: settled.screenDigest,
1382
- preEnterEvidenceInventory: settled.evidenceInventory,
1383
- materialization: settled.materialization,
1384
- enterCount: 1,
1385
- ...(preTextScreenDigest ? { preTextScreenDigest } : {})
1386
- };
816
+ return this.nativeInspectionBridge().submitCodexStatusProbe(terminalControl, agentVersion, options);
1387
817
  }
1388
818
  async observeNativeInspection(agent, terminalControl, request, options = {}) {
1389
- const adapter = this.registry.require(agent);
1390
- if (!adapter.observeNativeInspection) {
1391
- throw new Error(`${adapter.displayName} native inspection observation is not supported`);
1392
- }
1393
- const captured = await this.captureInspection(adapter, terminalControl, {
1394
- runtime: options.runtime,
1395
- scrollbackLines: options.scrollbackLines
1396
- });
1397
- const screenDigest = nativeInspectionScreenFingerprint(captured.screen);
1398
- const adapterObservation = adapter.observeNativeInspection({
1399
- operation: request.operation,
1400
- previousScreenFingerprint: request.previousScreenFingerprint,
1401
- preEnterEvidenceInventory: request.preEnterEvidenceInventory,
1402
- expectedNativeThreadId: request.expectedNativeThreadId,
1403
- expectedAgentVersion: request.expectedAgentVersion,
1404
- expectedCwd: request.expectedCwd,
1405
- screen: captured.screen
1406
- });
1407
- const observation = adapter.agent === "claude" &&
1408
- options.runtime?.requireExactClaudeAgentRow === true &&
1409
- adapterObservation.status === "observed" &&
1410
- adapterObservation.evidence === "claude_status_panel"
1411
- ? {
1412
- ...adapterObservation,
1413
- evidence: "claude_status_panel+claude_agents_exact_pid"
1414
- }
1415
- : adapterObservation;
1416
- return {
1417
- terminalControl: captured.terminalControl,
1418
- status: statusFromInspection(adapter, captured.terminalControl, captured.inspection, { screen: captured.screen, runtime: options.runtime }),
1419
- screenDigest,
1420
- observation
1421
- };
819
+ return this.nativeInspectionBridge().observeNativeInspection(agent, terminalControl, request, options);
1422
820
  }
1423
821
  /**
1424
822
  * Dismiss one exact adapter-owned modal result after re-observing the same
@@ -1426,507 +824,7 @@ export class TerminalAgentBridge {
1426
824
  * attempt and no automated retry across an uncertain dismissal boundary.
1427
825
  */
1428
826
  async dismissNativeInspection(agent, terminalControl, plan, request, expectedEvidenceFingerprint, options = {}) {
1429
- const adapter = this.registry.require(agent);
1430
- try {
1431
- assertClosedStatusInspectionPlan(adapter, terminalControl, plan);
1432
- assertClosedNativeInspectionDismissal(plan);
1433
- }
1434
- catch (error) {
1435
- throw new NativeInspectionDismissalError(error instanceof Error ? error.message : String(error), { cause: error });
1436
- }
1437
- const observeExactPanel = async (control) => {
1438
- const captured = await this.captureInspection(adapter, control, {
1439
- runtime: options.runtime,
1440
- scrollbackLines: options.scrollbackLines
1441
- });
1442
- const observation = adapter.observeNativeInspection?.({
1443
- operation: request.operation,
1444
- previousScreenFingerprint: request.previousScreenFingerprint,
1445
- preEnterEvidenceInventory: request.preEnterEvidenceInventory,
1446
- expectedNativeThreadId: request.expectedNativeThreadId,
1447
- expectedAgentVersion: request.expectedAgentVersion,
1448
- expectedCwd: request.expectedCwd,
1449
- screen: captured.screen
1450
- });
1451
- if (!observation ||
1452
- observation.status !== "observed" ||
1453
- observation.result?.kind !== "native_status" ||
1454
- observation.evidenceFingerprint !== expectedEvidenceFingerprint) {
1455
- throw new Error(observation?.reason ??
1456
- "the exact fresh native status panel changed before dismissal");
1457
- }
1458
- const verified = await this.verifyTerminalIdentity(adapter.agent, captured.terminalControl, options.runtime);
1459
- if (!sameTerminalControlIdentity(captured.terminalControl, verified)) {
1460
- throw new Error("terminal control identity changed before native status dismissal");
1461
- }
1462
- return verified;
1463
- };
1464
- try {
1465
- let verified = await observeExactPanel(terminalControl);
1466
- await options.beforeDismiss?.({
1467
- agent: adapter.agent,
1468
- terminalControl: verified,
1469
- plan,
1470
- evidenceFingerprint: expectedEvidenceFingerprint
1471
- });
1472
- verified = await observeExactPanel(verified);
1473
- try {
1474
- await this.terminalProvider.sendKeys(this.terminalProvider.endpoint(verified), plan.expectedResult.dismissal.keys);
1475
- }
1476
- catch (error) {
1477
- throw new NativeInspectionDismissalError("native status modal dismissal outcome is uncertain; do not retry automatically", { cause: error });
1478
- }
1479
- return {
1480
- stage: "dismiss_dispatched",
1481
- agent: adapter.agent,
1482
- terminalControl: verified,
1483
- keys: plan.expectedResult.dismissal.keys,
1484
- dismissCount: 1
1485
- };
1486
- }
1487
- catch (error) {
1488
- if (error instanceof NativeInspectionDismissalError) {
1489
- throw error;
1490
- }
1491
- throw new NativeInspectionDismissalError(error instanceof Error ? error.message : String(error), { cause: error });
1492
- }
1493
- }
1494
- async captureCodexReadyComposer(adapter, terminalControl, plan, runtime) {
1495
- if (adapter.agent !== "codex") {
1496
- throw new NativeInspectionDiagnosticError("unsupported_profile", "the closed Codex /status probe requires the Codex adapter");
1497
- }
1498
- const minimumViewport = CODEX_NATIVE_STATUS_MIN_VIEWPORT_BY_PROFILE[plan.behaviorProfile];
1499
- if (minimumViewport === undefined) {
1500
- throw new NativeInspectionDiagnosticError("unsupported_profile", `Codex ${plan.behaviorProfile} has no exact /status viewport profile`);
1501
- }
1502
- const captureReady = async (control) => {
1503
- const verified = await this.verifyTerminalIdentity(adapter.agent, control, runtime);
1504
- const endpoint = this.terminalProvider.endpoint(verified);
1505
- const viewportInspector = this.terminalProvider.inspectViewport;
1506
- let exactViewport;
1507
- let viewportUnavailableReason = "terminal provider has no exact viewport inspector";
1508
- if (viewportInspector) {
1509
- let viewport;
1510
- try {
1511
- viewport = await viewportInspector.call(this.terminalProvider, endpoint);
1512
- }
1513
- catch (error) {
1514
- throw new NativeInspectionDiagnosticError("viewport_unavailable", `Codex /status viewport inspection failed before terminal input: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
1515
- }
1516
- if (viewport) {
1517
- if (!Number.isSafeInteger(viewport.columns) ||
1518
- viewport.columns <= 0 ||
1519
- !Number.isSafeInteger(viewport.rows) ||
1520
- viewport.rows <= 0) {
1521
- throw new NativeInspectionDiagnosticError("viewport_unavailable", "Codex /status viewport inspector returned invalid geometry");
1522
- }
1523
- exactViewport = viewport.columns;
1524
- }
1525
- else {
1526
- viewportUnavailableReason =
1527
- "terminal provider could not prove exact viewport geometry";
1528
- }
1529
- }
1530
- const styledScreen = await this.terminalProvider.capture(endpoint, { scrollbackLines: 40, preserveEscapes: true });
1531
- const plainScreen = stripTerminalEscapeSequences(styledScreen);
1532
- const inspection = adapter.inspectScreen({ screen: plainScreen, runtime });
1533
- assertNativeInspectionComposerSafe(inspection, adapter.displayName);
1534
- const composer = exactCodexReadyStyledComposerCapture(styledScreen);
1535
- if (!composer) {
1536
- throw new NativeInspectionDiagnosticError("composer_not_ready", "Codex composer contains non-placeholder input or is not at the exact idle prompt");
1537
- }
1538
- const inferredViewport = inferCodexVisibleViewportColumns(styledScreen);
1539
- const observedViewport = exactViewport ?? inferredViewport;
1540
- if ((observedViewport !== undefined && observedViewport < minimumViewport) ||
1541
- hasTruncatedCodexStatusSessionLine(plainScreen)) {
1542
- throw new NativeInspectionDiagnosticError("viewport_too_narrow", `Codex /status requires a proven viewport of at least ` +
1543
- `${minimumViewport} columns to preserve the complete Session UUID` +
1544
- `${observedViewport === undefined
1545
- ? ""
1546
- : `; observed ${observedViewport}`}; widen or zoom the pane before retrying`);
1547
- }
1548
- if (exactViewport === undefined) {
1549
- throw new NativeInspectionDiagnosticError("viewport_unavailable", `Codex /status requires exact terminal viewport geometry before input; ` +
1550
- viewportUnavailableReason +
1551
- `${inferredViewport === undefined
1552
- ? ""
1553
- : ` (ANSI fallback estimated ${inferredViewport} columns)`}`);
1554
- }
1555
- const reverified = await this.verifyTerminalIdentity(adapter.agent, verified, runtime);
1556
- if (!sameTerminalControlIdentity(verified, reverified)) {
1557
- throw new NativeInspectionDiagnosticError("identity_unverified", "terminal control identity changed after the Codex pre-text composer capture");
1558
- }
1559
- return {
1560
- terminalControl: reverified,
1561
- screenDigest: nativeInspectionScreenFingerprint(styledScreen),
1562
- composerDigest: composer.digest
1563
- };
1564
- };
1565
- const first = await captureReady(terminalControl);
1566
- const second = await captureReady(first.terminalControl);
1567
- if (second.composerDigest !== first.composerDigest) {
1568
- throw new NativeInspectionDiagnosticError("composer_not_ready", "Codex empty composer changed across its stable pre-text captures");
1569
- }
1570
- return {
1571
- terminalControl: second.terminalControl,
1572
- screenDigest: second.screenDigest
1573
- };
1574
- }
1575
- async assertFinalCodexStatusViewport(terminalControl, plan, runtime) {
1576
- const minimumViewport = CODEX_NATIVE_STATUS_MIN_VIEWPORT_BY_PROFILE[plan.behaviorProfile];
1577
- if (minimumViewport === undefined) {
1578
- throw new NativeInspectionDiagnosticError("unsupported_profile", `Codex ${plan.behaviorProfile} has no exact /status viewport profile`);
1579
- }
1580
- const verified = await this.verifyTerminalIdentity("codex", terminalControl, runtime);
1581
- const viewportInspector = this.terminalProvider.inspectViewport;
1582
- if (!viewportInspector) {
1583
- throw new NativeInspectionDiagnosticError("viewport_unavailable", "Codex /status requires exact terminal viewport geometry immediately before Enter");
1584
- }
1585
- let viewport;
1586
- try {
1587
- viewport = await viewportInspector.call(this.terminalProvider, this.terminalProvider.endpoint(verified));
1588
- }
1589
- catch (error) {
1590
- throw new NativeInspectionDiagnosticError("viewport_unavailable", `Codex /status viewport inspection failed immediately before Enter: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
1591
- }
1592
- if (!viewport ||
1593
- !Number.isSafeInteger(viewport.columns) ||
1594
- viewport.columns <= 0 ||
1595
- !Number.isSafeInteger(viewport.rows) ||
1596
- viewport.rows <= 0) {
1597
- throw new NativeInspectionDiagnosticError("viewport_unavailable", "Codex /status exact terminal viewport became unavailable immediately before Enter");
1598
- }
1599
- if (viewport.columns < minimumViewport) {
1600
- throw new NativeInspectionDiagnosticError("viewport_too_narrow", `Codex /status viewport narrowed to ${viewport.columns} columns before Enter; ` +
1601
- `at least ${minimumViewport} are required to preserve the complete Session UUID`);
1602
- }
1603
- const reverified = await this.verifyTerminalIdentity("codex", verified, runtime);
1604
- if (!sameTerminalControlIdentity(verified, reverified)) {
1605
- throw new NativeInspectionDiagnosticError("identity_unverified", "terminal control identity changed after the final Codex viewport proof");
1606
- }
1607
- return reverified;
1608
- }
1609
- async settleNativeInspectionComposer(adapter, terminalControl, plan, runtime) {
1610
- const startedAt = this.nowMs();
1611
- const settleTimeoutMs = plan.composer.maximumSettleMs;
1612
- let stableDigest;
1613
- let stableKind;
1614
- let stableSince;
1615
- let stableCaptures = 0;
1616
- let capturesBeyondDeadline = 0;
1617
- let lastMismatchDiagnostic = "composer_not_exact";
1618
- while (true) {
1619
- const captured = await this.captureInspection(adapter, terminalControl, {
1620
- runtime,
1621
- scrollbackLines: CODEX_MULTILINE_SETTLE_SCROLLBACK_LINES
1622
- });
1623
- assertNativeInspectionComposerSafe(captured.inspection);
1624
- const materialized = exactNativeInspectionComposerCapture(adapter.agent, captured.screen, plan);
1625
- const now = this.nowMs();
1626
- if (materialized) {
1627
- if (materialized.digest === stableDigest &&
1628
- materialized.kind === stableKind) {
1629
- stableCaptures += 1;
1630
- }
1631
- else {
1632
- stableDigest = materialized.digest;
1633
- stableKind = materialized.kind;
1634
- stableSince = now;
1635
- stableCaptures = 1;
1636
- }
1637
- const stableForMs = stableSince === undefined ? 0 : now - stableSince;
1638
- if (stableCaptures >= CODEX_MULTILINE_STABLE_CAPTURES &&
1639
- stableForMs >= plan.composer.minimumStableMs) {
1640
- const evidence = {
1641
- kind: materialized.kind,
1642
- digest: materialized.digest,
1643
- stableForMs,
1644
- stableCaptures
1645
- };
1646
- return this.revalidateNativeInspectionComposer(adapter, captured.terminalControl, plan, evidence, runtime);
1647
- }
1648
- }
1649
- else {
1650
- lastMismatchDiagnostic = adapter.agent === "codex"
1651
- ? codexNativeInspectionComposerMismatchDiagnostic(captured.screen, plan)
1652
- : "composer_not_exact";
1653
- stableDigest = undefined;
1654
- stableKind = undefined;
1655
- stableSince = undefined;
1656
- stableCaptures = 0;
1657
- }
1658
- const elapsed = this.nowMs() - startedAt;
1659
- const remaining = settleTimeoutMs - elapsed;
1660
- if (remaining <= 0 && !materialized) {
1661
- break;
1662
- }
1663
- if (remaining <= 0) {
1664
- capturesBeyondDeadline += 1;
1665
- if (capturesBeyondDeadline > CODEX_EXACT_CANDIDATE_GRACE_CAPTURES) {
1666
- break;
1667
- }
1668
- }
1669
- const remainingStableMs = plan.composer.minimumStableMs -
1670
- (stableSince === undefined ? 0 : this.nowMs() - stableSince);
1671
- await this.sleep(Math.min(CODEX_MULTILINE_SETTLE_POLL_MS, remaining > 0
1672
- ? remaining
1673
- : Math.max(1, remainingStableMs)));
1674
- }
1675
- throw new NativeInspectionDiagnosticError(lastMismatchDiagnostic, lastMismatchDiagnostic === "composer_viewport_truncated"
1676
- ? `${adapter.displayName} /status slash popup was truncated by the viewport; widen or zoom the pane before retrying manually`
1677
- : `${adapter.displayName} /status composer did not become exact, idle, and stable before the bounded submit deadline`);
1678
- }
1679
- async revalidateNativeInspectionComposer(adapter, terminalControl, plan, expected, runtime) {
1680
- const captured = await this.captureInspection(adapter, terminalControl, {
1681
- runtime,
1682
- scrollbackLines: CODEX_MULTILINE_SETTLE_SCROLLBACK_LINES
1683
- });
1684
- assertNativeInspectionComposerSafe(captured.inspection);
1685
- const materialized = exactNativeInspectionComposerCapture(adapter.agent, captured.screen, plan);
1686
- if (!materialized ||
1687
- materialized.digest !== expected.digest ||
1688
- materialized.kind !== expected.kind) {
1689
- throw new NativeInspectionDiagnosticError("composer_drift", `${adapter.displayName} /status composer changed after its stable pre-submit capture`);
1690
- }
1691
- const baseline = adapter.observeNativeInspection?.({
1692
- operation: plan.operation,
1693
- screen: captured.screen
1694
- });
1695
- if (!baseline ||
1696
- baseline.status === "ambiguous" ||
1697
- !Array.isArray(baseline.evidenceInventory)) {
1698
- throw new NativeInspectionDiagnosticError("evidence_unproven", baseline?.reason ??
1699
- `${adapter.displayName} /status pre-Enter evidence inventory was not proven`);
1700
- }
1701
- const verifiedImmediatelyBeforeEnter = await this.verifyTerminalIdentity(adapter.agent, captured.terminalControl, runtime);
1702
- if (!sameTerminalControlIdentity(captured.terminalControl, verifiedImmediatelyBeforeEnter)) {
1703
- throw new NativeInspectionDiagnosticError("identity_unverified", `terminal control identity changed after the final ${adapter.displayName} /status composer capture`);
1704
- }
1705
- return {
1706
- terminalControl: verifiedImmediatelyBeforeEnter,
1707
- screenDigest: nativeInspectionScreenFingerprint(captured.screen),
1708
- evidenceInventory: baseline.evidenceInventory,
1709
- materialization: expected
1710
- };
1711
- }
1712
- async settleCodexMultilineComposer(adapter, terminalControl, expectedText, runtime, options = {}) {
1713
- const observation = await this.settleCodexComposerObservation(adapter, terminalControl, expectedText, runtime, {
1714
- minimumStableMs: CODEX_PASTE_ENTER_SETTLE_MS,
1715
- requiredState: "exact_draft",
1716
- allowOpaqueLargePastePlaceholder: options.allowOpaqueLargePastePlaceholder === true,
1717
- allowWorkingComposer: options.allowWorkingComposer === true
1718
- });
1719
- if (observation.state === "exact_draft") {
1720
- return observation.terminalControl;
1721
- }
1722
- if (observation.state === "working" ||
1723
- observation.state === "approval_or_modal") {
1724
- throw new Error("Codex became busy or blocked while its multiline composer was settling");
1725
- }
1726
- if (observation.state === "identity_drift") {
1727
- throw new Error(observation.reason);
1728
- }
1729
- throw new Error("Codex multiline composer did not become exact, idle, and stable before the bounded submit deadline");
1730
- }
1731
- async settleCodexComposerObservation(adapter, terminalControl, expectedText, runtime, options) {
1732
- const startedAt = this.nowMs();
1733
- let candidate;
1734
- let stableSince;
1735
- let stableCaptures = 0;
1736
- let finishObservedExactCandidate = false;
1737
- let capturesBeyondDeadline = 0;
1738
- let control = terminalControl;
1739
- while (true) {
1740
- const snapshot = await this.captureCodexComposerSnapshot(adapter, control, expectedText, runtime, {
1741
- allowOpaqueLargePastePlaceholder: options.allowOpaqueLargePastePlaceholder === true,
1742
- classifyOpaqueLargePasteAsDifferent: options.classifyOpaqueLargePasteAsDifferent === true,
1743
- allowWorkingComposer: options.allowWorkingComposer === true
1744
- });
1745
- if (snapshot.state === "identity_drift" ||
1746
- (snapshot.state === "unavailable" && snapshot.retryable !== true)) {
1747
- return snapshot;
1748
- }
1749
- if (this.nowMs() - startedAt > CODEX_MULTILINE_SETTLE_TIMEOUT_MS) {
1750
- capturesBeyondDeadline += 1;
1751
- if (capturesBeyondDeadline > CODEX_EXACT_CANDIDATE_GRACE_CAPTURES) {
1752
- return {
1753
- state: "unavailable",
1754
- reason: "Codex exact composer did not stabilize within its bounded capture grace"
1755
- };
1756
- }
1757
- }
1758
- control = snapshot.terminalControl;
1759
- const sameCandidate = candidate !== undefined &&
1760
- candidate.state === snapshot.state &&
1761
- candidate.digest === snapshot.digest &&
1762
- sameTerminalControlIdentity(candidate.terminalControl, snapshot.terminalControl);
1763
- if (sameCandidate) {
1764
- stableCaptures += 1;
1765
- }
1766
- else {
1767
- candidate = snapshot;
1768
- stableSince = this.nowMs();
1769
- stableCaptures = 1;
1770
- finishObservedExactCandidate = snapshot.state === "exact_draft";
1771
- }
1772
- const stableForMs = stableSince === undefined
1773
- ? 0
1774
- : this.nowMs() - stableSince;
1775
- if (stableCaptures >= CODEX_MULTILINE_STABLE_CAPTURES &&
1776
- stableForMs >= options.minimumStableMs &&
1777
- snapshot.state !== "unavailable" &&
1778
- (options.requiredState === undefined ||
1779
- snapshot.state === options.requiredState ||
1780
- snapshot.state === "working" ||
1781
- snapshot.state === "approval_or_modal")) {
1782
- // The caller's authorization hook must see evidence from a final,
1783
- // independently identity-fenced recapture, not the polling capture.
1784
- const finalSnapshot = await this.captureCodexComposerSnapshot(adapter, snapshot.terminalControl, expectedText, runtime, {
1785
- allowOpaqueLargePastePlaceholder: options.allowOpaqueLargePastePlaceholder === true,
1786
- classifyOpaqueLargePasteAsDifferent: options.classifyOpaqueLargePasteAsDifferent === true,
1787
- allowWorkingComposer: options.allowWorkingComposer === true
1788
- });
1789
- if (finalSnapshot.state === snapshot.state &&
1790
- "digest" in finalSnapshot &&
1791
- finalSnapshot.digest === snapshot.digest &&
1792
- sameTerminalControlIdentity(snapshot.terminalControl, finalSnapshot.terminalControl)) {
1793
- return {
1794
- ...finalSnapshot,
1795
- stableCaptures: stableCaptures + 1
1796
- };
1797
- }
1798
- if (finalSnapshot.state === "identity_drift" ||
1799
- (finalSnapshot.state === "unavailable" &&
1800
- finalSnapshot.retryable !== true)) {
1801
- return finalSnapshot;
1802
- }
1803
- if (options.requiredState !== undefined &&
1804
- snapshot.state === options.requiredState) {
1805
- return {
1806
- state: "unavailable",
1807
- reason: "Codex composer changed after its stable final recapture"
1808
- };
1809
- }
1810
- candidate = finalSnapshot;
1811
- control = finalSnapshot.terminalControl;
1812
- stableSince = this.nowMs();
1813
- stableCaptures = 1;
1814
- finishObservedExactCandidate = finalSnapshot.state === "exact_draft";
1815
- }
1816
- const elapsed = this.nowMs() - startedAt;
1817
- if (elapsed > CODEX_MULTILINE_SETTLE_TIMEOUT_MS &&
1818
- !finishObservedExactCandidate) {
1819
- return {
1820
- state: "unavailable",
1821
- reason: options.requiredState
1822
- ? `Codex composer did not become ${options.requiredState} before the bounded observation deadline`
1823
- : "Codex composer did not become stable before the bounded observation deadline"
1824
- };
1825
- }
1826
- if (elapsed > CODEX_MULTILINE_SETTLE_TIMEOUT_MS &&
1827
- options.requiredState !== undefined &&
1828
- snapshot.state !== options.requiredState) {
1829
- if (snapshot.state !== "unavailable") {
1830
- return {
1831
- ...snapshot,
1832
- stableCaptures
1833
- };
1834
- }
1835
- return {
1836
- state: "unavailable",
1837
- reason: snapshot.reason
1838
- };
1839
- }
1840
- const remainingStableMs = options.minimumStableMs - stableForMs;
1841
- await this.sleep(Math.max(1, Math.min(CODEX_MULTILINE_SETTLE_POLL_MS, remainingStableMs > 0
1842
- ? remainingStableMs
1843
- : CODEX_MULTILINE_SETTLE_POLL_MS)));
1844
- }
1845
- }
1846
- async captureCodexComposerSnapshot(adapter, terminalControl, expectedText, runtime, options = {}) {
1847
- let verifiedBefore;
1848
- try {
1849
- verifiedBefore = await this.verifyTerminalIdentity(adapter.agent, terminalControl, runtime);
1850
- }
1851
- catch (error) {
1852
- return {
1853
- state: "identity_drift",
1854
- reason: error instanceof Error ? error.message : String(error)
1855
- };
1856
- }
1857
- let styledScreen;
1858
- try {
1859
- styledScreen = await this.terminalProvider.capture(this.terminalProvider.endpoint(verifiedBefore), {
1860
- scrollbackLines: CODEX_MULTILINE_SETTLE_SCROLLBACK_LINES,
1861
- preserveEscapes: true
1862
- });
1863
- }
1864
- catch (error) {
1865
- return {
1866
- state: "unavailable",
1867
- reason: error instanceof Error ? error.message : String(error)
1868
- };
1869
- }
1870
- const screen = stripTerminalEscapeSequences(styledScreen);
1871
- let inspection;
1872
- try {
1873
- inspection = adapter.inspectScreen({ screen, runtime });
1874
- }
1875
- catch (error) {
1876
- return {
1877
- state: "unavailable",
1878
- reason: error instanceof Error ? error.message : String(error)
1879
- };
1880
- }
1881
- let verifiedAfter;
1882
- try {
1883
- verifiedAfter = await this.verifyTerminalIdentity(adapter.agent, verifiedBefore, runtime);
1884
- }
1885
- catch (error) {
1886
- return {
1887
- state: "identity_drift",
1888
- reason: error instanceof Error ? error.message : String(error)
1889
- };
1890
- }
1891
- if (!sameTerminalControlIdentity(verifiedBefore, verifiedAfter)) {
1892
- return {
1893
- state: "identity_drift",
1894
- reason: "terminal control identity changed across the Codex composer capture"
1895
- };
1896
- }
1897
- const screenDigest = createHash("sha256").update(styledScreen).digest("hex");
1898
- if (inspection.approval.blocked ||
1899
- inspection.activity.state === "awaiting_approval" ||
1900
- codexBlockingModalVisible(screen)) {
1901
- return {
1902
- state: "approval_or_modal",
1903
- terminalControl: verifiedAfter,
1904
- digest: screenDigest
1905
- };
1906
- }
1907
- if (inspection.activity.state === "working" &&
1908
- options.allowWorkingComposer !== true) {
1909
- return {
1910
- state: "working",
1911
- terminalControl: verifiedAfter,
1912
- digest: screenDigest
1913
- };
1914
- }
1915
- const composer = currentCodexComposerCapture(styledScreen, expectedText, options.allowOpaqueLargePastePlaceholder === true, options.classifyOpaqueLargePasteAsDifferent === true);
1916
- if (!composer) {
1917
- return {
1918
- state: "unavailable",
1919
- reason: "the current live Codex composer could not be proven",
1920
- retryable: true,
1921
- terminalControl: verifiedAfter,
1922
- digest: screenDigest
1923
- };
1924
- }
1925
- return {
1926
- state: composer.state,
1927
- terminalControl: verifiedAfter,
1928
- digest: composer.digest
1929
- };
827
+ return this.nativeInspectionBridge().dismissNativeInspection(agent, terminalControl, plan, request, expectedEvidenceFingerprint, options);
1930
828
  }
1931
829
  /**
1932
830
  * Prove that the exact managed draft is still present after Enter dispatch.
@@ -2276,114 +1174,7 @@ export class TerminalAgentBridge {
2276
1174
  };
2277
1175
  }
2278
1176
  async respondInteraction(agent, terminalControl, response, options) {
2279
- const runtime = {
2280
- ...options.runtime,
2281
- agentVersion: options.agentVersion
2282
- };
2283
- const optionReason = terminalInteractionOptionPreflightReason(options, runtime);
2284
- if (optionReason) {
2285
- return blockedTerminalInteractionResponse(response, optionReason);
2286
- }
2287
- const first = await this.captureTerminalInteractionOffer(agent, terminalControl, runtime, options.scrollbackLines).catch((error) => error);
2288
- if (first instanceof Error) {
2289
- return blockedTerminalInteractionResponse(response, first.message);
2290
- }
2291
- const firstReason = terminalInteractionOfferPreflightReason(first.offer, options, this.now());
2292
- if (firstReason || !first.offer) {
2293
- return blockedTerminalInteractionResponse(response, firstReason ?? "native questionnaire has no safe interaction offer", first.offer);
2294
- }
2295
- const validatedResponse = validateAnyTerminalInteractionResponse(response, first.offer.projection, {
2296
- now: this.now(),
2297
- // The capture immediately above is the authoritative freshness proof.
2298
- // A wall-clock bucket boundary must not override exact live terminal
2299
- // evidence before any input has been sent.
2300
- allowExpiredForLiveRecapture: true
2301
- });
2302
- const planReason = terminalInteractionPlanPreflightReason(validatedResponse, first.offer.actionPlan);
2303
- if (planReason) {
2304
- return blockedTerminalInteractionResponse(validatedResponse, planReason, first.offer);
2305
- }
2306
- const capabilityReason = terminalInteractionCapabilityReason(this.terminalProvider, first.terminalControl, first.offer.actionPlan);
2307
- if (capabilityReason) {
2308
- return blockedTerminalInteractionResponse(validatedResponse, capabilityReason, first.offer);
2309
- }
2310
- const authorization = await authorizeTerminalInteraction(options.authorize, interactionHookContext(agent, first.terminalControl, first.offer, validatedResponse, runtime));
2311
- if (!authorization.approved) {
2312
- return blockedTerminalInteractionResponse(validatedResponse, authorization.reason ?? "terminal interaction response was not authorized", first.offer);
2313
- }
2314
- const afterAuthorization = await this.captureTerminalInteractionOffer(agent, first.terminalControl, runtime, options.scrollbackLines).catch((error) => error);
2315
- if (afterAuthorization instanceof Error) {
2316
- return blockedTerminalInteractionResponse(validatedResponse, afterAuthorization.message, first.offer);
2317
- }
2318
- const afterAuthorizationReason = terminalInteractionOfferPreflightReason(afterAuthorization.offer, options, this.now());
2319
- if (afterAuthorizationReason) {
2320
- return blockedTerminalInteractionResponse(validatedResponse, afterAuthorizationReason, first.offer);
2321
- }
2322
- if (!sameTerminalInteractionOffer(first, afterAuthorization, {
2323
- // expires_at is generated from AKK wall-clock freshness buckets, not
2324
- // from the native prompt. All semantic projection and action-plan fields
2325
- // still have to match exactly.
2326
- ignoreExpiresAt: true
2327
- })) {
2328
- return blockedTerminalInteractionResponse(validatedResponse, "native questionnaire changed after authorization", first.offer);
2329
- }
2330
- await reserveTerminalInteractionDispatch(options.beforeDispatch, interactionHookContext(agent, afterAuthorization.terminalControl, afterAuthorization.offer, validatedResponse, runtime));
2331
- const finalCapture = await this.captureTerminalInteractionOffer(agent, afterAuthorization.terminalControl, runtime, options.scrollbackLines).catch((error) => {
2332
- if (options.beforeDispatch) {
2333
- throw terminalInteractionInputNotStartedError(error, "cannot recapture native questionnaire after dispatch reservation");
2334
- }
2335
- return error;
2336
- });
2337
- if (finalCapture instanceof Error) {
2338
- return blockedTerminalInteractionResponse(validatedResponse, finalCapture.message, first.offer);
2339
- }
2340
- if (!sameTerminalInteractionOffer(afterAuthorization, finalCapture, {
2341
- // The projection expiry is bucketed and may roll over during this final,
2342
- // bounded recapture even though the questionnaire is exact. Prompt,
2343
- // action plan, terminal identity, and owner remain exact-match fences.
2344
- ignoreExpiresAt: true
2345
- })) {
2346
- if (options.beforeDispatch) {
2347
- throw new TerminalInteractionInputNotStartedError("native questionnaire changed after dispatch reservation");
2348
- }
2349
- return blockedTerminalInteractionResponse(validatedResponse, "native questionnaire changed before dispatch", first.offer);
2350
- }
2351
- const verified = await this.verifyTerminalIdentity(agent, finalCapture.terminalControl, runtime).catch((error) => {
2352
- if (options.beforeDispatch) {
2353
- throw terminalInteractionInputNotStartedError(error, "terminal identity could not be verified immediately before interaction dispatch");
2354
- }
2355
- throw terminalInteractionReservedError("key_uncertain", error, "terminal identity could not be verified immediately before interaction dispatch");
2356
- });
2357
- if (!sameTerminalControlIdentity(finalCapture.terminalControl, verified)) {
2358
- if (options.beforeDispatch) {
2359
- throw new TerminalInteractionInputNotStartedError("terminal identity changed after the final questionnaire capture");
2360
- }
2361
- throw new TerminalInteractionDispatchReservedError("key_uncertain", "terminal identity changed after the final questionnaire capture");
2362
- }
2363
- const answer = validatedResponse.answers[0];
2364
- const outcome = await dispatchTerminalInteractionAnswer({
2365
- agent,
2366
- provider: this.terminalProvider,
2367
- terminalControl: verified,
2368
- actionPlan: finalCapture.offer.actionPlan,
2369
- answer,
2370
- sleep: this.sleep,
2371
- reverifyTerminalIdentity: async (currentControl) => {
2372
- const reverified = await this.verifyTerminalIdentity(agent, currentControl, runtime);
2373
- if (!sameTerminalControlIdentity(currentControl, reverified)) {
2374
- throw new Error("terminal identity changed between interaction text and Enter");
2375
- }
2376
- return reverified;
2377
- }
2378
- });
2379
- return {
2380
- responded: true,
2381
- blocked: false,
2382
- interactionId: validatedResponse.interaction_id,
2383
- questionId: answer.question_id,
2384
- responseKind: answer.response_kind,
2385
- outcome
2386
- };
1177
+ return this.interactionResponseBridge().respond(agent, terminalControl, response, options);
2387
1178
  }
2388
1179
  async monitorPoll(options) {
2389
1180
  const adapter = this.registry.require(options.agent);
@@ -2455,25 +1246,6 @@ export class TerminalAgentBridge {
2455
1246
  })
2456
1247
  };
2457
1248
  }
2458
- async captureTerminalInteractionOffer(agent, terminalControl, runtime, scrollbackLines) {
2459
- const adapter = this.registry.require(agent);
2460
- const captured = await this.captureInspection(adapter, terminalControl, {
2461
- scrollbackLines,
2462
- runtime
2463
- });
2464
- return {
2465
- terminalControl: captured.terminalControl,
2466
- inspection: captured.inspection,
2467
- offer: captureTerminalInteractionRuntimeOffer({
2468
- agent,
2469
- terminalControl: captured.terminalControl,
2470
- screen: captured.screen,
2471
- runtime,
2472
- now: this.now(),
2473
- approvalBlocked: captured.inspection.approval.blocked
2474
- })
2475
- };
2476
- }
2477
1249
  async verifyTerminalIdentity(agent, terminalControl, runtime) {
2478
1250
  let verifiedControl = terminalControl;
2479
1251
  if (this.verifyIdentity) {
@@ -2500,938 +1272,27 @@ export class TerminalAgentBridge {
2500
1272
  return this.terminalProvider.toControlRef(resolvedEndpoint, verifiedControl.capabilities);
2501
1273
  }
2502
1274
  }
2503
- function assertTerminalMutationCapabilities({ provider, terminal, semantic, transport }) {
2504
- const missingSemantic = semantic.filter((capability) => !provider.supportedCapabilities.includes(capability) ||
2505
- !terminal.capabilities.includes(capability));
2506
- const missingTransport = transport.filter((capability) => !provider.providerCapabilities.includes(capability));
2507
- if (missingSemantic.length === 0 && missingTransport.length === 0) {
2508
- return;
2509
- }
2510
- const missing = [
2511
- ...missingSemantic.map((value) => `terminal:${value}`),
2512
- ...missingTransport.map((value) => `provider:${value}`)
2513
- ];
2514
- throw new TerminalControlInputNotSentError(`terminal action capability preflight failed for ` +
2515
- `${terminal.identity.providerKind}:${terminal.route.label}: ` +
2516
- missing.join(", "));
2517
- }
2518
- function nativeInspectionSubmissionError(stage, error, fallbackDiagnostic) {
2519
- const diagnostic = error instanceof NativeInspectionSubmissionError
2520
- ? error.diagnostic
2521
- : error instanceof NativeInspectionDiagnosticError
2522
- ? error.diagnostic
2523
- : fallbackDiagnostic;
2524
- if (error instanceof NativeInspectionSubmissionError) {
2525
- const stageRank = {
2526
- not_started: 0,
2527
- text_injected: 1,
2528
- enter_uncertain: 2
2529
- };
2530
- if (stageRank[error.stage] >= stageRank[stage]) {
2531
- return error;
2532
- }
2533
- return new NativeInspectionSubmissionError(stage, error.message, {
2534
- cause: error,
2535
- diagnostic
2536
- });
2537
- }
2538
- return new NativeInspectionSubmissionError(stage, error instanceof Error ? error.message : String(error), { cause: error, diagnostic });
2539
- }
2540
- function assertClosedStatusInspectionPlan(adapter, terminalControl, plan) {
2541
- if (!terminalControl.capabilities.includes("send_keys")) {
2542
- throw new Error(`${adapter.displayName} terminal input is not supported`);
2543
- }
2544
- if (!terminalControl.capabilities.includes("screen_status")) {
2545
- throw new Error(`${adapter.displayName} terminal screen inspection is not supported`);
2546
- }
2547
- const codexProfile = adapter.agent === "codex" &&
2548
- CODEX_NATIVE_STATUS_POPUP_BY_PROFILE[plan.behaviorProfile] !== undefined;
2549
- const claudeProfile = adapter.agent === "claude" &&
2550
- CLAUDE_NATIVE_STATUS_POPUP_BY_PROFILE[plan.behaviorProfile] !== undefined;
2551
- const expectedSettle = codexProfile
2552
- ? { minimumStableMs: CODEX_PASTE_ENTER_SETTLE_MS, maximumSettleMs: 2_000 }
2553
- : CLAUDE_NATIVE_STATUS_SETTLE_BY_PROFILE[plan.behaviorProfile];
2554
- const exactPresentation = codexProfile
2555
- ? plan.expectedResult.presentation === "inline" &&
2556
- plan.expectedResult.dismissal === undefined &&
2557
- plan.composer.minimumStableMs === expectedSettle?.minimumStableMs &&
2558
- plan.composer.maximumSettleMs === expectedSettle.maximumSettleMs
2559
- : claudeProfile
2560
- ? plan.expectedResult.presentation === "modal" &&
2561
- plan.composer.minimumStableMs === expectedSettle?.minimumStableMs &&
2562
- plan.composer.maximumSettleMs === expectedSettle.maximumSettleMs &&
2563
- plan.expectedResult.dismissal?.expected === "idle_empty_composer" &&
2564
- JSON.stringify(plan.expectedResult.dismissal.keys) ===
2565
- JSON.stringify(["Escape"])
2566
- : false;
2567
- if (plan.operation.kind !== "status" ||
2568
- plan.command !== "/status" ||
2569
- plan.effect !== "read_only" ||
2570
- plan.requiresIdle !== true ||
2571
- plan.composer.kind !== "exact" ||
2572
- !Number.isFinite(plan.composer.minimumStableMs) ||
2573
- plan.composer.minimumStableMs < 0 ||
2574
- !Number.isFinite(plan.composer.maximumSettleMs) ||
2575
- plan.composer.maximumSettleMs < plan.composer.minimumStableMs ||
2576
- plan.expectedResult.kind !== "native_status" ||
2577
- !exactPresentation) {
2578
- throw new Error("refusing a non-closed native inspection plan");
2579
- }
2580
- }
2581
- function assertClosedNativeInspectionDismissal(plan) {
2582
- if (plan.expectedResult.presentation !== "modal" ||
2583
- plan.expectedResult.dismissal?.expected !== "idle_empty_composer" ||
2584
- JSON.stringify(plan.expectedResult.dismissal.keys) !==
2585
- JSON.stringify(["Escape"])) {
2586
- throw new Error("native inspection has no closed modal dismissal plan");
2587
- }
2588
- }
2589
- function assertNativeInspectionComposerSafe(inspection, displayName = "terminal agent") {
2590
- if (inspection.approval.blocked ||
2591
- inspection.activity.state === "awaiting_approval" ||
2592
- inspection.activity.state === "working") {
2593
- throw new NativeInspectionDiagnosticError("composer_not_ready", `${displayName} became busy or blocked while its /status composer was settling`);
2594
- }
2595
- // Codex's generic activity parser deliberately reports a non-empty slash
2596
- // composer as unknown. At this stage the caller has already proved an idle,
2597
- // empty styled composer under the terminal lock; the exact-current composer
2598
- // capture below is the stronger continuation proof after AKK injected only
2599
- // the adapter-owned /status command.
1275
+ async function terminalSettleDelay(milliseconds) {
1276
+ await new Promise((resolve) => setTimeout(resolve, milliseconds));
2600
1277
  }
2601
- function exactNativeInspectionComposerCapture(agent, screen, plan) {
2602
- return agent === "codex"
2603
- ? exactCodexNativeInspectionComposerCapture(screen, plan)
2604
- : exactClaudeNativeInspectionComposerCapture(screen, plan);
1278
+ function sameTerminalControlIdentity(left, right) {
1279
+ return sameTerminalControlIncarnation(left, right);
2605
1280
  }
2606
- function exactCodexNativeInspectionComposerCapture(screen, plan) {
2607
- const lines = screen.replace(/\r\n?/gu, "\n").split("\n");
2608
- let currentComposerIndex = -1;
2609
- for (let index = lines.length - 1; index >= 0; index -= 1) {
2610
- if (CODEX_COMPOSER_MARKER.test(lines[index])) {
2611
- currentComposerIndex = index;
2612
- break;
2613
- }
2614
- }
2615
- if (currentComposerIndex < 0) {
1281
+ export function terminalApprovalFingerprint(agent, terminalControl, inspection, options = {}) {
1282
+ if (!inspection.approval.approvable) {
2616
1283
  return undefined;
2617
1284
  }
2618
- const composerText = lines[currentComposerIndex]
2619
- .replace(/^[›»]\s?/u, "")
2620
- .trimEnd();
2621
- if (composerText !== plan.command) {
1285
+ const decision = options.decision ?? "approve_once";
1286
+ const choices = terminalApprovalChoices(inspection.approval);
1287
+ const action = terminalApprovalActionForDecision(inspection.approval, decision);
1288
+ if (!action) {
2622
1289
  return undefined;
2623
1290
  }
2624
- const footerIndex = lines.findIndex((line, candidateIndex) => candidateIndex > currentComposerIndex &&
2625
- CODEX_COMPOSER_FOOTER.test(line.trim()));
2626
- const region = lines.slice(currentComposerIndex, footerIndex < 0 ? lines.length : footerIndex);
2627
- while (region.length > 1 && region.at(-1)?.trim() === "") {
2628
- region.pop();
2629
- }
2630
- const popupRows = region.slice(1).filter((line) => line.trim().length > 0);
2631
- let kind;
2632
- if (popupRows.length === 0) {
2633
- kind = "exact_slash_composer";
2634
- }
2635
- else if (JSON.stringify(popupRows.map((line) => line.trimEnd())) ===
2636
- JSON.stringify(CODEX_NATIVE_STATUS_POPUP_BY_PROFILE[plan.behaviorProfile])) {
2637
- kind = "exact_slash_popup";
2638
- }
2639
- else {
2640
- return undefined;
2641
- }
2642
- return {
2643
- kind,
2644
- digest: createHash("sha256").update(region.join("\n")).digest("hex")
2645
- };
2646
- }
2647
- function exactClaudeNativeInspectionComposerCapture(screen, plan) {
2648
- const frame = exactClaudeComposerFrame(screen);
2649
- if (!frame) {
2650
- return undefined;
2651
- }
2652
- const { lines, openIndex, closeIndex, composerRows, trailing } = frame;
2653
- if (composerRows.length !== 1 ||
2654
- !/^\s*❯(?:\s|$)/u.test(composerRows[0]) ||
2655
- composerRows[0].replace(/^\s*❯\s?/u, "").trimEnd() !== plan.command) {
2656
- return undefined;
2657
- }
2658
- let kind;
2659
- if (trailing.length === 0 || claudeNativeInspectionTrailingIsFooter(trailing)) {
2660
- kind = "exact_slash_composer";
2661
- }
2662
- else {
2663
- const suggestions = [];
2664
- for (const line of trailing) {
2665
- const trimmed = line.trim();
2666
- if (trimmed.startsWith("/")) {
2667
- suggestions.push(trimmed.replace(/\s+/gu, " "));
2668
- }
2669
- else if (suggestions.length > 0) {
2670
- suggestions[suggestions.length - 1] +=
2671
- ` ${trimmed.replace(/\s+/gu, " ")}`;
2672
- }
2673
- else {
2674
- return undefined;
2675
- }
2676
- }
2677
- if (!closedClaudeNativeStatusSuggestionsMatch(suggestions, CLAUDE_NATIVE_STATUS_POPUP_BY_PROFILE[plan.behaviorProfile])) {
2678
- return undefined;
2679
- }
2680
- kind = "exact_slash_popup";
2681
- }
2682
- return {
2683
- kind,
2684
- digest: createHash("sha256")
2685
- .update(lines.slice(openIndex, closeIndex + 1).concat(trailing).join("\n"))
2686
- .digest("hex")
2687
- };
2688
- }
2689
- /**
2690
- * Claude truncates a suggestion row with a Unicode ellipsis at narrow pane
2691
- * widths. Keep authorization closed by accepting only an exact ordered row or
2692
- * an explicit ellipsis whose preceding text is an exact, non-trivial prefix of
2693
- * that same profiled row. A caller still cannot introduce, omit, or reorder a
2694
- * slash command.
2695
- */
2696
- function closedClaudeNativeStatusSuggestionsMatch(observed, expected) {
2697
- if (!expected || observed.length !== expected.length) {
2698
- return false;
2699
- }
2700
- return observed.every((row, index) => {
2701
- const exact = expected[index];
2702
- if (row === exact) {
2703
- return true;
2704
- }
2705
- if (!row.endsWith("…")) {
2706
- return false;
2707
- }
2708
- const prefix = row.slice(0, -1);
2709
- const commandEnd = exact.indexOf(" ");
2710
- return (commandEnd > 0 &&
2711
- prefix.length >= commandEnd + 12 &&
2712
- exact.startsWith(prefix));
2713
- });
2714
- }
2715
- /**
2716
- * Prove Claude Code's exact current idle input frame. This is shared by every
2717
- * automated-input path: a loose or historical `❯` prompt is not authority to
2718
- * inject text into the terminal.
2719
- */
2720
- export function isExactClaudeIdleComposer(screen) {
2721
- const frame = exactClaudeComposerFrame(screen);
2722
- if (!frame) {
2723
- return false;
2724
- }
2725
- return (frame.composerRows.length === 1 &&
2726
- /^\s*❯\s*$/u.test(frame.composerRows[0]) &&
2727
- (frame.trailing.length === 0 ||
2728
- claudeNativeInspectionTrailingIsFooter(frame.trailing)));
2729
- }
2730
- /**
2731
- * Compatibility export retained for callers that adopted the native-status
2732
- * name before the same exact-frame proof was reused by lifecycle handoff.
2733
- */
2734
- export function isExactClaudeNativeInspectionIdleComposer(screen) {
2735
- return isExactClaudeIdleComposer(screen);
2736
- }
2737
- function exactClaudeComposerFrame(screen) {
2738
- const lines = screen.replace(/\r\n?/gu, "\n").replace(/\u00a0/gu, " ")
2739
- .split("\n");
2740
- const dividerIndexes = lines
2741
- .map((line, index) => /^\s*[─━]{8,}\s*$/u.test(line) ? index : -1)
2742
- .filter((index) => index >= 0);
2743
- if (dividerIndexes.length < 2) {
2744
- return undefined;
2745
- }
2746
- const closeIndex = dividerIndexes.at(-1);
2747
- const openIndex = dividerIndexes.at(-2);
2748
- return {
2749
- lines,
2750
- openIndex,
2751
- closeIndex,
2752
- composerRows: lines.slice(openIndex + 1, closeIndex)
2753
- .filter((line) => line.trim().length > 0),
2754
- trailing: lines.slice(closeIndex + 1)
2755
- .filter((line) => line.trim().length > 0)
2756
- };
2757
- }
2758
- function claudeNativeInspectionTrailingIsFooter(lines) {
2759
- return lines.length <= 2 && lines.every((line) => /^\s*(?:[⏵⏴⏸]{1,2}|\?)\s*.*(?:manual mode|shift\+tab|accept edits|bypass permissions|for shortcuts|← for agents)/iu
2760
- .test(line));
2761
- }
2762
- function nativeInspectionScreenFingerprint(screen) {
2763
- return `sha256:${createHash("sha256").update(screen).digest("hex")}`;
2764
- }
2765
- function bareDigestFromNativeInspectionScreenFingerprint(fingerprint) {
2766
- const match = /^sha256:([0-9a-f]{64})$/u.exec(fingerprint);
2767
- if (!match) {
2768
- throw new Error("native inspection screen fingerprint is malformed");
2769
- }
2770
- return match[1];
2771
- }
2772
- function stripTerminalEscapeSequences(value) {
2773
- return value.replace(/\x1B(?:\[[0-?]*[ -/]*[@-~]|\][^\x07]*(?:\x07|\x1B\\))/gu, "");
2774
- }
2775
- export function exactCodexReadyStyledComposerCapture(screen) {
2776
- const lines = screen.replace(/\r\n?/gu, "\n").split("\n");
2777
- while (lines.length > 0 &&
2778
- stripTerminalEscapeSequences(lines.at(-1) ?? "").trim().length === 0) {
2779
- lines.pop();
2780
- }
2781
- const composerLine = [...lines.slice(-12)].reverse().find((line) => CODEX_COMPOSER_MARKER.test(stripTerminalEscapeSequences(line).trimEnd()));
2782
- if (composerLine === undefined) {
2783
- return undefined;
2784
- }
2785
- let dim = false;
2786
- const visible = [];
2787
- for (let index = 0; index < composerLine.length;) {
2788
- if (composerLine[index] === "\x1b") {
2789
- const escape = /^(?:\x1B\[([0-9;]*)m|\x1B\][^\x07]*(?:\x07|\x1B\\))/u
2790
- .exec(composerLine.slice(index));
2791
- if (escape) {
2792
- if (escape[1] !== undefined) {
2793
- const codes = escape[1] === ""
2794
- ? [0]
2795
- : escape[1].split(";").map((value) => Number(value));
2796
- for (let codeIndex = 0; codeIndex < codes.length; codeIndex += 1) {
2797
- const code = codes[codeIndex];
2798
- if ([38, 48, 58].includes(code) &&
2799
- codes[codeIndex + 1] === 2) {
2800
- codeIndex += 4;
2801
- continue;
2802
- }
2803
- if ([38, 48, 58].includes(code) &&
2804
- codes[codeIndex + 1] === 5) {
2805
- codeIndex += 2;
2806
- continue;
2807
- }
2808
- if (code === 0 || code === 22) {
2809
- dim = false;
2810
- }
2811
- else if (code === 2) {
2812
- dim = true;
2813
- }
2814
- }
2815
- }
2816
- index += escape[0].length;
2817
- continue;
2818
- }
2819
- }
2820
- const codePoint = composerLine.codePointAt(index);
2821
- if (codePoint === undefined) {
2822
- break;
2823
- }
2824
- const character = String.fromCodePoint(codePoint);
2825
- visible.push({ character, dim });
2826
- index += character.length;
2827
- }
2828
- const promptIndex = visible.findIndex(({ character }) => character === "›" || character === "»");
2829
- if (promptIndex < 0) {
2830
- return undefined;
2831
- }
2832
- const content = visible.slice(promptIndex + 1)
2833
- .filter(({ character }) => !/^\s$/u.test(character));
2834
- if (content.length > 0 && !content.every((entry) => entry.dim)) {
2835
- return undefined;
2836
- }
2837
- return {
2838
- digest: createHash("sha256").update(composerLine).digest("hex")
2839
- };
2840
- }
2841
- /**
2842
- * Infer a viewport only from fixed-width visible-buffer rows. Trimmed captures
2843
- * deliberately return undefined: a short content row is not proof of a short
2844
- * terminal. This keeps the fallback provider-neutral and fail-closed only on
2845
- * positive geometry evidence.
2846
- */
2847
- function inferCodexVisibleViewportColumns(screen) {
2848
- const rows = screen.replace(/\r\n?/gu, "\n").split("\n")
2849
- .map(stripTerminalEscapeSequences);
2850
- const widthOneRows = rows.filter((row) => /^[\x20-\x7e›»·─━╭╮╰╯│]*$/u.test(row));
2851
- const maxWidth = widthOneRows.reduce((maximum, row) => Math.max(maximum, Array.from(row).length), 0);
2852
- if (maxWidth < 20) {
2853
- return undefined;
2854
- }
2855
- const paddedAtMax = widthOneRows.filter((row) => row.endsWith(" ") && Array.from(row).length === maxWidth);
2856
- const composerAtMax = paddedAtMax.some((row) => CODEX_COMPOSER_MARKER.test(row.trimEnd()));
2857
- return paddedAtMax.length >= 3 && composerAtMax
2858
- ? maxWidth
2859
- : undefined;
2860
- }
2861
- function hasTruncatedCodexStatusSessionLine(screen) {
2862
- return screen.replace(/\r\n?/gu, "\n").split("\n").some((line) => {
2863
- const match = /^\s*│\s*Session:\s*([^│\s]+).*│?\s*$/iu.exec(line);
2864
- if (!match) {
2865
- return false;
2866
- }
2867
- return !/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu
2868
- .test(match[1]);
2869
- });
2870
- }
2871
- function codexNativeInspectionComposerMismatchDiagnostic(screen, plan) {
2872
- const expectedRows = CODEX_NATIVE_STATUS_POPUP_BY_PROFILE[plan.behaviorProfile];
2873
- if (!expectedRows) {
2874
- return "composer_not_exact";
2875
- }
2876
- const lines = screen.replace(/\r\n?/gu, "\n").split("\n");
2877
- let composerIndex = -1;
2878
- for (let index = lines.length - 1; index >= 0; index -= 1) {
2879
- if (CODEX_COMPOSER_MARKER.test(lines[index])) {
2880
- composerIndex = index;
2881
- break;
2882
- }
2883
- }
2884
- if (composerIndex < 0 ||
2885
- lines[composerIndex].replace(/^[›»]\s?/u, "").trimEnd() !== plan.command) {
2886
- return "composer_not_exact";
2887
- }
2888
- const footerIndex = lines.findIndex((line, index) => index > composerIndex && CODEX_COMPOSER_FOOTER.test(line.trim()));
2889
- const popupRows = lines.slice(composerIndex + 1, footerIndex < 0 ? lines.length : footerIndex).filter((line) => line.trim().length > 0);
2890
- if (popupRows.length === 0) {
2891
- return "composer_not_exact";
2892
- }
2893
- const logicalRows = [];
2894
- let observedTruncation = false;
2895
- for (const row of popupRows) {
2896
- const trimmed = row.trim().replace(/\s+/gu, " ");
2897
- if (trimmed.startsWith("/")) {
2898
- logicalRows.push(trimmed);
2899
- }
2900
- else if (logicalRows.length > 0) {
2901
- logicalRows[logicalRows.length - 1] += ` ${trimmed}`;
2902
- observedTruncation = true;
2903
- }
2904
- else {
2905
- return "composer_not_exact";
2906
- }
2907
- observedTruncation ||= trimmed.endsWith("…");
2908
- }
2909
- const normalizedExpected = expectedRows.map((row) => row.trim().replace(/\s+/gu, " "));
2910
- const everyKnownPrefix = logicalRows.length <= normalizedExpected.length &&
2911
- logicalRows.every((row, index) => {
2912
- const withoutEllipsis = row.endsWith("…")
2913
- ? row.slice(0, -1).trimEnd()
2914
- : row;
2915
- return normalizedExpected[index]?.startsWith(withoutEllipsis) === true;
2916
- });
2917
- return observedTruncation && everyKnownPrefix
2918
- ? "composer_viewport_truncated"
2919
- : "composer_not_exact";
2920
- }
2921
- function codexBlockingModalVisible(screen) {
2922
- const tail = screen.replace(/\r\n?/gu, "\n").split("\n").slice(-80)
2923
- .join("\n");
2924
- return /\b(?:press|use)\s+(?:esc|escape)\s+to\s+(?:cancel|close|dismiss)\b/iu
2925
- .test(tail) ||
2926
- /\besc\s+to\s+cancel\b/iu.test(tail) ||
2927
- codexActiveWriterViewerVisible(tail) ||
2928
- ["expanded", "ambiguous"].includes(inspectCodexAsyncQuestionInputMode(tail));
2929
- }
2930
- function codexActiveWriterViewerVisible(styledScreen) {
2931
- const lines = stripTerminalEscapeSequences(styledScreen)
2932
- .replace(/\r\n?/gu, "\n")
2933
- .split("\n")
2934
- .slice(-80);
2935
- let titleIndex = -1;
2936
- for (let index = lines.length - 1; index >= 0; index -= 1) {
2937
- if (/^ {2}🔒(?:\s|$)/u.test(lines[index])) {
2938
- titleIndex = index;
2939
- break;
2940
- }
2941
- }
2942
- if (titleIndex < 0) {
2943
- return false;
2944
- }
2945
- const laterMainComposer = lines.findIndex((line, index) => index > titleIndex && CODEX_COMPOSER_MARKER.test(line));
2946
- if (laterMainComposer >= 0) {
2947
- return false;
2948
- }
2949
- const frame = lines.slice(titleIndex, titleIndex + 12)
2950
- .map((line) => line.trim().replace(/\s+/gu, " "))
2951
- .filter((line) => line.length > 0)
2952
- .join(" ");
2953
- return /🔒\s+This conversation is open in another app\s+\S+ to Retry/iu
2954
- .test(frame) &&
2955
- /Close it there and press\s+\S+\s+to continue here\./iu.test(frame) &&
2956
- /\S+ retry\s+\S+ exit(?:\s+\S+ transcript)?/iu.test(frame);
2957
- }
2958
- const CODEX_ASYNC_QUESTION_FOOTER_LABEL = /\s(submit|skip|main prompt|prev question|next question|queued messages)(?=\s{2,}|\s*$)/giu;
2959
- function codexStructuredAsyncFooterEvidence(lines, evidenceFloor) {
2960
- let strongest = { index: -1, labelCount: 0 };
2961
- for (let start = evidenceFloor + 1; start < lines.length; start += 1) {
2962
- const labels = new Set();
2963
- for (let end = start; end < Math.min(lines.length, start + 5); end += 1) {
2964
- const line = lines[end];
2965
- if (!/^ {2}(?![ ↳?])\S/u.test(line)) {
2966
- break;
2967
- }
2968
- for (const match of line.matchAll(CODEX_ASYNC_QUESTION_FOOTER_LABEL)) {
2969
- labels.add(match[1].toLowerCase());
2970
- }
2971
- if (labels.size > strongest.labelCount) {
2972
- strongest = { index: end, labelCount: labels.size };
2973
- }
2974
- if (labels.size >= 2) {
2975
- return strongest;
2976
- }
2977
- }
2978
- }
2979
- return strongest;
2980
- }
2981
- function codexMainComposerFooterIndex(lines, composerIndex) {
2982
- for (let index = lines.length - 1; index > composerIndex; index -= 1) {
2983
- // A real statusline starts at column zero. Draft continuations are inset,
2984
- // even when their text happens to look exactly like a model statusline.
2985
- if (CODEX_COMPLETE_COMPOSER_FOOTER.test(lines[index].trimEnd())) {
2986
- return index;
2987
- }
2988
- }
2989
- return -1;
2990
- }
2991
- /**
2992
- * Classify Codex 0.154's non-blocking inline-question surface by current input
2993
- * ownership. The shared `Queued follow-up inputs` heading is not sufficient:
2994
- * ordinary queued messages use it too. An exact collapsed summary, after any
2995
- * ordinary queue preview is removed, proves Send-safe ownership even when the
2996
- * Composer is outside the viewport. Positive editor evidence without a
2997
- * complete shape remains fail-closed because paste plus Enter would otherwise
2998
- * submit the user's task as an answer.
2999
- */
3000
- export function inspectCodexAsyncQuestionInputMode(styledScreen) {
3001
- const lines = stripTerminalEscapeSequences(styledScreen)
3002
- .replace(/\r\n?/gu, "\n")
3003
- .replace(/\u00a0/gu, " ")
3004
- .split("\n")
3005
- .slice(-80);
3006
- while (lines.length > 0 && lines.at(-1)?.trim().length === 0) {
3007
- lines.pop();
3008
- }
3009
- const normalized = lines.map((line) => line.trim().replace(/\s+/gu, " "));
3010
- let mainComposerIndex = -1;
3011
- for (let index = lines.length - 1; index >= 0; index -= 1) {
3012
- // The main Composer marker is column zero. Async option and inline-text
3013
- // markers are inset by the menu surface, even after ANSI is removed.
3014
- if (CODEX_COMPOSER_MARKER.test(lines[index])) {
3015
- mainComposerIndex = index;
3016
- break;
3017
- }
3018
- }
3019
- let headerIndex = -1;
3020
- let wrappedHeaderRows = 1;
3021
- for (let index = lines.length - 1; index >= 0; index -= 1) {
3022
- if (/^•\s+Queued follow-up inputs\s*$/u.test(lines[index]) ||
3023
- /^•\s+Queued follow-up inpu…\s*$/u.test(lines[index]) ||
3024
- (/^•\s+Queued follow-up\s*$/u.test(lines[index]) &&
3025
- /^ {2}inputs\s*$/u.test(lines[index + 1] ?? ""))) {
3026
- headerIndex = index;
3027
- wrappedHeaderRows = /^•\s+Queued follow-up\s*$/u.test(lines[index])
3028
- ? 2
3029
- : 1;
3030
- break;
3031
- }
3032
- }
3033
- const headerBodyIndex = headerIndex < 0
3034
- ? -1
3035
- : headerIndex + wrappedHeaderRows;
3036
- let questionEvidenceStart = headerBodyIndex;
3037
- if (headerIndex >= 0) {
3038
- let cursor = headerBodyIndex;
3039
- while (cursor < lines.length && lines[cursor].trim().length === 0) {
3040
- cursor += 1;
3041
- }
3042
- let sawQueuedPreview = false;
3043
- while (cursor < lines.length) {
3044
- if (/^ {2}↳\s/u.test(lines[cursor])) {
3045
- sawQueuedPreview = true;
3046
- cursor += 1;
3047
- while (cursor < lines.length) {
3048
- if (lines[cursor].trim().length === 0 ||
3049
- /^ {4,}\S/u.test(lines[cursor])) {
3050
- cursor += 1;
3051
- continue;
3052
- }
3053
- break;
3054
- }
3055
- continue;
3056
- }
3057
- break;
3058
- }
3059
- if (sawQueuedPreview) {
3060
- questionEvidenceStart = cursor;
3061
- }
3062
- }
3063
- const mainComposerFooterIndex = mainComposerIndex < 0
3064
- ? -1
3065
- : codexMainComposerFooterIndex(lines, mainComposerIndex);
3066
- const summaryBeforeMainComposer = headerIndex < 0 || mainComposerIndex < 0
3067
- ? -1
3068
- : normalized.findIndex((line, index) => index >= questionEvidenceStart && index < mainComposerIndex &&
3069
- /^\?\s+\d+\s+questions?(?:\s+·\s+\d+s)?$/iu.test(line));
3070
- if (mainComposerIndex >= 0 && mainComposerFooterIndex < 0) {
3071
- // A statusline-disabled Composer is the strongest available ownership
3072
- // anchor. Treat all following wrapped rows as its arbitrary draft instead
3073
- // of letting question-like user text veto a human-priority Send.
3074
- return summaryBeforeMainComposer >= 0 ? "collapsed" : "absent";
3075
- }
3076
- const evidenceFloor = Math.max(mainComposerFooterIndex, questionEvidenceStart - 1);
3077
- const structuredFooter = codexStructuredAsyncFooterEvidence(lines, evidenceFloor);
3078
- if (structuredFooter.labelCount >= 2) {
3079
- return "expanded";
3080
- }
3081
- const clippedFooterIndex = normalized.findIndex((line, index) => index > evidenceFloor && /^ente(?:r)?…$/iu.test(line) &&
3082
- /^ctrl…$/iu.test(normalized[index + 1] ?? ""));
3083
- const clippedChoiceIndex = normalized.findIndex((line, index) => index > evidenceFloor &&
3084
- line === "Expand terminal to read the entire option");
3085
- if (clippedFooterIndex >= 0 || clippedChoiceIndex >= 0) {
3086
- return "ambiguous";
3087
- }
3088
- if (headerIndex < 0) {
3089
- const evidence = new Set();
3090
- normalized.forEach((line, index) => {
3091
- if (index <= evidenceFloor) {
3092
- return;
3093
- }
3094
- if (/^\d+\s+of(?:\s+\d+)?$/iu.test(line)) {
3095
- evidence.add("progress");
3096
- }
3097
- if (/^›\s+\S/u.test(line)) {
3098
- evidence.add("selector");
3099
- }
3100
- if (/^Type your answer(?:\s*\(optional\))?$/iu.test(line)) {
3101
- evidence.add("free_text");
3102
- }
3103
- });
3104
- return evidence.size >= 2 ||
3105
- (evidence.size >= 1 && structuredFooter.labelCount >= 1)
3106
- ? "ambiguous"
3107
- : "absent";
3108
- }
3109
- const weakQuestionIndex = normalized.findIndex((line, index) => index > evidenceFloor && index >= questionEvidenceStart && (/^\d+\s+of(?:\s+\d+)?$/iu.test(line) ||
3110
- /^›\s+\S/u.test(line) ||
3111
- /^Type your answer(?:\s*\(optional\))?$/iu.test(line)));
3112
- if (mainComposerIndex > headerIndex && weakQuestionIndex > evidenceFloor) {
3113
- return "ambiguous";
3114
- }
3115
- const summaryIndex = normalized.findIndex((line, index) => index >= questionEvidenceStart &&
3116
- (mainComposerIndex < 0 || index < mainComposerIndex) &&
3117
- /^\?\s+\d+\s+questions?(?:\s+·\s+\d+s)?$/iu.test(line));
3118
- if (summaryIndex >= 0) {
3119
- return weakQuestionIndex >= 0 ? "ambiguous" : "collapsed";
3120
- }
3121
- if (mainComposerIndex > headerIndex) {
3122
- return "absent";
3123
- }
3124
- const afterHeader = normalized.slice(questionEvidenceStart)
3125
- .filter((line) => line.length > 0);
3126
- const positiveAfterHeader = afterHeader.some((line) => /^\d+\s+of(?:\s+\d+)?$/iu.test(line) ||
3127
- /^›\s+\d+\.(?:\s|$)/u.test(line) ||
3128
- /^Type your answer(?:\s*\(optional\))?$/iu.test(line));
3129
- if (positiveAfterHeader) {
3130
- return "ambiguous";
3131
- }
3132
- // A bare heading (optionally followed by ↳ queued-message previews) is not
3133
- // question authority. Preserve human-explicit Send in that ordinary state.
3134
- if (afterHeader.length === 0) {
3135
- return "absent";
3136
- }
3137
- // An expanded editor can temporarily replace its footer with a flash. If
3138
- // there is question-like content under the shared heading and no proven main
3139
- // Composer, do not guess which input surface will receive the next paste.
3140
- return "ambiguous";
3141
- }
3142
- /**
3143
- * Classify only the bottom live Codex composer region. A matching transcript
3144
- * prompt elsewhere in scrollback is deliberately ignored.
3145
- */
3146
- function currentCodexComposerCapture(styledScreen, expectedText, allowOpaqueLargePastePlaceholder = false, classifyOpaqueLargePasteAsDifferent = false) {
3147
- const screen = stripTerminalEscapeSequences(styledScreen);
3148
- const lines = screen.replace(/\r\n?/gu, "\n").split("\n");
3149
- while (lines.length > 0 && lines.at(-1)?.trim().length === 0) {
3150
- lines.pop();
3151
- }
3152
- let composerIndex = -1;
3153
- for (let index = lines.length - 1; index >= 0; index -= 1) {
3154
- if (CODEX_COMPOSER_MARKER.test(lines[index])) {
3155
- composerIndex = index;
3156
- break;
3157
- }
3158
- }
3159
- if (composerIndex < 0) {
3160
- return undefined;
3161
- }
3162
- const footerIndex = lines.findIndex((line, index) => index > composerIndex &&
3163
- CODEX_COMPLETE_COMPOSER_FOOTER.test(line.trim()));
3164
- if (footerIndex >= 0 &&
3165
- lines.slice(footerIndex + 1).some((line) => line.trim().length > 0)) {
3166
- return undefined;
3167
- }
3168
- const regionEnd = footerIndex < 0 ? lines.length : footerIndex;
3169
- const region = lines.slice(composerIndex, regionEnd);
3170
- while (region.length > 1 && region.at(-1)?.trim().length === 0) {
3171
- region.pop();
3172
- }
3173
- if (region.length === 0 ||
3174
- region.slice(1).some((line) => line.length > 0 && !line.startsWith(" "))) {
3175
- return undefined;
3176
- }
3177
- const bodyRows = [
3178
- region[0].replace(/^[›»]\s?/u, ""),
3179
- ...region.slice(1).map((line) => line.startsWith(" ") ? line.slice(2) : line)
3180
- ];
3181
- const digest = createHash("sha256").update(region.join("\n")).digest("hex");
3182
- const positivelyEmpty = footerIndex >= 0 &&
3183
- bodyRows.slice(1).every((row) => row.trim().length === 0) &&
3184
- exactCodexReadyStyledComposerCapture(styledScreen) !== undefined;
3185
- if (positivelyEmpty) {
3186
- return { state: "exact_empty", digest };
3187
- }
3188
- if (footerIndex < 0) {
3189
- return undefined;
3190
- }
3191
- const expectedComparable = composerComparableText(expectedText);
3192
- const expectedCharacterCount = Array.from(expectedText).length;
3193
- const comparable = composerComparableText(bodyRows.join("\n"));
3194
- const opaqueLargePastePlaceholder = /^\[Pasted Content \d+ chars\]$/u.test(comparable);
3195
- if (opaqueLargePastePlaceholder && !allowOpaqueLargePastePlaceholder) {
3196
- return classifyOpaqueLargePasteAsDifferent
3197
- ? { state: "different_draft", digest }
3198
- : undefined;
3199
- }
3200
- const exactVisibleDraft = terminalComposerRowsMatchExpected(bodyRows, expectedComparable);
3201
- const exactLargePastePlaceholder = allowOpaqueLargePastePlaceholder &&
3202
- expectedCharacterCount > CODEX_LARGE_PASTE_CHAR_THRESHOLD &&
3203
- comparable === composerComparableText(`[Pasted Content ${expectedCharacterCount} chars]`);
3204
- if (exactVisibleDraft || exactLargePastePlaceholder) {
3205
- return { state: "exact_draft", digest };
3206
- }
3207
- return comparable.length > 0
3208
- ? { state: "different_draft", digest }
3209
- : undefined;
3210
- }
3211
- function exactCodexComposerCapture(screen, expectedText) {
3212
- const lines = screen.replace(/\r\n?/gu, "\n").split("\n");
3213
- const expectedComparable = composerComparableText(expectedText);
3214
- const expectedCharacterCount = Array.from(expectedText).length;
3215
- const largePasteComparable = composerComparableText(`[Pasted Content ${expectedCharacterCount} chars]`);
3216
- const matches = [];
3217
- for (let index = 0; index < lines.length; index += 1) {
3218
- if (!CODEX_COMPOSER_MARKER.test(lines[index])) {
3219
- continue;
3220
- }
3221
- const footerIndex = lines.findIndex((line, candidateIndex) => candidateIndex > index && CODEX_COMPOSER_FOOTER.test(line.trim()));
3222
- const region = lines.slice(index, footerIndex < 0 ? lines.length : footerIndex);
3223
- while (region.length > 1 && region.at(-1)?.trim() === "") {
3224
- region.pop();
3225
- }
3226
- const bodyRows = [
3227
- region[0].replace(/^[›»]\s?/u, ""),
3228
- ...region.slice(1).map((line) => line.startsWith(" ") ? line.slice(2) : line)
3229
- ];
3230
- const comparable = composerComparableText(bodyRows.join("\n"));
3231
- const exactVisibleDraft = terminalComposerRowsMatchExpected(bodyRows, expectedComparable);
3232
- const exactLargePastePlaceholder = expectedCharacterCount > CODEX_LARGE_PASTE_CHAR_THRESHOLD &&
3233
- comparable === largePasteComparable;
3234
- if (!exactVisibleDraft && !exactLargePastePlaceholder) {
3235
- continue;
3236
- }
3237
- matches.push({
3238
- digest: createHash("sha256")
3239
- .update(region.join("\n"))
3240
- .digest("hex")
3241
- });
3242
- }
3243
- if (matches.length > 1) {
3244
- throw new Error("multiple Codex composer regions matched the multiline terminal request");
3245
- }
3246
- return matches[0];
3247
- }
3248
- /**
3249
- * Terminal UIs paint wrapped composer content as independent screen rows, so
3250
- * a provider capture cannot distinguish a visual wrap from an authored
3251
- * newline. Align the rows against the exact text AKK injected instead of
3252
- * joining every row with `\n`.
3253
- *
3254
- * Only row boundaries are ambiguous: they may consume an authored newline, an
3255
- * omitted run of ASCII spaces at a word wrap, or no character at a CJK/token
3256
- * wrap. Every visible character remains character-for-character exact, and an
3257
- * empty row can only advance through an authored newline (plus terminal-trimmed
3258
- * spaces before it), so blank-line structure is preserved.
3259
- */
3260
- function terminalComposerRowsMatchExpected(rows, expectedText) {
3261
- const expected = composerComparableText(expectedText);
3262
- if (rows.length === 0) {
3263
- return expected.length === 0;
3264
- }
3265
- if (!expected.startsWith(rows[0])) {
3266
- return false;
3267
- }
3268
- let offsets = new Set([rows[0].length]);
3269
- for (let index = 1; index < rows.length && offsets.size > 0; index += 1) {
3270
- const row = rows[index];
3271
- const previousRow = rows[index - 1];
3272
- const nextOffsets = new Set();
3273
- for (const offset of offsets) {
3274
- const candidateStarts = new Set();
3275
- if (expected[offset] === "\n") {
3276
- candidateStarts.add(offset + 1);
3277
- }
3278
- let whitespaceEnd = offset;
3279
- while (expected[whitespaceEnd] === " ") {
3280
- whitespaceEnd += 1;
3281
- }
3282
- if (previousRow.length > 0 && row.length > 0) {
3283
- candidateStarts.add(offset);
3284
- if (whitespaceEnd > offset) {
3285
- candidateStarts.add(whitespaceEnd);
3286
- }
3287
- }
3288
- if (whitespaceEnd > offset &&
3289
- expected[whitespaceEnd] === "\n") {
3290
- candidateStarts.add(whitespaceEnd + 1);
3291
- }
3292
- for (const candidateStart of candidateStarts) {
3293
- if (expected.startsWith(row, candidateStart)) {
3294
- nextOffsets.add(candidateStart + row.length);
3295
- }
3296
- }
3297
- }
3298
- offsets = nextOffsets;
3299
- }
3300
- return offsets.has(expected.length);
3301
- }
3302
- function exactTerminalComposerCapture(agent, screen, expectedText) {
3303
- return agent === "codex"
3304
- ? exactCodexComposerCapture(screen, expectedText)
3305
- : exactClaudeComposerCapture(screen, expectedText);
3306
- }
3307
- function exactClaudeComposerCapture(screen, expectedText) {
3308
- const lines = screen.replace(/\r\n?/gu, "\n").split("\n");
3309
- const dividerIndexes = lines
3310
- .map((line, index) => /^\s*[─━]{8,}\s*$/u.test(line) ? index : -1)
3311
- .filter((index) => index >= 0);
3312
- if (dividerIndexes.length < 2) {
3313
- return undefined;
3314
- }
3315
- const closeIndex = dividerIndexes.at(-1);
3316
- const openIndex = dividerIndexes.at(-2);
3317
- const trailing = lines.slice(closeIndex + 1)
3318
- .filter((line) => line.trim().length > 0);
3319
- if (trailing.length > 2 ||
3320
- trailing.some((line) => !/^\s*(?:[⏵⏴⏸]{1,2}|\?)\s*.*(?:manual mode|shift\+tab|accept edits|bypass permissions|for shortcuts|← for agents)/iu
3321
- .test(line))) {
3322
- return undefined;
3323
- }
3324
- const region = lines.slice(openIndex + 1, closeIndex);
3325
- while (region.length > 1 && region.at(-1)?.trim() === "") {
3326
- region.pop();
3327
- }
3328
- if (region.length === 0 || !/^\s*❯(?:\s|$)/u.test(region[0])) {
3329
- return undefined;
3330
- }
3331
- const bodyRows = [
3332
- region[0].replace(/^\s*❯\s?/u, ""),
3333
- ...region.slice(1).map((line) => line.startsWith(" ") ? line.slice(2) : line)
3334
- ];
3335
- if (!terminalComposerRowsMatchExpected(bodyRows, expectedText)) {
3336
- return undefined;
3337
- }
3338
- return {
3339
- digest: createHash("sha256")
3340
- .update(lines.slice(openIndex, closeIndex + 1).join("\n"))
3341
- .digest("hex")
3342
- };
3343
- }
3344
- function exactClaudeInjectedPastePlaceholderCapture(screen, expectedText) {
3345
- const expectedNewlineCount = (composerComparableText(expectedText).match(/\n/gu) ?? []).length;
3346
- if (expectedNewlineCount === 0) {
3347
- return undefined;
3348
- }
3349
- const frame = exactClaudeComposerFrame(screen);
3350
- if (!frame ||
3351
- frame.composerRows.length !== 1 ||
3352
- !claudeInjectedPastePlaceholderTrailingMatches(frame.trailing)) {
3353
- return undefined;
3354
- }
3355
- const placeholder = /^\s*❯\s*\[Pasted text #([1-9]\d*) \+([1-9]\d*) lines\]\s*$/u
3356
- .exec(frame.composerRows[0]);
3357
- if (!placeholder) {
3358
- return undefined;
3359
- }
3360
- const pasteId = Number(placeholder[1]);
3361
- const newlineCount = Number(placeholder[2]);
3362
- if (!Number.isSafeInteger(pasteId) ||
3363
- !Number.isSafeInteger(newlineCount) ||
3364
- newlineCount !== expectedNewlineCount) {
3365
- return undefined;
3366
- }
3367
- return {
3368
- state: "exact_injected_paste_placeholder",
3369
- digest: createHash("sha256")
3370
- .update(frame.lines
3371
- .slice(frame.openIndex, frame.closeIndex + 1)
3372
- .concat(frame.trailing)
3373
- .join("\n"))
3374
- .digest("hex"),
3375
- pasteId,
3376
- newlineCount,
3377
- frameProfile: CLAUDE_INJECTED_PASTE_FRAME_PROFILE
3378
- };
3379
- }
3380
- function claudeInjectedPastePlaceholderTrailingMatches(lines) {
3381
- if (lines.length === 0) {
3382
- return false;
3383
- }
3384
- const hint = "paste again to expand";
3385
- const first = lines[0].trimStart();
3386
- if (!first.startsWith(hint)) {
3387
- return false;
3388
- }
3389
- const sameRowStatus = first.slice(hint.length);
3390
- if (sameRowStatus.length > 0 && !/^\s{2,}\S/u.test(sameRowStatus)) {
3391
- return false;
3392
- }
3393
- const footerRows = [
3394
- ...(sameRowStatus.trim().length > 0 ? [sameRowStatus.trim()] : []),
3395
- ...lines.slice(1).map((line) => line.trim())
3396
- ];
3397
- return footerRows.length === 0 ||
3398
- claudeNativeInspectionTrailingIsFooter(footerRows) ||
3399
- claudeInjectedPasteAuxiliaryFooterMatches(footerRows);
3400
- }
3401
- function claudeInjectedPasteAuxiliaryFooterMatches(lines) {
3402
- if (lines.length === 0 || lines.length > 2) {
3403
- return false;
3404
- }
3405
- const autoUpdate = lines.filter((line) => line === "✘ Auto-update failed · Run claude doctor").length;
3406
- const effort = lines.filter((line) => /^[●○◐◉] (?:low|medium|high|xhigh|max|ultracode) · \/effort$/u
3407
- .test(line)).length;
3408
- return autoUpdate <= 1 && effort <= 1 &&
3409
- autoUpdate + effort === lines.length;
3410
- }
3411
- function composerComparableText(value) {
3412
- return value.replace(/\r\n?/gu, "\n");
3413
- }
3414
- async function terminalSettleDelay(milliseconds) {
3415
- await new Promise((resolve) => setTimeout(resolve, milliseconds));
3416
- }
3417
- function sameTerminalControlIdentity(left, right) {
3418
- return sameTerminalControlIncarnation(left, right);
3419
- }
3420
- export function terminalApprovalFingerprint(agent, terminalControl, inspection, options = {}) {
3421
- if (!inspection.approval.approvable) {
3422
- return undefined;
3423
- }
3424
- const decision = options.decision ?? "approve_once";
3425
- const choices = terminalApprovalChoices(inspection.approval);
3426
- const action = terminalApprovalActionForDecision(inspection.approval, decision);
3427
- if (!action) {
3428
- return undefined;
3429
- }
3430
- const decisionMode = action.mode ?? "keys";
3431
- const promptEvidence = inspection.approval.promptEvidence;
3432
- if (decisionMode === "keys" &&
3433
- !isTerminalApprovalPromptEvidence(promptEvidence)) {
3434
- return undefined;
1291
+ const decisionMode = action.mode ?? "keys";
1292
+ const promptEvidence = inspection.approval.promptEvidence;
1293
+ if (decisionMode === "keys" &&
1294
+ !isTerminalApprovalPromptEvidence(promptEvidence)) {
1295
+ return undefined;
3435
1296
  }
3436
1297
  const terminal = terminalEndpointFromControlRef(terminalControl);
3437
1298
  // Legacy control records without canonical endpoint evidence bind v2 to
@@ -3527,414 +1388,19 @@ function isTerminalApprovalPromptEvidence(value) {
3527
1388
  typeof evidence.sha256 === "string" &&
3528
1389
  /^[0-9a-f]{64}$/u.test(evidence.sha256);
3529
1390
  }
3530
- function terminalInteractionTurnId(runtime) {
3531
- // Questionnaire mutation is managed-Turn-only. A raw terminal's
3532
- // conversationId is a discovery label, not durable response authority.
3533
- const candidate = runtime?.turnId;
3534
- return typeof candidate === "string" &&
3535
- TERMINAL_INTERACTION_IDENTIFIER_PATTERN.test(candidate)
3536
- ? candidate
3537
- : undefined;
3538
- }
3539
- function terminalInteractionSubject(runtime) {
3540
- if (runtime?.interactionSubject) {
3541
- return runtime.interactionSubject;
3542
- }
3543
- const turnId = terminalInteractionTurnId(runtime);
3544
- const messageId = runtime?.messageId;
3545
- return turnId && typeof messageId === "string" &&
3546
- TERMINAL_INTERACTION_IDENTIFIER_PATTERN.test(messageId)
3547
- ? { kind: "managed_turn", turn_id: turnId, message_id: messageId }
3548
- : undefined;
3549
- }
3550
- function terminalInteractionExpiry(now) {
3551
- const nowMs = now.getTime();
3552
- if (!Number.isFinite(nowMs)) {
3553
- return undefined;
3554
- }
3555
- const windowStart = Math.floor(nowMs / TERMINAL_INTERACTION_TTL_MS) *
3556
- TERMINAL_INTERACTION_TTL_MS;
3557
- return new Date(windowStart + TERMINAL_INTERACTION_TTL_MS).toISOString();
3558
- }
3559
- function terminalInteractionCanonicalIdentity(terminalControl) {
3560
- if (!hasCanonicalTerminalEndpoint(terminalControl)) {
3561
- return undefined;
3562
- }
3563
- const terminal = terminalEndpointFromControlRef(terminalControl);
3564
- if (!Number.isInteger(terminal.processAnchorPid) ||
3565
- Number(terminal.processAnchorPid) <= 0) {
3566
- return undefined;
3567
- }
3568
- // Surface identity is shared by independent observers. Keep physical
3569
- // terminal identity separate from runtime-native identity so an optional
3570
- // observer-only fence (for example processStartedAt) cannot split one live
3571
- // questionnaire into different Monitor and Watch surfaces. Dispatch still
3572
- // revalidates the complete runtime immediately before any terminal input.
3573
- return {
3574
- terminal_identity: terminalEndpointIdentityKey(terminal),
3575
- terminal_process_anchor_pid: terminal.processAnchorPid
3576
- };
3577
- }
3578
- function terminalInteractionNativeTaskIdentity(runtime) {
3579
- // Include only native-task fields that both managed Turn and exact Watch
3580
- // observations can derive from their durable anchors. Optional runtime
3581
- // verification fields remain on `runtime`; they are identity fences, not
3582
- // cross-observer surface identity.
3583
- return {
3584
- agent_pid: runtime.pid,
3585
- native_session_id: runtime.nativeSessionId,
3586
- native_process_uuid: runtime.nativeProcessUuid,
3587
- native_process_birth: runtime.nativeProcessBirth,
3588
- native_rollout: runtime.nativeRollout,
3589
- expected_native_session_id: runtime.expectedNativeSessionId,
3590
- expected_empty_native_session: runtime.expectedEmptyNativeSession === true
3591
- };
3592
- }
3593
- function legacyManagedInteractionProjection(projection) {
3594
- if (projection.subject.kind !== "managed_turn") {
3595
- throw new TypeError("only managed interaction subjects have a v1 projection");
3596
- }
3597
- return validateTerminalInteractionProjection({
3598
- schema: TERMINAL_INTERACTION_SCHEMA,
3599
- version: TERMINAL_INTERACTION_VERSION,
3600
- interaction_id: projection.interaction_id,
3601
- turn_id: projection.subject.turn_id,
3602
- agent: projection.agent,
3603
- kind: projection.kind,
3604
- state: projection.state,
3605
- step: projection.step,
3606
- questions: projection.questions,
3607
- expires_at: projection.expires_at,
3608
- capabilities: projection.capabilities
3609
- });
3610
- }
3611
- export function captureTerminalInteractionRuntimeOffer(input) {
3612
- const subject = terminalInteractionSubject(input.runtime);
3613
- const agentVersion = input.runtime?.agentVersion;
3614
- const expiry = terminalInteractionExpiry(input.now);
3615
- const canonicalIdentity = input.runtime
3616
- ? terminalInteractionCanonicalIdentity(input.terminalControl) ??
3617
- terminalInteractionTrustedWatchIdentity(input.terminalControl, input.runtime, input.trustedTerminalEvidence)
3618
- : undefined;
3619
- if (!subject ||
3620
- typeof agentVersion !== "string" ||
3621
- agentVersion.length === 0 ||
3622
- !expiry ||
3623
- !canonicalIdentity ||
3624
- input.approvalBlocked === true) {
3625
- return undefined;
3626
- }
3627
- const coreOffer = captureTerminalInteraction({
3628
- subject,
3629
- agent: input.agent,
3630
- agentVersion,
3631
- canonicalTerminalIdentity: canonicalIdentity,
3632
- nativeTaskIdentity: terminalInteractionNativeTaskIdentity(input.runtime),
3633
- screen: input.screen,
3634
- now: input.now,
3635
- expiresAt: expiry,
3636
- responseUncertain: input.runtime?.interactionDispatchState === "reserved" ||
3637
- input.runtime?.interactionDispatchState === "uncertain",
3638
- responseAuthority: input.runtime?.interactionResponseAuthority ?? "executable",
3639
- includeLegacyTurnId: subject.kind === "managed_turn"
3640
- });
3641
- if (!coreOffer) {
3642
- return undefined;
3643
- }
3644
- return {
3645
- projection: subject.kind === "managed_turn"
3646
- ? legacyManagedInteractionProjection(coreOffer.projection)
3647
- : coreOffer.projection,
3648
- promptFingerprint: coreOffer.promptFingerprint,
3649
- surfaceId: coreOffer.surfaceId,
3650
- actionPlan: coreOffer.actionPlan,
3651
- nativeInspection: coreOffer.nativeInspection
3652
- };
3653
- }
3654
- function terminalInteractionTrustedWatchIdentity(terminalControl, runtime, evidence) {
3655
- if (runtime.interactionSubject?.kind !== "terminal_watch" ||
3656
- !evidence ||
3657
- !terminalControlEvidenceMatches(evidence, terminalControl)) {
3658
- return undefined;
3659
- }
3660
- const identity = terminalEndpointIdentityFromEvidence(evidence);
3661
- if (!identity || !Number.isSafeInteger(evidence.process_anchor_pid) ||
3662
- Number(evidence.process_anchor_pid) <= 0) {
3663
- return undefined;
3664
- }
3665
- return {
3666
- terminal_identity: terminalEndpointIdentityKey(identity),
3667
- terminal_process_anchor_pid: evidence.process_anchor_pid
3668
- };
3669
- }
3670
- function terminalInteractionOptionPreflightReason(options, runtime) {
3671
- if (typeof options.agentVersion !== "string" ||
3672
- !/^\d+\.\d+\.\d+(?:[-+][A-Za-z0-9.-]+)?$/u.test(options.agentVersion)) {
3673
- return "an exact semantic agent version is required";
3674
- }
3675
- if (options.runtime?.agentVersion !== undefined &&
3676
- options.runtime.agentVersion !== options.agentVersion) {
3677
- return "runtime agent version does not match the requested interaction profile";
3678
- }
3679
- if (!terminalInteractionSubject(runtime)) {
3680
- return "a protocol-safe exact interaction subject is required";
3681
- }
3682
- if (!TERMINAL_INTERACTION_FINGERPRINT_PATTERN.test(options.expectedFingerprint)) {
3683
- return "a valid interaction prompt fingerprint is required";
3684
- }
3685
- if (!Number.isFinite(Date.parse(options.expectedExpiresAt))) {
3686
- return "a valid interaction expiry is required";
3687
- }
3688
- return undefined;
3689
- }
3690
- function terminalInteractionOfferPreflightReason(offer, options, now) {
3691
- if (!offer) {
3692
- return "native questionnaire has no identity-fenced interaction offer";
3693
- }
3694
- if (offer.projection.state !== "pending" ||
3695
- !offer.projection.capabilities.respond ||
3696
- offer.actionPlan.kind === "manual_only") {
3697
- return `native questionnaire requires manual response: ${offer.nativeInspection.status === "manual_required"
3698
- ? offer.nativeInspection.reason
3699
- : "unsupported action plan"}`;
3700
- }
3701
- if (offer.promptFingerprint !== options.expectedFingerprint) {
3702
- return "native questionnaire fingerprint changed before execution";
3703
- }
3704
- if (offer.projection.expires_at !== options.expectedExpiresAt &&
3705
- Date.parse(options.expectedExpiresAt) > now.getTime()) {
3706
- return "native questionnaire expiry changed before execution";
3707
- }
3708
- return undefined;
3709
- }
3710
- function terminalInteractionCapabilityReason(provider, terminalControl, plan) {
3711
- const requiresText = plan.kind === "free_text";
3712
- const missing = [
3713
- !terminalControl.capabilities.includes("screen_status")
3714
- ? "terminal:screen_status"
3715
- : undefined,
3716
- !terminalControl.capabilities.includes("send_keys")
3717
- ? "terminal:send_keys"
3718
- : undefined,
3719
- !provider.providerCapabilities.includes("stable_resource_resolution")
3720
- ? "provider:stable_resource_resolution"
3721
- : undefined,
3722
- !provider.providerCapabilities.includes("screen_capture")
3723
- ? "provider:screen_capture"
3724
- : undefined,
3725
- !provider.providerCapabilities.includes("key_delivery")
3726
- ? "provider:key_delivery"
3727
- : undefined,
3728
- requiresText && !provider.providerCapabilities.includes("text_delivery")
3729
- ? "provider:text_delivery"
3730
- : undefined
3731
- ].filter((value) => value !== undefined);
3732
- return missing.length > 0
3733
- ? `terminal interaction capability preflight failed: ${missing.join(", ")}`
1391
+ function statusFromInspection(adapter, terminalControl, inspection, options = {}) {
1392
+ const exactModelSurface = options.screen !== undefined &&
1393
+ exactCurrentModelControlSurface(adapter.agent, options.runtime?.agentVersion, options.screen);
1394
+ const modelSurfaceReason = exactModelSurface
1395
+ ? `exact ${adapter.displayName} native model-control surface is open`
3734
1396
  : undefined;
3735
- }
3736
- function terminalInteractionPlanPreflightReason(response, plan) {
3737
- const answer = response.answers[0];
3738
- if (plan.kind !== "free_text" || answer?.response_kind !== "free_text") {
3739
- return undefined;
3740
- }
3741
- const textStage = plan.stages[0];
3742
- if (textStage?.kind !== "answer_text" ||
3743
- !Number.isSafeInteger(textStage.max_characters) ||
3744
- textStage.max_characters < 1) {
3745
- return "native free-text interaction has no verified input limit";
3746
- }
3747
- return answer.text.length <= textStage.max_characters
3748
- ? undefined
3749
- : `free-text answer exceeds the verified native limit of ${textStage.max_characters} characters`;
3750
- }
3751
- function blockedTerminalInteractionResponse(response, reason, offer) {
3752
- const answer = Array.isArray(response.answers) ? response.answers[0] : undefined;
3753
- return {
3754
- responded: false,
3755
- blocked: true,
3756
- reason,
3757
- interactionId: response.interaction_id,
3758
- questionId: answer?.question_id ?? offer?.projection.questions[0]?.question_id,
3759
- responseKind: answer?.response_kind
3760
- };
3761
- }
3762
- function interactionHookContext(agent, terminalControl, offer, response, runtime) {
3763
- return {
3764
- agent,
3765
- terminalControl,
3766
- fingerprint: offer.promptFingerprint,
3767
- projection: offer.projection,
3768
- response,
3769
- runtime
3770
- };
3771
- }
3772
- async function authorizeTerminalInteraction(authorize, context) {
3773
- return authorize ? authorize(context) : { approved: true };
3774
- }
3775
- async function reserveTerminalInteractionDispatch(beforeDispatch, context) {
3776
- if (!beforeDispatch) {
3777
- return;
3778
- }
3779
- try {
3780
- await beforeDispatch(context);
3781
- }
3782
- catch (error) {
3783
- if (error instanceof TerminalInteractionInputNotStartedError) {
3784
- throw error;
3785
- }
3786
- throw terminalInteractionReservedError("reservation_uncertain", error, "terminal interaction dispatch reservation failed");
3787
- }
3788
- }
3789
- function sameTerminalInteractionOffer(left, right, options = {}) {
3790
- const leftProjection = options.ignoreExpiresAt && left.offer
3791
- ? terminalInteractionProjectionWithoutExpiry(left.offer.projection)
3792
- : left.offer?.projection;
3793
- const rightProjection = options.ignoreExpiresAt && right.offer
3794
- ? terminalInteractionProjectionWithoutExpiry(right.offer.projection)
3795
- : right.offer?.projection;
3796
- return Boolean(left.offer &&
3797
- right.offer &&
3798
- sameTerminalControlIdentity(left.terminalControl, right.terminalControl) &&
3799
- left.offer.promptFingerprint === right.offer.promptFingerprint &&
3800
- JSON.stringify(leftProjection) === JSON.stringify(rightProjection) &&
3801
- JSON.stringify(left.offer.actionPlan) === JSON.stringify(right.offer.actionPlan));
3802
- }
3803
- function terminalInteractionProjectionWithoutExpiry(projection) {
3804
- const { expires_at: _expiresAt, ...semanticProjection } = projection;
3805
- return semanticProjection;
3806
- }
3807
- function terminalInteractionInputNotStartedError(error, message) {
3808
- if (error instanceof TerminalInteractionInputNotStartedError) {
3809
- return error;
3810
- }
3811
- const detail = error instanceof Error ? error.message : String(error);
3812
- return new TerminalInteractionInputNotStartedError(`${message}: ${detail}`, { cause: error });
3813
- }
3814
- function terminalInteractionReservedError(stage, error, message) {
3815
- if (error instanceof TerminalInteractionDispatchReservedError) {
3816
- return error;
3817
- }
3818
- const detail = error instanceof Error ? error.message : String(error);
3819
- return new TerminalInteractionDispatchReservedError(stage, `${message}: ${detail}`, { cause: error });
3820
- }
3821
- function terminalInteractionChoiceAction(agent, plan, answer) {
3822
- const choice = plan.choices.find((candidate) => candidate.option_id === answer.selected_option_ids[0]);
3823
- if (!choice) {
3824
- throw new TerminalInteractionInputNotStartedError("single-select interaction has no closed adapter action");
3825
- }
3826
- const keys = choice.stages.flatMap((stage) => stage.kind === "key" ? [stage.key] : []);
3827
- const directChoice = choice.outcome === "submit_or_advance" &&
3828
- choice.stages.length === 1 &&
3829
- keys.length === 1 &&
3830
- /^[1-5]$/u.test(keys[0]);
3831
- const directCustomTextChoice = agent === "claude" &&
3832
- choice.outcome === "open_custom_text" &&
3833
- choice.stages.length === 1 &&
3834
- keys.length === 1 &&
3835
- /^[1-5]$/u.test(keys[0]);
3836
- const navigatedCustomTextChoice = agent === "codex" &&
3837
- choice.outcome === "open_custom_text" &&
3838
- choice.stages.length >= 1 &&
3839
- choice.stages.length <= 4 &&
3840
- keys.length === choice.stages.length &&
3841
- keys.at(-1) === "C-m" &&
3842
- keys.slice(0, -1).every((key) => key === "Down");
3843
- if (!directChoice && !directCustomTextChoice && !navigatedCustomTextChoice) {
3844
- throw new TerminalInteractionInputNotStartedError("single-select interaction has no closed adapter action");
3845
- }
3846
- return {
3847
- keys,
3848
- outcome: choice.outcome === "open_custom_text"
3849
- ? "custom_text_opened"
3850
- : "submitted_or_advanced"
3851
- };
3852
- }
3853
- function terminalInteractionConfirmKey(plan, answer) {
3854
- const stages = answer.confirm ? plan.confirm_stages : plan.cancel_stages;
3855
- const stage = stages[0];
3856
- if (stages.length !== 1 ||
3857
- stage?.kind !== "key" ||
3858
- !["C-m", "Escape", "1", "2"].includes(stage.key) ||
3859
- JSON.stringify(plan.confirm_stages) === JSON.stringify(plan.cancel_stages)) {
3860
- throw new TerminalInteractionInputNotStartedError("confirmation interaction has no closed adapter action");
3861
- }
3862
- return stage.key;
3863
- }
3864
- async function sendTerminalInteractionKeys(provider, terminalControl, keys) {
3865
- try {
3866
- await provider.sendKeys(provider.endpoint(terminalControl), keys);
3867
- }
3868
- catch (error) {
3869
- throw terminalInteractionReservedError("key_uncertain", error, "terminal interaction key dispatch is uncertain");
3870
- }
3871
- }
3872
- async function dispatchTerminalInteractionFreeText(input) {
3873
- const [textStage, enterStage] = input.plan.stages;
3874
- if (input.plan.stages.length !== 2 ||
3875
- textStage?.kind !== "answer_text" ||
3876
- textStage.single_line !== true ||
3877
- textStage.max_characters < input.answer.text.length ||
3878
- enterStage?.kind !== "key" ||
3879
- enterStage.key !== "C-m") {
3880
- throw new TerminalInteractionInputNotStartedError("free-text interaction has no closed adapter action");
3881
- }
3882
- try {
3883
- await input.provider.sendText(input.provider.endpoint(input.terminalControl), input.answer.text);
3884
- }
3885
- catch (error) {
3886
- throw terminalInteractionReservedError("text_uncertain", error, "terminal interaction text dispatch is uncertain");
3887
- }
3888
- if (input.agent === "codex") {
3889
- try {
3890
- await input.sleep(CODEX_PASTE_ENTER_SETTLE_MS);
3891
- }
3892
- catch (error) {
3893
- throw terminalInteractionReservedError("text_uncertain", error, "terminal interaction paste-settle outcome is uncertain");
3894
- }
3895
- }
3896
- let enterControl;
3897
- try {
3898
- enterControl = await input.reverifyTerminalIdentity(input.terminalControl);
3899
- if (!sameTerminalControlIdentity(input.terminalControl, enterControl)) {
3900
- throw new Error("terminal identity changed between interaction text and Enter");
1397
+ const approval = exactModelSurface
1398
+ ? {
1399
+ blocked: false,
1400
+ approvable: false,
1401
+ reason: modelSurfaceReason
3901
1402
  }
3902
- }
3903
- catch (error) {
3904
- throw terminalInteractionReservedError("text_uncertain", error, "terminal identity after interaction text dispatch is uncertain");
3905
- }
3906
- await sendTerminalInteractionKeys(input.provider, enterControl, [enterStage.key]);
3907
- return "submitted_or_advanced";
3908
- }
3909
- async function dispatchTerminalInteractionAnswer(input) {
3910
- if (input.actionPlan.kind === "single_select" &&
3911
- input.answer.response_kind === "single_select") {
3912
- const action = terminalInteractionChoiceAction(input.agent, input.actionPlan, input.answer);
3913
- await sendTerminalInteractionKeys(input.provider, input.terminalControl, action.keys);
3914
- return action.outcome;
3915
- }
3916
- if (input.actionPlan.kind === "confirm" &&
3917
- input.answer.response_kind === "confirm") {
3918
- const key = terminalInteractionConfirmKey(input.actionPlan, input.answer);
3919
- await sendTerminalInteractionKeys(input.provider, input.terminalControl, [key]);
3920
- return input.answer.confirm ? "confirmed" : "cancelled";
3921
- }
3922
- if (input.actionPlan.kind === "free_text" &&
3923
- input.answer.response_kind === "free_text") {
3924
- return dispatchTerminalInteractionFreeText({
3925
- agent: input.agent,
3926
- provider: input.provider,
3927
- terminalControl: input.terminalControl,
3928
- plan: input.actionPlan,
3929
- answer: input.answer,
3930
- reverifyTerminalIdentity: input.reverifyTerminalIdentity,
3931
- sleep: input.sleep
3932
- });
3933
- }
3934
- throw new TerminalInteractionInputNotStartedError("terminal interaction answer does not have a closed adapter action");
3935
- }
3936
- function statusFromInspection(adapter, terminalControl, inspection, options = {}) {
3937
- const approval = dispatchableApprovalInspection(adapter, inspection.approval);
1403
+ : dispatchableApprovalInspection(adapter, inspection.approval);
3938
1404
  const fingerprint = terminalApprovalFingerprint(adapter.agent, terminalControl, {
3939
1405
  ...inspection,
3940
1406
  approval
@@ -3952,7 +1418,7 @@ function statusFromInspection(adapter, terminalControl, inspection, options = {}
3952
1418
  }] : [];
3953
1419
  })
3954
1420
  : [];
3955
- const interaction = options.screen === undefined
1421
+ const interaction = options.screen === undefined || exactModelSurface
3956
1422
  ? undefined
3957
1423
  : captureTerminalInteractionRuntimeOffer({
3958
1424
  agent: adapter.agent,
@@ -3968,10 +1434,10 @@ function statusFromInspection(adapter, terminalControl, inspection, options = {}
3968
1434
  agent: adapter.agent,
3969
1435
  reachable: true,
3970
1436
  capabilities: adapter.capabilities,
3971
- activity_state: inspection.activity.state,
3972
- activity_reason: inspection.activity.reason,
3973
- screen_state: inspection.activity.state,
3974
- screen_reason: inspection.activity.reason,
1437
+ activity_state: exactModelSurface ? "unknown" : inspection.activity.state,
1438
+ activity_reason: modelSurfaceReason ?? inspection.activity.reason,
1439
+ screen_state: exactModelSurface ? "unknown" : inspection.activity.state,
1440
+ screen_reason: modelSurfaceReason ?? inspection.activity.reason,
3975
1441
  approval_state: {
3976
1442
  scanned: true,
3977
1443
  blocked: approval.blocked,
@@ -4009,7 +1475,7 @@ function statusFromInspection(adapter, terminalControl, inspection, options = {}
4009
1475
  approval: approvalOutput(approval)
4010
1476
  },
4011
1477
  ...(interaction === undefined ? {} : {
4012
- interaction_state: interaction.projection,
1478
+ interaction_state: terminalInteractionPublicCompatibilityProjection(interaction.projection),
4013
1479
  interaction_prompt_fingerprint: interaction.promptFingerprint,
4014
1480
  interaction_surface_id: interaction.surfaceId
4015
1481
  })
@@ -4116,4 +1582,33 @@ function unsupportedScreenStatus(adapter, terminalControl) {
4116
1582
  capability_limitation: reason
4117
1583
  };
4118
1584
  }
1585
+ function assertTerminalModelControlPlan(adapter, terminalControl, agentVersion, plan) {
1586
+ if (!terminalControl.capabilities.includes("send_keys") ||
1587
+ !terminalControl.capabilities.includes("screen_status")) {
1588
+ throw new Error("terminal model control requires exact screen-status and key-delivery capabilities");
1589
+ }
1590
+ const capabilities = adapter.probeModelControl?.(agentVersion);
1591
+ if (capabilities?.status !== "supported" ||
1592
+ capabilities.modelSelection !== true ||
1593
+ capabilities.reasoningEffortSelection !== true ||
1594
+ capabilities.agentVersion !== agentVersion) {
1595
+ throw new Error(capabilities?.reason ??
1596
+ `${adapter.displayName} has no verified model-control profile`);
1597
+ }
1598
+ const expected = adapter.planModelControl?.(capabilities);
1599
+ if (!expected ||
1600
+ expected.command !== "/model" ||
1601
+ expected.requiresIdle !== true ||
1602
+ expected.requiresExactEmptyComposer !== true ||
1603
+ JSON.stringify(expected) !== JSON.stringify(plan)) {
1604
+ throw new Error("the terminal adapter did not produce the exact current model-control plan");
1605
+ }
1606
+ if (!terminalModelControlPlanConforms({
1607
+ agent: adapter.agent,
1608
+ agentVersion,
1609
+ plan
1610
+ })) {
1611
+ throw new Error("refusing an unprofiled terminal model-control plan");
1612
+ }
1613
+ }
4119
1614
  //# sourceMappingURL=terminal-agent-bridge.js.map