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