@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.
- package/CHANGELOG.md +34 -0
- package/README.md +7 -7
- package/dist/src/cli-core.js +1136 -112
- package/dist/src/cli-core.js.map +1 -1
- package/dist/src/deferred-foreground-transfer.d.ts +3 -0
- package/dist/src/deferred-foreground-transfer.js +29 -0
- package/dist/src/deferred-foreground-transfer.js.map +1 -1
- package/dist/src/openclaw-plugin.js +6 -6
- package/dist/src/openclaw-plugin.js.map +1 -1
- package/docs/quickstart-tmux.md +1 -1
- package/package.json +1 -1
- package/templates/openclaw-skills/agent-knock-knock/SKILL.md +7 -7
package/dist/src/cli-core.js
CHANGED
|
@@ -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 (
|
|
1444
|
-
managedSession.status
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
receiptBindingId
|
|
1450
|
-
receiptBindingGeneration
|
|
1451
|
-
receiptNativeThreadId
|
|
1452
|
-
binding.binding_id
|
|
1453
|
-
binding.generation
|
|
1454
|
-
binding.native_thread_id
|
|
1455
|
-
|
|
1456
|
-
binding.native_process.pid
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
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 &&
|
|
2139
|
-
|
|
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
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
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
|
|
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 &&
|
|
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
|
-
|
|
3792
|
-
|
|
3793
|
-
|
|
3794
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
...
|
|
4459
|
+
...verifiedEmptyRawSendAction,
|
|
4141
4460
|
arguments: {
|
|
4142
|
-
...(isRecord(
|
|
4143
|
-
?
|
|
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
|
-
|
|
4188
|
-
|
|
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:
|
|
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
|
-
"
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
5608
|
-
ordinary_use: "Create a new managed
|
|
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 &&
|
|
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
|
-
(
|
|
7048
|
-
(sourceNativeThreadId &&
|
|
7049
|
-
|
|
7050
|
-
|
|
7051
|
-
|
|
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
|
-
|
|
7062
|
-
|
|
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
|
-
|
|
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 ?
|
|
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
|
-
? "
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
12057
|
-
|
|
12058
|
-
|
|
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
|
-
|
|
12071
|
-
|
|
12072
|
-
|
|
12073
|
-
|
|
12074
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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 (/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)
|