@scotthuang/agent-knock-knock 0.13.2 → 0.13.3

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 (54) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +17 -4
  3. package/dist/src/claude-terminal-agent-adapter.js +4 -0
  4. package/dist/src/claude-terminal-agent-adapter.js.map +1 -1
  5. package/dist/src/cli-core.js +9 -1
  6. package/dist/src/cli-core.js.map +1 -1
  7. package/dist/src/codex-terminal-agent-adapter.js +4 -0
  8. package/dist/src/codex-terminal-agent-adapter.js.map +1 -1
  9. package/dist/src/herdr-terminal-control-provider.js +6 -1
  10. package/dist/src/herdr-terminal-control-provider.js.map +1 -1
  11. package/dist/src/host-adapter.d.ts +1 -1
  12. package/dist/src/host-adapter.js +1 -1
  13. package/dist/src/host-bridge-tools.js +2 -2
  14. package/dist/src/managed-session.js +2 -18
  15. package/dist/src/managed-session.js.map +1 -1
  16. package/dist/src/native-thread-lifecycle-cli-adapter.d.ts +13 -0
  17. package/dist/src/native-thread-lifecycle-cli-adapter.js +578 -2
  18. package/dist/src/native-thread-lifecycle-cli-adapter.js.map +1 -1
  19. package/dist/src/openclaw-plugin-command-adapter.js +325 -35
  20. package/dist/src/openclaw-plugin-command-adapter.js.map +1 -1
  21. package/dist/src/openclaw-plugin-helpers.d.ts +27 -0
  22. package/dist/src/openclaw-plugin-helpers.js +682 -2
  23. package/dist/src/openclaw-plugin-helpers.js.map +1 -1
  24. package/dist/src/openclaw-plugin-schemas.d.ts +53 -0
  25. package/dist/src/openclaw-plugin-schemas.js +53 -0
  26. package/dist/src/openclaw-plugin-schemas.js.map +1 -1
  27. package/dist/src/terminal-action-projection.d.ts +83 -1
  28. package/dist/src/terminal-action-projection.js +174 -0
  29. package/dist/src/terminal-action-projection.js.map +1 -1
  30. package/dist/src/terminal-agent-adapter.d.ts +7 -0
  31. package/dist/src/terminal-agent-adapter.js +8 -0
  32. package/dist/src/terminal-agent-adapter.js.map +1 -1
  33. package/dist/src/terminal-agent-bridge.d.ts +29 -0
  34. package/dist/src/terminal-agent-bridge.js +580 -14
  35. package/dist/src/terminal-agent-bridge.js.map +1 -1
  36. package/dist/src/terminal-control-ref.d.ts +23 -0
  37. package/dist/src/terminal-control-ref.js +32 -0
  38. package/dist/src/terminal-control-ref.js.map +1 -1
  39. package/dist/src/terminal-list-cli-adapter.js +324 -16
  40. package/dist/src/terminal-list-cli-adapter.js.map +1 -1
  41. package/dist/src/terminal-list-renderer.d.ts +1 -0
  42. package/dist/src/terminal-list-renderer.js +87 -3
  43. package/dist/src/terminal-list-renderer.js.map +1 -1
  44. package/dist/src/terminal-model-control.d.ts +312 -0
  45. package/dist/src/terminal-model-control.js +1595 -0
  46. package/dist/src/terminal-model-control.js.map +1 -0
  47. package/dist/src/terminal-runtime-cli-adapter.d.ts +2 -0
  48. package/dist/src/terminal-runtime-cli-adapter.js +59 -17
  49. package/dist/src/terminal-runtime-cli-adapter.js.map +1 -1
  50. package/docs/quickstart-herdr.md +1 -1
  51. package/docs/quickstart-tmux.md +27 -2
  52. package/openclaw.plugin.json +13 -1
  53. package/package.json +1 -1
  54. package/templates/openclaw-skills/agent-knock-knock/SKILL.md +54 -11
@@ -0,0 +1,1595 @@
1
+ import { createHash } from "node:crypto";
2
+ import { terminalPhysicalBindingToken } from "./terminal-control-ref.js";
3
+ /** Caller-visible reasoning values. Native labels and menu positions stay private. */
4
+ export const TERMINAL_MODEL_REASONING_EFFORTS = [
5
+ "low",
6
+ "medium",
7
+ "high",
8
+ "xhigh",
9
+ "max",
10
+ "ultra"
11
+ ];
12
+ /**
13
+ * One action-specific physical authority token for fresh Codex model control.
14
+ * The domain separator and exact profile keep this authority from being reused
15
+ * as Send or native-lifecycle authority even though all bind the same pane.
16
+ */
17
+ export function terminalUserExplicitModelControlBindingToken(value) {
18
+ const physicalTerminalToken = terminalPhysicalBindingToken({
19
+ terminalId: value.terminalId,
20
+ terminalControl: value.terminalControl,
21
+ agent: "codex",
22
+ pid: value.pid,
23
+ workspace: value.workspace,
24
+ processUuid: value.processUuid,
25
+ processBirth: value.processBirth
26
+ });
27
+ return createHash("sha256")
28
+ .update(JSON.stringify({
29
+ version: 1,
30
+ authority: "terminal_user_explicit_model_control",
31
+ physical_terminal_token: physicalTerminalToken,
32
+ agent_version: value.agentVersion,
33
+ behavior_profile: value.behaviorProfile
34
+ }))
35
+ .digest("hex");
36
+ }
37
+ /**
38
+ * Snapshot authority for adopting and cleaning one exact native `/model`
39
+ * residual. The residual kind and normalized surface digest prevent a token
40
+ * for one popup, pane generation, or bare Composer from authorizing another.
41
+ */
42
+ export function terminalUserExplicitModelControlRepairBindingToken(value) {
43
+ const modelControlToken = terminalUserExplicitModelControlBindingToken(value);
44
+ return createHash("sha256")
45
+ .update(JSON.stringify({
46
+ version: 1,
47
+ authority: "terminal_user_explicit_model_control_repair",
48
+ model_control_token: modelControlToken,
49
+ residual_kind: value.residualKind,
50
+ residual_fingerprint: value.residualFingerprint
51
+ }))
52
+ .digest("hex");
53
+ }
54
+ /**
55
+ * Snapshot authority for continuing one exact native `/model` residual into
56
+ * the read-only catalog picker. This is deliberately domain-separated from
57
+ * cleanup-only repair: a repair offer never authorizes Enter, while this
58
+ * offer authorizes exactly one profiled slash-command dispatch.
59
+ */
60
+ export function terminalUserExplicitModelControlResidualEntryBindingToken(value) {
61
+ const modelControlToken = terminalUserExplicitModelControlBindingToken(value);
62
+ return createHash("sha256")
63
+ .update(JSON.stringify({
64
+ version: 1,
65
+ authority: "terminal_user_explicit_model_control_residual_entry",
66
+ model_control_token: modelControlToken,
67
+ residual_kind: value.residualKind,
68
+ residual_fingerprint: value.residualFingerprint
69
+ }))
70
+ .digest("hex");
71
+ }
72
+ export function parseCodexNativeModelCatalog(value) {
73
+ if (!isPlainRecord(value) || !Array.isArray(value.models)) {
74
+ throw new Error("the exact running Codex model catalog has an unknown shape");
75
+ }
76
+ const models = value.models.flatMap((candidate) => {
77
+ if (!isPlainRecord(candidate) || candidate.visibility !== "list")
78
+ return [];
79
+ const id = nonBlank(candidate.slug);
80
+ const label = nonBlank(candidate.display_name) ?? id;
81
+ if (!id || !label || !/^[a-z0-9][a-z0-9._:/+\-]{0,127}$/u.test(id) ||
82
+ !Array.isArray(candidate.supported_reasoning_levels)) {
83
+ throw new Error("the exact running Codex model catalog contains an invalid visible row");
84
+ }
85
+ const efforts = candidate.supported_reasoning_levels.flatMap((entry) => {
86
+ const effort = isPlainRecord(entry) ? entry.effort : undefined;
87
+ return isTerminalModelReasoningEffort(effort) ? [effort] : [];
88
+ });
89
+ if (efforts.length === 0 || new Set(efforts).size !== efforts.length) {
90
+ throw new Error(`the exact running Codex model catalog has no unique supported efforts for ${id}`);
91
+ }
92
+ return [{ id, label, reasoningEfforts: efforts }];
93
+ });
94
+ if (models.length === 0 ||
95
+ new Set(models.map((model) => model.id)).size !== models.length) {
96
+ throw new Error("the exact running Codex model catalog has no unique visible models");
97
+ }
98
+ return { models };
99
+ }
100
+ function observeCodexIdleModel(screen) {
101
+ const lines = stripAnsi(screen).replace(/\r\n?/gu, "\n").split("\n");
102
+ while (lines.length > 0 && lines.at(-1)?.trim() === "")
103
+ lines.pop();
104
+ const footer = lines.at(-1);
105
+ const match = footer
106
+ ? /^\s{2,}([a-z0-9][a-z0-9._:/+\-]{0,127})\s+(low|medium|high|xhigh|max|ultra)(?:\s+fast)?(?:\s+·.*|\s+Plan mode(?:\s+\([^)]*\))?)?\s*$/u
107
+ .exec(footer)
108
+ : undefined;
109
+ if (!match || !isTerminalModelReasoningEffort(match[2]))
110
+ return undefined;
111
+ return {
112
+ model: match[1],
113
+ reasoningEffort: match[2],
114
+ planMode: /\bPlan mode\b/u.test(footer ?? "")
115
+ };
116
+ }
117
+ async function inspectClaudeModelEfforts(input, terminalControl, picker, model) {
118
+ const selected = await transitionModelControl(input, terminalControl, picker, modelControlSelectModelKeys(picker, model), ["claude_model_picker"]);
119
+ if (selected.observation.state !== "claude_model_picker" ||
120
+ selectedClaudeModelId(selected.observation) !== model) {
121
+ throw new Error("Claude highlighted a different model during catalog inspection");
122
+ }
123
+ let control = selected.capture.terminalControl;
124
+ let live = selected.observation;
125
+ const initial = live.displayedEffort;
126
+ if (!initial) {
127
+ return {
128
+ capture: selected.capture,
129
+ observation: live,
130
+ reasoningEfforts: []
131
+ };
132
+ }
133
+ const seen = new Set([initial]);
134
+ for (let count = 0; count < 8; count += 1) {
135
+ const next = await transitionModelControl(input, control, live, claudeModelControlEffortKey("higher"), ["claude_model_picker"]);
136
+ if (next.observation.state !== "claude_model_picker" ||
137
+ selectedClaudeModelId(next.observation) !== model) {
138
+ throw new Error("Claude changed the highlighted model while probing its effort ring");
139
+ }
140
+ control = next.capture.terminalControl;
141
+ live = next.observation;
142
+ const effort = live.displayedEffort;
143
+ if (!effort) {
144
+ throw new Error("Claude removed its effort row while it was being inspected");
145
+ }
146
+ if (effort === initial) {
147
+ return {
148
+ capture: next.capture,
149
+ observation: live,
150
+ reasoningEfforts: TERMINAL_MODEL_REASONING_EFFORTS.filter((effort) => seen.has(effort))
151
+ };
152
+ }
153
+ if (seen.has(effort)) {
154
+ throw new Error("Claude's native effort ring did not return to its initial value");
155
+ }
156
+ seen.add(effort);
157
+ }
158
+ throw new Error("Claude's native effort ring exceeded the verified bounded size");
159
+ }
160
+ export function isTerminalModelReasoningEffort(value) {
161
+ return typeof value === "string" &&
162
+ TERMINAL_MODEL_REASONING_EFFORTS.includes(value);
163
+ }
164
+ 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
+ };
202
+ }
203
+ 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
+ };
222
+ }
223
+ /**
224
+ * Observe only complete, current model-control modal frames. Historical or
225
+ * partial picker text is never sufficient to authorize a key dispatch.
226
+ */
227
+ export function observeTerminalModelControl(plan, screen) {
228
+ const normalized = stripAnsi(screen).replace(/\r\n?/gu, "\n");
229
+ return plan.behaviorProfile === "codex-model-control-0.154.0"
230
+ ? observeCodexModelControl(normalized)
231
+ : observeClaudeModelControl(normalized);
232
+ }
233
+ export function terminalModelCatalogFingerprint(input) {
234
+ const canonical = {
235
+ agent: input.agent,
236
+ agent_version: input.agentVersion,
237
+ behavior_profile: input.behaviorProfile,
238
+ scope: input.scope,
239
+ current: {
240
+ model: input.current.model,
241
+ reasoning_effort: input.current.reasoningEffort ?? null
242
+ },
243
+ models: input.models.map((model) => ({
244
+ id: model.id,
245
+ label: model.label,
246
+ reasoning_efforts: [...model.reasoningEfforts]
247
+ }))
248
+ };
249
+ return createHash("sha256")
250
+ .update(JSON.stringify(canonical))
251
+ .digest("hex");
252
+ }
253
+ export function modelControlMoveKeys(currentIndex, targetIndex) {
254
+ if (!Number.isSafeInteger(currentIndex) ||
255
+ !Number.isSafeInteger(targetIndex) ||
256
+ currentIndex < 0 ||
257
+ targetIndex < 0) {
258
+ throw new Error("model-control selection index is invalid");
259
+ }
260
+ const key = targetIndex >= currentIndex ? "Down" : "Up";
261
+ return Array.from({ length: Math.abs(targetIndex - currentIndex) }, () => key);
262
+ }
263
+ /** Adapter-owned native navigation; never serialize these keys to a caller. */
264
+ export function modelControlSelectModelKeys(observation, model) {
265
+ const row = observation.rows.find((candidate) => candidate.id === model);
266
+ if (!row)
267
+ throw new Error("the requested model is absent from the current native picker");
268
+ return [
269
+ ...modelControlMoveKeys(observation.selectedIndex, row.nativeIndex),
270
+ ...(observation.state === "codex_model_picker" ? ["C-m"] : [])
271
+ ];
272
+ }
273
+ function selectedClaudeModelId(observation) {
274
+ return observation.rows.find((row) => row.nativeIndex === observation.selectedIndex)?.id;
275
+ }
276
+ export function codexModelControlSelectEffortKeys(observation, effort) {
277
+ const direct = observation.rows.find((row) => row.kind === "effort" && row.effort === effort);
278
+ const target = direct ?? (observation.state === "codex_reasoning_picker" &&
279
+ (effort === "max" || effort === "ultra")
280
+ ? observation.rows.find((row) => row.kind === "advanced")
281
+ : undefined);
282
+ if (!target) {
283
+ throw new Error("the requested reasoning effort is absent from the current native picker");
284
+ }
285
+ const targetIndex = observation.rows.indexOf(target);
286
+ return [
287
+ ...modelControlMoveKeys(observation.selectedIndex, targetIndex),
288
+ "C-m"
289
+ ];
290
+ }
291
+ export function codexModelControlAllModesKeys(observation) {
292
+ const targetIndex = observation.rows.indexOf("Apply to global default and Plan mode override");
293
+ if (targetIndex < 0) {
294
+ throw new Error("Codex Plan-mode scope picker has no global-default choice");
295
+ }
296
+ return [
297
+ ...modelControlMoveKeys(observation.selectedIndex, targetIndex),
298
+ "C-m"
299
+ ];
300
+ }
301
+ export function claudeModelControlEffortKey(direction) {
302
+ return [direction === "lower" ? "Left" : "Right"];
303
+ }
304
+ export function claudeModelControlCommitKeys(plan) {
305
+ if (plan.behaviorProfile !== "claude-model-control-2.1.266" ||
306
+ plan.scope !== "current_session") {
307
+ throw new Error("Claude model control cannot commit outside session-only scope");
308
+ }
309
+ return ["s"];
310
+ }
311
+ const MODEL_CONTROL_SETTLE_TIMEOUT_MS = 5_000;
312
+ const MODEL_CONTROL_POLL_MS = 40;
313
+ function modelControlCaptureBlocked(capture) {
314
+ return capture.approvalBlocked || capture.inputBlocked === true;
315
+ }
316
+ function modelCommandReadyForEnter(plan, capture) {
317
+ return capture.exactCommandReady ||
318
+ plan.behaviorProfile === "codex-model-control-0.154.0" &&
319
+ capture.exactBareCommand === true;
320
+ }
321
+ function residualFromCapture(plan, capture) {
322
+ if (plan.behaviorProfile !== "codex-model-control-0.154.0") {
323
+ return {
324
+ state: "unsafe",
325
+ reason: "model-control residual repair is profiled only for Codex 0.154.0",
326
+ terminalControl: capture.terminalControl
327
+ };
328
+ }
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) {
336
+ return {
337
+ state: "unsafe",
338
+ reason: "another prompt or active agent state owns terminal input",
339
+ terminalControl: capture.terminalControl
340
+ };
341
+ }
342
+ if (exactModelSurface) {
343
+ return {
344
+ state: "recoverable",
345
+ kind: "model_surface",
346
+ fingerprint: createHash("sha256")
347
+ .update(JSON.stringify({
348
+ version: 1,
349
+ behavior_profile: plan.behaviorProfile,
350
+ kind: "model_surface",
351
+ surface_state: modelObservation.state,
352
+ surface_fingerprint: modelObservation.fingerprint
353
+ }))
354
+ .digest("hex"),
355
+ terminalControl: capture.terminalControl
356
+ };
357
+ }
358
+ if (!capture.exactCommandComposer ||
359
+ !capture.exactCommandFingerprint ||
360
+ (!capture.exactCommandReady && capture.exactBareCommand !== true)) {
361
+ return {
362
+ state: "absent",
363
+ reason: capture.exactEmptyComposer
364
+ ? "the Composer is already empty"
365
+ : "the current Composer is not one exact profiled /model residual",
366
+ terminalControl: capture.terminalControl
367
+ };
368
+ }
369
+ const kind = capture.exactCommandReady
370
+ ? "profiled_command_popup"
371
+ : "bare_command";
372
+ return {
373
+ state: "recoverable",
374
+ kind,
375
+ fingerprint: createHash("sha256")
376
+ .update(JSON.stringify({
377
+ version: 1,
378
+ behavior_profile: plan.behaviorProfile,
379
+ kind,
380
+ exact_command_fingerprint: capture.exactCommandFingerprint
381
+ }))
382
+ .digest("hex"),
383
+ terminalControl: capture.terminalControl
384
+ };
385
+ }
386
+ /**
387
+ * Read-only, two-frame proof for one exact Codex `/model` residual or exact
388
+ * native model-control surface. It never treats arbitrary Composer text,
389
+ * transcript text, or an incomplete/unknown picker as recoverable authority.
390
+ */
391
+ export async function inspectTerminalModelControlResidual(input) {
392
+ const firstCapture = await input.ports.capture({
393
+ terminalControl: input.terminalControl,
394
+ expectedComposer: input.plan.command
395
+ });
396
+ const first = residualFromCapture(input.plan, firstCapture);
397
+ if (first.state !== "recoverable")
398
+ return first;
399
+ await input.ports.sleep(MODEL_CONTROL_POLL_MS);
400
+ const secondCapture = await input.ports.capture({
401
+ terminalControl: first.terminalControl,
402
+ expectedComposer: input.plan.command
403
+ });
404
+ const second = residualFromCapture(input.plan, secondCapture);
405
+ if (second.state !== "recoverable" ||
406
+ second.kind !== first.kind ||
407
+ second.fingerprint !== first.fingerprint) {
408
+ return {
409
+ state: "unsafe",
410
+ reason: "the exact /model residual changed across the stable snapshot",
411
+ terminalControl: second.terminalControl
412
+ };
413
+ }
414
+ return second;
415
+ }
416
+ /**
417
+ * Consume one current residual offer. Once any cleanup key is attempted, all
418
+ * failures are response-uncertain and callers must not retry automatically.
419
+ */
420
+ export async function repairTerminalModelControlResidual(input) {
421
+ const observed = await inspectTerminalModelControlResidual(input);
422
+ if (observed.state !== "recoverable") {
423
+ throw new Error(observed.reason);
424
+ }
425
+ if (observed.fingerprint !== input.expectedResidualFingerprint) {
426
+ throw new Error("the exact /model residual changed after authorization; refresh AKK list");
427
+ }
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
+ };
436
+ try {
437
+ const cleared = await unwindModelControl({ plan: input.plan, ports }, observed.terminalControl);
438
+ if (modelControlCaptureBlocked(cleared) ||
439
+ cleared.activityState !== "idle" ||
440
+ !cleared.exactEmptyComposer) {
441
+ throw new Error("cleanup did not prove an exact idle empty Composer");
442
+ }
443
+ return {
444
+ outcome: "repaired",
445
+ terminalControl: cleared.terminalControl,
446
+ terminalInputAttempted,
447
+ composerPostcondition: "empty",
448
+ doNotRetry: false
449
+ };
450
+ }
451
+ catch (error) {
452
+ if (!terminalInputAttempted)
453
+ throw error;
454
+ return {
455
+ outcome: "uncertain",
456
+ terminalControl: observed.terminalControl,
457
+ terminalInputAttempted: true,
458
+ composerPostcondition: "unproven",
459
+ doNotRetry: true,
460
+ reason: error instanceof Error ? error.message : String(error)
461
+ };
462
+ }
463
+ }
464
+ /**
465
+ * Run one bounded, reversible native catalog inspection. It enters only the
466
+ * adapter-owned `/model` surface and always returns through exact Escape paths.
467
+ */
468
+ export async function discoverTerminalModelOptions(input) {
469
+ const codexNativeCatalog = input.agent === "codex"
470
+ ? await input.ports.loadCodexCatalog?.()
471
+ : undefined;
472
+ if (input.agent === "codex" && !codexNativeCatalog) {
473
+ throw new Error("Codex model discovery requires the exact running executable's validated model catalog");
474
+ }
475
+ let control = input.terminalControl;
476
+ const opened = await openModelPicker(input, control);
477
+ control = opened.capture.terminalControl;
478
+ const picker = opened.observation;
479
+ try {
480
+ if (picker.state !== "codex_model_picker" &&
481
+ picker.state !== "claude_model_picker") {
482
+ throw new Error("the native model picker did not expose an exact catalog");
483
+ }
484
+ let currentEffort;
485
+ let models;
486
+ if (picker.state === "codex_model_picker") {
487
+ const nativeById = new Map(codexNativeCatalog?.models.map((model) => [model.id, model]) ?? []);
488
+ const currentChoice = nativeById.get(picker.currentModel);
489
+ let idleCurrent = observeCodexIdleModel(opened.idleCapture.screen);
490
+ models = picker.rows.flatMap((row) => {
491
+ const native = nativeById.get(row.id);
492
+ // A single-effort row commits on Enter instead of opening the profiled
493
+ // reasoning picker, so it is deliberately absent from this typed API.
494
+ return native && native.reasoningEfforts.length >= 2
495
+ ? [{
496
+ id: row.id,
497
+ label: native.label || row.label,
498
+ reasoningEfforts: native.reasoningEfforts
499
+ }]
500
+ : [];
501
+ });
502
+ const dismissed = await exactDismiss(input, control, picker);
503
+ control = dismissed.terminalControl;
504
+ // An adopted profiled slash popup temporarily replaces the ordinary
505
+ // footer. Once the read-only picker is dismissed, the exact idle footer
506
+ // is visible again and can complete the same catalog cross-check.
507
+ idleCurrent ??= observeCodexIdleModel(dismissed.screen);
508
+ if (!currentChoice || !idleCurrent ||
509
+ idleCurrent.model !== picker.currentModel ||
510
+ !idleCurrent.reasoningEffort ||
511
+ !currentChoice.reasoningEfforts.includes(idleCurrent.reasoningEffort)) {
512
+ throw new Error("the current Codex model and effort are not exact across its idle footer and live catalog");
513
+ }
514
+ currentEffort = idleCurrent.reasoningEffort;
515
+ }
516
+ else {
517
+ currentEffort = picker.currentEffort;
518
+ let livePicker = picker;
519
+ models = [];
520
+ for (const row of picker.rows) {
521
+ const inspected = await inspectClaudeModelEfforts(input, control, livePicker, row.id);
522
+ control = inspected.capture.terminalControl;
523
+ livePicker = inspected.observation;
524
+ if (inspected.reasoningEfforts.length > 0) {
525
+ models.push({
526
+ id: row.id,
527
+ label: row.label,
528
+ reasoningEfforts: inspected.reasoningEfforts
529
+ });
530
+ }
531
+ }
532
+ control = (await exactDismiss(input, control, livePicker)).terminalControl;
533
+ }
534
+ if (models.length === 0) {
535
+ throw new Error("the live native model picker had no verified model choices");
536
+ }
537
+ const current = {
538
+ model: picker.currentModel,
539
+ reasoningEffort: currentEffort
540
+ };
541
+ const fingerprintInput = {
542
+ agent: input.agent,
543
+ agentVersion: input.agentVersion,
544
+ behaviorProfile: input.plan.behaviorProfile,
545
+ scope: input.plan.scope,
546
+ current,
547
+ models
548
+ };
549
+ return {
550
+ terminalControl: control,
551
+ catalog: {
552
+ ...fingerprintInput,
553
+ catalogFingerprint: terminalModelCatalogFingerprint(fingerprintInput)
554
+ }
555
+ };
556
+ }
557
+ catch (error) {
558
+ const cleanupError = await tryUnwindModelControl(input, control);
559
+ const detail = error instanceof Error ? error.message : String(error);
560
+ if (cleanupError) {
561
+ throw new Error(`${detail}; exact native model-control cleanup failed: ${cleanupError}`);
562
+ }
563
+ throw error;
564
+ }
565
+ }
566
+ /**
567
+ * Re-discover the exact catalog, consume one adapter-owned model path, then
568
+ * reopen the picker to prove the effective postcondition. No automatic retry
569
+ * is possible after the native commit key is attempted.
570
+ */
571
+ export async function switchTerminalModel(input) {
572
+ const requestedEffort = input.request.reasoningEffort;
573
+ if (!requestedEffort || !isTerminalModelReasoningEffort(requestedEffort)) {
574
+ throw new Error("--reasoning-effort must be one current advertised semantic value");
575
+ }
576
+ const discovery = await discoverTerminalModelOptions(input);
577
+ if (discovery.catalog.catalogFingerprint !== input.expectedCatalogFingerprint) {
578
+ throw new Error("the native model catalog changed after discovery; run model-options again");
579
+ }
580
+ const offered = discovery.catalog.models.find((model) => model.id === input.request.model);
581
+ if (!offered || !offered.reasoningEfforts.includes(requestedEffort)) {
582
+ throw new Error("the requested model and reasoning effort are not in the current native catalog");
583
+ }
584
+ if (input.agent !== "codex" &&
585
+ discovery.catalog.current.model === input.request.model &&
586
+ discovery.catalog.current.reasoningEffort === requestedEffort) {
587
+ return {
588
+ terminalControl: discovery.terminalControl,
589
+ outcome: "already_effective",
590
+ scope: input.plan.scope,
591
+ defaultsChanged: false,
592
+ requested: input.request,
593
+ effective: discovery.catalog.current,
594
+ doNotRetry: false
595
+ };
596
+ }
597
+ let irreversible = false;
598
+ let control = discovery.terminalControl;
599
+ let codexPersistenceBaseline;
600
+ let codexPersistenceScreen;
601
+ try {
602
+ const opened = await openModelPicker(input, control);
603
+ control = opened.capture.terminalControl;
604
+ const picker = opened.observation;
605
+ if (picker.state === "codex_model_picker") {
606
+ codexPersistenceBaseline = opened.idleCapture.screen;
607
+ const committed = await applyCodexModelSelection(input, control, picker, input.request, () => { irreversible = true; });
608
+ control = committed.capture.terminalControl;
609
+ codexPersistenceScreen = committed.capture.screen;
610
+ }
611
+ else if (picker.state === "claude_model_picker") {
612
+ const selected = await transitionModelControl(input, control, picker, modelControlSelectModelKeys(picker, input.request.model), ["claude_model_picker"]);
613
+ control = selected.capture.terminalControl;
614
+ if (selected.observation.state !== "claude_model_picker") {
615
+ throw new Error("Claude model selection did not remain in its picker");
616
+ }
617
+ if (selectedClaudeModelId(selected.observation) !== input.request.model) {
618
+ throw new Error("Claude highlighted a different model before effort selection");
619
+ }
620
+ let effortPicker = selected.observation;
621
+ const seen = new Set();
622
+ for (let count = 0; count < 7; count += 1) {
623
+ const displayed = effortPicker.displayedEffort;
624
+ if (!displayed)
625
+ throw new Error("Claude model picker has no exact effort value");
626
+ if (displayed === requestedEffort)
627
+ break;
628
+ if (seen.has(displayed)) {
629
+ throw new Error("the requested effort is absent from Claude's live effort ring");
630
+ }
631
+ seen.add(displayed);
632
+ const next = await transitionModelControl(input, control, effortPicker, claudeModelControlEffortKey("higher"), ["claude_model_picker"]);
633
+ control = next.capture.terminalControl;
634
+ if (next.observation.state !== "claude_model_picker") {
635
+ throw new Error("Claude effort adjustment left the model picker");
636
+ }
637
+ if (selectedClaudeModelId(next.observation) !== input.request.model) {
638
+ throw new Error("Claude changed the highlighted model during effort selection");
639
+ }
640
+ effortPicker = next.observation;
641
+ }
642
+ if (effortPicker.displayedEffort !== requestedEffort) {
643
+ throw new Error("the requested effort is absent from Claude's live effort ring");
644
+ }
645
+ if (selectedClaudeModelId(effortPicker) !== input.request.model) {
646
+ throw new Error("Claude changed the highlighted model before session-only commit");
647
+ }
648
+ irreversible = true;
649
+ await exactDispatchKeys(input, control, effortPicker, claudeModelControlCommitKeys(input.plan));
650
+ const closed = await waitForObservation(input, control, ["none"], {
651
+ allowImmediateNone: true
652
+ });
653
+ control = closed.capture.terminalControl;
654
+ }
655
+ else {
656
+ throw new Error("the native model picker changed before selection");
657
+ }
658
+ let persistence = input.agent === "codex"
659
+ ? await waitForCodexPersistencePostcondition(input, control, codexPersistenceBaseline ?? "", input.request, codexPersistenceScreen)
660
+ : { proven: true };
661
+ let post = await discoverTerminalModelOptions({
662
+ ...input, terminalControl: control
663
+ });
664
+ control = post.terminalControl;
665
+ let effective = post.catalog.current;
666
+ if (input.agent === "codex" &&
667
+ persistence.proven &&
668
+ observeCodexIdleModel(codexPersistenceBaseline ?? "")?.planMode === true &&
669
+ discovery.catalog.current.model !== input.request.model &&
670
+ effective.model === input.request.model &&
671
+ effective.reasoningEffort === discovery.catalog.current.reasoningEffort &&
672
+ effective.reasoningEffort !== requestedEffort) {
673
+ const reopened = await openModelPicker(input, control);
674
+ if (reopened.observation.state !== "codex_model_picker") {
675
+ throw new Error("Codex Plan-mode reconciliation did not open its model picker");
676
+ }
677
+ const secondBaseline = reopened.idleCapture.screen;
678
+ const reconciled = await applyCodexModelSelection(input, reopened.capture.terminalControl, reopened.observation, input.request, () => { irreversible = true; });
679
+ control = reconciled.capture.terminalControl;
680
+ persistence = await waitForCodexPersistencePostcondition(input, control, secondBaseline, input.request, reconciled.capture.screen);
681
+ post = await discoverTerminalModelOptions({
682
+ ...input, terminalControl: control
683
+ });
684
+ control = post.terminalControl;
685
+ effective = post.catalog.current;
686
+ }
687
+ if (effective.model !== input.request.model ||
688
+ effective.reasoningEffort !== requestedEffort) {
689
+ return {
690
+ terminalControl: control,
691
+ outcome: "uncertain",
692
+ scope: input.plan.scope,
693
+ defaultsChanged: null,
694
+ requested: input.request,
695
+ effective,
696
+ doNotRetry: true,
697
+ reason: "the native postcondition did not match the requested model and effort"
698
+ };
699
+ }
700
+ if (!persistence.proven) {
701
+ return {
702
+ terminalControl: control,
703
+ outcome: "uncertain",
704
+ scope: input.plan.scope,
705
+ defaultsChanged: null,
706
+ requested: input.request,
707
+ effective,
708
+ doNotRetry: true,
709
+ reason: persistence.reason
710
+ };
711
+ }
712
+ const newSessionDefaults = input.agent === "codex"
713
+ ? {
714
+ model: input.request.model,
715
+ ...(requestedEffort === "ultra"
716
+ ? {}
717
+ : { reasoningEffort: requestedEffort })
718
+ }
719
+ : undefined;
720
+ return {
721
+ terminalControl: control,
722
+ outcome: "changed",
723
+ scope: input.plan.scope,
724
+ defaultsChanged: input.agent === "codex",
725
+ requested: input.request,
726
+ effective,
727
+ ...(newSessionDefaults ? { newSessionDefaults } : {}),
728
+ doNotRetry: false
729
+ };
730
+ }
731
+ catch (error) {
732
+ const cleanupError = await tryUnwindModelControl(input, control);
733
+ if (!irreversible && !cleanupError)
734
+ throw error;
735
+ const detail = error instanceof Error ? error.message : String(error);
736
+ return {
737
+ terminalControl: control,
738
+ outcome: "uncertain",
739
+ scope: input.plan.scope,
740
+ defaultsChanged: null,
741
+ requested: input.request,
742
+ doNotRetry: true,
743
+ reason: cleanupError
744
+ ? `${detail}; exact native model-control cleanup failed: ${cleanupError}`
745
+ : detail
746
+ };
747
+ }
748
+ }
749
+ async function applyCodexModelSelection(input, terminalControl, picker, request, beforeIrreversible) {
750
+ const target = picker.rows.find((row) => row.id === request.model);
751
+ if (!target) {
752
+ throw new Error("the requested Codex model left the current native picker");
753
+ }
754
+ const moved = await transitionModelControl(input, terminalControl, picker, modelControlMoveKeys(picker.selectedIndex, target.nativeIndex), ["codex_model_picker"]);
755
+ if (moved.observation.state !== "codex_model_picker") {
756
+ throw new Error("Codex model navigation left its native picker");
757
+ }
758
+ const movedSelection = moved.observation.selectedIndex;
759
+ if (moved.observation.rows.find((row) => row.nativeIndex === movedSelection)?.id !== request.model) {
760
+ throw new Error("Codex highlighted a different model before selection");
761
+ }
762
+ // A remotely refreshed model can become single-effort after discovery; in
763
+ // that case Enter may commit immediately instead of opening a submenu.
764
+ beforeIrreversible();
765
+ const reasoning = await transitionModelControl(input, moved.capture.terminalControl, moved.observation, ["C-m"], ["codex_reasoning_picker"]);
766
+ if (reasoning.observation.state !== "codex_reasoning_picker" ||
767
+ reasoning.observation.model !== request.model) {
768
+ throw new Error("Codex opened a different reasoning picker");
769
+ }
770
+ let selection = reasoning.observation;
771
+ let keys = codexModelControlSelectEffortKeys(selection, request.reasoningEffort);
772
+ const advanced = request.reasoningEffort === "max" ||
773
+ request.reasoningEffort === "ultra";
774
+ if (!advanced)
775
+ beforeIrreversible();
776
+ let selected = await transitionModelControl(input, reasoning.capture.terminalControl, selection, keys, advanced
777
+ ? ["codex_advanced_reasoning_picker"]
778
+ : ["none", "codex_plan_scope_picker"], advanced ? {} : { allowImmediateNone: true });
779
+ if (advanced) {
780
+ if (selected.observation.state !== "codex_advanced_reasoning_picker") {
781
+ throw new Error("Codex advanced-reasoning picker did not materialize");
782
+ }
783
+ selection = selected.observation;
784
+ keys = codexModelControlSelectEffortKeys(selection, request.reasoningEffort);
785
+ beforeIrreversible();
786
+ selected = await transitionModelControl(input, selected.capture.terminalControl, selection, keys, ["none", "codex_plan_scope_picker"], { allowImmediateNone: true });
787
+ }
788
+ if (selected.observation.state !== "codex_plan_scope_picker") {
789
+ return selected;
790
+ }
791
+ beforeIrreversible();
792
+ return transitionModelControl(input, selected.capture.terminalControl, selected.observation, codexModelControlAllModesKeys(selected.observation), ["none"], { allowImmediateNone: true });
793
+ }
794
+ async function openModelPicker(input, terminalControl) {
795
+ let cleanupControl = terminalControl;
796
+ let terminalInputAttempted = false;
797
+ try {
798
+ let idleCapture;
799
+ let revalidatedCommand;
800
+ 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");
803
+ }
804
+ const observed = await inspectTerminalModelControlResidual({
805
+ plan: input.plan,
806
+ terminalControl,
807
+ ports: input.ports
808
+ });
809
+ if (observed.state !== "recoverable" ||
810
+ observed.kind !== input.initialResidual.kind ||
811
+ observed.fingerprint !== input.initialResidual.fingerprint) {
812
+ throw new Error("the exact /model residual changed before native catalog discovery");
813
+ }
814
+ cleanupControl = observed.terminalControl;
815
+ await input.ports.beforeInput();
816
+ revalidatedCommand = await input.ports.capture({
817
+ terminalControl: observed.terminalControl,
818
+ expectedComposer: input.plan.command
819
+ });
820
+ const revalidatedResidual = residualFromCapture(input.plan, revalidatedCommand);
821
+ if (revalidatedResidual.state !== "recoverable" ||
822
+ revalidatedResidual.kind !== input.initialResidual.kind ||
823
+ revalidatedResidual.fingerprint !== input.initialResidual.fingerprint) {
824
+ throw new Error("the exact /model residual changed immediately before Enter");
825
+ }
826
+ idleCapture = revalidatedCommand;
827
+ }
828
+ else {
829
+ const before = await input.ports.capture({ terminalControl });
830
+ cleanupControl = before.terminalControl;
831
+ if (before.activityState !== "idle" ||
832
+ modelControlCaptureBlocked(before) ||
833
+ !before.exactEmptyComposer) {
834
+ throw new Error("model control requires an exact verified idle pane with an empty composer and no blocking interaction");
835
+ }
836
+ await input.ports.beforeInput();
837
+ const finalEmpty = await input.ports.capture({
838
+ terminalControl: before.terminalControl
839
+ });
840
+ if (finalEmpty.activityState !== "idle" ||
841
+ modelControlCaptureBlocked(finalEmpty) ||
842
+ !finalEmpty.exactEmptyComposer) {
843
+ throw new Error("the exact idle composer changed before /model input");
844
+ }
845
+ terminalInputAttempted = true;
846
+ await input.ports.sendText(finalEmpty.terminalControl, input.plan.command);
847
+ let commandCapture;
848
+ const startedAt = Date.now();
849
+ while (Date.now() - startedAt <= MODEL_CONTROL_SETTLE_TIMEOUT_MS) {
850
+ const captured = await input.ports.capture({
851
+ terminalControl: before.terminalControl,
852
+ expectedComposer: input.plan.command
853
+ });
854
+ if (!modelControlCaptureBlocked(captured) &&
855
+ captured.activityState !== "working" &&
856
+ modelCommandReadyForEnter(input.plan, captured)) {
857
+ commandCapture = captured;
858
+ break;
859
+ }
860
+ await input.ports.sleep(MODEL_CONTROL_POLL_MS);
861
+ }
862
+ if (!commandCapture) {
863
+ throw new Error("the exact /model composer did not materialize before Enter");
864
+ }
865
+ await input.ports.beforeInput();
866
+ revalidatedCommand = await input.ports.capture({
867
+ terminalControl: commandCapture.terminalControl,
868
+ expectedComposer: input.plan.command
869
+ });
870
+ if (modelControlCaptureBlocked(revalidatedCommand) ||
871
+ revalidatedCommand.activityState === "working" ||
872
+ !modelCommandReadyForEnter(input.plan, revalidatedCommand)) {
873
+ throw new Error("the exact /model composer changed before Enter");
874
+ }
875
+ idleCapture = finalEmpty;
876
+ }
877
+ cleanupControl = revalidatedCommand.terminalControl;
878
+ terminalInputAttempted = true;
879
+ await input.ports.sendKeys(revalidatedCommand.terminalControl, ["C-m"]);
880
+ let opened = await waitForObservation(input, revalidatedCommand.terminalControl, ["codex_entry_model_picker", "codex_model_picker", "claude_model_picker"]);
881
+ if (opened.observation.state === "codex_entry_model_picker") {
882
+ const entry = opened.observation;
883
+ if (entry.kind !== "quick_auto" ||
884
+ entry.allModelsNativeIndex === undefined ||
885
+ entry.selectedIndex !== entry.currentNativeIndex ||
886
+ entry.selectedIndex !== entry.allModelsNativeIndex) {
887
+ throw new Error(entry.kind === "luna_reserve"
888
+ ? "Codex Luna Reserve model control cannot safely change persisted defaults"
889
+ : "Codex quick-auto model control is supported only when All models is the exact current selection");
890
+ }
891
+ opened = await transitionModelControl(input, opened.capture.terminalControl, entry, ["C-m"], ["codex_model_picker"]);
892
+ }
893
+ if ((opened.observation.state !== "codex_model_picker" &&
894
+ opened.observation.state !== "claude_model_picker") ||
895
+ opened.observation.selectedIndex !==
896
+ opened.observation.currentNativeIndex) {
897
+ throw new Error("the native model picker did not open on the exact current selection");
898
+ }
899
+ // This is the exact idle frame revalidated after current-snapshot authority
900
+ // and immediately before `/model` input. Earlier idle captures cannot prove
901
+ // the catalog tuple, Plan mode, or a persistence-message baseline.
902
+ return { ...opened, idleCapture };
903
+ }
904
+ catch (error) {
905
+ if (!terminalInputAttempted)
906
+ throw error;
907
+ const cleanupError = await tryUnwindModelControl(input, cleanupControl);
908
+ if (!cleanupError)
909
+ throw error;
910
+ const detail = error instanceof Error ? error.message : String(error);
911
+ throw new Error(`${detail}; exact native model-control cleanup failed: ${cleanupError}`);
912
+ }
913
+ }
914
+ async function tryUnwindModelControl(input, terminalControl) {
915
+ try {
916
+ await unwindModelControl(input, terminalControl);
917
+ return undefined;
918
+ }
919
+ catch (error) {
920
+ return error instanceof Error ? error.message : String(error);
921
+ }
922
+ }
923
+ async function unwindModelControl(input, terminalControl) {
924
+ let control = terminalControl;
925
+ for (let count = 0; count < 7; count += 1) {
926
+ const captured = await input.ports.capture({
927
+ terminalControl: control,
928
+ expectedComposer: input.plan.command
929
+ });
930
+ control = captured.terminalControl;
931
+ if (modelControlCaptureBlocked(captured) ||
932
+ captured.activityState === "working") {
933
+ throw new Error("the terminal became busy or blocked during cleanup");
934
+ }
935
+ if (captured.exactEmptyComposer && captured.activityState === "idle") {
936
+ return captured;
937
+ }
938
+ if (captured.exactCommandComposer) {
939
+ await input.ports.beforeInput();
940
+ const finalCommand = await input.ports.capture({
941
+ terminalControl: control,
942
+ expectedComposer: input.plan.command
943
+ });
944
+ if (modelControlCaptureBlocked(finalCommand) ||
945
+ finalCommand.activityState === "working" ||
946
+ !finalCommand.exactCommandComposer) {
947
+ throw new Error("the exact /model composer changed during cleanup");
948
+ }
949
+ control = finalCommand.terminalControl;
950
+ if (finalCommand.exactCommandReady ||
951
+ input.plan.behaviorProfile === "claude-model-control-2.1.266" ||
952
+ finalCommand.exactBareCommand !== true) {
953
+ await input.ports.sendKeys(control, ["Escape"]);
954
+ const afterEscape = await waitForModelCommandCleanupState(input, control);
955
+ control = afterEscape.terminalControl;
956
+ if (afterEscape.exactEmptyComposer)
957
+ return afterEscape;
958
+ }
959
+ await input.ports.beforeInput();
960
+ const finalBareCommand = await input.ports.capture({
961
+ terminalControl: control,
962
+ expectedComposer: input.plan.command
963
+ });
964
+ if (modelControlCaptureBlocked(finalBareCommand) ||
965
+ finalBareCommand.activityState === "working" ||
966
+ !finalBareCommand.exactCommandComposer ||
967
+ finalBareCommand.exactCommandReady) {
968
+ throw new Error(`the exact bare ${input.plan.behaviorProfile.startsWith("codex-")
969
+ ? "Codex"
970
+ : "Claude"} /model composer changed during cleanup`);
971
+ }
972
+ control = finalBareCommand.terminalControl;
973
+ await input.ports.sendKeys(control, ["C-u"]);
974
+ const cleared = await waitForObservation(input, control, ["none"], {
975
+ allowImmediateNone: true
976
+ });
977
+ control = cleared.capture.terminalControl;
978
+ continue;
979
+ }
980
+ const observation = observeTerminalModelControl(input.plan, captured.screen);
981
+ if (observation.state === "none" || observation.state === "ambiguous") {
982
+ throw new Error("no exact current model-control frame was available for Escape");
983
+ }
984
+ control = await exactDispatchKeys(input, control, observation, ["Escape"]);
985
+ const next = await waitForObservation(input, control, [
986
+ "none",
987
+ "codex_entry_model_picker",
988
+ "codex_model_picker",
989
+ "codex_reasoning_picker",
990
+ "codex_advanced_reasoning_picker",
991
+ "claude_model_picker"
992
+ ], {
993
+ allowImmediateNone: true,
994
+ allowExactCommandComposer: true,
995
+ excludeFingerprint: observation.fingerprint
996
+ });
997
+ control = next.capture.terminalControl;
998
+ }
999
+ throw new Error("the native model-control surface exceeded its cleanup depth");
1000
+ }
1001
+ async function waitForModelCommandCleanupState(input, terminalControl) {
1002
+ const startedAt = Date.now();
1003
+ let stableState;
1004
+ let stableCaptures = 0;
1005
+ while (Date.now() - startedAt <= MODEL_CONTROL_SETTLE_TIMEOUT_MS) {
1006
+ const captured = await input.ports.capture({
1007
+ terminalControl,
1008
+ expectedComposer: input.plan.command
1009
+ });
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
1017
+ ? "empty"
1018
+ : captured.exactCommandFingerprint
1019
+ ? `bare:${captured.exactCommandFingerprint}`
1020
+ : input.plan.behaviorProfile === "claude-model-control-2.1.266"
1021
+ ? "bare:claude-profiled-composer"
1022
+ : undefined;
1023
+ if (!semanticState) {
1024
+ stableState = undefined;
1025
+ stableCaptures = 0;
1026
+ await input.ports.sleep(MODEL_CONTROL_POLL_MS);
1027
+ continue;
1028
+ }
1029
+ if (semanticState === stableState)
1030
+ stableCaptures += 1;
1031
+ else {
1032
+ stableState = semanticState;
1033
+ stableCaptures = 1;
1034
+ }
1035
+ if (stableCaptures >= 2)
1036
+ return captured;
1037
+ }
1038
+ else {
1039
+ stableState = undefined;
1040
+ stableCaptures = 0;
1041
+ }
1042
+ await input.ports.sleep(MODEL_CONTROL_POLL_MS);
1043
+ }
1044
+ throw new Error("/model suggestions did not close to an exact bare or empty composer");
1045
+ }
1046
+ async function transitionModelControl(input, terminalControl, observation, keys, expectedStates, options = {}) {
1047
+ if (keys.length === 0) {
1048
+ return waitForObservation(input, terminalControl, expectedStates, options);
1049
+ }
1050
+ let control = terminalControl;
1051
+ let current = observation;
1052
+ for (const [index, key] of keys.entries()) {
1053
+ control = await exactDispatchKeys(input, control, current, [key]);
1054
+ const isLast = index === keys.length - 1;
1055
+ const next = await waitForObservation(input, control, isLast ? expectedStates : [current.state], {
1056
+ ...(isLast ? options : {}),
1057
+ ...(key === "Up" || key === "Down" || key === "Left" || key === "Right"
1058
+ ? { excludeFingerprint: current.fingerprint }
1059
+ : {})
1060
+ });
1061
+ current = next.observation;
1062
+ control = next.capture.terminalControl;
1063
+ if (isLast)
1064
+ return next;
1065
+ }
1066
+ throw new Error("model-control transition dispatched no key");
1067
+ }
1068
+ async function exactDispatchKeys(input, terminalControl, observation, keys) {
1069
+ if (keys.length === 0)
1070
+ return terminalControl;
1071
+ if (keys.length !== 1) {
1072
+ throw new Error("model-control transport accepts exactly one native key per proof");
1073
+ }
1074
+ await input.ports.beforeInput();
1075
+ const captured = await input.ports.capture({ terminalControl });
1076
+ const current = observeTerminalModelControl(input.plan, captured.screen);
1077
+ if (modelControlCaptureBlocked(captured) ||
1078
+ captured.activityState === "working" ||
1079
+ current.state !== observation.state ||
1080
+ current.fingerprint !== observation.fingerprint) {
1081
+ throw new Error("the native model-control frame changed before key dispatch");
1082
+ }
1083
+ await input.ports.sendKeys(captured.terminalControl, keys);
1084
+ return captured.terminalControl;
1085
+ }
1086
+ async function waitForObservation(input, terminalControl, expectedStates, options = {}) {
1087
+ const startedAt = Date.now();
1088
+ let stableFingerprint;
1089
+ let stableCaptures = 0;
1090
+ while (Date.now() - startedAt <= MODEL_CONTROL_SETTLE_TIMEOUT_MS) {
1091
+ const captured = await input.ports.capture({ terminalControl });
1092
+ if (modelControlCaptureBlocked(captured) ||
1093
+ captured.activityState === "working") {
1094
+ throw new Error("the terminal became busy or blocked during model control");
1095
+ }
1096
+ const observation = observeTerminalModelControl(input.plan, captured.screen);
1097
+ const expectedNone = observation.state === "none" && (options.allowImmediateNone === true && captured.exactEmptyComposer ||
1098
+ options.allowExactCommandComposer === true &&
1099
+ captured.exactCommandComposer);
1100
+ const expected = observation.fingerprint !== options.excludeFingerprint &&
1101
+ expectedStates.includes(observation.state) &&
1102
+ (observation.state !== "none" || expectedNone);
1103
+ if (expected) {
1104
+ if (observation.fingerprint === stableFingerprint)
1105
+ stableCaptures += 1;
1106
+ else {
1107
+ stableFingerprint = observation.fingerprint;
1108
+ stableCaptures = 1;
1109
+ }
1110
+ if (stableCaptures >= 2)
1111
+ return { capture: captured, observation };
1112
+ }
1113
+ else {
1114
+ stableFingerprint = undefined;
1115
+ stableCaptures = 0;
1116
+ }
1117
+ await input.ports.sleep(MODEL_CONTROL_POLL_MS);
1118
+ }
1119
+ throw new Error("the expected native model-control frame did not become stable");
1120
+ }
1121
+ async function exactDismiss(input, terminalControl, observation) {
1122
+ const control = await exactDispatchKeys(input, terminalControl, observation, ["Escape"]);
1123
+ const closed = await waitForObservation(input, control, ["none"], {
1124
+ allowImmediateNone: true,
1125
+ allowExactCommandComposer: true,
1126
+ excludeFingerprint: observation.fingerprint
1127
+ });
1128
+ if (closed.capture.exactEmptyComposer &&
1129
+ closed.capture.activityState === "idle") {
1130
+ return closed.capture;
1131
+ }
1132
+ if (closed.capture.exactCommandComposer) {
1133
+ return unwindModelControl(input, closed.capture.terminalControl);
1134
+ }
1135
+ throw new Error("native model-control dismissal did not return to exact idle");
1136
+ }
1137
+ function observeCodexModelControl(screen) {
1138
+ const lines = terminalTailLines(screen);
1139
+ const fingerprint = screenFingerprint(lines);
1140
+ const entryHeader = lastIndex(lines, (line) => line.trim() === "Select Model");
1141
+ const modelHeader = lastIndex(lines, (line) => line.trim() === "Select Model and Effort");
1142
+ const reasoningHeader = lastIndex(lines, (line) => /^\s*Select Reasoning Level for \S+\s*$/u.test(line));
1143
+ const advancedHeader = lastIndex(lines, (line) => line.trim() === "Advanced Reasoning");
1144
+ const scopeHeader = lastIndex(lines, (line) => line.trim() === "Apply reasoning change");
1145
+ const latest = Math.max(entryHeader, modelHeader, reasoningHeader, advancedHeader, scopeHeader);
1146
+ if (latest < 0) {
1147
+ return { state: "none", fingerprint, reason: "no current Codex model picker is visible" };
1148
+ }
1149
+ const region = currentPickerRegion(lines, latest);
1150
+ if (!region) {
1151
+ return {
1152
+ state: "ambiguous",
1153
+ fingerprint,
1154
+ reason: "the current Codex model picker is incomplete or has trailing content"
1155
+ };
1156
+ }
1157
+ if (region.some((line) => line.includes("OpenAI base URL is overridden"))) {
1158
+ return {
1159
+ state: "ambiguous",
1160
+ fingerprint,
1161
+ reason: "Codex model control is unavailable with an overridden OpenAI base URL"
1162
+ };
1163
+ }
1164
+ if (latest === entryHeader) {
1165
+ const parsedEntry = parseCodexEntryModelPicker(region);
1166
+ if (!parsedEntry) {
1167
+ return {
1168
+ state: "ambiguous",
1169
+ fingerprint,
1170
+ reason: "the Codex entry model picker is not exact"
1171
+ };
1172
+ }
1173
+ return { state: "codex_entry_model_picker", fingerprint, ...parsedEntry };
1174
+ }
1175
+ if (latest === modelHeader) {
1176
+ const parsedRows = parseCodexModelRows(region);
1177
+ if (!parsedRows) {
1178
+ return { state: "ambiguous", fingerprint, reason: "the Codex model catalog frame is not exact" };
1179
+ }
1180
+ return { state: "codex_model_picker", fingerprint, ...parsedRows };
1181
+ }
1182
+ if (latest === reasoningHeader) {
1183
+ const model = /^\s*Select Reasoning Level for (\S+)\s*$/u.exec(region[0] ?? "")?.[1];
1184
+ const parsedRows = parseCodexEffortRows(region, false);
1185
+ if (!model || !parsedRows) {
1186
+ return { state: "ambiguous", fingerprint, reason: "the Codex reasoning frame is not exact" };
1187
+ }
1188
+ return {
1189
+ state: "codex_reasoning_picker",
1190
+ fingerprint,
1191
+ model,
1192
+ ...parsedRows
1193
+ };
1194
+ }
1195
+ if (latest === advancedHeader) {
1196
+ const parsedRows = parseCodexEffortRows(region, true);
1197
+ if (!parsedRows) {
1198
+ return { state: "ambiguous", fingerprint, reason: "the Codex advanced-reasoning frame is not exact" };
1199
+ }
1200
+ return {
1201
+ state: "codex_advanced_reasoning_picker",
1202
+ fingerprint,
1203
+ ...parsedRows
1204
+ };
1205
+ }
1206
+ const rows = region.flatMap((line) => {
1207
+ const match = /^\s*(?:›\s*)?\d+\.\s+(Apply to (?:Plan mode override|global default and Plan mode override))\s*$/u
1208
+ .exec(line);
1209
+ return match ? [match[1]] : [];
1210
+ });
1211
+ const numberedLines = region.filter((line) => /^\s*(?:›\s*)?\d+\./u.test(line));
1212
+ const selectedLines = numberedLines.filter((line) => /^\s*›\s*\d+\./u.test(line));
1213
+ if (rows.length !== 2 ||
1214
+ numberedLines.length !== rows.length ||
1215
+ selectedLines.length !== 1 ||
1216
+ rows[0] !== "Apply to Plan mode override" ||
1217
+ rows[1] !== "Apply to global default and Plan mode override" ||
1218
+ !numberedLines.every((line, index) => new RegExp(`^\\s*(?:›\\s*)?${index + 1}\\.`).test(line))) {
1219
+ return { state: "ambiguous", fingerprint, reason: "the Codex Plan-mode scope frame is not exact" };
1220
+ }
1221
+ const selectedRow = numberedLines.findIndex((line) => /^\s*›/u.test(line));
1222
+ return {
1223
+ state: "codex_plan_scope_picker",
1224
+ fingerprint,
1225
+ rows,
1226
+ selectedIndex: selectedRow
1227
+ };
1228
+ }
1229
+ function observeClaudeModelControl(screen) {
1230
+ const lines = terminalTailLines(screen);
1231
+ const fingerprint = screenFingerprint(lines);
1232
+ const headerIndex = lastIndex(lines, (line) => line.trim() === "Select model");
1233
+ if (headerIndex < 0) {
1234
+ return { state: "none", fingerprint, reason: "no current Claude model picker is visible" };
1235
+ }
1236
+ if (headerIndex === 0 ||
1237
+ !/^\s*[▔¯─━]{8,}\s*$/u.test(lines[headerIndex - 1])) {
1238
+ return { state: "ambiguous", fingerprint, reason: "the Claude model picker is not anchored to its top border" };
1239
+ }
1240
+ const region = lines.slice(headerIndex);
1241
+ const footerIndex = region.findIndex((line) => /^\s*Enter to set as default\s+·\s+s to use this session only\s+·\s+Esc to cancel\s*$/u
1242
+ .test(line));
1243
+ if (footerIndex < 0 || region.slice(footerIndex + 1).some((line) => line.trim())) {
1244
+ return { state: "ambiguous", fingerprint, reason: "the current Claude model picker footer is incomplete" };
1245
+ }
1246
+ const picker = region.slice(0, footerIndex + 1);
1247
+ const rows = [];
1248
+ const nativeRows = [];
1249
+ for (const line of picker) {
1250
+ const match = /^\s*(❯\s*)?(\d+)\.\s+(.+?)(?:\s{2,})(\S.*)$/u.exec(line);
1251
+ if (!match)
1252
+ continue;
1253
+ const rawLabel = match[3].trim();
1254
+ const label = rawLabel.replace(/\s+✔\s*$/u, "").trim();
1255
+ const description = match[4].trim();
1256
+ nativeRows.push({
1257
+ nativeIndex: Number(match[2]) - 1,
1258
+ selected: Boolean(match[1]),
1259
+ label,
1260
+ description,
1261
+ current: rawLabel.endsWith("✔")
1262
+ });
1263
+ }
1264
+ const numberedClaudeLines = picker.filter((line) => /^\s*(?:❯\s*)?\d+\./u.test(line));
1265
+ if (numberedClaudeLines.length !== nativeRows.length ||
1266
+ nativeRows.some((row, index) => row.nativeIndex !== index)) {
1267
+ return { state: "ambiguous", fingerprint, reason: "the Claude model numbering is not exact" };
1268
+ }
1269
+ for (const row of nativeRows) {
1270
+ if (/^Default(?:\s+\(recommended\))?$/iu.test(row.label) ||
1271
+ /Opus Plan Mode/iu.test(row.label) ||
1272
+ /Opus Plan Mode/iu.test(row.description) ||
1273
+ /\bFable\b/iu.test(row.label) ||
1274
+ /\bFable\b/iu.test(row.description))
1275
+ continue;
1276
+ const id = claudeSemanticModelId(`${row.label} ${row.description}`);
1277
+ if (!id)
1278
+ continue;
1279
+ rows.push({
1280
+ id,
1281
+ label: row.label,
1282
+ nativeIndex: row.nativeIndex,
1283
+ selected: row.selected,
1284
+ current: row.current,
1285
+ presetDefault: false
1286
+ });
1287
+ }
1288
+ const selectedRows = nativeRows.filter((row) => row.selected);
1289
+ const selectedNativeIndex = selectedRows[0]?.nativeIndex ?? -1;
1290
+ const currentNativeRows = nativeRows.filter((row) => row.current);
1291
+ const currentModel = currentNativeRows.length === 1
1292
+ ? claudeSemanticModelId(`${currentNativeRows[0].label} ${currentNativeRows[0].description}`)
1293
+ : undefined;
1294
+ if (rows.length === 0 ||
1295
+ selectedRows.length !== 1 ||
1296
+ !currentModel ||
1297
+ !rows.some((row) => row.id === currentModel) ||
1298
+ new Set(rows.map((row) => row.id)).size !== rows.length) {
1299
+ return { state: "ambiguous", fingerprint, reason: "the Claude model rows are not profiled" };
1300
+ }
1301
+ const displayedEfforts = picker.flatMap((line) => {
1302
+ const match = /^\s*[●○◐◉◈✦]\s+(Low|Medium|High|Extra high|Xhigh|Max|Ultracode)\s+effort\b/iu
1303
+ .exec(line);
1304
+ const value = match ? claudeEffortFromNativeLabel(match[1]) : undefined;
1305
+ return value ? [value] : [];
1306
+ });
1307
+ if (displayedEfforts.length > 1) {
1308
+ return { state: "ambiguous", fingerprint, reason: "the Claude effort row is not exact" };
1309
+ }
1310
+ const displayedEffort = displayedEfforts[0];
1311
+ return {
1312
+ state: "claude_model_picker",
1313
+ fingerprint,
1314
+ rows,
1315
+ selectedIndex: selectedNativeIndex,
1316
+ currentNativeIndex: currentNativeRows[0].nativeIndex,
1317
+ currentModel,
1318
+ ...(displayedEffort && displayedEffort !== "ultracode"
1319
+ ? { currentEffort: displayedEffort }
1320
+ : {}),
1321
+ ...(displayedEffort ? { displayedEffort } : {})
1322
+ };
1323
+ }
1324
+ function parseCodexModelRows(region) {
1325
+ const rows = [];
1326
+ for (const line of region.slice(1, -1)) {
1327
+ const match = /^\s*(›\s*)?(\d+)\.\s+([a-z0-9][a-z0-9._:/+\-]*)(?:\s+((?:\((?:current|default)\)\s*){1,2}))?(?:\s{2,}.*)?$/u
1328
+ .exec(line);
1329
+ if (!match)
1330
+ continue;
1331
+ const flags = match[4] ?? "";
1332
+ rows.push({
1333
+ id: match[3],
1334
+ label: match[3],
1335
+ nativeIndex: Number(match[2]) - 1,
1336
+ selected: Boolean(match[1]),
1337
+ current: flags.includes("(current)"),
1338
+ presetDefault: flags.includes("(default)")
1339
+ });
1340
+ }
1341
+ const numberedLines = region.slice(1, -1).filter((line) => /^\s*(?:›\s*)?\d+\./u.test(line));
1342
+ const selectedRows = rows.filter((row) => row.selected);
1343
+ const selectedIndex = selectedRows[0]?.nativeIndex ?? -1;
1344
+ const currentRows = rows.filter((row) => row.current);
1345
+ if (rows.length === 0 ||
1346
+ numberedLines.length !== rows.length ||
1347
+ rows.some((row, index) => row.nativeIndex !== index) ||
1348
+ selectedRows.length !== 1 ||
1349
+ currentRows.length !== 1 ||
1350
+ new Set(rows.map((row) => row.id)).size !== rows.length) {
1351
+ return undefined;
1352
+ }
1353
+ return {
1354
+ rows,
1355
+ selectedIndex,
1356
+ currentNativeIndex: currentRows[0].nativeIndex,
1357
+ currentModel: currentRows[0].id
1358
+ };
1359
+ }
1360
+ function parseCodexEntryModelPicker(region) {
1361
+ const subtitle = region[1]?.trim();
1362
+ const kind = subtitle === "Pick a quick auto mode or browse all models."
1363
+ ? "quick_auto"
1364
+ : subtitle === "Other models return when ordinary usage is available again."
1365
+ ? "luna_reserve"
1366
+ : undefined;
1367
+ if (!kind)
1368
+ return undefined;
1369
+ const numbered = region.slice(2, -1).filter((line) => /^\s*(?:›\s*)?\d+\./u.test(line));
1370
+ const rows = numbered.map((line) => {
1371
+ const match = /^\s*(›\s*)?(\d+)\.\s+(.+)$/u.exec(line);
1372
+ if (!match)
1373
+ return undefined;
1374
+ const current = /(?:^|\s)\(current\)(?:\s{2,}|\s*$)/u.test(match[3]);
1375
+ const label = match[3]
1376
+ .replace(/\s+\(current\)(?=\s{2,}|\s*$)/u, "")
1377
+ .split(/\s{2,}/u, 1)[0]
1378
+ ?.trim();
1379
+ return {
1380
+ nativeIndex: Number(match[2]) - 1,
1381
+ selected: Boolean(match[1]),
1382
+ current,
1383
+ label
1384
+ };
1385
+ });
1386
+ if (rows.some((row) => !row) ||
1387
+ rows.length === 0 ||
1388
+ rows.some((row, index) => row?.nativeIndex !== index))
1389
+ return undefined;
1390
+ const exactRows = rows.filter((row) => Boolean(row));
1391
+ const selected = exactRows.filter((row) => row.selected);
1392
+ const current = exactRows.filter((row) => row.current);
1393
+ if (selected.length !== 1 || current.length !== 1)
1394
+ return undefined;
1395
+ if (kind === "quick_auto") {
1396
+ const allModels = exactRows.filter((row) => row.label === "All models");
1397
+ if (allModels.length !== 1 ||
1398
+ exactRows.at(-1)?.label !== "All models" ||
1399
+ exactRows.slice(0, -1).some((row) => !/^codex-auto-[a-z0-9][a-z0-9._:/+\-]*$/u.test(row.label ?? "")))
1400
+ return undefined;
1401
+ return {
1402
+ kind,
1403
+ selectedIndex: selected[0].nativeIndex,
1404
+ currentNativeIndex: current[0].nativeIndex,
1405
+ allModelsNativeIndex: allModels[0].nativeIndex
1406
+ };
1407
+ }
1408
+ if (exactRows.length !== 1 || !exactRows[0].label)
1409
+ return undefined;
1410
+ return {
1411
+ kind,
1412
+ selectedIndex: selected[0].nativeIndex,
1413
+ currentNativeIndex: current[0].nativeIndex
1414
+ };
1415
+ }
1416
+ function parseCodexEffortRows(region, advanced) {
1417
+ const rows = [];
1418
+ for (const line of region.slice(1, -1)) {
1419
+ const match = /^\s*(›\s*)?\d+\.\s+(Low|Medium|High|Extra high|Max|Ultra|Persistent|More reasoning…)(?:\s+((?:\((?:current|default)\)\s*){1,2}))?(?:\s{2,}.*)?$/u
1420
+ .exec(line);
1421
+ if (!match)
1422
+ continue;
1423
+ const effort = effortFromNativeLabel(match[2]);
1424
+ const flags = match[3] ?? "";
1425
+ rows.push({
1426
+ ...(effort ? { effort } : {}),
1427
+ kind: match[2] === "More reasoning…"
1428
+ ? "advanced"
1429
+ : match[2] === "Persistent"
1430
+ ? "unsupported"
1431
+ : "effort",
1432
+ selected: Boolean(match[1]),
1433
+ current: flags.includes("(current)"),
1434
+ presetDefault: flags.includes("(default)")
1435
+ });
1436
+ }
1437
+ const numberedLines = region.slice(1, -1).filter((line) => /^\s*(?:›\s*)?\d+\./u.test(line));
1438
+ const selectedRows = rows.filter((row) => row.selected);
1439
+ const selectedIndex = rows.findIndex((row) => row.selected);
1440
+ const current = rows.filter((row) => row.current && row.effort);
1441
+ const defaults = rows.filter((row) => row.presetDefault && row.effort);
1442
+ const allowed = advanced
1443
+ ? rows.every((row) => row.kind === "effort" && ["max", "ultra"].includes(row.effort ?? ""))
1444
+ : rows.every((row) => row.kind === "advanced" || row.kind === "unsupported" ||
1445
+ !["max", "ultra"].includes(row.effort ?? ""));
1446
+ if (rows.length === 0 ||
1447
+ numberedLines.length !== rows.length ||
1448
+ !numberedLines.every((line, index) => new RegExp(`^\\s*(?:›\\s*)?${index + 1}\\.`).test(line)) ||
1449
+ selectedRows.length !== 1 ||
1450
+ selectedIndex < 0 ||
1451
+ current.length > 1 ||
1452
+ defaults.length > 1 ||
1453
+ !allowed)
1454
+ return undefined;
1455
+ return {
1456
+ rows,
1457
+ selectedIndex,
1458
+ ...(current[0]?.effort ? { currentEffort: current[0].effort } : {}),
1459
+ ...(defaults[0]?.effort
1460
+ ? { presetDefaultEffort: defaults[0].effort }
1461
+ : {})
1462
+ };
1463
+ }
1464
+ function currentPickerRegion(lines, headerIndex) {
1465
+ const after = lines.slice(headerIndex);
1466
+ const footerIndex = after.findIndex((line) => line.trim() === "Press enter to confirm or esc to go back");
1467
+ if (footerIndex < 0 || after.slice(footerIndex + 1).some((line) => line.trim())) {
1468
+ return undefined;
1469
+ }
1470
+ return after.slice(0, footerIndex + 1);
1471
+ }
1472
+ function effortFromNativeLabel(label) {
1473
+ return {
1474
+ Low: "low",
1475
+ Medium: "medium",
1476
+ High: "high",
1477
+ "Extra high": "xhigh",
1478
+ Max: "max",
1479
+ Ultra: "ultra"
1480
+ }[label];
1481
+ }
1482
+ function claudeEffortFromNativeLabel(label) {
1483
+ const normalized = label.trim().toLowerCase();
1484
+ return {
1485
+ low: "low",
1486
+ medium: "medium",
1487
+ high: "high",
1488
+ "extra high": "xhigh",
1489
+ xhigh: "xhigh",
1490
+ max: "max",
1491
+ ultracode: "ultracode"
1492
+ }[normalized];
1493
+ }
1494
+ function claudeSemanticModelId(label) {
1495
+ const family = /\b(opus|sonnet|haiku)\b/iu.exec(label)?.[1].toLowerCase();
1496
+ if (!family || !/^(?:opus|sonnet|haiku)$/u.test(family))
1497
+ return undefined;
1498
+ const version = new RegExp(`\\b${family}[-\\s]+(\\d+(?:\\.\\d+)?)\\b`, "iu").exec(label)?.[1] ?? /\b(\d+\.\d+)\b/u.exec(label)?.[1];
1499
+ const longContext = /(?:\[?1m\]?|1\s*million)(?:\s+context)?\b/iu.test(label);
1500
+ return [family, version, longContext ? "1m" : undefined]
1501
+ .filter((part) => Boolean(part))
1502
+ .join("-");
1503
+ }
1504
+ function terminalTailLines(screen) {
1505
+ const lines = screen.split("\n").slice(-160);
1506
+ while (lines.length > 0 && lines.at(-1)?.trim() === "")
1507
+ lines.pop();
1508
+ return lines;
1509
+ }
1510
+ function lastIndex(lines, predicate) {
1511
+ for (let index = lines.length - 1; index >= 0; index -= 1) {
1512
+ if (predicate(lines[index]))
1513
+ return index;
1514
+ }
1515
+ return -1;
1516
+ }
1517
+ function screenFingerprint(lines) {
1518
+ return createHash("sha256").update(lines.join("\n")).digest("hex");
1519
+ }
1520
+ function stripAnsi(value) {
1521
+ return value.replace(/\x1B(?:\[[0-?]*[ -/]*[@-~]|\][^\x07]*(?:\x07|\x1B\\))/gu, "");
1522
+ }
1523
+ function isPlainRecord(value) {
1524
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1525
+ }
1526
+ function nonBlank(value) {
1527
+ return typeof value === "string" && value.trim() ? value.trim() : undefined;
1528
+ }
1529
+ function codexPersistencePostcondition(beforeScreen, afterScreen, request) {
1530
+ const errorPatterns = [
1531
+ /Failed to save default model:/giu,
1532
+ /Failed to save Plan mode reasoning effort:/giu,
1533
+ /Saved default model and reasoning effort, but a higher-priority configuration\s+layer overrides the saved value\./giu,
1534
+ /Saved Plan mode reasoning effort, but a higher-priority configuration\s+layer overrides the saved value\./giu
1535
+ ];
1536
+ if (errorPatterns.some((pattern) => matchCount(afterScreen, pattern) > matchCount(beforeScreen, pattern))) {
1537
+ return {
1538
+ proven: false,
1539
+ final: true,
1540
+ reason: "Codex reported that its persisted model default failed or is overridden by a higher-priority configuration layer"
1541
+ };
1542
+ }
1543
+ const model = escapeRegExp(request.model);
1544
+ const effort = request.reasoningEffort === "xhigh"
1545
+ ? "(?:xhigh|extra high)"
1546
+ : escapeRegExp(request.reasoningEffort);
1547
+ const suffix = request.reasoningEffort === "ultra"
1548
+ ? "\\s+for this conversation"
1549
+ : "";
1550
+ const success = new RegExp(`Model changed to\\s+${model}\\s+${effort}${suffix}(?:\\s|$)`, "giu");
1551
+ if (matchCount(afterScreen, success) <= matchCount(beforeScreen, success)) {
1552
+ return {
1553
+ proven: false,
1554
+ final: false,
1555
+ reason: "Codex did not expose a fresh exact model-default persistence success frame"
1556
+ };
1557
+ }
1558
+ return { proven: true };
1559
+ }
1560
+ async function waitForCodexPersistencePostcondition(input, terminalControl, baselineScreen, request, initialScreen) {
1561
+ const startedAt = Date.now();
1562
+ let screen = initialScreen;
1563
+ while (Date.now() - startedAt <= MODEL_CONTROL_SETTLE_TIMEOUT_MS) {
1564
+ if (screen !== undefined) {
1565
+ const evidence = codexPersistencePostcondition(baselineScreen, screen, request);
1566
+ if (evidence.proven || evidence.final)
1567
+ return evidence;
1568
+ }
1569
+ const captured = await input.ports.capture({ terminalControl });
1570
+ if (modelControlCaptureBlocked(captured) ||
1571
+ captured.activityState !== "idle" ||
1572
+ !captured.exactEmptyComposer) {
1573
+ return {
1574
+ proven: false,
1575
+ final: true,
1576
+ reason: "Codex left the exact idle frame while persistence evidence was pending"
1577
+ };
1578
+ }
1579
+ terminalControl = captured.terminalControl;
1580
+ screen = captured.screen;
1581
+ await input.ports.sleep(MODEL_CONTROL_POLL_MS);
1582
+ }
1583
+ return {
1584
+ proven: false,
1585
+ final: true,
1586
+ reason: "Codex model-default persistence evidence did not materialize in time"
1587
+ };
1588
+ }
1589
+ function matchCount(value, pattern) {
1590
+ return [...value.matchAll(pattern)].length;
1591
+ }
1592
+ function escapeRegExp(value) {
1593
+ return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
1594
+ }
1595
+ //# sourceMappingURL=terminal-model-control.js.map