@scotthuang/agent-knock-knock 0.2.48 → 0.2.49
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 +19 -0
- package/README.md +77 -133
- package/dist/src/approval-policy.js +2 -2
- package/dist/src/approval-policy.js.map +1 -1
- package/dist/src/claude-hook-protocol.js.map +1 -1
- package/dist/src/claude-hook-store.js.map +1 -1
- package/dist/src/claude-local-transcript-provider.d.ts +40 -0
- package/dist/src/claude-local-transcript-provider.js +804 -0
- package/dist/src/claude-local-transcript-provider.js.map +1 -0
- package/dist/src/claude-terminal-agent-adapter.d.ts +3 -0
- package/dist/src/claude-terminal-agent-adapter.js +130 -46
- package/dist/src/claude-terminal-agent-adapter.js.map +1 -1
- package/dist/src/cli.js +1576 -802
- package/dist/src/cli.js.map +1 -1
- package/dist/src/codex-session-provider.js.map +1 -1
- package/dist/src/codex-store-adapter.js.map +1 -1
- package/dist/src/codex-terminal-agent-adapter.js.map +1 -1
- package/dist/src/doctor-capabilities.d.ts +22 -0
- package/dist/src/doctor-capabilities.js +29 -0
- package/dist/src/doctor-capabilities.js.map +1 -0
- package/dist/src/executors.js.map +1 -1
- package/dist/src/openclaw-plugin-helpers.js.map +1 -1
- package/dist/src/openclaw-plugin.js +2 -2
- package/dist/src/openclaw-plugin.js.map +1 -1
- package/dist/src/runtime-log.js.map +1 -1
- package/dist/src/terminal-agent-adapter.js.map +1 -1
- package/dist/src/terminal-agent-bridge.d.ts +19 -1
- package/dist/src/terminal-agent-bridge.js +90 -11
- package/dist/src/terminal-agent-bridge.js.map +1 -1
- package/dist/src/terminal-agent-registry.js.map +1 -1
- package/dist/src/terminal-control-provider.js.map +1 -1
- package/dist/src/terminal-process-source.js.map +1 -1
- package/dist/src/transcript.js.map +1 -1
- package/openclaw.plugin.json +2 -2
- package/package.json +3 -3
- package/templates/openclaw-skills/agent-knock-knock/SKILL.md +7 -7
package/dist/src/cli.js
CHANGED
|
@@ -7,9 +7,10 @@ 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
11
|
import { ClaudeHookStore, ClaudeHookStoreError, defaultClaudeHookStoreDir } from "./claude-hook-store.js";
|
|
11
12
|
import { claudePermissionHookOutput } from "./claude-hook-protocol.js";
|
|
12
|
-
import { defaultClaudeSettingsPath,
|
|
13
|
+
import { defaultClaudeSettingsPath, loadTrustedClaudeTokenjuiceLaunchers } from "./claude-hook-installer.js";
|
|
13
14
|
import { CodexLocalSessionProvider } from "./codex-local-session-provider.js";
|
|
14
15
|
import { CodexStoreAdapter } from "./codex-store-adapter.js";
|
|
15
16
|
import { applyMessageToConversation, budgetAction, createConversation, createMessage, executorForConversation, extractStructuredMessage, parseMessageJson, resolveExecutor } from "./protocol.js";
|
|
@@ -17,7 +18,7 @@ import { EXECUTOR_KINDS, acpxCommandForExecutor, executorDefinitionForKind, mode
|
|
|
17
18
|
import { executorBootstrapPrompt } from "./bootstrap.js";
|
|
18
19
|
import { redactString, writeRuntimeLog } from "./runtime-log.js";
|
|
19
20
|
import { formatTranscript, readNdjsonLog } from "./transcript.js";
|
|
20
|
-
import { appendEvent, defaultStoreDir, listConversations, logPathForStatePath, loadConversationById, loadState, messageEvent, pathsForConversation, pathsForConversationDir, saveState, statePathForConversationId } from "./store.js";
|
|
21
|
+
import { appendEvent, defaultStoreDir, ensureDir, listConversations, logPathForStatePath, loadConversationById, loadState, messageEvent, pathsForConversation, pathsForConversationDir, saveState, statePathForConversationId } from "./store.js";
|
|
21
22
|
import { planFork, planTakeover } from "./session-takeover-planner.js";
|
|
22
23
|
import { StaticTerminalControlProvider, TmuxTerminalControlProvider, terminalPaneContainsProcess } from "./terminal-control-provider.js";
|
|
23
24
|
import { parseTerminalConversationId } from "./terminal-agent-adapter.js";
|
|
@@ -25,10 +26,13 @@ import { createProductionTerminalAgentRegistry } from "./terminal-agent-registry
|
|
|
25
26
|
import { StaticTerminalProcessSource, SystemTerminalProcessSource } from "./terminal-process-source.js";
|
|
26
27
|
import { TerminalAgentBridge } from "./terminal-agent-bridge.js";
|
|
27
28
|
import { evaluateApprovalPolicy } from "./approval-policy.js";
|
|
29
|
+
import { evaluateDoctorCapabilities } from "./doctor-capabilities.js";
|
|
28
30
|
const DEFAULT_IDLE_TIMEOUT_MINUTES = 10080;
|
|
29
31
|
const DEFAULT_AGENT_TIMEOUT_MINUTES = 60;
|
|
30
32
|
const DEFAULT_AGENT_HARD_TIMEOUT_MINUTES = 720;
|
|
31
33
|
const DEFAULT_MONITOR_POLL_INTERVAL_MS = 5000;
|
|
34
|
+
const CLAUDE_SCREEN_APPROVAL_TTL_MS = 10 * 60 * 1000;
|
|
35
|
+
const CALLBACK_DELIVERY_TIMEOUT_MS = 30_000;
|
|
32
36
|
const CALLBACK_RETRY_DELAYS_MS = [5000, 15000, 60000, 60000];
|
|
33
37
|
const TERMINAL_BRIDGE_MONITOR_LOCK_VERSION = 1;
|
|
34
38
|
const MINIMUM_NODE_VERSION = "22.14.0";
|
|
@@ -155,7 +159,7 @@ async function runCommand(commandName, options) {
|
|
|
155
159
|
runRecover(options);
|
|
156
160
|
}
|
|
157
161
|
else if (commandName === "close") {
|
|
158
|
-
runClose(options);
|
|
162
|
+
await runClose(options);
|
|
159
163
|
}
|
|
160
164
|
else if (commandName === "transcript") {
|
|
161
165
|
runTranscript(options);
|
|
@@ -164,7 +168,7 @@ async function runCommand(commandName, options) {
|
|
|
164
168
|
runInstallOpenClaw(options);
|
|
165
169
|
}
|
|
166
170
|
else if (commandName === "install-claude-hooks") {
|
|
167
|
-
|
|
171
|
+
throw new Error("install-claude-hooks is no longer supported; Claude tmux control now works without modifying Claude Code settings");
|
|
168
172
|
}
|
|
169
173
|
else if (commandName === "claude-hook") {
|
|
170
174
|
await runClaudeHook(options);
|
|
@@ -239,34 +243,6 @@ function runInstallOpenClaw(options) {
|
|
|
239
243
|
: "Agent Knock Knock is installed. Try: AKK list"
|
|
240
244
|
});
|
|
241
245
|
}
|
|
242
|
-
function runInstallClaudeHooks(options) {
|
|
243
|
-
const configuredExecutable = stringValue(options.executablePath ?? options.binPath);
|
|
244
|
-
let executablePath;
|
|
245
|
-
if (configuredExecutable) {
|
|
246
|
-
executablePath = path.resolve(expandHome(configuredExecutable));
|
|
247
|
-
}
|
|
248
|
-
else {
|
|
249
|
-
try {
|
|
250
|
-
executablePath = resolveExecutable("agent-knock-knock");
|
|
251
|
-
}
|
|
252
|
-
catch {
|
|
253
|
-
executablePath = path.resolve(process.argv[1]);
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
const result = installClaudeHooks({
|
|
257
|
-
executablePath,
|
|
258
|
-
settingsPath: expandHome(options.settingsPath ?? options.settings),
|
|
259
|
-
dryRun: options.dryRun === true
|
|
260
|
-
});
|
|
261
|
-
printJson({
|
|
262
|
-
installed: result.written || !result.changed,
|
|
263
|
-
...result,
|
|
264
|
-
executablePath,
|
|
265
|
-
next: result.dryRun
|
|
266
|
-
? "Run again without --dry-run to install the Claude Code hooks."
|
|
267
|
-
: "Claude Code hooks are configured. Existing sessions pick them up through settings reload; start a new session if needed."
|
|
268
|
-
});
|
|
269
|
-
}
|
|
270
246
|
async function runClaudeHook(options) {
|
|
271
247
|
const rawInput = fs.readFileSync(0, "utf8");
|
|
272
248
|
let input;
|
|
@@ -345,7 +321,7 @@ function installOpenClawPlugin(openclawBin, root) {
|
|
|
345
321
|
return { mode: "replaced" };
|
|
346
322
|
}
|
|
347
323
|
function runDoctor(options) {
|
|
348
|
-
const commands = ["node", "openclaw", "acpx", "codex", "claude", "cursor"];
|
|
324
|
+
const commands = ["node", "openclaw", "tmux", "acpx", "codex", "claude", "cursor"];
|
|
349
325
|
const checks = commands.map((commandName) => {
|
|
350
326
|
const check = executableCheck(commandName);
|
|
351
327
|
return commandName === "node"
|
|
@@ -370,24 +346,23 @@ function runDoctor(options) {
|
|
|
370
346
|
exists: fs.existsSync(filePath)
|
|
371
347
|
};
|
|
372
348
|
});
|
|
373
|
-
const
|
|
374
|
-
.filter((check) => ["node", "openclaw", "acpx"].includes(check.command))
|
|
375
|
-
.every((check) => check.available &&
|
|
376
|
-
(check.command !== "node" ||
|
|
377
|
-
("version_supported" in check && check.version_supported === true)));
|
|
378
|
-
const agentOk = checks
|
|
379
|
-
.filter((check) => ["codex", "claude", "cursor"].includes(check.command))
|
|
380
|
-
.some((check) => check.available);
|
|
349
|
+
const capabilities = evaluateDoctorCapabilities(checks);
|
|
381
350
|
const filesOk = packageFiles.every((check) => check.exists);
|
|
382
|
-
const ok =
|
|
351
|
+
const ok = capabilities.coreOk && filesOk && capabilities.transportOk;
|
|
383
352
|
printJson({
|
|
384
353
|
ok,
|
|
385
354
|
package_root: root,
|
|
386
355
|
checks,
|
|
387
356
|
package_files: packageFiles,
|
|
357
|
+
capabilities: {
|
|
358
|
+
tmux: capabilities.tmux,
|
|
359
|
+
acp: capabilities.acp
|
|
360
|
+
},
|
|
388
361
|
notes: [
|
|
389
|
-
`Node.js ${MINIMUM_NODE_VERSION}
|
|
390
|
-
"
|
|
362
|
+
`Node.js ${MINIMUM_NODE_VERSION}+ and OpenClaw are required.`,
|
|
363
|
+
"Choose tmux (recommended), ACPX/ACP, or install both.",
|
|
364
|
+
"tmux supports Codex and Claude Code; ACPX supports Codex, Claude Code, and Cursor.",
|
|
365
|
+
"Claude tmux completion is hook-free and fails closed unless the local transcript schema is verified."
|
|
391
366
|
],
|
|
392
367
|
options
|
|
393
368
|
});
|
|
@@ -741,6 +716,11 @@ function createClaudeHookStore(options = {}) {
|
|
|
741
716
|
rootDir: expandHome(configuredRoot ?? defaultClaudeHookStoreDir())
|
|
742
717
|
});
|
|
743
718
|
}
|
|
719
|
+
function createConfiguredClaudeHookStore(options = {}) {
|
|
720
|
+
return stringValue(options.claudeHookStoreDir)
|
|
721
|
+
? createClaudeHookStore(options)
|
|
722
|
+
: undefined;
|
|
723
|
+
}
|
|
744
724
|
function loadClaudeAgentRows(options = {}) {
|
|
745
725
|
let value;
|
|
746
726
|
if (options.claudeAgentsJson !== undefined) {
|
|
@@ -793,6 +773,9 @@ function loadClaudeAgentRows(options = {}) {
|
|
|
793
773
|
...(stringValue(row.cwd) ? { cwd: stringValue(row.cwd) } : {}),
|
|
794
774
|
...(stringValue(row.kind) ? { kind: stringValue(row.kind) } : {}),
|
|
795
775
|
...(stringValue(row.sessionId) ? { sessionId: stringValue(row.sessionId) } : {}),
|
|
776
|
+
...(Number.isSafeInteger(Number(row.startedAt)) && Number(row.startedAt) > 0
|
|
777
|
+
? { startedAt: Number(row.startedAt) }
|
|
778
|
+
: {}),
|
|
796
779
|
...(stringValue(row.status) ? { status: stringValue(row.status) } : {})
|
|
797
780
|
}];
|
|
798
781
|
});
|
|
@@ -833,6 +816,7 @@ function isClaudeProcessCommand(commandText) {
|
|
|
833
816
|
/[\\/]\.local[\\/]share[\\/]claude[\\/]versions[\\/][^\\/\s]+$/u.test(executable);
|
|
834
817
|
}
|
|
835
818
|
function createRuntimeTerminalAgentRegistry(options) {
|
|
819
|
+
const claudeHookStore = createConfiguredClaudeHookStore(options);
|
|
836
820
|
return createProductionTerminalAgentRegistry({
|
|
837
821
|
overrides: [
|
|
838
822
|
createCodexTerminalAgentAdapter({
|
|
@@ -872,8 +856,18 @@ function createRuntimeTerminalAgentRegistry(options) {
|
|
|
872
856
|
}),
|
|
873
857
|
createClaudeTerminalAgentAdapter({
|
|
874
858
|
agentRows: loadClaudeAgentRows(options),
|
|
875
|
-
|
|
876
|
-
|
|
859
|
+
async detectDurableCompletion(request) {
|
|
860
|
+
return detectClaudeTranscriptCompletion(request, {
|
|
861
|
+
claudeHome: expandHome(options.claudeHome),
|
|
862
|
+
agentRows: loadClaudeAgentRows(options)
|
|
863
|
+
});
|
|
864
|
+
},
|
|
865
|
+
...(claudeHookStore
|
|
866
|
+
? {
|
|
867
|
+
hookStore: claudeHookStore,
|
|
868
|
+
trustedTokenjuiceLaunchers: loadTrustedClaudeTokenjuiceLaunchers(expandHome(options.claudeSettingsPath ?? defaultClaudeSettingsPath()))
|
|
869
|
+
}
|
|
870
|
+
: {})
|
|
877
871
|
})
|
|
878
872
|
]
|
|
879
873
|
});
|
|
@@ -1664,7 +1658,7 @@ function startExecutorMonitor({ statePath, logPath, pid, outputPath, agentTimeou
|
|
|
1664
1658
|
child.unref();
|
|
1665
1659
|
return child;
|
|
1666
1660
|
}
|
|
1667
|
-
function startTerminalBridgeMonitor({ statePath, logPath, agentTimeoutMinutes, agentHardTimeoutMinutes, pollIntervalMs, codexHome, claudeHookStoreDir }) {
|
|
1661
|
+
function startTerminalBridgeMonitor({ statePath, logPath, agentTimeoutMinutes, agentHardTimeoutMinutes, pollIntervalMs, codexHome, claudeHome, claudeHookStoreDir }) {
|
|
1668
1662
|
const args = [
|
|
1669
1663
|
new URL(import.meta.url).pathname,
|
|
1670
1664
|
"monitor",
|
|
@@ -1683,6 +1677,9 @@ function startTerminalBridgeMonitor({ statePath, logPath, agentTimeoutMinutes, a
|
|
|
1683
1677
|
if (codexHome) {
|
|
1684
1678
|
args.push("--codex-home", codexHome);
|
|
1685
1679
|
}
|
|
1680
|
+
if (claudeHome) {
|
|
1681
|
+
args.push("--claude-home", claudeHome);
|
|
1682
|
+
}
|
|
1686
1683
|
if (claudeHookStoreDir) {
|
|
1687
1684
|
args.push("--claude-hook-store-dir", claudeHookStoreDir);
|
|
1688
1685
|
}
|
|
@@ -1713,6 +1710,7 @@ function startTerminalBridgeMonitorForConversation({ conversation, statePath, lo
|
|
|
1713
1710
|
DEFAULT_AGENT_HARD_TIMEOUT_MINUTES),
|
|
1714
1711
|
pollIntervalMs: Number(options.monitorPollIntervalMs ?? DEFAULT_MONITOR_POLL_INTERVAL_MS),
|
|
1715
1712
|
codexHome: options.codexHome,
|
|
1713
|
+
claudeHome: options.claudeHome ?? nativeTakeover?.["claude_home"],
|
|
1716
1714
|
claudeHookStoreDir: options.claudeHookStoreDir ?? nativeTakeover?.["claude_hook_store_dir"]
|
|
1717
1715
|
});
|
|
1718
1716
|
}
|
|
@@ -1722,7 +1720,7 @@ function terminalBridgeEnabled(conversation) {
|
|
|
1722
1720
|
: undefined;
|
|
1723
1721
|
return nativeTakeover?.["terminal_bridge"] === true;
|
|
1724
1722
|
}
|
|
1725
|
-
function withTerminalBridgeState({ conversation, message, startedAt, agentTimeoutMinutes, agentHardTimeoutMinutes, preSendScreenFingerprint }) {
|
|
1723
|
+
function withTerminalBridgeState({ conversation, message, requestText, startedAt, agentTimeoutMinutes, agentHardTimeoutMinutes, preSendScreenFingerprint, claudeTranscriptAnchor, claudeHome }) {
|
|
1726
1724
|
const nativeTakeover = isRecord(conversation.native_session_takeover)
|
|
1727
1725
|
? conversation.native_session_takeover
|
|
1728
1726
|
: {};
|
|
@@ -1733,10 +1731,13 @@ function withTerminalBridgeState({ conversation, message, startedAt, agentTimeou
|
|
|
1733
1731
|
terminal_bridge: true,
|
|
1734
1732
|
terminal_bridge_started_at: startedAt,
|
|
1735
1733
|
terminal_bridge_message_id: message.id,
|
|
1736
|
-
terminal_bridge_request_text:
|
|
1737
|
-
terminal_bridge_request_hash: terminalBridgeRequestFingerprint(
|
|
1734
|
+
terminal_bridge_request_text: requestText,
|
|
1735
|
+
terminal_bridge_request_hash: terminalBridgeRequestFingerprint(requestText),
|
|
1738
1736
|
terminal_bridge_pre_send_screen_fingerprint: preSendScreenFingerprint,
|
|
1737
|
+
claude_transcript_anchor: claudeTranscriptAnchor,
|
|
1738
|
+
claude_home: claudeHome,
|
|
1739
1739
|
terminal_bridge_completion_claim: undefined,
|
|
1740
|
+
terminal_bridge_approval_dispatch: undefined,
|
|
1740
1741
|
terminal_bridge_monitor_lock_version: TERMINAL_BRIDGE_MONITOR_LOCK_VERSION,
|
|
1741
1742
|
terminal_bridge_monitor_started_at: startedAt,
|
|
1742
1743
|
terminal_bridge_last_activity_at: startedAt,
|
|
@@ -1749,6 +1750,9 @@ function withTerminalBridgeState({ conversation, message, startedAt, agentTimeou
|
|
|
1749
1750
|
};
|
|
1750
1751
|
}
|
|
1751
1752
|
function activateClaudeHookLease({ options, conversation, message, terminalControl, expiresAt }) {
|
|
1753
|
+
if (!stringValue(options.claudeHookStoreDir)) {
|
|
1754
|
+
return undefined;
|
|
1755
|
+
}
|
|
1752
1756
|
const runtime = terminalRuntimeIdentityForConversation(conversation, terminalControl);
|
|
1753
1757
|
const pid = runtime.pid;
|
|
1754
1758
|
const agentRow = pid === undefined
|
|
@@ -1855,7 +1859,9 @@ function renewClaudeHookLease(conversation, expiresAt) {
|
|
|
1855
1859
|
const nativeTakeover = isRecord(conversation?.native_session_takeover)
|
|
1856
1860
|
? conversation.native_session_takeover
|
|
1857
1861
|
: undefined;
|
|
1858
|
-
if (nativeTakeover?.agent !== "claude"
|
|
1862
|
+
if (nativeTakeover?.agent !== "claude" ||
|
|
1863
|
+
nativeTakeover.claude_hook_mode !== "enabled" ||
|
|
1864
|
+
!stringValue(nativeTakeover.claude_hook_store_dir)) {
|
|
1859
1865
|
return undefined;
|
|
1860
1866
|
}
|
|
1861
1867
|
const conversationId = stringValue(conversation.conversation_id);
|
|
@@ -2270,6 +2276,7 @@ async function runStatus(options) {
|
|
|
2270
2276
|
const terminalStatus = await terminalStatusForControl(terminalConversation.agent, terminalConversation.terminalControl, options, {
|
|
2271
2277
|
pid: terminalConversation.pid,
|
|
2272
2278
|
cwd: terminalConversation.terminalControl.currentPath,
|
|
2279
|
+
conversationId: terminalConversation.conversationId,
|
|
2273
2280
|
terminalTarget: terminalConversation.terminalControl.target
|
|
2274
2281
|
});
|
|
2275
2282
|
printJson({
|
|
@@ -2329,6 +2336,7 @@ async function runDescribe(options) {
|
|
|
2329
2336
|
const terminalStatus = await terminalStatusForControl(terminalConversation.agent, terminalConversation.terminalControl, options, {
|
|
2330
2337
|
pid: terminalConversation.pid,
|
|
2331
2338
|
cwd: terminalConversation.terminalControl.currentPath,
|
|
2339
|
+
conversationId: terminalConversation.conversationId,
|
|
2332
2340
|
terminalTarget: terminalConversation.terminalControl.target
|
|
2333
2341
|
});
|
|
2334
2342
|
if (terminalConversation.agent !== "codex") {
|
|
@@ -2496,21 +2504,49 @@ function terminalBridgeApprovalInstructions({ conversation, terminalControl, ter
|
|
|
2496
2504
|
"Do not use raw tmux, shell, or manual key presses for this approval. Do not approve without explicit user confirmation."
|
|
2497
2505
|
].filter((line) => line !== undefined).join("\n");
|
|
2498
2506
|
}
|
|
2499
|
-
function recordTerminalBridgeApprovalNotification({ statePath, logPath, terminalControl, terminalStatus, fingerprint }) {
|
|
2507
|
+
function recordTerminalBridgeApprovalNotification({ statePath, logPath, terminalControl, terminalStatus, fingerprint, expectedConversation, onRecorded }) {
|
|
2500
2508
|
const releaseLock = acquireFileLock(`${statePath}.lock`);
|
|
2501
2509
|
try {
|
|
2502
2510
|
const conversation = loadState(statePath);
|
|
2511
|
+
const currentNativeTakeover = isRecord(conversation.native_session_takeover)
|
|
2512
|
+
? conversation.native_session_takeover
|
|
2513
|
+
: undefined;
|
|
2514
|
+
const currentTerminalControl = terminalControlFromTakeover(currentNativeTakeover);
|
|
2515
|
+
if (!isWaitingForAgent(conversation.status) ||
|
|
2516
|
+
conversation.conversation_id !== expectedConversation.conversationId ||
|
|
2517
|
+
conversation.status !== expectedConversation.status ||
|
|
2518
|
+
conversation.updated_at !== expectedConversation.updatedAt ||
|
|
2519
|
+
currentNativeTakeover?.terminal_bridge !== true ||
|
|
2520
|
+
stringValue(currentNativeTakeover.terminal_bridge_message_id) !==
|
|
2521
|
+
expectedConversation.messageId ||
|
|
2522
|
+
!currentTerminalControl ||
|
|
2523
|
+
currentTerminalControl.kind !== terminalControl.kind ||
|
|
2524
|
+
currentTerminalControl.target !== terminalControl.target ||
|
|
2525
|
+
currentTerminalControl.socketPath !== terminalControl.socketPath ||
|
|
2526
|
+
currentTerminalControl.panePid !== terminalControl.panePid) {
|
|
2527
|
+
return {
|
|
2528
|
+
conversation,
|
|
2529
|
+
duplicate: false,
|
|
2530
|
+
stale: true,
|
|
2531
|
+
recorded: undefined
|
|
2532
|
+
};
|
|
2533
|
+
}
|
|
2503
2534
|
const nativeTakeover = isRecord(conversation.native_session_takeover)
|
|
2504
2535
|
? { ...conversation.native_session_takeover }
|
|
2505
2536
|
: {};
|
|
2506
2537
|
const previousApproval = isRecord(nativeTakeover.terminal_bridge_approval)
|
|
2507
2538
|
? nativeTakeover.terminal_bridge_approval
|
|
2508
2539
|
: undefined;
|
|
2509
|
-
|
|
2540
|
+
const previousNotifiedAt = validTimestampMs(previousApproval?.notified_at);
|
|
2541
|
+
if (previousApproval?.fingerprint === fingerprint &&
|
|
2542
|
+
previousNotifiedAt !== undefined &&
|
|
2543
|
+
Date.now() - previousNotifiedAt <= CLAUDE_SCREEN_APPROVAL_TTL_MS) {
|
|
2510
2544
|
return {
|
|
2511
2545
|
conversation,
|
|
2512
2546
|
duplicate: true,
|
|
2513
|
-
|
|
2547
|
+
stale: false,
|
|
2548
|
+
previousApproval,
|
|
2549
|
+
recorded: undefined
|
|
2514
2550
|
};
|
|
2515
2551
|
}
|
|
2516
2552
|
const now = new Date().toISOString();
|
|
@@ -2537,66 +2573,88 @@ function recordTerminalBridgeApprovalNotification({ statePath, logPath, terminal
|
|
|
2537
2573
|
});
|
|
2538
2574
|
return {
|
|
2539
2575
|
conversation: nextConversation,
|
|
2540
|
-
duplicate: false
|
|
2576
|
+
duplicate: false,
|
|
2577
|
+
stale: false,
|
|
2578
|
+
recorded: onRecorded?.(nextConversation)
|
|
2541
2579
|
};
|
|
2542
2580
|
}
|
|
2543
2581
|
finally {
|
|
2544
2582
|
releaseLock();
|
|
2545
2583
|
}
|
|
2546
2584
|
}
|
|
2547
|
-
function prepareManagedSend({ options, statePath, logPath, messageBody }) {
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2585
|
+
function prepareManagedSend({ options, statePath, logPath, messageBody, stateLockHeld = false, persist = true, rejectTerminalControl = false }) {
|
|
2586
|
+
if (!stateLockHeld) {
|
|
2587
|
+
const releaseLock = acquireFileLock(`${statePath}.lock`);
|
|
2588
|
+
try {
|
|
2589
|
+
return prepareManagedSend({
|
|
2590
|
+
options,
|
|
2591
|
+
statePath,
|
|
2592
|
+
logPath,
|
|
2593
|
+
messageBody,
|
|
2594
|
+
stateLockHeld: true,
|
|
2595
|
+
persist,
|
|
2596
|
+
rejectTerminalControl
|
|
2597
|
+
});
|
|
2553
2598
|
}
|
|
2554
|
-
|
|
2555
|
-
|
|
2599
|
+
finally {
|
|
2600
|
+
releaseLock();
|
|
2556
2601
|
}
|
|
2557
|
-
|
|
2558
|
-
|
|
2602
|
+
}
|
|
2603
|
+
const conversation = loadState(statePath);
|
|
2604
|
+
if (["done", "failed", "closed", "cancelled"].includes(conversation.status)) {
|
|
2605
|
+
throw new Error(`cannot send to ${conversation.conversation_id}; conversation is ${conversation.status}`);
|
|
2606
|
+
}
|
|
2607
|
+
if (conversation.status === "needs_recovery") {
|
|
2608
|
+
throw new Error(`cannot send to ${conversation.conversation_id}; choose recover, close, or delegate a new task first`);
|
|
2609
|
+
}
|
|
2610
|
+
if (conversation.status === "needs_model_selection" && !options.model) {
|
|
2611
|
+
throw new Error(`cannot send to ${conversation.conversation_id}; choose a supported model with --model first`);
|
|
2612
|
+
}
|
|
2613
|
+
const executor = executorForConversation(conversation);
|
|
2614
|
+
const type = options.type ??
|
|
2615
|
+
(conversation.status === "waiting_for_openclaw" ? "answer" : "task");
|
|
2616
|
+
const nativeTakeoverForSend = isRecord(conversation.native_session_takeover)
|
|
2617
|
+
? conversation.native_session_takeover
|
|
2618
|
+
: undefined;
|
|
2619
|
+
if (rejectTerminalControl &&
|
|
2620
|
+
terminalControlFromTakeover(nativeTakeoverForSend)) {
|
|
2621
|
+
throw new Error("terminal control changed while waiting to send; refresh status and retry");
|
|
2622
|
+
}
|
|
2623
|
+
const forkTakeoverForSend = isRecord(conversation.fork_context_takeover)
|
|
2624
|
+
? conversation.fork_context_takeover
|
|
2625
|
+
: undefined;
|
|
2626
|
+
const needsNativeTakeoverBootstrap = nativeTakeoverForSend?.["needs_bootstrap"] === true;
|
|
2627
|
+
const needsForkTakeoverBootstrap = forkTakeoverForSend?.["needs_bootstrap"] === true;
|
|
2628
|
+
const message = createMessage({
|
|
2629
|
+
conversation,
|
|
2630
|
+
from: "openclaw",
|
|
2631
|
+
to: executor.actor,
|
|
2632
|
+
type,
|
|
2633
|
+
body: messageBody,
|
|
2634
|
+
metadata: {
|
|
2635
|
+
executor_kind: executor.kind,
|
|
2636
|
+
executor_session: executor.session
|
|
2559
2637
|
}
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
body: messageBody,
|
|
2577
|
-
metadata: {
|
|
2578
|
-
executor_kind: executor.kind,
|
|
2579
|
-
executor_session: executor.session
|
|
2638
|
+
});
|
|
2639
|
+
const previousModelSelection = isRecord(conversation.model_selection)
|
|
2640
|
+
? conversation.model_selection
|
|
2641
|
+
: {};
|
|
2642
|
+
const nextConversation = {
|
|
2643
|
+
...applyMessageToConversation(conversation, message),
|
|
2644
|
+
executor,
|
|
2645
|
+
claude_session: executor.kind === "claude"
|
|
2646
|
+
? executor.session
|
|
2647
|
+
: conversation.claude_session,
|
|
2648
|
+
executor_model: options.model ?? conversation.executor_model,
|
|
2649
|
+
model_selection: conversation.status === "needs_model_selection"
|
|
2650
|
+
? {
|
|
2651
|
+
...previousModelSelection,
|
|
2652
|
+
resolved_at: new Date().toISOString(),
|
|
2653
|
+
selected_model: options.model
|
|
2580
2654
|
}
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
: {};
|
|
2585
|
-
const nextConversation = {
|
|
2586
|
-
...applyMessageToConversation(conversation, message),
|
|
2587
|
-
executor,
|
|
2588
|
-
claude_session: executor.kind === "claude"
|
|
2589
|
-
? executor.session
|
|
2590
|
-
: conversation.claude_session,
|
|
2591
|
-
executor_model: options.model ?? conversation.executor_model,
|
|
2592
|
-
model_selection: conversation.status === "needs_model_selection"
|
|
2593
|
-
? {
|
|
2594
|
-
...previousModelSelection,
|
|
2595
|
-
resolved_at: new Date().toISOString(),
|
|
2596
|
-
selected_model: options.model
|
|
2597
|
-
}
|
|
2598
|
-
: conversation.model_selection
|
|
2599
|
-
};
|
|
2655
|
+
: conversation.model_selection
|
|
2656
|
+
};
|
|
2657
|
+
if (persist) {
|
|
2600
2658
|
saveState(statePath, nextConversation);
|
|
2601
2659
|
appendEvent(logPath, messageEvent(message));
|
|
2602
2660
|
runtimeLog("info", "message_created", {
|
|
@@ -2608,20 +2666,17 @@ function prepareManagedSend({ options, statePath, logPath, messageBody }) {
|
|
|
2608
2666
|
event_log_path: logPath,
|
|
2609
2667
|
message: textSummary(messageBody)
|
|
2610
2668
|
});
|
|
2611
|
-
return {
|
|
2612
|
-
conversation,
|
|
2613
|
-
executor,
|
|
2614
|
-
nativeTakeoverForSend,
|
|
2615
|
-
forkTakeoverForSend,
|
|
2616
|
-
needsNativeTakeoverBootstrap,
|
|
2617
|
-
needsForkTakeoverBootstrap,
|
|
2618
|
-
message,
|
|
2619
|
-
nextConversation
|
|
2620
|
-
};
|
|
2621
|
-
}
|
|
2622
|
-
finally {
|
|
2623
|
-
releaseLock();
|
|
2624
2669
|
}
|
|
2670
|
+
return {
|
|
2671
|
+
conversation,
|
|
2672
|
+
executor,
|
|
2673
|
+
nativeTakeoverForSend,
|
|
2674
|
+
forkTakeoverForSend,
|
|
2675
|
+
needsNativeTakeoverBootstrap,
|
|
2676
|
+
needsForkTakeoverBootstrap,
|
|
2677
|
+
message,
|
|
2678
|
+
nextConversation
|
|
2679
|
+
};
|
|
2625
2680
|
}
|
|
2626
2681
|
async function runSend(options) {
|
|
2627
2682
|
const messageBody = required(options.message ?? options.request, "--message is required");
|
|
@@ -2632,25 +2687,43 @@ async function runSend(options) {
|
|
|
2632
2687
|
const terminalConversation = await resolveTerminalConversationFromOptions(options);
|
|
2633
2688
|
if (terminalConversation) {
|
|
2634
2689
|
if (options.background) {
|
|
2635
|
-
const
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2690
|
+
const releaseTerminalLock = acquireFileLock(terminalBridgeSendLockPath(storeDirFromOptions(options), terminalConversation.terminalControl), { timeoutMs: 30000 });
|
|
2691
|
+
let releaseStateLock;
|
|
2692
|
+
try {
|
|
2693
|
+
const managed = createManagedTerminalConversationFromRawId({
|
|
2694
|
+
options,
|
|
2695
|
+
conversationId: terminalConversation.conversationId,
|
|
2696
|
+
agent: terminalConversation.agent,
|
|
2697
|
+
pid: terminalConversation.pid,
|
|
2698
|
+
messageBody,
|
|
2699
|
+
terminalControl: terminalConversation.terminalControl
|
|
2700
|
+
});
|
|
2701
|
+
ensureDir(path.dirname(managed.statePath));
|
|
2702
|
+
releaseStateLock = acquireFileLock(`${managed.statePath}.lock`);
|
|
2703
|
+
await runTerminalControlSend({
|
|
2704
|
+
options,
|
|
2705
|
+
conversation: managed.conversation,
|
|
2706
|
+
nextConversation: managed.nextConversation,
|
|
2707
|
+
statePath: managed.statePath,
|
|
2708
|
+
logPath: managed.logPath,
|
|
2709
|
+
executor: managed.executor,
|
|
2710
|
+
message: managed.message,
|
|
2711
|
+
terminalControl: terminalConversation.terminalControl,
|
|
2712
|
+
needsNativeTakeoverBootstrap: true,
|
|
2713
|
+
terminalSendLockHeld: true,
|
|
2714
|
+
terminalStateLockHeld: true,
|
|
2715
|
+
recordMessageAfterSend: true,
|
|
2716
|
+
recordRawAttachmentAfterSend: true
|
|
2717
|
+
});
|
|
2718
|
+
}
|
|
2719
|
+
finally {
|
|
2720
|
+
try {
|
|
2721
|
+
releaseStateLock?.();
|
|
2722
|
+
}
|
|
2723
|
+
finally {
|
|
2724
|
+
releaseTerminalLock();
|
|
2725
|
+
}
|
|
2726
|
+
}
|
|
2654
2727
|
return;
|
|
2655
2728
|
}
|
|
2656
2729
|
await runTerminalConversationSend({
|
|
@@ -2665,15 +2738,66 @@ async function runSend(options) {
|
|
|
2665
2738
|
}
|
|
2666
2739
|
const loaded = loadConversationFromOptions(options);
|
|
2667
2740
|
const { statePath, logPath } = loaded;
|
|
2668
|
-
await migrateLegacyTerminalAgentIdentity({
|
|
2741
|
+
const migratedConversation = await migrateLegacyTerminalAgentIdentity({
|
|
2669
2742
|
...loaded,
|
|
2670
2743
|
options
|
|
2671
2744
|
});
|
|
2745
|
+
const migratedTakeover = isRecord(migratedConversation.native_session_takeover)
|
|
2746
|
+
? migratedConversation.native_session_takeover
|
|
2747
|
+
: undefined;
|
|
2748
|
+
const migratedTerminalControl = terminalControlFromTakeover(migratedTakeover);
|
|
2749
|
+
if (migratedTerminalControl) {
|
|
2750
|
+
const releaseTerminalLock = acquireFileLock(terminalBridgeSendLockPath(storeDirFromOptions(options), migratedTerminalControl), { timeoutMs: 30000 });
|
|
2751
|
+
let releaseStateLock;
|
|
2752
|
+
try {
|
|
2753
|
+
releaseStateLock = acquireFileLock(`${statePath}.lock`);
|
|
2754
|
+
const prepared = prepareManagedSend({
|
|
2755
|
+
options,
|
|
2756
|
+
statePath,
|
|
2757
|
+
logPath,
|
|
2758
|
+
messageBody,
|
|
2759
|
+
stateLockHeld: true,
|
|
2760
|
+
persist: false
|
|
2761
|
+
});
|
|
2762
|
+
const currentTerminalControl = terminalControlFromTakeover(prepared.nativeTakeoverForSend);
|
|
2763
|
+
if (!currentTerminalControl ||
|
|
2764
|
+
currentTerminalControl.kind !== migratedTerminalControl.kind ||
|
|
2765
|
+
currentTerminalControl.target !== migratedTerminalControl.target ||
|
|
2766
|
+
currentTerminalControl.socketPath !== migratedTerminalControl.socketPath ||
|
|
2767
|
+
currentTerminalControl.panePid !== migratedTerminalControl.panePid) {
|
|
2768
|
+
throw new Error("terminal control changed while waiting to send; refresh status and retry");
|
|
2769
|
+
}
|
|
2770
|
+
await runTerminalControlSend({
|
|
2771
|
+
options,
|
|
2772
|
+
conversation: prepared.conversation,
|
|
2773
|
+
nextConversation: prepared.nextConversation,
|
|
2774
|
+
statePath,
|
|
2775
|
+
logPath,
|
|
2776
|
+
executor: prepared.executor,
|
|
2777
|
+
message: prepared.message,
|
|
2778
|
+
terminalControl: currentTerminalControl,
|
|
2779
|
+
needsNativeTakeoverBootstrap: prepared.needsNativeTakeoverBootstrap,
|
|
2780
|
+
terminalSendLockHeld: true,
|
|
2781
|
+
terminalStateLockHeld: true,
|
|
2782
|
+
recordMessageAfterSend: true
|
|
2783
|
+
});
|
|
2784
|
+
}
|
|
2785
|
+
finally {
|
|
2786
|
+
try {
|
|
2787
|
+
releaseStateLock?.();
|
|
2788
|
+
}
|
|
2789
|
+
finally {
|
|
2790
|
+
releaseTerminalLock();
|
|
2791
|
+
}
|
|
2792
|
+
}
|
|
2793
|
+
return;
|
|
2794
|
+
}
|
|
2672
2795
|
const prepared = prepareManagedSend({
|
|
2673
2796
|
options,
|
|
2674
2797
|
statePath,
|
|
2675
2798
|
logPath,
|
|
2676
|
-
messageBody
|
|
2799
|
+
messageBody,
|
|
2800
|
+
rejectTerminalControl: true
|
|
2677
2801
|
});
|
|
2678
2802
|
const { conversation, executor, nativeTakeoverForSend, forkTakeoverForSend, needsNativeTakeoverBootstrap, needsForkTakeoverBootstrap, message, nextConversation } = prepared;
|
|
2679
2803
|
const executorEnv = environmentForExecutor(executor, {
|
|
@@ -2995,12 +3119,84 @@ async function runApprove(options) {
|
|
|
2995
3119
|
if (!terminalControl) {
|
|
2996
3120
|
throw new Error(`conversation ${conversation.conversation_id} is not controlled through a terminal`);
|
|
2997
3121
|
}
|
|
3122
|
+
if (["done", "failed", "closed", "cancelled"].includes(conversation.status)) {
|
|
3123
|
+
throw new Error(`cannot approve ${conversation.conversation_id}; conversation is ${conversation.status}`);
|
|
3124
|
+
}
|
|
2998
3125
|
const executor = executorForConversation(conversation);
|
|
2999
3126
|
const monitoredApproval = isRecord(nativeTakeover?.["terminal_bridge_approval"])
|
|
3000
3127
|
? nativeTakeover.terminal_bridge_approval
|
|
3001
3128
|
: undefined;
|
|
3002
|
-
const
|
|
3129
|
+
const suppliedExpectedFingerprint = stringValue(options.expectedApprovalFingerprint);
|
|
3130
|
+
const expectedFingerprint = suppliedExpectedFingerprint ??
|
|
3003
3131
|
stringValue(monitoredApproval?.fingerprint);
|
|
3132
|
+
const hooklessClaudeScreenApproval = executor.kind === "claude" &&
|
|
3133
|
+
nativeTakeover?.claude_hook_mode !== "enabled";
|
|
3134
|
+
if (hooklessClaudeScreenApproval) {
|
|
3135
|
+
const monitoredState = isRecord(monitoredApproval?.approval_state)
|
|
3136
|
+
? monitoredApproval.approval_state
|
|
3137
|
+
: undefined;
|
|
3138
|
+
const pendingDispatch = isRecord(nativeTakeover?.terminal_bridge_approval_dispatch)
|
|
3139
|
+
? nativeTakeover.terminal_bridge_approval_dispatch
|
|
3140
|
+
: undefined;
|
|
3141
|
+
const notifiedAt = validTimestampMs(monitoredApproval?.notified_at);
|
|
3142
|
+
if (conversation.status !== "waiting_for_openclaw" ||
|
|
3143
|
+
monitoredState?.decision_mode !== "keys" ||
|
|
3144
|
+
!stringValue(monitoredApproval?.fingerprint)) {
|
|
3145
|
+
printJson({
|
|
3146
|
+
conversation,
|
|
3147
|
+
approved: false,
|
|
3148
|
+
blocked: true,
|
|
3149
|
+
reason: "Claude screen approval requires a current managed-turn approval notification",
|
|
3150
|
+
terminal_control: terminalControl
|
|
3151
|
+
});
|
|
3152
|
+
return;
|
|
3153
|
+
}
|
|
3154
|
+
if (notifiedAt === undefined ||
|
|
3155
|
+
Date.now() - notifiedAt > CLAUDE_SCREEN_APPROVAL_TTL_MS) {
|
|
3156
|
+
printJson({
|
|
3157
|
+
conversation,
|
|
3158
|
+
approved: false,
|
|
3159
|
+
blocked: true,
|
|
3160
|
+
reason: "Claude screen approval expired; inspect and resolve the terminal manually",
|
|
3161
|
+
terminal_control: terminalControl
|
|
3162
|
+
});
|
|
3163
|
+
return;
|
|
3164
|
+
}
|
|
3165
|
+
if (!suppliedExpectedFingerprint ||
|
|
3166
|
+
expectedFingerprint !== monitoredApproval?.fingerprint) {
|
|
3167
|
+
printJson({
|
|
3168
|
+
conversation,
|
|
3169
|
+
approved: false,
|
|
3170
|
+
blocked: true,
|
|
3171
|
+
reason: "Claude screen approval requires the latest notified fingerprint",
|
|
3172
|
+
terminal_control: terminalControl
|
|
3173
|
+
});
|
|
3174
|
+
return;
|
|
3175
|
+
}
|
|
3176
|
+
if (pendingDispatch?.state === "reserved" &&
|
|
3177
|
+
pendingDispatch.terminal_bridge_message_id ===
|
|
3178
|
+
nativeTakeover?.terminal_bridge_message_id) {
|
|
3179
|
+
printJson({
|
|
3180
|
+
conversation,
|
|
3181
|
+
approved: false,
|
|
3182
|
+
blocked: true,
|
|
3183
|
+
reason: "a previous Claude approval dispatch has an uncertain outcome; inspect and resolve the terminal manually",
|
|
3184
|
+
terminal_control: terminalControl
|
|
3185
|
+
});
|
|
3186
|
+
return;
|
|
3187
|
+
}
|
|
3188
|
+
if (expectedFingerprint ===
|
|
3189
|
+
stringValue(nativeTakeover?.terminal_bridge_last_approval_fingerprint)) {
|
|
3190
|
+
printJson({
|
|
3191
|
+
conversation,
|
|
3192
|
+
approved: false,
|
|
3193
|
+
blocked: true,
|
|
3194
|
+
reason: "Claude screen approval fingerprint was already consumed",
|
|
3195
|
+
terminal_control: terminalControl
|
|
3196
|
+
});
|
|
3197
|
+
return;
|
|
3198
|
+
}
|
|
3199
|
+
}
|
|
3004
3200
|
const autoApproved = options.autoApproved === true;
|
|
3005
3201
|
const policyRuleId = stringValue(options.policyRuleId);
|
|
3006
3202
|
const policyFingerprint = stringValue(options.policyFingerprint);
|
|
@@ -3009,308 +3205,443 @@ async function runApprove(options) {
|
|
|
3009
3205
|
: undefined;
|
|
3010
3206
|
const runtimeIdentity = terminalRuntimeIdentityForConversation(conversation, terminalControl);
|
|
3011
3207
|
let executorPolicyDecision;
|
|
3012
|
-
const
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3208
|
+
const releaseTerminalLock = acquireFileLock(terminalBridgeSendLockPath(storeDirFromOptions(options), terminalControl), { timeoutMs: 30000 });
|
|
3209
|
+
let terminalLockReleased = false;
|
|
3210
|
+
const releaseApprovalTerminalLock = () => {
|
|
3211
|
+
if (!terminalLockReleased) {
|
|
3212
|
+
terminalLockReleased = true;
|
|
3213
|
+
releaseTerminalLock();
|
|
3214
|
+
}
|
|
3215
|
+
};
|
|
3216
|
+
let releaseStateLock;
|
|
3217
|
+
const releaseApprovalStateLock = () => {
|
|
3218
|
+
if (releaseStateLock) {
|
|
3219
|
+
const release = releaseStateLock;
|
|
3220
|
+
releaseStateLock = undefined;
|
|
3221
|
+
release();
|
|
3222
|
+
}
|
|
3223
|
+
};
|
|
3224
|
+
try {
|
|
3225
|
+
let approval;
|
|
3226
|
+
let lockedConversation = conversation;
|
|
3227
|
+
const currentConversation = loadState(statePath);
|
|
3228
|
+
const currentTakeover = isRecord(currentConversation.native_session_takeover)
|
|
3229
|
+
? currentConversation.native_session_takeover
|
|
3230
|
+
: undefined;
|
|
3231
|
+
const currentControl = terminalControlFromTakeover(currentTakeover);
|
|
3232
|
+
const currentApproval = isRecord(currentTakeover?.terminal_bridge_approval)
|
|
3233
|
+
? currentTakeover.terminal_bridge_approval
|
|
3234
|
+
: undefined;
|
|
3235
|
+
if (currentConversation.status !== conversation.status ||
|
|
3236
|
+
currentTakeover?.terminal_bridge_message_id !== nativeTakeover?.terminal_bridge_message_id ||
|
|
3237
|
+
currentControl?.target !== terminalControl.target ||
|
|
3238
|
+
currentControl?.socketPath !== terminalControl.socketPath ||
|
|
3239
|
+
(hooklessClaudeScreenApproval &&
|
|
3240
|
+
currentApproval?.fingerprint !== monitoredApproval?.fingerprint)) {
|
|
3241
|
+
throw new Error("approval state changed while waiting for terminal control; refresh status and retry");
|
|
3242
|
+
}
|
|
3243
|
+
lockedConversation = currentConversation;
|
|
3244
|
+
approval = await createTerminalAgentBridge(options).approve(executor.kind, terminalControl, {
|
|
3245
|
+
expectedFingerprint,
|
|
3246
|
+
scrollbackLines: Number(options.scrollbackLines ?? 120),
|
|
3247
|
+
runtime: runtimeIdentity,
|
|
3248
|
+
requiredDecisionMode: autoApproved && executor.kind === "claude"
|
|
3249
|
+
? "structured"
|
|
3250
|
+
: undefined,
|
|
3251
|
+
authorize: autoApproved
|
|
3252
|
+
? ({ agent, terminalControl: currentTerminalControl, inspection, fingerprint }) => {
|
|
3253
|
+
if (!autoApprovalPolicy) {
|
|
3254
|
+
return {
|
|
3255
|
+
approved: false,
|
|
3256
|
+
reason: "automatic approval requires an executor-side policy"
|
|
3257
|
+
};
|
|
3258
|
+
}
|
|
3259
|
+
const candidate = {
|
|
3260
|
+
agent,
|
|
3261
|
+
kind: inspection.approval.promptKind ?? "unknown",
|
|
3262
|
+
decisionMode: inspection.approval.approvable
|
|
3263
|
+
? inspection.approval.action.mode ?? "keys"
|
|
3264
|
+
: undefined,
|
|
3265
|
+
command: inspection.approval.command,
|
|
3266
|
+
cwd: inspection.approval.cwd ?? currentTerminalControl.currentPath,
|
|
3267
|
+
fingerprint: fingerprint ?? "",
|
|
3268
|
+
terminalTarget: currentTerminalControl.target
|
|
3052
3269
|
};
|
|
3270
|
+
executorPolicyDecision = evaluateApprovalPolicy({
|
|
3271
|
+
policy: autoApprovalPolicy,
|
|
3272
|
+
candidate
|
|
3273
|
+
});
|
|
3274
|
+
if (executorPolicyDecision.action !== "approve") {
|
|
3275
|
+
return {
|
|
3276
|
+
approved: false,
|
|
3277
|
+
reason: `executor-side auto-approval policy rejected the current request: ${executorPolicyDecision.reason}`
|
|
3278
|
+
};
|
|
3279
|
+
}
|
|
3280
|
+
if (policyRuleId && executorPolicyDecision.ruleId !== policyRuleId) {
|
|
3281
|
+
return {
|
|
3282
|
+
approved: false,
|
|
3283
|
+
reason: "executor-side auto-approval rule changed before execution"
|
|
3284
|
+
};
|
|
3285
|
+
}
|
|
3286
|
+
if (policyFingerprint &&
|
|
3287
|
+
executorPolicyDecision.policyFingerprint !== policyFingerprint) {
|
|
3288
|
+
return {
|
|
3289
|
+
approved: false,
|
|
3290
|
+
reason: "executor-side auto-approval policy changed before execution"
|
|
3291
|
+
};
|
|
3292
|
+
}
|
|
3293
|
+
return { approved: true };
|
|
3053
3294
|
}
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3295
|
+
: undefined,
|
|
3296
|
+
beforeKeyDispatch: hooklessClaudeScreenApproval
|
|
3297
|
+
? ({ fingerprint, terminalControl: dispatchControl, keys }) => {
|
|
3298
|
+
if (releaseStateLock) {
|
|
3299
|
+
throw new Error("Claude approval dispatch was already reserved");
|
|
3300
|
+
}
|
|
3301
|
+
releaseStateLock = acquireFileLock(`${statePath}.lock`);
|
|
3302
|
+
const latestConversation = loadState(statePath);
|
|
3303
|
+
const latestTakeover = isRecord(latestConversation.native_session_takeover)
|
|
3304
|
+
? latestConversation.native_session_takeover
|
|
3305
|
+
: undefined;
|
|
3306
|
+
const latestControl = terminalControlFromTakeover(latestTakeover);
|
|
3307
|
+
const latestApproval = isRecord(latestTakeover?.terminal_bridge_approval)
|
|
3308
|
+
? latestTakeover.terminal_bridge_approval
|
|
3309
|
+
: undefined;
|
|
3310
|
+
const latestNotifiedAt = validTimestampMs(latestApproval?.notified_at);
|
|
3311
|
+
const latestDispatch = isRecord(latestTakeover?.terminal_bridge_approval_dispatch)
|
|
3312
|
+
? latestTakeover.terminal_bridge_approval_dispatch
|
|
3313
|
+
: undefined;
|
|
3314
|
+
if (!latestTakeover ||
|
|
3315
|
+
latestConversation.status !== "waiting_for_openclaw" ||
|
|
3316
|
+
latestTakeover.terminal_bridge_message_id !==
|
|
3317
|
+
nativeTakeover?.terminal_bridge_message_id ||
|
|
3318
|
+
latestApproval?.fingerprint !== fingerprint ||
|
|
3319
|
+
latestNotifiedAt === undefined ||
|
|
3320
|
+
Date.now() - latestNotifiedAt > CLAUDE_SCREEN_APPROVAL_TTL_MS ||
|
|
3321
|
+
expectedFingerprint !== fingerprint ||
|
|
3322
|
+
latestControl?.target !== dispatchControl.target ||
|
|
3323
|
+
latestControl?.socketPath !== dispatchControl.socketPath) {
|
|
3324
|
+
throw new Error("approval state changed before terminal dispatch; refresh status and retry");
|
|
3325
|
+
}
|
|
3326
|
+
if (latestDispatch?.state === "reserved" &&
|
|
3327
|
+
latestDispatch.terminal_bridge_message_id ===
|
|
3328
|
+
latestTakeover.terminal_bridge_message_id) {
|
|
3329
|
+
throw new Error("a previous Claude approval dispatch has an uncertain outcome; inspect and resolve the terminal manually");
|
|
3330
|
+
}
|
|
3331
|
+
const reservedAt = new Date().toISOString();
|
|
3332
|
+
const reservedConversation = {
|
|
3333
|
+
...latestConversation,
|
|
3334
|
+
native_session_takeover: {
|
|
3335
|
+
...latestTakeover,
|
|
3336
|
+
terminal_bridge_approval_dispatch: {
|
|
3337
|
+
state: "reserved",
|
|
3338
|
+
attempt_id: randomUUID(),
|
|
3339
|
+
fingerprint,
|
|
3340
|
+
keys,
|
|
3341
|
+
terminal_target: dispatchControl.target,
|
|
3342
|
+
terminal_bridge_message_id: latestTakeover.terminal_bridge_message_id,
|
|
3343
|
+
reserved_at: reservedAt
|
|
3344
|
+
}
|
|
3345
|
+
},
|
|
3346
|
+
updated_at: reservedAt
|
|
3059
3347
|
};
|
|
3348
|
+
saveState(statePath, reservedConversation);
|
|
3349
|
+
lockedConversation = reservedConversation;
|
|
3060
3350
|
}
|
|
3061
|
-
|
|
3351
|
+
: undefined
|
|
3352
|
+
});
|
|
3353
|
+
const actualFingerprint = approval.fingerprint;
|
|
3354
|
+
const effectivePolicyRuleId = executorPolicyDecision?.ruleId ?? policyRuleId;
|
|
3355
|
+
const effectivePolicyFingerprint = executorPolicyDecision?.policyFingerprint ?? policyFingerprint;
|
|
3356
|
+
if (!approval.approved) {
|
|
3357
|
+
releaseApprovalStateLock();
|
|
3358
|
+
releaseApprovalTerminalLock();
|
|
3359
|
+
if (autoApproved) {
|
|
3360
|
+
appendEvent(logPath, {
|
|
3361
|
+
ts: new Date().toISOString(),
|
|
3362
|
+
conversation_id: conversation.conversation_id,
|
|
3363
|
+
event: "terminal_auto_approval_decision",
|
|
3364
|
+
action: "rejected",
|
|
3365
|
+
reason: approval.reason,
|
|
3366
|
+
terminal_control: terminalControl,
|
|
3367
|
+
expected_fingerprint: expectedFingerprint,
|
|
3368
|
+
actual_fingerprint: actualFingerprint,
|
|
3369
|
+
policy_rule_id: effectivePolicyRuleId,
|
|
3370
|
+
policy_fingerprint: effectivePolicyFingerprint
|
|
3371
|
+
});
|
|
3062
3372
|
}
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3373
|
+
printJson({
|
|
3374
|
+
conversation,
|
|
3375
|
+
approved: false,
|
|
3376
|
+
blocked: approval.blocked,
|
|
3377
|
+
reason: approval.reason,
|
|
3378
|
+
terminal_control: terminalControl,
|
|
3379
|
+
expected_approval_fingerprint: expectedFingerprint,
|
|
3380
|
+
actual_approval_fingerprint: actualFingerprint,
|
|
3381
|
+
screen_excerpt: approval.screenExcerpt
|
|
3382
|
+
});
|
|
3383
|
+
return;
|
|
3384
|
+
}
|
|
3385
|
+
appendEvent(logPath, {
|
|
3386
|
+
ts: new Date().toISOString(),
|
|
3387
|
+
conversation_id: conversation.conversation_id,
|
|
3388
|
+
event: "terminal_approval_send",
|
|
3389
|
+
terminal_control: terminalControl,
|
|
3390
|
+
key: approval.key,
|
|
3391
|
+
keys: approval.keys,
|
|
3392
|
+
label: approval.label,
|
|
3393
|
+
decision_mode: approval.decisionMode,
|
|
3394
|
+
request_id: approval.requestId,
|
|
3395
|
+
approval_fingerprint: actualFingerprint,
|
|
3396
|
+
auto_approved: autoApproved,
|
|
3397
|
+
policy_rule_id: effectivePolicyRuleId,
|
|
3398
|
+
policy_fingerprint: effectivePolicyFingerprint
|
|
3399
|
+
});
|
|
3069
3400
|
if (autoApproved) {
|
|
3070
3401
|
appendEvent(logPath, {
|
|
3071
3402
|
ts: new Date().toISOString(),
|
|
3072
3403
|
conversation_id: conversation.conversation_id,
|
|
3073
3404
|
event: "terminal_auto_approval_decision",
|
|
3074
|
-
action: "
|
|
3075
|
-
reason: approval.reason,
|
|
3405
|
+
action: "approved",
|
|
3076
3406
|
terminal_control: terminalControl,
|
|
3077
|
-
|
|
3078
|
-
actual_fingerprint: actualFingerprint,
|
|
3407
|
+
approval_fingerprint: actualFingerprint,
|
|
3079
3408
|
policy_rule_id: effectivePolicyRuleId,
|
|
3080
3409
|
policy_fingerprint: effectivePolicyFingerprint
|
|
3081
3410
|
});
|
|
3082
3411
|
}
|
|
3083
|
-
|
|
3084
|
-
conversation,
|
|
3085
|
-
approved: false,
|
|
3086
|
-
blocked: approval.blocked,
|
|
3087
|
-
reason: approval.reason,
|
|
3088
|
-
terminal_control: terminalControl,
|
|
3089
|
-
expected_approval_fingerprint: expectedFingerprint,
|
|
3090
|
-
actual_approval_fingerprint: actualFingerprint,
|
|
3091
|
-
screen_excerpt: approval.screenExcerpt
|
|
3092
|
-
});
|
|
3093
|
-
return;
|
|
3094
|
-
}
|
|
3095
|
-
appendEvent(logPath, {
|
|
3096
|
-
ts: new Date().toISOString(),
|
|
3097
|
-
conversation_id: conversation.conversation_id,
|
|
3098
|
-
event: "terminal_approval_send",
|
|
3099
|
-
terminal_control: terminalControl,
|
|
3100
|
-
key: approval.key,
|
|
3101
|
-
keys: approval.keys,
|
|
3102
|
-
label: approval.label,
|
|
3103
|
-
decision_mode: approval.decisionMode,
|
|
3104
|
-
request_id: approval.requestId,
|
|
3105
|
-
approval_fingerprint: actualFingerprint,
|
|
3106
|
-
auto_approved: autoApproved,
|
|
3107
|
-
policy_rule_id: effectivePolicyRuleId,
|
|
3108
|
-
policy_fingerprint: effectivePolicyFingerprint
|
|
3109
|
-
});
|
|
3110
|
-
if (autoApproved) {
|
|
3111
|
-
appendEvent(logPath, {
|
|
3112
|
-
ts: new Date().toISOString(),
|
|
3412
|
+
runtimeLog("info", "terminal_approval_send", {
|
|
3113
3413
|
conversation_id: conversation.conversation_id,
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3414
|
+
terminal_target: terminalControl.target,
|
|
3415
|
+
key: approval.key,
|
|
3416
|
+
keys: approval.keys,
|
|
3417
|
+
label: approval.label,
|
|
3418
|
+
decision_mode: approval.decisionMode,
|
|
3419
|
+
request_id: approval.requestId,
|
|
3117
3420
|
approval_fingerprint: actualFingerprint,
|
|
3421
|
+
auto_approved: autoApproved,
|
|
3118
3422
|
policy_rule_id: effectivePolicyRuleId,
|
|
3119
3423
|
policy_fingerprint: effectivePolicyFingerprint
|
|
3120
3424
|
});
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
|
|
3140
|
-
|
|
3141
|
-
|
|
3142
|
-
|
|
3143
|
-
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
|
|
3147
|
-
terminal_bridge_approval
|
|
3148
|
-
|
|
3149
|
-
|
|
3150
|
-
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
saveState(statePath, nextConversation);
|
|
3182
|
-
const bridgeMonitor = startTerminalBridgeMonitorForConversation({
|
|
3183
|
-
conversation: nextConversation,
|
|
3184
|
-
statePath,
|
|
3185
|
-
logPath,
|
|
3186
|
-
options
|
|
3187
|
-
});
|
|
3188
|
-
if (bridgeMonitor) {
|
|
3189
|
-
appendEvent(logPath, {
|
|
3190
|
-
ts: new Date().toISOString(),
|
|
3191
|
-
conversation_id: conversation.conversation_id,
|
|
3192
|
-
event: "terminal_bridge_monitor_launch",
|
|
3193
|
-
pid: bridgeMonitor.pid ?? null,
|
|
3194
|
-
terminal_control: terminalControl,
|
|
3195
|
-
reason: "approval_resolved",
|
|
3196
|
-
agent_timeout_minutes: agentTimeoutMinutes,
|
|
3197
|
-
agent_hard_timeout_minutes: agentHardTimeoutMinutes
|
|
3425
|
+
const nativeTakeoverForUpdate = isRecord(lockedConversation.native_session_takeover)
|
|
3426
|
+
? { ...lockedConversation.native_session_takeover }
|
|
3427
|
+
: {};
|
|
3428
|
+
const approvalResolvedAt = new Date().toISOString();
|
|
3429
|
+
const agentTimeoutMinutes = Number(options.agentTimeoutMinutes ??
|
|
3430
|
+
nativeTakeoverForUpdate.terminal_bridge_inactivity_timeout_minutes ??
|
|
3431
|
+
DEFAULT_AGENT_TIMEOUT_MINUTES);
|
|
3432
|
+
const agentHardTimeoutMinutes = positiveMinutes(options.agentHardTimeoutMinutes ??
|
|
3433
|
+
nativeTakeoverForUpdate.terminal_bridge_hard_timeout_minutes ??
|
|
3434
|
+
DEFAULT_AGENT_HARD_TIMEOUT_MINUTES, "--agent-hard-timeout-minutes");
|
|
3435
|
+
const nextNativeTakeover = {
|
|
3436
|
+
...nativeTakeoverForUpdate,
|
|
3437
|
+
terminal_bridge_approval: undefined,
|
|
3438
|
+
terminal_bridge_approval_dispatch: undefined,
|
|
3439
|
+
terminal_bridge_approval_resolved_at: approvalResolvedAt,
|
|
3440
|
+
terminal_bridge_last_approval_fingerprint: actualFingerprint,
|
|
3441
|
+
terminal_bridge_last_approval_at: approvalResolvedAt,
|
|
3442
|
+
terminal_bridge_monitor_lock_version: TERMINAL_BRIDGE_MONITOR_LOCK_VERSION,
|
|
3443
|
+
terminal_bridge_monitor_started_at: approvalResolvedAt,
|
|
3444
|
+
terminal_bridge_last_activity_at: approvalResolvedAt,
|
|
3445
|
+
terminal_bridge_last_activity_reason: "approval resolved",
|
|
3446
|
+
terminal_bridge_inactivity_timeout_minutes: agentTimeoutMinutes,
|
|
3447
|
+
terminal_bridge_hard_timeout_minutes: agentHardTimeoutMinutes,
|
|
3448
|
+
terminal_bridge_inactivity_deadline_at: deadlineAt(approvalResolvedAt, agentTimeoutMinutes),
|
|
3449
|
+
terminal_bridge_hard_deadline_at: deadlineAt(stringValue(nativeTakeoverForUpdate.terminal_bridge_started_at) ?? approvalResolvedAt, agentHardTimeoutMinutes)
|
|
3450
|
+
};
|
|
3451
|
+
delete nextNativeTakeover.terminal_bridge_approval;
|
|
3452
|
+
delete nextNativeTakeover.terminal_bridge_approval_dispatch;
|
|
3453
|
+
let nextConversation = {
|
|
3454
|
+
...lockedConversation,
|
|
3455
|
+
status: terminalBridgeEnabled(lockedConversation)
|
|
3456
|
+
? "waiting_for_agent"
|
|
3457
|
+
: lockedConversation.status,
|
|
3458
|
+
native_session_takeover: nextNativeTakeover,
|
|
3459
|
+
updated_at: approvalResolvedAt
|
|
3460
|
+
};
|
|
3461
|
+
const approvalLeaseDeadlines = [
|
|
3462
|
+
stringValue(nextNativeTakeover.terminal_bridge_inactivity_deadline_at),
|
|
3463
|
+
stringValue(nextNativeTakeover.terminal_bridge_hard_deadline_at)
|
|
3464
|
+
].filter((value) => Boolean(value));
|
|
3465
|
+
if (approvalLeaseDeadlines.length > 0) {
|
|
3466
|
+
const renewedLease = renewClaudeHookLease(nextConversation, new Date(Math.min(...approvalLeaseDeadlines.map((value) => Date.parse(value)))).toISOString());
|
|
3467
|
+
if (renewedLease) {
|
|
3468
|
+
nextConversation = {
|
|
3469
|
+
...nextConversation,
|
|
3470
|
+
native_session_takeover: {
|
|
3471
|
+
...nextNativeTakeover,
|
|
3472
|
+
claude_hook_lease_id: renewedLease.id
|
|
3473
|
+
}
|
|
3474
|
+
};
|
|
3475
|
+
}
|
|
3476
|
+
}
|
|
3477
|
+
saveState(statePath, nextConversation);
|
|
3478
|
+
releaseApprovalStateLock();
|
|
3479
|
+
releaseApprovalTerminalLock();
|
|
3480
|
+
const bridgeMonitor = startTerminalBridgeMonitorForConversation({
|
|
3481
|
+
conversation: nextConversation,
|
|
3482
|
+
statePath,
|
|
3483
|
+
logPath,
|
|
3484
|
+
options
|
|
3198
3485
|
});
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
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
|
+
printJson({
|
|
3505
|
+
conversation: nextConversation,
|
|
3506
|
+
approved: true,
|
|
3507
|
+
terminal_control: terminalControl,
|
|
3508
|
+
key: approval.key,
|
|
3509
|
+
keys: approval.keys,
|
|
3510
|
+
label: approval.label,
|
|
3511
|
+
decision_mode: approval.decisionMode,
|
|
3512
|
+
request_id: approval.requestId,
|
|
3513
|
+
approval_fingerprint: actualFingerprint,
|
|
3514
|
+
auto_approved: autoApproved,
|
|
3515
|
+
policy_rule_id: effectivePolicyRuleId,
|
|
3516
|
+
monitor_pid: bridgeMonitor?.pid ?? null
|
|
3204
3517
|
});
|
|
3205
3518
|
}
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
request_id: approval.requestId,
|
|
3215
|
-
approval_fingerprint: actualFingerprint,
|
|
3216
|
-
auto_approved: autoApproved,
|
|
3217
|
-
policy_rule_id: effectivePolicyRuleId,
|
|
3218
|
-
monitor_pid: bridgeMonitor?.pid ?? null
|
|
3219
|
-
});
|
|
3519
|
+
finally {
|
|
3520
|
+
try {
|
|
3521
|
+
releaseApprovalStateLock();
|
|
3522
|
+
}
|
|
3523
|
+
finally {
|
|
3524
|
+
releaseApprovalTerminalLock();
|
|
3525
|
+
}
|
|
3526
|
+
}
|
|
3220
3527
|
}
|
|
3221
3528
|
async function runTerminalConversationApprove({ options, conversationId, agent, terminalControl, pid }) {
|
|
3222
|
-
const
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
|
|
3529
|
+
const releaseTerminalLock = acquireFileLock(terminalBridgeSendLockPath(storeDirFromOptions(options), terminalControl), { timeoutMs: 30000 });
|
|
3530
|
+
try {
|
|
3531
|
+
if (agent === "claude") {
|
|
3532
|
+
printJson({
|
|
3533
|
+
conversation_id: conversationId,
|
|
3534
|
+
source: "terminal_control",
|
|
3535
|
+
approved: false,
|
|
3536
|
+
blocked: true,
|
|
3537
|
+
reason: "Claude screen approval requires `send --background` so AKK can bind it to an active managed turn",
|
|
3538
|
+
terminal_control: terminalControl
|
|
3539
|
+
});
|
|
3540
|
+
return;
|
|
3229
3541
|
}
|
|
3230
|
-
|
|
3231
|
-
|
|
3542
|
+
const approval = await createTerminalAgentBridge(options).approve(agent, terminalControl, {
|
|
3543
|
+
expectedFingerprint: stringValue(options.expectedApprovalFingerprint),
|
|
3544
|
+
scrollbackLines: Number(options.scrollbackLines ?? 120),
|
|
3545
|
+
runtime: {
|
|
3546
|
+
pid,
|
|
3547
|
+
cwd: terminalControl.currentPath,
|
|
3548
|
+
conversationId,
|
|
3549
|
+
terminalTarget: terminalControl.target
|
|
3550
|
+
}
|
|
3551
|
+
});
|
|
3552
|
+
if (!approval.approved) {
|
|
3553
|
+
printJson({
|
|
3554
|
+
conversation_id: conversationId,
|
|
3555
|
+
source: "terminal_control",
|
|
3556
|
+
approved: false,
|
|
3557
|
+
blocked: approval.blocked,
|
|
3558
|
+
reason: approval.reason,
|
|
3559
|
+
terminal_control: terminalControl,
|
|
3560
|
+
screen_excerpt: approval.screenExcerpt
|
|
3561
|
+
});
|
|
3562
|
+
return;
|
|
3563
|
+
}
|
|
3564
|
+
runtimeLog("info", "terminal_approval_send", {
|
|
3565
|
+
conversation_id: conversationId,
|
|
3566
|
+
agent,
|
|
3567
|
+
terminal_target: terminalControl.target,
|
|
3568
|
+
key: approval.key,
|
|
3569
|
+
keys: approval.keys,
|
|
3570
|
+
label: approval.label,
|
|
3571
|
+
decision_mode: approval.decisionMode,
|
|
3572
|
+
request_id: approval.requestId
|
|
3573
|
+
});
|
|
3232
3574
|
printJson({
|
|
3233
3575
|
conversation_id: conversationId,
|
|
3234
3576
|
source: "terminal_control",
|
|
3235
|
-
approved:
|
|
3236
|
-
blocked: approval.blocked,
|
|
3237
|
-
reason: approval.reason,
|
|
3577
|
+
approved: true,
|
|
3238
3578
|
terminal_control: terminalControl,
|
|
3239
|
-
|
|
3579
|
+
key: approval.key,
|
|
3580
|
+
keys: approval.keys,
|
|
3581
|
+
label: approval.label,
|
|
3582
|
+
approval_fingerprint: approval.fingerprint,
|
|
3583
|
+
decision_mode: approval.decisionMode,
|
|
3584
|
+
request_id: approval.requestId
|
|
3240
3585
|
});
|
|
3241
|
-
return;
|
|
3242
3586
|
}
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
terminal_target: terminalControl.target,
|
|
3247
|
-
key: approval.key,
|
|
3248
|
-
keys: approval.keys,
|
|
3249
|
-
label: approval.label,
|
|
3250
|
-
decision_mode: approval.decisionMode,
|
|
3251
|
-
request_id: approval.requestId
|
|
3252
|
-
});
|
|
3253
|
-
printJson({
|
|
3254
|
-
conversation_id: conversationId,
|
|
3255
|
-
source: "terminal_control",
|
|
3256
|
-
approved: true,
|
|
3257
|
-
terminal_control: terminalControl,
|
|
3258
|
-
key: approval.key,
|
|
3259
|
-
keys: approval.keys,
|
|
3260
|
-
label: approval.label,
|
|
3261
|
-
approval_fingerprint: approval.fingerprint,
|
|
3262
|
-
decision_mode: approval.decisionMode,
|
|
3263
|
-
request_id: approval.requestId
|
|
3264
|
-
});
|
|
3587
|
+
finally {
|
|
3588
|
+
releaseTerminalLock();
|
|
3589
|
+
}
|
|
3265
3590
|
}
|
|
3266
3591
|
async function runTerminalConversationSend({ options, conversationId, agent, pid, messageBody, terminalControl }) {
|
|
3267
|
-
const
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
const
|
|
3271
|
-
|
|
3592
|
+
const releaseTerminalLock = acquireFileLock(terminalBridgeSendLockPath(storeDirFromOptions(options), terminalControl), { timeoutMs: 30000 });
|
|
3593
|
+
try {
|
|
3594
|
+
const terminalPayload = terminalSubmissionPayload(String(messageBody));
|
|
3595
|
+
const terminalBridge = createTerminalAgentBridge(options);
|
|
3596
|
+
if (agent === "claude") {
|
|
3597
|
+
const status = await terminalBridge.status(agent, terminalControl, {
|
|
3598
|
+
scrollbackLines: Number(options.scrollbackLines ?? 120),
|
|
3599
|
+
runtime: {
|
|
3600
|
+
pid,
|
|
3601
|
+
cwd: terminalControl.currentPath,
|
|
3602
|
+
terminalTarget: terminalControl.target
|
|
3603
|
+
}
|
|
3604
|
+
});
|
|
3605
|
+
assertSafeClaudeTerminalSend(status);
|
|
3606
|
+
}
|
|
3607
|
+
await terminalBridge.send(agent, terminalControl, terminalPayload, {
|
|
3272
3608
|
runtime: {
|
|
3273
3609
|
pid,
|
|
3274
3610
|
cwd: terminalControl.currentPath,
|
|
3275
3611
|
terminalTarget: terminalControl.target
|
|
3276
3612
|
}
|
|
3277
3613
|
});
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
|
|
3286
|
-
});
|
|
3287
|
-
runtimeLog("info", "terminal_message_send", {
|
|
3288
|
-
conversation_id: conversationId,
|
|
3289
|
-
agent,
|
|
3290
|
-
terminal_target: terminalControl.target,
|
|
3291
|
-
message: textSummary(messageBody)
|
|
3292
|
-
});
|
|
3293
|
-
printJson({
|
|
3294
|
-
conversation_id: conversationId,
|
|
3295
|
-
source: "terminal_control",
|
|
3296
|
-
delivered: true,
|
|
3297
|
-
status: "async_pending",
|
|
3298
|
-
background: true,
|
|
3299
|
-
callback_expected: false,
|
|
3300
|
-
terminal_control: terminalControl,
|
|
3301
|
-
message: {
|
|
3302
|
-
body: messageBody
|
|
3303
|
-
},
|
|
3304
|
-
openclaw_next_action: openClawYieldNextAction({
|
|
3305
|
-
conversationId,
|
|
3614
|
+
runtimeLog("info", "terminal_message_send", {
|
|
3615
|
+
conversation_id: conversationId,
|
|
3616
|
+
agent,
|
|
3617
|
+
terminal_target: terminalControl.target,
|
|
3618
|
+
message: textSummary(messageBody)
|
|
3619
|
+
});
|
|
3620
|
+
printJson({
|
|
3621
|
+
conversation_id: conversationId,
|
|
3306
3622
|
source: "terminal_control",
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
3623
|
+
delivered: true,
|
|
3624
|
+
status: "async_pending",
|
|
3625
|
+
background: true,
|
|
3626
|
+
callback_expected: false,
|
|
3627
|
+
terminal_control: terminalControl,
|
|
3628
|
+
message: {
|
|
3629
|
+
body: messageBody
|
|
3630
|
+
},
|
|
3631
|
+
openclaw_next_action: openClawYieldNextAction({
|
|
3632
|
+
conversationId,
|
|
3633
|
+
source: "terminal_control",
|
|
3634
|
+
callbackExpected: false
|
|
3635
|
+
})
|
|
3636
|
+
});
|
|
3637
|
+
}
|
|
3638
|
+
finally {
|
|
3639
|
+
releaseTerminalLock();
|
|
3640
|
+
}
|
|
3310
3641
|
}
|
|
3311
|
-
async function runTerminalControlSend({ options, conversation, nextConversation, statePath, logPath, executor, message, terminalControl, needsNativeTakeoverBootstrap, terminalSendLockHeld = false }) {
|
|
3642
|
+
async function runTerminalControlSend({ options, conversation, nextConversation, statePath, logPath, executor, message, terminalControl, needsNativeTakeoverBootstrap, terminalSendLockHeld = false, terminalStateLockHeld = false, recordMessageAfterSend = false, recordRawAttachmentAfterSend = false }) {
|
|
3312
3643
|
const bridge = terminalBridgeEnabled(conversation);
|
|
3313
|
-
if (
|
|
3644
|
+
if (!terminalSendLockHeld) {
|
|
3314
3645
|
const releaseTerminalLock = acquireFileLock(terminalBridgeSendLockPath(storeDirFromOptions(options), terminalControl), { timeoutMs: 30000 });
|
|
3315
3646
|
try {
|
|
3316
3647
|
return await runTerminalControlSend({
|
|
@@ -3323,13 +3654,52 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
3323
3654
|
message,
|
|
3324
3655
|
terminalControl,
|
|
3325
3656
|
needsNativeTakeoverBootstrap,
|
|
3326
|
-
terminalSendLockHeld: true
|
|
3657
|
+
terminalSendLockHeld: true,
|
|
3658
|
+
terminalStateLockHeld,
|
|
3659
|
+
recordMessageAfterSend,
|
|
3660
|
+
recordRawAttachmentAfterSend
|
|
3327
3661
|
});
|
|
3328
3662
|
}
|
|
3329
3663
|
finally {
|
|
3330
3664
|
releaseTerminalLock();
|
|
3331
3665
|
}
|
|
3332
3666
|
}
|
|
3667
|
+
if (bridge && !terminalStateLockHeld) {
|
|
3668
|
+
const releaseStateLock = acquireFileLock(`${statePath}.lock`);
|
|
3669
|
+
try {
|
|
3670
|
+
const currentConversation = loadState(statePath);
|
|
3671
|
+
const currentTakeover = isRecord(currentConversation.native_session_takeover)
|
|
3672
|
+
? currentConversation.native_session_takeover
|
|
3673
|
+
: undefined;
|
|
3674
|
+
const currentControl = terminalControlFromTakeover(currentTakeover);
|
|
3675
|
+
if (currentConversation.conversation_id !== nextConversation.conversation_id ||
|
|
3676
|
+
currentConversation.updated_at !== nextConversation.updated_at ||
|
|
3677
|
+
currentConversation.status !== nextConversation.status ||
|
|
3678
|
+
currentConversation.response_rounds_used !== nextConversation.response_rounds_used ||
|
|
3679
|
+
currentControl?.target !== terminalControl.target ||
|
|
3680
|
+
currentControl?.socketPath !== terminalControl.socketPath) {
|
|
3681
|
+
throw new Error("conversation changed while waiting to send to the terminal; refresh status and retry");
|
|
3682
|
+
}
|
|
3683
|
+
return await runTerminalControlSend({
|
|
3684
|
+
options,
|
|
3685
|
+
conversation: currentConversation,
|
|
3686
|
+
nextConversation: currentConversation,
|
|
3687
|
+
statePath,
|
|
3688
|
+
logPath,
|
|
3689
|
+
executor,
|
|
3690
|
+
message,
|
|
3691
|
+
terminalControl,
|
|
3692
|
+
needsNativeTakeoverBootstrap,
|
|
3693
|
+
terminalSendLockHeld: true,
|
|
3694
|
+
terminalStateLockHeld: true,
|
|
3695
|
+
recordMessageAfterSend,
|
|
3696
|
+
recordRawAttachmentAfterSend
|
|
3697
|
+
});
|
|
3698
|
+
}
|
|
3699
|
+
finally {
|
|
3700
|
+
releaseStateLock();
|
|
3701
|
+
}
|
|
3702
|
+
}
|
|
3333
3703
|
const terminalBridge = createTerminalAgentBridge(options);
|
|
3334
3704
|
const bridgeStartedAt = new Date().toISOString();
|
|
3335
3705
|
const agentTimeoutMinutes = Number(options.agentTimeoutMinutes ?? DEFAULT_AGENT_TIMEOUT_MINUTES);
|
|
@@ -3349,6 +3719,10 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
3349
3719
|
messageId: message.id
|
|
3350
3720
|
};
|
|
3351
3721
|
let preSendScreenFingerprint;
|
|
3722
|
+
let claudeTranscriptAnchor;
|
|
3723
|
+
const claudeHome = executor.kind === "claude"
|
|
3724
|
+
? path.resolve(expandHome(options.claudeHome) ?? defaultClaudeHome())
|
|
3725
|
+
: undefined;
|
|
3352
3726
|
if (bridge) {
|
|
3353
3727
|
try {
|
|
3354
3728
|
const status = await terminalBridge.status(executor.kind, terminalControl, {
|
|
@@ -3358,7 +3732,21 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
3358
3732
|
if (executor.kind === "claude") {
|
|
3359
3733
|
assertSafeClaudeTerminalSend(status);
|
|
3360
3734
|
}
|
|
3361
|
-
preSendScreenFingerprint =
|
|
3735
|
+
preSendScreenFingerprint = stringValue(status.screen.digest) ??
|
|
3736
|
+
terminalBridgeScreenFingerprint(status.screen.excerpt);
|
|
3737
|
+
if (executor.kind === "claude") {
|
|
3738
|
+
claudeTranscriptAnchor = captureClaudeTranscriptAnchor({
|
|
3739
|
+
sessionId: preSendRuntime.sessionId,
|
|
3740
|
+
cwd: preSendRuntime.cwd,
|
|
3741
|
+
pid: preSendRuntime.pid,
|
|
3742
|
+
claudeHome,
|
|
3743
|
+
agentRows: loadClaudeAgentRows(options)
|
|
3744
|
+
});
|
|
3745
|
+
if (!claudeTranscriptAnchor &&
|
|
3746
|
+
!stringValue(options.claudeHookStoreDir)) {
|
|
3747
|
+
throw new Error("the hook-free completion monitor could not bind an owner-private Claude transcript boundary");
|
|
3748
|
+
}
|
|
3749
|
+
}
|
|
3362
3750
|
}
|
|
3363
3751
|
catch (error) {
|
|
3364
3752
|
if (executor.kind === "claude") {
|
|
@@ -3400,6 +3788,29 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
3400
3788
|
}
|
|
3401
3789
|
throw error;
|
|
3402
3790
|
}
|
|
3791
|
+
const bridgeConversation = bridge
|
|
3792
|
+
? withTerminalBridgeState({
|
|
3793
|
+
conversation: conversationWithHookLease,
|
|
3794
|
+
message,
|
|
3795
|
+
requestText: terminalPayload,
|
|
3796
|
+
startedAt: bridgeStartedAt,
|
|
3797
|
+
agentTimeoutMinutes,
|
|
3798
|
+
agentHardTimeoutMinutes,
|
|
3799
|
+
preSendScreenFingerprint,
|
|
3800
|
+
claudeTranscriptAnchor,
|
|
3801
|
+
claudeHome
|
|
3802
|
+
})
|
|
3803
|
+
: nextConversation;
|
|
3804
|
+
const deliveredConversation = markTakeoverBootstrapped({
|
|
3805
|
+
conversation: bridgeConversation,
|
|
3806
|
+
statePath,
|
|
3807
|
+
logPath,
|
|
3808
|
+
executor,
|
|
3809
|
+
native: needsNativeTakeoverBootstrap && !bridge,
|
|
3810
|
+
fork: false,
|
|
3811
|
+
stateLockHeld: terminalStateLockHeld
|
|
3812
|
+
});
|
|
3813
|
+
saveState(statePath, deliveredConversation);
|
|
3403
3814
|
const supersededConversationIds = bridge
|
|
3404
3815
|
? supersedeTerminalBridgeConversations({
|
|
3405
3816
|
storeDir: storeDirFromOptions(options),
|
|
@@ -3407,6 +3818,39 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
3407
3818
|
replacementConversationId: conversation.conversation_id
|
|
3408
3819
|
})
|
|
3409
3820
|
: [];
|
|
3821
|
+
if (recordRawAttachmentAfterSend) {
|
|
3822
|
+
const sourceConversationId = stringValue(isRecord(conversation.native_session_takeover)
|
|
3823
|
+
? conversation.native_session_takeover.native_session_id
|
|
3824
|
+
: undefined);
|
|
3825
|
+
appendEvent(logPath, {
|
|
3826
|
+
ts: new Date().toISOString(),
|
|
3827
|
+
conversation_id: conversation.conversation_id,
|
|
3828
|
+
event: "raw_terminal_session_attached",
|
|
3829
|
+
source_conversation_id: sourceConversationId,
|
|
3830
|
+
agent: executor.kind,
|
|
3831
|
+
terminal_control: terminalControl,
|
|
3832
|
+
executor
|
|
3833
|
+
});
|
|
3834
|
+
runtimeLog("info", "raw_terminal_session_attached", {
|
|
3835
|
+
conversation_id: conversation.conversation_id,
|
|
3836
|
+
source_conversation_id: sourceConversationId,
|
|
3837
|
+
terminal_target: terminalControl.target,
|
|
3838
|
+
state_path: statePath,
|
|
3839
|
+
event_log_path: logPath
|
|
3840
|
+
});
|
|
3841
|
+
}
|
|
3842
|
+
if (recordMessageAfterSend) {
|
|
3843
|
+
appendEvent(logPath, messageEvent(message));
|
|
3844
|
+
runtimeLog("info", "message_created", {
|
|
3845
|
+
conversation_id: conversation.conversation_id,
|
|
3846
|
+
agent: executor.kind,
|
|
3847
|
+
executor_session: executor.session,
|
|
3848
|
+
message_type: message.type,
|
|
3849
|
+
state_path: statePath,
|
|
3850
|
+
event_log_path: logPath,
|
|
3851
|
+
message: textSummary(message.body)
|
|
3852
|
+
});
|
|
3853
|
+
}
|
|
3410
3854
|
appendEvent(logPath, {
|
|
3411
3855
|
ts: new Date().toISOString(),
|
|
3412
3856
|
conversation_id: conversation.conversation_id,
|
|
@@ -3425,25 +3869,6 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
3425
3869
|
payload: textSummary(terminalPayload),
|
|
3426
3870
|
superseded_conversation_ids: supersededConversationIds
|
|
3427
3871
|
});
|
|
3428
|
-
const bridgeConversation = bridge
|
|
3429
|
-
? withTerminalBridgeState({
|
|
3430
|
-
conversation: conversationWithHookLease,
|
|
3431
|
-
message,
|
|
3432
|
-
startedAt: bridgeStartedAt,
|
|
3433
|
-
agentTimeoutMinutes,
|
|
3434
|
-
agentHardTimeoutMinutes,
|
|
3435
|
-
preSendScreenFingerprint
|
|
3436
|
-
})
|
|
3437
|
-
: nextConversation;
|
|
3438
|
-
const deliveredConversation = markTakeoverBootstrapped({
|
|
3439
|
-
conversation: bridgeConversation,
|
|
3440
|
-
statePath,
|
|
3441
|
-
logPath,
|
|
3442
|
-
executor,
|
|
3443
|
-
native: needsNativeTakeoverBootstrap && !bridge,
|
|
3444
|
-
fork: false
|
|
3445
|
-
});
|
|
3446
|
-
saveState(statePath, deliveredConversation);
|
|
3447
3872
|
const bridgeMonitor = bridge
|
|
3448
3873
|
? startTerminalBridgeMonitorForConversation({
|
|
3449
3874
|
conversation: deliveredConversation,
|
|
@@ -3487,7 +3912,7 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
3487
3912
|
});
|
|
3488
3913
|
}
|
|
3489
3914
|
function terminalSubmissionPayload(payload) {
|
|
3490
|
-
return payload.
|
|
3915
|
+
return payload.trimEnd();
|
|
3491
3916
|
}
|
|
3492
3917
|
function createManagedTerminalConversationFromRawId({ options, conversationId, agent, pid, messageBody, terminalControl }) {
|
|
3493
3918
|
const workspace = terminalControl.currentPath ?? process.cwd();
|
|
@@ -3551,23 +3976,6 @@ function createManagedTerminalConversationFromRawId({ options, conversationId, a
|
|
|
3551
3976
|
terminal_bridge: true
|
|
3552
3977
|
}
|
|
3553
3978
|
}, paths);
|
|
3554
|
-
saveState(paths.statePath, attachedConversation);
|
|
3555
|
-
appendEvent(paths.logPath, {
|
|
3556
|
-
ts: now.toISOString(),
|
|
3557
|
-
conversation_id: attachedConversation.conversation_id,
|
|
3558
|
-
event: "raw_terminal_session_attached",
|
|
3559
|
-
source_conversation_id: conversationId,
|
|
3560
|
-
agent,
|
|
3561
|
-
terminal_control: terminalControl,
|
|
3562
|
-
executor
|
|
3563
|
-
});
|
|
3564
|
-
runtimeLog("info", "raw_terminal_session_attached", {
|
|
3565
|
-
conversation_id: attachedConversation.conversation_id,
|
|
3566
|
-
source_conversation_id: conversationId,
|
|
3567
|
-
terminal_target: terminalControl.target,
|
|
3568
|
-
state_path: paths.statePath,
|
|
3569
|
-
event_log_path: paths.logPath
|
|
3570
|
-
});
|
|
3571
3979
|
const message = createMessage({
|
|
3572
3980
|
conversation: attachedConversation,
|
|
3573
3981
|
from: "openclaw",
|
|
@@ -3581,18 +3989,6 @@ function createManagedTerminalConversationFromRawId({ options, conversationId, a
|
|
|
3581
3989
|
}
|
|
3582
3990
|
});
|
|
3583
3991
|
const nextConversation = applyMessageToConversation(attachedConversation, message);
|
|
3584
|
-
saveState(paths.statePath, nextConversation);
|
|
3585
|
-
appendEvent(paths.logPath, messageEvent(message));
|
|
3586
|
-
runtimeLog("info", "message_created", {
|
|
3587
|
-
conversation_id: nextConversation.conversation_id,
|
|
3588
|
-
source_conversation_id: conversationId,
|
|
3589
|
-
agent: executor.kind,
|
|
3590
|
-
executor_session: executor.session,
|
|
3591
|
-
message_type: message.type,
|
|
3592
|
-
state_path: paths.statePath,
|
|
3593
|
-
event_log_path: paths.logPath,
|
|
3594
|
-
message: textSummary(messageBody)
|
|
3595
|
-
});
|
|
3596
3992
|
return {
|
|
3597
3993
|
conversation: attachedConversation,
|
|
3598
3994
|
nextConversation,
|
|
@@ -3904,22 +4300,36 @@ function buildAgentSendPayload({ conversation, executor, message, includeNativeT
|
|
|
3904
4300
|
function forkTakeoverSummaryText(forkTakeover) {
|
|
3905
4301
|
return String(isRecord(forkTakeover) ? forkTakeover.summary ?? "" : "").trim();
|
|
3906
4302
|
}
|
|
3907
|
-
function markTakeoverBootstrapped({ conversation, statePath, logPath, executor, native, fork }) {
|
|
4303
|
+
function markTakeoverBootstrapped({ conversation, statePath, logPath, executor, native, fork, stateLockHeld = false }) {
|
|
3908
4304
|
let nextConversation = conversation;
|
|
3909
4305
|
if (native) {
|
|
3910
|
-
nextConversation = markNativeSessionBootstrapped({
|
|
4306
|
+
nextConversation = markNativeSessionBootstrapped({
|
|
4307
|
+
conversation: nextConversation,
|
|
4308
|
+
statePath,
|
|
4309
|
+
logPath,
|
|
4310
|
+
executor,
|
|
4311
|
+
stateLockHeld
|
|
4312
|
+
});
|
|
3911
4313
|
}
|
|
3912
4314
|
if (fork) {
|
|
3913
|
-
nextConversation = markForkSessionBootstrapped({
|
|
4315
|
+
nextConversation = markForkSessionBootstrapped({
|
|
4316
|
+
conversation: nextConversation,
|
|
4317
|
+
statePath,
|
|
4318
|
+
logPath,
|
|
4319
|
+
executor,
|
|
4320
|
+
stateLockHeld
|
|
4321
|
+
});
|
|
3914
4322
|
}
|
|
3915
4323
|
return nextConversation;
|
|
3916
4324
|
}
|
|
3917
|
-
function markNativeSessionBootstrapped({ conversation, statePath, logPath, executor }) {
|
|
4325
|
+
function markNativeSessionBootstrapped({ conversation, statePath, logPath, executor, stateLockHeld = false }) {
|
|
3918
4326
|
const now = new Date().toISOString();
|
|
3919
|
-
const releaseLock =
|
|
4327
|
+
const releaseLock = stateLockHeld
|
|
4328
|
+
? undefined
|
|
4329
|
+
: acquireFileLock(`${statePath}.lock`);
|
|
3920
4330
|
let nextConversation = conversation;
|
|
3921
4331
|
try {
|
|
3922
|
-
const current = loadState(statePath);
|
|
4332
|
+
const current = stateLockHeld ? conversation : loadState(statePath);
|
|
3923
4333
|
const nativeTakeover = isRecord(current.native_session_takeover)
|
|
3924
4334
|
? current.native_session_takeover
|
|
3925
4335
|
: {};
|
|
@@ -3935,7 +4345,7 @@ function markNativeSessionBootstrapped({ conversation, statePath, logPath, execu
|
|
|
3935
4345
|
saveState(statePath, nextConversation);
|
|
3936
4346
|
}
|
|
3937
4347
|
finally {
|
|
3938
|
-
releaseLock();
|
|
4348
|
+
releaseLock?.();
|
|
3939
4349
|
}
|
|
3940
4350
|
appendEvent(logPath, {
|
|
3941
4351
|
ts: now,
|
|
@@ -3951,12 +4361,14 @@ function markNativeSessionBootstrapped({ conversation, statePath, logPath, execu
|
|
|
3951
4361
|
});
|
|
3952
4362
|
return nextConversation;
|
|
3953
4363
|
}
|
|
3954
|
-
function markForkSessionBootstrapped({ conversation, statePath, logPath, executor }) {
|
|
4364
|
+
function markForkSessionBootstrapped({ conversation, statePath, logPath, executor, stateLockHeld = false }) {
|
|
3955
4365
|
const now = new Date().toISOString();
|
|
3956
|
-
const releaseLock =
|
|
4366
|
+
const releaseLock = stateLockHeld
|
|
4367
|
+
? undefined
|
|
4368
|
+
: acquireFileLock(`${statePath}.lock`);
|
|
3957
4369
|
let nextConversation = conversation;
|
|
3958
4370
|
try {
|
|
3959
|
-
const current = loadState(statePath);
|
|
4371
|
+
const current = stateLockHeld ? conversation : loadState(statePath);
|
|
3960
4372
|
const forkTakeover = isRecord(current.fork_context_takeover)
|
|
3961
4373
|
? current.fork_context_takeover
|
|
3962
4374
|
: {};
|
|
@@ -3972,7 +4384,7 @@ function markForkSessionBootstrapped({ conversation, statePath, logPath, executo
|
|
|
3972
4384
|
saveState(statePath, nextConversation);
|
|
3973
4385
|
}
|
|
3974
4386
|
finally {
|
|
3975
|
-
releaseLock();
|
|
4387
|
+
releaseLock?.();
|
|
3976
4388
|
}
|
|
3977
4389
|
appendEvent(logPath, {
|
|
3978
4390
|
ts: now,
|
|
@@ -4093,75 +4505,108 @@ async function runRenew(options) {
|
|
|
4093
4505
|
if (!terminalExists) {
|
|
4094
4506
|
throw new Error(`cannot renew ${conversation.conversation_id}; terminal ${terminalControl.target} is no longer available`);
|
|
4095
4507
|
}
|
|
4096
|
-
const
|
|
4097
|
-
|
|
4098
|
-
|
|
4099
|
-
|
|
4100
|
-
|
|
4101
|
-
|
|
4102
|
-
const
|
|
4103
|
-
|
|
4104
|
-
|
|
4105
|
-
|
|
4508
|
+
const expectedMessageId = stringValue(nativeTakeover?.terminal_bridge_message_id);
|
|
4509
|
+
const expectedStartedAt = stringValue(nativeTakeover?.terminal_bridge_started_at);
|
|
4510
|
+
let renewed = conversation;
|
|
4511
|
+
let renewedTerminalControl = terminalControl;
|
|
4512
|
+
let inactivityTimeoutMinutes = 0;
|
|
4513
|
+
let hardTimeoutMinutes = 0;
|
|
4514
|
+
const releaseStateLock = acquireFileLock(`${statePath}.lock`);
|
|
4515
|
+
try {
|
|
4516
|
+
const current = loadState(statePath);
|
|
4517
|
+
if (current.status !== "stalled") {
|
|
4518
|
+
throw new Error(`cannot renew ${current.conversation_id}; conversation is ${current.status}, not stalled`);
|
|
4519
|
+
}
|
|
4520
|
+
const currentTakeover = isRecord(current.native_session_takeover)
|
|
4521
|
+
? current.native_session_takeover
|
|
4522
|
+
: undefined;
|
|
4523
|
+
const currentControl = terminalControlFromTakeover(currentTakeover);
|
|
4524
|
+
if (!currentControl || currentTakeover?.terminal_bridge !== true) {
|
|
4525
|
+
throw new Error(`cannot renew ${current.conversation_id}; conversation is not a terminal bridge task`);
|
|
4526
|
+
}
|
|
4527
|
+
if (current.conversation_id !== conversation.conversation_id ||
|
|
4528
|
+
currentControl.target !== terminalControl.target ||
|
|
4529
|
+
currentControl.socketPath !== terminalControl.socketPath ||
|
|
4530
|
+
currentControl.panePid !== terminalControl.panePid ||
|
|
4531
|
+
stringValue(currentTakeover.terminal_bridge_message_id) !== expectedMessageId ||
|
|
4532
|
+
stringValue(currentTakeover.terminal_bridge_started_at) !== expectedStartedAt) {
|
|
4533
|
+
throw new Error("conversation changed while waiting to renew; refresh status and retry");
|
|
4534
|
+
}
|
|
4535
|
+
renewedTerminalControl = currentControl;
|
|
4536
|
+
inactivityTimeoutMinutes = positiveMinutes(options.minutes ??
|
|
4537
|
+
options.agentTimeoutMinutes ??
|
|
4538
|
+
currentTakeover.terminal_bridge_inactivity_timeout_minutes ??
|
|
4539
|
+
DEFAULT_AGENT_TIMEOUT_MINUTES, "--minutes");
|
|
4540
|
+
hardTimeoutMinutes = positiveMinutes(currentTakeover.terminal_bridge_hard_timeout_minutes ??
|
|
4541
|
+
DEFAULT_AGENT_HARD_TIMEOUT_MINUTES, "--agent-hard-timeout-minutes");
|
|
4542
|
+
const startedAt = stringValue(currentTakeover.terminal_bridge_started_at);
|
|
4543
|
+
const startedAtMs = startedAt ? Date.parse(startedAt) : NaN;
|
|
4544
|
+
if (Number.isFinite(startedAtMs) &&
|
|
4545
|
+
Date.now() - startedAtMs >= hardTimeoutMinutes * 60 * 1000) {
|
|
4546
|
+
throw new Error(`cannot renew ${current.conversation_id}; terminal bridge hard lifetime of ${hardTimeoutMinutes} minutes has elapsed`);
|
|
4547
|
+
}
|
|
4548
|
+
const now = new Date().toISOString();
|
|
4549
|
+
const currentMessageId = stringValue(currentTakeover.terminal_bridge_message_id);
|
|
4550
|
+
const hardDeadline = deadlineAt(startedAt ?? now, hardTimeoutMinutes) ??
|
|
4551
|
+
new Date(Date.now() + hardTimeoutMinutes * 60 * 1000).toISOString();
|
|
4552
|
+
const inactivityDeadline = deadlineAt(now, inactivityTimeoutMinutes) ??
|
|
4553
|
+
new Date(Date.now() + inactivityTimeoutMinutes * 60 * 1000).toISOString();
|
|
4554
|
+
const renewedLease = executorForConversation(current).kind === "claude" && currentMessageId
|
|
4555
|
+
? activateClaudeHookLease({
|
|
4556
|
+
options,
|
|
4557
|
+
conversation: current,
|
|
4558
|
+
message: { id: currentMessageId },
|
|
4559
|
+
terminalControl: currentControl,
|
|
4560
|
+
expiresAt: new Date(Math.min(Date.parse(hardDeadline), Date.parse(inactivityDeadline))).toISOString()
|
|
4561
|
+
})
|
|
4562
|
+
: undefined;
|
|
4563
|
+
const renewedBase = renewedLease
|
|
4564
|
+
? withClaudeHookLeaseState(current, renewedLease)
|
|
4565
|
+
: current;
|
|
4566
|
+
const renewedNativeTakeover = isRecord(renewedBase.native_session_takeover)
|
|
4567
|
+
? renewedBase.native_session_takeover
|
|
4568
|
+
: currentTakeover;
|
|
4569
|
+
renewed = {
|
|
4570
|
+
...renewedBase,
|
|
4571
|
+
status: "waiting_for_agent",
|
|
4572
|
+
native_session_takeover: {
|
|
4573
|
+
...renewedNativeTakeover,
|
|
4574
|
+
terminal_bridge_monitor_lock_version: TERMINAL_BRIDGE_MONITOR_LOCK_VERSION,
|
|
4575
|
+
terminal_bridge_monitor_started_at: now,
|
|
4576
|
+
terminal_bridge_last_activity_at: now,
|
|
4577
|
+
terminal_bridge_inactivity_timeout_minutes: inactivityTimeoutMinutes,
|
|
4578
|
+
terminal_bridge_hard_timeout_minutes: hardTimeoutMinutes,
|
|
4579
|
+
terminal_bridge_inactivity_deadline_at: inactivityDeadline,
|
|
4580
|
+
terminal_bridge_hard_deadline_at: hardDeadline,
|
|
4581
|
+
terminal_bridge_renewed_at: now
|
|
4582
|
+
},
|
|
4583
|
+
updated_at: now
|
|
4584
|
+
};
|
|
4585
|
+
Reflect.deleteProperty(renewed, "stalled_at");
|
|
4586
|
+
Reflect.deleteProperty(renewed, "stalled_reason");
|
|
4587
|
+
Reflect.deleteProperty(renewed, "stalled_notification_sent_at");
|
|
4588
|
+
Reflect.deleteProperty(renewed, "stalled_notification_message_id");
|
|
4589
|
+
saveState(statePath, renewed);
|
|
4590
|
+
appendEvent(logPath, {
|
|
4591
|
+
ts: now,
|
|
4592
|
+
conversation_id: current.conversation_id,
|
|
4593
|
+
event: "terminal_bridge_renewed",
|
|
4594
|
+
previous_status: current.status,
|
|
4595
|
+
terminal_control: currentControl,
|
|
4596
|
+
agent_timeout_minutes: inactivityTimeoutMinutes,
|
|
4597
|
+
agent_hard_timeout_minutes: hardTimeoutMinutes,
|
|
4598
|
+
last_activity_at: now
|
|
4599
|
+
});
|
|
4600
|
+
runtimeLog("info", "terminal_bridge_renewed", {
|
|
4601
|
+
conversation_id: current.conversation_id,
|
|
4602
|
+
terminal_target: currentControl.target,
|
|
4603
|
+
agent_timeout_minutes: inactivityTimeoutMinutes,
|
|
4604
|
+
agent_hard_timeout_minutes: hardTimeoutMinutes
|
|
4605
|
+
});
|
|
4606
|
+
}
|
|
4607
|
+
finally {
|
|
4608
|
+
releaseStateLock();
|
|
4106
4609
|
}
|
|
4107
|
-
const now = new Date().toISOString();
|
|
4108
|
-
const currentMessageId = stringValue(nativeTakeover?.terminal_bridge_message_id);
|
|
4109
|
-
const hardDeadline = deadlineAt(startedAt ?? now, hardTimeoutMinutes) ??
|
|
4110
|
-
new Date(Date.now() + hardTimeoutMinutes * 60 * 1000).toISOString();
|
|
4111
|
-
const inactivityDeadline = deadlineAt(now, inactivityTimeoutMinutes) ??
|
|
4112
|
-
new Date(Date.now() + inactivityTimeoutMinutes * 60 * 1000).toISOString();
|
|
4113
|
-
const renewedLease = executorForConversation(conversation).kind === "claude" && currentMessageId
|
|
4114
|
-
? activateClaudeHookLease({
|
|
4115
|
-
options,
|
|
4116
|
-
conversation,
|
|
4117
|
-
message: { id: currentMessageId },
|
|
4118
|
-
terminalControl,
|
|
4119
|
-
expiresAt: new Date(Math.min(Date.parse(hardDeadline), Date.parse(inactivityDeadline))).toISOString()
|
|
4120
|
-
})
|
|
4121
|
-
: undefined;
|
|
4122
|
-
const renewedBase = renewedLease
|
|
4123
|
-
? withClaudeHookLeaseState(conversation, renewedLease)
|
|
4124
|
-
: conversation;
|
|
4125
|
-
const renewedNativeTakeover = isRecord(renewedBase.native_session_takeover)
|
|
4126
|
-
? renewedBase.native_session_takeover
|
|
4127
|
-
: nativeTakeover;
|
|
4128
|
-
const renewed = {
|
|
4129
|
-
...renewedBase,
|
|
4130
|
-
status: "waiting_for_agent",
|
|
4131
|
-
native_session_takeover: {
|
|
4132
|
-
...renewedNativeTakeover,
|
|
4133
|
-
terminal_bridge_monitor_lock_version: TERMINAL_BRIDGE_MONITOR_LOCK_VERSION,
|
|
4134
|
-
terminal_bridge_monitor_started_at: now,
|
|
4135
|
-
terminal_bridge_last_activity_at: now,
|
|
4136
|
-
terminal_bridge_inactivity_timeout_minutes: inactivityTimeoutMinutes,
|
|
4137
|
-
terminal_bridge_hard_timeout_minutes: hardTimeoutMinutes,
|
|
4138
|
-
terminal_bridge_inactivity_deadline_at: inactivityDeadline,
|
|
4139
|
-
terminal_bridge_hard_deadline_at: hardDeadline,
|
|
4140
|
-
terminal_bridge_renewed_at: now
|
|
4141
|
-
},
|
|
4142
|
-
updated_at: now
|
|
4143
|
-
};
|
|
4144
|
-
Reflect.deleteProperty(renewed, "stalled_at");
|
|
4145
|
-
Reflect.deleteProperty(renewed, "stalled_reason");
|
|
4146
|
-
Reflect.deleteProperty(renewed, "stalled_notification_sent_at");
|
|
4147
|
-
Reflect.deleteProperty(renewed, "stalled_notification_message_id");
|
|
4148
|
-
saveState(statePath, renewed);
|
|
4149
|
-
appendEvent(logPath, {
|
|
4150
|
-
ts: now,
|
|
4151
|
-
conversation_id: conversation.conversation_id,
|
|
4152
|
-
event: "terminal_bridge_renewed",
|
|
4153
|
-
previous_status: conversation.status,
|
|
4154
|
-
terminal_control: terminalControl,
|
|
4155
|
-
agent_timeout_minutes: inactivityTimeoutMinutes,
|
|
4156
|
-
agent_hard_timeout_minutes: hardTimeoutMinutes,
|
|
4157
|
-
last_activity_at: now
|
|
4158
|
-
});
|
|
4159
|
-
runtimeLog("info", "terminal_bridge_renewed", {
|
|
4160
|
-
conversation_id: conversation.conversation_id,
|
|
4161
|
-
terminal_target: terminalControl.target,
|
|
4162
|
-
agent_timeout_minutes: inactivityTimeoutMinutes,
|
|
4163
|
-
agent_hard_timeout_minutes: hardTimeoutMinutes
|
|
4164
|
-
});
|
|
4165
4610
|
const monitor = startTerminalBridgeMonitorForConversation({
|
|
4166
4611
|
conversation: renewed,
|
|
4167
4612
|
statePath,
|
|
@@ -4175,10 +4620,10 @@ async function runRenew(options) {
|
|
|
4175
4620
|
if (monitor) {
|
|
4176
4621
|
appendEvent(logPath, {
|
|
4177
4622
|
ts: new Date().toISOString(),
|
|
4178
|
-
conversation_id:
|
|
4623
|
+
conversation_id: renewed.conversation_id,
|
|
4179
4624
|
event: "terminal_bridge_monitor_launch",
|
|
4180
4625
|
pid: monitor.pid ?? null,
|
|
4181
|
-
terminal_control:
|
|
4626
|
+
terminal_control: renewedTerminalControl,
|
|
4182
4627
|
reason: "renewal",
|
|
4183
4628
|
agent_timeout_minutes: inactivityTimeoutMinutes,
|
|
4184
4629
|
agent_hard_timeout_minutes: hardTimeoutMinutes
|
|
@@ -4187,7 +4632,7 @@ async function runRenew(options) {
|
|
|
4187
4632
|
printJson({
|
|
4188
4633
|
conversation: renewed,
|
|
4189
4634
|
renewed: true,
|
|
4190
|
-
terminal_control:
|
|
4635
|
+
terminal_control: renewedTerminalControl,
|
|
4191
4636
|
agent_timeout_minutes: inactivityTimeoutMinutes,
|
|
4192
4637
|
agent_hard_timeout_minutes: hardTimeoutMinutes,
|
|
4193
4638
|
monitor_pid: monitor?.pid ?? null
|
|
@@ -4203,18 +4648,43 @@ async function runReconcileMonitors(options) {
|
|
|
4203
4648
|
let skipped = 0;
|
|
4204
4649
|
let errors = 0;
|
|
4205
4650
|
for (const listedConversation of conversations) {
|
|
4206
|
-
const listedNativeTakeover = isRecord(listedConversation.native_session_takeover)
|
|
4207
|
-
? listedConversation.native_session_takeover
|
|
4208
|
-
: undefined;
|
|
4209
|
-
if (listedNativeTakeover?.terminal_bridge !== true) {
|
|
4210
|
-
ignored += 1;
|
|
4211
|
-
continue;
|
|
4212
|
-
}
|
|
4213
4651
|
const statePath = expandHome(stringValue(listedConversation.state_path) ??
|
|
4214
4652
|
statePathForConversationId(listedConversation.conversation_id, storeDir));
|
|
4215
4653
|
const logPath = expandHome(stringValue(listedConversation.event_log_path) ??
|
|
4216
4654
|
logPathForStatePath(statePath));
|
|
4217
4655
|
try {
|
|
4656
|
+
const callbackRecovery = prepareCallbackDeliveryReconciliation({
|
|
4657
|
+
statePath,
|
|
4658
|
+
logPath,
|
|
4659
|
+
delayMs: options.callbackRetryDelayMs
|
|
4660
|
+
});
|
|
4661
|
+
if (callbackRecovery.handled) {
|
|
4662
|
+
if (callbackRecovery.status === "launched") {
|
|
4663
|
+
launched += 1;
|
|
4664
|
+
}
|
|
4665
|
+
else if (callbackRecovery.status === "already_running") {
|
|
4666
|
+
alreadyRunning += 1;
|
|
4667
|
+
}
|
|
4668
|
+
else {
|
|
4669
|
+
skipped += 1;
|
|
4670
|
+
}
|
|
4671
|
+
items.push({
|
|
4672
|
+
conversation_id: callbackRecovery.conversationId,
|
|
4673
|
+
status: callbackRecovery.status,
|
|
4674
|
+
reason: callbackRecovery.reason,
|
|
4675
|
+
...(callbackRecovery.monitorPid === undefined
|
|
4676
|
+
? {}
|
|
4677
|
+
: { monitor_pid: callbackRecovery.monitorPid })
|
|
4678
|
+
});
|
|
4679
|
+
continue;
|
|
4680
|
+
}
|
|
4681
|
+
const listedNativeTakeover = isRecord(listedConversation.native_session_takeover)
|
|
4682
|
+
? listedConversation.native_session_takeover
|
|
4683
|
+
: undefined;
|
|
4684
|
+
if (listedNativeTakeover?.terminal_bridge !== true) {
|
|
4685
|
+
ignored += 1;
|
|
4686
|
+
continue;
|
|
4687
|
+
}
|
|
4218
4688
|
const initialConversation = await migrateLegacyTerminalAgentIdentity({
|
|
4219
4689
|
conversation: loadState(statePath),
|
|
4220
4690
|
statePath,
|
|
@@ -4359,6 +4829,80 @@ async function runReconcileMonitors(options) {
|
|
|
4359
4829
|
items
|
|
4360
4830
|
});
|
|
4361
4831
|
}
|
|
4832
|
+
function prepareCallbackDeliveryReconciliation({ statePath, logPath, delayMs }) {
|
|
4833
|
+
const releaseStateLock = acquireFileLock(`${statePath}.lock`);
|
|
4834
|
+
try {
|
|
4835
|
+
const conversation = loadState(statePath);
|
|
4836
|
+
const callbackDelivery = isRecord(conversation.callback_delivery)
|
|
4837
|
+
? conversation.callback_delivery
|
|
4838
|
+
: undefined;
|
|
4839
|
+
if (!["callback_pending", "callback_failed"].includes(conversation.status)) {
|
|
4840
|
+
return {
|
|
4841
|
+
handled: false
|
|
4842
|
+
};
|
|
4843
|
+
}
|
|
4844
|
+
const conversationId = stringValue(conversation.conversation_id) ?? "unknown";
|
|
4845
|
+
const attempts = Number(callbackDelivery?.attempts ?? 0);
|
|
4846
|
+
if (!["pending", "failed"].includes(String(callbackDelivery?.status ?? "")) ||
|
|
4847
|
+
!isRecord(callbackDelivery?.message)) {
|
|
4848
|
+
return {
|
|
4849
|
+
handled: true,
|
|
4850
|
+
conversationId,
|
|
4851
|
+
status: "skipped",
|
|
4852
|
+
reason: "callback_delivery_metadata_missing"
|
|
4853
|
+
};
|
|
4854
|
+
}
|
|
4855
|
+
if (!Number.isSafeInteger(attempts) || attempts < 1 ||
|
|
4856
|
+
attempts > CALLBACK_RETRY_DELAYS_MS.length) {
|
|
4857
|
+
return {
|
|
4858
|
+
handled: true,
|
|
4859
|
+
conversationId,
|
|
4860
|
+
status: "skipped",
|
|
4861
|
+
reason: "callback_delivery_retries_exhausted"
|
|
4862
|
+
};
|
|
4863
|
+
}
|
|
4864
|
+
const configuredDelayMs = Number(delayMs);
|
|
4865
|
+
const retryDelayMs = Number.isFinite(configuredDelayMs) &&
|
|
4866
|
+
configuredDelayMs >= 0
|
|
4867
|
+
? configuredDelayMs
|
|
4868
|
+
: CALLBACK_RETRY_DELAYS_MS[Math.max(0, attempts - 1)];
|
|
4869
|
+
const retryMonitor = startCallbackRetryMonitor({
|
|
4870
|
+
statePath,
|
|
4871
|
+
delayMs: retryDelayMs
|
|
4872
|
+
});
|
|
4873
|
+
const launchedAt = new Date().toISOString();
|
|
4874
|
+
const nextAttemptAt = new Date(Date.now() + retryDelayMs).toISOString();
|
|
4875
|
+
const nextConversation = {
|
|
4876
|
+
...conversation,
|
|
4877
|
+
callback_delivery: {
|
|
4878
|
+
...callbackDelivery,
|
|
4879
|
+
retry_monitor_pid: retryMonitor.pid ?? null,
|
|
4880
|
+
next_attempt_at: nextAttemptAt
|
|
4881
|
+
},
|
|
4882
|
+
updated_at: launchedAt
|
|
4883
|
+
};
|
|
4884
|
+
saveState(statePath, nextConversation);
|
|
4885
|
+
appendEvent(logPath, {
|
|
4886
|
+
ts: launchedAt,
|
|
4887
|
+
conversation_id: conversationId,
|
|
4888
|
+
event: "callback_retry_monitor_launched",
|
|
4889
|
+
message_id: callbackDelivery.message.id,
|
|
4890
|
+
pid: retryMonitor.pid ?? null,
|
|
4891
|
+
next_attempt_at: nextAttemptAt,
|
|
4892
|
+
reason: "startup_reconciliation"
|
|
4893
|
+
});
|
|
4894
|
+
return {
|
|
4895
|
+
handled: true,
|
|
4896
|
+
conversationId,
|
|
4897
|
+
status: "launched",
|
|
4898
|
+
reason: "callback_delivery_reconciliation",
|
|
4899
|
+
monitorPid: retryMonitor.pid
|
|
4900
|
+
};
|
|
4901
|
+
}
|
|
4902
|
+
finally {
|
|
4903
|
+
releaseStateLock();
|
|
4904
|
+
}
|
|
4905
|
+
}
|
|
4362
4906
|
function terminalBridgeReconciliationEligibility(conversation) {
|
|
4363
4907
|
const nativeTakeover = isRecord(conversation?.native_session_takeover)
|
|
4364
4908
|
? conversation.native_session_takeover
|
|
@@ -4456,7 +5000,9 @@ function prepareTerminalBridgeMonitorReconciliation({ statePath, expectedMessage
|
|
|
4456
5000
|
};
|
|
4457
5001
|
}
|
|
4458
5002
|
let renewedLease;
|
|
4459
|
-
|
|
5003
|
+
const usesClaudeHookLease = executorForConversation(conversation).kind === "claude" &&
|
|
5004
|
+
eligibility.nativeTakeover.claude_hook_mode === "enabled";
|
|
5005
|
+
if (usesClaudeHookLease) {
|
|
4460
5006
|
const leaseDeadlineAtMs = Math.min(eligibility.inactivityDeadlineAtMs, eligibility.hardDeadlineAtMs);
|
|
4461
5007
|
if (leaseDeadlineAtMs <= Date.now()) {
|
|
4462
5008
|
return {
|
|
@@ -4585,115 +5131,147 @@ async function runCancel(options) {
|
|
|
4585
5131
|
}
|
|
4586
5132
|
if (cancelResult.status !== 0) {
|
|
4587
5133
|
throw new Error(cleanProcessText(cancelResult.stderr || cancelResult.stdout || `acpx ${executor.kind} cancel exited with status ${cancelResult.status}`));
|
|
4588
|
-
}
|
|
4589
|
-
const nextConversation = {
|
|
4590
|
-
...conversation,
|
|
4591
|
-
status: "cancelling",
|
|
4592
|
-
cancel_requested_at: now,
|
|
4593
|
-
updated_at: now
|
|
4594
|
-
};
|
|
4595
|
-
saveState(statePath, nextConversation);
|
|
4596
|
-
runtimeLog("info", "conversation_cancelling", {
|
|
4597
|
-
conversation_id: conversation.conversation_id,
|
|
4598
|
-
agent: executor.kind,
|
|
4599
|
-
executor_session: executor.session,
|
|
4600
|
-
state_path: statePath
|
|
4601
|
-
});
|
|
4602
|
-
printJson({
|
|
4603
|
-
conversation: nextConversation,
|
|
4604
|
-
cancel_requested: true,
|
|
4605
|
-
executor,
|
|
4606
|
-
acpx_command: ["acpx", ...cancelArgs],
|
|
4607
|
-
budget: budgetAction(nextConversation)
|
|
4608
|
-
});
|
|
4609
|
-
}
|
|
4610
|
-
async function runTerminalConversationCancel({ options, conversationId, agent, terminalControl, pid }) {
|
|
4611
|
-
const cancellation = await createTerminalAgentBridge(options).cancel(agent, terminalControl, {
|
|
4612
|
-
runtime: {
|
|
4613
|
-
pid,
|
|
4614
|
-
cwd: terminalControl.currentPath,
|
|
4615
|
-
terminalTarget: terminalControl.target
|
|
4616
|
-
},
|
|
4617
|
-
scrollbackLines: Number(options.scrollbackLines ?? 120)
|
|
4618
|
-
});
|
|
4619
|
-
runtimeLog("info", "terminal_cancel_requested", {
|
|
4620
|
-
conversation_id: conversationId,
|
|
4621
|
-
agent,
|
|
4622
|
-
terminal_target: terminalControl.target,
|
|
4623
|
-
key: cancellation.key,
|
|
4624
|
-
keys: cancellation.keys,
|
|
4625
|
-
denied_approval: cancellation.deniedApproval,
|
|
4626
|
-
request_id: cancellation.requestId,
|
|
4627
|
-
cancel_requested: cancellation.cancelRequested,
|
|
4628
|
-
reason: cancellation.reason
|
|
4629
|
-
});
|
|
4630
|
-
printJson({
|
|
4631
|
-
conversation_id: conversationId,
|
|
4632
|
-
source: "terminal_control",
|
|
4633
|
-
cancel_requested: cancellation.cancelRequested,
|
|
4634
|
-
reason: cancellation.reason,
|
|
4635
|
-
terminal_control: terminalControl,
|
|
4636
|
-
key: cancellation.key,
|
|
4637
|
-
keys: cancellation.keys,
|
|
4638
|
-
denied_approval: cancellation.deniedApproval,
|
|
4639
|
-
request_id: cancellation.requestId
|
|
4640
|
-
});
|
|
4641
|
-
}
|
|
4642
|
-
async function runTerminalControlCancel({ options, conversation, statePath, logPath, agent, terminalControl }) {
|
|
4643
|
-
const cancellation = await createTerminalAgentBridge(options).cancel(agent, terminalControl, {
|
|
4644
|
-
runtime: terminalRuntimeIdentityForConversation(conversation, terminalControl),
|
|
4645
|
-
scrollbackLines: Number(options.scrollbackLines ?? 120)
|
|
4646
|
-
});
|
|
4647
|
-
if (!cancellation.cancelRequested) {
|
|
4648
|
-
printJson({
|
|
4649
|
-
conversation,
|
|
4650
|
-
cancel_requested: false,
|
|
4651
|
-
reason: cancellation.reason,
|
|
4652
|
-
terminal_control: terminalControl,
|
|
4653
|
-
budget: budgetAction(conversation)
|
|
4654
|
-
});
|
|
4655
|
-
return;
|
|
4656
|
-
}
|
|
4657
|
-
const now = new Date().toISOString();
|
|
4658
|
-
appendEvent(logPath, {
|
|
4659
|
-
ts: now,
|
|
4660
|
-
conversation_id: conversation.conversation_id,
|
|
4661
|
-
event: "terminal_cancel_requested",
|
|
4662
|
-
terminal_control: terminalControl,
|
|
4663
|
-
key: cancellation.key,
|
|
4664
|
-
keys: cancellation.keys,
|
|
4665
|
-
denied_approval: cancellation.deniedApproval,
|
|
4666
|
-
request_id: cancellation.requestId
|
|
4667
|
-
});
|
|
4668
|
-
runtimeLog("info", "terminal_cancel_requested", {
|
|
4669
|
-
conversation_id: conversation.conversation_id,
|
|
4670
|
-
agent,
|
|
4671
|
-
terminal_target: terminalControl.target,
|
|
4672
|
-
key: cancellation.key,
|
|
4673
|
-
keys: cancellation.keys,
|
|
4674
|
-
denied_approval: cancellation.deniedApproval,
|
|
4675
|
-
request_id: cancellation.requestId
|
|
4676
|
-
});
|
|
5134
|
+
}
|
|
4677
5135
|
const nextConversation = {
|
|
4678
5136
|
...conversation,
|
|
4679
|
-
status: "
|
|
4680
|
-
|
|
4681
|
-
terminal_cancel_requested_at: now,
|
|
5137
|
+
status: "cancelling",
|
|
5138
|
+
cancel_requested_at: now,
|
|
4682
5139
|
updated_at: now
|
|
4683
5140
|
};
|
|
4684
5141
|
saveState(statePath, nextConversation);
|
|
4685
|
-
|
|
5142
|
+
runtimeLog("info", "conversation_cancelling", {
|
|
5143
|
+
conversation_id: conversation.conversation_id,
|
|
5144
|
+
agent: executor.kind,
|
|
5145
|
+
executor_session: executor.session,
|
|
5146
|
+
state_path: statePath
|
|
5147
|
+
});
|
|
4686
5148
|
printJson({
|
|
4687
5149
|
conversation: nextConversation,
|
|
4688
5150
|
cancel_requested: true,
|
|
4689
|
-
|
|
4690
|
-
|
|
4691
|
-
keys: cancellation.keys,
|
|
4692
|
-
denied_approval: cancellation.deniedApproval,
|
|
4693
|
-
request_id: cancellation.requestId,
|
|
5151
|
+
executor,
|
|
5152
|
+
acpx_command: ["acpx", ...cancelArgs],
|
|
4694
5153
|
budget: budgetAction(nextConversation)
|
|
4695
5154
|
});
|
|
4696
5155
|
}
|
|
5156
|
+
async function runTerminalConversationCancel({ options, conversationId, agent, terminalControl, pid }) {
|
|
5157
|
+
const releaseTerminalLock = acquireFileLock(terminalBridgeSendLockPath(storeDirFromOptions(options), terminalControl), { timeoutMs: 30000 });
|
|
5158
|
+
try {
|
|
5159
|
+
const cancellation = await createTerminalAgentBridge(options).cancel(agent, terminalControl, {
|
|
5160
|
+
runtime: {
|
|
5161
|
+
pid,
|
|
5162
|
+
cwd: terminalControl.currentPath,
|
|
5163
|
+
terminalTarget: terminalControl.target
|
|
5164
|
+
},
|
|
5165
|
+
scrollbackLines: Number(options.scrollbackLines ?? 120)
|
|
5166
|
+
});
|
|
5167
|
+
runtimeLog("info", "terminal_cancel_requested", {
|
|
5168
|
+
conversation_id: conversationId,
|
|
5169
|
+
agent,
|
|
5170
|
+
terminal_target: terminalControl.target,
|
|
5171
|
+
key: cancellation.key,
|
|
5172
|
+
keys: cancellation.keys,
|
|
5173
|
+
denied_approval: cancellation.deniedApproval,
|
|
5174
|
+
request_id: cancellation.requestId,
|
|
5175
|
+
cancel_requested: cancellation.cancelRequested,
|
|
5176
|
+
reason: cancellation.reason
|
|
5177
|
+
});
|
|
5178
|
+
printJson({
|
|
5179
|
+
conversation_id: conversationId,
|
|
5180
|
+
source: "terminal_control",
|
|
5181
|
+
cancel_requested: cancellation.cancelRequested,
|
|
5182
|
+
reason: cancellation.reason,
|
|
5183
|
+
terminal_control: terminalControl,
|
|
5184
|
+
key: cancellation.key,
|
|
5185
|
+
keys: cancellation.keys,
|
|
5186
|
+
denied_approval: cancellation.deniedApproval,
|
|
5187
|
+
request_id: cancellation.requestId
|
|
5188
|
+
});
|
|
5189
|
+
}
|
|
5190
|
+
finally {
|
|
5191
|
+
releaseTerminalLock();
|
|
5192
|
+
}
|
|
5193
|
+
}
|
|
5194
|
+
async function runTerminalControlCancel({ options, conversation, statePath, logPath, agent, terminalControl }) {
|
|
5195
|
+
const releaseTerminalLock = acquireFileLock(terminalBridgeSendLockPath(storeDirFromOptions(options), terminalControl), { timeoutMs: 30000 });
|
|
5196
|
+
let releaseStateLock;
|
|
5197
|
+
try {
|
|
5198
|
+
releaseStateLock = acquireFileLock(`${statePath}.lock`);
|
|
5199
|
+
const currentConversation = loadState(statePath);
|
|
5200
|
+
if (["closed", "cancelled"].includes(currentConversation.status)) {
|
|
5201
|
+
throw new Error(`cannot cancel ${currentConversation.conversation_id}; conversation is ${currentConversation.status}`);
|
|
5202
|
+
}
|
|
5203
|
+
const currentTakeover = isRecord(currentConversation.native_session_takeover)
|
|
5204
|
+
? currentConversation.native_session_takeover
|
|
5205
|
+
: undefined;
|
|
5206
|
+
const currentControl = terminalControlFromTakeover(currentTakeover);
|
|
5207
|
+
if (!currentControl ||
|
|
5208
|
+
currentControl.target !== terminalControl.target ||
|
|
5209
|
+
currentControl.socketPath !== terminalControl.socketPath) {
|
|
5210
|
+
throw new Error("terminal control changed while waiting to cancel; refresh status and retry");
|
|
5211
|
+
}
|
|
5212
|
+
const cancellation = await createTerminalAgentBridge(options).cancel(agent, currentControl, {
|
|
5213
|
+
runtime: terminalRuntimeIdentityForConversation(currentConversation, currentControl),
|
|
5214
|
+
scrollbackLines: Number(options.scrollbackLines ?? 120)
|
|
5215
|
+
});
|
|
5216
|
+
if (!cancellation.cancelRequested) {
|
|
5217
|
+
printJson({
|
|
5218
|
+
conversation: currentConversation,
|
|
5219
|
+
cancel_requested: false,
|
|
5220
|
+
reason: cancellation.reason,
|
|
5221
|
+
terminal_control: currentControl,
|
|
5222
|
+
budget: budgetAction(currentConversation)
|
|
5223
|
+
});
|
|
5224
|
+
return;
|
|
5225
|
+
}
|
|
5226
|
+
const now = new Date().toISOString();
|
|
5227
|
+
appendEvent(logPath, {
|
|
5228
|
+
ts: now,
|
|
5229
|
+
conversation_id: currentConversation.conversation_id,
|
|
5230
|
+
event: "terminal_cancel_requested",
|
|
5231
|
+
terminal_control: currentControl,
|
|
5232
|
+
key: cancellation.key,
|
|
5233
|
+
keys: cancellation.keys,
|
|
5234
|
+
denied_approval: cancellation.deniedApproval,
|
|
5235
|
+
request_id: cancellation.requestId
|
|
5236
|
+
});
|
|
5237
|
+
runtimeLog("info", "terminal_cancel_requested", {
|
|
5238
|
+
conversation_id: currentConversation.conversation_id,
|
|
5239
|
+
agent,
|
|
5240
|
+
terminal_target: currentControl.target,
|
|
5241
|
+
key: cancellation.key,
|
|
5242
|
+
keys: cancellation.keys,
|
|
5243
|
+
denied_approval: cancellation.deniedApproval,
|
|
5244
|
+
request_id: cancellation.requestId
|
|
5245
|
+
});
|
|
5246
|
+
const nextConversation = {
|
|
5247
|
+
...currentConversation,
|
|
5248
|
+
status: "cancelled",
|
|
5249
|
+
cancelled_at: now,
|
|
5250
|
+
terminal_cancel_requested_at: now,
|
|
5251
|
+
updated_at: now
|
|
5252
|
+
};
|
|
5253
|
+
saveState(statePath, nextConversation);
|
|
5254
|
+
releaseClaudeHookLease(nextConversation);
|
|
5255
|
+
printJson({
|
|
5256
|
+
conversation: nextConversation,
|
|
5257
|
+
cancel_requested: true,
|
|
5258
|
+
terminal_control: currentControl,
|
|
5259
|
+
key: cancellation.key,
|
|
5260
|
+
keys: cancellation.keys,
|
|
5261
|
+
denied_approval: cancellation.deniedApproval,
|
|
5262
|
+
request_id: cancellation.requestId,
|
|
5263
|
+
budget: budgetAction(nextConversation)
|
|
5264
|
+
});
|
|
5265
|
+
}
|
|
5266
|
+
finally {
|
|
5267
|
+
try {
|
|
5268
|
+
releaseStateLock?.();
|
|
5269
|
+
}
|
|
5270
|
+
finally {
|
|
5271
|
+
releaseTerminalLock();
|
|
5272
|
+
}
|
|
5273
|
+
}
|
|
5274
|
+
}
|
|
4697
5275
|
function runRecover(options) {
|
|
4698
5276
|
runRecoveryDecision({ ...options, mode: "recover" });
|
|
4699
5277
|
}
|
|
@@ -4861,36 +5439,57 @@ function formatProtocolHistoryForRecovery(events) {
|
|
|
4861
5439
|
});
|
|
4862
5440
|
return lines.length ? lines.join("\n") : "- No prior protocol messages were recorded.";
|
|
4863
5441
|
}
|
|
4864
|
-
function runClose(options) {
|
|
4865
|
-
const
|
|
4866
|
-
const
|
|
4867
|
-
const
|
|
4868
|
-
|
|
4869
|
-
|
|
4870
|
-
|
|
4871
|
-
|
|
4872
|
-
|
|
4873
|
-
|
|
4874
|
-
|
|
4875
|
-
|
|
4876
|
-
|
|
4877
|
-
|
|
4878
|
-
|
|
4879
|
-
|
|
4880
|
-
|
|
4881
|
-
|
|
4882
|
-
|
|
4883
|
-
|
|
4884
|
-
|
|
4885
|
-
|
|
4886
|
-
|
|
4887
|
-
|
|
4888
|
-
|
|
4889
|
-
|
|
4890
|
-
|
|
4891
|
-
|
|
4892
|
-
|
|
4893
|
-
|
|
5442
|
+
async function runClose(options) {
|
|
5443
|
+
const loaded = loadConversationFromOptions(options);
|
|
5444
|
+
const { statePath, logPath } = loaded;
|
|
5445
|
+
const nativeTakeover = isRecord(loaded.conversation.native_session_takeover)
|
|
5446
|
+
? loaded.conversation.native_session_takeover
|
|
5447
|
+
: undefined;
|
|
5448
|
+
const terminalControl = terminalControlFromTakeover(nativeTakeover);
|
|
5449
|
+
const releaseTerminalLock = terminalControl
|
|
5450
|
+
? acquireFileLock(terminalBridgeSendLockPath(storeDirFromOptions(options), terminalControl), { timeoutMs: 30000 })
|
|
5451
|
+
: () => { };
|
|
5452
|
+
let releaseStateLock;
|
|
5453
|
+
try {
|
|
5454
|
+
releaseStateLock = acquireFileLock(`${statePath}.lock`);
|
|
5455
|
+
const conversation = loadState(statePath);
|
|
5456
|
+
const now = new Date().toISOString();
|
|
5457
|
+
const closed = {
|
|
5458
|
+
...conversation,
|
|
5459
|
+
status: "closed",
|
|
5460
|
+
closed_at: now,
|
|
5461
|
+
close_reason: options.reason ?? "closed by request",
|
|
5462
|
+
updated_at: now
|
|
5463
|
+
};
|
|
5464
|
+
saveState(statePath, closed);
|
|
5465
|
+
releaseClaudeHookLease(closed);
|
|
5466
|
+
appendEvent(logPath, {
|
|
5467
|
+
ts: now,
|
|
5468
|
+
conversation_id: conversation.conversation_id,
|
|
5469
|
+
event: "conversation_closed",
|
|
5470
|
+
status: "closed",
|
|
5471
|
+
reason: closed.close_reason
|
|
5472
|
+
});
|
|
5473
|
+
runtimeLog("info", "conversation_closed", {
|
|
5474
|
+
conversation_id: conversation.conversation_id,
|
|
5475
|
+
status: "closed",
|
|
5476
|
+
reason: closed.close_reason,
|
|
5477
|
+
state_path: statePath,
|
|
5478
|
+
event_log_path: logPath
|
|
5479
|
+
});
|
|
5480
|
+
printJson({
|
|
5481
|
+
conversation: closed,
|
|
5482
|
+
closed: true
|
|
5483
|
+
});
|
|
5484
|
+
}
|
|
5485
|
+
finally {
|
|
5486
|
+
try {
|
|
5487
|
+
releaseStateLock?.();
|
|
5488
|
+
}
|
|
5489
|
+
finally {
|
|
5490
|
+
releaseTerminalLock();
|
|
5491
|
+
}
|
|
5492
|
+
}
|
|
4894
5493
|
}
|
|
4895
5494
|
async function runMonitor(options) {
|
|
4896
5495
|
if (options.callbackRetry) {
|
|
@@ -5027,13 +5626,16 @@ async function runMonitor(options) {
|
|
|
5027
5626
|
sleepSync(pollIntervalMs);
|
|
5028
5627
|
}
|
|
5029
5628
|
}
|
|
5030
|
-
function startCallbackRetryMonitor({ statePath }) {
|
|
5629
|
+
function startCallbackRetryMonitor({ statePath, delayMs = CALLBACK_RETRY_DELAYS_MS[0] }) {
|
|
5630
|
+
const normalizedDelayMs = Math.max(0, Number.isFinite(Number(delayMs)) ? Number(delayMs) : CALLBACK_RETRY_DELAYS_MS[0]);
|
|
5031
5631
|
const child = spawn(process.execPath, [
|
|
5032
5632
|
new URL(import.meta.url).pathname,
|
|
5033
5633
|
"monitor",
|
|
5034
5634
|
"--callback-retry",
|
|
5035
5635
|
"--state",
|
|
5036
|
-
statePath
|
|
5636
|
+
statePath,
|
|
5637
|
+
"--callback-retry-delay-ms",
|
|
5638
|
+
String(normalizedDelayMs)
|
|
5037
5639
|
], {
|
|
5038
5640
|
detached: true,
|
|
5039
5641
|
stdio: "ignore",
|
|
@@ -5045,27 +5647,45 @@ function startCallbackRetryMonitor({ statePath }) {
|
|
|
5045
5647
|
}
|
|
5046
5648
|
function runCallbackRetryMonitor(options) {
|
|
5047
5649
|
const statePath = expandHome(required(options.state, "--state is required"));
|
|
5650
|
+
const initialDelayMs = Math.max(0, Number.isFinite(Number(options.callbackRetryDelayMs))
|
|
5651
|
+
? Number(options.callbackRetryDelayMs)
|
|
5652
|
+
: CALLBACK_RETRY_DELAYS_MS[0]);
|
|
5653
|
+
sleepSync(initialDelayMs);
|
|
5048
5654
|
while (true) {
|
|
5049
5655
|
const conversation = loadState(statePath);
|
|
5050
5656
|
const callbackDelivery = isRecord(conversation.callback_delivery)
|
|
5051
5657
|
? conversation.callback_delivery
|
|
5052
5658
|
: undefined;
|
|
5053
5659
|
const attempts = Number(callbackDelivery?.attempts ?? 0);
|
|
5054
|
-
if (
|
|
5660
|
+
if (!["callback_pending", "callback_failed"].includes(conversation.status) ||
|
|
5661
|
+
!["pending", "failed"].includes(String(callbackDelivery?.status ?? "")) ||
|
|
5662
|
+
!isRecord(callbackDelivery?.message)) {
|
|
5055
5663
|
return;
|
|
5056
5664
|
}
|
|
5057
5665
|
if (attempts > CALLBACK_RETRY_DELAYS_MS.length) {
|
|
5058
5666
|
return;
|
|
5059
5667
|
}
|
|
5060
|
-
|
|
5061
|
-
|
|
5062
|
-
|
|
5668
|
+
let releaseLock;
|
|
5669
|
+
try {
|
|
5670
|
+
releaseLock = acquireFileLock(`${statePath}.lock`);
|
|
5671
|
+
}
|
|
5672
|
+
catch (error) {
|
|
5673
|
+
if (isRecord(error) && error.code === "LOCK_TIMEOUT") {
|
|
5674
|
+
sleepSync(1000);
|
|
5675
|
+
continue;
|
|
5676
|
+
}
|
|
5677
|
+
throw error;
|
|
5678
|
+
}
|
|
5063
5679
|
try {
|
|
5064
5680
|
const current = loadState(statePath);
|
|
5065
5681
|
const currentDelivery = isRecord(current.callback_delivery)
|
|
5066
5682
|
? current.callback_delivery
|
|
5067
5683
|
: undefined;
|
|
5068
|
-
|
|
5684
|
+
const currentAttempts = Number(currentDelivery?.attempts ?? 0);
|
|
5685
|
+
if (!["callback_pending", "callback_failed"].includes(current.status) ||
|
|
5686
|
+
!["pending", "failed"].includes(String(currentDelivery?.status ?? "")) ||
|
|
5687
|
+
!isRecord(currentDelivery?.message) ||
|
|
5688
|
+
currentAttempts > CALLBACK_RETRY_DELAYS_MS.length) {
|
|
5069
5689
|
return;
|
|
5070
5690
|
}
|
|
5071
5691
|
try {
|
|
@@ -5091,6 +5711,19 @@ function runCallbackRetryMonitor(options) {
|
|
|
5091
5711
|
finally {
|
|
5092
5712
|
releaseLock();
|
|
5093
5713
|
}
|
|
5714
|
+
const latest = loadState(statePath);
|
|
5715
|
+
const latestDelivery = isRecord(latest.callback_delivery)
|
|
5716
|
+
? latest.callback_delivery
|
|
5717
|
+
: undefined;
|
|
5718
|
+
const latestAttempts = Number(latestDelivery?.attempts ?? 0);
|
|
5719
|
+
if (!["callback_pending", "callback_failed"].includes(latest.status) ||
|
|
5720
|
+
!["pending", "failed"].includes(String(latestDelivery?.status ?? "")) ||
|
|
5721
|
+
!isRecord(latestDelivery?.message) ||
|
|
5722
|
+
latestAttempts > CALLBACK_RETRY_DELAYS_MS.length) {
|
|
5723
|
+
return;
|
|
5724
|
+
}
|
|
5725
|
+
const delayMs = CALLBACK_RETRY_DELAYS_MS[Math.max(0, latestAttempts - 1)];
|
|
5726
|
+
sleepSync(delayMs);
|
|
5094
5727
|
}
|
|
5095
5728
|
}
|
|
5096
5729
|
async function runTerminalBridgeMonitor(options) {
|
|
@@ -5267,6 +5900,25 @@ async function runTerminalBridgeMonitorWithLock(options) {
|
|
|
5267
5900
|
});
|
|
5268
5901
|
const terminalStatus = poll.status;
|
|
5269
5902
|
const approval = terminalStatus.approval_state;
|
|
5903
|
+
const currentScreenFingerprint = stringValue(terminalStatus?.screen?.digest) ??
|
|
5904
|
+
terminalBridgeScreenFingerprint(terminalStatus?.screen?.excerpt);
|
|
5905
|
+
const currentScreenChangedSinceSend = preSendScreenFingerprint !== undefined &&
|
|
5906
|
+
currentScreenFingerprint !== undefined &&
|
|
5907
|
+
currentScreenFingerprint !== preSendScreenFingerprint;
|
|
5908
|
+
if (executor.kind === "claude" &&
|
|
5909
|
+
isRecord(approval) &&
|
|
5910
|
+
approval.approvable === true &&
|
|
5911
|
+
approval.decision_mode === "keys" &&
|
|
5912
|
+
!currentScreenChangedSinceSend) {
|
|
5913
|
+
previousScreenFingerprint = currentScreenFingerprint;
|
|
5914
|
+
runtimeLog("warn", "claude_screen_approval_not_new", {
|
|
5915
|
+
conversation_id: conversation.conversation_id,
|
|
5916
|
+
terminal_target: terminalControl.target,
|
|
5917
|
+
reason: "permission screen is not proven to have changed since the managed send"
|
|
5918
|
+
});
|
|
5919
|
+
sleepSync(pollIntervalMs);
|
|
5920
|
+
continue;
|
|
5921
|
+
}
|
|
5270
5922
|
if (isRecord(approval) && approval.blocked === true && approval.approvable !== true) {
|
|
5271
5923
|
const approvalReason = stringValue(approval.reason) ??
|
|
5272
5924
|
"Claude Code permission state cannot be safely resolved through AKK";
|
|
@@ -5288,8 +5940,67 @@ async function runTerminalBridgeMonitorWithLock(options) {
|
|
|
5288
5940
|
logPath,
|
|
5289
5941
|
terminalControl,
|
|
5290
5942
|
terminalStatus,
|
|
5291
|
-
fingerprint
|
|
5943
|
+
fingerprint,
|
|
5944
|
+
expectedConversation: {
|
|
5945
|
+
conversationId: conversation.conversation_id,
|
|
5946
|
+
status: conversation.status,
|
|
5947
|
+
updatedAt: conversation.updated_at,
|
|
5948
|
+
messageId: currentMessageId
|
|
5949
|
+
},
|
|
5950
|
+
onRecorded: (notificationConversation) => {
|
|
5951
|
+
const callbackMessage = createMessage({
|
|
5952
|
+
conversation: notificationConversation,
|
|
5953
|
+
from: executor.actor,
|
|
5954
|
+
to: "openclaw",
|
|
5955
|
+
type: "blocked",
|
|
5956
|
+
requiresResponse: true,
|
|
5957
|
+
body: [
|
|
5958
|
+
`${executor.display_name} is waiting at a permission state that AKK cannot safely approve.`,
|
|
5959
|
+
approvalReason,
|
|
5960
|
+
"",
|
|
5961
|
+
`Conversation: ${notificationConversation.conversation_id}`,
|
|
5962
|
+
`Terminal: ${terminalControl.target}`,
|
|
5963
|
+
"Review and resolve this dialog in the terminal manually. AKK intentionally sends no key when the request identity cannot be revalidated."
|
|
5964
|
+
].join("\n"),
|
|
5965
|
+
metadata: {
|
|
5966
|
+
source: "terminal_bridge",
|
|
5967
|
+
reason: "approval_not_approvable",
|
|
5968
|
+
terminal_control: terminalControl,
|
|
5969
|
+
terminal_status: terminalStatus,
|
|
5970
|
+
approval_fingerprint: fingerprint
|
|
5971
|
+
}
|
|
5972
|
+
});
|
|
5973
|
+
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
|
+
return {
|
|
5989
|
+
callbackMessage,
|
|
5990
|
+
delivered: true
|
|
5991
|
+
};
|
|
5992
|
+
}
|
|
5993
|
+
return {
|
|
5994
|
+
callbackMessage,
|
|
5995
|
+
delivered: false
|
|
5996
|
+
};
|
|
5997
|
+
}
|
|
5292
5998
|
});
|
|
5999
|
+
if (notification.stale) {
|
|
6000
|
+
previousScreenFingerprint = currentScreenFingerprint;
|
|
6001
|
+
sleepSync(pollIntervalMs);
|
|
6002
|
+
continue;
|
|
6003
|
+
}
|
|
5293
6004
|
if (notification.duplicate) {
|
|
5294
6005
|
printJson({
|
|
5295
6006
|
conversation: notification.conversation,
|
|
@@ -5304,43 +6015,7 @@ async function runTerminalBridgeMonitorWithLock(options) {
|
|
|
5304
6015
|
});
|
|
5305
6016
|
return;
|
|
5306
6017
|
}
|
|
5307
|
-
|
|
5308
|
-
conversation: notification.conversation,
|
|
5309
|
-
from: executor.actor,
|
|
5310
|
-
to: "openclaw",
|
|
5311
|
-
type: "blocked",
|
|
5312
|
-
requiresResponse: true,
|
|
5313
|
-
body: [
|
|
5314
|
-
`${executor.display_name} is waiting at a permission state that AKK cannot safely approve.`,
|
|
5315
|
-
approvalReason,
|
|
5316
|
-
"",
|
|
5317
|
-
`Conversation: ${notification.conversation.conversation_id}`,
|
|
5318
|
-
`Terminal: ${terminalControl.target}`,
|
|
5319
|
-
"Review and resolve this dialog in the terminal manually. AKK intentionally sends no key when the request identity cannot be revalidated."
|
|
5320
|
-
].join("\n"),
|
|
5321
|
-
metadata: {
|
|
5322
|
-
source: "terminal_bridge",
|
|
5323
|
-
reason: "approval_not_approvable",
|
|
5324
|
-
terminal_control: terminalControl,
|
|
5325
|
-
terminal_status: terminalStatus,
|
|
5326
|
-
approval_fingerprint: fingerprint
|
|
5327
|
-
}
|
|
5328
|
-
});
|
|
5329
|
-
if (notification.conversation.gateway_method) {
|
|
5330
|
-
runLockedCallback({
|
|
5331
|
-
...options,
|
|
5332
|
-
statePath,
|
|
5333
|
-
log: logPath,
|
|
5334
|
-
messageJson: JSON.stringify(callbackMessage),
|
|
5335
|
-
gatewayMethod: notification.conversation.gateway_method,
|
|
5336
|
-
gatewaySession: notification.conversation.gateway_session,
|
|
5337
|
-
openclawSession: notification.conversation.openclaw_session,
|
|
5338
|
-
openclawBin: notification.conversation.openclaw_bin,
|
|
5339
|
-
gatewayUrl: stringValue(notification.conversation.gateway_token)
|
|
5340
|
-
? notification.conversation.gateway_url
|
|
5341
|
-
: undefined,
|
|
5342
|
-
token: stringValue(notification.conversation.gateway_token)
|
|
5343
|
-
});
|
|
6018
|
+
if (notification.recorded?.delivered) {
|
|
5344
6019
|
return;
|
|
5345
6020
|
}
|
|
5346
6021
|
printJson({
|
|
@@ -5350,7 +6025,7 @@ async function runTerminalBridgeMonitorWithLock(options) {
|
|
|
5350
6025
|
awaiting_approval: true,
|
|
5351
6026
|
approvable: false,
|
|
5352
6027
|
delivered: false,
|
|
5353
|
-
message: callbackMessage,
|
|
6028
|
+
message: notification.recorded?.callbackMessage,
|
|
5354
6029
|
reason: "gateway_method_missing",
|
|
5355
6030
|
terminal_control: terminalControl,
|
|
5356
6031
|
terminal_status: terminalStatus
|
|
@@ -5373,8 +6048,74 @@ async function runTerminalBridgeMonitorWithLock(options) {
|
|
|
5373
6048
|
logPath,
|
|
5374
6049
|
terminalControl,
|
|
5375
6050
|
terminalStatus,
|
|
5376
|
-
fingerprint
|
|
6051
|
+
fingerprint,
|
|
6052
|
+
expectedConversation: {
|
|
6053
|
+
conversationId: conversation.conversation_id,
|
|
6054
|
+
status: conversation.status,
|
|
6055
|
+
updatedAt: conversation.updated_at,
|
|
6056
|
+
messageId: currentMessageId
|
|
6057
|
+
},
|
|
6058
|
+
onRecorded: (notificationConversation) => {
|
|
6059
|
+
const callbackMessage = createMessage({
|
|
6060
|
+
conversation: notificationConversation,
|
|
6061
|
+
from: executor.actor,
|
|
6062
|
+
to: "openclaw",
|
|
6063
|
+
type: "question",
|
|
6064
|
+
requiresResponse: true,
|
|
6065
|
+
body: terminalBridgeApprovalInstructions({
|
|
6066
|
+
conversation: notificationConversation,
|
|
6067
|
+
terminalControl,
|
|
6068
|
+
terminalStatus
|
|
6069
|
+
}),
|
|
6070
|
+
metadata: {
|
|
6071
|
+
source: "terminal_bridge",
|
|
6072
|
+
reason: "approval_required",
|
|
6073
|
+
terminal_control: terminalControl,
|
|
6074
|
+
terminal_status: terminalStatus,
|
|
6075
|
+
approval_fingerprint: fingerprint,
|
|
6076
|
+
approval_candidate: terminalBridgeApprovalCandidate({
|
|
6077
|
+
executor,
|
|
6078
|
+
terminalControl,
|
|
6079
|
+
terminalStatus,
|
|
6080
|
+
fingerprint
|
|
6081
|
+
}),
|
|
6082
|
+
approve_command: `AKK approve ${notificationConversation.conversation_id} --expected-approval-fingerprint ${fingerprint}`,
|
|
6083
|
+
deny_command: `AKK cancel ${notificationConversation.conversation_id}`,
|
|
6084
|
+
approve_tool: "agent_knock_knock_approve",
|
|
6085
|
+
deny_tool: "agent_knock_knock_cancel"
|
|
6086
|
+
}
|
|
6087
|
+
});
|
|
6088
|
+
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
|
+
return {
|
|
6104
|
+
callbackMessage,
|
|
6105
|
+
delivered: true
|
|
6106
|
+
};
|
|
6107
|
+
}
|
|
6108
|
+
return {
|
|
6109
|
+
callbackMessage,
|
|
6110
|
+
delivered: false
|
|
6111
|
+
};
|
|
6112
|
+
}
|
|
5377
6113
|
});
|
|
6114
|
+
if (notification.stale) {
|
|
6115
|
+
previousScreenFingerprint = currentScreenFingerprint;
|
|
6116
|
+
sleepSync(pollIntervalMs);
|
|
6117
|
+
continue;
|
|
6118
|
+
}
|
|
5378
6119
|
if (notification.duplicate) {
|
|
5379
6120
|
printJson({
|
|
5380
6121
|
conversation: notification.conversation,
|
|
@@ -5387,48 +6128,7 @@ async function runTerminalBridgeMonitorWithLock(options) {
|
|
|
5387
6128
|
});
|
|
5388
6129
|
return;
|
|
5389
6130
|
}
|
|
5390
|
-
|
|
5391
|
-
conversation: notification.conversation,
|
|
5392
|
-
from: executor.actor,
|
|
5393
|
-
to: "openclaw",
|
|
5394
|
-
type: "question",
|
|
5395
|
-
requiresResponse: true,
|
|
5396
|
-
body: terminalBridgeApprovalInstructions({
|
|
5397
|
-
conversation: notification.conversation,
|
|
5398
|
-
terminalControl,
|
|
5399
|
-
terminalStatus
|
|
5400
|
-
}),
|
|
5401
|
-
metadata: {
|
|
5402
|
-
source: "terminal_bridge",
|
|
5403
|
-
reason: "approval_required",
|
|
5404
|
-
terminal_control: terminalControl,
|
|
5405
|
-
terminal_status: terminalStatus,
|
|
5406
|
-
approval_fingerprint: fingerprint,
|
|
5407
|
-
approval_candidate: terminalBridgeApprovalCandidate({
|
|
5408
|
-
executor,
|
|
5409
|
-
terminalControl,
|
|
5410
|
-
terminalStatus,
|
|
5411
|
-
fingerprint
|
|
5412
|
-
}),
|
|
5413
|
-
approve_command: `AKK approve ${notification.conversation.conversation_id} --expected-approval-fingerprint ${fingerprint}`,
|
|
5414
|
-
deny_command: `AKK cancel ${notification.conversation.conversation_id}`,
|
|
5415
|
-
approve_tool: "agent_knock_knock_approve",
|
|
5416
|
-
deny_tool: "agent_knock_knock_cancel"
|
|
5417
|
-
}
|
|
5418
|
-
});
|
|
5419
|
-
if (notification.conversation.gateway_method) {
|
|
5420
|
-
runLockedCallback({
|
|
5421
|
-
...options,
|
|
5422
|
-
statePath,
|
|
5423
|
-
log: logPath,
|
|
5424
|
-
messageJson: JSON.stringify(callbackMessage),
|
|
5425
|
-
gatewayMethod: notification.conversation.gateway_method,
|
|
5426
|
-
gatewaySession: notification.conversation.gateway_session,
|
|
5427
|
-
openclawSession: notification.conversation.openclaw_session,
|
|
5428
|
-
openclawBin: notification.conversation.openclaw_bin,
|
|
5429
|
-
gatewayUrl: stringValue(notification.conversation.gateway_token) ? notification.conversation.gateway_url : undefined,
|
|
5430
|
-
token: stringValue(notification.conversation.gateway_token)
|
|
5431
|
-
});
|
|
6131
|
+
if (notification.recorded?.delivered) {
|
|
5432
6132
|
return;
|
|
5433
6133
|
}
|
|
5434
6134
|
printJson({
|
|
@@ -5437,14 +6137,14 @@ async function runTerminalBridgeMonitorWithLock(options) {
|
|
|
5437
6137
|
terminal_bridge: true,
|
|
5438
6138
|
awaiting_approval: true,
|
|
5439
6139
|
delivered: false,
|
|
5440
|
-
message: callbackMessage,
|
|
6140
|
+
message: notification.recorded?.callbackMessage,
|
|
5441
6141
|
reason: "gateway_method_missing",
|
|
5442
6142
|
terminal_control: terminalControl,
|
|
5443
6143
|
terminal_status: terminalStatus
|
|
5444
6144
|
});
|
|
5445
6145
|
return;
|
|
5446
6146
|
}
|
|
5447
|
-
const screenFingerprint =
|
|
6147
|
+
const screenFingerprint = currentScreenFingerprint;
|
|
5448
6148
|
const screenChanged = previousScreenFingerprint !== undefined &&
|
|
5449
6149
|
screenFingerprint !== undefined &&
|
|
5450
6150
|
screenFingerprint !== previousScreenFingerprint;
|
|
@@ -5730,6 +6430,7 @@ function tryAcquireTerminalBridgeMonitorLock(statePath, terminalMessageId) {
|
|
|
5730
6430
|
}
|
|
5731
6431
|
}
|
|
5732
6432
|
function terminalBridgeSendLockPath(storeDir, terminalControl) {
|
|
6433
|
+
ensureDir(storeDir);
|
|
5733
6434
|
const terminalKey = createHash("sha256")
|
|
5734
6435
|
.update(JSON.stringify({
|
|
5735
6436
|
target: terminalControl.target,
|
|
@@ -6215,6 +6916,18 @@ function runLockedCallback(options) {
|
|
|
6215
6916
|
CONVERSATION_STATUSES.has(storedFinalStatus)
|
|
6216
6917
|
? storedFinalStatus
|
|
6217
6918
|
: nextConversation.status;
|
|
6919
|
+
const callbackRetryDelayMs = CALLBACK_RETRY_DELAYS_MS[Math.min(CALLBACK_RETRY_DELAYS_MS.length - 1, Math.max(0, deliveryAttempt - 1))];
|
|
6920
|
+
const callbackWatchdog = trackCallbackDelivery &&
|
|
6921
|
+
requiresDelivery &&
|
|
6922
|
+
options.recordOnly !== true &&
|
|
6923
|
+
!retryingPending &&
|
|
6924
|
+
options.disableCallbackRetry !== true &&
|
|
6925
|
+
deliveryAttempt <= CALLBACK_RETRY_DELAYS_MS.length
|
|
6926
|
+
? startCallbackRetryMonitor({
|
|
6927
|
+
statePath: options.statePath,
|
|
6928
|
+
delayMs: callbackRetryDelayMs
|
|
6929
|
+
})
|
|
6930
|
+
: undefined;
|
|
6218
6931
|
if (!retryingPending && !recoveringTerminalCompletion) {
|
|
6219
6932
|
appendEvent(logPath, messageEvent(message));
|
|
6220
6933
|
}
|
|
@@ -6235,7 +6948,13 @@ function runLockedCallback(options) {
|
|
|
6235
6948
|
openclaw_bin: options.openclawBin ?? conversation.openclaw_bin,
|
|
6236
6949
|
close_terminal_bridge_on_done: closeTerminalBridgeOnDone,
|
|
6237
6950
|
track_delivery: true,
|
|
6238
|
-
final_status: finalStatus
|
|
6951
|
+
final_status: finalStatus,
|
|
6952
|
+
...(callbackWatchdog
|
|
6953
|
+
? {
|
|
6954
|
+
retry_monitor_pid: callbackWatchdog.pid ?? null,
|
|
6955
|
+
next_attempt_at: new Date(Date.now() + callbackRetryDelayMs).toISOString()
|
|
6956
|
+
}
|
|
6957
|
+
: {})
|
|
6239
6958
|
},
|
|
6240
6959
|
updated_at: now
|
|
6241
6960
|
};
|
|
@@ -6249,6 +6968,18 @@ function runLockedCallback(options) {
|
|
|
6249
6968
|
message_id: message.id,
|
|
6250
6969
|
attempt: deliveryAttempt
|
|
6251
6970
|
});
|
|
6971
|
+
if (callbackWatchdog) {
|
|
6972
|
+
appendEvent(logPath, {
|
|
6973
|
+
ts: new Date().toISOString(),
|
|
6974
|
+
conversation_id: conversation.conversation_id,
|
|
6975
|
+
event: "callback_retry_monitor_launched",
|
|
6976
|
+
message_id: message.id,
|
|
6977
|
+
pid: callbackWatchdog.pid ?? null,
|
|
6978
|
+
next_attempt_at: isRecord(nextConversation.callback_delivery)
|
|
6979
|
+
? nextConversation.callback_delivery.next_attempt_at
|
|
6980
|
+
: undefined
|
|
6981
|
+
});
|
|
6982
|
+
}
|
|
6252
6983
|
}
|
|
6253
6984
|
saveState(options.statePath, nextConversation);
|
|
6254
6985
|
runtimeLog("info", "callback_received", {
|
|
@@ -7476,54 +8207,84 @@ function cleanupIdleConversations(storeDir, options = {}, now = new Date()) {
|
|
|
7476
8207
|
}
|
|
7477
8208
|
const conversations = listConversations(storeDir);
|
|
7478
8209
|
let closed = 0;
|
|
7479
|
-
for (const
|
|
7480
|
-
if (
|
|
8210
|
+
for (const listedConversation of conversations) {
|
|
8211
|
+
if (listedConversation.status !== "idle" || !listedConversation.idle_since) {
|
|
7481
8212
|
continue;
|
|
7482
8213
|
}
|
|
7483
|
-
const
|
|
7484
|
-
if (!Number.isFinite(
|
|
8214
|
+
const listedIdleSinceMs = Date.parse(listedConversation.idle_since);
|
|
8215
|
+
if (!Number.isFinite(listedIdleSinceMs)) {
|
|
7485
8216
|
continue;
|
|
7486
8217
|
}
|
|
7487
|
-
const
|
|
7488
|
-
isRecord(
|
|
7489
|
-
typeof
|
|
7490
|
-
if (!
|
|
8218
|
+
const listedTerminalBridge = terminalBridgeEnabled(listedConversation) &&
|
|
8219
|
+
isRecord(listedConversation.native_session_takeover) &&
|
|
8220
|
+
typeof listedConversation.native_session_takeover.terminal_bridge_message_id === "string";
|
|
8221
|
+
if (!listedTerminalBridge && now.getTime() - listedIdleSinceMs < timeoutMinutes * 60 * 1000) {
|
|
7491
8222
|
continue;
|
|
7492
8223
|
}
|
|
7493
|
-
const statePath =
|
|
7494
|
-
|
|
7495
|
-
|
|
7496
|
-
|
|
7497
|
-
|
|
7498
|
-
|
|
7499
|
-
|
|
7500
|
-
|
|
7501
|
-
|
|
7502
|
-
|
|
7503
|
-
|
|
7504
|
-
}
|
|
7505
|
-
|
|
7506
|
-
|
|
7507
|
-
|
|
7508
|
-
|
|
7509
|
-
|
|
7510
|
-
|
|
7511
|
-
|
|
7512
|
-
|
|
7513
|
-
|
|
7514
|
-
|
|
7515
|
-
|
|
7516
|
-
|
|
7517
|
-
|
|
7518
|
-
|
|
7519
|
-
|
|
7520
|
-
|
|
7521
|
-
|
|
7522
|
-
|
|
7523
|
-
|
|
7524
|
-
|
|
7525
|
-
|
|
7526
|
-
|
|
8224
|
+
const statePath = listedConversation.state_path ??
|
|
8225
|
+
statePathForConversationId(listedConversation.conversation_id, storeDir);
|
|
8226
|
+
let releaseStateLock;
|
|
8227
|
+
try {
|
|
8228
|
+
releaseStateLock = acquireFileLock(`${statePath}.lock`);
|
|
8229
|
+
}
|
|
8230
|
+
catch (error) {
|
|
8231
|
+
if (isRecord(error) && error.code === "LOCK_TIMEOUT") {
|
|
8232
|
+
continue;
|
|
8233
|
+
}
|
|
8234
|
+
throw error;
|
|
8235
|
+
}
|
|
8236
|
+
try {
|
|
8237
|
+
const conversation = loadState(statePath);
|
|
8238
|
+
if (conversation.status !== "idle" || !conversation.idle_since) {
|
|
8239
|
+
continue;
|
|
8240
|
+
}
|
|
8241
|
+
const idleSinceMs = Date.parse(conversation.idle_since);
|
|
8242
|
+
if (!Number.isFinite(idleSinceMs)) {
|
|
8243
|
+
continue;
|
|
8244
|
+
}
|
|
8245
|
+
const terminalBridge = terminalBridgeEnabled(conversation) &&
|
|
8246
|
+
isRecord(conversation.native_session_takeover) &&
|
|
8247
|
+
typeof conversation.native_session_takeover.terminal_bridge_message_id === "string";
|
|
8248
|
+
if (!terminalBridge && now.getTime() - idleSinceMs < timeoutMinutes * 60 * 1000) {
|
|
8249
|
+
continue;
|
|
8250
|
+
}
|
|
8251
|
+
const logPath = conversation.event_log_path ?? logPathForStatePath(statePath);
|
|
8252
|
+
const closeReason = terminalBridge
|
|
8253
|
+
? "terminal bridge task completed"
|
|
8254
|
+
: `idle timeout after ${timeoutMinutes} minutes`;
|
|
8255
|
+
const closedConversation = {
|
|
8256
|
+
...conversation,
|
|
8257
|
+
status: "closed",
|
|
8258
|
+
closed_at: now.toISOString(),
|
|
8259
|
+
close_reason: closeReason,
|
|
8260
|
+
updated_at: now.toISOString()
|
|
8261
|
+
};
|
|
8262
|
+
delete closedConversation.idle_since;
|
|
8263
|
+
saveState(statePath, closedConversation);
|
|
8264
|
+
appendEvent(logPath, {
|
|
8265
|
+
ts: now.toISOString(),
|
|
8266
|
+
conversation_id: conversation.conversation_id,
|
|
8267
|
+
event: "conversation_closed",
|
|
8268
|
+
status: "closed",
|
|
8269
|
+
reason: closedConversation.close_reason,
|
|
8270
|
+
idle_timeout_minutes: timeoutMinutes,
|
|
8271
|
+
terminal_bridge: terminalBridge
|
|
8272
|
+
});
|
|
8273
|
+
runtimeLog("info", "idle_conversation_closed", {
|
|
8274
|
+
conversation_id: conversation.conversation_id,
|
|
8275
|
+
agent: executorForConversation(conversation).kind,
|
|
8276
|
+
executor_session: executorForConversation(conversation).session,
|
|
8277
|
+
state_path: statePath,
|
|
8278
|
+
event_log_path: logPath,
|
|
8279
|
+
idle_since: conversation.idle_since,
|
|
8280
|
+
idle_timeout_minutes: timeoutMinutes,
|
|
8281
|
+
reason: closedConversation.close_reason
|
|
8282
|
+
});
|
|
8283
|
+
closed += 1;
|
|
8284
|
+
}
|
|
8285
|
+
finally {
|
|
8286
|
+
releaseStateLock();
|
|
8287
|
+
}
|
|
7527
8288
|
}
|
|
7528
8289
|
return {
|
|
7529
8290
|
checked: conversations.length,
|
|
@@ -7558,6 +8319,8 @@ function deliverToOpenClaw({ gatewayUrl, token, openclawSession, message }) {
|
|
|
7558
8319
|
const result = spawnSync("acpx", ["--agent", agent, JSON.stringify(message)], {
|
|
7559
8320
|
encoding: "utf8",
|
|
7560
8321
|
maxBuffer: 1024 * 1024 * 10,
|
|
8322
|
+
timeout: CALLBACK_DELIVERY_TIMEOUT_MS,
|
|
8323
|
+
killSignal: "SIGKILL",
|
|
7561
8324
|
env: openClawGatewayEnvironment(token)
|
|
7562
8325
|
});
|
|
7563
8326
|
if (result.error) {
|
|
@@ -7594,6 +8357,8 @@ function deliverToGatewayMethod({ method, openclawBin, gatewayUrl, token, sessio
|
|
|
7594
8357
|
const result = spawnSync(openclawBin ?? "openclaw", args, {
|
|
7595
8358
|
encoding: "utf8",
|
|
7596
8359
|
maxBuffer: 1024 * 1024 * 10,
|
|
8360
|
+
timeout: CALLBACK_DELIVERY_TIMEOUT_MS,
|
|
8361
|
+
killSignal: "SIGKILL",
|
|
7597
8362
|
env: openClawGatewayEnvironment(token)
|
|
7598
8363
|
});
|
|
7599
8364
|
if (result.error) {
|
|
@@ -7624,6 +8389,8 @@ function deliverToSessionSend({ openclawBin, gatewayUrl, token, params }) {
|
|
|
7624
8389
|
const result = spawnSync(openclawBin ?? "openclaw", args, {
|
|
7625
8390
|
encoding: "utf8",
|
|
7626
8391
|
maxBuffer: 1024 * 1024 * 10,
|
|
8392
|
+
timeout: CALLBACK_DELIVERY_TIMEOUT_MS,
|
|
8393
|
+
killSignal: "SIGKILL",
|
|
7627
8394
|
env: openClawGatewayEnvironment(token)
|
|
7628
8395
|
});
|
|
7629
8396
|
if (result.error) {
|
|
@@ -7654,6 +8421,8 @@ function deliverToChatSend({ openclawBin, gatewayUrl, token, params }) {
|
|
|
7654
8421
|
const result = spawnSync(openclawBin ?? "openclaw", args, {
|
|
7655
8422
|
encoding: "utf8",
|
|
7656
8423
|
maxBuffer: 1024 * 1024 * 10,
|
|
8424
|
+
timeout: CALLBACK_DELIVERY_TIMEOUT_MS,
|
|
8425
|
+
killSignal: "SIGKILL",
|
|
7657
8426
|
env: openClawGatewayEnvironment(token)
|
|
7658
8427
|
});
|
|
7659
8428
|
if (result.error) {
|
|
@@ -7776,6 +8545,12 @@ function redactCliOutput(value) {
|
|
|
7776
8545
|
if (key === "gateway_token" || key === "gatewayToken") {
|
|
7777
8546
|
return [];
|
|
7778
8547
|
}
|
|
8548
|
+
if (key === "claude_transcript_anchor" ||
|
|
8549
|
+
key === "claudeTranscriptAnchor" ||
|
|
8550
|
+
key === "claude_home" ||
|
|
8551
|
+
key === "claudeHome") {
|
|
8552
|
+
return [];
|
|
8553
|
+
}
|
|
7779
8554
|
if ((key === "callback_command" || key === "callbackCommand") && typeof item === "string") {
|
|
7780
8555
|
return [[key, redactString(item)]];
|
|
7781
8556
|
}
|
|
@@ -7957,7 +8732,6 @@ function usage() {
|
|
|
7957
8732
|
agent-knock-knock recover --conversation <id> [--session <name>] [--all-proxy <url>]
|
|
7958
8733
|
agent-knock-knock close --conversation <id> [--reason <text>]
|
|
7959
8734
|
agent-knock-knock install-openclaw [--openclaw-bin <path>] [--skill-path <path>] [--skill-only] [--no-restart]
|
|
7960
|
-
agent-knock-knock install-claude-hooks [--settings-path <path>] [--executable-path <path>] [--dry-run]
|
|
7961
8735
|
agent-knock-knock doctor
|
|
7962
8736
|
agent-knock-knock agent takeover --agent codex --session-id <id> --strategy terminate_then_resume|terminal_control|fork [--create-conversation]
|
|
7963
8737
|
agent-knock-knock callback --state <file> --message-json <json> [--record-only]
|