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