@scotthuang/agent-knock-knock 0.12.7 → 0.12.10

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.
@@ -1425,7 +1425,7 @@ function isTerminalControlCapability(value) {
1425
1425
  }
1426
1426
  function assertSafeAbortedTerminalRetryBinding({ owner, receipt, storeDir, terminalControl, messageId }) {
1427
1427
  if (!(receipt.status === "aborted" && receipt.safe_to_retry === true)) {
1428
- return;
1428
+ return undefined;
1429
1429
  }
1430
1430
  const sessionId = sessionIdForConversation(owner);
1431
1431
  const managedSession = tryLoadManagedSession(storeDir, sessionId);
@@ -1441,26 +1441,192 @@ function assertSafeAbortedTerminalRetryBinding({ owner, receipt, storeDir, termi
1441
1441
  stringValue(ownerTakeover?.terminal_agent_session_id) ??
1442
1442
  stringValue(ownerTakeover?.terminal_agent_expected_session_id);
1443
1443
  const ownerAgentPid = Number(ownerTakeover?.terminal_agent_pid);
1444
- if (!managedSession ||
1445
- managedSession.status !== "bound" ||
1446
- !binding ||
1447
- !receiptBindingId ||
1448
- !Number.isSafeInteger(receiptBindingGeneration) ||
1449
- !receiptNativeThreadId ||
1450
- receiptBindingId !== stringValue(owner.terminal_binding_id) ||
1451
- receiptBindingGeneration !== Number(owner.terminal_binding_generation) ||
1452
- receiptNativeThreadId !== ownerNativeThreadId ||
1453
- binding.binding_id !== receiptBindingId ||
1454
- binding.generation !== receiptBindingGeneration ||
1455
- binding.native_thread_id !== receiptNativeThreadId ||
1456
- !Number.isSafeInteger(ownerAgentPid) ||
1457
- binding.native_process.pid !== ownerAgentPid ||
1458
- !ownerControl ||
1459
- !terminalControlsShareIncarnation(ownerControl, terminalControl) ||
1460
- !terminalControlsShareIncarnation(binding.terminal_control, terminalControl)) {
1444
+ if (managedSession &&
1445
+ managedSession.status === "bound" &&
1446
+ binding &&
1447
+ receiptBindingId &&
1448
+ Number.isSafeInteger(receiptBindingGeneration) &&
1449
+ receiptNativeThreadId &&
1450
+ receiptBindingId === stringValue(owner.terminal_binding_id) &&
1451
+ receiptBindingGeneration === Number(owner.terminal_binding_generation) &&
1452
+ receiptNativeThreadId === ownerNativeThreadId &&
1453
+ binding.binding_id === receiptBindingId &&
1454
+ binding.generation === receiptBindingGeneration &&
1455
+ binding.native_thread_id === receiptNativeThreadId &&
1456
+ Number.isSafeInteger(ownerAgentPid) &&
1457
+ binding.native_process.pid === ownerAgentPid &&
1458
+ ownerControl &&
1459
+ terminalControlsShareIncarnation(ownerControl, terminalControl) &&
1460
+ terminalControlsShareIncarnation(binding.terminal_control, terminalControl)) {
1461
+ return managedSession;
1462
+ }
1463
+ const recoveredSource = safeAbortedDeferredRetrySourceSession({
1464
+ owner,
1465
+ receipt,
1466
+ storeDir,
1467
+ terminalControl,
1468
+ messageId
1469
+ });
1470
+ if (!recoveredSource) {
1461
1471
  throw new Error(`terminal idempotency key ${messageId} belongs to a safe-aborted Turn ` +
1462
1472
  "whose Session binding is no longer current; no terminal input was sent");
1463
1473
  }
1474
+ return recoveredSource;
1475
+ }
1476
+ function safeAbortedDeferredRetrySourceSession({ owner, receipt, storeDir, terminalControl, messageId }) {
1477
+ const takeover = isRecord(owner.native_session_takeover)
1478
+ ? owner.native_session_takeover
1479
+ : undefined;
1480
+ const transferId = stringValue(takeover?.deferred_foreground_transfer_id);
1481
+ const ownerControl = terminalControlFromTakeover(takeover);
1482
+ if (!transferId || !takeover || !ownerControl) {
1483
+ return undefined;
1484
+ }
1485
+ const transfer = loadDeferredForegroundTransfer(storeDir, transferId);
1486
+ const target = tryLoadManagedSession(storeDir, transfer.target_session_id);
1487
+ const source = tryLoadManagedSession(storeDir, transfer.source_session_id);
1488
+ const submission = terminalBridgeSubmission(owner);
1489
+ const matchingReceipts = terminalBridgeSubmissionReceipts(owner).filter((candidate) => stringValue(candidate.message_id) === messageId);
1490
+ const canonical = pathsForConversation(owner.conversation_id, storeDir);
1491
+ const targetBinding = transfer.abort_target_after_binding;
1492
+ const sourceBinding = transfer.abort_source_after_binding;
1493
+ const transferDispatchStartedAt = stringValue(transfer.dispatch_started_at);
1494
+ const terminalInputNotStartedAt = stringValue(transfer.terminal_input_not_started_at);
1495
+ const abortedBeforeDispatchIntent = transfer.input_stage === "none" &&
1496
+ transferDispatchStartedAt === undefined &&
1497
+ terminalInputNotStartedAt === undefined;
1498
+ const dispatchIntentProvedNotStarted = transfer.input_stage === "dispatch_started" &&
1499
+ transferDispatchStartedAt !== undefined &&
1500
+ terminalInputNotStartedAt !== undefined &&
1501
+ validTimestampMs(transferDispatchStartedAt) &&
1502
+ validTimestampMs(terminalInputNotStartedAt) &&
1503
+ Date.parse(terminalInputNotStartedAt) >=
1504
+ Date.parse(transferDispatchStartedAt);
1505
+ const forbiddenInputEvidence = [
1506
+ "text_injected_at",
1507
+ "enter_dispatched_at",
1508
+ "submitted_at",
1509
+ "agent_accepted_at",
1510
+ "not_accepted_at",
1511
+ "uncertain_at",
1512
+ "acceptance_evidence"
1513
+ ];
1514
+ if (transfer.version !== 2 ||
1515
+ transfer.status !== "abort_resolved" ||
1516
+ (!abortedBeforeDispatchIntent && !dispatchIntentProvedNotStarted) ||
1517
+ transfer.text_injected_at !== undefined ||
1518
+ transfer.enter_dispatched_at !== undefined ||
1519
+ transfer.agent_accepted_at !== undefined ||
1520
+ transfer.target_session_id !== sessionIdForConversation(owner) ||
1521
+ transfer.turn_id !== turnIdForConversation(owner) ||
1522
+ transfer.turn_id !== owner.conversation_id ||
1523
+ transfer.message_id !== messageId ||
1524
+ transfer.terminal_id !== stringValue(takeover.native_session_id) ||
1525
+ transfer.process_pid !== Number(takeover.terminal_agent_pid) ||
1526
+ transfer.process_uuid !== stringValue(takeover.terminal_agent_process_uuid) ||
1527
+ transfer.process_birth !== stringValue(takeover.terminal_agent_process_birth) ||
1528
+ path.resolve(transfer.workspace) !== path.resolve(owner.workspace) ||
1529
+ !terminalControlsShareIncarnation(ownerControl, terminalControl) ||
1530
+ !terminalControlEvidenceMatches(transfer.terminal_endpoint, terminalControl) ||
1531
+ owner.status !== "failed" ||
1532
+ isRecord(owner.callback_delivery) ||
1533
+ isRecord(owner.terminal_bridge_completion_claim) ||
1534
+ isRecord(takeover.terminal_bridge_completion_claim) ||
1535
+ stringValue(takeover.terminal_bridge_message_id) !== messageId ||
1536
+ stringValue(takeover.terminal_bridge_request_hash) !==
1537
+ transfer.request_hash ||
1538
+ stringValue(takeover.terminal_bridge_request_hash) !==
1539
+ stringValue(receipt.request_hash) ||
1540
+ stringValue(takeover.deferred_foreground_transfer_id) !==
1541
+ transfer.transfer_id ||
1542
+ path.resolve(stringValue(owner.state_path) ?? "") !==
1543
+ path.resolve(canonical.statePath) ||
1544
+ path.resolve(stringValue(owner.event_log_path) ?? "") !==
1545
+ path.resolve(canonical.logPath) ||
1546
+ path.resolve(managedSessionStoreDirForConversation(owner) ?? "") !==
1547
+ path.resolve(storeDir) ||
1548
+ !submission ||
1549
+ matchingReceipts.length !== 1 ||
1550
+ canonicalJson(matchingReceipts[0]) !== canonicalJson(submission) ||
1551
+ canonicalJson(submission) !== canonicalJson(receipt) ||
1552
+ submission.status !== "aborted" ||
1553
+ submission.safe_to_retry !== true ||
1554
+ stringValue(submission.last_proven_stage) !== "prepared" ||
1555
+ !validTimestampMs(submission.prepared_at) ||
1556
+ !validTimestampMs(submission.aborted_at) ||
1557
+ Date.parse(String(submission.aborted_at)) <
1558
+ Date.parse(String(submission.prepared_at)) ||
1559
+ (dispatchIntentProvedNotStarted &&
1560
+ stringValue(submission.aborted_at) !== terminalInputNotStartedAt) ||
1561
+ forbiddenInputEvidence.some((field) => submission[field] !== undefined) ||
1562
+ stringValue(submission.session_id) !== transfer.target_session_id ||
1563
+ stringValue(submission.turn_id) !== transfer.turn_id ||
1564
+ stringValue(submission.message_id) !== transfer.message_id ||
1565
+ stringValue(submission.request_hash) !== transfer.request_hash ||
1566
+ stringValue(submission.binding_id) !==
1567
+ transfer.target_before_binding?.binding_id ||
1568
+ Number(submission.binding_generation) !==
1569
+ transfer.target_before_binding?.generation ||
1570
+ stringValue(submission.native_thread_id) !== undefined ||
1571
+ !target ||
1572
+ target.status !== "detached" ||
1573
+ target.last_transition_id !== transfer.transfer_id ||
1574
+ target.lineage.transition_id !== transfer.transfer_id ||
1575
+ target.lineage.previous_session_id !== transfer.source_session_id ||
1576
+ transfer.abort_target_after_status !== "detached" ||
1577
+ !targetBinding ||
1578
+ managedSessionRevision(target) !== transfer.abort_target_after_revision ||
1579
+ managedSessionBindingToken(target) !==
1580
+ transfer.abort_target_after_binding_token ||
1581
+ JSON.stringify(target.binding) !== JSON.stringify(targetBinding) ||
1582
+ !source ||
1583
+ source.status !== "bound" ||
1584
+ source.last_transition_id !== transfer.source_previous_last_transition_id ||
1585
+ transfer.abort_source_after_status !== "bound" ||
1586
+ !sourceBinding ||
1587
+ managedSessionRevision(source) !== transfer.abort_source_after_revision ||
1588
+ managedSessionBindingToken(source) !==
1589
+ transfer.abort_source_after_binding_token ||
1590
+ JSON.stringify(source.binding) !== JSON.stringify(sourceBinding) ||
1591
+ JSON.stringify(sourceBinding) !==
1592
+ JSON.stringify(transfer.source_before_binding)) {
1593
+ return undefined;
1594
+ }
1595
+ const ledger = loadTerminalBridgeDispatchLedger(terminalControl);
1596
+ if (!ledger) {
1597
+ return undefined;
1598
+ }
1599
+ assertDeferredForegroundResolvedZeroInputLedger({
1600
+ storeDir,
1601
+ terminal: { terminalControl },
1602
+ transfer,
1603
+ ledger,
1604
+ statePath: canonical.statePath
1605
+ });
1606
+ if (dispatchIntentProvedNotStarted &&
1607
+ stringValue(ledger.aborted_at) !== terminalInputNotStartedAt) {
1608
+ return undefined;
1609
+ }
1610
+ return source;
1611
+ }
1612
+ function exactSafeAbortedRecoveredSessionMatches({ owner, receipt, storeDir, terminalControl, messageId, expectedSessionId }) {
1613
+ const exactReceipt = receipt ?? (() => {
1614
+ const matches = terminalBridgeSubmissionReceipts(owner).filter((candidate) => stringValue(candidate.message_id) === messageId);
1615
+ return matches.length === 1 ? matches[0] : undefined;
1616
+ })();
1617
+ if (!exactReceipt ||
1618
+ exactReceipt.status !== "aborted" ||
1619
+ exactReceipt.safe_to_retry !== true) {
1620
+ return false;
1621
+ }
1622
+ const recoveredSession = assertSafeAbortedTerminalRetryBinding({
1623
+ owner,
1624
+ receipt: exactReceipt,
1625
+ storeDir,
1626
+ terminalControl,
1627
+ messageId
1628
+ });
1629
+ return recoveredSession?.session_id === expectedSessionId;
1464
1630
  }
1465
1631
  function stableDelegateTerminalRoute({ options, request, workspace, requestedAgent }) {
1466
1632
  const messageId = stringValue(options.messageId);
@@ -1548,23 +1714,38 @@ function stableDelegateTerminalRoute({ options, request, workspace, requestedAge
1548
1714
  }
1549
1715
  if (selected.receipt.status === "aborted" &&
1550
1716
  selected.receipt.safe_to_retry === true) {
1551
- const sessionId = sessionIdForConversation(selected.owner);
1552
1717
  const ownerControl = terminalControlFromTakeover(isRecord(selected.owner.native_session_takeover)
1553
1718
  ? selected.owner.native_session_takeover
1554
1719
  : undefined);
1555
1720
  if (!ownerControl) {
1556
1721
  throw new Error(`terminal idempotency key ${messageId} has no durable terminal route`);
1557
1722
  }
1558
- assertSafeAbortedTerminalRetryBinding({
1723
+ const retrySession = assertSafeAbortedTerminalRetryBinding({
1559
1724
  owner: selected.owner,
1560
1725
  receipt: selected.receipt,
1561
1726
  storeDir,
1562
1727
  terminalControl: ownerControl,
1563
1728
  messageId
1564
1729
  });
1730
+ if (!retrySession) {
1731
+ throw new Error(`terminal idempotency key ${messageId} has no restored retry Session`);
1732
+ }
1733
+ if (retrySession.agent === "codex" &&
1734
+ isCompleteNativeRollout(retrySession.binding?.native_process.rollout)) {
1735
+ // A safe-aborted retry has proved that the original binding is unchanged,
1736
+ // but one open Codex rollout still does not prove the TUI foreground.
1737
+ // Preserve the stable terminal route so runSend captures fresh implicit
1738
+ // candidate authority and retries through the v3 transfer instead of the
1739
+ // forbidden strict Session path.
1740
+ return {
1741
+ kind: "terminal",
1742
+ conversationId: selected.conversationId,
1743
+ workspace: selected.workspace
1744
+ };
1745
+ }
1565
1746
  return {
1566
1747
  kind: "session",
1567
- sessionId,
1748
+ sessionId: retrySession.session_id,
1568
1749
  workspace: selected.workspace
1569
1750
  };
1570
1751
  }
@@ -2103,6 +2284,18 @@ function replayExactActiveTerminalSubmission({ options, terminalControl, request
2103
2284
  const loadedLedgerOwner = loadedLedger && loadedLedgerMessageId === messageId
2104
2285
  ? loadTerminalDispatchLedgerOwner(loadedLedger)
2105
2286
  : undefined;
2287
+ const loadedLedgerSessionId = stringValue(loadedLedger?.session_id);
2288
+ const loadedLedgerMatchesRecoveredSession = Boolean(expectedSessionId &&
2289
+ loadedLedgerSessionId !== undefined &&
2290
+ loadedLedgerSessionId !== expectedSessionId &&
2291
+ loadedLedgerOwner &&
2292
+ exactSafeAbortedRecoveredSessionMatches({
2293
+ owner: loadedLedgerOwner,
2294
+ storeDir: expectedStoreDir,
2295
+ terminalControl,
2296
+ messageId,
2297
+ expectedSessionId
2298
+ }));
2106
2299
  const expectedMessageBodyHash = createHash("sha256")
2107
2300
  .update(requestText)
2108
2301
  .digest("hex");
@@ -2136,8 +2329,9 @@ function replayExactActiveTerminalSubmission({ options, terminalControl, request
2136
2329
  (!terminalDispatchRecordMatchesControl(loadedLedger, terminalControl) ||
2137
2330
  (loadedLedgerStoreDir !== undefined &&
2138
2331
  path.resolve(loadedLedgerStoreDir) !== path.resolve(expectedStoreDir)) ||
2139
- (expectedSessionId && stringValue(loadedLedger.session_id) !== undefined &&
2140
- stringValue(loadedLedger.session_id) !== expectedSessionId) ||
2332
+ (expectedSessionId && loadedLedgerSessionId !== undefined &&
2333
+ loadedLedgerSessionId !== expectedSessionId &&
2334
+ !loadedLedgerMatchesRecoveredSession) ||
2141
2335
  (expectedTurnId && stringValue(loadedLedger.turn_id) !== undefined &&
2142
2336
  stringValue(loadedLedger.turn_id) !== expectedTurnId) ||
2143
2337
  (expectedStatePath && loadedLedgerStatePath !== undefined &&
@@ -2169,6 +2363,7 @@ function replayExactActiveTerminalSubmission({ options, terminalControl, request
2169
2363
  return true;
2170
2364
  }
2171
2365
  if (ledgerReceipt &&
2366
+ !(ledgerReceipt.status === "aborted" && ledgerReceipt.safe_to_retry === true) &&
2172
2367
  (loadedLedgerMessageId !== messageId ||
2173
2368
  !["submitted", "enter_dispatched", "agent_accepted"].includes(String(ledgerReceipt.status)) ||
2174
2369
  !["submitted", "enter_dispatched", "agent_accepted"].includes(String(loadedLedger?.status)))) {
@@ -2326,6 +2521,16 @@ function validateStoredTerminalSubmissionMatch({ owner, receipt, options, termin
2326
2521
  const receiptStoreDir = stringValue(receipt.store_dir) ?? ownerStoreDir;
2327
2522
  const receiptSessionId = stringValue(receipt.session_id) ??
2328
2523
  sessionIdForConversation(owner);
2524
+ const receiptMatchesRecoveredSession = Boolean(expectedSessionId &&
2525
+ receiptSessionId !== expectedSessionId &&
2526
+ exactSafeAbortedRecoveredSessionMatches({
2527
+ owner,
2528
+ receipt,
2529
+ storeDir: expectedStoreDir,
2530
+ terminalControl,
2531
+ messageId,
2532
+ expectedSessionId
2533
+ }));
2329
2534
  const receiptTurnId = stringValue(receipt.turn_id) ??
2330
2535
  turnIdForConversation(owner);
2331
2536
  const receiptOpenClawSession = stringValue(receipt.openclaw_session) ??
@@ -2352,7 +2557,8 @@ function validateStoredTerminalSubmissionMatch({ owner, receipt, options, termin
2352
2557
  !terminalControlEvidenceMatches(receiptTerminalEvidence, terminalControl) ||
2353
2558
  receiptSessionId !== sessionIdForConversation(owner) ||
2354
2559
  receiptTurnId !== turnIdForConversation(owner) ||
2355
- (expectedSessionId && receiptSessionId !== expectedSessionId) ||
2560
+ (expectedSessionId && receiptSessionId !== expectedSessionId &&
2561
+ !receiptMatchesRecoveredSession) ||
2356
2562
  (expectedTurnId && receiptTurnId !== expectedTurnId) ||
2357
2563
  (expectedStatePath && !sameCanonicalStatePath(owner.state_path, expectedStatePath)) ||
2358
2564
  (requestedOpenClawSession && receiptOpenClawSession !== requestedOpenClawSession) ||
@@ -2427,13 +2633,28 @@ function replayExactStoredTerminalSubmission({ options, terminalControl, request
2427
2633
  const match = matches[0];
2428
2634
  if (!match) {
2429
2635
  for (const { owner, receipt } of validatedMatches) {
2430
- assertSafeAbortedTerminalRetryBinding({
2431
- owner,
2432
- receipt,
2433
- storeDir: expectedStoreDir,
2434
- terminalControl,
2435
- messageId
2436
- });
2636
+ if (expectedSessionId) {
2637
+ if (!exactSafeAbortedRecoveredSessionMatches({
2638
+ owner,
2639
+ receipt,
2640
+ storeDir: expectedStoreDir,
2641
+ terminalControl,
2642
+ messageId,
2643
+ expectedSessionId
2644
+ })) {
2645
+ throw new Error(`terminal idempotency key ${messageId} does not match its ` +
2646
+ "restored retry Session; no terminal input was sent");
2647
+ }
2648
+ }
2649
+ else {
2650
+ assertSafeAbortedTerminalRetryBinding({
2651
+ owner,
2652
+ receipt,
2653
+ storeDir: expectedStoreDir,
2654
+ terminalControl,
2655
+ messageId
2656
+ });
2657
+ }
2437
2658
  }
2438
2659
  // A prepared-stage failure with a restored ledger proves that tmux was
2439
2660
  // untouched. The same exact id may therefore start a fresh attempt; all
@@ -3771,6 +3992,17 @@ function terminalFirstListProjection({ storeDir, terminals, managedSessions, ses
3771
3992
  stringValue(codexLatentClearResumeValue.source_native_thread_id)?.toLowerCase() === deferredCodexSourceNativeThreadId
3772
3993
  ? stringValue(codexLatentClearResumeValue.fingerprint)
3773
3994
  : undefined;
3995
+ if (deferredCodexLatentClearResumeFingerprint) {
3996
+ // The resume hint is useful operational context, but it is not durable
3997
+ // foreground authority: it can scroll away while the latent thread is
3998
+ // still current. Candidate routing and its token rely on the complete
3999
+ // rollout inventory and Store authority below instead.
4000
+ runtimeLog("info", "terminal_codex_latent_clear_hint_observed", {
4001
+ terminal_id: String(terminal.id),
4002
+ source_session_id: deferredCodexSource?.session_id,
4003
+ source_native_thread_id: deferredCodexSourceNativeThreadId
4004
+ });
4005
+ }
3774
4006
  const deferredCodexSourceActiveElsewhere = Boolean(deferredCodexSourceNativeThreadId &&
3775
4007
  terminals.some((candidate) => candidate !== terminal &&
3776
4008
  candidate.agent === "codex" &&
@@ -3848,11 +4080,12 @@ function terminalFirstListProjection({ storeDir, terminals, managedSessions, ses
3848
4080
  "explicitly_abandoned_predecessor"
3849
4081
  }) &&
3850
4082
  candidateSourceTransitionHistoryIsTerminal(storeDir, deferredCodexSource));
3851
- const deferredCodexCandidateRouteNeeded = Boolean(deferredCodexCandidateInventory &&
3852
- (deferredCodexCandidateInventory.status === "unbound" ||
3853
- nativeIdentityObservation?.status === "unavailable" ||
3854
- deferredCodexSource?.binding?.native_process.rollout === undefined ||
3855
- deferredCodexLatentClearResumeFingerprint !== undefined));
4083
+ // A complete one-root inventory proves only which rollout files are
4084
+ // materialized, not which logical thread the Codex TUI currently owns.
4085
+ // `/clear` can select a new thread before its rollout exists, so every
4086
+ // otherwise eligible nonempty inventory uses the same post-submission
4087
+ // candidate-set attribution as a multi-root inventory.
4088
+ const deferredCodexCandidateRouteNeeded = deferredCodexCandidateInventory !== undefined;
3856
4089
  const deferredCodexForegroundEligible = Boolean(mutationsAllowed &&
3857
4090
  !terminalHasNonterminalDeferredTransfer &&
3858
4091
  deferredCodexSource &&
@@ -3899,7 +4132,6 @@ function terminalFirstListProjection({ storeDir, terminals, managedSessions, ses
3899
4132
  sourceSession: deferredCodexSource,
3900
4133
  dispatchSnapshot: deferredCodexDispatchSnapshot,
3901
4134
  sourceTurnHistory: deferredCodexCandidateSourceTurnHistory,
3902
- candidateContextFingerprint: deferredCodexLatentClearResumeFingerprint,
3903
4135
  sourceRolloutAuthority: deferredCodexSourceRolloutAuthority,
3904
4136
  sourceAbandonmentFingerprint: deferredCodexSourceAbandonmentFingerprint,
3905
4137
  ...(deferredCodexCandidateInventory
@@ -4066,7 +4298,11 @@ function terminalFirstListProjection({ storeDir, terminals, managedSessions, ses
4066
4298
  });
4067
4299
  }
4068
4300
  }
4069
- const terminalCanAcceptSend = ownership.state === "none" && isRecord(sessionAwareRawActions.send);
4301
+ const rolloutBackedCodexSession = Boolean(authoritativeSession?.agent === "codex" &&
4302
+ isCompleteNativeRollout(authoritativeSession.binding?.native_process.rollout));
4303
+ const terminalCanAcceptSend = ownership.state === "none" &&
4304
+ Boolean(deferredCodexForegroundToken ||
4305
+ (!rolloutBackedCodexSession && isRecord(sessionAwareRawActions.send)));
4070
4306
  if (ownership.state === "current" &&
4071
4307
  !allRelated.some((conversation) => conversation.conversation_id === ownership.conversation.conversation_id)) {
4072
4308
  allRelated.push(ownership.conversation);
@@ -4111,7 +4347,8 @@ function terminalFirstListProjection({ storeDir, terminals, managedSessions, ses
4111
4347
  ? withoutGenericHandoffSourceClose(currentTurnProjection, blockingHandoffTurnIds)
4112
4348
  : undefined;
4113
4349
  const nonOwnerRawActions = authoritativeSession
4114
- ? Object.fromEntries(Object.entries(sessionAwareRawActions).filter(([actionName]) => actionName !== "approve"))
4350
+ ? Object.fromEntries(Object.entries(sessionAwareRawActions).filter(([actionName]) => actionName !== "approve" &&
4351
+ !(rolloutBackedCodexSession && actionName === "send")))
4115
4352
  : sessionAwareRawActions;
4116
4353
  const sortedDisplayed = [...sessionDisplayedRelated]
4117
4354
  .filter((conversation) => conversation.conversation_id !== currentTurn?.conversation_id)
@@ -4229,6 +4466,7 @@ function terminalFirstListProjection({ storeDir, terminals, managedSessions, ses
4229
4466
  }
4230
4467
  }
4231
4468
  : managedSessionId &&
4469
+ !rolloutBackedCodexSession &&
4232
4470
  sessionBindingMatchesLiveTerminal &&
4233
4471
  isRecord(sessionAwareRawActions.send)
4234
4472
  ? {
@@ -5615,16 +5853,18 @@ function managedListApprovalState(conversation) {
5615
5853
  }
5616
5854
  function listActionContracts() {
5617
5855
  return {
5618
- version: 14,
5856
+ version: 16,
5619
5857
  instructions: [
5620
5858
  "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.",
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.",
5859
+ "The session_exact scope uses session_id only when it is prefilled by the listed send action. A rollout-backed managed Codex pane instead uses the terminal_follow_current scope with its exact selector plus expected_terminal_token because even one materialized rollout does not prove the current TUI foreground thread. A turn id is never an ordinary send target.",
5860
+ "A user-explicit raw terminal selector, or a uniquely delegated raw send with no selector, may omit expected_terminal_token for convenience. If that terminal already has one rollout-backed managed Codex source, AKK captures an equivalent fresh candidate authority under the terminal and Store locks and still uses the same v3 follow-current transfer; it never degrades to sole-root strict continuation. Unmanaged first attach retains its existing behavior.",
5622
5861
  "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.",
5623
5862
  "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.",
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.",
5863
+ "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 any otherwise eligible quiescent rollout-backed source with a complete nonempty pinned open-rollout inventory may also expose this exact action. One materialized rollout does not prove the current Codex TUI foreground thread, and a /clear resume hint is diagnostic only. 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 predecessor 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.",
5625
5864
  "List resumable threads before resume; use only a complete native_thread_id and the action returned for that candidate.",
5626
5865
  "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.",
5627
5866
  "Use respond only for an in-flight turn that is explicitly waiting for OpenClaw.",
5867
+ "Approval fingerprint v2 binds stable terminal, process, and action fields plus an unredacted exact prompt-region digest. Whole-screen digests and scrollback excerpts are diagnostic only and never authorize approval. A keys-mode prompt without exact prompt-region evidence is not approvable and exposes no approve action.",
5628
5868
  "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.",
5629
5869
  "Start with the action's prefilled arguments, supply every missing_required field, and consult the top-level action's optional fields only when needed.",
5630
5870
  "Authoritative full IDs are prefilled; short_ref is for display and human input.",
@@ -5646,7 +5886,7 @@ function listActionContracts() {
5646
5886
  managed: {
5647
5887
  current_turn: "the authoritative dispatch-ledger owner, never inferred from history",
5648
5888
  recent_turn: "the latest visible non-owning turn in the current managed session",
5649
- session_id: "the continuing agent context and authoritative ordinary-send target",
5889
+ session_id: "the continuing agent context; it is an ordinary-send target only when the listed send action explicitly prefills it, and rollout-backed Codex uses selector/token instead",
5650
5890
  binding_id: "the immutable terminal-binding generation currently authorized for this Session",
5651
5891
  binding_token: "an optimistic concurrency token required by native-thread mutations",
5652
5892
  history: "older turns, present only with --all"
@@ -5684,7 +5924,17 @@ function listActionContracts() {
5684
5924
  tool: "agent_knock_knock_send",
5685
5925
  target_argument: "session_id",
5686
5926
  initial_attach_target_argument: "selector",
5687
- initial_attach_scope: "A discovery selector explicitly named by the user, or the selector prefilled by an unmanaged raw-terminal row's available send action; never infer, guess, or reuse one.",
5927
+ managed_scopes: {
5928
+ session_exact: {
5929
+ target_arguments: ["session_id"],
5930
+ follows_current_terminal: false
5931
+ },
5932
+ terminal_follow_current: {
5933
+ target_arguments: ["selector", "expected_terminal_token"],
5934
+ follows_current_terminal: true
5935
+ }
5936
+ },
5937
+ initial_attach_scope: "A discovery selector explicitly named by the user, the selector prefilled by an unmanaged raw-terminal row's available send action, or an omitted selector delegated only when exactly one eligible raw pane exists; never infer, guess, or reuse a selector or token. For an already managed rollout-backed Codex pane, an explicit or unique raw delegation without a token internally captures fresh v3 candidate authority under lock; it never becomes a strict Session continuation.",
5688
5938
  required: ["request"],
5689
5939
  optional: [
5690
5940
  "selector",
@@ -5696,8 +5946,8 @@ function listActionContracts() {
5696
5946
  ],
5697
5947
  unsupported: ["timeoutSeconds"],
5698
5948
  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.",
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.",
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."
5949
+ candidate_rollout_deferred_scope: "A quiescent rollout-backed Codex source uses a listed selector/token send whenever AKK can pin a complete nonempty candidate inventory. Inventory status resolved means only that one rollout is materialized; it does not prove the current TUI foreground thread. Released predecessor Turn history stays read-only while a separate provisional Session sends once and waits for one unique post-anchor request acceptance. A /clear resume hint is diagnostic only and is not token or routing authority. 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.",
5950
+ ordinary_use: "Create a new managed Turn through the exact action listed for the pane. An explicit session_id never follows the pane and is unavailable for rollout-backed Codex Sessions; their listed selector/token action binds only the unique exact rollout that accepts the submitted request. A user-explicit or uniquely delegated raw send without a token receives the same fresh under-lock candidate authority when it resolves to an already managed rollout-backed Codex source. A live terminal selector can also 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."
5701
5951
  },
5702
5952
  new_thread: {
5703
5953
  tool: "agent_knock_knock_new_thread",
@@ -5774,6 +6024,9 @@ function listActionContracts() {
5774
6024
  required: ["turn_id", "expected_approval_fingerprint"],
5775
6025
  optional: ["expected_terminal_token"],
5776
6026
  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.",
6027
+ fingerprint_contract: "v2_prompt_region",
6028
+ fingerprint_authority: "Stable terminal, process, and action fields plus the unredacted exact prompt-region digest; whole-screen digests and scrollback excerpts are diagnostic only.",
6029
+ missing_prompt_region_evidence: "not_approvable",
5777
6030
  requires_explicit_user_confirmation: true,
5778
6031
  requires_fresh_status: true
5779
6032
  },
@@ -7196,6 +7449,91 @@ function codexCandidateInventoryHasNoOtherManagedClaim({ storeDir, inventory, so
7196
7449
  isExactNativeThreadId(session.binding?.native_thread_id) &&
7197
7450
  candidateIds.has(session.binding.native_thread_id.toLowerCase()));
7198
7451
  }
7452
+ function exactReleasedSafeAbortedCandidateTurn({ storeDir, session, turn }) {
7453
+ const binding = session.binding;
7454
+ const takeover = isRecord(turn.native_session_takeover)
7455
+ ? turn.native_session_takeover
7456
+ : undefined;
7457
+ const submission = terminalBridgeSubmission(turn);
7458
+ const terminalControl = terminalControlFromTakeover(takeover);
7459
+ const messageId = stringValue(submission?.message_id);
7460
+ const preparedAt = stringValue(submission?.prepared_at);
7461
+ const abortedAt = stringValue(submission?.aborted_at);
7462
+ if (session.status !== "bound" ||
7463
+ session.agent !== "codex" ||
7464
+ !binding ||
7465
+ !isExactNativeThreadId(binding.native_thread_id) ||
7466
+ !TERMINAL_DISPATCH_RELEASE_STATUSES.has(turn.status) ||
7467
+ managedTurnNeedsAttention(turn) ||
7468
+ isRecord(turn.callback_delivery) ||
7469
+ isRecord(turn.terminal_bridge_completion_claim) ||
7470
+ !takeover ||
7471
+ isRecord(takeover.terminal_bridge_completion_claim) ||
7472
+ !submission ||
7473
+ !terminalControl ||
7474
+ !messageId ||
7475
+ submission.status !== "aborted" ||
7476
+ submission.safe_to_retry !== true ||
7477
+ stringValue(submission.last_proven_stage) !== "prepared" ||
7478
+ !preparedAt ||
7479
+ !abortedAt ||
7480
+ !validTimestampMs(preparedAt) ||
7481
+ !validTimestampMs(abortedAt) ||
7482
+ Date.parse(abortedAt) < Date.parse(preparedAt) ||
7483
+ [
7484
+ "text_injected_at",
7485
+ "enter_dispatched_at",
7486
+ "submitted_at",
7487
+ "agent_accepted_at",
7488
+ "not_accepted_at",
7489
+ "uncertain_at",
7490
+ "acceptance_evidence"
7491
+ ].some((field) => submission[field] !== undefined) ||
7492
+ stringValue(takeover.terminal_bridge_message_id) !== messageId ||
7493
+ stringValue(takeover.terminal_bridge_request_hash) !==
7494
+ stringValue(submission.request_hash) ||
7495
+ terminalBridgeRequestFingerprint(stringValue(takeover.terminal_bridge_request_text) ?? "") !== stringValue(submission.request_hash) ||
7496
+ executorForConversation(turn).kind !== "codex" ||
7497
+ sessionIdForConversation(turn) !== session.session_id ||
7498
+ stringValue(submission.session_id) !== session.session_id ||
7499
+ stringValue(submission.turn_id) !== turnIdForConversation(turn) ||
7500
+ stringValue(submission.executor_kind) !== "codex" ||
7501
+ stringValue(submission.openclaw_session) !== turn.openclaw_session ||
7502
+ stringValue(turn.terminal_binding_id) !== binding.binding_id ||
7503
+ Number(turn.terminal_binding_generation) !== binding.generation ||
7504
+ stringValue(takeover.terminal_binding_id) !== binding.binding_id ||
7505
+ Number(takeover.terminal_binding_generation) !== binding.generation ||
7506
+ stringValue(submission.binding_id) !== binding.binding_id ||
7507
+ Number(submission.binding_generation) !== binding.generation ||
7508
+ stringValue(turn.native_thread_id) !== binding.native_thread_id ||
7509
+ stringValue(takeover.terminal_agent_session_id) !==
7510
+ binding.native_thread_id ||
7511
+ stringValue(submission.native_thread_id) !== binding.native_thread_id ||
7512
+ Number(takeover.terminal_agent_pid) !== binding.native_process.pid ||
7513
+ stringValue(takeover.terminal_agent_process_uuid) !==
7514
+ binding.native_process.process_uuid ||
7515
+ stringValue(takeover.terminal_agent_process_birth) !==
7516
+ binding.native_process.process_birth ||
7517
+ !terminalControlsShareIncarnation(terminalControl, binding.terminal_control) ||
7518
+ !terminalControlEvidenceMatches(submission.terminal_endpoint ?? terminalControl, binding.terminal_control) ||
7519
+ path.resolve(turn.workspace) !== path.resolve(session.workspace)) {
7520
+ return false;
7521
+ }
7522
+ const canonical = pathsForConversation(turn.conversation_id, storeDir);
7523
+ if (path.resolve(stringValue(turn.state_path) ?? "") !==
7524
+ path.resolve(canonical.statePath) ||
7525
+ path.resolve(stringValue(turn.event_log_path) ?? "") !==
7526
+ path.resolve(canonical.logPath) ||
7527
+ path.resolve(managedSessionStoreDirForConversation(turn) ?? "") !==
7528
+ path.resolve(storeDir) ||
7529
+ path.resolve(stringValue(submission.store_dir) ?? "") !==
7530
+ path.resolve(storeDir)) {
7531
+ return false;
7532
+ }
7533
+ const matchingReceipts = terminalBridgeSubmissionReceipts(turn).filter((receipt) => stringValue(receipt.message_id) === messageId);
7534
+ return matchingReceipts.length === 1 &&
7535
+ canonicalJson(matchingReceipts[0]) === canonicalJson(submission);
7536
+ }
7199
7537
  function deferredCandidateSourceTurnHistory(storeDir, session) {
7200
7538
  const binding = session.binding;
7201
7539
  if (!binding?.native_thread_id) {
@@ -7210,13 +7548,19 @@ function deferredCandidateSourceTurnHistory(storeDir, session) {
7210
7548
  session,
7211
7549
  turn
7212
7550
  });
7551
+ const safelyAbortedBeforeInput = exactReleasedSafeAbortedCandidateTurn({
7552
+ storeDir,
7553
+ session,
7554
+ turn
7555
+ });
7213
7556
  return (!TERMINAL_DISPATCH_RELEASE_STATUSES.has(turn.status) ||
7214
7557
  managedTurnNeedsAttention(turn) ||
7215
7558
  (isRecord(turn.callback_delivery) &&
7216
7559
  !callbackDelivered) ||
7217
7560
  (Boolean(turn.gateway_method) &&
7218
7561
  !callbackDelivered &&
7219
- !explicitlyAbandonedUncertain) ||
7562
+ !explicitlyAbandonedUncertain &&
7563
+ !safelyAbortedBeforeInput) ||
7220
7564
  stringValue(turn.terminal_binding_id) !== binding.binding_id ||
7221
7565
  Number(turn.terminal_binding_generation) !== binding.generation ||
7222
7566
  (stringValue(turn.native_thread_id) ??
@@ -7497,7 +7841,7 @@ function candidateSourceTransitionHistoryIsTerminal(storeDir, session) {
7497
7841
  return false;
7498
7842
  }
7499
7843
  }
7500
- function deferredCodexForegroundBindingToken({ terminalId, terminalControl, pid, workspace, processUuid, processBirth, sourceSession, dispatchSnapshot, candidateInventory, sourceTurnHistory, candidateContextFingerprint, sourceRolloutAuthority = "present", sourceAbandonmentFingerprint }) {
7844
+ function deferredCodexForegroundBindingToken({ terminalId, terminalControl, pid, workspace, processUuid, processBirth, sourceSession, dispatchSnapshot, candidateInventory, sourceTurnHistory, sourceRolloutAuthority = "present", sourceAbandonmentFingerprint }) {
7501
7845
  const terminalToken = unmanagedTerminalBindingToken({
7502
7846
  terminalId,
7503
7847
  terminalControl,
@@ -7530,17 +7874,12 @@ function deferredCodexForegroundBindingToken({ terminalId, terminalControl, pid,
7530
7874
  source_revision: managedSessionRevision(sourceSession),
7531
7875
  source_binding_token: managedSessionBindingToken(sourceSession),
7532
7876
  terminal_dispatch_snapshot: dispatchSnapshot,
7533
- observation: candidateContextFingerprint
7534
- ? "latent_codex_thread_reset"
7535
- : candidateAuthority
7536
- ? "exact_open_root_inventory"
7537
- : "verified_absent",
7877
+ observation: candidateAuthority
7878
+ ? "exact_open_root_inventory"
7879
+ : "verified_absent",
7538
7880
  ...(sourceTurnHistoryFingerprint
7539
7881
  ? { source_turn_history_fingerprint: sourceTurnHistoryFingerprint }
7540
7882
  : {}),
7541
- ...(candidateContextFingerprint
7542
- ? { candidate_context_fingerprint: candidateContextFingerprint }
7543
- : {}),
7544
7883
  ...(candidateAuthority
7545
7884
  ? { source_rollout_authority: sourceRolloutAuthority }
7546
7885
  : {}),
@@ -7978,16 +8317,6 @@ async function assertDeferredCodexForegroundBindingBoundary({ options, boundary,
7978
8317
  physicalOnly: boundary.candidateAcceptanceAnchor !== undefined
7979
8318
  })
7980
8319
  });
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
- }
7991
8320
  if (status.reachable !== true ||
7992
8321
  status.approval_state.blocked === true ||
7993
8322
  !["idle", "unknown"].includes(status.activity_state)) {
@@ -8003,15 +8332,22 @@ async function assertDeferredCodexForegroundBindingBoundary({ options, boundary,
8003
8332
  }
8004
8333
  return source;
8005
8334
  }
8006
- async function prepareDeferredCodexForegroundBinding({ options, terminal, sourceSession, observation, candidateInventory, requestText }) {
8335
+ async function prepareDeferredCodexForegroundBinding({ options, terminal, sourceSession, observation, candidateInventory, requestText, allowImplicitFreshAuthority = false }) {
8007
8336
  const expectedToken = stringValue(options.expectedTerminalToken);
8008
8337
  const candidateMode = Boolean(candidateInventory?.roots.length);
8009
8338
  if (terminal.agent !== "codex" ||
8010
8339
  !sourceSession?.binding ||
8011
8340
  (observation.status !== "verified_absent" && !candidateMode) ||
8012
- !expectedToken) {
8341
+ (!expectedToken && !allowImplicitFreshAuthority)) {
8013
8342
  return undefined;
8014
8343
  }
8344
+ if (allowImplicitFreshAuthority &&
8345
+ (expectedToken !== undefined ||
8346
+ !candidateMode ||
8347
+ !isCompleteNativeRollout(sourceSession.binding.native_process.rollout))) {
8348
+ throw new Error("implicit Codex candidate authority requires one fresh complete " +
8349
+ "nonempty rollout inventory for a rollout-backed source");
8350
+ }
8015
8351
  const processUuid = sourceSession.binding.native_process.process_uuid;
8016
8352
  const processBirth = sourceSession.binding.native_process.process_birth;
8017
8353
  const workspace = terminal.terminalControl.currentPath;
@@ -8045,14 +8381,6 @@ async function prepareDeferredCodexForegroundBinding({ options, terminal, source
8045
8381
  dispatchSnapshot
8046
8382
  })
8047
8383
  : 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;
8056
8384
  const exactSource = !workspace
8057
8385
  ? false
8058
8386
  : sourceKind === "candidate_rollout_quiescent"
@@ -8103,15 +8431,26 @@ async function prepareDeferredCodexForegroundBinding({ options, terminal, source
8103
8431
  sourceSession,
8104
8432
  dispatchSnapshot,
8105
8433
  sourceTurnHistory: candidateSourceTurnHistory,
8106
- candidateContextFingerprint,
8107
8434
  sourceRolloutAuthority,
8108
8435
  sourceAbandonmentFingerprint,
8109
8436
  ...(candidateMode ? { candidateInventory } : {})
8110
8437
  });
8111
- if (expectedToken !== token) {
8438
+ if (!allowImplicitFreshAuthority && expectedToken !== token) {
8112
8439
  throw new Error("deferred Codex foreground binding requires the fresh exact terminal " +
8113
8440
  "token advertised by AKK list");
8114
8441
  }
8442
+ if (allowImplicitFreshAuthority) {
8443
+ runtimeLog("info", "deferred_codex_implicit_candidate_authority", {
8444
+ terminal_id: terminal.conversationId,
8445
+ terminal_target: terminal.terminalControl.target,
8446
+ source_session_id: sourceSession.session_id,
8447
+ inventory_status: candidateInventory?.status,
8448
+ inventory_fingerprint: candidateInventory?.inventoryFingerprint,
8449
+ candidate_count: candidateInventory?.roots.length,
8450
+ authority_scope: "terminal_follow_current",
8451
+ terminal_input_sent: false
8452
+ });
8453
+ }
8115
8454
  const targetSessionId = createManagedSessionId();
8116
8455
  const transferId = createDeferredForegroundTransferId();
8117
8456
  const candidateAcceptanceAnchor = candidateMode
@@ -8135,9 +8474,6 @@ async function prepareDeferredCodexForegroundBinding({ options, terminal, source
8135
8474
  ...(sourceAbandonmentFingerprint
8136
8475
  ? { sourceAbandonmentFingerprint }
8137
8476
  : {}),
8138
- ...(candidateContextFingerprint
8139
- ? { candidateContextFingerprint }
8140
- : {}),
8141
8477
  ...(sourceSession.last_transition_id
8142
8478
  ? { sourcePreviousLastTransitionId: sourceSession.last_transition_id }
8143
8479
  : {}),
@@ -8162,13 +8498,14 @@ async function prepareDeferredCodexForegroundBinding({ options, terminal, source
8162
8498
  terminalControl: terminal.terminalControl,
8163
8499
  excludedManagedSessionId: sourceSession.session_id
8164
8500
  });
8165
- if (sourceRolloutAuthority === "explicitly_abandoned_predecessor" &&
8501
+ if (candidateInventory &&
8166
8502
  !codexCandidateInventoryHasNoOtherManagedClaim({
8167
8503
  storeDir,
8168
- inventory: required(candidateInventory, "Codex candidate inventory is unavailable"),
8169
- sourceSessionId: sourceSession.session_id
8504
+ inventory: candidateInventory,
8505
+ sourceSessionId: sourceSession.session_id,
8506
+ includeDetached: sourceRolloutAuthority === "explicitly_abandoned_predecessor"
8170
8507
  })) {
8171
- throw new Error("the post-/clear Codex rollout candidate is already claimed by another Session");
8508
+ throw new Error("a Codex rollout candidate is already claimed by another Session");
8172
8509
  }
8173
8510
  if (candidateInventory &&
8174
8511
  sourceRolloutAuthority === "explicitly_abandoned_predecessor") {
@@ -12094,6 +12431,10 @@ function terminalBridgeApprovalFingerprint({ terminalControl, terminalStatus })
12094
12431
  if (adapterFingerprint) {
12095
12432
  return adapterFingerprint;
12096
12433
  }
12434
+ if (approval.approvable === true &&
12435
+ (stringValue(approval.decision_mode) ?? "keys") === "keys") {
12436
+ return undefined;
12437
+ }
12097
12438
  const endpoint = terminalEndpointFromControlRef(terminalControl);
12098
12439
  const processAnchorPid = Number(endpoint.processAnchorPid);
12099
12440
  if (!Number.isSafeInteger(processAnchorPid) || processAnchorPid <= 1) {
@@ -12569,6 +12910,11 @@ async function runSend(options) {
12569
12910
  }
12570
12911
  assertTerminalIncarnationCanStartTurn(rawStoreDir, terminalConversation.terminalControl);
12571
12912
  let claimedSession = soleBoundManagedSessionClaimForTerminal(rawStoreDir, terminalConversation);
12913
+ const suppliedExpectedTerminalToken = stringValue(options.expectedTerminalToken);
12914
+ const implicitCodexCandidateAuthority = Boolean(terminalConversation.agent === "codex" &&
12915
+ suppliedExpectedTerminalToken === undefined &&
12916
+ claimedSession?.status === "bound" &&
12917
+ isCompleteNativeRollout(claimedSession.binding?.native_process.rollout));
12572
12918
  let knownCodexCompanions = claimedSession
12573
12919
  ? codexAllowedCompanionSetForManagedSession({
12574
12920
  storeDir: rawStoreDir,
@@ -12588,7 +12934,7 @@ async function runSend(options) {
12588
12934
  });
12589
12935
  let deferredCodexCandidateInventory;
12590
12936
  if (terminalConversation.agent === "codex" &&
12591
- stringValue(options.expectedTerminalToken)) {
12937
+ (suppliedExpectedTerminalToken || implicitCodexCandidateAuthority)) {
12592
12938
  try {
12593
12939
  const inventory = await inspectCodexOpenRootRolloutInventory({
12594
12940
  options,
@@ -12600,12 +12946,18 @@ async function runSend(options) {
12600
12946
  }
12601
12947
  }
12602
12948
  catch (error) {
12603
- if (nativeIdentityObservation.status === "unavailable") {
12604
- throw new Error(`native Codex identity observation is unavailable and its exact ` +
12605
- `open-root inventory could not be proven: ${error instanceof Error ? error.message : String(error)}`);
12949
+ if (implicitCodexCandidateAuthority ||
12950
+ nativeIdentityObservation.status === "unavailable") {
12951
+ throw new Error(`native Codex foreground attribution requires a fresh complete ` +
12952
+ `open-root inventory: ${error instanceof Error ? error.message : String(error)}`);
12606
12953
  }
12607
12954
  }
12608
12955
  }
12956
+ if (implicitCodexCandidateAuthority &&
12957
+ !deferredCodexCandidateInventory) {
12958
+ throw new Error("rollout-backed Codex terminal send requires a fresh complete " +
12959
+ "nonempty open-root inventory; refresh AKK list before sending");
12960
+ }
12609
12961
  if (nativeIdentityObservation.status === "unavailable" &&
12610
12962
  !deferredCodexCandidateInventory) {
12611
12963
  throw new Error(`native ${terminalConversation.agent} identity observation is ` +
@@ -12614,12 +12966,17 @@ async function runSend(options) {
12614
12966
  let currentNativeIdentity = nativeIdentityObservation.status === "resolved"
12615
12967
  ? nativeIdentityObservation.identity
12616
12968
  : undefined;
12617
- const verifiedEmptyHandoff = await maybeDetachVerifiedEmptyCodexSource({
12618
- options,
12619
- terminal: terminalConversation,
12620
- sourceSession: claimedSession,
12621
- observation: nativeIdentityObservation
12622
- });
12969
+ // A fresh nonempty inventory is the stronger physical authority for an
12970
+ // implicit candidate send. Do not let an earlier verified-absent
12971
+ // observation divert this path into the token-only empty handoff.
12972
+ const verifiedEmptyHandoff = implicitCodexCandidateAuthority
12973
+ ? undefined
12974
+ : await maybeDetachVerifiedEmptyCodexSource({
12975
+ options,
12976
+ terminal: terminalConversation,
12977
+ sourceSession: claimedSession,
12978
+ observation: nativeIdentityObservation
12979
+ });
12623
12980
  if (verifiedEmptyHandoff) {
12624
12981
  // The old rollout is conclusively closed. Never carry it forward as
12625
12982
  // a pre-materialization companion for the new virgin Session.
@@ -12631,7 +12988,13 @@ async function runSend(options) {
12631
12988
  let handoff = await maybeAdoptObservedExternalThread({
12632
12989
  options,
12633
12990
  terminal: terminalConversation,
12634
- sourceSession: claimedSession,
12991
+ // A no-token raw send to an already managed rollout-backed Codex pane
12992
+ // is an internal follow-current delegation, never a sole-root strict
12993
+ // continuation or an external-handoff adoption. The dedicated v3
12994
+ // transfer below owns attribution and lineage.
12995
+ sourceSession: implicitCodexCandidateAuthority
12996
+ ? undefined
12997
+ : claimedSession,
12635
12998
  resolvedIdentity: currentNativeIdentity,
12636
12999
  storeDir: rawStoreDir
12637
13000
  });
@@ -12648,7 +13011,8 @@ async function runSend(options) {
12648
13011
  sourceSession: claimedSession,
12649
13012
  observation: nativeIdentityObservation,
12650
13013
  candidateInventory: deferredCodexCandidateInventory,
12651
- requestText: String(messageBody)
13014
+ requestText: String(messageBody),
13015
+ allowImplicitFreshAuthority: implicitCodexCandidateAuthority
12652
13016
  })
12653
13017
  : undefined;
12654
13018
  if (deferredCodexForegroundBinding) {
@@ -12657,7 +13021,7 @@ async function runSend(options) {
12657
13021
  currentNativeIdentity = undefined;
12658
13022
  handoff = { identity: undefined, adopted: false };
12659
13023
  }
12660
- else if (stringValue(options.expectedTerminalToken) &&
13024
+ else if ((suppliedExpectedTerminalToken || implicitCodexCandidateAuthority) &&
12661
13025
  !verifiedEmptyHandoff &&
12662
13026
  !handoff.adopted) {
12663
13027
  throw new Error("the expected terminal token no longer authorizes the current " +
@@ -12925,6 +13289,17 @@ async function runSend(options) {
12925
13289
  const releaseTerminalLock = acquireTerminalBridgeSendLock(storeDir, resolvedTerminal.terminalControl, { timeoutMs: 30000 });
12926
13290
  try {
12927
13291
  await withStoreWriterLeaseAsync(storeDir, async () => {
13292
+ const lockedStrictSession = tryLoadManagedSession(storeDir, sessionId);
13293
+ if (lockedStrictSession?.agent === "codex" &&
13294
+ lockedStrictSession.session_id === sessionId &&
13295
+ lockedStrictSession.status === "bound" &&
13296
+ isCompleteNativeRollout(lockedStrictSession.binding?.native_process.rollout)) {
13297
+ throw new Error(`Codex rollout-backed managed Session ${sessionId} cannot use a ` +
13298
+ "strict session_id send because an open rollout does not prove the " +
13299
+ "current TUI foreground thread. Refresh AKK list and use its exact " +
13300
+ "selector plus expected_terminal_token. No Turn was created and no " +
13301
+ "terminal input was sent.");
13302
+ }
12928
13303
  await recoverLifecycleFenceBeforeMutation({
12929
13304
  options,
12930
13305
  terminal: resolvedTerminal
@@ -12995,11 +13370,6 @@ async function runSend(options) {
12995
13370
  if (!bindingMatchesLiveTerminal(currentSession, resolvedTerminal, lockedNativeIdentity, storeDir)) {
12996
13371
  throw new Error("managed session identity changed while waiting to send; refresh list and retry");
12997
13372
  }
12998
- await assertStrictCodexSessionHasNoLatentClear({
12999
- options,
13000
- terminal: resolvedTerminal,
13001
- session: currentSession
13002
- });
13003
13373
  const logicalLockedNativeIdentity = logicalIdentityForManagedSession({
13004
13374
  storeDir,
13005
13375
  session: currentSession,
@@ -13446,331 +13816,337 @@ async function runApprove(options) {
13446
13816
  release();
13447
13817
  }
13448
13818
  };
13449
- releaseStateLock = acquireFileLock(`${statePath}.lock`);
13450
13819
  const writerStoreDir = pathsForConversationDir(path.dirname(statePath)).storeDir;
13451
- try {
13452
- return await withStoreWriterLeaseAsync(writerStoreDir, async () => {
13453
- let approval;
13454
- let lockedConversation = conversation;
13455
- const currentConversation = loadState(statePath);
13456
- const currentTakeover = isRecord(currentConversation.native_session_takeover)
13457
- ? currentConversation.native_session_takeover
13458
- : undefined;
13459
- const currentControl = terminalControlFromTakeover(currentTakeover);
13460
- const currentApproval = isRecord(currentTakeover?.terminal_bridge_approval)
13461
- ? currentTakeover.terminal_bridge_approval
13462
- : undefined;
13463
- if (currentConversation.status !== conversation.status ||
13464
- currentTakeover?.terminal_bridge_message_id !== nativeTakeover?.terminal_bridge_message_id ||
13465
- !currentControl ||
13466
- !terminalControlsShareIncarnation(currentControl, terminalControl) ||
13467
- (claudeScreenApproval &&
13468
- currentApproval?.fingerprint !== monitoredApproval?.fingerprint)) {
13469
- throw new Error("approval state changed while waiting for terminal control; refresh status and retry");
13470
- }
13471
- assertManagedTerminalDispatchOwner({
13472
- storeDir: writerStoreDir,
13473
- conversation: currentConversation,
13474
- terminalControl: currentControl,
13475
- action: "approve"
13476
- });
13477
- lockedConversation = currentConversation;
13478
- const currentRuntimeIdentity = terminalRuntimeIdentityForConversation(currentConversation, currentControl);
13479
- approval = await createTerminalAgentBridge(options).approve(executor.kind, currentControl, {
13480
- expectedFingerprint,
13481
- scrollbackLines: Number(options.scrollbackLines ?? 120),
13482
- runtime: currentRuntimeIdentity,
13483
- managedRequest: terminalDurableRequestForConversation(currentConversation, currentControl),
13484
- requiredDecisionMode: autoApproved && executor.kind === "claude" ? "keys" : undefined,
13485
- authorize: autoApproved
13486
- ? ({ agent, terminalControl: currentTerminalControl, inspection, fingerprint }) => {
13820
+ const runApprovalWithStateLock = async () => {
13821
+ let approval;
13822
+ let lockedConversation = conversation;
13823
+ const currentConversation = loadState(statePath);
13824
+ const currentTakeover = isRecord(currentConversation.native_session_takeover)
13825
+ ? currentConversation.native_session_takeover
13826
+ : undefined;
13827
+ const currentControl = terminalControlFromTakeover(currentTakeover);
13828
+ const currentApproval = isRecord(currentTakeover?.terminal_bridge_approval)
13829
+ ? currentTakeover.terminal_bridge_approval
13830
+ : undefined;
13831
+ if (currentConversation.status !== conversation.status ||
13832
+ currentTakeover?.terminal_bridge_message_id !== nativeTakeover?.terminal_bridge_message_id ||
13833
+ !currentControl ||
13834
+ !terminalControlsShareIncarnation(currentControl, terminalControl) ||
13835
+ (claudeScreenApproval &&
13836
+ currentApproval?.fingerprint !== monitoredApproval?.fingerprint)) {
13837
+ throw new Error("approval state changed while waiting for terminal control; refresh status and retry");
13838
+ }
13839
+ assertManagedTerminalDispatchOwner({
13840
+ storeDir: writerStoreDir,
13841
+ conversation: currentConversation,
13842
+ terminalControl: currentControl,
13843
+ action: "approve"
13844
+ });
13845
+ lockedConversation = currentConversation;
13846
+ const currentRuntimeIdentity = terminalRuntimeIdentityForConversation(currentConversation, currentControl);
13847
+ approval = await createTerminalAgentBridge(options).approve(executor.kind, currentControl, {
13848
+ expectedFingerprint,
13849
+ scrollbackLines: Number(options.scrollbackLines ?? 120),
13850
+ runtime: currentRuntimeIdentity,
13851
+ managedRequest: terminalDurableRequestForConversation(currentConversation, currentControl),
13852
+ requiredDecisionMode: autoApproved && executor.kind === "claude" ? "keys" : undefined,
13853
+ authorize: autoApproved
13854
+ ? ({ agent, terminalControl: currentTerminalControl, inspection, fingerprint }) => {
13855
+ if (!autoApprovalPolicy) {
13856
+ return {
13857
+ approved: false,
13858
+ reason: "automatic approval requires an executor-side policy"
13859
+ };
13860
+ }
13861
+ const candidate = policyCandidateForInspection({
13862
+ agent,
13863
+ currentTerminalControl,
13864
+ inspection,
13865
+ fingerprint
13866
+ });
13867
+ executorPolicyDecision = evaluateApprovalPolicy({
13868
+ policy: autoApprovalPolicy,
13869
+ candidate
13870
+ });
13871
+ if (executorPolicyDecision.action !== "approve") {
13872
+ return {
13873
+ approved: false,
13874
+ reason: `executor-side auto-approval policy rejected the current request: ${executorPolicyDecision.reason}`
13875
+ };
13876
+ }
13877
+ if (policyRuleId && executorPolicyDecision.ruleId !== policyRuleId) {
13878
+ return {
13879
+ approved: false,
13880
+ reason: "executor-side auto-approval rule changed before execution"
13881
+ };
13882
+ }
13883
+ if (policyFingerprint &&
13884
+ executorPolicyDecision.policyFingerprint !== policyFingerprint) {
13885
+ return {
13886
+ approved: false,
13887
+ reason: "executor-side auto-approval policy changed before execution"
13888
+ };
13889
+ }
13890
+ return { approved: true };
13891
+ }
13892
+ : undefined,
13893
+ beforeKeyDispatch: claudeScreenApproval
13894
+ ? ({ fingerprint, terminalControl: dispatchControl, inspection, keys }) => {
13895
+ if (autoApproved) {
13487
13896
  if (!autoApprovalPolicy) {
13488
- return {
13489
- approved: false,
13490
- reason: "automatic approval requires an executor-side policy"
13491
- };
13897
+ throw new Error("automatic approval requires an executor-side policy before dispatch");
13492
13898
  }
13493
- const candidate = policyCandidateForInspection({
13494
- agent,
13495
- currentTerminalControl,
13496
- inspection,
13497
- fingerprint
13498
- });
13499
- executorPolicyDecision = evaluateApprovalPolicy({
13899
+ const freshPolicyDecision = evaluateApprovalPolicy({
13500
13900
  policy: autoApprovalPolicy,
13501
- candidate
13901
+ candidate: policyCandidateForInspection({
13902
+ agent: executor.kind,
13903
+ currentTerminalControl: dispatchControl,
13904
+ inspection,
13905
+ fingerprint
13906
+ })
13502
13907
  });
13503
- if (executorPolicyDecision.action !== "approve") {
13504
- return {
13505
- approved: false,
13506
- reason: `executor-side auto-approval policy rejected the current request: ${executorPolicyDecision.reason}`
13507
- };
13908
+ if (freshPolicyDecision.action !== "approve") {
13909
+ throw new Error(`executor-side auto-approval policy rejected the recaptured request: ${freshPolicyDecision.reason}`);
13508
13910
  }
13509
- if (policyRuleId && executorPolicyDecision.ruleId !== policyRuleId) {
13510
- return {
13511
- approved: false,
13512
- reason: "executor-side auto-approval rule changed before execution"
13513
- };
13911
+ if (executorPolicyDecision?.ruleId &&
13912
+ freshPolicyDecision.ruleId !== executorPolicyDecision.ruleId) {
13913
+ throw new Error("executor-side auto-approval rule changed after recapture");
13914
+ }
13915
+ if (policyRuleId && freshPolicyDecision.ruleId !== policyRuleId) {
13916
+ throw new Error("executor-side auto-approval rule changed before dispatch");
13514
13917
  }
13515
13918
  if (policyFingerprint &&
13516
- executorPolicyDecision.policyFingerprint !== policyFingerprint) {
13517
- return {
13518
- approved: false,
13519
- reason: "executor-side auto-approval policy changed before execution"
13520
- };
13919
+ freshPolicyDecision.policyFingerprint !== policyFingerprint) {
13920
+ throw new Error("executor-side auto-approval policy changed before dispatch");
13521
13921
  }
13522
- return { approved: true };
13922
+ executorPolicyDecision = freshPolicyDecision;
13523
13923
  }
13524
- : undefined,
13525
- beforeKeyDispatch: claudeScreenApproval
13526
- ? ({ fingerprint, terminalControl: dispatchControl, inspection, keys }) => {
13527
- if (autoApproved) {
13528
- if (!autoApprovalPolicy) {
13529
- throw new Error("automatic approval requires an executor-side policy before dispatch");
13530
- }
13531
- const freshPolicyDecision = evaluateApprovalPolicy({
13532
- policy: autoApprovalPolicy,
13533
- candidate: policyCandidateForInspection({
13534
- agent: executor.kind,
13535
- currentTerminalControl: dispatchControl,
13536
- inspection,
13537
- fingerprint
13538
- })
13539
- });
13540
- if (freshPolicyDecision.action !== "approve") {
13541
- throw new Error(`executor-side auto-approval policy rejected the recaptured request: ${freshPolicyDecision.reason}`);
13542
- }
13543
- if (executorPolicyDecision?.ruleId &&
13544
- freshPolicyDecision.ruleId !== executorPolicyDecision.ruleId) {
13545
- throw new Error("executor-side auto-approval rule changed after recapture");
13546
- }
13547
- if (policyRuleId && freshPolicyDecision.ruleId !== policyRuleId) {
13548
- throw new Error("executor-side auto-approval rule changed before dispatch");
13549
- }
13550
- if (policyFingerprint &&
13551
- freshPolicyDecision.policyFingerprint !== policyFingerprint) {
13552
- throw new Error("executor-side auto-approval policy changed before dispatch");
13553
- }
13554
- executorPolicyDecision = freshPolicyDecision;
13555
- }
13556
- if (approvalDispatchReserved) {
13557
- throw new Error("Claude approval dispatch was already reserved");
13558
- }
13559
- approvalDispatchReserved = true;
13560
- if (!releaseStateLock) {
13561
- throw new Error("approval state lock was released before terminal dispatch");
13562
- }
13563
- const latestConversation = loadState(statePath);
13564
- const latestTakeover = isRecord(latestConversation.native_session_takeover)
13565
- ? latestConversation.native_session_takeover
13566
- : undefined;
13567
- const latestControl = terminalControlFromTakeover(latestTakeover);
13568
- const latestApproval = isRecord(latestTakeover?.terminal_bridge_approval)
13569
- ? latestTakeover.terminal_bridge_approval
13570
- : undefined;
13571
- const latestNotifiedAt = validTimestampMs(latestApproval?.notified_at);
13572
- const latestApprovalState = isRecord(latestApproval?.approval_state)
13573
- ? latestApproval.approval_state
13574
- : undefined;
13575
- const latestPolicyEvidence = isRecord(latestApprovalState?.policy_evidence)
13576
- ? latestApprovalState.policy_evidence
13577
- : undefined;
13578
- const recapturedPolicyEvidence = inspection.approval.approvable
13579
- ? inspection.approval.policyEvidence
13580
- : undefined;
13581
- const latestDispatch = isRecord(latestTakeover?.terminal_bridge_approval_dispatch)
13582
- ? latestTakeover.terminal_bridge_approval_dispatch
13583
- : undefined;
13584
- if (!latestTakeover ||
13585
- latestConversation.status !== "waiting_for_openclaw" ||
13586
- latestTakeover.terminal_bridge_message_id !==
13587
- nativeTakeover?.terminal_bridge_message_id ||
13588
- latestApproval?.fingerprint !== fingerprint ||
13589
- latestNotifiedAt === undefined ||
13590
- cliNowMs() - latestNotifiedAt > CLAUDE_SCREEN_APPROVAL_TTL_MS ||
13591
- expectedFingerprint !== fingerprint ||
13592
- !terminalControlsShareIncarnation(latestControl, dispatchControl) ||
13593
- (autoApproved &&
13594
- (latestPolicyEvidence?.source !== "claude_transcript" ||
13595
- latestPolicyEvidence.evidence_fingerprint !==
13596
- recapturedPolicyEvidence?.evidenceFingerprint))) {
13597
- throw new Error("approval state changed before terminal dispatch; refresh status and retry");
13598
- }
13599
- if (latestDispatch?.state === "reserved" &&
13600
- latestDispatch.terminal_bridge_message_id ===
13601
- latestTakeover.terminal_bridge_message_id) {
13602
- throw new Error("a previous Claude approval dispatch has an uncertain outcome; inspect and resolve the terminal manually");
13603
- }
13604
- const reservedAt = cliNow().toISOString();
13605
- const reservedConversation = {
13606
- ...latestConversation,
13607
- native_session_takeover: {
13608
- ...latestTakeover,
13609
- terminal_bridge_approval_dispatch: {
13610
- state: "reserved",
13611
- attempt_id: randomUUID(),
13612
- fingerprint,
13613
- keys,
13614
- terminal_target: dispatchControl.target,
13615
- terminal_bridge_message_id: latestTakeover.terminal_bridge_message_id,
13616
- reserved_at: reservedAt
13617
- }
13618
- },
13619
- updated_at: reservedAt
13620
- };
13621
- saveState(statePath, reservedConversation);
13622
- lockedConversation = reservedConversation;
13924
+ if (approvalDispatchReserved) {
13925
+ throw new Error("Claude approval dispatch was already reserved");
13623
13926
  }
13624
- : undefined
13625
- });
13626
- const actualFingerprint = approval.fingerprint;
13627
- const effectivePolicyRuleId = executorPolicyDecision?.ruleId ?? policyRuleId;
13628
- const effectivePolicyFingerprint = executorPolicyDecision?.policyFingerprint ?? policyFingerprint;
13629
- if (!approval.approved) {
13630
- releaseApprovalStateLock();
13631
- releaseApprovalTerminalLock();
13632
- if (autoApproved) {
13633
- appendEvent(logPath, {
13634
- ts: cliNow().toISOString(),
13635
- conversation_id: conversation.conversation_id,
13636
- event: "terminal_auto_approval_decision",
13637
- action: "rejected",
13638
- reason: approval.reason,
13639
- terminal_control: terminalControl,
13640
- expected_fingerprint: expectedFingerprint,
13641
- actual_fingerprint: actualFingerprint,
13642
- policy_rule_id: effectivePolicyRuleId,
13643
- policy_fingerprint: effectivePolicyFingerprint
13644
- });
13927
+ approvalDispatchReserved = true;
13928
+ if (!releaseStateLock) {
13929
+ throw new Error("approval state lock was released before terminal dispatch");
13930
+ }
13931
+ const latestConversation = loadState(statePath);
13932
+ const latestTakeover = isRecord(latestConversation.native_session_takeover)
13933
+ ? latestConversation.native_session_takeover
13934
+ : undefined;
13935
+ const latestControl = terminalControlFromTakeover(latestTakeover);
13936
+ const latestApproval = isRecord(latestTakeover?.terminal_bridge_approval)
13937
+ ? latestTakeover.terminal_bridge_approval
13938
+ : undefined;
13939
+ const latestNotifiedAt = validTimestampMs(latestApproval?.notified_at);
13940
+ const latestApprovalState = isRecord(latestApproval?.approval_state)
13941
+ ? latestApproval.approval_state
13942
+ : undefined;
13943
+ const latestPolicyEvidence = isRecord(latestApprovalState?.policy_evidence)
13944
+ ? latestApprovalState.policy_evidence
13945
+ : undefined;
13946
+ const recapturedPolicyEvidence = inspection.approval.approvable
13947
+ ? inspection.approval.policyEvidence
13948
+ : undefined;
13949
+ const latestDispatch = isRecord(latestTakeover?.terminal_bridge_approval_dispatch)
13950
+ ? latestTakeover.terminal_bridge_approval_dispatch
13951
+ : undefined;
13952
+ if (!latestTakeover ||
13953
+ latestConversation.status !== "waiting_for_openclaw" ||
13954
+ latestTakeover.terminal_bridge_message_id !==
13955
+ nativeTakeover?.terminal_bridge_message_id ||
13956
+ latestApproval?.fingerprint !== fingerprint ||
13957
+ latestNotifiedAt === undefined ||
13958
+ cliNowMs() - latestNotifiedAt > CLAUDE_SCREEN_APPROVAL_TTL_MS ||
13959
+ expectedFingerprint !== fingerprint ||
13960
+ !terminalControlsShareIncarnation(latestControl, dispatchControl) ||
13961
+ (autoApproved &&
13962
+ (latestPolicyEvidence?.source !== "claude_transcript" ||
13963
+ latestPolicyEvidence.evidence_fingerprint !==
13964
+ recapturedPolicyEvidence?.evidenceFingerprint))) {
13965
+ throw new Error("approval state changed before terminal dispatch; refresh status and retry");
13966
+ }
13967
+ if (latestDispatch?.state === "reserved" &&
13968
+ latestDispatch.terminal_bridge_message_id ===
13969
+ latestTakeover.terminal_bridge_message_id) {
13970
+ throw new Error("a previous Claude approval dispatch has an uncertain outcome; inspect and resolve the terminal manually");
13971
+ }
13972
+ const reservedAt = cliNow().toISOString();
13973
+ const reservedConversation = {
13974
+ ...latestConversation,
13975
+ native_session_takeover: {
13976
+ ...latestTakeover,
13977
+ terminal_bridge_approval_dispatch: {
13978
+ state: "reserved",
13979
+ attempt_id: randomUUID(),
13980
+ fingerprint,
13981
+ keys,
13982
+ terminal_target: dispatchControl.target,
13983
+ terminal_bridge_message_id: latestTakeover.terminal_bridge_message_id,
13984
+ reserved_at: reservedAt
13985
+ }
13986
+ },
13987
+ updated_at: reservedAt
13988
+ };
13989
+ saveState(statePath, reservedConversation);
13990
+ lockedConversation = reservedConversation;
13645
13991
  }
13646
- printJson({
13647
- conversation,
13648
- approved: false,
13649
- blocked: approval.blocked,
13650
- reason: approval.reason,
13651
- terminal_control: terminalControl,
13652
- expected_approval_fingerprint: expectedFingerprint,
13653
- actual_approval_fingerprint: actualFingerprint,
13654
- screen_excerpt: approval.screenExcerpt
13655
- });
13656
- return;
13657
- }
13658
- appendEvent(logPath, {
13659
- ts: cliNow().toISOString(),
13660
- conversation_id: conversation.conversation_id,
13661
- event: "terminal_approval_send",
13662
- terminal_control: terminalControl,
13663
- key: approval.key,
13664
- keys: approval.keys,
13665
- label: approval.label,
13666
- decision_mode: approval.decisionMode,
13667
- request_id: approval.requestId,
13668
- approval_fingerprint: actualFingerprint,
13669
- auto_approved: autoApproved,
13670
- policy_rule_id: effectivePolicyRuleId,
13671
- policy_fingerprint: effectivePolicyFingerprint
13672
- });
13992
+ : undefined
13993
+ });
13994
+ const actualFingerprint = approval.fingerprint;
13995
+ const effectivePolicyRuleId = executorPolicyDecision?.ruleId ?? policyRuleId;
13996
+ const effectivePolicyFingerprint = executorPolicyDecision?.policyFingerprint ?? policyFingerprint;
13997
+ if (!approval.approved) {
13998
+ releaseApprovalStateLock();
13673
13999
  if (autoApproved) {
13674
14000
  appendEvent(logPath, {
13675
14001
  ts: cliNow().toISOString(),
13676
14002
  conversation_id: conversation.conversation_id,
13677
14003
  event: "terminal_auto_approval_decision",
13678
- action: "approved",
14004
+ action: "rejected",
14005
+ reason: approval.reason,
13679
14006
  terminal_control: terminalControl,
13680
- approval_fingerprint: actualFingerprint,
14007
+ expected_fingerprint: expectedFingerprint,
14008
+ actual_fingerprint: actualFingerprint,
13681
14009
  policy_rule_id: effectivePolicyRuleId,
13682
14010
  policy_fingerprint: effectivePolicyFingerprint
13683
14011
  });
13684
14012
  }
13685
- runtimeLog("info", "terminal_approval_send", {
13686
- conversation_id: conversation.conversation_id,
13687
- terminal_target: terminalControl.target,
13688
- key: approval.key,
13689
- keys: approval.keys,
13690
- label: approval.label,
13691
- decision_mode: approval.decisionMode,
13692
- request_id: approval.requestId,
13693
- approval_fingerprint: actualFingerprint,
13694
- auto_approved: autoApproved,
13695
- policy_rule_id: effectivePolicyRuleId,
13696
- policy_fingerprint: effectivePolicyFingerprint
13697
- });
13698
- const nativeTakeoverForUpdate = isRecord(lockedConversation.native_session_takeover)
13699
- ? { ...lockedConversation.native_session_takeover }
13700
- : {};
13701
- const resolvedApproval = isRecord(nativeTakeoverForUpdate.terminal_bridge_approval)
13702
- ? nativeTakeoverForUpdate.terminal_bridge_approval
13703
- : undefined;
13704
- const resolvedApprovalScreenDigest = stringValue(resolvedApproval?.screen_digest);
13705
- const resolvedApprovalState = isRecord(resolvedApproval?.approval_state)
13706
- ? resolvedApproval.approval_state
13707
- : undefined;
13708
- const resolvedTranscriptIdentity = claudeTranscriptApprovalIdentity(resolvedApprovalState);
13709
- const approvalResolvedAt = cliNow().toISOString();
13710
- const agentTimeoutMinutes = Number(options.agentTimeoutMinutes ??
13711
- nativeTakeoverForUpdate.terminal_bridge_inactivity_timeout_minutes ??
13712
- DEFAULT_AGENT_TIMEOUT_MINUTES);
13713
- const agentHardTimeoutMinutes = positiveMinutes(options.agentHardTimeoutMinutes ??
13714
- nativeTakeoverForUpdate.terminal_bridge_hard_timeout_minutes ??
13715
- DEFAULT_AGENT_HARD_TIMEOUT_MINUTES, "--agent-hard-timeout-minutes");
13716
- const nextNativeTakeover = {
13717
- ...nativeTakeoverForUpdate,
13718
- terminal_bridge_approval: undefined,
13719
- terminal_bridge_approval_dispatch: undefined,
13720
- terminal_bridge_approval_resolved_at: approvalResolvedAt,
13721
- terminal_bridge_last_approval_fingerprint: actualFingerprint,
13722
- terminal_bridge_last_approval_screen_digest: resolvedApprovalScreenDigest,
13723
- terminal_bridge_last_approval_request_id: resolvedTranscriptIdentity?.requestId,
13724
- terminal_bridge_last_approval_evidence_fingerprint: resolvedTranscriptIdentity?.evidenceFingerprint,
13725
- terminal_bridge_last_approval_prompt_cleared_at: undefined,
13726
- terminal_bridge_last_approval_at: approvalResolvedAt,
13727
- terminal_bridge_last_approval_message_id: nativeTakeoverForUpdate.terminal_bridge_message_id,
13728
- terminal_bridge_monitor_lock_version: TERMINAL_BRIDGE_MONITOR_LOCK_VERSION,
13729
- terminal_bridge_monitor_started_at: approvalResolvedAt,
13730
- terminal_bridge_last_activity_at: approvalResolvedAt,
13731
- terminal_bridge_last_activity_reason: "approval resolved",
13732
- terminal_bridge_inactivity_timeout_minutes: agentTimeoutMinutes,
13733
- terminal_bridge_hard_timeout_minutes: agentHardTimeoutMinutes,
13734
- terminal_bridge_inactivity_deadline_at: deadlineAt(approvalResolvedAt, agentTimeoutMinutes),
13735
- terminal_bridge_hard_deadline_at: deadlineAt(stringValue(nativeTakeoverForUpdate.terminal_bridge_started_at) ?? approvalResolvedAt, agentHardTimeoutMinutes)
13736
- };
13737
- delete nextNativeTakeover.terminal_bridge_approval;
13738
- delete nextNativeTakeover.terminal_bridge_approval_dispatch;
13739
- delete nextNativeTakeover.terminal_bridge_last_approval_prompt_cleared_at;
13740
- const nextConversation = {
13741
- ...lockedConversation,
13742
- status: terminalBridgeEnabled(lockedConversation)
13743
- ? "waiting_for_agent"
13744
- : lockedConversation.status,
13745
- native_session_takeover: nextNativeTakeover,
13746
- updated_at: approvalResolvedAt
13747
- };
13748
- saveState(statePath, nextConversation);
13749
- releaseApprovalStateLock();
13750
- releaseApprovalTerminalLock();
13751
- const bridgeMonitor = ensureTerminalBridgeMonitorAfterApproval({
13752
- conversation: nextConversation,
13753
- statePath,
13754
- logPath,
13755
- terminalControl,
13756
- options
13757
- });
13758
14013
  printJson({
13759
- conversation: nextConversation,
13760
- approved: true,
14014
+ conversation,
14015
+ approved: false,
14016
+ blocked: approval.blocked,
14017
+ reason: approval.reason,
14018
+ terminal_control: terminalControl,
14019
+ expected_approval_fingerprint: expectedFingerprint,
14020
+ actual_approval_fingerprint: actualFingerprint,
14021
+ screen_excerpt: approval.screenExcerpt
14022
+ });
14023
+ return;
14024
+ }
14025
+ appendEvent(logPath, {
14026
+ ts: cliNow().toISOString(),
14027
+ conversation_id: conversation.conversation_id,
14028
+ event: "terminal_approval_send",
14029
+ terminal_control: terminalControl,
14030
+ key: approval.key,
14031
+ keys: approval.keys,
14032
+ label: approval.label,
14033
+ decision_mode: approval.decisionMode,
14034
+ request_id: approval.requestId,
14035
+ approval_fingerprint: actualFingerprint,
14036
+ auto_approved: autoApproved,
14037
+ policy_rule_id: effectivePolicyRuleId,
14038
+ policy_fingerprint: effectivePolicyFingerprint
14039
+ });
14040
+ if (autoApproved) {
14041
+ appendEvent(logPath, {
14042
+ ts: cliNow().toISOString(),
14043
+ conversation_id: conversation.conversation_id,
14044
+ event: "terminal_auto_approval_decision",
14045
+ action: "approved",
13761
14046
  terminal_control: terminalControl,
13762
- key: approval.key,
13763
- keys: approval.keys,
13764
- label: approval.label,
13765
- decision_mode: approval.decisionMode,
13766
- request_id: approval.requestId,
13767
14047
  approval_fingerprint: actualFingerprint,
13768
- auto_approved: autoApproved,
13769
14048
  policy_rule_id: effectivePolicyRuleId,
13770
- policy_fingerprint: effectivePolicyFingerprint,
13771
- monitor_pid: bridgeMonitor.monitorPid ?? null,
13772
- monitor_handoff_pid: bridgeMonitor.handoffWatchdog?.pid ?? null
14049
+ policy_fingerprint: effectivePolicyFingerprint
13773
14050
  });
14051
+ }
14052
+ runtimeLog("info", "terminal_approval_send", {
14053
+ conversation_id: conversation.conversation_id,
14054
+ terminal_target: terminalControl.target,
14055
+ key: approval.key,
14056
+ keys: approval.keys,
14057
+ label: approval.label,
14058
+ decision_mode: approval.decisionMode,
14059
+ request_id: approval.requestId,
14060
+ approval_fingerprint: actualFingerprint,
14061
+ auto_approved: autoApproved,
14062
+ policy_rule_id: effectivePolicyRuleId,
14063
+ policy_fingerprint: effectivePolicyFingerprint
14064
+ });
14065
+ const nativeTakeoverForUpdate = isRecord(lockedConversation.native_session_takeover)
14066
+ ? { ...lockedConversation.native_session_takeover }
14067
+ : {};
14068
+ const resolvedApproval = isRecord(nativeTakeoverForUpdate.terminal_bridge_approval)
14069
+ ? nativeTakeoverForUpdate.terminal_bridge_approval
14070
+ : undefined;
14071
+ const resolvedApprovalScreenDigest = stringValue(resolvedApproval?.screen_digest);
14072
+ const resolvedApprovalState = isRecord(resolvedApproval?.approval_state)
14073
+ ? resolvedApproval.approval_state
14074
+ : undefined;
14075
+ const resolvedTranscriptIdentity = claudeTranscriptApprovalIdentity(resolvedApprovalState);
14076
+ const approvalResolvedAt = cliNow().toISOString();
14077
+ const agentTimeoutMinutes = Number(options.agentTimeoutMinutes ??
14078
+ nativeTakeoverForUpdate.terminal_bridge_inactivity_timeout_minutes ??
14079
+ DEFAULT_AGENT_TIMEOUT_MINUTES);
14080
+ const agentHardTimeoutMinutes = positiveMinutes(options.agentHardTimeoutMinutes ??
14081
+ nativeTakeoverForUpdate.terminal_bridge_hard_timeout_minutes ??
14082
+ DEFAULT_AGENT_HARD_TIMEOUT_MINUTES, "--agent-hard-timeout-minutes");
14083
+ const nextNativeTakeover = {
14084
+ ...nativeTakeoverForUpdate,
14085
+ terminal_bridge_approval: undefined,
14086
+ terminal_bridge_approval_dispatch: undefined,
14087
+ terminal_bridge_approval_resolved_at: approvalResolvedAt,
14088
+ terminal_bridge_last_approval_fingerprint: actualFingerprint,
14089
+ terminal_bridge_last_approval_screen_digest: resolvedApprovalScreenDigest,
14090
+ terminal_bridge_last_approval_request_id: resolvedTranscriptIdentity?.requestId,
14091
+ terminal_bridge_last_approval_evidence_fingerprint: resolvedTranscriptIdentity?.evidenceFingerprint,
14092
+ terminal_bridge_last_approval_prompt_cleared_at: undefined,
14093
+ terminal_bridge_last_approval_at: approvalResolvedAt,
14094
+ terminal_bridge_last_approval_message_id: nativeTakeoverForUpdate.terminal_bridge_message_id,
14095
+ terminal_bridge_monitor_lock_version: TERMINAL_BRIDGE_MONITOR_LOCK_VERSION,
14096
+ terminal_bridge_monitor_started_at: approvalResolvedAt,
14097
+ terminal_bridge_last_activity_at: approvalResolvedAt,
14098
+ terminal_bridge_last_activity_reason: "approval resolved",
14099
+ terminal_bridge_inactivity_timeout_minutes: agentTimeoutMinutes,
14100
+ terminal_bridge_hard_timeout_minutes: agentHardTimeoutMinutes,
14101
+ terminal_bridge_inactivity_deadline_at: deadlineAt(approvalResolvedAt, agentTimeoutMinutes),
14102
+ terminal_bridge_hard_deadline_at: deadlineAt(stringValue(nativeTakeoverForUpdate.terminal_bridge_started_at) ?? approvalResolvedAt, agentHardTimeoutMinutes)
14103
+ };
14104
+ delete nextNativeTakeover.terminal_bridge_approval;
14105
+ delete nextNativeTakeover.terminal_bridge_approval_dispatch;
14106
+ delete nextNativeTakeover.terminal_bridge_last_approval_prompt_cleared_at;
14107
+ const nextConversation = {
14108
+ ...lockedConversation,
14109
+ status: terminalBridgeEnabled(lockedConversation)
14110
+ ? "waiting_for_agent"
14111
+ : lockedConversation.status,
14112
+ native_session_takeover: nextNativeTakeover,
14113
+ updated_at: approvalResolvedAt
14114
+ };
14115
+ saveState(statePath, nextConversation);
14116
+ releaseApprovalStateLock();
14117
+ const bridgeMonitor = ensureTerminalBridgeMonitorAfterApproval({
14118
+ conversation: nextConversation,
14119
+ statePath,
14120
+ logPath,
14121
+ terminalControl,
14122
+ options
14123
+ });
14124
+ printJson({
14125
+ conversation: nextConversation,
14126
+ approved: true,
14127
+ terminal_control: terminalControl,
14128
+ key: approval.key,
14129
+ keys: approval.keys,
14130
+ label: approval.label,
14131
+ decision_mode: approval.decisionMode,
14132
+ request_id: approval.requestId,
14133
+ approval_fingerprint: actualFingerprint,
14134
+ auto_approved: autoApproved,
14135
+ policy_rule_id: effectivePolicyRuleId,
14136
+ policy_fingerprint: effectivePolicyFingerprint,
14137
+ monitor_pid: bridgeMonitor.monitorPid ?? null,
14138
+ monitor_handoff_pid: bridgeMonitor.handoffWatchdog?.pid ?? null
14139
+ });
14140
+ };
14141
+ try {
14142
+ return await withStoreWriterLeaseAsync(writerStoreDir, async () => {
14143
+ releaseStateLock = acquireFileLock(`${statePath}.lock`);
14144
+ try {
14145
+ return await runApprovalWithStateLock();
14146
+ }
14147
+ finally {
14148
+ releaseApprovalStateLock();
14149
+ }
13774
14150
  });
13775
14151
  }
13776
14152
  finally {
@@ -19979,12 +20355,20 @@ async function runTerminalBridgeMonitorWithLock(options, lifecycle, expectedTerm
19979
20355
  });
19980
20356
  }
19981
20357
  }
20358
+ const approvalPromptEvidenceUnavailable = isRecord(approval) &&
20359
+ approval.blocked === true &&
20360
+ approval.approvable === true &&
20361
+ (stringValue(approval.decision_mode) ?? "keys") === "keys" &&
20362
+ !stringValue(approval.fingerprint);
19982
20363
  if (!suppressApprovalNotification &&
19983
20364
  isRecord(approval) &&
19984
20365
  approval.blocked === true &&
19985
- approval.approvable !== true) {
19986
- const approvalReason = stringValue(approval.reason) ??
19987
- "Claude Code permission state cannot be safely resolved through AKK";
20366
+ (approval.approvable !== true ||
20367
+ approvalPromptEvidenceUnavailable)) {
20368
+ const approvalReason = approvalPromptEvidenceUnavailable
20369
+ ? `${executor.display_name} approval prompt lacks exact prompt-region evidence`
20370
+ : stringValue(approval.reason) ??
20371
+ "Claude Code permission state cannot be safely resolved through AKK";
19988
20372
  appendEvent(logPath, {
19989
20373
  ts: cliNow().toISOString(),
19990
20374
  conversation_id: conversation.conversation_id,
@@ -20851,6 +21235,32 @@ function terminalLedgerReceiptCandidate(ledger) {
20851
21235
  }
20852
21236
  function mergeTerminalLedgerReceipt(previous, next) {
20853
21237
  const messageId = required(stringValue(previous.message_id), "terminal dispatch receipt message id is required");
21238
+ const safeAbortRetryGeneration = Boolean(previous.status === "aborted" &&
21239
+ previous.safe_to_retry === true &&
21240
+ stringValue(next.message_id) === messageId &&
21241
+ stringValue(next.previous_generation_id) === messageId &&
21242
+ [
21243
+ "text_injected",
21244
+ "enter_dispatched",
21245
+ "submitted",
21246
+ "agent_accepted",
21247
+ "not_accepted",
21248
+ "uncertain",
21249
+ "aborted"
21250
+ ].includes(String(next.status)) &&
21251
+ validTimestampMs(previous.aborted_at) !== undefined &&
21252
+ validTimestampMs(next.prepared_at) !== undefined &&
21253
+ Date.parse(String(next.prepared_at)) >=
21254
+ Date.parse(String(previous.aborted_at)));
21255
+ if (safeAbortRetryGeneration) {
21256
+ // A proved zero-input abort intentionally allows the same OpenClaw
21257
+ // idempotency key to start one fresh Turn/Session generation. The old
21258
+ // Turn and, for v3, its abort_resolved transfer retain that immutable
21259
+ // proof; the terminal-wide singleton receipt advances to the retry so its
21260
+ // different binding/Turn identity cannot collide with the abandoned
21261
+ // zero-input generation.
21262
+ return next;
21263
+ }
20854
21264
  for (const field of TERMINAL_LEDGER_RECEIPT_IMMUTABLE_FIELDS) {
20855
21265
  const previousValue = previous[field];
20856
21266
  const nextValue = next[field];
@@ -23745,47 +24155,6 @@ function codexLatentClearResumeObservation({ screen, agentVersion }) {
23745
24155
  })
23746
24156
  };
23747
24157
  }
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
- }
23789
24158
  function nativeInspectionComposerEmpty(agent, screen) {
23790
24159
  return agent === "codex"
23791
24160
  ? codexComposerEmpty(screen)