@scotthuang/agent-knock-knock 0.12.13 → 0.12.15

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 (170) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/README.md +13 -13
  3. package/dist/src/callback-cli-adapter.d.ts +9 -6
  4. package/dist/src/callback-cli-adapter.js +119 -30
  5. package/dist/src/callback-cli-adapter.js.map +1 -1
  6. package/dist/src/callback-outbox-policy.d.ts +39 -0
  7. package/dist/src/callback-outbox-policy.js +163 -6
  8. package/dist/src/callback-outbox-policy.js.map +1 -1
  9. package/dist/src/callback-outbox-service.d.ts +28 -1
  10. package/dist/src/callback-outbox-service.js +413 -82
  11. package/dist/src/callback-outbox-service.js.map +1 -1
  12. package/dist/src/callback-outbox-settlement.d.ts +14 -1
  13. package/dist/src/callback-outbox-settlement.js +151 -30
  14. package/dist/src/callback-outbox-settlement.js.map +1 -1
  15. package/dist/src/callback-route-authority.d.ts +59 -0
  16. package/dist/src/callback-route-authority.js +123 -0
  17. package/dist/src/callback-route-authority.js.map +1 -0
  18. package/dist/src/callback-transport.d.ts +134 -0
  19. package/dist/src/callback-transport.js +256 -0
  20. package/dist/src/callback-transport.js.map +1 -0
  21. package/dist/src/cli-core.js +56 -18
  22. package/dist/src/cli-core.js.map +1 -1
  23. package/dist/src/cli-runtime-context.d.ts +2 -0
  24. package/dist/src/cli-runtime-context.js.map +1 -1
  25. package/dist/src/codex-lifecycle-compatibility.js +2 -1
  26. package/dist/src/codex-lifecycle-compatibility.js.map +1 -1
  27. package/dist/src/deferred-foreground-application-service.d.ts +18 -0
  28. package/dist/src/deferred-foreground-application-service.js +72 -0
  29. package/dist/src/deferred-foreground-application-service.js.map +1 -1
  30. package/dist/src/deferred-foreground-authority-cli-adapter.js +10 -1
  31. package/dist/src/deferred-foreground-authority-cli-adapter.js.map +1 -1
  32. package/dist/src/deferred-foreground-recovery-cli-adapter.d.ts +1 -1
  33. package/dist/src/deferred-foreground-recovery-cli-adapter.js +108 -37
  34. package/dist/src/deferred-foreground-recovery-cli-adapter.js.map +1 -1
  35. package/dist/src/deferred-foreground-recovery-service.d.ts +1 -1
  36. package/dist/src/deferred-foreground-recovery-service.js +10 -3
  37. package/dist/src/deferred-foreground-recovery-service.js.map +1 -1
  38. package/dist/src/deferred-foreground-transfer-policy.d.ts +1 -1
  39. package/dist/src/deferred-foreground-transfer-policy.js +3 -2
  40. package/dist/src/deferred-foreground-transfer-policy.js.map +1 -1
  41. package/dist/src/deferred-foreground-transfer.d.ts +17 -1
  42. package/dist/src/deferred-foreground-transfer.js +244 -35
  43. package/dist/src/deferred-foreground-transfer.js.map +1 -1
  44. package/dist/src/deferred-foreground-user-close.d.ts +18 -0
  45. package/dist/src/deferred-foreground-user-close.js +180 -0
  46. package/dist/src/deferred-foreground-user-close.js.map +1 -0
  47. package/dist/src/install-doctor-command-adapter.js +1 -1
  48. package/dist/src/install-doctor-command-adapter.js.map +1 -1
  49. package/dist/src/openclaw-callback-transport.d.ts +4 -1
  50. package/dist/src/openclaw-callback-transport.js +403 -10
  51. package/dist/src/openclaw-callback-transport.js.map +1 -1
  52. package/dist/src/openclaw-doctor.js +16 -2
  53. package/dist/src/openclaw-doctor.js.map +1 -1
  54. package/dist/src/openclaw-managed-callback-cli-adapter.d.ts +23 -0
  55. package/dist/src/openclaw-managed-callback-cli-adapter.js +77 -0
  56. package/dist/src/openclaw-managed-callback-cli-adapter.js.map +1 -0
  57. package/dist/src/openclaw-plugin-command-adapter.js +93 -39
  58. package/dist/src/openclaw-plugin-command-adapter.js.map +1 -1
  59. package/dist/src/openclaw-plugin-helpers.d.ts +1 -0
  60. package/dist/src/openclaw-plugin-helpers.js +9 -1
  61. package/dist/src/openclaw-plugin-helpers.js.map +1 -1
  62. package/dist/src/openclaw-plugin-schemas.d.ts +20 -1
  63. package/dist/src/openclaw-plugin-schemas.js +27 -3
  64. package/dist/src/openclaw-plugin-schemas.js.map +1 -1
  65. package/dist/src/protocol.d.ts +3 -0
  66. package/dist/src/protocol.js +4 -0
  67. package/dist/src/protocol.js.map +1 -1
  68. package/dist/src/store.d.ts +12 -1
  69. package/dist/src/store.js +81 -3
  70. package/dist/src/store.js.map +1 -1
  71. package/dist/src/terminal-acceptance-cli-adapter.d.ts +2 -0
  72. package/dist/src/terminal-acceptance-cli-adapter.js +62 -12
  73. package/dist/src/terminal-acceptance-cli-adapter.js.map +1 -1
  74. package/dist/src/terminal-action-projection.d.ts +31 -3
  75. package/dist/src/terminal-action-projection.js +64 -9
  76. package/dist/src/terminal-action-projection.js.map +1 -1
  77. package/dist/src/terminal-agent-bridge.d.ts +82 -0
  78. package/dist/src/terminal-agent-bridge.js +553 -109
  79. package/dist/src/terminal-agent-bridge.js.map +1 -1
  80. package/dist/src/terminal-command-cli-adapter.d.ts +26 -2
  81. package/dist/src/terminal-command-cli-adapter.js +3162 -214
  82. package/dist/src/terminal-command-cli-adapter.js.map +1 -1
  83. package/dist/src/terminal-delegate-cli-adapter.d.ts +4 -22
  84. package/dist/src/terminal-delegate-cli-adapter.js +239 -192
  85. package/dist/src/terminal-delegate-cli-adapter.js.map +1 -1
  86. package/dist/src/terminal-delegate-send-binding.d.ts +63 -0
  87. package/dist/src/terminal-delegate-send-binding.js +360 -0
  88. package/dist/src/terminal-delegate-send-binding.js.map +1 -0
  89. package/dist/src/terminal-dispatch-application.d.ts +4 -1
  90. package/dist/src/terminal-dispatch-application.js +11 -2
  91. package/dist/src/terminal-dispatch-application.js.map +1 -1
  92. package/dist/src/terminal-dispatch-composition.d.ts +5 -0
  93. package/dist/src/terminal-dispatch-execution.js +3 -1
  94. package/dist/src/terminal-dispatch-execution.js.map +1 -1
  95. package/dist/src/terminal-dispatch-ledger-codec.d.ts +3 -0
  96. package/dist/src/terminal-dispatch-ledger-codec.js +46 -19
  97. package/dist/src/terminal-dispatch-ledger-codec.js.map +1 -1
  98. package/dist/src/terminal-dispatch-presenter.js +5 -3
  99. package/dist/src/terminal-dispatch-presenter.js.map +1 -1
  100. package/dist/src/terminal-dispatch-receipt.d.ts +3 -0
  101. package/dist/src/terminal-dispatch-receipt.js +15 -1
  102. package/dist/src/terminal-dispatch-receipt.js.map +1 -1
  103. package/dist/src/terminal-dispatch-recovery-cli-adapter.js +68 -7
  104. package/dist/src/terminal-dispatch-recovery-cli-adapter.js.map +1 -1
  105. package/dist/src/terminal-dispatch-recovery-service.d.ts +2 -0
  106. package/dist/src/terminal-dispatch-recovery-service.js +9 -0
  107. package/dist/src/terminal-dispatch-recovery-service.js.map +1 -1
  108. package/dist/src/terminal-dispatch-repository-cli-adapter.d.ts +4 -2
  109. package/dist/src/terminal-dispatch-repository-cli-adapter.js +10 -3
  110. package/dist/src/terminal-dispatch-repository-cli-adapter.js.map +1 -1
  111. package/dist/src/terminal-identity-authority-cli-adapter.d.ts +2 -0
  112. package/dist/src/terminal-identity-authority-cli-adapter.js +34 -27
  113. package/dist/src/terminal-identity-authority-cli-adapter.js.map +1 -1
  114. package/dist/src/terminal-list-cli-adapter.d.ts +6 -0
  115. package/dist/src/terminal-list-cli-adapter.js +391 -177
  116. package/dist/src/terminal-list-cli-adapter.js.map +1 -1
  117. package/dist/src/terminal-list-renderer.d.ts +9 -1
  118. package/dist/src/terminal-list-renderer.js +226 -104
  119. package/dist/src/terminal-list-renderer.js.map +1 -1
  120. package/dist/src/terminal-maintenance-cli-adapter.js +253 -326
  121. package/dist/src/terminal-maintenance-cli-adapter.js.map +1 -1
  122. package/dist/src/terminal-monitor-application-service.d.ts +5 -0
  123. package/dist/src/terminal-monitor-application-service.js +5 -0
  124. package/dist/src/terminal-monitor-application-service.js.map +1 -1
  125. package/dist/src/terminal-monitor-cli-adapter.d.ts +25 -1
  126. package/dist/src/terminal-monitor-cli-adapter.js +170 -27
  127. package/dist/src/terminal-monitor-cli-adapter.js.map +1 -1
  128. package/dist/src/terminal-monitor-reconciliation-eligibility.d.ts +1 -1
  129. package/dist/src/terminal-monitor-reconciliation-eligibility.js +220 -32
  130. package/dist/src/terminal-monitor-reconciliation-eligibility.js.map +1 -1
  131. package/dist/src/terminal-monitor-state-cli-adapter.d.ts +1 -1
  132. package/dist/src/terminal-monitor-state-cli-adapter.js +808 -147
  133. package/dist/src/terminal-monitor-state-cli-adapter.js.map +1 -1
  134. package/dist/src/terminal-monitor-state-reconciliation-service.d.ts +2 -1
  135. package/dist/src/terminal-monitor-state-reconciliation-service.js +12 -0
  136. package/dist/src/terminal-monitor-state-reconciliation-service.js.map +1 -1
  137. package/dist/src/terminal-monitor-supervision-cli-adapter.d.ts +2 -0
  138. package/dist/src/terminal-monitor-supervision-cli-adapter.js +31 -11
  139. package/dist/src/terminal-monitor-supervision-cli-adapter.js.map +1 -1
  140. package/dist/src/terminal-process-source.d.ts +13 -0
  141. package/dist/src/terminal-process-source.js +37 -0
  142. package/dist/src/terminal-process-source.js.map +1 -1
  143. package/dist/src/terminal-runtime-cli-adapter.js +14 -2
  144. package/dist/src/terminal-runtime-cli-adapter.js.map +1 -1
  145. package/dist/src/terminal-status-cli-adapter.d.ts +1 -0
  146. package/dist/src/terminal-status-cli-adapter.js +30 -28
  147. package/dist/src/terminal-status-cli-adapter.js.map +1 -1
  148. package/dist/src/terminal-submission-acceptance.js +78 -25
  149. package/dist/src/terminal-submission-acceptance.js.map +1 -1
  150. package/dist/src/terminal-submission-retry-service.d.ts +114 -0
  151. package/dist/src/terminal-submission-retry-service.js +589 -0
  152. package/dist/src/terminal-submission-retry-service.js.map +1 -0
  153. package/dist/src/terminal-user-send-intent.d.ts +65 -0
  154. package/dist/src/terminal-user-send-intent.js +317 -0
  155. package/dist/src/terminal-user-send-intent.js.map +1 -0
  156. package/dist/src/terminal-watch-callback-cli-adapter.d.ts +10 -2
  157. package/dist/src/terminal-watch-callback-cli-adapter.js +76 -37
  158. package/dist/src/terminal-watch-callback-cli-adapter.js.map +1 -1
  159. package/dist/src/terminal-watch-cli-adapter.js +41 -17
  160. package/dist/src/terminal-watch-cli-adapter.js.map +1 -1
  161. package/dist/src/terminal-watch-service.d.ts +11 -6
  162. package/dist/src/terminal-watch-service.js +210 -41
  163. package/dist/src/terminal-watch-service.js.map +1 -1
  164. package/dist/src/terminal-watch-store.d.ts +23 -0
  165. package/dist/src/terminal-watch-store.js +162 -4
  166. package/dist/src/terminal-watch-store.js.map +1 -1
  167. package/docs/quickstart-herdr.md +1 -1
  168. package/docs/quickstart-tmux.md +3 -3
  169. package/package.json +1 -1
  170. package/templates/openclaw-skills/agent-knock-knock/SKILL.md +14 -11
@@ -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.148.0 keep Enter in paste/newline mode for
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
- const CODEX_MULTILINE_SETTLE_TIMEOUT_MS = 2_000;
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,69 @@ 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
  }
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
+ }
394
+ }
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
+ }
407
+ }
320
408
  try {
321
- await options.beforeText?.({
322
- agent: adapter.agent,
323
- terminalControl: verifiedForText,
324
- multiline,
325
- text: normalized
326
- });
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");
412
+ }
413
+ verifiedForText = reverifiedForText;
327
414
  }
328
415
  catch (error) {
329
- throw new TerminalInputNotStartedError(error instanceof Error ? error.message : String(error), { cause: error });
416
+ const message = error instanceof Error ? error.message : String(error);
417
+ if (exactEmptyRetryAuthority) {
418
+ throw new TerminalEnterDispatchReservedError(message, {
419
+ cause: error
420
+ });
421
+ }
422
+ throw new TerminalInputNotStartedError(message, { cause: error });
330
423
  }
331
424
  try {
332
425
  await this.terminalProvider.sendText(this.terminalProvider.endpoint(verifiedForText), normalized);
333
426
  }
334
427
  catch (error) {
428
+ if (exactEmptyRetryAuthority) {
429
+ throw new TerminalEnterDispatchReservedError("reserved Codex replacement text delivery outcome is uncertain; do not retry", { cause: error });
430
+ }
335
431
  if (error instanceof TerminalControlInputNotSentError) {
336
432
  throw new TerminalInputNotStartedError(error.message, {
337
433
  cause: error
@@ -339,61 +435,187 @@ export class TerminalAgentBridge {
339
435
  }
340
436
  throw error;
341
437
  }
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
438
  let verifiedForEnter;
353
- if (adapter.agent === "codex" && requireExactComposer) {
354
- verifiedForEnter = await this.settleCodexMultilineComposer(adapter, terminalControl, normalized, options.runtime);
355
- }
356
- else if (requireExactComposer) {
357
- verifiedForEnter = await this.verifyExactComposerBeforeEnter(adapter, terminalControl, normalized, options.runtime);
358
- }
359
- else {
360
- try {
439
+ try {
440
+ await options.onTransportStage?.({
441
+ stage: "text_injected",
442
+ agent: adapter.agent,
443
+ terminalControl: verifiedForText,
444
+ 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.
449
+ if (adapter.agent === "codex" && requireExactComposer) {
450
+ verifiedForEnter = await this.settleCodexMultilineComposer(adapter, terminalControl, normalized, options.runtime, {
451
+ allowOpaqueLargePastePlaceholder: exactEmptyRetryAuthority === undefined
452
+ });
453
+ }
454
+ else if (requireExactComposer) {
455
+ verifiedForEnter = await this.verifyExactComposerBeforeEnter(adapter, terminalControl, normalized, options.runtime);
456
+ }
457
+ else {
361
458
  verifiedForEnter = await this.verifyTerminalIdentity(adapter.agent, terminalControl, options.runtime);
362
459
  }
363
- catch (error) {
460
+ await options.beforeEnter?.({
461
+ agent: adapter.agent,
462
+ terminalControl: verifiedForEnter,
463
+ multiline,
464
+ text: normalized
465
+ });
466
+ // `beforeEnter` may await Store and terminal identity fences. The human
467
+ // can still edit the composer or change the native thread while that
468
+ // callback is pending, so recapture before attempting Enter.
469
+ if (requireExactComposer) {
470
+ verifiedForEnter = adapter.agent === "codex"
471
+ ? await this.settleCodexMultilineComposer(adapter, terminalControl, normalized, options.runtime, {
472
+ allowOpaqueLargePastePlaceholder: exactEmptyRetryAuthority === undefined
473
+ })
474
+ : await this.verifyExactComposerBeforeEnter(adapter, terminalControl, normalized, options.runtime);
475
+ }
476
+ else if (options.beforeEnter) {
477
+ verifiedForEnter = await this.verifyTerminalIdentity(adapter.agent, terminalControl, options.runtime);
478
+ }
479
+ }
480
+ catch (error) {
481
+ if (exactEmptyRetryAuthority) {
482
+ if (error instanceof TerminalEnterDispatchReservedError) {
483
+ throw error;
484
+ }
485
+ throw new TerminalEnterDispatchReservedError(error instanceof Error ? error.message : String(error), { cause: error });
486
+ }
487
+ if (error instanceof TerminalEnterDispatchNotAttemptedError) {
364
488
  throw error;
365
489
  }
490
+ throw new TerminalEnterDispatchNotAttemptedError(error instanceof Error ? error.message : String(error), { cause: error });
366
491
  }
367
- await options.beforeEnter?.({
368
- agent: adapter.agent,
369
- terminalControl: verifiedForEnter,
370
- multiline,
371
- text: normalized
372
- });
373
- // `beforeEnter` may await Store and terminal identity fences. The human can
374
- // still edit the composer or change the native thread while that callback
375
- // is pending, so its successful return is not authority to press Enter.
376
- // Recapture the exact draft and process identity at the final boundary.
377
- if (requireExactComposer) {
378
- verifiedForEnter = adapter.agent === "codex"
379
- ? await this.settleCodexMultilineComposer(adapter, terminalControl, normalized, options.runtime)
380
- : await this.verifyExactComposerBeforeEnter(adapter, terminalControl, normalized, options.runtime);
381
- }
382
- else if (options.beforeEnter) {
383
- verifiedForEnter = await this.verifyTerminalIdentity(adapter.agent, terminalControl, options.runtime);
384
- }
385
- await this.terminalProvider.sendKeys(this.terminalProvider.endpoint(verifiedForEnter), ["C-m"]);
386
- await options.onTransportStage?.({
492
+ // Do not include this transport call (or anything after it) in the typed
493
+ // pre-Enter proof: a thrown key-delivery error may still have emitted C-m.
494
+ try {
495
+ await this.terminalProvider.sendKeys(this.terminalProvider.endpoint(verifiedForEnter), ["C-m"]);
496
+ await options.onTransportStage?.({
497
+ stage: "enter_dispatched",
498
+ agent: adapter.agent,
499
+ terminalControl: verifiedForEnter,
500
+ multiline
501
+ });
502
+ }
503
+ catch (error) {
504
+ if (exactEmptyRetryAuthority) {
505
+ throw new TerminalEnterDispatchReservedError("reserved Codex Enter dispatch outcome is uncertain; do not retry", { cause: error });
506
+ }
507
+ throw error;
508
+ }
509
+ return {
387
510
  stage: "enter_dispatched",
388
511
  agent: adapter.agent,
389
512
  terminalControl: verifiedForEnter,
390
513
  multiline
391
- });
514
+ };
515
+ }
516
+ /** Observe only the current, live Codex composer without exposing its text. */
517
+ async observeCodexComposer(terminalControl, expectedText, options = {}) {
518
+ const adapter = this.registry.require("codex");
519
+ const normalized = expectedText.trimEnd();
520
+ if (!normalized) {
521
+ return {
522
+ state: "unavailable",
523
+ reason: "expected Codex draft is empty"
524
+ };
525
+ }
526
+ try {
527
+ assertTerminalMutationCapabilities({
528
+ provider: this.terminalProvider,
529
+ terminal: this.terminalProvider.endpoint(terminalControl),
530
+ semantic: ["screen_status"],
531
+ transport: ["stable_resource_resolution", "screen_capture"]
532
+ });
533
+ }
534
+ catch (error) {
535
+ return {
536
+ state: "unavailable",
537
+ reason: error instanceof Error ? error.message : String(error)
538
+ };
539
+ }
540
+ return this.settleCodexComposerObservation(adapter, terminalControl, normalized, options.runtime, { minimumStableMs: CODEX_MULTILINE_SETTLE_POLL_MS });
541
+ }
542
+ /**
543
+ * Submit one already-present exact Codex draft. This primitive never writes
544
+ * text, clears the composer, or emits C-c, and contains exactly one C-m call.
545
+ */
546
+ async submitExactCodexDraft(terminalControl, expectedText, options) {
547
+ const adapter = this.registry.require("codex");
548
+ const normalized = expectedText.trimEnd();
549
+ if (!normalized) {
550
+ throw new TerminalEnterDispatchNotAttemptedError("expected Codex draft is empty");
551
+ }
552
+ let preliminaryObservation;
553
+ try {
554
+ assertTerminalMutationCapabilities({
555
+ provider: this.terminalProvider,
556
+ terminal: this.terminalProvider.endpoint(terminalControl),
557
+ semantic: ["send_keys", "screen_status"],
558
+ transport: [
559
+ "stable_resource_resolution",
560
+ "screen_capture",
561
+ "key_delivery"
562
+ ]
563
+ });
564
+ const observation = await this.settleCodexComposerObservation(adapter, terminalControl, normalized, options.runtime, {
565
+ minimumStableMs: CODEX_PASTE_ENTER_SETTLE_MS,
566
+ requiredState: "exact_draft"
567
+ });
568
+ if (observation.state !== "exact_draft") {
569
+ const reason = "reason" in observation
570
+ ? observation.reason
571
+ : `Codex composer is ${observation.state}`;
572
+ throw new Error(`refusing to submit an unproven exact Codex draft: ${reason}`);
573
+ }
574
+ preliminaryObservation = observation;
575
+ }
576
+ catch (error) {
577
+ if (error instanceof TerminalEnterDispatchNotAttemptedError) {
578
+ throw error;
579
+ }
580
+ throw new TerminalEnterDispatchNotAttemptedError(error instanceof Error ? error.message : String(error), { cause: error });
581
+ }
582
+ let finalObservation;
583
+ try {
584
+ // Calling this hook is itself the one-shot boundary. A throw may occur
585
+ // after durable storage committed, so every failure from here onward
586
+ // permanently consumes the attempt.
587
+ await options.beforeEnterReservation({
588
+ terminalControl: preliminaryObservation.terminalControl,
589
+ text: normalized,
590
+ composerDigest: preliminaryObservation.digest
591
+ });
592
+ const recaptured = await this.captureCodexComposerSnapshot(adapter, preliminaryObservation.terminalControl, normalized, options.runtime);
593
+ if (recaptured.state !== "exact_draft" ||
594
+ recaptured.digest !== preliminaryObservation.digest ||
595
+ !sameTerminalControlIdentity(preliminaryObservation.terminalControl, recaptured.terminalControl)) {
596
+ const reason = "reason" in recaptured
597
+ ? recaptured.reason
598
+ : `Codex composer is ${recaptured.state}`;
599
+ throw new Error(`reserved Codex draft changed before Enter: ${reason}`);
600
+ }
601
+ finalObservation = recaptured;
602
+ }
603
+ catch (error) {
604
+ if (error instanceof TerminalEnterDispatchReservedError) {
605
+ throw error;
606
+ }
607
+ throw new TerminalEnterDispatchReservedError(error instanceof Error ? error.message : String(error), { cause: error });
608
+ }
609
+ try {
610
+ await this.terminalProvider.sendKeys(this.terminalProvider.endpoint(finalObservation.terminalControl), ["C-m"]);
611
+ }
612
+ catch (error) {
613
+ throw new TerminalEnterDispatchReservedError("reserved Codex Enter dispatch outcome is uncertain; do not retry", { cause: error });
614
+ }
392
615
  return {
393
616
  stage: "enter_dispatched",
394
- agent: adapter.agent,
395
- terminalControl: verifiedForEnter,
396
- multiline
617
+ terminalControl: finalObservation.terminalControl,
618
+ enterCount: 1
397
619
  };
398
620
  }
399
621
  async verifyExactComposerBeforeEnter(adapter, terminalControl, expectedText, runtime) {
@@ -876,70 +1098,218 @@ export class TerminalAgentBridge {
876
1098
  materialization: expected
877
1099
  };
878
1100
  }
879
- async settleCodexMultilineComposer(adapter, terminalControl, expectedText, runtime) {
1101
+ async settleCodexMultilineComposer(adapter, terminalControl, expectedText, runtime, options = {}) {
1102
+ const observation = await this.settleCodexComposerObservation(adapter, terminalControl, expectedText, runtime, {
1103
+ minimumStableMs: CODEX_PASTE_ENTER_SETTLE_MS,
1104
+ requiredState: "exact_draft",
1105
+ allowOpaqueLargePastePlaceholder: options.allowOpaqueLargePastePlaceholder === true
1106
+ });
1107
+ if (observation.state === "exact_draft") {
1108
+ return observation.terminalControl;
1109
+ }
1110
+ if (observation.state === "working" ||
1111
+ observation.state === "approval_or_modal") {
1112
+ throw new Error("Codex became busy or blocked while its multiline composer was settling");
1113
+ }
1114
+ if (observation.state === "identity_drift") {
1115
+ throw new Error(observation.reason);
1116
+ }
1117
+ throw new Error("Codex multiline composer did not become exact, idle, and stable before the bounded submit deadline");
1118
+ }
1119
+ async settleCodexComposerObservation(adapter, terminalControl, expectedText, runtime, options) {
880
1120
  const startedAt = this.nowMs();
881
- let stableDigest;
1121
+ let candidate;
882
1122
  let stableSince;
883
1123
  let stableCaptures = 0;
884
- while (this.nowMs() - startedAt <= CODEX_MULTILINE_SETTLE_TIMEOUT_MS) {
885
- const captured = await this.captureInspection(adapter, terminalControl, {
886
- runtime,
887
- scrollbackLines: CODEX_MULTILINE_SETTLE_SCROLLBACK_LINES
1124
+ let finishObservedExactCandidate = false;
1125
+ let capturesBeyondDeadline = 0;
1126
+ let control = terminalControl;
1127
+ while (true) {
1128
+ const snapshot = await this.captureCodexComposerSnapshot(adapter, control, expectedText, runtime, {
1129
+ allowOpaqueLargePastePlaceholder: options.allowOpaqueLargePastePlaceholder === true
888
1130
  });
889
- if (captured.inspection.approval.blocked ||
890
- captured.inspection.activity.state === "awaiting_approval" ||
891
- captured.inspection.activity.state === "working") {
892
- throw new Error("Codex became busy or blocked while its multiline composer was settling");
893
- }
894
- const composer = exactCodexComposerCapture(captured.screen, expectedText);
895
- const composerIsIdle = captured.inspection.activity.state === "idle" ||
896
- (captured.inspection.activity.state === "unknown" &&
897
- composer !== undefined);
898
- if (composer && composerIsIdle) {
899
- if (composer.digest === stableDigest) {
900
- stableCaptures += 1;
1131
+ if (snapshot.state === "identity_drift" ||
1132
+ (snapshot.state === "unavailable" && snapshot.retryable !== true)) {
1133
+ return snapshot;
1134
+ }
1135
+ if (this.nowMs() - startedAt > CODEX_MULTILINE_SETTLE_TIMEOUT_MS) {
1136
+ capturesBeyondDeadline += 1;
1137
+ if (capturesBeyondDeadline > CODEX_EXACT_CANDIDATE_GRACE_CAPTURES) {
1138
+ return {
1139
+ state: "unavailable",
1140
+ reason: "Codex exact composer did not stabilize within its bounded capture grace"
1141
+ };
901
1142
  }
902
- else {
903
- stableDigest = composer.digest;
904
- stableSince = this.nowMs();
905
- stableCaptures = 1;
1143
+ }
1144
+ control = snapshot.terminalControl;
1145
+ const sameCandidate = candidate !== undefined &&
1146
+ candidate.state === snapshot.state &&
1147
+ candidate.digest === snapshot.digest &&
1148
+ sameTerminalControlIdentity(candidate.terminalControl, snapshot.terminalControl);
1149
+ if (sameCandidate) {
1150
+ stableCaptures += 1;
1151
+ }
1152
+ else {
1153
+ candidate = snapshot;
1154
+ stableSince = this.nowMs();
1155
+ stableCaptures = 1;
1156
+ finishObservedExactCandidate = snapshot.state === "exact_draft";
1157
+ }
1158
+ const stableForMs = stableSince === undefined
1159
+ ? 0
1160
+ : this.nowMs() - stableSince;
1161
+ if (stableCaptures >= CODEX_MULTILINE_STABLE_CAPTURES &&
1162
+ stableForMs >= options.minimumStableMs &&
1163
+ snapshot.state !== "unavailable" &&
1164
+ (options.requiredState === undefined ||
1165
+ snapshot.state === options.requiredState ||
1166
+ snapshot.state === "working" ||
1167
+ snapshot.state === "approval_or_modal")) {
1168
+ // The caller's authorization hook must see evidence from a final,
1169
+ // independently identity-fenced recapture, not the polling capture.
1170
+ const finalSnapshot = await this.captureCodexComposerSnapshot(adapter, snapshot.terminalControl, expectedText, runtime, {
1171
+ allowOpaqueLargePastePlaceholder: options.allowOpaqueLargePastePlaceholder === true
1172
+ });
1173
+ if (finalSnapshot.state === snapshot.state &&
1174
+ "digest" in finalSnapshot &&
1175
+ finalSnapshot.digest === snapshot.digest &&
1176
+ sameTerminalControlIdentity(snapshot.terminalControl, finalSnapshot.terminalControl)) {
1177
+ return {
1178
+ ...finalSnapshot,
1179
+ stableCaptures: stableCaptures + 1
1180
+ };
906
1181
  }
907
- if (stableCaptures >= CODEX_MULTILINE_STABLE_CAPTURES &&
908
- stableSince !== undefined &&
909
- this.nowMs() - stableSince >= CODEX_PASTE_ENTER_SETTLE_MS) {
910
- const finalCapture = await this.captureInspection(adapter, captured.terminalControl, {
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;
1182
+ if (finalSnapshot.state === "identity_drift" ||
1183
+ (finalSnapshot.state === "unavailable" &&
1184
+ finalSnapshot.retryable !== true)) {
1185
+ return finalSnapshot;
1186
+ }
1187
+ if (options.requiredState !== undefined &&
1188
+ snapshot.state === options.requiredState) {
1189
+ return {
1190
+ state: "unavailable",
1191
+ reason: "Codex composer changed after its stable final recapture"
1192
+ };
929
1193
  }
1194
+ candidate = finalSnapshot;
1195
+ control = finalSnapshot.terminalControl;
1196
+ stableSince = this.nowMs();
1197
+ stableCaptures = 1;
1198
+ finishObservedExactCandidate = finalSnapshot.state === "exact_draft";
930
1199
  }
931
- else {
932
- stableDigest = undefined;
933
- stableSince = undefined;
934
- stableCaptures = 0;
1200
+ const elapsed = this.nowMs() - startedAt;
1201
+ if (elapsed > CODEX_MULTILINE_SETTLE_TIMEOUT_MS &&
1202
+ !finishObservedExactCandidate) {
1203
+ return {
1204
+ state: "unavailable",
1205
+ reason: options.requiredState
1206
+ ? `Codex composer did not become ${options.requiredState} before the bounded observation deadline`
1207
+ : "Codex composer did not become stable before the bounded observation deadline"
1208
+ };
1209
+ }
1210
+ if (elapsed > CODEX_MULTILINE_SETTLE_TIMEOUT_MS &&
1211
+ options.requiredState !== undefined &&
1212
+ snapshot.state !== options.requiredState) {
1213
+ if (snapshot.state !== "unavailable") {
1214
+ return {
1215
+ ...snapshot,
1216
+ stableCaptures
1217
+ };
1218
+ }
1219
+ return {
1220
+ state: "unavailable",
1221
+ reason: snapshot.reason
1222
+ };
935
1223
  }
936
- const remainingSuppressionMs = CODEX_PASTE_ENTER_SETTLE_MS -
937
- (stableSince === undefined ? 0 : this.nowMs() - stableSince);
938
- await this.sleep(Math.max(1, Math.min(CODEX_MULTILINE_SETTLE_POLL_MS, remainingSuppressionMs > 0
939
- ? remainingSuppressionMs
1224
+ const remainingStableMs = options.minimumStableMs - stableForMs;
1225
+ await this.sleep(Math.max(1, Math.min(CODEX_MULTILINE_SETTLE_POLL_MS, remainingStableMs > 0
1226
+ ? remainingStableMs
940
1227
  : CODEX_MULTILINE_SETTLE_POLL_MS)));
941
1228
  }
942
- throw new Error("Codex multiline composer did not become exact, idle, and stable before the bounded submit deadline");
1229
+ }
1230
+ async captureCodexComposerSnapshot(adapter, terminalControl, expectedText, runtime, options = {}) {
1231
+ let verifiedBefore;
1232
+ try {
1233
+ verifiedBefore = await this.verifyTerminalIdentity(adapter.agent, terminalControl, runtime);
1234
+ }
1235
+ catch (error) {
1236
+ return {
1237
+ state: "identity_drift",
1238
+ reason: error instanceof Error ? error.message : String(error)
1239
+ };
1240
+ }
1241
+ let styledScreen;
1242
+ try {
1243
+ styledScreen = await this.terminalProvider.capture(this.terminalProvider.endpoint(verifiedBefore), {
1244
+ scrollbackLines: CODEX_MULTILINE_SETTLE_SCROLLBACK_LINES,
1245
+ preserveEscapes: true
1246
+ });
1247
+ }
1248
+ catch (error) {
1249
+ return {
1250
+ state: "unavailable",
1251
+ reason: error instanceof Error ? error.message : String(error)
1252
+ };
1253
+ }
1254
+ const screen = stripTerminalEscapeSequences(styledScreen);
1255
+ let inspection;
1256
+ try {
1257
+ inspection = adapter.inspectScreen({ screen, runtime });
1258
+ }
1259
+ catch (error) {
1260
+ return {
1261
+ state: "unavailable",
1262
+ reason: error instanceof Error ? error.message : String(error)
1263
+ };
1264
+ }
1265
+ let verifiedAfter;
1266
+ try {
1267
+ verifiedAfter = await this.verifyTerminalIdentity(adapter.agent, verifiedBefore, runtime);
1268
+ }
1269
+ catch (error) {
1270
+ return {
1271
+ state: "identity_drift",
1272
+ reason: error instanceof Error ? error.message : String(error)
1273
+ };
1274
+ }
1275
+ if (!sameTerminalControlIdentity(verifiedBefore, verifiedAfter)) {
1276
+ return {
1277
+ state: "identity_drift",
1278
+ reason: "terminal control identity changed across the Codex composer capture"
1279
+ };
1280
+ }
1281
+ const screenDigest = createHash("sha256").update(styledScreen).digest("hex");
1282
+ if (inspection.approval.blocked ||
1283
+ inspection.activity.state === "awaiting_approval" ||
1284
+ codexBlockingModalVisible(screen)) {
1285
+ return {
1286
+ state: "approval_or_modal",
1287
+ terminalControl: verifiedAfter,
1288
+ digest: screenDigest
1289
+ };
1290
+ }
1291
+ if (inspection.activity.state === "working") {
1292
+ return {
1293
+ state: "working",
1294
+ terminalControl: verifiedAfter,
1295
+ digest: screenDigest
1296
+ };
1297
+ }
1298
+ const composer = currentCodexComposerCapture(styledScreen, expectedText, options.allowOpaqueLargePastePlaceholder === true);
1299
+ if (!composer) {
1300
+ return {
1301
+ state: "unavailable",
1302
+ reason: "the current live Codex composer could not be proven",
1303
+ retryable: true,
1304
+ terminalControl: verifiedAfter,
1305
+ digest: screenDigest
1306
+ };
1307
+ }
1308
+ return {
1309
+ state: composer.state,
1310
+ terminalControl: verifiedAfter,
1311
+ digest: composer.digest
1312
+ };
943
1313
  }
944
1314
  /**
945
1315
  * Prove that the exact managed draft is still present after Enter dispatch.
@@ -1864,6 +2234,80 @@ function codexNativeInspectionComposerMismatchDiagnostic(screen, plan) {
1864
2234
  ? "composer_viewport_truncated"
1865
2235
  : "composer_not_exact";
1866
2236
  }
2237
+ function codexBlockingModalVisible(screen) {
2238
+ const tail = screen.replace(/\r\n?/gu, "\n").split("\n").slice(-80)
2239
+ .join("\n");
2240
+ return /\b(?:press|use)\s+(?:esc|escape)\s+to\s+(?:cancel|close|dismiss)\b/iu
2241
+ .test(tail) ||
2242
+ /\besc\s+to\s+cancel\b/iu.test(tail);
2243
+ }
2244
+ /**
2245
+ * Classify only the bottom live Codex composer region. A matching transcript
2246
+ * prompt elsewhere in scrollback is deliberately ignored.
2247
+ */
2248
+ function currentCodexComposerCapture(styledScreen, expectedText, allowOpaqueLargePastePlaceholder = false) {
2249
+ const screen = stripTerminalEscapeSequences(styledScreen);
2250
+ const lines = screen.replace(/\r\n?/gu, "\n").split("\n");
2251
+ while (lines.length > 0 && lines.at(-1)?.trim().length === 0) {
2252
+ lines.pop();
2253
+ }
2254
+ let composerIndex = -1;
2255
+ for (let index = lines.length - 1; index >= 0; index -= 1) {
2256
+ if (CODEX_COMPOSER_MARKER.test(lines[index])) {
2257
+ composerIndex = index;
2258
+ break;
2259
+ }
2260
+ }
2261
+ if (composerIndex < 0) {
2262
+ return undefined;
2263
+ }
2264
+ const footerIndex = lines.findIndex((line, index) => index > composerIndex &&
2265
+ CODEX_COMPLETE_COMPOSER_FOOTER.test(line.trim()));
2266
+ if (footerIndex >= 0 &&
2267
+ lines.slice(footerIndex + 1).some((line) => line.trim().length > 0)) {
2268
+ return undefined;
2269
+ }
2270
+ const regionEnd = footerIndex < 0 ? lines.length : footerIndex;
2271
+ const region = lines.slice(composerIndex, regionEnd);
2272
+ while (region.length > 1 && region.at(-1)?.trim().length === 0) {
2273
+ region.pop();
2274
+ }
2275
+ if (region.length === 0 ||
2276
+ region.slice(1).some((line) => line.length > 0 && !line.startsWith(" "))) {
2277
+ return undefined;
2278
+ }
2279
+ const bodyRows = [
2280
+ region[0].replace(/^[›»]\s?/u, ""),
2281
+ ...region.slice(1).map((line) => line.startsWith(" ") ? line.slice(2) : line)
2282
+ ];
2283
+ const digest = createHash("sha256").update(region.join("\n")).digest("hex");
2284
+ const positivelyEmpty = footerIndex >= 0 &&
2285
+ bodyRows.slice(1).every((row) => row.trim().length === 0) &&
2286
+ exactCodexReadyStyledComposerCapture(styledScreen) !== undefined;
2287
+ if (positivelyEmpty) {
2288
+ return { state: "exact_empty", digest };
2289
+ }
2290
+ if (footerIndex < 0) {
2291
+ return undefined;
2292
+ }
2293
+ const expectedComparable = composerComparableText(expectedText);
2294
+ const expectedCharacterCount = Array.from(expectedText).length;
2295
+ const comparable = composerComparableText(bodyRows.join("\n"));
2296
+ const opaqueLargePastePlaceholder = /^\[Pasted Content \d+ chars\]$/u.test(comparable);
2297
+ if (opaqueLargePastePlaceholder && !allowOpaqueLargePastePlaceholder) {
2298
+ return undefined;
2299
+ }
2300
+ const exactVisibleDraft = terminalComposerRowsMatchExpected(bodyRows, expectedComparable);
2301
+ const exactLargePastePlaceholder = allowOpaqueLargePastePlaceholder &&
2302
+ expectedCharacterCount > CODEX_LARGE_PASTE_CHAR_THRESHOLD &&
2303
+ comparable === composerComparableText(`[Pasted Content ${expectedCharacterCount} chars]`);
2304
+ if (exactVisibleDraft || exactLargePastePlaceholder) {
2305
+ return { state: "exact_draft", digest };
2306
+ }
2307
+ return comparable.length > 0
2308
+ ? { state: "different_draft", digest }
2309
+ : undefined;
2310
+ }
1867
2311
  function exactCodexComposerCapture(screen, expectedText) {
1868
2312
  const lines = screen.replace(/\r\n?/gu, "\n").split("\n");
1869
2313
  const expectedComparable = composerComparableText(expectedText);