@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.
Files changed (34) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +73 -50
  3. package/dist/src/approval-policy.d.ts +2 -1
  4. package/dist/src/approval-policy.js +12 -4
  5. package/dist/src/approval-policy.js.map +1 -1
  6. package/dist/src/claude-hook-store.js +17 -6
  7. package/dist/src/claude-hook-store.js.map +1 -1
  8. package/dist/src/claude-terminal-agent-adapter.js +16 -1
  9. package/dist/src/claude-terminal-agent-adapter.js.map +1 -1
  10. package/dist/src/cli.js +1037 -172
  11. package/dist/src/cli.js.map +1 -1
  12. package/dist/src/openclaw-plugin-helpers.d.ts +49 -0
  13. package/dist/src/openclaw-plugin-helpers.js +246 -0
  14. package/dist/src/openclaw-plugin-helpers.js.map +1 -0
  15. package/dist/src/openclaw-plugin.js +92 -269
  16. package/dist/src/openclaw-plugin.js.map +1 -1
  17. package/dist/src/runtime-log.js +54 -2
  18. package/dist/src/runtime-log.js.map +1 -1
  19. package/dist/src/store.js +432 -12
  20. package/dist/src/store.js.map +1 -1
  21. package/dist/src/terminal-agent-adapter.d.ts +3 -0
  22. package/dist/src/terminal-agent-adapter.js.map +1 -1
  23. package/dist/src/terminal-agent-bridge.d.ts +32 -2
  24. package/dist/src/terminal-agent-bridge.js +192 -34
  25. package/dist/src/terminal-agent-bridge.js.map +1 -1
  26. package/dist/src/terminal-control-provider.d.ts +3 -1
  27. package/dist/src/terminal-control-provider.js +41 -37
  28. package/dist/src/terminal-control-provider.js.map +1 -1
  29. package/dist/src/terminal-process-source.d.ts +12 -3
  30. package/dist/src/terminal-process-source.js +37 -6
  31. package/dist/src/terminal-process-source.js.map +1 -1
  32. package/openclaw.plugin.json +2 -2
  33. package/package.json +10 -5
  34. 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 DEFAULT_CODEX_ACPX_AGENT_COMMAND = "npx -y @agentclientprotocol/codex-acp@^1.1.0";
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 === "new") {
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) => executableCheck(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: requiredOk && agentOk && filesOk,
384
+ ok,
356
385
  package_root: root,
357
386
  checks,
358
387
  package_files: packageFiles,
359
388
  notes: [
360
- "node, openclaw, and acpx are required.",
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
- return createTerminalAgentBridge(options, terminalProvider).attachProcesses(provider.agent, activeSessions);
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: createRuntimeTerminalAgentRegistry(options),
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 = fs.openSync(outputPath, "a");
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: process.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: process.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 process.env;
1967
+ return environment;
1779
1968
  }
1780
1969
  return {
1781
- ...process.env,
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 = new TerminalAgentBridge({ registry, terminalProvider });
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 { conversation, statePath, logPath } = loadConversationFromOptions(options);
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 { conversation, statePath, logPath } = loadConversationFromOptions(options);
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 { conversation, statePath, logPath } = loadConversationFromOptions(options);
2383
- if (["done", "failed", "closed", "cancelled"].includes(conversation.status)) {
2384
- throw new Error(`cannot send to ${conversation.conversation_id}; conversation is ${conversation.status}`);
2385
- }
2386
- if (conversation.status === "needs_recovery") {
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 previousModelSelection = isRecord(conversation.model_selection)
2414
- ? conversation.model_selection
2415
- : {};
2416
- const nextConversation = {
2417
- ...applyMessageToConversation(conversation, message),
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 = fs.openSync(outputPath, "a");
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 { conversation, statePath, logPath } = loadConversationFromOptions(options);
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: terminalRuntimeIdentityForConversation(conversation, terminalControl),
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: policyRuleId,
2784
- policy_fingerprint: policyFingerprint
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: policyRuleId,
2812
- policy_fingerprint: policyFingerprint
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: policyRuleId,
2823
- policy_fingerprint: policyFingerprint
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: policyRuleId,
2837
- policy_fingerprint: policyFingerprint
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: policyRuleId,
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 = fs.openSync(outputPath, "a");
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 nextConversation = {
3618
- ...conversation,
3619
- native_session_takeover: {
3620
- ...nativeTakeover,
3621
- needs_bootstrap: false,
3622
- bootstrapped_at: now
3623
- },
3624
- updated_at: now
3625
- };
3626
- saveState(statePath, nextConversation);
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: conversation.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: conversation.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 nextConversation = {
3647
- ...conversation,
3648
- fork_context_takeover: {
3649
- ...forkTakeover,
3650
- needs_bootstrap: false,
3651
- bootstrapped_at: now
3652
- },
3653
- updated_at: now
3654
- };
3655
- saveState(statePath, nextConversation);
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: conversation.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: conversation.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 { conversation, statePath, logPath } = loadConversationFromOptions(options);
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 { conversation, statePath, logPath } = loadConversationFromOptions(options);
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 = fs.openSync(outputPath, "a");
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: process.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 = loadState(statePath);
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
- const fd = fs.openSync(lockPath, "wx");
5778
- fs.writeFileSync(fd, `${process.pid}\n`, "utf8");
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
- return () => {
5782
- fs.rmSync(lockPath, { force: true });
5783
- };
6516
+ fd = undefined;
6517
+ return () => releaseFileLock(lockPath, token);
5784
6518
  }
5785
6519
  catch (error) {
5786
- if (error.code !== "EEXIST") {
6520
+ if (fd !== undefined) {
6521
+ fs.closeSync(fd);
6522
+ }
6523
+ if (!isRecord(error) || error.code !== "EEXIST") {
5787
6524
  throw error;
5788
6525
  }
5789
- if (staleFileLock(lockPath)) {
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.statSync(lockPath);
5803
- const ownerText = fs.readFileSync(lockPath, "utf8").trim();
5804
- const ownerPid = Number(ownerText);
5805
- if (Number.isSafeInteger(ownerPid) && ownerPid > 1) {
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(ownerPid, 0);
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} --token ${token} --session ${openclawSession}`;
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>]