@scotthuang/agent-knock-knock 0.12.7 → 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 +17 -0
- package/README.md +7 -7
- package/dist/src/cli-core.js +491 -144
- package/dist/src/cli-core.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
|
@@ -1425,7 +1425,7 @@ function isTerminalControlCapability(value) {
|
|
|
1425
1425
|
}
|
|
1426
1426
|
function assertSafeAbortedTerminalRetryBinding({ owner, receipt, storeDir, terminalControl, messageId }) {
|
|
1427
1427
|
if (!(receipt.status === "aborted" && receipt.safe_to_retry === true)) {
|
|
1428
|
-
return;
|
|
1428
|
+
return undefined;
|
|
1429
1429
|
}
|
|
1430
1430
|
const sessionId = sessionIdForConversation(owner);
|
|
1431
1431
|
const managedSession = tryLoadManagedSession(storeDir, sessionId);
|
|
@@ -1441,26 +1441,192 @@ function assertSafeAbortedTerminalRetryBinding({ owner, receipt, storeDir, termi
|
|
|
1441
1441
|
stringValue(ownerTakeover?.terminal_agent_session_id) ??
|
|
1442
1442
|
stringValue(ownerTakeover?.terminal_agent_expected_session_id);
|
|
1443
1443
|
const ownerAgentPid = Number(ownerTakeover?.terminal_agent_pid);
|
|
1444
|
-
if (
|
|
1445
|
-
managedSession.status
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
receiptBindingId
|
|
1451
|
-
receiptBindingGeneration
|
|
1452
|
-
receiptNativeThreadId
|
|
1453
|
-
binding.binding_id
|
|
1454
|
-
binding.generation
|
|
1455
|
-
binding.native_thread_id
|
|
1456
|
-
|
|
1457
|
-
binding.native_process.pid
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1444
|
+
if (managedSession &&
|
|
1445
|
+
managedSession.status === "bound" &&
|
|
1446
|
+
binding &&
|
|
1447
|
+
receiptBindingId &&
|
|
1448
|
+
Number.isSafeInteger(receiptBindingGeneration) &&
|
|
1449
|
+
receiptNativeThreadId &&
|
|
1450
|
+
receiptBindingId === stringValue(owner.terminal_binding_id) &&
|
|
1451
|
+
receiptBindingGeneration === Number(owner.terminal_binding_generation) &&
|
|
1452
|
+
receiptNativeThreadId === ownerNativeThreadId &&
|
|
1453
|
+
binding.binding_id === receiptBindingId &&
|
|
1454
|
+
binding.generation === receiptBindingGeneration &&
|
|
1455
|
+
binding.native_thread_id === receiptNativeThreadId &&
|
|
1456
|
+
Number.isSafeInteger(ownerAgentPid) &&
|
|
1457
|
+
binding.native_process.pid === ownerAgentPid &&
|
|
1458
|
+
ownerControl &&
|
|
1459
|
+
terminalControlsShareIncarnation(ownerControl, terminalControl) &&
|
|
1460
|
+
terminalControlsShareIncarnation(binding.terminal_control, terminalControl)) {
|
|
1461
|
+
return managedSession;
|
|
1462
|
+
}
|
|
1463
|
+
const recoveredSource = safeAbortedDeferredRetrySourceSession({
|
|
1464
|
+
owner,
|
|
1465
|
+
receipt,
|
|
1466
|
+
storeDir,
|
|
1467
|
+
terminalControl,
|
|
1468
|
+
messageId
|
|
1469
|
+
});
|
|
1470
|
+
if (!recoveredSource) {
|
|
1461
1471
|
throw new Error(`terminal idempotency key ${messageId} belongs to a safe-aborted Turn ` +
|
|
1462
1472
|
"whose Session binding is no longer current; no terminal input was sent");
|
|
1463
1473
|
}
|
|
1474
|
+
return recoveredSource;
|
|
1475
|
+
}
|
|
1476
|
+
function safeAbortedDeferredRetrySourceSession({ owner, receipt, storeDir, terminalControl, messageId }) {
|
|
1477
|
+
const takeover = isRecord(owner.native_session_takeover)
|
|
1478
|
+
? owner.native_session_takeover
|
|
1479
|
+
: undefined;
|
|
1480
|
+
const transferId = stringValue(takeover?.deferred_foreground_transfer_id);
|
|
1481
|
+
const ownerControl = terminalControlFromTakeover(takeover);
|
|
1482
|
+
if (!transferId || !takeover || !ownerControl) {
|
|
1483
|
+
return undefined;
|
|
1484
|
+
}
|
|
1485
|
+
const transfer = loadDeferredForegroundTransfer(storeDir, transferId);
|
|
1486
|
+
const target = tryLoadManagedSession(storeDir, transfer.target_session_id);
|
|
1487
|
+
const source = tryLoadManagedSession(storeDir, transfer.source_session_id);
|
|
1488
|
+
const submission = terminalBridgeSubmission(owner);
|
|
1489
|
+
const matchingReceipts = terminalBridgeSubmissionReceipts(owner).filter((candidate) => stringValue(candidate.message_id) === messageId);
|
|
1490
|
+
const canonical = pathsForConversation(owner.conversation_id, storeDir);
|
|
1491
|
+
const targetBinding = transfer.abort_target_after_binding;
|
|
1492
|
+
const sourceBinding = transfer.abort_source_after_binding;
|
|
1493
|
+
const transferDispatchStartedAt = stringValue(transfer.dispatch_started_at);
|
|
1494
|
+
const terminalInputNotStartedAt = stringValue(transfer.terminal_input_not_started_at);
|
|
1495
|
+
const abortedBeforeDispatchIntent = transfer.input_stage === "none" &&
|
|
1496
|
+
transferDispatchStartedAt === undefined &&
|
|
1497
|
+
terminalInputNotStartedAt === undefined;
|
|
1498
|
+
const dispatchIntentProvedNotStarted = transfer.input_stage === "dispatch_started" &&
|
|
1499
|
+
transferDispatchStartedAt !== undefined &&
|
|
1500
|
+
terminalInputNotStartedAt !== undefined &&
|
|
1501
|
+
validTimestampMs(transferDispatchStartedAt) &&
|
|
1502
|
+
validTimestampMs(terminalInputNotStartedAt) &&
|
|
1503
|
+
Date.parse(terminalInputNotStartedAt) >=
|
|
1504
|
+
Date.parse(transferDispatchStartedAt);
|
|
1505
|
+
const forbiddenInputEvidence = [
|
|
1506
|
+
"text_injected_at",
|
|
1507
|
+
"enter_dispatched_at",
|
|
1508
|
+
"submitted_at",
|
|
1509
|
+
"agent_accepted_at",
|
|
1510
|
+
"not_accepted_at",
|
|
1511
|
+
"uncertain_at",
|
|
1512
|
+
"acceptance_evidence"
|
|
1513
|
+
];
|
|
1514
|
+
if (transfer.version !== 2 ||
|
|
1515
|
+
transfer.status !== "abort_resolved" ||
|
|
1516
|
+
(!abortedBeforeDispatchIntent && !dispatchIntentProvedNotStarted) ||
|
|
1517
|
+
transfer.text_injected_at !== undefined ||
|
|
1518
|
+
transfer.enter_dispatched_at !== undefined ||
|
|
1519
|
+
transfer.agent_accepted_at !== undefined ||
|
|
1520
|
+
transfer.target_session_id !== sessionIdForConversation(owner) ||
|
|
1521
|
+
transfer.turn_id !== turnIdForConversation(owner) ||
|
|
1522
|
+
transfer.turn_id !== owner.conversation_id ||
|
|
1523
|
+
transfer.message_id !== messageId ||
|
|
1524
|
+
transfer.terminal_id !== stringValue(takeover.native_session_id) ||
|
|
1525
|
+
transfer.process_pid !== Number(takeover.terminal_agent_pid) ||
|
|
1526
|
+
transfer.process_uuid !== stringValue(takeover.terminal_agent_process_uuid) ||
|
|
1527
|
+
transfer.process_birth !== stringValue(takeover.terminal_agent_process_birth) ||
|
|
1528
|
+
path.resolve(transfer.workspace) !== path.resolve(owner.workspace) ||
|
|
1529
|
+
!terminalControlsShareIncarnation(ownerControl, terminalControl) ||
|
|
1530
|
+
!terminalControlEvidenceMatches(transfer.terminal_endpoint, terminalControl) ||
|
|
1531
|
+
owner.status !== "failed" ||
|
|
1532
|
+
isRecord(owner.callback_delivery) ||
|
|
1533
|
+
isRecord(owner.terminal_bridge_completion_claim) ||
|
|
1534
|
+
isRecord(takeover.terminal_bridge_completion_claim) ||
|
|
1535
|
+
stringValue(takeover.terminal_bridge_message_id) !== messageId ||
|
|
1536
|
+
stringValue(takeover.terminal_bridge_request_hash) !==
|
|
1537
|
+
transfer.request_hash ||
|
|
1538
|
+
stringValue(takeover.terminal_bridge_request_hash) !==
|
|
1539
|
+
stringValue(receipt.request_hash) ||
|
|
1540
|
+
stringValue(takeover.deferred_foreground_transfer_id) !==
|
|
1541
|
+
transfer.transfer_id ||
|
|
1542
|
+
path.resolve(stringValue(owner.state_path) ?? "") !==
|
|
1543
|
+
path.resolve(canonical.statePath) ||
|
|
1544
|
+
path.resolve(stringValue(owner.event_log_path) ?? "") !==
|
|
1545
|
+
path.resolve(canonical.logPath) ||
|
|
1546
|
+
path.resolve(managedSessionStoreDirForConversation(owner) ?? "") !==
|
|
1547
|
+
path.resolve(storeDir) ||
|
|
1548
|
+
!submission ||
|
|
1549
|
+
matchingReceipts.length !== 1 ||
|
|
1550
|
+
canonicalJson(matchingReceipts[0]) !== canonicalJson(submission) ||
|
|
1551
|
+
canonicalJson(submission) !== canonicalJson(receipt) ||
|
|
1552
|
+
submission.status !== "aborted" ||
|
|
1553
|
+
submission.safe_to_retry !== true ||
|
|
1554
|
+
stringValue(submission.last_proven_stage) !== "prepared" ||
|
|
1555
|
+
!validTimestampMs(submission.prepared_at) ||
|
|
1556
|
+
!validTimestampMs(submission.aborted_at) ||
|
|
1557
|
+
Date.parse(String(submission.aborted_at)) <
|
|
1558
|
+
Date.parse(String(submission.prepared_at)) ||
|
|
1559
|
+
(dispatchIntentProvedNotStarted &&
|
|
1560
|
+
stringValue(submission.aborted_at) !== terminalInputNotStartedAt) ||
|
|
1561
|
+
forbiddenInputEvidence.some((field) => submission[field] !== undefined) ||
|
|
1562
|
+
stringValue(submission.session_id) !== transfer.target_session_id ||
|
|
1563
|
+
stringValue(submission.turn_id) !== transfer.turn_id ||
|
|
1564
|
+
stringValue(submission.message_id) !== transfer.message_id ||
|
|
1565
|
+
stringValue(submission.request_hash) !== transfer.request_hash ||
|
|
1566
|
+
stringValue(submission.binding_id) !==
|
|
1567
|
+
transfer.target_before_binding?.binding_id ||
|
|
1568
|
+
Number(submission.binding_generation) !==
|
|
1569
|
+
transfer.target_before_binding?.generation ||
|
|
1570
|
+
stringValue(submission.native_thread_id) !== undefined ||
|
|
1571
|
+
!target ||
|
|
1572
|
+
target.status !== "detached" ||
|
|
1573
|
+
target.last_transition_id !== transfer.transfer_id ||
|
|
1574
|
+
target.lineage.transition_id !== transfer.transfer_id ||
|
|
1575
|
+
target.lineage.previous_session_id !== transfer.source_session_id ||
|
|
1576
|
+
transfer.abort_target_after_status !== "detached" ||
|
|
1577
|
+
!targetBinding ||
|
|
1578
|
+
managedSessionRevision(target) !== transfer.abort_target_after_revision ||
|
|
1579
|
+
managedSessionBindingToken(target) !==
|
|
1580
|
+
transfer.abort_target_after_binding_token ||
|
|
1581
|
+
JSON.stringify(target.binding) !== JSON.stringify(targetBinding) ||
|
|
1582
|
+
!source ||
|
|
1583
|
+
source.status !== "bound" ||
|
|
1584
|
+
source.last_transition_id !== transfer.source_previous_last_transition_id ||
|
|
1585
|
+
transfer.abort_source_after_status !== "bound" ||
|
|
1586
|
+
!sourceBinding ||
|
|
1587
|
+
managedSessionRevision(source) !== transfer.abort_source_after_revision ||
|
|
1588
|
+
managedSessionBindingToken(source) !==
|
|
1589
|
+
transfer.abort_source_after_binding_token ||
|
|
1590
|
+
JSON.stringify(source.binding) !== JSON.stringify(sourceBinding) ||
|
|
1591
|
+
JSON.stringify(sourceBinding) !==
|
|
1592
|
+
JSON.stringify(transfer.source_before_binding)) {
|
|
1593
|
+
return undefined;
|
|
1594
|
+
}
|
|
1595
|
+
const ledger = loadTerminalBridgeDispatchLedger(terminalControl);
|
|
1596
|
+
if (!ledger) {
|
|
1597
|
+
return undefined;
|
|
1598
|
+
}
|
|
1599
|
+
assertDeferredForegroundResolvedZeroInputLedger({
|
|
1600
|
+
storeDir,
|
|
1601
|
+
terminal: { terminalControl },
|
|
1602
|
+
transfer,
|
|
1603
|
+
ledger,
|
|
1604
|
+
statePath: canonical.statePath
|
|
1605
|
+
});
|
|
1606
|
+
if (dispatchIntentProvedNotStarted &&
|
|
1607
|
+
stringValue(ledger.aborted_at) !== terminalInputNotStartedAt) {
|
|
1608
|
+
return undefined;
|
|
1609
|
+
}
|
|
1610
|
+
return source;
|
|
1611
|
+
}
|
|
1612
|
+
function exactSafeAbortedRecoveredSessionMatches({ owner, receipt, storeDir, terminalControl, messageId, expectedSessionId }) {
|
|
1613
|
+
const exactReceipt = receipt ?? (() => {
|
|
1614
|
+
const matches = terminalBridgeSubmissionReceipts(owner).filter((candidate) => stringValue(candidate.message_id) === messageId);
|
|
1615
|
+
return matches.length === 1 ? matches[0] : undefined;
|
|
1616
|
+
})();
|
|
1617
|
+
if (!exactReceipt ||
|
|
1618
|
+
exactReceipt.status !== "aborted" ||
|
|
1619
|
+
exactReceipt.safe_to_retry !== true) {
|
|
1620
|
+
return false;
|
|
1621
|
+
}
|
|
1622
|
+
const recoveredSession = assertSafeAbortedTerminalRetryBinding({
|
|
1623
|
+
owner,
|
|
1624
|
+
receipt: exactReceipt,
|
|
1625
|
+
storeDir,
|
|
1626
|
+
terminalControl,
|
|
1627
|
+
messageId
|
|
1628
|
+
});
|
|
1629
|
+
return recoveredSession?.session_id === expectedSessionId;
|
|
1464
1630
|
}
|
|
1465
1631
|
function stableDelegateTerminalRoute({ options, request, workspace, requestedAgent }) {
|
|
1466
1632
|
const messageId = stringValue(options.messageId);
|
|
@@ -1548,23 +1714,38 @@ function stableDelegateTerminalRoute({ options, request, workspace, requestedAge
|
|
|
1548
1714
|
}
|
|
1549
1715
|
if (selected.receipt.status === "aborted" &&
|
|
1550
1716
|
selected.receipt.safe_to_retry === true) {
|
|
1551
|
-
const sessionId = sessionIdForConversation(selected.owner);
|
|
1552
1717
|
const ownerControl = terminalControlFromTakeover(isRecord(selected.owner.native_session_takeover)
|
|
1553
1718
|
? selected.owner.native_session_takeover
|
|
1554
1719
|
: undefined);
|
|
1555
1720
|
if (!ownerControl) {
|
|
1556
1721
|
throw new Error(`terminal idempotency key ${messageId} has no durable terminal route`);
|
|
1557
1722
|
}
|
|
1558
|
-
assertSafeAbortedTerminalRetryBinding({
|
|
1723
|
+
const retrySession = assertSafeAbortedTerminalRetryBinding({
|
|
1559
1724
|
owner: selected.owner,
|
|
1560
1725
|
receipt: selected.receipt,
|
|
1561
1726
|
storeDir,
|
|
1562
1727
|
terminalControl: ownerControl,
|
|
1563
1728
|
messageId
|
|
1564
1729
|
});
|
|
1730
|
+
if (!retrySession) {
|
|
1731
|
+
throw new Error(`terminal idempotency key ${messageId} has no restored retry Session`);
|
|
1732
|
+
}
|
|
1733
|
+
if (retrySession.agent === "codex" &&
|
|
1734
|
+
isCompleteNativeRollout(retrySession.binding?.native_process.rollout)) {
|
|
1735
|
+
// A safe-aborted retry has proved that the original binding is unchanged,
|
|
1736
|
+
// but one open Codex rollout still does not prove the TUI foreground.
|
|
1737
|
+
// Preserve the stable terminal route so runSend captures fresh implicit
|
|
1738
|
+
// candidate authority and retries through the v3 transfer instead of the
|
|
1739
|
+
// forbidden strict Session path.
|
|
1740
|
+
return {
|
|
1741
|
+
kind: "terminal",
|
|
1742
|
+
conversationId: selected.conversationId,
|
|
1743
|
+
workspace: selected.workspace
|
|
1744
|
+
};
|
|
1745
|
+
}
|
|
1565
1746
|
return {
|
|
1566
1747
|
kind: "session",
|
|
1567
|
-
sessionId,
|
|
1748
|
+
sessionId: retrySession.session_id,
|
|
1568
1749
|
workspace: selected.workspace
|
|
1569
1750
|
};
|
|
1570
1751
|
}
|
|
@@ -2103,6 +2284,18 @@ function replayExactActiveTerminalSubmission({ options, terminalControl, request
|
|
|
2103
2284
|
const loadedLedgerOwner = loadedLedger && loadedLedgerMessageId === messageId
|
|
2104
2285
|
? loadTerminalDispatchLedgerOwner(loadedLedger)
|
|
2105
2286
|
: undefined;
|
|
2287
|
+
const loadedLedgerSessionId = stringValue(loadedLedger?.session_id);
|
|
2288
|
+
const loadedLedgerMatchesRecoveredSession = Boolean(expectedSessionId &&
|
|
2289
|
+
loadedLedgerSessionId !== undefined &&
|
|
2290
|
+
loadedLedgerSessionId !== expectedSessionId &&
|
|
2291
|
+
loadedLedgerOwner &&
|
|
2292
|
+
exactSafeAbortedRecoveredSessionMatches({
|
|
2293
|
+
owner: loadedLedgerOwner,
|
|
2294
|
+
storeDir: expectedStoreDir,
|
|
2295
|
+
terminalControl,
|
|
2296
|
+
messageId,
|
|
2297
|
+
expectedSessionId
|
|
2298
|
+
}));
|
|
2106
2299
|
const expectedMessageBodyHash = createHash("sha256")
|
|
2107
2300
|
.update(requestText)
|
|
2108
2301
|
.digest("hex");
|
|
@@ -2136,8 +2329,9 @@ function replayExactActiveTerminalSubmission({ options, terminalControl, request
|
|
|
2136
2329
|
(!terminalDispatchRecordMatchesControl(loadedLedger, terminalControl) ||
|
|
2137
2330
|
(loadedLedgerStoreDir !== undefined &&
|
|
2138
2331
|
path.resolve(loadedLedgerStoreDir) !== path.resolve(expectedStoreDir)) ||
|
|
2139
|
-
(expectedSessionId &&
|
|
2140
|
-
|
|
2332
|
+
(expectedSessionId && loadedLedgerSessionId !== undefined &&
|
|
2333
|
+
loadedLedgerSessionId !== expectedSessionId &&
|
|
2334
|
+
!loadedLedgerMatchesRecoveredSession) ||
|
|
2141
2335
|
(expectedTurnId && stringValue(loadedLedger.turn_id) !== undefined &&
|
|
2142
2336
|
stringValue(loadedLedger.turn_id) !== expectedTurnId) ||
|
|
2143
2337
|
(expectedStatePath && loadedLedgerStatePath !== undefined &&
|
|
@@ -2169,6 +2363,7 @@ function replayExactActiveTerminalSubmission({ options, terminalControl, request
|
|
|
2169
2363
|
return true;
|
|
2170
2364
|
}
|
|
2171
2365
|
if (ledgerReceipt &&
|
|
2366
|
+
!(ledgerReceipt.status === "aborted" && ledgerReceipt.safe_to_retry === true) &&
|
|
2172
2367
|
(loadedLedgerMessageId !== messageId ||
|
|
2173
2368
|
!["submitted", "enter_dispatched", "agent_accepted"].includes(String(ledgerReceipt.status)) ||
|
|
2174
2369
|
!["submitted", "enter_dispatched", "agent_accepted"].includes(String(loadedLedger?.status)))) {
|
|
@@ -2326,6 +2521,16 @@ function validateStoredTerminalSubmissionMatch({ owner, receipt, options, termin
|
|
|
2326
2521
|
const receiptStoreDir = stringValue(receipt.store_dir) ?? ownerStoreDir;
|
|
2327
2522
|
const receiptSessionId = stringValue(receipt.session_id) ??
|
|
2328
2523
|
sessionIdForConversation(owner);
|
|
2524
|
+
const receiptMatchesRecoveredSession = Boolean(expectedSessionId &&
|
|
2525
|
+
receiptSessionId !== expectedSessionId &&
|
|
2526
|
+
exactSafeAbortedRecoveredSessionMatches({
|
|
2527
|
+
owner,
|
|
2528
|
+
receipt,
|
|
2529
|
+
storeDir: expectedStoreDir,
|
|
2530
|
+
terminalControl,
|
|
2531
|
+
messageId,
|
|
2532
|
+
expectedSessionId
|
|
2533
|
+
}));
|
|
2329
2534
|
const receiptTurnId = stringValue(receipt.turn_id) ??
|
|
2330
2535
|
turnIdForConversation(owner);
|
|
2331
2536
|
const receiptOpenClawSession = stringValue(receipt.openclaw_session) ??
|
|
@@ -2352,7 +2557,8 @@ function validateStoredTerminalSubmissionMatch({ owner, receipt, options, termin
|
|
|
2352
2557
|
!terminalControlEvidenceMatches(receiptTerminalEvidence, terminalControl) ||
|
|
2353
2558
|
receiptSessionId !== sessionIdForConversation(owner) ||
|
|
2354
2559
|
receiptTurnId !== turnIdForConversation(owner) ||
|
|
2355
|
-
(expectedSessionId && receiptSessionId !== expectedSessionId
|
|
2560
|
+
(expectedSessionId && receiptSessionId !== expectedSessionId &&
|
|
2561
|
+
!receiptMatchesRecoveredSession) ||
|
|
2356
2562
|
(expectedTurnId && receiptTurnId !== expectedTurnId) ||
|
|
2357
2563
|
(expectedStatePath && !sameCanonicalStatePath(owner.state_path, expectedStatePath)) ||
|
|
2358
2564
|
(requestedOpenClawSession && receiptOpenClawSession !== requestedOpenClawSession) ||
|
|
@@ -2427,13 +2633,28 @@ function replayExactStoredTerminalSubmission({ options, terminalControl, request
|
|
|
2427
2633
|
const match = matches[0];
|
|
2428
2634
|
if (!match) {
|
|
2429
2635
|
for (const { owner, receipt } of validatedMatches) {
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2636
|
+
if (expectedSessionId) {
|
|
2637
|
+
if (!exactSafeAbortedRecoveredSessionMatches({
|
|
2638
|
+
owner,
|
|
2639
|
+
receipt,
|
|
2640
|
+
storeDir: expectedStoreDir,
|
|
2641
|
+
terminalControl,
|
|
2642
|
+
messageId,
|
|
2643
|
+
expectedSessionId
|
|
2644
|
+
})) {
|
|
2645
|
+
throw new Error(`terminal idempotency key ${messageId} does not match its ` +
|
|
2646
|
+
"restored retry Session; no terminal input was sent");
|
|
2647
|
+
}
|
|
2648
|
+
}
|
|
2649
|
+
else {
|
|
2650
|
+
assertSafeAbortedTerminalRetryBinding({
|
|
2651
|
+
owner,
|
|
2652
|
+
receipt,
|
|
2653
|
+
storeDir: expectedStoreDir,
|
|
2654
|
+
terminalControl,
|
|
2655
|
+
messageId
|
|
2656
|
+
});
|
|
2657
|
+
}
|
|
2437
2658
|
}
|
|
2438
2659
|
// A prepared-stage failure with a restored ledger proves that tmux was
|
|
2439
2660
|
// untouched. The same exact id may therefore start a fresh attempt; all
|
|
@@ -3771,6 +3992,17 @@ function terminalFirstListProjection({ storeDir, terminals, managedSessions, ses
|
|
|
3771
3992
|
stringValue(codexLatentClearResumeValue.source_native_thread_id)?.toLowerCase() === deferredCodexSourceNativeThreadId
|
|
3772
3993
|
? stringValue(codexLatentClearResumeValue.fingerprint)
|
|
3773
3994
|
: undefined;
|
|
3995
|
+
if (deferredCodexLatentClearResumeFingerprint) {
|
|
3996
|
+
// The resume hint is useful operational context, but it is not durable
|
|
3997
|
+
// foreground authority: it can scroll away while the latent thread is
|
|
3998
|
+
// still current. Candidate routing and its token rely on the complete
|
|
3999
|
+
// rollout inventory and Store authority below instead.
|
|
4000
|
+
runtimeLog("info", "terminal_codex_latent_clear_hint_observed", {
|
|
4001
|
+
terminal_id: String(terminal.id),
|
|
4002
|
+
source_session_id: deferredCodexSource?.session_id,
|
|
4003
|
+
source_native_thread_id: deferredCodexSourceNativeThreadId
|
|
4004
|
+
});
|
|
4005
|
+
}
|
|
3774
4006
|
const deferredCodexSourceActiveElsewhere = Boolean(deferredCodexSourceNativeThreadId &&
|
|
3775
4007
|
terminals.some((candidate) => candidate !== terminal &&
|
|
3776
4008
|
candidate.agent === "codex" &&
|
|
@@ -3848,11 +4080,12 @@ function terminalFirstListProjection({ storeDir, terminals, managedSessions, ses
|
|
|
3848
4080
|
"explicitly_abandoned_predecessor"
|
|
3849
4081
|
}) &&
|
|
3850
4082
|
candidateSourceTransitionHistoryIsTerminal(storeDir, deferredCodexSource));
|
|
3851
|
-
|
|
3852
|
-
|
|
3853
|
-
|
|
3854
|
-
|
|
3855
|
-
|
|
4083
|
+
// A complete one-root inventory proves only which rollout files are
|
|
4084
|
+
// materialized, not which logical thread the Codex TUI currently owns.
|
|
4085
|
+
// `/clear` can select a new thread before its rollout exists, so every
|
|
4086
|
+
// otherwise eligible nonempty inventory uses the same post-submission
|
|
4087
|
+
// candidate-set attribution as a multi-root inventory.
|
|
4088
|
+
const deferredCodexCandidateRouteNeeded = deferredCodexCandidateInventory !== undefined;
|
|
3856
4089
|
const deferredCodexForegroundEligible = Boolean(mutationsAllowed &&
|
|
3857
4090
|
!terminalHasNonterminalDeferredTransfer &&
|
|
3858
4091
|
deferredCodexSource &&
|
|
@@ -3899,7 +4132,6 @@ function terminalFirstListProjection({ storeDir, terminals, managedSessions, ses
|
|
|
3899
4132
|
sourceSession: deferredCodexSource,
|
|
3900
4133
|
dispatchSnapshot: deferredCodexDispatchSnapshot,
|
|
3901
4134
|
sourceTurnHistory: deferredCodexCandidateSourceTurnHistory,
|
|
3902
|
-
candidateContextFingerprint: deferredCodexLatentClearResumeFingerprint,
|
|
3903
4135
|
sourceRolloutAuthority: deferredCodexSourceRolloutAuthority,
|
|
3904
4136
|
sourceAbandonmentFingerprint: deferredCodexSourceAbandonmentFingerprint,
|
|
3905
4137
|
...(deferredCodexCandidateInventory
|
|
@@ -4066,7 +4298,11 @@ function terminalFirstListProjection({ storeDir, terminals, managedSessions, ses
|
|
|
4066
4298
|
});
|
|
4067
4299
|
}
|
|
4068
4300
|
}
|
|
4069
|
-
const
|
|
4301
|
+
const rolloutBackedCodexSession = Boolean(authoritativeSession?.agent === "codex" &&
|
|
4302
|
+
isCompleteNativeRollout(authoritativeSession.binding?.native_process.rollout));
|
|
4303
|
+
const terminalCanAcceptSend = ownership.state === "none" &&
|
|
4304
|
+
Boolean(deferredCodexForegroundToken ||
|
|
4305
|
+
(!rolloutBackedCodexSession && isRecord(sessionAwareRawActions.send)));
|
|
4070
4306
|
if (ownership.state === "current" &&
|
|
4071
4307
|
!allRelated.some((conversation) => conversation.conversation_id === ownership.conversation.conversation_id)) {
|
|
4072
4308
|
allRelated.push(ownership.conversation);
|
|
@@ -4111,7 +4347,8 @@ function terminalFirstListProjection({ storeDir, terminals, managedSessions, ses
|
|
|
4111
4347
|
? withoutGenericHandoffSourceClose(currentTurnProjection, blockingHandoffTurnIds)
|
|
4112
4348
|
: undefined;
|
|
4113
4349
|
const nonOwnerRawActions = authoritativeSession
|
|
4114
|
-
? Object.fromEntries(Object.entries(sessionAwareRawActions).filter(([actionName]) => actionName !== "approve"
|
|
4350
|
+
? Object.fromEntries(Object.entries(sessionAwareRawActions).filter(([actionName]) => actionName !== "approve" &&
|
|
4351
|
+
!(rolloutBackedCodexSession && actionName === "send")))
|
|
4115
4352
|
: sessionAwareRawActions;
|
|
4116
4353
|
const sortedDisplayed = [...sessionDisplayedRelated]
|
|
4117
4354
|
.filter((conversation) => conversation.conversation_id !== currentTurn?.conversation_id)
|
|
@@ -4229,6 +4466,7 @@ function terminalFirstListProjection({ storeDir, terminals, managedSessions, ses
|
|
|
4229
4466
|
}
|
|
4230
4467
|
}
|
|
4231
4468
|
: managedSessionId &&
|
|
4469
|
+
!rolloutBackedCodexSession &&
|
|
4232
4470
|
sessionBindingMatchesLiveTerminal &&
|
|
4233
4471
|
isRecord(sessionAwareRawActions.send)
|
|
4234
4472
|
? {
|
|
@@ -5615,13 +5853,14 @@ function managedListApprovalState(conversation) {
|
|
|
5615
5853
|
}
|
|
5616
5854
|
function listActionContracts() {
|
|
5617
5855
|
return {
|
|
5618
|
-
version:
|
|
5856
|
+
version: 15,
|
|
5619
5857
|
instructions: [
|
|
5620
5858
|
"Treat terminals[] as the primary resource and use only actions present in available_actions, except the snapshot-bound terminals[].handoff_decision.choices.take_over_current.action and an exact terminals[].blocking_turns[].recovery_action. Either nested action requires explicit user confirmation; after it succeeds, refresh list before any follow-current send.",
|
|
5621
|
-
"
|
|
5859
|
+
"The session_exact scope uses session_id only when it is prefilled by the listed send action. A rollout-backed managed Codex pane instead uses the terminal_follow_current scope with its exact selector plus expected_terminal_token because even one materialized rollout does not prove the current TUI foreground thread. A turn id is never an ordinary send target.",
|
|
5860
|
+
"A user-explicit raw terminal selector, or a uniquely delegated raw send with no selector, may omit expected_terminal_token for convenience. If that terminal already has one rollout-backed managed Codex source, AKK captures an equivalent fresh candidate authority under the terminal and Store locks and still uses the same v3 follow-current transfer; it never degrades to sole-root strict continuation. Unmanaged first attach retains its existing behavior.",
|
|
5622
5861
|
"Read-only native-thread listing targets an exact terminal_id. Native-thread new/resume mutations also use the listed expected_binding_token and never create a Turn.",
|
|
5623
5862
|
"Native inspection is a separate terminal action: use only its closed inspection enum and current exact terminal_id/token; AKK status does not execute a native slash command.",
|
|
5624
|
-
"A verified, idle human native-thread switch may expose a terminal-scoped send with expected_terminal_token; that action atomically adopts the live context before creating its Turn. A conclusively ended Codex rollout may expose the same snapshot-bound send only after AKK proves zero current rollout and an exact empty composer; it detaches the ended Session and creates an isolated virgin Session. A status-card-only zero-rollout source
|
|
5863
|
+
"A verified, idle human native-thread switch may expose a terminal-scoped send with expected_terminal_token; that action atomically adopts the live context before creating its Turn. A conclusively ended Codex rollout may expose the same snapshot-bound send only after AKK proves zero current rollout and an exact empty composer; it detaches the ended Session and creates an isolated virgin Session. A status-card-only zero-rollout source or any otherwise eligible quiescent rollout-backed source with a complete nonempty pinned open-rollout inventory may also expose this exact action. One materialized rollout does not prove the current Codex TUI foreground thread, and a /clear resume hint is diagnostic only. AKK freezes any released predecessor Turn history, submits the ordinary task once, and binds a separate provisional Session only after one post-anchor rollout uniquely accepts that exact request. The accepted UUID may equal or differ from the predecessor without merging their Session lineages, and narrow panes do not require /status. Until that promotion commits, strict session_id send, respond, approve, cancel, native lifecycle, and native_inspect remain unavailable, and the provisional binding has no callback authority. If dispatch, acceptance, or post-submit binding is uncertain, do not retry automatically. An explicitly closed uncertain Turn may authorize only a future candidate send when its exact resolved close ledger, append-only uncertain receipt, frozen predecessor history, and complete current rollout inventory prove that the old bound rollout is absent and every candidate is unclaimed; close never forges the lost callback, and uncertain submissions cannot be renewed. Other binding conflicts remain fail-closed and may expose only exact low-level reconcile_binding recovery.",
|
|
5625
5864
|
"List resumable threads before resume; use only a complete native_thread_id and the action returned for that candidate.",
|
|
5626
5865
|
"Use a terminal selector only when explicitly named by the user or prefilled by that terminal row's send action. A handoff action also carries expected_terminal_token; never infer, guess, or reuse either value.",
|
|
5627
5866
|
"Use respond only for an in-flight turn that is explicitly waiting for OpenClaw.",
|
|
@@ -5646,7 +5885,7 @@ function listActionContracts() {
|
|
|
5646
5885
|
managed: {
|
|
5647
5886
|
current_turn: "the authoritative dispatch-ledger owner, never inferred from history",
|
|
5648
5887
|
recent_turn: "the latest visible non-owning turn in the current managed session",
|
|
5649
|
-
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",
|
|
5650
5889
|
binding_id: "the immutable terminal-binding generation currently authorized for this Session",
|
|
5651
5890
|
binding_token: "an optimistic concurrency token required by native-thread mutations",
|
|
5652
5891
|
history: "older turns, present only with --all"
|
|
@@ -5684,7 +5923,17 @@ function listActionContracts() {
|
|
|
5684
5923
|
tool: "agent_knock_knock_send",
|
|
5685
5924
|
target_argument: "session_id",
|
|
5686
5925
|
initial_attach_target_argument: "selector",
|
|
5687
|
-
|
|
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.",
|
|
5688
5937
|
required: ["request"],
|
|
5689
5938
|
optional: [
|
|
5690
5939
|
"selector",
|
|
@@ -5696,8 +5945,8 @@ function listActionContracts() {
|
|
|
5696
5945
|
],
|
|
5697
5946
|
unsupported: ["timeoutSeconds"],
|
|
5698
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.",
|
|
5699
|
-
candidate_rollout_deferred_scope: "A quiescent rollout-backed Codex source
|
|
5700
|
-
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."
|
|
5701
5950
|
},
|
|
5702
5951
|
new_thread: {
|
|
5703
5952
|
tool: "agent_knock_knock_new_thread",
|
|
@@ -7196,6 +7445,91 @@ function codexCandidateInventoryHasNoOtherManagedClaim({ storeDir, inventory, so
|
|
|
7196
7445
|
isExactNativeThreadId(session.binding?.native_thread_id) &&
|
|
7197
7446
|
candidateIds.has(session.binding.native_thread_id.toLowerCase()));
|
|
7198
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
|
+
}
|
|
7199
7533
|
function deferredCandidateSourceTurnHistory(storeDir, session) {
|
|
7200
7534
|
const binding = session.binding;
|
|
7201
7535
|
if (!binding?.native_thread_id) {
|
|
@@ -7210,13 +7544,19 @@ function deferredCandidateSourceTurnHistory(storeDir, session) {
|
|
|
7210
7544
|
session,
|
|
7211
7545
|
turn
|
|
7212
7546
|
});
|
|
7547
|
+
const safelyAbortedBeforeInput = exactReleasedSafeAbortedCandidateTurn({
|
|
7548
|
+
storeDir,
|
|
7549
|
+
session,
|
|
7550
|
+
turn
|
|
7551
|
+
});
|
|
7213
7552
|
return (!TERMINAL_DISPATCH_RELEASE_STATUSES.has(turn.status) ||
|
|
7214
7553
|
managedTurnNeedsAttention(turn) ||
|
|
7215
7554
|
(isRecord(turn.callback_delivery) &&
|
|
7216
7555
|
!callbackDelivered) ||
|
|
7217
7556
|
(Boolean(turn.gateway_method) &&
|
|
7218
7557
|
!callbackDelivered &&
|
|
7219
|
-
!explicitlyAbandonedUncertain
|
|
7558
|
+
!explicitlyAbandonedUncertain &&
|
|
7559
|
+
!safelyAbortedBeforeInput) ||
|
|
7220
7560
|
stringValue(turn.terminal_binding_id) !== binding.binding_id ||
|
|
7221
7561
|
Number(turn.terminal_binding_generation) !== binding.generation ||
|
|
7222
7562
|
(stringValue(turn.native_thread_id) ??
|
|
@@ -7497,7 +7837,7 @@ function candidateSourceTransitionHistoryIsTerminal(storeDir, session) {
|
|
|
7497
7837
|
return false;
|
|
7498
7838
|
}
|
|
7499
7839
|
}
|
|
7500
|
-
function deferredCodexForegroundBindingToken({ terminalId, terminalControl, pid, workspace, processUuid, processBirth, sourceSession, dispatchSnapshot, candidateInventory, sourceTurnHistory,
|
|
7840
|
+
function deferredCodexForegroundBindingToken({ terminalId, terminalControl, pid, workspace, processUuid, processBirth, sourceSession, dispatchSnapshot, candidateInventory, sourceTurnHistory, sourceRolloutAuthority = "present", sourceAbandonmentFingerprint }) {
|
|
7501
7841
|
const terminalToken = unmanagedTerminalBindingToken({
|
|
7502
7842
|
terminalId,
|
|
7503
7843
|
terminalControl,
|
|
@@ -7530,17 +7870,12 @@ function deferredCodexForegroundBindingToken({ terminalId, terminalControl, pid,
|
|
|
7530
7870
|
source_revision: managedSessionRevision(sourceSession),
|
|
7531
7871
|
source_binding_token: managedSessionBindingToken(sourceSession),
|
|
7532
7872
|
terminal_dispatch_snapshot: dispatchSnapshot,
|
|
7533
|
-
observation:
|
|
7534
|
-
? "
|
|
7535
|
-
:
|
|
7536
|
-
? "exact_open_root_inventory"
|
|
7537
|
-
: "verified_absent",
|
|
7873
|
+
observation: candidateAuthority
|
|
7874
|
+
? "exact_open_root_inventory"
|
|
7875
|
+
: "verified_absent",
|
|
7538
7876
|
...(sourceTurnHistoryFingerprint
|
|
7539
7877
|
? { source_turn_history_fingerprint: sourceTurnHistoryFingerprint }
|
|
7540
7878
|
: {}),
|
|
7541
|
-
...(candidateContextFingerprint
|
|
7542
|
-
? { candidate_context_fingerprint: candidateContextFingerprint }
|
|
7543
|
-
: {}),
|
|
7544
7879
|
...(candidateAuthority
|
|
7545
7880
|
? { source_rollout_authority: sourceRolloutAuthority }
|
|
7546
7881
|
: {}),
|
|
@@ -7978,16 +8313,6 @@ async function assertDeferredCodexForegroundBindingBoundary({ options, boundary,
|
|
|
7978
8313
|
physicalOnly: boundary.candidateAcceptanceAnchor !== undefined
|
|
7979
8314
|
})
|
|
7980
8315
|
});
|
|
7981
|
-
if (requireEmptyComposer && boundary.candidateContextFingerprint) {
|
|
7982
|
-
const currentContextFingerprint = codexLatentClearResumeFingerprint({
|
|
7983
|
-
screen: status.screen.excerpt,
|
|
7984
|
-
sourceNativeThreadId: source.binding?.native_thread_id,
|
|
7985
|
-
agentVersion: agentVersionForRunningProcess("codex", boundary.terminal.pid, options)
|
|
7986
|
-
});
|
|
7987
|
-
if (currentContextFingerprint !== boundary.candidateContextFingerprint) {
|
|
7988
|
-
throw new Error("the Codex /clear foreground hint changed; refresh AKK list before sending");
|
|
7989
|
-
}
|
|
7990
|
-
}
|
|
7991
8316
|
if (status.reachable !== true ||
|
|
7992
8317
|
status.approval_state.blocked === true ||
|
|
7993
8318
|
!["idle", "unknown"].includes(status.activity_state)) {
|
|
@@ -8003,15 +8328,22 @@ async function assertDeferredCodexForegroundBindingBoundary({ options, boundary,
|
|
|
8003
8328
|
}
|
|
8004
8329
|
return source;
|
|
8005
8330
|
}
|
|
8006
|
-
async function prepareDeferredCodexForegroundBinding({ options, terminal, sourceSession, observation, candidateInventory, requestText }) {
|
|
8331
|
+
async function prepareDeferredCodexForegroundBinding({ options, terminal, sourceSession, observation, candidateInventory, requestText, allowImplicitFreshAuthority = false }) {
|
|
8007
8332
|
const expectedToken = stringValue(options.expectedTerminalToken);
|
|
8008
8333
|
const candidateMode = Boolean(candidateInventory?.roots.length);
|
|
8009
8334
|
if (terminal.agent !== "codex" ||
|
|
8010
8335
|
!sourceSession?.binding ||
|
|
8011
8336
|
(observation.status !== "verified_absent" && !candidateMode) ||
|
|
8012
|
-
!expectedToken) {
|
|
8337
|
+
(!expectedToken && !allowImplicitFreshAuthority)) {
|
|
8013
8338
|
return undefined;
|
|
8014
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
|
+
}
|
|
8015
8347
|
const processUuid = sourceSession.binding.native_process.process_uuid;
|
|
8016
8348
|
const processBirth = sourceSession.binding.native_process.process_birth;
|
|
8017
8349
|
const workspace = terminal.terminalControl.currentPath;
|
|
@@ -8045,14 +8377,6 @@ async function prepareDeferredCodexForegroundBinding({ options, terminal, source
|
|
|
8045
8377
|
dispatchSnapshot
|
|
8046
8378
|
})
|
|
8047
8379
|
: undefined;
|
|
8048
|
-
const candidateContextFingerprint = candidateMode &&
|
|
8049
|
-
sourceKind === "candidate_rollout_quiescent"
|
|
8050
|
-
? await observeCodexLatentClearResumeFingerprint({
|
|
8051
|
-
options,
|
|
8052
|
-
terminal,
|
|
8053
|
-
sourceNativeThreadId: sourceSession.binding.native_thread_id
|
|
8054
|
-
})
|
|
8055
|
-
: undefined;
|
|
8056
8380
|
const exactSource = !workspace
|
|
8057
8381
|
? false
|
|
8058
8382
|
: sourceKind === "candidate_rollout_quiescent"
|
|
@@ -8103,15 +8427,26 @@ async function prepareDeferredCodexForegroundBinding({ options, terminal, source
|
|
|
8103
8427
|
sourceSession,
|
|
8104
8428
|
dispatchSnapshot,
|
|
8105
8429
|
sourceTurnHistory: candidateSourceTurnHistory,
|
|
8106
|
-
candidateContextFingerprint,
|
|
8107
8430
|
sourceRolloutAuthority,
|
|
8108
8431
|
sourceAbandonmentFingerprint,
|
|
8109
8432
|
...(candidateMode ? { candidateInventory } : {})
|
|
8110
8433
|
});
|
|
8111
|
-
if (expectedToken !== token) {
|
|
8434
|
+
if (!allowImplicitFreshAuthority && expectedToken !== token) {
|
|
8112
8435
|
throw new Error("deferred Codex foreground binding requires the fresh exact terminal " +
|
|
8113
8436
|
"token advertised by AKK list");
|
|
8114
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
|
+
}
|
|
8115
8450
|
const targetSessionId = createManagedSessionId();
|
|
8116
8451
|
const transferId = createDeferredForegroundTransferId();
|
|
8117
8452
|
const candidateAcceptanceAnchor = candidateMode
|
|
@@ -8135,9 +8470,6 @@ async function prepareDeferredCodexForegroundBinding({ options, terminal, source
|
|
|
8135
8470
|
...(sourceAbandonmentFingerprint
|
|
8136
8471
|
? { sourceAbandonmentFingerprint }
|
|
8137
8472
|
: {}),
|
|
8138
|
-
...(candidateContextFingerprint
|
|
8139
|
-
? { candidateContextFingerprint }
|
|
8140
|
-
: {}),
|
|
8141
8473
|
...(sourceSession.last_transition_id
|
|
8142
8474
|
? { sourcePreviousLastTransitionId: sourceSession.last_transition_id }
|
|
8143
8475
|
: {}),
|
|
@@ -8162,13 +8494,14 @@ async function prepareDeferredCodexForegroundBinding({ options, terminal, source
|
|
|
8162
8494
|
terminalControl: terminal.terminalControl,
|
|
8163
8495
|
excludedManagedSessionId: sourceSession.session_id
|
|
8164
8496
|
});
|
|
8165
|
-
if (
|
|
8497
|
+
if (candidateInventory &&
|
|
8166
8498
|
!codexCandidateInventoryHasNoOtherManagedClaim({
|
|
8167
8499
|
storeDir,
|
|
8168
|
-
inventory:
|
|
8169
|
-
sourceSessionId: sourceSession.session_id
|
|
8500
|
+
inventory: candidateInventory,
|
|
8501
|
+
sourceSessionId: sourceSession.session_id,
|
|
8502
|
+
includeDetached: sourceRolloutAuthority === "explicitly_abandoned_predecessor"
|
|
8170
8503
|
})) {
|
|
8171
|
-
throw new Error("
|
|
8504
|
+
throw new Error("a Codex rollout candidate is already claimed by another Session");
|
|
8172
8505
|
}
|
|
8173
8506
|
if (candidateInventory &&
|
|
8174
8507
|
sourceRolloutAuthority === "explicitly_abandoned_predecessor") {
|
|
@@ -12569,6 +12902,11 @@ async function runSend(options) {
|
|
|
12569
12902
|
}
|
|
12570
12903
|
assertTerminalIncarnationCanStartTurn(rawStoreDir, terminalConversation.terminalControl);
|
|
12571
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));
|
|
12572
12910
|
let knownCodexCompanions = claimedSession
|
|
12573
12911
|
? codexAllowedCompanionSetForManagedSession({
|
|
12574
12912
|
storeDir: rawStoreDir,
|
|
@@ -12588,7 +12926,7 @@ async function runSend(options) {
|
|
|
12588
12926
|
});
|
|
12589
12927
|
let deferredCodexCandidateInventory;
|
|
12590
12928
|
if (terminalConversation.agent === "codex" &&
|
|
12591
|
-
|
|
12929
|
+
(suppliedExpectedTerminalToken || implicitCodexCandidateAuthority)) {
|
|
12592
12930
|
try {
|
|
12593
12931
|
const inventory = await inspectCodexOpenRootRolloutInventory({
|
|
12594
12932
|
options,
|
|
@@ -12600,12 +12938,18 @@ async function runSend(options) {
|
|
|
12600
12938
|
}
|
|
12601
12939
|
}
|
|
12602
12940
|
catch (error) {
|
|
12603
|
-
if (
|
|
12604
|
-
|
|
12605
|
-
|
|
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)}`);
|
|
12606
12945
|
}
|
|
12607
12946
|
}
|
|
12608
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
|
+
}
|
|
12609
12953
|
if (nativeIdentityObservation.status === "unavailable" &&
|
|
12610
12954
|
!deferredCodexCandidateInventory) {
|
|
12611
12955
|
throw new Error(`native ${terminalConversation.agent} identity observation is ` +
|
|
@@ -12614,12 +12958,17 @@ async function runSend(options) {
|
|
|
12614
12958
|
let currentNativeIdentity = nativeIdentityObservation.status === "resolved"
|
|
12615
12959
|
? nativeIdentityObservation.identity
|
|
12616
12960
|
: undefined;
|
|
12617
|
-
|
|
12618
|
-
|
|
12619
|
-
|
|
12620
|
-
|
|
12621
|
-
|
|
12622
|
-
|
|
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
|
+
});
|
|
12623
12972
|
if (verifiedEmptyHandoff) {
|
|
12624
12973
|
// The old rollout is conclusively closed. Never carry it forward as
|
|
12625
12974
|
// a pre-materialization companion for the new virgin Session.
|
|
@@ -12631,7 +12980,13 @@ async function runSend(options) {
|
|
|
12631
12980
|
let handoff = await maybeAdoptObservedExternalThread({
|
|
12632
12981
|
options,
|
|
12633
12982
|
terminal: terminalConversation,
|
|
12634
|
-
|
|
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,
|
|
12635
12990
|
resolvedIdentity: currentNativeIdentity,
|
|
12636
12991
|
storeDir: rawStoreDir
|
|
12637
12992
|
});
|
|
@@ -12648,7 +13003,8 @@ async function runSend(options) {
|
|
|
12648
13003
|
sourceSession: claimedSession,
|
|
12649
13004
|
observation: nativeIdentityObservation,
|
|
12650
13005
|
candidateInventory: deferredCodexCandidateInventory,
|
|
12651
|
-
requestText: String(messageBody)
|
|
13006
|
+
requestText: String(messageBody),
|
|
13007
|
+
allowImplicitFreshAuthority: implicitCodexCandidateAuthority
|
|
12652
13008
|
})
|
|
12653
13009
|
: undefined;
|
|
12654
13010
|
if (deferredCodexForegroundBinding) {
|
|
@@ -12657,7 +13013,7 @@ async function runSend(options) {
|
|
|
12657
13013
|
currentNativeIdentity = undefined;
|
|
12658
13014
|
handoff = { identity: undefined, adopted: false };
|
|
12659
13015
|
}
|
|
12660
|
-
else if (
|
|
13016
|
+
else if ((suppliedExpectedTerminalToken || implicitCodexCandidateAuthority) &&
|
|
12661
13017
|
!verifiedEmptyHandoff &&
|
|
12662
13018
|
!handoff.adopted) {
|
|
12663
13019
|
throw new Error("the expected terminal token no longer authorizes the current " +
|
|
@@ -12925,6 +13281,17 @@ async function runSend(options) {
|
|
|
12925
13281
|
const releaseTerminalLock = acquireTerminalBridgeSendLock(storeDir, resolvedTerminal.terminalControl, { timeoutMs: 30000 });
|
|
12926
13282
|
try {
|
|
12927
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
|
+
}
|
|
12928
13295
|
await recoverLifecycleFenceBeforeMutation({
|
|
12929
13296
|
options,
|
|
12930
13297
|
terminal: resolvedTerminal
|
|
@@ -12995,11 +13362,6 @@ async function runSend(options) {
|
|
|
12995
13362
|
if (!bindingMatchesLiveTerminal(currentSession, resolvedTerminal, lockedNativeIdentity, storeDir)) {
|
|
12996
13363
|
throw new Error("managed session identity changed while waiting to send; refresh list and retry");
|
|
12997
13364
|
}
|
|
12998
|
-
await assertStrictCodexSessionHasNoLatentClear({
|
|
12999
|
-
options,
|
|
13000
|
-
terminal: resolvedTerminal,
|
|
13001
|
-
session: currentSession
|
|
13002
|
-
});
|
|
13003
13365
|
const logicalLockedNativeIdentity = logicalIdentityForManagedSession({
|
|
13004
13366
|
storeDir,
|
|
13005
13367
|
session: currentSession,
|
|
@@ -20851,6 +21213,32 @@ function terminalLedgerReceiptCandidate(ledger) {
|
|
|
20851
21213
|
}
|
|
20852
21214
|
function mergeTerminalLedgerReceipt(previous, next) {
|
|
20853
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
|
+
}
|
|
20854
21242
|
for (const field of TERMINAL_LEDGER_RECEIPT_IMMUTABLE_FIELDS) {
|
|
20855
21243
|
const previousValue = previous[field];
|
|
20856
21244
|
const nextValue = next[field];
|
|
@@ -23745,47 +24133,6 @@ function codexLatentClearResumeObservation({ screen, agentVersion }) {
|
|
|
23745
24133
|
})
|
|
23746
24134
|
};
|
|
23747
24135
|
}
|
|
23748
|
-
function codexLatentClearResumeFingerprint({ screen, sourceNativeThreadId, agentVersion }) {
|
|
23749
|
-
if (!isExactNativeThreadId(sourceNativeThreadId)) {
|
|
23750
|
-
return undefined;
|
|
23751
|
-
}
|
|
23752
|
-
const observation = codexLatentClearResumeObservation({
|
|
23753
|
-
screen,
|
|
23754
|
-
agentVersion
|
|
23755
|
-
});
|
|
23756
|
-
return observation?.sourceNativeThreadId === sourceNativeThreadId.toLowerCase()
|
|
23757
|
-
? observation.fingerprint
|
|
23758
|
-
: undefined;
|
|
23759
|
-
}
|
|
23760
|
-
async function observeCodexLatentClearResumeFingerprint({ options, terminal, sourceNativeThreadId }) {
|
|
23761
|
-
if (terminal.agent !== "codex" || !isExactNativeThreadId(sourceNativeThreadId)) {
|
|
23762
|
-
return undefined;
|
|
23763
|
-
}
|
|
23764
|
-
const agentVersion = agentVersionForRunningProcess("codex", terminal.pid, options);
|
|
23765
|
-
if (!codexLifecycleBehaviorProfile(agentVersion)) {
|
|
23766
|
-
return undefined;
|
|
23767
|
-
}
|
|
23768
|
-
const status = await createTerminalAgentBridge(options).status("codex", terminal.terminalControl, { runtime: terminalRuntimeForLiveIdentity({ terminal, physicalOnly: true }) });
|
|
23769
|
-
return codexLatentClearResumeFingerprint({
|
|
23770
|
-
screen: status.screen.excerpt,
|
|
23771
|
-
sourceNativeThreadId,
|
|
23772
|
-
agentVersion
|
|
23773
|
-
});
|
|
23774
|
-
}
|
|
23775
|
-
async function assertStrictCodexSessionHasNoLatentClear({ options, terminal, session }) {
|
|
23776
|
-
if (session.agent !== "codex") {
|
|
23777
|
-
return;
|
|
23778
|
-
}
|
|
23779
|
-
const fingerprint = await observeCodexLatentClearResumeFingerprint({
|
|
23780
|
-
options,
|
|
23781
|
-
terminal,
|
|
23782
|
-
sourceNativeThreadId: session.binding?.native_thread_id
|
|
23783
|
-
});
|
|
23784
|
-
if (fingerprint) {
|
|
23785
|
-
throw new Error("Codex /clear changed the foreground logical thread; refresh AKK list " +
|
|
23786
|
-
"and use its snapshot-bound follow-current send. No task input was sent.");
|
|
23787
|
-
}
|
|
23788
|
-
}
|
|
23789
24136
|
function nativeInspectionComposerEmpty(agent, screen) {
|
|
23790
24137
|
return agent === "codex"
|
|
23791
24138
|
? codexComposerEmpty(screen)
|