@scotthuang/agent-knock-knock 0.12.3 → 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 +13 -0
- package/README.md +4 -4
- package/dist/src/cli-core.js +654 -135
- 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 +29 -0
- package/dist/src/terminal-agent-bridge.js +387 -16
- package/dist/src/terminal-agent-bridge.js.map +1 -1
- package/dist/src/terminal-control-provider.d.ts +19 -0
- package/dist/src/terminal-control-provider.js +86 -7
- package/dist/src/terminal-control-provider.js.map +1 -1
- package/package.json +1 -1
- package/templates/openclaw-skills/agent-knock-knock/SKILL.md +3 -3
package/dist/src/cli-core.js
CHANGED
|
@@ -20,7 +20,7 @@ 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";
|
|
@@ -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,
|
|
@@ -2999,9 +3302,7 @@ async function terminalControlledListEntry(session, activeSessions, options, bri
|
|
|
2999
3302
|
});
|
|
3000
3303
|
const codexLifecycleIncarnationAvailable = session.agent !== "codex" ||
|
|
3001
3304
|
Boolean(nativeProcessUuid && nativeProcessBirth);
|
|
3002
|
-
const
|
|
3003
|
-
terminalControlsShareIncarnation(terminalControlForManagedConversation(turn), terminalControl) &&
|
|
3004
|
-
SESSION_SEND_BLOCKING_STATUSES.has(turn.status));
|
|
3305
|
+
const terminalHasBlockingTurn = terminalIncarnationBlockingTurns(storeDirFromOptions(options), terminalControl).length > 0;
|
|
3005
3306
|
let automatedInputComposerReady = nativeInspectionComposerEmpty(session.agent, terminalState.screen_excerpt);
|
|
3006
3307
|
if (session.agent === "codex" &&
|
|
3007
3308
|
(terminalState.activity_state === "idle" ||
|
|
@@ -3078,7 +3379,7 @@ async function terminalControlledListEntry(session, activeSessions, options, bri
|
|
|
3078
3379
|
}
|
|
3079
3380
|
: {}),
|
|
3080
3381
|
commands: {
|
|
3081
|
-
send:
|
|
3382
|
+
send: !terminalHasBlockingTurn,
|
|
3082
3383
|
approve: terminalControl.capabilities.includes("terminal_approval") &&
|
|
3083
3384
|
terminalState.approval_state.approvable === true,
|
|
3084
3385
|
status: true,
|
|
@@ -3086,7 +3387,8 @@ async function terminalControlledListEntry(session, activeSessions, options, bri
|
|
|
3086
3387
|
close: orphanedDispatch !== undefined,
|
|
3087
3388
|
new_thread: lifecycleCapability.status === "supported" &&
|
|
3088
3389
|
lifecycleCapability.newThread === true &&
|
|
3089
|
-
codexLifecycleIncarnationAvailable
|
|
3390
|
+
codexLifecycleIncarnationAvailable &&
|
|
3391
|
+
!terminalHasBlockingTurn,
|
|
3090
3392
|
list_resumable_threads: lifecycleCapability.status === "supported" &&
|
|
3091
3393
|
lifecycleCapability.resumeExact === true &&
|
|
3092
3394
|
codexLifecycleIncarnationAvailable,
|
|
@@ -3101,7 +3403,7 @@ async function terminalControlledListEntry(session, activeSessions, options, bri
|
|
|
3101
3403
|
: Boolean(nativeAgentIdentity?.sessionId &&
|
|
3102
3404
|
nativeAgentIdentity.processUuid)) &&
|
|
3103
3405
|
orphanedDispatch === undefined &&
|
|
3104
|
-
!
|
|
3406
|
+
!terminalHasBlockingTurn
|
|
3105
3407
|
}
|
|
3106
3408
|
};
|
|
3107
3409
|
const availableActions = availableListActions(entry);
|
|
@@ -3283,6 +3585,13 @@ function terminalFirstListProjection({ storeDir, terminals, managedSessions, ses
|
|
|
3283
3585
|
})
|
|
3284
3586
|
: undefined;
|
|
3285
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)));
|
|
3286
3595
|
const nativeIdentityObservation = isRecord(terminal.native_agent_identity_observation)
|
|
3287
3596
|
? terminal.native_agent_identity_observation
|
|
3288
3597
|
: undefined;
|
|
@@ -3327,6 +3636,7 @@ function terminalFirstListProjection({ storeDir, terminals, managedSessions, ses
|
|
|
3327
3636
|
!(isRecord(terminal.approval_state) &&
|
|
3328
3637
|
terminal.approval_state.blocked === true) &&
|
|
3329
3638
|
blockingHandoffTurns.length === 0 &&
|
|
3639
|
+
terminalBlockingTurns.length === 0 &&
|
|
3330
3640
|
!managedSessionHasUnresolvedNativeTransition(storeDir, soleBindingConflict.session) &&
|
|
3331
3641
|
!verifiedEmptySourceActiveElsewhere &&
|
|
3332
3642
|
terminal.orphaned_terminal_dispatch === undefined &&
|
|
@@ -3361,6 +3671,7 @@ function terminalFirstListProjection({ storeDir, terminals, managedSessions, ses
|
|
|
3361
3671
|
!(isRecord(terminal.approval_state) &&
|
|
3362
3672
|
terminal.approval_state.blocked === true) &&
|
|
3363
3673
|
!conflictingSessionTurns.some((turn) => SESSION_SEND_BLOCKING_STATUSES.has(turn.status)) &&
|
|
3674
|
+
terminalBlockingTurns.length === 0 &&
|
|
3364
3675
|
!managedSessionHasUnresolvedNativeTransition(storeDir, soleBindingConflict.session)
|
|
3365
3676
|
? {
|
|
3366
3677
|
tool: "agent_knock_knock_reconcile_binding",
|
|
@@ -3383,6 +3694,7 @@ function terminalFirstListProjection({ storeDir, terminals, managedSessions, ses
|
|
|
3383
3694
|
!(isRecord(terminal.approval_state) &&
|
|
3384
3695
|
terminal.approval_state.blocked === true) &&
|
|
3385
3696
|
!conflictingSessionTurns.some((turn) => SESSION_SEND_BLOCKING_STATUSES.has(turn.status)) &&
|
|
3697
|
+
terminalBlockingTurns.length === 0 &&
|
|
3386
3698
|
!managedSessionHasUnresolvedNativeTransition(storeDir, soleBindingConflict.session) &&
|
|
3387
3699
|
externalHandoffTarget?.status === "eligible" &&
|
|
3388
3700
|
isRecord(rawActions.send) &&
|
|
@@ -3394,7 +3706,8 @@ function terminalFirstListProjection({ storeDir, terminals, managedSessions, ses
|
|
|
3394
3706
|
!(isRecord(terminal.approval_state) &&
|
|
3395
3707
|
terminal.approval_state.blocked === true) &&
|
|
3396
3708
|
!managedSessionHasUnresolvedNativeTransition(storeDir, soleBindingConflict.session) &&
|
|
3397
|
-
blockingHandoffTurns.length === 1
|
|
3709
|
+
blockingHandoffTurns.length === 1 &&
|
|
3710
|
+
terminalBlockingTurns.every((turn) => turn.conversation_id === blockingHandoffTurns[0].conversation_id)
|
|
3398
3711
|
? blockingHandoffTurns[0]
|
|
3399
3712
|
: undefined;
|
|
3400
3713
|
const handoffDecisionToken = handoffDecisionTurn &&
|
|
@@ -3434,6 +3747,14 @@ function terminalFirstListProjection({ storeDir, terminals, managedSessions, ses
|
|
|
3434
3747
|
}
|
|
3435
3748
|
}
|
|
3436
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));
|
|
3437
3758
|
const terminalCanAcceptSend = ownership.state === "none" && isRecord(sessionAwareRawActions.send);
|
|
3438
3759
|
if (ownership.state === "current" &&
|
|
3439
3760
|
!allRelated.some((conversation) => conversation.conversation_id === ownership.conversation.conversation_id)) {
|
|
@@ -3470,9 +3791,12 @@ function terminalFirstListProjection({ storeDir, terminals, managedSessions, ses
|
|
|
3470
3791
|
const currentTurnValue = ownership.state === "current"
|
|
3471
3792
|
? currentManagedTurnForTerminal(ownership.conversation, terminal, sessionAwareRawActions)
|
|
3472
3793
|
: undefined;
|
|
3473
|
-
const
|
|
3794
|
+
const currentTurnProjection = currentTurnValue && !mutationsAllowed
|
|
3474
3795
|
? readOnlyManagedTurn(currentTurnValue)
|
|
3475
3796
|
: currentTurnValue;
|
|
3797
|
+
const currentTurn = currentTurnProjection
|
|
3798
|
+
? withoutGenericHandoffSourceClose(currentTurnProjection, blockingHandoffTurnIds)
|
|
3799
|
+
: undefined;
|
|
3476
3800
|
const sortedDisplayed = [...sessionDisplayedRelated]
|
|
3477
3801
|
.filter((conversation) => conversation.conversation_id !== currentTurn?.conversation_id)
|
|
3478
3802
|
.sort(compareManagedConversationRecency);
|
|
@@ -3480,15 +3804,18 @@ function terminalFirstListProjection({ storeDir, terminals, managedSessions, ses
|
|
|
3480
3804
|
const recentTurnValue = recentConversation
|
|
3481
3805
|
? historicalManagedTurnForTerminal(recentConversation, terminalCanAcceptSend, terminal)
|
|
3482
3806
|
: undefined;
|
|
3483
|
-
const
|
|
3807
|
+
const recentTurnProjection = recentTurnValue && !mutationsAllowed
|
|
3484
3808
|
? readOnlyManagedTurn(recentTurnValue)
|
|
3485
3809
|
: recentTurnValue;
|
|
3810
|
+
const recentTurn = recentTurnProjection
|
|
3811
|
+
? withoutGenericHandoffSourceClose(recentTurnProjection, blockingHandoffTurnIds)
|
|
3812
|
+
: undefined;
|
|
3486
3813
|
const historyConversations = includeAll
|
|
3487
3814
|
? sortedDisplayed.filter((conversation) => conversation.conversation_id !== recentConversation?.conversation_id)
|
|
3488
3815
|
: [];
|
|
3489
3816
|
const history = historyConversations.map((conversation) => {
|
|
3490
3817
|
const turn = historicalManagedTurnForTerminal(conversation, terminalCanAcceptSend, terminal);
|
|
3491
|
-
return mutationsAllowed ? turn : readOnlyManagedTurn(turn);
|
|
3818
|
+
return withoutGenericHandoffSourceClose(mutationsAllowed ? turn : readOnlyManagedTurn(turn), blockingHandoffTurnIds);
|
|
3492
3819
|
});
|
|
3493
3820
|
const visibleTurnIds = new Set([currentTurn, recentTurn, ...history]
|
|
3494
3821
|
.map((turn) => stringValue(turn?.conversation_id))
|
|
@@ -3602,17 +3929,45 @@ function terminalFirstListProjection({ storeDir, terminals, managedSessions, ses
|
|
|
3602
3929
|
handoff_state: externalHandoffAdoptable
|
|
3603
3930
|
? "external_handoff_adoptable"
|
|
3604
3931
|
: "external_handoff_blocked",
|
|
3605
|
-
...(
|
|
3932
|
+
...(terminalRecoveryBlockingTurns.length > 0
|
|
3606
3933
|
? {
|
|
3607
|
-
handoff_blocked_reason: "the
|
|
3934
|
+
handoff_blocked_reason: "the terminal has unresolved managed Turn state; use only a listed blocking_turns recovery action"
|
|
3608
3935
|
}
|
|
3609
|
-
:
|
|
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
|
+
: {})
|
|
3610
3949
|
}
|
|
3611
3950
|
: {}),
|
|
3612
3951
|
...(verifiedEmptyCodexSnapshotToken
|
|
3613
3952
|
? { handoff_state: "verified_empty_native_session_adoptable" }
|
|
3614
3953
|
: {}),
|
|
3615
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
|
+
: {}),
|
|
3616
3971
|
managed: management,
|
|
3617
3972
|
available_actions: availableActions
|
|
3618
3973
|
};
|
|
@@ -3855,6 +4210,21 @@ function terminalControlForManagedConversation(conversation) {
|
|
|
3855
4210
|
? conversation.native_session_takeover
|
|
3856
4211
|
: undefined);
|
|
3857
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
|
+
}
|
|
3858
4228
|
function compareManagedConversationRecency(left, right) {
|
|
3859
4229
|
const leftTime = Date.parse(String(left.updated_at ?? left.created_at ?? ""));
|
|
3860
4230
|
const rightTime = Date.parse(String(right.updated_at ?? right.created_at ?? ""));
|
|
@@ -4074,6 +4444,20 @@ function readOnlyManagedTurn(managedTurn) {
|
|
|
4074
4444
|
: {})
|
|
4075
4445
|
};
|
|
4076
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
|
+
}
|
|
4077
4461
|
function historicalManagedTurnForTerminal(conversation, terminalCanAcceptSend, terminal) {
|
|
4078
4462
|
const managedTurn = managedTurnListEntry(summarizeConversation(conversation), {
|
|
4079
4463
|
terminalBridge: terminalBridgeEnabled(conversation),
|
|
@@ -4311,9 +4695,9 @@ function managedListApprovalState(conversation) {
|
|
|
4311
4695
|
}
|
|
4312
4696
|
function listActionContracts() {
|
|
4313
4697
|
return {
|
|
4314
|
-
version:
|
|
4698
|
+
version: 10,
|
|
4315
4699
|
instructions: [
|
|
4316
|
-
"Treat terminals[] as the primary resource and use only actions present in available_actions, except the snapshot-bound terminals[].handoff_decision.choices.take_over_current.action.
|
|
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.",
|
|
4317
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.",
|
|
4318
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.",
|
|
4319
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.",
|
|
@@ -4367,6 +4751,12 @@ function listActionContracts() {
|
|
|
4367
4751
|
authoritative_action_path: "terminals[].handoff_decision.choices.take_over_current.action",
|
|
4368
4752
|
requires_explicit_user_confirmation: true,
|
|
4369
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"
|
|
4370
4760
|
}
|
|
4371
4761
|
},
|
|
4372
4762
|
actions: {
|
|
@@ -5473,14 +5863,9 @@ async function verifyCodexPendingManagedSendStatus({ options, terminal, session,
|
|
|
5473
5863
|
if (!codexComposerVisible(initialStatus.screen.excerpt)) {
|
|
5474
5864
|
throw new Error("Codex status-card verification requires a visible idle composer");
|
|
5475
5865
|
}
|
|
5476
|
-
await assertCodexComposerReadyForAutomatedInput({
|
|
5477
|
-
options,
|
|
5478
|
-
terminalControl: terminal.terminalControl
|
|
5479
|
-
});
|
|
5480
5866
|
const observed = await probeCodexCurrentThread({
|
|
5481
5867
|
options,
|
|
5482
5868
|
terminal,
|
|
5483
|
-
initialStatus,
|
|
5484
5869
|
currentIdentity: logicalIdentity,
|
|
5485
5870
|
runtimeOverride: runtime
|
|
5486
5871
|
});
|
|
@@ -6044,10 +6429,12 @@ function assertExpectedHandoffTokenUsesExactTerminalSelector({ options, terminal
|
|
|
6044
6429
|
"conversation selector advertised by AKK list");
|
|
6045
6430
|
}
|
|
6046
6431
|
}
|
|
6047
|
-
async function observedExternalHandoffIdentity({ options, terminal, sourceSession, resolvedIdentity }) {
|
|
6432
|
+
async function observedExternalHandoffIdentity({ options, terminal, sourceSession, resolvedIdentity, requireSafeTerminal = true }) {
|
|
6048
6433
|
const bridge = createTerminalAgentBridge(options);
|
|
6049
6434
|
const status = await bridge.status(terminal.agent, terminal.terminalControl, { runtime: terminalRuntimeForLiveIdentity({ terminal, physicalOnly: true }) });
|
|
6050
|
-
|
|
6435
|
+
if (requireSafeTerminal) {
|
|
6436
|
+
assertSafeTerminalSend(terminal.agent, status);
|
|
6437
|
+
}
|
|
6051
6438
|
if (terminal.agent !== "codex") {
|
|
6052
6439
|
return { identity: resolvedIdentity, status };
|
|
6053
6440
|
}
|
|
@@ -6619,10 +7006,7 @@ function assertTerminalLifecycleReady({ options, terminal, terminalStatus }) {
|
|
|
6619
7006
|
throw new Error(`terminal ${terminal.terminalControl.target} is not at a verified idle prompt ` +
|
|
6620
7007
|
`(${terminalStatus.activity_state}: ${terminalStatus.activity_reason})`);
|
|
6621
7008
|
}
|
|
6622
|
-
const blockers =
|
|
6623
|
-
.filter(isDiscoverableTmuxConversation)
|
|
6624
|
-
.filter((turn) => terminalControlsShareIncarnation(terminalControlForManagedConversation(turn), terminal.terminalControl) &&
|
|
6625
|
-
SESSION_SEND_BLOCKING_STATUSES.has(turn.status));
|
|
7009
|
+
const blockers = terminalIncarnationBlockingTurns(storeDirFromOptions(options), terminal.terminalControl);
|
|
6626
7010
|
if (blockers.length > 0) {
|
|
6627
7011
|
throw new Error(`terminal ${terminal.terminalControl.target} still has unresolved Turn ` +
|
|
6628
7012
|
`${turnIdForConversation(blockers[0])} (${blockers[0].status})`);
|
|
@@ -7302,10 +7686,7 @@ function assertTerminalNativeInspectionReady({ options, terminal, terminalStatus
|
|
|
7302
7686
|
throw new Error(`terminal ${terminal.terminalControl.target} is not at a verified idle prompt ` +
|
|
7303
7687
|
`(${terminalStatus.activity_state}: ${terminalStatus.activity_reason})`);
|
|
7304
7688
|
}
|
|
7305
|
-
const blocker =
|
|
7306
|
-
.filter(isDiscoverableTmuxConversation)
|
|
7307
|
-
.find((turn) => terminalControlsShareIncarnation(terminalControlForManagedConversation(turn), terminal.terminalControl) &&
|
|
7308
|
-
SESSION_SEND_BLOCKING_STATUSES.has(turn.status));
|
|
7689
|
+
const blocker = terminalIncarnationBlockingTurns(storeDirFromOptions(options), terminal.terminalControl)[0];
|
|
7309
7690
|
if (blocker) {
|
|
7310
7691
|
throw new Error(`terminal ${terminal.terminalControl.target} still has unresolved Turn ` +
|
|
7311
7692
|
`${turnIdForConversation(blocker)} (${blocker.status})`);
|
|
@@ -7749,6 +8130,7 @@ async function waitForVerifiedThreadTransition({ options, terminal, beforeIdenti
|
|
|
7749
8130
|
const bridge = createTerminalAgentBridge(options);
|
|
7750
8131
|
let probeSent = false;
|
|
7751
8132
|
let postProbeBaselineDigest;
|
|
8133
|
+
let postProbeObservationScrollbackLines;
|
|
7752
8134
|
let stableIdentity;
|
|
7753
8135
|
let stableCount = 0;
|
|
7754
8136
|
const physicalBeforeFence = codexIdentityFence(physicalBeforeIdentity);
|
|
@@ -7816,22 +8198,37 @@ async function waitForVerifiedThreadTransition({ options, terminal, beforeIdenti
|
|
|
7816
8198
|
}
|
|
7817
8199
|
else {
|
|
7818
8200
|
const afterProbe = plan.steps.find((step) => step.kind === "identity_probe_after");
|
|
7819
|
-
if (!afterProbe ||
|
|
8201
|
+
if (!afterProbe ||
|
|
8202
|
+
afterProbe.effect !== "read_only" ||
|
|
8203
|
+
afterProbe.command !== "/status") {
|
|
7820
8204
|
throw new Error("the Codex lifecycle plan has no exact post-transition identity probe");
|
|
7821
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
|
+
}
|
|
7822
8210
|
const physicalRuntime = terminalRuntimeForLiveIdentity({
|
|
7823
8211
|
terminal,
|
|
7824
8212
|
physicalOnly: true
|
|
7825
8213
|
});
|
|
7826
|
-
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
|
+
});
|
|
7827
8222
|
const screenDigest = stringValue(status.screen.digest);
|
|
7828
8223
|
if (!probeSent &&
|
|
7829
8224
|
status.reachable &&
|
|
7830
8225
|
status.activity_state === "idle" &&
|
|
7831
8226
|
screenDigest !== undefined &&
|
|
7832
8227
|
screenDigest !== initialScreenDigest) {
|
|
7833
|
-
|
|
7834
|
-
|
|
8228
|
+
const submission = await bridge.submitCodexStatusProbe(terminal.terminalControl, agentVersion, { runtime: physicalRuntime });
|
|
8229
|
+
postProbeBaselineDigest = submission.observationBaselineDigest;
|
|
8230
|
+
postProbeObservationScrollbackLines =
|
|
8231
|
+
submission.observationScrollbackLines;
|
|
7835
8232
|
probeSent = true;
|
|
7836
8233
|
}
|
|
7837
8234
|
else if (status.reachable &&
|
|
@@ -7960,7 +8357,7 @@ function assertVerifiedClaudeLifecycleBeforeObservation({ options, terminal, ide
|
|
|
7960
8357
|
`for the current process incarnation${observed?.reason ? `: ${observed.reason}` : ""}`);
|
|
7961
8358
|
}
|
|
7962
8359
|
}
|
|
7963
|
-
async function probeCodexCurrentThread({ options, terminal,
|
|
8360
|
+
async function probeCodexCurrentThread({ options, terminal, currentIdentity, runtimeIdentity, runtimeOverride }) {
|
|
7964
8361
|
if (terminal.agent !== "codex") {
|
|
7965
8362
|
throw new Error("the current native thread identity is unavailable");
|
|
7966
8363
|
}
|
|
@@ -7972,24 +8369,31 @@ async function probeCodexCurrentThread({ options, terminal, initialStatus, curre
|
|
|
7972
8369
|
terminal,
|
|
7973
8370
|
expectedEmptyNativeSession: true
|
|
7974
8371
|
}));
|
|
7975
|
-
|
|
7976
|
-
|
|
7977
|
-
|
|
7978
|
-
|
|
7979
|
-
|
|
7980
|
-
|
|
7981
|
-
|
|
7982
|
-
});
|
|
7983
|
-
|
|
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;
|
|
7984
8382
|
for (let attempt = 0; attempt < 50; attempt += 1) {
|
|
7985
|
-
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
|
+
});
|
|
7986
8387
|
if (status.reachable &&
|
|
7987
|
-
status.screen.digest !==
|
|
8388
|
+
status.screen.digest !== submission.observationBaselineDigest) {
|
|
7988
8389
|
const observed = terminal.adapter.observeThreadLifecycle?.({
|
|
7989
8390
|
operation: { kind: "new_thread" },
|
|
7990
8391
|
phase: "before",
|
|
7991
8392
|
screen: status.screen.excerpt ?? ""
|
|
7992
8393
|
});
|
|
8394
|
+
lastObservationReason = observed?.reason ?? lastObservationReason;
|
|
8395
|
+
sawIncompleteSessionUuid ||= observed?.status === "missing" &&
|
|
8396
|
+
/complete Session UUID/iu.test(observed.reason ?? "");
|
|
7993
8397
|
if (observed?.status === "observed" &&
|
|
7994
8398
|
observed.nativeThreadId &&
|
|
7995
8399
|
status.activity_state === "idle") {
|
|
@@ -8006,7 +8410,12 @@ async function probeCodexCurrentThread({ options, terminal, initialStatus, curre
|
|
|
8006
8410
|
}
|
|
8007
8411
|
await cliSleep(100);
|
|
8008
8412
|
}
|
|
8009
|
-
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}`);
|
|
8010
8419
|
}
|
|
8011
8420
|
async function runNewThread(options) {
|
|
8012
8421
|
const requireRestorableOrigin = options.requireRestorableOrigin;
|
|
@@ -8363,7 +8772,6 @@ async function runNativeThreadTransition(options, operation) {
|
|
|
8363
8772
|
const statusIdentity = await probeCodexCurrentThread({
|
|
8364
8773
|
options,
|
|
8365
8774
|
terminal,
|
|
8366
|
-
initialStatus: terminalStatus,
|
|
8367
8775
|
currentIdentity: beforeIdentity,
|
|
8368
8776
|
runtimeIdentity: snapshot.runtimeIdentity,
|
|
8369
8777
|
runtimeOverride: beforeRuntime
|
|
@@ -9027,17 +9435,21 @@ async function reconcileStoreForStatus(storeDir, options, conversationId) {
|
|
|
9027
9435
|
}
|
|
9028
9436
|
throw error;
|
|
9029
9437
|
}
|
|
9030
|
-
const idle = reconcileIdleConversations(storeDir, options, cliNow(), conversationId);
|
|
9031
9438
|
const monitors = await reconcileMonitors(options, {
|
|
9032
9439
|
includeCallbackRecovery: false,
|
|
9033
9440
|
reason: "status_reconciliation",
|
|
9034
9441
|
conversationId
|
|
9035
9442
|
});
|
|
9443
|
+
const idle = reconcileIdleConversations(storeDir, options, cliNow(), conversationId);
|
|
9036
9444
|
return {
|
|
9037
9445
|
status: "completed",
|
|
9038
9446
|
checked: Math.max(idle.checked, monitors.checked),
|
|
9039
|
-
changed: idle.closed + monitors.launched,
|
|
9447
|
+
changed: idle.closed + monitors.launched + monitors.repaired,
|
|
9040
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"),
|
|
9041
9453
|
monitors_launched: monitors.launched,
|
|
9042
9454
|
monitors_already_running: monitors.already_running,
|
|
9043
9455
|
skipped: idle.skipped + monitors.skipped,
|
|
@@ -9564,6 +9976,7 @@ async function runSend(options) {
|
|
|
9564
9976
|
})) {
|
|
9565
9977
|
return;
|
|
9566
9978
|
}
|
|
9979
|
+
assertTerminalIncarnationCanStartTurn(rawStoreDir, terminalConversation.terminalControl);
|
|
9567
9980
|
let claimedSession = soleBoundManagedSessionClaimForTerminal(rawStoreDir, terminalConversation);
|
|
9568
9981
|
let knownCodexCompanions = claimedSession
|
|
9569
9982
|
? codexAllowedCompanionSetForManagedSession({
|
|
@@ -9854,6 +10267,7 @@ async function runSend(options) {
|
|
|
9854
10267
|
})) {
|
|
9855
10268
|
return;
|
|
9856
10269
|
}
|
|
10270
|
+
assertTerminalIncarnationCanStartTurn(storeDir, resolvedTerminal.terminalControl);
|
|
9857
10271
|
let currentSession = tryLoadManagedSession(storeDir, sessionId);
|
|
9858
10272
|
const knownCodexCompanions = currentSession
|
|
9859
10273
|
? codexAllowedCompanionSetForManagedSession({
|
|
@@ -13750,14 +14164,23 @@ async function runReconcileMonitors(options) {
|
|
|
13750
14164
|
}
|
|
13751
14165
|
async function reconcileMonitors(options, { includeCallbackRecovery, reason, conversationId }) {
|
|
13752
14166
|
const storeDir = storeDirFromOptions(options);
|
|
14167
|
+
const collateralStalls = await withStoreWriterLeaseAsync(storeDir, async () => reconcileTerminalBridgeCollateralStalls(storeDir, conversationId));
|
|
13753
14168
|
const conversations = listConversations(storeDir).filter((conversation) => conversationId === undefined || conversation.conversation_id === conversationId);
|
|
13754
|
-
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));
|
|
13755
14174
|
let ignored = 0;
|
|
13756
14175
|
let launched = 0;
|
|
13757
14176
|
let alreadyRunning = 0;
|
|
13758
14177
|
let skipped = 0;
|
|
13759
|
-
let errors =
|
|
14178
|
+
let errors = collateralStalls.errors.length;
|
|
13760
14179
|
for (const listedConversation of conversations) {
|
|
14180
|
+
if (repairedConversationIds.has(listedConversation.conversation_id)) {
|
|
14181
|
+
ignored += 1;
|
|
14182
|
+
continue;
|
|
14183
|
+
}
|
|
13761
14184
|
if (!matchesConfiguredWorkspace(options.workspace, listedConversation.workspace)) {
|
|
13762
14185
|
ignored += 1;
|
|
13763
14186
|
continue;
|
|
@@ -13991,6 +14414,9 @@ async function reconcileMonitors(options, { includeCallbackRecovery, reason, con
|
|
|
13991
14414
|
reconciled: true,
|
|
13992
14415
|
store_dir: storeDir,
|
|
13993
14416
|
checked: conversations.length,
|
|
14417
|
+
repaired: collateralStalls.repaired,
|
|
14418
|
+
collateral_stalls_checked: collateralStalls.checked,
|
|
14419
|
+
collateral_stalls_skipped: collateralStalls.skipped,
|
|
13994
14420
|
ignored,
|
|
13995
14421
|
launched,
|
|
13996
14422
|
already_running: alreadyRunning,
|
|
@@ -14457,7 +14883,7 @@ async function runObservedHandoffClose({ options, statePath, logPath, initialCon
|
|
|
14457
14883
|
if (stringValue(options.reason) !== "superseded_by_human_context_switch") {
|
|
14458
14884
|
throw new Error("a handoff close requires reason superseded_by_human_context_switch");
|
|
14459
14885
|
}
|
|
14460
|
-
const storeDir =
|
|
14886
|
+
const storeDir = pathsForConversationDir(path.dirname(statePath)).storeDir;
|
|
14461
14887
|
const sourceSessionId = sessionIdForConversation(initialConversation);
|
|
14462
14888
|
const initialSource = loadManagedSession(storeDir, sourceSessionId);
|
|
14463
14889
|
if (!initialSource.binding || initialSource.status !== "bound") {
|
|
@@ -14643,88 +15069,178 @@ async function runClose(options) {
|
|
|
14643
15069
|
});
|
|
14644
15070
|
return;
|
|
14645
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
|
+
}
|
|
14646
15076
|
const { statePath, logPath } = loaded;
|
|
15077
|
+
const closeStoreDir = pathsForConversationDir(path.dirname(statePath)).storeDir;
|
|
14647
15078
|
const nativeTakeover = isRecord(loaded.conversation.native_session_takeover)
|
|
14648
15079
|
? loaded.conversation.native_session_takeover
|
|
14649
15080
|
: undefined;
|
|
14650
15081
|
const terminalControl = terminalControlFromTakeover(nativeTakeover);
|
|
14651
15082
|
const releaseTerminalLock = terminalControl
|
|
14652
|
-
? acquireTerminalBridgeSendLock(
|
|
15083
|
+
? acquireTerminalBridgeSendLock(closeStoreDir, terminalControl, { timeoutMs: 30000 })
|
|
14653
15084
|
: () => { };
|
|
14654
|
-
|
|
14655
|
-
|
|
14656
|
-
|
|
14657
|
-
|
|
14658
|
-
|
|
14659
|
-
|
|
14660
|
-
|
|
14661
|
-
|
|
14662
|
-
|
|
14663
|
-
|
|
14664
|
-
|
|
14665
|
-
status: "closed",
|
|
14666
|
-
closed_at: now,
|
|
14667
|
-
close_reason: closeReason,
|
|
14668
|
-
...(handoffDisposition
|
|
14669
|
-
? { disposition: handoffDisposition }
|
|
14670
|
-
: {}),
|
|
14671
|
-
updated_at: now
|
|
14672
|
-
};
|
|
14673
|
-
saveState(statePath, closed);
|
|
14674
|
-
let dispatchLedgerResolved = false;
|
|
14675
|
-
let dispatchLedgerWarning;
|
|
14676
|
-
if (terminalControl) {
|
|
14677
|
-
try {
|
|
14678
|
-
dispatchLedgerResolved = resolveTerminalBridgeDispatchLedger({
|
|
14679
|
-
terminalControl,
|
|
14680
|
-
conversation: closed,
|
|
14681
|
-
expectedMessageId: stringValue(nativeTakeover?.terminal_bridge_message_id),
|
|
14682
|
-
reason: "conversation explicitly closed by request"
|
|
14683
|
-
});
|
|
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");
|
|
14684
15096
|
}
|
|
14685
|
-
|
|
14686
|
-
|
|
14687
|
-
|
|
14688
|
-
|
|
14689
|
-
|
|
14690
|
-
|
|
14691
|
-
error: dispatchLedgerWarning
|
|
15097
|
+
if (terminalControl && currentTerminalControl) {
|
|
15098
|
+
await assertGenericCloseDoesNotBypassObservedHandoff({
|
|
15099
|
+
options,
|
|
15100
|
+
storeDir: closeStoreDir,
|
|
15101
|
+
conversation,
|
|
15102
|
+
terminalControl: currentTerminalControl
|
|
14692
15103
|
});
|
|
14693
15104
|
}
|
|
14694
|
-
|
|
14695
|
-
|
|
14696
|
-
|
|
14697
|
-
|
|
14698
|
-
|
|
14699
|
-
|
|
14700
|
-
|
|
14701
|
-
|
|
14702
|
-
|
|
14703
|
-
|
|
14704
|
-
|
|
14705
|
-
|
|
14706
|
-
|
|
14707
|
-
|
|
14708
|
-
|
|
14709
|
-
|
|
14710
|
-
|
|
14711
|
-
|
|
14712
|
-
|
|
14713
|
-
|
|
14714
|
-
? {
|
|
14715
|
-
terminal_dispatch_warning: textSummary(dispatchLedgerWarning),
|
|
14716
|
-
do_not_retry: true
|
|
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
|
+
});
|
|
14717
15125
|
}
|
|
14718
|
-
|
|
14719
|
-
|
|
14720
|
-
|
|
14721
|
-
|
|
14722
|
-
|
|
14723
|
-
|
|
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
|
+
});
|
|
14724
15161
|
}
|
|
14725
15162
|
finally {
|
|
14726
|
-
|
|
15163
|
+
releaseStateLock();
|
|
15164
|
+
}
|
|
15165
|
+
};
|
|
15166
|
+
try {
|
|
15167
|
+
if (terminalControl) {
|
|
15168
|
+
await withStoreWriterLeaseAsync(closeStoreDir, closeWithFreshState);
|
|
15169
|
+
}
|
|
15170
|
+
else {
|
|
15171
|
+
await closeWithFreshState();
|
|
15172
|
+
}
|
|
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;
|
|
14727
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");
|
|
14728
15244
|
}
|
|
14729
15245
|
}
|
|
14730
15246
|
async function runTerminalDispatchClose({ options, terminalConversation }) {
|
|
@@ -18408,17 +18924,20 @@ async function probeManualCodexLifecycleRecoveryIdentity({ options, terminal, tr
|
|
|
18408
18924
|
if (!cleared?.screen.digest) {
|
|
18409
18925
|
throw new Error("Codex composer did not become empty after recovery clear-line");
|
|
18410
18926
|
}
|
|
18411
|
-
|
|
18412
|
-
|
|
18413
|
-
|
|
18414
|
-
}
|
|
18415
|
-
const
|
|
18416
|
-
|
|
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;
|
|
18417
18933
|
let stable;
|
|
18418
18934
|
let stableCount = 0;
|
|
18419
18935
|
let lastObservationReason = "no fresh idle status screen was captured";
|
|
18420
18936
|
for (let attempt = 0; attempt < 12; attempt += 1) {
|
|
18421
|
-
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
|
+
});
|
|
18422
18941
|
if (status.reachable &&
|
|
18423
18942
|
status.activity_state === "idle" &&
|
|
18424
18943
|
!status.approval_state.blocked &&
|