@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
@@ -1,18 +1,24 @@
1
1
  // Raw CLI infrastructure for ordinary terminal send/respond/approval commands.
2
2
  import { AsyncLocalStorage } from "node:async_hooks";
3
3
  import { createHash, randomUUID } from "node:crypto";
4
+ import os from "node:os";
4
5
  import path from "node:path";
6
+ import { callbackExpectedForConversation, callbackExpectedForConversationWithLegacyFallback, callbackRouteFingerprintForConversation } from "./callback-route-authority.js";
7
+ import { supersedeUnacceptedCallbackDeliveries } from "./callback-outbox-policy.js";
5
8
  import { captureClaudeTranscriptAnchor, defaultClaudeHome } from "./claude-local-transcript-provider.js";
9
+ import { validateCodexRolloutAcceptanceAnchor } from "./terminal-submission-acceptance.js";
6
10
  import { isRecord, nonBlankString as stringValue } from "./value-guards.js";
7
- import { applyMessageToConversation, budgetAction, createMessage, effectiveTurnStatus, executorForConversation, isTerminalDispatchOwnerReleasedStatus, sessionIdForConversation, turnIdForConversation } from "./protocol.js";
11
+ import { deferredForegroundActiveMessageId, isDeferredForegroundSubmissionRetryPending } from "./deferred-foreground-transfer.js";
12
+ import { applyMessageToConversation, budgetAction, createMessage, effectiveTurnStatus, executorForConversation, isSessionSendBlockingStatus, isTerminalDispatchOwnerReleasedStatus, sessionIdForConversation, turnIdForConversation } from "./protocol.js";
8
13
  import { readNdjsonLog } from "./transcript.js";
9
- import { appendEvent, ensureDir, ensureStoreWritable, listConversations, loadState, messageEvent, pathsForConversationDir, saveState, withStoreWriterLeaseAsync } from "./store.js";
10
- import { createManagedSessionId, managedSessionBindingToken } from "./managed-session.js";
11
- import { loadManagedSession, saveManagedSession, tryLoadManagedSession } from "./session-store.js";
14
+ import { appendEvent, appendExplicitUserCloseEvent, defaultStoreDir, ensureDir, ensureStoreWritable, listConversations, loadState, messageEvent, pathsForConversation, pathsForConversationDir, saveExplicitUserCloseState, saveState, withStoreWriterLeaseAsync } from "./store.js";
15
+ import { createManagedSessionId, managedSessionBindingToken, managedSessionRevision, unmanagedTerminalBindingToken } from "./managed-session.js";
16
+ import { listManagedSessions, loadManagedSession, saveManagedSession, tryLoadManagedSession } from "./session-store.js";
12
17
  import { terminalControlEvidenceMatches } from "./terminal-control-ref.js";
13
- import { TerminalInputNotStartedError } from "./terminal-agent-bridge.js";
18
+ import { isExactClaudeNativeInspectionIdleComposer, TerminalEnterDispatchNotAttemptedError, TerminalInputNotStartedError } from "./terminal-agent-bridge.js";
19
+ import { decideTerminalSubmissionRetry, loadTerminalSubmissionRetry, projectTerminalSubmissionRetryPending, saveTerminalSubmissionRetry, terminalSubmissionRetryLedgerFields, TERMINAL_SUBMISSION_RETRY_SCHEMA, TERMINAL_SUBMISSION_RETRY_VERSION } from "./terminal-submission-retry-service.js";
14
20
  import { evaluateApprovalPolicy } from "./approval-policy.js";
15
- import { codexCompanionsPresentInOpenRootInventory, isCompleteNativeRollout, nativeIdentityMatchesCodexPreMaterialization, terminalControlsShareIncarnation } from "./terminal-authority-policy.js";
21
+ import { codexCompanionsPresentInOpenRootInventory, isCompleteNativeRollout, nativeIdentityMatchesCodexPreMaterialization, terminalControlAliasMatches, terminalControlsShareIncarnation } from "./terminal-authority-policy.js";
16
22
  import { decideTerminalSendAuthority } from "./terminal-action-projection.js";
17
23
  import { terminalScopedCodexApprovalPromptSnapshot } from "./terminal-scoped-approval-authority.js";
18
24
  import { withCanonicalMutationLocks } from "./mutation-transaction.js";
@@ -23,8 +29,10 @@ import { sameCanonicalStatePath, terminalDispatchLedgerLooksLifecycle, terminalD
23
29
  import * as dispatchApplication from "./terminal-dispatch-application.js";
24
30
  import { bindDeferredForegroundApplicationScope, bindDeferredForegroundWriterScope } from "./deferred-foreground-capability.js";
25
31
  import * as deferredRecoveryAdapter from "./deferred-foreground-recovery-cli-adapter.js";
32
+ import { cleanupDeferredForegroundUserClose } from "./deferred-foreground-user-close.js";
26
33
  import { deferredForegroundBoundaryProjection } from "./deferred-foreground-preparation-cli-adapter.js";
27
34
  import { terminalSubmissionPayload } from "./terminal-dispatch-execution.js";
35
+ import { createTerminalUserSendIntentRepository, TerminalUserSendIntentBoundaryConflictError, TerminalUserSendIntentUncertainError } from "./terminal-user-send-intent.js";
28
36
  import { presentTerminalCompleted, presentTerminalDispatchReplay, presentTerminalIdentityFailure, presentTerminalUncertain, presentTerminalZeroInputAbort as renderTerminalZeroInputAbort } from "./terminal-dispatch-presenter.js";
29
37
  import * as dispatchReceipt from "./terminal-dispatch-receipt.js";
30
38
  import { expandHome, positiveMilliseconds, writeCliJson as printJson } from "./cli-command-runtime.js";
@@ -88,6 +96,7 @@ const observeCurrentNativeAgentSessionIdentity = rawPort("observeCurrentNativeAg
88
96
  const parseJsonOption = rawPort("parseJsonOption");
89
97
  const persistManagedSessionNativeIdentity = rawPort("persistManagedSessionNativeIdentity");
90
98
  const positiveMinutes = rawPort("positiveMinutes");
99
+ const processIncarnationForPid = rawPort("processIncarnationForPid");
91
100
  const prepareDeferredCodexForegroundBinding = rawPort("prepareDeferredCodexForegroundBinding");
92
101
  const quarantineManagedSessionBinding = rawPort("quarantineManagedSessionBinding");
93
102
  const reattachManagedSessionForNativeIdentity = rawPort("reattachManagedSessionForNativeIdentity");
@@ -96,6 +105,7 @@ const refineManagedSessionNativeIdentity = rawPort("refineManagedSessionNativeId
96
105
  const refineTerminalTurnEndpoint = rawPort("refineTerminalTurnEndpoint");
97
106
  const required = rawPort("required");
98
107
  const resolveCurrentNativeAgentSessionIdentity = rawPort("resolveCurrentNativeAgentSessionIdentity");
108
+ const resolveTerminalBridgeDispatchLedger = rawPort("resolveTerminalBridgeDispatchLedger");
99
109
  const resolveTerminalDispatchLedgerPaneIncarnation = rawPort("resolveTerminalDispatchLedgerPaneIncarnation");
100
110
  const resolveTerminalConversationFromOptions = rawPort("resolveTerminalConversationFromOptions");
101
111
  const stallOtherTerminalBridgeConversationsForUncertainDispatch = rawPort("stallOtherTerminalBridgeConversationsForUncertainDispatch");
@@ -417,6 +427,7 @@ function replayExactActiveTerminalSubmission({ options, terminalControl, request
417
427
  }
418
428
  });
419
429
  const acceptanceInvalid = replayReceipt.submission_outcome === "uncertain";
430
+ const callbackExpected = callbackExpectedForConversationWithLegacyFallback(owner, ledger.callback_expected);
420
431
  printJson({
421
432
  session_id: sessionIdForConversation(owner),
422
433
  turn_id: turnIdForConversation(owner),
@@ -426,7 +437,7 @@ function replayExactActiveTerminalSubmission({ options, terminalControl, request
426
437
  status: replayReceipt.status,
427
438
  submission_outcome: replayReceipt.submission_outcome,
428
439
  background: true,
429
- callback_expected: !acceptanceInvalid && Boolean(owner.gateway_method ?? ledger.callback_expected),
440
+ callback_expected: !acceptanceInvalid && callbackExpected,
430
441
  terminal_control: terminalControl,
431
442
  executor,
432
443
  replayed: replayReceipt.replayed,
@@ -445,7 +456,7 @@ function replayExactActiveTerminalSubmission({ options, terminalControl, request
445
456
  sessionId: sessionIdForConversation(owner),
446
457
  turnId: turnIdForConversation(owner),
447
458
  source: "terminal_control",
448
- callbackExpected: Boolean(owner.gateway_method ?? ledger.callback_expected)
459
+ callbackExpected
449
460
  })
450
461
  : {
451
462
  action: "inspect",
@@ -682,7 +693,7 @@ function replayExactStoredTerminalSubmission({ options, terminalControl, request
682
693
  }
683
694
  });
684
695
  const acceptanceInvalid = replayReceipt.submission_outcome === "uncertain";
685
- const callbackExpected = Boolean(owner.gateway_method);
696
+ const callbackExpected = callbackExpectedForConversation(owner);
686
697
  printJson({
687
698
  session_id: sessionIdForConversation(owner),
688
699
  turn_id: turnIdForConversation(owner),
@@ -761,140 +772,2987 @@ function assertNoUnresolvedTerminalBridgeSubmission(storeDir, terminalControl, c
761
772
  }
762
773
  }
763
774
  }
764
- function prepareManagedSend({ options, statePath, logPath, messageBody, stateLockHeld = false, persist = true, rejectTerminalControl = false }) {
765
- if (!stateLockHeld) {
766
- const releaseLock = acquireFileLock(`${statePath}.lock`);
775
+ function prepareManagedSend({ options, statePath, logPath, messageBody, stateLockHeld = false, persist = true, rejectTerminalControl = false }) {
776
+ if (!stateLockHeld) {
777
+ const releaseLock = acquireFileLock(`${statePath}.lock`);
778
+ try {
779
+ return prepareManagedSend({
780
+ options,
781
+ statePath,
782
+ logPath,
783
+ messageBody,
784
+ stateLockHeld: true,
785
+ persist,
786
+ rejectTerminalControl
787
+ });
788
+ }
789
+ finally {
790
+ releaseLock();
791
+ }
792
+ }
793
+ const conversation = loadState(statePath);
794
+ if (conversation.status !== "waiting_for_openclaw" ||
795
+ options.type !== "answer") {
796
+ throw new Error(`cannot respond to turn ${turnIdForConversation(conversation)}; ` +
797
+ `turn is ${conversation.status}`);
798
+ }
799
+ const executor = executorForConversation(conversation);
800
+ const type = "answer";
801
+ const nativeTakeoverForSend = isRecord(conversation.native_session_takeover)
802
+ ? conversation.native_session_takeover
803
+ : undefined;
804
+ const unresolvedSubmission = unresolvedTerminalBridgeSubmission(conversation);
805
+ if (unresolvedSubmission) {
806
+ throw new Error(`cannot send to ${conversation.conversation_id}; its previous terminal submission is ` +
807
+ `${unresolvedSubmission.status}. Inspect the conversation and terminal pane, then close ` +
808
+ "the AKK conversation before creating a replacement task.");
809
+ }
810
+ if (rejectTerminalControl &&
811
+ terminalControlFromTakeover(nativeTakeoverForSend)) {
812
+ throw new Error("terminal control changed while waiting to send; refresh status and retry");
813
+ }
814
+ const message = createMessage({
815
+ conversation,
816
+ id: stringValue(options.messageId),
817
+ from: "openclaw",
818
+ to: executor.actor,
819
+ type,
820
+ body: messageBody,
821
+ metadata: {
822
+ executor_kind: executor.kind,
823
+ executor_session: executor.session
824
+ }
825
+ });
826
+ const nextConversation = {
827
+ ...applyMessageToConversation(conversation, message),
828
+ executor,
829
+ claude_session: executor.kind === "claude"
830
+ ? executor.session
831
+ : conversation.claude_session
832
+ };
833
+ if (persist) {
834
+ saveState(statePath, nextConversation);
835
+ appendEvent(logPath, messageEvent(message));
836
+ runtimeLog("info", "message_created", {
837
+ conversation_id: conversation.conversation_id,
838
+ agent: executor.kind,
839
+ executor_session: executor.session,
840
+ message_type: type,
841
+ state_path: statePath,
842
+ event_log_path: logPath,
843
+ message: textSummary(messageBody)
844
+ });
845
+ }
846
+ return {
847
+ conversation,
848
+ executor,
849
+ nativeTakeoverForSend,
850
+ message,
851
+ nextConversation
852
+ };
853
+ }
854
+ function assertTerminalSubmissionRetryOptions(options) {
855
+ const turnId = required(stringValue(options.turn), "--turn is required");
856
+ const allowed = new Set(["turn", "storeDir"]);
857
+ const conflicts = Object.keys(options).filter((option) => options[option] !== undefined && !allowed.has(option));
858
+ if (conflicts.length > 0) {
859
+ throw new Error("send --turn is an exact submission recovery form and cannot be " +
860
+ `combined with ${conflicts.map((option) => `--${option}`).join(", ")}; ` +
861
+ "no terminal input was sent");
862
+ }
863
+ return turnId;
864
+ }
865
+ function terminalSubmissionRetryBaseRecord(input) {
866
+ return {
867
+ schema: TERMINAL_SUBMISSION_RETRY_SCHEMA,
868
+ version: TERMINAL_SUBMISSION_RETRY_VERSION,
869
+ revision: 1,
870
+ attempt_id: input.attemptId,
871
+ mode: input.mode,
872
+ state: input.state,
873
+ store_dir: path.resolve(input.storeDir),
874
+ state_path: path.resolve(input.statePath),
875
+ session_id: sessionIdForConversation(input.conversation),
876
+ turn_id: turnIdForConversation(input.conversation),
877
+ original_message_id: input.originalMessageId,
878
+ active_message_id: input.activeMessageId,
879
+ request_hash: input.requestHash,
880
+ terminal_target: input.terminalControl.target,
881
+ callback_route_fingerprint: callbackRouteFingerprintForConversation(input.conversation) ?? null,
882
+ deferred_foreground_transfer_id: input.deferredTransferId ?? null,
883
+ reserved_at: input.at,
884
+ updated_at: input.at
885
+ };
886
+ }
887
+ function assertTerminalSubmissionRetryAttemptIdentity(input) {
888
+ const attempt = input.attempt;
889
+ if (!attempt)
890
+ return;
891
+ const callbackFingerprint = callbackRouteFingerprintForConversation(input.conversation) ?? null;
892
+ if (attempt.store_dir !== path.resolve(input.storeDir) ||
893
+ attempt.state_path !== path.resolve(input.statePath) ||
894
+ attempt.session_id !== sessionIdForConversation(input.conversation) ||
895
+ attempt.turn_id !== turnIdForConversation(input.conversation) ||
896
+ attempt.original_message_id !== input.originalMessageId ||
897
+ attempt.request_hash !== input.requestHash ||
898
+ attempt.terminal_target !== input.terminalControl.target ||
899
+ attempt.callback_route_fingerprint !== callbackFingerprint ||
900
+ attempt.deferred_foreground_transfer_id !==
901
+ (input.deferredTransferId ?? null)) {
902
+ throw new Error("terminal submission retry authority changed; no terminal input was sent");
903
+ }
904
+ }
905
+ function terminalSubmissionRetryMessageType(submission) {
906
+ const messageType = stringValue(submission.message_type);
907
+ if (messageType !== "task" && messageType !== "answer") {
908
+ throw new Error("terminal submission retry message type is unavailable; no terminal input was sent");
909
+ }
910
+ return messageType;
911
+ }
912
+ function withDeferredTransferSubmissionAuthority(conversation, messageId, transferId) {
913
+ const takeover = isRecord(conversation.native_session_takeover)
914
+ ? conversation.native_session_takeover
915
+ : undefined;
916
+ const submission = terminalBridgeSubmission(conversation);
917
+ if (!takeover || !submission || submission.message_id !== messageId) {
918
+ throw new Error("cannot upgrade deferred transfer authority on a changed submission");
919
+ }
920
+ const existing = stringValue(submission.deferred_foreground_transfer_id);
921
+ if (existing && existing !== transferId) {
922
+ throw new Error("deferred transfer receipt authority conflicts");
923
+ }
924
+ const receiptsValue = takeover.terminal_bridge_submission_receipts;
925
+ if (receiptsValue !== undefined && !Array.isArray(receiptsValue)) {
926
+ throw new Error("terminal submission receipt history is malformed");
927
+ }
928
+ const receipts = (Array.isArray(receiptsValue) ? receiptsValue : []).map((receipt) => {
929
+ if (!isRecord(receipt) || !stringValue(receipt.message_id)) {
930
+ throw new Error("terminal submission receipt history is malformed");
931
+ }
932
+ if (receipt.message_id !== messageId)
933
+ return receipt;
934
+ const receiptTransferId = stringValue(receipt.deferred_foreground_transfer_id);
935
+ if (receiptTransferId && receiptTransferId !== transferId) {
936
+ throw new Error("deferred transfer receipt history conflicts");
937
+ }
938
+ return {
939
+ ...receipt,
940
+ deferred_foreground_transfer_id: transferId
941
+ };
942
+ });
943
+ return {
944
+ ...conversation,
945
+ native_session_takeover: {
946
+ ...takeover,
947
+ deferred_foreground_transfer_id: transferId,
948
+ terminal_bridge_submission: {
949
+ ...submission,
950
+ deferred_foreground_transfer_id: transferId
951
+ },
952
+ terminal_bridge_submission_receipts: receipts
953
+ }
954
+ };
955
+ }
956
+ function terminalSubmissionRetryUnstalled(conversation) {
957
+ const next = {
958
+ ...conversation,
959
+ status: "waiting_for_agent"
960
+ };
961
+ delete next.stalled_at;
962
+ delete next.stalled_reason;
963
+ delete next.failed_at;
964
+ delete next.failure_reason;
965
+ delete next.idle_since;
966
+ return next;
967
+ }
968
+ function withTerminalSubmissionRetryMonitorEpoch(conversation, at) {
969
+ const takeover = isRecord(conversation.native_session_takeover)
970
+ ? conversation.native_session_takeover
971
+ : {};
972
+ const configuredInactivity = Number(takeover.terminal_bridge_inactivity_timeout_minutes);
973
+ const configuredHard = Number(takeover.terminal_bridge_hard_timeout_minutes);
974
+ const inactivityMinutes = Number.isFinite(configuredInactivity) &&
975
+ configuredInactivity > 0
976
+ ? configuredInactivity
977
+ : DEFAULT_AGENT_TIMEOUT_MINUTES;
978
+ const hardMinutes = Number.isFinite(configuredHard) && configuredHard > 0
979
+ ? configuredHard
980
+ : DEFAULT_AGENT_HARD_TIMEOUT_MINUTES;
981
+ return {
982
+ ...conversation,
983
+ native_session_takeover: {
984
+ ...takeover,
985
+ terminal_bridge_started_at: at,
986
+ terminal_bridge_monitor_started_at: at,
987
+ terminal_bridge_last_activity_at: at,
988
+ terminal_bridge_last_activity_reason: "submission retry Enter dispatched",
989
+ terminal_bridge_inactivity_timeout_minutes: inactivityMinutes,
990
+ terminal_bridge_hard_timeout_minutes: hardMinutes,
991
+ terminal_bridge_inactivity_deadline_at: deadlineAt(at, inactivityMinutes),
992
+ terminal_bridge_hard_deadline_at: deadlineAt(at, hardMinutes)
993
+ },
994
+ updated_at: at
995
+ };
996
+ }
997
+ function terminalSubmissionRetryAccepted(input) {
998
+ const currentConversation = assertTerminalSubmissionRetryTurnOpen({
999
+ statePath: input.statePath,
1000
+ exactTurnId: turnIdForConversation(input.conversation)
1001
+ });
1002
+ const messageId = required(stringValue(input.submission.message_id), "terminal submission retry message id is unavailable");
1003
+ const preparedAt = required(stringValue(input.submission.prepared_at), "terminal submission retry prepared timestamp is unavailable");
1004
+ const acceptedConversation = withTerminalBridgeSubmission({
1005
+ conversation: terminalSubmissionRetryUnstalled(currentConversation),
1006
+ messageId,
1007
+ messageType: terminalSubmissionRetryMessageType(input.submission),
1008
+ requestText: input.requestText,
1009
+ status: "agent_accepted",
1010
+ preparedAt,
1011
+ textInjectedAt: stringValue(input.submission.text_injected_at),
1012
+ enterDispatchedAt: stringValue(input.submission.enter_dispatched_at),
1013
+ agentAcceptedAt: input.at,
1014
+ acceptanceEvidence: input.evidence,
1015
+ lastProvenStage: "agent_accepted"
1016
+ });
1017
+ saveState(input.statePath, acceptedConversation);
1018
+ let acceptedAttempt = input.attempt;
1019
+ if (acceptedAttempt && acceptedAttempt.state !== "agent_accepted") {
1020
+ acceptedAttempt = saveTerminalSubmissionRetryForOpenTurn(input.statePath, {
1021
+ ...acceptedAttempt,
1022
+ state: "agent_accepted",
1023
+ agent_accepted_at: input.at,
1024
+ updated_at: input.at
1025
+ }, acceptedAttempt.revision);
1026
+ }
1027
+ mutationDispatchLedger.save(input.scopes, input.resources, {
1028
+ ...input.ledger,
1029
+ ...terminalSubmissionRetryLedgerFields(acceptedAttempt),
1030
+ status: "agent_accepted",
1031
+ text_injected_at: stringValue(input.ledger.text_injected_at),
1032
+ enter_dispatched_at: stringValue(input.ledger.enter_dispatched_at),
1033
+ agent_accepted_at: input.at,
1034
+ acceptance_evidence: input.evidence,
1035
+ dispatcher_pid: null
1036
+ });
1037
+ appendEvent(input.logPath, {
1038
+ ts: input.at,
1039
+ conversation_id: acceptedConversation.conversation_id,
1040
+ event: "terminal_submission_retry_agent_accepted",
1041
+ message_id: messageId,
1042
+ terminal_input_sent: input.terminalInputSent
1043
+ });
1044
+ return { conversation: acceptedConversation, attempt: acceptedAttempt };
1045
+ }
1046
+ function terminalSubmissionRetryTerminalOutcome(input) {
1047
+ const currentConversation = assertTerminalSubmissionRetryTurnOpen({
1048
+ statePath: input.statePath,
1049
+ exactTurnId: turnIdForConversation(input.conversation)
1050
+ });
1051
+ const messageId = required(stringValue(input.submission.message_id), "terminal submission retry message id is unavailable");
1052
+ const preparedAt = required(stringValue(input.submission.prepared_at), "terminal submission retry prepared timestamp is unavailable");
1053
+ const stalled = {
1054
+ ...currentConversation,
1055
+ status: "stalled",
1056
+ stalled_at: input.at,
1057
+ stalled_reason: input.reason,
1058
+ updated_at: input.at
1059
+ };
1060
+ const conversation = withTerminalBridgeSubmission({
1061
+ conversation: stalled,
1062
+ messageId,
1063
+ messageType: terminalSubmissionRetryMessageType(input.submission),
1064
+ requestText: input.requestText,
1065
+ status: input.outcome,
1066
+ preparedAt,
1067
+ textInjectedAt: stringValue(input.submission.text_injected_at),
1068
+ enterDispatchedAt: stringValue(input.submission.enter_dispatched_at),
1069
+ ...(input.outcome === "not_accepted"
1070
+ ? { notAcceptedAt: input.at }
1071
+ : {
1072
+ uncertainAt: input.at,
1073
+ error: input.reason,
1074
+ safeToRetry: false
1075
+ }),
1076
+ lastProvenStage: "enter_dispatched"
1077
+ });
1078
+ saveState(input.statePath, conversation);
1079
+ mutationDispatchLedger.save(input.scopes, input.resources, {
1080
+ ...input.ledger,
1081
+ ...terminalSubmissionRetryLedgerFields(input.attempt),
1082
+ status: input.outcome,
1083
+ ...(input.outcome === "not_accepted"
1084
+ ? {
1085
+ not_accepted_at: input.at,
1086
+ uncertain_at: undefined,
1087
+ error: undefined,
1088
+ safe_to_retry: undefined
1089
+ }
1090
+ : {
1091
+ not_accepted_at: undefined,
1092
+ uncertain_at: input.at,
1093
+ error: input.reason,
1094
+ safe_to_retry: false
1095
+ }),
1096
+ acceptance_evidence: undefined,
1097
+ agent_accepted_at: undefined,
1098
+ dispatcher_pid: null
1099
+ });
1100
+ appendEvent(input.logPath, {
1101
+ ts: input.at,
1102
+ conversation_id: conversation.conversation_id,
1103
+ event: `terminal_submission_retry_${input.outcome}`,
1104
+ message_id: messageId,
1105
+ terminal_input_sent: true,
1106
+ reason: input.reason,
1107
+ do_not_retry: true
1108
+ });
1109
+ return conversation;
1110
+ }
1111
+ function finalizeDeferredTerminalSubmissionRetryAccepted(input) {
1112
+ const conversation = loadState(input.statePath);
1113
+ if (conversation.status === "closed") {
1114
+ throw new Error(`cannot finalize submission retry for closed Turn ` +
1115
+ `${turnIdForConversation(conversation)}; no retry state was changed`);
1116
+ }
1117
+ if (input.attempt &&
1118
+ turnIdForConversation(conversation) !== input.attempt.turn_id) {
1119
+ throw new Error("Turn identity changed during submission retry finalization; no retry " +
1120
+ "state was changed");
1121
+ }
1122
+ const submission = required(terminalBridgeSubmission(conversation), "deferred submission retry acceptance lost its Turn receipt");
1123
+ const ledger = required(mutationDispatchLedger.load(input.scopes, input.resources), "deferred submission retry acceptance lost its dispatch ledger");
1124
+ if (submission.status !== "agent_accepted" ||
1125
+ ledger.status !== "agent_accepted") {
1126
+ throw new Error("deferred submission retry acceptance was not durably finalized");
1127
+ }
1128
+ let attempt = input.attempt;
1129
+ if (attempt && attempt.state !== "agent_accepted") {
1130
+ const acceptedAt = stringValue(submission.agent_accepted_at) ??
1131
+ stringValue(ledger.agent_accepted_at) ?? cliNow().toISOString();
1132
+ attempt = saveTerminalSubmissionRetryForOpenTurn(input.statePath, {
1133
+ ...attempt,
1134
+ state: "agent_accepted",
1135
+ agent_accepted_at: acceptedAt,
1136
+ updated_at: acceptedAt
1137
+ }, attempt.revision);
1138
+ }
1139
+ if (attempt) {
1140
+ mutationDispatchLedger.save(input.scopes, input.resources, {
1141
+ ...ledger,
1142
+ ...terminalSubmissionRetryLedgerFields(attempt)
1143
+ });
1144
+ }
1145
+ return { conversation, attempt };
1146
+ }
1147
+ function reconcileTerminalSubmissionRetryPending(input) {
1148
+ const currentConversation = assertTerminalSubmissionRetryTurnOpen({
1149
+ statePath: input.statePath,
1150
+ exactTurnId: turnIdForConversation(input.conversation)
1151
+ });
1152
+ const { attempt, submission, ledger } = input;
1153
+ const projection = projectTerminalSubmissionRetryPending({
1154
+ attempt,
1155
+ submission,
1156
+ ledger
1157
+ });
1158
+ const { messageId, preparedAt, textInjectedAt, enterDispatchedAt } = projection;
1159
+ if (input.deferred) {
1160
+ const transfer = input.deferred.scope.loadTransfer(input.deferred.transfer.transfer_id);
1161
+ if (!isDeferredForegroundSubmissionRetryPending(transfer)) {
1162
+ throw new Error("deferred submission retry lacks exact pending transfer authority");
1163
+ }
1164
+ input.deferred.transfer = transfer;
1165
+ }
1166
+ const enteredConversation = withTerminalBridgeSubmission({
1167
+ conversation: withTerminalSubmissionRetryMonitorEpoch(terminalSubmissionRetryUnstalled(currentConversation), enterDispatchedAt),
1168
+ messageId,
1169
+ messageType: terminalSubmissionRetryMessageType(submission),
1170
+ requestText: input.requestText,
1171
+ status: "enter_dispatched",
1172
+ preparedAt,
1173
+ textInjectedAt,
1174
+ enterDispatchedAt,
1175
+ lastProvenStage: "enter_dispatched"
1176
+ });
1177
+ saveState(input.statePath, enteredConversation);
1178
+ mutationDispatchLedger.save(input.scopes, input.resources, {
1179
+ ...ledger,
1180
+ ...terminalSubmissionRetryLedgerFields(attempt),
1181
+ status: "enter_dispatched",
1182
+ text_injected_at: textInjectedAt,
1183
+ enter_dispatched_at: enterDispatchedAt,
1184
+ enter_not_attempted_at: undefined,
1185
+ enter_not_attempted_reason: undefined,
1186
+ uncertain_at: undefined,
1187
+ safe_to_retry: undefined,
1188
+ acceptance_evidence: undefined,
1189
+ agent_accepted_at: undefined,
1190
+ not_accepted_at: undefined,
1191
+ dispatcher_pid: null
1192
+ });
1193
+ appendEvent(input.logPath, {
1194
+ ts: enterDispatchedAt,
1195
+ conversation_id: enteredConversation.conversation_id,
1196
+ event: "terminal_submission_retry_pending_reconciled",
1197
+ message_id: messageId,
1198
+ terminal_input_sent: false
1199
+ });
1200
+ return enteredConversation;
1201
+ }
1202
+ const TERMINAL_SUBMISSION_RETRY_LEDGER_KEYS = [
1203
+ "submission_retry_attempt_id",
1204
+ "submission_retry_mode",
1205
+ "submission_retry_state",
1206
+ "submission_retry_revision",
1207
+ "submission_retry_original_message_id",
1208
+ "submission_retry_active_message_id",
1209
+ "submission_retry_reserved_at",
1210
+ "submission_retry_replacement_text_reserved_at",
1211
+ "submission_retry_replacement_text_injected_at",
1212
+ "submission_retry_enter_reserved_at",
1213
+ "submission_retry_enter_dispatched_at"
1214
+ ];
1215
+ function assertTerminalSubmissionRetryDeferredMirror(input) {
1216
+ const { attempt, transfer, submission } = input;
1217
+ const replacement = attempt.mode === "replacement_send";
1218
+ const stateRank = terminalSubmissionRetryTransportRank(attempt);
1219
+ const expectedPreparedAt = stringValue(submission.prepared_at);
1220
+ if (transfer.status !== "uncertain" ||
1221
+ transfer.message_id !== attempt.original_message_id ||
1222
+ transfer.submission_retry_attempt_id !== attempt.attempt_id ||
1223
+ transfer.submission_retry_mode !== attempt.mode ||
1224
+ transfer.submission_retry_message_id !== attempt.active_message_id ||
1225
+ transfer.submission_retry_message_id !== transfer.message_id ||
1226
+ transfer.submission_retry_prepared_at !== expectedPreparedAt ||
1227
+ transfer.submission_retry_prepared_at !== transfer.prepared_at ||
1228
+ transfer.submission_retry_text_reserved_at !==
1229
+ (replacement && stateRank >= 1
1230
+ ? attempt.replacement_text_reserved_at
1231
+ : undefined) ||
1232
+ transfer.submission_retry_text_injected_at !==
1233
+ (replacement && stateRank >= 2
1234
+ ? attempt.replacement_text_injected_at
1235
+ : undefined) ||
1236
+ transfer.submission_retry_enter_reserved_at !==
1237
+ (stateRank >= (replacement ? 3 : 0)
1238
+ ? attempt.enter_reserved_at
1239
+ : undefined) ||
1240
+ transfer.submission_retry_enter_dispatched_at !==
1241
+ (stateRank >= (replacement ? 4 : 1)
1242
+ ? attempt.enter_dispatched_at
1243
+ : undefined) ||
1244
+ (attempt.state === "enter_dispatched"
1245
+ ? transfer.input_stage !== "enter_dispatched" ||
1246
+ transfer.enter_dispatched_at !== attempt.enter_dispatched_at
1247
+ : transfer.input_stage !== "text_injected")) {
1248
+ throw new Error("deferred submission retry mirror conflicts with its durable attempt");
1249
+ }
1250
+ }
1251
+ function assertTerminalSubmissionRetryDeferredMirrorCanReconcile(input) {
1252
+ const { attempt, transfer, submission } = input;
1253
+ const expectedPreparedAt = stringValue(submission.prepared_at);
1254
+ const retryFields = [
1255
+ transfer.submission_retry_attempt_id,
1256
+ transfer.submission_retry_mode,
1257
+ transfer.submission_retry_message_id,
1258
+ transfer.submission_retry_prepared_at,
1259
+ transfer.submission_retry_text_reserved_at,
1260
+ transfer.submission_retry_text_injected_at,
1261
+ transfer.submission_retry_enter_reserved_at,
1262
+ transfer.submission_retry_enter_dispatched_at
1263
+ ];
1264
+ if (transfer.submission_retry_attempt_id === undefined) {
1265
+ const initialAttempt = (attempt.mode === "replacement_send" &&
1266
+ attempt.state === "replacement_reserved") ||
1267
+ (attempt.mode === "exact_draft_enter" &&
1268
+ attempt.state === "enter_reserved");
1269
+ if (!initialAttempt || retryFields.some((value) => value !== undefined)) {
1270
+ throw new Error("deferred submission retry mirror is missing beyond its first recoverable write");
1271
+ }
1272
+ return;
1273
+ }
1274
+ if (transfer.submission_retry_attempt_id !== attempt.attempt_id ||
1275
+ transfer.submission_retry_mode !== attempt.mode ||
1276
+ transfer.submission_retry_message_id !== attempt.active_message_id ||
1277
+ transfer.submission_retry_message_id !== transfer.message_id ||
1278
+ transfer.submission_retry_prepared_at !== expectedPreparedAt ||
1279
+ transfer.submission_retry_prepared_at !== transfer.prepared_at) {
1280
+ throw new Error("deferred submission retry mirror identity conflicts with its durable attempt");
1281
+ }
1282
+ const expectedStages = {
1283
+ submission_retry_text_reserved_at: attempt.mode === "replacement_send"
1284
+ ? attempt.replacement_text_reserved_at
1285
+ : undefined,
1286
+ submission_retry_text_injected_at: attempt.mode === "replacement_send"
1287
+ ? attempt.replacement_text_injected_at
1288
+ : undefined,
1289
+ submission_retry_enter_reserved_at: attempt.enter_reserved_at,
1290
+ submission_retry_enter_dispatched_at: attempt.enter_dispatched_at
1291
+ };
1292
+ for (const field of Object.keys(expectedStages)) {
1293
+ const actual = transfer[field];
1294
+ const expected = expectedStages[field];
1295
+ if (actual !== undefined && actual !== expected) {
1296
+ throw new Error("deferred submission retry mirror stage conflicts with its durable attempt");
1297
+ }
1298
+ }
1299
+ const transferRank = attempt.mode === "replacement_send"
1300
+ ? transfer.submission_retry_enter_dispatched_at !== undefined
1301
+ ? 4
1302
+ : transfer.submission_retry_enter_reserved_at !== undefined
1303
+ ? 3
1304
+ : transfer.submission_retry_text_injected_at !== undefined
1305
+ ? 2
1306
+ : transfer.submission_retry_text_reserved_at !== undefined
1307
+ ? 1
1308
+ : 0
1309
+ : transfer.submission_retry_enter_dispatched_at !== undefined
1310
+ ? 1
1311
+ : transfer.submission_retry_enter_reserved_at !== undefined
1312
+ ? 0
1313
+ : -1;
1314
+ const attemptRank = terminalSubmissionRetryTransportRank(attempt);
1315
+ if (transferRank > attemptRank || attemptRank - transferRank > 1) {
1316
+ throw new Error("deferred submission retry mirror is ahead or more than one stage behind");
1317
+ }
1318
+ }
1319
+ function terminalSubmissionRetryTransportRank(attempt) {
1320
+ const order = attempt.mode === "replacement_send"
1321
+ ? [
1322
+ "replacement_reserved", "replacement_text_reserved",
1323
+ "replacement_text_injected", "enter_reserved", "enter_dispatched"
1324
+ ]
1325
+ : ["enter_reserved", "enter_dispatched"];
1326
+ return order.indexOf(attempt.state);
1327
+ }
1328
+ function reconcileTerminalSubmissionRetryLedgerPrefix(input) {
1329
+ const { attempt, ledger } = input;
1330
+ if (attempt.state === "agent_accepted") {
1331
+ return { ledger, changed: false };
1332
+ }
1333
+ const currentAttemptId = stringValue(ledger.submission_retry_attempt_id);
1334
+ const currentHasRetryFields = TERMINAL_SUBMISSION_RETRY_LEDGER_KEYS.some((key) => ledger[key] !== undefined);
1335
+ if (!currentAttemptId) {
1336
+ if (currentHasRetryFields || attempt.revision !== 1) {
1337
+ throw new Error("terminal submission retry ledger has an unsafe missing retry prefix");
1338
+ }
1339
+ return {
1340
+ ledger: { ...ledger, ...terminalSubmissionRetryLedgerFields(attempt) },
1341
+ changed: true
1342
+ };
1343
+ }
1344
+ const currentState = stringValue(ledger.submission_retry_state);
1345
+ const currentRevision = Number(ledger.submission_retry_revision);
1346
+ const allowedStates = terminalSubmissionRetryLedgerStates(attempt);
1347
+ const same = currentState === attempt.state &&
1348
+ currentRevision === attempt.revision;
1349
+ const immediatelyLagging = currentState === allowedStates.previous &&
1350
+ currentRevision === attempt.revision - 1;
1351
+ if (!same && !immediatelyLagging) {
1352
+ throw new Error("terminal submission retry ledger is ahead, conflicting, or more than one stage behind");
1353
+ }
1354
+ const expectedCurrent = terminalSubmissionRetryLedgerFieldsAtState(attempt, currentState, currentRevision);
1355
+ if (TERMINAL_SUBMISSION_RETRY_LEDGER_KEYS.some((key) => JSON.stringify(ledger[key]) !== JSON.stringify(expectedCurrent[key]))) {
1356
+ throw new Error("terminal submission retry ledger prefix conflicts with its durable attempt");
1357
+ }
1358
+ return same
1359
+ ? { ledger, changed: false }
1360
+ : {
1361
+ ledger: { ...ledger, ...terminalSubmissionRetryLedgerFields(attempt) },
1362
+ changed: true
1363
+ };
1364
+ }
1365
+ function terminalSubmissionRetryLedgerStates(attempt) {
1366
+ const order = attempt.mode === "replacement_send"
1367
+ ? [
1368
+ "replacement_reserved", "replacement_text_reserved",
1369
+ "replacement_text_injected", "enter_reserved", "enter_dispatched"
1370
+ ]
1371
+ : ["enter_reserved", "enter_dispatched"];
1372
+ const index = order.indexOf(attempt.state);
1373
+ return { previous: index > 0 ? order[index - 1] : undefined };
1374
+ }
1375
+ function terminalSubmissionRetryLedgerFieldsAtState(attempt, state, revision) {
1376
+ const fields = {
1377
+ ...terminalSubmissionRetryLedgerFields(attempt),
1378
+ submission_retry_state: state,
1379
+ submission_retry_revision: revision
1380
+ };
1381
+ const rank = {
1382
+ replacement_reserved: 0,
1383
+ replacement_text_reserved: 1,
1384
+ replacement_text_injected: 2,
1385
+ enter_reserved: 3,
1386
+ enter_dispatched: 4,
1387
+ agent_accepted: 5
1388
+ };
1389
+ if (rank[state] < rank.replacement_text_reserved) {
1390
+ delete fields.submission_retry_replacement_text_reserved_at;
1391
+ }
1392
+ if (rank[state] < rank.replacement_text_injected) {
1393
+ delete fields.submission_retry_replacement_text_injected_at;
1394
+ }
1395
+ if (rank[state] < rank.enter_reserved) {
1396
+ delete fields.submission_retry_enter_reserved_at;
1397
+ }
1398
+ if (rank[state] < rank.enter_dispatched) {
1399
+ delete fields.submission_retry_enter_dispatched_at;
1400
+ }
1401
+ return fields;
1402
+ }
1403
+ function printTerminalSubmissionRetryOutcome(input) {
1404
+ printJson({
1405
+ session_id: sessionIdForConversation(input.conversation),
1406
+ turn_id: turnIdForConversation(input.conversation),
1407
+ conversation: input.conversation,
1408
+ delivered: input.outcome === "agent_accepted",
1409
+ delivery_receipt: input.outcome === "agent_accepted"
1410
+ ? "agent_accepted"
1411
+ : input.outcome === "enter_dispatched" ||
1412
+ input.outcome === "not_accepted"
1413
+ ? "enter_dispatched"
1414
+ : "uncertain",
1415
+ status: input.outcome === "agent_accepted"
1416
+ ? "async_pending"
1417
+ : input.outcome === "enter_dispatched"
1418
+ ? "submission_pending"
1419
+ : input.outcome === "not_accepted"
1420
+ ? "submission_not_accepted"
1421
+ : "submission_uncertain",
1422
+ submission_outcome: input.outcome === "enter_dispatched"
1423
+ ? "pending_acceptance"
1424
+ : input.outcome === "not_accepted"
1425
+ ? "not_accepted"
1426
+ : input.outcome === "refused"
1427
+ ? "uncertain"
1428
+ : "agent_accepted",
1429
+ replayed: true,
1430
+ terminal_control: input.terminalControl,
1431
+ terminal_input_sent: input.terminalInputSent,
1432
+ callback_expected: ["refused", "not_accepted"].includes(input.outcome)
1433
+ ? false
1434
+ : callbackExpectedForConversation(input.conversation),
1435
+ ...(input.attempt
1436
+ ? {
1437
+ submission_retry_attempt_id: input.attempt.attempt_id,
1438
+ submission_retry_state: input.attempt.state
1439
+ }
1440
+ : {}),
1441
+ do_not_retry: input.outcome !== "agent_accepted",
1442
+ reason: input.reason
1443
+ });
1444
+ }
1445
+ function loadExactTerminalSubmissionRetryTurn(input) {
1446
+ const conversation = loadState(input.statePath);
1447
+ if (turnIdForConversation(conversation) !== input.exactTurnId) {
1448
+ throw new Error("Turn identity changed during submission retry; no terminal input was " +
1449
+ "sent and no retry state was changed");
1450
+ }
1451
+ return conversation;
1452
+ }
1453
+ function assertTerminalSubmissionRetryTurnOpen(input) {
1454
+ const conversation = loadExactTerminalSubmissionRetryTurn(input);
1455
+ if (conversation.status === "closed") {
1456
+ throw new Error(`cannot retry submission for closed Turn ${input.exactTurnId}; no ` +
1457
+ "terminal input was sent and no retry state was changed");
1458
+ }
1459
+ return conversation;
1460
+ }
1461
+ function saveTerminalSubmissionRetryForOpenTurn(statePath, candidate, expectedRevision) {
1462
+ assertTerminalSubmissionRetryTurnOpen({
1463
+ statePath,
1464
+ exactTurnId: candidate.turn_id
1465
+ });
1466
+ return saveTerminalSubmissionRetry(statePath, candidate, expectedRevision);
1467
+ }
1468
+ function loadTerminalSubmissionRetryLockedAuthority(input) {
1469
+ const { options, exactTurnId, statePath, logPath, storeDir, terminalControl } = input.invocation;
1470
+ const route = bindTerminalDispatchRoute(input.scopes, input.resources);
1471
+ assertTerminalDispatchRouteMatches(route, {
1472
+ terminalControl,
1473
+ terminalKey: terminalBridgeRuntimeKey(terminalControl),
1474
+ storeDir,
1475
+ statePath,
1476
+ logPath
1477
+ });
1478
+ let conversation = assertTerminalSubmissionRetryTurnOpen({
1479
+ statePath,
1480
+ exactTurnId
1481
+ });
1482
+ const takeover = isRecord(conversation.native_session_takeover)
1483
+ ? conversation.native_session_takeover
1484
+ : undefined;
1485
+ const lockedControl = terminalControlFromTakeover(takeover);
1486
+ if (turnIdForConversation(conversation) !== exactTurnId ||
1487
+ executorForConversation(conversation).kind !== "codex" ||
1488
+ !lockedControl ||
1489
+ !terminalControlsShareIncarnation(lockedControl, terminalControl)) {
1490
+ throw new Error("Turn or terminal authority changed before submission retry; no terminal input was sent");
1491
+ }
1492
+ let submission = terminalBridgeSubmission(conversation);
1493
+ if (!submission) {
1494
+ throw new Error("Turn has no terminal submission receipt to recover; no terminal input was sent");
1495
+ }
1496
+ const currentMessageId = required(stringValue(submission.message_id), "terminal submission retry message id is unavailable");
1497
+ const attempt = loadTerminalSubmissionRetry(statePath);
1498
+ const originalMessageId = attempt?.original_message_id ?? currentMessageId;
1499
+ const requestText = terminalSubmissionPayload(required(stringValue(takeover?.terminal_bridge_request_text), "terminal submission retry request text is unavailable"));
1500
+ const requestHash = required(terminalBridgeRequestFingerprint(requestText), "terminal submission retry request hash is unavailable");
1501
+ const messageType = terminalSubmissionRetryMessageType(submission);
1502
+ const messageBodyHash = createHash("sha256").update(requestText).digest("hex");
1503
+ validateStoredTerminalSubmissionMatch({
1504
+ owner: conversation,
1505
+ receipt: submission,
1506
+ options,
1507
+ terminalControl,
1508
+ requestText,
1509
+ requestHash,
1510
+ expectedStoreDir: storeDir,
1511
+ expectedSessionId: sessionIdForConversation(conversation),
1512
+ expectedTurnId: exactTurnId,
1513
+ expectedMessageType: messageType,
1514
+ expectedStatePath: statePath
1515
+ });
1516
+ const ledger = mutationDispatchLedger.load(input.scopes, input.resources);
1517
+ assertTerminalSubmissionRetryLedgerAuthority({
1518
+ ledger,
1519
+ conversation,
1520
+ exactTurnId,
1521
+ currentMessageId,
1522
+ messageType,
1523
+ messageBodyHash,
1524
+ requestHash,
1525
+ storeDir,
1526
+ statePath,
1527
+ terminalControl
1528
+ });
1529
+ let currentLedger = ledger;
1530
+ const takeoverDeferredTransferId = stringValue(takeover?.deferred_foreground_transfer_id);
1531
+ const ledgerDeferredTransferId = stringValue(currentLedger.deferred_foreground_transfer_id);
1532
+ if (takeoverDeferredTransferId &&
1533
+ takeoverDeferredTransferId === ledgerDeferredTransferId &&
1534
+ stringValue(submission.deferred_foreground_transfer_id) === undefined) {
1535
+ conversation = withDeferredTransferSubmissionAuthority(conversation, currentMessageId, takeoverDeferredTransferId);
1536
+ saveState(statePath, conversation);
1537
+ submission = required(terminalBridgeSubmission(conversation), "deferred terminal submission receipt upgrade failed");
1538
+ }
1539
+ const expectedCallbackFingerprint = callbackRouteFingerprintForConversation(conversation) ?? null;
1540
+ if ((currentLedger.callback_route_fingerprint ?? null) !==
1541
+ expectedCallbackFingerprint ||
1542
+ (submission.callback_route_fingerprint ?? null) !==
1543
+ expectedCallbackFingerprint) {
1544
+ throw new Error("terminal submission callback route changed; no terminal input was sent");
1545
+ }
1546
+ const deferredTransferId = exactTerminalSubmissionRetryDeferredTransferId({
1547
+ takeover,
1548
+ submission,
1549
+ ledger: currentLedger
1550
+ });
1551
+ assertTerminalSubmissionRetryAttemptIdentity({
1552
+ attempt,
1553
+ conversation,
1554
+ storeDir,
1555
+ statePath,
1556
+ originalMessageId,
1557
+ requestHash,
1558
+ terminalControl,
1559
+ deferredTransferId
1560
+ });
1561
+ assertTerminalSubmissionRetryGeneration({
1562
+ attempt,
1563
+ takeover,
1564
+ currentMessageId,
1565
+ originalMessageId
1566
+ });
1567
+ const callbackDelivery = isRecord(conversation.callback_delivery)
1568
+ ? conversation.callback_delivery
1569
+ : undefined;
1570
+ return {
1571
+ conversation,
1572
+ takeover,
1573
+ submission,
1574
+ ledger: currentLedger,
1575
+ attempt,
1576
+ currentMessageId,
1577
+ originalMessageId,
1578
+ requestText,
1579
+ requestHash,
1580
+ deferredTransferId,
1581
+ lifecycleSettled: callbackDelivery?.status === "delivered" ||
1582
+ isTerminalDispatchOwnerReleasedStatus(effectiveTurnStatus(conversation))
1583
+ };
1584
+ }
1585
+ function assertTerminalSubmissionRetryLedgerAuthority(input) {
1586
+ const { ledger } = input;
1587
+ if (!ledger || terminalDispatchLedgerLooksLifecycle(ledger) ||
1588
+ stringValue(ledger.conversation_id) !== input.conversation.conversation_id ||
1589
+ stringValue(ledger.session_id) !== sessionIdForConversation(input.conversation) ||
1590
+ stringValue(ledger.turn_id) !== input.exactTurnId ||
1591
+ stringValue(ledger.generation_id) !== input.currentMessageId ||
1592
+ stringValue(ledger.message_id) !== input.currentMessageId ||
1593
+ stringValue(ledger.message_type) !== input.messageType ||
1594
+ stringValue(ledger.message_body_hash) !== input.messageBodyHash ||
1595
+ stringValue(ledger.executor_kind) !== "codex" ||
1596
+ stringValue(ledger.request_hash) !== input.requestHash ||
1597
+ path.resolve(stringValue(ledger.store_dir) ?? "") !==
1598
+ path.resolve(input.storeDir) ||
1599
+ !sameCanonicalStatePath(ledger.state_path, input.statePath) ||
1600
+ !terminalDispatchRecordMatchesControl(ledger, input.terminalControl)) {
1601
+ throw new Error("terminal dispatch ledger does not match the exact Turn generation; no terminal input was sent");
1602
+ }
1603
+ }
1604
+ function exactTerminalSubmissionRetryDeferredTransferId(input) {
1605
+ const references = [
1606
+ stringValue(input.takeover?.deferred_foreground_transfer_id),
1607
+ stringValue(input.submission.deferred_foreground_transfer_id),
1608
+ stringValue(input.ledger.deferred_foreground_transfer_id)
1609
+ ];
1610
+ const hasDeferred = references.some((value) => value !== undefined);
1611
+ if (hasDeferred &&
1612
+ (references.some((value) => value === undefined) ||
1613
+ new Set(references).size !== 1)) {
1614
+ throw new Error("deferred transfer references disagree across Turn and ledger; no terminal input was sent");
1615
+ }
1616
+ return hasDeferred ? references[0] : undefined;
1617
+ }
1618
+ function assertTerminalSubmissionRetryGeneration(input) {
1619
+ const currentGenerationEligible = input.attempt?.mode === "replacement_send"
1620
+ ? input.attempt.state === "replacement_reserved"
1621
+ ? [input.attempt.original_message_id, input.attempt.active_message_id]
1622
+ .includes(input.currentMessageId)
1623
+ : input.currentMessageId === input.attempt.active_message_id
1624
+ : input.currentMessageId === input.originalMessageId;
1625
+ if (stringValue(input.takeover?.terminal_bridge_message_id) !==
1626
+ input.currentMessageId ||
1627
+ !currentGenerationEligible) {
1628
+ throw new Error("terminal submission retry generation changed; no terminal input was sent");
1629
+ }
1630
+ }
1631
+ function reconcileTerminalSubmissionRetryDeferredTransfer(input) {
1632
+ const { attempt } = input;
1633
+ if (!attempt)
1634
+ return input.transfer;
1635
+ const application = deferredForegroundApplication(input.invocation.options, input.invocation.live);
1636
+ const boundary = deferredForegroundBoundaryProjection(input.boundary);
1637
+ let transfer = input.transfer;
1638
+ if (transfer.submission_retry_attempt_id === undefined) {
1639
+ if (attempt.mode === "replacement_send" &&
1640
+ attempt.state === "replacement_reserved") {
1641
+ transfer = application.reserveSubmissionRetry({
1642
+ scope: input.scope,
1643
+ boundary,
1644
+ attemptId: attempt.attempt_id,
1645
+ mode: attempt.mode,
1646
+ messageId: attempt.active_message_id,
1647
+ preparedAt: required(stringValue(input.submission.prepared_at), "deferred retry prepared timestamp is unavailable")
1648
+ });
1649
+ }
1650
+ else if (attempt.mode === "exact_draft_enter" &&
1651
+ attempt.state === "enter_reserved") {
1652
+ transfer = application.reserveSubmissionRetry({
1653
+ scope: input.scope,
1654
+ boundary,
1655
+ attemptId: attempt.attempt_id,
1656
+ mode: attempt.mode,
1657
+ messageId: attempt.active_message_id,
1658
+ preparedAt: required(stringValue(input.submission.prepared_at), "deferred retry prepared timestamp is unavailable")
1659
+ });
1660
+ transfer = application.advanceSubmissionRetry({
1661
+ scope: input.scope,
1662
+ boundary,
1663
+ attemptId: attempt.attempt_id,
1664
+ messageId: attempt.active_message_id,
1665
+ stage: "enter_reserved",
1666
+ at: attempt.enter_reserved_at
1667
+ });
1668
+ }
1669
+ else {
1670
+ throw new Error("deferred retry mirror is incomplete after an irreversible boundary; no terminal input was sent");
1671
+ }
1672
+ }
1673
+ if (!transfer.submission_retry_attempt_id)
1674
+ return transfer;
1675
+ if (attempt.mode === "replacement_send" &&
1676
+ [
1677
+ "replacement_text_reserved", "replacement_text_injected",
1678
+ "enter_reserved", "enter_dispatched"
1679
+ ].includes(attempt.state) &&
1680
+ !transfer.submission_retry_text_reserved_at) {
1681
+ transfer = application.advanceSubmissionRetry({
1682
+ scope: input.scope,
1683
+ boundary,
1684
+ attemptId: attempt.attempt_id,
1685
+ messageId: attempt.active_message_id,
1686
+ stage: "text_reserved",
1687
+ at: attempt.replacement_text_reserved_at
1688
+ });
1689
+ }
1690
+ if (["replacement_text_injected", "enter_reserved", "enter_dispatched"]
1691
+ .includes(attempt.state) &&
1692
+ !transfer.submission_retry_text_injected_at) {
1693
+ transfer = application.advanceSubmissionRetry({
1694
+ scope: input.scope,
1695
+ boundary,
1696
+ attemptId: attempt.attempt_id,
1697
+ messageId: attempt.active_message_id,
1698
+ stage: "text_injected",
1699
+ at: attempt.replacement_text_injected_at ??
1700
+ attempt.enter_reserved_at ?? attempt.reserved_at
1701
+ });
1702
+ }
1703
+ if (["enter_reserved", "enter_dispatched"].includes(attempt.state) &&
1704
+ !transfer.submission_retry_enter_reserved_at) {
1705
+ transfer = application.advanceSubmissionRetry({
1706
+ scope: input.scope,
1707
+ boundary,
1708
+ attemptId: attempt.attempt_id,
1709
+ messageId: attempt.active_message_id,
1710
+ stage: "enter_reserved",
1711
+ at: attempt.enter_reserved_at
1712
+ });
1713
+ }
1714
+ if (attempt.state === "enter_dispatched" &&
1715
+ !transfer.submission_retry_enter_dispatched_at) {
1716
+ transfer = application.advanceSubmissionRetry({
1717
+ scope: input.scope,
1718
+ boundary,
1719
+ attemptId: attempt.attempt_id,
1720
+ messageId: attempt.active_message_id,
1721
+ stage: "enter_dispatched",
1722
+ at: attempt.enter_dispatched_at
1723
+ });
1724
+ }
1725
+ return transfer;
1726
+ }
1727
+ function prepareTerminalSubmissionRetryDeferredContext(input) {
1728
+ assertTerminalSubmissionRetryTurnOpen({
1729
+ statePath: input.invocation.statePath,
1730
+ exactTurnId: input.invocation.exactTurnId
1731
+ });
1732
+ const { deferredTransferId, attempt, submission } = input.authority;
1733
+ if (!deferredTransferId)
1734
+ return { ledger: input.authority.ledger };
1735
+ const scope = bindDeferredForegroundApplicationScope(input.scopes, input.resources);
1736
+ const transfer = scope.loadTransfer(deferredTransferId);
1737
+ const boundary = deferredRecoveryAdapter.deferredCodexBoundaryFromTransfer(deferredForegroundRecoveryAdapterPorts(), { terminal: input.invocation.live, transfer });
1738
+ const projectedBoundary = deferredForegroundBoundaryProjection(boundary);
1739
+ scope.assertBoundary(projectedBoundary);
1740
+ if (attempt && attempt.state !== "agent_accepted") {
1741
+ assertTerminalSubmissionRetryDeferredMirrorCanReconcile({
1742
+ attempt,
1743
+ transfer,
1744
+ submission
1745
+ });
1746
+ }
1747
+ const ledgerMirror = attempt && attempt.state !== "agent_accepted"
1748
+ ? reconcileTerminalSubmissionRetryLedgerPrefix({
1749
+ attempt,
1750
+ ledger: input.authority.ledger
1751
+ })
1752
+ : undefined;
1753
+ const currentTransfer = reconcileTerminalSubmissionRetryDeferredTransfer({
1754
+ invocation: input.invocation,
1755
+ scope,
1756
+ boundary,
1757
+ transfer,
1758
+ attempt,
1759
+ submission
1760
+ });
1761
+ assertTerminalSubmissionRetryDeferredTransferAuthority({
1762
+ invocation: input.invocation,
1763
+ authority: input.authority,
1764
+ transfer: currentTransfer,
1765
+ attempt
1766
+ });
1767
+ deferredForegroundApplication(input.invocation.options, input.invocation.live).assertTransferAuthority(scope, currentTransfer, projectedBoundary);
1768
+ let ledger = input.authority.ledger;
1769
+ if (attempt && attempt.state !== "agent_accepted") {
1770
+ assertTerminalSubmissionRetryDeferredMirror({
1771
+ attempt,
1772
+ transfer: currentTransfer,
1773
+ submission
1774
+ });
1775
+ if (ledgerMirror?.changed) {
1776
+ mutationDispatchLedger.save(input.scopes, input.resources, ledgerMirror.ledger);
1777
+ ledger = ledgerMirror.ledger;
1778
+ }
1779
+ }
1780
+ return {
1781
+ deferred: { scope, transfer: currentTransfer, boundary },
1782
+ ledger
1783
+ };
1784
+ }
1785
+ function assertTerminalSubmissionRetryDeferredTransferAuthority(input) {
1786
+ const { transfer, attempt } = input;
1787
+ const plannedReplacement = Boolean(attempt?.mode === "replacement_send" &&
1788
+ attempt.state === "replacement_reserved" &&
1789
+ transfer.submission_retry_text_reserved_at === undefined);
1790
+ const activeMessageId = deferredForegroundActiveMessageId(transfer);
1791
+ const currentMessageId = input.authority.currentMessageId;
1792
+ if (transfer.turn_id !== input.invocation.exactTurnId ||
1793
+ transfer.target_session_id !==
1794
+ sessionIdForConversation(input.authority.conversation) ||
1795
+ transfer.request_hash !== input.authority.requestHash ||
1796
+ !sameCanonicalStatePath(transfer.state_path, input.invocation.statePath) ||
1797
+ transfer.message_id !== input.authority.originalMessageId ||
1798
+ (plannedReplacement
1799
+ ? ![input.authority.originalMessageId, attempt.active_message_id]
1800
+ .includes(currentMessageId)
1801
+ : activeMessageId !== currentMessageId) ||
1802
+ (attempt
1803
+ ? transfer.submission_retry_attempt_id !== attempt.attempt_id ||
1804
+ transfer.submission_retry_message_id !== attempt.active_message_id
1805
+ : transfer.submission_retry_attempt_id !== undefined)) {
1806
+ throw new Error("deferred submission retry transfer authority changed; no terminal input was sent");
1807
+ }
1808
+ }
1809
+ function reconcileTerminalSubmissionRetryDeferredPending(context, state) {
1810
+ if (!context.deferred || state.attempt?.state !== "enter_dispatched" ||
1811
+ state.submission.status === "agent_accepted" ||
1812
+ state.ledger.status === "agent_accepted") {
1813
+ return false;
1814
+ }
1815
+ state.conversation = reconcileTerminalSubmissionRetryPending({
1816
+ conversation: state.conversation,
1817
+ submission: state.submission,
1818
+ ledger: state.ledger,
1819
+ attempt: state.attempt,
1820
+ requestText: context.requestText,
1821
+ statePath: context.invocation.statePath,
1822
+ logPath: context.invocation.logPath,
1823
+ scopes: context.scopes,
1824
+ resources: context.resources,
1825
+ deferred: context.deferred
1826
+ });
1827
+ state.submission = required(terminalBridgeSubmission(state.conversation), "terminal submission retry receipt disappeared during pending reconciliation");
1828
+ state.ledger = required(mutationDispatchLedger.load(context.scopes, context.resources), "terminal submission retry ledger disappeared during pending reconciliation");
1829
+ return true;
1830
+ }
1831
+ async function recoverPartialTerminalSubmissionRetryAcceptance(context, state) {
1832
+ assertTerminalSubmissionRetryTurnOpen({
1833
+ statePath: context.invocation.statePath,
1834
+ exactTurnId: context.invocation.exactTurnId
1835
+ });
1836
+ const partialAcceptance = state.submission.status === "agent_accepted" ||
1837
+ state.ledger.status === "agent_accepted" ||
1838
+ state.attempt?.state === "agent_accepted";
1839
+ if (!partialAcceptance)
1840
+ return false;
1841
+ const { options, terminalControl, live, statePath, logPath, storeDir } = context.invocation;
1842
+ if (context.deferred &&
1843
+ ["uncertain", "dispatch_started"].includes(context.deferred.transfer.status)) {
1844
+ const recovered = await deferredRecoveryAdapter
1845
+ .recoverAcceptedDeferredForegroundDispatch(deferredForegroundRecoveryAdapterPorts(), {
1846
+ options,
1847
+ scope: context.deferred.scope,
1848
+ storeDir,
1849
+ terminal: live,
1850
+ transfer: context.deferred.transfer,
1851
+ boundary: context.deferred.boundary
1852
+ });
1853
+ assertTerminalSubmissionRetryTurnOpen({
1854
+ statePath,
1855
+ exactTurnId: context.invocation.exactTurnId
1856
+ });
1857
+ if (!recovered) {
1858
+ printTerminalSubmissionRetryOutcome({
1859
+ conversation: state.conversation,
1860
+ terminalControl,
1861
+ attempt: state.attempt,
1862
+ outcome: "refused",
1863
+ terminalInputSent: false,
1864
+ reason: "Partial deferred acceptance lacks current native evidence; no terminal input was sent."
1865
+ });
1866
+ return true;
1867
+ }
1868
+ const finalized = finalizeDeferredTerminalSubmissionRetryAccepted({
1869
+ statePath,
1870
+ scopes: context.scopes,
1871
+ resources: context.resources,
1872
+ attempt: state.attempt
1873
+ });
1874
+ state.conversation = finalized.conversation;
1875
+ state.attempt = finalized.attempt;
1876
+ startTerminalBridgeMonitorForConversation({
1877
+ conversation: state.conversation,
1878
+ statePath,
1879
+ logPath,
1880
+ options
1881
+ });
1882
+ printTerminalSubmissionRetryOutcome({
1883
+ conversation: state.conversation,
1884
+ terminalControl,
1885
+ attempt: state.attempt,
1886
+ outcome: "agent_accepted",
1887
+ terminalInputSent: false,
1888
+ reason: "Deferred native acceptance, source/target Sessions, and monitoring were reconciled without terminal input."
1889
+ });
1890
+ return true;
1891
+ }
1892
+ const durableEvidence = await context.execution.detectAcceptance({
1893
+ executor: "codex",
1894
+ conversation: state.conversation,
1895
+ terminalControl
1896
+ });
1897
+ assertTerminalSubmissionRetryTurnOpen({
1898
+ statePath,
1899
+ exactTurnId: context.invocation.exactTurnId
1900
+ });
1901
+ if (!durableEvidence) {
1902
+ printTerminalSubmissionRetryOutcome({
1903
+ conversation: state.conversation,
1904
+ terminalControl,
1905
+ attempt: state.attempt,
1906
+ outcome: "refused",
1907
+ terminalInputSent: false,
1908
+ reason: "Partial acceptance metadata lacks current native evidence; no terminal input was sent."
1909
+ });
1910
+ return true;
1911
+ }
1912
+ const repaired = terminalSubmissionRetryAccepted({
1913
+ conversation: state.conversation,
1914
+ submission: state.submission,
1915
+ ledger: state.ledger,
1916
+ evidence: durableEvidence,
1917
+ requestText: context.requestText,
1918
+ at: stringValue(state.submission.agent_accepted_at) ??
1919
+ stringValue(state.ledger.agent_accepted_at) ?? cliNow().toISOString(),
1920
+ statePath,
1921
+ logPath,
1922
+ scopes: context.scopes,
1923
+ resources: context.resources,
1924
+ attempt: state.attempt,
1925
+ terminalInputSent: false
1926
+ });
1927
+ state.conversation = repaired.conversation;
1928
+ state.attempt = repaired.attempt;
1929
+ startTerminalBridgeMonitorForConversation({
1930
+ conversation: state.conversation,
1931
+ statePath,
1932
+ logPath,
1933
+ options
1934
+ });
1935
+ printTerminalSubmissionRetryOutcome({
1936
+ conversation: state.conversation,
1937
+ terminalControl,
1938
+ attempt: state.attempt,
1939
+ outcome: "agent_accepted",
1940
+ terminalInputSent: false,
1941
+ reason: "Partial durable acceptance was reconciled without terminal input and monitoring was ensured."
1942
+ });
1943
+ return true;
1944
+ }
1945
+ function terminalSubmissionRetryIsEligible(context, state) {
1946
+ const legacyEligible = !state.attempt &&
1947
+ state.conversation.status === "stalled" &&
1948
+ state.submission.status === "uncertain" &&
1949
+ state.submission.last_proven_stage === "text_injected" &&
1950
+ context.currentMessageId === context.originalMessageId;
1951
+ if (legacyEligible || (state.attempt && state.attempt.state !== "agent_accepted")) {
1952
+ return true;
1953
+ }
1954
+ printTerminalSubmissionRetryOutcome({
1955
+ conversation: state.conversation,
1956
+ terminalControl: context.invocation.terminalControl,
1957
+ attempt: state.attempt,
1958
+ outcome: "refused",
1959
+ terminalInputSent: false,
1960
+ reason: "The Turn is not an eligible incomplete submission recovery; no terminal input was sent."
1961
+ });
1962
+ return false;
1963
+ }
1964
+ async function recoverTerminalSubmissionRetryAcceptance(context, state) {
1965
+ const { options, terminalControl, live, statePath, logPath, storeDir } = context.invocation;
1966
+ const deferredAccepted = context.deferred
1967
+ ? await deferredRecoveryAdapter.recoverAcceptedDeferredForegroundDispatch(deferredForegroundRecoveryAdapterPorts(), {
1968
+ options,
1969
+ scope: context.deferred.scope,
1970
+ storeDir,
1971
+ terminal: live,
1972
+ transfer: context.deferred.transfer,
1973
+ boundary: context.deferred.boundary
1974
+ })
1975
+ : false;
1976
+ assertTerminalSubmissionRetryTurnOpen({
1977
+ statePath,
1978
+ exactTurnId: context.invocation.exactTurnId
1979
+ });
1980
+ if (deferredAccepted) {
1981
+ const finalized = finalizeDeferredTerminalSubmissionRetryAccepted({
1982
+ statePath,
1983
+ scopes: context.scopes,
1984
+ resources: context.resources,
1985
+ attempt: state.attempt
1986
+ });
1987
+ state.conversation = finalized.conversation;
1988
+ state.attempt = finalized.attempt;
1989
+ startTerminalBridgeMonitorForConversation({
1990
+ conversation: state.conversation,
1991
+ statePath,
1992
+ logPath,
1993
+ options
1994
+ });
1995
+ printTerminalSubmissionRetryOutcome({
1996
+ conversation: state.conversation,
1997
+ terminalControl,
1998
+ attempt: state.attempt,
1999
+ outcome: "agent_accepted",
2000
+ terminalInputSent: false,
2001
+ reason: "Deferred native acceptance and source/target Session bindings were reconciled without terminal input."
2002
+ });
2003
+ return true;
2004
+ }
2005
+ const acceptedEvidence = context.deferred
2006
+ ? undefined
2007
+ : await context.execution.detectAcceptance({
2008
+ executor: "codex",
2009
+ conversation: state.conversation,
2010
+ terminalControl
2011
+ });
2012
+ assertTerminalSubmissionRetryTurnOpen({
2013
+ statePath,
2014
+ exactTurnId: context.invocation.exactTurnId
2015
+ });
2016
+ if (!acceptedEvidence)
2017
+ return false;
2018
+ const repaired = terminalSubmissionRetryAccepted({
2019
+ conversation: state.conversation,
2020
+ submission: state.submission,
2021
+ ledger: state.ledger,
2022
+ evidence: acceptedEvidence,
2023
+ requestText: context.requestText,
2024
+ at: cliNow().toISOString(),
2025
+ statePath,
2026
+ logPath,
2027
+ scopes: context.scopes,
2028
+ resources: context.resources,
2029
+ attempt: state.attempt,
2030
+ terminalInputSent: false
2031
+ });
2032
+ state.conversation = repaired.conversation;
2033
+ state.attempt = repaired.attempt;
2034
+ startTerminalBridgeMonitorForConversation({
2035
+ conversation: state.conversation,
2036
+ statePath,
2037
+ logPath,
2038
+ options
2039
+ });
2040
+ printTerminalSubmissionRetryOutcome({
2041
+ conversation: state.conversation,
2042
+ terminalControl,
2043
+ attempt: state.attempt,
2044
+ outcome: "agent_accepted",
2045
+ terminalInputSent: false,
2046
+ reason: "Native Codex acceptance was reconciled; no terminal input was sent."
2047
+ });
2048
+ return true;
2049
+ }
2050
+ function finishPendingTerminalSubmissionRetry(context, state, alreadyReconciled) {
2051
+ if (state.attempt?.state !== "enter_dispatched")
2052
+ return false;
2053
+ assertTerminalSubmissionRetryTurnOpen({
2054
+ statePath: context.invocation.statePath,
2055
+ exactTurnId: context.invocation.exactTurnId
2056
+ });
2057
+ if (!alreadyReconciled) {
2058
+ state.conversation = reconcileTerminalSubmissionRetryPending({
2059
+ conversation: state.conversation,
2060
+ submission: state.submission,
2061
+ ledger: state.ledger,
2062
+ attempt: state.attempt,
2063
+ requestText: context.requestText,
2064
+ statePath: context.invocation.statePath,
2065
+ logPath: context.invocation.logPath,
2066
+ scopes: context.scopes,
2067
+ resources: context.resources,
2068
+ deferred: context.deferred
2069
+ });
2070
+ }
2071
+ startTerminalBridgeMonitorForConversation({
2072
+ conversation: state.conversation,
2073
+ statePath: context.invocation.statePath,
2074
+ logPath: context.invocation.logPath,
2075
+ options: context.invocation.options
2076
+ });
2077
+ printTerminalSubmissionRetryOutcome({
2078
+ conversation: state.conversation,
2079
+ terminalControl: context.invocation.terminalControl,
2080
+ attempt: state.attempt,
2081
+ outcome: "enter_dispatched",
2082
+ terminalInputSent: false,
2083
+ reason: "The durable retry Enter was reconciled without additional terminal input; native acceptance remains pending."
2084
+ });
2085
+ return true;
2086
+ }
2087
+ function terminalSubmissionRetryHasInputAuthority(context, state) {
2088
+ if (!context.deferred ||
2089
+ validateCodexRolloutAcceptanceAnchor(context.takeover?.codex_rollout_acceptance_anchor)?.version === 3) {
2090
+ return true;
2091
+ }
2092
+ printTerminalSubmissionRetryOutcome({
2093
+ conversation: state.conversation,
2094
+ terminalControl: context.invocation.terminalControl,
2095
+ attempt: state.attempt,
2096
+ outcome: "refused",
2097
+ terminalInputSent: false,
2098
+ reason: "Deferred submission retry requires a candidate-set Codex acceptance anchor before any terminal input; no terminal input was sent."
2099
+ });
2100
+ return false;
2101
+ }
2102
+ async function runTerminalSubmissionRetryNoInputRecovery(context, state) {
2103
+ const pendingRetryReconciled = reconcileTerminalSubmissionRetryDeferredPending(context, state);
2104
+ if (await recoverPartialTerminalSubmissionRetryAcceptance(context, state)) {
2105
+ return true;
2106
+ }
2107
+ if (!terminalSubmissionRetryIsEligible(context, state))
2108
+ return true;
2109
+ if (await recoverTerminalSubmissionRetryAcceptance(context, state)) {
2110
+ return true;
2111
+ }
2112
+ if (finishPendingTerminalSubmissionRetry(context, state, pendingRetryReconciled)) {
2113
+ return true;
2114
+ }
2115
+ return !terminalSubmissionRetryHasInputAuthority(context, state);
2116
+ }
2117
+ async function runTerminalSubmissionRetryDecision(context, state) {
2118
+ const { options, bridge, terminalControl, storeDir, statePath, logPath } = context.invocation;
2119
+ if (context.deferred) {
2120
+ await assertDeferredCodexForegroundBindingBoundary({
2121
+ options,
2122
+ scope: context.deferred.scope,
2123
+ boundary: context.deferred.boundary,
2124
+ expectedSourceStatus: "transitioning",
2125
+ requireNoDispatch: false,
2126
+ requireEmptyComposer: false
2127
+ });
2128
+ }
2129
+ const observation = await bridge.observeCodexComposer(terminalControl, context.requestText, { runtime: terminalRuntimeIdentityForConversation(state.conversation, terminalControl) });
2130
+ assertTerminalSubmissionRetryTurnOpen({
2131
+ statePath,
2132
+ exactTurnId: context.invocation.exactTurnId
2133
+ });
2134
+ const decision = decideTerminalSubmissionRetry({
2135
+ agent: "codex",
2136
+ exactTurnTarget: true,
2137
+ accepted: false,
2138
+ composer: observation.state,
2139
+ submissionStatus: stringValue(state.submission.status),
2140
+ lastProvenStage: stringValue(state.submission.last_proven_stage),
2141
+ submissionTextInjectedAt: stringValue(state.submission.text_injected_at),
2142
+ enterDispatchedAt: stringValue(state.submission.enter_dispatched_at),
2143
+ enterNotAttemptedAt: stringValue(state.submission.enter_not_attempted_at),
2144
+ enterNotAttemptedReason: stringValue(state.submission.enter_not_attempted_reason),
2145
+ ledgerStatus: stringValue(state.ledger.status),
2146
+ ledgerTextInjectedAt: stringValue(state.ledger.text_injected_at),
2147
+ ledgerEnterDispatchedAt: stringValue(state.ledger.enter_dispatched_at),
2148
+ ledgerEnterNotAttemptedAt: stringValue(state.ledger.enter_not_attempted_at),
2149
+ ledgerEnterNotAttemptedReason: stringValue(state.ledger.enter_not_attempted_reason),
2150
+ ledgerAgentAcceptedAt: stringValue(state.ledger.agent_accepted_at),
2151
+ originalMessageId: context.originalMessageId,
2152
+ currentMessageId: stringValue(context.takeover?.terminal_bridge_message_id),
2153
+ attempt: state.attempt
2154
+ });
2155
+ if (decision.action === "refuse") {
2156
+ printTerminalSubmissionRetryOutcome({
2157
+ conversation: state.conversation,
2158
+ terminalControl,
2159
+ attempt: state.attempt,
2160
+ outcome: "refused",
2161
+ terminalInputSent: false,
2162
+ reason: `${decision.reason}; no terminal input was sent.`
2163
+ });
2164
+ return;
2165
+ }
2166
+ if (decision.action === "repair_accepted") {
2167
+ throw new Error("retry metadata claims acceptance without native evidence; no terminal input was sent");
2168
+ }
2169
+ if (decision.action === "start_replacement" ||
2170
+ decision.action === "resume_replacement") {
2171
+ await runTerminalSubmissionReplacement({
2172
+ options,
2173
+ bridge,
2174
+ execution: context.execution,
2175
+ observation,
2176
+ decision,
2177
+ conversation: state.conversation,
2178
+ submission: state.submission,
2179
+ ledger: state.ledger,
2180
+ requestText: context.requestText,
2181
+ requestHash: context.requestHash,
2182
+ originalMessageId: context.originalMessageId,
2183
+ terminalControl,
2184
+ storeDir,
2185
+ statePath,
2186
+ logPath,
2187
+ scopes: context.scopes,
2188
+ resources: context.resources,
2189
+ attempt: state.attempt,
2190
+ deferred: context.deferred
2191
+ });
2192
+ return;
2193
+ }
2194
+ await runTerminalSubmissionExactDraftEnter({
2195
+ options,
2196
+ bridge,
2197
+ execution: context.execution,
2198
+ conversation: state.conversation,
2199
+ submission: state.submission,
2200
+ ledger: state.ledger,
2201
+ requestText: context.requestText,
2202
+ requestHash: context.requestHash,
2203
+ originalMessageId: context.originalMessageId,
2204
+ activeMessageId: decision.activeMessageId,
2205
+ terminalControl,
2206
+ storeDir,
2207
+ statePath,
2208
+ logPath,
2209
+ scopes: context.scopes,
2210
+ resources: context.resources,
2211
+ attempt: state.attempt,
2212
+ deferred: context.deferred
2213
+ });
2214
+ }
2215
+ async function runTerminalSubmissionRetryLocked(input) {
2216
+ const freshConversation = loadExactTerminalSubmissionRetryTurn({
2217
+ statePath: input.invocation.statePath,
2218
+ exactTurnId: input.invocation.exactTurnId
2219
+ });
2220
+ if (freshConversation.status === "closed") {
2221
+ printTerminalSubmissionRetryOutcome({
2222
+ conversation: freshConversation,
2223
+ terminalControl: input.invocation.terminalControl,
2224
+ outcome: "refused",
2225
+ terminalInputSent: false,
2226
+ reason: "The Turn was explicitly closed; no terminal input was sent and no " +
2227
+ "retry state was changed."
2228
+ });
2229
+ return;
2230
+ }
2231
+ const authority = loadTerminalSubmissionRetryLockedAuthority(input);
2232
+ if (authority.lifecycleSettled) {
2233
+ printTerminalSubmissionRetryOutcome({
2234
+ conversation: authority.conversation,
2235
+ terminalControl: input.invocation.terminalControl,
2236
+ attempt: authority.attempt,
2237
+ outcome: "refused",
2238
+ terminalInputSent: false,
2239
+ reason: "The Turn lifecycle is already settled; no terminal input was sent."
2240
+ });
2241
+ return;
2242
+ }
2243
+ const prepared = prepareTerminalSubmissionRetryDeferredContext({
2244
+ invocation: input.invocation,
2245
+ authority,
2246
+ scopes: input.scopes,
2247
+ resources: input.resources
2248
+ });
2249
+ const state = {
2250
+ conversation: authority.conversation,
2251
+ submission: authority.submission,
2252
+ ledger: prepared.ledger,
2253
+ attempt: authority.attempt
2254
+ };
2255
+ const context = {
2256
+ invocation: input.invocation,
2257
+ scopes: input.scopes,
2258
+ resources: input.resources,
2259
+ execution: terminalDispatchExecution(input.invocation.options, input.invocation.bridge),
2260
+ deferred: prepared.deferred,
2261
+ takeover: authority.takeover,
2262
+ currentMessageId: authority.currentMessageId,
2263
+ originalMessageId: authority.originalMessageId,
2264
+ requestText: authority.requestText,
2265
+ requestHash: authority.requestHash
2266
+ };
2267
+ if (await runTerminalSubmissionRetryNoInputRecovery(context, state))
2268
+ return;
2269
+ await runTerminalSubmissionRetryDecision(context, state);
2270
+ }
2271
+ async function runTerminalSubmissionRetry(options) {
2272
+ const exactTurnId = assertTerminalSubmissionRetryOptions(options);
2273
+ const loaded = loadConversationFromOptions(options);
2274
+ const { statePath, logPath } = loaded;
2275
+ const storePaths = pathsForConversationDir(path.dirname(statePath));
2276
+ const storeDir = storePaths.storeDir;
2277
+ if (path.resolve(storePaths.statePath) !== path.resolve(statePath) ||
2278
+ path.basename(storePaths.conversationDir) !== exactTurnId ||
2279
+ turnIdForConversation(loaded.conversation) !== exactTurnId) {
2280
+ throw new Error("send --turn did not resolve one canonical Turn; no terminal input was sent");
2281
+ }
2282
+ const initialTakeover = isRecord(loaded.conversation.native_session_takeover)
2283
+ ? loaded.conversation.native_session_takeover
2284
+ : undefined;
2285
+ const storedControl = terminalControlFromTakeover(initialTakeover);
2286
+ const pid = Number(initialTakeover?.terminal_agent_pid);
2287
+ if (!storedControl || !Number.isSafeInteger(pid) || pid <= 1) {
2288
+ throw new Error(`turn ${exactTurnId} is not attached to a live terminal; no terminal input was sent`);
2289
+ }
2290
+ if (executorForConversation(loaded.conversation).kind !== "codex") {
2291
+ throw new Error("terminal submission retry is supported only for Codex; no terminal input was sent");
2292
+ }
2293
+ const bridge = createTerminalAgentBridge(options);
2294
+ const live = await bridge.resolveStoredTerminal("codex", pid, storedControl, terminalRuntimeIdentityForConversation(loaded.conversation, storedControl));
2295
+ if (!terminalControlsShareIncarnation(live.terminalControl, storedControl)) {
2296
+ throw new Error("terminal control changed before submission retry; no terminal input was sent");
2297
+ }
2298
+ const terminalControl = live.terminalControl;
2299
+ await withCanonicalMutationLocks(terminalWriterMutationLocks(storeDir, terminalControl), async (scopes, resources) => withTerminalDispatchStateScope(scopes, resources, statePath, logPath, async (dispatchScopes, dispatchResources) => runTerminalSubmissionRetryLocked({
2300
+ invocation: {
2301
+ options,
2302
+ exactTurnId,
2303
+ statePath,
2304
+ logPath,
2305
+ storeDir,
2306
+ bridge,
2307
+ live,
2308
+ terminalControl
2309
+ },
2310
+ scopes: dispatchScopes,
2311
+ resources: dispatchResources
2312
+ })));
2313
+ }
2314
+ async function runTerminalSubmissionExactDraftEnter(input) {
2315
+ let attempt = input.attempt;
2316
+ const reserveEnter = () => {
2317
+ assertTerminalSubmissionRetryTurnOpen({
2318
+ statePath: input.statePath,
2319
+ exactTurnId: turnIdForConversation(input.conversation)
2320
+ });
2321
+ const at = cliNow().toISOString();
2322
+ const persistAttemptLedger = () => {
2323
+ mutationDispatchLedger.save(input.scopes, input.resources, {
2324
+ ...mutationDispatchLedger.load(input.scopes, input.resources),
2325
+ ...terminalSubmissionRetryLedgerFields(attempt)
2326
+ });
2327
+ };
2328
+ const advanceDeferred = (stage, stageAt) => {
2329
+ if (!input.deferred || !attempt)
2330
+ return;
2331
+ input.deferred.transfer = deferredForegroundApplication(input.options, input.deferred.boundary.terminal).advanceSubmissionRetry({
2332
+ scope: input.deferred.scope,
2333
+ boundary: deferredForegroundBoundaryProjection(input.deferred.boundary),
2334
+ attemptId: attempt.attempt_id,
2335
+ messageId: attempt.active_message_id,
2336
+ stage,
2337
+ at: stageAt
2338
+ });
2339
+ };
2340
+ if (!attempt) {
2341
+ attempt = saveTerminalSubmissionRetryForOpenTurn(input.statePath, {
2342
+ ...terminalSubmissionRetryBaseRecord({
2343
+ mode: "exact_draft_enter",
2344
+ state: "enter_reserved",
2345
+ attemptId: `submission-retry-${randomUUID()}`,
2346
+ storeDir: input.storeDir,
2347
+ statePath: input.statePath,
2348
+ conversation: input.conversation,
2349
+ originalMessageId: input.originalMessageId,
2350
+ activeMessageId: input.activeMessageId,
2351
+ requestHash: input.requestHash,
2352
+ terminalControl: input.terminalControl,
2353
+ at,
2354
+ deferredTransferId: input.deferred?.transfer.transfer_id
2355
+ }),
2356
+ enter_reserved_at: at
2357
+ }, null);
2358
+ if (input.deferred) {
2359
+ input.deferred.transfer = deferredForegroundApplication(input.options, input.deferred.boundary.terminal).reserveSubmissionRetry({
2360
+ scope: input.deferred.scope,
2361
+ boundary: deferredForegroundBoundaryProjection(input.deferred.boundary),
2362
+ attemptId: attempt.attempt_id,
2363
+ mode: attempt.mode,
2364
+ messageId: attempt.active_message_id,
2365
+ preparedAt: required(stringValue(input.submission.prepared_at), "deferred retry prepared timestamp is unavailable")
2366
+ });
2367
+ }
2368
+ advanceDeferred("enter_reserved", at);
2369
+ persistAttemptLedger();
2370
+ return;
2371
+ }
2372
+ if (attempt.mode === "replacement_send" &&
2373
+ attempt.state === "replacement_text_reserved") {
2374
+ attempt = saveTerminalSubmissionRetryForOpenTurn(input.statePath, {
2375
+ ...attempt,
2376
+ state: "replacement_text_injected",
2377
+ replacement_text_injected_at: at,
2378
+ updated_at: at
2379
+ }, attempt.revision);
2380
+ advanceDeferred("text_injected", at);
2381
+ persistAttemptLedger();
2382
+ }
2383
+ if (attempt.state !== "replacement_text_injected") {
2384
+ throw new Error("submission retry Enter is no longer reservable; no key was sent");
2385
+ }
2386
+ const enterReservedAt = cliNow().toISOString();
2387
+ attempt = saveTerminalSubmissionRetryForOpenTurn(input.statePath, {
2388
+ ...attempt,
2389
+ state: "enter_reserved",
2390
+ enter_reserved_at: enterReservedAt,
2391
+ updated_at: enterReservedAt
2392
+ }, attempt.revision);
2393
+ advanceDeferred("enter_reserved", enterReservedAt);
2394
+ persistAttemptLedger();
2395
+ };
2396
+ await input.bridge.submitExactCodexDraft(input.terminalControl, input.requestText, {
2397
+ runtime: terminalRuntimeIdentityForConversation(input.conversation, input.terminalControl),
2398
+ beforeEnterReservation: reserveEnter
2399
+ });
2400
+ assertTerminalSubmissionRetryTurnOpen({
2401
+ statePath: input.statePath,
2402
+ exactTurnId: turnIdForConversation(input.conversation)
2403
+ });
2404
+ if (!attempt || attempt.state !== "enter_reserved") {
2405
+ throw new Error("terminal bridge dispatched Enter without a durable retry reservation");
2406
+ }
2407
+ const enterAt = cliNow().toISOString();
2408
+ attempt = saveTerminalSubmissionRetryForOpenTurn(input.statePath, {
2409
+ ...attempt,
2410
+ state: "enter_dispatched",
2411
+ enter_dispatched_at: enterAt,
2412
+ updated_at: enterAt
2413
+ }, attempt.revision);
2414
+ if (input.deferred) {
2415
+ input.deferred.transfer = deferredForegroundApplication(input.options, input.deferred.boundary.terminal).advanceSubmissionRetry({
2416
+ scope: input.deferred.scope,
2417
+ boundary: deferredForegroundBoundaryProjection(input.deferred.boundary),
2418
+ attemptId: attempt.attempt_id,
2419
+ messageId: attempt.active_message_id,
2420
+ stage: "enter_dispatched",
2421
+ at: enterAt
2422
+ });
2423
+ }
2424
+ const current = loadState(input.statePath);
2425
+ const currentSubmission = required(terminalBridgeSubmission(current), "terminal submission receipt disappeared after retry Enter");
2426
+ const enteredConversation = withTerminalBridgeSubmission({
2427
+ conversation: withTerminalSubmissionRetryMonitorEpoch(terminalSubmissionRetryUnstalled(current), enterAt),
2428
+ messageId: input.activeMessageId,
2429
+ messageType: terminalSubmissionRetryMessageType(currentSubmission),
2430
+ requestText: input.requestText,
2431
+ status: "enter_dispatched",
2432
+ preparedAt: required(stringValue(currentSubmission.prepared_at), "terminal submission retry prepared timestamp is unavailable"),
2433
+ textInjectedAt: stringValue(currentSubmission.text_injected_at) ??
2434
+ attempt.replacement_text_injected_at ?? attempt.reserved_at,
2435
+ enterDispatchedAt: enterAt,
2436
+ lastProvenStage: "enter_dispatched"
2437
+ });
2438
+ saveState(input.statePath, enteredConversation);
2439
+ let enteredLedger = {
2440
+ ...mutationDispatchLedger.load(input.scopes, input.resources),
2441
+ ...terminalSubmissionRetryLedgerFields(attempt),
2442
+ status: "enter_dispatched",
2443
+ text_injected_at: stringValue(currentSubmission.text_injected_at) ??
2444
+ attempt.replacement_text_injected_at ?? attempt.reserved_at,
2445
+ enter_dispatched_at: enterAt,
2446
+ dispatcher_pid: null
2447
+ };
2448
+ mutationDispatchLedger.save(input.scopes, input.resources, enteredLedger);
2449
+ appendEvent(input.logPath, {
2450
+ ts: enterAt,
2451
+ conversation_id: enteredConversation.conversation_id,
2452
+ event: "terminal_submission_retry_enter_dispatched",
2453
+ message_id: input.activeMessageId,
2454
+ terminal_input_sent: true
2455
+ });
2456
+ if (input.deferred) {
2457
+ const transfer = input.deferred.scope.loadTransfer(input.deferred.transfer.transfer_id);
2458
+ const recovered = await deferredRecoveryAdapter
2459
+ .recoverAcceptedDeferredForegroundDispatch(deferredForegroundRecoveryAdapterPorts(), {
2460
+ options: input.options,
2461
+ scope: input.deferred.scope,
2462
+ storeDir: input.storeDir,
2463
+ terminal: input.deferred.boundary.terminal,
2464
+ transfer,
2465
+ boundary: input.deferred.boundary
2466
+ });
2467
+ assertTerminalSubmissionRetryTurnOpen({
2468
+ statePath: input.statePath,
2469
+ exactTurnId: turnIdForConversation(input.conversation)
2470
+ });
2471
+ if (recovered) {
2472
+ const finalized = finalizeDeferredTerminalSubmissionRetryAccepted({
2473
+ statePath: input.statePath,
2474
+ scopes: input.scopes,
2475
+ resources: input.resources,
2476
+ attempt
2477
+ });
2478
+ attempt = required(finalized.attempt, "deferred retry attempt disappeared after accepted finalization");
2479
+ const acceptedConversation = finalized.conversation;
2480
+ startTerminalBridgeMonitorForConversation({
2481
+ conversation: acceptedConversation,
2482
+ statePath: input.statePath,
2483
+ logPath: input.logPath,
2484
+ options: input.options
2485
+ });
2486
+ printTerminalSubmissionRetryOutcome({
2487
+ conversation: acceptedConversation,
2488
+ terminalControl: input.terminalControl,
2489
+ attempt,
2490
+ outcome: "agent_accepted",
2491
+ terminalInputSent: true,
2492
+ reason: "The exact deferred Codex draft was submitted once and its transfer and source/target Sessions were committed."
2493
+ });
2494
+ return;
2495
+ }
2496
+ startTerminalBridgeMonitorForConversation({
2497
+ conversation: enteredConversation,
2498
+ statePath: input.statePath,
2499
+ logPath: input.logPath,
2500
+ options: input.options
2501
+ });
2502
+ printTerminalSubmissionRetryOutcome({
2503
+ conversation: enteredConversation,
2504
+ terminalControl: input.terminalControl,
2505
+ attempt,
2506
+ outcome: "enter_dispatched",
2507
+ terminalInputSent: true,
2508
+ reason: "The exact deferred Codex draft received one Enter; dedicated transfer acceptance remains pending and another retry is forbidden."
2509
+ });
2510
+ return;
2511
+ }
2512
+ const timeoutMs = positiveMilliseconds(input.options.terminalAcceptanceTimeoutMs ??
2513
+ DEFAULT_TERMINAL_ACCEPTANCE_TIMEOUT_MS, "--terminal-acceptance-timeout-ms");
2514
+ const acceptance = await input.execution.pollAcceptance({
2515
+ executor: "codex",
2516
+ conversation: enteredConversation,
2517
+ terminalControl: input.terminalControl,
2518
+ timeoutMs,
2519
+ pollIntervalMs: Math.max(10, Math.min(timeoutMs, Number(input.options.terminalAcceptancePollIntervalMs ??
2520
+ DEFAULT_TERMINAL_ACCEPTANCE_POLL_INTERVAL_MS))),
2521
+ scrollbackLines: Number(input.options.scrollbackLines ?? 240)
2522
+ });
2523
+ assertTerminalSubmissionRetryTurnOpen({
2524
+ statePath: input.statePath,
2525
+ exactTurnId: turnIdForConversation(input.conversation)
2526
+ });
2527
+ if (acceptance.outcome === "agent_accepted") {
2528
+ const repaired = terminalSubmissionRetryAccepted({
2529
+ conversation: enteredConversation,
2530
+ submission: required(terminalBridgeSubmission(enteredConversation), "terminal submission receipt disappeared after retry acceptance"),
2531
+ ledger: enteredLedger,
2532
+ evidence: acceptance.evidence,
2533
+ requestText: input.requestText,
2534
+ at: cliNow().toISOString(),
2535
+ statePath: input.statePath,
2536
+ logPath: input.logPath,
2537
+ scopes: input.scopes,
2538
+ resources: input.resources,
2539
+ attempt,
2540
+ terminalInputSent: true
2541
+ });
2542
+ startTerminalBridgeMonitorForConversation({
2543
+ conversation: repaired.conversation,
2544
+ statePath: input.statePath,
2545
+ logPath: input.logPath,
2546
+ options: input.options
2547
+ });
2548
+ printTerminalSubmissionRetryOutcome({
2549
+ conversation: repaired.conversation,
2550
+ terminalControl: input.terminalControl,
2551
+ attempt: repaired.attempt,
2552
+ outcome: "agent_accepted",
2553
+ terminalInputSent: true,
2554
+ reason: "The exact existing Codex draft was submitted once and native acceptance was proven."
2555
+ });
2556
+ return;
2557
+ }
2558
+ if (acceptance.outcome === "not_accepted") {
2559
+ const notAcceptedAt = cliNow().toISOString();
2560
+ const notAcceptedConversation = terminalSubmissionRetryTerminalOutcome({
2561
+ conversation: enteredConversation,
2562
+ submission: required(terminalBridgeSubmission(enteredConversation), "terminal submission receipt disappeared after retry rejection"),
2563
+ ledger: enteredLedger,
2564
+ requestText: input.requestText,
2565
+ reason: acceptance.reason,
2566
+ at: notAcceptedAt,
2567
+ statePath: input.statePath,
2568
+ logPath: input.logPath,
2569
+ scopes: input.scopes,
2570
+ resources: input.resources,
2571
+ attempt,
2572
+ outcome: "not_accepted"
2573
+ });
2574
+ printTerminalSubmissionRetryOutcome({
2575
+ conversation: notAcceptedConversation,
2576
+ terminalControl: input.terminalControl,
2577
+ attempt,
2578
+ outcome: "not_accepted",
2579
+ terminalInputSent: true,
2580
+ reason: "The exact draft remains in the Codex composer after one Enter; it was durably recorded as not accepted and another retry is forbidden."
2581
+ });
2582
+ return;
2583
+ }
2584
+ if (acceptance.outcome === "uncertain") {
2585
+ const uncertainConversation = terminalSubmissionRetryTerminalOutcome({
2586
+ conversation: enteredConversation,
2587
+ submission: required(terminalBridgeSubmission(enteredConversation), "terminal submission receipt disappeared after uncertain retry acceptance"),
2588
+ ledger: enteredLedger,
2589
+ requestText: input.requestText,
2590
+ reason: acceptance.reason,
2591
+ at: cliNow().toISOString(),
2592
+ statePath: input.statePath,
2593
+ logPath: input.logPath,
2594
+ scopes: input.scopes,
2595
+ resources: input.resources,
2596
+ attempt,
2597
+ outcome: "uncertain"
2598
+ });
2599
+ printTerminalSubmissionRetryOutcome({
2600
+ conversation: uncertainConversation,
2601
+ terminalControl: input.terminalControl,
2602
+ attempt,
2603
+ outcome: "refused",
2604
+ terminalInputSent: true,
2605
+ reason: `Exact-draft acceptance is uncertain (${acceptance.reason}); the terminal outcome is durable and another retry is forbidden.`
2606
+ });
2607
+ return;
2608
+ }
2609
+ startTerminalBridgeMonitorForConversation({
2610
+ conversation: enteredConversation,
2611
+ statePath: input.statePath,
2612
+ logPath: input.logPath,
2613
+ options: input.options
2614
+ });
2615
+ printTerminalSubmissionRetryOutcome({
2616
+ conversation: enteredConversation,
2617
+ terminalControl: input.terminalControl,
2618
+ attempt,
2619
+ outcome: "enter_dispatched",
2620
+ terminalInputSent: true,
2621
+ reason: "The exact existing Codex draft received one Enter; acceptance remains pending and another retry is forbidden."
2622
+ });
2623
+ }
2624
+ async function runTerminalSubmissionReplacement(input) {
2625
+ if (input.observation.state !== "exact_empty") {
2626
+ throw new Error("replacement send lost its positive empty-composer proof; no terminal input was sent");
2627
+ }
2628
+ assertTerminalSubmissionRetryTurnOpen({
2629
+ statePath: input.statePath,
2630
+ exactTurnId: turnIdForConversation(input.conversation)
2631
+ });
2632
+ let attempt = input.attempt;
2633
+ if (!attempt) {
2634
+ const at = cliNow().toISOString();
2635
+ attempt = saveTerminalSubmissionRetryForOpenTurn(input.statePath, terminalSubmissionRetryBaseRecord({
2636
+ mode: "replacement_send",
2637
+ state: "replacement_reserved",
2638
+ attemptId: `submission-retry-${randomUUID()}`,
2639
+ storeDir: input.storeDir,
2640
+ statePath: input.statePath,
2641
+ conversation: input.conversation,
2642
+ originalMessageId: input.originalMessageId,
2643
+ activeMessageId: input.originalMessageId,
2644
+ requestHash: input.requestHash,
2645
+ terminalControl: input.terminalControl,
2646
+ at,
2647
+ deferredTransferId: input.deferred?.transfer.transfer_id
2648
+ }), null);
2649
+ if (input.deferred) {
2650
+ input.deferred.transfer = deferredForegroundApplication(input.options, input.deferred.boundary.terminal).reserveSubmissionRetry({
2651
+ scope: input.deferred.scope,
2652
+ boundary: deferredForegroundBoundaryProjection(input.deferred.boundary),
2653
+ attemptId: attempt.attempt_id,
2654
+ mode: attempt.mode,
2655
+ messageId: attempt.active_message_id,
2656
+ preparedAt: required(stringValue(input.submission.prepared_at), "deferred retry prepared timestamp is unavailable")
2657
+ });
2658
+ }
2659
+ const reservedLedger = required(mutationDispatchLedger.load(input.scopes, input.resources), "terminal submission retry ledger disappeared after reservation");
2660
+ mutationDispatchLedger.save(input.scopes, input.resources, {
2661
+ ...reservedLedger,
2662
+ ...terminalSubmissionRetryLedgerFields(attempt)
2663
+ });
2664
+ }
2665
+ if (attempt.mode !== "replacement_send" ||
2666
+ attempt.state !== "replacement_reserved") {
2667
+ throw new Error("replacement text transport was already reserved; no additional text was sent");
2668
+ }
2669
+ const currentAttempt = () => required(attempt, "terminal submission retry attempt is unavailable");
2670
+ const saveAttempt = (state, at, fields) => {
2671
+ const current = currentAttempt();
2672
+ attempt = saveTerminalSubmissionRetryForOpenTurn(input.statePath, {
2673
+ ...current,
2674
+ ...fields,
2675
+ state,
2676
+ updated_at: at
2677
+ }, current.revision);
2678
+ };
2679
+ const persistRetryLedger = (fields = {}) => {
2680
+ const current = required(mutationDispatchLedger.load(input.scopes, input.resources), "terminal submission retry ledger disappeared");
2681
+ const next = {
2682
+ ...current,
2683
+ ...terminalSubmissionRetryLedgerFields(attempt),
2684
+ ...fields
2685
+ };
2686
+ mutationDispatchLedger.save(input.scopes, input.resources, next);
2687
+ return next;
2688
+ };
2689
+ let enteredConversation;
2690
+ let enteredLedger;
2691
+ await input.bridge.send("codex", input.terminalControl, input.requestText, {
2692
+ runtime: terminalRuntimeIdentityForConversation(input.conversation, input.terminalControl),
2693
+ requireExactComposerBeforeEnter: true,
2694
+ requireExactEmptyComposerAfterBeforeText: {
2695
+ preliminaryComposerDigest: input.observation.digest
2696
+ },
2697
+ beforeText: () => {
2698
+ assertTerminalSubmissionRetryTurnOpen({
2699
+ statePath: input.statePath,
2700
+ exactTurnId: turnIdForConversation(input.conversation)
2701
+ });
2702
+ const at = cliNow().toISOString();
2703
+ saveAttempt("replacement_text_reserved", at, {
2704
+ replacement_text_reserved_at: at
2705
+ });
2706
+ if (input.deferred) {
2707
+ input.deferred.transfer = deferredForegroundApplication(input.options, input.deferred.boundary.terminal).advanceSubmissionRetry({
2708
+ scope: input.deferred.scope,
2709
+ boundary: deferredForegroundBoundaryProjection(input.deferred.boundary),
2710
+ attemptId: currentAttempt().attempt_id,
2711
+ messageId: currentAttempt().active_message_id,
2712
+ stage: "text_reserved",
2713
+ at
2714
+ });
2715
+ }
2716
+ persistRetryLedger();
2717
+ },
2718
+ beforeEnter: () => {
2719
+ assertTerminalSubmissionRetryTurnOpen({
2720
+ statePath: input.statePath,
2721
+ exactTurnId: turnIdForConversation(input.conversation)
2722
+ });
2723
+ const at = cliNow().toISOString();
2724
+ saveAttempt("enter_reserved", at, { enter_reserved_at: at });
2725
+ if (input.deferred) {
2726
+ input.deferred.transfer = deferredForegroundApplication(input.options, input.deferred.boundary.terminal).advanceSubmissionRetry({
2727
+ scope: input.deferred.scope,
2728
+ boundary: deferredForegroundBoundaryProjection(input.deferred.boundary),
2729
+ attemptId: currentAttempt().attempt_id,
2730
+ messageId: currentAttempt().active_message_id,
2731
+ stage: "enter_reserved",
2732
+ at
2733
+ });
2734
+ }
2735
+ persistRetryLedger();
2736
+ },
2737
+ onTransportStage: ({ stage }) => {
2738
+ assertTerminalSubmissionRetryTurnOpen({
2739
+ statePath: input.statePath,
2740
+ exactTurnId: turnIdForConversation(input.conversation)
2741
+ });
2742
+ const at = cliNow().toISOString();
2743
+ if (stage === "text_injected") {
2744
+ saveAttempt("replacement_text_injected", at, {
2745
+ replacement_text_injected_at: at
2746
+ });
2747
+ }
2748
+ else {
2749
+ saveAttempt("enter_dispatched", at, {
2750
+ enter_dispatched_at: at
2751
+ });
2752
+ }
2753
+ if (input.deferred) {
2754
+ input.deferred.transfer = deferredForegroundApplication(input.options, input.deferred.boundary.terminal).advanceSubmissionRetry({
2755
+ scope: input.deferred.scope,
2756
+ boundary: deferredForegroundBoundaryProjection(input.deferred.boundary),
2757
+ attemptId: currentAttempt().attempt_id,
2758
+ messageId: currentAttempt().active_message_id,
2759
+ stage,
2760
+ at
2761
+ });
2762
+ }
2763
+ if (stage === "text_injected") {
2764
+ persistRetryLedger();
2765
+ return;
2766
+ }
2767
+ const current = loadState(input.statePath);
2768
+ const submission = required(terminalBridgeSubmission(current), "terminal submission receipt disappeared after replacement Enter");
2769
+ enteredConversation = withTerminalBridgeSubmission({
2770
+ conversation: withTerminalSubmissionRetryMonitorEpoch(terminalSubmissionRetryUnstalled(current), at),
2771
+ messageId: input.originalMessageId,
2772
+ messageType: terminalSubmissionRetryMessageType(submission),
2773
+ requestText: input.requestText,
2774
+ status: "enter_dispatched",
2775
+ preparedAt: required(stringValue(submission.prepared_at), "terminal submission retry prepared timestamp is unavailable"),
2776
+ textInjectedAt: currentAttempt().replacement_text_injected_at,
2777
+ enterDispatchedAt: at,
2778
+ lastProvenStage: "enter_dispatched"
2779
+ });
2780
+ saveState(input.statePath, enteredConversation);
2781
+ enteredLedger = persistRetryLedger({
2782
+ status: "enter_dispatched",
2783
+ text_injected_at: currentAttempt().replacement_text_injected_at,
2784
+ enter_dispatched_at: at,
2785
+ enter_not_attempted_at: undefined,
2786
+ enter_not_attempted_reason: undefined,
2787
+ uncertain_at: undefined,
2788
+ safe_to_retry: undefined,
2789
+ acceptance_evidence: undefined,
2790
+ agent_accepted_at: undefined,
2791
+ not_accepted_at: undefined,
2792
+ dispatcher_pid: null
2793
+ });
2794
+ appendEvent(input.logPath, {
2795
+ ts: at,
2796
+ conversation_id: enteredConversation.conversation_id,
2797
+ event: "terminal_submission_retry_enter_dispatched",
2798
+ message_id: input.originalMessageId,
2799
+ terminal_input_sent: true
2800
+ });
2801
+ }
2802
+ });
2803
+ assertTerminalSubmissionRetryTurnOpen({
2804
+ statePath: input.statePath,
2805
+ exactTurnId: turnIdForConversation(input.conversation)
2806
+ });
2807
+ if (currentAttempt().state !== "enter_dispatched" ||
2808
+ !enteredConversation || !enteredLedger) {
2809
+ throw new Error("replacement transport returned without durable Enter evidence");
2810
+ }
2811
+ if (input.deferred) {
2812
+ const transfer = input.deferred.scope.loadTransfer(input.deferred.transfer.transfer_id);
2813
+ const recovered = await deferredRecoveryAdapter
2814
+ .recoverAcceptedDeferredForegroundDispatch(deferredForegroundRecoveryAdapterPorts(), {
2815
+ options: input.options,
2816
+ scope: input.deferred.scope,
2817
+ storeDir: input.storeDir,
2818
+ terminal: input.deferred.boundary.terminal,
2819
+ transfer,
2820
+ boundary: input.deferred.boundary
2821
+ });
2822
+ assertTerminalSubmissionRetryTurnOpen({
2823
+ statePath: input.statePath,
2824
+ exactTurnId: turnIdForConversation(input.conversation)
2825
+ });
2826
+ if (recovered) {
2827
+ const finalized = finalizeDeferredTerminalSubmissionRetryAccepted({
2828
+ statePath: input.statePath,
2829
+ scopes: input.scopes,
2830
+ resources: input.resources,
2831
+ attempt
2832
+ });
2833
+ attempt = required(finalized.attempt, "deferred replacement retry attempt disappeared after accepted finalization");
2834
+ const acceptedConversation = finalized.conversation;
2835
+ startTerminalBridgeMonitorForConversation({
2836
+ conversation: acceptedConversation,
2837
+ statePath: input.statePath,
2838
+ logPath: input.logPath,
2839
+ options: input.options
2840
+ });
2841
+ printTerminalSubmissionRetryOutcome({
2842
+ conversation: acceptedConversation,
2843
+ terminalControl: input.terminalControl,
2844
+ attempt,
2845
+ outcome: "agent_accepted",
2846
+ terminalInputSent: true,
2847
+ reason: "The missing deferred Codex submission was completed once and its source/target Sessions were committed."
2848
+ });
2849
+ return;
2850
+ }
2851
+ }
2852
+ else {
2853
+ const timeoutMs = positiveMilliseconds(input.options.terminalAcceptanceTimeoutMs ??
2854
+ DEFAULT_TERMINAL_ACCEPTANCE_TIMEOUT_MS, "--terminal-acceptance-timeout-ms");
2855
+ const acceptance = await input.execution.pollAcceptance({
2856
+ executor: "codex",
2857
+ conversation: enteredConversation,
2858
+ terminalControl: input.terminalControl,
2859
+ timeoutMs,
2860
+ pollIntervalMs: Math.max(10, Math.min(timeoutMs, Number(input.options.terminalAcceptancePollIntervalMs ??
2861
+ DEFAULT_TERMINAL_ACCEPTANCE_POLL_INTERVAL_MS))),
2862
+ scrollbackLines: Number(input.options.scrollbackLines ?? 240)
2863
+ });
2864
+ assertTerminalSubmissionRetryTurnOpen({
2865
+ statePath: input.statePath,
2866
+ exactTurnId: turnIdForConversation(input.conversation)
2867
+ });
2868
+ if (acceptance.outcome === "agent_accepted") {
2869
+ const repaired = terminalSubmissionRetryAccepted({
2870
+ conversation: enteredConversation,
2871
+ submission: required(terminalBridgeSubmission(enteredConversation), "terminal submission receipt disappeared after replacement acceptance"),
2872
+ ledger: enteredLedger,
2873
+ evidence: acceptance.evidence,
2874
+ requestText: input.requestText,
2875
+ at: cliNow().toISOString(),
2876
+ statePath: input.statePath,
2877
+ logPath: input.logPath,
2878
+ scopes: input.scopes,
2879
+ resources: input.resources,
2880
+ attempt,
2881
+ terminalInputSent: true
2882
+ });
2883
+ startTerminalBridgeMonitorForConversation({
2884
+ conversation: repaired.conversation,
2885
+ statePath: input.statePath,
2886
+ logPath: input.logPath,
2887
+ options: input.options
2888
+ });
2889
+ printTerminalSubmissionRetryOutcome({
2890
+ conversation: repaired.conversation,
2891
+ terminalControl: input.terminalControl,
2892
+ attempt: repaired.attempt,
2893
+ outcome: "agent_accepted",
2894
+ terminalInputSent: true,
2895
+ reason: "The missing Codex submission was completed once and native acceptance was proven."
2896
+ });
2897
+ return;
2898
+ }
2899
+ if (acceptance.outcome === "not_accepted") {
2900
+ const notAcceptedConversation = terminalSubmissionRetryTerminalOutcome({
2901
+ conversation: enteredConversation,
2902
+ submission: required(terminalBridgeSubmission(enteredConversation), "terminal submission receipt disappeared after replacement rejection"),
2903
+ ledger: enteredLedger,
2904
+ requestText: input.requestText,
2905
+ reason: acceptance.reason,
2906
+ at: cliNow().toISOString(),
2907
+ statePath: input.statePath,
2908
+ logPath: input.logPath,
2909
+ scopes: input.scopes,
2910
+ resources: input.resources,
2911
+ attempt,
2912
+ outcome: "not_accepted"
2913
+ });
2914
+ printTerminalSubmissionRetryOutcome({
2915
+ conversation: notAcceptedConversation,
2916
+ terminalControl: input.terminalControl,
2917
+ attempt,
2918
+ outcome: "not_accepted",
2919
+ terminalInputSent: true,
2920
+ reason: "The replacement text remains in the Codex composer after one Enter; it was not accepted and another retry is forbidden."
2921
+ });
2922
+ return;
2923
+ }
2924
+ if (acceptance.outcome === "uncertain") {
2925
+ const uncertainConversation = terminalSubmissionRetryTerminalOutcome({
2926
+ conversation: enteredConversation,
2927
+ submission: required(terminalBridgeSubmission(enteredConversation), "terminal submission receipt disappeared after uncertain replacement acceptance"),
2928
+ ledger: enteredLedger,
2929
+ requestText: input.requestText,
2930
+ reason: acceptance.reason,
2931
+ at: cliNow().toISOString(),
2932
+ statePath: input.statePath,
2933
+ logPath: input.logPath,
2934
+ scopes: input.scopes,
2935
+ resources: input.resources,
2936
+ attempt,
2937
+ outcome: "uncertain"
2938
+ });
2939
+ printTerminalSubmissionRetryOutcome({
2940
+ conversation: uncertainConversation,
2941
+ terminalControl: input.terminalControl,
2942
+ attempt,
2943
+ outcome: "refused",
2944
+ terminalInputSent: true,
2945
+ reason: `Replacement acceptance is uncertain (${acceptance.reason}); another retry is forbidden.`
2946
+ });
2947
+ return;
2948
+ }
2949
+ }
2950
+ startTerminalBridgeMonitorForConversation({
2951
+ conversation: enteredConversation,
2952
+ statePath: input.statePath,
2953
+ logPath: input.logPath,
2954
+ options: input.options
2955
+ });
2956
+ printTerminalSubmissionRetryOutcome({
2957
+ conversation: enteredConversation,
2958
+ terminalControl: input.terminalControl,
2959
+ attempt,
2960
+ outcome: "enter_dispatched",
2961
+ terminalInputSent: true,
2962
+ reason: "The missing Codex submission received one text injection and one Enter; acceptance remains pending and another retry is forbidden."
2963
+ });
2964
+ }
2965
+ async function runSend(options) {
2966
+ if (options.respond !== true && stringValue(options.turn)) {
2967
+ return runTerminalSubmissionRetry(options);
2968
+ }
2969
+ const messageBody = required(options.message ?? options.request, "--message is required");
2970
+ // Ordinary send/respond owns Turn creation only. Reject native lifecycle
2971
+ // slash commands before resolving or mutating any Session/Turn state so a
2972
+ // caller cannot bypass lifecycle capabilities, CAS tokens, transition
2973
+ // recovery, or binding-generation updates by writing directly to tmux.
2974
+ terminalSubmissionPayload(messageBody);
2975
+ if (options.agentHardTimeoutMinutes !== undefined) {
2976
+ positiveMinutes(options.agentHardTimeoutMinutes, "--agent-hard-timeout-minutes");
2977
+ }
2978
+ if (options.respond === true) {
2979
+ return runTurnResponse({ options, messageBody });
2980
+ }
2981
+ if ((options.type ?? "task") !== "task") {
2982
+ throw new Error("ordinary send only accepts message type task; use respond --turn to answer an in-flight Turn");
2983
+ }
2984
+ const terminalConversation = await resolveTerminalConversationFromOptions(options);
2985
+ if (terminalConversation) {
2986
+ return runRawTerminalSend(options, messageBody, terminalConversation);
2987
+ }
2988
+ return runManagedSessionSend(options, messageBody);
2989
+ }
2990
+ function rawTerminalInitialAuthority({ options, terminal, storeDir }) {
2991
+ const claimedSession = soleBoundManagedSessionClaimForTerminal(storeDir, terminal);
2992
+ const suppliedExpectedTerminalToken = stringValue(options.expectedTerminalToken);
2993
+ const implicitCodexCandidateAuthority = Boolean(terminal.agent === "codex" &&
2994
+ suppliedExpectedTerminalToken === undefined &&
2995
+ claimedSession?.status === "bound" &&
2996
+ isCompleteNativeRollout(claimedSession.binding?.native_process.rollout));
2997
+ const knownCodexCompanions = claimedSession
2998
+ ? codexAllowedCompanionSetForManagedSession({
2999
+ storeDir,
3000
+ session: claimedSession
3001
+ })
3002
+ : { additional: [] };
3003
+ return {
3004
+ claimedSession,
3005
+ suppliedExpectedTerminalToken,
3006
+ implicitCodexCandidateAuthority,
3007
+ knownCodexCompanions
3008
+ };
3009
+ }
3010
+ function assertRawTerminalCandidateAuthority({ terminal, nativeIdentityObservation, deferredCodexCandidateInventory, implicitCodexCandidateAuthority }) {
3011
+ if (implicitCodexCandidateAuthority &&
3012
+ !deferredCodexCandidateInventory) {
3013
+ throw new Error("rollout-backed Codex terminal send requires a fresh complete " +
3014
+ "nonempty open-root inventory; refresh AKK list before sending");
3015
+ }
3016
+ if (nativeIdentityObservation.status === "unavailable" &&
3017
+ !deferredCodexCandidateInventory) {
3018
+ throw new Error(`native ${terminal.agent} identity observation is ` +
3019
+ `unavailable: ${nativeIdentityObservation.reason}`);
3020
+ }
3021
+ }
3022
+ function explicitTerminalSendToken(terminal) {
3023
+ const processIncarnation = processIncarnationForPid(terminal.pid);
3024
+ return unmanagedTerminalBindingToken({
3025
+ terminalId: terminal.conversationId,
3026
+ terminalControl: terminal.terminalControl,
3027
+ agent: terminal.agent,
3028
+ pid: terminal.pid,
3029
+ workspace: terminal.terminalControl.currentPath ?? "",
3030
+ processUuid: processIncarnation.processUuid,
3031
+ processBirth: processIncarnation.processBirth
3032
+ });
3033
+ }
3034
+ function explicitSendTurnReference(candidate) {
3035
+ try {
3036
+ return turnIdForConversation(candidate);
3037
+ }
3038
+ catch {
3039
+ return stringValue(candidate.conversation_id) ?? "unknown";
3040
+ }
3041
+ }
3042
+ function hasFreshExplicitTerminalSendToken(options, terminal) {
3043
+ const supplied = stringValue(options.expectedTerminalToken);
3044
+ return Boolean(supplied && supplied === explicitTerminalSendToken(terminal));
3045
+ }
3046
+ function assertFreshUserExplicitTerminalSendToken(options, terminal) {
3047
+ const expected = stringValue(options.expectedUserExplicitTerminalToken);
3048
+ if (expected && expected !== explicitTerminalSendToken(terminal)) {
3049
+ throw new Error("the explicit terminal send token is stale; refresh AKK list");
3050
+ }
3051
+ }
3052
+ async function assertFreshUserExplicitTerminalSendTargetWhileLocked(options, terminal) {
3053
+ if (!stringValue(options.expectedUserExplicitTerminalToken))
3054
+ return;
3055
+ const resolved = await createTerminalAgentBridge(options)
3056
+ .resolveConversationId(terminal.conversationId);
3057
+ if (!resolved ||
3058
+ resolved.agent !== terminal.agent ||
3059
+ resolved.pid !== terminal.pid ||
3060
+ !terminalControlsShareIncarnation(resolved.terminalControl, terminal.terminalControl)) {
3061
+ throw new Error("the explicitly selected terminal is no longer the same live process");
3062
+ }
3063
+ assertFreshUserExplicitTerminalSendToken(options, resolved);
3064
+ }
3065
+ function releaseTerminalDispatchForExplicitSend(input) {
3066
+ let ledger;
3067
+ try {
3068
+ ledger = loadTerminalBridgeDispatchLedger(input.terminal.terminalControl);
3069
+ }
3070
+ catch (error) {
3071
+ return {
3072
+ checked: false,
3073
+ warnings: [
3074
+ `terminal dispatch ownership could not be checked before Session ` +
3075
+ `release: ${error instanceof Error ? error.message : String(error)}`
3076
+ ]
3077
+ };
3078
+ }
3079
+ if (!ledger || ledger.status === "resolved") {
3080
+ return { checked: true, warnings: [] };
3081
+ }
3082
+ if (terminalDispatchLedgerLooksLifecycle(ledger)) {
3083
+ return { checked: true, lifecycleLedger: ledger, warnings: [] };
3084
+ }
3085
+ if (!terminalDispatchRecordMatchesControl(ledger, input.terminal.terminalControl)) {
3086
+ return { checked: true, warnings: [] };
3087
+ }
3088
+ const conversationId = stringValue(ledger.conversation_id);
3089
+ const expectedMessageId = stringValue(ledger.message_id);
3090
+ if (!conversationId || !expectedMessageId) {
3091
+ return {
3092
+ checked: true,
3093
+ warnings: [
3094
+ "orphaned terminal dispatch was kept because its exact conversation " +
3095
+ "or message generation could not be verified"
3096
+ ]
3097
+ };
3098
+ }
3099
+ if (input.conversations.some((conversation) => conversation.conversation_id === conversationId)) {
3100
+ return { checked: true, warnings: [] };
3101
+ }
3102
+ try {
3103
+ if (loadTerminalDispatchLedgerOwner(ledger)) {
3104
+ return { checked: true, warnings: [] };
3105
+ }
3106
+ }
3107
+ catch (error) {
3108
+ return {
3109
+ checked: true,
3110
+ warnings: [
3111
+ `orphaned terminal dispatch owner could not be checked: ${error instanceof Error ? error.message : String(error)}`
3112
+ ]
3113
+ };
3114
+ }
3115
+ try {
3116
+ const resolved = resolveTerminalBridgeDispatchLedger(input.terminal.terminalControl, {
3117
+ conversation: { conversation_id: conversationId },
3118
+ expectedMessageId,
3119
+ reason: "orphaned management superseded by explicit user Send"
3120
+ });
3121
+ return resolved
3122
+ ? { checked: true, warnings: [] }
3123
+ : {
3124
+ checked: true,
3125
+ warnings: [
3126
+ "orphaned terminal dispatch changed before explicit Send cleanup"
3127
+ ]
3128
+ };
3129
+ }
3130
+ catch (error) {
3131
+ return {
3132
+ checked: true,
3133
+ warnings: [
3134
+ `orphaned terminal dispatch could not be released: ${error instanceof Error ? error.message : String(error)}`
3135
+ ]
3136
+ };
3137
+ }
3138
+ }
3139
+ async function bestEffortReleaseTerminalManagementForExplicitSend(input) {
3140
+ try {
3141
+ return await withStoreWriterLeaseAsync(input.storeDir, async () => releaseTerminalManagementForExplicitSendUnderWriter(input), { timeoutMs: 0 });
3142
+ }
3143
+ catch (error) {
3144
+ return [
3145
+ `AKK management writer was unavailable: ${error instanceof Error ? error.message : String(error)}`
3146
+ ];
3147
+ }
3148
+ }
3149
+ function releaseTerminalManagementForExplicitSendUnderWriter(input) {
3150
+ const warnings = [];
3151
+ let conversations;
3152
+ try {
3153
+ conversations = listConversations(input.storeDir);
3154
+ }
3155
+ catch (error) {
3156
+ return [
3157
+ `managed Turn inventory could not be read: ${error instanceof Error ? error.message : String(error)}`
3158
+ ];
3159
+ }
3160
+ for (const candidate of conversations) {
3161
+ const reference = explicitSendTurnReference(candidate);
3162
+ try {
3163
+ if (!isSessionSendBlockingStatus(candidate.status))
3164
+ continue;
3165
+ const takeover = isRecord(candidate.native_session_takeover)
3166
+ ? candidate.native_session_takeover
3167
+ : undefined;
3168
+ const control = terminalControlFromTakeover(takeover);
3169
+ if (!control ||
3170
+ !terminalControlsShareIncarnation(control, input.terminal.terminalControl)) {
3171
+ continue;
3172
+ }
3173
+ const paths = pathsForConversation(candidate.conversation_id, input.storeDir);
3174
+ const releaseStateLock = acquireFileLock(`${paths.statePath}.lock`, { timeoutMs: 0 });
3175
+ try {
3176
+ const current = loadState(paths.statePath);
3177
+ if (!isSessionSendBlockingStatus(current.status))
3178
+ continue;
3179
+ const currentTakeover = isRecord(current.native_session_takeover)
3180
+ ? current.native_session_takeover
3181
+ : undefined;
3182
+ const currentControl = terminalControlFromTakeover(currentTakeover);
3183
+ if (!currentControl ||
3184
+ !terminalControlsShareIncarnation(currentControl, input.terminal.terminalControl)) {
3185
+ continue;
3186
+ }
3187
+ const now = cliNow().toISOString();
3188
+ const closed = {
3189
+ ...supersedeUnacceptedCallbackDeliveries(current, {
3190
+ at: now,
3191
+ reason: "superseded_by_user_explicit_send"
3192
+ }),
3193
+ status: "closed",
3194
+ closed_at: now,
3195
+ close_reason: "superseded by explicit user Send",
3196
+ disposition: "user_abandoned_management",
3197
+ callback_expected: false,
3198
+ updated_at: now
3199
+ };
3200
+ delete closed.idle_since;
3201
+ saveExplicitUserCloseState(paths.statePath, closed);
3202
+ try {
3203
+ const transferCleanup = cleanupDeferredForegroundUserClose({
3204
+ storeDir: input.storeDir,
3205
+ conversation: closed,
3206
+ at: now
3207
+ });
3208
+ warnings.push(...transferCleanup.warnings.map((warning) => `Turn ${explicitSendTurnReference(current)} deferred cleanup: ${warning}`));
3209
+ }
3210
+ catch (error) {
3211
+ warnings.push(`Turn ${explicitSendTurnReference(current)} deferred cleanup failed: ${error instanceof Error ? error.message : String(error)}`);
3212
+ }
3213
+ try {
3214
+ const expectedMessageId = stringValue(currentTakeover?.terminal_bridge_message_id);
3215
+ const dispatchResolved = resolveTerminalBridgeDispatchLedger(currentControl, {
3216
+ conversation: closed,
3217
+ expectedMessageId,
3218
+ reason: "management superseded by explicit user Send"
3219
+ });
3220
+ if (expectedMessageId && !dispatchResolved) {
3221
+ warnings.push(`Turn ${explicitSendTurnReference(current)} dispatch cleanup ` +
3222
+ "did not match its expected ledger");
3223
+ }
3224
+ }
3225
+ catch (error) {
3226
+ warnings.push(`Turn ${explicitSendTurnReference(current)} dispatch cleanup failed: ${error instanceof Error ? error.message : String(error)}`);
3227
+ }
3228
+ try {
3229
+ appendExplicitUserCloseEvent(paths.logPath, {
3230
+ ts: now,
3231
+ conversation_id: current.conversation_id,
3232
+ event: "conversation_closed",
3233
+ status: "closed",
3234
+ reason: closed.close_reason,
3235
+ disposition: "user_abandoned_management",
3236
+ terminal_input_sent: false,
3237
+ coding_agent_stopped: false,
3238
+ tmux_pane_closed: false
3239
+ });
3240
+ }
3241
+ catch (error) {
3242
+ warnings.push(`Turn ${explicitSendTurnReference(current)} close event failed: ${error instanceof Error ? error.message : String(error)}`);
3243
+ }
3244
+ }
3245
+ finally {
3246
+ releaseStateLock();
3247
+ }
3248
+ }
3249
+ catch (error) {
3250
+ warnings.push(`Turn ${reference} could not release AKK ` +
3251
+ `management: ${error instanceof Error ? error.message : String(error)}`);
3252
+ }
3253
+ }
3254
+ let currentConversations;
3255
+ try {
3256
+ currentConversations = listConversations(input.storeDir);
3257
+ }
3258
+ catch (error) {
3259
+ warnings.push(`managed Turn inventory could not be rechecked before Session release: ${error instanceof Error ? error.message : String(error)}`);
3260
+ return warnings;
3261
+ }
3262
+ const blockingSessionIds = new Set(currentConversations
3263
+ .filter((turn) => isSessionSendBlockingStatus(turn.status))
3264
+ .map((turn) => sessionIdForConversation(turn)));
3265
+ const dispatchRelease = releaseTerminalDispatchForExplicitSend({
3266
+ terminal: input.terminal,
3267
+ conversations: currentConversations
3268
+ });
3269
+ warnings.push(...dispatchRelease.warnings);
3270
+ if (!dispatchRelease.checked) {
3271
+ return warnings;
3272
+ }
3273
+ let sessions;
3274
+ try {
3275
+ sessions = listManagedSessions(input.storeDir);
3276
+ }
3277
+ catch (error) {
3278
+ warnings.push(`managed Session inventory could not be read: ${error instanceof Error ? error.message : String(error)}`);
3279
+ return warnings;
3280
+ }
3281
+ const lifecycleLedger = dispatchRelease.lifecycleLedger;
3282
+ for (const session of sessions) {
3283
+ try {
3284
+ const binding = session.binding;
3285
+ if (!["bound", "quarantined", "transitioning"].includes(session.status) ||
3286
+ session.agent !== input.terminal.agent ||
3287
+ !binding ||
3288
+ binding.native_process.pid !== input.terminal.pid ||
3289
+ !terminalControlAliasMatches(binding.terminal_id, binding.terminal_control, input.terminal.conversationId, input.terminal.terminalControl) ||
3290
+ blockingSessionIds.has(session.session_id)) {
3291
+ continue;
3292
+ }
3293
+ const lifecycleSourceSessionId = stringValue(lifecycleLedger?.source_session_id);
3294
+ const lifecycleTargetSessionId = stringValue(lifecycleLedger?.target_session_id);
3295
+ const lifecycleTransitionId = stringValue(lifecycleLedger?.transition_id);
3296
+ const lifecycleNamesSession = Boolean(lifecycleLedger &&
3297
+ (lifecycleSourceSessionId === session.session_id ||
3298
+ lifecycleTargetSessionId === session.session_id));
3299
+ if (lifecycleNamesSession) {
3300
+ const transitionRelationship = session.last_transition_id
3301
+ ? lifecycleTransitionId === session.last_transition_id
3302
+ ? `transition ${session.last_transition_id}`
3303
+ : `a lifecycle transition that drifted from Session marker ` +
3304
+ session.last_transition_id
3305
+ : `lifecycle transition ${lifecycleTransitionId ?? "with unknown id"}`;
3306
+ warnings.push(`Session ${session.session_id} kept AKK management because ` +
3307
+ `unresolved ${transitionRelationship} still names it; explicit ` +
3308
+ "Send does not prove that transition's native outcome");
3309
+ continue;
3310
+ }
3311
+ const detachedAt = cliNow().toISOString();
3312
+ saveManagedSession(input.storeDir, {
3313
+ ...session,
3314
+ status: "detached",
3315
+ detached_at: detachedAt,
3316
+ quarantine_reason: undefined,
3317
+ updated_at: detachedAt
3318
+ }, { expectedRevision: managedSessionRevision(session) });
3319
+ }
3320
+ catch (error) {
3321
+ warnings.push(`Session ${session.session_id} could not release AKK management: ${error instanceof Error ? error.message : String(error)}`);
3322
+ }
3323
+ }
3324
+ return warnings;
3325
+ }
3326
+ function explicitTerminalSendIntentRuntimeDir() {
3327
+ const configured = stringValue(cliEnv().AKK_RUNTIME_DIR);
3328
+ return configured
3329
+ ? path.resolve(required(expandHome(configured), "AKK runtime directory"))
3330
+ : path.join(path.dirname(defaultStoreDir()), "runtime-v2");
3331
+ }
3332
+ function assertSafeUserExplicitTerminalSend(status) {
3333
+ if (status?.reachable !== true) {
3334
+ throw new Error("the explicitly selected terminal is unreachable");
3335
+ }
3336
+ const approval = isRecord(status.approval_state)
3337
+ ? status.approval_state
3338
+ : undefined;
3339
+ if (approval?.scanned !== true) {
3340
+ throw new Error("the explicitly selected terminal approval state could not be verified");
3341
+ }
3342
+ if (approval?.blocked === true) {
3343
+ throw new Error(stringValue(approval.reason) ??
3344
+ "the explicitly selected terminal is waiting at an approval prompt");
3345
+ }
3346
+ }
3347
+ function terminalUserSendIntentContext(options, messageBody, terminal) {
3348
+ const messageId = required(stringValue(options.messageId), "explicit terminal Send message id is unavailable");
3349
+ const payload = terminalSubmissionPayload(String(messageBody));
3350
+ return {
3351
+ messageId,
3352
+ payload,
3353
+ boundary: {
3354
+ terminalRuntimeKey: terminalBridgeRuntimeKey(terminal.terminalControl),
3355
+ physicalToken: explicitTerminalSendToken(terminal),
3356
+ messageId,
3357
+ requestHash: createHash("sha256").update(payload).digest("hex")
3358
+ },
3359
+ repository: createTerminalUserSendIntentRepository({
3360
+ runtimeDir: explicitTerminalSendIntentRuntimeDir()
3361
+ })
3362
+ };
3363
+ }
3364
+ class TerminalUserSendReplayForbiddenError extends Error {
3365
+ constructor(message) {
3366
+ super(message);
3367
+ this.name = "TerminalUserSendReplayForbiddenError";
3368
+ }
3369
+ }
3370
+ function userExplicitEmergencyTerminalLockPath(terminal) {
3371
+ const userScope = createHash("sha256")
3372
+ .update(path.dirname(defaultStoreDir()))
3373
+ .digest("hex")
3374
+ .slice(0, 16);
3375
+ const terminalKey = createHash("sha256")
3376
+ .update(terminalBridgeRuntimeKey(terminal.terminalControl))
3377
+ .digest("hex");
3378
+ const directory = path.join(os.tmpdir(), `agent-knock-knock-user-send-${userScope}`);
3379
+ ensureDir(directory);
3380
+ return path.join(directory, `terminal-${terminalKey}.lock`);
3381
+ }
3382
+ function acquireUserExplicitTerminalSendLock(options, terminal, warnings) {
3383
+ let release;
3384
+ try {
3385
+ release = acquireTerminalBridgeSendLock(storeDirFromOptions(options), terminal.terminalControl, { timeoutMs: 30_000 });
3386
+ }
3387
+ catch (error) {
3388
+ if (isRecord(error) && error.code === "LOCK_TIMEOUT")
3389
+ throw error;
3390
+ warnings.push(`primary terminal serialization was unavailable: ${error instanceof Error ? error.message : String(error)}`);
3391
+ try {
3392
+ release = acquireFileLock(userExplicitEmergencyTerminalLockPath(terminal), { timeoutMs: 30_000 });
3393
+ }
3394
+ catch (fallbackError) {
3395
+ if (isRecord(fallbackError) &&
3396
+ fallbackError.code === "LOCK_TIMEOUT") {
3397
+ throw fallbackError;
3398
+ }
3399
+ warnings.push(`emergency terminal serialization was unavailable; proceeding ` +
3400
+ `with exact pre-input revalidation: ${fallbackError instanceof Error
3401
+ ? fallbackError.message
3402
+ : String(fallbackError)}`);
3403
+ return () => { };
3404
+ }
3405
+ }
3406
+ return () => {
3407
+ try {
3408
+ release?.();
3409
+ }
3410
+ catch (error) {
3411
+ const warning = `terminal serialization cleanup failed: ${error instanceof Error ? error.message : String(error)}`;
3412
+ warnings.push(warning);
3413
+ runtimeLog("warn", "terminal_user_explicit_send_lock_release_failed", {
3414
+ terminal_id: terminal.conversationId,
3415
+ warning
3416
+ });
3417
+ }
3418
+ };
3419
+ }
3420
+ function printReplayedUserExplicitSend(terminal, intent, deliveryMode) {
3421
+ if (deliveryMode === "managed") {
3422
+ printJson({
3423
+ delivered: false,
3424
+ replayed: true,
3425
+ status: "submission_pending_acceptance",
3426
+ submission_outcome: "pending_acceptance",
3427
+ delivery_receipt: "enter_dispatched",
3428
+ do_not_retry: true,
3429
+ management_mode: "managed",
3430
+ terminal_id: terminal.conversationId,
3431
+ message_id: intent.messageId,
3432
+ scope: "terminal_user_explicit"
3433
+ });
3434
+ return;
3435
+ }
3436
+ printJson({
3437
+ delivered: true,
3438
+ delivered_unmanaged: true,
3439
+ callback_expected: false,
3440
+ management_mode: "unmanaged_fallback",
3441
+ replayed: true,
3442
+ terminal_id: terminal.conversationId,
3443
+ message_id: intent.messageId,
3444
+ scope: "terminal_user_explicit"
3445
+ });
3446
+ }
3447
+ function reserveUserExplicitSendIntent(options, messageBody, terminal) {
3448
+ const intent = terminalUserSendIntentContext(options, messageBody, terminal);
3449
+ const routingWarning = stringValue(options.terminalUserSendRoutingWarning);
3450
+ const warnings = routingWarning ? [routingWarning] : [];
3451
+ try {
3452
+ const reservation = intent.repository.reserve(intent.boundary);
3453
+ if (reservation.outcome === "replay") {
3454
+ printReplayedUserExplicitSend(terminal, intent, required(reservation.intent.delivery_mode, "completed explicit Send delivery mode is unavailable"));
3455
+ return { outcome: "replayed" };
3456
+ }
3457
+ if (reservation.outcome === "uncertain") {
3458
+ throw new TerminalUserSendReplayForbiddenError(`explicit terminal Send ${intent.messageId} already reached ` +
3459
+ `${reservation.stage}; automatic replay is forbidden`);
3460
+ }
3461
+ return {
3462
+ outcome: "proceed",
3463
+ lease: { intent, durable: true, warnings }
3464
+ };
3465
+ }
3466
+ catch (error) {
3467
+ if (error instanceof TerminalUserSendIntentBoundaryConflictError ||
3468
+ error instanceof TerminalUserSendIntentUncertainError ||
3469
+ error instanceof TerminalUserSendReplayForbiddenError) {
3470
+ throw error;
3471
+ }
3472
+ // Runtime receipts strengthen same-id retry safety; their own damage is
3473
+ // never authority to suppress a fresh physical user Send.
3474
+ warnings.push(`durable user-Send intent unavailable: ${error instanceof Error ? error.message : String(error)}`);
3475
+ return {
3476
+ outcome: "proceed",
3477
+ lease: { intent, durable: false, warnings }
3478
+ };
3479
+ }
3480
+ }
3481
+ function cancelProvenZeroInputUserExplicitSendIntent(lease, terminal, reason) {
3482
+ if (!lease.durable)
3483
+ return;
3484
+ try {
3485
+ lease.intent.repository.cancelProvenZeroInput(lease.intent.boundary);
3486
+ lease.durable = false;
3487
+ }
3488
+ catch (error) {
3489
+ const warning = `durable zero-input user-Send reservation cleanup failed: ${error instanceof Error ? error.message : String(error)}`;
3490
+ lease.warnings.push(warning);
3491
+ runtimeLog("warn", "terminal_user_explicit_send_intent_cancel_failed", {
3492
+ terminal_id: terminal.conversationId,
3493
+ message_id: lease.intent.messageId,
3494
+ zero_input_reason: reason instanceof Error
3495
+ ? reason.message
3496
+ : String(reason),
3497
+ error: warning
3498
+ });
3499
+ }
3500
+ }
3501
+ function completeUserExplicitSendIntentWhileLocked(lease, deliveryMode) {
3502
+ if (!lease.durable)
3503
+ return;
3504
+ try {
3505
+ lease.intent.repository.complete(lease.intent.boundary, deliveryMode);
3506
+ }
3507
+ catch (error) {
3508
+ lease.durable = false;
3509
+ lease.warnings.push(`durable user-Send completion receipt failed: ${error instanceof Error ? error.message : String(error)}`);
3510
+ }
3511
+ }
3512
+ function completeManagedUserExplicitSendIntent(options, terminal, lease) {
3513
+ let releaseTerminalLock;
3514
+ if (lease.durable) {
767
3515
  try {
768
- return prepareManagedSend({
769
- options,
770
- statePath,
771
- logPath,
772
- messageBody,
773
- stateLockHeld: true,
774
- persist,
775
- rejectTerminalControl
776
- });
3516
+ releaseTerminalLock = acquireTerminalBridgeSendLock(storeDirFromOptions(options), terminal.terminalControl, { timeoutMs: 0 });
3517
+ completeUserExplicitSendIntentWhileLocked(lease, "managed");
3518
+ }
3519
+ catch (error) {
3520
+ lease.durable = false;
3521
+ lease.warnings.push(`durable managed Send completion receipt failed: ${error instanceof Error ? error.message : String(error)}`);
777
3522
  }
778
3523
  finally {
779
- releaseLock();
3524
+ try {
3525
+ releaseTerminalLock?.();
3526
+ }
3527
+ catch (error) {
3528
+ lease.warnings.push(`managed terminal serialization cleanup failed after Send: ${error instanceof Error ? error.message : String(error)}`);
3529
+ }
780
3530
  }
781
3531
  }
782
- const conversation = loadState(statePath);
783
- if (conversation.status !== "waiting_for_openclaw" ||
784
- options.type !== "answer") {
785
- throw new Error(`cannot respond to turn ${turnIdForConversation(conversation)}; ` +
786
- `turn is ${conversation.status}`);
3532
+ if (lease.warnings.length > 0) {
3533
+ runtimeLog("warn", "terminal_user_explicit_send_intent_warning", {
3534
+ terminal_id: terminal.conversationId,
3535
+ message_id: lease.intent.messageId,
3536
+ warnings: lease.warnings
3537
+ });
787
3538
  }
788
- const executor = executorForConversation(conversation);
789
- const type = "answer";
790
- const nativeTakeoverForSend = isRecord(conversation.native_session_takeover)
791
- ? conversation.native_session_takeover
792
- : undefined;
793
- const unresolvedSubmission = unresolvedTerminalBridgeSubmission(conversation);
794
- if (unresolvedSubmission) {
795
- throw new Error(`cannot send to ${conversation.conversation_id}; its previous terminal submission is ` +
796
- `${unresolvedSubmission.status}. Inspect the conversation and terminal pane, then close ` +
797
- "the AKK conversation before creating a replacement task.");
3539
+ }
3540
+ async function runUserExplicitTerminalFallback(options, terminal, managedFailure, intentLease) {
3541
+ const storeDir = storeDirFromOptions(options);
3542
+ let releaseTerminalLock;
3543
+ let terminalLockReleased = false;
3544
+ try {
3545
+ releaseTerminalLock = acquireUserExplicitTerminalSendLock(options, terminal, intentLease.warnings);
798
3546
  }
799
- if (rejectTerminalControl &&
800
- terminalControlFromTakeover(nativeTakeoverForSend)) {
801
- throw new Error("terminal control changed while waiting to send; refresh status and retry");
3547
+ catch (error) {
3548
+ // Both the failed managed path and this failed lock acquisition precede
3549
+ // fallback input. Release the same-id receipt so the user can retry.
3550
+ cancelProvenZeroInputUserExplicitSendIntent(intentLease, terminal, error);
3551
+ runtimeLog("warn", "terminal_user_explicit_send_zero_input", {
3552
+ terminal_id: terminal.conversationId,
3553
+ message_id: intentLease.intent.messageId,
3554
+ error: error instanceof Error ? error.message : String(error),
3555
+ retry_safe: true,
3556
+ intent_warnings: intentLease.warnings
3557
+ });
3558
+ throw error;
802
3559
  }
803
- const message = createMessage({
804
- conversation,
805
- id: stringValue(options.messageId),
806
- from: "openclaw",
807
- to: executor.actor,
808
- type,
809
- body: messageBody,
810
- metadata: {
811
- executor_kind: executor.kind,
812
- executor_session: executor.session
813
- }
814
- });
815
- const nextConversation = {
816
- ...applyMessageToConversation(conversation, message),
817
- executor,
818
- claude_session: executor.kind === "claude"
819
- ? executor.session
820
- : conversation.claude_session
3560
+ const releaseTerminalLockOnce = () => {
3561
+ if (terminalLockReleased)
3562
+ return;
3563
+ terminalLockReleased = true;
3564
+ releaseTerminalLock();
821
3565
  };
822
- if (persist) {
823
- saveState(statePath, nextConversation);
824
- appendEvent(logPath, messageEvent(message));
825
- runtimeLog("info", "message_created", {
826
- conversation_id: conversation.conversation_id,
827
- agent: executor.kind,
828
- executor_session: executor.session,
829
- message_type: type,
830
- state_path: statePath,
831
- event_log_path: logPath,
832
- message: textSummary(messageBody)
3566
+ try {
3567
+ let bridge;
3568
+ let fresh;
3569
+ let runtime;
3570
+ const { payload, messageId } = intentLease.intent;
3571
+ const intentWarnings = intentLease.warnings;
3572
+ try {
3573
+ bridge = createTerminalAgentBridge(options);
3574
+ const resolved = await bridge.resolveConversationId(terminal.conversationId);
3575
+ if (!resolved ||
3576
+ resolved.agent !== terminal.agent ||
3577
+ resolved.pid !== terminal.pid ||
3578
+ !terminalControlsShareIncarnation(resolved.terminalControl, terminal.terminalControl)) {
3579
+ throw new Error("the explicitly selected terminal is no longer the same live process");
3580
+ }
3581
+ fresh = resolved;
3582
+ if (!hasFreshExplicitTerminalSendToken(options, fresh)) {
3583
+ throw new Error("the explicit terminal send token is stale; refresh AKK list");
3584
+ }
3585
+ runtime = terminalRuntimeForLiveIdentity({
3586
+ terminal: fresh,
3587
+ physicalOnly: true
3588
+ });
3589
+ const status = await bridge.status(fresh.agent, fresh.terminalControl, { runtime, scrollbackLines: Number(options.scrollbackLines ?? 120) });
3590
+ assertSafeUserExplicitTerminalSend(status);
3591
+ if (fresh.agent === "codex") {
3592
+ await assertCodexComposerReadyForAutomatedInput({
3593
+ options,
3594
+ terminalControl: fresh.terminalControl
3595
+ });
3596
+ }
3597
+ }
3598
+ catch (error) {
3599
+ cancelProvenZeroInputUserExplicitSendIntent(intentLease, terminal, error);
3600
+ runtimeLog("warn", "terminal_user_explicit_send_zero_input", {
3601
+ terminal_id: terminal.conversationId,
3602
+ message_id: messageId,
3603
+ error: error instanceof Error ? error.message : String(error),
3604
+ retry_safe: true,
3605
+ intent_warnings: intentWarnings
3606
+ });
3607
+ throw error;
3608
+ }
3609
+ try {
3610
+ await bridge.send(fresh.agent, fresh.terminalControl, payload, {
3611
+ runtime,
3612
+ requireExactComposerBeforeEnter: true,
3613
+ beforeText: async ({ terminalControl }) => {
3614
+ const currentStatus = await bridge.status(fresh.agent, terminalControl, {
3615
+ runtime,
3616
+ scrollbackLines: Number(options.scrollbackLines ?? 120)
3617
+ });
3618
+ assertSafeUserExplicitTerminalSend(currentStatus);
3619
+ if (fresh.agent === "codex") {
3620
+ await assertCodexComposerReadyForAutomatedInput({
3621
+ options,
3622
+ terminalControl
3623
+ });
3624
+ }
3625
+ else if (!isExactClaudeNativeInspectionIdleComposer(currentStatus.screen.excerpt ?? "")) {
3626
+ throw new Error("the explicitly selected Claude composer is no longer empty");
3627
+ }
3628
+ }
3629
+ });
3630
+ completeUserExplicitSendIntentWhileLocked(intentLease, "unmanaged");
3631
+ releaseTerminalLockOnce();
3632
+ }
3633
+ catch (error) {
3634
+ const zeroInput = error instanceof TerminalInputNotStartedError;
3635
+ if (zeroInput) {
3636
+ cancelProvenZeroInputUserExplicitSendIntent(intentLease, fresh, error);
3637
+ }
3638
+ runtimeLog(zeroInput ? "warn" : "error", zeroInput
3639
+ ? "terminal_user_explicit_send_zero_input"
3640
+ : "terminal_user_explicit_send_uncertain", {
3641
+ terminal_id: fresh.conversationId,
3642
+ terminal_target: fresh.terminalControl.target,
3643
+ message_id: messageId,
3644
+ error: error instanceof Error ? error.message : String(error),
3645
+ ...(zeroInput
3646
+ ? { retry_safe: true }
3647
+ : { do_not_retry: true }),
3648
+ intent_warnings: intentWarnings
3649
+ });
3650
+ throw error;
3651
+ }
3652
+ let cleanupWarnings;
3653
+ try {
3654
+ cleanupWarnings = await bestEffortReleaseTerminalManagementForExplicitSend({
3655
+ storeDir,
3656
+ terminal: fresh
3657
+ });
3658
+ }
3659
+ catch (error) {
3660
+ cleanupWarnings = [
3661
+ `AKK management release was unavailable: ${error instanceof Error ? error.message : String(error)}`
3662
+ ];
3663
+ }
3664
+ const fallbackReason = managedFailure instanceof Error
3665
+ ? managedFailure.message
3666
+ : String(managedFailure);
3667
+ runtimeLog("warn", "terminal_user_explicit_send_fallback", {
3668
+ terminal_id: fresh.conversationId,
3669
+ terminal_target: fresh.terminalControl.target,
3670
+ message_id: messageId,
3671
+ managed_failure: fallbackReason,
3672
+ delivered_unmanaged: true
3673
+ });
3674
+ printJson({
3675
+ delivered: true,
3676
+ delivered_unmanaged: true,
3677
+ cleanup_warnings: cleanupWarnings,
3678
+ intent_warnings: intentWarnings,
3679
+ callback_expected: false,
3680
+ terminal_id: fresh.conversationId,
3681
+ message_id: messageId,
3682
+ scope: "terminal_user_explicit",
3683
+ management_mode: "unmanaged_fallback",
3684
+ previous_management_release_attempted: true,
3685
+ warning: textSummary(`AKK delivered the user's message after managed-state preparation ` +
3686
+ `failed (${fallbackReason}). No callback Turn was claimed.`),
3687
+ next_action: "refresh AKK list; the live coding agent continues independently of AKK callback state"
833
3688
  });
834
3689
  }
835
- return {
836
- conversation,
837
- executor,
838
- nativeTakeoverForSend,
839
- message,
840
- nextConversation
841
- };
842
- }
843
- async function runSend(options) {
844
- const messageBody = required(options.message ?? options.request, "--message is required");
845
- // Ordinary send/respond owns Turn creation only. Reject native lifecycle
846
- // slash commands before resolving or mutating any Session/Turn state so a
847
- // caller cannot bypass lifecycle capabilities, CAS tokens, transition
848
- // recovery, or binding-generation updates by writing directly to tmux.
849
- terminalSubmissionPayload(messageBody);
850
- if (options.agentHardTimeoutMinutes !== undefined) {
851
- positiveMinutes(options.agentHardTimeoutMinutes, "--agent-hard-timeout-minutes");
3690
+ finally {
3691
+ releaseTerminalLockOnce();
852
3692
  }
853
- if (options.respond === true) {
854
- return runTurnResponse({ options, messageBody });
3693
+ }
3694
+ async function runRawTerminalSend(options, messageBody, terminalConversation) {
3695
+ const suppliedExpectedTerminalToken = stringValue(options.expectedTerminalToken);
3696
+ const userExplicitAuthorityRequested = Boolean(suppliedExpectedTerminalToken && options.managedOnly !== true);
3697
+ const freshExplicitAuthority = userExplicitAuthorityRequested
3698
+ ? hasFreshExplicitTerminalSendToken(options, terminalConversation)
3699
+ : false;
3700
+ if (userExplicitAuthorityRequested &&
3701
+ !freshExplicitAuthority) {
3702
+ throw new Error("the explicit terminal send token is stale; refresh AKK list");
855
3703
  }
856
- if ((options.type ?? "task") !== "task") {
857
- throw new Error("ordinary send only accepts message type task; use respond --turn to answer an in-flight Turn");
3704
+ if (!freshExplicitAuthority) {
3705
+ await runManagedRawTerminalSend(options, messageBody, terminalConversation);
3706
+ return;
858
3707
  }
859
- const terminalConversation = await resolveTerminalConversationFromOptions(options);
860
- if (terminalConversation) {
861
- return runRawTerminalSend(options, messageBody, terminalConversation);
3708
+ assertExpectedHandoffTokenUsesExactTerminalSelector({
3709
+ options,
3710
+ terminal: terminalConversation
3711
+ });
3712
+ const explicitOptions = {
3713
+ ...options,
3714
+ messageId: stringValue(options.messageId) ?? `user-send-${randomUUID()}`
3715
+ };
3716
+ const reservation = reserveUserExplicitSendIntent(explicitOptions, messageBody, terminalConversation);
3717
+ if (reservation.outcome === "replayed")
3718
+ return;
3719
+ const intentLease = reservation.lease;
3720
+ const managedResult = await runManagedRawTerminalSend({
3721
+ ...explicitOptions,
3722
+ expectedUserExplicitTerminalToken: suppliedExpectedTerminalToken,
3723
+ expectedTerminalToken: stringValue(explicitOptions.expectedManagedTerminalToken),
3724
+ expectedManagedTerminalToken: undefined
3725
+ }, messageBody, terminalConversation, true);
3726
+ if (managedResult.outcome !== "zero_input") {
3727
+ if (managedResult.outcome === "replayed" ||
3728
+ managedResult.enterDispatched) {
3729
+ completeManagedUserExplicitSendIntent(explicitOptions, terminalConversation, intentLease);
3730
+ }
3731
+ return;
862
3732
  }
863
- return runManagedSessionSend(options, messageBody);
3733
+ return runUserExplicitTerminalFallback(explicitOptions, terminalConversation, managedResult.failure, intentLease);
864
3734
  }
865
- function rawTerminalInitialAuthority({ options, terminal, storeDir }) {
866
- const claimedSession = soleBoundManagedSessionClaimForTerminal(storeDir, terminal);
867
- const suppliedExpectedTerminalToken = stringValue(options.expectedTerminalToken);
868
- const implicitCodexCandidateAuthority = Boolean(terminal.agent === "codex" &&
869
- suppliedExpectedTerminalToken === undefined &&
870
- claimedSession?.status === "bound" &&
871
- isCompleteNativeRollout(claimedSession.binding?.native_process.rollout));
872
- const knownCodexCompanions = claimedSession
873
- ? codexAllowedCompanionSetForManagedSession({
874
- storeDir,
875
- session: claimedSession
876
- })
877
- : { additional: [] };
878
- return {
879
- claimedSession,
880
- suppliedExpectedTerminalToken,
881
- implicitCodexCandidateAuthority,
882
- knownCodexCompanions
3735
+ async function runManagedRawTerminalSend(options, messageBody, terminalConversation, deferZeroInputFailurePresentation = false) {
3736
+ const attempt = {
3737
+ terminalControlSendInvoked: false,
3738
+ result: undefined
883
3739
  };
884
- }
885
- function assertRawTerminalCandidateAuthority({ terminal, nativeIdentityObservation, deferredCodexCandidateInventory, implicitCodexCandidateAuthority }) {
886
- if (implicitCodexCandidateAuthority &&
887
- !deferredCodexCandidateInventory) {
888
- throw new Error("rollout-backed Codex terminal send requires a fresh complete " +
889
- "nonempty open-root inventory; refresh AKK list before sending");
3740
+ try {
3741
+ return await runManagedRawTerminalSendAttempt(options, messageBody, terminalConversation, deferZeroInputFailurePresentation, attempt);
890
3742
  }
891
- if (nativeIdentityObservation.status === "unavailable" &&
892
- !deferredCodexCandidateInventory) {
893
- throw new Error(`native ${terminal.agent} identity observation is ` +
894
- `unavailable: ${nativeIdentityObservation.reason}`);
3743
+ catch (error) {
3744
+ if (!deferZeroInputFailurePresentation)
3745
+ throw error;
3746
+ if (attempt.result)
3747
+ return attempt.result;
3748
+ if (!attempt.terminalControlSendInvoked) {
3749
+ return { outcome: "zero_input", failure: error };
3750
+ }
3751
+ throw new Error("managed terminal Send may already have started input; refusing an " +
3752
+ `automatic unmanaged fallback: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
895
3753
  }
896
3754
  }
897
- async function runRawTerminalSend(options, messageBody, terminalConversation) {
3755
+ async function runManagedRawTerminalSendAttempt(options, messageBody, terminalConversation, deferZeroInputFailurePresentation, attempt) {
898
3756
  // A token copied from list is authority for exactly the advertised full
899
3757
  // terminal selector. Reject aliases and implicit/no-selector resolution
900
3758
  // before taking locks or touching Store state.
@@ -906,7 +3764,9 @@ async function runRawTerminalSend(options, messageBody, terminalConversation) {
906
3764
  throw new Error("raw terminal sends require --background so AKK can persist and monitor the submission safely");
907
3765
  }
908
3766
  const rawStoreDir = storeDirFromOptions(options);
909
- await withCanonicalMutationLocks(terminalWriterMutationLocks(rawStoreDir, terminalConversation.terminalControl), async (scopes, resources) => {
3767
+ let controlSendResult;
3768
+ await withCanonicalMutationLocks(terminalWriterMutationLocks(rawStoreDir, terminalConversation.terminalControl, deferZeroInputFailurePresentation ? { timeoutMs: 0 } : undefined), async (scopes, resources) => {
3769
+ await assertFreshUserExplicitTerminalSendTargetWhileLocked(options, terminalConversation);
910
3770
  await mutationDispatchLedger.beforeMutation(scopes, resources, options, terminalConversation);
911
3771
  // Upgrade legacy Stores before resolving their Session authority. The
912
3772
  // protocol migration, rather than mutable Turn recency, is the only code
@@ -919,6 +3779,8 @@ async function runRawTerminalSend(options, messageBody, terminalConversation) {
919
3779
  expectedStoreDir: rawStoreDir,
920
3780
  expectedMessageType: "task"
921
3781
  })) {
3782
+ controlSendResult = { outcome: "replayed" };
3783
+ attempt.result = controlSendResult;
922
3784
  return;
923
3785
  }
924
3786
  terminalListCliFacade.assertTerminalIncarnationCanStartTurn(rawStoreDir, terminalConversation.terminalControl);
@@ -1164,7 +4026,8 @@ async function runRawTerminalSend(options, messageBody, terminalConversation) {
1164
4026
  ensureStoreWritable(managed.conversation.store_dir);
1165
4027
  ensureDir(path.dirname(managed.statePath));
1166
4028
  await withTerminalDispatchStateScope(scopes, resources, managed.statePath, managed.logPath, async (dispatchScopes, dispatchResources) => {
1167
- await runTerminalControlSend({
4029
+ attempt.terminalControlSendInvoked = true;
4030
+ controlSendResult = await runTerminalControlSend({
1168
4031
  transaction: {
1169
4032
  scopes: dispatchScopes,
1170
4033
  resources: dispatchResources
@@ -1176,59 +4039,64 @@ async function runRawTerminalSend(options, messageBody, terminalConversation) {
1176
4039
  message: managed.message,
1177
4040
  recordMessageAfterSend: true,
1178
4041
  recordRawAttachmentAfterSend: reusableTurn === undefined,
1179
- onTerminalPreflightVerified: pendingRawAttachSessionCreate
1180
- ? async (route) => {
1181
- const exactRoute = withExactTerminalDispatchRoute(route, {
1182
- terminalControl: terminalConversation.terminalControl,
1183
- terminalKey: terminalBridgeRuntimeKey(terminalConversation.terminalControl),
1184
- storeDir: rawStoreDir,
1185
- statePath: managed.statePath,
1186
- logPath: managed.logPath
1187
- }, (boundRoute) => boundRoute);
1188
- let createdSession;
1189
- if (deferredCodexForegroundBinding) {
1190
- const deferredScope = bindDeferredForegroundApplicationScope(dispatchScopes, dispatchResources);
1191
- const reserved = await deferredForegroundApplication(options, deferredCodexForegroundBinding.terminal).reserve({
1192
- scope: deferredScope,
1193
- boundary: deferredForegroundBoundaryProjection(deferredCodexForegroundBinding),
1194
- targetSession: pendingRawAttachSessionCreate,
1195
- messageId: managed.message.id,
1196
- turnId: turnIdForConversation(managed.conversation)
1197
- });
1198
- createdSession = reserved.createdSession;
1199
- managedSession = createdSession;
1200
- pendingRawAttachSessionCreate = undefined;
1201
- return (rollbackRoute) => {
1202
- assertTerminalDispatchRouteMatches(rollbackRoute, exactRoute);
1203
- reserved.rollback(deferredScope);
1204
- };
1205
- }
1206
- createdSession = saveManagedSession(exactRoute.storeDir, pendingRawAttachSessionCreate, {
1207
- expectedRevision: null
4042
+ deferZeroInputFailurePresentation,
4043
+ onTerminalPreflightVerified: async (route) => {
4044
+ assertFreshUserExplicitTerminalSendToken(options, {
4045
+ ...terminalConversation,
4046
+ terminalControl: route.terminalControl
4047
+ });
4048
+ if (!pendingRawAttachSessionCreate)
4049
+ return;
4050
+ const exactRoute = withExactTerminalDispatchRoute(route, {
4051
+ terminalControl: terminalConversation.terminalControl,
4052
+ terminalKey: terminalBridgeRuntimeKey(terminalConversation.terminalControl),
4053
+ storeDir: rawStoreDir,
4054
+ statePath: managed.statePath,
4055
+ logPath: managed.logPath
4056
+ }, (boundRoute) => boundRoute);
4057
+ let createdSession;
4058
+ if (deferredCodexForegroundBinding) {
4059
+ const deferredScope = bindDeferredForegroundApplicationScope(dispatchScopes, dispatchResources);
4060
+ const reserved = await deferredForegroundApplication(options, deferredCodexForegroundBinding.terminal).reserve({
4061
+ scope: deferredScope,
4062
+ boundary: deferredForegroundBoundaryProjection(deferredCodexForegroundBinding),
4063
+ targetSession: pendingRawAttachSessionCreate,
4064
+ messageId: managed.message.id,
4065
+ turnId: turnIdForConversation(managed.conversation)
1208
4066
  });
4067
+ createdSession = reserved.createdSession;
1209
4068
  managedSession = createdSession;
1210
4069
  pendingRawAttachSessionCreate = undefined;
1211
4070
  return (rollbackRoute) => {
1212
- assertTerminalDispatchRouteMatches(rollbackRoute, route);
1213
- const current = loadManagedSession(rollbackRoute.storeDir, createdSession.session_id);
1214
- if (current.status !== "bound" ||
1215
- current.revision !== createdSession.revision ||
1216
- managedSessionBindingToken(current) !==
1217
- managedSessionBindingToken(createdSession)) {
1218
- throw new Error(`new raw-attach Session ${createdSession.session_id} changed before pre-transport rollback`);
1219
- }
1220
- const detachedAt = cliNow().toISOString();
1221
- managedSession = saveManagedSession(rollbackRoute.storeDir, {
1222
- ...current,
1223
- status: "detached",
1224
- detached_at: detachedAt,
1225
- updated_at: detachedAt
1226
- }, {
1227
- expectedRevision: current.revision
1228
- });
4071
+ assertTerminalDispatchRouteMatches(rollbackRoute, exactRoute);
4072
+ reserved.rollback(deferredScope);
1229
4073
  };
1230
4074
  }
1231
- : undefined,
4075
+ createdSession = saveManagedSession(exactRoute.storeDir, pendingRawAttachSessionCreate, {
4076
+ expectedRevision: null
4077
+ });
4078
+ managedSession = createdSession;
4079
+ pendingRawAttachSessionCreate = undefined;
4080
+ return (rollbackRoute) => {
4081
+ assertTerminalDispatchRouteMatches(rollbackRoute, route);
4082
+ const current = loadManagedSession(rollbackRoute.storeDir, createdSession.session_id);
4083
+ if (current.status !== "bound" ||
4084
+ current.revision !== createdSession.revision ||
4085
+ managedSessionBindingToken(current) !==
4086
+ managedSessionBindingToken(createdSession)) {
4087
+ throw new Error(`new raw-attach Session ${createdSession.session_id} changed before pre-transport rollback`);
4088
+ }
4089
+ const detachedAt = cliNow().toISOString();
4090
+ managedSession = saveManagedSession(rollbackRoute.storeDir, {
4091
+ ...current,
4092
+ status: "detached",
4093
+ detached_at: detachedAt,
4094
+ updated_at: detachedAt
4095
+ }, {
4096
+ expectedRevision: current.revision
4097
+ });
4098
+ };
4099
+ },
1232
4100
  allowedPreMaterializationIdentity,
1233
4101
  allowedAdditionalIdentities,
1234
4102
  observedHandoff: handoff.adopted && handoff.transition
@@ -1240,9 +4108,14 @@ async function runRawTerminalSend(options, messageBody, terminalConversation) {
1240
4108
  verifiedEmptyCodexHandoff: verifiedEmptyHandoff?.boundary,
1241
4109
  deferredCodexForegroundBinding
1242
4110
  });
1243
- });
4111
+ attempt.result = controlSendResult;
4112
+ }, deferZeroInputFailurePresentation ? { timeoutMs: 0 } : undefined);
1244
4113
  });
1245
- return;
4114
+ if (!controlSendResult) {
4115
+ throw new Error("managed terminal Send completed without an outcome");
4116
+ }
4117
+ attempt.result = controlSendResult;
4118
+ return controlSendResult;
1246
4119
  }
1247
4120
  async function runManagedSessionSend(options, messageBody) {
1248
4121
  if (stringValue(options.expectedTerminalToken)) {
@@ -3092,17 +5965,33 @@ function launchAcceptedTerminalMonitor({ request, prepared, conversation, accept
3092
5965
  return undefined;
3093
5966
  }
3094
5967
  }
3095
- function presentTerminalDispatchTransportFailure({ error, request, prepared, application, progress, deferredForegroundScope, bridgeMonitor }) {
5968
+ function presentTerminalDispatchTransportFailure({ error, request, prepared, application, progress, deferredForegroundScope, bridgeMonitor, deferZeroInputFailurePresentation }) {
3096
5969
  const { options, executor, message, deferredCodexForegroundBinding } = request;
3097
5970
  const { lockedStoreDir, terminalControl, presentationContext, presentationPorts } = prepared;
3098
5971
  if (!progress.textInjectedAt &&
3099
5972
  error instanceof TerminalInputNotStartedError) {
3100
- renderTerminalZeroInputAbort(application.recordZeroInputAbort({
3101
- failureKind: "transport",
3102
- error,
3103
- abortedAt: cliNow().toISOString()
3104
- }), presentationContext, presentationPorts, bridgeMonitor?.pid);
3105
- return;
5973
+ let aborted;
5974
+ try {
5975
+ aborted = application.recordZeroInputAbort({
5976
+ failureKind: "transport",
5977
+ error,
5978
+ abortedAt: cliNow().toISOString()
5979
+ });
5980
+ }
5981
+ catch (persistenceError) {
5982
+ if (!deferZeroInputFailurePresentation)
5983
+ throw persistenceError;
5984
+ runtimeLog("warn", "terminal_user_explicit_zero_input_abort_unavailable", {
5985
+ terminal_target: prepared.terminalControl.target,
5986
+ error: persistenceError instanceof Error
5987
+ ? persistenceError.message
5988
+ : String(persistenceError)
5989
+ });
5990
+ }
5991
+ if (!deferZeroInputFailurePresentation && aborted) {
5992
+ renderTerminalZeroInputAbort(aborted, presentationContext, presentationPorts, bridgeMonitor?.pid);
5993
+ }
5994
+ return "zero_input";
3106
5995
  }
3107
5996
  const uncertainAt = cliNow().toISOString();
3108
5997
  const errorMessage = error instanceof Error ? error.message : String(error);
@@ -3123,7 +6012,13 @@ function presentTerminalDispatchTransportFailure({ error, request, prepared, app
3123
6012
  });
3124
6013
  }
3125
6014
  }
3126
- const uncertainConversation = application.applyUncertain(uncertainAt, error);
6015
+ const uncertainConversation = application.applyUncertain(uncertainAt, error, progress.textInjectedAt && !progress.enterDispatchedAt &&
6016
+ error instanceof TerminalEnterDispatchNotAttemptedError
6017
+ ? {
6018
+ enterNotAttemptedAt: uncertainAt,
6019
+ enterNotAttemptedReason: "pre_key_failure"
6020
+ }
6021
+ : {});
3127
6022
  const stalledConversationIds = stallOtherTerminalBridgeConversationsForUncertainDispatch({
3128
6023
  storeDir: lockedStoreDir,
3129
6024
  terminalControl,
@@ -3145,8 +6040,9 @@ function presentTerminalDispatchTransportFailure({ error, request, prepared, app
3145
6040
  enterDispatched: progress.enterDispatchedAt !== undefined,
3146
6041
  monitorPid: bridgeMonitor?.pid
3147
6042
  }, presentationContext, presentationPorts);
6043
+ return "input_started";
3148
6044
  }
3149
- async function runTerminalDispatchTransport({ request, prepared, application, progress, recordPostTransportBookkeepingFailure, deferredForegroundScope }) {
6045
+ async function runTerminalDispatchTransport({ request, prepared, application, progress, recordPostTransportBookkeepingFailure, deferredForegroundScope, deferZeroInputFailurePresentation }) {
3150
6046
  const { executor } = request;
3151
6047
  const { terminalBridge, terminalControl, terminalPayload, preSendRuntime, codexRolloutAcceptanceAnchor, presentationContext, presentationPorts } = prepared;
3152
6048
  let bridgeMonitor;
@@ -3168,7 +6064,11 @@ async function runTerminalDispatchTransport({ request, prepared, application, pr
3168
6064
  const submissionOwner = await resolveTerminalDispatchSubmissionOwner(prepared, request, application, progress.stagedConversation);
3169
6065
  if ("failureReason" in submissionOwner) {
3170
6066
  presentTerminalIdentityFailure(submissionOwner.conversation, submissionOwner.failureReason, presentationContext, presentationPorts, bridgeMonitor?.pid);
3171
- return { outcome: "handled" };
6067
+ return {
6068
+ outcome: "handled",
6069
+ terminalInput: "input_started",
6070
+ enterDispatched: true
6071
+ };
3172
6072
  }
3173
6073
  const acceptance = submissionOwner.deferredCandidateBindingPending
3174
6074
  ? { outcome: "pending_acceptance" }
@@ -3193,46 +6093,71 @@ async function runTerminalDispatchTransport({ request, prepared, application, pr
3193
6093
  };
3194
6094
  }
3195
6095
  catch (error) {
3196
- presentTerminalDispatchTransportFailure({
6096
+ const terminalInput = presentTerminalDispatchTransportFailure({
3197
6097
  error,
3198
6098
  request,
3199
6099
  prepared,
3200
6100
  application,
3201
6101
  progress,
3202
6102
  deferredForegroundScope,
3203
- bridgeMonitor
6103
+ bridgeMonitor,
6104
+ deferZeroInputFailurePresentation
3204
6105
  });
3205
- return { outcome: "handled" };
6106
+ return {
6107
+ outcome: "handled",
6108
+ terminalInput,
6109
+ enterDispatched: progress.enterDispatchedAt !== undefined,
6110
+ failure: error
6111
+ };
3206
6112
  }
3207
6113
  }
3208
6114
  async function runTerminalControlSend(request) {
3209
- const { transaction, recordMessageAfterSend = false, onTerminalPreflightVerified, deferredCodexForegroundBinding } = request;
3210
- const prepared = await prepareTerminalControlSend(request);
6115
+ const { transaction, recordMessageAfterSend = false, onTerminalPreflightVerified, deferredCodexForegroundBinding, deferZeroInputFailurePresentation = false } = request;
6116
+ let prepared;
6117
+ try {
6118
+ prepared = await prepareTerminalControlSend(request);
6119
+ }
6120
+ catch (error) {
6121
+ if (deferZeroInputFailurePresentation) {
6122
+ return { outcome: "zero_input", failure: error };
6123
+ }
6124
+ throw error;
6125
+ }
3211
6126
  if (!prepared) {
3212
- return;
6127
+ return { outcome: "replayed" };
3213
6128
  }
3214
6129
  const { route, presentationContext, presentationPorts } = prepared;
3215
- const deferredForegroundScope = deferredCodexForegroundBinding
3216
- ? bindDeferredForegroundApplicationScope(transaction.scopes, transaction.resources)
3217
- : undefined;
6130
+ let deferredForegroundScope;
3218
6131
  // A newly discovered raw terminal may not have an authoritative Session
3219
6132
  // yet. Commit that Session only after every pre-input terminal and native
3220
6133
  // acceptance check has passed, but before the Turn or dispatch ledger can
3221
6134
  // become durable. This prevents a failed virgin attach from leaving a
3222
6135
  // zero-identity `bound` Session that fences every later control action.
3223
- if (deferredCodexForegroundBinding) {
3224
- deferredForegroundScope?.assertBoundary(deferredForegroundBoundaryProjection(deferredCodexForegroundBinding));
6136
+ let dispatch;
6137
+ try {
6138
+ deferredForegroundScope = deferredCodexForegroundBinding
6139
+ ? bindDeferredForegroundApplicationScope(transaction.scopes, transaction.resources)
6140
+ : undefined;
6141
+ if (deferredCodexForegroundBinding) {
6142
+ deferredForegroundScope?.assertBoundary(deferredForegroundBoundaryProjection(deferredCodexForegroundBinding));
6143
+ }
6144
+ const preTransportRollback = await onTerminalPreflightVerified?.(route);
6145
+ const rollback = terminalDispatchRollbackRepositories({
6146
+ request,
6147
+ prepared,
6148
+ deferredForegroundScope,
6149
+ preTransportRollback: typeof preTransportRollback === "function"
6150
+ ? preTransportRollback
6151
+ : undefined
6152
+ });
6153
+ dispatch = createTerminalDispatchRuntime(request, prepared, rollback);
6154
+ }
6155
+ catch (error) {
6156
+ if (deferZeroInputFailurePresentation) {
6157
+ return { outcome: "zero_input", failure: error };
6158
+ }
6159
+ throw error;
3225
6160
  }
3226
- const preTransportRollback = await onTerminalPreflightVerified?.(route);
3227
- const rollback = terminalDispatchRollbackRepositories({
3228
- request,
3229
- prepared,
3230
- deferredForegroundScope,
3231
- preTransportRollback: typeof preTransportRollback === "function"
3232
- ? preTransportRollback
3233
- : undefined
3234
- });
3235
- const dispatch = createTerminalDispatchRuntime(request, prepared, rollback);
3236
6161
  const { application, progress, recordPostTransportBookkeepingFailure, validatePreparedMessageEvent } = dispatch;
3237
6162
  try {
3238
6163
  // Preserve legacy argument-evaluation priority inside the setup catch:
@@ -3243,13 +6168,29 @@ async function runTerminalControlSend(request) {
3243
6168
  : undefined);
3244
6169
  }
3245
6170
  catch (error) {
3246
- renderTerminalZeroInputAbort(application.recordZeroInputAbort({
3247
- failureKind: "setup",
3248
- error,
3249
- abortedAt: cliNow().toISOString(),
3250
- injectStatePersistenceFailure: cliEnv().AKK_TEST_ABORTED_STATE_PERSISTENCE_FAILURE === "1"
3251
- }), presentationContext, presentationPorts, undefined);
3252
- return;
6171
+ let aborted;
6172
+ try {
6173
+ aborted = application.recordZeroInputAbort({
6174
+ failureKind: "setup",
6175
+ error,
6176
+ abortedAt: cliNow().toISOString(),
6177
+ injectStatePersistenceFailure: cliEnv().AKK_TEST_ABORTED_STATE_PERSISTENCE_FAILURE === "1"
6178
+ });
6179
+ }
6180
+ catch (persistenceError) {
6181
+ if (!deferZeroInputFailurePresentation)
6182
+ throw persistenceError;
6183
+ runtimeLog("warn", "terminal_user_explicit_zero_input_abort_unavailable", {
6184
+ terminal_target: route.terminalControl.target,
6185
+ error: persistenceError instanceof Error
6186
+ ? persistenceError.message
6187
+ : String(persistenceError)
6188
+ });
6189
+ }
6190
+ if (!deferZeroInputFailurePresentation && aborted) {
6191
+ renderTerminalZeroInputAbort(aborted, presentationContext, presentationPorts, undefined);
6192
+ }
6193
+ return { outcome: "zero_input", failure: error };
3253
6194
  }
3254
6195
  const transport = await runTerminalDispatchTransport({
3255
6196
  request,
@@ -3257,10 +6198,16 @@ async function runTerminalControlSend(request) {
3257
6198
  application,
3258
6199
  progress,
3259
6200
  recordPostTransportBookkeepingFailure,
3260
- deferredForegroundScope
6201
+ deferredForegroundScope,
6202
+ deferZeroInputFailurePresentation
3261
6203
  });
3262
6204
  if (transport.outcome === "handled") {
3263
- return;
6205
+ return transport.terminalInput === "zero_input"
6206
+ ? { outcome: "zero_input", failure: transport.failure }
6207
+ : {
6208
+ outcome: "input_started",
6209
+ enterDispatched: transport.enterDispatched
6210
+ };
3264
6211
  }
3265
6212
  const deliveredConversation = transport.conversation;
3266
6213
  const acceptanceResult = transport.acceptance;
@@ -3276,6 +6223,7 @@ async function runTerminalControlSend(request) {
3276
6223
  monitorPid: bridgeMonitor?.pid,
3277
6224
  bookkeepingWarning: progress.bookkeepingWarning
3278
6225
  }, presentationContext, presentationPorts);
6226
+ return { outcome: "input_started", enterDispatched: true };
3279
6227
  }
3280
6228
  export function createTerminalCommandCliFacade(dependencies) {
3281
6229
  const call = (operation) => terminalCommandContext.run(dependencies, operation);