@scotthuang/agent-knock-knock 0.2.47 → 0.2.49
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +46 -0
- package/README.md +114 -147
- package/dist/src/approval-policy.d.ts +2 -1
- package/dist/src/approval-policy.js +14 -6
- package/dist/src/approval-policy.js.map +1 -1
- package/dist/src/claude-hook-protocol.js.map +1 -1
- package/dist/src/claude-hook-store.js +17 -6
- package/dist/src/claude-hook-store.js.map +1 -1
- package/dist/src/claude-local-transcript-provider.d.ts +40 -0
- package/dist/src/claude-local-transcript-provider.js +804 -0
- package/dist/src/claude-local-transcript-provider.js.map +1 -0
- package/dist/src/claude-terminal-agent-adapter.d.ts +3 -0
- package/dist/src/claude-terminal-agent-adapter.js +146 -47
- package/dist/src/claude-terminal-agent-adapter.js.map +1 -1
- package/dist/src/cli.js +2473 -834
- package/dist/src/cli.js.map +1 -1
- package/dist/src/codex-session-provider.js.map +1 -1
- package/dist/src/codex-store-adapter.js.map +1 -1
- package/dist/src/codex-terminal-agent-adapter.js.map +1 -1
- package/dist/src/doctor-capabilities.d.ts +22 -0
- package/dist/src/doctor-capabilities.js +29 -0
- package/dist/src/doctor-capabilities.js.map +1 -0
- package/dist/src/executors.js.map +1 -1
- package/dist/src/openclaw-plugin-helpers.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 +94 -271
- 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 +51 -3
- package/dist/src/terminal-agent-bridge.js +276 -39
- package/dist/src/terminal-agent-bridge.js.map +1 -1
- package/dist/src/terminal-agent-registry.js.map +1 -1
- package/dist/src/terminal-control-provider.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/dist/src/transcript.js.map +1 -1
- package/openclaw.plugin.json +4 -4
- package/package.json +11 -6
- package/templates/openclaw-skills/agent-knock-knock/SKILL.md +10 -24
package/dist/src/cli.js
CHANGED
|
@@ -7,29 +7,40 @@ import process from "node:process";
|
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
8
|
import { createCodexTerminalAgentAdapter, detectCodexDurableCompletion } from "./codex-terminal-agent-adapter.js";
|
|
9
9
|
import { createClaudeTerminalAgentAdapter } from "./claude-terminal-agent-adapter.js";
|
|
10
|
+
import { captureClaudeTranscriptAnchor, defaultClaudeHome, detectClaudeTranscriptCompletion } from "./claude-local-transcript-provider.js";
|
|
10
11
|
import { ClaudeHookStore, ClaudeHookStoreError, defaultClaudeHookStoreDir } from "./claude-hook-store.js";
|
|
11
12
|
import { claudePermissionHookOutput } from "./claude-hook-protocol.js";
|
|
12
|
-
import { defaultClaudeSettingsPath,
|
|
13
|
+
import { defaultClaudeSettingsPath, loadTrustedClaudeTokenjuiceLaunchers } from "./claude-hook-installer.js";
|
|
13
14
|
import { CodexLocalSessionProvider } from "./codex-local-session-provider.js";
|
|
14
15
|
import { CodexStoreAdapter } from "./codex-store-adapter.js";
|
|
15
16
|
import { applyMessageToConversation, budgetAction, createConversation, createMessage, executorForConversation, extractStructuredMessage, parseMessageJson, resolveExecutor } from "./protocol.js";
|
|
16
17
|
import { EXECUTOR_KINDS, acpxCommandForExecutor, executorDefinitionForKind, modelEnvForExecutor, normalizeModelForExecutor, proxyEnvForExecutor } from "./executors.js";
|
|
17
18
|
import { executorBootstrapPrompt } from "./bootstrap.js";
|
|
18
|
-
import { writeRuntimeLog } from "./runtime-log.js";
|
|
19
|
+
import { redactString, writeRuntimeLog } from "./runtime-log.js";
|
|
19
20
|
import { formatTranscript, readNdjsonLog } from "./transcript.js";
|
|
20
|
-
import { appendEvent, defaultStoreDir, listConversations, logPathForStatePath, loadConversationById, loadState, messageEvent, pathsForConversation, pathsForConversationDir, saveState, statePathForConversationId } from "./store.js";
|
|
21
|
+
import { appendEvent, defaultStoreDir, ensureDir, listConversations, logPathForStatePath, loadConversationById, loadState, messageEvent, pathsForConversation, pathsForConversationDir, saveState, statePathForConversationId } from "./store.js";
|
|
21
22
|
import { planFork, planTakeover } from "./session-takeover-planner.js";
|
|
22
|
-
import { StaticTerminalControlProvider, TmuxTerminalControlProvider } from "./terminal-control-provider.js";
|
|
23
|
+
import { StaticTerminalControlProvider, TmuxTerminalControlProvider, terminalPaneContainsProcess } from "./terminal-control-provider.js";
|
|
23
24
|
import { parseTerminalConversationId } from "./terminal-agent-adapter.js";
|
|
24
25
|
import { createProductionTerminalAgentRegistry } from "./terminal-agent-registry.js";
|
|
25
26
|
import { StaticTerminalProcessSource, SystemTerminalProcessSource } from "./terminal-process-source.js";
|
|
26
27
|
import { TerminalAgentBridge } from "./terminal-agent-bridge.js";
|
|
28
|
+
import { evaluateApprovalPolicy } from "./approval-policy.js";
|
|
29
|
+
import { evaluateDoctorCapabilities } from "./doctor-capabilities.js";
|
|
27
30
|
const DEFAULT_IDLE_TIMEOUT_MINUTES = 10080;
|
|
28
31
|
const DEFAULT_AGENT_TIMEOUT_MINUTES = 60;
|
|
29
32
|
const DEFAULT_AGENT_HARD_TIMEOUT_MINUTES = 720;
|
|
30
33
|
const DEFAULT_MONITOR_POLL_INTERVAL_MS = 5000;
|
|
34
|
+
const CLAUDE_SCREEN_APPROVAL_TTL_MS = 10 * 60 * 1000;
|
|
35
|
+
const CALLBACK_DELIVERY_TIMEOUT_MS = 30_000;
|
|
31
36
|
const CALLBACK_RETRY_DELAYS_MS = [5000, 15000, 60000, 60000];
|
|
32
|
-
const
|
|
37
|
+
const TERMINAL_BRIDGE_MONITOR_LOCK_VERSION = 1;
|
|
38
|
+
const MINIMUM_NODE_VERSION = "22.14.0";
|
|
39
|
+
const PRIVATE_LOCK_FILE_MODE = 0o600;
|
|
40
|
+
const NO_FOLLOW_FLAG = typeof fs.constants.O_NOFOLLOW === "number"
|
|
41
|
+
? fs.constants.O_NOFOLLOW
|
|
42
|
+
: 0;
|
|
43
|
+
const DEFAULT_CODEX_ACPX_AGENT_COMMAND = "npx -y @agentclientprotocol/codex-acp@1.1.7";
|
|
33
44
|
const CONVERSATION_STATUSES = new Set([
|
|
34
45
|
"created",
|
|
35
46
|
"running",
|
|
@@ -102,7 +113,13 @@ catch (error) {
|
|
|
102
113
|
process.exit(1);
|
|
103
114
|
}
|
|
104
115
|
async function runCommand(commandName, options) {
|
|
105
|
-
if (commandName === "
|
|
116
|
+
if (commandName === "help" || commandName === "--help" || commandName === "-h") {
|
|
117
|
+
usage();
|
|
118
|
+
}
|
|
119
|
+
else if (commandName === "version" || commandName === "--version" || commandName === "-v") {
|
|
120
|
+
printVersion();
|
|
121
|
+
}
|
|
122
|
+
else if (commandName === "new") {
|
|
106
123
|
runNew(options);
|
|
107
124
|
}
|
|
108
125
|
else if (commandName === "record") {
|
|
@@ -135,11 +152,14 @@ async function runCommand(commandName, options) {
|
|
|
135
152
|
else if (commandName === "renew") {
|
|
136
153
|
await runRenew(options);
|
|
137
154
|
}
|
|
155
|
+
else if (commandName === "reconcile-monitors") {
|
|
156
|
+
await runReconcileMonitors(options);
|
|
157
|
+
}
|
|
138
158
|
else if (commandName === "recover") {
|
|
139
159
|
runRecover(options);
|
|
140
160
|
}
|
|
141
161
|
else if (commandName === "close") {
|
|
142
|
-
runClose(options);
|
|
162
|
+
await runClose(options);
|
|
143
163
|
}
|
|
144
164
|
else if (commandName === "transcript") {
|
|
145
165
|
runTranscript(options);
|
|
@@ -148,7 +168,7 @@ async function runCommand(commandName, options) {
|
|
|
148
168
|
runInstallOpenClaw(options);
|
|
149
169
|
}
|
|
150
170
|
else if (commandName === "install-claude-hooks") {
|
|
151
|
-
|
|
171
|
+
throw new Error("install-claude-hooks is no longer supported; Claude tmux control now works without modifying Claude Code settings");
|
|
152
172
|
}
|
|
153
173
|
else if (commandName === "claude-hook") {
|
|
154
174
|
await runClaudeHook(options);
|
|
@@ -223,34 +243,6 @@ function runInstallOpenClaw(options) {
|
|
|
223
243
|
: "Agent Knock Knock is installed. Try: AKK list"
|
|
224
244
|
});
|
|
225
245
|
}
|
|
226
|
-
function runInstallClaudeHooks(options) {
|
|
227
|
-
const configuredExecutable = stringValue(options.executablePath ?? options.binPath);
|
|
228
|
-
let executablePath;
|
|
229
|
-
if (configuredExecutable) {
|
|
230
|
-
executablePath = path.resolve(expandHome(configuredExecutable));
|
|
231
|
-
}
|
|
232
|
-
else {
|
|
233
|
-
try {
|
|
234
|
-
executablePath = resolveExecutable("agent-knock-knock");
|
|
235
|
-
}
|
|
236
|
-
catch {
|
|
237
|
-
executablePath = path.resolve(process.argv[1]);
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
const result = installClaudeHooks({
|
|
241
|
-
executablePath,
|
|
242
|
-
settingsPath: expandHome(options.settingsPath ?? options.settings),
|
|
243
|
-
dryRun: options.dryRun === true
|
|
244
|
-
});
|
|
245
|
-
printJson({
|
|
246
|
-
installed: result.written || !result.changed,
|
|
247
|
-
...result,
|
|
248
|
-
executablePath,
|
|
249
|
-
next: result.dryRun
|
|
250
|
-
? "Run again without --dry-run to install the Claude Code hooks."
|
|
251
|
-
: "Claude Code hooks are configured. Existing sessions pick them up through settings reload; start a new session if needed."
|
|
252
|
-
});
|
|
253
|
-
}
|
|
254
246
|
async function runClaudeHook(options) {
|
|
255
247
|
const rawInput = fs.readFileSync(0, "utf8");
|
|
256
248
|
let input;
|
|
@@ -329,8 +321,18 @@ function installOpenClawPlugin(openclawBin, root) {
|
|
|
329
321
|
return { mode: "replaced" };
|
|
330
322
|
}
|
|
331
323
|
function runDoctor(options) {
|
|
332
|
-
const commands = ["node", "openclaw", "acpx", "codex", "claude", "cursor"];
|
|
333
|
-
const checks = commands.map((commandName) =>
|
|
324
|
+
const commands = ["node", "openclaw", "tmux", "acpx", "codex", "claude", "cursor"];
|
|
325
|
+
const checks = commands.map((commandName) => {
|
|
326
|
+
const check = executableCheck(commandName);
|
|
327
|
+
return commandName === "node"
|
|
328
|
+
? {
|
|
329
|
+
...check,
|
|
330
|
+
version: process.versions.node,
|
|
331
|
+
version_supported: versionAtLeast(process.versions.node, MINIMUM_NODE_VERSION),
|
|
332
|
+
minimum_version: MINIMUM_NODE_VERSION
|
|
333
|
+
}
|
|
334
|
+
: check;
|
|
335
|
+
});
|
|
334
336
|
const root = packageRootDir();
|
|
335
337
|
const packageFiles = [
|
|
336
338
|
"dist/src/cli.js",
|
|
@@ -344,24 +346,44 @@ function runDoctor(options) {
|
|
|
344
346
|
exists: fs.existsSync(filePath)
|
|
345
347
|
};
|
|
346
348
|
});
|
|
347
|
-
const
|
|
348
|
-
.filter((check) => ["node", "openclaw", "acpx"].includes(check.command))
|
|
349
|
-
.every((check) => check.available);
|
|
350
|
-
const agentOk = checks
|
|
351
|
-
.filter((check) => ["codex", "claude", "cursor"].includes(check.command))
|
|
352
|
-
.some((check) => check.available);
|
|
349
|
+
const capabilities = evaluateDoctorCapabilities(checks);
|
|
353
350
|
const filesOk = packageFiles.every((check) => check.exists);
|
|
351
|
+
const ok = capabilities.coreOk && filesOk && capabilities.transportOk;
|
|
354
352
|
printJson({
|
|
355
|
-
ok
|
|
353
|
+
ok,
|
|
356
354
|
package_root: root,
|
|
357
355
|
checks,
|
|
358
356
|
package_files: packageFiles,
|
|
357
|
+
capabilities: {
|
|
358
|
+
tmux: capabilities.tmux,
|
|
359
|
+
acp: capabilities.acp
|
|
360
|
+
},
|
|
359
361
|
notes: [
|
|
360
|
-
|
|
361
|
-
"
|
|
362
|
+
`Node.js ${MINIMUM_NODE_VERSION}+ and OpenClaw are required.`,
|
|
363
|
+
"Choose tmux (recommended), ACPX/ACP, or install both.",
|
|
364
|
+
"tmux supports Codex and Claude Code; ACPX supports Codex, Claude Code, and Cursor.",
|
|
365
|
+
"Claude tmux completion is hook-free and fails closed unless the local transcript schema is verified."
|
|
362
366
|
],
|
|
363
367
|
options
|
|
364
368
|
});
|
|
369
|
+
if (!ok) {
|
|
370
|
+
process.exitCode = 1;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
function versionAtLeast(version, minimum) {
|
|
374
|
+
const parsed = version.split(".").slice(0, 3).map((part) => Number.parseInt(part, 10));
|
|
375
|
+
const required = minimum.split(".").slice(0, 3).map((part) => Number.parseInt(part, 10));
|
|
376
|
+
if (parsed.length !== 3 ||
|
|
377
|
+
required.length !== 3 ||
|
|
378
|
+
[...parsed, ...required].some((part) => !Number.isInteger(part) || part < 0)) {
|
|
379
|
+
return false;
|
|
380
|
+
}
|
|
381
|
+
for (let index = 0; index < 3; index += 1) {
|
|
382
|
+
if (parsed[index] !== required[index]) {
|
|
383
|
+
return parsed[index] > required[index];
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
return true;
|
|
365
387
|
}
|
|
366
388
|
async function runAgent(options) {
|
|
367
389
|
const agentCommand = required(options.agentCommand, "agent subcommand is required: takeover");
|
|
@@ -505,6 +527,7 @@ async function runAgentTakeover(options) {
|
|
|
505
527
|
options,
|
|
506
528
|
takeoverMatchKind: "terminal_control",
|
|
507
529
|
terminalControl: target.terminalControl,
|
|
530
|
+
terminalAgentPid: target.pid,
|
|
508
531
|
needsBootstrap: false
|
|
509
532
|
});
|
|
510
533
|
return {
|
|
@@ -666,7 +689,11 @@ function selectTerminateTarget({ plan, session, activeSessions, expectedPid, all
|
|
|
666
689
|
}
|
|
667
690
|
async function listActiveSessionsWithTerminalControl(provider, options, terminalProvider = createTerminalControlProvider(options)) {
|
|
668
691
|
const activeSessions = await provider.listActiveSessions();
|
|
669
|
-
|
|
692
|
+
const activePids = new Set(activeSessions.map((session) => session.pid));
|
|
693
|
+
const processTree = activePids.size > 0
|
|
694
|
+
? await createTerminalProcessSource(options).listProcessSnapshots((snapshot) => activePids.has(snapshot.pid), { includeCwd: false, includeAncestors: true })
|
|
695
|
+
: [];
|
|
696
|
+
return createTerminalAgentBridge(options, terminalProvider).attachProcesses(provider.agent, activeSessions, { processTree });
|
|
670
697
|
}
|
|
671
698
|
function createTerminalControlProvider(options) {
|
|
672
699
|
if (options.terminalsJson || options.terminalScreensJson || options.processesJson) {
|
|
@@ -689,6 +716,11 @@ function createClaudeHookStore(options = {}) {
|
|
|
689
716
|
rootDir: expandHome(configuredRoot ?? defaultClaudeHookStoreDir())
|
|
690
717
|
});
|
|
691
718
|
}
|
|
719
|
+
function createConfiguredClaudeHookStore(options = {}) {
|
|
720
|
+
return stringValue(options.claudeHookStoreDir)
|
|
721
|
+
? createClaudeHookStore(options)
|
|
722
|
+
: undefined;
|
|
723
|
+
}
|
|
692
724
|
function loadClaudeAgentRows(options = {}) {
|
|
693
725
|
let value;
|
|
694
726
|
if (options.claudeAgentsJson !== undefined) {
|
|
@@ -741,6 +773,9 @@ function loadClaudeAgentRows(options = {}) {
|
|
|
741
773
|
...(stringValue(row.cwd) ? { cwd: stringValue(row.cwd) } : {}),
|
|
742
774
|
...(stringValue(row.kind) ? { kind: stringValue(row.kind) } : {}),
|
|
743
775
|
...(stringValue(row.sessionId) ? { sessionId: stringValue(row.sessionId) } : {}),
|
|
776
|
+
...(Number.isSafeInteger(Number(row.startedAt)) && Number(row.startedAt) > 0
|
|
777
|
+
? { startedAt: Number(row.startedAt) }
|
|
778
|
+
: {}),
|
|
744
779
|
...(stringValue(row.status) ? { status: stringValue(row.status) } : {})
|
|
745
780
|
}];
|
|
746
781
|
});
|
|
@@ -781,6 +816,7 @@ function isClaudeProcessCommand(commandText) {
|
|
|
781
816
|
/[\\/]\.local[\\/]share[\\/]claude[\\/]versions[\\/][^\\/\s]+$/u.test(executable);
|
|
782
817
|
}
|
|
783
818
|
function createRuntimeTerminalAgentRegistry(options) {
|
|
819
|
+
const claudeHookStore = createConfiguredClaudeHookStore(options);
|
|
784
820
|
return createProductionTerminalAgentRegistry({
|
|
785
821
|
overrides: [
|
|
786
822
|
createCodexTerminalAgentAdapter({
|
|
@@ -820,16 +856,51 @@ function createRuntimeTerminalAgentRegistry(options) {
|
|
|
820
856
|
}),
|
|
821
857
|
createClaudeTerminalAgentAdapter({
|
|
822
858
|
agentRows: loadClaudeAgentRows(options),
|
|
823
|
-
|
|
824
|
-
|
|
859
|
+
async detectDurableCompletion(request) {
|
|
860
|
+
return detectClaudeTranscriptCompletion(request, {
|
|
861
|
+
claudeHome: expandHome(options.claudeHome),
|
|
862
|
+
agentRows: loadClaudeAgentRows(options)
|
|
863
|
+
});
|
|
864
|
+
},
|
|
865
|
+
...(claudeHookStore
|
|
866
|
+
? {
|
|
867
|
+
hookStore: claudeHookStore,
|
|
868
|
+
trustedTokenjuiceLaunchers: loadTrustedClaudeTokenjuiceLaunchers(expandHome(options.claudeSettingsPath ?? defaultClaudeSettingsPath()))
|
|
869
|
+
}
|
|
870
|
+
: {})
|
|
825
871
|
})
|
|
826
872
|
]
|
|
827
873
|
});
|
|
828
874
|
}
|
|
829
|
-
function createTerminalAgentBridge(options, terminalProvider = createTerminalControlProvider(options)) {
|
|
875
|
+
function createTerminalAgentBridge(options, terminalProvider = createTerminalControlProvider(options), registry = createRuntimeTerminalAgentRegistry(options)) {
|
|
876
|
+
const processSource = createTerminalProcessSource(options);
|
|
830
877
|
return new TerminalAgentBridge({
|
|
831
|
-
registry
|
|
832
|
-
terminalProvider
|
|
878
|
+
registry,
|
|
879
|
+
terminalProvider,
|
|
880
|
+
async verifyIdentity({ agent, pid, terminalControl }) {
|
|
881
|
+
const adapter = registry.require(agent);
|
|
882
|
+
const snapshots = await processSource.listProcessSnapshots(undefined, { includeCwd: false });
|
|
883
|
+
const snapshot = snapshots.find((candidate) => candidate.pid === pid);
|
|
884
|
+
if (!snapshot || !adapter.classifyProcess(snapshot)) {
|
|
885
|
+
throw new Error(`terminal conversation agent ${agent} with pid ${pid} is no longer active`);
|
|
886
|
+
}
|
|
887
|
+
const panes = await terminalProvider.listPanes();
|
|
888
|
+
const pane = panes.find((candidate) => candidate.kind === terminalControl.kind &&
|
|
889
|
+
candidate.target === terminalControl.target &&
|
|
890
|
+
candidate.panePid === terminalControl.panePid);
|
|
891
|
+
if (!pane || !terminalPaneContainsProcess(snapshot, pane, snapshots)) {
|
|
892
|
+
throw new Error(`terminal conversation agent ${agent} with pid ${pid} no longer belongs to pane ${terminalControl.target}`);
|
|
893
|
+
}
|
|
894
|
+
return {
|
|
895
|
+
terminalControl: {
|
|
896
|
+
...terminalControl,
|
|
897
|
+
socketPath: pane.socketPath,
|
|
898
|
+
panePid: pane.panePid,
|
|
899
|
+
currentCommand: pane.currentCommand,
|
|
900
|
+
currentPath: pane.currentPath
|
|
901
|
+
}
|
|
902
|
+
};
|
|
903
|
+
}
|
|
833
904
|
});
|
|
834
905
|
}
|
|
835
906
|
function planTerminalControlTakeover(session, activeSessions) {
|
|
@@ -926,6 +997,115 @@ function terminalRuntimeIdentityForConversation(conversation, terminalControl) {
|
|
|
926
997
|
terminalTarget: terminalControl.target
|
|
927
998
|
};
|
|
928
999
|
}
|
|
1000
|
+
async function migrateLegacyTerminalAgentIdentity({ conversation, statePath, logPath, options }) {
|
|
1001
|
+
const nativeTakeover = isRecord(conversation?.native_session_takeover)
|
|
1002
|
+
? conversation.native_session_takeover
|
|
1003
|
+
: undefined;
|
|
1004
|
+
const terminalControl = terminalControlFromTakeover(nativeTakeover);
|
|
1005
|
+
if (!nativeTakeover || !terminalControl) {
|
|
1006
|
+
return conversation;
|
|
1007
|
+
}
|
|
1008
|
+
const runtime = terminalRuntimeIdentityForConversation(conversation, terminalControl);
|
|
1009
|
+
if (Number.isInteger(runtime.pid) && Number(runtime.pid) > 0) {
|
|
1010
|
+
return conversation;
|
|
1011
|
+
}
|
|
1012
|
+
const executor = executorForConversation(conversation);
|
|
1013
|
+
const nativeSessionId = stringValue(nativeTakeover.native_session_id);
|
|
1014
|
+
if (executor.kind !== "codex" ||
|
|
1015
|
+
!nativeSessionId ||
|
|
1016
|
+
parseTerminalConversationId(nativeSessionId)) {
|
|
1017
|
+
return conversation;
|
|
1018
|
+
}
|
|
1019
|
+
let matchedProcess;
|
|
1020
|
+
try {
|
|
1021
|
+
const registry = createRuntimeTerminalAgentRegistry(options);
|
|
1022
|
+
const adapter = registry.require("codex");
|
|
1023
|
+
const snapshots = await createTerminalProcessSource(options).listProcessSnapshots((snapshot) => adapter.classifyProcess(snapshot) !== undefined, { includeAncestors: true });
|
|
1024
|
+
const panes = await createTerminalControlProvider(options).listPanes();
|
|
1025
|
+
const matchingPanes = panes.filter((pane) => pane.kind === terminalControl.kind &&
|
|
1026
|
+
pane.target === terminalControl.target &&
|
|
1027
|
+
pane.panePid === terminalControl.panePid);
|
|
1028
|
+
if (matchingPanes.length !== 1) {
|
|
1029
|
+
return conversation;
|
|
1030
|
+
}
|
|
1031
|
+
const candidates = snapshots.flatMap((snapshot) => {
|
|
1032
|
+
const classified = adapter.classifyProcess(snapshot);
|
|
1033
|
+
return classified ? [{ ...classified, agent: "codex" }] : [];
|
|
1034
|
+
});
|
|
1035
|
+
const matches = candidates.filter((candidate) => candidate.sessionId === nativeSessionId &&
|
|
1036
|
+
terminalPaneContainsProcess(candidate, matchingPanes[0], snapshots));
|
|
1037
|
+
if (matches.length !== 1) {
|
|
1038
|
+
return conversation;
|
|
1039
|
+
}
|
|
1040
|
+
matchedProcess = matches[0];
|
|
1041
|
+
}
|
|
1042
|
+
catch (error) {
|
|
1043
|
+
runtimeLog("warn", "legacy_terminal_agent_identity_migration_failed", {
|
|
1044
|
+
conversation_id: conversation.conversation_id,
|
|
1045
|
+
terminal_target: terminalControl.target,
|
|
1046
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
1047
|
+
});
|
|
1048
|
+
return conversation;
|
|
1049
|
+
}
|
|
1050
|
+
if (!matchedProcess) {
|
|
1051
|
+
return conversation;
|
|
1052
|
+
}
|
|
1053
|
+
const releaseLock = acquireFileLock(`${statePath}.lock`);
|
|
1054
|
+
let migratedConversation = conversation;
|
|
1055
|
+
let migrated = false;
|
|
1056
|
+
try {
|
|
1057
|
+
const current = loadState(statePath);
|
|
1058
|
+
const currentTakeover = isRecord(current.native_session_takeover)
|
|
1059
|
+
? current.native_session_takeover
|
|
1060
|
+
: undefined;
|
|
1061
|
+
const currentControl = terminalControlFromTakeover(currentTakeover);
|
|
1062
|
+
if (!currentTakeover || !currentControl) {
|
|
1063
|
+
return current;
|
|
1064
|
+
}
|
|
1065
|
+
const currentRuntime = terminalRuntimeIdentityForConversation(current, currentControl);
|
|
1066
|
+
if (Number.isInteger(currentRuntime.pid) && Number(currentRuntime.pid) > 0) {
|
|
1067
|
+
return current;
|
|
1068
|
+
}
|
|
1069
|
+
if (currentTakeover.native_session_id !== nativeSessionId ||
|
|
1070
|
+
currentControl.target !== terminalControl.target ||
|
|
1071
|
+
currentControl.socketPath !== terminalControl.socketPath ||
|
|
1072
|
+
currentControl.panePid !== terminalControl.panePid) {
|
|
1073
|
+
return current;
|
|
1074
|
+
}
|
|
1075
|
+
const migratedAt = new Date().toISOString();
|
|
1076
|
+
migratedConversation = {
|
|
1077
|
+
...current,
|
|
1078
|
+
native_session_takeover: {
|
|
1079
|
+
...currentTakeover,
|
|
1080
|
+
terminal_agent_pid: matchedProcess.pid,
|
|
1081
|
+
terminal_agent_session_id: matchedProcess.sessionId,
|
|
1082
|
+
terminal_agent_identity_migrated_at: migratedAt
|
|
1083
|
+
},
|
|
1084
|
+
updated_at: migratedAt
|
|
1085
|
+
};
|
|
1086
|
+
saveState(statePath, migratedConversation);
|
|
1087
|
+
migrated = true;
|
|
1088
|
+
}
|
|
1089
|
+
finally {
|
|
1090
|
+
releaseLock();
|
|
1091
|
+
}
|
|
1092
|
+
if (migrated) {
|
|
1093
|
+
appendEvent(logPath, {
|
|
1094
|
+
ts: new Date().toISOString(),
|
|
1095
|
+
conversation_id: migratedConversation.conversation_id,
|
|
1096
|
+
event: "terminal_agent_identity_migrated",
|
|
1097
|
+
terminal_target: terminalControl.target,
|
|
1098
|
+
terminal_agent_pid: matchedProcess.pid,
|
|
1099
|
+
native_session_id: nativeSessionId
|
|
1100
|
+
});
|
|
1101
|
+
runtimeLog("info", "terminal_agent_identity_migrated", {
|
|
1102
|
+
conversation_id: migratedConversation.conversation_id,
|
|
1103
|
+
terminal_target: terminalControl.target,
|
|
1104
|
+
terminal_agent_pid: matchedProcess.pid
|
|
1105
|
+
});
|
|
1106
|
+
}
|
|
1107
|
+
return migratedConversation;
|
|
1108
|
+
}
|
|
929
1109
|
function isTerminalControlCapability(value) {
|
|
930
1110
|
return typeof value === "string" && [
|
|
931
1111
|
"screen_status",
|
|
@@ -1029,7 +1209,7 @@ function createForkConversation({ agent, strategy, session, contextPackage, fork
|
|
|
1029
1209
|
next: `Use AKK send ${forkedConversation.conversation_id}: <message> to start the forked ${agent} session with the approved summary.`
|
|
1030
1210
|
};
|
|
1031
1211
|
}
|
|
1032
|
-
function createNativeSessionConversation({ agent, strategy, session, modelInfo, options, takeoverMatchKind = strategy, terminalControl = undefined, needsBootstrap = true }) {
|
|
1212
|
+
function createNativeSessionConversation({ agent, strategy, session, modelInfo, options, takeoverMatchKind = strategy, terminalControl = undefined, terminalAgentPid = undefined, needsBootstrap = true }) {
|
|
1033
1213
|
const workspace = session.cwd;
|
|
1034
1214
|
const storeDir = expandHome(options.storeDir ?? options.logDir ?? defaultStoreDir(workspace));
|
|
1035
1215
|
cleanupIdleConversations(storeDir, options);
|
|
@@ -1078,6 +1258,7 @@ function createNativeSessionConversation({ agent, strategy, session, modelInfo,
|
|
|
1078
1258
|
native_session_takeover: {
|
|
1079
1259
|
agent,
|
|
1080
1260
|
native_session_id: session.id,
|
|
1261
|
+
terminal_agent_pid: terminalAgentPid,
|
|
1081
1262
|
source_cwd: session.cwd,
|
|
1082
1263
|
source_title: session.title,
|
|
1083
1264
|
strategy,
|
|
@@ -1325,7 +1506,7 @@ function runDelegate(options) {
|
|
|
1325
1506
|
throw new Error(cleanProcessText(ensureSession.stderr || ensureSession.stdout || `acpx ${executor.kind} sessions ensure exited with status ${ensureSession.status}`));
|
|
1326
1507
|
}
|
|
1327
1508
|
const outputPath = path.join(newResult.paths.conversationDir, `${executor.kind}-output.log`);
|
|
1328
|
-
const outputFd =
|
|
1509
|
+
const outputFd = openPrivateAppendFile(outputPath);
|
|
1329
1510
|
const child = spawn(acpxPath, acpxArgs, {
|
|
1330
1511
|
detached: true,
|
|
1331
1512
|
stdio: ["ignore", outputFd, outputFd],
|
|
@@ -1472,12 +1653,12 @@ function startExecutorMonitor({ statePath, logPath, pid, outputPath, agentTimeou
|
|
|
1472
1653
|
detached: true,
|
|
1473
1654
|
stdio: "ignore",
|
|
1474
1655
|
cwd: process.cwd(),
|
|
1475
|
-
env:
|
|
1656
|
+
env: environmentWithoutGatewayTokens()
|
|
1476
1657
|
});
|
|
1477
1658
|
child.unref();
|
|
1478
1659
|
return child;
|
|
1479
1660
|
}
|
|
1480
|
-
function startTerminalBridgeMonitor({ statePath, logPath, agentTimeoutMinutes, agentHardTimeoutMinutes, pollIntervalMs, codexHome, claudeHookStoreDir }) {
|
|
1661
|
+
function startTerminalBridgeMonitor({ statePath, logPath, agentTimeoutMinutes, agentHardTimeoutMinutes, pollIntervalMs, codexHome, claudeHome, claudeHookStoreDir }) {
|
|
1481
1662
|
const args = [
|
|
1482
1663
|
new URL(import.meta.url).pathname,
|
|
1483
1664
|
"monitor",
|
|
@@ -1496,6 +1677,9 @@ function startTerminalBridgeMonitor({ statePath, logPath, agentTimeoutMinutes, a
|
|
|
1496
1677
|
if (codexHome) {
|
|
1497
1678
|
args.push("--codex-home", codexHome);
|
|
1498
1679
|
}
|
|
1680
|
+
if (claudeHome) {
|
|
1681
|
+
args.push("--claude-home", claudeHome);
|
|
1682
|
+
}
|
|
1499
1683
|
if (claudeHookStoreDir) {
|
|
1500
1684
|
args.push("--claude-hook-store-dir", claudeHookStoreDir);
|
|
1501
1685
|
}
|
|
@@ -1503,7 +1687,7 @@ function startTerminalBridgeMonitor({ statePath, logPath, agentTimeoutMinutes, a
|
|
|
1503
1687
|
detached: true,
|
|
1504
1688
|
stdio: "ignore",
|
|
1505
1689
|
cwd: process.cwd(),
|
|
1506
|
-
env:
|
|
1690
|
+
env: environmentWithoutGatewayTokens()
|
|
1507
1691
|
});
|
|
1508
1692
|
child.unref();
|
|
1509
1693
|
return child;
|
|
@@ -1526,6 +1710,7 @@ function startTerminalBridgeMonitorForConversation({ conversation, statePath, lo
|
|
|
1526
1710
|
DEFAULT_AGENT_HARD_TIMEOUT_MINUTES),
|
|
1527
1711
|
pollIntervalMs: Number(options.monitorPollIntervalMs ?? DEFAULT_MONITOR_POLL_INTERVAL_MS),
|
|
1528
1712
|
codexHome: options.codexHome,
|
|
1713
|
+
claudeHome: options.claudeHome ?? nativeTakeover?.["claude_home"],
|
|
1529
1714
|
claudeHookStoreDir: options.claudeHookStoreDir ?? nativeTakeover?.["claude_hook_store_dir"]
|
|
1530
1715
|
});
|
|
1531
1716
|
}
|
|
@@ -1535,7 +1720,7 @@ function terminalBridgeEnabled(conversation) {
|
|
|
1535
1720
|
: undefined;
|
|
1536
1721
|
return nativeTakeover?.["terminal_bridge"] === true;
|
|
1537
1722
|
}
|
|
1538
|
-
function withTerminalBridgeState({ conversation, message, startedAt, agentTimeoutMinutes, agentHardTimeoutMinutes, preSendScreenFingerprint }) {
|
|
1723
|
+
function withTerminalBridgeState({ conversation, message, requestText, startedAt, agentTimeoutMinutes, agentHardTimeoutMinutes, preSendScreenFingerprint, claudeTranscriptAnchor, claudeHome }) {
|
|
1539
1724
|
const nativeTakeover = isRecord(conversation.native_session_takeover)
|
|
1540
1725
|
? conversation.native_session_takeover
|
|
1541
1726
|
: {};
|
|
@@ -1546,10 +1731,14 @@ function withTerminalBridgeState({ conversation, message, startedAt, agentTimeou
|
|
|
1546
1731
|
terminal_bridge: true,
|
|
1547
1732
|
terminal_bridge_started_at: startedAt,
|
|
1548
1733
|
terminal_bridge_message_id: message.id,
|
|
1549
|
-
terminal_bridge_request_text:
|
|
1550
|
-
terminal_bridge_request_hash: terminalBridgeRequestFingerprint(
|
|
1734
|
+
terminal_bridge_request_text: requestText,
|
|
1735
|
+
terminal_bridge_request_hash: terminalBridgeRequestFingerprint(requestText),
|
|
1551
1736
|
terminal_bridge_pre_send_screen_fingerprint: preSendScreenFingerprint,
|
|
1737
|
+
claude_transcript_anchor: claudeTranscriptAnchor,
|
|
1738
|
+
claude_home: claudeHome,
|
|
1552
1739
|
terminal_bridge_completion_claim: undefined,
|
|
1740
|
+
terminal_bridge_approval_dispatch: undefined,
|
|
1741
|
+
terminal_bridge_monitor_lock_version: TERMINAL_BRIDGE_MONITOR_LOCK_VERSION,
|
|
1553
1742
|
terminal_bridge_monitor_started_at: startedAt,
|
|
1554
1743
|
terminal_bridge_last_activity_at: startedAt,
|
|
1555
1744
|
terminal_bridge_inactivity_timeout_minutes: agentTimeoutMinutes,
|
|
@@ -1561,6 +1750,9 @@ function withTerminalBridgeState({ conversation, message, startedAt, agentTimeou
|
|
|
1561
1750
|
};
|
|
1562
1751
|
}
|
|
1563
1752
|
function activateClaudeHookLease({ options, conversation, message, terminalControl, expiresAt }) {
|
|
1753
|
+
if (!stringValue(options.claudeHookStoreDir)) {
|
|
1754
|
+
return undefined;
|
|
1755
|
+
}
|
|
1564
1756
|
const runtime = terminalRuntimeIdentityForConversation(conversation, terminalControl);
|
|
1565
1757
|
const pid = runtime.pid;
|
|
1566
1758
|
const agentRow = pid === undefined
|
|
@@ -1667,7 +1859,9 @@ function renewClaudeHookLease(conversation, expiresAt) {
|
|
|
1667
1859
|
const nativeTakeover = isRecord(conversation?.native_session_takeover)
|
|
1668
1860
|
? conversation.native_session_takeover
|
|
1669
1861
|
: undefined;
|
|
1670
|
-
if (nativeTakeover?.agent !== "claude"
|
|
1862
|
+
if (nativeTakeover?.agent !== "claude" ||
|
|
1863
|
+
nativeTakeover.claude_hook_mode !== "enabled" ||
|
|
1864
|
+
!stringValue(nativeTakeover.claude_hook_store_dir)) {
|
|
1671
1865
|
return undefined;
|
|
1672
1866
|
}
|
|
1673
1867
|
const conversationId = stringValue(conversation.conversation_id);
|
|
@@ -1773,16 +1967,23 @@ function modelForExecutor(executor, options = {}) {
|
|
|
1773
1967
|
return normalizeModelForExecutor(executor, modelEnvForExecutor(executor, process.env));
|
|
1774
1968
|
}
|
|
1775
1969
|
function environmentForExecutor(executor, options = {}) {
|
|
1970
|
+
const environment = environmentWithoutGatewayTokens();
|
|
1776
1971
|
const proxy = proxyForExecutor(executor, options);
|
|
1777
1972
|
if (!proxy) {
|
|
1778
|
-
return
|
|
1973
|
+
return environment;
|
|
1779
1974
|
}
|
|
1780
1975
|
return {
|
|
1781
|
-
...
|
|
1976
|
+
...environment,
|
|
1782
1977
|
ALL_PROXY: proxy,
|
|
1783
1978
|
all_proxy: proxy
|
|
1784
1979
|
};
|
|
1785
1980
|
}
|
|
1981
|
+
function environmentWithoutGatewayTokens() {
|
|
1982
|
+
const environment = { ...process.env };
|
|
1983
|
+
delete environment.AKK_GATEWAY_TOKEN;
|
|
1984
|
+
delete environment.OPENCLAW_GATEWAY_TOKEN;
|
|
1985
|
+
return environment;
|
|
1986
|
+
}
|
|
1786
1987
|
async function runList(options) {
|
|
1787
1988
|
const storeDir = expandHome(options.storeDir ?? options.logDir ?? defaultStoreDir(process.cwd()));
|
|
1788
1989
|
const cleanup = cleanupIdleConversations(storeDir, options);
|
|
@@ -1855,7 +2056,7 @@ async function buildNativeListGroups({ options, agentFilter, statusFilter }) {
|
|
|
1855
2056
|
};
|
|
1856
2057
|
}
|
|
1857
2058
|
const terminalProvider = createTerminalControlProvider(options);
|
|
1858
|
-
const bridge =
|
|
2059
|
+
const bridge = createTerminalAgentBridge(options, terminalProvider, registry);
|
|
1859
2060
|
const terminalScan = options.terminalDebug ? await terminalControlDiagnostics(terminalProvider) : undefined;
|
|
1860
2061
|
const terminalControlled = [];
|
|
1861
2062
|
const native = [];
|
|
@@ -1863,7 +2064,7 @@ async function buildNativeListGroups({ options, agentFilter, statusFilter }) {
|
|
|
1863
2064
|
const errors = [];
|
|
1864
2065
|
try {
|
|
1865
2066
|
const processSource = createTerminalProcessSource(options);
|
|
1866
|
-
const snapshots = await processSource.listProcessSnapshots((snapshot) => adapters.some((adapter) => adapter.capabilities.processDiscovery && adapter.classifyProcess(snapshot) !== undefined));
|
|
2067
|
+
const snapshots = await processSource.listProcessSnapshots((snapshot) => adapters.some((adapter) => adapter.capabilities.processDiscovery && adapter.classifyProcess(snapshot) !== undefined), { includeAncestors: true });
|
|
1867
2068
|
const activeSessions = await bridge.listProcesses(snapshots, adapters.map((adapter) => adapter.agent));
|
|
1868
2069
|
activeCount = activeSessions.length;
|
|
1869
2070
|
const rootSessions = rootActiveProcesses(activeSessions);
|
|
@@ -2030,7 +2231,7 @@ function rootActiveProcesses(processes) {
|
|
|
2030
2231
|
const seenTerminalTargets = new Set();
|
|
2031
2232
|
return roots.filter((process) => {
|
|
2032
2233
|
const terminalTarget = process.terminalControl?.target
|
|
2033
|
-
? `${process.agent}:${process.terminalControl.target}`
|
|
2234
|
+
? `${process.agent}:${process.terminalControl.target}:${process.terminalControl.panePid}`
|
|
2034
2235
|
: undefined;
|
|
2035
2236
|
if (!terminalTarget) {
|
|
2036
2237
|
return true;
|
|
@@ -2075,6 +2276,7 @@ async function runStatus(options) {
|
|
|
2075
2276
|
const terminalStatus = await terminalStatusForControl(terminalConversation.agent, terminalConversation.terminalControl, options, {
|
|
2076
2277
|
pid: terminalConversation.pid,
|
|
2077
2278
|
cwd: terminalConversation.terminalControl.currentPath,
|
|
2279
|
+
conversationId: terminalConversation.conversationId,
|
|
2078
2280
|
terminalTarget: terminalConversation.terminalControl.target
|
|
2079
2281
|
});
|
|
2080
2282
|
printJson({
|
|
@@ -2091,7 +2293,12 @@ async function runStatus(options) {
|
|
|
2091
2293
|
});
|
|
2092
2294
|
return;
|
|
2093
2295
|
}
|
|
2094
|
-
const
|
|
2296
|
+
const loaded = loadConversationFromOptions(options);
|
|
2297
|
+
const { statePath, logPath } = loaded;
|
|
2298
|
+
const conversation = await migrateLegacyTerminalAgentIdentity({
|
|
2299
|
+
...loaded,
|
|
2300
|
+
options
|
|
2301
|
+
});
|
|
2095
2302
|
const events = readExistingEvents(logPath);
|
|
2096
2303
|
const result = {
|
|
2097
2304
|
conversation,
|
|
@@ -2129,6 +2336,7 @@ async function runDescribe(options) {
|
|
|
2129
2336
|
const terminalStatus = await terminalStatusForControl(terminalConversation.agent, terminalConversation.terminalControl, options, {
|
|
2130
2337
|
pid: terminalConversation.pid,
|
|
2131
2338
|
cwd: terminalConversation.terminalControl.currentPath,
|
|
2339
|
+
conversationId: terminalConversation.conversationId,
|
|
2132
2340
|
terminalTarget: terminalConversation.terminalControl.target
|
|
2133
2341
|
});
|
|
2134
2342
|
if (terminalConversation.agent !== "codex") {
|
|
@@ -2172,7 +2380,12 @@ async function runDescribe(options) {
|
|
|
2172
2380
|
}));
|
|
2173
2381
|
return;
|
|
2174
2382
|
}
|
|
2175
|
-
const
|
|
2383
|
+
const loaded = loadConversationFromOptions(options);
|
|
2384
|
+
const { statePath, logPath } = loaded;
|
|
2385
|
+
const conversation = await migrateLegacyTerminalAgentIdentity({
|
|
2386
|
+
...loaded,
|
|
2387
|
+
options
|
|
2388
|
+
});
|
|
2176
2389
|
const events = readExistingEvents(logPath);
|
|
2177
2390
|
const terminalControl = terminalControlFromTakeover(isRecord(conversation.native_session_takeover) ? conversation.native_session_takeover : undefined);
|
|
2178
2391
|
const terminalStatus = terminalControl
|
|
@@ -2291,21 +2504,49 @@ function terminalBridgeApprovalInstructions({ conversation, terminalControl, ter
|
|
|
2291
2504
|
"Do not use raw tmux, shell, or manual key presses for this approval. Do not approve without explicit user confirmation."
|
|
2292
2505
|
].filter((line) => line !== undefined).join("\n");
|
|
2293
2506
|
}
|
|
2294
|
-
function recordTerminalBridgeApprovalNotification({ statePath, logPath, terminalControl, terminalStatus, fingerprint }) {
|
|
2507
|
+
function recordTerminalBridgeApprovalNotification({ statePath, logPath, terminalControl, terminalStatus, fingerprint, expectedConversation, onRecorded }) {
|
|
2295
2508
|
const releaseLock = acquireFileLock(`${statePath}.lock`);
|
|
2296
2509
|
try {
|
|
2297
2510
|
const conversation = loadState(statePath);
|
|
2511
|
+
const currentNativeTakeover = isRecord(conversation.native_session_takeover)
|
|
2512
|
+
? conversation.native_session_takeover
|
|
2513
|
+
: undefined;
|
|
2514
|
+
const currentTerminalControl = terminalControlFromTakeover(currentNativeTakeover);
|
|
2515
|
+
if (!isWaitingForAgent(conversation.status) ||
|
|
2516
|
+
conversation.conversation_id !== expectedConversation.conversationId ||
|
|
2517
|
+
conversation.status !== expectedConversation.status ||
|
|
2518
|
+
conversation.updated_at !== expectedConversation.updatedAt ||
|
|
2519
|
+
currentNativeTakeover?.terminal_bridge !== true ||
|
|
2520
|
+
stringValue(currentNativeTakeover.terminal_bridge_message_id) !==
|
|
2521
|
+
expectedConversation.messageId ||
|
|
2522
|
+
!currentTerminalControl ||
|
|
2523
|
+
currentTerminalControl.kind !== terminalControl.kind ||
|
|
2524
|
+
currentTerminalControl.target !== terminalControl.target ||
|
|
2525
|
+
currentTerminalControl.socketPath !== terminalControl.socketPath ||
|
|
2526
|
+
currentTerminalControl.panePid !== terminalControl.panePid) {
|
|
2527
|
+
return {
|
|
2528
|
+
conversation,
|
|
2529
|
+
duplicate: false,
|
|
2530
|
+
stale: true,
|
|
2531
|
+
recorded: undefined
|
|
2532
|
+
};
|
|
2533
|
+
}
|
|
2298
2534
|
const nativeTakeover = isRecord(conversation.native_session_takeover)
|
|
2299
2535
|
? { ...conversation.native_session_takeover }
|
|
2300
2536
|
: {};
|
|
2301
2537
|
const previousApproval = isRecord(nativeTakeover.terminal_bridge_approval)
|
|
2302
2538
|
? nativeTakeover.terminal_bridge_approval
|
|
2303
2539
|
: undefined;
|
|
2304
|
-
|
|
2540
|
+
const previousNotifiedAt = validTimestampMs(previousApproval?.notified_at);
|
|
2541
|
+
if (previousApproval?.fingerprint === fingerprint &&
|
|
2542
|
+
previousNotifiedAt !== undefined &&
|
|
2543
|
+
Date.now() - previousNotifiedAt <= CLAUDE_SCREEN_APPROVAL_TTL_MS) {
|
|
2305
2544
|
return {
|
|
2306
2545
|
conversation,
|
|
2307
2546
|
duplicate: true,
|
|
2308
|
-
|
|
2547
|
+
stale: false,
|
|
2548
|
+
previousApproval,
|
|
2549
|
+
recorded: undefined
|
|
2309
2550
|
};
|
|
2310
2551
|
}
|
|
2311
2552
|
const now = new Date().toISOString();
|
|
@@ -2332,54 +2573,34 @@ function recordTerminalBridgeApprovalNotification({ statePath, logPath, terminal
|
|
|
2332
2573
|
});
|
|
2333
2574
|
return {
|
|
2334
2575
|
conversation: nextConversation,
|
|
2335
|
-
duplicate: false
|
|
2576
|
+
duplicate: false,
|
|
2577
|
+
stale: false,
|
|
2578
|
+
recorded: onRecorded?.(nextConversation)
|
|
2336
2579
|
};
|
|
2337
2580
|
}
|
|
2338
2581
|
finally {
|
|
2339
2582
|
releaseLock();
|
|
2340
2583
|
}
|
|
2341
2584
|
}
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
cleanupIdleConversations(storeDirFromOptions(options), options);
|
|
2348
|
-
const terminalConversation = await resolveTerminalConversationFromOptions(options);
|
|
2349
|
-
if (terminalConversation) {
|
|
2350
|
-
if (options.background) {
|
|
2351
|
-
const managed = createManagedTerminalConversationFromRawId({
|
|
2585
|
+
function prepareManagedSend({ options, statePath, logPath, messageBody, stateLockHeld = false, persist = true, rejectTerminalControl = false }) {
|
|
2586
|
+
if (!stateLockHeld) {
|
|
2587
|
+
const releaseLock = acquireFileLock(`${statePath}.lock`);
|
|
2588
|
+
try {
|
|
2589
|
+
return prepareManagedSend({
|
|
2352
2590
|
options,
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
pid: terminalConversation.pid,
|
|
2591
|
+
statePath,
|
|
2592
|
+
logPath,
|
|
2356
2593
|
messageBody,
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
options,
|
|
2361
|
-
conversation: managed.conversation,
|
|
2362
|
-
nextConversation: managed.nextConversation,
|
|
2363
|
-
statePath: managed.statePath,
|
|
2364
|
-
logPath: managed.logPath,
|
|
2365
|
-
executor: managed.executor,
|
|
2366
|
-
message: managed.message,
|
|
2367
|
-
terminalControl: terminalConversation.terminalControl,
|
|
2368
|
-
needsNativeTakeoverBootstrap: true
|
|
2594
|
+
stateLockHeld: true,
|
|
2595
|
+
persist,
|
|
2596
|
+
rejectTerminalControl
|
|
2369
2597
|
});
|
|
2370
|
-
return;
|
|
2371
2598
|
}
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
agent: terminalConversation.agent,
|
|
2376
|
-
pid: terminalConversation.pid,
|
|
2377
|
-
messageBody,
|
|
2378
|
-
terminalControl: terminalConversation.terminalControl
|
|
2379
|
-
});
|
|
2380
|
-
return;
|
|
2599
|
+
finally {
|
|
2600
|
+
releaseLock();
|
|
2601
|
+
}
|
|
2381
2602
|
}
|
|
2382
|
-
const
|
|
2603
|
+
const conversation = loadState(statePath);
|
|
2383
2604
|
if (["done", "failed", "closed", "cancelled"].includes(conversation.status)) {
|
|
2384
2605
|
throw new Error(`cannot send to ${conversation.conversation_id}; conversation is ${conversation.status}`);
|
|
2385
2606
|
}
|
|
@@ -2390,10 +2611,15 @@ async function runSend(options) {
|
|
|
2390
2611
|
throw new Error(`cannot send to ${conversation.conversation_id}; choose a supported model with --model first`);
|
|
2391
2612
|
}
|
|
2392
2613
|
const executor = executorForConversation(conversation);
|
|
2393
|
-
const type = options.type ??
|
|
2614
|
+
const type = options.type ??
|
|
2615
|
+
(conversation.status === "waiting_for_openclaw" ? "answer" : "task");
|
|
2394
2616
|
const nativeTakeoverForSend = isRecord(conversation.native_session_takeover)
|
|
2395
2617
|
? conversation.native_session_takeover
|
|
2396
2618
|
: undefined;
|
|
2619
|
+
if (rejectTerminalControl &&
|
|
2620
|
+
terminalControlFromTakeover(nativeTakeoverForSend)) {
|
|
2621
|
+
throw new Error("terminal control changed while waiting to send; refresh status and retry");
|
|
2622
|
+
}
|
|
2397
2623
|
const forkTakeoverForSend = isRecord(conversation.fork_context_takeover)
|
|
2398
2624
|
? conversation.fork_context_takeover
|
|
2399
2625
|
: undefined;
|
|
@@ -2416,7 +2642,9 @@ async function runSend(options) {
|
|
|
2416
2642
|
const nextConversation = {
|
|
2417
2643
|
...applyMessageToConversation(conversation, message),
|
|
2418
2644
|
executor,
|
|
2419
|
-
claude_session: executor.kind === "claude"
|
|
2645
|
+
claude_session: executor.kind === "claude"
|
|
2646
|
+
? executor.session
|
|
2647
|
+
: conversation.claude_session,
|
|
2420
2648
|
executor_model: options.model ?? conversation.executor_model,
|
|
2421
2649
|
model_selection: conversation.status === "needs_model_selection"
|
|
2422
2650
|
? {
|
|
@@ -2426,17 +2654,152 @@ async function runSend(options) {
|
|
|
2426
2654
|
}
|
|
2427
2655
|
: conversation.model_selection
|
|
2428
2656
|
};
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2657
|
+
if (persist) {
|
|
2658
|
+
saveState(statePath, nextConversation);
|
|
2659
|
+
appendEvent(logPath, messageEvent(message));
|
|
2660
|
+
runtimeLog("info", "message_created", {
|
|
2661
|
+
conversation_id: conversation.conversation_id,
|
|
2662
|
+
agent: executor.kind,
|
|
2663
|
+
executor_session: executor.session,
|
|
2664
|
+
message_type: type,
|
|
2665
|
+
state_path: statePath,
|
|
2666
|
+
event_log_path: logPath,
|
|
2667
|
+
message: textSummary(messageBody)
|
|
2668
|
+
});
|
|
2669
|
+
}
|
|
2670
|
+
return {
|
|
2671
|
+
conversation,
|
|
2672
|
+
executor,
|
|
2673
|
+
nativeTakeoverForSend,
|
|
2674
|
+
forkTakeoverForSend,
|
|
2675
|
+
needsNativeTakeoverBootstrap,
|
|
2676
|
+
needsForkTakeoverBootstrap,
|
|
2677
|
+
message,
|
|
2678
|
+
nextConversation
|
|
2679
|
+
};
|
|
2680
|
+
}
|
|
2681
|
+
async function runSend(options) {
|
|
2682
|
+
const messageBody = required(options.message ?? options.request, "--message is required");
|
|
2683
|
+
if (options.agentHardTimeoutMinutes !== undefined) {
|
|
2684
|
+
positiveMinutes(options.agentHardTimeoutMinutes, "--agent-hard-timeout-minutes");
|
|
2685
|
+
}
|
|
2686
|
+
cleanupIdleConversations(storeDirFromOptions(options), options);
|
|
2687
|
+
const terminalConversation = await resolveTerminalConversationFromOptions(options);
|
|
2688
|
+
if (terminalConversation) {
|
|
2689
|
+
if (options.background) {
|
|
2690
|
+
const releaseTerminalLock = acquireFileLock(terminalBridgeSendLockPath(storeDirFromOptions(options), terminalConversation.terminalControl), { timeoutMs: 30000 });
|
|
2691
|
+
let releaseStateLock;
|
|
2692
|
+
try {
|
|
2693
|
+
const managed = createManagedTerminalConversationFromRawId({
|
|
2694
|
+
options,
|
|
2695
|
+
conversationId: terminalConversation.conversationId,
|
|
2696
|
+
agent: terminalConversation.agent,
|
|
2697
|
+
pid: terminalConversation.pid,
|
|
2698
|
+
messageBody,
|
|
2699
|
+
terminalControl: terminalConversation.terminalControl
|
|
2700
|
+
});
|
|
2701
|
+
ensureDir(path.dirname(managed.statePath));
|
|
2702
|
+
releaseStateLock = acquireFileLock(`${managed.statePath}.lock`);
|
|
2703
|
+
await runTerminalControlSend({
|
|
2704
|
+
options,
|
|
2705
|
+
conversation: managed.conversation,
|
|
2706
|
+
nextConversation: managed.nextConversation,
|
|
2707
|
+
statePath: managed.statePath,
|
|
2708
|
+
logPath: managed.logPath,
|
|
2709
|
+
executor: managed.executor,
|
|
2710
|
+
message: managed.message,
|
|
2711
|
+
terminalControl: terminalConversation.terminalControl,
|
|
2712
|
+
needsNativeTakeoverBootstrap: true,
|
|
2713
|
+
terminalSendLockHeld: true,
|
|
2714
|
+
terminalStateLockHeld: true,
|
|
2715
|
+
recordMessageAfterSend: true,
|
|
2716
|
+
recordRawAttachmentAfterSend: true
|
|
2717
|
+
});
|
|
2718
|
+
}
|
|
2719
|
+
finally {
|
|
2720
|
+
try {
|
|
2721
|
+
releaseStateLock?.();
|
|
2722
|
+
}
|
|
2723
|
+
finally {
|
|
2724
|
+
releaseTerminalLock();
|
|
2725
|
+
}
|
|
2726
|
+
}
|
|
2727
|
+
return;
|
|
2728
|
+
}
|
|
2729
|
+
await runTerminalConversationSend({
|
|
2730
|
+
options,
|
|
2731
|
+
conversationId: terminalConversation.conversationId,
|
|
2732
|
+
agent: terminalConversation.agent,
|
|
2733
|
+
pid: terminalConversation.pid,
|
|
2734
|
+
messageBody,
|
|
2735
|
+
terminalControl: terminalConversation.terminalControl
|
|
2736
|
+
});
|
|
2737
|
+
return;
|
|
2738
|
+
}
|
|
2739
|
+
const loaded = loadConversationFromOptions(options);
|
|
2740
|
+
const { statePath, logPath } = loaded;
|
|
2741
|
+
const migratedConversation = await migrateLegacyTerminalAgentIdentity({
|
|
2742
|
+
...loaded,
|
|
2743
|
+
options
|
|
2744
|
+
});
|
|
2745
|
+
const migratedTakeover = isRecord(migratedConversation.native_session_takeover)
|
|
2746
|
+
? migratedConversation.native_session_takeover
|
|
2747
|
+
: undefined;
|
|
2748
|
+
const migratedTerminalControl = terminalControlFromTakeover(migratedTakeover);
|
|
2749
|
+
if (migratedTerminalControl) {
|
|
2750
|
+
const releaseTerminalLock = acquireFileLock(terminalBridgeSendLockPath(storeDirFromOptions(options), migratedTerminalControl), { timeoutMs: 30000 });
|
|
2751
|
+
let releaseStateLock;
|
|
2752
|
+
try {
|
|
2753
|
+
releaseStateLock = acquireFileLock(`${statePath}.lock`);
|
|
2754
|
+
const prepared = prepareManagedSend({
|
|
2755
|
+
options,
|
|
2756
|
+
statePath,
|
|
2757
|
+
logPath,
|
|
2758
|
+
messageBody,
|
|
2759
|
+
stateLockHeld: true,
|
|
2760
|
+
persist: false
|
|
2761
|
+
});
|
|
2762
|
+
const currentTerminalControl = terminalControlFromTakeover(prepared.nativeTakeoverForSend);
|
|
2763
|
+
if (!currentTerminalControl ||
|
|
2764
|
+
currentTerminalControl.kind !== migratedTerminalControl.kind ||
|
|
2765
|
+
currentTerminalControl.target !== migratedTerminalControl.target ||
|
|
2766
|
+
currentTerminalControl.socketPath !== migratedTerminalControl.socketPath ||
|
|
2767
|
+
currentTerminalControl.panePid !== migratedTerminalControl.panePid) {
|
|
2768
|
+
throw new Error("terminal control changed while waiting to send; refresh status and retry");
|
|
2769
|
+
}
|
|
2770
|
+
await runTerminalControlSend({
|
|
2771
|
+
options,
|
|
2772
|
+
conversation: prepared.conversation,
|
|
2773
|
+
nextConversation: prepared.nextConversation,
|
|
2774
|
+
statePath,
|
|
2775
|
+
logPath,
|
|
2776
|
+
executor: prepared.executor,
|
|
2777
|
+
message: prepared.message,
|
|
2778
|
+
terminalControl: currentTerminalControl,
|
|
2779
|
+
needsNativeTakeoverBootstrap: prepared.needsNativeTakeoverBootstrap,
|
|
2780
|
+
terminalSendLockHeld: true,
|
|
2781
|
+
terminalStateLockHeld: true,
|
|
2782
|
+
recordMessageAfterSend: true
|
|
2783
|
+
});
|
|
2784
|
+
}
|
|
2785
|
+
finally {
|
|
2786
|
+
try {
|
|
2787
|
+
releaseStateLock?.();
|
|
2788
|
+
}
|
|
2789
|
+
finally {
|
|
2790
|
+
releaseTerminalLock();
|
|
2791
|
+
}
|
|
2792
|
+
}
|
|
2793
|
+
return;
|
|
2794
|
+
}
|
|
2795
|
+
const prepared = prepareManagedSend({
|
|
2796
|
+
options,
|
|
2797
|
+
statePath,
|
|
2798
|
+
logPath,
|
|
2799
|
+
messageBody,
|
|
2800
|
+
rejectTerminalControl: true
|
|
2439
2801
|
});
|
|
2802
|
+
const { conversation, executor, nativeTakeoverForSend, forkTakeoverForSend, needsNativeTakeoverBootstrap, needsForkTakeoverBootstrap, message, nextConversation } = prepared;
|
|
2440
2803
|
const executorEnv = environmentForExecutor(executor, {
|
|
2441
2804
|
allProxy: options.allProxy ?? conversation.executor_all_proxy
|
|
2442
2805
|
});
|
|
@@ -2566,7 +2929,7 @@ async function runSend(options) {
|
|
|
2566
2929
|
const acpxArgs = buildAcpxPromptArgs({ executor, payload, model: executorModel });
|
|
2567
2930
|
if (options.background) {
|
|
2568
2931
|
const outputPath = path.join(path.dirname(logPath), `${executor.kind}-followup-output.log`);
|
|
2569
|
-
const outputFd =
|
|
2932
|
+
const outputFd = openPrivateAppendFile(outputPath);
|
|
2570
2933
|
const child = spawn(acpxPath, acpxArgs, {
|
|
2571
2934
|
detached: true,
|
|
2572
2935
|
stdio: ["ignore", outputFd, outputFd],
|
|
@@ -2743,7 +3106,12 @@ async function runApprove(options) {
|
|
|
2743
3106
|
});
|
|
2744
3107
|
return;
|
|
2745
3108
|
}
|
|
2746
|
-
const
|
|
3109
|
+
const loaded = loadConversationFromOptions(options);
|
|
3110
|
+
const { statePath, logPath } = loaded;
|
|
3111
|
+
const conversation = await migrateLegacyTerminalAgentIdentity({
|
|
3112
|
+
...loaded,
|
|
3113
|
+
options
|
|
3114
|
+
});
|
|
2747
3115
|
const nativeTakeover = isRecord(conversation.native_session_takeover)
|
|
2748
3116
|
? conversation.native_session_takeover
|
|
2749
3117
|
: undefined;
|
|
@@ -2751,263 +3119,529 @@ async function runApprove(options) {
|
|
|
2751
3119
|
if (!terminalControl) {
|
|
2752
3120
|
throw new Error(`conversation ${conversation.conversation_id} is not controlled through a terminal`);
|
|
2753
3121
|
}
|
|
3122
|
+
if (["done", "failed", "closed", "cancelled"].includes(conversation.status)) {
|
|
3123
|
+
throw new Error(`cannot approve ${conversation.conversation_id}; conversation is ${conversation.status}`);
|
|
3124
|
+
}
|
|
2754
3125
|
const executor = executorForConversation(conversation);
|
|
2755
3126
|
const monitoredApproval = isRecord(nativeTakeover?.["terminal_bridge_approval"])
|
|
2756
3127
|
? nativeTakeover.terminal_bridge_approval
|
|
2757
3128
|
: undefined;
|
|
2758
|
-
const
|
|
3129
|
+
const suppliedExpectedFingerprint = stringValue(options.expectedApprovalFingerprint);
|
|
3130
|
+
const expectedFingerprint = suppliedExpectedFingerprint ??
|
|
2759
3131
|
stringValue(monitoredApproval?.fingerprint);
|
|
3132
|
+
const hooklessClaudeScreenApproval = executor.kind === "claude" &&
|
|
3133
|
+
nativeTakeover?.claude_hook_mode !== "enabled";
|
|
3134
|
+
if (hooklessClaudeScreenApproval) {
|
|
3135
|
+
const monitoredState = isRecord(monitoredApproval?.approval_state)
|
|
3136
|
+
? monitoredApproval.approval_state
|
|
3137
|
+
: undefined;
|
|
3138
|
+
const pendingDispatch = isRecord(nativeTakeover?.terminal_bridge_approval_dispatch)
|
|
3139
|
+
? nativeTakeover.terminal_bridge_approval_dispatch
|
|
3140
|
+
: undefined;
|
|
3141
|
+
const notifiedAt = validTimestampMs(monitoredApproval?.notified_at);
|
|
3142
|
+
if (conversation.status !== "waiting_for_openclaw" ||
|
|
3143
|
+
monitoredState?.decision_mode !== "keys" ||
|
|
3144
|
+
!stringValue(monitoredApproval?.fingerprint)) {
|
|
3145
|
+
printJson({
|
|
3146
|
+
conversation,
|
|
3147
|
+
approved: false,
|
|
3148
|
+
blocked: true,
|
|
3149
|
+
reason: "Claude screen approval requires a current managed-turn approval notification",
|
|
3150
|
+
terminal_control: terminalControl
|
|
3151
|
+
});
|
|
3152
|
+
return;
|
|
3153
|
+
}
|
|
3154
|
+
if (notifiedAt === undefined ||
|
|
3155
|
+
Date.now() - notifiedAt > CLAUDE_SCREEN_APPROVAL_TTL_MS) {
|
|
3156
|
+
printJson({
|
|
3157
|
+
conversation,
|
|
3158
|
+
approved: false,
|
|
3159
|
+
blocked: true,
|
|
3160
|
+
reason: "Claude screen approval expired; inspect and resolve the terminal manually",
|
|
3161
|
+
terminal_control: terminalControl
|
|
3162
|
+
});
|
|
3163
|
+
return;
|
|
3164
|
+
}
|
|
3165
|
+
if (!suppliedExpectedFingerprint ||
|
|
3166
|
+
expectedFingerprint !== monitoredApproval?.fingerprint) {
|
|
3167
|
+
printJson({
|
|
3168
|
+
conversation,
|
|
3169
|
+
approved: false,
|
|
3170
|
+
blocked: true,
|
|
3171
|
+
reason: "Claude screen approval requires the latest notified fingerprint",
|
|
3172
|
+
terminal_control: terminalControl
|
|
3173
|
+
});
|
|
3174
|
+
return;
|
|
3175
|
+
}
|
|
3176
|
+
if (pendingDispatch?.state === "reserved" &&
|
|
3177
|
+
pendingDispatch.terminal_bridge_message_id ===
|
|
3178
|
+
nativeTakeover?.terminal_bridge_message_id) {
|
|
3179
|
+
printJson({
|
|
3180
|
+
conversation,
|
|
3181
|
+
approved: false,
|
|
3182
|
+
blocked: true,
|
|
3183
|
+
reason: "a previous Claude approval dispatch has an uncertain outcome; inspect and resolve the terminal manually",
|
|
3184
|
+
terminal_control: terminalControl
|
|
3185
|
+
});
|
|
3186
|
+
return;
|
|
3187
|
+
}
|
|
3188
|
+
if (expectedFingerprint ===
|
|
3189
|
+
stringValue(nativeTakeover?.terminal_bridge_last_approval_fingerprint)) {
|
|
3190
|
+
printJson({
|
|
3191
|
+
conversation,
|
|
3192
|
+
approved: false,
|
|
3193
|
+
blocked: true,
|
|
3194
|
+
reason: "Claude screen approval fingerprint was already consumed",
|
|
3195
|
+
terminal_control: terminalControl
|
|
3196
|
+
});
|
|
3197
|
+
return;
|
|
3198
|
+
}
|
|
3199
|
+
}
|
|
2760
3200
|
const autoApproved = options.autoApproved === true;
|
|
2761
3201
|
const policyRuleId = stringValue(options.policyRuleId);
|
|
2762
3202
|
const policyFingerprint = stringValue(options.policyFingerprint);
|
|
2763
|
-
const
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
ts: new Date().toISOString(),
|
|
2776
|
-
conversation_id: conversation.conversation_id,
|
|
2777
|
-
event: "terminal_auto_approval_decision",
|
|
2778
|
-
action: "rejected",
|
|
2779
|
-
reason: approval.reason,
|
|
2780
|
-
terminal_control: terminalControl,
|
|
2781
|
-
expected_fingerprint: expectedFingerprint,
|
|
2782
|
-
actual_fingerprint: actualFingerprint,
|
|
2783
|
-
policy_rule_id: policyRuleId,
|
|
2784
|
-
policy_fingerprint: policyFingerprint
|
|
2785
|
-
});
|
|
2786
|
-
}
|
|
2787
|
-
printJson({
|
|
2788
|
-
conversation,
|
|
2789
|
-
approved: false,
|
|
2790
|
-
blocked: approval.blocked,
|
|
2791
|
-
reason: approval.reason,
|
|
2792
|
-
terminal_control: terminalControl,
|
|
2793
|
-
expected_approval_fingerprint: expectedFingerprint,
|
|
2794
|
-
actual_approval_fingerprint: actualFingerprint,
|
|
2795
|
-
screen_excerpt: approval.screenExcerpt
|
|
2796
|
-
});
|
|
2797
|
-
return;
|
|
2798
|
-
}
|
|
2799
|
-
appendEvent(logPath, {
|
|
2800
|
-
ts: new Date().toISOString(),
|
|
2801
|
-
conversation_id: conversation.conversation_id,
|
|
2802
|
-
event: "terminal_approval_send",
|
|
2803
|
-
terminal_control: terminalControl,
|
|
2804
|
-
key: approval.key,
|
|
2805
|
-
keys: approval.keys,
|
|
2806
|
-
label: approval.label,
|
|
2807
|
-
decision_mode: approval.decisionMode,
|
|
2808
|
-
request_id: approval.requestId,
|
|
2809
|
-
approval_fingerprint: actualFingerprint,
|
|
2810
|
-
auto_approved: autoApproved,
|
|
2811
|
-
policy_rule_id: policyRuleId,
|
|
2812
|
-
policy_fingerprint: policyFingerprint
|
|
2813
|
-
});
|
|
2814
|
-
if (autoApproved) {
|
|
2815
|
-
appendEvent(logPath, {
|
|
2816
|
-
ts: new Date().toISOString(),
|
|
2817
|
-
conversation_id: conversation.conversation_id,
|
|
2818
|
-
event: "terminal_auto_approval_decision",
|
|
2819
|
-
action: "approved",
|
|
2820
|
-
terminal_control: terminalControl,
|
|
2821
|
-
approval_fingerprint: actualFingerprint,
|
|
2822
|
-
policy_rule_id: policyRuleId,
|
|
2823
|
-
policy_fingerprint: policyFingerprint
|
|
2824
|
-
});
|
|
2825
|
-
}
|
|
2826
|
-
runtimeLog("info", "terminal_approval_send", {
|
|
2827
|
-
conversation_id: conversation.conversation_id,
|
|
2828
|
-
terminal_target: terminalControl.target,
|
|
2829
|
-
key: approval.key,
|
|
2830
|
-
keys: approval.keys,
|
|
2831
|
-
label: approval.label,
|
|
2832
|
-
decision_mode: approval.decisionMode,
|
|
2833
|
-
request_id: approval.requestId,
|
|
2834
|
-
approval_fingerprint: actualFingerprint,
|
|
2835
|
-
auto_approved: autoApproved,
|
|
2836
|
-
policy_rule_id: policyRuleId,
|
|
2837
|
-
policy_fingerprint: policyFingerprint
|
|
2838
|
-
});
|
|
2839
|
-
const nativeTakeoverForUpdate = isRecord(conversation.native_session_takeover)
|
|
2840
|
-
? { ...conversation.native_session_takeover }
|
|
2841
|
-
: {};
|
|
2842
|
-
const approvalResolvedAt = new Date().toISOString();
|
|
2843
|
-
const agentTimeoutMinutes = Number(options.agentTimeoutMinutes ??
|
|
2844
|
-
nativeTakeoverForUpdate.terminal_bridge_inactivity_timeout_minutes ??
|
|
2845
|
-
DEFAULT_AGENT_TIMEOUT_MINUTES);
|
|
2846
|
-
const agentHardTimeoutMinutes = positiveMinutes(options.agentHardTimeoutMinutes ??
|
|
2847
|
-
nativeTakeoverForUpdate.terminal_bridge_hard_timeout_minutes ??
|
|
2848
|
-
DEFAULT_AGENT_HARD_TIMEOUT_MINUTES, "--agent-hard-timeout-minutes");
|
|
2849
|
-
const nextNativeTakeover = {
|
|
2850
|
-
...nativeTakeoverForUpdate,
|
|
2851
|
-
terminal_bridge_approval: undefined,
|
|
2852
|
-
terminal_bridge_approval_resolved_at: approvalResolvedAt,
|
|
2853
|
-
terminal_bridge_monitor_started_at: approvalResolvedAt,
|
|
2854
|
-
terminal_bridge_last_activity_at: approvalResolvedAt,
|
|
2855
|
-
terminal_bridge_last_activity_reason: "approval resolved",
|
|
2856
|
-
terminal_bridge_inactivity_timeout_minutes: agentTimeoutMinutes,
|
|
2857
|
-
terminal_bridge_hard_timeout_minutes: agentHardTimeoutMinutes,
|
|
2858
|
-
terminal_bridge_inactivity_deadline_at: deadlineAt(approvalResolvedAt, agentTimeoutMinutes),
|
|
2859
|
-
terminal_bridge_hard_deadline_at: deadlineAt(stringValue(nativeTakeoverForUpdate.terminal_bridge_started_at) ?? approvalResolvedAt, agentHardTimeoutMinutes)
|
|
3203
|
+
const autoApprovalPolicy = autoApproved
|
|
3204
|
+
? parseJsonOption(options.autoApprovalPolicyJson, "--auto-approval-policy-json")
|
|
3205
|
+
: undefined;
|
|
3206
|
+
const runtimeIdentity = terminalRuntimeIdentityForConversation(conversation, terminalControl);
|
|
3207
|
+
let executorPolicyDecision;
|
|
3208
|
+
const releaseTerminalLock = acquireFileLock(terminalBridgeSendLockPath(storeDirFromOptions(options), terminalControl), { timeoutMs: 30000 });
|
|
3209
|
+
let terminalLockReleased = false;
|
|
3210
|
+
const releaseApprovalTerminalLock = () => {
|
|
3211
|
+
if (!terminalLockReleased) {
|
|
3212
|
+
terminalLockReleased = true;
|
|
3213
|
+
releaseTerminalLock();
|
|
3214
|
+
}
|
|
2860
3215
|
};
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
3216
|
+
let releaseStateLock;
|
|
3217
|
+
const releaseApprovalStateLock = () => {
|
|
3218
|
+
if (releaseStateLock) {
|
|
3219
|
+
const release = releaseStateLock;
|
|
3220
|
+
releaseStateLock = undefined;
|
|
3221
|
+
release();
|
|
3222
|
+
}
|
|
2867
3223
|
};
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
3224
|
+
try {
|
|
3225
|
+
let approval;
|
|
3226
|
+
let lockedConversation = conversation;
|
|
3227
|
+
const currentConversation = loadState(statePath);
|
|
3228
|
+
const currentTakeover = isRecord(currentConversation.native_session_takeover)
|
|
3229
|
+
? currentConversation.native_session_takeover
|
|
3230
|
+
: undefined;
|
|
3231
|
+
const currentControl = terminalControlFromTakeover(currentTakeover);
|
|
3232
|
+
const currentApproval = isRecord(currentTakeover?.terminal_bridge_approval)
|
|
3233
|
+
? currentTakeover.terminal_bridge_approval
|
|
3234
|
+
: undefined;
|
|
3235
|
+
if (currentConversation.status !== conversation.status ||
|
|
3236
|
+
currentTakeover?.terminal_bridge_message_id !== nativeTakeover?.terminal_bridge_message_id ||
|
|
3237
|
+
currentControl?.target !== terminalControl.target ||
|
|
3238
|
+
currentControl?.socketPath !== terminalControl.socketPath ||
|
|
3239
|
+
(hooklessClaudeScreenApproval &&
|
|
3240
|
+
currentApproval?.fingerprint !== monitoredApproval?.fingerprint)) {
|
|
3241
|
+
throw new Error("approval state changed while waiting for terminal control; refresh status and retry");
|
|
3242
|
+
}
|
|
3243
|
+
lockedConversation = currentConversation;
|
|
3244
|
+
approval = await createTerminalAgentBridge(options).approve(executor.kind, terminalControl, {
|
|
3245
|
+
expectedFingerprint,
|
|
3246
|
+
scrollbackLines: Number(options.scrollbackLines ?? 120),
|
|
3247
|
+
runtime: runtimeIdentity,
|
|
3248
|
+
requiredDecisionMode: autoApproved && executor.kind === "claude"
|
|
3249
|
+
? "structured"
|
|
3250
|
+
: undefined,
|
|
3251
|
+
authorize: autoApproved
|
|
3252
|
+
? ({ agent, terminalControl: currentTerminalControl, inspection, fingerprint }) => {
|
|
3253
|
+
if (!autoApprovalPolicy) {
|
|
3254
|
+
return {
|
|
3255
|
+
approved: false,
|
|
3256
|
+
reason: "automatic approval requires an executor-side policy"
|
|
3257
|
+
};
|
|
3258
|
+
}
|
|
3259
|
+
const candidate = {
|
|
3260
|
+
agent,
|
|
3261
|
+
kind: inspection.approval.promptKind ?? "unknown",
|
|
3262
|
+
decisionMode: inspection.approval.approvable
|
|
3263
|
+
? inspection.approval.action.mode ?? "keys"
|
|
3264
|
+
: undefined,
|
|
3265
|
+
command: inspection.approval.command,
|
|
3266
|
+
cwd: inspection.approval.cwd ?? currentTerminalControl.currentPath,
|
|
3267
|
+
fingerprint: fingerprint ?? "",
|
|
3268
|
+
terminalTarget: currentTerminalControl.target
|
|
3269
|
+
};
|
|
3270
|
+
executorPolicyDecision = evaluateApprovalPolicy({
|
|
3271
|
+
policy: autoApprovalPolicy,
|
|
3272
|
+
candidate
|
|
3273
|
+
});
|
|
3274
|
+
if (executorPolicyDecision.action !== "approve") {
|
|
3275
|
+
return {
|
|
3276
|
+
approved: false,
|
|
3277
|
+
reason: `executor-side auto-approval policy rejected the current request: ${executorPolicyDecision.reason}`
|
|
3278
|
+
};
|
|
3279
|
+
}
|
|
3280
|
+
if (policyRuleId && executorPolicyDecision.ruleId !== policyRuleId) {
|
|
3281
|
+
return {
|
|
3282
|
+
approved: false,
|
|
3283
|
+
reason: "executor-side auto-approval rule changed before execution"
|
|
3284
|
+
};
|
|
3285
|
+
}
|
|
3286
|
+
if (policyFingerprint &&
|
|
3287
|
+
executorPolicyDecision.policyFingerprint !== policyFingerprint) {
|
|
3288
|
+
return {
|
|
3289
|
+
approved: false,
|
|
3290
|
+
reason: "executor-side auto-approval policy changed before execution"
|
|
3291
|
+
};
|
|
3292
|
+
}
|
|
3293
|
+
return { approved: true };
|
|
2880
3294
|
}
|
|
2881
|
-
|
|
3295
|
+
: undefined,
|
|
3296
|
+
beforeKeyDispatch: hooklessClaudeScreenApproval
|
|
3297
|
+
? ({ fingerprint, terminalControl: dispatchControl, keys }) => {
|
|
3298
|
+
if (releaseStateLock) {
|
|
3299
|
+
throw new Error("Claude approval dispatch was already reserved");
|
|
3300
|
+
}
|
|
3301
|
+
releaseStateLock = acquireFileLock(`${statePath}.lock`);
|
|
3302
|
+
const latestConversation = loadState(statePath);
|
|
3303
|
+
const latestTakeover = isRecord(latestConversation.native_session_takeover)
|
|
3304
|
+
? latestConversation.native_session_takeover
|
|
3305
|
+
: undefined;
|
|
3306
|
+
const latestControl = terminalControlFromTakeover(latestTakeover);
|
|
3307
|
+
const latestApproval = isRecord(latestTakeover?.terminal_bridge_approval)
|
|
3308
|
+
? latestTakeover.terminal_bridge_approval
|
|
3309
|
+
: undefined;
|
|
3310
|
+
const latestNotifiedAt = validTimestampMs(latestApproval?.notified_at);
|
|
3311
|
+
const latestDispatch = isRecord(latestTakeover?.terminal_bridge_approval_dispatch)
|
|
3312
|
+
? latestTakeover.terminal_bridge_approval_dispatch
|
|
3313
|
+
: undefined;
|
|
3314
|
+
if (!latestTakeover ||
|
|
3315
|
+
latestConversation.status !== "waiting_for_openclaw" ||
|
|
3316
|
+
latestTakeover.terminal_bridge_message_id !==
|
|
3317
|
+
nativeTakeover?.terminal_bridge_message_id ||
|
|
3318
|
+
latestApproval?.fingerprint !== fingerprint ||
|
|
3319
|
+
latestNotifiedAt === undefined ||
|
|
3320
|
+
Date.now() - latestNotifiedAt > CLAUDE_SCREEN_APPROVAL_TTL_MS ||
|
|
3321
|
+
expectedFingerprint !== fingerprint ||
|
|
3322
|
+
latestControl?.target !== dispatchControl.target ||
|
|
3323
|
+
latestControl?.socketPath !== dispatchControl.socketPath) {
|
|
3324
|
+
throw new Error("approval state changed before terminal dispatch; refresh status and retry");
|
|
3325
|
+
}
|
|
3326
|
+
if (latestDispatch?.state === "reserved" &&
|
|
3327
|
+
latestDispatch.terminal_bridge_message_id ===
|
|
3328
|
+
latestTakeover.terminal_bridge_message_id) {
|
|
3329
|
+
throw new Error("a previous Claude approval dispatch has an uncertain outcome; inspect and resolve the terminal manually");
|
|
3330
|
+
}
|
|
3331
|
+
const reservedAt = new Date().toISOString();
|
|
3332
|
+
const reservedConversation = {
|
|
3333
|
+
...latestConversation,
|
|
3334
|
+
native_session_takeover: {
|
|
3335
|
+
...latestTakeover,
|
|
3336
|
+
terminal_bridge_approval_dispatch: {
|
|
3337
|
+
state: "reserved",
|
|
3338
|
+
attempt_id: randomUUID(),
|
|
3339
|
+
fingerprint,
|
|
3340
|
+
keys,
|
|
3341
|
+
terminal_target: dispatchControl.target,
|
|
3342
|
+
terminal_bridge_message_id: latestTakeover.terminal_bridge_message_id,
|
|
3343
|
+
reserved_at: reservedAt
|
|
3344
|
+
}
|
|
3345
|
+
},
|
|
3346
|
+
updated_at: reservedAt
|
|
3347
|
+
};
|
|
3348
|
+
saveState(statePath, reservedConversation);
|
|
3349
|
+
lockedConversation = reservedConversation;
|
|
3350
|
+
}
|
|
3351
|
+
: undefined
|
|
3352
|
+
});
|
|
3353
|
+
const actualFingerprint = approval.fingerprint;
|
|
3354
|
+
const effectivePolicyRuleId = executorPolicyDecision?.ruleId ?? policyRuleId;
|
|
3355
|
+
const effectivePolicyFingerprint = executorPolicyDecision?.policyFingerprint ?? policyFingerprint;
|
|
3356
|
+
if (!approval.approved) {
|
|
3357
|
+
releaseApprovalStateLock();
|
|
3358
|
+
releaseApprovalTerminalLock();
|
|
3359
|
+
if (autoApproved) {
|
|
3360
|
+
appendEvent(logPath, {
|
|
3361
|
+
ts: new Date().toISOString(),
|
|
3362
|
+
conversation_id: conversation.conversation_id,
|
|
3363
|
+
event: "terminal_auto_approval_decision",
|
|
3364
|
+
action: "rejected",
|
|
3365
|
+
reason: approval.reason,
|
|
3366
|
+
terminal_control: terminalControl,
|
|
3367
|
+
expected_fingerprint: expectedFingerprint,
|
|
3368
|
+
actual_fingerprint: actualFingerprint,
|
|
3369
|
+
policy_rule_id: effectivePolicyRuleId,
|
|
3370
|
+
policy_fingerprint: effectivePolicyFingerprint
|
|
3371
|
+
});
|
|
3372
|
+
}
|
|
3373
|
+
printJson({
|
|
3374
|
+
conversation,
|
|
3375
|
+
approved: false,
|
|
3376
|
+
blocked: approval.blocked,
|
|
3377
|
+
reason: approval.reason,
|
|
3378
|
+
terminal_control: terminalControl,
|
|
3379
|
+
expected_approval_fingerprint: expectedFingerprint,
|
|
3380
|
+
actual_approval_fingerprint: actualFingerprint,
|
|
3381
|
+
screen_excerpt: approval.screenExcerpt
|
|
3382
|
+
});
|
|
3383
|
+
return;
|
|
2882
3384
|
}
|
|
2883
|
-
}
|
|
2884
|
-
saveState(statePath, nextConversation);
|
|
2885
|
-
const bridgeMonitor = startTerminalBridgeMonitorForConversation({
|
|
2886
|
-
conversation: nextConversation,
|
|
2887
|
-
statePath,
|
|
2888
|
-
logPath,
|
|
2889
|
-
options
|
|
2890
|
-
});
|
|
2891
|
-
if (bridgeMonitor) {
|
|
2892
3385
|
appendEvent(logPath, {
|
|
2893
3386
|
ts: new Date().toISOString(),
|
|
2894
3387
|
conversation_id: conversation.conversation_id,
|
|
2895
|
-
event: "
|
|
2896
|
-
pid: bridgeMonitor.pid ?? null,
|
|
3388
|
+
event: "terminal_approval_send",
|
|
2897
3389
|
terminal_control: terminalControl,
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
3390
|
+
key: approval.key,
|
|
3391
|
+
keys: approval.keys,
|
|
3392
|
+
label: approval.label,
|
|
3393
|
+
decision_mode: approval.decisionMode,
|
|
3394
|
+
request_id: approval.requestId,
|
|
3395
|
+
approval_fingerprint: actualFingerprint,
|
|
3396
|
+
auto_approved: autoApproved,
|
|
3397
|
+
policy_rule_id: effectivePolicyRuleId,
|
|
3398
|
+
policy_fingerprint: effectivePolicyFingerprint
|
|
2901
3399
|
});
|
|
2902
|
-
|
|
3400
|
+
if (autoApproved) {
|
|
3401
|
+
appendEvent(logPath, {
|
|
3402
|
+
ts: new Date().toISOString(),
|
|
3403
|
+
conversation_id: conversation.conversation_id,
|
|
3404
|
+
event: "terminal_auto_approval_decision",
|
|
3405
|
+
action: "approved",
|
|
3406
|
+
terminal_control: terminalControl,
|
|
3407
|
+
approval_fingerprint: actualFingerprint,
|
|
3408
|
+
policy_rule_id: effectivePolicyRuleId,
|
|
3409
|
+
policy_fingerprint: effectivePolicyFingerprint
|
|
3410
|
+
});
|
|
3411
|
+
}
|
|
3412
|
+
runtimeLog("info", "terminal_approval_send", {
|
|
2903
3413
|
conversation_id: conversation.conversation_id,
|
|
2904
|
-
monitor_pid: bridgeMonitor.pid ?? null,
|
|
2905
3414
|
terminal_target: terminalControl.target,
|
|
2906
|
-
|
|
3415
|
+
key: approval.key,
|
|
3416
|
+
keys: approval.keys,
|
|
3417
|
+
label: approval.label,
|
|
3418
|
+
decision_mode: approval.decisionMode,
|
|
3419
|
+
request_id: approval.requestId,
|
|
3420
|
+
approval_fingerprint: actualFingerprint,
|
|
3421
|
+
auto_approved: autoApproved,
|
|
3422
|
+
policy_rule_id: effectivePolicyRuleId,
|
|
3423
|
+
policy_fingerprint: effectivePolicyFingerprint
|
|
3424
|
+
});
|
|
3425
|
+
const nativeTakeoverForUpdate = isRecord(lockedConversation.native_session_takeover)
|
|
3426
|
+
? { ...lockedConversation.native_session_takeover }
|
|
3427
|
+
: {};
|
|
3428
|
+
const approvalResolvedAt = new Date().toISOString();
|
|
3429
|
+
const agentTimeoutMinutes = Number(options.agentTimeoutMinutes ??
|
|
3430
|
+
nativeTakeoverForUpdate.terminal_bridge_inactivity_timeout_minutes ??
|
|
3431
|
+
DEFAULT_AGENT_TIMEOUT_MINUTES);
|
|
3432
|
+
const agentHardTimeoutMinutes = positiveMinutes(options.agentHardTimeoutMinutes ??
|
|
3433
|
+
nativeTakeoverForUpdate.terminal_bridge_hard_timeout_minutes ??
|
|
3434
|
+
DEFAULT_AGENT_HARD_TIMEOUT_MINUTES, "--agent-hard-timeout-minutes");
|
|
3435
|
+
const nextNativeTakeover = {
|
|
3436
|
+
...nativeTakeoverForUpdate,
|
|
3437
|
+
terminal_bridge_approval: undefined,
|
|
3438
|
+
terminal_bridge_approval_dispatch: undefined,
|
|
3439
|
+
terminal_bridge_approval_resolved_at: approvalResolvedAt,
|
|
3440
|
+
terminal_bridge_last_approval_fingerprint: actualFingerprint,
|
|
3441
|
+
terminal_bridge_last_approval_at: approvalResolvedAt,
|
|
3442
|
+
terminal_bridge_monitor_lock_version: TERMINAL_BRIDGE_MONITOR_LOCK_VERSION,
|
|
3443
|
+
terminal_bridge_monitor_started_at: approvalResolvedAt,
|
|
3444
|
+
terminal_bridge_last_activity_at: approvalResolvedAt,
|
|
3445
|
+
terminal_bridge_last_activity_reason: "approval resolved",
|
|
3446
|
+
terminal_bridge_inactivity_timeout_minutes: agentTimeoutMinutes,
|
|
3447
|
+
terminal_bridge_hard_timeout_minutes: agentHardTimeoutMinutes,
|
|
3448
|
+
terminal_bridge_inactivity_deadline_at: deadlineAt(approvalResolvedAt, agentTimeoutMinutes),
|
|
3449
|
+
terminal_bridge_hard_deadline_at: deadlineAt(stringValue(nativeTakeoverForUpdate.terminal_bridge_started_at) ?? approvalResolvedAt, agentHardTimeoutMinutes)
|
|
3450
|
+
};
|
|
3451
|
+
delete nextNativeTakeover.terminal_bridge_approval;
|
|
3452
|
+
delete nextNativeTakeover.terminal_bridge_approval_dispatch;
|
|
3453
|
+
let nextConversation = {
|
|
3454
|
+
...lockedConversation,
|
|
3455
|
+
status: terminalBridgeEnabled(lockedConversation)
|
|
3456
|
+
? "waiting_for_agent"
|
|
3457
|
+
: lockedConversation.status,
|
|
3458
|
+
native_session_takeover: nextNativeTakeover,
|
|
3459
|
+
updated_at: approvalResolvedAt
|
|
3460
|
+
};
|
|
3461
|
+
const approvalLeaseDeadlines = [
|
|
3462
|
+
stringValue(nextNativeTakeover.terminal_bridge_inactivity_deadline_at),
|
|
3463
|
+
stringValue(nextNativeTakeover.terminal_bridge_hard_deadline_at)
|
|
3464
|
+
].filter((value) => Boolean(value));
|
|
3465
|
+
if (approvalLeaseDeadlines.length > 0) {
|
|
3466
|
+
const renewedLease = renewClaudeHookLease(nextConversation, new Date(Math.min(...approvalLeaseDeadlines.map((value) => Date.parse(value)))).toISOString());
|
|
3467
|
+
if (renewedLease) {
|
|
3468
|
+
nextConversation = {
|
|
3469
|
+
...nextConversation,
|
|
3470
|
+
native_session_takeover: {
|
|
3471
|
+
...nextNativeTakeover,
|
|
3472
|
+
claude_hook_lease_id: renewedLease.id
|
|
3473
|
+
}
|
|
3474
|
+
};
|
|
3475
|
+
}
|
|
3476
|
+
}
|
|
3477
|
+
saveState(statePath, nextConversation);
|
|
3478
|
+
releaseApprovalStateLock();
|
|
3479
|
+
releaseApprovalTerminalLock();
|
|
3480
|
+
const bridgeMonitor = startTerminalBridgeMonitorForConversation({
|
|
3481
|
+
conversation: nextConversation,
|
|
3482
|
+
statePath,
|
|
3483
|
+
logPath,
|
|
3484
|
+
options
|
|
3485
|
+
});
|
|
3486
|
+
if (bridgeMonitor) {
|
|
3487
|
+
appendEvent(logPath, {
|
|
3488
|
+
ts: new Date().toISOString(),
|
|
3489
|
+
conversation_id: conversation.conversation_id,
|
|
3490
|
+
event: "terminal_bridge_monitor_launch",
|
|
3491
|
+
pid: bridgeMonitor.pid ?? null,
|
|
3492
|
+
terminal_control: terminalControl,
|
|
3493
|
+
reason: "approval_resolved",
|
|
3494
|
+
agent_timeout_minutes: agentTimeoutMinutes,
|
|
3495
|
+
agent_hard_timeout_minutes: agentHardTimeoutMinutes
|
|
3496
|
+
});
|
|
3497
|
+
runtimeLog("info", "terminal_bridge_monitor_launch", {
|
|
3498
|
+
conversation_id: conversation.conversation_id,
|
|
3499
|
+
monitor_pid: bridgeMonitor.pid ?? null,
|
|
3500
|
+
terminal_target: terminalControl.target,
|
|
3501
|
+
reason: "approval_resolved"
|
|
3502
|
+
});
|
|
3503
|
+
}
|
|
3504
|
+
printJson({
|
|
3505
|
+
conversation: nextConversation,
|
|
3506
|
+
approved: true,
|
|
3507
|
+
terminal_control: terminalControl,
|
|
3508
|
+
key: approval.key,
|
|
3509
|
+
keys: approval.keys,
|
|
3510
|
+
label: approval.label,
|
|
3511
|
+
decision_mode: approval.decisionMode,
|
|
3512
|
+
request_id: approval.requestId,
|
|
3513
|
+
approval_fingerprint: actualFingerprint,
|
|
3514
|
+
auto_approved: autoApproved,
|
|
3515
|
+
policy_rule_id: effectivePolicyRuleId,
|
|
3516
|
+
monitor_pid: bridgeMonitor?.pid ?? null
|
|
2907
3517
|
});
|
|
2908
3518
|
}
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
request_id: approval.requestId,
|
|
2918
|
-
approval_fingerprint: actualFingerprint,
|
|
2919
|
-
auto_approved: autoApproved,
|
|
2920
|
-
policy_rule_id: policyRuleId,
|
|
2921
|
-
monitor_pid: bridgeMonitor?.pid ?? null
|
|
2922
|
-
});
|
|
3519
|
+
finally {
|
|
3520
|
+
try {
|
|
3521
|
+
releaseApprovalStateLock();
|
|
3522
|
+
}
|
|
3523
|
+
finally {
|
|
3524
|
+
releaseApprovalTerminalLock();
|
|
3525
|
+
}
|
|
3526
|
+
}
|
|
2923
3527
|
}
|
|
2924
3528
|
async function runTerminalConversationApprove({ options, conversationId, agent, terminalControl, pid }) {
|
|
2925
|
-
const
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
3529
|
+
const releaseTerminalLock = acquireFileLock(terminalBridgeSendLockPath(storeDirFromOptions(options), terminalControl), { timeoutMs: 30000 });
|
|
3530
|
+
try {
|
|
3531
|
+
if (agent === "claude") {
|
|
3532
|
+
printJson({
|
|
3533
|
+
conversation_id: conversationId,
|
|
3534
|
+
source: "terminal_control",
|
|
3535
|
+
approved: false,
|
|
3536
|
+
blocked: true,
|
|
3537
|
+
reason: "Claude screen approval requires `send --background` so AKK can bind it to an active managed turn",
|
|
3538
|
+
terminal_control: terminalControl
|
|
3539
|
+
});
|
|
3540
|
+
return;
|
|
2932
3541
|
}
|
|
2933
|
-
|
|
2934
|
-
|
|
3542
|
+
const approval = await createTerminalAgentBridge(options).approve(agent, terminalControl, {
|
|
3543
|
+
expectedFingerprint: stringValue(options.expectedApprovalFingerprint),
|
|
3544
|
+
scrollbackLines: Number(options.scrollbackLines ?? 120),
|
|
3545
|
+
runtime: {
|
|
3546
|
+
pid,
|
|
3547
|
+
cwd: terminalControl.currentPath,
|
|
3548
|
+
conversationId,
|
|
3549
|
+
terminalTarget: terminalControl.target
|
|
3550
|
+
}
|
|
3551
|
+
});
|
|
3552
|
+
if (!approval.approved) {
|
|
3553
|
+
printJson({
|
|
3554
|
+
conversation_id: conversationId,
|
|
3555
|
+
source: "terminal_control",
|
|
3556
|
+
approved: false,
|
|
3557
|
+
blocked: approval.blocked,
|
|
3558
|
+
reason: approval.reason,
|
|
3559
|
+
terminal_control: terminalControl,
|
|
3560
|
+
screen_excerpt: approval.screenExcerpt
|
|
3561
|
+
});
|
|
3562
|
+
return;
|
|
3563
|
+
}
|
|
3564
|
+
runtimeLog("info", "terminal_approval_send", {
|
|
3565
|
+
conversation_id: conversationId,
|
|
3566
|
+
agent,
|
|
3567
|
+
terminal_target: terminalControl.target,
|
|
3568
|
+
key: approval.key,
|
|
3569
|
+
keys: approval.keys,
|
|
3570
|
+
label: approval.label,
|
|
3571
|
+
decision_mode: approval.decisionMode,
|
|
3572
|
+
request_id: approval.requestId
|
|
3573
|
+
});
|
|
2935
3574
|
printJson({
|
|
2936
3575
|
conversation_id: conversationId,
|
|
2937
3576
|
source: "terminal_control",
|
|
2938
|
-
approved:
|
|
2939
|
-
blocked: approval.blocked,
|
|
2940
|
-
reason: approval.reason,
|
|
3577
|
+
approved: true,
|
|
2941
3578
|
terminal_control: terminalControl,
|
|
2942
|
-
|
|
3579
|
+
key: approval.key,
|
|
3580
|
+
keys: approval.keys,
|
|
3581
|
+
label: approval.label,
|
|
3582
|
+
approval_fingerprint: approval.fingerprint,
|
|
3583
|
+
decision_mode: approval.decisionMode,
|
|
3584
|
+
request_id: approval.requestId
|
|
2943
3585
|
});
|
|
2944
|
-
return;
|
|
2945
3586
|
}
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
terminal_target: terminalControl.target,
|
|
2950
|
-
key: approval.key,
|
|
2951
|
-
keys: approval.keys,
|
|
2952
|
-
label: approval.label,
|
|
2953
|
-
decision_mode: approval.decisionMode,
|
|
2954
|
-
request_id: approval.requestId
|
|
2955
|
-
});
|
|
2956
|
-
printJson({
|
|
2957
|
-
conversation_id: conversationId,
|
|
2958
|
-
source: "terminal_control",
|
|
2959
|
-
approved: true,
|
|
2960
|
-
terminal_control: terminalControl,
|
|
2961
|
-
key: approval.key,
|
|
2962
|
-
keys: approval.keys,
|
|
2963
|
-
label: approval.label,
|
|
2964
|
-
approval_fingerprint: approval.fingerprint,
|
|
2965
|
-
decision_mode: approval.decisionMode,
|
|
2966
|
-
request_id: approval.requestId
|
|
2967
|
-
});
|
|
3587
|
+
finally {
|
|
3588
|
+
releaseTerminalLock();
|
|
3589
|
+
}
|
|
2968
3590
|
}
|
|
2969
3591
|
async function runTerminalConversationSend({ options, conversationId, agent, pid, messageBody, terminalControl }) {
|
|
2970
|
-
const
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
const
|
|
2974
|
-
|
|
3592
|
+
const releaseTerminalLock = acquireFileLock(terminalBridgeSendLockPath(storeDirFromOptions(options), terminalControl), { timeoutMs: 30000 });
|
|
3593
|
+
try {
|
|
3594
|
+
const terminalPayload = terminalSubmissionPayload(String(messageBody));
|
|
3595
|
+
const terminalBridge = createTerminalAgentBridge(options);
|
|
3596
|
+
if (agent === "claude") {
|
|
3597
|
+
const status = await terminalBridge.status(agent, terminalControl, {
|
|
3598
|
+
scrollbackLines: Number(options.scrollbackLines ?? 120),
|
|
3599
|
+
runtime: {
|
|
3600
|
+
pid,
|
|
3601
|
+
cwd: terminalControl.currentPath,
|
|
3602
|
+
terminalTarget: terminalControl.target
|
|
3603
|
+
}
|
|
3604
|
+
});
|
|
3605
|
+
assertSafeClaudeTerminalSend(status);
|
|
3606
|
+
}
|
|
3607
|
+
await terminalBridge.send(agent, terminalControl, terminalPayload, {
|
|
2975
3608
|
runtime: {
|
|
2976
3609
|
pid,
|
|
2977
3610
|
cwd: terminalControl.currentPath,
|
|
2978
3611
|
terminalTarget: terminalControl.target
|
|
2979
3612
|
}
|
|
2980
3613
|
});
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
});
|
|
2990
|
-
printJson({
|
|
2991
|
-
conversation_id: conversationId,
|
|
2992
|
-
source: "terminal_control",
|
|
2993
|
-
delivered: true,
|
|
2994
|
-
status: "async_pending",
|
|
2995
|
-
background: true,
|
|
2996
|
-
callback_expected: false,
|
|
2997
|
-
terminal_control: terminalControl,
|
|
2998
|
-
message: {
|
|
2999
|
-
body: messageBody
|
|
3000
|
-
},
|
|
3001
|
-
openclaw_next_action: openClawYieldNextAction({
|
|
3002
|
-
conversationId,
|
|
3614
|
+
runtimeLog("info", "terminal_message_send", {
|
|
3615
|
+
conversation_id: conversationId,
|
|
3616
|
+
agent,
|
|
3617
|
+
terminal_target: terminalControl.target,
|
|
3618
|
+
message: textSummary(messageBody)
|
|
3619
|
+
});
|
|
3620
|
+
printJson({
|
|
3621
|
+
conversation_id: conversationId,
|
|
3003
3622
|
source: "terminal_control",
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3623
|
+
delivered: true,
|
|
3624
|
+
status: "async_pending",
|
|
3625
|
+
background: true,
|
|
3626
|
+
callback_expected: false,
|
|
3627
|
+
terminal_control: terminalControl,
|
|
3628
|
+
message: {
|
|
3629
|
+
body: messageBody
|
|
3630
|
+
},
|
|
3631
|
+
openclaw_next_action: openClawYieldNextAction({
|
|
3632
|
+
conversationId,
|
|
3633
|
+
source: "terminal_control",
|
|
3634
|
+
callbackExpected: false
|
|
3635
|
+
})
|
|
3636
|
+
});
|
|
3637
|
+
}
|
|
3638
|
+
finally {
|
|
3639
|
+
releaseTerminalLock();
|
|
3640
|
+
}
|
|
3007
3641
|
}
|
|
3008
|
-
async function runTerminalControlSend({ options, conversation, nextConversation, statePath, logPath, executor, message, terminalControl, needsNativeTakeoverBootstrap, terminalSendLockHeld = false }) {
|
|
3642
|
+
async function runTerminalControlSend({ options, conversation, nextConversation, statePath, logPath, executor, message, terminalControl, needsNativeTakeoverBootstrap, terminalSendLockHeld = false, terminalStateLockHeld = false, recordMessageAfterSend = false, recordRawAttachmentAfterSend = false }) {
|
|
3009
3643
|
const bridge = terminalBridgeEnabled(conversation);
|
|
3010
|
-
if (
|
|
3644
|
+
if (!terminalSendLockHeld) {
|
|
3011
3645
|
const releaseTerminalLock = acquireFileLock(terminalBridgeSendLockPath(storeDirFromOptions(options), terminalControl), { timeoutMs: 30000 });
|
|
3012
3646
|
try {
|
|
3013
3647
|
return await runTerminalControlSend({
|
|
@@ -3020,13 +3654,52 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
3020
3654
|
message,
|
|
3021
3655
|
terminalControl,
|
|
3022
3656
|
needsNativeTakeoverBootstrap,
|
|
3023
|
-
terminalSendLockHeld: true
|
|
3657
|
+
terminalSendLockHeld: true,
|
|
3658
|
+
terminalStateLockHeld,
|
|
3659
|
+
recordMessageAfterSend,
|
|
3660
|
+
recordRawAttachmentAfterSend
|
|
3024
3661
|
});
|
|
3025
3662
|
}
|
|
3026
3663
|
finally {
|
|
3027
3664
|
releaseTerminalLock();
|
|
3028
3665
|
}
|
|
3029
3666
|
}
|
|
3667
|
+
if (bridge && !terminalStateLockHeld) {
|
|
3668
|
+
const releaseStateLock = acquireFileLock(`${statePath}.lock`);
|
|
3669
|
+
try {
|
|
3670
|
+
const currentConversation = loadState(statePath);
|
|
3671
|
+
const currentTakeover = isRecord(currentConversation.native_session_takeover)
|
|
3672
|
+
? currentConversation.native_session_takeover
|
|
3673
|
+
: undefined;
|
|
3674
|
+
const currentControl = terminalControlFromTakeover(currentTakeover);
|
|
3675
|
+
if (currentConversation.conversation_id !== nextConversation.conversation_id ||
|
|
3676
|
+
currentConversation.updated_at !== nextConversation.updated_at ||
|
|
3677
|
+
currentConversation.status !== nextConversation.status ||
|
|
3678
|
+
currentConversation.response_rounds_used !== nextConversation.response_rounds_used ||
|
|
3679
|
+
currentControl?.target !== terminalControl.target ||
|
|
3680
|
+
currentControl?.socketPath !== terminalControl.socketPath) {
|
|
3681
|
+
throw new Error("conversation changed while waiting to send to the terminal; refresh status and retry");
|
|
3682
|
+
}
|
|
3683
|
+
return await runTerminalControlSend({
|
|
3684
|
+
options,
|
|
3685
|
+
conversation: currentConversation,
|
|
3686
|
+
nextConversation: currentConversation,
|
|
3687
|
+
statePath,
|
|
3688
|
+
logPath,
|
|
3689
|
+
executor,
|
|
3690
|
+
message,
|
|
3691
|
+
terminalControl,
|
|
3692
|
+
needsNativeTakeoverBootstrap,
|
|
3693
|
+
terminalSendLockHeld: true,
|
|
3694
|
+
terminalStateLockHeld: true,
|
|
3695
|
+
recordMessageAfterSend,
|
|
3696
|
+
recordRawAttachmentAfterSend
|
|
3697
|
+
});
|
|
3698
|
+
}
|
|
3699
|
+
finally {
|
|
3700
|
+
releaseStateLock();
|
|
3701
|
+
}
|
|
3702
|
+
}
|
|
3030
3703
|
const terminalBridge = createTerminalAgentBridge(options);
|
|
3031
3704
|
const bridgeStartedAt = new Date().toISOString();
|
|
3032
3705
|
const agentTimeoutMinutes = Number(options.agentTimeoutMinutes ?? DEFAULT_AGENT_TIMEOUT_MINUTES);
|
|
@@ -3046,6 +3719,10 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
3046
3719
|
messageId: message.id
|
|
3047
3720
|
};
|
|
3048
3721
|
let preSendScreenFingerprint;
|
|
3722
|
+
let claudeTranscriptAnchor;
|
|
3723
|
+
const claudeHome = executor.kind === "claude"
|
|
3724
|
+
? path.resolve(expandHome(options.claudeHome) ?? defaultClaudeHome())
|
|
3725
|
+
: undefined;
|
|
3049
3726
|
if (bridge) {
|
|
3050
3727
|
try {
|
|
3051
3728
|
const status = await terminalBridge.status(executor.kind, terminalControl, {
|
|
@@ -3055,7 +3732,21 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
3055
3732
|
if (executor.kind === "claude") {
|
|
3056
3733
|
assertSafeClaudeTerminalSend(status);
|
|
3057
3734
|
}
|
|
3058
|
-
preSendScreenFingerprint =
|
|
3735
|
+
preSendScreenFingerprint = stringValue(status.screen.digest) ??
|
|
3736
|
+
terminalBridgeScreenFingerprint(status.screen.excerpt);
|
|
3737
|
+
if (executor.kind === "claude") {
|
|
3738
|
+
claudeTranscriptAnchor = captureClaudeTranscriptAnchor({
|
|
3739
|
+
sessionId: preSendRuntime.sessionId,
|
|
3740
|
+
cwd: preSendRuntime.cwd,
|
|
3741
|
+
pid: preSendRuntime.pid,
|
|
3742
|
+
claudeHome,
|
|
3743
|
+
agentRows: loadClaudeAgentRows(options)
|
|
3744
|
+
});
|
|
3745
|
+
if (!claudeTranscriptAnchor &&
|
|
3746
|
+
!stringValue(options.claudeHookStoreDir)) {
|
|
3747
|
+
throw new Error("the hook-free completion monitor could not bind an owner-private Claude transcript boundary");
|
|
3748
|
+
}
|
|
3749
|
+
}
|
|
3059
3750
|
}
|
|
3060
3751
|
catch (error) {
|
|
3061
3752
|
if (executor.kind === "claude") {
|
|
@@ -3087,7 +3778,9 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
3087
3778
|
? withClaudeHookLeaseState(nextConversation, claudeHookLeaseState)
|
|
3088
3779
|
: nextConversation;
|
|
3089
3780
|
try {
|
|
3090
|
-
await terminalBridge.send(executor.kind, terminalControl, terminalPayload
|
|
3781
|
+
await terminalBridge.send(executor.kind, terminalControl, terminalPayload, {
|
|
3782
|
+
runtime: preSendRuntime
|
|
3783
|
+
});
|
|
3091
3784
|
}
|
|
3092
3785
|
catch (error) {
|
|
3093
3786
|
if (claudeHookLeaseState) {
|
|
@@ -3095,39 +3788,17 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
3095
3788
|
}
|
|
3096
3789
|
throw error;
|
|
3097
3790
|
}
|
|
3098
|
-
const supersededConversationIds = bridge
|
|
3099
|
-
? supersedeTerminalBridgeConversations({
|
|
3100
|
-
storeDir: storeDirFromOptions(options),
|
|
3101
|
-
terminalControl,
|
|
3102
|
-
replacementConversationId: conversation.conversation_id
|
|
3103
|
-
})
|
|
3104
|
-
: [];
|
|
3105
|
-
appendEvent(logPath, {
|
|
3106
|
-
ts: new Date().toISOString(),
|
|
3107
|
-
conversation_id: conversation.conversation_id,
|
|
3108
|
-
event: "terminal_message_send",
|
|
3109
|
-
executor,
|
|
3110
|
-
terminal_control: terminalControl,
|
|
3111
|
-
message: textSummary(message.body),
|
|
3112
|
-
payload: textSummary(terminalPayload),
|
|
3113
|
-
superseded_conversation_ids: supersededConversationIds
|
|
3114
|
-
});
|
|
3115
|
-
runtimeLog("info", "terminal_message_send", {
|
|
3116
|
-
conversation_id: conversation.conversation_id,
|
|
3117
|
-
agent: executor.kind,
|
|
3118
|
-
terminal_target: terminalControl.target,
|
|
3119
|
-
message: textSummary(message.body),
|
|
3120
|
-
payload: textSummary(terminalPayload),
|
|
3121
|
-
superseded_conversation_ids: supersededConversationIds
|
|
3122
|
-
});
|
|
3123
3791
|
const bridgeConversation = bridge
|
|
3124
3792
|
? withTerminalBridgeState({
|
|
3125
3793
|
conversation: conversationWithHookLease,
|
|
3126
3794
|
message,
|
|
3795
|
+
requestText: terminalPayload,
|
|
3127
3796
|
startedAt: bridgeStartedAt,
|
|
3128
3797
|
agentTimeoutMinutes,
|
|
3129
3798
|
agentHardTimeoutMinutes,
|
|
3130
|
-
preSendScreenFingerprint
|
|
3799
|
+
preSendScreenFingerprint,
|
|
3800
|
+
claudeTranscriptAnchor,
|
|
3801
|
+
claudeHome
|
|
3131
3802
|
})
|
|
3132
3803
|
: nextConversation;
|
|
3133
3804
|
const deliveredConversation = markTakeoverBootstrapped({
|
|
@@ -3136,9 +3807,68 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
3136
3807
|
logPath,
|
|
3137
3808
|
executor,
|
|
3138
3809
|
native: needsNativeTakeoverBootstrap && !bridge,
|
|
3139
|
-
fork: false
|
|
3810
|
+
fork: false,
|
|
3811
|
+
stateLockHeld: terminalStateLockHeld
|
|
3812
|
+
});
|
|
3813
|
+
saveState(statePath, deliveredConversation);
|
|
3814
|
+
const supersededConversationIds = bridge
|
|
3815
|
+
? supersedeTerminalBridgeConversations({
|
|
3816
|
+
storeDir: storeDirFromOptions(options),
|
|
3817
|
+
terminalControl,
|
|
3818
|
+
replacementConversationId: conversation.conversation_id
|
|
3819
|
+
})
|
|
3820
|
+
: [];
|
|
3821
|
+
if (recordRawAttachmentAfterSend) {
|
|
3822
|
+
const sourceConversationId = stringValue(isRecord(conversation.native_session_takeover)
|
|
3823
|
+
? conversation.native_session_takeover.native_session_id
|
|
3824
|
+
: undefined);
|
|
3825
|
+
appendEvent(logPath, {
|
|
3826
|
+
ts: new Date().toISOString(),
|
|
3827
|
+
conversation_id: conversation.conversation_id,
|
|
3828
|
+
event: "raw_terminal_session_attached",
|
|
3829
|
+
source_conversation_id: sourceConversationId,
|
|
3830
|
+
agent: executor.kind,
|
|
3831
|
+
terminal_control: terminalControl,
|
|
3832
|
+
executor
|
|
3833
|
+
});
|
|
3834
|
+
runtimeLog("info", "raw_terminal_session_attached", {
|
|
3835
|
+
conversation_id: conversation.conversation_id,
|
|
3836
|
+
source_conversation_id: sourceConversationId,
|
|
3837
|
+
terminal_target: terminalControl.target,
|
|
3838
|
+
state_path: statePath,
|
|
3839
|
+
event_log_path: logPath
|
|
3840
|
+
});
|
|
3841
|
+
}
|
|
3842
|
+
if (recordMessageAfterSend) {
|
|
3843
|
+
appendEvent(logPath, messageEvent(message));
|
|
3844
|
+
runtimeLog("info", "message_created", {
|
|
3845
|
+
conversation_id: conversation.conversation_id,
|
|
3846
|
+
agent: executor.kind,
|
|
3847
|
+
executor_session: executor.session,
|
|
3848
|
+
message_type: message.type,
|
|
3849
|
+
state_path: statePath,
|
|
3850
|
+
event_log_path: logPath,
|
|
3851
|
+
message: textSummary(message.body)
|
|
3852
|
+
});
|
|
3853
|
+
}
|
|
3854
|
+
appendEvent(logPath, {
|
|
3855
|
+
ts: new Date().toISOString(),
|
|
3856
|
+
conversation_id: conversation.conversation_id,
|
|
3857
|
+
event: "terminal_message_send",
|
|
3858
|
+
executor,
|
|
3859
|
+
terminal_control: terminalControl,
|
|
3860
|
+
message: textSummary(message.body),
|
|
3861
|
+
payload: textSummary(terminalPayload),
|
|
3862
|
+
superseded_conversation_ids: supersededConversationIds
|
|
3863
|
+
});
|
|
3864
|
+
runtimeLog("info", "terminal_message_send", {
|
|
3865
|
+
conversation_id: conversation.conversation_id,
|
|
3866
|
+
agent: executor.kind,
|
|
3867
|
+
terminal_target: terminalControl.target,
|
|
3868
|
+
message: textSummary(message.body),
|
|
3869
|
+
payload: textSummary(terminalPayload),
|
|
3870
|
+
superseded_conversation_ids: supersededConversationIds
|
|
3140
3871
|
});
|
|
3141
|
-
saveState(statePath, deliveredConversation);
|
|
3142
3872
|
const bridgeMonitor = bridge
|
|
3143
3873
|
? startTerminalBridgeMonitorForConversation({
|
|
3144
3874
|
conversation: deliveredConversation,
|
|
@@ -3182,7 +3912,7 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
3182
3912
|
});
|
|
3183
3913
|
}
|
|
3184
3914
|
function terminalSubmissionPayload(payload) {
|
|
3185
|
-
return payload.
|
|
3915
|
+
return payload.trimEnd();
|
|
3186
3916
|
}
|
|
3187
3917
|
function createManagedTerminalConversationFromRawId({ options, conversationId, agent, pid, messageBody, terminalControl }) {
|
|
3188
3918
|
const workspace = terminalControl.currentPath ?? process.cwd();
|
|
@@ -3246,23 +3976,6 @@ function createManagedTerminalConversationFromRawId({ options, conversationId, a
|
|
|
3246
3976
|
terminal_bridge: true
|
|
3247
3977
|
}
|
|
3248
3978
|
}, paths);
|
|
3249
|
-
saveState(paths.statePath, attachedConversation);
|
|
3250
|
-
appendEvent(paths.logPath, {
|
|
3251
|
-
ts: now.toISOString(),
|
|
3252
|
-
conversation_id: attachedConversation.conversation_id,
|
|
3253
|
-
event: "raw_terminal_session_attached",
|
|
3254
|
-
source_conversation_id: conversationId,
|
|
3255
|
-
agent,
|
|
3256
|
-
terminal_control: terminalControl,
|
|
3257
|
-
executor
|
|
3258
|
-
});
|
|
3259
|
-
runtimeLog("info", "raw_terminal_session_attached", {
|
|
3260
|
-
conversation_id: attachedConversation.conversation_id,
|
|
3261
|
-
source_conversation_id: conversationId,
|
|
3262
|
-
terminal_target: terminalControl.target,
|
|
3263
|
-
state_path: paths.statePath,
|
|
3264
|
-
event_log_path: paths.logPath
|
|
3265
|
-
});
|
|
3266
3979
|
const message = createMessage({
|
|
3267
3980
|
conversation: attachedConversation,
|
|
3268
3981
|
from: "openclaw",
|
|
@@ -3276,18 +3989,6 @@ function createManagedTerminalConversationFromRawId({ options, conversationId, a
|
|
|
3276
3989
|
}
|
|
3277
3990
|
});
|
|
3278
3991
|
const nextConversation = applyMessageToConversation(attachedConversation, message);
|
|
3279
|
-
saveState(paths.statePath, nextConversation);
|
|
3280
|
-
appendEvent(paths.logPath, messageEvent(message));
|
|
3281
|
-
runtimeLog("info", "message_created", {
|
|
3282
|
-
conversation_id: nextConversation.conversation_id,
|
|
3283
|
-
source_conversation_id: conversationId,
|
|
3284
|
-
agent: executor.kind,
|
|
3285
|
-
executor_session: executor.session,
|
|
3286
|
-
message_type: message.type,
|
|
3287
|
-
state_path: paths.statePath,
|
|
3288
|
-
event_log_path: paths.logPath,
|
|
3289
|
-
message: textSummary(messageBody)
|
|
3290
|
-
});
|
|
3291
3992
|
return {
|
|
3292
3993
|
conversation: attachedConversation,
|
|
3293
3994
|
nextConversation,
|
|
@@ -3391,7 +4092,7 @@ function runNativeCodexResumeSend({ options, conversation, nextConversation, sta
|
|
|
3391
4092
|
});
|
|
3392
4093
|
if (options.background) {
|
|
3393
4094
|
const outputPath = path.join(path.dirname(logPath), `${executor.kind}-native-resume-output.log`);
|
|
3394
|
-
const outputFd =
|
|
4095
|
+
const outputFd = openPrivateAppendFile(outputPath);
|
|
3395
4096
|
const child = spawn(codexPath, codexArgs, {
|
|
3396
4097
|
detached: true,
|
|
3397
4098
|
stdio: ["ignore", outputFd, outputFd],
|
|
@@ -3599,68 +4300,100 @@ function buildAgentSendPayload({ conversation, executor, message, includeNativeT
|
|
|
3599
4300
|
function forkTakeoverSummaryText(forkTakeover) {
|
|
3600
4301
|
return String(isRecord(forkTakeover) ? forkTakeover.summary ?? "" : "").trim();
|
|
3601
4302
|
}
|
|
3602
|
-
function markTakeoverBootstrapped({ conversation, statePath, logPath, executor, native, fork }) {
|
|
4303
|
+
function markTakeoverBootstrapped({ conversation, statePath, logPath, executor, native, fork, stateLockHeld = false }) {
|
|
3603
4304
|
let nextConversation = conversation;
|
|
3604
4305
|
if (native) {
|
|
3605
|
-
nextConversation = markNativeSessionBootstrapped({
|
|
4306
|
+
nextConversation = markNativeSessionBootstrapped({
|
|
4307
|
+
conversation: nextConversation,
|
|
4308
|
+
statePath,
|
|
4309
|
+
logPath,
|
|
4310
|
+
executor,
|
|
4311
|
+
stateLockHeld
|
|
4312
|
+
});
|
|
3606
4313
|
}
|
|
3607
4314
|
if (fork) {
|
|
3608
|
-
nextConversation = markForkSessionBootstrapped({
|
|
4315
|
+
nextConversation = markForkSessionBootstrapped({
|
|
4316
|
+
conversation: nextConversation,
|
|
4317
|
+
statePath,
|
|
4318
|
+
logPath,
|
|
4319
|
+
executor,
|
|
4320
|
+
stateLockHeld
|
|
4321
|
+
});
|
|
3609
4322
|
}
|
|
3610
4323
|
return nextConversation;
|
|
3611
4324
|
}
|
|
3612
|
-
function markNativeSessionBootstrapped({ conversation, statePath, logPath, executor }) {
|
|
3613
|
-
const nativeTakeover = isRecord(conversation.native_session_takeover)
|
|
3614
|
-
? conversation.native_session_takeover
|
|
3615
|
-
: {};
|
|
4325
|
+
function markNativeSessionBootstrapped({ conversation, statePath, logPath, executor, stateLockHeld = false }) {
|
|
3616
4326
|
const now = new Date().toISOString();
|
|
3617
|
-
const
|
|
3618
|
-
|
|
3619
|
-
|
|
3620
|
-
|
|
3621
|
-
|
|
3622
|
-
|
|
3623
|
-
|
|
3624
|
-
|
|
3625
|
-
|
|
3626
|
-
|
|
4327
|
+
const releaseLock = stateLockHeld
|
|
4328
|
+
? undefined
|
|
4329
|
+
: acquireFileLock(`${statePath}.lock`);
|
|
4330
|
+
let nextConversation = conversation;
|
|
4331
|
+
try {
|
|
4332
|
+
const current = stateLockHeld ? conversation : loadState(statePath);
|
|
4333
|
+
const nativeTakeover = isRecord(current.native_session_takeover)
|
|
4334
|
+
? current.native_session_takeover
|
|
4335
|
+
: {};
|
|
4336
|
+
nextConversation = {
|
|
4337
|
+
...current,
|
|
4338
|
+
native_session_takeover: {
|
|
4339
|
+
...nativeTakeover,
|
|
4340
|
+
needs_bootstrap: false,
|
|
4341
|
+
bootstrapped_at: now
|
|
4342
|
+
},
|
|
4343
|
+
updated_at: now
|
|
4344
|
+
};
|
|
4345
|
+
saveState(statePath, nextConversation);
|
|
4346
|
+
}
|
|
4347
|
+
finally {
|
|
4348
|
+
releaseLock?.();
|
|
4349
|
+
}
|
|
3627
4350
|
appendEvent(logPath, {
|
|
3628
4351
|
ts: now,
|
|
3629
|
-
conversation_id:
|
|
4352
|
+
conversation_id: nextConversation.conversation_id,
|
|
3630
4353
|
event: "native_session_bootstrapped",
|
|
3631
4354
|
executor
|
|
3632
4355
|
});
|
|
3633
4356
|
runtimeLog("info", "native_session_bootstrapped", {
|
|
3634
|
-
conversation_id:
|
|
4357
|
+
conversation_id: nextConversation.conversation_id,
|
|
3635
4358
|
agent: executor.kind,
|
|
3636
4359
|
executor_session: executor.session,
|
|
3637
4360
|
state_path: statePath
|
|
3638
4361
|
});
|
|
3639
4362
|
return nextConversation;
|
|
3640
4363
|
}
|
|
3641
|
-
function markForkSessionBootstrapped({ conversation, statePath, logPath, executor }) {
|
|
3642
|
-
const forkTakeover = isRecord(conversation.fork_context_takeover)
|
|
3643
|
-
? conversation.fork_context_takeover
|
|
3644
|
-
: {};
|
|
4364
|
+
function markForkSessionBootstrapped({ conversation, statePath, logPath, executor, stateLockHeld = false }) {
|
|
3645
4365
|
const now = new Date().toISOString();
|
|
3646
|
-
const
|
|
3647
|
-
|
|
3648
|
-
|
|
3649
|
-
|
|
3650
|
-
|
|
3651
|
-
|
|
3652
|
-
|
|
3653
|
-
|
|
3654
|
-
|
|
3655
|
-
|
|
4366
|
+
const releaseLock = stateLockHeld
|
|
4367
|
+
? undefined
|
|
4368
|
+
: acquireFileLock(`${statePath}.lock`);
|
|
4369
|
+
let nextConversation = conversation;
|
|
4370
|
+
try {
|
|
4371
|
+
const current = stateLockHeld ? conversation : loadState(statePath);
|
|
4372
|
+
const forkTakeover = isRecord(current.fork_context_takeover)
|
|
4373
|
+
? current.fork_context_takeover
|
|
4374
|
+
: {};
|
|
4375
|
+
nextConversation = {
|
|
4376
|
+
...current,
|
|
4377
|
+
fork_context_takeover: {
|
|
4378
|
+
...forkTakeover,
|
|
4379
|
+
needs_bootstrap: false,
|
|
4380
|
+
bootstrapped_at: now
|
|
4381
|
+
},
|
|
4382
|
+
updated_at: now
|
|
4383
|
+
};
|
|
4384
|
+
saveState(statePath, nextConversation);
|
|
4385
|
+
}
|
|
4386
|
+
finally {
|
|
4387
|
+
releaseLock?.();
|
|
4388
|
+
}
|
|
3656
4389
|
appendEvent(logPath, {
|
|
3657
4390
|
ts: now,
|
|
3658
|
-
conversation_id:
|
|
4391
|
+
conversation_id: nextConversation.conversation_id,
|
|
3659
4392
|
event: "fork_session_bootstrapped",
|
|
3660
4393
|
executor
|
|
3661
4394
|
});
|
|
3662
4395
|
runtimeLog("info", "fork_session_bootstrapped", {
|
|
3663
|
-
conversation_id:
|
|
4396
|
+
conversation_id: nextConversation.conversation_id,
|
|
3664
4397
|
agent: executor.kind,
|
|
3665
4398
|
executor_session: executor.session,
|
|
3666
4399
|
state_path: statePath
|
|
@@ -3747,124 +4480,576 @@ function markConversationNeedsRecovery({ conversation, statePath, logPath, execu
|
|
|
3747
4480
|
};
|
|
3748
4481
|
}
|
|
3749
4482
|
async function runRenew(options) {
|
|
3750
|
-
const
|
|
4483
|
+
const loaded = loadConversationFromOptions(options);
|
|
4484
|
+
const { statePath, logPath } = loaded;
|
|
4485
|
+
const conversation = await migrateLegacyTerminalAgentIdentity({
|
|
4486
|
+
...loaded,
|
|
4487
|
+
options
|
|
4488
|
+
});
|
|
3751
4489
|
if (conversation.status === "closed") {
|
|
3752
4490
|
throw new Error(`cannot renew ${conversation.conversation_id}; conversation is closed`);
|
|
3753
4491
|
}
|
|
3754
|
-
if (conversation.status !== "stalled") {
|
|
3755
|
-
throw new Error(`cannot renew ${conversation.conversation_id}; conversation is ${conversation.status}, not stalled`);
|
|
4492
|
+
if (conversation.status !== "stalled") {
|
|
4493
|
+
throw new Error(`cannot renew ${conversation.conversation_id}; conversation is ${conversation.status}, not stalled`);
|
|
4494
|
+
}
|
|
4495
|
+
const nativeTakeover = isRecord(conversation.native_session_takeover)
|
|
4496
|
+
? conversation.native_session_takeover
|
|
4497
|
+
: undefined;
|
|
4498
|
+
const terminalControl = terminalControlFromTakeover(nativeTakeover);
|
|
4499
|
+
if (!terminalControl || nativeTakeover?.["terminal_bridge"] !== true) {
|
|
4500
|
+
throw new Error(`cannot renew ${conversation.conversation_id}; conversation is not a terminal bridge task`);
|
|
4501
|
+
}
|
|
4502
|
+
const panes = await createTerminalControlProvider(options).listPanes();
|
|
4503
|
+
const terminalExists = panes.some((pane) => pane.target === terminalControl.target &&
|
|
4504
|
+
(terminalControl.socketPath === undefined || pane.socketPath === terminalControl.socketPath));
|
|
4505
|
+
if (!terminalExists) {
|
|
4506
|
+
throw new Error(`cannot renew ${conversation.conversation_id}; terminal ${terminalControl.target} is no longer available`);
|
|
4507
|
+
}
|
|
4508
|
+
const expectedMessageId = stringValue(nativeTakeover?.terminal_bridge_message_id);
|
|
4509
|
+
const expectedStartedAt = stringValue(nativeTakeover?.terminal_bridge_started_at);
|
|
4510
|
+
let renewed = conversation;
|
|
4511
|
+
let renewedTerminalControl = terminalControl;
|
|
4512
|
+
let inactivityTimeoutMinutes = 0;
|
|
4513
|
+
let hardTimeoutMinutes = 0;
|
|
4514
|
+
const releaseStateLock = acquireFileLock(`${statePath}.lock`);
|
|
4515
|
+
try {
|
|
4516
|
+
const current = loadState(statePath);
|
|
4517
|
+
if (current.status !== "stalled") {
|
|
4518
|
+
throw new Error(`cannot renew ${current.conversation_id}; conversation is ${current.status}, not stalled`);
|
|
4519
|
+
}
|
|
4520
|
+
const currentTakeover = isRecord(current.native_session_takeover)
|
|
4521
|
+
? current.native_session_takeover
|
|
4522
|
+
: undefined;
|
|
4523
|
+
const currentControl = terminalControlFromTakeover(currentTakeover);
|
|
4524
|
+
if (!currentControl || currentTakeover?.terminal_bridge !== true) {
|
|
4525
|
+
throw new Error(`cannot renew ${current.conversation_id}; conversation is not a terminal bridge task`);
|
|
4526
|
+
}
|
|
4527
|
+
if (current.conversation_id !== conversation.conversation_id ||
|
|
4528
|
+
currentControl.target !== terminalControl.target ||
|
|
4529
|
+
currentControl.socketPath !== terminalControl.socketPath ||
|
|
4530
|
+
currentControl.panePid !== terminalControl.panePid ||
|
|
4531
|
+
stringValue(currentTakeover.terminal_bridge_message_id) !== expectedMessageId ||
|
|
4532
|
+
stringValue(currentTakeover.terminal_bridge_started_at) !== expectedStartedAt) {
|
|
4533
|
+
throw new Error("conversation changed while waiting to renew; refresh status and retry");
|
|
4534
|
+
}
|
|
4535
|
+
renewedTerminalControl = currentControl;
|
|
4536
|
+
inactivityTimeoutMinutes = positiveMinutes(options.minutes ??
|
|
4537
|
+
options.agentTimeoutMinutes ??
|
|
4538
|
+
currentTakeover.terminal_bridge_inactivity_timeout_minutes ??
|
|
4539
|
+
DEFAULT_AGENT_TIMEOUT_MINUTES, "--minutes");
|
|
4540
|
+
hardTimeoutMinutes = positiveMinutes(currentTakeover.terminal_bridge_hard_timeout_minutes ??
|
|
4541
|
+
DEFAULT_AGENT_HARD_TIMEOUT_MINUTES, "--agent-hard-timeout-minutes");
|
|
4542
|
+
const startedAt = stringValue(currentTakeover.terminal_bridge_started_at);
|
|
4543
|
+
const startedAtMs = startedAt ? Date.parse(startedAt) : NaN;
|
|
4544
|
+
if (Number.isFinite(startedAtMs) &&
|
|
4545
|
+
Date.now() - startedAtMs >= hardTimeoutMinutes * 60 * 1000) {
|
|
4546
|
+
throw new Error(`cannot renew ${current.conversation_id}; terminal bridge hard lifetime of ${hardTimeoutMinutes} minutes has elapsed`);
|
|
4547
|
+
}
|
|
4548
|
+
const now = new Date().toISOString();
|
|
4549
|
+
const currentMessageId = stringValue(currentTakeover.terminal_bridge_message_id);
|
|
4550
|
+
const hardDeadline = deadlineAt(startedAt ?? now, hardTimeoutMinutes) ??
|
|
4551
|
+
new Date(Date.now() + hardTimeoutMinutes * 60 * 1000).toISOString();
|
|
4552
|
+
const inactivityDeadline = deadlineAt(now, inactivityTimeoutMinutes) ??
|
|
4553
|
+
new Date(Date.now() + inactivityTimeoutMinutes * 60 * 1000).toISOString();
|
|
4554
|
+
const renewedLease = executorForConversation(current).kind === "claude" && currentMessageId
|
|
4555
|
+
? activateClaudeHookLease({
|
|
4556
|
+
options,
|
|
4557
|
+
conversation: current,
|
|
4558
|
+
message: { id: currentMessageId },
|
|
4559
|
+
terminalControl: currentControl,
|
|
4560
|
+
expiresAt: new Date(Math.min(Date.parse(hardDeadline), Date.parse(inactivityDeadline))).toISOString()
|
|
4561
|
+
})
|
|
4562
|
+
: undefined;
|
|
4563
|
+
const renewedBase = renewedLease
|
|
4564
|
+
? withClaudeHookLeaseState(current, renewedLease)
|
|
4565
|
+
: current;
|
|
4566
|
+
const renewedNativeTakeover = isRecord(renewedBase.native_session_takeover)
|
|
4567
|
+
? renewedBase.native_session_takeover
|
|
4568
|
+
: currentTakeover;
|
|
4569
|
+
renewed = {
|
|
4570
|
+
...renewedBase,
|
|
4571
|
+
status: "waiting_for_agent",
|
|
4572
|
+
native_session_takeover: {
|
|
4573
|
+
...renewedNativeTakeover,
|
|
4574
|
+
terminal_bridge_monitor_lock_version: TERMINAL_BRIDGE_MONITOR_LOCK_VERSION,
|
|
4575
|
+
terminal_bridge_monitor_started_at: now,
|
|
4576
|
+
terminal_bridge_last_activity_at: now,
|
|
4577
|
+
terminal_bridge_inactivity_timeout_minutes: inactivityTimeoutMinutes,
|
|
4578
|
+
terminal_bridge_hard_timeout_minutes: hardTimeoutMinutes,
|
|
4579
|
+
terminal_bridge_inactivity_deadline_at: inactivityDeadline,
|
|
4580
|
+
terminal_bridge_hard_deadline_at: hardDeadline,
|
|
4581
|
+
terminal_bridge_renewed_at: now
|
|
4582
|
+
},
|
|
4583
|
+
updated_at: now
|
|
4584
|
+
};
|
|
4585
|
+
Reflect.deleteProperty(renewed, "stalled_at");
|
|
4586
|
+
Reflect.deleteProperty(renewed, "stalled_reason");
|
|
4587
|
+
Reflect.deleteProperty(renewed, "stalled_notification_sent_at");
|
|
4588
|
+
Reflect.deleteProperty(renewed, "stalled_notification_message_id");
|
|
4589
|
+
saveState(statePath, renewed);
|
|
4590
|
+
appendEvent(logPath, {
|
|
4591
|
+
ts: now,
|
|
4592
|
+
conversation_id: current.conversation_id,
|
|
4593
|
+
event: "terminal_bridge_renewed",
|
|
4594
|
+
previous_status: current.status,
|
|
4595
|
+
terminal_control: currentControl,
|
|
4596
|
+
agent_timeout_minutes: inactivityTimeoutMinutes,
|
|
4597
|
+
agent_hard_timeout_minutes: hardTimeoutMinutes,
|
|
4598
|
+
last_activity_at: now
|
|
4599
|
+
});
|
|
4600
|
+
runtimeLog("info", "terminal_bridge_renewed", {
|
|
4601
|
+
conversation_id: current.conversation_id,
|
|
4602
|
+
terminal_target: currentControl.target,
|
|
4603
|
+
agent_timeout_minutes: inactivityTimeoutMinutes,
|
|
4604
|
+
agent_hard_timeout_minutes: hardTimeoutMinutes
|
|
4605
|
+
});
|
|
4606
|
+
}
|
|
4607
|
+
finally {
|
|
4608
|
+
releaseStateLock();
|
|
4609
|
+
}
|
|
4610
|
+
const monitor = startTerminalBridgeMonitorForConversation({
|
|
4611
|
+
conversation: renewed,
|
|
4612
|
+
statePath,
|
|
4613
|
+
logPath,
|
|
4614
|
+
options: {
|
|
4615
|
+
...options,
|
|
4616
|
+
agentTimeoutMinutes: inactivityTimeoutMinutes,
|
|
4617
|
+
agentHardTimeoutMinutes: hardTimeoutMinutes
|
|
4618
|
+
}
|
|
4619
|
+
});
|
|
4620
|
+
if (monitor) {
|
|
4621
|
+
appendEvent(logPath, {
|
|
4622
|
+
ts: new Date().toISOString(),
|
|
4623
|
+
conversation_id: renewed.conversation_id,
|
|
4624
|
+
event: "terminal_bridge_monitor_launch",
|
|
4625
|
+
pid: monitor.pid ?? null,
|
|
4626
|
+
terminal_control: renewedTerminalControl,
|
|
4627
|
+
reason: "renewal",
|
|
4628
|
+
agent_timeout_minutes: inactivityTimeoutMinutes,
|
|
4629
|
+
agent_hard_timeout_minutes: hardTimeoutMinutes
|
|
4630
|
+
});
|
|
4631
|
+
}
|
|
4632
|
+
printJson({
|
|
4633
|
+
conversation: renewed,
|
|
4634
|
+
renewed: true,
|
|
4635
|
+
terminal_control: renewedTerminalControl,
|
|
4636
|
+
agent_timeout_minutes: inactivityTimeoutMinutes,
|
|
4637
|
+
agent_hard_timeout_minutes: hardTimeoutMinutes,
|
|
4638
|
+
monitor_pid: monitor?.pid ?? null
|
|
4639
|
+
});
|
|
4640
|
+
}
|
|
4641
|
+
async function runReconcileMonitors(options) {
|
|
4642
|
+
const storeDir = storeDirFromOptions(options);
|
|
4643
|
+
const conversations = listConversations(storeDir);
|
|
4644
|
+
const items = [];
|
|
4645
|
+
let ignored = 0;
|
|
4646
|
+
let launched = 0;
|
|
4647
|
+
let alreadyRunning = 0;
|
|
4648
|
+
let skipped = 0;
|
|
4649
|
+
let errors = 0;
|
|
4650
|
+
for (const listedConversation of conversations) {
|
|
4651
|
+
const statePath = expandHome(stringValue(listedConversation.state_path) ??
|
|
4652
|
+
statePathForConversationId(listedConversation.conversation_id, storeDir));
|
|
4653
|
+
const logPath = expandHome(stringValue(listedConversation.event_log_path) ??
|
|
4654
|
+
logPathForStatePath(statePath));
|
|
4655
|
+
try {
|
|
4656
|
+
const callbackRecovery = prepareCallbackDeliveryReconciliation({
|
|
4657
|
+
statePath,
|
|
4658
|
+
logPath,
|
|
4659
|
+
delayMs: options.callbackRetryDelayMs
|
|
4660
|
+
});
|
|
4661
|
+
if (callbackRecovery.handled) {
|
|
4662
|
+
if (callbackRecovery.status === "launched") {
|
|
4663
|
+
launched += 1;
|
|
4664
|
+
}
|
|
4665
|
+
else if (callbackRecovery.status === "already_running") {
|
|
4666
|
+
alreadyRunning += 1;
|
|
4667
|
+
}
|
|
4668
|
+
else {
|
|
4669
|
+
skipped += 1;
|
|
4670
|
+
}
|
|
4671
|
+
items.push({
|
|
4672
|
+
conversation_id: callbackRecovery.conversationId,
|
|
4673
|
+
status: callbackRecovery.status,
|
|
4674
|
+
reason: callbackRecovery.reason,
|
|
4675
|
+
...(callbackRecovery.monitorPid === undefined
|
|
4676
|
+
? {}
|
|
4677
|
+
: { monitor_pid: callbackRecovery.monitorPid })
|
|
4678
|
+
});
|
|
4679
|
+
continue;
|
|
4680
|
+
}
|
|
4681
|
+
const listedNativeTakeover = isRecord(listedConversation.native_session_takeover)
|
|
4682
|
+
? listedConversation.native_session_takeover
|
|
4683
|
+
: undefined;
|
|
4684
|
+
if (listedNativeTakeover?.terminal_bridge !== true) {
|
|
4685
|
+
ignored += 1;
|
|
4686
|
+
continue;
|
|
4687
|
+
}
|
|
4688
|
+
const initialConversation = await migrateLegacyTerminalAgentIdentity({
|
|
4689
|
+
conversation: loadState(statePath),
|
|
4690
|
+
statePath,
|
|
4691
|
+
logPath,
|
|
4692
|
+
options
|
|
4693
|
+
});
|
|
4694
|
+
const initialEligibility = terminalBridgeReconciliationEligibility(initialConversation);
|
|
4695
|
+
if (!initialEligibility.eligible) {
|
|
4696
|
+
skipped += 1;
|
|
4697
|
+
items.push({
|
|
4698
|
+
conversation_id: initialConversation.conversation_id,
|
|
4699
|
+
status: "skipped",
|
|
4700
|
+
reason: initialEligibility.reason
|
|
4701
|
+
});
|
|
4702
|
+
continue;
|
|
4703
|
+
}
|
|
4704
|
+
const activeOwner = activeTerminalBridgeMonitorOwner(statePath, initialEligibility.terminalMessageId);
|
|
4705
|
+
if (activeOwner) {
|
|
4706
|
+
alreadyRunning += 1;
|
|
4707
|
+
items.push({
|
|
4708
|
+
conversation_id: initialConversation.conversation_id,
|
|
4709
|
+
status: "already_running",
|
|
4710
|
+
reason: "monitor_lock_owner_alive",
|
|
4711
|
+
monitor_owner_pid: activeOwner.ownerPid ?? null
|
|
4712
|
+
});
|
|
4713
|
+
continue;
|
|
4714
|
+
}
|
|
4715
|
+
const monitorLockVersion = Number(initialEligibility.nativeTakeover.terminal_bridge_monitor_lock_version);
|
|
4716
|
+
if (monitorLockVersion !== TERMINAL_BRIDGE_MONITOR_LOCK_VERSION) {
|
|
4717
|
+
if (Number.isFinite(monitorLockVersion)) {
|
|
4718
|
+
skipped += 1;
|
|
4719
|
+
items.push({
|
|
4720
|
+
conversation_id: initialConversation.conversation_id,
|
|
4721
|
+
status: "skipped",
|
|
4722
|
+
reason: "monitor_lock_version_unsupported",
|
|
4723
|
+
monitor_lock_version: monitorLockVersion
|
|
4724
|
+
});
|
|
4725
|
+
continue;
|
|
4726
|
+
}
|
|
4727
|
+
const legacyLaunchPid = latestTerminalBridgeMonitorLaunchPid(logPath);
|
|
4728
|
+
if (legacyLaunchPid === undefined) {
|
|
4729
|
+
skipped += 1;
|
|
4730
|
+
items.push({
|
|
4731
|
+
conversation_id: initialConversation.conversation_id,
|
|
4732
|
+
status: "skipped",
|
|
4733
|
+
reason: "legacy_monitor_ownership_unknown"
|
|
4734
|
+
});
|
|
4735
|
+
continue;
|
|
4736
|
+
}
|
|
4737
|
+
if (isProcessAlive(legacyLaunchPid)) {
|
|
4738
|
+
alreadyRunning += 1;
|
|
4739
|
+
items.push({
|
|
4740
|
+
conversation_id: initialConversation.conversation_id,
|
|
4741
|
+
status: "already_running",
|
|
4742
|
+
reason: "legacy_monitor_launch_pid_alive",
|
|
4743
|
+
monitor_owner_pid: legacyLaunchPid
|
|
4744
|
+
});
|
|
4745
|
+
continue;
|
|
4746
|
+
}
|
|
4747
|
+
}
|
|
4748
|
+
const prepared = prepareTerminalBridgeMonitorReconciliation({
|
|
4749
|
+
statePath,
|
|
4750
|
+
expectedMessageId: initialEligibility.terminalMessageId
|
|
4751
|
+
});
|
|
4752
|
+
if (!prepared.prepared) {
|
|
4753
|
+
if (prepared.alreadyRunning) {
|
|
4754
|
+
alreadyRunning += 1;
|
|
4755
|
+
items.push({
|
|
4756
|
+
conversation_id: initialConversation.conversation_id,
|
|
4757
|
+
status: "already_running",
|
|
4758
|
+
reason: prepared.reason,
|
|
4759
|
+
monitor_owner_pid: prepared.ownerPid ?? null
|
|
4760
|
+
});
|
|
4761
|
+
}
|
|
4762
|
+
else {
|
|
4763
|
+
skipped += 1;
|
|
4764
|
+
items.push({
|
|
4765
|
+
conversation_id: initialConversation.conversation_id,
|
|
4766
|
+
status: "skipped",
|
|
4767
|
+
reason: prepared.reason
|
|
4768
|
+
});
|
|
4769
|
+
}
|
|
4770
|
+
continue;
|
|
4771
|
+
}
|
|
4772
|
+
const monitor = startTerminalBridgeMonitorForConversation({
|
|
4773
|
+
conversation: prepared.conversation,
|
|
4774
|
+
statePath,
|
|
4775
|
+
logPath,
|
|
4776
|
+
options
|
|
4777
|
+
});
|
|
4778
|
+
if (!monitor) {
|
|
4779
|
+
skipped += 1;
|
|
4780
|
+
items.push({
|
|
4781
|
+
conversation_id: prepared.conversation.conversation_id,
|
|
4782
|
+
status: "skipped",
|
|
4783
|
+
reason: "terminal_bridge_monitor_launch_disabled"
|
|
4784
|
+
});
|
|
4785
|
+
continue;
|
|
4786
|
+
}
|
|
4787
|
+
const launchedAt = new Date().toISOString();
|
|
4788
|
+
appendEvent(logPath, {
|
|
4789
|
+
ts: launchedAt,
|
|
4790
|
+
conversation_id: prepared.conversation.conversation_id,
|
|
4791
|
+
event: "terminal_bridge_monitor_launch",
|
|
4792
|
+
pid: monitor.pid ?? null,
|
|
4793
|
+
terminal_control: prepared.terminalControl,
|
|
4794
|
+
reason: "startup_reconciliation",
|
|
4795
|
+
agent_timeout_minutes: prepared.inactivityTimeoutMinutes,
|
|
4796
|
+
agent_hard_timeout_minutes: prepared.hardTimeoutMinutes
|
|
4797
|
+
});
|
|
4798
|
+
runtimeLog("info", "terminal_bridge_monitor_reconciled", {
|
|
4799
|
+
conversation_id: prepared.conversation.conversation_id,
|
|
4800
|
+
monitor_pid: monitor.pid ?? null,
|
|
4801
|
+
terminal_target: prepared.terminalControl.target
|
|
4802
|
+
});
|
|
4803
|
+
launched += 1;
|
|
4804
|
+
items.push({
|
|
4805
|
+
conversation_id: prepared.conversation.conversation_id,
|
|
4806
|
+
status: "launched",
|
|
4807
|
+
reason: "startup_reconciliation",
|
|
4808
|
+
monitor_pid: monitor.pid ?? null
|
|
4809
|
+
});
|
|
4810
|
+
}
|
|
4811
|
+
catch (error) {
|
|
4812
|
+
errors += 1;
|
|
4813
|
+
items.push({
|
|
4814
|
+
conversation_id: listedConversation.conversation_id,
|
|
4815
|
+
status: "error",
|
|
4816
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
4817
|
+
});
|
|
4818
|
+
}
|
|
4819
|
+
}
|
|
4820
|
+
printJson({
|
|
4821
|
+
reconciled: true,
|
|
4822
|
+
store_dir: storeDir,
|
|
4823
|
+
checked: conversations.length,
|
|
4824
|
+
ignored,
|
|
4825
|
+
launched,
|
|
4826
|
+
already_running: alreadyRunning,
|
|
4827
|
+
skipped,
|
|
4828
|
+
errors,
|
|
4829
|
+
items
|
|
4830
|
+
});
|
|
4831
|
+
}
|
|
4832
|
+
function prepareCallbackDeliveryReconciliation({ statePath, logPath, delayMs }) {
|
|
4833
|
+
const releaseStateLock = acquireFileLock(`${statePath}.lock`);
|
|
4834
|
+
try {
|
|
4835
|
+
const conversation = loadState(statePath);
|
|
4836
|
+
const callbackDelivery = isRecord(conversation.callback_delivery)
|
|
4837
|
+
? conversation.callback_delivery
|
|
4838
|
+
: undefined;
|
|
4839
|
+
if (!["callback_pending", "callback_failed"].includes(conversation.status)) {
|
|
4840
|
+
return {
|
|
4841
|
+
handled: false
|
|
4842
|
+
};
|
|
4843
|
+
}
|
|
4844
|
+
const conversationId = stringValue(conversation.conversation_id) ?? "unknown";
|
|
4845
|
+
const attempts = Number(callbackDelivery?.attempts ?? 0);
|
|
4846
|
+
if (!["pending", "failed"].includes(String(callbackDelivery?.status ?? "")) ||
|
|
4847
|
+
!isRecord(callbackDelivery?.message)) {
|
|
4848
|
+
return {
|
|
4849
|
+
handled: true,
|
|
4850
|
+
conversationId,
|
|
4851
|
+
status: "skipped",
|
|
4852
|
+
reason: "callback_delivery_metadata_missing"
|
|
4853
|
+
};
|
|
4854
|
+
}
|
|
4855
|
+
if (!Number.isSafeInteger(attempts) || attempts < 1 ||
|
|
4856
|
+
attempts > CALLBACK_RETRY_DELAYS_MS.length) {
|
|
4857
|
+
return {
|
|
4858
|
+
handled: true,
|
|
4859
|
+
conversationId,
|
|
4860
|
+
status: "skipped",
|
|
4861
|
+
reason: "callback_delivery_retries_exhausted"
|
|
4862
|
+
};
|
|
4863
|
+
}
|
|
4864
|
+
const configuredDelayMs = Number(delayMs);
|
|
4865
|
+
const retryDelayMs = Number.isFinite(configuredDelayMs) &&
|
|
4866
|
+
configuredDelayMs >= 0
|
|
4867
|
+
? configuredDelayMs
|
|
4868
|
+
: CALLBACK_RETRY_DELAYS_MS[Math.max(0, attempts - 1)];
|
|
4869
|
+
const retryMonitor = startCallbackRetryMonitor({
|
|
4870
|
+
statePath,
|
|
4871
|
+
delayMs: retryDelayMs
|
|
4872
|
+
});
|
|
4873
|
+
const launchedAt = new Date().toISOString();
|
|
4874
|
+
const nextAttemptAt = new Date(Date.now() + retryDelayMs).toISOString();
|
|
4875
|
+
const nextConversation = {
|
|
4876
|
+
...conversation,
|
|
4877
|
+
callback_delivery: {
|
|
4878
|
+
...callbackDelivery,
|
|
4879
|
+
retry_monitor_pid: retryMonitor.pid ?? null,
|
|
4880
|
+
next_attempt_at: nextAttemptAt
|
|
4881
|
+
},
|
|
4882
|
+
updated_at: launchedAt
|
|
4883
|
+
};
|
|
4884
|
+
saveState(statePath, nextConversation);
|
|
4885
|
+
appendEvent(logPath, {
|
|
4886
|
+
ts: launchedAt,
|
|
4887
|
+
conversation_id: conversationId,
|
|
4888
|
+
event: "callback_retry_monitor_launched",
|
|
4889
|
+
message_id: callbackDelivery.message.id,
|
|
4890
|
+
pid: retryMonitor.pid ?? null,
|
|
4891
|
+
next_attempt_at: nextAttemptAt,
|
|
4892
|
+
reason: "startup_reconciliation"
|
|
4893
|
+
});
|
|
4894
|
+
return {
|
|
4895
|
+
handled: true,
|
|
4896
|
+
conversationId,
|
|
4897
|
+
status: "launched",
|
|
4898
|
+
reason: "callback_delivery_reconciliation",
|
|
4899
|
+
monitorPid: retryMonitor.pid
|
|
4900
|
+
};
|
|
4901
|
+
}
|
|
4902
|
+
finally {
|
|
4903
|
+
releaseStateLock();
|
|
3756
4904
|
}
|
|
3757
|
-
|
|
4905
|
+
}
|
|
4906
|
+
function terminalBridgeReconciliationEligibility(conversation) {
|
|
4907
|
+
const nativeTakeover = isRecord(conversation?.native_session_takeover)
|
|
3758
4908
|
? conversation.native_session_takeover
|
|
3759
4909
|
: undefined;
|
|
3760
|
-
|
|
3761
|
-
|
|
3762
|
-
throw new Error(`cannot renew ${conversation.conversation_id}; conversation is not a terminal bridge task`);
|
|
4910
|
+
if (nativeTakeover?.terminal_bridge !== true) {
|
|
4911
|
+
return { eligible: false, reason: "not_terminal_bridge" };
|
|
3763
4912
|
}
|
|
3764
|
-
|
|
3765
|
-
|
|
3766
|
-
(terminalControl.socketPath === undefined || pane.socketPath === terminalControl.socketPath));
|
|
3767
|
-
if (!terminalExists) {
|
|
3768
|
-
throw new Error(`cannot renew ${conversation.conversation_id}; terminal ${terminalControl.target} is no longer available`);
|
|
4913
|
+
if (!conversation.gateway_method) {
|
|
4914
|
+
return { eligible: false, reason: "gateway_method_missing" };
|
|
3769
4915
|
}
|
|
3770
|
-
|
|
3771
|
-
|
|
3772
|
-
nativeTakeover?.["terminal_bridge_inactivity_timeout_minutes"] ??
|
|
3773
|
-
DEFAULT_AGENT_TIMEOUT_MINUTES, "--minutes");
|
|
3774
|
-
const hardTimeoutMinutes = positiveMinutes(nativeTakeover?.["terminal_bridge_hard_timeout_minutes"] ??
|
|
3775
|
-
DEFAULT_AGENT_HARD_TIMEOUT_MINUTES, "--agent-hard-timeout-minutes");
|
|
3776
|
-
const startedAt = stringValue(nativeTakeover?.["terminal_bridge_started_at"]);
|
|
3777
|
-
const startedAtMs = startedAt ? Date.parse(startedAt) : NaN;
|
|
3778
|
-
if (Number.isFinite(startedAtMs) && Date.now() - startedAtMs >= hardTimeoutMinutes * 60 * 1000) {
|
|
3779
|
-
throw new Error(`cannot renew ${conversation.conversation_id}; terminal bridge hard lifetime of ${hardTimeoutMinutes} minutes has elapsed`);
|
|
4916
|
+
if (!stringValue(conversation.gateway_session) && !stringValue(conversation.openclaw_session)) {
|
|
4917
|
+
return { eligible: false, reason: "gateway_session_missing" };
|
|
3780
4918
|
}
|
|
3781
|
-
|
|
3782
|
-
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
|
|
3787
|
-
const
|
|
3788
|
-
|
|
3789
|
-
|
|
3790
|
-
|
|
3791
|
-
|
|
3792
|
-
|
|
3793
|
-
|
|
3794
|
-
}
|
|
3795
|
-
|
|
3796
|
-
const
|
|
3797
|
-
|
|
3798
|
-
|
|
3799
|
-
const
|
|
3800
|
-
|
|
3801
|
-
|
|
3802
|
-
|
|
3803
|
-
|
|
3804
|
-
|
|
3805
|
-
|
|
3806
|
-
|
|
3807
|
-
|
|
3808
|
-
|
|
3809
|
-
|
|
3810
|
-
|
|
3811
|
-
|
|
3812
|
-
|
|
3813
|
-
|
|
3814
|
-
|
|
3815
|
-
|
|
4919
|
+
if (!isWaitingForAgent(conversation.status)) {
|
|
4920
|
+
return {
|
|
4921
|
+
eligible: false,
|
|
4922
|
+
reason: `conversation_status_${String(conversation.status ?? "missing")}`
|
|
4923
|
+
};
|
|
4924
|
+
}
|
|
4925
|
+
const terminalMessageId = stringValue(nativeTakeover.terminal_bridge_message_id);
|
|
4926
|
+
const terminalControl = terminalControlFromTakeover(nativeTakeover);
|
|
4927
|
+
if (!terminalMessageId || !terminalControl) {
|
|
4928
|
+
return { eligible: false, reason: "terminal_bridge_identity_missing" };
|
|
4929
|
+
}
|
|
4930
|
+
const runtime = terminalRuntimeIdentityForConversation(conversation, terminalControl);
|
|
4931
|
+
if (!Number.isInteger(runtime.pid) || Number(runtime.pid) <= 0 || !stringValue(runtime.cwd)) {
|
|
4932
|
+
return { eligible: false, reason: "terminal_agent_identity_missing" };
|
|
4933
|
+
}
|
|
4934
|
+
const inactivityTimeoutMinutes = Number(nativeTakeover.terminal_bridge_inactivity_timeout_minutes);
|
|
4935
|
+
const hardTimeoutMinutes = Number(nativeTakeover.terminal_bridge_hard_timeout_minutes);
|
|
4936
|
+
const startedAtMs = validTimestampMs(nativeTakeover.terminal_bridge_started_at);
|
|
4937
|
+
const lastActivityAtMs = validTimestampMs(nativeTakeover.terminal_bridge_last_activity_at);
|
|
4938
|
+
const inactivityDeadlineAtMs = validTimestampMs(nativeTakeover.terminal_bridge_inactivity_deadline_at);
|
|
4939
|
+
const hardDeadlineAtMs = validTimestampMs(nativeTakeover.terminal_bridge_hard_deadline_at);
|
|
4940
|
+
if (!Number.isFinite(inactivityTimeoutMinutes) ||
|
|
4941
|
+
inactivityTimeoutMinutes <= 0 ||
|
|
4942
|
+
!Number.isFinite(hardTimeoutMinutes) ||
|
|
4943
|
+
hardTimeoutMinutes <= 0 ||
|
|
4944
|
+
startedAtMs === undefined ||
|
|
4945
|
+
lastActivityAtMs === undefined ||
|
|
4946
|
+
inactivityDeadlineAtMs === undefined ||
|
|
4947
|
+
hardDeadlineAtMs === undefined) {
|
|
4948
|
+
return { eligible: false, reason: "terminal_bridge_deadline_metadata_missing" };
|
|
4949
|
+
}
|
|
4950
|
+
return {
|
|
4951
|
+
eligible: true,
|
|
4952
|
+
nativeTakeover,
|
|
4953
|
+
terminalMessageId,
|
|
4954
|
+
terminalControl,
|
|
4955
|
+
runtime,
|
|
4956
|
+
inactivityTimeoutMinutes,
|
|
4957
|
+
hardTimeoutMinutes,
|
|
4958
|
+
inactivityDeadlineAtMs,
|
|
4959
|
+
hardDeadlineAtMs
|
|
3816
4960
|
};
|
|
3817
|
-
|
|
3818
|
-
|
|
3819
|
-
|
|
3820
|
-
|
|
3821
|
-
|
|
3822
|
-
|
|
3823
|
-
|
|
3824
|
-
|
|
3825
|
-
|
|
3826
|
-
|
|
3827
|
-
|
|
3828
|
-
|
|
3829
|
-
|
|
3830
|
-
|
|
3831
|
-
});
|
|
3832
|
-
|
|
3833
|
-
|
|
3834
|
-
|
|
3835
|
-
|
|
3836
|
-
|
|
3837
|
-
|
|
3838
|
-
|
|
3839
|
-
|
|
3840
|
-
|
|
3841
|
-
logPath,
|
|
3842
|
-
options: {
|
|
3843
|
-
...options,
|
|
3844
|
-
agentTimeoutMinutes: inactivityTimeoutMinutes,
|
|
3845
|
-
agentHardTimeoutMinutes: hardTimeoutMinutes
|
|
4961
|
+
}
|
|
4962
|
+
function latestTerminalBridgeMonitorLaunchPid(logPath) {
|
|
4963
|
+
let events;
|
|
4964
|
+
try {
|
|
4965
|
+
events = readExistingEvents(logPath);
|
|
4966
|
+
}
|
|
4967
|
+
catch {
|
|
4968
|
+
return undefined;
|
|
4969
|
+
}
|
|
4970
|
+
const launch = [...events].reverse().find((event) => event.event === "terminal_bridge_monitor_launch");
|
|
4971
|
+
const pid = Number(launch?.pid);
|
|
4972
|
+
return Number.isSafeInteger(pid) && pid > 1 ? pid : undefined;
|
|
4973
|
+
}
|
|
4974
|
+
function prepareTerminalBridgeMonitorReconciliation({ statePath, expectedMessageId }) {
|
|
4975
|
+
const releaseStateLock = acquireFileLock(`${statePath}.lock`);
|
|
4976
|
+
try {
|
|
4977
|
+
const conversation = loadState(statePath);
|
|
4978
|
+
const eligibility = terminalBridgeReconciliationEligibility(conversation);
|
|
4979
|
+
if (!eligibility.eligible) {
|
|
4980
|
+
return {
|
|
4981
|
+
prepared: false,
|
|
4982
|
+
alreadyRunning: false,
|
|
4983
|
+
reason: eligibility.reason
|
|
4984
|
+
};
|
|
3846
4985
|
}
|
|
3847
|
-
|
|
3848
|
-
|
|
3849
|
-
|
|
3850
|
-
|
|
3851
|
-
|
|
3852
|
-
|
|
3853
|
-
|
|
3854
|
-
|
|
3855
|
-
|
|
3856
|
-
|
|
3857
|
-
|
|
3858
|
-
|
|
4986
|
+
if (eligibility.terminalMessageId !== expectedMessageId) {
|
|
4987
|
+
return {
|
|
4988
|
+
prepared: false,
|
|
4989
|
+
alreadyRunning: false,
|
|
4990
|
+
reason: "terminal_bridge_task_replaced"
|
|
4991
|
+
};
|
|
4992
|
+
}
|
|
4993
|
+
const activeOwner = activeTerminalBridgeMonitorOwner(statePath, eligibility.terminalMessageId);
|
|
4994
|
+
if (activeOwner) {
|
|
4995
|
+
return {
|
|
4996
|
+
prepared: false,
|
|
4997
|
+
alreadyRunning: true,
|
|
4998
|
+
reason: "monitor_lock_owner_alive",
|
|
4999
|
+
ownerPid: activeOwner.ownerPid
|
|
5000
|
+
};
|
|
5001
|
+
}
|
|
5002
|
+
let renewedLease;
|
|
5003
|
+
const usesClaudeHookLease = executorForConversation(conversation).kind === "claude" &&
|
|
5004
|
+
eligibility.nativeTakeover.claude_hook_mode === "enabled";
|
|
5005
|
+
if (usesClaudeHookLease) {
|
|
5006
|
+
const leaseDeadlineAtMs = Math.min(eligibility.inactivityDeadlineAtMs, eligibility.hardDeadlineAtMs);
|
|
5007
|
+
if (leaseDeadlineAtMs <= Date.now()) {
|
|
5008
|
+
return {
|
|
5009
|
+
prepared: false,
|
|
5010
|
+
alreadyRunning: false,
|
|
5011
|
+
reason: "claude_hook_lease_deadline_elapsed"
|
|
5012
|
+
};
|
|
5013
|
+
}
|
|
5014
|
+
renewedLease = renewClaudeHookLease(conversation, new Date(leaseDeadlineAtMs).toISOString());
|
|
5015
|
+
if (!renewedLease) {
|
|
5016
|
+
return {
|
|
5017
|
+
prepared: false,
|
|
5018
|
+
alreadyRunning: false,
|
|
5019
|
+
reason: "claude_hook_lease_refresh_failed"
|
|
5020
|
+
};
|
|
5021
|
+
}
|
|
5022
|
+
}
|
|
5023
|
+
const nextNativeTakeover = {
|
|
5024
|
+
...eligibility.nativeTakeover,
|
|
5025
|
+
terminal_bridge_monitor_lock_version: TERMINAL_BRIDGE_MONITOR_LOCK_VERSION,
|
|
5026
|
+
...(renewedLease ? { claude_hook_lease_id: renewedLease.id } : {})
|
|
5027
|
+
};
|
|
5028
|
+
const needsSave = eligibility.nativeTakeover.terminal_bridge_monitor_lock_version !==
|
|
5029
|
+
TERMINAL_BRIDGE_MONITOR_LOCK_VERSION ||
|
|
5030
|
+
(renewedLease !== undefined &&
|
|
5031
|
+
eligibility.nativeTakeover.claude_hook_lease_id !== renewedLease.id);
|
|
5032
|
+
const preparedConversation = needsSave
|
|
5033
|
+
? {
|
|
5034
|
+
...conversation,
|
|
5035
|
+
native_session_takeover: nextNativeTakeover,
|
|
5036
|
+
updated_at: new Date().toISOString()
|
|
5037
|
+
}
|
|
5038
|
+
: conversation;
|
|
5039
|
+
if (needsSave) {
|
|
5040
|
+
saveState(statePath, preparedConversation);
|
|
5041
|
+
}
|
|
5042
|
+
return {
|
|
5043
|
+
prepared: true,
|
|
5044
|
+
conversation: preparedConversation,
|
|
5045
|
+
terminalControl: eligibility.terminalControl,
|
|
5046
|
+
inactivityTimeoutMinutes: eligibility.inactivityTimeoutMinutes,
|
|
5047
|
+
hardTimeoutMinutes: eligibility.hardTimeoutMinutes
|
|
5048
|
+
};
|
|
5049
|
+
}
|
|
5050
|
+
finally {
|
|
5051
|
+
releaseStateLock();
|
|
3859
5052
|
}
|
|
3860
|
-
printJson({
|
|
3861
|
-
conversation: renewed,
|
|
3862
|
-
renewed: true,
|
|
3863
|
-
terminal_control: terminalControl,
|
|
3864
|
-
agent_timeout_minutes: inactivityTimeoutMinutes,
|
|
3865
|
-
agent_hard_timeout_minutes: hardTimeoutMinutes,
|
|
3866
|
-
monitor_pid: monitor?.pid ?? null
|
|
3867
|
-
});
|
|
3868
5053
|
}
|
|
3869
5054
|
function positiveMinutes(value, optionName) {
|
|
3870
5055
|
const parsed = Number(value);
|
|
@@ -3886,7 +5071,12 @@ async function runCancel(options) {
|
|
|
3886
5071
|
});
|
|
3887
5072
|
return;
|
|
3888
5073
|
}
|
|
3889
|
-
const
|
|
5074
|
+
const loaded = loadConversationFromOptions(options);
|
|
5075
|
+
const { statePath, logPath } = loaded;
|
|
5076
|
+
const conversation = await migrateLegacyTerminalAgentIdentity({
|
|
5077
|
+
...loaded,
|
|
5078
|
+
options
|
|
5079
|
+
});
|
|
3890
5080
|
if (["closed", "cancelled"].includes(conversation.status)) {
|
|
3891
5081
|
throw new Error(`cannot cancel ${conversation.conversation_id}; conversation is ${conversation.status}`);
|
|
3892
5082
|
}
|
|
@@ -3964,91 +5154,123 @@ async function runCancel(options) {
|
|
|
3964
5154
|
});
|
|
3965
5155
|
}
|
|
3966
5156
|
async function runTerminalConversationCancel({ options, conversationId, agent, terminalControl, pid }) {
|
|
3967
|
-
const
|
|
3968
|
-
|
|
3969
|
-
|
|
3970
|
-
|
|
3971
|
-
|
|
3972
|
-
|
|
3973
|
-
|
|
3974
|
-
|
|
3975
|
-
|
|
3976
|
-
|
|
3977
|
-
|
|
3978
|
-
|
|
3979
|
-
|
|
3980
|
-
|
|
3981
|
-
|
|
3982
|
-
|
|
3983
|
-
|
|
3984
|
-
|
|
3985
|
-
|
|
3986
|
-
|
|
3987
|
-
|
|
3988
|
-
source: "terminal_control",
|
|
3989
|
-
cancel_requested: cancellation.cancelRequested,
|
|
3990
|
-
reason: cancellation.reason,
|
|
3991
|
-
terminal_control: terminalControl,
|
|
3992
|
-
key: cancellation.key,
|
|
3993
|
-
keys: cancellation.keys,
|
|
3994
|
-
denied_approval: cancellation.deniedApproval,
|
|
3995
|
-
request_id: cancellation.requestId
|
|
3996
|
-
});
|
|
3997
|
-
}
|
|
3998
|
-
async function runTerminalControlCancel({ options, conversation, statePath, logPath, agent, terminalControl }) {
|
|
3999
|
-
const cancellation = await createTerminalAgentBridge(options).cancel(agent, terminalControl, {
|
|
4000
|
-
runtime: terminalRuntimeIdentityForConversation(conversation, terminalControl),
|
|
4001
|
-
scrollbackLines: Number(options.scrollbackLines ?? 120)
|
|
4002
|
-
});
|
|
4003
|
-
if (!cancellation.cancelRequested) {
|
|
5157
|
+
const releaseTerminalLock = acquireFileLock(terminalBridgeSendLockPath(storeDirFromOptions(options), terminalControl), { timeoutMs: 30000 });
|
|
5158
|
+
try {
|
|
5159
|
+
const cancellation = await createTerminalAgentBridge(options).cancel(agent, terminalControl, {
|
|
5160
|
+
runtime: {
|
|
5161
|
+
pid,
|
|
5162
|
+
cwd: terminalControl.currentPath,
|
|
5163
|
+
terminalTarget: terminalControl.target
|
|
5164
|
+
},
|
|
5165
|
+
scrollbackLines: Number(options.scrollbackLines ?? 120)
|
|
5166
|
+
});
|
|
5167
|
+
runtimeLog("info", "terminal_cancel_requested", {
|
|
5168
|
+
conversation_id: conversationId,
|
|
5169
|
+
agent,
|
|
5170
|
+
terminal_target: terminalControl.target,
|
|
5171
|
+
key: cancellation.key,
|
|
5172
|
+
keys: cancellation.keys,
|
|
5173
|
+
denied_approval: cancellation.deniedApproval,
|
|
5174
|
+
request_id: cancellation.requestId,
|
|
5175
|
+
cancel_requested: cancellation.cancelRequested,
|
|
5176
|
+
reason: cancellation.reason
|
|
5177
|
+
});
|
|
4004
5178
|
printJson({
|
|
4005
|
-
|
|
4006
|
-
|
|
5179
|
+
conversation_id: conversationId,
|
|
5180
|
+
source: "terminal_control",
|
|
5181
|
+
cancel_requested: cancellation.cancelRequested,
|
|
4007
5182
|
reason: cancellation.reason,
|
|
4008
5183
|
terminal_control: terminalControl,
|
|
4009
|
-
|
|
5184
|
+
key: cancellation.key,
|
|
5185
|
+
keys: cancellation.keys,
|
|
5186
|
+
denied_approval: cancellation.deniedApproval,
|
|
5187
|
+
request_id: cancellation.requestId
|
|
4010
5188
|
});
|
|
4011
|
-
return;
|
|
4012
5189
|
}
|
|
4013
|
-
|
|
4014
|
-
|
|
4015
|
-
|
|
4016
|
-
|
|
4017
|
-
|
|
4018
|
-
|
|
4019
|
-
|
|
4020
|
-
|
|
4021
|
-
|
|
4022
|
-
|
|
4023
|
-
|
|
4024
|
-
|
|
4025
|
-
|
|
4026
|
-
|
|
4027
|
-
|
|
4028
|
-
|
|
4029
|
-
|
|
4030
|
-
|
|
4031
|
-
|
|
4032
|
-
|
|
4033
|
-
|
|
4034
|
-
|
|
4035
|
-
|
|
4036
|
-
|
|
4037
|
-
|
|
4038
|
-
|
|
4039
|
-
|
|
4040
|
-
|
|
4041
|
-
|
|
4042
|
-
|
|
4043
|
-
|
|
4044
|
-
|
|
4045
|
-
|
|
4046
|
-
|
|
4047
|
-
|
|
4048
|
-
|
|
4049
|
-
|
|
4050
|
-
|
|
4051
|
-
|
|
5190
|
+
finally {
|
|
5191
|
+
releaseTerminalLock();
|
|
5192
|
+
}
|
|
5193
|
+
}
|
|
5194
|
+
async function runTerminalControlCancel({ options, conversation, statePath, logPath, agent, terminalControl }) {
|
|
5195
|
+
const releaseTerminalLock = acquireFileLock(terminalBridgeSendLockPath(storeDirFromOptions(options), terminalControl), { timeoutMs: 30000 });
|
|
5196
|
+
let releaseStateLock;
|
|
5197
|
+
try {
|
|
5198
|
+
releaseStateLock = acquireFileLock(`${statePath}.lock`);
|
|
5199
|
+
const currentConversation = loadState(statePath);
|
|
5200
|
+
if (["closed", "cancelled"].includes(currentConversation.status)) {
|
|
5201
|
+
throw new Error(`cannot cancel ${currentConversation.conversation_id}; conversation is ${currentConversation.status}`);
|
|
5202
|
+
}
|
|
5203
|
+
const currentTakeover = isRecord(currentConversation.native_session_takeover)
|
|
5204
|
+
? currentConversation.native_session_takeover
|
|
5205
|
+
: undefined;
|
|
5206
|
+
const currentControl = terminalControlFromTakeover(currentTakeover);
|
|
5207
|
+
if (!currentControl ||
|
|
5208
|
+
currentControl.target !== terminalControl.target ||
|
|
5209
|
+
currentControl.socketPath !== terminalControl.socketPath) {
|
|
5210
|
+
throw new Error("terminal control changed while waiting to cancel; refresh status and retry");
|
|
5211
|
+
}
|
|
5212
|
+
const cancellation = await createTerminalAgentBridge(options).cancel(agent, currentControl, {
|
|
5213
|
+
runtime: terminalRuntimeIdentityForConversation(currentConversation, currentControl),
|
|
5214
|
+
scrollbackLines: Number(options.scrollbackLines ?? 120)
|
|
5215
|
+
});
|
|
5216
|
+
if (!cancellation.cancelRequested) {
|
|
5217
|
+
printJson({
|
|
5218
|
+
conversation: currentConversation,
|
|
5219
|
+
cancel_requested: false,
|
|
5220
|
+
reason: cancellation.reason,
|
|
5221
|
+
terminal_control: currentControl,
|
|
5222
|
+
budget: budgetAction(currentConversation)
|
|
5223
|
+
});
|
|
5224
|
+
return;
|
|
5225
|
+
}
|
|
5226
|
+
const now = new Date().toISOString();
|
|
5227
|
+
appendEvent(logPath, {
|
|
5228
|
+
ts: now,
|
|
5229
|
+
conversation_id: currentConversation.conversation_id,
|
|
5230
|
+
event: "terminal_cancel_requested",
|
|
5231
|
+
terminal_control: currentControl,
|
|
5232
|
+
key: cancellation.key,
|
|
5233
|
+
keys: cancellation.keys,
|
|
5234
|
+
denied_approval: cancellation.deniedApproval,
|
|
5235
|
+
request_id: cancellation.requestId
|
|
5236
|
+
});
|
|
5237
|
+
runtimeLog("info", "terminal_cancel_requested", {
|
|
5238
|
+
conversation_id: currentConversation.conversation_id,
|
|
5239
|
+
agent,
|
|
5240
|
+
terminal_target: currentControl.target,
|
|
5241
|
+
key: cancellation.key,
|
|
5242
|
+
keys: cancellation.keys,
|
|
5243
|
+
denied_approval: cancellation.deniedApproval,
|
|
5244
|
+
request_id: cancellation.requestId
|
|
5245
|
+
});
|
|
5246
|
+
const nextConversation = {
|
|
5247
|
+
...currentConversation,
|
|
5248
|
+
status: "cancelled",
|
|
5249
|
+
cancelled_at: now,
|
|
5250
|
+
terminal_cancel_requested_at: now,
|
|
5251
|
+
updated_at: now
|
|
5252
|
+
};
|
|
5253
|
+
saveState(statePath, nextConversation);
|
|
5254
|
+
releaseClaudeHookLease(nextConversation);
|
|
5255
|
+
printJson({
|
|
5256
|
+
conversation: nextConversation,
|
|
5257
|
+
cancel_requested: true,
|
|
5258
|
+
terminal_control: currentControl,
|
|
5259
|
+
key: cancellation.key,
|
|
5260
|
+
keys: cancellation.keys,
|
|
5261
|
+
denied_approval: cancellation.deniedApproval,
|
|
5262
|
+
request_id: cancellation.requestId,
|
|
5263
|
+
budget: budgetAction(nextConversation)
|
|
5264
|
+
});
|
|
5265
|
+
}
|
|
5266
|
+
finally {
|
|
5267
|
+
try {
|
|
5268
|
+
releaseStateLock?.();
|
|
5269
|
+
}
|
|
5270
|
+
finally {
|
|
5271
|
+
releaseTerminalLock();
|
|
5272
|
+
}
|
|
5273
|
+
}
|
|
4052
5274
|
}
|
|
4053
5275
|
function runRecover(options) {
|
|
4054
5276
|
runRecoveryDecision({ ...options, mode: "recover" });
|
|
@@ -4117,7 +5339,7 @@ function runRecoveryDecision(options) {
|
|
|
4117
5339
|
const acpxArgs = buildAcpxPromptArgs({ executor, payload, model: executorModel });
|
|
4118
5340
|
if (options.background) {
|
|
4119
5341
|
const outputPath = path.join(path.dirname(logPath), `${executor.kind}-${options.mode}-output.log`);
|
|
4120
|
-
const outputFd =
|
|
5342
|
+
const outputFd = openPrivateAppendFile(outputPath);
|
|
4121
5343
|
const child = spawn(acpxPath, acpxArgs, {
|
|
4122
5344
|
detached: true,
|
|
4123
5345
|
stdio: ["ignore", outputFd, outputFd],
|
|
@@ -4217,36 +5439,57 @@ function formatProtocolHistoryForRecovery(events) {
|
|
|
4217
5439
|
});
|
|
4218
5440
|
return lines.length ? lines.join("\n") : "- No prior protocol messages were recorded.";
|
|
4219
5441
|
}
|
|
4220
|
-
function runClose(options) {
|
|
4221
|
-
const
|
|
4222
|
-
const
|
|
4223
|
-
const
|
|
4224
|
-
|
|
4225
|
-
|
|
4226
|
-
|
|
4227
|
-
|
|
4228
|
-
|
|
4229
|
-
|
|
4230
|
-
|
|
4231
|
-
|
|
4232
|
-
|
|
4233
|
-
|
|
4234
|
-
|
|
4235
|
-
|
|
4236
|
-
|
|
4237
|
-
|
|
4238
|
-
|
|
4239
|
-
|
|
4240
|
-
|
|
4241
|
-
|
|
4242
|
-
|
|
4243
|
-
|
|
4244
|
-
|
|
4245
|
-
|
|
4246
|
-
|
|
4247
|
-
|
|
4248
|
-
|
|
4249
|
-
|
|
5442
|
+
async function runClose(options) {
|
|
5443
|
+
const loaded = loadConversationFromOptions(options);
|
|
5444
|
+
const { statePath, logPath } = loaded;
|
|
5445
|
+
const nativeTakeover = isRecord(loaded.conversation.native_session_takeover)
|
|
5446
|
+
? loaded.conversation.native_session_takeover
|
|
5447
|
+
: undefined;
|
|
5448
|
+
const terminalControl = terminalControlFromTakeover(nativeTakeover);
|
|
5449
|
+
const releaseTerminalLock = terminalControl
|
|
5450
|
+
? acquireFileLock(terminalBridgeSendLockPath(storeDirFromOptions(options), terminalControl), { timeoutMs: 30000 })
|
|
5451
|
+
: () => { };
|
|
5452
|
+
let releaseStateLock;
|
|
5453
|
+
try {
|
|
5454
|
+
releaseStateLock = acquireFileLock(`${statePath}.lock`);
|
|
5455
|
+
const conversation = loadState(statePath);
|
|
5456
|
+
const now = new Date().toISOString();
|
|
5457
|
+
const closed = {
|
|
5458
|
+
...conversation,
|
|
5459
|
+
status: "closed",
|
|
5460
|
+
closed_at: now,
|
|
5461
|
+
close_reason: options.reason ?? "closed by request",
|
|
5462
|
+
updated_at: now
|
|
5463
|
+
};
|
|
5464
|
+
saveState(statePath, closed);
|
|
5465
|
+
releaseClaudeHookLease(closed);
|
|
5466
|
+
appendEvent(logPath, {
|
|
5467
|
+
ts: now,
|
|
5468
|
+
conversation_id: conversation.conversation_id,
|
|
5469
|
+
event: "conversation_closed",
|
|
5470
|
+
status: "closed",
|
|
5471
|
+
reason: closed.close_reason
|
|
5472
|
+
});
|
|
5473
|
+
runtimeLog("info", "conversation_closed", {
|
|
5474
|
+
conversation_id: conversation.conversation_id,
|
|
5475
|
+
status: "closed",
|
|
5476
|
+
reason: closed.close_reason,
|
|
5477
|
+
state_path: statePath,
|
|
5478
|
+
event_log_path: logPath
|
|
5479
|
+
});
|
|
5480
|
+
printJson({
|
|
5481
|
+
conversation: closed,
|
|
5482
|
+
closed: true
|
|
5483
|
+
});
|
|
5484
|
+
}
|
|
5485
|
+
finally {
|
|
5486
|
+
try {
|
|
5487
|
+
releaseStateLock?.();
|
|
5488
|
+
}
|
|
5489
|
+
finally {
|
|
5490
|
+
releaseTerminalLock();
|
|
5491
|
+
}
|
|
5492
|
+
}
|
|
4250
5493
|
}
|
|
4251
5494
|
async function runMonitor(options) {
|
|
4252
5495
|
if (options.callbackRetry) {
|
|
@@ -4383,45 +5626,66 @@ async function runMonitor(options) {
|
|
|
4383
5626
|
sleepSync(pollIntervalMs);
|
|
4384
5627
|
}
|
|
4385
5628
|
}
|
|
4386
|
-
function startCallbackRetryMonitor({ statePath }) {
|
|
5629
|
+
function startCallbackRetryMonitor({ statePath, delayMs = CALLBACK_RETRY_DELAYS_MS[0] }) {
|
|
5630
|
+
const normalizedDelayMs = Math.max(0, Number.isFinite(Number(delayMs)) ? Number(delayMs) : CALLBACK_RETRY_DELAYS_MS[0]);
|
|
4387
5631
|
const child = spawn(process.execPath, [
|
|
4388
5632
|
new URL(import.meta.url).pathname,
|
|
4389
5633
|
"monitor",
|
|
4390
5634
|
"--callback-retry",
|
|
4391
5635
|
"--state",
|
|
4392
|
-
statePath
|
|
5636
|
+
statePath,
|
|
5637
|
+
"--callback-retry-delay-ms",
|
|
5638
|
+
String(normalizedDelayMs)
|
|
4393
5639
|
], {
|
|
4394
5640
|
detached: true,
|
|
4395
5641
|
stdio: "ignore",
|
|
4396
5642
|
cwd: process.cwd(),
|
|
4397
|
-
env:
|
|
5643
|
+
env: environmentWithoutGatewayTokens()
|
|
4398
5644
|
});
|
|
4399
5645
|
child.unref();
|
|
4400
5646
|
return child;
|
|
4401
5647
|
}
|
|
4402
5648
|
function runCallbackRetryMonitor(options) {
|
|
4403
5649
|
const statePath = expandHome(required(options.state, "--state is required"));
|
|
5650
|
+
const initialDelayMs = Math.max(0, Number.isFinite(Number(options.callbackRetryDelayMs))
|
|
5651
|
+
? Number(options.callbackRetryDelayMs)
|
|
5652
|
+
: CALLBACK_RETRY_DELAYS_MS[0]);
|
|
5653
|
+
sleepSync(initialDelayMs);
|
|
4404
5654
|
while (true) {
|
|
4405
5655
|
const conversation = loadState(statePath);
|
|
4406
5656
|
const callbackDelivery = isRecord(conversation.callback_delivery)
|
|
4407
5657
|
? conversation.callback_delivery
|
|
4408
5658
|
: undefined;
|
|
4409
5659
|
const attempts = Number(callbackDelivery?.attempts ?? 0);
|
|
4410
|
-
if (
|
|
5660
|
+
if (!["callback_pending", "callback_failed"].includes(conversation.status) ||
|
|
5661
|
+
!["pending", "failed"].includes(String(callbackDelivery?.status ?? "")) ||
|
|
5662
|
+
!isRecord(callbackDelivery?.message)) {
|
|
4411
5663
|
return;
|
|
4412
5664
|
}
|
|
4413
5665
|
if (attempts > CALLBACK_RETRY_DELAYS_MS.length) {
|
|
4414
5666
|
return;
|
|
4415
5667
|
}
|
|
4416
|
-
|
|
4417
|
-
|
|
4418
|
-
|
|
5668
|
+
let releaseLock;
|
|
5669
|
+
try {
|
|
5670
|
+
releaseLock = acquireFileLock(`${statePath}.lock`);
|
|
5671
|
+
}
|
|
5672
|
+
catch (error) {
|
|
5673
|
+
if (isRecord(error) && error.code === "LOCK_TIMEOUT") {
|
|
5674
|
+
sleepSync(1000);
|
|
5675
|
+
continue;
|
|
5676
|
+
}
|
|
5677
|
+
throw error;
|
|
5678
|
+
}
|
|
4419
5679
|
try {
|
|
4420
5680
|
const current = loadState(statePath);
|
|
4421
5681
|
const currentDelivery = isRecord(current.callback_delivery)
|
|
4422
5682
|
? current.callback_delivery
|
|
4423
5683
|
: undefined;
|
|
4424
|
-
|
|
5684
|
+
const currentAttempts = Number(currentDelivery?.attempts ?? 0);
|
|
5685
|
+
if (!["callback_pending", "callback_failed"].includes(current.status) ||
|
|
5686
|
+
!["pending", "failed"].includes(String(currentDelivery?.status ?? "")) ||
|
|
5687
|
+
!isRecord(currentDelivery?.message) ||
|
|
5688
|
+
currentAttempts > CALLBACK_RETRY_DELAYS_MS.length) {
|
|
4425
5689
|
return;
|
|
4426
5690
|
}
|
|
4427
5691
|
try {
|
|
@@ -4447,13 +5711,62 @@ function runCallbackRetryMonitor(options) {
|
|
|
4447
5711
|
finally {
|
|
4448
5712
|
releaseLock();
|
|
4449
5713
|
}
|
|
5714
|
+
const latest = loadState(statePath);
|
|
5715
|
+
const latestDelivery = isRecord(latest.callback_delivery)
|
|
5716
|
+
? latest.callback_delivery
|
|
5717
|
+
: undefined;
|
|
5718
|
+
const latestAttempts = Number(latestDelivery?.attempts ?? 0);
|
|
5719
|
+
if (!["callback_pending", "callback_failed"].includes(latest.status) ||
|
|
5720
|
+
!["pending", "failed"].includes(String(latestDelivery?.status ?? "")) ||
|
|
5721
|
+
!isRecord(latestDelivery?.message) ||
|
|
5722
|
+
latestAttempts > CALLBACK_RETRY_DELAYS_MS.length) {
|
|
5723
|
+
return;
|
|
5724
|
+
}
|
|
5725
|
+
const delayMs = CALLBACK_RETRY_DELAYS_MS[Math.max(0, latestAttempts - 1)];
|
|
5726
|
+
sleepSync(delayMs);
|
|
4450
5727
|
}
|
|
4451
5728
|
}
|
|
4452
5729
|
async function runTerminalBridgeMonitor(options) {
|
|
5730
|
+
const statePath = expandHome(required(options.state, "--state is required"));
|
|
5731
|
+
const conversation = loadState(statePath);
|
|
5732
|
+
const nativeTakeover = isRecord(conversation.native_session_takeover)
|
|
5733
|
+
? conversation.native_session_takeover
|
|
5734
|
+
: undefined;
|
|
5735
|
+
const terminalMessageId = stringValue(nativeTakeover?.terminal_bridge_message_id) ?? "missing-message-id";
|
|
5736
|
+
const monitorLock = tryAcquireTerminalBridgeMonitorLock(statePath, terminalMessageId);
|
|
5737
|
+
if (!monitorLock.acquired) {
|
|
5738
|
+
runtimeLog("info", "terminal_bridge_monitor_already_running", {
|
|
5739
|
+
conversation_id: conversation.conversation_id,
|
|
5740
|
+
terminal_bridge_message_id: terminalMessageId,
|
|
5741
|
+
monitor_owner_pid: monitorLock.ownerPid
|
|
5742
|
+
});
|
|
5743
|
+
printJson({
|
|
5744
|
+
conversation,
|
|
5745
|
+
monitored: false,
|
|
5746
|
+
terminal_bridge: true,
|
|
5747
|
+
already_running: true,
|
|
5748
|
+
reason: "terminal_bridge_monitor_already_running",
|
|
5749
|
+
monitor_owner_pid: monitorLock.ownerPid ?? null
|
|
5750
|
+
});
|
|
5751
|
+
return;
|
|
5752
|
+
}
|
|
5753
|
+
try {
|
|
5754
|
+
await runTerminalBridgeMonitorWithLock(options);
|
|
5755
|
+
}
|
|
5756
|
+
finally {
|
|
5757
|
+
monitorLock.release();
|
|
5758
|
+
}
|
|
5759
|
+
}
|
|
5760
|
+
async function runTerminalBridgeMonitorWithLock(options) {
|
|
4453
5761
|
const statePath = expandHome(required(options.state, "--state is required"));
|
|
4454
5762
|
const logPath = expandHome(options.log ?? logPathForStatePath(statePath));
|
|
4455
5763
|
const pollIntervalMs = Math.max(50, Number(options.pollIntervalMs ?? DEFAULT_MONITOR_POLL_INTERVAL_MS));
|
|
4456
|
-
let conversation =
|
|
5764
|
+
let conversation = await migrateLegacyTerminalAgentIdentity({
|
|
5765
|
+
conversation: loadState(statePath),
|
|
5766
|
+
statePath,
|
|
5767
|
+
logPath,
|
|
5768
|
+
options
|
|
5769
|
+
});
|
|
4457
5770
|
const initialNativeTakeover = isRecord(conversation.native_session_takeover)
|
|
4458
5771
|
? conversation.native_session_takeover
|
|
4459
5772
|
: undefined;
|
|
@@ -4587,6 +5900,25 @@ async function runTerminalBridgeMonitor(options) {
|
|
|
4587
5900
|
});
|
|
4588
5901
|
const terminalStatus = poll.status;
|
|
4589
5902
|
const approval = terminalStatus.approval_state;
|
|
5903
|
+
const currentScreenFingerprint = stringValue(terminalStatus?.screen?.digest) ??
|
|
5904
|
+
terminalBridgeScreenFingerprint(terminalStatus?.screen?.excerpt);
|
|
5905
|
+
const currentScreenChangedSinceSend = preSendScreenFingerprint !== undefined &&
|
|
5906
|
+
currentScreenFingerprint !== undefined &&
|
|
5907
|
+
currentScreenFingerprint !== preSendScreenFingerprint;
|
|
5908
|
+
if (executor.kind === "claude" &&
|
|
5909
|
+
isRecord(approval) &&
|
|
5910
|
+
approval.approvable === true &&
|
|
5911
|
+
approval.decision_mode === "keys" &&
|
|
5912
|
+
!currentScreenChangedSinceSend) {
|
|
5913
|
+
previousScreenFingerprint = currentScreenFingerprint;
|
|
5914
|
+
runtimeLog("warn", "claude_screen_approval_not_new", {
|
|
5915
|
+
conversation_id: conversation.conversation_id,
|
|
5916
|
+
terminal_target: terminalControl.target,
|
|
5917
|
+
reason: "permission screen is not proven to have changed since the managed send"
|
|
5918
|
+
});
|
|
5919
|
+
sleepSync(pollIntervalMs);
|
|
5920
|
+
continue;
|
|
5921
|
+
}
|
|
4590
5922
|
if (isRecord(approval) && approval.blocked === true && approval.approvable !== true) {
|
|
4591
5923
|
const approvalReason = stringValue(approval.reason) ??
|
|
4592
5924
|
"Claude Code permission state cannot be safely resolved through AKK";
|
|
@@ -4608,8 +5940,67 @@ async function runTerminalBridgeMonitor(options) {
|
|
|
4608
5940
|
logPath,
|
|
4609
5941
|
terminalControl,
|
|
4610
5942
|
terminalStatus,
|
|
4611
|
-
fingerprint
|
|
5943
|
+
fingerprint,
|
|
5944
|
+
expectedConversation: {
|
|
5945
|
+
conversationId: conversation.conversation_id,
|
|
5946
|
+
status: conversation.status,
|
|
5947
|
+
updatedAt: conversation.updated_at,
|
|
5948
|
+
messageId: currentMessageId
|
|
5949
|
+
},
|
|
5950
|
+
onRecorded: (notificationConversation) => {
|
|
5951
|
+
const callbackMessage = createMessage({
|
|
5952
|
+
conversation: notificationConversation,
|
|
5953
|
+
from: executor.actor,
|
|
5954
|
+
to: "openclaw",
|
|
5955
|
+
type: "blocked",
|
|
5956
|
+
requiresResponse: true,
|
|
5957
|
+
body: [
|
|
5958
|
+
`${executor.display_name} is waiting at a permission state that AKK cannot safely approve.`,
|
|
5959
|
+
approvalReason,
|
|
5960
|
+
"",
|
|
5961
|
+
`Conversation: ${notificationConversation.conversation_id}`,
|
|
5962
|
+
`Terminal: ${terminalControl.target}`,
|
|
5963
|
+
"Review and resolve this dialog in the terminal manually. AKK intentionally sends no key when the request identity cannot be revalidated."
|
|
5964
|
+
].join("\n"),
|
|
5965
|
+
metadata: {
|
|
5966
|
+
source: "terminal_bridge",
|
|
5967
|
+
reason: "approval_not_approvable",
|
|
5968
|
+
terminal_control: terminalControl,
|
|
5969
|
+
terminal_status: terminalStatus,
|
|
5970
|
+
approval_fingerprint: fingerprint
|
|
5971
|
+
}
|
|
5972
|
+
});
|
|
5973
|
+
if (notificationConversation.gateway_method) {
|
|
5974
|
+
runLockedCallback({
|
|
5975
|
+
...options,
|
|
5976
|
+
statePath,
|
|
5977
|
+
log: logPath,
|
|
5978
|
+
messageJson: JSON.stringify(callbackMessage),
|
|
5979
|
+
gatewayMethod: notificationConversation.gateway_method,
|
|
5980
|
+
gatewaySession: notificationConversation.gateway_session,
|
|
5981
|
+
openclawSession: notificationConversation.openclaw_session,
|
|
5982
|
+
openclawBin: notificationConversation.openclaw_bin,
|
|
5983
|
+
gatewayUrl: stringValue(notificationConversation.gateway_token)
|
|
5984
|
+
? notificationConversation.gateway_url
|
|
5985
|
+
: undefined,
|
|
5986
|
+
token: stringValue(notificationConversation.gateway_token)
|
|
5987
|
+
});
|
|
5988
|
+
return {
|
|
5989
|
+
callbackMessage,
|
|
5990
|
+
delivered: true
|
|
5991
|
+
};
|
|
5992
|
+
}
|
|
5993
|
+
return {
|
|
5994
|
+
callbackMessage,
|
|
5995
|
+
delivered: false
|
|
5996
|
+
};
|
|
5997
|
+
}
|
|
4612
5998
|
});
|
|
5999
|
+
if (notification.stale) {
|
|
6000
|
+
previousScreenFingerprint = currentScreenFingerprint;
|
|
6001
|
+
sleepSync(pollIntervalMs);
|
|
6002
|
+
continue;
|
|
6003
|
+
}
|
|
4613
6004
|
if (notification.duplicate) {
|
|
4614
6005
|
printJson({
|
|
4615
6006
|
conversation: notification.conversation,
|
|
@@ -4624,43 +6015,7 @@ async function runTerminalBridgeMonitor(options) {
|
|
|
4624
6015
|
});
|
|
4625
6016
|
return;
|
|
4626
6017
|
}
|
|
4627
|
-
|
|
4628
|
-
conversation: notification.conversation,
|
|
4629
|
-
from: executor.actor,
|
|
4630
|
-
to: "openclaw",
|
|
4631
|
-
type: "blocked",
|
|
4632
|
-
requiresResponse: true,
|
|
4633
|
-
body: [
|
|
4634
|
-
`${executor.display_name} is waiting at a permission state that AKK cannot safely approve.`,
|
|
4635
|
-
approvalReason,
|
|
4636
|
-
"",
|
|
4637
|
-
`Conversation: ${notification.conversation.conversation_id}`,
|
|
4638
|
-
`Terminal: ${terminalControl.target}`,
|
|
4639
|
-
"Review and resolve this dialog in the terminal manually. AKK intentionally sends no key when the request identity cannot be revalidated."
|
|
4640
|
-
].join("\n"),
|
|
4641
|
-
metadata: {
|
|
4642
|
-
source: "terminal_bridge",
|
|
4643
|
-
reason: "approval_not_approvable",
|
|
4644
|
-
terminal_control: terminalControl,
|
|
4645
|
-
terminal_status: terminalStatus,
|
|
4646
|
-
approval_fingerprint: fingerprint
|
|
4647
|
-
}
|
|
4648
|
-
});
|
|
4649
|
-
if (notification.conversation.gateway_method) {
|
|
4650
|
-
runLockedCallback({
|
|
4651
|
-
...options,
|
|
4652
|
-
statePath,
|
|
4653
|
-
log: logPath,
|
|
4654
|
-
messageJson: JSON.stringify(callbackMessage),
|
|
4655
|
-
gatewayMethod: notification.conversation.gateway_method,
|
|
4656
|
-
gatewaySession: notification.conversation.gateway_session,
|
|
4657
|
-
openclawSession: notification.conversation.openclaw_session,
|
|
4658
|
-
openclawBin: notification.conversation.openclaw_bin,
|
|
4659
|
-
gatewayUrl: stringValue(notification.conversation.gateway_token)
|
|
4660
|
-
? notification.conversation.gateway_url
|
|
4661
|
-
: undefined,
|
|
4662
|
-
token: stringValue(notification.conversation.gateway_token)
|
|
4663
|
-
});
|
|
6018
|
+
if (notification.recorded?.delivered) {
|
|
4664
6019
|
return;
|
|
4665
6020
|
}
|
|
4666
6021
|
printJson({
|
|
@@ -4670,7 +6025,7 @@ async function runTerminalBridgeMonitor(options) {
|
|
|
4670
6025
|
awaiting_approval: true,
|
|
4671
6026
|
approvable: false,
|
|
4672
6027
|
delivered: false,
|
|
4673
|
-
message: callbackMessage,
|
|
6028
|
+
message: notification.recorded?.callbackMessage,
|
|
4674
6029
|
reason: "gateway_method_missing",
|
|
4675
6030
|
terminal_control: terminalControl,
|
|
4676
6031
|
terminal_status: terminalStatus
|
|
@@ -4693,8 +6048,74 @@ async function runTerminalBridgeMonitor(options) {
|
|
|
4693
6048
|
logPath,
|
|
4694
6049
|
terminalControl,
|
|
4695
6050
|
terminalStatus,
|
|
4696
|
-
fingerprint
|
|
6051
|
+
fingerprint,
|
|
6052
|
+
expectedConversation: {
|
|
6053
|
+
conversationId: conversation.conversation_id,
|
|
6054
|
+
status: conversation.status,
|
|
6055
|
+
updatedAt: conversation.updated_at,
|
|
6056
|
+
messageId: currentMessageId
|
|
6057
|
+
},
|
|
6058
|
+
onRecorded: (notificationConversation) => {
|
|
6059
|
+
const callbackMessage = createMessage({
|
|
6060
|
+
conversation: notificationConversation,
|
|
6061
|
+
from: executor.actor,
|
|
6062
|
+
to: "openclaw",
|
|
6063
|
+
type: "question",
|
|
6064
|
+
requiresResponse: true,
|
|
6065
|
+
body: terminalBridgeApprovalInstructions({
|
|
6066
|
+
conversation: notificationConversation,
|
|
6067
|
+
terminalControl,
|
|
6068
|
+
terminalStatus
|
|
6069
|
+
}),
|
|
6070
|
+
metadata: {
|
|
6071
|
+
source: "terminal_bridge",
|
|
6072
|
+
reason: "approval_required",
|
|
6073
|
+
terminal_control: terminalControl,
|
|
6074
|
+
terminal_status: terminalStatus,
|
|
6075
|
+
approval_fingerprint: fingerprint,
|
|
6076
|
+
approval_candidate: terminalBridgeApprovalCandidate({
|
|
6077
|
+
executor,
|
|
6078
|
+
terminalControl,
|
|
6079
|
+
terminalStatus,
|
|
6080
|
+
fingerprint
|
|
6081
|
+
}),
|
|
6082
|
+
approve_command: `AKK approve ${notificationConversation.conversation_id} --expected-approval-fingerprint ${fingerprint}`,
|
|
6083
|
+
deny_command: `AKK cancel ${notificationConversation.conversation_id}`,
|
|
6084
|
+
approve_tool: "agent_knock_knock_approve",
|
|
6085
|
+
deny_tool: "agent_knock_knock_cancel"
|
|
6086
|
+
}
|
|
6087
|
+
});
|
|
6088
|
+
if (notificationConversation.gateway_method) {
|
|
6089
|
+
runLockedCallback({
|
|
6090
|
+
...options,
|
|
6091
|
+
statePath,
|
|
6092
|
+
log: logPath,
|
|
6093
|
+
messageJson: JSON.stringify(callbackMessage),
|
|
6094
|
+
gatewayMethod: notificationConversation.gateway_method,
|
|
6095
|
+
gatewaySession: notificationConversation.gateway_session,
|
|
6096
|
+
openclawSession: notificationConversation.openclaw_session,
|
|
6097
|
+
openclawBin: notificationConversation.openclaw_bin,
|
|
6098
|
+
gatewayUrl: stringValue(notificationConversation.gateway_token)
|
|
6099
|
+
? notificationConversation.gateway_url
|
|
6100
|
+
: undefined,
|
|
6101
|
+
token: stringValue(notificationConversation.gateway_token)
|
|
6102
|
+
});
|
|
6103
|
+
return {
|
|
6104
|
+
callbackMessage,
|
|
6105
|
+
delivered: true
|
|
6106
|
+
};
|
|
6107
|
+
}
|
|
6108
|
+
return {
|
|
6109
|
+
callbackMessage,
|
|
6110
|
+
delivered: false
|
|
6111
|
+
};
|
|
6112
|
+
}
|
|
4697
6113
|
});
|
|
6114
|
+
if (notification.stale) {
|
|
6115
|
+
previousScreenFingerprint = currentScreenFingerprint;
|
|
6116
|
+
sleepSync(pollIntervalMs);
|
|
6117
|
+
continue;
|
|
6118
|
+
}
|
|
4698
6119
|
if (notification.duplicate) {
|
|
4699
6120
|
printJson({
|
|
4700
6121
|
conversation: notification.conversation,
|
|
@@ -4707,48 +6128,7 @@ async function runTerminalBridgeMonitor(options) {
|
|
|
4707
6128
|
});
|
|
4708
6129
|
return;
|
|
4709
6130
|
}
|
|
4710
|
-
|
|
4711
|
-
conversation: notification.conversation,
|
|
4712
|
-
from: executor.actor,
|
|
4713
|
-
to: "openclaw",
|
|
4714
|
-
type: "question",
|
|
4715
|
-
requiresResponse: true,
|
|
4716
|
-
body: terminalBridgeApprovalInstructions({
|
|
4717
|
-
conversation: notification.conversation,
|
|
4718
|
-
terminalControl,
|
|
4719
|
-
terminalStatus
|
|
4720
|
-
}),
|
|
4721
|
-
metadata: {
|
|
4722
|
-
source: "terminal_bridge",
|
|
4723
|
-
reason: "approval_required",
|
|
4724
|
-
terminal_control: terminalControl,
|
|
4725
|
-
terminal_status: terminalStatus,
|
|
4726
|
-
approval_fingerprint: fingerprint,
|
|
4727
|
-
approval_candidate: terminalBridgeApprovalCandidate({
|
|
4728
|
-
executor,
|
|
4729
|
-
terminalControl,
|
|
4730
|
-
terminalStatus,
|
|
4731
|
-
fingerprint
|
|
4732
|
-
}),
|
|
4733
|
-
approve_command: `AKK approve ${notification.conversation.conversation_id} --expected-approval-fingerprint ${fingerprint}`,
|
|
4734
|
-
deny_command: `AKK cancel ${notification.conversation.conversation_id}`,
|
|
4735
|
-
approve_tool: "agent_knock_knock_approve",
|
|
4736
|
-
deny_tool: "agent_knock_knock_cancel"
|
|
4737
|
-
}
|
|
4738
|
-
});
|
|
4739
|
-
if (notification.conversation.gateway_method) {
|
|
4740
|
-
runLockedCallback({
|
|
4741
|
-
...options,
|
|
4742
|
-
statePath,
|
|
4743
|
-
log: logPath,
|
|
4744
|
-
messageJson: JSON.stringify(callbackMessage),
|
|
4745
|
-
gatewayMethod: notification.conversation.gateway_method,
|
|
4746
|
-
gatewaySession: notification.conversation.gateway_session,
|
|
4747
|
-
openclawSession: notification.conversation.openclaw_session,
|
|
4748
|
-
openclawBin: notification.conversation.openclaw_bin,
|
|
4749
|
-
gatewayUrl: stringValue(notification.conversation.gateway_token) ? notification.conversation.gateway_url : undefined,
|
|
4750
|
-
token: stringValue(notification.conversation.gateway_token)
|
|
4751
|
-
});
|
|
6131
|
+
if (notification.recorded?.delivered) {
|
|
4752
6132
|
return;
|
|
4753
6133
|
}
|
|
4754
6134
|
printJson({
|
|
@@ -4757,14 +6137,14 @@ async function runTerminalBridgeMonitor(options) {
|
|
|
4757
6137
|
terminal_bridge: true,
|
|
4758
6138
|
awaiting_approval: true,
|
|
4759
6139
|
delivered: false,
|
|
4760
|
-
message: callbackMessage,
|
|
6140
|
+
message: notification.recorded?.callbackMessage,
|
|
4761
6141
|
reason: "gateway_method_missing",
|
|
4762
6142
|
terminal_control: terminalControl,
|
|
4763
6143
|
terminal_status: terminalStatus
|
|
4764
6144
|
});
|
|
4765
6145
|
return;
|
|
4766
6146
|
}
|
|
4767
|
-
const screenFingerprint =
|
|
6147
|
+
const screenFingerprint = currentScreenFingerprint;
|
|
4768
6148
|
const screenChanged = previousScreenFingerprint !== undefined &&
|
|
4769
6149
|
screenFingerprint !== undefined &&
|
|
4770
6150
|
screenFingerprint !== previousScreenFingerprint;
|
|
@@ -5009,7 +6389,48 @@ function terminalBridgeScreenFingerprint(value) {
|
|
|
5009
6389
|
? createHash("sha256").update(value).digest("hex")
|
|
5010
6390
|
: undefined;
|
|
5011
6391
|
}
|
|
6392
|
+
function terminalBridgeMonitorLockPath(statePath, terminalMessageId) {
|
|
6393
|
+
const messageKey = createHash("sha256")
|
|
6394
|
+
.update(terminalMessageId)
|
|
6395
|
+
.digest("hex")
|
|
6396
|
+
.slice(0, 20);
|
|
6397
|
+
return `${statePath}.terminal-bridge-monitor-${messageKey}.lock`;
|
|
6398
|
+
}
|
|
6399
|
+
function fileLockOwnerPid(lockPath) {
|
|
6400
|
+
return readFileLockOwner(lockPath).pid;
|
|
6401
|
+
}
|
|
6402
|
+
function activeTerminalBridgeMonitorOwner(statePath, terminalMessageId) {
|
|
6403
|
+
const lockPath = terminalBridgeMonitorLockPath(statePath, terminalMessageId);
|
|
6404
|
+
if (!fs.existsSync(lockPath) || staleFileLock(lockPath)) {
|
|
6405
|
+
return undefined;
|
|
6406
|
+
}
|
|
6407
|
+
return {
|
|
6408
|
+
lockPath,
|
|
6409
|
+
ownerPid: fileLockOwnerPid(lockPath)
|
|
6410
|
+
};
|
|
6411
|
+
}
|
|
6412
|
+
function tryAcquireTerminalBridgeMonitorLock(statePath, terminalMessageId) {
|
|
6413
|
+
const lockPath = terminalBridgeMonitorLockPath(statePath, terminalMessageId);
|
|
6414
|
+
try {
|
|
6415
|
+
return {
|
|
6416
|
+
acquired: true,
|
|
6417
|
+
lockPath,
|
|
6418
|
+
release: acquireFileLock(lockPath, { timeoutMs: 0 })
|
|
6419
|
+
};
|
|
6420
|
+
}
|
|
6421
|
+
catch (error) {
|
|
6422
|
+
if (isRecord(error) && error.code === "LOCK_TIMEOUT") {
|
|
6423
|
+
return {
|
|
6424
|
+
acquired: false,
|
|
6425
|
+
lockPath,
|
|
6426
|
+
ownerPid: fileLockOwnerPid(lockPath)
|
|
6427
|
+
};
|
|
6428
|
+
}
|
|
6429
|
+
throw error;
|
|
6430
|
+
}
|
|
6431
|
+
}
|
|
5012
6432
|
function terminalBridgeSendLockPath(storeDir, terminalControl) {
|
|
6433
|
+
ensureDir(storeDir);
|
|
5013
6434
|
const terminalKey = createHash("sha256")
|
|
5014
6435
|
.update(JSON.stringify({
|
|
5015
6436
|
target: terminalControl.target,
|
|
@@ -5216,7 +6637,7 @@ function terminalBridgeApprovalCandidate({ executor, terminalControl, terminalSt
|
|
|
5216
6637
|
command: stringValue(approval.command),
|
|
5217
6638
|
tool_name: stringValue(approval.tool_name),
|
|
5218
6639
|
request_detail: stringValue(approval.request_detail),
|
|
5219
|
-
cwd: terminalControl.currentPath,
|
|
6640
|
+
cwd: stringValue(approval.cwd) ?? terminalControl.currentPath,
|
|
5220
6641
|
fingerprint,
|
|
5221
6642
|
terminal_target: terminalControl.target,
|
|
5222
6643
|
decision_mode: stringValue(approval.decision_mode)
|
|
@@ -5318,6 +6739,11 @@ function resolveOptionalExecutable(command) {
|
|
|
5318
6739
|
function packageRootDir() {
|
|
5319
6740
|
return path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
|
5320
6741
|
}
|
|
6742
|
+
function printVersion() {
|
|
6743
|
+
const packageJsonPath = path.join(packageRootDir(), "package.json");
|
|
6744
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
|
|
6745
|
+
process.stdout.write(`${packageJson.version}\n`);
|
|
6746
|
+
}
|
|
5321
6747
|
function runCheckedCommand(command, args, { label }) {
|
|
5322
6748
|
const result = spawnSync(command, args, {
|
|
5323
6749
|
encoding: "utf8",
|
|
@@ -5356,18 +6782,18 @@ function buildCallbackCommand({ statePath, gatewayUrl, token, openclawSession, g
|
|
|
5356
6782
|
"--state",
|
|
5357
6783
|
shellQuote(statePath)
|
|
5358
6784
|
];
|
|
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
6785
|
if (gatewayMethod) {
|
|
5366
6786
|
parts.push("--gateway-method", shellQuote(gatewayMethod), "--gateway-session", shellQuote(gatewaySession ?? openclawSession));
|
|
5367
6787
|
if (openclawBin) {
|
|
5368
6788
|
parts.push("--openclaw-bin", shellQuote(openclawBin));
|
|
5369
6789
|
}
|
|
5370
6790
|
}
|
|
6791
|
+
else if (token) {
|
|
6792
|
+
parts.push("--gateway-url", shellQuote(gatewayUrl), "--token", shellQuote(token), "--openclaw-session", shellQuote(openclawSession));
|
|
6793
|
+
}
|
|
6794
|
+
else {
|
|
6795
|
+
parts.push("--record-only");
|
|
6796
|
+
}
|
|
5371
6797
|
parts.push("--message-json", "'<structured-message-json>'");
|
|
5372
6798
|
return parts.join(" ");
|
|
5373
6799
|
}
|
|
@@ -5490,6 +6916,18 @@ function runLockedCallback(options) {
|
|
|
5490
6916
|
CONVERSATION_STATUSES.has(storedFinalStatus)
|
|
5491
6917
|
? storedFinalStatus
|
|
5492
6918
|
: nextConversation.status;
|
|
6919
|
+
const callbackRetryDelayMs = CALLBACK_RETRY_DELAYS_MS[Math.min(CALLBACK_RETRY_DELAYS_MS.length - 1, Math.max(0, deliveryAttempt - 1))];
|
|
6920
|
+
const callbackWatchdog = trackCallbackDelivery &&
|
|
6921
|
+
requiresDelivery &&
|
|
6922
|
+
options.recordOnly !== true &&
|
|
6923
|
+
!retryingPending &&
|
|
6924
|
+
options.disableCallbackRetry !== true &&
|
|
6925
|
+
deliveryAttempt <= CALLBACK_RETRY_DELAYS_MS.length
|
|
6926
|
+
? startCallbackRetryMonitor({
|
|
6927
|
+
statePath: options.statePath,
|
|
6928
|
+
delayMs: callbackRetryDelayMs
|
|
6929
|
+
})
|
|
6930
|
+
: undefined;
|
|
5493
6931
|
if (!retryingPending && !recoveringTerminalCompletion) {
|
|
5494
6932
|
appendEvent(logPath, messageEvent(message));
|
|
5495
6933
|
}
|
|
@@ -5510,7 +6948,13 @@ function runLockedCallback(options) {
|
|
|
5510
6948
|
openclaw_bin: options.openclawBin ?? conversation.openclaw_bin,
|
|
5511
6949
|
close_terminal_bridge_on_done: closeTerminalBridgeOnDone,
|
|
5512
6950
|
track_delivery: true,
|
|
5513
|
-
final_status: finalStatus
|
|
6951
|
+
final_status: finalStatus,
|
|
6952
|
+
...(callbackWatchdog
|
|
6953
|
+
? {
|
|
6954
|
+
retry_monitor_pid: callbackWatchdog.pid ?? null,
|
|
6955
|
+
next_attempt_at: new Date(Date.now() + callbackRetryDelayMs).toISOString()
|
|
6956
|
+
}
|
|
6957
|
+
: {})
|
|
5514
6958
|
},
|
|
5515
6959
|
updated_at: now
|
|
5516
6960
|
};
|
|
@@ -5524,6 +6968,18 @@ function runLockedCallback(options) {
|
|
|
5524
6968
|
message_id: message.id,
|
|
5525
6969
|
attempt: deliveryAttempt
|
|
5526
6970
|
});
|
|
6971
|
+
if (callbackWatchdog) {
|
|
6972
|
+
appendEvent(logPath, {
|
|
6973
|
+
ts: new Date().toISOString(),
|
|
6974
|
+
conversation_id: conversation.conversation_id,
|
|
6975
|
+
event: "callback_retry_monitor_launched",
|
|
6976
|
+
message_id: message.id,
|
|
6977
|
+
pid: callbackWatchdog.pid ?? null,
|
|
6978
|
+
next_attempt_at: isRecord(nextConversation.callback_delivery)
|
|
6979
|
+
? nextConversation.callback_delivery.next_attempt_at
|
|
6980
|
+
: undefined
|
|
6981
|
+
});
|
|
6982
|
+
}
|
|
5527
6983
|
}
|
|
5528
6984
|
saveState(options.statePath, nextConversation);
|
|
5529
6985
|
runtimeLog("info", "callback_received", {
|
|
@@ -5758,8 +7214,8 @@ function recordCallbackProcessDelivery({ logPath, conversation, message, event,
|
|
|
5758
7214
|
round: message.round,
|
|
5759
7215
|
...detail,
|
|
5760
7216
|
status: delivery.status,
|
|
5761
|
-
stdout: delivery.stdout,
|
|
5762
|
-
stderr: delivery.stderr
|
|
7217
|
+
stdout: redactString(delivery.stdout),
|
|
7218
|
+
stderr: redactString(delivery.stderr)
|
|
5763
7219
|
});
|
|
5764
7220
|
runtimeLog("info", runtimeEvent, {
|
|
5765
7221
|
conversation_id: conversation.conversation_id,
|
|
@@ -5772,26 +7228,37 @@ function recordCallbackProcessDelivery({ logPath, conversation, message, event,
|
|
|
5772
7228
|
}
|
|
5773
7229
|
function acquireFileLock(lockPath, { timeoutMs = 5000, retryMs = 50 } = {}) {
|
|
5774
7230
|
const started = Date.now();
|
|
7231
|
+
const token = randomUUID();
|
|
5775
7232
|
while (true) {
|
|
7233
|
+
let fd;
|
|
5776
7234
|
try {
|
|
5777
|
-
|
|
5778
|
-
|
|
7235
|
+
fd = fs.openSync(lockPath, fs.constants.O_CREAT |
|
|
7236
|
+
fs.constants.O_EXCL |
|
|
7237
|
+
fs.constants.O_WRONLY |
|
|
7238
|
+
NO_FOLLOW_FLAG, PRIVATE_LOCK_FILE_MODE);
|
|
7239
|
+
fs.fchmodSync(fd, PRIVATE_LOCK_FILE_MODE);
|
|
7240
|
+
fs.writeFileSync(fd, `${JSON.stringify({
|
|
7241
|
+
pid: process.pid,
|
|
7242
|
+
token,
|
|
7243
|
+
created_at: new Date().toISOString()
|
|
7244
|
+
})}\n`, "utf8");
|
|
5779
7245
|
fs.fsyncSync(fd);
|
|
5780
7246
|
fs.closeSync(fd);
|
|
5781
|
-
|
|
5782
|
-
|
|
5783
|
-
};
|
|
7247
|
+
fd = undefined;
|
|
7248
|
+
return () => releaseFileLock(lockPath, token);
|
|
5784
7249
|
}
|
|
5785
7250
|
catch (error) {
|
|
5786
|
-
if (
|
|
7251
|
+
if (fd !== undefined) {
|
|
7252
|
+
fs.closeSync(fd);
|
|
7253
|
+
}
|
|
7254
|
+
if (!isRecord(error) || error.code !== "EEXIST") {
|
|
5787
7255
|
throw error;
|
|
5788
7256
|
}
|
|
5789
|
-
if (
|
|
5790
|
-
fs.rmSync(lockPath, { force: true });
|
|
7257
|
+
if (reclaimStaleFileLock(lockPath)) {
|
|
5791
7258
|
continue;
|
|
5792
7259
|
}
|
|
5793
7260
|
if (Date.now() - started >= timeoutMs) {
|
|
5794
|
-
throw new Error(`timed out waiting for file lock: ${lockPath}`);
|
|
7261
|
+
throw Object.assign(new Error(`timed out waiting for file lock: ${lockPath}`), { code: "LOCK_TIMEOUT" });
|
|
5795
7262
|
}
|
|
5796
7263
|
sleepSync(retryMs);
|
|
5797
7264
|
}
|
|
@@ -5799,12 +7266,14 @@ function acquireFileLock(lockPath, { timeoutMs = 5000, retryMs = 50 } = {}) {
|
|
|
5799
7266
|
}
|
|
5800
7267
|
function staleFileLock(lockPath) {
|
|
5801
7268
|
try {
|
|
5802
|
-
const stat = fs.
|
|
5803
|
-
|
|
5804
|
-
|
|
5805
|
-
|
|
7269
|
+
const stat = fs.lstatSync(lockPath);
|
|
7270
|
+
if (stat.isSymbolicLink() || !stat.isFile()) {
|
|
7271
|
+
throw new Error(`file lock must be a regular file, not a symlink: ${lockPath}`);
|
|
7272
|
+
}
|
|
7273
|
+
const owner = readFileLockOwner(lockPath);
|
|
7274
|
+
if (owner.pid !== undefined) {
|
|
5806
7275
|
try {
|
|
5807
|
-
process.kill(
|
|
7276
|
+
process.kill(owner.pid, 0);
|
|
5808
7277
|
return false;
|
|
5809
7278
|
}
|
|
5810
7279
|
catch (error) {
|
|
@@ -5817,6 +7286,91 @@ function staleFileLock(lockPath) {
|
|
|
5817
7286
|
return isRecord(error) && error.code === "ENOENT";
|
|
5818
7287
|
}
|
|
5819
7288
|
}
|
|
7289
|
+
function reclaimStaleFileLock(lockPath) {
|
|
7290
|
+
const reclaimPath = `${lockPath}.reclaim`;
|
|
7291
|
+
let reclaimFd;
|
|
7292
|
+
try {
|
|
7293
|
+
reclaimFd = fs.openSync(reclaimPath, fs.constants.O_CREAT |
|
|
7294
|
+
fs.constants.O_EXCL |
|
|
7295
|
+
fs.constants.O_WRONLY |
|
|
7296
|
+
NO_FOLLOW_FLAG, PRIVATE_LOCK_FILE_MODE);
|
|
7297
|
+
fs.fchmodSync(reclaimFd, PRIVATE_LOCK_FILE_MODE);
|
|
7298
|
+
fs.writeFileSync(reclaimFd, `${process.pid}\n`, "utf8");
|
|
7299
|
+
fs.fsyncSync(reclaimFd);
|
|
7300
|
+
}
|
|
7301
|
+
catch (error) {
|
|
7302
|
+
if (reclaimFd !== undefined) {
|
|
7303
|
+
fs.closeSync(reclaimFd);
|
|
7304
|
+
}
|
|
7305
|
+
if (isRecord(error) && error.code === "EEXIST") {
|
|
7306
|
+
return false;
|
|
7307
|
+
}
|
|
7308
|
+
throw error;
|
|
7309
|
+
}
|
|
7310
|
+
try {
|
|
7311
|
+
if (!staleFileLock(lockPath)) {
|
|
7312
|
+
return false;
|
|
7313
|
+
}
|
|
7314
|
+
try {
|
|
7315
|
+
fs.unlinkSync(lockPath);
|
|
7316
|
+
return true;
|
|
7317
|
+
}
|
|
7318
|
+
catch (error) {
|
|
7319
|
+
return isRecord(error) && error.code === "ENOENT";
|
|
7320
|
+
}
|
|
7321
|
+
}
|
|
7322
|
+
finally {
|
|
7323
|
+
fs.closeSync(reclaimFd);
|
|
7324
|
+
try {
|
|
7325
|
+
fs.unlinkSync(reclaimPath);
|
|
7326
|
+
}
|
|
7327
|
+
catch (error) {
|
|
7328
|
+
if (!isRecord(error) || error.code !== "ENOENT") {
|
|
7329
|
+
throw error;
|
|
7330
|
+
}
|
|
7331
|
+
}
|
|
7332
|
+
}
|
|
7333
|
+
}
|
|
7334
|
+
function releaseFileLock(lockPath, token) {
|
|
7335
|
+
try {
|
|
7336
|
+
if (readFileLockOwner(lockPath).token !== token) {
|
|
7337
|
+
return;
|
|
7338
|
+
}
|
|
7339
|
+
fs.unlinkSync(lockPath);
|
|
7340
|
+
}
|
|
7341
|
+
catch (error) {
|
|
7342
|
+
if (!isRecord(error) || error.code !== "ENOENT") {
|
|
7343
|
+
throw error;
|
|
7344
|
+
}
|
|
7345
|
+
}
|
|
7346
|
+
}
|
|
7347
|
+
function readFileLockOwner(lockPath) {
|
|
7348
|
+
try {
|
|
7349
|
+
const text = fs.readFileSync(lockPath, "utf8").trim();
|
|
7350
|
+
try {
|
|
7351
|
+
const owner = JSON.parse(text);
|
|
7352
|
+
if (isRecord(owner)) {
|
|
7353
|
+
const pid = Number(owner.pid);
|
|
7354
|
+
return {
|
|
7355
|
+
pid: Number.isSafeInteger(pid) && pid > 1 ? pid : undefined,
|
|
7356
|
+
token: stringValue(owner.token)
|
|
7357
|
+
};
|
|
7358
|
+
}
|
|
7359
|
+
}
|
|
7360
|
+
catch {
|
|
7361
|
+
// Legacy locks contained only the owner PID.
|
|
7362
|
+
}
|
|
7363
|
+
const legacyPid = Number(text);
|
|
7364
|
+
return {
|
|
7365
|
+
pid: Number.isSafeInteger(legacyPid) && legacyPid > 1
|
|
7366
|
+
? legacyPid
|
|
7367
|
+
: undefined
|
|
7368
|
+
};
|
|
7369
|
+
}
|
|
7370
|
+
catch {
|
|
7371
|
+
return {};
|
|
7372
|
+
}
|
|
7373
|
+
}
|
|
5820
7374
|
function sleepSync(ms) {
|
|
5821
7375
|
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
5822
7376
|
}
|
|
@@ -6594,8 +8148,8 @@ function deliverStalledNotification({ statePath, logPath, conversation, message,
|
|
|
6594
8148
|
method: conversation.gateway_method,
|
|
6595
8149
|
message_id: message.id,
|
|
6596
8150
|
status: delivery.status,
|
|
6597
|
-
stdout: delivery.stdout,
|
|
6598
|
-
stderr: delivery.stderr
|
|
8151
|
+
stdout: redactString(delivery.stdout),
|
|
8152
|
+
stderr: redactString(delivery.stderr)
|
|
6599
8153
|
});
|
|
6600
8154
|
runtimeLog("info", `${eventPrefix}_gateway_method_delivery`, {
|
|
6601
8155
|
conversation_id: conversation.conversation_id,
|
|
@@ -6626,8 +8180,8 @@ function deliverStalledNotification({ statePath, logPath, conversation, message,
|
|
|
6626
8180
|
event: `${eventPrefix}_chat_send_delivery`,
|
|
6627
8181
|
message_id: message.id,
|
|
6628
8182
|
status: chatSendDelivery.status,
|
|
6629
|
-
stdout: chatSendDelivery.stdout,
|
|
6630
|
-
stderr: chatSendDelivery.stderr
|
|
8183
|
+
stdout: redactString(chatSendDelivery.stdout),
|
|
8184
|
+
stderr: redactString(chatSendDelivery.stderr)
|
|
6631
8185
|
});
|
|
6632
8186
|
runtimeLog("info", `${eventPrefix}_chat_send_delivery`, {
|
|
6633
8187
|
conversation_id: conversation.conversation_id,
|
|
@@ -6653,54 +8207,84 @@ function cleanupIdleConversations(storeDir, options = {}, now = new Date()) {
|
|
|
6653
8207
|
}
|
|
6654
8208
|
const conversations = listConversations(storeDir);
|
|
6655
8209
|
let closed = 0;
|
|
6656
|
-
for (const
|
|
6657
|
-
if (
|
|
8210
|
+
for (const listedConversation of conversations) {
|
|
8211
|
+
if (listedConversation.status !== "idle" || !listedConversation.idle_since) {
|
|
6658
8212
|
continue;
|
|
6659
8213
|
}
|
|
6660
|
-
const
|
|
6661
|
-
if (!Number.isFinite(
|
|
8214
|
+
const listedIdleSinceMs = Date.parse(listedConversation.idle_since);
|
|
8215
|
+
if (!Number.isFinite(listedIdleSinceMs)) {
|
|
6662
8216
|
continue;
|
|
6663
8217
|
}
|
|
6664
|
-
const
|
|
6665
|
-
isRecord(
|
|
6666
|
-
typeof
|
|
6667
|
-
if (!
|
|
8218
|
+
const listedTerminalBridge = terminalBridgeEnabled(listedConversation) &&
|
|
8219
|
+
isRecord(listedConversation.native_session_takeover) &&
|
|
8220
|
+
typeof listedConversation.native_session_takeover.terminal_bridge_message_id === "string";
|
|
8221
|
+
if (!listedTerminalBridge && now.getTime() - listedIdleSinceMs < timeoutMinutes * 60 * 1000) {
|
|
6668
8222
|
continue;
|
|
6669
8223
|
}
|
|
6670
|
-
const statePath =
|
|
6671
|
-
|
|
6672
|
-
|
|
6673
|
-
|
|
6674
|
-
|
|
6675
|
-
|
|
6676
|
-
|
|
6677
|
-
|
|
6678
|
-
|
|
6679
|
-
|
|
6680
|
-
|
|
6681
|
-
}
|
|
6682
|
-
|
|
6683
|
-
|
|
6684
|
-
|
|
6685
|
-
|
|
6686
|
-
|
|
6687
|
-
|
|
6688
|
-
|
|
6689
|
-
|
|
6690
|
-
|
|
6691
|
-
|
|
6692
|
-
|
|
6693
|
-
|
|
6694
|
-
|
|
6695
|
-
|
|
6696
|
-
|
|
6697
|
-
|
|
6698
|
-
|
|
6699
|
-
|
|
6700
|
-
|
|
6701
|
-
|
|
6702
|
-
|
|
6703
|
-
|
|
8224
|
+
const statePath = listedConversation.state_path ??
|
|
8225
|
+
statePathForConversationId(listedConversation.conversation_id, storeDir);
|
|
8226
|
+
let releaseStateLock;
|
|
8227
|
+
try {
|
|
8228
|
+
releaseStateLock = acquireFileLock(`${statePath}.lock`);
|
|
8229
|
+
}
|
|
8230
|
+
catch (error) {
|
|
8231
|
+
if (isRecord(error) && error.code === "LOCK_TIMEOUT") {
|
|
8232
|
+
continue;
|
|
8233
|
+
}
|
|
8234
|
+
throw error;
|
|
8235
|
+
}
|
|
8236
|
+
try {
|
|
8237
|
+
const conversation = loadState(statePath);
|
|
8238
|
+
if (conversation.status !== "idle" || !conversation.idle_since) {
|
|
8239
|
+
continue;
|
|
8240
|
+
}
|
|
8241
|
+
const idleSinceMs = Date.parse(conversation.idle_since);
|
|
8242
|
+
if (!Number.isFinite(idleSinceMs)) {
|
|
8243
|
+
continue;
|
|
8244
|
+
}
|
|
8245
|
+
const terminalBridge = terminalBridgeEnabled(conversation) &&
|
|
8246
|
+
isRecord(conversation.native_session_takeover) &&
|
|
8247
|
+
typeof conversation.native_session_takeover.terminal_bridge_message_id === "string";
|
|
8248
|
+
if (!terminalBridge && now.getTime() - idleSinceMs < timeoutMinutes * 60 * 1000) {
|
|
8249
|
+
continue;
|
|
8250
|
+
}
|
|
8251
|
+
const logPath = conversation.event_log_path ?? logPathForStatePath(statePath);
|
|
8252
|
+
const closeReason = terminalBridge
|
|
8253
|
+
? "terminal bridge task completed"
|
|
8254
|
+
: `idle timeout after ${timeoutMinutes} minutes`;
|
|
8255
|
+
const closedConversation = {
|
|
8256
|
+
...conversation,
|
|
8257
|
+
status: "closed",
|
|
8258
|
+
closed_at: now.toISOString(),
|
|
8259
|
+
close_reason: closeReason,
|
|
8260
|
+
updated_at: now.toISOString()
|
|
8261
|
+
};
|
|
8262
|
+
delete closedConversation.idle_since;
|
|
8263
|
+
saveState(statePath, closedConversation);
|
|
8264
|
+
appendEvent(logPath, {
|
|
8265
|
+
ts: now.toISOString(),
|
|
8266
|
+
conversation_id: conversation.conversation_id,
|
|
8267
|
+
event: "conversation_closed",
|
|
8268
|
+
status: "closed",
|
|
8269
|
+
reason: closedConversation.close_reason,
|
|
8270
|
+
idle_timeout_minutes: timeoutMinutes,
|
|
8271
|
+
terminal_bridge: terminalBridge
|
|
8272
|
+
});
|
|
8273
|
+
runtimeLog("info", "idle_conversation_closed", {
|
|
8274
|
+
conversation_id: conversation.conversation_id,
|
|
8275
|
+
agent: executorForConversation(conversation).kind,
|
|
8276
|
+
executor_session: executorForConversation(conversation).session,
|
|
8277
|
+
state_path: statePath,
|
|
8278
|
+
event_log_path: logPath,
|
|
8279
|
+
idle_since: conversation.idle_since,
|
|
8280
|
+
idle_timeout_minutes: timeoutMinutes,
|
|
8281
|
+
reason: closedConversation.close_reason
|
|
8282
|
+
});
|
|
8283
|
+
closed += 1;
|
|
8284
|
+
}
|
|
8285
|
+
finally {
|
|
8286
|
+
releaseStateLock();
|
|
8287
|
+
}
|
|
6704
8288
|
}
|
|
6705
8289
|
return {
|
|
6706
8290
|
checked: conversations.length,
|
|
@@ -6731,10 +8315,13 @@ function messageFingerprint(message) {
|
|
|
6731
8315
|
});
|
|
6732
8316
|
}
|
|
6733
8317
|
function deliverToOpenClaw({ gatewayUrl, token, openclawSession, message }) {
|
|
6734
|
-
const agent = `openclaw acp --url ${gatewayUrl} --
|
|
8318
|
+
const agent = `openclaw acp --url ${gatewayUrl} --session ${openclawSession}`;
|
|
6735
8319
|
const result = spawnSync("acpx", ["--agent", agent, JSON.stringify(message)], {
|
|
6736
8320
|
encoding: "utf8",
|
|
6737
|
-
maxBuffer: 1024 * 1024 * 10
|
|
8321
|
+
maxBuffer: 1024 * 1024 * 10,
|
|
8322
|
+
timeout: CALLBACK_DELIVERY_TIMEOUT_MS,
|
|
8323
|
+
killSignal: "SIGKILL",
|
|
8324
|
+
env: openClawGatewayEnvironment(token)
|
|
6738
8325
|
});
|
|
6739
8326
|
if (result.error) {
|
|
6740
8327
|
return {
|
|
@@ -6759,7 +8346,7 @@ function deliverToGatewayMethod({ method, openclawBin, gatewayUrl, token, sessio
|
|
|
6759
8346
|
sessionKey,
|
|
6760
8347
|
statePath,
|
|
6761
8348
|
logPath,
|
|
6762
|
-
conversation,
|
|
8349
|
+
conversation: redactCliOutput(conversation),
|
|
6763
8350
|
message
|
|
6764
8351
|
}),
|
|
6765
8352
|
"--json"
|
|
@@ -6767,12 +8354,12 @@ function deliverToGatewayMethod({ method, openclawBin, gatewayUrl, token, sessio
|
|
|
6767
8354
|
if (gatewayUrl) {
|
|
6768
8355
|
args.push("--url", gatewayUrl);
|
|
6769
8356
|
}
|
|
6770
|
-
if (token && token !== "<token>") {
|
|
6771
|
-
args.push("--token", token);
|
|
6772
|
-
}
|
|
6773
8357
|
const result = spawnSync(openclawBin ?? "openclaw", args, {
|
|
6774
8358
|
encoding: "utf8",
|
|
6775
|
-
maxBuffer: 1024 * 1024 * 10
|
|
8359
|
+
maxBuffer: 1024 * 1024 * 10,
|
|
8360
|
+
timeout: CALLBACK_DELIVERY_TIMEOUT_MS,
|
|
8361
|
+
killSignal: "SIGKILL",
|
|
8362
|
+
env: openClawGatewayEnvironment(token)
|
|
6776
8363
|
});
|
|
6777
8364
|
if (result.error) {
|
|
6778
8365
|
return {
|
|
@@ -6799,12 +8386,12 @@ function deliverToSessionSend({ openclawBin, gatewayUrl, token, params }) {
|
|
|
6799
8386
|
if (gatewayUrl) {
|
|
6800
8387
|
args.push("--url", gatewayUrl);
|
|
6801
8388
|
}
|
|
6802
|
-
if (token && token !== "<token>") {
|
|
6803
|
-
args.push("--token", token);
|
|
6804
|
-
}
|
|
6805
8389
|
const result = spawnSync(openclawBin ?? "openclaw", args, {
|
|
6806
8390
|
encoding: "utf8",
|
|
6807
|
-
maxBuffer: 1024 * 1024 * 10
|
|
8391
|
+
maxBuffer: 1024 * 1024 * 10,
|
|
8392
|
+
timeout: CALLBACK_DELIVERY_TIMEOUT_MS,
|
|
8393
|
+
killSignal: "SIGKILL",
|
|
8394
|
+
env: openClawGatewayEnvironment(token)
|
|
6808
8395
|
});
|
|
6809
8396
|
if (result.error) {
|
|
6810
8397
|
return {
|
|
@@ -6831,12 +8418,12 @@ function deliverToChatSend({ openclawBin, gatewayUrl, token, params }) {
|
|
|
6831
8418
|
if (gatewayUrl) {
|
|
6832
8419
|
args.push("--url", gatewayUrl);
|
|
6833
8420
|
}
|
|
6834
|
-
if (token && token !== "<token>") {
|
|
6835
|
-
args.push("--token", token);
|
|
6836
|
-
}
|
|
6837
8421
|
const result = spawnSync(openclawBin ?? "openclaw", args, {
|
|
6838
8422
|
encoding: "utf8",
|
|
6839
|
-
maxBuffer: 1024 * 1024 * 10
|
|
8423
|
+
maxBuffer: 1024 * 1024 * 10,
|
|
8424
|
+
timeout: CALLBACK_DELIVERY_TIMEOUT_MS,
|
|
8425
|
+
killSignal: "SIGKILL",
|
|
8426
|
+
env: openClawGatewayEnvironment(token)
|
|
6840
8427
|
});
|
|
6841
8428
|
if (result.error) {
|
|
6842
8429
|
return {
|
|
@@ -6851,6 +8438,15 @@ function deliverToChatSend({ openclawBin, gatewayUrl, token, params }) {
|
|
|
6851
8438
|
stderr: result.stderr ?? ""
|
|
6852
8439
|
};
|
|
6853
8440
|
}
|
|
8441
|
+
function openClawGatewayEnvironment(token) {
|
|
8442
|
+
if (!token || token === "<token>") {
|
|
8443
|
+
return process.env;
|
|
8444
|
+
}
|
|
8445
|
+
return {
|
|
8446
|
+
...process.env,
|
|
8447
|
+
OPENCLAW_GATEWAY_TOKEN: token
|
|
8448
|
+
};
|
|
8449
|
+
}
|
|
6854
8450
|
function captureJson(argv) {
|
|
6855
8451
|
const result = spawnSync(process.execPath, [new URL(import.meta.url).pathname, ...argv], {
|
|
6856
8452
|
encoding: "utf8"
|
|
@@ -6938,7 +8534,30 @@ function expandHome(filePath) {
|
|
|
6938
8534
|
return filePath;
|
|
6939
8535
|
}
|
|
6940
8536
|
function printJson(value) {
|
|
6941
|
-
process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
|
|
8537
|
+
process.stdout.write(`${JSON.stringify(redactCliOutput(value), null, 2)}\n`);
|
|
8538
|
+
}
|
|
8539
|
+
function redactCliOutput(value) {
|
|
8540
|
+
if (Array.isArray(value)) {
|
|
8541
|
+
return value.map((item) => redactCliOutput(item));
|
|
8542
|
+
}
|
|
8543
|
+
if (isRecord(value)) {
|
|
8544
|
+
return Object.fromEntries(Object.entries(value).flatMap(([key, item]) => {
|
|
8545
|
+
if (key === "gateway_token" || key === "gatewayToken") {
|
|
8546
|
+
return [];
|
|
8547
|
+
}
|
|
8548
|
+
if (key === "claude_transcript_anchor" ||
|
|
8549
|
+
key === "claudeTranscriptAnchor" ||
|
|
8550
|
+
key === "claude_home" ||
|
|
8551
|
+
key === "claudeHome") {
|
|
8552
|
+
return [];
|
|
8553
|
+
}
|
|
8554
|
+
if ((key === "callback_command" || key === "callbackCommand") && typeof item === "string") {
|
|
8555
|
+
return [[key, redactString(item)]];
|
|
8556
|
+
}
|
|
8557
|
+
return [[key, redactCliOutput(item)]];
|
|
8558
|
+
}));
|
|
8559
|
+
}
|
|
8560
|
+
return value;
|
|
6942
8561
|
}
|
|
6943
8562
|
function cleanProcessText(text) {
|
|
6944
8563
|
const value = String(text ?? "").trim();
|
|
@@ -7004,6 +8623,24 @@ function readOutputTail(outputPath, maxBytes = 65536) {
|
|
|
7004
8623
|
return "";
|
|
7005
8624
|
}
|
|
7006
8625
|
}
|
|
8626
|
+
function openPrivateAppendFile(filePath) {
|
|
8627
|
+
if (fs.existsSync(filePath) && fs.lstatSync(filePath).isSymbolicLink()) {
|
|
8628
|
+
throw new Error(`refusing agent output symlink: ${filePath}`);
|
|
8629
|
+
}
|
|
8630
|
+
const noFollow = typeof fs.constants.O_NOFOLLOW === "number"
|
|
8631
|
+
? fs.constants.O_NOFOLLOW
|
|
8632
|
+
: 0;
|
|
8633
|
+
const descriptor = fs.openSync(filePath, fs.constants.O_CREAT |
|
|
8634
|
+
fs.constants.O_APPEND |
|
|
8635
|
+
fs.constants.O_WRONLY |
|
|
8636
|
+
noFollow, 0o600);
|
|
8637
|
+
if (!fs.fstatSync(descriptor).isFile()) {
|
|
8638
|
+
fs.closeSync(descriptor);
|
|
8639
|
+
throw new Error(`agent output must be a regular file: ${filePath}`);
|
|
8640
|
+
}
|
|
8641
|
+
fs.fchmodSync(descriptor, 0o600);
|
|
8642
|
+
return descriptor;
|
|
8643
|
+
}
|
|
7007
8644
|
function detectModelSelectionError(text) {
|
|
7008
8645
|
const cleaned = cleanProcessText(text);
|
|
7009
8646
|
if (!cleaned) {
|
|
@@ -7077,6 +8714,8 @@ function withStoragePaths(conversation, paths) {
|
|
|
7077
8714
|
function usage() {
|
|
7078
8715
|
const agentList = EXECUTOR_KINDS.join("|");
|
|
7079
8716
|
process.stdout.write(`Usage:
|
|
8717
|
+
agent-knock-knock --help
|
|
8718
|
+
agent-knock-knock --version
|
|
7080
8719
|
agent-knock-knock new --request <text> [--agent ${agentList}] [--workspace <path>] [--store-dir <dir>]
|
|
7081
8720
|
agent-knock-knock record --state <file> --message-json <json>
|
|
7082
8721
|
agent-knock-knock bootstrap-prompt --callback-command <command> [--agent ${agentList}]
|
|
@@ -7088,11 +8727,11 @@ function usage() {
|
|
|
7088
8727
|
agent-knock-knock approve --conversation <id>
|
|
7089
8728
|
agent-knock-knock cancel --conversation <id> [--all-proxy <url>]
|
|
7090
8729
|
agent-knock-knock renew --conversation <id> [--minutes <inactivity-minutes>]
|
|
8730
|
+
agent-knock-knock reconcile-monitors [--store-dir <dir>]
|
|
7091
8731
|
agent-knock-knock retry-callback --conversation <id> [--store-dir <dir>]
|
|
7092
8732
|
agent-knock-knock recover --conversation <id> [--session <name>] [--all-proxy <url>]
|
|
7093
8733
|
agent-knock-knock close --conversation <id> [--reason <text>]
|
|
7094
8734
|
agent-knock-knock install-openclaw [--openclaw-bin <path>] [--skill-path <path>] [--skill-only] [--no-restart]
|
|
7095
|
-
agent-knock-knock install-claude-hooks [--settings-path <path>] [--executable-path <path>] [--dry-run]
|
|
7096
8735
|
agent-knock-knock doctor
|
|
7097
8736
|
agent-knock-knock agent takeover --agent codex --session-id <id> --strategy terminate_then_resume|terminal_control|fork [--create-conversation]
|
|
7098
8737
|
agent-knock-knock callback --state <file> --message-json <json> [--record-only]
|