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