@scotthuang/agent-knock-knock 0.12.6 → 0.12.7

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.
@@ -6,6 +6,7 @@ import path from "node:path";
6
6
  import process from "node:process";
7
7
  import { fileURLToPath } from "node:url";
8
8
  import { createCodexTerminalAgentAdapter, detectCodexDurableCompletion } from "./codex-terminal-agent-adapter.js";
9
+ import { codexLifecycleBehaviorProfile } from "./codex-lifecycle-compatibility.js";
9
10
  import { createClaudeTerminalAgentAdapter } from "./claude-terminal-agent-adapter.js";
10
11
  import { captureClaudeTranscriptAnchor, defaultClaudeHome, detectClaudeTranscriptAcceptance, detectClaudeTranscriptCompletion, detectClaudeTranscriptPendingApproval, listClaudeThreadLifecycleCandidates, revalidateClaudeThreadLifecycleCandidate } from "./claude-local-transcript-provider.js";
11
12
  import { captureCodexCandidateSetRolloutAcceptanceAnchor, captureCodexRolloutAcceptanceAnchor, detectCodexBoundRolloutCompletion, detectCodexCandidateSetRolloutAcceptance, detectCodexRolloutAcceptance, terminalSubmissionReplayReceipt, validateCodexRolloutAcceptanceAnchor, validateTerminalSubmissionAcceptanceEvidence } from "./terminal-submission-acceptance.js";
@@ -2600,8 +2601,8 @@ function assertNoUnresolvedTerminalBridgeSubmission(storeDir, terminalControl, c
2600
2601
  for (const candidate of listConversations(storeDir)) {
2601
2602
  const submission = terminalBridgeSubmission(candidate);
2602
2603
  if (candidate.conversation_id === currentConversationId ||
2603
- !isActiveStatus(candidate.status) ||
2604
2604
  !submission ||
2605
+ TERMINAL_DISPATCH_RELEASE_STATUSES.has(effectiveTurnStatus(candidate)) ||
2605
2606
  ![
2606
2607
  "prepared",
2607
2608
  "text_injected",
@@ -3329,6 +3330,12 @@ async function terminalControlledListEntry(session, activeSessions, options, bri
3329
3330
  statusInspection: false,
3330
3331
  reason: "native inspection is unavailable"
3331
3332
  };
3333
+ const codexLatentClearResumeObservationValue = session.agent === "codex"
3334
+ ? codexLatentClearResumeObservation({
3335
+ screen: terminalState.screen_excerpt,
3336
+ agentVersion
3337
+ })
3338
+ : undefined;
3332
3339
  const lifecycleBindingToken = unmanagedTerminalBindingToken({
3333
3340
  terminalId: bridge.terminalConversationId(session),
3334
3341
  terminalControl,
@@ -3347,7 +3354,8 @@ async function terminalControlledListEntry(session, activeSessions, options, bri
3347
3354
  if (session.agent === "codex" &&
3348
3355
  (terminalState.activity_state === "idle" ||
3349
3356
  (terminalState.activity_state === "unknown" &&
3350
- nativeIdentityObservation.status === "verified_absent")) &&
3357
+ (nativeIdentityObservation.status === "verified_absent" ||
3358
+ codexOpenRootRolloutInventory !== undefined))) &&
3351
3359
  terminalState.approval_state.blocked !== true &&
3352
3360
  terminalControl.capabilities.includes("send_keys") &&
3353
3361
  terminalControl.capabilities.includes("screen_status")) {
@@ -3403,6 +3411,14 @@ async function terminalControlledListEntry(session, activeSessions, options, bri
3403
3411
  // this field after gating every automated-input action that can follow a
3404
3412
  // human native-thread switch.
3405
3413
  _automated_input_composer_ready: automatedInputComposerReady,
3414
+ ...(codexLatentClearResumeObservationValue
3415
+ ? {
3416
+ _codex_latent_clear_resume: {
3417
+ source_native_thread_id: codexLatentClearResumeObservationValue.sourceNativeThreadId,
3418
+ fingerprint: codexLatentClearResumeObservationValue.fingerprint
3419
+ }
3420
+ }
3421
+ : {}),
3406
3422
  ...(codexOpenRootRolloutInventory
3407
3423
  ? {
3408
3424
  _codex_open_root_rollout_inventory: codexOpenRootRolloutInventory
@@ -3482,7 +3498,7 @@ function terminalFirstListProjection({ storeDir, terminals, managedSessions, ses
3482
3498
  return control ? [control] : [];
3483
3499
  });
3484
3500
  const projectedTerminals = terminals.map((terminal) => {
3485
- const { _automated_input_composer_ready: automatedInputComposerReady, _codex_open_root_rollout_inventory: codexOpenRootRolloutInventoryValue, ...publicTerminal } = terminal;
3501
+ const { _automated_input_composer_ready: automatedInputComposerReady, _codex_open_root_rollout_inventory: codexOpenRootRolloutInventoryValue, _codex_latent_clear_resume: codexLatentClearResumeValue, ...publicTerminal } = terminal;
3486
3502
  const codexOpenRootRolloutInventory = isRecord(codexOpenRootRolloutInventoryValue)
3487
3503
  ? codexOpenRootRolloutInventoryValue
3488
3504
  : undefined;
@@ -3732,16 +3748,29 @@ function terminalFirstListProjection({ storeDir, terminals, managedSessions, ses
3732
3748
  codexOpenRootRolloutInventory.roots.length > 0
3733
3749
  ? codexOpenRootRolloutInventory
3734
3750
  : undefined;
3735
- const deferredCodexSource = authoritativeSession &&
3751
+ const abandonedConflictSource = !authoritativeSession &&
3752
+ soleBindingConflict?.kind === "unverifiable" &&
3753
+ matchingSessions.length === 0 &&
3754
+ conflictingBoundSessionClaims.length === 1 &&
3755
+ unresolvedSessionClaims.length === 0 &&
3756
+ terminal.agent === "codex"
3757
+ ? soleBindingConflict.session
3758
+ : undefined;
3759
+ const deferredCodexSource = (authoritativeSession ??
3760
+ abandonedConflictSource) &&
3736
3761
  terminal.agent === "codex" &&
3737
3762
  (nativeIdentityObservation?.status === "verified_absent" ||
3738
3763
  deferredCodexCandidateInventory !== undefined)
3739
- ? authoritativeSession
3764
+ ? authoritativeSession ?? abandonedConflictSource
3740
3765
  : undefined;
3741
3766
  const deferredCodexProcessUuid = stringValue(terminal.native_agent_process_uuid);
3742
3767
  const deferredCodexProcessBirth = stringValue(terminal.native_agent_process_birth);
3743
3768
  const deferredCodexWorkspace = stringValue(terminal.workspace ?? terminal.cwd);
3744
3769
  const deferredCodexSourceNativeThreadId = stringValue(deferredCodexSource?.binding?.native_thread_id)?.toLowerCase();
3770
+ const deferredCodexLatentClearResumeFingerprint = isRecord(codexLatentClearResumeValue) &&
3771
+ stringValue(codexLatentClearResumeValue.source_native_thread_id)?.toLowerCase() === deferredCodexSourceNativeThreadId
3772
+ ? stringValue(codexLatentClearResumeValue.fingerprint)
3773
+ : undefined;
3745
3774
  const deferredCodexSourceActiveElsewhere = Boolean(deferredCodexSourceNativeThreadId &&
3746
3775
  terminals.some((candidate) => candidate !== terminal &&
3747
3776
  candidate.agent === "codex" &&
@@ -3750,9 +3779,30 @@ function terminalFirstListProjection({ storeDir, terminals, managedSessions, ses
3750
3779
  const deferredCodexDispatchSnapshot = terminalControl
3751
3780
  ? tryDeferredCodexForegroundDispatchSnapshot(terminalControl)
3752
3781
  : undefined;
3753
- const deferredCodexCandidateSourceTurnHistory = deferredCodexCandidateInventory && deferredCodexSource
3782
+ const deferredCodexCandidateSourceTurnHistory = deferredCodexCandidateInventory &&
3783
+ deferredCodexSource?.binding?.native_process.rollout
3754
3784
  ? deferredCandidateSourceTurnHistory(storeDir, deferredCodexSource)
3755
3785
  : undefined;
3786
+ const deferredCodexSourceRolloutPresent = Boolean(deferredCodexCandidateInventory &&
3787
+ deferredCodexSource?.binding?.native_process.rollout &&
3788
+ deferredCodexCandidateInventory.roots.some((root) => root.sessionId.toLowerCase() ===
3789
+ deferredCodexSource.binding?.native_thread_id?.toLowerCase() &&
3790
+ exactNativeRolloutMatches(root.rollout, deferredCodexSource.binding.native_process.rollout)));
3791
+ const deferredCodexSourceRolloutAuthority = abandonedConflictSource && !deferredCodexSourceRolloutPresent
3792
+ ? "explicitly_abandoned_predecessor"
3793
+ : "present";
3794
+ const deferredCodexSourceAbandonmentFingerprint = deferredCodexSourceRolloutAuthority ===
3795
+ "explicitly_abandoned_predecessor" &&
3796
+ deferredCodexSource &&
3797
+ deferredCodexCandidateSourceTurnHistory &&
3798
+ deferredCodexDispatchSnapshot
3799
+ ? explicitlyAbandonedCandidateSourceFingerprint({
3800
+ storeDir,
3801
+ session: deferredCodexSource,
3802
+ sourceTurnHistory: deferredCodexCandidateSourceTurnHistory,
3803
+ dispatchSnapshot: deferredCodexDispatchSnapshot
3804
+ })
3805
+ : undefined;
3756
3806
  const deferredCodexStatusCardSourceEligible = Boolean(deferredCodexSource &&
3757
3807
  terminalControl &&
3758
3808
  deferredCodexProcessUuid &&
@@ -3785,13 +3835,24 @@ function terminalFirstListProjection({ storeDir, terminals, managedSessions, ses
3785
3835
  workspace: deferredCodexWorkspace,
3786
3836
  processUuid: deferredCodexProcessUuid,
3787
3837
  processBirth: deferredCodexProcessBirth,
3788
- inventory: deferredCodexCandidateInventory
3838
+ inventory: deferredCodexCandidateInventory,
3839
+ sourceRolloutAuthority: deferredCodexSourceRolloutAuthority
3840
+ }) &&
3841
+ (deferredCodexSourceRolloutAuthority === "present" ||
3842
+ Boolean(deferredCodexSourceAbandonmentFingerprint)) &&
3843
+ codexCandidateInventoryHasNoOtherManagedClaim({
3844
+ storeDir,
3845
+ inventory: deferredCodexCandidateInventory,
3846
+ sourceSessionId: deferredCodexSource.session_id,
3847
+ includeDetached: deferredCodexSourceRolloutAuthority ===
3848
+ "explicitly_abandoned_predecessor"
3789
3849
  }) &&
3790
3850
  candidateSourceTransitionHistoryIsTerminal(storeDir, deferredCodexSource));
3791
3851
  const deferredCodexCandidateRouteNeeded = Boolean(deferredCodexCandidateInventory &&
3792
3852
  (deferredCodexCandidateInventory.status === "unbound" ||
3793
3853
  nativeIdentityObservation?.status === "unavailable" ||
3794
- deferredCodexSource?.binding?.native_process.rollout === undefined));
3854
+ deferredCodexSource?.binding?.native_process.rollout === undefined ||
3855
+ deferredCodexLatentClearResumeFingerprint !== undefined));
3795
3856
  const deferredCodexForegroundEligible = Boolean(mutationsAllowed &&
3796
3857
  !terminalHasNonterminalDeferredTransfer &&
3797
3858
  deferredCodexSource &&
@@ -3799,8 +3860,12 @@ function terminalFirstListProjection({ storeDir, terminals, managedSessions, ses
3799
3860
  hasCanonicalTerminalEndpoint(terminalControl) &&
3800
3861
  discoveredOwnership.state === "none" &&
3801
3862
  unresolvedSessionClaims.length === 0 &&
3802
- matchingSessions.length === 1 &&
3803
- conflictingBoundSessionClaims.length === 0 &&
3863
+ ((matchingSessions.length === 1 &&
3864
+ conflictingBoundSessionClaims.length === 0) ||
3865
+ (deferredCodexSourceRolloutAuthority ===
3866
+ "explicitly_abandoned_predecessor" &&
3867
+ matchingSessions.length === 0 &&
3868
+ conflictingBoundSessionClaims.length === 1)) &&
3804
3869
  deferredCodexProcessUuid &&
3805
3870
  deferredCodexProcessBirth &&
3806
3871
  deferredCodexWorkspace &&
@@ -3816,8 +3881,7 @@ function terminalFirstListProjection({ storeDir, terminals, managedSessions, ses
3816
3881
  terminal.approval_state.blocked === true) &&
3817
3882
  !deferredCodexSourceActiveElsewhere &&
3818
3883
  terminalControl.capabilities.includes("send_keys") &&
3819
- terminalControl.capabilities.includes("screen_status") &&
3820
- isRecord(rawActions.send));
3884
+ terminalControl.capabilities.includes("screen_status"));
3821
3885
  const deferredCodexForegroundToken = deferredCodexForegroundEligible &&
3822
3886
  deferredCodexSource &&
3823
3887
  terminalControl &&
@@ -3834,6 +3898,10 @@ function terminalFirstListProjection({ storeDir, terminals, managedSessions, ses
3834
3898
  processBirth: deferredCodexProcessBirth,
3835
3899
  sourceSession: deferredCodexSource,
3836
3900
  dispatchSnapshot: deferredCodexDispatchSnapshot,
3901
+ sourceTurnHistory: deferredCodexCandidateSourceTurnHistory,
3902
+ candidateContextFingerprint: deferredCodexLatentClearResumeFingerprint,
3903
+ sourceRolloutAuthority: deferredCodexSourceRolloutAuthority,
3904
+ sourceAbandonmentFingerprint: deferredCodexSourceAbandonmentFingerprint,
3837
3905
  ...(deferredCodexCandidateInventory
3838
3906
  ? { candidateInventory: deferredCodexCandidateInventory }
3839
3907
  : {})
@@ -4102,6 +4170,21 @@ function terminalFirstListProjection({ storeDir, terminals, managedSessions, ses
4102
4170
  : ownership.state === "conflict"
4103
4171
  ? {
4104
4172
  ...safeTerminalActionsDuringConflict(sessionAwareRawActions),
4173
+ ...(deferredCodexForegroundToken &&
4174
+ deferredCodexSourceRolloutAuthority ===
4175
+ "explicitly_abandoned_predecessor"
4176
+ ? {
4177
+ send: {
4178
+ ...verifiedEmptyRawSendAction,
4179
+ arguments: {
4180
+ ...(isRecord(verifiedEmptyRawSendAction.arguments)
4181
+ ? verifiedEmptyRawSendAction.arguments
4182
+ : {}),
4183
+ expected_terminal_token: deferredCodexForegroundToken
4184
+ }
4185
+ }
4186
+ }
4187
+ : {}),
4105
4188
  ...(externalHandoffAdoptable
4106
4189
  ? {
4107
4190
  send: {
@@ -4132,15 +4215,14 @@ function terminalFirstListProjection({ storeDir, terminals, managedSessions, ses
4132
4215
  ? { reconcile_binding: reconcileBindingAction }
4133
4216
  : {})
4134
4217
  }
4135
- : deferredCodexForegroundToken &&
4136
- isRecord(rawActions.send)
4218
+ : deferredCodexForegroundToken
4137
4219
  ? {
4138
4220
  ...nonOwnerRawActions,
4139
4221
  send: {
4140
- ...rawActions.send,
4222
+ ...verifiedEmptyRawSendAction,
4141
4223
  arguments: {
4142
- ...(isRecord(rawActions.send.arguments)
4143
- ? rawActions.send.arguments
4224
+ ...(isRecord(verifiedEmptyRawSendAction.arguments)
4225
+ ? verifiedEmptyRawSendAction.arguments
4144
4226
  : {}),
4145
4227
  expected_terminal_token: deferredCodexForegroundToken
4146
4228
  }
@@ -4183,9 +4265,19 @@ function terminalFirstListProjection({ storeDir, terminals, managedSessions, ses
4183
4265
  "while the same terminal process is at an exact empty prompt",
4184
4266
  recovery: "use only the exact snapshot-bound send action listed for this terminal"
4185
4267
  }
4186
- : ownership.state === "conflict"
4187
- ? ownership.conflict
4188
- : undefined;
4268
+ : ownership.state === "conflict" &&
4269
+ deferredCodexForegroundToken &&
4270
+ deferredCodexSourceRolloutAuthority ===
4271
+ "explicitly_abandoned_predecessor"
4272
+ ? {
4273
+ ...ownership.conflict,
4274
+ kind: "explicitly_abandoned_predecessor_adoptable",
4275
+ reason: "the explicitly abandoned Codex predecessor rollout is no longer open and the current exact rollout inventory is unclaimed",
4276
+ recovery: "use only the exact snapshot-bound follow-current send action listed for this terminal"
4277
+ }
4278
+ : ownership.state === "conflict"
4279
+ ? ownership.conflict
4280
+ : undefined;
4189
4281
  return {
4190
4282
  ...publicTerminal,
4191
4283
  ...authoritativeIdentity,
@@ -5523,13 +5615,13 @@ function managedListApprovalState(conversation) {
5523
5615
  }
5524
5616
  function listActionContracts() {
5525
5617
  return {
5526
- version: 12,
5618
+ version: 14,
5527
5619
  instructions: [
5528
5620
  "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.",
5529
5621
  "An existing managed session's ordinary send targets session_id and creates a new turn. A turn id is never an ordinary send target.",
5530
5622
  "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.",
5531
5623
  "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.",
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.",
5624
+ "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, a quiescent rollout-backed source whose complete pinned open-rollout inventory cannot identify one foreground candidate, or a supported Codex /clear foreground hint observed before its new rollout materializes 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. An explicitly closed uncertain Turn may authorize only a future candidate send when its exact resolved close ledger, append-only uncertain receipt, frozen Turn history, and complete current rollout inventory prove that the old bound rollout is absent and every candidate is unclaimed; close never forges the lost callback, and uncertain submissions cannot be renewed. Other binding conflicts remain fail-closed and may expose only exact low-level reconcile_binding recovery.",
5533
5625
  "List resumable threads before resume; use only a complete native_thread_id and the action returned for that candidate.",
5534
5626
  "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.",
5535
5627
  "Use respond only for an in-flight turn that is explicitly waiting for OpenClaw.",
@@ -5604,7 +5696,7 @@ function listActionContracts() {
5604
5696
  ],
5605
5697
  unsupported: ["timeoutSeconds"],
5606
5698
  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.",
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.",
5699
+ candidate_rollout_deferred_scope: "A quiescent rollout-backed Codex source may use a listed selector/token send when AKK pins the complete exact candidate inventory but cannot prove one foreground UUID, or when a supported /clear foreground hint signals that the sole old rollout is no longer sufficient authority. 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. Explicit close can abandon an uncertain receipt for future-send liveness only while the exact resolved close ledger, append-only receipt, frozen history, absent old rollout, and unclaimed candidate set remain authoritative; it never synthesizes callback delivery.",
5608
5700
  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."
5609
5701
  },
5610
5702
  new_thread: {
@@ -5852,7 +5944,9 @@ function availableListActions(entry, { conversation } = {}) {
5852
5944
  requires_user_intent: true
5853
5945
  };
5854
5946
  }
5855
- if (terminalBridgeReady && entry.status === "stalled") {
5947
+ if (terminalBridgeReady &&
5948
+ entry.status === "stalled" &&
5949
+ terminalBridgeSubmission(conversation)?.status !== "uncertain") {
5856
5950
  actions.renew = {
5857
5951
  tool: "agent_knock_knock_renew",
5858
5952
  arguments: targetArguments
@@ -6514,6 +6608,14 @@ function codexAllowedCompanionSetForManagedSession({ storeDir, session }) {
6514
6608
  });
6515
6609
  return { primary: selectedPrimary, additional };
6516
6610
  }
6611
+ function codexCompanionsPresentInOpenRootInventory(companions, inventory) {
6612
+ const present = [companions.primary, ...companions.additional].filter((candidate) => Boolean(candidate &&
6613
+ inventory.roots.some((root) => nativeIdentityMatchesCodexPreMaterialization(root, candidate))));
6614
+ return {
6615
+ primary: present[0],
6616
+ additional: present.slice(1)
6617
+ };
6618
+ }
6517
6619
  function codexManagedIdentityResolutionContext({ storeDir, terminal }) {
6518
6620
  const claimedSession = soleBoundManagedSessionClaimForTerminal(storeDir, terminal);
6519
6621
  const companions = claimedSession
@@ -7035,8 +7137,11 @@ function exactBoundCodexStatusCardSourceForDeferredSend({ session, terminalId, t
7035
7137
  liveProcessBirth: processBirth
7036
7138
  }) === "same");
7037
7139
  }
7038
- function exactCodexCandidateInventoryForDeferredSend({ inventory, sourceSession, pid, workspace, processUuid, processBirth }) {
7140
+ function exactCodexCandidateInventoryForDeferredSend({ inventory, sourceSession, pid, workspace, processUuid, processBirth, sourceRolloutAuthority = "present" }) {
7039
7141
  const sourceNativeThreadId = sourceSession.binding?.native_thread_id;
7142
+ const sourceRollout = sourceSession.binding?.native_process.rollout;
7143
+ const sourceRootPresent = Boolean(sourceNativeThreadId && inventory.roots.some((root) => root.sessionId.toLowerCase() === sourceNativeThreadId.toLowerCase()));
7144
+ const sourceRolloutPresent = Boolean(sourceRollout && inventory.roots.some((root) => exactNativeRolloutMatches(root.rollout, sourceRollout)));
7040
7145
  return Boolean(inventory.roots.length > 0 &&
7041
7146
  inventory.pid === pid &&
7042
7147
  inventory.processUuid === processUuid &&
@@ -7044,11 +7149,14 @@ function exactCodexCandidateInventoryForDeferredSend({ inventory, sourceSession,
7044
7149
  inventory.cwd &&
7045
7150
  path.resolve(inventory.cwd) === path.resolve(workspace) &&
7046
7151
  /^[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 }) {
7152
+ (sourceRolloutAuthority === "explicitly_abandoned_predecessor"
7153
+ ? isExactNativeThreadId(sourceNativeThreadId) &&
7154
+ isCompleteNativeRollout(sourceRollout) &&
7155
+ !sourceRootPresent &&
7156
+ !sourceRolloutPresent
7157
+ : sourceRollout === undefined || sourceRootPresent));
7158
+ }
7159
+ function exactBoundCodexCandidateSourceForDeferredSend({ session, terminalId, terminalControl, pid, workspace, processUuid, processBirth, inventory, sourceRolloutAuthority = "present" }) {
7052
7160
  const binding = session.binding;
7053
7161
  const sourceNativeThreadId = binding?.native_thread_id;
7054
7162
  const sourceRollout = binding?.native_process.rollout;
@@ -7058,8 +7166,11 @@ function exactBoundCodexCandidateSourceForDeferredSend({ session, terminalId, te
7058
7166
  binding &&
7059
7167
  isExactNativeThreadId(sourceNativeThreadId) &&
7060
7168
  isCompleteNativeRollout(sourceRollout) &&
7061
- inventoryRoot &&
7062
- exactNativeRolloutMatches(inventoryRoot.rollout, sourceRollout) &&
7169
+ (sourceRolloutAuthority === "explicitly_abandoned_predecessor"
7170
+ ? inventoryRoot === undefined &&
7171
+ !inventory.roots.some((root) => exactNativeRolloutMatches(root.rollout, sourceRollout))
7172
+ : inventoryRoot !== undefined &&
7173
+ exactNativeRolloutMatches(inventoryRoot.rollout, sourceRollout)) &&
7063
7174
  terminalControlAliasMatches(binding.terminal_id, binding.terminal_control, terminalId, terminalControl) &&
7064
7175
  path.resolve(session.workspace) === path.resolve(workspace) &&
7065
7176
  processIncarnationRelationship({
@@ -7074,22 +7185,68 @@ function exactBoundCodexCandidateSourceForDeferredSend({ session, terminalId, te
7074
7185
  pid,
7075
7186
  workspace,
7076
7187
  processUuid,
7077
- processBirth
7188
+ processBirth,
7189
+ sourceRolloutAuthority
7078
7190
  }));
7079
7191
  }
7192
+ function codexCandidateInventoryHasNoOtherManagedClaim({ storeDir, inventory, sourceSessionId, includeDetached = true }) {
7193
+ const candidateIds = new Set(inventory.roots.map((root) => root.sessionId.toLowerCase()));
7194
+ return !listManagedSessions(storeDir).some((session) => session.session_id !== sourceSessionId &&
7195
+ (includeDetached || session.status !== "detached") &&
7196
+ isExactNativeThreadId(session.binding?.native_thread_id) &&
7197
+ candidateIds.has(session.binding.native_thread_id.toLowerCase()));
7198
+ }
7080
7199
  function deferredCandidateSourceTurnHistory(storeDir, session) {
7200
+ const binding = session.binding;
7201
+ if (!binding?.native_thread_id) {
7202
+ return undefined;
7203
+ }
7204
+ const turns = managedTurnsForSession(storeDir, session.session_id);
7205
+ if (turns.some((turn) => {
7206
+ const callbackDelivered = isRecord(turn.callback_delivery) &&
7207
+ turn.callback_delivery.status === "delivered";
7208
+ const explicitlyAbandonedUncertain = exactExplicitlyAbandonedUncertainCandidateTurn({
7209
+ storeDir,
7210
+ session,
7211
+ turn
7212
+ });
7213
+ return (!TERMINAL_DISPATCH_RELEASE_STATUSES.has(turn.status) ||
7214
+ managedTurnNeedsAttention(turn) ||
7215
+ (isRecord(turn.callback_delivery) &&
7216
+ !callbackDelivered) ||
7217
+ (Boolean(turn.gateway_method) &&
7218
+ !callbackDelivered &&
7219
+ !explicitlyAbandonedUncertain) ||
7220
+ stringValue(turn.terminal_binding_id) !== binding.binding_id ||
7221
+ Number(turn.terminal_binding_generation) !== binding.generation ||
7222
+ (stringValue(turn.native_thread_id) ??
7223
+ stringValue(isRecord(turn.native_session_takeover)
7224
+ ? turn.native_session_takeover.terminal_agent_session_id
7225
+ : undefined)) !== binding.native_thread_id);
7226
+ })) {
7227
+ return undefined;
7228
+ }
7229
+ return turns
7230
+ .map((turn) => ({
7231
+ turn_id: turnIdForConversation(turn),
7232
+ status: turn.status,
7233
+ updated_at: required(stringValue(turn.updated_at), `managed Turn ${turnIdForConversation(turn)} updated_at is unavailable`),
7234
+ binding_id: binding.binding_id,
7235
+ binding_generation: binding.generation,
7236
+ native_thread_id: binding.native_thread_id,
7237
+ turn_fingerprint: createHash("sha256")
7238
+ .update(JSON.stringify(turn))
7239
+ .digest("hex")
7240
+ }))
7241
+ .sort((left, right) => left.turn_id.localeCompare(right.turn_id));
7242
+ }
7243
+ function frozenCandidateSourceTurnHistory(storeDir, session) {
7081
7244
  const binding = session.binding;
7082
7245
  if (!binding?.native_thread_id) {
7083
7246
  return undefined;
7084
7247
  }
7085
7248
  const turns = managedTurnsForSession(storeDir, session.session_id);
7086
7249
  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
7250
  stringValue(turn.terminal_binding_id) !== binding.binding_id ||
7094
7251
  Number(turn.terminal_binding_generation) !== binding.generation ||
7095
7252
  (stringValue(turn.native_thread_id) ??
@@ -7098,8 +7255,7 @@ function deferredCandidateSourceTurnHistory(storeDir, session) {
7098
7255
  : undefined)) !== binding.native_thread_id)) {
7099
7256
  return undefined;
7100
7257
  }
7101
- return turns
7102
- .map((turn) => ({
7258
+ return turns.map((turn) => ({
7103
7259
  turn_id: turnIdForConversation(turn),
7104
7260
  status: turn.status,
7105
7261
  updated_at: required(stringValue(turn.updated_at), `managed Turn ${turnIdForConversation(turn)} updated_at is unavailable`),
@@ -7109,8 +7265,220 @@ function deferredCandidateSourceTurnHistory(storeDir, session) {
7109
7265
  turn_fingerprint: createHash("sha256")
7110
7266
  .update(JSON.stringify(turn))
7111
7267
  .digest("hex")
7112
- }))
7113
- .sort((left, right) => left.turn_id.localeCompare(right.turn_id));
7268
+ })).sort((left, right) => left.turn_id.localeCompare(right.turn_id));
7269
+ }
7270
+ function exactExplicitlyAbandonedUncertainCandidateTurn({ storeDir, session, turn }) {
7271
+ return explicitlyAbandonedUncertainCandidateTurnFingerprint({
7272
+ storeDir,
7273
+ session,
7274
+ turn
7275
+ }) !== undefined;
7276
+ }
7277
+ function explicitlyAbandonedUncertainCandidateTurnProof({ storeDir, session, turn, ledgerOverride, requireResolvedTopLevel = true }) {
7278
+ if (turn.status !== "closed" ||
7279
+ isRecord(turn.callback_delivery) ||
7280
+ isRecord(turn.terminal_bridge_completion_claim) ||
7281
+ !stringValue(turn.closed_at) ||
7282
+ !stringValue(turn.close_reason)) {
7283
+ return undefined;
7284
+ }
7285
+ const takeover = isRecord(turn.native_session_takeover)
7286
+ ? turn.native_session_takeover
7287
+ : undefined;
7288
+ const submission = terminalBridgeSubmission(turn);
7289
+ const terminalControl = terminalControlFromTakeover(takeover);
7290
+ const messageId = stringValue(submission?.message_id);
7291
+ if (!takeover ||
7292
+ isRecord(takeover.terminal_bridge_completion_claim) ||
7293
+ !terminalControl ||
7294
+ submission?.status !== "uncertain" ||
7295
+ submission.safe_to_retry === true ||
7296
+ !messageId ||
7297
+ stringValue(takeover.terminal_bridge_message_id) !== messageId ||
7298
+ stringValue(submission.session_id) !== session.session_id ||
7299
+ stringValue(submission.turn_id) !== turnIdForConversation(turn)) {
7300
+ return undefined;
7301
+ }
7302
+ const canonical = pathsForConversation(turn.conversation_id, storeDir);
7303
+ if (path.resolve(stringValue(turn.state_path) ?? "") !==
7304
+ path.resolve(canonical.statePath) ||
7305
+ path.resolve(stringValue(turn.event_log_path) ?? "") !==
7306
+ path.resolve(canonical.logPath) ||
7307
+ path.resolve(managedSessionStoreDirForConversation(turn) ?? "") !==
7308
+ path.resolve(storeDir)) {
7309
+ return undefined;
7310
+ }
7311
+ let ledger;
7312
+ try {
7313
+ ledger = ledgerOverride ?? loadTerminalBridgeDispatchLedger(terminalControl);
7314
+ }
7315
+ catch {
7316
+ return undefined;
7317
+ }
7318
+ if (!ledger ||
7319
+ !terminalDispatchRecordMatchesControl(ledger, terminalControl) ||
7320
+ (requireResolvedTopLevel &&
7321
+ (ledger.status !== "resolved" ||
7322
+ ledger.reason !== "conversation explicitly closed by request" ||
7323
+ !validTimestampMs(ledger.resolved_at) ||
7324
+ stringValue(ledger.conversation_id) !== turn.conversation_id ||
7325
+ stringValue(ledger.session_id) !== session.session_id ||
7326
+ stringValue(ledger.turn_id) !== turnIdForConversation(turn) ||
7327
+ stringValue(ledger.message_id) !== messageId ||
7328
+ stringValue(ledger.request_hash) !==
7329
+ stringValue(submission.request_hash) ||
7330
+ stringValue(ledger.binding_id) !==
7331
+ stringValue(turn.terminal_binding_id) ||
7332
+ Number(ledger.binding_generation) !==
7333
+ Number(turn.terminal_binding_generation) ||
7334
+ path.resolve(stringValue(ledger.state_path) ?? "") !==
7335
+ path.resolve(canonical.statePath) ||
7336
+ path.resolve(stringValue(ledger.event_log_path) ?? "") !==
7337
+ path.resolve(canonical.logPath) ||
7338
+ path.resolve(stringValue(ledger.store_dir) ?? "") !==
7339
+ path.resolve(storeDir)))) {
7340
+ return undefined;
7341
+ }
7342
+ let receipt;
7343
+ try {
7344
+ const matchingReceipts = terminalLedgerReceiptHistory(ledger).filter((candidate) => stringValue(candidate.message_id) === messageId);
7345
+ receipt = matchingReceipts.length === 1
7346
+ ? matchingReceipts[0]
7347
+ : undefined;
7348
+ }
7349
+ catch {
7350
+ return undefined;
7351
+ }
7352
+ if (!receipt ||
7353
+ receipt.status !== "uncertain" ||
7354
+ receipt.safe_to_retry === true ||
7355
+ !validTimestampMs(receipt.uncertain_at) ||
7356
+ !terminalDispatchRecordMatchesControl(receipt, terminalControl) ||
7357
+ stringValue(receipt.conversation_id) !== turn.conversation_id ||
7358
+ stringValue(receipt.session_id) !== session.session_id ||
7359
+ stringValue(receipt.turn_id) !== turnIdForConversation(turn) ||
7360
+ stringValue(receipt.request_hash) !== stringValue(submission.request_hash) ||
7361
+ stringValue(receipt.binding_id) !== stringValue(turn.terminal_binding_id) ||
7362
+ Number(receipt.binding_generation) !==
7363
+ Number(turn.terminal_binding_generation) ||
7364
+ path.resolve(stringValue(receipt.state_path) ?? "") !==
7365
+ path.resolve(canonical.statePath) ||
7366
+ path.resolve(stringValue(receipt.event_log_path) ?? "") !==
7367
+ path.resolve(canonical.logPath) ||
7368
+ path.resolve(stringValue(receipt.store_dir) ?? "") !== path.resolve(storeDir)) {
7369
+ return undefined;
7370
+ }
7371
+ let closeEvent;
7372
+ try {
7373
+ closeEvent = readExistingEvents(canonical.logPath).find((event) => event.event === "conversation_closed" &&
7374
+ event.conversation_id === turn.conversation_id &&
7375
+ event.status === "closed" &&
7376
+ event.ts === turn.closed_at &&
7377
+ event.reason === turn.close_reason);
7378
+ }
7379
+ catch {
7380
+ return undefined;
7381
+ }
7382
+ if (!closeEvent) {
7383
+ return undefined;
7384
+ }
7385
+ // Hash the immutable close event and append-only uncertain receipt, not the
7386
+ // mutable top-level terminal ledger that the next dispatch will replace.
7387
+ return {
7388
+ turnId: turnIdForConversation(turn),
7389
+ messageId,
7390
+ turnFingerprint: createHash("sha256")
7391
+ .update(JSON.stringify(turn))
7392
+ .digest("hex"),
7393
+ closeEvent,
7394
+ uncertainReceipt: receipt
7395
+ };
7396
+ }
7397
+ function explicitlyAbandonedUncertainCandidateTurnFingerprint(args) {
7398
+ const proof = explicitlyAbandonedUncertainCandidateTurnProof(args);
7399
+ return proof
7400
+ ? terminalActionFingerprint({
7401
+ kind: "explicitly_abandoned_uncertain_codex_turn",
7402
+ source_session_id: args.session.session_id,
7403
+ source_revision: managedSessionRevision(args.session),
7404
+ source_binding_token: managedSessionBindingToken(args.session),
7405
+ turn_id: proof.turnId,
7406
+ turn_fingerprint: proof.turnFingerprint,
7407
+ close_event: proof.closeEvent,
7408
+ uncertain_receipt: proof.uncertainReceipt
7409
+ })
7410
+ : undefined;
7411
+ }
7412
+ function explicitlyAbandonedCandidateSourceFingerprint({ storeDir, session, sourceTurnHistory, dispatchSnapshot, sourceRevision = managedSessionRevision(session), sourceBindingToken = managedSessionBindingToken(session), ledgerOverride, requireResolvedTopLevel = true }) {
7413
+ if (dispatchSnapshot.status !== "resolved") {
7414
+ return undefined;
7415
+ }
7416
+ const abandonedTurnProofs = managedTurnsForSession(storeDir, session.session_id).flatMap((turn) => {
7417
+ const proof = explicitlyAbandonedUncertainCandidateTurnProof({
7418
+ storeDir,
7419
+ session,
7420
+ turn,
7421
+ ledgerOverride,
7422
+ requireResolvedTopLevel
7423
+ });
7424
+ return proof
7425
+ ? [{
7426
+ turn_id: proof.turnId,
7427
+ message_id: proof.messageId,
7428
+ turn_fingerprint: proof.turnFingerprint,
7429
+ close_event: proof.closeEvent,
7430
+ uncertain_receipt: proof.uncertainReceipt
7431
+ }]
7432
+ : [];
7433
+ }).sort((left, right) => left.turn_id.localeCompare(right.turn_id));
7434
+ if (abandonedTurnProofs.length === 0) {
7435
+ return undefined;
7436
+ }
7437
+ return terminalActionFingerprint({
7438
+ kind: "explicitly_abandoned_codex_predecessor",
7439
+ source_session_id: session.session_id,
7440
+ source_revision: sourceRevision,
7441
+ source_binding_token: sourceBindingToken,
7442
+ source_turn_history: sourceTurnHistory,
7443
+ previous_dispatch_snapshot: dispatchSnapshot,
7444
+ abandoned_turns: abandonedTurnProofs
7445
+ });
7446
+ }
7447
+ function assertFrozenExplicitlyAbandonedPredecessorAuthority({ storeDir, transfer, terminalControl }) {
7448
+ if (transfer.source_rollout_authority !==
7449
+ "explicitly_abandoned_predecessor") {
7450
+ return;
7451
+ }
7452
+ const source = loadManagedSession(storeDir, transfer.source_session_id);
7453
+ const sourceAsBound = {
7454
+ ...source,
7455
+ status: "bound",
7456
+ binding: transfer.source_before_binding,
7457
+ last_transition_id: transfer.source_previous_last_transition_id
7458
+ };
7459
+ const history = frozenCandidateSourceTurnHistory(storeDir, sourceAsBound);
7460
+ const ledger = loadTerminalBridgeDispatchLedger(terminalControl);
7461
+ const fingerprint = history && ledger
7462
+ ? explicitlyAbandonedCandidateSourceFingerprint({
7463
+ storeDir,
7464
+ session: sourceAsBound,
7465
+ sourceTurnHistory: history,
7466
+ dispatchSnapshot: {
7467
+ status: transfer.previous_dispatch_status,
7468
+ fingerprint: transfer.previous_dispatch_fingerprint
7469
+ },
7470
+ sourceRevision: transfer.source_expected_revision,
7471
+ sourceBindingToken: transfer.source_binding_token,
7472
+ ledgerOverride: ledger,
7473
+ requireResolvedTopLevel: false
7474
+ })
7475
+ : undefined;
7476
+ if (!history ||
7477
+ JSON.stringify(history) !== JSON.stringify(transfer.source_turn_history) ||
7478
+ fingerprint !== transfer.source_abandonment_fingerprint) {
7479
+ throw new Error(`deferred foreground transfer ${transfer.transfer_id} lost its exact ` +
7480
+ "explicitly abandoned predecessor authority");
7481
+ }
7114
7482
  }
7115
7483
  function candidateSourceTransitionHistoryIsTerminal(storeDir, session) {
7116
7484
  if (managedSessionHasUnresolvedNativeTransition(storeDir, session)) {
@@ -7129,7 +7497,7 @@ function candidateSourceTransitionHistoryIsTerminal(storeDir, session) {
7129
7497
  return false;
7130
7498
  }
7131
7499
  }
7132
- function deferredCodexForegroundBindingToken({ terminalId, terminalControl, pid, workspace, processUuid, processBirth, sourceSession, dispatchSnapshot, candidateInventory }) {
7500
+ function deferredCodexForegroundBindingToken({ terminalId, terminalControl, pid, workspace, processUuid, processBirth, sourceSession, dispatchSnapshot, candidateInventory, sourceTurnHistory, candidateContextFingerprint, sourceRolloutAuthority = "present", sourceAbandonmentFingerprint }) {
7133
7501
  const terminalToken = unmanagedTerminalBindingToken({
7134
7502
  terminalId,
7135
7503
  terminalControl,
@@ -7147,18 +7515,40 @@ function deferredCodexForegroundBindingToken({ terminalId, terminalControl, pid,
7147
7515
  candidate_native_thread_ids: candidateInventory.roots.map((root) => root.sessionId)
7148
7516
  }
7149
7517
  : undefined;
7518
+ const sourceTurnHistoryFingerprint = sourceTurnHistory
7519
+ ? createHash("sha256")
7520
+ .update(JSON.stringify(sourceTurnHistory))
7521
+ .digest("hex")
7522
+ : undefined;
7150
7523
  return createHash("sha256")
7151
7524
  .update(JSON.stringify({
7152
- version: candidateAuthority ? 3 : 2,
7525
+ version: candidateAuthority ? 5 : 2,
7153
7526
  kind: "deferred_codex_foreground_binding",
7154
7527
  terminal_token: terminalToken,
7528
+ composer_state: "styled_empty",
7155
7529
  source_session_id: sourceSession.session_id,
7156
7530
  source_revision: managedSessionRevision(sourceSession),
7157
7531
  source_binding_token: managedSessionBindingToken(sourceSession),
7158
7532
  terminal_dispatch_snapshot: dispatchSnapshot,
7159
- observation: candidateAuthority
7160
- ? "exact_unbound_open_root_inventory"
7161
- : "verified_absent",
7533
+ observation: candidateContextFingerprint
7534
+ ? "latent_codex_thread_reset"
7535
+ : candidateAuthority
7536
+ ? "exact_open_root_inventory"
7537
+ : "verified_absent",
7538
+ ...(sourceTurnHistoryFingerprint
7539
+ ? { source_turn_history_fingerprint: sourceTurnHistoryFingerprint }
7540
+ : {}),
7541
+ ...(candidateContextFingerprint
7542
+ ? { candidate_context_fingerprint: candidateContextFingerprint }
7543
+ : {}),
7544
+ ...(candidateAuthority
7545
+ ? { source_rollout_authority: sourceRolloutAuthority }
7546
+ : {}),
7547
+ ...(sourceAbandonmentFingerprint
7548
+ ? {
7549
+ source_abandonment_fingerprint: sourceAbandonmentFingerprint
7550
+ }
7551
+ : {}),
7162
7552
  ...(candidateAuthority ?? {})
7163
7553
  }))
7164
7554
  .digest("hex");
@@ -7484,7 +7874,8 @@ async function assertDeferredCodexForegroundBindingBoundary({ options, boundary,
7484
7874
  pid: boundary.terminal.pid,
7485
7875
  workspace: required(boundary.terminal.terminalControl.currentPath, "deferred Codex terminal workspace is unavailable"),
7486
7876
  processUuid: boundary.processUuid,
7487
- processBirth: boundary.processBirth
7877
+ processBirth: boundary.processBirth,
7878
+ sourceRolloutAuthority: boundary.sourceRolloutAuthority
7488
7879
  })) {
7489
7880
  throw new Error("the exact Codex open-root inventory changed; refresh AKK list before sending");
7490
7881
  }
@@ -7526,7 +7917,27 @@ async function assertDeferredCodexForegroundBindingBoundary({ options, boundary,
7526
7917
  }
7527
7918
  }
7528
7919
  else {
7529
- const history = deferredCandidateSourceTurnHistory(storeDir, sourceAsBound);
7920
+ const history = expectedSourceStatus === "bound"
7921
+ ? deferredCandidateSourceTurnHistory(storeDir, sourceAsBound)
7922
+ : frozenCandidateSourceTurnHistory(storeDir, sourceAsBound);
7923
+ const currentAbandonmentFingerprint = boundary.sourceRolloutAuthority ===
7924
+ "explicitly_abandoned_predecessor" &&
7925
+ history
7926
+ ? explicitlyAbandonedCandidateSourceFingerprint({
7927
+ storeDir,
7928
+ session: sourceAsBound,
7929
+ sourceTurnHistory: history,
7930
+ dispatchSnapshot: boundary.previousDispatchSnapshot,
7931
+ sourceRevision: boundary.sourceBoundRevision,
7932
+ sourceBindingToken: boundary.sourceBoundBindingToken,
7933
+ ...(expectedSourceStatus === "transitioning"
7934
+ ? {
7935
+ ledgerOverride: loadTerminalBridgeDispatchLedger(boundary.terminal.terminalControl),
7936
+ requireResolvedTopLevel: false
7937
+ }
7938
+ : {})
7939
+ })
7940
+ : undefined;
7530
7941
  if (!boundary.candidateAcceptanceAnchor ||
7531
7942
  !history ||
7532
7943
  JSON.stringify(history) !== JSON.stringify(boundary.sourceTurnHistory) ||
@@ -7538,8 +7949,15 @@ async function assertDeferredCodexForegroundBindingBoundary({ options, boundary,
7538
7949
  workspace: required(boundary.terminal.terminalControl.currentPath, "deferred Codex terminal workspace is unavailable"),
7539
7950
  processUuid: boundary.processUuid,
7540
7951
  processBirth: boundary.processBirth,
7541
- inventory: required(candidateInventoryForBoundary, "deferred Codex candidate inventory is unavailable")
7952
+ inventory: required(candidateInventoryForBoundary, "deferred Codex candidate inventory is unavailable"),
7953
+ sourceRolloutAuthority: boundary.sourceRolloutAuthority
7542
7954
  }) ||
7955
+ (boundary.sourceRolloutAuthority ===
7956
+ "explicitly_abandoned_predecessor" &&
7957
+ (!boundary.sourceAbandonmentFingerprint ||
7958
+ currentAbandonmentFingerprint !==
7959
+ boundary.sourceAbandonmentFingerprint ||
7960
+ boundary.previousDispatchSnapshot.status !== "resolved")) ||
7543
7961
  !candidateSourceTransitionHistoryIsTerminal(storeDir, sourceAsBound)) {
7544
7962
  throw new Error("the deferred Codex candidate source history or rollout authority changed");
7545
7963
  }
@@ -7560,6 +7978,16 @@ async function assertDeferredCodexForegroundBindingBoundary({ options, boundary,
7560
7978
  physicalOnly: boundary.candidateAcceptanceAnchor !== undefined
7561
7979
  })
7562
7980
  });
7981
+ if (requireEmptyComposer && boundary.candidateContextFingerprint) {
7982
+ const currentContextFingerprint = codexLatentClearResumeFingerprint({
7983
+ screen: status.screen.excerpt,
7984
+ sourceNativeThreadId: source.binding?.native_thread_id,
7985
+ agentVersion: agentVersionForRunningProcess("codex", boundary.terminal.pid, options)
7986
+ });
7987
+ if (currentContextFingerprint !== boundary.candidateContextFingerprint) {
7988
+ throw new Error("the Codex /clear foreground hint changed; refresh AKK list before sending");
7989
+ }
7990
+ }
7563
7991
  if (status.reachable !== true ||
7564
7992
  status.approval_state.blocked === true ||
7565
7993
  !["idle", "unknown"].includes(status.activity_state)) {
@@ -7596,6 +8024,35 @@ async function prepareDeferredCodexForegroundBinding({ options, terminal, source
7596
8024
  "candidate_rollout_quiescent"
7597
8025
  ? deferredCandidateSourceTurnHistory(storeDir, sourceSession)
7598
8026
  : undefined;
8027
+ const dispatchSnapshot = deferredCodexForegroundDispatchSnapshot(terminal.terminalControl);
8028
+ const sourceRolloutPresent = Boolean(candidateInventory &&
8029
+ sourceSession.binding.native_process.rollout &&
8030
+ candidateInventory.roots.some((root) => root.sessionId.toLowerCase() ===
8031
+ sourceSession.binding?.native_thread_id?.toLowerCase() &&
8032
+ exactNativeRolloutMatches(root.rollout, sourceSession.binding.native_process.rollout)));
8033
+ const sourceRolloutAuthority = sourceKind === "candidate_rollout_quiescent" &&
8034
+ candidateInventory &&
8035
+ sourceSession.binding.native_process.rollout &&
8036
+ !sourceRolloutPresent
8037
+ ? "explicitly_abandoned_predecessor"
8038
+ : "present";
8039
+ const sourceAbandonmentFingerprint = sourceRolloutAuthority === "explicitly_abandoned_predecessor" &&
8040
+ candidateSourceTurnHistory
8041
+ ? explicitlyAbandonedCandidateSourceFingerprint({
8042
+ storeDir,
8043
+ session: sourceSession,
8044
+ sourceTurnHistory: candidateSourceTurnHistory,
8045
+ dispatchSnapshot
8046
+ })
8047
+ : undefined;
8048
+ const candidateContextFingerprint = candidateMode &&
8049
+ sourceKind === "candidate_rollout_quiescent"
8050
+ ? await observeCodexLatentClearResumeFingerprint({
8051
+ options,
8052
+ terminal,
8053
+ sourceNativeThreadId: sourceSession.binding.native_thread_id
8054
+ })
8055
+ : undefined;
7599
8056
  const exactSource = !workspace
7600
8057
  ? false
7601
8058
  : sourceKind === "candidate_rollout_quiescent"
@@ -7610,7 +8067,8 @@ async function prepareDeferredCodexForegroundBinding({ options, terminal, source
7610
8067
  workspace,
7611
8068
  processUuid: liveIncarnation.processUuid,
7612
8069
  processBirth: liveIncarnation.processBirth,
7613
- inventory: candidateInventory
8070
+ inventory: candidateInventory,
8071
+ sourceRolloutAuthority
7614
8072
  })
7615
8073
  : exactBoundCodexStatusCardSourceForDeferredSend({
7616
8074
  session: sourceSession,
@@ -7631,7 +8089,10 @@ async function prepareDeferredCodexForegroundBinding({ options, terminal, source
7631
8089
  !exactSource) {
7632
8090
  return undefined;
7633
8091
  }
7634
- const dispatchSnapshot = deferredCodexForegroundDispatchSnapshot(terminal.terminalControl);
8092
+ if (sourceRolloutAuthority === "explicitly_abandoned_predecessor" &&
8093
+ !sourceAbandonmentFingerprint) {
8094
+ return undefined;
8095
+ }
7635
8096
  const token = deferredCodexForegroundBindingToken({
7636
8097
  terminalId: terminal.conversationId,
7637
8098
  terminalControl: terminal.terminalControl,
@@ -7641,6 +8102,10 @@ async function prepareDeferredCodexForegroundBinding({ options, terminal, source
7641
8102
  processBirth,
7642
8103
  sourceSession,
7643
8104
  dispatchSnapshot,
8105
+ sourceTurnHistory: candidateSourceTurnHistory,
8106
+ candidateContextFingerprint,
8107
+ sourceRolloutAuthority,
8108
+ sourceAbandonmentFingerprint,
7644
8109
  ...(candidateMode ? { candidateInventory } : {})
7645
8110
  });
7646
8111
  if (expectedToken !== token) {
@@ -7666,6 +8131,13 @@ async function prepareDeferredCodexForegroundBinding({ options, terminal, source
7666
8131
  processBirth,
7667
8132
  previousDispatchSnapshot: dispatchSnapshot,
7668
8133
  sourceKind,
8134
+ sourceRolloutAuthority,
8135
+ ...(sourceAbandonmentFingerprint
8136
+ ? { sourceAbandonmentFingerprint }
8137
+ : {}),
8138
+ ...(candidateContextFingerprint
8139
+ ? { candidateContextFingerprint }
8140
+ : {}),
7669
8141
  ...(sourceSession.last_transition_id
7670
8142
  ? { sourcePreviousLastTransitionId: sourceSession.last_transition_id }
7671
8143
  : {}),
@@ -7690,6 +8162,51 @@ async function prepareDeferredCodexForegroundBinding({ options, terminal, source
7690
8162
  terminalControl: terminal.terminalControl,
7691
8163
  excludedManagedSessionId: sourceSession.session_id
7692
8164
  });
8165
+ if (sourceRolloutAuthority === "explicitly_abandoned_predecessor" &&
8166
+ !codexCandidateInventoryHasNoOtherManagedClaim({
8167
+ storeDir,
8168
+ inventory: required(candidateInventory, "Codex candidate inventory is unavailable"),
8169
+ sourceSessionId: sourceSession.session_id
8170
+ })) {
8171
+ throw new Error("the post-/clear Codex rollout candidate is already claimed by another Session");
8172
+ }
8173
+ if (candidateInventory &&
8174
+ sourceRolloutAuthority === "explicitly_abandoned_predecessor") {
8175
+ for (const root of candidateInventory.roots) {
8176
+ await assertNativeThreadHasExclusiveOwnership({
8177
+ options,
8178
+ agent: "codex",
8179
+ currentPid: terminal.pid,
8180
+ nativeThreadId: root.sessionId,
8181
+ storeDir,
8182
+ terminalControl: terminal.terminalControl,
8183
+ excludedManagedSessionId: root.sessionId.toLowerCase() ===
8184
+ sourceSession.binding.native_thread_id?.toLowerCase()
8185
+ ? sourceSession.session_id
8186
+ : undefined
8187
+ });
8188
+ }
8189
+ }
8190
+ // The source semantic proof is about to be replaced by the transfer's own
8191
+ // top-level dispatch generation. Recheck it immediately before publishing
8192
+ // the prepared transfer so every later phase can rely on the immutable
8193
+ // fingerprint plus the Store protocol-5 source-Turn fence.
8194
+ if (sourceRolloutAuthority === "explicitly_abandoned_predecessor") {
8195
+ const freshHistory = deferredCandidateSourceTurnHistory(storeDir, sourceSession);
8196
+ const freshAbandonmentFingerprint = freshHistory
8197
+ ? explicitlyAbandonedCandidateSourceFingerprint({
8198
+ storeDir,
8199
+ session: sourceSession,
8200
+ sourceTurnHistory: freshHistory,
8201
+ dispatchSnapshot
8202
+ })
8203
+ : undefined;
8204
+ if (JSON.stringify(freshHistory) !==
8205
+ JSON.stringify(candidateSourceTurnHistory) ||
8206
+ freshAbandonmentFingerprint !== sourceAbandonmentFingerprint) {
8207
+ throw new Error("the explicitly abandoned Codex predecessor authority changed; refresh AKK list");
8208
+ }
8209
+ }
7693
8210
  const existingTransfer = listDeferredForegroundTransfers(storeDir).find((candidate) => !["resolved", "abort_resolved"].includes(candidate.status) &&
7694
8211
  (candidate.source_session_id === sourceSession.session_id ||
7695
8212
  (candidate.terminal_id === terminal.conversationId &&
@@ -7716,6 +8233,12 @@ async function prepareDeferredCodexForegroundBinding({ options, terminal, source
7716
8233
  source_previous_last_transition_id: sourceSession.last_transition_id,
7717
8234
  source_before_binding: sourceSession.binding,
7718
8235
  source_kind: sourceKind,
8236
+ ...(sourceRolloutAuthority === "explicitly_abandoned_predecessor"
8237
+ ? { source_rollout_authority: sourceRolloutAuthority }
8238
+ : {}),
8239
+ ...(sourceAbandonmentFingerprint
8240
+ ? { source_abandonment_fingerprint: sourceAbandonmentFingerprint }
8241
+ : {}),
7719
8242
  ...(candidateSourceTurnHistory
7720
8243
  ? { source_turn_history: candidateSourceTurnHistory }
7721
8244
  : {}),
@@ -7749,6 +8272,10 @@ function assertDeferredForegroundTransferMatchesBoundary({ transfer, boundary })
7749
8272
  (transfer.version === 1
7750
8273
  ? "status_card_only"
7751
8274
  : transfer.source_kind) !== boundary.sourceKind ||
8275
+ (transfer.source_rollout_authority ?? "present") !==
8276
+ boundary.sourceRolloutAuthority ||
8277
+ transfer.source_abandonment_fingerprint !==
8278
+ boundary.sourceAbandonmentFingerprint ||
7752
8279
  JSON.stringify(transfer.source_turn_history) !==
7753
8280
  JSON.stringify(boundary.sourceTurnHistory) ||
7754
8281
  transfer.source_previous_last_transition_id !==
@@ -8135,6 +8662,11 @@ async function commitDeferredCodexForegroundBinding({ options, boundary, identit
8135
8662
  const storeDir = storeDirFromOptions(options);
8136
8663
  let transfer = loadDeferredForegroundTransfer(storeDir, boundary.transferId);
8137
8664
  assertDeferredForegroundTransferMatchesBoundary({ transfer, boundary });
8665
+ assertFrozenExplicitlyAbandonedPredecessorAuthority({
8666
+ storeDir,
8667
+ transfer,
8668
+ terminalControl: boundary.terminal.terminalControl
8669
+ });
8138
8670
  if (!["dispatch_started", "uncertain"].includes(transfer.status) ||
8139
8671
  transfer.input_stage === "none" ||
8140
8672
  identity.processUuid !== transfer.process_uuid ||
@@ -8184,7 +8716,11 @@ async function commitDeferredCodexForegroundBinding({ options, boundary, identit
8184
8716
  ? transfer.target_session_id
8185
8717
  : sameNativeThread
8186
8718
  ? transfer.source_session_id
8187
- : transfer.target_session_id
8719
+ : transfer.target_session_id,
8720
+ allowedManagedSessionIds: transfer.source_rollout_authority ===
8721
+ "explicitly_abandoned_predecessor"
8722
+ ? [transfer.source_session_id]
8723
+ : []
8188
8724
  });
8189
8725
  const retirement = sameNativeThread
8190
8726
  ? "binding_scrubbed_same_native_thread"
@@ -8360,7 +8896,11 @@ async function resolveCommittedDeferredCodexForegroundTransfer({ options, bounda
8360
8896
  nativeThreadId: transfer.target_native_thread_id,
8361
8897
  storeDir,
8362
8898
  terminalControl: boundary.terminal.terminalControl,
8363
- excludedManagedSessionId: target.session_id
8899
+ excludedManagedSessionId: target.session_id,
8900
+ allowedManagedSessionIds: transfer.source_rollout_authority ===
8901
+ "explicitly_abandoned_predecessor"
8902
+ ? [transfer.source_session_id]
8903
+ : []
8364
8904
  });
8365
8905
  transfer = saveDeferredForegroundTransfer(storeDir, {
8366
8906
  ...transfer,
@@ -8563,7 +9103,13 @@ async function maybeAdoptObservedExternalThread({ options, terminal, sourceSessi
8563
9103
  options,
8564
9104
  terminal,
8565
9105
  sourceSession,
8566
- resolvedIdentity
9106
+ resolvedIdentity,
9107
+ // Snapshot-bound terminal actions apply their own stricter boundary
9108
+ // before any input. In particular, a post-/clear Codex composer can be
9109
+ // safely empty while the passive activity classifier is still unknown;
9110
+ // do not force the unrelated external-handoff idle check before the
9111
+ // deferred candidate boundary gets a chance to revalidate it.
9112
+ requireSafeTerminal: !stringValue(options.expectedTerminalToken)
8567
9113
  });
8568
9114
  const identity = observed.identity;
8569
9115
  const conflictKind = managedBindingConflictKindForResolvedTerminal({
@@ -9207,7 +9753,7 @@ function verifiedWorkspaceRelationship(targetWorkspace, candidateWorkspace) {
9207
9753
  return "unknown";
9208
9754
  }
9209
9755
  }
9210
- async function assertNativeThreadHasExclusiveOwnership({ options, agent, currentPid, nativeThreadId, storeDir, terminalControl, excludedManagedSessionId }) {
9756
+ async function assertNativeThreadHasExclusiveOwnership({ options, agent, currentPid, nativeThreadId, storeDir, terminalControl, excludedManagedSessionId, allowedManagedSessionIds = [] }) {
9211
9757
  const normalizedNativeThreadId = nativeThreadId.toLowerCase();
9212
9758
  if (!isExactNativeThreadId(normalizedNativeThreadId)) {
9213
9759
  throw new Error("native thread ownership requires an exact thread UUID");
@@ -9232,6 +9778,7 @@ async function assertNativeThreadHasExclusiveOwnership({ options, agent, current
9232
9778
  throw new Error(`native thread ownership is unverifiable: ${activeOwnerScan.uncertaintyReasons.join("; ")}`);
9233
9779
  }
9234
9780
  const conflictingSessions = listManagedSessions(storeDir).filter((session) => session.session_id !== excludedManagedSessionId &&
9781
+ !allowedManagedSessionIds.includes(session.session_id) &&
9235
9782
  session.agent === agent &&
9236
9783
  session.binding?.native_thread_id?.toLowerCase() === normalizedNativeThreadId);
9237
9784
  if (conflictingSessions.length > 0) {
@@ -12398,12 +12945,27 @@ async function runSend(options) {
12398
12945
  }
12399
12946
  assertTerminalIncarnationCanStartTurn(storeDir, resolvedTerminal.terminalControl);
12400
12947
  let currentSession = tryLoadManagedSession(storeDir, sessionId);
12401
- const knownCodexCompanions = currentSession
12948
+ let knownCodexCompanions = currentSession
12402
12949
  ? codexAllowedCompanionSetForManagedSession({
12403
12950
  storeDir,
12404
12951
  session: currentSession
12405
12952
  })
12406
12953
  : { additional: [] };
12954
+ if (currentSession?.agent === "codex" &&
12955
+ knownCodexCompanions.primary) {
12956
+ try {
12957
+ const inventory = await inspectCodexOpenRootRolloutInventory({
12958
+ options,
12959
+ pid: resolvedTerminal.pid,
12960
+ cwd: resolvedTerminal.terminalControl.currentPath
12961
+ });
12962
+ knownCodexCompanions = codexCompanionsPresentInOpenRootInventory(knownCodexCompanions, inventory);
12963
+ }
12964
+ catch {
12965
+ // Inventory proof is an optimization only. Preserve the existing
12966
+ // closed /status fence when exact open-root membership is unavailable.
12967
+ }
12968
+ }
12407
12969
  const lockedNativeIdentity = await resolveCurrentNativeAgentSessionIdentity({
12408
12970
  options,
12409
12971
  agent: resolvedTerminal.agent,
@@ -12433,6 +12995,11 @@ async function runSend(options) {
12433
12995
  if (!bindingMatchesLiveTerminal(currentSession, resolvedTerminal, lockedNativeIdentity, storeDir)) {
12434
12996
  throw new Error("managed session identity changed while waiting to send; refresh list and retry");
12435
12997
  }
12998
+ await assertStrictCodexSessionHasNoLatentClear({
12999
+ options,
13000
+ terminal: resolvedTerminal,
13001
+ session: currentSession
13002
+ });
12436
13003
  const logicalLockedNativeIdentity = logicalIdentityForManagedSession({
12437
13004
  storeDir,
12438
13005
  session: currentSession,
@@ -16596,6 +17163,11 @@ async function runRenew(options) {
16596
17163
  if (conversation.status !== "stalled") {
16597
17164
  throw new Error(`cannot renew ${conversation.conversation_id}; conversation is ${conversation.status}, not stalled`);
16598
17165
  }
17166
+ if (terminalBridgeSubmission(conversation)?.status === "uncertain") {
17167
+ throw new Error(`cannot renew ${conversation.conversation_id}; its terminal submission ` +
17168
+ "is uncertain and cannot be attributed by monitoring. Inspect the pane " +
17169
+ "and explicitly close the Turn to abandon the unresolved result.");
17170
+ }
16599
17171
  const nativeTakeover = isRecord(conversation.native_session_takeover)
16600
17172
  ? conversation.native_session_takeover
16601
17173
  : undefined;
@@ -16622,6 +17194,9 @@ async function runRenew(options) {
16622
17194
  if (current.status !== "stalled") {
16623
17195
  throw new Error(`cannot renew ${current.conversation_id}; conversation is ${current.status}, not stalled`);
16624
17196
  }
17197
+ if (terminalBridgeSubmission(current)?.status === "uncertain") {
17198
+ throw new Error(`cannot renew ${current.conversation_id}; its terminal submission is uncertain`);
17199
+ }
16625
17200
  const currentTakeover = isRecord(current.native_session_takeover)
16626
17201
  ? current.native_session_takeover
16627
17202
  : undefined;
@@ -20997,6 +21572,12 @@ function deferredCodexBoundaryFromTransfer({ terminal, transfer }) {
20997
21572
  sourceKind: transfer.version === 1
20998
21573
  ? "status_card_only"
20999
21574
  : required(transfer.source_kind, "deferred foreground source kind is unavailable"),
21575
+ sourceRolloutAuthority: transfer.source_rollout_authority ?? "present",
21576
+ ...(transfer.source_abandonment_fingerprint
21577
+ ? {
21578
+ sourceAbandonmentFingerprint: transfer.source_abandonment_fingerprint
21579
+ }
21580
+ : {}),
21000
21581
  ...(transfer.source_turn_history
21001
21582
  ? { sourceTurnHistory: transfer.source_turn_history }
21002
21583
  : {}),
@@ -21565,6 +22146,11 @@ async function recoverAcceptedDeferredForegroundDispatch({ options, storeDir, te
21565
22146
  statePath: authority.statePath,
21566
22147
  expectedMessageBodyHash: stringValue(authority.submission.message_body_hash)
21567
22148
  });
22149
+ assertFrozenExplicitlyAbandonedPredecessorAuthority({
22150
+ storeDir,
22151
+ transfer,
22152
+ terminalControl: terminal.terminalControl
22153
+ });
21568
22154
  if (![
21569
22155
  "prepared",
21570
22156
  "text_injected",
@@ -23109,6 +23695,97 @@ function codexComposerVisible(screen) {
23109
23695
  .slice(-8)
23110
23696
  .some((line) => /^[›»](?:\s|$)/u.test(line.trimEnd()));
23111
23697
  }
23698
+ function codexLatentClearResumeObservation({ screen, agentVersion }) {
23699
+ const behaviorProfile = codexLifecycleBehaviorProfile(agentVersion);
23700
+ if (!behaviorProfile) {
23701
+ return undefined;
23702
+ }
23703
+ const withoutEscapes = (line) => line.replace(/\x1B(?:\[[0-?]*[ -/]*[@-~]|\][^\x07]*(?:\x07|\x1B\\))/gu, "");
23704
+ const resumePrefix = /^\s*To continue this session, run codex resume\s+(.+?)\s*$/iu;
23705
+ const exactUuid = /^([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/iu;
23706
+ const lines = String(screen ?? "")
23707
+ .split(/\r?\n/u)
23708
+ .slice(-24)
23709
+ .map(withoutEscapes);
23710
+ const resumeIds = [];
23711
+ for (let index = 0; index < lines.length; index += 1) {
23712
+ const prefixMatch = resumePrefix.exec(lines[index]);
23713
+ if (!prefixMatch) {
23714
+ continue;
23715
+ }
23716
+ // Codex wraps the UUID after a hyphen in narrow panes. Only join the
23717
+ // immediately following line, strip layout whitespace, and still require
23718
+ // the resulting value to be one exact UUID. This keeps a prose lookalike
23719
+ // or an unrelated scrollback line from becoming routing authority.
23720
+ const firstFragment = prefixMatch[1].replace(/\s+/gu, "");
23721
+ const fragments = [
23722
+ firstFragment,
23723
+ ...(firstFragment.endsWith("-")
23724
+ ? [`${prefixMatch[1]}${lines[index + 1] ?? ""}`.replace(/\s+/gu, "")]
23725
+ : [])
23726
+ ];
23727
+ const matched = fragments.flatMap((candidate) => {
23728
+ const uuidMatch = exactUuid.exec(candidate);
23729
+ return uuidMatch ? [uuidMatch[1].toLowerCase()] : [];
23730
+ })[0];
23731
+ if (matched) {
23732
+ resumeIds.push(matched);
23733
+ }
23734
+ }
23735
+ const sourceNativeThreadId = resumeIds.at(-1);
23736
+ if (!sourceNativeThreadId) {
23737
+ return undefined;
23738
+ }
23739
+ return {
23740
+ sourceNativeThreadId,
23741
+ fingerprint: terminalActionFingerprint({
23742
+ kind: "codex_latent_clear_resume_hint",
23743
+ behavior_profile: behaviorProfile,
23744
+ source_native_thread_id: sourceNativeThreadId
23745
+ })
23746
+ };
23747
+ }
23748
+ function codexLatentClearResumeFingerprint({ screen, sourceNativeThreadId, agentVersion }) {
23749
+ if (!isExactNativeThreadId(sourceNativeThreadId)) {
23750
+ return undefined;
23751
+ }
23752
+ const observation = codexLatentClearResumeObservation({
23753
+ screen,
23754
+ agentVersion
23755
+ });
23756
+ return observation?.sourceNativeThreadId === sourceNativeThreadId.toLowerCase()
23757
+ ? observation.fingerprint
23758
+ : undefined;
23759
+ }
23760
+ async function observeCodexLatentClearResumeFingerprint({ options, terminal, sourceNativeThreadId }) {
23761
+ if (terminal.agent !== "codex" || !isExactNativeThreadId(sourceNativeThreadId)) {
23762
+ return undefined;
23763
+ }
23764
+ const agentVersion = agentVersionForRunningProcess("codex", terminal.pid, options);
23765
+ if (!codexLifecycleBehaviorProfile(agentVersion)) {
23766
+ return undefined;
23767
+ }
23768
+ const status = await createTerminalAgentBridge(options).status("codex", terminal.terminalControl, { runtime: terminalRuntimeForLiveIdentity({ terminal, physicalOnly: true }) });
23769
+ return codexLatentClearResumeFingerprint({
23770
+ screen: status.screen.excerpt,
23771
+ sourceNativeThreadId,
23772
+ agentVersion
23773
+ });
23774
+ }
23775
+ async function assertStrictCodexSessionHasNoLatentClear({ options, terminal, session }) {
23776
+ if (session.agent !== "codex") {
23777
+ return;
23778
+ }
23779
+ const fingerprint = await observeCodexLatentClearResumeFingerprint({
23780
+ options,
23781
+ terminal,
23782
+ sourceNativeThreadId: session.binding?.native_thread_id
23783
+ });
23784
+ if (fingerprint) {
23785
+ throw new Error("Codex /clear changed the foreground logical thread; refresh AKK list " +
23786
+ "and use its snapshot-bound follow-current send. No task input was sent.");
23787
+ }
23788
+ }
23112
23789
  function nativeInspectionComposerEmpty(agent, screen) {
23113
23790
  return agent === "codex"
23114
23791
  ? codexComposerEmpty(screen)