@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.
- package/CHANGELOG.md +25 -0
- package/README.md +7 -5
- package/dist/src/cli-core.js +3422 -459
- package/dist/src/cli-core.js.map +1 -1
- package/dist/src/deferred-foreground-transfer.d.ts +112 -0
- package/dist/src/deferred-foreground-transfer.js +1063 -0
- package/dist/src/deferred-foreground-transfer.js.map +1 -0
- package/dist/src/herdr-terminal-control-provider.d.ts +26 -1
- package/dist/src/herdr-terminal-control-provider.js +325 -4
- package/dist/src/herdr-terminal-control-provider.js.map +1 -1
- package/dist/src/openclaw-plugin.js +2 -2
- package/dist/src/openclaw-plugin.js.map +1 -1
- package/dist/src/session-store.js +7 -5
- package/dist/src/session-store.js.map +1 -1
- package/dist/src/store.d.ts +3 -1
- package/dist/src/store.js +24 -14
- package/dist/src/store.js.map +1 -1
- package/dist/src/terminal-agent-bridge.d.ts +29 -0
- package/dist/src/terminal-agent-bridge.js +387 -16
- package/dist/src/terminal-agent-bridge.js.map +1 -1
- package/dist/src/terminal-control-provider.d.ts +19 -0
- package/dist/src/terminal-control-provider.js +86 -7
- package/dist/src/terminal-control-provider.js.map +1 -1
- package/package.json +1 -1
- package/templates/openclaw-skills/agent-knock-knock/SKILL.md +9 -6
package/dist/src/cli-core.js
CHANGED
|
@@ -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
|
-
!
|
|
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 (!
|
|
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:
|
|
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
|
|
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
|
|
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:
|
|
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
|
-
!
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
:
|
|
3556
|
-
|
|
3557
|
-
isRecord(sessionAwareRawActions.send)
|
|
3977
|
+
: deferredCodexForegroundToken &&
|
|
3978
|
+
isRecord(rawActions.send)
|
|
3558
3979
|
? {
|
|
3559
3980
|
...sessionAwareRawActions,
|
|
3560
|
-
send:
|
|
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
|
-
:
|
|
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
|
-
...(
|
|
4041
|
+
...(terminalRecoveryBlockingTurns.length > 0
|
|
3606
4042
|
? {
|
|
3607
|
-
handoff_blocked_reason: "the
|
|
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:
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
|
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:
|
|
5811
|
-
kind: "
|
|
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
|
-
|
|
5821
|
-
const
|
|
5822
|
-
|
|
5823
|
-
|
|
5824
|
-
|
|
5825
|
-
|
|
5826
|
-
|
|
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
|
|
5829
|
-
|
|
5830
|
-
|
|
5831
|
-
|
|
5832
|
-
|
|
5833
|
-
|
|
5834
|
-
|
|
5835
|
-
|
|
5836
|
-
|
|
5837
|
-
|
|
5838
|
-
|
|
5839
|
-
|
|
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
|
|
5846
|
-
|
|
5847
|
-
|
|
5848
|
-
|
|
5849
|
-
|
|
5850
|
-
|
|
5851
|
-
|
|
5852
|
-
|
|
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
|
|
5991
|
-
const
|
|
5992
|
-
const
|
|
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
|
|
5998
|
-
|
|
5999
|
-
|
|
6000
|
-
|
|
6001
|
-
|
|
6757
|
+
workspace,
|
|
6758
|
+
processUuid,
|
|
6759
|
+
processBirth,
|
|
6760
|
+
sourceSession,
|
|
6761
|
+
dispatchSnapshot
|
|
6002
6762
|
});
|
|
6003
|
-
|
|
6004
|
-
|
|
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
|
-
|
|
6818
|
+
source_expected_revision: managedSessionRevision(sourceSession),
|
|
6007
6819
|
source_binding_token: managedSessionBindingToken(sourceSession),
|
|
6008
|
-
|
|
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
|
|
6012
|
-
const
|
|
6013
|
-
|
|
6014
|
-
|
|
6015
|
-
|
|
6016
|
-
return
|
|
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
|
-
|
|
6035
|
-
|
|
6036
|
-
|
|
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
|
|
6040
|
-
|
|
6041
|
-
|
|
6042
|
-
|
|
6043
|
-
|
|
6044
|
-
|
|
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
|
-
|
|
6048
|
-
|
|
6049
|
-
|
|
6050
|
-
|
|
6051
|
-
|
|
6052
|
-
|
|
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
|
-
|
|
6055
|
-
|
|
6056
|
-
|
|
6057
|
-
|
|
6058
|
-
|
|
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
|
-
|
|
6061
|
-
|
|
6062
|
-
|
|
6063
|
-
|
|
6064
|
-
|
|
6065
|
-
|
|
6066
|
-
|
|
6067
|
-
|
|
6068
|
-
|
|
6069
|
-
|
|
6070
|
-
|
|
6071
|
-
|
|
6072
|
-
|
|
6073
|
-
|
|
6074
|
-
|
|
6075
|
-
|
|
6076
|
-
|
|
6077
|
-
|
|
6078
|
-
|
|
6079
|
-
|
|
6080
|
-
|
|
6081
|
-
|
|
6082
|
-
|
|
6083
|
-
|
|
6084
|
-
|
|
6085
|
-
|
|
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 =
|
|
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 =
|
|
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 ||
|
|
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, {
|
|
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
|
-
|
|
7834
|
-
|
|
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,
|
|
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
|
-
|
|
7976
|
-
|
|
7977
|
-
|
|
7978
|
-
|
|
7979
|
-
|
|
7980
|
-
|
|
7981
|
-
|
|
7982
|
-
});
|
|
7983
|
-
|
|
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, {
|
|
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 !==
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
9620
|
-
|
|
11163
|
+
const deferredCodexForegroundBinding = !handoff.adopted
|
|
11164
|
+
? await prepareDeferredCodexForegroundBinding({
|
|
9621
11165
|
options,
|
|
9622
11166
|
terminal: terminalConversation,
|
|
9623
|
-
|
|
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:
|
|
11213
|
+
sessionId: deferredCodexForegroundBinding?.targetSessionId ??
|
|
11214
|
+
createManagedSessionId(),
|
|
9647
11215
|
terminal: terminalConversation,
|
|
9648
11216
|
identity: currentNativeIdentity,
|
|
9649
|
-
lineage:
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
11143
|
-
|
|
11144
|
-
|
|
11145
|
-
|
|
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
|
-
|
|
11159
|
-
|
|
11160
|
-
|
|
11161
|
-
|
|
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
|
-
|
|
11224
|
-
|
|
11225
|
-
|
|
11226
|
-
|
|
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
|
-
:
|
|
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
|
-
:
|
|
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
|
-
|
|
11545
|
-
|
|
11546
|
-
|
|
11547
|
-
|
|
11548
|
-
|
|
11549
|
-
|
|
11550
|
-
|
|
11551
|
-
|
|
11552
|
-
|
|
11553
|
-
|
|
11554
|
-
|
|
11555
|
-
|
|
11556
|
-
|
|
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
|
-
|
|
11614
|
-
|
|
11615
|
-
|
|
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
|
-
|
|
11839
|
-
|
|
11840
|
-
|
|
11841
|
-
|
|
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
|
|
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 =
|
|
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 =
|
|
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(
|
|
16486
|
+
? acquireTerminalBridgeSendLock(closeStoreDir, terminalControl, { timeoutMs: 30000 })
|
|
14653
16487
|
: () => { };
|
|
14654
|
-
|
|
14655
|
-
|
|
14656
|
-
|
|
14657
|
-
|
|
14658
|
-
|
|
14659
|
-
|
|
14660
|
-
|
|
14661
|
-
|
|
14662
|
-
|
|
14663
|
-
|
|
14664
|
-
|
|
14665
|
-
|
|
14666
|
-
|
|
14667
|
-
|
|
14668
|
-
|
|
14669
|
-
|
|
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
|
-
|
|
14686
|
-
|
|
14687
|
-
|
|
14688
|
-
|
|
14689
|
-
|
|
14690
|
-
|
|
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
|
-
|
|
14696
|
-
|
|
14697
|
-
|
|
14698
|
-
|
|
14699
|
-
|
|
14700
|
-
|
|
14701
|
-
|
|
14702
|
-
|
|
14703
|
-
|
|
14704
|
-
|
|
14705
|
-
|
|
14706
|
-
|
|
14707
|
-
|
|
14708
|
-
|
|
14709
|
-
|
|
14710
|
-
|
|
14711
|
-
|
|
14712
|
-
|
|
14713
|
-
|
|
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
|
-
|
|
14722
|
-
|
|
14723
|
-
|
|
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
|
-
|
|
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
|
-
|
|
17352
|
-
|
|
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
|
-
|
|
17356
|
-
|
|
17357
|
-
|
|
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
|
-
|
|
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
|
-
|
|
17363
|
-
|
|
17364
|
-
|
|
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
|
-
|
|
17368
|
-
|
|
17369
|
-
|
|
17370
|
-
|
|
17371
|
-
|
|
17372
|
-
|
|
17373
|
-
|
|
17374
|
-
|
|
17375
|
-
|
|
17376
|
-
|
|
17377
|
-
|
|
17378
|
-
|
|
17379
|
-
|
|
17380
|
-
|
|
17381
|
-
|
|
17382
|
-
|
|
17383
|
-
|
|
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
|
-
|
|
17388
|
-
|
|
17389
|
-
|
|
17390
|
-
|
|
17391
|
-
|
|
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
|
-
...
|
|
20306
|
+
...terminalBindingLedgerFields(acceptedConversation),
|
|
17394
20307
|
status: "agent_accepted",
|
|
17395
|
-
|
|
17396
|
-
|
|
17397
|
-
|
|
17398
|
-
|
|
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
|
-
|
|
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
|
|
20326
|
+
return { conversation: acceptedConversation, identity };
|
|
17403
20327
|
}
|
|
17404
|
-
|
|
17405
|
-
|
|
17406
|
-
|
|
17407
|
-
|
|
17408
|
-
|
|
17409
|
-
|
|
17410
|
-
|
|
17411
|
-
|
|
17412
|
-
|
|
17413
|
-
|
|
17414
|
-
|
|
17415
|
-
|
|
17416
|
-
|
|
17417
|
-
|
|
17418
|
-
|
|
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
|
-
|
|
17441
|
-
const
|
|
17442
|
-
|
|
17443
|
-
.
|
|
17444
|
-
|
|
17445
|
-
|
|
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
|
-
|
|
17449
|
-
|
|
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
|
-
|
|
17458
|
-
|
|
17459
|
-
|
|
17460
|
-
|
|
17461
|
-
|
|
17462
|
-
|
|
17463
|
-
|
|
17464
|
-
|
|
17465
|
-
|
|
17466
|
-
|
|
17467
|
-
|
|
17468
|
-
|
|
17469
|
-
|
|
17470
|
-
|
|
17471
|
-
|
|
17472
|
-
|
|
17473
|
-
|
|
17474
|
-
|
|
17475
|
-
|
|
17476
|
-
|
|
17477
|
-
|
|
17478
|
-
:
|
|
17479
|
-
|
|
17480
|
-
|
|
17481
|
-
|
|
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 (["
|
|
17490
|
-
|
|
17491
|
-
|
|
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 (
|
|
17494
|
-
|
|
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
|
-
|
|
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
|
-
|
|
18412
|
-
|
|
18413
|
-
|
|
18414
|
-
}
|
|
18415
|
-
const
|
|
18416
|
-
|
|
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, {
|
|
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;
|