@scotthuang/agent-knock-knock 0.12.4 → 0.12.5

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.
@@ -11,11 +11,12 @@ import { captureClaudeTranscriptAnchor, defaultClaudeHome, detectClaudeTranscrip
11
11
  import { captureCodexRolloutAcceptanceAnchor, detectCodexBoundRolloutCompletion, detectCodexRolloutAcceptance, terminalSubmissionReplayReceipt, validateTerminalSubmissionAcceptanceEvidence } from "./terminal-submission-acceptance.js";
12
12
  import { CodexLocalSessionProvider } from "./codex-local-session-provider.js";
13
13
  import { CodexStoreAdapter } from "./codex-store-adapter.js";
14
+ import { createDeferredForegroundTransferId, listDeferredForegroundTransfers, loadDeferredForegroundTransfer, saveDeferredForegroundTransfer } from "./deferred-foreground-transfer.js";
14
15
  import { applyMessageToConversation, budgetAction, createConversation, createMessage, effectiveTurnStatus, executorForConversation, extractStructuredMessage, isTurnPhaseStatus, normalizeLegacyCallbackStatus, parseMessageJson, resolveExecutor, sessionIdForConversation, turnIdForConversation } from "./protocol.js";
15
16
  import { EXECUTOR_KINDS, executorDefinitionForKind, isExecutorKind } from "./executors.js";
16
17
  import { redactString, writeRuntimeLog } from "./runtime-log.js";
17
18
  import { formatTranscript, readNdjsonLog } from "./transcript.js";
18
- import { appendEvent, assertStoreWriterCompatible, defaultStoreDir, ensureDir, ensureStoreWritable, inspectStoreCompatibility, listConversations, logPathForStatePath, loadConversationById, loadState, messageEvent, pathsForConversation, pathsForConversationDir, saveState, StoreLockTimeoutError, statePathForConversationId, withStoreWriterLease, withStoreWriterLeaseAsync } from "./store.js";
19
+ import { appendEvent, assertStoreWriterCompatible, defaultStoreDir, ensureDir, ensureStoreWritable, inspectStoreCompatibility, listConversations, logPathForStatePath, loadConversationById, loadState, messageEvent, pathsForConversation, pathsForConversationDir, saveState, STORE_SESSION_AUTHORITY_PROTOCOL, StoreLockTimeoutError, statePathForConversationId, withStoreWriterLease, withStoreWriterLeaseAsync } from "./store.js";
19
20
  import { createManagedSessionId, createNativeThreadTransitionId, humanObservedHandoffBindingToken, isExactNativeThreadId, legacyManagedSessionBindingToken, legacyUnmanagedTerminalBindingToken, managedSessionBindingToken, nativeThreadCommandFingerprint, terminalBindingFrom, unmanagedTerminalBindingToken } from "./managed-session.js";
20
21
  import { classifyCodexLifecyclePostcondition, evaluateResumeCandidateAvailability, hasStrongCodexLifecycleIdentity, isFreshCodexPostProbeScreen } from "./native-thread-lifecycle-policy.js";
21
22
  import { createNativeThreadResumeSnapshot, nativeThreadCandidateSnapshotFingerprint, nativeThreadResumeSnapshotRowsMatchCandidates, resolveNativeThreadResumeSelection, saveNativeThreadResumeSnapshot, sortNativeThreadCandidates, terminalActionFingerprint, verifiedPreviousResumeCandidate } from "./native-thread-resume-snapshot.js";
@@ -68,7 +69,8 @@ const TERMINAL_LEDGER_RECEIPT_IMMUTABLE_FIELDS = [
68
69
  "executor_kind",
69
70
  "openclaw_session",
70
71
  "state_path",
71
- "event_log_path"
72
+ "event_log_path",
73
+ "deferred_foreground_transfer_id"
72
74
  ];
73
75
  const TERMINAL_BRIDGE_SUPERSEDE_STATUSES = new Set([
74
76
  "created",
@@ -3022,7 +3024,7 @@ async function runList(options) {
3022
3024
  storeDir,
3023
3025
  terminals: physicalTerminals,
3024
3026
  managedSessions,
3025
- sessionAuthorityRequired: store.writer_protocol === 3,
3027
+ sessionAuthorityRequired: Number(store.writer_protocol) >= STORE_SESSION_AUTHORITY_PROTOCOL,
3026
3028
  allConversations: allManagedConversations.filter((conversation) => matchesConfiguredWorkspace(options.workspace, conversation.workspace)),
3027
3029
  displayedConversations: storedConversations,
3028
3030
  includeAll,
@@ -3414,6 +3416,15 @@ async function terminalControlledListEntry(session, activeSessions, options, bri
3414
3416
  };
3415
3417
  }
3416
3418
  function terminalFirstListProjection({ storeDir, terminals, managedSessions, sessionAuthorityRequired, allConversations, displayedConversations, includeAll, managedOnly, statusFilter, mutationsAllowed }) {
3419
+ const nonterminalDeferredTransfers = listDeferredForegroundTransfers(storeDir).filter((transfer) => !["resolved", "aborted", "abort_resolved"].includes(transfer.status));
3420
+ const nonterminalDeferredTransferIds = new Set(nonterminalDeferredTransfers.map((transfer) => transfer.transfer_id));
3421
+ const conversationHasNonterminalDeferredTransfer = (conversation) => {
3422
+ const takeover = isRecord(conversation.native_session_takeover)
3423
+ ? conversation.native_session_takeover
3424
+ : undefined;
3425
+ const transferId = stringValue(takeover?.deferred_foreground_transfer_id);
3426
+ return Boolean(transferId && nonterminalDeferredTransferIds.has(transferId));
3427
+ };
3417
3428
  const discoveredTerminalControls = terminals.flatMap((terminal) => {
3418
3429
  const control = isRecord(terminal.terminal_control)
3419
3430
  ? terminal.terminal_control
@@ -3425,6 +3436,9 @@ function terminalFirstListProjection({ storeDir, terminals, managedSessions, ses
3425
3436
  const terminalControl = isRecord(terminal.terminal_control)
3426
3437
  ? terminal.terminal_control
3427
3438
  : undefined;
3439
+ const terminalHasNonterminalDeferredTransfer = Boolean(terminalControl && nonterminalDeferredTransfers.some((transfer) => transfer.terminal_id === String(terminal.id) &&
3440
+ transfer.process_pid === Number(terminal.pid) &&
3441
+ terminalControlEvidenceMatches(transfer.terminal_endpoint, terminalControl)));
3428
3442
  const allRelated = terminalControl
3429
3443
  ? allConversations.filter((conversation) => terminalControlsShareIncarnation(terminalControlForManagedConversation(conversation), terminalControl))
3430
3444
  : [];
@@ -3529,10 +3543,13 @@ function terminalFirstListProjection({ storeDir, terminals, managedSessions, ses
3529
3543
  const bindingAwareRawActions = authoritativeSession
3530
3544
  ? actionsForManagedSessionBinding(rawActions, authoritativeSession)
3531
3545
  : rawActions;
3532
- const sessionAwareRawActions = authoritativeSession &&
3546
+ const sessionAwareRawActionsBase = authoritativeSession &&
3533
3547
  managedSessionHasUnresolvedNativeTransition(storeDir, authoritativeSession)
3534
3548
  ? Object.fromEntries(Object.entries(bindingAwareRawActions).filter(([actionName]) => actionName !== "native_inspect"))
3535
3549
  : bindingAwareRawActions;
3550
+ const sessionAwareRawActions = terminalHasNonterminalDeferredTransfer
3551
+ ? readOnlyListActions(sessionAwareRawActionsBase)
3552
+ : sessionAwareRawActionsBase;
3536
3553
  const soleBindingConflict = conflictingBoundSessionClaims.length === 1
3537
3554
  ? conflictingBoundSessionClaims[0]
3538
3555
  : undefined;
@@ -3658,6 +3675,76 @@ function terminalFirstListProjection({ storeDir, terminals, managedSessions, ses
3658
3675
  sourceSession: soleBindingConflict.session
3659
3676
  })
3660
3677
  : undefined;
3678
+ const deferredCodexSource = authoritativeSession &&
3679
+ terminal.agent === "codex" &&
3680
+ nativeIdentityObservation?.status === "verified_absent"
3681
+ ? authoritativeSession
3682
+ : undefined;
3683
+ const deferredCodexProcessUuid = stringValue(terminal.native_agent_process_uuid);
3684
+ const deferredCodexProcessBirth = stringValue(terminal.native_agent_process_birth);
3685
+ const deferredCodexWorkspace = stringValue(terminal.workspace ?? terminal.cwd);
3686
+ const deferredCodexSourceNativeThreadId = stringValue(deferredCodexSource?.binding?.native_thread_id)?.toLowerCase();
3687
+ const deferredCodexSourceActiveElsewhere = Boolean(deferredCodexSourceNativeThreadId &&
3688
+ terminals.some((candidate) => candidate !== terminal &&
3689
+ candidate.agent === "codex" &&
3690
+ stringValue(candidate.native_agent_session_id)?.toLowerCase() ===
3691
+ deferredCodexSourceNativeThreadId));
3692
+ const deferredCodexDispatchSnapshot = terminalControl
3693
+ ? tryDeferredCodexForegroundDispatchSnapshot(terminalControl)
3694
+ : undefined;
3695
+ const deferredCodexForegroundEligible = Boolean(mutationsAllowed &&
3696
+ !terminalHasNonterminalDeferredTransfer &&
3697
+ deferredCodexSource &&
3698
+ terminalControl &&
3699
+ hasCanonicalTerminalEndpoint(terminalControl) &&
3700
+ discoveredOwnership.state === "none" &&
3701
+ unresolvedSessionClaims.length === 0 &&
3702
+ matchingSessions.length === 1 &&
3703
+ conflictingBoundSessionClaims.length === 0 &&
3704
+ deferredCodexProcessUuid &&
3705
+ deferredCodexProcessBirth &&
3706
+ deferredCodexWorkspace &&
3707
+ exactBoundCodexStatusCardSourceForDeferredSend({
3708
+ session: deferredCodexSource,
3709
+ terminalId: String(terminal.id),
3710
+ terminalControl,
3711
+ pid: Number(terminal.pid),
3712
+ workspace: deferredCodexWorkspace,
3713
+ processUuid: deferredCodexProcessUuid,
3714
+ processBirth: deferredCodexProcessBirth
3715
+ }) &&
3716
+ managedTurnsForSession(storeDir, deferredCodexSource.session_id).length === 0 &&
3717
+ !deferredCodexSource.last_transition_id &&
3718
+ !managedSessionHasAnyNativeTransition(storeDir, deferredCodexSource) &&
3719
+ terminalBlockingTurns.length === 0 &&
3720
+ terminal.orphaned_terminal_dispatch === undefined &&
3721
+ deferredCodexDispatchSnapshot &&
3722
+ ["idle", "unknown"].includes(String(terminal.activity_state)) &&
3723
+ automatedInputComposerReady === true &&
3724
+ !(isRecord(terminal.approval_state) &&
3725
+ terminal.approval_state.blocked === true) &&
3726
+ !deferredCodexSourceActiveElsewhere &&
3727
+ terminalControl.capabilities.includes("send_keys") &&
3728
+ terminalControl.capabilities.includes("screen_status") &&
3729
+ isRecord(rawActions.send));
3730
+ const deferredCodexForegroundToken = deferredCodexForegroundEligible &&
3731
+ deferredCodexSource &&
3732
+ terminalControl &&
3733
+ deferredCodexProcessUuid &&
3734
+ deferredCodexProcessBirth &&
3735
+ deferredCodexWorkspace &&
3736
+ deferredCodexDispatchSnapshot
3737
+ ? deferredCodexForegroundBindingToken({
3738
+ terminalId: String(terminal.id),
3739
+ terminalControl,
3740
+ pid: Number(terminal.pid),
3741
+ workspace: deferredCodexWorkspace,
3742
+ processUuid: deferredCodexProcessUuid,
3743
+ processBirth: deferredCodexProcessBirth,
3744
+ sourceSession: deferredCodexSource,
3745
+ dispatchSnapshot: deferredCodexDispatchSnapshot
3746
+ })
3747
+ : undefined;
3661
3748
  const reconcileBindingAction = mutationsAllowed &&
3662
3749
  discoveredOwnership.state === "none" &&
3663
3750
  unresolvedSessionClaims.length === 0 &&
@@ -3754,7 +3841,8 @@ function terminalFirstListProjection({ storeDir, terminals, managedSessions, ses
3754
3841
  // the live native thread or Turn generation changed. `blocking_turns` is
3755
3842
  // reserved for other same-incarnation blockers such as legacy collateral
3756
3843
  // stalls; clear those first, refresh, and only then decide the handoff.
3757
- const terminalRecoveryBlockingTurns = terminalBlockingTurns.filter((turn) => !blockingHandoffTurnIds.has(turn.conversation_id));
3844
+ const terminalRecoveryBlockingTurns = terminalBlockingTurns.filter((turn) => !blockingHandoffTurnIds.has(turn.conversation_id) &&
3845
+ !conversationHasNonterminalDeferredTransfer(turn));
3758
3846
  const terminalCanAcceptSend = ownership.state === "none" && isRecord(sessionAwareRawActions.send);
3759
3847
  if (ownership.state === "current" &&
3760
3848
  !allRelated.some((conversation) => conversation.conversation_id === ownership.conversation.conversation_id)) {
@@ -3791,7 +3879,9 @@ function terminalFirstListProjection({ storeDir, terminals, managedSessions, ses
3791
3879
  const currentTurnValue = ownership.state === "current"
3792
3880
  ? currentManagedTurnForTerminal(ownership.conversation, terminal, sessionAwareRawActions)
3793
3881
  : undefined;
3794
- const currentTurnProjection = currentTurnValue && !mutationsAllowed
3882
+ const currentTurnProjection = currentTurnValue && (!mutationsAllowed ||
3883
+ (ownership.state === "current" &&
3884
+ conversationHasNonterminalDeferredTransfer(ownership.conversation)))
3795
3885
  ? readOnlyManagedTurn(currentTurnValue)
3796
3886
  : currentTurnValue;
3797
3887
  const currentTurn = currentTurnProjection
@@ -3804,7 +3894,9 @@ function terminalFirstListProjection({ storeDir, terminals, managedSessions, ses
3804
3894
  const recentTurnValue = recentConversation
3805
3895
  ? historicalManagedTurnForTerminal(recentConversation, terminalCanAcceptSend, terminal)
3806
3896
  : undefined;
3807
- const recentTurnProjection = recentTurnValue && !mutationsAllowed
3897
+ const recentTurnProjection = recentTurnValue && (!mutationsAllowed ||
3898
+ Boolean(recentConversation &&
3899
+ conversationHasNonterminalDeferredTransfer(recentConversation)))
3808
3900
  ? readOnlyManagedTurn(recentTurnValue)
3809
3901
  : recentTurnValue;
3810
3902
  const recentTurn = recentTurnProjection
@@ -3815,7 +3907,10 @@ function terminalFirstListProjection({ storeDir, terminals, managedSessions, ses
3815
3907
  : [];
3816
3908
  const history = historyConversations.map((conversation) => {
3817
3909
  const turn = historicalManagedTurnForTerminal(conversation, terminalCanAcceptSend, terminal);
3818
- return withoutGenericHandoffSourceClose(mutationsAllowed ? turn : readOnlyManagedTurn(turn), blockingHandoffTurnIds);
3910
+ return withoutGenericHandoffSourceClose(mutationsAllowed &&
3911
+ !conversationHasNonterminalDeferredTransfer(conversation)
3912
+ ? turn
3913
+ : readOnlyManagedTurn(turn), blockingHandoffTurnIds);
3819
3914
  });
3820
3915
  const visibleTurnIds = new Set([currentTurn, recentTurn, ...history]
3821
3916
  .map((turn) => stringValue(turn?.conversation_id))
@@ -3879,14 +3974,28 @@ function terminalFirstListProjection({ storeDir, terminals, managedSessions, ses
3879
3974
  ? { reconcile_binding: reconcileBindingAction }
3880
3975
  : {})
3881
3976
  }
3882
- : managedSessionId &&
3883
- sessionBindingMatchesLiveTerminal &&
3884
- isRecord(sessionAwareRawActions.send)
3977
+ : deferredCodexForegroundToken &&
3978
+ isRecord(rawActions.send)
3885
3979
  ? {
3886
3980
  ...sessionAwareRawActions,
3887
- send: sendActionForManagedSession(sessionAwareRawActions.send, managedSessionId)
3981
+ send: {
3982
+ ...rawActions.send,
3983
+ arguments: {
3984
+ ...(isRecord(rawActions.send.arguments)
3985
+ ? rawActions.send.arguments
3986
+ : {}),
3987
+ expected_terminal_token: deferredCodexForegroundToken
3988
+ }
3989
+ }
3888
3990
  }
3889
- : sessionAwareRawActions;
3991
+ : managedSessionId &&
3992
+ sessionBindingMatchesLiveTerminal &&
3993
+ isRecord(sessionAwareRawActions.send)
3994
+ ? {
3995
+ ...sessionAwareRawActions,
3996
+ send: sendActionForManagedSession(sessionAwareRawActions.send, managedSessionId)
3997
+ }
3998
+ : sessionAwareRawActions;
3890
3999
  const authoritativeBinding = authoritativeSession?.binding;
3891
4000
  const authoritativeIdentity = authoritativeBinding?.native_thread_id
3892
4001
  ? {
@@ -3992,7 +4101,8 @@ function terminalFirstListProjection({ storeDir, terminals, managedSessions, ses
3992
4101
  });
3993
4102
  return {
3994
4103
  ...managedTurn,
3995
- available_actions: mutationsAllowed
4104
+ available_actions: mutationsAllowed &&
4105
+ !conversationHasNonterminalDeferredTransfer(conversation)
3996
4106
  ? safeUnavailableManagedTurnActions(isRecord(managedTurn.available_actions)
3997
4107
  ? managedTurn.available_actions
3998
4108
  : {})
@@ -4205,6 +4315,36 @@ function managedSessionHasUnresolvedNativeTransition(storeDir, session) {
4205
4315
  }
4206
4316
  return false;
4207
4317
  }
4318
+ function managedSessionHasAnyNativeTransition(storeDir, session) {
4319
+ const root = nativeThreadTransitionsDir(storeDir);
4320
+ if (!fs.existsSync(root)) {
4321
+ return false;
4322
+ }
4323
+ let entries;
4324
+ try {
4325
+ entries = fs.readdirSync(root, { withFileTypes: true });
4326
+ }
4327
+ catch {
4328
+ return true;
4329
+ }
4330
+ for (const entry of entries) {
4331
+ if (!entry.isDirectory()) {
4332
+ continue;
4333
+ }
4334
+ let transition;
4335
+ try {
4336
+ transition = loadNativeThreadTransition(storeDir, entry.name);
4337
+ }
4338
+ catch {
4339
+ return true;
4340
+ }
4341
+ if (transition.source_session_id === session.session_id ||
4342
+ transition.target_session_id === session.session_id) {
4343
+ return true;
4344
+ }
4345
+ }
4346
+ return false;
4347
+ }
4208
4348
  function terminalControlForManagedConversation(conversation) {
4209
4349
  return terminalControlFromTakeover(isRecord(conversation.native_session_takeover)
4210
4350
  ? conversation.native_session_takeover
@@ -4695,13 +4835,13 @@ function managedListApprovalState(conversation) {
4695
4835
  }
4696
4836
  function listActionContracts() {
4697
4837
  return {
4698
- version: 10,
4838
+ version: 11,
4699
4839
  instructions: [
4700
4840
  "Treat terminals[] as the primary resource and use only actions present in available_actions, except the snapshot-bound terminals[].handoff_decision.choices.take_over_current.action and an exact terminals[].blocking_turns[].recovery_action. Either nested action requires explicit user confirmation; after it succeeds, refresh list before any follow-current send.",
4701
4841
  "An existing managed session's ordinary send targets session_id and creates a new turn. A turn id is never an ordinary send target.",
4702
4842
  "Read-only native-thread listing targets an exact terminal_id. Native-thread new/resume mutations also use the listed expected_binding_token and never create a Turn.",
4703
4843
  "Native inspection is a separate terminal action: use only its closed inspection enum and current exact terminal_id/token; AKK status does not execute a native slash command.",
4704
- "A verified, idle human native-thread switch may expose a terminal-scoped send with expected_terminal_token; that action atomically adopts the live context before creating its Turn. A conclusively ended Codex rollout may expose the same snapshot-bound send only after AKK proves zero current rollout and an exact empty composer; it detaches the ended Session and creates an isolated virgin Session. Other binding conflicts remain fail-closed and may expose only exact low-level reconcile_binding recovery.",
4844
+ "A verified, idle human native-thread switch may expose a terminal-scoped send with expected_terminal_token; that action atomically adopts the live context before creating its Turn. A conclusively ended Codex rollout may expose the same snapshot-bound send only after AKK proves zero current rollout and an exact empty composer; it detaches the ended Session and creates an isolated virgin Session. A status-card-only Codex Session with no rollout or Turn may also expose this exact action: AKK submits the ordinary task first and binds only the rollout that durably accepts that request, so narrow panes do not require /status. Until that promotion commits, strict session_id send, respond, approve, cancel, native lifecycle, and native_inspect remain unavailable, and the provisional binding has no callback authority. If dispatch, acceptance, or post-submit binding is uncertain, do not retry automatically. Other binding conflicts remain fail-closed and may expose only exact low-level reconcile_binding recovery.",
4705
4845
  "List resumable threads before resume; use only a complete native_thread_id and the action returned for that candidate.",
4706
4846
  "Use a terminal selector only when explicitly named by the user or prefilled by that terminal row's send action. A handoff action also carries expected_terminal_token; never infer, guess, or reuse either value.",
4707
4847
  "Use respond only for an in-flight turn that is explicitly waiting for OpenClaw.",
@@ -4775,7 +4915,8 @@ function listActionContracts() {
4775
4915
  "agentHardTimeoutMinutes"
4776
4916
  ],
4777
4917
  unsupported: ["timeoutSeconds"],
4778
- ordinary_use: "Create a new managed turn in the exact Session. A live terminal selector can attach an unmanaged pane, adopt one verified human-selected native context, or use the exact listed conflict send to detach a verified-empty Codex source before an isolated virgin attach; an explicit session_id never follows the pane."
4918
+ status_card_only_deferred_scope: "A zero-Turn Codex status-card binding has no rollout; only its listed selector/token send creates an isolated provisional Session and binds it after exact request acceptance. Until promotion commits, strict managed controls, native lifecycle, native_inspect, and callback authority remain unavailable; an uncertain dispatch, acceptance, or post-submit binding must not be retried automatically.",
4919
+ ordinary_use: "Create a new managed turn in the exact Session. A live terminal selector can attach an unmanaged pane, adopt one verified human-selected native context, detach a verified-empty Codex source, or replace a zero-Turn status-card-only binding after the submitted request proves its exact rollout; an explicit session_id never follows the pane."
4779
4920
  },
4780
4921
  new_thread: {
4781
4922
  tool: "agent_knock_knock_new_thread",
@@ -5135,7 +5276,7 @@ async function sessionSelectorCandidates(commandName, options) {
5135
5276
  managedSessions: selectorStore.readable
5136
5277
  ? listManagedSessions(storeDir).filter((session) => matchesConfiguredWorkspace(options.workspace, session.workspace))
5137
5278
  : [],
5138
- sessionAuthorityRequired: selectorStore.writer_protocol === 3,
5279
+ sessionAuthorityRequired: Number(selectorStore.writer_protocol) >= STORE_SESSION_AUTHORITY_PROTOCOL,
5139
5280
  allConversations: discoverableWorkspaceConversations,
5140
5281
  displayedConversations: discoverableWorkspaceConversations,
5141
5282
  includeAll: false,
@@ -6094,7 +6235,7 @@ function materializeCurrentManagedSession({ options, terminal, identity }) {
6094
6235
  }
6095
6236
  return existingById;
6096
6237
  }
6097
- throw new Error(`Store protocol 3 has Turn records for Session ${sessionId} but its ` +
6238
+ throw new Error(`Store protocol 3+ has Turn records for Session ${sessionId} but its ` +
6098
6239
  "authoritative Session state is missing");
6099
6240
  }
6100
6241
  async function reattachManagedSessionForNativeIdentity({ options, terminal, identity, storeDir }) {
@@ -6180,6 +6321,137 @@ function exactBoundCodexSourceForVerifiedEmptyHandoff({ session, terminalId, ter
6180
6321
  liveProcessBirth: processBirth
6181
6322
  }) === "same");
6182
6323
  }
6324
+ function exactBoundCodexStatusCardSourceForDeferredSend({ session, terminalId, terminalControl, pid, workspace, processUuid, processBirth }) {
6325
+ const binding = session.binding;
6326
+ return Boolean(session.agent === "codex" &&
6327
+ session.status === "bound" &&
6328
+ binding &&
6329
+ isExactNativeThreadId(binding.native_thread_id) &&
6330
+ !binding.native_process.rollout &&
6331
+ isCodexStatusCardEvidence(binding.native_process.evidence) &&
6332
+ binding.native_process.process_uuid &&
6333
+ binding.native_process.process_birth &&
6334
+ binding.native_process.pid === pid &&
6335
+ terminalControlAliasMatches(binding.terminal_id, binding.terminal_control, terminalId, terminalControl) &&
6336
+ workspace &&
6337
+ path.resolve(session.workspace) === path.resolve(workspace) &&
6338
+ processIncarnationRelationship({
6339
+ binding,
6340
+ livePid: pid,
6341
+ liveProcessUuid: processUuid,
6342
+ liveProcessBirth: processBirth
6343
+ }) === "same");
6344
+ }
6345
+ function deferredCodexForegroundBindingToken({ terminalId, terminalControl, pid, workspace, processUuid, processBirth, sourceSession, dispatchSnapshot }) {
6346
+ const terminalToken = unmanagedTerminalBindingToken({
6347
+ terminalId,
6348
+ terminalControl,
6349
+ agent: "codex",
6350
+ pid,
6351
+ workspace,
6352
+ processUuid,
6353
+ processBirth
6354
+ });
6355
+ return createHash("sha256")
6356
+ .update(JSON.stringify({
6357
+ version: 2,
6358
+ kind: "deferred_codex_foreground_binding",
6359
+ terminal_token: terminalToken,
6360
+ source_session_id: sourceSession.session_id,
6361
+ source_revision: managedSessionRevision(sourceSession),
6362
+ source_binding_token: managedSessionBindingToken(sourceSession),
6363
+ terminal_dispatch_snapshot: dispatchSnapshot,
6364
+ observation: "verified_absent"
6365
+ }))
6366
+ .digest("hex");
6367
+ }
6368
+ function deferredCodexForegroundDispatchSnapshot(terminalControl) {
6369
+ const ledger = loadTerminalBridgeDispatchLedger(terminalControl);
6370
+ return deferredCodexForegroundDispatchSnapshotForLedger(terminalControl, ledger);
6371
+ }
6372
+ function deferredCodexForegroundDispatchSnapshotForLedger(terminalControl, ledger) {
6373
+ if (!ledger) {
6374
+ return {
6375
+ status: "none",
6376
+ fingerprint: terminalActionFingerprint({
6377
+ kind: "deferred_codex_foreground_terminal_dispatch",
6378
+ status: "none"
6379
+ })
6380
+ };
6381
+ }
6382
+ const currentAnchor = terminalEndpointFromControlRef(terminalControl).processAnchorPid;
6383
+ const ledgerAnchor = terminalDispatchRecordProcessAnchor(ledger);
6384
+ if (ledger.status !== "resolved" ||
6385
+ !Number.isSafeInteger(currentAnchor) ||
6386
+ Number(currentAnchor) < 1 ||
6387
+ ledgerAnchor !== currentAnchor ||
6388
+ !terminalDispatchRecordMatchesControl(ledger, terminalControl, {
6389
+ requireCurrentRoute: true,
6390
+ requireProcessAnchor: true
6391
+ })) {
6392
+ throw new Error(`terminal ${terminalControl.target} does not have exact resolved ` +
6393
+ "dispatch authority");
6394
+ }
6395
+ const resolvedAt = stringValue(ledger.resolved_at);
6396
+ if (!resolvedAt || !Number.isFinite(Date.parse(resolvedAt))) {
6397
+ throw new Error(`terminal ${terminalControl.target} resolved dispatch receipt has no ` +
6398
+ "valid resolved_at");
6399
+ }
6400
+ const statePath = stringValue(ledger.state_path);
6401
+ const ledgerStoreDir = stringValue(ledger.store_dir);
6402
+ if (ledgerStoreDir && !path.isAbsolute(ledgerStoreDir)) {
6403
+ throw new Error(`terminal ${terminalControl.target} resolved dispatch receipt has a ` +
6404
+ "nonabsolute Store owner");
6405
+ }
6406
+ if (statePath) {
6407
+ if (!ledgerStoreDir || !path.isAbsolute(statePath) ||
6408
+ !path.isAbsolute(ledgerStoreDir)) {
6409
+ throw new Error(`terminal ${terminalControl.target} resolved dispatch receipt has ` +
6410
+ "incomplete Store authority");
6411
+ }
6412
+ const canonicalStatePath = path.resolve(statePath);
6413
+ const canonicalStoreDir = path.resolve(ledgerStoreDir);
6414
+ const canonical = pathsForConversationDir(path.dirname(canonicalStatePath));
6415
+ if (path.resolve(canonical.statePath) !== canonicalStatePath ||
6416
+ path.resolve(canonical.storeDir) !== canonicalStoreDir) {
6417
+ throw new Error(`terminal ${terminalControl.target} resolved dispatch receipt has a ` +
6418
+ "noncanonical Store/state owner");
6419
+ }
6420
+ const conversationId = stringValue(ledger.conversation_id);
6421
+ if (conversationId &&
6422
+ path.resolve(pathsForConversation(conversationId, canonicalStoreDir).statePath) !== canonicalStatePath) {
6423
+ throw new Error(`terminal ${terminalControl.target} resolved dispatch receipt has a ` +
6424
+ "mismatched conversation owner");
6425
+ }
6426
+ }
6427
+ return {
6428
+ status: "resolved",
6429
+ fingerprint: terminalActionFingerprint({
6430
+ kind: "deferred_codex_foreground_terminal_dispatch",
6431
+ status: "resolved",
6432
+ ledger
6433
+ })
6434
+ };
6435
+ }
6436
+ function deferredCodexPreviousDispatchSnapshotMatches({ transfer, terminalControl, ledger }) {
6437
+ let snapshot;
6438
+ try {
6439
+ snapshot = deferredCodexForegroundDispatchSnapshotForLedger(terminalControl, ledger);
6440
+ }
6441
+ catch {
6442
+ return false;
6443
+ }
6444
+ return snapshot.status === transfer.previous_dispatch_status &&
6445
+ snapshot.fingerprint === transfer.previous_dispatch_fingerprint;
6446
+ }
6447
+ function tryDeferredCodexForegroundDispatchSnapshot(terminalControl) {
6448
+ try {
6449
+ return deferredCodexForegroundDispatchSnapshot(terminalControl);
6450
+ }
6451
+ catch {
6452
+ return undefined;
6453
+ }
6454
+ }
6183
6455
  function verifiedEmptyCodexHandoffToken({ terminalId, terminalControl, pid, workspace, processUuid, processBirth, sourceSession }) {
6184
6456
  const terminalToken = unmanagedTerminalBindingToken({
6185
6457
  terminalId,
@@ -6372,85 +6644,944 @@ async function assertVerifiedEmptyCodexTransportBoundary({ options, boundary, re
6372
6644
  requireEmptyComposer
6373
6645
  });
6374
6646
  }
6375
- function observedHandoffAuthorityToken({ terminal, identity, sourceSession, target }) {
6376
- const exact = exactLifecycleProcessIdentity(terminal, identity);
6377
- const terminalToken = unmanagedTerminalBindingToken({
6378
- terminalId: terminal.conversationId,
6379
- terminalControl: terminal.terminalControl,
6380
- agent: terminal.agent,
6381
- pid: terminal.pid,
6382
- workspace: terminal.terminalControl.currentPath ?? cliCwd(),
6383
- nativeThreadId: exact.sessionId,
6384
- processUuid: exact.processUuid,
6385
- processBirth: exact.processBirth,
6386
- rollout: exact.rollout
6647
+ async function assertDeferredCodexForegroundBindingBoundary({ options, boundary, expectedSourceStatus, requireNoDispatch, requireEmptyComposer }) {
6648
+ const storeDir = storeDirFromOptions(options);
6649
+ const source = loadManagedSession(storeDir, boundary.sourceSessionId);
6650
+ const expectedRevision = expectedSourceStatus === "bound"
6651
+ ? boundary.sourceBoundRevision
6652
+ : boundary.sourceReservedRevision;
6653
+ const expectedBindingToken = expectedSourceStatus === "bound"
6654
+ ? boundary.sourceBoundBindingToken
6655
+ : boundary.sourceReservedBindingToken;
6656
+ if (source.status !== expectedSourceStatus ||
6657
+ expectedRevision === undefined ||
6658
+ managedSessionRevision(source) !== expectedRevision ||
6659
+ expectedBindingToken === undefined ||
6660
+ managedSessionBindingToken(source) !== expectedBindingToken) {
6661
+ throw new Error("the deferred Codex foreground source changed; refresh AKK list");
6662
+ }
6663
+ if (!exactBoundCodexStatusCardSourceForDeferredSend({
6664
+ session: { ...source, status: "bound" },
6665
+ terminalId: boundary.terminal.conversationId,
6666
+ terminalControl: boundary.terminal.terminalControl,
6667
+ pid: boundary.terminal.pid,
6668
+ workspace: boundary.terminal.terminalControl.currentPath,
6669
+ processUuid: boundary.processUuid,
6670
+ processBirth: boundary.processBirth
6671
+ }) ||
6672
+ source.last_transition_id !== (expectedSourceStatus === "bound" ? undefined : boundary.transferId) ||
6673
+ managedSessionHasAnyNativeTransition(storeDir, source) ||
6674
+ managedTurnsForSession(storeDir, source.session_id).length !== 0) {
6675
+ throw new Error("the deferred Codex foreground source is no longer an isolated status-card binding");
6676
+ }
6677
+ const liveIncarnation = codexProcessIncarnationForPid(boundary.terminal.pid);
6678
+ if (liveIncarnation.processUuid !== boundary.processUuid ||
6679
+ liveIncarnation.processBirth !== boundary.processBirth) {
6680
+ throw new Error("the deferred Codex foreground process incarnation changed");
6681
+ }
6682
+ const observation = await observeCurrentNativeAgentSessionIdentity({
6683
+ options,
6684
+ agent: "codex",
6685
+ pid: boundary.terminal.pid,
6686
+ cwd: boundary.terminal.terminalControl.currentPath
6387
6687
  });
6388
- return humanObservedHandoffBindingToken({
6389
- terminal_token: terminalToken,
6390
- source_session_id: sourceSession.session_id,
6391
- source_revision: managedSessionRevision(sourceSession),
6392
- source_binding_token: managedSessionBindingToken(sourceSession),
6393
- target
6688
+ if (observation.status === "unavailable") {
6689
+ throw new Error(`Codex native identity observation is unavailable: ${observation.reason}`);
6690
+ }
6691
+ if (observation.status !== "verified_absent") {
6692
+ throw new Error(`Codex materialized native thread ${observation.identity.sessionId}; ` +
6693
+ "refresh AKK list before sending");
6694
+ }
6695
+ if (requireNoDispatch) {
6696
+ assertTerminalIncarnationCanStartTurn(storeDir, boundary.terminal.terminalControl);
6697
+ const currentDispatchSnapshot = deferredCodexForegroundDispatchSnapshot(boundary.terminal.terminalControl);
6698
+ if (currentDispatchSnapshot.status !== boundary.previousDispatchSnapshot.status ||
6699
+ currentDispatchSnapshot.fingerprint !==
6700
+ boundary.previousDispatchSnapshot.fingerprint) {
6701
+ throw new Error("the deferred Codex foreground dispatch history changed; refresh AKK list");
6702
+ }
6703
+ }
6704
+ const status = await createTerminalAgentBridge(options).status("codex", boundary.terminal.terminalControl, {
6705
+ runtime: terminalRuntimeForLiveIdentity({
6706
+ terminal: boundary.terminal,
6707
+ expectedEmptyNativeSession: true
6708
+ })
6394
6709
  });
6395
- }
6396
- function activeTurnHandoffDecisionToken({ handoffToken, turn, ledger }) {
6397
- const takeover = isRecord(turn.native_session_takeover)
6398
- ? turn.native_session_takeover
6399
- : undefined;
6400
- const submission = terminalBridgeSubmission(turn);
6401
- return createHash("sha256")
6402
- .update(JSON.stringify({
6403
- version: 1,
6404
- kind: "active_turn_human_handoff",
6405
- handoff_token: handoffToken,
6406
- session_id: sessionIdForConversation(turn),
6407
- turn_id: turnIdForConversation(turn),
6408
- turn_status: turn.status,
6409
- turn_updated_at: turn.updated_at ?? null,
6410
- current_message_id: stringValue(takeover?.terminal_bridge_message_id) ??
6411
- stringValue(submission?.message_id) ??
6412
- null,
6413
- ledger_generation_id: stringValue(ledger?.generation_id) ?? null,
6414
- ledger_message_id: stringValue(ledger?.message_id) ?? null,
6415
- ledger_status: stringValue(ledger?.status) ?? null
6416
- }))
6417
- .digest("hex");
6418
- }
6419
- const originalExpectedTerminalSelector = new WeakMap();
6420
- function assertExpectedHandoffTokenUsesExactTerminalSelector({ options, terminal }) {
6421
- if (!stringValue(options.expectedTerminalToken)) {
6422
- return;
6710
+ if (status.reachable !== true ||
6711
+ status.approval_state.blocked === true ||
6712
+ !["idle", "unknown"].includes(status.activity_state)) {
6713
+ throw new Error(`terminal ${boundary.terminal.terminalControl.target} is not at a ` +
6714
+ `verified empty Codex prompt (${status.activity_state}: ` +
6715
+ `${status.activity_reason})`);
6423
6716
  }
6424
- const supplied = originalExpectedTerminalSelector.has(options)
6425
- ? originalExpectedTerminalSelector.get(options)
6426
- : stringValue(options.session ?? options.conversation ?? options.conversationId)?.trim();
6427
- if (supplied !== terminal.conversationId) {
6428
- throw new Error("--expected-terminal-token is valid only with the exact full terminal " +
6429
- "conversation selector advertised by AKK list");
6717
+ if (requireEmptyComposer) {
6718
+ await assertCodexComposerReadyForAutomatedInput({
6719
+ options,
6720
+ terminalControl: boundary.terminal.terminalControl
6721
+ });
6430
6722
  }
6723
+ return source;
6431
6724
  }
6432
- async function observedExternalHandoffIdentity({ options, terminal, sourceSession, resolvedIdentity, requireSafeTerminal = true }) {
6433
- const bridge = createTerminalAgentBridge(options);
6434
- const status = await bridge.status(terminal.agent, terminal.terminalControl, { runtime: terminalRuntimeForLiveIdentity({ terminal, physicalOnly: true }) });
6435
- if (requireSafeTerminal) {
6436
- assertSafeTerminalSend(terminal.agent, status);
6725
+ async function prepareDeferredCodexForegroundBinding({ options, terminal, sourceSession, observation, requestText }) {
6726
+ const expectedToken = stringValue(options.expectedTerminalToken);
6727
+ if (terminal.agent !== "codex" ||
6728
+ !sourceSession?.binding ||
6729
+ observation.status !== "verified_absent" ||
6730
+ !expectedToken) {
6731
+ return undefined;
6437
6732
  }
6438
- if (terminal.agent !== "codex") {
6439
- return { identity: resolvedIdentity, status };
6733
+ const processUuid = sourceSession.binding.native_process.process_uuid;
6734
+ const processBirth = sourceSession.binding.native_process.process_birth;
6735
+ const workspace = terminal.terminalControl.currentPath;
6736
+ const liveIncarnation = codexProcessIncarnationForPid(terminal.pid);
6737
+ if (!processUuid ||
6738
+ !processBirth ||
6739
+ !workspace ||
6740
+ !hasCanonicalTerminalEndpoint(terminal.terminalControl) ||
6741
+ !exactBoundCodexStatusCardSourceForDeferredSend({
6742
+ session: sourceSession,
6743
+ terminalId: terminal.conversationId,
6744
+ terminalControl: terminal.terminalControl,
6745
+ pid: terminal.pid,
6746
+ workspace,
6747
+ processUuid: liveIncarnation.processUuid,
6748
+ processBirth: liveIncarnation.processBirth
6749
+ })) {
6750
+ return undefined;
6440
6751
  }
6441
- const sourceBinding = sourceSession.binding;
6442
- const statusCard = terminal.adapter.observeThreadLifecycle?.({
6443
- operation: { kind: "new_thread" },
6444
- phase: "before",
6445
- screen: status.screen.excerpt ?? ""
6752
+ const dispatchSnapshot = deferredCodexForegroundDispatchSnapshot(terminal.terminalControl);
6753
+ const token = deferredCodexForegroundBindingToken({
6754
+ terminalId: terminal.conversationId,
6755
+ terminalControl: terminal.terminalControl,
6756
+ pid: terminal.pid,
6757
+ workspace,
6758
+ processUuid,
6759
+ processBirth,
6760
+ sourceSession,
6761
+ dispatchSnapshot
6446
6762
  });
6447
- const statusCardId = statusCard?.status === "observed" &&
6448
- isExactNativeThreadId(statusCard.nativeThreadId)
6449
- ? statusCard.nativeThreadId.toLowerCase()
6450
- : undefined;
6451
- const sourceId = sourceBinding?.native_thread_id?.toLowerCase();
6452
- if (statusCardId && sourceId && statusCardId !== sourceId) {
6453
- const processUuid = sourceBinding?.native_process.process_uuid;
6763
+ if (expectedToken !== token) {
6764
+ throw new Error("deferred Codex foreground binding requires the fresh exact terminal " +
6765
+ "token advertised by AKK list");
6766
+ }
6767
+ const targetSessionId = createManagedSessionId();
6768
+ const transferId = createDeferredForegroundTransferId();
6769
+ const boundary = {
6770
+ terminal,
6771
+ transferId,
6772
+ targetSessionId,
6773
+ sourceSessionId: sourceSession.session_id,
6774
+ sourceBoundRevision: managedSessionRevision(sourceSession),
6775
+ sourceBoundBindingToken: managedSessionBindingToken(sourceSession),
6776
+ processUuid,
6777
+ processBirth,
6778
+ previousDispatchSnapshot: dispatchSnapshot
6779
+ };
6780
+ await assertDeferredCodexForegroundBindingBoundary({
6781
+ options,
6782
+ boundary,
6783
+ expectedSourceStatus: "bound",
6784
+ requireNoDispatch: true,
6785
+ requireEmptyComposer: true
6786
+ });
6787
+ await assertNativeThreadHasExclusiveOwnership({
6788
+ options,
6789
+ agent: "codex",
6790
+ currentPid: terminal.pid,
6791
+ nativeThreadId: sourceSession.binding.native_thread_id,
6792
+ storeDir: storeDirFromOptions(options),
6793
+ terminalControl: terminal.terminalControl,
6794
+ excludedManagedSessionId: sourceSession.session_id
6795
+ });
6796
+ const storeDir = storeDirFromOptions(options);
6797
+ const existingTransfer = listDeferredForegroundTransfers(storeDir).find((candidate) => !["resolved", "aborted", "abort_resolved"].includes(candidate.status) &&
6798
+ (candidate.source_session_id === sourceSession.session_id ||
6799
+ (candidate.terminal_id === terminal.conversationId &&
6800
+ terminalControlEvidenceMatches(candidate.terminal_endpoint, terminal.terminalControl))));
6801
+ if (existingTransfer) {
6802
+ throw new Error(`deferred foreground transfer ${existingTransfer.transfer_id} is ` +
6803
+ `still ${existingTransfer.status}; refresh AKK list before sending`);
6804
+ }
6805
+ saveDeferredForegroundTransfer(storeDir, {
6806
+ schema: "agent-knock-knock/deferred-foreground-transfer",
6807
+ version: 1,
6808
+ transfer_id: transferId,
6809
+ status: "prepared",
6810
+ input_stage: "none",
6811
+ terminal_id: terminal.conversationId,
6812
+ terminal_endpoint: terminalControlEvidence(terminal.terminalControl),
6813
+ process_pid: terminal.pid,
6814
+ process_uuid: processUuid,
6815
+ process_birth: processBirth,
6816
+ workspace,
6817
+ source_session_id: sourceSession.session_id,
6818
+ source_expected_revision: managedSessionRevision(sourceSession),
6819
+ source_binding_token: managedSessionBindingToken(sourceSession),
6820
+ source_previous_last_transition_id: sourceSession.last_transition_id,
6821
+ source_before_binding: sourceSession.binding,
6822
+ target_session_id: targetSessionId,
6823
+ target_expected_revision: null,
6824
+ previous_dispatch_status: dispatchSnapshot.status,
6825
+ previous_dispatch_fingerprint: dispatchSnapshot.fingerprint,
6826
+ request_hash: required(terminalBridgeRequestFingerprint(terminalSubmissionPayload(requestText)), "deferred foreground request hash is unavailable"),
6827
+ dispatcher_pid: cliPid(),
6828
+ prepared_at: cliNow().toISOString()
6829
+ }, { expectedRevision: null });
6830
+ return boundary;
6831
+ }
6832
+ function deferredForegroundTransferRevision(transfer) {
6833
+ const revision = Number(transfer.revision);
6834
+ if (!Number.isSafeInteger(revision) || revision < 1) {
6835
+ throw new Error(`deferred foreground transfer ${transfer.transfer_id} has no valid revision`);
6836
+ }
6837
+ return revision;
6838
+ }
6839
+ function assertDeferredForegroundTransferMatchesBoundary({ transfer, boundary }) {
6840
+ if (transfer.transfer_id !== boundary.transferId ||
6841
+ transfer.source_session_id !== boundary.sourceSessionId ||
6842
+ transfer.target_session_id !== boundary.targetSessionId ||
6843
+ transfer.source_expected_revision !== boundary.sourceBoundRevision ||
6844
+ transfer.source_binding_token !== boundary.sourceBoundBindingToken ||
6845
+ transfer.previous_dispatch_status !==
6846
+ boundary.previousDispatchSnapshot.status ||
6847
+ transfer.previous_dispatch_fingerprint !==
6848
+ boundary.previousDispatchSnapshot.fingerprint ||
6849
+ transfer.process_pid !== boundary.terminal.pid ||
6850
+ transfer.process_uuid !== boundary.processUuid ||
6851
+ transfer.process_birth !== boundary.processBirth ||
6852
+ transfer.terminal_id !== boundary.terminal.conversationId ||
6853
+ !terminalControlEvidenceMatches(transfer.terminal_endpoint, boundary.terminal.terminalControl)) {
6854
+ throw new Error(`deferred foreground transfer ${boundary.transferId} authority changed`);
6855
+ }
6856
+ }
6857
+ function abortDeferredCodexForegroundTransferBeforeInput({ options, boundary, reason, terminalInputNotStartedAt }) {
6858
+ const storeDir = storeDirFromOptions(options);
6859
+ let transfer = loadDeferredForegroundTransfer(storeDir, boundary.transferId);
6860
+ assertDeferredForegroundTransferMatchesBoundary({ transfer, boundary });
6861
+ if (transfer.status === "abort_resolved") {
6862
+ return;
6863
+ }
6864
+ const provedDispatchDidNotStart = transfer.status === "dispatch_started" &&
6865
+ transfer.input_stage === "dispatch_started" &&
6866
+ terminalInputNotStartedAt !== undefined &&
6867
+ Number.isFinite(Date.parse(terminalInputNotStartedAt));
6868
+ if (transfer.input_stage !== "none" && !provedDispatchDidNotStart) {
6869
+ throw new Error(`deferred foreground transfer ${transfer.transfer_id} may have started ` +
6870
+ "terminal input and cannot restore its source");
6871
+ }
6872
+ if (![
6873
+ "prepared",
6874
+ "source_reserved",
6875
+ "target_prepared",
6876
+ "dispatch_started",
6877
+ "aborted"
6878
+ ].includes(transfer.status)) {
6879
+ throw new Error(`deferred foreground transfer ${transfer.transfer_id} cannot abort from ` +
6880
+ transfer.status);
6881
+ }
6882
+ // Publish the terminal abort intent before restoring either Session. A crash
6883
+ // after this receipt is completed by the Store-only finalizer below; a later
6884
+ // successful retry may evolve the Sessions only after `abort_resolved` makes
6885
+ // that cleanup receipt permanently self-contained.
6886
+ if (transfer.status !== "aborted") {
6887
+ transfer = saveDeferredForegroundTransfer(storeDir, {
6888
+ ...transfer,
6889
+ status: "aborted",
6890
+ aborted_at: cliNow().toISOString(),
6891
+ ...(provedDispatchDidNotStart
6892
+ ? { terminal_input_not_started_at: terminalInputNotStartedAt }
6893
+ : {}),
6894
+ error: JSON.stringify(textSummary(reason))
6895
+ }, { expectedRevision: deferredForegroundTransferRevision(transfer) });
6896
+ }
6897
+ transfer = finalizeAbortedDeferredCodexForegroundTransferCleanup({
6898
+ storeDir,
6899
+ transfer
6900
+ });
6901
+ runtimeLog("info", "deferred_codex_foreground_transfer_aborted", {
6902
+ transfer_id: transfer.transfer_id,
6903
+ source_session_id: transfer.source_session_id,
6904
+ target_session_id: transfer.target_session_id,
6905
+ terminal_input_sent: false,
6906
+ reason: textSummary(reason)
6907
+ });
6908
+ }
6909
+ function finalizeAbortedDeferredCodexForegroundTransferCleanup({ storeDir, transfer }) {
6910
+ let current = loadDeferredForegroundTransfer(storeDir, transfer.transfer_id);
6911
+ if (current.status === "abort_resolved") {
6912
+ return current;
6913
+ }
6914
+ const provedDispatchDidNotStart = current.input_stage === "dispatch_started" &&
6915
+ current.terminal_input_not_started_at !== undefined &&
6916
+ Number.isFinite(Date.parse(current.terminal_input_not_started_at));
6917
+ if (current.status !== "aborted" ||
6918
+ (current.input_stage !== "none" && !provedDispatchDidNotStart)) {
6919
+ throw new Error(`deferred foreground transfer ${current.transfer_id} does not carry a ` +
6920
+ "zero-input abort intent");
6921
+ }
6922
+ let target = tryLoadManagedSession(storeDir, current.target_session_id);
6923
+ if (target) {
6924
+ if (target.lineage.transition_id !== current.transfer_id ||
6925
+ target.lineage.previous_session_id !== current.source_session_id ||
6926
+ target.binding?.native_thread_id ||
6927
+ target.binding?.native_process.rollout ||
6928
+ target.last_transition_id !== current.transfer_id ||
6929
+ !current.target_before_binding ||
6930
+ JSON.stringify(target.binding) !==
6931
+ JSON.stringify(current.target_before_binding)) {
6932
+ throw new Error(`deferred foreground target ${target.session_id} changed before abort cleanup`);
6933
+ }
6934
+ if (target.status === "transitioning") {
6935
+ if (managedSessionRevision(target) !== current.target_prepared_revision ||
6936
+ managedSessionBindingToken(target) !==
6937
+ current.target_prepared_binding_token) {
6938
+ throw new Error(`deferred foreground target ${target.session_id} changed before abort cleanup`);
6939
+ }
6940
+ const detachedAt = cliNow().toISOString();
6941
+ target = saveManagedSession(storeDir, {
6942
+ ...target,
6943
+ status: "detached",
6944
+ detached_at: detachedAt,
6945
+ updated_at: detachedAt
6946
+ }, { expectedRevision: managedSessionRevision(target) });
6947
+ }
6948
+ else if (target.status !== "detached" ||
6949
+ managedSessionRevision(target) !==
6950
+ Number(current.target_prepared_revision) + 1 ||
6951
+ managedSessionBindingToken(target) !== managedSessionBindingToken({
6952
+ session_id: current.target_session_id,
6953
+ status: "detached",
6954
+ binding: current.target_before_binding
6955
+ })) {
6956
+ throw new Error(`deferred foreground target ${target.session_id} cannot finish abort ` +
6957
+ `cleanup from ${target.status}`);
6958
+ }
6959
+ }
6960
+ let source = loadManagedSession(storeDir, current.source_session_id);
6961
+ if (source.status === "transitioning" &&
6962
+ source.last_transition_id === current.transfer_id &&
6963
+ managedSessionRevision(source) === current.source_expected_revision + 1 &&
6964
+ managedSessionBindingToken(source) === managedSessionBindingToken({
6965
+ session_id: current.source_session_id,
6966
+ status: "transitioning",
6967
+ binding: current.source_before_binding
6968
+ }) &&
6969
+ JSON.stringify(source.binding) ===
6970
+ JSON.stringify(current.source_before_binding)) {
6971
+ const restoredAt = cliNow().toISOString();
6972
+ source = saveManagedSession(storeDir, {
6973
+ ...source,
6974
+ status: "bound",
6975
+ last_transition_id: current.source_previous_last_transition_id,
6976
+ updated_at: restoredAt
6977
+ }, { expectedRevision: managedSessionRevision(source) });
6978
+ }
6979
+ const allowedSourceRevisions = current.source_reserved_at
6980
+ ? [current.source_expected_revision + 2]
6981
+ : [
6982
+ current.source_expected_revision,
6983
+ // The source Session CAS precedes publication of source_reserved_at.
6984
+ // This exact +2 state is the idempotent restore after that crash gap.
6985
+ current.source_expected_revision + 2
6986
+ ];
6987
+ if (source.status !== "bound" ||
6988
+ source.last_transition_id !== current.source_previous_last_transition_id ||
6989
+ !allowedSourceRevisions.includes(managedSessionRevision(source)) ||
6990
+ managedSessionBindingToken(source) !== current.source_binding_token ||
6991
+ JSON.stringify(source.binding) !== JSON.stringify(current.source_before_binding)) {
6992
+ throw new Error(`deferred foreground source ${source.session_id} changed before abort cleanup`);
6993
+ }
6994
+ const completedAt = cliNow().toISOString();
6995
+ current = saveDeferredForegroundTransfer(storeDir, {
6996
+ ...current,
6997
+ status: "abort_resolved",
6998
+ abort_cleanup_completed_at: completedAt,
6999
+ abort_source_after_revision: managedSessionRevision(source),
7000
+ abort_source_after_status: "bound",
7001
+ abort_source_after_binding_token: managedSessionBindingToken(source),
7002
+ abort_source_after_binding: source.binding,
7003
+ abort_target_after_status: target ? "detached" : "absent",
7004
+ ...(target
7005
+ ? {
7006
+ abort_target_after_revision: managedSessionRevision(target),
7007
+ abort_target_after_binding_token: managedSessionBindingToken(target),
7008
+ abort_target_after_binding: target.binding
7009
+ }
7010
+ : {})
7011
+ }, { expectedRevision: deferredForegroundTransferRevision(current) });
7012
+ return current;
7013
+ }
7014
+ async function reserveDeferredCodexForegroundTransfer({ options, boundary, targetSession, messageId, turnId, statePath }) {
7015
+ const storeDir = storeDirFromOptions(options);
7016
+ let transfer = loadDeferredForegroundTransfer(storeDir, boundary.transferId);
7017
+ assertDeferredForegroundTransferMatchesBoundary({ transfer, boundary });
7018
+ if (transfer.status !== "prepared" || transfer.input_stage !== "none") {
7019
+ throw new Error(`deferred foreground transfer ${transfer.transfer_id} is already ` +
7020
+ transfer.status);
7021
+ }
7022
+ const source = await assertDeferredCodexForegroundBindingBoundary({
7023
+ options,
7024
+ boundary,
7025
+ expectedSourceStatus: "bound",
7026
+ requireNoDispatch: true,
7027
+ requireEmptyComposer: true
7028
+ });
7029
+ try {
7030
+ const reservedAt = cliNow().toISOString();
7031
+ const reserved = saveManagedSession(storeDir, {
7032
+ ...source,
7033
+ status: "transitioning",
7034
+ last_transition_id: transfer.transfer_id,
7035
+ updated_at: reservedAt
7036
+ }, { expectedRevision: managedSessionRevision(source) });
7037
+ if (cliEnv().AKK_TEST_EXIT_AFTER_DEFERRED_SOURCE_SESSION_RESERVED === "1") {
7038
+ // Crash seam for the only source-reservation window not yet reflected by
7039
+ // source_reserved_at in the transfer receipt. Recovery must prove and
7040
+ // restore the exact transitioning revision without terminal input.
7041
+ cliExit(86);
7042
+ }
7043
+ boundary.sourceReservedRevision = managedSessionRevision(reserved);
7044
+ boundary.sourceReservedBindingToken = managedSessionBindingToken(reserved);
7045
+ transfer = saveDeferredForegroundTransfer(storeDir, {
7046
+ ...transfer,
7047
+ status: "source_reserved",
7048
+ source_reserved_at: reservedAt
7049
+ }, { expectedRevision: deferredForegroundTransferRevision(transfer) });
7050
+ if (cliEnv().AKK_TEST_EXIT_AFTER_DEFERRED_SOURCE_RESERVED === "1") {
7051
+ cliExit(86);
7052
+ }
7053
+ const provisionalTarget = {
7054
+ ...targetSession,
7055
+ status: "transitioning",
7056
+ last_transition_id: transfer.transfer_id
7057
+ };
7058
+ boundary.targetPreparedRevision = 1;
7059
+ boundary.targetPreparedBindingToken = managedSessionBindingToken(provisionalTarget);
7060
+ // Publish the complete target CAS authority before the target itself. A
7061
+ // crash can therefore observe either no target or exactly this revision-1
7062
+ // provisional target; it never leaves an unrecorded Session behind.
7063
+ transfer = saveDeferredForegroundTransfer(storeDir, {
7064
+ ...transfer,
7065
+ status: "target_prepared",
7066
+ target_prepared_at: cliNow().toISOString(),
7067
+ target_prepared_revision: 1,
7068
+ target_prepared_status: "transitioning",
7069
+ target_prepared_last_transition_id: transfer.transfer_id,
7070
+ target_prepared_binding_token: managedSessionBindingToken(provisionalTarget),
7071
+ target_before_binding: provisionalTarget.binding,
7072
+ message_id: messageId,
7073
+ turn_id: turnId,
7074
+ state_path: path.resolve(statePath)
7075
+ }, { expectedRevision: deferredForegroundTransferRevision(transfer) });
7076
+ if (cliEnv().AKK_TEST_EXIT_AFTER_DEFERRED_TARGET_PREPARED === "1") {
7077
+ cliExit(86);
7078
+ }
7079
+ const createdSession = saveManagedSession(storeDir, provisionalTarget, {
7080
+ expectedRevision: null
7081
+ });
7082
+ if (managedSessionRevision(createdSession) !== 1 ||
7083
+ managedSessionBindingToken(createdSession) !==
7084
+ boundary.targetPreparedBindingToken) {
7085
+ throw new Error(`deferred foreground target ${createdSession.session_id} did not ` +
7086
+ "materialize at its prepared CAS authority");
7087
+ }
7088
+ runtimeLog("info", "deferred_codex_foreground_source_reserved", {
7089
+ transfer_id: transfer.transfer_id,
7090
+ terminal_id: boundary.terminal.conversationId,
7091
+ source_session_id: source.session_id,
7092
+ native_thread_id: source.binding?.native_thread_id,
7093
+ process_uuid: boundary.processUuid,
7094
+ process_birth: boundary.processBirth,
7095
+ terminal_input_sent: false
7096
+ });
7097
+ return {
7098
+ createdSession,
7099
+ rollback: () => abortDeferredCodexForegroundTransferBeforeInput({
7100
+ options,
7101
+ boundary,
7102
+ reason: "terminal input was proved not to have started"
7103
+ })
7104
+ };
7105
+ }
7106
+ catch (error) {
7107
+ try {
7108
+ abortDeferredCodexForegroundTransferBeforeInput({
7109
+ options,
7110
+ boundary,
7111
+ reason: error instanceof Error ? error.message : String(error)
7112
+ });
7113
+ }
7114
+ catch (abortError) {
7115
+ throw new Error(`deferred foreground reservation failed and could not be aborted: ${abortError instanceof Error ? abortError.message : String(abortError)}`, { cause: error });
7116
+ }
7117
+ throw error;
7118
+ }
7119
+ }
7120
+ function assertDeferredForegroundTargetPreparedAuthority({ storeDir, transfer }) {
7121
+ const target = loadManagedSession(storeDir, transfer.target_session_id);
7122
+ if (target.status !== "transitioning" ||
7123
+ target.last_transition_id !== transfer.transfer_id ||
7124
+ managedSessionRevision(target) !== transfer.target_prepared_revision ||
7125
+ managedSessionBindingToken(target) !==
7126
+ transfer.target_prepared_binding_token ||
7127
+ JSON.stringify(target.binding) !==
7128
+ JSON.stringify(transfer.target_before_binding) ||
7129
+ target.lineage.transition_id !== transfer.transfer_id ||
7130
+ target.lineage.previous_session_id !== transfer.source_session_id) {
7131
+ throw new Error(`deferred foreground target ${transfer.target_session_id} changed ` +
7132
+ "before terminal dispatch");
7133
+ }
7134
+ return target;
7135
+ }
7136
+ function advanceDeferredCodexForegroundTransferInputStage({ options, boundary, stage, at }) {
7137
+ const storeDir = storeDirFromOptions(options);
7138
+ let transfer = loadDeferredForegroundTransfer(storeDir, boundary.transferId);
7139
+ assertDeferredForegroundTransferMatchesBoundary({ transfer, boundary });
7140
+ assertDeferredForegroundTargetPreparedAuthority({ storeDir, transfer });
7141
+ const source = loadManagedSession(storeDir, transfer.source_session_id);
7142
+ if (source.status !== "transitioning" ||
7143
+ source.last_transition_id !== transfer.transfer_id ||
7144
+ managedSessionRevision(source) !== transfer.source_expected_revision + 1 ||
7145
+ managedSessionBindingToken(source) !== boundary.sourceReservedBindingToken ||
7146
+ JSON.stringify(source.binding) !==
7147
+ JSON.stringify(transfer.source_before_binding)) {
7148
+ throw new Error(`deferred foreground source ${transfer.source_session_id} changed ` +
7149
+ "during terminal dispatch");
7150
+ }
7151
+ if (stage === "text_injected" &&
7152
+ (transfer.status !== "dispatch_started" ||
7153
+ transfer.input_stage !== "dispatch_started")) {
7154
+ throw new Error(`deferred foreground transfer ${transfer.transfer_id} cannot record text ` +
7155
+ `from ${transfer.status}/${transfer.input_stage}`);
7156
+ }
7157
+ if (stage === "enter_dispatched" &&
7158
+ (transfer.status !== "dispatch_started" ||
7159
+ transfer.input_stage !== "text_injected")) {
7160
+ throw new Error(`deferred foreground transfer ${transfer.transfer_id} cannot record Enter ` +
7161
+ `from ${transfer.status}/${transfer.input_stage}`);
7162
+ }
7163
+ transfer = saveDeferredForegroundTransfer(storeDir, {
7164
+ ...transfer,
7165
+ status: "dispatch_started",
7166
+ input_stage: stage,
7167
+ dispatch_started_at: transfer.dispatch_started_at ?? at,
7168
+ ...(stage === "text_injected"
7169
+ ? { text_injected_at: at }
7170
+ : { enter_dispatched_at: at })
7171
+ }, { expectedRevision: deferredForegroundTransferRevision(transfer) });
7172
+ return transfer;
7173
+ }
7174
+ function beginDeferredCodexForegroundTransferDispatch({ options, boundary, at }) {
7175
+ const storeDir = storeDirFromOptions(options);
7176
+ let transfer = loadDeferredForegroundTransfer(storeDir, boundary.transferId);
7177
+ assertDeferredForegroundTransferMatchesBoundary({ transfer, boundary });
7178
+ assertDeferredForegroundTargetPreparedAuthority({ storeDir, transfer });
7179
+ if (transfer.status !== "target_prepared" || transfer.input_stage !== "none") {
7180
+ throw new Error(`deferred foreground transfer ${transfer.transfer_id} cannot begin ` +
7181
+ `dispatch from ${transfer.status}/${transfer.input_stage}`);
7182
+ }
7183
+ transfer = saveDeferredForegroundTransfer(storeDir, {
7184
+ ...transfer,
7185
+ status: "dispatch_started",
7186
+ input_stage: "dispatch_started",
7187
+ dispatch_started_at: at
7188
+ }, { expectedRevision: deferredForegroundTransferRevision(transfer) });
7189
+ return transfer;
7190
+ }
7191
+ function isExactDeferredScrubbedSource({ source, transfer }) {
7192
+ const before = transfer.source_before_binding;
7193
+ const after = source.binding;
7194
+ return Boolean(source.status === "transitioning" &&
7195
+ source.last_transition_id === transfer.transfer_id &&
7196
+ managedSessionRevision(source) === transfer.source_expected_revision + 2 &&
7197
+ after &&
7198
+ after.binding_id !== before.binding_id &&
7199
+ after.generation === before.generation + 1 &&
7200
+ after.terminal_id === before.terminal_id &&
7201
+ terminalControlsShareIncarnation(after.terminal_control, before.terminal_control) &&
7202
+ after.native_thread_id === undefined &&
7203
+ after.native_process.rollout === undefined &&
7204
+ after.native_process.pid === transfer.process_pid &&
7205
+ after.native_process.process_uuid === transfer.process_uuid &&
7206
+ after.native_process.process_birth === transfer.process_birth &&
7207
+ after.native_process.evidence ===
7208
+ "deferred_predecessor_binding_scrubbed");
7209
+ }
7210
+ function isExactDeferredAcceptedTarget({ target, transfer, identity }) {
7211
+ const before = transfer.target_before_binding;
7212
+ const after = target.binding;
7213
+ return Boolean(before &&
7214
+ after &&
7215
+ target.status === "transitioning" &&
7216
+ target.last_transition_id === transfer.transfer_id &&
7217
+ managedSessionRevision(target) ===
7218
+ Number(transfer.target_prepared_revision) + 1 &&
7219
+ after.binding_id === before.binding_id &&
7220
+ after.generation === before.generation &&
7221
+ after.native_thread_id === identity.sessionId &&
7222
+ after.native_process.pid === transfer.process_pid &&
7223
+ after.native_process.process_uuid === identity.processUuid &&
7224
+ after.native_process.process_birth === identity.processBirth &&
7225
+ exactNativeRolloutMatches(after.native_process.rollout, identity.rollout));
7226
+ }
7227
+ async function commitDeferredCodexForegroundBinding({ options, boundary, identity, acceptedAt }) {
7228
+ const storeDir = storeDirFromOptions(options);
7229
+ let transfer = loadDeferredForegroundTransfer(storeDir, boundary.transferId);
7230
+ assertDeferredForegroundTransferMatchesBoundary({ transfer, boundary });
7231
+ if (!["dispatch_started", "uncertain"].includes(transfer.status) ||
7232
+ transfer.input_stage === "none" ||
7233
+ identity.processUuid !== transfer.process_uuid ||
7234
+ identity.processBirth !== transfer.process_birth ||
7235
+ !isExactNativeThreadId(identity.sessionId) ||
7236
+ !isCompleteNativeRollout(identity.rollout)) {
7237
+ throw new Error(`deferred foreground transfer ${transfer.transfer_id} lacks exact ` +
7238
+ "accepted identity authority");
7239
+ }
7240
+ let source = loadManagedSession(storeDir, transfer.source_session_id);
7241
+ const sourceIsReserved = source.status === "transitioning" &&
7242
+ source.last_transition_id === transfer.transfer_id &&
7243
+ managedSessionRevision(source) === transfer.source_expected_revision + 1 &&
7244
+ managedSessionBindingToken(source) === boundary.sourceReservedBindingToken &&
7245
+ JSON.stringify(source.binding) ===
7246
+ JSON.stringify(transfer.source_before_binding);
7247
+ const sameNativeThread = identity.sessionId.toLowerCase() ===
7248
+ transfer.source_before_binding.native_thread_id?.toLowerCase();
7249
+ const sourceAlreadyScrubbed = sameNativeThread &&
7250
+ isExactDeferredScrubbedSource({ source, transfer });
7251
+ if (!sourceIsReserved && !sourceAlreadyScrubbed) {
7252
+ throw new Error(`deferred foreground source ${source.session_id} changed before commit`);
7253
+ }
7254
+ let target = loadManagedSession(storeDir, transfer.target_session_id);
7255
+ const targetAlreadyAccepted = isExactDeferredAcceptedTarget({
7256
+ target,
7257
+ transfer,
7258
+ identity
7259
+ });
7260
+ if (!targetAlreadyAccepted) {
7261
+ target = assertDeferredForegroundTargetPreparedAuthority({
7262
+ storeDir,
7263
+ transfer
7264
+ });
7265
+ }
7266
+ // Prove there is no third-party Store/process owner before the first
7267
+ // retirement or target-binding CAS. The one permitted existing claim is
7268
+ // the reserved predecessor when Codex reused its status-card UUID.
7269
+ await assertNativeThreadHasExclusiveOwnership({
7270
+ options,
7271
+ agent: "codex",
7272
+ currentPid: transfer.process_pid,
7273
+ nativeThreadId: identity.sessionId,
7274
+ storeDir,
7275
+ terminalControl: boundary.terminal.terminalControl,
7276
+ excludedManagedSessionId: targetAlreadyAccepted
7277
+ ? transfer.target_session_id
7278
+ : sameNativeThread
7279
+ ? transfer.source_session_id
7280
+ : transfer.target_session_id
7281
+ });
7282
+ const retirement = sameNativeThread
7283
+ ? "binding_scrubbed_same_native_thread"
7284
+ : "binding_retained";
7285
+ if (sameNativeThread && !sourceAlreadyScrubbed) {
7286
+ const scrubbedAt = cliNow();
7287
+ const scrubbedBinding = terminalBindingFrom({
7288
+ terminalId: transfer.source_before_binding.terminal_id,
7289
+ terminalControl: transfer.source_before_binding.terminal_control,
7290
+ pid: transfer.process_pid,
7291
+ processUuid: transfer.process_uuid,
7292
+ processBirth: transfer.process_birth,
7293
+ evidence: "deferred_predecessor_binding_scrubbed",
7294
+ generation: transfer.source_before_binding.generation + 1,
7295
+ now: scrubbedAt
7296
+ });
7297
+ source = saveManagedSession(storeDir, {
7298
+ ...source,
7299
+ binding: scrubbedBinding,
7300
+ updated_at: scrubbedAt.toISOString()
7301
+ }, { expectedRevision: managedSessionRevision(source) });
7302
+ if (cliEnv().AKK_TEST_EXIT_AFTER_DEFERRED_SOURCE_SCRUBBED === "1") {
7303
+ cliExit(86);
7304
+ }
7305
+ }
7306
+ if (!targetAlreadyAccepted) {
7307
+ const acceptedBinding = {
7308
+ ...target.binding,
7309
+ native_thread_id: identity.sessionId,
7310
+ native_process: {
7311
+ ...target.binding.native_process,
7312
+ process_uuid: identity.processUuid,
7313
+ process_birth: identity.processBirth,
7314
+ rollout: identity.rollout,
7315
+ evidence: identity.evidence
7316
+ },
7317
+ last_verified_at: acceptedAt
7318
+ };
7319
+ target = saveManagedSession(storeDir, {
7320
+ ...target,
7321
+ binding: acceptedBinding,
7322
+ updated_at: acceptedAt
7323
+ }, { expectedRevision: managedSessionRevision(target) });
7324
+ if (cliEnv().AKK_TEST_EXIT_AFTER_DEFERRED_TARGET_ACCEPTED === "1") {
7325
+ cliExit(86);
7326
+ }
7327
+ }
7328
+ const recoveredFromUncertainty = transfer.status === "uncertain";
7329
+ const transportProofAt = transfer.dispatch_started_at ?? acceptedAt;
7330
+ transfer = saveDeferredForegroundTransfer(storeDir, {
7331
+ ...transfer,
7332
+ status: "committed",
7333
+ input_stage: "agent_accepted",
7334
+ dispatch_started_at: transfer.dispatch_started_at ?? transportProofAt,
7335
+ text_injected_at: transfer.text_injected_at ?? transportProofAt,
7336
+ enter_dispatched_at: transfer.enter_dispatched_at ?? transportProofAt,
7337
+ agent_accepted_at: acceptedAt,
7338
+ target_native_thread_id: identity.sessionId,
7339
+ target_accepted_revision: managedSessionRevision(target),
7340
+ target_accepted_status: "transitioning",
7341
+ target_accepted_binding_token: managedSessionBindingToken(target),
7342
+ target_accepted_binding: target.binding,
7343
+ source_pre_retirement_revision: managedSessionRevision(source),
7344
+ source_pre_retirement_status: "transitioning",
7345
+ source_pre_retirement_binding_token: managedSessionBindingToken(source),
7346
+ source_pre_retirement_binding: source.binding,
7347
+ source_retirement: retirement,
7348
+ committed_at: acceptedAt,
7349
+ ...(recoveredFromUncertainty ? { recovered_at: acceptedAt } : {})
7350
+ }, { expectedRevision: deferredForegroundTransferRevision(transfer) });
7351
+ if (cliEnv().AKK_TEST_EXIT_AFTER_DEFERRED_COMMITTED === "1") {
7352
+ cliExit(86);
7353
+ }
7354
+ return resolveCommittedDeferredCodexForegroundTransfer({
7355
+ options,
7356
+ boundary
7357
+ });
7358
+ }
7359
+ async function resolveCommittedDeferredCodexForegroundTransfer({ options, boundary }) {
7360
+ const storeDir = storeDirFromOptions(options);
7361
+ let transfer = loadDeferredForegroundTransfer(storeDir, boundary.transferId);
7362
+ assertDeferredForegroundTransferMatchesBoundary({ transfer, boundary });
7363
+ if (transfer.status === "resolved") {
7364
+ const resolvedTarget = loadManagedSession(storeDir, transfer.target_session_id);
7365
+ if (resolvedTarget.status !== "bound" ||
7366
+ managedSessionRevision(resolvedTarget) !== transfer.target_after_revision ||
7367
+ managedSessionBindingToken(resolvedTarget) !==
7368
+ transfer.target_after_binding_token) {
7369
+ throw new Error(`resolved deferred target ${resolvedTarget.session_id} changed`);
7370
+ }
7371
+ return resolvedTarget;
7372
+ }
7373
+ if (transfer.status !== "committed" ||
7374
+ !transfer.source_pre_retirement_binding ||
7375
+ !transfer.source_pre_retirement_revision ||
7376
+ !transfer.source_pre_retirement_binding_token ||
7377
+ !transfer.target_accepted_binding ||
7378
+ !transfer.target_accepted_revision ||
7379
+ !transfer.target_accepted_binding_token ||
7380
+ !transfer.target_native_thread_id) {
7381
+ throw new Error(`deferred foreground transfer ${transfer.transfer_id} is not committed`);
7382
+ }
7383
+ let source = loadManagedSession(storeDir, transfer.source_session_id);
7384
+ if (source.status === "transitioning") {
7385
+ if (source.last_transition_id !== transfer.transfer_id ||
7386
+ managedSessionRevision(source) !== transfer.source_pre_retirement_revision ||
7387
+ managedSessionBindingToken(source) !==
7388
+ transfer.source_pre_retirement_binding_token ||
7389
+ JSON.stringify(source.binding) !==
7390
+ JSON.stringify(transfer.source_pre_retirement_binding)) {
7391
+ throw new Error(`deferred foreground source ${source.session_id} changed during commit recovery`);
7392
+ }
7393
+ const detachedAt = cliNow().toISOString();
7394
+ source = saveManagedSession(storeDir, {
7395
+ ...source,
7396
+ status: "detached",
7397
+ last_transition_id: transfer.source_previous_last_transition_id,
7398
+ detached_at: detachedAt,
7399
+ updated_at: detachedAt
7400
+ }, { expectedRevision: managedSessionRevision(source) });
7401
+ if (cliEnv().AKK_TEST_EXIT_AFTER_DEFERRED_SOURCE_DETACHED === "1") {
7402
+ cliExit(86);
7403
+ }
7404
+ }
7405
+ else if (source.status !== "detached" ||
7406
+ managedSessionRevision(source) !==
7407
+ transfer.source_pre_retirement_revision + 1 ||
7408
+ managedSessionBindingToken(source) !== managedSessionBindingToken({
7409
+ session_id: source.session_id,
7410
+ status: "detached",
7411
+ binding: transfer.source_pre_retirement_binding
7412
+ }) ||
7413
+ JSON.stringify(source.binding) !==
7414
+ JSON.stringify(transfer.source_pre_retirement_binding)) {
7415
+ throw new Error(`deferred foreground source ${source.session_id} has no exact retired receipt`);
7416
+ }
7417
+ let target = loadManagedSession(storeDir, transfer.target_session_id);
7418
+ if (target.status === "transitioning") {
7419
+ if (target.last_transition_id !== transfer.transfer_id ||
7420
+ managedSessionRevision(target) !== transfer.target_accepted_revision ||
7421
+ managedSessionBindingToken(target) !==
7422
+ transfer.target_accepted_binding_token ||
7423
+ JSON.stringify(target.binding) !==
7424
+ JSON.stringify(transfer.target_accepted_binding)) {
7425
+ throw new Error(`deferred foreground target ${target.session_id} changed during commit recovery`);
7426
+ }
7427
+ const boundAt = cliNow().toISOString();
7428
+ target = saveManagedSession(storeDir, {
7429
+ ...target,
7430
+ status: "bound",
7431
+ last_transition_id: undefined,
7432
+ updated_at: boundAt
7433
+ }, { expectedRevision: managedSessionRevision(target) });
7434
+ if (cliEnv().AKK_TEST_EXIT_AFTER_DEFERRED_TARGET_BOUND === "1") {
7435
+ cliExit(86);
7436
+ }
7437
+ }
7438
+ else if (target.status !== "bound" ||
7439
+ managedSessionRevision(target) !== transfer.target_accepted_revision + 1 ||
7440
+ managedSessionBindingToken(target) !== managedSessionBindingToken({
7441
+ session_id: target.session_id,
7442
+ status: "bound",
7443
+ binding: transfer.target_accepted_binding
7444
+ }) ||
7445
+ JSON.stringify(target.binding) !==
7446
+ JSON.stringify(transfer.target_accepted_binding)) {
7447
+ throw new Error(`deferred foreground target ${target.session_id} has no exact bound receipt`);
7448
+ }
7449
+ await assertNativeThreadHasExclusiveOwnership({
7450
+ options,
7451
+ agent: "codex",
7452
+ currentPid: transfer.process_pid,
7453
+ nativeThreadId: transfer.target_native_thread_id,
7454
+ storeDir,
7455
+ terminalControl: boundary.terminal.terminalControl,
7456
+ excludedManagedSessionId: target.session_id
7457
+ });
7458
+ transfer = saveDeferredForegroundTransfer(storeDir, {
7459
+ ...transfer,
7460
+ status: "resolved",
7461
+ target_after_revision: managedSessionRevision(target),
7462
+ target_after_status: "bound",
7463
+ target_after_binding_token: managedSessionBindingToken(target),
7464
+ source_after_revision: managedSessionRevision(source),
7465
+ source_after_status: "detached",
7466
+ source_after_binding: source.binding,
7467
+ source_after_binding_token: managedSessionBindingToken(source),
7468
+ resolved_at: cliNow().toISOString()
7469
+ }, { expectedRevision: deferredForegroundTransferRevision(transfer) });
7470
+ runtimeLog("info", "deferred_codex_foreground_transfer_resolved", {
7471
+ transfer_id: transfer.transfer_id,
7472
+ source_session_id: source.session_id,
7473
+ target_session_id: target.session_id,
7474
+ native_thread_id: transfer.target_native_thread_id,
7475
+ source_retirement: transfer.source_retirement
7476
+ });
7477
+ return target;
7478
+ }
7479
+ function markDeferredCodexForegroundTransferUncertain({ options, boundary, reason }) {
7480
+ const storeDir = storeDirFromOptions(options);
7481
+ let transfer = loadDeferredForegroundTransfer(storeDir, boundary.transferId);
7482
+ assertDeferredForegroundTransferMatchesBoundary({ transfer, boundary });
7483
+ if (["committed", "resolved", "uncertain"].includes(transfer.status)) {
7484
+ return transfer;
7485
+ }
7486
+ if (![
7487
+ "target_prepared",
7488
+ "dispatch_started"
7489
+ ].includes(transfer.status)) {
7490
+ throw new Error(`deferred foreground transfer ${transfer.transfer_id} has no durable ` +
7491
+ "dispatch-start fence");
7492
+ }
7493
+ transfer = saveDeferredForegroundTransfer(storeDir, {
7494
+ ...transfer,
7495
+ status: "uncertain",
7496
+ input_stage: transfer.input_stage === "none"
7497
+ ? "dispatch_started"
7498
+ : transfer.input_stage,
7499
+ dispatch_started_at: transfer.dispatch_started_at ?? cliNow().toISOString(),
7500
+ uncertain_at: cliNow().toISOString(),
7501
+ error: reason.slice(0, 2000),
7502
+ do_not_retry: true
7503
+ }, { expectedRevision: deferredForegroundTransferRevision(transfer) });
7504
+ return transfer;
7505
+ }
7506
+ function observedHandoffAuthorityToken({ terminal, identity, sourceSession, target }) {
7507
+ const exact = exactLifecycleProcessIdentity(terminal, identity);
7508
+ const terminalToken = unmanagedTerminalBindingToken({
7509
+ terminalId: terminal.conversationId,
7510
+ terminalControl: terminal.terminalControl,
7511
+ agent: terminal.agent,
7512
+ pid: terminal.pid,
7513
+ workspace: terminal.terminalControl.currentPath ?? cliCwd(),
7514
+ nativeThreadId: exact.sessionId,
7515
+ processUuid: exact.processUuid,
7516
+ processBirth: exact.processBirth,
7517
+ rollout: exact.rollout
7518
+ });
7519
+ return humanObservedHandoffBindingToken({
7520
+ terminal_token: terminalToken,
7521
+ source_session_id: sourceSession.session_id,
7522
+ source_revision: managedSessionRevision(sourceSession),
7523
+ source_binding_token: managedSessionBindingToken(sourceSession),
7524
+ target
7525
+ });
7526
+ }
7527
+ function activeTurnHandoffDecisionToken({ handoffToken, turn, ledger }) {
7528
+ const takeover = isRecord(turn.native_session_takeover)
7529
+ ? turn.native_session_takeover
7530
+ : undefined;
7531
+ const submission = terminalBridgeSubmission(turn);
7532
+ return createHash("sha256")
7533
+ .update(JSON.stringify({
7534
+ version: 1,
7535
+ kind: "active_turn_human_handoff",
7536
+ handoff_token: handoffToken,
7537
+ session_id: sessionIdForConversation(turn),
7538
+ turn_id: turnIdForConversation(turn),
7539
+ turn_status: turn.status,
7540
+ turn_updated_at: turn.updated_at ?? null,
7541
+ current_message_id: stringValue(takeover?.terminal_bridge_message_id) ??
7542
+ stringValue(submission?.message_id) ??
7543
+ null,
7544
+ ledger_generation_id: stringValue(ledger?.generation_id) ?? null,
7545
+ ledger_message_id: stringValue(ledger?.message_id) ?? null,
7546
+ ledger_status: stringValue(ledger?.status) ?? null
7547
+ }))
7548
+ .digest("hex");
7549
+ }
7550
+ const originalExpectedTerminalSelector = new WeakMap();
7551
+ function assertExpectedHandoffTokenUsesExactTerminalSelector({ options, terminal }) {
7552
+ if (!stringValue(options.expectedTerminalToken)) {
7553
+ return;
7554
+ }
7555
+ const supplied = originalExpectedTerminalSelector.has(options)
7556
+ ? originalExpectedTerminalSelector.get(options)
7557
+ : stringValue(options.session ?? options.conversation ?? options.conversationId)?.trim();
7558
+ if (supplied !== terminal.conversationId) {
7559
+ throw new Error("--expected-terminal-token is valid only with the exact full terminal " +
7560
+ "conversation selector advertised by AKK list");
7561
+ }
7562
+ }
7563
+ async function observedExternalHandoffIdentity({ options, terminal, sourceSession, resolvedIdentity, requireSafeTerminal = true }) {
7564
+ const bridge = createTerminalAgentBridge(options);
7565
+ const status = await bridge.status(terminal.agent, terminal.terminalControl, { runtime: terminalRuntimeForLiveIdentity({ terminal, physicalOnly: true }) });
7566
+ if (requireSafeTerminal) {
7567
+ assertSafeTerminalSend(terminal.agent, status);
7568
+ }
7569
+ if (terminal.agent !== "codex") {
7570
+ return { identity: resolvedIdentity, status };
7571
+ }
7572
+ const sourceBinding = sourceSession.binding;
7573
+ const statusCard = terminal.adapter.observeThreadLifecycle?.({
7574
+ operation: { kind: "new_thread" },
7575
+ phase: "before",
7576
+ screen: status.screen.excerpt ?? ""
7577
+ });
7578
+ const statusCardId = statusCard?.status === "observed" &&
7579
+ isExactNativeThreadId(statusCard.nativeThreadId)
7580
+ ? statusCard.nativeThreadId.toLowerCase()
7581
+ : undefined;
7582
+ const sourceId = sourceBinding?.native_thread_id?.toLowerCase();
7583
+ if (statusCardId && sourceId && statusCardId !== sourceId) {
7584
+ const processUuid = sourceBinding?.native_process.process_uuid;
6454
7585
  const processBirth = sourceBinding?.native_process.process_birth;
6455
7586
  if (!processUuid || !processBirth) {
6456
7587
  return { identity: undefined, status };
@@ -10016,7 +11147,7 @@ async function runSend(options) {
10016
11147
  currentNativeIdentity = undefined;
10017
11148
  }
10018
11149
  const physicalNativeIdentityBeforeHandoff = currentNativeIdentity;
10019
- const handoff = await maybeAdoptObservedExternalThread({
11150
+ let handoff = await maybeAdoptObservedExternalThread({
10020
11151
  options,
10021
11152
  terminal: terminalConversation,
10022
11153
  sourceSession: claimedSession,
@@ -10029,19 +11160,42 @@ async function runSend(options) {
10029
11160
  !handoff.session?.binding?.native_process.rollout
10030
11161
  ? physicalNativeIdentityBeforeHandoff
10031
11162
  : handoff.identity;
10032
- let managedSession = handoff.session ??
10033
- materializeCurrentManagedSession({
11163
+ const deferredCodexForegroundBinding = !handoff.adopted
11164
+ ? await prepareDeferredCodexForegroundBinding({
10034
11165
  options,
10035
11166
  terminal: terminalConversation,
10036
- identity: currentNativeIdentity
10037
- });
10038
- if (!managedSession && currentNativeIdentity) {
10039
- managedSession = await reattachManagedSessionForNativeIdentity({
10040
- options,
10041
- terminal: terminalConversation,
10042
- identity: currentNativeIdentity,
10043
- storeDir: rawStoreDir
10044
- });
11167
+ sourceSession: claimedSession,
11168
+ observation: nativeIdentityObservation,
11169
+ requestText: String(messageBody)
11170
+ })
11171
+ : undefined;
11172
+ if (deferredCodexForegroundBinding) {
11173
+ claimedSession = undefined;
11174
+ knownCodexCompanions = { additional: [] };
11175
+ currentNativeIdentity = undefined;
11176
+ handoff = { identity: undefined, adopted: false };
11177
+ }
11178
+ else if (stringValue(options.expectedTerminalToken) &&
11179
+ !verifiedEmptyHandoff &&
11180
+ !handoff.adopted) {
11181
+ throw new Error("the expected terminal token no longer authorizes the current " +
11182
+ "terminal context; refresh AKK list");
11183
+ }
11184
+ let managedSession = deferredCodexForegroundBinding
11185
+ ? undefined
11186
+ : handoff.session ??
11187
+ materializeCurrentManagedSession({
11188
+ options,
11189
+ terminal: terminalConversation,
11190
+ identity: currentNativeIdentity
11191
+ });
11192
+ if (!managedSession && currentNativeIdentity) {
11193
+ managedSession = await reattachManagedSessionForNativeIdentity({
11194
+ options,
11195
+ terminal: terminalConversation,
11196
+ identity: currentNativeIdentity,
11197
+ storeDir: rawStoreDir
11198
+ });
10045
11199
  }
10046
11200
  let pendingRawAttachSessionCreate;
10047
11201
  if (!managedSession) {
@@ -10056,13 +11210,29 @@ async function runSend(options) {
10056
11210
  });
10057
11211
  }
10058
11212
  managedSession = createBoundManagedSession({
10059
- sessionId: createManagedSessionId(),
11213
+ sessionId: deferredCodexForegroundBinding?.targetSessionId ??
11214
+ createManagedSessionId(),
10060
11215
  terminal: terminalConversation,
10061
11216
  identity: currentNativeIdentity,
10062
- lineage: { created_by: "attach" }
11217
+ lineage: deferredCodexForegroundBinding
11218
+ ? {
11219
+ created_by: "attach",
11220
+ previous_session_id: deferredCodexForegroundBinding.sourceSessionId,
11221
+ transition_id: deferredCodexForegroundBinding.transferId
11222
+ }
11223
+ : { created_by: "attach" }
10063
11224
  });
10064
11225
  if (terminalConversation.agent === "codex" &&
10065
11226
  !currentNativeIdentity) {
11227
+ if (deferredCodexForegroundBinding) {
11228
+ managedSession = {
11229
+ ...managedSession,
11230
+ // Both endpoints stay fenced from older writers until the
11231
+ // dedicated transfer reaches its resolved receipt.
11232
+ status: "transitioning",
11233
+ last_transition_id: deferredCodexForegroundBinding.transferId
11234
+ };
11235
+ }
10066
11236
  pendingRawAttachSessionCreate = managedSession;
10067
11237
  }
10068
11238
  else {
@@ -10131,7 +11301,8 @@ async function runSend(options) {
10131
11301
  terminalControl: terminalConversation.terminalControl,
10132
11302
  previousTurn: reusableTurn,
10133
11303
  managedSession,
10134
- nativeAgentIdentity: materializedNativeIdentity
11304
+ nativeAgentIdentity: materializedNativeIdentity,
11305
+ deferredForegroundTransferId: deferredCodexForegroundBinding?.transferId
10135
11306
  });
10136
11307
  ensureStoreWritable(managed.conversation.store_dir);
10137
11308
  ensureDir(path.dirname(managed.statePath));
@@ -10152,8 +11323,30 @@ async function runSend(options) {
10152
11323
  recordMessageAfterSend: true,
10153
11324
  recordRawAttachmentAfterSend: reusableTurn === undefined,
10154
11325
  onTerminalPreflightVerified: pendingRawAttachSessionCreate
10155
- ? () => {
10156
- const createdSession = saveManagedSession(rawStoreDir, pendingRawAttachSessionCreate, { expectedRevision: null });
11326
+ ? async () => {
11327
+ let createdSession;
11328
+ try {
11329
+ if (deferredCodexForegroundBinding) {
11330
+ const reserved = await reserveDeferredCodexForegroundTransfer({
11331
+ options,
11332
+ boundary: deferredCodexForegroundBinding,
11333
+ targetSession: pendingRawAttachSessionCreate,
11334
+ messageId: managed.message.id,
11335
+ turnId: turnIdForConversation(managed.conversation),
11336
+ statePath: managed.statePath
11337
+ });
11338
+ createdSession = reserved.createdSession;
11339
+ managedSession = createdSession;
11340
+ pendingRawAttachSessionCreate = undefined;
11341
+ return reserved.rollback;
11342
+ }
11343
+ createdSession = saveManagedSession(rawStoreDir, pendingRawAttachSessionCreate, {
11344
+ expectedRevision: null
11345
+ });
11346
+ }
11347
+ catch (error) {
11348
+ throw error;
11349
+ }
10157
11350
  managedSession = createdSession;
10158
11351
  pendingRawAttachSessionCreate = undefined;
10159
11352
  return () => {
@@ -10184,7 +11377,8 @@ async function runSend(options) {
10184
11377
  transition: handoff.transition
10185
11378
  }
10186
11379
  : undefined,
10187
- verifiedEmptyCodexHandoff: verifiedEmptyHandoff?.boundary
11380
+ verifiedEmptyCodexHandoff: verifiedEmptyHandoff?.boundary,
11381
+ deferredCodexForegroundBinding
10188
11382
  });
10189
11383
  }
10190
11384
  finally {
@@ -10769,6 +11963,7 @@ async function runApprove(options) {
10769
11963
  throw new Error("approval state changed while waiting for terminal control; refresh status and retry");
10770
11964
  }
10771
11965
  assertManagedTerminalDispatchOwner({
11966
+ storeDir: writerStoreDir,
10772
11967
  conversation: currentConversation,
10773
11968
  terminalControl: currentControl,
10774
11969
  action: "approve"
@@ -11144,7 +12339,7 @@ async function runTerminalConversationApprove({ options, conversationId, agent,
11144
12339
  releaseTerminalLock();
11145
12340
  }
11146
12341
  }
11147
- async function runTerminalControlSend({ options, conversation, nextConversation, statePath, logPath, executor, message, terminalControl, terminalSendLockHeld = false, terminalStateLockHeld = false, storeWriterLeaseHeld = false, recordMessageAfterSend = false, recordRawAttachmentAfterSend = false, onTerminalPreflightVerified = undefined, allowedPreMaterializationIdentity = undefined, allowedAdditionalIdentities = [], observedHandoff = undefined, verifiedEmptyCodexHandoff = undefined, continuingTurnResponse = false }) {
12342
+ async function runTerminalControlSend({ options, conversation, nextConversation, statePath, logPath, executor, message, terminalControl, terminalSendLockHeld = false, terminalStateLockHeld = false, storeWriterLeaseHeld = false, recordMessageAfterSend = false, recordRawAttachmentAfterSend = false, onTerminalPreflightVerified = undefined, allowedPreMaterializationIdentity = undefined, allowedAdditionalIdentities = [], observedHandoff = undefined, verifiedEmptyCodexHandoff = undefined, deferredCodexForegroundBinding = undefined, continuingTurnResponse = false }) {
11148
12343
  const bridge = terminalBridgeEnabled(conversation);
11149
12344
  if (!terminalSendLockHeld) {
11150
12345
  const releaseTerminalLock = acquireTerminalBridgeSendLock(storeDirFromOptions(options), terminalControl, { timeoutMs: 30000 });
@@ -11168,6 +12363,7 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
11168
12363
  allowedAdditionalIdentities,
11169
12364
  observedHandoff,
11170
12365
  verifiedEmptyCodexHandoff,
12366
+ deferredCodexForegroundBinding,
11171
12367
  continuingTurnResponse
11172
12368
  });
11173
12369
  }
@@ -11209,6 +12405,7 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
11209
12405
  allowedAdditionalIdentities,
11210
12406
  observedHandoff,
11211
12407
  verifiedEmptyCodexHandoff,
12408
+ deferredCodexForegroundBinding,
11212
12409
  continuingTurnResponse
11213
12410
  });
11214
12411
  }
@@ -11237,6 +12434,7 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
11237
12434
  allowedAdditionalIdentities,
11238
12435
  observedHandoff,
11239
12436
  verifiedEmptyCodexHandoff,
12437
+ deferredCodexForegroundBinding,
11240
12438
  continuingTurnResponse
11241
12439
  }));
11242
12440
  }
@@ -11245,7 +12443,7 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
11245
12443
  const agentTimeoutMinutes = Number(options.agentTimeoutMinutes ?? DEFAULT_AGENT_TIMEOUT_MINUTES);
11246
12444
  const agentHardTimeoutMinutes = positiveMinutes(options.agentHardTimeoutMinutes ?? DEFAULT_AGENT_HARD_TIMEOUT_MINUTES, "--agent-hard-timeout-minutes");
11247
12445
  const terminalPayload = terminalSubmissionPayload(String(message.body ?? ""));
11248
- const terminalRequestHash = terminalBridgeRequestFingerprint(terminalPayload);
12446
+ const terminalRequestHash = required(terminalBridgeRequestFingerprint(terminalPayload), "terminal request hash is unavailable");
11249
12447
  let previousDispatchLedger = resolveTerminalDispatchLedgerPaneIncarnation(terminalControl, loadTerminalBridgeDispatchLedger(terminalControl));
11250
12448
  previousDispatchLedger =
11251
12449
  reconcilePreparedTerminalDispatchLedger(terminalControl, previousDispatchLedger);
@@ -11418,12 +12616,12 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
11418
12616
  scrollbackLines: Number(options.scrollbackLines ?? 120),
11419
12617
  runtime: preSendRuntime
11420
12618
  });
11421
- if (verifiedEmptyCodexHandoff) {
12619
+ if (verifiedEmptyCodexHandoff || deferredCodexForegroundBinding) {
11422
12620
  if (executor.kind !== "codex" ||
11423
12621
  status.reachable !== true ||
11424
12622
  status.approval_state.blocked === true ||
11425
12623
  !["idle", "unknown"].includes(status.activity_state)) {
11426
- throw new Error(`Codex verified-empty handoff is not at a safe prompt ` +
12624
+ throw new Error(`Codex deferred foreground send is not at a safe prompt ` +
11427
12625
  `(${status.activity_state}: ${status.activity_reason})`);
11428
12626
  }
11429
12627
  }
@@ -11488,7 +12686,7 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
11488
12686
  // acceptance check has passed, but before the Turn or dispatch ledger can
11489
12687
  // become durable. This prevents a failed virgin attach from leaving a
11490
12688
  // zero-identity `bound` Session that fences every later control action.
11491
- let rollbackPreTransportAttach = onTerminalPreflightVerified?.();
12689
+ let rollbackPreTransportAttach = await onTerminalPreflightVerified?.();
11492
12690
  const rollbackRawAttachBeforeTransport = () => {
11493
12691
  if (!rollbackPreTransportAttach) {
11494
12692
  return true;
@@ -11508,6 +12706,39 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
11508
12706
  return false;
11509
12707
  }
11510
12708
  };
12709
+ const abortDeferredPreInputBeforeTransport = (terminalInputNotStartedAt) => {
12710
+ if (!deferredCodexForegroundBinding) {
12711
+ return false;
12712
+ }
12713
+ if (!terminalStateLockHeld) {
12714
+ throw new Error("deferred Codex pre-input abort requires the exact Turn state lock");
12715
+ }
12716
+ const deferredStoreDir = storeDirFromOptions(options);
12717
+ const transfer = loadDeferredForegroundTransfer(deferredStoreDir, deferredCodexForegroundBinding.transferId);
12718
+ const durableTerminalInputNotStartedAt = terminalInputNotStartedAt === undefined || transfer.input_stage === "none"
12719
+ ? undefined
12720
+ : transfer.status === "dispatch_started" &&
12721
+ transfer.input_stage === "dispatch_started"
12722
+ ? terminalInputNotStartedAt
12723
+ : (() => {
12724
+ throw new Error(`deferred foreground transfer ${transfer.transfer_id} has ` +
12725
+ `${transfer.input_stage} input evidence and cannot accept a ` +
12726
+ "terminal-input-not-started result");
12727
+ })();
12728
+ abortPreparedDeferredForegroundTurn({
12729
+ storeDir: deferredStoreDir,
12730
+ terminal: deferredCodexForegroundBinding.terminal,
12731
+ transfer,
12732
+ boundary: deferredCodexForegroundBinding,
12733
+ stateLockHeld: true,
12734
+ terminalInputNotStartedAt: durableTerminalInputNotStartedAt
12735
+ });
12736
+ // abortPreparedDeferredForegroundTurn has already made the Turn, ledger,
12737
+ // transfer, and both Sessions durable in zero-input terminal states. Do
12738
+ // not invoke the reservation rollback closure a second time.
12739
+ rollbackPreTransportAttach = undefined;
12740
+ return true;
12741
+ };
11511
12742
  const bridgeConversation = bridge
11512
12743
  ? withTerminalBridgeState({
11513
12744
  conversation: nextConversation,
@@ -11553,11 +12784,13 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
11553
12784
  }
11554
12785
  catch (error) {
11555
12786
  try {
11556
- restoreTerminalBridgeDispatchLedger({
11557
- terminalControl,
11558
- previousLedger: previousDispatchLedger,
11559
- reason: "prepared ledger persistence failed before terminal input"
11560
- });
12787
+ if (!abortDeferredPreInputBeforeTransport()) {
12788
+ restoreTerminalBridgeDispatchLedger({
12789
+ terminalControl,
12790
+ previousLedger: previousDispatchLedger,
12791
+ reason: "prepared ledger persistence failed before terminal input"
12792
+ });
12793
+ }
11561
12794
  }
11562
12795
  finally {
11563
12796
  rollbackRawAttachBeforeTransport();
@@ -11569,11 +12802,13 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
11569
12802
  }
11570
12803
  catch (error) {
11571
12804
  try {
11572
- restoreTerminalBridgeDispatchLedger({
11573
- terminalControl,
11574
- previousLedger: previousDispatchLedger,
11575
- reason: "prepared state persistence failed before terminal input"
11576
- });
12805
+ if (!abortDeferredPreInputBeforeTransport()) {
12806
+ restoreTerminalBridgeDispatchLedger({
12807
+ terminalControl,
12808
+ previousLedger: previousDispatchLedger,
12809
+ reason: "prepared state persistence failed before terminal input"
12810
+ });
12811
+ }
11577
12812
  }
11578
12813
  finally {
11579
12814
  rollbackRawAttachBeforeTransport();
@@ -11634,11 +12869,13 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
11634
12869
  const errorMessage = error instanceof Error ? error.message : String(error);
11635
12870
  let dispatchLedgerRestored = true;
11636
12871
  try {
11637
- restoreTerminalBridgeDispatchLedger({
11638
- terminalControl,
11639
- previousLedger: previousDispatchLedger,
11640
- reason: "terminal submission aborted before terminal input"
11641
- });
12872
+ if (!abortDeferredPreInputBeforeTransport()) {
12873
+ restoreTerminalBridgeDispatchLedger({
12874
+ terminalControl,
12875
+ previousLedger: previousDispatchLedger,
12876
+ reason: "terminal submission aborted before terminal input"
12877
+ });
12878
+ }
11642
12879
  }
11643
12880
  catch (ledgerError) {
11644
12881
  dispatchLedgerRestored = false;
@@ -11800,7 +13037,8 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
11800
13037
  await terminalBridge.send(executor.kind, terminalControl, terminalPayload, {
11801
13038
  runtime: preSendRuntime,
11802
13039
  requireExactComposerBeforeEnter: observedHandoff !== undefined ||
11803
- verifiedEmptyCodexHandoff !== undefined,
13040
+ verifiedEmptyCodexHandoff !== undefined ||
13041
+ deferredCodexForegroundBinding !== undefined,
11804
13042
  beforeText: observedHandoff
11805
13043
  ? async () => {
11806
13044
  await assertObservedHandoffTransportBoundary({
@@ -11818,7 +13056,22 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
11818
13056
  requireEmptyComposer: true
11819
13057
  });
11820
13058
  }
11821
- : undefined,
13059
+ : deferredCodexForegroundBinding
13060
+ ? async () => {
13061
+ await assertDeferredCodexForegroundBindingBoundary({
13062
+ options,
13063
+ boundary: deferredCodexForegroundBinding,
13064
+ expectedSourceStatus: "transitioning",
13065
+ requireNoDispatch: false,
13066
+ requireEmptyComposer: true
13067
+ });
13068
+ beginDeferredCodexForegroundTransferDispatch({
13069
+ options,
13070
+ boundary: deferredCodexForegroundBinding,
13071
+ at: cliNow().toISOString()
13072
+ });
13073
+ }
13074
+ : undefined,
11822
13075
  beforeEnter: observedHandoff
11823
13076
  ? async () => {
11824
13077
  await assertObservedHandoffTransportBoundary({
@@ -11836,7 +13089,17 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
11836
13089
  requireEmptyComposer: false
11837
13090
  });
11838
13091
  }
11839
- : undefined,
13092
+ : deferredCodexForegroundBinding
13093
+ ? async () => {
13094
+ await assertDeferredCodexForegroundBindingBoundary({
13095
+ options,
13096
+ boundary: deferredCodexForegroundBinding,
13097
+ expectedSourceStatus: "transitioning",
13098
+ requireNoDispatch: false,
13099
+ requireEmptyComposer: false
13100
+ });
13101
+ }
13102
+ : undefined,
11840
13103
  async onTransportStage(event) {
11841
13104
  const stageAt = cliNow().toISOString();
11842
13105
  if (event.stage === "text_injected") {
@@ -11877,6 +13140,14 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
11877
13140
  previous_generation_id: stringValue(previousDispatchLedger?.generation_id) ??
11878
13141
  stringValue(previousDispatchLedger?.message_id)
11879
13142
  });
13143
+ if (deferredCodexForegroundBinding) {
13144
+ advanceDeferredCodexForegroundTransferInputStage({
13145
+ options,
13146
+ boundary: deferredCodexForegroundBinding,
13147
+ stage: event.stage,
13148
+ at: stageAt
13149
+ });
13150
+ }
11880
13151
  if (event.stage === "text_injected" && observedHandoff) {
11881
13152
  await assertObservedHandoffTransportBoundary({
11882
13153
  options,
@@ -11892,6 +13163,15 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
11892
13163
  requireEmptyComposer: false
11893
13164
  });
11894
13165
  }
13166
+ if (event.stage === "text_injected" && deferredCodexForegroundBinding) {
13167
+ await assertDeferredCodexForegroundBindingBoundary({
13168
+ options,
13169
+ boundary: deferredCodexForegroundBinding,
13170
+ expectedSourceStatus: "transitioning",
13171
+ requireNoDispatch: false,
13172
+ requireEmptyComposer: false
13173
+ });
13174
+ }
11895
13175
  try {
11896
13176
  appendEvent(logPath, {
11897
13177
  ts: stageAt,
@@ -11921,6 +13201,11 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
11921
13201
  let boundConversation;
11922
13202
  let bindingError;
11923
13203
  try {
13204
+ if (deferredCodexForegroundBinding &&
13205
+ !allowsSyntheticTerminalAcceptance(options) &&
13206
+ codexRolloutAcceptanceAnchor?.version !== 2) {
13207
+ throw new Error("deferred Codex foreground binding requires a virgin rollout acceptance anchor");
13208
+ }
11924
13209
  boundIdentity = await pollNativeAgentSessionIdentity({
11925
13210
  options,
11926
13211
  executor,
@@ -11928,7 +13213,19 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
11928
13213
  pid: terminalAgentPid,
11929
13214
  expectedSessionId: stringValue(sendTakeover?.terminal_agent_expected_session_id),
11930
13215
  allowedPreMaterializationIdentity,
11931
- allowedAdditionalIdentities
13216
+ allowedAdditionalIdentities,
13217
+ ...(deferredCodexForegroundBinding &&
13218
+ codexRolloutAcceptanceAnchor?.version === 2
13219
+ ? {
13220
+ requiredCodexAcceptance: {
13221
+ anchor: codexRolloutAcceptanceAnchor,
13222
+ requestHash: terminalRequestHash
13223
+ },
13224
+ attempts: Math.max(1, Math.ceil(DEFAULT_TERMINAL_ACCEPTANCE_TIMEOUT_MS /
13225
+ DEFAULT_TERMINAL_ACCEPTANCE_POLL_INTERVAL_MS)),
13226
+ delayMs: DEFAULT_TERMINAL_ACCEPTANCE_POLL_INTERVAL_MS
13227
+ }
13228
+ : {})
11932
13229
  });
11933
13230
  }
11934
13231
  catch (error) {
@@ -11955,20 +13252,30 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
11955
13252
  if (!bindingStoreDir) {
11956
13253
  throw new Error("managed Session Store is unavailable before native identity commit");
11957
13254
  }
11958
- await assertNativeThreadHasExclusiveOwnership({
11959
- options,
11960
- agent: executor.kind,
11961
- currentPid: terminalAgentPid,
11962
- nativeThreadId: boundIdentity.sessionId,
11963
- storeDir: bindingStoreDir,
11964
- terminalControl,
11965
- excludedManagedSessionId: sessionIdForConversation(boundConversation)
11966
- });
11967
- persistManagedSessionNativeIdentity({
11968
- conversation: boundConversation,
11969
- terminalControl,
11970
- identity: boundIdentity
11971
- });
13255
+ if (deferredCodexForegroundBinding) {
13256
+ await commitDeferredCodexForegroundBinding({
13257
+ options,
13258
+ boundary: deferredCodexForegroundBinding,
13259
+ identity: boundIdentity,
13260
+ acceptedAt: cliNow().toISOString()
13261
+ });
13262
+ }
13263
+ else {
13264
+ await assertNativeThreadHasExclusiveOwnership({
13265
+ options,
13266
+ agent: executor.kind,
13267
+ currentPid: terminalAgentPid,
13268
+ nativeThreadId: boundIdentity.sessionId,
13269
+ storeDir: bindingStoreDir,
13270
+ terminalControl,
13271
+ excludedManagedSessionId: sessionIdForConversation(boundConversation)
13272
+ });
13273
+ persistManagedSessionNativeIdentity({
13274
+ conversation: boundConversation,
13275
+ terminalControl,
13276
+ identity: boundIdentity
13277
+ });
13278
+ }
11972
13279
  if (codexRolloutAcceptanceAnchor?.version === 2 &&
11973
13280
  cliEnv().AKK_TEST_EXIT_AFTER_VIRGIN_SESSION_BINDING === "1") {
11974
13281
  cliExit(86);
@@ -11991,11 +13298,69 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
11991
13298
  boundConversation = undefined;
11992
13299
  }
11993
13300
  }
13301
+ if (!boundIdentity && deferredCodexForegroundBinding) {
13302
+ try {
13303
+ const transfer = loadDeferredForegroundTransfer(storeDirFromOptions(options), deferredCodexForegroundBinding.transferId);
13304
+ if (["committed", "resolved"].includes(transfer.status)) {
13305
+ const recoveredTarget = await resolveCommittedDeferredCodexForegroundTransfer({
13306
+ options,
13307
+ boundary: deferredCodexForegroundBinding
13308
+ });
13309
+ const binding = recoveredTarget.binding;
13310
+ if (!binding?.native_thread_id ||
13311
+ !binding.native_process.process_uuid ||
13312
+ !binding.native_process.process_birth ||
13313
+ !isCompleteNativeRollout(binding.native_process.rollout)) {
13314
+ throw new Error("resolved deferred target lacks exact native identity");
13315
+ }
13316
+ const recoveredIdentity = {
13317
+ sessionId: binding.native_thread_id,
13318
+ processUuid: binding.native_process.process_uuid,
13319
+ processBirth: binding.native_process.process_birth,
13320
+ rollout: binding.native_process.rollout,
13321
+ evidence: binding.native_process.evidence
13322
+ };
13323
+ const recoveredConversation = withNativeAgentSessionIdentity(stagedConversation, recoveredIdentity);
13324
+ // A committed transfer proves Session ownership, but it cannot
13325
+ // silently substitute for the Turn's exact staged binding. Only
13326
+ // publish the recovered identity to the normal acceptance path
13327
+ // after the same Turn-vs-Session/native assertion used by the
13328
+ // primary commit path succeeds.
13329
+ assertNativeAgentIdentityForTurn({
13330
+ conversation: recoveredConversation,
13331
+ currentIdentity: recoveredIdentity,
13332
+ operation: "recover deferred foreground binding for"
13333
+ });
13334
+ boundIdentity = recoveredIdentity;
13335
+ boundConversation = recoveredConversation;
13336
+ }
13337
+ }
13338
+ catch (error) {
13339
+ bindingError = error instanceof Error ? error.message : String(error);
13340
+ boundIdentity = undefined;
13341
+ boundConversation = undefined;
13342
+ }
13343
+ }
11994
13344
  if (!boundIdentity) {
11995
13345
  const bindingFailedAt = cliNow().toISOString();
11996
13346
  const bindingReason = bindingError
11997
13347
  ? `AKK dispatched terminal input but could not verify the new native agent session: ${bindingError}`
11998
13348
  : "AKK dispatched terminal input but no exact native agent session appeared within the binding window";
13349
+ if (deferredCodexForegroundBinding) {
13350
+ try {
13351
+ markDeferredCodexForegroundTransferUncertain({
13352
+ options,
13353
+ boundary: deferredCodexForegroundBinding,
13354
+ reason: bindingReason
13355
+ });
13356
+ }
13357
+ catch (error) {
13358
+ runtimeLog("error", "deferred_codex_foreground_uncertain_persist_failed", {
13359
+ transfer_id: deferredCodexForegroundBinding.transferId,
13360
+ error: error instanceof Error ? error.message : String(error)
13361
+ });
13362
+ }
13363
+ }
11999
13364
  const unfencedBase = {
12000
13365
  ...stagedConversation,
12001
13366
  status: "stalled",
@@ -12024,10 +13389,12 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
12024
13389
  error: bindingReason,
12025
13390
  lastProvenStage: "enter_dispatched"
12026
13391
  });
12027
- quarantineManagedSessionBinding({
12028
- conversation: unfencedConversation,
12029
- reason: bindingReason
12030
- });
13392
+ if (!deferredCodexForegroundBinding) {
13393
+ quarantineManagedSessionBinding({
13394
+ conversation: unfencedConversation,
13395
+ reason: bindingReason
13396
+ });
13397
+ }
12031
13398
  saveTerminalBridgeDispatchLedger(terminalControl, {
12032
13399
  ...terminalBindingLedgerFields(unfencedConversation),
12033
13400
  status: "uncertain",
@@ -12249,11 +13616,13 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
12249
13616
  const errorMessage = error.message;
12250
13617
  let dispatchLedgerRestored = true;
12251
13618
  try {
12252
- restoreTerminalBridgeDispatchLedger({
12253
- terminalControl,
12254
- previousLedger: previousDispatchLedger,
12255
- reason: "terminal transport was proved not to have started"
12256
- });
13619
+ if (!abortDeferredPreInputBeforeTransport(abortedAt)) {
13620
+ restoreTerminalBridgeDispatchLedger({
13621
+ terminalControl,
13622
+ previousLedger: previousDispatchLedger,
13623
+ reason: "terminal transport was proved not to have started"
13624
+ });
13625
+ }
12257
13626
  }
12258
13627
  catch (ledgerError) {
12259
13628
  dispatchLedgerRestored = false;
@@ -12388,6 +13757,23 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
12388
13757
  }
12389
13758
  const uncertainAt = cliNow().toISOString();
12390
13759
  const errorMessage = error instanceof Error ? error.message : String(error);
13760
+ if (deferredCodexForegroundBinding) {
13761
+ try {
13762
+ markDeferredCodexForegroundTransferUncertain({
13763
+ options,
13764
+ boundary: deferredCodexForegroundBinding,
13765
+ reason: errorMessage
13766
+ });
13767
+ }
13768
+ catch (transferError) {
13769
+ runtimeLog("error", "deferred_codex_foreground_uncertain_persist_failed", {
13770
+ transfer_id: deferredCodexForegroundBinding.transferId,
13771
+ error: transferError instanceof Error
13772
+ ? transferError.message
13773
+ : String(transferError)
13774
+ });
13775
+ }
13776
+ }
12391
13777
  const failureBase = stagedConversation;
12392
13778
  const stalledFailureBase = {
12393
13779
  ...failureBase,
@@ -13556,7 +14942,7 @@ function assertTurnBindingCurrent(conversation, operation) {
13556
14942
  // the Store writer lease. This makes the freshly materialized Session
13557
14943
  // authoritative even for the first callback/control after an upgrade.
13558
14944
  const manifest = ensureStoreWritable(storeDir);
13559
- if (manifest.writer_protocol !== 3) {
14945
+ if (manifest.writer_protocol < STORE_SESSION_AUTHORITY_PROTOCOL) {
13560
14946
  throw new Error(`cannot ${operation} Turn ${turnIdForConversation(conversation)}: ` +
13561
14947
  "its Store has no protocol-3 Session authority");
13562
14948
  }
@@ -13799,7 +15185,7 @@ function quarantineManagedSessionBinding({ conversation, reason }) {
13799
15185
  expectedRevision: managedSessionRevision(current)
13800
15186
  });
13801
15187
  }
13802
- async function pollNativeAgentSessionIdentity({ options, executor, terminalControl, pid, expectedSessionId, allowedPreMaterializationIdentity, allowedAdditionalIdentities = [], attempts = 40, delayMs = 50 }) {
15188
+ async function pollNativeAgentSessionIdentity({ options, executor, terminalControl, pid, expectedSessionId, allowedPreMaterializationIdentity, allowedAdditionalIdentities = [], requiredCodexAcceptance, attempts = 40, delayMs = 50 }) {
13803
15189
  for (let attempt = 0; attempt < attempts; attempt += 1) {
13804
15190
  const identity = await resolveCurrentNativeAgentSessionIdentity({
13805
15191
  options,
@@ -13814,6 +15200,19 @@ async function pollNativeAgentSessionIdentity({ options, executor, terminalContr
13814
15200
  });
13815
15201
  if (identity) {
13816
15202
  if (!expectedSessionId || identity.sessionId === expectedSessionId) {
15203
+ if (requiredCodexAcceptance) {
15204
+ const acceptance = detectCodexRolloutAcceptance({
15205
+ anchor: requiredCodexAcceptance.anchor,
15206
+ currentIdentity: identity,
15207
+ requestHash: requiredCodexAcceptance.requestHash
15208
+ });
15209
+ if (!acceptance) {
15210
+ if (attempt + 1 < attempts) {
15211
+ await cliSleep(delayMs);
15212
+ }
15213
+ continue;
15214
+ }
15215
+ }
13817
15216
  return identity;
13818
15217
  }
13819
15218
  if (nativeIdentityMatchesCodexPreMaterialization(identity, allowedPreMaterializationIdentity)) {
@@ -13900,7 +15299,7 @@ function managedTurnMatchesResolvedTerminal(conversation, terminalConversation,
13900
15299
  terminalControlsShareIncarnation(storedControl, terminalConversation.terminalControl) &&
13901
15300
  matchesConfiguredWorkspace(conversation.workspace, terminalConversation.terminalControl.currentPath));
13902
15301
  }
13903
- function createManagedTerminalTurn({ options, conversationId, agent, pid, messageBody, terminalControl, previousTurn, managedSession, nativeAgentIdentity }) {
15302
+ function createManagedTerminalTurn({ options, conversationId, agent, pid, messageBody, terminalControl, previousTurn, managedSession, nativeAgentIdentity, deferredForegroundTransferId = undefined }) {
13904
15303
  const workspace = terminalControl.currentPath ?? cliCwd();
13905
15304
  const storeDir = expandHome(options.storeDir ?? options.logDir ?? defaultStoreDir(workspace));
13906
15305
  const executor = previousTurn
@@ -13968,7 +15367,10 @@ function createManagedTerminalTurn({ options, conversationId, agent, pid, messag
13968
15367
  : "raw_terminal_send",
13969
15368
  ...terminalEndpointTakeoverFields(terminalControl),
13970
15369
  needs_bootstrap: false,
13971
- terminal_bridge: true
15370
+ terminal_bridge: true,
15371
+ ...(deferredForegroundTransferId
15372
+ ? { deferred_foreground_transfer_id: deferredForegroundTransferId }
15373
+ : {})
13972
15374
  }
13973
15375
  }, paths);
13974
15376
  const message = createMessage({
@@ -14807,6 +16209,7 @@ async function runTerminalControlCancel({ options, statePath, logPath, agent, te
14807
16209
  throw new Error("terminal control changed while waiting to cancel; refresh status and retry");
14808
16210
  }
14809
16211
  assertManagedTerminalDispatchOwner({
16212
+ storeDir: writerStoreDir,
14810
16213
  conversation: currentConversation,
14811
16214
  terminalControl: currentControl,
14812
16215
  action: "cancel"
@@ -15086,6 +16489,11 @@ async function runClose(options) {
15086
16489
  const releaseStateLock = acquireFileLock(`${statePath}.lock`);
15087
16490
  try {
15088
16491
  const conversation = loadState(statePath);
16492
+ assertConversationHasNoNonterminalDeferredForegroundTransfer({
16493
+ storeDir: closeStoreDir,
16494
+ conversation,
16495
+ action: "close"
16496
+ });
15089
16497
  const currentTakeover = isRecord(conversation.native_session_takeover)
15090
16498
  ? conversation.native_session_takeover
15091
16499
  : undefined;
@@ -15253,6 +16661,30 @@ async function runTerminalDispatchClose({ options, terminalConversation }) {
15253
16661
  if (!ledger || ledger.status === "resolved") {
15254
16662
  throw new Error(`terminal ${terminalControl.target} has no unresolved AKK dispatch fence`);
15255
16663
  }
16664
+ const deferredTransferId = stringValue(ledger.deferred_foreground_transfer_id);
16665
+ if (deferredTransferId) {
16666
+ const ledgerStatePath = path.resolve(required(stringValue(ledger.state_path), "deferred terminal dispatch state path is unavailable"));
16667
+ const ledgerStoreDir = path.resolve(required(stringValue(ledger.store_dir), "deferred terminal dispatch Store is unavailable"));
16668
+ const canonicalLedgerStoreDir = path.resolve(pathsForConversationDir(path.dirname(ledgerStatePath)).storeDir);
16669
+ if (ledgerStoreDir !== path.resolve(storeDir) ||
16670
+ canonicalLedgerStoreDir !== ledgerStoreDir) {
16671
+ throw new Error(`terminal ${terminalControl.target} deferred dispatch belongs to ` +
16672
+ "another or noncanonical Store; generic terminal close cannot resolve it");
16673
+ }
16674
+ const transfer = loadDeferredForegroundTransfer(storeDir, deferredTransferId);
16675
+ if (transfer.state_path === undefined ||
16676
+ path.resolve(transfer.state_path) !== ledgerStatePath ||
16677
+ transfer.terminal_id !== terminalConversation.conversationId ||
16678
+ !terminalControlEvidenceMatches(transfer.terminal_endpoint, terminalControl)) {
16679
+ throw new Error(`terminal ${terminalControl.target} deferred dispatch authority ` +
16680
+ "does not match its exact Store/terminal transfer");
16681
+ }
16682
+ if (!["resolved", "aborted", "abort_resolved"].includes(transfer.status)) {
16683
+ throw new Error(`terminal ${terminalControl.target} dispatch is fenced by deferred ` +
16684
+ `foreground transfer ${transfer.transfer_id} (${transfer.status}); ` +
16685
+ "generic terminal close cannot resolve it");
16686
+ }
16687
+ }
15256
16688
  if (![
15257
16689
  "prepared",
15258
16690
  "text_injected",
@@ -17611,7 +19043,12 @@ function loadTerminalDispatchLedgerOwner(ledger) {
17611
19043
  return undefined;
17612
19044
  }
17613
19045
  }
17614
- function assertManagedTerminalDispatchOwner({ conversation, terminalControl, action }) {
19046
+ function assertManagedTerminalDispatchOwner({ storeDir, conversation, terminalControl, action }) {
19047
+ assertConversationHasNoNonterminalDeferredForegroundTransfer({
19048
+ storeDir,
19049
+ conversation,
19050
+ action
19051
+ });
17615
19052
  assertTurnBindingCurrent(conversation, action);
17616
19053
  const nativeTakeover = isRecord(conversation.native_session_takeover)
17617
19054
  ? conversation.native_session_takeover
@@ -17649,6 +19086,7 @@ function terminalBindingLedgerFields(conversation) {
17649
19086
  const submission = terminalBridgeSubmission(conversation);
17650
19087
  const messageType = stringValue(submission?.message_type);
17651
19088
  const messageBodyHash = stringValue(submission?.message_body_hash);
19089
+ const deferredForegroundTransferId = stringValue(takeover?.deferred_foreground_transfer_id);
17652
19090
  return {
17653
19091
  ...(bindingId ? { binding_id: bindingId } : {}),
17654
19092
  ...(Number.isSafeInteger(generation)
@@ -17658,12 +19096,30 @@ function terminalBindingLedgerFields(conversation) {
17658
19096
  ...(storeDir ? { store_dir: path.resolve(storeDir) } : {}),
17659
19097
  ...(messageType ? { message_type: messageType } : {}),
17660
19098
  ...(messageBodyHash ? { message_body_hash: messageBodyHash } : {}),
19099
+ ...(deferredForegroundTransferId
19100
+ ? { deferred_foreground_transfer_id: deferredForegroundTransferId }
19101
+ : {}),
17661
19102
  executor_kind: executorForConversation(conversation).kind,
17662
19103
  ...(conversation.openclaw_session
17663
19104
  ? { openclaw_session: conversation.openclaw_session }
17664
19105
  : {})
17665
19106
  };
17666
19107
  }
19108
+ function assertConversationHasNoNonterminalDeferredForegroundTransfer({ storeDir, conversation, action }) {
19109
+ const takeover = isRecord(conversation.native_session_takeover)
19110
+ ? conversation.native_session_takeover
19111
+ : undefined;
19112
+ const transferId = stringValue(takeover?.deferred_foreground_transfer_id);
19113
+ if (!transferId) {
19114
+ return;
19115
+ }
19116
+ const transfer = loadDeferredForegroundTransfer(storeDir, transferId);
19117
+ if (!["resolved", "aborted", "abort_resolved"].includes(transfer.status)) {
19118
+ throw new Error(`cannot ${action} Turn ${turnIdForConversation(conversation)} while ` +
19119
+ `deferred foreground transfer ${transfer.transfer_id} is ` +
19120
+ `${transfer.status}; dedicated transfer recovery must finish first`);
19121
+ }
19122
+ }
17667
19123
  function reconcilePreparedTerminalDispatchLedger(terminalControl, ledger) {
17668
19124
  if (terminalDispatchLedgerLooksLifecycle(ledger)) {
17669
19125
  return ledger;
@@ -18011,7 +19467,986 @@ function terminalSubmissionProofRank(status, lastProven) {
18011
19467
  }
18012
19468
  return 0;
18013
19469
  }
19470
+ function deferredCodexBoundaryFromTransfer({ terminal, transfer }) {
19471
+ const liveIncarnation = codexProcessIncarnationForPid(terminal.pid);
19472
+ if (terminal.agent !== "codex" ||
19473
+ transfer.terminal_id !== terminal.conversationId ||
19474
+ transfer.process_pid !== terminal.pid ||
19475
+ transfer.process_uuid !== liveIncarnation.processUuid ||
19476
+ transfer.process_birth !== liveIncarnation.processBirth ||
19477
+ path.resolve(transfer.workspace) !== path.resolve(terminal.terminalControl.currentPath ?? "") ||
19478
+ !terminalControlEvidenceMatches(transfer.terminal_endpoint, terminal.terminalControl)) {
19479
+ throw new Error(`deferred foreground transfer ${transfer.transfer_id} no longer ` +
19480
+ "matches the exact terminal incarnation");
19481
+ }
19482
+ return {
19483
+ terminal,
19484
+ transferId: transfer.transfer_id,
19485
+ targetSessionId: transfer.target_session_id,
19486
+ sourceSessionId: transfer.source_session_id,
19487
+ sourceBoundRevision: transfer.source_expected_revision,
19488
+ sourceBoundBindingToken: transfer.source_binding_token,
19489
+ processUuid: transfer.process_uuid,
19490
+ processBirth: transfer.process_birth,
19491
+ previousDispatchSnapshot: {
19492
+ status: transfer.previous_dispatch_status,
19493
+ fingerprint: transfer.previous_dispatch_fingerprint
19494
+ },
19495
+ sourceReservedRevision: transfer.source_expected_revision + 1,
19496
+ sourceReservedBindingToken: managedSessionBindingToken({
19497
+ session_id: transfer.source_session_id,
19498
+ status: "transitioning",
19499
+ binding: transfer.source_before_binding
19500
+ }),
19501
+ targetPreparedRevision: transfer.target_prepared_revision,
19502
+ targetPreparedBindingToken: transfer.target_prepared_binding_token
19503
+ };
19504
+ }
19505
+ function loadDeferredForegroundTurnAuthority({ storeDir, terminal, transfer }) {
19506
+ const statePath = path.resolve(required(transfer.state_path, "deferred foreground Turn state path is unavailable"));
19507
+ const canonical = pathsForConversationDir(path.dirname(statePath));
19508
+ if (path.resolve(canonical.storeDir) !== path.resolve(storeDir) ||
19509
+ path.resolve(canonical.statePath) !== statePath) {
19510
+ throw new Error(`deferred foreground transfer ${transfer.transfer_id} has a noncanonical ` +
19511
+ "Turn state path");
19512
+ }
19513
+ const conversation = loadState(statePath);
19514
+ const takeover = isRecord(conversation.native_session_takeover)
19515
+ ? conversation.native_session_takeover
19516
+ : undefined;
19517
+ const submission = terminalBridgeSubmission(conversation);
19518
+ const terminalControl = terminalControlFromTakeover(takeover);
19519
+ const anchor = takeover?.codex_rollout_acceptance_anchor;
19520
+ const target = loadManagedSession(storeDir, transfer.target_session_id);
19521
+ const targetBefore = transfer.target_before_binding;
19522
+ const requestText = stringValue(takeover?.terminal_bridge_request_text);
19523
+ const messageBodyHash = stringValue(submission?.message_body_hash);
19524
+ const executorKind = executorForConversation(conversation).kind;
19525
+ const turnNativeThreadId = stringValue(conversation.native_thread_id);
19526
+ const takeoverNativeThreadId = stringValue(takeover?.terminal_agent_session_id);
19527
+ const submissionNativeThreadId = stringValue(submission?.native_thread_id);
19528
+ const turnRollout = takeover?.terminal_agent_rollout;
19529
+ const committedTargetAuthority = Boolean(["committed", "resolved"].includes(transfer.status) &&
19530
+ transfer.target_accepted_binding &&
19531
+ ((target.status === "transitioning" &&
19532
+ target.last_transition_id === transfer.transfer_id &&
19533
+ managedSessionRevision(target) === transfer.target_accepted_revision &&
19534
+ managedSessionBindingToken(target) ===
19535
+ transfer.target_accepted_binding_token) ||
19536
+ (target.status === "bound" &&
19537
+ !target.last_transition_id &&
19538
+ managedSessionRevision(target) ===
19539
+ Number(transfer.target_accepted_revision) + 1 &&
19540
+ managedSessionBindingToken(target) === managedSessionBindingToken({
19541
+ session_id: transfer.target_session_id,
19542
+ status: "bound",
19543
+ binding: transfer.target_accepted_binding
19544
+ }))) &&
19545
+ JSON.stringify(target.binding) ===
19546
+ JSON.stringify(transfer.target_accepted_binding));
19547
+ const precommitTargetAuthority = Boolean(!["committed", "resolved"].includes(transfer.status) &&
19548
+ target.status === "transitioning" &&
19549
+ target.last_transition_id === transfer.transfer_id);
19550
+ const prebindingTurn = turnNativeThreadId === undefined &&
19551
+ takeoverNativeThreadId === undefined &&
19552
+ submissionNativeThreadId === undefined &&
19553
+ turnRollout === undefined;
19554
+ let acceptedTurn = false;
19555
+ if (turnNativeThreadId &&
19556
+ takeoverNativeThreadId === turnNativeThreadId &&
19557
+ submissionNativeThreadId === turnNativeThreadId &&
19558
+ isCompleteNativeRollout(turnRollout) &&
19559
+ String(submission?.status) === "agent_accepted") {
19560
+ try {
19561
+ validateTerminalSubmissionAcceptanceEvidence(submission?.acceptance_evidence, {
19562
+ source: "codex_rollout",
19563
+ nativeThreadId: turnNativeThreadId,
19564
+ requestHash: transfer.request_hash
19565
+ });
19566
+ acceptedTurn = true;
19567
+ }
19568
+ catch {
19569
+ acceptedTurn = false;
19570
+ }
19571
+ }
19572
+ if (!takeover ||
19573
+ !submission ||
19574
+ !terminalControl ||
19575
+ !targetBefore ||
19576
+ !terminalControlsShareIncarnation(terminalControl, terminal.terminalControl) ||
19577
+ !terminalControlEvidenceMatches(takeover.terminal_endpoint ?? terminalControl, terminal.terminalControl) ||
19578
+ executorKind !== "codex" ||
19579
+ conversation.conversation_id !== transfer.turn_id ||
19580
+ sessionIdForConversation(conversation) !== transfer.target_session_id ||
19581
+ turnIdForConversation(conversation) !== transfer.turn_id ||
19582
+ path.resolve(required(stringValue(conversation.state_path), "deferred foreground Turn state path is missing")) !== statePath ||
19583
+ path.resolve(pathsForConversation(conversation.conversation_id, storeDir).statePath) !== statePath ||
19584
+ path.resolve(required(managedSessionStoreDirForConversation(conversation), "deferred foreground Turn Store is missing")) !== path.resolve(storeDir) ||
19585
+ stringValue(takeover.deferred_foreground_transfer_id) !==
19586
+ transfer.transfer_id ||
19587
+ stringValue(takeover.terminal_bridge_message_id) !== transfer.message_id ||
19588
+ stringValue(submission.session_id) !== transfer.target_session_id ||
19589
+ stringValue(submission.turn_id) !== transfer.turn_id ||
19590
+ stringValue(submission.message_id) !== transfer.message_id ||
19591
+ stringValue(submission.message_type) !== "task" ||
19592
+ stringValue(submission.executor_kind) !== "codex" ||
19593
+ !messageBodyHash ||
19594
+ !/^[0-9a-f]{64}$/u.test(messageBodyHash) ||
19595
+ createHash("sha256").update(String(conversation.user_request)).digest("hex") !==
19596
+ messageBodyHash ||
19597
+ terminalBridgeRequestFingerprint(terminalSubmissionPayload(String(conversation.user_request))) !== transfer.request_hash ||
19598
+ stringValue(takeover.terminal_bridge_request_hash) !==
19599
+ transfer.request_hash ||
19600
+ stringValue(submission.request_hash) !== transfer.request_hash ||
19601
+ !requestText ||
19602
+ terminalBridgeRequestFingerprint(requestText) !== transfer.request_hash ||
19603
+ path.resolve(required(stringValue(submission.store_dir), "deferred foreground submission Store is missing")) !== path.resolve(storeDir) ||
19604
+ stringValue(conversation.terminal_binding_id) !== targetBefore.binding_id ||
19605
+ Number(conversation.terminal_binding_generation) !==
19606
+ targetBefore.generation ||
19607
+ stringValue(takeover.terminal_binding_id) !== targetBefore.binding_id ||
19608
+ Number(takeover.terminal_binding_generation) !== targetBefore.generation ||
19609
+ stringValue(submission.binding_id) !== targetBefore.binding_id ||
19610
+ Number(submission.binding_generation) !== targetBefore.generation ||
19611
+ (!prebindingTurn && !acceptedTurn) ||
19612
+ stringValue(takeover.terminal_agent_expected_session_id) !== undefined ||
19613
+ Number(takeover.terminal_agent_pid) !== transfer.process_pid ||
19614
+ stringValue(takeover.terminal_agent_process_uuid) !==
19615
+ transfer.process_uuid ||
19616
+ stringValue(takeover.terminal_agent_process_birth) !==
19617
+ transfer.process_birth ||
19618
+ target.session_id !== transfer.target_session_id ||
19619
+ (!precommitTargetAuthority && !committedTargetAuthority) ||
19620
+ target.lineage.transition_id !== transfer.transfer_id ||
19621
+ target.lineage.previous_session_id !== transfer.source_session_id ||
19622
+ !target.binding ||
19623
+ target.binding.binding_id !== targetBefore.binding_id ||
19624
+ target.binding.generation !== targetBefore.generation ||
19625
+ target.binding.terminal_id !== targetBefore.terminal_id ||
19626
+ !terminalControlsShareIncarnation(target.binding.terminal_control, targetBefore.terminal_control) ||
19627
+ target.binding.native_process.pid !== transfer.process_pid ||
19628
+ target.binding.native_process.process_uuid !== transfer.process_uuid ||
19629
+ target.binding.native_process.process_birth !== transfer.process_birth ||
19630
+ !isRecord(anchor) ||
19631
+ anchor.schema !== "agent-knock-knock/codex-rollout-acceptance-anchor" ||
19632
+ anchor.version !== 2 ||
19633
+ anchor.mode !== "pre_materialization" ||
19634
+ anchor.native_thread_binding !== "post_submission" ||
19635
+ anchor.file_existed !== false ||
19636
+ anchor.offset_bytes !== 0 ||
19637
+ anchor.expected_empty_native_session !== true ||
19638
+ anchor.native_thread_id !== undefined ||
19639
+ anchor.rollout !== undefined ||
19640
+ anchor.process_uuid !== transfer.process_uuid ||
19641
+ anchor.process_birth !== transfer.process_birth ||
19642
+ typeof anchor.anchor_fingerprint !== "string" ||
19643
+ !/^[0-9a-f]{64}$/u.test(anchor.anchor_fingerprint)) {
19644
+ throw new Error(`deferred foreground transfer ${transfer.transfer_id} Turn authority ` +
19645
+ "does not match its exact request/terminal/process fence");
19646
+ }
19647
+ return {
19648
+ conversation,
19649
+ statePath,
19650
+ logPath: canonical.logPath,
19651
+ takeover,
19652
+ submission,
19653
+ anchor: anchor
19654
+ };
19655
+ }
19656
+ function assertDeferredForegroundLedgerAuthority({ storeDir, terminal, transfer, ledger, statePath, expectedMessageBodyHash }) {
19657
+ const targetBefore = transfer.target_before_binding;
19658
+ const ledgerMessageBodyHash = stringValue(ledger.message_body_hash);
19659
+ let exactCommittedAcceptance = false;
19660
+ if (["committed", "resolved"].includes(transfer.status) &&
19661
+ ledger.status === "agent_accepted" &&
19662
+ stringValue(ledger.native_thread_id) === transfer.target_native_thread_id &&
19663
+ ledger.dispatcher_pid === null &&
19664
+ stringValue(ledger.agent_accepted_at) === transfer.agent_accepted_at &&
19665
+ transfer.target_native_thread_id) {
19666
+ try {
19667
+ validateTerminalSubmissionAcceptanceEvidence(ledger.acceptance_evidence, {
19668
+ source: "codex_rollout",
19669
+ nativeThreadId: transfer.target_native_thread_id,
19670
+ requestHash: transfer.request_hash
19671
+ });
19672
+ exactCommittedAcceptance = true;
19673
+ }
19674
+ catch {
19675
+ exactCommittedAcceptance = false;
19676
+ }
19677
+ }
19678
+ if (!targetBefore ||
19679
+ terminalDispatchLedgerLooksLifecycle(ledger) ||
19680
+ !terminalDispatchRecordMatchesControl(ledger, terminal.terminalControl) ||
19681
+ stringValue(ledger.deferred_foreground_transfer_id) !==
19682
+ transfer.transfer_id ||
19683
+ stringValue(ledger.generation_id) !== transfer.message_id ||
19684
+ stringValue(ledger.conversation_id) !== transfer.turn_id ||
19685
+ stringValue(ledger.session_id) !== transfer.target_session_id ||
19686
+ stringValue(ledger.turn_id) !== transfer.turn_id ||
19687
+ stringValue(ledger.message_id) !== transfer.message_id ||
19688
+ stringValue(ledger.message_type) !== "task" ||
19689
+ stringValue(ledger.executor_kind) !== "codex" ||
19690
+ !ledgerMessageBodyHash ||
19691
+ !/^[0-9a-f]{64}$/u.test(ledgerMessageBodyHash) ||
19692
+ (expectedMessageBodyHash !== undefined &&
19693
+ ledgerMessageBodyHash !== expectedMessageBodyHash) ||
19694
+ stringValue(ledger.request_hash) !== transfer.request_hash ||
19695
+ stringValue(ledger.binding_id) !== targetBefore.binding_id ||
19696
+ Number(ledger.binding_generation) !== targetBefore.generation ||
19697
+ (exactCommittedAcceptance
19698
+ ? stringValue(ledger.native_thread_id) !==
19699
+ transfer.target_native_thread_id
19700
+ : stringValue(ledger.native_thread_id) !== undefined) ||
19701
+ path.resolve(required(ledger.state_path, "dispatch state path is missing")) !==
19702
+ statePath ||
19703
+ path.resolve(required(ledger.store_dir, "dispatch Store is missing")) !==
19704
+ path.resolve(storeDir) ||
19705
+ (ledger.status !== "resolved" &&
19706
+ !exactCommittedAcceptance &&
19707
+ Number(ledger.dispatcher_pid) !== transfer.dispatcher_pid)) {
19708
+ throw new Error(`deferred foreground transfer ${transfer.transfer_id} dispatch ledger ` +
19709
+ "does not match its exact Turn authority");
19710
+ }
19711
+ }
19712
+ function assertDeferredForegroundResolvedZeroInputLedger({ storeDir, terminal, transfer, ledger, statePath }) {
19713
+ assertDeferredForegroundLedgerAuthority({
19714
+ storeDir,
19715
+ terminal,
19716
+ transfer,
19717
+ ledger,
19718
+ statePath
19719
+ });
19720
+ const abortedAt = stringValue(ledger.aborted_at);
19721
+ const resolvedAt = stringValue(ledger.resolved_at);
19722
+ const forbiddenEvidence = [
19723
+ "dispatch_started_at",
19724
+ "text_injected_at",
19725
+ "enter_dispatched_at",
19726
+ "submitted_at",
19727
+ "agent_accepted_at",
19728
+ "not_accepted_at",
19729
+ "uncertain_at",
19730
+ "acceptance_evidence"
19731
+ ];
19732
+ if (ledger.status !== "resolved" ||
19733
+ ledger.safe_to_retry !== true ||
19734
+ !abortedAt ||
19735
+ !resolvedAt ||
19736
+ !Number.isFinite(Date.parse(abortedAt)) ||
19737
+ !Number.isFinite(Date.parse(resolvedAt)) ||
19738
+ Date.parse(resolvedAt) < Date.parse(abortedAt) ||
19739
+ forbiddenEvidence.some((field) => ledger[field] !== undefined)) {
19740
+ throw new Error(`deferred foreground transfer ${transfer.transfer_id} resolved ledger ` +
19741
+ "does not prove a zero-input abort");
19742
+ }
19743
+ // Validate the whole history for malformed/duplicate identities, then
19744
+ // require the append-only copy of this exact generation. The top-level
19745
+ // resolved record alone is insufficient because it can later be replaced.
19746
+ terminalLedgerReceiptHistory(ledger);
19747
+ const rawHistory = ledger.terminal_submission_receipts;
19748
+ if (!Array.isArray(rawHistory)) {
19749
+ throw new Error(`deferred foreground transfer ${transfer.transfer_id} resolved ledger ` +
19750
+ "has no append-only abort receipt");
19751
+ }
19752
+ const ownReceipts = rawHistory.filter((receipt) => isRecord(receipt) &&
19753
+ stringValue(receipt.message_id) === transfer.message_id);
19754
+ if (ownReceipts.length !== 1) {
19755
+ throw new Error(`deferred foreground transfer ${transfer.transfer_id} resolved ledger ` +
19756
+ "does not have one exact append-only abort receipt");
19757
+ }
19758
+ const ownReceipt = ownReceipts[0];
19759
+ assertDeferredForegroundLedgerAuthority({
19760
+ storeDir,
19761
+ terminal,
19762
+ transfer,
19763
+ // A receipt uses `aborted`, while the same exact owner fields at the
19764
+ // top-level use `resolved`. Validate its authority in that same domain.
19765
+ ledger: { ...ownReceipt, status: "resolved" },
19766
+ statePath
19767
+ });
19768
+ if (ownReceipt.status !== "aborted" ||
19769
+ ownReceipt.safe_to_retry !== true ||
19770
+ stringValue(ownReceipt.aborted_at) !== abortedAt ||
19771
+ stringValue(ownReceipt.resolved_at) !== resolvedAt ||
19772
+ forbiddenEvidence.some((field) => ownReceipt[field] !== undefined)) {
19773
+ throw new Error(`deferred foreground transfer ${transfer.transfer_id} append-only ` +
19774
+ "receipt does not prove a zero-input abort");
19775
+ }
19776
+ }
19777
+ function deferredCodexDurableInputNotStartedAt(transfer) {
19778
+ if (transfer.status !== "dispatch_started" ||
19779
+ transfer.input_stage !== "dispatch_started" ||
19780
+ !transfer.state_path) {
19781
+ return undefined;
19782
+ }
19783
+ const conversation = loadState(path.resolve(transfer.state_path));
19784
+ const submission = terminalBridgeSubmission(conversation);
19785
+ const abortedAt = stringValue(submission?.aborted_at);
19786
+ if (submission?.status !== "aborted" ||
19787
+ submission.safe_to_retry !== true ||
19788
+ !abortedAt ||
19789
+ !Number.isFinite(Date.parse(abortedAt)) ||
19790
+ stringValue(submission.text_injected_at) !== undefined ||
19791
+ stringValue(submission.enter_dispatched_at) !== undefined ||
19792
+ stringValue(submission.agent_accepted_at) !== undefined) {
19793
+ return undefined;
19794
+ }
19795
+ return abortedAt;
19796
+ }
19797
+ function abortPreparedDeferredForegroundTurn({ storeDir, terminal, transfer, boundary, stateLockHeld = false, terminalInputNotStartedAt }) {
19798
+ const dispatchIntentWasProvedNotStarted = transfer.status === "dispatch_started" &&
19799
+ transfer.input_stage === "dispatch_started" &&
19800
+ terminalInputNotStartedAt !== undefined &&
19801
+ Number.isFinite(Date.parse(terminalInputNotStartedAt));
19802
+ if (terminalInputNotStartedAt !== undefined &&
19803
+ !dispatchIntentWasProvedNotStarted) {
19804
+ throw new Error(`deferred foreground transfer ${transfer.transfer_id} cannot attach ` +
19805
+ "terminal-input-not-started proof to its current stage");
19806
+ }
19807
+ const observedLedger = loadTerminalBridgeDispatchLedger(terminal.terminalControl);
19808
+ // Before this transfer publishes its own ordinary dispatch ledger, the
19809
+ // terminal may still carry the exact resolved history that was snapshot into
19810
+ // the immutable transfer receipt. Treat only that exact fingerprint (or the
19811
+ // exact persisted `none`) as no new dispatch; every other change must prove
19812
+ // this transfer's own ledger below or fail closed.
19813
+ const previousDispatchStillCurrent = deferredCodexPreviousDispatchSnapshotMatches({
19814
+ transfer,
19815
+ terminalControl: terminal.terminalControl,
19816
+ ledger: observedLedger
19817
+ });
19818
+ if (!previousDispatchStillCurrent && !observedLedger) {
19819
+ throw new Error(`deferred foreground transfer ${transfer.transfer_id} previous dispatch ` +
19820
+ "snapshot disappeared before its own ledger was published");
19821
+ }
19822
+ const ledger = previousDispatchStillCurrent
19823
+ ? undefined
19824
+ : observedLedger;
19825
+ if (!transfer.state_path) {
19826
+ if (ledger) {
19827
+ throw new Error(`deferred foreground transfer ${transfer.transfer_id} has a dispatch ` +
19828
+ "before its Turn authority was prepared");
19829
+ }
19830
+ abortDeferredCodexForegroundTransferBeforeInput({
19831
+ options: { storeDir },
19832
+ boundary,
19833
+ reason: "recovered before target Turn preparation"
19834
+ });
19835
+ return;
19836
+ }
19837
+ const statePath = path.resolve(transfer.state_path);
19838
+ const canonical = pathsForConversationDir(path.dirname(statePath));
19839
+ if (path.resolve(canonical.storeDir) !== path.resolve(storeDir) ||
19840
+ path.resolve(canonical.statePath) !== statePath) {
19841
+ throw new Error(`deferred foreground transfer ${transfer.transfer_id} has a noncanonical ` +
19842
+ "Turn state path");
19843
+ }
19844
+ if (!fs.existsSync(statePath)) {
19845
+ if (dispatchIntentWasProvedNotStarted) {
19846
+ throw new Error(`deferred foreground transfer ${transfer.transfer_id} lost its Turn ` +
19847
+ "before terminal-input-not-started proof became durable");
19848
+ }
19849
+ if (ledger) {
19850
+ // runTerminalControlSend publishes the exact prepared terminal ledger
19851
+ // before its Turn state. A crash in that narrow window still proves
19852
+ // zero input, provided every immutable ledger field matches this
19853
+ // transfer. Resolve that owner before aborting/restoring the Sessions.
19854
+ assertDeferredForegroundLedgerAuthority({
19855
+ storeDir,
19856
+ terminal,
19857
+ transfer,
19858
+ ledger,
19859
+ statePath
19860
+ });
19861
+ if (!["prepared", "resolved"].includes(String(ledger.status))) {
19862
+ throw new Error(`deferred foreground transfer ${transfer.transfer_id} has ` +
19863
+ `${String(ledger.status)} transport evidence but no exact Turn state`);
19864
+ }
19865
+ if (ledger.status === "prepared") {
19866
+ const abortedAt = cliNow().toISOString();
19867
+ saveTerminalBridgeDispatchLedger(terminal.terminalControl, {
19868
+ ...ledger,
19869
+ status: "resolved",
19870
+ aborted_at: abortedAt,
19871
+ safe_to_retry: true,
19872
+ resolved_at: abortedAt,
19873
+ dispatcher_pid: null,
19874
+ reason: "deferred dispatcher exited after ledger prepare and before Turn persistence"
19875
+ });
19876
+ }
19877
+ else {
19878
+ assertDeferredForegroundResolvedZeroInputLedger({
19879
+ storeDir,
19880
+ terminal,
19881
+ transfer,
19882
+ ledger,
19883
+ statePath
19884
+ });
19885
+ }
19886
+ if (cliEnv().AKK_TEST_EXIT_AFTER_DEFERRED_LEDGER_ABORT_WITHOUT_STATE === "1") {
19887
+ // The zero-input top-level and append-only ledger receipts are both
19888
+ // durable, while the transfer still fences both Sessions. A second
19889
+ // writer must accept this exact resolved form and finish Store-only
19890
+ // rollback without creating or replaying a Turn.
19891
+ cliExit(86);
19892
+ }
19893
+ }
19894
+ abortDeferredCodexForegroundTransferBeforeInput({
19895
+ options: { storeDir },
19896
+ boundary,
19897
+ reason: ledger
19898
+ ? "recovered exact prepared ledger before target Turn state became durable"
19899
+ : "recovered before target Turn state became durable"
19900
+ });
19901
+ return;
19902
+ }
19903
+ const releaseStateLock = stateLockHeld
19904
+ ? undefined
19905
+ : acquireFileLock(`${statePath}.lock`);
19906
+ try {
19907
+ const authority = loadDeferredForegroundTurnAuthority({
19908
+ storeDir,
19909
+ terminal,
19910
+ transfer
19911
+ });
19912
+ const submissionStatus = String(authority.submission.status);
19913
+ let durableNotStartedAt = terminalInputNotStartedAt;
19914
+ if (!ledger && submissionStatus !== "aborted") {
19915
+ throw new Error(`deferred foreground transfer ${transfer.transfer_id} has prepared Turn ` +
19916
+ "state without its exact terminal ledger");
19917
+ }
19918
+ if (ledger) {
19919
+ assertDeferredForegroundLedgerAuthority({
19920
+ storeDir,
19921
+ terminal,
19922
+ transfer,
19923
+ ledger,
19924
+ statePath: authority.statePath,
19925
+ expectedMessageBodyHash: stringValue(authority.submission.message_body_hash)
19926
+ });
19927
+ if (!["prepared", "resolved"].includes(String(ledger.status))) {
19928
+ throw new Error(`deferred foreground transfer ${transfer.transfer_id} has ` +
19929
+ `${String(ledger.status)} transport evidence and cannot be aborted`);
19930
+ }
19931
+ }
19932
+ if (dispatchIntentWasProvedNotStarted && submissionStatus === "aborted") {
19933
+ const existingAbortedAt = stringValue(authority.submission.aborted_at);
19934
+ if (authority.submission.safe_to_retry !== true ||
19935
+ !existingAbortedAt ||
19936
+ !Number.isFinite(Date.parse(existingAbortedAt)) ||
19937
+ stringValue(authority.submission.text_injected_at) !== undefined ||
19938
+ stringValue(authority.submission.enter_dispatched_at) !== undefined ||
19939
+ stringValue(authority.submission.agent_accepted_at) !== undefined ||
19940
+ (durableNotStartedAt !== undefined &&
19941
+ durableNotStartedAt !== existingAbortedAt)) {
19942
+ throw new Error(`deferred foreground transfer ${transfer.transfer_id} has ` +
19943
+ "inconsistent terminal-input-not-started Turn proof");
19944
+ }
19945
+ durableNotStartedAt = existingAbortedAt;
19946
+ }
19947
+ if (submissionStatus === "prepared") {
19948
+ const abortedAt = durableNotStartedAt ?? cliNow().toISOString();
19949
+ const aborted = withTerminalBridgeSubmission({
19950
+ conversation: {
19951
+ ...authority.conversation,
19952
+ status: "failed",
19953
+ failed_at: abortedAt,
19954
+ failure_reason: "dispatcher exited before the deferred terminal-input boundary",
19955
+ updated_at: abortedAt
19956
+ },
19957
+ messageId: transfer.message_id,
19958
+ messageType: "task",
19959
+ requestText: String(authority.takeover.terminal_bridge_request_text ?? ""),
19960
+ status: "aborted",
19961
+ preparedAt: stringValue(authority.submission.prepared_at) ?? abortedAt,
19962
+ abortedAt,
19963
+ error: "dispatcher exited before terminal input",
19964
+ safeToRetry: true
19965
+ });
19966
+ saveState(authority.statePath, aborted);
19967
+ }
19968
+ else if (submissionStatus !== "aborted") {
19969
+ throw new Error(`deferred foreground transfer ${transfer.transfer_id} Turn has ` +
19970
+ `${submissionStatus} input evidence and cannot be aborted`);
19971
+ }
19972
+ if (dispatchIntentWasProvedNotStarted &&
19973
+ ledger?.status === "resolved" &&
19974
+ (ledger.safe_to_retry !== true ||
19975
+ stringValue(ledger.aborted_at) !== durableNotStartedAt ||
19976
+ stringValue(ledger.text_injected_at) !== undefined ||
19977
+ stringValue(ledger.enter_dispatched_at) !== undefined ||
19978
+ stringValue(ledger.agent_accepted_at) !== undefined)) {
19979
+ throw new Error(`deferred foreground transfer ${transfer.transfer_id} has ` +
19980
+ "inconsistent terminal-input-not-started ledger proof");
19981
+ }
19982
+ if (ledger && ledger.status !== "resolved") {
19983
+ const abortedAt = durableNotStartedAt ?? cliNow().toISOString();
19984
+ saveTerminalBridgeDispatchLedger(terminal.terminalControl, {
19985
+ ...ledger,
19986
+ status: "resolved",
19987
+ aborted_at: abortedAt,
19988
+ safe_to_retry: true,
19989
+ resolved_at: abortedAt,
19990
+ dispatcher_pid: null,
19991
+ reason: "deferred dispatcher exited before the durable terminal-input boundary"
19992
+ });
19993
+ }
19994
+ }
19995
+ finally {
19996
+ releaseStateLock?.();
19997
+ }
19998
+ if (cliEnv().AKK_TEST_EXIT_AFTER_DEFERRED_PREINPUT_ABORT_RECEIPTS === "1") {
19999
+ // The exact Turn and append-only submission receipt are both terminal,
20000
+ // while the transfer and Sessions are intentionally still fenced. The
20001
+ // next writer must finish only the Store rollback and never send input.
20002
+ cliExit(86);
20003
+ }
20004
+ abortDeferredCodexForegroundTransferBeforeInput({
20005
+ options: { storeDir },
20006
+ boundary,
20007
+ reason: "recovered exact prepared Turn with zero terminal input",
20008
+ terminalInputNotStartedAt
20009
+ });
20010
+ }
20011
+ async function recoverAcceptedDeferredForegroundDispatch({ options, storeDir, terminal, transfer, boundary }) {
20012
+ const statePath = path.resolve(required(transfer.state_path, "deferred foreground Turn state path is unavailable"));
20013
+ const releaseStateLock = acquireFileLock(`${statePath}.lock`);
20014
+ try {
20015
+ const authority = loadDeferredForegroundTurnAuthority({
20016
+ storeDir,
20017
+ terminal,
20018
+ transfer
20019
+ });
20020
+ const ledger = loadTerminalBridgeDispatchLedger(terminal.terminalControl);
20021
+ if (!ledger) {
20022
+ throw new Error(`deferred foreground transfer ${transfer.transfer_id} has no exact ` +
20023
+ "terminal dispatch ledger for acceptance recovery");
20024
+ }
20025
+ assertDeferredForegroundLedgerAuthority({
20026
+ storeDir,
20027
+ terminal,
20028
+ transfer,
20029
+ ledger,
20030
+ statePath: authority.statePath,
20031
+ expectedMessageBodyHash: stringValue(authority.submission.message_body_hash)
20032
+ });
20033
+ if (![
20034
+ "prepared",
20035
+ "text_injected",
20036
+ "enter_dispatched",
20037
+ "uncertain",
20038
+ "agent_accepted"
20039
+ ].includes(String(ledger.status))) {
20040
+ throw new Error(`deferred foreground transfer ${transfer.transfer_id} has invalid ` +
20041
+ `${String(ledger.status)} dispatch evidence for acceptance recovery`);
20042
+ }
20043
+ const identity = await resolveCurrentNativeAgentSessionIdentity({
20044
+ options,
20045
+ agent: "codex",
20046
+ pid: transfer.process_pid,
20047
+ cwd: terminal.terminalControl.currentPath
20048
+ });
20049
+ if (!identity) {
20050
+ return false;
20051
+ }
20052
+ const acceptance = detectCodexRolloutAcceptance({
20053
+ anchor: authority.anchor,
20054
+ currentIdentity: identity,
20055
+ requestHash: transfer.request_hash
20056
+ });
20057
+ if (!acceptance) {
20058
+ return false;
20059
+ }
20060
+ if (ledger.status === "agent_accepted") {
20061
+ validateTerminalSubmissionAcceptanceEvidence(ledger.acceptance_evidence, {
20062
+ source: "codex_rollout",
20063
+ nativeThreadId: identity.sessionId,
20064
+ requestHash: transfer.request_hash
20065
+ });
20066
+ }
20067
+ const acceptedAt = cliNow().toISOString();
20068
+ const boundConversation = withNativeAgentSessionIdentity(authority.conversation, identity);
20069
+ const acceptedConversation = withTerminalBridgeSubmission({
20070
+ conversation: boundConversation,
20071
+ messageId: transfer.message_id,
20072
+ messageType: "task",
20073
+ requestText: String(authority.takeover.terminal_bridge_request_text ?? ""),
20074
+ status: "agent_accepted",
20075
+ preparedAt: stringValue(authority.submission.prepared_at) ?? transfer.prepared_at,
20076
+ textInjectedAt: stringValue(authority.submission.text_injected_at) ??
20077
+ transfer.text_injected_at ?? transfer.dispatch_started_at,
20078
+ enterDispatchedAt: stringValue(authority.submission.enter_dispatched_at) ??
20079
+ transfer.enter_dispatched_at ?? transfer.dispatch_started_at,
20080
+ agentAcceptedAt: acceptedAt,
20081
+ acceptanceEvidence: acceptance
20082
+ });
20083
+ let acceptedStatePersisted = false;
20084
+ // Persist the strongest exact request receipt before mutating either
20085
+ // Session. A crash after the transfer resolves can then be recovered from
20086
+ // this agent_accepted Turn without replaying terminal input.
20087
+ saveState(authority.statePath, acceptedConversation);
20088
+ acceptedStatePersisted = true;
20089
+ if (cliEnv().AKK_TEST_EXIT_AFTER_DEFERRED_ACCEPTED_TURN === "1") {
20090
+ cliExit(86);
20091
+ }
20092
+ let transferCommitted = false;
20093
+ try {
20094
+ await commitDeferredCodexForegroundBinding({
20095
+ options,
20096
+ boundary,
20097
+ identity,
20098
+ acceptedAt
20099
+ });
20100
+ transferCommitted = true;
20101
+ assertNativeAgentIdentityForTurn({
20102
+ conversation: acceptedConversation,
20103
+ currentIdentity: identity,
20104
+ operation: "recover deferred foreground binding for"
20105
+ });
20106
+ saveTerminalBridgeDispatchLedger(terminal.terminalControl, {
20107
+ ...ledger,
20108
+ ...terminalBindingLedgerFields(acceptedConversation),
20109
+ status: "agent_accepted",
20110
+ generation_id: transfer.message_id,
20111
+ conversation_id: acceptedConversation.conversation_id,
20112
+ session_id: transfer.target_session_id,
20113
+ turn_id: transfer.turn_id,
20114
+ message_id: transfer.message_id,
20115
+ message_type: "task",
20116
+ request_hash: transfer.request_hash,
20117
+ prepared_at: transfer.prepared_at,
20118
+ text_injected_at: transfer.text_injected_at ?? transfer.dispatch_started_at,
20119
+ enter_dispatched_at: transfer.enter_dispatched_at ?? transfer.dispatch_started_at,
20120
+ agent_accepted_at: acceptedAt,
20121
+ acceptance_evidence: acceptance,
20122
+ dispatcher_pid: null,
20123
+ state_path: authority.statePath,
20124
+ event_log_path: authority.logPath,
20125
+ callback_expected: Boolean(acceptedConversation.gateway_method),
20126
+ reason: "recovered exact deferred foreground request acceptance"
20127
+ });
20128
+ return true;
20129
+ }
20130
+ catch (error) {
20131
+ if (transferCommitted && !acceptedStatePersisted) {
20132
+ const uncertainAt = cliNow().toISOString();
20133
+ const reason = "deferred Session transfer resolved but exact Turn identity " +
20134
+ `bookkeeping failed: ${error instanceof Error ? error.message : String(error)}`;
20135
+ const uncertainConversation = withTerminalBridgeSubmission({
20136
+ conversation: {
20137
+ ...authority.conversation,
20138
+ status: "stalled",
20139
+ stalled_at: uncertainAt,
20140
+ stalled_reason: reason,
20141
+ updated_at: uncertainAt
20142
+ },
20143
+ messageId: transfer.message_id,
20144
+ messageType: "task",
20145
+ requestText: String(authority.takeover.terminal_bridge_request_text ?? ""),
20146
+ status: "uncertain",
20147
+ preparedAt: stringValue(authority.submission.prepared_at) ?? transfer.prepared_at,
20148
+ textInjectedAt: stringValue(authority.submission.text_injected_at) ??
20149
+ transfer.text_injected_at ?? transfer.dispatch_started_at,
20150
+ enterDispatchedAt: stringValue(authority.submission.enter_dispatched_at) ??
20151
+ transfer.enter_dispatched_at ?? transfer.dispatch_started_at,
20152
+ uncertainAt,
20153
+ error: reason,
20154
+ lastProvenStage: "enter_dispatched",
20155
+ safeToRetry: false
20156
+ });
20157
+ saveState(authority.statePath, uncertainConversation);
20158
+ saveTerminalBridgeDispatchLedger(terminal.terminalControl, {
20159
+ ...ledger,
20160
+ ...terminalBindingLedgerFields(uncertainConversation),
20161
+ status: "uncertain",
20162
+ uncertain_at: uncertainAt,
20163
+ dispatcher_pid: null,
20164
+ callback_expected: false,
20165
+ reason
20166
+ });
20167
+ }
20168
+ else if (transferCommitted) {
20169
+ // Do not downgrade a durable exact acceptance to the older pre-commit
20170
+ // authority merely because ledger bookkeeping failed. The accepted
20171
+ // receipt remains the monotonic source for later ledger reconciliation,
20172
+ // while the Turn itself is stalled so callback/monitor work cannot
20173
+ // proceed past a failed exact binding assertion.
20174
+ const stalledAt = cliNow().toISOString();
20175
+ const reason = "deferred transfer resolved, but exact accepted Turn bookkeeping " +
20176
+ `failed: ${error instanceof Error ? error.message : String(error)}`;
20177
+ saveState(authority.statePath, {
20178
+ ...acceptedConversation,
20179
+ status: "stalled",
20180
+ stalled_at: stalledAt,
20181
+ stalled_reason: reason,
20182
+ updated_at: stalledAt
20183
+ });
20184
+ try {
20185
+ saveTerminalBridgeDispatchLedger(terminal.terminalControl, {
20186
+ ...ledger,
20187
+ ...terminalBindingLedgerFields(acceptedConversation),
20188
+ status: "uncertain",
20189
+ uncertain_at: stalledAt,
20190
+ last_proven_stage: "agent_accepted",
20191
+ acceptance_evidence: acceptance,
20192
+ dispatcher_pid: null,
20193
+ callback_expected: false,
20194
+ reason
20195
+ });
20196
+ }
20197
+ catch {
20198
+ // The exact accepted Turn above remains authoritative even when the
20199
+ // ledger itself is the bookkeeping failure being reported.
20200
+ }
20201
+ runtimeLog("error", "deferred_acceptance_ledger_bookkeeping_failed", {
20202
+ transfer_id: transfer.transfer_id,
20203
+ turn_id: transfer.turn_id,
20204
+ state_path: authority.statePath,
20205
+ error: error instanceof Error ? error.message : String(error)
20206
+ });
20207
+ }
20208
+ throw error;
20209
+ }
20210
+ }
20211
+ finally {
20212
+ releaseStateLock();
20213
+ }
20214
+ }
20215
+ function persistCommittedDeferredForegroundTurnAcceptance({ storeDir, terminal, transfer }) {
20216
+ const current = loadDeferredForegroundTransfer(storeDir, transfer.transfer_id);
20217
+ if (current.status !== "committed" ||
20218
+ !current.target_accepted_binding ||
20219
+ !current.target_native_thread_id ||
20220
+ !current.agent_accepted_at) {
20221
+ throw new Error(`deferred foreground transfer ${transfer.transfer_id} has no exact ` +
20222
+ "committed acceptance authority");
20223
+ }
20224
+ const acceptedBinding = current.target_accepted_binding;
20225
+ const rollout = acceptedBinding.native_process.rollout;
20226
+ const processUuid = acceptedBinding.native_process.process_uuid;
20227
+ const processBirth = acceptedBinding.native_process.process_birth;
20228
+ if (acceptedBinding.native_thread_id !== current.target_native_thread_id ||
20229
+ processUuid !== current.process_uuid ||
20230
+ processBirth !== current.process_birth ||
20231
+ !isCompleteNativeRollout(rollout)) {
20232
+ throw new Error(`deferred foreground transfer ${transfer.transfer_id} committed binding ` +
20233
+ "is incomplete");
20234
+ }
20235
+ const identity = {
20236
+ sessionId: current.target_native_thread_id,
20237
+ processUuid,
20238
+ processBirth,
20239
+ rollout,
20240
+ evidence: acceptedBinding.native_process.evidence
20241
+ };
20242
+ const statePath = path.resolve(required(current.state_path, "deferred foreground Turn state path is unavailable"));
20243
+ const releaseStateLock = acquireFileLock(`${statePath}.lock`);
20244
+ try {
20245
+ const authority = loadDeferredForegroundTurnAuthority({
20246
+ storeDir,
20247
+ terminal,
20248
+ transfer: current
20249
+ });
20250
+ const ledger = loadTerminalBridgeDispatchLedger(terminal.terminalControl);
20251
+ if (!ledger) {
20252
+ throw new Error(`deferred foreground transfer ${current.transfer_id} has no exact ` +
20253
+ "terminal dispatch ledger for committed recovery");
20254
+ }
20255
+ assertDeferredForegroundLedgerAuthority({
20256
+ storeDir,
20257
+ terminal,
20258
+ transfer: current,
20259
+ ledger,
20260
+ statePath: authority.statePath,
20261
+ expectedMessageBodyHash: stringValue(authority.submission.message_body_hash)
20262
+ });
20263
+ if (![
20264
+ "prepared",
20265
+ "text_injected",
20266
+ "enter_dispatched",
20267
+ "uncertain",
20268
+ "agent_accepted"
20269
+ ].includes(String(ledger.status))) {
20270
+ throw new Error(`deferred foreground transfer ${current.transfer_id} has invalid ` +
20271
+ `${String(ledger.status)} dispatch evidence for committed recovery`);
20272
+ }
20273
+ const acceptance = detectCodexRolloutAcceptance({
20274
+ anchor: authority.anchor,
20275
+ currentIdentity: identity,
20276
+ requestHash: current.request_hash
20277
+ });
20278
+ if (!acceptance) {
20279
+ throw new Error(`deferred foreground transfer ${current.transfer_id} cannot re-prove ` +
20280
+ "its exact request acceptance");
20281
+ }
20282
+ if (ledger.status === "agent_accepted") {
20283
+ validateTerminalSubmissionAcceptanceEvidence(ledger.acceptance_evidence, {
20284
+ source: "codex_rollout",
20285
+ nativeThreadId: identity.sessionId,
20286
+ requestHash: current.request_hash
20287
+ });
20288
+ }
20289
+ const acceptedConversation = withTerminalBridgeSubmission({
20290
+ conversation: withNativeAgentSessionIdentity(authority.conversation, identity),
20291
+ messageId: current.message_id,
20292
+ messageType: "task",
20293
+ requestText: String(authority.takeover.terminal_bridge_request_text ?? ""),
20294
+ status: "agent_accepted",
20295
+ preparedAt: stringValue(authority.submission.prepared_at) ?? current.prepared_at,
20296
+ textInjectedAt: stringValue(authority.submission.text_injected_at) ??
20297
+ current.text_injected_at ?? current.dispatch_started_at,
20298
+ enterDispatchedAt: stringValue(authority.submission.enter_dispatched_at) ??
20299
+ current.enter_dispatched_at ?? current.dispatch_started_at,
20300
+ agentAcceptedAt: current.agent_accepted_at,
20301
+ acceptanceEvidence: acceptance
20302
+ });
20303
+ saveState(authority.statePath, acceptedConversation);
20304
+ saveTerminalBridgeDispatchLedger(terminal.terminalControl, {
20305
+ ...ledger,
20306
+ ...terminalBindingLedgerFields(acceptedConversation),
20307
+ status: "agent_accepted",
20308
+ generation_id: current.message_id,
20309
+ conversation_id: acceptedConversation.conversation_id,
20310
+ session_id: current.target_session_id,
20311
+ turn_id: current.turn_id,
20312
+ message_id: current.message_id,
20313
+ message_type: "task",
20314
+ request_hash: current.request_hash,
20315
+ prepared_at: current.prepared_at,
20316
+ text_injected_at: current.text_injected_at ?? current.dispatch_started_at,
20317
+ enter_dispatched_at: current.enter_dispatched_at ?? current.dispatch_started_at,
20318
+ agent_accepted_at: current.agent_accepted_at,
20319
+ acceptance_evidence: acceptance,
20320
+ dispatcher_pid: null,
20321
+ state_path: authority.statePath,
20322
+ event_log_path: authority.logPath,
20323
+ callback_expected: Boolean(acceptedConversation.gateway_method),
20324
+ reason: "recovered committed deferred foreground request acceptance"
20325
+ });
20326
+ return { conversation: acceptedConversation, identity };
20327
+ }
20328
+ finally {
20329
+ releaseStateLock();
20330
+ }
20331
+ }
20332
+ async function recoverDeferredCodexForegroundTransferBeforeMutation({ options, terminal }) {
20333
+ const storeDir = storeDirFromOptions(options);
20334
+ let transfers = listDeferredForegroundTransfers(storeDir);
20335
+ // An abort intent is Store authority: it is finalized without consulting a
20336
+ // live pane, PID, cwd, or process birth. Once the exact cleanup receipt is
20337
+ // durable, later legitimate Session revisions never need to be revalidated
20338
+ // against this historical transfer.
20339
+ for (const aborted of transfers.filter((entry) => entry.status === "aborted")) {
20340
+ finalizeAbortedDeferredCodexForegroundTransferCleanup({
20341
+ storeDir,
20342
+ transfer: aborted
20343
+ });
20344
+ }
20345
+ transfers = listDeferredForegroundTransfers(storeDir);
20346
+ const matching = transfers.filter((transfer) => transfer.terminal_id === terminal.conversationId &&
20347
+ transfer.process_pid === terminal.pid &&
20348
+ terminalControlEvidenceMatches(transfer.terminal_endpoint, terminal.terminalControl));
20349
+ const candidates = matching.filter((transfer) => !["resolved", "aborted", "abort_resolved"].includes(transfer.status));
20350
+ if (candidates.length === 0) {
20351
+ return;
20352
+ }
20353
+ if (candidates.length !== 1) {
20354
+ throw new Error(`terminal ${terminal.terminalControl.target} has multiple unresolved ` +
20355
+ "deferred foreground transfers; no terminal input was sent");
20356
+ }
20357
+ let transfer = candidates[0];
20358
+ const boundary = deferredCodexBoundaryFromTransfer({ terminal, transfer });
20359
+ if (transfer.status === "committed") {
20360
+ const accepted = persistCommittedDeferredForegroundTurnAcceptance({
20361
+ storeDir,
20362
+ terminal,
20363
+ transfer
20364
+ });
20365
+ if (cliEnv().AKK_TEST_EXIT_AFTER_DEFERRED_COMMITTED_ACCEPTANCE_BACKFILL ===
20366
+ "1") {
20367
+ // The exact accepted Turn and ledger are durable while both Sessions
20368
+ // remain fenced by the committed transfer. Recovery must accept this
20369
+ // monotonic ledger shape and finish Session resolution without input.
20370
+ cliExit(86);
20371
+ }
20372
+ await resolveCommittedDeferredCodexForegroundTransfer({
20373
+ options,
20374
+ boundary
20375
+ });
20376
+ assertNativeAgentIdentityForTurn({
20377
+ conversation: accepted.conversation,
20378
+ currentIdentity: accepted.identity,
20379
+ operation: "recover committed deferred foreground binding for"
20380
+ });
20381
+ return;
20382
+ }
20383
+ if (["prepared", "source_reserved", "target_prepared"].includes(transfer.status)) {
20384
+ abortPreparedDeferredForegroundTurn({
20385
+ storeDir,
20386
+ terminal,
20387
+ transfer,
20388
+ boundary
20389
+ });
20390
+ return;
20391
+ }
20392
+ if (["dispatch_started", "uncertain"].includes(transfer.status)) {
20393
+ const durableInputNotStartedAt = deferredCodexDurableInputNotStartedAt(transfer);
20394
+ if (durableInputNotStartedAt) {
20395
+ abortPreparedDeferredForegroundTurn({
20396
+ storeDir,
20397
+ terminal,
20398
+ transfer,
20399
+ boundary,
20400
+ terminalInputNotStartedAt: durableInputNotStartedAt
20401
+ });
20402
+ return;
20403
+ }
20404
+ try {
20405
+ if (await recoverAcceptedDeferredForegroundDispatch({
20406
+ options,
20407
+ storeDir,
20408
+ terminal,
20409
+ transfer,
20410
+ boundary
20411
+ })) {
20412
+ return;
20413
+ }
20414
+ }
20415
+ catch (error) {
20416
+ const durable = loadDeferredForegroundTransfer(storeDir, transfer.transfer_id);
20417
+ if (durable.status === "dispatch_started") {
20418
+ transfer = markDeferredCodexForegroundTransferUncertain({
20419
+ options,
20420
+ boundary,
20421
+ reason: "exact deferred acceptance recovery failed: " +
20422
+ (error instanceof Error ? error.message : String(error))
20423
+ });
20424
+ }
20425
+ throw new Error(`terminal ${terminal.terminalControl.target} deferred foreground ` +
20426
+ `recovery failed closed: ${error instanceof Error
20427
+ ? error.message
20428
+ : String(error)}`, { cause: error });
20429
+ }
20430
+ if (transfer.status !== "uncertain") {
20431
+ transfer = markDeferredCodexForegroundTransferUncertain({
20432
+ options,
20433
+ boundary,
20434
+ reason: "dispatcher exited after the conservative terminal-input boundary " +
20435
+ "without exact native request acceptance"
20436
+ });
20437
+ }
20438
+ throw new Error(`terminal ${terminal.terminalControl.target} has deferred foreground ` +
20439
+ `transfer ${transfer.transfer_id} with an uncertain dispatch; do not ` +
20440
+ "retry or resolve its Turn outside dedicated recovery");
20441
+ }
20442
+ throw new Error(`deferred foreground transfer ${transfer.transfer_id} has unsupported ` +
20443
+ `recovery status ${transfer.status}`);
20444
+ }
18014
20445
  async function recoverLifecycleFenceBeforeMutation({ options, terminal }) {
20446
+ await recoverDeferredCodexForegroundTransferBeforeMutation({
20447
+ options,
20448
+ terminal
20449
+ });
18015
20450
  let ledger = loadTerminalBridgeDispatchLedger(terminal.terminalControl);
18016
20451
  if (!ledger || ledger.status === "resolved") {
18017
20452
  ledger = rebuildObservedHandoffLedgerFromTransition({
@@ -19966,6 +22401,15 @@ function runCallback(options) {
19966
22401
  }
19967
22402
  function runRetryCallback(options) {
19968
22403
  const { conversation, statePath, logPath } = loadConversationFromOptions(options);
22404
+ const retryStoreDir = pathsForConversationDir(path.dirname(statePath)).storeDir;
22405
+ withStoreWriterLease(retryStoreDir, () => {
22406
+ const fresh = loadState(statePath);
22407
+ assertConversationHasNoNonterminalDeferredForegroundTransfer({
22408
+ storeDir: retryStoreDir,
22409
+ conversation: fresh,
22410
+ action: "retry callback for"
22411
+ });
22412
+ });
19969
22413
  const callbackDelivery = isRecord(conversation.callback_delivery)
19970
22414
  ? conversation.callback_delivery
19971
22415
  : undefined;