@scotthuang/agent-knock-knock 0.2.49 → 0.2.51
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 +61 -32
- package/dist/src/approval-policy.d.ts +4 -1
- package/dist/src/approval-policy.js +99 -13
- package/dist/src/approval-policy.js.map +1 -1
- package/dist/src/claude-local-transcript-provider.d.ts +27 -0
- package/dist/src/claude-local-transcript-provider.js +306 -13
- package/dist/src/claude-local-transcript-provider.js.map +1 -1
- package/dist/src/claude-terminal-agent-adapter.d.ts +22 -1
- package/dist/src/claude-terminal-agent-adapter.js +175 -7
- package/dist/src/claude-terminal-agent-adapter.js.map +1 -1
- package/dist/src/cli.js +1201 -268
- package/dist/src/cli.js.map +1 -1
- package/dist/src/openclaw-plugin.js +4 -3
- package/dist/src/openclaw-plugin.js.map +1 -1
- package/dist/src/protocol.d.ts +3 -1
- package/dist/src/protocol.js +2 -2
- package/dist/src/protocol.js.map +1 -1
- package/dist/src/terminal-agent-adapter.d.ts +22 -0
- package/dist/src/terminal-agent-adapter.js.map +1 -1
- package/dist/src/terminal-agent-bridge.d.ts +11 -2
- package/dist/src/terminal-agent-bridge.js +115 -13
- package/dist/src/terminal-agent-bridge.js.map +1 -1
- package/openclaw.plugin.json +2 -2
- package/package.json +1 -1
- package/templates/openclaw-skills/agent-knock-knock/SKILL.md +2 -2
package/dist/src/cli.js
CHANGED
|
@@ -7,7 +7,7 @@ import process from "node:process";
|
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
8
|
import { createCodexTerminalAgentAdapter, detectCodexDurableCompletion } from "./codex-terminal-agent-adapter.js";
|
|
9
9
|
import { createClaudeTerminalAgentAdapter } from "./claude-terminal-agent-adapter.js";
|
|
10
|
-
import { captureClaudeTranscriptAnchor, defaultClaudeHome, detectClaudeTranscriptCompletion } from "./claude-local-transcript-provider.js";
|
|
10
|
+
import { captureClaudeTranscriptAnchor, defaultClaudeHome, detectClaudeTranscriptCompletion, detectClaudeTranscriptPendingApproval } from "./claude-local-transcript-provider.js";
|
|
11
11
|
import { ClaudeHookStore, ClaudeHookStoreError, defaultClaudeHookStoreDir } from "./claude-hook-store.js";
|
|
12
12
|
import { claudePermissionHookOutput } from "./claude-hook-protocol.js";
|
|
13
13
|
import { defaultClaudeSettingsPath, loadTrustedClaudeTokenjuiceLaunchers } from "./claude-hook-installer.js";
|
|
@@ -856,6 +856,12 @@ function createRuntimeTerminalAgentRegistry(options) {
|
|
|
856
856
|
}),
|
|
857
857
|
createClaudeTerminalAgentAdapter({
|
|
858
858
|
agentRows: loadClaudeAgentRows(options),
|
|
859
|
+
detectPendingApproval(request) {
|
|
860
|
+
return detectClaudeTranscriptPendingApproval(request, {
|
|
861
|
+
claudeHome: expandHome(options.claudeHome),
|
|
862
|
+
agentRows: loadClaudeAgentRows(options)
|
|
863
|
+
});
|
|
864
|
+
},
|
|
859
865
|
async detectDurableCompletion(request) {
|
|
860
866
|
return detectClaudeTranscriptCompletion(request, {
|
|
861
867
|
claudeHome: expandHome(options.claudeHome),
|
|
@@ -997,6 +1003,25 @@ function terminalRuntimeIdentityForConversation(conversation, terminalControl) {
|
|
|
997
1003
|
terminalTarget: terminalControl.target
|
|
998
1004
|
};
|
|
999
1005
|
}
|
|
1006
|
+
function terminalDurableRequestForConversation(conversation, terminalControl) {
|
|
1007
|
+
const nativeTakeover = isRecord(conversation?.native_session_takeover)
|
|
1008
|
+
? conversation.native_session_takeover
|
|
1009
|
+
: undefined;
|
|
1010
|
+
const runtime = terminalRuntimeIdentityForConversation(conversation, terminalControl);
|
|
1011
|
+
const requestText = String(nativeTakeover?.terminal_bridge_request_text ?? conversation?.user_request ?? "");
|
|
1012
|
+
return {
|
|
1013
|
+
sessionId: runtime.sessionId,
|
|
1014
|
+
cwd: stringValue(nativeTakeover?.source_cwd),
|
|
1015
|
+
requestText,
|
|
1016
|
+
requestHash: stringValue(nativeTakeover?.terminal_bridge_request_hash),
|
|
1017
|
+
startedAt: stringValue(nativeTakeover?.terminal_bridge_started_at),
|
|
1018
|
+
context: {
|
|
1019
|
+
conversation,
|
|
1020
|
+
nativeTakeover,
|
|
1021
|
+
...runtime
|
|
1022
|
+
}
|
|
1023
|
+
};
|
|
1024
|
+
}
|
|
1000
1025
|
async function migrateLegacyTerminalAgentIdentity({ conversation, statePath, logPath, options }) {
|
|
1001
1026
|
const nativeTakeover = isRecord(conversation?.native_session_takeover)
|
|
1002
1027
|
? conversation.native_session_takeover
|
|
@@ -1714,6 +1739,151 @@ function startTerminalBridgeMonitorForConversation({ conversation, statePath, lo
|
|
|
1714
1739
|
claudeHookStoreDir: options.claudeHookStoreDir ?? nativeTakeover?.["claude_hook_store_dir"]
|
|
1715
1740
|
});
|
|
1716
1741
|
}
|
|
1742
|
+
function startTerminalBridgeMonitorHandoffWatchdog({ conversation, statePath, logPath, options }) {
|
|
1743
|
+
if (options.disableTerminalBridgeMonitor === true ||
|
|
1744
|
+
!terminalBridgeEnabled(conversation)) {
|
|
1745
|
+
return undefined;
|
|
1746
|
+
}
|
|
1747
|
+
const nativeTakeover = isRecord(conversation.native_session_takeover)
|
|
1748
|
+
? conversation.native_session_takeover
|
|
1749
|
+
: undefined;
|
|
1750
|
+
const terminalMessageId = stringValue(nativeTakeover?.terminal_bridge_message_id);
|
|
1751
|
+
if (!terminalMessageId) {
|
|
1752
|
+
return undefined;
|
|
1753
|
+
}
|
|
1754
|
+
const args = [
|
|
1755
|
+
new URL(import.meta.url).pathname,
|
|
1756
|
+
"monitor",
|
|
1757
|
+
"--terminal-bridge-handoff",
|
|
1758
|
+
"--state",
|
|
1759
|
+
statePath,
|
|
1760
|
+
"--log",
|
|
1761
|
+
logPath,
|
|
1762
|
+
"--expected-terminal-message-id",
|
|
1763
|
+
terminalMessageId
|
|
1764
|
+
];
|
|
1765
|
+
const monitorPollIntervalMs = Number(options.monitorPollIntervalMs);
|
|
1766
|
+
if (Number.isFinite(monitorPollIntervalMs) && monitorPollIntervalMs > 0) {
|
|
1767
|
+
args.push("--monitor-poll-interval-ms", String(monitorPollIntervalMs));
|
|
1768
|
+
}
|
|
1769
|
+
const handoffPollIntervalMs = Number(options.monitorHandoffPollIntervalMs);
|
|
1770
|
+
if (Number.isFinite(handoffPollIntervalMs) && handoffPollIntervalMs > 0) {
|
|
1771
|
+
args.push("--monitor-handoff-poll-interval-ms", String(handoffPollIntervalMs));
|
|
1772
|
+
}
|
|
1773
|
+
const claudeHome = options.claudeHome ?? nativeTakeover?.["claude_home"];
|
|
1774
|
+
const claudeHookStoreDir = options.claudeHookStoreDir ??
|
|
1775
|
+
nativeTakeover?.["claude_hook_store_dir"];
|
|
1776
|
+
if (options.codexHome) {
|
|
1777
|
+
args.push("--codex-home", options.codexHome);
|
|
1778
|
+
}
|
|
1779
|
+
if (claudeHome) {
|
|
1780
|
+
args.push("--claude-home", claudeHome);
|
|
1781
|
+
}
|
|
1782
|
+
if (claudeHookStoreDir) {
|
|
1783
|
+
args.push("--claude-hook-store-dir", claudeHookStoreDir);
|
|
1784
|
+
}
|
|
1785
|
+
const child = spawn(process.execPath, args, {
|
|
1786
|
+
detached: true,
|
|
1787
|
+
stdio: "ignore",
|
|
1788
|
+
cwd: process.cwd(),
|
|
1789
|
+
env: environmentWithoutGatewayTokens()
|
|
1790
|
+
});
|
|
1791
|
+
child.unref();
|
|
1792
|
+
return child;
|
|
1793
|
+
}
|
|
1794
|
+
function ensureTerminalBridgeMonitorAfterApproval({ conversation, statePath, logPath, terminalControl, options, reason = "approval_resolved" }) {
|
|
1795
|
+
const nativeTakeover = isRecord(conversation.native_session_takeover)
|
|
1796
|
+
? conversation.native_session_takeover
|
|
1797
|
+
: undefined;
|
|
1798
|
+
const terminalMessageId = stringValue(nativeTakeover?.terminal_bridge_message_id);
|
|
1799
|
+
const activeMonitor = terminalMessageId
|
|
1800
|
+
? activeTerminalBridgeMonitorOwner(statePath, terminalMessageId)
|
|
1801
|
+
: undefined;
|
|
1802
|
+
const launchedMonitor = activeMonitor
|
|
1803
|
+
? undefined
|
|
1804
|
+
: startTerminalBridgeMonitorForConversation({
|
|
1805
|
+
conversation,
|
|
1806
|
+
statePath,
|
|
1807
|
+
logPath,
|
|
1808
|
+
options
|
|
1809
|
+
});
|
|
1810
|
+
const handoffWatchdog = activeMonitor
|
|
1811
|
+
? startTerminalBridgeMonitorHandoffWatchdog({
|
|
1812
|
+
conversation,
|
|
1813
|
+
statePath,
|
|
1814
|
+
logPath,
|
|
1815
|
+
options
|
|
1816
|
+
})
|
|
1817
|
+
: undefined;
|
|
1818
|
+
const monitorPid = activeMonitor?.ownerPid ?? launchedMonitor?.pid;
|
|
1819
|
+
const agentTimeoutMinutes = Number(options.agentTimeoutMinutes ??
|
|
1820
|
+
nativeTakeover?.terminal_bridge_inactivity_timeout_minutes ??
|
|
1821
|
+
DEFAULT_AGENT_TIMEOUT_MINUTES);
|
|
1822
|
+
const agentHardTimeoutMinutes = Number(options.agentHardTimeoutMinutes ??
|
|
1823
|
+
nativeTakeover?.terminal_bridge_hard_timeout_minutes ??
|
|
1824
|
+
DEFAULT_AGENT_HARD_TIMEOUT_MINUTES);
|
|
1825
|
+
if (activeMonitor) {
|
|
1826
|
+
appendEvent(logPath, {
|
|
1827
|
+
ts: new Date().toISOString(),
|
|
1828
|
+
conversation_id: conversation.conversation_id,
|
|
1829
|
+
event: "terminal_bridge_monitor_reused",
|
|
1830
|
+
pid: activeMonitor.ownerPid ?? null,
|
|
1831
|
+
terminal_control: terminalControl,
|
|
1832
|
+
reason,
|
|
1833
|
+
agent_timeout_minutes: agentTimeoutMinutes,
|
|
1834
|
+
agent_hard_timeout_minutes: agentHardTimeoutMinutes
|
|
1835
|
+
});
|
|
1836
|
+
runtimeLog("info", "terminal_bridge_monitor_reused", {
|
|
1837
|
+
conversation_id: conversation.conversation_id,
|
|
1838
|
+
monitor_pid: activeMonitor.ownerPid ?? null,
|
|
1839
|
+
terminal_target: terminalControl.target,
|
|
1840
|
+
reason
|
|
1841
|
+
});
|
|
1842
|
+
if (handoffWatchdog) {
|
|
1843
|
+
appendEvent(logPath, {
|
|
1844
|
+
ts: new Date().toISOString(),
|
|
1845
|
+
conversation_id: conversation.conversation_id,
|
|
1846
|
+
event: "terminal_bridge_monitor_handoff_watchdog_launch",
|
|
1847
|
+
pid: handoffWatchdog.pid ?? null,
|
|
1848
|
+
monitor_owner_pid: activeMonitor.ownerPid ?? null,
|
|
1849
|
+
terminal_bridge_message_id: terminalMessageId,
|
|
1850
|
+
terminal_control: terminalControl,
|
|
1851
|
+
reason
|
|
1852
|
+
});
|
|
1853
|
+
runtimeLog("info", "terminal_bridge_monitor_handoff_watchdog_launch", {
|
|
1854
|
+
conversation_id: conversation.conversation_id,
|
|
1855
|
+
watchdog_pid: handoffWatchdog.pid ?? null,
|
|
1856
|
+
monitor_owner_pid: activeMonitor.ownerPid ?? null,
|
|
1857
|
+
terminal_target: terminalControl.target,
|
|
1858
|
+
reason
|
|
1859
|
+
});
|
|
1860
|
+
}
|
|
1861
|
+
}
|
|
1862
|
+
else if (launchedMonitor) {
|
|
1863
|
+
appendEvent(logPath, {
|
|
1864
|
+
ts: new Date().toISOString(),
|
|
1865
|
+
conversation_id: conversation.conversation_id,
|
|
1866
|
+
event: "terminal_bridge_monitor_launch",
|
|
1867
|
+
pid: launchedMonitor.pid ?? null,
|
|
1868
|
+
terminal_control: terminalControl,
|
|
1869
|
+
reason,
|
|
1870
|
+
agent_timeout_minutes: agentTimeoutMinutes,
|
|
1871
|
+
agent_hard_timeout_minutes: agentHardTimeoutMinutes
|
|
1872
|
+
});
|
|
1873
|
+
runtimeLog("info", "terminal_bridge_monitor_launch", {
|
|
1874
|
+
conversation_id: conversation.conversation_id,
|
|
1875
|
+
monitor_pid: launchedMonitor.pid ?? null,
|
|
1876
|
+
terminal_target: terminalControl.target,
|
|
1877
|
+
reason
|
|
1878
|
+
});
|
|
1879
|
+
}
|
|
1880
|
+
return {
|
|
1881
|
+
activeMonitor,
|
|
1882
|
+
launchedMonitor,
|
|
1883
|
+
handoffWatchdog,
|
|
1884
|
+
monitorPid
|
|
1885
|
+
};
|
|
1886
|
+
}
|
|
1717
1887
|
function terminalBridgeEnabled(conversation) {
|
|
1718
1888
|
const nativeTakeover = isRecord(conversation.native_session_takeover)
|
|
1719
1889
|
? conversation.native_session_takeover
|
|
@@ -2434,6 +2604,24 @@ function terminalBridgeApprovalFingerprint({ terminalControl, terminalStatus })
|
|
|
2434
2604
|
}))
|
|
2435
2605
|
.digest("hex");
|
|
2436
2606
|
}
|
|
2607
|
+
function claudeTranscriptApprovalIdentity(approvalState) {
|
|
2608
|
+
const policyEvidence = isRecord(approvalState?.policy_evidence)
|
|
2609
|
+
? approvalState.policy_evidence
|
|
2610
|
+
: undefined;
|
|
2611
|
+
const requestId = stringValue(policyEvidence?.request_id);
|
|
2612
|
+
const evidenceFingerprint = stringValue(policyEvidence?.evidence_fingerprint);
|
|
2613
|
+
if (policyEvidence?.source !== "claude_transcript" ||
|
|
2614
|
+
policyEvidence?.kind !== "run_command" ||
|
|
2615
|
+
!requestId ||
|
|
2616
|
+
!evidenceFingerprint ||
|
|
2617
|
+
!/^[0-9a-f]{64}$/u.test(evidenceFingerprint)) {
|
|
2618
|
+
return undefined;
|
|
2619
|
+
}
|
|
2620
|
+
return {
|
|
2621
|
+
requestId,
|
|
2622
|
+
evidenceFingerprint
|
|
2623
|
+
};
|
|
2624
|
+
}
|
|
2437
2625
|
function assertSafeClaudeTerminalSend(terminalStatus) {
|
|
2438
2626
|
const approval = isRecord(terminalStatus?.approval_state)
|
|
2439
2627
|
? terminalStatus.approval_state
|
|
@@ -2470,6 +2658,8 @@ function terminalBridgeApprovalInstructions({ conversation, terminalControl, ter
|
|
|
2470
2658
|
const requestDetail = stringValue(approval.request_detail);
|
|
2471
2659
|
const requestId = stringValue(approval.request_id);
|
|
2472
2660
|
const excerpt = stringValue(screen.excerpt) || "(No terminal excerpt was available.)";
|
|
2661
|
+
const requiresDirectTerminalReview = executor.kind === "claude" &&
|
|
2662
|
+
decisionMode === "keys";
|
|
2473
2663
|
return [
|
|
2474
2664
|
`${agentName} is waiting for approval in a terminal-controlled AKK session.`,
|
|
2475
2665
|
"",
|
|
@@ -2487,6 +2677,13 @@ function terminalBridgeApprovalInstructions({ conversation, terminalControl, ter
|
|
|
2487
2677
|
excerpt,
|
|
2488
2678
|
"```",
|
|
2489
2679
|
"",
|
|
2680
|
+
requiresDirectTerminalReview
|
|
2681
|
+
? `Before asking for approval, have the user personally inspect the live ${terminalControl.kind} pane ${terminalControl.target}.`
|
|
2682
|
+
: undefined,
|
|
2683
|
+
requiresDirectTerminalReview
|
|
2684
|
+
? "This hookless callback intentionally omits raw command details; do not approve from the hash or summary alone."
|
|
2685
|
+
: undefined,
|
|
2686
|
+
requiresDirectTerminalReview ? "" : undefined,
|
|
2490
2687
|
`Ask the user whether to approve or deny this ${agentName} request.`,
|
|
2491
2688
|
"",
|
|
2492
2689
|
"If the user approves, call `agent_knock_knock_approve` with:",
|
|
@@ -2534,13 +2731,86 @@ function recordTerminalBridgeApprovalNotification({ statePath, logPath, terminal
|
|
|
2534
2731
|
const nativeTakeover = isRecord(conversation.native_session_takeover)
|
|
2535
2732
|
? { ...conversation.native_session_takeover }
|
|
2536
2733
|
: {};
|
|
2734
|
+
const approvalScreenDigest = stringValue(isRecord(terminalStatus?.screen)
|
|
2735
|
+
? terminalStatus.screen.digest
|
|
2736
|
+
: undefined);
|
|
2537
2737
|
const previousApproval = isRecord(nativeTakeover.terminal_bridge_approval)
|
|
2538
2738
|
? nativeTakeover.terminal_bridge_approval
|
|
2539
2739
|
: undefined;
|
|
2540
2740
|
const previousNotifiedAt = validTimestampMs(previousApproval?.notified_at);
|
|
2741
|
+
const callbackDelivery = isRecord(conversation.callback_delivery)
|
|
2742
|
+
? conversation.callback_delivery
|
|
2743
|
+
: undefined;
|
|
2744
|
+
const callbackMessage = isRecord(callbackDelivery?.message)
|
|
2745
|
+
? callbackDelivery.message
|
|
2746
|
+
: undefined;
|
|
2747
|
+
const previousCallbackMessageId = stringValue(previousApproval?.callback_message_id);
|
|
2748
|
+
const matchingApprovalOutbox = callbackDelivery?.kind === "approval_notification" &&
|
|
2749
|
+
previousCallbackMessageId !== undefined &&
|
|
2750
|
+
callbackMessage?.id === previousCallbackMessageId;
|
|
2751
|
+
const callbackDeliveryStatus = stringValue(callbackDelivery?.status);
|
|
2752
|
+
const callbackDeliveryAttempts = Number(callbackDelivery?.attempts ?? 0);
|
|
2753
|
+
const conflictingActiveOutbox = !matchingApprovalOutbox &&
|
|
2754
|
+
(callbackDeliveryStatus === "pending" ||
|
|
2755
|
+
(callbackDeliveryStatus === "failed" &&
|
|
2756
|
+
Number.isFinite(callbackDeliveryAttempts) &&
|
|
2757
|
+
callbackDeliveryAttempts <= CALLBACK_RETRY_DELAYS_MS.length));
|
|
2541
2758
|
if (previousApproval?.fingerprint === fingerprint &&
|
|
2542
2759
|
previousNotifiedAt !== undefined &&
|
|
2543
2760
|
Date.now() - previousNotifiedAt <= CLAUDE_SCREEN_APPROVAL_TTL_MS) {
|
|
2761
|
+
if (conflictingActiveOutbox) {
|
|
2762
|
+
return {
|
|
2763
|
+
conversation,
|
|
2764
|
+
duplicate: false,
|
|
2765
|
+
stale: true,
|
|
2766
|
+
deferred: true,
|
|
2767
|
+
previousApproval,
|
|
2768
|
+
recorded: undefined
|
|
2769
|
+
};
|
|
2770
|
+
}
|
|
2771
|
+
if (!matchingApprovalOutbox && !conflictingActiveOutbox) {
|
|
2772
|
+
const recoveryMessageId = previousCallbackMessageId ?? `msg-${randomUUID()}`;
|
|
2773
|
+
const recoveryMessageTs = stringValue(previousApproval?.callback_message_ts) ??
|
|
2774
|
+
stringValue(previousApproval?.notified_at) ??
|
|
2775
|
+
new Date().toISOString();
|
|
2776
|
+
const recoveryConversation = previousCallbackMessageId
|
|
2777
|
+
? conversation
|
|
2778
|
+
: {
|
|
2779
|
+
...conversation,
|
|
2780
|
+
native_session_takeover: {
|
|
2781
|
+
...nativeTakeover,
|
|
2782
|
+
terminal_bridge_approval: {
|
|
2783
|
+
...previousApproval,
|
|
2784
|
+
callback_message_id: recoveryMessageId,
|
|
2785
|
+
callback_message_ts: recoveryMessageTs
|
|
2786
|
+
}
|
|
2787
|
+
}
|
|
2788
|
+
};
|
|
2789
|
+
if (!previousCallbackMessageId) {
|
|
2790
|
+
saveState(statePath, recoveryConversation);
|
|
2791
|
+
}
|
|
2792
|
+
const recorded = onRecorded?.(recoveryConversation, {
|
|
2793
|
+
recoverMissingOutbox: true
|
|
2794
|
+
});
|
|
2795
|
+
appendEvent(logPath, {
|
|
2796
|
+
ts: new Date().toISOString(),
|
|
2797
|
+
conversation_id: recoveryConversation.conversation_id,
|
|
2798
|
+
event: "terminal_bridge_approval_notification_outbox_recovered",
|
|
2799
|
+
terminal_control: terminalControl,
|
|
2800
|
+
fingerprint,
|
|
2801
|
+
callback_message_id: recoveryMessageId
|
|
2802
|
+
});
|
|
2803
|
+
return {
|
|
2804
|
+
conversation: isRecord(recorded) && isRecord(recorded.prepared)
|
|
2805
|
+
? recorded.prepared.conversation
|
|
2806
|
+
: recoveryConversation,
|
|
2807
|
+
duplicate: false,
|
|
2808
|
+
recovered: true,
|
|
2809
|
+
stale: false,
|
|
2810
|
+
previousApproval,
|
|
2811
|
+
recorded
|
|
2812
|
+
};
|
|
2813
|
+
}
|
|
2544
2814
|
return {
|
|
2545
2815
|
conversation,
|
|
2546
2816
|
duplicate: true,
|
|
@@ -2550,38 +2820,113 @@ function recordTerminalBridgeApprovalNotification({ statePath, logPath, terminal
|
|
|
2550
2820
|
};
|
|
2551
2821
|
}
|
|
2552
2822
|
const now = new Date().toISOString();
|
|
2823
|
+
const callbackMessageId = `msg-${randomUUID()}`;
|
|
2553
2824
|
const nextConversation = {
|
|
2554
2825
|
...conversation,
|
|
2555
2826
|
native_session_takeover: {
|
|
2556
2827
|
...nativeTakeover,
|
|
2557
2828
|
terminal_bridge_approval: {
|
|
2558
2829
|
fingerprint,
|
|
2830
|
+
screen_digest: approvalScreenDigest,
|
|
2559
2831
|
notified_at: now,
|
|
2560
2832
|
terminal_control: terminalControl,
|
|
2561
|
-
approval_state: terminalStatus.approval_state
|
|
2833
|
+
approval_state: terminalStatus.approval_state,
|
|
2834
|
+
callback_message_id: callbackMessageId,
|
|
2835
|
+
callback_message_ts: now
|
|
2562
2836
|
}
|
|
2563
2837
|
},
|
|
2564
2838
|
updated_at: now
|
|
2565
2839
|
};
|
|
2840
|
+
// Persist the stable callback identity before any message/outbox event. If
|
|
2841
|
+
// the process exits during callback preparation, recovery can recreate the
|
|
2842
|
+
// exact same message and safely finish the outbox transaction.
|
|
2566
2843
|
saveState(statePath, nextConversation);
|
|
2567
2844
|
appendEvent(logPath, {
|
|
2568
2845
|
ts: now,
|
|
2569
2846
|
conversation_id: conversation.conversation_id,
|
|
2570
2847
|
event: "terminal_bridge_approval_notification_recorded",
|
|
2571
2848
|
terminal_control: terminalControl,
|
|
2572
|
-
fingerprint
|
|
2849
|
+
fingerprint,
|
|
2850
|
+
screen_digest: approvalScreenDigest
|
|
2573
2851
|
});
|
|
2852
|
+
const recorded = onRecorded?.(nextConversation);
|
|
2574
2853
|
return {
|
|
2575
|
-
conversation:
|
|
2854
|
+
conversation: isRecord(recorded) && isRecord(recorded.prepared)
|
|
2855
|
+
? recorded.prepared.conversation
|
|
2856
|
+
: nextConversation,
|
|
2576
2857
|
duplicate: false,
|
|
2577
2858
|
stale: false,
|
|
2578
|
-
recorded
|
|
2859
|
+
recorded
|
|
2860
|
+
};
|
|
2861
|
+
}
|
|
2862
|
+
finally {
|
|
2863
|
+
releaseLock();
|
|
2864
|
+
}
|
|
2865
|
+
}
|
|
2866
|
+
function markTerminalBridgeApprovalPromptCleared({ statePath, logPath, expectedConversationId, expectedMessageId }) {
|
|
2867
|
+
const releaseLock = acquireFileLock(`${statePath}.lock`);
|
|
2868
|
+
try {
|
|
2869
|
+
const conversation = loadState(statePath);
|
|
2870
|
+
const nativeTakeover = isRecord(conversation.native_session_takeover)
|
|
2871
|
+
? conversation.native_session_takeover
|
|
2872
|
+
: undefined;
|
|
2873
|
+
if (conversation.conversation_id !== expectedConversationId ||
|
|
2874
|
+
conversation.status !== "waiting_for_agent" ||
|
|
2875
|
+
nativeTakeover?.terminal_bridge !== true ||
|
|
2876
|
+
stringValue(nativeTakeover.terminal_bridge_message_id) !==
|
|
2877
|
+
expectedMessageId ||
|
|
2878
|
+
stringValue(nativeTakeover.terminal_bridge_last_approval_message_id) !==
|
|
2879
|
+
expectedMessageId ||
|
|
2880
|
+
validTimestampMs(nativeTakeover.terminal_bridge_approval_resolved_at) === undefined ||
|
|
2881
|
+
validTimestampMs(nativeTakeover.terminal_bridge_last_approval_prompt_cleared_at) !== undefined) {
|
|
2882
|
+
return {
|
|
2883
|
+
conversation,
|
|
2884
|
+
marked: false
|
|
2885
|
+
};
|
|
2886
|
+
}
|
|
2887
|
+
const clearedAt = new Date().toISOString();
|
|
2888
|
+
const nextConversation = {
|
|
2889
|
+
...conversation,
|
|
2890
|
+
native_session_takeover: {
|
|
2891
|
+
...nativeTakeover,
|
|
2892
|
+
terminal_bridge_last_approval_prompt_cleared_at: clearedAt
|
|
2893
|
+
},
|
|
2894
|
+
updated_at: clearedAt
|
|
2895
|
+
};
|
|
2896
|
+
saveState(statePath, nextConversation);
|
|
2897
|
+
appendEvent(logPath, {
|
|
2898
|
+
ts: clearedAt,
|
|
2899
|
+
conversation_id: conversation.conversation_id,
|
|
2900
|
+
event: "terminal_bridge_approval_prompt_cleared",
|
|
2901
|
+
terminal_bridge_message_id: expectedMessageId
|
|
2902
|
+
});
|
|
2903
|
+
return {
|
|
2904
|
+
conversation: nextConversation,
|
|
2905
|
+
marked: true
|
|
2579
2906
|
};
|
|
2580
2907
|
}
|
|
2581
2908
|
finally {
|
|
2582
2909
|
releaseLock();
|
|
2583
2910
|
}
|
|
2584
2911
|
}
|
|
2912
|
+
function terminalBridgeApprovalCallbackIdentity(conversation) {
|
|
2913
|
+
const nativeTakeover = isRecord(conversation?.native_session_takeover)
|
|
2914
|
+
? conversation.native_session_takeover
|
|
2915
|
+
: undefined;
|
|
2916
|
+
const approval = isRecord(nativeTakeover?.terminal_bridge_approval)
|
|
2917
|
+
? nativeTakeover.terminal_bridge_approval
|
|
2918
|
+
: undefined;
|
|
2919
|
+
const id = stringValue(approval?.callback_message_id);
|
|
2920
|
+
const timestamp = stringValue(approval?.callback_message_ts);
|
|
2921
|
+
const timestampMs = validTimestampMs(timestamp);
|
|
2922
|
+
if (!id || timestampMs === undefined) {
|
|
2923
|
+
throw new Error("terminal approval notification has no stable callback identity");
|
|
2924
|
+
}
|
|
2925
|
+
return {
|
|
2926
|
+
id,
|
|
2927
|
+
now: new Date(timestampMs)
|
|
2928
|
+
};
|
|
2929
|
+
}
|
|
2585
2930
|
function prepareManagedSend({ options, statePath, logPath, messageBody, stateLockHeld = false, persist = true, rejectTerminalControl = false }) {
|
|
2586
2931
|
if (!stateLockHeld) {
|
|
2587
2932
|
const releaseLock = acquireFileLock(`${statePath}.lock`);
|
|
@@ -3129,6 +3474,7 @@ async function runApprove(options) {
|
|
|
3129
3474
|
const suppliedExpectedFingerprint = stringValue(options.expectedApprovalFingerprint);
|
|
3130
3475
|
const expectedFingerprint = suppliedExpectedFingerprint ??
|
|
3131
3476
|
stringValue(monitoredApproval?.fingerprint);
|
|
3477
|
+
const autoApproved = options.autoApproved === true;
|
|
3132
3478
|
const hooklessClaudeScreenApproval = executor.kind === "claude" &&
|
|
3133
3479
|
nativeTakeover?.claude_hook_mode !== "enabled";
|
|
3134
3480
|
if (hooklessClaudeScreenApproval) {
|
|
@@ -3138,6 +3484,39 @@ async function runApprove(options) {
|
|
|
3138
3484
|
const pendingDispatch = isRecord(nativeTakeover?.terminal_bridge_approval_dispatch)
|
|
3139
3485
|
? nativeTakeover.terminal_bridge_approval_dispatch
|
|
3140
3486
|
: undefined;
|
|
3487
|
+
const lastApprovalFingerprint = stringValue(nativeTakeover?.terminal_bridge_last_approval_fingerprint);
|
|
3488
|
+
const lastApprovalMessageId = stringValue(nativeTakeover?.terminal_bridge_last_approval_message_id);
|
|
3489
|
+
const currentMessageId = stringValue(nativeTakeover?.terminal_bridge_message_id);
|
|
3490
|
+
const approvalResolvedAt = validTimestampMs(nativeTakeover?.terminal_bridge_approval_resolved_at);
|
|
3491
|
+
if (autoApproved &&
|
|
3492
|
+
monitoredApproval === undefined &&
|
|
3493
|
+
pendingDispatch === undefined &&
|
|
3494
|
+
conversation.status === "waiting_for_agent" &&
|
|
3495
|
+
suppliedExpectedFingerprint !== undefined &&
|
|
3496
|
+
suppliedExpectedFingerprint === lastApprovalFingerprint &&
|
|
3497
|
+
lastApprovalMessageId !== undefined &&
|
|
3498
|
+
lastApprovalMessageId === currentMessageId &&
|
|
3499
|
+
approvalResolvedAt !== undefined) {
|
|
3500
|
+
const monitor = ensureTerminalBridgeMonitorAfterApproval({
|
|
3501
|
+
conversation,
|
|
3502
|
+
statePath,
|
|
3503
|
+
logPath,
|
|
3504
|
+
terminalControl,
|
|
3505
|
+
options,
|
|
3506
|
+
reason: "approval_already_resolved"
|
|
3507
|
+
});
|
|
3508
|
+
printJson({
|
|
3509
|
+
conversation,
|
|
3510
|
+
approved: false,
|
|
3511
|
+
already_approved: true,
|
|
3512
|
+
blocked: false,
|
|
3513
|
+
reason: "Claude screen approval fingerprint was already consumed",
|
|
3514
|
+
terminal_control: terminalControl,
|
|
3515
|
+
monitor_pid: monitor.monitorPid ?? null,
|
|
3516
|
+
monitor_handoff_pid: monitor.handoffWatchdog?.pid ?? null
|
|
3517
|
+
});
|
|
3518
|
+
return;
|
|
3519
|
+
}
|
|
3141
3520
|
const notifiedAt = validTimestampMs(monitoredApproval?.notified_at);
|
|
3142
3521
|
if (conversation.status !== "waiting_for_openclaw" ||
|
|
3143
3522
|
monitoredState?.decision_mode !== "keys" ||
|
|
@@ -3197,7 +3576,6 @@ async function runApprove(options) {
|
|
|
3197
3576
|
return;
|
|
3198
3577
|
}
|
|
3199
3578
|
}
|
|
3200
|
-
const autoApproved = options.autoApproved === true;
|
|
3201
3579
|
const policyRuleId = stringValue(options.policyRuleId);
|
|
3202
3580
|
const policyFingerprint = stringValue(options.policyFingerprint);
|
|
3203
3581
|
const autoApprovalPolicy = autoApproved
|
|
@@ -3205,6 +3583,28 @@ async function runApprove(options) {
|
|
|
3205
3583
|
: undefined;
|
|
3206
3584
|
const runtimeIdentity = terminalRuntimeIdentityForConversation(conversation, terminalControl);
|
|
3207
3585
|
let executorPolicyDecision;
|
|
3586
|
+
const policyCandidateForInspection = ({ agent, currentTerminalControl, inspection, fingerprint }) => {
|
|
3587
|
+
const evidence = inspection.approval.approvable
|
|
3588
|
+
? inspection.approval.policyEvidence
|
|
3589
|
+
: undefined;
|
|
3590
|
+
return {
|
|
3591
|
+
agent,
|
|
3592
|
+
kind: evidence?.kind ?? inspection.approval.promptKind ?? "unknown",
|
|
3593
|
+
decisionMode: inspection.approval.approvable
|
|
3594
|
+
? inspection.approval.action.mode ?? "keys"
|
|
3595
|
+
: undefined,
|
|
3596
|
+
command: evidence?.command ?? inspection.approval.command,
|
|
3597
|
+
cwd: evidence?.cwd ?? inspection.approval.cwd ?? currentTerminalControl.currentPath,
|
|
3598
|
+
fingerprint: fingerprint ?? "",
|
|
3599
|
+
terminalTarget: currentTerminalControl.target,
|
|
3600
|
+
...(evidence?.source === "claude_transcript"
|
|
3601
|
+
? {
|
|
3602
|
+
evidenceSource: "claude_transcript",
|
|
3603
|
+
evidenceFingerprint: evidence.evidenceFingerprint
|
|
3604
|
+
}
|
|
3605
|
+
: {})
|
|
3606
|
+
};
|
|
3607
|
+
};
|
|
3208
3608
|
const releaseTerminalLock = acquireFileLock(terminalBridgeSendLockPath(storeDirFromOptions(options), terminalControl), { timeoutMs: 30000 });
|
|
3209
3609
|
let terminalLockReleased = false;
|
|
3210
3610
|
const releaseApprovalTerminalLock = () => {
|
|
@@ -3245,8 +3645,11 @@ async function runApprove(options) {
|
|
|
3245
3645
|
expectedFingerprint,
|
|
3246
3646
|
scrollbackLines: Number(options.scrollbackLines ?? 120),
|
|
3247
3647
|
runtime: runtimeIdentity,
|
|
3648
|
+
managedRequest: terminalDurableRequestForConversation(currentConversation, terminalControl),
|
|
3248
3649
|
requiredDecisionMode: autoApproved && executor.kind === "claude"
|
|
3249
|
-
?
|
|
3650
|
+
? hooklessClaudeScreenApproval
|
|
3651
|
+
? "keys"
|
|
3652
|
+
: "structured"
|
|
3250
3653
|
: undefined,
|
|
3251
3654
|
authorize: autoApproved
|
|
3252
3655
|
? ({ agent, terminalControl: currentTerminalControl, inspection, fingerprint }) => {
|
|
@@ -3256,17 +3659,12 @@ async function runApprove(options) {
|
|
|
3256
3659
|
reason: "automatic approval requires an executor-side policy"
|
|
3257
3660
|
};
|
|
3258
3661
|
}
|
|
3259
|
-
const candidate = {
|
|
3662
|
+
const candidate = policyCandidateForInspection({
|
|
3260
3663
|
agent,
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
command: inspection.approval.command,
|
|
3266
|
-
cwd: inspection.approval.cwd ?? currentTerminalControl.currentPath,
|
|
3267
|
-
fingerprint: fingerprint ?? "",
|
|
3268
|
-
terminalTarget: currentTerminalControl.target
|
|
3269
|
-
};
|
|
3664
|
+
currentTerminalControl,
|
|
3665
|
+
inspection,
|
|
3666
|
+
fingerprint
|
|
3667
|
+
});
|
|
3270
3668
|
executorPolicyDecision = evaluateApprovalPolicy({
|
|
3271
3669
|
policy: autoApprovalPolicy,
|
|
3272
3670
|
candidate
|
|
@@ -3294,7 +3692,36 @@ async function runApprove(options) {
|
|
|
3294
3692
|
}
|
|
3295
3693
|
: undefined,
|
|
3296
3694
|
beforeKeyDispatch: hooklessClaudeScreenApproval
|
|
3297
|
-
? ({ fingerprint, terminalControl: dispatchControl, keys }) => {
|
|
3695
|
+
? ({ fingerprint, terminalControl: dispatchControl, inspection, keys }) => {
|
|
3696
|
+
if (autoApproved) {
|
|
3697
|
+
if (!autoApprovalPolicy) {
|
|
3698
|
+
throw new Error("automatic approval requires an executor-side policy before dispatch");
|
|
3699
|
+
}
|
|
3700
|
+
const freshPolicyDecision = evaluateApprovalPolicy({
|
|
3701
|
+
policy: autoApprovalPolicy,
|
|
3702
|
+
candidate: policyCandidateForInspection({
|
|
3703
|
+
agent: executor.kind,
|
|
3704
|
+
currentTerminalControl: dispatchControl,
|
|
3705
|
+
inspection,
|
|
3706
|
+
fingerprint
|
|
3707
|
+
})
|
|
3708
|
+
});
|
|
3709
|
+
if (freshPolicyDecision.action !== "approve") {
|
|
3710
|
+
throw new Error(`executor-side auto-approval policy rejected the recaptured request: ${freshPolicyDecision.reason}`);
|
|
3711
|
+
}
|
|
3712
|
+
if (executorPolicyDecision?.ruleId &&
|
|
3713
|
+
freshPolicyDecision.ruleId !== executorPolicyDecision.ruleId) {
|
|
3714
|
+
throw new Error("executor-side auto-approval rule changed after recapture");
|
|
3715
|
+
}
|
|
3716
|
+
if (policyRuleId && freshPolicyDecision.ruleId !== policyRuleId) {
|
|
3717
|
+
throw new Error("executor-side auto-approval rule changed before dispatch");
|
|
3718
|
+
}
|
|
3719
|
+
if (policyFingerprint &&
|
|
3720
|
+
freshPolicyDecision.policyFingerprint !== policyFingerprint) {
|
|
3721
|
+
throw new Error("executor-side auto-approval policy changed before dispatch");
|
|
3722
|
+
}
|
|
3723
|
+
executorPolicyDecision = freshPolicyDecision;
|
|
3724
|
+
}
|
|
3298
3725
|
if (releaseStateLock) {
|
|
3299
3726
|
throw new Error("Claude approval dispatch was already reserved");
|
|
3300
3727
|
}
|
|
@@ -3308,6 +3735,15 @@ async function runApprove(options) {
|
|
|
3308
3735
|
? latestTakeover.terminal_bridge_approval
|
|
3309
3736
|
: undefined;
|
|
3310
3737
|
const latestNotifiedAt = validTimestampMs(latestApproval?.notified_at);
|
|
3738
|
+
const latestApprovalState = isRecord(latestApproval?.approval_state)
|
|
3739
|
+
? latestApproval.approval_state
|
|
3740
|
+
: undefined;
|
|
3741
|
+
const latestPolicyEvidence = isRecord(latestApprovalState?.policy_evidence)
|
|
3742
|
+
? latestApprovalState.policy_evidence
|
|
3743
|
+
: undefined;
|
|
3744
|
+
const recapturedPolicyEvidence = inspection.approval.approvable
|
|
3745
|
+
? inspection.approval.policyEvidence
|
|
3746
|
+
: undefined;
|
|
3311
3747
|
const latestDispatch = isRecord(latestTakeover?.terminal_bridge_approval_dispatch)
|
|
3312
3748
|
? latestTakeover.terminal_bridge_approval_dispatch
|
|
3313
3749
|
: undefined;
|
|
@@ -3320,7 +3756,11 @@ async function runApprove(options) {
|
|
|
3320
3756
|
Date.now() - latestNotifiedAt > CLAUDE_SCREEN_APPROVAL_TTL_MS ||
|
|
3321
3757
|
expectedFingerprint !== fingerprint ||
|
|
3322
3758
|
latestControl?.target !== dispatchControl.target ||
|
|
3323
|
-
latestControl?.socketPath !== dispatchControl.socketPath
|
|
3759
|
+
latestControl?.socketPath !== dispatchControl.socketPath ||
|
|
3760
|
+
(autoApproved &&
|
|
3761
|
+
(latestPolicyEvidence?.source !== "claude_transcript" ||
|
|
3762
|
+
latestPolicyEvidence.evidence_fingerprint !==
|
|
3763
|
+
recapturedPolicyEvidence?.evidenceFingerprint))) {
|
|
3324
3764
|
throw new Error("approval state changed before terminal dispatch; refresh status and retry");
|
|
3325
3765
|
}
|
|
3326
3766
|
if (latestDispatch?.state === "reserved" &&
|
|
@@ -3425,6 +3865,14 @@ async function runApprove(options) {
|
|
|
3425
3865
|
const nativeTakeoverForUpdate = isRecord(lockedConversation.native_session_takeover)
|
|
3426
3866
|
? { ...lockedConversation.native_session_takeover }
|
|
3427
3867
|
: {};
|
|
3868
|
+
const resolvedApproval = isRecord(nativeTakeoverForUpdate.terminal_bridge_approval)
|
|
3869
|
+
? nativeTakeoverForUpdate.terminal_bridge_approval
|
|
3870
|
+
: undefined;
|
|
3871
|
+
const resolvedApprovalScreenDigest = stringValue(resolvedApproval?.screen_digest);
|
|
3872
|
+
const resolvedApprovalState = isRecord(resolvedApproval?.approval_state)
|
|
3873
|
+
? resolvedApproval.approval_state
|
|
3874
|
+
: undefined;
|
|
3875
|
+
const resolvedTranscriptIdentity = claudeTranscriptApprovalIdentity(resolvedApprovalState);
|
|
3428
3876
|
const approvalResolvedAt = new Date().toISOString();
|
|
3429
3877
|
const agentTimeoutMinutes = Number(options.agentTimeoutMinutes ??
|
|
3430
3878
|
nativeTakeoverForUpdate.terminal_bridge_inactivity_timeout_minutes ??
|
|
@@ -3438,7 +3886,12 @@ async function runApprove(options) {
|
|
|
3438
3886
|
terminal_bridge_approval_dispatch: undefined,
|
|
3439
3887
|
terminal_bridge_approval_resolved_at: approvalResolvedAt,
|
|
3440
3888
|
terminal_bridge_last_approval_fingerprint: actualFingerprint,
|
|
3889
|
+
terminal_bridge_last_approval_screen_digest: resolvedApprovalScreenDigest,
|
|
3890
|
+
terminal_bridge_last_approval_request_id: resolvedTranscriptIdentity?.requestId,
|
|
3891
|
+
terminal_bridge_last_approval_evidence_fingerprint: resolvedTranscriptIdentity?.evidenceFingerprint,
|
|
3892
|
+
terminal_bridge_last_approval_prompt_cleared_at: undefined,
|
|
3441
3893
|
terminal_bridge_last_approval_at: approvalResolvedAt,
|
|
3894
|
+
terminal_bridge_last_approval_message_id: nativeTakeoverForUpdate.terminal_bridge_message_id,
|
|
3442
3895
|
terminal_bridge_monitor_lock_version: TERMINAL_BRIDGE_MONITOR_LOCK_VERSION,
|
|
3443
3896
|
terminal_bridge_monitor_started_at: approvalResolvedAt,
|
|
3444
3897
|
terminal_bridge_last_activity_at: approvalResolvedAt,
|
|
@@ -3450,6 +3903,7 @@ async function runApprove(options) {
|
|
|
3450
3903
|
};
|
|
3451
3904
|
delete nextNativeTakeover.terminal_bridge_approval;
|
|
3452
3905
|
delete nextNativeTakeover.terminal_bridge_approval_dispatch;
|
|
3906
|
+
delete nextNativeTakeover.terminal_bridge_last_approval_prompt_cleared_at;
|
|
3453
3907
|
let nextConversation = {
|
|
3454
3908
|
...lockedConversation,
|
|
3455
3909
|
status: terminalBridgeEnabled(lockedConversation)
|
|
@@ -3477,30 +3931,13 @@ async function runApprove(options) {
|
|
|
3477
3931
|
saveState(statePath, nextConversation);
|
|
3478
3932
|
releaseApprovalStateLock();
|
|
3479
3933
|
releaseApprovalTerminalLock();
|
|
3480
|
-
const bridgeMonitor =
|
|
3934
|
+
const bridgeMonitor = ensureTerminalBridgeMonitorAfterApproval({
|
|
3481
3935
|
conversation: nextConversation,
|
|
3482
3936
|
statePath,
|
|
3483
3937
|
logPath,
|
|
3938
|
+
terminalControl,
|
|
3484
3939
|
options
|
|
3485
3940
|
});
|
|
3486
|
-
if (bridgeMonitor) {
|
|
3487
|
-
appendEvent(logPath, {
|
|
3488
|
-
ts: new Date().toISOString(),
|
|
3489
|
-
conversation_id: conversation.conversation_id,
|
|
3490
|
-
event: "terminal_bridge_monitor_launch",
|
|
3491
|
-
pid: bridgeMonitor.pid ?? null,
|
|
3492
|
-
terminal_control: terminalControl,
|
|
3493
|
-
reason: "approval_resolved",
|
|
3494
|
-
agent_timeout_minutes: agentTimeoutMinutes,
|
|
3495
|
-
agent_hard_timeout_minutes: agentHardTimeoutMinutes
|
|
3496
|
-
});
|
|
3497
|
-
runtimeLog("info", "terminal_bridge_monitor_launch", {
|
|
3498
|
-
conversation_id: conversation.conversation_id,
|
|
3499
|
-
monitor_pid: bridgeMonitor.pid ?? null,
|
|
3500
|
-
terminal_target: terminalControl.target,
|
|
3501
|
-
reason: "approval_resolved"
|
|
3502
|
-
});
|
|
3503
|
-
}
|
|
3504
3941
|
printJson({
|
|
3505
3942
|
conversation: nextConversation,
|
|
3506
3943
|
approved: true,
|
|
@@ -3513,7 +3950,9 @@ async function runApprove(options) {
|
|
|
3513
3950
|
approval_fingerprint: actualFingerprint,
|
|
3514
3951
|
auto_approved: autoApproved,
|
|
3515
3952
|
policy_rule_id: effectivePolicyRuleId,
|
|
3516
|
-
|
|
3953
|
+
policy_fingerprint: effectivePolicyFingerprint,
|
|
3954
|
+
monitor_pid: bridgeMonitor.monitorPid ?? null,
|
|
3955
|
+
monitor_handoff_pid: bridgeMonitor.handoffWatchdog?.pid ?? null
|
|
3517
3956
|
});
|
|
3518
3957
|
}
|
|
3519
3958
|
finally {
|
|
@@ -4836,7 +5275,9 @@ function prepareCallbackDeliveryReconciliation({ statePath, logPath, delayMs })
|
|
|
4836
5275
|
const callbackDelivery = isRecord(conversation.callback_delivery)
|
|
4837
5276
|
? conversation.callback_delivery
|
|
4838
5277
|
: undefined;
|
|
4839
|
-
|
|
5278
|
+
const preservesConversationStatus = callbackDelivery?.preserve_conversation_status === true;
|
|
5279
|
+
if (!preservesConversationStatus &&
|
|
5280
|
+
!["callback_pending", "callback_failed"].includes(conversation.status)) {
|
|
4840
5281
|
return {
|
|
4841
5282
|
handled: false
|
|
4842
5283
|
};
|
|
@@ -4852,6 +5293,14 @@ function prepareCallbackDeliveryReconciliation({ statePath, logPath, delayMs })
|
|
|
4852
5293
|
reason: "callback_delivery_metadata_missing"
|
|
4853
5294
|
};
|
|
4854
5295
|
}
|
|
5296
|
+
if (!isRetryableCallbackDelivery(conversation, callbackDelivery)) {
|
|
5297
|
+
return {
|
|
5298
|
+
handled: true,
|
|
5299
|
+
conversationId,
|
|
5300
|
+
status: "skipped",
|
|
5301
|
+
reason: "callback_delivery_in_flight"
|
|
5302
|
+
};
|
|
5303
|
+
}
|
|
4855
5304
|
if (!Number.isSafeInteger(attempts) || attempts < 1 ||
|
|
4856
5305
|
attempts > CALLBACK_RETRY_DELAYS_MS.length) {
|
|
4857
5306
|
return {
|
|
@@ -4971,10 +5420,18 @@ function latestTerminalBridgeMonitorLaunchPid(logPath) {
|
|
|
4971
5420
|
const pid = Number(launch?.pid);
|
|
4972
5421
|
return Number.isSafeInteger(pid) && pid > 1 ? pid : undefined;
|
|
4973
5422
|
}
|
|
4974
|
-
function prepareTerminalBridgeMonitorReconciliation({ statePath, expectedMessageId }) {
|
|
5423
|
+
function prepareTerminalBridgeMonitorReconciliation({ statePath, expectedMessageId, requireWaitingForAgentStatus = false }) {
|
|
4975
5424
|
const releaseStateLock = acquireFileLock(`${statePath}.lock`);
|
|
4976
5425
|
try {
|
|
4977
5426
|
const conversation = loadState(statePath);
|
|
5427
|
+
if (requireWaitingForAgentStatus &&
|
|
5428
|
+
conversation.status !== "waiting_for_agent") {
|
|
5429
|
+
return {
|
|
5430
|
+
prepared: false,
|
|
5431
|
+
alreadyRunning: false,
|
|
5432
|
+
reason: `conversation_status_${String(conversation.status ?? "missing")}`
|
|
5433
|
+
};
|
|
5434
|
+
}
|
|
4978
5435
|
const eligibility = terminalBridgeReconciliationEligibility(conversation);
|
|
4979
5436
|
if (!eligibility.eligible) {
|
|
4980
5437
|
return {
|
|
@@ -5495,6 +5952,9 @@ async function runMonitor(options) {
|
|
|
5495
5952
|
if (options.callbackRetry) {
|
|
5496
5953
|
return runCallbackRetryMonitor(options);
|
|
5497
5954
|
}
|
|
5955
|
+
if (options.terminalBridgeHandoff) {
|
|
5956
|
+
return runTerminalBridgeMonitorHandoff(options);
|
|
5957
|
+
}
|
|
5498
5958
|
if (options.terminalBridge) {
|
|
5499
5959
|
return await runTerminalBridgeMonitor(options);
|
|
5500
5960
|
}
|
|
@@ -5657,68 +6117,54 @@ function runCallbackRetryMonitor(options) {
|
|
|
5657
6117
|
? conversation.callback_delivery
|
|
5658
6118
|
: undefined;
|
|
5659
6119
|
const attempts = Number(callbackDelivery?.attempts ?? 0);
|
|
5660
|
-
if (!
|
|
5661
|
-
!
|
|
5662
|
-
!
|
|
6120
|
+
if (!callbackDelivery ||
|
|
6121
|
+
!isRecord(callbackDelivery.message) ||
|
|
6122
|
+
!["pending", "failed"].includes(String(callbackDelivery.status ?? ""))) {
|
|
5663
6123
|
return;
|
|
5664
6124
|
}
|
|
5665
6125
|
if (attempts > CALLBACK_RETRY_DELAYS_MS.length) {
|
|
5666
6126
|
return;
|
|
5667
6127
|
}
|
|
5668
|
-
|
|
5669
|
-
|
|
5670
|
-
|
|
5671
|
-
|
|
5672
|
-
|
|
5673
|
-
if (
|
|
6128
|
+
if (!isRetryableCallbackDelivery(conversation, callbackDelivery)) {
|
|
6129
|
+
const attemptPidValue = Number(callbackDelivery.attempt_pid);
|
|
6130
|
+
const attemptPid = Number.isSafeInteger(attemptPidValue) && attemptPidValue > 0
|
|
6131
|
+
? attemptPidValue
|
|
6132
|
+
: undefined;
|
|
6133
|
+
if (callbackDelivery.status === "pending" &&
|
|
6134
|
+
attemptPid !== undefined &&
|
|
6135
|
+
isProcessAlive(attemptPid)) {
|
|
5674
6136
|
sleepSync(1000);
|
|
5675
6137
|
continue;
|
|
5676
6138
|
}
|
|
5677
|
-
|
|
6139
|
+
return;
|
|
5678
6140
|
}
|
|
5679
6141
|
try {
|
|
5680
|
-
|
|
5681
|
-
|
|
5682
|
-
|
|
5683
|
-
:
|
|
5684
|
-
|
|
5685
|
-
|
|
5686
|
-
|
|
5687
|
-
|
|
5688
|
-
|
|
5689
|
-
|
|
5690
|
-
|
|
5691
|
-
|
|
5692
|
-
|
|
5693
|
-
|
|
5694
|
-
messageJson: JSON.stringify(currentDelivery.message),
|
|
5695
|
-
gatewayMethod: stringValue(currentDelivery.gateway_method) ?? current.gateway_method,
|
|
5696
|
-
gatewaySession: stringValue(currentDelivery.gateway_session) ?? current.gateway_session,
|
|
5697
|
-
openclawSession: current.openclaw_session,
|
|
5698
|
-
openclawBin: stringValue(currentDelivery.openclaw_bin) ?? current.openclaw_bin,
|
|
5699
|
-
gatewayUrl: stringValue(currentDelivery.gateway_url) ?? current.gateway_url,
|
|
5700
|
-
token: current.gateway_token,
|
|
5701
|
-
closeTerminalBridgeOnDone: currentDelivery.close_terminal_bridge_on_done === true,
|
|
5702
|
-
retryPending: true,
|
|
5703
|
-
disableCallbackRetry: true
|
|
5704
|
-
});
|
|
5705
|
-
return;
|
|
5706
|
-
}
|
|
5707
|
-
catch {
|
|
5708
|
-
// The failed attempt is persisted by runLockedCallback; continue with bounded backoff.
|
|
5709
|
-
}
|
|
6142
|
+
runCallbackTransaction({
|
|
6143
|
+
statePath,
|
|
6144
|
+
messageJson: JSON.stringify(callbackDelivery.message),
|
|
6145
|
+
gatewayMethod: stringValue(callbackDelivery.gateway_method) ?? conversation.gateway_method,
|
|
6146
|
+
gatewaySession: stringValue(callbackDelivery.gateway_session) ?? conversation.gateway_session,
|
|
6147
|
+
openclawSession: conversation.openclaw_session,
|
|
6148
|
+
openclawBin: stringValue(callbackDelivery.openclaw_bin) ?? conversation.openclaw_bin,
|
|
6149
|
+
gatewayUrl: stringValue(callbackDelivery.gateway_url) ?? conversation.gateway_url,
|
|
6150
|
+
token: conversation.gateway_token,
|
|
6151
|
+
closeTerminalBridgeOnDone: callbackDelivery.close_terminal_bridge_on_done === true,
|
|
6152
|
+
retryPending: true,
|
|
6153
|
+
disableCallbackRetry: true
|
|
6154
|
+
});
|
|
6155
|
+
return;
|
|
5710
6156
|
}
|
|
5711
|
-
|
|
5712
|
-
|
|
6157
|
+
catch {
|
|
6158
|
+
// The failed attempt is persisted before the next bounded retry.
|
|
5713
6159
|
}
|
|
5714
6160
|
const latest = loadState(statePath);
|
|
5715
6161
|
const latestDelivery = isRecord(latest.callback_delivery)
|
|
5716
6162
|
? latest.callback_delivery
|
|
5717
6163
|
: undefined;
|
|
5718
6164
|
const latestAttempts = Number(latestDelivery?.attempts ?? 0);
|
|
5719
|
-
if (!
|
|
5720
|
-
!
|
|
5721
|
-
!
|
|
6165
|
+
if (!latestDelivery ||
|
|
6166
|
+
!isRecord(latestDelivery.message) ||
|
|
6167
|
+
!isRetryableCallbackDelivery(latest, latestDelivery) ||
|
|
5722
6168
|
latestAttempts > CALLBACK_RETRY_DELAYS_MS.length) {
|
|
5723
6169
|
return;
|
|
5724
6170
|
}
|
|
@@ -5726,6 +6172,157 @@ function runCallbackRetryMonitor(options) {
|
|
|
5726
6172
|
sleepSync(delayMs);
|
|
5727
6173
|
}
|
|
5728
6174
|
}
|
|
6175
|
+
function runTerminalBridgeMonitorHandoff(options) {
|
|
6176
|
+
const statePath = expandHome(required(options.state, "--state is required"));
|
|
6177
|
+
const logPath = expandHome(options.log ?? logPathForStatePath(statePath));
|
|
6178
|
+
const expectedMessageId = required(options.expectedTerminalMessageId, "--expected-terminal-message-id is required");
|
|
6179
|
+
const configuredPollIntervalMs = Number(options.monitorHandoffPollIntervalMs);
|
|
6180
|
+
const pollIntervalMs = Math.max(50, Number.isFinite(configuredPollIntervalMs)
|
|
6181
|
+
? configuredPollIntervalMs
|
|
6182
|
+
: 100);
|
|
6183
|
+
const handoffLockPath = terminalBridgeMonitorHandoffLockPath(statePath, expectedMessageId);
|
|
6184
|
+
let releaseHandoffLock;
|
|
6185
|
+
try {
|
|
6186
|
+
releaseHandoffLock = acquireFileLock(handoffLockPath, { timeoutMs: 0 });
|
|
6187
|
+
}
|
|
6188
|
+
catch (error) {
|
|
6189
|
+
if (!isRecord(error) || error.code !== "LOCK_TIMEOUT") {
|
|
6190
|
+
throw error;
|
|
6191
|
+
}
|
|
6192
|
+
printJson({
|
|
6193
|
+
monitored: false,
|
|
6194
|
+
terminal_bridge: true,
|
|
6195
|
+
handoff_watchdog: false,
|
|
6196
|
+
already_running: true,
|
|
6197
|
+
reason: "terminal_bridge_monitor_handoff_watchdog_already_running"
|
|
6198
|
+
});
|
|
6199
|
+
return;
|
|
6200
|
+
}
|
|
6201
|
+
try {
|
|
6202
|
+
const startedConversation = loadState(statePath);
|
|
6203
|
+
appendEvent(logPath, {
|
|
6204
|
+
ts: new Date().toISOString(),
|
|
6205
|
+
conversation_id: startedConversation.conversation_id,
|
|
6206
|
+
event: "terminal_bridge_monitor_handoff_watchdog_started",
|
|
6207
|
+
terminal_bridge_message_id: expectedMessageId
|
|
6208
|
+
});
|
|
6209
|
+
while (true) {
|
|
6210
|
+
const conversation = loadState(statePath);
|
|
6211
|
+
const nativeTakeover = isRecord(conversation.native_session_takeover)
|
|
6212
|
+
? conversation.native_session_takeover
|
|
6213
|
+
: undefined;
|
|
6214
|
+
const currentMessageId = stringValue(nativeTakeover?.terminal_bridge_message_id);
|
|
6215
|
+
if (currentMessageId !== expectedMessageId) {
|
|
6216
|
+
appendEvent(logPath, {
|
|
6217
|
+
ts: new Date().toISOString(),
|
|
6218
|
+
conversation_id: conversation.conversation_id,
|
|
6219
|
+
event: "terminal_bridge_monitor_handoff_watchdog_finished",
|
|
6220
|
+
terminal_bridge_message_id: expectedMessageId,
|
|
6221
|
+
current_terminal_bridge_message_id: currentMessageId,
|
|
6222
|
+
reason: "terminal_bridge_task_replaced"
|
|
6223
|
+
});
|
|
6224
|
+
return;
|
|
6225
|
+
}
|
|
6226
|
+
if (conversation.status === "waiting_for_openclaw") {
|
|
6227
|
+
sleepSync(pollIntervalMs);
|
|
6228
|
+
continue;
|
|
6229
|
+
}
|
|
6230
|
+
if (conversation.status !== "waiting_for_agent") {
|
|
6231
|
+
appendEvent(logPath, {
|
|
6232
|
+
ts: new Date().toISOString(),
|
|
6233
|
+
conversation_id: conversation.conversation_id,
|
|
6234
|
+
event: "terminal_bridge_monitor_handoff_watchdog_finished",
|
|
6235
|
+
terminal_bridge_message_id: expectedMessageId,
|
|
6236
|
+
status: conversation.status,
|
|
6237
|
+
reason: "conversation_no_longer_waiting_for_agent"
|
|
6238
|
+
});
|
|
6239
|
+
return;
|
|
6240
|
+
}
|
|
6241
|
+
const eligibility = terminalBridgeReconciliationEligibility(conversation);
|
|
6242
|
+
if (!eligibility.eligible) {
|
|
6243
|
+
appendEvent(logPath, {
|
|
6244
|
+
ts: new Date().toISOString(),
|
|
6245
|
+
conversation_id: conversation.conversation_id,
|
|
6246
|
+
event: "terminal_bridge_monitor_handoff_watchdog_finished",
|
|
6247
|
+
terminal_bridge_message_id: expectedMessageId,
|
|
6248
|
+
reason: eligibility.reason
|
|
6249
|
+
});
|
|
6250
|
+
return;
|
|
6251
|
+
}
|
|
6252
|
+
const activeOwner = activeTerminalBridgeMonitorOwner(statePath, expectedMessageId);
|
|
6253
|
+
if (activeOwner) {
|
|
6254
|
+
sleepSync(pollIntervalMs);
|
|
6255
|
+
continue;
|
|
6256
|
+
}
|
|
6257
|
+
const prepared = prepareTerminalBridgeMonitorReconciliation({
|
|
6258
|
+
statePath,
|
|
6259
|
+
expectedMessageId,
|
|
6260
|
+
requireWaitingForAgentStatus: true
|
|
6261
|
+
});
|
|
6262
|
+
if (!prepared.prepared) {
|
|
6263
|
+
if (prepared.alreadyRunning) {
|
|
6264
|
+
sleepSync(pollIntervalMs);
|
|
6265
|
+
continue;
|
|
6266
|
+
}
|
|
6267
|
+
appendEvent(logPath, {
|
|
6268
|
+
ts: new Date().toISOString(),
|
|
6269
|
+
conversation_id: conversation.conversation_id,
|
|
6270
|
+
event: "terminal_bridge_monitor_handoff_watchdog_finished",
|
|
6271
|
+
terminal_bridge_message_id: expectedMessageId,
|
|
6272
|
+
reason: prepared.reason
|
|
6273
|
+
});
|
|
6274
|
+
return;
|
|
6275
|
+
}
|
|
6276
|
+
const monitor = startTerminalBridgeMonitorForConversation({
|
|
6277
|
+
conversation: prepared.conversation,
|
|
6278
|
+
statePath,
|
|
6279
|
+
logPath,
|
|
6280
|
+
options
|
|
6281
|
+
});
|
|
6282
|
+
if (!monitor) {
|
|
6283
|
+
appendEvent(logPath, {
|
|
6284
|
+
ts: new Date().toISOString(),
|
|
6285
|
+
conversation_id: prepared.conversation.conversation_id,
|
|
6286
|
+
event: "terminal_bridge_monitor_handoff_watchdog_finished",
|
|
6287
|
+
terminal_bridge_message_id: expectedMessageId,
|
|
6288
|
+
reason: "terminal_bridge_monitor_launch_disabled"
|
|
6289
|
+
});
|
|
6290
|
+
return;
|
|
6291
|
+
}
|
|
6292
|
+
const launchedAt = new Date().toISOString();
|
|
6293
|
+
appendEvent(logPath, {
|
|
6294
|
+
ts: launchedAt,
|
|
6295
|
+
conversation_id: prepared.conversation.conversation_id,
|
|
6296
|
+
event: "terminal_bridge_monitor_launch",
|
|
6297
|
+
pid: monitor.pid ?? null,
|
|
6298
|
+
terminal_control: prepared.terminalControl,
|
|
6299
|
+
terminal_bridge_message_id: expectedMessageId,
|
|
6300
|
+
reason: "approval_handoff_reconciliation",
|
|
6301
|
+
agent_timeout_minutes: prepared.inactivityTimeoutMinutes,
|
|
6302
|
+
agent_hard_timeout_minutes: prepared.hardTimeoutMinutes
|
|
6303
|
+
});
|
|
6304
|
+
runtimeLog("info", "terminal_bridge_monitor_handoff_reconciled", {
|
|
6305
|
+
conversation_id: prepared.conversation.conversation_id,
|
|
6306
|
+
monitor_pid: monitor.pid ?? null,
|
|
6307
|
+
terminal_target: prepared.terminalControl.target,
|
|
6308
|
+
terminal_bridge_message_id: expectedMessageId
|
|
6309
|
+
});
|
|
6310
|
+
printJson({
|
|
6311
|
+
conversation: prepared.conversation,
|
|
6312
|
+
monitored: true,
|
|
6313
|
+
terminal_bridge: true,
|
|
6314
|
+
handoff_watchdog: true,
|
|
6315
|
+
launched: true,
|
|
6316
|
+
monitor_pid: monitor.pid ?? null,
|
|
6317
|
+
reason: "approval_handoff_reconciliation"
|
|
6318
|
+
});
|
|
6319
|
+
return;
|
|
6320
|
+
}
|
|
6321
|
+
}
|
|
6322
|
+
finally {
|
|
6323
|
+
releaseHandoffLock();
|
|
6324
|
+
}
|
|
6325
|
+
}
|
|
5729
6326
|
async function runTerminalBridgeMonitor(options) {
|
|
5730
6327
|
const statePath = expandHome(required(options.state, "--state is required"));
|
|
5731
6328
|
const conversation = loadState(statePath);
|
|
@@ -5830,7 +6427,7 @@ async function runTerminalBridgeMonitorWithLock(options) {
|
|
|
5830
6427
|
});
|
|
5831
6428
|
return;
|
|
5832
6429
|
}
|
|
5833
|
-
|
|
6430
|
+
let nativeTakeover = isRecord(conversation.native_session_takeover)
|
|
5834
6431
|
? conversation.native_session_takeover
|
|
5835
6432
|
: undefined;
|
|
5836
6433
|
const currentMessageId = stringValue(nativeTakeover?.["terminal_bridge_message_id"]);
|
|
@@ -5871,7 +6468,6 @@ async function runTerminalBridgeMonitorWithLock(options) {
|
|
|
5871
6468
|
return;
|
|
5872
6469
|
}
|
|
5873
6470
|
const requestText = String(nativeTakeover?.["terminal_bridge_request_text"] ?? conversation.user_request ?? "");
|
|
5874
|
-
const startedAt = stringValue(nativeTakeover?.["terminal_bridge_started_at"]);
|
|
5875
6471
|
const terminalRuntime = terminalRuntimeIdentityForConversation(conversation, terminalControl);
|
|
5876
6472
|
const screenChangedSinceSend = preSendScreenFingerprint !== undefined &&
|
|
5877
6473
|
previousScreenFingerprint !== undefined &&
|
|
@@ -5885,18 +6481,7 @@ async function runTerminalBridgeMonitorWithLock(options) {
|
|
|
5885
6481
|
screenChangedSinceSend,
|
|
5886
6482
|
runtime: terminalRuntime
|
|
5887
6483
|
},
|
|
5888
|
-
durableRequest:
|
|
5889
|
-
sessionId: terminalRuntime.sessionId,
|
|
5890
|
-
cwd: stringValue(nativeTakeover?.["source_cwd"]),
|
|
5891
|
-
requestText,
|
|
5892
|
-
requestHash: stringValue(nativeTakeover?.["terminal_bridge_request_hash"]),
|
|
5893
|
-
startedAt,
|
|
5894
|
-
context: {
|
|
5895
|
-
conversation,
|
|
5896
|
-
nativeTakeover,
|
|
5897
|
-
...terminalRuntime
|
|
5898
|
-
}
|
|
5899
|
-
}
|
|
6484
|
+
durableRequest: terminalDurableRequestForConversation(conversation, terminalControl)
|
|
5900
6485
|
});
|
|
5901
6486
|
const terminalStatus = poll.status;
|
|
5902
6487
|
const approval = terminalStatus.approval_state;
|
|
@@ -5905,21 +6490,107 @@ async function runTerminalBridgeMonitorWithLock(options) {
|
|
|
5905
6490
|
const currentScreenChangedSinceSend = preSendScreenFingerprint !== undefined &&
|
|
5906
6491
|
currentScreenFingerprint !== undefined &&
|
|
5907
6492
|
currentScreenFingerprint !== preSendScreenFingerprint;
|
|
6493
|
+
const currentClaudePermissionVisible = executor.kind === "claude" &&
|
|
6494
|
+
isRecord(approval) &&
|
|
6495
|
+
approval.blocked === true &&
|
|
6496
|
+
approval.prompt_kind === "claude_permission";
|
|
6497
|
+
const lastApprovalMessageMatches = currentMessageId !== undefined &&
|
|
6498
|
+
currentMessageId ===
|
|
6499
|
+
stringValue(nativeTakeover?.terminal_bridge_last_approval_message_id);
|
|
6500
|
+
const lastApprovalPromptCleared = validTimestampMs(nativeTakeover?.terminal_bridge_last_approval_prompt_cleared_at) !== undefined;
|
|
6501
|
+
if (executor.kind === "claude" &&
|
|
6502
|
+
terminalStatus.reachable === true &&
|
|
6503
|
+
approval.scanned === true &&
|
|
6504
|
+
!currentClaudePermissionVisible &&
|
|
6505
|
+
lastApprovalMessageMatches &&
|
|
6506
|
+
!lastApprovalPromptCleared &&
|
|
6507
|
+
validTimestampMs(nativeTakeover?.terminal_bridge_approval_resolved_at) !== undefined) {
|
|
6508
|
+
const cleared = markTerminalBridgeApprovalPromptCleared({
|
|
6509
|
+
statePath,
|
|
6510
|
+
logPath,
|
|
6511
|
+
expectedConversationId: conversation.conversation_id,
|
|
6512
|
+
expectedMessageId: currentMessageId
|
|
6513
|
+
});
|
|
6514
|
+
if (cleared.marked) {
|
|
6515
|
+
conversation = cleared.conversation;
|
|
6516
|
+
nativeTakeover = isRecord(conversation.native_session_takeover)
|
|
6517
|
+
? conversation.native_session_takeover
|
|
6518
|
+
: undefined;
|
|
6519
|
+
}
|
|
6520
|
+
}
|
|
6521
|
+
let suppressApprovalNotification = false;
|
|
5908
6522
|
if (executor.kind === "claude" &&
|
|
5909
6523
|
isRecord(approval) &&
|
|
5910
6524
|
approval.approvable === true &&
|
|
5911
6525
|
approval.decision_mode === "keys" &&
|
|
5912
6526
|
!currentScreenChangedSinceSend) {
|
|
5913
6527
|
previousScreenFingerprint = currentScreenFingerprint;
|
|
6528
|
+
suppressApprovalNotification = true;
|
|
5914
6529
|
runtimeLog("warn", "claude_screen_approval_not_new", {
|
|
5915
6530
|
conversation_id: conversation.conversation_id,
|
|
5916
6531
|
terminal_target: terminalControl.target,
|
|
5917
6532
|
reason: "permission screen is not proven to have changed since the managed send"
|
|
5918
6533
|
});
|
|
5919
|
-
sleepSync(pollIntervalMs);
|
|
5920
|
-
continue;
|
|
5921
6534
|
}
|
|
5922
|
-
if (
|
|
6535
|
+
if (currentClaudePermissionVisible) {
|
|
6536
|
+
const observedFingerprint = terminalBridgeApprovalFingerprint({
|
|
6537
|
+
terminalControl,
|
|
6538
|
+
terminalStatus
|
|
6539
|
+
});
|
|
6540
|
+
const currentTranscriptIdentity = claudeTranscriptApprovalIdentity(approval);
|
|
6541
|
+
const lastApprovalRequestId = stringValue(nativeTakeover?.terminal_bridge_last_approval_request_id);
|
|
6542
|
+
const lastApprovalEvidenceFingerprint = stringValue(nativeTakeover
|
|
6543
|
+
?.terminal_bridge_last_approval_evidence_fingerprint);
|
|
6544
|
+
const lastApprovalScreenDigest = stringValue(nativeTakeover?.terminal_bridge_last_approval_screen_digest);
|
|
6545
|
+
const sameConsumedTranscriptRequest = lastApprovalMessageMatches &&
|
|
6546
|
+
currentTranscriptIdentity !== undefined &&
|
|
6547
|
+
((lastApprovalRequestId !== undefined &&
|
|
6548
|
+
currentTranscriptIdentity.requestId === lastApprovalRequestId) ||
|
|
6549
|
+
(lastApprovalEvidenceFingerprint !== undefined &&
|
|
6550
|
+
currentTranscriptIdentity.evidenceFingerprint ===
|
|
6551
|
+
lastApprovalEvidenceFingerprint));
|
|
6552
|
+
const promptClearWasObserved = validTimestampMs(nativeTakeover?.terminal_bridge_last_approval_prompt_cleared_at) !== undefined;
|
|
6553
|
+
const sameUnrepaintedConsumedScreen = lastApprovalMessageMatches &&
|
|
6554
|
+
currentTranscriptIdentity === undefined &&
|
|
6555
|
+
!promptClearWasObserved &&
|
|
6556
|
+
lastApprovalScreenDigest !== undefined &&
|
|
6557
|
+
currentScreenFingerprint === lastApprovalScreenDigest;
|
|
6558
|
+
const consumedPromptWithoutEvidenceBeforeClear = lastApprovalMessageMatches &&
|
|
6559
|
+
currentTranscriptIdentity === undefined &&
|
|
6560
|
+
!promptClearWasObserved &&
|
|
6561
|
+
stringValue(nativeTakeover?.terminal_bridge_last_approval_fingerprint) !== undefined;
|
|
6562
|
+
const legacySameConsumedApproval = lastApprovalMessageMatches &&
|
|
6563
|
+
!lastApprovalRequestId &&
|
|
6564
|
+
!lastApprovalEvidenceFingerprint &&
|
|
6565
|
+
!promptClearWasObserved &&
|
|
6566
|
+
(sameUnrepaintedConsumedScreen ||
|
|
6567
|
+
observedFingerprint ===
|
|
6568
|
+
stringValue(nativeTakeover?.terminal_bridge_last_approval_fingerprint));
|
|
6569
|
+
if (sameConsumedTranscriptRequest ||
|
|
6570
|
+
sameUnrepaintedConsumedScreen ||
|
|
6571
|
+
consumedPromptWithoutEvidenceBeforeClear ||
|
|
6572
|
+
legacySameConsumedApproval) {
|
|
6573
|
+
previousScreenFingerprint = currentScreenFingerprint;
|
|
6574
|
+
suppressApprovalNotification = true;
|
|
6575
|
+
runtimeLog("info", "claude_consumed_approval_screen_still_visible", {
|
|
6576
|
+
conversation_id: conversation.conversation_id,
|
|
6577
|
+
terminal_target: terminalControl.target,
|
|
6578
|
+
fingerprint: observedFingerprint,
|
|
6579
|
+
screen_digest: currentScreenFingerprint,
|
|
6580
|
+
reason: sameConsumedTranscriptRequest
|
|
6581
|
+
? "same_transcript_request"
|
|
6582
|
+
: sameUnrepaintedConsumedScreen
|
|
6583
|
+
? "same_unrepainted_screen"
|
|
6584
|
+
: legacySameConsumedApproval
|
|
6585
|
+
? "legacy_consumed_approval"
|
|
6586
|
+
: "prompt_not_observed_cleared"
|
|
6587
|
+
});
|
|
6588
|
+
}
|
|
6589
|
+
}
|
|
6590
|
+
if (!suppressApprovalNotification &&
|
|
6591
|
+
isRecord(approval) &&
|
|
6592
|
+
approval.blocked === true &&
|
|
6593
|
+
approval.approvable !== true) {
|
|
5923
6594
|
const approvalReason = stringValue(approval.reason) ??
|
|
5924
6595
|
"Claude Code permission state cannot be safely resolved through AKK";
|
|
5925
6596
|
appendEvent(logPath, {
|
|
@@ -5947,9 +6618,11 @@ async function runTerminalBridgeMonitorWithLock(options) {
|
|
|
5947
6618
|
updatedAt: conversation.updated_at,
|
|
5948
6619
|
messageId: currentMessageId
|
|
5949
6620
|
},
|
|
5950
|
-
onRecorded: (notificationConversation) => {
|
|
6621
|
+
onRecorded: (notificationConversation, notificationContext) => {
|
|
6622
|
+
const callbackIdentity = terminalBridgeApprovalCallbackIdentity(notificationConversation);
|
|
5951
6623
|
const callbackMessage = createMessage({
|
|
5952
6624
|
conversation: notificationConversation,
|
|
6625
|
+
id: callbackIdentity.id,
|
|
5953
6626
|
from: executor.actor,
|
|
5954
6627
|
to: "openclaw",
|
|
5955
6628
|
type: "blocked",
|
|
@@ -5968,26 +6641,32 @@ async function runTerminalBridgeMonitorWithLock(options) {
|
|
|
5968
6641
|
terminal_control: terminalControl,
|
|
5969
6642
|
terminal_status: terminalStatus,
|
|
5970
6643
|
approval_fingerprint: fingerprint
|
|
5971
|
-
}
|
|
6644
|
+
},
|
|
6645
|
+
now: callbackIdentity.now
|
|
5972
6646
|
});
|
|
5973
6647
|
if (notificationConversation.gateway_method) {
|
|
5974
|
-
runLockedCallback({
|
|
5975
|
-
...options,
|
|
5976
|
-
statePath,
|
|
5977
|
-
log: logPath,
|
|
5978
|
-
messageJson: JSON.stringify(callbackMessage),
|
|
5979
|
-
gatewayMethod: notificationConversation.gateway_method,
|
|
5980
|
-
gatewaySession: notificationConversation.gateway_session,
|
|
5981
|
-
openclawSession: notificationConversation.openclaw_session,
|
|
5982
|
-
openclawBin: notificationConversation.openclaw_bin,
|
|
5983
|
-
gatewayUrl: stringValue(notificationConversation.gateway_token)
|
|
5984
|
-
? notificationConversation.gateway_url
|
|
5985
|
-
: undefined,
|
|
5986
|
-
token: stringValue(notificationConversation.gateway_token)
|
|
5987
|
-
});
|
|
5988
6648
|
return {
|
|
5989
6649
|
callbackMessage,
|
|
5990
|
-
|
|
6650
|
+
prepared: prepareLockedCallback({
|
|
6651
|
+
...options,
|
|
6652
|
+
statePath,
|
|
6653
|
+
log: logPath,
|
|
6654
|
+
messageJson: JSON.stringify(callbackMessage),
|
|
6655
|
+
gatewayMethod: notificationConversation.gateway_method,
|
|
6656
|
+
gatewaySession: notificationConversation.gateway_session,
|
|
6657
|
+
openclawSession: notificationConversation.openclaw_session,
|
|
6658
|
+
openclawBin: notificationConversation.openclaw_bin,
|
|
6659
|
+
gatewayUrl: stringValue(notificationConversation.gateway_token)
|
|
6660
|
+
? notificationConversation.gateway_url
|
|
6661
|
+
: undefined,
|
|
6662
|
+
token: stringValue(notificationConversation.gateway_token),
|
|
6663
|
+
preserveMessageId: true,
|
|
6664
|
+
trackCallbackDelivery: true,
|
|
6665
|
+
preserveCallbackStatus: true,
|
|
6666
|
+
callbackDeliveryKind: "approval_notification",
|
|
6667
|
+
recoverMissingOutbox: notificationContext?.recoverMissingOutbox === true,
|
|
6668
|
+
conversationOverride: notificationConversation
|
|
6669
|
+
})
|
|
5991
6670
|
};
|
|
5992
6671
|
}
|
|
5993
6672
|
return {
|
|
@@ -6015,7 +6694,8 @@ async function runTerminalBridgeMonitorWithLock(options) {
|
|
|
6015
6694
|
});
|
|
6016
6695
|
return;
|
|
6017
6696
|
}
|
|
6018
|
-
if (notification.recorded?.
|
|
6697
|
+
if (notification.recorded?.prepared) {
|
|
6698
|
+
runPreparedCallback(notification.recorded.prepared);
|
|
6019
6699
|
return;
|
|
6020
6700
|
}
|
|
6021
6701
|
printJson({
|
|
@@ -6032,7 +6712,9 @@ async function runTerminalBridgeMonitorWithLock(options) {
|
|
|
6032
6712
|
});
|
|
6033
6713
|
return;
|
|
6034
6714
|
}
|
|
6035
|
-
if (
|
|
6715
|
+
if (!suppressApprovalNotification &&
|
|
6716
|
+
isRecord(approval) &&
|
|
6717
|
+
approval.blocked === true) {
|
|
6036
6718
|
const fingerprint = terminalBridgeApprovalFingerprint({ terminalControl, terminalStatus });
|
|
6037
6719
|
appendEvent(logPath, {
|
|
6038
6720
|
ts: new Date().toISOString(),
|
|
@@ -6055,9 +6737,11 @@ async function runTerminalBridgeMonitorWithLock(options) {
|
|
|
6055
6737
|
updatedAt: conversation.updated_at,
|
|
6056
6738
|
messageId: currentMessageId
|
|
6057
6739
|
},
|
|
6058
|
-
onRecorded: (notificationConversation) => {
|
|
6740
|
+
onRecorded: (notificationConversation, notificationContext) => {
|
|
6741
|
+
const callbackIdentity = terminalBridgeApprovalCallbackIdentity(notificationConversation);
|
|
6059
6742
|
const callbackMessage = createMessage({
|
|
6060
6743
|
conversation: notificationConversation,
|
|
6744
|
+
id: callbackIdentity.id,
|
|
6061
6745
|
from: executor.actor,
|
|
6062
6746
|
to: "openclaw",
|
|
6063
6747
|
type: "question",
|
|
@@ -6083,26 +6767,32 @@ async function runTerminalBridgeMonitorWithLock(options) {
|
|
|
6083
6767
|
deny_command: `AKK cancel ${notificationConversation.conversation_id}`,
|
|
6084
6768
|
approve_tool: "agent_knock_knock_approve",
|
|
6085
6769
|
deny_tool: "agent_knock_knock_cancel"
|
|
6086
|
-
}
|
|
6770
|
+
},
|
|
6771
|
+
now: callbackIdentity.now
|
|
6087
6772
|
});
|
|
6088
6773
|
if (notificationConversation.gateway_method) {
|
|
6089
|
-
runLockedCallback({
|
|
6090
|
-
...options,
|
|
6091
|
-
statePath,
|
|
6092
|
-
log: logPath,
|
|
6093
|
-
messageJson: JSON.stringify(callbackMessage),
|
|
6094
|
-
gatewayMethod: notificationConversation.gateway_method,
|
|
6095
|
-
gatewaySession: notificationConversation.gateway_session,
|
|
6096
|
-
openclawSession: notificationConversation.openclaw_session,
|
|
6097
|
-
openclawBin: notificationConversation.openclaw_bin,
|
|
6098
|
-
gatewayUrl: stringValue(notificationConversation.gateway_token)
|
|
6099
|
-
? notificationConversation.gateway_url
|
|
6100
|
-
: undefined,
|
|
6101
|
-
token: stringValue(notificationConversation.gateway_token)
|
|
6102
|
-
});
|
|
6103
6774
|
return {
|
|
6104
6775
|
callbackMessage,
|
|
6105
|
-
|
|
6776
|
+
prepared: prepareLockedCallback({
|
|
6777
|
+
...options,
|
|
6778
|
+
statePath,
|
|
6779
|
+
log: logPath,
|
|
6780
|
+
messageJson: JSON.stringify(callbackMessage),
|
|
6781
|
+
gatewayMethod: notificationConversation.gateway_method,
|
|
6782
|
+
gatewaySession: notificationConversation.gateway_session,
|
|
6783
|
+
openclawSession: notificationConversation.openclaw_session,
|
|
6784
|
+
openclawBin: notificationConversation.openclaw_bin,
|
|
6785
|
+
gatewayUrl: stringValue(notificationConversation.gateway_token)
|
|
6786
|
+
? notificationConversation.gateway_url
|
|
6787
|
+
: undefined,
|
|
6788
|
+
token: stringValue(notificationConversation.gateway_token),
|
|
6789
|
+
preserveMessageId: true,
|
|
6790
|
+
trackCallbackDelivery: true,
|
|
6791
|
+
preserveCallbackStatus: true,
|
|
6792
|
+
callbackDeliveryKind: "approval_notification",
|
|
6793
|
+
recoverMissingOutbox: notificationContext?.recoverMissingOutbox === true,
|
|
6794
|
+
conversationOverride: notificationConversation
|
|
6795
|
+
})
|
|
6106
6796
|
};
|
|
6107
6797
|
}
|
|
6108
6798
|
return {
|
|
@@ -6128,7 +6818,50 @@ async function runTerminalBridgeMonitorWithLock(options) {
|
|
|
6128
6818
|
});
|
|
6129
6819
|
return;
|
|
6130
6820
|
}
|
|
6131
|
-
if (notification.recorded?.
|
|
6821
|
+
if (notification.recorded?.prepared) {
|
|
6822
|
+
const callbackResult = runPreparedCallback(notification.recorded.prepared, { emit: false });
|
|
6823
|
+
if (callbackResult.delivered === true &&
|
|
6824
|
+
process.env
|
|
6825
|
+
.AKK_TEST_EXIT_AFTER_APPROVAL_CALLBACK_DELIVERED === "1") {
|
|
6826
|
+
appendEvent(logPath, {
|
|
6827
|
+
ts: new Date().toISOString(),
|
|
6828
|
+
conversation_id: conversation.conversation_id,
|
|
6829
|
+
event: "terminal_bridge_test_exit_after_approval_callback_delivered",
|
|
6830
|
+
terminal_control: terminalControl,
|
|
6831
|
+
fingerprint
|
|
6832
|
+
});
|
|
6833
|
+
process.exit(86);
|
|
6834
|
+
}
|
|
6835
|
+
const afterCallback = loadState(statePath);
|
|
6836
|
+
const afterTakeover = isRecord(afterCallback.native_session_takeover)
|
|
6837
|
+
? afterCallback.native_session_takeover
|
|
6838
|
+
: undefined;
|
|
6839
|
+
const approvalWasConsumed = isWaitingForAgent(afterCallback.status) &&
|
|
6840
|
+
afterTakeover?.terminal_bridge_approval === undefined &&
|
|
6841
|
+
stringValue(afterTakeover?.terminal_bridge_message_id) ===
|
|
6842
|
+
currentMessageId &&
|
|
6843
|
+
stringValue(afterTakeover?.terminal_bridge_last_approval_message_id) === currentMessageId &&
|
|
6844
|
+
stringValue(afterTakeover?.terminal_bridge_last_approval_fingerprint) === fingerprint;
|
|
6845
|
+
if (approvalWasConsumed) {
|
|
6846
|
+
conversation = afterCallback;
|
|
6847
|
+
previousScreenFingerprint = currentScreenFingerprint;
|
|
6848
|
+
previousDurableFingerprint = undefined;
|
|
6849
|
+
idleCompletionFingerprint = undefined;
|
|
6850
|
+
lastActivityAtMs =
|
|
6851
|
+
validTimestampMs(afterTakeover?.terminal_bridge_last_activity_at) ?? Date.now();
|
|
6852
|
+
lastPersistedActivityAtMs = lastActivityAtMs;
|
|
6853
|
+
persistedActivityReason = stringValue(afterTakeover?.terminal_bridge_last_activity_reason);
|
|
6854
|
+
appendEvent(logPath, {
|
|
6855
|
+
ts: new Date().toISOString(),
|
|
6856
|
+
conversation_id: conversation.conversation_id,
|
|
6857
|
+
event: "terminal_bridge_monitor_continued_after_approval",
|
|
6858
|
+
terminal_control: terminalControl,
|
|
6859
|
+
fingerprint
|
|
6860
|
+
});
|
|
6861
|
+
sleepSync(pollIntervalMs);
|
|
6862
|
+
continue;
|
|
6863
|
+
}
|
|
6864
|
+
emitPreparedCallbackResult(callbackResult);
|
|
6132
6865
|
return;
|
|
6133
6866
|
}
|
|
6134
6867
|
printJson({
|
|
@@ -6236,6 +6969,7 @@ async function runTerminalBridgeMonitorWithLock(options) {
|
|
|
6236
6969
|
});
|
|
6237
6970
|
return;
|
|
6238
6971
|
}
|
|
6972
|
+
let preparedCallback;
|
|
6239
6973
|
try {
|
|
6240
6974
|
conversation = claim.conversation;
|
|
6241
6975
|
appendEvent(logPath, {
|
|
@@ -6279,7 +7013,7 @@ async function runTerminalBridgeMonitorWithLock(options) {
|
|
|
6279
7013
|
}),
|
|
6280
7014
|
id: callbackMessageId
|
|
6281
7015
|
};
|
|
6282
|
-
|
|
7016
|
+
preparedCallback = prepareLockedCallback({
|
|
6283
7017
|
...options,
|
|
6284
7018
|
statePath,
|
|
6285
7019
|
log: logPath,
|
|
@@ -6300,6 +7034,7 @@ async function runTerminalBridgeMonitorWithLock(options) {
|
|
|
6300
7034
|
releaseClaudeHookLease(conversation);
|
|
6301
7035
|
claim.release();
|
|
6302
7036
|
}
|
|
7037
|
+
runPreparedCallback(preparedCallback);
|
|
6303
7038
|
return;
|
|
6304
7039
|
}
|
|
6305
7040
|
// A concrete approval or completion observed on this poll wins over a timeout boundary.
|
|
@@ -6396,6 +7131,13 @@ function terminalBridgeMonitorLockPath(statePath, terminalMessageId) {
|
|
|
6396
7131
|
.slice(0, 20);
|
|
6397
7132
|
return `${statePath}.terminal-bridge-monitor-${messageKey}.lock`;
|
|
6398
7133
|
}
|
|
7134
|
+
function terminalBridgeMonitorHandoffLockPath(statePath, terminalMessageId) {
|
|
7135
|
+
const messageKey = createHash("sha256")
|
|
7136
|
+
.update(terminalMessageId)
|
|
7137
|
+
.digest("hex")
|
|
7138
|
+
.slice(0, 20);
|
|
7139
|
+
return `${statePath}.terminal-bridge-monitor-handoff-${messageKey}.lock`;
|
|
7140
|
+
}
|
|
6399
7141
|
function fileLockOwnerPid(lockPath) {
|
|
6400
7142
|
return readFileLockOwner(lockPath).pid;
|
|
6401
7143
|
}
|
|
@@ -6631,16 +7373,36 @@ function terminalBridgeApprovalCandidate({ executor, terminalControl, terminalSt
|
|
|
6631
7373
|
if (approval.approvable !== true) {
|
|
6632
7374
|
return undefined;
|
|
6633
7375
|
}
|
|
7376
|
+
const policyEvidence = isRecord(approval.policy_evidence)
|
|
7377
|
+
? approval.policy_evidence
|
|
7378
|
+
: undefined;
|
|
7379
|
+
const localClaudeEvidence = executor.kind === "claude" &&
|
|
7380
|
+
policyEvidence?.source === "claude_transcript" &&
|
|
7381
|
+
policyEvidence?.kind === "run_command";
|
|
6634
7382
|
return {
|
|
6635
7383
|
agent: executor.kind,
|
|
6636
|
-
kind:
|
|
6637
|
-
|
|
7384
|
+
kind: localClaudeEvidence
|
|
7385
|
+
? "run_command"
|
|
7386
|
+
: stringValue(approval.prompt_kind) ?? "unknown",
|
|
7387
|
+
command: localClaudeEvidence ? undefined : stringValue(approval.command),
|
|
6638
7388
|
tool_name: stringValue(approval.tool_name),
|
|
6639
7389
|
request_detail: stringValue(approval.request_detail),
|
|
6640
7390
|
cwd: stringValue(approval.cwd) ?? terminalControl.currentPath,
|
|
6641
7391
|
fingerprint,
|
|
6642
7392
|
terminal_target: terminalControl.target,
|
|
6643
|
-
decision_mode: stringValue(approval.decision_mode)
|
|
7393
|
+
decision_mode: stringValue(approval.decision_mode),
|
|
7394
|
+
...(localClaudeEvidence
|
|
7395
|
+
? {
|
|
7396
|
+
command_source: "executor_local",
|
|
7397
|
+
policy_evidence: {
|
|
7398
|
+
source: "claude_transcript",
|
|
7399
|
+
kind: "run_command",
|
|
7400
|
+
command_sha256: stringValue(policyEvidence.command_sha256),
|
|
7401
|
+
evidence_fingerprint: stringValue(policyEvidence.evidence_fingerprint),
|
|
7402
|
+
request_id: stringValue(policyEvidence.request_id)
|
|
7403
|
+
}
|
|
7404
|
+
}
|
|
7405
|
+
: {})
|
|
6644
7406
|
};
|
|
6645
7407
|
}
|
|
6646
7408
|
async function loadCodexTerminalContext({ conversation, nativeTakeover, options }) {
|
|
@@ -6814,49 +7576,50 @@ function runTranscript(options) {
|
|
|
6814
7576
|
}
|
|
6815
7577
|
function runCallback(options) {
|
|
6816
7578
|
const statePath = expandHome(required(options.state, "--state is required"));
|
|
6817
|
-
|
|
6818
|
-
try {
|
|
6819
|
-
runLockedCallback({ ...options, statePath });
|
|
6820
|
-
}
|
|
6821
|
-
finally {
|
|
6822
|
-
releaseLock();
|
|
6823
|
-
}
|
|
7579
|
+
runCallbackTransaction({ ...options, statePath });
|
|
6824
7580
|
}
|
|
6825
7581
|
function runRetryCallback(options) {
|
|
6826
7582
|
const { conversation, statePath } = loadConversationFromOptions(options);
|
|
6827
7583
|
const callbackDelivery = isRecord(conversation.callback_delivery)
|
|
6828
7584
|
? conversation.callback_delivery
|
|
6829
7585
|
: undefined;
|
|
6830
|
-
if (!
|
|
6831
|
-
throw new Error(`cannot retry callback for ${conversation.conversation_id};
|
|
7586
|
+
if (!isRetryableCallbackDelivery(conversation, callbackDelivery)) {
|
|
7587
|
+
throw new Error(`cannot retry callback for ${conversation.conversation_id}; no retryable callback delivery is available`);
|
|
6832
7588
|
}
|
|
6833
7589
|
if (!callbackDelivery || !isRecord(callbackDelivery.message)) {
|
|
6834
7590
|
throw new Error(`cannot retry callback for ${conversation.conversation_id}; pending callback is missing`);
|
|
6835
7591
|
}
|
|
6836
|
-
|
|
7592
|
+
runCallbackTransaction({
|
|
7593
|
+
...options,
|
|
7594
|
+
statePath,
|
|
7595
|
+
messageJson: JSON.stringify(callbackDelivery.message),
|
|
7596
|
+
gatewayMethod: stringValue(callbackDelivery.gateway_method) ?? conversation.gateway_method,
|
|
7597
|
+
gatewaySession: stringValue(callbackDelivery.gateway_session) ?? conversation.gateway_session,
|
|
7598
|
+
openclawSession: conversation.openclaw_session,
|
|
7599
|
+
openclawBin: stringValue(callbackDelivery.openclaw_bin) ?? conversation.openclaw_bin,
|
|
7600
|
+
gatewayUrl: stringValue(callbackDelivery.gateway_url) ?? conversation.gateway_url,
|
|
7601
|
+
token: stringValue(callbackDelivery.gateway_token) ?? conversation.gateway_token,
|
|
7602
|
+
closeTerminalBridgeOnDone: callbackDelivery.close_terminal_bridge_on_done === true,
|
|
7603
|
+
retryPending: true
|
|
7604
|
+
});
|
|
7605
|
+
}
|
|
7606
|
+
function runCallbackTransaction(options) {
|
|
7607
|
+
const releaseLock = acquireFileLock(`${options.statePath}.lock`);
|
|
7608
|
+
let prepared;
|
|
6837
7609
|
try {
|
|
6838
|
-
|
|
6839
|
-
...options,
|
|
6840
|
-
statePath,
|
|
6841
|
-
messageJson: JSON.stringify(callbackDelivery.message),
|
|
6842
|
-
gatewayMethod: stringValue(callbackDelivery.gateway_method) ?? conversation.gateway_method,
|
|
6843
|
-
gatewaySession: stringValue(callbackDelivery.gateway_session) ?? conversation.gateway_session,
|
|
6844
|
-
openclawSession: conversation.openclaw_session,
|
|
6845
|
-
openclawBin: stringValue(callbackDelivery.openclaw_bin) ?? conversation.openclaw_bin,
|
|
6846
|
-
gatewayUrl: stringValue(callbackDelivery.gateway_url) ?? conversation.gateway_url,
|
|
6847
|
-
token: stringValue(callbackDelivery.gateway_token) ?? conversation.gateway_token,
|
|
6848
|
-
closeTerminalBridgeOnDone: callbackDelivery.close_terminal_bridge_on_done === true,
|
|
6849
|
-
retryPending: true
|
|
6850
|
-
});
|
|
7610
|
+
prepared = prepareLockedCallback(options);
|
|
6851
7611
|
}
|
|
6852
7612
|
finally {
|
|
6853
7613
|
releaseLock();
|
|
6854
7614
|
}
|
|
7615
|
+
return runPreparedCallback(prepared);
|
|
6855
7616
|
}
|
|
6856
|
-
function
|
|
7617
|
+
function prepareLockedCallback(options) {
|
|
6857
7618
|
const messageInput = required(options.messageJson, "--message-json is required");
|
|
6858
7619
|
const logPath = expandHome(options.log ?? logPathForStatePath(options.statePath));
|
|
6859
|
-
const conversation =
|
|
7620
|
+
const conversation = isRecord(options.conversationOverride)
|
|
7621
|
+
? options.conversationOverride
|
|
7622
|
+
: loadState(options.statePath);
|
|
6860
7623
|
const executor = executorForConversation(conversation);
|
|
6861
7624
|
const message = options.retryPending === true || options.preserveMessageId === true
|
|
6862
7625
|
? parseMessageJson(messageInput)
|
|
@@ -6873,15 +7636,28 @@ function runLockedCallback(options) {
|
|
|
6873
7636
|
const callbackDelivery = isRecord(conversation.callback_delivery)
|
|
6874
7637
|
? conversation.callback_delivery
|
|
6875
7638
|
: undefined;
|
|
7639
|
+
const sameDeliveryMessage = isRecord(callbackDelivery?.message) &&
|
|
7640
|
+
callbackDelivery.message.id === message.id;
|
|
7641
|
+
const inheritedDelivery = sameDeliveryMessage
|
|
7642
|
+
? callbackDelivery
|
|
7643
|
+
: undefined;
|
|
6876
7644
|
const retryingPending = options.retryPending === true &&
|
|
6877
|
-
|
|
6878
|
-
|
|
6879
|
-
["pending", "failed"].includes(String(callbackDelivery.status ?? ""));
|
|
7645
|
+
sameDeliveryMessage &&
|
|
7646
|
+
isRetryableCallbackDelivery(conversation, inheritedDelivery);
|
|
6880
7647
|
const duplicateMessage = isDuplicateMessage(existingEvents, message);
|
|
7648
|
+
const recoveryMessageAlreadyLogged = options.recoverMissingOutbox === true
|
|
7649
|
+
? exactLoggedMessageForRecovery(existingEvents, message)
|
|
7650
|
+
: false;
|
|
7651
|
+
const recoveringMissingOutbox = options.recoverMissingOutbox === true &&
|
|
7652
|
+
(!isRecord(callbackDelivery?.message) ||
|
|
7653
|
+
callbackDelivery.message.id !== message.id);
|
|
6881
7654
|
const recoveringTerminalCompletion = options.recoverTerminalCompletion === true &&
|
|
6882
7655
|
duplicateMessage &&
|
|
6883
7656
|
isWaitingForAgent(conversation.status);
|
|
6884
|
-
if (duplicateMessage &&
|
|
7657
|
+
if (duplicateMessage &&
|
|
7658
|
+
!retryingPending &&
|
|
7659
|
+
!recoveringTerminalCompletion &&
|
|
7660
|
+
!recoveringMissingOutbox) {
|
|
6885
7661
|
runtimeLog("info", "callback_duplicate", {
|
|
6886
7662
|
conversation_id: conversation.conversation_id,
|
|
6887
7663
|
agent: executor.kind,
|
|
@@ -6892,26 +7668,28 @@ function runLockedCallback(options) {
|
|
|
6892
7668
|
state_path: options.statePath,
|
|
6893
7669
|
event_log_path: logPath
|
|
6894
7670
|
});
|
|
6895
|
-
|
|
7671
|
+
return {
|
|
7672
|
+
outcome: "duplicate",
|
|
6896
7673
|
conversation,
|
|
6897
7674
|
message,
|
|
6898
|
-
|
|
6899
|
-
|
|
6900
|
-
duplicate: true
|
|
6901
|
-
});
|
|
6902
|
-
return;
|
|
7675
|
+
logPath
|
|
7676
|
+
};
|
|
6903
7677
|
}
|
|
6904
7678
|
const closeTerminalBridgeOnDone = message.type === "done" &&
|
|
6905
7679
|
options.closeTerminalBridgeOnDone === true;
|
|
7680
|
+
const preserveConversationStatus = options.preserveCallbackStatus === true ||
|
|
7681
|
+
inheritedDelivery?.preserve_conversation_status === true;
|
|
6906
7682
|
const trackCallbackDelivery = closeTerminalBridgeOnDone ||
|
|
6907
7683
|
options.trackCallbackDelivery === true ||
|
|
6908
|
-
|
|
7684
|
+
inheritedDelivery?.track_delivery === true ||
|
|
7685
|
+
preserveConversationStatus;
|
|
6909
7686
|
const requiresDelivery = Boolean(options.gatewayMethod) || options.recordOnly !== true;
|
|
6910
|
-
const deliveryAttempt = Number(
|
|
7687
|
+
const deliveryAttempt = Number(inheritedDelivery?.attempts ?? 0) + 1;
|
|
7688
|
+
const deliveryAttemptId = randomUUID();
|
|
6911
7689
|
let nextConversation = retryingPending
|
|
6912
7690
|
? conversation
|
|
6913
7691
|
: applyMessageToConversation(conversation, message);
|
|
6914
|
-
const storedFinalStatus = stringValue(
|
|
7692
|
+
const storedFinalStatus = stringValue(inheritedDelivery?.final_status);
|
|
6915
7693
|
const finalStatus = storedFinalStatus &&
|
|
6916
7694
|
CONVERSATION_STATUSES.has(storedFinalStatus)
|
|
6917
7695
|
? storedFinalStatus
|
|
@@ -6928,19 +7706,25 @@ function runLockedCallback(options) {
|
|
|
6928
7706
|
delayMs: callbackRetryDelayMs
|
|
6929
7707
|
})
|
|
6930
7708
|
: undefined;
|
|
6931
|
-
if (!retryingPending &&
|
|
7709
|
+
if (!retryingPending &&
|
|
7710
|
+
!recoveringTerminalCompletion &&
|
|
7711
|
+
!(recoveringMissingOutbox && recoveryMessageAlreadyLogged)) {
|
|
6932
7712
|
appendEvent(logPath, messageEvent(message));
|
|
6933
7713
|
}
|
|
6934
7714
|
if (trackCallbackDelivery && requiresDelivery) {
|
|
6935
7715
|
const now = new Date().toISOString();
|
|
6936
7716
|
nextConversation = {
|
|
6937
7717
|
...nextConversation,
|
|
6938
|
-
status:
|
|
7718
|
+
status: preserveConversationStatus
|
|
7719
|
+
? nextConversation.status
|
|
7720
|
+
: "callback_pending",
|
|
6939
7721
|
callback_delivery: {
|
|
6940
7722
|
status: "pending",
|
|
6941
7723
|
message,
|
|
6942
7724
|
attempts: deliveryAttempt,
|
|
6943
|
-
|
|
7725
|
+
attempt_id: deliveryAttemptId,
|
|
7726
|
+
attempt_pid: process.pid,
|
|
7727
|
+
created_at: stringValue(inheritedDelivery?.created_at) ?? now,
|
|
6944
7728
|
last_attempt_at: now,
|
|
6945
7729
|
gateway_method: options.gatewayMethod,
|
|
6946
7730
|
gateway_session: options.gatewaySession ?? options.openclawSession ?? conversation.openclaw_session,
|
|
@@ -6949,6 +7733,9 @@ function runLockedCallback(options) {
|
|
|
6949
7733
|
close_terminal_bridge_on_done: closeTerminalBridgeOnDone,
|
|
6950
7734
|
track_delivery: true,
|
|
6951
7735
|
final_status: finalStatus,
|
|
7736
|
+
preserve_conversation_status: preserveConversationStatus,
|
|
7737
|
+
kind: stringValue(options.callbackDeliveryKind) ??
|
|
7738
|
+
stringValue(inheritedDelivery?.kind),
|
|
6952
7739
|
...(callbackWatchdog
|
|
6953
7740
|
? {
|
|
6954
7741
|
retry_monitor_pid: callbackWatchdog.pid ?? null,
|
|
@@ -7000,113 +7787,233 @@ function runLockedCallback(options) {
|
|
|
7000
7787
|
conversation_id: conversation.conversation_id,
|
|
7001
7788
|
status: nextConversation.status
|
|
7002
7789
|
});
|
|
7003
|
-
|
|
7790
|
+
return {
|
|
7791
|
+
outcome: "record_only",
|
|
7004
7792
|
conversation: nextConversation,
|
|
7005
7793
|
message,
|
|
7006
|
-
|
|
7794
|
+
logPath
|
|
7795
|
+
};
|
|
7796
|
+
}
|
|
7797
|
+
return {
|
|
7798
|
+
outcome: "deliver",
|
|
7799
|
+
options,
|
|
7800
|
+
statePath: options.statePath,
|
|
7801
|
+
logPath,
|
|
7802
|
+
conversation: nextConversation,
|
|
7803
|
+
message,
|
|
7804
|
+
trackCallbackDelivery,
|
|
7805
|
+
preserveConversationStatus,
|
|
7806
|
+
closeTerminalBridgeOnDone,
|
|
7807
|
+
finalStatus,
|
|
7808
|
+
deliveryAttempt,
|
|
7809
|
+
deliveryAttemptId
|
|
7810
|
+
};
|
|
7811
|
+
}
|
|
7812
|
+
function emitPreparedCallbackResult(result) {
|
|
7813
|
+
printJson({
|
|
7814
|
+
conversation: result.conversation,
|
|
7815
|
+
message: result.message,
|
|
7816
|
+
budget: budgetAction(result.conversation),
|
|
7817
|
+
delivered: result.delivered,
|
|
7818
|
+
duplicate: result.duplicate,
|
|
7819
|
+
...(result.delivery === undefined ? {} : { delivery: result.delivery })
|
|
7820
|
+
});
|
|
7821
|
+
}
|
|
7822
|
+
function runPreparedCallback(prepared, { emit = true } = {}) {
|
|
7823
|
+
if (prepared.outcome === "duplicate") {
|
|
7824
|
+
const result = {
|
|
7007
7825
|
delivered: false,
|
|
7008
|
-
duplicate:
|
|
7009
|
-
|
|
7010
|
-
|
|
7826
|
+
duplicate: true,
|
|
7827
|
+
conversation: prepared.conversation,
|
|
7828
|
+
message: prepared.message
|
|
7829
|
+
};
|
|
7830
|
+
if (emit) {
|
|
7831
|
+
emitPreparedCallbackResult(result);
|
|
7832
|
+
}
|
|
7833
|
+
return result;
|
|
7834
|
+
}
|
|
7835
|
+
if (prepared.outcome === "record_only") {
|
|
7836
|
+
const result = {
|
|
7837
|
+
delivered: false,
|
|
7838
|
+
duplicate: false,
|
|
7839
|
+
conversation: prepared.conversation,
|
|
7840
|
+
message: prepared.message
|
|
7841
|
+
};
|
|
7842
|
+
if (emit) {
|
|
7843
|
+
emitPreparedCallbackResult(result);
|
|
7844
|
+
}
|
|
7845
|
+
return result;
|
|
7011
7846
|
}
|
|
7012
7847
|
try {
|
|
7013
7848
|
const deliveryKind = deliverCallbackToOpenClaw({
|
|
7014
|
-
options,
|
|
7015
|
-
statePath:
|
|
7016
|
-
logPath,
|
|
7017
|
-
conversation:
|
|
7018
|
-
message
|
|
7019
|
-
});
|
|
7020
|
-
const
|
|
7021
|
-
|
|
7022
|
-
|
|
7023
|
-
|
|
7024
|
-
|
|
7025
|
-
|
|
7026
|
-
|
|
7027
|
-
|
|
7849
|
+
options: prepared.options,
|
|
7850
|
+
statePath: prepared.statePath,
|
|
7851
|
+
logPath: prepared.logPath,
|
|
7852
|
+
conversation: prepared.conversation,
|
|
7853
|
+
message: prepared.message
|
|
7854
|
+
});
|
|
7855
|
+
const deliveredConversation = prepared.trackCallbackDelivery
|
|
7856
|
+
? settlePreparedCallbackDelivery(prepared, { delivered: true })
|
|
7857
|
+
: loadState(prepared.statePath);
|
|
7858
|
+
const result = {
|
|
7859
|
+
delivered: true,
|
|
7860
|
+
duplicate: false,
|
|
7861
|
+
conversation: deliveredConversation,
|
|
7862
|
+
message: prepared.message,
|
|
7863
|
+
delivery: deliveryKind
|
|
7864
|
+
};
|
|
7865
|
+
if (emit) {
|
|
7866
|
+
emitPreparedCallbackResult(result);
|
|
7867
|
+
}
|
|
7868
|
+
return result;
|
|
7869
|
+
}
|
|
7870
|
+
catch (error) {
|
|
7871
|
+
if (prepared.trackCallbackDelivery) {
|
|
7872
|
+
settlePreparedCallbackDelivery(prepared, { delivered: false, error });
|
|
7873
|
+
}
|
|
7874
|
+
throw error;
|
|
7875
|
+
}
|
|
7876
|
+
}
|
|
7877
|
+
function settlePreparedCallbackDelivery(prepared, result) {
|
|
7878
|
+
const releaseLock = acquireFileLock(`${prepared.statePath}.lock`);
|
|
7879
|
+
try {
|
|
7880
|
+
const current = loadState(prepared.statePath);
|
|
7881
|
+
const currentDelivery = isRecord(current.callback_delivery)
|
|
7882
|
+
? current.callback_delivery
|
|
7883
|
+
: undefined;
|
|
7884
|
+
if (!currentDelivery ||
|
|
7885
|
+
!isRecord(currentDelivery.message) ||
|
|
7886
|
+
currentDelivery.message.id !== prepared.message.id ||
|
|
7887
|
+
currentDelivery.attempt_id !== prepared.deliveryAttemptId ||
|
|
7888
|
+
Number(currentDelivery.attempts) !== prepared.deliveryAttempt ||
|
|
7889
|
+
currentDelivery.status !== "pending") {
|
|
7890
|
+
appendEvent(prepared.logPath, {
|
|
7891
|
+
ts: new Date().toISOString(),
|
|
7892
|
+
conversation_id: current.conversation_id,
|
|
7893
|
+
event: "callback_delivery_settle_skipped",
|
|
7894
|
+
message_id: prepared.message.id,
|
|
7895
|
+
attempt: prepared.deliveryAttempt,
|
|
7896
|
+
result: result.delivered ? "delivered" : "failed",
|
|
7897
|
+
reason: "callback delivery claim changed before settlement",
|
|
7898
|
+
current_status: current.status
|
|
7899
|
+
});
|
|
7900
|
+
return current;
|
|
7901
|
+
}
|
|
7902
|
+
if (result.delivered) {
|
|
7903
|
+
const deliveredAt = new Date().toISOString();
|
|
7904
|
+
const ownsConversationStatus = currentDelivery.preserve_conversation_status !== true &&
|
|
7905
|
+
["callback_pending", "callback_failed"].includes(current.status);
|
|
7906
|
+
const deliveredStatus = prepared.closeTerminalBridgeOnDone
|
|
7907
|
+
? "closed"
|
|
7908
|
+
: prepared.finalStatus;
|
|
7909
|
+
const nextConversation = {
|
|
7910
|
+
...current,
|
|
7911
|
+
status: ownsConversationStatus ? deliveredStatus : current.status,
|
|
7912
|
+
...(ownsConversationStatus && prepared.closeTerminalBridgeOnDone
|
|
7028
7913
|
? {
|
|
7029
7914
|
closed_at: deliveredAt,
|
|
7030
7915
|
close_reason: "terminal bridge task completed"
|
|
7031
7916
|
}
|
|
7032
7917
|
: {}),
|
|
7033
7918
|
callback_delivery: {
|
|
7034
|
-
...
|
|
7919
|
+
...currentDelivery,
|
|
7035
7920
|
status: "delivered",
|
|
7036
7921
|
delivered_at: deliveredAt,
|
|
7037
7922
|
last_error: undefined
|
|
7038
7923
|
},
|
|
7039
|
-
updated_at: deliveredAt
|
|
7924
|
+
updated_at: ownsConversationStatus ? deliveredAt : current.updated_at
|
|
7040
7925
|
};
|
|
7041
|
-
|
|
7042
|
-
|
|
7043
|
-
|
|
7926
|
+
if (ownsConversationStatus) {
|
|
7927
|
+
delete nextConversation.idle_since;
|
|
7928
|
+
}
|
|
7929
|
+
saveState(prepared.statePath, nextConversation);
|
|
7930
|
+
appendEvent(prepared.logPath, {
|
|
7044
7931
|
ts: deliveredAt,
|
|
7045
|
-
conversation_id:
|
|
7932
|
+
conversation_id: current.conversation_id,
|
|
7046
7933
|
event: "callback_delivery_succeeded",
|
|
7047
|
-
message_id: message.id,
|
|
7048
|
-
attempt: deliveryAttempt,
|
|
7049
|
-
status:
|
|
7934
|
+
message_id: prepared.message.id,
|
|
7935
|
+
attempt: prepared.deliveryAttempt,
|
|
7936
|
+
status: nextConversation.status,
|
|
7937
|
+
state_preserved: !ownsConversationStatus
|
|
7050
7938
|
});
|
|
7051
|
-
|
|
7052
|
-
|
|
7053
|
-
|
|
7054
|
-
|
|
7055
|
-
|
|
7056
|
-
|
|
7057
|
-
|
|
7058
|
-
|
|
7059
|
-
|
|
7060
|
-
|
|
7061
|
-
|
|
7062
|
-
|
|
7063
|
-
|
|
7064
|
-
|
|
7065
|
-
|
|
7066
|
-
|
|
7067
|
-
|
|
7068
|
-
|
|
7069
|
-
|
|
7070
|
-
|
|
7071
|
-
|
|
7072
|
-
|
|
7073
|
-
|
|
7074
|
-
|
|
7075
|
-
|
|
7076
|
-
|
|
7077
|
-
|
|
7078
|
-
|
|
7079
|
-
event: "callback_delivery_failed",
|
|
7080
|
-
message_id: message.id,
|
|
7081
|
-
attempt: deliveryAttempt,
|
|
7082
|
-
error: failedConversation.callback_delivery.last_error
|
|
7083
|
-
});
|
|
7084
|
-
if (options.retryPending !== true &&
|
|
7085
|
-
options.disableCallbackRetry !== true &&
|
|
7086
|
-
deliveryAttempt <= CALLBACK_RETRY_DELAYS_MS.length) {
|
|
7087
|
-
const retryMonitor = startCallbackRetryMonitor({ statePath: options.statePath });
|
|
7088
|
-
const retryDelayMs = CALLBACK_RETRY_DELAYS_MS[Math.max(0, deliveryAttempt - 1)];
|
|
7089
|
-
const retryState = {
|
|
7090
|
-
...failedConversation,
|
|
7091
|
-
callback_delivery: {
|
|
7092
|
-
...failedConversation.callback_delivery,
|
|
7939
|
+
return nextConversation;
|
|
7940
|
+
}
|
|
7941
|
+
const failedAt = new Date().toISOString();
|
|
7942
|
+
const lastError = result.error instanceof Error
|
|
7943
|
+
? result.error.message
|
|
7944
|
+
: String(result.error);
|
|
7945
|
+
const ownsConversationStatus = currentDelivery.preserve_conversation_status !== true &&
|
|
7946
|
+
current.status === "callback_pending";
|
|
7947
|
+
const shouldLaunchRetry = prepared.options.retryPending !== true &&
|
|
7948
|
+
prepared.options.disableCallbackRetry !== true &&
|
|
7949
|
+
prepared.deliveryAttempt <= CALLBACK_RETRY_DELAYS_MS.length;
|
|
7950
|
+
const retryDelayMs = CALLBACK_RETRY_DELAYS_MS[Math.max(0, prepared.deliveryAttempt - 1)];
|
|
7951
|
+
const retryMonitor = shouldLaunchRetry
|
|
7952
|
+
? startCallbackRetryMonitor({ statePath: prepared.statePath })
|
|
7953
|
+
: undefined;
|
|
7954
|
+
const nextAttemptAt = retryMonitor
|
|
7955
|
+
? new Date(Date.now() + retryDelayMs).toISOString()
|
|
7956
|
+
: undefined;
|
|
7957
|
+
const failedConversation = {
|
|
7958
|
+
...current,
|
|
7959
|
+
status: ownsConversationStatus ? "callback_failed" : current.status,
|
|
7960
|
+
callback_delivery: {
|
|
7961
|
+
...currentDelivery,
|
|
7962
|
+
status: "failed",
|
|
7963
|
+
failed_at: failedAt,
|
|
7964
|
+
last_error: lastError,
|
|
7965
|
+
...(retryMonitor
|
|
7966
|
+
? {
|
|
7093
7967
|
retry_monitor_pid: retryMonitor.pid ?? null,
|
|
7094
|
-
next_attempt_at:
|
|
7968
|
+
next_attempt_at: nextAttemptAt
|
|
7095
7969
|
}
|
|
7096
|
-
|
|
7097
|
-
|
|
7098
|
-
|
|
7099
|
-
|
|
7100
|
-
|
|
7101
|
-
|
|
7102
|
-
|
|
7103
|
-
|
|
7104
|
-
|
|
7105
|
-
|
|
7106
|
-
|
|
7970
|
+
: {})
|
|
7971
|
+
},
|
|
7972
|
+
updated_at: ownsConversationStatus ? failedAt : current.updated_at
|
|
7973
|
+
};
|
|
7974
|
+
saveState(prepared.statePath, failedConversation);
|
|
7975
|
+
appendEvent(prepared.logPath, {
|
|
7976
|
+
ts: failedAt,
|
|
7977
|
+
conversation_id: current.conversation_id,
|
|
7978
|
+
event: "callback_delivery_failed",
|
|
7979
|
+
message_id: prepared.message.id,
|
|
7980
|
+
attempt: prepared.deliveryAttempt,
|
|
7981
|
+
error: lastError,
|
|
7982
|
+
state_preserved: !ownsConversationStatus
|
|
7983
|
+
});
|
|
7984
|
+
if (retryMonitor) {
|
|
7985
|
+
appendEvent(prepared.logPath, {
|
|
7986
|
+
ts: new Date().toISOString(),
|
|
7987
|
+
conversation_id: current.conversation_id,
|
|
7988
|
+
event: "callback_retry_monitor_launched",
|
|
7989
|
+
message_id: prepared.message.id,
|
|
7990
|
+
pid: retryMonitor.pid ?? null,
|
|
7991
|
+
next_attempt_at: nextAttemptAt
|
|
7992
|
+
});
|
|
7107
7993
|
}
|
|
7108
|
-
|
|
7994
|
+
return failedConversation;
|
|
7995
|
+
}
|
|
7996
|
+
finally {
|
|
7997
|
+
releaseLock();
|
|
7998
|
+
}
|
|
7999
|
+
}
|
|
8000
|
+
function isRetryableCallbackDelivery(conversation, callbackDelivery) {
|
|
8001
|
+
if (!isRecord(callbackDelivery) ||
|
|
8002
|
+
!isRecord(callbackDelivery.message) ||
|
|
8003
|
+
!["pending", "failed"].includes(String(callbackDelivery.status ?? ""))) {
|
|
8004
|
+
return false;
|
|
7109
8005
|
}
|
|
8006
|
+
if (callbackDelivery.preserve_conversation_status !== true &&
|
|
8007
|
+
!["callback_pending", "callback_failed"].includes(conversation.status)) {
|
|
8008
|
+
return false;
|
|
8009
|
+
}
|
|
8010
|
+
const attemptPidValue = Number(callbackDelivery.attempt_pid);
|
|
8011
|
+
const attemptPid = Number.isSafeInteger(attemptPidValue) && attemptPidValue > 0
|
|
8012
|
+
? attemptPidValue
|
|
8013
|
+
: undefined;
|
|
8014
|
+
return callbackDelivery.status === "failed" ||
|
|
8015
|
+
attemptPid === undefined ||
|
|
8016
|
+
!isProcessAlive(attemptPid);
|
|
7110
8017
|
}
|
|
7111
8018
|
function deliverCallbackToOpenClaw({ options, statePath, logPath, conversation, message }) {
|
|
7112
8019
|
if (options.gatewayMethod) {
|
|
@@ -8304,6 +9211,32 @@ function isDuplicateMessage(events, message) {
|
|
|
8304
9211
|
return messageFingerprint(existing) === messageFingerprint(message);
|
|
8305
9212
|
});
|
|
8306
9213
|
}
|
|
9214
|
+
function exactLoggedMessageForRecovery(events, message) {
|
|
9215
|
+
const matchingId = events
|
|
9216
|
+
.filter((event) => event.event === "message")
|
|
9217
|
+
.map((event) => event.message ?? event)
|
|
9218
|
+
.filter((existing) => existing.id === message.id);
|
|
9219
|
+
if (matchingId.length === 0) {
|
|
9220
|
+
return false;
|
|
9221
|
+
}
|
|
9222
|
+
if (matchingId.length !== 1 ||
|
|
9223
|
+
canonicalJson(matchingId[0]) !== canonicalJson(message)) {
|
|
9224
|
+
throw new Error(`callback recovery message ${message.id} conflicts with its logged payload`);
|
|
9225
|
+
}
|
|
9226
|
+
return true;
|
|
9227
|
+
}
|
|
9228
|
+
function canonicalJson(value) {
|
|
9229
|
+
if (Array.isArray(value)) {
|
|
9230
|
+
return `[${value.map(canonicalJson).join(",")}]`;
|
|
9231
|
+
}
|
|
9232
|
+
if (isRecord(value)) {
|
|
9233
|
+
return `{${Object.keys(value)
|
|
9234
|
+
.sort()
|
|
9235
|
+
.map((key) => `${JSON.stringify(key)}:${canonicalJson(value[key])}`)
|
|
9236
|
+
.join(",")}}`;
|
|
9237
|
+
}
|
|
9238
|
+
return JSON.stringify(value) ?? "undefined";
|
|
9239
|
+
}
|
|
8307
9240
|
function messageFingerprint(message) {
|
|
8308
9241
|
return JSON.stringify({
|
|
8309
9242
|
conversation_id: message.conversation_id,
|