@scotthuang/agent-knock-knock 0.12.3 → 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,16 +11,17 @@ 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";
22
23
  import { listNativeThreadTransitions, listManagedSessions, loadManagedSession, loadNativeThreadTransition, nativeThreadTransitionsDir, saveManagedSession, saveNativeThreadTransition, tryLoadManagedSession } from "./session-store.js";
23
- import { createTerminalControlProviderRegistry as createProviderRegistry, StaticTerminalControlProvider, TmuxTerminalControlProvider } from "./terminal-control-provider.js";
24
+ import { createTerminalControlProviderRegistry as createProviderRegistry, StaticTerminalControlProvider, TerminalControlUnavailableError, TmuxTerminalControlProvider } from "./terminal-control-provider.js";
24
25
  import { HerdrTerminalControlProvider } from "./herdr-terminal-control-provider.js";
25
26
  import { parseTerminalConversationId } from "./terminal-agent-adapter.js";
26
27
  import { associateTerminalEndpointEvidence, hasCanonicalTerminalEndpoint, sameTerminalControlEvidenceIncarnation, sameTerminalControlIncarnation, terminalControlEvidence, terminalControlEvidenceMatches, terminalEndpointFromControlRef, terminalEndpointIdentityFromEvidence, terminalEndpointIdentityKey, terminalLegacyRuntimeRoute, sameTerminalEndpointIdentity } from "./terminal-control-ref.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",
@@ -96,6 +98,7 @@ const SESSION_SEND_BLOCKING_STATUSES = new Set([
96
98
  "callback_failed",
97
99
  "cancelling"
98
100
  ]);
101
+ const TERMINAL_BRIDGE_UNCERTAIN_COLLATERAL_STALL_REASON = "a newer terminal submission has an uncertain outcome; inspect the shared terminal pane before continuing";
99
102
  const TERMINAL_BRIDGE_MONITOR_LOCK_VERSION = 1;
100
103
  const MINIMUM_NODE_VERSION = "22.19.0";
101
104
  const PRIVATE_LOCK_FILE_MODE = 0o600;
@@ -2627,7 +2630,7 @@ function stallOtherTerminalBridgeConversationsForUncertainDispatch({ storeDir, t
2627
2630
  const stalledConversationIds = [];
2628
2631
  for (const listed of listConversations(storeDir)) {
2629
2632
  if (listed.conversation_id === currentConversationId ||
2630
- !isActiveStatus(listed.status)) {
2633
+ !SESSION_SEND_BLOCKING_STATUSES.has(listed.status)) {
2631
2634
  continue;
2632
2635
  }
2633
2636
  const listedTakeover = isRecord(listed.native_session_takeover)
@@ -2647,7 +2650,7 @@ function stallOtherTerminalBridgeConversationsForUncertainDispatch({ storeDir, t
2647
2650
  const currentTakeover = isRecord(current.native_session_takeover)
2648
2651
  ? current.native_session_takeover
2649
2652
  : undefined;
2650
- if (!isActiveStatus(current.status) ||
2653
+ if (!SESSION_SEND_BLOCKING_STATUSES.has(current.status) ||
2651
2654
  currentTakeover?.terminal_bridge !== true ||
2652
2655
  !terminalControlsShareIncarnation(terminalControlFromTakeover(currentTakeover), terminalControl)) {
2653
2656
  continue;
@@ -2657,12 +2660,13 @@ function stallOtherTerminalBridgeConversationsForUncertainDispatch({ storeDir, t
2657
2660
  ...current,
2658
2661
  status: "stalled",
2659
2662
  stalled_at: stalledAt,
2660
- stalled_reason: "a newer terminal submission has an uncertain outcome; inspect the shared terminal pane before continuing",
2663
+ stalled_reason: TERMINAL_BRIDGE_UNCERTAIN_COLLATERAL_STALL_REASON,
2661
2664
  native_session_takeover: {
2662
2665
  ...currentTakeover,
2663
2666
  terminal_bridge_uncertain_dispatch_fence: {
2664
2667
  message_id: uncertainMessageId,
2665
- observed_at: stalledAt
2668
+ observed_at: stalledAt,
2669
+ previous_status: current.status
2666
2670
  }
2667
2671
  },
2668
2672
  updated_at: stalledAt
@@ -2688,6 +2692,303 @@ function stallOtherTerminalBridgeConversationsForUncertainDispatch({ storeDir, t
2688
2692
  }
2689
2693
  return stalledConversationIds;
2690
2694
  }
2695
+ function exactTerminalBridgeCollateralStallRepairEvidence({ conversation, storeDir }) {
2696
+ if (conversation.status !== "stalled" ||
2697
+ conversation.stalled_reason !==
2698
+ TERMINAL_BRIDGE_UNCERTAIN_COLLATERAL_STALL_REASON) {
2699
+ return undefined;
2700
+ }
2701
+ const takeover = isRecord(conversation.native_session_takeover)
2702
+ ? conversation.native_session_takeover
2703
+ : undefined;
2704
+ const fence = isRecord(takeover?.terminal_bridge_uncertain_dispatch_fence)
2705
+ ? takeover.terminal_bridge_uncertain_dispatch_fence
2706
+ : undefined;
2707
+ const uncertainMessageId = stringValue(fence?.message_id);
2708
+ const fenceObservedAt = stringValue(fence?.observed_at);
2709
+ const previousStatus = stringValue(fence?.previous_status);
2710
+ const ownMessageId = stringValue(takeover?.terminal_bridge_message_id);
2711
+ const ownSubmission = terminalBridgeSubmission(conversation);
2712
+ const completionClaim = isRecord(takeover?.terminal_bridge_completion_claim)
2713
+ ? takeover.terminal_bridge_completion_claim
2714
+ : undefined;
2715
+ const callbackDelivery = isRecord(conversation.callback_delivery)
2716
+ ? conversation.callback_delivery
2717
+ : undefined;
2718
+ const callbackMessage = isRecord(callbackDelivery?.message)
2719
+ ? callbackDelivery.message
2720
+ : undefined;
2721
+ const idleSince = stringValue(conversation.idle_since);
2722
+ const deliveredAt = stringValue(callbackDelivery?.delivered_at);
2723
+ const claimedAt = stringValue(completionClaim?.claimed_at);
2724
+ const fenceAtMs = validTimestampMs(fenceObservedAt);
2725
+ const idleAtMs = validTimestampMs(idleSince);
2726
+ const deliveredAtMs = validTimestampMs(deliveredAt);
2727
+ const claimedAtMs = validTimestampMs(claimedAt);
2728
+ if (takeover?.terminal_bridge !== true ||
2729
+ !uncertainMessageId ||
2730
+ !fenceObservedAt ||
2731
+ fenceAtMs === undefined ||
2732
+ uncertainMessageId === ownMessageId ||
2733
+ (previousStatus !== undefined && previousStatus !== "idle") ||
2734
+ conversation.stalled_at !== fenceObservedAt ||
2735
+ conversation.updated_at !== fenceObservedAt ||
2736
+ !idleSince ||
2737
+ idleAtMs === undefined ||
2738
+ idleAtMs > fenceAtMs ||
2739
+ !ownMessageId ||
2740
+ ownSubmission?.status !== "agent_accepted" ||
2741
+ stringValue(ownSubmission.message_id) !== ownMessageId ||
2742
+ stringValue(ownSubmission.session_id) !==
2743
+ sessionIdForConversation(conversation) ||
2744
+ stringValue(ownSubmission.turn_id) !== turnIdForConversation(conversation) ||
2745
+ !completionClaim ||
2746
+ stringValue(completionClaim.terminal_bridge_message_id) !== ownMessageId ||
2747
+ completionClaim.outcome !== "success" ||
2748
+ !claimedAt ||
2749
+ claimedAtMs === undefined ||
2750
+ claimedAtMs > fenceAtMs ||
2751
+ callbackDelivery?.status !== "delivered" ||
2752
+ callbackDelivery.final_status !== "idle" ||
2753
+ callbackDelivery.preserve_conversation_status !== true ||
2754
+ !callbackMessage ||
2755
+ callbackMessage.type !== "done" ||
2756
+ callbackMessage.requires_response !== false ||
2757
+ stringValue(callbackMessage.id) !==
2758
+ stringValue(completionClaim.callback_message_id) ||
2759
+ stringValue(callbackMessage.conversation_id) !==
2760
+ conversation.conversation_id ||
2761
+ stringValue(callbackMessage.session_id) !==
2762
+ sessionIdForConversation(conversation) ||
2763
+ stringValue(callbackMessage.turn_id) !== turnIdForConversation(conversation) ||
2764
+ stringValue(isRecord(callbackMessage.metadata)
2765
+ ? callbackMessage.metadata.terminal_bridge_message_id
2766
+ : undefined) !== ownMessageId ||
2767
+ !deliveredAt ||
2768
+ deliveredAtMs === undefined ||
2769
+ deliveredAtMs > fenceAtMs) {
2770
+ return undefined;
2771
+ }
2772
+ const control = terminalControlForManagedConversation(conversation);
2773
+ if (!control) {
2774
+ return undefined;
2775
+ }
2776
+ // The target state lock is already held by the caller. Rescan owner state
2777
+ // now instead of trusting the unlocked discovery snapshot used to find the
2778
+ // target candidate.
2779
+ const ownerCandidates = listConversations(storeDir).filter((candidate) => {
2780
+ if (candidate.conversation_id === conversation.conversation_id) {
2781
+ return false;
2782
+ }
2783
+ const candidateTakeover = isRecord(candidate.native_session_takeover)
2784
+ ? candidate.native_session_takeover
2785
+ : undefined;
2786
+ const candidateSubmission = terminalBridgeSubmission(candidate);
2787
+ return candidateTakeover?.terminal_bridge === true &&
2788
+ stringValue(candidateTakeover.terminal_bridge_message_id) ===
2789
+ uncertainMessageId &&
2790
+ stringValue(candidateSubmission?.message_id) === uncertainMessageId &&
2791
+ terminalControlsShareIncarnation(terminalControlForManagedConversation(candidate), control);
2792
+ });
2793
+ if (ownerCandidates.length !== 1) {
2794
+ return undefined;
2795
+ }
2796
+ const ownerListed = ownerCandidates[0];
2797
+ const ownerStatePath = stringValue(ownerListed.state_path);
2798
+ if (!ownerStatePath) {
2799
+ return undefined;
2800
+ }
2801
+ let owner;
2802
+ let ownerEvents;
2803
+ let events;
2804
+ try {
2805
+ owner = loadState(ownerStatePath);
2806
+ ownerEvents = readNdjsonLog(stringValue(owner.event_log_path) ?? logPathForStatePath(ownerStatePath));
2807
+ const statePath = stringValue(conversation.state_path);
2808
+ if (!statePath) {
2809
+ return undefined;
2810
+ }
2811
+ events = readNdjsonLog(stringValue(conversation.event_log_path) ??
2812
+ logPathForStatePath(statePath));
2813
+ }
2814
+ catch {
2815
+ return undefined;
2816
+ }
2817
+ const ownerTakeover = isRecord(owner.native_session_takeover)
2818
+ ? owner.native_session_takeover
2819
+ : undefined;
2820
+ const ownerSubmission = terminalBridgeSubmission(owner);
2821
+ const ownerClosedAt = stringValue(owner.closed_at);
2822
+ const ownerClosedAtMs = validTimestampMs(ownerClosedAt);
2823
+ if (owner.conversation_id !== ownerListed.conversation_id ||
2824
+ owner.status !== "closed" ||
2825
+ !ownerClosedAt ||
2826
+ ownerClosedAtMs === undefined ||
2827
+ ownerClosedAtMs < fenceAtMs ||
2828
+ !stringValue(owner.close_reason) ||
2829
+ owner.updated_at !== ownerClosedAt ||
2830
+ ownerTakeover?.terminal_bridge !== true ||
2831
+ stringValue(ownerTakeover.terminal_bridge_message_id) !==
2832
+ uncertainMessageId ||
2833
+ ownerSubmission?.status !== "uncertain" ||
2834
+ stringValue(ownerSubmission.message_id) !== uncertainMessageId ||
2835
+ stringValue(ownerSubmission.session_id) !== sessionIdForConversation(owner) ||
2836
+ stringValue(ownerSubmission.turn_id) !== turnIdForConversation(owner) ||
2837
+ isRecord(ownerTakeover.terminal_bridge_uncertain_dispatch_fence) ||
2838
+ !terminalControlsShareIncarnation(terminalControlForManagedConversation(owner), control)) {
2839
+ return undefined;
2840
+ }
2841
+ const callbackMessageId = stringValue(callbackMessage.id);
2842
+ const hasCompletionClaim = events.some((event) => event.event === "terminal_bridge_completion_claimed" &&
2843
+ event.conversation_id === conversation.conversation_id &&
2844
+ event.terminal_bridge_message_id === ownMessageId &&
2845
+ event.callback_message_id === callbackMessageId &&
2846
+ event.completion_fingerprint === completionClaim.completion_fingerprint &&
2847
+ event.completion_id === completionClaim.completion_id &&
2848
+ event.outcome === "success" &&
2849
+ event.ts === claimedAt);
2850
+ const hasCompletionDetected = events.some((event) => event.event === "terminal_bridge_completion_detected" &&
2851
+ event.conversation_id === conversation.conversation_id &&
2852
+ event.terminal_bridge_message_id === ownMessageId &&
2853
+ event.callback_message_id === callbackMessageId &&
2854
+ event.completion_id === completionClaim.completion_id &&
2855
+ event.completion_outcome === "success" &&
2856
+ validTimestampMs(event.ts) !== undefined &&
2857
+ validTimestampMs(event.ts) <= fenceAtMs);
2858
+ const hasDeliveredCallback = events.some((event) => event.event === "callback_delivery_succeeded" &&
2859
+ event.conversation_id === conversation.conversation_id &&
2860
+ event.message_id === callbackMessageId &&
2861
+ event.status === "idle" &&
2862
+ event.ts === deliveredAt);
2863
+ const hasExactFence = events.some((event) => event.event === "terminal_bridge_stalled_by_uncertain_dispatch" &&
2864
+ event.conversation_id === conversation.conversation_id &&
2865
+ event.uncertain_message_id === uncertainMessageId &&
2866
+ event.ts === fenceObservedAt);
2867
+ const hasExactOwnerClose = ownerEvents.some((event) => event.event === "conversation_closed" &&
2868
+ event.conversation_id === owner.conversation_id &&
2869
+ event.status === "closed" &&
2870
+ event.ts === ownerClosedAt &&
2871
+ event.reason === owner.close_reason);
2872
+ if (!hasCompletionClaim ||
2873
+ !hasCompletionDetected ||
2874
+ !hasDeliveredCallback ||
2875
+ !hasExactFence ||
2876
+ !hasExactOwnerClose) {
2877
+ return undefined;
2878
+ }
2879
+ return {
2880
+ uncertainMessageId,
2881
+ ownerConversationId: owner.conversation_id,
2882
+ restoredStatus: "idle"
2883
+ };
2884
+ }
2885
+ function reconcileTerminalBridgeCollateralStalls(storeDir, conversationId) {
2886
+ const candidates = listConversations(storeDir).filter((conversation) => {
2887
+ const takeover = isRecord(conversation.native_session_takeover)
2888
+ ? conversation.native_session_takeover
2889
+ : undefined;
2890
+ return (conversationId === undefined ||
2891
+ conversation.conversation_id === conversationId) &&
2892
+ conversation.status === "stalled" &&
2893
+ isRecord(takeover?.terminal_bridge_uncertain_dispatch_fence);
2894
+ });
2895
+ let repaired = 0;
2896
+ let skipped = 0;
2897
+ const errors = [];
2898
+ const items = [];
2899
+ for (const listed of candidates) {
2900
+ const statePath = stringValue(listed.state_path);
2901
+ if (!statePath) {
2902
+ skipped += 1;
2903
+ continue;
2904
+ }
2905
+ let releaseStateLock;
2906
+ try {
2907
+ releaseStateLock = acquireFileLock(`${statePath}.lock`);
2908
+ const current = loadState(statePath);
2909
+ const evidence = exactTerminalBridgeCollateralStallRepairEvidence({
2910
+ conversation: current,
2911
+ storeDir
2912
+ });
2913
+ if (!evidence) {
2914
+ skipped += 1;
2915
+ continue;
2916
+ }
2917
+ const takeover = {
2918
+ ...current.native_session_takeover
2919
+ };
2920
+ delete takeover.terminal_bridge_uncertain_dispatch_fence;
2921
+ const repairedAt = cliNow().toISOString();
2922
+ takeover.terminal_bridge_collateral_stall_repair = {
2923
+ repaired_at: repairedAt,
2924
+ uncertain_message_id: evidence.uncertainMessageId,
2925
+ uncertain_owner_conversation_id: evidence.ownerConversationId,
2926
+ restored_status: evidence.restoredStatus,
2927
+ evidence: "foreign_uncertain_fence+completion_claim+delivered_callback+closed_owner"
2928
+ };
2929
+ const repairedConversation = {
2930
+ ...current,
2931
+ status: evidence.restoredStatus,
2932
+ native_session_takeover: takeover,
2933
+ updated_at: repairedAt
2934
+ };
2935
+ delete repairedConversation.stalled_at;
2936
+ delete repairedConversation.stalled_reason;
2937
+ saveState(statePath, repairedConversation);
2938
+ let eventWarning;
2939
+ try {
2940
+ appendEvent(stringValue(current.event_log_path) ?? logPathForStatePath(statePath), {
2941
+ ts: repairedAt,
2942
+ conversation_id: current.conversation_id,
2943
+ event: "terminal_bridge_collateral_stall_repaired",
2944
+ uncertain_message_id: evidence.uncertainMessageId,
2945
+ uncertain_owner_conversation_id: evidence.ownerConversationId,
2946
+ previous_status: "stalled",
2947
+ restored_status: evidence.restoredStatus,
2948
+ evidence: "foreign_uncertain_fence+completion_claim+delivered_callback+closed_owner"
2949
+ });
2950
+ }
2951
+ catch (error) {
2952
+ eventWarning = error instanceof Error ? error.message : String(error);
2953
+ runtimeLog("warn", "terminal_bridge_collateral_stall_repair_event_failed", {
2954
+ conversation_id: current.conversation_id,
2955
+ uncertain_message_id: evidence.uncertainMessageId,
2956
+ error: eventWarning
2957
+ });
2958
+ }
2959
+ repaired += 1;
2960
+ items.push({
2961
+ conversation_id: current.conversation_id,
2962
+ status: "repaired",
2963
+ reason: "legacy_terminal_bridge_collateral_stall",
2964
+ uncertain_message_id: evidence.uncertainMessageId,
2965
+ uncertain_owner_conversation_id: evidence.ownerConversationId,
2966
+ restored_status: evidence.restoredStatus,
2967
+ ...(eventWarning ? { event_warning: eventWarning } : {})
2968
+ });
2969
+ }
2970
+ catch (error) {
2971
+ skipped += 1;
2972
+ const reason = error instanceof Error ? error.message : String(error);
2973
+ errors.push(`${listed.conversation_id}: ${reason}`);
2974
+ items.push({
2975
+ conversation_id: listed.conversation_id,
2976
+ status: "error",
2977
+ reason
2978
+ });
2979
+ }
2980
+ finally {
2981
+ releaseStateLock?.();
2982
+ }
2983
+ }
2984
+ return {
2985
+ checked: candidates.length,
2986
+ repaired,
2987
+ skipped,
2988
+ errors,
2989
+ items
2990
+ };
2991
+ }
2691
2992
  function environmentWithoutGatewayTokens() {
2692
2993
  const environment = { ...cliEnv() };
2693
2994
  delete environment.AKK_GATEWAY_TOKEN;
@@ -2723,7 +3024,7 @@ async function runList(options) {
2723
3024
  storeDir,
2724
3025
  terminals: physicalTerminals,
2725
3026
  managedSessions,
2726
- sessionAuthorityRequired: store.writer_protocol === 3,
3027
+ sessionAuthorityRequired: Number(store.writer_protocol) >= STORE_SESSION_AUTHORITY_PROTOCOL,
2727
3028
  allConversations: allManagedConversations.filter((conversation) => matchesConfiguredWorkspace(options.workspace, conversation.workspace)),
2728
3029
  displayedConversations: storedConversations,
2729
3030
  includeAll,
@@ -2768,17 +3069,21 @@ async function reconcileStoreForList(storeDir, options) {
2768
3069
  }
2769
3070
  throw error;
2770
3071
  }
2771
- const idle = reconcileIdleConversations(storeDir, options);
2772
3072
  const monitors = await reconcileMonitors(options, {
2773
3073
  includeCallbackRecovery: false,
2774
3074
  reason: "list_reconciliation",
2775
3075
  conversationId: undefined
2776
3076
  });
3077
+ const idle = reconcileIdleConversations(storeDir, options);
2777
3078
  return {
2778
3079
  status: "completed",
2779
3080
  checked: Math.max(idle.checked, monitors.checked),
2780
- changed: idle.closed + monitors.launched,
3081
+ changed: idle.closed + monitors.launched + monitors.repaired,
2781
3082
  closed: idle.closed,
3083
+ repaired: monitors.repaired,
3084
+ collateral_stalls_checked: monitors.collateral_stalls_checked,
3085
+ collateral_stalls_skipped: monitors.collateral_stalls_skipped,
3086
+ collateral_stall_repairs: monitors.items.filter((item) => item.status === "repaired"),
2782
3087
  monitors_launched: monitors.launched,
2783
3088
  monitors_already_running: monitors.already_running,
2784
3089
  skipped: idle.skipped + monitors.skipped,
@@ -2999,9 +3304,7 @@ async function terminalControlledListEntry(session, activeSessions, options, bri
2999
3304
  });
3000
3305
  const codexLifecycleIncarnationAvailable = session.agent !== "codex" ||
3001
3306
  Boolean(nativeProcessUuid && nativeProcessBirth);
3002
- const nativeInspectionHasBlockingTurn = listConversations(storeDirFromOptions(options)).some((turn) => isDiscoverableTmuxConversation(turn) &&
3003
- terminalControlsShareIncarnation(terminalControlForManagedConversation(turn), terminalControl) &&
3004
- SESSION_SEND_BLOCKING_STATUSES.has(turn.status));
3307
+ const terminalHasBlockingTurn = terminalIncarnationBlockingTurns(storeDirFromOptions(options), terminalControl).length > 0;
3005
3308
  let automatedInputComposerReady = nativeInspectionComposerEmpty(session.agent, terminalState.screen_excerpt);
3006
3309
  if (session.agent === "codex" &&
3007
3310
  (terminalState.activity_state === "idle" ||
@@ -3078,7 +3381,7 @@ async function terminalControlledListEntry(session, activeSessions, options, bri
3078
3381
  }
3079
3382
  : {}),
3080
3383
  commands: {
3081
- send: true,
3384
+ send: !terminalHasBlockingTurn,
3082
3385
  approve: terminalControl.capabilities.includes("terminal_approval") &&
3083
3386
  terminalState.approval_state.approvable === true,
3084
3387
  status: true,
@@ -3086,7 +3389,8 @@ async function terminalControlledListEntry(session, activeSessions, options, bri
3086
3389
  close: orphanedDispatch !== undefined,
3087
3390
  new_thread: lifecycleCapability.status === "supported" &&
3088
3391
  lifecycleCapability.newThread === true &&
3089
- codexLifecycleIncarnationAvailable,
3392
+ codexLifecycleIncarnationAvailable &&
3393
+ !terminalHasBlockingTurn,
3090
3394
  list_resumable_threads: lifecycleCapability.status === "supported" &&
3091
3395
  lifecycleCapability.resumeExact === true &&
3092
3396
  codexLifecycleIncarnationAvailable,
@@ -3101,7 +3405,7 @@ async function terminalControlledListEntry(session, activeSessions, options, bri
3101
3405
  : Boolean(nativeAgentIdentity?.sessionId &&
3102
3406
  nativeAgentIdentity.processUuid)) &&
3103
3407
  orphanedDispatch === undefined &&
3104
- !nativeInspectionHasBlockingTurn
3408
+ !terminalHasBlockingTurn
3105
3409
  }
3106
3410
  };
3107
3411
  const availableActions = availableListActions(entry);
@@ -3112,6 +3416,15 @@ async function terminalControlledListEntry(session, activeSessions, options, bri
3112
3416
  };
3113
3417
  }
3114
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
+ };
3115
3428
  const discoveredTerminalControls = terminals.flatMap((terminal) => {
3116
3429
  const control = isRecord(terminal.terminal_control)
3117
3430
  ? terminal.terminal_control
@@ -3123,6 +3436,9 @@ function terminalFirstListProjection({ storeDir, terminals, managedSessions, ses
3123
3436
  const terminalControl = isRecord(terminal.terminal_control)
3124
3437
  ? terminal.terminal_control
3125
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)));
3126
3442
  const allRelated = terminalControl
3127
3443
  ? allConversations.filter((conversation) => terminalControlsShareIncarnation(terminalControlForManagedConversation(conversation), terminalControl))
3128
3444
  : [];
@@ -3227,10 +3543,13 @@ function terminalFirstListProjection({ storeDir, terminals, managedSessions, ses
3227
3543
  const bindingAwareRawActions = authoritativeSession
3228
3544
  ? actionsForManagedSessionBinding(rawActions, authoritativeSession)
3229
3545
  : rawActions;
3230
- const sessionAwareRawActions = authoritativeSession &&
3546
+ const sessionAwareRawActionsBase = authoritativeSession &&
3231
3547
  managedSessionHasUnresolvedNativeTransition(storeDir, authoritativeSession)
3232
3548
  ? Object.fromEntries(Object.entries(bindingAwareRawActions).filter(([actionName]) => actionName !== "native_inspect"))
3233
3549
  : bindingAwareRawActions;
3550
+ const sessionAwareRawActions = terminalHasNonterminalDeferredTransfer
3551
+ ? readOnlyListActions(sessionAwareRawActionsBase)
3552
+ : sessionAwareRawActionsBase;
3234
3553
  const soleBindingConflict = conflictingBoundSessionClaims.length === 1
3235
3554
  ? conflictingBoundSessionClaims[0]
3236
3555
  : undefined;
@@ -3283,6 +3602,13 @@ function terminalFirstListProjection({ storeDir, terminals, managedSessions, ses
3283
3602
  })
3284
3603
  : undefined;
3285
3604
  const blockingHandoffTurns = conflictingSessionTurns.filter((turn) => SESSION_SEND_BLOCKING_STATUSES.has(turn.status));
3605
+ const terminalBlockingTurns = terminalControl
3606
+ ? terminalIncarnationBlockingTurns(storeDir, terminalControl, allRelated)
3607
+ : [];
3608
+ const externalHandoffSourceSessionIds = new Set(conflictingBoundSessionClaims
3609
+ .filter(({ kind }) => kind === "live_external_thread_change")
3610
+ .map(({ session }) => session.session_id));
3611
+ const handoffSourceBlockingTurns = terminalBlockingTurns.filter((turn) => externalHandoffSourceSessionIds.has(sessionIdForConversation(turn)));
3286
3612
  const nativeIdentityObservation = isRecord(terminal.native_agent_identity_observation)
3287
3613
  ? terminal.native_agent_identity_observation
3288
3614
  : undefined;
@@ -3327,6 +3653,7 @@ function terminalFirstListProjection({ storeDir, terminals, managedSessions, ses
3327
3653
  !(isRecord(terminal.approval_state) &&
3328
3654
  terminal.approval_state.blocked === true) &&
3329
3655
  blockingHandoffTurns.length === 0 &&
3656
+ terminalBlockingTurns.length === 0 &&
3330
3657
  !managedSessionHasUnresolvedNativeTransition(storeDir, soleBindingConflict.session) &&
3331
3658
  !verifiedEmptySourceActiveElsewhere &&
3332
3659
  terminal.orphaned_terminal_dispatch === undefined &&
@@ -3348,6 +3675,76 @@ function terminalFirstListProjection({ storeDir, terminals, managedSessions, ses
3348
3675
  sourceSession: soleBindingConflict.session
3349
3676
  })
3350
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;
3351
3748
  const reconcileBindingAction = mutationsAllowed &&
3352
3749
  discoveredOwnership.state === "none" &&
3353
3750
  unresolvedSessionClaims.length === 0 &&
@@ -3361,6 +3758,7 @@ function terminalFirstListProjection({ storeDir, terminals, managedSessions, ses
3361
3758
  !(isRecord(terminal.approval_state) &&
3362
3759
  terminal.approval_state.blocked === true) &&
3363
3760
  !conflictingSessionTurns.some((turn) => SESSION_SEND_BLOCKING_STATUSES.has(turn.status)) &&
3761
+ terminalBlockingTurns.length === 0 &&
3364
3762
  !managedSessionHasUnresolvedNativeTransition(storeDir, soleBindingConflict.session)
3365
3763
  ? {
3366
3764
  tool: "agent_knock_knock_reconcile_binding",
@@ -3383,6 +3781,7 @@ function terminalFirstListProjection({ storeDir, terminals, managedSessions, ses
3383
3781
  !(isRecord(terminal.approval_state) &&
3384
3782
  terminal.approval_state.blocked === true) &&
3385
3783
  !conflictingSessionTurns.some((turn) => SESSION_SEND_BLOCKING_STATUSES.has(turn.status)) &&
3784
+ terminalBlockingTurns.length === 0 &&
3386
3785
  !managedSessionHasUnresolvedNativeTransition(storeDir, soleBindingConflict.session) &&
3387
3786
  externalHandoffTarget?.status === "eligible" &&
3388
3787
  isRecord(rawActions.send) &&
@@ -3394,7 +3793,8 @@ function terminalFirstListProjection({ storeDir, terminals, managedSessions, ses
3394
3793
  !(isRecord(terminal.approval_state) &&
3395
3794
  terminal.approval_state.blocked === true) &&
3396
3795
  !managedSessionHasUnresolvedNativeTransition(storeDir, soleBindingConflict.session) &&
3397
- blockingHandoffTurns.length === 1
3796
+ blockingHandoffTurns.length === 1 &&
3797
+ terminalBlockingTurns.every((turn) => turn.conversation_id === blockingHandoffTurns[0].conversation_id)
3398
3798
  ? blockingHandoffTurns[0]
3399
3799
  : undefined;
3400
3800
  const handoffDecisionToken = handoffDecisionTurn &&
@@ -3434,6 +3834,15 @@ function terminalFirstListProjection({ storeDir, terminals, managedSessions, ses
3434
3834
  }
3435
3835
  }
3436
3836
  : undefined;
3837
+ const blockingHandoffTurnIds = new Set(handoffSourceBlockingTurns.map((turn) => turn.conversation_id));
3838
+ // An active source Turn may be superseded only through the snapshot-bound
3839
+ // handoff decision above. Never expose the generic Store-only close action
3840
+ // for that Turn, because doing so would bypass expected_handoff_token after
3841
+ // the live native thread or Turn generation changed. `blocking_turns` is
3842
+ // reserved for other same-incarnation blockers such as legacy collateral
3843
+ // stalls; clear those first, refresh, and only then decide the handoff.
3844
+ const terminalRecoveryBlockingTurns = terminalBlockingTurns.filter((turn) => !blockingHandoffTurnIds.has(turn.conversation_id) &&
3845
+ !conversationHasNonterminalDeferredTransfer(turn));
3437
3846
  const terminalCanAcceptSend = ownership.state === "none" && isRecord(sessionAwareRawActions.send);
3438
3847
  if (ownership.state === "current" &&
3439
3848
  !allRelated.some((conversation) => conversation.conversation_id === ownership.conversation.conversation_id)) {
@@ -3470,9 +3879,14 @@ function terminalFirstListProjection({ storeDir, terminals, managedSessions, ses
3470
3879
  const currentTurnValue = ownership.state === "current"
3471
3880
  ? currentManagedTurnForTerminal(ownership.conversation, terminal, sessionAwareRawActions)
3472
3881
  : undefined;
3473
- const currentTurn = currentTurnValue && !mutationsAllowed
3882
+ const currentTurnProjection = currentTurnValue && (!mutationsAllowed ||
3883
+ (ownership.state === "current" &&
3884
+ conversationHasNonterminalDeferredTransfer(ownership.conversation)))
3474
3885
  ? readOnlyManagedTurn(currentTurnValue)
3475
3886
  : currentTurnValue;
3887
+ const currentTurn = currentTurnProjection
3888
+ ? withoutGenericHandoffSourceClose(currentTurnProjection, blockingHandoffTurnIds)
3889
+ : undefined;
3476
3890
  const sortedDisplayed = [...sessionDisplayedRelated]
3477
3891
  .filter((conversation) => conversation.conversation_id !== currentTurn?.conversation_id)
3478
3892
  .sort(compareManagedConversationRecency);
@@ -3480,15 +3894,23 @@ function terminalFirstListProjection({ storeDir, terminals, managedSessions, ses
3480
3894
  const recentTurnValue = recentConversation
3481
3895
  ? historicalManagedTurnForTerminal(recentConversation, terminalCanAcceptSend, terminal)
3482
3896
  : undefined;
3483
- const recentTurn = recentTurnValue && !mutationsAllowed
3897
+ const recentTurnProjection = recentTurnValue && (!mutationsAllowed ||
3898
+ Boolean(recentConversation &&
3899
+ conversationHasNonterminalDeferredTransfer(recentConversation)))
3484
3900
  ? readOnlyManagedTurn(recentTurnValue)
3485
3901
  : recentTurnValue;
3902
+ const recentTurn = recentTurnProjection
3903
+ ? withoutGenericHandoffSourceClose(recentTurnProjection, blockingHandoffTurnIds)
3904
+ : undefined;
3486
3905
  const historyConversations = includeAll
3487
3906
  ? sortedDisplayed.filter((conversation) => conversation.conversation_id !== recentConversation?.conversation_id)
3488
3907
  : [];
3489
3908
  const history = historyConversations.map((conversation) => {
3490
3909
  const turn = historicalManagedTurnForTerminal(conversation, terminalCanAcceptSend, terminal);
3491
- return mutationsAllowed ? turn : readOnlyManagedTurn(turn);
3910
+ return withoutGenericHandoffSourceClose(mutationsAllowed &&
3911
+ !conversationHasNonterminalDeferredTransfer(conversation)
3912
+ ? turn
3913
+ : readOnlyManagedTurn(turn), blockingHandoffTurnIds);
3492
3914
  });
3493
3915
  const visibleTurnIds = new Set([currentTurn, recentTurn, ...history]
3494
3916
  .map((turn) => stringValue(turn?.conversation_id))
@@ -3552,14 +3974,28 @@ function terminalFirstListProjection({ storeDir, terminals, managedSessions, ses
3552
3974
  ? { reconcile_binding: reconcileBindingAction }
3553
3975
  : {})
3554
3976
  }
3555
- : managedSessionId &&
3556
- sessionBindingMatchesLiveTerminal &&
3557
- isRecord(sessionAwareRawActions.send)
3977
+ : deferredCodexForegroundToken &&
3978
+ isRecord(rawActions.send)
3558
3979
  ? {
3559
3980
  ...sessionAwareRawActions,
3560
- 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
+ }
3561
3990
  }
3562
- : sessionAwareRawActions;
3991
+ : managedSessionId &&
3992
+ sessionBindingMatchesLiveTerminal &&
3993
+ isRecord(sessionAwareRawActions.send)
3994
+ ? {
3995
+ ...sessionAwareRawActions,
3996
+ send: sendActionForManagedSession(sessionAwareRawActions.send, managedSessionId)
3997
+ }
3998
+ : sessionAwareRawActions;
3563
3999
  const authoritativeBinding = authoritativeSession?.binding;
3564
4000
  const authoritativeIdentity = authoritativeBinding?.native_thread_id
3565
4001
  ? {
@@ -3602,17 +4038,45 @@ function terminalFirstListProjection({ storeDir, terminals, managedSessions, ses
3602
4038
  handoff_state: externalHandoffAdoptable
3603
4039
  ? "external_handoff_adoptable"
3604
4040
  : "external_handoff_blocked",
3605
- ...(automatedInputComposerReady !== true
4041
+ ...(terminalRecoveryBlockingTurns.length > 0
3606
4042
  ? {
3607
- handoff_blocked_reason: "the current terminal composer is not an exact empty idle frame"
4043
+ handoff_blocked_reason: "the terminal has unresolved managed Turn state; use only a listed blocking_turns recovery action"
3608
4044
  }
3609
- : {})
4045
+ : handoffDecision
4046
+ ? {
4047
+ handoff_blocked_reason: "the source Session has one active Turn; use only the snapshot-bound handoff_decision after explicit confirmation"
4048
+ }
4049
+ : handoffSourceBlockingTurns.length > 0
4050
+ ? {
4051
+ handoff_blocked_reason: "the conflicting source Session has unresolved Turn state but no snapshot-bound handoff decision is currently safe; inspect the conflict and refresh"
4052
+ }
4053
+ : automatedInputComposerReady !== true
4054
+ ? {
4055
+ handoff_blocked_reason: "the current terminal composer is not an exact empty idle frame"
4056
+ }
4057
+ : {})
3610
4058
  }
3611
4059
  : {}),
3612
4060
  ...(verifiedEmptyCodexSnapshotToken
3613
4061
  ? { handoff_state: "verified_empty_native_session_adoptable" }
3614
4062
  : {}),
3615
4063
  ...(handoffDecision ? { handoff_decision: handoffDecision } : {}),
4064
+ ...(terminalRecoveryBlockingTurns.length > 0
4065
+ ? {
4066
+ blocking_turns: terminalRecoveryBlockingTurns.map((turn) => ({
4067
+ session_id: sessionIdForConversation(turn),
4068
+ turn_id: turnIdForConversation(turn),
4069
+ status: turn.status,
4070
+ recovery_action: {
4071
+ tool: "agent_knock_knock_close",
4072
+ arguments: {
4073
+ turn_id: turnIdForConversation(turn)
4074
+ },
4075
+ requires_explicit_user_confirmation: true
4076
+ }
4077
+ }))
4078
+ }
4079
+ : {}),
3616
4080
  managed: management,
3617
4081
  available_actions: availableActions
3618
4082
  };
@@ -3637,7 +4101,8 @@ function terminalFirstListProjection({ storeDir, terminals, managedSessions, ses
3637
4101
  });
3638
4102
  return {
3639
4103
  ...managedTurn,
3640
- available_actions: mutationsAllowed
4104
+ available_actions: mutationsAllowed &&
4105
+ !conversationHasNonterminalDeferredTransfer(conversation)
3641
4106
  ? safeUnavailableManagedTurnActions(isRecord(managedTurn.available_actions)
3642
4107
  ? managedTurn.available_actions
3643
4108
  : {})
@@ -3850,11 +4315,56 @@ function managedSessionHasUnresolvedNativeTransition(storeDir, session) {
3850
4315
  }
3851
4316
  return false;
3852
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
+ }
3853
4348
  function terminalControlForManagedConversation(conversation) {
3854
4349
  return terminalControlFromTakeover(isRecord(conversation.native_session_takeover)
3855
4350
  ? conversation.native_session_takeover
3856
4351
  : undefined);
3857
4352
  }
4353
+ function terminalIncarnationBlockingTurns(storeDir, terminalControl, conversations = listConversations(storeDir)) {
4354
+ return conversations
4355
+ .filter(isDiscoverableTmuxConversation)
4356
+ .filter((turn) => terminalControlsShareIncarnation(terminalControlForManagedConversation(turn), terminalControl) &&
4357
+ SESSION_SEND_BLOCKING_STATUSES.has(turn.status))
4358
+ .sort(compareManagedConversationRecency);
4359
+ }
4360
+ function assertTerminalIncarnationCanStartTurn(storeDir, terminalControl) {
4361
+ const blocker = terminalIncarnationBlockingTurns(storeDir, terminalControl)[0];
4362
+ if (!blocker) {
4363
+ return;
4364
+ }
4365
+ throw new Error(`terminal ${terminalControl.target} still has unresolved Turn ` +
4366
+ `${turnIdForConversation(blocker)} (${blocker.status})`);
4367
+ }
3858
4368
  function compareManagedConversationRecency(left, right) {
3859
4369
  const leftTime = Date.parse(String(left.updated_at ?? left.created_at ?? ""));
3860
4370
  const rightTime = Date.parse(String(right.updated_at ?? right.created_at ?? ""));
@@ -4074,6 +4584,20 @@ function readOnlyManagedTurn(managedTurn) {
4074
4584
  : {})
4075
4585
  };
4076
4586
  }
4587
+ function withoutGenericHandoffSourceClose(managedTurn, blockingHandoffTurnIds) {
4588
+ const conversationId = stringValue(managedTurn.conversation_id);
4589
+ if (!conversationId || !blockingHandoffTurnIds.has(conversationId)) {
4590
+ return managedTurn;
4591
+ }
4592
+ const availableActions = isRecord(managedTurn.available_actions)
4593
+ ? managedTurn.available_actions
4594
+ : {};
4595
+ const { close: _genericClose, ...safeActions } = availableActions;
4596
+ return {
4597
+ ...managedTurn,
4598
+ available_actions: safeActions
4599
+ };
4600
+ }
4077
4601
  function historicalManagedTurnForTerminal(conversation, terminalCanAcceptSend, terminal) {
4078
4602
  const managedTurn = managedTurnListEntry(summarizeConversation(conversation), {
4079
4603
  terminalBridge: terminalBridgeEnabled(conversation),
@@ -4311,13 +4835,13 @@ function managedListApprovalState(conversation) {
4311
4835
  }
4312
4836
  function listActionContracts() {
4313
4837
  return {
4314
- version: 9,
4838
+ version: 11,
4315
4839
  instructions: [
4316
- "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. That nested action requires explicit user confirmation; after it succeeds, refresh list before any follow-current send.",
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.",
4317
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.",
4318
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.",
4319
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.",
4320
- "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.",
4321
4845
  "List resumable threads before resume; use only a complete native_thread_id and the action returned for that candidate.",
4322
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.",
4323
4847
  "Use respond only for an in-flight turn that is explicitly waiting for OpenClaw.",
@@ -4367,6 +4891,12 @@ function listActionContracts() {
4367
4891
  authoritative_action_path: "terminals[].handoff_decision.choices.take_over_current.action",
4368
4892
  requires_explicit_user_confirmation: true,
4369
4893
  after_success: "refresh list before using a follow-current send action"
4894
+ },
4895
+ blocking_turns: {
4896
+ meaning: "terminal-incarnation-wide collateral unresolved managed Turns that suppress send, lifecycle, and native-inspection actions; an active human-handoff source Turn is never listed here and remains governed only by handoff_decision",
4897
+ authoritative_action_path: "terminals[].blocking_turns[].recovery_action",
4898
+ requires_explicit_user_confirmation: true,
4899
+ after_success: "refresh list before using any terminal action"
4370
4900
  }
4371
4901
  },
4372
4902
  actions: {
@@ -4385,7 +4915,8 @@ function listActionContracts() {
4385
4915
  "agentHardTimeoutMinutes"
4386
4916
  ],
4387
4917
  unsupported: ["timeoutSeconds"],
4388
- 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."
4389
4920
  },
4390
4921
  new_thread: {
4391
4922
  tool: "agent_knock_knock_new_thread",
@@ -4745,7 +5276,7 @@ async function sessionSelectorCandidates(commandName, options) {
4745
5276
  managedSessions: selectorStore.readable
4746
5277
  ? listManagedSessions(storeDir).filter((session) => matchesConfiguredWorkspace(options.workspace, session.workspace))
4747
5278
  : [],
4748
- sessionAuthorityRequired: selectorStore.writer_protocol === 3,
5279
+ sessionAuthorityRequired: Number(selectorStore.writer_protocol) >= STORE_SESSION_AUTHORITY_PROTOCOL,
4749
5280
  allConversations: discoverableWorkspaceConversations,
4750
5281
  displayedConversations: discoverableWorkspaceConversations,
4751
5282
  includeAll: false,
@@ -5473,14 +6004,9 @@ async function verifyCodexPendingManagedSendStatus({ options, terminal, session,
5473
6004
  if (!codexComposerVisible(initialStatus.screen.excerpt)) {
5474
6005
  throw new Error("Codex status-card verification requires a visible idle composer");
5475
6006
  }
5476
- await assertCodexComposerReadyForAutomatedInput({
5477
- options,
5478
- terminalControl: terminal.terminalControl
5479
- });
5480
6007
  const observed = await probeCodexCurrentThread({
5481
6008
  options,
5482
6009
  terminal,
5483
- initialStatus,
5484
6010
  currentIdentity: logicalIdentity,
5485
6011
  runtimeOverride: runtime
5486
6012
  });
@@ -5709,7 +6235,7 @@ function materializeCurrentManagedSession({ options, terminal, identity }) {
5709
6235
  }
5710
6236
  return existingById;
5711
6237
  }
5712
- 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 ` +
5713
6239
  "authoritative Session state is missing");
5714
6240
  }
5715
6241
  async function reattachManagedSessionForNativeIdentity({ options, terminal, identity, storeDir }) {
@@ -5795,7 +6321,28 @@ function exactBoundCodexSourceForVerifiedEmptyHandoff({ session, terminalId, ter
5795
6321
  liveProcessBirth: processBirth
5796
6322
  }) === "same");
5797
6323
  }
5798
- function verifiedEmptyCodexHandoffToken({ terminalId, terminalControl, pid, workspace, processUuid, processBirth, sourceSession }) {
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 }) {
5799
6346
  const terminalToken = unmanagedTerminalBindingToken({
5800
6347
  terminalId,
5801
6348
  terminalControl,
@@ -5807,49 +6354,159 @@ function verifiedEmptyCodexHandoffToken({ terminalId, terminalControl, pid, work
5807
6354
  });
5808
6355
  return createHash("sha256")
5809
6356
  .update(JSON.stringify({
5810
- version: 1,
5811
- kind: "verified_empty_codex_handoff",
6357
+ version: 2,
6358
+ kind: "deferred_codex_foreground_binding",
5812
6359
  terminal_token: terminalToken,
5813
6360
  source_session_id: sourceSession.session_id,
5814
6361
  source_revision: managedSessionRevision(sourceSession),
5815
6362
  source_binding_token: managedSessionBindingToken(sourceSession),
6363
+ terminal_dispatch_snapshot: dispatchSnapshot,
5816
6364
  observation: "verified_absent"
5817
6365
  }))
5818
6366
  .digest("hex");
5819
6367
  }
5820
- async function assertVerifiedEmptyCodexHandoffBoundary({ options, terminal, sourceSession, expectedSourceStatus, requireNoDispatch, requireEmptyComposer = true }) {
5821
- const currentSource = loadManagedSession(storeDirFromOptions(options), sourceSession.session_id);
5822
- if (currentSource.status !== expectedSourceStatus ||
5823
- currentSource.revision !== sourceSession.revision ||
5824
- managedSessionBindingToken(currentSource) !==
5825
- managedSessionBindingToken(sourceSession)) {
5826
- throw new Error("the verified-empty source Session changed; refresh AKK list");
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
+ };
5827
6381
  }
5828
- const liveIncarnation = codexProcessIncarnationForPid(terminal.pid);
5829
- if (!exactBoundCodexSourceForVerifiedEmptyHandoff({
5830
- session: {
5831
- ...currentSource,
5832
- // The exact source binding remains authoritative after the monotonic
5833
- // detach; only the Session status changes.
5834
- status: "bound"
5835
- },
5836
- terminalId: terminal.conversationId,
5837
- terminalControl: terminal.terminalControl,
5838
- pid: terminal.pid,
5839
- workspace: terminal.terminalControl.currentPath,
5840
- processUuid: liveIncarnation.processUuid,
5841
- processBirth: liveIncarnation.processBirth
5842
- })) {
5843
- throw new Error("the verified-empty source binding no longer matches the terminal");
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");
5844
6394
  }
5845
- const observation = await observeCurrentNativeAgentSessionIdentity({
5846
- options,
5847
- agent: "codex",
5848
- pid: terminal.pid,
5849
- cwd: terminal.terminalControl.currentPath
5850
- });
5851
- if (observation.status === "unavailable") {
5852
- throw new Error(`Codex native identity observation is unavailable: ${observation.reason}`);
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
+ }
6455
+ function verifiedEmptyCodexHandoffToken({ terminalId, terminalControl, pid, workspace, processUuid, processBirth, sourceSession }) {
6456
+ const terminalToken = unmanagedTerminalBindingToken({
6457
+ terminalId,
6458
+ terminalControl,
6459
+ agent: "codex",
6460
+ pid,
6461
+ workspace,
6462
+ processUuid,
6463
+ processBirth
6464
+ });
6465
+ return createHash("sha256")
6466
+ .update(JSON.stringify({
6467
+ version: 1,
6468
+ kind: "verified_empty_codex_handoff",
6469
+ terminal_token: terminalToken,
6470
+ source_session_id: sourceSession.session_id,
6471
+ source_revision: managedSessionRevision(sourceSession),
6472
+ source_binding_token: managedSessionBindingToken(sourceSession),
6473
+ observation: "verified_absent"
6474
+ }))
6475
+ .digest("hex");
6476
+ }
6477
+ async function assertVerifiedEmptyCodexHandoffBoundary({ options, terminal, sourceSession, expectedSourceStatus, requireNoDispatch, requireEmptyComposer = true }) {
6478
+ const currentSource = loadManagedSession(storeDirFromOptions(options), sourceSession.session_id);
6479
+ if (currentSource.status !== expectedSourceStatus ||
6480
+ currentSource.revision !== sourceSession.revision ||
6481
+ managedSessionBindingToken(currentSource) !==
6482
+ managedSessionBindingToken(sourceSession)) {
6483
+ throw new Error("the verified-empty source Session changed; refresh AKK list");
6484
+ }
6485
+ const liveIncarnation = codexProcessIncarnationForPid(terminal.pid);
6486
+ if (!exactBoundCodexSourceForVerifiedEmptyHandoff({
6487
+ session: {
6488
+ ...currentSource,
6489
+ // The exact source binding remains authoritative after the monotonic
6490
+ // detach; only the Session status changes.
6491
+ status: "bound"
6492
+ },
6493
+ terminalId: terminal.conversationId,
6494
+ terminalControl: terminal.terminalControl,
6495
+ pid: terminal.pid,
6496
+ workspace: terminal.terminalControl.currentPath,
6497
+ processUuid: liveIncarnation.processUuid,
6498
+ processBirth: liveIncarnation.processBirth
6499
+ })) {
6500
+ throw new Error("the verified-empty source binding no longer matches the terminal");
6501
+ }
6502
+ const observation = await observeCurrentNativeAgentSessionIdentity({
6503
+ options,
6504
+ agent: "codex",
6505
+ pid: terminal.pid,
6506
+ cwd: terminal.terminalControl.currentPath
6507
+ });
6508
+ if (observation.status === "unavailable") {
6509
+ throw new Error(`Codex native identity observation is unavailable: ${observation.reason}`);
5853
6510
  }
5854
6511
  if (observation.status !== "verified_absent") {
5855
6512
  throw new Error(`Codex materialized native thread ${observation.identity.sessionId}; ` +
@@ -5987,102 +6644,963 @@ async function assertVerifiedEmptyCodexTransportBoundary({ options, boundary, re
5987
6644
  requireEmptyComposer
5988
6645
  });
5989
6646
  }
5990
- function observedHandoffAuthorityToken({ terminal, identity, sourceSession, target }) {
5991
- const exact = exactLifecycleProcessIdentity(terminal, identity);
5992
- const terminalToken = unmanagedTerminalBindingToken({
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
6687
+ });
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
+ })
6709
+ });
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})`);
6716
+ }
6717
+ if (requireEmptyComposer) {
6718
+ await assertCodexComposerReadyForAutomatedInput({
6719
+ options,
6720
+ terminalControl: boundary.terminal.terminalControl
6721
+ });
6722
+ }
6723
+ return source;
6724
+ }
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;
6732
+ }
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;
6751
+ }
6752
+ const dispatchSnapshot = deferredCodexForegroundDispatchSnapshot(terminal.terminalControl);
6753
+ const token = deferredCodexForegroundBindingToken({
5993
6754
  terminalId: terminal.conversationId,
5994
6755
  terminalControl: terminal.terminalControl,
5995
- agent: terminal.agent,
5996
6756
  pid: terminal.pid,
5997
- workspace: terminal.terminalControl.currentPath ?? cliCwd(),
5998
- nativeThreadId: exact.sessionId,
5999
- processUuid: exact.processUuid,
6000
- processBirth: exact.processBirth,
6001
- rollout: exact.rollout
6757
+ workspace,
6758
+ processUuid,
6759
+ processBirth,
6760
+ sourceSession,
6761
+ dispatchSnapshot
6002
6762
  });
6003
- return humanObservedHandoffBindingToken({
6004
- terminal_token: terminalToken,
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,
6005
6817
  source_session_id: sourceSession.session_id,
6006
- source_revision: managedSessionRevision(sourceSession),
6818
+ source_expected_revision: managedSessionRevision(sourceSession),
6007
6819
  source_binding_token: managedSessionBindingToken(sourceSession),
6008
- target
6009
- });
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;
6010
6831
  }
6011
- function activeTurnHandoffDecisionToken({ handoffToken, turn, ledger }) {
6012
- const takeover = isRecord(turn.native_session_takeover)
6013
- ? turn.native_session_takeover
6014
- : undefined;
6015
- const submission = terminalBridgeSubmission(turn);
6016
- return createHash("sha256")
6017
- .update(JSON.stringify({
6018
- version: 1,
6019
- kind: "active_turn_human_handoff",
6020
- handoff_token: handoffToken,
6021
- session_id: sessionIdForConversation(turn),
6022
- turn_id: turnIdForConversation(turn),
6023
- turn_status: turn.status,
6024
- turn_updated_at: turn.updated_at ?? null,
6025
- current_message_id: stringValue(takeover?.terminal_bridge_message_id) ??
6026
- stringValue(submission?.message_id) ??
6027
- null,
6028
- ledger_generation_id: stringValue(ledger?.generation_id) ?? null,
6029
- ledger_message_id: stringValue(ledger?.message_id) ?? null,
6030
- ledger_status: stringValue(ledger?.status) ?? null
6031
- }))
6032
- .digest("hex");
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;
6033
6838
  }
6034
- const originalExpectedTerminalSelector = new WeakMap();
6035
- function assertExpectedHandoffTokenUsesExactTerminalSelector({ options, terminal }) {
6036
- if (!stringValue(options.expectedTerminalToken)) {
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") {
6037
6862
  return;
6038
6863
  }
6039
- const supplied = originalExpectedTerminalSelector.has(options)
6040
- ? originalExpectedTerminalSelector.get(options)
6041
- : stringValue(options.session ?? options.conversation ?? options.conversationId)?.trim();
6042
- if (supplied !== terminal.conversationId) {
6043
- throw new Error("--expected-terminal-token is valid only with the exact full terminal " +
6044
- "conversation selector advertised by AKK list");
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");
6045
6871
  }
6046
- }
6047
- async function observedExternalHandoffIdentity({ options, terminal, sourceSession, resolvedIdentity }) {
6048
- const bridge = createTerminalAgentBridge(options);
6049
- const status = await bridge.status(terminal.agent, terminal.terminalControl, { runtime: terminalRuntimeForLiveIdentity({ terminal, physicalOnly: true }) });
6050
- assertSafeTerminalSend(terminal.agent, status);
6051
- if (terminal.agent !== "codex") {
6052
- return { identity: resolvedIdentity, status };
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) });
6053
6896
  }
6054
- const sourceBinding = sourceSession.binding;
6055
- const statusCard = terminal.adapter.observeThreadLifecycle?.({
6056
- operation: { kind: "new_thread" },
6057
- phase: "before",
6058
- screen: status.screen.excerpt ?? ""
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)
6059
6907
  });
6060
- const statusCardId = statusCard?.status === "observed" &&
6061
- isExactNativeThreadId(statusCard.nativeThreadId)
6062
- ? statusCard.nativeThreadId.toLowerCase()
6063
- : undefined;
6064
- const sourceId = sourceBinding?.native_thread_id?.toLowerCase();
6065
- if (statusCardId && sourceId && statusCardId !== sourceId) {
6066
- const processUuid = sourceBinding?.native_process.process_uuid;
6067
- const processBirth = sourceBinding?.native_process.process_birth;
6068
- if (!processUuid || !processBirth) {
6069
- return { identity: undefined, status };
6070
- }
6071
- const resolvedMatchesStatus = resolvedIdentity?.sessionId.toLowerCase() === statusCardId;
6072
- return {
6073
- identity: {
6074
- sessionId: statusCardId,
6075
- processUuid,
6076
- processBirth,
6077
- rollout: resolvedMatchesStatus ? resolvedIdentity?.rollout : undefined,
6078
- evidence: resolvedMatchesStatus
6079
- ? `${resolvedIdentity?.evidence ?? "native_thread_boundary"}+codex_status_card`
6080
- : statusCard?.evidence ?? "codex_status_card"
6081
- },
6082
- status
6083
- };
6084
- }
6085
- return { identity: resolvedIdentity, status };
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;
7585
+ const processBirth = sourceBinding?.native_process.process_birth;
7586
+ if (!processUuid || !processBirth) {
7587
+ return { identity: undefined, status };
7588
+ }
7589
+ const resolvedMatchesStatus = resolvedIdentity?.sessionId.toLowerCase() === statusCardId;
7590
+ return {
7591
+ identity: {
7592
+ sessionId: statusCardId,
7593
+ processUuid,
7594
+ processBirth,
7595
+ rollout: resolvedMatchesStatus ? resolvedIdentity?.rollout : undefined,
7596
+ evidence: resolvedMatchesStatus
7597
+ ? `${resolvedIdentity?.evidence ?? "native_thread_boundary"}+codex_status_card`
7598
+ : statusCard?.evidence ?? "codex_status_card"
7599
+ },
7600
+ status
7601
+ };
7602
+ }
7603
+ return { identity: resolvedIdentity, status };
6086
7604
  }
6087
7605
  function observedHandoffTargetResolution({ storeDir, agent, workspace, nativeThreadId, sourceSessionId }) {
6088
7606
  const matches = listManagedSessions(storeDir).filter((session) => session.session_id !== sourceSessionId &&
@@ -6619,10 +8137,7 @@ function assertTerminalLifecycleReady({ options, terminal, terminalStatus }) {
6619
8137
  throw new Error(`terminal ${terminal.terminalControl.target} is not at a verified idle prompt ` +
6620
8138
  `(${terminalStatus.activity_state}: ${terminalStatus.activity_reason})`);
6621
8139
  }
6622
- const blockers = listConversations(storeDirFromOptions(options))
6623
- .filter(isDiscoverableTmuxConversation)
6624
- .filter((turn) => terminalControlsShareIncarnation(terminalControlForManagedConversation(turn), terminal.terminalControl) &&
6625
- SESSION_SEND_BLOCKING_STATUSES.has(turn.status));
8140
+ const blockers = terminalIncarnationBlockingTurns(storeDirFromOptions(options), terminal.terminalControl);
6626
8141
  if (blockers.length > 0) {
6627
8142
  throw new Error(`terminal ${terminal.terminalControl.target} still has unresolved Turn ` +
6628
8143
  `${turnIdForConversation(blockers[0])} (${blockers[0].status})`);
@@ -7302,10 +8817,7 @@ function assertTerminalNativeInspectionReady({ options, terminal, terminalStatus
7302
8817
  throw new Error(`terminal ${terminal.terminalControl.target} is not at a verified idle prompt ` +
7303
8818
  `(${terminalStatus.activity_state}: ${terminalStatus.activity_reason})`);
7304
8819
  }
7305
- const blocker = listConversations(storeDirFromOptions(options))
7306
- .filter(isDiscoverableTmuxConversation)
7307
- .find((turn) => terminalControlsShareIncarnation(terminalControlForManagedConversation(turn), terminal.terminalControl) &&
7308
- SESSION_SEND_BLOCKING_STATUSES.has(turn.status));
8820
+ const blocker = terminalIncarnationBlockingTurns(storeDirFromOptions(options), terminal.terminalControl)[0];
7309
8821
  if (blocker) {
7310
8822
  throw new Error(`terminal ${terminal.terminalControl.target} still has unresolved Turn ` +
7311
8823
  `${turnIdForConversation(blocker)} (${blocker.status})`);
@@ -7749,6 +9261,7 @@ async function waitForVerifiedThreadTransition({ options, terminal, beforeIdenti
7749
9261
  const bridge = createTerminalAgentBridge(options);
7750
9262
  let probeSent = false;
7751
9263
  let postProbeBaselineDigest;
9264
+ let postProbeObservationScrollbackLines;
7752
9265
  let stableIdentity;
7753
9266
  let stableCount = 0;
7754
9267
  const physicalBeforeFence = codexIdentityFence(physicalBeforeIdentity);
@@ -7816,22 +9329,37 @@ async function waitForVerifiedThreadTransition({ options, terminal, beforeIdenti
7816
9329
  }
7817
9330
  else {
7818
9331
  const afterProbe = plan.steps.find((step) => step.kind === "identity_probe_after");
7819
- if (!afterProbe || afterProbe.effect !== "read_only") {
9332
+ if (!afterProbe ||
9333
+ afterProbe.effect !== "read_only" ||
9334
+ afterProbe.command !== "/status") {
7820
9335
  throw new Error("the Codex lifecycle plan has no exact post-transition identity probe");
7821
9336
  }
9337
+ const agentVersion = agentVersionForRunningProcess(terminal.agent, terminal.pid, options);
9338
+ if (!agentVersion) {
9339
+ throw new Error("Codex lifecycle postcondition /status requires the exact running version before terminal input");
9340
+ }
7822
9341
  const physicalRuntime = terminalRuntimeForLiveIdentity({
7823
9342
  terminal,
7824
9343
  physicalOnly: true
7825
9344
  });
7826
- const status = await bridge.status(terminal.agent, terminal.terminalControl, { runtime: physicalRuntime });
9345
+ const status = await bridge.status(terminal.agent, terminal.terminalControl, {
9346
+ runtime: physicalRuntime,
9347
+ ...(postProbeObservationScrollbackLines === undefined
9348
+ ? {}
9349
+ : {
9350
+ scrollbackLines: postProbeObservationScrollbackLines
9351
+ })
9352
+ });
7827
9353
  const screenDigest = stringValue(status.screen.digest);
7828
9354
  if (!probeSent &&
7829
9355
  status.reachable &&
7830
9356
  status.activity_state === "idle" &&
7831
9357
  screenDigest !== undefined &&
7832
9358
  screenDigest !== initialScreenDigest) {
7833
- postProbeBaselineDigest = screenDigest;
7834
- await bridge.send(terminal.agent, terminal.terminalControl, afterProbe.command, { runtime: physicalRuntime });
9359
+ const submission = await bridge.submitCodexStatusProbe(terminal.terminalControl, agentVersion, { runtime: physicalRuntime });
9360
+ postProbeBaselineDigest = submission.observationBaselineDigest;
9361
+ postProbeObservationScrollbackLines =
9362
+ submission.observationScrollbackLines;
7835
9363
  probeSent = true;
7836
9364
  }
7837
9365
  else if (status.reachable &&
@@ -7960,7 +9488,7 @@ function assertVerifiedClaudeLifecycleBeforeObservation({ options, terminal, ide
7960
9488
  `for the current process incarnation${observed?.reason ? `: ${observed.reason}` : ""}`);
7961
9489
  }
7962
9490
  }
7963
- async function probeCodexCurrentThread({ options, terminal, initialStatus, currentIdentity, runtimeIdentity, runtimeOverride }) {
9491
+ async function probeCodexCurrentThread({ options, terminal, currentIdentity, runtimeIdentity, runtimeOverride }) {
7964
9492
  if (terminal.agent !== "codex") {
7965
9493
  throw new Error("the current native thread identity is unavailable");
7966
9494
  }
@@ -7972,24 +9500,31 @@ async function probeCodexCurrentThread({ options, terminal, initialStatus, curre
7972
9500
  terminal,
7973
9501
  expectedEmptyNativeSession: true
7974
9502
  }));
7975
- // The plain activity parser intentionally recognizes Codex's composer even
7976
- // when a footer follows it, so "idle" alone is not authority to press Enter.
7977
- // Re-read the styled pane immediately before the slash probe and accept only
7978
- // an empty composer (or Codex's fully dim replace-on-type placeholder).
7979
- await assertCodexComposerReadyForAutomatedInput({
7980
- options,
7981
- terminalControl: terminal.terminalControl
7982
- });
7983
- await bridge.send(terminal.agent, terminal.terminalControl, "/status", { runtime: probeRuntime });
9503
+ const agentVersion = agentVersionForRunningProcess(terminal.agent, terminal.pid, options);
9504
+ if (!agentVersion) {
9505
+ throw new Error("Codex /status requires the exact running version before terminal input");
9506
+ }
9507
+ // Internal status probes use the same closed submission boundary as native
9508
+ // inspection: exact viewport and styled-empty composer proof, Codex's paste
9509
+ // suppression settle, identity revalidation, and exactly one Enter.
9510
+ const submission = await bridge.submitCodexStatusProbe(terminal.terminalControl, agentVersion, { runtime: probeRuntime });
9511
+ let lastObservationReason = "no fresh idle Codex /status card was captured after the one Enter dispatch";
9512
+ let sawIncompleteSessionUuid = false;
7984
9513
  for (let attempt = 0; attempt < 50; attempt += 1) {
7985
- const status = await bridge.status(terminal.agent, terminal.terminalControl, { runtime: probeRuntime });
9514
+ const status = await bridge.status(terminal.agent, terminal.terminalControl, {
9515
+ runtime: probeRuntime,
9516
+ scrollbackLines: submission.observationScrollbackLines
9517
+ });
7986
9518
  if (status.reachable &&
7987
- status.screen.digest !== initialStatus.screen.digest) {
9519
+ status.screen.digest !== submission.observationBaselineDigest) {
7988
9520
  const observed = terminal.adapter.observeThreadLifecycle?.({
7989
9521
  operation: { kind: "new_thread" },
7990
9522
  phase: "before",
7991
9523
  screen: status.screen.excerpt ?? ""
7992
9524
  });
9525
+ lastObservationReason = observed?.reason ?? lastObservationReason;
9526
+ sawIncompleteSessionUuid ||= observed?.status === "missing" &&
9527
+ /complete Session UUID/iu.test(observed.reason ?? "");
7993
9528
  if (observed?.status === "observed" &&
7994
9529
  observed.nativeThreadId &&
7995
9530
  status.activity_state === "idle") {
@@ -8006,7 +9541,12 @@ async function probeCodexCurrentThread({ options, terminal, initialStatus, curre
8006
9541
  }
8007
9542
  await cliSleep(100);
8008
9543
  }
8009
- throw new Error("Codex /status did not expose a complete current Session UUID");
9544
+ throw new Error(sawIncompleteSessionUuid
9545
+ ? "Codex /status Enter was dispatched exactly once, but the status card " +
9546
+ "was truncated before the complete current Session UUID; widen or zoom " +
9547
+ "the pane, refresh AKK list, and do not retry the probe automatically"
9548
+ : "Codex /status Enter was dispatched exactly once, but no fresh exact " +
9549
+ `current Session UUID was proven; do not retry automatically: ${lastObservationReason}`);
8010
9550
  }
8011
9551
  async function runNewThread(options) {
8012
9552
  const requireRestorableOrigin = options.requireRestorableOrigin;
@@ -8363,7 +9903,6 @@ async function runNativeThreadTransition(options, operation) {
8363
9903
  const statusIdentity = await probeCodexCurrentThread({
8364
9904
  options,
8365
9905
  terminal,
8366
- initialStatus: terminalStatus,
8367
9906
  currentIdentity: beforeIdentity,
8368
9907
  runtimeIdentity: snapshot.runtimeIdentity,
8369
9908
  runtimeOverride: beforeRuntime
@@ -9027,17 +10566,21 @@ async function reconcileStoreForStatus(storeDir, options, conversationId) {
9027
10566
  }
9028
10567
  throw error;
9029
10568
  }
9030
- const idle = reconcileIdleConversations(storeDir, options, cliNow(), conversationId);
9031
10569
  const monitors = await reconcileMonitors(options, {
9032
10570
  includeCallbackRecovery: false,
9033
10571
  reason: "status_reconciliation",
9034
10572
  conversationId
9035
10573
  });
10574
+ const idle = reconcileIdleConversations(storeDir, options, cliNow(), conversationId);
9036
10575
  return {
9037
10576
  status: "completed",
9038
10577
  checked: Math.max(idle.checked, monitors.checked),
9039
- changed: idle.closed + monitors.launched,
10578
+ changed: idle.closed + monitors.launched + monitors.repaired,
9040
10579
  closed: idle.closed,
10580
+ repaired: monitors.repaired,
10581
+ collateral_stalls_checked: monitors.collateral_stalls_checked,
10582
+ collateral_stalls_skipped: monitors.collateral_stalls_skipped,
10583
+ collateral_stall_repairs: monitors.items.filter((item) => item.status === "repaired"),
9041
10584
  monitors_launched: monitors.launched,
9042
10585
  monitors_already_running: monitors.already_running,
9043
10586
  skipped: idle.skipped + monitors.skipped,
@@ -9564,6 +11107,7 @@ async function runSend(options) {
9564
11107
  })) {
9565
11108
  return;
9566
11109
  }
11110
+ assertTerminalIncarnationCanStartTurn(rawStoreDir, terminalConversation.terminalControl);
9567
11111
  let claimedSession = soleBoundManagedSessionClaimForTerminal(rawStoreDir, terminalConversation);
9568
11112
  let knownCodexCompanions = claimedSession
9569
11113
  ? codexAllowedCompanionSetForManagedSession({
@@ -9603,7 +11147,7 @@ async function runSend(options) {
9603
11147
  currentNativeIdentity = undefined;
9604
11148
  }
9605
11149
  const physicalNativeIdentityBeforeHandoff = currentNativeIdentity;
9606
- const handoff = await maybeAdoptObservedExternalThread({
11150
+ let handoff = await maybeAdoptObservedExternalThread({
9607
11151
  options,
9608
11152
  terminal: terminalConversation,
9609
11153
  sourceSession: claimedSession,
@@ -9616,12 +11160,35 @@ async function runSend(options) {
9616
11160
  !handoff.session?.binding?.native_process.rollout
9617
11161
  ? physicalNativeIdentityBeforeHandoff
9618
11162
  : handoff.identity;
9619
- let managedSession = handoff.session ??
9620
- materializeCurrentManagedSession({
11163
+ const deferredCodexForegroundBinding = !handoff.adopted
11164
+ ? await prepareDeferredCodexForegroundBinding({
9621
11165
  options,
9622
11166
  terminal: terminalConversation,
9623
- identity: currentNativeIdentity
9624
- });
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
+ });
9625
11192
  if (!managedSession && currentNativeIdentity) {
9626
11193
  managedSession = await reattachManagedSessionForNativeIdentity({
9627
11194
  options,
@@ -9643,13 +11210,29 @@ async function runSend(options) {
9643
11210
  });
9644
11211
  }
9645
11212
  managedSession = createBoundManagedSession({
9646
- sessionId: createManagedSessionId(),
11213
+ sessionId: deferredCodexForegroundBinding?.targetSessionId ??
11214
+ createManagedSessionId(),
9647
11215
  terminal: terminalConversation,
9648
11216
  identity: currentNativeIdentity,
9649
- 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" }
9650
11224
  });
9651
11225
  if (terminalConversation.agent === "codex" &&
9652
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
+ }
9653
11236
  pendingRawAttachSessionCreate = managedSession;
9654
11237
  }
9655
11238
  else {
@@ -9718,7 +11301,8 @@ async function runSend(options) {
9718
11301
  terminalControl: terminalConversation.terminalControl,
9719
11302
  previousTurn: reusableTurn,
9720
11303
  managedSession,
9721
- nativeAgentIdentity: materializedNativeIdentity
11304
+ nativeAgentIdentity: materializedNativeIdentity,
11305
+ deferredForegroundTransferId: deferredCodexForegroundBinding?.transferId
9722
11306
  });
9723
11307
  ensureStoreWritable(managed.conversation.store_dir);
9724
11308
  ensureDir(path.dirname(managed.statePath));
@@ -9739,8 +11323,30 @@ async function runSend(options) {
9739
11323
  recordMessageAfterSend: true,
9740
11324
  recordRawAttachmentAfterSend: reusableTurn === undefined,
9741
11325
  onTerminalPreflightVerified: pendingRawAttachSessionCreate
9742
- ? () => {
9743
- 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
+ }
9744
11350
  managedSession = createdSession;
9745
11351
  pendingRawAttachSessionCreate = undefined;
9746
11352
  return () => {
@@ -9771,7 +11377,8 @@ async function runSend(options) {
9771
11377
  transition: handoff.transition
9772
11378
  }
9773
11379
  : undefined,
9774
- verifiedEmptyCodexHandoff: verifiedEmptyHandoff?.boundary
11380
+ verifiedEmptyCodexHandoff: verifiedEmptyHandoff?.boundary,
11381
+ deferredCodexForegroundBinding
9775
11382
  });
9776
11383
  }
9777
11384
  finally {
@@ -9854,6 +11461,7 @@ async function runSend(options) {
9854
11461
  })) {
9855
11462
  return;
9856
11463
  }
11464
+ assertTerminalIncarnationCanStartTurn(storeDir, resolvedTerminal.terminalControl);
9857
11465
  let currentSession = tryLoadManagedSession(storeDir, sessionId);
9858
11466
  const knownCodexCompanions = currentSession
9859
11467
  ? codexAllowedCompanionSetForManagedSession({
@@ -10355,6 +11963,7 @@ async function runApprove(options) {
10355
11963
  throw new Error("approval state changed while waiting for terminal control; refresh status and retry");
10356
11964
  }
10357
11965
  assertManagedTerminalDispatchOwner({
11966
+ storeDir: writerStoreDir,
10358
11967
  conversation: currentConversation,
10359
11968
  terminalControl: currentControl,
10360
11969
  action: "approve"
@@ -10730,7 +12339,7 @@ async function runTerminalConversationApprove({ options, conversationId, agent,
10730
12339
  releaseTerminalLock();
10731
12340
  }
10732
12341
  }
10733
- 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 }) {
10734
12343
  const bridge = terminalBridgeEnabled(conversation);
10735
12344
  if (!terminalSendLockHeld) {
10736
12345
  const releaseTerminalLock = acquireTerminalBridgeSendLock(storeDirFromOptions(options), terminalControl, { timeoutMs: 30000 });
@@ -10754,6 +12363,7 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
10754
12363
  allowedAdditionalIdentities,
10755
12364
  observedHandoff,
10756
12365
  verifiedEmptyCodexHandoff,
12366
+ deferredCodexForegroundBinding,
10757
12367
  continuingTurnResponse
10758
12368
  });
10759
12369
  }
@@ -10795,6 +12405,7 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
10795
12405
  allowedAdditionalIdentities,
10796
12406
  observedHandoff,
10797
12407
  verifiedEmptyCodexHandoff,
12408
+ deferredCodexForegroundBinding,
10798
12409
  continuingTurnResponse
10799
12410
  });
10800
12411
  }
@@ -10823,6 +12434,7 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
10823
12434
  allowedAdditionalIdentities,
10824
12435
  observedHandoff,
10825
12436
  verifiedEmptyCodexHandoff,
12437
+ deferredCodexForegroundBinding,
10826
12438
  continuingTurnResponse
10827
12439
  }));
10828
12440
  }
@@ -10831,7 +12443,7 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
10831
12443
  const agentTimeoutMinutes = Number(options.agentTimeoutMinutes ?? DEFAULT_AGENT_TIMEOUT_MINUTES);
10832
12444
  const agentHardTimeoutMinutes = positiveMinutes(options.agentHardTimeoutMinutes ?? DEFAULT_AGENT_HARD_TIMEOUT_MINUTES, "--agent-hard-timeout-minutes");
10833
12445
  const terminalPayload = terminalSubmissionPayload(String(message.body ?? ""));
10834
- const terminalRequestHash = terminalBridgeRequestFingerprint(terminalPayload);
12446
+ const terminalRequestHash = required(terminalBridgeRequestFingerprint(terminalPayload), "terminal request hash is unavailable");
10835
12447
  let previousDispatchLedger = resolveTerminalDispatchLedgerPaneIncarnation(terminalControl, loadTerminalBridgeDispatchLedger(terminalControl));
10836
12448
  previousDispatchLedger =
10837
12449
  reconcilePreparedTerminalDispatchLedger(terminalControl, previousDispatchLedger);
@@ -11004,12 +12616,12 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
11004
12616
  scrollbackLines: Number(options.scrollbackLines ?? 120),
11005
12617
  runtime: preSendRuntime
11006
12618
  });
11007
- if (verifiedEmptyCodexHandoff) {
12619
+ if (verifiedEmptyCodexHandoff || deferredCodexForegroundBinding) {
11008
12620
  if (executor.kind !== "codex" ||
11009
12621
  status.reachable !== true ||
11010
12622
  status.approval_state.blocked === true ||
11011
12623
  !["idle", "unknown"].includes(status.activity_state)) {
11012
- 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 ` +
11013
12625
  `(${status.activity_state}: ${status.activity_reason})`);
11014
12626
  }
11015
12627
  }
@@ -11074,7 +12686,7 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
11074
12686
  // acceptance check has passed, but before the Turn or dispatch ledger can
11075
12687
  // become durable. This prevents a failed virgin attach from leaving a
11076
12688
  // zero-identity `bound` Session that fences every later control action.
11077
- let rollbackPreTransportAttach = onTerminalPreflightVerified?.();
12689
+ let rollbackPreTransportAttach = await onTerminalPreflightVerified?.();
11078
12690
  const rollbackRawAttachBeforeTransport = () => {
11079
12691
  if (!rollbackPreTransportAttach) {
11080
12692
  return true;
@@ -11094,6 +12706,39 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
11094
12706
  return false;
11095
12707
  }
11096
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
+ };
11097
12742
  const bridgeConversation = bridge
11098
12743
  ? withTerminalBridgeState({
11099
12744
  conversation: nextConversation,
@@ -11139,11 +12784,13 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
11139
12784
  }
11140
12785
  catch (error) {
11141
12786
  try {
11142
- restoreTerminalBridgeDispatchLedger({
11143
- terminalControl,
11144
- previousLedger: previousDispatchLedger,
11145
- reason: "prepared ledger persistence failed before terminal input"
11146
- });
12787
+ if (!abortDeferredPreInputBeforeTransport()) {
12788
+ restoreTerminalBridgeDispatchLedger({
12789
+ terminalControl,
12790
+ previousLedger: previousDispatchLedger,
12791
+ reason: "prepared ledger persistence failed before terminal input"
12792
+ });
12793
+ }
11147
12794
  }
11148
12795
  finally {
11149
12796
  rollbackRawAttachBeforeTransport();
@@ -11155,11 +12802,13 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
11155
12802
  }
11156
12803
  catch (error) {
11157
12804
  try {
11158
- restoreTerminalBridgeDispatchLedger({
11159
- terminalControl,
11160
- previousLedger: previousDispatchLedger,
11161
- reason: "prepared state persistence failed before terminal input"
11162
- });
12805
+ if (!abortDeferredPreInputBeforeTransport()) {
12806
+ restoreTerminalBridgeDispatchLedger({
12807
+ terminalControl,
12808
+ previousLedger: previousDispatchLedger,
12809
+ reason: "prepared state persistence failed before terminal input"
12810
+ });
12811
+ }
11163
12812
  }
11164
12813
  finally {
11165
12814
  rollbackRawAttachBeforeTransport();
@@ -11220,11 +12869,13 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
11220
12869
  const errorMessage = error instanceof Error ? error.message : String(error);
11221
12870
  let dispatchLedgerRestored = true;
11222
12871
  try {
11223
- restoreTerminalBridgeDispatchLedger({
11224
- terminalControl,
11225
- previousLedger: previousDispatchLedger,
11226
- reason: "terminal submission aborted before terminal input"
11227
- });
12872
+ if (!abortDeferredPreInputBeforeTransport()) {
12873
+ restoreTerminalBridgeDispatchLedger({
12874
+ terminalControl,
12875
+ previousLedger: previousDispatchLedger,
12876
+ reason: "terminal submission aborted before terminal input"
12877
+ });
12878
+ }
11228
12879
  }
11229
12880
  catch (ledgerError) {
11230
12881
  dispatchLedgerRestored = false;
@@ -11386,7 +13037,8 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
11386
13037
  await terminalBridge.send(executor.kind, terminalControl, terminalPayload, {
11387
13038
  runtime: preSendRuntime,
11388
13039
  requireExactComposerBeforeEnter: observedHandoff !== undefined ||
11389
- verifiedEmptyCodexHandoff !== undefined,
13040
+ verifiedEmptyCodexHandoff !== undefined ||
13041
+ deferredCodexForegroundBinding !== undefined,
11390
13042
  beforeText: observedHandoff
11391
13043
  ? async () => {
11392
13044
  await assertObservedHandoffTransportBoundary({
@@ -11404,7 +13056,22 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
11404
13056
  requireEmptyComposer: true
11405
13057
  });
11406
13058
  }
11407
- : 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,
11408
13075
  beforeEnter: observedHandoff
11409
13076
  ? async () => {
11410
13077
  await assertObservedHandoffTransportBoundary({
@@ -11422,7 +13089,17 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
11422
13089
  requireEmptyComposer: false
11423
13090
  });
11424
13091
  }
11425
- : 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,
11426
13103
  async onTransportStage(event) {
11427
13104
  const stageAt = cliNow().toISOString();
11428
13105
  if (event.stage === "text_injected") {
@@ -11463,6 +13140,14 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
11463
13140
  previous_generation_id: stringValue(previousDispatchLedger?.generation_id) ??
11464
13141
  stringValue(previousDispatchLedger?.message_id)
11465
13142
  });
13143
+ if (deferredCodexForegroundBinding) {
13144
+ advanceDeferredCodexForegroundTransferInputStage({
13145
+ options,
13146
+ boundary: deferredCodexForegroundBinding,
13147
+ stage: event.stage,
13148
+ at: stageAt
13149
+ });
13150
+ }
11466
13151
  if (event.stage === "text_injected" && observedHandoff) {
11467
13152
  await assertObservedHandoffTransportBoundary({
11468
13153
  options,
@@ -11478,6 +13163,15 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
11478
13163
  requireEmptyComposer: false
11479
13164
  });
11480
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
+ }
11481
13175
  try {
11482
13176
  appendEvent(logPath, {
11483
13177
  ts: stageAt,
@@ -11507,6 +13201,11 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
11507
13201
  let boundConversation;
11508
13202
  let bindingError;
11509
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
+ }
11510
13209
  boundIdentity = await pollNativeAgentSessionIdentity({
11511
13210
  options,
11512
13211
  executor,
@@ -11514,7 +13213,19 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
11514
13213
  pid: terminalAgentPid,
11515
13214
  expectedSessionId: stringValue(sendTakeover?.terminal_agent_expected_session_id),
11516
13215
  allowedPreMaterializationIdentity,
11517
- 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
+ : {})
11518
13229
  });
11519
13230
  }
11520
13231
  catch (error) {
@@ -11541,20 +13252,30 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
11541
13252
  if (!bindingStoreDir) {
11542
13253
  throw new Error("managed Session Store is unavailable before native identity commit");
11543
13254
  }
11544
- await assertNativeThreadHasExclusiveOwnership({
11545
- options,
11546
- agent: executor.kind,
11547
- currentPid: terminalAgentPid,
11548
- nativeThreadId: boundIdentity.sessionId,
11549
- storeDir: bindingStoreDir,
11550
- terminalControl,
11551
- excludedManagedSessionId: sessionIdForConversation(boundConversation)
11552
- });
11553
- persistManagedSessionNativeIdentity({
11554
- conversation: boundConversation,
11555
- terminalControl,
11556
- identity: boundIdentity
11557
- });
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
+ }
11558
13279
  if (codexRolloutAcceptanceAnchor?.version === 2 &&
11559
13280
  cliEnv().AKK_TEST_EXIT_AFTER_VIRGIN_SESSION_BINDING === "1") {
11560
13281
  cliExit(86);
@@ -11577,11 +13298,69 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
11577
13298
  boundConversation = undefined;
11578
13299
  }
11579
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
+ }
11580
13344
  if (!boundIdentity) {
11581
13345
  const bindingFailedAt = cliNow().toISOString();
11582
13346
  const bindingReason = bindingError
11583
13347
  ? `AKK dispatched terminal input but could not verify the new native agent session: ${bindingError}`
11584
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
+ }
11585
13364
  const unfencedBase = {
11586
13365
  ...stagedConversation,
11587
13366
  status: "stalled",
@@ -11610,10 +13389,12 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
11610
13389
  error: bindingReason,
11611
13390
  lastProvenStage: "enter_dispatched"
11612
13391
  });
11613
- quarantineManagedSessionBinding({
11614
- conversation: unfencedConversation,
11615
- reason: bindingReason
11616
- });
13392
+ if (!deferredCodexForegroundBinding) {
13393
+ quarantineManagedSessionBinding({
13394
+ conversation: unfencedConversation,
13395
+ reason: bindingReason
13396
+ });
13397
+ }
11617
13398
  saveTerminalBridgeDispatchLedger(terminalControl, {
11618
13399
  ...terminalBindingLedgerFields(unfencedConversation),
11619
13400
  status: "uncertain",
@@ -11835,11 +13616,13 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
11835
13616
  const errorMessage = error.message;
11836
13617
  let dispatchLedgerRestored = true;
11837
13618
  try {
11838
- restoreTerminalBridgeDispatchLedger({
11839
- terminalControl,
11840
- previousLedger: previousDispatchLedger,
11841
- reason: "terminal transport was proved not to have started"
11842
- });
13619
+ if (!abortDeferredPreInputBeforeTransport(abortedAt)) {
13620
+ restoreTerminalBridgeDispatchLedger({
13621
+ terminalControl,
13622
+ previousLedger: previousDispatchLedger,
13623
+ reason: "terminal transport was proved not to have started"
13624
+ });
13625
+ }
11843
13626
  }
11844
13627
  catch (ledgerError) {
11845
13628
  dispatchLedgerRestored = false;
@@ -11974,6 +13757,23 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
11974
13757
  }
11975
13758
  const uncertainAt = cliNow().toISOString();
11976
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
+ }
11977
13777
  const failureBase = stagedConversation;
11978
13778
  const stalledFailureBase = {
11979
13779
  ...failureBase,
@@ -13142,7 +14942,7 @@ function assertTurnBindingCurrent(conversation, operation) {
13142
14942
  // the Store writer lease. This makes the freshly materialized Session
13143
14943
  // authoritative even for the first callback/control after an upgrade.
13144
14944
  const manifest = ensureStoreWritable(storeDir);
13145
- if (manifest.writer_protocol !== 3) {
14945
+ if (manifest.writer_protocol < STORE_SESSION_AUTHORITY_PROTOCOL) {
13146
14946
  throw new Error(`cannot ${operation} Turn ${turnIdForConversation(conversation)}: ` +
13147
14947
  "its Store has no protocol-3 Session authority");
13148
14948
  }
@@ -13385,7 +15185,7 @@ function quarantineManagedSessionBinding({ conversation, reason }) {
13385
15185
  expectedRevision: managedSessionRevision(current)
13386
15186
  });
13387
15187
  }
13388
- 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 }) {
13389
15189
  for (let attempt = 0; attempt < attempts; attempt += 1) {
13390
15190
  const identity = await resolveCurrentNativeAgentSessionIdentity({
13391
15191
  options,
@@ -13400,6 +15200,19 @@ async function pollNativeAgentSessionIdentity({ options, executor, terminalContr
13400
15200
  });
13401
15201
  if (identity) {
13402
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
+ }
13403
15216
  return identity;
13404
15217
  }
13405
15218
  if (nativeIdentityMatchesCodexPreMaterialization(identity, allowedPreMaterializationIdentity)) {
@@ -13486,7 +15299,7 @@ function managedTurnMatchesResolvedTerminal(conversation, terminalConversation,
13486
15299
  terminalControlsShareIncarnation(storedControl, terminalConversation.terminalControl) &&
13487
15300
  matchesConfiguredWorkspace(conversation.workspace, terminalConversation.terminalControl.currentPath));
13488
15301
  }
13489
- 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 }) {
13490
15303
  const workspace = terminalControl.currentPath ?? cliCwd();
13491
15304
  const storeDir = expandHome(options.storeDir ?? options.logDir ?? defaultStoreDir(workspace));
13492
15305
  const executor = previousTurn
@@ -13554,7 +15367,10 @@ function createManagedTerminalTurn({ options, conversationId, agent, pid, messag
13554
15367
  : "raw_terminal_send",
13555
15368
  ...terminalEndpointTakeoverFields(terminalControl),
13556
15369
  needs_bootstrap: false,
13557
- terminal_bridge: true
15370
+ terminal_bridge: true,
15371
+ ...(deferredForegroundTransferId
15372
+ ? { deferred_foreground_transfer_id: deferredForegroundTransferId }
15373
+ : {})
13558
15374
  }
13559
15375
  }, paths);
13560
15376
  const message = createMessage({
@@ -13750,14 +15566,23 @@ async function runReconcileMonitors(options) {
13750
15566
  }
13751
15567
  async function reconcileMonitors(options, { includeCallbackRecovery, reason, conversationId }) {
13752
15568
  const storeDir = storeDirFromOptions(options);
15569
+ const collateralStalls = await withStoreWriterLeaseAsync(storeDir, async () => reconcileTerminalBridgeCollateralStalls(storeDir, conversationId));
13753
15570
  const conversations = listConversations(storeDir).filter((conversation) => conversationId === undefined || conversation.conversation_id === conversationId);
13754
- const items = [];
15571
+ const items = [...collateralStalls.items];
15572
+ const repairedConversationIds = new Set(collateralStalls.items
15573
+ .filter((item) => item.status === "repaired")
15574
+ .map((item) => stringValue(item.conversation_id))
15575
+ .filter((id) => id !== undefined));
13755
15576
  let ignored = 0;
13756
15577
  let launched = 0;
13757
15578
  let alreadyRunning = 0;
13758
15579
  let skipped = 0;
13759
- let errors = 0;
15580
+ let errors = collateralStalls.errors.length;
13760
15581
  for (const listedConversation of conversations) {
15582
+ if (repairedConversationIds.has(listedConversation.conversation_id)) {
15583
+ ignored += 1;
15584
+ continue;
15585
+ }
13761
15586
  if (!matchesConfiguredWorkspace(options.workspace, listedConversation.workspace)) {
13762
15587
  ignored += 1;
13763
15588
  continue;
@@ -13991,6 +15816,9 @@ async function reconcileMonitors(options, { includeCallbackRecovery, reason, con
13991
15816
  reconciled: true,
13992
15817
  store_dir: storeDir,
13993
15818
  checked: conversations.length,
15819
+ repaired: collateralStalls.repaired,
15820
+ collateral_stalls_checked: collateralStalls.checked,
15821
+ collateral_stalls_skipped: collateralStalls.skipped,
13994
15822
  ignored,
13995
15823
  launched,
13996
15824
  already_running: alreadyRunning,
@@ -14381,6 +16209,7 @@ async function runTerminalControlCancel({ options, statePath, logPath, agent, te
14381
16209
  throw new Error("terminal control changed while waiting to cancel; refresh status and retry");
14382
16210
  }
14383
16211
  assertManagedTerminalDispatchOwner({
16212
+ storeDir: writerStoreDir,
14384
16213
  conversation: currentConversation,
14385
16214
  terminalControl: currentControl,
14386
16215
  action: "cancel"
@@ -14457,7 +16286,7 @@ async function runObservedHandoffClose({ options, statePath, logPath, initialCon
14457
16286
  if (stringValue(options.reason) !== "superseded_by_human_context_switch") {
14458
16287
  throw new Error("a handoff close requires reason superseded_by_human_context_switch");
14459
16288
  }
14460
- const storeDir = storeDirFromOptions(options);
16289
+ const storeDir = pathsForConversationDir(path.dirname(statePath)).storeDir;
14461
16290
  const sourceSessionId = sessionIdForConversation(initialConversation);
14462
16291
  const initialSource = loadManagedSession(storeDir, sourceSessionId);
14463
16292
  if (!initialSource.binding || initialSource.status !== "bound") {
@@ -14643,88 +16472,183 @@ async function runClose(options) {
14643
16472
  });
14644
16473
  return;
14645
16474
  }
16475
+ if (stringValue(options.reason) === "superseded_by_human_context_switch") {
16476
+ throw new Error("reason superseded_by_human_context_switch requires the fresh " +
16477
+ "expected_handoff_token advertised by AKK list");
16478
+ }
14646
16479
  const { statePath, logPath } = loaded;
16480
+ const closeStoreDir = pathsForConversationDir(path.dirname(statePath)).storeDir;
14647
16481
  const nativeTakeover = isRecord(loaded.conversation.native_session_takeover)
14648
16482
  ? loaded.conversation.native_session_takeover
14649
16483
  : undefined;
14650
16484
  const terminalControl = terminalControlFromTakeover(nativeTakeover);
14651
16485
  const releaseTerminalLock = terminalControl
14652
- ? acquireTerminalBridgeSendLock(storeDirFromOptions(options), terminalControl, { timeoutMs: 30000 })
16486
+ ? acquireTerminalBridgeSendLock(closeStoreDir, terminalControl, { timeoutMs: 30000 })
14653
16487
  : () => { };
14654
- let releaseStateLock;
14655
- try {
14656
- releaseStateLock = acquireFileLock(`${statePath}.lock`);
14657
- const conversation = loadState(statePath);
14658
- const now = cliNow().toISOString();
14659
- const closeReason = options.reason ?? "closed by request";
14660
- const handoffDisposition = closeReason === "superseded_by_human_context_switch"
14661
- ? closeReason
14662
- : undefined;
14663
- const closed = {
14664
- ...conversation,
14665
- status: "closed",
14666
- closed_at: now,
14667
- close_reason: closeReason,
14668
- ...(handoffDisposition
14669
- ? { disposition: handoffDisposition }
14670
- : {}),
14671
- updated_at: now
14672
- };
14673
- saveState(statePath, closed);
14674
- let dispatchLedgerResolved = false;
14675
- let dispatchLedgerWarning;
14676
- if (terminalControl) {
14677
- try {
14678
- dispatchLedgerResolved = resolveTerminalBridgeDispatchLedger({
14679
- terminalControl,
14680
- conversation: closed,
14681
- expectedMessageId: stringValue(nativeTakeover?.terminal_bridge_message_id),
14682
- reason: "conversation explicitly closed by request"
14683
- });
16488
+ const closeWithFreshState = async () => {
16489
+ const releaseStateLock = acquireFileLock(`${statePath}.lock`);
16490
+ try {
16491
+ const conversation = loadState(statePath);
16492
+ assertConversationHasNoNonterminalDeferredForegroundTransfer({
16493
+ storeDir: closeStoreDir,
16494
+ conversation,
16495
+ action: "close"
16496
+ });
16497
+ const currentTakeover = isRecord(conversation.native_session_takeover)
16498
+ ? conversation.native_session_takeover
16499
+ : undefined;
16500
+ const currentTerminalControl = terminalControlFromTakeover(currentTakeover);
16501
+ if (terminalControl &&
16502
+ !terminalControlsShareIncarnation(currentTerminalControl, terminalControl)) {
16503
+ throw new Error("terminal control changed after the close action was listed; refresh AKK list");
14684
16504
  }
14685
- catch (error) {
14686
- dispatchLedgerWarning =
14687
- error instanceof Error ? error.message : String(error);
14688
- runtimeLog("error", "terminal_dispatch_ledger_resolve_failed", {
14689
- conversation_id: closed.conversation_id,
14690
- terminal_target: terminalControl.target,
14691
- error: dispatchLedgerWarning
16505
+ if (terminalControl && currentTerminalControl) {
16506
+ await assertGenericCloseDoesNotBypassObservedHandoff({
16507
+ options,
16508
+ storeDir: closeStoreDir,
16509
+ conversation,
16510
+ terminalControl: currentTerminalControl
14692
16511
  });
14693
16512
  }
14694
- }
14695
- appendEvent(logPath, {
14696
- ts: now,
14697
- conversation_id: conversation.conversation_id,
14698
- event: "conversation_closed",
14699
- status: "closed",
14700
- reason: closed.close_reason
14701
- });
14702
- runtimeLog("info", "conversation_closed", {
14703
- conversation_id: conversation.conversation_id,
14704
- status: "closed",
14705
- reason: closed.close_reason,
14706
- state_path: statePath,
14707
- event_log_path: logPath
14708
- });
14709
- printJson({
14710
- conversation: closed,
14711
- closed: true,
14712
- terminal_dispatch_resolved: dispatchLedgerResolved,
14713
- ...(dispatchLedgerWarning
14714
- ? {
14715
- terminal_dispatch_warning: textSummary(dispatchLedgerWarning),
14716
- do_not_retry: true
16513
+ const now = cliNow().toISOString();
16514
+ const closeReason = options.reason ?? "closed by request";
16515
+ const closed = {
16516
+ ...conversation,
16517
+ status: "closed",
16518
+ closed_at: now,
16519
+ close_reason: closeReason,
16520
+ updated_at: now
16521
+ };
16522
+ saveState(statePath, closed);
16523
+ let dispatchLedgerResolved = false;
16524
+ let dispatchLedgerWarning;
16525
+ if (currentTerminalControl) {
16526
+ try {
16527
+ dispatchLedgerResolved = resolveTerminalBridgeDispatchLedger({
16528
+ terminalControl: currentTerminalControl,
16529
+ conversation: closed,
16530
+ expectedMessageId: stringValue(currentTakeover?.terminal_bridge_message_id),
16531
+ reason: "conversation explicitly closed by request"
16532
+ });
14717
16533
  }
14718
- : {})
14719
- });
14720
- }
14721
- finally {
14722
- try {
14723
- releaseStateLock?.();
16534
+ catch (error) {
16535
+ dispatchLedgerWarning =
16536
+ error instanceof Error ? error.message : String(error);
16537
+ runtimeLog("error", "terminal_dispatch_ledger_resolve_failed", {
16538
+ conversation_id: closed.conversation_id,
16539
+ terminal_target: currentTerminalControl.target,
16540
+ error: dispatchLedgerWarning
16541
+ });
16542
+ }
16543
+ }
16544
+ appendEvent(logPath, {
16545
+ ts: now,
16546
+ conversation_id: conversation.conversation_id,
16547
+ event: "conversation_closed",
16548
+ status: "closed",
16549
+ reason: closed.close_reason
16550
+ });
16551
+ runtimeLog("info", "conversation_closed", {
16552
+ conversation_id: conversation.conversation_id,
16553
+ status: "closed",
16554
+ reason: closed.close_reason,
16555
+ state_path: statePath,
16556
+ event_log_path: logPath
16557
+ });
16558
+ printJson({
16559
+ conversation: closed,
16560
+ closed: true,
16561
+ terminal_dispatch_resolved: dispatchLedgerResolved,
16562
+ ...(dispatchLedgerWarning
16563
+ ? {
16564
+ terminal_dispatch_warning: textSummary(dispatchLedgerWarning),
16565
+ do_not_retry: true
16566
+ }
16567
+ : {})
16568
+ });
14724
16569
  }
14725
16570
  finally {
14726
- releaseTerminalLock();
16571
+ releaseStateLock();
16572
+ }
16573
+ };
16574
+ try {
16575
+ if (terminalControl) {
16576
+ await withStoreWriterLeaseAsync(closeStoreDir, closeWithFreshState);
16577
+ }
16578
+ else {
16579
+ await closeWithFreshState();
16580
+ }
16581
+ }
16582
+ finally {
16583
+ releaseTerminalLock();
16584
+ }
16585
+ }
16586
+ async function assertGenericCloseDoesNotBypassObservedHandoff({ options, storeDir, conversation, terminalControl }) {
16587
+ if (!SESSION_SEND_BLOCKING_STATUSES.has(conversation.status)) {
16588
+ return;
16589
+ }
16590
+ const sourceSession = tryLoadManagedSession(storeDir, sessionIdForConversation(conversation));
16591
+ if (sourceSession?.status !== "bound" ||
16592
+ !sourceSession.binding ||
16593
+ !terminalControlsShareIncarnation(sourceSession.binding.terminal_control, terminalControl)) {
16594
+ return;
16595
+ }
16596
+ const takeover = isRecord(conversation.native_session_takeover)
16597
+ ? conversation.native_session_takeover
16598
+ : undefined;
16599
+ const pid = Number(takeover?.terminal_agent_pid);
16600
+ if (!Number.isSafeInteger(pid) || pid <= 1) {
16601
+ return;
16602
+ }
16603
+ const bridge = createTerminalAgentBridge(options);
16604
+ let terminal;
16605
+ try {
16606
+ terminal = await bridge.resolveStoredTerminal(sourceSession.agent, pid, terminalControl, { pid });
16607
+ }
16608
+ catch (error) {
16609
+ if (!(error instanceof TerminalControlUnavailableError)) {
16610
+ throw error;
14727
16611
  }
16612
+ // An unavailable terminal cannot prove a live A -> B handoff. Preserve the
16613
+ // explicit Store-only close path used to retire unavailable Turn history.
16614
+ return;
16615
+ }
16616
+ const companions = sourceSession.agent === "codex"
16617
+ ? codexAllowedCompanionSetForManagedSession({
16618
+ storeDir,
16619
+ session: sourceSession
16620
+ })
16621
+ : { additional: [] };
16622
+ const identityObservation = await observeCurrentNativeAgentSessionIdentity({
16623
+ options,
16624
+ agent: terminal.agent,
16625
+ pid: terminal.pid,
16626
+ cwd: terminal.terminalControl.currentPath,
16627
+ preferredSessionId: companions.primary
16628
+ ? sourceSession.binding.native_thread_id
16629
+ : undefined,
16630
+ allowedCompanionIdentity: companions.primary,
16631
+ allowedAdditionalIdentities: companions.additional
16632
+ });
16633
+ const resolvedIdentity = identityObservation.status === "resolved"
16634
+ ? identityObservation.identity
16635
+ : undefined;
16636
+ const observed = await observedExternalHandoffIdentity({
16637
+ options,
16638
+ terminal,
16639
+ sourceSession,
16640
+ resolvedIdentity,
16641
+ requireSafeTerminal: false
16642
+ });
16643
+ if (managedBindingConflictKindForResolvedTerminal({
16644
+ storeDir,
16645
+ session: sourceSession,
16646
+ terminal,
16647
+ identity: observed.identity
16648
+ }) === "live_external_thread_change") {
16649
+ throw new Error(`terminal ${terminalControl.target} changed native thread after the ` +
16650
+ "generic close action was listed; refresh AKK list and use only its " +
16651
+ "fresh snapshot-bound handoff_decision");
14728
16652
  }
14729
16653
  }
14730
16654
  async function runTerminalDispatchClose({ options, terminalConversation }) {
@@ -14737,6 +16661,30 @@ async function runTerminalDispatchClose({ options, terminalConversation }) {
14737
16661
  if (!ledger || ledger.status === "resolved") {
14738
16662
  throw new Error(`terminal ${terminalControl.target} has no unresolved AKK dispatch fence`);
14739
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
+ }
14740
16688
  if (![
14741
16689
  "prepared",
14742
16690
  "text_injected",
@@ -17095,7 +19043,12 @@ function loadTerminalDispatchLedgerOwner(ledger) {
17095
19043
  return undefined;
17096
19044
  }
17097
19045
  }
17098
- function assertManagedTerminalDispatchOwner({ conversation, terminalControl, action }) {
19046
+ function assertManagedTerminalDispatchOwner({ storeDir, conversation, terminalControl, action }) {
19047
+ assertConversationHasNoNonterminalDeferredForegroundTransfer({
19048
+ storeDir,
19049
+ conversation,
19050
+ action
19051
+ });
17099
19052
  assertTurnBindingCurrent(conversation, action);
17100
19053
  const nativeTakeover = isRecord(conversation.native_session_takeover)
17101
19054
  ? conversation.native_session_takeover
@@ -17133,6 +19086,7 @@ function terminalBindingLedgerFields(conversation) {
17133
19086
  const submission = terminalBridgeSubmission(conversation);
17134
19087
  const messageType = stringValue(submission?.message_type);
17135
19088
  const messageBodyHash = stringValue(submission?.message_body_hash);
19089
+ const deferredForegroundTransferId = stringValue(takeover?.deferred_foreground_transfer_id);
17136
19090
  return {
17137
19091
  ...(bindingId ? { binding_id: bindingId } : {}),
17138
19092
  ...(Number.isSafeInteger(generation)
@@ -17142,12 +19096,30 @@ function terminalBindingLedgerFields(conversation) {
17142
19096
  ...(storeDir ? { store_dir: path.resolve(storeDir) } : {}),
17143
19097
  ...(messageType ? { message_type: messageType } : {}),
17144
19098
  ...(messageBodyHash ? { message_body_hash: messageBodyHash } : {}),
19099
+ ...(deferredForegroundTransferId
19100
+ ? { deferred_foreground_transfer_id: deferredForegroundTransferId }
19101
+ : {}),
17145
19102
  executor_kind: executorForConversation(conversation).kind,
17146
19103
  ...(conversation.openclaw_session
17147
19104
  ? { openclaw_session: conversation.openclaw_session }
17148
19105
  : {})
17149
19106
  };
17150
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
+ }
17151
19123
  function reconcilePreparedTerminalDispatchLedger(terminalControl, ledger) {
17152
19124
  if (terminalDispatchLedgerLooksLifecycle(ledger)) {
17153
19125
  return ledger;
@@ -17341,161 +19313,1140 @@ function reconcileLaggingTerminalDispatchLedger(terminalControl, ledger) {
17341
19313
  String(ledger[key]) !== String(bindingFields[key])) {
17342
19314
  return ledger;
17343
19315
  }
17344
- }
17345
- const stateStatus = String(submission?.status ?? "");
17346
- let stateAcceptanceEvidence;
17347
- if (stateStatus === "agent_accepted") {
17348
- try {
17349
- stateAcceptanceEvidence = terminalAcceptanceEvidenceForConversation(conversation, requestText, submission?.acceptance_evidence);
19316
+ }
19317
+ const stateStatus = String(submission?.status ?? "");
19318
+ let stateAcceptanceEvidence;
19319
+ if (stateStatus === "agent_accepted") {
19320
+ try {
19321
+ stateAcceptanceEvidence = terminalAcceptanceEvidenceForConversation(conversation, requestText, submission?.acceptance_evidence);
19322
+ }
19323
+ catch {
19324
+ stateAcceptanceEvidence = undefined;
19325
+ }
19326
+ }
19327
+ let ledgerAcceptanceEvidence;
19328
+ let ledgerAcceptanceError;
19329
+ if (ledger.status === "agent_accepted") {
19330
+ try {
19331
+ ledgerAcceptanceEvidence = terminalAcceptanceEvidenceForConversation(conversation, requestText, ledger.acceptance_evidence);
19332
+ }
19333
+ catch (error) {
19334
+ ledgerAcceptanceError = error instanceof Error
19335
+ ? error.message
19336
+ : String(error);
19337
+ }
19338
+ }
19339
+ if (ledgerAcceptanceEvidence) {
19340
+ if (!stateAcceptanceEvidence) {
19341
+ const acceptedAt = stringValue(ledger.agent_accepted_at) ?? cliNow().toISOString();
19342
+ const acceptedConversation = withTerminalBridgeSubmission({
19343
+ conversation,
19344
+ messageId,
19345
+ requestText,
19346
+ status: "agent_accepted",
19347
+ preparedAt: stringValue(submission?.prepared_at) ??
19348
+ stringValue(ledger.prepared_at) ??
19349
+ acceptedAt,
19350
+ textInjectedAt: stringValue(submission?.text_injected_at) ??
19351
+ stringValue(ledger.text_injected_at),
19352
+ enterDispatchedAt: stringValue(submission?.enter_dispatched_at) ??
19353
+ stringValue(ledger.enter_dispatched_at),
19354
+ agentAcceptedAt: acceptedAt,
19355
+ acceptanceEvidence: ledgerAcceptanceEvidence
19356
+ });
19357
+ saveState(statePath, acceptedConversation);
19358
+ }
19359
+ return ledger;
19360
+ }
19361
+ if (stateAcceptanceEvidence) {
19362
+ const acceptedAt = stringValue(submission?.agent_accepted_at) ?? cliNow().toISOString();
19363
+ saveTerminalBridgeDispatchLedger(terminalControl, {
19364
+ ...ledger,
19365
+ ...bindingFields,
19366
+ status: "agent_accepted",
19367
+ text_injected_at: submission?.text_injected_at,
19368
+ enter_dispatched_at: submission?.enter_dispatched_at,
19369
+ agent_accepted_at: acceptedAt,
19370
+ acceptance_evidence: stateAcceptanceEvidence,
19371
+ dispatcher_pid: null,
19372
+ reason: "recovered the strongest durable native acceptance receipt"
19373
+ });
19374
+ return loadTerminalBridgeDispatchLedger(terminalControl);
19375
+ }
19376
+ if (ledger.status === "agent_accepted") {
19377
+ const uncertainAt = cliNow().toISOString();
19378
+ const uncertainConversation = withTerminalBridgeSubmission({
19379
+ conversation: {
19380
+ ...conversation,
19381
+ status: "stalled",
19382
+ stalled_at: uncertainAt,
19383
+ stalled_reason: "stored native acceptance evidence is invalid",
19384
+ updated_at: uncertainAt
19385
+ },
19386
+ messageId,
19387
+ requestText,
19388
+ status: "uncertain",
19389
+ preparedAt: stringValue(submission?.prepared_at) ??
19390
+ stringValue(ledger.prepared_at) ??
19391
+ uncertainAt,
19392
+ textInjectedAt: stringValue(submission?.text_injected_at) ??
19393
+ stringValue(ledger.text_injected_at),
19394
+ enterDispatchedAt: stringValue(submission?.enter_dispatched_at) ??
19395
+ stringValue(ledger.enter_dispatched_at),
19396
+ uncertainAt,
19397
+ error: ledgerAcceptanceError ??
19398
+ "stored native acceptance evidence is invalid",
19399
+ lastProvenStage: "enter_dispatched"
19400
+ });
19401
+ saveState(statePath, uncertainConversation);
19402
+ saveTerminalBridgeDispatchLedger(terminalControl, {
19403
+ ...ledger,
19404
+ ...bindingFields,
19405
+ status: "uncertain",
19406
+ uncertain_at: uncertainAt,
19407
+ dispatcher_pid: null,
19408
+ reason: "stored native acceptance evidence is invalid"
19409
+ });
19410
+ return loadTerminalBridgeDispatchLedger(terminalControl);
19411
+ }
19412
+ const stateRank = terminalSubmissionProofRank(stateStatus, stringValue(submission?.last_proven_stage));
19413
+ const ledgerRank = terminalSubmissionProofRank(String(ledger.status), stringValue(ledger.last_proven_stage));
19414
+ const stateIsTerminal = ["not_accepted", "uncertain", "aborted"]
19415
+ .includes(stateStatus);
19416
+ if (!stateIsTerminal && stateRank <= ledgerRank) {
19417
+ return ledger;
19418
+ }
19419
+ if (![
19420
+ "text_injected",
19421
+ "enter_dispatched",
19422
+ "submitted",
19423
+ "not_accepted",
19424
+ "uncertain",
19425
+ "aborted"
19426
+ ].includes(stateStatus)) {
19427
+ return ledger;
19428
+ }
19429
+ saveTerminalBridgeDispatchLedger(terminalControl, {
19430
+ ...ledger,
19431
+ ...bindingFields,
19432
+ status: stateStatus,
19433
+ ...(submission?.text_injected_at
19434
+ ? { text_injected_at: submission.text_injected_at }
19435
+ : {}),
19436
+ ...(submission?.enter_dispatched_at
19437
+ ? { enter_dispatched_at: submission.enter_dispatched_at }
19438
+ : {}),
19439
+ ...(submission?.submitted_at
19440
+ ? { submitted_at: submission.submitted_at }
19441
+ : {}),
19442
+ ...(submission?.not_accepted_at
19443
+ ? { not_accepted_at: submission.not_accepted_at }
19444
+ : {}),
19445
+ ...(submission?.uncertain_at
19446
+ ? { uncertain_at: submission.uncertain_at }
19447
+ : {}),
19448
+ ...(submission?.aborted_at
19449
+ ? { aborted_at: submission.aborted_at }
19450
+ : {}),
19451
+ last_proven_stage: submission?.last_proven_stage,
19452
+ ...(stateIsTerminal ? { dispatcher_pid: null } : {}),
19453
+ reason: "recovered the strongest durable conversation proof level"
19454
+ });
19455
+ return loadTerminalBridgeDispatchLedger(terminalControl);
19456
+ }
19457
+ function terminalSubmissionProofRank(status, lastProven) {
19458
+ if (status === "agent_accepted" || lastProven === "agent_accepted") {
19459
+ return 3;
19460
+ }
19461
+ if (["enter_dispatched", "submitted", "not_accepted"].includes(status) ||
19462
+ lastProven === "enter_dispatched") {
19463
+ return 2;
19464
+ }
19465
+ if (status === "text_injected" || lastProven === "text_injected") {
19466
+ return 1;
19467
+ }
19468
+ return 0;
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;
17350
20059
  }
17351
- catch {
17352
- stateAcceptanceEvidence = undefined;
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
+ });
17353
20066
  }
17354
- }
17355
- let ledgerAcceptanceEvidence;
17356
- let ledgerAcceptanceError;
17357
- if (ledger.status === "agent_accepted") {
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;
17358
20093
  try {
17359
- ledgerAcceptanceEvidence = terminalAcceptanceEvidenceForConversation(conversation, requestText, ledger.acceptance_evidence);
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;
17360
20129
  }
17361
20130
  catch (error) {
17362
- ledgerAcceptanceError = error instanceof Error
17363
- ? error.message
17364
- : String(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;
17365
20209
  }
17366
20210
  }
17367
- if (ledgerAcceptanceEvidence) {
17368
- if (!stateAcceptanceEvidence) {
17369
- const acceptedAt = stringValue(ledger.agent_accepted_at) ?? cliNow().toISOString();
17370
- const acceptedConversation = withTerminalBridgeSubmission({
17371
- conversation,
17372
- messageId,
17373
- requestText,
17374
- status: "agent_accepted",
17375
- preparedAt: stringValue(submission?.prepared_at) ??
17376
- stringValue(ledger.prepared_at) ??
17377
- acceptedAt,
17378
- textInjectedAt: stringValue(submission?.text_injected_at) ??
17379
- stringValue(ledger.text_injected_at),
17380
- enterDispatchedAt: stringValue(submission?.enter_dispatched_at) ??
17381
- stringValue(ledger.enter_dispatched_at),
17382
- agentAcceptedAt: acceptedAt,
17383
- acceptanceEvidence: ledgerAcceptanceEvidence
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
17384
20287
  });
17385
- saveState(statePath, acceptedConversation);
17386
20288
  }
17387
- return ledger;
17388
- }
17389
- if (stateAcceptanceEvidence) {
17390
- const acceptedAt = stringValue(submission?.agent_accepted_at) ?? cliNow().toISOString();
17391
- saveTerminalBridgeDispatchLedger(terminalControl, {
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, {
17392
20305
  ...ledger,
17393
- ...bindingFields,
20306
+ ...terminalBindingLedgerFields(acceptedConversation),
17394
20307
  status: "agent_accepted",
17395
- text_injected_at: submission?.text_injected_at,
17396
- enter_dispatched_at: submission?.enter_dispatched_at,
17397
- agent_accepted_at: acceptedAt,
17398
- acceptance_evidence: stateAcceptanceEvidence,
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,
17399
20320
  dispatcher_pid: null,
17400
- reason: "recovered the strongest durable native acceptance receipt"
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"
17401
20325
  });
17402
- return loadTerminalBridgeDispatchLedger(terminalControl);
20326
+ return { conversation: acceptedConversation, identity };
17403
20327
  }
17404
- if (ledger.status === "agent_accepted") {
17405
- const uncertainAt = cliNow().toISOString();
17406
- const uncertainConversation = withTerminalBridgeSubmission({
17407
- conversation: {
17408
- ...conversation,
17409
- status: "stalled",
17410
- stalled_at: uncertainAt,
17411
- stalled_reason: "stored native acceptance evidence is invalid",
17412
- updated_at: uncertainAt
17413
- },
17414
- messageId,
17415
- requestText,
17416
- status: "uncertain",
17417
- preparedAt: stringValue(submission?.prepared_at) ??
17418
- stringValue(ledger.prepared_at) ??
17419
- uncertainAt,
17420
- textInjectedAt: stringValue(submission?.text_injected_at) ??
17421
- stringValue(ledger.text_injected_at),
17422
- enterDispatchedAt: stringValue(submission?.enter_dispatched_at) ??
17423
- stringValue(ledger.enter_dispatched_at),
17424
- uncertainAt,
17425
- error: ledgerAcceptanceError ??
17426
- "stored native acceptance evidence is invalid",
17427
- lastProvenStage: "enter_dispatched"
17428
- });
17429
- saveState(statePath, uncertainConversation);
17430
- saveTerminalBridgeDispatchLedger(terminalControl, {
17431
- ...ledger,
17432
- ...bindingFields,
17433
- status: "uncertain",
17434
- uncertain_at: uncertainAt,
17435
- dispatcher_pid: null,
17436
- reason: "stored native acceptance evidence is invalid"
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
17437
20343
  });
17438
- return loadTerminalBridgeDispatchLedger(terminalControl);
17439
20344
  }
17440
- const stateRank = terminalSubmissionProofRank(stateStatus, stringValue(submission?.last_proven_stage));
17441
- const ledgerRank = terminalSubmissionProofRank(String(ledger.status), stringValue(ledger.last_proven_stage));
17442
- const stateIsTerminal = ["not_accepted", "uncertain", "aborted"]
17443
- .includes(stateStatus);
17444
- if (!stateIsTerminal && stateRank <= ledgerRank) {
17445
- return ledger;
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;
17446
20352
  }
17447
- if (![
17448
- "text_injected",
17449
- "enter_dispatched",
17450
- "submitted",
17451
- "not_accepted",
17452
- "uncertain",
17453
- "aborted"
17454
- ].includes(stateStatus)) {
17455
- return ledger;
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");
17456
20356
  }
17457
- saveTerminalBridgeDispatchLedger(terminalControl, {
17458
- ...ledger,
17459
- ...bindingFields,
17460
- status: stateStatus,
17461
- ...(submission?.text_injected_at
17462
- ? { text_injected_at: submission.text_injected_at }
17463
- : {}),
17464
- ...(submission?.enter_dispatched_at
17465
- ? { enter_dispatched_at: submission.enter_dispatched_at }
17466
- : {}),
17467
- ...(submission?.submitted_at
17468
- ? { submitted_at: submission.submitted_at }
17469
- : {}),
17470
- ...(submission?.not_accepted_at
17471
- ? { not_accepted_at: submission.not_accepted_at }
17472
- : {}),
17473
- ...(submission?.uncertain_at
17474
- ? { uncertain_at: submission.uncertain_at }
17475
- : {}),
17476
- ...(submission?.aborted_at
17477
- ? { aborted_at: submission.aborted_at }
17478
- : {}),
17479
- last_proven_stage: submission?.last_proven_stage,
17480
- ...(stateIsTerminal ? { dispatcher_pid: null } : {}),
17481
- reason: "recovered the strongest durable conversation proof level"
17482
- });
17483
- return loadTerminalBridgeDispatchLedger(terminalControl);
17484
- }
17485
- function terminalSubmissionProofRank(status, lastProven) {
17486
- if (status === "agent_accepted" || lastProven === "agent_accepted") {
17487
- return 3;
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;
17488
20382
  }
17489
- if (["enter_dispatched", "submitted", "not_accepted"].includes(status) ||
17490
- lastProven === "enter_dispatched") {
17491
- return 2;
20383
+ if (["prepared", "source_reserved", "target_prepared"].includes(transfer.status)) {
20384
+ abortPreparedDeferredForegroundTurn({
20385
+ storeDir,
20386
+ terminal,
20387
+ transfer,
20388
+ boundary
20389
+ });
20390
+ return;
17492
20391
  }
17493
- if (status === "text_injected" || lastProven === "text_injected") {
17494
- return 1;
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");
17495
20441
  }
17496
- return 0;
20442
+ throw new Error(`deferred foreground transfer ${transfer.transfer_id} has unsupported ` +
20443
+ `recovery status ${transfer.status}`);
17497
20444
  }
17498
20445
  async function recoverLifecycleFenceBeforeMutation({ options, terminal }) {
20446
+ await recoverDeferredCodexForegroundTransferBeforeMutation({
20447
+ options,
20448
+ terminal
20449
+ });
17499
20450
  let ledger = loadTerminalBridgeDispatchLedger(terminal.terminalControl);
17500
20451
  if (!ledger || ledger.status === "resolved") {
17501
20452
  ledger = rebuildObservedHandoffLedgerFromTransition({
@@ -18408,17 +21359,20 @@ async function probeManualCodexLifecycleRecoveryIdentity({ options, terminal, tr
18408
21359
  if (!cleared?.screen.digest) {
18409
21360
  throw new Error("Codex composer did not become empty after recovery clear-line");
18410
21361
  }
18411
- await assertCodexComposerReadyForAutomatedInput({
18412
- options,
18413
- terminalControl: terminal.terminalControl
18414
- });
18415
- const baselineDigest = cleared.screen.digest;
18416
- await bridge.send(terminal.agent, terminal.terminalControl, "/status", { runtime: recoveryRuntime });
21362
+ const agentVersion = agentVersionForRunningProcess(terminal.agent, terminal.pid, options);
21363
+ if (!agentVersion) {
21364
+ throw new Error("Codex lifecycle recovery /status requires the exact running version before terminal input");
21365
+ }
21366
+ const submission = await bridge.submitCodexStatusProbe(terminal.terminalControl, agentVersion, { runtime: recoveryRuntime });
21367
+ const baselineDigest = submission.observationBaselineDigest;
18417
21368
  let stable;
18418
21369
  let stableCount = 0;
18419
21370
  let lastObservationReason = "no fresh idle status screen was captured";
18420
21371
  for (let attempt = 0; attempt < 12; attempt += 1) {
18421
- const status = await bridge.status(terminal.agent, terminal.terminalControl, { runtime: recoveryRuntime });
21372
+ const status = await bridge.status(terminal.agent, terminal.terminalControl, {
21373
+ runtime: recoveryRuntime,
21374
+ scrollbackLines: submission.observationScrollbackLines
21375
+ });
18422
21376
  if (status.reachable &&
18423
21377
  status.activity_state === "idle" &&
18424
21378
  !status.approval_state.blocked &&
@@ -19447,6 +22401,15 @@ function runCallback(options) {
19447
22401
  }
19448
22402
  function runRetryCallback(options) {
19449
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
+ });
19450
22413
  const callbackDelivery = isRecord(conversation.callback_delivery)
19451
22414
  ? conversation.callback_delivery
19452
22415
  : undefined;