@scotthuang/agent-knock-knock 0.12.6 → 0.12.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,6 +6,7 @@ import path from "node:path";
6
6
  import process from "node:process";
7
7
  import { fileURLToPath } from "node:url";
8
8
  import { createCodexTerminalAgentAdapter, detectCodexDurableCompletion } from "./codex-terminal-agent-adapter.js";
9
+ import { codexLifecycleBehaviorProfile } from "./codex-lifecycle-compatibility.js";
9
10
  import { createClaudeTerminalAgentAdapter } from "./claude-terminal-agent-adapter.js";
10
11
  import { captureClaudeTranscriptAnchor, defaultClaudeHome, detectClaudeTranscriptAcceptance, detectClaudeTranscriptCompletion, detectClaudeTranscriptPendingApproval, listClaudeThreadLifecycleCandidates, revalidateClaudeThreadLifecycleCandidate } from "./claude-local-transcript-provider.js";
11
12
  import { captureCodexCandidateSetRolloutAcceptanceAnchor, captureCodexRolloutAcceptanceAnchor, detectCodexBoundRolloutCompletion, detectCodexCandidateSetRolloutAcceptance, detectCodexRolloutAcceptance, terminalSubmissionReplayReceipt, validateCodexRolloutAcceptanceAnchor, validateTerminalSubmissionAcceptanceEvidence } from "./terminal-submission-acceptance.js";
@@ -1424,7 +1425,7 @@ function isTerminalControlCapability(value) {
1424
1425
  }
1425
1426
  function assertSafeAbortedTerminalRetryBinding({ owner, receipt, storeDir, terminalControl, messageId }) {
1426
1427
  if (!(receipt.status === "aborted" && receipt.safe_to_retry === true)) {
1427
- return;
1428
+ return undefined;
1428
1429
  }
1429
1430
  const sessionId = sessionIdForConversation(owner);
1430
1431
  const managedSession = tryLoadManagedSession(storeDir, sessionId);
@@ -1440,26 +1441,192 @@ function assertSafeAbortedTerminalRetryBinding({ owner, receipt, storeDir, termi
1440
1441
  stringValue(ownerTakeover?.terminal_agent_session_id) ??
1441
1442
  stringValue(ownerTakeover?.terminal_agent_expected_session_id);
1442
1443
  const ownerAgentPid = Number(ownerTakeover?.terminal_agent_pid);
1443
- if (!managedSession ||
1444
- managedSession.status !== "bound" ||
1445
- !binding ||
1446
- !receiptBindingId ||
1447
- !Number.isSafeInteger(receiptBindingGeneration) ||
1448
- !receiptNativeThreadId ||
1449
- receiptBindingId !== stringValue(owner.terminal_binding_id) ||
1450
- receiptBindingGeneration !== Number(owner.terminal_binding_generation) ||
1451
- receiptNativeThreadId !== ownerNativeThreadId ||
1452
- binding.binding_id !== receiptBindingId ||
1453
- binding.generation !== receiptBindingGeneration ||
1454
- binding.native_thread_id !== receiptNativeThreadId ||
1455
- !Number.isSafeInteger(ownerAgentPid) ||
1456
- binding.native_process.pid !== ownerAgentPid ||
1457
- !ownerControl ||
1458
- !terminalControlsShareIncarnation(ownerControl, terminalControl) ||
1459
- !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) {
1460
1471
  throw new Error(`terminal idempotency key ${messageId} belongs to a safe-aborted Turn ` +
1461
1472
  "whose Session binding is no longer current; no terminal input was sent");
1462
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;
1463
1630
  }
1464
1631
  function stableDelegateTerminalRoute({ options, request, workspace, requestedAgent }) {
1465
1632
  const messageId = stringValue(options.messageId);
@@ -1547,23 +1714,38 @@ function stableDelegateTerminalRoute({ options, request, workspace, requestedAge
1547
1714
  }
1548
1715
  if (selected.receipt.status === "aborted" &&
1549
1716
  selected.receipt.safe_to_retry === true) {
1550
- const sessionId = sessionIdForConversation(selected.owner);
1551
1717
  const ownerControl = terminalControlFromTakeover(isRecord(selected.owner.native_session_takeover)
1552
1718
  ? selected.owner.native_session_takeover
1553
1719
  : undefined);
1554
1720
  if (!ownerControl) {
1555
1721
  throw new Error(`terminal idempotency key ${messageId} has no durable terminal route`);
1556
1722
  }
1557
- assertSafeAbortedTerminalRetryBinding({
1723
+ const retrySession = assertSafeAbortedTerminalRetryBinding({
1558
1724
  owner: selected.owner,
1559
1725
  receipt: selected.receipt,
1560
1726
  storeDir,
1561
1727
  terminalControl: ownerControl,
1562
1728
  messageId
1563
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
+ }
1564
1746
  return {
1565
1747
  kind: "session",
1566
- sessionId,
1748
+ sessionId: retrySession.session_id,
1567
1749
  workspace: selected.workspace
1568
1750
  };
1569
1751
  }
@@ -2102,6 +2284,18 @@ function replayExactActiveTerminalSubmission({ options, terminalControl, request
2102
2284
  const loadedLedgerOwner = loadedLedger && loadedLedgerMessageId === messageId
2103
2285
  ? loadTerminalDispatchLedgerOwner(loadedLedger)
2104
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
+ }));
2105
2299
  const expectedMessageBodyHash = createHash("sha256")
2106
2300
  .update(requestText)
2107
2301
  .digest("hex");
@@ -2135,8 +2329,9 @@ function replayExactActiveTerminalSubmission({ options, terminalControl, request
2135
2329
  (!terminalDispatchRecordMatchesControl(loadedLedger, terminalControl) ||
2136
2330
  (loadedLedgerStoreDir !== undefined &&
2137
2331
  path.resolve(loadedLedgerStoreDir) !== path.resolve(expectedStoreDir)) ||
2138
- (expectedSessionId && stringValue(loadedLedger.session_id) !== undefined &&
2139
- stringValue(loadedLedger.session_id) !== expectedSessionId) ||
2332
+ (expectedSessionId && loadedLedgerSessionId !== undefined &&
2333
+ loadedLedgerSessionId !== expectedSessionId &&
2334
+ !loadedLedgerMatchesRecoveredSession) ||
2140
2335
  (expectedTurnId && stringValue(loadedLedger.turn_id) !== undefined &&
2141
2336
  stringValue(loadedLedger.turn_id) !== expectedTurnId) ||
2142
2337
  (expectedStatePath && loadedLedgerStatePath !== undefined &&
@@ -2168,6 +2363,7 @@ function replayExactActiveTerminalSubmission({ options, terminalControl, request
2168
2363
  return true;
2169
2364
  }
2170
2365
  if (ledgerReceipt &&
2366
+ !(ledgerReceipt.status === "aborted" && ledgerReceipt.safe_to_retry === true) &&
2171
2367
  (loadedLedgerMessageId !== messageId ||
2172
2368
  !["submitted", "enter_dispatched", "agent_accepted"].includes(String(ledgerReceipt.status)) ||
2173
2369
  !["submitted", "enter_dispatched", "agent_accepted"].includes(String(loadedLedger?.status)))) {
@@ -2325,6 +2521,16 @@ function validateStoredTerminalSubmissionMatch({ owner, receipt, options, termin
2325
2521
  const receiptStoreDir = stringValue(receipt.store_dir) ?? ownerStoreDir;
2326
2522
  const receiptSessionId = stringValue(receipt.session_id) ??
2327
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
+ }));
2328
2534
  const receiptTurnId = stringValue(receipt.turn_id) ??
2329
2535
  turnIdForConversation(owner);
2330
2536
  const receiptOpenClawSession = stringValue(receipt.openclaw_session) ??
@@ -2351,7 +2557,8 @@ function validateStoredTerminalSubmissionMatch({ owner, receipt, options, termin
2351
2557
  !terminalControlEvidenceMatches(receiptTerminalEvidence, terminalControl) ||
2352
2558
  receiptSessionId !== sessionIdForConversation(owner) ||
2353
2559
  receiptTurnId !== turnIdForConversation(owner) ||
2354
- (expectedSessionId && receiptSessionId !== expectedSessionId) ||
2560
+ (expectedSessionId && receiptSessionId !== expectedSessionId &&
2561
+ !receiptMatchesRecoveredSession) ||
2355
2562
  (expectedTurnId && receiptTurnId !== expectedTurnId) ||
2356
2563
  (expectedStatePath && !sameCanonicalStatePath(owner.state_path, expectedStatePath)) ||
2357
2564
  (requestedOpenClawSession && receiptOpenClawSession !== requestedOpenClawSession) ||
@@ -2426,13 +2633,28 @@ function replayExactStoredTerminalSubmission({ options, terminalControl, request
2426
2633
  const match = matches[0];
2427
2634
  if (!match) {
2428
2635
  for (const { owner, receipt } of validatedMatches) {
2429
- assertSafeAbortedTerminalRetryBinding({
2430
- owner,
2431
- receipt,
2432
- storeDir: expectedStoreDir,
2433
- terminalControl,
2434
- messageId
2435
- });
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
+ }
2436
2658
  }
2437
2659
  // A prepared-stage failure with a restored ledger proves that tmux was
2438
2660
  // untouched. The same exact id may therefore start a fresh attempt; all
@@ -2600,8 +2822,8 @@ function assertNoUnresolvedTerminalBridgeSubmission(storeDir, terminalControl, c
2600
2822
  for (const candidate of listConversations(storeDir)) {
2601
2823
  const submission = terminalBridgeSubmission(candidate);
2602
2824
  if (candidate.conversation_id === currentConversationId ||
2603
- !isActiveStatus(candidate.status) ||
2604
2825
  !submission ||
2826
+ TERMINAL_DISPATCH_RELEASE_STATUSES.has(effectiveTurnStatus(candidate)) ||
2605
2827
  ![
2606
2828
  "prepared",
2607
2829
  "text_injected",
@@ -3329,6 +3551,12 @@ async function terminalControlledListEntry(session, activeSessions, options, bri
3329
3551
  statusInspection: false,
3330
3552
  reason: "native inspection is unavailable"
3331
3553
  };
3554
+ const codexLatentClearResumeObservationValue = session.agent === "codex"
3555
+ ? codexLatentClearResumeObservation({
3556
+ screen: terminalState.screen_excerpt,
3557
+ agentVersion
3558
+ })
3559
+ : undefined;
3332
3560
  const lifecycleBindingToken = unmanagedTerminalBindingToken({
3333
3561
  terminalId: bridge.terminalConversationId(session),
3334
3562
  terminalControl,
@@ -3347,7 +3575,8 @@ async function terminalControlledListEntry(session, activeSessions, options, bri
3347
3575
  if (session.agent === "codex" &&
3348
3576
  (terminalState.activity_state === "idle" ||
3349
3577
  (terminalState.activity_state === "unknown" &&
3350
- nativeIdentityObservation.status === "verified_absent")) &&
3578
+ (nativeIdentityObservation.status === "verified_absent" ||
3579
+ codexOpenRootRolloutInventory !== undefined))) &&
3351
3580
  terminalState.approval_state.blocked !== true &&
3352
3581
  terminalControl.capabilities.includes("send_keys") &&
3353
3582
  terminalControl.capabilities.includes("screen_status")) {
@@ -3403,6 +3632,14 @@ async function terminalControlledListEntry(session, activeSessions, options, bri
3403
3632
  // this field after gating every automated-input action that can follow a
3404
3633
  // human native-thread switch.
3405
3634
  _automated_input_composer_ready: automatedInputComposerReady,
3635
+ ...(codexLatentClearResumeObservationValue
3636
+ ? {
3637
+ _codex_latent_clear_resume: {
3638
+ source_native_thread_id: codexLatentClearResumeObservationValue.sourceNativeThreadId,
3639
+ fingerprint: codexLatentClearResumeObservationValue.fingerprint
3640
+ }
3641
+ }
3642
+ : {}),
3406
3643
  ...(codexOpenRootRolloutInventory
3407
3644
  ? {
3408
3645
  _codex_open_root_rollout_inventory: codexOpenRootRolloutInventory
@@ -3482,7 +3719,7 @@ function terminalFirstListProjection({ storeDir, terminals, managedSessions, ses
3482
3719
  return control ? [control] : [];
3483
3720
  });
3484
3721
  const projectedTerminals = terminals.map((terminal) => {
3485
- const { _automated_input_composer_ready: automatedInputComposerReady, _codex_open_root_rollout_inventory: codexOpenRootRolloutInventoryValue, ...publicTerminal } = terminal;
3722
+ const { _automated_input_composer_ready: automatedInputComposerReady, _codex_open_root_rollout_inventory: codexOpenRootRolloutInventoryValue, _codex_latent_clear_resume: codexLatentClearResumeValue, ...publicTerminal } = terminal;
3486
3723
  const codexOpenRootRolloutInventory = isRecord(codexOpenRootRolloutInventoryValue)
3487
3724
  ? codexOpenRootRolloutInventoryValue
3488
3725
  : undefined;
@@ -3732,16 +3969,40 @@ function terminalFirstListProjection({ storeDir, terminals, managedSessions, ses
3732
3969
  codexOpenRootRolloutInventory.roots.length > 0
3733
3970
  ? codexOpenRootRolloutInventory
3734
3971
  : undefined;
3735
- const deferredCodexSource = authoritativeSession &&
3972
+ const abandonedConflictSource = !authoritativeSession &&
3973
+ soleBindingConflict?.kind === "unverifiable" &&
3974
+ matchingSessions.length === 0 &&
3975
+ conflictingBoundSessionClaims.length === 1 &&
3976
+ unresolvedSessionClaims.length === 0 &&
3977
+ terminal.agent === "codex"
3978
+ ? soleBindingConflict.session
3979
+ : undefined;
3980
+ const deferredCodexSource = (authoritativeSession ??
3981
+ abandonedConflictSource) &&
3736
3982
  terminal.agent === "codex" &&
3737
3983
  (nativeIdentityObservation?.status === "verified_absent" ||
3738
3984
  deferredCodexCandidateInventory !== undefined)
3739
- ? authoritativeSession
3985
+ ? authoritativeSession ?? abandonedConflictSource
3740
3986
  : undefined;
3741
3987
  const deferredCodexProcessUuid = stringValue(terminal.native_agent_process_uuid);
3742
3988
  const deferredCodexProcessBirth = stringValue(terminal.native_agent_process_birth);
3743
3989
  const deferredCodexWorkspace = stringValue(terminal.workspace ?? terminal.cwd);
3744
3990
  const deferredCodexSourceNativeThreadId = stringValue(deferredCodexSource?.binding?.native_thread_id)?.toLowerCase();
3991
+ const deferredCodexLatentClearResumeFingerprint = isRecord(codexLatentClearResumeValue) &&
3992
+ stringValue(codexLatentClearResumeValue.source_native_thread_id)?.toLowerCase() === deferredCodexSourceNativeThreadId
3993
+ ? stringValue(codexLatentClearResumeValue.fingerprint)
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
+ }
3745
4006
  const deferredCodexSourceActiveElsewhere = Boolean(deferredCodexSourceNativeThreadId &&
3746
4007
  terminals.some((candidate) => candidate !== terminal &&
3747
4008
  candidate.agent === "codex" &&
@@ -3750,9 +4011,30 @@ function terminalFirstListProjection({ storeDir, terminals, managedSessions, ses
3750
4011
  const deferredCodexDispatchSnapshot = terminalControl
3751
4012
  ? tryDeferredCodexForegroundDispatchSnapshot(terminalControl)
3752
4013
  : undefined;
3753
- const deferredCodexCandidateSourceTurnHistory = deferredCodexCandidateInventory && deferredCodexSource
4014
+ const deferredCodexCandidateSourceTurnHistory = deferredCodexCandidateInventory &&
4015
+ deferredCodexSource?.binding?.native_process.rollout
3754
4016
  ? deferredCandidateSourceTurnHistory(storeDir, deferredCodexSource)
3755
4017
  : undefined;
4018
+ const deferredCodexSourceRolloutPresent = Boolean(deferredCodexCandidateInventory &&
4019
+ deferredCodexSource?.binding?.native_process.rollout &&
4020
+ deferredCodexCandidateInventory.roots.some((root) => root.sessionId.toLowerCase() ===
4021
+ deferredCodexSource.binding?.native_thread_id?.toLowerCase() &&
4022
+ exactNativeRolloutMatches(root.rollout, deferredCodexSource.binding.native_process.rollout)));
4023
+ const deferredCodexSourceRolloutAuthority = abandonedConflictSource && !deferredCodexSourceRolloutPresent
4024
+ ? "explicitly_abandoned_predecessor"
4025
+ : "present";
4026
+ const deferredCodexSourceAbandonmentFingerprint = deferredCodexSourceRolloutAuthority ===
4027
+ "explicitly_abandoned_predecessor" &&
4028
+ deferredCodexSource &&
4029
+ deferredCodexCandidateSourceTurnHistory &&
4030
+ deferredCodexDispatchSnapshot
4031
+ ? explicitlyAbandonedCandidateSourceFingerprint({
4032
+ storeDir,
4033
+ session: deferredCodexSource,
4034
+ sourceTurnHistory: deferredCodexCandidateSourceTurnHistory,
4035
+ dispatchSnapshot: deferredCodexDispatchSnapshot
4036
+ })
4037
+ : undefined;
3756
4038
  const deferredCodexStatusCardSourceEligible = Boolean(deferredCodexSource &&
3757
4039
  terminalControl &&
3758
4040
  deferredCodexProcessUuid &&
@@ -3785,13 +4067,25 @@ function terminalFirstListProjection({ storeDir, terminals, managedSessions, ses
3785
4067
  workspace: deferredCodexWorkspace,
3786
4068
  processUuid: deferredCodexProcessUuid,
3787
4069
  processBirth: deferredCodexProcessBirth,
3788
- inventory: deferredCodexCandidateInventory
4070
+ inventory: deferredCodexCandidateInventory,
4071
+ sourceRolloutAuthority: deferredCodexSourceRolloutAuthority
4072
+ }) &&
4073
+ (deferredCodexSourceRolloutAuthority === "present" ||
4074
+ Boolean(deferredCodexSourceAbandonmentFingerprint)) &&
4075
+ codexCandidateInventoryHasNoOtherManagedClaim({
4076
+ storeDir,
4077
+ inventory: deferredCodexCandidateInventory,
4078
+ sourceSessionId: deferredCodexSource.session_id,
4079
+ includeDetached: deferredCodexSourceRolloutAuthority ===
4080
+ "explicitly_abandoned_predecessor"
3789
4081
  }) &&
3790
4082
  candidateSourceTransitionHistoryIsTerminal(storeDir, deferredCodexSource));
3791
- const deferredCodexCandidateRouteNeeded = Boolean(deferredCodexCandidateInventory &&
3792
- (deferredCodexCandidateInventory.status === "unbound" ||
3793
- nativeIdentityObservation?.status === "unavailable" ||
3794
- deferredCodexSource?.binding?.native_process.rollout === 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;
3795
4089
  const deferredCodexForegroundEligible = Boolean(mutationsAllowed &&
3796
4090
  !terminalHasNonterminalDeferredTransfer &&
3797
4091
  deferredCodexSource &&
@@ -3799,8 +4093,12 @@ function terminalFirstListProjection({ storeDir, terminals, managedSessions, ses
3799
4093
  hasCanonicalTerminalEndpoint(terminalControl) &&
3800
4094
  discoveredOwnership.state === "none" &&
3801
4095
  unresolvedSessionClaims.length === 0 &&
3802
- matchingSessions.length === 1 &&
3803
- conflictingBoundSessionClaims.length === 0 &&
4096
+ ((matchingSessions.length === 1 &&
4097
+ conflictingBoundSessionClaims.length === 0) ||
4098
+ (deferredCodexSourceRolloutAuthority ===
4099
+ "explicitly_abandoned_predecessor" &&
4100
+ matchingSessions.length === 0 &&
4101
+ conflictingBoundSessionClaims.length === 1)) &&
3804
4102
  deferredCodexProcessUuid &&
3805
4103
  deferredCodexProcessBirth &&
3806
4104
  deferredCodexWorkspace &&
@@ -3816,8 +4114,7 @@ function terminalFirstListProjection({ storeDir, terminals, managedSessions, ses
3816
4114
  terminal.approval_state.blocked === true) &&
3817
4115
  !deferredCodexSourceActiveElsewhere &&
3818
4116
  terminalControl.capabilities.includes("send_keys") &&
3819
- terminalControl.capabilities.includes("screen_status") &&
3820
- isRecord(rawActions.send));
4117
+ terminalControl.capabilities.includes("screen_status"));
3821
4118
  const deferredCodexForegroundToken = deferredCodexForegroundEligible &&
3822
4119
  deferredCodexSource &&
3823
4120
  terminalControl &&
@@ -3834,6 +4131,9 @@ function terminalFirstListProjection({ storeDir, terminals, managedSessions, ses
3834
4131
  processBirth: deferredCodexProcessBirth,
3835
4132
  sourceSession: deferredCodexSource,
3836
4133
  dispatchSnapshot: deferredCodexDispatchSnapshot,
4134
+ sourceTurnHistory: deferredCodexCandidateSourceTurnHistory,
4135
+ sourceRolloutAuthority: deferredCodexSourceRolloutAuthority,
4136
+ sourceAbandonmentFingerprint: deferredCodexSourceAbandonmentFingerprint,
3837
4137
  ...(deferredCodexCandidateInventory
3838
4138
  ? { candidateInventory: deferredCodexCandidateInventory }
3839
4139
  : {})
@@ -3998,7 +4298,11 @@ function terminalFirstListProjection({ storeDir, terminals, managedSessions, ses
3998
4298
  });
3999
4299
  }
4000
4300
  }
4001
- 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)));
4002
4306
  if (ownership.state === "current" &&
4003
4307
  !allRelated.some((conversation) => conversation.conversation_id === ownership.conversation.conversation_id)) {
4004
4308
  allRelated.push(ownership.conversation);
@@ -4043,7 +4347,8 @@ function terminalFirstListProjection({ storeDir, terminals, managedSessions, ses
4043
4347
  ? withoutGenericHandoffSourceClose(currentTurnProjection, blockingHandoffTurnIds)
4044
4348
  : undefined;
4045
4349
  const nonOwnerRawActions = authoritativeSession
4046
- ? Object.fromEntries(Object.entries(sessionAwareRawActions).filter(([actionName]) => actionName !== "approve"))
4350
+ ? Object.fromEntries(Object.entries(sessionAwareRawActions).filter(([actionName]) => actionName !== "approve" &&
4351
+ !(rolloutBackedCodexSession && actionName === "send")))
4047
4352
  : sessionAwareRawActions;
4048
4353
  const sortedDisplayed = [...sessionDisplayedRelated]
4049
4354
  .filter((conversation) => conversation.conversation_id !== currentTurn?.conversation_id)
@@ -4102,6 +4407,21 @@ function terminalFirstListProjection({ storeDir, terminals, managedSessions, ses
4102
4407
  : ownership.state === "conflict"
4103
4408
  ? {
4104
4409
  ...safeTerminalActionsDuringConflict(sessionAwareRawActions),
4410
+ ...(deferredCodexForegroundToken &&
4411
+ deferredCodexSourceRolloutAuthority ===
4412
+ "explicitly_abandoned_predecessor"
4413
+ ? {
4414
+ send: {
4415
+ ...verifiedEmptyRawSendAction,
4416
+ arguments: {
4417
+ ...(isRecord(verifiedEmptyRawSendAction.arguments)
4418
+ ? verifiedEmptyRawSendAction.arguments
4419
+ : {}),
4420
+ expected_terminal_token: deferredCodexForegroundToken
4421
+ }
4422
+ }
4423
+ }
4424
+ : {}),
4105
4425
  ...(externalHandoffAdoptable
4106
4426
  ? {
4107
4427
  send: {
@@ -4132,21 +4452,21 @@ function terminalFirstListProjection({ storeDir, terminals, managedSessions, ses
4132
4452
  ? { reconcile_binding: reconcileBindingAction }
4133
4453
  : {})
4134
4454
  }
4135
- : deferredCodexForegroundToken &&
4136
- isRecord(rawActions.send)
4455
+ : deferredCodexForegroundToken
4137
4456
  ? {
4138
4457
  ...nonOwnerRawActions,
4139
4458
  send: {
4140
- ...rawActions.send,
4459
+ ...verifiedEmptyRawSendAction,
4141
4460
  arguments: {
4142
- ...(isRecord(rawActions.send.arguments)
4143
- ? rawActions.send.arguments
4461
+ ...(isRecord(verifiedEmptyRawSendAction.arguments)
4462
+ ? verifiedEmptyRawSendAction.arguments
4144
4463
  : {}),
4145
4464
  expected_terminal_token: deferredCodexForegroundToken
4146
4465
  }
4147
4466
  }
4148
4467
  }
4149
4468
  : managedSessionId &&
4469
+ !rolloutBackedCodexSession &&
4150
4470
  sessionBindingMatchesLiveTerminal &&
4151
4471
  isRecord(sessionAwareRawActions.send)
4152
4472
  ? {
@@ -4183,9 +4503,19 @@ function terminalFirstListProjection({ storeDir, terminals, managedSessions, ses
4183
4503
  "while the same terminal process is at an exact empty prompt",
4184
4504
  recovery: "use only the exact snapshot-bound send action listed for this terminal"
4185
4505
  }
4186
- : ownership.state === "conflict"
4187
- ? ownership.conflict
4188
- : undefined;
4506
+ : ownership.state === "conflict" &&
4507
+ deferredCodexForegroundToken &&
4508
+ deferredCodexSourceRolloutAuthority ===
4509
+ "explicitly_abandoned_predecessor"
4510
+ ? {
4511
+ ...ownership.conflict,
4512
+ kind: "explicitly_abandoned_predecessor_adoptable",
4513
+ reason: "the explicitly abandoned Codex predecessor rollout is no longer open and the current exact rollout inventory is unclaimed",
4514
+ recovery: "use only the exact snapshot-bound follow-current send action listed for this terminal"
4515
+ }
4516
+ : ownership.state === "conflict"
4517
+ ? ownership.conflict
4518
+ : undefined;
4189
4519
  return {
4190
4520
  ...publicTerminal,
4191
4521
  ...authoritativeIdentity,
@@ -5523,13 +5853,14 @@ function managedListApprovalState(conversation) {
5523
5853
  }
5524
5854
  function listActionContracts() {
5525
5855
  return {
5526
- version: 12,
5856
+ version: 15,
5527
5857
  instructions: [
5528
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.",
5529
- "An existing managed session's ordinary send targets session_id and creates a new turn. A turn id is never an ordinary send target.",
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.",
5530
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.",
5531
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.",
5532
- "A verified, idle human native-thread switch may expose a terminal-scoped send with expected_terminal_token; that action atomically adopts the live context before creating its Turn. A conclusively ended Codex rollout may expose the same snapshot-bound send only after AKK proves zero current rollout and an exact empty composer; it detaches the ended Session and creates an isolated virgin Session. A status-card-only zero-rollout source, or a quiescent rollout-backed source whose complete pinned open-rollout inventory cannot identify one foreground candidate, may also expose this exact action. AKK freezes any released predecessor Turn history, submits the ordinary task once, and binds a separate provisional Session only after one post-anchor rollout uniquely accepts that exact request. The accepted UUID may equal or differ from the predecessor without merging their Session lineages, and narrow panes do not require /status. Until that promotion commits, strict session_id send, respond, approve, cancel, native lifecycle, and native_inspect remain unavailable, and the provisional binding has no callback authority. If dispatch, acceptance, or post-submit binding is uncertain, do not retry automatically. Other binding conflicts remain fail-closed and may expose only exact low-level reconcile_binding recovery.",
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.",
5533
5864
  "List resumable threads before resume; use only a complete native_thread_id and the action returned for that candidate.",
5534
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.",
5535
5866
  "Use respond only for an in-flight turn that is explicitly waiting for OpenClaw.",
@@ -5554,7 +5885,7 @@ function listActionContracts() {
5554
5885
  managed: {
5555
5886
  current_turn: "the authoritative dispatch-ledger owner, never inferred from history",
5556
5887
  recent_turn: "the latest visible non-owning turn in the current managed session",
5557
- session_id: "the continuing agent context and authoritative ordinary-send target",
5888
+ 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",
5558
5889
  binding_id: "the immutable terminal-binding generation currently authorized for this Session",
5559
5890
  binding_token: "an optimistic concurrency token required by native-thread mutations",
5560
5891
  history: "older turns, present only with --all"
@@ -5592,7 +5923,17 @@ function listActionContracts() {
5592
5923
  tool: "agent_knock_knock_send",
5593
5924
  target_argument: "session_id",
5594
5925
  initial_attach_target_argument: "selector",
5595
- 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.",
5926
+ managed_scopes: {
5927
+ session_exact: {
5928
+ target_arguments: ["session_id"],
5929
+ follows_current_terminal: false
5930
+ },
5931
+ terminal_follow_current: {
5932
+ target_arguments: ["selector", "expected_terminal_token"],
5933
+ follows_current_terminal: true
5934
+ }
5935
+ },
5936
+ 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.",
5596
5937
  required: ["request"],
5597
5938
  optional: [
5598
5939
  "selector",
@@ -5604,8 +5945,8 @@ function listActionContracts() {
5604
5945
  ],
5605
5946
  unsupported: ["timeoutSeconds"],
5606
5947
  status_card_only_deferred_scope: "A zero-Turn Codex status-card binding has no rollout; only its listed selector/token send creates an isolated provisional Session and binds it after exact request acceptance. Until promotion commits, strict managed controls, native lifecycle, native_inspect, and callback authority remain unavailable; an uncertain dispatch, acceptance, or post-submit binding must not be retried automatically.",
5607
- candidate_rollout_deferred_scope: "A quiescent rollout-backed Codex source may use a listed selector/token send only when AKK pins the complete exact candidate inventory but cannot prove one foreground UUID. Released predecessor Turn history stays read-only while a separate provisional Session sends once and waits for one unique post-anchor request acceptance. Same-UUID and different-UUID results keep separate Session lineages; zero, multiple, drifted, or uncertain acceptance is never retried blindly.",
5608
- ordinary_use: "Create a new managed turn in the exact Session. A live terminal selector can attach an unmanaged pane, adopt one verified human-selected native context, detach a verified-empty Codex source, or replace an eligible status-card/candidate-rollout source after the submitted request proves its unique exact rollout; an explicit session_id never follows the pane."
5948
+ 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.",
5949
+ 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."
5609
5950
  },
5610
5951
  new_thread: {
5611
5952
  tool: "agent_knock_knock_new_thread",
@@ -5852,7 +6193,9 @@ function availableListActions(entry, { conversation } = {}) {
5852
6193
  requires_user_intent: true
5853
6194
  };
5854
6195
  }
5855
- if (terminalBridgeReady && entry.status === "stalled") {
6196
+ if (terminalBridgeReady &&
6197
+ entry.status === "stalled" &&
6198
+ terminalBridgeSubmission(conversation)?.status !== "uncertain") {
5856
6199
  actions.renew = {
5857
6200
  tool: "agent_knock_knock_renew",
5858
6201
  arguments: targetArguments
@@ -6514,6 +6857,14 @@ function codexAllowedCompanionSetForManagedSession({ storeDir, session }) {
6514
6857
  });
6515
6858
  return { primary: selectedPrimary, additional };
6516
6859
  }
6860
+ function codexCompanionsPresentInOpenRootInventory(companions, inventory) {
6861
+ const present = [companions.primary, ...companions.additional].filter((candidate) => Boolean(candidate &&
6862
+ inventory.roots.some((root) => nativeIdentityMatchesCodexPreMaterialization(root, candidate))));
6863
+ return {
6864
+ primary: present[0],
6865
+ additional: present.slice(1)
6866
+ };
6867
+ }
6517
6868
  function codexManagedIdentityResolutionContext({ storeDir, terminal }) {
6518
6869
  const claimedSession = soleBoundManagedSessionClaimForTerminal(storeDir, terminal);
6519
6870
  const companions = claimedSession
@@ -7035,8 +7386,11 @@ function exactBoundCodexStatusCardSourceForDeferredSend({ session, terminalId, t
7035
7386
  liveProcessBirth: processBirth
7036
7387
  }) === "same");
7037
7388
  }
7038
- function exactCodexCandidateInventoryForDeferredSend({ inventory, sourceSession, pid, workspace, processUuid, processBirth }) {
7389
+ function exactCodexCandidateInventoryForDeferredSend({ inventory, sourceSession, pid, workspace, processUuid, processBirth, sourceRolloutAuthority = "present" }) {
7039
7390
  const sourceNativeThreadId = sourceSession.binding?.native_thread_id;
7391
+ const sourceRollout = sourceSession.binding?.native_process.rollout;
7392
+ const sourceRootPresent = Boolean(sourceNativeThreadId && inventory.roots.some((root) => root.sessionId.toLowerCase() === sourceNativeThreadId.toLowerCase()));
7393
+ const sourceRolloutPresent = Boolean(sourceRollout && inventory.roots.some((root) => exactNativeRolloutMatches(root.rollout, sourceRollout)));
7040
7394
  return Boolean(inventory.roots.length > 0 &&
7041
7395
  inventory.pid === pid &&
7042
7396
  inventory.processUuid === processUuid &&
@@ -7044,11 +7398,14 @@ function exactCodexCandidateInventoryForDeferredSend({ inventory, sourceSession,
7044
7398
  inventory.cwd &&
7045
7399
  path.resolve(inventory.cwd) === path.resolve(workspace) &&
7046
7400
  /^[0-9a-f]{64}$/u.test(inventory.inventoryFingerprint) &&
7047
- (sourceSession.binding?.native_process.rollout === undefined ||
7048
- (sourceNativeThreadId &&
7049
- inventory.roots.some((root) => root.sessionId.toLowerCase() === sourceNativeThreadId.toLowerCase()))));
7050
- }
7051
- function exactBoundCodexCandidateSourceForDeferredSend({ session, terminalId, terminalControl, pid, workspace, processUuid, processBirth, inventory }) {
7401
+ (sourceRolloutAuthority === "explicitly_abandoned_predecessor"
7402
+ ? isExactNativeThreadId(sourceNativeThreadId) &&
7403
+ isCompleteNativeRollout(sourceRollout) &&
7404
+ !sourceRootPresent &&
7405
+ !sourceRolloutPresent
7406
+ : sourceRollout === undefined || sourceRootPresent));
7407
+ }
7408
+ function exactBoundCodexCandidateSourceForDeferredSend({ session, terminalId, terminalControl, pid, workspace, processUuid, processBirth, inventory, sourceRolloutAuthority = "present" }) {
7052
7409
  const binding = session.binding;
7053
7410
  const sourceNativeThreadId = binding?.native_thread_id;
7054
7411
  const sourceRollout = binding?.native_process.rollout;
@@ -7058,8 +7415,11 @@ function exactBoundCodexCandidateSourceForDeferredSend({ session, terminalId, te
7058
7415
  binding &&
7059
7416
  isExactNativeThreadId(sourceNativeThreadId) &&
7060
7417
  isCompleteNativeRollout(sourceRollout) &&
7061
- inventoryRoot &&
7062
- exactNativeRolloutMatches(inventoryRoot.rollout, sourceRollout) &&
7418
+ (sourceRolloutAuthority === "explicitly_abandoned_predecessor"
7419
+ ? inventoryRoot === undefined &&
7420
+ !inventory.roots.some((root) => exactNativeRolloutMatches(root.rollout, sourceRollout))
7421
+ : inventoryRoot !== undefined &&
7422
+ exactNativeRolloutMatches(inventoryRoot.rollout, sourceRollout)) &&
7063
7423
  terminalControlAliasMatches(binding.terminal_id, binding.terminal_control, terminalId, terminalControl) &&
7064
7424
  path.resolve(session.workspace) === path.resolve(workspace) &&
7065
7425
  processIncarnationRelationship({
@@ -7074,22 +7434,159 @@ function exactBoundCodexCandidateSourceForDeferredSend({ session, terminalId, te
7074
7434
  pid,
7075
7435
  workspace,
7076
7436
  processUuid,
7077
- processBirth
7437
+ processBirth,
7438
+ sourceRolloutAuthority
7078
7439
  }));
7079
7440
  }
7441
+ function codexCandidateInventoryHasNoOtherManagedClaim({ storeDir, inventory, sourceSessionId, includeDetached = true }) {
7442
+ const candidateIds = new Set(inventory.roots.map((root) => root.sessionId.toLowerCase()));
7443
+ return !listManagedSessions(storeDir).some((session) => session.session_id !== sourceSessionId &&
7444
+ (includeDetached || session.status !== "detached") &&
7445
+ isExactNativeThreadId(session.binding?.native_thread_id) &&
7446
+ candidateIds.has(session.binding.native_thread_id.toLowerCase()));
7447
+ }
7448
+ function exactReleasedSafeAbortedCandidateTurn({ storeDir, session, turn }) {
7449
+ const binding = session.binding;
7450
+ const takeover = isRecord(turn.native_session_takeover)
7451
+ ? turn.native_session_takeover
7452
+ : undefined;
7453
+ const submission = terminalBridgeSubmission(turn);
7454
+ const terminalControl = terminalControlFromTakeover(takeover);
7455
+ const messageId = stringValue(submission?.message_id);
7456
+ const preparedAt = stringValue(submission?.prepared_at);
7457
+ const abortedAt = stringValue(submission?.aborted_at);
7458
+ if (session.status !== "bound" ||
7459
+ session.agent !== "codex" ||
7460
+ !binding ||
7461
+ !isExactNativeThreadId(binding.native_thread_id) ||
7462
+ !TERMINAL_DISPATCH_RELEASE_STATUSES.has(turn.status) ||
7463
+ managedTurnNeedsAttention(turn) ||
7464
+ isRecord(turn.callback_delivery) ||
7465
+ isRecord(turn.terminal_bridge_completion_claim) ||
7466
+ !takeover ||
7467
+ isRecord(takeover.terminal_bridge_completion_claim) ||
7468
+ !submission ||
7469
+ !terminalControl ||
7470
+ !messageId ||
7471
+ submission.status !== "aborted" ||
7472
+ submission.safe_to_retry !== true ||
7473
+ stringValue(submission.last_proven_stage) !== "prepared" ||
7474
+ !preparedAt ||
7475
+ !abortedAt ||
7476
+ !validTimestampMs(preparedAt) ||
7477
+ !validTimestampMs(abortedAt) ||
7478
+ Date.parse(abortedAt) < Date.parse(preparedAt) ||
7479
+ [
7480
+ "text_injected_at",
7481
+ "enter_dispatched_at",
7482
+ "submitted_at",
7483
+ "agent_accepted_at",
7484
+ "not_accepted_at",
7485
+ "uncertain_at",
7486
+ "acceptance_evidence"
7487
+ ].some((field) => submission[field] !== undefined) ||
7488
+ stringValue(takeover.terminal_bridge_message_id) !== messageId ||
7489
+ stringValue(takeover.terminal_bridge_request_hash) !==
7490
+ stringValue(submission.request_hash) ||
7491
+ terminalBridgeRequestFingerprint(stringValue(takeover.terminal_bridge_request_text) ?? "") !== stringValue(submission.request_hash) ||
7492
+ executorForConversation(turn).kind !== "codex" ||
7493
+ sessionIdForConversation(turn) !== session.session_id ||
7494
+ stringValue(submission.session_id) !== session.session_id ||
7495
+ stringValue(submission.turn_id) !== turnIdForConversation(turn) ||
7496
+ stringValue(submission.executor_kind) !== "codex" ||
7497
+ stringValue(submission.openclaw_session) !== turn.openclaw_session ||
7498
+ stringValue(turn.terminal_binding_id) !== binding.binding_id ||
7499
+ Number(turn.terminal_binding_generation) !== binding.generation ||
7500
+ stringValue(takeover.terminal_binding_id) !== binding.binding_id ||
7501
+ Number(takeover.terminal_binding_generation) !== binding.generation ||
7502
+ stringValue(submission.binding_id) !== binding.binding_id ||
7503
+ Number(submission.binding_generation) !== binding.generation ||
7504
+ stringValue(turn.native_thread_id) !== binding.native_thread_id ||
7505
+ stringValue(takeover.terminal_agent_session_id) !==
7506
+ binding.native_thread_id ||
7507
+ stringValue(submission.native_thread_id) !== binding.native_thread_id ||
7508
+ Number(takeover.terminal_agent_pid) !== binding.native_process.pid ||
7509
+ stringValue(takeover.terminal_agent_process_uuid) !==
7510
+ binding.native_process.process_uuid ||
7511
+ stringValue(takeover.terminal_agent_process_birth) !==
7512
+ binding.native_process.process_birth ||
7513
+ !terminalControlsShareIncarnation(terminalControl, binding.terminal_control) ||
7514
+ !terminalControlEvidenceMatches(submission.terminal_endpoint ?? terminalControl, binding.terminal_control) ||
7515
+ path.resolve(turn.workspace) !== path.resolve(session.workspace)) {
7516
+ return false;
7517
+ }
7518
+ const canonical = pathsForConversation(turn.conversation_id, storeDir);
7519
+ if (path.resolve(stringValue(turn.state_path) ?? "") !==
7520
+ path.resolve(canonical.statePath) ||
7521
+ path.resolve(stringValue(turn.event_log_path) ?? "") !==
7522
+ path.resolve(canonical.logPath) ||
7523
+ path.resolve(managedSessionStoreDirForConversation(turn) ?? "") !==
7524
+ path.resolve(storeDir) ||
7525
+ path.resolve(stringValue(submission.store_dir) ?? "") !==
7526
+ path.resolve(storeDir)) {
7527
+ return false;
7528
+ }
7529
+ const matchingReceipts = terminalBridgeSubmissionReceipts(turn).filter((receipt) => stringValue(receipt.message_id) === messageId);
7530
+ return matchingReceipts.length === 1 &&
7531
+ canonicalJson(matchingReceipts[0]) === canonicalJson(submission);
7532
+ }
7080
7533
  function deferredCandidateSourceTurnHistory(storeDir, session) {
7534
+ const binding = session.binding;
7535
+ if (!binding?.native_thread_id) {
7536
+ return undefined;
7537
+ }
7538
+ const turns = managedTurnsForSession(storeDir, session.session_id);
7539
+ if (turns.some((turn) => {
7540
+ const callbackDelivered = isRecord(turn.callback_delivery) &&
7541
+ turn.callback_delivery.status === "delivered";
7542
+ const explicitlyAbandonedUncertain = exactExplicitlyAbandonedUncertainCandidateTurn({
7543
+ storeDir,
7544
+ session,
7545
+ turn
7546
+ });
7547
+ const safelyAbortedBeforeInput = exactReleasedSafeAbortedCandidateTurn({
7548
+ storeDir,
7549
+ session,
7550
+ turn
7551
+ });
7552
+ return (!TERMINAL_DISPATCH_RELEASE_STATUSES.has(turn.status) ||
7553
+ managedTurnNeedsAttention(turn) ||
7554
+ (isRecord(turn.callback_delivery) &&
7555
+ !callbackDelivered) ||
7556
+ (Boolean(turn.gateway_method) &&
7557
+ !callbackDelivered &&
7558
+ !explicitlyAbandonedUncertain &&
7559
+ !safelyAbortedBeforeInput) ||
7560
+ stringValue(turn.terminal_binding_id) !== binding.binding_id ||
7561
+ Number(turn.terminal_binding_generation) !== binding.generation ||
7562
+ (stringValue(turn.native_thread_id) ??
7563
+ stringValue(isRecord(turn.native_session_takeover)
7564
+ ? turn.native_session_takeover.terminal_agent_session_id
7565
+ : undefined)) !== binding.native_thread_id);
7566
+ })) {
7567
+ return undefined;
7568
+ }
7569
+ return turns
7570
+ .map((turn) => ({
7571
+ turn_id: turnIdForConversation(turn),
7572
+ status: turn.status,
7573
+ updated_at: required(stringValue(turn.updated_at), `managed Turn ${turnIdForConversation(turn)} updated_at is unavailable`),
7574
+ binding_id: binding.binding_id,
7575
+ binding_generation: binding.generation,
7576
+ native_thread_id: binding.native_thread_id,
7577
+ turn_fingerprint: createHash("sha256")
7578
+ .update(JSON.stringify(turn))
7579
+ .digest("hex")
7580
+ }))
7581
+ .sort((left, right) => left.turn_id.localeCompare(right.turn_id));
7582
+ }
7583
+ function frozenCandidateSourceTurnHistory(storeDir, session) {
7081
7584
  const binding = session.binding;
7082
7585
  if (!binding?.native_thread_id) {
7083
7586
  return undefined;
7084
7587
  }
7085
7588
  const turns = managedTurnsForSession(storeDir, session.session_id);
7086
7589
  if (turns.some((turn) => !TERMINAL_DISPATCH_RELEASE_STATUSES.has(turn.status) ||
7087
- managedTurnNeedsAttention(turn) ||
7088
- (isRecord(turn.callback_delivery) &&
7089
- turn.callback_delivery.status !== "delivered") ||
7090
- (Boolean(turn.gateway_method) &&
7091
- (!isRecord(turn.callback_delivery) ||
7092
- turn.callback_delivery.status !== "delivered")) ||
7093
7590
  stringValue(turn.terminal_binding_id) !== binding.binding_id ||
7094
7591
  Number(turn.terminal_binding_generation) !== binding.generation ||
7095
7592
  (stringValue(turn.native_thread_id) ??
@@ -7098,8 +7595,7 @@ function deferredCandidateSourceTurnHistory(storeDir, session) {
7098
7595
  : undefined)) !== binding.native_thread_id)) {
7099
7596
  return undefined;
7100
7597
  }
7101
- return turns
7102
- .map((turn) => ({
7598
+ return turns.map((turn) => ({
7103
7599
  turn_id: turnIdForConversation(turn),
7104
7600
  status: turn.status,
7105
7601
  updated_at: required(stringValue(turn.updated_at), `managed Turn ${turnIdForConversation(turn)} updated_at is unavailable`),
@@ -7109,8 +7605,220 @@ function deferredCandidateSourceTurnHistory(storeDir, session) {
7109
7605
  turn_fingerprint: createHash("sha256")
7110
7606
  .update(JSON.stringify(turn))
7111
7607
  .digest("hex")
7112
- }))
7113
- .sort((left, right) => left.turn_id.localeCompare(right.turn_id));
7608
+ })).sort((left, right) => left.turn_id.localeCompare(right.turn_id));
7609
+ }
7610
+ function exactExplicitlyAbandonedUncertainCandidateTurn({ storeDir, session, turn }) {
7611
+ return explicitlyAbandonedUncertainCandidateTurnFingerprint({
7612
+ storeDir,
7613
+ session,
7614
+ turn
7615
+ }) !== undefined;
7616
+ }
7617
+ function explicitlyAbandonedUncertainCandidateTurnProof({ storeDir, session, turn, ledgerOverride, requireResolvedTopLevel = true }) {
7618
+ if (turn.status !== "closed" ||
7619
+ isRecord(turn.callback_delivery) ||
7620
+ isRecord(turn.terminal_bridge_completion_claim) ||
7621
+ !stringValue(turn.closed_at) ||
7622
+ !stringValue(turn.close_reason)) {
7623
+ return undefined;
7624
+ }
7625
+ const takeover = isRecord(turn.native_session_takeover)
7626
+ ? turn.native_session_takeover
7627
+ : undefined;
7628
+ const submission = terminalBridgeSubmission(turn);
7629
+ const terminalControl = terminalControlFromTakeover(takeover);
7630
+ const messageId = stringValue(submission?.message_id);
7631
+ if (!takeover ||
7632
+ isRecord(takeover.terminal_bridge_completion_claim) ||
7633
+ !terminalControl ||
7634
+ submission?.status !== "uncertain" ||
7635
+ submission.safe_to_retry === true ||
7636
+ !messageId ||
7637
+ stringValue(takeover.terminal_bridge_message_id) !== messageId ||
7638
+ stringValue(submission.session_id) !== session.session_id ||
7639
+ stringValue(submission.turn_id) !== turnIdForConversation(turn)) {
7640
+ return undefined;
7641
+ }
7642
+ const canonical = pathsForConversation(turn.conversation_id, storeDir);
7643
+ if (path.resolve(stringValue(turn.state_path) ?? "") !==
7644
+ path.resolve(canonical.statePath) ||
7645
+ path.resolve(stringValue(turn.event_log_path) ?? "") !==
7646
+ path.resolve(canonical.logPath) ||
7647
+ path.resolve(managedSessionStoreDirForConversation(turn) ?? "") !==
7648
+ path.resolve(storeDir)) {
7649
+ return undefined;
7650
+ }
7651
+ let ledger;
7652
+ try {
7653
+ ledger = ledgerOverride ?? loadTerminalBridgeDispatchLedger(terminalControl);
7654
+ }
7655
+ catch {
7656
+ return undefined;
7657
+ }
7658
+ if (!ledger ||
7659
+ !terminalDispatchRecordMatchesControl(ledger, terminalControl) ||
7660
+ (requireResolvedTopLevel &&
7661
+ (ledger.status !== "resolved" ||
7662
+ ledger.reason !== "conversation explicitly closed by request" ||
7663
+ !validTimestampMs(ledger.resolved_at) ||
7664
+ stringValue(ledger.conversation_id) !== turn.conversation_id ||
7665
+ stringValue(ledger.session_id) !== session.session_id ||
7666
+ stringValue(ledger.turn_id) !== turnIdForConversation(turn) ||
7667
+ stringValue(ledger.message_id) !== messageId ||
7668
+ stringValue(ledger.request_hash) !==
7669
+ stringValue(submission.request_hash) ||
7670
+ stringValue(ledger.binding_id) !==
7671
+ stringValue(turn.terminal_binding_id) ||
7672
+ Number(ledger.binding_generation) !==
7673
+ Number(turn.terminal_binding_generation) ||
7674
+ path.resolve(stringValue(ledger.state_path) ?? "") !==
7675
+ path.resolve(canonical.statePath) ||
7676
+ path.resolve(stringValue(ledger.event_log_path) ?? "") !==
7677
+ path.resolve(canonical.logPath) ||
7678
+ path.resolve(stringValue(ledger.store_dir) ?? "") !==
7679
+ path.resolve(storeDir)))) {
7680
+ return undefined;
7681
+ }
7682
+ let receipt;
7683
+ try {
7684
+ const matchingReceipts = terminalLedgerReceiptHistory(ledger).filter((candidate) => stringValue(candidate.message_id) === messageId);
7685
+ receipt = matchingReceipts.length === 1
7686
+ ? matchingReceipts[0]
7687
+ : undefined;
7688
+ }
7689
+ catch {
7690
+ return undefined;
7691
+ }
7692
+ if (!receipt ||
7693
+ receipt.status !== "uncertain" ||
7694
+ receipt.safe_to_retry === true ||
7695
+ !validTimestampMs(receipt.uncertain_at) ||
7696
+ !terminalDispatchRecordMatchesControl(receipt, terminalControl) ||
7697
+ stringValue(receipt.conversation_id) !== turn.conversation_id ||
7698
+ stringValue(receipt.session_id) !== session.session_id ||
7699
+ stringValue(receipt.turn_id) !== turnIdForConversation(turn) ||
7700
+ stringValue(receipt.request_hash) !== stringValue(submission.request_hash) ||
7701
+ stringValue(receipt.binding_id) !== stringValue(turn.terminal_binding_id) ||
7702
+ Number(receipt.binding_generation) !==
7703
+ Number(turn.terminal_binding_generation) ||
7704
+ path.resolve(stringValue(receipt.state_path) ?? "") !==
7705
+ path.resolve(canonical.statePath) ||
7706
+ path.resolve(stringValue(receipt.event_log_path) ?? "") !==
7707
+ path.resolve(canonical.logPath) ||
7708
+ path.resolve(stringValue(receipt.store_dir) ?? "") !== path.resolve(storeDir)) {
7709
+ return undefined;
7710
+ }
7711
+ let closeEvent;
7712
+ try {
7713
+ closeEvent = readExistingEvents(canonical.logPath).find((event) => event.event === "conversation_closed" &&
7714
+ event.conversation_id === turn.conversation_id &&
7715
+ event.status === "closed" &&
7716
+ event.ts === turn.closed_at &&
7717
+ event.reason === turn.close_reason);
7718
+ }
7719
+ catch {
7720
+ return undefined;
7721
+ }
7722
+ if (!closeEvent) {
7723
+ return undefined;
7724
+ }
7725
+ // Hash the immutable close event and append-only uncertain receipt, not the
7726
+ // mutable top-level terminal ledger that the next dispatch will replace.
7727
+ return {
7728
+ turnId: turnIdForConversation(turn),
7729
+ messageId,
7730
+ turnFingerprint: createHash("sha256")
7731
+ .update(JSON.stringify(turn))
7732
+ .digest("hex"),
7733
+ closeEvent,
7734
+ uncertainReceipt: receipt
7735
+ };
7736
+ }
7737
+ function explicitlyAbandonedUncertainCandidateTurnFingerprint(args) {
7738
+ const proof = explicitlyAbandonedUncertainCandidateTurnProof(args);
7739
+ return proof
7740
+ ? terminalActionFingerprint({
7741
+ kind: "explicitly_abandoned_uncertain_codex_turn",
7742
+ source_session_id: args.session.session_id,
7743
+ source_revision: managedSessionRevision(args.session),
7744
+ source_binding_token: managedSessionBindingToken(args.session),
7745
+ turn_id: proof.turnId,
7746
+ turn_fingerprint: proof.turnFingerprint,
7747
+ close_event: proof.closeEvent,
7748
+ uncertain_receipt: proof.uncertainReceipt
7749
+ })
7750
+ : undefined;
7751
+ }
7752
+ function explicitlyAbandonedCandidateSourceFingerprint({ storeDir, session, sourceTurnHistory, dispatchSnapshot, sourceRevision = managedSessionRevision(session), sourceBindingToken = managedSessionBindingToken(session), ledgerOverride, requireResolvedTopLevel = true }) {
7753
+ if (dispatchSnapshot.status !== "resolved") {
7754
+ return undefined;
7755
+ }
7756
+ const abandonedTurnProofs = managedTurnsForSession(storeDir, session.session_id).flatMap((turn) => {
7757
+ const proof = explicitlyAbandonedUncertainCandidateTurnProof({
7758
+ storeDir,
7759
+ session,
7760
+ turn,
7761
+ ledgerOverride,
7762
+ requireResolvedTopLevel
7763
+ });
7764
+ return proof
7765
+ ? [{
7766
+ turn_id: proof.turnId,
7767
+ message_id: proof.messageId,
7768
+ turn_fingerprint: proof.turnFingerprint,
7769
+ close_event: proof.closeEvent,
7770
+ uncertain_receipt: proof.uncertainReceipt
7771
+ }]
7772
+ : [];
7773
+ }).sort((left, right) => left.turn_id.localeCompare(right.turn_id));
7774
+ if (abandonedTurnProofs.length === 0) {
7775
+ return undefined;
7776
+ }
7777
+ return terminalActionFingerprint({
7778
+ kind: "explicitly_abandoned_codex_predecessor",
7779
+ source_session_id: session.session_id,
7780
+ source_revision: sourceRevision,
7781
+ source_binding_token: sourceBindingToken,
7782
+ source_turn_history: sourceTurnHistory,
7783
+ previous_dispatch_snapshot: dispatchSnapshot,
7784
+ abandoned_turns: abandonedTurnProofs
7785
+ });
7786
+ }
7787
+ function assertFrozenExplicitlyAbandonedPredecessorAuthority({ storeDir, transfer, terminalControl }) {
7788
+ if (transfer.source_rollout_authority !==
7789
+ "explicitly_abandoned_predecessor") {
7790
+ return;
7791
+ }
7792
+ const source = loadManagedSession(storeDir, transfer.source_session_id);
7793
+ const sourceAsBound = {
7794
+ ...source,
7795
+ status: "bound",
7796
+ binding: transfer.source_before_binding,
7797
+ last_transition_id: transfer.source_previous_last_transition_id
7798
+ };
7799
+ const history = frozenCandidateSourceTurnHistory(storeDir, sourceAsBound);
7800
+ const ledger = loadTerminalBridgeDispatchLedger(terminalControl);
7801
+ const fingerprint = history && ledger
7802
+ ? explicitlyAbandonedCandidateSourceFingerprint({
7803
+ storeDir,
7804
+ session: sourceAsBound,
7805
+ sourceTurnHistory: history,
7806
+ dispatchSnapshot: {
7807
+ status: transfer.previous_dispatch_status,
7808
+ fingerprint: transfer.previous_dispatch_fingerprint
7809
+ },
7810
+ sourceRevision: transfer.source_expected_revision,
7811
+ sourceBindingToken: transfer.source_binding_token,
7812
+ ledgerOverride: ledger,
7813
+ requireResolvedTopLevel: false
7814
+ })
7815
+ : undefined;
7816
+ if (!history ||
7817
+ JSON.stringify(history) !== JSON.stringify(transfer.source_turn_history) ||
7818
+ fingerprint !== transfer.source_abandonment_fingerprint) {
7819
+ throw new Error(`deferred foreground transfer ${transfer.transfer_id} lost its exact ` +
7820
+ "explicitly abandoned predecessor authority");
7821
+ }
7114
7822
  }
7115
7823
  function candidateSourceTransitionHistoryIsTerminal(storeDir, session) {
7116
7824
  if (managedSessionHasUnresolvedNativeTransition(storeDir, session)) {
@@ -7129,7 +7837,7 @@ function candidateSourceTransitionHistoryIsTerminal(storeDir, session) {
7129
7837
  return false;
7130
7838
  }
7131
7839
  }
7132
- function deferredCodexForegroundBindingToken({ terminalId, terminalControl, pid, workspace, processUuid, processBirth, sourceSession, dispatchSnapshot, candidateInventory }) {
7840
+ function deferredCodexForegroundBindingToken({ terminalId, terminalControl, pid, workspace, processUuid, processBirth, sourceSession, dispatchSnapshot, candidateInventory, sourceTurnHistory, sourceRolloutAuthority = "present", sourceAbandonmentFingerprint }) {
7133
7841
  const terminalToken = unmanagedTerminalBindingToken({
7134
7842
  terminalId,
7135
7843
  terminalControl,
@@ -7147,18 +7855,35 @@ function deferredCodexForegroundBindingToken({ terminalId, terminalControl, pid,
7147
7855
  candidate_native_thread_ids: candidateInventory.roots.map((root) => root.sessionId)
7148
7856
  }
7149
7857
  : undefined;
7858
+ const sourceTurnHistoryFingerprint = sourceTurnHistory
7859
+ ? createHash("sha256")
7860
+ .update(JSON.stringify(sourceTurnHistory))
7861
+ .digest("hex")
7862
+ : undefined;
7150
7863
  return createHash("sha256")
7151
7864
  .update(JSON.stringify({
7152
- version: candidateAuthority ? 3 : 2,
7865
+ version: candidateAuthority ? 5 : 2,
7153
7866
  kind: "deferred_codex_foreground_binding",
7154
7867
  terminal_token: terminalToken,
7868
+ composer_state: "styled_empty",
7155
7869
  source_session_id: sourceSession.session_id,
7156
7870
  source_revision: managedSessionRevision(sourceSession),
7157
7871
  source_binding_token: managedSessionBindingToken(sourceSession),
7158
7872
  terminal_dispatch_snapshot: dispatchSnapshot,
7159
7873
  observation: candidateAuthority
7160
- ? "exact_unbound_open_root_inventory"
7874
+ ? "exact_open_root_inventory"
7161
7875
  : "verified_absent",
7876
+ ...(sourceTurnHistoryFingerprint
7877
+ ? { source_turn_history_fingerprint: sourceTurnHistoryFingerprint }
7878
+ : {}),
7879
+ ...(candidateAuthority
7880
+ ? { source_rollout_authority: sourceRolloutAuthority }
7881
+ : {}),
7882
+ ...(sourceAbandonmentFingerprint
7883
+ ? {
7884
+ source_abandonment_fingerprint: sourceAbandonmentFingerprint
7885
+ }
7886
+ : {}),
7162
7887
  ...(candidateAuthority ?? {})
7163
7888
  }))
7164
7889
  .digest("hex");
@@ -7484,7 +8209,8 @@ async function assertDeferredCodexForegroundBindingBoundary({ options, boundary,
7484
8209
  pid: boundary.terminal.pid,
7485
8210
  workspace: required(boundary.terminal.terminalControl.currentPath, "deferred Codex terminal workspace is unavailable"),
7486
8211
  processUuid: boundary.processUuid,
7487
- processBirth: boundary.processBirth
8212
+ processBirth: boundary.processBirth,
8213
+ sourceRolloutAuthority: boundary.sourceRolloutAuthority
7488
8214
  })) {
7489
8215
  throw new Error("the exact Codex open-root inventory changed; refresh AKK list before sending");
7490
8216
  }
@@ -7526,7 +8252,27 @@ async function assertDeferredCodexForegroundBindingBoundary({ options, boundary,
7526
8252
  }
7527
8253
  }
7528
8254
  else {
7529
- const history = deferredCandidateSourceTurnHistory(storeDir, sourceAsBound);
8255
+ const history = expectedSourceStatus === "bound"
8256
+ ? deferredCandidateSourceTurnHistory(storeDir, sourceAsBound)
8257
+ : frozenCandidateSourceTurnHistory(storeDir, sourceAsBound);
8258
+ const currentAbandonmentFingerprint = boundary.sourceRolloutAuthority ===
8259
+ "explicitly_abandoned_predecessor" &&
8260
+ history
8261
+ ? explicitlyAbandonedCandidateSourceFingerprint({
8262
+ storeDir,
8263
+ session: sourceAsBound,
8264
+ sourceTurnHistory: history,
8265
+ dispatchSnapshot: boundary.previousDispatchSnapshot,
8266
+ sourceRevision: boundary.sourceBoundRevision,
8267
+ sourceBindingToken: boundary.sourceBoundBindingToken,
8268
+ ...(expectedSourceStatus === "transitioning"
8269
+ ? {
8270
+ ledgerOverride: loadTerminalBridgeDispatchLedger(boundary.terminal.terminalControl),
8271
+ requireResolvedTopLevel: false
8272
+ }
8273
+ : {})
8274
+ })
8275
+ : undefined;
7530
8276
  if (!boundary.candidateAcceptanceAnchor ||
7531
8277
  !history ||
7532
8278
  JSON.stringify(history) !== JSON.stringify(boundary.sourceTurnHistory) ||
@@ -7538,8 +8284,15 @@ async function assertDeferredCodexForegroundBindingBoundary({ options, boundary,
7538
8284
  workspace: required(boundary.terminal.terminalControl.currentPath, "deferred Codex terminal workspace is unavailable"),
7539
8285
  processUuid: boundary.processUuid,
7540
8286
  processBirth: boundary.processBirth,
7541
- inventory: required(candidateInventoryForBoundary, "deferred Codex candidate inventory is unavailable")
8287
+ inventory: required(candidateInventoryForBoundary, "deferred Codex candidate inventory is unavailable"),
8288
+ sourceRolloutAuthority: boundary.sourceRolloutAuthority
7542
8289
  }) ||
8290
+ (boundary.sourceRolloutAuthority ===
8291
+ "explicitly_abandoned_predecessor" &&
8292
+ (!boundary.sourceAbandonmentFingerprint ||
8293
+ currentAbandonmentFingerprint !==
8294
+ boundary.sourceAbandonmentFingerprint ||
8295
+ boundary.previousDispatchSnapshot.status !== "resolved")) ||
7543
8296
  !candidateSourceTransitionHistoryIsTerminal(storeDir, sourceAsBound)) {
7544
8297
  throw new Error("the deferred Codex candidate source history or rollout authority changed");
7545
8298
  }
@@ -7575,15 +8328,22 @@ async function assertDeferredCodexForegroundBindingBoundary({ options, boundary,
7575
8328
  }
7576
8329
  return source;
7577
8330
  }
7578
- async function prepareDeferredCodexForegroundBinding({ options, terminal, sourceSession, observation, candidateInventory, requestText }) {
8331
+ async function prepareDeferredCodexForegroundBinding({ options, terminal, sourceSession, observation, candidateInventory, requestText, allowImplicitFreshAuthority = false }) {
7579
8332
  const expectedToken = stringValue(options.expectedTerminalToken);
7580
8333
  const candidateMode = Boolean(candidateInventory?.roots.length);
7581
8334
  if (terminal.agent !== "codex" ||
7582
8335
  !sourceSession?.binding ||
7583
8336
  (observation.status !== "verified_absent" && !candidateMode) ||
7584
- !expectedToken) {
8337
+ (!expectedToken && !allowImplicitFreshAuthority)) {
7585
8338
  return undefined;
7586
8339
  }
8340
+ if (allowImplicitFreshAuthority &&
8341
+ (expectedToken !== undefined ||
8342
+ !candidateMode ||
8343
+ !isCompleteNativeRollout(sourceSession.binding.native_process.rollout))) {
8344
+ throw new Error("implicit Codex candidate authority requires one fresh complete " +
8345
+ "nonempty rollout inventory for a rollout-backed source");
8346
+ }
7587
8347
  const processUuid = sourceSession.binding.native_process.process_uuid;
7588
8348
  const processBirth = sourceSession.binding.native_process.process_birth;
7589
8349
  const workspace = terminal.terminalControl.currentPath;
@@ -7596,6 +8356,27 @@ async function prepareDeferredCodexForegroundBinding({ options, terminal, source
7596
8356
  "candidate_rollout_quiescent"
7597
8357
  ? deferredCandidateSourceTurnHistory(storeDir, sourceSession)
7598
8358
  : undefined;
8359
+ const dispatchSnapshot = deferredCodexForegroundDispatchSnapshot(terminal.terminalControl);
8360
+ const sourceRolloutPresent = Boolean(candidateInventory &&
8361
+ sourceSession.binding.native_process.rollout &&
8362
+ candidateInventory.roots.some((root) => root.sessionId.toLowerCase() ===
8363
+ sourceSession.binding?.native_thread_id?.toLowerCase() &&
8364
+ exactNativeRolloutMatches(root.rollout, sourceSession.binding.native_process.rollout)));
8365
+ const sourceRolloutAuthority = sourceKind === "candidate_rollout_quiescent" &&
8366
+ candidateInventory &&
8367
+ sourceSession.binding.native_process.rollout &&
8368
+ !sourceRolloutPresent
8369
+ ? "explicitly_abandoned_predecessor"
8370
+ : "present";
8371
+ const sourceAbandonmentFingerprint = sourceRolloutAuthority === "explicitly_abandoned_predecessor" &&
8372
+ candidateSourceTurnHistory
8373
+ ? explicitlyAbandonedCandidateSourceFingerprint({
8374
+ storeDir,
8375
+ session: sourceSession,
8376
+ sourceTurnHistory: candidateSourceTurnHistory,
8377
+ dispatchSnapshot
8378
+ })
8379
+ : undefined;
7599
8380
  const exactSource = !workspace
7600
8381
  ? false
7601
8382
  : sourceKind === "candidate_rollout_quiescent"
@@ -7610,7 +8391,8 @@ async function prepareDeferredCodexForegroundBinding({ options, terminal, source
7610
8391
  workspace,
7611
8392
  processUuid: liveIncarnation.processUuid,
7612
8393
  processBirth: liveIncarnation.processBirth,
7613
- inventory: candidateInventory
8394
+ inventory: candidateInventory,
8395
+ sourceRolloutAuthority
7614
8396
  })
7615
8397
  : exactBoundCodexStatusCardSourceForDeferredSend({
7616
8398
  session: sourceSession,
@@ -7631,7 +8413,10 @@ async function prepareDeferredCodexForegroundBinding({ options, terminal, source
7631
8413
  !exactSource) {
7632
8414
  return undefined;
7633
8415
  }
7634
- const dispatchSnapshot = deferredCodexForegroundDispatchSnapshot(terminal.terminalControl);
8416
+ if (sourceRolloutAuthority === "explicitly_abandoned_predecessor" &&
8417
+ !sourceAbandonmentFingerprint) {
8418
+ return undefined;
8419
+ }
7635
8420
  const token = deferredCodexForegroundBindingToken({
7636
8421
  terminalId: terminal.conversationId,
7637
8422
  terminalControl: terminal.terminalControl,
@@ -7641,12 +8426,27 @@ async function prepareDeferredCodexForegroundBinding({ options, terminal, source
7641
8426
  processBirth,
7642
8427
  sourceSession,
7643
8428
  dispatchSnapshot,
8429
+ sourceTurnHistory: candidateSourceTurnHistory,
8430
+ sourceRolloutAuthority,
8431
+ sourceAbandonmentFingerprint,
7644
8432
  ...(candidateMode ? { candidateInventory } : {})
7645
8433
  });
7646
- if (expectedToken !== token) {
8434
+ if (!allowImplicitFreshAuthority && expectedToken !== token) {
7647
8435
  throw new Error("deferred Codex foreground binding requires the fresh exact terminal " +
7648
8436
  "token advertised by AKK list");
7649
8437
  }
8438
+ if (allowImplicitFreshAuthority) {
8439
+ runtimeLog("info", "deferred_codex_implicit_candidate_authority", {
8440
+ terminal_id: terminal.conversationId,
8441
+ terminal_target: terminal.terminalControl.target,
8442
+ source_session_id: sourceSession.session_id,
8443
+ inventory_status: candidateInventory?.status,
8444
+ inventory_fingerprint: candidateInventory?.inventoryFingerprint,
8445
+ candidate_count: candidateInventory?.roots.length,
8446
+ authority_scope: "terminal_follow_current",
8447
+ terminal_input_sent: false
8448
+ });
8449
+ }
7650
8450
  const targetSessionId = createManagedSessionId();
7651
8451
  const transferId = createDeferredForegroundTransferId();
7652
8452
  const candidateAcceptanceAnchor = candidateMode
@@ -7666,6 +8466,10 @@ async function prepareDeferredCodexForegroundBinding({ options, terminal, source
7666
8466
  processBirth,
7667
8467
  previousDispatchSnapshot: dispatchSnapshot,
7668
8468
  sourceKind,
8469
+ sourceRolloutAuthority,
8470
+ ...(sourceAbandonmentFingerprint
8471
+ ? { sourceAbandonmentFingerprint }
8472
+ : {}),
7669
8473
  ...(sourceSession.last_transition_id
7670
8474
  ? { sourcePreviousLastTransitionId: sourceSession.last_transition_id }
7671
8475
  : {}),
@@ -7690,6 +8494,52 @@ async function prepareDeferredCodexForegroundBinding({ options, terminal, source
7690
8494
  terminalControl: terminal.terminalControl,
7691
8495
  excludedManagedSessionId: sourceSession.session_id
7692
8496
  });
8497
+ if (candidateInventory &&
8498
+ !codexCandidateInventoryHasNoOtherManagedClaim({
8499
+ storeDir,
8500
+ inventory: candidateInventory,
8501
+ sourceSessionId: sourceSession.session_id,
8502
+ includeDetached: sourceRolloutAuthority === "explicitly_abandoned_predecessor"
8503
+ })) {
8504
+ throw new Error("a Codex rollout candidate is already claimed by another Session");
8505
+ }
8506
+ if (candidateInventory &&
8507
+ sourceRolloutAuthority === "explicitly_abandoned_predecessor") {
8508
+ for (const root of candidateInventory.roots) {
8509
+ await assertNativeThreadHasExclusiveOwnership({
8510
+ options,
8511
+ agent: "codex",
8512
+ currentPid: terminal.pid,
8513
+ nativeThreadId: root.sessionId,
8514
+ storeDir,
8515
+ terminalControl: terminal.terminalControl,
8516
+ excludedManagedSessionId: root.sessionId.toLowerCase() ===
8517
+ sourceSession.binding.native_thread_id?.toLowerCase()
8518
+ ? sourceSession.session_id
8519
+ : undefined
8520
+ });
8521
+ }
8522
+ }
8523
+ // The source semantic proof is about to be replaced by the transfer's own
8524
+ // top-level dispatch generation. Recheck it immediately before publishing
8525
+ // the prepared transfer so every later phase can rely on the immutable
8526
+ // fingerprint plus the Store protocol-5 source-Turn fence.
8527
+ if (sourceRolloutAuthority === "explicitly_abandoned_predecessor") {
8528
+ const freshHistory = deferredCandidateSourceTurnHistory(storeDir, sourceSession);
8529
+ const freshAbandonmentFingerprint = freshHistory
8530
+ ? explicitlyAbandonedCandidateSourceFingerprint({
8531
+ storeDir,
8532
+ session: sourceSession,
8533
+ sourceTurnHistory: freshHistory,
8534
+ dispatchSnapshot
8535
+ })
8536
+ : undefined;
8537
+ if (JSON.stringify(freshHistory) !==
8538
+ JSON.stringify(candidateSourceTurnHistory) ||
8539
+ freshAbandonmentFingerprint !== sourceAbandonmentFingerprint) {
8540
+ throw new Error("the explicitly abandoned Codex predecessor authority changed; refresh AKK list");
8541
+ }
8542
+ }
7693
8543
  const existingTransfer = listDeferredForegroundTransfers(storeDir).find((candidate) => !["resolved", "abort_resolved"].includes(candidate.status) &&
7694
8544
  (candidate.source_session_id === sourceSession.session_id ||
7695
8545
  (candidate.terminal_id === terminal.conversationId &&
@@ -7716,6 +8566,12 @@ async function prepareDeferredCodexForegroundBinding({ options, terminal, source
7716
8566
  source_previous_last_transition_id: sourceSession.last_transition_id,
7717
8567
  source_before_binding: sourceSession.binding,
7718
8568
  source_kind: sourceKind,
8569
+ ...(sourceRolloutAuthority === "explicitly_abandoned_predecessor"
8570
+ ? { source_rollout_authority: sourceRolloutAuthority }
8571
+ : {}),
8572
+ ...(sourceAbandonmentFingerprint
8573
+ ? { source_abandonment_fingerprint: sourceAbandonmentFingerprint }
8574
+ : {}),
7719
8575
  ...(candidateSourceTurnHistory
7720
8576
  ? { source_turn_history: candidateSourceTurnHistory }
7721
8577
  : {}),
@@ -7749,6 +8605,10 @@ function assertDeferredForegroundTransferMatchesBoundary({ transfer, boundary })
7749
8605
  (transfer.version === 1
7750
8606
  ? "status_card_only"
7751
8607
  : transfer.source_kind) !== boundary.sourceKind ||
8608
+ (transfer.source_rollout_authority ?? "present") !==
8609
+ boundary.sourceRolloutAuthority ||
8610
+ transfer.source_abandonment_fingerprint !==
8611
+ boundary.sourceAbandonmentFingerprint ||
7752
8612
  JSON.stringify(transfer.source_turn_history) !==
7753
8613
  JSON.stringify(boundary.sourceTurnHistory) ||
7754
8614
  transfer.source_previous_last_transition_id !==
@@ -8135,6 +8995,11 @@ async function commitDeferredCodexForegroundBinding({ options, boundary, identit
8135
8995
  const storeDir = storeDirFromOptions(options);
8136
8996
  let transfer = loadDeferredForegroundTransfer(storeDir, boundary.transferId);
8137
8997
  assertDeferredForegroundTransferMatchesBoundary({ transfer, boundary });
8998
+ assertFrozenExplicitlyAbandonedPredecessorAuthority({
8999
+ storeDir,
9000
+ transfer,
9001
+ terminalControl: boundary.terminal.terminalControl
9002
+ });
8138
9003
  if (!["dispatch_started", "uncertain"].includes(transfer.status) ||
8139
9004
  transfer.input_stage === "none" ||
8140
9005
  identity.processUuid !== transfer.process_uuid ||
@@ -8184,7 +9049,11 @@ async function commitDeferredCodexForegroundBinding({ options, boundary, identit
8184
9049
  ? transfer.target_session_id
8185
9050
  : sameNativeThread
8186
9051
  ? transfer.source_session_id
8187
- : transfer.target_session_id
9052
+ : transfer.target_session_id,
9053
+ allowedManagedSessionIds: transfer.source_rollout_authority ===
9054
+ "explicitly_abandoned_predecessor"
9055
+ ? [transfer.source_session_id]
9056
+ : []
8188
9057
  });
8189
9058
  const retirement = sameNativeThread
8190
9059
  ? "binding_scrubbed_same_native_thread"
@@ -8360,7 +9229,11 @@ async function resolveCommittedDeferredCodexForegroundTransfer({ options, bounda
8360
9229
  nativeThreadId: transfer.target_native_thread_id,
8361
9230
  storeDir,
8362
9231
  terminalControl: boundary.terminal.terminalControl,
8363
- excludedManagedSessionId: target.session_id
9232
+ excludedManagedSessionId: target.session_id,
9233
+ allowedManagedSessionIds: transfer.source_rollout_authority ===
9234
+ "explicitly_abandoned_predecessor"
9235
+ ? [transfer.source_session_id]
9236
+ : []
8364
9237
  });
8365
9238
  transfer = saveDeferredForegroundTransfer(storeDir, {
8366
9239
  ...transfer,
@@ -8563,7 +9436,13 @@ async function maybeAdoptObservedExternalThread({ options, terminal, sourceSessi
8563
9436
  options,
8564
9437
  terminal,
8565
9438
  sourceSession,
8566
- resolvedIdentity
9439
+ resolvedIdentity,
9440
+ // Snapshot-bound terminal actions apply their own stricter boundary
9441
+ // before any input. In particular, a post-/clear Codex composer can be
9442
+ // safely empty while the passive activity classifier is still unknown;
9443
+ // do not force the unrelated external-handoff idle check before the
9444
+ // deferred candidate boundary gets a chance to revalidate it.
9445
+ requireSafeTerminal: !stringValue(options.expectedTerminalToken)
8567
9446
  });
8568
9447
  const identity = observed.identity;
8569
9448
  const conflictKind = managedBindingConflictKindForResolvedTerminal({
@@ -9207,7 +10086,7 @@ function verifiedWorkspaceRelationship(targetWorkspace, candidateWorkspace) {
9207
10086
  return "unknown";
9208
10087
  }
9209
10088
  }
9210
- async function assertNativeThreadHasExclusiveOwnership({ options, agent, currentPid, nativeThreadId, storeDir, terminalControl, excludedManagedSessionId }) {
10089
+ async function assertNativeThreadHasExclusiveOwnership({ options, agent, currentPid, nativeThreadId, storeDir, terminalControl, excludedManagedSessionId, allowedManagedSessionIds = [] }) {
9211
10090
  const normalizedNativeThreadId = nativeThreadId.toLowerCase();
9212
10091
  if (!isExactNativeThreadId(normalizedNativeThreadId)) {
9213
10092
  throw new Error("native thread ownership requires an exact thread UUID");
@@ -9232,6 +10111,7 @@ async function assertNativeThreadHasExclusiveOwnership({ options, agent, current
9232
10111
  throw new Error(`native thread ownership is unverifiable: ${activeOwnerScan.uncertaintyReasons.join("; ")}`);
9233
10112
  }
9234
10113
  const conflictingSessions = listManagedSessions(storeDir).filter((session) => session.session_id !== excludedManagedSessionId &&
10114
+ !allowedManagedSessionIds.includes(session.session_id) &&
9235
10115
  session.agent === agent &&
9236
10116
  session.binding?.native_thread_id?.toLowerCase() === normalizedNativeThreadId);
9237
10117
  if (conflictingSessions.length > 0) {
@@ -12022,6 +12902,11 @@ async function runSend(options) {
12022
12902
  }
12023
12903
  assertTerminalIncarnationCanStartTurn(rawStoreDir, terminalConversation.terminalControl);
12024
12904
  let claimedSession = soleBoundManagedSessionClaimForTerminal(rawStoreDir, terminalConversation);
12905
+ const suppliedExpectedTerminalToken = stringValue(options.expectedTerminalToken);
12906
+ const implicitCodexCandidateAuthority = Boolean(terminalConversation.agent === "codex" &&
12907
+ suppliedExpectedTerminalToken === undefined &&
12908
+ claimedSession?.status === "bound" &&
12909
+ isCompleteNativeRollout(claimedSession.binding?.native_process.rollout));
12025
12910
  let knownCodexCompanions = claimedSession
12026
12911
  ? codexAllowedCompanionSetForManagedSession({
12027
12912
  storeDir: rawStoreDir,
@@ -12041,7 +12926,7 @@ async function runSend(options) {
12041
12926
  });
12042
12927
  let deferredCodexCandidateInventory;
12043
12928
  if (terminalConversation.agent === "codex" &&
12044
- stringValue(options.expectedTerminalToken)) {
12929
+ (suppliedExpectedTerminalToken || implicitCodexCandidateAuthority)) {
12045
12930
  try {
12046
12931
  const inventory = await inspectCodexOpenRootRolloutInventory({
12047
12932
  options,
@@ -12053,12 +12938,18 @@ async function runSend(options) {
12053
12938
  }
12054
12939
  }
12055
12940
  catch (error) {
12056
- if (nativeIdentityObservation.status === "unavailable") {
12057
- throw new Error(`native Codex identity observation is unavailable and its exact ` +
12058
- `open-root inventory could not be proven: ${error instanceof Error ? error.message : String(error)}`);
12941
+ if (implicitCodexCandidateAuthority ||
12942
+ nativeIdentityObservation.status === "unavailable") {
12943
+ throw new Error(`native Codex foreground attribution requires a fresh complete ` +
12944
+ `open-root inventory: ${error instanceof Error ? error.message : String(error)}`);
12059
12945
  }
12060
12946
  }
12061
12947
  }
12948
+ if (implicitCodexCandidateAuthority &&
12949
+ !deferredCodexCandidateInventory) {
12950
+ throw new Error("rollout-backed Codex terminal send requires a fresh complete " +
12951
+ "nonempty open-root inventory; refresh AKK list before sending");
12952
+ }
12062
12953
  if (nativeIdentityObservation.status === "unavailable" &&
12063
12954
  !deferredCodexCandidateInventory) {
12064
12955
  throw new Error(`native ${terminalConversation.agent} identity observation is ` +
@@ -12067,12 +12958,17 @@ async function runSend(options) {
12067
12958
  let currentNativeIdentity = nativeIdentityObservation.status === "resolved"
12068
12959
  ? nativeIdentityObservation.identity
12069
12960
  : undefined;
12070
- const verifiedEmptyHandoff = await maybeDetachVerifiedEmptyCodexSource({
12071
- options,
12072
- terminal: terminalConversation,
12073
- sourceSession: claimedSession,
12074
- observation: nativeIdentityObservation
12075
- });
12961
+ // A fresh nonempty inventory is the stronger physical authority for an
12962
+ // implicit candidate send. Do not let an earlier verified-absent
12963
+ // observation divert this path into the token-only empty handoff.
12964
+ const verifiedEmptyHandoff = implicitCodexCandidateAuthority
12965
+ ? undefined
12966
+ : await maybeDetachVerifiedEmptyCodexSource({
12967
+ options,
12968
+ terminal: terminalConversation,
12969
+ sourceSession: claimedSession,
12970
+ observation: nativeIdentityObservation
12971
+ });
12076
12972
  if (verifiedEmptyHandoff) {
12077
12973
  // The old rollout is conclusively closed. Never carry it forward as
12078
12974
  // a pre-materialization companion for the new virgin Session.
@@ -12084,7 +12980,13 @@ async function runSend(options) {
12084
12980
  let handoff = await maybeAdoptObservedExternalThread({
12085
12981
  options,
12086
12982
  terminal: terminalConversation,
12087
- sourceSession: claimedSession,
12983
+ // A no-token raw send to an already managed rollout-backed Codex pane
12984
+ // is an internal follow-current delegation, never a sole-root strict
12985
+ // continuation or an external-handoff adoption. The dedicated v3
12986
+ // transfer below owns attribution and lineage.
12987
+ sourceSession: implicitCodexCandidateAuthority
12988
+ ? undefined
12989
+ : claimedSession,
12088
12990
  resolvedIdentity: currentNativeIdentity,
12089
12991
  storeDir: rawStoreDir
12090
12992
  });
@@ -12101,7 +13003,8 @@ async function runSend(options) {
12101
13003
  sourceSession: claimedSession,
12102
13004
  observation: nativeIdentityObservation,
12103
13005
  candidateInventory: deferredCodexCandidateInventory,
12104
- requestText: String(messageBody)
13006
+ requestText: String(messageBody),
13007
+ allowImplicitFreshAuthority: implicitCodexCandidateAuthority
12105
13008
  })
12106
13009
  : undefined;
12107
13010
  if (deferredCodexForegroundBinding) {
@@ -12110,7 +13013,7 @@ async function runSend(options) {
12110
13013
  currentNativeIdentity = undefined;
12111
13014
  handoff = { identity: undefined, adopted: false };
12112
13015
  }
12113
- else if (stringValue(options.expectedTerminalToken) &&
13016
+ else if ((suppliedExpectedTerminalToken || implicitCodexCandidateAuthority) &&
12114
13017
  !verifiedEmptyHandoff &&
12115
13018
  !handoff.adopted) {
12116
13019
  throw new Error("the expected terminal token no longer authorizes the current " +
@@ -12378,6 +13281,17 @@ async function runSend(options) {
12378
13281
  const releaseTerminalLock = acquireTerminalBridgeSendLock(storeDir, resolvedTerminal.terminalControl, { timeoutMs: 30000 });
12379
13282
  try {
12380
13283
  await withStoreWriterLeaseAsync(storeDir, async () => {
13284
+ const lockedStrictSession = tryLoadManagedSession(storeDir, sessionId);
13285
+ if (lockedStrictSession?.agent === "codex" &&
13286
+ lockedStrictSession.session_id === sessionId &&
13287
+ lockedStrictSession.status === "bound" &&
13288
+ isCompleteNativeRollout(lockedStrictSession.binding?.native_process.rollout)) {
13289
+ throw new Error(`Codex rollout-backed managed Session ${sessionId} cannot use a ` +
13290
+ "strict session_id send because an open rollout does not prove the " +
13291
+ "current TUI foreground thread. Refresh AKK list and use its exact " +
13292
+ "selector plus expected_terminal_token. No Turn was created and no " +
13293
+ "terminal input was sent.");
13294
+ }
12381
13295
  await recoverLifecycleFenceBeforeMutation({
12382
13296
  options,
12383
13297
  terminal: resolvedTerminal
@@ -12398,12 +13312,27 @@ async function runSend(options) {
12398
13312
  }
12399
13313
  assertTerminalIncarnationCanStartTurn(storeDir, resolvedTerminal.terminalControl);
12400
13314
  let currentSession = tryLoadManagedSession(storeDir, sessionId);
12401
- const knownCodexCompanions = currentSession
13315
+ let knownCodexCompanions = currentSession
12402
13316
  ? codexAllowedCompanionSetForManagedSession({
12403
13317
  storeDir,
12404
13318
  session: currentSession
12405
13319
  })
12406
13320
  : { additional: [] };
13321
+ if (currentSession?.agent === "codex" &&
13322
+ knownCodexCompanions.primary) {
13323
+ try {
13324
+ const inventory = await inspectCodexOpenRootRolloutInventory({
13325
+ options,
13326
+ pid: resolvedTerminal.pid,
13327
+ cwd: resolvedTerminal.terminalControl.currentPath
13328
+ });
13329
+ knownCodexCompanions = codexCompanionsPresentInOpenRootInventory(knownCodexCompanions, inventory);
13330
+ }
13331
+ catch {
13332
+ // Inventory proof is an optimization only. Preserve the existing
13333
+ // closed /status fence when exact open-root membership is unavailable.
13334
+ }
13335
+ }
12407
13336
  const lockedNativeIdentity = await resolveCurrentNativeAgentSessionIdentity({
12408
13337
  options,
12409
13338
  agent: resolvedTerminal.agent,
@@ -16596,6 +17525,11 @@ async function runRenew(options) {
16596
17525
  if (conversation.status !== "stalled") {
16597
17526
  throw new Error(`cannot renew ${conversation.conversation_id}; conversation is ${conversation.status}, not stalled`);
16598
17527
  }
17528
+ if (terminalBridgeSubmission(conversation)?.status === "uncertain") {
17529
+ throw new Error(`cannot renew ${conversation.conversation_id}; its terminal submission ` +
17530
+ "is uncertain and cannot be attributed by monitoring. Inspect the pane " +
17531
+ "and explicitly close the Turn to abandon the unresolved result.");
17532
+ }
16599
17533
  const nativeTakeover = isRecord(conversation.native_session_takeover)
16600
17534
  ? conversation.native_session_takeover
16601
17535
  : undefined;
@@ -16622,6 +17556,9 @@ async function runRenew(options) {
16622
17556
  if (current.status !== "stalled") {
16623
17557
  throw new Error(`cannot renew ${current.conversation_id}; conversation is ${current.status}, not stalled`);
16624
17558
  }
17559
+ if (terminalBridgeSubmission(current)?.status === "uncertain") {
17560
+ throw new Error(`cannot renew ${current.conversation_id}; its terminal submission is uncertain`);
17561
+ }
16625
17562
  const currentTakeover = isRecord(current.native_session_takeover)
16626
17563
  ? current.native_session_takeover
16627
17564
  : undefined;
@@ -20276,6 +21213,32 @@ function terminalLedgerReceiptCandidate(ledger) {
20276
21213
  }
20277
21214
  function mergeTerminalLedgerReceipt(previous, next) {
20278
21215
  const messageId = required(stringValue(previous.message_id), "terminal dispatch receipt message id is required");
21216
+ const safeAbortRetryGeneration = Boolean(previous.status === "aborted" &&
21217
+ previous.safe_to_retry === true &&
21218
+ stringValue(next.message_id) === messageId &&
21219
+ stringValue(next.previous_generation_id) === messageId &&
21220
+ [
21221
+ "text_injected",
21222
+ "enter_dispatched",
21223
+ "submitted",
21224
+ "agent_accepted",
21225
+ "not_accepted",
21226
+ "uncertain",
21227
+ "aborted"
21228
+ ].includes(String(next.status)) &&
21229
+ validTimestampMs(previous.aborted_at) !== undefined &&
21230
+ validTimestampMs(next.prepared_at) !== undefined &&
21231
+ Date.parse(String(next.prepared_at)) >=
21232
+ Date.parse(String(previous.aborted_at)));
21233
+ if (safeAbortRetryGeneration) {
21234
+ // A proved zero-input abort intentionally allows the same OpenClaw
21235
+ // idempotency key to start one fresh Turn/Session generation. The old
21236
+ // Turn and, for v3, its abort_resolved transfer retain that immutable
21237
+ // proof; the terminal-wide singleton receipt advances to the retry so its
21238
+ // different binding/Turn identity cannot collide with the abandoned
21239
+ // zero-input generation.
21240
+ return next;
21241
+ }
20279
21242
  for (const field of TERMINAL_LEDGER_RECEIPT_IMMUTABLE_FIELDS) {
20280
21243
  const previousValue = previous[field];
20281
21244
  const nextValue = next[field];
@@ -20997,6 +21960,12 @@ function deferredCodexBoundaryFromTransfer({ terminal, transfer }) {
20997
21960
  sourceKind: transfer.version === 1
20998
21961
  ? "status_card_only"
20999
21962
  : required(transfer.source_kind, "deferred foreground source kind is unavailable"),
21963
+ sourceRolloutAuthority: transfer.source_rollout_authority ?? "present",
21964
+ ...(transfer.source_abandonment_fingerprint
21965
+ ? {
21966
+ sourceAbandonmentFingerprint: transfer.source_abandonment_fingerprint
21967
+ }
21968
+ : {}),
21000
21969
  ...(transfer.source_turn_history
21001
21970
  ? { sourceTurnHistory: transfer.source_turn_history }
21002
21971
  : {}),
@@ -21565,6 +22534,11 @@ async function recoverAcceptedDeferredForegroundDispatch({ options, storeDir, te
21565
22534
  statePath: authority.statePath,
21566
22535
  expectedMessageBodyHash: stringValue(authority.submission.message_body_hash)
21567
22536
  });
22537
+ assertFrozenExplicitlyAbandonedPredecessorAuthority({
22538
+ storeDir,
22539
+ transfer,
22540
+ terminalControl: terminal.terminalControl
22541
+ });
21568
22542
  if (![
21569
22543
  "prepared",
21570
22544
  "text_injected",
@@ -23109,6 +24083,56 @@ function codexComposerVisible(screen) {
23109
24083
  .slice(-8)
23110
24084
  .some((line) => /^[›»](?:\s|$)/u.test(line.trimEnd()));
23111
24085
  }
24086
+ function codexLatentClearResumeObservation({ screen, agentVersion }) {
24087
+ const behaviorProfile = codexLifecycleBehaviorProfile(agentVersion);
24088
+ if (!behaviorProfile) {
24089
+ return undefined;
24090
+ }
24091
+ const withoutEscapes = (line) => line.replace(/\x1B(?:\[[0-?]*[ -/]*[@-~]|\][^\x07]*(?:\x07|\x1B\\))/gu, "");
24092
+ const resumePrefix = /^\s*To continue this session, run codex resume\s+(.+?)\s*$/iu;
24093
+ const exactUuid = /^([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/iu;
24094
+ const lines = String(screen ?? "")
24095
+ .split(/\r?\n/u)
24096
+ .slice(-24)
24097
+ .map(withoutEscapes);
24098
+ const resumeIds = [];
24099
+ for (let index = 0; index < lines.length; index += 1) {
24100
+ const prefixMatch = resumePrefix.exec(lines[index]);
24101
+ if (!prefixMatch) {
24102
+ continue;
24103
+ }
24104
+ // Codex wraps the UUID after a hyphen in narrow panes. Only join the
24105
+ // immediately following line, strip layout whitespace, and still require
24106
+ // the resulting value to be one exact UUID. This keeps a prose lookalike
24107
+ // or an unrelated scrollback line from becoming routing authority.
24108
+ const firstFragment = prefixMatch[1].replace(/\s+/gu, "");
24109
+ const fragments = [
24110
+ firstFragment,
24111
+ ...(firstFragment.endsWith("-")
24112
+ ? [`${prefixMatch[1]}${lines[index + 1] ?? ""}`.replace(/\s+/gu, "")]
24113
+ : [])
24114
+ ];
24115
+ const matched = fragments.flatMap((candidate) => {
24116
+ const uuidMatch = exactUuid.exec(candidate);
24117
+ return uuidMatch ? [uuidMatch[1].toLowerCase()] : [];
24118
+ })[0];
24119
+ if (matched) {
24120
+ resumeIds.push(matched);
24121
+ }
24122
+ }
24123
+ const sourceNativeThreadId = resumeIds.at(-1);
24124
+ if (!sourceNativeThreadId) {
24125
+ return undefined;
24126
+ }
24127
+ return {
24128
+ sourceNativeThreadId,
24129
+ fingerprint: terminalActionFingerprint({
24130
+ kind: "codex_latent_clear_resume_hint",
24131
+ behavior_profile: behaviorProfile,
24132
+ source_native_thread_id: sourceNativeThreadId
24133
+ })
24134
+ };
24135
+ }
23112
24136
  function nativeInspectionComposerEmpty(agent, screen) {
23113
24137
  return agent === "codex"
23114
24138
  ? codexComposerEmpty(screen)