@scotthuang/agent-knock-knock 0.2.47 → 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 +27 -0
- package/README.md +73 -50
- package/dist/src/approval-policy.d.ts +2 -1
- package/dist/src/approval-policy.js +12 -4
- package/dist/src/approval-policy.js.map +1 -1
- package/dist/src/claude-hook-store.js +17 -6
- package/dist/src/claude-hook-store.js.map +1 -1
- package/dist/src/claude-terminal-agent-adapter.js +16 -1
- package/dist/src/claude-terminal-agent-adapter.js.map +1 -1
- package/dist/src/cli.js +1037 -172
- 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 +92 -269
- 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 +3 -0
- package/dist/src/terminal-agent-adapter.js.map +1 -1
- package/dist/src/terminal-agent-bridge.d.ts +32 -2
- package/dist/src/terminal-agent-bridge.js +192 -34
- package/dist/src/terminal-agent-bridge.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 +2 -2
- package/package.json +10 -5
- package/templates/openclaw-skills/agent-knock-knock/SKILL.md +4 -18
package/dist/src/cli.js
CHANGED
|
@@ -15,21 +15,28 @@ import { CodexStoreAdapter } from "./codex-store-adapter.js";
|
|
|
15
15
|
import { applyMessageToConversation, budgetAction, createConversation, createMessage, executorForConversation, extractStructuredMessage, parseMessageJson, resolveExecutor } from "./protocol.js";
|
|
16
16
|
import { EXECUTOR_KINDS, acpxCommandForExecutor, executorDefinitionForKind, modelEnvForExecutor, normalizeModelForExecutor, proxyEnvForExecutor } from "./executors.js";
|
|
17
17
|
import { executorBootstrapPrompt } from "./bootstrap.js";
|
|
18
|
-
import { writeRuntimeLog } from "./runtime-log.js";
|
|
18
|
+
import { redactString, writeRuntimeLog } from "./runtime-log.js";
|
|
19
19
|
import { formatTranscript, readNdjsonLog } from "./transcript.js";
|
|
20
20
|
import { appendEvent, defaultStoreDir, listConversations, logPathForStatePath, loadConversationById, loadState, messageEvent, pathsForConversation, pathsForConversationDir, saveState, statePathForConversationId } from "./store.js";
|
|
21
21
|
import { planFork, planTakeover } from "./session-takeover-planner.js";
|
|
22
|
-
import { StaticTerminalControlProvider, TmuxTerminalControlProvider } from "./terminal-control-provider.js";
|
|
22
|
+
import { StaticTerminalControlProvider, TmuxTerminalControlProvider, terminalPaneContainsProcess } from "./terminal-control-provider.js";
|
|
23
23
|
import { parseTerminalConversationId } from "./terminal-agent-adapter.js";
|
|
24
24
|
import { createProductionTerminalAgentRegistry } from "./terminal-agent-registry.js";
|
|
25
25
|
import { StaticTerminalProcessSource, SystemTerminalProcessSource } from "./terminal-process-source.js";
|
|
26
26
|
import { TerminalAgentBridge } from "./terminal-agent-bridge.js";
|
|
27
|
+
import { evaluateApprovalPolicy } from "./approval-policy.js";
|
|
27
28
|
const DEFAULT_IDLE_TIMEOUT_MINUTES = 10080;
|
|
28
29
|
const DEFAULT_AGENT_TIMEOUT_MINUTES = 60;
|
|
29
30
|
const DEFAULT_AGENT_HARD_TIMEOUT_MINUTES = 720;
|
|
30
31
|
const DEFAULT_MONITOR_POLL_INTERVAL_MS = 5000;
|
|
31
32
|
const CALLBACK_RETRY_DELAYS_MS = [5000, 15000, 60000, 60000];
|
|
32
|
-
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";
|
|
33
40
|
const CONVERSATION_STATUSES = new Set([
|
|
34
41
|
"created",
|
|
35
42
|
"running",
|
|
@@ -102,7 +109,13 @@ catch (error) {
|
|
|
102
109
|
process.exit(1);
|
|
103
110
|
}
|
|
104
111
|
async function runCommand(commandName, options) {
|
|
105
|
-
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") {
|
|
106
119
|
runNew(options);
|
|
107
120
|
}
|
|
108
121
|
else if (commandName === "record") {
|
|
@@ -135,6 +148,9 @@ async function runCommand(commandName, options) {
|
|
|
135
148
|
else if (commandName === "renew") {
|
|
136
149
|
await runRenew(options);
|
|
137
150
|
}
|
|
151
|
+
else if (commandName === "reconcile-monitors") {
|
|
152
|
+
await runReconcileMonitors(options);
|
|
153
|
+
}
|
|
138
154
|
else if (commandName === "recover") {
|
|
139
155
|
runRecover(options);
|
|
140
156
|
}
|
|
@@ -330,7 +346,17 @@ function installOpenClawPlugin(openclawBin, root) {
|
|
|
330
346
|
}
|
|
331
347
|
function runDoctor(options) {
|
|
332
348
|
const commands = ["node", "openclaw", "acpx", "codex", "claude", "cursor"];
|
|
333
|
-
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
|
+
});
|
|
334
360
|
const root = packageRootDir();
|
|
335
361
|
const packageFiles = [
|
|
336
362
|
"dist/src/cli.js",
|
|
@@ -346,22 +372,43 @@ function runDoctor(options) {
|
|
|
346
372
|
});
|
|
347
373
|
const requiredOk = checks
|
|
348
374
|
.filter((check) => ["node", "openclaw", "acpx"].includes(check.command))
|
|
349
|
-
.every((check) => check.available
|
|
375
|
+
.every((check) => check.available &&
|
|
376
|
+
(check.command !== "node" ||
|
|
377
|
+
("version_supported" in check && check.version_supported === true)));
|
|
350
378
|
const agentOk = checks
|
|
351
379
|
.filter((check) => ["codex", "claude", "cursor"].includes(check.command))
|
|
352
380
|
.some((check) => check.available);
|
|
353
381
|
const filesOk = packageFiles.every((check) => check.exists);
|
|
382
|
+
const ok = requiredOk && agentOk && filesOk;
|
|
354
383
|
printJson({
|
|
355
|
-
ok
|
|
384
|
+
ok,
|
|
356
385
|
package_root: root,
|
|
357
386
|
checks,
|
|
358
387
|
package_files: packageFiles,
|
|
359
388
|
notes: [
|
|
360
|
-
|
|
389
|
+
`Node.js ${MINIMUM_NODE_VERSION}+, openclaw, and acpx are required.`,
|
|
361
390
|
"At least one local coding agent command should be available: codex, claude, or cursor."
|
|
362
391
|
],
|
|
363
392
|
options
|
|
364
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;
|
|
365
412
|
}
|
|
366
413
|
async function runAgent(options) {
|
|
367
414
|
const agentCommand = required(options.agentCommand, "agent subcommand is required: takeover");
|
|
@@ -505,6 +552,7 @@ async function runAgentTakeover(options) {
|
|
|
505
552
|
options,
|
|
506
553
|
takeoverMatchKind: "terminal_control",
|
|
507
554
|
terminalControl: target.terminalControl,
|
|
555
|
+
terminalAgentPid: target.pid,
|
|
508
556
|
needsBootstrap: false
|
|
509
557
|
});
|
|
510
558
|
return {
|
|
@@ -666,7 +714,11 @@ function selectTerminateTarget({ plan, session, activeSessions, expectedPid, all
|
|
|
666
714
|
}
|
|
667
715
|
async function listActiveSessionsWithTerminalControl(provider, options, terminalProvider = createTerminalControlProvider(options)) {
|
|
668
716
|
const activeSessions = await provider.listActiveSessions();
|
|
669
|
-
|
|
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 });
|
|
670
722
|
}
|
|
671
723
|
function createTerminalControlProvider(options) {
|
|
672
724
|
if (options.terminalsJson || options.terminalScreensJson || options.processesJson) {
|
|
@@ -826,10 +878,35 @@ function createRuntimeTerminalAgentRegistry(options) {
|
|
|
826
878
|
]
|
|
827
879
|
});
|
|
828
880
|
}
|
|
829
|
-
function createTerminalAgentBridge(options, terminalProvider = createTerminalControlProvider(options)) {
|
|
881
|
+
function createTerminalAgentBridge(options, terminalProvider = createTerminalControlProvider(options), registry = createRuntimeTerminalAgentRegistry(options)) {
|
|
882
|
+
const processSource = createTerminalProcessSource(options);
|
|
830
883
|
return new TerminalAgentBridge({
|
|
831
|
-
registry
|
|
832
|
-
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
|
+
}
|
|
833
910
|
});
|
|
834
911
|
}
|
|
835
912
|
function planTerminalControlTakeover(session, activeSessions) {
|
|
@@ -926,6 +1003,115 @@ function terminalRuntimeIdentityForConversation(conversation, terminalControl) {
|
|
|
926
1003
|
terminalTarget: terminalControl.target
|
|
927
1004
|
};
|
|
928
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
|
+
}
|
|
929
1115
|
function isTerminalControlCapability(value) {
|
|
930
1116
|
return typeof value === "string" && [
|
|
931
1117
|
"screen_status",
|
|
@@ -1029,7 +1215,7 @@ function createForkConversation({ agent, strategy, session, contextPackage, fork
|
|
|
1029
1215
|
next: `Use AKK send ${forkedConversation.conversation_id}: <message> to start the forked ${agent} session with the approved summary.`
|
|
1030
1216
|
};
|
|
1031
1217
|
}
|
|
1032
|
-
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 }) {
|
|
1033
1219
|
const workspace = session.cwd;
|
|
1034
1220
|
const storeDir = expandHome(options.storeDir ?? options.logDir ?? defaultStoreDir(workspace));
|
|
1035
1221
|
cleanupIdleConversations(storeDir, options);
|
|
@@ -1078,6 +1264,7 @@ function createNativeSessionConversation({ agent, strategy, session, modelInfo,
|
|
|
1078
1264
|
native_session_takeover: {
|
|
1079
1265
|
agent,
|
|
1080
1266
|
native_session_id: session.id,
|
|
1267
|
+
terminal_agent_pid: terminalAgentPid,
|
|
1081
1268
|
source_cwd: session.cwd,
|
|
1082
1269
|
source_title: session.title,
|
|
1083
1270
|
strategy,
|
|
@@ -1325,7 +1512,7 @@ function runDelegate(options) {
|
|
|
1325
1512
|
throw new Error(cleanProcessText(ensureSession.stderr || ensureSession.stdout || `acpx ${executor.kind} sessions ensure exited with status ${ensureSession.status}`));
|
|
1326
1513
|
}
|
|
1327
1514
|
const outputPath = path.join(newResult.paths.conversationDir, `${executor.kind}-output.log`);
|
|
1328
|
-
const outputFd =
|
|
1515
|
+
const outputFd = openPrivateAppendFile(outputPath);
|
|
1329
1516
|
const child = spawn(acpxPath, acpxArgs, {
|
|
1330
1517
|
detached: true,
|
|
1331
1518
|
stdio: ["ignore", outputFd, outputFd],
|
|
@@ -1472,7 +1659,7 @@ function startExecutorMonitor({ statePath, logPath, pid, outputPath, agentTimeou
|
|
|
1472
1659
|
detached: true,
|
|
1473
1660
|
stdio: "ignore",
|
|
1474
1661
|
cwd: process.cwd(),
|
|
1475
|
-
env:
|
|
1662
|
+
env: environmentWithoutGatewayTokens()
|
|
1476
1663
|
});
|
|
1477
1664
|
child.unref();
|
|
1478
1665
|
return child;
|
|
@@ -1503,7 +1690,7 @@ function startTerminalBridgeMonitor({ statePath, logPath, agentTimeoutMinutes, a
|
|
|
1503
1690
|
detached: true,
|
|
1504
1691
|
stdio: "ignore",
|
|
1505
1692
|
cwd: process.cwd(),
|
|
1506
|
-
env:
|
|
1693
|
+
env: environmentWithoutGatewayTokens()
|
|
1507
1694
|
});
|
|
1508
1695
|
child.unref();
|
|
1509
1696
|
return child;
|
|
@@ -1550,6 +1737,7 @@ function withTerminalBridgeState({ conversation, message, startedAt, agentTimeou
|
|
|
1550
1737
|
terminal_bridge_request_hash: terminalBridgeRequestFingerprint(message.body),
|
|
1551
1738
|
terminal_bridge_pre_send_screen_fingerprint: preSendScreenFingerprint,
|
|
1552
1739
|
terminal_bridge_completion_claim: undefined,
|
|
1740
|
+
terminal_bridge_monitor_lock_version: TERMINAL_BRIDGE_MONITOR_LOCK_VERSION,
|
|
1553
1741
|
terminal_bridge_monitor_started_at: startedAt,
|
|
1554
1742
|
terminal_bridge_last_activity_at: startedAt,
|
|
1555
1743
|
terminal_bridge_inactivity_timeout_minutes: agentTimeoutMinutes,
|
|
@@ -1773,16 +1961,23 @@ function modelForExecutor(executor, options = {}) {
|
|
|
1773
1961
|
return normalizeModelForExecutor(executor, modelEnvForExecutor(executor, process.env));
|
|
1774
1962
|
}
|
|
1775
1963
|
function environmentForExecutor(executor, options = {}) {
|
|
1964
|
+
const environment = environmentWithoutGatewayTokens();
|
|
1776
1965
|
const proxy = proxyForExecutor(executor, options);
|
|
1777
1966
|
if (!proxy) {
|
|
1778
|
-
return
|
|
1967
|
+
return environment;
|
|
1779
1968
|
}
|
|
1780
1969
|
return {
|
|
1781
|
-
...
|
|
1970
|
+
...environment,
|
|
1782
1971
|
ALL_PROXY: proxy,
|
|
1783
1972
|
all_proxy: proxy
|
|
1784
1973
|
};
|
|
1785
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
|
+
}
|
|
1786
1981
|
async function runList(options) {
|
|
1787
1982
|
const storeDir = expandHome(options.storeDir ?? options.logDir ?? defaultStoreDir(process.cwd()));
|
|
1788
1983
|
const cleanup = cleanupIdleConversations(storeDir, options);
|
|
@@ -1855,7 +2050,7 @@ async function buildNativeListGroups({ options, agentFilter, statusFilter }) {
|
|
|
1855
2050
|
};
|
|
1856
2051
|
}
|
|
1857
2052
|
const terminalProvider = createTerminalControlProvider(options);
|
|
1858
|
-
const bridge =
|
|
2053
|
+
const bridge = createTerminalAgentBridge(options, terminalProvider, registry);
|
|
1859
2054
|
const terminalScan = options.terminalDebug ? await terminalControlDiagnostics(terminalProvider) : undefined;
|
|
1860
2055
|
const terminalControlled = [];
|
|
1861
2056
|
const native = [];
|
|
@@ -1863,7 +2058,7 @@ async function buildNativeListGroups({ options, agentFilter, statusFilter }) {
|
|
|
1863
2058
|
const errors = [];
|
|
1864
2059
|
try {
|
|
1865
2060
|
const processSource = createTerminalProcessSource(options);
|
|
1866
|
-
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 });
|
|
1867
2062
|
const activeSessions = await bridge.listProcesses(snapshots, adapters.map((adapter) => adapter.agent));
|
|
1868
2063
|
activeCount = activeSessions.length;
|
|
1869
2064
|
const rootSessions = rootActiveProcesses(activeSessions);
|
|
@@ -2030,7 +2225,7 @@ function rootActiveProcesses(processes) {
|
|
|
2030
2225
|
const seenTerminalTargets = new Set();
|
|
2031
2226
|
return roots.filter((process) => {
|
|
2032
2227
|
const terminalTarget = process.terminalControl?.target
|
|
2033
|
-
? `${process.agent}:${process.terminalControl.target}`
|
|
2228
|
+
? `${process.agent}:${process.terminalControl.target}:${process.terminalControl.panePid}`
|
|
2034
2229
|
: undefined;
|
|
2035
2230
|
if (!terminalTarget) {
|
|
2036
2231
|
return true;
|
|
@@ -2091,7 +2286,12 @@ async function runStatus(options) {
|
|
|
2091
2286
|
});
|
|
2092
2287
|
return;
|
|
2093
2288
|
}
|
|
2094
|
-
const
|
|
2289
|
+
const loaded = loadConversationFromOptions(options);
|
|
2290
|
+
const { statePath, logPath } = loaded;
|
|
2291
|
+
const conversation = await migrateLegacyTerminalAgentIdentity({
|
|
2292
|
+
...loaded,
|
|
2293
|
+
options
|
|
2294
|
+
});
|
|
2095
2295
|
const events = readExistingEvents(logPath);
|
|
2096
2296
|
const result = {
|
|
2097
2297
|
conversation,
|
|
@@ -2172,7 +2372,12 @@ async function runDescribe(options) {
|
|
|
2172
2372
|
}));
|
|
2173
2373
|
return;
|
|
2174
2374
|
}
|
|
2175
|
-
const
|
|
2375
|
+
const loaded = loadConversationFromOptions(options);
|
|
2376
|
+
const { statePath, logPath } = loaded;
|
|
2377
|
+
const conversation = await migrateLegacyTerminalAgentIdentity({
|
|
2378
|
+
...loaded,
|
|
2379
|
+
options
|
|
2380
|
+
});
|
|
2176
2381
|
const events = readExistingEvents(logPath);
|
|
2177
2382
|
const terminalControl = terminalControlFromTakeover(isRecord(conversation.native_session_takeover) ? conversation.native_session_takeover : undefined);
|
|
2178
2383
|
const terminalStatus = terminalControl
|
|
@@ -2339,6 +2544,85 @@ function recordTerminalBridgeApprovalNotification({ statePath, logPath, terminal
|
|
|
2339
2544
|
releaseLock();
|
|
2340
2545
|
}
|
|
2341
2546
|
}
|
|
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
|
+
}
|
|
2342
2626
|
async function runSend(options) {
|
|
2343
2627
|
const messageBody = required(options.message ?? options.request, "--message is required");
|
|
2344
2628
|
if (options.agentHardTimeoutMinutes !== undefined) {
|
|
@@ -2379,64 +2663,19 @@ async function runSend(options) {
|
|
|
2379
2663
|
});
|
|
2380
2664
|
return;
|
|
2381
2665
|
}
|
|
2382
|
-
const
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
throw new Error(`cannot send to ${conversation.conversation_id}; choose recover, close, or delegate a new task first`);
|
|
2388
|
-
}
|
|
2389
|
-
if (conversation.status === "needs_model_selection" && !options.model) {
|
|
2390
|
-
throw new Error(`cannot send to ${conversation.conversation_id}; choose a supported model with --model first`);
|
|
2391
|
-
}
|
|
2392
|
-
const executor = executorForConversation(conversation);
|
|
2393
|
-
const type = options.type ?? (conversation.status === "waiting_for_openclaw" ? "answer" : "task");
|
|
2394
|
-
const nativeTakeoverForSend = isRecord(conversation.native_session_takeover)
|
|
2395
|
-
? conversation.native_session_takeover
|
|
2396
|
-
: undefined;
|
|
2397
|
-
const forkTakeoverForSend = isRecord(conversation.fork_context_takeover)
|
|
2398
|
-
? conversation.fork_context_takeover
|
|
2399
|
-
: undefined;
|
|
2400
|
-
const needsNativeTakeoverBootstrap = nativeTakeoverForSend?.["needs_bootstrap"] === true;
|
|
2401
|
-
const needsForkTakeoverBootstrap = forkTakeoverForSend?.["needs_bootstrap"] === true;
|
|
2402
|
-
const message = createMessage({
|
|
2403
|
-
conversation,
|
|
2404
|
-
from: "openclaw",
|
|
2405
|
-
to: executor.actor,
|
|
2406
|
-
type,
|
|
2407
|
-
body: messageBody,
|
|
2408
|
-
metadata: {
|
|
2409
|
-
executor_kind: executor.kind,
|
|
2410
|
-
executor_session: executor.session
|
|
2411
|
-
}
|
|
2666
|
+
const loaded = loadConversationFromOptions(options);
|
|
2667
|
+
const { statePath, logPath } = loaded;
|
|
2668
|
+
await migrateLegacyTerminalAgentIdentity({
|
|
2669
|
+
...loaded,
|
|
2670
|
+
options
|
|
2412
2671
|
});
|
|
2413
|
-
const
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
executor,
|
|
2419
|
-
claude_session: executor.kind === "claude" ? executor.session : conversation.claude_session,
|
|
2420
|
-
executor_model: options.model ?? conversation.executor_model,
|
|
2421
|
-
model_selection: conversation.status === "needs_model_selection"
|
|
2422
|
-
? {
|
|
2423
|
-
...previousModelSelection,
|
|
2424
|
-
resolved_at: new Date().toISOString(),
|
|
2425
|
-
selected_model: options.model
|
|
2426
|
-
}
|
|
2427
|
-
: conversation.model_selection
|
|
2428
|
-
};
|
|
2429
|
-
saveState(statePath, nextConversation);
|
|
2430
|
-
appendEvent(logPath, messageEvent(message));
|
|
2431
|
-
runtimeLog("info", "message_created", {
|
|
2432
|
-
conversation_id: conversation.conversation_id,
|
|
2433
|
-
agent: executor.kind,
|
|
2434
|
-
executor_session: executor.session,
|
|
2435
|
-
message_type: type,
|
|
2436
|
-
state_path: statePath,
|
|
2437
|
-
event_log_path: logPath,
|
|
2438
|
-
message: textSummary(messageBody)
|
|
2672
|
+
const prepared = prepareManagedSend({
|
|
2673
|
+
options,
|
|
2674
|
+
statePath,
|
|
2675
|
+
logPath,
|
|
2676
|
+
messageBody
|
|
2439
2677
|
});
|
|
2678
|
+
const { conversation, executor, nativeTakeoverForSend, forkTakeoverForSend, needsNativeTakeoverBootstrap, needsForkTakeoverBootstrap, message, nextConversation } = prepared;
|
|
2440
2679
|
const executorEnv = environmentForExecutor(executor, {
|
|
2441
2680
|
allProxy: options.allProxy ?? conversation.executor_all_proxy
|
|
2442
2681
|
});
|
|
@@ -2566,7 +2805,7 @@ async function runSend(options) {
|
|
|
2566
2805
|
const acpxArgs = buildAcpxPromptArgs({ executor, payload, model: executorModel });
|
|
2567
2806
|
if (options.background) {
|
|
2568
2807
|
const outputPath = path.join(path.dirname(logPath), `${executor.kind}-followup-output.log`);
|
|
2569
|
-
const outputFd =
|
|
2808
|
+
const outputFd = openPrivateAppendFile(outputPath);
|
|
2570
2809
|
const child = spawn(acpxPath, acpxArgs, {
|
|
2571
2810
|
detached: true,
|
|
2572
2811
|
stdio: ["ignore", outputFd, outputFd],
|
|
@@ -2743,7 +2982,12 @@ async function runApprove(options) {
|
|
|
2743
2982
|
});
|
|
2744
2983
|
return;
|
|
2745
2984
|
}
|
|
2746
|
-
const
|
|
2985
|
+
const loaded = loadConversationFromOptions(options);
|
|
2986
|
+
const { statePath, logPath } = loaded;
|
|
2987
|
+
const conversation = await migrateLegacyTerminalAgentIdentity({
|
|
2988
|
+
...loaded,
|
|
2989
|
+
options
|
|
2990
|
+
});
|
|
2747
2991
|
const nativeTakeover = isRecord(conversation.native_session_takeover)
|
|
2748
2992
|
? conversation.native_session_takeover
|
|
2749
2993
|
: undefined;
|
|
@@ -2760,15 +3004,67 @@ async function runApprove(options) {
|
|
|
2760
3004
|
const autoApproved = options.autoApproved === true;
|
|
2761
3005
|
const policyRuleId = stringValue(options.policyRuleId);
|
|
2762
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;
|
|
2763
3012
|
const approval = await createTerminalAgentBridge(options).approve(executor.kind, terminalControl, {
|
|
2764
3013
|
expectedFingerprint,
|
|
2765
3014
|
scrollbackLines: Number(options.scrollbackLines ?? 120),
|
|
2766
|
-
runtime:
|
|
3015
|
+
runtime: runtimeIdentity,
|
|
2767
3016
|
requiredDecisionMode: autoApproved && executor.kind === "claude"
|
|
2768
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
|
+
}
|
|
2769
3063
|
: undefined
|
|
2770
3064
|
});
|
|
2771
3065
|
const actualFingerprint = approval.fingerprint;
|
|
3066
|
+
const effectivePolicyRuleId = executorPolicyDecision?.ruleId ?? policyRuleId;
|
|
3067
|
+
const effectivePolicyFingerprint = executorPolicyDecision?.policyFingerprint ?? policyFingerprint;
|
|
2772
3068
|
if (!approval.approved) {
|
|
2773
3069
|
if (autoApproved) {
|
|
2774
3070
|
appendEvent(logPath, {
|
|
@@ -2780,8 +3076,8 @@ async function runApprove(options) {
|
|
|
2780
3076
|
terminal_control: terminalControl,
|
|
2781
3077
|
expected_fingerprint: expectedFingerprint,
|
|
2782
3078
|
actual_fingerprint: actualFingerprint,
|
|
2783
|
-
policy_rule_id:
|
|
2784
|
-
policy_fingerprint:
|
|
3079
|
+
policy_rule_id: effectivePolicyRuleId,
|
|
3080
|
+
policy_fingerprint: effectivePolicyFingerprint
|
|
2785
3081
|
});
|
|
2786
3082
|
}
|
|
2787
3083
|
printJson({
|
|
@@ -2808,8 +3104,8 @@ async function runApprove(options) {
|
|
|
2808
3104
|
request_id: approval.requestId,
|
|
2809
3105
|
approval_fingerprint: actualFingerprint,
|
|
2810
3106
|
auto_approved: autoApproved,
|
|
2811
|
-
policy_rule_id:
|
|
2812
|
-
policy_fingerprint:
|
|
3107
|
+
policy_rule_id: effectivePolicyRuleId,
|
|
3108
|
+
policy_fingerprint: effectivePolicyFingerprint
|
|
2813
3109
|
});
|
|
2814
3110
|
if (autoApproved) {
|
|
2815
3111
|
appendEvent(logPath, {
|
|
@@ -2819,8 +3115,8 @@ async function runApprove(options) {
|
|
|
2819
3115
|
action: "approved",
|
|
2820
3116
|
terminal_control: terminalControl,
|
|
2821
3117
|
approval_fingerprint: actualFingerprint,
|
|
2822
|
-
policy_rule_id:
|
|
2823
|
-
policy_fingerprint:
|
|
3118
|
+
policy_rule_id: effectivePolicyRuleId,
|
|
3119
|
+
policy_fingerprint: effectivePolicyFingerprint
|
|
2824
3120
|
});
|
|
2825
3121
|
}
|
|
2826
3122
|
runtimeLog("info", "terminal_approval_send", {
|
|
@@ -2833,8 +3129,8 @@ async function runApprove(options) {
|
|
|
2833
3129
|
request_id: approval.requestId,
|
|
2834
3130
|
approval_fingerprint: actualFingerprint,
|
|
2835
3131
|
auto_approved: autoApproved,
|
|
2836
|
-
policy_rule_id:
|
|
2837
|
-
policy_fingerprint:
|
|
3132
|
+
policy_rule_id: effectivePolicyRuleId,
|
|
3133
|
+
policy_fingerprint: effectivePolicyFingerprint
|
|
2838
3134
|
});
|
|
2839
3135
|
const nativeTakeoverForUpdate = isRecord(conversation.native_session_takeover)
|
|
2840
3136
|
? { ...conversation.native_session_takeover }
|
|
@@ -2850,6 +3146,7 @@ async function runApprove(options) {
|
|
|
2850
3146
|
...nativeTakeoverForUpdate,
|
|
2851
3147
|
terminal_bridge_approval: undefined,
|
|
2852
3148
|
terminal_bridge_approval_resolved_at: approvalResolvedAt,
|
|
3149
|
+
terminal_bridge_monitor_lock_version: TERMINAL_BRIDGE_MONITOR_LOCK_VERSION,
|
|
2853
3150
|
terminal_bridge_monitor_started_at: approvalResolvedAt,
|
|
2854
3151
|
terminal_bridge_last_activity_at: approvalResolvedAt,
|
|
2855
3152
|
terminal_bridge_last_activity_reason: "approval resolved",
|
|
@@ -2917,7 +3214,7 @@ async function runApprove(options) {
|
|
|
2917
3214
|
request_id: approval.requestId,
|
|
2918
3215
|
approval_fingerprint: actualFingerprint,
|
|
2919
3216
|
auto_approved: autoApproved,
|
|
2920
|
-
policy_rule_id:
|
|
3217
|
+
policy_rule_id: effectivePolicyRuleId,
|
|
2921
3218
|
monitor_pid: bridgeMonitor?.pid ?? null
|
|
2922
3219
|
});
|
|
2923
3220
|
}
|
|
@@ -2980,7 +3277,13 @@ async function runTerminalConversationSend({ options, conversationId, agent, pid
|
|
|
2980
3277
|
});
|
|
2981
3278
|
assertSafeClaudeTerminalSend(status);
|
|
2982
3279
|
}
|
|
2983
|
-
await terminalBridge.send(agent, terminalControl, terminalPayload
|
|
3280
|
+
await terminalBridge.send(agent, terminalControl, terminalPayload, {
|
|
3281
|
+
runtime: {
|
|
3282
|
+
pid,
|
|
3283
|
+
cwd: terminalControl.currentPath,
|
|
3284
|
+
terminalTarget: terminalControl.target
|
|
3285
|
+
}
|
|
3286
|
+
});
|
|
2984
3287
|
runtimeLog("info", "terminal_message_send", {
|
|
2985
3288
|
conversation_id: conversationId,
|
|
2986
3289
|
agent,
|
|
@@ -3087,7 +3390,9 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
3087
3390
|
? withClaudeHookLeaseState(nextConversation, claudeHookLeaseState)
|
|
3088
3391
|
: nextConversation;
|
|
3089
3392
|
try {
|
|
3090
|
-
await terminalBridge.send(executor.kind, terminalControl, terminalPayload
|
|
3393
|
+
await terminalBridge.send(executor.kind, terminalControl, terminalPayload, {
|
|
3394
|
+
runtime: preSendRuntime
|
|
3395
|
+
});
|
|
3091
3396
|
}
|
|
3092
3397
|
catch (error) {
|
|
3093
3398
|
if (claudeHookLeaseState) {
|
|
@@ -3391,7 +3696,7 @@ function runNativeCodexResumeSend({ options, conversation, nextConversation, sta
|
|
|
3391
3696
|
});
|
|
3392
3697
|
if (options.background) {
|
|
3393
3698
|
const outputPath = path.join(path.dirname(logPath), `${executor.kind}-native-resume-output.log`);
|
|
3394
|
-
const outputFd =
|
|
3699
|
+
const outputFd = openPrivateAppendFile(outputPath);
|
|
3395
3700
|
const child = spawn(codexPath, codexArgs, {
|
|
3396
3701
|
detached: true,
|
|
3397
3702
|
stdio: ["ignore", outputFd, outputFd],
|
|
@@ -3610,28 +3915,36 @@ function markTakeoverBootstrapped({ conversation, statePath, logPath, executor,
|
|
|
3610
3915
|
return nextConversation;
|
|
3611
3916
|
}
|
|
3612
3917
|
function markNativeSessionBootstrapped({ conversation, statePath, logPath, executor }) {
|
|
3613
|
-
const nativeTakeover = isRecord(conversation.native_session_takeover)
|
|
3614
|
-
? conversation.native_session_takeover
|
|
3615
|
-
: {};
|
|
3616
3918
|
const now = new Date().toISOString();
|
|
3617
|
-
const
|
|
3618
|
-
|
|
3619
|
-
|
|
3620
|
-
|
|
3621
|
-
|
|
3622
|
-
|
|
3623
|
-
|
|
3624
|
-
|
|
3625
|
-
|
|
3626
|
-
|
|
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
|
+
}
|
|
3627
3940
|
appendEvent(logPath, {
|
|
3628
3941
|
ts: now,
|
|
3629
|
-
conversation_id:
|
|
3942
|
+
conversation_id: nextConversation.conversation_id,
|
|
3630
3943
|
event: "native_session_bootstrapped",
|
|
3631
3944
|
executor
|
|
3632
3945
|
});
|
|
3633
3946
|
runtimeLog("info", "native_session_bootstrapped", {
|
|
3634
|
-
conversation_id:
|
|
3947
|
+
conversation_id: nextConversation.conversation_id,
|
|
3635
3948
|
agent: executor.kind,
|
|
3636
3949
|
executor_session: executor.session,
|
|
3637
3950
|
state_path: statePath
|
|
@@ -3639,28 +3952,36 @@ function markNativeSessionBootstrapped({ conversation, statePath, logPath, execu
|
|
|
3639
3952
|
return nextConversation;
|
|
3640
3953
|
}
|
|
3641
3954
|
function markForkSessionBootstrapped({ conversation, statePath, logPath, executor }) {
|
|
3642
|
-
const forkTakeover = isRecord(conversation.fork_context_takeover)
|
|
3643
|
-
? conversation.fork_context_takeover
|
|
3644
|
-
: {};
|
|
3645
3955
|
const now = new Date().toISOString();
|
|
3646
|
-
const
|
|
3647
|
-
|
|
3648
|
-
|
|
3649
|
-
|
|
3650
|
-
|
|
3651
|
-
|
|
3652
|
-
|
|
3653
|
-
|
|
3654
|
-
|
|
3655
|
-
|
|
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
|
+
}
|
|
3656
3977
|
appendEvent(logPath, {
|
|
3657
3978
|
ts: now,
|
|
3658
|
-
conversation_id:
|
|
3979
|
+
conversation_id: nextConversation.conversation_id,
|
|
3659
3980
|
event: "fork_session_bootstrapped",
|
|
3660
3981
|
executor
|
|
3661
3982
|
});
|
|
3662
3983
|
runtimeLog("info", "fork_session_bootstrapped", {
|
|
3663
|
-
conversation_id:
|
|
3984
|
+
conversation_id: nextConversation.conversation_id,
|
|
3664
3985
|
agent: executor.kind,
|
|
3665
3986
|
executor_session: executor.session,
|
|
3666
3987
|
state_path: statePath
|
|
@@ -3747,7 +4068,12 @@ function markConversationNeedsRecovery({ conversation, statePath, logPath, execu
|
|
|
3747
4068
|
};
|
|
3748
4069
|
}
|
|
3749
4070
|
async function runRenew(options) {
|
|
3750
|
-
const
|
|
4071
|
+
const loaded = loadConversationFromOptions(options);
|
|
4072
|
+
const { statePath, logPath } = loaded;
|
|
4073
|
+
const conversation = await migrateLegacyTerminalAgentIdentity({
|
|
4074
|
+
...loaded,
|
|
4075
|
+
options
|
|
4076
|
+
});
|
|
3751
4077
|
if (conversation.status === "closed") {
|
|
3752
4078
|
throw new Error(`cannot renew ${conversation.conversation_id}; conversation is closed`);
|
|
3753
4079
|
}
|
|
@@ -3804,6 +4130,7 @@ async function runRenew(options) {
|
|
|
3804
4130
|
status: "waiting_for_agent",
|
|
3805
4131
|
native_session_takeover: {
|
|
3806
4132
|
...renewedNativeTakeover,
|
|
4133
|
+
terminal_bridge_monitor_lock_version: TERMINAL_BRIDGE_MONITOR_LOCK_VERSION,
|
|
3807
4134
|
terminal_bridge_monitor_started_at: now,
|
|
3808
4135
|
terminal_bridge_last_activity_at: now,
|
|
3809
4136
|
terminal_bridge_inactivity_timeout_minutes: inactivityTimeoutMinutes,
|
|
@@ -3866,6 +4193,318 @@ async function runRenew(options) {
|
|
|
3866
4193
|
monitor_pid: monitor?.pid ?? null
|
|
3867
4194
|
});
|
|
3868
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
|
+
}
|
|
3869
4508
|
function positiveMinutes(value, optionName) {
|
|
3870
4509
|
const parsed = Number(value);
|
|
3871
4510
|
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
@@ -3886,7 +4525,12 @@ async function runCancel(options) {
|
|
|
3886
4525
|
});
|
|
3887
4526
|
return;
|
|
3888
4527
|
}
|
|
3889
|
-
const
|
|
4528
|
+
const loaded = loadConversationFromOptions(options);
|
|
4529
|
+
const { statePath, logPath } = loaded;
|
|
4530
|
+
const conversation = await migrateLegacyTerminalAgentIdentity({
|
|
4531
|
+
...loaded,
|
|
4532
|
+
options
|
|
4533
|
+
});
|
|
3890
4534
|
if (["closed", "cancelled"].includes(conversation.status)) {
|
|
3891
4535
|
throw new Error(`cannot cancel ${conversation.conversation_id}; conversation is ${conversation.status}`);
|
|
3892
4536
|
}
|
|
@@ -4117,7 +4761,7 @@ function runRecoveryDecision(options) {
|
|
|
4117
4761
|
const acpxArgs = buildAcpxPromptArgs({ executor, payload, model: executorModel });
|
|
4118
4762
|
if (options.background) {
|
|
4119
4763
|
const outputPath = path.join(path.dirname(logPath), `${executor.kind}-${options.mode}-output.log`);
|
|
4120
|
-
const outputFd =
|
|
4764
|
+
const outputFd = openPrivateAppendFile(outputPath);
|
|
4121
4765
|
const child = spawn(acpxPath, acpxArgs, {
|
|
4122
4766
|
detached: true,
|
|
4123
4767
|
stdio: ["ignore", outputFd, outputFd],
|
|
@@ -4394,7 +5038,7 @@ function startCallbackRetryMonitor({ statePath }) {
|
|
|
4394
5038
|
detached: true,
|
|
4395
5039
|
stdio: "ignore",
|
|
4396
5040
|
cwd: process.cwd(),
|
|
4397
|
-
env:
|
|
5041
|
+
env: environmentWithoutGatewayTokens()
|
|
4398
5042
|
});
|
|
4399
5043
|
child.unref();
|
|
4400
5044
|
return child;
|
|
@@ -4450,10 +5094,46 @@ function runCallbackRetryMonitor(options) {
|
|
|
4450
5094
|
}
|
|
4451
5095
|
}
|
|
4452
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) {
|
|
4453
5128
|
const statePath = expandHome(required(options.state, "--state is required"));
|
|
4454
5129
|
const logPath = expandHome(options.log ?? logPathForStatePath(statePath));
|
|
4455
5130
|
const pollIntervalMs = Math.max(50, Number(options.pollIntervalMs ?? DEFAULT_MONITOR_POLL_INTERVAL_MS));
|
|
4456
|
-
let conversation =
|
|
5131
|
+
let conversation = await migrateLegacyTerminalAgentIdentity({
|
|
5132
|
+
conversation: loadState(statePath),
|
|
5133
|
+
statePath,
|
|
5134
|
+
logPath,
|
|
5135
|
+
options
|
|
5136
|
+
});
|
|
4457
5137
|
const initialNativeTakeover = isRecord(conversation.native_session_takeover)
|
|
4458
5138
|
? conversation.native_session_takeover
|
|
4459
5139
|
: undefined;
|
|
@@ -5009,6 +5689,46 @@ function terminalBridgeScreenFingerprint(value) {
|
|
|
5009
5689
|
? createHash("sha256").update(value).digest("hex")
|
|
5010
5690
|
: undefined;
|
|
5011
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
|
+
}
|
|
5012
5732
|
function terminalBridgeSendLockPath(storeDir, terminalControl) {
|
|
5013
5733
|
const terminalKey = createHash("sha256")
|
|
5014
5734
|
.update(JSON.stringify({
|
|
@@ -5216,7 +5936,7 @@ function terminalBridgeApprovalCandidate({ executor, terminalControl, terminalSt
|
|
|
5216
5936
|
command: stringValue(approval.command),
|
|
5217
5937
|
tool_name: stringValue(approval.tool_name),
|
|
5218
5938
|
request_detail: stringValue(approval.request_detail),
|
|
5219
|
-
cwd: terminalControl.currentPath,
|
|
5939
|
+
cwd: stringValue(approval.cwd) ?? terminalControl.currentPath,
|
|
5220
5940
|
fingerprint,
|
|
5221
5941
|
terminal_target: terminalControl.target,
|
|
5222
5942
|
decision_mode: stringValue(approval.decision_mode)
|
|
@@ -5318,6 +6038,11 @@ function resolveOptionalExecutable(command) {
|
|
|
5318
6038
|
function packageRootDir() {
|
|
5319
6039
|
return path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
|
5320
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
|
+
}
|
|
5321
6046
|
function runCheckedCommand(command, args, { label }) {
|
|
5322
6047
|
const result = spawnSync(command, args, {
|
|
5323
6048
|
encoding: "utf8",
|
|
@@ -5356,18 +6081,18 @@ function buildCallbackCommand({ statePath, gatewayUrl, token, openclawSession, g
|
|
|
5356
6081
|
"--state",
|
|
5357
6082
|
shellQuote(statePath)
|
|
5358
6083
|
];
|
|
5359
|
-
if (token) {
|
|
5360
|
-
parts.push("--gateway-url", shellQuote(gatewayUrl), "--token", shellQuote(token), "--openclaw-session", shellQuote(openclawSession));
|
|
5361
|
-
}
|
|
5362
|
-
else if (!gatewayMethod) {
|
|
5363
|
-
parts.push("--record-only");
|
|
5364
|
-
}
|
|
5365
6084
|
if (gatewayMethod) {
|
|
5366
6085
|
parts.push("--gateway-method", shellQuote(gatewayMethod), "--gateway-session", shellQuote(gatewaySession ?? openclawSession));
|
|
5367
6086
|
if (openclawBin) {
|
|
5368
6087
|
parts.push("--openclaw-bin", shellQuote(openclawBin));
|
|
5369
6088
|
}
|
|
5370
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
|
+
}
|
|
5371
6096
|
parts.push("--message-json", "'<structured-message-json>'");
|
|
5372
6097
|
return parts.join(" ");
|
|
5373
6098
|
}
|
|
@@ -5758,8 +6483,8 @@ function recordCallbackProcessDelivery({ logPath, conversation, message, event,
|
|
|
5758
6483
|
round: message.round,
|
|
5759
6484
|
...detail,
|
|
5760
6485
|
status: delivery.status,
|
|
5761
|
-
stdout: delivery.stdout,
|
|
5762
|
-
stderr: delivery.stderr
|
|
6486
|
+
stdout: redactString(delivery.stdout),
|
|
6487
|
+
stderr: redactString(delivery.stderr)
|
|
5763
6488
|
});
|
|
5764
6489
|
runtimeLog("info", runtimeEvent, {
|
|
5765
6490
|
conversation_id: conversation.conversation_id,
|
|
@@ -5772,26 +6497,37 @@ function recordCallbackProcessDelivery({ logPath, conversation, message, event,
|
|
|
5772
6497
|
}
|
|
5773
6498
|
function acquireFileLock(lockPath, { timeoutMs = 5000, retryMs = 50 } = {}) {
|
|
5774
6499
|
const started = Date.now();
|
|
6500
|
+
const token = randomUUID();
|
|
5775
6501
|
while (true) {
|
|
6502
|
+
let fd;
|
|
5776
6503
|
try {
|
|
5777
|
-
|
|
5778
|
-
|
|
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");
|
|
5779
6514
|
fs.fsyncSync(fd);
|
|
5780
6515
|
fs.closeSync(fd);
|
|
5781
|
-
|
|
5782
|
-
|
|
5783
|
-
};
|
|
6516
|
+
fd = undefined;
|
|
6517
|
+
return () => releaseFileLock(lockPath, token);
|
|
5784
6518
|
}
|
|
5785
6519
|
catch (error) {
|
|
5786
|
-
if (
|
|
6520
|
+
if (fd !== undefined) {
|
|
6521
|
+
fs.closeSync(fd);
|
|
6522
|
+
}
|
|
6523
|
+
if (!isRecord(error) || error.code !== "EEXIST") {
|
|
5787
6524
|
throw error;
|
|
5788
6525
|
}
|
|
5789
|
-
if (
|
|
5790
|
-
fs.rmSync(lockPath, { force: true });
|
|
6526
|
+
if (reclaimStaleFileLock(lockPath)) {
|
|
5791
6527
|
continue;
|
|
5792
6528
|
}
|
|
5793
6529
|
if (Date.now() - started >= timeoutMs) {
|
|
5794
|
-
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" });
|
|
5795
6531
|
}
|
|
5796
6532
|
sleepSync(retryMs);
|
|
5797
6533
|
}
|
|
@@ -5799,12 +6535,14 @@ function acquireFileLock(lockPath, { timeoutMs = 5000, retryMs = 50 } = {}) {
|
|
|
5799
6535
|
}
|
|
5800
6536
|
function staleFileLock(lockPath) {
|
|
5801
6537
|
try {
|
|
5802
|
-
const stat = fs.
|
|
5803
|
-
|
|
5804
|
-
|
|
5805
|
-
|
|
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) {
|
|
5806
6544
|
try {
|
|
5807
|
-
process.kill(
|
|
6545
|
+
process.kill(owner.pid, 0);
|
|
5808
6546
|
return false;
|
|
5809
6547
|
}
|
|
5810
6548
|
catch (error) {
|
|
@@ -5817,6 +6555,91 @@ function staleFileLock(lockPath) {
|
|
|
5817
6555
|
return isRecord(error) && error.code === "ENOENT";
|
|
5818
6556
|
}
|
|
5819
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
|
+
}
|
|
5820
6643
|
function sleepSync(ms) {
|
|
5821
6644
|
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
5822
6645
|
}
|
|
@@ -6594,8 +7417,8 @@ function deliverStalledNotification({ statePath, logPath, conversation, message,
|
|
|
6594
7417
|
method: conversation.gateway_method,
|
|
6595
7418
|
message_id: message.id,
|
|
6596
7419
|
status: delivery.status,
|
|
6597
|
-
stdout: delivery.stdout,
|
|
6598
|
-
stderr: delivery.stderr
|
|
7420
|
+
stdout: redactString(delivery.stdout),
|
|
7421
|
+
stderr: redactString(delivery.stderr)
|
|
6599
7422
|
});
|
|
6600
7423
|
runtimeLog("info", `${eventPrefix}_gateway_method_delivery`, {
|
|
6601
7424
|
conversation_id: conversation.conversation_id,
|
|
@@ -6626,8 +7449,8 @@ function deliverStalledNotification({ statePath, logPath, conversation, message,
|
|
|
6626
7449
|
event: `${eventPrefix}_chat_send_delivery`,
|
|
6627
7450
|
message_id: message.id,
|
|
6628
7451
|
status: chatSendDelivery.status,
|
|
6629
|
-
stdout: chatSendDelivery.stdout,
|
|
6630
|
-
stderr: chatSendDelivery.stderr
|
|
7452
|
+
stdout: redactString(chatSendDelivery.stdout),
|
|
7453
|
+
stderr: redactString(chatSendDelivery.stderr)
|
|
6631
7454
|
});
|
|
6632
7455
|
runtimeLog("info", `${eventPrefix}_chat_send_delivery`, {
|
|
6633
7456
|
conversation_id: conversation.conversation_id,
|
|
@@ -6731,10 +7554,11 @@ function messageFingerprint(message) {
|
|
|
6731
7554
|
});
|
|
6732
7555
|
}
|
|
6733
7556
|
function deliverToOpenClaw({ gatewayUrl, token, openclawSession, message }) {
|
|
6734
|
-
const agent = `openclaw acp --url ${gatewayUrl} --
|
|
7557
|
+
const agent = `openclaw acp --url ${gatewayUrl} --session ${openclawSession}`;
|
|
6735
7558
|
const result = spawnSync("acpx", ["--agent", agent, JSON.stringify(message)], {
|
|
6736
7559
|
encoding: "utf8",
|
|
6737
|
-
maxBuffer: 1024 * 1024 * 10
|
|
7560
|
+
maxBuffer: 1024 * 1024 * 10,
|
|
7561
|
+
env: openClawGatewayEnvironment(token)
|
|
6738
7562
|
});
|
|
6739
7563
|
if (result.error) {
|
|
6740
7564
|
return {
|
|
@@ -6759,7 +7583,7 @@ function deliverToGatewayMethod({ method, openclawBin, gatewayUrl, token, sessio
|
|
|
6759
7583
|
sessionKey,
|
|
6760
7584
|
statePath,
|
|
6761
7585
|
logPath,
|
|
6762
|
-
conversation,
|
|
7586
|
+
conversation: redactCliOutput(conversation),
|
|
6763
7587
|
message
|
|
6764
7588
|
}),
|
|
6765
7589
|
"--json"
|
|
@@ -6767,12 +7591,10 @@ function deliverToGatewayMethod({ method, openclawBin, gatewayUrl, token, sessio
|
|
|
6767
7591
|
if (gatewayUrl) {
|
|
6768
7592
|
args.push("--url", gatewayUrl);
|
|
6769
7593
|
}
|
|
6770
|
-
if (token && token !== "<token>") {
|
|
6771
|
-
args.push("--token", token);
|
|
6772
|
-
}
|
|
6773
7594
|
const result = spawnSync(openclawBin ?? "openclaw", args, {
|
|
6774
7595
|
encoding: "utf8",
|
|
6775
|
-
maxBuffer: 1024 * 1024 * 10
|
|
7596
|
+
maxBuffer: 1024 * 1024 * 10,
|
|
7597
|
+
env: openClawGatewayEnvironment(token)
|
|
6776
7598
|
});
|
|
6777
7599
|
if (result.error) {
|
|
6778
7600
|
return {
|
|
@@ -6799,12 +7621,10 @@ function deliverToSessionSend({ openclawBin, gatewayUrl, token, params }) {
|
|
|
6799
7621
|
if (gatewayUrl) {
|
|
6800
7622
|
args.push("--url", gatewayUrl);
|
|
6801
7623
|
}
|
|
6802
|
-
if (token && token !== "<token>") {
|
|
6803
|
-
args.push("--token", token);
|
|
6804
|
-
}
|
|
6805
7624
|
const result = spawnSync(openclawBin ?? "openclaw", args, {
|
|
6806
7625
|
encoding: "utf8",
|
|
6807
|
-
maxBuffer: 1024 * 1024 * 10
|
|
7626
|
+
maxBuffer: 1024 * 1024 * 10,
|
|
7627
|
+
env: openClawGatewayEnvironment(token)
|
|
6808
7628
|
});
|
|
6809
7629
|
if (result.error) {
|
|
6810
7630
|
return {
|
|
@@ -6831,12 +7651,10 @@ function deliverToChatSend({ openclawBin, gatewayUrl, token, params }) {
|
|
|
6831
7651
|
if (gatewayUrl) {
|
|
6832
7652
|
args.push("--url", gatewayUrl);
|
|
6833
7653
|
}
|
|
6834
|
-
if (token && token !== "<token>") {
|
|
6835
|
-
args.push("--token", token);
|
|
6836
|
-
}
|
|
6837
7654
|
const result = spawnSync(openclawBin ?? "openclaw", args, {
|
|
6838
7655
|
encoding: "utf8",
|
|
6839
|
-
maxBuffer: 1024 * 1024 * 10
|
|
7656
|
+
maxBuffer: 1024 * 1024 * 10,
|
|
7657
|
+
env: openClawGatewayEnvironment(token)
|
|
6840
7658
|
});
|
|
6841
7659
|
if (result.error) {
|
|
6842
7660
|
return {
|
|
@@ -6851,6 +7669,15 @@ function deliverToChatSend({ openclawBin, gatewayUrl, token, params }) {
|
|
|
6851
7669
|
stderr: result.stderr ?? ""
|
|
6852
7670
|
};
|
|
6853
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
|
+
}
|
|
6854
7681
|
function captureJson(argv) {
|
|
6855
7682
|
const result = spawnSync(process.execPath, [new URL(import.meta.url).pathname, ...argv], {
|
|
6856
7683
|
encoding: "utf8"
|
|
@@ -6938,7 +7765,24 @@ function expandHome(filePath) {
|
|
|
6938
7765
|
return filePath;
|
|
6939
7766
|
}
|
|
6940
7767
|
function printJson(value) {
|
|
6941
|
-
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;
|
|
6942
7786
|
}
|
|
6943
7787
|
function cleanProcessText(text) {
|
|
6944
7788
|
const value = String(text ?? "").trim();
|
|
@@ -7004,6 +7848,24 @@ function readOutputTail(outputPath, maxBytes = 65536) {
|
|
|
7004
7848
|
return "";
|
|
7005
7849
|
}
|
|
7006
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
|
+
}
|
|
7007
7869
|
function detectModelSelectionError(text) {
|
|
7008
7870
|
const cleaned = cleanProcessText(text);
|
|
7009
7871
|
if (!cleaned) {
|
|
@@ -7077,6 +7939,8 @@ function withStoragePaths(conversation, paths) {
|
|
|
7077
7939
|
function usage() {
|
|
7078
7940
|
const agentList = EXECUTOR_KINDS.join("|");
|
|
7079
7941
|
process.stdout.write(`Usage:
|
|
7942
|
+
agent-knock-knock --help
|
|
7943
|
+
agent-knock-knock --version
|
|
7080
7944
|
agent-knock-knock new --request <text> [--agent ${agentList}] [--workspace <path>] [--store-dir <dir>]
|
|
7081
7945
|
agent-knock-knock record --state <file> --message-json <json>
|
|
7082
7946
|
agent-knock-knock bootstrap-prompt --callback-command <command> [--agent ${agentList}]
|
|
@@ -7088,6 +7952,7 @@ function usage() {
|
|
|
7088
7952
|
agent-knock-knock approve --conversation <id>
|
|
7089
7953
|
agent-knock-knock cancel --conversation <id> [--all-proxy <url>]
|
|
7090
7954
|
agent-knock-knock renew --conversation <id> [--minutes <inactivity-minutes>]
|
|
7955
|
+
agent-knock-knock reconcile-monitors [--store-dir <dir>]
|
|
7091
7956
|
agent-knock-knock retry-callback --conversation <id> [--store-dir <dir>]
|
|
7092
7957
|
agent-knock-knock recover --conversation <id> [--session <name>] [--all-proxy <url>]
|
|
7093
7958
|
agent-knock-knock close --conversation <id> [--reason <text>]
|