@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.
Files changed (66) hide show
  1. package/CHANGELOG.md +30 -1
  2. package/README.md +11 -11
  3. package/dist/src/claude-lifecycle-compatibility.d.ts +9 -0
  4. package/dist/src/claude-lifecycle-compatibility.js +50 -5
  5. package/dist/src/claude-lifecycle-compatibility.js.map +1 -1
  6. package/dist/src/claude-local-transcript-provider.js +5 -6
  7. package/dist/src/claude-local-transcript-provider.js.map +1 -1
  8. package/dist/src/claude-terminal-agent-adapter.js +38 -30
  9. package/dist/src/claude-terminal-agent-adapter.js.map +1 -1
  10. package/dist/src/codex-lifecycle-compatibility.d.ts +19 -0
  11. package/dist/src/codex-lifecycle-compatibility.js +37 -0
  12. package/dist/src/codex-lifecycle-compatibility.js.map +1 -1
  13. package/dist/src/codex-store-adapter.js +5 -6
  14. package/dist/src/codex-store-adapter.js.map +1 -1
  15. package/dist/src/codex-terminal-agent-adapter.js +46 -28
  16. package/dist/src/codex-terminal-agent-adapter.js.map +1 -1
  17. package/dist/src/deferred-foreground-application-service.js +1 -0
  18. package/dist/src/deferred-foreground-application-service.js.map +1 -1
  19. package/dist/src/deferred-foreground-boundary.d.ts +2 -0
  20. package/dist/src/deferred-foreground-preparation-cli-adapter.js +2 -0
  21. package/dist/src/deferred-foreground-preparation-cli-adapter.js.map +1 -1
  22. package/dist/src/deferred-foreground-preparation-service.js +4 -2
  23. package/dist/src/deferred-foreground-preparation-service.js.map +1 -1
  24. package/dist/src/deferred-foreground-recovery-cli-adapter.js +1 -0
  25. package/dist/src/deferred-foreground-recovery-cli-adapter.js.map +1 -1
  26. package/dist/src/doctor-capabilities.d.ts +4 -3
  27. package/dist/src/doctor-capabilities.js +26 -6
  28. package/dist/src/doctor-capabilities.js.map +1 -1
  29. package/dist/src/install-doctor-command-adapter.js +1 -1
  30. package/dist/src/install-doctor-command-adapter.js.map +1 -1
  31. package/dist/src/native-thread-lifecycle-cli-adapter.js +17 -2
  32. package/dist/src/native-thread-lifecycle-cli-adapter.js.map +1 -1
  33. package/dist/src/native-thread-transition-application.js +9 -3
  34. package/dist/src/native-thread-transition-application.js.map +1 -1
  35. package/dist/src/openclaw-plugin-command-adapter.js +15 -11
  36. package/dist/src/openclaw-plugin-command-adapter.js.map +1 -1
  37. package/dist/src/openclaw-plugin-helpers.js +22 -1
  38. package/dist/src/openclaw-plugin-helpers.js.map +1 -1
  39. package/dist/src/openclaw-plugin-schemas.js +2 -2
  40. package/dist/src/openclaw-plugin-schemas.js.map +1 -1
  41. package/dist/src/terminal-action-projection.d.ts +1 -1
  42. package/dist/src/terminal-action-projection.js +1 -1
  43. package/dist/src/terminal-action-projection.js.map +1 -1
  44. package/dist/src/terminal-agent-adapter.d.ts +10 -2
  45. package/dist/src/terminal-agent-adapter.js.map +1 -1
  46. package/dist/src/terminal-agent-bridge.d.ts +44 -0
  47. package/dist/src/terminal-agent-bridge.js +348 -48
  48. package/dist/src/terminal-agent-bridge.js.map +1 -1
  49. package/dist/src/terminal-command-cli-adapter.js +98 -47
  50. package/dist/src/terminal-command-cli-adapter.js.map +1 -1
  51. package/dist/src/terminal-delegate-cli-adapter.js +4 -3
  52. package/dist/src/terminal-delegate-cli-adapter.js.map +1 -1
  53. package/dist/src/terminal-dispatch-composition.d.ts +2 -0
  54. package/dist/src/terminal-dispatch-execution.d.ts +2 -0
  55. package/dist/src/terminal-dispatch-execution.js.map +1 -1
  56. package/dist/src/terminal-list-cli-adapter.js +43 -9
  57. package/dist/src/terminal-list-cli-adapter.js.map +1 -1
  58. package/dist/src/terminal-list-renderer.js +21 -8
  59. package/dist/src/terminal-list-renderer.js.map +1 -1
  60. package/dist/src/terminal-watch-cli-adapter.js +27 -0
  61. package/dist/src/terminal-watch-cli-adapter.js.map +1 -1
  62. package/docs/quickstart-herdr.md +2 -2
  63. package/docs/quickstart-tmux.md +4 -4
  64. package/openclaw.plugin.json +1 -1
  65. package/package.json +1 -1
  66. 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.requireExactEmptyComposerAfterBeforeText;
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
- try {
409
- const reverifiedForText = await this.verifyTerminalIdentity(adapter.agent, verifiedForText, options.runtime);
410
- if (!sameTerminalControlIdentity(verifiedForText, reverifiedForText)) {
411
- throw new Error("terminal identity changed after the final pre-text check");
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
- catch (error) {
416
- const message = error instanceof Error ? error.message : String(error);
417
- if (exactEmptyRetryAuthority) {
418
- throw new TerminalEnterDispatchReservedError(message, {
419
- cause: error
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.onTransportStage?.({
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 tmux operations. Revalidate between them
447
- // and never submit after identity or composer drift. Every failure in
448
- // this region is typed proof that this invocation did not call C-m.
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.beforeEnter?.({
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.onTransportStage?.({
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 or unverified native inspection plan");
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 verified modal dismissal plan");
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 undefined;
2596
+ return classifyOpaqueLargePasteAsDifferent
2597
+ ? { state: "different_draft", digest }
2598
+ : undefined;
2299
2599
  }
2300
2600
  const exactVisibleDraft = terminalComposerRowsMatchExpected(bodyRows, expectedComparable);
2301
2601
  const exactLargePastePlaceholder = allowOpaqueLargePastePlaceholder &&