@scotthuang/agent-knock-knock 0.12.2 → 0.12.4
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 +4 -4
- package/dist/src/cli-core.js +1105 -176
- package/dist/src/cli-core.js.map +1 -1
- 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/terminal-agent-bridge.d.ts +37 -3
- package/dist/src/terminal-agent-bridge.js +410 -31
- 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 +3 -3
package/dist/src/cli-core.js
CHANGED
|
@@ -20,13 +20,13 @@ import { createManagedSessionId, createNativeThreadTransitionId, humanObservedHa
|
|
|
20
20
|
import { classifyCodexLifecyclePostcondition, evaluateResumeCandidateAvailability, hasStrongCodexLifecycleIdentity, isFreshCodexPostProbeScreen } from "./native-thread-lifecycle-policy.js";
|
|
21
21
|
import { createNativeThreadResumeSnapshot, nativeThreadCandidateSnapshotFingerprint, nativeThreadResumeSnapshotRowsMatchCandidates, resolveNativeThreadResumeSelection, saveNativeThreadResumeSnapshot, sortNativeThreadCandidates, terminalActionFingerprint, verifiedPreviousResumeCandidate } from "./native-thread-resume-snapshot.js";
|
|
22
22
|
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";
|
|
23
|
+
import { createTerminalControlProviderRegistry as createProviderRegistry, StaticTerminalControlProvider, TerminalControlUnavailableError, TmuxTerminalControlProvider } from "./terminal-control-provider.js";
|
|
24
24
|
import { HerdrTerminalControlProvider } from "./herdr-terminal-control-provider.js";
|
|
25
25
|
import { parseTerminalConversationId } from "./terminal-agent-adapter.js";
|
|
26
26
|
import { associateTerminalEndpointEvidence, hasCanonicalTerminalEndpoint, sameTerminalControlEvidenceIncarnation, sameTerminalControlIncarnation, terminalControlEvidence, terminalControlEvidenceMatches, terminalEndpointFromControlRef, terminalEndpointIdentityFromEvidence, terminalEndpointIdentityKey, terminalLegacyRuntimeRoute, sameTerminalEndpointIdentity } from "./terminal-control-ref.js";
|
|
27
27
|
import { createProductionTerminalAgentRegistry } from "./terminal-agent-registry.js";
|
|
28
28
|
import { parseProcessElapsedSeconds, StaticTerminalProcessSource, SystemTerminalProcessSource } from "./terminal-process-source.js";
|
|
29
|
-
import { isExactClaudeNativeInspectionIdleComposer, NativeInspectionSubmissionError, TerminalAgentBridge, TerminalInputNotStartedError } from "./terminal-agent-bridge.js";
|
|
29
|
+
import { isExactClaudeIdleComposer, isExactClaudeNativeInspectionIdleComposer, NativeInspectionSubmissionError, TerminalAgentBridge, TerminalInputNotStartedError } from "./terminal-agent-bridge.js";
|
|
30
30
|
import { evaluateApprovalPolicy } from "./approval-policy.js";
|
|
31
31
|
import { evaluateDoctorCapabilities, runDoctorCapabilityProbes } from "./doctor-capabilities.js";
|
|
32
32
|
import { runOpenClawChainDiagnostics } from "./openclaw-doctor.js";
|
|
@@ -96,6 +96,7 @@ const SESSION_SEND_BLOCKING_STATUSES = new Set([
|
|
|
96
96
|
"callback_failed",
|
|
97
97
|
"cancelling"
|
|
98
98
|
]);
|
|
99
|
+
const TERMINAL_BRIDGE_UNCERTAIN_COLLATERAL_STALL_REASON = "a newer terminal submission has an uncertain outcome; inspect the shared terminal pane before continuing";
|
|
99
100
|
const TERMINAL_BRIDGE_MONITOR_LOCK_VERSION = 1;
|
|
100
101
|
const MINIMUM_NODE_VERSION = "22.19.0";
|
|
101
102
|
const PRIVATE_LOCK_FILE_MODE = 0o600;
|
|
@@ -2627,7 +2628,7 @@ function stallOtherTerminalBridgeConversationsForUncertainDispatch({ storeDir, t
|
|
|
2627
2628
|
const stalledConversationIds = [];
|
|
2628
2629
|
for (const listed of listConversations(storeDir)) {
|
|
2629
2630
|
if (listed.conversation_id === currentConversationId ||
|
|
2630
|
-
!
|
|
2631
|
+
!SESSION_SEND_BLOCKING_STATUSES.has(listed.status)) {
|
|
2631
2632
|
continue;
|
|
2632
2633
|
}
|
|
2633
2634
|
const listedTakeover = isRecord(listed.native_session_takeover)
|
|
@@ -2647,7 +2648,7 @@ function stallOtherTerminalBridgeConversationsForUncertainDispatch({ storeDir, t
|
|
|
2647
2648
|
const currentTakeover = isRecord(current.native_session_takeover)
|
|
2648
2649
|
? current.native_session_takeover
|
|
2649
2650
|
: undefined;
|
|
2650
|
-
if (!
|
|
2651
|
+
if (!SESSION_SEND_BLOCKING_STATUSES.has(current.status) ||
|
|
2651
2652
|
currentTakeover?.terminal_bridge !== true ||
|
|
2652
2653
|
!terminalControlsShareIncarnation(terminalControlFromTakeover(currentTakeover), terminalControl)) {
|
|
2653
2654
|
continue;
|
|
@@ -2657,12 +2658,13 @@ function stallOtherTerminalBridgeConversationsForUncertainDispatch({ storeDir, t
|
|
|
2657
2658
|
...current,
|
|
2658
2659
|
status: "stalled",
|
|
2659
2660
|
stalled_at: stalledAt,
|
|
2660
|
-
stalled_reason:
|
|
2661
|
+
stalled_reason: TERMINAL_BRIDGE_UNCERTAIN_COLLATERAL_STALL_REASON,
|
|
2661
2662
|
native_session_takeover: {
|
|
2662
2663
|
...currentTakeover,
|
|
2663
2664
|
terminal_bridge_uncertain_dispatch_fence: {
|
|
2664
2665
|
message_id: uncertainMessageId,
|
|
2665
|
-
observed_at: stalledAt
|
|
2666
|
+
observed_at: stalledAt,
|
|
2667
|
+
previous_status: current.status
|
|
2666
2668
|
}
|
|
2667
2669
|
},
|
|
2668
2670
|
updated_at: stalledAt
|
|
@@ -2688,6 +2690,303 @@ function stallOtherTerminalBridgeConversationsForUncertainDispatch({ storeDir, t
|
|
|
2688
2690
|
}
|
|
2689
2691
|
return stalledConversationIds;
|
|
2690
2692
|
}
|
|
2693
|
+
function exactTerminalBridgeCollateralStallRepairEvidence({ conversation, storeDir }) {
|
|
2694
|
+
if (conversation.status !== "stalled" ||
|
|
2695
|
+
conversation.stalled_reason !==
|
|
2696
|
+
TERMINAL_BRIDGE_UNCERTAIN_COLLATERAL_STALL_REASON) {
|
|
2697
|
+
return undefined;
|
|
2698
|
+
}
|
|
2699
|
+
const takeover = isRecord(conversation.native_session_takeover)
|
|
2700
|
+
? conversation.native_session_takeover
|
|
2701
|
+
: undefined;
|
|
2702
|
+
const fence = isRecord(takeover?.terminal_bridge_uncertain_dispatch_fence)
|
|
2703
|
+
? takeover.terminal_bridge_uncertain_dispatch_fence
|
|
2704
|
+
: undefined;
|
|
2705
|
+
const uncertainMessageId = stringValue(fence?.message_id);
|
|
2706
|
+
const fenceObservedAt = stringValue(fence?.observed_at);
|
|
2707
|
+
const previousStatus = stringValue(fence?.previous_status);
|
|
2708
|
+
const ownMessageId = stringValue(takeover?.terminal_bridge_message_id);
|
|
2709
|
+
const ownSubmission = terminalBridgeSubmission(conversation);
|
|
2710
|
+
const completionClaim = isRecord(takeover?.terminal_bridge_completion_claim)
|
|
2711
|
+
? takeover.terminal_bridge_completion_claim
|
|
2712
|
+
: undefined;
|
|
2713
|
+
const callbackDelivery = isRecord(conversation.callback_delivery)
|
|
2714
|
+
? conversation.callback_delivery
|
|
2715
|
+
: undefined;
|
|
2716
|
+
const callbackMessage = isRecord(callbackDelivery?.message)
|
|
2717
|
+
? callbackDelivery.message
|
|
2718
|
+
: undefined;
|
|
2719
|
+
const idleSince = stringValue(conversation.idle_since);
|
|
2720
|
+
const deliveredAt = stringValue(callbackDelivery?.delivered_at);
|
|
2721
|
+
const claimedAt = stringValue(completionClaim?.claimed_at);
|
|
2722
|
+
const fenceAtMs = validTimestampMs(fenceObservedAt);
|
|
2723
|
+
const idleAtMs = validTimestampMs(idleSince);
|
|
2724
|
+
const deliveredAtMs = validTimestampMs(deliveredAt);
|
|
2725
|
+
const claimedAtMs = validTimestampMs(claimedAt);
|
|
2726
|
+
if (takeover?.terminal_bridge !== true ||
|
|
2727
|
+
!uncertainMessageId ||
|
|
2728
|
+
!fenceObservedAt ||
|
|
2729
|
+
fenceAtMs === undefined ||
|
|
2730
|
+
uncertainMessageId === ownMessageId ||
|
|
2731
|
+
(previousStatus !== undefined && previousStatus !== "idle") ||
|
|
2732
|
+
conversation.stalled_at !== fenceObservedAt ||
|
|
2733
|
+
conversation.updated_at !== fenceObservedAt ||
|
|
2734
|
+
!idleSince ||
|
|
2735
|
+
idleAtMs === undefined ||
|
|
2736
|
+
idleAtMs > fenceAtMs ||
|
|
2737
|
+
!ownMessageId ||
|
|
2738
|
+
ownSubmission?.status !== "agent_accepted" ||
|
|
2739
|
+
stringValue(ownSubmission.message_id) !== ownMessageId ||
|
|
2740
|
+
stringValue(ownSubmission.session_id) !==
|
|
2741
|
+
sessionIdForConversation(conversation) ||
|
|
2742
|
+
stringValue(ownSubmission.turn_id) !== turnIdForConversation(conversation) ||
|
|
2743
|
+
!completionClaim ||
|
|
2744
|
+
stringValue(completionClaim.terminal_bridge_message_id) !== ownMessageId ||
|
|
2745
|
+
completionClaim.outcome !== "success" ||
|
|
2746
|
+
!claimedAt ||
|
|
2747
|
+
claimedAtMs === undefined ||
|
|
2748
|
+
claimedAtMs > fenceAtMs ||
|
|
2749
|
+
callbackDelivery?.status !== "delivered" ||
|
|
2750
|
+
callbackDelivery.final_status !== "idle" ||
|
|
2751
|
+
callbackDelivery.preserve_conversation_status !== true ||
|
|
2752
|
+
!callbackMessage ||
|
|
2753
|
+
callbackMessage.type !== "done" ||
|
|
2754
|
+
callbackMessage.requires_response !== false ||
|
|
2755
|
+
stringValue(callbackMessage.id) !==
|
|
2756
|
+
stringValue(completionClaim.callback_message_id) ||
|
|
2757
|
+
stringValue(callbackMessage.conversation_id) !==
|
|
2758
|
+
conversation.conversation_id ||
|
|
2759
|
+
stringValue(callbackMessage.session_id) !==
|
|
2760
|
+
sessionIdForConversation(conversation) ||
|
|
2761
|
+
stringValue(callbackMessage.turn_id) !== turnIdForConversation(conversation) ||
|
|
2762
|
+
stringValue(isRecord(callbackMessage.metadata)
|
|
2763
|
+
? callbackMessage.metadata.terminal_bridge_message_id
|
|
2764
|
+
: undefined) !== ownMessageId ||
|
|
2765
|
+
!deliveredAt ||
|
|
2766
|
+
deliveredAtMs === undefined ||
|
|
2767
|
+
deliveredAtMs > fenceAtMs) {
|
|
2768
|
+
return undefined;
|
|
2769
|
+
}
|
|
2770
|
+
const control = terminalControlForManagedConversation(conversation);
|
|
2771
|
+
if (!control) {
|
|
2772
|
+
return undefined;
|
|
2773
|
+
}
|
|
2774
|
+
// The target state lock is already held by the caller. Rescan owner state
|
|
2775
|
+
// now instead of trusting the unlocked discovery snapshot used to find the
|
|
2776
|
+
// target candidate.
|
|
2777
|
+
const ownerCandidates = listConversations(storeDir).filter((candidate) => {
|
|
2778
|
+
if (candidate.conversation_id === conversation.conversation_id) {
|
|
2779
|
+
return false;
|
|
2780
|
+
}
|
|
2781
|
+
const candidateTakeover = isRecord(candidate.native_session_takeover)
|
|
2782
|
+
? candidate.native_session_takeover
|
|
2783
|
+
: undefined;
|
|
2784
|
+
const candidateSubmission = terminalBridgeSubmission(candidate);
|
|
2785
|
+
return candidateTakeover?.terminal_bridge === true &&
|
|
2786
|
+
stringValue(candidateTakeover.terminal_bridge_message_id) ===
|
|
2787
|
+
uncertainMessageId &&
|
|
2788
|
+
stringValue(candidateSubmission?.message_id) === uncertainMessageId &&
|
|
2789
|
+
terminalControlsShareIncarnation(terminalControlForManagedConversation(candidate), control);
|
|
2790
|
+
});
|
|
2791
|
+
if (ownerCandidates.length !== 1) {
|
|
2792
|
+
return undefined;
|
|
2793
|
+
}
|
|
2794
|
+
const ownerListed = ownerCandidates[0];
|
|
2795
|
+
const ownerStatePath = stringValue(ownerListed.state_path);
|
|
2796
|
+
if (!ownerStatePath) {
|
|
2797
|
+
return undefined;
|
|
2798
|
+
}
|
|
2799
|
+
let owner;
|
|
2800
|
+
let ownerEvents;
|
|
2801
|
+
let events;
|
|
2802
|
+
try {
|
|
2803
|
+
owner = loadState(ownerStatePath);
|
|
2804
|
+
ownerEvents = readNdjsonLog(stringValue(owner.event_log_path) ?? logPathForStatePath(ownerStatePath));
|
|
2805
|
+
const statePath = stringValue(conversation.state_path);
|
|
2806
|
+
if (!statePath) {
|
|
2807
|
+
return undefined;
|
|
2808
|
+
}
|
|
2809
|
+
events = readNdjsonLog(stringValue(conversation.event_log_path) ??
|
|
2810
|
+
logPathForStatePath(statePath));
|
|
2811
|
+
}
|
|
2812
|
+
catch {
|
|
2813
|
+
return undefined;
|
|
2814
|
+
}
|
|
2815
|
+
const ownerTakeover = isRecord(owner.native_session_takeover)
|
|
2816
|
+
? owner.native_session_takeover
|
|
2817
|
+
: undefined;
|
|
2818
|
+
const ownerSubmission = terminalBridgeSubmission(owner);
|
|
2819
|
+
const ownerClosedAt = stringValue(owner.closed_at);
|
|
2820
|
+
const ownerClosedAtMs = validTimestampMs(ownerClosedAt);
|
|
2821
|
+
if (owner.conversation_id !== ownerListed.conversation_id ||
|
|
2822
|
+
owner.status !== "closed" ||
|
|
2823
|
+
!ownerClosedAt ||
|
|
2824
|
+
ownerClosedAtMs === undefined ||
|
|
2825
|
+
ownerClosedAtMs < fenceAtMs ||
|
|
2826
|
+
!stringValue(owner.close_reason) ||
|
|
2827
|
+
owner.updated_at !== ownerClosedAt ||
|
|
2828
|
+
ownerTakeover?.terminal_bridge !== true ||
|
|
2829
|
+
stringValue(ownerTakeover.terminal_bridge_message_id) !==
|
|
2830
|
+
uncertainMessageId ||
|
|
2831
|
+
ownerSubmission?.status !== "uncertain" ||
|
|
2832
|
+
stringValue(ownerSubmission.message_id) !== uncertainMessageId ||
|
|
2833
|
+
stringValue(ownerSubmission.session_id) !== sessionIdForConversation(owner) ||
|
|
2834
|
+
stringValue(ownerSubmission.turn_id) !== turnIdForConversation(owner) ||
|
|
2835
|
+
isRecord(ownerTakeover.terminal_bridge_uncertain_dispatch_fence) ||
|
|
2836
|
+
!terminalControlsShareIncarnation(terminalControlForManagedConversation(owner), control)) {
|
|
2837
|
+
return undefined;
|
|
2838
|
+
}
|
|
2839
|
+
const callbackMessageId = stringValue(callbackMessage.id);
|
|
2840
|
+
const hasCompletionClaim = events.some((event) => event.event === "terminal_bridge_completion_claimed" &&
|
|
2841
|
+
event.conversation_id === conversation.conversation_id &&
|
|
2842
|
+
event.terminal_bridge_message_id === ownMessageId &&
|
|
2843
|
+
event.callback_message_id === callbackMessageId &&
|
|
2844
|
+
event.completion_fingerprint === completionClaim.completion_fingerprint &&
|
|
2845
|
+
event.completion_id === completionClaim.completion_id &&
|
|
2846
|
+
event.outcome === "success" &&
|
|
2847
|
+
event.ts === claimedAt);
|
|
2848
|
+
const hasCompletionDetected = events.some((event) => event.event === "terminal_bridge_completion_detected" &&
|
|
2849
|
+
event.conversation_id === conversation.conversation_id &&
|
|
2850
|
+
event.terminal_bridge_message_id === ownMessageId &&
|
|
2851
|
+
event.callback_message_id === callbackMessageId &&
|
|
2852
|
+
event.completion_id === completionClaim.completion_id &&
|
|
2853
|
+
event.completion_outcome === "success" &&
|
|
2854
|
+
validTimestampMs(event.ts) !== undefined &&
|
|
2855
|
+
validTimestampMs(event.ts) <= fenceAtMs);
|
|
2856
|
+
const hasDeliveredCallback = events.some((event) => event.event === "callback_delivery_succeeded" &&
|
|
2857
|
+
event.conversation_id === conversation.conversation_id &&
|
|
2858
|
+
event.message_id === callbackMessageId &&
|
|
2859
|
+
event.status === "idle" &&
|
|
2860
|
+
event.ts === deliveredAt);
|
|
2861
|
+
const hasExactFence = events.some((event) => event.event === "terminal_bridge_stalled_by_uncertain_dispatch" &&
|
|
2862
|
+
event.conversation_id === conversation.conversation_id &&
|
|
2863
|
+
event.uncertain_message_id === uncertainMessageId &&
|
|
2864
|
+
event.ts === fenceObservedAt);
|
|
2865
|
+
const hasExactOwnerClose = ownerEvents.some((event) => event.event === "conversation_closed" &&
|
|
2866
|
+
event.conversation_id === owner.conversation_id &&
|
|
2867
|
+
event.status === "closed" &&
|
|
2868
|
+
event.ts === ownerClosedAt &&
|
|
2869
|
+
event.reason === owner.close_reason);
|
|
2870
|
+
if (!hasCompletionClaim ||
|
|
2871
|
+
!hasCompletionDetected ||
|
|
2872
|
+
!hasDeliveredCallback ||
|
|
2873
|
+
!hasExactFence ||
|
|
2874
|
+
!hasExactOwnerClose) {
|
|
2875
|
+
return undefined;
|
|
2876
|
+
}
|
|
2877
|
+
return {
|
|
2878
|
+
uncertainMessageId,
|
|
2879
|
+
ownerConversationId: owner.conversation_id,
|
|
2880
|
+
restoredStatus: "idle"
|
|
2881
|
+
};
|
|
2882
|
+
}
|
|
2883
|
+
function reconcileTerminalBridgeCollateralStalls(storeDir, conversationId) {
|
|
2884
|
+
const candidates = listConversations(storeDir).filter((conversation) => {
|
|
2885
|
+
const takeover = isRecord(conversation.native_session_takeover)
|
|
2886
|
+
? conversation.native_session_takeover
|
|
2887
|
+
: undefined;
|
|
2888
|
+
return (conversationId === undefined ||
|
|
2889
|
+
conversation.conversation_id === conversationId) &&
|
|
2890
|
+
conversation.status === "stalled" &&
|
|
2891
|
+
isRecord(takeover?.terminal_bridge_uncertain_dispatch_fence);
|
|
2892
|
+
});
|
|
2893
|
+
let repaired = 0;
|
|
2894
|
+
let skipped = 0;
|
|
2895
|
+
const errors = [];
|
|
2896
|
+
const items = [];
|
|
2897
|
+
for (const listed of candidates) {
|
|
2898
|
+
const statePath = stringValue(listed.state_path);
|
|
2899
|
+
if (!statePath) {
|
|
2900
|
+
skipped += 1;
|
|
2901
|
+
continue;
|
|
2902
|
+
}
|
|
2903
|
+
let releaseStateLock;
|
|
2904
|
+
try {
|
|
2905
|
+
releaseStateLock = acquireFileLock(`${statePath}.lock`);
|
|
2906
|
+
const current = loadState(statePath);
|
|
2907
|
+
const evidence = exactTerminalBridgeCollateralStallRepairEvidence({
|
|
2908
|
+
conversation: current,
|
|
2909
|
+
storeDir
|
|
2910
|
+
});
|
|
2911
|
+
if (!evidence) {
|
|
2912
|
+
skipped += 1;
|
|
2913
|
+
continue;
|
|
2914
|
+
}
|
|
2915
|
+
const takeover = {
|
|
2916
|
+
...current.native_session_takeover
|
|
2917
|
+
};
|
|
2918
|
+
delete takeover.terminal_bridge_uncertain_dispatch_fence;
|
|
2919
|
+
const repairedAt = cliNow().toISOString();
|
|
2920
|
+
takeover.terminal_bridge_collateral_stall_repair = {
|
|
2921
|
+
repaired_at: repairedAt,
|
|
2922
|
+
uncertain_message_id: evidence.uncertainMessageId,
|
|
2923
|
+
uncertain_owner_conversation_id: evidence.ownerConversationId,
|
|
2924
|
+
restored_status: evidence.restoredStatus,
|
|
2925
|
+
evidence: "foreign_uncertain_fence+completion_claim+delivered_callback+closed_owner"
|
|
2926
|
+
};
|
|
2927
|
+
const repairedConversation = {
|
|
2928
|
+
...current,
|
|
2929
|
+
status: evidence.restoredStatus,
|
|
2930
|
+
native_session_takeover: takeover,
|
|
2931
|
+
updated_at: repairedAt
|
|
2932
|
+
};
|
|
2933
|
+
delete repairedConversation.stalled_at;
|
|
2934
|
+
delete repairedConversation.stalled_reason;
|
|
2935
|
+
saveState(statePath, repairedConversation);
|
|
2936
|
+
let eventWarning;
|
|
2937
|
+
try {
|
|
2938
|
+
appendEvent(stringValue(current.event_log_path) ?? logPathForStatePath(statePath), {
|
|
2939
|
+
ts: repairedAt,
|
|
2940
|
+
conversation_id: current.conversation_id,
|
|
2941
|
+
event: "terminal_bridge_collateral_stall_repaired",
|
|
2942
|
+
uncertain_message_id: evidence.uncertainMessageId,
|
|
2943
|
+
uncertain_owner_conversation_id: evidence.ownerConversationId,
|
|
2944
|
+
previous_status: "stalled",
|
|
2945
|
+
restored_status: evidence.restoredStatus,
|
|
2946
|
+
evidence: "foreign_uncertain_fence+completion_claim+delivered_callback+closed_owner"
|
|
2947
|
+
});
|
|
2948
|
+
}
|
|
2949
|
+
catch (error) {
|
|
2950
|
+
eventWarning = error instanceof Error ? error.message : String(error);
|
|
2951
|
+
runtimeLog("warn", "terminal_bridge_collateral_stall_repair_event_failed", {
|
|
2952
|
+
conversation_id: current.conversation_id,
|
|
2953
|
+
uncertain_message_id: evidence.uncertainMessageId,
|
|
2954
|
+
error: eventWarning
|
|
2955
|
+
});
|
|
2956
|
+
}
|
|
2957
|
+
repaired += 1;
|
|
2958
|
+
items.push({
|
|
2959
|
+
conversation_id: current.conversation_id,
|
|
2960
|
+
status: "repaired",
|
|
2961
|
+
reason: "legacy_terminal_bridge_collateral_stall",
|
|
2962
|
+
uncertain_message_id: evidence.uncertainMessageId,
|
|
2963
|
+
uncertain_owner_conversation_id: evidence.ownerConversationId,
|
|
2964
|
+
restored_status: evidence.restoredStatus,
|
|
2965
|
+
...(eventWarning ? { event_warning: eventWarning } : {})
|
|
2966
|
+
});
|
|
2967
|
+
}
|
|
2968
|
+
catch (error) {
|
|
2969
|
+
skipped += 1;
|
|
2970
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
2971
|
+
errors.push(`${listed.conversation_id}: ${reason}`);
|
|
2972
|
+
items.push({
|
|
2973
|
+
conversation_id: listed.conversation_id,
|
|
2974
|
+
status: "error",
|
|
2975
|
+
reason
|
|
2976
|
+
});
|
|
2977
|
+
}
|
|
2978
|
+
finally {
|
|
2979
|
+
releaseStateLock?.();
|
|
2980
|
+
}
|
|
2981
|
+
}
|
|
2982
|
+
return {
|
|
2983
|
+
checked: candidates.length,
|
|
2984
|
+
repaired,
|
|
2985
|
+
skipped,
|
|
2986
|
+
errors,
|
|
2987
|
+
items
|
|
2988
|
+
};
|
|
2989
|
+
}
|
|
2691
2990
|
function environmentWithoutGatewayTokens() {
|
|
2692
2991
|
const environment = { ...cliEnv() };
|
|
2693
2992
|
delete environment.AKK_GATEWAY_TOKEN;
|
|
@@ -2768,17 +3067,21 @@ async function reconcileStoreForList(storeDir, options) {
|
|
|
2768
3067
|
}
|
|
2769
3068
|
throw error;
|
|
2770
3069
|
}
|
|
2771
|
-
const idle = reconcileIdleConversations(storeDir, options);
|
|
2772
3070
|
const monitors = await reconcileMonitors(options, {
|
|
2773
3071
|
includeCallbackRecovery: false,
|
|
2774
3072
|
reason: "list_reconciliation",
|
|
2775
3073
|
conversationId: undefined
|
|
2776
3074
|
});
|
|
3075
|
+
const idle = reconcileIdleConversations(storeDir, options);
|
|
2777
3076
|
return {
|
|
2778
3077
|
status: "completed",
|
|
2779
3078
|
checked: Math.max(idle.checked, monitors.checked),
|
|
2780
|
-
changed: idle.closed + monitors.launched,
|
|
3079
|
+
changed: idle.closed + monitors.launched + monitors.repaired,
|
|
2781
3080
|
closed: idle.closed,
|
|
3081
|
+
repaired: monitors.repaired,
|
|
3082
|
+
collateral_stalls_checked: monitors.collateral_stalls_checked,
|
|
3083
|
+
collateral_stalls_skipped: monitors.collateral_stalls_skipped,
|
|
3084
|
+
collateral_stall_repairs: monitors.items.filter((item) => item.status === "repaired"),
|
|
2782
3085
|
monitors_launched: monitors.launched,
|
|
2783
3086
|
monitors_already_running: monitors.already_running,
|
|
2784
3087
|
skipped: idle.skipped + monitors.skipped,
|
|
@@ -2916,24 +3219,24 @@ async function terminalControlledListEntry(session, activeSessions, options, bri
|
|
|
2916
3219
|
});
|
|
2917
3220
|
}
|
|
2918
3221
|
}
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
3222
|
+
const nativeIdentityObservation = await observeCurrentNativeAgentSessionIdentity({
|
|
3223
|
+
options,
|
|
3224
|
+
agent: session.agent,
|
|
3225
|
+
pid: session.pid,
|
|
3226
|
+
cwd: session.cwd ?? terminalControl.currentPath,
|
|
3227
|
+
preferredSessionId: codexIdentityContext?.preferredSessionId,
|
|
3228
|
+
allowedCompanionIdentity: codexIdentityContext?.companions.primary,
|
|
3229
|
+
allowedAdditionalIdentities: codexIdentityContext?.companions.additional
|
|
3230
|
+
});
|
|
3231
|
+
const nativeAgentIdentity = nativeIdentityObservation.status === "resolved"
|
|
3232
|
+
? nativeIdentityObservation.identity
|
|
3233
|
+
: undefined;
|
|
3234
|
+
if (nativeIdentityObservation.status === "unavailable") {
|
|
2932
3235
|
runtimeLog("warn", "terminal_native_session_identity_unavailable", {
|
|
2933
3236
|
agent: session.agent,
|
|
2934
3237
|
terminal_target: terminalControl.target,
|
|
2935
3238
|
pid: session.pid,
|
|
2936
|
-
error:
|
|
3239
|
+
error: nativeIdentityObservation.reason
|
|
2937
3240
|
});
|
|
2938
3241
|
}
|
|
2939
3242
|
let nativeProcessUuid = nativeAgentIdentity?.processUuid;
|
|
@@ -2999,27 +3302,25 @@ async function terminalControlledListEntry(session, activeSessions, options, bri
|
|
|
2999
3302
|
});
|
|
3000
3303
|
const codexLifecycleIncarnationAvailable = session.agent !== "codex" ||
|
|
3001
3304
|
Boolean(nativeProcessUuid && nativeProcessBirth);
|
|
3002
|
-
const
|
|
3003
|
-
|
|
3004
|
-
SESSION_SEND_BLOCKING_STATUSES.has(turn.status));
|
|
3005
|
-
let nativeInspectionComposerReady = nativeInspectionComposerEmpty(session.agent, terminalState.screen_excerpt);
|
|
3305
|
+
const terminalHasBlockingTurn = terminalIncarnationBlockingTurns(storeDirFromOptions(options), terminalControl).length > 0;
|
|
3306
|
+
let automatedInputComposerReady = nativeInspectionComposerEmpty(session.agent, terminalState.screen_excerpt);
|
|
3006
3307
|
if (session.agent === "codex" &&
|
|
3007
|
-
terminalState.activity_state === "idle"
|
|
3308
|
+
(terminalState.activity_state === "idle" ||
|
|
3309
|
+
(terminalState.activity_state === "unknown" &&
|
|
3310
|
+
nativeIdentityObservation.status === "verified_absent")) &&
|
|
3008
3311
|
terminalState.approval_state.blocked !== true &&
|
|
3009
|
-
nativeInspectionCapability.status === "supported" &&
|
|
3010
|
-
nativeInspectionCapability.statusInspection === true &&
|
|
3011
3312
|
terminalControl.capabilities.includes("send_keys") &&
|
|
3012
3313
|
terminalControl.capabilities.includes("screen_status")) {
|
|
3013
3314
|
try {
|
|
3014
3315
|
const provider = createTerminalControlProvider(options);
|
|
3015
3316
|
const resolvedTerminal = await provider.resolve(provider.endpoint(terminalControl));
|
|
3016
3317
|
const styledScreen = await provider.capture(resolvedTerminal, { scrollbackLines: 40, preserveEscapes: true });
|
|
3017
|
-
|
|
3318
|
+
automatedInputComposerReady = codexStyledComposerEmpty(styledScreen);
|
|
3018
3319
|
}
|
|
3019
3320
|
catch {
|
|
3020
3321
|
// Advertising an input action is optional. The action itself repeats the
|
|
3021
3322
|
// same styled composer proof under the terminal lock before any input.
|
|
3022
|
-
|
|
3323
|
+
automatedInputComposerReady = false;
|
|
3023
3324
|
}
|
|
3024
3325
|
}
|
|
3025
3326
|
const entry = {
|
|
@@ -3040,6 +3341,14 @@ async function terminalControlledListEntry(session, activeSessions, options, bri
|
|
|
3040
3341
|
native_agent_process_birth: nativeProcessBirth,
|
|
3041
3342
|
native_agent_rollout: nativeAgentIdentity?.rollout,
|
|
3042
3343
|
native_agent_identity_evidence: nativeProcessEvidence,
|
|
3344
|
+
native_agent_identity_observation: {
|
|
3345
|
+
status: nativeIdentityObservation.status,
|
|
3346
|
+
...(nativeIdentityObservation.status === "unavailable"
|
|
3347
|
+
? { reason: nativeIdentityObservation.reason }
|
|
3348
|
+
: nativeIdentityObservation.status === "verified_absent"
|
|
3349
|
+
? { evidence: nativeIdentityObservation.evidence }
|
|
3350
|
+
: {})
|
|
3351
|
+
},
|
|
3043
3352
|
agent_version: agentVersion,
|
|
3044
3353
|
native_thread_lifecycle: lifecycleCapability,
|
|
3045
3354
|
native_inspection: nativeInspectionCapability,
|
|
@@ -3050,6 +3359,10 @@ async function terminalControlledListEntry(session, activeSessions, options, bri
|
|
|
3050
3359
|
approval_state: terminalState.approval_state,
|
|
3051
3360
|
activity_state: terminalState.activity_state,
|
|
3052
3361
|
activity_reason: terminalState.activity_reason,
|
|
3362
|
+
// Internal action-projection evidence. terminalFirstListProjection strips
|
|
3363
|
+
// this field after gating every automated-input action that can follow a
|
|
3364
|
+
// human native-thread switch.
|
|
3365
|
+
_automated_input_composer_ready: automatedInputComposerReady,
|
|
3053
3366
|
...(orphanedDispatch
|
|
3054
3367
|
? {
|
|
3055
3368
|
orphaned_terminal_dispatch: {
|
|
@@ -3066,7 +3379,7 @@ async function terminalControlledListEntry(session, activeSessions, options, bri
|
|
|
3066
3379
|
}
|
|
3067
3380
|
: {}),
|
|
3068
3381
|
commands: {
|
|
3069
|
-
send:
|
|
3382
|
+
send: !terminalHasBlockingTurn,
|
|
3070
3383
|
approve: terminalControl.capabilities.includes("terminal_approval") &&
|
|
3071
3384
|
terminalState.approval_state.approvable === true,
|
|
3072
3385
|
status: true,
|
|
@@ -3074,13 +3387,15 @@ async function terminalControlledListEntry(session, activeSessions, options, bri
|
|
|
3074
3387
|
close: orphanedDispatch !== undefined,
|
|
3075
3388
|
new_thread: lifecycleCapability.status === "supported" &&
|
|
3076
3389
|
lifecycleCapability.newThread === true &&
|
|
3077
|
-
codexLifecycleIncarnationAvailable
|
|
3390
|
+
codexLifecycleIncarnationAvailable &&
|
|
3391
|
+
!terminalHasBlockingTurn,
|
|
3078
3392
|
list_resumable_threads: lifecycleCapability.status === "supported" &&
|
|
3079
3393
|
lifecycleCapability.resumeExact === true &&
|
|
3080
3394
|
codexLifecycleIncarnationAvailable,
|
|
3081
3395
|
native_inspect: nativeInspectionCapability.status === "supported" &&
|
|
3082
3396
|
nativeInspectionCapability.statusInspection === true &&
|
|
3083
|
-
|
|
3397
|
+
terminalState.activity_state === "idle" &&
|
|
3398
|
+
automatedInputComposerReady &&
|
|
3084
3399
|
terminalControl.capabilities.includes("send_keys") &&
|
|
3085
3400
|
terminalControl.capabilities.includes("screen_status") &&
|
|
3086
3401
|
(session.agent === "codex"
|
|
@@ -3088,7 +3403,7 @@ async function terminalControlledListEntry(session, activeSessions, options, bri
|
|
|
3088
3403
|
: Boolean(nativeAgentIdentity?.sessionId &&
|
|
3089
3404
|
nativeAgentIdentity.processUuid)) &&
|
|
3090
3405
|
orphanedDispatch === undefined &&
|
|
3091
|
-
!
|
|
3406
|
+
!terminalHasBlockingTurn
|
|
3092
3407
|
}
|
|
3093
3408
|
};
|
|
3094
3409
|
const availableActions = availableListActions(entry);
|
|
@@ -3106,6 +3421,7 @@ function terminalFirstListProjection({ storeDir, terminals, managedSessions, ses
|
|
|
3106
3421
|
return control ? [control] : [];
|
|
3107
3422
|
});
|
|
3108
3423
|
const projectedTerminals = terminals.map((terminal) => {
|
|
3424
|
+
const { _automated_input_composer_ready: automatedInputComposerReady, ...publicTerminal } = terminal;
|
|
3109
3425
|
const terminalControl = isRecord(terminal.terminal_control)
|
|
3110
3426
|
? terminal.terminal_control
|
|
3111
3427
|
: undefined;
|
|
@@ -3269,6 +3585,79 @@ function terminalFirstListProjection({ storeDir, terminals, managedSessions, ses
|
|
|
3269
3585
|
})
|
|
3270
3586
|
: undefined;
|
|
3271
3587
|
const blockingHandoffTurns = conflictingSessionTurns.filter((turn) => SESSION_SEND_BLOCKING_STATUSES.has(turn.status));
|
|
3588
|
+
const terminalBlockingTurns = terminalControl
|
|
3589
|
+
? terminalIncarnationBlockingTurns(storeDir, terminalControl, allRelated)
|
|
3590
|
+
: [];
|
|
3591
|
+
const externalHandoffSourceSessionIds = new Set(conflictingBoundSessionClaims
|
|
3592
|
+
.filter(({ kind }) => kind === "live_external_thread_change")
|
|
3593
|
+
.map(({ session }) => session.session_id));
|
|
3594
|
+
const handoffSourceBlockingTurns = terminalBlockingTurns.filter((turn) => externalHandoffSourceSessionIds.has(sessionIdForConversation(turn)));
|
|
3595
|
+
const nativeIdentityObservation = isRecord(terminal.native_agent_identity_observation)
|
|
3596
|
+
? terminal.native_agent_identity_observation
|
|
3597
|
+
: undefined;
|
|
3598
|
+
const verifiedEmptyProcessUuid = stringValue(terminal.native_agent_process_uuid);
|
|
3599
|
+
const verifiedEmptyProcessBirth = stringValue(terminal.native_agent_process_birth);
|
|
3600
|
+
const verifiedEmptyWorkspace = stringValue(terminal.workspace ?? terminal.cwd);
|
|
3601
|
+
const verifiedEmptySourceNativeThreadId = stringValue(soleBindingConflict?.session.binding?.native_thread_id)?.toLowerCase();
|
|
3602
|
+
const verifiedEmptySourceActiveElsewhere = Boolean(verifiedEmptySourceNativeThreadId &&
|
|
3603
|
+
terminals.some((candidate) => candidate !== terminal &&
|
|
3604
|
+
candidate.agent === "codex" &&
|
|
3605
|
+
stringValue(candidate.native_agent_session_id)?.toLowerCase() ===
|
|
3606
|
+
verifiedEmptySourceNativeThreadId));
|
|
3607
|
+
const verifiedEmptyRawSendAction = isRecord(rawActions.send)
|
|
3608
|
+
? rawActions.send
|
|
3609
|
+
: {
|
|
3610
|
+
tool: "agent_knock_knock_send",
|
|
3611
|
+
arguments: { selector: stringValue(terminal.id) },
|
|
3612
|
+
missing_required: ["request"]
|
|
3613
|
+
};
|
|
3614
|
+
const verifiedEmptyCodexHandoffEligible = Boolean(mutationsAllowed &&
|
|
3615
|
+
terminal.agent === "codex" &&
|
|
3616
|
+
terminalControl &&
|
|
3617
|
+
discoveredOwnership.state === "none" &&
|
|
3618
|
+
unresolvedSessionClaims.length === 0 &&
|
|
3619
|
+
matchingSessions.length === 0 &&
|
|
3620
|
+
soleBindingConflict?.kind === "unverifiable" &&
|
|
3621
|
+
nativeIdentityObservation?.status === "verified_absent" &&
|
|
3622
|
+
verifiedEmptyProcessUuid &&
|
|
3623
|
+
verifiedEmptyProcessBirth &&
|
|
3624
|
+
verifiedEmptyWorkspace &&
|
|
3625
|
+
exactBoundCodexSourceForVerifiedEmptyHandoff({
|
|
3626
|
+
session: soleBindingConflict.session,
|
|
3627
|
+
terminalId: String(terminal.id),
|
|
3628
|
+
terminalControl,
|
|
3629
|
+
pid: Number(terminal.pid),
|
|
3630
|
+
workspace: verifiedEmptyWorkspace,
|
|
3631
|
+
processUuid: verifiedEmptyProcessUuid,
|
|
3632
|
+
processBirth: verifiedEmptyProcessBirth
|
|
3633
|
+
}) &&
|
|
3634
|
+
["idle", "unknown"].includes(String(terminal.activity_state)) &&
|
|
3635
|
+
automatedInputComposerReady === true &&
|
|
3636
|
+
!(isRecord(terminal.approval_state) &&
|
|
3637
|
+
terminal.approval_state.blocked === true) &&
|
|
3638
|
+
blockingHandoffTurns.length === 0 &&
|
|
3639
|
+
terminalBlockingTurns.length === 0 &&
|
|
3640
|
+
!managedSessionHasUnresolvedNativeTransition(storeDir, soleBindingConflict.session) &&
|
|
3641
|
+
!verifiedEmptySourceActiveElsewhere &&
|
|
3642
|
+
terminal.orphaned_terminal_dispatch === undefined &&
|
|
3643
|
+
terminalControl?.capabilities.includes("send_keys") &&
|
|
3644
|
+
terminalControl.capabilities.includes("screen_status"));
|
|
3645
|
+
const verifiedEmptyCodexSnapshotToken = verifiedEmptyCodexHandoffEligible &&
|
|
3646
|
+
terminalControl &&
|
|
3647
|
+
soleBindingConflict &&
|
|
3648
|
+
verifiedEmptyProcessUuid &&
|
|
3649
|
+
verifiedEmptyProcessBirth &&
|
|
3650
|
+
verifiedEmptyWorkspace
|
|
3651
|
+
? verifiedEmptyCodexHandoffToken({
|
|
3652
|
+
terminalId: String(terminal.id),
|
|
3653
|
+
terminalControl,
|
|
3654
|
+
pid: Number(terminal.pid),
|
|
3655
|
+
workspace: verifiedEmptyWorkspace,
|
|
3656
|
+
processUuid: verifiedEmptyProcessUuid,
|
|
3657
|
+
processBirth: verifiedEmptyProcessBirth,
|
|
3658
|
+
sourceSession: soleBindingConflict.session
|
|
3659
|
+
})
|
|
3660
|
+
: undefined;
|
|
3272
3661
|
const reconcileBindingAction = mutationsAllowed &&
|
|
3273
3662
|
discoveredOwnership.state === "none" &&
|
|
3274
3663
|
unresolvedSessionClaims.length === 0 &&
|
|
@@ -3282,6 +3671,7 @@ function terminalFirstListProjection({ storeDir, terminals, managedSessions, ses
|
|
|
3282
3671
|
!(isRecord(terminal.approval_state) &&
|
|
3283
3672
|
terminal.approval_state.blocked === true) &&
|
|
3284
3673
|
!conflictingSessionTurns.some((turn) => SESSION_SEND_BLOCKING_STATUSES.has(turn.status)) &&
|
|
3674
|
+
terminalBlockingTurns.length === 0 &&
|
|
3285
3675
|
!managedSessionHasUnresolvedNativeTransition(storeDir, soleBindingConflict.session)
|
|
3286
3676
|
? {
|
|
3287
3677
|
tool: "agent_knock_knock_reconcile_binding",
|
|
@@ -3300,9 +3690,11 @@ function terminalFirstListProjection({ storeDir, terminals, managedSessions, ses
|
|
|
3300
3690
|
unresolvedSessionClaims.length === 0 &&
|
|
3301
3691
|
soleBindingConflict?.kind === "live_external_thread_change" &&
|
|
3302
3692
|
terminal.activity_state === "idle" &&
|
|
3693
|
+
automatedInputComposerReady === true &&
|
|
3303
3694
|
!(isRecord(terminal.approval_state) &&
|
|
3304
3695
|
terminal.approval_state.blocked === true) &&
|
|
3305
3696
|
!conflictingSessionTurns.some((turn) => SESSION_SEND_BLOCKING_STATUSES.has(turn.status)) &&
|
|
3697
|
+
terminalBlockingTurns.length === 0 &&
|
|
3306
3698
|
!managedSessionHasUnresolvedNativeTransition(storeDir, soleBindingConflict.session) &&
|
|
3307
3699
|
externalHandoffTarget?.status === "eligible" &&
|
|
3308
3700
|
isRecord(rawActions.send) &&
|
|
@@ -3314,7 +3706,8 @@ function terminalFirstListProjection({ storeDir, terminals, managedSessions, ses
|
|
|
3314
3706
|
!(isRecord(terminal.approval_state) &&
|
|
3315
3707
|
terminal.approval_state.blocked === true) &&
|
|
3316
3708
|
!managedSessionHasUnresolvedNativeTransition(storeDir, soleBindingConflict.session) &&
|
|
3317
|
-
blockingHandoffTurns.length === 1
|
|
3709
|
+
blockingHandoffTurns.length === 1 &&
|
|
3710
|
+
terminalBlockingTurns.every((turn) => turn.conversation_id === blockingHandoffTurns[0].conversation_id)
|
|
3318
3711
|
? blockingHandoffTurns[0]
|
|
3319
3712
|
: undefined;
|
|
3320
3713
|
const handoffDecisionToken = handoffDecisionTurn &&
|
|
@@ -3354,6 +3747,14 @@ function terminalFirstListProjection({ storeDir, terminals, managedSessions, ses
|
|
|
3354
3747
|
}
|
|
3355
3748
|
}
|
|
3356
3749
|
: undefined;
|
|
3750
|
+
const blockingHandoffTurnIds = new Set(handoffSourceBlockingTurns.map((turn) => turn.conversation_id));
|
|
3751
|
+
// An active source Turn may be superseded only through the snapshot-bound
|
|
3752
|
+
// handoff decision above. Never expose the generic Store-only close action
|
|
3753
|
+
// for that Turn, because doing so would bypass expected_handoff_token after
|
|
3754
|
+
// the live native thread or Turn generation changed. `blocking_turns` is
|
|
3755
|
+
// reserved for other same-incarnation blockers such as legacy collateral
|
|
3756
|
+
// stalls; clear those first, refresh, and only then decide the handoff.
|
|
3757
|
+
const terminalRecoveryBlockingTurns = terminalBlockingTurns.filter((turn) => !blockingHandoffTurnIds.has(turn.conversation_id));
|
|
3357
3758
|
const terminalCanAcceptSend = ownership.state === "none" && isRecord(sessionAwareRawActions.send);
|
|
3358
3759
|
if (ownership.state === "current" &&
|
|
3359
3760
|
!allRelated.some((conversation) => conversation.conversation_id === ownership.conversation.conversation_id)) {
|
|
@@ -3390,9 +3791,12 @@ function terminalFirstListProjection({ storeDir, terminals, managedSessions, ses
|
|
|
3390
3791
|
const currentTurnValue = ownership.state === "current"
|
|
3391
3792
|
? currentManagedTurnForTerminal(ownership.conversation, terminal, sessionAwareRawActions)
|
|
3392
3793
|
: undefined;
|
|
3393
|
-
const
|
|
3794
|
+
const currentTurnProjection = currentTurnValue && !mutationsAllowed
|
|
3394
3795
|
? readOnlyManagedTurn(currentTurnValue)
|
|
3395
3796
|
: currentTurnValue;
|
|
3797
|
+
const currentTurn = currentTurnProjection
|
|
3798
|
+
? withoutGenericHandoffSourceClose(currentTurnProjection, blockingHandoffTurnIds)
|
|
3799
|
+
: undefined;
|
|
3396
3800
|
const sortedDisplayed = [...sessionDisplayedRelated]
|
|
3397
3801
|
.filter((conversation) => conversation.conversation_id !== currentTurn?.conversation_id)
|
|
3398
3802
|
.sort(compareManagedConversationRecency);
|
|
@@ -3400,15 +3804,18 @@ function terminalFirstListProjection({ storeDir, terminals, managedSessions, ses
|
|
|
3400
3804
|
const recentTurnValue = recentConversation
|
|
3401
3805
|
? historicalManagedTurnForTerminal(recentConversation, terminalCanAcceptSend, terminal)
|
|
3402
3806
|
: undefined;
|
|
3403
|
-
const
|
|
3807
|
+
const recentTurnProjection = recentTurnValue && !mutationsAllowed
|
|
3404
3808
|
? readOnlyManagedTurn(recentTurnValue)
|
|
3405
3809
|
: recentTurnValue;
|
|
3810
|
+
const recentTurn = recentTurnProjection
|
|
3811
|
+
? withoutGenericHandoffSourceClose(recentTurnProjection, blockingHandoffTurnIds)
|
|
3812
|
+
: undefined;
|
|
3406
3813
|
const historyConversations = includeAll
|
|
3407
3814
|
? sortedDisplayed.filter((conversation) => conversation.conversation_id !== recentConversation?.conversation_id)
|
|
3408
3815
|
: [];
|
|
3409
3816
|
const history = historyConversations.map((conversation) => {
|
|
3410
3817
|
const turn = historicalManagedTurnForTerminal(conversation, terminalCanAcceptSend, terminal);
|
|
3411
|
-
return mutationsAllowed ? turn : readOnlyManagedTurn(turn);
|
|
3818
|
+
return withoutGenericHandoffSourceClose(mutationsAllowed ? turn : readOnlyManagedTurn(turn), blockingHandoffTurnIds);
|
|
3412
3819
|
});
|
|
3413
3820
|
const visibleTurnIds = new Set([currentTurn, recentTurn, ...history]
|
|
3414
3821
|
.map((turn) => stringValue(turn?.conversation_id))
|
|
@@ -3455,6 +3862,19 @@ function terminalFirstListProjection({ storeDir, terminals, managedSessions, ses
|
|
|
3455
3862
|
}
|
|
3456
3863
|
}
|
|
3457
3864
|
: {}),
|
|
3865
|
+
...(verifiedEmptyCodexSnapshotToken
|
|
3866
|
+
? {
|
|
3867
|
+
send: {
|
|
3868
|
+
...verifiedEmptyRawSendAction,
|
|
3869
|
+
arguments: {
|
|
3870
|
+
...(isRecord(verifiedEmptyRawSendAction.arguments)
|
|
3871
|
+
? verifiedEmptyRawSendAction.arguments
|
|
3872
|
+
: {}),
|
|
3873
|
+
expected_terminal_token: verifiedEmptyCodexSnapshotToken
|
|
3874
|
+
}
|
|
3875
|
+
}
|
|
3876
|
+
}
|
|
3877
|
+
: {}),
|
|
3458
3878
|
...(reconcileBindingAction
|
|
3459
3879
|
? { reconcile_binding: reconcileBindingAction }
|
|
3460
3880
|
: {})
|
|
@@ -3482,8 +3902,19 @@ function terminalFirstListProjection({ storeDir, terminals, managedSessions, ses
|
|
|
3482
3902
|
lifecycle_binding_token: managedSessionBindingToken(authoritativeSession)
|
|
3483
3903
|
}
|
|
3484
3904
|
: {};
|
|
3905
|
+
const publicManagementConflict = ownership.state === "conflict" && verifiedEmptyCodexSnapshotToken
|
|
3906
|
+
? {
|
|
3907
|
+
...ownership.conflict,
|
|
3908
|
+
kind: "verified_empty_native_session",
|
|
3909
|
+
reason: "the previously bound Codex rollout is conclusively closed " +
|
|
3910
|
+
"while the same terminal process is at an exact empty prompt",
|
|
3911
|
+
recovery: "use only the exact snapshot-bound send action listed for this terminal"
|
|
3912
|
+
}
|
|
3913
|
+
: ownership.state === "conflict"
|
|
3914
|
+
? ownership.conflict
|
|
3915
|
+
: undefined;
|
|
3485
3916
|
return {
|
|
3486
|
-
...
|
|
3917
|
+
...publicTerminal,
|
|
3487
3918
|
...authoritativeIdentity,
|
|
3488
3919
|
management_state: ownership.state === "conflict"
|
|
3489
3920
|
? "conflict"
|
|
@@ -3491,16 +3922,52 @@ function terminalFirstListProjection({ storeDir, terminals, managedSessions, ses
|
|
|
3491
3922
|
? "managed"
|
|
3492
3923
|
: "unmanaged",
|
|
3493
3924
|
...(ownership.state === "conflict"
|
|
3494
|
-
? { management_conflict:
|
|
3925
|
+
? { management_conflict: publicManagementConflict }
|
|
3495
3926
|
: {}),
|
|
3496
3927
|
...(externalHandoffDetected
|
|
3497
3928
|
? {
|
|
3498
3929
|
handoff_state: externalHandoffAdoptable
|
|
3499
3930
|
? "external_handoff_adoptable"
|
|
3500
|
-
: "external_handoff_blocked"
|
|
3931
|
+
: "external_handoff_blocked",
|
|
3932
|
+
...(terminalRecoveryBlockingTurns.length > 0
|
|
3933
|
+
? {
|
|
3934
|
+
handoff_blocked_reason: "the terminal has unresolved managed Turn state; use only a listed blocking_turns recovery action"
|
|
3935
|
+
}
|
|
3936
|
+
: handoffDecision
|
|
3937
|
+
? {
|
|
3938
|
+
handoff_blocked_reason: "the source Session has one active Turn; use only the snapshot-bound handoff_decision after explicit confirmation"
|
|
3939
|
+
}
|
|
3940
|
+
: handoffSourceBlockingTurns.length > 0
|
|
3941
|
+
? {
|
|
3942
|
+
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"
|
|
3943
|
+
}
|
|
3944
|
+
: automatedInputComposerReady !== true
|
|
3945
|
+
? {
|
|
3946
|
+
handoff_blocked_reason: "the current terminal composer is not an exact empty idle frame"
|
|
3947
|
+
}
|
|
3948
|
+
: {})
|
|
3501
3949
|
}
|
|
3502
3950
|
: {}),
|
|
3951
|
+
...(verifiedEmptyCodexSnapshotToken
|
|
3952
|
+
? { handoff_state: "verified_empty_native_session_adoptable" }
|
|
3953
|
+
: {}),
|
|
3503
3954
|
...(handoffDecision ? { handoff_decision: handoffDecision } : {}),
|
|
3955
|
+
...(terminalRecoveryBlockingTurns.length > 0
|
|
3956
|
+
? {
|
|
3957
|
+
blocking_turns: terminalRecoveryBlockingTurns.map((turn) => ({
|
|
3958
|
+
session_id: sessionIdForConversation(turn),
|
|
3959
|
+
turn_id: turnIdForConversation(turn),
|
|
3960
|
+
status: turn.status,
|
|
3961
|
+
recovery_action: {
|
|
3962
|
+
tool: "agent_knock_knock_close",
|
|
3963
|
+
arguments: {
|
|
3964
|
+
turn_id: turnIdForConversation(turn)
|
|
3965
|
+
},
|
|
3966
|
+
requires_explicit_user_confirmation: true
|
|
3967
|
+
}
|
|
3968
|
+
}))
|
|
3969
|
+
}
|
|
3970
|
+
: {}),
|
|
3504
3971
|
managed: management,
|
|
3505
3972
|
available_actions: availableActions
|
|
3506
3973
|
};
|
|
@@ -3743,6 +4210,21 @@ function terminalControlForManagedConversation(conversation) {
|
|
|
3743
4210
|
? conversation.native_session_takeover
|
|
3744
4211
|
: undefined);
|
|
3745
4212
|
}
|
|
4213
|
+
function terminalIncarnationBlockingTurns(storeDir, terminalControl, conversations = listConversations(storeDir)) {
|
|
4214
|
+
return conversations
|
|
4215
|
+
.filter(isDiscoverableTmuxConversation)
|
|
4216
|
+
.filter((turn) => terminalControlsShareIncarnation(terminalControlForManagedConversation(turn), terminalControl) &&
|
|
4217
|
+
SESSION_SEND_BLOCKING_STATUSES.has(turn.status))
|
|
4218
|
+
.sort(compareManagedConversationRecency);
|
|
4219
|
+
}
|
|
4220
|
+
function assertTerminalIncarnationCanStartTurn(storeDir, terminalControl) {
|
|
4221
|
+
const blocker = terminalIncarnationBlockingTurns(storeDir, terminalControl)[0];
|
|
4222
|
+
if (!blocker) {
|
|
4223
|
+
return;
|
|
4224
|
+
}
|
|
4225
|
+
throw new Error(`terminal ${terminalControl.target} still has unresolved Turn ` +
|
|
4226
|
+
`${turnIdForConversation(blocker)} (${blocker.status})`);
|
|
4227
|
+
}
|
|
3746
4228
|
function compareManagedConversationRecency(left, right) {
|
|
3747
4229
|
const leftTime = Date.parse(String(left.updated_at ?? left.created_at ?? ""));
|
|
3748
4230
|
const rightTime = Date.parse(String(right.updated_at ?? right.created_at ?? ""));
|
|
@@ -3962,6 +4444,20 @@ function readOnlyManagedTurn(managedTurn) {
|
|
|
3962
4444
|
: {})
|
|
3963
4445
|
};
|
|
3964
4446
|
}
|
|
4447
|
+
function withoutGenericHandoffSourceClose(managedTurn, blockingHandoffTurnIds) {
|
|
4448
|
+
const conversationId = stringValue(managedTurn.conversation_id);
|
|
4449
|
+
if (!conversationId || !blockingHandoffTurnIds.has(conversationId)) {
|
|
4450
|
+
return managedTurn;
|
|
4451
|
+
}
|
|
4452
|
+
const availableActions = isRecord(managedTurn.available_actions)
|
|
4453
|
+
? managedTurn.available_actions
|
|
4454
|
+
: {};
|
|
4455
|
+
const { close: _genericClose, ...safeActions } = availableActions;
|
|
4456
|
+
return {
|
|
4457
|
+
...managedTurn,
|
|
4458
|
+
available_actions: safeActions
|
|
4459
|
+
};
|
|
4460
|
+
}
|
|
3965
4461
|
function historicalManagedTurnForTerminal(conversation, terminalCanAcceptSend, terminal) {
|
|
3966
4462
|
const managedTurn = managedTurnListEntry(summarizeConversation(conversation), {
|
|
3967
4463
|
terminalBridge: terminalBridgeEnabled(conversation),
|
|
@@ -4199,13 +4695,13 @@ function managedListApprovalState(conversation) {
|
|
|
4199
4695
|
}
|
|
4200
4696
|
function listActionContracts() {
|
|
4201
4697
|
return {
|
|
4202
|
-
version:
|
|
4698
|
+
version: 10,
|
|
4203
4699
|
instructions: [
|
|
4204
|
-
"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.
|
|
4700
|
+
"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.",
|
|
4205
4701
|
"An existing managed session's ordinary send targets session_id and creates a new turn. A turn id is never an ordinary send target.",
|
|
4206
4702
|
"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.",
|
|
4207
4703
|
"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.",
|
|
4208
|
-
"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. Other binding conflicts remain fail-closed and may expose only exact low-level reconcile_binding recovery.",
|
|
4704
|
+
"A verified, idle human native-thread switch may expose a terminal-scoped send with expected_terminal_token; that action atomically adopts the live context before creating its Turn. A conclusively ended Codex rollout may expose the same snapshot-bound send only after AKK proves zero current rollout and an exact empty composer; it detaches the ended Session and creates an isolated virgin Session. Other binding conflicts remain fail-closed and may expose only exact low-level reconcile_binding recovery.",
|
|
4209
4705
|
"List resumable threads before resume; use only a complete native_thread_id and the action returned for that candidate.",
|
|
4210
4706
|
"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.",
|
|
4211
4707
|
"Use respond only for an in-flight turn that is explicitly waiting for OpenClaw.",
|
|
@@ -4239,11 +4735,28 @@ function listActionContracts() {
|
|
|
4239
4735
|
meaning: "currently_safe_actions",
|
|
4240
4736
|
authoritative_for_tool_calls: true
|
|
4241
4737
|
},
|
|
4738
|
+
native_agent_identity_observation: {
|
|
4739
|
+
meaning: "the latest bounded native identity probe; verified_absent is distinct from an unavailable resolver",
|
|
4740
|
+
authoritative_for_tool_calls: false
|
|
4741
|
+
},
|
|
4742
|
+
management_conflict: {
|
|
4743
|
+
verified_empty_native_session: "a previously bound Codex rollout is conclusively closed; only the exact snapshot-bound send in available_actions may detach it and create an isolated virgin Session"
|
|
4744
|
+
},
|
|
4745
|
+
handoff_state: {
|
|
4746
|
+
meaning: "why a conflict-scoped follow-current send is available or blocked",
|
|
4747
|
+
verified_empty_native_session_adoptable: "the exact current terminal snapshot permits the listed conflict send; availability is revalidated before terminal input"
|
|
4748
|
+
},
|
|
4242
4749
|
handoff_decision: {
|
|
4243
4750
|
meaning: "an explicit human choice required before superseding an active source Turn",
|
|
4244
4751
|
authoritative_action_path: "terminals[].handoff_decision.choices.take_over_current.action",
|
|
4245
4752
|
requires_explicit_user_confirmation: true,
|
|
4246
4753
|
after_success: "refresh list before using a follow-current send action"
|
|
4754
|
+
},
|
|
4755
|
+
blocking_turns: {
|
|
4756
|
+
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",
|
|
4757
|
+
authoritative_action_path: "terminals[].blocking_turns[].recovery_action",
|
|
4758
|
+
requires_explicit_user_confirmation: true,
|
|
4759
|
+
after_success: "refresh list before using any terminal action"
|
|
4247
4760
|
}
|
|
4248
4761
|
},
|
|
4249
4762
|
actions: {
|
|
@@ -4262,7 +4775,7 @@ function listActionContracts() {
|
|
|
4262
4775
|
"agentHardTimeoutMinutes"
|
|
4263
4776
|
],
|
|
4264
4777
|
unsupported: ["timeoutSeconds"],
|
|
4265
|
-
ordinary_use: "Create a new managed turn in the exact Session. A live terminal selector can attach an unmanaged pane
|
|
4778
|
+
ordinary_use: "Create a new managed turn in the exact Session. A live terminal selector can attach an unmanaged pane, adopt one verified human-selected native context, or use the exact listed conflict send to detach a verified-empty Codex source before an isolated virgin attach; an explicit session_id never follows the pane."
|
|
4266
4779
|
},
|
|
4267
4780
|
new_thread: {
|
|
4268
4781
|
tool: "agent_knock_knock_new_thread",
|
|
@@ -5350,14 +5863,9 @@ async function verifyCodexPendingManagedSendStatus({ options, terminal, session,
|
|
|
5350
5863
|
if (!codexComposerVisible(initialStatus.screen.excerpt)) {
|
|
5351
5864
|
throw new Error("Codex status-card verification requires a visible idle composer");
|
|
5352
5865
|
}
|
|
5353
|
-
await assertCodexComposerReadyForAutomatedInput({
|
|
5354
|
-
options,
|
|
5355
|
-
terminalControl: terminal.terminalControl
|
|
5356
|
-
});
|
|
5357
5866
|
const observed = await probeCodexCurrentThread({
|
|
5358
5867
|
options,
|
|
5359
5868
|
terminal,
|
|
5360
|
-
initialStatus,
|
|
5361
5869
|
currentIdentity: logicalIdentity,
|
|
5362
5870
|
runtimeOverride: runtime
|
|
5363
5871
|
});
|
|
@@ -5652,6 +6160,218 @@ async function reattachManagedSessionForNativeIdentity({ options, terminal, iden
|
|
|
5652
6160
|
}, { expectedRevision: managedSessionRevision(existing) });
|
|
5653
6161
|
}
|
|
5654
6162
|
const HUMAN_OBSERVED_HANDOFF_FINGERPRINT = nativeThreadCommandFingerprint("adopt_external_thread:human_observed:no_terminal_input:v1");
|
|
6163
|
+
function exactBoundCodexSourceForVerifiedEmptyHandoff({ session, terminalId, terminalControl, pid, workspace, processUuid, processBirth }) {
|
|
6164
|
+
const binding = session.binding;
|
|
6165
|
+
return Boolean(session.agent === "codex" &&
|
|
6166
|
+
session.status === "bound" &&
|
|
6167
|
+
binding &&
|
|
6168
|
+
isExactNativeThreadId(binding.native_thread_id) &&
|
|
6169
|
+
isCompleteNativeRollout(binding.native_process.rollout) &&
|
|
6170
|
+
binding.native_process.process_uuid &&
|
|
6171
|
+
binding.native_process.process_birth &&
|
|
6172
|
+
binding.native_process.pid === pid &&
|
|
6173
|
+
terminalControlAliasMatches(binding.terminal_id, binding.terminal_control, terminalId, terminalControl) &&
|
|
6174
|
+
workspace &&
|
|
6175
|
+
path.resolve(session.workspace) === path.resolve(workspace) &&
|
|
6176
|
+
processIncarnationRelationship({
|
|
6177
|
+
binding,
|
|
6178
|
+
livePid: pid,
|
|
6179
|
+
liveProcessUuid: processUuid,
|
|
6180
|
+
liveProcessBirth: processBirth
|
|
6181
|
+
}) === "same");
|
|
6182
|
+
}
|
|
6183
|
+
function verifiedEmptyCodexHandoffToken({ terminalId, terminalControl, pid, workspace, processUuid, processBirth, sourceSession }) {
|
|
6184
|
+
const terminalToken = unmanagedTerminalBindingToken({
|
|
6185
|
+
terminalId,
|
|
6186
|
+
terminalControl,
|
|
6187
|
+
agent: "codex",
|
|
6188
|
+
pid,
|
|
6189
|
+
workspace,
|
|
6190
|
+
processUuid,
|
|
6191
|
+
processBirth
|
|
6192
|
+
});
|
|
6193
|
+
return createHash("sha256")
|
|
6194
|
+
.update(JSON.stringify({
|
|
6195
|
+
version: 1,
|
|
6196
|
+
kind: "verified_empty_codex_handoff",
|
|
6197
|
+
terminal_token: terminalToken,
|
|
6198
|
+
source_session_id: sourceSession.session_id,
|
|
6199
|
+
source_revision: managedSessionRevision(sourceSession),
|
|
6200
|
+
source_binding_token: managedSessionBindingToken(sourceSession),
|
|
6201
|
+
observation: "verified_absent"
|
|
6202
|
+
}))
|
|
6203
|
+
.digest("hex");
|
|
6204
|
+
}
|
|
6205
|
+
async function assertVerifiedEmptyCodexHandoffBoundary({ options, terminal, sourceSession, expectedSourceStatus, requireNoDispatch, requireEmptyComposer = true }) {
|
|
6206
|
+
const currentSource = loadManagedSession(storeDirFromOptions(options), sourceSession.session_id);
|
|
6207
|
+
if (currentSource.status !== expectedSourceStatus ||
|
|
6208
|
+
currentSource.revision !== sourceSession.revision ||
|
|
6209
|
+
managedSessionBindingToken(currentSource) !==
|
|
6210
|
+
managedSessionBindingToken(sourceSession)) {
|
|
6211
|
+
throw new Error("the verified-empty source Session changed; refresh AKK list");
|
|
6212
|
+
}
|
|
6213
|
+
const liveIncarnation = codexProcessIncarnationForPid(terminal.pid);
|
|
6214
|
+
if (!exactBoundCodexSourceForVerifiedEmptyHandoff({
|
|
6215
|
+
session: {
|
|
6216
|
+
...currentSource,
|
|
6217
|
+
// The exact source binding remains authoritative after the monotonic
|
|
6218
|
+
// detach; only the Session status changes.
|
|
6219
|
+
status: "bound"
|
|
6220
|
+
},
|
|
6221
|
+
terminalId: terminal.conversationId,
|
|
6222
|
+
terminalControl: terminal.terminalControl,
|
|
6223
|
+
pid: terminal.pid,
|
|
6224
|
+
workspace: terminal.terminalControl.currentPath,
|
|
6225
|
+
processUuid: liveIncarnation.processUuid,
|
|
6226
|
+
processBirth: liveIncarnation.processBirth
|
|
6227
|
+
})) {
|
|
6228
|
+
throw new Error("the verified-empty source binding no longer matches the terminal");
|
|
6229
|
+
}
|
|
6230
|
+
const observation = await observeCurrentNativeAgentSessionIdentity({
|
|
6231
|
+
options,
|
|
6232
|
+
agent: "codex",
|
|
6233
|
+
pid: terminal.pid,
|
|
6234
|
+
cwd: terminal.terminalControl.currentPath
|
|
6235
|
+
});
|
|
6236
|
+
if (observation.status === "unavailable") {
|
|
6237
|
+
throw new Error(`Codex native identity observation is unavailable: ${observation.reason}`);
|
|
6238
|
+
}
|
|
6239
|
+
if (observation.status !== "verified_absent") {
|
|
6240
|
+
throw new Error(`Codex materialized native thread ${observation.identity.sessionId}; ` +
|
|
6241
|
+
"refresh AKK list before sending");
|
|
6242
|
+
}
|
|
6243
|
+
const status = await createTerminalAgentBridge(options).status("codex", terminal.terminalControl, {
|
|
6244
|
+
runtime: terminalRuntimeForLiveIdentity({
|
|
6245
|
+
terminal,
|
|
6246
|
+
expectedEmptyNativeSession: true
|
|
6247
|
+
})
|
|
6248
|
+
});
|
|
6249
|
+
if (status.reachable !== true ||
|
|
6250
|
+
status.approval_state.blocked === true ||
|
|
6251
|
+
!["idle", "unknown"].includes(status.activity_state)) {
|
|
6252
|
+
throw new Error(`terminal ${terminal.terminalControl.target} is not at a verified ` +
|
|
6253
|
+
`empty Codex prompt (${status.activity_state}: ${status.activity_reason})`);
|
|
6254
|
+
}
|
|
6255
|
+
if (requireNoDispatch) {
|
|
6256
|
+
assertTerminalLifecycleReady({
|
|
6257
|
+
options,
|
|
6258
|
+
terminal,
|
|
6259
|
+
// A fully styled empty/placeholder composer below is stronger prompt
|
|
6260
|
+
// evidence than the plain-screen parser's conservative `unknown`.
|
|
6261
|
+
terminalStatus: { ...status, activity_state: "idle" }
|
|
6262
|
+
});
|
|
6263
|
+
}
|
|
6264
|
+
if (managedSessionHasUnresolvedNativeTransition(storeDirFromOptions(options), currentSource)) {
|
|
6265
|
+
throw new Error(`managed Session ${currentSource.session_id} has an unresolved native-thread transition`);
|
|
6266
|
+
}
|
|
6267
|
+
if (requireEmptyComposer) {
|
|
6268
|
+
await assertCodexComposerReadyForAutomatedInput({
|
|
6269
|
+
options,
|
|
6270
|
+
terminalControl: terminal.terminalControl
|
|
6271
|
+
});
|
|
6272
|
+
}
|
|
6273
|
+
}
|
|
6274
|
+
async function maybeDetachVerifiedEmptyCodexSource({ options, terminal, sourceSession, observation }) {
|
|
6275
|
+
if (terminal.agent !== "codex" ||
|
|
6276
|
+
!sourceSession?.binding ||
|
|
6277
|
+
observation.status !== "verified_absent") {
|
|
6278
|
+
return undefined;
|
|
6279
|
+
}
|
|
6280
|
+
const processUuid = sourceSession.binding.native_process.process_uuid;
|
|
6281
|
+
const processBirth = sourceSession.binding.native_process.process_birth;
|
|
6282
|
+
const workspace = terminal.terminalControl.currentPath;
|
|
6283
|
+
const liveIncarnation = codexProcessIncarnationForPid(terminal.pid);
|
|
6284
|
+
if (!processUuid ||
|
|
6285
|
+
!processBirth ||
|
|
6286
|
+
!workspace ||
|
|
6287
|
+
!exactBoundCodexSourceForVerifiedEmptyHandoff({
|
|
6288
|
+
session: sourceSession,
|
|
6289
|
+
terminalId: terminal.conversationId,
|
|
6290
|
+
terminalControl: terminal.terminalControl,
|
|
6291
|
+
pid: terminal.pid,
|
|
6292
|
+
workspace,
|
|
6293
|
+
processUuid: liveIncarnation.processUuid,
|
|
6294
|
+
processBirth: liveIncarnation.processBirth
|
|
6295
|
+
})) {
|
|
6296
|
+
return undefined;
|
|
6297
|
+
}
|
|
6298
|
+
const expectedToken = verifiedEmptyCodexHandoffToken({
|
|
6299
|
+
terminalId: terminal.conversationId,
|
|
6300
|
+
terminalControl: terminal.terminalControl,
|
|
6301
|
+
pid: terminal.pid,
|
|
6302
|
+
workspace,
|
|
6303
|
+
processUuid,
|
|
6304
|
+
processBirth,
|
|
6305
|
+
sourceSession
|
|
6306
|
+
});
|
|
6307
|
+
if (stringValue(options.expectedTerminalToken) !== expectedToken) {
|
|
6308
|
+
throw new Error("verified-empty Codex handoff requires the fresh exact terminal token " +
|
|
6309
|
+
"advertised by AKK list");
|
|
6310
|
+
}
|
|
6311
|
+
await assertVerifiedEmptyCodexHandoffBoundary({
|
|
6312
|
+
options,
|
|
6313
|
+
terminal,
|
|
6314
|
+
sourceSession,
|
|
6315
|
+
expectedSourceStatus: "bound",
|
|
6316
|
+
requireNoDispatch: true
|
|
6317
|
+
});
|
|
6318
|
+
await assertNativeThreadHasExclusiveOwnership({
|
|
6319
|
+
options,
|
|
6320
|
+
agent: "codex",
|
|
6321
|
+
currentPid: terminal.pid,
|
|
6322
|
+
nativeThreadId: sourceSession.binding.native_thread_id,
|
|
6323
|
+
storeDir: storeDirFromOptions(options),
|
|
6324
|
+
terminalControl: terminal.terminalControl,
|
|
6325
|
+
excludedManagedSessionId: sourceSession.session_id
|
|
6326
|
+
});
|
|
6327
|
+
const detachedAt = cliNow().toISOString();
|
|
6328
|
+
const detached = saveManagedSession(storeDirFromOptions(options), {
|
|
6329
|
+
...sourceSession,
|
|
6330
|
+
status: "detached",
|
|
6331
|
+
detached_at: detachedAt,
|
|
6332
|
+
updated_at: detachedAt
|
|
6333
|
+
}, {
|
|
6334
|
+
expectedRevision: managedSessionRevision(sourceSession)
|
|
6335
|
+
});
|
|
6336
|
+
runtimeLog("info", "verified_empty_codex_source_detached", {
|
|
6337
|
+
terminal_id: terminal.conversationId,
|
|
6338
|
+
source_session_id: sourceSession.session_id,
|
|
6339
|
+
native_thread_id: sourceSession.binding.native_thread_id,
|
|
6340
|
+
process_uuid: processUuid,
|
|
6341
|
+
process_birth: processBirth,
|
|
6342
|
+
terminal_input_sent: false
|
|
6343
|
+
});
|
|
6344
|
+
return {
|
|
6345
|
+
detached,
|
|
6346
|
+
boundary: {
|
|
6347
|
+
terminal,
|
|
6348
|
+
detachedSourceSessionId: detached.session_id,
|
|
6349
|
+
detachedSourceRevision: managedSessionRevision(detached),
|
|
6350
|
+
detachedSourceBindingToken: managedSessionBindingToken(detached),
|
|
6351
|
+
processUuid,
|
|
6352
|
+
processBirth
|
|
6353
|
+
}
|
|
6354
|
+
};
|
|
6355
|
+
}
|
|
6356
|
+
async function assertVerifiedEmptyCodexTransportBoundary({ options, boundary, requireEmptyComposer }) {
|
|
6357
|
+
const source = loadManagedSession(storeDirFromOptions(options), boundary.detachedSourceSessionId);
|
|
6358
|
+
if (source.status !== "detached" ||
|
|
6359
|
+
managedSessionRevision(source) !== boundary.detachedSourceRevision ||
|
|
6360
|
+
managedSessionBindingToken(source) !==
|
|
6361
|
+
boundary.detachedSourceBindingToken ||
|
|
6362
|
+
source.binding?.native_process.process_uuid !== boundary.processUuid ||
|
|
6363
|
+
source.binding.native_process.process_birth !== boundary.processBirth) {
|
|
6364
|
+
throw new Error("verified-empty Codex source authority changed before text injection");
|
|
6365
|
+
}
|
|
6366
|
+
await assertVerifiedEmptyCodexHandoffBoundary({
|
|
6367
|
+
options,
|
|
6368
|
+
terminal: boundary.terminal,
|
|
6369
|
+
sourceSession: source,
|
|
6370
|
+
expectedSourceStatus: "detached",
|
|
6371
|
+
requireNoDispatch: false,
|
|
6372
|
+
requireEmptyComposer
|
|
6373
|
+
});
|
|
6374
|
+
}
|
|
5655
6375
|
function observedHandoffAuthorityToken({ terminal, identity, sourceSession, target }) {
|
|
5656
6376
|
const exact = exactLifecycleProcessIdentity(terminal, identity);
|
|
5657
6377
|
const terminalToken = unmanagedTerminalBindingToken({
|
|
@@ -5709,10 +6429,12 @@ function assertExpectedHandoffTokenUsesExactTerminalSelector({ options, terminal
|
|
|
5709
6429
|
"conversation selector advertised by AKK list");
|
|
5710
6430
|
}
|
|
5711
6431
|
}
|
|
5712
|
-
async function observedExternalHandoffIdentity({ options, terminal, sourceSession, resolvedIdentity }) {
|
|
6432
|
+
async function observedExternalHandoffIdentity({ options, terminal, sourceSession, resolvedIdentity, requireSafeTerminal = true }) {
|
|
5713
6433
|
const bridge = createTerminalAgentBridge(options);
|
|
5714
6434
|
const status = await bridge.status(terminal.agent, terminal.terminalControl, { runtime: terminalRuntimeForLiveIdentity({ terminal, physicalOnly: true }) });
|
|
5715
|
-
|
|
6435
|
+
if (requireSafeTerminal) {
|
|
6436
|
+
assertSafeTerminalSend(terminal.agent, status);
|
|
6437
|
+
}
|
|
5716
6438
|
if (terminal.agent !== "codex") {
|
|
5717
6439
|
return { identity: resolvedIdentity, status };
|
|
5718
6440
|
}
|
|
@@ -6284,10 +7006,7 @@ function assertTerminalLifecycleReady({ options, terminal, terminalStatus }) {
|
|
|
6284
7006
|
throw new Error(`terminal ${terminal.terminalControl.target} is not at a verified idle prompt ` +
|
|
6285
7007
|
`(${terminalStatus.activity_state}: ${terminalStatus.activity_reason})`);
|
|
6286
7008
|
}
|
|
6287
|
-
const blockers =
|
|
6288
|
-
.filter(isDiscoverableTmuxConversation)
|
|
6289
|
-
.filter((turn) => terminalControlsShareIncarnation(terminalControlForManagedConversation(turn), terminal.terminalControl) &&
|
|
6290
|
-
SESSION_SEND_BLOCKING_STATUSES.has(turn.status));
|
|
7009
|
+
const blockers = terminalIncarnationBlockingTurns(storeDirFromOptions(options), terminal.terminalControl);
|
|
6291
7010
|
if (blockers.length > 0) {
|
|
6292
7011
|
throw new Error(`terminal ${terminal.terminalControl.target} still has unresolved Turn ` +
|
|
6293
7012
|
`${turnIdForConversation(blockers[0])} (${blockers[0].status})`);
|
|
@@ -6967,10 +7686,7 @@ function assertTerminalNativeInspectionReady({ options, terminal, terminalStatus
|
|
|
6967
7686
|
throw new Error(`terminal ${terminal.terminalControl.target} is not at a verified idle prompt ` +
|
|
6968
7687
|
`(${terminalStatus.activity_state}: ${terminalStatus.activity_reason})`);
|
|
6969
7688
|
}
|
|
6970
|
-
const blocker =
|
|
6971
|
-
.filter(isDiscoverableTmuxConversation)
|
|
6972
|
-
.find((turn) => terminalControlsShareIncarnation(terminalControlForManagedConversation(turn), terminal.terminalControl) &&
|
|
6973
|
-
SESSION_SEND_BLOCKING_STATUSES.has(turn.status));
|
|
7689
|
+
const blocker = terminalIncarnationBlockingTurns(storeDirFromOptions(options), terminal.terminalControl)[0];
|
|
6974
7690
|
if (blocker) {
|
|
6975
7691
|
throw new Error(`terminal ${terminal.terminalControl.target} still has unresolved Turn ` +
|
|
6976
7692
|
`${turnIdForConversation(blocker)} (${blocker.status})`);
|
|
@@ -7414,6 +8130,7 @@ async function waitForVerifiedThreadTransition({ options, terminal, beforeIdenti
|
|
|
7414
8130
|
const bridge = createTerminalAgentBridge(options);
|
|
7415
8131
|
let probeSent = false;
|
|
7416
8132
|
let postProbeBaselineDigest;
|
|
8133
|
+
let postProbeObservationScrollbackLines;
|
|
7417
8134
|
let stableIdentity;
|
|
7418
8135
|
let stableCount = 0;
|
|
7419
8136
|
const physicalBeforeFence = codexIdentityFence(physicalBeforeIdentity);
|
|
@@ -7481,22 +8198,37 @@ async function waitForVerifiedThreadTransition({ options, terminal, beforeIdenti
|
|
|
7481
8198
|
}
|
|
7482
8199
|
else {
|
|
7483
8200
|
const afterProbe = plan.steps.find((step) => step.kind === "identity_probe_after");
|
|
7484
|
-
if (!afterProbe ||
|
|
8201
|
+
if (!afterProbe ||
|
|
8202
|
+
afterProbe.effect !== "read_only" ||
|
|
8203
|
+
afterProbe.command !== "/status") {
|
|
7485
8204
|
throw new Error("the Codex lifecycle plan has no exact post-transition identity probe");
|
|
7486
8205
|
}
|
|
8206
|
+
const agentVersion = agentVersionForRunningProcess(terminal.agent, terminal.pid, options);
|
|
8207
|
+
if (!agentVersion) {
|
|
8208
|
+
throw new Error("Codex lifecycle postcondition /status requires the exact running version before terminal input");
|
|
8209
|
+
}
|
|
7487
8210
|
const physicalRuntime = terminalRuntimeForLiveIdentity({
|
|
7488
8211
|
terminal,
|
|
7489
8212
|
physicalOnly: true
|
|
7490
8213
|
});
|
|
7491
|
-
const status = await bridge.status(terminal.agent, terminal.terminalControl, {
|
|
8214
|
+
const status = await bridge.status(terminal.agent, terminal.terminalControl, {
|
|
8215
|
+
runtime: physicalRuntime,
|
|
8216
|
+
...(postProbeObservationScrollbackLines === undefined
|
|
8217
|
+
? {}
|
|
8218
|
+
: {
|
|
8219
|
+
scrollbackLines: postProbeObservationScrollbackLines
|
|
8220
|
+
})
|
|
8221
|
+
});
|
|
7492
8222
|
const screenDigest = stringValue(status.screen.digest);
|
|
7493
8223
|
if (!probeSent &&
|
|
7494
8224
|
status.reachable &&
|
|
7495
8225
|
status.activity_state === "idle" &&
|
|
7496
8226
|
screenDigest !== undefined &&
|
|
7497
8227
|
screenDigest !== initialScreenDigest) {
|
|
7498
|
-
|
|
7499
|
-
|
|
8228
|
+
const submission = await bridge.submitCodexStatusProbe(terminal.terminalControl, agentVersion, { runtime: physicalRuntime });
|
|
8229
|
+
postProbeBaselineDigest = submission.observationBaselineDigest;
|
|
8230
|
+
postProbeObservationScrollbackLines =
|
|
8231
|
+
submission.observationScrollbackLines;
|
|
7500
8232
|
probeSent = true;
|
|
7501
8233
|
}
|
|
7502
8234
|
else if (status.reachable &&
|
|
@@ -7625,7 +8357,7 @@ function assertVerifiedClaudeLifecycleBeforeObservation({ options, terminal, ide
|
|
|
7625
8357
|
`for the current process incarnation${observed?.reason ? `: ${observed.reason}` : ""}`);
|
|
7626
8358
|
}
|
|
7627
8359
|
}
|
|
7628
|
-
async function probeCodexCurrentThread({ options, terminal,
|
|
8360
|
+
async function probeCodexCurrentThread({ options, terminal, currentIdentity, runtimeIdentity, runtimeOverride }) {
|
|
7629
8361
|
if (terminal.agent !== "codex") {
|
|
7630
8362
|
throw new Error("the current native thread identity is unavailable");
|
|
7631
8363
|
}
|
|
@@ -7637,24 +8369,31 @@ async function probeCodexCurrentThread({ options, terminal, initialStatus, curre
|
|
|
7637
8369
|
terminal,
|
|
7638
8370
|
expectedEmptyNativeSession: true
|
|
7639
8371
|
}));
|
|
7640
|
-
|
|
7641
|
-
|
|
7642
|
-
|
|
7643
|
-
|
|
7644
|
-
|
|
7645
|
-
|
|
7646
|
-
|
|
7647
|
-
});
|
|
7648
|
-
|
|
8372
|
+
const agentVersion = agentVersionForRunningProcess(terminal.agent, terminal.pid, options);
|
|
8373
|
+
if (!agentVersion) {
|
|
8374
|
+
throw new Error("Codex /status requires the exact running version before terminal input");
|
|
8375
|
+
}
|
|
8376
|
+
// Internal status probes use the same closed submission boundary as native
|
|
8377
|
+
// inspection: exact viewport and styled-empty composer proof, Codex's paste
|
|
8378
|
+
// suppression settle, identity revalidation, and exactly one Enter.
|
|
8379
|
+
const submission = await bridge.submitCodexStatusProbe(terminal.terminalControl, agentVersion, { runtime: probeRuntime });
|
|
8380
|
+
let lastObservationReason = "no fresh idle Codex /status card was captured after the one Enter dispatch";
|
|
8381
|
+
let sawIncompleteSessionUuid = false;
|
|
7649
8382
|
for (let attempt = 0; attempt < 50; attempt += 1) {
|
|
7650
|
-
const status = await bridge.status(terminal.agent, terminal.terminalControl, {
|
|
8383
|
+
const status = await bridge.status(terminal.agent, terminal.terminalControl, {
|
|
8384
|
+
runtime: probeRuntime,
|
|
8385
|
+
scrollbackLines: submission.observationScrollbackLines
|
|
8386
|
+
});
|
|
7651
8387
|
if (status.reachable &&
|
|
7652
|
-
status.screen.digest !==
|
|
8388
|
+
status.screen.digest !== submission.observationBaselineDigest) {
|
|
7653
8389
|
const observed = terminal.adapter.observeThreadLifecycle?.({
|
|
7654
8390
|
operation: { kind: "new_thread" },
|
|
7655
8391
|
phase: "before",
|
|
7656
8392
|
screen: status.screen.excerpt ?? ""
|
|
7657
8393
|
});
|
|
8394
|
+
lastObservationReason = observed?.reason ?? lastObservationReason;
|
|
8395
|
+
sawIncompleteSessionUuid ||= observed?.status === "missing" &&
|
|
8396
|
+
/complete Session UUID/iu.test(observed.reason ?? "");
|
|
7658
8397
|
if (observed?.status === "observed" &&
|
|
7659
8398
|
observed.nativeThreadId &&
|
|
7660
8399
|
status.activity_state === "idle") {
|
|
@@ -7671,7 +8410,12 @@ async function probeCodexCurrentThread({ options, terminal, initialStatus, curre
|
|
|
7671
8410
|
}
|
|
7672
8411
|
await cliSleep(100);
|
|
7673
8412
|
}
|
|
7674
|
-
throw new Error(
|
|
8413
|
+
throw new Error(sawIncompleteSessionUuid
|
|
8414
|
+
? "Codex /status Enter was dispatched exactly once, but the status card " +
|
|
8415
|
+
"was truncated before the complete current Session UUID; widen or zoom " +
|
|
8416
|
+
"the pane, refresh AKK list, and do not retry the probe automatically"
|
|
8417
|
+
: "Codex /status Enter was dispatched exactly once, but no fresh exact " +
|
|
8418
|
+
`current Session UUID was proven; do not retry automatically: ${lastObservationReason}`);
|
|
7675
8419
|
}
|
|
7676
8420
|
async function runNewThread(options) {
|
|
7677
8421
|
const requireRestorableOrigin = options.requireRestorableOrigin;
|
|
@@ -8028,7 +8772,6 @@ async function runNativeThreadTransition(options, operation) {
|
|
|
8028
8772
|
const statusIdentity = await probeCodexCurrentThread({
|
|
8029
8773
|
options,
|
|
8030
8774
|
terminal,
|
|
8031
|
-
initialStatus: terminalStatus,
|
|
8032
8775
|
currentIdentity: beforeIdentity,
|
|
8033
8776
|
runtimeIdentity: snapshot.runtimeIdentity,
|
|
8034
8777
|
runtimeOverride: beforeRuntime
|
|
@@ -8692,17 +9435,21 @@ async function reconcileStoreForStatus(storeDir, options, conversationId) {
|
|
|
8692
9435
|
}
|
|
8693
9436
|
throw error;
|
|
8694
9437
|
}
|
|
8695
|
-
const idle = reconcileIdleConversations(storeDir, options, cliNow(), conversationId);
|
|
8696
9438
|
const monitors = await reconcileMonitors(options, {
|
|
8697
9439
|
includeCallbackRecovery: false,
|
|
8698
9440
|
reason: "status_reconciliation",
|
|
8699
9441
|
conversationId
|
|
8700
9442
|
});
|
|
9443
|
+
const idle = reconcileIdleConversations(storeDir, options, cliNow(), conversationId);
|
|
8701
9444
|
return {
|
|
8702
9445
|
status: "completed",
|
|
8703
9446
|
checked: Math.max(idle.checked, monitors.checked),
|
|
8704
|
-
changed: idle.closed + monitors.launched,
|
|
9447
|
+
changed: idle.closed + monitors.launched + monitors.repaired,
|
|
8705
9448
|
closed: idle.closed,
|
|
9449
|
+
repaired: monitors.repaired,
|
|
9450
|
+
collateral_stalls_checked: monitors.collateral_stalls_checked,
|
|
9451
|
+
collateral_stalls_skipped: monitors.collateral_stalls_skipped,
|
|
9452
|
+
collateral_stall_repairs: monitors.items.filter((item) => item.status === "repaired"),
|
|
8706
9453
|
monitors_launched: monitors.launched,
|
|
8707
9454
|
monitors_already_running: monitors.already_running,
|
|
8708
9455
|
skipped: idle.skipped + monitors.skipped,
|
|
@@ -9229,14 +9976,15 @@ async function runSend(options) {
|
|
|
9229
9976
|
})) {
|
|
9230
9977
|
return;
|
|
9231
9978
|
}
|
|
9232
|
-
|
|
9233
|
-
|
|
9979
|
+
assertTerminalIncarnationCanStartTurn(rawStoreDir, terminalConversation.terminalControl);
|
|
9980
|
+
let claimedSession = soleBoundManagedSessionClaimForTerminal(rawStoreDir, terminalConversation);
|
|
9981
|
+
let knownCodexCompanions = claimedSession
|
|
9234
9982
|
? codexAllowedCompanionSetForManagedSession({
|
|
9235
9983
|
storeDir: rawStoreDir,
|
|
9236
9984
|
session: claimedSession
|
|
9237
9985
|
})
|
|
9238
9986
|
: { additional: [] };
|
|
9239
|
-
|
|
9987
|
+
const nativeIdentityObservation = await observeCurrentNativeAgentSessionIdentity({
|
|
9240
9988
|
options,
|
|
9241
9989
|
agent: terminalConversation.agent,
|
|
9242
9990
|
pid: terminalConversation.pid,
|
|
@@ -9247,6 +9995,26 @@ async function runSend(options) {
|
|
|
9247
9995
|
allowedCompanionIdentity: knownCodexCompanions.primary,
|
|
9248
9996
|
allowedAdditionalIdentities: knownCodexCompanions.additional
|
|
9249
9997
|
});
|
|
9998
|
+
if (nativeIdentityObservation.status === "unavailable") {
|
|
9999
|
+
throw new Error(`native ${terminalConversation.agent} identity observation is ` +
|
|
10000
|
+
`unavailable: ${nativeIdentityObservation.reason}`);
|
|
10001
|
+
}
|
|
10002
|
+
let currentNativeIdentity = nativeIdentityObservation.status === "resolved"
|
|
10003
|
+
? nativeIdentityObservation.identity
|
|
10004
|
+
: undefined;
|
|
10005
|
+
const verifiedEmptyHandoff = await maybeDetachVerifiedEmptyCodexSource({
|
|
10006
|
+
options,
|
|
10007
|
+
terminal: terminalConversation,
|
|
10008
|
+
sourceSession: claimedSession,
|
|
10009
|
+
observation: nativeIdentityObservation
|
|
10010
|
+
});
|
|
10011
|
+
if (verifiedEmptyHandoff) {
|
|
10012
|
+
// The old rollout is conclusively closed. Never carry it forward as
|
|
10013
|
+
// a pre-materialization companion for the new virgin Session.
|
|
10014
|
+
claimedSession = undefined;
|
|
10015
|
+
knownCodexCompanions = { additional: [] };
|
|
10016
|
+
currentNativeIdentity = undefined;
|
|
10017
|
+
}
|
|
9250
10018
|
const physicalNativeIdentityBeforeHandoff = currentNativeIdentity;
|
|
9251
10019
|
const handoff = await maybeAdoptObservedExternalThread({
|
|
9252
10020
|
options,
|
|
@@ -9415,7 +10183,8 @@ async function runSend(options) {
|
|
|
9415
10183
|
terminal: terminalConversation,
|
|
9416
10184
|
transition: handoff.transition
|
|
9417
10185
|
}
|
|
9418
|
-
: undefined
|
|
10186
|
+
: undefined,
|
|
10187
|
+
verifiedEmptyCodexHandoff: verifiedEmptyHandoff?.boundary
|
|
9419
10188
|
});
|
|
9420
10189
|
}
|
|
9421
10190
|
finally {
|
|
@@ -9498,6 +10267,7 @@ async function runSend(options) {
|
|
|
9498
10267
|
})) {
|
|
9499
10268
|
return;
|
|
9500
10269
|
}
|
|
10270
|
+
assertTerminalIncarnationCanStartTurn(storeDir, resolvedTerminal.terminalControl);
|
|
9501
10271
|
let currentSession = tryLoadManagedSession(storeDir, sessionId);
|
|
9502
10272
|
const knownCodexCompanions = currentSession
|
|
9503
10273
|
? codexAllowedCompanionSetForManagedSession({
|
|
@@ -10374,7 +11144,7 @@ async function runTerminalConversationApprove({ options, conversationId, agent,
|
|
|
10374
11144
|
releaseTerminalLock();
|
|
10375
11145
|
}
|
|
10376
11146
|
}
|
|
10377
|
-
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, continuingTurnResponse = false }) {
|
|
11147
|
+
async function runTerminalControlSend({ options, conversation, nextConversation, statePath, logPath, executor, message, terminalControl, terminalSendLockHeld = false, terminalStateLockHeld = false, storeWriterLeaseHeld = false, recordMessageAfterSend = false, recordRawAttachmentAfterSend = false, onTerminalPreflightVerified = undefined, allowedPreMaterializationIdentity = undefined, allowedAdditionalIdentities = [], observedHandoff = undefined, verifiedEmptyCodexHandoff = undefined, continuingTurnResponse = false }) {
|
|
10378
11148
|
const bridge = terminalBridgeEnabled(conversation);
|
|
10379
11149
|
if (!terminalSendLockHeld) {
|
|
10380
11150
|
const releaseTerminalLock = acquireTerminalBridgeSendLock(storeDirFromOptions(options), terminalControl, { timeoutMs: 30000 });
|
|
@@ -10397,6 +11167,7 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
10397
11167
|
allowedPreMaterializationIdentity,
|
|
10398
11168
|
allowedAdditionalIdentities,
|
|
10399
11169
|
observedHandoff,
|
|
11170
|
+
verifiedEmptyCodexHandoff,
|
|
10400
11171
|
continuingTurnResponse
|
|
10401
11172
|
});
|
|
10402
11173
|
}
|
|
@@ -10437,6 +11208,7 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
10437
11208
|
allowedPreMaterializationIdentity,
|
|
10438
11209
|
allowedAdditionalIdentities,
|
|
10439
11210
|
observedHandoff,
|
|
11211
|
+
verifiedEmptyCodexHandoff,
|
|
10440
11212
|
continuingTurnResponse
|
|
10441
11213
|
});
|
|
10442
11214
|
}
|
|
@@ -10464,6 +11236,7 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
10464
11236
|
allowedPreMaterializationIdentity,
|
|
10465
11237
|
allowedAdditionalIdentities,
|
|
10466
11238
|
observedHandoff,
|
|
11239
|
+
verifiedEmptyCodexHandoff,
|
|
10467
11240
|
continuingTurnResponse
|
|
10468
11241
|
}));
|
|
10469
11242
|
}
|
|
@@ -10645,7 +11418,18 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
10645
11418
|
scrollbackLines: Number(options.scrollbackLines ?? 120),
|
|
10646
11419
|
runtime: preSendRuntime
|
|
10647
11420
|
});
|
|
10648
|
-
|
|
11421
|
+
if (verifiedEmptyCodexHandoff) {
|
|
11422
|
+
if (executor.kind !== "codex" ||
|
|
11423
|
+
status.reachable !== true ||
|
|
11424
|
+
status.approval_state.blocked === true ||
|
|
11425
|
+
!["idle", "unknown"].includes(status.activity_state)) {
|
|
11426
|
+
throw new Error(`Codex verified-empty handoff is not at a safe prompt ` +
|
|
11427
|
+
`(${status.activity_state}: ${status.activity_reason})`);
|
|
11428
|
+
}
|
|
11429
|
+
}
|
|
11430
|
+
else {
|
|
11431
|
+
assertSafeTerminalSend(executor.kind, status);
|
|
11432
|
+
}
|
|
10649
11433
|
if (executor.kind === "codex" && needsPostSendNativeBinding) {
|
|
10650
11434
|
if (pendingManagedNativeBinding || allowedPreMaterializationIdentity) {
|
|
10651
11435
|
const expectedForegroundId = stringValue(sendTakeover?.terminal_agent_expected_session_id);
|
|
@@ -11015,7 +11799,8 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
11015
11799
|
try {
|
|
11016
11800
|
await terminalBridge.send(executor.kind, terminalControl, terminalPayload, {
|
|
11017
11801
|
runtime: preSendRuntime,
|
|
11018
|
-
requireExactComposerBeforeEnter: observedHandoff !== undefined
|
|
11802
|
+
requireExactComposerBeforeEnter: observedHandoff !== undefined ||
|
|
11803
|
+
verifiedEmptyCodexHandoff !== undefined,
|
|
11019
11804
|
beforeText: observedHandoff
|
|
11020
11805
|
? async () => {
|
|
11021
11806
|
await assertObservedHandoffTransportBoundary({
|
|
@@ -11025,7 +11810,15 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
11025
11810
|
requireEmptyComposer: true
|
|
11026
11811
|
});
|
|
11027
11812
|
}
|
|
11028
|
-
:
|
|
11813
|
+
: verifiedEmptyCodexHandoff
|
|
11814
|
+
? async () => {
|
|
11815
|
+
await assertVerifiedEmptyCodexTransportBoundary({
|
|
11816
|
+
options,
|
|
11817
|
+
boundary: verifiedEmptyCodexHandoff,
|
|
11818
|
+
requireEmptyComposer: true
|
|
11819
|
+
});
|
|
11820
|
+
}
|
|
11821
|
+
: undefined,
|
|
11029
11822
|
beforeEnter: observedHandoff
|
|
11030
11823
|
? async () => {
|
|
11031
11824
|
await assertObservedHandoffTransportBoundary({
|
|
@@ -11035,7 +11828,15 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
11035
11828
|
requireEmptyComposer: false
|
|
11036
11829
|
});
|
|
11037
11830
|
}
|
|
11038
|
-
:
|
|
11831
|
+
: verifiedEmptyCodexHandoff
|
|
11832
|
+
? async () => {
|
|
11833
|
+
await assertVerifiedEmptyCodexTransportBoundary({
|
|
11834
|
+
options,
|
|
11835
|
+
boundary: verifiedEmptyCodexHandoff,
|
|
11836
|
+
requireEmptyComposer: false
|
|
11837
|
+
});
|
|
11838
|
+
}
|
|
11839
|
+
: undefined,
|
|
11039
11840
|
async onTransportStage(event) {
|
|
11040
11841
|
const stageAt = cliNow().toISOString();
|
|
11041
11842
|
if (event.stage === "text_injected") {
|
|
@@ -11084,6 +11885,13 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
11084
11885
|
requireEmptyComposer: false
|
|
11085
11886
|
});
|
|
11086
11887
|
}
|
|
11888
|
+
if (event.stage === "text_injected" && verifiedEmptyCodexHandoff) {
|
|
11889
|
+
await assertVerifiedEmptyCodexTransportBoundary({
|
|
11890
|
+
options,
|
|
11891
|
+
boundary: verifiedEmptyCodexHandoff,
|
|
11892
|
+
requireEmptyComposer: false
|
|
11893
|
+
});
|
|
11894
|
+
}
|
|
11087
11895
|
try {
|
|
11088
11896
|
appendEvent(logPath, {
|
|
11089
11897
|
ts: stageAt,
|
|
@@ -12523,6 +13331,29 @@ async function resolveCurrentNativeAgentSessionIdentity({ options, agent, pid, c
|
|
|
12523
13331
|
evidence: "claude_agents_exact_pid"
|
|
12524
13332
|
};
|
|
12525
13333
|
}
|
|
13334
|
+
/**
|
|
13335
|
+
* Preserve the security-significant difference between a successful native
|
|
13336
|
+
* identity probe that found no current Session and a probe that could not be
|
|
13337
|
+
* completed. `undefined` from the Codex provider is authoritative only after
|
|
13338
|
+
* both the process-incarnation and open-rollout inspections succeeded.
|
|
13339
|
+
*/
|
|
13340
|
+
async function observeCurrentNativeAgentSessionIdentity(request) {
|
|
13341
|
+
try {
|
|
13342
|
+
const identity = await resolveCurrentNativeAgentSessionIdentity(request);
|
|
13343
|
+
return identity
|
|
13344
|
+
? { status: "resolved", identity }
|
|
13345
|
+
: {
|
|
13346
|
+
status: "verified_absent",
|
|
13347
|
+
evidence: "native_identity_resolver_verified_absent"
|
|
13348
|
+
};
|
|
13349
|
+
}
|
|
13350
|
+
catch (error) {
|
|
13351
|
+
return {
|
|
13352
|
+
status: "unavailable",
|
|
13353
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
13354
|
+
};
|
|
13355
|
+
}
|
|
13356
|
+
}
|
|
12526
13357
|
function nativeAgentIdentityMatchesTurn(conversation, currentIdentity) {
|
|
12527
13358
|
const takeover = isRecord(conversation.native_session_takeover)
|
|
12528
13359
|
? conversation.native_session_takeover
|
|
@@ -13333,14 +14164,23 @@ async function runReconcileMonitors(options) {
|
|
|
13333
14164
|
}
|
|
13334
14165
|
async function reconcileMonitors(options, { includeCallbackRecovery, reason, conversationId }) {
|
|
13335
14166
|
const storeDir = storeDirFromOptions(options);
|
|
14167
|
+
const collateralStalls = await withStoreWriterLeaseAsync(storeDir, async () => reconcileTerminalBridgeCollateralStalls(storeDir, conversationId));
|
|
13336
14168
|
const conversations = listConversations(storeDir).filter((conversation) => conversationId === undefined || conversation.conversation_id === conversationId);
|
|
13337
|
-
const items = [];
|
|
14169
|
+
const items = [...collateralStalls.items];
|
|
14170
|
+
const repairedConversationIds = new Set(collateralStalls.items
|
|
14171
|
+
.filter((item) => item.status === "repaired")
|
|
14172
|
+
.map((item) => stringValue(item.conversation_id))
|
|
14173
|
+
.filter((id) => id !== undefined));
|
|
13338
14174
|
let ignored = 0;
|
|
13339
14175
|
let launched = 0;
|
|
13340
14176
|
let alreadyRunning = 0;
|
|
13341
14177
|
let skipped = 0;
|
|
13342
|
-
let errors =
|
|
14178
|
+
let errors = collateralStalls.errors.length;
|
|
13343
14179
|
for (const listedConversation of conversations) {
|
|
14180
|
+
if (repairedConversationIds.has(listedConversation.conversation_id)) {
|
|
14181
|
+
ignored += 1;
|
|
14182
|
+
continue;
|
|
14183
|
+
}
|
|
13344
14184
|
if (!matchesConfiguredWorkspace(options.workspace, listedConversation.workspace)) {
|
|
13345
14185
|
ignored += 1;
|
|
13346
14186
|
continue;
|
|
@@ -13574,6 +14414,9 @@ async function reconcileMonitors(options, { includeCallbackRecovery, reason, con
|
|
|
13574
14414
|
reconciled: true,
|
|
13575
14415
|
store_dir: storeDir,
|
|
13576
14416
|
checked: conversations.length,
|
|
14417
|
+
repaired: collateralStalls.repaired,
|
|
14418
|
+
collateral_stalls_checked: collateralStalls.checked,
|
|
14419
|
+
collateral_stalls_skipped: collateralStalls.skipped,
|
|
13577
14420
|
ignored,
|
|
13578
14421
|
launched,
|
|
13579
14422
|
already_running: alreadyRunning,
|
|
@@ -14040,7 +14883,7 @@ async function runObservedHandoffClose({ options, statePath, logPath, initialCon
|
|
|
14040
14883
|
if (stringValue(options.reason) !== "superseded_by_human_context_switch") {
|
|
14041
14884
|
throw new Error("a handoff close requires reason superseded_by_human_context_switch");
|
|
14042
14885
|
}
|
|
14043
|
-
const storeDir =
|
|
14886
|
+
const storeDir = pathsForConversationDir(path.dirname(statePath)).storeDir;
|
|
14044
14887
|
const sourceSessionId = sessionIdForConversation(initialConversation);
|
|
14045
14888
|
const initialSource = loadManagedSession(storeDir, sourceSessionId);
|
|
14046
14889
|
if (!initialSource.binding || initialSource.status !== "bound") {
|
|
@@ -14226,89 +15069,179 @@ async function runClose(options) {
|
|
|
14226
15069
|
});
|
|
14227
15070
|
return;
|
|
14228
15071
|
}
|
|
15072
|
+
if (stringValue(options.reason) === "superseded_by_human_context_switch") {
|
|
15073
|
+
throw new Error("reason superseded_by_human_context_switch requires the fresh " +
|
|
15074
|
+
"expected_handoff_token advertised by AKK list");
|
|
15075
|
+
}
|
|
14229
15076
|
const { statePath, logPath } = loaded;
|
|
15077
|
+
const closeStoreDir = pathsForConversationDir(path.dirname(statePath)).storeDir;
|
|
14230
15078
|
const nativeTakeover = isRecord(loaded.conversation.native_session_takeover)
|
|
14231
15079
|
? loaded.conversation.native_session_takeover
|
|
14232
15080
|
: undefined;
|
|
14233
15081
|
const terminalControl = terminalControlFromTakeover(nativeTakeover);
|
|
14234
15082
|
const releaseTerminalLock = terminalControl
|
|
14235
|
-
? acquireTerminalBridgeSendLock(
|
|
15083
|
+
? acquireTerminalBridgeSendLock(closeStoreDir, terminalControl, { timeoutMs: 30000 })
|
|
14236
15084
|
: () => { };
|
|
14237
|
-
|
|
14238
|
-
|
|
14239
|
-
|
|
14240
|
-
|
|
14241
|
-
|
|
14242
|
-
|
|
14243
|
-
|
|
14244
|
-
|
|
14245
|
-
|
|
14246
|
-
|
|
14247
|
-
|
|
14248
|
-
status: "closed",
|
|
14249
|
-
closed_at: now,
|
|
14250
|
-
close_reason: closeReason,
|
|
14251
|
-
...(handoffDisposition
|
|
14252
|
-
? { disposition: handoffDisposition }
|
|
14253
|
-
: {}),
|
|
14254
|
-
updated_at: now
|
|
14255
|
-
};
|
|
14256
|
-
saveState(statePath, closed);
|
|
14257
|
-
let dispatchLedgerResolved = false;
|
|
14258
|
-
let dispatchLedgerWarning;
|
|
14259
|
-
if (terminalControl) {
|
|
14260
|
-
try {
|
|
14261
|
-
dispatchLedgerResolved = resolveTerminalBridgeDispatchLedger({
|
|
14262
|
-
terminalControl,
|
|
14263
|
-
conversation: closed,
|
|
14264
|
-
expectedMessageId: stringValue(nativeTakeover?.terminal_bridge_message_id),
|
|
14265
|
-
reason: "conversation explicitly closed by request"
|
|
14266
|
-
});
|
|
15085
|
+
const closeWithFreshState = async () => {
|
|
15086
|
+
const releaseStateLock = acquireFileLock(`${statePath}.lock`);
|
|
15087
|
+
try {
|
|
15088
|
+
const conversation = loadState(statePath);
|
|
15089
|
+
const currentTakeover = isRecord(conversation.native_session_takeover)
|
|
15090
|
+
? conversation.native_session_takeover
|
|
15091
|
+
: undefined;
|
|
15092
|
+
const currentTerminalControl = terminalControlFromTakeover(currentTakeover);
|
|
15093
|
+
if (terminalControl &&
|
|
15094
|
+
!terminalControlsShareIncarnation(currentTerminalControl, terminalControl)) {
|
|
15095
|
+
throw new Error("terminal control changed after the close action was listed; refresh AKK list");
|
|
14267
15096
|
}
|
|
14268
|
-
|
|
14269
|
-
|
|
14270
|
-
|
|
14271
|
-
|
|
14272
|
-
|
|
14273
|
-
|
|
14274
|
-
error: dispatchLedgerWarning
|
|
15097
|
+
if (terminalControl && currentTerminalControl) {
|
|
15098
|
+
await assertGenericCloseDoesNotBypassObservedHandoff({
|
|
15099
|
+
options,
|
|
15100
|
+
storeDir: closeStoreDir,
|
|
15101
|
+
conversation,
|
|
15102
|
+
terminalControl: currentTerminalControl
|
|
14275
15103
|
});
|
|
14276
15104
|
}
|
|
14277
|
-
|
|
14278
|
-
|
|
14279
|
-
|
|
14280
|
-
|
|
14281
|
-
|
|
14282
|
-
|
|
14283
|
-
|
|
14284
|
-
|
|
14285
|
-
|
|
14286
|
-
|
|
14287
|
-
|
|
14288
|
-
|
|
14289
|
-
|
|
14290
|
-
|
|
14291
|
-
|
|
14292
|
-
|
|
14293
|
-
|
|
14294
|
-
|
|
14295
|
-
|
|
14296
|
-
|
|
14297
|
-
? {
|
|
14298
|
-
terminal_dispatch_warning: textSummary(dispatchLedgerWarning),
|
|
14299
|
-
do_not_retry: true
|
|
15105
|
+
const now = cliNow().toISOString();
|
|
15106
|
+
const closeReason = options.reason ?? "closed by request";
|
|
15107
|
+
const closed = {
|
|
15108
|
+
...conversation,
|
|
15109
|
+
status: "closed",
|
|
15110
|
+
closed_at: now,
|
|
15111
|
+
close_reason: closeReason,
|
|
15112
|
+
updated_at: now
|
|
15113
|
+
};
|
|
15114
|
+
saveState(statePath, closed);
|
|
15115
|
+
let dispatchLedgerResolved = false;
|
|
15116
|
+
let dispatchLedgerWarning;
|
|
15117
|
+
if (currentTerminalControl) {
|
|
15118
|
+
try {
|
|
15119
|
+
dispatchLedgerResolved = resolveTerminalBridgeDispatchLedger({
|
|
15120
|
+
terminalControl: currentTerminalControl,
|
|
15121
|
+
conversation: closed,
|
|
15122
|
+
expectedMessageId: stringValue(currentTakeover?.terminal_bridge_message_id),
|
|
15123
|
+
reason: "conversation explicitly closed by request"
|
|
15124
|
+
});
|
|
14300
15125
|
}
|
|
14301
|
-
|
|
14302
|
-
|
|
14303
|
-
|
|
14304
|
-
|
|
14305
|
-
|
|
14306
|
-
|
|
15126
|
+
catch (error) {
|
|
15127
|
+
dispatchLedgerWarning =
|
|
15128
|
+
error instanceof Error ? error.message : String(error);
|
|
15129
|
+
runtimeLog("error", "terminal_dispatch_ledger_resolve_failed", {
|
|
15130
|
+
conversation_id: closed.conversation_id,
|
|
15131
|
+
terminal_target: currentTerminalControl.target,
|
|
15132
|
+
error: dispatchLedgerWarning
|
|
15133
|
+
});
|
|
15134
|
+
}
|
|
15135
|
+
}
|
|
15136
|
+
appendEvent(logPath, {
|
|
15137
|
+
ts: now,
|
|
15138
|
+
conversation_id: conversation.conversation_id,
|
|
15139
|
+
event: "conversation_closed",
|
|
15140
|
+
status: "closed",
|
|
15141
|
+
reason: closed.close_reason
|
|
15142
|
+
});
|
|
15143
|
+
runtimeLog("info", "conversation_closed", {
|
|
15144
|
+
conversation_id: conversation.conversation_id,
|
|
15145
|
+
status: "closed",
|
|
15146
|
+
reason: closed.close_reason,
|
|
15147
|
+
state_path: statePath,
|
|
15148
|
+
event_log_path: logPath
|
|
15149
|
+
});
|
|
15150
|
+
printJson({
|
|
15151
|
+
conversation: closed,
|
|
15152
|
+
closed: true,
|
|
15153
|
+
terminal_dispatch_resolved: dispatchLedgerResolved,
|
|
15154
|
+
...(dispatchLedgerWarning
|
|
15155
|
+
? {
|
|
15156
|
+
terminal_dispatch_warning: textSummary(dispatchLedgerWarning),
|
|
15157
|
+
do_not_retry: true
|
|
15158
|
+
}
|
|
15159
|
+
: {})
|
|
15160
|
+
});
|
|
14307
15161
|
}
|
|
14308
15162
|
finally {
|
|
14309
|
-
|
|
15163
|
+
releaseStateLock();
|
|
15164
|
+
}
|
|
15165
|
+
};
|
|
15166
|
+
try {
|
|
15167
|
+
if (terminalControl) {
|
|
15168
|
+
await withStoreWriterLeaseAsync(closeStoreDir, closeWithFreshState);
|
|
15169
|
+
}
|
|
15170
|
+
else {
|
|
15171
|
+
await closeWithFreshState();
|
|
14310
15172
|
}
|
|
14311
15173
|
}
|
|
15174
|
+
finally {
|
|
15175
|
+
releaseTerminalLock();
|
|
15176
|
+
}
|
|
15177
|
+
}
|
|
15178
|
+
async function assertGenericCloseDoesNotBypassObservedHandoff({ options, storeDir, conversation, terminalControl }) {
|
|
15179
|
+
if (!SESSION_SEND_BLOCKING_STATUSES.has(conversation.status)) {
|
|
15180
|
+
return;
|
|
15181
|
+
}
|
|
15182
|
+
const sourceSession = tryLoadManagedSession(storeDir, sessionIdForConversation(conversation));
|
|
15183
|
+
if (sourceSession?.status !== "bound" ||
|
|
15184
|
+
!sourceSession.binding ||
|
|
15185
|
+
!terminalControlsShareIncarnation(sourceSession.binding.terminal_control, terminalControl)) {
|
|
15186
|
+
return;
|
|
15187
|
+
}
|
|
15188
|
+
const takeover = isRecord(conversation.native_session_takeover)
|
|
15189
|
+
? conversation.native_session_takeover
|
|
15190
|
+
: undefined;
|
|
15191
|
+
const pid = Number(takeover?.terminal_agent_pid);
|
|
15192
|
+
if (!Number.isSafeInteger(pid) || pid <= 1) {
|
|
15193
|
+
return;
|
|
15194
|
+
}
|
|
15195
|
+
const bridge = createTerminalAgentBridge(options);
|
|
15196
|
+
let terminal;
|
|
15197
|
+
try {
|
|
15198
|
+
terminal = await bridge.resolveStoredTerminal(sourceSession.agent, pid, terminalControl, { pid });
|
|
15199
|
+
}
|
|
15200
|
+
catch (error) {
|
|
15201
|
+
if (!(error instanceof TerminalControlUnavailableError)) {
|
|
15202
|
+
throw error;
|
|
15203
|
+
}
|
|
15204
|
+
// An unavailable terminal cannot prove a live A -> B handoff. Preserve the
|
|
15205
|
+
// explicit Store-only close path used to retire unavailable Turn history.
|
|
15206
|
+
return;
|
|
15207
|
+
}
|
|
15208
|
+
const companions = sourceSession.agent === "codex"
|
|
15209
|
+
? codexAllowedCompanionSetForManagedSession({
|
|
15210
|
+
storeDir,
|
|
15211
|
+
session: sourceSession
|
|
15212
|
+
})
|
|
15213
|
+
: { additional: [] };
|
|
15214
|
+
const identityObservation = await observeCurrentNativeAgentSessionIdentity({
|
|
15215
|
+
options,
|
|
15216
|
+
agent: terminal.agent,
|
|
15217
|
+
pid: terminal.pid,
|
|
15218
|
+
cwd: terminal.terminalControl.currentPath,
|
|
15219
|
+
preferredSessionId: companions.primary
|
|
15220
|
+
? sourceSession.binding.native_thread_id
|
|
15221
|
+
: undefined,
|
|
15222
|
+
allowedCompanionIdentity: companions.primary,
|
|
15223
|
+
allowedAdditionalIdentities: companions.additional
|
|
15224
|
+
});
|
|
15225
|
+
const resolvedIdentity = identityObservation.status === "resolved"
|
|
15226
|
+
? identityObservation.identity
|
|
15227
|
+
: undefined;
|
|
15228
|
+
const observed = await observedExternalHandoffIdentity({
|
|
15229
|
+
options,
|
|
15230
|
+
terminal,
|
|
15231
|
+
sourceSession,
|
|
15232
|
+
resolvedIdentity,
|
|
15233
|
+
requireSafeTerminal: false
|
|
15234
|
+
});
|
|
15235
|
+
if (managedBindingConflictKindForResolvedTerminal({
|
|
15236
|
+
storeDir,
|
|
15237
|
+
session: sourceSession,
|
|
15238
|
+
terminal,
|
|
15239
|
+
identity: observed.identity
|
|
15240
|
+
}) === "live_external_thread_change") {
|
|
15241
|
+
throw new Error(`terminal ${terminalControl.target} changed native thread after the ` +
|
|
15242
|
+
"generic close action was listed; refresh AKK list and use only its " +
|
|
15243
|
+
"fresh snapshot-bound handoff_decision");
|
|
15244
|
+
}
|
|
14312
15245
|
}
|
|
14313
15246
|
async function runTerminalDispatchClose({ options, terminalConversation }) {
|
|
14314
15247
|
const terminalControl = terminalConversation.terminalControl;
|
|
@@ -17991,17 +18924,20 @@ async function probeManualCodexLifecycleRecoveryIdentity({ options, terminal, tr
|
|
|
17991
18924
|
if (!cleared?.screen.digest) {
|
|
17992
18925
|
throw new Error("Codex composer did not become empty after recovery clear-line");
|
|
17993
18926
|
}
|
|
17994
|
-
|
|
17995
|
-
|
|
17996
|
-
|
|
17997
|
-
}
|
|
17998
|
-
const
|
|
17999
|
-
|
|
18927
|
+
const agentVersion = agentVersionForRunningProcess(terminal.agent, terminal.pid, options);
|
|
18928
|
+
if (!agentVersion) {
|
|
18929
|
+
throw new Error("Codex lifecycle recovery /status requires the exact running version before terminal input");
|
|
18930
|
+
}
|
|
18931
|
+
const submission = await bridge.submitCodexStatusProbe(terminal.terminalControl, agentVersion, { runtime: recoveryRuntime });
|
|
18932
|
+
const baselineDigest = submission.observationBaselineDigest;
|
|
18000
18933
|
let stable;
|
|
18001
18934
|
let stableCount = 0;
|
|
18002
18935
|
let lastObservationReason = "no fresh idle status screen was captured";
|
|
18003
18936
|
for (let attempt = 0; attempt < 12; attempt += 1) {
|
|
18004
|
-
const status = await bridge.status(terminal.agent, terminal.terminalControl, {
|
|
18937
|
+
const status = await bridge.status(terminal.agent, terminal.terminalControl, {
|
|
18938
|
+
runtime: recoveryRuntime,
|
|
18939
|
+
scrollbackLines: submission.observationScrollbackLines
|
|
18940
|
+
});
|
|
18005
18941
|
if (status.reachable &&
|
|
18006
18942
|
status.activity_state === "idle" &&
|
|
18007
18943
|
!status.approval_state.blocked &&
|
|
@@ -18232,14 +19168,7 @@ function claudeComposerVisible(screen) {
|
|
|
18232
19168
|
.some((line) => /^\s*❯(?:\s|$)/u.test(line));
|
|
18233
19169
|
}
|
|
18234
19170
|
function claudeComposerEmpty(screen) {
|
|
18235
|
-
|
|
18236
|
-
while (lines.length > 0 && lines.at(-1)?.trim() === "") {
|
|
18237
|
-
lines.pop();
|
|
18238
|
-
}
|
|
18239
|
-
const composers = lines
|
|
18240
|
-
.slice(-8)
|
|
18241
|
-
.filter((line) => /^\s*❯(?:\s|$)/u.test(line));
|
|
18242
|
-
return composers.length === 1 && /^\s*❯\s*$/u.test(composers[0]);
|
|
19171
|
+
return isExactClaudeIdleComposer(String(screen ?? ""));
|
|
18243
19172
|
}
|
|
18244
19173
|
function assertManualLifecycleProcessIncarnation(transition, live) {
|
|
18245
19174
|
if (live.processUuid !== transition.before_process_uuid ||
|