@scotthuang/agent-knock-knock 0.13.3 → 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 (259) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +13 -5
  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/codex-process-incarnation.d.ts +8 -0
  14. package/dist/src/codex-process-incarnation.js +36 -0
  15. package/dist/src/codex-process-incarnation.js.map +1 -0
  16. package/dist/src/deferred-foreground-conflict-policy.d.ts +22 -0
  17. package/dist/src/deferred-foreground-conflict-policy.js +52 -0
  18. package/dist/src/deferred-foreground-conflict-policy.js.map +1 -0
  19. package/dist/src/durable-notification-kernel.d.ts +94 -0
  20. package/dist/src/durable-notification-kernel.js +68 -0
  21. package/dist/src/durable-notification-kernel.js.map +1 -0
  22. package/dist/src/herdr-socket-transport.d.ts +43 -0
  23. package/dist/src/herdr-socket-transport.js +170 -0
  24. package/dist/src/herdr-socket-transport.js.map +1 -0
  25. package/dist/src/herdr-terminal-control-provider.d.ts +2 -45
  26. package/dist/src/herdr-terminal-control-provider.js +2 -172
  27. package/dist/src/herdr-terminal-control-provider.js.map +1 -1
  28. package/dist/src/host-adapter-capabilities.d.ts +35 -0
  29. package/dist/src/host-adapter-capabilities.js +146 -0
  30. package/dist/src/host-adapter-capabilities.js.map +1 -0
  31. package/dist/src/host-adapter.d.ts +9 -3
  32. package/dist/src/host-adapter.js +33 -9
  33. package/dist/src/host-adapter.js.map +1 -1
  34. package/dist/src/host-bridge-tools.d.ts +3 -4
  35. package/dist/src/host-bridge-tools.js +49 -92
  36. package/dist/src/host-bridge-tools.js.map +1 -1
  37. package/dist/src/host-bridge.js +5 -5
  38. package/dist/src/host-bridge.js.map +1 -1
  39. package/dist/src/host-monitor-reconciliation.d.ts +6 -0
  40. package/dist/src/host-monitor-reconciliation.js +96 -0
  41. package/dist/src/host-monitor-reconciliation.js.map +1 -0
  42. package/dist/src/native-thread-lifecycle-cli-adapter.js +116 -60
  43. package/dist/src/native-thread-lifecycle-cli-adapter.js.map +1 -1
  44. package/dist/src/openclaw-list-projection.d.ts +2 -0
  45. package/dist/src/openclaw-list-projection.js +3 -0
  46. package/dist/src/openclaw-list-projection.js.map +1 -0
  47. package/dist/src/openclaw-model-facing-field-policy.d.ts +2 -0
  48. package/dist/src/openclaw-model-facing-field-policy.js +3 -0
  49. package/dist/src/openclaw-model-facing-field-policy.js.map +1 -0
  50. package/dist/src/openclaw-plugin-command-adapter.d.ts +6 -29
  51. package/dist/src/openclaw-plugin-command-adapter.js +7 -2660
  52. package/dist/src/openclaw-plugin-command-adapter.js.map +1 -1
  53. package/dist/src/openclaw-plugin-helpers.d.ts +2 -148
  54. package/dist/src/openclaw-plugin-helpers.js +2 -1844
  55. package/dist/src/openclaw-plugin-helpers.js.map +1 -1
  56. package/dist/src/openclaw-plugin-schemas.d.ts +2 -563
  57. package/dist/src/openclaw-plugin-schemas.js +2 -572
  58. package/dist/src/openclaw-plugin-schemas.js.map +1 -1
  59. package/dist/src/openclaw-plugin-supervisor.d.ts +4 -5
  60. package/dist/src/openclaw-plugin-supervisor.js +5 -94
  61. package/dist/src/openclaw-plugin-supervisor.js.map +1 -1
  62. package/dist/src/openclaw-plugin-value-helpers.d.ts +2 -0
  63. package/dist/src/openclaw-plugin-value-helpers.js +3 -0
  64. package/dist/src/openclaw-plugin-value-helpers.js.map +1 -0
  65. package/dist/src/openclaw-private-authority-offers.d.ts +4 -39
  66. package/dist/src/openclaw-private-authority-offers.js +3 -258
  67. package/dist/src/openclaw-private-authority-offers.js.map +1 -1
  68. package/dist/src/semantic-private-authority-offers.d.ts +41 -0
  69. package/dist/src/semantic-private-authority-offers.js +261 -0
  70. package/dist/src/semantic-private-authority-offers.js.map +1 -0
  71. package/dist/src/semantic-tool-arguments.d.ts +9 -0
  72. package/dist/src/semantic-tool-arguments.js +64 -0
  73. package/dist/src/semantic-tool-arguments.js.map +1 -0
  74. package/dist/src/semantic-tool-catalog.d.ts +48 -0
  75. package/dist/src/semantic-tool-catalog.js +105 -0
  76. package/dist/src/semantic-tool-catalog.js.map +1 -0
  77. package/dist/src/semantic-tool-command-helpers.d.ts +145 -0
  78. package/dist/src/semantic-tool-command-helpers.js +1272 -0
  79. package/dist/src/semantic-tool-command-helpers.js.map +1 -0
  80. package/dist/src/semantic-tool-list-projection.d.ts +10 -0
  81. package/dist/src/semantic-tool-list-projection.js +497 -0
  82. package/dist/src/semantic-tool-list-projection.js.map +1 -0
  83. package/dist/src/semantic-tool-model-facing-field-policy.d.ts +3 -0
  84. package/dist/src/semantic-tool-model-facing-field-policy.js +66 -0
  85. package/dist/src/semantic-tool-model-facing-field-policy.js.map +1 -0
  86. package/dist/src/semantic-tool-presentation.d.ts +37 -0
  87. package/dist/src/semantic-tool-presentation.js +680 -0
  88. package/dist/src/semantic-tool-presentation.js.map +1 -0
  89. package/dist/src/semantic-tool-private-authority.d.ts +39 -0
  90. package/dist/src/semantic-tool-private-authority.js +602 -0
  91. package/dist/src/semantic-tool-private-authority.js.map +1 -0
  92. package/dist/src/semantic-tool-relay.d.ts +16 -0
  93. package/dist/src/semantic-tool-relay.js +175 -0
  94. package/dist/src/semantic-tool-relay.js.map +1 -0
  95. package/dist/src/semantic-tool-runtime.d.ts +10 -0
  96. package/dist/src/semantic-tool-runtime.js +1157 -0
  97. package/dist/src/semantic-tool-runtime.js.map +1 -0
  98. package/dist/src/semantic-tool-schemas.d.ts +563 -0
  99. package/dist/src/semantic-tool-schemas.js +573 -0
  100. package/dist/src/semantic-tool-schemas.js.map +1 -0
  101. package/dist/src/semantic-tool-value-helpers.d.ts +5 -0
  102. package/dist/src/semantic-tool-value-helpers.js +27 -0
  103. package/dist/src/semantic-tool-value-helpers.js.map +1 -0
  104. package/dist/src/store-event-presentation.d.ts +17 -0
  105. package/dist/src/store-event-presentation.js +33 -0
  106. package/dist/src/store-event-presentation.js.map +1 -0
  107. package/dist/src/store.d.ts +3 -16
  108. package/dist/src/store.js +2 -30
  109. package/dist/src/store.js.map +1 -1
  110. package/dist/src/terminal-action-projection.d.ts +2 -98
  111. package/dist/src/terminal-action-projection.js +13 -215
  112. package/dist/src/terminal-action-projection.js.map +1 -1
  113. package/dist/src/terminal-agent-bridge.d.ts +14 -340
  114. package/dist/src/terminal-agent-bridge.js +155 -3226
  115. package/dist/src/terminal-agent-bridge.js.map +1 -1
  116. package/dist/src/terminal-approval-cli-adapter.d.ts +17 -0
  117. package/dist/src/terminal-approval-cli-adapter.js +921 -0
  118. package/dist/src/terminal-approval-cli-adapter.js.map +1 -0
  119. package/dist/src/terminal-command-cli-adapter.d.ts +2 -429
  120. package/dist/src/terminal-command-cli-adapter.js +344 -6061
  121. package/dist/src/terminal-command-cli-adapter.js.map +1 -1
  122. package/dist/src/terminal-command-cli-ports.d.ts +443 -0
  123. package/dist/src/terminal-command-cli-ports.js +2 -0
  124. package/dist/src/terminal-command-cli-ports.js.map +1 -0
  125. package/dist/src/terminal-command-dispatch-preparation.d.ts +81 -0
  126. package/dist/src/terminal-command-dispatch-preparation.js +209 -0
  127. package/dist/src/terminal-command-dispatch-preparation.js.map +1 -0
  128. package/dist/src/terminal-command-dispatch-transport.d.ts +35 -0
  129. package/dist/src/terminal-command-dispatch-transport.js +859 -0
  130. package/dist/src/terminal-command-dispatch-transport.js.map +1 -0
  131. package/dist/src/terminal-command-foreground-proof.d.ts +32 -0
  132. package/dist/src/terminal-command-foreground-proof.js +72 -0
  133. package/dist/src/terminal-command-foreground-proof.js.map +1 -0
  134. package/dist/src/terminal-command-send-preflight.d.ts +41 -0
  135. package/dist/src/terminal-command-send-preflight.js +86 -0
  136. package/dist/src/terminal-command-send-preflight.js.map +1 -0
  137. package/dist/src/terminal-composer-classifier.d.ts +55 -0
  138. package/dist/src/terminal-composer-classifier.js +638 -0
  139. package/dist/src/terminal-composer-classifier.js.map +1 -0
  140. package/dist/src/terminal-dispatch-recovery-authority.d.ts +56 -0
  141. package/dist/src/terminal-dispatch-recovery-authority.js +251 -0
  142. package/dist/src/terminal-dispatch-recovery-authority.js.map +1 -0
  143. package/dist/src/terminal-dispatch-recovery-cli-adapter.d.ts +1 -9
  144. package/dist/src/terminal-dispatch-recovery-cli-adapter.js +5 -240
  145. package/dist/src/terminal-dispatch-recovery-cli-adapter.js.map +1 -1
  146. package/dist/src/terminal-handoff-cli-adapter.d.ts +1 -11
  147. package/dist/src/terminal-handoff-cli-adapter.js +2 -39
  148. package/dist/src/terminal-handoff-cli-adapter.js.map +1 -1
  149. package/dist/src/terminal-human-explicit-send-cli-adapter.d.ts +25 -0
  150. package/dist/src/terminal-human-explicit-send-cli-adapter.js +903 -0
  151. package/dist/src/terminal-human-explicit-send-cli-adapter.js.map +1 -0
  152. package/dist/src/terminal-identity-authority-cli-adapter.d.ts +1 -6
  153. package/dist/src/terminal-identity-authority-cli-adapter.js +3 -33
  154. package/dist/src/terminal-identity-authority-cli-adapter.js.map +1 -1
  155. package/dist/src/terminal-interaction-cli-adapter.js +209 -210
  156. package/dist/src/terminal-interaction-cli-adapter.js.map +1 -1
  157. package/dist/src/terminal-interaction-core.d.ts +0 -2
  158. package/dist/src/terminal-interaction-core.js +4 -3
  159. package/dist/src/terminal-interaction-core.js.map +1 -1
  160. package/dist/src/terminal-interaction-protocol.d.ts +21 -0
  161. package/dist/src/terminal-interaction-protocol.js +74 -0
  162. package/dist/src/terminal-interaction-protocol.js.map +1 -1
  163. package/dist/src/terminal-interaction-response-bridge.d.ts +126 -0
  164. package/dist/src/terminal-interaction-response-bridge.js +579 -0
  165. package/dist/src/terminal-interaction-response-bridge.js.map +1 -0
  166. package/dist/src/terminal-interaction-response-transaction.d.ts +44 -0
  167. package/dist/src/terminal-interaction-response-transaction.js +56 -0
  168. package/dist/src/terminal-interaction-response-transaction.js.map +1 -0
  169. package/dist/src/terminal-list-action-policy.d.ts +43 -0
  170. package/dist/src/terminal-list-action-policy.js +164 -0
  171. package/dist/src/terminal-list-action-policy.js.map +1 -0
  172. package/dist/src/terminal-list-cli-adapter.d.ts +6 -36
  173. package/dist/src/terminal-list-cli-adapter.js +241 -1946
  174. package/dist/src/terminal-list-cli-adapter.js.map +1 -1
  175. package/dist/src/terminal-list-facts.d.ts +129 -0
  176. package/dist/src/terminal-list-facts.js +179 -0
  177. package/dist/src/terminal-list-facts.js.map +1 -0
  178. package/dist/src/terminal-list-inventory.d.ts +59 -0
  179. package/dist/src/terminal-list-inventory.js +194 -0
  180. package/dist/src/terminal-list-inventory.js.map +1 -0
  181. package/dist/src/terminal-list-ownership-service.d.ts +118 -0
  182. package/dist/src/terminal-list-ownership-service.js +1003 -0
  183. package/dist/src/terminal-list-ownership-service.js.map +1 -0
  184. package/dist/src/terminal-list-projection-service.d.ts +49 -0
  185. package/dist/src/terminal-list-projection-service.js +407 -0
  186. package/dist/src/terminal-list-projection-service.js.map +1 -0
  187. package/dist/src/terminal-list-renderer.d.ts +11 -11
  188. package/dist/src/terminal-list-renderer.js +168 -119
  189. package/dist/src/terminal-list-renderer.js.map +1 -1
  190. package/dist/src/terminal-managed-send-cli-adapter.d.ts +33 -0
  191. package/dist/src/terminal-managed-send-cli-adapter.js +994 -0
  192. package/dist/src/terminal-managed-send-cli-adapter.js.map +1 -0
  193. package/dist/src/terminal-managed-turn-list-action-policy.d.ts +47 -0
  194. package/dist/src/terminal-managed-turn-list-action-policy.js +64 -0
  195. package/dist/src/terminal-managed-turn-list-action-policy.js.map +1 -0
  196. package/dist/src/terminal-model-control-action-policy.d.ts +60 -0
  197. package/dist/src/terminal-model-control-action-policy.js +96 -0
  198. package/dist/src/terminal-model-control-action-policy.js.map +1 -0
  199. package/dist/src/terminal-model-control-availability.d.ts +68 -0
  200. package/dist/src/terminal-model-control-availability.js +157 -0
  201. package/dist/src/terminal-model-control-availability.js.map +1 -0
  202. package/dist/src/terminal-model-control-bridge.d.ts +48 -0
  203. package/dist/src/terminal-model-control-bridge.js +259 -0
  204. package/dist/src/terminal-model-control-bridge.js.map +1 -0
  205. package/dist/src/terminal-model-control-profile.d.ts +55 -0
  206. package/dist/src/terminal-model-control-profile.js +137 -0
  207. package/dist/src/terminal-model-control-profile.js.map +1 -0
  208. package/dist/src/terminal-model-control-subject.d.ts +34 -0
  209. package/dist/src/terminal-model-control-subject.js +58 -0
  210. package/dist/src/terminal-model-control-subject.js.map +1 -0
  211. package/dist/src/terminal-model-control-transaction.d.ts +19 -0
  212. package/dist/src/terminal-model-control-transaction.js +39 -0
  213. package/dist/src/terminal-model-control-transaction.js.map +1 -0
  214. package/dist/src/terminal-model-control.d.ts +37 -18
  215. package/dist/src/terminal-model-control.js +285 -194
  216. package/dist/src/terminal-model-control.js.map +1 -1
  217. package/dist/src/terminal-monitor-application-service.js +50 -17
  218. package/dist/src/terminal-monitor-application-service.js.map +1 -1
  219. package/dist/src/terminal-native-inspection-bridge.d.ts +211 -0
  220. package/dist/src/terminal-native-inspection-bridge.js +1127 -0
  221. package/dist/src/terminal-native-inspection-bridge.js.map +1 -0
  222. package/dist/src/terminal-runtime-cli-adapter.d.ts +2 -2
  223. package/dist/src/terminal-runtime-cli-adapter.js.map +1 -1
  224. package/dist/src/terminal-submission-retry-application.d.ts +24 -0
  225. package/dist/src/terminal-submission-retry-application.js +402 -0
  226. package/dist/src/terminal-submission-retry-application.js.map +1 -0
  227. package/dist/src/terminal-submission-retry-ports.d.ts +30 -0
  228. package/dist/src/terminal-submission-retry-ports.js +2 -0
  229. package/dist/src/terminal-submission-retry-ports.js.map +1 -0
  230. package/dist/src/terminal-submission-retry-reconciliation.d.ts +236 -0
  231. package/dist/src/terminal-submission-retry-reconciliation.js +1130 -0
  232. package/dist/src/terminal-submission-retry-reconciliation.js.map +1 -0
  233. package/dist/src/terminal-submission-retry-transport.d.ts +58 -0
  234. package/dist/src/terminal-submission-retry-transport.js +675 -0
  235. package/dist/src/terminal-submission-retry-transport.js.map +1 -0
  236. package/dist/src/terminal-text-submission-bridge.d.ts +200 -0
  237. package/dist/src/terminal-text-submission-bridge.js +632 -0
  238. package/dist/src/terminal-text-submission-bridge.js.map +1 -0
  239. package/dist/src/terminal-user-explicit-send-policy.d.ts +29 -0
  240. package/dist/src/terminal-user-explicit-send-policy.js +46 -0
  241. package/dist/src/terminal-user-explicit-send-policy.js.map +1 -0
  242. package/dist/src/terminal-watch-cli-adapter.js +91 -143
  243. package/dist/src/terminal-watch-cli-adapter.js.map +1 -1
  244. package/dist/src/terminal-watch-codec.d.ts +9 -0
  245. package/dist/src/terminal-watch-codec.js +1051 -0
  246. package/dist/src/terminal-watch-codec.js.map +1 -0
  247. package/dist/src/terminal-watch-interaction-response-store.d.ts +19 -0
  248. package/dist/src/terminal-watch-interaction-response-store.js +87 -0
  249. package/dist/src/terminal-watch-interaction-response-store.js.map +1 -0
  250. package/dist/src/terminal-watch-record.d.ts +319 -0
  251. package/dist/src/terminal-watch-record.js +742 -0
  252. package/dist/src/terminal-watch-record.js.map +1 -0
  253. package/dist/src/terminal-watch-service.js +48 -27
  254. package/dist/src/terminal-watch-service.js.map +1 -1
  255. package/dist/src/terminal-watch-store.d.ts +3 -279
  256. package/dist/src/terminal-watch-store.js +5 -1779
  257. package/dist/src/terminal-watch-store.js.map +1 -1
  258. package/package.json +7 -2
  259. package/templates/openclaw-skills/agent-knock-knock/SKILL.md +1 -1
@@ -1,358 +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
- import { inspectNativeQuestionnaire } from "./terminal-questionnaire-adapter.js";
9
- import { discoverTerminalModelOptions, inspectTerminalModelControlResidual, observeTerminalModelControl, planTerminalModelControl, probeTerminalModelControl, repairTerminalModelControlResidual, switchTerminalModel } from "./terminal-model-control.js";
10
- // Verified Codex profiles through 0.154.0 keep Enter in paste/newline mode for
11
- // 120ms after burst input. Cross that boundary rather than landing on it, and
12
- // also require observable composer stability instead of treating this delay
13
- // alone as acceptance.
14
- const CODEX_PASTE_ENTER_SETTLE_MS = 121;
15
- const CODEX_MULTILINE_SETTLE_POLL_MS = 30;
16
- // A capture can itself take longer than the old two-second deadline on a busy
17
- // tmux server. Give repaint discovery a bounded five-second window and, once
18
- // an exact frame has been observed, always allow the required stable/final
19
- // captures to complete instead of letting the first I/O consume their budget.
20
- const CODEX_MULTILINE_SETTLE_TIMEOUT_MS = 5_000;
21
- const CODEX_MULTILINE_STABLE_CAPTURES = 2;
22
- const CODEX_EXACT_CANDIDATE_GRACE_CAPTURES = 8;
23
- const CODEX_MULTILINE_SETTLE_SCROLLBACK_LINES = 240;
24
- const CODEX_COMPOSER_MARKER = /^[›»](?:\s|$)/u;
25
- const CODEX_COMPOSER_FOOTER = /^(?:gpt-[\w.-]+(?:\s|$)|[-\w.]+ default ·)/u;
26
- const CODEX_COMPLETE_COMPOSER_FOOTER = /^(?:gpt-[\w.-]+(?:\s+\S+)?|[-\w.]+ default)\s+·\s+\S.*$/u;
27
- const CODEX_MODEL_CONTROL_SLASH_POPUP_BY_PROFILE = {
28
- "codex-model-control-0.154.0": [
29
- " /model choose what model and reasoning effort to use"
30
- ]
31
- };
32
- // Keep every exact slash-completion shape closed per behavior profile so a
33
- // version adding another matching command cannot silently become an authorized
34
- // native command surface.
35
- const CODEX_NATIVE_STATUS_POPUP_BY_PROFILE = {
36
- "codex-tui-0.146.0": [
37
- " /status show current session configuration and token usage"
38
- ],
39
- "codex-tui-0.146.1": [
40
- " /status show current session configuration and token usage"
41
- ],
42
- "codex-tui-0.147.0": [
43
- " /status show current session configuration and token usage",
44
- " /statusline configure which items appear in the status line"
45
- ],
46
- "codex-tui-0.148.0": [
47
- " /status show current session configuration and token usage",
48
- " /statusline configure which items appear in the status line"
49
- ],
50
- "codex-tui-0.149.1": [
51
- " /status show current session configuration and token usage",
52
- " /statusline configure which items appear in the status line"
53
- ],
54
- "codex-tui-0.150.1": [
55
- " /status show current session configuration and token usage",
56
- " /statusline configure which items appear in the status line"
57
- ],
58
- "codex-tui-0.151.0": [
59
- " /status show current session configuration and token usage",
60
- " /statusline configure which items appear in the status line"
61
- ],
62
- "codex-tui-0.153.0": [
63
- " /status show current session configuration and token usage",
64
- " /statusline configure which items appear in the status line"
65
- ],
66
- "codex-tui-0.153.4": [
67
- " /status show current session configuration and token usage",
68
- " /statusline configure which items appear in the status line"
69
- ],
70
- "codex-tui-0.154.0": [
71
- " /status show current session configuration and token usage",
72
- " /statusline configure which items appear in the status line"
73
- ],
74
- "codex-tui-generic-v1": [
75
- " /status show current session configuration and token usage",
76
- " /statusline configure which items appear in the status line"
77
- ]
78
- };
79
- // Codex's verified `/status` profiles are exercised against its canonical
80
- // 80-column status surface. Narrower layouts can truncate the 36-character
81
- // Session UUID after a dynamically sized label column. Exact provider geometry
82
- // is required before input; ANSI visible-buffer width is only a conservative
83
- // fallback diagnostic and never upgrades unknown geometry to safe.
84
- const CODEX_NATIVE_STATUS_MIN_VIEWPORT_BY_PROFILE = {
85
- "codex-tui-0.146.0": 80,
86
- "codex-tui-0.146.1": 80,
87
- "codex-tui-0.147.0": 80,
88
- "codex-tui-0.148.0": 80,
89
- "codex-tui-0.149.1": 80,
90
- "codex-tui-0.150.1": 80,
91
- "codex-tui-0.151.0": 80,
92
- "codex-tui-0.153.0": 80,
93
- "codex-tui-0.153.4": 80,
94
- "codex-tui-0.154.0": 80,
95
- "codex-tui-generic-v1": 80
96
- };
97
- const CLAUDE_NATIVE_STATUS_POPUP_BY_PROFILE = {
98
- "claude-code-2.1.218-native-status": [
99
- "/status Show Claude Code status including version, model, account, API connectivity, and tool statuses",
100
- "/statusline Set up Claude Code's status line UI",
101
- "/ide Manage IDE integrations and show status",
102
- "/usage Show session cost, plan usage, and activity stats"
103
- ],
104
- "claude-code-2.1.226-native-status": [
105
- "/status Show Claude Code status including version, model, account, API connectivity, and tool statuses",
106
- "/statusline Set up Claude Code's status line UI",
107
- "/ide Manage IDE integrations and show status",
108
- "/usage Show session cost, plan usage, and activity stats"
109
- ],
110
- "claude-code-2.1.237-native-status": [
111
- "/status Show Claude Code status including version, model, account, API connectivity, and tool statuses",
112
- "/statusline Set up Claude Code's status line UI",
113
- "/ide Manage IDE integrations and show status",
114
- "/usage Show session cost, plan usage, and activity stats"
115
- ],
116
- "claude-code-2.1.251-native-status": [
117
- "/status Show Claude Code status including version, model, account, API connectivity, and tool statuses",
118
- "/statusline Set up Claude Code's status line UI",
119
- "/ide Manage IDE integrations and show status",
120
- "/usage Show session cost, plan usage, and activity stats"
121
- ],
122
- "claude-code-2.1.259-native-status": [
123
- "/status Show Claude Code status including version, model, account, API connectivity, and tool statuses",
124
- "/statusline Set up Claude Code's status line UI",
125
- "/ide Manage IDE integrations and show status",
126
- "/usage Show session cost, plan usage, and activity stats"
127
- ],
128
- "claude-code-2.1.263-native-status": [
129
- "/status Show Claude Code status including version, model, account, API connectivity, and tool statuses",
130
- "/statusline Set up Claude Code's status line UI",
131
- "/ide Manage IDE integrations and show status",
132
- "/usage Show session cost, plan usage, and activity stats"
133
- ],
134
- "claude-code-2.1.266-native-status": [
135
- "/status Show Claude Code status including version, model, account, API connectivity, and tool statuses",
136
- "/statusline Set up Claude Code's status line UI",
137
- "/ide Manage IDE integrations and show status",
138
- "/usage Show session cost, plan usage, and activity stats"
139
- ],
140
- "claude-code-2.1.267-native-status": [
141
- "/status Show Claude Code status including version, model, account, API connectivity, and tool statuses",
142
- "/statusline Set up Claude Code's status line UI",
143
- "/ide Manage IDE integrations and show status",
144
- "/usage Show session cost, plan usage, and activity stats"
145
- ],
146
- "claude-code-unverified-native-status-v1": [
147
- "/status Show Claude Code status including version, model, account, API connectivity, and tool statuses",
148
- "/statusline Set up Claude Code's status line UI",
149
- "/ide Manage IDE integrations and show status",
150
- "/usage Show session cost, plan usage, and activity stats"
151
- ]
152
- };
153
- const CLAUDE_NATIVE_STATUS_SETTLE_BY_PROFILE = {
154
- "claude-code-2.1.218-native-status": {
155
- minimumStableMs: 80,
156
- maximumSettleMs: 2_000
157
- },
158
- "claude-code-2.1.226-native-status": {
159
- minimumStableMs: 80,
160
- maximumSettleMs: 5_000
161
- },
162
- "claude-code-2.1.237-native-status": {
163
- minimumStableMs: 80,
164
- maximumSettleMs: 5_000
165
- },
166
- "claude-code-2.1.251-native-status": {
167
- minimumStableMs: 80,
168
- maximumSettleMs: 5_000
169
- },
170
- "claude-code-2.1.259-native-status": {
171
- minimumStableMs: 80,
172
- maximumSettleMs: 5_000
173
- },
174
- "claude-code-2.1.263-native-status": {
175
- minimumStableMs: 80,
176
- maximumSettleMs: 5_000
177
- },
178
- "claude-code-2.1.266-native-status": {
179
- minimumStableMs: 80,
180
- maximumSettleMs: 5_000
181
- },
182
- "claude-code-2.1.267-native-status": {
183
- minimumStableMs: 80,
184
- maximumSettleMs: 5_000
185
- },
186
- "claude-code-unverified-native-status-v1": {
187
- minimumStableMs: 80,
188
- maximumSettleMs: 5_000
189
- }
190
- };
191
- const CODEX_LARGE_PASTE_CHAR_THRESHOLD = 1_000;
192
- const TERMINAL_INTERACTION_TTL_MS = 10 * 60 * 1_000;
193
- const TERMINAL_INTERACTION_IDENTIFIER_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/u;
194
- const TERMINAL_INTERACTION_FINGERPRINT_PATTERN = /^[0-9a-f]{64}$/u;
195
- /** A terminal send failed at a boundary that proves input never started. */
196
- export class TerminalInputNotStartedError extends Error {
197
- code = "AKK_TERMINAL_INPUT_NOT_STARTED";
198
- constructor(message, options = {}) {
199
- super(message, options);
200
- this.name = "TerminalInputNotStartedError";
201
- }
202
- }
203
- /**
204
- * Text is already in the terminal composer, but this bridge invocation proved
205
- * that it never called the Enter transport boundary.
206
- *
207
- * Callers may persist this as narrow recovery evidence. It says nothing about
208
- * whether the draft remains exact or whether a later human action submitted
209
- * it; those facts must be re-observed under the terminal lock.
210
- */
211
- export class TerminalEnterDispatchNotAttemptedError extends Error {
212
- code = "AKK_TERMINAL_ENTER_DISPATCH_NOT_ATTEMPTED";
213
- stage = "enter_not_attempted";
214
- constructor(message, options = {}) {
215
- super(message, options);
216
- this.name = "TerminalEnterDispatchNotAttemptedError";
217
- }
218
- }
219
- /**
220
- * The caller's one-shot Enter reservation was invoked, so this recovery
221
- * attempt is permanently consumed even though C-m was not proven dispatched.
222
- */
223
- export class TerminalEnterDispatchReservedError extends Error {
224
- code = "AKK_TERMINAL_ENTER_DISPATCH_RESERVED";
225
- stage = "enter_reserved";
226
- doNotRetry = true;
227
- constructor(message, options = {}) {
228
- super(message, options);
229
- this.name = "TerminalEnterDispatchReservedError";
230
- }
231
- }
232
- /**
233
- * A fail-closed native-inspection submission result.
234
- *
235
- * Only `not_started` proves that a caller may safely retry. Once text has
236
- * reached the composer, or Enter has been attempted, automated retries could
237
- * duplicate input or execute a different command after terminal drift.
238
- */
239
- export class NativeInspectionSubmissionError extends Error {
240
- stage;
241
- code = "AKK_NATIVE_INSPECTION_SUBMISSION_FAILED";
242
- doNotRetry;
243
- constructor(stage, message, options = {}) {
244
- super(message, options);
245
- this.stage = stage;
246
- this.name = "NativeInspectionSubmissionError";
247
- this.doNotRetry = stage !== "not_started";
248
- this.diagnostic = options.diagnostic;
249
- }
250
- diagnostic;
251
- }
252
- class NativeInspectionDiagnosticError extends Error {
253
- diagnostic;
254
- constructor(diagnostic, message, options = {}) {
255
- super(message, options);
256
- this.diagnostic = diagnostic;
257
- this.name = "NativeInspectionDiagnosticError";
258
- }
259
- }
260
- /** A verified modal could not be dismissed across one exact key attempt. */
261
- export class NativeInspectionDismissalError extends Error {
262
- code = "AKK_NATIVE_INSPECTION_DISMISSAL_FAILED";
263
- doNotRetry = true;
264
- constructor(message, options = {}) {
265
- super(message, options);
266
- this.name = "NativeInspectionDismissalError";
267
- }
268
- }
269
- /**
270
- * A response reservation or terminal input may already have happened. The
271
- * same interaction must never be dispatched automatically after this error.
272
- */
273
- export class TerminalInteractionDispatchReservedError extends Error {
274
- stage;
275
- code = "AKK_TERMINAL_INTERACTION_DISPATCH_RESERVED";
276
- doNotRetry = true;
277
- constructor(stage, message, options = {}) {
278
- super(message, options);
279
- this.stage = stage;
280
- this.name = "TerminalInteractionDispatchReservedError";
281
- }
282
- }
283
- /**
284
- * A durable response reservation exists, but the bridge proved that no
285
- * terminal text or key delivery was attempted. The caller must discard the
286
- * one-shot offer and obtain a fresh status projection before trying again.
287
- */
288
- export class TerminalInteractionInputNotStartedError extends Error {
289
- code = "AKK_TERMINAL_INTERACTION_INPUT_NOT_STARTED";
290
- stage = "input_not_started";
291
- doNotRetry = true;
292
- requiresFreshOffer = true;
293
- constructor(message, options = {}) {
294
- super(message, options);
295
- this.name = "TerminalInteractionInputNotStartedError";
296
- }
297
- }
298
- function terminalSendComposerRequirements(agent, multiline, options) {
299
- const exactEmptyRetryAuthority = options.requireExactEmptyComposerAfterBeforeText;
300
- const requireExactEmptyComposerBeforeText = options.requireExactEmptyComposerBeforeText === true;
301
- const userExplicitEnterAfterTextWithoutComposerVeto = options.userExplicitEnterAfterTextWithoutComposerVeto === true;
302
- if (userExplicitEnterAfterTextWithoutComposerVeto &&
303
- (agent !== "codex" || !requireExactEmptyComposerBeforeText)) {
304
- throw new TerminalInputNotStartedError("composer-veto-free Enter requires Codex and an exact-empty pre-text user-explicit boundary");
305
- }
306
- const requireExactComposer = !userExplicitEnterAfterTextWithoutComposerVeto &&
307
- ((agent === "codex" && multiline) ||
308
- options.requireExactComposerBeforeEnter === true ||
309
- requireExactEmptyComposerBeforeText ||
310
- exactEmptyRetryAuthority !== undefined);
311
- if (exactEmptyRetryAuthority !== undefined &&
312
- (agent !== "codex" ||
313
- options.beforeText === undefined ||
314
- !/^[0-9a-f]{64}$/u.test(exactEmptyRetryAuthority.preliminaryComposerDigest))) {
315
- throw new TerminalInputNotStartedError("exact-empty replacement requires Codex, a reservation hook, and a preliminary composer digest");
316
- }
317
- return {
318
- exactEmptyRetryAuthority,
319
- requireExactEmptyComposerBeforeText,
320
- requireExactComposer
321
- };
322
- }
323
- async function recordTerminalSendStage(options, event, preserveUserInput, previousError) {
324
- try {
325
- await options.onTransportStage?.(event);
326
- return previousError;
327
- }
328
- catch (error) {
329
- if (!preserveUserInput)
330
- throw error;
331
- return previousError ?? error;
332
- }
333
- }
334
- async function runTerminalSendBeforeEnter(options, context, preserveUserInput, previousError) {
335
- try {
336
- await options.beforeEnter?.(context);
337
- return previousError;
338
- }
339
- catch (error) {
340
- if (!preserveUserInput)
341
- throw error;
342
- return previousError ?? error;
343
- }
344
- }
345
- async function waitForUserExplicitCodexEnter(input) {
346
- if (!input.enabled)
347
- return;
348
- await input.sleep(Math.max(0, CODEX_PASTE_ENTER_SETTLE_MS -
349
- (input.nowMs() - input.textInjectedAt)));
350
- }
351
- function throwPostTextHookFailureAfterUserEnter(enabled, error) {
352
- if (!enabled || error === undefined)
353
- return;
354
- throw new TerminalEnterDispatchReservedError("user-explicit Codex Enter was dispatched but post-text bookkeeping failed; do not retry", { cause: error });
355
- }
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";
356
16
  function terminalApprovalPreflight(input) {
357
17
  const { adapter, inspection, decision } = input;
358
18
  if (!inspection.approval.approvable) {
@@ -755,534 +415,126 @@ export class TerminalAgentBridge {
755
415
  if (!options.beforeInput) {
756
416
  throw new Error("terminal model control requires current-snapshot authority before every input");
757
417
  }
758
- // `claude agents` deliberately reports the same interactive process as
759
- // `waiting` / `dialog open` while Claude's native model picker is open.
760
- // Keep that lifecycle relaxation private to this model-control operation:
761
- // a capture may cross the state boundary read-only, but a terminal key is
762
- // authorized only by the immediately preceding, exact profiled picker
763
- // frame. The permit is one-shot so no later transport can inherit it.
764
- let claudeModelDialogInputPermit;
765
- let codexModelInputPermit;
766
- const consumeCodexModelInputPermit = () => {
767
- const permit = codexModelInputPermit;
768
- codexModelInputPermit = undefined;
769
- return permit;
770
- };
771
- const capture = async (input) => {
772
- claudeModelDialogInputPermit = undefined;
773
- codexModelInputPermit = undefined;
774
- const original = input.terminalControl;
775
- const verified = await this.verifyModelControlTerminalIdentity(adapter, plan, original, options.runtime, { claudeModelDialog: "allow" });
776
- const styledScreen = await this.terminalProvider.capture(this.terminalProvider.endpoint(verified), { scrollbackLines: 160, preserveEscapes: true });
777
- const screen = stripTerminalEscapeSequences(styledScreen);
778
- const inspection = adapter.inspectScreen({
779
- screen,
780
- runtime: options.runtime
781
- });
782
- const modelObservation = adapter.observeModelControl?.(plan, screen);
783
- const exactCodexModelSurface = adapter.agent === "codex" &&
784
- modelObservation !== undefined &&
785
- modelObservation.state !== "none" &&
786
- modelObservation.state !== "ambiguous";
787
- const exactClaudeModelDialog = adapter.agent === "claude" &&
788
- modelObservation?.state === "claude_model_picker";
789
- const reverified = await this.verifyModelControlTerminalIdentity(adapter, plan, verified, options.runtime, {
790
- claudeModelDialog: exactClaudeModelDialog
791
- ? "require"
792
- : "forbid"
793
- });
794
- if (!sameTerminalControlIdentity(verified, reverified)) {
795
- throw new Error("terminal control identity changed across model-control capture");
796
- }
797
- if (exactClaudeModelDialog) {
798
- claudeModelDialogInputPermit = {
799
- terminalControl: reverified,
800
- fingerprint: modelObservation.fingerprint
801
- };
802
- }
803
- const codexComposer = adapter.agent === "codex"
804
- ? currentCodexComposerCapture(styledScreen, input.expectedComposer ?? "", false, false, input.expectedComposer === plan.command
805
- ? CODEX_MODEL_CONTROL_SLASH_POPUP_BY_PROFILE[plan.behaviorProfile]
806
- : undefined)
807
- : undefined;
808
- let codexInputBlocked = false;
809
- if (adapter.agent === "codex") {
810
- try {
811
- const questionnaire = inspectNativeQuestionnaire({
812
- agent: "codex",
813
- version: "0.154.0",
814
- screen: styledScreen
815
- });
816
- const asyncQuestionMode = inspectCodexAsyncQuestionInputMode(styledScreen);
817
- codexInputBlocked = !exactCodexModelSurface && (questionnaire.status !== "none" ||
818
- codexActiveWriterViewerVisible(styledScreen) ||
819
- asyncQuestionMode === "expanded" ||
820
- asyncQuestionMode === "ambiguous");
821
- }
822
- catch {
823
- codexInputBlocked = !exactCodexModelSurface;
824
- }
825
- }
826
- const exactEmptyComposer = adapter.agent === "codex"
827
- ? codexComposer?.state === "exact_empty"
828
- : isExactClaudeIdleComposer(screen);
829
- const claudeProfiledCommand = adapter.agent === "claude" &&
830
- input.expectedComposer === plan.command &&
831
- exactClaudeModelControlComposerCapture(screen, plan, input.expectedComposer) !== undefined;
832
- const claudeBareCommand = adapter.agent === "claude" &&
833
- input.expectedComposer !== undefined &&
834
- exactTerminalComposerCapture("claude", screen, input.expectedComposer) !== undefined;
835
- const exactCommandComposer = input.expectedComposer !== undefined && (adapter.agent === "codex"
836
- ? codexComposer?.state === "exact_draft"
837
- : claudeProfiledCommand || claudeBareCommand);
838
- const exactCommandReady = adapter.agent === "codex"
839
- ? codexComposer?.state === "exact_draft" &&
840
- codexComposer.profiledSlashPopup === true
841
- : claudeProfiledCommand;
842
- const exactBareCommand = adapter.agent === "codex"
843
- ? codexComposer?.state === "exact_draft" &&
844
- codexComposer.bareCommand === true
845
- : claudeBareCommand && !claudeProfiledCommand;
846
- if (adapter.agent === "codex") {
847
- const exactModelSurface = modelObservation &&
848
- modelObservation.state !== "none" &&
849
- modelObservation.state !== "ambiguous"
850
- ? {
851
- kind: "model_surface",
852
- fingerprint: modelObservation.fingerprint
853
- }
854
- : undefined;
855
- const exactComposerSurface = codexComposer?.state === "exact_empty"
856
- ? {
857
- kind: "composer_empty",
858
- fingerprint: codexComposer.digest
859
- }
860
- : codexComposer?.state === "exact_draft"
861
- ? {
862
- kind: "composer_command",
863
- fingerprint: codexComposer.digest
864
- }
865
- : undefined;
866
- const proof = exactModelSurface ?? exactComposerSurface;
867
- if (proof) {
868
- codexModelInputPermit = {
869
- terminalControl: reverified,
870
- expectedComposer: input.expectedComposer,
871
- ...proof
872
- };
873
- }
874
- }
875
- return {
876
- terminalControl: reverified,
877
- screen,
878
- // The exact profiled picker is the current input owner. Generic
879
- // lifecycle/questionnaire detectors may still see stale transcript
880
- // markers behind that overlay, so keep their result from vetoing the
881
- // model-control state machine. Unknown or partial pickers receive no
882
- // such override and remain fail-closed.
883
- activityState: exactCodexModelSurface
884
- ? "unknown"
885
- : inspection.activity.state,
886
- approvalBlocked: exactCodexModelSurface
887
- ? false
888
- : inspection.approval.blocked,
889
- exactEmptyComposer,
890
- exactCommandReady,
891
- exactCommandComposer,
892
- exactBareCommand,
893
- ...(codexComposer?.state === "exact_draft"
894
- ? { exactCommandFingerprint: codexComposer.digest }
895
- : {}),
896
- inputBlocked: codexInputBlocked
897
- };
898
- };
899
- return {
418
+ return createTerminalModelControlPorts({
419
+ adapter,
420
+ plan,
421
+ runtime: options.runtime,
900
422
  beforeInput: options.beforeInput,
901
423
  loadCodexCatalog: options.loadCodexCatalog,
902
- capture,
903
- sendText: async (control, text) => {
904
- claudeModelDialogInputPermit = undefined;
905
- if (adapter.agent === "codex") {
906
- const permit = consumeCodexModelInputPermit();
907
- if (!permit || permit.kind !== "composer_empty") {
908
- throw new Error("Codex /model text requires one fresh exact empty-composer permit");
909
- }
910
- const fresh = await capture({
911
- terminalControl: control,
912
- expectedComposer: permit.expectedComposer
913
- });
914
- const freshPermit = consumeCodexModelInputPermit();
915
- if (!freshPermit ||
916
- freshPermit.kind !== permit.kind ||
917
- freshPermit.fingerprint !== permit.fingerprint ||
918
- !sameTerminalControlIdentity(permit.terminalControl, freshPermit.terminalControl) ||
919
- fresh.inputBlocked === true ||
920
- fresh.approvalBlocked ||
921
- fresh.activityState === "working" ||
922
- fresh.activityState === "awaiting_approval" ||
923
- !fresh.exactEmptyComposer) {
924
- throw new Error("the exact Codex empty composer changed before /model text delivery");
925
- }
926
- await this.terminalProvider.sendText(this.terminalProvider.endpoint(freshPermit.terminalControl), text);
927
- return;
928
- }
929
- const verified = await this.verifyTerminalIdentity(adapter.agent, control, options.runtime);
930
- await this.terminalProvider.sendText(this.terminalProvider.endpoint(verified), text);
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
931
430
  },
932
- sendKeys: async (control, keys) => {
933
- const requestedControl = control;
934
- if (adapter.agent === "codex") {
935
- const permit = consumeCodexModelInputPermit();
936
- claudeModelDialogInputPermit = undefined;
937
- if (!permit || !sameTerminalControlIdentity(permit.terminalControl, requestedControl)) {
938
- throw new Error("Codex model-control key requires one fresh exact input-surface permit");
939
- }
940
- const fresh = await capture({
941
- terminalControl: requestedControl,
942
- expectedComposer: permit.expectedComposer
943
- });
944
- const freshPermit = consumeCodexModelInputPermit();
945
- if (!freshPermit ||
946
- freshPermit.kind !== permit.kind ||
947
- freshPermit.fingerprint !== permit.fingerprint ||
948
- !sameTerminalControlIdentity(permit.terminalControl, freshPermit.terminalControl) ||
949
- fresh.inputBlocked === true ||
950
- fresh.approvalBlocked ||
951
- fresh.activityState === "working" ||
952
- fresh.activityState === "awaiting_approval") {
953
- throw new Error("the exact Codex model-control input surface changed before key dispatch");
954
- }
955
- await this.terminalProvider.sendKeys(this.terminalProvider.endpoint(freshPermit.terminalControl), keys);
956
- return;
957
- }
958
- const permit = claudeModelDialogInputPermit;
959
- claudeModelDialogInputPermit = undefined;
960
- const requireClaudeModelDialog = permit !== undefined &&
961
- sameTerminalControlIdentity(permit.terminalControl, requestedControl);
962
- const verified = await this.verifyModelControlTerminalIdentity(adapter, plan, requestedControl, options.runtime, {
963
- claudeModelDialog: requireClaudeModelDialog
964
- ? "require"
965
- : "forbid"
966
- });
967
- if (requireClaudeModelDialog) {
968
- const styledScreen = await this.terminalProvider.capture(this.terminalProvider.endpoint(verified), { scrollbackLines: 160, preserveEscapes: true });
969
- const screen = stripTerminalEscapeSequences(styledScreen);
970
- const inspection = adapter.inspectScreen({
971
- screen,
972
- runtime: options.runtime
973
- });
974
- const observation = adapter.observeModelControl?.(plan, screen);
975
- if (inspection.approval.blocked ||
976
- inspection.activity.state === "working" ||
977
- observation?.state !== "claude_model_picker" ||
978
- observation.fingerprint !== permit?.fingerprint) {
979
- throw new Error("the exact Claude model picker changed before key dispatch");
431
+ classifiers: {
432
+ stripEscapes: stripTerminalEscapeSequences,
433
+ sameIdentity: sameTerminalControlIdentity,
434
+ currentCodexComposer: currentCodexComposerCapture,
435
+ inspectCodexAsyncQuestionInputMode,
436
+ codexActiveWriterViewerVisible,
437
+ isExactClaudeIdleComposer,
438
+ exactClaudeModelControlComposer: exactClaudeModelControlComposerCapture,
439
+ exactTerminalComposer: exactTerminalComposerCapture
440
+ }
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
+ });
980
481
  }
981
- }
982
- await this.terminalProvider.sendKeys(this.terminalProvider.endpoint(verified), keys);
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)
983
493
  },
984
- sleep: this.sleep
985
- };
986
- }
987
- async verifyModelControlTerminalIdentity(adapter, plan, terminalControl, runtime, options) {
988
- const canUseClaudeModelDialog = adapter.agent === "claude" &&
989
- plan.behaviorProfile === "claude-model-control-2.1.266" &&
990
- runtime?.requireExactClaudeAgentRow === true &&
991
- runtime.exactClaudeAgentState === "idle";
992
- if (options.claudeModelDialog === "require") {
993
- if (!canUseClaudeModelDialog) {
994
- throw new Error("Claude model-dialog identity cannot be required outside its exact profile");
995
- }
996
- return this.verifyTerminalIdentity(adapter.agent, terminalControl, { ...runtime, exactClaudeAgentState: "status_dialog" });
997
- }
998
- try {
999
- return await this.verifyTerminalIdentity(adapter.agent, terminalControl, runtime);
1000
- }
1001
- catch (idleError) {
1002
- if (options.claudeModelDialog !== "allow" ||
1003
- !canUseClaudeModelDialog) {
1004
- throw idleError;
1005
- }
1006
- try {
1007
- return await this.verifyTerminalIdentity(adapter.agent, terminalControl, { ...runtime, exactClaudeAgentState: "status_dialog" });
494
+ classifiers: {
495
+ sameIdentity: sameTerminalControlIdentity,
496
+ stripEscapes: stripTerminalEscapeSequences,
497
+ codexBlockingModalVisible,
498
+ inspectCodexAsyncQuestionInputMode,
499
+ currentCodexComposer: currentCodexComposerCapture,
500
+ exactTerminalComposer: exactTerminalComposerCapture,
501
+ exactClaudeComposer: exactClaudeComposerCapture,
502
+ exactClaudeInjectedPastePlaceholder: exactClaudeInjectedPastePlaceholderCapture
1008
503
  }
1009
- catch {
1010
- // Preserve the original current-snapshot failure. Both attempts use
1011
- // the same PID, process incarnation, native Session, cwd, and terminal
1012
- // control fence; the second changes only the exact allowed agents row
1013
- // state from idle to `waiting` / `dialog open`.
1014
- throw idleError;
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)
1015
527
  }
1016
- }
528
+ });
1017
529
  }
1018
530
  async send(agent, terminalControl, text, options = {}) {
1019
531
  const adapter = this.registry.require(agent);
1020
- const multiline = /[\r\n]/u.test(text.trimEnd());
1021
- const { exactEmptyRetryAuthority, requireExactEmptyComposerBeforeText, requireExactComposer } = terminalSendComposerRequirements(adapter.agent, multiline, options);
1022
- try {
1023
- assertTerminalMutationCapabilities({
1024
- provider: this.terminalProvider,
1025
- terminal: this.terminalProvider.endpoint(terminalControl),
1026
- semantic: [
1027
- "send_keys",
1028
- ...(requireExactComposer || exactEmptyRetryAuthority
1029
- ? ["screen_status"]
1030
- : [])
1031
- ],
1032
- transport: [
1033
- "stable_resource_resolution",
1034
- "text_delivery",
1035
- "key_delivery",
1036
- ...(requireExactComposer || exactEmptyRetryAuthority
1037
- ? ["screen_capture"]
1038
- : [])
1039
- ]
1040
- });
1041
- }
1042
- catch (error) {
1043
- throw new TerminalInputNotStartedError(error instanceof Error ? error.message : String(error), { cause: error });
1044
- }
1045
- const normalized = text.trimEnd();
1046
- if (!normalized) {
1047
- throw new TerminalInputNotStartedError("terminal message is empty");
1048
- }
1049
- let verifiedForText;
1050
- try {
1051
- verifiedForText = await this.verifyTerminalIdentity(adapter.agent, terminalControl, options.runtime);
1052
- }
1053
- catch (error) {
1054
- throw new TerminalInputNotStartedError(error instanceof Error ? error.message : String(error), { cause: error });
1055
- }
1056
- if (!verifiedForText.capabilities.includes("send_keys")) {
1057
- throw new TerminalInputNotStartedError(`${adapter.displayName} terminal input capability changed before injection`);
1058
- }
1059
- let composerVerifiedImmediatelyBeforeText = false;
1060
- if (exactEmptyRetryAuthority) {
1061
- try {
1062
- // Invoking this retry-only hook consumes the durable attempt. The
1063
- // independently identity-fenced recapture closes the human-edit race
1064
- // before any physical text delivery.
1065
- await options.beforeText?.({
1066
- agent: adapter.agent,
1067
- terminalControl: verifiedForText,
1068
- multiline,
1069
- text: normalized
1070
- });
1071
- const finalEmpty = await this.captureCodexComposerSnapshot(adapter, verifiedForText, normalized, options.runtime);
1072
- if (finalEmpty.state !== "exact_empty" ||
1073
- finalEmpty.digest !==
1074
- exactEmptyRetryAuthority.preliminaryComposerDigest ||
1075
- !sameTerminalControlIdentity(verifiedForText, finalEmpty.terminalControl)) {
1076
- const reason = "reason" in finalEmpty
1077
- ? finalEmpty.reason
1078
- : `Codex composer is ${finalEmpty.state}`;
1079
- throw new Error(`reserved Codex replacement lost its exact-empty composer: ${reason}`);
1080
- }
1081
- verifiedForText = finalEmpty.terminalControl;
1082
- }
1083
- catch (error) {
1084
- throw new TerminalEnterDispatchReservedError(error instanceof Error ? error.message : String(error), { cause: error });
1085
- }
1086
- }
1087
- else {
1088
- try {
1089
- await options.beforeText?.({
1090
- agent: adapter.agent,
1091
- terminalControl: verifiedForText,
1092
- multiline,
1093
- text: normalized
1094
- });
1095
- }
1096
- catch (error) {
1097
- throw new TerminalInputNotStartedError(error instanceof Error ? error.message : String(error), { cause: error });
1098
- }
1099
- if (requireExactEmptyComposerBeforeText) {
1100
- try {
1101
- verifiedForText = await this.verifyExactEmptyComposerBeforeText(adapter, verifiedForText, normalized, options.runtime, options.allowWorkingComposerForUserExplicit === true);
1102
- composerVerifiedImmediatelyBeforeText = true;
1103
- }
1104
- catch (error) {
1105
- throw new TerminalInputNotStartedError(error instanceof Error ? error.message : String(error), { cause: error });
1106
- }
1107
- }
1108
- }
1109
- if (!composerVerifiedImmediatelyBeforeText) {
1110
- try {
1111
- const reverifiedForText = await this.verifyTerminalIdentity(adapter.agent, verifiedForText, options.runtime);
1112
- if (!sameTerminalControlIdentity(verifiedForText, reverifiedForText)) {
1113
- throw new Error("terminal identity changed after the final pre-text check");
1114
- }
1115
- verifiedForText = reverifiedForText;
1116
- }
1117
- catch (error) {
1118
- const message = error instanceof Error ? error.message : String(error);
1119
- if (exactEmptyRetryAuthority) {
1120
- throw new TerminalEnterDispatchReservedError(message, {
1121
- cause: error
1122
- });
1123
- }
1124
- throw new TerminalInputNotStartedError(message, { cause: error });
1125
- }
1126
- }
1127
- try {
1128
- await this.terminalProvider.sendText(this.terminalProvider.endpoint(verifiedForText), normalized);
1129
- }
1130
- catch (error) {
1131
- if (exactEmptyRetryAuthority) {
1132
- throw new TerminalEnterDispatchReservedError("reserved Codex replacement text delivery outcome is uncertain; do not retry", { cause: error });
1133
- }
1134
- if (error instanceof TerminalControlInputNotSentError) {
1135
- throw new TerminalInputNotStartedError(error.message, {
1136
- cause: error
1137
- });
1138
- }
1139
- throw error;
1140
- }
1141
- const textInjectedAt = this.nowMs();
1142
- const preserveUserInput = options.userExplicitEnterAfterTextWithoutComposerVeto === true;
1143
- const allowClaudeInjectedPastePlaceholder = claudeInjectedPasteProofEligible({
1144
- agent: adapter.agent,
1145
- multiline,
1146
- composerVerifiedImmediatelyBeforeText,
1147
- terminalControl: verifiedForText
1148
- });
1149
- let postTextHookError;
1150
- let verifiedForEnter;
1151
- let claudeComposerProof;
1152
- try {
1153
- postTextHookError = await recordTerminalSendStage(options, {
1154
- stage: "text_injected",
1155
- agent: adapter.agent,
1156
- terminalControl: verifiedForText,
1157
- multiline
1158
- }, preserveUserInput);
1159
- // Text and Enter are separate transport operations. Autonomous sends
1160
- // retain their exact Composer proof. A user-explicit managed Codex Send
1161
- // crosses only the native paste-suppression window, then keeps terminal
1162
- // identity as its final physical boundary.
1163
- if (adapter.agent === "codex" && requireExactComposer) {
1164
- verifiedForEnter = await this.settleCodexMultilineComposer(adapter, terminalControl, normalized, options.runtime, {
1165
- allowOpaqueLargePastePlaceholder: exactEmptyRetryAuthority === undefined,
1166
- allowWorkingComposer: options.allowWorkingComposerForUserExplicit === true
1167
- });
1168
- }
1169
- else if (allowClaudeInjectedPastePlaceholder) {
1170
- claudeComposerProof = await settleClaudeInjectedPasteComposer({
1171
- terminalControl: verifiedForText,
1172
- expectedText: normalized,
1173
- runtime: options.runtime,
1174
- allowWorkingComposer: options.allowWorkingComposerForUserExplicit === true,
1175
- ports: this.claudeInjectedPasteProofPorts(adapter)
1176
- });
1177
- verifiedForEnter = claudeComposerProof.terminalControl;
1178
- }
1179
- else if (requireExactComposer) {
1180
- verifiedForEnter = await this.verifyExactComposerBeforeEnter(adapter, terminalControl, normalized, options.runtime, options.allowWorkingComposerForUserExplicit === true);
1181
- }
1182
- else {
1183
- await waitForUserExplicitCodexEnter({
1184
- enabled: preserveUserInput,
1185
- textInjectedAt,
1186
- nowMs: this.nowMs,
1187
- sleep: this.sleep
1188
- });
1189
- verifiedForEnter = await this.verifyTerminalIdentity(adapter.agent, terminalControl, options.runtime);
1190
- }
1191
- postTextHookError = await runTerminalSendBeforeEnter(options, {
1192
- agent: adapter.agent,
1193
- terminalControl: verifiedForEnter,
1194
- multiline,
1195
- text: normalized
1196
- }, preserveUserInput, postTextHookError);
1197
- // `beforeEnter` may await Store and terminal identity fences. The human
1198
- // can still edit the composer or change the native thread while that
1199
- // callback is pending, so recapture before attempting Enter.
1200
- if (requireExactComposer) {
1201
- if (adapter.agent === "codex") {
1202
- verifiedForEnter = await this.settleCodexMultilineComposer(adapter, terminalControl, normalized, options.runtime, {
1203
- allowOpaqueLargePastePlaceholder: exactEmptyRetryAuthority === undefined,
1204
- allowWorkingComposer: options.allowWorkingComposerForUserExplicit === true
1205
- });
1206
- }
1207
- else if (allowClaudeInjectedPastePlaceholder && claudeComposerProof) {
1208
- verifiedForEnter = await revalidateClaudeComposerProof({
1209
- proof: claudeComposerProof,
1210
- expectedText: normalized,
1211
- runtime: options.runtime,
1212
- allowWorkingComposer: options.allowWorkingComposerForUserExplicit === true,
1213
- ports: this.claudeInjectedPasteProofPorts(adapter)
1214
- });
1215
- }
1216
- else {
1217
- verifiedForEnter = await this.verifyExactComposerBeforeEnter(adapter, terminalControl, normalized, options.runtime, options.allowWorkingComposerForUserExplicit === true);
1218
- }
1219
- }
1220
- else if (options.beforeEnter) {
1221
- verifiedForEnter = await this.verifyTerminalIdentity(adapter.agent, terminalControl, options.runtime);
1222
- }
1223
- }
1224
- catch (error) {
1225
- if (exactEmptyRetryAuthority) {
1226
- if (error instanceof TerminalEnterDispatchReservedError) {
1227
- throw error;
1228
- }
1229
- throw new TerminalEnterDispatchReservedError(error instanceof Error ? error.message : String(error), { cause: error });
1230
- }
1231
- if (error instanceof TerminalEnterDispatchNotAttemptedError) {
1232
- throw error;
1233
- }
1234
- throw new TerminalEnterDispatchNotAttemptedError(error instanceof Error ? error.message : String(error), { cause: error });
1235
- }
1236
- // Do not include this transport call (or anything after it) in the typed
1237
- // pre-Enter proof: a thrown key-delivery error may still have emitted C-m.
1238
- try {
1239
- await this.terminalProvider.sendKeys(this.terminalProvider.endpoint(verifiedForEnter), ["C-m"]);
1240
- postTextHookError = await recordTerminalSendStage(options, {
1241
- stage: "enter_dispatched",
1242
- agent: adapter.agent,
1243
- terminalControl: verifiedForEnter,
1244
- multiline
1245
- }, preserveUserInput, postTextHookError);
1246
- throwPostTextHookFailureAfterUserEnter(preserveUserInput, postTextHookError);
1247
- }
1248
- catch (error) {
1249
- if (exactEmptyRetryAuthority) {
1250
- throw new TerminalEnterDispatchReservedError("reserved Codex Enter dispatch outcome is uncertain; do not retry", { cause: error });
1251
- }
1252
- throw error;
1253
- }
1254
- return {
1255
- stage: "enter_dispatched",
1256
- agent: adapter.agent,
1257
- terminalControl: verifiedForEnter,
1258
- multiline
1259
- };
532
+ return this.textSubmissionBridge().send(adapter, terminalControl, text, options);
1260
533
  }
1261
534
  /** Observe only the current, live Codex composer without exposing its text. */
1262
535
  async observeCodexComposer(terminalControl, expectedText, options = {}) {
1263
536
  const adapter = this.registry.require("codex");
1264
- const normalized = expectedText.trimEnd();
1265
- if (!normalized) {
1266
- return {
1267
- state: "unavailable",
1268
- reason: "expected Codex draft is empty"
1269
- };
1270
- }
1271
- try {
1272
- assertTerminalMutationCapabilities({
1273
- provider: this.terminalProvider,
1274
- terminal: this.terminalProvider.endpoint(terminalControl),
1275
- semantic: ["screen_status"],
1276
- transport: ["stable_resource_resolution", "screen_capture"]
1277
- });
1278
- }
1279
- catch (error) {
1280
- return {
1281
- state: "unavailable",
1282
- reason: error instanceof Error ? error.message : String(error)
1283
- };
1284
- }
1285
- return this.settleCodexComposerObservation(adapter, terminalControl, normalized, options.runtime, { minimumStableMs: CODEX_MULTILINE_SETTLE_POLL_MS });
537
+ return this.textSubmissionBridge().observeCodexComposer(adapter, terminalControl, expectedText, options.runtime);
1286
538
  }
1287
539
  /**
1288
540
  * Honor one explicit user Send against the current Codex terminal. The
@@ -1470,6 +722,7 @@ export class TerminalAgentBridge {
1470
722
  */
1471
723
  async submitExactCodexDraft(terminalControl, expectedText, options) {
1472
724
  const adapter = this.registry.require("codex");
725
+ const textSubmission = this.textSubmissionBridge();
1473
726
  const normalized = expectedText.trimEnd();
1474
727
  if (!normalized) {
1475
728
  throw new TerminalEnterDispatchNotAttemptedError("expected Codex draft is empty");
@@ -1486,7 +739,7 @@ export class TerminalAgentBridge {
1486
739
  "key_delivery"
1487
740
  ]
1488
741
  });
1489
- const observation = await this.settleCodexComposerObservation(adapter, terminalControl, normalized, options.runtime, {
742
+ const observation = await textSubmission.settleCodexComposerObservation(adapter, terminalControl, normalized, options.runtime, {
1490
743
  minimumStableMs: CODEX_PASTE_ENTER_SETTLE_MS,
1491
744
  requiredState: "exact_draft"
1492
745
  });
@@ -1514,7 +767,7 @@ export class TerminalAgentBridge {
1514
767
  text: normalized,
1515
768
  composerDigest: preliminaryObservation.digest
1516
769
  });
1517
- const recaptured = await this.captureCodexComposerSnapshot(adapter, preliminaryObservation.terminalControl, normalized, options.runtime);
770
+ const recaptured = await textSubmission.captureCodexComposerSnapshot(adapter, preliminaryObservation.terminalControl, normalized, options.runtime);
1518
771
  if (recaptured.state !== "exact_draft" ||
1519
772
  recaptured.digest !== preliminaryObservation.digest ||
1520
773
  !sameTerminalControlIdentity(preliminaryObservation.terminalControl, recaptured.terminalControl)) {
@@ -1543,66 +796,6 @@ export class TerminalAgentBridge {
1543
796
  enterCount: 1
1544
797
  };
1545
798
  }
1546
- async verifyExactComposerBeforeEnter(adapter, terminalControl, expectedText, runtime, allowWorkingComposer = false) {
1547
- const captured = await this.captureInspection(adapter, terminalControl, {
1548
- runtime,
1549
- scrollbackLines: CODEX_MULTILINE_SETTLE_SCROLLBACK_LINES
1550
- });
1551
- const exactComposer = exactTerminalComposerCapture(adapter.agent, captured.screen, expectedText);
1552
- if (captured.inspection.approval.blocked ||
1553
- !(["idle", "unknown"].includes(captured.inspection.activity.state) ||
1554
- (allowWorkingComposer &&
1555
- captured.inspection.activity.state === "working")) ||
1556
- !exactComposer) {
1557
- throw new Error(`${adapter.displayName} composer was not exact and idle immediately before Enter`);
1558
- }
1559
- const verifiedImmediatelyBeforeEnter = await this.verifyTerminalIdentity(adapter.agent, captured.terminalControl, runtime);
1560
- if (!sameTerminalControlIdentity(captured.terminalControl, verifiedImmediatelyBeforeEnter)) {
1561
- throw new Error("terminal control identity changed after the final exact composer capture");
1562
- }
1563
- return verifiedImmediatelyBeforeEnter;
1564
- }
1565
- claudeInjectedPasteProofPorts(adapter) {
1566
- return {
1567
- nowMs: this.nowMs,
1568
- sleep: this.sleep,
1569
- capture: (terminalControl, runtime) => this.captureInspection(adapter, terminalControl, {
1570
- runtime,
1571
- scrollbackLines: CODEX_MULTILINE_SETTLE_SCROLLBACK_LINES
1572
- }),
1573
- verifyIdentity: (terminalControl, runtime) => this.verifyTerminalIdentity(adapter.agent, terminalControl, runtime),
1574
- exactDraft: exactClaudeComposerCapture,
1575
- exactInjectedPastePlaceholder: exactClaudeInjectedPastePlaceholderCapture
1576
- };
1577
- }
1578
- async verifyExactEmptyComposerBeforeText(adapter, terminalControl, expectedText, runtime, allowWorkingComposer = false) {
1579
- if (adapter.agent === "codex") {
1580
- const snapshot = await this.captureCodexComposerSnapshot(adapter, terminalControl, expectedText, runtime);
1581
- if (snapshot.state !== "exact_empty") {
1582
- const reason = "reason" in snapshot
1583
- ? snapshot.reason
1584
- : `Codex composer is ${snapshot.state}`;
1585
- throw new Error(`Codex composer was not exactly empty immediately before text: ${reason}`);
1586
- }
1587
- return snapshot.terminalControl;
1588
- }
1589
- const captured = await this.captureInspection(adapter, terminalControl, {
1590
- runtime,
1591
- scrollbackLines: CODEX_MULTILINE_SETTLE_SCROLLBACK_LINES
1592
- });
1593
- if (captured.inspection.approval.blocked ||
1594
- !(["idle", "unknown"].includes(captured.inspection.activity.state) ||
1595
- (allowWorkingComposer &&
1596
- captured.inspection.activity.state === "working")) ||
1597
- !exactTerminalComposerCapture(adapter.agent, captured.screen, "")) {
1598
- throw new Error(`${adapter.displayName} composer was not exactly empty immediately before text`);
1599
- }
1600
- const verified = await this.verifyTerminalIdentity(adapter.agent, captured.terminalControl, runtime);
1601
- if (!sameTerminalControlIdentity(captured.terminalControl, verified)) {
1602
- throw new Error("terminal control identity changed after the final empty composer capture");
1603
- }
1604
- return verified;
1605
- }
1606
799
  /**
1607
800
  * Submit one closed, adapter-owned native inspection command.
1608
801
  *
@@ -1611,697 +804,34 @@ export class TerminalAgentBridge {
1611
804
  * draft untouched instead of issuing the legacy best-effort C-u cleanup.
1612
805
  */
1613
806
  async submitNativeInspection(agent, terminalControl, plan, options = {}) {
1614
- return this.submitClosedNativeInspection(agent, terminalControl, plan, options, { requireCodexReadyComposer: agent === "codex" });
807
+ return this.nativeInspectionBridge().submitNativeInspection(agent, terminalControl, plan, options);
1615
808
  }
1616
809
  /**
1617
810
  * Submit Codex's closed, version-profiled `/status` probe.
1618
811
  *
1619
812
  * Unlike the generic native-inspection entry point, callers provide only
1620
- * the detected Codex version, never a command or plan. The bridge proves an
1621
- * exact empty (or fully dim replace-on-type) ANSI composer before injecting
1622
- * text, then crosses Codex's paste suppression window under the same exact
1623
- * composer and terminal-identity fences used by native inspection.
813
+ * the detected Codex version, never a command or plan.
1624
814
  */
1625
815
  async submitCodexStatusProbe(terminalControl, agentVersion, options = {}) {
1626
- const adapter = this.registry.require("codex");
1627
- let plan;
1628
- try {
1629
- const capability = adapter.probeNativeInspection?.(agentVersion);
1630
- if (capability?.status !== "supported" ||
1631
- capability.statusInspection !== true) {
1632
- throw new NativeInspectionDiagnosticError("unsupported_profile", capability?.reason ??
1633
- `Codex ${agentVersion} has no closed /status behavior profile`);
1634
- }
1635
- const planned = adapter.planNativeInspection?.({ kind: "status" }, capability);
1636
- if (!planned) {
1637
- throw new NativeInspectionDiagnosticError("unsupported_profile", `Codex ${agentVersion} did not produce a closed /status plan`);
1638
- }
1639
- plan = planned;
1640
- }
1641
- catch (error) {
1642
- throw nativeInspectionSubmissionError("not_started", error, "unsupported_profile");
1643
- }
1644
- const result = await this.submitClosedNativeInspection("codex", terminalControl, plan, options, { requireCodexReadyComposer: true });
1645
- if (!result.preTextScreenDigest) {
1646
- throw new Error("Codex /status pre-text composer evidence is missing");
1647
- }
1648
- return {
1649
- ...result,
1650
- agent: "codex",
1651
- preTextScreenDigest: result.preTextScreenDigest,
1652
- observationBaselineDigest: bareDigestFromNativeInspectionScreenFingerprint(result.preEnterScreenDigest),
1653
- observationScrollbackLines: CODEX_MULTILINE_SETTLE_SCROLLBACK_LINES
1654
- };
816
+ return this.nativeInspectionBridge().submitCodexStatusProbe(terminalControl, agentVersion, options);
817
+ }
818
+ async observeNativeInspection(agent, terminalControl, request, options = {}) {
819
+ return this.nativeInspectionBridge().observeNativeInspection(agent, terminalControl, request, options);
820
+ }
821
+ /**
822
+ * Dismiss one exact adapter-owned modal result after re-observing the same
823
+ * evidence under the terminal identity fence. There is exactly one key
824
+ * attempt and no automated retry across an uncertain dismissal boundary.
825
+ */
826
+ async dismissNativeInspection(agent, terminalControl, plan, request, expectedEvidenceFingerprint, options = {}) {
827
+ return this.nativeInspectionBridge().dismissNativeInspection(agent, terminalControl, plan, request, expectedEvidenceFingerprint, options);
1655
828
  }
1656
- async submitClosedNativeInspection(agent, terminalControl, plan, options, safety = {}) {
1657
- const adapter = this.registry.require(agent);
1658
- try {
1659
- assertClosedStatusInspectionPlan(adapter, terminalControl, plan);
1660
- assertTerminalMutationCapabilities({
1661
- provider: this.terminalProvider,
1662
- terminal: this.terminalProvider.endpoint(terminalControl),
1663
- semantic: ["send_keys", "screen_status"],
1664
- transport: [
1665
- "stable_resource_resolution",
1666
- "screen_capture",
1667
- "text_delivery",
1668
- "key_delivery",
1669
- ...(safety.requireCodexReadyComposer
1670
- ? ["ansi_capture"]
1671
- : [])
1672
- ]
1673
- });
1674
- }
1675
- catch (error) {
1676
- throw nativeInspectionSubmissionError("not_started", error, "capability_unavailable");
1677
- }
1678
- let verifiedForText;
1679
- try {
1680
- verifiedForText = await this.verifyTerminalIdentity(adapter.agent, terminalControl, options.runtime);
1681
- }
1682
- catch (error) {
1683
- throw nativeInspectionSubmissionError("not_started", error, "identity_unverified");
1684
- }
1685
- let preTextScreenDigest;
1686
- if (safety.requireCodexReadyComposer) {
1687
- try {
1688
- const ready = await this.captureCodexReadyComposer(adapter, verifiedForText, plan, options.runtime);
1689
- verifiedForText = ready.terminalControl;
1690
- preTextScreenDigest = ready.screenDigest;
1691
- }
1692
- catch (error) {
1693
- throw nativeInspectionSubmissionError("not_started", error, "composer_not_ready");
1694
- }
1695
- }
1696
- try {
1697
- if (!verifiedForText.capabilities.includes("send_keys")) {
1698
- throw new TerminalControlInputNotSentError(`${adapter.displayName} native inspection input capability changed`);
1699
- }
1700
- await this.terminalProvider.sendText(this.terminalProvider.endpoint(verifiedForText), plan.command);
1701
- }
1702
- catch (error) {
1703
- if (error instanceof TerminalControlInputNotSentError) {
1704
- throw nativeInspectionSubmissionError("not_started", error, "text_delivery_unproven");
1705
- }
1706
- // The transport cannot prove whether an untyped failure happened before
1707
- // or after tmux accepted the literal input. Fail closed as injected.
1708
- throw nativeInspectionSubmissionError("text_injected", error, "text_delivery_unproven");
1709
- }
1710
- let settled;
1711
- try {
1712
- settled = await this.settleNativeInspectionComposer(adapter, terminalControl, plan, options.runtime);
1713
- await options.beforeEnter?.({
1714
- agent: adapter.agent,
1715
- terminalControl: settled.terminalControl,
1716
- plan,
1717
- preEnterScreenDigest: settled.screenDigest,
1718
- materialization: settled.materialization
1719
- });
1720
- if (safety.requireCodexReadyComposer) {
1721
- settled = {
1722
- ...settled,
1723
- terminalControl: await this.assertFinalCodexStatusViewport(settled.terminalControl, plan, options.runtime)
1724
- };
1725
- }
1726
- // Viewport inspection may await provider I/O and the PTY may receive
1727
- // human input while that proof is in flight. Keep the exact composer
1728
- // capture as the final substantive asynchronous evidence before the
1729
- // single Enter attempt.
1730
- settled = await this.revalidateNativeInspectionComposer(adapter, settled.terminalControl, plan, settled.materialization, options.runtime);
1731
- }
1732
- catch (error) {
1733
- throw nativeInspectionSubmissionError("text_injected", error, "composer_not_exact");
1734
- }
1735
- try {
1736
- // Exactly one Enter attempt. Any error is submission-uncertain and must
1737
- // never trigger a fallback executable or a blind second Enter.
1738
- await this.terminalProvider.sendKeys(this.terminalProvider.endpoint(settled.terminalControl), ["C-m"]);
1739
- }
1740
- catch (error) {
1741
- throw nativeInspectionSubmissionError("enter_uncertain", error, "enter_uncertain");
1742
- }
1743
- return {
1744
- stage: "enter_dispatched",
1745
- agent: adapter.agent,
1746
- terminalControl: settled.terminalControl,
1747
- command: plan.command,
1748
- behaviorProfile: plan.behaviorProfile,
1749
- preEnterScreenDigest: settled.screenDigest,
1750
- preEnterEvidenceInventory: settled.evidenceInventory,
1751
- materialization: settled.materialization,
1752
- enterCount: 1,
1753
- ...(preTextScreenDigest ? { preTextScreenDigest } : {})
1754
- };
1755
- }
1756
- async observeNativeInspection(agent, terminalControl, request, options = {}) {
1757
- const adapter = this.registry.require(agent);
1758
- if (!adapter.observeNativeInspection) {
1759
- throw new Error(`${adapter.displayName} native inspection observation is not supported`);
1760
- }
1761
- const captured = await this.captureInspection(adapter, terminalControl, {
1762
- runtime: options.runtime,
1763
- scrollbackLines: options.scrollbackLines
1764
- });
1765
- const screenDigest = nativeInspectionScreenFingerprint(captured.screen);
1766
- const adapterObservation = adapter.observeNativeInspection({
1767
- operation: request.operation,
1768
- previousScreenFingerprint: request.previousScreenFingerprint,
1769
- preEnterEvidenceInventory: request.preEnterEvidenceInventory,
1770
- expectedNativeThreadId: request.expectedNativeThreadId,
1771
- expectedAgentVersion: request.expectedAgentVersion,
1772
- expectedCwd: request.expectedCwd,
1773
- screen: captured.screen
1774
- });
1775
- const observation = adapter.agent === "claude" &&
1776
- options.runtime?.requireExactClaudeAgentRow === true &&
1777
- adapterObservation.status === "observed" &&
1778
- adapterObservation.evidence === "claude_status_panel"
1779
- ? {
1780
- ...adapterObservation,
1781
- evidence: "claude_status_panel+claude_agents_exact_pid"
1782
- }
1783
- : adapterObservation;
1784
- return {
1785
- terminalControl: captured.terminalControl,
1786
- status: statusFromInspection(adapter, captured.terminalControl, captured.inspection, { screen: captured.screen, runtime: options.runtime }),
1787
- screenDigest,
1788
- observation
1789
- };
1790
- }
1791
- /**
1792
- * Dismiss one exact adapter-owned modal result after re-observing the same
1793
- * evidence under the terminal identity fence. There is exactly one key
1794
- * attempt and no automated retry across an uncertain dismissal boundary.
1795
- */
1796
- async dismissNativeInspection(agent, terminalControl, plan, request, expectedEvidenceFingerprint, options = {}) {
1797
- const adapter = this.registry.require(agent);
1798
- try {
1799
- assertClosedStatusInspectionPlan(adapter, terminalControl, plan);
1800
- assertClosedNativeInspectionDismissal(plan);
1801
- }
1802
- catch (error) {
1803
- throw new NativeInspectionDismissalError(error instanceof Error ? error.message : String(error), { cause: error });
1804
- }
1805
- const observeExactPanel = async (control) => {
1806
- const captured = await this.captureInspection(adapter, control, {
1807
- runtime: options.runtime,
1808
- scrollbackLines: options.scrollbackLines
1809
- });
1810
- const observation = adapter.observeNativeInspection?.({
1811
- operation: request.operation,
1812
- previousScreenFingerprint: request.previousScreenFingerprint,
1813
- preEnterEvidenceInventory: request.preEnterEvidenceInventory,
1814
- expectedNativeThreadId: request.expectedNativeThreadId,
1815
- expectedAgentVersion: request.expectedAgentVersion,
1816
- expectedCwd: request.expectedCwd,
1817
- screen: captured.screen
1818
- });
1819
- if (!observation ||
1820
- observation.status !== "observed" ||
1821
- observation.result?.kind !== "native_status" ||
1822
- observation.evidenceFingerprint !== expectedEvidenceFingerprint) {
1823
- throw new Error(observation?.reason ??
1824
- "the exact fresh native status panel changed before dismissal");
1825
- }
1826
- const verified = await this.verifyTerminalIdentity(adapter.agent, captured.terminalControl, options.runtime);
1827
- if (!sameTerminalControlIdentity(captured.terminalControl, verified)) {
1828
- throw new Error("terminal control identity changed before native status dismissal");
1829
- }
1830
- return verified;
1831
- };
1832
- try {
1833
- let verified = await observeExactPanel(terminalControl);
1834
- await options.beforeDismiss?.({
1835
- agent: adapter.agent,
1836
- terminalControl: verified,
1837
- plan,
1838
- evidenceFingerprint: expectedEvidenceFingerprint
1839
- });
1840
- verified = await observeExactPanel(verified);
1841
- try {
1842
- await this.terminalProvider.sendKeys(this.terminalProvider.endpoint(verified), plan.expectedResult.dismissal.keys);
1843
- }
1844
- catch (error) {
1845
- throw new NativeInspectionDismissalError("native status modal dismissal outcome is uncertain; do not retry automatically", { cause: error });
1846
- }
1847
- return {
1848
- stage: "dismiss_dispatched",
1849
- agent: adapter.agent,
1850
- terminalControl: verified,
1851
- keys: plan.expectedResult.dismissal.keys,
1852
- dismissCount: 1
1853
- };
1854
- }
1855
- catch (error) {
1856
- if (error instanceof NativeInspectionDismissalError) {
1857
- throw error;
1858
- }
1859
- throw new NativeInspectionDismissalError(error instanceof Error ? error.message : String(error), { cause: error });
1860
- }
1861
- }
1862
- async captureCodexReadyComposer(adapter, terminalControl, plan, runtime) {
1863
- if (adapter.agent !== "codex") {
1864
- throw new NativeInspectionDiagnosticError("unsupported_profile", "the closed Codex /status probe requires the Codex adapter");
1865
- }
1866
- const minimumViewport = CODEX_NATIVE_STATUS_MIN_VIEWPORT_BY_PROFILE[plan.behaviorProfile];
1867
- if (minimumViewport === undefined) {
1868
- throw new NativeInspectionDiagnosticError("unsupported_profile", `Codex ${plan.behaviorProfile} has no exact /status viewport profile`);
1869
- }
1870
- const captureReady = async (control) => {
1871
- const verified = await this.verifyTerminalIdentity(adapter.agent, control, runtime);
1872
- const endpoint = this.terminalProvider.endpoint(verified);
1873
- const viewportInspector = this.terminalProvider.inspectViewport;
1874
- let exactViewport;
1875
- let viewportUnavailableReason = "terminal provider has no exact viewport inspector";
1876
- if (viewportInspector) {
1877
- let viewport;
1878
- try {
1879
- viewport = await viewportInspector.call(this.terminalProvider, endpoint);
1880
- }
1881
- catch (error) {
1882
- throw new NativeInspectionDiagnosticError("viewport_unavailable", `Codex /status viewport inspection failed before terminal input: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
1883
- }
1884
- if (viewport) {
1885
- if (!Number.isSafeInteger(viewport.columns) ||
1886
- viewport.columns <= 0 ||
1887
- !Number.isSafeInteger(viewport.rows) ||
1888
- viewport.rows <= 0) {
1889
- throw new NativeInspectionDiagnosticError("viewport_unavailable", "Codex /status viewport inspector returned invalid geometry");
1890
- }
1891
- exactViewport = viewport.columns;
1892
- }
1893
- else {
1894
- viewportUnavailableReason =
1895
- "terminal provider could not prove exact viewport geometry";
1896
- }
1897
- }
1898
- const styledScreen = await this.terminalProvider.capture(endpoint, { scrollbackLines: 40, preserveEscapes: true });
1899
- const plainScreen = stripTerminalEscapeSequences(styledScreen);
1900
- const inspection = adapter.inspectScreen({ screen: plainScreen, runtime });
1901
- assertNativeInspectionComposerSafe(inspection, adapter.displayName);
1902
- const composer = exactCodexReadyStyledComposerCapture(styledScreen);
1903
- if (!composer) {
1904
- throw new NativeInspectionDiagnosticError("composer_not_ready", "Codex composer contains non-placeholder input or is not at the exact idle prompt");
1905
- }
1906
- const inferredViewport = inferCodexVisibleViewportColumns(styledScreen);
1907
- const observedViewport = exactViewport ?? inferredViewport;
1908
- if ((observedViewport !== undefined && observedViewport < minimumViewport) ||
1909
- hasTruncatedCodexStatusSessionLine(plainScreen)) {
1910
- throw new NativeInspectionDiagnosticError("viewport_too_narrow", `Codex /status requires a proven viewport of at least ` +
1911
- `${minimumViewport} columns to preserve the complete Session UUID` +
1912
- `${observedViewport === undefined
1913
- ? ""
1914
- : `; observed ${observedViewport}`}; widen or zoom the pane before retrying`);
1915
- }
1916
- if (exactViewport === undefined) {
1917
- throw new NativeInspectionDiagnosticError("viewport_unavailable", `Codex /status requires exact terminal viewport geometry before input; ` +
1918
- viewportUnavailableReason +
1919
- `${inferredViewport === undefined
1920
- ? ""
1921
- : ` (ANSI fallback estimated ${inferredViewport} columns)`}`);
1922
- }
1923
- const reverified = await this.verifyTerminalIdentity(adapter.agent, verified, runtime);
1924
- if (!sameTerminalControlIdentity(verified, reverified)) {
1925
- throw new NativeInspectionDiagnosticError("identity_unverified", "terminal control identity changed after the Codex pre-text composer capture");
1926
- }
1927
- return {
1928
- terminalControl: reverified,
1929
- screenDigest: nativeInspectionScreenFingerprint(styledScreen),
1930
- composerDigest: composer.digest
1931
- };
1932
- };
1933
- const first = await captureReady(terminalControl);
1934
- const second = await captureReady(first.terminalControl);
1935
- if (second.composerDigest !== first.composerDigest) {
1936
- throw new NativeInspectionDiagnosticError("composer_not_ready", "Codex empty composer changed across its stable pre-text captures");
1937
- }
1938
- return {
1939
- terminalControl: second.terminalControl,
1940
- screenDigest: second.screenDigest
1941
- };
1942
- }
1943
- async assertFinalCodexStatusViewport(terminalControl, plan, runtime) {
1944
- const minimumViewport = CODEX_NATIVE_STATUS_MIN_VIEWPORT_BY_PROFILE[plan.behaviorProfile];
1945
- if (minimumViewport === undefined) {
1946
- throw new NativeInspectionDiagnosticError("unsupported_profile", `Codex ${plan.behaviorProfile} has no exact /status viewport profile`);
1947
- }
1948
- const verified = await this.verifyTerminalIdentity("codex", terminalControl, runtime);
1949
- const viewportInspector = this.terminalProvider.inspectViewport;
1950
- if (!viewportInspector) {
1951
- throw new NativeInspectionDiagnosticError("viewport_unavailable", "Codex /status requires exact terminal viewport geometry immediately before Enter");
1952
- }
1953
- let viewport;
1954
- try {
1955
- viewport = await viewportInspector.call(this.terminalProvider, this.terminalProvider.endpoint(verified));
1956
- }
1957
- catch (error) {
1958
- throw new NativeInspectionDiagnosticError("viewport_unavailable", `Codex /status viewport inspection failed immediately before Enter: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
1959
- }
1960
- if (!viewport ||
1961
- !Number.isSafeInteger(viewport.columns) ||
1962
- viewport.columns <= 0 ||
1963
- !Number.isSafeInteger(viewport.rows) ||
1964
- viewport.rows <= 0) {
1965
- throw new NativeInspectionDiagnosticError("viewport_unavailable", "Codex /status exact terminal viewport became unavailable immediately before Enter");
1966
- }
1967
- if (viewport.columns < minimumViewport) {
1968
- throw new NativeInspectionDiagnosticError("viewport_too_narrow", `Codex /status viewport narrowed to ${viewport.columns} columns before Enter; ` +
1969
- `at least ${minimumViewport} are required to preserve the complete Session UUID`);
1970
- }
1971
- const reverified = await this.verifyTerminalIdentity("codex", verified, runtime);
1972
- if (!sameTerminalControlIdentity(verified, reverified)) {
1973
- throw new NativeInspectionDiagnosticError("identity_unverified", "terminal control identity changed after the final Codex viewport proof");
1974
- }
1975
- return reverified;
1976
- }
1977
- async settleNativeInspectionComposer(adapter, terminalControl, plan, runtime) {
1978
- const startedAt = this.nowMs();
1979
- const settleTimeoutMs = plan.composer.maximumSettleMs;
1980
- let stableDigest;
1981
- let stableKind;
1982
- let stableSince;
1983
- let stableCaptures = 0;
1984
- let capturesBeyondDeadline = 0;
1985
- let lastMismatchDiagnostic = "composer_not_exact";
1986
- while (true) {
1987
- const captured = await this.captureInspection(adapter, terminalControl, {
1988
- runtime,
1989
- scrollbackLines: CODEX_MULTILINE_SETTLE_SCROLLBACK_LINES
1990
- });
1991
- assertNativeInspectionComposerSafe(captured.inspection);
1992
- const materialized = exactNativeInspectionComposerCapture(adapter.agent, captured.screen, plan);
1993
- const now = this.nowMs();
1994
- if (materialized) {
1995
- if (materialized.digest === stableDigest &&
1996
- materialized.kind === stableKind) {
1997
- stableCaptures += 1;
1998
- }
1999
- else {
2000
- stableDigest = materialized.digest;
2001
- stableKind = materialized.kind;
2002
- stableSince = now;
2003
- stableCaptures = 1;
2004
- }
2005
- const stableForMs = stableSince === undefined ? 0 : now - stableSince;
2006
- if (stableCaptures >= CODEX_MULTILINE_STABLE_CAPTURES &&
2007
- stableForMs >= plan.composer.minimumStableMs) {
2008
- const evidence = {
2009
- kind: materialized.kind,
2010
- digest: materialized.digest,
2011
- stableForMs,
2012
- stableCaptures
2013
- };
2014
- return this.revalidateNativeInspectionComposer(adapter, captured.terminalControl, plan, evidence, runtime);
2015
- }
2016
- }
2017
- else {
2018
- lastMismatchDiagnostic = adapter.agent === "codex"
2019
- ? codexNativeInspectionComposerMismatchDiagnostic(captured.screen, plan)
2020
- : "composer_not_exact";
2021
- stableDigest = undefined;
2022
- stableKind = undefined;
2023
- stableSince = undefined;
2024
- stableCaptures = 0;
2025
- }
2026
- const elapsed = this.nowMs() - startedAt;
2027
- const remaining = settleTimeoutMs - elapsed;
2028
- if (remaining <= 0 && !materialized) {
2029
- break;
2030
- }
2031
- if (remaining <= 0) {
2032
- capturesBeyondDeadline += 1;
2033
- if (capturesBeyondDeadline > CODEX_EXACT_CANDIDATE_GRACE_CAPTURES) {
2034
- break;
2035
- }
2036
- }
2037
- const remainingStableMs = plan.composer.minimumStableMs -
2038
- (stableSince === undefined ? 0 : this.nowMs() - stableSince);
2039
- await this.sleep(Math.min(CODEX_MULTILINE_SETTLE_POLL_MS, remaining > 0
2040
- ? remaining
2041
- : Math.max(1, remainingStableMs)));
2042
- }
2043
- throw new NativeInspectionDiagnosticError(lastMismatchDiagnostic, lastMismatchDiagnostic === "composer_viewport_truncated"
2044
- ? `${adapter.displayName} /status slash popup was truncated by the viewport; widen or zoom the pane before retrying manually`
2045
- : `${adapter.displayName} /status composer did not become exact, idle, and stable before the bounded submit deadline`);
2046
- }
2047
- async revalidateNativeInspectionComposer(adapter, terminalControl, plan, expected, runtime) {
2048
- const captured = await this.captureInspection(adapter, terminalControl, {
2049
- runtime,
2050
- scrollbackLines: CODEX_MULTILINE_SETTLE_SCROLLBACK_LINES
2051
- });
2052
- assertNativeInspectionComposerSafe(captured.inspection);
2053
- const materialized = exactNativeInspectionComposerCapture(adapter.agent, captured.screen, plan);
2054
- if (!materialized ||
2055
- materialized.digest !== expected.digest ||
2056
- materialized.kind !== expected.kind) {
2057
- throw new NativeInspectionDiagnosticError("composer_drift", `${adapter.displayName} /status composer changed after its stable pre-submit capture`);
2058
- }
2059
- const baseline = adapter.observeNativeInspection?.({
2060
- operation: plan.operation,
2061
- screen: captured.screen
2062
- });
2063
- if (!baseline ||
2064
- baseline.status === "ambiguous" ||
2065
- !Array.isArray(baseline.evidenceInventory)) {
2066
- throw new NativeInspectionDiagnosticError("evidence_unproven", baseline?.reason ??
2067
- `${adapter.displayName} /status pre-Enter evidence inventory was not proven`);
2068
- }
2069
- const verifiedImmediatelyBeforeEnter = await this.verifyTerminalIdentity(adapter.agent, captured.terminalControl, runtime);
2070
- if (!sameTerminalControlIdentity(captured.terminalControl, verifiedImmediatelyBeforeEnter)) {
2071
- throw new NativeInspectionDiagnosticError("identity_unverified", `terminal control identity changed after the final ${adapter.displayName} /status composer capture`);
2072
- }
2073
- return {
2074
- terminalControl: verifiedImmediatelyBeforeEnter,
2075
- screenDigest: nativeInspectionScreenFingerprint(captured.screen),
2076
- evidenceInventory: baseline.evidenceInventory,
2077
- materialization: expected
2078
- };
2079
- }
2080
- async settleCodexMultilineComposer(adapter, terminalControl, expectedText, runtime, options = {}) {
2081
- const observation = await this.settleCodexComposerObservation(adapter, terminalControl, expectedText, runtime, {
2082
- minimumStableMs: CODEX_PASTE_ENTER_SETTLE_MS,
2083
- requiredState: "exact_draft",
2084
- allowOpaqueLargePastePlaceholder: options.allowOpaqueLargePastePlaceholder === true,
2085
- allowWorkingComposer: options.allowWorkingComposer === true
2086
- });
2087
- if (observation.state === "exact_draft") {
2088
- return observation.terminalControl;
2089
- }
2090
- if (observation.state === "working" ||
2091
- observation.state === "approval_or_modal") {
2092
- throw new Error("Codex became busy or blocked while its multiline composer was settling");
2093
- }
2094
- if (observation.state === "identity_drift") {
2095
- throw new Error(observation.reason);
2096
- }
2097
- throw new Error("Codex multiline composer did not become exact, idle, and stable before the bounded submit deadline");
2098
- }
2099
- async settleCodexComposerObservation(adapter, terminalControl, expectedText, runtime, options) {
2100
- const startedAt = this.nowMs();
2101
- let candidate;
2102
- let stableSince;
2103
- let stableCaptures = 0;
2104
- let finishObservedExactCandidate = false;
2105
- let capturesBeyondDeadline = 0;
2106
- let control = terminalControl;
2107
- while (true) {
2108
- const snapshot = await this.captureCodexComposerSnapshot(adapter, control, expectedText, runtime, {
2109
- allowOpaqueLargePastePlaceholder: options.allowOpaqueLargePastePlaceholder === true,
2110
- classifyOpaqueLargePasteAsDifferent: options.classifyOpaqueLargePasteAsDifferent === true,
2111
- allowWorkingComposer: options.allowWorkingComposer === true
2112
- });
2113
- if (snapshot.state === "identity_drift" ||
2114
- (snapshot.state === "unavailable" && snapshot.retryable !== true)) {
2115
- return snapshot;
2116
- }
2117
- if (this.nowMs() - startedAt > CODEX_MULTILINE_SETTLE_TIMEOUT_MS) {
2118
- capturesBeyondDeadline += 1;
2119
- if (capturesBeyondDeadline > CODEX_EXACT_CANDIDATE_GRACE_CAPTURES) {
2120
- return {
2121
- state: "unavailable",
2122
- reason: "Codex exact composer did not stabilize within its bounded capture grace"
2123
- };
2124
- }
2125
- }
2126
- control = snapshot.terminalControl;
2127
- const sameCandidate = candidate !== undefined &&
2128
- candidate.state === snapshot.state &&
2129
- candidate.digest === snapshot.digest &&
2130
- sameTerminalControlIdentity(candidate.terminalControl, snapshot.terminalControl);
2131
- if (sameCandidate) {
2132
- stableCaptures += 1;
2133
- }
2134
- else {
2135
- candidate = snapshot;
2136
- stableSince = this.nowMs();
2137
- stableCaptures = 1;
2138
- finishObservedExactCandidate = snapshot.state === "exact_draft";
2139
- }
2140
- const stableForMs = stableSince === undefined
2141
- ? 0
2142
- : this.nowMs() - stableSince;
2143
- if (stableCaptures >= CODEX_MULTILINE_STABLE_CAPTURES &&
2144
- stableForMs >= options.minimumStableMs &&
2145
- snapshot.state !== "unavailable" &&
2146
- (options.requiredState === undefined ||
2147
- snapshot.state === options.requiredState ||
2148
- snapshot.state === "working" ||
2149
- snapshot.state === "approval_or_modal")) {
2150
- // The caller's authorization hook must see evidence from a final,
2151
- // independently identity-fenced recapture, not the polling capture.
2152
- const finalSnapshot = await this.captureCodexComposerSnapshot(adapter, snapshot.terminalControl, expectedText, runtime, {
2153
- allowOpaqueLargePastePlaceholder: options.allowOpaqueLargePastePlaceholder === true,
2154
- classifyOpaqueLargePasteAsDifferent: options.classifyOpaqueLargePasteAsDifferent === true,
2155
- allowWorkingComposer: options.allowWorkingComposer === true
2156
- });
2157
- if (finalSnapshot.state === snapshot.state &&
2158
- "digest" in finalSnapshot &&
2159
- finalSnapshot.digest === snapshot.digest &&
2160
- sameTerminalControlIdentity(snapshot.terminalControl, finalSnapshot.terminalControl)) {
2161
- return {
2162
- ...finalSnapshot,
2163
- stableCaptures: stableCaptures + 1
2164
- };
2165
- }
2166
- if (finalSnapshot.state === "identity_drift" ||
2167
- (finalSnapshot.state === "unavailable" &&
2168
- finalSnapshot.retryable !== true)) {
2169
- return finalSnapshot;
2170
- }
2171
- if (options.requiredState !== undefined &&
2172
- snapshot.state === options.requiredState) {
2173
- return {
2174
- state: "unavailable",
2175
- reason: "Codex composer changed after its stable final recapture"
2176
- };
2177
- }
2178
- candidate = finalSnapshot;
2179
- control = finalSnapshot.terminalControl;
2180
- stableSince = this.nowMs();
2181
- stableCaptures = 1;
2182
- finishObservedExactCandidate = finalSnapshot.state === "exact_draft";
2183
- }
2184
- const elapsed = this.nowMs() - startedAt;
2185
- if (elapsed > CODEX_MULTILINE_SETTLE_TIMEOUT_MS &&
2186
- !finishObservedExactCandidate) {
2187
- return {
2188
- state: "unavailable",
2189
- reason: options.requiredState
2190
- ? `Codex composer did not become ${options.requiredState} before the bounded observation deadline`
2191
- : "Codex composer did not become stable before the bounded observation deadline"
2192
- };
2193
- }
2194
- if (elapsed > CODEX_MULTILINE_SETTLE_TIMEOUT_MS &&
2195
- options.requiredState !== undefined &&
2196
- snapshot.state !== options.requiredState) {
2197
- if (snapshot.state !== "unavailable") {
2198
- return {
2199
- ...snapshot,
2200
- stableCaptures
2201
- };
2202
- }
2203
- return {
2204
- state: "unavailable",
2205
- reason: snapshot.reason
2206
- };
2207
- }
2208
- const remainingStableMs = options.minimumStableMs - stableForMs;
2209
- await this.sleep(Math.max(1, Math.min(CODEX_MULTILINE_SETTLE_POLL_MS, remainingStableMs > 0
2210
- ? remainingStableMs
2211
- : CODEX_MULTILINE_SETTLE_POLL_MS)));
2212
- }
2213
- }
2214
- async captureCodexComposerSnapshot(adapter, terminalControl, expectedText, runtime, options = {}) {
2215
- let verifiedBefore;
2216
- try {
2217
- verifiedBefore = await this.verifyTerminalIdentity(adapter.agent, terminalControl, runtime);
2218
- }
2219
- catch (error) {
2220
- return {
2221
- state: "identity_drift",
2222
- reason: error instanceof Error ? error.message : String(error)
2223
- };
2224
- }
2225
- let styledScreen;
2226
- try {
2227
- styledScreen = await this.terminalProvider.capture(this.terminalProvider.endpoint(verifiedBefore), {
2228
- scrollbackLines: CODEX_MULTILINE_SETTLE_SCROLLBACK_LINES,
2229
- preserveEscapes: true
2230
- });
2231
- }
2232
- catch (error) {
2233
- return {
2234
- state: "unavailable",
2235
- reason: error instanceof Error ? error.message : String(error)
2236
- };
2237
- }
2238
- const screen = stripTerminalEscapeSequences(styledScreen);
2239
- let inspection;
2240
- try {
2241
- inspection = adapter.inspectScreen({ screen, runtime });
2242
- }
2243
- catch (error) {
2244
- return {
2245
- state: "unavailable",
2246
- reason: error instanceof Error ? error.message : String(error)
2247
- };
2248
- }
2249
- let verifiedAfter;
2250
- try {
2251
- verifiedAfter = await this.verifyTerminalIdentity(adapter.agent, verifiedBefore, runtime);
2252
- }
2253
- catch (error) {
2254
- return {
2255
- state: "identity_drift",
2256
- reason: error instanceof Error ? error.message : String(error)
2257
- };
2258
- }
2259
- if (!sameTerminalControlIdentity(verifiedBefore, verifiedAfter)) {
2260
- return {
2261
- state: "identity_drift",
2262
- reason: "terminal control identity changed across the Codex composer capture"
2263
- };
2264
- }
2265
- const screenDigest = createHash("sha256").update(styledScreen).digest("hex");
2266
- if (inspection.approval.blocked ||
2267
- inspection.activity.state === "awaiting_approval" ||
2268
- codexBlockingModalVisible(screen)) {
2269
- return {
2270
- state: "approval_or_modal",
2271
- terminalControl: verifiedAfter,
2272
- digest: screenDigest
2273
- };
2274
- }
2275
- if (inspection.activity.state === "working" &&
2276
- options.allowWorkingComposer !== true) {
2277
- return {
2278
- state: "working",
2279
- terminalControl: verifiedAfter,
2280
- digest: screenDigest
2281
- };
2282
- }
2283
- const composer = currentCodexComposerCapture(styledScreen, expectedText, options.allowOpaqueLargePastePlaceholder === true, options.classifyOpaqueLargePasteAsDifferent === true);
2284
- if (!composer) {
2285
- return {
2286
- state: "unavailable",
2287
- reason: "the current live Codex composer could not be proven",
2288
- retryable: true,
2289
- terminalControl: verifiedAfter,
2290
- digest: screenDigest
2291
- };
2292
- }
2293
- return {
2294
- state: composer.state,
2295
- terminalControl: verifiedAfter,
2296
- digest: composer.digest
2297
- };
2298
- }
2299
- /**
2300
- * Prove that the exact managed draft is still present after Enter dispatch.
2301
- * Two identity-fenced captures are required so transient repaint state never
2302
- * becomes a hard `not_accepted` conclusion.
2303
- */
2304
- async proveExactDraftStillPresent(agent, terminalControl, expectedText, options = {}) {
829
+ /**
830
+ * Prove that the exact managed draft is still present after Enter dispatch.
831
+ * Two identity-fenced captures are required so transient repaint state never
832
+ * becomes a hard `not_accepted` conclusion.
833
+ */
834
+ async proveExactDraftStillPresent(agent, terminalControl, expectedText, options = {}) {
2305
835
  const adapter = this.registry.require(agent);
2306
836
  const captureExactDraft = async (control) => {
2307
837
  const captured = await this.captureInspection(adapter, control, {
@@ -2644,114 +1174,7 @@ export class TerminalAgentBridge {
2644
1174
  };
2645
1175
  }
2646
1176
  async respondInteraction(agent, terminalControl, response, options) {
2647
- const runtime = {
2648
- ...options.runtime,
2649
- agentVersion: options.agentVersion
2650
- };
2651
- const optionReason = terminalInteractionOptionPreflightReason(options, runtime);
2652
- if (optionReason) {
2653
- return blockedTerminalInteractionResponse(response, optionReason);
2654
- }
2655
- const first = await this.captureTerminalInteractionOffer(agent, terminalControl, runtime, options.scrollbackLines).catch((error) => error);
2656
- if (first instanceof Error) {
2657
- return blockedTerminalInteractionResponse(response, first.message);
2658
- }
2659
- const firstReason = terminalInteractionOfferPreflightReason(first.offer, options, this.now());
2660
- if (firstReason || !first.offer) {
2661
- return blockedTerminalInteractionResponse(response, firstReason ?? "native questionnaire has no safe interaction offer", first.offer);
2662
- }
2663
- const validatedResponse = validateAnyTerminalInteractionResponse(response, first.offer.projection, {
2664
- now: this.now(),
2665
- // The capture immediately above is the authoritative freshness proof.
2666
- // A wall-clock bucket boundary must not override exact live terminal
2667
- // evidence before any input has been sent.
2668
- allowExpiredForLiveRecapture: true
2669
- });
2670
- const planReason = terminalInteractionPlanPreflightReason(validatedResponse, first.offer.actionPlan);
2671
- if (planReason) {
2672
- return blockedTerminalInteractionResponse(validatedResponse, planReason, first.offer);
2673
- }
2674
- const capabilityReason = terminalInteractionCapabilityReason(this.terminalProvider, first.terminalControl, first.offer.actionPlan);
2675
- if (capabilityReason) {
2676
- return blockedTerminalInteractionResponse(validatedResponse, capabilityReason, first.offer);
2677
- }
2678
- const authorization = await authorizeTerminalInteraction(options.authorize, interactionHookContext(agent, first.terminalControl, first.offer, validatedResponse, runtime));
2679
- if (!authorization.approved) {
2680
- return blockedTerminalInteractionResponse(validatedResponse, authorization.reason ?? "terminal interaction response was not authorized", first.offer);
2681
- }
2682
- const afterAuthorization = await this.captureTerminalInteractionOffer(agent, first.terminalControl, runtime, options.scrollbackLines).catch((error) => error);
2683
- if (afterAuthorization instanceof Error) {
2684
- return blockedTerminalInteractionResponse(validatedResponse, afterAuthorization.message, first.offer);
2685
- }
2686
- const afterAuthorizationReason = terminalInteractionOfferPreflightReason(afterAuthorization.offer, options, this.now());
2687
- if (afterAuthorizationReason) {
2688
- return blockedTerminalInteractionResponse(validatedResponse, afterAuthorizationReason, first.offer);
2689
- }
2690
- if (!sameTerminalInteractionOffer(first, afterAuthorization, {
2691
- // expires_at is generated from AKK wall-clock freshness buckets, not
2692
- // from the native prompt. All semantic projection and action-plan fields
2693
- // still have to match exactly.
2694
- ignoreExpiresAt: true
2695
- })) {
2696
- return blockedTerminalInteractionResponse(validatedResponse, "native questionnaire changed after authorization", first.offer);
2697
- }
2698
- await reserveTerminalInteractionDispatch(options.beforeDispatch, interactionHookContext(agent, afterAuthorization.terminalControl, afterAuthorization.offer, validatedResponse, runtime));
2699
- const finalCapture = await this.captureTerminalInteractionOffer(agent, afterAuthorization.terminalControl, runtime, options.scrollbackLines).catch((error) => {
2700
- if (options.beforeDispatch) {
2701
- throw terminalInteractionInputNotStartedError(error, "cannot recapture native questionnaire after dispatch reservation");
2702
- }
2703
- return error;
2704
- });
2705
- if (finalCapture instanceof Error) {
2706
- return blockedTerminalInteractionResponse(validatedResponse, finalCapture.message, first.offer);
2707
- }
2708
- if (!sameTerminalInteractionOffer(afterAuthorization, finalCapture, {
2709
- // The projection expiry is bucketed and may roll over during this final,
2710
- // bounded recapture even though the questionnaire is exact. Prompt,
2711
- // action plan, terminal identity, and owner remain exact-match fences.
2712
- ignoreExpiresAt: true
2713
- })) {
2714
- if (options.beforeDispatch) {
2715
- throw new TerminalInteractionInputNotStartedError("native questionnaire changed after dispatch reservation");
2716
- }
2717
- return blockedTerminalInteractionResponse(validatedResponse, "native questionnaire changed before dispatch", first.offer);
2718
- }
2719
- const verified = await this.verifyTerminalIdentity(agent, finalCapture.terminalControl, runtime).catch((error) => {
2720
- if (options.beforeDispatch) {
2721
- throw terminalInteractionInputNotStartedError(error, "terminal identity could not be verified immediately before interaction dispatch");
2722
- }
2723
- throw terminalInteractionReservedError("key_uncertain", error, "terminal identity could not be verified immediately before interaction dispatch");
2724
- });
2725
- if (!sameTerminalControlIdentity(finalCapture.terminalControl, verified)) {
2726
- if (options.beforeDispatch) {
2727
- throw new TerminalInteractionInputNotStartedError("terminal identity changed after the final questionnaire capture");
2728
- }
2729
- throw new TerminalInteractionDispatchReservedError("key_uncertain", "terminal identity changed after the final questionnaire capture");
2730
- }
2731
- const answer = validatedResponse.answers[0];
2732
- const outcome = await dispatchTerminalInteractionAnswer({
2733
- agent,
2734
- provider: this.terminalProvider,
2735
- terminalControl: verified,
2736
- actionPlan: finalCapture.offer.actionPlan,
2737
- answer,
2738
- sleep: this.sleep,
2739
- reverifyTerminalIdentity: async (currentControl) => {
2740
- const reverified = await this.verifyTerminalIdentity(agent, currentControl, runtime);
2741
- if (!sameTerminalControlIdentity(currentControl, reverified)) {
2742
- throw new Error("terminal identity changed between interaction text and Enter");
2743
- }
2744
- return reverified;
2745
- }
2746
- });
2747
- return {
2748
- responded: true,
2749
- blocked: false,
2750
- interactionId: validatedResponse.interaction_id,
2751
- questionId: answer.question_id,
2752
- responseKind: answer.response_kind,
2753
- outcome
2754
- };
1177
+ return this.interactionResponseBridge().respond(agent, terminalControl, response, options);
2755
1178
  }
2756
1179
  async monitorPoll(options) {
2757
1180
  const adapter = this.registry.require(options.agent);
@@ -2823,25 +1246,6 @@ export class TerminalAgentBridge {
2823
1246
  })
2824
1247
  };
2825
1248
  }
2826
- async captureTerminalInteractionOffer(agent, terminalControl, runtime, scrollbackLines) {
2827
- const adapter = this.registry.require(agent);
2828
- const captured = await this.captureInspection(adapter, terminalControl, {
2829
- scrollbackLines,
2830
- runtime
2831
- });
2832
- return {
2833
- terminalControl: captured.terminalControl,
2834
- inspection: captured.inspection,
2835
- offer: captureTerminalInteractionRuntimeOffer({
2836
- agent,
2837
- terminalControl: captured.terminalControl,
2838
- screen: captured.screen,
2839
- runtime,
2840
- now: this.now(),
2841
- approvalBlocked: captured.inspection.approval.blocked
2842
- })
2843
- };
2844
- }
2845
1249
  async verifyTerminalIdentity(agent, terminalControl, runtime) {
2846
1250
  let verifiedControl = terminalControl;
2847
1251
  if (this.verifyIdentity) {
@@ -2868,1066 +1272,15 @@ export class TerminalAgentBridge {
2868
1272
  return this.terminalProvider.toControlRef(resolvedEndpoint, verifiedControl.capabilities);
2869
1273
  }
2870
1274
  }
2871
- function assertTerminalMutationCapabilities({ provider, terminal, semantic, transport }) {
2872
- const missingSemantic = semantic.filter((capability) => !provider.supportedCapabilities.includes(capability) ||
2873
- !terminal.capabilities.includes(capability));
2874
- const missingTransport = transport.filter((capability) => !provider.providerCapabilities.includes(capability));
2875
- if (missingSemantic.length === 0 && missingTransport.length === 0) {
2876
- return;
2877
- }
2878
- const missing = [
2879
- ...missingSemantic.map((value) => `terminal:${value}`),
2880
- ...missingTransport.map((value) => `provider:${value}`)
2881
- ];
2882
- throw new TerminalControlInputNotSentError(`terminal action capability preflight failed for ` +
2883
- `${terminal.identity.providerKind}:${terminal.route.label}: ` +
2884
- missing.join(", "));
1275
+ async function terminalSettleDelay(milliseconds) {
1276
+ await new Promise((resolve) => setTimeout(resolve, milliseconds));
2885
1277
  }
2886
- function nativeInspectionSubmissionError(stage, error, fallbackDiagnostic) {
2887
- const diagnostic = error instanceof NativeInspectionSubmissionError
2888
- ? error.diagnostic
2889
- : error instanceof NativeInspectionDiagnosticError
2890
- ? error.diagnostic
2891
- : fallbackDiagnostic;
2892
- if (error instanceof NativeInspectionSubmissionError) {
2893
- const stageRank = {
2894
- not_started: 0,
2895
- text_injected: 1,
2896
- enter_uncertain: 2
2897
- };
2898
- if (stageRank[error.stage] >= stageRank[stage]) {
2899
- return error;
2900
- }
2901
- return new NativeInspectionSubmissionError(stage, error.message, {
2902
- cause: error,
2903
- diagnostic
2904
- });
2905
- }
2906
- return new NativeInspectionSubmissionError(stage, error instanceof Error ? error.message : String(error), { cause: error, diagnostic });
1278
+ function sameTerminalControlIdentity(left, right) {
1279
+ return sameTerminalControlIncarnation(left, right);
2907
1280
  }
2908
- function assertClosedStatusInspectionPlan(adapter, terminalControl, plan) {
2909
- if (!terminalControl.capabilities.includes("send_keys")) {
2910
- throw new Error(`${adapter.displayName} terminal input is not supported`);
2911
- }
2912
- if (!terminalControl.capabilities.includes("screen_status")) {
2913
- throw new Error(`${adapter.displayName} terminal screen inspection is not supported`);
2914
- }
2915
- const codexProfile = adapter.agent === "codex" &&
2916
- CODEX_NATIVE_STATUS_POPUP_BY_PROFILE[plan.behaviorProfile] !== undefined;
2917
- const claudeProfile = adapter.agent === "claude" &&
2918
- CLAUDE_NATIVE_STATUS_POPUP_BY_PROFILE[plan.behaviorProfile] !== undefined;
2919
- const expectedSettle = codexProfile
2920
- ? { minimumStableMs: CODEX_PASTE_ENTER_SETTLE_MS, maximumSettleMs: 2_000 }
2921
- : CLAUDE_NATIVE_STATUS_SETTLE_BY_PROFILE[plan.behaviorProfile];
2922
- const exactPresentation = codexProfile
2923
- ? plan.expectedResult.presentation === "inline" &&
2924
- plan.expectedResult.dismissal === undefined &&
2925
- plan.composer.minimumStableMs === expectedSettle?.minimumStableMs &&
2926
- plan.composer.maximumSettleMs === expectedSettle.maximumSettleMs
2927
- : claudeProfile
2928
- ? plan.expectedResult.presentation === "modal" &&
2929
- plan.composer.minimumStableMs === expectedSettle?.minimumStableMs &&
2930
- plan.composer.maximumSettleMs === expectedSettle.maximumSettleMs &&
2931
- plan.expectedResult.dismissal?.expected === "idle_empty_composer" &&
2932
- JSON.stringify(plan.expectedResult.dismissal.keys) ===
2933
- JSON.stringify(["Escape"])
2934
- : false;
2935
- if (plan.operation.kind !== "status" ||
2936
- plan.command !== "/status" ||
2937
- plan.effect !== "read_only" ||
2938
- plan.requiresIdle !== true ||
2939
- plan.composer.kind !== "exact" ||
2940
- !Number.isFinite(plan.composer.minimumStableMs) ||
2941
- plan.composer.minimumStableMs < 0 ||
2942
- !Number.isFinite(plan.composer.maximumSettleMs) ||
2943
- plan.composer.maximumSettleMs < plan.composer.minimumStableMs ||
2944
- plan.expectedResult.kind !== "native_status" ||
2945
- !exactPresentation) {
2946
- throw new Error("refusing a non-closed native inspection plan");
2947
- }
2948
- }
2949
- function assertClosedNativeInspectionDismissal(plan) {
2950
- if (plan.expectedResult.presentation !== "modal" ||
2951
- plan.expectedResult.dismissal?.expected !== "idle_empty_composer" ||
2952
- JSON.stringify(plan.expectedResult.dismissal.keys) !==
2953
- JSON.stringify(["Escape"])) {
2954
- throw new Error("native inspection has no closed modal dismissal plan");
2955
- }
2956
- }
2957
- function assertNativeInspectionComposerSafe(inspection, displayName = "terminal agent") {
2958
- if (inspection.approval.blocked ||
2959
- inspection.activity.state === "awaiting_approval" ||
2960
- inspection.activity.state === "working") {
2961
- throw new NativeInspectionDiagnosticError("composer_not_ready", `${displayName} became busy or blocked while its /status composer was settling`);
2962
- }
2963
- // Codex's generic activity parser deliberately reports a non-empty slash
2964
- // composer as unknown. At this stage the caller has already proved an idle,
2965
- // empty styled composer under the terminal lock; the exact-current composer
2966
- // capture below is the stronger continuation proof after AKK injected only
2967
- // the adapter-owned /status command.
2968
- }
2969
- function exactNativeInspectionComposerCapture(agent, screen, plan) {
2970
- return agent === "codex"
2971
- ? exactCodexNativeInspectionComposerCapture(screen, plan)
2972
- : exactClaudeNativeInspectionComposerCapture(screen, plan);
2973
- }
2974
- function exactCodexNativeInspectionComposerCapture(screen, plan) {
2975
- const lines = screen.replace(/\r\n?/gu, "\n").split("\n");
2976
- let currentComposerIndex = -1;
2977
- for (let index = lines.length - 1; index >= 0; index -= 1) {
2978
- if (CODEX_COMPOSER_MARKER.test(lines[index])) {
2979
- currentComposerIndex = index;
2980
- break;
2981
- }
2982
- }
2983
- if (currentComposerIndex < 0) {
2984
- return undefined;
2985
- }
2986
- const composerText = lines[currentComposerIndex]
2987
- .replace(/^[›»]\s?/u, "")
2988
- .trimEnd();
2989
- if (composerText !== plan.command) {
2990
- return undefined;
2991
- }
2992
- const footerIndex = lines.findIndex((line, candidateIndex) => candidateIndex > currentComposerIndex &&
2993
- CODEX_COMPOSER_FOOTER.test(line.trim()));
2994
- const region = lines.slice(currentComposerIndex, footerIndex < 0 ? lines.length : footerIndex);
2995
- while (region.length > 1 && region.at(-1)?.trim() === "") {
2996
- region.pop();
2997
- }
2998
- const popupRows = region.slice(1).filter((line) => line.trim().length > 0);
2999
- let kind;
3000
- if (popupRows.length === 0) {
3001
- kind = "exact_slash_composer";
3002
- }
3003
- else if (JSON.stringify(popupRows.map((line) => line.trimEnd())) ===
3004
- JSON.stringify(CODEX_NATIVE_STATUS_POPUP_BY_PROFILE[plan.behaviorProfile])) {
3005
- kind = "exact_slash_popup";
3006
- }
3007
- else {
3008
- return undefined;
3009
- }
3010
- return {
3011
- kind,
3012
- digest: createHash("sha256").update(region.join("\n")).digest("hex")
3013
- };
3014
- }
3015
- function exactClaudeNativeInspectionComposerCapture(screen, plan) {
3016
- const frame = exactClaudeComposerFrame(screen);
3017
- if (!frame) {
3018
- return undefined;
3019
- }
3020
- const { lines, openIndex, closeIndex, composerRows, trailing } = frame;
3021
- if (composerRows.length !== 1 ||
3022
- !/^\s*❯(?:\s|$)/u.test(composerRows[0]) ||
3023
- composerRows[0].replace(/^\s*❯\s?/u, "").trimEnd() !== plan.command) {
3024
- return undefined;
3025
- }
3026
- let kind;
3027
- if (trailing.length === 0 || claudeNativeInspectionTrailingIsFooter(trailing)) {
3028
- kind = "exact_slash_composer";
3029
- }
3030
- else {
3031
- const suggestions = [];
3032
- for (const line of trailing) {
3033
- const trimmed = line.trim();
3034
- if (trimmed.startsWith("/")) {
3035
- suggestions.push(trimmed.replace(/\s+/gu, " "));
3036
- }
3037
- else if (suggestions.length > 0) {
3038
- suggestions[suggestions.length - 1] +=
3039
- ` ${trimmed.replace(/\s+/gu, " ")}`;
3040
- }
3041
- else {
3042
- return undefined;
3043
- }
3044
- }
3045
- if (!closedClaudeNativeStatusSuggestionsMatch(suggestions, CLAUDE_NATIVE_STATUS_POPUP_BY_PROFILE[plan.behaviorProfile])) {
3046
- return undefined;
3047
- }
3048
- kind = "exact_slash_popup";
3049
- }
3050
- return {
3051
- kind,
3052
- digest: createHash("sha256")
3053
- .update(lines.slice(openIndex, closeIndex + 1).concat(trailing).join("\n"))
3054
- .digest("hex")
3055
- };
3056
- }
3057
- /**
3058
- * Claude truncates a suggestion row with a Unicode ellipsis at narrow pane
3059
- * widths. Keep authorization closed by accepting only an exact ordered row or
3060
- * an explicit ellipsis whose preceding text is an exact, non-trivial prefix of
3061
- * that same profiled row. A caller still cannot introduce, omit, or reorder a
3062
- * slash command.
3063
- */
3064
- function closedClaudeNativeStatusSuggestionsMatch(observed, expected) {
3065
- if (!expected || observed.length !== expected.length) {
3066
- return false;
3067
- }
3068
- return observed.every((row, index) => {
3069
- const exact = expected[index];
3070
- if (row === exact) {
3071
- return true;
3072
- }
3073
- if (!row.endsWith("…")) {
3074
- return false;
3075
- }
3076
- const prefix = row.slice(0, -1);
3077
- const commandEnd = exact.indexOf(" ");
3078
- return (commandEnd > 0 &&
3079
- prefix.length >= commandEnd + 12 &&
3080
- exact.startsWith(prefix));
3081
- });
3082
- }
3083
- /**
3084
- * Prove Claude Code's exact current idle input frame. This is shared by every
3085
- * automated-input path: a loose or historical `❯` prompt is not authority to
3086
- * inject text into the terminal.
3087
- */
3088
- export function isExactClaudeIdleComposer(screen) {
3089
- const frame = exactClaudeComposerFrame(screen);
3090
- if (!frame) {
3091
- return false;
3092
- }
3093
- return (frame.composerRows.length === 1 &&
3094
- /^\s*❯\s*$/u.test(frame.composerRows[0]) &&
3095
- (frame.trailing.length === 0 ||
3096
- claudeNativeInspectionTrailingIsFooter(frame.trailing)));
3097
- }
3098
- /**
3099
- * Compatibility export retained for callers that adopted the native-status
3100
- * name before the same exact-frame proof was reused by lifecycle handoff.
3101
- */
3102
- export function isExactClaudeNativeInspectionIdleComposer(screen) {
3103
- return isExactClaudeIdleComposer(screen);
3104
- }
3105
- function exactClaudeComposerFrame(screen) {
3106
- const lines = screen.replace(/\r\n?/gu, "\n").replace(/\u00a0/gu, " ")
3107
- .split("\n");
3108
- const dividerIndexes = lines
3109
- .map((line, index) => /^\s*[─━]{8,}\s*$/u.test(line) ? index : -1)
3110
- .filter((index) => index >= 0);
3111
- if (dividerIndexes.length < 2) {
3112
- return undefined;
3113
- }
3114
- const closeIndex = dividerIndexes.at(-1);
3115
- const openIndex = dividerIndexes.at(-2);
3116
- return {
3117
- lines,
3118
- openIndex,
3119
- closeIndex,
3120
- composerRows: lines.slice(openIndex + 1, closeIndex)
3121
- .filter((line) => line.trim().length > 0),
3122
- trailing: lines.slice(closeIndex + 1)
3123
- .filter((line) => line.trim().length > 0)
3124
- };
3125
- }
3126
- function exactClaudeModelControlComposerCapture(screen, plan, expectedText) {
3127
- if (plan.behaviorProfile !== "claude-model-control-2.1.266" ||
3128
- expectedText !== plan.command ||
3129
- expectedText !== "/model")
3130
- return undefined;
3131
- const frame = exactClaudeComposerFrame(screen);
3132
- if (!frame ||
3133
- frame.composerRows.length !== 1 ||
3134
- frame.composerRows[0].replace(/^\s*❯\s?/u, "").trimEnd() !== expectedText ||
3135
- frame.trailing.length > 2 ||
3136
- !claudeNativeInspectionTrailingIsFooter(frame.trailing))
3137
- return undefined;
3138
- const beforeComposer = frame.lines.slice(0, frame.openIndex);
3139
- while (beforeComposer.length > 0 &&
3140
- beforeComposer.at(-1)?.trim() === "") {
3141
- beforeComposer.pop();
3142
- }
3143
- let popupStart = beforeComposer.length;
3144
- while (popupStart > 0) {
3145
- const line = beforeComposer[popupStart - 1];
3146
- if (/^\s*(?:(?:❯|›)\s*)?\/[a-z][a-z0-9-]*(?:\s{2,}|\s*$)/iu.test(line) ||
3147
- /^\s{2,}\S/u.test(line)) {
3148
- popupStart -= 1;
3149
- continue;
3150
- }
3151
- break;
3152
- }
3153
- const popupRows = beforeComposer.slice(popupStart);
3154
- if (popupRows.length === 0 || popupRows.length > 32)
3155
- return undefined;
3156
- const suggestions = [];
3157
- for (const [lineIndex, line] of popupRows.entries()) {
3158
- const match = /^\s*(?:(❯|›)\s*)?(\/[a-z][a-z0-9-]*)(?:\s{2,}|\s*$)(.*)$/iu
3159
- .exec(line);
3160
- if (match) {
3161
- suggestions.push({
3162
- lineIndex,
3163
- selected: Boolean(match[1]),
3164
- command: match[2],
3165
- normalized: `${match[2]} ${match[3]}`.trim().replace(/\s+/gu, " ")
3166
- });
3167
- continue;
3168
- }
3169
- if (suggestions.length === 0 || !/^\s{2,}\S/u.test(line)) {
3170
- return undefined;
3171
- }
3172
- const previous = suggestions.at(-1);
3173
- previous.normalized = `${previous.normalized} ${line.trim()}`
3174
- .replace(/\s+/gu, " ");
3175
- }
3176
- if (suggestions.length === 0 || suggestions[0].lineIndex !== 0) {
3177
- return undefined;
3178
- }
3179
- const selected = suggestions.filter((row) => row.selected);
3180
- if (selected.length > 1 ||
3181
- selected.length === 1 && selected[0].lineIndex !== 0 ||
3182
- suggestions.slice(1).some((row) => row.command === "/model"))
3183
- return undefined;
3184
- const first = suggestions[0].normalized;
3185
- if (!/^\/model Set the AI model for Claude Code \(currently [A-Za-z0-9][A-Za-z0-9._:/+\[\]\- ]{0,127}\)$/u.test(first)) {
3186
- return undefined;
3187
- }
3188
- return {
3189
- digest: createHash("sha256")
3190
- .update(frame.lines.slice(frame.openIndex).join("\n"))
3191
- .digest("hex")
3192
- };
3193
- }
3194
- function claudeNativeInspectionTrailingIsFooter(lines) {
3195
- return lines.length <= 2 && lines.every(isClaudeNativeComposerFooterLine);
3196
- }
3197
- function isClaudeNativeComposerFooterLine(line) {
3198
- return /^\s*(?:[⏵⏴⏸]{1,2}|\?)\s*.*(?:manual mode|shift\+tab|accept edits|bypass permissions|for shortcuts|← for agents)/iu
3199
- .test(line);
3200
- }
3201
- function nativeInspectionScreenFingerprint(screen) {
3202
- return `sha256:${createHash("sha256").update(screen).digest("hex")}`;
3203
- }
3204
- function bareDigestFromNativeInspectionScreenFingerprint(fingerprint) {
3205
- const match = /^sha256:([0-9a-f]{64})$/u.exec(fingerprint);
3206
- if (!match) {
3207
- throw new Error("native inspection screen fingerprint is malformed");
3208
- }
3209
- return match[1];
3210
- }
3211
- function stripTerminalEscapeSequences(value) {
3212
- return value.replace(/\x1B(?:\[[0-?]*[ -/]*[@-~]|\][^\x07]*(?:\x07|\x1B\\))/gu, "");
3213
- }
3214
- export function exactCodexReadyStyledComposerCapture(screen) {
3215
- const lines = screen.replace(/\r\n?/gu, "\n").split("\n");
3216
- while (lines.length > 0 &&
3217
- stripTerminalEscapeSequences(lines.at(-1) ?? "").trim().length === 0) {
3218
- lines.pop();
3219
- }
3220
- const composerLine = [...lines.slice(-12)].reverse().find((line) => CODEX_COMPOSER_MARKER.test(stripTerminalEscapeSequences(line).trimEnd()));
3221
- if (composerLine === undefined) {
3222
- return undefined;
3223
- }
3224
- let dim = false;
3225
- const visible = [];
3226
- for (let index = 0; index < composerLine.length;) {
3227
- if (composerLine[index] === "\x1b") {
3228
- const escape = /^(?:\x1B\[([0-9;]*)m|\x1B\][^\x07]*(?:\x07|\x1B\\))/u
3229
- .exec(composerLine.slice(index));
3230
- if (escape) {
3231
- if (escape[1] !== undefined) {
3232
- const codes = escape[1] === ""
3233
- ? [0]
3234
- : escape[1].split(";").map((value) => Number(value));
3235
- for (let codeIndex = 0; codeIndex < codes.length; codeIndex += 1) {
3236
- const code = codes[codeIndex];
3237
- if ([38, 48, 58].includes(code) &&
3238
- codes[codeIndex + 1] === 2) {
3239
- codeIndex += 4;
3240
- continue;
3241
- }
3242
- if ([38, 48, 58].includes(code) &&
3243
- codes[codeIndex + 1] === 5) {
3244
- codeIndex += 2;
3245
- continue;
3246
- }
3247
- if (code === 0 || code === 22) {
3248
- dim = false;
3249
- }
3250
- else if (code === 2) {
3251
- dim = true;
3252
- }
3253
- }
3254
- }
3255
- index += escape[0].length;
3256
- continue;
3257
- }
3258
- }
3259
- const codePoint = composerLine.codePointAt(index);
3260
- if (codePoint === undefined) {
3261
- break;
3262
- }
3263
- const character = String.fromCodePoint(codePoint);
3264
- visible.push({ character, dim });
3265
- index += character.length;
3266
- }
3267
- const promptIndex = visible.findIndex(({ character }) => character === "›" || character === "»");
3268
- if (promptIndex < 0) {
3269
- return undefined;
3270
- }
3271
- const content = visible.slice(promptIndex + 1)
3272
- .filter(({ character }) => !/^\s$/u.test(character));
3273
- if (content.length > 0 && !content.every((entry) => entry.dim)) {
3274
- return undefined;
3275
- }
3276
- return {
3277
- digest: createHash("sha256").update(composerLine).digest("hex")
3278
- };
3279
- }
3280
- /**
3281
- * Infer a viewport only from fixed-width visible-buffer rows. Trimmed captures
3282
- * deliberately return undefined: a short content row is not proof of a short
3283
- * terminal. This keeps the fallback provider-neutral and fail-closed only on
3284
- * positive geometry evidence.
3285
- */
3286
- function inferCodexVisibleViewportColumns(screen) {
3287
- const rows = screen.replace(/\r\n?/gu, "\n").split("\n")
3288
- .map(stripTerminalEscapeSequences);
3289
- const widthOneRows = rows.filter((row) => /^[\x20-\x7e›»·─━╭╮╰╯│]*$/u.test(row));
3290
- const maxWidth = widthOneRows.reduce((maximum, row) => Math.max(maximum, Array.from(row).length), 0);
3291
- if (maxWidth < 20) {
3292
- return undefined;
3293
- }
3294
- const paddedAtMax = widthOneRows.filter((row) => row.endsWith(" ") && Array.from(row).length === maxWidth);
3295
- const composerAtMax = paddedAtMax.some((row) => CODEX_COMPOSER_MARKER.test(row.trimEnd()));
3296
- return paddedAtMax.length >= 3 && composerAtMax
3297
- ? maxWidth
3298
- : undefined;
3299
- }
3300
- function hasTruncatedCodexStatusSessionLine(screen) {
3301
- return screen.replace(/\r\n?/gu, "\n").split("\n").some((line) => {
3302
- const match = /^\s*│\s*Session:\s*([^│\s]+).*│?\s*$/iu.exec(line);
3303
- if (!match) {
3304
- return false;
3305
- }
3306
- 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
3307
- .test(match[1]);
3308
- });
3309
- }
3310
- function codexNativeInspectionComposerMismatchDiagnostic(screen, plan) {
3311
- const expectedRows = CODEX_NATIVE_STATUS_POPUP_BY_PROFILE[plan.behaviorProfile];
3312
- if (!expectedRows) {
3313
- return "composer_not_exact";
3314
- }
3315
- const lines = screen.replace(/\r\n?/gu, "\n").split("\n");
3316
- let composerIndex = -1;
3317
- for (let index = lines.length - 1; index >= 0; index -= 1) {
3318
- if (CODEX_COMPOSER_MARKER.test(lines[index])) {
3319
- composerIndex = index;
3320
- break;
3321
- }
3322
- }
3323
- if (composerIndex < 0 ||
3324
- lines[composerIndex].replace(/^[›»]\s?/u, "").trimEnd() !== plan.command) {
3325
- return "composer_not_exact";
3326
- }
3327
- const footerIndex = lines.findIndex((line, index) => index > composerIndex && CODEX_COMPOSER_FOOTER.test(line.trim()));
3328
- const popupRows = lines.slice(composerIndex + 1, footerIndex < 0 ? lines.length : footerIndex).filter((line) => line.trim().length > 0);
3329
- if (popupRows.length === 0) {
3330
- return "composer_not_exact";
3331
- }
3332
- const logicalRows = [];
3333
- let observedTruncation = false;
3334
- for (const row of popupRows) {
3335
- const trimmed = row.trim().replace(/\s+/gu, " ");
3336
- if (trimmed.startsWith("/")) {
3337
- logicalRows.push(trimmed);
3338
- }
3339
- else if (logicalRows.length > 0) {
3340
- logicalRows[logicalRows.length - 1] += ` ${trimmed}`;
3341
- observedTruncation = true;
3342
- }
3343
- else {
3344
- return "composer_not_exact";
3345
- }
3346
- observedTruncation ||= trimmed.endsWith("…");
3347
- }
3348
- const normalizedExpected = expectedRows.map((row) => row.trim().replace(/\s+/gu, " "));
3349
- const everyKnownPrefix = logicalRows.length <= normalizedExpected.length &&
3350
- logicalRows.every((row, index) => {
3351
- const withoutEllipsis = row.endsWith("…")
3352
- ? row.slice(0, -1).trimEnd()
3353
- : row;
3354
- return normalizedExpected[index]?.startsWith(withoutEllipsis) === true;
3355
- });
3356
- return observedTruncation && everyKnownPrefix
3357
- ? "composer_viewport_truncated"
3358
- : "composer_not_exact";
3359
- }
3360
- function codexBlockingModalVisible(screen) {
3361
- const tail = screen.replace(/\r\n?/gu, "\n").split("\n").slice(-80)
3362
- .join("\n");
3363
- return /\b(?:press|use)\s+(?:esc|escape)\s+to\s+(?:cancel|close|dismiss)\b/iu
3364
- .test(tail) ||
3365
- /\besc\s+to\s+cancel\b/iu.test(tail) ||
3366
- codexActiveWriterViewerVisible(tail) ||
3367
- ["expanded", "ambiguous"].includes(inspectCodexAsyncQuestionInputMode(tail));
3368
- }
3369
- function codexActiveWriterViewerVisible(styledScreen) {
3370
- const lines = stripTerminalEscapeSequences(styledScreen)
3371
- .replace(/\r\n?/gu, "\n")
3372
- .split("\n")
3373
- .slice(-80);
3374
- let titleIndex = -1;
3375
- for (let index = lines.length - 1; index >= 0; index -= 1) {
3376
- if (/^ {2}🔒(?:\s|$)/u.test(lines[index])) {
3377
- titleIndex = index;
3378
- break;
3379
- }
3380
- }
3381
- if (titleIndex < 0) {
3382
- return false;
3383
- }
3384
- const laterMainComposer = lines.findIndex((line, index) => index > titleIndex && CODEX_COMPOSER_MARKER.test(line));
3385
- if (laterMainComposer >= 0) {
3386
- return false;
3387
- }
3388
- const frame = lines.slice(titleIndex, titleIndex + 12)
3389
- .map((line) => line.trim().replace(/\s+/gu, " "))
3390
- .filter((line) => line.length > 0)
3391
- .join(" ");
3392
- return /🔒\s+This conversation is open in another app\s+\S+ to Retry/iu
3393
- .test(frame) &&
3394
- /Close it there and press\s+\S+\s+to continue here\./iu.test(frame) &&
3395
- /\S+ retry\s+\S+ exit(?:\s+\S+ transcript)?/iu.test(frame);
3396
- }
3397
- const CODEX_ASYNC_QUESTION_FOOTER_LABEL = /\s(submit|skip|main prompt|prev question|next question|queued messages)(?=\s{2,}|\s*$)/giu;
3398
- function codexStructuredAsyncFooterEvidence(lines, evidenceFloor) {
3399
- let strongest = { index: -1, labelCount: 0 };
3400
- for (let start = evidenceFloor + 1; start < lines.length; start += 1) {
3401
- const labels = new Set();
3402
- for (let end = start; end < Math.min(lines.length, start + 5); end += 1) {
3403
- const line = lines[end];
3404
- if (!/^ {2}(?![ ↳?])\S/u.test(line)) {
3405
- break;
3406
- }
3407
- for (const match of line.matchAll(CODEX_ASYNC_QUESTION_FOOTER_LABEL)) {
3408
- labels.add(match[1].toLowerCase());
3409
- }
3410
- if (labels.size > strongest.labelCount) {
3411
- strongest = { index: end, labelCount: labels.size };
3412
- }
3413
- if (labels.size >= 2) {
3414
- return strongest;
3415
- }
3416
- }
3417
- }
3418
- return strongest;
3419
- }
3420
- function codexMainComposerFooterIndex(lines, composerIndex) {
3421
- for (let index = lines.length - 1; index > composerIndex; index -= 1) {
3422
- // A real statusline starts at column zero. Draft continuations are inset,
3423
- // even when their text happens to look exactly like a model statusline.
3424
- if (CODEX_COMPLETE_COMPOSER_FOOTER.test(lines[index].trimEnd())) {
3425
- return index;
3426
- }
3427
- }
3428
- return -1;
3429
- }
3430
- /**
3431
- * Classify Codex 0.154's non-blocking inline-question surface by current input
3432
- * ownership. The shared `Queued follow-up inputs` heading is not sufficient:
3433
- * ordinary queued messages use it too. An exact collapsed summary, after any
3434
- * ordinary queue preview is removed, proves Send-safe ownership even when the
3435
- * Composer is outside the viewport. Positive editor evidence without a
3436
- * complete shape remains fail-closed because paste plus Enter would otherwise
3437
- * submit the user's task as an answer.
3438
- */
3439
- export function inspectCodexAsyncQuestionInputMode(styledScreen) {
3440
- const lines = stripTerminalEscapeSequences(styledScreen)
3441
- .replace(/\r\n?/gu, "\n")
3442
- .replace(/\u00a0/gu, " ")
3443
- .split("\n")
3444
- .slice(-80);
3445
- while (lines.length > 0 && lines.at(-1)?.trim().length === 0) {
3446
- lines.pop();
3447
- }
3448
- const normalized = lines.map((line) => line.trim().replace(/\s+/gu, " "));
3449
- let mainComposerIndex = -1;
3450
- for (let index = lines.length - 1; index >= 0; index -= 1) {
3451
- // The main Composer marker is column zero. Async option and inline-text
3452
- // markers are inset by the menu surface, even after ANSI is removed.
3453
- if (CODEX_COMPOSER_MARKER.test(lines[index])) {
3454
- mainComposerIndex = index;
3455
- break;
3456
- }
3457
- }
3458
- let headerIndex = -1;
3459
- let wrappedHeaderRows = 1;
3460
- for (let index = lines.length - 1; index >= 0; index -= 1) {
3461
- if (/^•\s+Queued follow-up inputs\s*$/u.test(lines[index]) ||
3462
- /^•\s+Queued follow-up inpu…\s*$/u.test(lines[index]) ||
3463
- (/^•\s+Queued follow-up\s*$/u.test(lines[index]) &&
3464
- /^ {2}inputs\s*$/u.test(lines[index + 1] ?? ""))) {
3465
- headerIndex = index;
3466
- wrappedHeaderRows = /^•\s+Queued follow-up\s*$/u.test(lines[index])
3467
- ? 2
3468
- : 1;
3469
- break;
3470
- }
3471
- }
3472
- const headerBodyIndex = headerIndex < 0
3473
- ? -1
3474
- : headerIndex + wrappedHeaderRows;
3475
- let questionEvidenceStart = headerBodyIndex;
3476
- if (headerIndex >= 0) {
3477
- let cursor = headerBodyIndex;
3478
- while (cursor < lines.length && lines[cursor].trim().length === 0) {
3479
- cursor += 1;
3480
- }
3481
- let sawQueuedPreview = false;
3482
- while (cursor < lines.length) {
3483
- if (/^ {2}↳\s/u.test(lines[cursor])) {
3484
- sawQueuedPreview = true;
3485
- cursor += 1;
3486
- while (cursor < lines.length) {
3487
- if (lines[cursor].trim().length === 0 ||
3488
- /^ {4,}\S/u.test(lines[cursor])) {
3489
- cursor += 1;
3490
- continue;
3491
- }
3492
- break;
3493
- }
3494
- continue;
3495
- }
3496
- break;
3497
- }
3498
- if (sawQueuedPreview) {
3499
- questionEvidenceStart = cursor;
3500
- }
3501
- }
3502
- const mainComposerFooterIndex = mainComposerIndex < 0
3503
- ? -1
3504
- : codexMainComposerFooterIndex(lines, mainComposerIndex);
3505
- const summaryBeforeMainComposer = headerIndex < 0 || mainComposerIndex < 0
3506
- ? -1
3507
- : normalized.findIndex((line, index) => index >= questionEvidenceStart && index < mainComposerIndex &&
3508
- /^\?\s+\d+\s+questions?(?:\s+·\s+\d+s)?$/iu.test(line));
3509
- if (mainComposerIndex >= 0 && mainComposerFooterIndex < 0) {
3510
- // A statusline-disabled Composer is the strongest available ownership
3511
- // anchor. Treat all following wrapped rows as its arbitrary draft instead
3512
- // of letting question-like user text veto a human-priority Send.
3513
- return summaryBeforeMainComposer >= 0 ? "collapsed" : "absent";
3514
- }
3515
- const evidenceFloor = Math.max(mainComposerFooterIndex, questionEvidenceStart - 1);
3516
- const structuredFooter = codexStructuredAsyncFooterEvidence(lines, evidenceFloor);
3517
- if (structuredFooter.labelCount >= 2) {
3518
- return "expanded";
3519
- }
3520
- const clippedFooterIndex = normalized.findIndex((line, index) => index > evidenceFloor && /^ente(?:r)?…$/iu.test(line) &&
3521
- /^ctrl…$/iu.test(normalized[index + 1] ?? ""));
3522
- const clippedChoiceIndex = normalized.findIndex((line, index) => index > evidenceFloor &&
3523
- line === "Expand terminal to read the entire option");
3524
- if (clippedFooterIndex >= 0 || clippedChoiceIndex >= 0) {
3525
- return "ambiguous";
3526
- }
3527
- if (headerIndex < 0) {
3528
- const evidence = new Set();
3529
- normalized.forEach((line, index) => {
3530
- if (index <= evidenceFloor) {
3531
- return;
3532
- }
3533
- if (/^\d+\s+of(?:\s+\d+)?$/iu.test(line)) {
3534
- evidence.add("progress");
3535
- }
3536
- if (/^›\s+\S/u.test(line)) {
3537
- evidence.add("selector");
3538
- }
3539
- if (/^Type your answer(?:\s*\(optional\))?$/iu.test(line)) {
3540
- evidence.add("free_text");
3541
- }
3542
- });
3543
- return evidence.size >= 2 ||
3544
- (evidence.size >= 1 && structuredFooter.labelCount >= 1)
3545
- ? "ambiguous"
3546
- : "absent";
3547
- }
3548
- const weakQuestionIndex = normalized.findIndex((line, index) => index > evidenceFloor && index >= questionEvidenceStart && (/^\d+\s+of(?:\s+\d+)?$/iu.test(line) ||
3549
- /^›\s+\S/u.test(line) ||
3550
- /^Type your answer(?:\s*\(optional\))?$/iu.test(line)));
3551
- if (mainComposerIndex > headerIndex && weakQuestionIndex > evidenceFloor) {
3552
- return "ambiguous";
3553
- }
3554
- const summaryIndex = normalized.findIndex((line, index) => index >= questionEvidenceStart &&
3555
- (mainComposerIndex < 0 || index < mainComposerIndex) &&
3556
- /^\?\s+\d+\s+questions?(?:\s+·\s+\d+s)?$/iu.test(line));
3557
- if (summaryIndex >= 0) {
3558
- return weakQuestionIndex >= 0 ? "ambiguous" : "collapsed";
3559
- }
3560
- if (mainComposerIndex > headerIndex) {
3561
- return "absent";
3562
- }
3563
- const afterHeader = normalized.slice(questionEvidenceStart)
3564
- .filter((line) => line.length > 0);
3565
- const positiveAfterHeader = afterHeader.some((line) => /^\d+\s+of(?:\s+\d+)?$/iu.test(line) ||
3566
- /^›\s+\d+\.(?:\s|$)/u.test(line) ||
3567
- /^Type your answer(?:\s*\(optional\))?$/iu.test(line));
3568
- if (positiveAfterHeader) {
3569
- return "ambiguous";
3570
- }
3571
- // A bare heading (optionally followed by ↳ queued-message previews) is not
3572
- // question authority. Preserve human-explicit Send in that ordinary state.
3573
- if (afterHeader.length === 0) {
3574
- return "absent";
3575
- }
3576
- // An expanded editor can temporarily replace its footer with a flash. If
3577
- // there is question-like content under the shared heading and no proven main
3578
- // Composer, do not guess which input surface will receive the next paste.
3579
- return "ambiguous";
3580
- }
3581
- /**
3582
- * Classify only the bottom live Codex composer region. A matching transcript
3583
- * prompt elsewhere in scrollback is deliberately ignored.
3584
- */
3585
- function currentCodexComposerCapture(styledScreen, expectedText, allowOpaqueLargePastePlaceholder = false, classifyOpaqueLargePasteAsDifferent = false, exactSlashPopupRows) {
3586
- const screen = stripTerminalEscapeSequences(styledScreen);
3587
- const lines = screen.replace(/\r\n?/gu, "\n").split("\n");
3588
- while (lines.length > 0 && lines.at(-1)?.trim().length === 0) {
3589
- lines.pop();
3590
- }
3591
- let composerIndex = -1;
3592
- for (let index = lines.length - 1; index >= 0; index -= 1) {
3593
- if (CODEX_COMPOSER_MARKER.test(lines[index])) {
3594
- composerIndex = index;
3595
- break;
3596
- }
3597
- }
3598
- if (composerIndex < 0) {
3599
- return undefined;
3600
- }
3601
- const footerIndex = lines.findIndex((line, index) => index > composerIndex &&
3602
- CODEX_COMPLETE_COMPOSER_FOOTER.test(line.trim()));
3603
- if (footerIndex >= 0 &&
3604
- lines.slice(footerIndex + 1).some((line) => line.trim().length > 0)) {
3605
- return undefined;
3606
- }
3607
- const regionEnd = footerIndex < 0 ? lines.length : footerIndex;
3608
- const region = lines.slice(composerIndex, regionEnd);
3609
- while (region.length > 1 && region.at(-1)?.trim().length === 0) {
3610
- region.pop();
3611
- }
3612
- if (region.length === 0 ||
3613
- region.slice(1).some((line) => line.length > 0 && !line.startsWith(" "))) {
3614
- return undefined;
3615
- }
3616
- const bodyRows = [
3617
- region[0].replace(/^[›»]\s?/u, ""),
3618
- ...region.slice(1).map((line) => line.startsWith(" ") ? line.slice(2) : line)
3619
- ];
3620
- const digest = createHash("sha256").update(region.join("\n")).digest("hex");
3621
- const positivelyEmpty = footerIndex >= 0 &&
3622
- bodyRows.slice(1).every((row) => row.trim().length === 0) &&
3623
- exactCodexReadyStyledComposerCapture(styledScreen) !== undefined;
3624
- if (positivelyEmpty) {
3625
- return { state: "exact_empty", digest };
3626
- }
3627
- const expectedComparable = composerComparableText(expectedText);
3628
- const exactStyledProfiledCommand = exactSlashPopupRows !== undefined &&
3629
- exactCodexStyledCommandComposerCapture(styledScreen, expectedText) !== undefined;
3630
- const exactFooterlessProfiledCommand = footerIndex < 0 &&
3631
- exactStyledProfiledCommand;
3632
- // Codex 0.154 replaces the ordinary model/cwd footer with its slash
3633
- // completion surface. Real terminal renderers may retain one or more blank
3634
- // layout rows between the Composer and that surface. The exact styled
3635
- // command is sufficient only for reversible cleanup; Enter additionally
3636
- // requires the unique, ordered, version-profiled completion rows below.
3637
- const popupRows = region.slice(1).map((row) => row.trimEnd());
3638
- const exactPopupLayout = exactSlashPopupRows !== undefined && (JSON.stringify(popupRows) === JSON.stringify(exactSlashPopupRows) ||
3639
- popupRows.length === exactSlashPopupRows.length + 1 &&
3640
- popupRows[0] === "" &&
3641
- JSON.stringify(popupRows.slice(1)) ===
3642
- JSON.stringify(exactSlashPopupRows));
3643
- const exactProfiledSlashPopup = exactSlashPopupRows !== undefined &&
3644
- composerComparableText(bodyRows[0] ?? "").trimEnd() ===
3645
- expectedComparable &&
3646
- exactPopupLayout;
3647
- if (footerIndex < 0 && !exactFooterlessProfiledCommand) {
3648
- return undefined;
3649
- }
3650
- const expectedCharacterCount = Array.from(expectedText).length;
3651
- const comparable = composerComparableText(bodyRows.join("\n"));
3652
- const opaqueLargePastePlaceholder = /^\[Pasted Content \d+ chars\]$/u.test(comparable);
3653
- if (opaqueLargePastePlaceholder && !allowOpaqueLargePastePlaceholder) {
3654
- return classifyOpaqueLargePasteAsDifferent
3655
- ? { state: "different_draft", digest }
3656
- : undefined;
3657
- }
3658
- // Herdr's ANSI visible buffer preserves Codex's fixed-width Composer paint:
3659
- // the typed command is followed by layout padding through the viewport edge.
3660
- // Accept that padding only when the closed 0.154 profile, full-row
3661
- // background paint, exact command text, and complete model/cwd footer all
3662
- // agree. Plain padded text remains untrusted.
3663
- const exactVisibleDraft = footerIndex >= 0 && (terminalComposerRowsMatchExpected(bodyRows, expectedComparable) ||
3664
- exactStyledProfiledCommand);
3665
- const exactLargePastePlaceholder = allowOpaqueLargePastePlaceholder &&
3666
- expectedCharacterCount > CODEX_LARGE_PASTE_CHAR_THRESHOLD &&
3667
- comparable === composerComparableText(`[Pasted Content ${expectedCharacterCount} chars]`);
3668
- if (exactVisibleDraft ||
3669
- exactLargePastePlaceholder ||
3670
- exactProfiledSlashPopup ||
3671
- exactFooterlessProfiledCommand) {
3672
- return {
3673
- state: "exact_draft",
3674
- digest,
3675
- ...(exactProfiledSlashPopup ? { profiledSlashPopup: true } : {}),
3676
- ...(exactVisibleDraft && footerIndex >= 0
3677
- ? { bareCommand: true }
3678
- : {})
3679
- };
3680
- }
3681
- return comparable.length > 0
3682
- ? { state: "different_draft", digest }
3683
- : undefined;
3684
- }
3685
- /**
3686
- * Prove the live footerless Codex slash Composer without trusting transcript
3687
- * text alone. The 0.154 TUI paints this row across the current viewport while
3688
- * its completion popup replaces the ordinary model/cwd footer.
3689
- */
3690
- function exactCodexStyledCommandComposerCapture(styledScreen, expectedText) {
3691
- const rows = styledScreen.replace(/\r\n?/gu, "\n").split("\n");
3692
- const viewportColumns = inferCodexVisibleViewportColumns(styledScreen);
3693
- if (!viewportColumns)
3694
- return undefined;
3695
- let composerLine;
3696
- for (let index = rows.length - 1; index >= 0; index -= 1) {
3697
- const plain = stripTerminalEscapeSequences(rows[index]);
3698
- if (CODEX_COMPOSER_MARKER.test(plain)) {
3699
- composerLine = rows[index];
3700
- break;
3701
- }
3702
- }
3703
- if (!composerLine)
3704
- return undefined;
3705
- const plain = stripTerminalEscapeSequences(composerLine);
3706
- const exactCommand = plain.trimEnd().replace(/^[›»]\s?/u, "") ===
3707
- expectedText;
3708
- const spansViewport = Array.from(plain).length === viewportColumns &&
3709
- plain.endsWith(" ");
3710
- const hasBackground = [...composerLine.matchAll(/\x1b\[([0-9;]*)m/gu)]
3711
- .some((match) => match[1].split(";").map(Number).includes(48));
3712
- if (!exactCommand || !spansViewport || !hasBackground) {
3713
- return undefined;
3714
- }
3715
- return {
3716
- digest: createHash("sha256").update(composerLine).digest("hex")
3717
- };
3718
- }
3719
- function exactCodexComposerCapture(screen, expectedText) {
3720
- const lines = screen.replace(/\r\n?/gu, "\n").split("\n");
3721
- const expectedComparable = composerComparableText(expectedText);
3722
- const expectedCharacterCount = Array.from(expectedText).length;
3723
- const largePasteComparable = composerComparableText(`[Pasted Content ${expectedCharacterCount} chars]`);
3724
- const matches = [];
3725
- for (let index = 0; index < lines.length; index += 1) {
3726
- if (!CODEX_COMPOSER_MARKER.test(lines[index])) {
3727
- continue;
3728
- }
3729
- const footerIndex = lines.findIndex((line, candidateIndex) => candidateIndex > index && CODEX_COMPOSER_FOOTER.test(line.trim()));
3730
- const region = lines.slice(index, footerIndex < 0 ? lines.length : footerIndex);
3731
- while (region.length > 1 && region.at(-1)?.trim() === "") {
3732
- region.pop();
3733
- }
3734
- const bodyRows = [
3735
- region[0].replace(/^[›»]\s?/u, ""),
3736
- ...region.slice(1).map((line) => line.startsWith(" ") ? line.slice(2) : line)
3737
- ];
3738
- const comparable = composerComparableText(bodyRows.join("\n"));
3739
- const exactVisibleDraft = terminalComposerRowsMatchExpected(bodyRows, expectedComparable);
3740
- const exactLargePastePlaceholder = expectedCharacterCount > CODEX_LARGE_PASTE_CHAR_THRESHOLD &&
3741
- comparable === largePasteComparable;
3742
- if (!exactVisibleDraft && !exactLargePastePlaceholder) {
3743
- continue;
3744
- }
3745
- matches.push({
3746
- digest: createHash("sha256")
3747
- .update(region.join("\n"))
3748
- .digest("hex")
3749
- });
3750
- }
3751
- if (matches.length > 1) {
3752
- throw new Error("multiple Codex composer regions matched the multiline terminal request");
3753
- }
3754
- return matches[0];
3755
- }
3756
- /**
3757
- * Terminal UIs paint wrapped composer content as independent screen rows, so
3758
- * a provider capture cannot distinguish a visual wrap from an authored
3759
- * newline. Align the rows against the exact text AKK injected instead of
3760
- * joining every row with `\n`.
3761
- *
3762
- * Only row boundaries are ambiguous: they may consume an authored newline, an
3763
- * omitted run of ASCII spaces at a word wrap, or no character at a CJK/token
3764
- * wrap. Every visible character remains character-for-character exact, and an
3765
- * empty row can only advance through an authored newline (plus terminal-trimmed
3766
- * spaces before it), so blank-line structure is preserved.
3767
- */
3768
- function terminalComposerRowsMatchExpected(rows, expectedText) {
3769
- const expected = composerComparableText(expectedText);
3770
- if (rows.length === 0) {
3771
- return expected.length === 0;
3772
- }
3773
- if (!expected.startsWith(rows[0])) {
3774
- return false;
3775
- }
3776
- let offsets = new Set([rows[0].length]);
3777
- for (let index = 1; index < rows.length && offsets.size > 0; index += 1) {
3778
- const row = rows[index];
3779
- const previousRow = rows[index - 1];
3780
- const nextOffsets = new Set();
3781
- for (const offset of offsets) {
3782
- const candidateStarts = new Set();
3783
- if (expected[offset] === "\n") {
3784
- candidateStarts.add(offset + 1);
3785
- }
3786
- let whitespaceEnd = offset;
3787
- while (expected[whitespaceEnd] === " ") {
3788
- whitespaceEnd += 1;
3789
- }
3790
- if (previousRow.length > 0 && row.length > 0) {
3791
- candidateStarts.add(offset);
3792
- if (whitespaceEnd > offset) {
3793
- candidateStarts.add(whitespaceEnd);
3794
- }
3795
- }
3796
- if (whitespaceEnd > offset &&
3797
- expected[whitespaceEnd] === "\n") {
3798
- candidateStarts.add(whitespaceEnd + 1);
3799
- }
3800
- for (const candidateStart of candidateStarts) {
3801
- if (expected.startsWith(row, candidateStart)) {
3802
- nextOffsets.add(candidateStart + row.length);
3803
- }
3804
- }
3805
- }
3806
- offsets = nextOffsets;
3807
- }
3808
- return offsets.has(expected.length);
3809
- }
3810
- function exactTerminalComposerCapture(agent, screen, expectedText) {
3811
- return agent === "codex"
3812
- ? exactCodexComposerCapture(screen, expectedText)
3813
- : exactClaudeComposerCapture(screen, expectedText);
3814
- }
3815
- function exactClaudeComposerCapture(screen, expectedText) {
3816
- const lines = screen.replace(/\r\n?/gu, "\n").split("\n");
3817
- const dividerIndexes = lines
3818
- .map((line, index) => /^\s*[─━]{8,}\s*$/u.test(line) ? index : -1)
3819
- .filter((index) => index >= 0);
3820
- if (dividerIndexes.length < 2) {
3821
- return undefined;
3822
- }
3823
- const closeIndex = dividerIndexes.at(-1);
3824
- const openIndex = dividerIndexes.at(-2);
3825
- const trailing = lines.slice(closeIndex + 1)
3826
- .filter((line) => line.trim().length > 0);
3827
- if (trailing.length > 2 ||
3828
- trailing.some((line) => !/^\s*(?:[⏵⏴⏸]{1,2}|\?)\s*.*(?:manual mode|shift\+tab|accept edits|bypass permissions|for shortcuts|← for agents)/iu
3829
- .test(line))) {
3830
- return undefined;
3831
- }
3832
- const region = lines.slice(openIndex + 1, closeIndex);
3833
- while (region.length > 1 && region.at(-1)?.trim() === "") {
3834
- region.pop();
3835
- }
3836
- if (region.length === 0 || !/^\s*❯(?:\s|$)/u.test(region[0])) {
3837
- return undefined;
3838
- }
3839
- const bodyRows = [
3840
- region[0].replace(/^\s*❯\s?/u, ""),
3841
- ...region.slice(1).map((line) => line.startsWith(" ") ? line.slice(2) : line)
3842
- ];
3843
- if (!terminalComposerRowsMatchExpected(bodyRows, expectedText)) {
3844
- return undefined;
3845
- }
3846
- return {
3847
- digest: createHash("sha256")
3848
- .update(lines.slice(openIndex, closeIndex + 1).join("\n"))
3849
- .digest("hex")
3850
- };
3851
- }
3852
- function exactClaudeInjectedPastePlaceholderCapture(screen, expectedText) {
3853
- const expectedNewlineCount = (composerComparableText(expectedText).match(/\n/gu) ?? []).length;
3854
- if (expectedNewlineCount === 0) {
3855
- return undefined;
3856
- }
3857
- const frame = exactClaudeComposerFrame(screen);
3858
- if (!frame ||
3859
- frame.composerRows.length !== 1 ||
3860
- !claudeInjectedPastePlaceholderTrailingMatches(frame.trailing)) {
3861
- return undefined;
3862
- }
3863
- const placeholder = /^\s*❯\s*\[Pasted text #([1-9]\d*) \+([1-9]\d*) lines\]\s*$/u
3864
- .exec(frame.composerRows[0]);
3865
- if (!placeholder) {
3866
- return undefined;
3867
- }
3868
- const pasteId = Number(placeholder[1]);
3869
- const newlineCount = Number(placeholder[2]);
3870
- if (!Number.isSafeInteger(pasteId) ||
3871
- !Number.isSafeInteger(newlineCount) ||
3872
- newlineCount !== expectedNewlineCount) {
3873
- return undefined;
3874
- }
3875
- return {
3876
- state: "exact_injected_paste_placeholder",
3877
- digest: createHash("sha256")
3878
- .update(frame.lines
3879
- .slice(frame.openIndex, frame.closeIndex + 1)
3880
- .concat(frame.trailing)
3881
- .join("\n"))
3882
- .digest("hex"),
3883
- pasteId,
3884
- newlineCount,
3885
- frameProfile: CLAUDE_INJECTED_PASTE_FRAME_PROFILE
3886
- };
3887
- }
3888
- function claudeInjectedPastePlaceholderTrailingMatches(lines) {
3889
- if (lines.length === 0) {
3890
- return false;
3891
- }
3892
- const hint = "paste again to expand";
3893
- const first = lines[0].trimStart();
3894
- if (!first.startsWith(hint)) {
3895
- return false;
3896
- }
3897
- const sameRowStatus = first.slice(hint.length);
3898
- if (sameRowStatus.length > 0 && !/^\s{2,}\S/u.test(sameRowStatus)) {
3899
- return false;
3900
- }
3901
- const footerRows = [
3902
- ...(sameRowStatus.trim().length > 0 ? [sameRowStatus.trim()] : []),
3903
- ...lines.slice(1).map((line) => line.trim())
3904
- ];
3905
- return footerRows.length === 0 ||
3906
- claudeNativeInspectionTrailingIsFooter(footerRows) ||
3907
- claudeInjectedPasteAuxiliaryFooterMatches(footerRows);
3908
- }
3909
- function claudeInjectedPasteAuxiliaryFooterMatches(lines) {
3910
- if (lines.length === 0 || lines.length > 2) {
3911
- return false;
3912
- }
3913
- const autoUpdate = lines.filter((line) => line === "✘ Auto-update failed · Run claude doctor").length;
3914
- const effort = lines.filter((line) => /^[●○◐◉] (?:low|medium|high|xhigh|max|ultracode) · \/effort$/u
3915
- .test(line)).length;
3916
- return autoUpdate <= 1 && effort <= 1 &&
3917
- autoUpdate + effort === lines.length;
3918
- }
3919
- function composerComparableText(value) {
3920
- return value.replace(/\r\n?/gu, "\n");
3921
- }
3922
- async function terminalSettleDelay(milliseconds) {
3923
- await new Promise((resolve) => setTimeout(resolve, milliseconds));
3924
- }
3925
- function sameTerminalControlIdentity(left, right) {
3926
- return sameTerminalControlIncarnation(left, right);
3927
- }
3928
- export function terminalApprovalFingerprint(agent, terminalControl, inspection, options = {}) {
3929
- if (!inspection.approval.approvable) {
3930
- return undefined;
1281
+ export function terminalApprovalFingerprint(agent, terminalControl, inspection, options = {}) {
1282
+ if (!inspection.approval.approvable) {
1283
+ return undefined;
3931
1284
  }
3932
1285
  const decision = options.decision ?? "approve_once";
3933
1286
  const choices = terminalApprovalChoices(inspection.approval);
@@ -4035,430 +1388,6 @@ function isTerminalApprovalPromptEvidence(value) {
4035
1388
  typeof evidence.sha256 === "string" &&
4036
1389
  /^[0-9a-f]{64}$/u.test(evidence.sha256);
4037
1390
  }
4038
- function terminalInteractionTurnId(runtime) {
4039
- // Questionnaire mutation is managed-Turn-only. A raw terminal's
4040
- // conversationId is a discovery label, not durable response authority.
4041
- const candidate = runtime?.turnId;
4042
- return typeof candidate === "string" &&
4043
- TERMINAL_INTERACTION_IDENTIFIER_PATTERN.test(candidate)
4044
- ? candidate
4045
- : undefined;
4046
- }
4047
- function terminalInteractionSubject(runtime) {
4048
- if (runtime?.interactionSubject) {
4049
- return runtime.interactionSubject;
4050
- }
4051
- const turnId = terminalInteractionTurnId(runtime);
4052
- const messageId = runtime?.messageId;
4053
- return turnId && typeof messageId === "string" &&
4054
- TERMINAL_INTERACTION_IDENTIFIER_PATTERN.test(messageId)
4055
- ? { kind: "managed_turn", turn_id: turnId, message_id: messageId }
4056
- : undefined;
4057
- }
4058
- function terminalInteractionExpiry(now) {
4059
- const nowMs = now.getTime();
4060
- if (!Number.isFinite(nowMs)) {
4061
- return undefined;
4062
- }
4063
- const windowStart = Math.floor(nowMs / TERMINAL_INTERACTION_TTL_MS) *
4064
- TERMINAL_INTERACTION_TTL_MS;
4065
- return new Date(windowStart + TERMINAL_INTERACTION_TTL_MS).toISOString();
4066
- }
4067
- function terminalInteractionCanonicalIdentity(terminalControl) {
4068
- if (!hasCanonicalTerminalEndpoint(terminalControl)) {
4069
- return undefined;
4070
- }
4071
- const terminal = terminalEndpointFromControlRef(terminalControl);
4072
- if (!Number.isInteger(terminal.processAnchorPid) ||
4073
- Number(terminal.processAnchorPid) <= 0) {
4074
- return undefined;
4075
- }
4076
- // Surface identity is shared by independent observers. Keep physical
4077
- // terminal identity separate from runtime-native identity so an optional
4078
- // observer-only fence (for example processStartedAt) cannot split one live
4079
- // questionnaire into different Monitor and Watch surfaces. Dispatch still
4080
- // revalidates the complete runtime immediately before any terminal input.
4081
- return {
4082
- terminal_identity: terminalEndpointIdentityKey(terminal),
4083
- terminal_process_anchor_pid: terminal.processAnchorPid
4084
- };
4085
- }
4086
- function terminalInteractionNativeTaskIdentity(runtime) {
4087
- // Include only native-task fields that both managed Turn and exact Watch
4088
- // observations can derive from their durable anchors. Optional runtime
4089
- // verification fields remain on `runtime`; they are identity fences, not
4090
- // cross-observer surface identity.
4091
- return {
4092
- agent_pid: runtime.pid,
4093
- native_session_id: runtime.nativeSessionId,
4094
- native_process_uuid: runtime.nativeProcessUuid,
4095
- native_process_birth: runtime.nativeProcessBirth,
4096
- native_rollout: runtime.nativeRollout,
4097
- expected_native_session_id: runtime.expectedNativeSessionId,
4098
- expected_empty_native_session: runtime.expectedEmptyNativeSession === true
4099
- };
4100
- }
4101
- function legacyManagedInteractionProjection(projection) {
4102
- if (projection.subject.kind !== "managed_turn") {
4103
- throw new TypeError("only managed interaction subjects have a v1 projection");
4104
- }
4105
- return validateTerminalInteractionProjection({
4106
- schema: TERMINAL_INTERACTION_SCHEMA,
4107
- version: TERMINAL_INTERACTION_VERSION,
4108
- interaction_id: projection.interaction_id,
4109
- turn_id: projection.subject.turn_id,
4110
- agent: projection.agent,
4111
- kind: projection.kind,
4112
- state: projection.state,
4113
- step: projection.step,
4114
- questions: projection.questions,
4115
- expires_at: projection.expires_at,
4116
- capabilities: projection.capabilities
4117
- });
4118
- }
4119
- export function captureTerminalInteractionRuntimeOffer(input) {
4120
- const subject = terminalInteractionSubject(input.runtime);
4121
- const agentVersion = input.runtime?.agentVersion;
4122
- const expiry = terminalInteractionExpiry(input.now);
4123
- const canonicalIdentity = input.runtime
4124
- ? terminalInteractionCanonicalIdentity(input.terminalControl) ??
4125
- terminalInteractionTrustedWatchIdentity(input.terminalControl, input.runtime, input.trustedTerminalEvidence)
4126
- : undefined;
4127
- if (!subject ||
4128
- typeof agentVersion !== "string" ||
4129
- agentVersion.length === 0 ||
4130
- !expiry ||
4131
- !canonicalIdentity ||
4132
- input.approvalBlocked === true) {
4133
- return undefined;
4134
- }
4135
- if (exactCurrentModelControlSurface(input.agent, agentVersion, input.screen)) {
4136
- return undefined;
4137
- }
4138
- const coreOffer = captureTerminalInteraction({
4139
- subject,
4140
- agent: input.agent,
4141
- agentVersion,
4142
- canonicalTerminalIdentity: canonicalIdentity,
4143
- nativeTaskIdentity: terminalInteractionNativeTaskIdentity(input.runtime),
4144
- screen: input.screen,
4145
- now: input.now,
4146
- expiresAt: expiry,
4147
- responseUncertain: input.runtime?.interactionDispatchState === "reserved" ||
4148
- input.runtime?.interactionDispatchState === "uncertain",
4149
- responseAuthority: input.runtime?.interactionResponseAuthority ?? "executable",
4150
- includeLegacyTurnId: subject.kind === "managed_turn"
4151
- });
4152
- if (!coreOffer) {
4153
- return undefined;
4154
- }
4155
- return {
4156
- projection: subject.kind === "managed_turn"
4157
- ? legacyManagedInteractionProjection(coreOffer.projection)
4158
- : coreOffer.projection,
4159
- promptFingerprint: coreOffer.promptFingerprint,
4160
- surfaceId: coreOffer.surfaceId,
4161
- actionPlan: coreOffer.actionPlan,
4162
- nativeInspection: coreOffer.nativeInspection
4163
- };
4164
- }
4165
- function exactCurrentModelControlSurface(agent, agentVersion, screen) {
4166
- if (!agentVersion)
4167
- return false;
4168
- try {
4169
- const capabilities = probeTerminalModelControl(agent, agentVersion);
4170
- if (capabilities.status !== "supported")
4171
- return false;
4172
- const observation = observeTerminalModelControl(planTerminalModelControl(capabilities), screen);
4173
- return observation.state !== "none" &&
4174
- observation.state !== "ambiguous";
4175
- }
4176
- catch {
4177
- return false;
4178
- }
4179
- }
4180
- function terminalInteractionTrustedWatchIdentity(terminalControl, runtime, evidence) {
4181
- if (runtime.interactionSubject?.kind !== "terminal_watch" ||
4182
- !evidence ||
4183
- !terminalControlEvidenceMatches(evidence, terminalControl)) {
4184
- return undefined;
4185
- }
4186
- const identity = terminalEndpointIdentityFromEvidence(evidence);
4187
- if (!identity || !Number.isSafeInteger(evidence.process_anchor_pid) ||
4188
- Number(evidence.process_anchor_pid) <= 0) {
4189
- return undefined;
4190
- }
4191
- return {
4192
- terminal_identity: terminalEndpointIdentityKey(identity),
4193
- terminal_process_anchor_pid: evidence.process_anchor_pid
4194
- };
4195
- }
4196
- function terminalInteractionOptionPreflightReason(options, runtime) {
4197
- if (typeof options.agentVersion !== "string" ||
4198
- !/^\d+\.\d+\.\d+(?:[-+][A-Za-z0-9.-]+)?$/u.test(options.agentVersion)) {
4199
- return "an exact semantic agent version is required";
4200
- }
4201
- if (options.runtime?.agentVersion !== undefined &&
4202
- options.runtime.agentVersion !== options.agentVersion) {
4203
- return "runtime agent version does not match the requested interaction profile";
4204
- }
4205
- if (!terminalInteractionSubject(runtime)) {
4206
- return "a protocol-safe exact interaction subject is required";
4207
- }
4208
- if (!TERMINAL_INTERACTION_FINGERPRINT_PATTERN.test(options.expectedFingerprint)) {
4209
- return "a valid interaction prompt fingerprint is required";
4210
- }
4211
- if (!Number.isFinite(Date.parse(options.expectedExpiresAt))) {
4212
- return "a valid interaction expiry is required";
4213
- }
4214
- return undefined;
4215
- }
4216
- function terminalInteractionOfferPreflightReason(offer, options, now) {
4217
- if (!offer) {
4218
- return "native questionnaire has no identity-fenced interaction offer";
4219
- }
4220
- if (offer.projection.state !== "pending" ||
4221
- !offer.projection.capabilities.respond ||
4222
- offer.actionPlan.kind === "manual_only") {
4223
- return `native questionnaire requires manual response: ${offer.nativeInspection.status === "manual_required"
4224
- ? offer.nativeInspection.reason
4225
- : "unsupported action plan"}`;
4226
- }
4227
- if (offer.promptFingerprint !== options.expectedFingerprint) {
4228
- return "native questionnaire fingerprint changed before execution";
4229
- }
4230
- if (offer.projection.expires_at !== options.expectedExpiresAt &&
4231
- Date.parse(options.expectedExpiresAt) > now.getTime()) {
4232
- return "native questionnaire expiry changed before execution";
4233
- }
4234
- return undefined;
4235
- }
4236
- function terminalInteractionCapabilityReason(provider, terminalControl, plan) {
4237
- const requiresText = plan.kind === "free_text";
4238
- const missing = [
4239
- !terminalControl.capabilities.includes("screen_status")
4240
- ? "terminal:screen_status"
4241
- : undefined,
4242
- !terminalControl.capabilities.includes("send_keys")
4243
- ? "terminal:send_keys"
4244
- : undefined,
4245
- !provider.providerCapabilities.includes("stable_resource_resolution")
4246
- ? "provider:stable_resource_resolution"
4247
- : undefined,
4248
- !provider.providerCapabilities.includes("screen_capture")
4249
- ? "provider:screen_capture"
4250
- : undefined,
4251
- !provider.providerCapabilities.includes("key_delivery")
4252
- ? "provider:key_delivery"
4253
- : undefined,
4254
- requiresText && !provider.providerCapabilities.includes("text_delivery")
4255
- ? "provider:text_delivery"
4256
- : undefined
4257
- ].filter((value) => value !== undefined);
4258
- return missing.length > 0
4259
- ? `terminal interaction capability preflight failed: ${missing.join(", ")}`
4260
- : undefined;
4261
- }
4262
- function terminalInteractionPlanPreflightReason(response, plan) {
4263
- const answer = response.answers[0];
4264
- if (plan.kind !== "free_text" || answer?.response_kind !== "free_text") {
4265
- return undefined;
4266
- }
4267
- const textStage = plan.stages[0];
4268
- if (textStage?.kind !== "answer_text" ||
4269
- !Number.isSafeInteger(textStage.max_characters) ||
4270
- textStage.max_characters < 1) {
4271
- return "native free-text interaction has no verified input limit";
4272
- }
4273
- return answer.text.length <= textStage.max_characters
4274
- ? undefined
4275
- : `free-text answer exceeds the verified native limit of ${textStage.max_characters} characters`;
4276
- }
4277
- function blockedTerminalInteractionResponse(response, reason, offer) {
4278
- const answer = Array.isArray(response.answers) ? response.answers[0] : undefined;
4279
- return {
4280
- responded: false,
4281
- blocked: true,
4282
- reason,
4283
- interactionId: response.interaction_id,
4284
- questionId: answer?.question_id ?? offer?.projection.questions[0]?.question_id,
4285
- responseKind: answer?.response_kind
4286
- };
4287
- }
4288
- function interactionHookContext(agent, terminalControl, offer, response, runtime) {
4289
- return {
4290
- agent,
4291
- terminalControl,
4292
- fingerprint: offer.promptFingerprint,
4293
- projection: offer.projection,
4294
- response,
4295
- runtime
4296
- };
4297
- }
4298
- async function authorizeTerminalInteraction(authorize, context) {
4299
- return authorize ? authorize(context) : { approved: true };
4300
- }
4301
- async function reserveTerminalInteractionDispatch(beforeDispatch, context) {
4302
- if (!beforeDispatch) {
4303
- return;
4304
- }
4305
- try {
4306
- await beforeDispatch(context);
4307
- }
4308
- catch (error) {
4309
- if (error instanceof TerminalInteractionInputNotStartedError) {
4310
- throw error;
4311
- }
4312
- throw terminalInteractionReservedError("reservation_uncertain", error, "terminal interaction dispatch reservation failed");
4313
- }
4314
- }
4315
- function sameTerminalInteractionOffer(left, right, options = {}) {
4316
- const leftProjection = options.ignoreExpiresAt && left.offer
4317
- ? terminalInteractionProjectionWithoutExpiry(left.offer.projection)
4318
- : left.offer?.projection;
4319
- const rightProjection = options.ignoreExpiresAt && right.offer
4320
- ? terminalInteractionProjectionWithoutExpiry(right.offer.projection)
4321
- : right.offer?.projection;
4322
- return Boolean(left.offer &&
4323
- right.offer &&
4324
- sameTerminalControlIdentity(left.terminalControl, right.terminalControl) &&
4325
- left.offer.promptFingerprint === right.offer.promptFingerprint &&
4326
- JSON.stringify(leftProjection) === JSON.stringify(rightProjection) &&
4327
- JSON.stringify(left.offer.actionPlan) === JSON.stringify(right.offer.actionPlan));
4328
- }
4329
- function terminalInteractionProjectionWithoutExpiry(projection) {
4330
- const { expires_at: _expiresAt, ...semanticProjection } = projection;
4331
- return semanticProjection;
4332
- }
4333
- function terminalInteractionInputNotStartedError(error, message) {
4334
- if (error instanceof TerminalInteractionInputNotStartedError) {
4335
- return error;
4336
- }
4337
- const detail = error instanceof Error ? error.message : String(error);
4338
- return new TerminalInteractionInputNotStartedError(`${message}: ${detail}`, { cause: error });
4339
- }
4340
- function terminalInteractionReservedError(stage, error, message) {
4341
- if (error instanceof TerminalInteractionDispatchReservedError) {
4342
- return error;
4343
- }
4344
- const detail = error instanceof Error ? error.message : String(error);
4345
- return new TerminalInteractionDispatchReservedError(stage, `${message}: ${detail}`, { cause: error });
4346
- }
4347
- function terminalInteractionChoiceAction(agent, plan, answer) {
4348
- const choice = plan.choices.find((candidate) => candidate.option_id === answer.selected_option_ids[0]);
4349
- if (!choice) {
4350
- throw new TerminalInteractionInputNotStartedError("single-select interaction has no closed adapter action");
4351
- }
4352
- const keys = choice.stages.flatMap((stage) => stage.kind === "key" ? [stage.key] : []);
4353
- const directChoice = choice.outcome === "submit_or_advance" &&
4354
- choice.stages.length === 1 &&
4355
- keys.length === 1 &&
4356
- /^[1-5]$/u.test(keys[0]);
4357
- const directCustomTextChoice = agent === "claude" &&
4358
- choice.outcome === "open_custom_text" &&
4359
- choice.stages.length === 1 &&
4360
- keys.length === 1 &&
4361
- /^[1-5]$/u.test(keys[0]);
4362
- const navigatedCustomTextChoice = agent === "codex" &&
4363
- choice.outcome === "open_custom_text" &&
4364
- choice.stages.length >= 1 &&
4365
- choice.stages.length <= 4 &&
4366
- keys.length === choice.stages.length &&
4367
- keys.at(-1) === "C-m" &&
4368
- keys.slice(0, -1).every((key) => key === "Down");
4369
- if (!directChoice && !directCustomTextChoice && !navigatedCustomTextChoice) {
4370
- throw new TerminalInteractionInputNotStartedError("single-select interaction has no closed adapter action");
4371
- }
4372
- return {
4373
- keys,
4374
- outcome: choice.outcome === "open_custom_text"
4375
- ? "custom_text_opened"
4376
- : "submitted_or_advanced"
4377
- };
4378
- }
4379
- function terminalInteractionConfirmKey(plan, answer) {
4380
- const stages = answer.confirm ? plan.confirm_stages : plan.cancel_stages;
4381
- const stage = stages[0];
4382
- if (stages.length !== 1 ||
4383
- stage?.kind !== "key" ||
4384
- !["C-m", "Escape", "1", "2"].includes(stage.key) ||
4385
- JSON.stringify(plan.confirm_stages) === JSON.stringify(plan.cancel_stages)) {
4386
- throw new TerminalInteractionInputNotStartedError("confirmation interaction has no closed adapter action");
4387
- }
4388
- return stage.key;
4389
- }
4390
- async function sendTerminalInteractionKeys(provider, terminalControl, keys) {
4391
- try {
4392
- await provider.sendKeys(provider.endpoint(terminalControl), keys);
4393
- }
4394
- catch (error) {
4395
- throw terminalInteractionReservedError("key_uncertain", error, "terminal interaction key dispatch is uncertain");
4396
- }
4397
- }
4398
- async function dispatchTerminalInteractionFreeText(input) {
4399
- const [textStage, enterStage] = input.plan.stages;
4400
- if (input.plan.stages.length !== 2 ||
4401
- textStage?.kind !== "answer_text" ||
4402
- textStage.single_line !== true ||
4403
- textStage.max_characters < input.answer.text.length ||
4404
- enterStage?.kind !== "key" ||
4405
- enterStage.key !== "C-m") {
4406
- throw new TerminalInteractionInputNotStartedError("free-text interaction has no closed adapter action");
4407
- }
4408
- try {
4409
- await input.provider.sendText(input.provider.endpoint(input.terminalControl), input.answer.text);
4410
- }
4411
- catch (error) {
4412
- throw terminalInteractionReservedError("text_uncertain", error, "terminal interaction text dispatch is uncertain");
4413
- }
4414
- if (input.agent === "codex") {
4415
- try {
4416
- await input.sleep(CODEX_PASTE_ENTER_SETTLE_MS);
4417
- }
4418
- catch (error) {
4419
- throw terminalInteractionReservedError("text_uncertain", error, "terminal interaction paste-settle outcome is uncertain");
4420
- }
4421
- }
4422
- let enterControl;
4423
- try {
4424
- enterControl = await input.reverifyTerminalIdentity(input.terminalControl);
4425
- if (!sameTerminalControlIdentity(input.terminalControl, enterControl)) {
4426
- throw new Error("terminal identity changed between interaction text and Enter");
4427
- }
4428
- }
4429
- catch (error) {
4430
- throw terminalInteractionReservedError("text_uncertain", error, "terminal identity after interaction text dispatch is uncertain");
4431
- }
4432
- await sendTerminalInteractionKeys(input.provider, enterControl, [enterStage.key]);
4433
- return "submitted_or_advanced";
4434
- }
4435
- async function dispatchTerminalInteractionAnswer(input) {
4436
- if (input.actionPlan.kind === "single_select" &&
4437
- input.answer.response_kind === "single_select") {
4438
- const action = terminalInteractionChoiceAction(input.agent, input.actionPlan, input.answer);
4439
- await sendTerminalInteractionKeys(input.provider, input.terminalControl, action.keys);
4440
- return action.outcome;
4441
- }
4442
- if (input.actionPlan.kind === "confirm" &&
4443
- input.answer.response_kind === "confirm") {
4444
- const key = terminalInteractionConfirmKey(input.actionPlan, input.answer);
4445
- await sendTerminalInteractionKeys(input.provider, input.terminalControl, [key]);
4446
- return input.answer.confirm ? "confirmed" : "cancelled";
4447
- }
4448
- if (input.actionPlan.kind === "free_text" &&
4449
- input.answer.response_kind === "free_text") {
4450
- return dispatchTerminalInteractionFreeText({
4451
- agent: input.agent,
4452
- provider: input.provider,
4453
- terminalControl: input.terminalControl,
4454
- plan: input.actionPlan,
4455
- answer: input.answer,
4456
- reverifyTerminalIdentity: input.reverifyTerminalIdentity,
4457
- sleep: input.sleep
4458
- });
4459
- }
4460
- throw new TerminalInteractionInputNotStartedError("terminal interaction answer does not have a closed adapter action");
4461
- }
4462
1391
  function statusFromInspection(adapter, terminalControl, inspection, options = {}) {
4463
1392
  const exactModelSurface = options.screen !== undefined &&
4464
1393
  exactCurrentModelControlSurface(adapter.agent, options.runtime?.agentVersion, options.screen);
@@ -4546,7 +1475,7 @@ function statusFromInspection(adapter, terminalControl, inspection, options = {}
4546
1475
  approval: approvalOutput(approval)
4547
1476
  },
4548
1477
  ...(interaction === undefined ? {} : {
4549
- interaction_state: interaction.projection,
1478
+ interaction_state: terminalInteractionPublicCompatibilityProjection(interaction.projection),
4550
1479
  interaction_prompt_fingerprint: interaction.promptFingerprint,
4551
1480
  interaction_surface_id: interaction.surfaceId
4552
1481
  })
@@ -4674,11 +1603,11 @@ function assertTerminalModelControlPlan(adapter, terminalControl, agentVersion,
4674
1603
  JSON.stringify(expected) !== JSON.stringify(plan)) {
4675
1604
  throw new Error("the terminal adapter did not produce the exact current model-control plan");
4676
1605
  }
4677
- if (adapter.agent === "codex"
4678
- ? plan.behaviorProfile !== "codex-model-control-0.154.0" ||
4679
- plan.scope !== "current_and_new_sessions"
4680
- : plan.behaviorProfile !== "claude-model-control-2.1.266" ||
4681
- plan.scope !== "current_session") {
1606
+ if (!terminalModelControlPlanConforms({
1607
+ agent: adapter.agent,
1608
+ agentVersion,
1609
+ plan
1610
+ })) {
4682
1611
  throw new Error("refusing an unprofiled terminal model-control plan");
4683
1612
  }
4684
1613
  }