@scotthuang/agent-knock-knock 0.2.46 → 0.2.48
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 +39 -0
- package/README.md +93 -55
- package/dist/src/approval-policy.d.ts +3 -1
- package/dist/src/approval-policy.js +18 -4
- 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 +1075 -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 +935 -0
- package/dist/src/claude-terminal-agent-adapter.js.map +1 -0
- package/dist/src/cli.js +2065 -316
- package/dist/src/cli.js.map +1 -1
- package/dist/src/openclaw-plugin-helpers.d.ts +49 -0
- package/dist/src/openclaw-plugin-helpers.js +246 -0
- package/dist/src/openclaw-plugin-helpers.js.map +1 -0
- package/dist/src/openclaw-plugin.js +98 -275
- package/dist/src/openclaw-plugin.js.map +1 -1
- package/dist/src/runtime-log.js +54 -2
- package/dist/src/runtime-log.js.map +1 -1
- package/dist/src/store.js +432 -12
- package/dist/src/store.js.map +1 -1
- package/dist/src/terminal-agent-adapter.d.ts +37 -0
- package/dist/src/terminal-agent-adapter.js.map +1 -1
- package/dist/src/terminal-agent-bridge.d.ts +51 -4
- package/dist/src/terminal-agent-bridge.js +326 -43
- 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/dist/src/terminal-control-provider.d.ts +3 -1
- package/dist/src/terminal-control-provider.js +41 -37
- package/dist/src/terminal-control-provider.js.map +1 -1
- package/dist/src/terminal-process-source.d.ts +12 -3
- package/dist/src/terminal-process-source.js +37 -6
- package/dist/src/terminal-process-source.js.map +1 -1
- package/openclaw.plugin.json +5 -5
- package/package.json +10 -5
- package/templates/openclaw-skills/agent-knock-knock/SKILL.md +19 -30
package/dist/src/cli.js
CHANGED
|
@@ -6,26 +6,53 @@ 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";
|
|
12
16
|
import { EXECUTOR_KINDS, acpxCommandForExecutor, executorDefinitionForKind, modelEnvForExecutor, normalizeModelForExecutor, proxyEnvForExecutor } from "./executors.js";
|
|
13
17
|
import { executorBootstrapPrompt } from "./bootstrap.js";
|
|
14
|
-
import { writeRuntimeLog } from "./runtime-log.js";
|
|
18
|
+
import { redactString, writeRuntimeLog } from "./runtime-log.js";
|
|
15
19
|
import { formatTranscript, readNdjsonLog } from "./transcript.js";
|
|
16
20
|
import { appendEvent, defaultStoreDir, listConversations, logPathForStatePath, loadConversationById, loadState, messageEvent, pathsForConversation, pathsForConversationDir, saveState, statePathForConversationId } from "./store.js";
|
|
17
21
|
import { planFork, planTakeover } from "./session-takeover-planner.js";
|
|
18
|
-
import { StaticTerminalControlProvider, TmuxTerminalControlProvider } from "./terminal-control-provider.js";
|
|
22
|
+
import { StaticTerminalControlProvider, TmuxTerminalControlProvider, terminalPaneContainsProcess } from "./terminal-control-provider.js";
|
|
19
23
|
import { parseTerminalConversationId } from "./terminal-agent-adapter.js";
|
|
20
24
|
import { createProductionTerminalAgentRegistry } from "./terminal-agent-registry.js";
|
|
21
25
|
import { StaticTerminalProcessSource, SystemTerminalProcessSource } from "./terminal-process-source.js";
|
|
22
26
|
import { TerminalAgentBridge } from "./terminal-agent-bridge.js";
|
|
27
|
+
import { evaluateApprovalPolicy } from "./approval-policy.js";
|
|
23
28
|
const DEFAULT_IDLE_TIMEOUT_MINUTES = 10080;
|
|
24
29
|
const DEFAULT_AGENT_TIMEOUT_MINUTES = 60;
|
|
25
30
|
const DEFAULT_AGENT_HARD_TIMEOUT_MINUTES = 720;
|
|
26
31
|
const DEFAULT_MONITOR_POLL_INTERVAL_MS = 5000;
|
|
27
32
|
const CALLBACK_RETRY_DELAYS_MS = [5000, 15000, 60000, 60000];
|
|
28
|
-
const
|
|
33
|
+
const TERMINAL_BRIDGE_MONITOR_LOCK_VERSION = 1;
|
|
34
|
+
const MINIMUM_NODE_VERSION = "22.14.0";
|
|
35
|
+
const PRIVATE_LOCK_FILE_MODE = 0o600;
|
|
36
|
+
const NO_FOLLOW_FLAG = typeof fs.constants.O_NOFOLLOW === "number"
|
|
37
|
+
? fs.constants.O_NOFOLLOW
|
|
38
|
+
: 0;
|
|
39
|
+
const DEFAULT_CODEX_ACPX_AGENT_COMMAND = "npx -y @agentclientprotocol/codex-acp@1.1.7";
|
|
40
|
+
const CONVERSATION_STATUSES = new Set([
|
|
41
|
+
"created",
|
|
42
|
+
"running",
|
|
43
|
+
"waiting_for_agent",
|
|
44
|
+
"waiting_for_openclaw",
|
|
45
|
+
"idle",
|
|
46
|
+
"stalled",
|
|
47
|
+
"needs_recovery",
|
|
48
|
+
"needs_model_selection",
|
|
49
|
+
"callback_pending",
|
|
50
|
+
"callback_failed",
|
|
51
|
+
"failed",
|
|
52
|
+
"closed",
|
|
53
|
+
"cancelled",
|
|
54
|
+
"cancelling"
|
|
55
|
+
]);
|
|
29
56
|
class InlineCodexSessionAdapter {
|
|
30
57
|
threads;
|
|
31
58
|
processes;
|
|
@@ -82,7 +109,13 @@ catch (error) {
|
|
|
82
109
|
process.exit(1);
|
|
83
110
|
}
|
|
84
111
|
async function runCommand(commandName, options) {
|
|
85
|
-
if (commandName === "
|
|
112
|
+
if (commandName === "help" || commandName === "--help" || commandName === "-h") {
|
|
113
|
+
usage();
|
|
114
|
+
}
|
|
115
|
+
else if (commandName === "version" || commandName === "--version" || commandName === "-v") {
|
|
116
|
+
printVersion();
|
|
117
|
+
}
|
|
118
|
+
else if (commandName === "new") {
|
|
86
119
|
runNew(options);
|
|
87
120
|
}
|
|
88
121
|
else if (commandName === "record") {
|
|
@@ -115,6 +148,9 @@ async function runCommand(commandName, options) {
|
|
|
115
148
|
else if (commandName === "renew") {
|
|
116
149
|
await runRenew(options);
|
|
117
150
|
}
|
|
151
|
+
else if (commandName === "reconcile-monitors") {
|
|
152
|
+
await runReconcileMonitors(options);
|
|
153
|
+
}
|
|
118
154
|
else if (commandName === "recover") {
|
|
119
155
|
runRecover(options);
|
|
120
156
|
}
|
|
@@ -127,6 +163,12 @@ async function runCommand(commandName, options) {
|
|
|
127
163
|
else if (commandName === "install-openclaw") {
|
|
128
164
|
runInstallOpenClaw(options);
|
|
129
165
|
}
|
|
166
|
+
else if (commandName === "install-claude-hooks") {
|
|
167
|
+
runInstallClaudeHooks(options);
|
|
168
|
+
}
|
|
169
|
+
else if (commandName === "claude-hook") {
|
|
170
|
+
await runClaudeHook(options);
|
|
171
|
+
}
|
|
130
172
|
else if (commandName === "doctor") {
|
|
131
173
|
runDoctor(options);
|
|
132
174
|
}
|
|
@@ -197,6 +239,88 @@ function runInstallOpenClaw(options) {
|
|
|
197
239
|
: "Agent Knock Knock is installed. Try: AKK list"
|
|
198
240
|
});
|
|
199
241
|
}
|
|
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
|
+
async function runClaudeHook(options) {
|
|
271
|
+
const rawInput = fs.readFileSync(0, "utf8");
|
|
272
|
+
let input;
|
|
273
|
+
try {
|
|
274
|
+
input = JSON.parse(rawInput);
|
|
275
|
+
}
|
|
276
|
+
catch {
|
|
277
|
+
throw new Error("Claude hook input must be valid JSON");
|
|
278
|
+
}
|
|
279
|
+
const agentRows = loadClaudeAgentRows(options);
|
|
280
|
+
const claudePid = inferClaudeAncestorPid(agentRows);
|
|
281
|
+
const store = createClaudeHookStore(options);
|
|
282
|
+
const record = store.record(input, {
|
|
283
|
+
...(claudePid === undefined ? {} : { claudePid })
|
|
284
|
+
});
|
|
285
|
+
if (record.event.input.hook_event_name !== "PermissionRequest" || !record.permission) {
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
const permission = record.permission;
|
|
289
|
+
const requestedTimeout = options.permissionWaitTimeoutMs ?? options.timeoutMs;
|
|
290
|
+
const timeoutMs = requestedTimeout === undefined
|
|
291
|
+
? undefined
|
|
292
|
+
: Math.max(0, Number(requestedTimeout));
|
|
293
|
+
try {
|
|
294
|
+
const decision = await store.waitForPermissionDecision({
|
|
295
|
+
sessionId: permission.sessionId,
|
|
296
|
+
requestId: permission.requestId,
|
|
297
|
+
fingerprint: permission.fingerprint,
|
|
298
|
+
conversationId: permission.conversationId,
|
|
299
|
+
messageId: permission.messageId,
|
|
300
|
+
...(timeoutMs === undefined ? {} : { timeoutMs })
|
|
301
|
+
});
|
|
302
|
+
process.stdout.write(`${JSON.stringify(decision?.hookOutput ?? claudePermissionHookOutput({
|
|
303
|
+
behavior: "deny",
|
|
304
|
+
interrupt: false,
|
|
305
|
+
message: "Agent Knock Knock approval timed out. Review the request and retry the task."
|
|
306
|
+
}))}\n`);
|
|
307
|
+
}
|
|
308
|
+
catch (error) {
|
|
309
|
+
if (error instanceof ClaudeHookStoreError && [
|
|
310
|
+
"PERMISSION_EXPIRED",
|
|
311
|
+
"PERMISSION_CONSUMED",
|
|
312
|
+
"PERMISSION_ALREADY_DECIDED"
|
|
313
|
+
].includes(error.code)) {
|
|
314
|
+
process.stdout.write(`${JSON.stringify(claudePermissionHookOutput({
|
|
315
|
+
behavior: "deny",
|
|
316
|
+
interrupt: false,
|
|
317
|
+
message: "Agent Knock Knock approval expired before a safe decision was received."
|
|
318
|
+
}))}\n`);
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
throw error;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
200
324
|
function installOpenClawPlugin(openclawBin, root) {
|
|
201
325
|
const linked = spawnSync(openclawBin, ["plugins", "install", "--link", root], {
|
|
202
326
|
encoding: "utf8",
|
|
@@ -222,7 +346,17 @@ function installOpenClawPlugin(openclawBin, root) {
|
|
|
222
346
|
}
|
|
223
347
|
function runDoctor(options) {
|
|
224
348
|
const commands = ["node", "openclaw", "acpx", "codex", "claude", "cursor"];
|
|
225
|
-
const checks = commands.map((commandName) =>
|
|
349
|
+
const checks = commands.map((commandName) => {
|
|
350
|
+
const check = executableCheck(commandName);
|
|
351
|
+
return commandName === "node"
|
|
352
|
+
? {
|
|
353
|
+
...check,
|
|
354
|
+
version: process.versions.node,
|
|
355
|
+
version_supported: versionAtLeast(process.versions.node, MINIMUM_NODE_VERSION),
|
|
356
|
+
minimum_version: MINIMUM_NODE_VERSION
|
|
357
|
+
}
|
|
358
|
+
: check;
|
|
359
|
+
});
|
|
226
360
|
const root = packageRootDir();
|
|
227
361
|
const packageFiles = [
|
|
228
362
|
"dist/src/cli.js",
|
|
@@ -238,22 +372,43 @@ function runDoctor(options) {
|
|
|
238
372
|
});
|
|
239
373
|
const requiredOk = checks
|
|
240
374
|
.filter((check) => ["node", "openclaw", "acpx"].includes(check.command))
|
|
241
|
-
.every((check) => check.available
|
|
375
|
+
.every((check) => check.available &&
|
|
376
|
+
(check.command !== "node" ||
|
|
377
|
+
("version_supported" in check && check.version_supported === true)));
|
|
242
378
|
const agentOk = checks
|
|
243
379
|
.filter((check) => ["codex", "claude", "cursor"].includes(check.command))
|
|
244
380
|
.some((check) => check.available);
|
|
245
381
|
const filesOk = packageFiles.every((check) => check.exists);
|
|
382
|
+
const ok = requiredOk && agentOk && filesOk;
|
|
246
383
|
printJson({
|
|
247
|
-
ok
|
|
384
|
+
ok,
|
|
248
385
|
package_root: root,
|
|
249
386
|
checks,
|
|
250
387
|
package_files: packageFiles,
|
|
251
388
|
notes: [
|
|
252
|
-
|
|
389
|
+
`Node.js ${MINIMUM_NODE_VERSION}+, openclaw, and acpx are required.`,
|
|
253
390
|
"At least one local coding agent command should be available: codex, claude, or cursor."
|
|
254
391
|
],
|
|
255
392
|
options
|
|
256
393
|
});
|
|
394
|
+
if (!ok) {
|
|
395
|
+
process.exitCode = 1;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
function versionAtLeast(version, minimum) {
|
|
399
|
+
const parsed = version.split(".").slice(0, 3).map((part) => Number.parseInt(part, 10));
|
|
400
|
+
const required = minimum.split(".").slice(0, 3).map((part) => Number.parseInt(part, 10));
|
|
401
|
+
if (parsed.length !== 3 ||
|
|
402
|
+
required.length !== 3 ||
|
|
403
|
+
[...parsed, ...required].some((part) => !Number.isInteger(part) || part < 0)) {
|
|
404
|
+
return false;
|
|
405
|
+
}
|
|
406
|
+
for (let index = 0; index < 3; index += 1) {
|
|
407
|
+
if (parsed[index] !== required[index]) {
|
|
408
|
+
return parsed[index] > required[index];
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
return true;
|
|
257
412
|
}
|
|
258
413
|
async function runAgent(options) {
|
|
259
414
|
const agentCommand = required(options.agentCommand, "agent subcommand is required: takeover");
|
|
@@ -397,6 +552,7 @@ async function runAgentTakeover(options) {
|
|
|
397
552
|
options,
|
|
398
553
|
takeoverMatchKind: "terminal_control",
|
|
399
554
|
terminalControl: target.terminalControl,
|
|
555
|
+
terminalAgentPid: target.pid,
|
|
400
556
|
needsBootstrap: false
|
|
401
557
|
});
|
|
402
558
|
return {
|
|
@@ -558,7 +714,11 @@ function selectTerminateTarget({ plan, session, activeSessions, expectedPid, all
|
|
|
558
714
|
}
|
|
559
715
|
async function listActiveSessionsWithTerminalControl(provider, options, terminalProvider = createTerminalControlProvider(options)) {
|
|
560
716
|
const activeSessions = await provider.listActiveSessions();
|
|
561
|
-
|
|
717
|
+
const activePids = new Set(activeSessions.map((session) => session.pid));
|
|
718
|
+
const processTree = activePids.size > 0
|
|
719
|
+
? await createTerminalProcessSource(options).listProcessSnapshots((snapshot) => activePids.has(snapshot.pid), { includeCwd: false, includeAncestors: true })
|
|
720
|
+
: [];
|
|
721
|
+
return createTerminalAgentBridge(options, terminalProvider).attachProcesses(provider.agent, activeSessions, { processTree });
|
|
562
722
|
}
|
|
563
723
|
function createTerminalControlProvider(options) {
|
|
564
724
|
if (options.terminalsJson || options.terminalScreensJson || options.processesJson) {
|
|
@@ -575,9 +735,107 @@ function createTerminalProcessSource(options) {
|
|
|
575
735
|
}
|
|
576
736
|
return new SystemTerminalProcessSource();
|
|
577
737
|
}
|
|
738
|
+
function createClaudeHookStore(options = {}) {
|
|
739
|
+
const configuredRoot = stringValue(options.claudeHookStoreDir);
|
|
740
|
+
return new ClaudeHookStore({
|
|
741
|
+
rootDir: expandHome(configuredRoot ?? defaultClaudeHookStoreDir())
|
|
742
|
+
});
|
|
743
|
+
}
|
|
744
|
+
function loadClaudeAgentRows(options = {}) {
|
|
745
|
+
let value;
|
|
746
|
+
if (options.claudeAgentsJson !== undefined) {
|
|
747
|
+
value = typeof options.claudeAgentsJson === "string"
|
|
748
|
+
? parseJsonOption(options.claudeAgentsJson, "--claude-agents-json")
|
|
749
|
+
: options.claudeAgentsJson;
|
|
750
|
+
}
|
|
751
|
+
else if (options.processesJson || options.terminalsJson || options.terminalScreensJson) {
|
|
752
|
+
return [];
|
|
753
|
+
}
|
|
754
|
+
else {
|
|
755
|
+
const claudeExecutable = resolveOptionalExecutable("claude");
|
|
756
|
+
if (!claudeExecutable) {
|
|
757
|
+
return [];
|
|
758
|
+
}
|
|
759
|
+
const result = spawnSync(claudeExecutable, ["agents", "--json", "--all"], {
|
|
760
|
+
encoding: "utf8",
|
|
761
|
+
maxBuffer: 1024 * 1024 * 10,
|
|
762
|
+
timeout: 10_000
|
|
763
|
+
});
|
|
764
|
+
if (result.error || result.status !== 0) {
|
|
765
|
+
runtimeLog("warn", "claude_agents_list_failed", {
|
|
766
|
+
status: result.status ?? null,
|
|
767
|
+
error: result.error?.message,
|
|
768
|
+
stderr: textSummary(cleanProcessText(result.stderr))
|
|
769
|
+
});
|
|
770
|
+
return [];
|
|
771
|
+
}
|
|
772
|
+
try {
|
|
773
|
+
value = JSON.parse(result.stdout);
|
|
774
|
+
}
|
|
775
|
+
catch {
|
|
776
|
+
runtimeLog("warn", "claude_agents_list_invalid_json", {
|
|
777
|
+
stdout: textSummary(result.stdout)
|
|
778
|
+
});
|
|
779
|
+
return [];
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
const rows = Array.isArray(value)
|
|
783
|
+
? value
|
|
784
|
+
: isRecord(value) && Array.isArray(value.agents)
|
|
785
|
+
? value.agents
|
|
786
|
+
: [];
|
|
787
|
+
return rows.flatMap((row) => {
|
|
788
|
+
if (!isRecord(row) || !Number.isInteger(Number(row.pid))) {
|
|
789
|
+
return [];
|
|
790
|
+
}
|
|
791
|
+
return [{
|
|
792
|
+
pid: Number(row.pid),
|
|
793
|
+
...(stringValue(row.cwd) ? { cwd: stringValue(row.cwd) } : {}),
|
|
794
|
+
...(stringValue(row.kind) ? { kind: stringValue(row.kind) } : {}),
|
|
795
|
+
...(stringValue(row.sessionId) ? { sessionId: stringValue(row.sessionId) } : {}),
|
|
796
|
+
...(stringValue(row.status) ? { status: stringValue(row.status) } : {})
|
|
797
|
+
}];
|
|
798
|
+
});
|
|
799
|
+
}
|
|
800
|
+
function inferClaudeAncestorPid(agentRows, startingPid = process.ppid) {
|
|
801
|
+
const interactivePids = new Set(agentRows
|
|
802
|
+
.filter((row) => row.kind === undefined || row.kind === "interactive")
|
|
803
|
+
.map((row) => row.pid)
|
|
804
|
+
.filter((pid) => Number.isInteger(pid)));
|
|
805
|
+
let pid = startingPid;
|
|
806
|
+
const visited = new Set();
|
|
807
|
+
for (let depth = 0; depth < 16 && Number.isInteger(pid) && pid > 1 && !visited.has(pid); depth += 1) {
|
|
808
|
+
visited.add(pid);
|
|
809
|
+
const processRow = spawnSync("ps", ["-p", String(pid), "-o", "pid=,ppid=,command="], {
|
|
810
|
+
encoding: "utf8",
|
|
811
|
+
timeout: 2_000
|
|
812
|
+
});
|
|
813
|
+
if (processRow.error || processRow.status !== 0) {
|
|
814
|
+
return undefined;
|
|
815
|
+
}
|
|
816
|
+
const match = /^\s*(\d+)\s+(\d+)\s+(.+)$/u.exec(String(processRow.stdout).trim());
|
|
817
|
+
if (!match) {
|
|
818
|
+
return undefined;
|
|
819
|
+
}
|
|
820
|
+
const currentPid = Number(match[1]);
|
|
821
|
+
const parentPid = Number(match[2]);
|
|
822
|
+
const commandText = match[3];
|
|
823
|
+
if (interactivePids.has(currentPid) || isClaudeProcessCommand(commandText)) {
|
|
824
|
+
return currentPid;
|
|
825
|
+
}
|
|
826
|
+
pid = parentPid;
|
|
827
|
+
}
|
|
828
|
+
return undefined;
|
|
829
|
+
}
|
|
830
|
+
function isClaudeProcessCommand(commandText) {
|
|
831
|
+
const executable = commandText.trim().split(/\s+/u, 1)[0];
|
|
832
|
+
return path.basename(executable) === "claude" ||
|
|
833
|
+
/[\\/]\.local[\\/]share[\\/]claude[\\/]versions[\\/][^\\/\s]+$/u.test(executable);
|
|
834
|
+
}
|
|
578
835
|
function createRuntimeTerminalAgentRegistry(options) {
|
|
579
836
|
return createProductionTerminalAgentRegistry({
|
|
580
|
-
overrides: [
|
|
837
|
+
overrides: [
|
|
838
|
+
createCodexTerminalAgentAdapter({
|
|
581
839
|
async detectDurableCompletion(request) {
|
|
582
840
|
const runtime = isRecord(request.context) ? request.context : undefined;
|
|
583
841
|
const conversation = runtime?.conversation;
|
|
@@ -611,13 +869,44 @@ function createRuntimeTerminalAgentRegistry(options) {
|
|
|
611
869
|
}
|
|
612
870
|
: undefined;
|
|
613
871
|
}
|
|
614
|
-
})
|
|
872
|
+
}),
|
|
873
|
+
createClaudeTerminalAgentAdapter({
|
|
874
|
+
agentRows: loadClaudeAgentRows(options),
|
|
875
|
+
hookStore: createClaudeHookStore(options),
|
|
876
|
+
trustedTokenjuiceLaunchers: loadTrustedClaudeTokenjuiceLaunchers(expandHome(options.claudeSettingsPath ?? defaultClaudeSettingsPath()))
|
|
877
|
+
})
|
|
878
|
+
]
|
|
615
879
|
});
|
|
616
880
|
}
|
|
617
|
-
function createTerminalAgentBridge(options, terminalProvider = createTerminalControlProvider(options)) {
|
|
881
|
+
function createTerminalAgentBridge(options, terminalProvider = createTerminalControlProvider(options), registry = createRuntimeTerminalAgentRegistry(options)) {
|
|
882
|
+
const processSource = createTerminalProcessSource(options);
|
|
618
883
|
return new TerminalAgentBridge({
|
|
619
|
-
registry
|
|
620
|
-
terminalProvider
|
|
884
|
+
registry,
|
|
885
|
+
terminalProvider,
|
|
886
|
+
async verifyIdentity({ agent, pid, terminalControl }) {
|
|
887
|
+
const adapter = registry.require(agent);
|
|
888
|
+
const snapshots = await processSource.listProcessSnapshots(undefined, { includeCwd: false });
|
|
889
|
+
const snapshot = snapshots.find((candidate) => candidate.pid === pid);
|
|
890
|
+
if (!snapshot || !adapter.classifyProcess(snapshot)) {
|
|
891
|
+
throw new Error(`terminal conversation agent ${agent} with pid ${pid} is no longer active`);
|
|
892
|
+
}
|
|
893
|
+
const panes = await terminalProvider.listPanes();
|
|
894
|
+
const pane = panes.find((candidate) => candidate.kind === terminalControl.kind &&
|
|
895
|
+
candidate.target === terminalControl.target &&
|
|
896
|
+
candidate.panePid === terminalControl.panePid);
|
|
897
|
+
if (!pane || !terminalPaneContainsProcess(snapshot, pane, snapshots)) {
|
|
898
|
+
throw new Error(`terminal conversation agent ${agent} with pid ${pid} no longer belongs to pane ${terminalControl.target}`);
|
|
899
|
+
}
|
|
900
|
+
return {
|
|
901
|
+
terminalControl: {
|
|
902
|
+
...terminalControl,
|
|
903
|
+
socketPath: pane.socketPath,
|
|
904
|
+
panePid: pane.panePid,
|
|
905
|
+
currentCommand: pane.currentCommand,
|
|
906
|
+
currentPath: pane.currentPath
|
|
907
|
+
}
|
|
908
|
+
};
|
|
909
|
+
}
|
|
621
910
|
});
|
|
622
911
|
}
|
|
623
912
|
function planTerminalControlTakeover(session, activeSessions) {
|
|
@@ -695,6 +984,134 @@ function terminalControlFromTakeover(nativeTakeover) {
|
|
|
695
984
|
]
|
|
696
985
|
};
|
|
697
986
|
}
|
|
987
|
+
function terminalRuntimeIdentityForConversation(conversation, terminalControl) {
|
|
988
|
+
const nativeTakeover = isRecord(conversation?.native_session_takeover)
|
|
989
|
+
? conversation.native_session_takeover
|
|
990
|
+
: undefined;
|
|
991
|
+
const nativeSessionId = stringValue(nativeTakeover?.native_session_id);
|
|
992
|
+
const terminalIdentity = parseTerminalConversationId(nativeSessionId);
|
|
993
|
+
const explicitSessionId = stringValue(nativeTakeover?.terminal_agent_session_id) ??
|
|
994
|
+
(terminalIdentity ? undefined : nativeSessionId);
|
|
995
|
+
return {
|
|
996
|
+
pid: Number.isInteger(Number(nativeTakeover?.terminal_agent_pid))
|
|
997
|
+
? Number(nativeTakeover?.terminal_agent_pid)
|
|
998
|
+
: terminalIdentity?.pid,
|
|
999
|
+
sessionId: explicitSessionId,
|
|
1000
|
+
cwd: stringValue(nativeTakeover?.source_cwd) ?? terminalControl.currentPath,
|
|
1001
|
+
conversationId: stringValue(conversation?.conversation_id),
|
|
1002
|
+
messageId: stringValue(nativeTakeover?.terminal_bridge_message_id),
|
|
1003
|
+
terminalTarget: terminalControl.target
|
|
1004
|
+
};
|
|
1005
|
+
}
|
|
1006
|
+
async function migrateLegacyTerminalAgentIdentity({ conversation, statePath, logPath, options }) {
|
|
1007
|
+
const nativeTakeover = isRecord(conversation?.native_session_takeover)
|
|
1008
|
+
? conversation.native_session_takeover
|
|
1009
|
+
: undefined;
|
|
1010
|
+
const terminalControl = terminalControlFromTakeover(nativeTakeover);
|
|
1011
|
+
if (!nativeTakeover || !terminalControl) {
|
|
1012
|
+
return conversation;
|
|
1013
|
+
}
|
|
1014
|
+
const runtime = terminalRuntimeIdentityForConversation(conversation, terminalControl);
|
|
1015
|
+
if (Number.isInteger(runtime.pid) && Number(runtime.pid) > 0) {
|
|
1016
|
+
return conversation;
|
|
1017
|
+
}
|
|
1018
|
+
const executor = executorForConversation(conversation);
|
|
1019
|
+
const nativeSessionId = stringValue(nativeTakeover.native_session_id);
|
|
1020
|
+
if (executor.kind !== "codex" ||
|
|
1021
|
+
!nativeSessionId ||
|
|
1022
|
+
parseTerminalConversationId(nativeSessionId)) {
|
|
1023
|
+
return conversation;
|
|
1024
|
+
}
|
|
1025
|
+
let matchedProcess;
|
|
1026
|
+
try {
|
|
1027
|
+
const registry = createRuntimeTerminalAgentRegistry(options);
|
|
1028
|
+
const adapter = registry.require("codex");
|
|
1029
|
+
const snapshots = await createTerminalProcessSource(options).listProcessSnapshots((snapshot) => adapter.classifyProcess(snapshot) !== undefined, { includeAncestors: true });
|
|
1030
|
+
const panes = await createTerminalControlProvider(options).listPanes();
|
|
1031
|
+
const matchingPanes = panes.filter((pane) => pane.kind === terminalControl.kind &&
|
|
1032
|
+
pane.target === terminalControl.target &&
|
|
1033
|
+
pane.panePid === terminalControl.panePid);
|
|
1034
|
+
if (matchingPanes.length !== 1) {
|
|
1035
|
+
return conversation;
|
|
1036
|
+
}
|
|
1037
|
+
const candidates = snapshots.flatMap((snapshot) => {
|
|
1038
|
+
const classified = adapter.classifyProcess(snapshot);
|
|
1039
|
+
return classified ? [{ ...classified, agent: "codex" }] : [];
|
|
1040
|
+
});
|
|
1041
|
+
const matches = candidates.filter((candidate) => candidate.sessionId === nativeSessionId &&
|
|
1042
|
+
terminalPaneContainsProcess(candidate, matchingPanes[0], snapshots));
|
|
1043
|
+
if (matches.length !== 1) {
|
|
1044
|
+
return conversation;
|
|
1045
|
+
}
|
|
1046
|
+
matchedProcess = matches[0];
|
|
1047
|
+
}
|
|
1048
|
+
catch (error) {
|
|
1049
|
+
runtimeLog("warn", "legacy_terminal_agent_identity_migration_failed", {
|
|
1050
|
+
conversation_id: conversation.conversation_id,
|
|
1051
|
+
terminal_target: terminalControl.target,
|
|
1052
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
1053
|
+
});
|
|
1054
|
+
return conversation;
|
|
1055
|
+
}
|
|
1056
|
+
if (!matchedProcess) {
|
|
1057
|
+
return conversation;
|
|
1058
|
+
}
|
|
1059
|
+
const releaseLock = acquireFileLock(`${statePath}.lock`);
|
|
1060
|
+
let migratedConversation = conversation;
|
|
1061
|
+
let migrated = false;
|
|
1062
|
+
try {
|
|
1063
|
+
const current = loadState(statePath);
|
|
1064
|
+
const currentTakeover = isRecord(current.native_session_takeover)
|
|
1065
|
+
? current.native_session_takeover
|
|
1066
|
+
: undefined;
|
|
1067
|
+
const currentControl = terminalControlFromTakeover(currentTakeover);
|
|
1068
|
+
if (!currentTakeover || !currentControl) {
|
|
1069
|
+
return current;
|
|
1070
|
+
}
|
|
1071
|
+
const currentRuntime = terminalRuntimeIdentityForConversation(current, currentControl);
|
|
1072
|
+
if (Number.isInteger(currentRuntime.pid) && Number(currentRuntime.pid) > 0) {
|
|
1073
|
+
return current;
|
|
1074
|
+
}
|
|
1075
|
+
if (currentTakeover.native_session_id !== nativeSessionId ||
|
|
1076
|
+
currentControl.target !== terminalControl.target ||
|
|
1077
|
+
currentControl.socketPath !== terminalControl.socketPath ||
|
|
1078
|
+
currentControl.panePid !== terminalControl.panePid) {
|
|
1079
|
+
return current;
|
|
1080
|
+
}
|
|
1081
|
+
const migratedAt = new Date().toISOString();
|
|
1082
|
+
migratedConversation = {
|
|
1083
|
+
...current,
|
|
1084
|
+
native_session_takeover: {
|
|
1085
|
+
...currentTakeover,
|
|
1086
|
+
terminal_agent_pid: matchedProcess.pid,
|
|
1087
|
+
terminal_agent_session_id: matchedProcess.sessionId,
|
|
1088
|
+
terminal_agent_identity_migrated_at: migratedAt
|
|
1089
|
+
},
|
|
1090
|
+
updated_at: migratedAt
|
|
1091
|
+
};
|
|
1092
|
+
saveState(statePath, migratedConversation);
|
|
1093
|
+
migrated = true;
|
|
1094
|
+
}
|
|
1095
|
+
finally {
|
|
1096
|
+
releaseLock();
|
|
1097
|
+
}
|
|
1098
|
+
if (migrated) {
|
|
1099
|
+
appendEvent(logPath, {
|
|
1100
|
+
ts: new Date().toISOString(),
|
|
1101
|
+
conversation_id: migratedConversation.conversation_id,
|
|
1102
|
+
event: "terminal_agent_identity_migrated",
|
|
1103
|
+
terminal_target: terminalControl.target,
|
|
1104
|
+
terminal_agent_pid: matchedProcess.pid,
|
|
1105
|
+
native_session_id: nativeSessionId
|
|
1106
|
+
});
|
|
1107
|
+
runtimeLog("info", "terminal_agent_identity_migrated", {
|
|
1108
|
+
conversation_id: migratedConversation.conversation_id,
|
|
1109
|
+
terminal_target: terminalControl.target,
|
|
1110
|
+
terminal_agent_pid: matchedProcess.pid
|
|
1111
|
+
});
|
|
1112
|
+
}
|
|
1113
|
+
return migratedConversation;
|
|
1114
|
+
}
|
|
698
1115
|
function isTerminalControlCapability(value) {
|
|
699
1116
|
return typeof value === "string" && [
|
|
700
1117
|
"screen_status",
|
|
@@ -798,7 +1215,7 @@ function createForkConversation({ agent, strategy, session, contextPackage, fork
|
|
|
798
1215
|
next: `Use AKK send ${forkedConversation.conversation_id}: <message> to start the forked ${agent} session with the approved summary.`
|
|
799
1216
|
};
|
|
800
1217
|
}
|
|
801
|
-
function createNativeSessionConversation({ agent, strategy, session, modelInfo, options, takeoverMatchKind = strategy, terminalControl = undefined, needsBootstrap = true }) {
|
|
1218
|
+
function createNativeSessionConversation({ agent, strategy, session, modelInfo, options, takeoverMatchKind = strategy, terminalControl = undefined, terminalAgentPid = undefined, needsBootstrap = true }) {
|
|
802
1219
|
const workspace = session.cwd;
|
|
803
1220
|
const storeDir = expandHome(options.storeDir ?? options.logDir ?? defaultStoreDir(workspace));
|
|
804
1221
|
cleanupIdleConversations(storeDir, options);
|
|
@@ -847,6 +1264,7 @@ function createNativeSessionConversation({ agent, strategy, session, modelInfo,
|
|
|
847
1264
|
native_session_takeover: {
|
|
848
1265
|
agent,
|
|
849
1266
|
native_session_id: session.id,
|
|
1267
|
+
terminal_agent_pid: terminalAgentPid,
|
|
850
1268
|
source_cwd: session.cwd,
|
|
851
1269
|
source_title: session.title,
|
|
852
1270
|
strategy,
|
|
@@ -1094,7 +1512,7 @@ function runDelegate(options) {
|
|
|
1094
1512
|
throw new Error(cleanProcessText(ensureSession.stderr || ensureSession.stdout || `acpx ${executor.kind} sessions ensure exited with status ${ensureSession.status}`));
|
|
1095
1513
|
}
|
|
1096
1514
|
const outputPath = path.join(newResult.paths.conversationDir, `${executor.kind}-output.log`);
|
|
1097
|
-
const outputFd =
|
|
1515
|
+
const outputFd = openPrivateAppendFile(outputPath);
|
|
1098
1516
|
const child = spawn(acpxPath, acpxArgs, {
|
|
1099
1517
|
detached: true,
|
|
1100
1518
|
stdio: ["ignore", outputFd, outputFd],
|
|
@@ -1241,12 +1659,12 @@ function startExecutorMonitor({ statePath, logPath, pid, outputPath, agentTimeou
|
|
|
1241
1659
|
detached: true,
|
|
1242
1660
|
stdio: "ignore",
|
|
1243
1661
|
cwd: process.cwd(),
|
|
1244
|
-
env:
|
|
1662
|
+
env: environmentWithoutGatewayTokens()
|
|
1245
1663
|
});
|
|
1246
1664
|
child.unref();
|
|
1247
1665
|
return child;
|
|
1248
1666
|
}
|
|
1249
|
-
function startTerminalBridgeMonitor({ statePath, logPath, agentTimeoutMinutes, agentHardTimeoutMinutes, pollIntervalMs, codexHome }) {
|
|
1667
|
+
function startTerminalBridgeMonitor({ statePath, logPath, agentTimeoutMinutes, agentHardTimeoutMinutes, pollIntervalMs, codexHome, claudeHookStoreDir }) {
|
|
1250
1668
|
const args = [
|
|
1251
1669
|
new URL(import.meta.url).pathname,
|
|
1252
1670
|
"monitor",
|
|
@@ -1265,11 +1683,14 @@ function startTerminalBridgeMonitor({ statePath, logPath, agentTimeoutMinutes, a
|
|
|
1265
1683
|
if (codexHome) {
|
|
1266
1684
|
args.push("--codex-home", codexHome);
|
|
1267
1685
|
}
|
|
1686
|
+
if (claudeHookStoreDir) {
|
|
1687
|
+
args.push("--claude-hook-store-dir", claudeHookStoreDir);
|
|
1688
|
+
}
|
|
1268
1689
|
const child = spawn(process.execPath, args, {
|
|
1269
1690
|
detached: true,
|
|
1270
1691
|
stdio: "ignore",
|
|
1271
1692
|
cwd: process.cwd(),
|
|
1272
|
-
env:
|
|
1693
|
+
env: environmentWithoutGatewayTokens()
|
|
1273
1694
|
});
|
|
1274
1695
|
child.unref();
|
|
1275
1696
|
return child;
|
|
@@ -1291,7 +1712,8 @@ function startTerminalBridgeMonitorForConversation({ conversation, statePath, lo
|
|
|
1291
1712
|
nativeTakeover?.["terminal_bridge_hard_timeout_minutes"] ??
|
|
1292
1713
|
DEFAULT_AGENT_HARD_TIMEOUT_MINUTES),
|
|
1293
1714
|
pollIntervalMs: Number(options.monitorPollIntervalMs ?? DEFAULT_MONITOR_POLL_INTERVAL_MS),
|
|
1294
|
-
codexHome: options.codexHome
|
|
1715
|
+
codexHome: options.codexHome,
|
|
1716
|
+
claudeHookStoreDir: options.claudeHookStoreDir ?? nativeTakeover?.["claude_hook_store_dir"]
|
|
1295
1717
|
});
|
|
1296
1718
|
}
|
|
1297
1719
|
function terminalBridgeEnabled(conversation) {
|
|
@@ -1314,6 +1736,8 @@ function withTerminalBridgeState({ conversation, message, startedAt, agentTimeou
|
|
|
1314
1736
|
terminal_bridge_request_text: String(message.body ?? ""),
|
|
1315
1737
|
terminal_bridge_request_hash: terminalBridgeRequestFingerprint(message.body),
|
|
1316
1738
|
terminal_bridge_pre_send_screen_fingerprint: preSendScreenFingerprint,
|
|
1739
|
+
terminal_bridge_completion_claim: undefined,
|
|
1740
|
+
terminal_bridge_monitor_lock_version: TERMINAL_BRIDGE_MONITOR_LOCK_VERSION,
|
|
1317
1741
|
terminal_bridge_monitor_started_at: startedAt,
|
|
1318
1742
|
terminal_bridge_last_activity_at: startedAt,
|
|
1319
1743
|
terminal_bridge_inactivity_timeout_minutes: agentTimeoutMinutes,
|
|
@@ -1324,6 +1748,165 @@ function withTerminalBridgeState({ conversation, message, startedAt, agentTimeou
|
|
|
1324
1748
|
updated_at: startedAt
|
|
1325
1749
|
};
|
|
1326
1750
|
}
|
|
1751
|
+
function activateClaudeHookLease({ options, conversation, message, terminalControl, expiresAt }) {
|
|
1752
|
+
const runtime = terminalRuntimeIdentityForConversation(conversation, terminalControl);
|
|
1753
|
+
const pid = runtime.pid;
|
|
1754
|
+
const agentRow = pid === undefined
|
|
1755
|
+
? undefined
|
|
1756
|
+
: loadClaudeAgentRows(options).find((row) => row.pid === pid && (row.kind === undefined || row.kind === "interactive"));
|
|
1757
|
+
const sessionId = agentRow?.sessionId ?? runtime.sessionId;
|
|
1758
|
+
const cwd = agentRow?.cwd ?? runtime.cwd ?? terminalControl.currentPath;
|
|
1759
|
+
if (!cwd || (pid === undefined && !sessionId)) {
|
|
1760
|
+
runtimeLog("warn", "claude_hook_lease_unavailable", {
|
|
1761
|
+
conversation_id: conversation.conversation_id,
|
|
1762
|
+
terminal_target: terminalControl.target,
|
|
1763
|
+
reason: "exact Claude pid/session identity is unavailable"
|
|
1764
|
+
});
|
|
1765
|
+
return undefined;
|
|
1766
|
+
}
|
|
1767
|
+
const store = createClaudeHookStore(options);
|
|
1768
|
+
try {
|
|
1769
|
+
const previous = store.resolveLease({
|
|
1770
|
+
...(sessionId === undefined ? {} : { sessionId }),
|
|
1771
|
+
...(pid === undefined ? {} : { pid }),
|
|
1772
|
+
cwd,
|
|
1773
|
+
requireUnique: true
|
|
1774
|
+
});
|
|
1775
|
+
if (previous && (previous.lease.conversationId !== conversation.conversation_id ||
|
|
1776
|
+
previous.lease.messageId !== message.id)) {
|
|
1777
|
+
store.releaseLease({ leaseId: previous.lease.id });
|
|
1778
|
+
}
|
|
1779
|
+
const lease = store.activateLease({
|
|
1780
|
+
...(sessionId === undefined ? {} : { sessionId }),
|
|
1781
|
+
...(pid === undefined ? {} : { pid }),
|
|
1782
|
+
cwd,
|
|
1783
|
+
conversationId: conversation.conversation_id,
|
|
1784
|
+
messageId: message.id,
|
|
1785
|
+
terminalTarget: terminalControl.target,
|
|
1786
|
+
expiresAt
|
|
1787
|
+
});
|
|
1788
|
+
runtimeLog("info", "claude_hook_lease_activated", {
|
|
1789
|
+
conversation_id: conversation.conversation_id,
|
|
1790
|
+
message_id: message.id,
|
|
1791
|
+
terminal_target: terminalControl.target,
|
|
1792
|
+
lease_id: lease.id,
|
|
1793
|
+
pid,
|
|
1794
|
+
session_id: sessionId,
|
|
1795
|
+
expires_at: lease.expiresAt
|
|
1796
|
+
});
|
|
1797
|
+
return {
|
|
1798
|
+
lease,
|
|
1799
|
+
storeDir: store.rootDir,
|
|
1800
|
+
...(pid === undefined ? {} : { pid }),
|
|
1801
|
+
...(sessionId === undefined ? {} : { sessionId })
|
|
1802
|
+
};
|
|
1803
|
+
}
|
|
1804
|
+
catch (error) {
|
|
1805
|
+
runtimeLog("warn", "claude_hook_lease_unavailable", {
|
|
1806
|
+
conversation_id: conversation.conversation_id,
|
|
1807
|
+
terminal_target: terminalControl.target,
|
|
1808
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
1809
|
+
});
|
|
1810
|
+
return undefined;
|
|
1811
|
+
}
|
|
1812
|
+
}
|
|
1813
|
+
function withClaudeHookLeaseState(conversation, state) {
|
|
1814
|
+
const nativeTakeover = isRecord(conversation.native_session_takeover)
|
|
1815
|
+
? conversation.native_session_takeover
|
|
1816
|
+
: {};
|
|
1817
|
+
return {
|
|
1818
|
+
...conversation,
|
|
1819
|
+
native_session_takeover: {
|
|
1820
|
+
...nativeTakeover,
|
|
1821
|
+
claude_hook_mode: "enabled",
|
|
1822
|
+
claude_hook_store_dir: state.storeDir,
|
|
1823
|
+
claude_hook_lease_id: state.lease.id,
|
|
1824
|
+
terminal_agent_pid: state.pid,
|
|
1825
|
+
terminal_agent_session_id: state.sessionId
|
|
1826
|
+
}
|
|
1827
|
+
};
|
|
1828
|
+
}
|
|
1829
|
+
function releaseClaudeHookLease(conversation) {
|
|
1830
|
+
const nativeTakeover = isRecord(conversation?.native_session_takeover)
|
|
1831
|
+
? conversation.native_session_takeover
|
|
1832
|
+
: undefined;
|
|
1833
|
+
const leaseId = stringValue(nativeTakeover?.claude_hook_lease_id);
|
|
1834
|
+
if (!leaseId || nativeTakeover?.agent !== "claude") {
|
|
1835
|
+
return;
|
|
1836
|
+
}
|
|
1837
|
+
const storeDir = stringValue(nativeTakeover.claude_hook_store_dir) ?? defaultClaudeHookStoreDir();
|
|
1838
|
+
try {
|
|
1839
|
+
const released = new ClaudeHookStore({ rootDir: storeDir }).releaseLease({ leaseId });
|
|
1840
|
+
runtimeLog("info", "claude_hook_lease_released", {
|
|
1841
|
+
conversation_id: conversation.conversation_id,
|
|
1842
|
+
message_id: released?.messageId,
|
|
1843
|
+
lease_id: leaseId
|
|
1844
|
+
});
|
|
1845
|
+
}
|
|
1846
|
+
catch (error) {
|
|
1847
|
+
runtimeLog("warn", "claude_hook_lease_release_failed", {
|
|
1848
|
+
conversation_id: conversation.conversation_id,
|
|
1849
|
+
lease_id: leaseId,
|
|
1850
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
1851
|
+
});
|
|
1852
|
+
}
|
|
1853
|
+
}
|
|
1854
|
+
function renewClaudeHookLease(conversation, expiresAt) {
|
|
1855
|
+
const nativeTakeover = isRecord(conversation?.native_session_takeover)
|
|
1856
|
+
? conversation.native_session_takeover
|
|
1857
|
+
: undefined;
|
|
1858
|
+
if (nativeTakeover?.agent !== "claude") {
|
|
1859
|
+
return undefined;
|
|
1860
|
+
}
|
|
1861
|
+
const conversationId = stringValue(conversation.conversation_id);
|
|
1862
|
+
const messageId = stringValue(nativeTakeover.terminal_bridge_message_id);
|
|
1863
|
+
const terminalControl = terminalControlFromTakeover(nativeTakeover);
|
|
1864
|
+
const cwd = stringValue(nativeTakeover.source_cwd) ?? terminalControl?.currentPath;
|
|
1865
|
+
const pid = Number.isInteger(Number(nativeTakeover.terminal_agent_pid))
|
|
1866
|
+
? Number(nativeTakeover.terminal_agent_pid)
|
|
1867
|
+
: undefined;
|
|
1868
|
+
const sessionId = stringValue(nativeTakeover.terminal_agent_session_id);
|
|
1869
|
+
if (!conversationId || !messageId || !terminalControl || !cwd || (pid === undefined && !sessionId)) {
|
|
1870
|
+
return undefined;
|
|
1871
|
+
}
|
|
1872
|
+
const storeDir = stringValue(nativeTakeover.claude_hook_store_dir) ?? defaultClaudeHookStoreDir();
|
|
1873
|
+
try {
|
|
1874
|
+
return new ClaudeHookStore({ rootDir: storeDir }).activateLease({
|
|
1875
|
+
...(sessionId === undefined ? {} : { sessionId }),
|
|
1876
|
+
...(pid === undefined ? {} : { pid }),
|
|
1877
|
+
cwd,
|
|
1878
|
+
conversationId,
|
|
1879
|
+
messageId,
|
|
1880
|
+
terminalTarget: terminalControl.target,
|
|
1881
|
+
expiresAt
|
|
1882
|
+
});
|
|
1883
|
+
}
|
|
1884
|
+
catch (error) {
|
|
1885
|
+
runtimeLog("warn", "claude_hook_lease_renew_failed", {
|
|
1886
|
+
conversation_id: conversationId,
|
|
1887
|
+
message_id: messageId,
|
|
1888
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
1889
|
+
});
|
|
1890
|
+
return undefined;
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1893
|
+
function releaseClaudeHookLeasesForTerminal({ storeDir, terminalControl, replacementConversationId }) {
|
|
1894
|
+
for (const candidate of listConversations(storeDir)) {
|
|
1895
|
+
if (candidate.conversation_id === replacementConversationId) {
|
|
1896
|
+
continue;
|
|
1897
|
+
}
|
|
1898
|
+
const nativeTakeover = isRecord(candidate.native_session_takeover)
|
|
1899
|
+
? candidate.native_session_takeover
|
|
1900
|
+
: undefined;
|
|
1901
|
+
const candidateControl = terminalControlFromTakeover(nativeTakeover);
|
|
1902
|
+
if (nativeTakeover?.agent === "claude" &&
|
|
1903
|
+
nativeTakeover?.terminal_bridge === true &&
|
|
1904
|
+
candidateControl?.target === terminalControl.target &&
|
|
1905
|
+
candidateControl?.socketPath === terminalControl.socketPath) {
|
|
1906
|
+
releaseClaudeHookLease(candidate);
|
|
1907
|
+
}
|
|
1908
|
+
}
|
|
1909
|
+
}
|
|
1327
1910
|
function uniqueDelegateSessionName(kind) {
|
|
1328
1911
|
const { sessionPrefix } = executorDefinitionForKind(kind || "claude");
|
|
1329
1912
|
const timestamp = new Date().toISOString().replace(/\D/g, "").slice(0, 14);
|
|
@@ -1378,16 +1961,23 @@ function modelForExecutor(executor, options = {}) {
|
|
|
1378
1961
|
return normalizeModelForExecutor(executor, modelEnvForExecutor(executor, process.env));
|
|
1379
1962
|
}
|
|
1380
1963
|
function environmentForExecutor(executor, options = {}) {
|
|
1964
|
+
const environment = environmentWithoutGatewayTokens();
|
|
1381
1965
|
const proxy = proxyForExecutor(executor, options);
|
|
1382
1966
|
if (!proxy) {
|
|
1383
|
-
return
|
|
1967
|
+
return environment;
|
|
1384
1968
|
}
|
|
1385
1969
|
return {
|
|
1386
|
-
...
|
|
1970
|
+
...environment,
|
|
1387
1971
|
ALL_PROXY: proxy,
|
|
1388
1972
|
all_proxy: proxy
|
|
1389
1973
|
};
|
|
1390
1974
|
}
|
|
1975
|
+
function environmentWithoutGatewayTokens() {
|
|
1976
|
+
const environment = { ...process.env };
|
|
1977
|
+
delete environment.AKK_GATEWAY_TOKEN;
|
|
1978
|
+
delete environment.OPENCLAW_GATEWAY_TOKEN;
|
|
1979
|
+
return environment;
|
|
1980
|
+
}
|
|
1391
1981
|
async function runList(options) {
|
|
1392
1982
|
const storeDir = expandHome(options.storeDir ?? options.logDir ?? defaultStoreDir(process.cwd()));
|
|
1393
1983
|
const cleanup = cleanupIdleConversations(storeDir, options);
|
|
@@ -1460,7 +2050,7 @@ async function buildNativeListGroups({ options, agentFilter, statusFilter }) {
|
|
|
1460
2050
|
};
|
|
1461
2051
|
}
|
|
1462
2052
|
const terminalProvider = createTerminalControlProvider(options);
|
|
1463
|
-
const bridge =
|
|
2053
|
+
const bridge = createTerminalAgentBridge(options, terminalProvider, registry);
|
|
1464
2054
|
const terminalScan = options.terminalDebug ? await terminalControlDiagnostics(terminalProvider) : undefined;
|
|
1465
2055
|
const terminalControlled = [];
|
|
1466
2056
|
const native = [];
|
|
@@ -1468,7 +2058,7 @@ async function buildNativeListGroups({ options, agentFilter, statusFilter }) {
|
|
|
1468
2058
|
const errors = [];
|
|
1469
2059
|
try {
|
|
1470
2060
|
const processSource = createTerminalProcessSource(options);
|
|
1471
|
-
const snapshots = await processSource.listProcessSnapshots((snapshot) => adapters.some((adapter) => adapter.capabilities.processDiscovery && adapter.classifyProcess(snapshot) !== undefined));
|
|
2061
|
+
const snapshots = await processSource.listProcessSnapshots((snapshot) => adapters.some((adapter) => adapter.capabilities.processDiscovery && adapter.classifyProcess(snapshot) !== undefined), { includeAncestors: true });
|
|
1472
2062
|
const activeSessions = await bridge.listProcesses(snapshots, adapters.map((adapter) => adapter.agent));
|
|
1473
2063
|
activeCount = activeSessions.length;
|
|
1474
2064
|
const rootSessions = rootActiveProcesses(activeSessions);
|
|
@@ -1554,7 +2144,12 @@ async function terminalControlledListEntry(session, activeSessions, options, bri
|
|
|
1554
2144
|
if (!terminalControl) {
|
|
1555
2145
|
throw new Error(`process ${session.pid} is not terminal-controlled`);
|
|
1556
2146
|
}
|
|
1557
|
-
const terminalState = await listStateForTerminal(session.agent, terminalControl, options, bridge
|
|
2147
|
+
const terminalState = await listStateForTerminal(session.agent, terminalControl, options, bridge, {
|
|
2148
|
+
pid: session.pid,
|
|
2149
|
+
sessionId: session.sessionId,
|
|
2150
|
+
cwd: session.cwd,
|
|
2151
|
+
terminalTarget: terminalControl.target
|
|
2152
|
+
});
|
|
1558
2153
|
return {
|
|
1559
2154
|
id: bridge.terminalConversationId(session),
|
|
1560
2155
|
source: "terminal_control",
|
|
@@ -1583,7 +2178,7 @@ async function terminalControlledListEntry(session, activeSessions, options, bri
|
|
|
1583
2178
|
}
|
|
1584
2179
|
};
|
|
1585
2180
|
}
|
|
1586
|
-
async function listStateForTerminal(agent, terminalControl, options, bridge = createTerminalAgentBridge(options)) {
|
|
2181
|
+
async function listStateForTerminal(agent, terminalControl, options, bridge = createTerminalAgentBridge(options), runtime) {
|
|
1587
2182
|
if (options.noApprovalScan) {
|
|
1588
2183
|
return {
|
|
1589
2184
|
approval_state: {
|
|
@@ -1598,7 +2193,8 @@ async function listStateForTerminal(agent, terminalControl, options, bridge = cr
|
|
|
1598
2193
|
}
|
|
1599
2194
|
try {
|
|
1600
2195
|
const status = await bridge.status(agent, terminalControl, {
|
|
1601
|
-
scrollbackLines: Number(options.scrollbackLines ?? 120)
|
|
2196
|
+
scrollbackLines: Number(options.scrollbackLines ?? 120),
|
|
2197
|
+
runtime
|
|
1602
2198
|
});
|
|
1603
2199
|
return {
|
|
1604
2200
|
approval_state: {
|
|
@@ -1629,7 +2225,7 @@ function rootActiveProcesses(processes) {
|
|
|
1629
2225
|
const seenTerminalTargets = new Set();
|
|
1630
2226
|
return roots.filter((process) => {
|
|
1631
2227
|
const terminalTarget = process.terminalControl?.target
|
|
1632
|
-
? `${process.agent}:${process.terminalControl.target}`
|
|
2228
|
+
? `${process.agent}:${process.terminalControl.target}:${process.terminalControl.panePid}`
|
|
1633
2229
|
: undefined;
|
|
1634
2230
|
if (!terminalTarget) {
|
|
1635
2231
|
return true;
|
|
@@ -1671,7 +2267,11 @@ async function runStatus(options) {
|
|
|
1671
2267
|
cleanupIdleConversations(storeDirFromOptions(options), options);
|
|
1672
2268
|
const terminalConversation = await resolveTerminalConversationFromOptions(options);
|
|
1673
2269
|
if (terminalConversation) {
|
|
1674
|
-
const terminalStatus = await terminalStatusForControl(terminalConversation.agent, terminalConversation.terminalControl, options
|
|
2270
|
+
const terminalStatus = await terminalStatusForControl(terminalConversation.agent, terminalConversation.terminalControl, options, {
|
|
2271
|
+
pid: terminalConversation.pid,
|
|
2272
|
+
cwd: terminalConversation.terminalControl.currentPath,
|
|
2273
|
+
terminalTarget: terminalConversation.terminalControl.target
|
|
2274
|
+
});
|
|
1675
2275
|
printJson({
|
|
1676
2276
|
conversation_id: terminalConversation.conversationId,
|
|
1677
2277
|
source: "terminal_control",
|
|
@@ -1686,7 +2286,12 @@ async function runStatus(options) {
|
|
|
1686
2286
|
});
|
|
1687
2287
|
return;
|
|
1688
2288
|
}
|
|
1689
|
-
const
|
|
2289
|
+
const loaded = loadConversationFromOptions(options);
|
|
2290
|
+
const { statePath, logPath } = loaded;
|
|
2291
|
+
const conversation = await migrateLegacyTerminalAgentIdentity({
|
|
2292
|
+
...loaded,
|
|
2293
|
+
options
|
|
2294
|
+
});
|
|
1690
2295
|
const events = readExistingEvents(logPath);
|
|
1691
2296
|
const result = {
|
|
1692
2297
|
conversation,
|
|
@@ -1703,7 +2308,7 @@ async function runStatus(options) {
|
|
|
1703
2308
|
if (terminalControl) {
|
|
1704
2309
|
const executor = executorForConversation(conversation);
|
|
1705
2310
|
result.terminal_control = terminalControl;
|
|
1706
|
-
result.terminal_status = await terminalStatusForControl(executor.kind, terminalControl, options);
|
|
2311
|
+
result.terminal_status = await terminalStatusForControl(executor.kind, terminalControl, options, terminalRuntimeIdentityForConversation(conversation, terminalControl));
|
|
1707
2312
|
result.terminal_screen = result.terminal_status.screen;
|
|
1708
2313
|
}
|
|
1709
2314
|
printJson(result);
|
|
@@ -1721,7 +2326,11 @@ async function runDescribe(options) {
|
|
|
1721
2326
|
const conversationId = required(options.conversation ?? options.conversationId, "--conversation is required");
|
|
1722
2327
|
const terminalConversation = await resolveTerminalConversationFromOptions(options);
|
|
1723
2328
|
if (terminalConversation) {
|
|
1724
|
-
const terminalStatus = await terminalStatusForControl(terminalConversation.agent, terminalConversation.terminalControl, options
|
|
2329
|
+
const terminalStatus = await terminalStatusForControl(terminalConversation.agent, terminalConversation.terminalControl, options, {
|
|
2330
|
+
pid: terminalConversation.pid,
|
|
2331
|
+
cwd: terminalConversation.terminalControl.currentPath,
|
|
2332
|
+
terminalTarget: terminalConversation.terminalControl.target
|
|
2333
|
+
});
|
|
1725
2334
|
if (terminalConversation.agent !== "codex") {
|
|
1726
2335
|
const adapter = createRuntimeTerminalAgentRegistry(options).require(terminalConversation.agent);
|
|
1727
2336
|
printJson({
|
|
@@ -1763,11 +2372,16 @@ async function runDescribe(options) {
|
|
|
1763
2372
|
}));
|
|
1764
2373
|
return;
|
|
1765
2374
|
}
|
|
1766
|
-
const
|
|
2375
|
+
const loaded = loadConversationFromOptions(options);
|
|
2376
|
+
const { statePath, logPath } = loaded;
|
|
2377
|
+
const conversation = await migrateLegacyTerminalAgentIdentity({
|
|
2378
|
+
...loaded,
|
|
2379
|
+
options
|
|
2380
|
+
});
|
|
1767
2381
|
const events = readExistingEvents(logPath);
|
|
1768
2382
|
const terminalControl = terminalControlFromTakeover(isRecord(conversation.native_session_takeover) ? conversation.native_session_takeover : undefined);
|
|
1769
2383
|
const terminalStatus = terminalControl
|
|
1770
|
-
? await terminalStatusForControl(executorForConversation(conversation).kind, terminalControl, options)
|
|
2384
|
+
? await terminalStatusForControl(executorForConversation(conversation).kind, terminalControl, options, terminalRuntimeIdentityForConversation(conversation, terminalControl))
|
|
1771
2385
|
: undefined;
|
|
1772
2386
|
printJson({
|
|
1773
2387
|
conversation_id: conversation.conversation_id,
|
|
@@ -1786,9 +2400,10 @@ async function runDescribe(options) {
|
|
|
1786
2400
|
event_log_path: logPath
|
|
1787
2401
|
});
|
|
1788
2402
|
}
|
|
1789
|
-
async function terminalStatusForControl(agent, terminalControl, options) {
|
|
2403
|
+
async function terminalStatusForControl(agent, terminalControl, options, runtime) {
|
|
1790
2404
|
return createTerminalAgentBridge(options).status(agent, terminalControl, {
|
|
1791
|
-
scrollbackLines: Number(options.scrollbackLines ?? 120)
|
|
2405
|
+
scrollbackLines: Number(options.scrollbackLines ?? 120),
|
|
2406
|
+
runtime
|
|
1792
2407
|
});
|
|
1793
2408
|
}
|
|
1794
2409
|
function terminalBridgeApprovalFingerprint({ terminalControl, terminalStatus }) {
|
|
@@ -1805,10 +2420,26 @@ function terminalBridgeApprovalFingerprint({ terminalControl, terminalStatus })
|
|
|
1805
2420
|
label: approval.label,
|
|
1806
2421
|
prompt_kind: approval.prompt_kind,
|
|
1807
2422
|
command: approval.command,
|
|
2423
|
+
tool_name: approval.tool_name,
|
|
2424
|
+
request_detail: approval.request_detail,
|
|
1808
2425
|
excerpt: screen.excerpt
|
|
1809
2426
|
}))
|
|
1810
2427
|
.digest("hex");
|
|
1811
2428
|
}
|
|
2429
|
+
function assertSafeClaudeTerminalSend(terminalStatus) {
|
|
2430
|
+
const approval = isRecord(terminalStatus?.approval_state)
|
|
2431
|
+
? terminalStatus.approval_state
|
|
2432
|
+
: undefined;
|
|
2433
|
+
if (terminalStatus?.reachable !== true) {
|
|
2434
|
+
throw new Error("Claude Code terminal status is unavailable");
|
|
2435
|
+
}
|
|
2436
|
+
if (approval?.blocked === true) {
|
|
2437
|
+
throw new Error(stringValue(approval.reason) ?? "Claude Code is waiting at a permission dialog");
|
|
2438
|
+
}
|
|
2439
|
+
if (terminalStatus.activity_state !== "idle") {
|
|
2440
|
+
throw new Error(`Claude Code terminal is ${stringValue(terminalStatus.activity_state) ?? "unknown"}, not idle`);
|
|
2441
|
+
}
|
|
2442
|
+
}
|
|
1812
2443
|
function terminalBridgeApprovalInstructions({ conversation, terminalControl, terminalStatus }) {
|
|
1813
2444
|
const approval = isRecord(terminalStatus?.approval_state) ? terminalStatus.approval_state : {};
|
|
1814
2445
|
const screen = isRecord(terminalStatus?.screen) ? terminalStatus.screen : {};
|
|
@@ -1818,10 +2449,18 @@ function terminalBridgeApprovalInstructions({ conversation, terminalControl, ter
|
|
|
1818
2449
|
const keys = Array.isArray(approval.keys)
|
|
1819
2450
|
? approval.keys.filter((value) => typeof value === "string")
|
|
1820
2451
|
: [];
|
|
1821
|
-
const
|
|
1822
|
-
|
|
1823
|
-
|
|
2452
|
+
const decisionMode = stringValue(approval.decision_mode);
|
|
2453
|
+
const keyDescription = decisionMode === "structured"
|
|
2454
|
+
? "structured one-time Hook decision"
|
|
2455
|
+
: keys.length > 0
|
|
2456
|
+
? keys.join(" then ")
|
|
2457
|
+
: stringValue(approval.key) || "the detected approve key sequence";
|
|
1824
2458
|
const fingerprint = stringValue(approval.fingerprint);
|
|
2459
|
+
const promptKind = stringValue(approval.prompt_kind);
|
|
2460
|
+
const command = stringValue(approval.command);
|
|
2461
|
+
const toolName = stringValue(approval.tool_name);
|
|
2462
|
+
const requestDetail = stringValue(approval.request_detail);
|
|
2463
|
+
const requestId = stringValue(approval.request_id);
|
|
1825
2464
|
const excerpt = stringValue(screen.excerpt) || "(No terminal excerpt was available.)";
|
|
1826
2465
|
return [
|
|
1827
2466
|
`${agentName} is waiting for approval in a terminal-controlled AKK session.`,
|
|
@@ -1829,6 +2468,11 @@ function terminalBridgeApprovalInstructions({ conversation, terminalControl, ter
|
|
|
1829
2468
|
`Conversation: ${conversation.conversation_id}`,
|
|
1830
2469
|
`Terminal: ${terminalControl.kind}:${terminalControl.target}`,
|
|
1831
2470
|
`Approval option: ${label} (${keyDescription})`,
|
|
2471
|
+
promptKind ? `Request kind: ${promptKind}` : undefined,
|
|
2472
|
+
toolName ? `Tool: ${toolName}` : undefined,
|
|
2473
|
+
requestDetail ? `Request: ${requestDetail}` : undefined,
|
|
2474
|
+
command ? `Command: ${command}` : undefined,
|
|
2475
|
+
requestId ? `Structured request id: ${requestId}` : undefined,
|
|
1832
2476
|
"",
|
|
1833
2477
|
"Safe terminal excerpt:",
|
|
1834
2478
|
"```text",
|
|
@@ -1850,7 +2494,7 @@ function terminalBridgeApprovalInstructions({ conversation, terminalControl, ter
|
|
|
1850
2494
|
"Equivalent user command: `AKK cancel " + conversation.conversation_id + "`",
|
|
1851
2495
|
"",
|
|
1852
2496
|
"Do not use raw tmux, shell, or manual key presses for this approval. Do not approve without explicit user confirmation."
|
|
1853
|
-
].join("\n");
|
|
2497
|
+
].filter((line) => line !== undefined).join("\n");
|
|
1854
2498
|
}
|
|
1855
2499
|
function recordTerminalBridgeApprovalNotification({ statePath, logPath, terminalControl, terminalStatus, fingerprint }) {
|
|
1856
2500
|
const releaseLock = acquireFileLock(`${statePath}.lock`);
|
|
@@ -1900,102 +2544,138 @@ function recordTerminalBridgeApprovalNotification({ statePath, logPath, terminal
|
|
|
1900
2544
|
releaseLock();
|
|
1901
2545
|
}
|
|
1902
2546
|
}
|
|
1903
|
-
|
|
1904
|
-
const
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
2547
|
+
function prepareManagedSend({ options, statePath, logPath, messageBody }) {
|
|
2548
|
+
const releaseLock = acquireFileLock(`${statePath}.lock`);
|
|
2549
|
+
try {
|
|
2550
|
+
const conversation = loadState(statePath);
|
|
2551
|
+
if (["done", "failed", "closed", "cancelled"].includes(conversation.status)) {
|
|
2552
|
+
throw new Error(`cannot send to ${conversation.conversation_id}; conversation is ${conversation.status}`);
|
|
2553
|
+
}
|
|
2554
|
+
if (conversation.status === "needs_recovery") {
|
|
2555
|
+
throw new Error(`cannot send to ${conversation.conversation_id}; choose recover, close, or delegate a new task first`);
|
|
2556
|
+
}
|
|
2557
|
+
if (conversation.status === "needs_model_selection" && !options.model) {
|
|
2558
|
+
throw new Error(`cannot send to ${conversation.conversation_id}; choose a supported model with --model first`);
|
|
2559
|
+
}
|
|
2560
|
+
const executor = executorForConversation(conversation);
|
|
2561
|
+
const type = options.type ??
|
|
2562
|
+
(conversation.status === "waiting_for_openclaw" ? "answer" : "task");
|
|
2563
|
+
const nativeTakeoverForSend = isRecord(conversation.native_session_takeover)
|
|
2564
|
+
? conversation.native_session_takeover
|
|
2565
|
+
: undefined;
|
|
2566
|
+
const forkTakeoverForSend = isRecord(conversation.fork_context_takeover)
|
|
2567
|
+
? conversation.fork_context_takeover
|
|
2568
|
+
: undefined;
|
|
2569
|
+
const needsNativeTakeoverBootstrap = nativeTakeoverForSend?.["needs_bootstrap"] === true;
|
|
2570
|
+
const needsForkTakeoverBootstrap = forkTakeoverForSend?.["needs_bootstrap"] === true;
|
|
2571
|
+
const message = createMessage({
|
|
2572
|
+
conversation,
|
|
2573
|
+
from: "openclaw",
|
|
2574
|
+
to: executor.actor,
|
|
2575
|
+
type,
|
|
2576
|
+
body: messageBody,
|
|
2577
|
+
metadata: {
|
|
2578
|
+
executor_kind: executor.kind,
|
|
2579
|
+
executor_session: executor.session
|
|
2580
|
+
}
|
|
2581
|
+
});
|
|
2582
|
+
const previousModelSelection = isRecord(conversation.model_selection)
|
|
2583
|
+
? conversation.model_selection
|
|
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
|
+
};
|
|
2600
|
+
saveState(statePath, nextConversation);
|
|
2601
|
+
appendEvent(logPath, messageEvent(message));
|
|
2602
|
+
runtimeLog("info", "message_created", {
|
|
2603
|
+
conversation_id: conversation.conversation_id,
|
|
2604
|
+
agent: executor.kind,
|
|
2605
|
+
executor_session: executor.session,
|
|
2606
|
+
message_type: type,
|
|
2607
|
+
state_path: statePath,
|
|
2608
|
+
event_log_path: logPath,
|
|
2609
|
+
message: textSummary(messageBody)
|
|
2610
|
+
});
|
|
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
|
+
}
|
|
2625
|
+
}
|
|
2626
|
+
async function runSend(options) {
|
|
2627
|
+
const messageBody = required(options.message ?? options.request, "--message is required");
|
|
2628
|
+
if (options.agentHardTimeoutMinutes !== undefined) {
|
|
2629
|
+
positiveMinutes(options.agentHardTimeoutMinutes, "--agent-hard-timeout-minutes");
|
|
2630
|
+
}
|
|
2631
|
+
cleanupIdleConversations(storeDirFromOptions(options), options);
|
|
2632
|
+
const terminalConversation = await resolveTerminalConversationFromOptions(options);
|
|
2633
|
+
if (terminalConversation) {
|
|
2634
|
+
if (options.background) {
|
|
2635
|
+
const managed = createManagedTerminalConversationFromRawId({
|
|
2636
|
+
options,
|
|
2637
|
+
conversationId: terminalConversation.conversationId,
|
|
2638
|
+
agent: terminalConversation.agent,
|
|
2639
|
+
pid: terminalConversation.pid,
|
|
2640
|
+
messageBody,
|
|
2641
|
+
terminalControl: terminalConversation.terminalControl
|
|
2642
|
+
});
|
|
2643
|
+
await runTerminalControlSend({
|
|
2644
|
+
options,
|
|
2645
|
+
conversation: managed.conversation,
|
|
2646
|
+
nextConversation: managed.nextConversation,
|
|
2647
|
+
statePath: managed.statePath,
|
|
2648
|
+
logPath: managed.logPath,
|
|
2649
|
+
executor: managed.executor,
|
|
2650
|
+
message: managed.message,
|
|
2651
|
+
terminalControl: terminalConversation.terminalControl,
|
|
2652
|
+
needsNativeTakeoverBootstrap: true
|
|
2653
|
+
});
|
|
2654
|
+
return;
|
|
1931
2655
|
}
|
|
1932
2656
|
await runTerminalConversationSend({
|
|
1933
2657
|
options,
|
|
1934
2658
|
conversationId: terminalConversation.conversationId,
|
|
1935
2659
|
agent: terminalConversation.agent,
|
|
2660
|
+
pid: terminalConversation.pid,
|
|
1936
2661
|
messageBody,
|
|
1937
2662
|
terminalControl: terminalConversation.terminalControl
|
|
1938
2663
|
});
|
|
1939
2664
|
return;
|
|
1940
2665
|
}
|
|
1941
|
-
const
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
throw new Error(`cannot send to ${conversation.conversation_id}; choose recover, close, or delegate a new task first`);
|
|
1947
|
-
}
|
|
1948
|
-
if (conversation.status === "needs_model_selection" && !options.model) {
|
|
1949
|
-
throw new Error(`cannot send to ${conversation.conversation_id}; choose a supported model with --model first`);
|
|
1950
|
-
}
|
|
1951
|
-
const executor = executorForConversation(conversation);
|
|
1952
|
-
const type = options.type ?? (conversation.status === "waiting_for_openclaw" ? "answer" : "task");
|
|
1953
|
-
const nativeTakeoverForSend = isRecord(conversation.native_session_takeover)
|
|
1954
|
-
? conversation.native_session_takeover
|
|
1955
|
-
: undefined;
|
|
1956
|
-
const forkTakeoverForSend = isRecord(conversation.fork_context_takeover)
|
|
1957
|
-
? conversation.fork_context_takeover
|
|
1958
|
-
: undefined;
|
|
1959
|
-
const needsNativeTakeoverBootstrap = nativeTakeoverForSend?.["needs_bootstrap"] === true;
|
|
1960
|
-
const needsForkTakeoverBootstrap = forkTakeoverForSend?.["needs_bootstrap"] === true;
|
|
1961
|
-
const message = createMessage({
|
|
1962
|
-
conversation,
|
|
1963
|
-
from: "openclaw",
|
|
1964
|
-
to: executor.actor,
|
|
1965
|
-
type,
|
|
1966
|
-
body: messageBody,
|
|
1967
|
-
metadata: {
|
|
1968
|
-
executor_kind: executor.kind,
|
|
1969
|
-
executor_session: executor.session
|
|
1970
|
-
}
|
|
2666
|
+
const loaded = loadConversationFromOptions(options);
|
|
2667
|
+
const { statePath, logPath } = loaded;
|
|
2668
|
+
await migrateLegacyTerminalAgentIdentity({
|
|
2669
|
+
...loaded,
|
|
2670
|
+
options
|
|
1971
2671
|
});
|
|
1972
|
-
const
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
executor,
|
|
1978
|
-
claude_session: executor.kind === "claude" ? executor.session : conversation.claude_session,
|
|
1979
|
-
executor_model: options.model ?? conversation.executor_model,
|
|
1980
|
-
model_selection: conversation.status === "needs_model_selection"
|
|
1981
|
-
? {
|
|
1982
|
-
...previousModelSelection,
|
|
1983
|
-
resolved_at: new Date().toISOString(),
|
|
1984
|
-
selected_model: options.model
|
|
1985
|
-
}
|
|
1986
|
-
: conversation.model_selection
|
|
1987
|
-
};
|
|
1988
|
-
saveState(statePath, nextConversation);
|
|
1989
|
-
appendEvent(logPath, messageEvent(message));
|
|
1990
|
-
runtimeLog("info", "message_created", {
|
|
1991
|
-
conversation_id: conversation.conversation_id,
|
|
1992
|
-
agent: executor.kind,
|
|
1993
|
-
executor_session: executor.session,
|
|
1994
|
-
message_type: type,
|
|
1995
|
-
state_path: statePath,
|
|
1996
|
-
event_log_path: logPath,
|
|
1997
|
-
message: textSummary(messageBody)
|
|
2672
|
+
const prepared = prepareManagedSend({
|
|
2673
|
+
options,
|
|
2674
|
+
statePath,
|
|
2675
|
+
logPath,
|
|
2676
|
+
messageBody
|
|
1998
2677
|
});
|
|
2678
|
+
const { conversation, executor, nativeTakeoverForSend, forkTakeoverForSend, needsNativeTakeoverBootstrap, needsForkTakeoverBootstrap, message, nextConversation } = prepared;
|
|
1999
2679
|
const executorEnv = environmentForExecutor(executor, {
|
|
2000
2680
|
allProxy: options.allProxy ?? conversation.executor_all_proxy
|
|
2001
2681
|
});
|
|
@@ -2125,7 +2805,7 @@ async function runSend(options) {
|
|
|
2125
2805
|
const acpxArgs = buildAcpxPromptArgs({ executor, payload, model: executorModel });
|
|
2126
2806
|
if (options.background) {
|
|
2127
2807
|
const outputPath = path.join(path.dirname(logPath), `${executor.kind}-followup-output.log`);
|
|
2128
|
-
const outputFd =
|
|
2808
|
+
const outputFd = openPrivateAppendFile(outputPath);
|
|
2129
2809
|
const child = spawn(acpxPath, acpxArgs, {
|
|
2130
2810
|
detached: true,
|
|
2131
2811
|
stdio: ["ignore", outputFd, outputFd],
|
|
@@ -2297,11 +2977,17 @@ async function runApprove(options) {
|
|
|
2297
2977
|
options,
|
|
2298
2978
|
conversationId: terminalConversation.conversationId,
|
|
2299
2979
|
agent: terminalConversation.agent,
|
|
2300
|
-
terminalControl: terminalConversation.terminalControl
|
|
2980
|
+
terminalControl: terminalConversation.terminalControl,
|
|
2981
|
+
pid: terminalConversation.pid
|
|
2301
2982
|
});
|
|
2302
2983
|
return;
|
|
2303
2984
|
}
|
|
2304
|
-
const
|
|
2985
|
+
const loaded = loadConversationFromOptions(options);
|
|
2986
|
+
const { statePath, logPath } = loaded;
|
|
2987
|
+
const conversation = await migrateLegacyTerminalAgentIdentity({
|
|
2988
|
+
...loaded,
|
|
2989
|
+
options
|
|
2990
|
+
});
|
|
2305
2991
|
const nativeTakeover = isRecord(conversation.native_session_takeover)
|
|
2306
2992
|
? conversation.native_session_takeover
|
|
2307
2993
|
: undefined;
|
|
@@ -2318,11 +3004,67 @@ async function runApprove(options) {
|
|
|
2318
3004
|
const autoApproved = options.autoApproved === true;
|
|
2319
3005
|
const policyRuleId = stringValue(options.policyRuleId);
|
|
2320
3006
|
const policyFingerprint = stringValue(options.policyFingerprint);
|
|
3007
|
+
const autoApprovalPolicy = autoApproved
|
|
3008
|
+
? parseJsonOption(options.autoApprovalPolicyJson, "--auto-approval-policy-json")
|
|
3009
|
+
: undefined;
|
|
3010
|
+
const runtimeIdentity = terminalRuntimeIdentityForConversation(conversation, terminalControl);
|
|
3011
|
+
let executorPolicyDecision;
|
|
2321
3012
|
const approval = await createTerminalAgentBridge(options).approve(executor.kind, terminalControl, {
|
|
2322
3013
|
expectedFingerprint,
|
|
2323
|
-
scrollbackLines: Number(options.scrollbackLines ?? 120)
|
|
3014
|
+
scrollbackLines: Number(options.scrollbackLines ?? 120),
|
|
3015
|
+
runtime: runtimeIdentity,
|
|
3016
|
+
requiredDecisionMode: autoApproved && executor.kind === "claude"
|
|
3017
|
+
? "structured"
|
|
3018
|
+
: undefined,
|
|
3019
|
+
authorize: autoApproved
|
|
3020
|
+
? ({ agent, terminalControl: currentTerminalControl, inspection, fingerprint }) => {
|
|
3021
|
+
if (!autoApprovalPolicy) {
|
|
3022
|
+
return {
|
|
3023
|
+
approved: false,
|
|
3024
|
+
reason: "automatic approval requires an executor-side policy"
|
|
3025
|
+
};
|
|
3026
|
+
}
|
|
3027
|
+
const candidate = {
|
|
3028
|
+
agent,
|
|
3029
|
+
kind: inspection.approval.promptKind ?? "unknown",
|
|
3030
|
+
decisionMode: inspection.approval.approvable
|
|
3031
|
+
? inspection.approval.action.mode ?? "keys"
|
|
3032
|
+
: undefined,
|
|
3033
|
+
command: inspection.approval.command,
|
|
3034
|
+
cwd: inspection.approval.cwd ?? currentTerminalControl.currentPath,
|
|
3035
|
+
fingerprint: fingerprint ?? "",
|
|
3036
|
+
terminalTarget: currentTerminalControl.target
|
|
3037
|
+
};
|
|
3038
|
+
executorPolicyDecision = evaluateApprovalPolicy({
|
|
3039
|
+
policy: autoApprovalPolicy,
|
|
3040
|
+
candidate
|
|
3041
|
+
});
|
|
3042
|
+
if (executorPolicyDecision.action !== "approve") {
|
|
3043
|
+
return {
|
|
3044
|
+
approved: false,
|
|
3045
|
+
reason: `executor-side auto-approval policy rejected the current request: ${executorPolicyDecision.reason}`
|
|
3046
|
+
};
|
|
3047
|
+
}
|
|
3048
|
+
if (policyRuleId && executorPolicyDecision.ruleId !== policyRuleId) {
|
|
3049
|
+
return {
|
|
3050
|
+
approved: false,
|
|
3051
|
+
reason: "executor-side auto-approval rule changed before execution"
|
|
3052
|
+
};
|
|
3053
|
+
}
|
|
3054
|
+
if (policyFingerprint &&
|
|
3055
|
+
executorPolicyDecision.policyFingerprint !== policyFingerprint) {
|
|
3056
|
+
return {
|
|
3057
|
+
approved: false,
|
|
3058
|
+
reason: "executor-side auto-approval policy changed before execution"
|
|
3059
|
+
};
|
|
3060
|
+
}
|
|
3061
|
+
return { approved: true };
|
|
3062
|
+
}
|
|
3063
|
+
: undefined
|
|
2324
3064
|
});
|
|
2325
3065
|
const actualFingerprint = approval.fingerprint;
|
|
3066
|
+
const effectivePolicyRuleId = executorPolicyDecision?.ruleId ?? policyRuleId;
|
|
3067
|
+
const effectivePolicyFingerprint = executorPolicyDecision?.policyFingerprint ?? policyFingerprint;
|
|
2326
3068
|
if (!approval.approved) {
|
|
2327
3069
|
if (autoApproved) {
|
|
2328
3070
|
appendEvent(logPath, {
|
|
@@ -2334,8 +3076,8 @@ async function runApprove(options) {
|
|
|
2334
3076
|
terminal_control: terminalControl,
|
|
2335
3077
|
expected_fingerprint: expectedFingerprint,
|
|
2336
3078
|
actual_fingerprint: actualFingerprint,
|
|
2337
|
-
policy_rule_id:
|
|
2338
|
-
policy_fingerprint:
|
|
3079
|
+
policy_rule_id: effectivePolicyRuleId,
|
|
3080
|
+
policy_fingerprint: effectivePolicyFingerprint
|
|
2339
3081
|
});
|
|
2340
3082
|
}
|
|
2341
3083
|
printJson({
|
|
@@ -2358,10 +3100,12 @@ async function runApprove(options) {
|
|
|
2358
3100
|
key: approval.key,
|
|
2359
3101
|
keys: approval.keys,
|
|
2360
3102
|
label: approval.label,
|
|
3103
|
+
decision_mode: approval.decisionMode,
|
|
3104
|
+
request_id: approval.requestId,
|
|
2361
3105
|
approval_fingerprint: actualFingerprint,
|
|
2362
3106
|
auto_approved: autoApproved,
|
|
2363
|
-
policy_rule_id:
|
|
2364
|
-
policy_fingerprint:
|
|
3107
|
+
policy_rule_id: effectivePolicyRuleId,
|
|
3108
|
+
policy_fingerprint: effectivePolicyFingerprint
|
|
2365
3109
|
});
|
|
2366
3110
|
if (autoApproved) {
|
|
2367
3111
|
appendEvent(logPath, {
|
|
@@ -2371,8 +3115,8 @@ async function runApprove(options) {
|
|
|
2371
3115
|
action: "approved",
|
|
2372
3116
|
terminal_control: terminalControl,
|
|
2373
3117
|
approval_fingerprint: actualFingerprint,
|
|
2374
|
-
policy_rule_id:
|
|
2375
|
-
policy_fingerprint:
|
|
3118
|
+
policy_rule_id: effectivePolicyRuleId,
|
|
3119
|
+
policy_fingerprint: effectivePolicyFingerprint
|
|
2376
3120
|
});
|
|
2377
3121
|
}
|
|
2378
3122
|
runtimeLog("info", "terminal_approval_send", {
|
|
@@ -2381,10 +3125,12 @@ async function runApprove(options) {
|
|
|
2381
3125
|
key: approval.key,
|
|
2382
3126
|
keys: approval.keys,
|
|
2383
3127
|
label: approval.label,
|
|
3128
|
+
decision_mode: approval.decisionMode,
|
|
3129
|
+
request_id: approval.requestId,
|
|
2384
3130
|
approval_fingerprint: actualFingerprint,
|
|
2385
3131
|
auto_approved: autoApproved,
|
|
2386
|
-
policy_rule_id:
|
|
2387
|
-
policy_fingerprint:
|
|
3132
|
+
policy_rule_id: effectivePolicyRuleId,
|
|
3133
|
+
policy_fingerprint: effectivePolicyFingerprint
|
|
2388
3134
|
});
|
|
2389
3135
|
const nativeTakeoverForUpdate = isRecord(conversation.native_session_takeover)
|
|
2390
3136
|
? { ...conversation.native_session_takeover }
|
|
@@ -2400,6 +3146,7 @@ async function runApprove(options) {
|
|
|
2400
3146
|
...nativeTakeoverForUpdate,
|
|
2401
3147
|
terminal_bridge_approval: undefined,
|
|
2402
3148
|
terminal_bridge_approval_resolved_at: approvalResolvedAt,
|
|
3149
|
+
terminal_bridge_monitor_lock_version: TERMINAL_BRIDGE_MONITOR_LOCK_VERSION,
|
|
2403
3150
|
terminal_bridge_monitor_started_at: approvalResolvedAt,
|
|
2404
3151
|
terminal_bridge_last_activity_at: approvalResolvedAt,
|
|
2405
3152
|
terminal_bridge_last_activity_reason: "approval resolved",
|
|
@@ -2409,12 +3156,28 @@ async function runApprove(options) {
|
|
|
2409
3156
|
terminal_bridge_hard_deadline_at: deadlineAt(stringValue(nativeTakeoverForUpdate.terminal_bridge_started_at) ?? approvalResolvedAt, agentHardTimeoutMinutes)
|
|
2410
3157
|
};
|
|
2411
3158
|
delete nextNativeTakeover.terminal_bridge_approval;
|
|
2412
|
-
|
|
3159
|
+
let nextConversation = {
|
|
2413
3160
|
...conversation,
|
|
2414
3161
|
status: terminalBridgeEnabled(conversation) ? "waiting_for_agent" : conversation.status,
|
|
2415
3162
|
native_session_takeover: nextNativeTakeover,
|
|
2416
3163
|
updated_at: approvalResolvedAt
|
|
2417
3164
|
};
|
|
3165
|
+
const approvalLeaseDeadlines = [
|
|
3166
|
+
stringValue(nextNativeTakeover.terminal_bridge_inactivity_deadline_at),
|
|
3167
|
+
stringValue(nextNativeTakeover.terminal_bridge_hard_deadline_at)
|
|
3168
|
+
].filter((value) => Boolean(value));
|
|
3169
|
+
if (approvalLeaseDeadlines.length > 0) {
|
|
3170
|
+
const renewedLease = renewClaudeHookLease(nextConversation, new Date(Math.min(...approvalLeaseDeadlines.map((value) => Date.parse(value)))).toISOString());
|
|
3171
|
+
if (renewedLease) {
|
|
3172
|
+
nextConversation = {
|
|
3173
|
+
...nextConversation,
|
|
3174
|
+
native_session_takeover: {
|
|
3175
|
+
...nextNativeTakeover,
|
|
3176
|
+
claude_hook_lease_id: renewedLease.id
|
|
3177
|
+
}
|
|
3178
|
+
};
|
|
3179
|
+
}
|
|
3180
|
+
}
|
|
2418
3181
|
saveState(statePath, nextConversation);
|
|
2419
3182
|
const bridgeMonitor = startTerminalBridgeMonitorForConversation({
|
|
2420
3183
|
conversation: nextConversation,
|
|
@@ -2447,16 +3210,23 @@ async function runApprove(options) {
|
|
|
2447
3210
|
key: approval.key,
|
|
2448
3211
|
keys: approval.keys,
|
|
2449
3212
|
label: approval.label,
|
|
3213
|
+
decision_mode: approval.decisionMode,
|
|
3214
|
+
request_id: approval.requestId,
|
|
2450
3215
|
approval_fingerprint: actualFingerprint,
|
|
2451
3216
|
auto_approved: autoApproved,
|
|
2452
|
-
policy_rule_id:
|
|
3217
|
+
policy_rule_id: effectivePolicyRuleId,
|
|
2453
3218
|
monitor_pid: bridgeMonitor?.pid ?? null
|
|
2454
3219
|
});
|
|
2455
3220
|
}
|
|
2456
|
-
async function runTerminalConversationApprove({ options, conversationId, agent, terminalControl }) {
|
|
3221
|
+
async function runTerminalConversationApprove({ options, conversationId, agent, terminalControl, pid }) {
|
|
2457
3222
|
const approval = await createTerminalAgentBridge(options).approve(agent, terminalControl, {
|
|
2458
3223
|
expectedFingerprint: stringValue(options.expectedApprovalFingerprint),
|
|
2459
|
-
scrollbackLines: Number(options.scrollbackLines ?? 120)
|
|
3224
|
+
scrollbackLines: Number(options.scrollbackLines ?? 120),
|
|
3225
|
+
runtime: {
|
|
3226
|
+
pid,
|
|
3227
|
+
cwd: terminalControl.currentPath,
|
|
3228
|
+
terminalTarget: terminalControl.target
|
|
3229
|
+
}
|
|
2460
3230
|
});
|
|
2461
3231
|
if (!approval.approved) {
|
|
2462
3232
|
printJson({
|
|
@@ -2476,7 +3246,9 @@ async function runTerminalConversationApprove({ options, conversationId, agent,
|
|
|
2476
3246
|
terminal_target: terminalControl.target,
|
|
2477
3247
|
key: approval.key,
|
|
2478
3248
|
keys: approval.keys,
|
|
2479
|
-
label: approval.label
|
|
3249
|
+
label: approval.label,
|
|
3250
|
+
decision_mode: approval.decisionMode,
|
|
3251
|
+
request_id: approval.requestId
|
|
2480
3252
|
});
|
|
2481
3253
|
printJson({
|
|
2482
3254
|
conversation_id: conversationId,
|
|
@@ -2486,12 +3258,32 @@ async function runTerminalConversationApprove({ options, conversationId, agent,
|
|
|
2486
3258
|
key: approval.key,
|
|
2487
3259
|
keys: approval.keys,
|
|
2488
3260
|
label: approval.label,
|
|
2489
|
-
approval_fingerprint: approval.fingerprint
|
|
3261
|
+
approval_fingerprint: approval.fingerprint,
|
|
3262
|
+
decision_mode: approval.decisionMode,
|
|
3263
|
+
request_id: approval.requestId
|
|
2490
3264
|
});
|
|
2491
3265
|
}
|
|
2492
|
-
async function runTerminalConversationSend({ options, conversationId, agent, messageBody, terminalControl }) {
|
|
3266
|
+
async function runTerminalConversationSend({ options, conversationId, agent, pid, messageBody, terminalControl }) {
|
|
2493
3267
|
const terminalPayload = terminalSubmissionPayload(String(messageBody));
|
|
2494
|
-
|
|
3268
|
+
const terminalBridge = createTerminalAgentBridge(options);
|
|
3269
|
+
if (agent === "claude") {
|
|
3270
|
+
const status = await terminalBridge.status(agent, terminalControl, {
|
|
3271
|
+
scrollbackLines: Number(options.scrollbackLines ?? 120),
|
|
3272
|
+
runtime: {
|
|
3273
|
+
pid,
|
|
3274
|
+
cwd: terminalControl.currentPath,
|
|
3275
|
+
terminalTarget: terminalControl.target
|
|
3276
|
+
}
|
|
3277
|
+
});
|
|
3278
|
+
assertSafeClaudeTerminalSend(status);
|
|
3279
|
+
}
|
|
3280
|
+
await terminalBridge.send(agent, terminalControl, terminalPayload, {
|
|
3281
|
+
runtime: {
|
|
3282
|
+
pid,
|
|
3283
|
+
cwd: terminalControl.currentPath,
|
|
3284
|
+
terminalTarget: terminalControl.target
|
|
3285
|
+
}
|
|
3286
|
+
});
|
|
2495
3287
|
runtimeLog("info", "terminal_message_send", {
|
|
2496
3288
|
conversation_id: conversationId,
|
|
2497
3289
|
agent,
|
|
@@ -2552,19 +3344,62 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
2552
3344
|
forkTakeover: undefined
|
|
2553
3345
|
}))
|
|
2554
3346
|
: terminalSubmissionPayload(String(message.body ?? ""));
|
|
3347
|
+
const preSendRuntime = {
|
|
3348
|
+
...terminalRuntimeIdentityForConversation(nextConversation, terminalControl),
|
|
3349
|
+
messageId: message.id
|
|
3350
|
+
};
|
|
2555
3351
|
let preSendScreenFingerprint;
|
|
2556
3352
|
if (bridge) {
|
|
2557
3353
|
try {
|
|
2558
3354
|
const status = await terminalBridge.status(executor.kind, terminalControl, {
|
|
2559
|
-
scrollbackLines: Number(options.scrollbackLines ?? 120)
|
|
3355
|
+
scrollbackLines: Number(options.scrollbackLines ?? 120),
|
|
3356
|
+
runtime: preSendRuntime
|
|
2560
3357
|
});
|
|
3358
|
+
if (executor.kind === "claude") {
|
|
3359
|
+
assertSafeClaudeTerminalSend(status);
|
|
3360
|
+
}
|
|
2561
3361
|
preSendScreenFingerprint = terminalBridgeScreenFingerprint(status.screen.excerpt);
|
|
2562
3362
|
}
|
|
2563
|
-
catch {
|
|
2564
|
-
|
|
3363
|
+
catch (error) {
|
|
3364
|
+
if (executor.kind === "claude") {
|
|
3365
|
+
throw new Error(`refusing to send to Claude Code without a verified idle terminal: ${error instanceof Error ? error.message : String(error)}`);
|
|
3366
|
+
}
|
|
3367
|
+
// Codex delivery can still use its established fallback when capture is unavailable.
|
|
3368
|
+
}
|
|
3369
|
+
}
|
|
3370
|
+
if (bridge && executor.kind === "claude") {
|
|
3371
|
+
releaseClaudeHookLeasesForTerminal({
|
|
3372
|
+
storeDir: storeDirFromOptions(options),
|
|
3373
|
+
terminalControl,
|
|
3374
|
+
replacementConversationId: conversation.conversation_id
|
|
3375
|
+
});
|
|
3376
|
+
releaseClaudeHookLease(nextConversation);
|
|
3377
|
+
}
|
|
3378
|
+
const claudeHookLeaseState = bridge && executor.kind === "claude"
|
|
3379
|
+
? activateClaudeHookLease({
|
|
3380
|
+
options,
|
|
3381
|
+
conversation: nextConversation,
|
|
3382
|
+
message,
|
|
3383
|
+
terminalControl,
|
|
3384
|
+
expiresAt: deadlineAt(bridgeStartedAt, agentTimeoutMinutes > 0
|
|
3385
|
+
? Math.min(agentTimeoutMinutes, agentHardTimeoutMinutes)
|
|
3386
|
+
: agentHardTimeoutMinutes)
|
|
3387
|
+
})
|
|
3388
|
+
: undefined;
|
|
3389
|
+
const conversationWithHookLease = claudeHookLeaseState
|
|
3390
|
+
? withClaudeHookLeaseState(nextConversation, claudeHookLeaseState)
|
|
3391
|
+
: nextConversation;
|
|
3392
|
+
try {
|
|
3393
|
+
await terminalBridge.send(executor.kind, terminalControl, terminalPayload, {
|
|
3394
|
+
runtime: preSendRuntime
|
|
3395
|
+
});
|
|
3396
|
+
}
|
|
3397
|
+
catch (error) {
|
|
3398
|
+
if (claudeHookLeaseState) {
|
|
3399
|
+
releaseClaudeHookLease(conversationWithHookLease);
|
|
2565
3400
|
}
|
|
3401
|
+
throw error;
|
|
2566
3402
|
}
|
|
2567
|
-
await terminalBridge.send(executor.kind, terminalControl, terminalPayload);
|
|
2568
3403
|
const supersededConversationIds = bridge
|
|
2569
3404
|
? supersedeTerminalBridgeConversations({
|
|
2570
3405
|
storeDir: storeDirFromOptions(options),
|
|
@@ -2592,7 +3427,7 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
2592
3427
|
});
|
|
2593
3428
|
const bridgeConversation = bridge
|
|
2594
3429
|
? withTerminalBridgeState({
|
|
2595
|
-
conversation:
|
|
3430
|
+
conversation: conversationWithHookLease,
|
|
2596
3431
|
message,
|
|
2597
3432
|
startedAt: bridgeStartedAt,
|
|
2598
3433
|
agentTimeoutMinutes,
|
|
@@ -2654,7 +3489,7 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
2654
3489
|
function terminalSubmissionPayload(payload) {
|
|
2655
3490
|
return payload.replace(/[\r\n]+$/u, "");
|
|
2656
3491
|
}
|
|
2657
|
-
function createManagedTerminalConversationFromRawId({ options, conversationId, agent, messageBody, terminalControl }) {
|
|
3492
|
+
function createManagedTerminalConversationFromRawId({ options, conversationId, agent, pid, messageBody, terminalControl }) {
|
|
2658
3493
|
const workspace = terminalControl.currentPath ?? process.cwd();
|
|
2659
3494
|
const storeDir = expandHome(options.storeDir ?? options.logDir ?? defaultStoreDir(workspace));
|
|
2660
3495
|
cleanupIdleConversations(storeDir, options);
|
|
@@ -2685,6 +3520,9 @@ function createManagedTerminalConversationFromRawId({ options, conversationId, a
|
|
|
2685
3520
|
gatewaySession: options.gatewaySession,
|
|
2686
3521
|
openclawBin: options.openclawBin ?? resolveOptionalExecutable("openclaw")
|
|
2687
3522
|
});
|
|
3523
|
+
const claudeAgent = agent === "claude"
|
|
3524
|
+
? loadClaudeAgentRows(options).find((row) => row.pid === pid)
|
|
3525
|
+
: undefined;
|
|
2688
3526
|
const attachedConversation = withStoragePaths({
|
|
2689
3527
|
...conversation,
|
|
2690
3528
|
executor,
|
|
@@ -2701,6 +3539,8 @@ function createManagedTerminalConversationFromRawId({ options, conversationId, a
|
|
|
2701
3539
|
native_session_takeover: {
|
|
2702
3540
|
agent,
|
|
2703
3541
|
native_session_id: conversationId,
|
|
3542
|
+
terminal_agent_pid: pid,
|
|
3543
|
+
terminal_agent_session_id: claudeAgent?.sessionId,
|
|
2704
3544
|
source_cwd: workspace,
|
|
2705
3545
|
source_title: `Terminal-controlled ${executor.display_name} ${terminalControl.target}`,
|
|
2706
3546
|
strategy: "terminal_control",
|
|
@@ -2805,14 +3645,16 @@ function supersedeTerminalBridgeConversations({ storeDir, terminalControl, repla
|
|
|
2805
3645
|
continue;
|
|
2806
3646
|
}
|
|
2807
3647
|
const now = new Date().toISOString();
|
|
2808
|
-
|
|
3648
|
+
const closedConversation = {
|
|
2809
3649
|
...current,
|
|
2810
3650
|
status: "closed",
|
|
2811
3651
|
closed_at: now,
|
|
2812
3652
|
close_reason: "terminal bridge superseded by a newer task on the same terminal",
|
|
2813
3653
|
superseded_by_conversation_id: replacementConversationId,
|
|
2814
3654
|
updated_at: now
|
|
2815
|
-
}
|
|
3655
|
+
};
|
|
3656
|
+
saveState(candidateStatePath, closedConversation);
|
|
3657
|
+
releaseClaudeHookLease(closedConversation);
|
|
2816
3658
|
appendEvent(logPathForStatePath(candidateStatePath), {
|
|
2817
3659
|
ts: now,
|
|
2818
3660
|
conversation_id: current.conversation_id,
|
|
@@ -2854,7 +3696,7 @@ function runNativeCodexResumeSend({ options, conversation, nextConversation, sta
|
|
|
2854
3696
|
});
|
|
2855
3697
|
if (options.background) {
|
|
2856
3698
|
const outputPath = path.join(path.dirname(logPath), `${executor.kind}-native-resume-output.log`);
|
|
2857
|
-
const outputFd =
|
|
3699
|
+
const outputFd = openPrivateAppendFile(outputPath);
|
|
2858
3700
|
const child = spawn(codexPath, codexArgs, {
|
|
2859
3701
|
detached: true,
|
|
2860
3702
|
stdio: ["ignore", outputFd, outputFd],
|
|
@@ -3073,28 +3915,36 @@ function markTakeoverBootstrapped({ conversation, statePath, logPath, executor,
|
|
|
3073
3915
|
return nextConversation;
|
|
3074
3916
|
}
|
|
3075
3917
|
function markNativeSessionBootstrapped({ conversation, statePath, logPath, executor }) {
|
|
3076
|
-
const nativeTakeover = isRecord(conversation.native_session_takeover)
|
|
3077
|
-
? conversation.native_session_takeover
|
|
3078
|
-
: {};
|
|
3079
3918
|
const now = new Date().toISOString();
|
|
3080
|
-
const
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3919
|
+
const releaseLock = acquireFileLock(`${statePath}.lock`);
|
|
3920
|
+
let nextConversation = conversation;
|
|
3921
|
+
try {
|
|
3922
|
+
const current = loadState(statePath);
|
|
3923
|
+
const nativeTakeover = isRecord(current.native_session_takeover)
|
|
3924
|
+
? current.native_session_takeover
|
|
3925
|
+
: {};
|
|
3926
|
+
nextConversation = {
|
|
3927
|
+
...current,
|
|
3928
|
+
native_session_takeover: {
|
|
3929
|
+
...nativeTakeover,
|
|
3930
|
+
needs_bootstrap: false,
|
|
3931
|
+
bootstrapped_at: now
|
|
3932
|
+
},
|
|
3933
|
+
updated_at: now
|
|
3934
|
+
};
|
|
3935
|
+
saveState(statePath, nextConversation);
|
|
3936
|
+
}
|
|
3937
|
+
finally {
|
|
3938
|
+
releaseLock();
|
|
3939
|
+
}
|
|
3090
3940
|
appendEvent(logPath, {
|
|
3091
3941
|
ts: now,
|
|
3092
|
-
conversation_id:
|
|
3942
|
+
conversation_id: nextConversation.conversation_id,
|
|
3093
3943
|
event: "native_session_bootstrapped",
|
|
3094
3944
|
executor
|
|
3095
3945
|
});
|
|
3096
3946
|
runtimeLog("info", "native_session_bootstrapped", {
|
|
3097
|
-
conversation_id:
|
|
3947
|
+
conversation_id: nextConversation.conversation_id,
|
|
3098
3948
|
agent: executor.kind,
|
|
3099
3949
|
executor_session: executor.session,
|
|
3100
3950
|
state_path: statePath
|
|
@@ -3102,28 +3952,36 @@ function markNativeSessionBootstrapped({ conversation, statePath, logPath, execu
|
|
|
3102
3952
|
return nextConversation;
|
|
3103
3953
|
}
|
|
3104
3954
|
function markForkSessionBootstrapped({ conversation, statePath, logPath, executor }) {
|
|
3105
|
-
const forkTakeover = isRecord(conversation.fork_context_takeover)
|
|
3106
|
-
? conversation.fork_context_takeover
|
|
3107
|
-
: {};
|
|
3108
3955
|
const now = new Date().toISOString();
|
|
3109
|
-
const
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
3956
|
+
const releaseLock = acquireFileLock(`${statePath}.lock`);
|
|
3957
|
+
let nextConversation = conversation;
|
|
3958
|
+
try {
|
|
3959
|
+
const current = loadState(statePath);
|
|
3960
|
+
const forkTakeover = isRecord(current.fork_context_takeover)
|
|
3961
|
+
? current.fork_context_takeover
|
|
3962
|
+
: {};
|
|
3963
|
+
nextConversation = {
|
|
3964
|
+
...current,
|
|
3965
|
+
fork_context_takeover: {
|
|
3966
|
+
...forkTakeover,
|
|
3967
|
+
needs_bootstrap: false,
|
|
3968
|
+
bootstrapped_at: now
|
|
3969
|
+
},
|
|
3970
|
+
updated_at: now
|
|
3971
|
+
};
|
|
3972
|
+
saveState(statePath, nextConversation);
|
|
3973
|
+
}
|
|
3974
|
+
finally {
|
|
3975
|
+
releaseLock();
|
|
3976
|
+
}
|
|
3119
3977
|
appendEvent(logPath, {
|
|
3120
3978
|
ts: now,
|
|
3121
|
-
conversation_id:
|
|
3979
|
+
conversation_id: nextConversation.conversation_id,
|
|
3122
3980
|
event: "fork_session_bootstrapped",
|
|
3123
3981
|
executor
|
|
3124
3982
|
});
|
|
3125
3983
|
runtimeLog("info", "fork_session_bootstrapped", {
|
|
3126
|
-
conversation_id:
|
|
3984
|
+
conversation_id: nextConversation.conversation_id,
|
|
3127
3985
|
agent: executor.kind,
|
|
3128
3986
|
executor_session: executor.session,
|
|
3129
3987
|
state_path: statePath
|
|
@@ -3210,7 +4068,12 @@ function markConversationNeedsRecovery({ conversation, statePath, logPath, execu
|
|
|
3210
4068
|
};
|
|
3211
4069
|
}
|
|
3212
4070
|
async function runRenew(options) {
|
|
3213
|
-
const
|
|
4071
|
+
const loaded = loadConversationFromOptions(options);
|
|
4072
|
+
const { statePath, logPath } = loaded;
|
|
4073
|
+
const conversation = await migrateLegacyTerminalAgentIdentity({
|
|
4074
|
+
...loaded,
|
|
4075
|
+
options
|
|
4076
|
+
});
|
|
3214
4077
|
if (conversation.status === "closed") {
|
|
3215
4078
|
throw new Error(`cannot renew ${conversation.conversation_id}; conversation is closed`);
|
|
3216
4079
|
}
|
|
@@ -3242,17 +4105,38 @@ async function runRenew(options) {
|
|
|
3242
4105
|
throw new Error(`cannot renew ${conversation.conversation_id}; terminal bridge hard lifetime of ${hardTimeoutMinutes} minutes has elapsed`);
|
|
3243
4106
|
}
|
|
3244
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;
|
|
3245
4128
|
const renewed = {
|
|
3246
|
-
...
|
|
4129
|
+
...renewedBase,
|
|
3247
4130
|
status: "waiting_for_agent",
|
|
3248
4131
|
native_session_takeover: {
|
|
3249
|
-
...
|
|
4132
|
+
...renewedNativeTakeover,
|
|
4133
|
+
terminal_bridge_monitor_lock_version: TERMINAL_BRIDGE_MONITOR_LOCK_VERSION,
|
|
3250
4134
|
terminal_bridge_monitor_started_at: now,
|
|
3251
4135
|
terminal_bridge_last_activity_at: now,
|
|
3252
4136
|
terminal_bridge_inactivity_timeout_minutes: inactivityTimeoutMinutes,
|
|
3253
4137
|
terminal_bridge_hard_timeout_minutes: hardTimeoutMinutes,
|
|
3254
|
-
terminal_bridge_inactivity_deadline_at:
|
|
3255
|
-
terminal_bridge_hard_deadline_at:
|
|
4138
|
+
terminal_bridge_inactivity_deadline_at: inactivityDeadline,
|
|
4139
|
+
terminal_bridge_hard_deadline_at: hardDeadline,
|
|
3256
4140
|
terminal_bridge_renewed_at: now
|
|
3257
4141
|
},
|
|
3258
4142
|
updated_at: now
|
|
@@ -3309,6 +4193,318 @@ async function runRenew(options) {
|
|
|
3309
4193
|
monitor_pid: monitor?.pid ?? null
|
|
3310
4194
|
});
|
|
3311
4195
|
}
|
|
4196
|
+
async function runReconcileMonitors(options) {
|
|
4197
|
+
const storeDir = storeDirFromOptions(options);
|
|
4198
|
+
const conversations = listConversations(storeDir);
|
|
4199
|
+
const items = [];
|
|
4200
|
+
let ignored = 0;
|
|
4201
|
+
let launched = 0;
|
|
4202
|
+
let alreadyRunning = 0;
|
|
4203
|
+
let skipped = 0;
|
|
4204
|
+
let errors = 0;
|
|
4205
|
+
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
|
+
const statePath = expandHome(stringValue(listedConversation.state_path) ??
|
|
4214
|
+
statePathForConversationId(listedConversation.conversation_id, storeDir));
|
|
4215
|
+
const logPath = expandHome(stringValue(listedConversation.event_log_path) ??
|
|
4216
|
+
logPathForStatePath(statePath));
|
|
4217
|
+
try {
|
|
4218
|
+
const initialConversation = await migrateLegacyTerminalAgentIdentity({
|
|
4219
|
+
conversation: loadState(statePath),
|
|
4220
|
+
statePath,
|
|
4221
|
+
logPath,
|
|
4222
|
+
options
|
|
4223
|
+
});
|
|
4224
|
+
const initialEligibility = terminalBridgeReconciliationEligibility(initialConversation);
|
|
4225
|
+
if (!initialEligibility.eligible) {
|
|
4226
|
+
skipped += 1;
|
|
4227
|
+
items.push({
|
|
4228
|
+
conversation_id: initialConversation.conversation_id,
|
|
4229
|
+
status: "skipped",
|
|
4230
|
+
reason: initialEligibility.reason
|
|
4231
|
+
});
|
|
4232
|
+
continue;
|
|
4233
|
+
}
|
|
4234
|
+
const activeOwner = activeTerminalBridgeMonitorOwner(statePath, initialEligibility.terminalMessageId);
|
|
4235
|
+
if (activeOwner) {
|
|
4236
|
+
alreadyRunning += 1;
|
|
4237
|
+
items.push({
|
|
4238
|
+
conversation_id: initialConversation.conversation_id,
|
|
4239
|
+
status: "already_running",
|
|
4240
|
+
reason: "monitor_lock_owner_alive",
|
|
4241
|
+
monitor_owner_pid: activeOwner.ownerPid ?? null
|
|
4242
|
+
});
|
|
4243
|
+
continue;
|
|
4244
|
+
}
|
|
4245
|
+
const monitorLockVersion = Number(initialEligibility.nativeTakeover.terminal_bridge_monitor_lock_version);
|
|
4246
|
+
if (monitorLockVersion !== TERMINAL_BRIDGE_MONITOR_LOCK_VERSION) {
|
|
4247
|
+
if (Number.isFinite(monitorLockVersion)) {
|
|
4248
|
+
skipped += 1;
|
|
4249
|
+
items.push({
|
|
4250
|
+
conversation_id: initialConversation.conversation_id,
|
|
4251
|
+
status: "skipped",
|
|
4252
|
+
reason: "monitor_lock_version_unsupported",
|
|
4253
|
+
monitor_lock_version: monitorLockVersion
|
|
4254
|
+
});
|
|
4255
|
+
continue;
|
|
4256
|
+
}
|
|
4257
|
+
const legacyLaunchPid = latestTerminalBridgeMonitorLaunchPid(logPath);
|
|
4258
|
+
if (legacyLaunchPid === undefined) {
|
|
4259
|
+
skipped += 1;
|
|
4260
|
+
items.push({
|
|
4261
|
+
conversation_id: initialConversation.conversation_id,
|
|
4262
|
+
status: "skipped",
|
|
4263
|
+
reason: "legacy_monitor_ownership_unknown"
|
|
4264
|
+
});
|
|
4265
|
+
continue;
|
|
4266
|
+
}
|
|
4267
|
+
if (isProcessAlive(legacyLaunchPid)) {
|
|
4268
|
+
alreadyRunning += 1;
|
|
4269
|
+
items.push({
|
|
4270
|
+
conversation_id: initialConversation.conversation_id,
|
|
4271
|
+
status: "already_running",
|
|
4272
|
+
reason: "legacy_monitor_launch_pid_alive",
|
|
4273
|
+
monitor_owner_pid: legacyLaunchPid
|
|
4274
|
+
});
|
|
4275
|
+
continue;
|
|
4276
|
+
}
|
|
4277
|
+
}
|
|
4278
|
+
const prepared = prepareTerminalBridgeMonitorReconciliation({
|
|
4279
|
+
statePath,
|
|
4280
|
+
expectedMessageId: initialEligibility.terminalMessageId
|
|
4281
|
+
});
|
|
4282
|
+
if (!prepared.prepared) {
|
|
4283
|
+
if (prepared.alreadyRunning) {
|
|
4284
|
+
alreadyRunning += 1;
|
|
4285
|
+
items.push({
|
|
4286
|
+
conversation_id: initialConversation.conversation_id,
|
|
4287
|
+
status: "already_running",
|
|
4288
|
+
reason: prepared.reason,
|
|
4289
|
+
monitor_owner_pid: prepared.ownerPid ?? null
|
|
4290
|
+
});
|
|
4291
|
+
}
|
|
4292
|
+
else {
|
|
4293
|
+
skipped += 1;
|
|
4294
|
+
items.push({
|
|
4295
|
+
conversation_id: initialConversation.conversation_id,
|
|
4296
|
+
status: "skipped",
|
|
4297
|
+
reason: prepared.reason
|
|
4298
|
+
});
|
|
4299
|
+
}
|
|
4300
|
+
continue;
|
|
4301
|
+
}
|
|
4302
|
+
const monitor = startTerminalBridgeMonitorForConversation({
|
|
4303
|
+
conversation: prepared.conversation,
|
|
4304
|
+
statePath,
|
|
4305
|
+
logPath,
|
|
4306
|
+
options
|
|
4307
|
+
});
|
|
4308
|
+
if (!monitor) {
|
|
4309
|
+
skipped += 1;
|
|
4310
|
+
items.push({
|
|
4311
|
+
conversation_id: prepared.conversation.conversation_id,
|
|
4312
|
+
status: "skipped",
|
|
4313
|
+
reason: "terminal_bridge_monitor_launch_disabled"
|
|
4314
|
+
});
|
|
4315
|
+
continue;
|
|
4316
|
+
}
|
|
4317
|
+
const launchedAt = new Date().toISOString();
|
|
4318
|
+
appendEvent(logPath, {
|
|
4319
|
+
ts: launchedAt,
|
|
4320
|
+
conversation_id: prepared.conversation.conversation_id,
|
|
4321
|
+
event: "terminal_bridge_monitor_launch",
|
|
4322
|
+
pid: monitor.pid ?? null,
|
|
4323
|
+
terminal_control: prepared.terminalControl,
|
|
4324
|
+
reason: "startup_reconciliation",
|
|
4325
|
+
agent_timeout_minutes: prepared.inactivityTimeoutMinutes,
|
|
4326
|
+
agent_hard_timeout_minutes: prepared.hardTimeoutMinutes
|
|
4327
|
+
});
|
|
4328
|
+
runtimeLog("info", "terminal_bridge_monitor_reconciled", {
|
|
4329
|
+
conversation_id: prepared.conversation.conversation_id,
|
|
4330
|
+
monitor_pid: monitor.pid ?? null,
|
|
4331
|
+
terminal_target: prepared.terminalControl.target
|
|
4332
|
+
});
|
|
4333
|
+
launched += 1;
|
|
4334
|
+
items.push({
|
|
4335
|
+
conversation_id: prepared.conversation.conversation_id,
|
|
4336
|
+
status: "launched",
|
|
4337
|
+
reason: "startup_reconciliation",
|
|
4338
|
+
monitor_pid: monitor.pid ?? null
|
|
4339
|
+
});
|
|
4340
|
+
}
|
|
4341
|
+
catch (error) {
|
|
4342
|
+
errors += 1;
|
|
4343
|
+
items.push({
|
|
4344
|
+
conversation_id: listedConversation.conversation_id,
|
|
4345
|
+
status: "error",
|
|
4346
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
4347
|
+
});
|
|
4348
|
+
}
|
|
4349
|
+
}
|
|
4350
|
+
printJson({
|
|
4351
|
+
reconciled: true,
|
|
4352
|
+
store_dir: storeDir,
|
|
4353
|
+
checked: conversations.length,
|
|
4354
|
+
ignored,
|
|
4355
|
+
launched,
|
|
4356
|
+
already_running: alreadyRunning,
|
|
4357
|
+
skipped,
|
|
4358
|
+
errors,
|
|
4359
|
+
items
|
|
4360
|
+
});
|
|
4361
|
+
}
|
|
4362
|
+
function terminalBridgeReconciliationEligibility(conversation) {
|
|
4363
|
+
const nativeTakeover = isRecord(conversation?.native_session_takeover)
|
|
4364
|
+
? conversation.native_session_takeover
|
|
4365
|
+
: undefined;
|
|
4366
|
+
if (nativeTakeover?.terminal_bridge !== true) {
|
|
4367
|
+
return { eligible: false, reason: "not_terminal_bridge" };
|
|
4368
|
+
}
|
|
4369
|
+
if (!conversation.gateway_method) {
|
|
4370
|
+
return { eligible: false, reason: "gateway_method_missing" };
|
|
4371
|
+
}
|
|
4372
|
+
if (!stringValue(conversation.gateway_session) && !stringValue(conversation.openclaw_session)) {
|
|
4373
|
+
return { eligible: false, reason: "gateway_session_missing" };
|
|
4374
|
+
}
|
|
4375
|
+
if (!isWaitingForAgent(conversation.status)) {
|
|
4376
|
+
return {
|
|
4377
|
+
eligible: false,
|
|
4378
|
+
reason: `conversation_status_${String(conversation.status ?? "missing")}`
|
|
4379
|
+
};
|
|
4380
|
+
}
|
|
4381
|
+
const terminalMessageId = stringValue(nativeTakeover.terminal_bridge_message_id);
|
|
4382
|
+
const terminalControl = terminalControlFromTakeover(nativeTakeover);
|
|
4383
|
+
if (!terminalMessageId || !terminalControl) {
|
|
4384
|
+
return { eligible: false, reason: "terminal_bridge_identity_missing" };
|
|
4385
|
+
}
|
|
4386
|
+
const runtime = terminalRuntimeIdentityForConversation(conversation, terminalControl);
|
|
4387
|
+
if (!Number.isInteger(runtime.pid) || Number(runtime.pid) <= 0 || !stringValue(runtime.cwd)) {
|
|
4388
|
+
return { eligible: false, reason: "terminal_agent_identity_missing" };
|
|
4389
|
+
}
|
|
4390
|
+
const inactivityTimeoutMinutes = Number(nativeTakeover.terminal_bridge_inactivity_timeout_minutes);
|
|
4391
|
+
const hardTimeoutMinutes = Number(nativeTakeover.terminal_bridge_hard_timeout_minutes);
|
|
4392
|
+
const startedAtMs = validTimestampMs(nativeTakeover.terminal_bridge_started_at);
|
|
4393
|
+
const lastActivityAtMs = validTimestampMs(nativeTakeover.terminal_bridge_last_activity_at);
|
|
4394
|
+
const inactivityDeadlineAtMs = validTimestampMs(nativeTakeover.terminal_bridge_inactivity_deadline_at);
|
|
4395
|
+
const hardDeadlineAtMs = validTimestampMs(nativeTakeover.terminal_bridge_hard_deadline_at);
|
|
4396
|
+
if (!Number.isFinite(inactivityTimeoutMinutes) ||
|
|
4397
|
+
inactivityTimeoutMinutes <= 0 ||
|
|
4398
|
+
!Number.isFinite(hardTimeoutMinutes) ||
|
|
4399
|
+
hardTimeoutMinutes <= 0 ||
|
|
4400
|
+
startedAtMs === undefined ||
|
|
4401
|
+
lastActivityAtMs === undefined ||
|
|
4402
|
+
inactivityDeadlineAtMs === undefined ||
|
|
4403
|
+
hardDeadlineAtMs === undefined) {
|
|
4404
|
+
return { eligible: false, reason: "terminal_bridge_deadline_metadata_missing" };
|
|
4405
|
+
}
|
|
4406
|
+
return {
|
|
4407
|
+
eligible: true,
|
|
4408
|
+
nativeTakeover,
|
|
4409
|
+
terminalMessageId,
|
|
4410
|
+
terminalControl,
|
|
4411
|
+
runtime,
|
|
4412
|
+
inactivityTimeoutMinutes,
|
|
4413
|
+
hardTimeoutMinutes,
|
|
4414
|
+
inactivityDeadlineAtMs,
|
|
4415
|
+
hardDeadlineAtMs
|
|
4416
|
+
};
|
|
4417
|
+
}
|
|
4418
|
+
function latestTerminalBridgeMonitorLaunchPid(logPath) {
|
|
4419
|
+
let events;
|
|
4420
|
+
try {
|
|
4421
|
+
events = readExistingEvents(logPath);
|
|
4422
|
+
}
|
|
4423
|
+
catch {
|
|
4424
|
+
return undefined;
|
|
4425
|
+
}
|
|
4426
|
+
const launch = [...events].reverse().find((event) => event.event === "terminal_bridge_monitor_launch");
|
|
4427
|
+
const pid = Number(launch?.pid);
|
|
4428
|
+
return Number.isSafeInteger(pid) && pid > 1 ? pid : undefined;
|
|
4429
|
+
}
|
|
4430
|
+
function prepareTerminalBridgeMonitorReconciliation({ statePath, expectedMessageId }) {
|
|
4431
|
+
const releaseStateLock = acquireFileLock(`${statePath}.lock`);
|
|
4432
|
+
try {
|
|
4433
|
+
const conversation = loadState(statePath);
|
|
4434
|
+
const eligibility = terminalBridgeReconciliationEligibility(conversation);
|
|
4435
|
+
if (!eligibility.eligible) {
|
|
4436
|
+
return {
|
|
4437
|
+
prepared: false,
|
|
4438
|
+
alreadyRunning: false,
|
|
4439
|
+
reason: eligibility.reason
|
|
4440
|
+
};
|
|
4441
|
+
}
|
|
4442
|
+
if (eligibility.terminalMessageId !== expectedMessageId) {
|
|
4443
|
+
return {
|
|
4444
|
+
prepared: false,
|
|
4445
|
+
alreadyRunning: false,
|
|
4446
|
+
reason: "terminal_bridge_task_replaced"
|
|
4447
|
+
};
|
|
4448
|
+
}
|
|
4449
|
+
const activeOwner = activeTerminalBridgeMonitorOwner(statePath, eligibility.terminalMessageId);
|
|
4450
|
+
if (activeOwner) {
|
|
4451
|
+
return {
|
|
4452
|
+
prepared: false,
|
|
4453
|
+
alreadyRunning: true,
|
|
4454
|
+
reason: "monitor_lock_owner_alive",
|
|
4455
|
+
ownerPid: activeOwner.ownerPid
|
|
4456
|
+
};
|
|
4457
|
+
}
|
|
4458
|
+
let renewedLease;
|
|
4459
|
+
if (executorForConversation(conversation).kind === "claude") {
|
|
4460
|
+
const leaseDeadlineAtMs = Math.min(eligibility.inactivityDeadlineAtMs, eligibility.hardDeadlineAtMs);
|
|
4461
|
+
if (leaseDeadlineAtMs <= Date.now()) {
|
|
4462
|
+
return {
|
|
4463
|
+
prepared: false,
|
|
4464
|
+
alreadyRunning: false,
|
|
4465
|
+
reason: "claude_hook_lease_deadline_elapsed"
|
|
4466
|
+
};
|
|
4467
|
+
}
|
|
4468
|
+
renewedLease = renewClaudeHookLease(conversation, new Date(leaseDeadlineAtMs).toISOString());
|
|
4469
|
+
if (!renewedLease) {
|
|
4470
|
+
return {
|
|
4471
|
+
prepared: false,
|
|
4472
|
+
alreadyRunning: false,
|
|
4473
|
+
reason: "claude_hook_lease_refresh_failed"
|
|
4474
|
+
};
|
|
4475
|
+
}
|
|
4476
|
+
}
|
|
4477
|
+
const nextNativeTakeover = {
|
|
4478
|
+
...eligibility.nativeTakeover,
|
|
4479
|
+
terminal_bridge_monitor_lock_version: TERMINAL_BRIDGE_MONITOR_LOCK_VERSION,
|
|
4480
|
+
...(renewedLease ? { claude_hook_lease_id: renewedLease.id } : {})
|
|
4481
|
+
};
|
|
4482
|
+
const needsSave = eligibility.nativeTakeover.terminal_bridge_monitor_lock_version !==
|
|
4483
|
+
TERMINAL_BRIDGE_MONITOR_LOCK_VERSION ||
|
|
4484
|
+
(renewedLease !== undefined &&
|
|
4485
|
+
eligibility.nativeTakeover.claude_hook_lease_id !== renewedLease.id);
|
|
4486
|
+
const preparedConversation = needsSave
|
|
4487
|
+
? {
|
|
4488
|
+
...conversation,
|
|
4489
|
+
native_session_takeover: nextNativeTakeover,
|
|
4490
|
+
updated_at: new Date().toISOString()
|
|
4491
|
+
}
|
|
4492
|
+
: conversation;
|
|
4493
|
+
if (needsSave) {
|
|
4494
|
+
saveState(statePath, preparedConversation);
|
|
4495
|
+
}
|
|
4496
|
+
return {
|
|
4497
|
+
prepared: true,
|
|
4498
|
+
conversation: preparedConversation,
|
|
4499
|
+
terminalControl: eligibility.terminalControl,
|
|
4500
|
+
inactivityTimeoutMinutes: eligibility.inactivityTimeoutMinutes,
|
|
4501
|
+
hardTimeoutMinutes: eligibility.hardTimeoutMinutes
|
|
4502
|
+
};
|
|
4503
|
+
}
|
|
4504
|
+
finally {
|
|
4505
|
+
releaseStateLock();
|
|
4506
|
+
}
|
|
4507
|
+
}
|
|
3312
4508
|
function positiveMinutes(value, optionName) {
|
|
3313
4509
|
const parsed = Number(value);
|
|
3314
4510
|
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
@@ -3324,11 +4520,17 @@ async function runCancel(options) {
|
|
|
3324
4520
|
options,
|
|
3325
4521
|
conversationId: terminalConversation.conversationId,
|
|
3326
4522
|
agent: terminalConversation.agent,
|
|
3327
|
-
terminalControl: terminalConversation.terminalControl
|
|
4523
|
+
terminalControl: terminalConversation.terminalControl,
|
|
4524
|
+
pid: terminalConversation.pid
|
|
3328
4525
|
});
|
|
3329
4526
|
return;
|
|
3330
4527
|
}
|
|
3331
|
-
const
|
|
4528
|
+
const loaded = loadConversationFromOptions(options);
|
|
4529
|
+
const { statePath, logPath } = loaded;
|
|
4530
|
+
const conversation = await migrateLegacyTerminalAgentIdentity({
|
|
4531
|
+
...loaded,
|
|
4532
|
+
options
|
|
4533
|
+
});
|
|
3332
4534
|
if (["closed", "cancelled"].includes(conversation.status)) {
|
|
3333
4535
|
throw new Error(`cannot cancel ${conversation.conversation_id}; conversation is ${conversation.status}`);
|
|
3334
4536
|
}
|
|
@@ -3405,14 +4607,23 @@ async function runCancel(options) {
|
|
|
3405
4607
|
budget: budgetAction(nextConversation)
|
|
3406
4608
|
});
|
|
3407
4609
|
}
|
|
3408
|
-
async function runTerminalConversationCancel({ options, conversationId, agent, terminalControl }) {
|
|
3409
|
-
const cancellation = await createTerminalAgentBridge(options).cancel(agent, terminalControl
|
|
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
|
+
});
|
|
3410
4619
|
runtimeLog("info", "terminal_cancel_requested", {
|
|
3411
4620
|
conversation_id: conversationId,
|
|
3412
4621
|
agent,
|
|
3413
4622
|
terminal_target: terminalControl.target,
|
|
3414
4623
|
key: cancellation.key,
|
|
3415
4624
|
keys: cancellation.keys,
|
|
4625
|
+
denied_approval: cancellation.deniedApproval,
|
|
4626
|
+
request_id: cancellation.requestId,
|
|
3416
4627
|
cancel_requested: cancellation.cancelRequested,
|
|
3417
4628
|
reason: cancellation.reason
|
|
3418
4629
|
});
|
|
@@ -3423,11 +4634,16 @@ async function runTerminalConversationCancel({ options, conversationId, agent, t
|
|
|
3423
4634
|
reason: cancellation.reason,
|
|
3424
4635
|
terminal_control: terminalControl,
|
|
3425
4636
|
key: cancellation.key,
|
|
3426
|
-
keys: cancellation.keys
|
|
4637
|
+
keys: cancellation.keys,
|
|
4638
|
+
denied_approval: cancellation.deniedApproval,
|
|
4639
|
+
request_id: cancellation.requestId
|
|
3427
4640
|
});
|
|
3428
4641
|
}
|
|
3429
4642
|
async function runTerminalControlCancel({ options, conversation, statePath, logPath, agent, terminalControl }) {
|
|
3430
|
-
const cancellation = await createTerminalAgentBridge(options).cancel(agent, terminalControl
|
|
4643
|
+
const cancellation = await createTerminalAgentBridge(options).cancel(agent, terminalControl, {
|
|
4644
|
+
runtime: terminalRuntimeIdentityForConversation(conversation, terminalControl),
|
|
4645
|
+
scrollbackLines: Number(options.scrollbackLines ?? 120)
|
|
4646
|
+
});
|
|
3431
4647
|
if (!cancellation.cancelRequested) {
|
|
3432
4648
|
printJson({
|
|
3433
4649
|
conversation,
|
|
@@ -3445,27 +4661,36 @@ async function runTerminalControlCancel({ options, conversation, statePath, logP
|
|
|
3445
4661
|
event: "terminal_cancel_requested",
|
|
3446
4662
|
terminal_control: terminalControl,
|
|
3447
4663
|
key: cancellation.key,
|
|
3448
|
-
keys: cancellation.keys
|
|
4664
|
+
keys: cancellation.keys,
|
|
4665
|
+
denied_approval: cancellation.deniedApproval,
|
|
4666
|
+
request_id: cancellation.requestId
|
|
3449
4667
|
});
|
|
3450
4668
|
runtimeLog("info", "terminal_cancel_requested", {
|
|
3451
4669
|
conversation_id: conversation.conversation_id,
|
|
3452
4670
|
agent,
|
|
3453
4671
|
terminal_target: terminalControl.target,
|
|
3454
4672
|
key: cancellation.key,
|
|
3455
|
-
keys: cancellation.keys
|
|
4673
|
+
keys: cancellation.keys,
|
|
4674
|
+
denied_approval: cancellation.deniedApproval,
|
|
4675
|
+
request_id: cancellation.requestId
|
|
3456
4676
|
});
|
|
3457
4677
|
const nextConversation = {
|
|
3458
4678
|
...conversation,
|
|
4679
|
+
status: "cancelled",
|
|
4680
|
+
cancelled_at: now,
|
|
3459
4681
|
terminal_cancel_requested_at: now,
|
|
3460
4682
|
updated_at: now
|
|
3461
4683
|
};
|
|
3462
4684
|
saveState(statePath, nextConversation);
|
|
4685
|
+
releaseClaudeHookLease(nextConversation);
|
|
3463
4686
|
printJson({
|
|
3464
4687
|
conversation: nextConversation,
|
|
3465
4688
|
cancel_requested: true,
|
|
3466
4689
|
terminal_control: terminalControl,
|
|
3467
4690
|
key: cancellation.key,
|
|
3468
4691
|
keys: cancellation.keys,
|
|
4692
|
+
denied_approval: cancellation.deniedApproval,
|
|
4693
|
+
request_id: cancellation.requestId,
|
|
3469
4694
|
budget: budgetAction(nextConversation)
|
|
3470
4695
|
});
|
|
3471
4696
|
}
|
|
@@ -3536,7 +4761,7 @@ function runRecoveryDecision(options) {
|
|
|
3536
4761
|
const acpxArgs = buildAcpxPromptArgs({ executor, payload, model: executorModel });
|
|
3537
4762
|
if (options.background) {
|
|
3538
4763
|
const outputPath = path.join(path.dirname(logPath), `${executor.kind}-${options.mode}-output.log`);
|
|
3539
|
-
const outputFd =
|
|
4764
|
+
const outputFd = openPrivateAppendFile(outputPath);
|
|
3540
4765
|
const child = spawn(acpxPath, acpxArgs, {
|
|
3541
4766
|
detached: true,
|
|
3542
4767
|
stdio: ["ignore", outputFd, outputFd],
|
|
@@ -3647,6 +4872,7 @@ function runClose(options) {
|
|
|
3647
4872
|
updated_at: now
|
|
3648
4873
|
};
|
|
3649
4874
|
saveState(statePath, closed);
|
|
4875
|
+
releaseClaudeHookLease(closed);
|
|
3650
4876
|
appendEvent(logPath, {
|
|
3651
4877
|
ts: now,
|
|
3652
4878
|
conversation_id: conversation.conversation_id,
|
|
@@ -3812,7 +5038,7 @@ function startCallbackRetryMonitor({ statePath }) {
|
|
|
3812
5038
|
detached: true,
|
|
3813
5039
|
stdio: "ignore",
|
|
3814
5040
|
cwd: process.cwd(),
|
|
3815
|
-
env:
|
|
5041
|
+
env: environmentWithoutGatewayTokens()
|
|
3816
5042
|
});
|
|
3817
5043
|
child.unref();
|
|
3818
5044
|
return child;
|
|
@@ -3868,10 +5094,46 @@ function runCallbackRetryMonitor(options) {
|
|
|
3868
5094
|
}
|
|
3869
5095
|
}
|
|
3870
5096
|
async function runTerminalBridgeMonitor(options) {
|
|
5097
|
+
const statePath = expandHome(required(options.state, "--state is required"));
|
|
5098
|
+
const conversation = loadState(statePath);
|
|
5099
|
+
const nativeTakeover = isRecord(conversation.native_session_takeover)
|
|
5100
|
+
? conversation.native_session_takeover
|
|
5101
|
+
: undefined;
|
|
5102
|
+
const terminalMessageId = stringValue(nativeTakeover?.terminal_bridge_message_id) ?? "missing-message-id";
|
|
5103
|
+
const monitorLock = tryAcquireTerminalBridgeMonitorLock(statePath, terminalMessageId);
|
|
5104
|
+
if (!monitorLock.acquired) {
|
|
5105
|
+
runtimeLog("info", "terminal_bridge_monitor_already_running", {
|
|
5106
|
+
conversation_id: conversation.conversation_id,
|
|
5107
|
+
terminal_bridge_message_id: terminalMessageId,
|
|
5108
|
+
monitor_owner_pid: monitorLock.ownerPid
|
|
5109
|
+
});
|
|
5110
|
+
printJson({
|
|
5111
|
+
conversation,
|
|
5112
|
+
monitored: false,
|
|
5113
|
+
terminal_bridge: true,
|
|
5114
|
+
already_running: true,
|
|
5115
|
+
reason: "terminal_bridge_monitor_already_running",
|
|
5116
|
+
monitor_owner_pid: monitorLock.ownerPid ?? null
|
|
5117
|
+
});
|
|
5118
|
+
return;
|
|
5119
|
+
}
|
|
5120
|
+
try {
|
|
5121
|
+
await runTerminalBridgeMonitorWithLock(options);
|
|
5122
|
+
}
|
|
5123
|
+
finally {
|
|
5124
|
+
monitorLock.release();
|
|
5125
|
+
}
|
|
5126
|
+
}
|
|
5127
|
+
async function runTerminalBridgeMonitorWithLock(options) {
|
|
3871
5128
|
const statePath = expandHome(required(options.state, "--state is required"));
|
|
3872
5129
|
const logPath = expandHome(options.log ?? logPathForStatePath(statePath));
|
|
3873
5130
|
const pollIntervalMs = Math.max(50, Number(options.pollIntervalMs ?? DEFAULT_MONITOR_POLL_INTERVAL_MS));
|
|
3874
|
-
let conversation =
|
|
5131
|
+
let conversation = await migrateLegacyTerminalAgentIdentity({
|
|
5132
|
+
conversation: loadState(statePath),
|
|
5133
|
+
statePath,
|
|
5134
|
+
logPath,
|
|
5135
|
+
options
|
|
5136
|
+
});
|
|
3875
5137
|
const initialNativeTakeover = isRecord(conversation.native_session_takeover)
|
|
3876
5138
|
? conversation.native_session_takeover
|
|
3877
5139
|
: undefined;
|
|
@@ -3967,38 +5229,134 @@ async function runTerminalBridgeMonitor(options) {
|
|
|
3967
5229
|
}
|
|
3968
5230
|
});
|
|
3969
5231
|
printJson({
|
|
3970
|
-
conversation: stalledConversation,
|
|
5232
|
+
conversation: stalledConversation,
|
|
5233
|
+
monitored: true,
|
|
5234
|
+
terminal_bridge: true,
|
|
5235
|
+
stalled: true,
|
|
5236
|
+
reason: stalledConversation?.stalled_reason
|
|
5237
|
+
});
|
|
5238
|
+
return;
|
|
5239
|
+
}
|
|
5240
|
+
const requestText = String(nativeTakeover?.["terminal_bridge_request_text"] ?? conversation.user_request ?? "");
|
|
5241
|
+
const startedAt = stringValue(nativeTakeover?.["terminal_bridge_started_at"]);
|
|
5242
|
+
const terminalRuntime = terminalRuntimeIdentityForConversation(conversation, terminalControl);
|
|
5243
|
+
const screenChangedSinceSend = preSendScreenFingerprint !== undefined &&
|
|
5244
|
+
previousScreenFingerprint !== undefined &&
|
|
5245
|
+
previousScreenFingerprint !== preSendScreenFingerprint;
|
|
5246
|
+
const poll = await terminalBridge.monitorPoll({
|
|
5247
|
+
agent: executor.kind,
|
|
5248
|
+
terminalControl,
|
|
5249
|
+
screenOptions: {
|
|
5250
|
+
scrollbackLines: Number(options.scrollbackLines ?? 120),
|
|
5251
|
+
requestText,
|
|
5252
|
+
screenChangedSinceSend,
|
|
5253
|
+
runtime: terminalRuntime
|
|
5254
|
+
},
|
|
5255
|
+
durableRequest: {
|
|
5256
|
+
sessionId: terminalRuntime.sessionId,
|
|
5257
|
+
cwd: stringValue(nativeTakeover?.["source_cwd"]),
|
|
5258
|
+
requestText,
|
|
5259
|
+
requestHash: stringValue(nativeTakeover?.["terminal_bridge_request_hash"]),
|
|
5260
|
+
startedAt,
|
|
5261
|
+
context: {
|
|
5262
|
+
conversation,
|
|
5263
|
+
nativeTakeover,
|
|
5264
|
+
...terminalRuntime
|
|
5265
|
+
}
|
|
5266
|
+
}
|
|
5267
|
+
});
|
|
5268
|
+
const terminalStatus = poll.status;
|
|
5269
|
+
const approval = terminalStatus.approval_state;
|
|
5270
|
+
if (isRecord(approval) && approval.blocked === true && approval.approvable !== true) {
|
|
5271
|
+
const approvalReason = stringValue(approval.reason) ??
|
|
5272
|
+
"Claude Code permission state cannot be safely resolved through AKK";
|
|
5273
|
+
appendEvent(logPath, {
|
|
5274
|
+
ts: new Date().toISOString(),
|
|
5275
|
+
conversation_id: conversation.conversation_id,
|
|
5276
|
+
event: "terminal_bridge_approval_not_approvable",
|
|
5277
|
+
terminal_control: terminalControl,
|
|
5278
|
+
activity_state: terminalStatus.activity_state,
|
|
5279
|
+
reason: approvalReason
|
|
5280
|
+
});
|
|
5281
|
+
if (/waiting for hook consumption/iu.test(approvalReason)) {
|
|
5282
|
+
sleepSync(pollIntervalMs);
|
|
5283
|
+
continue;
|
|
5284
|
+
}
|
|
5285
|
+
const fingerprint = terminalBridgeApprovalFingerprint({ terminalControl, terminalStatus });
|
|
5286
|
+
const notification = recordTerminalBridgeApprovalNotification({
|
|
5287
|
+
statePath,
|
|
5288
|
+
logPath,
|
|
5289
|
+
terminalControl,
|
|
5290
|
+
terminalStatus,
|
|
5291
|
+
fingerprint
|
|
5292
|
+
});
|
|
5293
|
+
if (notification.duplicate) {
|
|
5294
|
+
printJson({
|
|
5295
|
+
conversation: notification.conversation,
|
|
5296
|
+
monitored: true,
|
|
5297
|
+
terminal_bridge: true,
|
|
5298
|
+
awaiting_approval: true,
|
|
5299
|
+
approvable: false,
|
|
5300
|
+
duplicate: true,
|
|
5301
|
+
reason: approvalReason,
|
|
5302
|
+
terminal_control: terminalControl,
|
|
5303
|
+
terminal_status: terminalStatus
|
|
5304
|
+
});
|
|
5305
|
+
return;
|
|
5306
|
+
}
|
|
5307
|
+
const callbackMessage = createMessage({
|
|
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
|
+
});
|
|
5344
|
+
return;
|
|
5345
|
+
}
|
|
5346
|
+
printJson({
|
|
5347
|
+
conversation: notification.conversation,
|
|
3971
5348
|
monitored: true,
|
|
3972
5349
|
terminal_bridge: true,
|
|
3973
|
-
|
|
3974
|
-
|
|
5350
|
+
awaiting_approval: true,
|
|
5351
|
+
approvable: false,
|
|
5352
|
+
delivered: false,
|
|
5353
|
+
message: callbackMessage,
|
|
5354
|
+
reason: "gateway_method_missing",
|
|
5355
|
+
terminal_control: terminalControl,
|
|
5356
|
+
terminal_status: terminalStatus
|
|
3975
5357
|
});
|
|
3976
5358
|
return;
|
|
3977
5359
|
}
|
|
3978
|
-
const requestText = String(nativeTakeover?.["terminal_bridge_request_text"] ?? conversation.user_request ?? "");
|
|
3979
|
-
const startedAt = stringValue(nativeTakeover?.["terminal_bridge_started_at"]);
|
|
3980
|
-
const screenChangedSinceSend = preSendScreenFingerprint !== undefined &&
|
|
3981
|
-
previousScreenFingerprint !== undefined &&
|
|
3982
|
-
previousScreenFingerprint !== preSendScreenFingerprint;
|
|
3983
|
-
const poll = await terminalBridge.monitorPoll({
|
|
3984
|
-
agent: executor.kind,
|
|
3985
|
-
terminalControl,
|
|
3986
|
-
screenOptions: {
|
|
3987
|
-
scrollbackLines: Number(options.scrollbackLines ?? 120),
|
|
3988
|
-
requestText,
|
|
3989
|
-
screenChangedSinceSend
|
|
3990
|
-
},
|
|
3991
|
-
durableRequest: {
|
|
3992
|
-
sessionId: stringValue(nativeTakeover?.["native_session_id"]),
|
|
3993
|
-
cwd: stringValue(nativeTakeover?.["source_cwd"]),
|
|
3994
|
-
requestText,
|
|
3995
|
-
requestHash: stringValue(nativeTakeover?.["terminal_bridge_request_hash"]),
|
|
3996
|
-
startedAt,
|
|
3997
|
-
context: { conversation, nativeTakeover }
|
|
3998
|
-
}
|
|
3999
|
-
});
|
|
4000
|
-
const terminalStatus = poll.status;
|
|
4001
|
-
const approval = terminalStatus.approval_state;
|
|
4002
5360
|
if (isRecord(approval) && approval.blocked === true) {
|
|
4003
5361
|
const fingerprint = terminalBridgeApprovalFingerprint({ terminalControl, terminalStatus });
|
|
4004
5362
|
appendEvent(logPath, {
|
|
@@ -4150,55 +5508,98 @@ async function runTerminalBridgeMonitor(options) {
|
|
|
4150
5508
|
: undefined;
|
|
4151
5509
|
const completionStable = completionFingerprint !== undefined && completionFingerprint === idleCompletionFingerprint;
|
|
4152
5510
|
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
|
|
5511
|
+
if (completion && completionStable && completionFingerprint) {
|
|
5512
|
+
const completionOutcome = completion.outcome === "failure" ? "failure" : "success";
|
|
5513
|
+
const callbackMessageId = deterministicTerminalCallbackMessageId({
|
|
5514
|
+
conversationId: conversation.conversation_id,
|
|
5515
|
+
terminalMessageId: currentMessageId,
|
|
5516
|
+
completionFingerprint,
|
|
5517
|
+
outcome: completionOutcome
|
|
4167
5518
|
});
|
|
4168
|
-
const
|
|
4169
|
-
|
|
4170
|
-
|
|
4171
|
-
|
|
4172
|
-
|
|
4173
|
-
|
|
4174
|
-
|
|
4175
|
-
|
|
4176
|
-
|
|
5519
|
+
const claim = claimTerminalBridgeCompletion({
|
|
5520
|
+
statePath,
|
|
5521
|
+
logPath,
|
|
5522
|
+
terminalMessageId: currentMessageId,
|
|
5523
|
+
completionFingerprint,
|
|
5524
|
+
completionId: completion.id,
|
|
5525
|
+
callbackMessageId,
|
|
5526
|
+
outcome: completionOutcome
|
|
5527
|
+
});
|
|
5528
|
+
if (!claim.claimed) {
|
|
5529
|
+
printJson({
|
|
5530
|
+
conversation: claim.conversation,
|
|
5531
|
+
monitored: true,
|
|
5532
|
+
terminal_bridge: true,
|
|
5533
|
+
completed: false,
|
|
5534
|
+
duplicate: true,
|
|
5535
|
+
reason: claim.reason
|
|
5536
|
+
});
|
|
5537
|
+
return;
|
|
5538
|
+
}
|
|
5539
|
+
try {
|
|
5540
|
+
conversation = claim.conversation;
|
|
5541
|
+
appendEvent(logPath, {
|
|
5542
|
+
ts: new Date().toISOString(),
|
|
5543
|
+
conversation_id: conversation.conversation_id,
|
|
5544
|
+
event: "terminal_bridge_completion_detected",
|
|
4177
5545
|
terminal_control: terminalControl,
|
|
4178
|
-
|
|
5546
|
+
match: completionMatch,
|
|
4179
5547
|
completion_source: completion.source,
|
|
5548
|
+
completion_outcome: completionOutcome,
|
|
4180
5549
|
completion_id: completion.id,
|
|
4181
5550
|
terminal_session: completionMetadata.session,
|
|
4182
|
-
|
|
4183
|
-
match: completionMatch,
|
|
5551
|
+
context_match: completionMetadata.context_match,
|
|
4184
5552
|
assistant_timestamp: completion?.timestamp,
|
|
4185
5553
|
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
|
-
|
|
5554
|
+
terminal_bridge_message_id: currentMessageId,
|
|
5555
|
+
callback_message_id: callbackMessageId
|
|
5556
|
+
});
|
|
5557
|
+
const callbackMessage = {
|
|
5558
|
+
...createMessage({
|
|
5559
|
+
conversation,
|
|
5560
|
+
from: executor.actor,
|
|
5561
|
+
to: "openclaw",
|
|
5562
|
+
type: completionOutcome === "failure" ? "error" : "done",
|
|
5563
|
+
requiresResponse: false,
|
|
5564
|
+
body: completion.text,
|
|
5565
|
+
metadata: {
|
|
5566
|
+
source: "terminal_bridge",
|
|
5567
|
+
terminal_control: terminalControl,
|
|
5568
|
+
...completionMetadata,
|
|
5569
|
+
completion_source: completion.source,
|
|
5570
|
+
completion_outcome: completionOutcome,
|
|
5571
|
+
completion_id: completion.id,
|
|
5572
|
+
terminal_session: completionMetadata.session,
|
|
5573
|
+
confidence: completion.confidence,
|
|
5574
|
+
match: completionMatch,
|
|
5575
|
+
assistant_timestamp: completion?.timestamp,
|
|
5576
|
+
rollout_turn_id: completion.source === "durable" ? completion.id : undefined,
|
|
5577
|
+
terminal_bridge_message_id: currentMessageId
|
|
5578
|
+
}
|
|
5579
|
+
}),
|
|
5580
|
+
id: callbackMessageId
|
|
5581
|
+
};
|
|
5582
|
+
runLockedCallback({
|
|
5583
|
+
...options,
|
|
5584
|
+
statePath,
|
|
5585
|
+
log: logPath,
|
|
5586
|
+
closeTerminalBridgeOnDone: completionOutcome === "success",
|
|
5587
|
+
trackCallbackDelivery: true,
|
|
5588
|
+
recoverTerminalCompletion: claim.resumed === true,
|
|
5589
|
+
preserveMessageId: true,
|
|
5590
|
+
messageJson: JSON.stringify(callbackMessage),
|
|
5591
|
+
gatewayMethod: conversation.gateway_method,
|
|
5592
|
+
gatewaySession: conversation.gateway_session,
|
|
5593
|
+
openclawSession: conversation.openclaw_session,
|
|
5594
|
+
openclawBin: conversation.openclaw_bin,
|
|
5595
|
+
gatewayUrl: stringValue(conversation.gateway_token) ? conversation.gateway_url : undefined,
|
|
5596
|
+
token: stringValue(conversation.gateway_token)
|
|
5597
|
+
});
|
|
5598
|
+
}
|
|
5599
|
+
finally {
|
|
5600
|
+
releaseClaudeHookLease(conversation);
|
|
5601
|
+
claim.release();
|
|
5602
|
+
}
|
|
4202
5603
|
return;
|
|
4203
5604
|
}
|
|
4204
5605
|
// A concrete approval or completion observed on this poll wins over a timeout boundary.
|
|
@@ -4288,6 +5689,46 @@ function terminalBridgeScreenFingerprint(value) {
|
|
|
4288
5689
|
? createHash("sha256").update(value).digest("hex")
|
|
4289
5690
|
: undefined;
|
|
4290
5691
|
}
|
|
5692
|
+
function terminalBridgeMonitorLockPath(statePath, terminalMessageId) {
|
|
5693
|
+
const messageKey = createHash("sha256")
|
|
5694
|
+
.update(terminalMessageId)
|
|
5695
|
+
.digest("hex")
|
|
5696
|
+
.slice(0, 20);
|
|
5697
|
+
return `${statePath}.terminal-bridge-monitor-${messageKey}.lock`;
|
|
5698
|
+
}
|
|
5699
|
+
function fileLockOwnerPid(lockPath) {
|
|
5700
|
+
return readFileLockOwner(lockPath).pid;
|
|
5701
|
+
}
|
|
5702
|
+
function activeTerminalBridgeMonitorOwner(statePath, terminalMessageId) {
|
|
5703
|
+
const lockPath = terminalBridgeMonitorLockPath(statePath, terminalMessageId);
|
|
5704
|
+
if (!fs.existsSync(lockPath) || staleFileLock(lockPath)) {
|
|
5705
|
+
return undefined;
|
|
5706
|
+
}
|
|
5707
|
+
return {
|
|
5708
|
+
lockPath,
|
|
5709
|
+
ownerPid: fileLockOwnerPid(lockPath)
|
|
5710
|
+
};
|
|
5711
|
+
}
|
|
5712
|
+
function tryAcquireTerminalBridgeMonitorLock(statePath, terminalMessageId) {
|
|
5713
|
+
const lockPath = terminalBridgeMonitorLockPath(statePath, terminalMessageId);
|
|
5714
|
+
try {
|
|
5715
|
+
return {
|
|
5716
|
+
acquired: true,
|
|
5717
|
+
lockPath,
|
|
5718
|
+
release: acquireFileLock(lockPath, { timeoutMs: 0 })
|
|
5719
|
+
};
|
|
5720
|
+
}
|
|
5721
|
+
catch (error) {
|
|
5722
|
+
if (isRecord(error) && error.code === "LOCK_TIMEOUT") {
|
|
5723
|
+
return {
|
|
5724
|
+
acquired: false,
|
|
5725
|
+
lockPath,
|
|
5726
|
+
ownerPid: fileLockOwnerPid(lockPath)
|
|
5727
|
+
};
|
|
5728
|
+
}
|
|
5729
|
+
throw error;
|
|
5730
|
+
}
|
|
5731
|
+
}
|
|
4291
5732
|
function terminalBridgeSendLockPath(storeDir, terminalControl) {
|
|
4292
5733
|
const terminalKey = createHash("sha256")
|
|
4293
5734
|
.update(JSON.stringify({
|
|
@@ -4302,6 +5743,111 @@ function terminalBridgeRequestFingerprint(value) {
|
|
|
4302
5743
|
const text = String(value ?? "").replace(/\s+/gu, " ").trim();
|
|
4303
5744
|
return text ? createHash("sha256").update(text).digest("hex") : undefined;
|
|
4304
5745
|
}
|
|
5746
|
+
function deterministicTerminalCallbackMessageId({ conversationId, terminalMessageId, completionFingerprint, outcome }) {
|
|
5747
|
+
const digest = createHash("sha256")
|
|
5748
|
+
.update(JSON.stringify({
|
|
5749
|
+
conversation_id: conversationId,
|
|
5750
|
+
terminal_message_id: terminalMessageId,
|
|
5751
|
+
completion_fingerprint: completionFingerprint,
|
|
5752
|
+
outcome
|
|
5753
|
+
}))
|
|
5754
|
+
.digest("hex")
|
|
5755
|
+
.slice(0, 32);
|
|
5756
|
+
return `msg-terminal-${digest}`;
|
|
5757
|
+
}
|
|
5758
|
+
function claimTerminalBridgeCompletion({ statePath, logPath, terminalMessageId, completionFingerprint, completionId, callbackMessageId, outcome }) {
|
|
5759
|
+
const release = acquireFileLock(`${statePath}.lock`);
|
|
5760
|
+
try {
|
|
5761
|
+
const conversation = loadState(statePath);
|
|
5762
|
+
const nativeTakeover = isRecord(conversation.native_session_takeover)
|
|
5763
|
+
? conversation.native_session_takeover
|
|
5764
|
+
: {};
|
|
5765
|
+
if (!isWaitingForAgent(conversation.status)) {
|
|
5766
|
+
release();
|
|
5767
|
+
return {
|
|
5768
|
+
claimed: false,
|
|
5769
|
+
conversation,
|
|
5770
|
+
reason: "conversation_no_longer_waiting"
|
|
5771
|
+
};
|
|
5772
|
+
}
|
|
5773
|
+
if (stringValue(nativeTakeover.terminal_bridge_message_id) !== terminalMessageId) {
|
|
5774
|
+
release();
|
|
5775
|
+
return {
|
|
5776
|
+
claimed: false,
|
|
5777
|
+
conversation,
|
|
5778
|
+
reason: "terminal_bridge_task_replaced"
|
|
5779
|
+
};
|
|
5780
|
+
}
|
|
5781
|
+
const existing = isRecord(nativeTakeover.terminal_bridge_completion_claim)
|
|
5782
|
+
? nativeTakeover.terminal_bridge_completion_claim
|
|
5783
|
+
: undefined;
|
|
5784
|
+
if (existing) {
|
|
5785
|
+
if (existing.callback_message_id === callbackMessageId &&
|
|
5786
|
+
existing.terminal_bridge_message_id === terminalMessageId &&
|
|
5787
|
+
existing.completion_fingerprint === completionFingerprint &&
|
|
5788
|
+
existing.outcome === outcome) {
|
|
5789
|
+
appendEvent(logPath, {
|
|
5790
|
+
ts: new Date().toISOString(),
|
|
5791
|
+
conversation_id: conversation.conversation_id,
|
|
5792
|
+
event: "terminal_bridge_completion_claim_resumed",
|
|
5793
|
+
terminal_bridge_message_id: terminalMessageId,
|
|
5794
|
+
completion_fingerprint: completionFingerprint,
|
|
5795
|
+
callback_message_id: callbackMessageId,
|
|
5796
|
+
outcome
|
|
5797
|
+
});
|
|
5798
|
+
return {
|
|
5799
|
+
claimed: true,
|
|
5800
|
+
resumed: true,
|
|
5801
|
+
conversation,
|
|
5802
|
+
release
|
|
5803
|
+
};
|
|
5804
|
+
}
|
|
5805
|
+
release();
|
|
5806
|
+
return {
|
|
5807
|
+
claimed: false,
|
|
5808
|
+
conversation,
|
|
5809
|
+
reason: "terminal_bridge_completion_claim_conflict"
|
|
5810
|
+
};
|
|
5811
|
+
}
|
|
5812
|
+
const claimedAt = new Date().toISOString();
|
|
5813
|
+
const claimedConversation = {
|
|
5814
|
+
...conversation,
|
|
5815
|
+
native_session_takeover: {
|
|
5816
|
+
...nativeTakeover,
|
|
5817
|
+
terminal_bridge_completion_claim: {
|
|
5818
|
+
terminal_bridge_message_id: terminalMessageId,
|
|
5819
|
+
completion_fingerprint: completionFingerprint,
|
|
5820
|
+
completion_id: completionId,
|
|
5821
|
+
callback_message_id: callbackMessageId,
|
|
5822
|
+
outcome,
|
|
5823
|
+
claimed_at: claimedAt
|
|
5824
|
+
}
|
|
5825
|
+
},
|
|
5826
|
+
updated_at: claimedAt
|
|
5827
|
+
};
|
|
5828
|
+
saveState(statePath, claimedConversation);
|
|
5829
|
+
appendEvent(logPath, {
|
|
5830
|
+
ts: claimedAt,
|
|
5831
|
+
conversation_id: conversation.conversation_id,
|
|
5832
|
+
event: "terminal_bridge_completion_claimed",
|
|
5833
|
+
terminal_bridge_message_id: terminalMessageId,
|
|
5834
|
+
completion_fingerprint: completionFingerprint,
|
|
5835
|
+
completion_id: completionId,
|
|
5836
|
+
callback_message_id: callbackMessageId,
|
|
5837
|
+
outcome
|
|
5838
|
+
});
|
|
5839
|
+
return {
|
|
5840
|
+
claimed: true,
|
|
5841
|
+
resumed: false,
|
|
5842
|
+
conversation: claimedConversation,
|
|
5843
|
+
release
|
|
5844
|
+
};
|
|
5845
|
+
}
|
|
5846
|
+
catch (error) {
|
|
5847
|
+
release();
|
|
5848
|
+
throw error;
|
|
5849
|
+
}
|
|
5850
|
+
}
|
|
4305
5851
|
function terminalBridgeActivityPersistIntervalMs(timeoutMinutes, pollIntervalMs) {
|
|
4306
5852
|
if (!Number.isFinite(timeoutMinutes) || timeoutMinutes <= 0) {
|
|
4307
5853
|
return 5 * 60 * 1000;
|
|
@@ -4330,6 +5876,13 @@ function persistTerminalBridgeActivity({ conversation, statePath, logPath, obser
|
|
|
4330
5876
|
const inactivityDeadlineAt = Number.isFinite(timeoutMinutes) && timeoutMinutes > 0
|
|
4331
5877
|
? new Date(observedAtMs + timeoutMinutes * 60 * 1000).toISOString()
|
|
4332
5878
|
: undefined;
|
|
5879
|
+
const hardDeadlineAt = stringValue(nativeTakeover.terminal_bridge_hard_deadline_at);
|
|
5880
|
+
const leaseDeadlineAt = inactivityDeadlineAt && hardDeadlineAt
|
|
5881
|
+
? new Date(Math.min(Date.parse(inactivityDeadlineAt), Date.parse(hardDeadlineAt))).toISOString()
|
|
5882
|
+
: inactivityDeadlineAt ?? hardDeadlineAt;
|
|
5883
|
+
const renewedLease = leaseDeadlineAt
|
|
5884
|
+
? renewClaudeHookLease(currentConversation, leaseDeadlineAt)
|
|
5885
|
+
: undefined;
|
|
4333
5886
|
const nextConversation = {
|
|
4334
5887
|
...currentConversation,
|
|
4335
5888
|
native_session_takeover: {
|
|
@@ -4338,7 +5891,8 @@ function persistTerminalBridgeActivity({ conversation, statePath, logPath, obser
|
|
|
4338
5891
|
terminal_bridge_last_activity_reason: reason,
|
|
4339
5892
|
terminal_bridge_inactivity_deadline_at: inactivityDeadlineAt,
|
|
4340
5893
|
terminal_bridge_inactivity_timeout_minutes: timeoutMinutes,
|
|
4341
|
-
terminal_bridge_hard_timeout_minutes: hardTimeoutMinutes
|
|
5894
|
+
terminal_bridge_hard_timeout_minutes: hardTimeoutMinutes,
|
|
5895
|
+
claude_hook_lease_id: renewedLease?.id ?? nativeTakeover.claude_hook_lease_id
|
|
4342
5896
|
},
|
|
4343
5897
|
updated_at: observedAt
|
|
4344
5898
|
};
|
|
@@ -4380,9 +5934,12 @@ function terminalBridgeApprovalCandidate({ executor, terminalControl, terminalSt
|
|
|
4380
5934
|
agent: executor.kind,
|
|
4381
5935
|
kind: stringValue(approval.prompt_kind) ?? "unknown",
|
|
4382
5936
|
command: stringValue(approval.command),
|
|
4383
|
-
|
|
5937
|
+
tool_name: stringValue(approval.tool_name),
|
|
5938
|
+
request_detail: stringValue(approval.request_detail),
|
|
5939
|
+
cwd: stringValue(approval.cwd) ?? terminalControl.currentPath,
|
|
4384
5940
|
fingerprint,
|
|
4385
|
-
terminal_target: terminalControl.target
|
|
5941
|
+
terminal_target: terminalControl.target,
|
|
5942
|
+
decision_mode: stringValue(approval.decision_mode)
|
|
4386
5943
|
};
|
|
4387
5944
|
}
|
|
4388
5945
|
async function loadCodexTerminalContext({ conversation, nativeTakeover, options }) {
|
|
@@ -4481,6 +6038,11 @@ function resolveOptionalExecutable(command) {
|
|
|
4481
6038
|
function packageRootDir() {
|
|
4482
6039
|
return path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
|
4483
6040
|
}
|
|
6041
|
+
function printVersion() {
|
|
6042
|
+
const packageJsonPath = path.join(packageRootDir(), "package.json");
|
|
6043
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
|
|
6044
|
+
process.stdout.write(`${packageJson.version}\n`);
|
|
6045
|
+
}
|
|
4484
6046
|
function runCheckedCommand(command, args, { label }) {
|
|
4485
6047
|
const result = spawnSync(command, args, {
|
|
4486
6048
|
encoding: "utf8",
|
|
@@ -4519,18 +6081,18 @@ function buildCallbackCommand({ statePath, gatewayUrl, token, openclawSession, g
|
|
|
4519
6081
|
"--state",
|
|
4520
6082
|
shellQuote(statePath)
|
|
4521
6083
|
];
|
|
4522
|
-
if (token) {
|
|
4523
|
-
parts.push("--gateway-url", shellQuote(gatewayUrl), "--token", shellQuote(token), "--openclaw-session", shellQuote(openclawSession));
|
|
4524
|
-
}
|
|
4525
|
-
else if (!gatewayMethod) {
|
|
4526
|
-
parts.push("--record-only");
|
|
4527
|
-
}
|
|
4528
6084
|
if (gatewayMethod) {
|
|
4529
6085
|
parts.push("--gateway-method", shellQuote(gatewayMethod), "--gateway-session", shellQuote(gatewaySession ?? openclawSession));
|
|
4530
6086
|
if (openclawBin) {
|
|
4531
6087
|
parts.push("--openclaw-bin", shellQuote(openclawBin));
|
|
4532
6088
|
}
|
|
4533
6089
|
}
|
|
6090
|
+
else if (token) {
|
|
6091
|
+
parts.push("--gateway-url", shellQuote(gatewayUrl), "--token", shellQuote(token), "--openclaw-session", shellQuote(openclawSession));
|
|
6092
|
+
}
|
|
6093
|
+
else {
|
|
6094
|
+
parts.push("--record-only");
|
|
6095
|
+
}
|
|
4534
6096
|
parts.push("--message-json", "'<structured-message-json>'");
|
|
4535
6097
|
return parts.join(" ");
|
|
4536
6098
|
}
|
|
@@ -4595,7 +6157,7 @@ function runLockedCallback(options) {
|
|
|
4595
6157
|
const logPath = expandHome(options.log ?? logPathForStatePath(options.statePath));
|
|
4596
6158
|
const conversation = loadState(options.statePath);
|
|
4597
6159
|
const executor = executorForConversation(conversation);
|
|
4598
|
-
const message = options.retryPending === true
|
|
6160
|
+
const message = options.retryPending === true || options.preserveMessageId === true
|
|
4599
6161
|
? parseMessageJson(messageInput)
|
|
4600
6162
|
: extractStructuredMessage({
|
|
4601
6163
|
conversation,
|
|
@@ -4614,7 +6176,11 @@ function runLockedCallback(options) {
|
|
|
4614
6176
|
isRecord(callbackDelivery?.message) &&
|
|
4615
6177
|
callbackDelivery.message.id === message.id &&
|
|
4616
6178
|
["pending", "failed"].includes(String(callbackDelivery.status ?? ""));
|
|
4617
|
-
|
|
6179
|
+
const duplicateMessage = isDuplicateMessage(existingEvents, message);
|
|
6180
|
+
const recoveringTerminalCompletion = options.recoverTerminalCompletion === true &&
|
|
6181
|
+
duplicateMessage &&
|
|
6182
|
+
isWaitingForAgent(conversation.status);
|
|
6183
|
+
if (duplicateMessage && !retryingPending && !recoveringTerminalCompletion) {
|
|
4618
6184
|
runtimeLog("info", "callback_duplicate", {
|
|
4619
6185
|
conversation_id: conversation.conversation_id,
|
|
4620
6186
|
agent: executor.kind,
|
|
@@ -4636,15 +6202,23 @@ function runLockedCallback(options) {
|
|
|
4636
6202
|
}
|
|
4637
6203
|
const closeTerminalBridgeOnDone = message.type === "done" &&
|
|
4638
6204
|
options.closeTerminalBridgeOnDone === true;
|
|
6205
|
+
const trackCallbackDelivery = closeTerminalBridgeOnDone ||
|
|
6206
|
+
options.trackCallbackDelivery === true ||
|
|
6207
|
+
callbackDelivery?.track_delivery === true;
|
|
4639
6208
|
const requiresDelivery = Boolean(options.gatewayMethod) || options.recordOnly !== true;
|
|
4640
6209
|
const deliveryAttempt = Number(callbackDelivery?.attempts ?? 0) + 1;
|
|
4641
6210
|
let nextConversation = retryingPending
|
|
4642
6211
|
? conversation
|
|
4643
6212
|
: applyMessageToConversation(conversation, message);
|
|
4644
|
-
|
|
6213
|
+
const storedFinalStatus = stringValue(callbackDelivery?.final_status);
|
|
6214
|
+
const finalStatus = storedFinalStatus &&
|
|
6215
|
+
CONVERSATION_STATUSES.has(storedFinalStatus)
|
|
6216
|
+
? storedFinalStatus
|
|
6217
|
+
: nextConversation.status;
|
|
6218
|
+
if (!retryingPending && !recoveringTerminalCompletion) {
|
|
4645
6219
|
appendEvent(logPath, messageEvent(message));
|
|
4646
6220
|
}
|
|
4647
|
-
if (
|
|
6221
|
+
if (trackCallbackDelivery && requiresDelivery) {
|
|
4648
6222
|
const now = new Date().toISOString();
|
|
4649
6223
|
nextConversation = {
|
|
4650
6224
|
...nextConversation,
|
|
@@ -4659,7 +6233,9 @@ function runLockedCallback(options) {
|
|
|
4659
6233
|
gateway_session: options.gatewaySession ?? options.openclawSession ?? conversation.openclaw_session,
|
|
4660
6234
|
gateway_url: options.gatewayUrl ?? conversation.gateway_url,
|
|
4661
6235
|
openclaw_bin: options.openclawBin ?? conversation.openclaw_bin,
|
|
4662
|
-
close_terminal_bridge_on_done:
|
|
6236
|
+
close_terminal_bridge_on_done: closeTerminalBridgeOnDone,
|
|
6237
|
+
track_delivery: true,
|
|
6238
|
+
final_status: finalStatus
|
|
4663
6239
|
},
|
|
4664
6240
|
updated_at: now
|
|
4665
6241
|
};
|
|
@@ -4712,12 +6288,17 @@ function runLockedCallback(options) {
|
|
|
4712
6288
|
});
|
|
4713
6289
|
const deliveredAt = new Date().toISOString();
|
|
4714
6290
|
let deliveredConversation = nextConversation;
|
|
4715
|
-
if (
|
|
6291
|
+
if (trackCallbackDelivery) {
|
|
6292
|
+
const deliveredStatus = closeTerminalBridgeOnDone ? "closed" : finalStatus;
|
|
4716
6293
|
deliveredConversation = {
|
|
4717
6294
|
...nextConversation,
|
|
4718
|
-
status:
|
|
4719
|
-
|
|
4720
|
-
|
|
6295
|
+
status: deliveredStatus,
|
|
6296
|
+
...(closeTerminalBridgeOnDone
|
|
6297
|
+
? {
|
|
6298
|
+
closed_at: deliveredAt,
|
|
6299
|
+
close_reason: "terminal bridge task completed"
|
|
6300
|
+
}
|
|
6301
|
+
: {}),
|
|
4721
6302
|
callback_delivery: {
|
|
4722
6303
|
...(isRecord(nextConversation.callback_delivery) ? nextConversation.callback_delivery : {}),
|
|
4723
6304
|
status: "delivered",
|
|
@@ -4734,7 +6315,7 @@ function runLockedCallback(options) {
|
|
|
4734
6315
|
event: "callback_delivery_succeeded",
|
|
4735
6316
|
message_id: message.id,
|
|
4736
6317
|
attempt: deliveryAttempt,
|
|
4737
|
-
status:
|
|
6318
|
+
status: deliveredStatus
|
|
4738
6319
|
});
|
|
4739
6320
|
}
|
|
4740
6321
|
printJson({
|
|
@@ -4747,7 +6328,7 @@ function runLockedCallback(options) {
|
|
|
4747
6328
|
});
|
|
4748
6329
|
}
|
|
4749
6330
|
catch (error) {
|
|
4750
|
-
if (
|
|
6331
|
+
if (trackCallbackDelivery) {
|
|
4751
6332
|
const failedAt = new Date().toISOString();
|
|
4752
6333
|
const failedConversation = {
|
|
4753
6334
|
...nextConversation,
|
|
@@ -4902,8 +6483,8 @@ function recordCallbackProcessDelivery({ logPath, conversation, message, event,
|
|
|
4902
6483
|
round: message.round,
|
|
4903
6484
|
...detail,
|
|
4904
6485
|
status: delivery.status,
|
|
4905
|
-
stdout: delivery.stdout,
|
|
4906
|
-
stderr: delivery.stderr
|
|
6486
|
+
stdout: redactString(delivery.stdout),
|
|
6487
|
+
stderr: redactString(delivery.stderr)
|
|
4907
6488
|
});
|
|
4908
6489
|
runtimeLog("info", runtimeEvent, {
|
|
4909
6490
|
conversation_id: conversation.conversation_id,
|
|
@@ -4916,25 +6497,149 @@ function recordCallbackProcessDelivery({ logPath, conversation, message, event,
|
|
|
4916
6497
|
}
|
|
4917
6498
|
function acquireFileLock(lockPath, { timeoutMs = 5000, retryMs = 50 } = {}) {
|
|
4918
6499
|
const started = Date.now();
|
|
6500
|
+
const token = randomUUID();
|
|
4919
6501
|
while (true) {
|
|
6502
|
+
let fd;
|
|
4920
6503
|
try {
|
|
4921
|
-
|
|
6504
|
+
fd = fs.openSync(lockPath, fs.constants.O_CREAT |
|
|
6505
|
+
fs.constants.O_EXCL |
|
|
6506
|
+
fs.constants.O_WRONLY |
|
|
6507
|
+
NO_FOLLOW_FLAG, PRIVATE_LOCK_FILE_MODE);
|
|
6508
|
+
fs.fchmodSync(fd, PRIVATE_LOCK_FILE_MODE);
|
|
6509
|
+
fs.writeFileSync(fd, `${JSON.stringify({
|
|
6510
|
+
pid: process.pid,
|
|
6511
|
+
token,
|
|
6512
|
+
created_at: new Date().toISOString()
|
|
6513
|
+
})}\n`, "utf8");
|
|
6514
|
+
fs.fsyncSync(fd);
|
|
4922
6515
|
fs.closeSync(fd);
|
|
4923
|
-
|
|
4924
|
-
|
|
4925
|
-
};
|
|
6516
|
+
fd = undefined;
|
|
6517
|
+
return () => releaseFileLock(lockPath, token);
|
|
4926
6518
|
}
|
|
4927
6519
|
catch (error) {
|
|
4928
|
-
if (
|
|
6520
|
+
if (fd !== undefined) {
|
|
6521
|
+
fs.closeSync(fd);
|
|
6522
|
+
}
|
|
6523
|
+
if (!isRecord(error) || error.code !== "EEXIST") {
|
|
4929
6524
|
throw error;
|
|
4930
6525
|
}
|
|
6526
|
+
if (reclaimStaleFileLock(lockPath)) {
|
|
6527
|
+
continue;
|
|
6528
|
+
}
|
|
4931
6529
|
if (Date.now() - started >= timeoutMs) {
|
|
4932
|
-
throw new Error(`timed out waiting for file lock: ${lockPath}`);
|
|
6530
|
+
throw Object.assign(new Error(`timed out waiting for file lock: ${lockPath}`), { code: "LOCK_TIMEOUT" });
|
|
4933
6531
|
}
|
|
4934
6532
|
sleepSync(retryMs);
|
|
4935
6533
|
}
|
|
4936
6534
|
}
|
|
4937
6535
|
}
|
|
6536
|
+
function staleFileLock(lockPath) {
|
|
6537
|
+
try {
|
|
6538
|
+
const stat = fs.lstatSync(lockPath);
|
|
6539
|
+
if (stat.isSymbolicLink() || !stat.isFile()) {
|
|
6540
|
+
throw new Error(`file lock must be a regular file, not a symlink: ${lockPath}`);
|
|
6541
|
+
}
|
|
6542
|
+
const owner = readFileLockOwner(lockPath);
|
|
6543
|
+
if (owner.pid !== undefined) {
|
|
6544
|
+
try {
|
|
6545
|
+
process.kill(owner.pid, 0);
|
|
6546
|
+
return false;
|
|
6547
|
+
}
|
|
6548
|
+
catch (error) {
|
|
6549
|
+
return isRecord(error) && error.code === "ESRCH";
|
|
6550
|
+
}
|
|
6551
|
+
}
|
|
6552
|
+
return Date.now() - stat.mtimeMs > 30_000;
|
|
6553
|
+
}
|
|
6554
|
+
catch (error) {
|
|
6555
|
+
return isRecord(error) && error.code === "ENOENT";
|
|
6556
|
+
}
|
|
6557
|
+
}
|
|
6558
|
+
function reclaimStaleFileLock(lockPath) {
|
|
6559
|
+
const reclaimPath = `${lockPath}.reclaim`;
|
|
6560
|
+
let reclaimFd;
|
|
6561
|
+
try {
|
|
6562
|
+
reclaimFd = fs.openSync(reclaimPath, fs.constants.O_CREAT |
|
|
6563
|
+
fs.constants.O_EXCL |
|
|
6564
|
+
fs.constants.O_WRONLY |
|
|
6565
|
+
NO_FOLLOW_FLAG, PRIVATE_LOCK_FILE_MODE);
|
|
6566
|
+
fs.fchmodSync(reclaimFd, PRIVATE_LOCK_FILE_MODE);
|
|
6567
|
+
fs.writeFileSync(reclaimFd, `${process.pid}\n`, "utf8");
|
|
6568
|
+
fs.fsyncSync(reclaimFd);
|
|
6569
|
+
}
|
|
6570
|
+
catch (error) {
|
|
6571
|
+
if (reclaimFd !== undefined) {
|
|
6572
|
+
fs.closeSync(reclaimFd);
|
|
6573
|
+
}
|
|
6574
|
+
if (isRecord(error) && error.code === "EEXIST") {
|
|
6575
|
+
return false;
|
|
6576
|
+
}
|
|
6577
|
+
throw error;
|
|
6578
|
+
}
|
|
6579
|
+
try {
|
|
6580
|
+
if (!staleFileLock(lockPath)) {
|
|
6581
|
+
return false;
|
|
6582
|
+
}
|
|
6583
|
+
try {
|
|
6584
|
+
fs.unlinkSync(lockPath);
|
|
6585
|
+
return true;
|
|
6586
|
+
}
|
|
6587
|
+
catch (error) {
|
|
6588
|
+
return isRecord(error) && error.code === "ENOENT";
|
|
6589
|
+
}
|
|
6590
|
+
}
|
|
6591
|
+
finally {
|
|
6592
|
+
fs.closeSync(reclaimFd);
|
|
6593
|
+
try {
|
|
6594
|
+
fs.unlinkSync(reclaimPath);
|
|
6595
|
+
}
|
|
6596
|
+
catch (error) {
|
|
6597
|
+
if (!isRecord(error) || error.code !== "ENOENT") {
|
|
6598
|
+
throw error;
|
|
6599
|
+
}
|
|
6600
|
+
}
|
|
6601
|
+
}
|
|
6602
|
+
}
|
|
6603
|
+
function releaseFileLock(lockPath, token) {
|
|
6604
|
+
try {
|
|
6605
|
+
if (readFileLockOwner(lockPath).token !== token) {
|
|
6606
|
+
return;
|
|
6607
|
+
}
|
|
6608
|
+
fs.unlinkSync(lockPath);
|
|
6609
|
+
}
|
|
6610
|
+
catch (error) {
|
|
6611
|
+
if (!isRecord(error) || error.code !== "ENOENT") {
|
|
6612
|
+
throw error;
|
|
6613
|
+
}
|
|
6614
|
+
}
|
|
6615
|
+
}
|
|
6616
|
+
function readFileLockOwner(lockPath) {
|
|
6617
|
+
try {
|
|
6618
|
+
const text = fs.readFileSync(lockPath, "utf8").trim();
|
|
6619
|
+
try {
|
|
6620
|
+
const owner = JSON.parse(text);
|
|
6621
|
+
if (isRecord(owner)) {
|
|
6622
|
+
const pid = Number(owner.pid);
|
|
6623
|
+
return {
|
|
6624
|
+
pid: Number.isSafeInteger(pid) && pid > 1 ? pid : undefined,
|
|
6625
|
+
token: stringValue(owner.token)
|
|
6626
|
+
};
|
|
6627
|
+
}
|
|
6628
|
+
}
|
|
6629
|
+
catch {
|
|
6630
|
+
// Legacy locks contained only the owner PID.
|
|
6631
|
+
}
|
|
6632
|
+
const legacyPid = Number(text);
|
|
6633
|
+
return {
|
|
6634
|
+
pid: Number.isSafeInteger(legacyPid) && legacyPid > 1
|
|
6635
|
+
? legacyPid
|
|
6636
|
+
: undefined
|
|
6637
|
+
};
|
|
6638
|
+
}
|
|
6639
|
+
catch {
|
|
6640
|
+
return {};
|
|
6641
|
+
}
|
|
6642
|
+
}
|
|
4938
6643
|
function sleepSync(ms) {
|
|
4939
6644
|
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
4940
6645
|
}
|
|
@@ -5497,6 +7202,7 @@ function markConversationStalled({ statePath, logPath, reason, detail = {} }) {
|
|
|
5497
7202
|
updated_at: now
|
|
5498
7203
|
};
|
|
5499
7204
|
saveState(statePath, stalledConversation);
|
|
7205
|
+
releaseClaudeHookLease(stalledConversation);
|
|
5500
7206
|
appendEvent(logPath, {
|
|
5501
7207
|
ts: now,
|
|
5502
7208
|
conversation_id: conversation.conversation_id,
|
|
@@ -5711,8 +7417,8 @@ function deliverStalledNotification({ statePath, logPath, conversation, message,
|
|
|
5711
7417
|
method: conversation.gateway_method,
|
|
5712
7418
|
message_id: message.id,
|
|
5713
7419
|
status: delivery.status,
|
|
5714
|
-
stdout: delivery.stdout,
|
|
5715
|
-
stderr: delivery.stderr
|
|
7420
|
+
stdout: redactString(delivery.stdout),
|
|
7421
|
+
stderr: redactString(delivery.stderr)
|
|
5716
7422
|
});
|
|
5717
7423
|
runtimeLog("info", `${eventPrefix}_gateway_method_delivery`, {
|
|
5718
7424
|
conversation_id: conversation.conversation_id,
|
|
@@ -5743,8 +7449,8 @@ function deliverStalledNotification({ statePath, logPath, conversation, message,
|
|
|
5743
7449
|
event: `${eventPrefix}_chat_send_delivery`,
|
|
5744
7450
|
message_id: message.id,
|
|
5745
7451
|
status: chatSendDelivery.status,
|
|
5746
|
-
stdout: chatSendDelivery.stdout,
|
|
5747
|
-
stderr: chatSendDelivery.stderr
|
|
7452
|
+
stdout: redactString(chatSendDelivery.stdout),
|
|
7453
|
+
stderr: redactString(chatSendDelivery.stderr)
|
|
5748
7454
|
});
|
|
5749
7455
|
runtimeLog("info", `${eventPrefix}_chat_send_delivery`, {
|
|
5750
7456
|
conversation_id: conversation.conversation_id,
|
|
@@ -5848,10 +7554,11 @@ function messageFingerprint(message) {
|
|
|
5848
7554
|
});
|
|
5849
7555
|
}
|
|
5850
7556
|
function deliverToOpenClaw({ gatewayUrl, token, openclawSession, message }) {
|
|
5851
|
-
const agent = `openclaw acp --url ${gatewayUrl} --
|
|
7557
|
+
const agent = `openclaw acp --url ${gatewayUrl} --session ${openclawSession}`;
|
|
5852
7558
|
const result = spawnSync("acpx", ["--agent", agent, JSON.stringify(message)], {
|
|
5853
7559
|
encoding: "utf8",
|
|
5854
|
-
maxBuffer: 1024 * 1024 * 10
|
|
7560
|
+
maxBuffer: 1024 * 1024 * 10,
|
|
7561
|
+
env: openClawGatewayEnvironment(token)
|
|
5855
7562
|
});
|
|
5856
7563
|
if (result.error) {
|
|
5857
7564
|
return {
|
|
@@ -5876,7 +7583,7 @@ function deliverToGatewayMethod({ method, openclawBin, gatewayUrl, token, sessio
|
|
|
5876
7583
|
sessionKey,
|
|
5877
7584
|
statePath,
|
|
5878
7585
|
logPath,
|
|
5879
|
-
conversation,
|
|
7586
|
+
conversation: redactCliOutput(conversation),
|
|
5880
7587
|
message
|
|
5881
7588
|
}),
|
|
5882
7589
|
"--json"
|
|
@@ -5884,12 +7591,10 @@ function deliverToGatewayMethod({ method, openclawBin, gatewayUrl, token, sessio
|
|
|
5884
7591
|
if (gatewayUrl) {
|
|
5885
7592
|
args.push("--url", gatewayUrl);
|
|
5886
7593
|
}
|
|
5887
|
-
if (token && token !== "<token>") {
|
|
5888
|
-
args.push("--token", token);
|
|
5889
|
-
}
|
|
5890
7594
|
const result = spawnSync(openclawBin ?? "openclaw", args, {
|
|
5891
7595
|
encoding: "utf8",
|
|
5892
|
-
maxBuffer: 1024 * 1024 * 10
|
|
7596
|
+
maxBuffer: 1024 * 1024 * 10,
|
|
7597
|
+
env: openClawGatewayEnvironment(token)
|
|
5893
7598
|
});
|
|
5894
7599
|
if (result.error) {
|
|
5895
7600
|
return {
|
|
@@ -5916,12 +7621,10 @@ function deliverToSessionSend({ openclawBin, gatewayUrl, token, params }) {
|
|
|
5916
7621
|
if (gatewayUrl) {
|
|
5917
7622
|
args.push("--url", gatewayUrl);
|
|
5918
7623
|
}
|
|
5919
|
-
if (token && token !== "<token>") {
|
|
5920
|
-
args.push("--token", token);
|
|
5921
|
-
}
|
|
5922
7624
|
const result = spawnSync(openclawBin ?? "openclaw", args, {
|
|
5923
7625
|
encoding: "utf8",
|
|
5924
|
-
maxBuffer: 1024 * 1024 * 10
|
|
7626
|
+
maxBuffer: 1024 * 1024 * 10,
|
|
7627
|
+
env: openClawGatewayEnvironment(token)
|
|
5925
7628
|
});
|
|
5926
7629
|
if (result.error) {
|
|
5927
7630
|
return {
|
|
@@ -5948,12 +7651,10 @@ function deliverToChatSend({ openclawBin, gatewayUrl, token, params }) {
|
|
|
5948
7651
|
if (gatewayUrl) {
|
|
5949
7652
|
args.push("--url", gatewayUrl);
|
|
5950
7653
|
}
|
|
5951
|
-
if (token && token !== "<token>") {
|
|
5952
|
-
args.push("--token", token);
|
|
5953
|
-
}
|
|
5954
7654
|
const result = spawnSync(openclawBin ?? "openclaw", args, {
|
|
5955
7655
|
encoding: "utf8",
|
|
5956
|
-
maxBuffer: 1024 * 1024 * 10
|
|
7656
|
+
maxBuffer: 1024 * 1024 * 10,
|
|
7657
|
+
env: openClawGatewayEnvironment(token)
|
|
5957
7658
|
});
|
|
5958
7659
|
if (result.error) {
|
|
5959
7660
|
return {
|
|
@@ -5968,6 +7669,15 @@ function deliverToChatSend({ openclawBin, gatewayUrl, token, params }) {
|
|
|
5968
7669
|
stderr: result.stderr ?? ""
|
|
5969
7670
|
};
|
|
5970
7671
|
}
|
|
7672
|
+
function openClawGatewayEnvironment(token) {
|
|
7673
|
+
if (!token || token === "<token>") {
|
|
7674
|
+
return process.env;
|
|
7675
|
+
}
|
|
7676
|
+
return {
|
|
7677
|
+
...process.env,
|
|
7678
|
+
OPENCLAW_GATEWAY_TOKEN: token
|
|
7679
|
+
};
|
|
7680
|
+
}
|
|
5971
7681
|
function captureJson(argv) {
|
|
5972
7682
|
const result = spawnSync(process.execPath, [new URL(import.meta.url).pathname, ...argv], {
|
|
5973
7683
|
encoding: "utf8"
|
|
@@ -6055,7 +7765,24 @@ function expandHome(filePath) {
|
|
|
6055
7765
|
return filePath;
|
|
6056
7766
|
}
|
|
6057
7767
|
function printJson(value) {
|
|
6058
|
-
process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
|
|
7768
|
+
process.stdout.write(`${JSON.stringify(redactCliOutput(value), null, 2)}\n`);
|
|
7769
|
+
}
|
|
7770
|
+
function redactCliOutput(value) {
|
|
7771
|
+
if (Array.isArray(value)) {
|
|
7772
|
+
return value.map((item) => redactCliOutput(item));
|
|
7773
|
+
}
|
|
7774
|
+
if (isRecord(value)) {
|
|
7775
|
+
return Object.fromEntries(Object.entries(value).flatMap(([key, item]) => {
|
|
7776
|
+
if (key === "gateway_token" || key === "gatewayToken") {
|
|
7777
|
+
return [];
|
|
7778
|
+
}
|
|
7779
|
+
if ((key === "callback_command" || key === "callbackCommand") && typeof item === "string") {
|
|
7780
|
+
return [[key, redactString(item)]];
|
|
7781
|
+
}
|
|
7782
|
+
return [[key, redactCliOutput(item)]];
|
|
7783
|
+
}));
|
|
7784
|
+
}
|
|
7785
|
+
return value;
|
|
6059
7786
|
}
|
|
6060
7787
|
function cleanProcessText(text) {
|
|
6061
7788
|
const value = String(text ?? "").trim();
|
|
@@ -6121,6 +7848,24 @@ function readOutputTail(outputPath, maxBytes = 65536) {
|
|
|
6121
7848
|
return "";
|
|
6122
7849
|
}
|
|
6123
7850
|
}
|
|
7851
|
+
function openPrivateAppendFile(filePath) {
|
|
7852
|
+
if (fs.existsSync(filePath) && fs.lstatSync(filePath).isSymbolicLink()) {
|
|
7853
|
+
throw new Error(`refusing agent output symlink: ${filePath}`);
|
|
7854
|
+
}
|
|
7855
|
+
const noFollow = typeof fs.constants.O_NOFOLLOW === "number"
|
|
7856
|
+
? fs.constants.O_NOFOLLOW
|
|
7857
|
+
: 0;
|
|
7858
|
+
const descriptor = fs.openSync(filePath, fs.constants.O_CREAT |
|
|
7859
|
+
fs.constants.O_APPEND |
|
|
7860
|
+
fs.constants.O_WRONLY |
|
|
7861
|
+
noFollow, 0o600);
|
|
7862
|
+
if (!fs.fstatSync(descriptor).isFile()) {
|
|
7863
|
+
fs.closeSync(descriptor);
|
|
7864
|
+
throw new Error(`agent output must be a regular file: ${filePath}`);
|
|
7865
|
+
}
|
|
7866
|
+
fs.fchmodSync(descriptor, 0o600);
|
|
7867
|
+
return descriptor;
|
|
7868
|
+
}
|
|
6124
7869
|
function detectModelSelectionError(text) {
|
|
6125
7870
|
const cleaned = cleanProcessText(text);
|
|
6126
7871
|
if (!cleaned) {
|
|
@@ -6194,6 +7939,8 @@ function withStoragePaths(conversation, paths) {
|
|
|
6194
7939
|
function usage() {
|
|
6195
7940
|
const agentList = EXECUTOR_KINDS.join("|");
|
|
6196
7941
|
process.stdout.write(`Usage:
|
|
7942
|
+
agent-knock-knock --help
|
|
7943
|
+
agent-knock-knock --version
|
|
6197
7944
|
agent-knock-knock new --request <text> [--agent ${agentList}] [--workspace <path>] [--store-dir <dir>]
|
|
6198
7945
|
agent-knock-knock record --state <file> --message-json <json>
|
|
6199
7946
|
agent-knock-knock bootstrap-prompt --callback-command <command> [--agent ${agentList}]
|
|
@@ -6205,10 +7952,12 @@ function usage() {
|
|
|
6205
7952
|
agent-knock-knock approve --conversation <id>
|
|
6206
7953
|
agent-knock-knock cancel --conversation <id> [--all-proxy <url>]
|
|
6207
7954
|
agent-knock-knock renew --conversation <id> [--minutes <inactivity-minutes>]
|
|
7955
|
+
agent-knock-knock reconcile-monitors [--store-dir <dir>]
|
|
6208
7956
|
agent-knock-knock retry-callback --conversation <id> [--store-dir <dir>]
|
|
6209
7957
|
agent-knock-knock recover --conversation <id> [--session <name>] [--all-proxy <url>]
|
|
6210
7958
|
agent-knock-knock close --conversation <id> [--reason <text>]
|
|
6211
7959
|
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]
|
|
6212
7961
|
agent-knock-knock doctor
|
|
6213
7962
|
agent-knock-knock agent takeover --agent codex --session-id <id> --strategy terminate_then_resume|terminal_control|fork [--create-conversation]
|
|
6214
7963
|
agent-knock-knock callback --state <file> --message-json <json> [--record-only]
|