@scotthuang/agent-knock-knock 0.12.13 → 0.12.14
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 +18 -0
- package/README.md +5 -5
- package/dist/src/callback-cli-adapter.d.ts +9 -6
- package/dist/src/callback-cli-adapter.js +119 -30
- package/dist/src/callback-cli-adapter.js.map +1 -1
- package/dist/src/callback-outbox-policy.d.ts +39 -0
- package/dist/src/callback-outbox-policy.js +163 -6
- package/dist/src/callback-outbox-policy.js.map +1 -1
- package/dist/src/callback-outbox-service.d.ts +28 -1
- package/dist/src/callback-outbox-service.js +413 -82
- package/dist/src/callback-outbox-service.js.map +1 -1
- package/dist/src/callback-outbox-settlement.d.ts +14 -1
- package/dist/src/callback-outbox-settlement.js +151 -30
- package/dist/src/callback-outbox-settlement.js.map +1 -1
- package/dist/src/callback-route-authority.d.ts +59 -0
- package/dist/src/callback-route-authority.js +123 -0
- package/dist/src/callback-route-authority.js.map +1 -0
- package/dist/src/callback-transport.d.ts +134 -0
- package/dist/src/callback-transport.js +256 -0
- package/dist/src/callback-transport.js.map +1 -0
- package/dist/src/cli-core.js +17 -4
- package/dist/src/cli-core.js.map +1 -1
- package/dist/src/codex-lifecycle-compatibility.js +2 -1
- package/dist/src/codex-lifecycle-compatibility.js.map +1 -1
- package/dist/src/deferred-foreground-application-service.d.ts +18 -0
- package/dist/src/deferred-foreground-application-service.js +72 -0
- package/dist/src/deferred-foreground-application-service.js.map +1 -1
- package/dist/src/deferred-foreground-authority-cli-adapter.js +2 -1
- package/dist/src/deferred-foreground-authority-cli-adapter.js.map +1 -1
- package/dist/src/deferred-foreground-recovery-cli-adapter.d.ts +1 -1
- package/dist/src/deferred-foreground-recovery-cli-adapter.js +108 -37
- package/dist/src/deferred-foreground-recovery-cli-adapter.js.map +1 -1
- package/dist/src/deferred-foreground-recovery-service.d.ts +1 -1
- package/dist/src/deferred-foreground-recovery-service.js +10 -3
- package/dist/src/deferred-foreground-recovery-service.js.map +1 -1
- package/dist/src/deferred-foreground-transfer-policy.d.ts +1 -1
- package/dist/src/deferred-foreground-transfer-policy.js +3 -2
- package/dist/src/deferred-foreground-transfer-policy.js.map +1 -1
- package/dist/src/deferred-foreground-transfer.d.ts +17 -1
- package/dist/src/deferred-foreground-transfer.js +244 -35
- package/dist/src/deferred-foreground-transfer.js.map +1 -1
- package/dist/src/deferred-foreground-user-close.d.ts +18 -0
- package/dist/src/deferred-foreground-user-close.js +180 -0
- package/dist/src/deferred-foreground-user-close.js.map +1 -0
- package/dist/src/openclaw-callback-transport.d.ts +4 -1
- package/dist/src/openclaw-callback-transport.js +403 -10
- package/dist/src/openclaw-callback-transport.js.map +1 -1
- package/dist/src/openclaw-managed-callback-cli-adapter.d.ts +23 -0
- package/dist/src/openclaw-managed-callback-cli-adapter.js +77 -0
- package/dist/src/openclaw-managed-callback-cli-adapter.js.map +1 -0
- package/dist/src/openclaw-plugin-command-adapter.js +28 -31
- package/dist/src/openclaw-plugin-command-adapter.js.map +1 -1
- package/dist/src/openclaw-plugin-helpers.js +8 -0
- package/dist/src/openclaw-plugin-helpers.js.map +1 -1
- package/dist/src/openclaw-plugin-schemas.d.ts +20 -1
- package/dist/src/openclaw-plugin-schemas.js +26 -2
- package/dist/src/openclaw-plugin-schemas.js.map +1 -1
- package/dist/src/protocol.d.ts +3 -0
- package/dist/src/protocol.js +4 -0
- package/dist/src/protocol.js.map +1 -1
- package/dist/src/store.d.ts +9 -0
- package/dist/src/store.js +79 -1
- package/dist/src/store.js.map +1 -1
- package/dist/src/terminal-acceptance-cli-adapter.d.ts +2 -0
- package/dist/src/terminal-acceptance-cli-adapter.js +62 -12
- package/dist/src/terminal-acceptance-cli-adapter.js.map +1 -1
- package/dist/src/terminal-action-projection.d.ts +1 -2
- package/dist/src/terminal-action-projection.js +4 -5
- package/dist/src/terminal-action-projection.js.map +1 -1
- package/dist/src/terminal-agent-bridge.d.ts +82 -0
- package/dist/src/terminal-agent-bridge.js +539 -111
- package/dist/src/terminal-agent-bridge.js.map +1 -1
- package/dist/src/terminal-command-cli-adapter.d.ts +9 -0
- package/dist/src/terminal-command-cli-adapter.js +2130 -5
- package/dist/src/terminal-command-cli-adapter.js.map +1 -1
- package/dist/src/terminal-dispatch-application.d.ts +4 -1
- package/dist/src/terminal-dispatch-application.js +11 -2
- package/dist/src/terminal-dispatch-application.js.map +1 -1
- package/dist/src/terminal-dispatch-execution.js +3 -1
- package/dist/src/terminal-dispatch-execution.js.map +1 -1
- package/dist/src/terminal-dispatch-ledger-codec.d.ts +3 -0
- package/dist/src/terminal-dispatch-ledger-codec.js +46 -19
- package/dist/src/terminal-dispatch-ledger-codec.js.map +1 -1
- package/dist/src/terminal-dispatch-presenter.js +5 -3
- package/dist/src/terminal-dispatch-presenter.js.map +1 -1
- package/dist/src/terminal-dispatch-receipt.d.ts +3 -0
- package/dist/src/terminal-dispatch-receipt.js +15 -1
- package/dist/src/terminal-dispatch-receipt.js.map +1 -1
- package/dist/src/terminal-dispatch-recovery-cli-adapter.js +68 -7
- package/dist/src/terminal-dispatch-recovery-cli-adapter.js.map +1 -1
- package/dist/src/terminal-dispatch-recovery-service.d.ts +2 -0
- package/dist/src/terminal-dispatch-recovery-service.js +9 -0
- package/dist/src/terminal-dispatch-recovery-service.js.map +1 -1
- package/dist/src/terminal-identity-authority-cli-adapter.d.ts +2 -0
- package/dist/src/terminal-identity-authority-cli-adapter.js +30 -27
- package/dist/src/terminal-identity-authority-cli-adapter.js.map +1 -1
- package/dist/src/terminal-list-cli-adapter.d.ts +1 -0
- package/dist/src/terminal-list-cli-adapter.js +50 -31
- package/dist/src/terminal-list-cli-adapter.js.map +1 -1
- package/dist/src/terminal-list-renderer.d.ts +9 -1
- package/dist/src/terminal-list-renderer.js +219 -98
- package/dist/src/terminal-list-renderer.js.map +1 -1
- package/dist/src/terminal-maintenance-cli-adapter.js +253 -326
- package/dist/src/terminal-maintenance-cli-adapter.js.map +1 -1
- package/dist/src/terminal-monitor-application-service.d.ts +5 -0
- package/dist/src/terminal-monitor-application-service.js +5 -0
- package/dist/src/terminal-monitor-application-service.js.map +1 -1
- package/dist/src/terminal-monitor-cli-adapter.d.ts +25 -1
- package/dist/src/terminal-monitor-cli-adapter.js +170 -27
- package/dist/src/terminal-monitor-cli-adapter.js.map +1 -1
- package/dist/src/terminal-monitor-reconciliation-eligibility.d.ts +1 -1
- package/dist/src/terminal-monitor-reconciliation-eligibility.js +220 -32
- package/dist/src/terminal-monitor-reconciliation-eligibility.js.map +1 -1
- package/dist/src/terminal-monitor-state-cli-adapter.d.ts +1 -1
- package/dist/src/terminal-monitor-state-cli-adapter.js +808 -147
- package/dist/src/terminal-monitor-state-cli-adapter.js.map +1 -1
- package/dist/src/terminal-monitor-state-reconciliation-service.d.ts +2 -1
- package/dist/src/terminal-monitor-state-reconciliation-service.js +12 -0
- package/dist/src/terminal-monitor-state-reconciliation-service.js.map +1 -1
- package/dist/src/terminal-monitor-supervision-cli-adapter.d.ts +2 -0
- package/dist/src/terminal-monitor-supervision-cli-adapter.js +31 -11
- package/dist/src/terminal-monitor-supervision-cli-adapter.js.map +1 -1
- package/dist/src/terminal-status-cli-adapter.d.ts +1 -0
- package/dist/src/terminal-status-cli-adapter.js +30 -28
- package/dist/src/terminal-status-cli-adapter.js.map +1 -1
- package/dist/src/terminal-submission-acceptance.js +78 -25
- package/dist/src/terminal-submission-acceptance.js.map +1 -1
- package/dist/src/terminal-submission-retry-service.d.ts +114 -0
- package/dist/src/terminal-submission-retry-service.js +589 -0
- package/dist/src/terminal-submission-retry-service.js.map +1 -0
- package/dist/src/terminal-watch-callback-cli-adapter.d.ts +10 -2
- package/dist/src/terminal-watch-callback-cli-adapter.js +76 -37
- package/dist/src/terminal-watch-callback-cli-adapter.js.map +1 -1
- package/dist/src/terminal-watch-cli-adapter.js +41 -17
- package/dist/src/terminal-watch-cli-adapter.js.map +1 -1
- package/dist/src/terminal-watch-service.d.ts +11 -6
- package/dist/src/terminal-watch-service.js +210 -41
- package/dist/src/terminal-watch-service.js.map +1 -1
- package/dist/src/terminal-watch-store.d.ts +23 -0
- package/dist/src/terminal-watch-store.js +162 -4
- package/dist/src/terminal-watch-store.js.map +1 -1
- package/docs/quickstart-tmux.md +1 -1
- package/package.json +1 -1
- package/templates/openclaw-skills/agent-knock-knock/SKILL.md +8 -5
|
@@ -2,17 +2,23 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import { formatTerminalConversationId, parseTerminalConversationId, terminalControlCapabilitiesForAdapter } from "./terminal-agent-adapter.js";
|
|
3
3
|
import { enrichActiveProcessesWithTerminalControl, TerminalControlInputNotSentError } from "./terminal-control-provider.js";
|
|
4
4
|
import { hasCanonicalTerminalEndpoint, sameTerminalControlIncarnation, terminalEndpointFromControlRef, terminalEndpointIdentityKey } from "./terminal-control-ref.js";
|
|
5
|
-
// Verified Codex profiles through 0.
|
|
5
|
+
// Verified Codex profiles through 0.149.1 keep Enter in paste/newline mode for
|
|
6
6
|
// 120ms after burst input. Cross that boundary rather than landing on it, and
|
|
7
7
|
// also require observable composer stability instead of treating this delay
|
|
8
8
|
// alone as acceptance.
|
|
9
9
|
const CODEX_PASTE_ENTER_SETTLE_MS = 121;
|
|
10
10
|
const CODEX_MULTILINE_SETTLE_POLL_MS = 30;
|
|
11
|
-
|
|
11
|
+
// A capture can itself take longer than the old two-second deadline on a busy
|
|
12
|
+
// tmux server. Give repaint discovery a bounded five-second window and, once
|
|
13
|
+
// an exact frame has been observed, always allow the required stable/final
|
|
14
|
+
// captures to complete instead of letting the first I/O consume their budget.
|
|
15
|
+
const CODEX_MULTILINE_SETTLE_TIMEOUT_MS = 5_000;
|
|
12
16
|
const CODEX_MULTILINE_STABLE_CAPTURES = 2;
|
|
17
|
+
const CODEX_EXACT_CANDIDATE_GRACE_CAPTURES = 8;
|
|
13
18
|
const CODEX_MULTILINE_SETTLE_SCROLLBACK_LINES = 240;
|
|
14
19
|
const CODEX_COMPOSER_MARKER = /^[›»](?:\s|$)/u;
|
|
15
20
|
const CODEX_COMPOSER_FOOTER = /^(?:gpt-[\w.-]+(?:\s|$)|[-\w.]+ default ·)/u;
|
|
21
|
+
const CODEX_COMPLETE_COMPOSER_FOOTER = /^(?:gpt-[\w.-]+(?:\s+\S+)?|[-\w.]+ default)\s+·\s+\S.*$/u;
|
|
16
22
|
// Keep every exact slash-completion shape closed per behavior profile so a
|
|
17
23
|
// version adding another matching command cannot silently become an authorized
|
|
18
24
|
// native command surface.
|
|
@@ -30,6 +36,10 @@ const CODEX_NATIVE_STATUS_POPUP_BY_PROFILE = {
|
|
|
30
36
|
"codex-tui-0.148.0": [
|
|
31
37
|
" /status show current session configuration and token usage",
|
|
32
38
|
" /statusline configure which items appear in the status line"
|
|
39
|
+
],
|
|
40
|
+
"codex-tui-0.149.1": [
|
|
41
|
+
" /status show current session configuration and token usage",
|
|
42
|
+
" /statusline configure which items appear in the status line"
|
|
33
43
|
]
|
|
34
44
|
};
|
|
35
45
|
// Codex's verified `/status` profiles are exercised against its canonical
|
|
@@ -41,7 +51,8 @@ const CODEX_NATIVE_STATUS_MIN_VIEWPORT_BY_PROFILE = {
|
|
|
41
51
|
"codex-tui-0.146.0": 80,
|
|
42
52
|
"codex-tui-0.146.1": 80,
|
|
43
53
|
"codex-tui-0.147.0": 80,
|
|
44
|
-
"codex-tui-0.148.0": 80
|
|
54
|
+
"codex-tui-0.148.0": 80,
|
|
55
|
+
"codex-tui-0.149.1": 80
|
|
45
56
|
};
|
|
46
57
|
const CLAUDE_NATIVE_STATUS_POPUP_BY_PROFILE = {
|
|
47
58
|
"claude-code-2.1.218-native-status": [
|
|
@@ -86,6 +97,35 @@ export class TerminalInputNotStartedError extends Error {
|
|
|
86
97
|
this.name = "TerminalInputNotStartedError";
|
|
87
98
|
}
|
|
88
99
|
}
|
|
100
|
+
/**
|
|
101
|
+
* Text is already in the terminal composer, but this bridge invocation proved
|
|
102
|
+
* that it never called the Enter transport boundary.
|
|
103
|
+
*
|
|
104
|
+
* Callers may persist this as narrow recovery evidence. It says nothing about
|
|
105
|
+
* whether the draft remains exact or whether a later human action submitted
|
|
106
|
+
* it; those facts must be re-observed under the terminal lock.
|
|
107
|
+
*/
|
|
108
|
+
export class TerminalEnterDispatchNotAttemptedError extends Error {
|
|
109
|
+
code = "AKK_TERMINAL_ENTER_DISPATCH_NOT_ATTEMPTED";
|
|
110
|
+
stage = "enter_not_attempted";
|
|
111
|
+
constructor(message, options = {}) {
|
|
112
|
+
super(message, options);
|
|
113
|
+
this.name = "TerminalEnterDispatchNotAttemptedError";
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* The caller's one-shot Enter reservation was invoked, so this recovery
|
|
118
|
+
* attempt is permanently consumed even though C-m was not proven dispatched.
|
|
119
|
+
*/
|
|
120
|
+
export class TerminalEnterDispatchReservedError extends Error {
|
|
121
|
+
code = "AKK_TERMINAL_ENTER_DISPATCH_RESERVED";
|
|
122
|
+
stage = "enter_reserved";
|
|
123
|
+
doNotRetry = true;
|
|
124
|
+
constructor(message, options = {}) {
|
|
125
|
+
super(message, options);
|
|
126
|
+
this.name = "TerminalEnterDispatchReservedError";
|
|
127
|
+
}
|
|
128
|
+
}
|
|
89
129
|
/**
|
|
90
130
|
* A fail-closed native-inspection submission result.
|
|
91
131
|
*
|
|
@@ -278,15 +318,23 @@ export class TerminalAgentBridge {
|
|
|
278
318
|
async send(agent, terminalControl, text, options = {}) {
|
|
279
319
|
const adapter = this.registry.require(agent);
|
|
280
320
|
const multiline = /[\r\n]/u.test(text.trimEnd());
|
|
321
|
+
const exactEmptyRetryAuthority = options.requireExactEmptyComposerAfterBeforeText;
|
|
281
322
|
const requireExactComposer = (adapter.agent === "codex" && multiline) ||
|
|
282
|
-
options.requireExactComposerBeforeEnter === true
|
|
323
|
+
options.requireExactComposerBeforeEnter === true ||
|
|
324
|
+
exactEmptyRetryAuthority !== undefined;
|
|
325
|
+
if (exactEmptyRetryAuthority !== undefined &&
|
|
326
|
+
(adapter.agent !== "codex" ||
|
|
327
|
+
options.beforeText === undefined ||
|
|
328
|
+
!/^[0-9a-f]{64}$/u.test(exactEmptyRetryAuthority.preliminaryComposerDigest))) {
|
|
329
|
+
throw new TerminalInputNotStartedError("exact-empty replacement requires Codex, a reservation hook, and a preliminary composer digest");
|
|
330
|
+
}
|
|
283
331
|
try {
|
|
284
332
|
assertTerminalMutationCapabilities({
|
|
285
333
|
provider: this.terminalProvider,
|
|
286
334
|
terminal: this.terminalProvider.endpoint(terminalControl),
|
|
287
335
|
semantic: [
|
|
288
336
|
"send_keys",
|
|
289
|
-
...(requireExactComposer
|
|
337
|
+
...(requireExactComposer || exactEmptyRetryAuthority
|
|
290
338
|
? ["screen_status"]
|
|
291
339
|
: [])
|
|
292
340
|
],
|
|
@@ -294,7 +342,7 @@ export class TerminalAgentBridge {
|
|
|
294
342
|
"stable_resource_resolution",
|
|
295
343
|
"text_delivery",
|
|
296
344
|
"key_delivery",
|
|
297
|
-
...(requireExactComposer
|
|
345
|
+
...(requireExactComposer || exactEmptyRetryAuthority
|
|
298
346
|
? ["screen_capture"]
|
|
299
347
|
: [])
|
|
300
348
|
]
|
|
@@ -317,21 +365,53 @@ export class TerminalAgentBridge {
|
|
|
317
365
|
if (!verifiedForText.capabilities.includes("send_keys")) {
|
|
318
366
|
throw new TerminalInputNotStartedError(`${adapter.displayName} terminal input capability changed before injection`);
|
|
319
367
|
}
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
368
|
+
if (exactEmptyRetryAuthority) {
|
|
369
|
+
try {
|
|
370
|
+
// Invoking this retry-only hook consumes the durable attempt. The
|
|
371
|
+
// independently identity-fenced recapture closes the human-edit race
|
|
372
|
+
// before any physical text delivery.
|
|
373
|
+
await options.beforeText?.({
|
|
374
|
+
agent: adapter.agent,
|
|
375
|
+
terminalControl: verifiedForText,
|
|
376
|
+
multiline,
|
|
377
|
+
text: normalized
|
|
378
|
+
});
|
|
379
|
+
const finalEmpty = await this.captureCodexComposerSnapshot(adapter, verifiedForText, normalized, options.runtime);
|
|
380
|
+
if (finalEmpty.state !== "exact_empty" ||
|
|
381
|
+
finalEmpty.digest !==
|
|
382
|
+
exactEmptyRetryAuthority.preliminaryComposerDigest ||
|
|
383
|
+
!sameTerminalControlIdentity(verifiedForText, finalEmpty.terminalControl)) {
|
|
384
|
+
const reason = "reason" in finalEmpty
|
|
385
|
+
? finalEmpty.reason
|
|
386
|
+
: `Codex composer is ${finalEmpty.state}`;
|
|
387
|
+
throw new Error(`reserved Codex replacement lost its exact-empty composer: ${reason}`);
|
|
388
|
+
}
|
|
389
|
+
verifiedForText = finalEmpty.terminalControl;
|
|
390
|
+
}
|
|
391
|
+
catch (error) {
|
|
392
|
+
throw new TerminalEnterDispatchReservedError(error instanceof Error ? error.message : String(error), { cause: error });
|
|
393
|
+
}
|
|
327
394
|
}
|
|
328
|
-
|
|
329
|
-
|
|
395
|
+
else {
|
|
396
|
+
try {
|
|
397
|
+
await options.beforeText?.({
|
|
398
|
+
agent: adapter.agent,
|
|
399
|
+
terminalControl: verifiedForText,
|
|
400
|
+
multiline,
|
|
401
|
+
text: normalized
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
catch (error) {
|
|
405
|
+
throw new TerminalInputNotStartedError(error instanceof Error ? error.message : String(error), { cause: error });
|
|
406
|
+
}
|
|
330
407
|
}
|
|
331
408
|
try {
|
|
332
409
|
await this.terminalProvider.sendText(this.terminalProvider.endpoint(verifiedForText), normalized);
|
|
333
410
|
}
|
|
334
411
|
catch (error) {
|
|
412
|
+
if (exactEmptyRetryAuthority) {
|
|
413
|
+
throw new TerminalEnterDispatchReservedError("reserved Codex replacement text delivery outcome is uncertain; do not retry", { cause: error });
|
|
414
|
+
}
|
|
335
415
|
if (error instanceof TerminalControlInputNotSentError) {
|
|
336
416
|
throw new TerminalInputNotStartedError(error.message, {
|
|
337
417
|
cause: error
|
|
@@ -339,61 +419,187 @@ export class TerminalAgentBridge {
|
|
|
339
419
|
}
|
|
340
420
|
throw error;
|
|
341
421
|
}
|
|
342
|
-
await options.onTransportStage?.({
|
|
343
|
-
stage: "text_injected",
|
|
344
|
-
agent: adapter.agent,
|
|
345
|
-
terminalControl: verifiedForText,
|
|
346
|
-
multiline
|
|
347
|
-
});
|
|
348
|
-
// Text and Enter are separate tmux operations. Revalidate between them and
|
|
349
|
-
// never submit after identity or composer drift. A failed revalidation
|
|
350
|
-
// leaves the injected draft untouched because cleanup is also terminal
|
|
351
|
-
// input and cannot safely use a stale route.
|
|
352
422
|
let verifiedForEnter;
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
423
|
+
try {
|
|
424
|
+
await options.onTransportStage?.({
|
|
425
|
+
stage: "text_injected",
|
|
426
|
+
agent: adapter.agent,
|
|
427
|
+
terminalControl: verifiedForText,
|
|
428
|
+
multiline
|
|
429
|
+
});
|
|
430
|
+
// Text and Enter are separate tmux operations. Revalidate between them
|
|
431
|
+
// and never submit after identity or composer drift. Every failure in
|
|
432
|
+
// this region is typed proof that this invocation did not call C-m.
|
|
433
|
+
if (adapter.agent === "codex" && requireExactComposer) {
|
|
434
|
+
verifiedForEnter = await this.settleCodexMultilineComposer(adapter, terminalControl, normalized, options.runtime, {
|
|
435
|
+
allowOpaqueLargePastePlaceholder: exactEmptyRetryAuthority === undefined
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
else if (requireExactComposer) {
|
|
439
|
+
verifiedForEnter = await this.verifyExactComposerBeforeEnter(adapter, terminalControl, normalized, options.runtime);
|
|
440
|
+
}
|
|
441
|
+
else {
|
|
361
442
|
verifiedForEnter = await this.verifyTerminalIdentity(adapter.agent, terminalControl, options.runtime);
|
|
362
443
|
}
|
|
363
|
-
|
|
444
|
+
await options.beforeEnter?.({
|
|
445
|
+
agent: adapter.agent,
|
|
446
|
+
terminalControl: verifiedForEnter,
|
|
447
|
+
multiline,
|
|
448
|
+
text: normalized
|
|
449
|
+
});
|
|
450
|
+
// `beforeEnter` may await Store and terminal identity fences. The human
|
|
451
|
+
// can still edit the composer or change the native thread while that
|
|
452
|
+
// callback is pending, so recapture before attempting Enter.
|
|
453
|
+
if (requireExactComposer) {
|
|
454
|
+
verifiedForEnter = adapter.agent === "codex"
|
|
455
|
+
? await this.settleCodexMultilineComposer(adapter, terminalControl, normalized, options.runtime, {
|
|
456
|
+
allowOpaqueLargePastePlaceholder: exactEmptyRetryAuthority === undefined
|
|
457
|
+
})
|
|
458
|
+
: await this.verifyExactComposerBeforeEnter(adapter, terminalControl, normalized, options.runtime);
|
|
459
|
+
}
|
|
460
|
+
else if (options.beforeEnter) {
|
|
461
|
+
verifiedForEnter = await this.verifyTerminalIdentity(adapter.agent, terminalControl, options.runtime);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
catch (error) {
|
|
465
|
+
if (exactEmptyRetryAuthority) {
|
|
466
|
+
if (error instanceof TerminalEnterDispatchReservedError) {
|
|
467
|
+
throw error;
|
|
468
|
+
}
|
|
469
|
+
throw new TerminalEnterDispatchReservedError(error instanceof Error ? error.message : String(error), { cause: error });
|
|
470
|
+
}
|
|
471
|
+
if (error instanceof TerminalEnterDispatchNotAttemptedError) {
|
|
364
472
|
throw error;
|
|
365
473
|
}
|
|
474
|
+
throw new TerminalEnterDispatchNotAttemptedError(error instanceof Error ? error.message : String(error), { cause: error });
|
|
366
475
|
}
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
await this.terminalProvider.sendKeys(this.terminalProvider.endpoint(verifiedForEnter), ["C-m"]);
|
|
386
|
-
await options.onTransportStage?.({
|
|
476
|
+
// Do not include this transport call (or anything after it) in the typed
|
|
477
|
+
// pre-Enter proof: a thrown key-delivery error may still have emitted C-m.
|
|
478
|
+
try {
|
|
479
|
+
await this.terminalProvider.sendKeys(this.terminalProvider.endpoint(verifiedForEnter), ["C-m"]);
|
|
480
|
+
await options.onTransportStage?.({
|
|
481
|
+
stage: "enter_dispatched",
|
|
482
|
+
agent: adapter.agent,
|
|
483
|
+
terminalControl: verifiedForEnter,
|
|
484
|
+
multiline
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
catch (error) {
|
|
488
|
+
if (exactEmptyRetryAuthority) {
|
|
489
|
+
throw new TerminalEnterDispatchReservedError("reserved Codex Enter dispatch outcome is uncertain; do not retry", { cause: error });
|
|
490
|
+
}
|
|
491
|
+
throw error;
|
|
492
|
+
}
|
|
493
|
+
return {
|
|
387
494
|
stage: "enter_dispatched",
|
|
388
495
|
agent: adapter.agent,
|
|
389
496
|
terminalControl: verifiedForEnter,
|
|
390
497
|
multiline
|
|
391
|
-
}
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
/** Observe only the current, live Codex composer without exposing its text. */
|
|
501
|
+
async observeCodexComposer(terminalControl, expectedText, options = {}) {
|
|
502
|
+
const adapter = this.registry.require("codex");
|
|
503
|
+
const normalized = expectedText.trimEnd();
|
|
504
|
+
if (!normalized) {
|
|
505
|
+
return {
|
|
506
|
+
state: "unavailable",
|
|
507
|
+
reason: "expected Codex draft is empty"
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
try {
|
|
511
|
+
assertTerminalMutationCapabilities({
|
|
512
|
+
provider: this.terminalProvider,
|
|
513
|
+
terminal: this.terminalProvider.endpoint(terminalControl),
|
|
514
|
+
semantic: ["screen_status"],
|
|
515
|
+
transport: ["stable_resource_resolution", "screen_capture"]
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
catch (error) {
|
|
519
|
+
return {
|
|
520
|
+
state: "unavailable",
|
|
521
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
return this.settleCodexComposerObservation(adapter, terminalControl, normalized, options.runtime, { minimumStableMs: CODEX_MULTILINE_SETTLE_POLL_MS });
|
|
525
|
+
}
|
|
526
|
+
/**
|
|
527
|
+
* Submit one already-present exact Codex draft. This primitive never writes
|
|
528
|
+
* text, clears the composer, or emits C-c, and contains exactly one C-m call.
|
|
529
|
+
*/
|
|
530
|
+
async submitExactCodexDraft(terminalControl, expectedText, options) {
|
|
531
|
+
const adapter = this.registry.require("codex");
|
|
532
|
+
const normalized = expectedText.trimEnd();
|
|
533
|
+
if (!normalized) {
|
|
534
|
+
throw new TerminalEnterDispatchNotAttemptedError("expected Codex draft is empty");
|
|
535
|
+
}
|
|
536
|
+
let preliminaryObservation;
|
|
537
|
+
try {
|
|
538
|
+
assertTerminalMutationCapabilities({
|
|
539
|
+
provider: this.terminalProvider,
|
|
540
|
+
terminal: this.terminalProvider.endpoint(terminalControl),
|
|
541
|
+
semantic: ["send_keys", "screen_status"],
|
|
542
|
+
transport: [
|
|
543
|
+
"stable_resource_resolution",
|
|
544
|
+
"screen_capture",
|
|
545
|
+
"key_delivery"
|
|
546
|
+
]
|
|
547
|
+
});
|
|
548
|
+
const observation = await this.settleCodexComposerObservation(adapter, terminalControl, normalized, options.runtime, {
|
|
549
|
+
minimumStableMs: CODEX_PASTE_ENTER_SETTLE_MS,
|
|
550
|
+
requiredState: "exact_draft"
|
|
551
|
+
});
|
|
552
|
+
if (observation.state !== "exact_draft") {
|
|
553
|
+
const reason = "reason" in observation
|
|
554
|
+
? observation.reason
|
|
555
|
+
: `Codex composer is ${observation.state}`;
|
|
556
|
+
throw new Error(`refusing to submit an unproven exact Codex draft: ${reason}`);
|
|
557
|
+
}
|
|
558
|
+
preliminaryObservation = observation;
|
|
559
|
+
}
|
|
560
|
+
catch (error) {
|
|
561
|
+
if (error instanceof TerminalEnterDispatchNotAttemptedError) {
|
|
562
|
+
throw error;
|
|
563
|
+
}
|
|
564
|
+
throw new TerminalEnterDispatchNotAttemptedError(error instanceof Error ? error.message : String(error), { cause: error });
|
|
565
|
+
}
|
|
566
|
+
let finalObservation;
|
|
567
|
+
try {
|
|
568
|
+
// Calling this hook is itself the one-shot boundary. A throw may occur
|
|
569
|
+
// after durable storage committed, so every failure from here onward
|
|
570
|
+
// permanently consumes the attempt.
|
|
571
|
+
await options.beforeEnterReservation({
|
|
572
|
+
terminalControl: preliminaryObservation.terminalControl,
|
|
573
|
+
text: normalized,
|
|
574
|
+
composerDigest: preliminaryObservation.digest
|
|
575
|
+
});
|
|
576
|
+
const recaptured = await this.captureCodexComposerSnapshot(adapter, preliminaryObservation.terminalControl, normalized, options.runtime);
|
|
577
|
+
if (recaptured.state !== "exact_draft" ||
|
|
578
|
+
recaptured.digest !== preliminaryObservation.digest ||
|
|
579
|
+
!sameTerminalControlIdentity(preliminaryObservation.terminalControl, recaptured.terminalControl)) {
|
|
580
|
+
const reason = "reason" in recaptured
|
|
581
|
+
? recaptured.reason
|
|
582
|
+
: `Codex composer is ${recaptured.state}`;
|
|
583
|
+
throw new Error(`reserved Codex draft changed before Enter: ${reason}`);
|
|
584
|
+
}
|
|
585
|
+
finalObservation = recaptured;
|
|
586
|
+
}
|
|
587
|
+
catch (error) {
|
|
588
|
+
if (error instanceof TerminalEnterDispatchReservedError) {
|
|
589
|
+
throw error;
|
|
590
|
+
}
|
|
591
|
+
throw new TerminalEnterDispatchReservedError(error instanceof Error ? error.message : String(error), { cause: error });
|
|
592
|
+
}
|
|
593
|
+
try {
|
|
594
|
+
await this.terminalProvider.sendKeys(this.terminalProvider.endpoint(finalObservation.terminalControl), ["C-m"]);
|
|
595
|
+
}
|
|
596
|
+
catch (error) {
|
|
597
|
+
throw new TerminalEnterDispatchReservedError("reserved Codex Enter dispatch outcome is uncertain; do not retry", { cause: error });
|
|
598
|
+
}
|
|
392
599
|
return {
|
|
393
600
|
stage: "enter_dispatched",
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
multiline
|
|
601
|
+
terminalControl: finalObservation.terminalControl,
|
|
602
|
+
enterCount: 1
|
|
397
603
|
};
|
|
398
604
|
}
|
|
399
605
|
async verifyExactComposerBeforeEnter(adapter, terminalControl, expectedText, runtime) {
|
|
@@ -876,70 +1082,218 @@ export class TerminalAgentBridge {
|
|
|
876
1082
|
materialization: expected
|
|
877
1083
|
};
|
|
878
1084
|
}
|
|
879
|
-
async settleCodexMultilineComposer(adapter, terminalControl, expectedText, runtime) {
|
|
1085
|
+
async settleCodexMultilineComposer(adapter, terminalControl, expectedText, runtime, options = {}) {
|
|
1086
|
+
const observation = await this.settleCodexComposerObservation(adapter, terminalControl, expectedText, runtime, {
|
|
1087
|
+
minimumStableMs: CODEX_PASTE_ENTER_SETTLE_MS,
|
|
1088
|
+
requiredState: "exact_draft",
|
|
1089
|
+
allowOpaqueLargePastePlaceholder: options.allowOpaqueLargePastePlaceholder === true
|
|
1090
|
+
});
|
|
1091
|
+
if (observation.state === "exact_draft") {
|
|
1092
|
+
return observation.terminalControl;
|
|
1093
|
+
}
|
|
1094
|
+
if (observation.state === "working" ||
|
|
1095
|
+
observation.state === "approval_or_modal") {
|
|
1096
|
+
throw new Error("Codex became busy or blocked while its multiline composer was settling");
|
|
1097
|
+
}
|
|
1098
|
+
if (observation.state === "identity_drift") {
|
|
1099
|
+
throw new Error(observation.reason);
|
|
1100
|
+
}
|
|
1101
|
+
throw new Error("Codex multiline composer did not become exact, idle, and stable before the bounded submit deadline");
|
|
1102
|
+
}
|
|
1103
|
+
async settleCodexComposerObservation(adapter, terminalControl, expectedText, runtime, options) {
|
|
880
1104
|
const startedAt = this.nowMs();
|
|
881
|
-
let
|
|
1105
|
+
let candidate;
|
|
882
1106
|
let stableSince;
|
|
883
1107
|
let stableCaptures = 0;
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
1108
|
+
let finishObservedExactCandidate = false;
|
|
1109
|
+
let capturesBeyondDeadline = 0;
|
|
1110
|
+
let control = terminalControl;
|
|
1111
|
+
while (true) {
|
|
1112
|
+
const snapshot = await this.captureCodexComposerSnapshot(adapter, control, expectedText, runtime, {
|
|
1113
|
+
allowOpaqueLargePastePlaceholder: options.allowOpaqueLargePastePlaceholder === true
|
|
888
1114
|
});
|
|
889
|
-
if (
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
stableCaptures += 1;
|
|
1115
|
+
if (snapshot.state === "identity_drift" ||
|
|
1116
|
+
(snapshot.state === "unavailable" && snapshot.retryable !== true)) {
|
|
1117
|
+
return snapshot;
|
|
1118
|
+
}
|
|
1119
|
+
if (this.nowMs() - startedAt > CODEX_MULTILINE_SETTLE_TIMEOUT_MS) {
|
|
1120
|
+
capturesBeyondDeadline += 1;
|
|
1121
|
+
if (capturesBeyondDeadline > CODEX_EXACT_CANDIDATE_GRACE_CAPTURES) {
|
|
1122
|
+
return {
|
|
1123
|
+
state: "unavailable",
|
|
1124
|
+
reason: "Codex exact composer did not stabilize within its bounded capture grace"
|
|
1125
|
+
};
|
|
901
1126
|
}
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
1127
|
+
}
|
|
1128
|
+
control = snapshot.terminalControl;
|
|
1129
|
+
const sameCandidate = candidate !== undefined &&
|
|
1130
|
+
candidate.state === snapshot.state &&
|
|
1131
|
+
candidate.digest === snapshot.digest &&
|
|
1132
|
+
sameTerminalControlIdentity(candidate.terminalControl, snapshot.terminalControl);
|
|
1133
|
+
if (sameCandidate) {
|
|
1134
|
+
stableCaptures += 1;
|
|
1135
|
+
}
|
|
1136
|
+
else {
|
|
1137
|
+
candidate = snapshot;
|
|
1138
|
+
stableSince = this.nowMs();
|
|
1139
|
+
stableCaptures = 1;
|
|
1140
|
+
finishObservedExactCandidate = snapshot.state === "exact_draft";
|
|
1141
|
+
}
|
|
1142
|
+
const stableForMs = stableSince === undefined
|
|
1143
|
+
? 0
|
|
1144
|
+
: this.nowMs() - stableSince;
|
|
1145
|
+
if (stableCaptures >= CODEX_MULTILINE_STABLE_CAPTURES &&
|
|
1146
|
+
stableForMs >= options.minimumStableMs &&
|
|
1147
|
+
snapshot.state !== "unavailable" &&
|
|
1148
|
+
(options.requiredState === undefined ||
|
|
1149
|
+
snapshot.state === options.requiredState ||
|
|
1150
|
+
snapshot.state === "working" ||
|
|
1151
|
+
snapshot.state === "approval_or_modal")) {
|
|
1152
|
+
// The caller's authorization hook must see evidence from a final,
|
|
1153
|
+
// independently identity-fenced recapture, not the polling capture.
|
|
1154
|
+
const finalSnapshot = await this.captureCodexComposerSnapshot(adapter, snapshot.terminalControl, expectedText, runtime, {
|
|
1155
|
+
allowOpaqueLargePastePlaceholder: options.allowOpaqueLargePastePlaceholder === true
|
|
1156
|
+
});
|
|
1157
|
+
if (finalSnapshot.state === snapshot.state &&
|
|
1158
|
+
"digest" in finalSnapshot &&
|
|
1159
|
+
finalSnapshot.digest === snapshot.digest &&
|
|
1160
|
+
sameTerminalControlIdentity(snapshot.terminalControl, finalSnapshot.terminalControl)) {
|
|
1161
|
+
return {
|
|
1162
|
+
...finalSnapshot,
|
|
1163
|
+
stableCaptures: stableCaptures + 1
|
|
1164
|
+
};
|
|
906
1165
|
}
|
|
907
|
-
if (
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
runtime,
|
|
912
|
-
scrollbackLines: CODEX_MULTILINE_SETTLE_SCROLLBACK_LINES
|
|
913
|
-
});
|
|
914
|
-
const finalComposer = exactCodexComposerCapture(finalCapture.screen, expectedText);
|
|
915
|
-
const finalIsIdle = finalCapture.inspection.activity.state === "idle" ||
|
|
916
|
-
(finalCapture.inspection.activity.state === "unknown" &&
|
|
917
|
-
finalComposer !== undefined);
|
|
918
|
-
if (finalCapture.inspection.approval.blocked ||
|
|
919
|
-
!finalIsIdle ||
|
|
920
|
-
!finalComposer ||
|
|
921
|
-
finalComposer.digest !== stableDigest) {
|
|
922
|
-
throw new Error("Codex multiline composer changed after its stable pre-submit capture");
|
|
923
|
-
}
|
|
924
|
-
const verifiedImmediatelyBeforeEnter = await this.verifyTerminalIdentity(adapter.agent, finalCapture.terminalControl, runtime);
|
|
925
|
-
if (!sameTerminalControlIdentity(finalCapture.terminalControl, verifiedImmediatelyBeforeEnter)) {
|
|
926
|
-
throw new Error("terminal control identity changed after the final Codex composer capture");
|
|
927
|
-
}
|
|
928
|
-
return verifiedImmediatelyBeforeEnter;
|
|
1166
|
+
if (finalSnapshot.state === "identity_drift" ||
|
|
1167
|
+
(finalSnapshot.state === "unavailable" &&
|
|
1168
|
+
finalSnapshot.retryable !== true)) {
|
|
1169
|
+
return finalSnapshot;
|
|
929
1170
|
}
|
|
1171
|
+
if (options.requiredState !== undefined &&
|
|
1172
|
+
snapshot.state === options.requiredState) {
|
|
1173
|
+
return {
|
|
1174
|
+
state: "unavailable",
|
|
1175
|
+
reason: "Codex composer changed after its stable final recapture"
|
|
1176
|
+
};
|
|
1177
|
+
}
|
|
1178
|
+
candidate = finalSnapshot;
|
|
1179
|
+
control = finalSnapshot.terminalControl;
|
|
1180
|
+
stableSince = this.nowMs();
|
|
1181
|
+
stableCaptures = 1;
|
|
1182
|
+
finishObservedExactCandidate = finalSnapshot.state === "exact_draft";
|
|
930
1183
|
}
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
1184
|
+
const elapsed = this.nowMs() - startedAt;
|
|
1185
|
+
if (elapsed > CODEX_MULTILINE_SETTLE_TIMEOUT_MS &&
|
|
1186
|
+
!finishObservedExactCandidate) {
|
|
1187
|
+
return {
|
|
1188
|
+
state: "unavailable",
|
|
1189
|
+
reason: options.requiredState
|
|
1190
|
+
? `Codex composer did not become ${options.requiredState} before the bounded observation deadline`
|
|
1191
|
+
: "Codex composer did not become stable before the bounded observation deadline"
|
|
1192
|
+
};
|
|
935
1193
|
}
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
1194
|
+
if (elapsed > CODEX_MULTILINE_SETTLE_TIMEOUT_MS &&
|
|
1195
|
+
options.requiredState !== undefined &&
|
|
1196
|
+
snapshot.state !== options.requiredState) {
|
|
1197
|
+
if (snapshot.state !== "unavailable") {
|
|
1198
|
+
return {
|
|
1199
|
+
...snapshot,
|
|
1200
|
+
stableCaptures
|
|
1201
|
+
};
|
|
1202
|
+
}
|
|
1203
|
+
return {
|
|
1204
|
+
state: "unavailable",
|
|
1205
|
+
reason: snapshot.reason
|
|
1206
|
+
};
|
|
1207
|
+
}
|
|
1208
|
+
const remainingStableMs = options.minimumStableMs - stableForMs;
|
|
1209
|
+
await this.sleep(Math.max(1, Math.min(CODEX_MULTILINE_SETTLE_POLL_MS, remainingStableMs > 0
|
|
1210
|
+
? remainingStableMs
|
|
940
1211
|
: CODEX_MULTILINE_SETTLE_POLL_MS)));
|
|
941
1212
|
}
|
|
942
|
-
|
|
1213
|
+
}
|
|
1214
|
+
async captureCodexComposerSnapshot(adapter, terminalControl, expectedText, runtime, options = {}) {
|
|
1215
|
+
let verifiedBefore;
|
|
1216
|
+
try {
|
|
1217
|
+
verifiedBefore = await this.verifyTerminalIdentity(adapter.agent, terminalControl, runtime);
|
|
1218
|
+
}
|
|
1219
|
+
catch (error) {
|
|
1220
|
+
return {
|
|
1221
|
+
state: "identity_drift",
|
|
1222
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
1223
|
+
};
|
|
1224
|
+
}
|
|
1225
|
+
let styledScreen;
|
|
1226
|
+
try {
|
|
1227
|
+
styledScreen = await this.terminalProvider.capture(this.terminalProvider.endpoint(verifiedBefore), {
|
|
1228
|
+
scrollbackLines: CODEX_MULTILINE_SETTLE_SCROLLBACK_LINES,
|
|
1229
|
+
preserveEscapes: true
|
|
1230
|
+
});
|
|
1231
|
+
}
|
|
1232
|
+
catch (error) {
|
|
1233
|
+
return {
|
|
1234
|
+
state: "unavailable",
|
|
1235
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
1236
|
+
};
|
|
1237
|
+
}
|
|
1238
|
+
const screen = stripTerminalEscapeSequences(styledScreen);
|
|
1239
|
+
let inspection;
|
|
1240
|
+
try {
|
|
1241
|
+
inspection = adapter.inspectScreen({ screen, runtime });
|
|
1242
|
+
}
|
|
1243
|
+
catch (error) {
|
|
1244
|
+
return {
|
|
1245
|
+
state: "unavailable",
|
|
1246
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
1247
|
+
};
|
|
1248
|
+
}
|
|
1249
|
+
let verifiedAfter;
|
|
1250
|
+
try {
|
|
1251
|
+
verifiedAfter = await this.verifyTerminalIdentity(adapter.agent, verifiedBefore, runtime);
|
|
1252
|
+
}
|
|
1253
|
+
catch (error) {
|
|
1254
|
+
return {
|
|
1255
|
+
state: "identity_drift",
|
|
1256
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
1257
|
+
};
|
|
1258
|
+
}
|
|
1259
|
+
if (!sameTerminalControlIdentity(verifiedBefore, verifiedAfter)) {
|
|
1260
|
+
return {
|
|
1261
|
+
state: "identity_drift",
|
|
1262
|
+
reason: "terminal control identity changed across the Codex composer capture"
|
|
1263
|
+
};
|
|
1264
|
+
}
|
|
1265
|
+
const screenDigest = createHash("sha256").update(styledScreen).digest("hex");
|
|
1266
|
+
if (inspection.approval.blocked ||
|
|
1267
|
+
inspection.activity.state === "awaiting_approval" ||
|
|
1268
|
+
codexBlockingModalVisible(screen)) {
|
|
1269
|
+
return {
|
|
1270
|
+
state: "approval_or_modal",
|
|
1271
|
+
terminalControl: verifiedAfter,
|
|
1272
|
+
digest: screenDigest
|
|
1273
|
+
};
|
|
1274
|
+
}
|
|
1275
|
+
if (inspection.activity.state === "working") {
|
|
1276
|
+
return {
|
|
1277
|
+
state: "working",
|
|
1278
|
+
terminalControl: verifiedAfter,
|
|
1279
|
+
digest: screenDigest
|
|
1280
|
+
};
|
|
1281
|
+
}
|
|
1282
|
+
const composer = currentCodexComposerCapture(styledScreen, expectedText, options.allowOpaqueLargePastePlaceholder === true);
|
|
1283
|
+
if (!composer) {
|
|
1284
|
+
return {
|
|
1285
|
+
state: "unavailable",
|
|
1286
|
+
reason: "the current live Codex composer could not be proven",
|
|
1287
|
+
retryable: true,
|
|
1288
|
+
terminalControl: verifiedAfter,
|
|
1289
|
+
digest: screenDigest
|
|
1290
|
+
};
|
|
1291
|
+
}
|
|
1292
|
+
return {
|
|
1293
|
+
state: composer.state,
|
|
1294
|
+
terminalControl: verifiedAfter,
|
|
1295
|
+
digest: composer.digest
|
|
1296
|
+
};
|
|
943
1297
|
}
|
|
944
1298
|
/**
|
|
945
1299
|
* Prove that the exact managed draft is still present after Enter dispatch.
|
|
@@ -1864,6 +2218,80 @@ function codexNativeInspectionComposerMismatchDiagnostic(screen, plan) {
|
|
|
1864
2218
|
? "composer_viewport_truncated"
|
|
1865
2219
|
: "composer_not_exact";
|
|
1866
2220
|
}
|
|
2221
|
+
function codexBlockingModalVisible(screen) {
|
|
2222
|
+
const tail = screen.replace(/\r\n?/gu, "\n").split("\n").slice(-80)
|
|
2223
|
+
.join("\n");
|
|
2224
|
+
return /\b(?:press|use)\s+(?:esc|escape)\s+to\s+(?:cancel|close|dismiss)\b/iu
|
|
2225
|
+
.test(tail) ||
|
|
2226
|
+
/\besc\s+to\s+cancel\b/iu.test(tail);
|
|
2227
|
+
}
|
|
2228
|
+
/**
|
|
2229
|
+
* Classify only the bottom live Codex composer region. A matching transcript
|
|
2230
|
+
* prompt elsewhere in scrollback is deliberately ignored.
|
|
2231
|
+
*/
|
|
2232
|
+
function currentCodexComposerCapture(styledScreen, expectedText, allowOpaqueLargePastePlaceholder = false) {
|
|
2233
|
+
const screen = stripTerminalEscapeSequences(styledScreen);
|
|
2234
|
+
const lines = screen.replace(/\r\n?/gu, "\n").split("\n");
|
|
2235
|
+
while (lines.length > 0 && lines.at(-1)?.trim().length === 0) {
|
|
2236
|
+
lines.pop();
|
|
2237
|
+
}
|
|
2238
|
+
let composerIndex = -1;
|
|
2239
|
+
for (let index = lines.length - 1; index >= 0; index -= 1) {
|
|
2240
|
+
if (CODEX_COMPOSER_MARKER.test(lines[index])) {
|
|
2241
|
+
composerIndex = index;
|
|
2242
|
+
break;
|
|
2243
|
+
}
|
|
2244
|
+
}
|
|
2245
|
+
if (composerIndex < 0) {
|
|
2246
|
+
return undefined;
|
|
2247
|
+
}
|
|
2248
|
+
const footerIndex = lines.findIndex((line, index) => index > composerIndex &&
|
|
2249
|
+
CODEX_COMPLETE_COMPOSER_FOOTER.test(line.trim()));
|
|
2250
|
+
if (footerIndex >= 0 &&
|
|
2251
|
+
lines.slice(footerIndex + 1).some((line) => line.trim().length > 0)) {
|
|
2252
|
+
return undefined;
|
|
2253
|
+
}
|
|
2254
|
+
const regionEnd = footerIndex < 0 ? lines.length : footerIndex;
|
|
2255
|
+
const region = lines.slice(composerIndex, regionEnd);
|
|
2256
|
+
while (region.length > 1 && region.at(-1)?.trim().length === 0) {
|
|
2257
|
+
region.pop();
|
|
2258
|
+
}
|
|
2259
|
+
if (region.length === 0 ||
|
|
2260
|
+
region.slice(1).some((line) => line.length > 0 && !line.startsWith(" "))) {
|
|
2261
|
+
return undefined;
|
|
2262
|
+
}
|
|
2263
|
+
const bodyRows = [
|
|
2264
|
+
region[0].replace(/^[›»]\s?/u, ""),
|
|
2265
|
+
...region.slice(1).map((line) => line.startsWith(" ") ? line.slice(2) : line)
|
|
2266
|
+
];
|
|
2267
|
+
const digest = createHash("sha256").update(region.join("\n")).digest("hex");
|
|
2268
|
+
const positivelyEmpty = footerIndex >= 0 &&
|
|
2269
|
+
bodyRows.slice(1).every((row) => row.trim().length === 0) &&
|
|
2270
|
+
exactCodexReadyStyledComposerCapture(styledScreen) !== undefined;
|
|
2271
|
+
if (positivelyEmpty) {
|
|
2272
|
+
return { state: "exact_empty", digest };
|
|
2273
|
+
}
|
|
2274
|
+
if (footerIndex < 0) {
|
|
2275
|
+
return undefined;
|
|
2276
|
+
}
|
|
2277
|
+
const expectedComparable = composerComparableText(expectedText);
|
|
2278
|
+
const expectedCharacterCount = Array.from(expectedText).length;
|
|
2279
|
+
const comparable = composerComparableText(bodyRows.join("\n"));
|
|
2280
|
+
const opaqueLargePastePlaceholder = /^\[Pasted Content \d+ chars\]$/u.test(comparable);
|
|
2281
|
+
if (opaqueLargePastePlaceholder && !allowOpaqueLargePastePlaceholder) {
|
|
2282
|
+
return undefined;
|
|
2283
|
+
}
|
|
2284
|
+
const exactVisibleDraft = terminalComposerRowsMatchExpected(bodyRows, expectedComparable);
|
|
2285
|
+
const exactLargePastePlaceholder = allowOpaqueLargePastePlaceholder &&
|
|
2286
|
+
expectedCharacterCount > CODEX_LARGE_PASTE_CHAR_THRESHOLD &&
|
|
2287
|
+
comparable === composerComparableText(`[Pasted Content ${expectedCharacterCount} chars]`);
|
|
2288
|
+
if (exactVisibleDraft || exactLargePastePlaceholder) {
|
|
2289
|
+
return { state: "exact_draft", digest };
|
|
2290
|
+
}
|
|
2291
|
+
return comparable.length > 0
|
|
2292
|
+
? { state: "different_draft", digest }
|
|
2293
|
+
: undefined;
|
|
2294
|
+
}
|
|
1867
2295
|
function exactCodexComposerCapture(screen, expectedText) {
|
|
1868
2296
|
const lines = screen.replace(/\r\n?/gu, "\n").split("\n");
|
|
1869
2297
|
const expectedComparable = composerComparableText(expectedText);
|