@scotthuang/agent-knock-knock 0.12.13 → 0.12.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (144) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +5 -5
  3. package/dist/src/callback-cli-adapter.d.ts +9 -6
  4. package/dist/src/callback-cli-adapter.js +119 -30
  5. package/dist/src/callback-cli-adapter.js.map +1 -1
  6. package/dist/src/callback-outbox-policy.d.ts +39 -0
  7. package/dist/src/callback-outbox-policy.js +163 -6
  8. package/dist/src/callback-outbox-policy.js.map +1 -1
  9. package/dist/src/callback-outbox-service.d.ts +28 -1
  10. package/dist/src/callback-outbox-service.js +413 -82
  11. package/dist/src/callback-outbox-service.js.map +1 -1
  12. package/dist/src/callback-outbox-settlement.d.ts +14 -1
  13. package/dist/src/callback-outbox-settlement.js +151 -30
  14. package/dist/src/callback-outbox-settlement.js.map +1 -1
  15. package/dist/src/callback-route-authority.d.ts +59 -0
  16. package/dist/src/callback-route-authority.js +123 -0
  17. package/dist/src/callback-route-authority.js.map +1 -0
  18. package/dist/src/callback-transport.d.ts +134 -0
  19. package/dist/src/callback-transport.js +256 -0
  20. package/dist/src/callback-transport.js.map +1 -0
  21. package/dist/src/cli-core.js +17 -4
  22. package/dist/src/cli-core.js.map +1 -1
  23. package/dist/src/codex-lifecycle-compatibility.js +2 -1
  24. package/dist/src/codex-lifecycle-compatibility.js.map +1 -1
  25. package/dist/src/deferred-foreground-application-service.d.ts +18 -0
  26. package/dist/src/deferred-foreground-application-service.js +72 -0
  27. package/dist/src/deferred-foreground-application-service.js.map +1 -1
  28. package/dist/src/deferred-foreground-authority-cli-adapter.js +2 -1
  29. package/dist/src/deferred-foreground-authority-cli-adapter.js.map +1 -1
  30. package/dist/src/deferred-foreground-recovery-cli-adapter.d.ts +1 -1
  31. package/dist/src/deferred-foreground-recovery-cli-adapter.js +108 -37
  32. package/dist/src/deferred-foreground-recovery-cli-adapter.js.map +1 -1
  33. package/dist/src/deferred-foreground-recovery-service.d.ts +1 -1
  34. package/dist/src/deferred-foreground-recovery-service.js +10 -3
  35. package/dist/src/deferred-foreground-recovery-service.js.map +1 -1
  36. package/dist/src/deferred-foreground-transfer-policy.d.ts +1 -1
  37. package/dist/src/deferred-foreground-transfer-policy.js +3 -2
  38. package/dist/src/deferred-foreground-transfer-policy.js.map +1 -1
  39. package/dist/src/deferred-foreground-transfer.d.ts +17 -1
  40. package/dist/src/deferred-foreground-transfer.js +244 -35
  41. package/dist/src/deferred-foreground-transfer.js.map +1 -1
  42. package/dist/src/deferred-foreground-user-close.d.ts +18 -0
  43. package/dist/src/deferred-foreground-user-close.js +180 -0
  44. package/dist/src/deferred-foreground-user-close.js.map +1 -0
  45. package/dist/src/openclaw-callback-transport.d.ts +4 -1
  46. package/dist/src/openclaw-callback-transport.js +403 -10
  47. package/dist/src/openclaw-callback-transport.js.map +1 -1
  48. package/dist/src/openclaw-managed-callback-cli-adapter.d.ts +23 -0
  49. package/dist/src/openclaw-managed-callback-cli-adapter.js +77 -0
  50. package/dist/src/openclaw-managed-callback-cli-adapter.js.map +1 -0
  51. package/dist/src/openclaw-plugin-command-adapter.js +28 -31
  52. package/dist/src/openclaw-plugin-command-adapter.js.map +1 -1
  53. package/dist/src/openclaw-plugin-helpers.js +8 -0
  54. package/dist/src/openclaw-plugin-helpers.js.map +1 -1
  55. package/dist/src/openclaw-plugin-schemas.d.ts +20 -1
  56. package/dist/src/openclaw-plugin-schemas.js +26 -2
  57. package/dist/src/openclaw-plugin-schemas.js.map +1 -1
  58. package/dist/src/protocol.d.ts +3 -0
  59. package/dist/src/protocol.js +4 -0
  60. package/dist/src/protocol.js.map +1 -1
  61. package/dist/src/store.d.ts +9 -0
  62. package/dist/src/store.js +79 -1
  63. package/dist/src/store.js.map +1 -1
  64. package/dist/src/terminal-acceptance-cli-adapter.d.ts +2 -0
  65. package/dist/src/terminal-acceptance-cli-adapter.js +62 -12
  66. package/dist/src/terminal-acceptance-cli-adapter.js.map +1 -1
  67. package/dist/src/terminal-action-projection.d.ts +1 -2
  68. package/dist/src/terminal-action-projection.js +4 -5
  69. package/dist/src/terminal-action-projection.js.map +1 -1
  70. package/dist/src/terminal-agent-bridge.d.ts +82 -0
  71. package/dist/src/terminal-agent-bridge.js +539 -111
  72. package/dist/src/terminal-agent-bridge.js.map +1 -1
  73. package/dist/src/terminal-command-cli-adapter.d.ts +9 -0
  74. package/dist/src/terminal-command-cli-adapter.js +2130 -5
  75. package/dist/src/terminal-command-cli-adapter.js.map +1 -1
  76. package/dist/src/terminal-dispatch-application.d.ts +4 -1
  77. package/dist/src/terminal-dispatch-application.js +11 -2
  78. package/dist/src/terminal-dispatch-application.js.map +1 -1
  79. package/dist/src/terminal-dispatch-execution.js +3 -1
  80. package/dist/src/terminal-dispatch-execution.js.map +1 -1
  81. package/dist/src/terminal-dispatch-ledger-codec.d.ts +3 -0
  82. package/dist/src/terminal-dispatch-ledger-codec.js +46 -19
  83. package/dist/src/terminal-dispatch-ledger-codec.js.map +1 -1
  84. package/dist/src/terminal-dispatch-presenter.js +5 -3
  85. package/dist/src/terminal-dispatch-presenter.js.map +1 -1
  86. package/dist/src/terminal-dispatch-receipt.d.ts +3 -0
  87. package/dist/src/terminal-dispatch-receipt.js +15 -1
  88. package/dist/src/terminal-dispatch-receipt.js.map +1 -1
  89. package/dist/src/terminal-dispatch-recovery-cli-adapter.js +68 -7
  90. package/dist/src/terminal-dispatch-recovery-cli-adapter.js.map +1 -1
  91. package/dist/src/terminal-dispatch-recovery-service.d.ts +2 -0
  92. package/dist/src/terminal-dispatch-recovery-service.js +9 -0
  93. package/dist/src/terminal-dispatch-recovery-service.js.map +1 -1
  94. package/dist/src/terminal-identity-authority-cli-adapter.d.ts +2 -0
  95. package/dist/src/terminal-identity-authority-cli-adapter.js +30 -27
  96. package/dist/src/terminal-identity-authority-cli-adapter.js.map +1 -1
  97. package/dist/src/terminal-list-cli-adapter.d.ts +1 -0
  98. package/dist/src/terminal-list-cli-adapter.js +50 -31
  99. package/dist/src/terminal-list-cli-adapter.js.map +1 -1
  100. package/dist/src/terminal-list-renderer.d.ts +9 -1
  101. package/dist/src/terminal-list-renderer.js +219 -98
  102. package/dist/src/terminal-list-renderer.js.map +1 -1
  103. package/dist/src/terminal-maintenance-cli-adapter.js +253 -326
  104. package/dist/src/terminal-maintenance-cli-adapter.js.map +1 -1
  105. package/dist/src/terminal-monitor-application-service.d.ts +5 -0
  106. package/dist/src/terminal-monitor-application-service.js +5 -0
  107. package/dist/src/terminal-monitor-application-service.js.map +1 -1
  108. package/dist/src/terminal-monitor-cli-adapter.d.ts +25 -1
  109. package/dist/src/terminal-monitor-cli-adapter.js +170 -27
  110. package/dist/src/terminal-monitor-cli-adapter.js.map +1 -1
  111. package/dist/src/terminal-monitor-reconciliation-eligibility.d.ts +1 -1
  112. package/dist/src/terminal-monitor-reconciliation-eligibility.js +220 -32
  113. package/dist/src/terminal-monitor-reconciliation-eligibility.js.map +1 -1
  114. package/dist/src/terminal-monitor-state-cli-adapter.d.ts +1 -1
  115. package/dist/src/terminal-monitor-state-cli-adapter.js +808 -147
  116. package/dist/src/terminal-monitor-state-cli-adapter.js.map +1 -1
  117. package/dist/src/terminal-monitor-state-reconciliation-service.d.ts +2 -1
  118. package/dist/src/terminal-monitor-state-reconciliation-service.js +12 -0
  119. package/dist/src/terminal-monitor-state-reconciliation-service.js.map +1 -1
  120. package/dist/src/terminal-monitor-supervision-cli-adapter.d.ts +2 -0
  121. package/dist/src/terminal-monitor-supervision-cli-adapter.js +31 -11
  122. package/dist/src/terminal-monitor-supervision-cli-adapter.js.map +1 -1
  123. package/dist/src/terminal-status-cli-adapter.d.ts +1 -0
  124. package/dist/src/terminal-status-cli-adapter.js +30 -28
  125. package/dist/src/terminal-status-cli-adapter.js.map +1 -1
  126. package/dist/src/terminal-submission-acceptance.js +78 -25
  127. package/dist/src/terminal-submission-acceptance.js.map +1 -1
  128. package/dist/src/terminal-submission-retry-service.d.ts +114 -0
  129. package/dist/src/terminal-submission-retry-service.js +589 -0
  130. package/dist/src/terminal-submission-retry-service.js.map +1 -0
  131. package/dist/src/terminal-watch-callback-cli-adapter.d.ts +10 -2
  132. package/dist/src/terminal-watch-callback-cli-adapter.js +76 -37
  133. package/dist/src/terminal-watch-callback-cli-adapter.js.map +1 -1
  134. package/dist/src/terminal-watch-cli-adapter.js +41 -17
  135. package/dist/src/terminal-watch-cli-adapter.js.map +1 -1
  136. package/dist/src/terminal-watch-service.d.ts +11 -6
  137. package/dist/src/terminal-watch-service.js +210 -41
  138. package/dist/src/terminal-watch-service.js.map +1 -1
  139. package/dist/src/terminal-watch-store.d.ts +23 -0
  140. package/dist/src/terminal-watch-store.js +162 -4
  141. package/dist/src/terminal-watch-store.js.map +1 -1
  142. package/docs/quickstart-tmux.md +1 -1
  143. package/package.json +1 -1
  144. package/templates/openclaw-skills/agent-knock-knock/SKILL.md +8 -5
@@ -2,15 +2,19 @@
2
2
  import { AsyncLocalStorage } from "node:async_hooks";
3
3
  import { createHash, randomUUID } from "node:crypto";
4
4
  import path from "node:path";
5
+ import { callbackExpectedForConversation, callbackExpectedForConversationWithLegacyFallback, callbackRouteFingerprintForConversation } from "./callback-route-authority.js";
5
6
  import { captureClaudeTranscriptAnchor, defaultClaudeHome } from "./claude-local-transcript-provider.js";
7
+ import { validateCodexRolloutAcceptanceAnchor } from "./terminal-submission-acceptance.js";
6
8
  import { isRecord, nonBlankString as stringValue } from "./value-guards.js";
9
+ import { deferredForegroundActiveMessageId, isDeferredForegroundSubmissionRetryPending } from "./deferred-foreground-transfer.js";
7
10
  import { applyMessageToConversation, budgetAction, createMessage, effectiveTurnStatus, executorForConversation, isTerminalDispatchOwnerReleasedStatus, sessionIdForConversation, turnIdForConversation } from "./protocol.js";
8
11
  import { readNdjsonLog } from "./transcript.js";
9
12
  import { appendEvent, ensureDir, ensureStoreWritable, listConversations, loadState, messageEvent, pathsForConversationDir, saveState, withStoreWriterLeaseAsync } from "./store.js";
10
13
  import { createManagedSessionId, managedSessionBindingToken } from "./managed-session.js";
11
14
  import { loadManagedSession, saveManagedSession, tryLoadManagedSession } from "./session-store.js";
12
15
  import { terminalControlEvidenceMatches } from "./terminal-control-ref.js";
13
- import { TerminalInputNotStartedError } from "./terminal-agent-bridge.js";
16
+ import { TerminalEnterDispatchNotAttemptedError, TerminalInputNotStartedError } from "./terminal-agent-bridge.js";
17
+ import { decideTerminalSubmissionRetry, loadTerminalSubmissionRetry, projectTerminalSubmissionRetryPending, saveTerminalSubmissionRetry, terminalSubmissionRetryLedgerFields, TERMINAL_SUBMISSION_RETRY_SCHEMA, TERMINAL_SUBMISSION_RETRY_VERSION } from "./terminal-submission-retry-service.js";
14
18
  import { evaluateApprovalPolicy } from "./approval-policy.js";
15
19
  import { codexCompanionsPresentInOpenRootInventory, isCompleteNativeRollout, nativeIdentityMatchesCodexPreMaterialization, terminalControlsShareIncarnation } from "./terminal-authority-policy.js";
16
20
  import { decideTerminalSendAuthority } from "./terminal-action-projection.js";
@@ -417,6 +421,7 @@ function replayExactActiveTerminalSubmission({ options, terminalControl, request
417
421
  }
418
422
  });
419
423
  const acceptanceInvalid = replayReceipt.submission_outcome === "uncertain";
424
+ const callbackExpected = callbackExpectedForConversationWithLegacyFallback(owner, ledger.callback_expected);
420
425
  printJson({
421
426
  session_id: sessionIdForConversation(owner),
422
427
  turn_id: turnIdForConversation(owner),
@@ -426,7 +431,7 @@ function replayExactActiveTerminalSubmission({ options, terminalControl, request
426
431
  status: replayReceipt.status,
427
432
  submission_outcome: replayReceipt.submission_outcome,
428
433
  background: true,
429
- callback_expected: !acceptanceInvalid && Boolean(owner.gateway_method ?? ledger.callback_expected),
434
+ callback_expected: !acceptanceInvalid && callbackExpected,
430
435
  terminal_control: terminalControl,
431
436
  executor,
432
437
  replayed: replayReceipt.replayed,
@@ -445,7 +450,7 @@ function replayExactActiveTerminalSubmission({ options, terminalControl, request
445
450
  sessionId: sessionIdForConversation(owner),
446
451
  turnId: turnIdForConversation(owner),
447
452
  source: "terminal_control",
448
- callbackExpected: Boolean(owner.gateway_method ?? ledger.callback_expected)
453
+ callbackExpected
449
454
  })
450
455
  : {
451
456
  action: "inspect",
@@ -682,7 +687,7 @@ function replayExactStoredTerminalSubmission({ options, terminalControl, request
682
687
  }
683
688
  });
684
689
  const acceptanceInvalid = replayReceipt.submission_outcome === "uncertain";
685
- const callbackExpected = Boolean(owner.gateway_method);
690
+ const callbackExpected = callbackExpectedForConversation(owner);
686
691
  printJson({
687
692
  session_id: sessionIdForConversation(owner),
688
693
  turn_id: turnIdForConversation(owner),
@@ -840,7 +845,2121 @@ function prepareManagedSend({ options, statePath, logPath, messageBody, stateLoc
840
845
  nextConversation
841
846
  };
842
847
  }
848
+ function assertTerminalSubmissionRetryOptions(options) {
849
+ const turnId = required(stringValue(options.turn), "--turn is required");
850
+ const allowed = new Set(["turn", "storeDir"]);
851
+ const conflicts = Object.keys(options).filter((option) => options[option] !== undefined && !allowed.has(option));
852
+ if (conflicts.length > 0) {
853
+ throw new Error("send --turn is an exact submission recovery form and cannot be " +
854
+ `combined with ${conflicts.map((option) => `--${option}`).join(", ")}; ` +
855
+ "no terminal input was sent");
856
+ }
857
+ return turnId;
858
+ }
859
+ function terminalSubmissionRetryBaseRecord(input) {
860
+ return {
861
+ schema: TERMINAL_SUBMISSION_RETRY_SCHEMA,
862
+ version: TERMINAL_SUBMISSION_RETRY_VERSION,
863
+ revision: 1,
864
+ attempt_id: input.attemptId,
865
+ mode: input.mode,
866
+ state: input.state,
867
+ store_dir: path.resolve(input.storeDir),
868
+ state_path: path.resolve(input.statePath),
869
+ session_id: sessionIdForConversation(input.conversation),
870
+ turn_id: turnIdForConversation(input.conversation),
871
+ original_message_id: input.originalMessageId,
872
+ active_message_id: input.activeMessageId,
873
+ request_hash: input.requestHash,
874
+ terminal_target: input.terminalControl.target,
875
+ callback_route_fingerprint: callbackRouteFingerprintForConversation(input.conversation) ?? null,
876
+ deferred_foreground_transfer_id: input.deferredTransferId ?? null,
877
+ reserved_at: input.at,
878
+ updated_at: input.at
879
+ };
880
+ }
881
+ function assertTerminalSubmissionRetryAttemptIdentity(input) {
882
+ const attempt = input.attempt;
883
+ if (!attempt)
884
+ return;
885
+ const callbackFingerprint = callbackRouteFingerprintForConversation(input.conversation) ?? null;
886
+ if (attempt.store_dir !== path.resolve(input.storeDir) ||
887
+ attempt.state_path !== path.resolve(input.statePath) ||
888
+ attempt.session_id !== sessionIdForConversation(input.conversation) ||
889
+ attempt.turn_id !== turnIdForConversation(input.conversation) ||
890
+ attempt.original_message_id !== input.originalMessageId ||
891
+ attempt.request_hash !== input.requestHash ||
892
+ attempt.terminal_target !== input.terminalControl.target ||
893
+ attempt.callback_route_fingerprint !== callbackFingerprint ||
894
+ attempt.deferred_foreground_transfer_id !==
895
+ (input.deferredTransferId ?? null)) {
896
+ throw new Error("terminal submission retry authority changed; no terminal input was sent");
897
+ }
898
+ }
899
+ function terminalSubmissionRetryMessageType(submission) {
900
+ const messageType = stringValue(submission.message_type);
901
+ if (messageType !== "task" && messageType !== "answer") {
902
+ throw new Error("terminal submission retry message type is unavailable; no terminal input was sent");
903
+ }
904
+ return messageType;
905
+ }
906
+ function withDeferredTransferSubmissionAuthority(conversation, messageId, transferId) {
907
+ const takeover = isRecord(conversation.native_session_takeover)
908
+ ? conversation.native_session_takeover
909
+ : undefined;
910
+ const submission = terminalBridgeSubmission(conversation);
911
+ if (!takeover || !submission || submission.message_id !== messageId) {
912
+ throw new Error("cannot upgrade deferred transfer authority on a changed submission");
913
+ }
914
+ const existing = stringValue(submission.deferred_foreground_transfer_id);
915
+ if (existing && existing !== transferId) {
916
+ throw new Error("deferred transfer receipt authority conflicts");
917
+ }
918
+ const receiptsValue = takeover.terminal_bridge_submission_receipts;
919
+ if (receiptsValue !== undefined && !Array.isArray(receiptsValue)) {
920
+ throw new Error("terminal submission receipt history is malformed");
921
+ }
922
+ const receipts = (Array.isArray(receiptsValue) ? receiptsValue : []).map((receipt) => {
923
+ if (!isRecord(receipt) || !stringValue(receipt.message_id)) {
924
+ throw new Error("terminal submission receipt history is malformed");
925
+ }
926
+ if (receipt.message_id !== messageId)
927
+ return receipt;
928
+ const receiptTransferId = stringValue(receipt.deferred_foreground_transfer_id);
929
+ if (receiptTransferId && receiptTransferId !== transferId) {
930
+ throw new Error("deferred transfer receipt history conflicts");
931
+ }
932
+ return {
933
+ ...receipt,
934
+ deferred_foreground_transfer_id: transferId
935
+ };
936
+ });
937
+ return {
938
+ ...conversation,
939
+ native_session_takeover: {
940
+ ...takeover,
941
+ deferred_foreground_transfer_id: transferId,
942
+ terminal_bridge_submission: {
943
+ ...submission,
944
+ deferred_foreground_transfer_id: transferId
945
+ },
946
+ terminal_bridge_submission_receipts: receipts
947
+ }
948
+ };
949
+ }
950
+ function terminalSubmissionRetryUnstalled(conversation) {
951
+ const next = {
952
+ ...conversation,
953
+ status: "waiting_for_agent"
954
+ };
955
+ delete next.stalled_at;
956
+ delete next.stalled_reason;
957
+ delete next.failed_at;
958
+ delete next.failure_reason;
959
+ delete next.idle_since;
960
+ return next;
961
+ }
962
+ function withTerminalSubmissionRetryMonitorEpoch(conversation, at) {
963
+ const takeover = isRecord(conversation.native_session_takeover)
964
+ ? conversation.native_session_takeover
965
+ : {};
966
+ const configuredInactivity = Number(takeover.terminal_bridge_inactivity_timeout_minutes);
967
+ const configuredHard = Number(takeover.terminal_bridge_hard_timeout_minutes);
968
+ const inactivityMinutes = Number.isFinite(configuredInactivity) &&
969
+ configuredInactivity > 0
970
+ ? configuredInactivity
971
+ : DEFAULT_AGENT_TIMEOUT_MINUTES;
972
+ const hardMinutes = Number.isFinite(configuredHard) && configuredHard > 0
973
+ ? configuredHard
974
+ : DEFAULT_AGENT_HARD_TIMEOUT_MINUTES;
975
+ return {
976
+ ...conversation,
977
+ native_session_takeover: {
978
+ ...takeover,
979
+ terminal_bridge_started_at: at,
980
+ terminal_bridge_monitor_started_at: at,
981
+ terminal_bridge_last_activity_at: at,
982
+ terminal_bridge_last_activity_reason: "submission retry Enter dispatched",
983
+ terminal_bridge_inactivity_timeout_minutes: inactivityMinutes,
984
+ terminal_bridge_hard_timeout_minutes: hardMinutes,
985
+ terminal_bridge_inactivity_deadline_at: deadlineAt(at, inactivityMinutes),
986
+ terminal_bridge_hard_deadline_at: deadlineAt(at, hardMinutes)
987
+ },
988
+ updated_at: at
989
+ };
990
+ }
991
+ function terminalSubmissionRetryAccepted(input) {
992
+ const currentConversation = assertTerminalSubmissionRetryTurnOpen({
993
+ statePath: input.statePath,
994
+ exactTurnId: turnIdForConversation(input.conversation)
995
+ });
996
+ const messageId = required(stringValue(input.submission.message_id), "terminal submission retry message id is unavailable");
997
+ const preparedAt = required(stringValue(input.submission.prepared_at), "terminal submission retry prepared timestamp is unavailable");
998
+ const acceptedConversation = withTerminalBridgeSubmission({
999
+ conversation: terminalSubmissionRetryUnstalled(currentConversation),
1000
+ messageId,
1001
+ messageType: terminalSubmissionRetryMessageType(input.submission),
1002
+ requestText: input.requestText,
1003
+ status: "agent_accepted",
1004
+ preparedAt,
1005
+ textInjectedAt: stringValue(input.submission.text_injected_at),
1006
+ enterDispatchedAt: stringValue(input.submission.enter_dispatched_at),
1007
+ agentAcceptedAt: input.at,
1008
+ acceptanceEvidence: input.evidence,
1009
+ lastProvenStage: "agent_accepted"
1010
+ });
1011
+ saveState(input.statePath, acceptedConversation);
1012
+ let acceptedAttempt = input.attempt;
1013
+ if (acceptedAttempt && acceptedAttempt.state !== "agent_accepted") {
1014
+ acceptedAttempt = saveTerminalSubmissionRetryForOpenTurn(input.statePath, {
1015
+ ...acceptedAttempt,
1016
+ state: "agent_accepted",
1017
+ agent_accepted_at: input.at,
1018
+ updated_at: input.at
1019
+ }, acceptedAttempt.revision);
1020
+ }
1021
+ mutationDispatchLedger.save(input.scopes, input.resources, {
1022
+ ...input.ledger,
1023
+ ...terminalSubmissionRetryLedgerFields(acceptedAttempt),
1024
+ status: "agent_accepted",
1025
+ text_injected_at: stringValue(input.ledger.text_injected_at),
1026
+ enter_dispatched_at: stringValue(input.ledger.enter_dispatched_at),
1027
+ agent_accepted_at: input.at,
1028
+ acceptance_evidence: input.evidence,
1029
+ dispatcher_pid: null
1030
+ });
1031
+ appendEvent(input.logPath, {
1032
+ ts: input.at,
1033
+ conversation_id: acceptedConversation.conversation_id,
1034
+ event: "terminal_submission_retry_agent_accepted",
1035
+ message_id: messageId,
1036
+ terminal_input_sent: input.terminalInputSent
1037
+ });
1038
+ return { conversation: acceptedConversation, attempt: acceptedAttempt };
1039
+ }
1040
+ function terminalSubmissionRetryTerminalOutcome(input) {
1041
+ const currentConversation = assertTerminalSubmissionRetryTurnOpen({
1042
+ statePath: input.statePath,
1043
+ exactTurnId: turnIdForConversation(input.conversation)
1044
+ });
1045
+ const messageId = required(stringValue(input.submission.message_id), "terminal submission retry message id is unavailable");
1046
+ const preparedAt = required(stringValue(input.submission.prepared_at), "terminal submission retry prepared timestamp is unavailable");
1047
+ const stalled = {
1048
+ ...currentConversation,
1049
+ status: "stalled",
1050
+ stalled_at: input.at,
1051
+ stalled_reason: input.reason,
1052
+ updated_at: input.at
1053
+ };
1054
+ const conversation = withTerminalBridgeSubmission({
1055
+ conversation: stalled,
1056
+ messageId,
1057
+ messageType: terminalSubmissionRetryMessageType(input.submission),
1058
+ requestText: input.requestText,
1059
+ status: input.outcome,
1060
+ preparedAt,
1061
+ textInjectedAt: stringValue(input.submission.text_injected_at),
1062
+ enterDispatchedAt: stringValue(input.submission.enter_dispatched_at),
1063
+ ...(input.outcome === "not_accepted"
1064
+ ? { notAcceptedAt: input.at }
1065
+ : {
1066
+ uncertainAt: input.at,
1067
+ error: input.reason,
1068
+ safeToRetry: false
1069
+ }),
1070
+ lastProvenStage: "enter_dispatched"
1071
+ });
1072
+ saveState(input.statePath, conversation);
1073
+ mutationDispatchLedger.save(input.scopes, input.resources, {
1074
+ ...input.ledger,
1075
+ ...terminalSubmissionRetryLedgerFields(input.attempt),
1076
+ status: input.outcome,
1077
+ ...(input.outcome === "not_accepted"
1078
+ ? {
1079
+ not_accepted_at: input.at,
1080
+ uncertain_at: undefined,
1081
+ error: undefined,
1082
+ safe_to_retry: undefined
1083
+ }
1084
+ : {
1085
+ not_accepted_at: undefined,
1086
+ uncertain_at: input.at,
1087
+ error: input.reason,
1088
+ safe_to_retry: false
1089
+ }),
1090
+ acceptance_evidence: undefined,
1091
+ agent_accepted_at: undefined,
1092
+ dispatcher_pid: null
1093
+ });
1094
+ appendEvent(input.logPath, {
1095
+ ts: input.at,
1096
+ conversation_id: conversation.conversation_id,
1097
+ event: `terminal_submission_retry_${input.outcome}`,
1098
+ message_id: messageId,
1099
+ terminal_input_sent: true,
1100
+ reason: input.reason,
1101
+ do_not_retry: true
1102
+ });
1103
+ return conversation;
1104
+ }
1105
+ function finalizeDeferredTerminalSubmissionRetryAccepted(input) {
1106
+ const conversation = loadState(input.statePath);
1107
+ if (conversation.status === "closed") {
1108
+ throw new Error(`cannot finalize submission retry for closed Turn ` +
1109
+ `${turnIdForConversation(conversation)}; no retry state was changed`);
1110
+ }
1111
+ if (input.attempt &&
1112
+ turnIdForConversation(conversation) !== input.attempt.turn_id) {
1113
+ throw new Error("Turn identity changed during submission retry finalization; no retry " +
1114
+ "state was changed");
1115
+ }
1116
+ const submission = required(terminalBridgeSubmission(conversation), "deferred submission retry acceptance lost its Turn receipt");
1117
+ const ledger = required(mutationDispatchLedger.load(input.scopes, input.resources), "deferred submission retry acceptance lost its dispatch ledger");
1118
+ if (submission.status !== "agent_accepted" ||
1119
+ ledger.status !== "agent_accepted") {
1120
+ throw new Error("deferred submission retry acceptance was not durably finalized");
1121
+ }
1122
+ let attempt = input.attempt;
1123
+ if (attempt && attempt.state !== "agent_accepted") {
1124
+ const acceptedAt = stringValue(submission.agent_accepted_at) ??
1125
+ stringValue(ledger.agent_accepted_at) ?? cliNow().toISOString();
1126
+ attempt = saveTerminalSubmissionRetryForOpenTurn(input.statePath, {
1127
+ ...attempt,
1128
+ state: "agent_accepted",
1129
+ agent_accepted_at: acceptedAt,
1130
+ updated_at: acceptedAt
1131
+ }, attempt.revision);
1132
+ }
1133
+ if (attempt) {
1134
+ mutationDispatchLedger.save(input.scopes, input.resources, {
1135
+ ...ledger,
1136
+ ...terminalSubmissionRetryLedgerFields(attempt)
1137
+ });
1138
+ }
1139
+ return { conversation, attempt };
1140
+ }
1141
+ function reconcileTerminalSubmissionRetryPending(input) {
1142
+ const currentConversation = assertTerminalSubmissionRetryTurnOpen({
1143
+ statePath: input.statePath,
1144
+ exactTurnId: turnIdForConversation(input.conversation)
1145
+ });
1146
+ const { attempt, submission, ledger } = input;
1147
+ const projection = projectTerminalSubmissionRetryPending({
1148
+ attempt,
1149
+ submission,
1150
+ ledger
1151
+ });
1152
+ const { messageId, preparedAt, textInjectedAt, enterDispatchedAt } = projection;
1153
+ if (input.deferred) {
1154
+ const transfer = input.deferred.scope.loadTransfer(input.deferred.transfer.transfer_id);
1155
+ if (!isDeferredForegroundSubmissionRetryPending(transfer)) {
1156
+ throw new Error("deferred submission retry lacks exact pending transfer authority");
1157
+ }
1158
+ input.deferred.transfer = transfer;
1159
+ }
1160
+ const enteredConversation = withTerminalBridgeSubmission({
1161
+ conversation: withTerminalSubmissionRetryMonitorEpoch(terminalSubmissionRetryUnstalled(currentConversation), enterDispatchedAt),
1162
+ messageId,
1163
+ messageType: terminalSubmissionRetryMessageType(submission),
1164
+ requestText: input.requestText,
1165
+ status: "enter_dispatched",
1166
+ preparedAt,
1167
+ textInjectedAt,
1168
+ enterDispatchedAt,
1169
+ lastProvenStage: "enter_dispatched"
1170
+ });
1171
+ saveState(input.statePath, enteredConversation);
1172
+ mutationDispatchLedger.save(input.scopes, input.resources, {
1173
+ ...ledger,
1174
+ ...terminalSubmissionRetryLedgerFields(attempt),
1175
+ status: "enter_dispatched",
1176
+ text_injected_at: textInjectedAt,
1177
+ enter_dispatched_at: enterDispatchedAt,
1178
+ enter_not_attempted_at: undefined,
1179
+ enter_not_attempted_reason: undefined,
1180
+ uncertain_at: undefined,
1181
+ safe_to_retry: undefined,
1182
+ acceptance_evidence: undefined,
1183
+ agent_accepted_at: undefined,
1184
+ not_accepted_at: undefined,
1185
+ dispatcher_pid: null
1186
+ });
1187
+ appendEvent(input.logPath, {
1188
+ ts: enterDispatchedAt,
1189
+ conversation_id: enteredConversation.conversation_id,
1190
+ event: "terminal_submission_retry_pending_reconciled",
1191
+ message_id: messageId,
1192
+ terminal_input_sent: false
1193
+ });
1194
+ return enteredConversation;
1195
+ }
1196
+ const TERMINAL_SUBMISSION_RETRY_LEDGER_KEYS = [
1197
+ "submission_retry_attempt_id",
1198
+ "submission_retry_mode",
1199
+ "submission_retry_state",
1200
+ "submission_retry_revision",
1201
+ "submission_retry_original_message_id",
1202
+ "submission_retry_active_message_id",
1203
+ "submission_retry_reserved_at",
1204
+ "submission_retry_replacement_text_reserved_at",
1205
+ "submission_retry_replacement_text_injected_at",
1206
+ "submission_retry_enter_reserved_at",
1207
+ "submission_retry_enter_dispatched_at"
1208
+ ];
1209
+ function assertTerminalSubmissionRetryDeferredMirror(input) {
1210
+ const { attempt, transfer, submission } = input;
1211
+ const replacement = attempt.mode === "replacement_send";
1212
+ const stateRank = terminalSubmissionRetryTransportRank(attempt);
1213
+ const expectedPreparedAt = stringValue(submission.prepared_at);
1214
+ if (transfer.status !== "uncertain" ||
1215
+ transfer.message_id !== attempt.original_message_id ||
1216
+ transfer.submission_retry_attempt_id !== attempt.attempt_id ||
1217
+ transfer.submission_retry_mode !== attempt.mode ||
1218
+ transfer.submission_retry_message_id !== attempt.active_message_id ||
1219
+ transfer.submission_retry_message_id !== transfer.message_id ||
1220
+ transfer.submission_retry_prepared_at !== expectedPreparedAt ||
1221
+ transfer.submission_retry_prepared_at !== transfer.prepared_at ||
1222
+ transfer.submission_retry_text_reserved_at !==
1223
+ (replacement && stateRank >= 1
1224
+ ? attempt.replacement_text_reserved_at
1225
+ : undefined) ||
1226
+ transfer.submission_retry_text_injected_at !==
1227
+ (replacement && stateRank >= 2
1228
+ ? attempt.replacement_text_injected_at
1229
+ : undefined) ||
1230
+ transfer.submission_retry_enter_reserved_at !==
1231
+ (stateRank >= (replacement ? 3 : 0)
1232
+ ? attempt.enter_reserved_at
1233
+ : undefined) ||
1234
+ transfer.submission_retry_enter_dispatched_at !==
1235
+ (stateRank >= (replacement ? 4 : 1)
1236
+ ? attempt.enter_dispatched_at
1237
+ : undefined) ||
1238
+ (attempt.state === "enter_dispatched"
1239
+ ? transfer.input_stage !== "enter_dispatched" ||
1240
+ transfer.enter_dispatched_at !== attempt.enter_dispatched_at
1241
+ : transfer.input_stage !== "text_injected")) {
1242
+ throw new Error("deferred submission retry mirror conflicts with its durable attempt");
1243
+ }
1244
+ }
1245
+ function assertTerminalSubmissionRetryDeferredMirrorCanReconcile(input) {
1246
+ const { attempt, transfer, submission } = input;
1247
+ const expectedPreparedAt = stringValue(submission.prepared_at);
1248
+ const retryFields = [
1249
+ transfer.submission_retry_attempt_id,
1250
+ transfer.submission_retry_mode,
1251
+ transfer.submission_retry_message_id,
1252
+ transfer.submission_retry_prepared_at,
1253
+ transfer.submission_retry_text_reserved_at,
1254
+ transfer.submission_retry_text_injected_at,
1255
+ transfer.submission_retry_enter_reserved_at,
1256
+ transfer.submission_retry_enter_dispatched_at
1257
+ ];
1258
+ if (transfer.submission_retry_attempt_id === undefined) {
1259
+ const initialAttempt = (attempt.mode === "replacement_send" &&
1260
+ attempt.state === "replacement_reserved") ||
1261
+ (attempt.mode === "exact_draft_enter" &&
1262
+ attempt.state === "enter_reserved");
1263
+ if (!initialAttempt || retryFields.some((value) => value !== undefined)) {
1264
+ throw new Error("deferred submission retry mirror is missing beyond its first recoverable write");
1265
+ }
1266
+ return;
1267
+ }
1268
+ if (transfer.submission_retry_attempt_id !== attempt.attempt_id ||
1269
+ transfer.submission_retry_mode !== attempt.mode ||
1270
+ transfer.submission_retry_message_id !== attempt.active_message_id ||
1271
+ transfer.submission_retry_message_id !== transfer.message_id ||
1272
+ transfer.submission_retry_prepared_at !== expectedPreparedAt ||
1273
+ transfer.submission_retry_prepared_at !== transfer.prepared_at) {
1274
+ throw new Error("deferred submission retry mirror identity conflicts with its durable attempt");
1275
+ }
1276
+ const expectedStages = {
1277
+ submission_retry_text_reserved_at: attempt.mode === "replacement_send"
1278
+ ? attempt.replacement_text_reserved_at
1279
+ : undefined,
1280
+ submission_retry_text_injected_at: attempt.mode === "replacement_send"
1281
+ ? attempt.replacement_text_injected_at
1282
+ : undefined,
1283
+ submission_retry_enter_reserved_at: attempt.enter_reserved_at,
1284
+ submission_retry_enter_dispatched_at: attempt.enter_dispatched_at
1285
+ };
1286
+ for (const field of Object.keys(expectedStages)) {
1287
+ const actual = transfer[field];
1288
+ const expected = expectedStages[field];
1289
+ if (actual !== undefined && actual !== expected) {
1290
+ throw new Error("deferred submission retry mirror stage conflicts with its durable attempt");
1291
+ }
1292
+ }
1293
+ const transferRank = attempt.mode === "replacement_send"
1294
+ ? transfer.submission_retry_enter_dispatched_at !== undefined
1295
+ ? 4
1296
+ : transfer.submission_retry_enter_reserved_at !== undefined
1297
+ ? 3
1298
+ : transfer.submission_retry_text_injected_at !== undefined
1299
+ ? 2
1300
+ : transfer.submission_retry_text_reserved_at !== undefined
1301
+ ? 1
1302
+ : 0
1303
+ : transfer.submission_retry_enter_dispatched_at !== undefined
1304
+ ? 1
1305
+ : transfer.submission_retry_enter_reserved_at !== undefined
1306
+ ? 0
1307
+ : -1;
1308
+ const attemptRank = terminalSubmissionRetryTransportRank(attempt);
1309
+ if (transferRank > attemptRank || attemptRank - transferRank > 1) {
1310
+ throw new Error("deferred submission retry mirror is ahead or more than one stage behind");
1311
+ }
1312
+ }
1313
+ function terminalSubmissionRetryTransportRank(attempt) {
1314
+ const order = attempt.mode === "replacement_send"
1315
+ ? [
1316
+ "replacement_reserved", "replacement_text_reserved",
1317
+ "replacement_text_injected", "enter_reserved", "enter_dispatched"
1318
+ ]
1319
+ : ["enter_reserved", "enter_dispatched"];
1320
+ return order.indexOf(attempt.state);
1321
+ }
1322
+ function reconcileTerminalSubmissionRetryLedgerPrefix(input) {
1323
+ const { attempt, ledger } = input;
1324
+ if (attempt.state === "agent_accepted") {
1325
+ return { ledger, changed: false };
1326
+ }
1327
+ const currentAttemptId = stringValue(ledger.submission_retry_attempt_id);
1328
+ const currentHasRetryFields = TERMINAL_SUBMISSION_RETRY_LEDGER_KEYS.some((key) => ledger[key] !== undefined);
1329
+ if (!currentAttemptId) {
1330
+ if (currentHasRetryFields || attempt.revision !== 1) {
1331
+ throw new Error("terminal submission retry ledger has an unsafe missing retry prefix");
1332
+ }
1333
+ return {
1334
+ ledger: { ...ledger, ...terminalSubmissionRetryLedgerFields(attempt) },
1335
+ changed: true
1336
+ };
1337
+ }
1338
+ const currentState = stringValue(ledger.submission_retry_state);
1339
+ const currentRevision = Number(ledger.submission_retry_revision);
1340
+ const allowedStates = terminalSubmissionRetryLedgerStates(attempt);
1341
+ const same = currentState === attempt.state &&
1342
+ currentRevision === attempt.revision;
1343
+ const immediatelyLagging = currentState === allowedStates.previous &&
1344
+ currentRevision === attempt.revision - 1;
1345
+ if (!same && !immediatelyLagging) {
1346
+ throw new Error("terminal submission retry ledger is ahead, conflicting, or more than one stage behind");
1347
+ }
1348
+ const expectedCurrent = terminalSubmissionRetryLedgerFieldsAtState(attempt, currentState, currentRevision);
1349
+ if (TERMINAL_SUBMISSION_RETRY_LEDGER_KEYS.some((key) => JSON.stringify(ledger[key]) !== JSON.stringify(expectedCurrent[key]))) {
1350
+ throw new Error("terminal submission retry ledger prefix conflicts with its durable attempt");
1351
+ }
1352
+ return same
1353
+ ? { ledger, changed: false }
1354
+ : {
1355
+ ledger: { ...ledger, ...terminalSubmissionRetryLedgerFields(attempt) },
1356
+ changed: true
1357
+ };
1358
+ }
1359
+ function terminalSubmissionRetryLedgerStates(attempt) {
1360
+ const order = attempt.mode === "replacement_send"
1361
+ ? [
1362
+ "replacement_reserved", "replacement_text_reserved",
1363
+ "replacement_text_injected", "enter_reserved", "enter_dispatched"
1364
+ ]
1365
+ : ["enter_reserved", "enter_dispatched"];
1366
+ const index = order.indexOf(attempt.state);
1367
+ return { previous: index > 0 ? order[index - 1] : undefined };
1368
+ }
1369
+ function terminalSubmissionRetryLedgerFieldsAtState(attempt, state, revision) {
1370
+ const fields = {
1371
+ ...terminalSubmissionRetryLedgerFields(attempt),
1372
+ submission_retry_state: state,
1373
+ submission_retry_revision: revision
1374
+ };
1375
+ const rank = {
1376
+ replacement_reserved: 0,
1377
+ replacement_text_reserved: 1,
1378
+ replacement_text_injected: 2,
1379
+ enter_reserved: 3,
1380
+ enter_dispatched: 4,
1381
+ agent_accepted: 5
1382
+ };
1383
+ if (rank[state] < rank.replacement_text_reserved) {
1384
+ delete fields.submission_retry_replacement_text_reserved_at;
1385
+ }
1386
+ if (rank[state] < rank.replacement_text_injected) {
1387
+ delete fields.submission_retry_replacement_text_injected_at;
1388
+ }
1389
+ if (rank[state] < rank.enter_reserved) {
1390
+ delete fields.submission_retry_enter_reserved_at;
1391
+ }
1392
+ if (rank[state] < rank.enter_dispatched) {
1393
+ delete fields.submission_retry_enter_dispatched_at;
1394
+ }
1395
+ return fields;
1396
+ }
1397
+ function printTerminalSubmissionRetryOutcome(input) {
1398
+ printJson({
1399
+ session_id: sessionIdForConversation(input.conversation),
1400
+ turn_id: turnIdForConversation(input.conversation),
1401
+ conversation: input.conversation,
1402
+ delivered: input.outcome === "agent_accepted",
1403
+ delivery_receipt: input.outcome === "agent_accepted"
1404
+ ? "agent_accepted"
1405
+ : input.outcome === "enter_dispatched" ||
1406
+ input.outcome === "not_accepted"
1407
+ ? "enter_dispatched"
1408
+ : "uncertain",
1409
+ status: input.outcome === "agent_accepted"
1410
+ ? "async_pending"
1411
+ : input.outcome === "enter_dispatched"
1412
+ ? "submission_pending"
1413
+ : input.outcome === "not_accepted"
1414
+ ? "submission_not_accepted"
1415
+ : "submission_uncertain",
1416
+ submission_outcome: input.outcome === "enter_dispatched"
1417
+ ? "pending_acceptance"
1418
+ : input.outcome === "not_accepted"
1419
+ ? "not_accepted"
1420
+ : input.outcome === "refused"
1421
+ ? "uncertain"
1422
+ : "agent_accepted",
1423
+ replayed: true,
1424
+ terminal_control: input.terminalControl,
1425
+ terminal_input_sent: input.terminalInputSent,
1426
+ callback_expected: ["refused", "not_accepted"].includes(input.outcome)
1427
+ ? false
1428
+ : callbackExpectedForConversation(input.conversation),
1429
+ ...(input.attempt
1430
+ ? {
1431
+ submission_retry_attempt_id: input.attempt.attempt_id,
1432
+ submission_retry_state: input.attempt.state
1433
+ }
1434
+ : {}),
1435
+ do_not_retry: input.outcome !== "agent_accepted",
1436
+ reason: input.reason
1437
+ });
1438
+ }
1439
+ function loadExactTerminalSubmissionRetryTurn(input) {
1440
+ const conversation = loadState(input.statePath);
1441
+ if (turnIdForConversation(conversation) !== input.exactTurnId) {
1442
+ throw new Error("Turn identity changed during submission retry; no terminal input was " +
1443
+ "sent and no retry state was changed");
1444
+ }
1445
+ return conversation;
1446
+ }
1447
+ function assertTerminalSubmissionRetryTurnOpen(input) {
1448
+ const conversation = loadExactTerminalSubmissionRetryTurn(input);
1449
+ if (conversation.status === "closed") {
1450
+ throw new Error(`cannot retry submission for closed Turn ${input.exactTurnId}; no ` +
1451
+ "terminal input was sent and no retry state was changed");
1452
+ }
1453
+ return conversation;
1454
+ }
1455
+ function saveTerminalSubmissionRetryForOpenTurn(statePath, candidate, expectedRevision) {
1456
+ assertTerminalSubmissionRetryTurnOpen({
1457
+ statePath,
1458
+ exactTurnId: candidate.turn_id
1459
+ });
1460
+ return saveTerminalSubmissionRetry(statePath, candidate, expectedRevision);
1461
+ }
1462
+ function loadTerminalSubmissionRetryLockedAuthority(input) {
1463
+ const { options, exactTurnId, statePath, logPath, storeDir, terminalControl } = input.invocation;
1464
+ const route = bindTerminalDispatchRoute(input.scopes, input.resources);
1465
+ assertTerminalDispatchRouteMatches(route, {
1466
+ terminalControl,
1467
+ terminalKey: terminalBridgeRuntimeKey(terminalControl),
1468
+ storeDir,
1469
+ statePath,
1470
+ logPath
1471
+ });
1472
+ let conversation = assertTerminalSubmissionRetryTurnOpen({
1473
+ statePath,
1474
+ exactTurnId
1475
+ });
1476
+ const takeover = isRecord(conversation.native_session_takeover)
1477
+ ? conversation.native_session_takeover
1478
+ : undefined;
1479
+ const lockedControl = terminalControlFromTakeover(takeover);
1480
+ if (turnIdForConversation(conversation) !== exactTurnId ||
1481
+ executorForConversation(conversation).kind !== "codex" ||
1482
+ !lockedControl ||
1483
+ !terminalControlsShareIncarnation(lockedControl, terminalControl)) {
1484
+ throw new Error("Turn or terminal authority changed before submission retry; no terminal input was sent");
1485
+ }
1486
+ let submission = terminalBridgeSubmission(conversation);
1487
+ if (!submission) {
1488
+ throw new Error("Turn has no terminal submission receipt to recover; no terminal input was sent");
1489
+ }
1490
+ const currentMessageId = required(stringValue(submission.message_id), "terminal submission retry message id is unavailable");
1491
+ const attempt = loadTerminalSubmissionRetry(statePath);
1492
+ const originalMessageId = attempt?.original_message_id ?? currentMessageId;
1493
+ const requestText = terminalSubmissionPayload(required(stringValue(takeover?.terminal_bridge_request_text), "terminal submission retry request text is unavailable"));
1494
+ const requestHash = required(terminalBridgeRequestFingerprint(requestText), "terminal submission retry request hash is unavailable");
1495
+ const messageType = terminalSubmissionRetryMessageType(submission);
1496
+ const messageBodyHash = createHash("sha256").update(requestText).digest("hex");
1497
+ validateStoredTerminalSubmissionMatch({
1498
+ owner: conversation,
1499
+ receipt: submission,
1500
+ options,
1501
+ terminalControl,
1502
+ requestText,
1503
+ requestHash,
1504
+ expectedStoreDir: storeDir,
1505
+ expectedSessionId: sessionIdForConversation(conversation),
1506
+ expectedTurnId: exactTurnId,
1507
+ expectedMessageType: messageType,
1508
+ expectedStatePath: statePath
1509
+ });
1510
+ const ledger = mutationDispatchLedger.load(input.scopes, input.resources);
1511
+ assertTerminalSubmissionRetryLedgerAuthority({
1512
+ ledger,
1513
+ conversation,
1514
+ exactTurnId,
1515
+ currentMessageId,
1516
+ messageType,
1517
+ messageBodyHash,
1518
+ requestHash,
1519
+ storeDir,
1520
+ statePath,
1521
+ terminalControl
1522
+ });
1523
+ let currentLedger = ledger;
1524
+ const takeoverDeferredTransferId = stringValue(takeover?.deferred_foreground_transfer_id);
1525
+ const ledgerDeferredTransferId = stringValue(currentLedger.deferred_foreground_transfer_id);
1526
+ if (takeoverDeferredTransferId &&
1527
+ takeoverDeferredTransferId === ledgerDeferredTransferId &&
1528
+ stringValue(submission.deferred_foreground_transfer_id) === undefined) {
1529
+ conversation = withDeferredTransferSubmissionAuthority(conversation, currentMessageId, takeoverDeferredTransferId);
1530
+ saveState(statePath, conversation);
1531
+ submission = required(terminalBridgeSubmission(conversation), "deferred terminal submission receipt upgrade failed");
1532
+ }
1533
+ const expectedCallbackFingerprint = callbackRouteFingerprintForConversation(conversation) ?? null;
1534
+ if ((currentLedger.callback_route_fingerprint ?? null) !==
1535
+ expectedCallbackFingerprint ||
1536
+ (submission.callback_route_fingerprint ?? null) !==
1537
+ expectedCallbackFingerprint) {
1538
+ throw new Error("terminal submission callback route changed; no terminal input was sent");
1539
+ }
1540
+ const deferredTransferId = exactTerminalSubmissionRetryDeferredTransferId({
1541
+ takeover,
1542
+ submission,
1543
+ ledger: currentLedger
1544
+ });
1545
+ assertTerminalSubmissionRetryAttemptIdentity({
1546
+ attempt,
1547
+ conversation,
1548
+ storeDir,
1549
+ statePath,
1550
+ originalMessageId,
1551
+ requestHash,
1552
+ terminalControl,
1553
+ deferredTransferId
1554
+ });
1555
+ assertTerminalSubmissionRetryGeneration({
1556
+ attempt,
1557
+ takeover,
1558
+ currentMessageId,
1559
+ originalMessageId
1560
+ });
1561
+ const callbackDelivery = isRecord(conversation.callback_delivery)
1562
+ ? conversation.callback_delivery
1563
+ : undefined;
1564
+ return {
1565
+ conversation,
1566
+ takeover,
1567
+ submission,
1568
+ ledger: currentLedger,
1569
+ attempt,
1570
+ currentMessageId,
1571
+ originalMessageId,
1572
+ requestText,
1573
+ requestHash,
1574
+ deferredTransferId,
1575
+ lifecycleSettled: callbackDelivery?.status === "delivered" ||
1576
+ isTerminalDispatchOwnerReleasedStatus(effectiveTurnStatus(conversation))
1577
+ };
1578
+ }
1579
+ function assertTerminalSubmissionRetryLedgerAuthority(input) {
1580
+ const { ledger } = input;
1581
+ if (!ledger || terminalDispatchLedgerLooksLifecycle(ledger) ||
1582
+ stringValue(ledger.conversation_id) !== input.conversation.conversation_id ||
1583
+ stringValue(ledger.session_id) !== sessionIdForConversation(input.conversation) ||
1584
+ stringValue(ledger.turn_id) !== input.exactTurnId ||
1585
+ stringValue(ledger.generation_id) !== input.currentMessageId ||
1586
+ stringValue(ledger.message_id) !== input.currentMessageId ||
1587
+ stringValue(ledger.message_type) !== input.messageType ||
1588
+ stringValue(ledger.message_body_hash) !== input.messageBodyHash ||
1589
+ stringValue(ledger.executor_kind) !== "codex" ||
1590
+ stringValue(ledger.request_hash) !== input.requestHash ||
1591
+ path.resolve(stringValue(ledger.store_dir) ?? "") !==
1592
+ path.resolve(input.storeDir) ||
1593
+ !sameCanonicalStatePath(ledger.state_path, input.statePath) ||
1594
+ !terminalDispatchRecordMatchesControl(ledger, input.terminalControl)) {
1595
+ throw new Error("terminal dispatch ledger does not match the exact Turn generation; no terminal input was sent");
1596
+ }
1597
+ }
1598
+ function exactTerminalSubmissionRetryDeferredTransferId(input) {
1599
+ const references = [
1600
+ stringValue(input.takeover?.deferred_foreground_transfer_id),
1601
+ stringValue(input.submission.deferred_foreground_transfer_id),
1602
+ stringValue(input.ledger.deferred_foreground_transfer_id)
1603
+ ];
1604
+ const hasDeferred = references.some((value) => value !== undefined);
1605
+ if (hasDeferred &&
1606
+ (references.some((value) => value === undefined) ||
1607
+ new Set(references).size !== 1)) {
1608
+ throw new Error("deferred transfer references disagree across Turn and ledger; no terminal input was sent");
1609
+ }
1610
+ return hasDeferred ? references[0] : undefined;
1611
+ }
1612
+ function assertTerminalSubmissionRetryGeneration(input) {
1613
+ const currentGenerationEligible = input.attempt?.mode === "replacement_send"
1614
+ ? input.attempt.state === "replacement_reserved"
1615
+ ? [input.attempt.original_message_id, input.attempt.active_message_id]
1616
+ .includes(input.currentMessageId)
1617
+ : input.currentMessageId === input.attempt.active_message_id
1618
+ : input.currentMessageId === input.originalMessageId;
1619
+ if (stringValue(input.takeover?.terminal_bridge_message_id) !==
1620
+ input.currentMessageId ||
1621
+ !currentGenerationEligible) {
1622
+ throw new Error("terminal submission retry generation changed; no terminal input was sent");
1623
+ }
1624
+ }
1625
+ function reconcileTerminalSubmissionRetryDeferredTransfer(input) {
1626
+ const { attempt } = input;
1627
+ if (!attempt)
1628
+ return input.transfer;
1629
+ const application = deferredForegroundApplication(input.invocation.options, input.invocation.live);
1630
+ const boundary = deferredForegroundBoundaryProjection(input.boundary);
1631
+ let transfer = input.transfer;
1632
+ if (transfer.submission_retry_attempt_id === undefined) {
1633
+ if (attempt.mode === "replacement_send" &&
1634
+ attempt.state === "replacement_reserved") {
1635
+ transfer = application.reserveSubmissionRetry({
1636
+ scope: input.scope,
1637
+ boundary,
1638
+ attemptId: attempt.attempt_id,
1639
+ mode: attempt.mode,
1640
+ messageId: attempt.active_message_id,
1641
+ preparedAt: required(stringValue(input.submission.prepared_at), "deferred retry prepared timestamp is unavailable")
1642
+ });
1643
+ }
1644
+ else if (attempt.mode === "exact_draft_enter" &&
1645
+ attempt.state === "enter_reserved") {
1646
+ transfer = application.reserveSubmissionRetry({
1647
+ scope: input.scope,
1648
+ boundary,
1649
+ attemptId: attempt.attempt_id,
1650
+ mode: attempt.mode,
1651
+ messageId: attempt.active_message_id,
1652
+ preparedAt: required(stringValue(input.submission.prepared_at), "deferred retry prepared timestamp is unavailable")
1653
+ });
1654
+ transfer = application.advanceSubmissionRetry({
1655
+ scope: input.scope,
1656
+ boundary,
1657
+ attemptId: attempt.attempt_id,
1658
+ messageId: attempt.active_message_id,
1659
+ stage: "enter_reserved",
1660
+ at: attempt.enter_reserved_at
1661
+ });
1662
+ }
1663
+ else {
1664
+ throw new Error("deferred retry mirror is incomplete after an irreversible boundary; no terminal input was sent");
1665
+ }
1666
+ }
1667
+ if (!transfer.submission_retry_attempt_id)
1668
+ return transfer;
1669
+ if (attempt.mode === "replacement_send" &&
1670
+ [
1671
+ "replacement_text_reserved", "replacement_text_injected",
1672
+ "enter_reserved", "enter_dispatched"
1673
+ ].includes(attempt.state) &&
1674
+ !transfer.submission_retry_text_reserved_at) {
1675
+ transfer = application.advanceSubmissionRetry({
1676
+ scope: input.scope,
1677
+ boundary,
1678
+ attemptId: attempt.attempt_id,
1679
+ messageId: attempt.active_message_id,
1680
+ stage: "text_reserved",
1681
+ at: attempt.replacement_text_reserved_at
1682
+ });
1683
+ }
1684
+ if (["replacement_text_injected", "enter_reserved", "enter_dispatched"]
1685
+ .includes(attempt.state) &&
1686
+ !transfer.submission_retry_text_injected_at) {
1687
+ transfer = application.advanceSubmissionRetry({
1688
+ scope: input.scope,
1689
+ boundary,
1690
+ attemptId: attempt.attempt_id,
1691
+ messageId: attempt.active_message_id,
1692
+ stage: "text_injected",
1693
+ at: attempt.replacement_text_injected_at ??
1694
+ attempt.enter_reserved_at ?? attempt.reserved_at
1695
+ });
1696
+ }
1697
+ if (["enter_reserved", "enter_dispatched"].includes(attempt.state) &&
1698
+ !transfer.submission_retry_enter_reserved_at) {
1699
+ transfer = application.advanceSubmissionRetry({
1700
+ scope: input.scope,
1701
+ boundary,
1702
+ attemptId: attempt.attempt_id,
1703
+ messageId: attempt.active_message_id,
1704
+ stage: "enter_reserved",
1705
+ at: attempt.enter_reserved_at
1706
+ });
1707
+ }
1708
+ if (attempt.state === "enter_dispatched" &&
1709
+ !transfer.submission_retry_enter_dispatched_at) {
1710
+ transfer = application.advanceSubmissionRetry({
1711
+ scope: input.scope,
1712
+ boundary,
1713
+ attemptId: attempt.attempt_id,
1714
+ messageId: attempt.active_message_id,
1715
+ stage: "enter_dispatched",
1716
+ at: attempt.enter_dispatched_at
1717
+ });
1718
+ }
1719
+ return transfer;
1720
+ }
1721
+ function prepareTerminalSubmissionRetryDeferredContext(input) {
1722
+ assertTerminalSubmissionRetryTurnOpen({
1723
+ statePath: input.invocation.statePath,
1724
+ exactTurnId: input.invocation.exactTurnId
1725
+ });
1726
+ const { deferredTransferId, attempt, submission } = input.authority;
1727
+ if (!deferredTransferId)
1728
+ return { ledger: input.authority.ledger };
1729
+ const scope = bindDeferredForegroundApplicationScope(input.scopes, input.resources);
1730
+ const transfer = scope.loadTransfer(deferredTransferId);
1731
+ const boundary = deferredRecoveryAdapter.deferredCodexBoundaryFromTransfer(deferredForegroundRecoveryAdapterPorts(), { terminal: input.invocation.live, transfer });
1732
+ const projectedBoundary = deferredForegroundBoundaryProjection(boundary);
1733
+ scope.assertBoundary(projectedBoundary);
1734
+ if (attempt && attempt.state !== "agent_accepted") {
1735
+ assertTerminalSubmissionRetryDeferredMirrorCanReconcile({
1736
+ attempt,
1737
+ transfer,
1738
+ submission
1739
+ });
1740
+ }
1741
+ const ledgerMirror = attempt && attempt.state !== "agent_accepted"
1742
+ ? reconcileTerminalSubmissionRetryLedgerPrefix({
1743
+ attempt,
1744
+ ledger: input.authority.ledger
1745
+ })
1746
+ : undefined;
1747
+ const currentTransfer = reconcileTerminalSubmissionRetryDeferredTransfer({
1748
+ invocation: input.invocation,
1749
+ scope,
1750
+ boundary,
1751
+ transfer,
1752
+ attempt,
1753
+ submission
1754
+ });
1755
+ assertTerminalSubmissionRetryDeferredTransferAuthority({
1756
+ invocation: input.invocation,
1757
+ authority: input.authority,
1758
+ transfer: currentTransfer,
1759
+ attempt
1760
+ });
1761
+ deferredForegroundApplication(input.invocation.options, input.invocation.live).assertTransferAuthority(scope, currentTransfer, projectedBoundary);
1762
+ let ledger = input.authority.ledger;
1763
+ if (attempt && attempt.state !== "agent_accepted") {
1764
+ assertTerminalSubmissionRetryDeferredMirror({
1765
+ attempt,
1766
+ transfer: currentTransfer,
1767
+ submission
1768
+ });
1769
+ if (ledgerMirror?.changed) {
1770
+ mutationDispatchLedger.save(input.scopes, input.resources, ledgerMirror.ledger);
1771
+ ledger = ledgerMirror.ledger;
1772
+ }
1773
+ }
1774
+ return {
1775
+ deferred: { scope, transfer: currentTransfer, boundary },
1776
+ ledger
1777
+ };
1778
+ }
1779
+ function assertTerminalSubmissionRetryDeferredTransferAuthority(input) {
1780
+ const { transfer, attempt } = input;
1781
+ const plannedReplacement = Boolean(attempt?.mode === "replacement_send" &&
1782
+ attempt.state === "replacement_reserved" &&
1783
+ transfer.submission_retry_text_reserved_at === undefined);
1784
+ const activeMessageId = deferredForegroundActiveMessageId(transfer);
1785
+ const currentMessageId = input.authority.currentMessageId;
1786
+ if (transfer.turn_id !== input.invocation.exactTurnId ||
1787
+ transfer.target_session_id !==
1788
+ sessionIdForConversation(input.authority.conversation) ||
1789
+ transfer.request_hash !== input.authority.requestHash ||
1790
+ !sameCanonicalStatePath(transfer.state_path, input.invocation.statePath) ||
1791
+ transfer.message_id !== input.authority.originalMessageId ||
1792
+ (plannedReplacement
1793
+ ? ![input.authority.originalMessageId, attempt.active_message_id]
1794
+ .includes(currentMessageId)
1795
+ : activeMessageId !== currentMessageId) ||
1796
+ (attempt
1797
+ ? transfer.submission_retry_attempt_id !== attempt.attempt_id ||
1798
+ transfer.submission_retry_message_id !== attempt.active_message_id
1799
+ : transfer.submission_retry_attempt_id !== undefined)) {
1800
+ throw new Error("deferred submission retry transfer authority changed; no terminal input was sent");
1801
+ }
1802
+ }
1803
+ function reconcileTerminalSubmissionRetryDeferredPending(context, state) {
1804
+ if (!context.deferred || state.attempt?.state !== "enter_dispatched" ||
1805
+ state.submission.status === "agent_accepted" ||
1806
+ state.ledger.status === "agent_accepted") {
1807
+ return false;
1808
+ }
1809
+ state.conversation = reconcileTerminalSubmissionRetryPending({
1810
+ conversation: state.conversation,
1811
+ submission: state.submission,
1812
+ ledger: state.ledger,
1813
+ attempt: state.attempt,
1814
+ requestText: context.requestText,
1815
+ statePath: context.invocation.statePath,
1816
+ logPath: context.invocation.logPath,
1817
+ scopes: context.scopes,
1818
+ resources: context.resources,
1819
+ deferred: context.deferred
1820
+ });
1821
+ state.submission = required(terminalBridgeSubmission(state.conversation), "terminal submission retry receipt disappeared during pending reconciliation");
1822
+ state.ledger = required(mutationDispatchLedger.load(context.scopes, context.resources), "terminal submission retry ledger disappeared during pending reconciliation");
1823
+ return true;
1824
+ }
1825
+ async function recoverPartialTerminalSubmissionRetryAcceptance(context, state) {
1826
+ assertTerminalSubmissionRetryTurnOpen({
1827
+ statePath: context.invocation.statePath,
1828
+ exactTurnId: context.invocation.exactTurnId
1829
+ });
1830
+ const partialAcceptance = state.submission.status === "agent_accepted" ||
1831
+ state.ledger.status === "agent_accepted" ||
1832
+ state.attempt?.state === "agent_accepted";
1833
+ if (!partialAcceptance)
1834
+ return false;
1835
+ const { options, terminalControl, live, statePath, logPath, storeDir } = context.invocation;
1836
+ if (context.deferred &&
1837
+ ["uncertain", "dispatch_started"].includes(context.deferred.transfer.status)) {
1838
+ const recovered = await deferredRecoveryAdapter
1839
+ .recoverAcceptedDeferredForegroundDispatch(deferredForegroundRecoveryAdapterPorts(), {
1840
+ options,
1841
+ scope: context.deferred.scope,
1842
+ storeDir,
1843
+ terminal: live,
1844
+ transfer: context.deferred.transfer,
1845
+ boundary: context.deferred.boundary
1846
+ });
1847
+ assertTerminalSubmissionRetryTurnOpen({
1848
+ statePath,
1849
+ exactTurnId: context.invocation.exactTurnId
1850
+ });
1851
+ if (!recovered) {
1852
+ printTerminalSubmissionRetryOutcome({
1853
+ conversation: state.conversation,
1854
+ terminalControl,
1855
+ attempt: state.attempt,
1856
+ outcome: "refused",
1857
+ terminalInputSent: false,
1858
+ reason: "Partial deferred acceptance lacks current native evidence; no terminal input was sent."
1859
+ });
1860
+ return true;
1861
+ }
1862
+ const finalized = finalizeDeferredTerminalSubmissionRetryAccepted({
1863
+ statePath,
1864
+ scopes: context.scopes,
1865
+ resources: context.resources,
1866
+ attempt: state.attempt
1867
+ });
1868
+ state.conversation = finalized.conversation;
1869
+ state.attempt = finalized.attempt;
1870
+ startTerminalBridgeMonitorForConversation({
1871
+ conversation: state.conversation,
1872
+ statePath,
1873
+ logPath,
1874
+ options
1875
+ });
1876
+ printTerminalSubmissionRetryOutcome({
1877
+ conversation: state.conversation,
1878
+ terminalControl,
1879
+ attempt: state.attempt,
1880
+ outcome: "agent_accepted",
1881
+ terminalInputSent: false,
1882
+ reason: "Deferred native acceptance, source/target Sessions, and monitoring were reconciled without terminal input."
1883
+ });
1884
+ return true;
1885
+ }
1886
+ const durableEvidence = await context.execution.detectAcceptance({
1887
+ executor: "codex",
1888
+ conversation: state.conversation,
1889
+ terminalControl
1890
+ });
1891
+ assertTerminalSubmissionRetryTurnOpen({
1892
+ statePath,
1893
+ exactTurnId: context.invocation.exactTurnId
1894
+ });
1895
+ if (!durableEvidence) {
1896
+ printTerminalSubmissionRetryOutcome({
1897
+ conversation: state.conversation,
1898
+ terminalControl,
1899
+ attempt: state.attempt,
1900
+ outcome: "refused",
1901
+ terminalInputSent: false,
1902
+ reason: "Partial acceptance metadata lacks current native evidence; no terminal input was sent."
1903
+ });
1904
+ return true;
1905
+ }
1906
+ const repaired = terminalSubmissionRetryAccepted({
1907
+ conversation: state.conversation,
1908
+ submission: state.submission,
1909
+ ledger: state.ledger,
1910
+ evidence: durableEvidence,
1911
+ requestText: context.requestText,
1912
+ at: stringValue(state.submission.agent_accepted_at) ??
1913
+ stringValue(state.ledger.agent_accepted_at) ?? cliNow().toISOString(),
1914
+ statePath,
1915
+ logPath,
1916
+ scopes: context.scopes,
1917
+ resources: context.resources,
1918
+ attempt: state.attempt,
1919
+ terminalInputSent: false
1920
+ });
1921
+ state.conversation = repaired.conversation;
1922
+ state.attempt = repaired.attempt;
1923
+ startTerminalBridgeMonitorForConversation({
1924
+ conversation: state.conversation,
1925
+ statePath,
1926
+ logPath,
1927
+ options
1928
+ });
1929
+ printTerminalSubmissionRetryOutcome({
1930
+ conversation: state.conversation,
1931
+ terminalControl,
1932
+ attempt: state.attempt,
1933
+ outcome: "agent_accepted",
1934
+ terminalInputSent: false,
1935
+ reason: "Partial durable acceptance was reconciled without terminal input and monitoring was ensured."
1936
+ });
1937
+ return true;
1938
+ }
1939
+ function terminalSubmissionRetryIsEligible(context, state) {
1940
+ const legacyEligible = !state.attempt &&
1941
+ state.conversation.status === "stalled" &&
1942
+ state.submission.status === "uncertain" &&
1943
+ state.submission.last_proven_stage === "text_injected" &&
1944
+ context.currentMessageId === context.originalMessageId;
1945
+ if (legacyEligible || (state.attempt && state.attempt.state !== "agent_accepted")) {
1946
+ return true;
1947
+ }
1948
+ printTerminalSubmissionRetryOutcome({
1949
+ conversation: state.conversation,
1950
+ terminalControl: context.invocation.terminalControl,
1951
+ attempt: state.attempt,
1952
+ outcome: "refused",
1953
+ terminalInputSent: false,
1954
+ reason: "The Turn is not an eligible incomplete submission recovery; no terminal input was sent."
1955
+ });
1956
+ return false;
1957
+ }
1958
+ async function recoverTerminalSubmissionRetryAcceptance(context, state) {
1959
+ const { options, terminalControl, live, statePath, logPath, storeDir } = context.invocation;
1960
+ const deferredAccepted = context.deferred
1961
+ ? await deferredRecoveryAdapter.recoverAcceptedDeferredForegroundDispatch(deferredForegroundRecoveryAdapterPorts(), {
1962
+ options,
1963
+ scope: context.deferred.scope,
1964
+ storeDir,
1965
+ terminal: live,
1966
+ transfer: context.deferred.transfer,
1967
+ boundary: context.deferred.boundary
1968
+ })
1969
+ : false;
1970
+ assertTerminalSubmissionRetryTurnOpen({
1971
+ statePath,
1972
+ exactTurnId: context.invocation.exactTurnId
1973
+ });
1974
+ if (deferredAccepted) {
1975
+ const finalized = finalizeDeferredTerminalSubmissionRetryAccepted({
1976
+ statePath,
1977
+ scopes: context.scopes,
1978
+ resources: context.resources,
1979
+ attempt: state.attempt
1980
+ });
1981
+ state.conversation = finalized.conversation;
1982
+ state.attempt = finalized.attempt;
1983
+ startTerminalBridgeMonitorForConversation({
1984
+ conversation: state.conversation,
1985
+ statePath,
1986
+ logPath,
1987
+ options
1988
+ });
1989
+ printTerminalSubmissionRetryOutcome({
1990
+ conversation: state.conversation,
1991
+ terminalControl,
1992
+ attempt: state.attempt,
1993
+ outcome: "agent_accepted",
1994
+ terminalInputSent: false,
1995
+ reason: "Deferred native acceptance and source/target Session bindings were reconciled without terminal input."
1996
+ });
1997
+ return true;
1998
+ }
1999
+ const acceptedEvidence = context.deferred
2000
+ ? undefined
2001
+ : await context.execution.detectAcceptance({
2002
+ executor: "codex",
2003
+ conversation: state.conversation,
2004
+ terminalControl
2005
+ });
2006
+ assertTerminalSubmissionRetryTurnOpen({
2007
+ statePath,
2008
+ exactTurnId: context.invocation.exactTurnId
2009
+ });
2010
+ if (!acceptedEvidence)
2011
+ return false;
2012
+ const repaired = terminalSubmissionRetryAccepted({
2013
+ conversation: state.conversation,
2014
+ submission: state.submission,
2015
+ ledger: state.ledger,
2016
+ evidence: acceptedEvidence,
2017
+ requestText: context.requestText,
2018
+ at: cliNow().toISOString(),
2019
+ statePath,
2020
+ logPath,
2021
+ scopes: context.scopes,
2022
+ resources: context.resources,
2023
+ attempt: state.attempt,
2024
+ terminalInputSent: false
2025
+ });
2026
+ state.conversation = repaired.conversation;
2027
+ state.attempt = repaired.attempt;
2028
+ startTerminalBridgeMonitorForConversation({
2029
+ conversation: state.conversation,
2030
+ statePath,
2031
+ logPath,
2032
+ options
2033
+ });
2034
+ printTerminalSubmissionRetryOutcome({
2035
+ conversation: state.conversation,
2036
+ terminalControl,
2037
+ attempt: state.attempt,
2038
+ outcome: "agent_accepted",
2039
+ terminalInputSent: false,
2040
+ reason: "Native Codex acceptance was reconciled; no terminal input was sent."
2041
+ });
2042
+ return true;
2043
+ }
2044
+ function finishPendingTerminalSubmissionRetry(context, state, alreadyReconciled) {
2045
+ if (state.attempt?.state !== "enter_dispatched")
2046
+ return false;
2047
+ assertTerminalSubmissionRetryTurnOpen({
2048
+ statePath: context.invocation.statePath,
2049
+ exactTurnId: context.invocation.exactTurnId
2050
+ });
2051
+ if (!alreadyReconciled) {
2052
+ state.conversation = reconcileTerminalSubmissionRetryPending({
2053
+ conversation: state.conversation,
2054
+ submission: state.submission,
2055
+ ledger: state.ledger,
2056
+ attempt: state.attempt,
2057
+ requestText: context.requestText,
2058
+ statePath: context.invocation.statePath,
2059
+ logPath: context.invocation.logPath,
2060
+ scopes: context.scopes,
2061
+ resources: context.resources,
2062
+ deferred: context.deferred
2063
+ });
2064
+ }
2065
+ startTerminalBridgeMonitorForConversation({
2066
+ conversation: state.conversation,
2067
+ statePath: context.invocation.statePath,
2068
+ logPath: context.invocation.logPath,
2069
+ options: context.invocation.options
2070
+ });
2071
+ printTerminalSubmissionRetryOutcome({
2072
+ conversation: state.conversation,
2073
+ terminalControl: context.invocation.terminalControl,
2074
+ attempt: state.attempt,
2075
+ outcome: "enter_dispatched",
2076
+ terminalInputSent: false,
2077
+ reason: "The durable retry Enter was reconciled without additional terminal input; native acceptance remains pending."
2078
+ });
2079
+ return true;
2080
+ }
2081
+ function terminalSubmissionRetryHasInputAuthority(context, state) {
2082
+ if (!context.deferred ||
2083
+ validateCodexRolloutAcceptanceAnchor(context.takeover?.codex_rollout_acceptance_anchor)?.version === 3) {
2084
+ return true;
2085
+ }
2086
+ printTerminalSubmissionRetryOutcome({
2087
+ conversation: state.conversation,
2088
+ terminalControl: context.invocation.terminalControl,
2089
+ attempt: state.attempt,
2090
+ outcome: "refused",
2091
+ terminalInputSent: false,
2092
+ reason: "Deferred submission retry requires a candidate-set Codex acceptance anchor before any terminal input; no terminal input was sent."
2093
+ });
2094
+ return false;
2095
+ }
2096
+ async function runTerminalSubmissionRetryNoInputRecovery(context, state) {
2097
+ const pendingRetryReconciled = reconcileTerminalSubmissionRetryDeferredPending(context, state);
2098
+ if (await recoverPartialTerminalSubmissionRetryAcceptance(context, state)) {
2099
+ return true;
2100
+ }
2101
+ if (!terminalSubmissionRetryIsEligible(context, state))
2102
+ return true;
2103
+ if (await recoverTerminalSubmissionRetryAcceptance(context, state)) {
2104
+ return true;
2105
+ }
2106
+ if (finishPendingTerminalSubmissionRetry(context, state, pendingRetryReconciled)) {
2107
+ return true;
2108
+ }
2109
+ return !terminalSubmissionRetryHasInputAuthority(context, state);
2110
+ }
2111
+ async function runTerminalSubmissionRetryDecision(context, state) {
2112
+ const { options, bridge, terminalControl, storeDir, statePath, logPath } = context.invocation;
2113
+ if (context.deferred) {
2114
+ await assertDeferredCodexForegroundBindingBoundary({
2115
+ options,
2116
+ scope: context.deferred.scope,
2117
+ boundary: context.deferred.boundary,
2118
+ expectedSourceStatus: "transitioning",
2119
+ requireNoDispatch: false,
2120
+ requireEmptyComposer: false
2121
+ });
2122
+ }
2123
+ const observation = await bridge.observeCodexComposer(terminalControl, context.requestText, { runtime: terminalRuntimeIdentityForConversation(state.conversation, terminalControl) });
2124
+ assertTerminalSubmissionRetryTurnOpen({
2125
+ statePath,
2126
+ exactTurnId: context.invocation.exactTurnId
2127
+ });
2128
+ const decision = decideTerminalSubmissionRetry({
2129
+ agent: "codex",
2130
+ exactTurnTarget: true,
2131
+ accepted: false,
2132
+ composer: observation.state,
2133
+ submissionStatus: stringValue(state.submission.status),
2134
+ lastProvenStage: stringValue(state.submission.last_proven_stage),
2135
+ submissionTextInjectedAt: stringValue(state.submission.text_injected_at),
2136
+ enterDispatchedAt: stringValue(state.submission.enter_dispatched_at),
2137
+ enterNotAttemptedAt: stringValue(state.submission.enter_not_attempted_at),
2138
+ enterNotAttemptedReason: stringValue(state.submission.enter_not_attempted_reason),
2139
+ ledgerStatus: stringValue(state.ledger.status),
2140
+ ledgerTextInjectedAt: stringValue(state.ledger.text_injected_at),
2141
+ ledgerEnterDispatchedAt: stringValue(state.ledger.enter_dispatched_at),
2142
+ ledgerEnterNotAttemptedAt: stringValue(state.ledger.enter_not_attempted_at),
2143
+ ledgerEnterNotAttemptedReason: stringValue(state.ledger.enter_not_attempted_reason),
2144
+ ledgerAgentAcceptedAt: stringValue(state.ledger.agent_accepted_at),
2145
+ originalMessageId: context.originalMessageId,
2146
+ currentMessageId: stringValue(context.takeover?.terminal_bridge_message_id),
2147
+ attempt: state.attempt
2148
+ });
2149
+ if (decision.action === "refuse") {
2150
+ printTerminalSubmissionRetryOutcome({
2151
+ conversation: state.conversation,
2152
+ terminalControl,
2153
+ attempt: state.attempt,
2154
+ outcome: "refused",
2155
+ terminalInputSent: false,
2156
+ reason: `${decision.reason}; no terminal input was sent.`
2157
+ });
2158
+ return;
2159
+ }
2160
+ if (decision.action === "repair_accepted") {
2161
+ throw new Error("retry metadata claims acceptance without native evidence; no terminal input was sent");
2162
+ }
2163
+ if (decision.action === "start_replacement" ||
2164
+ decision.action === "resume_replacement") {
2165
+ await runTerminalSubmissionReplacement({
2166
+ options,
2167
+ bridge,
2168
+ execution: context.execution,
2169
+ observation,
2170
+ decision,
2171
+ conversation: state.conversation,
2172
+ submission: state.submission,
2173
+ ledger: state.ledger,
2174
+ requestText: context.requestText,
2175
+ requestHash: context.requestHash,
2176
+ originalMessageId: context.originalMessageId,
2177
+ terminalControl,
2178
+ storeDir,
2179
+ statePath,
2180
+ logPath,
2181
+ scopes: context.scopes,
2182
+ resources: context.resources,
2183
+ attempt: state.attempt,
2184
+ deferred: context.deferred
2185
+ });
2186
+ return;
2187
+ }
2188
+ await runTerminalSubmissionExactDraftEnter({
2189
+ options,
2190
+ bridge,
2191
+ execution: context.execution,
2192
+ conversation: state.conversation,
2193
+ submission: state.submission,
2194
+ ledger: state.ledger,
2195
+ requestText: context.requestText,
2196
+ requestHash: context.requestHash,
2197
+ originalMessageId: context.originalMessageId,
2198
+ activeMessageId: decision.activeMessageId,
2199
+ terminalControl,
2200
+ storeDir,
2201
+ statePath,
2202
+ logPath,
2203
+ scopes: context.scopes,
2204
+ resources: context.resources,
2205
+ attempt: state.attempt,
2206
+ deferred: context.deferred
2207
+ });
2208
+ }
2209
+ async function runTerminalSubmissionRetryLocked(input) {
2210
+ const freshConversation = loadExactTerminalSubmissionRetryTurn({
2211
+ statePath: input.invocation.statePath,
2212
+ exactTurnId: input.invocation.exactTurnId
2213
+ });
2214
+ if (freshConversation.status === "closed") {
2215
+ printTerminalSubmissionRetryOutcome({
2216
+ conversation: freshConversation,
2217
+ terminalControl: input.invocation.terminalControl,
2218
+ outcome: "refused",
2219
+ terminalInputSent: false,
2220
+ reason: "The Turn was explicitly closed; no terminal input was sent and no " +
2221
+ "retry state was changed."
2222
+ });
2223
+ return;
2224
+ }
2225
+ const authority = loadTerminalSubmissionRetryLockedAuthority(input);
2226
+ if (authority.lifecycleSettled) {
2227
+ printTerminalSubmissionRetryOutcome({
2228
+ conversation: authority.conversation,
2229
+ terminalControl: input.invocation.terminalControl,
2230
+ attempt: authority.attempt,
2231
+ outcome: "refused",
2232
+ terminalInputSent: false,
2233
+ reason: "The Turn lifecycle is already settled; no terminal input was sent."
2234
+ });
2235
+ return;
2236
+ }
2237
+ const prepared = prepareTerminalSubmissionRetryDeferredContext({
2238
+ invocation: input.invocation,
2239
+ authority,
2240
+ scopes: input.scopes,
2241
+ resources: input.resources
2242
+ });
2243
+ const state = {
2244
+ conversation: authority.conversation,
2245
+ submission: authority.submission,
2246
+ ledger: prepared.ledger,
2247
+ attempt: authority.attempt
2248
+ };
2249
+ const context = {
2250
+ invocation: input.invocation,
2251
+ scopes: input.scopes,
2252
+ resources: input.resources,
2253
+ execution: terminalDispatchExecution(input.invocation.options, input.invocation.bridge),
2254
+ deferred: prepared.deferred,
2255
+ takeover: authority.takeover,
2256
+ currentMessageId: authority.currentMessageId,
2257
+ originalMessageId: authority.originalMessageId,
2258
+ requestText: authority.requestText,
2259
+ requestHash: authority.requestHash
2260
+ };
2261
+ if (await runTerminalSubmissionRetryNoInputRecovery(context, state))
2262
+ return;
2263
+ await runTerminalSubmissionRetryDecision(context, state);
2264
+ }
2265
+ async function runTerminalSubmissionRetry(options) {
2266
+ const exactTurnId = assertTerminalSubmissionRetryOptions(options);
2267
+ const loaded = loadConversationFromOptions(options);
2268
+ const { statePath, logPath } = loaded;
2269
+ const storePaths = pathsForConversationDir(path.dirname(statePath));
2270
+ const storeDir = storePaths.storeDir;
2271
+ if (path.resolve(storePaths.statePath) !== path.resolve(statePath) ||
2272
+ path.basename(storePaths.conversationDir) !== exactTurnId ||
2273
+ turnIdForConversation(loaded.conversation) !== exactTurnId) {
2274
+ throw new Error("send --turn did not resolve one canonical Turn; no terminal input was sent");
2275
+ }
2276
+ const initialTakeover = isRecord(loaded.conversation.native_session_takeover)
2277
+ ? loaded.conversation.native_session_takeover
2278
+ : undefined;
2279
+ const storedControl = terminalControlFromTakeover(initialTakeover);
2280
+ const pid = Number(initialTakeover?.terminal_agent_pid);
2281
+ if (!storedControl || !Number.isSafeInteger(pid) || pid <= 1) {
2282
+ throw new Error(`turn ${exactTurnId} is not attached to a live terminal; no terminal input was sent`);
2283
+ }
2284
+ if (executorForConversation(loaded.conversation).kind !== "codex") {
2285
+ throw new Error("terminal submission retry is supported only for Codex; no terminal input was sent");
2286
+ }
2287
+ const bridge = createTerminalAgentBridge(options);
2288
+ const live = await bridge.resolveStoredTerminal("codex", pid, storedControl, terminalRuntimeIdentityForConversation(loaded.conversation, storedControl));
2289
+ if (!terminalControlsShareIncarnation(live.terminalControl, storedControl)) {
2290
+ throw new Error("terminal control changed before submission retry; no terminal input was sent");
2291
+ }
2292
+ const terminalControl = live.terminalControl;
2293
+ await withCanonicalMutationLocks(terminalWriterMutationLocks(storeDir, terminalControl), async (scopes, resources) => withTerminalDispatchStateScope(scopes, resources, statePath, logPath, async (dispatchScopes, dispatchResources) => runTerminalSubmissionRetryLocked({
2294
+ invocation: {
2295
+ options,
2296
+ exactTurnId,
2297
+ statePath,
2298
+ logPath,
2299
+ storeDir,
2300
+ bridge,
2301
+ live,
2302
+ terminalControl
2303
+ },
2304
+ scopes: dispatchScopes,
2305
+ resources: dispatchResources
2306
+ })));
2307
+ }
2308
+ async function runTerminalSubmissionExactDraftEnter(input) {
2309
+ let attempt = input.attempt;
2310
+ const reserveEnter = () => {
2311
+ assertTerminalSubmissionRetryTurnOpen({
2312
+ statePath: input.statePath,
2313
+ exactTurnId: turnIdForConversation(input.conversation)
2314
+ });
2315
+ const at = cliNow().toISOString();
2316
+ const persistAttemptLedger = () => {
2317
+ mutationDispatchLedger.save(input.scopes, input.resources, {
2318
+ ...mutationDispatchLedger.load(input.scopes, input.resources),
2319
+ ...terminalSubmissionRetryLedgerFields(attempt)
2320
+ });
2321
+ };
2322
+ const advanceDeferred = (stage, stageAt) => {
2323
+ if (!input.deferred || !attempt)
2324
+ return;
2325
+ input.deferred.transfer = deferredForegroundApplication(input.options, input.deferred.boundary.terminal).advanceSubmissionRetry({
2326
+ scope: input.deferred.scope,
2327
+ boundary: deferredForegroundBoundaryProjection(input.deferred.boundary),
2328
+ attemptId: attempt.attempt_id,
2329
+ messageId: attempt.active_message_id,
2330
+ stage,
2331
+ at: stageAt
2332
+ });
2333
+ };
2334
+ if (!attempt) {
2335
+ attempt = saveTerminalSubmissionRetryForOpenTurn(input.statePath, {
2336
+ ...terminalSubmissionRetryBaseRecord({
2337
+ mode: "exact_draft_enter",
2338
+ state: "enter_reserved",
2339
+ attemptId: `submission-retry-${randomUUID()}`,
2340
+ storeDir: input.storeDir,
2341
+ statePath: input.statePath,
2342
+ conversation: input.conversation,
2343
+ originalMessageId: input.originalMessageId,
2344
+ activeMessageId: input.activeMessageId,
2345
+ requestHash: input.requestHash,
2346
+ terminalControl: input.terminalControl,
2347
+ at,
2348
+ deferredTransferId: input.deferred?.transfer.transfer_id
2349
+ }),
2350
+ enter_reserved_at: at
2351
+ }, null);
2352
+ if (input.deferred) {
2353
+ input.deferred.transfer = deferredForegroundApplication(input.options, input.deferred.boundary.terminal).reserveSubmissionRetry({
2354
+ scope: input.deferred.scope,
2355
+ boundary: deferredForegroundBoundaryProjection(input.deferred.boundary),
2356
+ attemptId: attempt.attempt_id,
2357
+ mode: attempt.mode,
2358
+ messageId: attempt.active_message_id,
2359
+ preparedAt: required(stringValue(input.submission.prepared_at), "deferred retry prepared timestamp is unavailable")
2360
+ });
2361
+ }
2362
+ advanceDeferred("enter_reserved", at);
2363
+ persistAttemptLedger();
2364
+ return;
2365
+ }
2366
+ if (attempt.mode === "replacement_send" &&
2367
+ attempt.state === "replacement_text_reserved") {
2368
+ attempt = saveTerminalSubmissionRetryForOpenTurn(input.statePath, {
2369
+ ...attempt,
2370
+ state: "replacement_text_injected",
2371
+ replacement_text_injected_at: at,
2372
+ updated_at: at
2373
+ }, attempt.revision);
2374
+ advanceDeferred("text_injected", at);
2375
+ persistAttemptLedger();
2376
+ }
2377
+ if (attempt.state !== "replacement_text_injected") {
2378
+ throw new Error("submission retry Enter is no longer reservable; no key was sent");
2379
+ }
2380
+ const enterReservedAt = cliNow().toISOString();
2381
+ attempt = saveTerminalSubmissionRetryForOpenTurn(input.statePath, {
2382
+ ...attempt,
2383
+ state: "enter_reserved",
2384
+ enter_reserved_at: enterReservedAt,
2385
+ updated_at: enterReservedAt
2386
+ }, attempt.revision);
2387
+ advanceDeferred("enter_reserved", enterReservedAt);
2388
+ persistAttemptLedger();
2389
+ };
2390
+ await input.bridge.submitExactCodexDraft(input.terminalControl, input.requestText, {
2391
+ runtime: terminalRuntimeIdentityForConversation(input.conversation, input.terminalControl),
2392
+ beforeEnterReservation: reserveEnter
2393
+ });
2394
+ assertTerminalSubmissionRetryTurnOpen({
2395
+ statePath: input.statePath,
2396
+ exactTurnId: turnIdForConversation(input.conversation)
2397
+ });
2398
+ if (!attempt || attempt.state !== "enter_reserved") {
2399
+ throw new Error("terminal bridge dispatched Enter without a durable retry reservation");
2400
+ }
2401
+ const enterAt = cliNow().toISOString();
2402
+ attempt = saveTerminalSubmissionRetryForOpenTurn(input.statePath, {
2403
+ ...attempt,
2404
+ state: "enter_dispatched",
2405
+ enter_dispatched_at: enterAt,
2406
+ updated_at: enterAt
2407
+ }, attempt.revision);
2408
+ if (input.deferred) {
2409
+ input.deferred.transfer = deferredForegroundApplication(input.options, input.deferred.boundary.terminal).advanceSubmissionRetry({
2410
+ scope: input.deferred.scope,
2411
+ boundary: deferredForegroundBoundaryProjection(input.deferred.boundary),
2412
+ attemptId: attempt.attempt_id,
2413
+ messageId: attempt.active_message_id,
2414
+ stage: "enter_dispatched",
2415
+ at: enterAt
2416
+ });
2417
+ }
2418
+ const current = loadState(input.statePath);
2419
+ const currentSubmission = required(terminalBridgeSubmission(current), "terminal submission receipt disappeared after retry Enter");
2420
+ const enteredConversation = withTerminalBridgeSubmission({
2421
+ conversation: withTerminalSubmissionRetryMonitorEpoch(terminalSubmissionRetryUnstalled(current), enterAt),
2422
+ messageId: input.activeMessageId,
2423
+ messageType: terminalSubmissionRetryMessageType(currentSubmission),
2424
+ requestText: input.requestText,
2425
+ status: "enter_dispatched",
2426
+ preparedAt: required(stringValue(currentSubmission.prepared_at), "terminal submission retry prepared timestamp is unavailable"),
2427
+ textInjectedAt: stringValue(currentSubmission.text_injected_at) ??
2428
+ attempt.replacement_text_injected_at ?? attempt.reserved_at,
2429
+ enterDispatchedAt: enterAt,
2430
+ lastProvenStage: "enter_dispatched"
2431
+ });
2432
+ saveState(input.statePath, enteredConversation);
2433
+ let enteredLedger = {
2434
+ ...mutationDispatchLedger.load(input.scopes, input.resources),
2435
+ ...terminalSubmissionRetryLedgerFields(attempt),
2436
+ status: "enter_dispatched",
2437
+ text_injected_at: stringValue(currentSubmission.text_injected_at) ??
2438
+ attempt.replacement_text_injected_at ?? attempt.reserved_at,
2439
+ enter_dispatched_at: enterAt,
2440
+ dispatcher_pid: null
2441
+ };
2442
+ mutationDispatchLedger.save(input.scopes, input.resources, enteredLedger);
2443
+ appendEvent(input.logPath, {
2444
+ ts: enterAt,
2445
+ conversation_id: enteredConversation.conversation_id,
2446
+ event: "terminal_submission_retry_enter_dispatched",
2447
+ message_id: input.activeMessageId,
2448
+ terminal_input_sent: true
2449
+ });
2450
+ if (input.deferred) {
2451
+ const transfer = input.deferred.scope.loadTransfer(input.deferred.transfer.transfer_id);
2452
+ const recovered = await deferredRecoveryAdapter
2453
+ .recoverAcceptedDeferredForegroundDispatch(deferredForegroundRecoveryAdapterPorts(), {
2454
+ options: input.options,
2455
+ scope: input.deferred.scope,
2456
+ storeDir: input.storeDir,
2457
+ terminal: input.deferred.boundary.terminal,
2458
+ transfer,
2459
+ boundary: input.deferred.boundary
2460
+ });
2461
+ assertTerminalSubmissionRetryTurnOpen({
2462
+ statePath: input.statePath,
2463
+ exactTurnId: turnIdForConversation(input.conversation)
2464
+ });
2465
+ if (recovered) {
2466
+ const finalized = finalizeDeferredTerminalSubmissionRetryAccepted({
2467
+ statePath: input.statePath,
2468
+ scopes: input.scopes,
2469
+ resources: input.resources,
2470
+ attempt
2471
+ });
2472
+ attempt = required(finalized.attempt, "deferred retry attempt disappeared after accepted finalization");
2473
+ const acceptedConversation = finalized.conversation;
2474
+ startTerminalBridgeMonitorForConversation({
2475
+ conversation: acceptedConversation,
2476
+ statePath: input.statePath,
2477
+ logPath: input.logPath,
2478
+ options: input.options
2479
+ });
2480
+ printTerminalSubmissionRetryOutcome({
2481
+ conversation: acceptedConversation,
2482
+ terminalControl: input.terminalControl,
2483
+ attempt,
2484
+ outcome: "agent_accepted",
2485
+ terminalInputSent: true,
2486
+ reason: "The exact deferred Codex draft was submitted once and its transfer and source/target Sessions were committed."
2487
+ });
2488
+ return;
2489
+ }
2490
+ startTerminalBridgeMonitorForConversation({
2491
+ conversation: enteredConversation,
2492
+ statePath: input.statePath,
2493
+ logPath: input.logPath,
2494
+ options: input.options
2495
+ });
2496
+ printTerminalSubmissionRetryOutcome({
2497
+ conversation: enteredConversation,
2498
+ terminalControl: input.terminalControl,
2499
+ attempt,
2500
+ outcome: "enter_dispatched",
2501
+ terminalInputSent: true,
2502
+ reason: "The exact deferred Codex draft received one Enter; dedicated transfer acceptance remains pending and another retry is forbidden."
2503
+ });
2504
+ return;
2505
+ }
2506
+ const timeoutMs = positiveMilliseconds(input.options.terminalAcceptanceTimeoutMs ??
2507
+ DEFAULT_TERMINAL_ACCEPTANCE_TIMEOUT_MS, "--terminal-acceptance-timeout-ms");
2508
+ const acceptance = await input.execution.pollAcceptance({
2509
+ executor: "codex",
2510
+ conversation: enteredConversation,
2511
+ terminalControl: input.terminalControl,
2512
+ timeoutMs,
2513
+ pollIntervalMs: Math.max(10, Math.min(timeoutMs, Number(input.options.terminalAcceptancePollIntervalMs ??
2514
+ DEFAULT_TERMINAL_ACCEPTANCE_POLL_INTERVAL_MS))),
2515
+ scrollbackLines: Number(input.options.scrollbackLines ?? 240)
2516
+ });
2517
+ assertTerminalSubmissionRetryTurnOpen({
2518
+ statePath: input.statePath,
2519
+ exactTurnId: turnIdForConversation(input.conversation)
2520
+ });
2521
+ if (acceptance.outcome === "agent_accepted") {
2522
+ const repaired = terminalSubmissionRetryAccepted({
2523
+ conversation: enteredConversation,
2524
+ submission: required(terminalBridgeSubmission(enteredConversation), "terminal submission receipt disappeared after retry acceptance"),
2525
+ ledger: enteredLedger,
2526
+ evidence: acceptance.evidence,
2527
+ requestText: input.requestText,
2528
+ at: cliNow().toISOString(),
2529
+ statePath: input.statePath,
2530
+ logPath: input.logPath,
2531
+ scopes: input.scopes,
2532
+ resources: input.resources,
2533
+ attempt,
2534
+ terminalInputSent: true
2535
+ });
2536
+ startTerminalBridgeMonitorForConversation({
2537
+ conversation: repaired.conversation,
2538
+ statePath: input.statePath,
2539
+ logPath: input.logPath,
2540
+ options: input.options
2541
+ });
2542
+ printTerminalSubmissionRetryOutcome({
2543
+ conversation: repaired.conversation,
2544
+ terminalControl: input.terminalControl,
2545
+ attempt: repaired.attempt,
2546
+ outcome: "agent_accepted",
2547
+ terminalInputSent: true,
2548
+ reason: "The exact existing Codex draft was submitted once and native acceptance was proven."
2549
+ });
2550
+ return;
2551
+ }
2552
+ if (acceptance.outcome === "not_accepted") {
2553
+ const notAcceptedAt = cliNow().toISOString();
2554
+ const notAcceptedConversation = terminalSubmissionRetryTerminalOutcome({
2555
+ conversation: enteredConversation,
2556
+ submission: required(terminalBridgeSubmission(enteredConversation), "terminal submission receipt disappeared after retry rejection"),
2557
+ ledger: enteredLedger,
2558
+ requestText: input.requestText,
2559
+ reason: acceptance.reason,
2560
+ at: notAcceptedAt,
2561
+ statePath: input.statePath,
2562
+ logPath: input.logPath,
2563
+ scopes: input.scopes,
2564
+ resources: input.resources,
2565
+ attempt,
2566
+ outcome: "not_accepted"
2567
+ });
2568
+ printTerminalSubmissionRetryOutcome({
2569
+ conversation: notAcceptedConversation,
2570
+ terminalControl: input.terminalControl,
2571
+ attempt,
2572
+ outcome: "not_accepted",
2573
+ terminalInputSent: true,
2574
+ reason: "The exact draft remains in the Codex composer after one Enter; it was durably recorded as not accepted and another retry is forbidden."
2575
+ });
2576
+ return;
2577
+ }
2578
+ if (acceptance.outcome === "uncertain") {
2579
+ const uncertainConversation = terminalSubmissionRetryTerminalOutcome({
2580
+ conversation: enteredConversation,
2581
+ submission: required(terminalBridgeSubmission(enteredConversation), "terminal submission receipt disappeared after uncertain retry acceptance"),
2582
+ ledger: enteredLedger,
2583
+ requestText: input.requestText,
2584
+ reason: acceptance.reason,
2585
+ at: cliNow().toISOString(),
2586
+ statePath: input.statePath,
2587
+ logPath: input.logPath,
2588
+ scopes: input.scopes,
2589
+ resources: input.resources,
2590
+ attempt,
2591
+ outcome: "uncertain"
2592
+ });
2593
+ printTerminalSubmissionRetryOutcome({
2594
+ conversation: uncertainConversation,
2595
+ terminalControl: input.terminalControl,
2596
+ attempt,
2597
+ outcome: "refused",
2598
+ terminalInputSent: true,
2599
+ reason: `Exact-draft acceptance is uncertain (${acceptance.reason}); the terminal outcome is durable and another retry is forbidden.`
2600
+ });
2601
+ return;
2602
+ }
2603
+ startTerminalBridgeMonitorForConversation({
2604
+ conversation: enteredConversation,
2605
+ statePath: input.statePath,
2606
+ logPath: input.logPath,
2607
+ options: input.options
2608
+ });
2609
+ printTerminalSubmissionRetryOutcome({
2610
+ conversation: enteredConversation,
2611
+ terminalControl: input.terminalControl,
2612
+ attempt,
2613
+ outcome: "enter_dispatched",
2614
+ terminalInputSent: true,
2615
+ reason: "The exact existing Codex draft received one Enter; acceptance remains pending and another retry is forbidden."
2616
+ });
2617
+ }
2618
+ async function runTerminalSubmissionReplacement(input) {
2619
+ if (input.observation.state !== "exact_empty") {
2620
+ throw new Error("replacement send lost its positive empty-composer proof; no terminal input was sent");
2621
+ }
2622
+ assertTerminalSubmissionRetryTurnOpen({
2623
+ statePath: input.statePath,
2624
+ exactTurnId: turnIdForConversation(input.conversation)
2625
+ });
2626
+ let attempt = input.attempt;
2627
+ if (!attempt) {
2628
+ const at = cliNow().toISOString();
2629
+ attempt = saveTerminalSubmissionRetryForOpenTurn(input.statePath, terminalSubmissionRetryBaseRecord({
2630
+ mode: "replacement_send",
2631
+ state: "replacement_reserved",
2632
+ attemptId: `submission-retry-${randomUUID()}`,
2633
+ storeDir: input.storeDir,
2634
+ statePath: input.statePath,
2635
+ conversation: input.conversation,
2636
+ originalMessageId: input.originalMessageId,
2637
+ activeMessageId: input.originalMessageId,
2638
+ requestHash: input.requestHash,
2639
+ terminalControl: input.terminalControl,
2640
+ at,
2641
+ deferredTransferId: input.deferred?.transfer.transfer_id
2642
+ }), null);
2643
+ if (input.deferred) {
2644
+ input.deferred.transfer = deferredForegroundApplication(input.options, input.deferred.boundary.terminal).reserveSubmissionRetry({
2645
+ scope: input.deferred.scope,
2646
+ boundary: deferredForegroundBoundaryProjection(input.deferred.boundary),
2647
+ attemptId: attempt.attempt_id,
2648
+ mode: attempt.mode,
2649
+ messageId: attempt.active_message_id,
2650
+ preparedAt: required(stringValue(input.submission.prepared_at), "deferred retry prepared timestamp is unavailable")
2651
+ });
2652
+ }
2653
+ const reservedLedger = required(mutationDispatchLedger.load(input.scopes, input.resources), "terminal submission retry ledger disappeared after reservation");
2654
+ mutationDispatchLedger.save(input.scopes, input.resources, {
2655
+ ...reservedLedger,
2656
+ ...terminalSubmissionRetryLedgerFields(attempt)
2657
+ });
2658
+ }
2659
+ if (attempt.mode !== "replacement_send" ||
2660
+ attempt.state !== "replacement_reserved") {
2661
+ throw new Error("replacement text transport was already reserved; no additional text was sent");
2662
+ }
2663
+ const currentAttempt = () => required(attempt, "terminal submission retry attempt is unavailable");
2664
+ const saveAttempt = (state, at, fields) => {
2665
+ const current = currentAttempt();
2666
+ attempt = saveTerminalSubmissionRetryForOpenTurn(input.statePath, {
2667
+ ...current,
2668
+ ...fields,
2669
+ state,
2670
+ updated_at: at
2671
+ }, current.revision);
2672
+ };
2673
+ const persistRetryLedger = (fields = {}) => {
2674
+ const current = required(mutationDispatchLedger.load(input.scopes, input.resources), "terminal submission retry ledger disappeared");
2675
+ const next = {
2676
+ ...current,
2677
+ ...terminalSubmissionRetryLedgerFields(attempt),
2678
+ ...fields
2679
+ };
2680
+ mutationDispatchLedger.save(input.scopes, input.resources, next);
2681
+ return next;
2682
+ };
2683
+ let enteredConversation;
2684
+ let enteredLedger;
2685
+ await input.bridge.send("codex", input.terminalControl, input.requestText, {
2686
+ runtime: terminalRuntimeIdentityForConversation(input.conversation, input.terminalControl),
2687
+ requireExactComposerBeforeEnter: true,
2688
+ requireExactEmptyComposerAfterBeforeText: {
2689
+ preliminaryComposerDigest: input.observation.digest
2690
+ },
2691
+ beforeText: () => {
2692
+ assertTerminalSubmissionRetryTurnOpen({
2693
+ statePath: input.statePath,
2694
+ exactTurnId: turnIdForConversation(input.conversation)
2695
+ });
2696
+ const at = cliNow().toISOString();
2697
+ saveAttempt("replacement_text_reserved", at, {
2698
+ replacement_text_reserved_at: at
2699
+ });
2700
+ if (input.deferred) {
2701
+ input.deferred.transfer = deferredForegroundApplication(input.options, input.deferred.boundary.terminal).advanceSubmissionRetry({
2702
+ scope: input.deferred.scope,
2703
+ boundary: deferredForegroundBoundaryProjection(input.deferred.boundary),
2704
+ attemptId: currentAttempt().attempt_id,
2705
+ messageId: currentAttempt().active_message_id,
2706
+ stage: "text_reserved",
2707
+ at
2708
+ });
2709
+ }
2710
+ persistRetryLedger();
2711
+ },
2712
+ beforeEnter: () => {
2713
+ assertTerminalSubmissionRetryTurnOpen({
2714
+ statePath: input.statePath,
2715
+ exactTurnId: turnIdForConversation(input.conversation)
2716
+ });
2717
+ const at = cliNow().toISOString();
2718
+ saveAttempt("enter_reserved", at, { enter_reserved_at: at });
2719
+ if (input.deferred) {
2720
+ input.deferred.transfer = deferredForegroundApplication(input.options, input.deferred.boundary.terminal).advanceSubmissionRetry({
2721
+ scope: input.deferred.scope,
2722
+ boundary: deferredForegroundBoundaryProjection(input.deferred.boundary),
2723
+ attemptId: currentAttempt().attempt_id,
2724
+ messageId: currentAttempt().active_message_id,
2725
+ stage: "enter_reserved",
2726
+ at
2727
+ });
2728
+ }
2729
+ persistRetryLedger();
2730
+ },
2731
+ onTransportStage: ({ stage }) => {
2732
+ assertTerminalSubmissionRetryTurnOpen({
2733
+ statePath: input.statePath,
2734
+ exactTurnId: turnIdForConversation(input.conversation)
2735
+ });
2736
+ const at = cliNow().toISOString();
2737
+ if (stage === "text_injected") {
2738
+ saveAttempt("replacement_text_injected", at, {
2739
+ replacement_text_injected_at: at
2740
+ });
2741
+ }
2742
+ else {
2743
+ saveAttempt("enter_dispatched", at, {
2744
+ enter_dispatched_at: at
2745
+ });
2746
+ }
2747
+ if (input.deferred) {
2748
+ input.deferred.transfer = deferredForegroundApplication(input.options, input.deferred.boundary.terminal).advanceSubmissionRetry({
2749
+ scope: input.deferred.scope,
2750
+ boundary: deferredForegroundBoundaryProjection(input.deferred.boundary),
2751
+ attemptId: currentAttempt().attempt_id,
2752
+ messageId: currentAttempt().active_message_id,
2753
+ stage,
2754
+ at
2755
+ });
2756
+ }
2757
+ if (stage === "text_injected") {
2758
+ persistRetryLedger();
2759
+ return;
2760
+ }
2761
+ const current = loadState(input.statePath);
2762
+ const submission = required(terminalBridgeSubmission(current), "terminal submission receipt disappeared after replacement Enter");
2763
+ enteredConversation = withTerminalBridgeSubmission({
2764
+ conversation: withTerminalSubmissionRetryMonitorEpoch(terminalSubmissionRetryUnstalled(current), at),
2765
+ messageId: input.originalMessageId,
2766
+ messageType: terminalSubmissionRetryMessageType(submission),
2767
+ requestText: input.requestText,
2768
+ status: "enter_dispatched",
2769
+ preparedAt: required(stringValue(submission.prepared_at), "terminal submission retry prepared timestamp is unavailable"),
2770
+ textInjectedAt: currentAttempt().replacement_text_injected_at,
2771
+ enterDispatchedAt: at,
2772
+ lastProvenStage: "enter_dispatched"
2773
+ });
2774
+ saveState(input.statePath, enteredConversation);
2775
+ enteredLedger = persistRetryLedger({
2776
+ status: "enter_dispatched",
2777
+ text_injected_at: currentAttempt().replacement_text_injected_at,
2778
+ enter_dispatched_at: at,
2779
+ enter_not_attempted_at: undefined,
2780
+ enter_not_attempted_reason: undefined,
2781
+ uncertain_at: undefined,
2782
+ safe_to_retry: undefined,
2783
+ acceptance_evidence: undefined,
2784
+ agent_accepted_at: undefined,
2785
+ not_accepted_at: undefined,
2786
+ dispatcher_pid: null
2787
+ });
2788
+ appendEvent(input.logPath, {
2789
+ ts: at,
2790
+ conversation_id: enteredConversation.conversation_id,
2791
+ event: "terminal_submission_retry_enter_dispatched",
2792
+ message_id: input.originalMessageId,
2793
+ terminal_input_sent: true
2794
+ });
2795
+ }
2796
+ });
2797
+ assertTerminalSubmissionRetryTurnOpen({
2798
+ statePath: input.statePath,
2799
+ exactTurnId: turnIdForConversation(input.conversation)
2800
+ });
2801
+ if (currentAttempt().state !== "enter_dispatched" ||
2802
+ !enteredConversation || !enteredLedger) {
2803
+ throw new Error("replacement transport returned without durable Enter evidence");
2804
+ }
2805
+ if (input.deferred) {
2806
+ const transfer = input.deferred.scope.loadTransfer(input.deferred.transfer.transfer_id);
2807
+ const recovered = await deferredRecoveryAdapter
2808
+ .recoverAcceptedDeferredForegroundDispatch(deferredForegroundRecoveryAdapterPorts(), {
2809
+ options: input.options,
2810
+ scope: input.deferred.scope,
2811
+ storeDir: input.storeDir,
2812
+ terminal: input.deferred.boundary.terminal,
2813
+ transfer,
2814
+ boundary: input.deferred.boundary
2815
+ });
2816
+ assertTerminalSubmissionRetryTurnOpen({
2817
+ statePath: input.statePath,
2818
+ exactTurnId: turnIdForConversation(input.conversation)
2819
+ });
2820
+ if (recovered) {
2821
+ const finalized = finalizeDeferredTerminalSubmissionRetryAccepted({
2822
+ statePath: input.statePath,
2823
+ scopes: input.scopes,
2824
+ resources: input.resources,
2825
+ attempt
2826
+ });
2827
+ attempt = required(finalized.attempt, "deferred replacement retry attempt disappeared after accepted finalization");
2828
+ const acceptedConversation = finalized.conversation;
2829
+ startTerminalBridgeMonitorForConversation({
2830
+ conversation: acceptedConversation,
2831
+ statePath: input.statePath,
2832
+ logPath: input.logPath,
2833
+ options: input.options
2834
+ });
2835
+ printTerminalSubmissionRetryOutcome({
2836
+ conversation: acceptedConversation,
2837
+ terminalControl: input.terminalControl,
2838
+ attempt,
2839
+ outcome: "agent_accepted",
2840
+ terminalInputSent: true,
2841
+ reason: "The missing deferred Codex submission was completed once and its source/target Sessions were committed."
2842
+ });
2843
+ return;
2844
+ }
2845
+ }
2846
+ else {
2847
+ const timeoutMs = positiveMilliseconds(input.options.terminalAcceptanceTimeoutMs ??
2848
+ DEFAULT_TERMINAL_ACCEPTANCE_TIMEOUT_MS, "--terminal-acceptance-timeout-ms");
2849
+ const acceptance = await input.execution.pollAcceptance({
2850
+ executor: "codex",
2851
+ conversation: enteredConversation,
2852
+ terminalControl: input.terminalControl,
2853
+ timeoutMs,
2854
+ pollIntervalMs: Math.max(10, Math.min(timeoutMs, Number(input.options.terminalAcceptancePollIntervalMs ??
2855
+ DEFAULT_TERMINAL_ACCEPTANCE_POLL_INTERVAL_MS))),
2856
+ scrollbackLines: Number(input.options.scrollbackLines ?? 240)
2857
+ });
2858
+ assertTerminalSubmissionRetryTurnOpen({
2859
+ statePath: input.statePath,
2860
+ exactTurnId: turnIdForConversation(input.conversation)
2861
+ });
2862
+ if (acceptance.outcome === "agent_accepted") {
2863
+ const repaired = terminalSubmissionRetryAccepted({
2864
+ conversation: enteredConversation,
2865
+ submission: required(terminalBridgeSubmission(enteredConversation), "terminal submission receipt disappeared after replacement acceptance"),
2866
+ ledger: enteredLedger,
2867
+ evidence: acceptance.evidence,
2868
+ requestText: input.requestText,
2869
+ at: cliNow().toISOString(),
2870
+ statePath: input.statePath,
2871
+ logPath: input.logPath,
2872
+ scopes: input.scopes,
2873
+ resources: input.resources,
2874
+ attempt,
2875
+ terminalInputSent: true
2876
+ });
2877
+ startTerminalBridgeMonitorForConversation({
2878
+ conversation: repaired.conversation,
2879
+ statePath: input.statePath,
2880
+ logPath: input.logPath,
2881
+ options: input.options
2882
+ });
2883
+ printTerminalSubmissionRetryOutcome({
2884
+ conversation: repaired.conversation,
2885
+ terminalControl: input.terminalControl,
2886
+ attempt: repaired.attempt,
2887
+ outcome: "agent_accepted",
2888
+ terminalInputSent: true,
2889
+ reason: "The missing Codex submission was completed once and native acceptance was proven."
2890
+ });
2891
+ return;
2892
+ }
2893
+ if (acceptance.outcome === "not_accepted") {
2894
+ const notAcceptedConversation = terminalSubmissionRetryTerminalOutcome({
2895
+ conversation: enteredConversation,
2896
+ submission: required(terminalBridgeSubmission(enteredConversation), "terminal submission receipt disappeared after replacement rejection"),
2897
+ ledger: enteredLedger,
2898
+ requestText: input.requestText,
2899
+ reason: acceptance.reason,
2900
+ at: cliNow().toISOString(),
2901
+ statePath: input.statePath,
2902
+ logPath: input.logPath,
2903
+ scopes: input.scopes,
2904
+ resources: input.resources,
2905
+ attempt,
2906
+ outcome: "not_accepted"
2907
+ });
2908
+ printTerminalSubmissionRetryOutcome({
2909
+ conversation: notAcceptedConversation,
2910
+ terminalControl: input.terminalControl,
2911
+ attempt,
2912
+ outcome: "not_accepted",
2913
+ terminalInputSent: true,
2914
+ reason: "The replacement text remains in the Codex composer after one Enter; it was not accepted and another retry is forbidden."
2915
+ });
2916
+ return;
2917
+ }
2918
+ if (acceptance.outcome === "uncertain") {
2919
+ const uncertainConversation = terminalSubmissionRetryTerminalOutcome({
2920
+ conversation: enteredConversation,
2921
+ submission: required(terminalBridgeSubmission(enteredConversation), "terminal submission receipt disappeared after uncertain replacement acceptance"),
2922
+ ledger: enteredLedger,
2923
+ requestText: input.requestText,
2924
+ reason: acceptance.reason,
2925
+ at: cliNow().toISOString(),
2926
+ statePath: input.statePath,
2927
+ logPath: input.logPath,
2928
+ scopes: input.scopes,
2929
+ resources: input.resources,
2930
+ attempt,
2931
+ outcome: "uncertain"
2932
+ });
2933
+ printTerminalSubmissionRetryOutcome({
2934
+ conversation: uncertainConversation,
2935
+ terminalControl: input.terminalControl,
2936
+ attempt,
2937
+ outcome: "refused",
2938
+ terminalInputSent: true,
2939
+ reason: `Replacement acceptance is uncertain (${acceptance.reason}); another retry is forbidden.`
2940
+ });
2941
+ return;
2942
+ }
2943
+ }
2944
+ startTerminalBridgeMonitorForConversation({
2945
+ conversation: enteredConversation,
2946
+ statePath: input.statePath,
2947
+ logPath: input.logPath,
2948
+ options: input.options
2949
+ });
2950
+ printTerminalSubmissionRetryOutcome({
2951
+ conversation: enteredConversation,
2952
+ terminalControl: input.terminalControl,
2953
+ attempt,
2954
+ outcome: "enter_dispatched",
2955
+ terminalInputSent: true,
2956
+ reason: "The missing Codex submission received one text injection and one Enter; acceptance remains pending and another retry is forbidden."
2957
+ });
2958
+ }
843
2959
  async function runSend(options) {
2960
+ if (options.respond !== true && stringValue(options.turn)) {
2961
+ return runTerminalSubmissionRetry(options);
2962
+ }
844
2963
  const messageBody = required(options.message ?? options.request, "--message is required");
845
2964
  // Ordinary send/respond owns Turn creation only. Reject native lifecycle
846
2965
  // slash commands before resolving or mutating any Session/Turn state so a
@@ -3123,7 +5242,13 @@ function presentTerminalDispatchTransportFailure({ error, request, prepared, app
3123
5242
  });
3124
5243
  }
3125
5244
  }
3126
- const uncertainConversation = application.applyUncertain(uncertainAt, error);
5245
+ const uncertainConversation = application.applyUncertain(uncertainAt, error, progress.textInjectedAt && !progress.enterDispatchedAt &&
5246
+ error instanceof TerminalEnterDispatchNotAttemptedError
5247
+ ? {
5248
+ enterNotAttemptedAt: uncertainAt,
5249
+ enterNotAttemptedReason: "pre_key_failure"
5250
+ }
5251
+ : {});
3127
5252
  const stalledConversationIds = stallOtherTerminalBridgeConversationsForUncertainDispatch({
3128
5253
  storeDir: lockedStoreDir,
3129
5254
  terminalControl,