@scotthuang/agent-knock-knock 0.12.7 → 0.12.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +35 -0
- package/README.md +9 -7
- package/dist/src/claude-terminal-agent-adapter.js +20 -5
- package/dist/src/claude-terminal-agent-adapter.js.map +1 -1
- package/dist/src/cli-core.js +814 -445
- package/dist/src/cli-core.js.map +1 -1
- package/dist/src/codex-terminal-agent-adapter.d.ts +2 -1
- package/dist/src/codex-terminal-agent-adapter.js +134 -38
- package/dist/src/codex-terminal-agent-adapter.js.map +1 -1
- package/dist/src/openclaw-plugin.js +6 -6
- package/dist/src/openclaw-plugin.js.map +1 -1
- package/dist/src/terminal-agent-adapter.d.ts +21 -0
- package/dist/src/terminal-agent-adapter.js +20 -0
- package/dist/src/terminal-agent-adapter.js.map +1 -1
- package/dist/src/terminal-agent-bridge.js +75 -9
- package/dist/src/terminal-agent-bridge.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 +9 -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,16 +5853,18 @@ function managedListApprovalState(conversation) {
|
|
|
5615
5853
|
}
|
|
5616
5854
|
function listActionContracts() {
|
|
5617
5855
|
return {
|
|
5618
|
-
version:
|
|
5856
|
+
version: 16,
|
|
5619
5857
|
instructions: [
|
|
5620
5858
|
"Treat terminals[] as the primary resource and use only actions present in available_actions, except the snapshot-bound terminals[].handoff_decision.choices.take_over_current.action and an exact terminals[].blocking_turns[].recovery_action. Either nested action requires explicit user confirmation; after it succeeds, refresh list before any follow-current send.",
|
|
5621
|
-
"
|
|
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.",
|
|
5867
|
+
"Approval fingerprint v2 binds stable terminal, process, and action fields plus an unredacted exact prompt-region digest. Whole-screen digests and scrollback excerpts are diagnostic only and never authorize approval. A keys-mode prompt without exact prompt-region evidence is not approvable and exposes no approve action.",
|
|
5628
5868
|
"Managed controls target turn_id. A raw terminal may be controlled only through its own list-prefilled conversation_id action. When a Codex managed prompt has no usable AKK Turn owner, list may expose one manual terminal-scoped approve carrying expected_terminal_token; copy the complete action after explicit human confirmation. It does not mutate the Turn or Session binding, has no durable dispatch receipt, and is never eligible for automatic approval. If its result is interrupted, refresh status and inspect the live prompt instead of retrying blindly. Never construct, guess, or reuse either selector or token.",
|
|
5629
5869
|
"Start with the action's prefilled arguments, supply every missing_required field, and consult the top-level action's optional fields only when needed.",
|
|
5630
5870
|
"Authoritative full IDs are prefilled; short_ref is for display and human input.",
|
|
@@ -5646,7 +5886,7 @@ function listActionContracts() {
|
|
|
5646
5886
|
managed: {
|
|
5647
5887
|
current_turn: "the authoritative dispatch-ledger owner, never inferred from history",
|
|
5648
5888
|
recent_turn: "the latest visible non-owning turn in the current managed session",
|
|
5649
|
-
session_id: "the continuing agent context
|
|
5889
|
+
session_id: "the continuing agent context; it is an ordinary-send target only when the listed send action explicitly prefills it, and rollout-backed Codex uses selector/token instead",
|
|
5650
5890
|
binding_id: "the immutable terminal-binding generation currently authorized for this Session",
|
|
5651
5891
|
binding_token: "an optimistic concurrency token required by native-thread mutations",
|
|
5652
5892
|
history: "older turns, present only with --all"
|
|
@@ -5684,7 +5924,17 @@ function listActionContracts() {
|
|
|
5684
5924
|
tool: "agent_knock_knock_send",
|
|
5685
5925
|
target_argument: "session_id",
|
|
5686
5926
|
initial_attach_target_argument: "selector",
|
|
5687
|
-
|
|
5927
|
+
managed_scopes: {
|
|
5928
|
+
session_exact: {
|
|
5929
|
+
target_arguments: ["session_id"],
|
|
5930
|
+
follows_current_terminal: false
|
|
5931
|
+
},
|
|
5932
|
+
terminal_follow_current: {
|
|
5933
|
+
target_arguments: ["selector", "expected_terminal_token"],
|
|
5934
|
+
follows_current_terminal: true
|
|
5935
|
+
}
|
|
5936
|
+
},
|
|
5937
|
+
initial_attach_scope: "A discovery selector explicitly named by the user, the selector prefilled by an unmanaged raw-terminal row's available send action, or an omitted selector delegated only when exactly one eligible raw pane exists; never infer, guess, or reuse a selector or token. For an already managed rollout-backed Codex pane, an explicit or unique raw delegation without a token internally captures fresh v3 candidate authority under lock; it never becomes a strict Session continuation.",
|
|
5688
5938
|
required: ["request"],
|
|
5689
5939
|
optional: [
|
|
5690
5940
|
"selector",
|
|
@@ -5696,8 +5946,8 @@ function listActionContracts() {
|
|
|
5696
5946
|
],
|
|
5697
5947
|
unsupported: ["timeoutSeconds"],
|
|
5698
5948
|
status_card_only_deferred_scope: "A zero-Turn Codex status-card binding has no rollout; only its listed selector/token send creates an isolated provisional Session and binds it after exact request acceptance. Until promotion commits, strict managed controls, native lifecycle, native_inspect, and callback authority remain unavailable; an uncertain dispatch, acceptance, or post-submit binding must not be retried automatically.",
|
|
5699
|
-
candidate_rollout_deferred_scope: "A quiescent rollout-backed Codex source
|
|
5700
|
-
ordinary_use: "Create a new managed
|
|
5949
|
+
candidate_rollout_deferred_scope: "A quiescent rollout-backed Codex source uses a listed selector/token send whenever AKK can pin a complete nonempty candidate inventory. Inventory status resolved means only that one rollout is materialized; it does not prove the current TUI foreground thread. Released predecessor Turn history stays read-only while a separate provisional Session sends once and waits for one unique post-anchor request acceptance. A /clear resume hint is diagnostic only and is not token or routing authority. Same-UUID and different-UUID results keep separate Session lineages; zero, multiple, drifted, or uncertain acceptance is never retried blindly. Explicit close can abandon an uncertain receipt for future-send liveness only while the exact resolved close ledger, append-only receipt, frozen history, absent old rollout, and unclaimed candidate set remain authoritative; it never synthesizes callback delivery.",
|
|
5950
|
+
ordinary_use: "Create a new managed Turn through the exact action listed for the pane. An explicit session_id never follows the pane and is unavailable for rollout-backed Codex Sessions; their listed selector/token action binds only the unique exact rollout that accepts the submitted request. A user-explicit or uniquely delegated raw send without a token receives the same fresh under-lock candidate authority when it resolves to an already managed rollout-backed Codex source. A live terminal selector can also attach an unmanaged pane, adopt one verified human-selected native context, detach a verified-empty Codex source, or replace an eligible status-card/candidate-rollout source."
|
|
5701
5951
|
},
|
|
5702
5952
|
new_thread: {
|
|
5703
5953
|
tool: "agent_knock_knock_new_thread",
|
|
@@ -5774,6 +6024,9 @@ function listActionContracts() {
|
|
|
5774
6024
|
required: ["turn_id", "expected_approval_fingerprint"],
|
|
5775
6025
|
optional: ["expected_terminal_token"],
|
|
5776
6026
|
terminal_scoped_scope: "Only the latest list-prefilled Codex conversation_id/token action may approve a managed current prompt without a usable Turn owner. It remains human-confirmed, revalidates exact terminal/process/Session/dispatch/prompt state before keys, never mutates managed state, has no durable dispatch receipt, cannot be auto-approved, and must not be blindly retried after an interrupted result.",
|
|
6027
|
+
fingerprint_contract: "v2_prompt_region",
|
|
6028
|
+
fingerprint_authority: "Stable terminal, process, and action fields plus the unredacted exact prompt-region digest; whole-screen digests and scrollback excerpts are diagnostic only.",
|
|
6029
|
+
missing_prompt_region_evidence: "not_approvable",
|
|
5777
6030
|
requires_explicit_user_confirmation: true,
|
|
5778
6031
|
requires_fresh_status: true
|
|
5779
6032
|
},
|
|
@@ -7196,6 +7449,91 @@ function codexCandidateInventoryHasNoOtherManagedClaim({ storeDir, inventory, so
|
|
|
7196
7449
|
isExactNativeThreadId(session.binding?.native_thread_id) &&
|
|
7197
7450
|
candidateIds.has(session.binding.native_thread_id.toLowerCase()));
|
|
7198
7451
|
}
|
|
7452
|
+
function exactReleasedSafeAbortedCandidateTurn({ storeDir, session, turn }) {
|
|
7453
|
+
const binding = session.binding;
|
|
7454
|
+
const takeover = isRecord(turn.native_session_takeover)
|
|
7455
|
+
? turn.native_session_takeover
|
|
7456
|
+
: undefined;
|
|
7457
|
+
const submission = terminalBridgeSubmission(turn);
|
|
7458
|
+
const terminalControl = terminalControlFromTakeover(takeover);
|
|
7459
|
+
const messageId = stringValue(submission?.message_id);
|
|
7460
|
+
const preparedAt = stringValue(submission?.prepared_at);
|
|
7461
|
+
const abortedAt = stringValue(submission?.aborted_at);
|
|
7462
|
+
if (session.status !== "bound" ||
|
|
7463
|
+
session.agent !== "codex" ||
|
|
7464
|
+
!binding ||
|
|
7465
|
+
!isExactNativeThreadId(binding.native_thread_id) ||
|
|
7466
|
+
!TERMINAL_DISPATCH_RELEASE_STATUSES.has(turn.status) ||
|
|
7467
|
+
managedTurnNeedsAttention(turn) ||
|
|
7468
|
+
isRecord(turn.callback_delivery) ||
|
|
7469
|
+
isRecord(turn.terminal_bridge_completion_claim) ||
|
|
7470
|
+
!takeover ||
|
|
7471
|
+
isRecord(takeover.terminal_bridge_completion_claim) ||
|
|
7472
|
+
!submission ||
|
|
7473
|
+
!terminalControl ||
|
|
7474
|
+
!messageId ||
|
|
7475
|
+
submission.status !== "aborted" ||
|
|
7476
|
+
submission.safe_to_retry !== true ||
|
|
7477
|
+
stringValue(submission.last_proven_stage) !== "prepared" ||
|
|
7478
|
+
!preparedAt ||
|
|
7479
|
+
!abortedAt ||
|
|
7480
|
+
!validTimestampMs(preparedAt) ||
|
|
7481
|
+
!validTimestampMs(abortedAt) ||
|
|
7482
|
+
Date.parse(abortedAt) < Date.parse(preparedAt) ||
|
|
7483
|
+
[
|
|
7484
|
+
"text_injected_at",
|
|
7485
|
+
"enter_dispatched_at",
|
|
7486
|
+
"submitted_at",
|
|
7487
|
+
"agent_accepted_at",
|
|
7488
|
+
"not_accepted_at",
|
|
7489
|
+
"uncertain_at",
|
|
7490
|
+
"acceptance_evidence"
|
|
7491
|
+
].some((field) => submission[field] !== undefined) ||
|
|
7492
|
+
stringValue(takeover.terminal_bridge_message_id) !== messageId ||
|
|
7493
|
+
stringValue(takeover.terminal_bridge_request_hash) !==
|
|
7494
|
+
stringValue(submission.request_hash) ||
|
|
7495
|
+
terminalBridgeRequestFingerprint(stringValue(takeover.terminal_bridge_request_text) ?? "") !== stringValue(submission.request_hash) ||
|
|
7496
|
+
executorForConversation(turn).kind !== "codex" ||
|
|
7497
|
+
sessionIdForConversation(turn) !== session.session_id ||
|
|
7498
|
+
stringValue(submission.session_id) !== session.session_id ||
|
|
7499
|
+
stringValue(submission.turn_id) !== turnIdForConversation(turn) ||
|
|
7500
|
+
stringValue(submission.executor_kind) !== "codex" ||
|
|
7501
|
+
stringValue(submission.openclaw_session) !== turn.openclaw_session ||
|
|
7502
|
+
stringValue(turn.terminal_binding_id) !== binding.binding_id ||
|
|
7503
|
+
Number(turn.terminal_binding_generation) !== binding.generation ||
|
|
7504
|
+
stringValue(takeover.terminal_binding_id) !== binding.binding_id ||
|
|
7505
|
+
Number(takeover.terminal_binding_generation) !== binding.generation ||
|
|
7506
|
+
stringValue(submission.binding_id) !== binding.binding_id ||
|
|
7507
|
+
Number(submission.binding_generation) !== binding.generation ||
|
|
7508
|
+
stringValue(turn.native_thread_id) !== binding.native_thread_id ||
|
|
7509
|
+
stringValue(takeover.terminal_agent_session_id) !==
|
|
7510
|
+
binding.native_thread_id ||
|
|
7511
|
+
stringValue(submission.native_thread_id) !== binding.native_thread_id ||
|
|
7512
|
+
Number(takeover.terminal_agent_pid) !== binding.native_process.pid ||
|
|
7513
|
+
stringValue(takeover.terminal_agent_process_uuid) !==
|
|
7514
|
+
binding.native_process.process_uuid ||
|
|
7515
|
+
stringValue(takeover.terminal_agent_process_birth) !==
|
|
7516
|
+
binding.native_process.process_birth ||
|
|
7517
|
+
!terminalControlsShareIncarnation(terminalControl, binding.terminal_control) ||
|
|
7518
|
+
!terminalControlEvidenceMatches(submission.terminal_endpoint ?? terminalControl, binding.terminal_control) ||
|
|
7519
|
+
path.resolve(turn.workspace) !== path.resolve(session.workspace)) {
|
|
7520
|
+
return false;
|
|
7521
|
+
}
|
|
7522
|
+
const canonical = pathsForConversation(turn.conversation_id, storeDir);
|
|
7523
|
+
if (path.resolve(stringValue(turn.state_path) ?? "") !==
|
|
7524
|
+
path.resolve(canonical.statePath) ||
|
|
7525
|
+
path.resolve(stringValue(turn.event_log_path) ?? "") !==
|
|
7526
|
+
path.resolve(canonical.logPath) ||
|
|
7527
|
+
path.resolve(managedSessionStoreDirForConversation(turn) ?? "") !==
|
|
7528
|
+
path.resolve(storeDir) ||
|
|
7529
|
+
path.resolve(stringValue(submission.store_dir) ?? "") !==
|
|
7530
|
+
path.resolve(storeDir)) {
|
|
7531
|
+
return false;
|
|
7532
|
+
}
|
|
7533
|
+
const matchingReceipts = terminalBridgeSubmissionReceipts(turn).filter((receipt) => stringValue(receipt.message_id) === messageId);
|
|
7534
|
+
return matchingReceipts.length === 1 &&
|
|
7535
|
+
canonicalJson(matchingReceipts[0]) === canonicalJson(submission);
|
|
7536
|
+
}
|
|
7199
7537
|
function deferredCandidateSourceTurnHistory(storeDir, session) {
|
|
7200
7538
|
const binding = session.binding;
|
|
7201
7539
|
if (!binding?.native_thread_id) {
|
|
@@ -7210,13 +7548,19 @@ function deferredCandidateSourceTurnHistory(storeDir, session) {
|
|
|
7210
7548
|
session,
|
|
7211
7549
|
turn
|
|
7212
7550
|
});
|
|
7551
|
+
const safelyAbortedBeforeInput = exactReleasedSafeAbortedCandidateTurn({
|
|
7552
|
+
storeDir,
|
|
7553
|
+
session,
|
|
7554
|
+
turn
|
|
7555
|
+
});
|
|
7213
7556
|
return (!TERMINAL_DISPATCH_RELEASE_STATUSES.has(turn.status) ||
|
|
7214
7557
|
managedTurnNeedsAttention(turn) ||
|
|
7215
7558
|
(isRecord(turn.callback_delivery) &&
|
|
7216
7559
|
!callbackDelivered) ||
|
|
7217
7560
|
(Boolean(turn.gateway_method) &&
|
|
7218
7561
|
!callbackDelivered &&
|
|
7219
|
-
!explicitlyAbandonedUncertain
|
|
7562
|
+
!explicitlyAbandonedUncertain &&
|
|
7563
|
+
!safelyAbortedBeforeInput) ||
|
|
7220
7564
|
stringValue(turn.terminal_binding_id) !== binding.binding_id ||
|
|
7221
7565
|
Number(turn.terminal_binding_generation) !== binding.generation ||
|
|
7222
7566
|
(stringValue(turn.native_thread_id) ??
|
|
@@ -7497,7 +7841,7 @@ function candidateSourceTransitionHistoryIsTerminal(storeDir, session) {
|
|
|
7497
7841
|
return false;
|
|
7498
7842
|
}
|
|
7499
7843
|
}
|
|
7500
|
-
function deferredCodexForegroundBindingToken({ terminalId, terminalControl, pid, workspace, processUuid, processBirth, sourceSession, dispatchSnapshot, candidateInventory, sourceTurnHistory,
|
|
7844
|
+
function deferredCodexForegroundBindingToken({ terminalId, terminalControl, pid, workspace, processUuid, processBirth, sourceSession, dispatchSnapshot, candidateInventory, sourceTurnHistory, sourceRolloutAuthority = "present", sourceAbandonmentFingerprint }) {
|
|
7501
7845
|
const terminalToken = unmanagedTerminalBindingToken({
|
|
7502
7846
|
terminalId,
|
|
7503
7847
|
terminalControl,
|
|
@@ -7530,17 +7874,12 @@ function deferredCodexForegroundBindingToken({ terminalId, terminalControl, pid,
|
|
|
7530
7874
|
source_revision: managedSessionRevision(sourceSession),
|
|
7531
7875
|
source_binding_token: managedSessionBindingToken(sourceSession),
|
|
7532
7876
|
terminal_dispatch_snapshot: dispatchSnapshot,
|
|
7533
|
-
observation:
|
|
7534
|
-
? "
|
|
7535
|
-
:
|
|
7536
|
-
? "exact_open_root_inventory"
|
|
7537
|
-
: "verified_absent",
|
|
7877
|
+
observation: candidateAuthority
|
|
7878
|
+
? "exact_open_root_inventory"
|
|
7879
|
+
: "verified_absent",
|
|
7538
7880
|
...(sourceTurnHistoryFingerprint
|
|
7539
7881
|
? { source_turn_history_fingerprint: sourceTurnHistoryFingerprint }
|
|
7540
7882
|
: {}),
|
|
7541
|
-
...(candidateContextFingerprint
|
|
7542
|
-
? { candidate_context_fingerprint: candidateContextFingerprint }
|
|
7543
|
-
: {}),
|
|
7544
7883
|
...(candidateAuthority
|
|
7545
7884
|
? { source_rollout_authority: sourceRolloutAuthority }
|
|
7546
7885
|
: {}),
|
|
@@ -7978,16 +8317,6 @@ async function assertDeferredCodexForegroundBindingBoundary({ options, boundary,
|
|
|
7978
8317
|
physicalOnly: boundary.candidateAcceptanceAnchor !== undefined
|
|
7979
8318
|
})
|
|
7980
8319
|
});
|
|
7981
|
-
if (requireEmptyComposer && boundary.candidateContextFingerprint) {
|
|
7982
|
-
const currentContextFingerprint = codexLatentClearResumeFingerprint({
|
|
7983
|
-
screen: status.screen.excerpt,
|
|
7984
|
-
sourceNativeThreadId: source.binding?.native_thread_id,
|
|
7985
|
-
agentVersion: agentVersionForRunningProcess("codex", boundary.terminal.pid, options)
|
|
7986
|
-
});
|
|
7987
|
-
if (currentContextFingerprint !== boundary.candidateContextFingerprint) {
|
|
7988
|
-
throw new Error("the Codex /clear foreground hint changed; refresh AKK list before sending");
|
|
7989
|
-
}
|
|
7990
|
-
}
|
|
7991
8320
|
if (status.reachable !== true ||
|
|
7992
8321
|
status.approval_state.blocked === true ||
|
|
7993
8322
|
!["idle", "unknown"].includes(status.activity_state)) {
|
|
@@ -8003,15 +8332,22 @@ async function assertDeferredCodexForegroundBindingBoundary({ options, boundary,
|
|
|
8003
8332
|
}
|
|
8004
8333
|
return source;
|
|
8005
8334
|
}
|
|
8006
|
-
async function prepareDeferredCodexForegroundBinding({ options, terminal, sourceSession, observation, candidateInventory, requestText }) {
|
|
8335
|
+
async function prepareDeferredCodexForegroundBinding({ options, terminal, sourceSession, observation, candidateInventory, requestText, allowImplicitFreshAuthority = false }) {
|
|
8007
8336
|
const expectedToken = stringValue(options.expectedTerminalToken);
|
|
8008
8337
|
const candidateMode = Boolean(candidateInventory?.roots.length);
|
|
8009
8338
|
if (terminal.agent !== "codex" ||
|
|
8010
8339
|
!sourceSession?.binding ||
|
|
8011
8340
|
(observation.status !== "verified_absent" && !candidateMode) ||
|
|
8012
|
-
!expectedToken) {
|
|
8341
|
+
(!expectedToken && !allowImplicitFreshAuthority)) {
|
|
8013
8342
|
return undefined;
|
|
8014
8343
|
}
|
|
8344
|
+
if (allowImplicitFreshAuthority &&
|
|
8345
|
+
(expectedToken !== undefined ||
|
|
8346
|
+
!candidateMode ||
|
|
8347
|
+
!isCompleteNativeRollout(sourceSession.binding.native_process.rollout))) {
|
|
8348
|
+
throw new Error("implicit Codex candidate authority requires one fresh complete " +
|
|
8349
|
+
"nonempty rollout inventory for a rollout-backed source");
|
|
8350
|
+
}
|
|
8015
8351
|
const processUuid = sourceSession.binding.native_process.process_uuid;
|
|
8016
8352
|
const processBirth = sourceSession.binding.native_process.process_birth;
|
|
8017
8353
|
const workspace = terminal.terminalControl.currentPath;
|
|
@@ -8045,14 +8381,6 @@ async function prepareDeferredCodexForegroundBinding({ options, terminal, source
|
|
|
8045
8381
|
dispatchSnapshot
|
|
8046
8382
|
})
|
|
8047
8383
|
: undefined;
|
|
8048
|
-
const candidateContextFingerprint = candidateMode &&
|
|
8049
|
-
sourceKind === "candidate_rollout_quiescent"
|
|
8050
|
-
? await observeCodexLatentClearResumeFingerprint({
|
|
8051
|
-
options,
|
|
8052
|
-
terminal,
|
|
8053
|
-
sourceNativeThreadId: sourceSession.binding.native_thread_id
|
|
8054
|
-
})
|
|
8055
|
-
: undefined;
|
|
8056
8384
|
const exactSource = !workspace
|
|
8057
8385
|
? false
|
|
8058
8386
|
: sourceKind === "candidate_rollout_quiescent"
|
|
@@ -8103,15 +8431,26 @@ async function prepareDeferredCodexForegroundBinding({ options, terminal, source
|
|
|
8103
8431
|
sourceSession,
|
|
8104
8432
|
dispatchSnapshot,
|
|
8105
8433
|
sourceTurnHistory: candidateSourceTurnHistory,
|
|
8106
|
-
candidateContextFingerprint,
|
|
8107
8434
|
sourceRolloutAuthority,
|
|
8108
8435
|
sourceAbandonmentFingerprint,
|
|
8109
8436
|
...(candidateMode ? { candidateInventory } : {})
|
|
8110
8437
|
});
|
|
8111
|
-
if (expectedToken !== token) {
|
|
8438
|
+
if (!allowImplicitFreshAuthority && expectedToken !== token) {
|
|
8112
8439
|
throw new Error("deferred Codex foreground binding requires the fresh exact terminal " +
|
|
8113
8440
|
"token advertised by AKK list");
|
|
8114
8441
|
}
|
|
8442
|
+
if (allowImplicitFreshAuthority) {
|
|
8443
|
+
runtimeLog("info", "deferred_codex_implicit_candidate_authority", {
|
|
8444
|
+
terminal_id: terminal.conversationId,
|
|
8445
|
+
terminal_target: terminal.terminalControl.target,
|
|
8446
|
+
source_session_id: sourceSession.session_id,
|
|
8447
|
+
inventory_status: candidateInventory?.status,
|
|
8448
|
+
inventory_fingerprint: candidateInventory?.inventoryFingerprint,
|
|
8449
|
+
candidate_count: candidateInventory?.roots.length,
|
|
8450
|
+
authority_scope: "terminal_follow_current",
|
|
8451
|
+
terminal_input_sent: false
|
|
8452
|
+
});
|
|
8453
|
+
}
|
|
8115
8454
|
const targetSessionId = createManagedSessionId();
|
|
8116
8455
|
const transferId = createDeferredForegroundTransferId();
|
|
8117
8456
|
const candidateAcceptanceAnchor = candidateMode
|
|
@@ -8135,9 +8474,6 @@ async function prepareDeferredCodexForegroundBinding({ options, terminal, source
|
|
|
8135
8474
|
...(sourceAbandonmentFingerprint
|
|
8136
8475
|
? { sourceAbandonmentFingerprint }
|
|
8137
8476
|
: {}),
|
|
8138
|
-
...(candidateContextFingerprint
|
|
8139
|
-
? { candidateContextFingerprint }
|
|
8140
|
-
: {}),
|
|
8141
8477
|
...(sourceSession.last_transition_id
|
|
8142
8478
|
? { sourcePreviousLastTransitionId: sourceSession.last_transition_id }
|
|
8143
8479
|
: {}),
|
|
@@ -8162,13 +8498,14 @@ async function prepareDeferredCodexForegroundBinding({ options, terminal, source
|
|
|
8162
8498
|
terminalControl: terminal.terminalControl,
|
|
8163
8499
|
excludedManagedSessionId: sourceSession.session_id
|
|
8164
8500
|
});
|
|
8165
|
-
if (
|
|
8501
|
+
if (candidateInventory &&
|
|
8166
8502
|
!codexCandidateInventoryHasNoOtherManagedClaim({
|
|
8167
8503
|
storeDir,
|
|
8168
|
-
inventory:
|
|
8169
|
-
sourceSessionId: sourceSession.session_id
|
|
8504
|
+
inventory: candidateInventory,
|
|
8505
|
+
sourceSessionId: sourceSession.session_id,
|
|
8506
|
+
includeDetached: sourceRolloutAuthority === "explicitly_abandoned_predecessor"
|
|
8170
8507
|
})) {
|
|
8171
|
-
throw new Error("
|
|
8508
|
+
throw new Error("a Codex rollout candidate is already claimed by another Session");
|
|
8172
8509
|
}
|
|
8173
8510
|
if (candidateInventory &&
|
|
8174
8511
|
sourceRolloutAuthority === "explicitly_abandoned_predecessor") {
|
|
@@ -12094,6 +12431,10 @@ function terminalBridgeApprovalFingerprint({ terminalControl, terminalStatus })
|
|
|
12094
12431
|
if (adapterFingerprint) {
|
|
12095
12432
|
return adapterFingerprint;
|
|
12096
12433
|
}
|
|
12434
|
+
if (approval.approvable === true &&
|
|
12435
|
+
(stringValue(approval.decision_mode) ?? "keys") === "keys") {
|
|
12436
|
+
return undefined;
|
|
12437
|
+
}
|
|
12097
12438
|
const endpoint = terminalEndpointFromControlRef(terminalControl);
|
|
12098
12439
|
const processAnchorPid = Number(endpoint.processAnchorPid);
|
|
12099
12440
|
if (!Number.isSafeInteger(processAnchorPid) || processAnchorPid <= 1) {
|
|
@@ -12569,6 +12910,11 @@ async function runSend(options) {
|
|
|
12569
12910
|
}
|
|
12570
12911
|
assertTerminalIncarnationCanStartTurn(rawStoreDir, terminalConversation.terminalControl);
|
|
12571
12912
|
let claimedSession = soleBoundManagedSessionClaimForTerminal(rawStoreDir, terminalConversation);
|
|
12913
|
+
const suppliedExpectedTerminalToken = stringValue(options.expectedTerminalToken);
|
|
12914
|
+
const implicitCodexCandidateAuthority = Boolean(terminalConversation.agent === "codex" &&
|
|
12915
|
+
suppliedExpectedTerminalToken === undefined &&
|
|
12916
|
+
claimedSession?.status === "bound" &&
|
|
12917
|
+
isCompleteNativeRollout(claimedSession.binding?.native_process.rollout));
|
|
12572
12918
|
let knownCodexCompanions = claimedSession
|
|
12573
12919
|
? codexAllowedCompanionSetForManagedSession({
|
|
12574
12920
|
storeDir: rawStoreDir,
|
|
@@ -12588,7 +12934,7 @@ async function runSend(options) {
|
|
|
12588
12934
|
});
|
|
12589
12935
|
let deferredCodexCandidateInventory;
|
|
12590
12936
|
if (terminalConversation.agent === "codex" &&
|
|
12591
|
-
|
|
12937
|
+
(suppliedExpectedTerminalToken || implicitCodexCandidateAuthority)) {
|
|
12592
12938
|
try {
|
|
12593
12939
|
const inventory = await inspectCodexOpenRootRolloutInventory({
|
|
12594
12940
|
options,
|
|
@@ -12600,12 +12946,18 @@ async function runSend(options) {
|
|
|
12600
12946
|
}
|
|
12601
12947
|
}
|
|
12602
12948
|
catch (error) {
|
|
12603
|
-
if (
|
|
12604
|
-
|
|
12605
|
-
|
|
12949
|
+
if (implicitCodexCandidateAuthority ||
|
|
12950
|
+
nativeIdentityObservation.status === "unavailable") {
|
|
12951
|
+
throw new Error(`native Codex foreground attribution requires a fresh complete ` +
|
|
12952
|
+
`open-root inventory: ${error instanceof Error ? error.message : String(error)}`);
|
|
12606
12953
|
}
|
|
12607
12954
|
}
|
|
12608
12955
|
}
|
|
12956
|
+
if (implicitCodexCandidateAuthority &&
|
|
12957
|
+
!deferredCodexCandidateInventory) {
|
|
12958
|
+
throw new Error("rollout-backed Codex terminal send requires a fresh complete " +
|
|
12959
|
+
"nonempty open-root inventory; refresh AKK list before sending");
|
|
12960
|
+
}
|
|
12609
12961
|
if (nativeIdentityObservation.status === "unavailable" &&
|
|
12610
12962
|
!deferredCodexCandidateInventory) {
|
|
12611
12963
|
throw new Error(`native ${terminalConversation.agent} identity observation is ` +
|
|
@@ -12614,12 +12966,17 @@ async function runSend(options) {
|
|
|
12614
12966
|
let currentNativeIdentity = nativeIdentityObservation.status === "resolved"
|
|
12615
12967
|
? nativeIdentityObservation.identity
|
|
12616
12968
|
: undefined;
|
|
12617
|
-
|
|
12618
|
-
|
|
12619
|
-
|
|
12620
|
-
|
|
12621
|
-
|
|
12622
|
-
|
|
12969
|
+
// A fresh nonempty inventory is the stronger physical authority for an
|
|
12970
|
+
// implicit candidate send. Do not let an earlier verified-absent
|
|
12971
|
+
// observation divert this path into the token-only empty handoff.
|
|
12972
|
+
const verifiedEmptyHandoff = implicitCodexCandidateAuthority
|
|
12973
|
+
? undefined
|
|
12974
|
+
: await maybeDetachVerifiedEmptyCodexSource({
|
|
12975
|
+
options,
|
|
12976
|
+
terminal: terminalConversation,
|
|
12977
|
+
sourceSession: claimedSession,
|
|
12978
|
+
observation: nativeIdentityObservation
|
|
12979
|
+
});
|
|
12623
12980
|
if (verifiedEmptyHandoff) {
|
|
12624
12981
|
// The old rollout is conclusively closed. Never carry it forward as
|
|
12625
12982
|
// a pre-materialization companion for the new virgin Session.
|
|
@@ -12631,7 +12988,13 @@ async function runSend(options) {
|
|
|
12631
12988
|
let handoff = await maybeAdoptObservedExternalThread({
|
|
12632
12989
|
options,
|
|
12633
12990
|
terminal: terminalConversation,
|
|
12634
|
-
|
|
12991
|
+
// A no-token raw send to an already managed rollout-backed Codex pane
|
|
12992
|
+
// is an internal follow-current delegation, never a sole-root strict
|
|
12993
|
+
// continuation or an external-handoff adoption. The dedicated v3
|
|
12994
|
+
// transfer below owns attribution and lineage.
|
|
12995
|
+
sourceSession: implicitCodexCandidateAuthority
|
|
12996
|
+
? undefined
|
|
12997
|
+
: claimedSession,
|
|
12635
12998
|
resolvedIdentity: currentNativeIdentity,
|
|
12636
12999
|
storeDir: rawStoreDir
|
|
12637
13000
|
});
|
|
@@ -12648,7 +13011,8 @@ async function runSend(options) {
|
|
|
12648
13011
|
sourceSession: claimedSession,
|
|
12649
13012
|
observation: nativeIdentityObservation,
|
|
12650
13013
|
candidateInventory: deferredCodexCandidateInventory,
|
|
12651
|
-
requestText: String(messageBody)
|
|
13014
|
+
requestText: String(messageBody),
|
|
13015
|
+
allowImplicitFreshAuthority: implicitCodexCandidateAuthority
|
|
12652
13016
|
})
|
|
12653
13017
|
: undefined;
|
|
12654
13018
|
if (deferredCodexForegroundBinding) {
|
|
@@ -12657,7 +13021,7 @@ async function runSend(options) {
|
|
|
12657
13021
|
currentNativeIdentity = undefined;
|
|
12658
13022
|
handoff = { identity: undefined, adopted: false };
|
|
12659
13023
|
}
|
|
12660
|
-
else if (
|
|
13024
|
+
else if ((suppliedExpectedTerminalToken || implicitCodexCandidateAuthority) &&
|
|
12661
13025
|
!verifiedEmptyHandoff &&
|
|
12662
13026
|
!handoff.adopted) {
|
|
12663
13027
|
throw new Error("the expected terminal token no longer authorizes the current " +
|
|
@@ -12925,6 +13289,17 @@ async function runSend(options) {
|
|
|
12925
13289
|
const releaseTerminalLock = acquireTerminalBridgeSendLock(storeDir, resolvedTerminal.terminalControl, { timeoutMs: 30000 });
|
|
12926
13290
|
try {
|
|
12927
13291
|
await withStoreWriterLeaseAsync(storeDir, async () => {
|
|
13292
|
+
const lockedStrictSession = tryLoadManagedSession(storeDir, sessionId);
|
|
13293
|
+
if (lockedStrictSession?.agent === "codex" &&
|
|
13294
|
+
lockedStrictSession.session_id === sessionId &&
|
|
13295
|
+
lockedStrictSession.status === "bound" &&
|
|
13296
|
+
isCompleteNativeRollout(lockedStrictSession.binding?.native_process.rollout)) {
|
|
13297
|
+
throw new Error(`Codex rollout-backed managed Session ${sessionId} cannot use a ` +
|
|
13298
|
+
"strict session_id send because an open rollout does not prove the " +
|
|
13299
|
+
"current TUI foreground thread. Refresh AKK list and use its exact " +
|
|
13300
|
+
"selector plus expected_terminal_token. No Turn was created and no " +
|
|
13301
|
+
"terminal input was sent.");
|
|
13302
|
+
}
|
|
12928
13303
|
await recoverLifecycleFenceBeforeMutation({
|
|
12929
13304
|
options,
|
|
12930
13305
|
terminal: resolvedTerminal
|
|
@@ -12995,11 +13370,6 @@ async function runSend(options) {
|
|
|
12995
13370
|
if (!bindingMatchesLiveTerminal(currentSession, resolvedTerminal, lockedNativeIdentity, storeDir)) {
|
|
12996
13371
|
throw new Error("managed session identity changed while waiting to send; refresh list and retry");
|
|
12997
13372
|
}
|
|
12998
|
-
await assertStrictCodexSessionHasNoLatentClear({
|
|
12999
|
-
options,
|
|
13000
|
-
terminal: resolvedTerminal,
|
|
13001
|
-
session: currentSession
|
|
13002
|
-
});
|
|
13003
13373
|
const logicalLockedNativeIdentity = logicalIdentityForManagedSession({
|
|
13004
13374
|
storeDir,
|
|
13005
13375
|
session: currentSession,
|
|
@@ -13446,331 +13816,337 @@ async function runApprove(options) {
|
|
|
13446
13816
|
release();
|
|
13447
13817
|
}
|
|
13448
13818
|
};
|
|
13449
|
-
releaseStateLock = acquireFileLock(`${statePath}.lock`);
|
|
13450
13819
|
const writerStoreDir = pathsForConversationDir(path.dirname(statePath)).storeDir;
|
|
13451
|
-
|
|
13452
|
-
|
|
13453
|
-
|
|
13454
|
-
|
|
13455
|
-
|
|
13456
|
-
|
|
13457
|
-
|
|
13458
|
-
|
|
13459
|
-
|
|
13460
|
-
|
|
13461
|
-
|
|
13462
|
-
|
|
13463
|
-
|
|
13464
|
-
|
|
13465
|
-
|
|
13466
|
-
|
|
13467
|
-
|
|
13468
|
-
|
|
13469
|
-
|
|
13470
|
-
|
|
13471
|
-
|
|
13472
|
-
|
|
13473
|
-
|
|
13474
|
-
|
|
13475
|
-
|
|
13476
|
-
|
|
13477
|
-
|
|
13478
|
-
|
|
13479
|
-
|
|
13480
|
-
|
|
13481
|
-
|
|
13482
|
-
|
|
13483
|
-
|
|
13484
|
-
|
|
13485
|
-
|
|
13486
|
-
|
|
13820
|
+
const runApprovalWithStateLock = async () => {
|
|
13821
|
+
let approval;
|
|
13822
|
+
let lockedConversation = conversation;
|
|
13823
|
+
const currentConversation = loadState(statePath);
|
|
13824
|
+
const currentTakeover = isRecord(currentConversation.native_session_takeover)
|
|
13825
|
+
? currentConversation.native_session_takeover
|
|
13826
|
+
: undefined;
|
|
13827
|
+
const currentControl = terminalControlFromTakeover(currentTakeover);
|
|
13828
|
+
const currentApproval = isRecord(currentTakeover?.terminal_bridge_approval)
|
|
13829
|
+
? currentTakeover.terminal_bridge_approval
|
|
13830
|
+
: undefined;
|
|
13831
|
+
if (currentConversation.status !== conversation.status ||
|
|
13832
|
+
currentTakeover?.terminal_bridge_message_id !== nativeTakeover?.terminal_bridge_message_id ||
|
|
13833
|
+
!currentControl ||
|
|
13834
|
+
!terminalControlsShareIncarnation(currentControl, terminalControl) ||
|
|
13835
|
+
(claudeScreenApproval &&
|
|
13836
|
+
currentApproval?.fingerprint !== monitoredApproval?.fingerprint)) {
|
|
13837
|
+
throw new Error("approval state changed while waiting for terminal control; refresh status and retry");
|
|
13838
|
+
}
|
|
13839
|
+
assertManagedTerminalDispatchOwner({
|
|
13840
|
+
storeDir: writerStoreDir,
|
|
13841
|
+
conversation: currentConversation,
|
|
13842
|
+
terminalControl: currentControl,
|
|
13843
|
+
action: "approve"
|
|
13844
|
+
});
|
|
13845
|
+
lockedConversation = currentConversation;
|
|
13846
|
+
const currentRuntimeIdentity = terminalRuntimeIdentityForConversation(currentConversation, currentControl);
|
|
13847
|
+
approval = await createTerminalAgentBridge(options).approve(executor.kind, currentControl, {
|
|
13848
|
+
expectedFingerprint,
|
|
13849
|
+
scrollbackLines: Number(options.scrollbackLines ?? 120),
|
|
13850
|
+
runtime: currentRuntimeIdentity,
|
|
13851
|
+
managedRequest: terminalDurableRequestForConversation(currentConversation, currentControl),
|
|
13852
|
+
requiredDecisionMode: autoApproved && executor.kind === "claude" ? "keys" : undefined,
|
|
13853
|
+
authorize: autoApproved
|
|
13854
|
+
? ({ agent, terminalControl: currentTerminalControl, inspection, fingerprint }) => {
|
|
13855
|
+
if (!autoApprovalPolicy) {
|
|
13856
|
+
return {
|
|
13857
|
+
approved: false,
|
|
13858
|
+
reason: "automatic approval requires an executor-side policy"
|
|
13859
|
+
};
|
|
13860
|
+
}
|
|
13861
|
+
const candidate = policyCandidateForInspection({
|
|
13862
|
+
agent,
|
|
13863
|
+
currentTerminalControl,
|
|
13864
|
+
inspection,
|
|
13865
|
+
fingerprint
|
|
13866
|
+
});
|
|
13867
|
+
executorPolicyDecision = evaluateApprovalPolicy({
|
|
13868
|
+
policy: autoApprovalPolicy,
|
|
13869
|
+
candidate
|
|
13870
|
+
});
|
|
13871
|
+
if (executorPolicyDecision.action !== "approve") {
|
|
13872
|
+
return {
|
|
13873
|
+
approved: false,
|
|
13874
|
+
reason: `executor-side auto-approval policy rejected the current request: ${executorPolicyDecision.reason}`
|
|
13875
|
+
};
|
|
13876
|
+
}
|
|
13877
|
+
if (policyRuleId && executorPolicyDecision.ruleId !== policyRuleId) {
|
|
13878
|
+
return {
|
|
13879
|
+
approved: false,
|
|
13880
|
+
reason: "executor-side auto-approval rule changed before execution"
|
|
13881
|
+
};
|
|
13882
|
+
}
|
|
13883
|
+
if (policyFingerprint &&
|
|
13884
|
+
executorPolicyDecision.policyFingerprint !== policyFingerprint) {
|
|
13885
|
+
return {
|
|
13886
|
+
approved: false,
|
|
13887
|
+
reason: "executor-side auto-approval policy changed before execution"
|
|
13888
|
+
};
|
|
13889
|
+
}
|
|
13890
|
+
return { approved: true };
|
|
13891
|
+
}
|
|
13892
|
+
: undefined,
|
|
13893
|
+
beforeKeyDispatch: claudeScreenApproval
|
|
13894
|
+
? ({ fingerprint, terminalControl: dispatchControl, inspection, keys }) => {
|
|
13895
|
+
if (autoApproved) {
|
|
13487
13896
|
if (!autoApprovalPolicy) {
|
|
13488
|
-
|
|
13489
|
-
approved: false,
|
|
13490
|
-
reason: "automatic approval requires an executor-side policy"
|
|
13491
|
-
};
|
|
13897
|
+
throw new Error("automatic approval requires an executor-side policy before dispatch");
|
|
13492
13898
|
}
|
|
13493
|
-
const
|
|
13494
|
-
agent,
|
|
13495
|
-
currentTerminalControl,
|
|
13496
|
-
inspection,
|
|
13497
|
-
fingerprint
|
|
13498
|
-
});
|
|
13499
|
-
executorPolicyDecision = evaluateApprovalPolicy({
|
|
13899
|
+
const freshPolicyDecision = evaluateApprovalPolicy({
|
|
13500
13900
|
policy: autoApprovalPolicy,
|
|
13501
|
-
candidate
|
|
13901
|
+
candidate: policyCandidateForInspection({
|
|
13902
|
+
agent: executor.kind,
|
|
13903
|
+
currentTerminalControl: dispatchControl,
|
|
13904
|
+
inspection,
|
|
13905
|
+
fingerprint
|
|
13906
|
+
})
|
|
13502
13907
|
});
|
|
13503
|
-
if (
|
|
13504
|
-
|
|
13505
|
-
approved: false,
|
|
13506
|
-
reason: `executor-side auto-approval policy rejected the current request: ${executorPolicyDecision.reason}`
|
|
13507
|
-
};
|
|
13908
|
+
if (freshPolicyDecision.action !== "approve") {
|
|
13909
|
+
throw new Error(`executor-side auto-approval policy rejected the recaptured request: ${freshPolicyDecision.reason}`);
|
|
13508
13910
|
}
|
|
13509
|
-
if (
|
|
13510
|
-
|
|
13511
|
-
|
|
13512
|
-
|
|
13513
|
-
|
|
13911
|
+
if (executorPolicyDecision?.ruleId &&
|
|
13912
|
+
freshPolicyDecision.ruleId !== executorPolicyDecision.ruleId) {
|
|
13913
|
+
throw new Error("executor-side auto-approval rule changed after recapture");
|
|
13914
|
+
}
|
|
13915
|
+
if (policyRuleId && freshPolicyDecision.ruleId !== policyRuleId) {
|
|
13916
|
+
throw new Error("executor-side auto-approval rule changed before dispatch");
|
|
13514
13917
|
}
|
|
13515
13918
|
if (policyFingerprint &&
|
|
13516
|
-
|
|
13517
|
-
|
|
13518
|
-
approved: false,
|
|
13519
|
-
reason: "executor-side auto-approval policy changed before execution"
|
|
13520
|
-
};
|
|
13919
|
+
freshPolicyDecision.policyFingerprint !== policyFingerprint) {
|
|
13920
|
+
throw new Error("executor-side auto-approval policy changed before dispatch");
|
|
13521
13921
|
}
|
|
13522
|
-
|
|
13922
|
+
executorPolicyDecision = freshPolicyDecision;
|
|
13523
13923
|
}
|
|
13524
|
-
|
|
13525
|
-
|
|
13526
|
-
? ({ fingerprint, terminalControl: dispatchControl, inspection, keys }) => {
|
|
13527
|
-
if (autoApproved) {
|
|
13528
|
-
if (!autoApprovalPolicy) {
|
|
13529
|
-
throw new Error("automatic approval requires an executor-side policy before dispatch");
|
|
13530
|
-
}
|
|
13531
|
-
const freshPolicyDecision = evaluateApprovalPolicy({
|
|
13532
|
-
policy: autoApprovalPolicy,
|
|
13533
|
-
candidate: policyCandidateForInspection({
|
|
13534
|
-
agent: executor.kind,
|
|
13535
|
-
currentTerminalControl: dispatchControl,
|
|
13536
|
-
inspection,
|
|
13537
|
-
fingerprint
|
|
13538
|
-
})
|
|
13539
|
-
});
|
|
13540
|
-
if (freshPolicyDecision.action !== "approve") {
|
|
13541
|
-
throw new Error(`executor-side auto-approval policy rejected the recaptured request: ${freshPolicyDecision.reason}`);
|
|
13542
|
-
}
|
|
13543
|
-
if (executorPolicyDecision?.ruleId &&
|
|
13544
|
-
freshPolicyDecision.ruleId !== executorPolicyDecision.ruleId) {
|
|
13545
|
-
throw new Error("executor-side auto-approval rule changed after recapture");
|
|
13546
|
-
}
|
|
13547
|
-
if (policyRuleId && freshPolicyDecision.ruleId !== policyRuleId) {
|
|
13548
|
-
throw new Error("executor-side auto-approval rule changed before dispatch");
|
|
13549
|
-
}
|
|
13550
|
-
if (policyFingerprint &&
|
|
13551
|
-
freshPolicyDecision.policyFingerprint !== policyFingerprint) {
|
|
13552
|
-
throw new Error("executor-side auto-approval policy changed before dispatch");
|
|
13553
|
-
}
|
|
13554
|
-
executorPolicyDecision = freshPolicyDecision;
|
|
13555
|
-
}
|
|
13556
|
-
if (approvalDispatchReserved) {
|
|
13557
|
-
throw new Error("Claude approval dispatch was already reserved");
|
|
13558
|
-
}
|
|
13559
|
-
approvalDispatchReserved = true;
|
|
13560
|
-
if (!releaseStateLock) {
|
|
13561
|
-
throw new Error("approval state lock was released before terminal dispatch");
|
|
13562
|
-
}
|
|
13563
|
-
const latestConversation = loadState(statePath);
|
|
13564
|
-
const latestTakeover = isRecord(latestConversation.native_session_takeover)
|
|
13565
|
-
? latestConversation.native_session_takeover
|
|
13566
|
-
: undefined;
|
|
13567
|
-
const latestControl = terminalControlFromTakeover(latestTakeover);
|
|
13568
|
-
const latestApproval = isRecord(latestTakeover?.terminal_bridge_approval)
|
|
13569
|
-
? latestTakeover.terminal_bridge_approval
|
|
13570
|
-
: undefined;
|
|
13571
|
-
const latestNotifiedAt = validTimestampMs(latestApproval?.notified_at);
|
|
13572
|
-
const latestApprovalState = isRecord(latestApproval?.approval_state)
|
|
13573
|
-
? latestApproval.approval_state
|
|
13574
|
-
: undefined;
|
|
13575
|
-
const latestPolicyEvidence = isRecord(latestApprovalState?.policy_evidence)
|
|
13576
|
-
? latestApprovalState.policy_evidence
|
|
13577
|
-
: undefined;
|
|
13578
|
-
const recapturedPolicyEvidence = inspection.approval.approvable
|
|
13579
|
-
? inspection.approval.policyEvidence
|
|
13580
|
-
: undefined;
|
|
13581
|
-
const latestDispatch = isRecord(latestTakeover?.terminal_bridge_approval_dispatch)
|
|
13582
|
-
? latestTakeover.terminal_bridge_approval_dispatch
|
|
13583
|
-
: undefined;
|
|
13584
|
-
if (!latestTakeover ||
|
|
13585
|
-
latestConversation.status !== "waiting_for_openclaw" ||
|
|
13586
|
-
latestTakeover.terminal_bridge_message_id !==
|
|
13587
|
-
nativeTakeover?.terminal_bridge_message_id ||
|
|
13588
|
-
latestApproval?.fingerprint !== fingerprint ||
|
|
13589
|
-
latestNotifiedAt === undefined ||
|
|
13590
|
-
cliNowMs() - latestNotifiedAt > CLAUDE_SCREEN_APPROVAL_TTL_MS ||
|
|
13591
|
-
expectedFingerprint !== fingerprint ||
|
|
13592
|
-
!terminalControlsShareIncarnation(latestControl, dispatchControl) ||
|
|
13593
|
-
(autoApproved &&
|
|
13594
|
-
(latestPolicyEvidence?.source !== "claude_transcript" ||
|
|
13595
|
-
latestPolicyEvidence.evidence_fingerprint !==
|
|
13596
|
-
recapturedPolicyEvidence?.evidenceFingerprint))) {
|
|
13597
|
-
throw new Error("approval state changed before terminal dispatch; refresh status and retry");
|
|
13598
|
-
}
|
|
13599
|
-
if (latestDispatch?.state === "reserved" &&
|
|
13600
|
-
latestDispatch.terminal_bridge_message_id ===
|
|
13601
|
-
latestTakeover.terminal_bridge_message_id) {
|
|
13602
|
-
throw new Error("a previous Claude approval dispatch has an uncertain outcome; inspect and resolve the terminal manually");
|
|
13603
|
-
}
|
|
13604
|
-
const reservedAt = cliNow().toISOString();
|
|
13605
|
-
const reservedConversation = {
|
|
13606
|
-
...latestConversation,
|
|
13607
|
-
native_session_takeover: {
|
|
13608
|
-
...latestTakeover,
|
|
13609
|
-
terminal_bridge_approval_dispatch: {
|
|
13610
|
-
state: "reserved",
|
|
13611
|
-
attempt_id: randomUUID(),
|
|
13612
|
-
fingerprint,
|
|
13613
|
-
keys,
|
|
13614
|
-
terminal_target: dispatchControl.target,
|
|
13615
|
-
terminal_bridge_message_id: latestTakeover.terminal_bridge_message_id,
|
|
13616
|
-
reserved_at: reservedAt
|
|
13617
|
-
}
|
|
13618
|
-
},
|
|
13619
|
-
updated_at: reservedAt
|
|
13620
|
-
};
|
|
13621
|
-
saveState(statePath, reservedConversation);
|
|
13622
|
-
lockedConversation = reservedConversation;
|
|
13924
|
+
if (approvalDispatchReserved) {
|
|
13925
|
+
throw new Error("Claude approval dispatch was already reserved");
|
|
13623
13926
|
}
|
|
13624
|
-
|
|
13625
|
-
|
|
13626
|
-
|
|
13627
|
-
|
|
13628
|
-
|
|
13629
|
-
|
|
13630
|
-
|
|
13631
|
-
|
|
13632
|
-
|
|
13633
|
-
|
|
13634
|
-
|
|
13635
|
-
|
|
13636
|
-
|
|
13637
|
-
|
|
13638
|
-
|
|
13639
|
-
|
|
13640
|
-
|
|
13641
|
-
|
|
13642
|
-
|
|
13643
|
-
|
|
13644
|
-
|
|
13927
|
+
approvalDispatchReserved = true;
|
|
13928
|
+
if (!releaseStateLock) {
|
|
13929
|
+
throw new Error("approval state lock was released before terminal dispatch");
|
|
13930
|
+
}
|
|
13931
|
+
const latestConversation = loadState(statePath);
|
|
13932
|
+
const latestTakeover = isRecord(latestConversation.native_session_takeover)
|
|
13933
|
+
? latestConversation.native_session_takeover
|
|
13934
|
+
: undefined;
|
|
13935
|
+
const latestControl = terminalControlFromTakeover(latestTakeover);
|
|
13936
|
+
const latestApproval = isRecord(latestTakeover?.terminal_bridge_approval)
|
|
13937
|
+
? latestTakeover.terminal_bridge_approval
|
|
13938
|
+
: undefined;
|
|
13939
|
+
const latestNotifiedAt = validTimestampMs(latestApproval?.notified_at);
|
|
13940
|
+
const latestApprovalState = isRecord(latestApproval?.approval_state)
|
|
13941
|
+
? latestApproval.approval_state
|
|
13942
|
+
: undefined;
|
|
13943
|
+
const latestPolicyEvidence = isRecord(latestApprovalState?.policy_evidence)
|
|
13944
|
+
? latestApprovalState.policy_evidence
|
|
13945
|
+
: undefined;
|
|
13946
|
+
const recapturedPolicyEvidence = inspection.approval.approvable
|
|
13947
|
+
? inspection.approval.policyEvidence
|
|
13948
|
+
: undefined;
|
|
13949
|
+
const latestDispatch = isRecord(latestTakeover?.terminal_bridge_approval_dispatch)
|
|
13950
|
+
? latestTakeover.terminal_bridge_approval_dispatch
|
|
13951
|
+
: undefined;
|
|
13952
|
+
if (!latestTakeover ||
|
|
13953
|
+
latestConversation.status !== "waiting_for_openclaw" ||
|
|
13954
|
+
latestTakeover.terminal_bridge_message_id !==
|
|
13955
|
+
nativeTakeover?.terminal_bridge_message_id ||
|
|
13956
|
+
latestApproval?.fingerprint !== fingerprint ||
|
|
13957
|
+
latestNotifiedAt === undefined ||
|
|
13958
|
+
cliNowMs() - latestNotifiedAt > CLAUDE_SCREEN_APPROVAL_TTL_MS ||
|
|
13959
|
+
expectedFingerprint !== fingerprint ||
|
|
13960
|
+
!terminalControlsShareIncarnation(latestControl, dispatchControl) ||
|
|
13961
|
+
(autoApproved &&
|
|
13962
|
+
(latestPolicyEvidence?.source !== "claude_transcript" ||
|
|
13963
|
+
latestPolicyEvidence.evidence_fingerprint !==
|
|
13964
|
+
recapturedPolicyEvidence?.evidenceFingerprint))) {
|
|
13965
|
+
throw new Error("approval state changed before terminal dispatch; refresh status and retry");
|
|
13966
|
+
}
|
|
13967
|
+
if (latestDispatch?.state === "reserved" &&
|
|
13968
|
+
latestDispatch.terminal_bridge_message_id ===
|
|
13969
|
+
latestTakeover.terminal_bridge_message_id) {
|
|
13970
|
+
throw new Error("a previous Claude approval dispatch has an uncertain outcome; inspect and resolve the terminal manually");
|
|
13971
|
+
}
|
|
13972
|
+
const reservedAt = cliNow().toISOString();
|
|
13973
|
+
const reservedConversation = {
|
|
13974
|
+
...latestConversation,
|
|
13975
|
+
native_session_takeover: {
|
|
13976
|
+
...latestTakeover,
|
|
13977
|
+
terminal_bridge_approval_dispatch: {
|
|
13978
|
+
state: "reserved",
|
|
13979
|
+
attempt_id: randomUUID(),
|
|
13980
|
+
fingerprint,
|
|
13981
|
+
keys,
|
|
13982
|
+
terminal_target: dispatchControl.target,
|
|
13983
|
+
terminal_bridge_message_id: latestTakeover.terminal_bridge_message_id,
|
|
13984
|
+
reserved_at: reservedAt
|
|
13985
|
+
}
|
|
13986
|
+
},
|
|
13987
|
+
updated_at: reservedAt
|
|
13988
|
+
};
|
|
13989
|
+
saveState(statePath, reservedConversation);
|
|
13990
|
+
lockedConversation = reservedConversation;
|
|
13645
13991
|
}
|
|
13646
|
-
|
|
13647
|
-
|
|
13648
|
-
|
|
13649
|
-
|
|
13650
|
-
|
|
13651
|
-
|
|
13652
|
-
|
|
13653
|
-
actual_approval_fingerprint: actualFingerprint,
|
|
13654
|
-
screen_excerpt: approval.screenExcerpt
|
|
13655
|
-
});
|
|
13656
|
-
return;
|
|
13657
|
-
}
|
|
13658
|
-
appendEvent(logPath, {
|
|
13659
|
-
ts: cliNow().toISOString(),
|
|
13660
|
-
conversation_id: conversation.conversation_id,
|
|
13661
|
-
event: "terminal_approval_send",
|
|
13662
|
-
terminal_control: terminalControl,
|
|
13663
|
-
key: approval.key,
|
|
13664
|
-
keys: approval.keys,
|
|
13665
|
-
label: approval.label,
|
|
13666
|
-
decision_mode: approval.decisionMode,
|
|
13667
|
-
request_id: approval.requestId,
|
|
13668
|
-
approval_fingerprint: actualFingerprint,
|
|
13669
|
-
auto_approved: autoApproved,
|
|
13670
|
-
policy_rule_id: effectivePolicyRuleId,
|
|
13671
|
-
policy_fingerprint: effectivePolicyFingerprint
|
|
13672
|
-
});
|
|
13992
|
+
: undefined
|
|
13993
|
+
});
|
|
13994
|
+
const actualFingerprint = approval.fingerprint;
|
|
13995
|
+
const effectivePolicyRuleId = executorPolicyDecision?.ruleId ?? policyRuleId;
|
|
13996
|
+
const effectivePolicyFingerprint = executorPolicyDecision?.policyFingerprint ?? policyFingerprint;
|
|
13997
|
+
if (!approval.approved) {
|
|
13998
|
+
releaseApprovalStateLock();
|
|
13673
13999
|
if (autoApproved) {
|
|
13674
14000
|
appendEvent(logPath, {
|
|
13675
14001
|
ts: cliNow().toISOString(),
|
|
13676
14002
|
conversation_id: conversation.conversation_id,
|
|
13677
14003
|
event: "terminal_auto_approval_decision",
|
|
13678
|
-
action: "
|
|
14004
|
+
action: "rejected",
|
|
14005
|
+
reason: approval.reason,
|
|
13679
14006
|
terminal_control: terminalControl,
|
|
13680
|
-
|
|
14007
|
+
expected_fingerprint: expectedFingerprint,
|
|
14008
|
+
actual_fingerprint: actualFingerprint,
|
|
13681
14009
|
policy_rule_id: effectivePolicyRuleId,
|
|
13682
14010
|
policy_fingerprint: effectivePolicyFingerprint
|
|
13683
14011
|
});
|
|
13684
14012
|
}
|
|
13685
|
-
runtimeLog("info", "terminal_approval_send", {
|
|
13686
|
-
conversation_id: conversation.conversation_id,
|
|
13687
|
-
terminal_target: terminalControl.target,
|
|
13688
|
-
key: approval.key,
|
|
13689
|
-
keys: approval.keys,
|
|
13690
|
-
label: approval.label,
|
|
13691
|
-
decision_mode: approval.decisionMode,
|
|
13692
|
-
request_id: approval.requestId,
|
|
13693
|
-
approval_fingerprint: actualFingerprint,
|
|
13694
|
-
auto_approved: autoApproved,
|
|
13695
|
-
policy_rule_id: effectivePolicyRuleId,
|
|
13696
|
-
policy_fingerprint: effectivePolicyFingerprint
|
|
13697
|
-
});
|
|
13698
|
-
const nativeTakeoverForUpdate = isRecord(lockedConversation.native_session_takeover)
|
|
13699
|
-
? { ...lockedConversation.native_session_takeover }
|
|
13700
|
-
: {};
|
|
13701
|
-
const resolvedApproval = isRecord(nativeTakeoverForUpdate.terminal_bridge_approval)
|
|
13702
|
-
? nativeTakeoverForUpdate.terminal_bridge_approval
|
|
13703
|
-
: undefined;
|
|
13704
|
-
const resolvedApprovalScreenDigest = stringValue(resolvedApproval?.screen_digest);
|
|
13705
|
-
const resolvedApprovalState = isRecord(resolvedApproval?.approval_state)
|
|
13706
|
-
? resolvedApproval.approval_state
|
|
13707
|
-
: undefined;
|
|
13708
|
-
const resolvedTranscriptIdentity = claudeTranscriptApprovalIdentity(resolvedApprovalState);
|
|
13709
|
-
const approvalResolvedAt = cliNow().toISOString();
|
|
13710
|
-
const agentTimeoutMinutes = Number(options.agentTimeoutMinutes ??
|
|
13711
|
-
nativeTakeoverForUpdate.terminal_bridge_inactivity_timeout_minutes ??
|
|
13712
|
-
DEFAULT_AGENT_TIMEOUT_MINUTES);
|
|
13713
|
-
const agentHardTimeoutMinutes = positiveMinutes(options.agentHardTimeoutMinutes ??
|
|
13714
|
-
nativeTakeoverForUpdate.terminal_bridge_hard_timeout_minutes ??
|
|
13715
|
-
DEFAULT_AGENT_HARD_TIMEOUT_MINUTES, "--agent-hard-timeout-minutes");
|
|
13716
|
-
const nextNativeTakeover = {
|
|
13717
|
-
...nativeTakeoverForUpdate,
|
|
13718
|
-
terminal_bridge_approval: undefined,
|
|
13719
|
-
terminal_bridge_approval_dispatch: undefined,
|
|
13720
|
-
terminal_bridge_approval_resolved_at: approvalResolvedAt,
|
|
13721
|
-
terminal_bridge_last_approval_fingerprint: actualFingerprint,
|
|
13722
|
-
terminal_bridge_last_approval_screen_digest: resolvedApprovalScreenDigest,
|
|
13723
|
-
terminal_bridge_last_approval_request_id: resolvedTranscriptIdentity?.requestId,
|
|
13724
|
-
terminal_bridge_last_approval_evidence_fingerprint: resolvedTranscriptIdentity?.evidenceFingerprint,
|
|
13725
|
-
terminal_bridge_last_approval_prompt_cleared_at: undefined,
|
|
13726
|
-
terminal_bridge_last_approval_at: approvalResolvedAt,
|
|
13727
|
-
terminal_bridge_last_approval_message_id: nativeTakeoverForUpdate.terminal_bridge_message_id,
|
|
13728
|
-
terminal_bridge_monitor_lock_version: TERMINAL_BRIDGE_MONITOR_LOCK_VERSION,
|
|
13729
|
-
terminal_bridge_monitor_started_at: approvalResolvedAt,
|
|
13730
|
-
terminal_bridge_last_activity_at: approvalResolvedAt,
|
|
13731
|
-
terminal_bridge_last_activity_reason: "approval resolved",
|
|
13732
|
-
terminal_bridge_inactivity_timeout_minutes: agentTimeoutMinutes,
|
|
13733
|
-
terminal_bridge_hard_timeout_minutes: agentHardTimeoutMinutes,
|
|
13734
|
-
terminal_bridge_inactivity_deadline_at: deadlineAt(approvalResolvedAt, agentTimeoutMinutes),
|
|
13735
|
-
terminal_bridge_hard_deadline_at: deadlineAt(stringValue(nativeTakeoverForUpdate.terminal_bridge_started_at) ?? approvalResolvedAt, agentHardTimeoutMinutes)
|
|
13736
|
-
};
|
|
13737
|
-
delete nextNativeTakeover.terminal_bridge_approval;
|
|
13738
|
-
delete nextNativeTakeover.terminal_bridge_approval_dispatch;
|
|
13739
|
-
delete nextNativeTakeover.terminal_bridge_last_approval_prompt_cleared_at;
|
|
13740
|
-
const nextConversation = {
|
|
13741
|
-
...lockedConversation,
|
|
13742
|
-
status: terminalBridgeEnabled(lockedConversation)
|
|
13743
|
-
? "waiting_for_agent"
|
|
13744
|
-
: lockedConversation.status,
|
|
13745
|
-
native_session_takeover: nextNativeTakeover,
|
|
13746
|
-
updated_at: approvalResolvedAt
|
|
13747
|
-
};
|
|
13748
|
-
saveState(statePath, nextConversation);
|
|
13749
|
-
releaseApprovalStateLock();
|
|
13750
|
-
releaseApprovalTerminalLock();
|
|
13751
|
-
const bridgeMonitor = ensureTerminalBridgeMonitorAfterApproval({
|
|
13752
|
-
conversation: nextConversation,
|
|
13753
|
-
statePath,
|
|
13754
|
-
logPath,
|
|
13755
|
-
terminalControl,
|
|
13756
|
-
options
|
|
13757
|
-
});
|
|
13758
14013
|
printJson({
|
|
13759
|
-
conversation
|
|
13760
|
-
approved:
|
|
14014
|
+
conversation,
|
|
14015
|
+
approved: false,
|
|
14016
|
+
blocked: approval.blocked,
|
|
14017
|
+
reason: approval.reason,
|
|
14018
|
+
terminal_control: terminalControl,
|
|
14019
|
+
expected_approval_fingerprint: expectedFingerprint,
|
|
14020
|
+
actual_approval_fingerprint: actualFingerprint,
|
|
14021
|
+
screen_excerpt: approval.screenExcerpt
|
|
14022
|
+
});
|
|
14023
|
+
return;
|
|
14024
|
+
}
|
|
14025
|
+
appendEvent(logPath, {
|
|
14026
|
+
ts: cliNow().toISOString(),
|
|
14027
|
+
conversation_id: conversation.conversation_id,
|
|
14028
|
+
event: "terminal_approval_send",
|
|
14029
|
+
terminal_control: terminalControl,
|
|
14030
|
+
key: approval.key,
|
|
14031
|
+
keys: approval.keys,
|
|
14032
|
+
label: approval.label,
|
|
14033
|
+
decision_mode: approval.decisionMode,
|
|
14034
|
+
request_id: approval.requestId,
|
|
14035
|
+
approval_fingerprint: actualFingerprint,
|
|
14036
|
+
auto_approved: autoApproved,
|
|
14037
|
+
policy_rule_id: effectivePolicyRuleId,
|
|
14038
|
+
policy_fingerprint: effectivePolicyFingerprint
|
|
14039
|
+
});
|
|
14040
|
+
if (autoApproved) {
|
|
14041
|
+
appendEvent(logPath, {
|
|
14042
|
+
ts: cliNow().toISOString(),
|
|
14043
|
+
conversation_id: conversation.conversation_id,
|
|
14044
|
+
event: "terminal_auto_approval_decision",
|
|
14045
|
+
action: "approved",
|
|
13761
14046
|
terminal_control: terminalControl,
|
|
13762
|
-
key: approval.key,
|
|
13763
|
-
keys: approval.keys,
|
|
13764
|
-
label: approval.label,
|
|
13765
|
-
decision_mode: approval.decisionMode,
|
|
13766
|
-
request_id: approval.requestId,
|
|
13767
14047
|
approval_fingerprint: actualFingerprint,
|
|
13768
|
-
auto_approved: autoApproved,
|
|
13769
14048
|
policy_rule_id: effectivePolicyRuleId,
|
|
13770
|
-
policy_fingerprint: effectivePolicyFingerprint
|
|
13771
|
-
monitor_pid: bridgeMonitor.monitorPid ?? null,
|
|
13772
|
-
monitor_handoff_pid: bridgeMonitor.handoffWatchdog?.pid ?? null
|
|
14049
|
+
policy_fingerprint: effectivePolicyFingerprint
|
|
13773
14050
|
});
|
|
14051
|
+
}
|
|
14052
|
+
runtimeLog("info", "terminal_approval_send", {
|
|
14053
|
+
conversation_id: conversation.conversation_id,
|
|
14054
|
+
terminal_target: terminalControl.target,
|
|
14055
|
+
key: approval.key,
|
|
14056
|
+
keys: approval.keys,
|
|
14057
|
+
label: approval.label,
|
|
14058
|
+
decision_mode: approval.decisionMode,
|
|
14059
|
+
request_id: approval.requestId,
|
|
14060
|
+
approval_fingerprint: actualFingerprint,
|
|
14061
|
+
auto_approved: autoApproved,
|
|
14062
|
+
policy_rule_id: effectivePolicyRuleId,
|
|
14063
|
+
policy_fingerprint: effectivePolicyFingerprint
|
|
14064
|
+
});
|
|
14065
|
+
const nativeTakeoverForUpdate = isRecord(lockedConversation.native_session_takeover)
|
|
14066
|
+
? { ...lockedConversation.native_session_takeover }
|
|
14067
|
+
: {};
|
|
14068
|
+
const resolvedApproval = isRecord(nativeTakeoverForUpdate.terminal_bridge_approval)
|
|
14069
|
+
? nativeTakeoverForUpdate.terminal_bridge_approval
|
|
14070
|
+
: undefined;
|
|
14071
|
+
const resolvedApprovalScreenDigest = stringValue(resolvedApproval?.screen_digest);
|
|
14072
|
+
const resolvedApprovalState = isRecord(resolvedApproval?.approval_state)
|
|
14073
|
+
? resolvedApproval.approval_state
|
|
14074
|
+
: undefined;
|
|
14075
|
+
const resolvedTranscriptIdentity = claudeTranscriptApprovalIdentity(resolvedApprovalState);
|
|
14076
|
+
const approvalResolvedAt = cliNow().toISOString();
|
|
14077
|
+
const agentTimeoutMinutes = Number(options.agentTimeoutMinutes ??
|
|
14078
|
+
nativeTakeoverForUpdate.terminal_bridge_inactivity_timeout_minutes ??
|
|
14079
|
+
DEFAULT_AGENT_TIMEOUT_MINUTES);
|
|
14080
|
+
const agentHardTimeoutMinutes = positiveMinutes(options.agentHardTimeoutMinutes ??
|
|
14081
|
+
nativeTakeoverForUpdate.terminal_bridge_hard_timeout_minutes ??
|
|
14082
|
+
DEFAULT_AGENT_HARD_TIMEOUT_MINUTES, "--agent-hard-timeout-minutes");
|
|
14083
|
+
const nextNativeTakeover = {
|
|
14084
|
+
...nativeTakeoverForUpdate,
|
|
14085
|
+
terminal_bridge_approval: undefined,
|
|
14086
|
+
terminal_bridge_approval_dispatch: undefined,
|
|
14087
|
+
terminal_bridge_approval_resolved_at: approvalResolvedAt,
|
|
14088
|
+
terminal_bridge_last_approval_fingerprint: actualFingerprint,
|
|
14089
|
+
terminal_bridge_last_approval_screen_digest: resolvedApprovalScreenDigest,
|
|
14090
|
+
terminal_bridge_last_approval_request_id: resolvedTranscriptIdentity?.requestId,
|
|
14091
|
+
terminal_bridge_last_approval_evidence_fingerprint: resolvedTranscriptIdentity?.evidenceFingerprint,
|
|
14092
|
+
terminal_bridge_last_approval_prompt_cleared_at: undefined,
|
|
14093
|
+
terminal_bridge_last_approval_at: approvalResolvedAt,
|
|
14094
|
+
terminal_bridge_last_approval_message_id: nativeTakeoverForUpdate.terminal_bridge_message_id,
|
|
14095
|
+
terminal_bridge_monitor_lock_version: TERMINAL_BRIDGE_MONITOR_LOCK_VERSION,
|
|
14096
|
+
terminal_bridge_monitor_started_at: approvalResolvedAt,
|
|
14097
|
+
terminal_bridge_last_activity_at: approvalResolvedAt,
|
|
14098
|
+
terminal_bridge_last_activity_reason: "approval resolved",
|
|
14099
|
+
terminal_bridge_inactivity_timeout_minutes: agentTimeoutMinutes,
|
|
14100
|
+
terminal_bridge_hard_timeout_minutes: agentHardTimeoutMinutes,
|
|
14101
|
+
terminal_bridge_inactivity_deadline_at: deadlineAt(approvalResolvedAt, agentTimeoutMinutes),
|
|
14102
|
+
terminal_bridge_hard_deadline_at: deadlineAt(stringValue(nativeTakeoverForUpdate.terminal_bridge_started_at) ?? approvalResolvedAt, agentHardTimeoutMinutes)
|
|
14103
|
+
};
|
|
14104
|
+
delete nextNativeTakeover.terminal_bridge_approval;
|
|
14105
|
+
delete nextNativeTakeover.terminal_bridge_approval_dispatch;
|
|
14106
|
+
delete nextNativeTakeover.terminal_bridge_last_approval_prompt_cleared_at;
|
|
14107
|
+
const nextConversation = {
|
|
14108
|
+
...lockedConversation,
|
|
14109
|
+
status: terminalBridgeEnabled(lockedConversation)
|
|
14110
|
+
? "waiting_for_agent"
|
|
14111
|
+
: lockedConversation.status,
|
|
14112
|
+
native_session_takeover: nextNativeTakeover,
|
|
14113
|
+
updated_at: approvalResolvedAt
|
|
14114
|
+
};
|
|
14115
|
+
saveState(statePath, nextConversation);
|
|
14116
|
+
releaseApprovalStateLock();
|
|
14117
|
+
const bridgeMonitor = ensureTerminalBridgeMonitorAfterApproval({
|
|
14118
|
+
conversation: nextConversation,
|
|
14119
|
+
statePath,
|
|
14120
|
+
logPath,
|
|
14121
|
+
terminalControl,
|
|
14122
|
+
options
|
|
14123
|
+
});
|
|
14124
|
+
printJson({
|
|
14125
|
+
conversation: nextConversation,
|
|
14126
|
+
approved: true,
|
|
14127
|
+
terminal_control: terminalControl,
|
|
14128
|
+
key: approval.key,
|
|
14129
|
+
keys: approval.keys,
|
|
14130
|
+
label: approval.label,
|
|
14131
|
+
decision_mode: approval.decisionMode,
|
|
14132
|
+
request_id: approval.requestId,
|
|
14133
|
+
approval_fingerprint: actualFingerprint,
|
|
14134
|
+
auto_approved: autoApproved,
|
|
14135
|
+
policy_rule_id: effectivePolicyRuleId,
|
|
14136
|
+
policy_fingerprint: effectivePolicyFingerprint,
|
|
14137
|
+
monitor_pid: bridgeMonitor.monitorPid ?? null,
|
|
14138
|
+
monitor_handoff_pid: bridgeMonitor.handoffWatchdog?.pid ?? null
|
|
14139
|
+
});
|
|
14140
|
+
};
|
|
14141
|
+
try {
|
|
14142
|
+
return await withStoreWriterLeaseAsync(writerStoreDir, async () => {
|
|
14143
|
+
releaseStateLock = acquireFileLock(`${statePath}.lock`);
|
|
14144
|
+
try {
|
|
14145
|
+
return await runApprovalWithStateLock();
|
|
14146
|
+
}
|
|
14147
|
+
finally {
|
|
14148
|
+
releaseApprovalStateLock();
|
|
14149
|
+
}
|
|
13774
14150
|
});
|
|
13775
14151
|
}
|
|
13776
14152
|
finally {
|
|
@@ -19979,12 +20355,20 @@ async function runTerminalBridgeMonitorWithLock(options, lifecycle, expectedTerm
|
|
|
19979
20355
|
});
|
|
19980
20356
|
}
|
|
19981
20357
|
}
|
|
20358
|
+
const approvalPromptEvidenceUnavailable = isRecord(approval) &&
|
|
20359
|
+
approval.blocked === true &&
|
|
20360
|
+
approval.approvable === true &&
|
|
20361
|
+
(stringValue(approval.decision_mode) ?? "keys") === "keys" &&
|
|
20362
|
+
!stringValue(approval.fingerprint);
|
|
19982
20363
|
if (!suppressApprovalNotification &&
|
|
19983
20364
|
isRecord(approval) &&
|
|
19984
20365
|
approval.blocked === true &&
|
|
19985
|
-
approval.approvable !== true
|
|
19986
|
-
|
|
19987
|
-
|
|
20366
|
+
(approval.approvable !== true ||
|
|
20367
|
+
approvalPromptEvidenceUnavailable)) {
|
|
20368
|
+
const approvalReason = approvalPromptEvidenceUnavailable
|
|
20369
|
+
? `${executor.display_name} approval prompt lacks exact prompt-region evidence`
|
|
20370
|
+
: stringValue(approval.reason) ??
|
|
20371
|
+
"Claude Code permission state cannot be safely resolved through AKK";
|
|
19988
20372
|
appendEvent(logPath, {
|
|
19989
20373
|
ts: cliNow().toISOString(),
|
|
19990
20374
|
conversation_id: conversation.conversation_id,
|
|
@@ -20851,6 +21235,32 @@ function terminalLedgerReceiptCandidate(ledger) {
|
|
|
20851
21235
|
}
|
|
20852
21236
|
function mergeTerminalLedgerReceipt(previous, next) {
|
|
20853
21237
|
const messageId = required(stringValue(previous.message_id), "terminal dispatch receipt message id is required");
|
|
21238
|
+
const safeAbortRetryGeneration = Boolean(previous.status === "aborted" &&
|
|
21239
|
+
previous.safe_to_retry === true &&
|
|
21240
|
+
stringValue(next.message_id) === messageId &&
|
|
21241
|
+
stringValue(next.previous_generation_id) === messageId &&
|
|
21242
|
+
[
|
|
21243
|
+
"text_injected",
|
|
21244
|
+
"enter_dispatched",
|
|
21245
|
+
"submitted",
|
|
21246
|
+
"agent_accepted",
|
|
21247
|
+
"not_accepted",
|
|
21248
|
+
"uncertain",
|
|
21249
|
+
"aborted"
|
|
21250
|
+
].includes(String(next.status)) &&
|
|
21251
|
+
validTimestampMs(previous.aborted_at) !== undefined &&
|
|
21252
|
+
validTimestampMs(next.prepared_at) !== undefined &&
|
|
21253
|
+
Date.parse(String(next.prepared_at)) >=
|
|
21254
|
+
Date.parse(String(previous.aborted_at)));
|
|
21255
|
+
if (safeAbortRetryGeneration) {
|
|
21256
|
+
// A proved zero-input abort intentionally allows the same OpenClaw
|
|
21257
|
+
// idempotency key to start one fresh Turn/Session generation. The old
|
|
21258
|
+
// Turn and, for v3, its abort_resolved transfer retain that immutable
|
|
21259
|
+
// proof; the terminal-wide singleton receipt advances to the retry so its
|
|
21260
|
+
// different binding/Turn identity cannot collide with the abandoned
|
|
21261
|
+
// zero-input generation.
|
|
21262
|
+
return next;
|
|
21263
|
+
}
|
|
20854
21264
|
for (const field of TERMINAL_LEDGER_RECEIPT_IMMUTABLE_FIELDS) {
|
|
20855
21265
|
const previousValue = previous[field];
|
|
20856
21266
|
const nextValue = next[field];
|
|
@@ -23745,47 +24155,6 @@ function codexLatentClearResumeObservation({ screen, agentVersion }) {
|
|
|
23745
24155
|
})
|
|
23746
24156
|
};
|
|
23747
24157
|
}
|
|
23748
|
-
function codexLatentClearResumeFingerprint({ screen, sourceNativeThreadId, agentVersion }) {
|
|
23749
|
-
if (!isExactNativeThreadId(sourceNativeThreadId)) {
|
|
23750
|
-
return undefined;
|
|
23751
|
-
}
|
|
23752
|
-
const observation = codexLatentClearResumeObservation({
|
|
23753
|
-
screen,
|
|
23754
|
-
agentVersion
|
|
23755
|
-
});
|
|
23756
|
-
return observation?.sourceNativeThreadId === sourceNativeThreadId.toLowerCase()
|
|
23757
|
-
? observation.fingerprint
|
|
23758
|
-
: undefined;
|
|
23759
|
-
}
|
|
23760
|
-
async function observeCodexLatentClearResumeFingerprint({ options, terminal, sourceNativeThreadId }) {
|
|
23761
|
-
if (terminal.agent !== "codex" || !isExactNativeThreadId(sourceNativeThreadId)) {
|
|
23762
|
-
return undefined;
|
|
23763
|
-
}
|
|
23764
|
-
const agentVersion = agentVersionForRunningProcess("codex", terminal.pid, options);
|
|
23765
|
-
if (!codexLifecycleBehaviorProfile(agentVersion)) {
|
|
23766
|
-
return undefined;
|
|
23767
|
-
}
|
|
23768
|
-
const status = await createTerminalAgentBridge(options).status("codex", terminal.terminalControl, { runtime: terminalRuntimeForLiveIdentity({ terminal, physicalOnly: true }) });
|
|
23769
|
-
return codexLatentClearResumeFingerprint({
|
|
23770
|
-
screen: status.screen.excerpt,
|
|
23771
|
-
sourceNativeThreadId,
|
|
23772
|
-
agentVersion
|
|
23773
|
-
});
|
|
23774
|
-
}
|
|
23775
|
-
async function assertStrictCodexSessionHasNoLatentClear({ options, terminal, session }) {
|
|
23776
|
-
if (session.agent !== "codex") {
|
|
23777
|
-
return;
|
|
23778
|
-
}
|
|
23779
|
-
const fingerprint = await observeCodexLatentClearResumeFingerprint({
|
|
23780
|
-
options,
|
|
23781
|
-
terminal,
|
|
23782
|
-
sourceNativeThreadId: session.binding?.native_thread_id
|
|
23783
|
-
});
|
|
23784
|
-
if (fingerprint) {
|
|
23785
|
-
throw new Error("Codex /clear changed the foreground logical thread; refresh AKK list " +
|
|
23786
|
-
"and use its snapshot-bound follow-current send. No task input was sent.");
|
|
23787
|
-
}
|
|
23788
|
-
}
|
|
23789
24158
|
function nativeInspectionComposerEmpty(agent, screen) {
|
|
23790
24159
|
return agent === "codex"
|
|
23791
24160
|
? codexComposerEmpty(screen)
|