@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,5 +1,10 @@
1
1
  import { createHash } from "node:crypto";
2
+ import { CODEX_MODEL_CONTROL_AGENT_VERSION, isTerminalModelControlPlanForAgent, planTerminalModelControlProfile, probeTerminalModelControlProfile, terminalModelControlProfileForPlan } from "./terminal-model-control-profile.js";
3
+ import { canonicalModelControlSubject } from "./terminal-model-control-subject.js";
4
+ import { decideTerminalModelControlFailure, reduceTerminalModelControlTransactionPhase } from "./terminal-model-control-transaction.js";
2
5
  import { terminalPhysicalBindingToken } from "./terminal-control-ref.js";
6
+ export { CLAUDE_MODEL_CONTROL_AGENT_VERSION, CODEX_MODEL_CONTROL_AGENT_VERSION, TERMINAL_MODEL_CONTROL_PROFILE_IDS, isTerminalModelControlPlanForAgent, terminalModelControlPlanConforms, terminalModelControlProfileFor, terminalModelControlProfileForPlan, terminalModelControlProfiles, terminalModelControlSlashCompletionRows } from "./terminal-model-control-profile.js";
7
+ export { canonicalModelControlSubject } from "./terminal-model-control-subject.js";
3
8
  /** Caller-visible reasoning values. Native labels and menu positions stay private. */
4
9
  export const TERMINAL_MODEL_REASONING_EFFORTS = [
5
10
  "low",
@@ -15,25 +20,36 @@ export const TERMINAL_MODEL_REASONING_EFFORTS = [
15
20
  * as Send or native-lifecycle authority even though all bind the same pane.
16
21
  */
17
22
  export function terminalUserExplicitModelControlBindingToken(value) {
23
+ const subject = requiredModelControlSubject({
24
+ ...value,
25
+ agent: "codex"
26
+ });
18
27
  const physicalTerminalToken = terminalPhysicalBindingToken({
19
- terminalId: value.terminalId,
20
- terminalControl: value.terminalControl,
28
+ terminalId: subject.terminalId,
29
+ terminalControl: subject.terminalControl,
21
30
  agent: "codex",
22
- pid: value.pid,
23
- workspace: value.workspace,
24
- processUuid: value.processUuid,
25
- processBirth: value.processBirth
31
+ pid: subject.pid,
32
+ workspace: subject.workspace,
33
+ processUuid: subject.processUuid,
34
+ processBirth: subject.processBirth
26
35
  });
27
36
  return createHash("sha256")
28
37
  .update(JSON.stringify({
29
38
  version: 1,
30
39
  authority: "terminal_user_explicit_model_control",
31
40
  physical_terminal_token: physicalTerminalToken,
32
- agent_version: value.agentVersion,
33
- behavior_profile: value.behaviorProfile
41
+ agent_version: subject.agentVersion,
42
+ behavior_profile: subject.behaviorProfile
34
43
  }))
35
44
  .digest("hex");
36
45
  }
46
+ function requiredModelControlSubject(input) {
47
+ const subject = canonicalModelControlSubject(input);
48
+ if (!subject) {
49
+ throw new Error("model-control authority requires one canonical supported terminal subject");
50
+ }
51
+ return subject;
52
+ }
37
53
  /**
38
54
  * Snapshot authority for adopting and cleaning one exact native `/model`
39
55
  * residual. The residual kind and normalized surface digest prevent a token
@@ -162,63 +178,10 @@ export function isTerminalModelReasoningEffort(value) {
162
178
  TERMINAL_MODEL_REASONING_EFFORTS.includes(value);
163
179
  }
164
180
  export function probeTerminalModelControl(agent, agentVersion) {
165
- if (!agentVersion) {
166
- return {
167
- status: "unknown",
168
- modelSelection: false,
169
- reasoningEffortSelection: false,
170
- reason: `the running ${agent === "codex" ? "Codex" : "Claude Code"} version could not be verified`
171
- };
172
- }
173
- if (agent === "codex" && agentVersion === "0.154.0") {
174
- return {
175
- status: "supported",
176
- agentVersion,
177
- behaviorProfile: "codex-model-control-0.154.0",
178
- scope: "current_and_new_sessions",
179
- modelSelection: true,
180
- reasoningEffortSelection: true,
181
- reason: "Codex 0.154.0 /model control changes the current session and persisted defaults"
182
- };
183
- }
184
- if (agent === "claude" && agentVersion === "2.1.266") {
185
- return {
186
- status: "supported",
187
- agentVersion,
188
- behaviorProfile: "claude-model-control-2.1.266",
189
- scope: "current_session",
190
- modelSelection: true,
191
- reasoningEffortSelection: true,
192
- reason: "Claude Code 2.1.266 /model supports the explicit session-only selection path"
193
- };
194
- }
195
- return {
196
- status: "unsupported",
197
- agentVersion,
198
- modelSelection: false,
199
- reasoningEffortSelection: false,
200
- reason: `${agent === "codex" ? "Codex" : "Claude Code"} ${agentVersion} has no verified model-control profile`
201
- };
181
+ return probeTerminalModelControlProfile(agent, agentVersion);
202
182
  }
203
183
  export function planTerminalModelControl(capabilities) {
204
- if (capabilities.status !== "supported" ||
205
- capabilities.modelSelection !== true ||
206
- capabilities.reasoningEffortSelection !== true ||
207
- !capabilities.behaviorProfile ||
208
- !capabilities.scope) {
209
- throw new Error(capabilities.reason);
210
- }
211
- if (capabilities.behaviorProfile !== "codex-model-control-0.154.0" &&
212
- capabilities.behaviorProfile !== "claude-model-control-2.1.266") {
213
- throw new Error("refusing an unprofiled terminal model-control plan");
214
- }
215
- return {
216
- behaviorProfile: capabilities.behaviorProfile,
217
- command: "/model",
218
- scope: capabilities.scope,
219
- requiresIdle: true,
220
- requiresExactEmptyComposer: true
221
- };
184
+ return planTerminalModelControlProfile(capabilities);
222
185
  }
223
186
  /**
224
187
  * Observe only complete, current model-control modal frames. Historical or
@@ -226,7 +189,7 @@ export function planTerminalModelControl(capabilities) {
226
189
  */
227
190
  export function observeTerminalModelControl(plan, screen) {
228
191
  const normalized = stripAnsi(screen).replace(/\r\n?/gu, "\n");
229
- return plan.behaviorProfile === "codex-model-control-0.154.0"
192
+ return isTerminalModelControlPlanForAgent(plan, "codex")
230
193
  ? observeCodexModelControl(normalized)
231
194
  : observeClaudeModelControl(normalized);
232
195
  }
@@ -302,44 +265,167 @@ export function claudeModelControlEffortKey(direction) {
302
265
  return [direction === "lower" ? "Left" : "Right"];
303
266
  }
304
267
  export function claudeModelControlCommitKeys(plan) {
305
- if (plan.behaviorProfile !== "claude-model-control-2.1.266" ||
268
+ if (!isTerminalModelControlPlanForAgent(plan, "claude") ||
306
269
  plan.scope !== "current_session") {
307
270
  throw new Error("Claude model control cannot commit outside session-only scope");
308
271
  }
309
272
  return ["s"];
310
273
  }
274
+ function isExactTerminalModelControlObservation(observation) {
275
+ return observation.state !== "none" && observation.state !== "ambiguous";
276
+ }
277
+ /** Pure capture classifier. It never authorizes input by itself. */
278
+ export function classifyTerminalModelControlSurface(plan, capture) {
279
+ const observation = observeTerminalModelControl(plan, capture.screen);
280
+ // Approval and a positively identified non-model input owner remain hard
281
+ // safety boundaries even when stale transcript text resembles a picker.
282
+ const hardBlock = modelControlHardBlock(capture);
283
+ if (hardBlock)
284
+ return hardBlock;
285
+ // Exact profile-owned surfaces outrank generic lifecycle activity. Codex and
286
+ // Claude can report working/waiting while their native picker owns input.
287
+ if (isExactTerminalModelControlObservation(observation)) {
288
+ return {
289
+ state: "picker",
290
+ pickerKind: modelControlPickerKind(observation),
291
+ observation
292
+ };
293
+ }
294
+ // Only a complete picker owns the lifecycle override. Composer proofs do
295
+ // not supersede genuine working/approval lifecycle state.
296
+ const activityBlock = modelControlActivityBlock(capture);
297
+ if (activityBlock)
298
+ return activityBlock;
299
+ return classifyModelControlComposerSurface(capture, observation);
300
+ }
301
+ function modelControlHardBlock(capture) {
302
+ if (capture.approvalBlocked) {
303
+ return { state: "blocked", owner: "approval" };
304
+ }
305
+ return capture.inputBlocked === true
306
+ ? { state: "blocked", owner: "input" }
307
+ : undefined;
308
+ }
309
+ function modelControlActivityBlock(capture) {
310
+ return capture.activityState === "working" ||
311
+ capture.activityState === "awaiting_approval"
312
+ ? { state: "blocked", owner: "agent" }
313
+ : undefined;
314
+ }
315
+ function modelControlPickerKind(observation) {
316
+ if (observation.state === "codex_reasoning_picker" ||
317
+ observation.state === "codex_advanced_reasoning_picker") {
318
+ return "effort";
319
+ }
320
+ return observation.state === "codex_plan_scope_picker" ? "scope" : "model";
321
+ }
322
+ function classifyModelControlComposerSurface(capture, observation) {
323
+ const contradictoryComposerProof = capture.exactEmptyComposer && capture.exactCommandComposer ||
324
+ capture.exactCommandReady && capture.exactBareCommand === true ||
325
+ (capture.exactCommandReady || capture.exactBareCommand === true) &&
326
+ !capture.exactCommandComposer;
327
+ if (contradictoryComposerProof) {
328
+ return {
329
+ state: "unknown",
330
+ reason: "model-control Composer proofs conflict"
331
+ };
332
+ }
333
+ if (capture.exactCommandComposer &&
334
+ capture.exactCommandReady) {
335
+ return {
336
+ state: "command_popup",
337
+ ...(capture.exactCommandFingerprint
338
+ ? { fingerprint: capture.exactCommandFingerprint }
339
+ : {})
340
+ };
341
+ }
342
+ if (capture.exactCommandComposer &&
343
+ capture.exactBareCommand === true) {
344
+ return {
345
+ state: "bare_command",
346
+ ...(capture.exactCommandFingerprint
347
+ ? { fingerprint: capture.exactCommandFingerprint }
348
+ : {})
349
+ };
350
+ }
351
+ if (capture.exactCommandComposer) {
352
+ return {
353
+ state: "command_draft",
354
+ ...(capture.exactCommandFingerprint
355
+ ? { fingerprint: capture.exactCommandFingerprint }
356
+ : {})
357
+ };
358
+ }
359
+ if (capture.exactEmptyComposer && capture.activityState === "idle") {
360
+ return { state: "idle_empty" };
361
+ }
362
+ return {
363
+ state: "unknown",
364
+ reason: observation.state === "ambiguous"
365
+ ? observation.reason
366
+ : "no exact current model-control surface is proven"
367
+ };
368
+ }
311
369
  const MODEL_CONTROL_SETTLE_TIMEOUT_MS = 5_000;
312
370
  const MODEL_CONTROL_POLL_MS = 40;
371
+ function advanceTerminalModelControlTransaction(transaction, event) {
372
+ transaction.phase = reduceTerminalModelControlTransactionPhase(transaction.phase, event);
373
+ }
374
+ /** Track every nested native write even when its transport receipt rejects. */
375
+ function beginTerminalModelControlTransaction(input) {
376
+ const transaction = { phase: "no_input" };
377
+ return {
378
+ ...input,
379
+ transaction,
380
+ ports: {
381
+ ...input.ports,
382
+ sendText: async (terminalControl, text) => {
383
+ advanceTerminalModelControlTransaction(transaction, "reversible_input_attempted");
384
+ await input.ports.sendText(terminalControl, text);
385
+ },
386
+ sendKeys: async (terminalControl, keys) => {
387
+ advanceTerminalModelControlTransaction(transaction, "reversible_input_attempted");
388
+ await input.ports.sendKeys(terminalControl, keys);
389
+ }
390
+ }
391
+ };
392
+ }
393
+ function terminalModelControlInputAttempted(transaction) {
394
+ return transaction.phase === "reversible_input" ||
395
+ transaction.phase === "commit_attempted";
396
+ }
397
+ async function settleTerminalModelControlFailure(input, terminalControl) {
398
+ const pending = decideTerminalModelControlFailure(input.transaction.phase, "not_attempted");
399
+ if (!pending.unwind)
400
+ return { decision: pending };
401
+ const cleanupError = await tryUnwindModelControl(input, terminalControl);
402
+ return {
403
+ decision: decideTerminalModelControlFailure(input.transaction.phase, cleanupError ? "failed" : "proven"),
404
+ ...(cleanupError ? { cleanupError } : {})
405
+ };
406
+ }
313
407
  function modelControlCaptureBlocked(capture) {
314
408
  return capture.approvalBlocked || capture.inputBlocked === true;
315
409
  }
316
- function modelCommandReadyForEnter(plan, capture) {
317
- return capture.exactCommandReady ||
318
- plan.behaviorProfile === "codex-model-control-0.154.0" &&
319
- capture.exactBareCommand === true;
320
- }
321
410
  function residualFromCapture(plan, capture) {
322
- if (plan.behaviorProfile !== "codex-model-control-0.154.0") {
411
+ const profile = terminalModelControlProfileForPlan(plan);
412
+ if (!profile?.supportsResidualRepair) {
323
413
  return {
324
414
  state: "unsafe",
325
- reason: "model-control residual repair is profiled only for Codex 0.154.0",
415
+ reason: "model-control residual repair is profiled only for Codex " +
416
+ CODEX_MODEL_CONTROL_AGENT_VERSION,
326
417
  terminalControl: capture.terminalControl
327
418
  };
328
419
  }
329
- const modelObservation = observeTerminalModelControl(plan, capture.screen);
330
- const exactModelSurface = modelObservation.state !== "none" &&
331
- modelObservation.state !== "ambiguous";
332
- if (capture.approvalBlocked ||
333
- capture.inputBlocked === true && !exactModelSurface ||
334
- (capture.activityState === "working" ||
335
- capture.activityState === "awaiting_approval") && !exactModelSurface) {
420
+ const surface = classifyTerminalModelControlSurface(plan, capture);
421
+ if (surface.state === "blocked") {
336
422
  return {
337
423
  state: "unsafe",
338
424
  reason: "another prompt or active agent state owns terminal input",
339
425
  terminalControl: capture.terminalControl
340
426
  };
341
427
  }
342
- if (exactModelSurface) {
428
+ if (surface.state === "picker") {
343
429
  return {
344
430
  state: "recoverable",
345
431
  kind: "model_surface",
@@ -348,25 +434,31 @@ function residualFromCapture(plan, capture) {
348
434
  version: 1,
349
435
  behavior_profile: plan.behaviorProfile,
350
436
  kind: "model_surface",
351
- surface_state: modelObservation.state,
352
- surface_fingerprint: modelObservation.fingerprint
437
+ surface_state: surface.observation.state,
438
+ surface_fingerprint: surface.observation.fingerprint
353
439
  }))
354
440
  .digest("hex"),
355
441
  terminalControl: capture.terminalControl
356
442
  };
357
443
  }
358
- if (!capture.exactCommandComposer ||
359
- !capture.exactCommandFingerprint ||
360
- (!capture.exactCommandReady && capture.exactBareCommand !== true)) {
444
+ if (surface.state !== "command_popup" &&
445
+ surface.state !== "bare_command") {
361
446
  return {
362
447
  state: "absent",
363
- reason: capture.exactEmptyComposer
448
+ reason: surface.state === "idle_empty"
364
449
  ? "the Composer is already empty"
365
450
  : "the current Composer is not one exact profiled /model residual",
366
451
  terminalControl: capture.terminalControl
367
452
  };
368
453
  }
369
- const kind = capture.exactCommandReady
454
+ if (!surface.fingerprint) {
455
+ return {
456
+ state: "absent",
457
+ reason: "the current Composer is not one exact profiled /model residual",
458
+ terminalControl: capture.terminalControl
459
+ };
460
+ }
461
+ const kind = surface.state === "command_popup"
370
462
  ? "profiled_command_popup"
371
463
  : "bare_command";
372
464
  return {
@@ -377,7 +469,7 @@ function residualFromCapture(plan, capture) {
377
469
  version: 1,
378
470
  behavior_profile: plan.behaviorProfile,
379
471
  kind,
380
- exact_command_fingerprint: capture.exactCommandFingerprint
472
+ exact_command_fingerprint: surface.fingerprint
381
473
  }))
382
474
  .digest("hex"),
383
475
  terminalControl: capture.terminalControl
@@ -425,21 +517,15 @@ export async function repairTerminalModelControlResidual(input) {
425
517
  if (observed.fingerprint !== input.expectedResidualFingerprint) {
426
518
  throw new Error("the exact /model residual changed after authorization; refresh AKK list");
427
519
  }
428
- let terminalInputAttempted = false;
429
- const ports = {
430
- ...input.ports,
431
- sendKeys: async (terminalControl, keys) => {
432
- terminalInputAttempted = true;
433
- await input.ports.sendKeys(terminalControl, keys);
434
- }
435
- };
520
+ const operation = beginTerminalModelControlTransaction(input);
436
521
  try {
437
- const cleared = await unwindModelControl({ plan: input.plan, ports }, observed.terminalControl);
438
- if (modelControlCaptureBlocked(cleared) ||
439
- cleared.activityState !== "idle" ||
440
- !cleared.exactEmptyComposer) {
522
+ const cleared = await unwindModelControl(operation, observed.terminalControl);
523
+ if (classifyTerminalModelControlSurface(input.plan, cleared).state !==
524
+ "idle_empty") {
441
525
  throw new Error("cleanup did not prove an exact idle empty Composer");
442
526
  }
527
+ const terminalInputAttempted = terminalModelControlInputAttempted(operation.transaction);
528
+ advanceTerminalModelControlTransaction(operation.transaction, "postcondition_proven");
443
529
  return {
444
530
  outcome: "repaired",
445
531
  terminalControl: cleared.terminalControl,
@@ -449,14 +535,15 @@ export async function repairTerminalModelControlResidual(input) {
449
535
  };
450
536
  }
451
537
  catch (error) {
452
- if (!terminalInputAttempted)
538
+ const decision = decideTerminalModelControlFailure(operation.transaction.phase, "failed");
539
+ if (decision.outcome === "throw")
453
540
  throw error;
454
541
  return {
455
542
  outcome: "uncertain",
456
543
  terminalControl: observed.terminalControl,
457
544
  terminalInputAttempted: true,
458
545
  composerPostcondition: "unproven",
459
- doNotRetry: true,
546
+ doNotRetry: decision.doNotRetry,
460
547
  reason: error instanceof Error ? error.message : String(error)
461
548
  };
462
549
  }
@@ -466,14 +553,15 @@ export async function repairTerminalModelControlResidual(input) {
466
553
  * adapter-owned `/model` surface and always returns through exact Escape paths.
467
554
  */
468
555
  export async function discoverTerminalModelOptions(input) {
469
- const codexNativeCatalog = input.agent === "codex"
470
- ? await input.ports.loadCodexCatalog?.()
556
+ const operation = beginTerminalModelControlTransaction(input);
557
+ const codexNativeCatalog = operation.agent === "codex"
558
+ ? await operation.ports.loadCodexCatalog?.()
471
559
  : undefined;
472
- if (input.agent === "codex" && !codexNativeCatalog) {
560
+ if (operation.agent === "codex" && !codexNativeCatalog) {
473
561
  throw new Error("Codex model discovery requires the exact running executable's validated model catalog");
474
562
  }
475
- let control = input.terminalControl;
476
- const opened = await openModelPicker(input, control);
563
+ let control = operation.terminalControl;
564
+ const opened = await openModelPicker(operation, control);
477
565
  control = opened.capture.terminalControl;
478
566
  const picker = opened.observation;
479
567
  try {
@@ -499,7 +587,7 @@ export async function discoverTerminalModelOptions(input) {
499
587
  }]
500
588
  : [];
501
589
  });
502
- const dismissed = await exactDismiss(input, control, picker);
590
+ const dismissed = await exactDismiss(operation, control, picker);
503
591
  control = dismissed.terminalControl;
504
592
  // An adopted profiled slash popup temporarily replaces the ordinary
505
593
  // footer. Once the read-only picker is dismissed, the exact idle footer
@@ -518,7 +606,7 @@ export async function discoverTerminalModelOptions(input) {
518
606
  let livePicker = picker;
519
607
  models = [];
520
608
  for (const row of picker.rows) {
521
- const inspected = await inspectClaudeModelEfforts(input, control, livePicker, row.id);
609
+ const inspected = await inspectClaudeModelEfforts(operation, control, livePicker, row.id);
522
610
  control = inspected.capture.terminalControl;
523
611
  livePicker = inspected.observation;
524
612
  if (inspected.reasoningEfforts.length > 0) {
@@ -529,7 +617,7 @@ export async function discoverTerminalModelOptions(input) {
529
617
  });
530
618
  }
531
619
  }
532
- control = (await exactDismiss(input, control, livePicker)).terminalControl;
620
+ control = (await exactDismiss(operation, control, livePicker)).terminalControl;
533
621
  }
534
622
  if (models.length === 0) {
535
623
  throw new Error("the live native model picker had no verified model choices");
@@ -539,23 +627,25 @@ export async function discoverTerminalModelOptions(input) {
539
627
  reasoningEffort: currentEffort
540
628
  };
541
629
  const fingerprintInput = {
542
- agent: input.agent,
543
- agentVersion: input.agentVersion,
544
- behaviorProfile: input.plan.behaviorProfile,
545
- scope: input.plan.scope,
630
+ agent: operation.agent,
631
+ agentVersion: operation.agentVersion,
632
+ behaviorProfile: operation.plan.behaviorProfile,
633
+ scope: operation.plan.scope,
546
634
  current,
547
635
  models
548
636
  };
549
- return {
637
+ const result = {
550
638
  terminalControl: control,
551
639
  catalog: {
552
640
  ...fingerprintInput,
553
641
  catalogFingerprint: terminalModelCatalogFingerprint(fingerprintInput)
554
642
  }
555
643
  };
644
+ advanceTerminalModelControlTransaction(operation.transaction, "postcondition_proven");
645
+ return result;
556
646
  }
557
647
  catch (error) {
558
- const cleanupError = await tryUnwindModelControl(input, control);
648
+ const { cleanupError } = await settleTerminalModelControlFailure(operation, control);
559
649
  const detail = error instanceof Error ? error.message : String(error);
560
650
  if (cleanupError) {
561
651
  throw new Error(`${detail}; exact native model-control cleanup failed: ${cleanupError}`);
@@ -594,22 +684,22 @@ export async function switchTerminalModel(input) {
594
684
  doNotRetry: false
595
685
  };
596
686
  }
597
- let irreversible = false;
687
+ const operation = beginTerminalModelControlTransaction(input);
598
688
  let control = discovery.terminalControl;
599
689
  let codexPersistenceBaseline;
600
690
  let codexPersistenceScreen;
601
691
  try {
602
- const opened = await openModelPicker(input, control);
692
+ const opened = await openModelPicker(operation, control);
603
693
  control = opened.capture.terminalControl;
604
694
  const picker = opened.observation;
605
695
  if (picker.state === "codex_model_picker") {
606
696
  codexPersistenceBaseline = opened.idleCapture.screen;
607
- const committed = await applyCodexModelSelection(input, control, picker, input.request, () => { irreversible = true; });
697
+ const committed = await applyCodexModelSelection(operation, control, picker, input.request, () => advanceTerminalModelControlTransaction(operation.transaction, "commit_attempted"));
608
698
  control = committed.capture.terminalControl;
609
699
  codexPersistenceScreen = committed.capture.screen;
610
700
  }
611
701
  else if (picker.state === "claude_model_picker") {
612
- const selected = await transitionModelControl(input, control, picker, modelControlSelectModelKeys(picker, input.request.model), ["claude_model_picker"]);
702
+ const selected = await transitionModelControl(operation, control, picker, modelControlSelectModelKeys(picker, input.request.model), ["claude_model_picker"]);
613
703
  control = selected.capture.terminalControl;
614
704
  if (selected.observation.state !== "claude_model_picker") {
615
705
  throw new Error("Claude model selection did not remain in its picker");
@@ -629,7 +719,7 @@ export async function switchTerminalModel(input) {
629
719
  throw new Error("the requested effort is absent from Claude's live effort ring");
630
720
  }
631
721
  seen.add(displayed);
632
- const next = await transitionModelControl(input, control, effortPicker, claudeModelControlEffortKey("higher"), ["claude_model_picker"]);
722
+ const next = await transitionModelControl(operation, control, effortPicker, claudeModelControlEffortKey("higher"), ["claude_model_picker"]);
633
723
  control = next.capture.terminalControl;
634
724
  if (next.observation.state !== "claude_model_picker") {
635
725
  throw new Error("Claude effort adjustment left the model picker");
@@ -645,9 +735,9 @@ export async function switchTerminalModel(input) {
645
735
  if (selectedClaudeModelId(effortPicker) !== input.request.model) {
646
736
  throw new Error("Claude changed the highlighted model before session-only commit");
647
737
  }
648
- irreversible = true;
649
- await exactDispatchKeys(input, control, effortPicker, claudeModelControlCommitKeys(input.plan));
650
- const closed = await waitForObservation(input, control, ["none"], {
738
+ advanceTerminalModelControlTransaction(operation.transaction, "commit_attempted");
739
+ await exactDispatchKeys(operation, control, effortPicker, claudeModelControlCommitKeys(input.plan));
740
+ const closed = await waitForObservation(operation, control, ["none"], {
651
741
  allowImmediateNone: true
652
742
  });
653
743
  control = closed.capture.terminalControl;
@@ -656,7 +746,7 @@ export async function switchTerminalModel(input) {
656
746
  throw new Error("the native model picker changed before selection");
657
747
  }
658
748
  let persistence = input.agent === "codex"
659
- ? await waitForCodexPersistencePostcondition(input, control, codexPersistenceBaseline ?? "", input.request, codexPersistenceScreen)
749
+ ? await waitForCodexPersistencePostcondition(operation, control, codexPersistenceBaseline ?? "", input.request, codexPersistenceScreen)
660
750
  : { proven: true };
661
751
  let post = await discoverTerminalModelOptions({
662
752
  ...input, terminalControl: control
@@ -670,14 +760,14 @@ export async function switchTerminalModel(input) {
670
760
  effective.model === input.request.model &&
671
761
  effective.reasoningEffort === discovery.catalog.current.reasoningEffort &&
672
762
  effective.reasoningEffort !== requestedEffort) {
673
- const reopened = await openModelPicker(input, control);
763
+ const reopened = await openModelPicker(operation, control);
674
764
  if (reopened.observation.state !== "codex_model_picker") {
675
765
  throw new Error("Codex Plan-mode reconciliation did not open its model picker");
676
766
  }
677
767
  const secondBaseline = reopened.idleCapture.screen;
678
- const reconciled = await applyCodexModelSelection(input, reopened.capture.terminalControl, reopened.observation, input.request, () => { irreversible = true; });
768
+ const reconciled = await applyCodexModelSelection(operation, reopened.capture.terminalControl, reopened.observation, input.request, () => advanceTerminalModelControlTransaction(operation.transaction, "commit_attempted"));
679
769
  control = reconciled.capture.terminalControl;
680
- persistence = await waitForCodexPersistencePostcondition(input, control, secondBaseline, input.request, reconciled.capture.screen);
770
+ persistence = await waitForCodexPersistencePostcondition(operation, control, secondBaseline, input.request, reconciled.capture.screen);
681
771
  post = await discoverTerminalModelOptions({
682
772
  ...input, terminalControl: control
683
773
  });
@@ -717,6 +807,7 @@ export async function switchTerminalModel(input) {
717
807
  : { reasoningEffort: requestedEffort })
718
808
  }
719
809
  : undefined;
810
+ advanceTerminalModelControlTransaction(operation.transaction, "postcondition_proven");
720
811
  return {
721
812
  terminalControl: control,
722
813
  outcome: "changed",
@@ -729,8 +820,8 @@ export async function switchTerminalModel(input) {
729
820
  };
730
821
  }
731
822
  catch (error) {
732
- const cleanupError = await tryUnwindModelControl(input, control);
733
- if (!irreversible && !cleanupError)
823
+ const { decision, cleanupError } = await settleTerminalModelControlFailure(operation, control);
824
+ if (decision.outcome === "throw")
734
825
  throw error;
735
826
  const detail = error instanceof Error ? error.message : String(error);
736
827
  return {
@@ -739,14 +830,14 @@ export async function switchTerminalModel(input) {
739
830
  scope: input.plan.scope,
740
831
  defaultsChanged: null,
741
832
  requested: input.request,
742
- doNotRetry: true,
833
+ doNotRetry: decision.doNotRetry,
743
834
  reason: cleanupError
744
835
  ? `${detail}; exact native model-control cleanup failed: ${cleanupError}`
745
836
  : detail
746
837
  };
747
838
  }
748
839
  }
749
- async function applyCodexModelSelection(input, terminalControl, picker, request, beforeIrreversible) {
840
+ async function applyCodexModelSelection(input, terminalControl, picker, request, beforeCommitAttempt) {
750
841
  const target = picker.rows.find((row) => row.id === request.model);
751
842
  if (!target) {
752
843
  throw new Error("the requested Codex model left the current native picker");
@@ -761,7 +852,7 @@ async function applyCodexModelSelection(input, terminalControl, picker, request,
761
852
  }
762
853
  // A remotely refreshed model can become single-effort after discovery; in
763
854
  // that case Enter may commit immediately instead of opening a submenu.
764
- beforeIrreversible();
855
+ beforeCommitAttempt();
765
856
  const reasoning = await transitionModelControl(input, moved.capture.terminalControl, moved.observation, ["C-m"], ["codex_reasoning_picker"]);
766
857
  if (reasoning.observation.state !== "codex_reasoning_picker" ||
767
858
  reasoning.observation.model !== request.model) {
@@ -772,7 +863,7 @@ async function applyCodexModelSelection(input, terminalControl, picker, request,
772
863
  const advanced = request.reasoningEffort === "max" ||
773
864
  request.reasoningEffort === "ultra";
774
865
  if (!advanced)
775
- beforeIrreversible();
866
+ beforeCommitAttempt();
776
867
  let selected = await transitionModelControl(input, reasoning.capture.terminalControl, selection, keys, advanced
777
868
  ? ["codex_advanced_reasoning_picker"]
778
869
  : ["none", "codex_plan_scope_picker"], advanced ? {} : { allowImmediateNone: true });
@@ -782,24 +873,25 @@ async function applyCodexModelSelection(input, terminalControl, picker, request,
782
873
  }
783
874
  selection = selected.observation;
784
875
  keys = codexModelControlSelectEffortKeys(selection, request.reasoningEffort);
785
- beforeIrreversible();
876
+ beforeCommitAttempt();
786
877
  selected = await transitionModelControl(input, selected.capture.terminalControl, selection, keys, ["none", "codex_plan_scope_picker"], { allowImmediateNone: true });
787
878
  }
788
879
  if (selected.observation.state !== "codex_plan_scope_picker") {
789
880
  return selected;
790
881
  }
791
- beforeIrreversible();
882
+ beforeCommitAttempt();
792
883
  return transitionModelControl(input, selected.capture.terminalControl, selected.observation, codexModelControlAllModesKeys(selected.observation), ["none"], { allowImmediateNone: true });
793
884
  }
794
885
  async function openModelPicker(input, terminalControl) {
795
886
  let cleanupControl = terminalControl;
796
- let terminalInputAttempted = false;
797
887
  try {
798
888
  let idleCapture;
799
889
  let revalidatedCommand;
800
890
  if (input.initialResidual) {
801
- if (input.plan.behaviorProfile !== "codex-model-control-0.154.0") {
802
- throw new Error("native model-control continuation is profiled only for Codex 0.154.0");
891
+ const profile = terminalModelControlProfileForPlan(input.plan);
892
+ if (!profile?.supportsResidualContinuation) {
893
+ throw new Error("native model-control continuation is profiled only for Codex " +
894
+ CODEX_MODEL_CONTROL_AGENT_VERSION);
803
895
  }
804
896
  const observed = await inspectTerminalModelControlResidual({
805
897
  plan: input.plan,
@@ -828,21 +920,18 @@ async function openModelPicker(input, terminalControl) {
828
920
  else {
829
921
  const before = await input.ports.capture({ terminalControl });
830
922
  cleanupControl = before.terminalControl;
831
- if (before.activityState !== "idle" ||
832
- modelControlCaptureBlocked(before) ||
833
- !before.exactEmptyComposer) {
923
+ if (classifyTerminalModelControlSurface(input.plan, before).state !==
924
+ "idle_empty") {
834
925
  throw new Error("model control requires an exact verified idle pane with an empty composer and no blocking interaction");
835
926
  }
836
927
  await input.ports.beforeInput();
837
928
  const finalEmpty = await input.ports.capture({
838
929
  terminalControl: before.terminalControl
839
930
  });
840
- if (finalEmpty.activityState !== "idle" ||
841
- modelControlCaptureBlocked(finalEmpty) ||
842
- !finalEmpty.exactEmptyComposer) {
931
+ if (classifyTerminalModelControlSurface(input.plan, finalEmpty).state !==
932
+ "idle_empty") {
843
933
  throw new Error("the exact idle composer changed before /model input");
844
934
  }
845
- terminalInputAttempted = true;
846
935
  await input.ports.sendText(finalEmpty.terminalControl, input.plan.command);
847
936
  let commandCapture;
848
937
  const startedAt = Date.now();
@@ -851,9 +940,10 @@ async function openModelPicker(input, terminalControl) {
851
940
  terminalControl: before.terminalControl,
852
941
  expectedComposer: input.plan.command
853
942
  });
854
- if (!modelControlCaptureBlocked(captured) &&
855
- captured.activityState !== "working" &&
856
- modelCommandReadyForEnter(input.plan, captured)) {
943
+ const surface = classifyTerminalModelControlSurface(input.plan, captured);
944
+ if (surface.state === "command_popup" ||
945
+ isTerminalModelControlPlanForAgent(input.plan, "codex") &&
946
+ surface.state === "bare_command") {
857
947
  commandCapture = captured;
858
948
  break;
859
949
  }
@@ -867,15 +957,15 @@ async function openModelPicker(input, terminalControl) {
867
957
  terminalControl: commandCapture.terminalControl,
868
958
  expectedComposer: input.plan.command
869
959
  });
870
- if (modelControlCaptureBlocked(revalidatedCommand) ||
871
- revalidatedCommand.activityState === "working" ||
872
- !modelCommandReadyForEnter(input.plan, revalidatedCommand)) {
960
+ const revalidatedSurface = classifyTerminalModelControlSurface(input.plan, revalidatedCommand);
961
+ if (revalidatedSurface.state !== "command_popup" &&
962
+ !(isTerminalModelControlPlanForAgent(input.plan, "codex") &&
963
+ revalidatedSurface.state === "bare_command")) {
873
964
  throw new Error("the exact /model composer changed before Enter");
874
965
  }
875
966
  idleCapture = finalEmpty;
876
967
  }
877
968
  cleanupControl = revalidatedCommand.terminalControl;
878
- terminalInputAttempted = true;
879
969
  await input.ports.sendKeys(revalidatedCommand.terminalControl, ["C-m"]);
880
970
  let opened = await waitForObservation(input, revalidatedCommand.terminalControl, ["codex_entry_model_picker", "codex_model_picker", "claude_model_picker"]);
881
971
  if (opened.observation.state === "codex_entry_model_picker") {
@@ -902,9 +992,7 @@ async function openModelPicker(input, terminalControl) {
902
992
  return { ...opened, idleCapture };
903
993
  }
904
994
  catch (error) {
905
- if (!terminalInputAttempted)
906
- throw error;
907
- const cleanupError = await tryUnwindModelControl(input, cleanupControl);
995
+ const { cleanupError } = await settleTerminalModelControlFailure(input, cleanupControl);
908
996
  if (!cleanupError)
909
997
  throw error;
910
998
  const detail = error instanceof Error ? error.message : String(error);
@@ -928,32 +1016,34 @@ async function unwindModelControl(input, terminalControl) {
928
1016
  expectedComposer: input.plan.command
929
1017
  });
930
1018
  control = captured.terminalControl;
931
- if (modelControlCaptureBlocked(captured) ||
932
- captured.activityState === "working") {
1019
+ const surface = classifyTerminalModelControlSurface(input.plan, captured);
1020
+ if (surface.state === "blocked") {
933
1021
  throw new Error("the terminal became busy or blocked during cleanup");
934
1022
  }
935
- if (captured.exactEmptyComposer && captured.activityState === "idle") {
1023
+ if (surface.state === "idle_empty") {
936
1024
  return captured;
937
1025
  }
938
- if (captured.exactCommandComposer) {
1026
+ if (surface.state === "command_popup" ||
1027
+ surface.state === "bare_command" ||
1028
+ surface.state === "command_draft") {
939
1029
  await input.ports.beforeInput();
940
1030
  const finalCommand = await input.ports.capture({
941
1031
  terminalControl: control,
942
1032
  expectedComposer: input.plan.command
943
1033
  });
944
- if (modelControlCaptureBlocked(finalCommand) ||
945
- finalCommand.activityState === "working" ||
946
- !finalCommand.exactCommandComposer) {
1034
+ const finalCommandSurface = classifyTerminalModelControlSurface(input.plan, finalCommand);
1035
+ if (finalCommandSurface.state !== "command_popup" &&
1036
+ finalCommandSurface.state !== "bare_command" &&
1037
+ finalCommandSurface.state !== "command_draft") {
947
1038
  throw new Error("the exact /model composer changed during cleanup");
948
1039
  }
949
1040
  control = finalCommand.terminalControl;
950
- if (finalCommand.exactCommandReady ||
951
- input.plan.behaviorProfile === "claude-model-control-2.1.266" ||
952
- finalCommand.exactBareCommand !== true) {
1041
+ if (finalCommandSurface.state !== "bare_command" ||
1042
+ isTerminalModelControlPlanForAgent(input.plan, "claude")) {
953
1043
  await input.ports.sendKeys(control, ["Escape"]);
954
1044
  const afterEscape = await waitForModelCommandCleanupState(input, control);
955
1045
  control = afterEscape.terminalControl;
956
- if (afterEscape.exactEmptyComposer)
1046
+ if (classifyTerminalModelControlSurface(input.plan, afterEscape).state === "idle_empty")
957
1047
  return afterEscape;
958
1048
  }
959
1049
  await input.ports.beforeInput();
@@ -961,10 +1051,7 @@ async function unwindModelControl(input, terminalControl) {
961
1051
  terminalControl: control,
962
1052
  expectedComposer: input.plan.command
963
1053
  });
964
- if (modelControlCaptureBlocked(finalBareCommand) ||
965
- finalBareCommand.activityState === "working" ||
966
- !finalBareCommand.exactCommandComposer ||
967
- finalBareCommand.exactCommandReady) {
1054
+ if (classifyTerminalModelControlSurface(input.plan, finalBareCommand).state !== "bare_command") {
968
1055
  throw new Error(`the exact bare ${input.plan.behaviorProfile.startsWith("codex-")
969
1056
  ? "Codex"
970
1057
  : "Claude"} /model composer changed during cleanup`);
@@ -977,10 +1064,10 @@ async function unwindModelControl(input, terminalControl) {
977
1064
  control = cleared.capture.terminalControl;
978
1065
  continue;
979
1066
  }
980
- const observation = observeTerminalModelControl(input.plan, captured.screen);
981
- if (observation.state === "none" || observation.state === "ambiguous") {
1067
+ if (surface.state !== "picker") {
982
1068
  throw new Error("no exact current model-control frame was available for Escape");
983
1069
  }
1070
+ const observation = surface.observation;
984
1071
  control = await exactDispatchKeys(input, control, observation, ["Escape"]);
985
1072
  const next = await waitForObservation(input, control, [
986
1073
  "none",
@@ -1007,17 +1094,13 @@ async function waitForModelCommandCleanupState(input, terminalControl) {
1007
1094
  terminalControl,
1008
1095
  expectedComposer: input.plan.command
1009
1096
  });
1010
- const exactState = captured.exactEmptyComposer ||
1011
- captured.exactCommandComposer && !captured.exactCommandReady &&
1012
- (input.plan.behaviorProfile === "claude-model-control-2.1.266" ||
1013
- captured.exactBareCommand === true);
1014
- if (!modelControlCaptureBlocked(captured) &&
1015
- captured.activityState !== "working" && exactState) {
1016
- const semanticState = captured.exactEmptyComposer
1097
+ const surface = classifyTerminalModelControlSurface(input.plan, captured);
1098
+ if (surface.state === "idle_empty" || surface.state === "bare_command") {
1099
+ const semanticState = surface.state === "idle_empty"
1017
1100
  ? "empty"
1018
- : captured.exactCommandFingerprint
1019
- ? `bare:${captured.exactCommandFingerprint}`
1020
- : input.plan.behaviorProfile === "claude-model-control-2.1.266"
1101
+ : surface.fingerprint
1102
+ ? `bare:${surface.fingerprint}`
1103
+ : isTerminalModelControlPlanForAgent(input.plan, "claude")
1021
1104
  ? "bare:claude-profiled-composer"
1022
1105
  : undefined;
1023
1106
  if (!semanticState) {
@@ -1073,9 +1156,11 @@ async function exactDispatchKeys(input, terminalControl, observation, keys) {
1073
1156
  }
1074
1157
  await input.ports.beforeInput();
1075
1158
  const captured = await input.ports.capture({ terminalControl });
1076
- const current = observeTerminalModelControl(input.plan, captured.screen);
1077
- if (modelControlCaptureBlocked(captured) ||
1078
- captured.activityState === "working" ||
1159
+ const surface = classifyTerminalModelControlSurface(input.plan, captured);
1160
+ const current = surface.state === "picker"
1161
+ ? surface.observation
1162
+ : undefined;
1163
+ if (!current ||
1079
1164
  current.state !== observation.state ||
1080
1165
  current.fingerprint !== observation.fingerprint) {
1081
1166
  throw new Error("the native model-control frame changed before key dispatch");
@@ -1089,14 +1174,18 @@ async function waitForObservation(input, terminalControl, expectedStates, option
1089
1174
  let stableCaptures = 0;
1090
1175
  while (Date.now() - startedAt <= MODEL_CONTROL_SETTLE_TIMEOUT_MS) {
1091
1176
  const captured = await input.ports.capture({ terminalControl });
1092
- if (modelControlCaptureBlocked(captured) ||
1093
- captured.activityState === "working") {
1177
+ const surface = classifyTerminalModelControlSurface(input.plan, captured);
1178
+ if (surface.state === "blocked") {
1094
1179
  throw new Error("the terminal became busy or blocked during model control");
1095
1180
  }
1096
- const observation = observeTerminalModelControl(input.plan, captured.screen);
1097
- const expectedNone = observation.state === "none" && (options.allowImmediateNone === true && captured.exactEmptyComposer ||
1181
+ const observation = surface.state === "picker"
1182
+ ? surface.observation
1183
+ : observeTerminalModelControl(input.plan, captured.screen);
1184
+ const expectedNone = observation.state === "none" && (options.allowImmediateNone === true && surface.state === "idle_empty" ||
1098
1185
  options.allowExactCommandComposer === true &&
1099
- captured.exactCommandComposer);
1186
+ (surface.state === "command_popup" ||
1187
+ surface.state === "bare_command" ||
1188
+ surface.state === "command_draft"));
1100
1189
  const expected = observation.fingerprint !== options.excludeFingerprint &&
1101
1190
  expectedStates.includes(observation.state) &&
1102
1191
  (observation.state !== "none" || expectedNone);
@@ -1125,11 +1214,13 @@ async function exactDismiss(input, terminalControl, observation) {
1125
1214
  allowExactCommandComposer: true,
1126
1215
  excludeFingerprint: observation.fingerprint
1127
1216
  });
1128
- if (closed.capture.exactEmptyComposer &&
1129
- closed.capture.activityState === "idle") {
1217
+ const closedSurface = classifyTerminalModelControlSurface(input.plan, closed.capture);
1218
+ if (closedSurface.state === "idle_empty") {
1130
1219
  return closed.capture;
1131
1220
  }
1132
- if (closed.capture.exactCommandComposer) {
1221
+ if (closedSurface.state === "command_popup" ||
1222
+ closedSurface.state === "bare_command" ||
1223
+ closedSurface.state === "command_draft") {
1133
1224
  return unwindModelControl(input, closed.capture.terminalControl);
1134
1225
  }
1135
1226
  throw new Error("native model-control dismissal did not return to exact idle");