@scotthuang/agent-knock-knock 0.12.4 → 0.12.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/README.md +6 -4
- package/dist/src/cli-core.js +2604 -160
- package/dist/src/cli-core.js.map +1 -1
- package/dist/src/deferred-foreground-transfer.d.ts +112 -0
- package/dist/src/deferred-foreground-transfer.js +1063 -0
- package/dist/src/deferred-foreground-transfer.js.map +1 -0
- package/dist/src/session-store.js +7 -5
- package/dist/src/session-store.js.map +1 -1
- package/dist/src/store.d.ts +3 -1
- package/dist/src/store.js +24 -14
- package/dist/src/store.js.map +1 -1
- package/package.json +1 -1
- package/templates/openclaw-skills/agent-knock-knock/SKILL.md +8 -5
package/dist/src/cli-core.js
CHANGED
|
@@ -11,11 +11,12 @@ import { captureClaudeTranscriptAnchor, defaultClaudeHome, detectClaudeTranscrip
|
|
|
11
11
|
import { captureCodexRolloutAcceptanceAnchor, detectCodexBoundRolloutCompletion, detectCodexRolloutAcceptance, terminalSubmissionReplayReceipt, validateTerminalSubmissionAcceptanceEvidence } from "./terminal-submission-acceptance.js";
|
|
12
12
|
import { CodexLocalSessionProvider } from "./codex-local-session-provider.js";
|
|
13
13
|
import { CodexStoreAdapter } from "./codex-store-adapter.js";
|
|
14
|
+
import { createDeferredForegroundTransferId, listDeferredForegroundTransfers, loadDeferredForegroundTransfer, saveDeferredForegroundTransfer } from "./deferred-foreground-transfer.js";
|
|
14
15
|
import { applyMessageToConversation, budgetAction, createConversation, createMessage, effectiveTurnStatus, executorForConversation, extractStructuredMessage, isTurnPhaseStatus, normalizeLegacyCallbackStatus, parseMessageJson, resolveExecutor, sessionIdForConversation, turnIdForConversation } from "./protocol.js";
|
|
15
16
|
import { EXECUTOR_KINDS, executorDefinitionForKind, isExecutorKind } from "./executors.js";
|
|
16
17
|
import { redactString, writeRuntimeLog } from "./runtime-log.js";
|
|
17
18
|
import { formatTranscript, readNdjsonLog } from "./transcript.js";
|
|
18
|
-
import { appendEvent, assertStoreWriterCompatible, defaultStoreDir, ensureDir, ensureStoreWritable, inspectStoreCompatibility, listConversations, logPathForStatePath, loadConversationById, loadState, messageEvent, pathsForConversation, pathsForConversationDir, saveState, StoreLockTimeoutError, statePathForConversationId, withStoreWriterLease, withStoreWriterLeaseAsync } from "./store.js";
|
|
19
|
+
import { appendEvent, assertStoreWriterCompatible, defaultStoreDir, ensureDir, ensureStoreWritable, inspectStoreCompatibility, listConversations, logPathForStatePath, loadConversationById, loadState, messageEvent, pathsForConversation, pathsForConversationDir, saveState, STORE_SESSION_AUTHORITY_PROTOCOL, StoreLockTimeoutError, statePathForConversationId, withStoreWriterLease, withStoreWriterLeaseAsync } from "./store.js";
|
|
19
20
|
import { createManagedSessionId, createNativeThreadTransitionId, humanObservedHandoffBindingToken, isExactNativeThreadId, legacyManagedSessionBindingToken, legacyUnmanagedTerminalBindingToken, managedSessionBindingToken, nativeThreadCommandFingerprint, terminalBindingFrom, unmanagedTerminalBindingToken } from "./managed-session.js";
|
|
20
21
|
import { classifyCodexLifecyclePostcondition, evaluateResumeCandidateAvailability, hasStrongCodexLifecycleIdentity, isFreshCodexPostProbeScreen } from "./native-thread-lifecycle-policy.js";
|
|
21
22
|
import { createNativeThreadResumeSnapshot, nativeThreadCandidateSnapshotFingerprint, nativeThreadResumeSnapshotRowsMatchCandidates, resolveNativeThreadResumeSelection, saveNativeThreadResumeSnapshot, sortNativeThreadCandidates, terminalActionFingerprint, verifiedPreviousResumeCandidate } from "./native-thread-resume-snapshot.js";
|
|
@@ -68,7 +69,8 @@ const TERMINAL_LEDGER_RECEIPT_IMMUTABLE_FIELDS = [
|
|
|
68
69
|
"executor_kind",
|
|
69
70
|
"openclaw_session",
|
|
70
71
|
"state_path",
|
|
71
|
-
"event_log_path"
|
|
72
|
+
"event_log_path",
|
|
73
|
+
"deferred_foreground_transfer_id"
|
|
72
74
|
];
|
|
73
75
|
const TERMINAL_BRIDGE_SUPERSEDE_STATUSES = new Set([
|
|
74
76
|
"created",
|
|
@@ -3022,7 +3024,7 @@ async function runList(options) {
|
|
|
3022
3024
|
storeDir,
|
|
3023
3025
|
terminals: physicalTerminals,
|
|
3024
3026
|
managedSessions,
|
|
3025
|
-
sessionAuthorityRequired: store.writer_protocol
|
|
3027
|
+
sessionAuthorityRequired: Number(store.writer_protocol) >= STORE_SESSION_AUTHORITY_PROTOCOL,
|
|
3026
3028
|
allConversations: allManagedConversations.filter((conversation) => matchesConfiguredWorkspace(options.workspace, conversation.workspace)),
|
|
3027
3029
|
displayedConversations: storedConversations,
|
|
3028
3030
|
includeAll,
|
|
@@ -3414,6 +3416,15 @@ async function terminalControlledListEntry(session, activeSessions, options, bri
|
|
|
3414
3416
|
};
|
|
3415
3417
|
}
|
|
3416
3418
|
function terminalFirstListProjection({ storeDir, terminals, managedSessions, sessionAuthorityRequired, allConversations, displayedConversations, includeAll, managedOnly, statusFilter, mutationsAllowed }) {
|
|
3419
|
+
const nonterminalDeferredTransfers = listDeferredForegroundTransfers(storeDir).filter((transfer) => !["resolved", "aborted", "abort_resolved"].includes(transfer.status));
|
|
3420
|
+
const nonterminalDeferredTransferIds = new Set(nonterminalDeferredTransfers.map((transfer) => transfer.transfer_id));
|
|
3421
|
+
const conversationHasNonterminalDeferredTransfer = (conversation) => {
|
|
3422
|
+
const takeover = isRecord(conversation.native_session_takeover)
|
|
3423
|
+
? conversation.native_session_takeover
|
|
3424
|
+
: undefined;
|
|
3425
|
+
const transferId = stringValue(takeover?.deferred_foreground_transfer_id);
|
|
3426
|
+
return Boolean(transferId && nonterminalDeferredTransferIds.has(transferId));
|
|
3427
|
+
};
|
|
3417
3428
|
const discoveredTerminalControls = terminals.flatMap((terminal) => {
|
|
3418
3429
|
const control = isRecord(terminal.terminal_control)
|
|
3419
3430
|
? terminal.terminal_control
|
|
@@ -3425,6 +3436,9 @@ function terminalFirstListProjection({ storeDir, terminals, managedSessions, ses
|
|
|
3425
3436
|
const terminalControl = isRecord(terminal.terminal_control)
|
|
3426
3437
|
? terminal.terminal_control
|
|
3427
3438
|
: undefined;
|
|
3439
|
+
const terminalHasNonterminalDeferredTransfer = Boolean(terminalControl && nonterminalDeferredTransfers.some((transfer) => transfer.terminal_id === String(terminal.id) &&
|
|
3440
|
+
transfer.process_pid === Number(terminal.pid) &&
|
|
3441
|
+
terminalControlEvidenceMatches(transfer.terminal_endpoint, terminalControl)));
|
|
3428
3442
|
const allRelated = terminalControl
|
|
3429
3443
|
? allConversations.filter((conversation) => terminalControlsShareIncarnation(terminalControlForManagedConversation(conversation), terminalControl))
|
|
3430
3444
|
: [];
|
|
@@ -3529,10 +3543,13 @@ function terminalFirstListProjection({ storeDir, terminals, managedSessions, ses
|
|
|
3529
3543
|
const bindingAwareRawActions = authoritativeSession
|
|
3530
3544
|
? actionsForManagedSessionBinding(rawActions, authoritativeSession)
|
|
3531
3545
|
: rawActions;
|
|
3532
|
-
const
|
|
3546
|
+
const sessionAwareRawActionsBase = authoritativeSession &&
|
|
3533
3547
|
managedSessionHasUnresolvedNativeTransition(storeDir, authoritativeSession)
|
|
3534
3548
|
? Object.fromEntries(Object.entries(bindingAwareRawActions).filter(([actionName]) => actionName !== "native_inspect"))
|
|
3535
3549
|
: bindingAwareRawActions;
|
|
3550
|
+
const sessionAwareRawActions = terminalHasNonterminalDeferredTransfer
|
|
3551
|
+
? readOnlyListActions(sessionAwareRawActionsBase)
|
|
3552
|
+
: sessionAwareRawActionsBase;
|
|
3536
3553
|
const soleBindingConflict = conflictingBoundSessionClaims.length === 1
|
|
3537
3554
|
? conflictingBoundSessionClaims[0]
|
|
3538
3555
|
: undefined;
|
|
@@ -3658,6 +3675,76 @@ function terminalFirstListProjection({ storeDir, terminals, managedSessions, ses
|
|
|
3658
3675
|
sourceSession: soleBindingConflict.session
|
|
3659
3676
|
})
|
|
3660
3677
|
: undefined;
|
|
3678
|
+
const deferredCodexSource = authoritativeSession &&
|
|
3679
|
+
terminal.agent === "codex" &&
|
|
3680
|
+
nativeIdentityObservation?.status === "verified_absent"
|
|
3681
|
+
? authoritativeSession
|
|
3682
|
+
: undefined;
|
|
3683
|
+
const deferredCodexProcessUuid = stringValue(terminal.native_agent_process_uuid);
|
|
3684
|
+
const deferredCodexProcessBirth = stringValue(terminal.native_agent_process_birth);
|
|
3685
|
+
const deferredCodexWorkspace = stringValue(terminal.workspace ?? terminal.cwd);
|
|
3686
|
+
const deferredCodexSourceNativeThreadId = stringValue(deferredCodexSource?.binding?.native_thread_id)?.toLowerCase();
|
|
3687
|
+
const deferredCodexSourceActiveElsewhere = Boolean(deferredCodexSourceNativeThreadId &&
|
|
3688
|
+
terminals.some((candidate) => candidate !== terminal &&
|
|
3689
|
+
candidate.agent === "codex" &&
|
|
3690
|
+
stringValue(candidate.native_agent_session_id)?.toLowerCase() ===
|
|
3691
|
+
deferredCodexSourceNativeThreadId));
|
|
3692
|
+
const deferredCodexDispatchSnapshot = terminalControl
|
|
3693
|
+
? tryDeferredCodexForegroundDispatchSnapshot(terminalControl)
|
|
3694
|
+
: undefined;
|
|
3695
|
+
const deferredCodexForegroundEligible = Boolean(mutationsAllowed &&
|
|
3696
|
+
!terminalHasNonterminalDeferredTransfer &&
|
|
3697
|
+
deferredCodexSource &&
|
|
3698
|
+
terminalControl &&
|
|
3699
|
+
hasCanonicalTerminalEndpoint(terminalControl) &&
|
|
3700
|
+
discoveredOwnership.state === "none" &&
|
|
3701
|
+
unresolvedSessionClaims.length === 0 &&
|
|
3702
|
+
matchingSessions.length === 1 &&
|
|
3703
|
+
conflictingBoundSessionClaims.length === 0 &&
|
|
3704
|
+
deferredCodexProcessUuid &&
|
|
3705
|
+
deferredCodexProcessBirth &&
|
|
3706
|
+
deferredCodexWorkspace &&
|
|
3707
|
+
exactBoundCodexStatusCardSourceForDeferredSend({
|
|
3708
|
+
session: deferredCodexSource,
|
|
3709
|
+
terminalId: String(terminal.id),
|
|
3710
|
+
terminalControl,
|
|
3711
|
+
pid: Number(terminal.pid),
|
|
3712
|
+
workspace: deferredCodexWorkspace,
|
|
3713
|
+
processUuid: deferredCodexProcessUuid,
|
|
3714
|
+
processBirth: deferredCodexProcessBirth
|
|
3715
|
+
}) &&
|
|
3716
|
+
managedTurnsForSession(storeDir, deferredCodexSource.session_id).length === 0 &&
|
|
3717
|
+
!deferredCodexSource.last_transition_id &&
|
|
3718
|
+
!managedSessionHasAnyNativeTransition(storeDir, deferredCodexSource) &&
|
|
3719
|
+
terminalBlockingTurns.length === 0 &&
|
|
3720
|
+
terminal.orphaned_terminal_dispatch === undefined &&
|
|
3721
|
+
deferredCodexDispatchSnapshot &&
|
|
3722
|
+
["idle", "unknown"].includes(String(terminal.activity_state)) &&
|
|
3723
|
+
automatedInputComposerReady === true &&
|
|
3724
|
+
!(isRecord(terminal.approval_state) &&
|
|
3725
|
+
terminal.approval_state.blocked === true) &&
|
|
3726
|
+
!deferredCodexSourceActiveElsewhere &&
|
|
3727
|
+
terminalControl.capabilities.includes("send_keys") &&
|
|
3728
|
+
terminalControl.capabilities.includes("screen_status") &&
|
|
3729
|
+
isRecord(rawActions.send));
|
|
3730
|
+
const deferredCodexForegroundToken = deferredCodexForegroundEligible &&
|
|
3731
|
+
deferredCodexSource &&
|
|
3732
|
+
terminalControl &&
|
|
3733
|
+
deferredCodexProcessUuid &&
|
|
3734
|
+
deferredCodexProcessBirth &&
|
|
3735
|
+
deferredCodexWorkspace &&
|
|
3736
|
+
deferredCodexDispatchSnapshot
|
|
3737
|
+
? deferredCodexForegroundBindingToken({
|
|
3738
|
+
terminalId: String(terminal.id),
|
|
3739
|
+
terminalControl,
|
|
3740
|
+
pid: Number(terminal.pid),
|
|
3741
|
+
workspace: deferredCodexWorkspace,
|
|
3742
|
+
processUuid: deferredCodexProcessUuid,
|
|
3743
|
+
processBirth: deferredCodexProcessBirth,
|
|
3744
|
+
sourceSession: deferredCodexSource,
|
|
3745
|
+
dispatchSnapshot: deferredCodexDispatchSnapshot
|
|
3746
|
+
})
|
|
3747
|
+
: undefined;
|
|
3661
3748
|
const reconcileBindingAction = mutationsAllowed &&
|
|
3662
3749
|
discoveredOwnership.state === "none" &&
|
|
3663
3750
|
unresolvedSessionClaims.length === 0 &&
|
|
@@ -3754,7 +3841,8 @@ function terminalFirstListProjection({ storeDir, terminals, managedSessions, ses
|
|
|
3754
3841
|
// the live native thread or Turn generation changed. `blocking_turns` is
|
|
3755
3842
|
// reserved for other same-incarnation blockers such as legacy collateral
|
|
3756
3843
|
// stalls; clear those first, refresh, and only then decide the handoff.
|
|
3757
|
-
const terminalRecoveryBlockingTurns = terminalBlockingTurns.filter((turn) => !blockingHandoffTurnIds.has(turn.conversation_id)
|
|
3844
|
+
const terminalRecoveryBlockingTurns = terminalBlockingTurns.filter((turn) => !blockingHandoffTurnIds.has(turn.conversation_id) &&
|
|
3845
|
+
!conversationHasNonterminalDeferredTransfer(turn));
|
|
3758
3846
|
const terminalCanAcceptSend = ownership.state === "none" && isRecord(sessionAwareRawActions.send);
|
|
3759
3847
|
if (ownership.state === "current" &&
|
|
3760
3848
|
!allRelated.some((conversation) => conversation.conversation_id === ownership.conversation.conversation_id)) {
|
|
@@ -3791,7 +3879,9 @@ function terminalFirstListProjection({ storeDir, terminals, managedSessions, ses
|
|
|
3791
3879
|
const currentTurnValue = ownership.state === "current"
|
|
3792
3880
|
? currentManagedTurnForTerminal(ownership.conversation, terminal, sessionAwareRawActions)
|
|
3793
3881
|
: undefined;
|
|
3794
|
-
const currentTurnProjection = currentTurnValue && !mutationsAllowed
|
|
3882
|
+
const currentTurnProjection = currentTurnValue && (!mutationsAllowed ||
|
|
3883
|
+
(ownership.state === "current" &&
|
|
3884
|
+
conversationHasNonterminalDeferredTransfer(ownership.conversation)))
|
|
3795
3885
|
? readOnlyManagedTurn(currentTurnValue)
|
|
3796
3886
|
: currentTurnValue;
|
|
3797
3887
|
const currentTurn = currentTurnProjection
|
|
@@ -3804,7 +3894,9 @@ function terminalFirstListProjection({ storeDir, terminals, managedSessions, ses
|
|
|
3804
3894
|
const recentTurnValue = recentConversation
|
|
3805
3895
|
? historicalManagedTurnForTerminal(recentConversation, terminalCanAcceptSend, terminal)
|
|
3806
3896
|
: undefined;
|
|
3807
|
-
const recentTurnProjection = recentTurnValue && !mutationsAllowed
|
|
3897
|
+
const recentTurnProjection = recentTurnValue && (!mutationsAllowed ||
|
|
3898
|
+
Boolean(recentConversation &&
|
|
3899
|
+
conversationHasNonterminalDeferredTransfer(recentConversation)))
|
|
3808
3900
|
? readOnlyManagedTurn(recentTurnValue)
|
|
3809
3901
|
: recentTurnValue;
|
|
3810
3902
|
const recentTurn = recentTurnProjection
|
|
@@ -3815,7 +3907,10 @@ function terminalFirstListProjection({ storeDir, terminals, managedSessions, ses
|
|
|
3815
3907
|
: [];
|
|
3816
3908
|
const history = historyConversations.map((conversation) => {
|
|
3817
3909
|
const turn = historicalManagedTurnForTerminal(conversation, terminalCanAcceptSend, terminal);
|
|
3818
|
-
return withoutGenericHandoffSourceClose(mutationsAllowed
|
|
3910
|
+
return withoutGenericHandoffSourceClose(mutationsAllowed &&
|
|
3911
|
+
!conversationHasNonterminalDeferredTransfer(conversation)
|
|
3912
|
+
? turn
|
|
3913
|
+
: readOnlyManagedTurn(turn), blockingHandoffTurnIds);
|
|
3819
3914
|
});
|
|
3820
3915
|
const visibleTurnIds = new Set([currentTurn, recentTurn, ...history]
|
|
3821
3916
|
.map((turn) => stringValue(turn?.conversation_id))
|
|
@@ -3879,14 +3974,28 @@ function terminalFirstListProjection({ storeDir, terminals, managedSessions, ses
|
|
|
3879
3974
|
? { reconcile_binding: reconcileBindingAction }
|
|
3880
3975
|
: {})
|
|
3881
3976
|
}
|
|
3882
|
-
:
|
|
3883
|
-
|
|
3884
|
-
isRecord(sessionAwareRawActions.send)
|
|
3977
|
+
: deferredCodexForegroundToken &&
|
|
3978
|
+
isRecord(rawActions.send)
|
|
3885
3979
|
? {
|
|
3886
3980
|
...sessionAwareRawActions,
|
|
3887
|
-
send:
|
|
3981
|
+
send: {
|
|
3982
|
+
...rawActions.send,
|
|
3983
|
+
arguments: {
|
|
3984
|
+
...(isRecord(rawActions.send.arguments)
|
|
3985
|
+
? rawActions.send.arguments
|
|
3986
|
+
: {}),
|
|
3987
|
+
expected_terminal_token: deferredCodexForegroundToken
|
|
3988
|
+
}
|
|
3989
|
+
}
|
|
3888
3990
|
}
|
|
3889
|
-
:
|
|
3991
|
+
: managedSessionId &&
|
|
3992
|
+
sessionBindingMatchesLiveTerminal &&
|
|
3993
|
+
isRecord(sessionAwareRawActions.send)
|
|
3994
|
+
? {
|
|
3995
|
+
...sessionAwareRawActions,
|
|
3996
|
+
send: sendActionForManagedSession(sessionAwareRawActions.send, managedSessionId)
|
|
3997
|
+
}
|
|
3998
|
+
: sessionAwareRawActions;
|
|
3890
3999
|
const authoritativeBinding = authoritativeSession?.binding;
|
|
3891
4000
|
const authoritativeIdentity = authoritativeBinding?.native_thread_id
|
|
3892
4001
|
? {
|
|
@@ -3992,7 +4101,8 @@ function terminalFirstListProjection({ storeDir, terminals, managedSessions, ses
|
|
|
3992
4101
|
});
|
|
3993
4102
|
return {
|
|
3994
4103
|
...managedTurn,
|
|
3995
|
-
available_actions: mutationsAllowed
|
|
4104
|
+
available_actions: mutationsAllowed &&
|
|
4105
|
+
!conversationHasNonterminalDeferredTransfer(conversation)
|
|
3996
4106
|
? safeUnavailableManagedTurnActions(isRecord(managedTurn.available_actions)
|
|
3997
4107
|
? managedTurn.available_actions
|
|
3998
4108
|
: {})
|
|
@@ -4205,6 +4315,36 @@ function managedSessionHasUnresolvedNativeTransition(storeDir, session) {
|
|
|
4205
4315
|
}
|
|
4206
4316
|
return false;
|
|
4207
4317
|
}
|
|
4318
|
+
function managedSessionHasAnyNativeTransition(storeDir, session) {
|
|
4319
|
+
const root = nativeThreadTransitionsDir(storeDir);
|
|
4320
|
+
if (!fs.existsSync(root)) {
|
|
4321
|
+
return false;
|
|
4322
|
+
}
|
|
4323
|
+
let entries;
|
|
4324
|
+
try {
|
|
4325
|
+
entries = fs.readdirSync(root, { withFileTypes: true });
|
|
4326
|
+
}
|
|
4327
|
+
catch {
|
|
4328
|
+
return true;
|
|
4329
|
+
}
|
|
4330
|
+
for (const entry of entries) {
|
|
4331
|
+
if (!entry.isDirectory()) {
|
|
4332
|
+
continue;
|
|
4333
|
+
}
|
|
4334
|
+
let transition;
|
|
4335
|
+
try {
|
|
4336
|
+
transition = loadNativeThreadTransition(storeDir, entry.name);
|
|
4337
|
+
}
|
|
4338
|
+
catch {
|
|
4339
|
+
return true;
|
|
4340
|
+
}
|
|
4341
|
+
if (transition.source_session_id === session.session_id ||
|
|
4342
|
+
transition.target_session_id === session.session_id) {
|
|
4343
|
+
return true;
|
|
4344
|
+
}
|
|
4345
|
+
}
|
|
4346
|
+
return false;
|
|
4347
|
+
}
|
|
4208
4348
|
function terminalControlForManagedConversation(conversation) {
|
|
4209
4349
|
return terminalControlFromTakeover(isRecord(conversation.native_session_takeover)
|
|
4210
4350
|
? conversation.native_session_takeover
|
|
@@ -4695,13 +4835,13 @@ function managedListApprovalState(conversation) {
|
|
|
4695
4835
|
}
|
|
4696
4836
|
function listActionContracts() {
|
|
4697
4837
|
return {
|
|
4698
|
-
version:
|
|
4838
|
+
version: 11,
|
|
4699
4839
|
instructions: [
|
|
4700
4840
|
"Treat terminals[] as the primary resource and use only actions present in available_actions, except the snapshot-bound terminals[].handoff_decision.choices.take_over_current.action and an exact terminals[].blocking_turns[].recovery_action. Either nested action requires explicit user confirmation; after it succeeds, refresh list before any follow-current send.",
|
|
4701
4841
|
"An existing managed session's ordinary send targets session_id and creates a new turn. A turn id is never an ordinary send target.",
|
|
4702
4842
|
"Read-only native-thread listing targets an exact terminal_id. Native-thread new/resume mutations also use the listed expected_binding_token and never create a Turn.",
|
|
4703
4843
|
"Native inspection is a separate terminal action: use only its closed inspection enum and current exact terminal_id/token; AKK status does not execute a native slash command.",
|
|
4704
|
-
"A verified, idle human native-thread switch may expose a terminal-scoped send with expected_terminal_token; that action atomically adopts the live context before creating its Turn. A conclusively ended Codex rollout may expose the same snapshot-bound send only after AKK proves zero current rollout and an exact empty composer; it detaches the ended Session and creates an isolated virgin Session. Other binding conflicts remain fail-closed and may expose only exact low-level reconcile_binding recovery.",
|
|
4844
|
+
"A verified, idle human native-thread switch may expose a terminal-scoped send with expected_terminal_token; that action atomically adopts the live context before creating its Turn. A conclusively ended Codex rollout may expose the same snapshot-bound send only after AKK proves zero current rollout and an exact empty composer; it detaches the ended Session and creates an isolated virgin Session. A status-card-only Codex Session with no rollout or Turn may also expose this exact action: AKK submits the ordinary task first and binds only the rollout that durably accepts that request, so narrow panes do not require /status. Until that promotion commits, strict session_id send, respond, approve, cancel, native lifecycle, and native_inspect remain unavailable, and the provisional binding has no callback authority. If dispatch, acceptance, or post-submit binding is uncertain, do not retry automatically. Other binding conflicts remain fail-closed and may expose only exact low-level reconcile_binding recovery.",
|
|
4705
4845
|
"List resumable threads before resume; use only a complete native_thread_id and the action returned for that candidate.",
|
|
4706
4846
|
"Use a terminal selector only when explicitly named by the user or prefilled by that terminal row's send action. A handoff action also carries expected_terminal_token; never infer, guess, or reuse either value.",
|
|
4707
4847
|
"Use respond only for an in-flight turn that is explicitly waiting for OpenClaw.",
|
|
@@ -4775,7 +4915,8 @@ function listActionContracts() {
|
|
|
4775
4915
|
"agentHardTimeoutMinutes"
|
|
4776
4916
|
],
|
|
4777
4917
|
unsupported: ["timeoutSeconds"],
|
|
4778
|
-
|
|
4918
|
+
status_card_only_deferred_scope: "A zero-Turn Codex status-card binding has no rollout; only its listed selector/token send creates an isolated provisional Session and binds it after exact request acceptance. Until promotion commits, strict managed controls, native lifecycle, native_inspect, and callback authority remain unavailable; an uncertain dispatch, acceptance, or post-submit binding must not be retried automatically.",
|
|
4919
|
+
ordinary_use: "Create a new managed turn in the exact Session. A live terminal selector can attach an unmanaged pane, adopt one verified human-selected native context, detach a verified-empty Codex source, or replace a zero-Turn status-card-only binding after the submitted request proves its exact rollout; an explicit session_id never follows the pane."
|
|
4779
4920
|
},
|
|
4780
4921
|
new_thread: {
|
|
4781
4922
|
tool: "agent_knock_knock_new_thread",
|
|
@@ -5135,7 +5276,7 @@ async function sessionSelectorCandidates(commandName, options) {
|
|
|
5135
5276
|
managedSessions: selectorStore.readable
|
|
5136
5277
|
? listManagedSessions(storeDir).filter((session) => matchesConfiguredWorkspace(options.workspace, session.workspace))
|
|
5137
5278
|
: [],
|
|
5138
|
-
sessionAuthorityRequired: selectorStore.writer_protocol
|
|
5279
|
+
sessionAuthorityRequired: Number(selectorStore.writer_protocol) >= STORE_SESSION_AUTHORITY_PROTOCOL,
|
|
5139
5280
|
allConversations: discoverableWorkspaceConversations,
|
|
5140
5281
|
displayedConversations: discoverableWorkspaceConversations,
|
|
5141
5282
|
includeAll: false,
|
|
@@ -6094,7 +6235,7 @@ function materializeCurrentManagedSession({ options, terminal, identity }) {
|
|
|
6094
6235
|
}
|
|
6095
6236
|
return existingById;
|
|
6096
6237
|
}
|
|
6097
|
-
throw new Error(`Store protocol 3 has Turn records for Session ${sessionId} but its ` +
|
|
6238
|
+
throw new Error(`Store protocol 3+ has Turn records for Session ${sessionId} but its ` +
|
|
6098
6239
|
"authoritative Session state is missing");
|
|
6099
6240
|
}
|
|
6100
6241
|
async function reattachManagedSessionForNativeIdentity({ options, terminal, identity, storeDir }) {
|
|
@@ -6180,6 +6321,137 @@ function exactBoundCodexSourceForVerifiedEmptyHandoff({ session, terminalId, ter
|
|
|
6180
6321
|
liveProcessBirth: processBirth
|
|
6181
6322
|
}) === "same");
|
|
6182
6323
|
}
|
|
6324
|
+
function exactBoundCodexStatusCardSourceForDeferredSend({ session, terminalId, terminalControl, pid, workspace, processUuid, processBirth }) {
|
|
6325
|
+
const binding = session.binding;
|
|
6326
|
+
return Boolean(session.agent === "codex" &&
|
|
6327
|
+
session.status === "bound" &&
|
|
6328
|
+
binding &&
|
|
6329
|
+
isExactNativeThreadId(binding.native_thread_id) &&
|
|
6330
|
+
!binding.native_process.rollout &&
|
|
6331
|
+
isCodexStatusCardEvidence(binding.native_process.evidence) &&
|
|
6332
|
+
binding.native_process.process_uuid &&
|
|
6333
|
+
binding.native_process.process_birth &&
|
|
6334
|
+
binding.native_process.pid === pid &&
|
|
6335
|
+
terminalControlAliasMatches(binding.terminal_id, binding.terminal_control, terminalId, terminalControl) &&
|
|
6336
|
+
workspace &&
|
|
6337
|
+
path.resolve(session.workspace) === path.resolve(workspace) &&
|
|
6338
|
+
processIncarnationRelationship({
|
|
6339
|
+
binding,
|
|
6340
|
+
livePid: pid,
|
|
6341
|
+
liveProcessUuid: processUuid,
|
|
6342
|
+
liveProcessBirth: processBirth
|
|
6343
|
+
}) === "same");
|
|
6344
|
+
}
|
|
6345
|
+
function deferredCodexForegroundBindingToken({ terminalId, terminalControl, pid, workspace, processUuid, processBirth, sourceSession, dispatchSnapshot }) {
|
|
6346
|
+
const terminalToken = unmanagedTerminalBindingToken({
|
|
6347
|
+
terminalId,
|
|
6348
|
+
terminalControl,
|
|
6349
|
+
agent: "codex",
|
|
6350
|
+
pid,
|
|
6351
|
+
workspace,
|
|
6352
|
+
processUuid,
|
|
6353
|
+
processBirth
|
|
6354
|
+
});
|
|
6355
|
+
return createHash("sha256")
|
|
6356
|
+
.update(JSON.stringify({
|
|
6357
|
+
version: 2,
|
|
6358
|
+
kind: "deferred_codex_foreground_binding",
|
|
6359
|
+
terminal_token: terminalToken,
|
|
6360
|
+
source_session_id: sourceSession.session_id,
|
|
6361
|
+
source_revision: managedSessionRevision(sourceSession),
|
|
6362
|
+
source_binding_token: managedSessionBindingToken(sourceSession),
|
|
6363
|
+
terminal_dispatch_snapshot: dispatchSnapshot,
|
|
6364
|
+
observation: "verified_absent"
|
|
6365
|
+
}))
|
|
6366
|
+
.digest("hex");
|
|
6367
|
+
}
|
|
6368
|
+
function deferredCodexForegroundDispatchSnapshot(terminalControl) {
|
|
6369
|
+
const ledger = loadTerminalBridgeDispatchLedger(terminalControl);
|
|
6370
|
+
return deferredCodexForegroundDispatchSnapshotForLedger(terminalControl, ledger);
|
|
6371
|
+
}
|
|
6372
|
+
function deferredCodexForegroundDispatchSnapshotForLedger(terminalControl, ledger) {
|
|
6373
|
+
if (!ledger) {
|
|
6374
|
+
return {
|
|
6375
|
+
status: "none",
|
|
6376
|
+
fingerprint: terminalActionFingerprint({
|
|
6377
|
+
kind: "deferred_codex_foreground_terminal_dispatch",
|
|
6378
|
+
status: "none"
|
|
6379
|
+
})
|
|
6380
|
+
};
|
|
6381
|
+
}
|
|
6382
|
+
const currentAnchor = terminalEndpointFromControlRef(terminalControl).processAnchorPid;
|
|
6383
|
+
const ledgerAnchor = terminalDispatchRecordProcessAnchor(ledger);
|
|
6384
|
+
if (ledger.status !== "resolved" ||
|
|
6385
|
+
!Number.isSafeInteger(currentAnchor) ||
|
|
6386
|
+
Number(currentAnchor) < 1 ||
|
|
6387
|
+
ledgerAnchor !== currentAnchor ||
|
|
6388
|
+
!terminalDispatchRecordMatchesControl(ledger, terminalControl, {
|
|
6389
|
+
requireCurrentRoute: true,
|
|
6390
|
+
requireProcessAnchor: true
|
|
6391
|
+
})) {
|
|
6392
|
+
throw new Error(`terminal ${terminalControl.target} does not have exact resolved ` +
|
|
6393
|
+
"dispatch authority");
|
|
6394
|
+
}
|
|
6395
|
+
const resolvedAt = stringValue(ledger.resolved_at);
|
|
6396
|
+
if (!resolvedAt || !Number.isFinite(Date.parse(resolvedAt))) {
|
|
6397
|
+
throw new Error(`terminal ${terminalControl.target} resolved dispatch receipt has no ` +
|
|
6398
|
+
"valid resolved_at");
|
|
6399
|
+
}
|
|
6400
|
+
const statePath = stringValue(ledger.state_path);
|
|
6401
|
+
const ledgerStoreDir = stringValue(ledger.store_dir);
|
|
6402
|
+
if (ledgerStoreDir && !path.isAbsolute(ledgerStoreDir)) {
|
|
6403
|
+
throw new Error(`terminal ${terminalControl.target} resolved dispatch receipt has a ` +
|
|
6404
|
+
"nonabsolute Store owner");
|
|
6405
|
+
}
|
|
6406
|
+
if (statePath) {
|
|
6407
|
+
if (!ledgerStoreDir || !path.isAbsolute(statePath) ||
|
|
6408
|
+
!path.isAbsolute(ledgerStoreDir)) {
|
|
6409
|
+
throw new Error(`terminal ${terminalControl.target} resolved dispatch receipt has ` +
|
|
6410
|
+
"incomplete Store authority");
|
|
6411
|
+
}
|
|
6412
|
+
const canonicalStatePath = path.resolve(statePath);
|
|
6413
|
+
const canonicalStoreDir = path.resolve(ledgerStoreDir);
|
|
6414
|
+
const canonical = pathsForConversationDir(path.dirname(canonicalStatePath));
|
|
6415
|
+
if (path.resolve(canonical.statePath) !== canonicalStatePath ||
|
|
6416
|
+
path.resolve(canonical.storeDir) !== canonicalStoreDir) {
|
|
6417
|
+
throw new Error(`terminal ${terminalControl.target} resolved dispatch receipt has a ` +
|
|
6418
|
+
"noncanonical Store/state owner");
|
|
6419
|
+
}
|
|
6420
|
+
const conversationId = stringValue(ledger.conversation_id);
|
|
6421
|
+
if (conversationId &&
|
|
6422
|
+
path.resolve(pathsForConversation(conversationId, canonicalStoreDir).statePath) !== canonicalStatePath) {
|
|
6423
|
+
throw new Error(`terminal ${terminalControl.target} resolved dispatch receipt has a ` +
|
|
6424
|
+
"mismatched conversation owner");
|
|
6425
|
+
}
|
|
6426
|
+
}
|
|
6427
|
+
return {
|
|
6428
|
+
status: "resolved",
|
|
6429
|
+
fingerprint: terminalActionFingerprint({
|
|
6430
|
+
kind: "deferred_codex_foreground_terminal_dispatch",
|
|
6431
|
+
status: "resolved",
|
|
6432
|
+
ledger
|
|
6433
|
+
})
|
|
6434
|
+
};
|
|
6435
|
+
}
|
|
6436
|
+
function deferredCodexPreviousDispatchSnapshotMatches({ transfer, terminalControl, ledger }) {
|
|
6437
|
+
let snapshot;
|
|
6438
|
+
try {
|
|
6439
|
+
snapshot = deferredCodexForegroundDispatchSnapshotForLedger(terminalControl, ledger);
|
|
6440
|
+
}
|
|
6441
|
+
catch {
|
|
6442
|
+
return false;
|
|
6443
|
+
}
|
|
6444
|
+
return snapshot.status === transfer.previous_dispatch_status &&
|
|
6445
|
+
snapshot.fingerprint === transfer.previous_dispatch_fingerprint;
|
|
6446
|
+
}
|
|
6447
|
+
function tryDeferredCodexForegroundDispatchSnapshot(terminalControl) {
|
|
6448
|
+
try {
|
|
6449
|
+
return deferredCodexForegroundDispatchSnapshot(terminalControl);
|
|
6450
|
+
}
|
|
6451
|
+
catch {
|
|
6452
|
+
return undefined;
|
|
6453
|
+
}
|
|
6454
|
+
}
|
|
6183
6455
|
function verifiedEmptyCodexHandoffToken({ terminalId, terminalControl, pid, workspace, processUuid, processBirth, sourceSession }) {
|
|
6184
6456
|
const terminalToken = unmanagedTerminalBindingToken({
|
|
6185
6457
|
terminalId,
|
|
@@ -6372,85 +6644,944 @@ async function assertVerifiedEmptyCodexTransportBoundary({ options, boundary, re
|
|
|
6372
6644
|
requireEmptyComposer
|
|
6373
6645
|
});
|
|
6374
6646
|
}
|
|
6375
|
-
function
|
|
6376
|
-
const
|
|
6377
|
-
const
|
|
6378
|
-
|
|
6379
|
-
|
|
6380
|
-
|
|
6381
|
-
|
|
6382
|
-
|
|
6383
|
-
|
|
6384
|
-
|
|
6385
|
-
|
|
6386
|
-
|
|
6647
|
+
async function assertDeferredCodexForegroundBindingBoundary({ options, boundary, expectedSourceStatus, requireNoDispatch, requireEmptyComposer }) {
|
|
6648
|
+
const storeDir = storeDirFromOptions(options);
|
|
6649
|
+
const source = loadManagedSession(storeDir, boundary.sourceSessionId);
|
|
6650
|
+
const expectedRevision = expectedSourceStatus === "bound"
|
|
6651
|
+
? boundary.sourceBoundRevision
|
|
6652
|
+
: boundary.sourceReservedRevision;
|
|
6653
|
+
const expectedBindingToken = expectedSourceStatus === "bound"
|
|
6654
|
+
? boundary.sourceBoundBindingToken
|
|
6655
|
+
: boundary.sourceReservedBindingToken;
|
|
6656
|
+
if (source.status !== expectedSourceStatus ||
|
|
6657
|
+
expectedRevision === undefined ||
|
|
6658
|
+
managedSessionRevision(source) !== expectedRevision ||
|
|
6659
|
+
expectedBindingToken === undefined ||
|
|
6660
|
+
managedSessionBindingToken(source) !== expectedBindingToken) {
|
|
6661
|
+
throw new Error("the deferred Codex foreground source changed; refresh AKK list");
|
|
6662
|
+
}
|
|
6663
|
+
if (!exactBoundCodexStatusCardSourceForDeferredSend({
|
|
6664
|
+
session: { ...source, status: "bound" },
|
|
6665
|
+
terminalId: boundary.terminal.conversationId,
|
|
6666
|
+
terminalControl: boundary.terminal.terminalControl,
|
|
6667
|
+
pid: boundary.terminal.pid,
|
|
6668
|
+
workspace: boundary.terminal.terminalControl.currentPath,
|
|
6669
|
+
processUuid: boundary.processUuid,
|
|
6670
|
+
processBirth: boundary.processBirth
|
|
6671
|
+
}) ||
|
|
6672
|
+
source.last_transition_id !== (expectedSourceStatus === "bound" ? undefined : boundary.transferId) ||
|
|
6673
|
+
managedSessionHasAnyNativeTransition(storeDir, source) ||
|
|
6674
|
+
managedTurnsForSession(storeDir, source.session_id).length !== 0) {
|
|
6675
|
+
throw new Error("the deferred Codex foreground source is no longer an isolated status-card binding");
|
|
6676
|
+
}
|
|
6677
|
+
const liveIncarnation = codexProcessIncarnationForPid(boundary.terminal.pid);
|
|
6678
|
+
if (liveIncarnation.processUuid !== boundary.processUuid ||
|
|
6679
|
+
liveIncarnation.processBirth !== boundary.processBirth) {
|
|
6680
|
+
throw new Error("the deferred Codex foreground process incarnation changed");
|
|
6681
|
+
}
|
|
6682
|
+
const observation = await observeCurrentNativeAgentSessionIdentity({
|
|
6683
|
+
options,
|
|
6684
|
+
agent: "codex",
|
|
6685
|
+
pid: boundary.terminal.pid,
|
|
6686
|
+
cwd: boundary.terminal.terminalControl.currentPath
|
|
6387
6687
|
});
|
|
6388
|
-
|
|
6389
|
-
|
|
6390
|
-
|
|
6391
|
-
|
|
6392
|
-
|
|
6393
|
-
|
|
6688
|
+
if (observation.status === "unavailable") {
|
|
6689
|
+
throw new Error(`Codex native identity observation is unavailable: ${observation.reason}`);
|
|
6690
|
+
}
|
|
6691
|
+
if (observation.status !== "verified_absent") {
|
|
6692
|
+
throw new Error(`Codex materialized native thread ${observation.identity.sessionId}; ` +
|
|
6693
|
+
"refresh AKK list before sending");
|
|
6694
|
+
}
|
|
6695
|
+
if (requireNoDispatch) {
|
|
6696
|
+
assertTerminalIncarnationCanStartTurn(storeDir, boundary.terminal.terminalControl);
|
|
6697
|
+
const currentDispatchSnapshot = deferredCodexForegroundDispatchSnapshot(boundary.terminal.terminalControl);
|
|
6698
|
+
if (currentDispatchSnapshot.status !== boundary.previousDispatchSnapshot.status ||
|
|
6699
|
+
currentDispatchSnapshot.fingerprint !==
|
|
6700
|
+
boundary.previousDispatchSnapshot.fingerprint) {
|
|
6701
|
+
throw new Error("the deferred Codex foreground dispatch history changed; refresh AKK list");
|
|
6702
|
+
}
|
|
6703
|
+
}
|
|
6704
|
+
const status = await createTerminalAgentBridge(options).status("codex", boundary.terminal.terminalControl, {
|
|
6705
|
+
runtime: terminalRuntimeForLiveIdentity({
|
|
6706
|
+
terminal: boundary.terminal,
|
|
6707
|
+
expectedEmptyNativeSession: true
|
|
6708
|
+
})
|
|
6394
6709
|
});
|
|
6395
|
-
|
|
6396
|
-
|
|
6397
|
-
|
|
6398
|
-
|
|
6399
|
-
|
|
6400
|
-
|
|
6401
|
-
return createHash("sha256")
|
|
6402
|
-
.update(JSON.stringify({
|
|
6403
|
-
version: 1,
|
|
6404
|
-
kind: "active_turn_human_handoff",
|
|
6405
|
-
handoff_token: handoffToken,
|
|
6406
|
-
session_id: sessionIdForConversation(turn),
|
|
6407
|
-
turn_id: turnIdForConversation(turn),
|
|
6408
|
-
turn_status: turn.status,
|
|
6409
|
-
turn_updated_at: turn.updated_at ?? null,
|
|
6410
|
-
current_message_id: stringValue(takeover?.terminal_bridge_message_id) ??
|
|
6411
|
-
stringValue(submission?.message_id) ??
|
|
6412
|
-
null,
|
|
6413
|
-
ledger_generation_id: stringValue(ledger?.generation_id) ?? null,
|
|
6414
|
-
ledger_message_id: stringValue(ledger?.message_id) ?? null,
|
|
6415
|
-
ledger_status: stringValue(ledger?.status) ?? null
|
|
6416
|
-
}))
|
|
6417
|
-
.digest("hex");
|
|
6418
|
-
}
|
|
6419
|
-
const originalExpectedTerminalSelector = new WeakMap();
|
|
6420
|
-
function assertExpectedHandoffTokenUsesExactTerminalSelector({ options, terminal }) {
|
|
6421
|
-
if (!stringValue(options.expectedTerminalToken)) {
|
|
6422
|
-
return;
|
|
6710
|
+
if (status.reachable !== true ||
|
|
6711
|
+
status.approval_state.blocked === true ||
|
|
6712
|
+
!["idle", "unknown"].includes(status.activity_state)) {
|
|
6713
|
+
throw new Error(`terminal ${boundary.terminal.terminalControl.target} is not at a ` +
|
|
6714
|
+
`verified empty Codex prompt (${status.activity_state}: ` +
|
|
6715
|
+
`${status.activity_reason})`);
|
|
6423
6716
|
}
|
|
6424
|
-
|
|
6425
|
-
|
|
6426
|
-
|
|
6427
|
-
|
|
6428
|
-
|
|
6429
|
-
"conversation selector advertised by AKK list");
|
|
6717
|
+
if (requireEmptyComposer) {
|
|
6718
|
+
await assertCodexComposerReadyForAutomatedInput({
|
|
6719
|
+
options,
|
|
6720
|
+
terminalControl: boundary.terminal.terminalControl
|
|
6721
|
+
});
|
|
6430
6722
|
}
|
|
6723
|
+
return source;
|
|
6431
6724
|
}
|
|
6432
|
-
async function
|
|
6433
|
-
const
|
|
6434
|
-
|
|
6435
|
-
|
|
6436
|
-
|
|
6725
|
+
async function prepareDeferredCodexForegroundBinding({ options, terminal, sourceSession, observation, requestText }) {
|
|
6726
|
+
const expectedToken = stringValue(options.expectedTerminalToken);
|
|
6727
|
+
if (terminal.agent !== "codex" ||
|
|
6728
|
+
!sourceSession?.binding ||
|
|
6729
|
+
observation.status !== "verified_absent" ||
|
|
6730
|
+
!expectedToken) {
|
|
6731
|
+
return undefined;
|
|
6437
6732
|
}
|
|
6438
|
-
|
|
6439
|
-
|
|
6733
|
+
const processUuid = sourceSession.binding.native_process.process_uuid;
|
|
6734
|
+
const processBirth = sourceSession.binding.native_process.process_birth;
|
|
6735
|
+
const workspace = terminal.terminalControl.currentPath;
|
|
6736
|
+
const liveIncarnation = codexProcessIncarnationForPid(terminal.pid);
|
|
6737
|
+
if (!processUuid ||
|
|
6738
|
+
!processBirth ||
|
|
6739
|
+
!workspace ||
|
|
6740
|
+
!hasCanonicalTerminalEndpoint(terminal.terminalControl) ||
|
|
6741
|
+
!exactBoundCodexStatusCardSourceForDeferredSend({
|
|
6742
|
+
session: sourceSession,
|
|
6743
|
+
terminalId: terminal.conversationId,
|
|
6744
|
+
terminalControl: terminal.terminalControl,
|
|
6745
|
+
pid: terminal.pid,
|
|
6746
|
+
workspace,
|
|
6747
|
+
processUuid: liveIncarnation.processUuid,
|
|
6748
|
+
processBirth: liveIncarnation.processBirth
|
|
6749
|
+
})) {
|
|
6750
|
+
return undefined;
|
|
6440
6751
|
}
|
|
6441
|
-
const
|
|
6442
|
-
const
|
|
6443
|
-
|
|
6444
|
-
|
|
6445
|
-
|
|
6752
|
+
const dispatchSnapshot = deferredCodexForegroundDispatchSnapshot(terminal.terminalControl);
|
|
6753
|
+
const token = deferredCodexForegroundBindingToken({
|
|
6754
|
+
terminalId: terminal.conversationId,
|
|
6755
|
+
terminalControl: terminal.terminalControl,
|
|
6756
|
+
pid: terminal.pid,
|
|
6757
|
+
workspace,
|
|
6758
|
+
processUuid,
|
|
6759
|
+
processBirth,
|
|
6760
|
+
sourceSession,
|
|
6761
|
+
dispatchSnapshot
|
|
6446
6762
|
});
|
|
6447
|
-
|
|
6448
|
-
|
|
6449
|
-
|
|
6450
|
-
|
|
6451
|
-
const
|
|
6452
|
-
|
|
6453
|
-
|
|
6763
|
+
if (expectedToken !== token) {
|
|
6764
|
+
throw new Error("deferred Codex foreground binding requires the fresh exact terminal " +
|
|
6765
|
+
"token advertised by AKK list");
|
|
6766
|
+
}
|
|
6767
|
+
const targetSessionId = createManagedSessionId();
|
|
6768
|
+
const transferId = createDeferredForegroundTransferId();
|
|
6769
|
+
const boundary = {
|
|
6770
|
+
terminal,
|
|
6771
|
+
transferId,
|
|
6772
|
+
targetSessionId,
|
|
6773
|
+
sourceSessionId: sourceSession.session_id,
|
|
6774
|
+
sourceBoundRevision: managedSessionRevision(sourceSession),
|
|
6775
|
+
sourceBoundBindingToken: managedSessionBindingToken(sourceSession),
|
|
6776
|
+
processUuid,
|
|
6777
|
+
processBirth,
|
|
6778
|
+
previousDispatchSnapshot: dispatchSnapshot
|
|
6779
|
+
};
|
|
6780
|
+
await assertDeferredCodexForegroundBindingBoundary({
|
|
6781
|
+
options,
|
|
6782
|
+
boundary,
|
|
6783
|
+
expectedSourceStatus: "bound",
|
|
6784
|
+
requireNoDispatch: true,
|
|
6785
|
+
requireEmptyComposer: true
|
|
6786
|
+
});
|
|
6787
|
+
await assertNativeThreadHasExclusiveOwnership({
|
|
6788
|
+
options,
|
|
6789
|
+
agent: "codex",
|
|
6790
|
+
currentPid: terminal.pid,
|
|
6791
|
+
nativeThreadId: sourceSession.binding.native_thread_id,
|
|
6792
|
+
storeDir: storeDirFromOptions(options),
|
|
6793
|
+
terminalControl: terminal.terminalControl,
|
|
6794
|
+
excludedManagedSessionId: sourceSession.session_id
|
|
6795
|
+
});
|
|
6796
|
+
const storeDir = storeDirFromOptions(options);
|
|
6797
|
+
const existingTransfer = listDeferredForegroundTransfers(storeDir).find((candidate) => !["resolved", "aborted", "abort_resolved"].includes(candidate.status) &&
|
|
6798
|
+
(candidate.source_session_id === sourceSession.session_id ||
|
|
6799
|
+
(candidate.terminal_id === terminal.conversationId &&
|
|
6800
|
+
terminalControlEvidenceMatches(candidate.terminal_endpoint, terminal.terminalControl))));
|
|
6801
|
+
if (existingTransfer) {
|
|
6802
|
+
throw new Error(`deferred foreground transfer ${existingTransfer.transfer_id} is ` +
|
|
6803
|
+
`still ${existingTransfer.status}; refresh AKK list before sending`);
|
|
6804
|
+
}
|
|
6805
|
+
saveDeferredForegroundTransfer(storeDir, {
|
|
6806
|
+
schema: "agent-knock-knock/deferred-foreground-transfer",
|
|
6807
|
+
version: 1,
|
|
6808
|
+
transfer_id: transferId,
|
|
6809
|
+
status: "prepared",
|
|
6810
|
+
input_stage: "none",
|
|
6811
|
+
terminal_id: terminal.conversationId,
|
|
6812
|
+
terminal_endpoint: terminalControlEvidence(terminal.terminalControl),
|
|
6813
|
+
process_pid: terminal.pid,
|
|
6814
|
+
process_uuid: processUuid,
|
|
6815
|
+
process_birth: processBirth,
|
|
6816
|
+
workspace,
|
|
6817
|
+
source_session_id: sourceSession.session_id,
|
|
6818
|
+
source_expected_revision: managedSessionRevision(sourceSession),
|
|
6819
|
+
source_binding_token: managedSessionBindingToken(sourceSession),
|
|
6820
|
+
source_previous_last_transition_id: sourceSession.last_transition_id,
|
|
6821
|
+
source_before_binding: sourceSession.binding,
|
|
6822
|
+
target_session_id: targetSessionId,
|
|
6823
|
+
target_expected_revision: null,
|
|
6824
|
+
previous_dispatch_status: dispatchSnapshot.status,
|
|
6825
|
+
previous_dispatch_fingerprint: dispatchSnapshot.fingerprint,
|
|
6826
|
+
request_hash: required(terminalBridgeRequestFingerprint(terminalSubmissionPayload(requestText)), "deferred foreground request hash is unavailable"),
|
|
6827
|
+
dispatcher_pid: cliPid(),
|
|
6828
|
+
prepared_at: cliNow().toISOString()
|
|
6829
|
+
}, { expectedRevision: null });
|
|
6830
|
+
return boundary;
|
|
6831
|
+
}
|
|
6832
|
+
function deferredForegroundTransferRevision(transfer) {
|
|
6833
|
+
const revision = Number(transfer.revision);
|
|
6834
|
+
if (!Number.isSafeInteger(revision) || revision < 1) {
|
|
6835
|
+
throw new Error(`deferred foreground transfer ${transfer.transfer_id} has no valid revision`);
|
|
6836
|
+
}
|
|
6837
|
+
return revision;
|
|
6838
|
+
}
|
|
6839
|
+
function assertDeferredForegroundTransferMatchesBoundary({ transfer, boundary }) {
|
|
6840
|
+
if (transfer.transfer_id !== boundary.transferId ||
|
|
6841
|
+
transfer.source_session_id !== boundary.sourceSessionId ||
|
|
6842
|
+
transfer.target_session_id !== boundary.targetSessionId ||
|
|
6843
|
+
transfer.source_expected_revision !== boundary.sourceBoundRevision ||
|
|
6844
|
+
transfer.source_binding_token !== boundary.sourceBoundBindingToken ||
|
|
6845
|
+
transfer.previous_dispatch_status !==
|
|
6846
|
+
boundary.previousDispatchSnapshot.status ||
|
|
6847
|
+
transfer.previous_dispatch_fingerprint !==
|
|
6848
|
+
boundary.previousDispatchSnapshot.fingerprint ||
|
|
6849
|
+
transfer.process_pid !== boundary.terminal.pid ||
|
|
6850
|
+
transfer.process_uuid !== boundary.processUuid ||
|
|
6851
|
+
transfer.process_birth !== boundary.processBirth ||
|
|
6852
|
+
transfer.terminal_id !== boundary.terminal.conversationId ||
|
|
6853
|
+
!terminalControlEvidenceMatches(transfer.terminal_endpoint, boundary.terminal.terminalControl)) {
|
|
6854
|
+
throw new Error(`deferred foreground transfer ${boundary.transferId} authority changed`);
|
|
6855
|
+
}
|
|
6856
|
+
}
|
|
6857
|
+
function abortDeferredCodexForegroundTransferBeforeInput({ options, boundary, reason, terminalInputNotStartedAt }) {
|
|
6858
|
+
const storeDir = storeDirFromOptions(options);
|
|
6859
|
+
let transfer = loadDeferredForegroundTransfer(storeDir, boundary.transferId);
|
|
6860
|
+
assertDeferredForegroundTransferMatchesBoundary({ transfer, boundary });
|
|
6861
|
+
if (transfer.status === "abort_resolved") {
|
|
6862
|
+
return;
|
|
6863
|
+
}
|
|
6864
|
+
const provedDispatchDidNotStart = transfer.status === "dispatch_started" &&
|
|
6865
|
+
transfer.input_stage === "dispatch_started" &&
|
|
6866
|
+
terminalInputNotStartedAt !== undefined &&
|
|
6867
|
+
Number.isFinite(Date.parse(terminalInputNotStartedAt));
|
|
6868
|
+
if (transfer.input_stage !== "none" && !provedDispatchDidNotStart) {
|
|
6869
|
+
throw new Error(`deferred foreground transfer ${transfer.transfer_id} may have started ` +
|
|
6870
|
+
"terminal input and cannot restore its source");
|
|
6871
|
+
}
|
|
6872
|
+
if (![
|
|
6873
|
+
"prepared",
|
|
6874
|
+
"source_reserved",
|
|
6875
|
+
"target_prepared",
|
|
6876
|
+
"dispatch_started",
|
|
6877
|
+
"aborted"
|
|
6878
|
+
].includes(transfer.status)) {
|
|
6879
|
+
throw new Error(`deferred foreground transfer ${transfer.transfer_id} cannot abort from ` +
|
|
6880
|
+
transfer.status);
|
|
6881
|
+
}
|
|
6882
|
+
// Publish the terminal abort intent before restoring either Session. A crash
|
|
6883
|
+
// after this receipt is completed by the Store-only finalizer below; a later
|
|
6884
|
+
// successful retry may evolve the Sessions only after `abort_resolved` makes
|
|
6885
|
+
// that cleanup receipt permanently self-contained.
|
|
6886
|
+
if (transfer.status !== "aborted") {
|
|
6887
|
+
transfer = saveDeferredForegroundTransfer(storeDir, {
|
|
6888
|
+
...transfer,
|
|
6889
|
+
status: "aborted",
|
|
6890
|
+
aborted_at: cliNow().toISOString(),
|
|
6891
|
+
...(provedDispatchDidNotStart
|
|
6892
|
+
? { terminal_input_not_started_at: terminalInputNotStartedAt }
|
|
6893
|
+
: {}),
|
|
6894
|
+
error: JSON.stringify(textSummary(reason))
|
|
6895
|
+
}, { expectedRevision: deferredForegroundTransferRevision(transfer) });
|
|
6896
|
+
}
|
|
6897
|
+
transfer = finalizeAbortedDeferredCodexForegroundTransferCleanup({
|
|
6898
|
+
storeDir,
|
|
6899
|
+
transfer
|
|
6900
|
+
});
|
|
6901
|
+
runtimeLog("info", "deferred_codex_foreground_transfer_aborted", {
|
|
6902
|
+
transfer_id: transfer.transfer_id,
|
|
6903
|
+
source_session_id: transfer.source_session_id,
|
|
6904
|
+
target_session_id: transfer.target_session_id,
|
|
6905
|
+
terminal_input_sent: false,
|
|
6906
|
+
reason: textSummary(reason)
|
|
6907
|
+
});
|
|
6908
|
+
}
|
|
6909
|
+
function finalizeAbortedDeferredCodexForegroundTransferCleanup({ storeDir, transfer }) {
|
|
6910
|
+
let current = loadDeferredForegroundTransfer(storeDir, transfer.transfer_id);
|
|
6911
|
+
if (current.status === "abort_resolved") {
|
|
6912
|
+
return current;
|
|
6913
|
+
}
|
|
6914
|
+
const provedDispatchDidNotStart = current.input_stage === "dispatch_started" &&
|
|
6915
|
+
current.terminal_input_not_started_at !== undefined &&
|
|
6916
|
+
Number.isFinite(Date.parse(current.terminal_input_not_started_at));
|
|
6917
|
+
if (current.status !== "aborted" ||
|
|
6918
|
+
(current.input_stage !== "none" && !provedDispatchDidNotStart)) {
|
|
6919
|
+
throw new Error(`deferred foreground transfer ${current.transfer_id} does not carry a ` +
|
|
6920
|
+
"zero-input abort intent");
|
|
6921
|
+
}
|
|
6922
|
+
let target = tryLoadManagedSession(storeDir, current.target_session_id);
|
|
6923
|
+
if (target) {
|
|
6924
|
+
if (target.lineage.transition_id !== current.transfer_id ||
|
|
6925
|
+
target.lineage.previous_session_id !== current.source_session_id ||
|
|
6926
|
+
target.binding?.native_thread_id ||
|
|
6927
|
+
target.binding?.native_process.rollout ||
|
|
6928
|
+
target.last_transition_id !== current.transfer_id ||
|
|
6929
|
+
!current.target_before_binding ||
|
|
6930
|
+
JSON.stringify(target.binding) !==
|
|
6931
|
+
JSON.stringify(current.target_before_binding)) {
|
|
6932
|
+
throw new Error(`deferred foreground target ${target.session_id} changed before abort cleanup`);
|
|
6933
|
+
}
|
|
6934
|
+
if (target.status === "transitioning") {
|
|
6935
|
+
if (managedSessionRevision(target) !== current.target_prepared_revision ||
|
|
6936
|
+
managedSessionBindingToken(target) !==
|
|
6937
|
+
current.target_prepared_binding_token) {
|
|
6938
|
+
throw new Error(`deferred foreground target ${target.session_id} changed before abort cleanup`);
|
|
6939
|
+
}
|
|
6940
|
+
const detachedAt = cliNow().toISOString();
|
|
6941
|
+
target = saveManagedSession(storeDir, {
|
|
6942
|
+
...target,
|
|
6943
|
+
status: "detached",
|
|
6944
|
+
detached_at: detachedAt,
|
|
6945
|
+
updated_at: detachedAt
|
|
6946
|
+
}, { expectedRevision: managedSessionRevision(target) });
|
|
6947
|
+
}
|
|
6948
|
+
else if (target.status !== "detached" ||
|
|
6949
|
+
managedSessionRevision(target) !==
|
|
6950
|
+
Number(current.target_prepared_revision) + 1 ||
|
|
6951
|
+
managedSessionBindingToken(target) !== managedSessionBindingToken({
|
|
6952
|
+
session_id: current.target_session_id,
|
|
6953
|
+
status: "detached",
|
|
6954
|
+
binding: current.target_before_binding
|
|
6955
|
+
})) {
|
|
6956
|
+
throw new Error(`deferred foreground target ${target.session_id} cannot finish abort ` +
|
|
6957
|
+
`cleanup from ${target.status}`);
|
|
6958
|
+
}
|
|
6959
|
+
}
|
|
6960
|
+
let source = loadManagedSession(storeDir, current.source_session_id);
|
|
6961
|
+
if (source.status === "transitioning" &&
|
|
6962
|
+
source.last_transition_id === current.transfer_id &&
|
|
6963
|
+
managedSessionRevision(source) === current.source_expected_revision + 1 &&
|
|
6964
|
+
managedSessionBindingToken(source) === managedSessionBindingToken({
|
|
6965
|
+
session_id: current.source_session_id,
|
|
6966
|
+
status: "transitioning",
|
|
6967
|
+
binding: current.source_before_binding
|
|
6968
|
+
}) &&
|
|
6969
|
+
JSON.stringify(source.binding) ===
|
|
6970
|
+
JSON.stringify(current.source_before_binding)) {
|
|
6971
|
+
const restoredAt = cliNow().toISOString();
|
|
6972
|
+
source = saveManagedSession(storeDir, {
|
|
6973
|
+
...source,
|
|
6974
|
+
status: "bound",
|
|
6975
|
+
last_transition_id: current.source_previous_last_transition_id,
|
|
6976
|
+
updated_at: restoredAt
|
|
6977
|
+
}, { expectedRevision: managedSessionRevision(source) });
|
|
6978
|
+
}
|
|
6979
|
+
const allowedSourceRevisions = current.source_reserved_at
|
|
6980
|
+
? [current.source_expected_revision + 2]
|
|
6981
|
+
: [
|
|
6982
|
+
current.source_expected_revision,
|
|
6983
|
+
// The source Session CAS precedes publication of source_reserved_at.
|
|
6984
|
+
// This exact +2 state is the idempotent restore after that crash gap.
|
|
6985
|
+
current.source_expected_revision + 2
|
|
6986
|
+
];
|
|
6987
|
+
if (source.status !== "bound" ||
|
|
6988
|
+
source.last_transition_id !== current.source_previous_last_transition_id ||
|
|
6989
|
+
!allowedSourceRevisions.includes(managedSessionRevision(source)) ||
|
|
6990
|
+
managedSessionBindingToken(source) !== current.source_binding_token ||
|
|
6991
|
+
JSON.stringify(source.binding) !== JSON.stringify(current.source_before_binding)) {
|
|
6992
|
+
throw new Error(`deferred foreground source ${source.session_id} changed before abort cleanup`);
|
|
6993
|
+
}
|
|
6994
|
+
const completedAt = cliNow().toISOString();
|
|
6995
|
+
current = saveDeferredForegroundTransfer(storeDir, {
|
|
6996
|
+
...current,
|
|
6997
|
+
status: "abort_resolved",
|
|
6998
|
+
abort_cleanup_completed_at: completedAt,
|
|
6999
|
+
abort_source_after_revision: managedSessionRevision(source),
|
|
7000
|
+
abort_source_after_status: "bound",
|
|
7001
|
+
abort_source_after_binding_token: managedSessionBindingToken(source),
|
|
7002
|
+
abort_source_after_binding: source.binding,
|
|
7003
|
+
abort_target_after_status: target ? "detached" : "absent",
|
|
7004
|
+
...(target
|
|
7005
|
+
? {
|
|
7006
|
+
abort_target_after_revision: managedSessionRevision(target),
|
|
7007
|
+
abort_target_after_binding_token: managedSessionBindingToken(target),
|
|
7008
|
+
abort_target_after_binding: target.binding
|
|
7009
|
+
}
|
|
7010
|
+
: {})
|
|
7011
|
+
}, { expectedRevision: deferredForegroundTransferRevision(current) });
|
|
7012
|
+
return current;
|
|
7013
|
+
}
|
|
7014
|
+
async function reserveDeferredCodexForegroundTransfer({ options, boundary, targetSession, messageId, turnId, statePath }) {
|
|
7015
|
+
const storeDir = storeDirFromOptions(options);
|
|
7016
|
+
let transfer = loadDeferredForegroundTransfer(storeDir, boundary.transferId);
|
|
7017
|
+
assertDeferredForegroundTransferMatchesBoundary({ transfer, boundary });
|
|
7018
|
+
if (transfer.status !== "prepared" || transfer.input_stage !== "none") {
|
|
7019
|
+
throw new Error(`deferred foreground transfer ${transfer.transfer_id} is already ` +
|
|
7020
|
+
transfer.status);
|
|
7021
|
+
}
|
|
7022
|
+
const source = await assertDeferredCodexForegroundBindingBoundary({
|
|
7023
|
+
options,
|
|
7024
|
+
boundary,
|
|
7025
|
+
expectedSourceStatus: "bound",
|
|
7026
|
+
requireNoDispatch: true,
|
|
7027
|
+
requireEmptyComposer: true
|
|
7028
|
+
});
|
|
7029
|
+
try {
|
|
7030
|
+
const reservedAt = cliNow().toISOString();
|
|
7031
|
+
const reserved = saveManagedSession(storeDir, {
|
|
7032
|
+
...source,
|
|
7033
|
+
status: "transitioning",
|
|
7034
|
+
last_transition_id: transfer.transfer_id,
|
|
7035
|
+
updated_at: reservedAt
|
|
7036
|
+
}, { expectedRevision: managedSessionRevision(source) });
|
|
7037
|
+
if (cliEnv().AKK_TEST_EXIT_AFTER_DEFERRED_SOURCE_SESSION_RESERVED === "1") {
|
|
7038
|
+
// Crash seam for the only source-reservation window not yet reflected by
|
|
7039
|
+
// source_reserved_at in the transfer receipt. Recovery must prove and
|
|
7040
|
+
// restore the exact transitioning revision without terminal input.
|
|
7041
|
+
cliExit(86);
|
|
7042
|
+
}
|
|
7043
|
+
boundary.sourceReservedRevision = managedSessionRevision(reserved);
|
|
7044
|
+
boundary.sourceReservedBindingToken = managedSessionBindingToken(reserved);
|
|
7045
|
+
transfer = saveDeferredForegroundTransfer(storeDir, {
|
|
7046
|
+
...transfer,
|
|
7047
|
+
status: "source_reserved",
|
|
7048
|
+
source_reserved_at: reservedAt
|
|
7049
|
+
}, { expectedRevision: deferredForegroundTransferRevision(transfer) });
|
|
7050
|
+
if (cliEnv().AKK_TEST_EXIT_AFTER_DEFERRED_SOURCE_RESERVED === "1") {
|
|
7051
|
+
cliExit(86);
|
|
7052
|
+
}
|
|
7053
|
+
const provisionalTarget = {
|
|
7054
|
+
...targetSession,
|
|
7055
|
+
status: "transitioning",
|
|
7056
|
+
last_transition_id: transfer.transfer_id
|
|
7057
|
+
};
|
|
7058
|
+
boundary.targetPreparedRevision = 1;
|
|
7059
|
+
boundary.targetPreparedBindingToken = managedSessionBindingToken(provisionalTarget);
|
|
7060
|
+
// Publish the complete target CAS authority before the target itself. A
|
|
7061
|
+
// crash can therefore observe either no target or exactly this revision-1
|
|
7062
|
+
// provisional target; it never leaves an unrecorded Session behind.
|
|
7063
|
+
transfer = saveDeferredForegroundTransfer(storeDir, {
|
|
7064
|
+
...transfer,
|
|
7065
|
+
status: "target_prepared",
|
|
7066
|
+
target_prepared_at: cliNow().toISOString(),
|
|
7067
|
+
target_prepared_revision: 1,
|
|
7068
|
+
target_prepared_status: "transitioning",
|
|
7069
|
+
target_prepared_last_transition_id: transfer.transfer_id,
|
|
7070
|
+
target_prepared_binding_token: managedSessionBindingToken(provisionalTarget),
|
|
7071
|
+
target_before_binding: provisionalTarget.binding,
|
|
7072
|
+
message_id: messageId,
|
|
7073
|
+
turn_id: turnId,
|
|
7074
|
+
state_path: path.resolve(statePath)
|
|
7075
|
+
}, { expectedRevision: deferredForegroundTransferRevision(transfer) });
|
|
7076
|
+
if (cliEnv().AKK_TEST_EXIT_AFTER_DEFERRED_TARGET_PREPARED === "1") {
|
|
7077
|
+
cliExit(86);
|
|
7078
|
+
}
|
|
7079
|
+
const createdSession = saveManagedSession(storeDir, provisionalTarget, {
|
|
7080
|
+
expectedRevision: null
|
|
7081
|
+
});
|
|
7082
|
+
if (managedSessionRevision(createdSession) !== 1 ||
|
|
7083
|
+
managedSessionBindingToken(createdSession) !==
|
|
7084
|
+
boundary.targetPreparedBindingToken) {
|
|
7085
|
+
throw new Error(`deferred foreground target ${createdSession.session_id} did not ` +
|
|
7086
|
+
"materialize at its prepared CAS authority");
|
|
7087
|
+
}
|
|
7088
|
+
runtimeLog("info", "deferred_codex_foreground_source_reserved", {
|
|
7089
|
+
transfer_id: transfer.transfer_id,
|
|
7090
|
+
terminal_id: boundary.terminal.conversationId,
|
|
7091
|
+
source_session_id: source.session_id,
|
|
7092
|
+
native_thread_id: source.binding?.native_thread_id,
|
|
7093
|
+
process_uuid: boundary.processUuid,
|
|
7094
|
+
process_birth: boundary.processBirth,
|
|
7095
|
+
terminal_input_sent: false
|
|
7096
|
+
});
|
|
7097
|
+
return {
|
|
7098
|
+
createdSession,
|
|
7099
|
+
rollback: () => abortDeferredCodexForegroundTransferBeforeInput({
|
|
7100
|
+
options,
|
|
7101
|
+
boundary,
|
|
7102
|
+
reason: "terminal input was proved not to have started"
|
|
7103
|
+
})
|
|
7104
|
+
};
|
|
7105
|
+
}
|
|
7106
|
+
catch (error) {
|
|
7107
|
+
try {
|
|
7108
|
+
abortDeferredCodexForegroundTransferBeforeInput({
|
|
7109
|
+
options,
|
|
7110
|
+
boundary,
|
|
7111
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
7112
|
+
});
|
|
7113
|
+
}
|
|
7114
|
+
catch (abortError) {
|
|
7115
|
+
throw new Error(`deferred foreground reservation failed and could not be aborted: ${abortError instanceof Error ? abortError.message : String(abortError)}`, { cause: error });
|
|
7116
|
+
}
|
|
7117
|
+
throw error;
|
|
7118
|
+
}
|
|
7119
|
+
}
|
|
7120
|
+
function assertDeferredForegroundTargetPreparedAuthority({ storeDir, transfer }) {
|
|
7121
|
+
const target = loadManagedSession(storeDir, transfer.target_session_id);
|
|
7122
|
+
if (target.status !== "transitioning" ||
|
|
7123
|
+
target.last_transition_id !== transfer.transfer_id ||
|
|
7124
|
+
managedSessionRevision(target) !== transfer.target_prepared_revision ||
|
|
7125
|
+
managedSessionBindingToken(target) !==
|
|
7126
|
+
transfer.target_prepared_binding_token ||
|
|
7127
|
+
JSON.stringify(target.binding) !==
|
|
7128
|
+
JSON.stringify(transfer.target_before_binding) ||
|
|
7129
|
+
target.lineage.transition_id !== transfer.transfer_id ||
|
|
7130
|
+
target.lineage.previous_session_id !== transfer.source_session_id) {
|
|
7131
|
+
throw new Error(`deferred foreground target ${transfer.target_session_id} changed ` +
|
|
7132
|
+
"before terminal dispatch");
|
|
7133
|
+
}
|
|
7134
|
+
return target;
|
|
7135
|
+
}
|
|
7136
|
+
function advanceDeferredCodexForegroundTransferInputStage({ options, boundary, stage, at }) {
|
|
7137
|
+
const storeDir = storeDirFromOptions(options);
|
|
7138
|
+
let transfer = loadDeferredForegroundTransfer(storeDir, boundary.transferId);
|
|
7139
|
+
assertDeferredForegroundTransferMatchesBoundary({ transfer, boundary });
|
|
7140
|
+
assertDeferredForegroundTargetPreparedAuthority({ storeDir, transfer });
|
|
7141
|
+
const source = loadManagedSession(storeDir, transfer.source_session_id);
|
|
7142
|
+
if (source.status !== "transitioning" ||
|
|
7143
|
+
source.last_transition_id !== transfer.transfer_id ||
|
|
7144
|
+
managedSessionRevision(source) !== transfer.source_expected_revision + 1 ||
|
|
7145
|
+
managedSessionBindingToken(source) !== boundary.sourceReservedBindingToken ||
|
|
7146
|
+
JSON.stringify(source.binding) !==
|
|
7147
|
+
JSON.stringify(transfer.source_before_binding)) {
|
|
7148
|
+
throw new Error(`deferred foreground source ${transfer.source_session_id} changed ` +
|
|
7149
|
+
"during terminal dispatch");
|
|
7150
|
+
}
|
|
7151
|
+
if (stage === "text_injected" &&
|
|
7152
|
+
(transfer.status !== "dispatch_started" ||
|
|
7153
|
+
transfer.input_stage !== "dispatch_started")) {
|
|
7154
|
+
throw new Error(`deferred foreground transfer ${transfer.transfer_id} cannot record text ` +
|
|
7155
|
+
`from ${transfer.status}/${transfer.input_stage}`);
|
|
7156
|
+
}
|
|
7157
|
+
if (stage === "enter_dispatched" &&
|
|
7158
|
+
(transfer.status !== "dispatch_started" ||
|
|
7159
|
+
transfer.input_stage !== "text_injected")) {
|
|
7160
|
+
throw new Error(`deferred foreground transfer ${transfer.transfer_id} cannot record Enter ` +
|
|
7161
|
+
`from ${transfer.status}/${transfer.input_stage}`);
|
|
7162
|
+
}
|
|
7163
|
+
transfer = saveDeferredForegroundTransfer(storeDir, {
|
|
7164
|
+
...transfer,
|
|
7165
|
+
status: "dispatch_started",
|
|
7166
|
+
input_stage: stage,
|
|
7167
|
+
dispatch_started_at: transfer.dispatch_started_at ?? at,
|
|
7168
|
+
...(stage === "text_injected"
|
|
7169
|
+
? { text_injected_at: at }
|
|
7170
|
+
: { enter_dispatched_at: at })
|
|
7171
|
+
}, { expectedRevision: deferredForegroundTransferRevision(transfer) });
|
|
7172
|
+
return transfer;
|
|
7173
|
+
}
|
|
7174
|
+
function beginDeferredCodexForegroundTransferDispatch({ options, boundary, at }) {
|
|
7175
|
+
const storeDir = storeDirFromOptions(options);
|
|
7176
|
+
let transfer = loadDeferredForegroundTransfer(storeDir, boundary.transferId);
|
|
7177
|
+
assertDeferredForegroundTransferMatchesBoundary({ transfer, boundary });
|
|
7178
|
+
assertDeferredForegroundTargetPreparedAuthority({ storeDir, transfer });
|
|
7179
|
+
if (transfer.status !== "target_prepared" || transfer.input_stage !== "none") {
|
|
7180
|
+
throw new Error(`deferred foreground transfer ${transfer.transfer_id} cannot begin ` +
|
|
7181
|
+
`dispatch from ${transfer.status}/${transfer.input_stage}`);
|
|
7182
|
+
}
|
|
7183
|
+
transfer = saveDeferredForegroundTransfer(storeDir, {
|
|
7184
|
+
...transfer,
|
|
7185
|
+
status: "dispatch_started",
|
|
7186
|
+
input_stage: "dispatch_started",
|
|
7187
|
+
dispatch_started_at: at
|
|
7188
|
+
}, { expectedRevision: deferredForegroundTransferRevision(transfer) });
|
|
7189
|
+
return transfer;
|
|
7190
|
+
}
|
|
7191
|
+
function isExactDeferredScrubbedSource({ source, transfer }) {
|
|
7192
|
+
const before = transfer.source_before_binding;
|
|
7193
|
+
const after = source.binding;
|
|
7194
|
+
return Boolean(source.status === "transitioning" &&
|
|
7195
|
+
source.last_transition_id === transfer.transfer_id &&
|
|
7196
|
+
managedSessionRevision(source) === transfer.source_expected_revision + 2 &&
|
|
7197
|
+
after &&
|
|
7198
|
+
after.binding_id !== before.binding_id &&
|
|
7199
|
+
after.generation === before.generation + 1 &&
|
|
7200
|
+
after.terminal_id === before.terminal_id &&
|
|
7201
|
+
terminalControlsShareIncarnation(after.terminal_control, before.terminal_control) &&
|
|
7202
|
+
after.native_thread_id === undefined &&
|
|
7203
|
+
after.native_process.rollout === undefined &&
|
|
7204
|
+
after.native_process.pid === transfer.process_pid &&
|
|
7205
|
+
after.native_process.process_uuid === transfer.process_uuid &&
|
|
7206
|
+
after.native_process.process_birth === transfer.process_birth &&
|
|
7207
|
+
after.native_process.evidence ===
|
|
7208
|
+
"deferred_predecessor_binding_scrubbed");
|
|
7209
|
+
}
|
|
7210
|
+
function isExactDeferredAcceptedTarget({ target, transfer, identity }) {
|
|
7211
|
+
const before = transfer.target_before_binding;
|
|
7212
|
+
const after = target.binding;
|
|
7213
|
+
return Boolean(before &&
|
|
7214
|
+
after &&
|
|
7215
|
+
target.status === "transitioning" &&
|
|
7216
|
+
target.last_transition_id === transfer.transfer_id &&
|
|
7217
|
+
managedSessionRevision(target) ===
|
|
7218
|
+
Number(transfer.target_prepared_revision) + 1 &&
|
|
7219
|
+
after.binding_id === before.binding_id &&
|
|
7220
|
+
after.generation === before.generation &&
|
|
7221
|
+
after.native_thread_id === identity.sessionId &&
|
|
7222
|
+
after.native_process.pid === transfer.process_pid &&
|
|
7223
|
+
after.native_process.process_uuid === identity.processUuid &&
|
|
7224
|
+
after.native_process.process_birth === identity.processBirth &&
|
|
7225
|
+
exactNativeRolloutMatches(after.native_process.rollout, identity.rollout));
|
|
7226
|
+
}
|
|
7227
|
+
async function commitDeferredCodexForegroundBinding({ options, boundary, identity, acceptedAt }) {
|
|
7228
|
+
const storeDir = storeDirFromOptions(options);
|
|
7229
|
+
let transfer = loadDeferredForegroundTransfer(storeDir, boundary.transferId);
|
|
7230
|
+
assertDeferredForegroundTransferMatchesBoundary({ transfer, boundary });
|
|
7231
|
+
if (!["dispatch_started", "uncertain"].includes(transfer.status) ||
|
|
7232
|
+
transfer.input_stage === "none" ||
|
|
7233
|
+
identity.processUuid !== transfer.process_uuid ||
|
|
7234
|
+
identity.processBirth !== transfer.process_birth ||
|
|
7235
|
+
!isExactNativeThreadId(identity.sessionId) ||
|
|
7236
|
+
!isCompleteNativeRollout(identity.rollout)) {
|
|
7237
|
+
throw new Error(`deferred foreground transfer ${transfer.transfer_id} lacks exact ` +
|
|
7238
|
+
"accepted identity authority");
|
|
7239
|
+
}
|
|
7240
|
+
let source = loadManagedSession(storeDir, transfer.source_session_id);
|
|
7241
|
+
const sourceIsReserved = source.status === "transitioning" &&
|
|
7242
|
+
source.last_transition_id === transfer.transfer_id &&
|
|
7243
|
+
managedSessionRevision(source) === transfer.source_expected_revision + 1 &&
|
|
7244
|
+
managedSessionBindingToken(source) === boundary.sourceReservedBindingToken &&
|
|
7245
|
+
JSON.stringify(source.binding) ===
|
|
7246
|
+
JSON.stringify(transfer.source_before_binding);
|
|
7247
|
+
const sameNativeThread = identity.sessionId.toLowerCase() ===
|
|
7248
|
+
transfer.source_before_binding.native_thread_id?.toLowerCase();
|
|
7249
|
+
const sourceAlreadyScrubbed = sameNativeThread &&
|
|
7250
|
+
isExactDeferredScrubbedSource({ source, transfer });
|
|
7251
|
+
if (!sourceIsReserved && !sourceAlreadyScrubbed) {
|
|
7252
|
+
throw new Error(`deferred foreground source ${source.session_id} changed before commit`);
|
|
7253
|
+
}
|
|
7254
|
+
let target = loadManagedSession(storeDir, transfer.target_session_id);
|
|
7255
|
+
const targetAlreadyAccepted = isExactDeferredAcceptedTarget({
|
|
7256
|
+
target,
|
|
7257
|
+
transfer,
|
|
7258
|
+
identity
|
|
7259
|
+
});
|
|
7260
|
+
if (!targetAlreadyAccepted) {
|
|
7261
|
+
target = assertDeferredForegroundTargetPreparedAuthority({
|
|
7262
|
+
storeDir,
|
|
7263
|
+
transfer
|
|
7264
|
+
});
|
|
7265
|
+
}
|
|
7266
|
+
// Prove there is no third-party Store/process owner before the first
|
|
7267
|
+
// retirement or target-binding CAS. The one permitted existing claim is
|
|
7268
|
+
// the reserved predecessor when Codex reused its status-card UUID.
|
|
7269
|
+
await assertNativeThreadHasExclusiveOwnership({
|
|
7270
|
+
options,
|
|
7271
|
+
agent: "codex",
|
|
7272
|
+
currentPid: transfer.process_pid,
|
|
7273
|
+
nativeThreadId: identity.sessionId,
|
|
7274
|
+
storeDir,
|
|
7275
|
+
terminalControl: boundary.terminal.terminalControl,
|
|
7276
|
+
excludedManagedSessionId: targetAlreadyAccepted
|
|
7277
|
+
? transfer.target_session_id
|
|
7278
|
+
: sameNativeThread
|
|
7279
|
+
? transfer.source_session_id
|
|
7280
|
+
: transfer.target_session_id
|
|
7281
|
+
});
|
|
7282
|
+
const retirement = sameNativeThread
|
|
7283
|
+
? "binding_scrubbed_same_native_thread"
|
|
7284
|
+
: "binding_retained";
|
|
7285
|
+
if (sameNativeThread && !sourceAlreadyScrubbed) {
|
|
7286
|
+
const scrubbedAt = cliNow();
|
|
7287
|
+
const scrubbedBinding = terminalBindingFrom({
|
|
7288
|
+
terminalId: transfer.source_before_binding.terminal_id,
|
|
7289
|
+
terminalControl: transfer.source_before_binding.terminal_control,
|
|
7290
|
+
pid: transfer.process_pid,
|
|
7291
|
+
processUuid: transfer.process_uuid,
|
|
7292
|
+
processBirth: transfer.process_birth,
|
|
7293
|
+
evidence: "deferred_predecessor_binding_scrubbed",
|
|
7294
|
+
generation: transfer.source_before_binding.generation + 1,
|
|
7295
|
+
now: scrubbedAt
|
|
7296
|
+
});
|
|
7297
|
+
source = saveManagedSession(storeDir, {
|
|
7298
|
+
...source,
|
|
7299
|
+
binding: scrubbedBinding,
|
|
7300
|
+
updated_at: scrubbedAt.toISOString()
|
|
7301
|
+
}, { expectedRevision: managedSessionRevision(source) });
|
|
7302
|
+
if (cliEnv().AKK_TEST_EXIT_AFTER_DEFERRED_SOURCE_SCRUBBED === "1") {
|
|
7303
|
+
cliExit(86);
|
|
7304
|
+
}
|
|
7305
|
+
}
|
|
7306
|
+
if (!targetAlreadyAccepted) {
|
|
7307
|
+
const acceptedBinding = {
|
|
7308
|
+
...target.binding,
|
|
7309
|
+
native_thread_id: identity.sessionId,
|
|
7310
|
+
native_process: {
|
|
7311
|
+
...target.binding.native_process,
|
|
7312
|
+
process_uuid: identity.processUuid,
|
|
7313
|
+
process_birth: identity.processBirth,
|
|
7314
|
+
rollout: identity.rollout,
|
|
7315
|
+
evidence: identity.evidence
|
|
7316
|
+
},
|
|
7317
|
+
last_verified_at: acceptedAt
|
|
7318
|
+
};
|
|
7319
|
+
target = saveManagedSession(storeDir, {
|
|
7320
|
+
...target,
|
|
7321
|
+
binding: acceptedBinding,
|
|
7322
|
+
updated_at: acceptedAt
|
|
7323
|
+
}, { expectedRevision: managedSessionRevision(target) });
|
|
7324
|
+
if (cliEnv().AKK_TEST_EXIT_AFTER_DEFERRED_TARGET_ACCEPTED === "1") {
|
|
7325
|
+
cliExit(86);
|
|
7326
|
+
}
|
|
7327
|
+
}
|
|
7328
|
+
const recoveredFromUncertainty = transfer.status === "uncertain";
|
|
7329
|
+
const transportProofAt = transfer.dispatch_started_at ?? acceptedAt;
|
|
7330
|
+
transfer = saveDeferredForegroundTransfer(storeDir, {
|
|
7331
|
+
...transfer,
|
|
7332
|
+
status: "committed",
|
|
7333
|
+
input_stage: "agent_accepted",
|
|
7334
|
+
dispatch_started_at: transfer.dispatch_started_at ?? transportProofAt,
|
|
7335
|
+
text_injected_at: transfer.text_injected_at ?? transportProofAt,
|
|
7336
|
+
enter_dispatched_at: transfer.enter_dispatched_at ?? transportProofAt,
|
|
7337
|
+
agent_accepted_at: acceptedAt,
|
|
7338
|
+
target_native_thread_id: identity.sessionId,
|
|
7339
|
+
target_accepted_revision: managedSessionRevision(target),
|
|
7340
|
+
target_accepted_status: "transitioning",
|
|
7341
|
+
target_accepted_binding_token: managedSessionBindingToken(target),
|
|
7342
|
+
target_accepted_binding: target.binding,
|
|
7343
|
+
source_pre_retirement_revision: managedSessionRevision(source),
|
|
7344
|
+
source_pre_retirement_status: "transitioning",
|
|
7345
|
+
source_pre_retirement_binding_token: managedSessionBindingToken(source),
|
|
7346
|
+
source_pre_retirement_binding: source.binding,
|
|
7347
|
+
source_retirement: retirement,
|
|
7348
|
+
committed_at: acceptedAt,
|
|
7349
|
+
...(recoveredFromUncertainty ? { recovered_at: acceptedAt } : {})
|
|
7350
|
+
}, { expectedRevision: deferredForegroundTransferRevision(transfer) });
|
|
7351
|
+
if (cliEnv().AKK_TEST_EXIT_AFTER_DEFERRED_COMMITTED === "1") {
|
|
7352
|
+
cliExit(86);
|
|
7353
|
+
}
|
|
7354
|
+
return resolveCommittedDeferredCodexForegroundTransfer({
|
|
7355
|
+
options,
|
|
7356
|
+
boundary
|
|
7357
|
+
});
|
|
7358
|
+
}
|
|
7359
|
+
async function resolveCommittedDeferredCodexForegroundTransfer({ options, boundary }) {
|
|
7360
|
+
const storeDir = storeDirFromOptions(options);
|
|
7361
|
+
let transfer = loadDeferredForegroundTransfer(storeDir, boundary.transferId);
|
|
7362
|
+
assertDeferredForegroundTransferMatchesBoundary({ transfer, boundary });
|
|
7363
|
+
if (transfer.status === "resolved") {
|
|
7364
|
+
const resolvedTarget = loadManagedSession(storeDir, transfer.target_session_id);
|
|
7365
|
+
if (resolvedTarget.status !== "bound" ||
|
|
7366
|
+
managedSessionRevision(resolvedTarget) !== transfer.target_after_revision ||
|
|
7367
|
+
managedSessionBindingToken(resolvedTarget) !==
|
|
7368
|
+
transfer.target_after_binding_token) {
|
|
7369
|
+
throw new Error(`resolved deferred target ${resolvedTarget.session_id} changed`);
|
|
7370
|
+
}
|
|
7371
|
+
return resolvedTarget;
|
|
7372
|
+
}
|
|
7373
|
+
if (transfer.status !== "committed" ||
|
|
7374
|
+
!transfer.source_pre_retirement_binding ||
|
|
7375
|
+
!transfer.source_pre_retirement_revision ||
|
|
7376
|
+
!transfer.source_pre_retirement_binding_token ||
|
|
7377
|
+
!transfer.target_accepted_binding ||
|
|
7378
|
+
!transfer.target_accepted_revision ||
|
|
7379
|
+
!transfer.target_accepted_binding_token ||
|
|
7380
|
+
!transfer.target_native_thread_id) {
|
|
7381
|
+
throw new Error(`deferred foreground transfer ${transfer.transfer_id} is not committed`);
|
|
7382
|
+
}
|
|
7383
|
+
let source = loadManagedSession(storeDir, transfer.source_session_id);
|
|
7384
|
+
if (source.status === "transitioning") {
|
|
7385
|
+
if (source.last_transition_id !== transfer.transfer_id ||
|
|
7386
|
+
managedSessionRevision(source) !== transfer.source_pre_retirement_revision ||
|
|
7387
|
+
managedSessionBindingToken(source) !==
|
|
7388
|
+
transfer.source_pre_retirement_binding_token ||
|
|
7389
|
+
JSON.stringify(source.binding) !==
|
|
7390
|
+
JSON.stringify(transfer.source_pre_retirement_binding)) {
|
|
7391
|
+
throw new Error(`deferred foreground source ${source.session_id} changed during commit recovery`);
|
|
7392
|
+
}
|
|
7393
|
+
const detachedAt = cliNow().toISOString();
|
|
7394
|
+
source = saveManagedSession(storeDir, {
|
|
7395
|
+
...source,
|
|
7396
|
+
status: "detached",
|
|
7397
|
+
last_transition_id: transfer.source_previous_last_transition_id,
|
|
7398
|
+
detached_at: detachedAt,
|
|
7399
|
+
updated_at: detachedAt
|
|
7400
|
+
}, { expectedRevision: managedSessionRevision(source) });
|
|
7401
|
+
if (cliEnv().AKK_TEST_EXIT_AFTER_DEFERRED_SOURCE_DETACHED === "1") {
|
|
7402
|
+
cliExit(86);
|
|
7403
|
+
}
|
|
7404
|
+
}
|
|
7405
|
+
else if (source.status !== "detached" ||
|
|
7406
|
+
managedSessionRevision(source) !==
|
|
7407
|
+
transfer.source_pre_retirement_revision + 1 ||
|
|
7408
|
+
managedSessionBindingToken(source) !== managedSessionBindingToken({
|
|
7409
|
+
session_id: source.session_id,
|
|
7410
|
+
status: "detached",
|
|
7411
|
+
binding: transfer.source_pre_retirement_binding
|
|
7412
|
+
}) ||
|
|
7413
|
+
JSON.stringify(source.binding) !==
|
|
7414
|
+
JSON.stringify(transfer.source_pre_retirement_binding)) {
|
|
7415
|
+
throw new Error(`deferred foreground source ${source.session_id} has no exact retired receipt`);
|
|
7416
|
+
}
|
|
7417
|
+
let target = loadManagedSession(storeDir, transfer.target_session_id);
|
|
7418
|
+
if (target.status === "transitioning") {
|
|
7419
|
+
if (target.last_transition_id !== transfer.transfer_id ||
|
|
7420
|
+
managedSessionRevision(target) !== transfer.target_accepted_revision ||
|
|
7421
|
+
managedSessionBindingToken(target) !==
|
|
7422
|
+
transfer.target_accepted_binding_token ||
|
|
7423
|
+
JSON.stringify(target.binding) !==
|
|
7424
|
+
JSON.stringify(transfer.target_accepted_binding)) {
|
|
7425
|
+
throw new Error(`deferred foreground target ${target.session_id} changed during commit recovery`);
|
|
7426
|
+
}
|
|
7427
|
+
const boundAt = cliNow().toISOString();
|
|
7428
|
+
target = saveManagedSession(storeDir, {
|
|
7429
|
+
...target,
|
|
7430
|
+
status: "bound",
|
|
7431
|
+
last_transition_id: undefined,
|
|
7432
|
+
updated_at: boundAt
|
|
7433
|
+
}, { expectedRevision: managedSessionRevision(target) });
|
|
7434
|
+
if (cliEnv().AKK_TEST_EXIT_AFTER_DEFERRED_TARGET_BOUND === "1") {
|
|
7435
|
+
cliExit(86);
|
|
7436
|
+
}
|
|
7437
|
+
}
|
|
7438
|
+
else if (target.status !== "bound" ||
|
|
7439
|
+
managedSessionRevision(target) !== transfer.target_accepted_revision + 1 ||
|
|
7440
|
+
managedSessionBindingToken(target) !== managedSessionBindingToken({
|
|
7441
|
+
session_id: target.session_id,
|
|
7442
|
+
status: "bound",
|
|
7443
|
+
binding: transfer.target_accepted_binding
|
|
7444
|
+
}) ||
|
|
7445
|
+
JSON.stringify(target.binding) !==
|
|
7446
|
+
JSON.stringify(transfer.target_accepted_binding)) {
|
|
7447
|
+
throw new Error(`deferred foreground target ${target.session_id} has no exact bound receipt`);
|
|
7448
|
+
}
|
|
7449
|
+
await assertNativeThreadHasExclusiveOwnership({
|
|
7450
|
+
options,
|
|
7451
|
+
agent: "codex",
|
|
7452
|
+
currentPid: transfer.process_pid,
|
|
7453
|
+
nativeThreadId: transfer.target_native_thread_id,
|
|
7454
|
+
storeDir,
|
|
7455
|
+
terminalControl: boundary.terminal.terminalControl,
|
|
7456
|
+
excludedManagedSessionId: target.session_id
|
|
7457
|
+
});
|
|
7458
|
+
transfer = saveDeferredForegroundTransfer(storeDir, {
|
|
7459
|
+
...transfer,
|
|
7460
|
+
status: "resolved",
|
|
7461
|
+
target_after_revision: managedSessionRevision(target),
|
|
7462
|
+
target_after_status: "bound",
|
|
7463
|
+
target_after_binding_token: managedSessionBindingToken(target),
|
|
7464
|
+
source_after_revision: managedSessionRevision(source),
|
|
7465
|
+
source_after_status: "detached",
|
|
7466
|
+
source_after_binding: source.binding,
|
|
7467
|
+
source_after_binding_token: managedSessionBindingToken(source),
|
|
7468
|
+
resolved_at: cliNow().toISOString()
|
|
7469
|
+
}, { expectedRevision: deferredForegroundTransferRevision(transfer) });
|
|
7470
|
+
runtimeLog("info", "deferred_codex_foreground_transfer_resolved", {
|
|
7471
|
+
transfer_id: transfer.transfer_id,
|
|
7472
|
+
source_session_id: source.session_id,
|
|
7473
|
+
target_session_id: target.session_id,
|
|
7474
|
+
native_thread_id: transfer.target_native_thread_id,
|
|
7475
|
+
source_retirement: transfer.source_retirement
|
|
7476
|
+
});
|
|
7477
|
+
return target;
|
|
7478
|
+
}
|
|
7479
|
+
function markDeferredCodexForegroundTransferUncertain({ options, boundary, reason }) {
|
|
7480
|
+
const storeDir = storeDirFromOptions(options);
|
|
7481
|
+
let transfer = loadDeferredForegroundTransfer(storeDir, boundary.transferId);
|
|
7482
|
+
assertDeferredForegroundTransferMatchesBoundary({ transfer, boundary });
|
|
7483
|
+
if (["committed", "resolved", "uncertain"].includes(transfer.status)) {
|
|
7484
|
+
return transfer;
|
|
7485
|
+
}
|
|
7486
|
+
if (![
|
|
7487
|
+
"target_prepared",
|
|
7488
|
+
"dispatch_started"
|
|
7489
|
+
].includes(transfer.status)) {
|
|
7490
|
+
throw new Error(`deferred foreground transfer ${transfer.transfer_id} has no durable ` +
|
|
7491
|
+
"dispatch-start fence");
|
|
7492
|
+
}
|
|
7493
|
+
transfer = saveDeferredForegroundTransfer(storeDir, {
|
|
7494
|
+
...transfer,
|
|
7495
|
+
status: "uncertain",
|
|
7496
|
+
input_stage: transfer.input_stage === "none"
|
|
7497
|
+
? "dispatch_started"
|
|
7498
|
+
: transfer.input_stage,
|
|
7499
|
+
dispatch_started_at: transfer.dispatch_started_at ?? cliNow().toISOString(),
|
|
7500
|
+
uncertain_at: cliNow().toISOString(),
|
|
7501
|
+
error: reason.slice(0, 2000),
|
|
7502
|
+
do_not_retry: true
|
|
7503
|
+
}, { expectedRevision: deferredForegroundTransferRevision(transfer) });
|
|
7504
|
+
return transfer;
|
|
7505
|
+
}
|
|
7506
|
+
function observedHandoffAuthorityToken({ terminal, identity, sourceSession, target }) {
|
|
7507
|
+
const exact = exactLifecycleProcessIdentity(terminal, identity);
|
|
7508
|
+
const terminalToken = unmanagedTerminalBindingToken({
|
|
7509
|
+
terminalId: terminal.conversationId,
|
|
7510
|
+
terminalControl: terminal.terminalControl,
|
|
7511
|
+
agent: terminal.agent,
|
|
7512
|
+
pid: terminal.pid,
|
|
7513
|
+
workspace: terminal.terminalControl.currentPath ?? cliCwd(),
|
|
7514
|
+
nativeThreadId: exact.sessionId,
|
|
7515
|
+
processUuid: exact.processUuid,
|
|
7516
|
+
processBirth: exact.processBirth,
|
|
7517
|
+
rollout: exact.rollout
|
|
7518
|
+
});
|
|
7519
|
+
return humanObservedHandoffBindingToken({
|
|
7520
|
+
terminal_token: terminalToken,
|
|
7521
|
+
source_session_id: sourceSession.session_id,
|
|
7522
|
+
source_revision: managedSessionRevision(sourceSession),
|
|
7523
|
+
source_binding_token: managedSessionBindingToken(sourceSession),
|
|
7524
|
+
target
|
|
7525
|
+
});
|
|
7526
|
+
}
|
|
7527
|
+
function activeTurnHandoffDecisionToken({ handoffToken, turn, ledger }) {
|
|
7528
|
+
const takeover = isRecord(turn.native_session_takeover)
|
|
7529
|
+
? turn.native_session_takeover
|
|
7530
|
+
: undefined;
|
|
7531
|
+
const submission = terminalBridgeSubmission(turn);
|
|
7532
|
+
return createHash("sha256")
|
|
7533
|
+
.update(JSON.stringify({
|
|
7534
|
+
version: 1,
|
|
7535
|
+
kind: "active_turn_human_handoff",
|
|
7536
|
+
handoff_token: handoffToken,
|
|
7537
|
+
session_id: sessionIdForConversation(turn),
|
|
7538
|
+
turn_id: turnIdForConversation(turn),
|
|
7539
|
+
turn_status: turn.status,
|
|
7540
|
+
turn_updated_at: turn.updated_at ?? null,
|
|
7541
|
+
current_message_id: stringValue(takeover?.terminal_bridge_message_id) ??
|
|
7542
|
+
stringValue(submission?.message_id) ??
|
|
7543
|
+
null,
|
|
7544
|
+
ledger_generation_id: stringValue(ledger?.generation_id) ?? null,
|
|
7545
|
+
ledger_message_id: stringValue(ledger?.message_id) ?? null,
|
|
7546
|
+
ledger_status: stringValue(ledger?.status) ?? null
|
|
7547
|
+
}))
|
|
7548
|
+
.digest("hex");
|
|
7549
|
+
}
|
|
7550
|
+
const originalExpectedTerminalSelector = new WeakMap();
|
|
7551
|
+
function assertExpectedHandoffTokenUsesExactTerminalSelector({ options, terminal }) {
|
|
7552
|
+
if (!stringValue(options.expectedTerminalToken)) {
|
|
7553
|
+
return;
|
|
7554
|
+
}
|
|
7555
|
+
const supplied = originalExpectedTerminalSelector.has(options)
|
|
7556
|
+
? originalExpectedTerminalSelector.get(options)
|
|
7557
|
+
: stringValue(options.session ?? options.conversation ?? options.conversationId)?.trim();
|
|
7558
|
+
if (supplied !== terminal.conversationId) {
|
|
7559
|
+
throw new Error("--expected-terminal-token is valid only with the exact full terminal " +
|
|
7560
|
+
"conversation selector advertised by AKK list");
|
|
7561
|
+
}
|
|
7562
|
+
}
|
|
7563
|
+
async function observedExternalHandoffIdentity({ options, terminal, sourceSession, resolvedIdentity, requireSafeTerminal = true }) {
|
|
7564
|
+
const bridge = createTerminalAgentBridge(options);
|
|
7565
|
+
const status = await bridge.status(terminal.agent, terminal.terminalControl, { runtime: terminalRuntimeForLiveIdentity({ terminal, physicalOnly: true }) });
|
|
7566
|
+
if (requireSafeTerminal) {
|
|
7567
|
+
assertSafeTerminalSend(terminal.agent, status);
|
|
7568
|
+
}
|
|
7569
|
+
if (terminal.agent !== "codex") {
|
|
7570
|
+
return { identity: resolvedIdentity, status };
|
|
7571
|
+
}
|
|
7572
|
+
const sourceBinding = sourceSession.binding;
|
|
7573
|
+
const statusCard = terminal.adapter.observeThreadLifecycle?.({
|
|
7574
|
+
operation: { kind: "new_thread" },
|
|
7575
|
+
phase: "before",
|
|
7576
|
+
screen: status.screen.excerpt ?? ""
|
|
7577
|
+
});
|
|
7578
|
+
const statusCardId = statusCard?.status === "observed" &&
|
|
7579
|
+
isExactNativeThreadId(statusCard.nativeThreadId)
|
|
7580
|
+
? statusCard.nativeThreadId.toLowerCase()
|
|
7581
|
+
: undefined;
|
|
7582
|
+
const sourceId = sourceBinding?.native_thread_id?.toLowerCase();
|
|
7583
|
+
if (statusCardId && sourceId && statusCardId !== sourceId) {
|
|
7584
|
+
const processUuid = sourceBinding?.native_process.process_uuid;
|
|
6454
7585
|
const processBirth = sourceBinding?.native_process.process_birth;
|
|
6455
7586
|
if (!processUuid || !processBirth) {
|
|
6456
7587
|
return { identity: undefined, status };
|
|
@@ -10016,7 +11147,7 @@ async function runSend(options) {
|
|
|
10016
11147
|
currentNativeIdentity = undefined;
|
|
10017
11148
|
}
|
|
10018
11149
|
const physicalNativeIdentityBeforeHandoff = currentNativeIdentity;
|
|
10019
|
-
|
|
11150
|
+
let handoff = await maybeAdoptObservedExternalThread({
|
|
10020
11151
|
options,
|
|
10021
11152
|
terminal: terminalConversation,
|
|
10022
11153
|
sourceSession: claimedSession,
|
|
@@ -10029,19 +11160,42 @@ async function runSend(options) {
|
|
|
10029
11160
|
!handoff.session?.binding?.native_process.rollout
|
|
10030
11161
|
? physicalNativeIdentityBeforeHandoff
|
|
10031
11162
|
: handoff.identity;
|
|
10032
|
-
|
|
10033
|
-
|
|
11163
|
+
const deferredCodexForegroundBinding = !handoff.adopted
|
|
11164
|
+
? await prepareDeferredCodexForegroundBinding({
|
|
10034
11165
|
options,
|
|
10035
11166
|
terminal: terminalConversation,
|
|
10036
|
-
|
|
10037
|
-
|
|
10038
|
-
|
|
10039
|
-
|
|
10040
|
-
|
|
10041
|
-
|
|
10042
|
-
|
|
10043
|
-
|
|
10044
|
-
|
|
11167
|
+
sourceSession: claimedSession,
|
|
11168
|
+
observation: nativeIdentityObservation,
|
|
11169
|
+
requestText: String(messageBody)
|
|
11170
|
+
})
|
|
11171
|
+
: undefined;
|
|
11172
|
+
if (deferredCodexForegroundBinding) {
|
|
11173
|
+
claimedSession = undefined;
|
|
11174
|
+
knownCodexCompanions = { additional: [] };
|
|
11175
|
+
currentNativeIdentity = undefined;
|
|
11176
|
+
handoff = { identity: undefined, adopted: false };
|
|
11177
|
+
}
|
|
11178
|
+
else if (stringValue(options.expectedTerminalToken) &&
|
|
11179
|
+
!verifiedEmptyHandoff &&
|
|
11180
|
+
!handoff.adopted) {
|
|
11181
|
+
throw new Error("the expected terminal token no longer authorizes the current " +
|
|
11182
|
+
"terminal context; refresh AKK list");
|
|
11183
|
+
}
|
|
11184
|
+
let managedSession = deferredCodexForegroundBinding
|
|
11185
|
+
? undefined
|
|
11186
|
+
: handoff.session ??
|
|
11187
|
+
materializeCurrentManagedSession({
|
|
11188
|
+
options,
|
|
11189
|
+
terminal: terminalConversation,
|
|
11190
|
+
identity: currentNativeIdentity
|
|
11191
|
+
});
|
|
11192
|
+
if (!managedSession && currentNativeIdentity) {
|
|
11193
|
+
managedSession = await reattachManagedSessionForNativeIdentity({
|
|
11194
|
+
options,
|
|
11195
|
+
terminal: terminalConversation,
|
|
11196
|
+
identity: currentNativeIdentity,
|
|
11197
|
+
storeDir: rawStoreDir
|
|
11198
|
+
});
|
|
10045
11199
|
}
|
|
10046
11200
|
let pendingRawAttachSessionCreate;
|
|
10047
11201
|
if (!managedSession) {
|
|
@@ -10056,13 +11210,29 @@ async function runSend(options) {
|
|
|
10056
11210
|
});
|
|
10057
11211
|
}
|
|
10058
11212
|
managedSession = createBoundManagedSession({
|
|
10059
|
-
sessionId:
|
|
11213
|
+
sessionId: deferredCodexForegroundBinding?.targetSessionId ??
|
|
11214
|
+
createManagedSessionId(),
|
|
10060
11215
|
terminal: terminalConversation,
|
|
10061
11216
|
identity: currentNativeIdentity,
|
|
10062
|
-
lineage:
|
|
11217
|
+
lineage: deferredCodexForegroundBinding
|
|
11218
|
+
? {
|
|
11219
|
+
created_by: "attach",
|
|
11220
|
+
previous_session_id: deferredCodexForegroundBinding.sourceSessionId,
|
|
11221
|
+
transition_id: deferredCodexForegroundBinding.transferId
|
|
11222
|
+
}
|
|
11223
|
+
: { created_by: "attach" }
|
|
10063
11224
|
});
|
|
10064
11225
|
if (terminalConversation.agent === "codex" &&
|
|
10065
11226
|
!currentNativeIdentity) {
|
|
11227
|
+
if (deferredCodexForegroundBinding) {
|
|
11228
|
+
managedSession = {
|
|
11229
|
+
...managedSession,
|
|
11230
|
+
// Both endpoints stay fenced from older writers until the
|
|
11231
|
+
// dedicated transfer reaches its resolved receipt.
|
|
11232
|
+
status: "transitioning",
|
|
11233
|
+
last_transition_id: deferredCodexForegroundBinding.transferId
|
|
11234
|
+
};
|
|
11235
|
+
}
|
|
10066
11236
|
pendingRawAttachSessionCreate = managedSession;
|
|
10067
11237
|
}
|
|
10068
11238
|
else {
|
|
@@ -10131,7 +11301,8 @@ async function runSend(options) {
|
|
|
10131
11301
|
terminalControl: terminalConversation.terminalControl,
|
|
10132
11302
|
previousTurn: reusableTurn,
|
|
10133
11303
|
managedSession,
|
|
10134
|
-
nativeAgentIdentity: materializedNativeIdentity
|
|
11304
|
+
nativeAgentIdentity: materializedNativeIdentity,
|
|
11305
|
+
deferredForegroundTransferId: deferredCodexForegroundBinding?.transferId
|
|
10135
11306
|
});
|
|
10136
11307
|
ensureStoreWritable(managed.conversation.store_dir);
|
|
10137
11308
|
ensureDir(path.dirname(managed.statePath));
|
|
@@ -10152,8 +11323,30 @@ async function runSend(options) {
|
|
|
10152
11323
|
recordMessageAfterSend: true,
|
|
10153
11324
|
recordRawAttachmentAfterSend: reusableTurn === undefined,
|
|
10154
11325
|
onTerminalPreflightVerified: pendingRawAttachSessionCreate
|
|
10155
|
-
? () => {
|
|
10156
|
-
|
|
11326
|
+
? async () => {
|
|
11327
|
+
let createdSession;
|
|
11328
|
+
try {
|
|
11329
|
+
if (deferredCodexForegroundBinding) {
|
|
11330
|
+
const reserved = await reserveDeferredCodexForegroundTransfer({
|
|
11331
|
+
options,
|
|
11332
|
+
boundary: deferredCodexForegroundBinding,
|
|
11333
|
+
targetSession: pendingRawAttachSessionCreate,
|
|
11334
|
+
messageId: managed.message.id,
|
|
11335
|
+
turnId: turnIdForConversation(managed.conversation),
|
|
11336
|
+
statePath: managed.statePath
|
|
11337
|
+
});
|
|
11338
|
+
createdSession = reserved.createdSession;
|
|
11339
|
+
managedSession = createdSession;
|
|
11340
|
+
pendingRawAttachSessionCreate = undefined;
|
|
11341
|
+
return reserved.rollback;
|
|
11342
|
+
}
|
|
11343
|
+
createdSession = saveManagedSession(rawStoreDir, pendingRawAttachSessionCreate, {
|
|
11344
|
+
expectedRevision: null
|
|
11345
|
+
});
|
|
11346
|
+
}
|
|
11347
|
+
catch (error) {
|
|
11348
|
+
throw error;
|
|
11349
|
+
}
|
|
10157
11350
|
managedSession = createdSession;
|
|
10158
11351
|
pendingRawAttachSessionCreate = undefined;
|
|
10159
11352
|
return () => {
|
|
@@ -10184,7 +11377,8 @@ async function runSend(options) {
|
|
|
10184
11377
|
transition: handoff.transition
|
|
10185
11378
|
}
|
|
10186
11379
|
: undefined,
|
|
10187
|
-
verifiedEmptyCodexHandoff: verifiedEmptyHandoff?.boundary
|
|
11380
|
+
verifiedEmptyCodexHandoff: verifiedEmptyHandoff?.boundary,
|
|
11381
|
+
deferredCodexForegroundBinding
|
|
10188
11382
|
});
|
|
10189
11383
|
}
|
|
10190
11384
|
finally {
|
|
@@ -10769,6 +11963,7 @@ async function runApprove(options) {
|
|
|
10769
11963
|
throw new Error("approval state changed while waiting for terminal control; refresh status and retry");
|
|
10770
11964
|
}
|
|
10771
11965
|
assertManagedTerminalDispatchOwner({
|
|
11966
|
+
storeDir: writerStoreDir,
|
|
10772
11967
|
conversation: currentConversation,
|
|
10773
11968
|
terminalControl: currentControl,
|
|
10774
11969
|
action: "approve"
|
|
@@ -11144,7 +12339,7 @@ async function runTerminalConversationApprove({ options, conversationId, agent,
|
|
|
11144
12339
|
releaseTerminalLock();
|
|
11145
12340
|
}
|
|
11146
12341
|
}
|
|
11147
|
-
async function runTerminalControlSend({ options, conversation, nextConversation, statePath, logPath, executor, message, terminalControl, terminalSendLockHeld = false, terminalStateLockHeld = false, storeWriterLeaseHeld = false, recordMessageAfterSend = false, recordRawAttachmentAfterSend = false, onTerminalPreflightVerified = undefined, allowedPreMaterializationIdentity = undefined, allowedAdditionalIdentities = [], observedHandoff = undefined, verifiedEmptyCodexHandoff = undefined, continuingTurnResponse = false }) {
|
|
12342
|
+
async function runTerminalControlSend({ options, conversation, nextConversation, statePath, logPath, executor, message, terminalControl, terminalSendLockHeld = false, terminalStateLockHeld = false, storeWriterLeaseHeld = false, recordMessageAfterSend = false, recordRawAttachmentAfterSend = false, onTerminalPreflightVerified = undefined, allowedPreMaterializationIdentity = undefined, allowedAdditionalIdentities = [], observedHandoff = undefined, verifiedEmptyCodexHandoff = undefined, deferredCodexForegroundBinding = undefined, continuingTurnResponse = false }) {
|
|
11148
12343
|
const bridge = terminalBridgeEnabled(conversation);
|
|
11149
12344
|
if (!terminalSendLockHeld) {
|
|
11150
12345
|
const releaseTerminalLock = acquireTerminalBridgeSendLock(storeDirFromOptions(options), terminalControl, { timeoutMs: 30000 });
|
|
@@ -11168,6 +12363,7 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
11168
12363
|
allowedAdditionalIdentities,
|
|
11169
12364
|
observedHandoff,
|
|
11170
12365
|
verifiedEmptyCodexHandoff,
|
|
12366
|
+
deferredCodexForegroundBinding,
|
|
11171
12367
|
continuingTurnResponse
|
|
11172
12368
|
});
|
|
11173
12369
|
}
|
|
@@ -11209,6 +12405,7 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
11209
12405
|
allowedAdditionalIdentities,
|
|
11210
12406
|
observedHandoff,
|
|
11211
12407
|
verifiedEmptyCodexHandoff,
|
|
12408
|
+
deferredCodexForegroundBinding,
|
|
11212
12409
|
continuingTurnResponse
|
|
11213
12410
|
});
|
|
11214
12411
|
}
|
|
@@ -11237,6 +12434,7 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
11237
12434
|
allowedAdditionalIdentities,
|
|
11238
12435
|
observedHandoff,
|
|
11239
12436
|
verifiedEmptyCodexHandoff,
|
|
12437
|
+
deferredCodexForegroundBinding,
|
|
11240
12438
|
continuingTurnResponse
|
|
11241
12439
|
}));
|
|
11242
12440
|
}
|
|
@@ -11245,7 +12443,7 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
11245
12443
|
const agentTimeoutMinutes = Number(options.agentTimeoutMinutes ?? DEFAULT_AGENT_TIMEOUT_MINUTES);
|
|
11246
12444
|
const agentHardTimeoutMinutes = positiveMinutes(options.agentHardTimeoutMinutes ?? DEFAULT_AGENT_HARD_TIMEOUT_MINUTES, "--agent-hard-timeout-minutes");
|
|
11247
12445
|
const terminalPayload = terminalSubmissionPayload(String(message.body ?? ""));
|
|
11248
|
-
const terminalRequestHash = terminalBridgeRequestFingerprint(terminalPayload);
|
|
12446
|
+
const terminalRequestHash = required(terminalBridgeRequestFingerprint(terminalPayload), "terminal request hash is unavailable");
|
|
11249
12447
|
let previousDispatchLedger = resolveTerminalDispatchLedgerPaneIncarnation(terminalControl, loadTerminalBridgeDispatchLedger(terminalControl));
|
|
11250
12448
|
previousDispatchLedger =
|
|
11251
12449
|
reconcilePreparedTerminalDispatchLedger(terminalControl, previousDispatchLedger);
|
|
@@ -11418,12 +12616,12 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
11418
12616
|
scrollbackLines: Number(options.scrollbackLines ?? 120),
|
|
11419
12617
|
runtime: preSendRuntime
|
|
11420
12618
|
});
|
|
11421
|
-
if (verifiedEmptyCodexHandoff) {
|
|
12619
|
+
if (verifiedEmptyCodexHandoff || deferredCodexForegroundBinding) {
|
|
11422
12620
|
if (executor.kind !== "codex" ||
|
|
11423
12621
|
status.reachable !== true ||
|
|
11424
12622
|
status.approval_state.blocked === true ||
|
|
11425
12623
|
!["idle", "unknown"].includes(status.activity_state)) {
|
|
11426
|
-
throw new Error(`Codex
|
|
12624
|
+
throw new Error(`Codex deferred foreground send is not at a safe prompt ` +
|
|
11427
12625
|
`(${status.activity_state}: ${status.activity_reason})`);
|
|
11428
12626
|
}
|
|
11429
12627
|
}
|
|
@@ -11488,7 +12686,7 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
11488
12686
|
// acceptance check has passed, but before the Turn or dispatch ledger can
|
|
11489
12687
|
// become durable. This prevents a failed virgin attach from leaving a
|
|
11490
12688
|
// zero-identity `bound` Session that fences every later control action.
|
|
11491
|
-
let rollbackPreTransportAttach = onTerminalPreflightVerified?.();
|
|
12689
|
+
let rollbackPreTransportAttach = await onTerminalPreflightVerified?.();
|
|
11492
12690
|
const rollbackRawAttachBeforeTransport = () => {
|
|
11493
12691
|
if (!rollbackPreTransportAttach) {
|
|
11494
12692
|
return true;
|
|
@@ -11508,6 +12706,39 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
11508
12706
|
return false;
|
|
11509
12707
|
}
|
|
11510
12708
|
};
|
|
12709
|
+
const abortDeferredPreInputBeforeTransport = (terminalInputNotStartedAt) => {
|
|
12710
|
+
if (!deferredCodexForegroundBinding) {
|
|
12711
|
+
return false;
|
|
12712
|
+
}
|
|
12713
|
+
if (!terminalStateLockHeld) {
|
|
12714
|
+
throw new Error("deferred Codex pre-input abort requires the exact Turn state lock");
|
|
12715
|
+
}
|
|
12716
|
+
const deferredStoreDir = storeDirFromOptions(options);
|
|
12717
|
+
const transfer = loadDeferredForegroundTransfer(deferredStoreDir, deferredCodexForegroundBinding.transferId);
|
|
12718
|
+
const durableTerminalInputNotStartedAt = terminalInputNotStartedAt === undefined || transfer.input_stage === "none"
|
|
12719
|
+
? undefined
|
|
12720
|
+
: transfer.status === "dispatch_started" &&
|
|
12721
|
+
transfer.input_stage === "dispatch_started"
|
|
12722
|
+
? terminalInputNotStartedAt
|
|
12723
|
+
: (() => {
|
|
12724
|
+
throw new Error(`deferred foreground transfer ${transfer.transfer_id} has ` +
|
|
12725
|
+
`${transfer.input_stage} input evidence and cannot accept a ` +
|
|
12726
|
+
"terminal-input-not-started result");
|
|
12727
|
+
})();
|
|
12728
|
+
abortPreparedDeferredForegroundTurn({
|
|
12729
|
+
storeDir: deferredStoreDir,
|
|
12730
|
+
terminal: deferredCodexForegroundBinding.terminal,
|
|
12731
|
+
transfer,
|
|
12732
|
+
boundary: deferredCodexForegroundBinding,
|
|
12733
|
+
stateLockHeld: true,
|
|
12734
|
+
terminalInputNotStartedAt: durableTerminalInputNotStartedAt
|
|
12735
|
+
});
|
|
12736
|
+
// abortPreparedDeferredForegroundTurn has already made the Turn, ledger,
|
|
12737
|
+
// transfer, and both Sessions durable in zero-input terminal states. Do
|
|
12738
|
+
// not invoke the reservation rollback closure a second time.
|
|
12739
|
+
rollbackPreTransportAttach = undefined;
|
|
12740
|
+
return true;
|
|
12741
|
+
};
|
|
11511
12742
|
const bridgeConversation = bridge
|
|
11512
12743
|
? withTerminalBridgeState({
|
|
11513
12744
|
conversation: nextConversation,
|
|
@@ -11553,11 +12784,13 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
11553
12784
|
}
|
|
11554
12785
|
catch (error) {
|
|
11555
12786
|
try {
|
|
11556
|
-
|
|
11557
|
-
|
|
11558
|
-
|
|
11559
|
-
|
|
11560
|
-
|
|
12787
|
+
if (!abortDeferredPreInputBeforeTransport()) {
|
|
12788
|
+
restoreTerminalBridgeDispatchLedger({
|
|
12789
|
+
terminalControl,
|
|
12790
|
+
previousLedger: previousDispatchLedger,
|
|
12791
|
+
reason: "prepared ledger persistence failed before terminal input"
|
|
12792
|
+
});
|
|
12793
|
+
}
|
|
11561
12794
|
}
|
|
11562
12795
|
finally {
|
|
11563
12796
|
rollbackRawAttachBeforeTransport();
|
|
@@ -11569,11 +12802,13 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
11569
12802
|
}
|
|
11570
12803
|
catch (error) {
|
|
11571
12804
|
try {
|
|
11572
|
-
|
|
11573
|
-
|
|
11574
|
-
|
|
11575
|
-
|
|
11576
|
-
|
|
12805
|
+
if (!abortDeferredPreInputBeforeTransport()) {
|
|
12806
|
+
restoreTerminalBridgeDispatchLedger({
|
|
12807
|
+
terminalControl,
|
|
12808
|
+
previousLedger: previousDispatchLedger,
|
|
12809
|
+
reason: "prepared state persistence failed before terminal input"
|
|
12810
|
+
});
|
|
12811
|
+
}
|
|
11577
12812
|
}
|
|
11578
12813
|
finally {
|
|
11579
12814
|
rollbackRawAttachBeforeTransport();
|
|
@@ -11634,11 +12869,13 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
11634
12869
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
11635
12870
|
let dispatchLedgerRestored = true;
|
|
11636
12871
|
try {
|
|
11637
|
-
|
|
11638
|
-
|
|
11639
|
-
|
|
11640
|
-
|
|
11641
|
-
|
|
12872
|
+
if (!abortDeferredPreInputBeforeTransport()) {
|
|
12873
|
+
restoreTerminalBridgeDispatchLedger({
|
|
12874
|
+
terminalControl,
|
|
12875
|
+
previousLedger: previousDispatchLedger,
|
|
12876
|
+
reason: "terminal submission aborted before terminal input"
|
|
12877
|
+
});
|
|
12878
|
+
}
|
|
11642
12879
|
}
|
|
11643
12880
|
catch (ledgerError) {
|
|
11644
12881
|
dispatchLedgerRestored = false;
|
|
@@ -11800,7 +13037,8 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
11800
13037
|
await terminalBridge.send(executor.kind, terminalControl, terminalPayload, {
|
|
11801
13038
|
runtime: preSendRuntime,
|
|
11802
13039
|
requireExactComposerBeforeEnter: observedHandoff !== undefined ||
|
|
11803
|
-
verifiedEmptyCodexHandoff !== undefined
|
|
13040
|
+
verifiedEmptyCodexHandoff !== undefined ||
|
|
13041
|
+
deferredCodexForegroundBinding !== undefined,
|
|
11804
13042
|
beforeText: observedHandoff
|
|
11805
13043
|
? async () => {
|
|
11806
13044
|
await assertObservedHandoffTransportBoundary({
|
|
@@ -11818,7 +13056,22 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
11818
13056
|
requireEmptyComposer: true
|
|
11819
13057
|
});
|
|
11820
13058
|
}
|
|
11821
|
-
:
|
|
13059
|
+
: deferredCodexForegroundBinding
|
|
13060
|
+
? async () => {
|
|
13061
|
+
await assertDeferredCodexForegroundBindingBoundary({
|
|
13062
|
+
options,
|
|
13063
|
+
boundary: deferredCodexForegroundBinding,
|
|
13064
|
+
expectedSourceStatus: "transitioning",
|
|
13065
|
+
requireNoDispatch: false,
|
|
13066
|
+
requireEmptyComposer: true
|
|
13067
|
+
});
|
|
13068
|
+
beginDeferredCodexForegroundTransferDispatch({
|
|
13069
|
+
options,
|
|
13070
|
+
boundary: deferredCodexForegroundBinding,
|
|
13071
|
+
at: cliNow().toISOString()
|
|
13072
|
+
});
|
|
13073
|
+
}
|
|
13074
|
+
: undefined,
|
|
11822
13075
|
beforeEnter: observedHandoff
|
|
11823
13076
|
? async () => {
|
|
11824
13077
|
await assertObservedHandoffTransportBoundary({
|
|
@@ -11836,7 +13089,17 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
11836
13089
|
requireEmptyComposer: false
|
|
11837
13090
|
});
|
|
11838
13091
|
}
|
|
11839
|
-
:
|
|
13092
|
+
: deferredCodexForegroundBinding
|
|
13093
|
+
? async () => {
|
|
13094
|
+
await assertDeferredCodexForegroundBindingBoundary({
|
|
13095
|
+
options,
|
|
13096
|
+
boundary: deferredCodexForegroundBinding,
|
|
13097
|
+
expectedSourceStatus: "transitioning",
|
|
13098
|
+
requireNoDispatch: false,
|
|
13099
|
+
requireEmptyComposer: false
|
|
13100
|
+
});
|
|
13101
|
+
}
|
|
13102
|
+
: undefined,
|
|
11840
13103
|
async onTransportStage(event) {
|
|
11841
13104
|
const stageAt = cliNow().toISOString();
|
|
11842
13105
|
if (event.stage === "text_injected") {
|
|
@@ -11877,6 +13140,14 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
11877
13140
|
previous_generation_id: stringValue(previousDispatchLedger?.generation_id) ??
|
|
11878
13141
|
stringValue(previousDispatchLedger?.message_id)
|
|
11879
13142
|
});
|
|
13143
|
+
if (deferredCodexForegroundBinding) {
|
|
13144
|
+
advanceDeferredCodexForegroundTransferInputStage({
|
|
13145
|
+
options,
|
|
13146
|
+
boundary: deferredCodexForegroundBinding,
|
|
13147
|
+
stage: event.stage,
|
|
13148
|
+
at: stageAt
|
|
13149
|
+
});
|
|
13150
|
+
}
|
|
11880
13151
|
if (event.stage === "text_injected" && observedHandoff) {
|
|
11881
13152
|
await assertObservedHandoffTransportBoundary({
|
|
11882
13153
|
options,
|
|
@@ -11892,6 +13163,15 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
11892
13163
|
requireEmptyComposer: false
|
|
11893
13164
|
});
|
|
11894
13165
|
}
|
|
13166
|
+
if (event.stage === "text_injected" && deferredCodexForegroundBinding) {
|
|
13167
|
+
await assertDeferredCodexForegroundBindingBoundary({
|
|
13168
|
+
options,
|
|
13169
|
+
boundary: deferredCodexForegroundBinding,
|
|
13170
|
+
expectedSourceStatus: "transitioning",
|
|
13171
|
+
requireNoDispatch: false,
|
|
13172
|
+
requireEmptyComposer: false
|
|
13173
|
+
});
|
|
13174
|
+
}
|
|
11895
13175
|
try {
|
|
11896
13176
|
appendEvent(logPath, {
|
|
11897
13177
|
ts: stageAt,
|
|
@@ -11921,6 +13201,11 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
11921
13201
|
let boundConversation;
|
|
11922
13202
|
let bindingError;
|
|
11923
13203
|
try {
|
|
13204
|
+
if (deferredCodexForegroundBinding &&
|
|
13205
|
+
!allowsSyntheticTerminalAcceptance(options) &&
|
|
13206
|
+
codexRolloutAcceptanceAnchor?.version !== 2) {
|
|
13207
|
+
throw new Error("deferred Codex foreground binding requires a virgin rollout acceptance anchor");
|
|
13208
|
+
}
|
|
11924
13209
|
boundIdentity = await pollNativeAgentSessionIdentity({
|
|
11925
13210
|
options,
|
|
11926
13211
|
executor,
|
|
@@ -11928,7 +13213,19 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
11928
13213
|
pid: terminalAgentPid,
|
|
11929
13214
|
expectedSessionId: stringValue(sendTakeover?.terminal_agent_expected_session_id),
|
|
11930
13215
|
allowedPreMaterializationIdentity,
|
|
11931
|
-
allowedAdditionalIdentities
|
|
13216
|
+
allowedAdditionalIdentities,
|
|
13217
|
+
...(deferredCodexForegroundBinding &&
|
|
13218
|
+
codexRolloutAcceptanceAnchor?.version === 2
|
|
13219
|
+
? {
|
|
13220
|
+
requiredCodexAcceptance: {
|
|
13221
|
+
anchor: codexRolloutAcceptanceAnchor,
|
|
13222
|
+
requestHash: terminalRequestHash
|
|
13223
|
+
},
|
|
13224
|
+
attempts: Math.max(1, Math.ceil(DEFAULT_TERMINAL_ACCEPTANCE_TIMEOUT_MS /
|
|
13225
|
+
DEFAULT_TERMINAL_ACCEPTANCE_POLL_INTERVAL_MS)),
|
|
13226
|
+
delayMs: DEFAULT_TERMINAL_ACCEPTANCE_POLL_INTERVAL_MS
|
|
13227
|
+
}
|
|
13228
|
+
: {})
|
|
11932
13229
|
});
|
|
11933
13230
|
}
|
|
11934
13231
|
catch (error) {
|
|
@@ -11955,20 +13252,30 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
11955
13252
|
if (!bindingStoreDir) {
|
|
11956
13253
|
throw new Error("managed Session Store is unavailable before native identity commit");
|
|
11957
13254
|
}
|
|
11958
|
-
|
|
11959
|
-
|
|
11960
|
-
|
|
11961
|
-
|
|
11962
|
-
|
|
11963
|
-
|
|
11964
|
-
|
|
11965
|
-
|
|
11966
|
-
|
|
11967
|
-
|
|
11968
|
-
|
|
11969
|
-
|
|
11970
|
-
|
|
11971
|
-
|
|
13255
|
+
if (deferredCodexForegroundBinding) {
|
|
13256
|
+
await commitDeferredCodexForegroundBinding({
|
|
13257
|
+
options,
|
|
13258
|
+
boundary: deferredCodexForegroundBinding,
|
|
13259
|
+
identity: boundIdentity,
|
|
13260
|
+
acceptedAt: cliNow().toISOString()
|
|
13261
|
+
});
|
|
13262
|
+
}
|
|
13263
|
+
else {
|
|
13264
|
+
await assertNativeThreadHasExclusiveOwnership({
|
|
13265
|
+
options,
|
|
13266
|
+
agent: executor.kind,
|
|
13267
|
+
currentPid: terminalAgentPid,
|
|
13268
|
+
nativeThreadId: boundIdentity.sessionId,
|
|
13269
|
+
storeDir: bindingStoreDir,
|
|
13270
|
+
terminalControl,
|
|
13271
|
+
excludedManagedSessionId: sessionIdForConversation(boundConversation)
|
|
13272
|
+
});
|
|
13273
|
+
persistManagedSessionNativeIdentity({
|
|
13274
|
+
conversation: boundConversation,
|
|
13275
|
+
terminalControl,
|
|
13276
|
+
identity: boundIdentity
|
|
13277
|
+
});
|
|
13278
|
+
}
|
|
11972
13279
|
if (codexRolloutAcceptanceAnchor?.version === 2 &&
|
|
11973
13280
|
cliEnv().AKK_TEST_EXIT_AFTER_VIRGIN_SESSION_BINDING === "1") {
|
|
11974
13281
|
cliExit(86);
|
|
@@ -11991,11 +13298,69 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
11991
13298
|
boundConversation = undefined;
|
|
11992
13299
|
}
|
|
11993
13300
|
}
|
|
13301
|
+
if (!boundIdentity && deferredCodexForegroundBinding) {
|
|
13302
|
+
try {
|
|
13303
|
+
const transfer = loadDeferredForegroundTransfer(storeDirFromOptions(options), deferredCodexForegroundBinding.transferId);
|
|
13304
|
+
if (["committed", "resolved"].includes(transfer.status)) {
|
|
13305
|
+
const recoveredTarget = await resolveCommittedDeferredCodexForegroundTransfer({
|
|
13306
|
+
options,
|
|
13307
|
+
boundary: deferredCodexForegroundBinding
|
|
13308
|
+
});
|
|
13309
|
+
const binding = recoveredTarget.binding;
|
|
13310
|
+
if (!binding?.native_thread_id ||
|
|
13311
|
+
!binding.native_process.process_uuid ||
|
|
13312
|
+
!binding.native_process.process_birth ||
|
|
13313
|
+
!isCompleteNativeRollout(binding.native_process.rollout)) {
|
|
13314
|
+
throw new Error("resolved deferred target lacks exact native identity");
|
|
13315
|
+
}
|
|
13316
|
+
const recoveredIdentity = {
|
|
13317
|
+
sessionId: binding.native_thread_id,
|
|
13318
|
+
processUuid: binding.native_process.process_uuid,
|
|
13319
|
+
processBirth: binding.native_process.process_birth,
|
|
13320
|
+
rollout: binding.native_process.rollout,
|
|
13321
|
+
evidence: binding.native_process.evidence
|
|
13322
|
+
};
|
|
13323
|
+
const recoveredConversation = withNativeAgentSessionIdentity(stagedConversation, recoveredIdentity);
|
|
13324
|
+
// A committed transfer proves Session ownership, but it cannot
|
|
13325
|
+
// silently substitute for the Turn's exact staged binding. Only
|
|
13326
|
+
// publish the recovered identity to the normal acceptance path
|
|
13327
|
+
// after the same Turn-vs-Session/native assertion used by the
|
|
13328
|
+
// primary commit path succeeds.
|
|
13329
|
+
assertNativeAgentIdentityForTurn({
|
|
13330
|
+
conversation: recoveredConversation,
|
|
13331
|
+
currentIdentity: recoveredIdentity,
|
|
13332
|
+
operation: "recover deferred foreground binding for"
|
|
13333
|
+
});
|
|
13334
|
+
boundIdentity = recoveredIdentity;
|
|
13335
|
+
boundConversation = recoveredConversation;
|
|
13336
|
+
}
|
|
13337
|
+
}
|
|
13338
|
+
catch (error) {
|
|
13339
|
+
bindingError = error instanceof Error ? error.message : String(error);
|
|
13340
|
+
boundIdentity = undefined;
|
|
13341
|
+
boundConversation = undefined;
|
|
13342
|
+
}
|
|
13343
|
+
}
|
|
11994
13344
|
if (!boundIdentity) {
|
|
11995
13345
|
const bindingFailedAt = cliNow().toISOString();
|
|
11996
13346
|
const bindingReason = bindingError
|
|
11997
13347
|
? `AKK dispatched terminal input but could not verify the new native agent session: ${bindingError}`
|
|
11998
13348
|
: "AKK dispatched terminal input but no exact native agent session appeared within the binding window";
|
|
13349
|
+
if (deferredCodexForegroundBinding) {
|
|
13350
|
+
try {
|
|
13351
|
+
markDeferredCodexForegroundTransferUncertain({
|
|
13352
|
+
options,
|
|
13353
|
+
boundary: deferredCodexForegroundBinding,
|
|
13354
|
+
reason: bindingReason
|
|
13355
|
+
});
|
|
13356
|
+
}
|
|
13357
|
+
catch (error) {
|
|
13358
|
+
runtimeLog("error", "deferred_codex_foreground_uncertain_persist_failed", {
|
|
13359
|
+
transfer_id: deferredCodexForegroundBinding.transferId,
|
|
13360
|
+
error: error instanceof Error ? error.message : String(error)
|
|
13361
|
+
});
|
|
13362
|
+
}
|
|
13363
|
+
}
|
|
11999
13364
|
const unfencedBase = {
|
|
12000
13365
|
...stagedConversation,
|
|
12001
13366
|
status: "stalled",
|
|
@@ -12024,10 +13389,12 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
12024
13389
|
error: bindingReason,
|
|
12025
13390
|
lastProvenStage: "enter_dispatched"
|
|
12026
13391
|
});
|
|
12027
|
-
|
|
12028
|
-
|
|
12029
|
-
|
|
12030
|
-
|
|
13392
|
+
if (!deferredCodexForegroundBinding) {
|
|
13393
|
+
quarantineManagedSessionBinding({
|
|
13394
|
+
conversation: unfencedConversation,
|
|
13395
|
+
reason: bindingReason
|
|
13396
|
+
});
|
|
13397
|
+
}
|
|
12031
13398
|
saveTerminalBridgeDispatchLedger(terminalControl, {
|
|
12032
13399
|
...terminalBindingLedgerFields(unfencedConversation),
|
|
12033
13400
|
status: "uncertain",
|
|
@@ -12249,11 +13616,13 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
12249
13616
|
const errorMessage = error.message;
|
|
12250
13617
|
let dispatchLedgerRestored = true;
|
|
12251
13618
|
try {
|
|
12252
|
-
|
|
12253
|
-
|
|
12254
|
-
|
|
12255
|
-
|
|
12256
|
-
|
|
13619
|
+
if (!abortDeferredPreInputBeforeTransport(abortedAt)) {
|
|
13620
|
+
restoreTerminalBridgeDispatchLedger({
|
|
13621
|
+
terminalControl,
|
|
13622
|
+
previousLedger: previousDispatchLedger,
|
|
13623
|
+
reason: "terminal transport was proved not to have started"
|
|
13624
|
+
});
|
|
13625
|
+
}
|
|
12257
13626
|
}
|
|
12258
13627
|
catch (ledgerError) {
|
|
12259
13628
|
dispatchLedgerRestored = false;
|
|
@@ -12388,6 +13757,23 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
12388
13757
|
}
|
|
12389
13758
|
const uncertainAt = cliNow().toISOString();
|
|
12390
13759
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
13760
|
+
if (deferredCodexForegroundBinding) {
|
|
13761
|
+
try {
|
|
13762
|
+
markDeferredCodexForegroundTransferUncertain({
|
|
13763
|
+
options,
|
|
13764
|
+
boundary: deferredCodexForegroundBinding,
|
|
13765
|
+
reason: errorMessage
|
|
13766
|
+
});
|
|
13767
|
+
}
|
|
13768
|
+
catch (transferError) {
|
|
13769
|
+
runtimeLog("error", "deferred_codex_foreground_uncertain_persist_failed", {
|
|
13770
|
+
transfer_id: deferredCodexForegroundBinding.transferId,
|
|
13771
|
+
error: transferError instanceof Error
|
|
13772
|
+
? transferError.message
|
|
13773
|
+
: String(transferError)
|
|
13774
|
+
});
|
|
13775
|
+
}
|
|
13776
|
+
}
|
|
12391
13777
|
const failureBase = stagedConversation;
|
|
12392
13778
|
const stalledFailureBase = {
|
|
12393
13779
|
...failureBase,
|
|
@@ -13556,7 +14942,7 @@ function assertTurnBindingCurrent(conversation, operation) {
|
|
|
13556
14942
|
// the Store writer lease. This makes the freshly materialized Session
|
|
13557
14943
|
// authoritative even for the first callback/control after an upgrade.
|
|
13558
14944
|
const manifest = ensureStoreWritable(storeDir);
|
|
13559
|
-
if (manifest.writer_protocol
|
|
14945
|
+
if (manifest.writer_protocol < STORE_SESSION_AUTHORITY_PROTOCOL) {
|
|
13560
14946
|
throw new Error(`cannot ${operation} Turn ${turnIdForConversation(conversation)}: ` +
|
|
13561
14947
|
"its Store has no protocol-3 Session authority");
|
|
13562
14948
|
}
|
|
@@ -13799,7 +15185,7 @@ function quarantineManagedSessionBinding({ conversation, reason }) {
|
|
|
13799
15185
|
expectedRevision: managedSessionRevision(current)
|
|
13800
15186
|
});
|
|
13801
15187
|
}
|
|
13802
|
-
async function pollNativeAgentSessionIdentity({ options, executor, terminalControl, pid, expectedSessionId, allowedPreMaterializationIdentity, allowedAdditionalIdentities = [], attempts = 40, delayMs = 50 }) {
|
|
15188
|
+
async function pollNativeAgentSessionIdentity({ options, executor, terminalControl, pid, expectedSessionId, allowedPreMaterializationIdentity, allowedAdditionalIdentities = [], requiredCodexAcceptance, attempts = 40, delayMs = 50 }) {
|
|
13803
15189
|
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
13804
15190
|
const identity = await resolveCurrentNativeAgentSessionIdentity({
|
|
13805
15191
|
options,
|
|
@@ -13814,6 +15200,19 @@ async function pollNativeAgentSessionIdentity({ options, executor, terminalContr
|
|
|
13814
15200
|
});
|
|
13815
15201
|
if (identity) {
|
|
13816
15202
|
if (!expectedSessionId || identity.sessionId === expectedSessionId) {
|
|
15203
|
+
if (requiredCodexAcceptance) {
|
|
15204
|
+
const acceptance = detectCodexRolloutAcceptance({
|
|
15205
|
+
anchor: requiredCodexAcceptance.anchor,
|
|
15206
|
+
currentIdentity: identity,
|
|
15207
|
+
requestHash: requiredCodexAcceptance.requestHash
|
|
15208
|
+
});
|
|
15209
|
+
if (!acceptance) {
|
|
15210
|
+
if (attempt + 1 < attempts) {
|
|
15211
|
+
await cliSleep(delayMs);
|
|
15212
|
+
}
|
|
15213
|
+
continue;
|
|
15214
|
+
}
|
|
15215
|
+
}
|
|
13817
15216
|
return identity;
|
|
13818
15217
|
}
|
|
13819
15218
|
if (nativeIdentityMatchesCodexPreMaterialization(identity, allowedPreMaterializationIdentity)) {
|
|
@@ -13900,7 +15299,7 @@ function managedTurnMatchesResolvedTerminal(conversation, terminalConversation,
|
|
|
13900
15299
|
terminalControlsShareIncarnation(storedControl, terminalConversation.terminalControl) &&
|
|
13901
15300
|
matchesConfiguredWorkspace(conversation.workspace, terminalConversation.terminalControl.currentPath));
|
|
13902
15301
|
}
|
|
13903
|
-
function createManagedTerminalTurn({ options, conversationId, agent, pid, messageBody, terminalControl, previousTurn, managedSession, nativeAgentIdentity }) {
|
|
15302
|
+
function createManagedTerminalTurn({ options, conversationId, agent, pid, messageBody, terminalControl, previousTurn, managedSession, nativeAgentIdentity, deferredForegroundTransferId = undefined }) {
|
|
13904
15303
|
const workspace = terminalControl.currentPath ?? cliCwd();
|
|
13905
15304
|
const storeDir = expandHome(options.storeDir ?? options.logDir ?? defaultStoreDir(workspace));
|
|
13906
15305
|
const executor = previousTurn
|
|
@@ -13968,7 +15367,10 @@ function createManagedTerminalTurn({ options, conversationId, agent, pid, messag
|
|
|
13968
15367
|
: "raw_terminal_send",
|
|
13969
15368
|
...terminalEndpointTakeoverFields(terminalControl),
|
|
13970
15369
|
needs_bootstrap: false,
|
|
13971
|
-
terminal_bridge: true
|
|
15370
|
+
terminal_bridge: true,
|
|
15371
|
+
...(deferredForegroundTransferId
|
|
15372
|
+
? { deferred_foreground_transfer_id: deferredForegroundTransferId }
|
|
15373
|
+
: {})
|
|
13972
15374
|
}
|
|
13973
15375
|
}, paths);
|
|
13974
15376
|
const message = createMessage({
|
|
@@ -14807,6 +16209,7 @@ async function runTerminalControlCancel({ options, statePath, logPath, agent, te
|
|
|
14807
16209
|
throw new Error("terminal control changed while waiting to cancel; refresh status and retry");
|
|
14808
16210
|
}
|
|
14809
16211
|
assertManagedTerminalDispatchOwner({
|
|
16212
|
+
storeDir: writerStoreDir,
|
|
14810
16213
|
conversation: currentConversation,
|
|
14811
16214
|
terminalControl: currentControl,
|
|
14812
16215
|
action: "cancel"
|
|
@@ -15086,6 +16489,11 @@ async function runClose(options) {
|
|
|
15086
16489
|
const releaseStateLock = acquireFileLock(`${statePath}.lock`);
|
|
15087
16490
|
try {
|
|
15088
16491
|
const conversation = loadState(statePath);
|
|
16492
|
+
assertConversationHasNoNonterminalDeferredForegroundTransfer({
|
|
16493
|
+
storeDir: closeStoreDir,
|
|
16494
|
+
conversation,
|
|
16495
|
+
action: "close"
|
|
16496
|
+
});
|
|
15089
16497
|
const currentTakeover = isRecord(conversation.native_session_takeover)
|
|
15090
16498
|
? conversation.native_session_takeover
|
|
15091
16499
|
: undefined;
|
|
@@ -15253,6 +16661,30 @@ async function runTerminalDispatchClose({ options, terminalConversation }) {
|
|
|
15253
16661
|
if (!ledger || ledger.status === "resolved") {
|
|
15254
16662
|
throw new Error(`terminal ${terminalControl.target} has no unresolved AKK dispatch fence`);
|
|
15255
16663
|
}
|
|
16664
|
+
const deferredTransferId = stringValue(ledger.deferred_foreground_transfer_id);
|
|
16665
|
+
if (deferredTransferId) {
|
|
16666
|
+
const ledgerStatePath = path.resolve(required(stringValue(ledger.state_path), "deferred terminal dispatch state path is unavailable"));
|
|
16667
|
+
const ledgerStoreDir = path.resolve(required(stringValue(ledger.store_dir), "deferred terminal dispatch Store is unavailable"));
|
|
16668
|
+
const canonicalLedgerStoreDir = path.resolve(pathsForConversationDir(path.dirname(ledgerStatePath)).storeDir);
|
|
16669
|
+
if (ledgerStoreDir !== path.resolve(storeDir) ||
|
|
16670
|
+
canonicalLedgerStoreDir !== ledgerStoreDir) {
|
|
16671
|
+
throw new Error(`terminal ${terminalControl.target} deferred dispatch belongs to ` +
|
|
16672
|
+
"another or noncanonical Store; generic terminal close cannot resolve it");
|
|
16673
|
+
}
|
|
16674
|
+
const transfer = loadDeferredForegroundTransfer(storeDir, deferredTransferId);
|
|
16675
|
+
if (transfer.state_path === undefined ||
|
|
16676
|
+
path.resolve(transfer.state_path) !== ledgerStatePath ||
|
|
16677
|
+
transfer.terminal_id !== terminalConversation.conversationId ||
|
|
16678
|
+
!terminalControlEvidenceMatches(transfer.terminal_endpoint, terminalControl)) {
|
|
16679
|
+
throw new Error(`terminal ${terminalControl.target} deferred dispatch authority ` +
|
|
16680
|
+
"does not match its exact Store/terminal transfer");
|
|
16681
|
+
}
|
|
16682
|
+
if (!["resolved", "aborted", "abort_resolved"].includes(transfer.status)) {
|
|
16683
|
+
throw new Error(`terminal ${terminalControl.target} dispatch is fenced by deferred ` +
|
|
16684
|
+
`foreground transfer ${transfer.transfer_id} (${transfer.status}); ` +
|
|
16685
|
+
"generic terminal close cannot resolve it");
|
|
16686
|
+
}
|
|
16687
|
+
}
|
|
15256
16688
|
if (![
|
|
15257
16689
|
"prepared",
|
|
15258
16690
|
"text_injected",
|
|
@@ -17611,7 +19043,12 @@ function loadTerminalDispatchLedgerOwner(ledger) {
|
|
|
17611
19043
|
return undefined;
|
|
17612
19044
|
}
|
|
17613
19045
|
}
|
|
17614
|
-
function assertManagedTerminalDispatchOwner({ conversation, terminalControl, action }) {
|
|
19046
|
+
function assertManagedTerminalDispatchOwner({ storeDir, conversation, terminalControl, action }) {
|
|
19047
|
+
assertConversationHasNoNonterminalDeferredForegroundTransfer({
|
|
19048
|
+
storeDir,
|
|
19049
|
+
conversation,
|
|
19050
|
+
action
|
|
19051
|
+
});
|
|
17615
19052
|
assertTurnBindingCurrent(conversation, action);
|
|
17616
19053
|
const nativeTakeover = isRecord(conversation.native_session_takeover)
|
|
17617
19054
|
? conversation.native_session_takeover
|
|
@@ -17649,6 +19086,7 @@ function terminalBindingLedgerFields(conversation) {
|
|
|
17649
19086
|
const submission = terminalBridgeSubmission(conversation);
|
|
17650
19087
|
const messageType = stringValue(submission?.message_type);
|
|
17651
19088
|
const messageBodyHash = stringValue(submission?.message_body_hash);
|
|
19089
|
+
const deferredForegroundTransferId = stringValue(takeover?.deferred_foreground_transfer_id);
|
|
17652
19090
|
return {
|
|
17653
19091
|
...(bindingId ? { binding_id: bindingId } : {}),
|
|
17654
19092
|
...(Number.isSafeInteger(generation)
|
|
@@ -17658,12 +19096,30 @@ function terminalBindingLedgerFields(conversation) {
|
|
|
17658
19096
|
...(storeDir ? { store_dir: path.resolve(storeDir) } : {}),
|
|
17659
19097
|
...(messageType ? { message_type: messageType } : {}),
|
|
17660
19098
|
...(messageBodyHash ? { message_body_hash: messageBodyHash } : {}),
|
|
19099
|
+
...(deferredForegroundTransferId
|
|
19100
|
+
? { deferred_foreground_transfer_id: deferredForegroundTransferId }
|
|
19101
|
+
: {}),
|
|
17661
19102
|
executor_kind: executorForConversation(conversation).kind,
|
|
17662
19103
|
...(conversation.openclaw_session
|
|
17663
19104
|
? { openclaw_session: conversation.openclaw_session }
|
|
17664
19105
|
: {})
|
|
17665
19106
|
};
|
|
17666
19107
|
}
|
|
19108
|
+
function assertConversationHasNoNonterminalDeferredForegroundTransfer({ storeDir, conversation, action }) {
|
|
19109
|
+
const takeover = isRecord(conversation.native_session_takeover)
|
|
19110
|
+
? conversation.native_session_takeover
|
|
19111
|
+
: undefined;
|
|
19112
|
+
const transferId = stringValue(takeover?.deferred_foreground_transfer_id);
|
|
19113
|
+
if (!transferId) {
|
|
19114
|
+
return;
|
|
19115
|
+
}
|
|
19116
|
+
const transfer = loadDeferredForegroundTransfer(storeDir, transferId);
|
|
19117
|
+
if (!["resolved", "aborted", "abort_resolved"].includes(transfer.status)) {
|
|
19118
|
+
throw new Error(`cannot ${action} Turn ${turnIdForConversation(conversation)} while ` +
|
|
19119
|
+
`deferred foreground transfer ${transfer.transfer_id} is ` +
|
|
19120
|
+
`${transfer.status}; dedicated transfer recovery must finish first`);
|
|
19121
|
+
}
|
|
19122
|
+
}
|
|
17667
19123
|
function reconcilePreparedTerminalDispatchLedger(terminalControl, ledger) {
|
|
17668
19124
|
if (terminalDispatchLedgerLooksLifecycle(ledger)) {
|
|
17669
19125
|
return ledger;
|
|
@@ -18011,7 +19467,986 @@ function terminalSubmissionProofRank(status, lastProven) {
|
|
|
18011
19467
|
}
|
|
18012
19468
|
return 0;
|
|
18013
19469
|
}
|
|
19470
|
+
function deferredCodexBoundaryFromTransfer({ terminal, transfer }) {
|
|
19471
|
+
const liveIncarnation = codexProcessIncarnationForPid(terminal.pid);
|
|
19472
|
+
if (terminal.agent !== "codex" ||
|
|
19473
|
+
transfer.terminal_id !== terminal.conversationId ||
|
|
19474
|
+
transfer.process_pid !== terminal.pid ||
|
|
19475
|
+
transfer.process_uuid !== liveIncarnation.processUuid ||
|
|
19476
|
+
transfer.process_birth !== liveIncarnation.processBirth ||
|
|
19477
|
+
path.resolve(transfer.workspace) !== path.resolve(terminal.terminalControl.currentPath ?? "") ||
|
|
19478
|
+
!terminalControlEvidenceMatches(transfer.terminal_endpoint, terminal.terminalControl)) {
|
|
19479
|
+
throw new Error(`deferred foreground transfer ${transfer.transfer_id} no longer ` +
|
|
19480
|
+
"matches the exact terminal incarnation");
|
|
19481
|
+
}
|
|
19482
|
+
return {
|
|
19483
|
+
terminal,
|
|
19484
|
+
transferId: transfer.transfer_id,
|
|
19485
|
+
targetSessionId: transfer.target_session_id,
|
|
19486
|
+
sourceSessionId: transfer.source_session_id,
|
|
19487
|
+
sourceBoundRevision: transfer.source_expected_revision,
|
|
19488
|
+
sourceBoundBindingToken: transfer.source_binding_token,
|
|
19489
|
+
processUuid: transfer.process_uuid,
|
|
19490
|
+
processBirth: transfer.process_birth,
|
|
19491
|
+
previousDispatchSnapshot: {
|
|
19492
|
+
status: transfer.previous_dispatch_status,
|
|
19493
|
+
fingerprint: transfer.previous_dispatch_fingerprint
|
|
19494
|
+
},
|
|
19495
|
+
sourceReservedRevision: transfer.source_expected_revision + 1,
|
|
19496
|
+
sourceReservedBindingToken: managedSessionBindingToken({
|
|
19497
|
+
session_id: transfer.source_session_id,
|
|
19498
|
+
status: "transitioning",
|
|
19499
|
+
binding: transfer.source_before_binding
|
|
19500
|
+
}),
|
|
19501
|
+
targetPreparedRevision: transfer.target_prepared_revision,
|
|
19502
|
+
targetPreparedBindingToken: transfer.target_prepared_binding_token
|
|
19503
|
+
};
|
|
19504
|
+
}
|
|
19505
|
+
function loadDeferredForegroundTurnAuthority({ storeDir, terminal, transfer }) {
|
|
19506
|
+
const statePath = path.resolve(required(transfer.state_path, "deferred foreground Turn state path is unavailable"));
|
|
19507
|
+
const canonical = pathsForConversationDir(path.dirname(statePath));
|
|
19508
|
+
if (path.resolve(canonical.storeDir) !== path.resolve(storeDir) ||
|
|
19509
|
+
path.resolve(canonical.statePath) !== statePath) {
|
|
19510
|
+
throw new Error(`deferred foreground transfer ${transfer.transfer_id} has a noncanonical ` +
|
|
19511
|
+
"Turn state path");
|
|
19512
|
+
}
|
|
19513
|
+
const conversation = loadState(statePath);
|
|
19514
|
+
const takeover = isRecord(conversation.native_session_takeover)
|
|
19515
|
+
? conversation.native_session_takeover
|
|
19516
|
+
: undefined;
|
|
19517
|
+
const submission = terminalBridgeSubmission(conversation);
|
|
19518
|
+
const terminalControl = terminalControlFromTakeover(takeover);
|
|
19519
|
+
const anchor = takeover?.codex_rollout_acceptance_anchor;
|
|
19520
|
+
const target = loadManagedSession(storeDir, transfer.target_session_id);
|
|
19521
|
+
const targetBefore = transfer.target_before_binding;
|
|
19522
|
+
const requestText = stringValue(takeover?.terminal_bridge_request_text);
|
|
19523
|
+
const messageBodyHash = stringValue(submission?.message_body_hash);
|
|
19524
|
+
const executorKind = executorForConversation(conversation).kind;
|
|
19525
|
+
const turnNativeThreadId = stringValue(conversation.native_thread_id);
|
|
19526
|
+
const takeoverNativeThreadId = stringValue(takeover?.terminal_agent_session_id);
|
|
19527
|
+
const submissionNativeThreadId = stringValue(submission?.native_thread_id);
|
|
19528
|
+
const turnRollout = takeover?.terminal_agent_rollout;
|
|
19529
|
+
const committedTargetAuthority = Boolean(["committed", "resolved"].includes(transfer.status) &&
|
|
19530
|
+
transfer.target_accepted_binding &&
|
|
19531
|
+
((target.status === "transitioning" &&
|
|
19532
|
+
target.last_transition_id === transfer.transfer_id &&
|
|
19533
|
+
managedSessionRevision(target) === transfer.target_accepted_revision &&
|
|
19534
|
+
managedSessionBindingToken(target) ===
|
|
19535
|
+
transfer.target_accepted_binding_token) ||
|
|
19536
|
+
(target.status === "bound" &&
|
|
19537
|
+
!target.last_transition_id &&
|
|
19538
|
+
managedSessionRevision(target) ===
|
|
19539
|
+
Number(transfer.target_accepted_revision) + 1 &&
|
|
19540
|
+
managedSessionBindingToken(target) === managedSessionBindingToken({
|
|
19541
|
+
session_id: transfer.target_session_id,
|
|
19542
|
+
status: "bound",
|
|
19543
|
+
binding: transfer.target_accepted_binding
|
|
19544
|
+
}))) &&
|
|
19545
|
+
JSON.stringify(target.binding) ===
|
|
19546
|
+
JSON.stringify(transfer.target_accepted_binding));
|
|
19547
|
+
const precommitTargetAuthority = Boolean(!["committed", "resolved"].includes(transfer.status) &&
|
|
19548
|
+
target.status === "transitioning" &&
|
|
19549
|
+
target.last_transition_id === transfer.transfer_id);
|
|
19550
|
+
const prebindingTurn = turnNativeThreadId === undefined &&
|
|
19551
|
+
takeoverNativeThreadId === undefined &&
|
|
19552
|
+
submissionNativeThreadId === undefined &&
|
|
19553
|
+
turnRollout === undefined;
|
|
19554
|
+
let acceptedTurn = false;
|
|
19555
|
+
if (turnNativeThreadId &&
|
|
19556
|
+
takeoverNativeThreadId === turnNativeThreadId &&
|
|
19557
|
+
submissionNativeThreadId === turnNativeThreadId &&
|
|
19558
|
+
isCompleteNativeRollout(turnRollout) &&
|
|
19559
|
+
String(submission?.status) === "agent_accepted") {
|
|
19560
|
+
try {
|
|
19561
|
+
validateTerminalSubmissionAcceptanceEvidence(submission?.acceptance_evidence, {
|
|
19562
|
+
source: "codex_rollout",
|
|
19563
|
+
nativeThreadId: turnNativeThreadId,
|
|
19564
|
+
requestHash: transfer.request_hash
|
|
19565
|
+
});
|
|
19566
|
+
acceptedTurn = true;
|
|
19567
|
+
}
|
|
19568
|
+
catch {
|
|
19569
|
+
acceptedTurn = false;
|
|
19570
|
+
}
|
|
19571
|
+
}
|
|
19572
|
+
if (!takeover ||
|
|
19573
|
+
!submission ||
|
|
19574
|
+
!terminalControl ||
|
|
19575
|
+
!targetBefore ||
|
|
19576
|
+
!terminalControlsShareIncarnation(terminalControl, terminal.terminalControl) ||
|
|
19577
|
+
!terminalControlEvidenceMatches(takeover.terminal_endpoint ?? terminalControl, terminal.terminalControl) ||
|
|
19578
|
+
executorKind !== "codex" ||
|
|
19579
|
+
conversation.conversation_id !== transfer.turn_id ||
|
|
19580
|
+
sessionIdForConversation(conversation) !== transfer.target_session_id ||
|
|
19581
|
+
turnIdForConversation(conversation) !== transfer.turn_id ||
|
|
19582
|
+
path.resolve(required(stringValue(conversation.state_path), "deferred foreground Turn state path is missing")) !== statePath ||
|
|
19583
|
+
path.resolve(pathsForConversation(conversation.conversation_id, storeDir).statePath) !== statePath ||
|
|
19584
|
+
path.resolve(required(managedSessionStoreDirForConversation(conversation), "deferred foreground Turn Store is missing")) !== path.resolve(storeDir) ||
|
|
19585
|
+
stringValue(takeover.deferred_foreground_transfer_id) !==
|
|
19586
|
+
transfer.transfer_id ||
|
|
19587
|
+
stringValue(takeover.terminal_bridge_message_id) !== transfer.message_id ||
|
|
19588
|
+
stringValue(submission.session_id) !== transfer.target_session_id ||
|
|
19589
|
+
stringValue(submission.turn_id) !== transfer.turn_id ||
|
|
19590
|
+
stringValue(submission.message_id) !== transfer.message_id ||
|
|
19591
|
+
stringValue(submission.message_type) !== "task" ||
|
|
19592
|
+
stringValue(submission.executor_kind) !== "codex" ||
|
|
19593
|
+
!messageBodyHash ||
|
|
19594
|
+
!/^[0-9a-f]{64}$/u.test(messageBodyHash) ||
|
|
19595
|
+
createHash("sha256").update(String(conversation.user_request)).digest("hex") !==
|
|
19596
|
+
messageBodyHash ||
|
|
19597
|
+
terminalBridgeRequestFingerprint(terminalSubmissionPayload(String(conversation.user_request))) !== transfer.request_hash ||
|
|
19598
|
+
stringValue(takeover.terminal_bridge_request_hash) !==
|
|
19599
|
+
transfer.request_hash ||
|
|
19600
|
+
stringValue(submission.request_hash) !== transfer.request_hash ||
|
|
19601
|
+
!requestText ||
|
|
19602
|
+
terminalBridgeRequestFingerprint(requestText) !== transfer.request_hash ||
|
|
19603
|
+
path.resolve(required(stringValue(submission.store_dir), "deferred foreground submission Store is missing")) !== path.resolve(storeDir) ||
|
|
19604
|
+
stringValue(conversation.terminal_binding_id) !== targetBefore.binding_id ||
|
|
19605
|
+
Number(conversation.terminal_binding_generation) !==
|
|
19606
|
+
targetBefore.generation ||
|
|
19607
|
+
stringValue(takeover.terminal_binding_id) !== targetBefore.binding_id ||
|
|
19608
|
+
Number(takeover.terminal_binding_generation) !== targetBefore.generation ||
|
|
19609
|
+
stringValue(submission.binding_id) !== targetBefore.binding_id ||
|
|
19610
|
+
Number(submission.binding_generation) !== targetBefore.generation ||
|
|
19611
|
+
(!prebindingTurn && !acceptedTurn) ||
|
|
19612
|
+
stringValue(takeover.terminal_agent_expected_session_id) !== undefined ||
|
|
19613
|
+
Number(takeover.terminal_agent_pid) !== transfer.process_pid ||
|
|
19614
|
+
stringValue(takeover.terminal_agent_process_uuid) !==
|
|
19615
|
+
transfer.process_uuid ||
|
|
19616
|
+
stringValue(takeover.terminal_agent_process_birth) !==
|
|
19617
|
+
transfer.process_birth ||
|
|
19618
|
+
target.session_id !== transfer.target_session_id ||
|
|
19619
|
+
(!precommitTargetAuthority && !committedTargetAuthority) ||
|
|
19620
|
+
target.lineage.transition_id !== transfer.transfer_id ||
|
|
19621
|
+
target.lineage.previous_session_id !== transfer.source_session_id ||
|
|
19622
|
+
!target.binding ||
|
|
19623
|
+
target.binding.binding_id !== targetBefore.binding_id ||
|
|
19624
|
+
target.binding.generation !== targetBefore.generation ||
|
|
19625
|
+
target.binding.terminal_id !== targetBefore.terminal_id ||
|
|
19626
|
+
!terminalControlsShareIncarnation(target.binding.terminal_control, targetBefore.terminal_control) ||
|
|
19627
|
+
target.binding.native_process.pid !== transfer.process_pid ||
|
|
19628
|
+
target.binding.native_process.process_uuid !== transfer.process_uuid ||
|
|
19629
|
+
target.binding.native_process.process_birth !== transfer.process_birth ||
|
|
19630
|
+
!isRecord(anchor) ||
|
|
19631
|
+
anchor.schema !== "agent-knock-knock/codex-rollout-acceptance-anchor" ||
|
|
19632
|
+
anchor.version !== 2 ||
|
|
19633
|
+
anchor.mode !== "pre_materialization" ||
|
|
19634
|
+
anchor.native_thread_binding !== "post_submission" ||
|
|
19635
|
+
anchor.file_existed !== false ||
|
|
19636
|
+
anchor.offset_bytes !== 0 ||
|
|
19637
|
+
anchor.expected_empty_native_session !== true ||
|
|
19638
|
+
anchor.native_thread_id !== undefined ||
|
|
19639
|
+
anchor.rollout !== undefined ||
|
|
19640
|
+
anchor.process_uuid !== transfer.process_uuid ||
|
|
19641
|
+
anchor.process_birth !== transfer.process_birth ||
|
|
19642
|
+
typeof anchor.anchor_fingerprint !== "string" ||
|
|
19643
|
+
!/^[0-9a-f]{64}$/u.test(anchor.anchor_fingerprint)) {
|
|
19644
|
+
throw new Error(`deferred foreground transfer ${transfer.transfer_id} Turn authority ` +
|
|
19645
|
+
"does not match its exact request/terminal/process fence");
|
|
19646
|
+
}
|
|
19647
|
+
return {
|
|
19648
|
+
conversation,
|
|
19649
|
+
statePath,
|
|
19650
|
+
logPath: canonical.logPath,
|
|
19651
|
+
takeover,
|
|
19652
|
+
submission,
|
|
19653
|
+
anchor: anchor
|
|
19654
|
+
};
|
|
19655
|
+
}
|
|
19656
|
+
function assertDeferredForegroundLedgerAuthority({ storeDir, terminal, transfer, ledger, statePath, expectedMessageBodyHash }) {
|
|
19657
|
+
const targetBefore = transfer.target_before_binding;
|
|
19658
|
+
const ledgerMessageBodyHash = stringValue(ledger.message_body_hash);
|
|
19659
|
+
let exactCommittedAcceptance = false;
|
|
19660
|
+
if (["committed", "resolved"].includes(transfer.status) &&
|
|
19661
|
+
ledger.status === "agent_accepted" &&
|
|
19662
|
+
stringValue(ledger.native_thread_id) === transfer.target_native_thread_id &&
|
|
19663
|
+
ledger.dispatcher_pid === null &&
|
|
19664
|
+
stringValue(ledger.agent_accepted_at) === transfer.agent_accepted_at &&
|
|
19665
|
+
transfer.target_native_thread_id) {
|
|
19666
|
+
try {
|
|
19667
|
+
validateTerminalSubmissionAcceptanceEvidence(ledger.acceptance_evidence, {
|
|
19668
|
+
source: "codex_rollout",
|
|
19669
|
+
nativeThreadId: transfer.target_native_thread_id,
|
|
19670
|
+
requestHash: transfer.request_hash
|
|
19671
|
+
});
|
|
19672
|
+
exactCommittedAcceptance = true;
|
|
19673
|
+
}
|
|
19674
|
+
catch {
|
|
19675
|
+
exactCommittedAcceptance = false;
|
|
19676
|
+
}
|
|
19677
|
+
}
|
|
19678
|
+
if (!targetBefore ||
|
|
19679
|
+
terminalDispatchLedgerLooksLifecycle(ledger) ||
|
|
19680
|
+
!terminalDispatchRecordMatchesControl(ledger, terminal.terminalControl) ||
|
|
19681
|
+
stringValue(ledger.deferred_foreground_transfer_id) !==
|
|
19682
|
+
transfer.transfer_id ||
|
|
19683
|
+
stringValue(ledger.generation_id) !== transfer.message_id ||
|
|
19684
|
+
stringValue(ledger.conversation_id) !== transfer.turn_id ||
|
|
19685
|
+
stringValue(ledger.session_id) !== transfer.target_session_id ||
|
|
19686
|
+
stringValue(ledger.turn_id) !== transfer.turn_id ||
|
|
19687
|
+
stringValue(ledger.message_id) !== transfer.message_id ||
|
|
19688
|
+
stringValue(ledger.message_type) !== "task" ||
|
|
19689
|
+
stringValue(ledger.executor_kind) !== "codex" ||
|
|
19690
|
+
!ledgerMessageBodyHash ||
|
|
19691
|
+
!/^[0-9a-f]{64}$/u.test(ledgerMessageBodyHash) ||
|
|
19692
|
+
(expectedMessageBodyHash !== undefined &&
|
|
19693
|
+
ledgerMessageBodyHash !== expectedMessageBodyHash) ||
|
|
19694
|
+
stringValue(ledger.request_hash) !== transfer.request_hash ||
|
|
19695
|
+
stringValue(ledger.binding_id) !== targetBefore.binding_id ||
|
|
19696
|
+
Number(ledger.binding_generation) !== targetBefore.generation ||
|
|
19697
|
+
(exactCommittedAcceptance
|
|
19698
|
+
? stringValue(ledger.native_thread_id) !==
|
|
19699
|
+
transfer.target_native_thread_id
|
|
19700
|
+
: stringValue(ledger.native_thread_id) !== undefined) ||
|
|
19701
|
+
path.resolve(required(ledger.state_path, "dispatch state path is missing")) !==
|
|
19702
|
+
statePath ||
|
|
19703
|
+
path.resolve(required(ledger.store_dir, "dispatch Store is missing")) !==
|
|
19704
|
+
path.resolve(storeDir) ||
|
|
19705
|
+
(ledger.status !== "resolved" &&
|
|
19706
|
+
!exactCommittedAcceptance &&
|
|
19707
|
+
Number(ledger.dispatcher_pid) !== transfer.dispatcher_pid)) {
|
|
19708
|
+
throw new Error(`deferred foreground transfer ${transfer.transfer_id} dispatch ledger ` +
|
|
19709
|
+
"does not match its exact Turn authority");
|
|
19710
|
+
}
|
|
19711
|
+
}
|
|
19712
|
+
function assertDeferredForegroundResolvedZeroInputLedger({ storeDir, terminal, transfer, ledger, statePath }) {
|
|
19713
|
+
assertDeferredForegroundLedgerAuthority({
|
|
19714
|
+
storeDir,
|
|
19715
|
+
terminal,
|
|
19716
|
+
transfer,
|
|
19717
|
+
ledger,
|
|
19718
|
+
statePath
|
|
19719
|
+
});
|
|
19720
|
+
const abortedAt = stringValue(ledger.aborted_at);
|
|
19721
|
+
const resolvedAt = stringValue(ledger.resolved_at);
|
|
19722
|
+
const forbiddenEvidence = [
|
|
19723
|
+
"dispatch_started_at",
|
|
19724
|
+
"text_injected_at",
|
|
19725
|
+
"enter_dispatched_at",
|
|
19726
|
+
"submitted_at",
|
|
19727
|
+
"agent_accepted_at",
|
|
19728
|
+
"not_accepted_at",
|
|
19729
|
+
"uncertain_at",
|
|
19730
|
+
"acceptance_evidence"
|
|
19731
|
+
];
|
|
19732
|
+
if (ledger.status !== "resolved" ||
|
|
19733
|
+
ledger.safe_to_retry !== true ||
|
|
19734
|
+
!abortedAt ||
|
|
19735
|
+
!resolvedAt ||
|
|
19736
|
+
!Number.isFinite(Date.parse(abortedAt)) ||
|
|
19737
|
+
!Number.isFinite(Date.parse(resolvedAt)) ||
|
|
19738
|
+
Date.parse(resolvedAt) < Date.parse(abortedAt) ||
|
|
19739
|
+
forbiddenEvidence.some((field) => ledger[field] !== undefined)) {
|
|
19740
|
+
throw new Error(`deferred foreground transfer ${transfer.transfer_id} resolved ledger ` +
|
|
19741
|
+
"does not prove a zero-input abort");
|
|
19742
|
+
}
|
|
19743
|
+
// Validate the whole history for malformed/duplicate identities, then
|
|
19744
|
+
// require the append-only copy of this exact generation. The top-level
|
|
19745
|
+
// resolved record alone is insufficient because it can later be replaced.
|
|
19746
|
+
terminalLedgerReceiptHistory(ledger);
|
|
19747
|
+
const rawHistory = ledger.terminal_submission_receipts;
|
|
19748
|
+
if (!Array.isArray(rawHistory)) {
|
|
19749
|
+
throw new Error(`deferred foreground transfer ${transfer.transfer_id} resolved ledger ` +
|
|
19750
|
+
"has no append-only abort receipt");
|
|
19751
|
+
}
|
|
19752
|
+
const ownReceipts = rawHistory.filter((receipt) => isRecord(receipt) &&
|
|
19753
|
+
stringValue(receipt.message_id) === transfer.message_id);
|
|
19754
|
+
if (ownReceipts.length !== 1) {
|
|
19755
|
+
throw new Error(`deferred foreground transfer ${transfer.transfer_id} resolved ledger ` +
|
|
19756
|
+
"does not have one exact append-only abort receipt");
|
|
19757
|
+
}
|
|
19758
|
+
const ownReceipt = ownReceipts[0];
|
|
19759
|
+
assertDeferredForegroundLedgerAuthority({
|
|
19760
|
+
storeDir,
|
|
19761
|
+
terminal,
|
|
19762
|
+
transfer,
|
|
19763
|
+
// A receipt uses `aborted`, while the same exact owner fields at the
|
|
19764
|
+
// top-level use `resolved`. Validate its authority in that same domain.
|
|
19765
|
+
ledger: { ...ownReceipt, status: "resolved" },
|
|
19766
|
+
statePath
|
|
19767
|
+
});
|
|
19768
|
+
if (ownReceipt.status !== "aborted" ||
|
|
19769
|
+
ownReceipt.safe_to_retry !== true ||
|
|
19770
|
+
stringValue(ownReceipt.aborted_at) !== abortedAt ||
|
|
19771
|
+
stringValue(ownReceipt.resolved_at) !== resolvedAt ||
|
|
19772
|
+
forbiddenEvidence.some((field) => ownReceipt[field] !== undefined)) {
|
|
19773
|
+
throw new Error(`deferred foreground transfer ${transfer.transfer_id} append-only ` +
|
|
19774
|
+
"receipt does not prove a zero-input abort");
|
|
19775
|
+
}
|
|
19776
|
+
}
|
|
19777
|
+
function deferredCodexDurableInputNotStartedAt(transfer) {
|
|
19778
|
+
if (transfer.status !== "dispatch_started" ||
|
|
19779
|
+
transfer.input_stage !== "dispatch_started" ||
|
|
19780
|
+
!transfer.state_path) {
|
|
19781
|
+
return undefined;
|
|
19782
|
+
}
|
|
19783
|
+
const conversation = loadState(path.resolve(transfer.state_path));
|
|
19784
|
+
const submission = terminalBridgeSubmission(conversation);
|
|
19785
|
+
const abortedAt = stringValue(submission?.aborted_at);
|
|
19786
|
+
if (submission?.status !== "aborted" ||
|
|
19787
|
+
submission.safe_to_retry !== true ||
|
|
19788
|
+
!abortedAt ||
|
|
19789
|
+
!Number.isFinite(Date.parse(abortedAt)) ||
|
|
19790
|
+
stringValue(submission.text_injected_at) !== undefined ||
|
|
19791
|
+
stringValue(submission.enter_dispatched_at) !== undefined ||
|
|
19792
|
+
stringValue(submission.agent_accepted_at) !== undefined) {
|
|
19793
|
+
return undefined;
|
|
19794
|
+
}
|
|
19795
|
+
return abortedAt;
|
|
19796
|
+
}
|
|
19797
|
+
function abortPreparedDeferredForegroundTurn({ storeDir, terminal, transfer, boundary, stateLockHeld = false, terminalInputNotStartedAt }) {
|
|
19798
|
+
const dispatchIntentWasProvedNotStarted = transfer.status === "dispatch_started" &&
|
|
19799
|
+
transfer.input_stage === "dispatch_started" &&
|
|
19800
|
+
terminalInputNotStartedAt !== undefined &&
|
|
19801
|
+
Number.isFinite(Date.parse(terminalInputNotStartedAt));
|
|
19802
|
+
if (terminalInputNotStartedAt !== undefined &&
|
|
19803
|
+
!dispatchIntentWasProvedNotStarted) {
|
|
19804
|
+
throw new Error(`deferred foreground transfer ${transfer.transfer_id} cannot attach ` +
|
|
19805
|
+
"terminal-input-not-started proof to its current stage");
|
|
19806
|
+
}
|
|
19807
|
+
const observedLedger = loadTerminalBridgeDispatchLedger(terminal.terminalControl);
|
|
19808
|
+
// Before this transfer publishes its own ordinary dispatch ledger, the
|
|
19809
|
+
// terminal may still carry the exact resolved history that was snapshot into
|
|
19810
|
+
// the immutable transfer receipt. Treat only that exact fingerprint (or the
|
|
19811
|
+
// exact persisted `none`) as no new dispatch; every other change must prove
|
|
19812
|
+
// this transfer's own ledger below or fail closed.
|
|
19813
|
+
const previousDispatchStillCurrent = deferredCodexPreviousDispatchSnapshotMatches({
|
|
19814
|
+
transfer,
|
|
19815
|
+
terminalControl: terminal.terminalControl,
|
|
19816
|
+
ledger: observedLedger
|
|
19817
|
+
});
|
|
19818
|
+
if (!previousDispatchStillCurrent && !observedLedger) {
|
|
19819
|
+
throw new Error(`deferred foreground transfer ${transfer.transfer_id} previous dispatch ` +
|
|
19820
|
+
"snapshot disappeared before its own ledger was published");
|
|
19821
|
+
}
|
|
19822
|
+
const ledger = previousDispatchStillCurrent
|
|
19823
|
+
? undefined
|
|
19824
|
+
: observedLedger;
|
|
19825
|
+
if (!transfer.state_path) {
|
|
19826
|
+
if (ledger) {
|
|
19827
|
+
throw new Error(`deferred foreground transfer ${transfer.transfer_id} has a dispatch ` +
|
|
19828
|
+
"before its Turn authority was prepared");
|
|
19829
|
+
}
|
|
19830
|
+
abortDeferredCodexForegroundTransferBeforeInput({
|
|
19831
|
+
options: { storeDir },
|
|
19832
|
+
boundary,
|
|
19833
|
+
reason: "recovered before target Turn preparation"
|
|
19834
|
+
});
|
|
19835
|
+
return;
|
|
19836
|
+
}
|
|
19837
|
+
const statePath = path.resolve(transfer.state_path);
|
|
19838
|
+
const canonical = pathsForConversationDir(path.dirname(statePath));
|
|
19839
|
+
if (path.resolve(canonical.storeDir) !== path.resolve(storeDir) ||
|
|
19840
|
+
path.resolve(canonical.statePath) !== statePath) {
|
|
19841
|
+
throw new Error(`deferred foreground transfer ${transfer.transfer_id} has a noncanonical ` +
|
|
19842
|
+
"Turn state path");
|
|
19843
|
+
}
|
|
19844
|
+
if (!fs.existsSync(statePath)) {
|
|
19845
|
+
if (dispatchIntentWasProvedNotStarted) {
|
|
19846
|
+
throw new Error(`deferred foreground transfer ${transfer.transfer_id} lost its Turn ` +
|
|
19847
|
+
"before terminal-input-not-started proof became durable");
|
|
19848
|
+
}
|
|
19849
|
+
if (ledger) {
|
|
19850
|
+
// runTerminalControlSend publishes the exact prepared terminal ledger
|
|
19851
|
+
// before its Turn state. A crash in that narrow window still proves
|
|
19852
|
+
// zero input, provided every immutable ledger field matches this
|
|
19853
|
+
// transfer. Resolve that owner before aborting/restoring the Sessions.
|
|
19854
|
+
assertDeferredForegroundLedgerAuthority({
|
|
19855
|
+
storeDir,
|
|
19856
|
+
terminal,
|
|
19857
|
+
transfer,
|
|
19858
|
+
ledger,
|
|
19859
|
+
statePath
|
|
19860
|
+
});
|
|
19861
|
+
if (!["prepared", "resolved"].includes(String(ledger.status))) {
|
|
19862
|
+
throw new Error(`deferred foreground transfer ${transfer.transfer_id} has ` +
|
|
19863
|
+
`${String(ledger.status)} transport evidence but no exact Turn state`);
|
|
19864
|
+
}
|
|
19865
|
+
if (ledger.status === "prepared") {
|
|
19866
|
+
const abortedAt = cliNow().toISOString();
|
|
19867
|
+
saveTerminalBridgeDispatchLedger(terminal.terminalControl, {
|
|
19868
|
+
...ledger,
|
|
19869
|
+
status: "resolved",
|
|
19870
|
+
aborted_at: abortedAt,
|
|
19871
|
+
safe_to_retry: true,
|
|
19872
|
+
resolved_at: abortedAt,
|
|
19873
|
+
dispatcher_pid: null,
|
|
19874
|
+
reason: "deferred dispatcher exited after ledger prepare and before Turn persistence"
|
|
19875
|
+
});
|
|
19876
|
+
}
|
|
19877
|
+
else {
|
|
19878
|
+
assertDeferredForegroundResolvedZeroInputLedger({
|
|
19879
|
+
storeDir,
|
|
19880
|
+
terminal,
|
|
19881
|
+
transfer,
|
|
19882
|
+
ledger,
|
|
19883
|
+
statePath
|
|
19884
|
+
});
|
|
19885
|
+
}
|
|
19886
|
+
if (cliEnv().AKK_TEST_EXIT_AFTER_DEFERRED_LEDGER_ABORT_WITHOUT_STATE === "1") {
|
|
19887
|
+
// The zero-input top-level and append-only ledger receipts are both
|
|
19888
|
+
// durable, while the transfer still fences both Sessions. A second
|
|
19889
|
+
// writer must accept this exact resolved form and finish Store-only
|
|
19890
|
+
// rollback without creating or replaying a Turn.
|
|
19891
|
+
cliExit(86);
|
|
19892
|
+
}
|
|
19893
|
+
}
|
|
19894
|
+
abortDeferredCodexForegroundTransferBeforeInput({
|
|
19895
|
+
options: { storeDir },
|
|
19896
|
+
boundary,
|
|
19897
|
+
reason: ledger
|
|
19898
|
+
? "recovered exact prepared ledger before target Turn state became durable"
|
|
19899
|
+
: "recovered before target Turn state became durable"
|
|
19900
|
+
});
|
|
19901
|
+
return;
|
|
19902
|
+
}
|
|
19903
|
+
const releaseStateLock = stateLockHeld
|
|
19904
|
+
? undefined
|
|
19905
|
+
: acquireFileLock(`${statePath}.lock`);
|
|
19906
|
+
try {
|
|
19907
|
+
const authority = loadDeferredForegroundTurnAuthority({
|
|
19908
|
+
storeDir,
|
|
19909
|
+
terminal,
|
|
19910
|
+
transfer
|
|
19911
|
+
});
|
|
19912
|
+
const submissionStatus = String(authority.submission.status);
|
|
19913
|
+
let durableNotStartedAt = terminalInputNotStartedAt;
|
|
19914
|
+
if (!ledger && submissionStatus !== "aborted") {
|
|
19915
|
+
throw new Error(`deferred foreground transfer ${transfer.transfer_id} has prepared Turn ` +
|
|
19916
|
+
"state without its exact terminal ledger");
|
|
19917
|
+
}
|
|
19918
|
+
if (ledger) {
|
|
19919
|
+
assertDeferredForegroundLedgerAuthority({
|
|
19920
|
+
storeDir,
|
|
19921
|
+
terminal,
|
|
19922
|
+
transfer,
|
|
19923
|
+
ledger,
|
|
19924
|
+
statePath: authority.statePath,
|
|
19925
|
+
expectedMessageBodyHash: stringValue(authority.submission.message_body_hash)
|
|
19926
|
+
});
|
|
19927
|
+
if (!["prepared", "resolved"].includes(String(ledger.status))) {
|
|
19928
|
+
throw new Error(`deferred foreground transfer ${transfer.transfer_id} has ` +
|
|
19929
|
+
`${String(ledger.status)} transport evidence and cannot be aborted`);
|
|
19930
|
+
}
|
|
19931
|
+
}
|
|
19932
|
+
if (dispatchIntentWasProvedNotStarted && submissionStatus === "aborted") {
|
|
19933
|
+
const existingAbortedAt = stringValue(authority.submission.aborted_at);
|
|
19934
|
+
if (authority.submission.safe_to_retry !== true ||
|
|
19935
|
+
!existingAbortedAt ||
|
|
19936
|
+
!Number.isFinite(Date.parse(existingAbortedAt)) ||
|
|
19937
|
+
stringValue(authority.submission.text_injected_at) !== undefined ||
|
|
19938
|
+
stringValue(authority.submission.enter_dispatched_at) !== undefined ||
|
|
19939
|
+
stringValue(authority.submission.agent_accepted_at) !== undefined ||
|
|
19940
|
+
(durableNotStartedAt !== undefined &&
|
|
19941
|
+
durableNotStartedAt !== existingAbortedAt)) {
|
|
19942
|
+
throw new Error(`deferred foreground transfer ${transfer.transfer_id} has ` +
|
|
19943
|
+
"inconsistent terminal-input-not-started Turn proof");
|
|
19944
|
+
}
|
|
19945
|
+
durableNotStartedAt = existingAbortedAt;
|
|
19946
|
+
}
|
|
19947
|
+
if (submissionStatus === "prepared") {
|
|
19948
|
+
const abortedAt = durableNotStartedAt ?? cliNow().toISOString();
|
|
19949
|
+
const aborted = withTerminalBridgeSubmission({
|
|
19950
|
+
conversation: {
|
|
19951
|
+
...authority.conversation,
|
|
19952
|
+
status: "failed",
|
|
19953
|
+
failed_at: abortedAt,
|
|
19954
|
+
failure_reason: "dispatcher exited before the deferred terminal-input boundary",
|
|
19955
|
+
updated_at: abortedAt
|
|
19956
|
+
},
|
|
19957
|
+
messageId: transfer.message_id,
|
|
19958
|
+
messageType: "task",
|
|
19959
|
+
requestText: String(authority.takeover.terminal_bridge_request_text ?? ""),
|
|
19960
|
+
status: "aborted",
|
|
19961
|
+
preparedAt: stringValue(authority.submission.prepared_at) ?? abortedAt,
|
|
19962
|
+
abortedAt,
|
|
19963
|
+
error: "dispatcher exited before terminal input",
|
|
19964
|
+
safeToRetry: true
|
|
19965
|
+
});
|
|
19966
|
+
saveState(authority.statePath, aborted);
|
|
19967
|
+
}
|
|
19968
|
+
else if (submissionStatus !== "aborted") {
|
|
19969
|
+
throw new Error(`deferred foreground transfer ${transfer.transfer_id} Turn has ` +
|
|
19970
|
+
`${submissionStatus} input evidence and cannot be aborted`);
|
|
19971
|
+
}
|
|
19972
|
+
if (dispatchIntentWasProvedNotStarted &&
|
|
19973
|
+
ledger?.status === "resolved" &&
|
|
19974
|
+
(ledger.safe_to_retry !== true ||
|
|
19975
|
+
stringValue(ledger.aborted_at) !== durableNotStartedAt ||
|
|
19976
|
+
stringValue(ledger.text_injected_at) !== undefined ||
|
|
19977
|
+
stringValue(ledger.enter_dispatched_at) !== undefined ||
|
|
19978
|
+
stringValue(ledger.agent_accepted_at) !== undefined)) {
|
|
19979
|
+
throw new Error(`deferred foreground transfer ${transfer.transfer_id} has ` +
|
|
19980
|
+
"inconsistent terminal-input-not-started ledger proof");
|
|
19981
|
+
}
|
|
19982
|
+
if (ledger && ledger.status !== "resolved") {
|
|
19983
|
+
const abortedAt = durableNotStartedAt ?? cliNow().toISOString();
|
|
19984
|
+
saveTerminalBridgeDispatchLedger(terminal.terminalControl, {
|
|
19985
|
+
...ledger,
|
|
19986
|
+
status: "resolved",
|
|
19987
|
+
aborted_at: abortedAt,
|
|
19988
|
+
safe_to_retry: true,
|
|
19989
|
+
resolved_at: abortedAt,
|
|
19990
|
+
dispatcher_pid: null,
|
|
19991
|
+
reason: "deferred dispatcher exited before the durable terminal-input boundary"
|
|
19992
|
+
});
|
|
19993
|
+
}
|
|
19994
|
+
}
|
|
19995
|
+
finally {
|
|
19996
|
+
releaseStateLock?.();
|
|
19997
|
+
}
|
|
19998
|
+
if (cliEnv().AKK_TEST_EXIT_AFTER_DEFERRED_PREINPUT_ABORT_RECEIPTS === "1") {
|
|
19999
|
+
// The exact Turn and append-only submission receipt are both terminal,
|
|
20000
|
+
// while the transfer and Sessions are intentionally still fenced. The
|
|
20001
|
+
// next writer must finish only the Store rollback and never send input.
|
|
20002
|
+
cliExit(86);
|
|
20003
|
+
}
|
|
20004
|
+
abortDeferredCodexForegroundTransferBeforeInput({
|
|
20005
|
+
options: { storeDir },
|
|
20006
|
+
boundary,
|
|
20007
|
+
reason: "recovered exact prepared Turn with zero terminal input",
|
|
20008
|
+
terminalInputNotStartedAt
|
|
20009
|
+
});
|
|
20010
|
+
}
|
|
20011
|
+
async function recoverAcceptedDeferredForegroundDispatch({ options, storeDir, terminal, transfer, boundary }) {
|
|
20012
|
+
const statePath = path.resolve(required(transfer.state_path, "deferred foreground Turn state path is unavailable"));
|
|
20013
|
+
const releaseStateLock = acquireFileLock(`${statePath}.lock`);
|
|
20014
|
+
try {
|
|
20015
|
+
const authority = loadDeferredForegroundTurnAuthority({
|
|
20016
|
+
storeDir,
|
|
20017
|
+
terminal,
|
|
20018
|
+
transfer
|
|
20019
|
+
});
|
|
20020
|
+
const ledger = loadTerminalBridgeDispatchLedger(terminal.terminalControl);
|
|
20021
|
+
if (!ledger) {
|
|
20022
|
+
throw new Error(`deferred foreground transfer ${transfer.transfer_id} has no exact ` +
|
|
20023
|
+
"terminal dispatch ledger for acceptance recovery");
|
|
20024
|
+
}
|
|
20025
|
+
assertDeferredForegroundLedgerAuthority({
|
|
20026
|
+
storeDir,
|
|
20027
|
+
terminal,
|
|
20028
|
+
transfer,
|
|
20029
|
+
ledger,
|
|
20030
|
+
statePath: authority.statePath,
|
|
20031
|
+
expectedMessageBodyHash: stringValue(authority.submission.message_body_hash)
|
|
20032
|
+
});
|
|
20033
|
+
if (![
|
|
20034
|
+
"prepared",
|
|
20035
|
+
"text_injected",
|
|
20036
|
+
"enter_dispatched",
|
|
20037
|
+
"uncertain",
|
|
20038
|
+
"agent_accepted"
|
|
20039
|
+
].includes(String(ledger.status))) {
|
|
20040
|
+
throw new Error(`deferred foreground transfer ${transfer.transfer_id} has invalid ` +
|
|
20041
|
+
`${String(ledger.status)} dispatch evidence for acceptance recovery`);
|
|
20042
|
+
}
|
|
20043
|
+
const identity = await resolveCurrentNativeAgentSessionIdentity({
|
|
20044
|
+
options,
|
|
20045
|
+
agent: "codex",
|
|
20046
|
+
pid: transfer.process_pid,
|
|
20047
|
+
cwd: terminal.terminalControl.currentPath
|
|
20048
|
+
});
|
|
20049
|
+
if (!identity) {
|
|
20050
|
+
return false;
|
|
20051
|
+
}
|
|
20052
|
+
const acceptance = detectCodexRolloutAcceptance({
|
|
20053
|
+
anchor: authority.anchor,
|
|
20054
|
+
currentIdentity: identity,
|
|
20055
|
+
requestHash: transfer.request_hash
|
|
20056
|
+
});
|
|
20057
|
+
if (!acceptance) {
|
|
20058
|
+
return false;
|
|
20059
|
+
}
|
|
20060
|
+
if (ledger.status === "agent_accepted") {
|
|
20061
|
+
validateTerminalSubmissionAcceptanceEvidence(ledger.acceptance_evidence, {
|
|
20062
|
+
source: "codex_rollout",
|
|
20063
|
+
nativeThreadId: identity.sessionId,
|
|
20064
|
+
requestHash: transfer.request_hash
|
|
20065
|
+
});
|
|
20066
|
+
}
|
|
20067
|
+
const acceptedAt = cliNow().toISOString();
|
|
20068
|
+
const boundConversation = withNativeAgentSessionIdentity(authority.conversation, identity);
|
|
20069
|
+
const acceptedConversation = withTerminalBridgeSubmission({
|
|
20070
|
+
conversation: boundConversation,
|
|
20071
|
+
messageId: transfer.message_id,
|
|
20072
|
+
messageType: "task",
|
|
20073
|
+
requestText: String(authority.takeover.terminal_bridge_request_text ?? ""),
|
|
20074
|
+
status: "agent_accepted",
|
|
20075
|
+
preparedAt: stringValue(authority.submission.prepared_at) ?? transfer.prepared_at,
|
|
20076
|
+
textInjectedAt: stringValue(authority.submission.text_injected_at) ??
|
|
20077
|
+
transfer.text_injected_at ?? transfer.dispatch_started_at,
|
|
20078
|
+
enterDispatchedAt: stringValue(authority.submission.enter_dispatched_at) ??
|
|
20079
|
+
transfer.enter_dispatched_at ?? transfer.dispatch_started_at,
|
|
20080
|
+
agentAcceptedAt: acceptedAt,
|
|
20081
|
+
acceptanceEvidence: acceptance
|
|
20082
|
+
});
|
|
20083
|
+
let acceptedStatePersisted = false;
|
|
20084
|
+
// Persist the strongest exact request receipt before mutating either
|
|
20085
|
+
// Session. A crash after the transfer resolves can then be recovered from
|
|
20086
|
+
// this agent_accepted Turn without replaying terminal input.
|
|
20087
|
+
saveState(authority.statePath, acceptedConversation);
|
|
20088
|
+
acceptedStatePersisted = true;
|
|
20089
|
+
if (cliEnv().AKK_TEST_EXIT_AFTER_DEFERRED_ACCEPTED_TURN === "1") {
|
|
20090
|
+
cliExit(86);
|
|
20091
|
+
}
|
|
20092
|
+
let transferCommitted = false;
|
|
20093
|
+
try {
|
|
20094
|
+
await commitDeferredCodexForegroundBinding({
|
|
20095
|
+
options,
|
|
20096
|
+
boundary,
|
|
20097
|
+
identity,
|
|
20098
|
+
acceptedAt
|
|
20099
|
+
});
|
|
20100
|
+
transferCommitted = true;
|
|
20101
|
+
assertNativeAgentIdentityForTurn({
|
|
20102
|
+
conversation: acceptedConversation,
|
|
20103
|
+
currentIdentity: identity,
|
|
20104
|
+
operation: "recover deferred foreground binding for"
|
|
20105
|
+
});
|
|
20106
|
+
saveTerminalBridgeDispatchLedger(terminal.terminalControl, {
|
|
20107
|
+
...ledger,
|
|
20108
|
+
...terminalBindingLedgerFields(acceptedConversation),
|
|
20109
|
+
status: "agent_accepted",
|
|
20110
|
+
generation_id: transfer.message_id,
|
|
20111
|
+
conversation_id: acceptedConversation.conversation_id,
|
|
20112
|
+
session_id: transfer.target_session_id,
|
|
20113
|
+
turn_id: transfer.turn_id,
|
|
20114
|
+
message_id: transfer.message_id,
|
|
20115
|
+
message_type: "task",
|
|
20116
|
+
request_hash: transfer.request_hash,
|
|
20117
|
+
prepared_at: transfer.prepared_at,
|
|
20118
|
+
text_injected_at: transfer.text_injected_at ?? transfer.dispatch_started_at,
|
|
20119
|
+
enter_dispatched_at: transfer.enter_dispatched_at ?? transfer.dispatch_started_at,
|
|
20120
|
+
agent_accepted_at: acceptedAt,
|
|
20121
|
+
acceptance_evidence: acceptance,
|
|
20122
|
+
dispatcher_pid: null,
|
|
20123
|
+
state_path: authority.statePath,
|
|
20124
|
+
event_log_path: authority.logPath,
|
|
20125
|
+
callback_expected: Boolean(acceptedConversation.gateway_method),
|
|
20126
|
+
reason: "recovered exact deferred foreground request acceptance"
|
|
20127
|
+
});
|
|
20128
|
+
return true;
|
|
20129
|
+
}
|
|
20130
|
+
catch (error) {
|
|
20131
|
+
if (transferCommitted && !acceptedStatePersisted) {
|
|
20132
|
+
const uncertainAt = cliNow().toISOString();
|
|
20133
|
+
const reason = "deferred Session transfer resolved but exact Turn identity " +
|
|
20134
|
+
`bookkeeping failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
20135
|
+
const uncertainConversation = withTerminalBridgeSubmission({
|
|
20136
|
+
conversation: {
|
|
20137
|
+
...authority.conversation,
|
|
20138
|
+
status: "stalled",
|
|
20139
|
+
stalled_at: uncertainAt,
|
|
20140
|
+
stalled_reason: reason,
|
|
20141
|
+
updated_at: uncertainAt
|
|
20142
|
+
},
|
|
20143
|
+
messageId: transfer.message_id,
|
|
20144
|
+
messageType: "task",
|
|
20145
|
+
requestText: String(authority.takeover.terminal_bridge_request_text ?? ""),
|
|
20146
|
+
status: "uncertain",
|
|
20147
|
+
preparedAt: stringValue(authority.submission.prepared_at) ?? transfer.prepared_at,
|
|
20148
|
+
textInjectedAt: stringValue(authority.submission.text_injected_at) ??
|
|
20149
|
+
transfer.text_injected_at ?? transfer.dispatch_started_at,
|
|
20150
|
+
enterDispatchedAt: stringValue(authority.submission.enter_dispatched_at) ??
|
|
20151
|
+
transfer.enter_dispatched_at ?? transfer.dispatch_started_at,
|
|
20152
|
+
uncertainAt,
|
|
20153
|
+
error: reason,
|
|
20154
|
+
lastProvenStage: "enter_dispatched",
|
|
20155
|
+
safeToRetry: false
|
|
20156
|
+
});
|
|
20157
|
+
saveState(authority.statePath, uncertainConversation);
|
|
20158
|
+
saveTerminalBridgeDispatchLedger(terminal.terminalControl, {
|
|
20159
|
+
...ledger,
|
|
20160
|
+
...terminalBindingLedgerFields(uncertainConversation),
|
|
20161
|
+
status: "uncertain",
|
|
20162
|
+
uncertain_at: uncertainAt,
|
|
20163
|
+
dispatcher_pid: null,
|
|
20164
|
+
callback_expected: false,
|
|
20165
|
+
reason
|
|
20166
|
+
});
|
|
20167
|
+
}
|
|
20168
|
+
else if (transferCommitted) {
|
|
20169
|
+
// Do not downgrade a durable exact acceptance to the older pre-commit
|
|
20170
|
+
// authority merely because ledger bookkeeping failed. The accepted
|
|
20171
|
+
// receipt remains the monotonic source for later ledger reconciliation,
|
|
20172
|
+
// while the Turn itself is stalled so callback/monitor work cannot
|
|
20173
|
+
// proceed past a failed exact binding assertion.
|
|
20174
|
+
const stalledAt = cliNow().toISOString();
|
|
20175
|
+
const reason = "deferred transfer resolved, but exact accepted Turn bookkeeping " +
|
|
20176
|
+
`failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
20177
|
+
saveState(authority.statePath, {
|
|
20178
|
+
...acceptedConversation,
|
|
20179
|
+
status: "stalled",
|
|
20180
|
+
stalled_at: stalledAt,
|
|
20181
|
+
stalled_reason: reason,
|
|
20182
|
+
updated_at: stalledAt
|
|
20183
|
+
});
|
|
20184
|
+
try {
|
|
20185
|
+
saveTerminalBridgeDispatchLedger(terminal.terminalControl, {
|
|
20186
|
+
...ledger,
|
|
20187
|
+
...terminalBindingLedgerFields(acceptedConversation),
|
|
20188
|
+
status: "uncertain",
|
|
20189
|
+
uncertain_at: stalledAt,
|
|
20190
|
+
last_proven_stage: "agent_accepted",
|
|
20191
|
+
acceptance_evidence: acceptance,
|
|
20192
|
+
dispatcher_pid: null,
|
|
20193
|
+
callback_expected: false,
|
|
20194
|
+
reason
|
|
20195
|
+
});
|
|
20196
|
+
}
|
|
20197
|
+
catch {
|
|
20198
|
+
// The exact accepted Turn above remains authoritative even when the
|
|
20199
|
+
// ledger itself is the bookkeeping failure being reported.
|
|
20200
|
+
}
|
|
20201
|
+
runtimeLog("error", "deferred_acceptance_ledger_bookkeeping_failed", {
|
|
20202
|
+
transfer_id: transfer.transfer_id,
|
|
20203
|
+
turn_id: transfer.turn_id,
|
|
20204
|
+
state_path: authority.statePath,
|
|
20205
|
+
error: error instanceof Error ? error.message : String(error)
|
|
20206
|
+
});
|
|
20207
|
+
}
|
|
20208
|
+
throw error;
|
|
20209
|
+
}
|
|
20210
|
+
}
|
|
20211
|
+
finally {
|
|
20212
|
+
releaseStateLock();
|
|
20213
|
+
}
|
|
20214
|
+
}
|
|
20215
|
+
function persistCommittedDeferredForegroundTurnAcceptance({ storeDir, terminal, transfer }) {
|
|
20216
|
+
const current = loadDeferredForegroundTransfer(storeDir, transfer.transfer_id);
|
|
20217
|
+
if (current.status !== "committed" ||
|
|
20218
|
+
!current.target_accepted_binding ||
|
|
20219
|
+
!current.target_native_thread_id ||
|
|
20220
|
+
!current.agent_accepted_at) {
|
|
20221
|
+
throw new Error(`deferred foreground transfer ${transfer.transfer_id} has no exact ` +
|
|
20222
|
+
"committed acceptance authority");
|
|
20223
|
+
}
|
|
20224
|
+
const acceptedBinding = current.target_accepted_binding;
|
|
20225
|
+
const rollout = acceptedBinding.native_process.rollout;
|
|
20226
|
+
const processUuid = acceptedBinding.native_process.process_uuid;
|
|
20227
|
+
const processBirth = acceptedBinding.native_process.process_birth;
|
|
20228
|
+
if (acceptedBinding.native_thread_id !== current.target_native_thread_id ||
|
|
20229
|
+
processUuid !== current.process_uuid ||
|
|
20230
|
+
processBirth !== current.process_birth ||
|
|
20231
|
+
!isCompleteNativeRollout(rollout)) {
|
|
20232
|
+
throw new Error(`deferred foreground transfer ${transfer.transfer_id} committed binding ` +
|
|
20233
|
+
"is incomplete");
|
|
20234
|
+
}
|
|
20235
|
+
const identity = {
|
|
20236
|
+
sessionId: current.target_native_thread_id,
|
|
20237
|
+
processUuid,
|
|
20238
|
+
processBirth,
|
|
20239
|
+
rollout,
|
|
20240
|
+
evidence: acceptedBinding.native_process.evidence
|
|
20241
|
+
};
|
|
20242
|
+
const statePath = path.resolve(required(current.state_path, "deferred foreground Turn state path is unavailable"));
|
|
20243
|
+
const releaseStateLock = acquireFileLock(`${statePath}.lock`);
|
|
20244
|
+
try {
|
|
20245
|
+
const authority = loadDeferredForegroundTurnAuthority({
|
|
20246
|
+
storeDir,
|
|
20247
|
+
terminal,
|
|
20248
|
+
transfer: current
|
|
20249
|
+
});
|
|
20250
|
+
const ledger = loadTerminalBridgeDispatchLedger(terminal.terminalControl);
|
|
20251
|
+
if (!ledger) {
|
|
20252
|
+
throw new Error(`deferred foreground transfer ${current.transfer_id} has no exact ` +
|
|
20253
|
+
"terminal dispatch ledger for committed recovery");
|
|
20254
|
+
}
|
|
20255
|
+
assertDeferredForegroundLedgerAuthority({
|
|
20256
|
+
storeDir,
|
|
20257
|
+
terminal,
|
|
20258
|
+
transfer: current,
|
|
20259
|
+
ledger,
|
|
20260
|
+
statePath: authority.statePath,
|
|
20261
|
+
expectedMessageBodyHash: stringValue(authority.submission.message_body_hash)
|
|
20262
|
+
});
|
|
20263
|
+
if (![
|
|
20264
|
+
"prepared",
|
|
20265
|
+
"text_injected",
|
|
20266
|
+
"enter_dispatched",
|
|
20267
|
+
"uncertain",
|
|
20268
|
+
"agent_accepted"
|
|
20269
|
+
].includes(String(ledger.status))) {
|
|
20270
|
+
throw new Error(`deferred foreground transfer ${current.transfer_id} has invalid ` +
|
|
20271
|
+
`${String(ledger.status)} dispatch evidence for committed recovery`);
|
|
20272
|
+
}
|
|
20273
|
+
const acceptance = detectCodexRolloutAcceptance({
|
|
20274
|
+
anchor: authority.anchor,
|
|
20275
|
+
currentIdentity: identity,
|
|
20276
|
+
requestHash: current.request_hash
|
|
20277
|
+
});
|
|
20278
|
+
if (!acceptance) {
|
|
20279
|
+
throw new Error(`deferred foreground transfer ${current.transfer_id} cannot re-prove ` +
|
|
20280
|
+
"its exact request acceptance");
|
|
20281
|
+
}
|
|
20282
|
+
if (ledger.status === "agent_accepted") {
|
|
20283
|
+
validateTerminalSubmissionAcceptanceEvidence(ledger.acceptance_evidence, {
|
|
20284
|
+
source: "codex_rollout",
|
|
20285
|
+
nativeThreadId: identity.sessionId,
|
|
20286
|
+
requestHash: current.request_hash
|
|
20287
|
+
});
|
|
20288
|
+
}
|
|
20289
|
+
const acceptedConversation = withTerminalBridgeSubmission({
|
|
20290
|
+
conversation: withNativeAgentSessionIdentity(authority.conversation, identity),
|
|
20291
|
+
messageId: current.message_id,
|
|
20292
|
+
messageType: "task",
|
|
20293
|
+
requestText: String(authority.takeover.terminal_bridge_request_text ?? ""),
|
|
20294
|
+
status: "agent_accepted",
|
|
20295
|
+
preparedAt: stringValue(authority.submission.prepared_at) ?? current.prepared_at,
|
|
20296
|
+
textInjectedAt: stringValue(authority.submission.text_injected_at) ??
|
|
20297
|
+
current.text_injected_at ?? current.dispatch_started_at,
|
|
20298
|
+
enterDispatchedAt: stringValue(authority.submission.enter_dispatched_at) ??
|
|
20299
|
+
current.enter_dispatched_at ?? current.dispatch_started_at,
|
|
20300
|
+
agentAcceptedAt: current.agent_accepted_at,
|
|
20301
|
+
acceptanceEvidence: acceptance
|
|
20302
|
+
});
|
|
20303
|
+
saveState(authority.statePath, acceptedConversation);
|
|
20304
|
+
saveTerminalBridgeDispatchLedger(terminal.terminalControl, {
|
|
20305
|
+
...ledger,
|
|
20306
|
+
...terminalBindingLedgerFields(acceptedConversation),
|
|
20307
|
+
status: "agent_accepted",
|
|
20308
|
+
generation_id: current.message_id,
|
|
20309
|
+
conversation_id: acceptedConversation.conversation_id,
|
|
20310
|
+
session_id: current.target_session_id,
|
|
20311
|
+
turn_id: current.turn_id,
|
|
20312
|
+
message_id: current.message_id,
|
|
20313
|
+
message_type: "task",
|
|
20314
|
+
request_hash: current.request_hash,
|
|
20315
|
+
prepared_at: current.prepared_at,
|
|
20316
|
+
text_injected_at: current.text_injected_at ?? current.dispatch_started_at,
|
|
20317
|
+
enter_dispatched_at: current.enter_dispatched_at ?? current.dispatch_started_at,
|
|
20318
|
+
agent_accepted_at: current.agent_accepted_at,
|
|
20319
|
+
acceptance_evidence: acceptance,
|
|
20320
|
+
dispatcher_pid: null,
|
|
20321
|
+
state_path: authority.statePath,
|
|
20322
|
+
event_log_path: authority.logPath,
|
|
20323
|
+
callback_expected: Boolean(acceptedConversation.gateway_method),
|
|
20324
|
+
reason: "recovered committed deferred foreground request acceptance"
|
|
20325
|
+
});
|
|
20326
|
+
return { conversation: acceptedConversation, identity };
|
|
20327
|
+
}
|
|
20328
|
+
finally {
|
|
20329
|
+
releaseStateLock();
|
|
20330
|
+
}
|
|
20331
|
+
}
|
|
20332
|
+
async function recoverDeferredCodexForegroundTransferBeforeMutation({ options, terminal }) {
|
|
20333
|
+
const storeDir = storeDirFromOptions(options);
|
|
20334
|
+
let transfers = listDeferredForegroundTransfers(storeDir);
|
|
20335
|
+
// An abort intent is Store authority: it is finalized without consulting a
|
|
20336
|
+
// live pane, PID, cwd, or process birth. Once the exact cleanup receipt is
|
|
20337
|
+
// durable, later legitimate Session revisions never need to be revalidated
|
|
20338
|
+
// against this historical transfer.
|
|
20339
|
+
for (const aborted of transfers.filter((entry) => entry.status === "aborted")) {
|
|
20340
|
+
finalizeAbortedDeferredCodexForegroundTransferCleanup({
|
|
20341
|
+
storeDir,
|
|
20342
|
+
transfer: aborted
|
|
20343
|
+
});
|
|
20344
|
+
}
|
|
20345
|
+
transfers = listDeferredForegroundTransfers(storeDir);
|
|
20346
|
+
const matching = transfers.filter((transfer) => transfer.terminal_id === terminal.conversationId &&
|
|
20347
|
+
transfer.process_pid === terminal.pid &&
|
|
20348
|
+
terminalControlEvidenceMatches(transfer.terminal_endpoint, terminal.terminalControl));
|
|
20349
|
+
const candidates = matching.filter((transfer) => !["resolved", "aborted", "abort_resolved"].includes(transfer.status));
|
|
20350
|
+
if (candidates.length === 0) {
|
|
20351
|
+
return;
|
|
20352
|
+
}
|
|
20353
|
+
if (candidates.length !== 1) {
|
|
20354
|
+
throw new Error(`terminal ${terminal.terminalControl.target} has multiple unresolved ` +
|
|
20355
|
+
"deferred foreground transfers; no terminal input was sent");
|
|
20356
|
+
}
|
|
20357
|
+
let transfer = candidates[0];
|
|
20358
|
+
const boundary = deferredCodexBoundaryFromTransfer({ terminal, transfer });
|
|
20359
|
+
if (transfer.status === "committed") {
|
|
20360
|
+
const accepted = persistCommittedDeferredForegroundTurnAcceptance({
|
|
20361
|
+
storeDir,
|
|
20362
|
+
terminal,
|
|
20363
|
+
transfer
|
|
20364
|
+
});
|
|
20365
|
+
if (cliEnv().AKK_TEST_EXIT_AFTER_DEFERRED_COMMITTED_ACCEPTANCE_BACKFILL ===
|
|
20366
|
+
"1") {
|
|
20367
|
+
// The exact accepted Turn and ledger are durable while both Sessions
|
|
20368
|
+
// remain fenced by the committed transfer. Recovery must accept this
|
|
20369
|
+
// monotonic ledger shape and finish Session resolution without input.
|
|
20370
|
+
cliExit(86);
|
|
20371
|
+
}
|
|
20372
|
+
await resolveCommittedDeferredCodexForegroundTransfer({
|
|
20373
|
+
options,
|
|
20374
|
+
boundary
|
|
20375
|
+
});
|
|
20376
|
+
assertNativeAgentIdentityForTurn({
|
|
20377
|
+
conversation: accepted.conversation,
|
|
20378
|
+
currentIdentity: accepted.identity,
|
|
20379
|
+
operation: "recover committed deferred foreground binding for"
|
|
20380
|
+
});
|
|
20381
|
+
return;
|
|
20382
|
+
}
|
|
20383
|
+
if (["prepared", "source_reserved", "target_prepared"].includes(transfer.status)) {
|
|
20384
|
+
abortPreparedDeferredForegroundTurn({
|
|
20385
|
+
storeDir,
|
|
20386
|
+
terminal,
|
|
20387
|
+
transfer,
|
|
20388
|
+
boundary
|
|
20389
|
+
});
|
|
20390
|
+
return;
|
|
20391
|
+
}
|
|
20392
|
+
if (["dispatch_started", "uncertain"].includes(transfer.status)) {
|
|
20393
|
+
const durableInputNotStartedAt = deferredCodexDurableInputNotStartedAt(transfer);
|
|
20394
|
+
if (durableInputNotStartedAt) {
|
|
20395
|
+
abortPreparedDeferredForegroundTurn({
|
|
20396
|
+
storeDir,
|
|
20397
|
+
terminal,
|
|
20398
|
+
transfer,
|
|
20399
|
+
boundary,
|
|
20400
|
+
terminalInputNotStartedAt: durableInputNotStartedAt
|
|
20401
|
+
});
|
|
20402
|
+
return;
|
|
20403
|
+
}
|
|
20404
|
+
try {
|
|
20405
|
+
if (await recoverAcceptedDeferredForegroundDispatch({
|
|
20406
|
+
options,
|
|
20407
|
+
storeDir,
|
|
20408
|
+
terminal,
|
|
20409
|
+
transfer,
|
|
20410
|
+
boundary
|
|
20411
|
+
})) {
|
|
20412
|
+
return;
|
|
20413
|
+
}
|
|
20414
|
+
}
|
|
20415
|
+
catch (error) {
|
|
20416
|
+
const durable = loadDeferredForegroundTransfer(storeDir, transfer.transfer_id);
|
|
20417
|
+
if (durable.status === "dispatch_started") {
|
|
20418
|
+
transfer = markDeferredCodexForegroundTransferUncertain({
|
|
20419
|
+
options,
|
|
20420
|
+
boundary,
|
|
20421
|
+
reason: "exact deferred acceptance recovery failed: " +
|
|
20422
|
+
(error instanceof Error ? error.message : String(error))
|
|
20423
|
+
});
|
|
20424
|
+
}
|
|
20425
|
+
throw new Error(`terminal ${terminal.terminalControl.target} deferred foreground ` +
|
|
20426
|
+
`recovery failed closed: ${error instanceof Error
|
|
20427
|
+
? error.message
|
|
20428
|
+
: String(error)}`, { cause: error });
|
|
20429
|
+
}
|
|
20430
|
+
if (transfer.status !== "uncertain") {
|
|
20431
|
+
transfer = markDeferredCodexForegroundTransferUncertain({
|
|
20432
|
+
options,
|
|
20433
|
+
boundary,
|
|
20434
|
+
reason: "dispatcher exited after the conservative terminal-input boundary " +
|
|
20435
|
+
"without exact native request acceptance"
|
|
20436
|
+
});
|
|
20437
|
+
}
|
|
20438
|
+
throw new Error(`terminal ${terminal.terminalControl.target} has deferred foreground ` +
|
|
20439
|
+
`transfer ${transfer.transfer_id} with an uncertain dispatch; do not ` +
|
|
20440
|
+
"retry or resolve its Turn outside dedicated recovery");
|
|
20441
|
+
}
|
|
20442
|
+
throw new Error(`deferred foreground transfer ${transfer.transfer_id} has unsupported ` +
|
|
20443
|
+
`recovery status ${transfer.status}`);
|
|
20444
|
+
}
|
|
18014
20445
|
async function recoverLifecycleFenceBeforeMutation({ options, terminal }) {
|
|
20446
|
+
await recoverDeferredCodexForegroundTransferBeforeMutation({
|
|
20447
|
+
options,
|
|
20448
|
+
terminal
|
|
20449
|
+
});
|
|
18015
20450
|
let ledger = loadTerminalBridgeDispatchLedger(terminal.terminalControl);
|
|
18016
20451
|
if (!ledger || ledger.status === "resolved") {
|
|
18017
20452
|
ledger = rebuildObservedHandoffLedgerFromTransition({
|
|
@@ -19966,6 +22401,15 @@ function runCallback(options) {
|
|
|
19966
22401
|
}
|
|
19967
22402
|
function runRetryCallback(options) {
|
|
19968
22403
|
const { conversation, statePath, logPath } = loadConversationFromOptions(options);
|
|
22404
|
+
const retryStoreDir = pathsForConversationDir(path.dirname(statePath)).storeDir;
|
|
22405
|
+
withStoreWriterLease(retryStoreDir, () => {
|
|
22406
|
+
const fresh = loadState(statePath);
|
|
22407
|
+
assertConversationHasNoNonterminalDeferredForegroundTransfer({
|
|
22408
|
+
storeDir: retryStoreDir,
|
|
22409
|
+
conversation: fresh,
|
|
22410
|
+
action: "retry callback for"
|
|
22411
|
+
});
|
|
22412
|
+
});
|
|
19969
22413
|
const callbackDelivery = isRecord(conversation.callback_delivery)
|
|
19970
22414
|
? conversation.callback_delivery
|
|
19971
22415
|
: undefined;
|