@scotthuang/agent-knock-knock 0.12.5 → 0.12.6

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