@scotthuang/agent-knock-knock 0.12.16 → 0.12.18
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 +30 -1
- package/README.md +11 -11
- package/dist/src/claude-lifecycle-compatibility.d.ts +9 -0
- package/dist/src/claude-lifecycle-compatibility.js +50 -5
- package/dist/src/claude-lifecycle-compatibility.js.map +1 -1
- package/dist/src/claude-local-transcript-provider.js +5 -6
- package/dist/src/claude-local-transcript-provider.js.map +1 -1
- package/dist/src/claude-terminal-agent-adapter.js +38 -30
- package/dist/src/claude-terminal-agent-adapter.js.map +1 -1
- package/dist/src/codex-lifecycle-compatibility.d.ts +19 -0
- package/dist/src/codex-lifecycle-compatibility.js +37 -0
- package/dist/src/codex-lifecycle-compatibility.js.map +1 -1
- package/dist/src/codex-store-adapter.js +5 -6
- package/dist/src/codex-store-adapter.js.map +1 -1
- package/dist/src/codex-terminal-agent-adapter.js +46 -28
- package/dist/src/codex-terminal-agent-adapter.js.map +1 -1
- package/dist/src/deferred-foreground-application-service.js +1 -0
- package/dist/src/deferred-foreground-application-service.js.map +1 -1
- package/dist/src/deferred-foreground-boundary.d.ts +2 -0
- package/dist/src/deferred-foreground-preparation-cli-adapter.js +2 -0
- package/dist/src/deferred-foreground-preparation-cli-adapter.js.map +1 -1
- package/dist/src/deferred-foreground-preparation-service.js +4 -2
- package/dist/src/deferred-foreground-preparation-service.js.map +1 -1
- package/dist/src/deferred-foreground-recovery-cli-adapter.js +1 -0
- package/dist/src/deferred-foreground-recovery-cli-adapter.js.map +1 -1
- package/dist/src/doctor-capabilities.d.ts +4 -3
- package/dist/src/doctor-capabilities.js +26 -6
- package/dist/src/doctor-capabilities.js.map +1 -1
- package/dist/src/install-doctor-command-adapter.js +1 -1
- package/dist/src/install-doctor-command-adapter.js.map +1 -1
- package/dist/src/native-thread-lifecycle-cli-adapter.js +17 -2
- package/dist/src/native-thread-lifecycle-cli-adapter.js.map +1 -1
- package/dist/src/native-thread-transition-application.js +9 -3
- package/dist/src/native-thread-transition-application.js.map +1 -1
- package/dist/src/openclaw-plugin-command-adapter.js +15 -11
- package/dist/src/openclaw-plugin-command-adapter.js.map +1 -1
- package/dist/src/openclaw-plugin-helpers.js +22 -1
- package/dist/src/openclaw-plugin-helpers.js.map +1 -1
- package/dist/src/openclaw-plugin-schemas.js +2 -2
- package/dist/src/openclaw-plugin-schemas.js.map +1 -1
- package/dist/src/terminal-action-projection.d.ts +1 -1
- package/dist/src/terminal-action-projection.js +1 -1
- package/dist/src/terminal-action-projection.js.map +1 -1
- package/dist/src/terminal-agent-adapter.d.ts +10 -2
- package/dist/src/terminal-agent-adapter.js.map +1 -1
- package/dist/src/terminal-agent-bridge.d.ts +44 -0
- package/dist/src/terminal-agent-bridge.js +348 -48
- package/dist/src/terminal-agent-bridge.js.map +1 -1
- package/dist/src/terminal-command-cli-adapter.js +98 -47
- package/dist/src/terminal-command-cli-adapter.js.map +1 -1
- package/dist/src/terminal-delegate-cli-adapter.js +4 -3
- package/dist/src/terminal-delegate-cli-adapter.js.map +1 -1
- package/dist/src/terminal-dispatch-composition.d.ts +2 -0
- package/dist/src/terminal-dispatch-execution.d.ts +2 -0
- package/dist/src/terminal-dispatch-execution.js.map +1 -1
- package/dist/src/terminal-list-cli-adapter.js +43 -9
- package/dist/src/terminal-list-cli-adapter.js.map +1 -1
- package/dist/src/terminal-list-renderer.js +21 -8
- package/dist/src/terminal-list-renderer.js.map +1 -1
- package/dist/src/terminal-watch-cli-adapter.js +27 -0
- package/dist/src/terminal-watch-cli-adapter.js.map +1 -1
- package/docs/quickstart-herdr.md +2 -2
- package/docs/quickstart-tmux.md +4 -4
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/templates/openclaw-skills/agent-knock-knock/SKILL.md +12 -10
|
@@ -40,6 +40,10 @@ const CODEX_NATIVE_STATUS_POPUP_BY_PROFILE = {
|
|
|
40
40
|
"codex-tui-0.149.1": [
|
|
41
41
|
" /status show current session configuration and token usage",
|
|
42
42
|
" /statusline configure which items appear in the status line"
|
|
43
|
+
],
|
|
44
|
+
"codex-tui-generic-v1": [
|
|
45
|
+
" /status show current session configuration and token usage",
|
|
46
|
+
" /statusline configure which items appear in the status line"
|
|
43
47
|
]
|
|
44
48
|
};
|
|
45
49
|
// Codex's verified `/status` profiles are exercised against its canonical
|
|
@@ -52,7 +56,8 @@ const CODEX_NATIVE_STATUS_MIN_VIEWPORT_BY_PROFILE = {
|
|
|
52
56
|
"codex-tui-0.146.1": 80,
|
|
53
57
|
"codex-tui-0.147.0": 80,
|
|
54
58
|
"codex-tui-0.148.0": 80,
|
|
55
|
-
"codex-tui-0.149.1": 80
|
|
59
|
+
"codex-tui-0.149.1": 80,
|
|
60
|
+
"codex-tui-generic-v1": 80
|
|
56
61
|
};
|
|
57
62
|
const CLAUDE_NATIVE_STATUS_POPUP_BY_PROFILE = {
|
|
58
63
|
"claude-code-2.1.218-native-status": [
|
|
@@ -72,6 +77,12 @@ const CLAUDE_NATIVE_STATUS_POPUP_BY_PROFILE = {
|
|
|
72
77
|
"/statusline Set up Claude Code's status line UI",
|
|
73
78
|
"/ide Manage IDE integrations and show status",
|
|
74
79
|
"/usage Show session cost, plan usage, and activity stats"
|
|
80
|
+
],
|
|
81
|
+
"claude-code-unverified-native-status-v1": [
|
|
82
|
+
"/status Show Claude Code status including version, model, account, API connectivity, and tool statuses",
|
|
83
|
+
"/statusline Set up Claude Code's status line UI",
|
|
84
|
+
"/ide Manage IDE integrations and show status",
|
|
85
|
+
"/usage Show session cost, plan usage, and activity stats"
|
|
75
86
|
]
|
|
76
87
|
};
|
|
77
88
|
const CLAUDE_NATIVE_STATUS_SETTLE_BY_PROFILE = {
|
|
@@ -86,6 +97,10 @@ const CLAUDE_NATIVE_STATUS_SETTLE_BY_PROFILE = {
|
|
|
86
97
|
"claude-code-2.1.237-native-status": {
|
|
87
98
|
minimumStableMs: 80,
|
|
88
99
|
maximumSettleMs: 5_000
|
|
100
|
+
},
|
|
101
|
+
"claude-code-unverified-native-status-v1": {
|
|
102
|
+
minimumStableMs: 80,
|
|
103
|
+
maximumSettleMs: 5_000
|
|
89
104
|
}
|
|
90
105
|
};
|
|
91
106
|
const CODEX_LARGE_PASTE_CHAR_THRESHOLD = 1_000;
|
|
@@ -163,6 +178,64 @@ export class NativeInspectionDismissalError extends Error {
|
|
|
163
178
|
this.name = "NativeInspectionDismissalError";
|
|
164
179
|
}
|
|
165
180
|
}
|
|
181
|
+
function terminalSendComposerRequirements(agent, multiline, options) {
|
|
182
|
+
const exactEmptyRetryAuthority = options.requireExactEmptyComposerAfterBeforeText;
|
|
183
|
+
const requireExactEmptyComposerBeforeText = options.requireExactEmptyComposerBeforeText === true;
|
|
184
|
+
const userExplicitEnterAfterTextWithoutComposerVeto = options.userExplicitEnterAfterTextWithoutComposerVeto === true;
|
|
185
|
+
if (userExplicitEnterAfterTextWithoutComposerVeto &&
|
|
186
|
+
(agent !== "codex" || !requireExactEmptyComposerBeforeText)) {
|
|
187
|
+
throw new TerminalInputNotStartedError("composer-veto-free Enter requires Codex and an exact-empty pre-text user-explicit boundary");
|
|
188
|
+
}
|
|
189
|
+
const requireExactComposer = !userExplicitEnterAfterTextWithoutComposerVeto &&
|
|
190
|
+
((agent === "codex" && multiline) ||
|
|
191
|
+
options.requireExactComposerBeforeEnter === true ||
|
|
192
|
+
requireExactEmptyComposerBeforeText ||
|
|
193
|
+
exactEmptyRetryAuthority !== undefined);
|
|
194
|
+
if (exactEmptyRetryAuthority !== undefined &&
|
|
195
|
+
(agent !== "codex" ||
|
|
196
|
+
options.beforeText === undefined ||
|
|
197
|
+
!/^[0-9a-f]{64}$/u.test(exactEmptyRetryAuthority.preliminaryComposerDigest))) {
|
|
198
|
+
throw new TerminalInputNotStartedError("exact-empty replacement requires Codex, a reservation hook, and a preliminary composer digest");
|
|
199
|
+
}
|
|
200
|
+
return {
|
|
201
|
+
exactEmptyRetryAuthority,
|
|
202
|
+
requireExactEmptyComposerBeforeText,
|
|
203
|
+
requireExactComposer
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
async function recordTerminalSendStage(options, event, preserveUserInput, previousError) {
|
|
207
|
+
try {
|
|
208
|
+
await options.onTransportStage?.(event);
|
|
209
|
+
return previousError;
|
|
210
|
+
}
|
|
211
|
+
catch (error) {
|
|
212
|
+
if (!preserveUserInput)
|
|
213
|
+
throw error;
|
|
214
|
+
return previousError ?? error;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
async function runTerminalSendBeforeEnter(options, context, preserveUserInput, previousError) {
|
|
218
|
+
try {
|
|
219
|
+
await options.beforeEnter?.(context);
|
|
220
|
+
return previousError;
|
|
221
|
+
}
|
|
222
|
+
catch (error) {
|
|
223
|
+
if (!preserveUserInput)
|
|
224
|
+
throw error;
|
|
225
|
+
return previousError ?? error;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
async function waitForUserExplicitCodexEnter(input) {
|
|
229
|
+
if (!input.enabled)
|
|
230
|
+
return;
|
|
231
|
+
await input.sleep(Math.max(0, CODEX_PASTE_ENTER_SETTLE_MS -
|
|
232
|
+
(input.nowMs() - input.textInjectedAt)));
|
|
233
|
+
}
|
|
234
|
+
function throwPostTextHookFailureAfterUserEnter(enabled, error) {
|
|
235
|
+
if (!enabled || error === undefined)
|
|
236
|
+
return;
|
|
237
|
+
throw new TerminalEnterDispatchReservedError("user-explicit Codex Enter was dispatched but post-text bookkeeping failed; do not retry", { cause: error });
|
|
238
|
+
}
|
|
166
239
|
export class TerminalAgentBridge {
|
|
167
240
|
registry;
|
|
168
241
|
terminalProvider;
|
|
@@ -318,16 +391,7 @@ export class TerminalAgentBridge {
|
|
|
318
391
|
async send(agent, terminalControl, text, options = {}) {
|
|
319
392
|
const adapter = this.registry.require(agent);
|
|
320
393
|
const multiline = /[\r\n]/u.test(text.trimEnd());
|
|
321
|
-
const exactEmptyRetryAuthority = options
|
|
322
|
-
const requireExactComposer = (adapter.agent === "codex" && multiline) ||
|
|
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
|
-
}
|
|
394
|
+
const { exactEmptyRetryAuthority, requireExactEmptyComposerBeforeText, requireExactComposer } = terminalSendComposerRequirements(adapter.agent, multiline, options);
|
|
331
395
|
try {
|
|
332
396
|
assertTerminalMutationCapabilities({
|
|
333
397
|
provider: this.terminalProvider,
|
|
@@ -365,6 +429,7 @@ export class TerminalAgentBridge {
|
|
|
365
429
|
if (!verifiedForText.capabilities.includes("send_keys")) {
|
|
366
430
|
throw new TerminalInputNotStartedError(`${adapter.displayName} terminal input capability changed before injection`);
|
|
367
431
|
}
|
|
432
|
+
let composerVerifiedImmediatelyBeforeText = false;
|
|
368
433
|
if (exactEmptyRetryAuthority) {
|
|
369
434
|
try {
|
|
370
435
|
// Invoking this retry-only hook consumes the durable attempt. The
|
|
@@ -404,22 +469,33 @@ export class TerminalAgentBridge {
|
|
|
404
469
|
catch (error) {
|
|
405
470
|
throw new TerminalInputNotStartedError(error instanceof Error ? error.message : String(error), { cause: error });
|
|
406
471
|
}
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
472
|
+
if (requireExactEmptyComposerBeforeText) {
|
|
473
|
+
try {
|
|
474
|
+
verifiedForText = await this.verifyExactEmptyComposerBeforeText(adapter, verifiedForText, normalized, options.runtime, options.allowWorkingComposerForUserExplicit === true);
|
|
475
|
+
composerVerifiedImmediatelyBeforeText = true;
|
|
476
|
+
}
|
|
477
|
+
catch (error) {
|
|
478
|
+
throw new TerminalInputNotStartedError(error instanceof Error ? error.message : String(error), { cause: error });
|
|
479
|
+
}
|
|
412
480
|
}
|
|
413
|
-
verifiedForText = reverifiedForText;
|
|
414
481
|
}
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
}
|
|
482
|
+
if (!composerVerifiedImmediatelyBeforeText) {
|
|
483
|
+
try {
|
|
484
|
+
const reverifiedForText = await this.verifyTerminalIdentity(adapter.agent, verifiedForText, options.runtime);
|
|
485
|
+
if (!sameTerminalControlIdentity(verifiedForText, reverifiedForText)) {
|
|
486
|
+
throw new Error("terminal identity changed after the final pre-text check");
|
|
487
|
+
}
|
|
488
|
+
verifiedForText = reverifiedForText;
|
|
489
|
+
}
|
|
490
|
+
catch (error) {
|
|
491
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
492
|
+
if (exactEmptyRetryAuthority) {
|
|
493
|
+
throw new TerminalEnterDispatchReservedError(message, {
|
|
494
|
+
cause: error
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
throw new TerminalInputNotStartedError(message, { cause: error });
|
|
421
498
|
}
|
|
422
|
-
throw new TerminalInputNotStartedError(message, { cause: error });
|
|
423
499
|
}
|
|
424
500
|
try {
|
|
425
501
|
await this.terminalProvider.sendText(this.terminalProvider.endpoint(verifiedForText), normalized);
|
|
@@ -435,43 +511,55 @@ export class TerminalAgentBridge {
|
|
|
435
511
|
}
|
|
436
512
|
throw error;
|
|
437
513
|
}
|
|
514
|
+
const textInjectedAt = this.nowMs();
|
|
515
|
+
const preserveUserInput = options.userExplicitEnterAfterTextWithoutComposerVeto === true;
|
|
516
|
+
let postTextHookError;
|
|
438
517
|
let verifiedForEnter;
|
|
439
518
|
try {
|
|
440
|
-
await options
|
|
519
|
+
postTextHookError = await recordTerminalSendStage(options, {
|
|
441
520
|
stage: "text_injected",
|
|
442
521
|
agent: adapter.agent,
|
|
443
522
|
terminalControl: verifiedForText,
|
|
444
523
|
multiline
|
|
445
|
-
});
|
|
446
|
-
// Text and Enter are separate
|
|
447
|
-
//
|
|
448
|
-
//
|
|
524
|
+
}, preserveUserInput);
|
|
525
|
+
// Text and Enter are separate transport operations. Autonomous sends
|
|
526
|
+
// retain their exact Composer proof. A user-explicit managed Codex Send
|
|
527
|
+
// crosses only the native paste-suppression window, then keeps terminal
|
|
528
|
+
// identity as its final physical boundary.
|
|
449
529
|
if (adapter.agent === "codex" && requireExactComposer) {
|
|
450
530
|
verifiedForEnter = await this.settleCodexMultilineComposer(adapter, terminalControl, normalized, options.runtime, {
|
|
451
|
-
allowOpaqueLargePastePlaceholder: exactEmptyRetryAuthority === undefined
|
|
531
|
+
allowOpaqueLargePastePlaceholder: exactEmptyRetryAuthority === undefined,
|
|
532
|
+
allowWorkingComposer: options.allowWorkingComposerForUserExplicit === true
|
|
452
533
|
});
|
|
453
534
|
}
|
|
454
535
|
else if (requireExactComposer) {
|
|
455
|
-
verifiedForEnter = await this.verifyExactComposerBeforeEnter(adapter, terminalControl, normalized, options.runtime);
|
|
536
|
+
verifiedForEnter = await this.verifyExactComposerBeforeEnter(adapter, terminalControl, normalized, options.runtime, options.allowWorkingComposerForUserExplicit === true);
|
|
456
537
|
}
|
|
457
538
|
else {
|
|
539
|
+
await waitForUserExplicitCodexEnter({
|
|
540
|
+
enabled: preserveUserInput,
|
|
541
|
+
textInjectedAt,
|
|
542
|
+
nowMs: this.nowMs,
|
|
543
|
+
sleep: this.sleep
|
|
544
|
+
});
|
|
458
545
|
verifiedForEnter = await this.verifyTerminalIdentity(adapter.agent, terminalControl, options.runtime);
|
|
459
546
|
}
|
|
460
|
-
await options
|
|
547
|
+
postTextHookError = await runTerminalSendBeforeEnter(options, {
|
|
461
548
|
agent: adapter.agent,
|
|
462
549
|
terminalControl: verifiedForEnter,
|
|
463
550
|
multiline,
|
|
464
551
|
text: normalized
|
|
465
|
-
});
|
|
552
|
+
}, preserveUserInput, postTextHookError);
|
|
466
553
|
// `beforeEnter` may await Store and terminal identity fences. The human
|
|
467
554
|
// can still edit the composer or change the native thread while that
|
|
468
555
|
// callback is pending, so recapture before attempting Enter.
|
|
469
556
|
if (requireExactComposer) {
|
|
470
557
|
verifiedForEnter = adapter.agent === "codex"
|
|
471
558
|
? await this.settleCodexMultilineComposer(adapter, terminalControl, normalized, options.runtime, {
|
|
472
|
-
allowOpaqueLargePastePlaceholder: exactEmptyRetryAuthority === undefined
|
|
559
|
+
allowOpaqueLargePastePlaceholder: exactEmptyRetryAuthority === undefined,
|
|
560
|
+
allowWorkingComposer: options.allowWorkingComposerForUserExplicit === true
|
|
473
561
|
})
|
|
474
|
-
: await this.verifyExactComposerBeforeEnter(adapter, terminalControl, normalized, options.runtime);
|
|
562
|
+
: await this.verifyExactComposerBeforeEnter(adapter, terminalControl, normalized, options.runtime, options.allowWorkingComposerForUserExplicit === true);
|
|
475
563
|
}
|
|
476
564
|
else if (options.beforeEnter) {
|
|
477
565
|
verifiedForEnter = await this.verifyTerminalIdentity(adapter.agent, terminalControl, options.runtime);
|
|
@@ -493,12 +581,13 @@ export class TerminalAgentBridge {
|
|
|
493
581
|
// pre-Enter proof: a thrown key-delivery error may still have emitted C-m.
|
|
494
582
|
try {
|
|
495
583
|
await this.terminalProvider.sendKeys(this.terminalProvider.endpoint(verifiedForEnter), ["C-m"]);
|
|
496
|
-
await options
|
|
584
|
+
postTextHookError = await recordTerminalSendStage(options, {
|
|
497
585
|
stage: "enter_dispatched",
|
|
498
586
|
agent: adapter.agent,
|
|
499
587
|
terminalControl: verifiedForEnter,
|
|
500
588
|
multiline
|
|
501
|
-
});
|
|
589
|
+
}, preserveUserInput, postTextHookError);
|
|
590
|
+
throwPostTextHookFailureAfterUserEnter(preserveUserInput, postTextHookError);
|
|
502
591
|
}
|
|
503
592
|
catch (error) {
|
|
504
593
|
if (exactEmptyRetryAuthority) {
|
|
@@ -539,6 +628,179 @@ export class TerminalAgentBridge {
|
|
|
539
628
|
}
|
|
540
629
|
return this.settleCodexComposerObservation(adapter, terminalControl, normalized, options.runtime, { minimumStableMs: CODEX_MULTILINE_SETTLE_POLL_MS });
|
|
541
630
|
}
|
|
631
|
+
/**
|
|
632
|
+
* Honor one explicit user Send against the current Codex terminal. The
|
|
633
|
+
* rendered Composer is not authority: clear the current draft once, inject
|
|
634
|
+
* the user's request, cross Codex's paste-suppression window, and press Enter
|
|
635
|
+
* once. Approval/modal and terminal identity remain hard pre-mutation gates.
|
|
636
|
+
* This primitive is intentionally not used by autonomous managed sends.
|
|
637
|
+
*/
|
|
638
|
+
async sendUserExplicitCodex(terminalControl, text, options) {
|
|
639
|
+
const adapter = this.registry.require("codex");
|
|
640
|
+
const normalized = text.trimEnd();
|
|
641
|
+
const multiline = /[\r\n]/u.test(normalized);
|
|
642
|
+
if (!normalized) {
|
|
643
|
+
throw new TerminalInputNotStartedError("terminal message is empty");
|
|
644
|
+
}
|
|
645
|
+
try {
|
|
646
|
+
assertTerminalMutationCapabilities({
|
|
647
|
+
provider: this.terminalProvider,
|
|
648
|
+
terminal: this.terminalProvider.endpoint(terminalControl),
|
|
649
|
+
semantic: ["send_keys", "screen_status"],
|
|
650
|
+
transport: [
|
|
651
|
+
"stable_resource_resolution",
|
|
652
|
+
"screen_capture",
|
|
653
|
+
"text_delivery",
|
|
654
|
+
"key_delivery"
|
|
655
|
+
]
|
|
656
|
+
});
|
|
657
|
+
}
|
|
658
|
+
catch (error) {
|
|
659
|
+
throw new TerminalInputNotStartedError(error instanceof Error ? error.message : String(error), { cause: error });
|
|
660
|
+
}
|
|
661
|
+
const notStarted = (error, prefix) => error instanceof TerminalInputNotStartedError
|
|
662
|
+
? error
|
|
663
|
+
: new TerminalInputNotStartedError(`${prefix ? `${prefix}: ` : ""}${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
664
|
+
const uncertain = (message, error) => error instanceof TerminalEnterDispatchReservedError
|
|
665
|
+
? error
|
|
666
|
+
: new TerminalEnterDispatchReservedError(message, { cause: error });
|
|
667
|
+
const captureSafePrompt = async (control) => {
|
|
668
|
+
try {
|
|
669
|
+
const captured = await this.captureInspection(adapter, control, {
|
|
670
|
+
runtime: options.runtime,
|
|
671
|
+
// Approval/modal authority belongs to the current viewport. Do
|
|
672
|
+
// not let historical Esc instructions in scrollback veto Send.
|
|
673
|
+
scrollbackLines: 0
|
|
674
|
+
});
|
|
675
|
+
if (captured.inspection.approval.blocked ||
|
|
676
|
+
captured.inspection.activity.state === "awaiting_approval" ||
|
|
677
|
+
codexBlockingModalVisible(captured.screen)) {
|
|
678
|
+
throw new Error("the explicit user Send is blocked by a Codex approval or modal prompt");
|
|
679
|
+
}
|
|
680
|
+
const verified = await this.verifyTerminalIdentity(adapter.agent, captured.terminalControl, options.runtime);
|
|
681
|
+
if (!sameTerminalControlIdentity(captured.terminalControl, verified)) {
|
|
682
|
+
throw new Error("terminal identity changed across the explicit Send approval scan");
|
|
683
|
+
}
|
|
684
|
+
return verified;
|
|
685
|
+
}
|
|
686
|
+
catch (error) {
|
|
687
|
+
throw notStarted(error);
|
|
688
|
+
}
|
|
689
|
+
};
|
|
690
|
+
const initiallySafe = await captureSafePrompt(terminalControl);
|
|
691
|
+
const reservationContext = {
|
|
692
|
+
terminalControl: initiallySafe,
|
|
693
|
+
text: normalized
|
|
694
|
+
};
|
|
695
|
+
try {
|
|
696
|
+
await options.beforeMutationReservation(reservationContext);
|
|
697
|
+
}
|
|
698
|
+
catch (error) {
|
|
699
|
+
throw notStarted(error);
|
|
700
|
+
}
|
|
701
|
+
// The reservation callback may await Store or terminal locks. Recapture
|
|
702
|
+
// only approval/modal and identity authority immediately before the first
|
|
703
|
+
// physical mutation; Composer visibility and contents are never a veto.
|
|
704
|
+
const verifiedForClear = await captureSafePrompt(initiallySafe);
|
|
705
|
+
let clearEndpoint;
|
|
706
|
+
try {
|
|
707
|
+
clearEndpoint = this.terminalProvider.endpoint(verifiedForClear);
|
|
708
|
+
}
|
|
709
|
+
catch (error) {
|
|
710
|
+
throw notStarted(error);
|
|
711
|
+
}
|
|
712
|
+
try {
|
|
713
|
+
await this.terminalProvider.sendKeys(clearEndpoint, ["C-u"]);
|
|
714
|
+
}
|
|
715
|
+
catch (error) {
|
|
716
|
+
if (error instanceof TerminalControlInputNotSentError) {
|
|
717
|
+
throw notStarted(error);
|
|
718
|
+
}
|
|
719
|
+
throw uncertain("explicit Codex draft-clear outcome is uncertain; do not retry automatically", error);
|
|
720
|
+
}
|
|
721
|
+
let postMutationHookError;
|
|
722
|
+
try {
|
|
723
|
+
await options.onComposerClearDispatched?.({
|
|
724
|
+
terminalControl: verifiedForClear,
|
|
725
|
+
text: normalized
|
|
726
|
+
});
|
|
727
|
+
}
|
|
728
|
+
catch (error) {
|
|
729
|
+
postMutationHookError = error;
|
|
730
|
+
}
|
|
731
|
+
let verifiedForText;
|
|
732
|
+
try {
|
|
733
|
+
verifiedForText = await this.verifyTerminalIdentity(adapter.agent, verifiedForClear, options.runtime);
|
|
734
|
+
if (!sameTerminalControlIdentity(verifiedForClear, verifiedForText)) {
|
|
735
|
+
throw new Error("terminal identity changed after clearing the Composer");
|
|
736
|
+
}
|
|
737
|
+
await this.terminalProvider.sendText(this.terminalProvider.endpoint(verifiedForText), normalized);
|
|
738
|
+
}
|
|
739
|
+
catch (error) {
|
|
740
|
+
throw uncertain("explicit Codex replacement text outcome is uncertain after clearing the prior draft; do not retry automatically", error);
|
|
741
|
+
}
|
|
742
|
+
const textInjectedAt = this.nowMs();
|
|
743
|
+
try {
|
|
744
|
+
try {
|
|
745
|
+
await options.onTransportStage?.({
|
|
746
|
+
stage: "text_injected",
|
|
747
|
+
agent: adapter.agent,
|
|
748
|
+
terminalControl: verifiedForText,
|
|
749
|
+
multiline
|
|
750
|
+
});
|
|
751
|
+
}
|
|
752
|
+
catch (error) {
|
|
753
|
+
postMutationHookError ??= error;
|
|
754
|
+
}
|
|
755
|
+
await this.sleep(Math.max(0, CODEX_PASTE_ENTER_SETTLE_MS - (this.nowMs() - textInjectedAt)));
|
|
756
|
+
}
|
|
757
|
+
catch (error) {
|
|
758
|
+
throw uncertain("explicit Codex text was injected but its Enter outcome is unresolved; do not retry automatically", error);
|
|
759
|
+
}
|
|
760
|
+
let verifiedForEnter;
|
|
761
|
+
try {
|
|
762
|
+
verifiedForEnter = await this.verifyTerminalIdentity(adapter.agent, verifiedForText, options.runtime);
|
|
763
|
+
if (!sameTerminalControlIdentity(verifiedForText, verifiedForEnter)) {
|
|
764
|
+
throw new Error("terminal identity changed before explicit Send Enter");
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
catch (error) {
|
|
768
|
+
throw uncertain("explicit Codex Enter endpoint is unresolved after terminal input; do not retry automatically", error);
|
|
769
|
+
}
|
|
770
|
+
try {
|
|
771
|
+
await this.terminalProvider.sendKeys(this.terminalProvider.endpoint(verifiedForEnter), ["C-m"]);
|
|
772
|
+
}
|
|
773
|
+
catch (error) {
|
|
774
|
+
throw uncertain("explicit Codex Send Enter outcome is uncertain; do not retry automatically", error);
|
|
775
|
+
}
|
|
776
|
+
try {
|
|
777
|
+
try {
|
|
778
|
+
await options.onTransportStage?.({
|
|
779
|
+
stage: "enter_dispatched",
|
|
780
|
+
agent: adapter.agent,
|
|
781
|
+
terminalControl: verifiedForEnter,
|
|
782
|
+
multiline
|
|
783
|
+
});
|
|
784
|
+
}
|
|
785
|
+
catch (error) {
|
|
786
|
+
postMutationHookError ??= error;
|
|
787
|
+
}
|
|
788
|
+
if (postMutationHookError !== undefined) {
|
|
789
|
+
throw postMutationHookError;
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
catch (error) {
|
|
793
|
+
throw uncertain("explicit Codex Send Enter was dispatched but its post-mutation acknowledgement failed; do not retry automatically", error);
|
|
794
|
+
}
|
|
795
|
+
return {
|
|
796
|
+
stage: "enter_dispatched",
|
|
797
|
+
terminalControl: verifiedForEnter,
|
|
798
|
+
disposition: "replaced_current_composer",
|
|
799
|
+
clearCount: 1,
|
|
800
|
+
textInjectionCount: 1,
|
|
801
|
+
enterCount: 1
|
|
802
|
+
};
|
|
803
|
+
}
|
|
542
804
|
/**
|
|
543
805
|
* Submit one already-present exact Codex draft. This primitive never writes
|
|
544
806
|
* text, clears the composer, or emits C-c, and contains exactly one C-m call.
|
|
@@ -618,14 +880,16 @@ export class TerminalAgentBridge {
|
|
|
618
880
|
enterCount: 1
|
|
619
881
|
};
|
|
620
882
|
}
|
|
621
|
-
async verifyExactComposerBeforeEnter(adapter, terminalControl, expectedText, runtime) {
|
|
883
|
+
async verifyExactComposerBeforeEnter(adapter, terminalControl, expectedText, runtime, allowWorkingComposer = false) {
|
|
622
884
|
const captured = await this.captureInspection(adapter, terminalControl, {
|
|
623
885
|
runtime,
|
|
624
886
|
scrollbackLines: CODEX_MULTILINE_SETTLE_SCROLLBACK_LINES
|
|
625
887
|
});
|
|
626
888
|
const exactComposer = exactTerminalComposerCapture(adapter.agent, captured.screen, expectedText);
|
|
627
889
|
if (captured.inspection.approval.blocked ||
|
|
628
|
-
!["idle", "unknown"].includes(captured.inspection.activity.state) ||
|
|
890
|
+
!(["idle", "unknown"].includes(captured.inspection.activity.state) ||
|
|
891
|
+
(allowWorkingComposer &&
|
|
892
|
+
captured.inspection.activity.state === "working")) ||
|
|
629
893
|
!exactComposer) {
|
|
630
894
|
throw new Error(`${adapter.displayName} composer was not exact and idle immediately before Enter`);
|
|
631
895
|
}
|
|
@@ -635,6 +899,34 @@ export class TerminalAgentBridge {
|
|
|
635
899
|
}
|
|
636
900
|
return verifiedImmediatelyBeforeEnter;
|
|
637
901
|
}
|
|
902
|
+
async verifyExactEmptyComposerBeforeText(adapter, terminalControl, expectedText, runtime, allowWorkingComposer = false) {
|
|
903
|
+
if (adapter.agent === "codex") {
|
|
904
|
+
const snapshot = await this.captureCodexComposerSnapshot(adapter, terminalControl, expectedText, runtime);
|
|
905
|
+
if (snapshot.state !== "exact_empty") {
|
|
906
|
+
const reason = "reason" in snapshot
|
|
907
|
+
? snapshot.reason
|
|
908
|
+
: `Codex composer is ${snapshot.state}`;
|
|
909
|
+
throw new Error(`Codex composer was not exactly empty immediately before text: ${reason}`);
|
|
910
|
+
}
|
|
911
|
+
return snapshot.terminalControl;
|
|
912
|
+
}
|
|
913
|
+
const captured = await this.captureInspection(adapter, terminalControl, {
|
|
914
|
+
runtime,
|
|
915
|
+
scrollbackLines: CODEX_MULTILINE_SETTLE_SCROLLBACK_LINES
|
|
916
|
+
});
|
|
917
|
+
if (captured.inspection.approval.blocked ||
|
|
918
|
+
!(["idle", "unknown"].includes(captured.inspection.activity.state) ||
|
|
919
|
+
(allowWorkingComposer &&
|
|
920
|
+
captured.inspection.activity.state === "working")) ||
|
|
921
|
+
!exactTerminalComposerCapture(adapter.agent, captured.screen, "")) {
|
|
922
|
+
throw new Error(`${adapter.displayName} composer was not exactly empty immediately before text`);
|
|
923
|
+
}
|
|
924
|
+
const verified = await this.verifyTerminalIdentity(adapter.agent, captured.terminalControl, runtime);
|
|
925
|
+
if (!sameTerminalControlIdentity(captured.terminalControl, verified)) {
|
|
926
|
+
throw new Error("terminal control identity changed after the final empty composer capture");
|
|
927
|
+
}
|
|
928
|
+
return verified;
|
|
929
|
+
}
|
|
638
930
|
/**
|
|
639
931
|
* Submit one closed, adapter-owned native inspection command.
|
|
640
932
|
*
|
|
@@ -1102,7 +1394,8 @@ export class TerminalAgentBridge {
|
|
|
1102
1394
|
const observation = await this.settleCodexComposerObservation(adapter, terminalControl, expectedText, runtime, {
|
|
1103
1395
|
minimumStableMs: CODEX_PASTE_ENTER_SETTLE_MS,
|
|
1104
1396
|
requiredState: "exact_draft",
|
|
1105
|
-
allowOpaqueLargePastePlaceholder: options.allowOpaqueLargePastePlaceholder === true
|
|
1397
|
+
allowOpaqueLargePastePlaceholder: options.allowOpaqueLargePastePlaceholder === true,
|
|
1398
|
+
allowWorkingComposer: options.allowWorkingComposer === true
|
|
1106
1399
|
});
|
|
1107
1400
|
if (observation.state === "exact_draft") {
|
|
1108
1401
|
return observation.terminalControl;
|
|
@@ -1126,7 +1419,9 @@ export class TerminalAgentBridge {
|
|
|
1126
1419
|
let control = terminalControl;
|
|
1127
1420
|
while (true) {
|
|
1128
1421
|
const snapshot = await this.captureCodexComposerSnapshot(adapter, control, expectedText, runtime, {
|
|
1129
|
-
allowOpaqueLargePastePlaceholder: options.allowOpaqueLargePastePlaceholder === true
|
|
1422
|
+
allowOpaqueLargePastePlaceholder: options.allowOpaqueLargePastePlaceholder === true,
|
|
1423
|
+
classifyOpaqueLargePasteAsDifferent: options.classifyOpaqueLargePasteAsDifferent === true,
|
|
1424
|
+
allowWorkingComposer: options.allowWorkingComposer === true
|
|
1130
1425
|
});
|
|
1131
1426
|
if (snapshot.state === "identity_drift" ||
|
|
1132
1427
|
(snapshot.state === "unavailable" && snapshot.retryable !== true)) {
|
|
@@ -1168,7 +1463,9 @@ export class TerminalAgentBridge {
|
|
|
1168
1463
|
// The caller's authorization hook must see evidence from a final,
|
|
1169
1464
|
// independently identity-fenced recapture, not the polling capture.
|
|
1170
1465
|
const finalSnapshot = await this.captureCodexComposerSnapshot(adapter, snapshot.terminalControl, expectedText, runtime, {
|
|
1171
|
-
allowOpaqueLargePastePlaceholder: options.allowOpaqueLargePastePlaceholder === true
|
|
1466
|
+
allowOpaqueLargePastePlaceholder: options.allowOpaqueLargePastePlaceholder === true,
|
|
1467
|
+
classifyOpaqueLargePasteAsDifferent: options.classifyOpaqueLargePasteAsDifferent === true,
|
|
1468
|
+
allowWorkingComposer: options.allowWorkingComposer === true
|
|
1172
1469
|
});
|
|
1173
1470
|
if (finalSnapshot.state === snapshot.state &&
|
|
1174
1471
|
"digest" in finalSnapshot &&
|
|
@@ -1288,14 +1585,15 @@ export class TerminalAgentBridge {
|
|
|
1288
1585
|
digest: screenDigest
|
|
1289
1586
|
};
|
|
1290
1587
|
}
|
|
1291
|
-
if (inspection.activity.state === "working"
|
|
1588
|
+
if (inspection.activity.state === "working" &&
|
|
1589
|
+
options.allowWorkingComposer !== true) {
|
|
1292
1590
|
return {
|
|
1293
1591
|
state: "working",
|
|
1294
1592
|
terminalControl: verifiedAfter,
|
|
1295
1593
|
digest: screenDigest
|
|
1296
1594
|
};
|
|
1297
1595
|
}
|
|
1298
|
-
const composer = currentCodexComposerCapture(styledScreen, expectedText, options.allowOpaqueLargePastePlaceholder === true);
|
|
1596
|
+
const composer = currentCodexComposerCapture(styledScreen, expectedText, options.allowOpaqueLargePastePlaceholder === true, options.classifyOpaqueLargePasteAsDifferent === true);
|
|
1299
1597
|
if (!composer) {
|
|
1300
1598
|
return {
|
|
1301
1599
|
state: "unavailable",
|
|
@@ -1891,7 +2189,7 @@ function assertClosedStatusInspectionPlan(adapter, terminalControl, plan) {
|
|
|
1891
2189
|
plan.composer.maximumSettleMs < plan.composer.minimumStableMs ||
|
|
1892
2190
|
plan.expectedResult.kind !== "native_status" ||
|
|
1893
2191
|
!exactPresentation) {
|
|
1894
|
-
throw new Error("refusing a non-closed
|
|
2192
|
+
throw new Error("refusing a non-closed native inspection plan");
|
|
1895
2193
|
}
|
|
1896
2194
|
}
|
|
1897
2195
|
function assertClosedNativeInspectionDismissal(plan) {
|
|
@@ -1899,7 +2197,7 @@ function assertClosedNativeInspectionDismissal(plan) {
|
|
|
1899
2197
|
plan.expectedResult.dismissal?.expected !== "idle_empty_composer" ||
|
|
1900
2198
|
JSON.stringify(plan.expectedResult.dismissal.keys) !==
|
|
1901
2199
|
JSON.stringify(["Escape"])) {
|
|
1902
|
-
throw new Error("native inspection has no closed
|
|
2200
|
+
throw new Error("native inspection has no closed modal dismissal plan");
|
|
1903
2201
|
}
|
|
1904
2202
|
}
|
|
1905
2203
|
function assertNativeInspectionComposerSafe(inspection, displayName = "terminal agent") {
|
|
@@ -2245,7 +2543,7 @@ function codexBlockingModalVisible(screen) {
|
|
|
2245
2543
|
* Classify only the bottom live Codex composer region. A matching transcript
|
|
2246
2544
|
* prompt elsewhere in scrollback is deliberately ignored.
|
|
2247
2545
|
*/
|
|
2248
|
-
function currentCodexComposerCapture(styledScreen, expectedText, allowOpaqueLargePastePlaceholder = false) {
|
|
2546
|
+
function currentCodexComposerCapture(styledScreen, expectedText, allowOpaqueLargePastePlaceholder = false, classifyOpaqueLargePasteAsDifferent = false) {
|
|
2249
2547
|
const screen = stripTerminalEscapeSequences(styledScreen);
|
|
2250
2548
|
const lines = screen.replace(/\r\n?/gu, "\n").split("\n");
|
|
2251
2549
|
while (lines.length > 0 && lines.at(-1)?.trim().length === 0) {
|
|
@@ -2295,7 +2593,9 @@ function currentCodexComposerCapture(styledScreen, expectedText, allowOpaqueLarg
|
|
|
2295
2593
|
const comparable = composerComparableText(bodyRows.join("\n"));
|
|
2296
2594
|
const opaqueLargePastePlaceholder = /^\[Pasted Content \d+ chars\]$/u.test(comparable);
|
|
2297
2595
|
if (opaqueLargePastePlaceholder && !allowOpaqueLargePastePlaceholder) {
|
|
2298
|
-
return
|
|
2596
|
+
return classifyOpaqueLargePasteAsDifferent
|
|
2597
|
+
? { state: "different_draft", digest }
|
|
2598
|
+
: undefined;
|
|
2299
2599
|
}
|
|
2300
2600
|
const exactVisibleDraft = terminalComposerRowsMatchExpected(bodyRows, expectedComparable);
|
|
2301
2601
|
const exactLargePastePlaceholder = allowOpaqueLargePastePlaceholder &&
|