@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.
- package/CHANGELOG.md +18 -0
- package/README.md +5 -5
- package/dist/src/callback-cli-adapter.d.ts +9 -6
- package/dist/src/callback-cli-adapter.js +119 -30
- package/dist/src/callback-cli-adapter.js.map +1 -1
- package/dist/src/callback-outbox-policy.d.ts +39 -0
- package/dist/src/callback-outbox-policy.js +163 -6
- package/dist/src/callback-outbox-policy.js.map +1 -1
- package/dist/src/callback-outbox-service.d.ts +28 -1
- package/dist/src/callback-outbox-service.js +413 -82
- package/dist/src/callback-outbox-service.js.map +1 -1
- package/dist/src/callback-outbox-settlement.d.ts +14 -1
- package/dist/src/callback-outbox-settlement.js +151 -30
- package/dist/src/callback-outbox-settlement.js.map +1 -1
- package/dist/src/callback-route-authority.d.ts +59 -0
- package/dist/src/callback-route-authority.js +123 -0
- package/dist/src/callback-route-authority.js.map +1 -0
- package/dist/src/callback-transport.d.ts +134 -0
- package/dist/src/callback-transport.js +256 -0
- package/dist/src/callback-transport.js.map +1 -0
- package/dist/src/cli-core.js +17 -4
- package/dist/src/cli-core.js.map +1 -1
- package/dist/src/codex-lifecycle-compatibility.js +2 -1
- package/dist/src/codex-lifecycle-compatibility.js.map +1 -1
- package/dist/src/deferred-foreground-application-service.d.ts +18 -0
- package/dist/src/deferred-foreground-application-service.js +72 -0
- package/dist/src/deferred-foreground-application-service.js.map +1 -1
- package/dist/src/deferred-foreground-authority-cli-adapter.js +2 -1
- package/dist/src/deferred-foreground-authority-cli-adapter.js.map +1 -1
- package/dist/src/deferred-foreground-recovery-cli-adapter.d.ts +1 -1
- package/dist/src/deferred-foreground-recovery-cli-adapter.js +108 -37
- package/dist/src/deferred-foreground-recovery-cli-adapter.js.map +1 -1
- package/dist/src/deferred-foreground-recovery-service.d.ts +1 -1
- package/dist/src/deferred-foreground-recovery-service.js +10 -3
- package/dist/src/deferred-foreground-recovery-service.js.map +1 -1
- package/dist/src/deferred-foreground-transfer-policy.d.ts +1 -1
- package/dist/src/deferred-foreground-transfer-policy.js +3 -2
- package/dist/src/deferred-foreground-transfer-policy.js.map +1 -1
- package/dist/src/deferred-foreground-transfer.d.ts +17 -1
- package/dist/src/deferred-foreground-transfer.js +244 -35
- package/dist/src/deferred-foreground-transfer.js.map +1 -1
- package/dist/src/deferred-foreground-user-close.d.ts +18 -0
- package/dist/src/deferred-foreground-user-close.js +180 -0
- package/dist/src/deferred-foreground-user-close.js.map +1 -0
- package/dist/src/openclaw-callback-transport.d.ts +4 -1
- package/dist/src/openclaw-callback-transport.js +403 -10
- package/dist/src/openclaw-callback-transport.js.map +1 -1
- package/dist/src/openclaw-managed-callback-cli-adapter.d.ts +23 -0
- package/dist/src/openclaw-managed-callback-cli-adapter.js +77 -0
- package/dist/src/openclaw-managed-callback-cli-adapter.js.map +1 -0
- package/dist/src/openclaw-plugin-command-adapter.js +28 -31
- package/dist/src/openclaw-plugin-command-adapter.js.map +1 -1
- package/dist/src/openclaw-plugin-helpers.js +8 -0
- package/dist/src/openclaw-plugin-helpers.js.map +1 -1
- package/dist/src/openclaw-plugin-schemas.d.ts +20 -1
- package/dist/src/openclaw-plugin-schemas.js +26 -2
- package/dist/src/openclaw-plugin-schemas.js.map +1 -1
- package/dist/src/protocol.d.ts +3 -0
- package/dist/src/protocol.js +4 -0
- package/dist/src/protocol.js.map +1 -1
- package/dist/src/store.d.ts +9 -0
- package/dist/src/store.js +79 -1
- package/dist/src/store.js.map +1 -1
- package/dist/src/terminal-acceptance-cli-adapter.d.ts +2 -0
- package/dist/src/terminal-acceptance-cli-adapter.js +62 -12
- package/dist/src/terminal-acceptance-cli-adapter.js.map +1 -1
- package/dist/src/terminal-action-projection.d.ts +1 -2
- package/dist/src/terminal-action-projection.js +4 -5
- package/dist/src/terminal-action-projection.js.map +1 -1
- package/dist/src/terminal-agent-bridge.d.ts +82 -0
- package/dist/src/terminal-agent-bridge.js +539 -111
- package/dist/src/terminal-agent-bridge.js.map +1 -1
- package/dist/src/terminal-command-cli-adapter.d.ts +9 -0
- package/dist/src/terminal-command-cli-adapter.js +2130 -5
- package/dist/src/terminal-command-cli-adapter.js.map +1 -1
- package/dist/src/terminal-dispatch-application.d.ts +4 -1
- package/dist/src/terminal-dispatch-application.js +11 -2
- package/dist/src/terminal-dispatch-application.js.map +1 -1
- package/dist/src/terminal-dispatch-execution.js +3 -1
- package/dist/src/terminal-dispatch-execution.js.map +1 -1
- package/dist/src/terminal-dispatch-ledger-codec.d.ts +3 -0
- package/dist/src/terminal-dispatch-ledger-codec.js +46 -19
- package/dist/src/terminal-dispatch-ledger-codec.js.map +1 -1
- package/dist/src/terminal-dispatch-presenter.js +5 -3
- package/dist/src/terminal-dispatch-presenter.js.map +1 -1
- package/dist/src/terminal-dispatch-receipt.d.ts +3 -0
- package/dist/src/terminal-dispatch-receipt.js +15 -1
- package/dist/src/terminal-dispatch-receipt.js.map +1 -1
- package/dist/src/terminal-dispatch-recovery-cli-adapter.js +68 -7
- package/dist/src/terminal-dispatch-recovery-cli-adapter.js.map +1 -1
- package/dist/src/terminal-dispatch-recovery-service.d.ts +2 -0
- package/dist/src/terminal-dispatch-recovery-service.js +9 -0
- package/dist/src/terminal-dispatch-recovery-service.js.map +1 -1
- package/dist/src/terminal-identity-authority-cli-adapter.d.ts +2 -0
- package/dist/src/terminal-identity-authority-cli-adapter.js +30 -27
- package/dist/src/terminal-identity-authority-cli-adapter.js.map +1 -1
- package/dist/src/terminal-list-cli-adapter.d.ts +1 -0
- package/dist/src/terminal-list-cli-adapter.js +50 -31
- package/dist/src/terminal-list-cli-adapter.js.map +1 -1
- package/dist/src/terminal-list-renderer.d.ts +9 -1
- package/dist/src/terminal-list-renderer.js +219 -98
- package/dist/src/terminal-list-renderer.js.map +1 -1
- package/dist/src/terminal-maintenance-cli-adapter.js +253 -326
- package/dist/src/terminal-maintenance-cli-adapter.js.map +1 -1
- package/dist/src/terminal-monitor-application-service.d.ts +5 -0
- package/dist/src/terminal-monitor-application-service.js +5 -0
- package/dist/src/terminal-monitor-application-service.js.map +1 -1
- package/dist/src/terminal-monitor-cli-adapter.d.ts +25 -1
- package/dist/src/terminal-monitor-cli-adapter.js +170 -27
- package/dist/src/terminal-monitor-cli-adapter.js.map +1 -1
- package/dist/src/terminal-monitor-reconciliation-eligibility.d.ts +1 -1
- package/dist/src/terminal-monitor-reconciliation-eligibility.js +220 -32
- package/dist/src/terminal-monitor-reconciliation-eligibility.js.map +1 -1
- package/dist/src/terminal-monitor-state-cli-adapter.d.ts +1 -1
- package/dist/src/terminal-monitor-state-cli-adapter.js +808 -147
- package/dist/src/terminal-monitor-state-cli-adapter.js.map +1 -1
- package/dist/src/terminal-monitor-state-reconciliation-service.d.ts +2 -1
- package/dist/src/terminal-monitor-state-reconciliation-service.js +12 -0
- package/dist/src/terminal-monitor-state-reconciliation-service.js.map +1 -1
- package/dist/src/terminal-monitor-supervision-cli-adapter.d.ts +2 -0
- package/dist/src/terminal-monitor-supervision-cli-adapter.js +31 -11
- package/dist/src/terminal-monitor-supervision-cli-adapter.js.map +1 -1
- package/dist/src/terminal-status-cli-adapter.d.ts +1 -0
- package/dist/src/terminal-status-cli-adapter.js +30 -28
- package/dist/src/terminal-status-cli-adapter.js.map +1 -1
- package/dist/src/terminal-submission-acceptance.js +78 -25
- package/dist/src/terminal-submission-acceptance.js.map +1 -1
- package/dist/src/terminal-submission-retry-service.d.ts +114 -0
- package/dist/src/terminal-submission-retry-service.js +589 -0
- package/dist/src/terminal-submission-retry-service.js.map +1 -0
- package/dist/src/terminal-watch-callback-cli-adapter.d.ts +10 -2
- package/dist/src/terminal-watch-callback-cli-adapter.js +76 -37
- package/dist/src/terminal-watch-callback-cli-adapter.js.map +1 -1
- package/dist/src/terminal-watch-cli-adapter.js +41 -17
- package/dist/src/terminal-watch-cli-adapter.js.map +1 -1
- package/dist/src/terminal-watch-service.d.ts +11 -6
- package/dist/src/terminal-watch-service.js +210 -41
- package/dist/src/terminal-watch-service.js.map +1 -1
- package/dist/src/terminal-watch-store.d.ts +23 -0
- package/dist/src/terminal-watch-store.js +162 -4
- package/dist/src/terminal-watch-store.js.map +1 -1
- package/docs/quickstart-tmux.md +1 -1
- package/package.json +1 -1
- 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 &&
|
|
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
|
|
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 =
|
|
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,
|