@scotthuang/agent-knock-knock 0.12.13 → 0.12.14

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