@scotthuang/agent-knock-knock 0.12.5 → 0.12.6
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 +21 -0
- package/README.md +14 -10
- package/dist/src/agent-session-provider.d.ts +34 -0
- package/dist/src/cli-core.js +2390 -734
- package/dist/src/cli-core.js.map +1 -1
- package/dist/src/codex-local-session-provider.d.ts +3 -1
- package/dist/src/codex-local-session-provider.js +6 -0
- package/dist/src/codex-local-session-provider.js.map +1 -1
- package/dist/src/codex-store-adapter.d.ts +18 -1
- package/dist/src/codex-store-adapter.js +135 -16
- package/dist/src/codex-store-adapter.js.map +1 -1
- package/dist/src/deferred-foreground-transfer.d.ts +19 -4
- package/dist/src/deferred-foreground-transfer.js +77 -5
- package/dist/src/deferred-foreground-transfer.js.map +1 -1
- package/dist/src/openclaw-plugin.js +22 -4
- package/dist/src/openclaw-plugin.js.map +1 -1
- package/dist/src/store.d.ts +1 -1
- package/dist/src/store.js +96 -6
- package/dist/src/store.js.map +1 -1
- package/dist/src/terminal-submission-acceptance.d.ts +51 -1
- package/dist/src/terminal-submission-acceptance.js +361 -71
- package/dist/src/terminal-submission-acceptance.js.map +1 -1
- package/package.json +1 -1
- package/templates/openclaw-skills/agent-knock-knock/SKILL.md +8 -8
package/dist/src/cli-core.js
CHANGED
|
@@ -8,7 +8,7 @@ import { fileURLToPath } from "node:url";
|
|
|
8
8
|
import { createCodexTerminalAgentAdapter, detectCodexDurableCompletion } from "./codex-terminal-agent-adapter.js";
|
|
9
9
|
import { createClaudeTerminalAgentAdapter } from "./claude-terminal-agent-adapter.js";
|
|
10
10
|
import { captureClaudeTranscriptAnchor, defaultClaudeHome, detectClaudeTranscriptAcceptance, detectClaudeTranscriptCompletion, detectClaudeTranscriptPendingApproval, listClaudeThreadLifecycleCandidates, revalidateClaudeThreadLifecycleCandidate } from "./claude-local-transcript-provider.js";
|
|
11
|
-
import { captureCodexRolloutAcceptanceAnchor, detectCodexBoundRolloutCompletion, detectCodexRolloutAcceptance, terminalSubmissionReplayReceipt, validateTerminalSubmissionAcceptanceEvidence } from "./terminal-submission-acceptance.js";
|
|
11
|
+
import { captureCodexCandidateSetRolloutAcceptanceAnchor, captureCodexRolloutAcceptanceAnchor, detectCodexBoundRolloutCompletion, detectCodexCandidateSetRolloutAcceptance, detectCodexRolloutAcceptance, terminalSubmissionReplayReceipt, validateCodexRolloutAcceptanceAnchor, validateTerminalSubmissionAcceptanceEvidence } from "./terminal-submission-acceptance.js";
|
|
12
12
|
import { CodexLocalSessionProvider } from "./codex-local-session-provider.js";
|
|
13
13
|
import { CodexStoreAdapter } from "./codex-store-adapter.js";
|
|
14
14
|
import { createDeferredForegroundTransferId, listDeferredForegroundTransfers, loadDeferredForegroundTransfer, saveDeferredForegroundTransfer } from "./deferred-foreground-transfer.js";
|
|
@@ -1710,7 +1710,8 @@ function startTerminalBridgeMonitor({ statePath, logPath, agentTimeoutMinutes, a
|
|
|
1710
1710
|
return child;
|
|
1711
1711
|
}
|
|
1712
1712
|
function startTerminalBridgeMonitorForConversation({ conversation, statePath, logPath, options }) {
|
|
1713
|
-
if (!terminalBridgeEnabled(conversation) ||
|
|
1713
|
+
if (!terminalBridgeEnabled(conversation) ||
|
|
1714
|
+
options.disableTerminalBridgeMonitor === true) {
|
|
1714
1715
|
return undefined;
|
|
1715
1716
|
}
|
|
1716
1717
|
const nativeTakeover = isRecord(conversation.native_session_takeover)
|
|
@@ -2883,12 +2884,18 @@ function exactTerminalBridgeCollateralStallRepairEvidence({ conversation, storeD
|
|
|
2883
2884
|
};
|
|
2884
2885
|
}
|
|
2885
2886
|
function reconcileTerminalBridgeCollateralStalls(storeDir, conversationId) {
|
|
2887
|
+
const reservedSourceTurnIds = new Set(listDeferredForegroundTransfers(storeDir)
|
|
2888
|
+
.filter((transfer) => transfer.version === 2 &&
|
|
2889
|
+
transfer.source_kind === "candidate_rollout_quiescent" &&
|
|
2890
|
+
!["resolved", "abort_resolved"].includes(transfer.status))
|
|
2891
|
+
.flatMap((transfer) => (transfer.source_turn_history ?? []).map((turn) => turn.turn_id)));
|
|
2886
2892
|
const candidates = listConversations(storeDir).filter((conversation) => {
|
|
2887
2893
|
const takeover = isRecord(conversation.native_session_takeover)
|
|
2888
2894
|
? conversation.native_session_takeover
|
|
2889
2895
|
: undefined;
|
|
2890
2896
|
return (conversationId === undefined ||
|
|
2891
2897
|
conversation.conversation_id === conversationId) &&
|
|
2898
|
+
!reservedSourceTurnIds.has(turnIdForConversation(conversation)) &&
|
|
2892
2899
|
conversation.status === "stalled" &&
|
|
2893
2900
|
isRecord(takeover?.terminal_bridge_uncertain_dispatch_fence);
|
|
2894
2901
|
});
|
|
@@ -3191,8 +3198,14 @@ async function terminalControlledListEntry(session, activeSessions, options, bri
|
|
|
3191
3198
|
}
|
|
3192
3199
|
const terminalState = await listStateForTerminal(session.agent, terminalControl, options, bridge, {
|
|
3193
3200
|
pid: session.pid,
|
|
3194
|
-
sessionId: session.sessionId,
|
|
3195
3201
|
cwd: session.cwd,
|
|
3202
|
+
// Raw Codex status and approval use the exact full terminal selector as
|
|
3203
|
+
// their runtime conversation identity. Use the same identity while
|
|
3204
|
+
// projecting list actions so a prompt-bound terminal token can be
|
|
3205
|
+
// revalidated by the later status/approve calls.
|
|
3206
|
+
...(session.agent === "codex"
|
|
3207
|
+
? { conversationId: bridge.terminalConversationId(session) }
|
|
3208
|
+
: { sessionId: session.sessionId }),
|
|
3196
3209
|
terminalTarget: terminalControl.target
|
|
3197
3210
|
});
|
|
3198
3211
|
const orphanedDispatch = orphanedTerminalDispatchForRecovery(terminalControl);
|
|
@@ -3233,6 +3246,26 @@ async function terminalControlledListEntry(session, activeSessions, options, bri
|
|
|
3233
3246
|
const nativeAgentIdentity = nativeIdentityObservation.status === "resolved"
|
|
3234
3247
|
? nativeIdentityObservation.identity
|
|
3235
3248
|
: undefined;
|
|
3249
|
+
let codexOpenRootRolloutInventory;
|
|
3250
|
+
if (session.agent === "codex") {
|
|
3251
|
+
try {
|
|
3252
|
+
const inventory = await inspectCodexOpenRootRolloutInventory({
|
|
3253
|
+
options,
|
|
3254
|
+
pid: session.pid,
|
|
3255
|
+
cwd: session.cwd ?? terminalControl.currentPath
|
|
3256
|
+
});
|
|
3257
|
+
if (inventory.roots.length > 0) {
|
|
3258
|
+
codexOpenRootRolloutInventory = inventory;
|
|
3259
|
+
}
|
|
3260
|
+
}
|
|
3261
|
+
catch (error) {
|
|
3262
|
+
runtimeLog("warn", "terminal_codex_open_root_inventory_unavailable", {
|
|
3263
|
+
terminal_target: terminalControl.target,
|
|
3264
|
+
pid: session.pid,
|
|
3265
|
+
error: error instanceof Error ? error.message : String(error)
|
|
3266
|
+
});
|
|
3267
|
+
}
|
|
3268
|
+
}
|
|
3236
3269
|
if (nativeIdentityObservation.status === "unavailable") {
|
|
3237
3270
|
runtimeLog("warn", "terminal_native_session_identity_unavailable", {
|
|
3238
3271
|
agent: session.agent,
|
|
@@ -3244,6 +3277,11 @@ async function terminalControlledListEntry(session, activeSessions, options, bri
|
|
|
3244
3277
|
let nativeProcessUuid = nativeAgentIdentity?.processUuid;
|
|
3245
3278
|
let nativeProcessBirth = nativeAgentIdentity?.processBirth;
|
|
3246
3279
|
let nativeProcessEvidence = nativeAgentIdentity?.evidence;
|
|
3280
|
+
if (codexOpenRootRolloutInventory) {
|
|
3281
|
+
nativeProcessUuid = codexOpenRootRolloutInventory.processUuid;
|
|
3282
|
+
nativeProcessBirth = codexOpenRootRolloutInventory.processBirth;
|
|
3283
|
+
nativeProcessEvidence = "codex_open_root_rollout_inventory";
|
|
3284
|
+
}
|
|
3247
3285
|
if (session.agent === "codex" &&
|
|
3248
3286
|
(!nativeProcessUuid || !nativeProcessBirth)) {
|
|
3249
3287
|
try {
|
|
@@ -3365,6 +3403,11 @@ async function terminalControlledListEntry(session, activeSessions, options, bri
|
|
|
3365
3403
|
// this field after gating every automated-input action that can follow a
|
|
3366
3404
|
// human native-thread switch.
|
|
3367
3405
|
_automated_input_composer_ready: automatedInputComposerReady,
|
|
3406
|
+
...(codexOpenRootRolloutInventory
|
|
3407
|
+
? {
|
|
3408
|
+
_codex_open_root_rollout_inventory: codexOpenRootRolloutInventory
|
|
3409
|
+
}
|
|
3410
|
+
: {}),
|
|
3368
3411
|
...(orphanedDispatch
|
|
3369
3412
|
? {
|
|
3370
3413
|
orphaned_terminal_dispatch: {
|
|
@@ -3416,9 +3459,16 @@ async function terminalControlledListEntry(session, activeSessions, options, bri
|
|
|
3416
3459
|
};
|
|
3417
3460
|
}
|
|
3418
3461
|
function terminalFirstListProjection({ storeDir, terminals, managedSessions, sessionAuthorityRequired, allConversations, displayedConversations, includeAll, managedOnly, statusFilter, mutationsAllowed }) {
|
|
3419
|
-
const nonterminalDeferredTransfers = listDeferredForegroundTransfers(storeDir).filter((transfer) => !["resolved", "
|
|
3462
|
+
const nonterminalDeferredTransfers = listDeferredForegroundTransfers(storeDir).filter((transfer) => !["resolved", "abort_resolved"].includes(transfer.status));
|
|
3420
3463
|
const nonterminalDeferredTransferIds = new Set(nonterminalDeferredTransfers.map((transfer) => transfer.transfer_id));
|
|
3464
|
+
const nonterminalDeferredSourceTurnIds = new Set(nonterminalDeferredTransfers.flatMap((transfer) => transfer.version === 2 &&
|
|
3465
|
+
transfer.source_kind === "candidate_rollout_quiescent"
|
|
3466
|
+
? (transfer.source_turn_history ?? []).map((turn) => turn.turn_id)
|
|
3467
|
+
: []));
|
|
3421
3468
|
const conversationHasNonterminalDeferredTransfer = (conversation) => {
|
|
3469
|
+
if (nonterminalDeferredSourceTurnIds.has(turnIdForConversation(conversation))) {
|
|
3470
|
+
return true;
|
|
3471
|
+
}
|
|
3422
3472
|
const takeover = isRecord(conversation.native_session_takeover)
|
|
3423
3473
|
? conversation.native_session_takeover
|
|
3424
3474
|
: undefined;
|
|
@@ -3432,7 +3482,10 @@ function terminalFirstListProjection({ storeDir, terminals, managedSessions, ses
|
|
|
3432
3482
|
return control ? [control] : [];
|
|
3433
3483
|
});
|
|
3434
3484
|
const projectedTerminals = terminals.map((terminal) => {
|
|
3435
|
-
const { _automated_input_composer_ready: automatedInputComposerReady, ...publicTerminal } = terminal;
|
|
3485
|
+
const { _automated_input_composer_ready: automatedInputComposerReady, _codex_open_root_rollout_inventory: codexOpenRootRolloutInventoryValue, ...publicTerminal } = terminal;
|
|
3486
|
+
const codexOpenRootRolloutInventory = isRecord(codexOpenRootRolloutInventoryValue)
|
|
3487
|
+
? codexOpenRootRolloutInventoryValue
|
|
3488
|
+
: undefined;
|
|
3436
3489
|
const terminalControl = isRecord(terminal.terminal_control)
|
|
3437
3490
|
? terminal.terminal_control
|
|
3438
3491
|
: undefined;
|
|
@@ -3675,9 +3728,14 @@ function terminalFirstListProjection({ storeDir, terminals, managedSessions, ses
|
|
|
3675
3728
|
sourceSession: soleBindingConflict.session
|
|
3676
3729
|
})
|
|
3677
3730
|
: undefined;
|
|
3731
|
+
const deferredCodexCandidateInventory = codexOpenRootRolloutInventory &&
|
|
3732
|
+
codexOpenRootRolloutInventory.roots.length > 0
|
|
3733
|
+
? codexOpenRootRolloutInventory
|
|
3734
|
+
: undefined;
|
|
3678
3735
|
const deferredCodexSource = authoritativeSession &&
|
|
3679
3736
|
terminal.agent === "codex" &&
|
|
3680
|
-
nativeIdentityObservation?.status === "verified_absent"
|
|
3737
|
+
(nativeIdentityObservation?.status === "verified_absent" ||
|
|
3738
|
+
deferredCodexCandidateInventory !== undefined)
|
|
3681
3739
|
? authoritativeSession
|
|
3682
3740
|
: undefined;
|
|
3683
3741
|
const deferredCodexProcessUuid = stringValue(terminal.native_agent_process_uuid);
|
|
@@ -3692,15 +3750,11 @@ function terminalFirstListProjection({ storeDir, terminals, managedSessions, ses
|
|
|
3692
3750
|
const deferredCodexDispatchSnapshot = terminalControl
|
|
3693
3751
|
? tryDeferredCodexForegroundDispatchSnapshot(terminalControl)
|
|
3694
3752
|
: undefined;
|
|
3695
|
-
const
|
|
3696
|
-
|
|
3697
|
-
|
|
3753
|
+
const deferredCodexCandidateSourceTurnHistory = deferredCodexCandidateInventory && deferredCodexSource
|
|
3754
|
+
? deferredCandidateSourceTurnHistory(storeDir, deferredCodexSource)
|
|
3755
|
+
: undefined;
|
|
3756
|
+
const deferredCodexStatusCardSourceEligible = Boolean(deferredCodexSource &&
|
|
3698
3757
|
terminalControl &&
|
|
3699
|
-
hasCanonicalTerminalEndpoint(terminalControl) &&
|
|
3700
|
-
discoveredOwnership.state === "none" &&
|
|
3701
|
-
unresolvedSessionClaims.length === 0 &&
|
|
3702
|
-
matchingSessions.length === 1 &&
|
|
3703
|
-
conflictingBoundSessionClaims.length === 0 &&
|
|
3704
3758
|
deferredCodexProcessUuid &&
|
|
3705
3759
|
deferredCodexProcessBirth &&
|
|
3706
3760
|
deferredCodexWorkspace &&
|
|
@@ -3715,7 +3769,44 @@ function terminalFirstListProjection({ storeDir, terminals, managedSessions, ses
|
|
|
3715
3769
|
}) &&
|
|
3716
3770
|
managedTurnsForSession(storeDir, deferredCodexSource.session_id).length === 0 &&
|
|
3717
3771
|
!deferredCodexSource.last_transition_id &&
|
|
3718
|
-
!managedSessionHasAnyNativeTransition(storeDir, deferredCodexSource)
|
|
3772
|
+
!managedSessionHasAnyNativeTransition(storeDir, deferredCodexSource));
|
|
3773
|
+
const deferredCodexCandidateSourceEligible = Boolean(deferredCodexSource &&
|
|
3774
|
+
terminalControl &&
|
|
3775
|
+
deferredCodexProcessUuid &&
|
|
3776
|
+
deferredCodexProcessBirth &&
|
|
3777
|
+
deferredCodexWorkspace &&
|
|
3778
|
+
deferredCodexCandidateInventory &&
|
|
3779
|
+
deferredCodexCandidateSourceTurnHistory &&
|
|
3780
|
+
exactBoundCodexCandidateSourceForDeferredSend({
|
|
3781
|
+
session: deferredCodexSource,
|
|
3782
|
+
terminalId: String(terminal.id),
|
|
3783
|
+
terminalControl,
|
|
3784
|
+
pid: Number(terminal.pid),
|
|
3785
|
+
workspace: deferredCodexWorkspace,
|
|
3786
|
+
processUuid: deferredCodexProcessUuid,
|
|
3787
|
+
processBirth: deferredCodexProcessBirth,
|
|
3788
|
+
inventory: deferredCodexCandidateInventory
|
|
3789
|
+
}) &&
|
|
3790
|
+
candidateSourceTransitionHistoryIsTerminal(storeDir, deferredCodexSource));
|
|
3791
|
+
const deferredCodexCandidateRouteNeeded = Boolean(deferredCodexCandidateInventory &&
|
|
3792
|
+
(deferredCodexCandidateInventory.status === "unbound" ||
|
|
3793
|
+
nativeIdentityObservation?.status === "unavailable" ||
|
|
3794
|
+
deferredCodexSource?.binding?.native_process.rollout === undefined));
|
|
3795
|
+
const deferredCodexForegroundEligible = Boolean(mutationsAllowed &&
|
|
3796
|
+
!terminalHasNonterminalDeferredTransfer &&
|
|
3797
|
+
deferredCodexSource &&
|
|
3798
|
+
terminalControl &&
|
|
3799
|
+
hasCanonicalTerminalEndpoint(terminalControl) &&
|
|
3800
|
+
discoveredOwnership.state === "none" &&
|
|
3801
|
+
unresolvedSessionClaims.length === 0 &&
|
|
3802
|
+
matchingSessions.length === 1 &&
|
|
3803
|
+
conflictingBoundSessionClaims.length === 0 &&
|
|
3804
|
+
deferredCodexProcessUuid &&
|
|
3805
|
+
deferredCodexProcessBirth &&
|
|
3806
|
+
deferredCodexWorkspace &&
|
|
3807
|
+
(deferredCodexStatusCardSourceEligible ||
|
|
3808
|
+
(deferredCodexCandidateRouteNeeded &&
|
|
3809
|
+
deferredCodexCandidateSourceEligible)) &&
|
|
3719
3810
|
terminalBlockingTurns.length === 0 &&
|
|
3720
3811
|
terminal.orphaned_terminal_dispatch === undefined &&
|
|
3721
3812
|
deferredCodexDispatchSnapshot &&
|
|
@@ -3742,7 +3833,10 @@ function terminalFirstListProjection({ storeDir, terminals, managedSessions, ses
|
|
|
3742
3833
|
processUuid: deferredCodexProcessUuid,
|
|
3743
3834
|
processBirth: deferredCodexProcessBirth,
|
|
3744
3835
|
sourceSession: deferredCodexSource,
|
|
3745
|
-
dispatchSnapshot: deferredCodexDispatchSnapshot
|
|
3836
|
+
dispatchSnapshot: deferredCodexDispatchSnapshot,
|
|
3837
|
+
...(deferredCodexCandidateInventory
|
|
3838
|
+
? { candidateInventory: deferredCodexCandidateInventory }
|
|
3839
|
+
: {})
|
|
3746
3840
|
})
|
|
3747
3841
|
: undefined;
|
|
3748
3842
|
const reconcileBindingAction = mutationsAllowed &&
|
|
@@ -3843,6 +3937,67 @@ function terminalFirstListProjection({ storeDir, terminals, managedSessions, ses
|
|
|
3843
3937
|
// stalls; clear those first, refresh, and only then decide the handoff.
|
|
3844
3938
|
const terminalRecoveryBlockingTurns = terminalBlockingTurns.filter((turn) => !blockingHandoffTurnIds.has(turn.conversation_id) &&
|
|
3845
3939
|
!conversationHasNonterminalDeferredTransfer(turn));
|
|
3940
|
+
let terminalScopedCodexApprovalAction;
|
|
3941
|
+
const terminalScopedCodexApprovalPrompt = terminalScopedCodexApprovalPromptSnapshot(terminal.approval_state);
|
|
3942
|
+
if (mutationsAllowed &&
|
|
3943
|
+
terminal.agent === "codex" &&
|
|
3944
|
+
terminalControl &&
|
|
3945
|
+
hasCanonicalTerminalEndpoint(terminalControl) &&
|
|
3946
|
+
authoritativeSession &&
|
|
3947
|
+
!sessionAuthorityConflict &&
|
|
3948
|
+
!terminalHasNonterminalDeferredTransfer &&
|
|
3949
|
+
unresolvedSessionClaims.length === 0 &&
|
|
3950
|
+
conflictingBoundSessionClaims.length === 0 &&
|
|
3951
|
+
terminalScopedCodexApprovalPrompt &&
|
|
3952
|
+
isRecord(rawActions.approve)) {
|
|
3953
|
+
try {
|
|
3954
|
+
const ledger = loadTerminalBridgeDispatchLedger(terminalControl);
|
|
3955
|
+
const boundary = ownership.state === "conflict" &&
|
|
3956
|
+
discoveredOwnership.state === "current" &&
|
|
3957
|
+
ledger
|
|
3958
|
+
? terminalScopedCodexApprovalBoundaryFromSnapshot({
|
|
3959
|
+
storeDir,
|
|
3960
|
+
terminal,
|
|
3961
|
+
owner: discoveredOwnership.conversation,
|
|
3962
|
+
session: authoritativeSession,
|
|
3963
|
+
ledger,
|
|
3964
|
+
approval: terminalScopedCodexApprovalPrompt
|
|
3965
|
+
})
|
|
3966
|
+
: ownership.state === "none" &&
|
|
3967
|
+
discoveredOwnership.state === "none"
|
|
3968
|
+
? terminalScopedCodexApprovalWithoutDispatchOwnerBoundary({
|
|
3969
|
+
storeDir,
|
|
3970
|
+
terminal,
|
|
3971
|
+
session: authoritativeSession,
|
|
3972
|
+
ledger,
|
|
3973
|
+
approval: terminalScopedCodexApprovalPrompt
|
|
3974
|
+
})
|
|
3975
|
+
: undefined;
|
|
3976
|
+
if (!boundary) {
|
|
3977
|
+
throw new Error("terminal-scoped Codex approval has no eligible managed authority");
|
|
3978
|
+
}
|
|
3979
|
+
terminalScopedCodexApprovalAction = {
|
|
3980
|
+
...rawActions.approve,
|
|
3981
|
+
arguments: {
|
|
3982
|
+
conversation_id: String(terminal.id),
|
|
3983
|
+
expected_terminal_token: boundary.token
|
|
3984
|
+
},
|
|
3985
|
+
scope: "terminal_current_prompt",
|
|
3986
|
+
authority: boundary.authority.kind,
|
|
3987
|
+
managed_state_unchanged: true,
|
|
3988
|
+
automatic_approval_eligible: false,
|
|
3989
|
+
durable_dispatch_receipt: false,
|
|
3990
|
+
uncertain_outcome_recovery: "refresh status and inspect the live prompt; do not retry blindly"
|
|
3991
|
+
};
|
|
3992
|
+
}
|
|
3993
|
+
catch (error) {
|
|
3994
|
+
runtimeLog("info", "terminal_scoped_codex_approval_not_advertised", {
|
|
3995
|
+
terminal_id: String(terminal.id),
|
|
3996
|
+
terminal_target: terminalControl.target,
|
|
3997
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
3998
|
+
});
|
|
3999
|
+
}
|
|
4000
|
+
}
|
|
3846
4001
|
const terminalCanAcceptSend = ownership.state === "none" && isRecord(sessionAwareRawActions.send);
|
|
3847
4002
|
if (ownership.state === "current" &&
|
|
3848
4003
|
!allRelated.some((conversation) => conversation.conversation_id === ownership.conversation.conversation_id)) {
|
|
@@ -3887,6 +4042,9 @@ function terminalFirstListProjection({ storeDir, terminals, managedSessions, ses
|
|
|
3887
4042
|
const currentTurn = currentTurnProjection
|
|
3888
4043
|
? withoutGenericHandoffSourceClose(currentTurnProjection, blockingHandoffTurnIds)
|
|
3889
4044
|
: undefined;
|
|
4045
|
+
const nonOwnerRawActions = authoritativeSession
|
|
4046
|
+
? Object.fromEntries(Object.entries(sessionAwareRawActions).filter(([actionName]) => actionName !== "approve"))
|
|
4047
|
+
: sessionAwareRawActions;
|
|
3890
4048
|
const sortedDisplayed = [...sessionDisplayedRelated]
|
|
3891
4049
|
.filter((conversation) => conversation.conversation_id !== currentTurn?.conversation_id)
|
|
3892
4050
|
.sort(compareManagedConversationRecency);
|
|
@@ -3939,7 +4097,7 @@ function terminalFirstListProjection({ storeDir, terminals, managedSessions, ses
|
|
|
3939
4097
|
: {}),
|
|
3940
4098
|
...(includeAll ? { history } : {})
|
|
3941
4099
|
};
|
|
3942
|
-
const
|
|
4100
|
+
const baseAvailableActions = ownership.state === "current"
|
|
3943
4101
|
? currentTerminalActions(currentTurn)
|
|
3944
4102
|
: ownership.state === "conflict"
|
|
3945
4103
|
? {
|
|
@@ -3977,7 +4135,7 @@ function terminalFirstListProjection({ storeDir, terminals, managedSessions, ses
|
|
|
3977
4135
|
: deferredCodexForegroundToken &&
|
|
3978
4136
|
isRecord(rawActions.send)
|
|
3979
4137
|
? {
|
|
3980
|
-
...
|
|
4138
|
+
...nonOwnerRawActions,
|
|
3981
4139
|
send: {
|
|
3982
4140
|
...rawActions.send,
|
|
3983
4141
|
arguments: {
|
|
@@ -3992,10 +4150,16 @@ function terminalFirstListProjection({ storeDir, terminals, managedSessions, ses
|
|
|
3992
4150
|
sessionBindingMatchesLiveTerminal &&
|
|
3993
4151
|
isRecord(sessionAwareRawActions.send)
|
|
3994
4152
|
? {
|
|
3995
|
-
...
|
|
4153
|
+
...nonOwnerRawActions,
|
|
3996
4154
|
send: sendActionForManagedSession(sessionAwareRawActions.send, managedSessionId)
|
|
3997
4155
|
}
|
|
3998
|
-
:
|
|
4156
|
+
: nonOwnerRawActions;
|
|
4157
|
+
const availableActions = terminalScopedCodexApprovalAction
|
|
4158
|
+
? {
|
|
4159
|
+
...baseAvailableActions,
|
|
4160
|
+
approve: terminalScopedCodexApprovalAction
|
|
4161
|
+
}
|
|
4162
|
+
: baseAvailableActions;
|
|
3999
4163
|
const authoritativeBinding = authoritativeSession?.binding;
|
|
4000
4164
|
const authoritativeIdentity = authoritativeBinding?.native_thread_id
|
|
4001
4165
|
? {
|
|
@@ -4144,6 +4308,24 @@ function managedSessionMatchesLiveTerminalEntry(session, terminal, storeDir) {
|
|
|
4144
4308
|
return false;
|
|
4145
4309
|
}
|
|
4146
4310
|
if (!liveThreadId) {
|
|
4311
|
+
const candidateInventory = isRecord(terminal._codex_open_root_rollout_inventory)
|
|
4312
|
+
? terminal._codex_open_root_rollout_inventory
|
|
4313
|
+
: undefined;
|
|
4314
|
+
const boundRollout = binding.native_process.rollout;
|
|
4315
|
+
const candidateRoot = candidateInventory?.roots.find((root) => root.sessionId.toLowerCase() ===
|
|
4316
|
+
binding.native_thread_id?.toLowerCase());
|
|
4317
|
+
if (session.agent === "codex" &&
|
|
4318
|
+
binding.native_thread_id &&
|
|
4319
|
+
isCompleteNativeRollout(boundRollout) &&
|
|
4320
|
+
candidateRoot &&
|
|
4321
|
+
exactNativeRolloutMatches(candidateRoot.rollout, boundRollout) &&
|
|
4322
|
+
candidateInventory?.pid === binding.native_process.pid &&
|
|
4323
|
+
candidateInventory.processUuid ===
|
|
4324
|
+
binding.native_process.process_uuid &&
|
|
4325
|
+
candidateInventory.processBirth ===
|
|
4326
|
+
binding.native_process.process_birth) {
|
|
4327
|
+
return true;
|
|
4328
|
+
}
|
|
4147
4329
|
return Boolean(session.agent === "codex" &&
|
|
4148
4330
|
binding.native_thread_id &&
|
|
4149
4331
|
isCodexStatusCardEvidence(binding.native_process.evidence) &&
|
|
@@ -4504,118 +4686,624 @@ function localTerminalDispatchOwnership(ledgerOwner, localConversations, termina
|
|
|
4504
4686
|
}
|
|
4505
4687
|
};
|
|
4506
4688
|
}
|
|
4507
|
-
function
|
|
4508
|
-
const
|
|
4509
|
-
|
|
4510
|
-
|
|
4511
|
-
|
|
4512
|
-
|
|
4513
|
-
|
|
4514
|
-
function currentTerminalActions(currentTurn) {
|
|
4515
|
-
if (!currentTurn || !isRecord(currentTurn.available_actions)) {
|
|
4516
|
-
return {};
|
|
4517
|
-
}
|
|
4518
|
-
const actions = {};
|
|
4519
|
-
for (const action of ["status", "respond", "approve", "cancel", "renew", "retry_callback"]) {
|
|
4520
|
-
if (isRecord(currentTurn.available_actions[action])) {
|
|
4521
|
-
actions[action] = currentTurn.available_actions[action];
|
|
4522
|
-
}
|
|
4689
|
+
function terminalScopedCodexApprovalObservation(terminal) {
|
|
4690
|
+
const observation = isRecord(terminal.native_agent_identity_observation)
|
|
4691
|
+
? terminal.native_agent_identity_observation
|
|
4692
|
+
: undefined;
|
|
4693
|
+
const status = stringValue(observation?.status);
|
|
4694
|
+
if (!status || !["resolved", "verified_absent", "unavailable"].includes(status)) {
|
|
4695
|
+
return undefined;
|
|
4523
4696
|
}
|
|
4524
|
-
|
|
4525
|
-
}
|
|
4526
|
-
function safeTerminalActionsDuringConflict(rawActions) {
|
|
4527
|
-
const actions = {};
|
|
4528
|
-
for (const action of ["status", "close"]) {
|
|
4529
|
-
if (isRecord(rawActions[action])) {
|
|
4530
|
-
actions[action] = rawActions[action];
|
|
4531
|
-
}
|
|
4697
|
+
if (status !== "resolved") {
|
|
4698
|
+
return { status };
|
|
4532
4699
|
}
|
|
4533
|
-
|
|
4534
|
-
|
|
4535
|
-
|
|
4536
|
-
const { selector: _selector, ...existingArguments } = isRecord(action.arguments)
|
|
4537
|
-
? action.arguments
|
|
4538
|
-
: {};
|
|
4700
|
+
const rollout = isRecord(terminal.native_agent_rollout)
|
|
4701
|
+
? terminal.native_agent_rollout
|
|
4702
|
+
: undefined;
|
|
4539
4703
|
return {
|
|
4540
|
-
|
|
4541
|
-
|
|
4542
|
-
|
|
4543
|
-
|
|
4544
|
-
|
|
4704
|
+
status,
|
|
4705
|
+
session_id: stringValue(terminal.native_agent_session_id) ?? null,
|
|
4706
|
+
process_uuid: stringValue(terminal.native_agent_process_uuid) ?? null,
|
|
4707
|
+
process_birth: stringValue(terminal.native_agent_process_birth) ?? null,
|
|
4708
|
+
rollout: isCompleteNativeRollout(rollout)
|
|
4709
|
+
? {
|
|
4710
|
+
fd: rollout.fd,
|
|
4711
|
+
device: rollout.device,
|
|
4712
|
+
inode: rollout.inode,
|
|
4713
|
+
path: rollout.path
|
|
4714
|
+
}
|
|
4715
|
+
: null
|
|
4545
4716
|
};
|
|
4546
4717
|
}
|
|
4547
|
-
function
|
|
4548
|
-
|
|
4549
|
-
|
|
4550
|
-
for (const actionName of ["new_thread", "resume_thread", "native_inspect"]) {
|
|
4551
|
-
const action = isRecord(next[actionName]) ? next[actionName] : undefined;
|
|
4552
|
-
if (!action) {
|
|
4553
|
-
continue;
|
|
4554
|
-
}
|
|
4555
|
-
next[actionName] = {
|
|
4556
|
-
...action,
|
|
4557
|
-
arguments: {
|
|
4558
|
-
...(isRecord(action.arguments) ? action.arguments : {}),
|
|
4559
|
-
expected_binding_token: token
|
|
4560
|
-
}
|
|
4561
|
-
};
|
|
4718
|
+
function terminalScopedCodexApprovalPromptSnapshot(approvalState) {
|
|
4719
|
+
if (!isRecord(approvalState) || approvalState.approvable !== true) {
|
|
4720
|
+
return undefined;
|
|
4562
4721
|
}
|
|
4563
|
-
|
|
4564
|
-
|
|
4565
|
-
|
|
4566
|
-
|
|
4567
|
-
|
|
4568
|
-
|
|
4569
|
-
|
|
4570
|
-
|
|
4722
|
+
const fingerprint = stringValue(approvalState.fingerprint);
|
|
4723
|
+
const rawKeys = Array.isArray(approvalState.keys)
|
|
4724
|
+
? approvalState.keys
|
|
4725
|
+
: stringValue(approvalState.key)
|
|
4726
|
+
? [approvalState.key]
|
|
4727
|
+
: [];
|
|
4728
|
+
const keys = rawKeys.filter((value) => typeof value === "string" && value.length > 0);
|
|
4729
|
+
if (!fingerprint ||
|
|
4730
|
+
!/^[0-9a-f]{64}$/u.test(fingerprint) ||
|
|
4731
|
+
keys.length === 0 ||
|
|
4732
|
+
keys.length !== rawKeys.length ||
|
|
4733
|
+
stringValue(approvalState.decision_mode) !== "keys") {
|
|
4734
|
+
return undefined;
|
|
4571
4735
|
}
|
|
4572
|
-
|
|
4573
|
-
}
|
|
4574
|
-
function readOnlyListActions(actionsValue) {
|
|
4575
|
-
return isRecord(actionsValue.status)
|
|
4576
|
-
? { status: actionsValue.status }
|
|
4577
|
-
: {};
|
|
4578
|
-
}
|
|
4579
|
-
function readOnlyManagedTurn(managedTurn) {
|
|
4736
|
+
const requestId = stringValue(approvalState.request_id) ?? null;
|
|
4580
4737
|
return {
|
|
4581
|
-
|
|
4582
|
-
|
|
4583
|
-
|
|
4584
|
-
|
|
4738
|
+
fingerprint,
|
|
4739
|
+
keys,
|
|
4740
|
+
requestId,
|
|
4741
|
+
digest: terminalActionFingerprint({
|
|
4742
|
+
version: 1,
|
|
4743
|
+
kind: "terminal_scoped_codex_approval_prompt",
|
|
4744
|
+
fingerprint,
|
|
4745
|
+
keys,
|
|
4746
|
+
request_id: requestId
|
|
4747
|
+
})
|
|
4585
4748
|
};
|
|
4586
4749
|
}
|
|
4587
|
-
function
|
|
4588
|
-
const
|
|
4589
|
-
|
|
4590
|
-
|
|
4591
|
-
|
|
4592
|
-
const
|
|
4593
|
-
|
|
4594
|
-
|
|
4595
|
-
|
|
4596
|
-
|
|
4597
|
-
|
|
4598
|
-
|
|
4599
|
-
|
|
4750
|
+
function terminalScopedCodexApprovalToken({ storeDir, terminal, authority, approval }) {
|
|
4751
|
+
const terminalControl = terminal.terminal_control;
|
|
4752
|
+
const workspace = stringValue(terminal.workspace ?? terminal.cwd);
|
|
4753
|
+
const processUuid = stringValue(terminal.native_agent_process_uuid);
|
|
4754
|
+
const processBirth = stringValue(terminal.native_agent_process_birth);
|
|
4755
|
+
const terminalToken = unmanagedTerminalBindingToken({
|
|
4756
|
+
terminalId: String(terminal.id),
|
|
4757
|
+
terminalControl,
|
|
4758
|
+
agent: "codex",
|
|
4759
|
+
pid: Number(terminal.pid),
|
|
4760
|
+
workspace,
|
|
4761
|
+
processUuid,
|
|
4762
|
+
processBirth
|
|
4763
|
+
});
|
|
4764
|
+
const session = authority.session;
|
|
4765
|
+
const owner = authority.kind === "current_dispatch_owner"
|
|
4766
|
+
? authority.owner
|
|
4767
|
+
: undefined;
|
|
4768
|
+
const takeover = owner && isRecord(owner.native_session_takeover)
|
|
4769
|
+
? owner.native_session_takeover
|
|
4770
|
+
: undefined;
|
|
4771
|
+
const ledger = authority.ledger;
|
|
4772
|
+
return createHash("sha256")
|
|
4773
|
+
.update(JSON.stringify({
|
|
4774
|
+
version: 2,
|
|
4775
|
+
kind: "terminal_scoped_codex_manual_approval",
|
|
4776
|
+
store_dir: path.resolve(storeDir),
|
|
4777
|
+
terminal_token: terminalToken,
|
|
4778
|
+
observation: terminalScopedCodexApprovalObservation(terminal),
|
|
4779
|
+
authority: authority.kind,
|
|
4780
|
+
owner_session_id: session.session_id,
|
|
4781
|
+
owner_session_revision: managedSessionRevision(session),
|
|
4782
|
+
owner_binding_token: managedSessionBindingToken(session),
|
|
4783
|
+
owner_turn_id: owner ? turnIdForConversation(owner) : null,
|
|
4784
|
+
owner_turn_status: owner?.status ?? null,
|
|
4785
|
+
owner_turn_updated_at: owner?.updated_at ?? null,
|
|
4786
|
+
owner_message_id: stringValue(takeover?.terminal_bridge_message_id) ?? null,
|
|
4787
|
+
dispatch_snapshot: ledger
|
|
4788
|
+
? {
|
|
4789
|
+
status: stringValue(ledger.status),
|
|
4790
|
+
fingerprint: terminalActionFingerprint(ledger)
|
|
4791
|
+
}
|
|
4792
|
+
: { status: "none", fingerprint: null },
|
|
4793
|
+
approval_snapshot_digest: approval.digest,
|
|
4794
|
+
approval_fingerprint: approval.fingerprint,
|
|
4795
|
+
approval_keys: approval.keys,
|
|
4796
|
+
approval_request_id: approval.requestId
|
|
4797
|
+
}))
|
|
4798
|
+
.digest("hex");
|
|
4600
4799
|
}
|
|
4601
|
-
function
|
|
4602
|
-
const
|
|
4603
|
-
|
|
4604
|
-
|
|
4605
|
-
|
|
4800
|
+
function terminalScopedCodexApprovalBoundaryFromSnapshot({ storeDir, terminal, owner, session, ledger, approval }) {
|
|
4801
|
+
const terminalControl = isRecord(terminal.terminal_control)
|
|
4802
|
+
? terminal.terminal_control
|
|
4803
|
+
: undefined;
|
|
4804
|
+
const binding = session.binding;
|
|
4805
|
+
const workspace = stringValue(terminal.workspace ?? terminal.cwd);
|
|
4806
|
+
const processUuid = stringValue(terminal.native_agent_process_uuid);
|
|
4807
|
+
const processBirth = stringValue(terminal.native_agent_process_birth);
|
|
4808
|
+
const observation = terminalScopedCodexApprovalObservation(terminal);
|
|
4809
|
+
if (terminal.agent !== "codex" ||
|
|
4810
|
+
!terminalControl ||
|
|
4811
|
+
!hasCanonicalTerminalEndpoint(terminalControl) ||
|
|
4812
|
+
!Number.isSafeInteger(Number(terminal.pid)) ||
|
|
4813
|
+
Number(terminal.pid) <= 1 ||
|
|
4814
|
+
!workspace ||
|
|
4815
|
+
!processUuid ||
|
|
4816
|
+
!processBirth ||
|
|
4817
|
+
!observation ||
|
|
4818
|
+
!approval) {
|
|
4819
|
+
throw new Error("terminal-scoped Codex approval requires an exact terminal and process incarnation");
|
|
4820
|
+
}
|
|
4821
|
+
if (session.status !== "bound" ||
|
|
4822
|
+
session.agent !== "codex" ||
|
|
4823
|
+
!binding ||
|
|
4824
|
+
binding.native_process.pid !== Number(terminal.pid) ||
|
|
4825
|
+
binding.native_process.process_uuid !== processUuid ||
|
|
4826
|
+
binding.native_process.process_birth !== processBirth ||
|
|
4827
|
+
!terminalControlAliasMatches(binding.terminal_id, binding.terminal_control, terminal.id, terminalControl) ||
|
|
4828
|
+
!terminalControlsShareIncarnation(binding.terminal_control, terminalControl) ||
|
|
4829
|
+
path.resolve(session.workspace) !== path.resolve(workspace) ||
|
|
4830
|
+
sessionIdForConversation(owner) !== session.session_id ||
|
|
4831
|
+
!["waiting_for_agent", "waiting_for_openclaw"].includes(owner.status)) {
|
|
4832
|
+
throw new Error("terminal-scoped Codex approval has no single current managed owner");
|
|
4833
|
+
}
|
|
4834
|
+
const relatedBoundSessions = listManagedSessions(storeDir).filter((candidate) => candidate.status === "bound" &&
|
|
4835
|
+
candidate.agent === "codex" &&
|
|
4836
|
+
candidate.binding?.native_process.pid === Number(terminal.pid) &&
|
|
4837
|
+
terminalControlsShareIncarnation(candidate.binding?.terminal_control, terminalControl));
|
|
4838
|
+
if (relatedBoundSessions.length !== 1 ||
|
|
4839
|
+
relatedBoundSessions[0].session_id !== session.session_id) {
|
|
4840
|
+
throw new Error("terminal-scoped Codex approval has ambiguous managed Session ownership");
|
|
4841
|
+
}
|
|
4842
|
+
const blockingTurns = terminalIncarnationBlockingTurns(storeDir, terminalControl);
|
|
4843
|
+
if (blockingTurns.length !== 1 ||
|
|
4844
|
+
turnIdForConversation(blockingTurns[0]) !== turnIdForConversation(owner)) {
|
|
4845
|
+
throw new Error("terminal-scoped Codex approval has collateral unresolved Turn state");
|
|
4846
|
+
}
|
|
4847
|
+
if (managedSessionHasAnyNativeTransition(storeDir, session) ||
|
|
4848
|
+
listDeferredForegroundTransfers(storeDir).some((transfer) => !["resolved", "abort_resolved"].includes(transfer.status) &&
|
|
4849
|
+
(transfer.source_session_id === session.session_id ||
|
|
4850
|
+
transfer.target_session_id === session.session_id ||
|
|
4851
|
+
(transfer.terminal_id === String(terminal.id) &&
|
|
4852
|
+
terminalControlEvidenceMatches(transfer.terminal_endpoint, terminalControl))))) {
|
|
4853
|
+
throw new Error("terminal-scoped Codex approval is blocked by managed recovery state");
|
|
4854
|
+
}
|
|
4855
|
+
assertManagedTerminalDispatchOwner({
|
|
4856
|
+
storeDir,
|
|
4857
|
+
conversation: owner,
|
|
4858
|
+
terminalControl,
|
|
4859
|
+
action: "approve"
|
|
4606
4860
|
});
|
|
4607
|
-
|
|
4608
|
-
|
|
4609
|
-
|
|
4610
|
-
|
|
4861
|
+
if (!terminalDispatchRecordMatchesControl(ledger, terminalControl) ||
|
|
4862
|
+
!["submitted", "agent_accepted"].includes(String(ledger.status))) {
|
|
4863
|
+
throw new Error(`terminal-scoped Codex approval cannot use ${String(ledger.status)} dispatch ownership`);
|
|
4864
|
+
}
|
|
4865
|
+
const observedSessionId = stringValue(terminal.native_agent_session_id);
|
|
4866
|
+
const statusCardSessionId = stringValue(terminal.native_agent_status_card_session_id);
|
|
4867
|
+
for (const knownSessionId of [observedSessionId, statusCardSessionId]) {
|
|
4868
|
+
if (knownSessionId &&
|
|
4869
|
+
knownSessionId.toLowerCase() !==
|
|
4870
|
+
String(binding.native_thread_id ?? "").toLowerCase()) {
|
|
4871
|
+
throw new Error("terminal-scoped Codex approval observed a different native thread");
|
|
4872
|
+
}
|
|
4873
|
+
}
|
|
4874
|
+
if (observedSessionId &&
|
|
4875
|
+
(stringValue(terminal.native_agent_process_uuid) !==
|
|
4876
|
+
binding.native_process.process_uuid ||
|
|
4877
|
+
stringValue(terminal.native_agent_process_birth) !==
|
|
4878
|
+
binding.native_process.process_birth ||
|
|
4879
|
+
(binding.native_process.rollout &&
|
|
4880
|
+
!exactNativeRolloutMatches(binding.native_process.rollout, isRecord(terminal.native_agent_rollout)
|
|
4881
|
+
? terminal.native_agent_rollout
|
|
4882
|
+
: undefined)))) {
|
|
4883
|
+
throw new Error("terminal-scoped Codex approval observed a changed native rollout incarnation");
|
|
4884
|
+
}
|
|
4885
|
+
const currentIdentity = observedSessionId
|
|
4886
|
+
? {
|
|
4887
|
+
sessionId: observedSessionId,
|
|
4888
|
+
processUuid,
|
|
4889
|
+
processBirth,
|
|
4890
|
+
rollout: isRecord(terminal.native_agent_rollout)
|
|
4891
|
+
? terminal.native_agent_rollout
|
|
4892
|
+
: undefined,
|
|
4893
|
+
evidence: "terminal_scoped_approval"
|
|
4894
|
+
}
|
|
4895
|
+
: undefined;
|
|
4896
|
+
if (nativeAgentIdentityMatchesTurn(owner, currentIdentity)) {
|
|
4897
|
+
throw new Error("the Codex managed identity is exact; use the managed Turn approval action");
|
|
4898
|
+
}
|
|
4611
4899
|
return {
|
|
4612
|
-
|
|
4613
|
-
|
|
4900
|
+
authority: {
|
|
4901
|
+
kind: "current_dispatch_owner",
|
|
4902
|
+
owner,
|
|
4903
|
+
session,
|
|
4904
|
+
ledger
|
|
4905
|
+
},
|
|
4906
|
+
approval,
|
|
4907
|
+
token: terminalScopedCodexApprovalToken({
|
|
4908
|
+
storeDir,
|
|
4909
|
+
terminal,
|
|
4910
|
+
authority: {
|
|
4911
|
+
kind: "current_dispatch_owner",
|
|
4912
|
+
owner,
|
|
4913
|
+
session,
|
|
4914
|
+
ledger
|
|
4915
|
+
},
|
|
4916
|
+
approval
|
|
4917
|
+
})
|
|
4614
4918
|
};
|
|
4615
4919
|
}
|
|
4616
|
-
function
|
|
4617
|
-
const
|
|
4618
|
-
?
|
|
4920
|
+
function terminalScopedCodexApprovalWithoutDispatchOwnerBoundary({ storeDir, terminal, session, ledger, approval }) {
|
|
4921
|
+
const terminalControl = isRecord(terminal.terminal_control)
|
|
4922
|
+
? terminal.terminal_control
|
|
4923
|
+
: undefined;
|
|
4924
|
+
const binding = session.binding;
|
|
4925
|
+
const workspace = stringValue(terminal.workspace ?? terminal.cwd);
|
|
4926
|
+
const processUuid = stringValue(terminal.native_agent_process_uuid);
|
|
4927
|
+
const processBirth = stringValue(terminal.native_agent_process_birth);
|
|
4928
|
+
const observation = terminalScopedCodexApprovalObservation(terminal);
|
|
4929
|
+
if (terminal.agent !== "codex" ||
|
|
4930
|
+
!terminalControl ||
|
|
4931
|
+
!hasCanonicalTerminalEndpoint(terminalControl) ||
|
|
4932
|
+
!Number.isSafeInteger(Number(terminal.pid)) ||
|
|
4933
|
+
Number(terminal.pid) <= 1 ||
|
|
4934
|
+
!workspace ||
|
|
4935
|
+
!processUuid ||
|
|
4936
|
+
!processBirth ||
|
|
4937
|
+
!observation ||
|
|
4938
|
+
!approval) {
|
|
4939
|
+
throw new Error("terminal-scoped Codex approval requires an exact terminal, process, and prompt incarnation");
|
|
4940
|
+
}
|
|
4941
|
+
if (session.status !== "bound" ||
|
|
4942
|
+
session.agent !== "codex" ||
|
|
4943
|
+
!binding ||
|
|
4944
|
+
!isExactNativeThreadId(binding.native_thread_id) ||
|
|
4945
|
+
binding.native_process.pid !== Number(terminal.pid) ||
|
|
4946
|
+
binding.native_process.process_uuid !== processUuid ||
|
|
4947
|
+
binding.native_process.process_birth !== processBirth ||
|
|
4948
|
+
!terminalControlAliasMatches(binding.terminal_id, binding.terminal_control, terminal.id, terminalControl) ||
|
|
4949
|
+
!terminalControlsShareIncarnation(binding.terminal_control, terminalControl) ||
|
|
4950
|
+
path.resolve(session.workspace) !== path.resolve(workspace)) {
|
|
4951
|
+
throw new Error("terminal-scoped Codex approval has no single exact managed Session");
|
|
4952
|
+
}
|
|
4953
|
+
const relatedBoundSessions = listManagedSessions(storeDir).filter((candidate) => candidate.status === "bound" &&
|
|
4954
|
+
candidate.agent === "codex" &&
|
|
4955
|
+
candidate.binding?.native_process.pid === Number(terminal.pid) &&
|
|
4956
|
+
terminalControlsShareIncarnation(candidate.binding?.terminal_control, terminalControl));
|
|
4957
|
+
if (relatedBoundSessions.length !== 1 ||
|
|
4958
|
+
relatedBoundSessions[0].session_id !== session.session_id) {
|
|
4959
|
+
throw new Error("terminal-scoped Codex approval has ambiguous managed Session ownership");
|
|
4960
|
+
}
|
|
4961
|
+
if (terminalDispatchOwnership(terminalControl).state !== "none") {
|
|
4962
|
+
throw new Error("terminal-scoped Codex approval acquired a current or conflicted dispatch owner");
|
|
4963
|
+
}
|
|
4964
|
+
if (ledger &&
|
|
4965
|
+
(ledger.status !== "resolved" ||
|
|
4966
|
+
!terminalDispatchRecordMatchesControl(ledger, terminalControl))) {
|
|
4967
|
+
throw new Error(`terminal-scoped Codex approval cannot use ${String(ledger.status)} dispatch ownership`);
|
|
4968
|
+
}
|
|
4969
|
+
if (terminalIncarnationBlockingTurns(storeDir, terminalControl).length > 0) {
|
|
4970
|
+
throw new Error("terminal-scoped Codex approval has unresolved managed Turn state");
|
|
4971
|
+
}
|
|
4972
|
+
if (managedSessionHasAnyNativeTransition(storeDir, session) ||
|
|
4973
|
+
orphanedTerminalDispatchForRecovery(terminalControl) ||
|
|
4974
|
+
listDeferredForegroundTransfers(storeDir).some((transfer) => !["resolved", "abort_resolved"].includes(transfer.status) &&
|
|
4975
|
+
(transfer.source_session_id === session.session_id ||
|
|
4976
|
+
transfer.target_session_id === session.session_id ||
|
|
4977
|
+
(transfer.terminal_id === String(terminal.id) &&
|
|
4978
|
+
terminalControlEvidenceMatches(transfer.terminal_endpoint, terminalControl))))) {
|
|
4979
|
+
throw new Error("terminal-scoped Codex approval is blocked by managed recovery state");
|
|
4980
|
+
}
|
|
4981
|
+
const observedSessionId = stringValue(terminal.native_agent_session_id);
|
|
4982
|
+
const statusCardSessionId = stringValue(terminal.native_agent_status_card_session_id);
|
|
4983
|
+
for (const knownSessionId of [observedSessionId, statusCardSessionId]) {
|
|
4984
|
+
if (knownSessionId &&
|
|
4985
|
+
knownSessionId.toLowerCase() !== binding.native_thread_id.toLowerCase()) {
|
|
4986
|
+
throw new Error("terminal-scoped Codex approval observed a different native thread");
|
|
4987
|
+
}
|
|
4988
|
+
}
|
|
4989
|
+
if (observedSessionId &&
|
|
4990
|
+
(stringValue(terminal.native_agent_process_uuid) !==
|
|
4991
|
+
binding.native_process.process_uuid ||
|
|
4992
|
+
stringValue(terminal.native_agent_process_birth) !==
|
|
4993
|
+
binding.native_process.process_birth ||
|
|
4994
|
+
(binding.native_process.rollout &&
|
|
4995
|
+
!exactNativeRolloutMatches(binding.native_process.rollout, isRecord(terminal.native_agent_rollout)
|
|
4996
|
+
? terminal.native_agent_rollout
|
|
4997
|
+
: undefined)))) {
|
|
4998
|
+
throw new Error("terminal-scoped Codex approval observed a changed native rollout incarnation");
|
|
4999
|
+
}
|
|
5000
|
+
const authority = {
|
|
5001
|
+
kind: "managed_session_no_dispatch_owner",
|
|
5002
|
+
session,
|
|
5003
|
+
...(ledger ? { ledger } : {})
|
|
5004
|
+
};
|
|
5005
|
+
return {
|
|
5006
|
+
authority,
|
|
5007
|
+
approval,
|
|
5008
|
+
token: terminalScopedCodexApprovalToken({
|
|
5009
|
+
storeDir,
|
|
5010
|
+
terminal,
|
|
5011
|
+
authority,
|
|
5012
|
+
approval
|
|
5013
|
+
})
|
|
5014
|
+
};
|
|
5015
|
+
}
|
|
5016
|
+
async function resolveTerminalScopedCodexApproval({ options, terminal, approvalSnapshot }) {
|
|
5017
|
+
if (terminal.agent !== "codex") {
|
|
5018
|
+
return { state: "blocked", reason: "raw Claude approval remains unsupported" };
|
|
5019
|
+
}
|
|
5020
|
+
const storeDir = path.resolve(storeDirFromOptions(options));
|
|
5021
|
+
let ledger;
|
|
5022
|
+
try {
|
|
5023
|
+
ledger = loadTerminalBridgeDispatchLedger(terminal.terminalControl);
|
|
5024
|
+
}
|
|
5025
|
+
catch (error) {
|
|
5026
|
+
return {
|
|
5027
|
+
state: "blocked",
|
|
5028
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
5029
|
+
};
|
|
5030
|
+
}
|
|
5031
|
+
const relatedSessions = listManagedSessions(storeDir).filter((session) => session.binding?.native_process.pid === terminal.pid &&
|
|
5032
|
+
terminalControlsShareIncarnation(session.binding?.terminal_control, terminal.terminalControl));
|
|
5033
|
+
const relatedTurns = listConversations(storeDir).filter((turn) => terminalControlsShareIncarnation(terminalControlForManagedConversation(turn), terminal.terminalControl));
|
|
5034
|
+
if (ledger?.status === "uncertain") {
|
|
5035
|
+
return {
|
|
5036
|
+
state: "blocked",
|
|
5037
|
+
reason: "terminal dispatch is uncertain; inspect the pane and do not retry approval"
|
|
5038
|
+
};
|
|
5039
|
+
}
|
|
5040
|
+
const ownership = terminalDispatchOwnership(terminal.terminalControl);
|
|
5041
|
+
if (ownership.state === "conflict") {
|
|
5042
|
+
return {
|
|
5043
|
+
state: "blocked",
|
|
5044
|
+
reason: stringValue(ownership.conflict.reason) ??
|
|
5045
|
+
"terminal dispatch ownership is conflicted"
|
|
5046
|
+
};
|
|
5047
|
+
}
|
|
5048
|
+
if (ownership.state === "none" &&
|
|
5049
|
+
relatedSessions.length === 0 &&
|
|
5050
|
+
relatedTurns.length === 0) {
|
|
5051
|
+
return { state: "unmanaged" };
|
|
5052
|
+
}
|
|
5053
|
+
const owner = ownership.state === "current"
|
|
5054
|
+
? ownership.conversation
|
|
5055
|
+
: undefined;
|
|
5056
|
+
if (owner) {
|
|
5057
|
+
const ownerStoreDir = managedSessionStoreDirForConversation(owner);
|
|
5058
|
+
if (!ownerStoreDir || path.resolve(ownerStoreDir) !== storeDir) {
|
|
5059
|
+
return {
|
|
5060
|
+
state: "blocked",
|
|
5061
|
+
reason: "terminal dispatch owner belongs to another AKK Store"
|
|
5062
|
+
};
|
|
5063
|
+
}
|
|
5064
|
+
}
|
|
5065
|
+
const ownerSession = owner
|
|
5066
|
+
? tryLoadManagedSession(storeDir, sessionIdForConversation(owner))
|
|
5067
|
+
: undefined;
|
|
5068
|
+
if (owner && !ownerSession) {
|
|
5069
|
+
return {
|
|
5070
|
+
state: "blocked",
|
|
5071
|
+
reason: "terminal dispatch owner has no current managed Session"
|
|
5072
|
+
};
|
|
5073
|
+
}
|
|
5074
|
+
let identityContext;
|
|
5075
|
+
try {
|
|
5076
|
+
identityContext = codexManagedIdentityResolutionContext({
|
|
5077
|
+
storeDir,
|
|
5078
|
+
terminal
|
|
5079
|
+
});
|
|
5080
|
+
}
|
|
5081
|
+
catch {
|
|
5082
|
+
// The terminal-scoped path deliberately tolerates an unavailable Codex
|
|
5083
|
+
// rollout resolver. Store, pane, process, owner, and prompt fences below
|
|
5084
|
+
// remain mandatory.
|
|
5085
|
+
}
|
|
5086
|
+
const observation = await observeCurrentNativeAgentSessionIdentity({
|
|
5087
|
+
options,
|
|
5088
|
+
agent: "codex",
|
|
5089
|
+
pid: terminal.pid,
|
|
5090
|
+
cwd: terminal.terminalControl.currentPath,
|
|
5091
|
+
preferredSessionId: identityContext?.preferredSessionId,
|
|
5092
|
+
allowedCompanionIdentity: identityContext?.companions.primary,
|
|
5093
|
+
allowedAdditionalIdentities: identityContext?.companions.additional
|
|
5094
|
+
});
|
|
5095
|
+
let processIncarnation;
|
|
5096
|
+
try {
|
|
5097
|
+
processIncarnation = codexProcessIncarnationForPid(terminal.pid);
|
|
5098
|
+
}
|
|
5099
|
+
catch (error) {
|
|
5100
|
+
return {
|
|
5101
|
+
state: "blocked",
|
|
5102
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
5103
|
+
};
|
|
5104
|
+
}
|
|
5105
|
+
const identity = observation.status === "resolved"
|
|
5106
|
+
? observation.identity
|
|
5107
|
+
: undefined;
|
|
5108
|
+
let approval = approvalSnapshot;
|
|
5109
|
+
if (!approval) {
|
|
5110
|
+
try {
|
|
5111
|
+
const terminalStatus = await terminalStatusForControl("codex", terminal.terminalControl, options, {
|
|
5112
|
+
pid: terminal.pid,
|
|
5113
|
+
cwd: terminal.terminalControl.currentPath,
|
|
5114
|
+
conversationId: terminal.conversationId,
|
|
5115
|
+
terminalTarget: terminal.terminalControl.target
|
|
5116
|
+
});
|
|
5117
|
+
approval = terminalScopedCodexApprovalPromptSnapshot(terminalStatus.approval_state);
|
|
5118
|
+
}
|
|
5119
|
+
catch (error) {
|
|
5120
|
+
return {
|
|
5121
|
+
state: "blocked",
|
|
5122
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
5123
|
+
};
|
|
5124
|
+
}
|
|
5125
|
+
}
|
|
5126
|
+
if (!approval) {
|
|
5127
|
+
return {
|
|
5128
|
+
state: "blocked",
|
|
5129
|
+
reason: "terminal-scoped Codex approval requires one fresh exact approval prompt"
|
|
5130
|
+
};
|
|
5131
|
+
}
|
|
5132
|
+
const terminalSnapshot = {
|
|
5133
|
+
id: terminal.conversationId,
|
|
5134
|
+
source: "terminal",
|
|
5135
|
+
agent: "codex",
|
|
5136
|
+
pid: terminal.pid,
|
|
5137
|
+
cwd: terminal.terminalControl.currentPath,
|
|
5138
|
+
workspace: terminal.terminalControl.currentPath,
|
|
5139
|
+
terminal_control: terminal.terminalControl,
|
|
5140
|
+
native_agent_session_id: identity?.sessionId,
|
|
5141
|
+
native_agent_process_uuid: identity?.processUuid ?? processIncarnation.processUuid,
|
|
5142
|
+
native_agent_process_birth: identity?.processBirth ?? processIncarnation.processBirth,
|
|
5143
|
+
native_agent_rollout: identity?.rollout,
|
|
5144
|
+
native_agent_identity_observation: { status: observation.status },
|
|
5145
|
+
approval_state: {
|
|
5146
|
+
approvable: true,
|
|
5147
|
+
fingerprint: approval.fingerprint,
|
|
5148
|
+
keys: approval.keys,
|
|
5149
|
+
decision_mode: "keys",
|
|
5150
|
+
request_id: approval.requestId
|
|
5151
|
+
}
|
|
5152
|
+
};
|
|
5153
|
+
try {
|
|
5154
|
+
if (owner && ownerSession && ledger) {
|
|
5155
|
+
return {
|
|
5156
|
+
state: "eligible",
|
|
5157
|
+
boundary: terminalScopedCodexApprovalBoundaryFromSnapshot({
|
|
5158
|
+
storeDir,
|
|
5159
|
+
terminal: terminalSnapshot,
|
|
5160
|
+
owner,
|
|
5161
|
+
session: ownerSession,
|
|
5162
|
+
ledger,
|
|
5163
|
+
approval
|
|
5164
|
+
})
|
|
5165
|
+
};
|
|
5166
|
+
}
|
|
5167
|
+
if (ownership.state !== "none") {
|
|
5168
|
+
throw new Error("managed terminal approval has no current exact dispatch owner");
|
|
5169
|
+
}
|
|
5170
|
+
if (ledger && ledger.status !== "resolved") {
|
|
5171
|
+
throw new Error(`terminal-scoped Codex approval cannot use ${String(ledger.status)} dispatch ownership`);
|
|
5172
|
+
}
|
|
5173
|
+
const matchingSessions = relatedSessions.filter((session) => managedSessionMatchesLiveTerminalEntry(session, terminalSnapshot, storeDir));
|
|
5174
|
+
if (matchingSessions.length !== 1) {
|
|
5175
|
+
throw new Error("terminal-scoped Codex approval has no single exact managed Session");
|
|
5176
|
+
}
|
|
5177
|
+
return {
|
|
5178
|
+
state: "eligible",
|
|
5179
|
+
boundary: terminalScopedCodexApprovalWithoutDispatchOwnerBoundary({
|
|
5180
|
+
storeDir,
|
|
5181
|
+
terminal: terminalSnapshot,
|
|
5182
|
+
session: matchingSessions[0],
|
|
5183
|
+
ledger,
|
|
5184
|
+
approval
|
|
5185
|
+
})
|
|
5186
|
+
};
|
|
5187
|
+
}
|
|
5188
|
+
catch (error) {
|
|
5189
|
+
return {
|
|
5190
|
+
state: "blocked",
|
|
5191
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
5192
|
+
};
|
|
5193
|
+
}
|
|
5194
|
+
}
|
|
5195
|
+
function sameCanonicalStatePath(left, right) {
|
|
5196
|
+
const leftPath = stringValue(left);
|
|
5197
|
+
const rightPath = stringValue(right);
|
|
5198
|
+
return Boolean(leftPath &&
|
|
5199
|
+
rightPath &&
|
|
5200
|
+
path.resolve(leftPath) === path.resolve(rightPath));
|
|
5201
|
+
}
|
|
5202
|
+
function currentTerminalActions(currentTurn) {
|
|
5203
|
+
if (!currentTurn || !isRecord(currentTurn.available_actions)) {
|
|
5204
|
+
return {};
|
|
5205
|
+
}
|
|
5206
|
+
const actions = {};
|
|
5207
|
+
for (const action of ["status", "respond", "approve", "cancel", "renew", "retry_callback"]) {
|
|
5208
|
+
if (isRecord(currentTurn.available_actions[action])) {
|
|
5209
|
+
actions[action] = currentTurn.available_actions[action];
|
|
5210
|
+
}
|
|
5211
|
+
}
|
|
5212
|
+
return actions;
|
|
5213
|
+
}
|
|
5214
|
+
function safeTerminalActionsDuringConflict(rawActions) {
|
|
5215
|
+
const actions = {};
|
|
5216
|
+
for (const action of ["status", "close"]) {
|
|
5217
|
+
if (isRecord(rawActions[action])) {
|
|
5218
|
+
actions[action] = rawActions[action];
|
|
5219
|
+
}
|
|
5220
|
+
}
|
|
5221
|
+
return actions;
|
|
5222
|
+
}
|
|
5223
|
+
function sendActionForManagedSession(action, sessionId) {
|
|
5224
|
+
const { selector: _selector, ...existingArguments } = isRecord(action.arguments)
|
|
5225
|
+
? action.arguments
|
|
5226
|
+
: {};
|
|
5227
|
+
return {
|
|
5228
|
+
...action,
|
|
5229
|
+
arguments: {
|
|
5230
|
+
...existingArguments,
|
|
5231
|
+
session_id: sessionId
|
|
5232
|
+
}
|
|
5233
|
+
};
|
|
5234
|
+
}
|
|
5235
|
+
function actionsForManagedSessionBinding(actions, session) {
|
|
5236
|
+
const token = managedSessionBindingToken(session);
|
|
5237
|
+
const next = { ...actions };
|
|
5238
|
+
for (const actionName of ["new_thread", "resume_thread", "native_inspect"]) {
|
|
5239
|
+
const action = isRecord(next[actionName]) ? next[actionName] : undefined;
|
|
5240
|
+
if (!action) {
|
|
5241
|
+
continue;
|
|
5242
|
+
}
|
|
5243
|
+
next[actionName] = {
|
|
5244
|
+
...action,
|
|
5245
|
+
arguments: {
|
|
5246
|
+
...(isRecord(action.arguments) ? action.arguments : {}),
|
|
5247
|
+
expected_binding_token: token
|
|
5248
|
+
}
|
|
5249
|
+
};
|
|
5250
|
+
}
|
|
5251
|
+
return next;
|
|
5252
|
+
}
|
|
5253
|
+
function safeUnavailableManagedTurnActions(actionsValue) {
|
|
5254
|
+
const actions = {};
|
|
5255
|
+
for (const action of ["status", "retry_callback", "close"]) {
|
|
5256
|
+
if (isRecord(actionsValue[action])) {
|
|
5257
|
+
actions[action] = actionsValue[action];
|
|
5258
|
+
}
|
|
5259
|
+
}
|
|
5260
|
+
return actions;
|
|
5261
|
+
}
|
|
5262
|
+
function readOnlyListActions(actionsValue) {
|
|
5263
|
+
return isRecord(actionsValue.status)
|
|
5264
|
+
? { status: actionsValue.status }
|
|
5265
|
+
: {};
|
|
5266
|
+
}
|
|
5267
|
+
function readOnlyManagedTurn(managedTurn) {
|
|
5268
|
+
return {
|
|
5269
|
+
...managedTurn,
|
|
5270
|
+
available_actions: readOnlyListActions(isRecord(managedTurn.available_actions)
|
|
5271
|
+
? managedTurn.available_actions
|
|
5272
|
+
: {})
|
|
5273
|
+
};
|
|
5274
|
+
}
|
|
5275
|
+
function withoutGenericHandoffSourceClose(managedTurn, blockingHandoffTurnIds) {
|
|
5276
|
+
const conversationId = stringValue(managedTurn.conversation_id);
|
|
5277
|
+
if (!conversationId || !blockingHandoffTurnIds.has(conversationId)) {
|
|
5278
|
+
return managedTurn;
|
|
5279
|
+
}
|
|
5280
|
+
const availableActions = isRecord(managedTurn.available_actions)
|
|
5281
|
+
? managedTurn.available_actions
|
|
5282
|
+
: {};
|
|
5283
|
+
const { close: _genericClose, ...safeActions } = availableActions;
|
|
5284
|
+
return {
|
|
5285
|
+
...managedTurn,
|
|
5286
|
+
available_actions: safeActions
|
|
5287
|
+
};
|
|
5288
|
+
}
|
|
5289
|
+
function historicalManagedTurnForTerminal(conversation, terminalCanAcceptSend, terminal) {
|
|
5290
|
+
const managedTurn = managedTurnListEntry(summarizeConversation(conversation), {
|
|
5291
|
+
terminalBridge: terminalBridgeEnabled(conversation),
|
|
5292
|
+
approvalState: managedListApprovalState(conversation),
|
|
5293
|
+
conversation
|
|
5294
|
+
});
|
|
5295
|
+
const availableActions = isRecord(managedTurn.available_actions)
|
|
5296
|
+
? managedTurn.available_actions
|
|
5297
|
+
: {};
|
|
5298
|
+
const safeActions = safeUnavailableManagedTurnActions(availableActions);
|
|
5299
|
+
return {
|
|
5300
|
+
...managedTurn,
|
|
5301
|
+
available_actions: safeActions
|
|
5302
|
+
};
|
|
5303
|
+
}
|
|
5304
|
+
function managedTurnMatchesLiveTerminal(conversation, terminal) {
|
|
5305
|
+
const takeover = isRecord(conversation.native_session_takeover)
|
|
5306
|
+
? conversation.native_session_takeover
|
|
4619
5307
|
: undefined;
|
|
4620
5308
|
const liveControl = isRecord(terminal.terminal_control)
|
|
4621
5309
|
? terminal.terminal_control
|
|
@@ -4835,17 +5523,17 @@ function managedListApprovalState(conversation) {
|
|
|
4835
5523
|
}
|
|
4836
5524
|
function listActionContracts() {
|
|
4837
5525
|
return {
|
|
4838
|
-
version:
|
|
5526
|
+
version: 12,
|
|
4839
5527
|
instructions: [
|
|
4840
5528
|
"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.",
|
|
4841
5529
|
"An existing managed session's ordinary send targets session_id and creates a new turn. A turn id is never an ordinary send target.",
|
|
4842
5530
|
"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.",
|
|
4843
5531
|
"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.",
|
|
4844
|
-
"A verified, idle human native-thread switch may expose a terminal-scoped send with expected_terminal_token; that action atomically adopts the live context before creating its Turn. A conclusively ended Codex rollout may expose the same snapshot-bound send only after AKK proves zero current rollout and an exact empty composer; it detaches the ended Session and creates an isolated virgin Session. A status-card-only
|
|
5532
|
+
"A verified, idle human native-thread switch may expose a terminal-scoped send with expected_terminal_token; that action atomically adopts the live context before creating its Turn. A conclusively ended Codex rollout may expose the same snapshot-bound send only after AKK proves zero current rollout and an exact empty composer; it detaches the ended Session and creates an isolated virgin Session. A status-card-only zero-rollout source, or a quiescent rollout-backed source whose complete pinned open-rollout inventory cannot identify one foreground candidate, may also expose this exact action. AKK freezes any released predecessor Turn history, submits the ordinary task once, and binds a separate provisional Session only after one post-anchor rollout uniquely accepts that exact request. The accepted UUID may equal or differ from the predecessor without merging their Session lineages, and narrow panes do not require /status. Until that promotion commits, strict session_id send, respond, approve, cancel, native lifecycle, and native_inspect remain unavailable, and the provisional binding has no callback authority. If dispatch, acceptance, or post-submit binding is uncertain, do not retry automatically. Other binding conflicts remain fail-closed and may expose only exact low-level reconcile_binding recovery.",
|
|
4845
5533
|
"List resumable threads before resume; use only a complete native_thread_id and the action returned for that candidate.",
|
|
4846
5534
|
"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.",
|
|
4847
5535
|
"Use respond only for an in-flight turn that is explicitly waiting for OpenClaw.",
|
|
4848
|
-
"Managed controls target turn_id. A raw terminal may be controlled only through its own list-prefilled conversation_id action; never construct, guess, or reuse
|
|
5536
|
+
"Managed controls target turn_id. A raw terminal may be controlled only through its own list-prefilled conversation_id action. When a Codex managed prompt has no usable AKK Turn owner, list may expose one manual terminal-scoped approve carrying expected_terminal_token; copy the complete action after explicit human confirmation. It does not mutate the Turn or Session binding, has no durable dispatch receipt, and is never eligible for automatic approval. If its result is interrupted, refresh status and inspect the live prompt instead of retrying blindly. Never construct, guess, or reuse either selector or token.",
|
|
4849
5537
|
"Start with the action's prefilled arguments, supply every missing_required field, and consult the top-level action's optional fields only when needed.",
|
|
4850
5538
|
"Authoritative full IDs are prefilled; short_ref is for display and human input.",
|
|
4851
5539
|
"Availability is a snapshot. AKK revalidates process, provider-owned terminal identity, workspace, activity, approval, and recovery state before side effects."
|
|
@@ -4916,7 +5604,8 @@ function listActionContracts() {
|
|
|
4916
5604
|
],
|
|
4917
5605
|
unsupported: ["timeoutSeconds"],
|
|
4918
5606
|
status_card_only_deferred_scope: "A zero-Turn Codex status-card binding has no rollout; only its listed selector/token send creates an isolated provisional Session and binds it after exact request acceptance. Until promotion commits, strict managed controls, native lifecycle, native_inspect, and callback authority remain unavailable; an uncertain dispatch, acceptance, or post-submit binding must not be retried automatically.",
|
|
4919
|
-
|
|
5607
|
+
candidate_rollout_deferred_scope: "A quiescent rollout-backed Codex source may use a listed selector/token send only when AKK pins the complete exact candidate inventory but cannot prove one foreground UUID. Released predecessor Turn history stays read-only while a separate provisional Session sends once and waits for one unique post-anchor request acceptance. Same-UUID and different-UUID results keep separate Session lineages; zero, multiple, drifted, or uncertain acceptance is never retried blindly.",
|
|
5608
|
+
ordinary_use: "Create a new managed turn in the exact Session. A live terminal selector can attach an unmanaged pane, adopt one verified human-selected native context, detach a verified-empty Codex source, or replace an eligible status-card/candidate-rollout source after the submitted request proves its unique exact rollout; an explicit session_id never follows the pane."
|
|
4920
5609
|
},
|
|
4921
5610
|
new_thread: {
|
|
4922
5611
|
tool: "agent_knock_knock_new_thread",
|
|
@@ -4989,8 +5678,10 @@ function listActionContracts() {
|
|
|
4989
5678
|
tool: "agent_knock_knock_approve",
|
|
4990
5679
|
target_argument: "turn_id",
|
|
4991
5680
|
compatibility_target_argument: "conversation_id",
|
|
4992
|
-
compatibility_scope: "A deprecated legacy Turn alias, or only the exact selector prefilled by
|
|
5681
|
+
compatibility_scope: "A deprecated legacy Turn alias, or only the exact selector prefilled by a raw-terminal or manual terminal-scoped Codex approval action; never construct, guess, or reuse it.",
|
|
4993
5682
|
required: ["turn_id", "expected_approval_fingerprint"],
|
|
5683
|
+
optional: ["expected_terminal_token"],
|
|
5684
|
+
terminal_scoped_scope: "Only the latest list-prefilled Codex conversation_id/token action may approve a managed current prompt without a usable Turn owner. It remains human-confirmed, revalidates exact terminal/process/Session/dispatch/prompt state before keys, never mutates managed state, has no durable dispatch receipt, cannot be auto-approved, and must not be blindly retried after an interrupted result.",
|
|
4994
5685
|
requires_explicit_user_confirmation: true,
|
|
4995
5686
|
requires_fresh_status: true
|
|
4996
5687
|
},
|
|
@@ -5203,12 +5894,14 @@ function availableListActions(entry, { conversation } = {}) {
|
|
|
5203
5894
|
}
|
|
5204
5895
|
async function resolveConversationSelectorOption(commandName, options) {
|
|
5205
5896
|
const sendOperation = commandName === "send";
|
|
5206
|
-
if (
|
|
5897
|
+
if (stringValue(options.expectedTerminalToken)) {
|
|
5207
5898
|
// Selector resolution may replace an alias (or an omitted selector) with a
|
|
5208
5899
|
// discovered full terminal id. Preserve the caller's actual authority so
|
|
5209
5900
|
// the handoff token fence cannot mistake that convenience resolution for
|
|
5210
5901
|
// an exact selector supplied by the caller.
|
|
5211
|
-
originalExpectedTerminalSelector.set(options, stringValue(
|
|
5902
|
+
originalExpectedTerminalSelector.set(options, stringValue(sendOperation
|
|
5903
|
+
? options.session ?? options.conversation ?? options.conversationId
|
|
5904
|
+
: options.turn ?? options.conversation ?? options.conversationId)?.trim());
|
|
5212
5905
|
}
|
|
5213
5906
|
if (!SESSION_SELECTOR_COMMANDS.has(String(commandName ?? "")) ||
|
|
5214
5907
|
options.state) {
|
|
@@ -6342,7 +7035,101 @@ function exactBoundCodexStatusCardSourceForDeferredSend({ session, terminalId, t
|
|
|
6342
7035
|
liveProcessBirth: processBirth
|
|
6343
7036
|
}) === "same");
|
|
6344
7037
|
}
|
|
6345
|
-
function
|
|
7038
|
+
function exactCodexCandidateInventoryForDeferredSend({ inventory, sourceSession, pid, workspace, processUuid, processBirth }) {
|
|
7039
|
+
const sourceNativeThreadId = sourceSession.binding?.native_thread_id;
|
|
7040
|
+
return Boolean(inventory.roots.length > 0 &&
|
|
7041
|
+
inventory.pid === pid &&
|
|
7042
|
+
inventory.processUuid === processUuid &&
|
|
7043
|
+
inventory.processBirth === processBirth &&
|
|
7044
|
+
inventory.cwd &&
|
|
7045
|
+
path.resolve(inventory.cwd) === path.resolve(workspace) &&
|
|
7046
|
+
/^[0-9a-f]{64}$/u.test(inventory.inventoryFingerprint) &&
|
|
7047
|
+
(sourceSession.binding?.native_process.rollout === undefined ||
|
|
7048
|
+
(sourceNativeThreadId &&
|
|
7049
|
+
inventory.roots.some((root) => root.sessionId.toLowerCase() === sourceNativeThreadId.toLowerCase()))));
|
|
7050
|
+
}
|
|
7051
|
+
function exactBoundCodexCandidateSourceForDeferredSend({ session, terminalId, terminalControl, pid, workspace, processUuid, processBirth, inventory }) {
|
|
7052
|
+
const binding = session.binding;
|
|
7053
|
+
const sourceNativeThreadId = binding?.native_thread_id;
|
|
7054
|
+
const sourceRollout = binding?.native_process.rollout;
|
|
7055
|
+
const inventoryRoot = inventory.roots.find((root) => root.sessionId.toLowerCase() === sourceNativeThreadId?.toLowerCase());
|
|
7056
|
+
return Boolean(session.agent === "codex" &&
|
|
7057
|
+
session.status === "bound" &&
|
|
7058
|
+
binding &&
|
|
7059
|
+
isExactNativeThreadId(sourceNativeThreadId) &&
|
|
7060
|
+
isCompleteNativeRollout(sourceRollout) &&
|
|
7061
|
+
inventoryRoot &&
|
|
7062
|
+
exactNativeRolloutMatches(inventoryRoot.rollout, sourceRollout) &&
|
|
7063
|
+
terminalControlAliasMatches(binding.terminal_id, binding.terminal_control, terminalId, terminalControl) &&
|
|
7064
|
+
path.resolve(session.workspace) === path.resolve(workspace) &&
|
|
7065
|
+
processIncarnationRelationship({
|
|
7066
|
+
binding,
|
|
7067
|
+
livePid: pid,
|
|
7068
|
+
liveProcessUuid: processUuid,
|
|
7069
|
+
liveProcessBirth: processBirth
|
|
7070
|
+
}) === "same" &&
|
|
7071
|
+
exactCodexCandidateInventoryForDeferredSend({
|
|
7072
|
+
inventory,
|
|
7073
|
+
sourceSession: session,
|
|
7074
|
+
pid,
|
|
7075
|
+
workspace,
|
|
7076
|
+
processUuid,
|
|
7077
|
+
processBirth
|
|
7078
|
+
}));
|
|
7079
|
+
}
|
|
7080
|
+
function deferredCandidateSourceTurnHistory(storeDir, session) {
|
|
7081
|
+
const binding = session.binding;
|
|
7082
|
+
if (!binding?.native_thread_id) {
|
|
7083
|
+
return undefined;
|
|
7084
|
+
}
|
|
7085
|
+
const turns = managedTurnsForSession(storeDir, session.session_id);
|
|
7086
|
+
if (turns.some((turn) => !TERMINAL_DISPATCH_RELEASE_STATUSES.has(turn.status) ||
|
|
7087
|
+
managedTurnNeedsAttention(turn) ||
|
|
7088
|
+
(isRecord(turn.callback_delivery) &&
|
|
7089
|
+
turn.callback_delivery.status !== "delivered") ||
|
|
7090
|
+
(Boolean(turn.gateway_method) &&
|
|
7091
|
+
(!isRecord(turn.callback_delivery) ||
|
|
7092
|
+
turn.callback_delivery.status !== "delivered")) ||
|
|
7093
|
+
stringValue(turn.terminal_binding_id) !== binding.binding_id ||
|
|
7094
|
+
Number(turn.terminal_binding_generation) !== binding.generation ||
|
|
7095
|
+
(stringValue(turn.native_thread_id) ??
|
|
7096
|
+
stringValue(isRecord(turn.native_session_takeover)
|
|
7097
|
+
? turn.native_session_takeover.terminal_agent_session_id
|
|
7098
|
+
: undefined)) !== binding.native_thread_id)) {
|
|
7099
|
+
return undefined;
|
|
7100
|
+
}
|
|
7101
|
+
return turns
|
|
7102
|
+
.map((turn) => ({
|
|
7103
|
+
turn_id: turnIdForConversation(turn),
|
|
7104
|
+
status: turn.status,
|
|
7105
|
+
updated_at: required(stringValue(turn.updated_at), `managed Turn ${turnIdForConversation(turn)} updated_at is unavailable`),
|
|
7106
|
+
binding_id: binding.binding_id,
|
|
7107
|
+
binding_generation: binding.generation,
|
|
7108
|
+
native_thread_id: binding.native_thread_id,
|
|
7109
|
+
turn_fingerprint: createHash("sha256")
|
|
7110
|
+
.update(JSON.stringify(turn))
|
|
7111
|
+
.digest("hex")
|
|
7112
|
+
}))
|
|
7113
|
+
.sort((left, right) => left.turn_id.localeCompare(right.turn_id));
|
|
7114
|
+
}
|
|
7115
|
+
function candidateSourceTransitionHistoryIsTerminal(storeDir, session) {
|
|
7116
|
+
if (managedSessionHasUnresolvedNativeTransition(storeDir, session)) {
|
|
7117
|
+
return false;
|
|
7118
|
+
}
|
|
7119
|
+
if (!session.last_transition_id) {
|
|
7120
|
+
return true;
|
|
7121
|
+
}
|
|
7122
|
+
try {
|
|
7123
|
+
const transition = loadNativeThreadTransition(storeDir, session.last_transition_id);
|
|
7124
|
+
return ["committed", "aborted"].includes(transition.status) &&
|
|
7125
|
+
(transition.source_session_id === session.session_id ||
|
|
7126
|
+
transition.target_session_id === session.session_id);
|
|
7127
|
+
}
|
|
7128
|
+
catch {
|
|
7129
|
+
return false;
|
|
7130
|
+
}
|
|
7131
|
+
}
|
|
7132
|
+
function deferredCodexForegroundBindingToken({ terminalId, terminalControl, pid, workspace, processUuid, processBirth, sourceSession, dispatchSnapshot, candidateInventory }) {
|
|
6346
7133
|
const terminalToken = unmanagedTerminalBindingToken({
|
|
6347
7134
|
terminalId,
|
|
6348
7135
|
terminalControl,
|
|
@@ -6352,16 +7139,27 @@ function deferredCodexForegroundBindingToken({ terminalId, terminalControl, pid,
|
|
|
6352
7139
|
processUuid,
|
|
6353
7140
|
processBirth
|
|
6354
7141
|
});
|
|
7142
|
+
const candidateAuthority = candidateInventory
|
|
7143
|
+
? {
|
|
7144
|
+
inventory_pid: candidateInventory.pid,
|
|
7145
|
+
inventory_cwd: candidateInventory.cwd,
|
|
7146
|
+
inventory_fingerprint: candidateInventory.inventoryFingerprint,
|
|
7147
|
+
candidate_native_thread_ids: candidateInventory.roots.map((root) => root.sessionId)
|
|
7148
|
+
}
|
|
7149
|
+
: undefined;
|
|
6355
7150
|
return createHash("sha256")
|
|
6356
7151
|
.update(JSON.stringify({
|
|
6357
|
-
version: 2,
|
|
7152
|
+
version: candidateAuthority ? 3 : 2,
|
|
6358
7153
|
kind: "deferred_codex_foreground_binding",
|
|
6359
7154
|
terminal_token: terminalToken,
|
|
6360
7155
|
source_session_id: sourceSession.session_id,
|
|
6361
7156
|
source_revision: managedSessionRevision(sourceSession),
|
|
6362
7157
|
source_binding_token: managedSessionBindingToken(sourceSession),
|
|
6363
7158
|
terminal_dispatch_snapshot: dispatchSnapshot,
|
|
6364
|
-
observation:
|
|
7159
|
+
observation: candidateAuthority
|
|
7160
|
+
? "exact_unbound_open_root_inventory"
|
|
7161
|
+
: "verified_absent",
|
|
7162
|
+
...(candidateAuthority ?? {})
|
|
6365
7163
|
}))
|
|
6366
7164
|
.digest("hex");
|
|
6367
7165
|
}
|
|
@@ -6660,37 +7458,91 @@ async function assertDeferredCodexForegroundBindingBoundary({ options, boundary,
|
|
|
6660
7458
|
managedSessionBindingToken(source) !== expectedBindingToken) {
|
|
6661
7459
|
throw new Error("the deferred Codex foreground source changed; refresh AKK list");
|
|
6662
7460
|
}
|
|
6663
|
-
if (
|
|
6664
|
-
|
|
6665
|
-
|
|
6666
|
-
|
|
6667
|
-
pid: boundary.terminal.pid,
|
|
6668
|
-
workspace: boundary.terminal.terminalControl.currentPath,
|
|
6669
|
-
processUuid: boundary.processUuid,
|
|
6670
|
-
processBirth: boundary.processBirth
|
|
6671
|
-
}) ||
|
|
6672
|
-
source.last_transition_id !== (expectedSourceStatus === "bound" ? undefined : boundary.transferId) ||
|
|
6673
|
-
managedSessionHasAnyNativeTransition(storeDir, source) ||
|
|
6674
|
-
managedTurnsForSession(storeDir, source.session_id).length !== 0) {
|
|
6675
|
-
throw new Error("the deferred Codex foreground source is no longer an isolated status-card binding");
|
|
7461
|
+
if (source.last_transition_id !== (expectedSourceStatus === "bound"
|
|
7462
|
+
? boundary.sourcePreviousLastTransitionId
|
|
7463
|
+
: boundary.transferId)) {
|
|
7464
|
+
throw new Error("the deferred Codex foreground source transition authority changed");
|
|
6676
7465
|
}
|
|
6677
7466
|
const liveIncarnation = codexProcessIncarnationForPid(boundary.terminal.pid);
|
|
6678
7467
|
if (liveIncarnation.processUuid !== boundary.processUuid ||
|
|
6679
7468
|
liveIncarnation.processBirth !== boundary.processBirth) {
|
|
6680
7469
|
throw new Error("the deferred Codex foreground process incarnation changed");
|
|
6681
7470
|
}
|
|
6682
|
-
|
|
6683
|
-
|
|
6684
|
-
|
|
6685
|
-
|
|
6686
|
-
|
|
6687
|
-
|
|
6688
|
-
|
|
6689
|
-
|
|
7471
|
+
let candidateInventoryForBoundary;
|
|
7472
|
+
if (boundary.candidateAcceptanceAnchor) {
|
|
7473
|
+
const inventory = await inspectCodexOpenRootRolloutInventory({
|
|
7474
|
+
options,
|
|
7475
|
+
pid: boundary.terminal.pid,
|
|
7476
|
+
cwd: boundary.terminal.terminalControl.currentPath
|
|
7477
|
+
});
|
|
7478
|
+
candidateInventoryForBoundary = inventory;
|
|
7479
|
+
if (inventory.inventoryFingerprint !==
|
|
7480
|
+
boundary.candidateAcceptanceAnchor.inventory_fingerprint ||
|
|
7481
|
+
!exactCodexCandidateInventoryForDeferredSend({
|
|
7482
|
+
inventory,
|
|
7483
|
+
sourceSession: source,
|
|
7484
|
+
pid: boundary.terminal.pid,
|
|
7485
|
+
workspace: required(boundary.terminal.terminalControl.currentPath, "deferred Codex terminal workspace is unavailable"),
|
|
7486
|
+
processUuid: boundary.processUuid,
|
|
7487
|
+
processBirth: boundary.processBirth
|
|
7488
|
+
})) {
|
|
7489
|
+
throw new Error("the exact Codex open-root inventory changed; refresh AKK list before sending");
|
|
7490
|
+
}
|
|
6690
7491
|
}
|
|
6691
|
-
|
|
6692
|
-
|
|
6693
|
-
|
|
7492
|
+
else {
|
|
7493
|
+
const observation = await observeCurrentNativeAgentSessionIdentity({
|
|
7494
|
+
options,
|
|
7495
|
+
agent: "codex",
|
|
7496
|
+
pid: boundary.terminal.pid,
|
|
7497
|
+
cwd: boundary.terminal.terminalControl.currentPath
|
|
7498
|
+
});
|
|
7499
|
+
if (observation.status === "unavailable") {
|
|
7500
|
+
throw new Error(`Codex native identity observation is unavailable: ${observation.reason}`);
|
|
7501
|
+
}
|
|
7502
|
+
if (observation.status !== "verified_absent") {
|
|
7503
|
+
throw new Error(`Codex materialized native thread ${observation.identity.sessionId}; ` +
|
|
7504
|
+
"refresh AKK list before sending");
|
|
7505
|
+
}
|
|
7506
|
+
}
|
|
7507
|
+
const sourceAsBound = {
|
|
7508
|
+
...source,
|
|
7509
|
+
status: "bound",
|
|
7510
|
+
last_transition_id: boundary.sourcePreviousLastTransitionId
|
|
7511
|
+
};
|
|
7512
|
+
if (boundary.sourceKind === "status_card_only") {
|
|
7513
|
+
if (!exactBoundCodexStatusCardSourceForDeferredSend({
|
|
7514
|
+
session: sourceAsBound,
|
|
7515
|
+
terminalId: boundary.terminal.conversationId,
|
|
7516
|
+
terminalControl: boundary.terminal.terminalControl,
|
|
7517
|
+
pid: boundary.terminal.pid,
|
|
7518
|
+
workspace: boundary.terminal.terminalControl.currentPath,
|
|
7519
|
+
processUuid: boundary.processUuid,
|
|
7520
|
+
processBirth: boundary.processBirth
|
|
7521
|
+
}) ||
|
|
7522
|
+
managedTurnsForSession(storeDir, source.session_id).length !== 0 ||
|
|
7523
|
+
boundary.sourcePreviousLastTransitionId !== undefined ||
|
|
7524
|
+
managedSessionHasAnyNativeTransition(storeDir, sourceAsBound)) {
|
|
7525
|
+
throw new Error("the deferred Codex foreground source is no longer an isolated status-card binding");
|
|
7526
|
+
}
|
|
7527
|
+
}
|
|
7528
|
+
else {
|
|
7529
|
+
const history = deferredCandidateSourceTurnHistory(storeDir, sourceAsBound);
|
|
7530
|
+
if (!boundary.candidateAcceptanceAnchor ||
|
|
7531
|
+
!history ||
|
|
7532
|
+
JSON.stringify(history) !== JSON.stringify(boundary.sourceTurnHistory) ||
|
|
7533
|
+
!exactBoundCodexCandidateSourceForDeferredSend({
|
|
7534
|
+
session: sourceAsBound,
|
|
7535
|
+
terminalId: boundary.terminal.conversationId,
|
|
7536
|
+
terminalControl: boundary.terminal.terminalControl,
|
|
7537
|
+
pid: boundary.terminal.pid,
|
|
7538
|
+
workspace: required(boundary.terminal.terminalControl.currentPath, "deferred Codex terminal workspace is unavailable"),
|
|
7539
|
+
processUuid: boundary.processUuid,
|
|
7540
|
+
processBirth: boundary.processBirth,
|
|
7541
|
+
inventory: required(candidateInventoryForBoundary, "deferred Codex candidate inventory is unavailable")
|
|
7542
|
+
}) ||
|
|
7543
|
+
!candidateSourceTransitionHistoryIsTerminal(storeDir, sourceAsBound)) {
|
|
7544
|
+
throw new Error("the deferred Codex candidate source history or rollout authority changed");
|
|
7545
|
+
}
|
|
6694
7546
|
}
|
|
6695
7547
|
if (requireNoDispatch) {
|
|
6696
7548
|
assertTerminalIncarnationCanStartTurn(storeDir, boundary.terminal.terminalControl);
|
|
@@ -6704,7 +7556,8 @@ async function assertDeferredCodexForegroundBindingBoundary({ options, boundary,
|
|
|
6704
7556
|
const status = await createTerminalAgentBridge(options).status("codex", boundary.terminal.terminalControl, {
|
|
6705
7557
|
runtime: terminalRuntimeForLiveIdentity({
|
|
6706
7558
|
terminal: boundary.terminal,
|
|
6707
|
-
expectedEmptyNativeSession:
|
|
7559
|
+
expectedEmptyNativeSession: boundary.candidateAcceptanceAnchor === undefined,
|
|
7560
|
+
physicalOnly: boundary.candidateAcceptanceAnchor !== undefined
|
|
6708
7561
|
})
|
|
6709
7562
|
});
|
|
6710
7563
|
if (status.reachable !== true ||
|
|
@@ -6722,11 +7575,12 @@ async function assertDeferredCodexForegroundBindingBoundary({ options, boundary,
|
|
|
6722
7575
|
}
|
|
6723
7576
|
return source;
|
|
6724
7577
|
}
|
|
6725
|
-
async function prepareDeferredCodexForegroundBinding({ options, terminal, sourceSession, observation, requestText }) {
|
|
7578
|
+
async function prepareDeferredCodexForegroundBinding({ options, terminal, sourceSession, observation, candidateInventory, requestText }) {
|
|
6726
7579
|
const expectedToken = stringValue(options.expectedTerminalToken);
|
|
7580
|
+
const candidateMode = Boolean(candidateInventory?.roots.length);
|
|
6727
7581
|
if (terminal.agent !== "codex" ||
|
|
6728
7582
|
!sourceSession?.binding ||
|
|
6729
|
-
observation.status !== "verified_absent" ||
|
|
7583
|
+
(observation.status !== "verified_absent" && !candidateMode) ||
|
|
6730
7584
|
!expectedToken) {
|
|
6731
7585
|
return undefined;
|
|
6732
7586
|
}
|
|
@@ -6734,19 +7588,47 @@ async function prepareDeferredCodexForegroundBinding({ options, terminal, source
|
|
|
6734
7588
|
const processBirth = sourceSession.binding.native_process.process_birth;
|
|
6735
7589
|
const workspace = terminal.terminalControl.currentPath;
|
|
6736
7590
|
const liveIncarnation = codexProcessIncarnationForPid(terminal.pid);
|
|
7591
|
+
const storeDir = storeDirFromOptions(options);
|
|
7592
|
+
const sourceKind = sourceSession.binding.native_process.rollout
|
|
7593
|
+
? "candidate_rollout_quiescent"
|
|
7594
|
+
: "status_card_only";
|
|
7595
|
+
const candidateSourceTurnHistory = sourceKind ===
|
|
7596
|
+
"candidate_rollout_quiescent"
|
|
7597
|
+
? deferredCandidateSourceTurnHistory(storeDir, sourceSession)
|
|
7598
|
+
: undefined;
|
|
7599
|
+
const exactSource = !workspace
|
|
7600
|
+
? false
|
|
7601
|
+
: sourceKind === "candidate_rollout_quiescent"
|
|
7602
|
+
? candidateInventory !== undefined &&
|
|
7603
|
+
candidateSourceTurnHistory !== undefined &&
|
|
7604
|
+
candidateSourceTransitionHistoryIsTerminal(storeDir, sourceSession) &&
|
|
7605
|
+
exactBoundCodexCandidateSourceForDeferredSend({
|
|
7606
|
+
session: sourceSession,
|
|
7607
|
+
terminalId: terminal.conversationId,
|
|
7608
|
+
terminalControl: terminal.terminalControl,
|
|
7609
|
+
pid: terminal.pid,
|
|
7610
|
+
workspace,
|
|
7611
|
+
processUuid: liveIncarnation.processUuid,
|
|
7612
|
+
processBirth: liveIncarnation.processBirth,
|
|
7613
|
+
inventory: candidateInventory
|
|
7614
|
+
})
|
|
7615
|
+
: exactBoundCodexStatusCardSourceForDeferredSend({
|
|
7616
|
+
session: sourceSession,
|
|
7617
|
+
terminalId: terminal.conversationId,
|
|
7618
|
+
terminalControl: terminal.terminalControl,
|
|
7619
|
+
pid: terminal.pid,
|
|
7620
|
+
workspace,
|
|
7621
|
+
processUuid: liveIncarnation.processUuid,
|
|
7622
|
+
processBirth: liveIncarnation.processBirth
|
|
7623
|
+
}) &&
|
|
7624
|
+
managedTurnsForSession(storeDir, sourceSession.session_id).length === 0 &&
|
|
7625
|
+
!sourceSession.last_transition_id &&
|
|
7626
|
+
!managedSessionHasAnyNativeTransition(storeDir, sourceSession);
|
|
6737
7627
|
if (!processUuid ||
|
|
6738
7628
|
!processBirth ||
|
|
6739
7629
|
!workspace ||
|
|
6740
7630
|
!hasCanonicalTerminalEndpoint(terminal.terminalControl) ||
|
|
6741
|
-
!
|
|
6742
|
-
session: sourceSession,
|
|
6743
|
-
terminalId: terminal.conversationId,
|
|
6744
|
-
terminalControl: terminal.terminalControl,
|
|
6745
|
-
pid: terminal.pid,
|
|
6746
|
-
workspace,
|
|
6747
|
-
processUuid: liveIncarnation.processUuid,
|
|
6748
|
-
processBirth: liveIncarnation.processBirth
|
|
6749
|
-
})) {
|
|
7631
|
+
!exactSource) {
|
|
6750
7632
|
return undefined;
|
|
6751
7633
|
}
|
|
6752
7634
|
const dispatchSnapshot = deferredCodexForegroundDispatchSnapshot(terminal.terminalControl);
|
|
@@ -6758,7 +7640,8 @@ async function prepareDeferredCodexForegroundBinding({ options, terminal, source
|
|
|
6758
7640
|
processUuid,
|
|
6759
7641
|
processBirth,
|
|
6760
7642
|
sourceSession,
|
|
6761
|
-
dispatchSnapshot
|
|
7643
|
+
dispatchSnapshot,
|
|
7644
|
+
...(candidateMode ? { candidateInventory } : {})
|
|
6762
7645
|
});
|
|
6763
7646
|
if (expectedToken !== token) {
|
|
6764
7647
|
throw new Error("deferred Codex foreground binding requires the fresh exact terminal " +
|
|
@@ -6766,6 +7649,12 @@ async function prepareDeferredCodexForegroundBinding({ options, terminal, source
|
|
|
6766
7649
|
}
|
|
6767
7650
|
const targetSessionId = createManagedSessionId();
|
|
6768
7651
|
const transferId = createDeferredForegroundTransferId();
|
|
7652
|
+
const candidateAcceptanceAnchor = candidateMode
|
|
7653
|
+
? captureCodexCandidateSetRolloutAcceptanceAnchor({
|
|
7654
|
+
inventory: required(candidateInventory, "Codex candidate inventory is unavailable"),
|
|
7655
|
+
now: cliNow()
|
|
7656
|
+
})
|
|
7657
|
+
: undefined;
|
|
6769
7658
|
const boundary = {
|
|
6770
7659
|
terminal,
|
|
6771
7660
|
transferId,
|
|
@@ -6775,7 +7664,15 @@ async function prepareDeferredCodexForegroundBinding({ options, terminal, source
|
|
|
6775
7664
|
sourceBoundBindingToken: managedSessionBindingToken(sourceSession),
|
|
6776
7665
|
processUuid,
|
|
6777
7666
|
processBirth,
|
|
6778
|
-
previousDispatchSnapshot: dispatchSnapshot
|
|
7667
|
+
previousDispatchSnapshot: dispatchSnapshot,
|
|
7668
|
+
sourceKind,
|
|
7669
|
+
...(sourceSession.last_transition_id
|
|
7670
|
+
? { sourcePreviousLastTransitionId: sourceSession.last_transition_id }
|
|
7671
|
+
: {}),
|
|
7672
|
+
...(candidateSourceTurnHistory
|
|
7673
|
+
? { sourceTurnHistory: candidateSourceTurnHistory }
|
|
7674
|
+
: {}),
|
|
7675
|
+
...(candidateAcceptanceAnchor ? { candidateAcceptanceAnchor } : {})
|
|
6779
7676
|
};
|
|
6780
7677
|
await assertDeferredCodexForegroundBindingBoundary({
|
|
6781
7678
|
options,
|
|
@@ -6793,8 +7690,7 @@ async function prepareDeferredCodexForegroundBinding({ options, terminal, source
|
|
|
6793
7690
|
terminalControl: terminal.terminalControl,
|
|
6794
7691
|
excludedManagedSessionId: sourceSession.session_id
|
|
6795
7692
|
});
|
|
6796
|
-
const
|
|
6797
|
-
const existingTransfer = listDeferredForegroundTransfers(storeDir).find((candidate) => !["resolved", "aborted", "abort_resolved"].includes(candidate.status) &&
|
|
7693
|
+
const existingTransfer = listDeferredForegroundTransfers(storeDir).find((candidate) => !["resolved", "abort_resolved"].includes(candidate.status) &&
|
|
6798
7694
|
(candidate.source_session_id === sourceSession.session_id ||
|
|
6799
7695
|
(candidate.terminal_id === terminal.conversationId &&
|
|
6800
7696
|
terminalControlEvidenceMatches(candidate.terminal_endpoint, terminal.terminalControl))));
|
|
@@ -6804,7 +7700,7 @@ async function prepareDeferredCodexForegroundBinding({ options, terminal, source
|
|
|
6804
7700
|
}
|
|
6805
7701
|
saveDeferredForegroundTransfer(storeDir, {
|
|
6806
7702
|
schema: "agent-knock-knock/deferred-foreground-transfer",
|
|
6807
|
-
version:
|
|
7703
|
+
version: 2,
|
|
6808
7704
|
transfer_id: transferId,
|
|
6809
7705
|
status: "prepared",
|
|
6810
7706
|
input_stage: "none",
|
|
@@ -6819,6 +7715,10 @@ async function prepareDeferredCodexForegroundBinding({ options, terminal, source
|
|
|
6819
7715
|
source_binding_token: managedSessionBindingToken(sourceSession),
|
|
6820
7716
|
source_previous_last_transition_id: sourceSession.last_transition_id,
|
|
6821
7717
|
source_before_binding: sourceSession.binding,
|
|
7718
|
+
source_kind: sourceKind,
|
|
7719
|
+
...(candidateSourceTurnHistory
|
|
7720
|
+
? { source_turn_history: candidateSourceTurnHistory }
|
|
7721
|
+
: {}),
|
|
6822
7722
|
target_session_id: targetSessionId,
|
|
6823
7723
|
target_expected_revision: null,
|
|
6824
7724
|
previous_dispatch_status: dispatchSnapshot.status,
|
|
@@ -6846,6 +7746,13 @@ function assertDeferredForegroundTransferMatchesBoundary({ transfer, boundary })
|
|
|
6846
7746
|
boundary.previousDispatchSnapshot.status ||
|
|
6847
7747
|
transfer.previous_dispatch_fingerprint !==
|
|
6848
7748
|
boundary.previousDispatchSnapshot.fingerprint ||
|
|
7749
|
+
(transfer.version === 1
|
|
7750
|
+
? "status_card_only"
|
|
7751
|
+
: transfer.source_kind) !== boundary.sourceKind ||
|
|
7752
|
+
JSON.stringify(transfer.source_turn_history) !==
|
|
7753
|
+
JSON.stringify(boundary.sourceTurnHistory) ||
|
|
7754
|
+
transfer.source_previous_last_transition_id !==
|
|
7755
|
+
boundary.sourcePreviousLastTransitionId ||
|
|
6849
7756
|
transfer.process_pid !== boundary.terminal.pid ||
|
|
6850
7757
|
transfer.process_uuid !== boundary.processUuid ||
|
|
6851
7758
|
transfer.process_birth !== boundary.processBirth ||
|
|
@@ -10757,209 +11664,215 @@ function terminalBridgeApprovalInstructions({ conversation, terminalControl, ter
|
|
|
10757
11664
|
].filter((line) => line !== undefined).join("\n");
|
|
10758
11665
|
}
|
|
10759
11666
|
function recordTerminalBridgeApprovalNotification({ statePath, logPath, terminalControl, terminalStatus, fingerprint, expectedConversation, onRecorded }) {
|
|
10760
|
-
const
|
|
10761
|
-
|
|
10762
|
-
const
|
|
10763
|
-
|
|
10764
|
-
|
|
10765
|
-
|
|
10766
|
-
|
|
10767
|
-
|
|
10768
|
-
|
|
10769
|
-
conversation.status
|
|
10770
|
-
|
|
10771
|
-
|
|
10772
|
-
|
|
10773
|
-
|
|
10774
|
-
|
|
10775
|
-
|
|
10776
|
-
|
|
10777
|
-
|
|
10778
|
-
duplicate: false,
|
|
10779
|
-
stale: true,
|
|
10780
|
-
recorded: undefined
|
|
10781
|
-
};
|
|
10782
|
-
}
|
|
10783
|
-
const nativeTakeover = isRecord(conversation.native_session_takeover)
|
|
10784
|
-
? { ...conversation.native_session_takeover }
|
|
10785
|
-
: {};
|
|
10786
|
-
const approvalScreenDigest = stringValue(isRecord(terminalStatus?.screen)
|
|
10787
|
-
? terminalStatus.screen.digest
|
|
10788
|
-
: undefined);
|
|
10789
|
-
const previousApproval = isRecord(nativeTakeover.terminal_bridge_approval)
|
|
10790
|
-
? nativeTakeover.terminal_bridge_approval
|
|
10791
|
-
: undefined;
|
|
10792
|
-
const previousNotifiedAt = validTimestampMs(previousApproval?.notified_at);
|
|
10793
|
-
const callbackDelivery = isRecord(conversation.callback_delivery)
|
|
10794
|
-
? conversation.callback_delivery
|
|
10795
|
-
: undefined;
|
|
10796
|
-
const callbackMessage = isRecord(callbackDelivery?.message)
|
|
10797
|
-
? callbackDelivery.message
|
|
10798
|
-
: undefined;
|
|
10799
|
-
const previousCallbackMessageId = stringValue(previousApproval?.callback_message_id);
|
|
10800
|
-
const matchingApprovalOutbox = callbackDelivery?.kind === "approval_notification" &&
|
|
10801
|
-
previousCallbackMessageId !== undefined &&
|
|
10802
|
-
callbackMessage?.id === previousCallbackMessageId;
|
|
10803
|
-
const callbackDeliveryStatus = stringValue(callbackDelivery?.status);
|
|
10804
|
-
const callbackDeliveryAttempts = Number(callbackDelivery?.attempts ?? 0);
|
|
10805
|
-
const conflictingActiveOutbox = !matchingApprovalOutbox &&
|
|
10806
|
-
(callbackDeliveryStatus === "pending" ||
|
|
10807
|
-
(callbackDeliveryStatus === "failed" &&
|
|
10808
|
-
Number.isFinite(callbackDeliveryAttempts) &&
|
|
10809
|
-
callbackDeliveryAttempts <= CALLBACK_RETRY_DELAYS_MS.length));
|
|
10810
|
-
if (previousApproval?.fingerprint === fingerprint &&
|
|
10811
|
-
previousNotifiedAt !== undefined &&
|
|
10812
|
-
cliNowMs() - previousNotifiedAt <= CLAUDE_SCREEN_APPROVAL_TTL_MS) {
|
|
10813
|
-
if (conflictingActiveOutbox) {
|
|
11667
|
+
const storeDir = pathsForConversationDir(path.dirname(statePath)).storeDir;
|
|
11668
|
+
return withStoreWriterLease(storeDir, () => {
|
|
11669
|
+
const releaseLock = acquireFileLock(`${statePath}.lock`);
|
|
11670
|
+
try {
|
|
11671
|
+
const conversation = loadState(statePath);
|
|
11672
|
+
const currentNativeTakeover = isRecord(conversation.native_session_takeover)
|
|
11673
|
+
? conversation.native_session_takeover
|
|
11674
|
+
: undefined;
|
|
11675
|
+
const currentTerminalControl = terminalControlFromTakeover(currentNativeTakeover);
|
|
11676
|
+
if (!isWaitingForAgent(conversation.status) ||
|
|
11677
|
+
conversation.conversation_id !== expectedConversation.conversationId ||
|
|
11678
|
+
conversation.status !== expectedConversation.status ||
|
|
11679
|
+
conversation.updated_at !== expectedConversation.updatedAt ||
|
|
11680
|
+
currentNativeTakeover?.terminal_bridge !== true ||
|
|
11681
|
+
stringValue(currentNativeTakeover.terminal_bridge_message_id) !==
|
|
11682
|
+
expectedConversation.messageId ||
|
|
11683
|
+
!currentTerminalControl ||
|
|
11684
|
+
!terminalControlsShareIncarnation(currentTerminalControl, terminalControl)) {
|
|
10814
11685
|
return {
|
|
10815
11686
|
conversation,
|
|
10816
11687
|
duplicate: false,
|
|
10817
11688
|
stale: true,
|
|
10818
|
-
deferred: true,
|
|
10819
|
-
previousApproval,
|
|
10820
11689
|
recorded: undefined
|
|
10821
11690
|
};
|
|
10822
11691
|
}
|
|
10823
|
-
|
|
10824
|
-
|
|
10825
|
-
|
|
10826
|
-
|
|
10827
|
-
|
|
10828
|
-
|
|
10829
|
-
|
|
10830
|
-
|
|
10831
|
-
|
|
10832
|
-
|
|
10833
|
-
|
|
10834
|
-
|
|
10835
|
-
|
|
10836
|
-
|
|
10837
|
-
|
|
11692
|
+
const nativeTakeover = isRecord(conversation.native_session_takeover)
|
|
11693
|
+
? { ...conversation.native_session_takeover }
|
|
11694
|
+
: {};
|
|
11695
|
+
const approvalScreenDigest = stringValue(isRecord(terminalStatus?.screen)
|
|
11696
|
+
? terminalStatus.screen.digest
|
|
11697
|
+
: undefined);
|
|
11698
|
+
const previousApproval = isRecord(nativeTakeover.terminal_bridge_approval)
|
|
11699
|
+
? nativeTakeover.terminal_bridge_approval
|
|
11700
|
+
: undefined;
|
|
11701
|
+
const previousNotifiedAt = validTimestampMs(previousApproval?.notified_at);
|
|
11702
|
+
const callbackDelivery = isRecord(conversation.callback_delivery)
|
|
11703
|
+
? conversation.callback_delivery
|
|
11704
|
+
: undefined;
|
|
11705
|
+
const callbackMessage = isRecord(callbackDelivery?.message)
|
|
11706
|
+
? callbackDelivery.message
|
|
11707
|
+
: undefined;
|
|
11708
|
+
const previousCallbackMessageId = stringValue(previousApproval?.callback_message_id);
|
|
11709
|
+
const matchingApprovalOutbox = callbackDelivery?.kind === "approval_notification" &&
|
|
11710
|
+
previousCallbackMessageId !== undefined &&
|
|
11711
|
+
callbackMessage?.id === previousCallbackMessageId;
|
|
11712
|
+
const callbackDeliveryStatus = stringValue(callbackDelivery?.status);
|
|
11713
|
+
const callbackDeliveryAttempts = Number(callbackDelivery?.attempts ?? 0);
|
|
11714
|
+
const conflictingActiveOutbox = !matchingApprovalOutbox &&
|
|
11715
|
+
(callbackDeliveryStatus === "pending" ||
|
|
11716
|
+
(callbackDeliveryStatus === "failed" &&
|
|
11717
|
+
Number.isFinite(callbackDeliveryAttempts) &&
|
|
11718
|
+
callbackDeliveryAttempts <= CALLBACK_RETRY_DELAYS_MS.length));
|
|
11719
|
+
if (previousApproval?.fingerprint === fingerprint &&
|
|
11720
|
+
previousNotifiedAt !== undefined &&
|
|
11721
|
+
cliNowMs() - previousNotifiedAt <= CLAUDE_SCREEN_APPROVAL_TTL_MS) {
|
|
11722
|
+
if (conflictingActiveOutbox) {
|
|
11723
|
+
return {
|
|
11724
|
+
conversation,
|
|
11725
|
+
duplicate: false,
|
|
11726
|
+
stale: true,
|
|
11727
|
+
deferred: true,
|
|
11728
|
+
previousApproval,
|
|
11729
|
+
recorded: undefined
|
|
11730
|
+
};
|
|
11731
|
+
}
|
|
11732
|
+
if (!matchingApprovalOutbox && !conflictingActiveOutbox) {
|
|
11733
|
+
const recoveryMessageId = previousCallbackMessageId ?? `msg-${randomUUID()}`;
|
|
11734
|
+
const recoveryMessageTs = stringValue(previousApproval?.callback_message_ts) ??
|
|
11735
|
+
stringValue(previousApproval?.notified_at) ??
|
|
11736
|
+
cliNow().toISOString();
|
|
11737
|
+
const recoveryConversation = previousCallbackMessageId
|
|
11738
|
+
? conversation
|
|
11739
|
+
: {
|
|
11740
|
+
...conversation,
|
|
11741
|
+
native_session_takeover: {
|
|
11742
|
+
...nativeTakeover,
|
|
11743
|
+
terminal_bridge_approval: {
|
|
11744
|
+
...previousApproval,
|
|
11745
|
+
callback_message_id: recoveryMessageId,
|
|
11746
|
+
callback_message_ts: recoveryMessageTs
|
|
11747
|
+
}
|
|
10838
11748
|
}
|
|
10839
|
-
}
|
|
11749
|
+
};
|
|
11750
|
+
if (!previousCallbackMessageId) {
|
|
11751
|
+
saveState(statePath, recoveryConversation);
|
|
11752
|
+
}
|
|
11753
|
+
const recorded = onRecorded?.(recoveryConversation, {
|
|
11754
|
+
recoverMissingOutbox: true
|
|
11755
|
+
});
|
|
11756
|
+
appendEvent(logPath, {
|
|
11757
|
+
ts: cliNow().toISOString(),
|
|
11758
|
+
conversation_id: recoveryConversation.conversation_id,
|
|
11759
|
+
event: "terminal_bridge_approval_notification_outbox_recovered",
|
|
11760
|
+
terminal_control: terminalControl,
|
|
11761
|
+
fingerprint,
|
|
11762
|
+
callback_message_id: recoveryMessageId
|
|
11763
|
+
});
|
|
11764
|
+
return {
|
|
11765
|
+
conversation: isRecord(recorded) && isRecord(recorded.prepared)
|
|
11766
|
+
? recorded.prepared.conversation
|
|
11767
|
+
: recoveryConversation,
|
|
11768
|
+
duplicate: false,
|
|
11769
|
+
recovered: true,
|
|
11770
|
+
stale: false,
|
|
11771
|
+
previousApproval,
|
|
11772
|
+
recorded
|
|
10840
11773
|
};
|
|
10841
|
-
if (!previousCallbackMessageId) {
|
|
10842
|
-
saveState(statePath, recoveryConversation);
|
|
10843
11774
|
}
|
|
10844
|
-
const recorded = onRecorded?.(recoveryConversation, {
|
|
10845
|
-
recoverMissingOutbox: true
|
|
10846
|
-
});
|
|
10847
|
-
appendEvent(logPath, {
|
|
10848
|
-
ts: cliNow().toISOString(),
|
|
10849
|
-
conversation_id: recoveryConversation.conversation_id,
|
|
10850
|
-
event: "terminal_bridge_approval_notification_outbox_recovered",
|
|
10851
|
-
terminal_control: terminalControl,
|
|
10852
|
-
fingerprint,
|
|
10853
|
-
callback_message_id: recoveryMessageId
|
|
10854
|
-
});
|
|
10855
11775
|
return {
|
|
10856
|
-
conversation
|
|
10857
|
-
|
|
10858
|
-
: recoveryConversation,
|
|
10859
|
-
duplicate: false,
|
|
10860
|
-
recovered: true,
|
|
11776
|
+
conversation,
|
|
11777
|
+
duplicate: true,
|
|
10861
11778
|
stale: false,
|
|
10862
11779
|
previousApproval,
|
|
10863
|
-
recorded
|
|
11780
|
+
recorded: undefined
|
|
10864
11781
|
};
|
|
10865
11782
|
}
|
|
11783
|
+
const now = cliNow().toISOString();
|
|
11784
|
+
const callbackMessageId = `msg-${randomUUID()}`;
|
|
11785
|
+
const nextConversation = {
|
|
11786
|
+
...conversation,
|
|
11787
|
+
native_session_takeover: {
|
|
11788
|
+
...nativeTakeover,
|
|
11789
|
+
terminal_bridge_approval: {
|
|
11790
|
+
fingerprint,
|
|
11791
|
+
screen_digest: approvalScreenDigest,
|
|
11792
|
+
notified_at: now,
|
|
11793
|
+
terminal_control: terminalControl,
|
|
11794
|
+
approval_state: terminalStatus.approval_state,
|
|
11795
|
+
callback_message_id: callbackMessageId,
|
|
11796
|
+
callback_message_ts: now
|
|
11797
|
+
}
|
|
11798
|
+
},
|
|
11799
|
+
updated_at: now
|
|
11800
|
+
};
|
|
11801
|
+
// Persist the stable callback identity before any message/outbox event. If
|
|
11802
|
+
// the process exits during callback preparation, recovery can recreate the
|
|
11803
|
+
// exact same message and safely finish the outbox transaction.
|
|
11804
|
+
saveState(statePath, nextConversation);
|
|
11805
|
+
appendEvent(logPath, {
|
|
11806
|
+
ts: now,
|
|
11807
|
+
conversation_id: conversation.conversation_id,
|
|
11808
|
+
event: "terminal_bridge_approval_notification_recorded",
|
|
11809
|
+
terminal_control: terminalControl,
|
|
11810
|
+
fingerprint,
|
|
11811
|
+
screen_digest: approvalScreenDigest
|
|
11812
|
+
});
|
|
11813
|
+
const recorded = onRecorded?.(nextConversation);
|
|
10866
11814
|
return {
|
|
10867
|
-
conversation
|
|
10868
|
-
|
|
11815
|
+
conversation: isRecord(recorded) && isRecord(recorded.prepared)
|
|
11816
|
+
? recorded.prepared.conversation
|
|
11817
|
+
: nextConversation,
|
|
11818
|
+
duplicate: false,
|
|
10869
11819
|
stale: false,
|
|
10870
|
-
|
|
10871
|
-
recorded: undefined
|
|
11820
|
+
recorded
|
|
10872
11821
|
};
|
|
10873
11822
|
}
|
|
10874
|
-
|
|
10875
|
-
|
|
10876
|
-
|
|
10877
|
-
|
|
10878
|
-
native_session_takeover: {
|
|
10879
|
-
...nativeTakeover,
|
|
10880
|
-
terminal_bridge_approval: {
|
|
10881
|
-
fingerprint,
|
|
10882
|
-
screen_digest: approvalScreenDigest,
|
|
10883
|
-
notified_at: now,
|
|
10884
|
-
terminal_control: terminalControl,
|
|
10885
|
-
approval_state: terminalStatus.approval_state,
|
|
10886
|
-
callback_message_id: callbackMessageId,
|
|
10887
|
-
callback_message_ts: now
|
|
10888
|
-
}
|
|
10889
|
-
},
|
|
10890
|
-
updated_at: now
|
|
10891
|
-
};
|
|
10892
|
-
// Persist the stable callback identity before any message/outbox event. If
|
|
10893
|
-
// the process exits during callback preparation, recovery can recreate the
|
|
10894
|
-
// exact same message and safely finish the outbox transaction.
|
|
10895
|
-
saveState(statePath, nextConversation);
|
|
10896
|
-
appendEvent(logPath, {
|
|
10897
|
-
ts: now,
|
|
10898
|
-
conversation_id: conversation.conversation_id,
|
|
10899
|
-
event: "terminal_bridge_approval_notification_recorded",
|
|
10900
|
-
terminal_control: terminalControl,
|
|
10901
|
-
fingerprint,
|
|
10902
|
-
screen_digest: approvalScreenDigest
|
|
10903
|
-
});
|
|
10904
|
-
const recorded = onRecorded?.(nextConversation);
|
|
10905
|
-
return {
|
|
10906
|
-
conversation: isRecord(recorded) && isRecord(recorded.prepared)
|
|
10907
|
-
? recorded.prepared.conversation
|
|
10908
|
-
: nextConversation,
|
|
10909
|
-
duplicate: false,
|
|
10910
|
-
stale: false,
|
|
10911
|
-
recorded
|
|
10912
|
-
};
|
|
10913
|
-
}
|
|
10914
|
-
finally {
|
|
10915
|
-
releaseLock();
|
|
10916
|
-
}
|
|
11823
|
+
finally {
|
|
11824
|
+
releaseLock();
|
|
11825
|
+
}
|
|
11826
|
+
});
|
|
10917
11827
|
}
|
|
10918
11828
|
function markTerminalBridgeApprovalPromptCleared({ statePath, logPath, expectedConversationId, expectedMessageId }) {
|
|
10919
|
-
const
|
|
10920
|
-
|
|
10921
|
-
const
|
|
10922
|
-
|
|
10923
|
-
|
|
10924
|
-
|
|
10925
|
-
|
|
10926
|
-
|
|
10927
|
-
|
|
10928
|
-
|
|
10929
|
-
|
|
10930
|
-
|
|
10931
|
-
|
|
10932
|
-
|
|
10933
|
-
|
|
11829
|
+
const storeDir = pathsForConversationDir(path.dirname(statePath)).storeDir;
|
|
11830
|
+
return withStoreWriterLease(storeDir, () => {
|
|
11831
|
+
const releaseLock = acquireFileLock(`${statePath}.lock`);
|
|
11832
|
+
try {
|
|
11833
|
+
const conversation = loadState(statePath);
|
|
11834
|
+
const nativeTakeover = isRecord(conversation.native_session_takeover)
|
|
11835
|
+
? conversation.native_session_takeover
|
|
11836
|
+
: undefined;
|
|
11837
|
+
if (conversation.conversation_id !== expectedConversationId ||
|
|
11838
|
+
conversation.status !== "waiting_for_agent" ||
|
|
11839
|
+
nativeTakeover?.terminal_bridge !== true ||
|
|
11840
|
+
stringValue(nativeTakeover.terminal_bridge_message_id) !==
|
|
11841
|
+
expectedMessageId ||
|
|
11842
|
+
stringValue(nativeTakeover.terminal_bridge_last_approval_message_id) !==
|
|
11843
|
+
expectedMessageId ||
|
|
11844
|
+
validTimestampMs(nativeTakeover.terminal_bridge_approval_resolved_at) === undefined ||
|
|
11845
|
+
validTimestampMs(nativeTakeover.terminal_bridge_last_approval_prompt_cleared_at) !== undefined) {
|
|
11846
|
+
return {
|
|
11847
|
+
conversation,
|
|
11848
|
+
marked: false
|
|
11849
|
+
};
|
|
11850
|
+
}
|
|
11851
|
+
const clearedAt = cliNow().toISOString();
|
|
11852
|
+
const nextConversation = {
|
|
11853
|
+
...conversation,
|
|
11854
|
+
native_session_takeover: {
|
|
11855
|
+
...nativeTakeover,
|
|
11856
|
+
terminal_bridge_last_approval_prompt_cleared_at: clearedAt
|
|
11857
|
+
},
|
|
11858
|
+
updated_at: clearedAt
|
|
11859
|
+
};
|
|
11860
|
+
saveState(statePath, nextConversation);
|
|
11861
|
+
appendEvent(logPath, {
|
|
11862
|
+
ts: clearedAt,
|
|
11863
|
+
conversation_id: conversation.conversation_id,
|
|
11864
|
+
event: "terminal_bridge_approval_prompt_cleared",
|
|
11865
|
+
terminal_bridge_message_id: expectedMessageId
|
|
11866
|
+
});
|
|
10934
11867
|
return {
|
|
10935
|
-
conversation,
|
|
10936
|
-
marked:
|
|
11868
|
+
conversation: nextConversation,
|
|
11869
|
+
marked: true
|
|
10937
11870
|
};
|
|
10938
11871
|
}
|
|
10939
|
-
|
|
10940
|
-
|
|
10941
|
-
|
|
10942
|
-
|
|
10943
|
-
...nativeTakeover,
|
|
10944
|
-
terminal_bridge_last_approval_prompt_cleared_at: clearedAt
|
|
10945
|
-
},
|
|
10946
|
-
updated_at: clearedAt
|
|
10947
|
-
};
|
|
10948
|
-
saveState(statePath, nextConversation);
|
|
10949
|
-
appendEvent(logPath, {
|
|
10950
|
-
ts: clearedAt,
|
|
10951
|
-
conversation_id: conversation.conversation_id,
|
|
10952
|
-
event: "terminal_bridge_approval_prompt_cleared",
|
|
10953
|
-
terminal_bridge_message_id: expectedMessageId
|
|
10954
|
-
});
|
|
10955
|
-
return {
|
|
10956
|
-
conversation: nextConversation,
|
|
10957
|
-
marked: true
|
|
10958
|
-
};
|
|
10959
|
-
}
|
|
10960
|
-
finally {
|
|
10961
|
-
releaseLock();
|
|
10962
|
-
}
|
|
11872
|
+
finally {
|
|
11873
|
+
releaseLock();
|
|
11874
|
+
}
|
|
11875
|
+
});
|
|
10963
11876
|
}
|
|
10964
11877
|
function terminalBridgeApprovalCallbackIdentity(conversation) {
|
|
10965
11878
|
const nativeTakeover = isRecord(conversation?.native_session_takeover)
|
|
@@ -11126,7 +12039,28 @@ async function runSend(options) {
|
|
|
11126
12039
|
allowedCompanionIdentity: knownCodexCompanions.primary,
|
|
11127
12040
|
allowedAdditionalIdentities: knownCodexCompanions.additional
|
|
11128
12041
|
});
|
|
11129
|
-
|
|
12042
|
+
let deferredCodexCandidateInventory;
|
|
12043
|
+
if (terminalConversation.agent === "codex" &&
|
|
12044
|
+
stringValue(options.expectedTerminalToken)) {
|
|
12045
|
+
try {
|
|
12046
|
+
const inventory = await inspectCodexOpenRootRolloutInventory({
|
|
12047
|
+
options,
|
|
12048
|
+
pid: terminalConversation.pid,
|
|
12049
|
+
cwd: terminalConversation.terminalControl.currentPath
|
|
12050
|
+
});
|
|
12051
|
+
if (inventory.roots.length > 0) {
|
|
12052
|
+
deferredCodexCandidateInventory = inventory;
|
|
12053
|
+
}
|
|
12054
|
+
}
|
|
12055
|
+
catch (error) {
|
|
12056
|
+
if (nativeIdentityObservation.status === "unavailable") {
|
|
12057
|
+
throw new Error(`native Codex identity observation is unavailable and its exact ` +
|
|
12058
|
+
`open-root inventory could not be proven: ${error instanceof Error ? error.message : String(error)}`);
|
|
12059
|
+
}
|
|
12060
|
+
}
|
|
12061
|
+
}
|
|
12062
|
+
if (nativeIdentityObservation.status === "unavailable" &&
|
|
12063
|
+
!deferredCodexCandidateInventory) {
|
|
11130
12064
|
throw new Error(`native ${terminalConversation.agent} identity observation is ` +
|
|
11131
12065
|
`unavailable: ${nativeIdentityObservation.reason}`);
|
|
11132
12066
|
}
|
|
@@ -11166,6 +12100,7 @@ async function runSend(options) {
|
|
|
11166
12100
|
terminal: terminalConversation,
|
|
11167
12101
|
sourceSession: claimedSession,
|
|
11168
12102
|
observation: nativeIdentityObservation,
|
|
12103
|
+
candidateInventory: deferredCodexCandidateInventory,
|
|
11169
12104
|
requestText: String(messageBody)
|
|
11170
12105
|
})
|
|
11171
12106
|
: undefined;
|
|
@@ -11762,12 +12697,16 @@ async function runTurnResponse({ options, messageBody }) {
|
|
|
11762
12697
|
async function runApprove(options) {
|
|
11763
12698
|
const terminalConversation = await resolveTerminalConversationFromOptions(options);
|
|
11764
12699
|
if (terminalConversation) {
|
|
12700
|
+
assertExpectedHandoffTokenUsesExactTerminalSelector({
|
|
12701
|
+
options,
|
|
12702
|
+
terminal: terminalConversation
|
|
12703
|
+
});
|
|
12704
|
+
if (options.autoApproved === true) {
|
|
12705
|
+
throw new Error("automatic approval requires an exact managed Turn and cannot use a raw terminal selector");
|
|
12706
|
+
}
|
|
11765
12707
|
await runTerminalConversationApprove({
|
|
11766
12708
|
options,
|
|
11767
|
-
|
|
11768
|
-
agent: terminalConversation.agent,
|
|
11769
|
-
terminalControl: terminalConversation.terminalControl,
|
|
11770
|
-
pid: terminalConversation.pid
|
|
12709
|
+
terminal: terminalConversation
|
|
11771
12710
|
});
|
|
11772
12711
|
return;
|
|
11773
12712
|
}
|
|
@@ -12276,8 +13215,10 @@ async function runApprove(options) {
|
|
|
12276
13215
|
}
|
|
12277
13216
|
}
|
|
12278
13217
|
}
|
|
12279
|
-
async function runTerminalConversationApprove({ options,
|
|
12280
|
-
const
|
|
13218
|
+
async function runTerminalConversationApprove({ options, terminal }) {
|
|
13219
|
+
const { conversationId, agent, terminalControl, pid } = terminal;
|
|
13220
|
+
const storeDir = storeDirFromOptions(options);
|
|
13221
|
+
const releaseTerminalLock = acquireTerminalBridgeSendLock(storeDir, terminalControl, { timeoutMs: 30000 });
|
|
12281
13222
|
try {
|
|
12282
13223
|
if (agent === "claude") {
|
|
12283
13224
|
printJson({
|
|
@@ -12290,16 +13231,74 @@ async function runTerminalConversationApprove({ options, conversationId, agent,
|
|
|
12290
13231
|
});
|
|
12291
13232
|
return;
|
|
12292
13233
|
}
|
|
12293
|
-
const
|
|
13234
|
+
const suppliedTerminalToken = stringValue(options.expectedTerminalToken);
|
|
13235
|
+
const initialResolution = await resolveTerminalScopedCodexApproval({
|
|
13236
|
+
options,
|
|
13237
|
+
terminal
|
|
13238
|
+
});
|
|
13239
|
+
if (initialResolution.state === "blocked") {
|
|
13240
|
+
throw new Error(initialResolution.reason);
|
|
13241
|
+
}
|
|
13242
|
+
if (initialResolution.state === "unmanaged" && suppliedTerminalToken) {
|
|
13243
|
+
throw new Error("--expected-terminal-token does not match an advertised terminal-scoped Codex approval");
|
|
13244
|
+
}
|
|
13245
|
+
if (initialResolution.state === "eligible" &&
|
|
13246
|
+
suppliedTerminalToken !== initialResolution.boundary.token) {
|
|
13247
|
+
throw new Error("terminal-scoped Codex approval token is missing or stale; refresh AKK list");
|
|
13248
|
+
}
|
|
13249
|
+
const runtime = {
|
|
13250
|
+
pid,
|
|
13251
|
+
cwd: terminalControl.currentPath,
|
|
13252
|
+
conversationId,
|
|
13253
|
+
terminalTarget: terminalControl.target
|
|
13254
|
+
};
|
|
13255
|
+
const approveCurrentPrompt = async (terminalScoped) => createTerminalAgentBridge(options).approve(agent, terminalControl, {
|
|
12294
13256
|
expectedFingerprint: stringValue(options.expectedApprovalFingerprint),
|
|
12295
13257
|
scrollbackLines: Number(options.scrollbackLines ?? 120),
|
|
12296
|
-
runtime
|
|
12297
|
-
|
|
12298
|
-
|
|
12299
|
-
|
|
12300
|
-
|
|
12301
|
-
|
|
13258
|
+
runtime,
|
|
13259
|
+
beforeKeyDispatch: terminalScoped
|
|
13260
|
+
? async (context) => {
|
|
13261
|
+
const approvalSnapshot = terminalScopedCodexApprovalPromptSnapshot({
|
|
13262
|
+
approvable: true,
|
|
13263
|
+
fingerprint: context.fingerprint,
|
|
13264
|
+
keys: context.keys,
|
|
13265
|
+
decision_mode: context.inspection.approval.approvable
|
|
13266
|
+
? context.inspection.approval.action.mode ?? "keys"
|
|
13267
|
+
: undefined,
|
|
13268
|
+
request_id: context.inspection.approval.approvable
|
|
13269
|
+
? context.inspection.approval.action.requestId
|
|
13270
|
+
: undefined
|
|
13271
|
+
});
|
|
13272
|
+
const current = await resolveTerminalScopedCodexApproval({
|
|
13273
|
+
options,
|
|
13274
|
+
terminal,
|
|
13275
|
+
approvalSnapshot
|
|
13276
|
+
});
|
|
13277
|
+
if (current.state !== "eligible" ||
|
|
13278
|
+
current.boundary.token !== suppliedTerminalToken) {
|
|
13279
|
+
throw new Error(current.state === "blocked"
|
|
13280
|
+
? current.reason
|
|
13281
|
+
: "terminal-scoped Codex approval authority changed before key dispatch");
|
|
13282
|
+
}
|
|
13283
|
+
}
|
|
13284
|
+
: undefined
|
|
12302
13285
|
});
|
|
13286
|
+
const terminalScoped = initialResolution.state === "eligible";
|
|
13287
|
+
const approval = terminalScoped
|
|
13288
|
+
? await withStoreWriterLeaseAsync(storeDir, async () => {
|
|
13289
|
+
const current = await resolveTerminalScopedCodexApproval({
|
|
13290
|
+
options,
|
|
13291
|
+
terminal
|
|
13292
|
+
});
|
|
13293
|
+
if (current.state !== "eligible" ||
|
|
13294
|
+
current.boundary.token !== suppliedTerminalToken) {
|
|
13295
|
+
throw new Error(current.state === "blocked"
|
|
13296
|
+
? current.reason
|
|
13297
|
+
: "terminal-scoped Codex approval authority changed while waiting for Store control");
|
|
13298
|
+
}
|
|
13299
|
+
return approveCurrentPrompt(true);
|
|
13300
|
+
})
|
|
13301
|
+
: await approveCurrentPrompt(false);
|
|
12303
13302
|
if (!approval.approved) {
|
|
12304
13303
|
printJson({
|
|
12305
13304
|
conversation_id: conversationId,
|
|
@@ -12332,7 +13331,14 @@ async function runTerminalConversationApprove({ options, conversationId, agent,
|
|
|
12332
13331
|
label: approval.label,
|
|
12333
13332
|
approval_fingerprint: approval.fingerprint,
|
|
12334
13333
|
decision_mode: approval.decisionMode,
|
|
12335
|
-
request_id: approval.requestId
|
|
13334
|
+
request_id: approval.requestId,
|
|
13335
|
+
terminal_scoped: terminalScoped,
|
|
13336
|
+
...(terminalScoped
|
|
13337
|
+
? {
|
|
13338
|
+
durable_dispatch_receipt: false,
|
|
13339
|
+
uncertain_outcome_recovery: "refresh status and inspect the live prompt; do not retry blindly"
|
|
13340
|
+
}
|
|
13341
|
+
: {})
|
|
12336
13342
|
});
|
|
12337
13343
|
}
|
|
12338
13344
|
finally {
|
|
@@ -12570,17 +13576,20 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
12570
13576
|
: undefined;
|
|
12571
13577
|
const terminalAgentPid = Number(sendTakeover?.terminal_agent_pid);
|
|
12572
13578
|
const expectedManagedNativeThreadId = stringValue(sendTakeover?.terminal_agent_expected_session_id) ?? stringValue(sendTakeover?.terminal_agent_session_id);
|
|
12573
|
-
const currentNativeIdentity =
|
|
12574
|
-
|
|
12575
|
-
|
|
12576
|
-
|
|
12577
|
-
|
|
12578
|
-
|
|
12579
|
-
|
|
12580
|
-
:
|
|
12581
|
-
|
|
12582
|
-
|
|
12583
|
-
|
|
13579
|
+
const currentNativeIdentity = deferredCodexForegroundBinding
|
|
13580
|
+
?.candidateAcceptanceAnchor
|
|
13581
|
+
? undefined
|
|
13582
|
+
: await resolveCurrentNativeAgentSessionIdentity({
|
|
13583
|
+
options,
|
|
13584
|
+
agent: executor.kind,
|
|
13585
|
+
pid: terminalAgentPid,
|
|
13586
|
+
cwd: terminalControl.currentPath,
|
|
13587
|
+
preferredSessionId: allowedPreMaterializationIdentity
|
|
13588
|
+
? expectedManagedNativeThreadId
|
|
13589
|
+
: undefined,
|
|
13590
|
+
allowedCompanionIdentity: allowedPreMaterializationIdentity,
|
|
13591
|
+
allowedAdditionalIdentities
|
|
13592
|
+
});
|
|
12584
13593
|
const virginRawAttach = Boolean(recordRawAttachmentAfterSend &&
|
|
12585
13594
|
!stringValue(sendTakeover?.terminal_agent_session_id));
|
|
12586
13595
|
const pendingManagedNativeBinding = Boolean(!stringValue(sendTakeover?.terminal_agent_session_id) &&
|
|
@@ -12600,7 +13609,12 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
12600
13609
|
});
|
|
12601
13610
|
}
|
|
12602
13611
|
const preSendRuntime = {
|
|
12603
|
-
...
|
|
13612
|
+
...(deferredCodexForegroundBinding?.candidateAcceptanceAnchor
|
|
13613
|
+
? terminalRuntimeForLiveIdentity({
|
|
13614
|
+
terminal: deferredCodexForegroundBinding.terminal,
|
|
13615
|
+
physicalOnly: true
|
|
13616
|
+
})
|
|
13617
|
+
: terminalRuntimeIdentityForConversation(nextConversation, terminalControl)),
|
|
12604
13618
|
allowedPreMaterializationNativeIdentity: allowedPreMaterializationIdentity,
|
|
12605
13619
|
allowedAdditionalNativeIdentities: allowedAdditionalIdentities,
|
|
12606
13620
|
messageId: message.id
|
|
@@ -12661,7 +13675,8 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
12661
13675
|
boundProcessUuid: stringValue(sendTakeover?.terminal_agent_process_uuid),
|
|
12662
13676
|
boundProcessBirth: stringValue(sendTakeover?.terminal_agent_process_birth),
|
|
12663
13677
|
allowedPreMaterializationIdentity,
|
|
12664
|
-
needsPostSendNativeBinding
|
|
13678
|
+
needsPostSendNativeBinding,
|
|
13679
|
+
candidateSetAnchor: deferredCodexForegroundBinding?.candidateAcceptanceAnchor
|
|
12665
13680
|
});
|
|
12666
13681
|
}
|
|
12667
13682
|
else {
|
|
@@ -13196,6 +14211,7 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
13196
14211
|
cliExit(86);
|
|
13197
14212
|
}
|
|
13198
14213
|
let submittedBase = stagedConversation;
|
|
14214
|
+
let deferredCandidateBindingPending = false;
|
|
13199
14215
|
if (needsPostSendNativeBinding) {
|
|
13200
14216
|
let boundIdentity;
|
|
13201
14217
|
let boundConversation;
|
|
@@ -13203,7 +14219,7 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
13203
14219
|
try {
|
|
13204
14220
|
if (deferredCodexForegroundBinding &&
|
|
13205
14221
|
!allowsSyntheticTerminalAcceptance(options) &&
|
|
13206
|
-
codexRolloutAcceptanceAnchor?.version
|
|
14222
|
+
![2, 3].includes(Number(codexRolloutAcceptanceAnchor?.version))) {
|
|
13207
14223
|
throw new Error("deferred Codex foreground binding requires a virgin rollout acceptance anchor");
|
|
13208
14224
|
}
|
|
13209
14225
|
boundIdentity = await pollNativeAgentSessionIdentity({
|
|
@@ -13215,7 +14231,8 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
13215
14231
|
allowedPreMaterializationIdentity,
|
|
13216
14232
|
allowedAdditionalIdentities,
|
|
13217
14233
|
...(deferredCodexForegroundBinding &&
|
|
13218
|
-
codexRolloutAcceptanceAnchor
|
|
14234
|
+
codexRolloutAcceptanceAnchor &&
|
|
14235
|
+
[2, 3].includes(codexRolloutAcceptanceAnchor.version)
|
|
13219
14236
|
? {
|
|
13220
14237
|
requiredCodexAcceptance: {
|
|
13221
14238
|
anchor: codexRolloutAcceptanceAnchor,
|
|
@@ -13342,138 +14359,154 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
13342
14359
|
}
|
|
13343
14360
|
}
|
|
13344
14361
|
if (!boundIdentity) {
|
|
13345
|
-
|
|
13346
|
-
|
|
13347
|
-
|
|
13348
|
-
|
|
13349
|
-
|
|
13350
|
-
|
|
13351
|
-
|
|
13352
|
-
|
|
13353
|
-
|
|
13354
|
-
|
|
13355
|
-
|
|
14362
|
+
if (deferredCodexForegroundBinding &&
|
|
14363
|
+
codexRolloutAcceptanceAnchor?.version === 3 &&
|
|
14364
|
+
bindingError === undefined) {
|
|
14365
|
+
// The exact candidate inventory is still stable but Codex has not
|
|
14366
|
+
// materialized the request in any root yet. Enter is durable; keep
|
|
14367
|
+
// the transfer fenced at dispatch_started and let the monitor or a
|
|
14368
|
+
// later dedicated recovery finish the UUID binding without replay.
|
|
14369
|
+
deferredCandidateBindingPending = true;
|
|
14370
|
+
submittedBase = stagedConversation;
|
|
14371
|
+
}
|
|
14372
|
+
else {
|
|
14373
|
+
const bindingFailedAt = cliNow().toISOString();
|
|
14374
|
+
const bindingReason = bindingError
|
|
14375
|
+
? `AKK dispatched terminal input but could not verify the new native agent session: ${bindingError}`
|
|
14376
|
+
: "AKK dispatched terminal input but no exact native agent session appeared within the binding window";
|
|
14377
|
+
if (deferredCodexForegroundBinding) {
|
|
14378
|
+
try {
|
|
14379
|
+
markDeferredCodexForegroundTransferUncertain({
|
|
14380
|
+
options,
|
|
14381
|
+
boundary: deferredCodexForegroundBinding,
|
|
14382
|
+
reason: bindingReason
|
|
14383
|
+
});
|
|
14384
|
+
}
|
|
14385
|
+
catch (error) {
|
|
14386
|
+
runtimeLog("error", "deferred_codex_foreground_uncertain_persist_failed", {
|
|
14387
|
+
transfer_id: deferredCodexForegroundBinding.transferId,
|
|
14388
|
+
error: error instanceof Error ? error.message : String(error)
|
|
14389
|
+
});
|
|
14390
|
+
}
|
|
13356
14391
|
}
|
|
13357
|
-
|
|
13358
|
-
|
|
13359
|
-
|
|
13360
|
-
|
|
14392
|
+
const unfencedBase = {
|
|
14393
|
+
...stagedConversation,
|
|
14394
|
+
status: "stalled",
|
|
14395
|
+
stalled_at: bindingFailedAt,
|
|
14396
|
+
stalled_reason: bindingReason,
|
|
14397
|
+
native_session_takeover: {
|
|
14398
|
+
...(isRecord(stagedConversation.native_session_takeover)
|
|
14399
|
+
? stagedConversation.native_session_takeover
|
|
14400
|
+
: {}),
|
|
14401
|
+
terminal_agent_identity_status: "unresolved_after_submit",
|
|
14402
|
+
terminal_agent_identity_error: textSummary(bindingReason)
|
|
14403
|
+
},
|
|
14404
|
+
updated_at: bindingFailedAt
|
|
14405
|
+
};
|
|
14406
|
+
const unfencedConversation = withTerminalBridgeSubmission({
|
|
14407
|
+
conversation: unfencedBase,
|
|
14408
|
+
messageId: message.id,
|
|
14409
|
+
messageType: message.type,
|
|
14410
|
+
messageBody: String(message.body),
|
|
14411
|
+
requestText: terminalPayload,
|
|
14412
|
+
status: "uncertain",
|
|
14413
|
+
preparedAt: bridgeStartedAt,
|
|
14414
|
+
textInjectedAt,
|
|
14415
|
+
enterDispatchedAt,
|
|
14416
|
+
uncertainAt: bindingFailedAt,
|
|
14417
|
+
error: bindingReason,
|
|
14418
|
+
lastProvenStage: "enter_dispatched"
|
|
14419
|
+
});
|
|
14420
|
+
if (!deferredCodexForegroundBinding) {
|
|
14421
|
+
quarantineManagedSessionBinding({
|
|
14422
|
+
conversation: unfencedConversation,
|
|
14423
|
+
reason: bindingReason
|
|
13361
14424
|
});
|
|
13362
14425
|
}
|
|
13363
|
-
|
|
13364
|
-
|
|
13365
|
-
|
|
13366
|
-
|
|
13367
|
-
|
|
13368
|
-
|
|
13369
|
-
|
|
13370
|
-
|
|
13371
|
-
|
|
13372
|
-
|
|
13373
|
-
|
|
13374
|
-
|
|
13375
|
-
|
|
13376
|
-
|
|
13377
|
-
|
|
13378
|
-
|
|
13379
|
-
|
|
13380
|
-
|
|
13381
|
-
|
|
13382
|
-
|
|
13383
|
-
|
|
13384
|
-
|
|
13385
|
-
preparedAt: bridgeStartedAt,
|
|
13386
|
-
textInjectedAt,
|
|
13387
|
-
enterDispatchedAt,
|
|
13388
|
-
uncertainAt: bindingFailedAt,
|
|
13389
|
-
error: bindingReason,
|
|
13390
|
-
lastProvenStage: "enter_dispatched"
|
|
13391
|
-
});
|
|
13392
|
-
if (!deferredCodexForegroundBinding) {
|
|
13393
|
-
quarantineManagedSessionBinding({
|
|
13394
|
-
conversation: unfencedConversation,
|
|
13395
|
-
reason: bindingReason
|
|
14426
|
+
saveTerminalBridgeDispatchLedger(terminalControl, {
|
|
14427
|
+
...terminalBindingLedgerFields(unfencedConversation),
|
|
14428
|
+
status: "uncertain",
|
|
14429
|
+
generation_id: message.id,
|
|
14430
|
+
conversation_id: unfencedConversation.conversation_id,
|
|
14431
|
+
session_id: sessionIdForConversation(unfencedConversation),
|
|
14432
|
+
turn_id: turnIdForConversation(unfencedConversation),
|
|
14433
|
+
message_id: message.id,
|
|
14434
|
+
message_type: message.type,
|
|
14435
|
+
request_hash: terminalRequestHash,
|
|
14436
|
+
prepared_at: bridgeStartedAt,
|
|
14437
|
+
text_injected_at: textInjectedAt,
|
|
14438
|
+
enter_dispatched_at: enterDispatchedAt,
|
|
14439
|
+
uncertain_at: bindingFailedAt,
|
|
14440
|
+
dispatcher_pid: cliPid(),
|
|
14441
|
+
state_path: statePath,
|
|
14442
|
+
event_log_path: logPath,
|
|
14443
|
+
callback_expected: false,
|
|
14444
|
+
native_identity_status: "unresolved_after_submit",
|
|
14445
|
+
error: textSummary(bindingReason),
|
|
14446
|
+
previous_generation_id: stringValue(previousDispatchLedger?.generation_id) ??
|
|
14447
|
+
stringValue(previousDispatchLedger?.message_id)
|
|
13396
14448
|
});
|
|
13397
|
-
|
|
13398
|
-
|
|
13399
|
-
|
|
13400
|
-
|
|
13401
|
-
|
|
13402
|
-
|
|
13403
|
-
|
|
13404
|
-
|
|
13405
|
-
|
|
13406
|
-
|
|
13407
|
-
|
|
13408
|
-
|
|
13409
|
-
|
|
13410
|
-
enter_dispatched_at: enterDispatchedAt,
|
|
13411
|
-
uncertain_at: bindingFailedAt,
|
|
13412
|
-
dispatcher_pid: cliPid(),
|
|
13413
|
-
state_path: statePath,
|
|
13414
|
-
event_log_path: logPath,
|
|
13415
|
-
callback_expected: false,
|
|
13416
|
-
native_identity_status: "unresolved_after_submit",
|
|
13417
|
-
error: textSummary(bindingReason),
|
|
13418
|
-
previous_generation_id: stringValue(previousDispatchLedger?.generation_id) ??
|
|
13419
|
-
stringValue(previousDispatchLedger?.message_id)
|
|
13420
|
-
});
|
|
13421
|
-
saveState(statePath, unfencedConversation);
|
|
13422
|
-
appendEvent(logPath, {
|
|
13423
|
-
ts: bindingFailedAt,
|
|
13424
|
-
conversation_id: unfencedConversation.conversation_id,
|
|
13425
|
-
event: "terminal_agent_identity_binding_failed",
|
|
13426
|
-
message_id: message.id,
|
|
13427
|
-
executor,
|
|
13428
|
-
terminal_control: terminalControl,
|
|
13429
|
-
error: textSummary(bindingReason),
|
|
13430
|
-
delivered: false,
|
|
13431
|
-
do_not_retry: true
|
|
13432
|
-
});
|
|
13433
|
-
runtimeLog("error", "terminal_agent_identity_binding_failed", {
|
|
13434
|
-
conversation_id: unfencedConversation.conversation_id,
|
|
13435
|
-
agent: executor.kind,
|
|
13436
|
-
terminal_target: terminalControl.target,
|
|
13437
|
-
error: bindingReason,
|
|
13438
|
-
delivered: false,
|
|
13439
|
-
do_not_retry: true
|
|
13440
|
-
});
|
|
13441
|
-
printJson({
|
|
13442
|
-
session_id: sessionIdForConversation(unfencedConversation),
|
|
13443
|
-
turn_id: turnIdForConversation(unfencedConversation),
|
|
13444
|
-
conversation: unfencedConversation,
|
|
13445
|
-
message,
|
|
13446
|
-
delivered: false,
|
|
13447
|
-
status: "submission_uncertain",
|
|
13448
|
-
submission_outcome: "uncertain",
|
|
13449
|
-
background: true,
|
|
13450
|
-
callback_expected: false,
|
|
13451
|
-
terminal_control: terminalControl,
|
|
13452
|
-
monitor_pid: bridgeMonitor?.pid ?? null,
|
|
13453
|
-
executor,
|
|
13454
|
-
delivery_receipt: "enter_dispatched",
|
|
13455
|
-
do_not_retry: true,
|
|
13456
|
-
reason: bindingReason,
|
|
13457
|
-
openclaw_next_action: {
|
|
13458
|
-
action: "inspect",
|
|
14449
|
+
saveState(statePath, unfencedConversation);
|
|
14450
|
+
appendEvent(logPath, {
|
|
14451
|
+
ts: bindingFailedAt,
|
|
14452
|
+
conversation_id: unfencedConversation.conversation_id,
|
|
14453
|
+
event: "terminal_agent_identity_binding_failed",
|
|
14454
|
+
message_id: message.id,
|
|
14455
|
+
executor,
|
|
14456
|
+
terminal_control: terminalControl,
|
|
14457
|
+
error: textSummary(bindingReason),
|
|
14458
|
+
delivered: false,
|
|
14459
|
+
do_not_retry: true
|
|
14460
|
+
});
|
|
14461
|
+
runtimeLog("error", "terminal_agent_identity_binding_failed", {
|
|
13459
14462
|
conversation_id: unfencedConversation.conversation_id,
|
|
14463
|
+
agent: executor.kind,
|
|
14464
|
+
terminal_target: terminalControl.target,
|
|
14465
|
+
error: bindingReason,
|
|
14466
|
+
delivered: false,
|
|
14467
|
+
do_not_retry: true
|
|
14468
|
+
});
|
|
14469
|
+
printJson({
|
|
13460
14470
|
session_id: sessionIdForConversation(unfencedConversation),
|
|
13461
14471
|
turn_id: turnIdForConversation(unfencedConversation),
|
|
14472
|
+
conversation: unfencedConversation,
|
|
14473
|
+
message,
|
|
14474
|
+
delivered: false,
|
|
14475
|
+
status: "submission_uncertain",
|
|
14476
|
+
submission_outcome: "uncertain",
|
|
14477
|
+
background: true,
|
|
14478
|
+
callback_expected: false,
|
|
14479
|
+
terminal_control: terminalControl,
|
|
14480
|
+
monitor_pid: bridgeMonitor?.pid ?? null,
|
|
14481
|
+
executor,
|
|
14482
|
+
delivery_receipt: "enter_dispatched",
|
|
13462
14483
|
do_not_retry: true,
|
|
13463
|
-
reason:
|
|
13464
|
-
|
|
13465
|
-
|
|
13466
|
-
|
|
14484
|
+
reason: bindingReason,
|
|
14485
|
+
openclaw_next_action: {
|
|
14486
|
+
action: "inspect",
|
|
14487
|
+
conversation_id: unfencedConversation.conversation_id,
|
|
14488
|
+
session_id: sessionIdForConversation(unfencedConversation),
|
|
14489
|
+
turn_id: turnIdForConversation(unfencedConversation),
|
|
14490
|
+
do_not_retry: true,
|
|
14491
|
+
reason: "The input was submitted, but AKK could not fence later side effects to an exact native session. Inspect the pane and close this Turn before continuing."
|
|
14492
|
+
}
|
|
14493
|
+
});
|
|
14494
|
+
return;
|
|
14495
|
+
}
|
|
14496
|
+
}
|
|
14497
|
+
if (!deferredCandidateBindingPending) {
|
|
14498
|
+
submittedBase = boundConversation;
|
|
13467
14499
|
}
|
|
13468
|
-
submittedBase = boundConversation;
|
|
13469
14500
|
}
|
|
13470
|
-
acceptanceResult =
|
|
13471
|
-
|
|
13472
|
-
|
|
13473
|
-
|
|
13474
|
-
|
|
13475
|
-
|
|
13476
|
-
|
|
14501
|
+
acceptanceResult = deferredCandidateBindingPending
|
|
14502
|
+
? { outcome: "pending_acceptance" }
|
|
14503
|
+
: await pollTerminalSubmissionAcceptance({
|
|
14504
|
+
options,
|
|
14505
|
+
executor: executor.kind,
|
|
14506
|
+
conversation: submittedBase,
|
|
14507
|
+
terminalControl,
|
|
14508
|
+
terminalBridge
|
|
14509
|
+
});
|
|
13477
14510
|
const acceptanceResolvedAt = cliNow().toISOString();
|
|
13478
14511
|
const terminalStatus = acceptanceResult.outcome === "pending_acceptance"
|
|
13479
14512
|
? "enter_dispatched"
|
|
@@ -14031,10 +15064,13 @@ function syntheticTerminalAcceptanceEvidence({ options, executor, conversation,
|
|
|
14031
15064
|
.digest("hex")
|
|
14032
15065
|
};
|
|
14033
15066
|
}
|
|
14034
|
-
function captureCodexAcceptanceAnchorForSend({ options, currentIdentity, expectedNativeThreadId, boundProcessUuid, boundProcessBirth, allowedPreMaterializationIdentity, needsPostSendNativeBinding }) {
|
|
15067
|
+
function captureCodexAcceptanceAnchorForSend({ options, currentIdentity, expectedNativeThreadId, boundProcessUuid, boundProcessBirth, allowedPreMaterializationIdentity, needsPostSendNativeBinding, candidateSetAnchor }) {
|
|
14035
15068
|
if (allowsSyntheticTerminalAcceptance(options)) {
|
|
14036
15069
|
return undefined;
|
|
14037
15070
|
}
|
|
15071
|
+
if (candidateSetAnchor) {
|
|
15072
|
+
return candidateSetAnchor;
|
|
15073
|
+
}
|
|
14038
15074
|
const nativeThreadId = needsPostSendNativeBinding
|
|
14039
15075
|
? expectedNativeThreadId
|
|
14040
15076
|
: currentIdentity?.sessionId;
|
|
@@ -14319,6 +15355,27 @@ async function detectTerminalSubmissionAcceptance({ options, executor, conversat
|
|
|
14319
15355
|
throw new Error("Codex rollout acceptance anchor is unavailable");
|
|
14320
15356
|
}
|
|
14321
15357
|
const pid = Number(nativeTakeover?.terminal_agent_pid);
|
|
15358
|
+
const validatedAnchor = validateCodexRolloutAcceptanceAnchor(anchor);
|
|
15359
|
+
if (validatedAnchor.version === 3) {
|
|
15360
|
+
const inventory = await inspectCodexOpenRootRolloutInventory({
|
|
15361
|
+
options,
|
|
15362
|
+
pid,
|
|
15363
|
+
cwd: terminalControl.currentPath
|
|
15364
|
+
});
|
|
15365
|
+
const result = detectCodexCandidateSetRolloutAcceptance({
|
|
15366
|
+
anchor: validatedAnchor,
|
|
15367
|
+
currentInventory: inventory,
|
|
15368
|
+
requestHash
|
|
15369
|
+
});
|
|
15370
|
+
if (result.status === "accepted") {
|
|
15371
|
+
return result.evidence;
|
|
15372
|
+
}
|
|
15373
|
+
if (result.status === "uncertain") {
|
|
15374
|
+
throw new Error(`Codex candidate-set acceptance is uncertain (${result.code}): ` +
|
|
15375
|
+
result.reason);
|
|
15376
|
+
}
|
|
15377
|
+
return undefined;
|
|
15378
|
+
}
|
|
14322
15379
|
const currentIdentity = await resolveCurrentNativeAgentSessionIdentity({
|
|
14323
15380
|
options,
|
|
14324
15381
|
agent: "codex",
|
|
@@ -14330,7 +15387,7 @@ async function detectTerminalSubmissionAcceptance({ options, executor, conversat
|
|
|
14330
15387
|
return undefined;
|
|
14331
15388
|
}
|
|
14332
15389
|
return detectCodexRolloutAcceptance({
|
|
14333
|
-
anchor:
|
|
15390
|
+
anchor: validatedAnchor,
|
|
14334
15391
|
currentIdentity,
|
|
14335
15392
|
requestHash
|
|
14336
15393
|
});
|
|
@@ -14401,6 +15458,90 @@ async function pollTerminalSubmissionAcceptance({ options, executor, conversatio
|
|
|
14401
15458
|
return { outcome: "pending_acceptance" };
|
|
14402
15459
|
}
|
|
14403
15460
|
async function reconcileTerminalAcceptanceInMonitor({ options, conversation, statePath, logPath, terminalControl, executor, terminalBridge }) {
|
|
15461
|
+
const initialTakeover = isRecord(conversation.native_session_takeover)
|
|
15462
|
+
? conversation.native_session_takeover
|
|
15463
|
+
: undefined;
|
|
15464
|
+
const deferredTransferId = stringValue(initialTakeover?.deferred_foreground_transfer_id);
|
|
15465
|
+
if (deferredTransferId) {
|
|
15466
|
+
const transferStoreDir = pathsForConversationDir(path.dirname(statePath)).storeDir;
|
|
15467
|
+
let transfer = loadDeferredForegroundTransfer(transferStoreDir, deferredTransferId);
|
|
15468
|
+
const pid = Number(initialTakeover?.terminal_agent_pid);
|
|
15469
|
+
if (!Number.isSafeInteger(pid) || pid <= 1) {
|
|
15470
|
+
throw new Error("deferred Codex acceptance monitor lost its exact process identity");
|
|
15471
|
+
}
|
|
15472
|
+
const terminal = await terminalBridge.resolveStoredTerminal("codex", pid, terminalControl, { pid });
|
|
15473
|
+
if (!["resolved", "abort_resolved"].includes(transfer.status)) {
|
|
15474
|
+
await recoverDeferredCodexForegroundTransferBeforeMutation({
|
|
15475
|
+
options: {
|
|
15476
|
+
...options,
|
|
15477
|
+
storeDir: transferStoreDir
|
|
15478
|
+
},
|
|
15479
|
+
terminal
|
|
15480
|
+
});
|
|
15481
|
+
transfer = loadDeferredForegroundTransfer(transferStoreDir, deferredTransferId);
|
|
15482
|
+
conversation = loadState(statePath);
|
|
15483
|
+
if (!["resolved", "abort_resolved"].includes(transfer.status)) {
|
|
15484
|
+
return { outcome: "pending" };
|
|
15485
|
+
}
|
|
15486
|
+
}
|
|
15487
|
+
if (transfer.status === "abort_resolved") {
|
|
15488
|
+
const submission = terminalBridgeSubmission(conversation);
|
|
15489
|
+
if (conversation.conversation_id !== transfer.turn_id ||
|
|
15490
|
+
stringValue(submission?.message_id) !== transfer.message_id ||
|
|
15491
|
+
submission?.status !== "aborted" ||
|
|
15492
|
+
submission.safe_to_retry !== true) {
|
|
15493
|
+
throw new Error(`deferred foreground transfer ${transfer.transfer_id} resolved its ` +
|
|
15494
|
+
"abort without an exact zero-input Turn receipt");
|
|
15495
|
+
}
|
|
15496
|
+
return { outcome: "not_accepted", conversation };
|
|
15497
|
+
}
|
|
15498
|
+
// Dedicated deferred recovery is authoritative and may have closed the
|
|
15499
|
+
// transfer while this monitor held the terminal lock. Do not fall through
|
|
15500
|
+
// to the generic enter_dispatched reconciler: it would reject the stronger
|
|
15501
|
+
// agent_accepted Turn/ledger that recovery just persisted. Re-prove that
|
|
15502
|
+
// exact monotonic authority here, then let this same monitor invocation
|
|
15503
|
+
// continue into completion polling without replaying terminal input.
|
|
15504
|
+
const authority = loadDeferredForegroundTurnAuthority({
|
|
15505
|
+
storeDir: transferStoreDir,
|
|
15506
|
+
terminal,
|
|
15507
|
+
transfer
|
|
15508
|
+
});
|
|
15509
|
+
const ledger = loadTerminalBridgeDispatchLedger(terminal.terminalControl);
|
|
15510
|
+
if (!ledger) {
|
|
15511
|
+
throw new Error(`deferred foreground transfer ${transfer.transfer_id} resolved ` +
|
|
15512
|
+
"without an exact terminal dispatch ledger");
|
|
15513
|
+
}
|
|
15514
|
+
assertDeferredForegroundLedgerAuthority({
|
|
15515
|
+
storeDir: transferStoreDir,
|
|
15516
|
+
terminal,
|
|
15517
|
+
transfer,
|
|
15518
|
+
ledger,
|
|
15519
|
+
statePath: authority.statePath,
|
|
15520
|
+
expectedMessageBodyHash: stringValue(authority.submission.message_body_hash)
|
|
15521
|
+
});
|
|
15522
|
+
const acceptedBinding = transfer.target_accepted_binding;
|
|
15523
|
+
const rollout = acceptedBinding?.native_process.rollout;
|
|
15524
|
+
if (!acceptedBinding ||
|
|
15525
|
+
!transfer.target_native_thread_id ||
|
|
15526
|
+
acceptedBinding.native_thread_id !== transfer.target_native_thread_id ||
|
|
15527
|
+
!isCompleteNativeRollout(rollout)) {
|
|
15528
|
+
throw new Error(`deferred foreground transfer ${transfer.transfer_id} resolved ` +
|
|
15529
|
+
"without an exact accepted native binding");
|
|
15530
|
+
}
|
|
15531
|
+
const acceptedIdentity = {
|
|
15532
|
+
sessionId: transfer.target_native_thread_id,
|
|
15533
|
+
processUuid: acceptedBinding.native_process.process_uuid,
|
|
15534
|
+
processBirth: acceptedBinding.native_process.process_birth,
|
|
15535
|
+
rollout,
|
|
15536
|
+
evidence: acceptedBinding.native_process.evidence
|
|
15537
|
+
};
|
|
15538
|
+
assertNativeAgentIdentityForTurn({
|
|
15539
|
+
conversation: authority.conversation,
|
|
15540
|
+
currentIdentity: acceptedIdentity,
|
|
15541
|
+
operation: "continue deferred foreground monitor for"
|
|
15542
|
+
});
|
|
15543
|
+
return { outcome: "accepted", conversation: authority.conversation };
|
|
15544
|
+
}
|
|
14404
15545
|
let bindingRecovery = await recoverVirginCodexPostSubmissionBinding({
|
|
14405
15546
|
options,
|
|
14406
15547
|
conversation,
|
|
@@ -14667,6 +15808,13 @@ function assertOrdinaryTerminalPayloadDoesNotInvokeNativeLifecycle(payload) {
|
|
|
14667
15808
|
"use an advertised dedicated native action when one exists, or enter " +
|
|
14668
15809
|
"the unsupported native command manually in the terminal UI");
|
|
14669
15810
|
}
|
|
15811
|
+
async function inspectCodexOpenRootRolloutInventory({ options, pid, cwd }) {
|
|
15812
|
+
const provider = createAgentSessionProvider("codex", options);
|
|
15813
|
+
if (!provider.inspectOpenRootRolloutInventoryForPid) {
|
|
15814
|
+
throw new Error("Codex open-root rollout inventory inspection is unavailable");
|
|
15815
|
+
}
|
|
15816
|
+
return provider.inspectOpenRootRolloutInventoryForPid(pid, cwd);
|
|
15817
|
+
}
|
|
14670
15818
|
function isCompleteNativeRollout(value) {
|
|
14671
15819
|
return isRecord(value) &&
|
|
14672
15820
|
Boolean(stringValue(value.fd)) &&
|
|
@@ -15187,6 +16335,29 @@ function quarantineManagedSessionBinding({ conversation, reason }) {
|
|
|
15187
16335
|
}
|
|
15188
16336
|
async function pollNativeAgentSessionIdentity({ options, executor, terminalControl, pid, expectedSessionId, allowedPreMaterializationIdentity, allowedAdditionalIdentities = [], requiredCodexAcceptance, attempts = 40, delayMs = 50 }) {
|
|
15189
16337
|
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
16338
|
+
if (requiredCodexAcceptance?.anchor.version === 3) {
|
|
16339
|
+
const inventory = await inspectCodexOpenRootRolloutInventory({
|
|
16340
|
+
options,
|
|
16341
|
+
pid,
|
|
16342
|
+
cwd: terminalControl.currentPath
|
|
16343
|
+
});
|
|
16344
|
+
const result = detectCodexCandidateSetRolloutAcceptance({
|
|
16345
|
+
anchor: requiredCodexAcceptance.anchor,
|
|
16346
|
+
currentInventory: inventory,
|
|
16347
|
+
requestHash: requiredCodexAcceptance.requestHash
|
|
16348
|
+
});
|
|
16349
|
+
if (result.status === "accepted") {
|
|
16350
|
+
return result.identity;
|
|
16351
|
+
}
|
|
16352
|
+
if (result.status === "uncertain") {
|
|
16353
|
+
throw new Error(`Codex candidate-set acceptance is uncertain (${result.code}): ` +
|
|
16354
|
+
result.reason);
|
|
16355
|
+
}
|
|
16356
|
+
if (attempt + 1 < attempts) {
|
|
16357
|
+
await cliSleep(delayMs);
|
|
16358
|
+
}
|
|
16359
|
+
continue;
|
|
16360
|
+
}
|
|
15190
16361
|
const identity = await resolveCurrentNativeAgentSessionIdentity({
|
|
15191
16362
|
options,
|
|
15192
16363
|
agent: executor.kind,
|
|
@@ -15592,12 +16763,26 @@ async function reconcileMonitors(options, { includeCallbackRecovery, reason, con
|
|
|
15592
16763
|
const logPath = expandHome(stringValue(listedConversation.event_log_path) ??
|
|
15593
16764
|
logPathForStatePath(statePath));
|
|
15594
16765
|
try {
|
|
16766
|
+
const localCompletion = settleLocalTerminalBridgeCompletionClaim({
|
|
16767
|
+
storeDir,
|
|
16768
|
+
statePath,
|
|
16769
|
+
logPath
|
|
16770
|
+
});
|
|
16771
|
+
if (localCompletion.handled) {
|
|
16772
|
+
skipped += 1;
|
|
16773
|
+
items.push({
|
|
16774
|
+
conversation_id: listedConversation.conversation_id,
|
|
16775
|
+
status: localCompletion.recovered ? "recovered" : "skipped",
|
|
16776
|
+
reason: localCompletion.reason
|
|
16777
|
+
});
|
|
16778
|
+
continue;
|
|
16779
|
+
}
|
|
15595
16780
|
if (includeCallbackRecovery) {
|
|
15596
|
-
const callbackRecovery = prepareCallbackDeliveryReconciliation({
|
|
16781
|
+
const callbackRecovery = withStoreWriterLease(storeDir, () => prepareCallbackDeliveryReconciliation({
|
|
15597
16782
|
statePath,
|
|
15598
16783
|
logPath,
|
|
15599
16784
|
delayMs: options.callbackRetryDelayMs
|
|
15600
|
-
});
|
|
16785
|
+
}));
|
|
15601
16786
|
if (callbackRecovery.handled) {
|
|
15602
16787
|
if (callbackRecovery.status === "launched") {
|
|
15603
16788
|
launched += 1;
|
|
@@ -15644,13 +16829,78 @@ async function reconcileMonitors(options, { includeCallbackRecovery, reason, con
|
|
|
15644
16829
|
logPath,
|
|
15645
16830
|
options
|
|
15646
16831
|
});
|
|
16832
|
+
const initialTakeover = isRecord(initialConversation.native_session_takeover)
|
|
16833
|
+
? initialConversation.native_session_takeover
|
|
16834
|
+
: undefined;
|
|
16835
|
+
const deferredTransferId = stringValue(initialTakeover?.deferred_foreground_transfer_id);
|
|
16836
|
+
if (deferredTransferId) {
|
|
16837
|
+
const deferredTransfer = loadDeferredForegroundTransfer(storeDir, deferredTransferId);
|
|
16838
|
+
if (![
|
|
16839
|
+
"resolved",
|
|
16840
|
+
"abort_resolved"
|
|
16841
|
+
].includes(deferredTransfer.status)) {
|
|
16842
|
+
const deferredControl = terminalControlFromTakeover(initialTakeover);
|
|
16843
|
+
const deferredPid = Number(initialTakeover?.terminal_agent_pid);
|
|
16844
|
+
if (!deferredControl ||
|
|
16845
|
+
!Number.isSafeInteger(deferredPid) ||
|
|
16846
|
+
deferredPid <= 1) {
|
|
16847
|
+
throw new Error(`deferred foreground Turn ${initialConversation.conversation_id} ` +
|
|
16848
|
+
"lost its exact terminal process authority");
|
|
16849
|
+
}
|
|
16850
|
+
const bridge = createTerminalAgentBridge(options);
|
|
16851
|
+
const deferredTerminal = await bridge.resolveStoredTerminal("codex", deferredPid, deferredControl, { pid: deferredPid });
|
|
16852
|
+
const releaseTerminalLock = acquireTerminalBridgeSendLock(storeDir, deferredControl, { timeoutMs: 30000 });
|
|
16853
|
+
try {
|
|
16854
|
+
await withStoreWriterLeaseAsync(storeDir, async () => {
|
|
16855
|
+
await recoverDeferredCodexForegroundTransferBeforeMutation({
|
|
16856
|
+
options,
|
|
16857
|
+
terminal: deferredTerminal
|
|
16858
|
+
});
|
|
16859
|
+
});
|
|
16860
|
+
}
|
|
16861
|
+
finally {
|
|
16862
|
+
releaseTerminalLock();
|
|
16863
|
+
}
|
|
16864
|
+
initialConversation = loadState(statePath);
|
|
16865
|
+
const refreshedTransfer = loadDeferredForegroundTransfer(storeDir, deferredTransferId);
|
|
16866
|
+
if (![
|
|
16867
|
+
"resolved",
|
|
16868
|
+
"abort_resolved"
|
|
16869
|
+
].includes(refreshedTransfer.status)) {
|
|
16870
|
+
const pendingSubmission = terminalBridgeSubmission(initialConversation);
|
|
16871
|
+
const pendingTakeover = isRecord(initialConversation.native_session_takeover)
|
|
16872
|
+
? initialConversation.native_session_takeover
|
|
16873
|
+
: undefined;
|
|
16874
|
+
const pendingAnchor = validateCodexRolloutAcceptanceAnchor(pendingTakeover?.codex_rollout_acceptance_anchor);
|
|
16875
|
+
if (pendingAnchor?.version !== 3 ||
|
|
16876
|
+
initialConversation.status !== "waiting_for_agent" ||
|
|
16877
|
+
pendingSubmission?.status !== "enter_dispatched" ||
|
|
16878
|
+
stringValue(pendingSubmission.message_id) !==
|
|
16879
|
+
stringValue(pendingTakeover?.terminal_bridge_message_id) ||
|
|
16880
|
+
refreshedTransfer.status !== "dispatch_started") {
|
|
16881
|
+
throw new Error(`deferred foreground Turn ${initialConversation.conversation_id} ` +
|
|
16882
|
+
"is not an exact pending candidate-set acceptance");
|
|
16883
|
+
}
|
|
16884
|
+
}
|
|
16885
|
+
}
|
|
16886
|
+
}
|
|
15647
16887
|
initialConversation = (await recoverVirginCodexPostSubmissionBinding({
|
|
15648
16888
|
options,
|
|
15649
16889
|
conversation: initialConversation,
|
|
15650
16890
|
statePath,
|
|
15651
16891
|
logPath
|
|
15652
16892
|
})).conversation;
|
|
15653
|
-
|
|
16893
|
+
const reconciledTakeover = isRecord(initialConversation.native_session_takeover)
|
|
16894
|
+
? initialConversation.native_session_takeover
|
|
16895
|
+
: undefined;
|
|
16896
|
+
const reconciledTransferId = stringValue(reconciledTakeover?.deferred_foreground_transfer_id);
|
|
16897
|
+
const reconciledTransfer = reconciledTransferId
|
|
16898
|
+
? loadDeferredForegroundTransfer(storeDir, reconciledTransferId)
|
|
16899
|
+
: undefined;
|
|
16900
|
+
if (!reconciledTransfer ||
|
|
16901
|
+
["resolved", "abort_resolved"].includes(reconciledTransfer.status)) {
|
|
16902
|
+
assertTurnBindingCurrent(initialConversation, "reconcile monitor for");
|
|
16903
|
+
}
|
|
15654
16904
|
const initialEligibility = terminalBridgeReconciliationEligibility(initialConversation);
|
|
15655
16905
|
if (!initialEligibility.eligible) {
|
|
15656
16906
|
skipped += 1;
|
|
@@ -15849,6 +17099,12 @@ function prepareCallbackDeliveryReconciliation({ statePath, logPath, delayMs })
|
|
|
15849
17099
|
handled: false
|
|
15850
17100
|
};
|
|
15851
17101
|
}
|
|
17102
|
+
const storeDir = pathsForConversationDir(path.dirname(statePath)).storeDir;
|
|
17103
|
+
assertConversationHasNoNonterminalDeferredForegroundTransfer({
|
|
17104
|
+
storeDir,
|
|
17105
|
+
conversation,
|
|
17106
|
+
action: "reconcile callback delivery for"
|
|
17107
|
+
});
|
|
15852
17108
|
const conversationId = stringValue(conversation.conversation_id) ?? "unknown";
|
|
15853
17109
|
const attempts = Number(callbackDelivery?.attempts ?? 0);
|
|
15854
17110
|
if (!["pending", "failed"].includes(String(callbackDelivery?.status ?? "")) ||
|
|
@@ -15955,12 +17211,6 @@ function terminalBridgeReconciliationEligibility(conversation) {
|
|
|
15955
17211
|
if (nativeTakeover?.terminal_bridge !== true) {
|
|
15956
17212
|
return { eligible: false, reason: "not_terminal_bridge" };
|
|
15957
17213
|
}
|
|
15958
|
-
if (!conversation.gateway_method) {
|
|
15959
|
-
return { eligible: false, reason: "gateway_method_missing" };
|
|
15960
|
-
}
|
|
15961
|
-
if (!stringValue(conversation.gateway_session) && !stringValue(conversation.openclaw_session)) {
|
|
15962
|
-
return { eligible: false, reason: "gateway_session_missing" };
|
|
15963
|
-
}
|
|
15964
17214
|
if (!isWaitingForAgent(conversation.status)) {
|
|
15965
17215
|
return {
|
|
15966
17216
|
eligible: false,
|
|
@@ -15973,18 +17223,38 @@ function terminalBridgeReconciliationEligibility(conversation) {
|
|
|
15973
17223
|
return { eligible: false, reason: "terminal_bridge_identity_missing" };
|
|
15974
17224
|
}
|
|
15975
17225
|
const dispatchLedger = loadTerminalBridgeDispatchLedger(terminalControl);
|
|
15976
|
-
|
|
15977
|
-
|
|
15978
|
-
|
|
15979
|
-
|
|
15980
|
-
|
|
15981
|
-
|
|
15982
|
-
|
|
15983
|
-
|
|
15984
|
-
|
|
17226
|
+
const conversationStatePath = stringValue(conversation.state_path);
|
|
17227
|
+
const conversationStoreDir = managedSessionStoreDirForConversation(conversation);
|
|
17228
|
+
const bindingId = stringValue(conversation.terminal_binding_id);
|
|
17229
|
+
const bindingGeneration = Number(conversation.terminal_binding_generation);
|
|
17230
|
+
if ((!dispatchLedger ||
|
|
17231
|
+
stringValue(dispatchLedger.message_id) !== terminalMessageId ||
|
|
17232
|
+
!terminalDispatchRecordMatchesControl(dispatchLedger, terminalControl) ||
|
|
17233
|
+
stringValue(dispatchLedger.conversation_id) !==
|
|
17234
|
+
conversation.conversation_id ||
|
|
17235
|
+
stringValue(dispatchLedger.session_id) !==
|
|
17236
|
+
sessionIdForConversation(conversation) ||
|
|
17237
|
+
stringValue(dispatchLedger.turn_id) !==
|
|
17238
|
+
turnIdForConversation(conversation) ||
|
|
17239
|
+
!conversationStatePath ||
|
|
17240
|
+
!sameCanonicalStatePath(dispatchLedger.state_path, conversationStatePath) ||
|
|
17241
|
+
!conversationStoreDir ||
|
|
17242
|
+
path.resolve(stringValue(dispatchLedger.store_dir) ?? "") !==
|
|
17243
|
+
path.resolve(conversationStoreDir) ||
|
|
17244
|
+
(bindingId !== undefined &&
|
|
17245
|
+
stringValue(dispatchLedger.binding_id) !== bindingId) ||
|
|
17246
|
+
(Number.isSafeInteger(bindingGeneration) &&
|
|
17247
|
+
Number(dispatchLedger.binding_generation) !== bindingGeneration) ||
|
|
17248
|
+
![
|
|
17249
|
+
"prepared",
|
|
17250
|
+
"text_injected",
|
|
17251
|
+
"enter_dispatched",
|
|
17252
|
+
"agent_accepted",
|
|
17253
|
+
"submitted"
|
|
17254
|
+
].includes(String(dispatchLedger.status)))) {
|
|
15985
17255
|
return {
|
|
15986
17256
|
eligible: false,
|
|
15987
|
-
reason: `terminal_dispatch_${String(dispatchLedger
|
|
17257
|
+
reason: `terminal_dispatch_${String(dispatchLedger?.status ?? "missing_or_generation_replaced")}`
|
|
15988
17258
|
};
|
|
15989
17259
|
}
|
|
15990
17260
|
const submission = terminalBridgeSubmission(conversation);
|
|
@@ -16000,6 +17270,27 @@ function terminalBridgeReconciliationEligibility(conversation) {
|
|
|
16000
17270
|
if (!Number.isInteger(runtime.pid) || Number(runtime.pid) <= 0 || !stringValue(runtime.cwd)) {
|
|
16001
17271
|
return { eligible: false, reason: "terminal_agent_identity_missing" };
|
|
16002
17272
|
}
|
|
17273
|
+
const storeDir = managedSessionStoreDirForConversation(conversation);
|
|
17274
|
+
const deferredTransferId = stringValue(nativeTakeover.deferred_foreground_transfer_id);
|
|
17275
|
+
const deferredTransfer = storeDir && deferredTransferId
|
|
17276
|
+
? loadDeferredForegroundTransfer(storeDir, deferredTransferId)
|
|
17277
|
+
: undefined;
|
|
17278
|
+
if (deferredTransfer &&
|
|
17279
|
+
!["resolved", "abort_resolved"].includes(deferredTransfer.status)) {
|
|
17280
|
+
const anchor = validateCodexRolloutAcceptanceAnchor(nativeTakeover.codex_rollout_acceptance_anchor);
|
|
17281
|
+
if (anchor?.version !== 3 ||
|
|
17282
|
+
deferredTransfer.status !== "dispatch_started" ||
|
|
17283
|
+
submission?.status !== "enter_dispatched" ||
|
|
17284
|
+
deferredTransfer.turn_id !== turnIdForConversation(conversation) ||
|
|
17285
|
+
deferredTransfer.message_id !== terminalMessageId ||
|
|
17286
|
+
deferredTransfer.target_session_id !==
|
|
17287
|
+
sessionIdForConversation(conversation)) {
|
|
17288
|
+
return {
|
|
17289
|
+
eligible: false,
|
|
17290
|
+
reason: `deferred_foreground_transfer_${deferredTransfer.status}`
|
|
17291
|
+
};
|
|
17292
|
+
}
|
|
17293
|
+
}
|
|
16003
17294
|
const inactivityTimeoutMinutes = Number(nativeTakeover.terminal_bridge_inactivity_timeout_minutes);
|
|
16004
17295
|
const hardTimeoutMinutes = Number(nativeTakeover.terminal_bridge_hard_timeout_minutes);
|
|
16005
17296
|
const startedAtMs = validTimestampMs(nativeTakeover.terminal_bridge_started_at);
|
|
@@ -16028,6 +17319,179 @@ function terminalBridgeReconciliationEligibility(conversation) {
|
|
|
16028
17319
|
hardDeadlineAtMs
|
|
16029
17320
|
};
|
|
16030
17321
|
}
|
|
17322
|
+
function settleLocalTerminalBridgeCompletionClaim({ storeDir, statePath, logPath }) {
|
|
17323
|
+
const initial = loadState(statePath);
|
|
17324
|
+
const initialTakeover = isRecord(initial.native_session_takeover)
|
|
17325
|
+
? initial.native_session_takeover
|
|
17326
|
+
: undefined;
|
|
17327
|
+
const initialClaim = isRecord(initialTakeover?.terminal_bridge_completion_claim)
|
|
17328
|
+
? initialTakeover.terminal_bridge_completion_claim
|
|
17329
|
+
: undefined;
|
|
17330
|
+
if (stringValue(initial.gateway_method) ||
|
|
17331
|
+
!initialClaim ||
|
|
17332
|
+
!["idle", "failed"].includes(String(initial.status))) {
|
|
17333
|
+
return {
|
|
17334
|
+
handled: false,
|
|
17335
|
+
recovered: false,
|
|
17336
|
+
reason: "local_completion_not_applicable"
|
|
17337
|
+
};
|
|
17338
|
+
}
|
|
17339
|
+
const initialTerminalControl = terminalControlFromTakeover(initialTakeover);
|
|
17340
|
+
if (!initialTerminalControl) {
|
|
17341
|
+
throw new Error(`local terminal completion ${initial.conversation_id} lost its terminal authority`);
|
|
17342
|
+
}
|
|
17343
|
+
const releaseTerminalLock = acquireTerminalBridgeSendLock(storeDir, initialTerminalControl, { timeoutMs: 30000 });
|
|
17344
|
+
try {
|
|
17345
|
+
return withStoreWriterLease(storeDir, () => {
|
|
17346
|
+
const releaseStateLock = acquireFileLock(`${statePath}.lock`);
|
|
17347
|
+
try {
|
|
17348
|
+
const conversation = loadState(statePath);
|
|
17349
|
+
const takeover = isRecord(conversation.native_session_takeover)
|
|
17350
|
+
? conversation.native_session_takeover
|
|
17351
|
+
: undefined;
|
|
17352
|
+
const claim = isRecord(takeover?.terminal_bridge_completion_claim)
|
|
17353
|
+
? takeover.terminal_bridge_completion_claim
|
|
17354
|
+
: undefined;
|
|
17355
|
+
const terminalControl = terminalControlFromTakeover(takeover);
|
|
17356
|
+
const terminalMessageId = stringValue(takeover?.terminal_bridge_message_id);
|
|
17357
|
+
const callbackMessageId = stringValue(claim?.callback_message_id);
|
|
17358
|
+
const completionFingerprint = stringValue(claim?.completion_fingerprint);
|
|
17359
|
+
const completionId = stringValue(claim?.completion_id);
|
|
17360
|
+
const claimedAt = stringValue(claim?.claimed_at);
|
|
17361
|
+
const outcome = stringValue(claim?.outcome);
|
|
17362
|
+
const expectedMessageType = outcome === "success"
|
|
17363
|
+
? "done"
|
|
17364
|
+
: outcome === "failure"
|
|
17365
|
+
? "error"
|
|
17366
|
+
: undefined;
|
|
17367
|
+
if (stringValue(conversation.gateway_method) ||
|
|
17368
|
+
conversation.callback_delivery !== undefined ||
|
|
17369
|
+
!claim ||
|
|
17370
|
+
!terminalControl ||
|
|
17371
|
+
!terminalControlsShareIncarnation(terminalControl, initialTerminalControl) ||
|
|
17372
|
+
!terminalMessageId ||
|
|
17373
|
+
!callbackMessageId ||
|
|
17374
|
+
!completionFingerprint ||
|
|
17375
|
+
!completionId ||
|
|
17376
|
+
!claimedAt ||
|
|
17377
|
+
validTimestampMs(claimedAt) === undefined ||
|
|
17378
|
+
!expectedMessageType ||
|
|
17379
|
+
(outcome === "success" && conversation.status !== "idle") ||
|
|
17380
|
+
(outcome === "failure" && conversation.status !== "failed") ||
|
|
17381
|
+
deterministicTerminalCallbackMessageId({
|
|
17382
|
+
conversationId: conversation.conversation_id,
|
|
17383
|
+
terminalMessageId,
|
|
17384
|
+
completionFingerprint,
|
|
17385
|
+
outcome
|
|
17386
|
+
}) !== callbackMessageId) {
|
|
17387
|
+
throw new Error(`local terminal completion ${conversation.conversation_id} has ` +
|
|
17388
|
+
"inconsistent claim, Turn phase, or callback authority");
|
|
17389
|
+
}
|
|
17390
|
+
const submission = terminalBridgeSubmission(conversation);
|
|
17391
|
+
if (submission?.status !== "agent_accepted" ||
|
|
17392
|
+
stringValue(submission.message_id) !== terminalMessageId ||
|
|
17393
|
+
stringValue(submission.session_id) !==
|
|
17394
|
+
sessionIdForConversation(conversation) ||
|
|
17395
|
+
stringValue(submission.turn_id) !== turnIdForConversation(conversation)) {
|
|
17396
|
+
throw new Error(`local terminal completion ${conversation.conversation_id} is ` +
|
|
17397
|
+
"not tied to one accepted terminal submission");
|
|
17398
|
+
}
|
|
17399
|
+
const events = readExistingEvents(logPath);
|
|
17400
|
+
const claimed = events.some((event) => event.event === "terminal_bridge_completion_claimed" &&
|
|
17401
|
+
event.conversation_id === conversation.conversation_id &&
|
|
17402
|
+
event.terminal_bridge_message_id === terminalMessageId &&
|
|
17403
|
+
event.completion_fingerprint === completionFingerprint &&
|
|
17404
|
+
event.completion_id === completionId &&
|
|
17405
|
+
event.callback_message_id === callbackMessageId &&
|
|
17406
|
+
event.outcome === outcome &&
|
|
17407
|
+
event.ts === claimedAt);
|
|
17408
|
+
const detected = events.some((event) => event.event === "terminal_bridge_completion_detected" &&
|
|
17409
|
+
event.conversation_id === conversation.conversation_id &&
|
|
17410
|
+
event.terminal_bridge_message_id === terminalMessageId &&
|
|
17411
|
+
event.completion_id === completionId &&
|
|
17412
|
+
event.callback_message_id === callbackMessageId &&
|
|
17413
|
+
event.completion_outcome === outcome);
|
|
17414
|
+
const messageRecorded = events.some((event) => {
|
|
17415
|
+
const message = isRecord(event.message) ? event.message : undefined;
|
|
17416
|
+
const metadata = isRecord(message?.metadata)
|
|
17417
|
+
? message.metadata
|
|
17418
|
+
: undefined;
|
|
17419
|
+
return event.event === "message" &&
|
|
17420
|
+
event.conversation_id === conversation.conversation_id &&
|
|
17421
|
+
event.session_id === sessionIdForConversation(conversation) &&
|
|
17422
|
+
event.turn_id === turnIdForConversation(conversation) &&
|
|
17423
|
+
message?.id === callbackMessageId &&
|
|
17424
|
+
message?.type === expectedMessageType &&
|
|
17425
|
+
message?.to === "openclaw" &&
|
|
17426
|
+
message?.requires_response === false &&
|
|
17427
|
+
metadata?.terminal_bridge_message_id === terminalMessageId;
|
|
17428
|
+
});
|
|
17429
|
+
if (!claimed || !detected || !messageRecorded) {
|
|
17430
|
+
throw new Error(`local terminal completion ${conversation.conversation_id} is ` +
|
|
17431
|
+
"missing exact claim, detection, or message evidence");
|
|
17432
|
+
}
|
|
17433
|
+
const ledger = loadTerminalBridgeDispatchLedger(terminalControl);
|
|
17434
|
+
const expectedBindingId = stringValue(conversation.terminal_binding_id);
|
|
17435
|
+
const expectedBindingGeneration = Number(conversation.terminal_binding_generation);
|
|
17436
|
+
if (!ledger ||
|
|
17437
|
+
!["agent_accepted", "resolved"].includes(String(ledger.status)) ||
|
|
17438
|
+
!terminalDispatchRecordMatchesControl(ledger, terminalControl) ||
|
|
17439
|
+
stringValue(ledger.conversation_id) !== conversation.conversation_id ||
|
|
17440
|
+
stringValue(ledger.session_id) !== sessionIdForConversation(conversation) ||
|
|
17441
|
+
stringValue(ledger.turn_id) !== turnIdForConversation(conversation) ||
|
|
17442
|
+
stringValue(ledger.message_id) !== terminalMessageId ||
|
|
17443
|
+
stringValue(ledger.native_thread_id) !==
|
|
17444
|
+
stringValue(conversation.native_thread_id) ||
|
|
17445
|
+
(expectedBindingId !== undefined &&
|
|
17446
|
+
stringValue(ledger.binding_id) !== expectedBindingId) ||
|
|
17447
|
+
(Number.isSafeInteger(expectedBindingGeneration) &&
|
|
17448
|
+
Number(ledger.binding_generation) !== expectedBindingGeneration) ||
|
|
17449
|
+
path.resolve(stringValue(ledger.store_dir) ?? "") !==
|
|
17450
|
+
path.resolve(storeDir) ||
|
|
17451
|
+
!sameCanonicalStatePath(ledger.state_path, statePath)) {
|
|
17452
|
+
throw new Error(`local terminal completion ${conversation.conversation_id} has ` +
|
|
17453
|
+
"no exact accepted terminal ledger");
|
|
17454
|
+
}
|
|
17455
|
+
if (ledger.status === "resolved") {
|
|
17456
|
+
return {
|
|
17457
|
+
handled: true,
|
|
17458
|
+
recovered: false,
|
|
17459
|
+
reason: "local_terminal_completion_already_settled"
|
|
17460
|
+
};
|
|
17461
|
+
}
|
|
17462
|
+
if (!resolveTerminalBridgeDispatchLedger({
|
|
17463
|
+
terminalControl,
|
|
17464
|
+
conversation,
|
|
17465
|
+
expectedMessageId: terminalMessageId,
|
|
17466
|
+
reason: "callbackless terminal bridge task reached durable completion"
|
|
17467
|
+
})) {
|
|
17468
|
+
throw new Error(`local terminal completion ${conversation.conversation_id} ` +
|
|
17469
|
+
"changed before ledger settlement");
|
|
17470
|
+
}
|
|
17471
|
+
appendEvent(logPath, {
|
|
17472
|
+
ts: cliNow().toISOString(),
|
|
17473
|
+
conversation_id: conversation.conversation_id,
|
|
17474
|
+
event: "terminal_bridge_local_completion_settled",
|
|
17475
|
+
terminal_bridge_message_id: terminalMessageId,
|
|
17476
|
+
completion_id: completionId,
|
|
17477
|
+
callback_message_id: callbackMessageId,
|
|
17478
|
+
outcome
|
|
17479
|
+
});
|
|
17480
|
+
return {
|
|
17481
|
+
handled: true,
|
|
17482
|
+
recovered: true,
|
|
17483
|
+
reason: "local_terminal_completion_ledger_recovered"
|
|
17484
|
+
};
|
|
17485
|
+
}
|
|
17486
|
+
finally {
|
|
17487
|
+
releaseStateLock();
|
|
17488
|
+
}
|
|
17489
|
+
});
|
|
17490
|
+
}
|
|
17491
|
+
finally {
|
|
17492
|
+
releaseTerminalLock();
|
|
17493
|
+
}
|
|
17494
|
+
}
|
|
16031
17495
|
function latestTerminalBridgeMonitorLaunchPid(logPath) {
|
|
16032
17496
|
let events;
|
|
16033
17497
|
try {
|
|
@@ -16152,37 +17616,46 @@ async function runCancel(options) {
|
|
|
16152
17616
|
throw new Error(`conversation ${conversation.conversation_id} is not attached to a live terminal`);
|
|
16153
17617
|
}
|
|
16154
17618
|
async function runTerminalConversationCancel({ options, conversationId, agent, terminalControl, pid }) {
|
|
16155
|
-
const
|
|
17619
|
+
const storeDir = storeDirFromOptions(options);
|
|
17620
|
+
const releaseTerminalLock = acquireTerminalBridgeSendLock(storeDir, terminalControl, { timeoutMs: 30000 });
|
|
16156
17621
|
try {
|
|
16157
|
-
|
|
16158
|
-
|
|
17622
|
+
await withStoreWriterLeaseAsync(storeDir, async () => {
|
|
17623
|
+
assertTerminalHasNoNonterminalDeferredForegroundTransfer({
|
|
17624
|
+
storeDir,
|
|
16159
17625
|
pid,
|
|
16160
|
-
|
|
16161
|
-
|
|
16162
|
-
}
|
|
16163
|
-
|
|
16164
|
-
|
|
16165
|
-
|
|
16166
|
-
|
|
16167
|
-
|
|
16168
|
-
|
|
16169
|
-
|
|
16170
|
-
|
|
16171
|
-
|
|
16172
|
-
|
|
16173
|
-
|
|
16174
|
-
|
|
16175
|
-
|
|
16176
|
-
|
|
16177
|
-
|
|
16178
|
-
|
|
16179
|
-
|
|
16180
|
-
|
|
16181
|
-
|
|
16182
|
-
|
|
16183
|
-
|
|
16184
|
-
|
|
16185
|
-
|
|
17626
|
+
terminalControl,
|
|
17627
|
+
action: "cancel"
|
|
17628
|
+
});
|
|
17629
|
+
const cancellation = await createTerminalAgentBridge(options).cancel(agent, terminalControl, {
|
|
17630
|
+
runtime: {
|
|
17631
|
+
pid,
|
|
17632
|
+
cwd: terminalControl.currentPath,
|
|
17633
|
+
terminalTarget: terminalControl.target
|
|
17634
|
+
},
|
|
17635
|
+
scrollbackLines: Number(options.scrollbackLines ?? 120)
|
|
17636
|
+
});
|
|
17637
|
+
runtimeLog("info", "terminal_cancel_requested", {
|
|
17638
|
+
conversation_id: conversationId,
|
|
17639
|
+
agent,
|
|
17640
|
+
terminal_target: terminalControl.target,
|
|
17641
|
+
key: cancellation.key,
|
|
17642
|
+
keys: cancellation.keys,
|
|
17643
|
+
denied_approval: cancellation.deniedApproval,
|
|
17644
|
+
request_id: cancellation.requestId,
|
|
17645
|
+
cancel_requested: cancellation.cancelRequested,
|
|
17646
|
+
reason: cancellation.reason
|
|
17647
|
+
});
|
|
17648
|
+
printJson({
|
|
17649
|
+
conversation_id: conversationId,
|
|
17650
|
+
source: "terminal_control",
|
|
17651
|
+
cancel_requested: cancellation.cancelRequested,
|
|
17652
|
+
reason: cancellation.reason,
|
|
17653
|
+
terminal_control: terminalControl,
|
|
17654
|
+
key: cancellation.key,
|
|
17655
|
+
keys: cancellation.keys,
|
|
17656
|
+
denied_approval: cancellation.deniedApproval,
|
|
17657
|
+
request_id: cancellation.requestId
|
|
17658
|
+
});
|
|
16186
17659
|
});
|
|
16187
17660
|
}
|
|
16188
17661
|
finally {
|
|
@@ -16679,7 +18152,7 @@ async function runTerminalDispatchClose({ options, terminalConversation }) {
|
|
|
16679
18152
|
throw new Error(`terminal ${terminalControl.target} deferred dispatch authority ` +
|
|
16680
18153
|
"does not match its exact Store/terminal transfer");
|
|
16681
18154
|
}
|
|
16682
|
-
if (!["resolved", "
|
|
18155
|
+
if (!["resolved", "abort_resolved"].includes(transfer.status)) {
|
|
16683
18156
|
throw new Error(`terminal ${terminalControl.target} dispatch is fenced by deferred ` +
|
|
16684
18157
|
`foreground transfer ${transfer.transfer_id} (${transfer.status}); ` +
|
|
16685
18158
|
"generic terminal close cannot resolve it");
|
|
@@ -17149,7 +18622,7 @@ async function runTerminalBridgeMonitor(options) {
|
|
|
17149
18622
|
return;
|
|
17150
18623
|
}
|
|
17151
18624
|
catch (error) {
|
|
17152
|
-
if (!(error
|
|
18625
|
+
if (!isStoreMutationLockTimeout(error)) {
|
|
17153
18626
|
throw error;
|
|
17154
18627
|
}
|
|
17155
18628
|
storeDeferredAttempts += 1;
|
|
@@ -17158,8 +18631,12 @@ async function runTerminalBridgeMonitor(options) {
|
|
|
17158
18631
|
runtimeLog("warn", "terminal_bridge_monitor_store_operation_deferred", {
|
|
17159
18632
|
conversation_id: conversation.conversation_id,
|
|
17160
18633
|
terminal_bridge_message_id: terminalMessageId,
|
|
17161
|
-
error_code: error
|
|
17162
|
-
|
|
18634
|
+
error_code: isRecord(error)
|
|
18635
|
+
? stringValue(error.code) ?? "LOCK_TIMEOUT"
|
|
18636
|
+
: "LOCK_TIMEOUT",
|
|
18637
|
+
lock_kind: error instanceof StoreLockTimeoutError
|
|
18638
|
+
? error.lockKind
|
|
18639
|
+
: "conversation",
|
|
17163
18640
|
attempt: storeDeferredAttempts,
|
|
17164
18641
|
retry_in_ms: retryInMs
|
|
17165
18642
|
});
|
|
@@ -17376,6 +18853,9 @@ async function runTerminalBridgeMonitorWithLock(options, lifecycle, expectedTerm
|
|
|
17376
18853
|
}
|
|
17377
18854
|
}
|
|
17378
18855
|
catch (error) {
|
|
18856
|
+
if (isStoreMutationLockTimeout(error)) {
|
|
18857
|
+
throw error;
|
|
18858
|
+
}
|
|
17379
18859
|
const reason = error instanceof Error ? error.message : String(error);
|
|
17380
18860
|
conversation = markTerminalAcceptanceUncertain({
|
|
17381
18861
|
conversation,
|
|
@@ -19106,6 +20586,17 @@ function terminalBindingLedgerFields(conversation) {
|
|
|
19106
20586
|
};
|
|
19107
20587
|
}
|
|
19108
20588
|
function assertConversationHasNoNonterminalDeferredForegroundTransfer({ storeDir, conversation, action }) {
|
|
20589
|
+
const turnId = turnIdForConversation(conversation);
|
|
20590
|
+
const sourceTransfer = listDeferredForegroundTransfers(storeDir).find((candidate) => candidate.version === 2 &&
|
|
20591
|
+
candidate.source_kind === "candidate_rollout_quiescent" &&
|
|
20592
|
+
!["resolved", "abort_resolved"].includes(candidate.status) &&
|
|
20593
|
+
(candidate.source_turn_history ?? []).some((sourceTurn) => sourceTurn.turn_id === turnId));
|
|
20594
|
+
if (sourceTransfer) {
|
|
20595
|
+
throw new Error(`cannot ${action} Turn ${turnId} while deferred foreground transfer ` +
|
|
20596
|
+
`${sourceTransfer.transfer_id} reserves it as immutable source ` +
|
|
20597
|
+
`history in ${sourceTransfer.status}; dedicated transfer recovery ` +
|
|
20598
|
+
"must finish first");
|
|
20599
|
+
}
|
|
19109
20600
|
const takeover = isRecord(conversation.native_session_takeover)
|
|
19110
20601
|
? conversation.native_session_takeover
|
|
19111
20602
|
: undefined;
|
|
@@ -19114,12 +20605,23 @@ function assertConversationHasNoNonterminalDeferredForegroundTransfer({ storeDir
|
|
|
19114
20605
|
return;
|
|
19115
20606
|
}
|
|
19116
20607
|
const transfer = loadDeferredForegroundTransfer(storeDir, transferId);
|
|
19117
|
-
if (!["resolved", "
|
|
20608
|
+
if (!["resolved", "abort_resolved"].includes(transfer.status)) {
|
|
19118
20609
|
throw new Error(`cannot ${action} Turn ${turnIdForConversation(conversation)} while ` +
|
|
19119
20610
|
`deferred foreground transfer ${transfer.transfer_id} is ` +
|
|
19120
20611
|
`${transfer.status}; dedicated transfer recovery must finish first`);
|
|
19121
20612
|
}
|
|
19122
20613
|
}
|
|
20614
|
+
function assertTerminalHasNoNonterminalDeferredForegroundTransfer({ storeDir, pid, terminalControl, action }) {
|
|
20615
|
+
const transfer = listDeferredForegroundTransfers(storeDir).find((candidate) => !["resolved", "abort_resolved"].includes(candidate.status) &&
|
|
20616
|
+
candidate.process_pid === pid &&
|
|
20617
|
+
terminalControlEvidenceMatches(candidate.terminal_endpoint, terminalControl));
|
|
20618
|
+
if (!transfer) {
|
|
20619
|
+
return;
|
|
20620
|
+
}
|
|
20621
|
+
throw new Error(`cannot ${action} terminal ${terminalControl.target} while deferred ` +
|
|
20622
|
+
`foreground transfer ${transfer.transfer_id} is ${transfer.status}; ` +
|
|
20623
|
+
"dedicated transfer recovery must finish first");
|
|
20624
|
+
}
|
|
19123
20625
|
function reconcilePreparedTerminalDispatchLedger(terminalControl, ledger) {
|
|
19124
20626
|
if (terminalDispatchLedgerLooksLifecycle(ledger)) {
|
|
19125
20627
|
return ledger;
|
|
@@ -19492,6 +20994,17 @@ function deferredCodexBoundaryFromTransfer({ terminal, transfer }) {
|
|
|
19492
20994
|
status: transfer.previous_dispatch_status,
|
|
19493
20995
|
fingerprint: transfer.previous_dispatch_fingerprint
|
|
19494
20996
|
},
|
|
20997
|
+
sourceKind: transfer.version === 1
|
|
20998
|
+
? "status_card_only"
|
|
20999
|
+
: required(transfer.source_kind, "deferred foreground source kind is unavailable"),
|
|
21000
|
+
...(transfer.source_turn_history
|
|
21001
|
+
? { sourceTurnHistory: transfer.source_turn_history }
|
|
21002
|
+
: {}),
|
|
21003
|
+
...(transfer.source_previous_last_transition_id
|
|
21004
|
+
? {
|
|
21005
|
+
sourcePreviousLastTransitionId: transfer.source_previous_last_transition_id
|
|
21006
|
+
}
|
|
21007
|
+
: {}),
|
|
19495
21008
|
sourceReservedRevision: transfer.source_expected_revision + 1,
|
|
19496
21009
|
sourceReservedBindingToken: managedSessionBindingToken({
|
|
19497
21010
|
session_id: transfer.source_session_id,
|
|
@@ -19517,6 +21030,28 @@ function loadDeferredForegroundTurnAuthority({ storeDir, terminal, transfer }) {
|
|
|
19517
21030
|
const submission = terminalBridgeSubmission(conversation);
|
|
19518
21031
|
const terminalControl = terminalControlFromTakeover(takeover);
|
|
19519
21032
|
const anchor = takeover?.codex_rollout_acceptance_anchor;
|
|
21033
|
+
let validatedAnchor;
|
|
21034
|
+
try {
|
|
21035
|
+
validatedAnchor = validateCodexRolloutAcceptanceAnchor(anchor);
|
|
21036
|
+
}
|
|
21037
|
+
catch {
|
|
21038
|
+
validatedAnchor = undefined;
|
|
21039
|
+
}
|
|
21040
|
+
const exactDeferredAnchor = Boolean(validatedAnchor &&
|
|
21041
|
+
validatedAnchor.process_uuid === transfer.process_uuid &&
|
|
21042
|
+
validatedAnchor.process_birth === transfer.process_birth &&
|
|
21043
|
+
((validatedAnchor.version === 2 &&
|
|
21044
|
+
validatedAnchor.mode === "pre_materialization" &&
|
|
21045
|
+
validatedAnchor.native_thread_binding === "post_submission" &&
|
|
21046
|
+
validatedAnchor.file_existed === false &&
|
|
21047
|
+
validatedAnchor.offset_bytes === 0 &&
|
|
21048
|
+
validatedAnchor.expected_empty_native_session === true) ||
|
|
21049
|
+
(validatedAnchor.version === 3 &&
|
|
21050
|
+
validatedAnchor.mode === "candidate_set" &&
|
|
21051
|
+
validatedAnchor.inventory_pid === transfer.process_pid &&
|
|
21052
|
+
path.resolve(validatedAnchor.inventory_cwd ?? "") ===
|
|
21053
|
+
path.resolve(transfer.workspace) &&
|
|
21054
|
+
validatedAnchor.candidate_rollouts.length > 0)));
|
|
19520
21055
|
const target = loadManagedSession(storeDir, transfer.target_session_id);
|
|
19521
21056
|
const targetBefore = transfer.target_before_binding;
|
|
19522
21057
|
const requestText = stringValue(takeover?.terminal_bridge_request_text);
|
|
@@ -19627,20 +21162,7 @@ function loadDeferredForegroundTurnAuthority({ storeDir, terminal, transfer }) {
|
|
|
19627
21162
|
target.binding.native_process.pid !== transfer.process_pid ||
|
|
19628
21163
|
target.binding.native_process.process_uuid !== transfer.process_uuid ||
|
|
19629
21164
|
target.binding.native_process.process_birth !== transfer.process_birth ||
|
|
19630
|
-
!
|
|
19631
|
-
anchor.schema !== "agent-knock-knock/codex-rollout-acceptance-anchor" ||
|
|
19632
|
-
anchor.version !== 2 ||
|
|
19633
|
-
anchor.mode !== "pre_materialization" ||
|
|
19634
|
-
anchor.native_thread_binding !== "post_submission" ||
|
|
19635
|
-
anchor.file_existed !== false ||
|
|
19636
|
-
anchor.offset_bytes !== 0 ||
|
|
19637
|
-
anchor.expected_empty_native_session !== true ||
|
|
19638
|
-
anchor.native_thread_id !== undefined ||
|
|
19639
|
-
anchor.rollout !== undefined ||
|
|
19640
|
-
anchor.process_uuid !== transfer.process_uuid ||
|
|
19641
|
-
anchor.process_birth !== transfer.process_birth ||
|
|
19642
|
-
typeof anchor.anchor_fingerprint !== "string" ||
|
|
19643
|
-
!/^[0-9a-f]{64}$/u.test(anchor.anchor_fingerprint)) {
|
|
21165
|
+
!exactDeferredAnchor) {
|
|
19644
21166
|
throw new Error(`deferred foreground transfer ${transfer.transfer_id} Turn authority ` +
|
|
19645
21167
|
"does not match its exact request/terminal/process fence");
|
|
19646
21168
|
}
|
|
@@ -19650,7 +21172,7 @@ function loadDeferredForegroundTurnAuthority({ storeDir, terminal, transfer }) {
|
|
|
19650
21172
|
logPath: canonical.logPath,
|
|
19651
21173
|
takeover,
|
|
19652
21174
|
submission,
|
|
19653
|
-
anchor:
|
|
21175
|
+
anchor: validatedAnchor
|
|
19654
21176
|
};
|
|
19655
21177
|
}
|
|
19656
21178
|
function assertDeferredForegroundLedgerAuthority({ storeDir, terminal, transfer, ledger, statePath, expectedMessageBodyHash }) {
|
|
@@ -19675,6 +21197,18 @@ function assertDeferredForegroundLedgerAuthority({ storeDir, terminal, transfer,
|
|
|
19675
21197
|
exactCommittedAcceptance = false;
|
|
19676
21198
|
}
|
|
19677
21199
|
}
|
|
21200
|
+
const exactPostDispatchPending = Boolean(transfer.status === "dispatch_started" &&
|
|
21201
|
+
transfer.input_stage === "enter_dispatched" &&
|
|
21202
|
+
ledger.status === "enter_dispatched" &&
|
|
21203
|
+
ledger.dispatcher_pid === null &&
|
|
21204
|
+
stringValue(ledger.prepared_at) === transfer.prepared_at &&
|
|
21205
|
+
stringValue(ledger.text_injected_at) === transfer.text_injected_at &&
|
|
21206
|
+
stringValue(ledger.enter_dispatched_at) === transfer.enter_dispatched_at &&
|
|
21207
|
+
ledger.agent_accepted_at === undefined &&
|
|
21208
|
+
ledger.not_accepted_at === undefined &&
|
|
21209
|
+
ledger.uncertain_at === undefined &&
|
|
21210
|
+
ledger.acceptance_evidence === undefined &&
|
|
21211
|
+
ledger.safe_to_retry !== true);
|
|
19678
21212
|
if (!targetBefore ||
|
|
19679
21213
|
terminalDispatchLedgerLooksLifecycle(ledger) ||
|
|
19680
21214
|
!terminalDispatchRecordMatchesControl(ledger, terminal.terminalControl) ||
|
|
@@ -19704,6 +21238,7 @@ function assertDeferredForegroundLedgerAuthority({ storeDir, terminal, transfer,
|
|
|
19704
21238
|
path.resolve(storeDir) ||
|
|
19705
21239
|
(ledger.status !== "resolved" &&
|
|
19706
21240
|
!exactCommittedAcceptance &&
|
|
21241
|
+
!exactPostDispatchPending &&
|
|
19707
21242
|
Number(ledger.dispatcher_pid) !== transfer.dispatcher_pid)) {
|
|
19708
21243
|
throw new Error(`deferred foreground transfer ${transfer.transfer_id} dispatch ledger ` +
|
|
19709
21244
|
"does not match its exact Turn authority");
|
|
@@ -20040,21 +21575,44 @@ async function recoverAcceptedDeferredForegroundDispatch({ options, storeDir, te
|
|
|
20040
21575
|
throw new Error(`deferred foreground transfer ${transfer.transfer_id} has invalid ` +
|
|
20041
21576
|
`${String(ledger.status)} dispatch evidence for acceptance recovery`);
|
|
20042
21577
|
}
|
|
20043
|
-
|
|
20044
|
-
|
|
20045
|
-
|
|
20046
|
-
|
|
20047
|
-
|
|
20048
|
-
|
|
20049
|
-
|
|
20050
|
-
|
|
21578
|
+
let identity;
|
|
21579
|
+
let acceptance;
|
|
21580
|
+
if (authority.anchor.version === 3) {
|
|
21581
|
+
const inventory = await inspectCodexOpenRootRolloutInventory({
|
|
21582
|
+
options,
|
|
21583
|
+
pid: transfer.process_pid,
|
|
21584
|
+
cwd: terminal.terminalControl.currentPath
|
|
21585
|
+
});
|
|
21586
|
+
const result = detectCodexCandidateSetRolloutAcceptance({
|
|
21587
|
+
anchor: authority.anchor,
|
|
21588
|
+
currentInventory: inventory,
|
|
21589
|
+
requestHash: transfer.request_hash
|
|
21590
|
+
});
|
|
21591
|
+
if (result.status === "uncertain") {
|
|
21592
|
+
throw new Error(`Codex candidate-set recovery is uncertain (${result.code}): ` +
|
|
21593
|
+
result.reason);
|
|
21594
|
+
}
|
|
21595
|
+
if (result.status === "accepted") {
|
|
21596
|
+
identity = result.identity;
|
|
21597
|
+
acceptance = result.evidence;
|
|
21598
|
+
}
|
|
20051
21599
|
}
|
|
20052
|
-
|
|
20053
|
-
|
|
20054
|
-
|
|
20055
|
-
|
|
20056
|
-
|
|
20057
|
-
|
|
21600
|
+
else {
|
|
21601
|
+
identity = await resolveCurrentNativeAgentSessionIdentity({
|
|
21602
|
+
options,
|
|
21603
|
+
agent: "codex",
|
|
21604
|
+
pid: transfer.process_pid,
|
|
21605
|
+
cwd: terminal.terminalControl.currentPath
|
|
21606
|
+
});
|
|
21607
|
+
if (identity) {
|
|
21608
|
+
acceptance = detectCodexRolloutAcceptance({
|
|
21609
|
+
anchor: authority.anchor,
|
|
21610
|
+
currentIdentity: identity,
|
|
21611
|
+
requestHash: transfer.request_hash
|
|
21612
|
+
});
|
|
21613
|
+
}
|
|
21614
|
+
}
|
|
21615
|
+
if (!identity || !acceptance) {
|
|
20058
21616
|
return false;
|
|
20059
21617
|
}
|
|
20060
21618
|
if (ledger.status === "agent_accepted") {
|
|
@@ -20212,7 +21770,7 @@ async function recoverAcceptedDeferredForegroundDispatch({ options, storeDir, te
|
|
|
20212
21770
|
releaseStateLock();
|
|
20213
21771
|
}
|
|
20214
21772
|
}
|
|
20215
|
-
function persistCommittedDeferredForegroundTurnAcceptance({ storeDir, terminal, transfer }) {
|
|
21773
|
+
async function persistCommittedDeferredForegroundTurnAcceptance({ options, storeDir, terminal, transfer }) {
|
|
20216
21774
|
const current = loadDeferredForegroundTransfer(storeDir, transfer.transfer_id);
|
|
20217
21775
|
if (current.status !== "committed" ||
|
|
20218
21776
|
!current.target_accepted_binding ||
|
|
@@ -20270,11 +21828,36 @@ function persistCommittedDeferredForegroundTurnAcceptance({ storeDir, terminal,
|
|
|
20270
21828
|
throw new Error(`deferred foreground transfer ${current.transfer_id} has invalid ` +
|
|
20271
21829
|
`${String(ledger.status)} dispatch evidence for committed recovery`);
|
|
20272
21830
|
}
|
|
20273
|
-
|
|
20274
|
-
|
|
20275
|
-
|
|
20276
|
-
|
|
20277
|
-
|
|
21831
|
+
let acceptance;
|
|
21832
|
+
if (authority.anchor.version === 3) {
|
|
21833
|
+
const inventory = await inspectCodexOpenRootRolloutInventory({
|
|
21834
|
+
options,
|
|
21835
|
+
pid: current.process_pid,
|
|
21836
|
+
cwd: terminal.terminalControl.currentPath
|
|
21837
|
+
});
|
|
21838
|
+
const result = detectCodexCandidateSetRolloutAcceptance({
|
|
21839
|
+
anchor: authority.anchor,
|
|
21840
|
+
currentInventory: inventory,
|
|
21841
|
+
requestHash: current.request_hash
|
|
21842
|
+
});
|
|
21843
|
+
if (result.status === "uncertain") {
|
|
21844
|
+
throw new Error(`Codex committed candidate acceptance is uncertain (${result.code}): ` +
|
|
21845
|
+
result.reason);
|
|
21846
|
+
}
|
|
21847
|
+
if (result.status === "accepted") {
|
|
21848
|
+
if (result.identity.sessionId !== identity.sessionId) {
|
|
21849
|
+
throw new Error("committed candidate acceptance resolved to a different native thread");
|
|
21850
|
+
}
|
|
21851
|
+
acceptance = result.evidence;
|
|
21852
|
+
}
|
|
21853
|
+
}
|
|
21854
|
+
else {
|
|
21855
|
+
acceptance = detectCodexRolloutAcceptance({
|
|
21856
|
+
anchor: authority.anchor,
|
|
21857
|
+
currentIdentity: identity,
|
|
21858
|
+
requestHash: current.request_hash
|
|
21859
|
+
});
|
|
21860
|
+
}
|
|
20278
21861
|
if (!acceptance) {
|
|
20279
21862
|
throw new Error(`deferred foreground transfer ${current.transfer_id} cannot re-prove ` +
|
|
20280
21863
|
"its exact request acceptance");
|
|
@@ -20331,6 +21914,15 @@ function persistCommittedDeferredForegroundTurnAcceptance({ storeDir, terminal,
|
|
|
20331
21914
|
}
|
|
20332
21915
|
async function recoverDeferredCodexForegroundTransferBeforeMutation({ options, terminal }) {
|
|
20333
21916
|
const storeDir = storeDirFromOptions(options);
|
|
21917
|
+
await withStoreWriterLeaseAsync(storeDir, async () => {
|
|
21918
|
+
await recoverDeferredCodexForegroundTransferWhileWriterLease({
|
|
21919
|
+
options,
|
|
21920
|
+
terminal,
|
|
21921
|
+
storeDir
|
|
21922
|
+
});
|
|
21923
|
+
});
|
|
21924
|
+
}
|
|
21925
|
+
async function recoverDeferredCodexForegroundTransferWhileWriterLease({ options, terminal, storeDir }) {
|
|
20334
21926
|
let transfers = listDeferredForegroundTransfers(storeDir);
|
|
20335
21927
|
// An abort intent is Store authority: it is finalized without consulting a
|
|
20336
21928
|
// live pane, PID, cwd, or process birth. Once the exact cleanup receipt is
|
|
@@ -20357,7 +21949,8 @@ async function recoverDeferredCodexForegroundTransferBeforeMutation({ options, t
|
|
|
20357
21949
|
let transfer = candidates[0];
|
|
20358
21950
|
const boundary = deferredCodexBoundaryFromTransfer({ terminal, transfer });
|
|
20359
21951
|
if (transfer.status === "committed") {
|
|
20360
|
-
const accepted = persistCommittedDeferredForegroundTurnAcceptance({
|
|
21952
|
+
const accepted = await persistCommittedDeferredForegroundTurnAcceptance({
|
|
21953
|
+
options,
|
|
20361
21954
|
storeDir,
|
|
20362
21955
|
terminal,
|
|
20363
21956
|
transfer
|
|
@@ -20413,6 +22006,9 @@ async function recoverDeferredCodexForegroundTransferBeforeMutation({ options, t
|
|
|
20413
22006
|
}
|
|
20414
22007
|
}
|
|
20415
22008
|
catch (error) {
|
|
22009
|
+
if (isStoreMutationLockTimeout(error)) {
|
|
22010
|
+
throw error;
|
|
22011
|
+
}
|
|
20416
22012
|
const durable = loadDeferredForegroundTransfer(storeDir, transfer.transfer_id);
|
|
20417
22013
|
if (durable.status === "dispatch_started") {
|
|
20418
22014
|
transfer = markDeferredCodexForegroundTransferUncertain({
|
|
@@ -20428,6 +22024,18 @@ async function recoverDeferredCodexForegroundTransferBeforeMutation({ options, t
|
|
|
20428
22024
|
: String(error)}`, { cause: error });
|
|
20429
22025
|
}
|
|
20430
22026
|
if (transfer.status !== "uncertain") {
|
|
22027
|
+
const pendingAuthority = loadDeferredForegroundTurnAuthority({
|
|
22028
|
+
storeDir,
|
|
22029
|
+
terminal,
|
|
22030
|
+
transfer
|
|
22031
|
+
});
|
|
22032
|
+
if (pendingAuthority.anchor.version === 3) {
|
|
22033
|
+
// A stable candidate inventory with no exact request match is normal
|
|
22034
|
+
// materialization latency, not ambiguity. Keep the durable Enter and
|
|
22035
|
+
// transfer fences active; monitor/recovery will rescan the same v3
|
|
22036
|
+
// anchor and must never replay terminal input.
|
|
22037
|
+
return;
|
|
22038
|
+
}
|
|
20431
22039
|
transfer = markDeferredCodexForegroundTransferUncertain({
|
|
20432
22040
|
options,
|
|
20433
22041
|
boundary,
|
|
@@ -21838,7 +23446,17 @@ function terminalBridgeCompletionFingerprint({ completion, terminalMessageId })
|
|
|
21838
23446
|
}))
|
|
21839
23447
|
.digest("hex");
|
|
21840
23448
|
}
|
|
21841
|
-
function prepareTerminalBridgeCompletionCallback(
|
|
23449
|
+
function prepareTerminalBridgeCompletionCallback(args) {
|
|
23450
|
+
const storeDir = pathsForConversationDir(path.dirname(args.statePath)).storeDir;
|
|
23451
|
+
const releaseTerminalLock = acquireTerminalBridgeSendLock(storeDir, args.terminalControl, { timeoutMs: 30000 });
|
|
23452
|
+
try {
|
|
23453
|
+
return withStoreWriterLease(storeDir, () => prepareTerminalBridgeCompletionCallbackWithLocksHeld(args));
|
|
23454
|
+
}
|
|
23455
|
+
finally {
|
|
23456
|
+
releaseTerminalLock();
|
|
23457
|
+
}
|
|
23458
|
+
}
|
|
23459
|
+
function prepareTerminalBridgeCompletionCallbackWithLocksHeld({ options, statePath, logPath, conversation, executor, terminalControl, terminalMessageId, completion, allowSupersedeRecovery = false, completionFingerprint = terminalBridgeCompletionFingerprint({
|
|
21842
23460
|
completion,
|
|
21843
23461
|
terminalMessageId
|
|
21844
23462
|
}) }) {
|
|
@@ -21926,21 +23544,26 @@ function prepareTerminalBridgeCompletionCallback({ options, statePath, logPath,
|
|
|
21926
23544
|
gatewayUrl: stringValue(claimedConversation.gateway_token)
|
|
21927
23545
|
? claimedConversation.gateway_url
|
|
21928
23546
|
: undefined,
|
|
21929
|
-
token: stringValue(claimedConversation.gateway_token)
|
|
23547
|
+
token: stringValue(claimedConversation.gateway_token),
|
|
23548
|
+
recordOnly: !stringValue(claimedConversation.gateway_method)
|
|
21930
23549
|
});
|
|
23550
|
+
if (prepared.outcome === "record_only" &&
|
|
23551
|
+
cliEnv().AKK_TEST_EXIT_AFTER_LOCAL_COMPLETION_STATE === "1") {
|
|
23552
|
+
// The final Turn phase, completion claim, detection event, and local
|
|
23553
|
+
// message are durable. Startup reconciliation must finish the terminal
|
|
23554
|
+
// ledger without manufacturing a callback outbox or replaying input.
|
|
23555
|
+
cliExit(86);
|
|
23556
|
+
}
|
|
21931
23557
|
claim.release();
|
|
21932
23558
|
claimReleased = true;
|
|
21933
|
-
|
|
21934
|
-
|
|
21935
|
-
|
|
21936
|
-
|
|
21937
|
-
|
|
21938
|
-
|
|
21939
|
-
|
|
21940
|
-
|
|
21941
|
-
}
|
|
21942
|
-
finally {
|
|
21943
|
-
releaseTerminalLock();
|
|
23559
|
+
if (!resolveTerminalBridgeDispatchLedger({
|
|
23560
|
+
terminalControl,
|
|
23561
|
+
conversation: claimedConversation,
|
|
23562
|
+
expectedMessageId: terminalMessageId,
|
|
23563
|
+
reason: "terminal bridge task reached durable completion"
|
|
23564
|
+
})) {
|
|
23565
|
+
throw new Error(`terminal completion ${claimedConversation.conversation_id} ` +
|
|
23566
|
+
"changed before its dispatch ledger could be settled");
|
|
21944
23567
|
}
|
|
21945
23568
|
return {
|
|
21946
23569
|
claimed: true,
|
|
@@ -22057,73 +23680,76 @@ function terminalBridgeActivityPersistIntervalMs(timeoutMinutes, pollIntervalMs)
|
|
|
22057
23680
|
return Math.max(pollIntervalMs, Math.min(timeoutMinutes * 30 * 1000, 5 * 60 * 1000));
|
|
22058
23681
|
}
|
|
22059
23682
|
function persistTerminalBridgeActivity({ conversation, statePath, logPath, observedAtMs, reason, activityState, timeoutMinutes, hardTimeoutMinutes }) {
|
|
22060
|
-
const
|
|
22061
|
-
|
|
22062
|
-
const
|
|
22063
|
-
|
|
22064
|
-
|
|
22065
|
-
|
|
22066
|
-
|
|
22067
|
-
|
|
22068
|
-
|
|
22069
|
-
|
|
22070
|
-
|
|
22071
|
-
|
|
22072
|
-
|
|
22073
|
-
|
|
22074
|
-
|
|
22075
|
-
|
|
22076
|
-
|
|
22077
|
-
|
|
22078
|
-
|
|
22079
|
-
|
|
22080
|
-
|
|
22081
|
-
|
|
22082
|
-
|
|
22083
|
-
|
|
22084
|
-
...
|
|
22085
|
-
|
|
22086
|
-
|
|
22087
|
-
|
|
22088
|
-
|
|
22089
|
-
|
|
22090
|
-
|
|
22091
|
-
|
|
22092
|
-
|
|
22093
|
-
|
|
22094
|
-
|
|
22095
|
-
|
|
22096
|
-
conversation_id: currentConversation.conversation_id,
|
|
22097
|
-
event: "terminal_bridge_activity_observed",
|
|
22098
|
-
reason,
|
|
22099
|
-
last_activity_at: observedAt,
|
|
22100
|
-
terminal_activity_state: activityState
|
|
22101
|
-
});
|
|
22102
|
-
if (inactivityDeadlineAt) {
|
|
23683
|
+
const storeDir = pathsForConversationDir(path.dirname(statePath)).storeDir;
|
|
23684
|
+
return withStoreWriterLease(storeDir, () => {
|
|
23685
|
+
const releaseLock = acquireFileLock(`${statePath}.lock`);
|
|
23686
|
+
try {
|
|
23687
|
+
const currentConversation = loadState(statePath);
|
|
23688
|
+
if (!isWaitingForAgent(currentConversation.status)) {
|
|
23689
|
+
return currentConversation;
|
|
23690
|
+
}
|
|
23691
|
+
const expectedNativeTakeover = isRecord(conversation.native_session_takeover)
|
|
23692
|
+
? conversation.native_session_takeover
|
|
23693
|
+
: {};
|
|
23694
|
+
const nativeTakeover = isRecord(currentConversation.native_session_takeover)
|
|
23695
|
+
? currentConversation.native_session_takeover
|
|
23696
|
+
: {};
|
|
23697
|
+
if (nativeTakeover["terminal_bridge_message_id"] !==
|
|
23698
|
+
expectedNativeTakeover["terminal_bridge_message_id"]) {
|
|
23699
|
+
return currentConversation;
|
|
23700
|
+
}
|
|
23701
|
+
const previousActivityAtMs = validTimestampMs(nativeTakeover["terminal_bridge_last_activity_at"]);
|
|
23702
|
+
const observedAt = new Date(observedAtMs).toISOString();
|
|
23703
|
+
const inactivityDeadlineAt = Number.isFinite(timeoutMinutes) && timeoutMinutes > 0
|
|
23704
|
+
? new Date(observedAtMs + timeoutMinutes * 60 * 1000).toISOString()
|
|
23705
|
+
: undefined;
|
|
23706
|
+
const nextConversation = {
|
|
23707
|
+
...currentConversation,
|
|
23708
|
+
native_session_takeover: {
|
|
23709
|
+
...nativeTakeover,
|
|
23710
|
+
terminal_bridge_last_activity_at: observedAt,
|
|
23711
|
+
terminal_bridge_last_activity_reason: reason,
|
|
23712
|
+
terminal_bridge_inactivity_deadline_at: inactivityDeadlineAt,
|
|
23713
|
+
terminal_bridge_inactivity_timeout_minutes: timeoutMinutes,
|
|
23714
|
+
terminal_bridge_hard_timeout_minutes: hardTimeoutMinutes
|
|
23715
|
+
},
|
|
23716
|
+
updated_at: observedAt
|
|
23717
|
+
};
|
|
23718
|
+
saveState(statePath, nextConversation);
|
|
22103
23719
|
appendEvent(logPath, {
|
|
22104
23720
|
ts: observedAt,
|
|
22105
23721
|
conversation_id: currentConversation.conversation_id,
|
|
22106
|
-
event: "
|
|
23722
|
+
event: "terminal_bridge_activity_observed",
|
|
22107
23723
|
reason,
|
|
22108
|
-
previous_last_activity_at: previousActivityAtMs === undefined
|
|
22109
|
-
? null
|
|
22110
|
-
: new Date(previousActivityAtMs).toISOString(),
|
|
22111
23724
|
last_activity_at: observedAt,
|
|
22112
|
-
|
|
22113
|
-
agent_timeout_minutes: timeoutMinutes
|
|
23725
|
+
terminal_activity_state: activityState
|
|
22114
23726
|
});
|
|
23727
|
+
if (inactivityDeadlineAt) {
|
|
23728
|
+
appendEvent(logPath, {
|
|
23729
|
+
ts: observedAt,
|
|
23730
|
+
conversation_id: currentConversation.conversation_id,
|
|
23731
|
+
event: "terminal_bridge_inactivity_deadline_extended",
|
|
23732
|
+
reason,
|
|
23733
|
+
previous_last_activity_at: previousActivityAtMs === undefined
|
|
23734
|
+
? null
|
|
23735
|
+
: new Date(previousActivityAtMs).toISOString(),
|
|
23736
|
+
last_activity_at: observedAt,
|
|
23737
|
+
inactivity_deadline_at: inactivityDeadlineAt,
|
|
23738
|
+
agent_timeout_minutes: timeoutMinutes
|
|
23739
|
+
});
|
|
23740
|
+
}
|
|
23741
|
+
return nextConversation;
|
|
22115
23742
|
}
|
|
22116
|
-
|
|
22117
|
-
|
|
22118
|
-
|
|
22119
|
-
|
|
22120
|
-
}
|
|
23743
|
+
finally {
|
|
23744
|
+
releaseLock();
|
|
23745
|
+
}
|
|
23746
|
+
});
|
|
22121
23747
|
}
|
|
22122
23748
|
function persistTerminalBridgeDetectorDiagnostic({ statePath, logPath, expectedConversationId, expectedMessageId, limitation, fingerprint }) {
|
|
22123
23749
|
const storeDir = pathsForConversationDir(path.dirname(statePath)).storeDir;
|
|
22124
|
-
|
|
22125
|
-
|
|
22126
|
-
|
|
23750
|
+
return withStoreWriterLease(storeDir, () => {
|
|
23751
|
+
const releaseLock = acquireFileLock(`${statePath}.lock`);
|
|
23752
|
+
try {
|
|
22127
23753
|
const conversation = loadState(statePath);
|
|
22128
23754
|
const nativeTakeover = isRecord(conversation.native_session_takeover)
|
|
22129
23755
|
? conversation.native_session_takeover
|
|
@@ -22208,11 +23834,11 @@ function persistTerminalBridgeDetectorDiagnostic({ statePath, logPath, expectedC
|
|
|
22208
23834
|
conversation: nextConversation,
|
|
22209
23835
|
diagnostic: nextDiagnostic
|
|
22210
23836
|
};
|
|
22211
|
-
}
|
|
22212
|
-
|
|
22213
|
-
|
|
22214
|
-
|
|
22215
|
-
}
|
|
23837
|
+
}
|
|
23838
|
+
finally {
|
|
23839
|
+
releaseLock();
|
|
23840
|
+
}
|
|
23841
|
+
});
|
|
22216
23842
|
}
|
|
22217
23843
|
function terminalBridgeApprovalCandidate({ executor, terminalControl, terminalStatus, fingerprint }) {
|
|
22218
23844
|
const approval = isRecord(terminalStatus?.approval_state) ? terminalStatus.approval_state : {};
|
|
@@ -22506,14 +24132,17 @@ function resolveCallbackGatewayRoute(...candidates) {
|
|
|
22506
24132
|
};
|
|
22507
24133
|
}
|
|
22508
24134
|
function runCallbackTransaction(options) {
|
|
22509
|
-
const releaseLock = acquireFileLock(`${options.statePath}.lock`);
|
|
22510
24135
|
let prepared;
|
|
22511
|
-
|
|
22512
|
-
|
|
22513
|
-
|
|
22514
|
-
|
|
22515
|
-
|
|
22516
|
-
|
|
24136
|
+
const callbackStoreDir = pathsForConversationDir(path.dirname(options.statePath)).storeDir;
|
|
24137
|
+
withStoreWriterLease(callbackStoreDir, () => {
|
|
24138
|
+
const releaseLock = acquireFileLock(`${options.statePath}.lock`);
|
|
24139
|
+
try {
|
|
24140
|
+
prepared = prepareLockedCallback(options);
|
|
24141
|
+
}
|
|
24142
|
+
finally {
|
|
24143
|
+
releaseLock();
|
|
24144
|
+
}
|
|
24145
|
+
});
|
|
22517
24146
|
return runPreparedCallback(prepared);
|
|
22518
24147
|
}
|
|
22519
24148
|
function prepareLockedCallback(options) {
|
|
@@ -22523,6 +24152,12 @@ function prepareLockedCallback(options) {
|
|
|
22523
24152
|
? options.conversationOverride
|
|
22524
24153
|
: loadState(options.statePath);
|
|
22525
24154
|
const conversation = normalizeLegacyCallbackStatus(loadedConversation);
|
|
24155
|
+
const callbackStoreDir = pathsForConversationDir(path.dirname(options.statePath)).storeDir;
|
|
24156
|
+
assertConversationHasNoNonterminalDeferredForegroundTransfer({
|
|
24157
|
+
storeDir: callbackStoreDir,
|
|
24158
|
+
conversation,
|
|
24159
|
+
action: "prepare callback for"
|
|
24160
|
+
});
|
|
22526
24161
|
const executor = executorForConversation(conversation);
|
|
22527
24162
|
const persistedGatewayRoute = resolveCallbackGatewayRoute({
|
|
22528
24163
|
gatewayUrl: options.gatewayUrl,
|
|
@@ -23607,6 +25242,10 @@ function acquireFileLock(lockPath, { timeoutMs = 5000, retryMs = 50 } = {}) {
|
|
|
23607
25242
|
}
|
|
23608
25243
|
}
|
|
23609
25244
|
}
|
|
25245
|
+
function isStoreMutationLockTimeout(error) {
|
|
25246
|
+
return error instanceof StoreLockTimeoutError ||
|
|
25247
|
+
(isRecord(error) && error.code === "LOCK_TIMEOUT");
|
|
25248
|
+
}
|
|
23610
25249
|
function staleFileLock(lockPath) {
|
|
23611
25250
|
try {
|
|
23612
25251
|
const stat = fs.lstatSync(lockPath);
|
|
@@ -24239,72 +25878,80 @@ function isZombieProcess(pid) {
|
|
|
24239
25878
|
return result.stdout.trim().toUpperCase().startsWith("Z");
|
|
24240
25879
|
}
|
|
24241
25880
|
function markConversationStalled({ statePath, logPath, reason, detail = {} }) {
|
|
24242
|
-
const
|
|
25881
|
+
const storeDir = pathsForConversationDir(path.dirname(statePath)).storeDir;
|
|
24243
25882
|
let stalledConversation;
|
|
24244
25883
|
let stalledMessage;
|
|
24245
|
-
|
|
24246
|
-
|
|
24247
|
-
|
|
24248
|
-
|
|
25884
|
+
let unchangedConversation;
|
|
25885
|
+
withStoreWriterLease(storeDir, () => {
|
|
25886
|
+
const releaseLock = acquireFileLock(`${statePath}.lock`);
|
|
25887
|
+
try {
|
|
25888
|
+
const conversation = loadState(statePath);
|
|
25889
|
+
if (!isWaitingForAgent(conversation.status)) {
|
|
25890
|
+
runtimeLog("info", "executor_monitor_finished", {
|
|
25891
|
+
conversation_id: conversation.conversation_id,
|
|
25892
|
+
status: conversation.status,
|
|
25893
|
+
reason: "conversation_changed_before_stall"
|
|
25894
|
+
});
|
|
25895
|
+
unchangedConversation = conversation;
|
|
25896
|
+
return;
|
|
25897
|
+
}
|
|
25898
|
+
const now = cliNow().toISOString();
|
|
25899
|
+
const executor = executorForConversation(conversation);
|
|
25900
|
+
const terminalBridge = terminalBridgeEnabled(conversation);
|
|
25901
|
+
const shouldNotify = Boolean(conversation.gateway_method && !conversation.stalled_notification_sent_at);
|
|
25902
|
+
stalledMessage = shouldNotify
|
|
25903
|
+
? createMessage({
|
|
25904
|
+
conversation,
|
|
25905
|
+
from: executor.actor,
|
|
25906
|
+
to: "openclaw",
|
|
25907
|
+
type: "error",
|
|
25908
|
+
requiresResponse: false,
|
|
25909
|
+
body: [
|
|
25910
|
+
`AKK marked this ${executor.display_name} task as stalled: ${reason}.`,
|
|
25911
|
+
"",
|
|
25912
|
+
`Turn: ${turnIdForConversation(conversation)}`,
|
|
25913
|
+
`AKK session: ${sessionIdForConversation(conversation)}`,
|
|
25914
|
+
`Agent session: ${executor.session}`,
|
|
25915
|
+
terminalBridge
|
|
25916
|
+
? `Use \`AKK status --turn ${turnIdForConversation(conversation)}\` for details, \`AKK renew --turn ${turnIdForConversation(conversation)}\` to resume monitoring in this Turn, or \`AKK close --turn ${turnIdForConversation(conversation)}\` to close it. Start any independent retry with \`AKK send --session ${sessionIdForConversation(conversation)}\`.`
|
|
25917
|
+
: `Use \`AKK status --turn ${turnIdForConversation(conversation)}\` for details or \`AKK close --turn ${turnIdForConversation(conversation)}\` to close this Turn.`
|
|
25918
|
+
].join("\n")
|
|
25919
|
+
})
|
|
25920
|
+
: undefined;
|
|
25921
|
+
stalledConversation = {
|
|
25922
|
+
...conversation,
|
|
25923
|
+
status: "stalled",
|
|
25924
|
+
stalled_at: now,
|
|
25925
|
+
stalled_reason: reason,
|
|
25926
|
+
stalled_notification_sent_at: shouldNotify ? now : conversation.stalled_notification_sent_at,
|
|
25927
|
+
stalled_notification_message_id: stalledMessage?.id ?? conversation.stalled_notification_message_id,
|
|
25928
|
+
updated_at: now
|
|
25929
|
+
};
|
|
25930
|
+
saveState(statePath, stalledConversation);
|
|
25931
|
+
appendEvent(logPath, {
|
|
25932
|
+
ts: now,
|
|
24249
25933
|
conversation_id: conversation.conversation_id,
|
|
24250
|
-
|
|
24251
|
-
|
|
25934
|
+
event: "conversation_stalled",
|
|
25935
|
+
status: "stalled",
|
|
25936
|
+
reason,
|
|
25937
|
+
...detail
|
|
25938
|
+
});
|
|
25939
|
+
runtimeLog("warn", "conversation_stalled", {
|
|
25940
|
+
conversation_id: conversation.conversation_id,
|
|
25941
|
+
agent: executorForConversation(conversation).kind,
|
|
25942
|
+
executor_session: executorForConversation(conversation).session,
|
|
25943
|
+
state_path: statePath,
|
|
25944
|
+
event_log_path: logPath,
|
|
25945
|
+
reason,
|
|
25946
|
+
...detail
|
|
24252
25947
|
});
|
|
24253
|
-
return conversation;
|
|
24254
25948
|
}
|
|
24255
|
-
|
|
24256
|
-
|
|
24257
|
-
|
|
24258
|
-
|
|
24259
|
-
|
|
24260
|
-
|
|
24261
|
-
conversation,
|
|
24262
|
-
from: executor.actor,
|
|
24263
|
-
to: "openclaw",
|
|
24264
|
-
type: "error",
|
|
24265
|
-
requiresResponse: false,
|
|
24266
|
-
body: [
|
|
24267
|
-
`AKK marked this ${executor.display_name} task as stalled: ${reason}.`,
|
|
24268
|
-
"",
|
|
24269
|
-
`Turn: ${turnIdForConversation(conversation)}`,
|
|
24270
|
-
`AKK session: ${sessionIdForConversation(conversation)}`,
|
|
24271
|
-
`Agent session: ${executor.session}`,
|
|
24272
|
-
terminalBridge
|
|
24273
|
-
? `Use \`AKK status --turn ${turnIdForConversation(conversation)}\` for details, \`AKK renew --turn ${turnIdForConversation(conversation)}\` to resume monitoring in this Turn, or \`AKK close --turn ${turnIdForConversation(conversation)}\` to close it. Start any independent retry with \`AKK send --session ${sessionIdForConversation(conversation)}\`.`
|
|
24274
|
-
: `Use \`AKK status --turn ${turnIdForConversation(conversation)}\` for details or \`AKK close --turn ${turnIdForConversation(conversation)}\` to close this Turn.`
|
|
24275
|
-
].join("\n")
|
|
24276
|
-
})
|
|
24277
|
-
: undefined;
|
|
24278
|
-
stalledConversation = {
|
|
24279
|
-
...conversation,
|
|
24280
|
-
status: "stalled",
|
|
24281
|
-
stalled_at: now,
|
|
24282
|
-
stalled_reason: reason,
|
|
24283
|
-
stalled_notification_sent_at: shouldNotify ? now : conversation.stalled_notification_sent_at,
|
|
24284
|
-
stalled_notification_message_id: stalledMessage?.id ?? conversation.stalled_notification_message_id,
|
|
24285
|
-
updated_at: now
|
|
24286
|
-
};
|
|
24287
|
-
saveState(statePath, stalledConversation);
|
|
24288
|
-
appendEvent(logPath, {
|
|
24289
|
-
ts: now,
|
|
24290
|
-
conversation_id: conversation.conversation_id,
|
|
24291
|
-
event: "conversation_stalled",
|
|
24292
|
-
status: "stalled",
|
|
24293
|
-
reason,
|
|
24294
|
-
...detail
|
|
24295
|
-
});
|
|
24296
|
-
runtimeLog("warn", "conversation_stalled", {
|
|
24297
|
-
conversation_id: conversation.conversation_id,
|
|
24298
|
-
agent: executorForConversation(conversation).kind,
|
|
24299
|
-
executor_session: executorForConversation(conversation).session,
|
|
24300
|
-
state_path: statePath,
|
|
24301
|
-
event_log_path: logPath,
|
|
24302
|
-
reason,
|
|
24303
|
-
...detail
|
|
24304
|
-
});
|
|
24305
|
-
}
|
|
24306
|
-
finally {
|
|
24307
|
-
releaseLock();
|
|
25949
|
+
finally {
|
|
25950
|
+
releaseLock();
|
|
25951
|
+
}
|
|
25952
|
+
});
|
|
25953
|
+
if (unchangedConversation) {
|
|
25954
|
+
return unchangedConversation;
|
|
24308
25955
|
}
|
|
24309
25956
|
if (stalledConversation && stalledMessage) {
|
|
24310
25957
|
deliverStalledNotification({
|
|
@@ -24397,6 +26044,11 @@ function reconcileIdleConversations(storeDir, options = {}, now = cliNow(), conv
|
|
|
24397
26044
|
ensureStoreWritable(storeDir);
|
|
24398
26045
|
const conversations = listConversations(storeDir).filter((conversation) => (conversationId === undefined || conversation.conversation_id === conversationId) &&
|
|
24399
26046
|
matchesConfiguredWorkspace(options.workspace, conversation.workspace));
|
|
26047
|
+
const reservedSourceTurnIds = new Set(listDeferredForegroundTransfers(storeDir)
|
|
26048
|
+
.filter((transfer) => transfer.version === 2 &&
|
|
26049
|
+
transfer.source_kind === "candidate_rollout_quiescent" &&
|
|
26050
|
+
!["resolved", "abort_resolved"].includes(transfer.status))
|
|
26051
|
+
.flatMap((transfer) => (transfer.source_turn_history ?? []).map((turn) => turn.turn_id)));
|
|
24400
26052
|
let closed = 0;
|
|
24401
26053
|
let skipped = 0;
|
|
24402
26054
|
for (const listedConversation of conversations) {
|
|
@@ -24410,6 +26062,10 @@ function reconcileIdleConversations(storeDir, options = {}, now = cliNow(), conv
|
|
|
24410
26062
|
if (now.getTime() - listedIdleSinceMs < timeoutMinutes * 60 * 1000) {
|
|
24411
26063
|
continue;
|
|
24412
26064
|
}
|
|
26065
|
+
if (reservedSourceTurnIds.has(turnIdForConversation(listedConversation))) {
|
|
26066
|
+
skipped += 1;
|
|
26067
|
+
continue;
|
|
26068
|
+
}
|
|
24413
26069
|
const statePath = listedConversation.state_path ??
|
|
24414
26070
|
statePathForConversationId(listedConversation.conversation_id, storeDir);
|
|
24415
26071
|
let releaseStateLock;
|
|
@@ -24987,7 +26643,7 @@ function usage() {
|
|
|
24987
26643
|
agent-knock-knock resume-thread --terminal <exact-terminal-id> (--selection-handle <handle> | --selection-snapshot <id> (--selection-number <n> | --selection-short-id <@id>)) --selection-scope <opaque-scope>
|
|
24988
26644
|
agent-knock-knock reconcile-binding --terminal <exact-terminal-id> --conflicting-session <session-id> --expected-session-revision <n> --expected-binding-token <token> --expected-terminal-token <token>
|
|
24989
26645
|
agent-knock-knock respond --turn <turn-id|selector> --message <text> [--conversation <selector>]
|
|
24990
|
-
agent-knock-knock approve [--turn <turn-id|selector>] [--conversation <selector>] --expected-approval-fingerprint <fingerprint>
|
|
26646
|
+
agent-knock-knock approve [--turn <turn-id|selector>] [--conversation <selector>] [--expected-terminal-token <token>] --expected-approval-fingerprint <fingerprint>
|
|
24991
26647
|
agent-knock-knock cancel [--turn <turn-id|selector>] [--conversation <selector>]
|
|
24992
26648
|
agent-knock-knock renew [--turn <turn-id|selector>] [--conversation <selector>]
|
|
24993
26649
|
agent-knock-knock retry-callback [--turn <turn-id|selector>] [--conversation <selector>]
|