@scotthuang/agent-knock-knock 0.2.46 → 0.2.47
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 +12 -0
- package/README.md +25 -10
- package/dist/src/approval-policy.d.ts +1 -0
- package/dist/src/approval-policy.js +6 -0
- package/dist/src/approval-policy.js.map +1 -1
- package/dist/src/claude-hook-installer.d.ts +65 -0
- package/dist/src/claude-hook-installer.js +410 -0
- package/dist/src/claude-hook-installer.js.map +1 -0
- package/dist/src/claude-hook-protocol.d.ts +89 -0
- package/dist/src/claude-hook-protocol.js +243 -0
- package/dist/src/claude-hook-protocol.js.map +1 -0
- package/dist/src/claude-hook-store.d.ts +247 -0
- package/dist/src/claude-hook-store.js +1064 -0
- package/dist/src/claude-hook-store.js.map +1 -0
- package/dist/src/claude-terminal-agent-adapter.d.ts +47 -0
- package/dist/src/claude-terminal-agent-adapter.js +920 -0
- package/dist/src/claude-terminal-agent-adapter.js.map +1 -0
- package/dist/src/cli.js +987 -103
- package/dist/src/cli.js.map +1 -1
- package/dist/src/openclaw-plugin.js +6 -6
- package/dist/src/openclaw-plugin.js.map +1 -1
- package/dist/src/terminal-agent-adapter.d.ts +34 -0
- package/dist/src/terminal-agent-adapter.js.map +1 -1
- package/dist/src/terminal-agent-bridge.d.ts +19 -2
- package/dist/src/terminal-agent-bridge.js +140 -15
- package/dist/src/terminal-agent-bridge.js.map +1 -1
- package/dist/src/terminal-agent-registry.js +3 -1
- package/dist/src/terminal-agent-registry.js.map +1 -1
- package/openclaw.plugin.json +3 -3
- package/package.json +1 -1
- package/templates/openclaw-skills/agent-knock-knock/SKILL.md +16 -13
package/dist/src/cli.js
CHANGED
|
@@ -6,6 +6,10 @@ import path from "node:path";
|
|
|
6
6
|
import process from "node:process";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
8
|
import { createCodexTerminalAgentAdapter, detectCodexDurableCompletion } from "./codex-terminal-agent-adapter.js";
|
|
9
|
+
import { createClaudeTerminalAgentAdapter } from "./claude-terminal-agent-adapter.js";
|
|
10
|
+
import { ClaudeHookStore, ClaudeHookStoreError, defaultClaudeHookStoreDir } from "./claude-hook-store.js";
|
|
11
|
+
import { claudePermissionHookOutput } from "./claude-hook-protocol.js";
|
|
12
|
+
import { defaultClaudeSettingsPath, installClaudeHooks, loadTrustedClaudeTokenjuiceLaunchers } from "./claude-hook-installer.js";
|
|
9
13
|
import { CodexLocalSessionProvider } from "./codex-local-session-provider.js";
|
|
10
14
|
import { CodexStoreAdapter } from "./codex-store-adapter.js";
|
|
11
15
|
import { applyMessageToConversation, budgetAction, createConversation, createMessage, executorForConversation, extractStructuredMessage, parseMessageJson, resolveExecutor } from "./protocol.js";
|
|
@@ -26,6 +30,22 @@ const DEFAULT_AGENT_HARD_TIMEOUT_MINUTES = 720;
|
|
|
26
30
|
const DEFAULT_MONITOR_POLL_INTERVAL_MS = 5000;
|
|
27
31
|
const CALLBACK_RETRY_DELAYS_MS = [5000, 15000, 60000, 60000];
|
|
28
32
|
const DEFAULT_CODEX_ACPX_AGENT_COMMAND = "npx -y @agentclientprotocol/codex-acp@^1.1.0";
|
|
33
|
+
const CONVERSATION_STATUSES = new Set([
|
|
34
|
+
"created",
|
|
35
|
+
"running",
|
|
36
|
+
"waiting_for_agent",
|
|
37
|
+
"waiting_for_openclaw",
|
|
38
|
+
"idle",
|
|
39
|
+
"stalled",
|
|
40
|
+
"needs_recovery",
|
|
41
|
+
"needs_model_selection",
|
|
42
|
+
"callback_pending",
|
|
43
|
+
"callback_failed",
|
|
44
|
+
"failed",
|
|
45
|
+
"closed",
|
|
46
|
+
"cancelled",
|
|
47
|
+
"cancelling"
|
|
48
|
+
]);
|
|
29
49
|
class InlineCodexSessionAdapter {
|
|
30
50
|
threads;
|
|
31
51
|
processes;
|
|
@@ -127,6 +147,12 @@ async function runCommand(commandName, options) {
|
|
|
127
147
|
else if (commandName === "install-openclaw") {
|
|
128
148
|
runInstallOpenClaw(options);
|
|
129
149
|
}
|
|
150
|
+
else if (commandName === "install-claude-hooks") {
|
|
151
|
+
runInstallClaudeHooks(options);
|
|
152
|
+
}
|
|
153
|
+
else if (commandName === "claude-hook") {
|
|
154
|
+
await runClaudeHook(options);
|
|
155
|
+
}
|
|
130
156
|
else if (commandName === "doctor") {
|
|
131
157
|
runDoctor(options);
|
|
132
158
|
}
|
|
@@ -197,6 +223,88 @@ function runInstallOpenClaw(options) {
|
|
|
197
223
|
: "Agent Knock Knock is installed. Try: AKK list"
|
|
198
224
|
});
|
|
199
225
|
}
|
|
226
|
+
function runInstallClaudeHooks(options) {
|
|
227
|
+
const configuredExecutable = stringValue(options.executablePath ?? options.binPath);
|
|
228
|
+
let executablePath;
|
|
229
|
+
if (configuredExecutable) {
|
|
230
|
+
executablePath = path.resolve(expandHome(configuredExecutable));
|
|
231
|
+
}
|
|
232
|
+
else {
|
|
233
|
+
try {
|
|
234
|
+
executablePath = resolveExecutable("agent-knock-knock");
|
|
235
|
+
}
|
|
236
|
+
catch {
|
|
237
|
+
executablePath = path.resolve(process.argv[1]);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
const result = installClaudeHooks({
|
|
241
|
+
executablePath,
|
|
242
|
+
settingsPath: expandHome(options.settingsPath ?? options.settings),
|
|
243
|
+
dryRun: options.dryRun === true
|
|
244
|
+
});
|
|
245
|
+
printJson({
|
|
246
|
+
installed: result.written || !result.changed,
|
|
247
|
+
...result,
|
|
248
|
+
executablePath,
|
|
249
|
+
next: result.dryRun
|
|
250
|
+
? "Run again without --dry-run to install the Claude Code hooks."
|
|
251
|
+
: "Claude Code hooks are configured. Existing sessions pick them up through settings reload; start a new session if needed."
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
async function runClaudeHook(options) {
|
|
255
|
+
const rawInput = fs.readFileSync(0, "utf8");
|
|
256
|
+
let input;
|
|
257
|
+
try {
|
|
258
|
+
input = JSON.parse(rawInput);
|
|
259
|
+
}
|
|
260
|
+
catch {
|
|
261
|
+
throw new Error("Claude hook input must be valid JSON");
|
|
262
|
+
}
|
|
263
|
+
const agentRows = loadClaudeAgentRows(options);
|
|
264
|
+
const claudePid = inferClaudeAncestorPid(agentRows);
|
|
265
|
+
const store = createClaudeHookStore(options);
|
|
266
|
+
const record = store.record(input, {
|
|
267
|
+
...(claudePid === undefined ? {} : { claudePid })
|
|
268
|
+
});
|
|
269
|
+
if (record.event.input.hook_event_name !== "PermissionRequest" || !record.permission) {
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
const permission = record.permission;
|
|
273
|
+
const requestedTimeout = options.permissionWaitTimeoutMs ?? options.timeoutMs;
|
|
274
|
+
const timeoutMs = requestedTimeout === undefined
|
|
275
|
+
? undefined
|
|
276
|
+
: Math.max(0, Number(requestedTimeout));
|
|
277
|
+
try {
|
|
278
|
+
const decision = await store.waitForPermissionDecision({
|
|
279
|
+
sessionId: permission.sessionId,
|
|
280
|
+
requestId: permission.requestId,
|
|
281
|
+
fingerprint: permission.fingerprint,
|
|
282
|
+
conversationId: permission.conversationId,
|
|
283
|
+
messageId: permission.messageId,
|
|
284
|
+
...(timeoutMs === undefined ? {} : { timeoutMs })
|
|
285
|
+
});
|
|
286
|
+
process.stdout.write(`${JSON.stringify(decision?.hookOutput ?? claudePermissionHookOutput({
|
|
287
|
+
behavior: "deny",
|
|
288
|
+
interrupt: false,
|
|
289
|
+
message: "Agent Knock Knock approval timed out. Review the request and retry the task."
|
|
290
|
+
}))}\n`);
|
|
291
|
+
}
|
|
292
|
+
catch (error) {
|
|
293
|
+
if (error instanceof ClaudeHookStoreError && [
|
|
294
|
+
"PERMISSION_EXPIRED",
|
|
295
|
+
"PERMISSION_CONSUMED",
|
|
296
|
+
"PERMISSION_ALREADY_DECIDED"
|
|
297
|
+
].includes(error.code)) {
|
|
298
|
+
process.stdout.write(`${JSON.stringify(claudePermissionHookOutput({
|
|
299
|
+
behavior: "deny",
|
|
300
|
+
interrupt: false,
|
|
301
|
+
message: "Agent Knock Knock approval expired before a safe decision was received."
|
|
302
|
+
}))}\n`);
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
throw error;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
200
308
|
function installOpenClawPlugin(openclawBin, root) {
|
|
201
309
|
const linked = spawnSync(openclawBin, ["plugins", "install", "--link", root], {
|
|
202
310
|
encoding: "utf8",
|
|
@@ -575,9 +683,107 @@ function createTerminalProcessSource(options) {
|
|
|
575
683
|
}
|
|
576
684
|
return new SystemTerminalProcessSource();
|
|
577
685
|
}
|
|
686
|
+
function createClaudeHookStore(options = {}) {
|
|
687
|
+
const configuredRoot = stringValue(options.claudeHookStoreDir);
|
|
688
|
+
return new ClaudeHookStore({
|
|
689
|
+
rootDir: expandHome(configuredRoot ?? defaultClaudeHookStoreDir())
|
|
690
|
+
});
|
|
691
|
+
}
|
|
692
|
+
function loadClaudeAgentRows(options = {}) {
|
|
693
|
+
let value;
|
|
694
|
+
if (options.claudeAgentsJson !== undefined) {
|
|
695
|
+
value = typeof options.claudeAgentsJson === "string"
|
|
696
|
+
? parseJsonOption(options.claudeAgentsJson, "--claude-agents-json")
|
|
697
|
+
: options.claudeAgentsJson;
|
|
698
|
+
}
|
|
699
|
+
else if (options.processesJson || options.terminalsJson || options.terminalScreensJson) {
|
|
700
|
+
return [];
|
|
701
|
+
}
|
|
702
|
+
else {
|
|
703
|
+
const claudeExecutable = resolveOptionalExecutable("claude");
|
|
704
|
+
if (!claudeExecutable) {
|
|
705
|
+
return [];
|
|
706
|
+
}
|
|
707
|
+
const result = spawnSync(claudeExecutable, ["agents", "--json", "--all"], {
|
|
708
|
+
encoding: "utf8",
|
|
709
|
+
maxBuffer: 1024 * 1024 * 10,
|
|
710
|
+
timeout: 10_000
|
|
711
|
+
});
|
|
712
|
+
if (result.error || result.status !== 0) {
|
|
713
|
+
runtimeLog("warn", "claude_agents_list_failed", {
|
|
714
|
+
status: result.status ?? null,
|
|
715
|
+
error: result.error?.message,
|
|
716
|
+
stderr: textSummary(cleanProcessText(result.stderr))
|
|
717
|
+
});
|
|
718
|
+
return [];
|
|
719
|
+
}
|
|
720
|
+
try {
|
|
721
|
+
value = JSON.parse(result.stdout);
|
|
722
|
+
}
|
|
723
|
+
catch {
|
|
724
|
+
runtimeLog("warn", "claude_agents_list_invalid_json", {
|
|
725
|
+
stdout: textSummary(result.stdout)
|
|
726
|
+
});
|
|
727
|
+
return [];
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
const rows = Array.isArray(value)
|
|
731
|
+
? value
|
|
732
|
+
: isRecord(value) && Array.isArray(value.agents)
|
|
733
|
+
? value.agents
|
|
734
|
+
: [];
|
|
735
|
+
return rows.flatMap((row) => {
|
|
736
|
+
if (!isRecord(row) || !Number.isInteger(Number(row.pid))) {
|
|
737
|
+
return [];
|
|
738
|
+
}
|
|
739
|
+
return [{
|
|
740
|
+
pid: Number(row.pid),
|
|
741
|
+
...(stringValue(row.cwd) ? { cwd: stringValue(row.cwd) } : {}),
|
|
742
|
+
...(stringValue(row.kind) ? { kind: stringValue(row.kind) } : {}),
|
|
743
|
+
...(stringValue(row.sessionId) ? { sessionId: stringValue(row.sessionId) } : {}),
|
|
744
|
+
...(stringValue(row.status) ? { status: stringValue(row.status) } : {})
|
|
745
|
+
}];
|
|
746
|
+
});
|
|
747
|
+
}
|
|
748
|
+
function inferClaudeAncestorPid(agentRows, startingPid = process.ppid) {
|
|
749
|
+
const interactivePids = new Set(agentRows
|
|
750
|
+
.filter((row) => row.kind === undefined || row.kind === "interactive")
|
|
751
|
+
.map((row) => row.pid)
|
|
752
|
+
.filter((pid) => Number.isInteger(pid)));
|
|
753
|
+
let pid = startingPid;
|
|
754
|
+
const visited = new Set();
|
|
755
|
+
for (let depth = 0; depth < 16 && Number.isInteger(pid) && pid > 1 && !visited.has(pid); depth += 1) {
|
|
756
|
+
visited.add(pid);
|
|
757
|
+
const processRow = spawnSync("ps", ["-p", String(pid), "-o", "pid=,ppid=,command="], {
|
|
758
|
+
encoding: "utf8",
|
|
759
|
+
timeout: 2_000
|
|
760
|
+
});
|
|
761
|
+
if (processRow.error || processRow.status !== 0) {
|
|
762
|
+
return undefined;
|
|
763
|
+
}
|
|
764
|
+
const match = /^\s*(\d+)\s+(\d+)\s+(.+)$/u.exec(String(processRow.stdout).trim());
|
|
765
|
+
if (!match) {
|
|
766
|
+
return undefined;
|
|
767
|
+
}
|
|
768
|
+
const currentPid = Number(match[1]);
|
|
769
|
+
const parentPid = Number(match[2]);
|
|
770
|
+
const commandText = match[3];
|
|
771
|
+
if (interactivePids.has(currentPid) || isClaudeProcessCommand(commandText)) {
|
|
772
|
+
return currentPid;
|
|
773
|
+
}
|
|
774
|
+
pid = parentPid;
|
|
775
|
+
}
|
|
776
|
+
return undefined;
|
|
777
|
+
}
|
|
778
|
+
function isClaudeProcessCommand(commandText) {
|
|
779
|
+
const executable = commandText.trim().split(/\s+/u, 1)[0];
|
|
780
|
+
return path.basename(executable) === "claude" ||
|
|
781
|
+
/[\\/]\.local[\\/]share[\\/]claude[\\/]versions[\\/][^\\/\s]+$/u.test(executable);
|
|
782
|
+
}
|
|
578
783
|
function createRuntimeTerminalAgentRegistry(options) {
|
|
579
784
|
return createProductionTerminalAgentRegistry({
|
|
580
|
-
overrides: [
|
|
785
|
+
overrides: [
|
|
786
|
+
createCodexTerminalAgentAdapter({
|
|
581
787
|
async detectDurableCompletion(request) {
|
|
582
788
|
const runtime = isRecord(request.context) ? request.context : undefined;
|
|
583
789
|
const conversation = runtime?.conversation;
|
|
@@ -611,7 +817,13 @@ function createRuntimeTerminalAgentRegistry(options) {
|
|
|
611
817
|
}
|
|
612
818
|
: undefined;
|
|
613
819
|
}
|
|
614
|
-
})
|
|
820
|
+
}),
|
|
821
|
+
createClaudeTerminalAgentAdapter({
|
|
822
|
+
agentRows: loadClaudeAgentRows(options),
|
|
823
|
+
hookStore: createClaudeHookStore(options),
|
|
824
|
+
trustedTokenjuiceLaunchers: loadTrustedClaudeTokenjuiceLaunchers(expandHome(options.claudeSettingsPath ?? defaultClaudeSettingsPath()))
|
|
825
|
+
})
|
|
826
|
+
]
|
|
615
827
|
});
|
|
616
828
|
}
|
|
617
829
|
function createTerminalAgentBridge(options, terminalProvider = createTerminalControlProvider(options)) {
|
|
@@ -695,6 +907,25 @@ function terminalControlFromTakeover(nativeTakeover) {
|
|
|
695
907
|
]
|
|
696
908
|
};
|
|
697
909
|
}
|
|
910
|
+
function terminalRuntimeIdentityForConversation(conversation, terminalControl) {
|
|
911
|
+
const nativeTakeover = isRecord(conversation?.native_session_takeover)
|
|
912
|
+
? conversation.native_session_takeover
|
|
913
|
+
: undefined;
|
|
914
|
+
const nativeSessionId = stringValue(nativeTakeover?.native_session_id);
|
|
915
|
+
const terminalIdentity = parseTerminalConversationId(nativeSessionId);
|
|
916
|
+
const explicitSessionId = stringValue(nativeTakeover?.terminal_agent_session_id) ??
|
|
917
|
+
(terminalIdentity ? undefined : nativeSessionId);
|
|
918
|
+
return {
|
|
919
|
+
pid: Number.isInteger(Number(nativeTakeover?.terminal_agent_pid))
|
|
920
|
+
? Number(nativeTakeover?.terminal_agent_pid)
|
|
921
|
+
: terminalIdentity?.pid,
|
|
922
|
+
sessionId: explicitSessionId,
|
|
923
|
+
cwd: stringValue(nativeTakeover?.source_cwd) ?? terminalControl.currentPath,
|
|
924
|
+
conversationId: stringValue(conversation?.conversation_id),
|
|
925
|
+
messageId: stringValue(nativeTakeover?.terminal_bridge_message_id),
|
|
926
|
+
terminalTarget: terminalControl.target
|
|
927
|
+
};
|
|
928
|
+
}
|
|
698
929
|
function isTerminalControlCapability(value) {
|
|
699
930
|
return typeof value === "string" && [
|
|
700
931
|
"screen_status",
|
|
@@ -1246,7 +1477,7 @@ function startExecutorMonitor({ statePath, logPath, pid, outputPath, agentTimeou
|
|
|
1246
1477
|
child.unref();
|
|
1247
1478
|
return child;
|
|
1248
1479
|
}
|
|
1249
|
-
function startTerminalBridgeMonitor({ statePath, logPath, agentTimeoutMinutes, agentHardTimeoutMinutes, pollIntervalMs, codexHome }) {
|
|
1480
|
+
function startTerminalBridgeMonitor({ statePath, logPath, agentTimeoutMinutes, agentHardTimeoutMinutes, pollIntervalMs, codexHome, claudeHookStoreDir }) {
|
|
1250
1481
|
const args = [
|
|
1251
1482
|
new URL(import.meta.url).pathname,
|
|
1252
1483
|
"monitor",
|
|
@@ -1265,6 +1496,9 @@ function startTerminalBridgeMonitor({ statePath, logPath, agentTimeoutMinutes, a
|
|
|
1265
1496
|
if (codexHome) {
|
|
1266
1497
|
args.push("--codex-home", codexHome);
|
|
1267
1498
|
}
|
|
1499
|
+
if (claudeHookStoreDir) {
|
|
1500
|
+
args.push("--claude-hook-store-dir", claudeHookStoreDir);
|
|
1501
|
+
}
|
|
1268
1502
|
const child = spawn(process.execPath, args, {
|
|
1269
1503
|
detached: true,
|
|
1270
1504
|
stdio: "ignore",
|
|
@@ -1291,7 +1525,8 @@ function startTerminalBridgeMonitorForConversation({ conversation, statePath, lo
|
|
|
1291
1525
|
nativeTakeover?.["terminal_bridge_hard_timeout_minutes"] ??
|
|
1292
1526
|
DEFAULT_AGENT_HARD_TIMEOUT_MINUTES),
|
|
1293
1527
|
pollIntervalMs: Number(options.monitorPollIntervalMs ?? DEFAULT_MONITOR_POLL_INTERVAL_MS),
|
|
1294
|
-
codexHome: options.codexHome
|
|
1528
|
+
codexHome: options.codexHome,
|
|
1529
|
+
claudeHookStoreDir: options.claudeHookStoreDir ?? nativeTakeover?.["claude_hook_store_dir"]
|
|
1295
1530
|
});
|
|
1296
1531
|
}
|
|
1297
1532
|
function terminalBridgeEnabled(conversation) {
|
|
@@ -1314,6 +1549,7 @@ function withTerminalBridgeState({ conversation, message, startedAt, agentTimeou
|
|
|
1314
1549
|
terminal_bridge_request_text: String(message.body ?? ""),
|
|
1315
1550
|
terminal_bridge_request_hash: terminalBridgeRequestFingerprint(message.body),
|
|
1316
1551
|
terminal_bridge_pre_send_screen_fingerprint: preSendScreenFingerprint,
|
|
1552
|
+
terminal_bridge_completion_claim: undefined,
|
|
1317
1553
|
terminal_bridge_monitor_started_at: startedAt,
|
|
1318
1554
|
terminal_bridge_last_activity_at: startedAt,
|
|
1319
1555
|
terminal_bridge_inactivity_timeout_minutes: agentTimeoutMinutes,
|
|
@@ -1324,6 +1560,165 @@ function withTerminalBridgeState({ conversation, message, startedAt, agentTimeou
|
|
|
1324
1560
|
updated_at: startedAt
|
|
1325
1561
|
};
|
|
1326
1562
|
}
|
|
1563
|
+
function activateClaudeHookLease({ options, conversation, message, terminalControl, expiresAt }) {
|
|
1564
|
+
const runtime = terminalRuntimeIdentityForConversation(conversation, terminalControl);
|
|
1565
|
+
const pid = runtime.pid;
|
|
1566
|
+
const agentRow = pid === undefined
|
|
1567
|
+
? undefined
|
|
1568
|
+
: loadClaudeAgentRows(options).find((row) => row.pid === pid && (row.kind === undefined || row.kind === "interactive"));
|
|
1569
|
+
const sessionId = agentRow?.sessionId ?? runtime.sessionId;
|
|
1570
|
+
const cwd = agentRow?.cwd ?? runtime.cwd ?? terminalControl.currentPath;
|
|
1571
|
+
if (!cwd || (pid === undefined && !sessionId)) {
|
|
1572
|
+
runtimeLog("warn", "claude_hook_lease_unavailable", {
|
|
1573
|
+
conversation_id: conversation.conversation_id,
|
|
1574
|
+
terminal_target: terminalControl.target,
|
|
1575
|
+
reason: "exact Claude pid/session identity is unavailable"
|
|
1576
|
+
});
|
|
1577
|
+
return undefined;
|
|
1578
|
+
}
|
|
1579
|
+
const store = createClaudeHookStore(options);
|
|
1580
|
+
try {
|
|
1581
|
+
const previous = store.resolveLease({
|
|
1582
|
+
...(sessionId === undefined ? {} : { sessionId }),
|
|
1583
|
+
...(pid === undefined ? {} : { pid }),
|
|
1584
|
+
cwd,
|
|
1585
|
+
requireUnique: true
|
|
1586
|
+
});
|
|
1587
|
+
if (previous && (previous.lease.conversationId !== conversation.conversation_id ||
|
|
1588
|
+
previous.lease.messageId !== message.id)) {
|
|
1589
|
+
store.releaseLease({ leaseId: previous.lease.id });
|
|
1590
|
+
}
|
|
1591
|
+
const lease = store.activateLease({
|
|
1592
|
+
...(sessionId === undefined ? {} : { sessionId }),
|
|
1593
|
+
...(pid === undefined ? {} : { pid }),
|
|
1594
|
+
cwd,
|
|
1595
|
+
conversationId: conversation.conversation_id,
|
|
1596
|
+
messageId: message.id,
|
|
1597
|
+
terminalTarget: terminalControl.target,
|
|
1598
|
+
expiresAt
|
|
1599
|
+
});
|
|
1600
|
+
runtimeLog("info", "claude_hook_lease_activated", {
|
|
1601
|
+
conversation_id: conversation.conversation_id,
|
|
1602
|
+
message_id: message.id,
|
|
1603
|
+
terminal_target: terminalControl.target,
|
|
1604
|
+
lease_id: lease.id,
|
|
1605
|
+
pid,
|
|
1606
|
+
session_id: sessionId,
|
|
1607
|
+
expires_at: lease.expiresAt
|
|
1608
|
+
});
|
|
1609
|
+
return {
|
|
1610
|
+
lease,
|
|
1611
|
+
storeDir: store.rootDir,
|
|
1612
|
+
...(pid === undefined ? {} : { pid }),
|
|
1613
|
+
...(sessionId === undefined ? {} : { sessionId })
|
|
1614
|
+
};
|
|
1615
|
+
}
|
|
1616
|
+
catch (error) {
|
|
1617
|
+
runtimeLog("warn", "claude_hook_lease_unavailable", {
|
|
1618
|
+
conversation_id: conversation.conversation_id,
|
|
1619
|
+
terminal_target: terminalControl.target,
|
|
1620
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
1621
|
+
});
|
|
1622
|
+
return undefined;
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
function withClaudeHookLeaseState(conversation, state) {
|
|
1626
|
+
const nativeTakeover = isRecord(conversation.native_session_takeover)
|
|
1627
|
+
? conversation.native_session_takeover
|
|
1628
|
+
: {};
|
|
1629
|
+
return {
|
|
1630
|
+
...conversation,
|
|
1631
|
+
native_session_takeover: {
|
|
1632
|
+
...nativeTakeover,
|
|
1633
|
+
claude_hook_mode: "enabled",
|
|
1634
|
+
claude_hook_store_dir: state.storeDir,
|
|
1635
|
+
claude_hook_lease_id: state.lease.id,
|
|
1636
|
+
terminal_agent_pid: state.pid,
|
|
1637
|
+
terminal_agent_session_id: state.sessionId
|
|
1638
|
+
}
|
|
1639
|
+
};
|
|
1640
|
+
}
|
|
1641
|
+
function releaseClaudeHookLease(conversation) {
|
|
1642
|
+
const nativeTakeover = isRecord(conversation?.native_session_takeover)
|
|
1643
|
+
? conversation.native_session_takeover
|
|
1644
|
+
: undefined;
|
|
1645
|
+
const leaseId = stringValue(nativeTakeover?.claude_hook_lease_id);
|
|
1646
|
+
if (!leaseId || nativeTakeover?.agent !== "claude") {
|
|
1647
|
+
return;
|
|
1648
|
+
}
|
|
1649
|
+
const storeDir = stringValue(nativeTakeover.claude_hook_store_dir) ?? defaultClaudeHookStoreDir();
|
|
1650
|
+
try {
|
|
1651
|
+
const released = new ClaudeHookStore({ rootDir: storeDir }).releaseLease({ leaseId });
|
|
1652
|
+
runtimeLog("info", "claude_hook_lease_released", {
|
|
1653
|
+
conversation_id: conversation.conversation_id,
|
|
1654
|
+
message_id: released?.messageId,
|
|
1655
|
+
lease_id: leaseId
|
|
1656
|
+
});
|
|
1657
|
+
}
|
|
1658
|
+
catch (error) {
|
|
1659
|
+
runtimeLog("warn", "claude_hook_lease_release_failed", {
|
|
1660
|
+
conversation_id: conversation.conversation_id,
|
|
1661
|
+
lease_id: leaseId,
|
|
1662
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
1663
|
+
});
|
|
1664
|
+
}
|
|
1665
|
+
}
|
|
1666
|
+
function renewClaudeHookLease(conversation, expiresAt) {
|
|
1667
|
+
const nativeTakeover = isRecord(conversation?.native_session_takeover)
|
|
1668
|
+
? conversation.native_session_takeover
|
|
1669
|
+
: undefined;
|
|
1670
|
+
if (nativeTakeover?.agent !== "claude") {
|
|
1671
|
+
return undefined;
|
|
1672
|
+
}
|
|
1673
|
+
const conversationId = stringValue(conversation.conversation_id);
|
|
1674
|
+
const messageId = stringValue(nativeTakeover.terminal_bridge_message_id);
|
|
1675
|
+
const terminalControl = terminalControlFromTakeover(nativeTakeover);
|
|
1676
|
+
const cwd = stringValue(nativeTakeover.source_cwd) ?? terminalControl?.currentPath;
|
|
1677
|
+
const pid = Number.isInteger(Number(nativeTakeover.terminal_agent_pid))
|
|
1678
|
+
? Number(nativeTakeover.terminal_agent_pid)
|
|
1679
|
+
: undefined;
|
|
1680
|
+
const sessionId = stringValue(nativeTakeover.terminal_agent_session_id);
|
|
1681
|
+
if (!conversationId || !messageId || !terminalControl || !cwd || (pid === undefined && !sessionId)) {
|
|
1682
|
+
return undefined;
|
|
1683
|
+
}
|
|
1684
|
+
const storeDir = stringValue(nativeTakeover.claude_hook_store_dir) ?? defaultClaudeHookStoreDir();
|
|
1685
|
+
try {
|
|
1686
|
+
return new ClaudeHookStore({ rootDir: storeDir }).activateLease({
|
|
1687
|
+
...(sessionId === undefined ? {} : { sessionId }),
|
|
1688
|
+
...(pid === undefined ? {} : { pid }),
|
|
1689
|
+
cwd,
|
|
1690
|
+
conversationId,
|
|
1691
|
+
messageId,
|
|
1692
|
+
terminalTarget: terminalControl.target,
|
|
1693
|
+
expiresAt
|
|
1694
|
+
});
|
|
1695
|
+
}
|
|
1696
|
+
catch (error) {
|
|
1697
|
+
runtimeLog("warn", "claude_hook_lease_renew_failed", {
|
|
1698
|
+
conversation_id: conversationId,
|
|
1699
|
+
message_id: messageId,
|
|
1700
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
1701
|
+
});
|
|
1702
|
+
return undefined;
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
function releaseClaudeHookLeasesForTerminal({ storeDir, terminalControl, replacementConversationId }) {
|
|
1706
|
+
for (const candidate of listConversations(storeDir)) {
|
|
1707
|
+
if (candidate.conversation_id === replacementConversationId) {
|
|
1708
|
+
continue;
|
|
1709
|
+
}
|
|
1710
|
+
const nativeTakeover = isRecord(candidate.native_session_takeover)
|
|
1711
|
+
? candidate.native_session_takeover
|
|
1712
|
+
: undefined;
|
|
1713
|
+
const candidateControl = terminalControlFromTakeover(nativeTakeover);
|
|
1714
|
+
if (nativeTakeover?.agent === "claude" &&
|
|
1715
|
+
nativeTakeover?.terminal_bridge === true &&
|
|
1716
|
+
candidateControl?.target === terminalControl.target &&
|
|
1717
|
+
candidateControl?.socketPath === terminalControl.socketPath) {
|
|
1718
|
+
releaseClaudeHookLease(candidate);
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1327
1722
|
function uniqueDelegateSessionName(kind) {
|
|
1328
1723
|
const { sessionPrefix } = executorDefinitionForKind(kind || "claude");
|
|
1329
1724
|
const timestamp = new Date().toISOString().replace(/\D/g, "").slice(0, 14);
|
|
@@ -1554,7 +1949,12 @@ async function terminalControlledListEntry(session, activeSessions, options, bri
|
|
|
1554
1949
|
if (!terminalControl) {
|
|
1555
1950
|
throw new Error(`process ${session.pid} is not terminal-controlled`);
|
|
1556
1951
|
}
|
|
1557
|
-
const terminalState = await listStateForTerminal(session.agent, terminalControl, options, bridge
|
|
1952
|
+
const terminalState = await listStateForTerminal(session.agent, terminalControl, options, bridge, {
|
|
1953
|
+
pid: session.pid,
|
|
1954
|
+
sessionId: session.sessionId,
|
|
1955
|
+
cwd: session.cwd,
|
|
1956
|
+
terminalTarget: terminalControl.target
|
|
1957
|
+
});
|
|
1558
1958
|
return {
|
|
1559
1959
|
id: bridge.terminalConversationId(session),
|
|
1560
1960
|
source: "terminal_control",
|
|
@@ -1583,7 +1983,7 @@ async function terminalControlledListEntry(session, activeSessions, options, bri
|
|
|
1583
1983
|
}
|
|
1584
1984
|
};
|
|
1585
1985
|
}
|
|
1586
|
-
async function listStateForTerminal(agent, terminalControl, options, bridge = createTerminalAgentBridge(options)) {
|
|
1986
|
+
async function listStateForTerminal(agent, terminalControl, options, bridge = createTerminalAgentBridge(options), runtime) {
|
|
1587
1987
|
if (options.noApprovalScan) {
|
|
1588
1988
|
return {
|
|
1589
1989
|
approval_state: {
|
|
@@ -1598,7 +1998,8 @@ async function listStateForTerminal(agent, terminalControl, options, bridge = cr
|
|
|
1598
1998
|
}
|
|
1599
1999
|
try {
|
|
1600
2000
|
const status = await bridge.status(agent, terminalControl, {
|
|
1601
|
-
scrollbackLines: Number(options.scrollbackLines ?? 120)
|
|
2001
|
+
scrollbackLines: Number(options.scrollbackLines ?? 120),
|
|
2002
|
+
runtime
|
|
1602
2003
|
});
|
|
1603
2004
|
return {
|
|
1604
2005
|
approval_state: {
|
|
@@ -1671,7 +2072,11 @@ async function runStatus(options) {
|
|
|
1671
2072
|
cleanupIdleConversations(storeDirFromOptions(options), options);
|
|
1672
2073
|
const terminalConversation = await resolveTerminalConversationFromOptions(options);
|
|
1673
2074
|
if (terminalConversation) {
|
|
1674
|
-
const terminalStatus = await terminalStatusForControl(terminalConversation.agent, terminalConversation.terminalControl, options
|
|
2075
|
+
const terminalStatus = await terminalStatusForControl(terminalConversation.agent, terminalConversation.terminalControl, options, {
|
|
2076
|
+
pid: terminalConversation.pid,
|
|
2077
|
+
cwd: terminalConversation.terminalControl.currentPath,
|
|
2078
|
+
terminalTarget: terminalConversation.terminalControl.target
|
|
2079
|
+
});
|
|
1675
2080
|
printJson({
|
|
1676
2081
|
conversation_id: terminalConversation.conversationId,
|
|
1677
2082
|
source: "terminal_control",
|
|
@@ -1703,7 +2108,7 @@ async function runStatus(options) {
|
|
|
1703
2108
|
if (terminalControl) {
|
|
1704
2109
|
const executor = executorForConversation(conversation);
|
|
1705
2110
|
result.terminal_control = terminalControl;
|
|
1706
|
-
result.terminal_status = await terminalStatusForControl(executor.kind, terminalControl, options);
|
|
2111
|
+
result.terminal_status = await terminalStatusForControl(executor.kind, terminalControl, options, terminalRuntimeIdentityForConversation(conversation, terminalControl));
|
|
1707
2112
|
result.terminal_screen = result.terminal_status.screen;
|
|
1708
2113
|
}
|
|
1709
2114
|
printJson(result);
|
|
@@ -1721,7 +2126,11 @@ async function runDescribe(options) {
|
|
|
1721
2126
|
const conversationId = required(options.conversation ?? options.conversationId, "--conversation is required");
|
|
1722
2127
|
const terminalConversation = await resolveTerminalConversationFromOptions(options);
|
|
1723
2128
|
if (terminalConversation) {
|
|
1724
|
-
const terminalStatus = await terminalStatusForControl(terminalConversation.agent, terminalConversation.terminalControl, options
|
|
2129
|
+
const terminalStatus = await terminalStatusForControl(terminalConversation.agent, terminalConversation.terminalControl, options, {
|
|
2130
|
+
pid: terminalConversation.pid,
|
|
2131
|
+
cwd: terminalConversation.terminalControl.currentPath,
|
|
2132
|
+
terminalTarget: terminalConversation.terminalControl.target
|
|
2133
|
+
});
|
|
1725
2134
|
if (terminalConversation.agent !== "codex") {
|
|
1726
2135
|
const adapter = createRuntimeTerminalAgentRegistry(options).require(terminalConversation.agent);
|
|
1727
2136
|
printJson({
|
|
@@ -1767,7 +2176,7 @@ async function runDescribe(options) {
|
|
|
1767
2176
|
const events = readExistingEvents(logPath);
|
|
1768
2177
|
const terminalControl = terminalControlFromTakeover(isRecord(conversation.native_session_takeover) ? conversation.native_session_takeover : undefined);
|
|
1769
2178
|
const terminalStatus = terminalControl
|
|
1770
|
-
? await terminalStatusForControl(executorForConversation(conversation).kind, terminalControl, options)
|
|
2179
|
+
? await terminalStatusForControl(executorForConversation(conversation).kind, terminalControl, options, terminalRuntimeIdentityForConversation(conversation, terminalControl))
|
|
1771
2180
|
: undefined;
|
|
1772
2181
|
printJson({
|
|
1773
2182
|
conversation_id: conversation.conversation_id,
|
|
@@ -1786,9 +2195,10 @@ async function runDescribe(options) {
|
|
|
1786
2195
|
event_log_path: logPath
|
|
1787
2196
|
});
|
|
1788
2197
|
}
|
|
1789
|
-
async function terminalStatusForControl(agent, terminalControl, options) {
|
|
2198
|
+
async function terminalStatusForControl(agent, terminalControl, options, runtime) {
|
|
1790
2199
|
return createTerminalAgentBridge(options).status(agent, terminalControl, {
|
|
1791
|
-
scrollbackLines: Number(options.scrollbackLines ?? 120)
|
|
2200
|
+
scrollbackLines: Number(options.scrollbackLines ?? 120),
|
|
2201
|
+
runtime
|
|
1792
2202
|
});
|
|
1793
2203
|
}
|
|
1794
2204
|
function terminalBridgeApprovalFingerprint({ terminalControl, terminalStatus }) {
|
|
@@ -1805,10 +2215,26 @@ function terminalBridgeApprovalFingerprint({ terminalControl, terminalStatus })
|
|
|
1805
2215
|
label: approval.label,
|
|
1806
2216
|
prompt_kind: approval.prompt_kind,
|
|
1807
2217
|
command: approval.command,
|
|
2218
|
+
tool_name: approval.tool_name,
|
|
2219
|
+
request_detail: approval.request_detail,
|
|
1808
2220
|
excerpt: screen.excerpt
|
|
1809
2221
|
}))
|
|
1810
2222
|
.digest("hex");
|
|
1811
2223
|
}
|
|
2224
|
+
function assertSafeClaudeTerminalSend(terminalStatus) {
|
|
2225
|
+
const approval = isRecord(terminalStatus?.approval_state)
|
|
2226
|
+
? terminalStatus.approval_state
|
|
2227
|
+
: undefined;
|
|
2228
|
+
if (terminalStatus?.reachable !== true) {
|
|
2229
|
+
throw new Error("Claude Code terminal status is unavailable");
|
|
2230
|
+
}
|
|
2231
|
+
if (approval?.blocked === true) {
|
|
2232
|
+
throw new Error(stringValue(approval.reason) ?? "Claude Code is waiting at a permission dialog");
|
|
2233
|
+
}
|
|
2234
|
+
if (terminalStatus.activity_state !== "idle") {
|
|
2235
|
+
throw new Error(`Claude Code terminal is ${stringValue(terminalStatus.activity_state) ?? "unknown"}, not idle`);
|
|
2236
|
+
}
|
|
2237
|
+
}
|
|
1812
2238
|
function terminalBridgeApprovalInstructions({ conversation, terminalControl, terminalStatus }) {
|
|
1813
2239
|
const approval = isRecord(terminalStatus?.approval_state) ? terminalStatus.approval_state : {};
|
|
1814
2240
|
const screen = isRecord(terminalStatus?.screen) ? terminalStatus.screen : {};
|
|
@@ -1818,10 +2244,18 @@ function terminalBridgeApprovalInstructions({ conversation, terminalControl, ter
|
|
|
1818
2244
|
const keys = Array.isArray(approval.keys)
|
|
1819
2245
|
? approval.keys.filter((value) => typeof value === "string")
|
|
1820
2246
|
: [];
|
|
1821
|
-
const
|
|
1822
|
-
|
|
1823
|
-
|
|
2247
|
+
const decisionMode = stringValue(approval.decision_mode);
|
|
2248
|
+
const keyDescription = decisionMode === "structured"
|
|
2249
|
+
? "structured one-time Hook decision"
|
|
2250
|
+
: keys.length > 0
|
|
2251
|
+
? keys.join(" then ")
|
|
2252
|
+
: stringValue(approval.key) || "the detected approve key sequence";
|
|
1824
2253
|
const fingerprint = stringValue(approval.fingerprint);
|
|
2254
|
+
const promptKind = stringValue(approval.prompt_kind);
|
|
2255
|
+
const command = stringValue(approval.command);
|
|
2256
|
+
const toolName = stringValue(approval.tool_name);
|
|
2257
|
+
const requestDetail = stringValue(approval.request_detail);
|
|
2258
|
+
const requestId = stringValue(approval.request_id);
|
|
1825
2259
|
const excerpt = stringValue(screen.excerpt) || "(No terminal excerpt was available.)";
|
|
1826
2260
|
return [
|
|
1827
2261
|
`${agentName} is waiting for approval in a terminal-controlled AKK session.`,
|
|
@@ -1829,6 +2263,11 @@ function terminalBridgeApprovalInstructions({ conversation, terminalControl, ter
|
|
|
1829
2263
|
`Conversation: ${conversation.conversation_id}`,
|
|
1830
2264
|
`Terminal: ${terminalControl.kind}:${terminalControl.target}`,
|
|
1831
2265
|
`Approval option: ${label} (${keyDescription})`,
|
|
2266
|
+
promptKind ? `Request kind: ${promptKind}` : undefined,
|
|
2267
|
+
toolName ? `Tool: ${toolName}` : undefined,
|
|
2268
|
+
requestDetail ? `Request: ${requestDetail}` : undefined,
|
|
2269
|
+
command ? `Command: ${command}` : undefined,
|
|
2270
|
+
requestId ? `Structured request id: ${requestId}` : undefined,
|
|
1832
2271
|
"",
|
|
1833
2272
|
"Safe terminal excerpt:",
|
|
1834
2273
|
"```text",
|
|
@@ -1850,7 +2289,7 @@ function terminalBridgeApprovalInstructions({ conversation, terminalControl, ter
|
|
|
1850
2289
|
"Equivalent user command: `AKK cancel " + conversation.conversation_id + "`",
|
|
1851
2290
|
"",
|
|
1852
2291
|
"Do not use raw tmux, shell, or manual key presses for this approval. Do not approve without explicit user confirmation."
|
|
1853
|
-
].join("\n");
|
|
2292
|
+
].filter((line) => line !== undefined).join("\n");
|
|
1854
2293
|
}
|
|
1855
2294
|
function recordTerminalBridgeApprovalNotification({ statePath, logPath, terminalControl, terminalStatus, fingerprint }) {
|
|
1856
2295
|
const releaseLock = acquireFileLock(`${statePath}.lock`);
|
|
@@ -1913,6 +2352,7 @@ async function runSend(options) {
|
|
|
1913
2352
|
options,
|
|
1914
2353
|
conversationId: terminalConversation.conversationId,
|
|
1915
2354
|
agent: terminalConversation.agent,
|
|
2355
|
+
pid: terminalConversation.pid,
|
|
1916
2356
|
messageBody,
|
|
1917
2357
|
terminalControl: terminalConversation.terminalControl
|
|
1918
2358
|
});
|
|
@@ -1933,6 +2373,7 @@ async function runSend(options) {
|
|
|
1933
2373
|
options,
|
|
1934
2374
|
conversationId: terminalConversation.conversationId,
|
|
1935
2375
|
agent: terminalConversation.agent,
|
|
2376
|
+
pid: terminalConversation.pid,
|
|
1936
2377
|
messageBody,
|
|
1937
2378
|
terminalControl: terminalConversation.terminalControl
|
|
1938
2379
|
});
|
|
@@ -2297,7 +2738,8 @@ async function runApprove(options) {
|
|
|
2297
2738
|
options,
|
|
2298
2739
|
conversationId: terminalConversation.conversationId,
|
|
2299
2740
|
agent: terminalConversation.agent,
|
|
2300
|
-
terminalControl: terminalConversation.terminalControl
|
|
2741
|
+
terminalControl: terminalConversation.terminalControl,
|
|
2742
|
+
pid: terminalConversation.pid
|
|
2301
2743
|
});
|
|
2302
2744
|
return;
|
|
2303
2745
|
}
|
|
@@ -2320,7 +2762,11 @@ async function runApprove(options) {
|
|
|
2320
2762
|
const policyFingerprint = stringValue(options.policyFingerprint);
|
|
2321
2763
|
const approval = await createTerminalAgentBridge(options).approve(executor.kind, terminalControl, {
|
|
2322
2764
|
expectedFingerprint,
|
|
2323
|
-
scrollbackLines: Number(options.scrollbackLines ?? 120)
|
|
2765
|
+
scrollbackLines: Number(options.scrollbackLines ?? 120),
|
|
2766
|
+
runtime: terminalRuntimeIdentityForConversation(conversation, terminalControl),
|
|
2767
|
+
requiredDecisionMode: autoApproved && executor.kind === "claude"
|
|
2768
|
+
? "structured"
|
|
2769
|
+
: undefined
|
|
2324
2770
|
});
|
|
2325
2771
|
const actualFingerprint = approval.fingerprint;
|
|
2326
2772
|
if (!approval.approved) {
|
|
@@ -2358,6 +2804,8 @@ async function runApprove(options) {
|
|
|
2358
2804
|
key: approval.key,
|
|
2359
2805
|
keys: approval.keys,
|
|
2360
2806
|
label: approval.label,
|
|
2807
|
+
decision_mode: approval.decisionMode,
|
|
2808
|
+
request_id: approval.requestId,
|
|
2361
2809
|
approval_fingerprint: actualFingerprint,
|
|
2362
2810
|
auto_approved: autoApproved,
|
|
2363
2811
|
policy_rule_id: policyRuleId,
|
|
@@ -2381,6 +2829,8 @@ async function runApprove(options) {
|
|
|
2381
2829
|
key: approval.key,
|
|
2382
2830
|
keys: approval.keys,
|
|
2383
2831
|
label: approval.label,
|
|
2832
|
+
decision_mode: approval.decisionMode,
|
|
2833
|
+
request_id: approval.requestId,
|
|
2384
2834
|
approval_fingerprint: actualFingerprint,
|
|
2385
2835
|
auto_approved: autoApproved,
|
|
2386
2836
|
policy_rule_id: policyRuleId,
|
|
@@ -2409,12 +2859,28 @@ async function runApprove(options) {
|
|
|
2409
2859
|
terminal_bridge_hard_deadline_at: deadlineAt(stringValue(nativeTakeoverForUpdate.terminal_bridge_started_at) ?? approvalResolvedAt, agentHardTimeoutMinutes)
|
|
2410
2860
|
};
|
|
2411
2861
|
delete nextNativeTakeover.terminal_bridge_approval;
|
|
2412
|
-
|
|
2862
|
+
let nextConversation = {
|
|
2413
2863
|
...conversation,
|
|
2414
2864
|
status: terminalBridgeEnabled(conversation) ? "waiting_for_agent" : conversation.status,
|
|
2415
2865
|
native_session_takeover: nextNativeTakeover,
|
|
2416
2866
|
updated_at: approvalResolvedAt
|
|
2417
2867
|
};
|
|
2868
|
+
const approvalLeaseDeadlines = [
|
|
2869
|
+
stringValue(nextNativeTakeover.terminal_bridge_inactivity_deadline_at),
|
|
2870
|
+
stringValue(nextNativeTakeover.terminal_bridge_hard_deadline_at)
|
|
2871
|
+
].filter((value) => Boolean(value));
|
|
2872
|
+
if (approvalLeaseDeadlines.length > 0) {
|
|
2873
|
+
const renewedLease = renewClaudeHookLease(nextConversation, new Date(Math.min(...approvalLeaseDeadlines.map((value) => Date.parse(value)))).toISOString());
|
|
2874
|
+
if (renewedLease) {
|
|
2875
|
+
nextConversation = {
|
|
2876
|
+
...nextConversation,
|
|
2877
|
+
native_session_takeover: {
|
|
2878
|
+
...nextNativeTakeover,
|
|
2879
|
+
claude_hook_lease_id: renewedLease.id
|
|
2880
|
+
}
|
|
2881
|
+
};
|
|
2882
|
+
}
|
|
2883
|
+
}
|
|
2418
2884
|
saveState(statePath, nextConversation);
|
|
2419
2885
|
const bridgeMonitor = startTerminalBridgeMonitorForConversation({
|
|
2420
2886
|
conversation: nextConversation,
|
|
@@ -2447,16 +2913,23 @@ async function runApprove(options) {
|
|
|
2447
2913
|
key: approval.key,
|
|
2448
2914
|
keys: approval.keys,
|
|
2449
2915
|
label: approval.label,
|
|
2916
|
+
decision_mode: approval.decisionMode,
|
|
2917
|
+
request_id: approval.requestId,
|
|
2450
2918
|
approval_fingerprint: actualFingerprint,
|
|
2451
2919
|
auto_approved: autoApproved,
|
|
2452
2920
|
policy_rule_id: policyRuleId,
|
|
2453
2921
|
monitor_pid: bridgeMonitor?.pid ?? null
|
|
2454
2922
|
});
|
|
2455
2923
|
}
|
|
2456
|
-
async function runTerminalConversationApprove({ options, conversationId, agent, terminalControl }) {
|
|
2924
|
+
async function runTerminalConversationApprove({ options, conversationId, agent, terminalControl, pid }) {
|
|
2457
2925
|
const approval = await createTerminalAgentBridge(options).approve(agent, terminalControl, {
|
|
2458
2926
|
expectedFingerprint: stringValue(options.expectedApprovalFingerprint),
|
|
2459
|
-
scrollbackLines: Number(options.scrollbackLines ?? 120)
|
|
2927
|
+
scrollbackLines: Number(options.scrollbackLines ?? 120),
|
|
2928
|
+
runtime: {
|
|
2929
|
+
pid,
|
|
2930
|
+
cwd: terminalControl.currentPath,
|
|
2931
|
+
terminalTarget: terminalControl.target
|
|
2932
|
+
}
|
|
2460
2933
|
});
|
|
2461
2934
|
if (!approval.approved) {
|
|
2462
2935
|
printJson({
|
|
@@ -2476,7 +2949,9 @@ async function runTerminalConversationApprove({ options, conversationId, agent,
|
|
|
2476
2949
|
terminal_target: terminalControl.target,
|
|
2477
2950
|
key: approval.key,
|
|
2478
2951
|
keys: approval.keys,
|
|
2479
|
-
label: approval.label
|
|
2952
|
+
label: approval.label,
|
|
2953
|
+
decision_mode: approval.decisionMode,
|
|
2954
|
+
request_id: approval.requestId
|
|
2480
2955
|
});
|
|
2481
2956
|
printJson({
|
|
2482
2957
|
conversation_id: conversationId,
|
|
@@ -2486,12 +2961,26 @@ async function runTerminalConversationApprove({ options, conversationId, agent,
|
|
|
2486
2961
|
key: approval.key,
|
|
2487
2962
|
keys: approval.keys,
|
|
2488
2963
|
label: approval.label,
|
|
2489
|
-
approval_fingerprint: approval.fingerprint
|
|
2964
|
+
approval_fingerprint: approval.fingerprint,
|
|
2965
|
+
decision_mode: approval.decisionMode,
|
|
2966
|
+
request_id: approval.requestId
|
|
2490
2967
|
});
|
|
2491
2968
|
}
|
|
2492
|
-
async function runTerminalConversationSend({ options, conversationId, agent, messageBody, terminalControl }) {
|
|
2969
|
+
async function runTerminalConversationSend({ options, conversationId, agent, pid, messageBody, terminalControl }) {
|
|
2493
2970
|
const terminalPayload = terminalSubmissionPayload(String(messageBody));
|
|
2494
|
-
|
|
2971
|
+
const terminalBridge = createTerminalAgentBridge(options);
|
|
2972
|
+
if (agent === "claude") {
|
|
2973
|
+
const status = await terminalBridge.status(agent, terminalControl, {
|
|
2974
|
+
scrollbackLines: Number(options.scrollbackLines ?? 120),
|
|
2975
|
+
runtime: {
|
|
2976
|
+
pid,
|
|
2977
|
+
cwd: terminalControl.currentPath,
|
|
2978
|
+
terminalTarget: terminalControl.target
|
|
2979
|
+
}
|
|
2980
|
+
});
|
|
2981
|
+
assertSafeClaudeTerminalSend(status);
|
|
2982
|
+
}
|
|
2983
|
+
await terminalBridge.send(agent, terminalControl, terminalPayload);
|
|
2495
2984
|
runtimeLog("info", "terminal_message_send", {
|
|
2496
2985
|
conversation_id: conversationId,
|
|
2497
2986
|
agent,
|
|
@@ -2552,19 +3041,60 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
2552
3041
|
forkTakeover: undefined
|
|
2553
3042
|
}))
|
|
2554
3043
|
: terminalSubmissionPayload(String(message.body ?? ""));
|
|
3044
|
+
const preSendRuntime = {
|
|
3045
|
+
...terminalRuntimeIdentityForConversation(nextConversation, terminalControl),
|
|
3046
|
+
messageId: message.id
|
|
3047
|
+
};
|
|
2555
3048
|
let preSendScreenFingerprint;
|
|
2556
3049
|
if (bridge) {
|
|
2557
3050
|
try {
|
|
2558
3051
|
const status = await terminalBridge.status(executor.kind, terminalControl, {
|
|
2559
|
-
scrollbackLines: Number(options.scrollbackLines ?? 120)
|
|
3052
|
+
scrollbackLines: Number(options.scrollbackLines ?? 120),
|
|
3053
|
+
runtime: preSendRuntime
|
|
2560
3054
|
});
|
|
3055
|
+
if (executor.kind === "claude") {
|
|
3056
|
+
assertSafeClaudeTerminalSend(status);
|
|
3057
|
+
}
|
|
2561
3058
|
preSendScreenFingerprint = terminalBridgeScreenFingerprint(status.screen.excerpt);
|
|
2562
3059
|
}
|
|
2563
|
-
catch {
|
|
2564
|
-
|
|
3060
|
+
catch (error) {
|
|
3061
|
+
if (executor.kind === "claude") {
|
|
3062
|
+
throw new Error(`refusing to send to Claude Code without a verified idle terminal: ${error instanceof Error ? error.message : String(error)}`);
|
|
3063
|
+
}
|
|
3064
|
+
// Codex delivery can still use its established fallback when capture is unavailable.
|
|
2565
3065
|
}
|
|
2566
3066
|
}
|
|
2567
|
-
|
|
3067
|
+
if (bridge && executor.kind === "claude") {
|
|
3068
|
+
releaseClaudeHookLeasesForTerminal({
|
|
3069
|
+
storeDir: storeDirFromOptions(options),
|
|
3070
|
+
terminalControl,
|
|
3071
|
+
replacementConversationId: conversation.conversation_id
|
|
3072
|
+
});
|
|
3073
|
+
releaseClaudeHookLease(nextConversation);
|
|
3074
|
+
}
|
|
3075
|
+
const claudeHookLeaseState = bridge && executor.kind === "claude"
|
|
3076
|
+
? activateClaudeHookLease({
|
|
3077
|
+
options,
|
|
3078
|
+
conversation: nextConversation,
|
|
3079
|
+
message,
|
|
3080
|
+
terminalControl,
|
|
3081
|
+
expiresAt: deadlineAt(bridgeStartedAt, agentTimeoutMinutes > 0
|
|
3082
|
+
? Math.min(agentTimeoutMinutes, agentHardTimeoutMinutes)
|
|
3083
|
+
: agentHardTimeoutMinutes)
|
|
3084
|
+
})
|
|
3085
|
+
: undefined;
|
|
3086
|
+
const conversationWithHookLease = claudeHookLeaseState
|
|
3087
|
+
? withClaudeHookLeaseState(nextConversation, claudeHookLeaseState)
|
|
3088
|
+
: nextConversation;
|
|
3089
|
+
try {
|
|
3090
|
+
await terminalBridge.send(executor.kind, terminalControl, terminalPayload);
|
|
3091
|
+
}
|
|
3092
|
+
catch (error) {
|
|
3093
|
+
if (claudeHookLeaseState) {
|
|
3094
|
+
releaseClaudeHookLease(conversationWithHookLease);
|
|
3095
|
+
}
|
|
3096
|
+
throw error;
|
|
3097
|
+
}
|
|
2568
3098
|
const supersededConversationIds = bridge
|
|
2569
3099
|
? supersedeTerminalBridgeConversations({
|
|
2570
3100
|
storeDir: storeDirFromOptions(options),
|
|
@@ -2592,7 +3122,7 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
2592
3122
|
});
|
|
2593
3123
|
const bridgeConversation = bridge
|
|
2594
3124
|
? withTerminalBridgeState({
|
|
2595
|
-
conversation:
|
|
3125
|
+
conversation: conversationWithHookLease,
|
|
2596
3126
|
message,
|
|
2597
3127
|
startedAt: bridgeStartedAt,
|
|
2598
3128
|
agentTimeoutMinutes,
|
|
@@ -2654,7 +3184,7 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
2654
3184
|
function terminalSubmissionPayload(payload) {
|
|
2655
3185
|
return payload.replace(/[\r\n]+$/u, "");
|
|
2656
3186
|
}
|
|
2657
|
-
function createManagedTerminalConversationFromRawId({ options, conversationId, agent, messageBody, terminalControl }) {
|
|
3187
|
+
function createManagedTerminalConversationFromRawId({ options, conversationId, agent, pid, messageBody, terminalControl }) {
|
|
2658
3188
|
const workspace = terminalControl.currentPath ?? process.cwd();
|
|
2659
3189
|
const storeDir = expandHome(options.storeDir ?? options.logDir ?? defaultStoreDir(workspace));
|
|
2660
3190
|
cleanupIdleConversations(storeDir, options);
|
|
@@ -2685,6 +3215,9 @@ function createManagedTerminalConversationFromRawId({ options, conversationId, a
|
|
|
2685
3215
|
gatewaySession: options.gatewaySession,
|
|
2686
3216
|
openclawBin: options.openclawBin ?? resolveOptionalExecutable("openclaw")
|
|
2687
3217
|
});
|
|
3218
|
+
const claudeAgent = agent === "claude"
|
|
3219
|
+
? loadClaudeAgentRows(options).find((row) => row.pid === pid)
|
|
3220
|
+
: undefined;
|
|
2688
3221
|
const attachedConversation = withStoragePaths({
|
|
2689
3222
|
...conversation,
|
|
2690
3223
|
executor,
|
|
@@ -2701,6 +3234,8 @@ function createManagedTerminalConversationFromRawId({ options, conversationId, a
|
|
|
2701
3234
|
native_session_takeover: {
|
|
2702
3235
|
agent,
|
|
2703
3236
|
native_session_id: conversationId,
|
|
3237
|
+
terminal_agent_pid: pid,
|
|
3238
|
+
terminal_agent_session_id: claudeAgent?.sessionId,
|
|
2704
3239
|
source_cwd: workspace,
|
|
2705
3240
|
source_title: `Terminal-controlled ${executor.display_name} ${terminalControl.target}`,
|
|
2706
3241
|
strategy: "terminal_control",
|
|
@@ -2805,14 +3340,16 @@ function supersedeTerminalBridgeConversations({ storeDir, terminalControl, repla
|
|
|
2805
3340
|
continue;
|
|
2806
3341
|
}
|
|
2807
3342
|
const now = new Date().toISOString();
|
|
2808
|
-
|
|
3343
|
+
const closedConversation = {
|
|
2809
3344
|
...current,
|
|
2810
3345
|
status: "closed",
|
|
2811
3346
|
closed_at: now,
|
|
2812
3347
|
close_reason: "terminal bridge superseded by a newer task on the same terminal",
|
|
2813
3348
|
superseded_by_conversation_id: replacementConversationId,
|
|
2814
3349
|
updated_at: now
|
|
2815
|
-
}
|
|
3350
|
+
};
|
|
3351
|
+
saveState(candidateStatePath, closedConversation);
|
|
3352
|
+
releaseClaudeHookLease(closedConversation);
|
|
2816
3353
|
appendEvent(logPathForStatePath(candidateStatePath), {
|
|
2817
3354
|
ts: now,
|
|
2818
3355
|
conversation_id: current.conversation_id,
|
|
@@ -3242,17 +3779,37 @@ async function runRenew(options) {
|
|
|
3242
3779
|
throw new Error(`cannot renew ${conversation.conversation_id}; terminal bridge hard lifetime of ${hardTimeoutMinutes} minutes has elapsed`);
|
|
3243
3780
|
}
|
|
3244
3781
|
const now = new Date().toISOString();
|
|
3782
|
+
const currentMessageId = stringValue(nativeTakeover?.terminal_bridge_message_id);
|
|
3783
|
+
const hardDeadline = deadlineAt(startedAt ?? now, hardTimeoutMinutes) ??
|
|
3784
|
+
new Date(Date.now() + hardTimeoutMinutes * 60 * 1000).toISOString();
|
|
3785
|
+
const inactivityDeadline = deadlineAt(now, inactivityTimeoutMinutes) ??
|
|
3786
|
+
new Date(Date.now() + inactivityTimeoutMinutes * 60 * 1000).toISOString();
|
|
3787
|
+
const renewedLease = executorForConversation(conversation).kind === "claude" && currentMessageId
|
|
3788
|
+
? activateClaudeHookLease({
|
|
3789
|
+
options,
|
|
3790
|
+
conversation,
|
|
3791
|
+
message: { id: currentMessageId },
|
|
3792
|
+
terminalControl,
|
|
3793
|
+
expiresAt: new Date(Math.min(Date.parse(hardDeadline), Date.parse(inactivityDeadline))).toISOString()
|
|
3794
|
+
})
|
|
3795
|
+
: undefined;
|
|
3796
|
+
const renewedBase = renewedLease
|
|
3797
|
+
? withClaudeHookLeaseState(conversation, renewedLease)
|
|
3798
|
+
: conversation;
|
|
3799
|
+
const renewedNativeTakeover = isRecord(renewedBase.native_session_takeover)
|
|
3800
|
+
? renewedBase.native_session_takeover
|
|
3801
|
+
: nativeTakeover;
|
|
3245
3802
|
const renewed = {
|
|
3246
|
-
...
|
|
3803
|
+
...renewedBase,
|
|
3247
3804
|
status: "waiting_for_agent",
|
|
3248
3805
|
native_session_takeover: {
|
|
3249
|
-
...
|
|
3806
|
+
...renewedNativeTakeover,
|
|
3250
3807
|
terminal_bridge_monitor_started_at: now,
|
|
3251
3808
|
terminal_bridge_last_activity_at: now,
|
|
3252
3809
|
terminal_bridge_inactivity_timeout_minutes: inactivityTimeoutMinutes,
|
|
3253
3810
|
terminal_bridge_hard_timeout_minutes: hardTimeoutMinutes,
|
|
3254
|
-
terminal_bridge_inactivity_deadline_at:
|
|
3255
|
-
terminal_bridge_hard_deadline_at:
|
|
3811
|
+
terminal_bridge_inactivity_deadline_at: inactivityDeadline,
|
|
3812
|
+
terminal_bridge_hard_deadline_at: hardDeadline,
|
|
3256
3813
|
terminal_bridge_renewed_at: now
|
|
3257
3814
|
},
|
|
3258
3815
|
updated_at: now
|
|
@@ -3324,7 +3881,8 @@ async function runCancel(options) {
|
|
|
3324
3881
|
options,
|
|
3325
3882
|
conversationId: terminalConversation.conversationId,
|
|
3326
3883
|
agent: terminalConversation.agent,
|
|
3327
|
-
terminalControl: terminalConversation.terminalControl
|
|
3884
|
+
terminalControl: terminalConversation.terminalControl,
|
|
3885
|
+
pid: terminalConversation.pid
|
|
3328
3886
|
});
|
|
3329
3887
|
return;
|
|
3330
3888
|
}
|
|
@@ -3405,14 +3963,23 @@ async function runCancel(options) {
|
|
|
3405
3963
|
budget: budgetAction(nextConversation)
|
|
3406
3964
|
});
|
|
3407
3965
|
}
|
|
3408
|
-
async function runTerminalConversationCancel({ options, conversationId, agent, terminalControl }) {
|
|
3409
|
-
const cancellation = await createTerminalAgentBridge(options).cancel(agent, terminalControl
|
|
3966
|
+
async function runTerminalConversationCancel({ options, conversationId, agent, terminalControl, pid }) {
|
|
3967
|
+
const cancellation = await createTerminalAgentBridge(options).cancel(agent, terminalControl, {
|
|
3968
|
+
runtime: {
|
|
3969
|
+
pid,
|
|
3970
|
+
cwd: terminalControl.currentPath,
|
|
3971
|
+
terminalTarget: terminalControl.target
|
|
3972
|
+
},
|
|
3973
|
+
scrollbackLines: Number(options.scrollbackLines ?? 120)
|
|
3974
|
+
});
|
|
3410
3975
|
runtimeLog("info", "terminal_cancel_requested", {
|
|
3411
3976
|
conversation_id: conversationId,
|
|
3412
3977
|
agent,
|
|
3413
3978
|
terminal_target: terminalControl.target,
|
|
3414
3979
|
key: cancellation.key,
|
|
3415
3980
|
keys: cancellation.keys,
|
|
3981
|
+
denied_approval: cancellation.deniedApproval,
|
|
3982
|
+
request_id: cancellation.requestId,
|
|
3416
3983
|
cancel_requested: cancellation.cancelRequested,
|
|
3417
3984
|
reason: cancellation.reason
|
|
3418
3985
|
});
|
|
@@ -3423,11 +3990,16 @@ async function runTerminalConversationCancel({ options, conversationId, agent, t
|
|
|
3423
3990
|
reason: cancellation.reason,
|
|
3424
3991
|
terminal_control: terminalControl,
|
|
3425
3992
|
key: cancellation.key,
|
|
3426
|
-
keys: cancellation.keys
|
|
3993
|
+
keys: cancellation.keys,
|
|
3994
|
+
denied_approval: cancellation.deniedApproval,
|
|
3995
|
+
request_id: cancellation.requestId
|
|
3427
3996
|
});
|
|
3428
3997
|
}
|
|
3429
3998
|
async function runTerminalControlCancel({ options, conversation, statePath, logPath, agent, terminalControl }) {
|
|
3430
|
-
const cancellation = await createTerminalAgentBridge(options).cancel(agent, terminalControl
|
|
3999
|
+
const cancellation = await createTerminalAgentBridge(options).cancel(agent, terminalControl, {
|
|
4000
|
+
runtime: terminalRuntimeIdentityForConversation(conversation, terminalControl),
|
|
4001
|
+
scrollbackLines: Number(options.scrollbackLines ?? 120)
|
|
4002
|
+
});
|
|
3431
4003
|
if (!cancellation.cancelRequested) {
|
|
3432
4004
|
printJson({
|
|
3433
4005
|
conversation,
|
|
@@ -3445,27 +4017,36 @@ async function runTerminalControlCancel({ options, conversation, statePath, logP
|
|
|
3445
4017
|
event: "terminal_cancel_requested",
|
|
3446
4018
|
terminal_control: terminalControl,
|
|
3447
4019
|
key: cancellation.key,
|
|
3448
|
-
keys: cancellation.keys
|
|
4020
|
+
keys: cancellation.keys,
|
|
4021
|
+
denied_approval: cancellation.deniedApproval,
|
|
4022
|
+
request_id: cancellation.requestId
|
|
3449
4023
|
});
|
|
3450
4024
|
runtimeLog("info", "terminal_cancel_requested", {
|
|
3451
4025
|
conversation_id: conversation.conversation_id,
|
|
3452
4026
|
agent,
|
|
3453
4027
|
terminal_target: terminalControl.target,
|
|
3454
4028
|
key: cancellation.key,
|
|
3455
|
-
keys: cancellation.keys
|
|
4029
|
+
keys: cancellation.keys,
|
|
4030
|
+
denied_approval: cancellation.deniedApproval,
|
|
4031
|
+
request_id: cancellation.requestId
|
|
3456
4032
|
});
|
|
3457
4033
|
const nextConversation = {
|
|
3458
4034
|
...conversation,
|
|
4035
|
+
status: "cancelled",
|
|
4036
|
+
cancelled_at: now,
|
|
3459
4037
|
terminal_cancel_requested_at: now,
|
|
3460
4038
|
updated_at: now
|
|
3461
4039
|
};
|
|
3462
4040
|
saveState(statePath, nextConversation);
|
|
4041
|
+
releaseClaudeHookLease(nextConversation);
|
|
3463
4042
|
printJson({
|
|
3464
4043
|
conversation: nextConversation,
|
|
3465
4044
|
cancel_requested: true,
|
|
3466
4045
|
terminal_control: terminalControl,
|
|
3467
4046
|
key: cancellation.key,
|
|
3468
4047
|
keys: cancellation.keys,
|
|
4048
|
+
denied_approval: cancellation.deniedApproval,
|
|
4049
|
+
request_id: cancellation.requestId,
|
|
3469
4050
|
budget: budgetAction(nextConversation)
|
|
3470
4051
|
});
|
|
3471
4052
|
}
|
|
@@ -3647,6 +4228,7 @@ function runClose(options) {
|
|
|
3647
4228
|
updated_at: now
|
|
3648
4229
|
};
|
|
3649
4230
|
saveState(statePath, closed);
|
|
4231
|
+
releaseClaudeHookLease(closed);
|
|
3650
4232
|
appendEvent(logPath, {
|
|
3651
4233
|
ts: now,
|
|
3652
4234
|
conversation_id: conversation.conversation_id,
|
|
@@ -3977,6 +4559,7 @@ async function runTerminalBridgeMonitor(options) {
|
|
|
3977
4559
|
}
|
|
3978
4560
|
const requestText = String(nativeTakeover?.["terminal_bridge_request_text"] ?? conversation.user_request ?? "");
|
|
3979
4561
|
const startedAt = stringValue(nativeTakeover?.["terminal_bridge_started_at"]);
|
|
4562
|
+
const terminalRuntime = terminalRuntimeIdentityForConversation(conversation, terminalControl);
|
|
3980
4563
|
const screenChangedSinceSend = preSendScreenFingerprint !== undefined &&
|
|
3981
4564
|
previousScreenFingerprint !== undefined &&
|
|
3982
4565
|
previousScreenFingerprint !== preSendScreenFingerprint;
|
|
@@ -3986,19 +4569,114 @@ async function runTerminalBridgeMonitor(options) {
|
|
|
3986
4569
|
screenOptions: {
|
|
3987
4570
|
scrollbackLines: Number(options.scrollbackLines ?? 120),
|
|
3988
4571
|
requestText,
|
|
3989
|
-
screenChangedSinceSend
|
|
4572
|
+
screenChangedSinceSend,
|
|
4573
|
+
runtime: terminalRuntime
|
|
3990
4574
|
},
|
|
3991
4575
|
durableRequest: {
|
|
3992
|
-
sessionId:
|
|
4576
|
+
sessionId: terminalRuntime.sessionId,
|
|
3993
4577
|
cwd: stringValue(nativeTakeover?.["source_cwd"]),
|
|
3994
4578
|
requestText,
|
|
3995
4579
|
requestHash: stringValue(nativeTakeover?.["terminal_bridge_request_hash"]),
|
|
3996
4580
|
startedAt,
|
|
3997
|
-
context: {
|
|
4581
|
+
context: {
|
|
4582
|
+
conversation,
|
|
4583
|
+
nativeTakeover,
|
|
4584
|
+
...terminalRuntime
|
|
4585
|
+
}
|
|
3998
4586
|
}
|
|
3999
4587
|
});
|
|
4000
4588
|
const terminalStatus = poll.status;
|
|
4001
4589
|
const approval = terminalStatus.approval_state;
|
|
4590
|
+
if (isRecord(approval) && approval.blocked === true && approval.approvable !== true) {
|
|
4591
|
+
const approvalReason = stringValue(approval.reason) ??
|
|
4592
|
+
"Claude Code permission state cannot be safely resolved through AKK";
|
|
4593
|
+
appendEvent(logPath, {
|
|
4594
|
+
ts: new Date().toISOString(),
|
|
4595
|
+
conversation_id: conversation.conversation_id,
|
|
4596
|
+
event: "terminal_bridge_approval_not_approvable",
|
|
4597
|
+
terminal_control: terminalControl,
|
|
4598
|
+
activity_state: terminalStatus.activity_state,
|
|
4599
|
+
reason: approvalReason
|
|
4600
|
+
});
|
|
4601
|
+
if (/waiting for hook consumption/iu.test(approvalReason)) {
|
|
4602
|
+
sleepSync(pollIntervalMs);
|
|
4603
|
+
continue;
|
|
4604
|
+
}
|
|
4605
|
+
const fingerprint = terminalBridgeApprovalFingerprint({ terminalControl, terminalStatus });
|
|
4606
|
+
const notification = recordTerminalBridgeApprovalNotification({
|
|
4607
|
+
statePath,
|
|
4608
|
+
logPath,
|
|
4609
|
+
terminalControl,
|
|
4610
|
+
terminalStatus,
|
|
4611
|
+
fingerprint
|
|
4612
|
+
});
|
|
4613
|
+
if (notification.duplicate) {
|
|
4614
|
+
printJson({
|
|
4615
|
+
conversation: notification.conversation,
|
|
4616
|
+
monitored: true,
|
|
4617
|
+
terminal_bridge: true,
|
|
4618
|
+
awaiting_approval: true,
|
|
4619
|
+
approvable: false,
|
|
4620
|
+
duplicate: true,
|
|
4621
|
+
reason: approvalReason,
|
|
4622
|
+
terminal_control: terminalControl,
|
|
4623
|
+
terminal_status: terminalStatus
|
|
4624
|
+
});
|
|
4625
|
+
return;
|
|
4626
|
+
}
|
|
4627
|
+
const callbackMessage = createMessage({
|
|
4628
|
+
conversation: notification.conversation,
|
|
4629
|
+
from: executor.actor,
|
|
4630
|
+
to: "openclaw",
|
|
4631
|
+
type: "blocked",
|
|
4632
|
+
requiresResponse: true,
|
|
4633
|
+
body: [
|
|
4634
|
+
`${executor.display_name} is waiting at a permission state that AKK cannot safely approve.`,
|
|
4635
|
+
approvalReason,
|
|
4636
|
+
"",
|
|
4637
|
+
`Conversation: ${notification.conversation.conversation_id}`,
|
|
4638
|
+
`Terminal: ${terminalControl.target}`,
|
|
4639
|
+
"Review and resolve this dialog in the terminal manually. AKK intentionally sends no key when the request identity cannot be revalidated."
|
|
4640
|
+
].join("\n"),
|
|
4641
|
+
metadata: {
|
|
4642
|
+
source: "terminal_bridge",
|
|
4643
|
+
reason: "approval_not_approvable",
|
|
4644
|
+
terminal_control: terminalControl,
|
|
4645
|
+
terminal_status: terminalStatus,
|
|
4646
|
+
approval_fingerprint: fingerprint
|
|
4647
|
+
}
|
|
4648
|
+
});
|
|
4649
|
+
if (notification.conversation.gateway_method) {
|
|
4650
|
+
runLockedCallback({
|
|
4651
|
+
...options,
|
|
4652
|
+
statePath,
|
|
4653
|
+
log: logPath,
|
|
4654
|
+
messageJson: JSON.stringify(callbackMessage),
|
|
4655
|
+
gatewayMethod: notification.conversation.gateway_method,
|
|
4656
|
+
gatewaySession: notification.conversation.gateway_session,
|
|
4657
|
+
openclawSession: notification.conversation.openclaw_session,
|
|
4658
|
+
openclawBin: notification.conversation.openclaw_bin,
|
|
4659
|
+
gatewayUrl: stringValue(notification.conversation.gateway_token)
|
|
4660
|
+
? notification.conversation.gateway_url
|
|
4661
|
+
: undefined,
|
|
4662
|
+
token: stringValue(notification.conversation.gateway_token)
|
|
4663
|
+
});
|
|
4664
|
+
return;
|
|
4665
|
+
}
|
|
4666
|
+
printJson({
|
|
4667
|
+
conversation: notification.conversation,
|
|
4668
|
+
monitored: true,
|
|
4669
|
+
terminal_bridge: true,
|
|
4670
|
+
awaiting_approval: true,
|
|
4671
|
+
approvable: false,
|
|
4672
|
+
delivered: false,
|
|
4673
|
+
message: callbackMessage,
|
|
4674
|
+
reason: "gateway_method_missing",
|
|
4675
|
+
terminal_control: terminalControl,
|
|
4676
|
+
terminal_status: terminalStatus
|
|
4677
|
+
});
|
|
4678
|
+
return;
|
|
4679
|
+
}
|
|
4002
4680
|
if (isRecord(approval) && approval.blocked === true) {
|
|
4003
4681
|
const fingerprint = terminalBridgeApprovalFingerprint({ terminalControl, terminalStatus });
|
|
4004
4682
|
appendEvent(logPath, {
|
|
@@ -4150,55 +4828,98 @@ async function runTerminalBridgeMonitor(options) {
|
|
|
4150
4828
|
: undefined;
|
|
4151
4829
|
const completionStable = completionFingerprint !== undefined && completionFingerprint === idleCompletionFingerprint;
|
|
4152
4830
|
idleCompletionFingerprint = completionFingerprint;
|
|
4153
|
-
if (completion && completionStable) {
|
|
4154
|
-
|
|
4155
|
-
|
|
4156
|
-
|
|
4157
|
-
|
|
4158
|
-
|
|
4159
|
-
|
|
4160
|
-
completion_source: completion.source,
|
|
4161
|
-
completion_id: completion.id,
|
|
4162
|
-
terminal_session: completionMetadata.session,
|
|
4163
|
-
context_match: completionMetadata.context_match,
|
|
4164
|
-
assistant_timestamp: completion?.timestamp,
|
|
4165
|
-
rollout_turn_id: completion.source === "durable" ? completion.id : undefined,
|
|
4166
|
-
terminal_bridge_message_id: currentMessageId
|
|
4831
|
+
if (completion && completionStable && completionFingerprint) {
|
|
4832
|
+
const completionOutcome = completion.outcome === "failure" ? "failure" : "success";
|
|
4833
|
+
const callbackMessageId = deterministicTerminalCallbackMessageId({
|
|
4834
|
+
conversationId: conversation.conversation_id,
|
|
4835
|
+
terminalMessageId: currentMessageId,
|
|
4836
|
+
completionFingerprint,
|
|
4837
|
+
outcome: completionOutcome
|
|
4167
4838
|
});
|
|
4168
|
-
const
|
|
4169
|
-
|
|
4170
|
-
|
|
4171
|
-
|
|
4172
|
-
|
|
4173
|
-
|
|
4174
|
-
|
|
4175
|
-
|
|
4176
|
-
|
|
4839
|
+
const claim = claimTerminalBridgeCompletion({
|
|
4840
|
+
statePath,
|
|
4841
|
+
logPath,
|
|
4842
|
+
terminalMessageId: currentMessageId,
|
|
4843
|
+
completionFingerprint,
|
|
4844
|
+
completionId: completion.id,
|
|
4845
|
+
callbackMessageId,
|
|
4846
|
+
outcome: completionOutcome
|
|
4847
|
+
});
|
|
4848
|
+
if (!claim.claimed) {
|
|
4849
|
+
printJson({
|
|
4850
|
+
conversation: claim.conversation,
|
|
4851
|
+
monitored: true,
|
|
4852
|
+
terminal_bridge: true,
|
|
4853
|
+
completed: false,
|
|
4854
|
+
duplicate: true,
|
|
4855
|
+
reason: claim.reason
|
|
4856
|
+
});
|
|
4857
|
+
return;
|
|
4858
|
+
}
|
|
4859
|
+
try {
|
|
4860
|
+
conversation = claim.conversation;
|
|
4861
|
+
appendEvent(logPath, {
|
|
4862
|
+
ts: new Date().toISOString(),
|
|
4863
|
+
conversation_id: conversation.conversation_id,
|
|
4864
|
+
event: "terminal_bridge_completion_detected",
|
|
4177
4865
|
terminal_control: terminalControl,
|
|
4178
|
-
|
|
4866
|
+
match: completionMatch,
|
|
4179
4867
|
completion_source: completion.source,
|
|
4868
|
+
completion_outcome: completionOutcome,
|
|
4180
4869
|
completion_id: completion.id,
|
|
4181
4870
|
terminal_session: completionMetadata.session,
|
|
4182
|
-
|
|
4183
|
-
match: completionMatch,
|
|
4871
|
+
context_match: completionMetadata.context_match,
|
|
4184
4872
|
assistant_timestamp: completion?.timestamp,
|
|
4185
4873
|
rollout_turn_id: completion.source === "durable" ? completion.id : undefined,
|
|
4186
|
-
terminal_bridge_message_id: currentMessageId
|
|
4187
|
-
|
|
4188
|
-
|
|
4189
|
-
|
|
4190
|
-
|
|
4191
|
-
|
|
4192
|
-
|
|
4193
|
-
|
|
4194
|
-
|
|
4195
|
-
|
|
4196
|
-
|
|
4197
|
-
|
|
4198
|
-
|
|
4199
|
-
|
|
4200
|
-
|
|
4201
|
-
|
|
4874
|
+
terminal_bridge_message_id: currentMessageId,
|
|
4875
|
+
callback_message_id: callbackMessageId
|
|
4876
|
+
});
|
|
4877
|
+
const callbackMessage = {
|
|
4878
|
+
...createMessage({
|
|
4879
|
+
conversation,
|
|
4880
|
+
from: executor.actor,
|
|
4881
|
+
to: "openclaw",
|
|
4882
|
+
type: completionOutcome === "failure" ? "error" : "done",
|
|
4883
|
+
requiresResponse: false,
|
|
4884
|
+
body: completion.text,
|
|
4885
|
+
metadata: {
|
|
4886
|
+
source: "terminal_bridge",
|
|
4887
|
+
terminal_control: terminalControl,
|
|
4888
|
+
...completionMetadata,
|
|
4889
|
+
completion_source: completion.source,
|
|
4890
|
+
completion_outcome: completionOutcome,
|
|
4891
|
+
completion_id: completion.id,
|
|
4892
|
+
terminal_session: completionMetadata.session,
|
|
4893
|
+
confidence: completion.confidence,
|
|
4894
|
+
match: completionMatch,
|
|
4895
|
+
assistant_timestamp: completion?.timestamp,
|
|
4896
|
+
rollout_turn_id: completion.source === "durable" ? completion.id : undefined,
|
|
4897
|
+
terminal_bridge_message_id: currentMessageId
|
|
4898
|
+
}
|
|
4899
|
+
}),
|
|
4900
|
+
id: callbackMessageId
|
|
4901
|
+
};
|
|
4902
|
+
runLockedCallback({
|
|
4903
|
+
...options,
|
|
4904
|
+
statePath,
|
|
4905
|
+
log: logPath,
|
|
4906
|
+
closeTerminalBridgeOnDone: completionOutcome === "success",
|
|
4907
|
+
trackCallbackDelivery: true,
|
|
4908
|
+
recoverTerminalCompletion: claim.resumed === true,
|
|
4909
|
+
preserveMessageId: true,
|
|
4910
|
+
messageJson: JSON.stringify(callbackMessage),
|
|
4911
|
+
gatewayMethod: conversation.gateway_method,
|
|
4912
|
+
gatewaySession: conversation.gateway_session,
|
|
4913
|
+
openclawSession: conversation.openclaw_session,
|
|
4914
|
+
openclawBin: conversation.openclaw_bin,
|
|
4915
|
+
gatewayUrl: stringValue(conversation.gateway_token) ? conversation.gateway_url : undefined,
|
|
4916
|
+
token: stringValue(conversation.gateway_token)
|
|
4917
|
+
});
|
|
4918
|
+
}
|
|
4919
|
+
finally {
|
|
4920
|
+
releaseClaudeHookLease(conversation);
|
|
4921
|
+
claim.release();
|
|
4922
|
+
}
|
|
4202
4923
|
return;
|
|
4203
4924
|
}
|
|
4204
4925
|
// A concrete approval or completion observed on this poll wins over a timeout boundary.
|
|
@@ -4302,6 +5023,111 @@ function terminalBridgeRequestFingerprint(value) {
|
|
|
4302
5023
|
const text = String(value ?? "").replace(/\s+/gu, " ").trim();
|
|
4303
5024
|
return text ? createHash("sha256").update(text).digest("hex") : undefined;
|
|
4304
5025
|
}
|
|
5026
|
+
function deterministicTerminalCallbackMessageId({ conversationId, terminalMessageId, completionFingerprint, outcome }) {
|
|
5027
|
+
const digest = createHash("sha256")
|
|
5028
|
+
.update(JSON.stringify({
|
|
5029
|
+
conversation_id: conversationId,
|
|
5030
|
+
terminal_message_id: terminalMessageId,
|
|
5031
|
+
completion_fingerprint: completionFingerprint,
|
|
5032
|
+
outcome
|
|
5033
|
+
}))
|
|
5034
|
+
.digest("hex")
|
|
5035
|
+
.slice(0, 32);
|
|
5036
|
+
return `msg-terminal-${digest}`;
|
|
5037
|
+
}
|
|
5038
|
+
function claimTerminalBridgeCompletion({ statePath, logPath, terminalMessageId, completionFingerprint, completionId, callbackMessageId, outcome }) {
|
|
5039
|
+
const release = acquireFileLock(`${statePath}.lock`);
|
|
5040
|
+
try {
|
|
5041
|
+
const conversation = loadState(statePath);
|
|
5042
|
+
const nativeTakeover = isRecord(conversation.native_session_takeover)
|
|
5043
|
+
? conversation.native_session_takeover
|
|
5044
|
+
: {};
|
|
5045
|
+
if (!isWaitingForAgent(conversation.status)) {
|
|
5046
|
+
release();
|
|
5047
|
+
return {
|
|
5048
|
+
claimed: false,
|
|
5049
|
+
conversation,
|
|
5050
|
+
reason: "conversation_no_longer_waiting"
|
|
5051
|
+
};
|
|
5052
|
+
}
|
|
5053
|
+
if (stringValue(nativeTakeover.terminal_bridge_message_id) !== terminalMessageId) {
|
|
5054
|
+
release();
|
|
5055
|
+
return {
|
|
5056
|
+
claimed: false,
|
|
5057
|
+
conversation,
|
|
5058
|
+
reason: "terminal_bridge_task_replaced"
|
|
5059
|
+
};
|
|
5060
|
+
}
|
|
5061
|
+
const existing = isRecord(nativeTakeover.terminal_bridge_completion_claim)
|
|
5062
|
+
? nativeTakeover.terminal_bridge_completion_claim
|
|
5063
|
+
: undefined;
|
|
5064
|
+
if (existing) {
|
|
5065
|
+
if (existing.callback_message_id === callbackMessageId &&
|
|
5066
|
+
existing.terminal_bridge_message_id === terminalMessageId &&
|
|
5067
|
+
existing.completion_fingerprint === completionFingerprint &&
|
|
5068
|
+
existing.outcome === outcome) {
|
|
5069
|
+
appendEvent(logPath, {
|
|
5070
|
+
ts: new Date().toISOString(),
|
|
5071
|
+
conversation_id: conversation.conversation_id,
|
|
5072
|
+
event: "terminal_bridge_completion_claim_resumed",
|
|
5073
|
+
terminal_bridge_message_id: terminalMessageId,
|
|
5074
|
+
completion_fingerprint: completionFingerprint,
|
|
5075
|
+
callback_message_id: callbackMessageId,
|
|
5076
|
+
outcome
|
|
5077
|
+
});
|
|
5078
|
+
return {
|
|
5079
|
+
claimed: true,
|
|
5080
|
+
resumed: true,
|
|
5081
|
+
conversation,
|
|
5082
|
+
release
|
|
5083
|
+
};
|
|
5084
|
+
}
|
|
5085
|
+
release();
|
|
5086
|
+
return {
|
|
5087
|
+
claimed: false,
|
|
5088
|
+
conversation,
|
|
5089
|
+
reason: "terminal_bridge_completion_claim_conflict"
|
|
5090
|
+
};
|
|
5091
|
+
}
|
|
5092
|
+
const claimedAt = new Date().toISOString();
|
|
5093
|
+
const claimedConversation = {
|
|
5094
|
+
...conversation,
|
|
5095
|
+
native_session_takeover: {
|
|
5096
|
+
...nativeTakeover,
|
|
5097
|
+
terminal_bridge_completion_claim: {
|
|
5098
|
+
terminal_bridge_message_id: terminalMessageId,
|
|
5099
|
+
completion_fingerprint: completionFingerprint,
|
|
5100
|
+
completion_id: completionId,
|
|
5101
|
+
callback_message_id: callbackMessageId,
|
|
5102
|
+
outcome,
|
|
5103
|
+
claimed_at: claimedAt
|
|
5104
|
+
}
|
|
5105
|
+
},
|
|
5106
|
+
updated_at: claimedAt
|
|
5107
|
+
};
|
|
5108
|
+
saveState(statePath, claimedConversation);
|
|
5109
|
+
appendEvent(logPath, {
|
|
5110
|
+
ts: claimedAt,
|
|
5111
|
+
conversation_id: conversation.conversation_id,
|
|
5112
|
+
event: "terminal_bridge_completion_claimed",
|
|
5113
|
+
terminal_bridge_message_id: terminalMessageId,
|
|
5114
|
+
completion_fingerprint: completionFingerprint,
|
|
5115
|
+
completion_id: completionId,
|
|
5116
|
+
callback_message_id: callbackMessageId,
|
|
5117
|
+
outcome
|
|
5118
|
+
});
|
|
5119
|
+
return {
|
|
5120
|
+
claimed: true,
|
|
5121
|
+
resumed: false,
|
|
5122
|
+
conversation: claimedConversation,
|
|
5123
|
+
release
|
|
5124
|
+
};
|
|
5125
|
+
}
|
|
5126
|
+
catch (error) {
|
|
5127
|
+
release();
|
|
5128
|
+
throw error;
|
|
5129
|
+
}
|
|
5130
|
+
}
|
|
4305
5131
|
function terminalBridgeActivityPersistIntervalMs(timeoutMinutes, pollIntervalMs) {
|
|
4306
5132
|
if (!Number.isFinite(timeoutMinutes) || timeoutMinutes <= 0) {
|
|
4307
5133
|
return 5 * 60 * 1000;
|
|
@@ -4330,6 +5156,13 @@ function persistTerminalBridgeActivity({ conversation, statePath, logPath, obser
|
|
|
4330
5156
|
const inactivityDeadlineAt = Number.isFinite(timeoutMinutes) && timeoutMinutes > 0
|
|
4331
5157
|
? new Date(observedAtMs + timeoutMinutes * 60 * 1000).toISOString()
|
|
4332
5158
|
: undefined;
|
|
5159
|
+
const hardDeadlineAt = stringValue(nativeTakeover.terminal_bridge_hard_deadline_at);
|
|
5160
|
+
const leaseDeadlineAt = inactivityDeadlineAt && hardDeadlineAt
|
|
5161
|
+
? new Date(Math.min(Date.parse(inactivityDeadlineAt), Date.parse(hardDeadlineAt))).toISOString()
|
|
5162
|
+
: inactivityDeadlineAt ?? hardDeadlineAt;
|
|
5163
|
+
const renewedLease = leaseDeadlineAt
|
|
5164
|
+
? renewClaudeHookLease(currentConversation, leaseDeadlineAt)
|
|
5165
|
+
: undefined;
|
|
4333
5166
|
const nextConversation = {
|
|
4334
5167
|
...currentConversation,
|
|
4335
5168
|
native_session_takeover: {
|
|
@@ -4338,7 +5171,8 @@ function persistTerminalBridgeActivity({ conversation, statePath, logPath, obser
|
|
|
4338
5171
|
terminal_bridge_last_activity_reason: reason,
|
|
4339
5172
|
terminal_bridge_inactivity_deadline_at: inactivityDeadlineAt,
|
|
4340
5173
|
terminal_bridge_inactivity_timeout_minutes: timeoutMinutes,
|
|
4341
|
-
terminal_bridge_hard_timeout_minutes: hardTimeoutMinutes
|
|
5174
|
+
terminal_bridge_hard_timeout_minutes: hardTimeoutMinutes,
|
|
5175
|
+
claude_hook_lease_id: renewedLease?.id ?? nativeTakeover.claude_hook_lease_id
|
|
4342
5176
|
},
|
|
4343
5177
|
updated_at: observedAt
|
|
4344
5178
|
};
|
|
@@ -4380,9 +5214,12 @@ function terminalBridgeApprovalCandidate({ executor, terminalControl, terminalSt
|
|
|
4380
5214
|
agent: executor.kind,
|
|
4381
5215
|
kind: stringValue(approval.prompt_kind) ?? "unknown",
|
|
4382
5216
|
command: stringValue(approval.command),
|
|
5217
|
+
tool_name: stringValue(approval.tool_name),
|
|
5218
|
+
request_detail: stringValue(approval.request_detail),
|
|
4383
5219
|
cwd: terminalControl.currentPath,
|
|
4384
5220
|
fingerprint,
|
|
4385
|
-
terminal_target: terminalControl.target
|
|
5221
|
+
terminal_target: terminalControl.target,
|
|
5222
|
+
decision_mode: stringValue(approval.decision_mode)
|
|
4386
5223
|
};
|
|
4387
5224
|
}
|
|
4388
5225
|
async function loadCodexTerminalContext({ conversation, nativeTakeover, options }) {
|
|
@@ -4595,7 +5432,7 @@ function runLockedCallback(options) {
|
|
|
4595
5432
|
const logPath = expandHome(options.log ?? logPathForStatePath(options.statePath));
|
|
4596
5433
|
const conversation = loadState(options.statePath);
|
|
4597
5434
|
const executor = executorForConversation(conversation);
|
|
4598
|
-
const message = options.retryPending === true
|
|
5435
|
+
const message = options.retryPending === true || options.preserveMessageId === true
|
|
4599
5436
|
? parseMessageJson(messageInput)
|
|
4600
5437
|
: extractStructuredMessage({
|
|
4601
5438
|
conversation,
|
|
@@ -4614,7 +5451,11 @@ function runLockedCallback(options) {
|
|
|
4614
5451
|
isRecord(callbackDelivery?.message) &&
|
|
4615
5452
|
callbackDelivery.message.id === message.id &&
|
|
4616
5453
|
["pending", "failed"].includes(String(callbackDelivery.status ?? ""));
|
|
4617
|
-
|
|
5454
|
+
const duplicateMessage = isDuplicateMessage(existingEvents, message);
|
|
5455
|
+
const recoveringTerminalCompletion = options.recoverTerminalCompletion === true &&
|
|
5456
|
+
duplicateMessage &&
|
|
5457
|
+
isWaitingForAgent(conversation.status);
|
|
5458
|
+
if (duplicateMessage && !retryingPending && !recoveringTerminalCompletion) {
|
|
4618
5459
|
runtimeLog("info", "callback_duplicate", {
|
|
4619
5460
|
conversation_id: conversation.conversation_id,
|
|
4620
5461
|
agent: executor.kind,
|
|
@@ -4636,15 +5477,23 @@ function runLockedCallback(options) {
|
|
|
4636
5477
|
}
|
|
4637
5478
|
const closeTerminalBridgeOnDone = message.type === "done" &&
|
|
4638
5479
|
options.closeTerminalBridgeOnDone === true;
|
|
5480
|
+
const trackCallbackDelivery = closeTerminalBridgeOnDone ||
|
|
5481
|
+
options.trackCallbackDelivery === true ||
|
|
5482
|
+
callbackDelivery?.track_delivery === true;
|
|
4639
5483
|
const requiresDelivery = Boolean(options.gatewayMethod) || options.recordOnly !== true;
|
|
4640
5484
|
const deliveryAttempt = Number(callbackDelivery?.attempts ?? 0) + 1;
|
|
4641
5485
|
let nextConversation = retryingPending
|
|
4642
5486
|
? conversation
|
|
4643
5487
|
: applyMessageToConversation(conversation, message);
|
|
4644
|
-
|
|
5488
|
+
const storedFinalStatus = stringValue(callbackDelivery?.final_status);
|
|
5489
|
+
const finalStatus = storedFinalStatus &&
|
|
5490
|
+
CONVERSATION_STATUSES.has(storedFinalStatus)
|
|
5491
|
+
? storedFinalStatus
|
|
5492
|
+
: nextConversation.status;
|
|
5493
|
+
if (!retryingPending && !recoveringTerminalCompletion) {
|
|
4645
5494
|
appendEvent(logPath, messageEvent(message));
|
|
4646
5495
|
}
|
|
4647
|
-
if (
|
|
5496
|
+
if (trackCallbackDelivery && requiresDelivery) {
|
|
4648
5497
|
const now = new Date().toISOString();
|
|
4649
5498
|
nextConversation = {
|
|
4650
5499
|
...nextConversation,
|
|
@@ -4659,7 +5508,9 @@ function runLockedCallback(options) {
|
|
|
4659
5508
|
gateway_session: options.gatewaySession ?? options.openclawSession ?? conversation.openclaw_session,
|
|
4660
5509
|
gateway_url: options.gatewayUrl ?? conversation.gateway_url,
|
|
4661
5510
|
openclaw_bin: options.openclawBin ?? conversation.openclaw_bin,
|
|
4662
|
-
close_terminal_bridge_on_done:
|
|
5511
|
+
close_terminal_bridge_on_done: closeTerminalBridgeOnDone,
|
|
5512
|
+
track_delivery: true,
|
|
5513
|
+
final_status: finalStatus
|
|
4663
5514
|
},
|
|
4664
5515
|
updated_at: now
|
|
4665
5516
|
};
|
|
@@ -4712,12 +5563,17 @@ function runLockedCallback(options) {
|
|
|
4712
5563
|
});
|
|
4713
5564
|
const deliveredAt = new Date().toISOString();
|
|
4714
5565
|
let deliveredConversation = nextConversation;
|
|
4715
|
-
if (
|
|
5566
|
+
if (trackCallbackDelivery) {
|
|
5567
|
+
const deliveredStatus = closeTerminalBridgeOnDone ? "closed" : finalStatus;
|
|
4716
5568
|
deliveredConversation = {
|
|
4717
5569
|
...nextConversation,
|
|
4718
|
-
status:
|
|
4719
|
-
|
|
4720
|
-
|
|
5570
|
+
status: deliveredStatus,
|
|
5571
|
+
...(closeTerminalBridgeOnDone
|
|
5572
|
+
? {
|
|
5573
|
+
closed_at: deliveredAt,
|
|
5574
|
+
close_reason: "terminal bridge task completed"
|
|
5575
|
+
}
|
|
5576
|
+
: {}),
|
|
4721
5577
|
callback_delivery: {
|
|
4722
5578
|
...(isRecord(nextConversation.callback_delivery) ? nextConversation.callback_delivery : {}),
|
|
4723
5579
|
status: "delivered",
|
|
@@ -4734,7 +5590,7 @@ function runLockedCallback(options) {
|
|
|
4734
5590
|
event: "callback_delivery_succeeded",
|
|
4735
5591
|
message_id: message.id,
|
|
4736
5592
|
attempt: deliveryAttempt,
|
|
4737
|
-
status:
|
|
5593
|
+
status: deliveredStatus
|
|
4738
5594
|
});
|
|
4739
5595
|
}
|
|
4740
5596
|
printJson({
|
|
@@ -4747,7 +5603,7 @@ function runLockedCallback(options) {
|
|
|
4747
5603
|
});
|
|
4748
5604
|
}
|
|
4749
5605
|
catch (error) {
|
|
4750
|
-
if (
|
|
5606
|
+
if (trackCallbackDelivery) {
|
|
4751
5607
|
const failedAt = new Date().toISOString();
|
|
4752
5608
|
const failedConversation = {
|
|
4753
5609
|
...nextConversation,
|
|
@@ -4919,6 +5775,8 @@ function acquireFileLock(lockPath, { timeoutMs = 5000, retryMs = 50 } = {}) {
|
|
|
4919
5775
|
while (true) {
|
|
4920
5776
|
try {
|
|
4921
5777
|
const fd = fs.openSync(lockPath, "wx");
|
|
5778
|
+
fs.writeFileSync(fd, `${process.pid}\n`, "utf8");
|
|
5779
|
+
fs.fsyncSync(fd);
|
|
4922
5780
|
fs.closeSync(fd);
|
|
4923
5781
|
return () => {
|
|
4924
5782
|
fs.rmSync(lockPath, { force: true });
|
|
@@ -4928,6 +5786,10 @@ function acquireFileLock(lockPath, { timeoutMs = 5000, retryMs = 50 } = {}) {
|
|
|
4928
5786
|
if (error.code !== "EEXIST") {
|
|
4929
5787
|
throw error;
|
|
4930
5788
|
}
|
|
5789
|
+
if (staleFileLock(lockPath)) {
|
|
5790
|
+
fs.rmSync(lockPath, { force: true });
|
|
5791
|
+
continue;
|
|
5792
|
+
}
|
|
4931
5793
|
if (Date.now() - started >= timeoutMs) {
|
|
4932
5794
|
throw new Error(`timed out waiting for file lock: ${lockPath}`);
|
|
4933
5795
|
}
|
|
@@ -4935,6 +5797,26 @@ function acquireFileLock(lockPath, { timeoutMs = 5000, retryMs = 50 } = {}) {
|
|
|
4935
5797
|
}
|
|
4936
5798
|
}
|
|
4937
5799
|
}
|
|
5800
|
+
function staleFileLock(lockPath) {
|
|
5801
|
+
try {
|
|
5802
|
+
const stat = fs.statSync(lockPath);
|
|
5803
|
+
const ownerText = fs.readFileSync(lockPath, "utf8").trim();
|
|
5804
|
+
const ownerPid = Number(ownerText);
|
|
5805
|
+
if (Number.isSafeInteger(ownerPid) && ownerPid > 1) {
|
|
5806
|
+
try {
|
|
5807
|
+
process.kill(ownerPid, 0);
|
|
5808
|
+
return false;
|
|
5809
|
+
}
|
|
5810
|
+
catch (error) {
|
|
5811
|
+
return isRecord(error) && error.code === "ESRCH";
|
|
5812
|
+
}
|
|
5813
|
+
}
|
|
5814
|
+
return Date.now() - stat.mtimeMs > 30_000;
|
|
5815
|
+
}
|
|
5816
|
+
catch (error) {
|
|
5817
|
+
return isRecord(error) && error.code === "ENOENT";
|
|
5818
|
+
}
|
|
5819
|
+
}
|
|
4938
5820
|
function sleepSync(ms) {
|
|
4939
5821
|
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
4940
5822
|
}
|
|
@@ -5497,6 +6379,7 @@ function markConversationStalled({ statePath, logPath, reason, detail = {} }) {
|
|
|
5497
6379
|
updated_at: now
|
|
5498
6380
|
};
|
|
5499
6381
|
saveState(statePath, stalledConversation);
|
|
6382
|
+
releaseClaudeHookLease(stalledConversation);
|
|
5500
6383
|
appendEvent(logPath, {
|
|
5501
6384
|
ts: now,
|
|
5502
6385
|
conversation_id: conversation.conversation_id,
|
|
@@ -6209,6 +7092,7 @@ function usage() {
|
|
|
6209
7092
|
agent-knock-knock recover --conversation <id> [--session <name>] [--all-proxy <url>]
|
|
6210
7093
|
agent-knock-knock close --conversation <id> [--reason <text>]
|
|
6211
7094
|
agent-knock-knock install-openclaw [--openclaw-bin <path>] [--skill-path <path>] [--skill-only] [--no-restart]
|
|
7095
|
+
agent-knock-knock install-claude-hooks [--settings-path <path>] [--executable-path <path>] [--dry-run]
|
|
6212
7096
|
agent-knock-knock doctor
|
|
6213
7097
|
agent-knock-knock agent takeover --agent codex --session-id <id> --strategy terminate_then_resume|terminal_control|fork [--create-conversation]
|
|
6214
7098
|
agent-knock-knock callback --state <file> --message-json <json> [--record-only]
|