@scotthuang/agent-knock-knock 0.1.0
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 +104 -0
- package/LICENSE +21 -0
- package/README.md +397 -0
- package/dist/src/acpx-output.d.ts +18 -0
- package/dist/src/acpx-output.js +223 -0
- package/dist/src/acpx-output.js.map +1 -0
- package/dist/src/bootstrap.d.ts +8 -0
- package/dist/src/bootstrap.js +45 -0
- package/dist/src/bootstrap.js.map +1 -0
- package/dist/src/cli.d.ts +2 -0
- package/dist/src/cli.js +2364 -0
- package/dist/src/cli.js.map +1 -0
- package/dist/src/executors.d.ts +101 -0
- package/dist/src/executors.js +118 -0
- package/dist/src/executors.js.map +1 -0
- package/dist/src/openclaw-plugin.d.ts +7 -0
- package/dist/src/openclaw-plugin.js +916 -0
- package/dist/src/openclaw-plugin.js.map +1 -0
- package/dist/src/protocol.d.ts +96 -0
- package/dist/src/protocol.js +329 -0
- package/dist/src/protocol.js.map +1 -0
- package/dist/src/runtime-log.d.ts +37 -0
- package/dist/src/runtime-log.js +149 -0
- package/dist/src/runtime-log.js.map +1 -0
- package/dist/src/store.d.ts +35 -0
- package/dist/src/store.js +121 -0
- package/dist/src/store.js.map +1 -0
- package/dist/src/transcript.d.ts +24 -0
- package/dist/src/transcript.js +88 -0
- package/dist/src/transcript.js.map +1 -0
- package/openclaw.plugin.json +150 -0
- package/package.json +63 -0
- package/templates/openclaw-skills/agent-knock-knock/SKILL.md +181 -0
package/dist/src/cli.js
ADDED
|
@@ -0,0 +1,2364 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
4
|
+
import fs from "node:fs";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import process from "node:process";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
import { applyMessageToConversation, budgetAction, createConversation, createMessage, executorForConversation, extractStructuredMessage, parseMessageJson, resolveExecutor } from "./protocol.js";
|
|
9
|
+
import { EXECUTOR_KINDS, acpxCommandForExecutor, executorDefinitionForKind, modelEnvForExecutor, proxyEnvForExecutor, sessionRecoveryStrategyForExecutor } from "./executors.js";
|
|
10
|
+
import { executorBootstrapPrompt } from "./bootstrap.js";
|
|
11
|
+
import { writeRuntimeLog } from "./runtime-log.js";
|
|
12
|
+
import { formatTranscript, readNdjsonLog } from "./transcript.js";
|
|
13
|
+
import { appendEvent, defaultStoreDir, listConversations, logPathForStatePath, loadConversationById, loadState, messageEvent, pathsForConversation, pathsForConversationDir, saveState, statePathForConversationId } from "./store.js";
|
|
14
|
+
const DEFAULT_IDLE_TIMEOUT_MINUTES = 10080;
|
|
15
|
+
const DEFAULT_AGENT_TIMEOUT_MINUTES = 60;
|
|
16
|
+
const DEFAULT_MONITOR_POLL_INTERVAL_MS = 5000;
|
|
17
|
+
const command = process.argv[2];
|
|
18
|
+
const args = parseArgs(process.argv.slice(3));
|
|
19
|
+
runtimeLog("info", "cli_start", {
|
|
20
|
+
command: command ?? "help",
|
|
21
|
+
cwd: process.cwd(),
|
|
22
|
+
option_keys: Object.keys(args).sort()
|
|
23
|
+
});
|
|
24
|
+
try {
|
|
25
|
+
runCommand(command, args);
|
|
26
|
+
runtimeLog("info", "cli_finish", {
|
|
27
|
+
command: command ?? "help",
|
|
28
|
+
exit_code: process.exitCode ?? 0
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
runtimeLog("error", "cli_error", {
|
|
33
|
+
command: command ?? "help",
|
|
34
|
+
message: error.message,
|
|
35
|
+
stack: error.stack
|
|
36
|
+
});
|
|
37
|
+
console.error(error.message);
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
function runCommand(commandName, options) {
|
|
41
|
+
if (commandName === "new") {
|
|
42
|
+
runNew(options);
|
|
43
|
+
}
|
|
44
|
+
else if (commandName === "record") {
|
|
45
|
+
runRecord(options);
|
|
46
|
+
}
|
|
47
|
+
else if (commandName === "bootstrap-prompt") {
|
|
48
|
+
runBootstrapPrompt(options);
|
|
49
|
+
}
|
|
50
|
+
else if (commandName === "delegate") {
|
|
51
|
+
runDelegate(options);
|
|
52
|
+
}
|
|
53
|
+
else if (commandName === "list") {
|
|
54
|
+
runList(options);
|
|
55
|
+
}
|
|
56
|
+
else if (commandName === "status") {
|
|
57
|
+
runStatus(options);
|
|
58
|
+
}
|
|
59
|
+
else if (commandName === "send") {
|
|
60
|
+
runSend(options);
|
|
61
|
+
}
|
|
62
|
+
else if (commandName === "cancel") {
|
|
63
|
+
runCancel(options);
|
|
64
|
+
}
|
|
65
|
+
else if (commandName === "recover") {
|
|
66
|
+
runRecover(options);
|
|
67
|
+
}
|
|
68
|
+
else if (commandName === "restart") {
|
|
69
|
+
runRestart(options);
|
|
70
|
+
}
|
|
71
|
+
else if (commandName === "close") {
|
|
72
|
+
runClose(options);
|
|
73
|
+
}
|
|
74
|
+
else if (commandName === "transcript") {
|
|
75
|
+
runTranscript(options);
|
|
76
|
+
}
|
|
77
|
+
else if (commandName === "install-openclaw") {
|
|
78
|
+
runInstallOpenClaw(options);
|
|
79
|
+
}
|
|
80
|
+
else if (commandName === "doctor") {
|
|
81
|
+
runDoctor(options);
|
|
82
|
+
}
|
|
83
|
+
else if (commandName === "callback") {
|
|
84
|
+
runCallback(options);
|
|
85
|
+
}
|
|
86
|
+
else if (commandName === "monitor") {
|
|
87
|
+
runMonitor(options);
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
usage();
|
|
91
|
+
process.exitCode = commandName ? 1 : 0;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
function runInstallOpenClaw(options) {
|
|
95
|
+
const root = packageRootDir();
|
|
96
|
+
const openclawBin = options.openclawBin ?? resolveExecutable("openclaw");
|
|
97
|
+
const skillSource = path.join(root, "templates", "openclaw-skills", "agent-knock-knock", "SKILL.md");
|
|
98
|
+
const skillDest = expandHome(options.skillPath ?? "~/.openclaw/skills/agent-knock-knock/SKILL.md");
|
|
99
|
+
const steps = [];
|
|
100
|
+
runCheckedCommand(openclawBin, ["plugins", "install", "--link", root], {
|
|
101
|
+
label: "openclaw plugins install"
|
|
102
|
+
});
|
|
103
|
+
steps.push({
|
|
104
|
+
name: "plugin_installed",
|
|
105
|
+
path: root
|
|
106
|
+
});
|
|
107
|
+
runCheckedCommand(openclawBin, ["plugins", "enable", "agent-knock-knock"], {
|
|
108
|
+
label: "openclaw plugins enable"
|
|
109
|
+
});
|
|
110
|
+
steps.push({
|
|
111
|
+
name: "plugin_enabled",
|
|
112
|
+
plugin: "agent-knock-knock"
|
|
113
|
+
});
|
|
114
|
+
fs.mkdirSync(path.dirname(skillDest), { recursive: true });
|
|
115
|
+
fs.copyFileSync(skillSource, skillDest);
|
|
116
|
+
steps.push({
|
|
117
|
+
name: "skill_installed",
|
|
118
|
+
path: skillDest
|
|
119
|
+
});
|
|
120
|
+
if (options.noRestart !== true) {
|
|
121
|
+
runCheckedCommand(openclawBin, ["gateway", "restart"], {
|
|
122
|
+
label: "openclaw gateway restart"
|
|
123
|
+
});
|
|
124
|
+
steps.push({
|
|
125
|
+
name: "gateway_restarted"
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
printJson({
|
|
129
|
+
installed: true,
|
|
130
|
+
package_root: root,
|
|
131
|
+
openclaw_bin: openclawBin,
|
|
132
|
+
steps,
|
|
133
|
+
next: options.noRestart === true
|
|
134
|
+
? "Restart the OpenClaw Gateway before using Agent Knock Knock."
|
|
135
|
+
: "Agent Knock Knock is installed. Try: AKK list"
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
function runDoctor(options) {
|
|
139
|
+
const commands = ["node", "openclaw", "acpx", "codex", "claude", "cursor"];
|
|
140
|
+
const checks = commands.map((commandName) => executableCheck(commandName));
|
|
141
|
+
const root = packageRootDir();
|
|
142
|
+
const packageFiles = [
|
|
143
|
+
"dist/src/cli.js",
|
|
144
|
+
"dist/src/openclaw-plugin.js",
|
|
145
|
+
"templates/openclaw-skills/agent-knock-knock/SKILL.md",
|
|
146
|
+
"openclaw.plugin.json"
|
|
147
|
+
].map((relativePath) => {
|
|
148
|
+
const filePath = path.join(root, relativePath);
|
|
149
|
+
return {
|
|
150
|
+
path: filePath,
|
|
151
|
+
exists: fs.existsSync(filePath)
|
|
152
|
+
};
|
|
153
|
+
});
|
|
154
|
+
const requiredOk = checks
|
|
155
|
+
.filter((check) => ["node", "openclaw", "acpx"].includes(check.command))
|
|
156
|
+
.every((check) => check.available);
|
|
157
|
+
const agentOk = checks
|
|
158
|
+
.filter((check) => ["codex", "claude", "cursor"].includes(check.command))
|
|
159
|
+
.some((check) => check.available);
|
|
160
|
+
const filesOk = packageFiles.every((check) => check.exists);
|
|
161
|
+
printJson({
|
|
162
|
+
ok: requiredOk && agentOk && filesOk,
|
|
163
|
+
package_root: root,
|
|
164
|
+
checks,
|
|
165
|
+
package_files: packageFiles,
|
|
166
|
+
notes: [
|
|
167
|
+
"node, openclaw, and acpx are required.",
|
|
168
|
+
"At least one local coding agent command should be available: codex, claude, or cursor."
|
|
169
|
+
],
|
|
170
|
+
options
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
function runNew(options) {
|
|
174
|
+
const request = required(options.request, "--request is required");
|
|
175
|
+
const workspace = options.workspace ?? process.cwd();
|
|
176
|
+
cleanupIdleConversations(expandHome(options.storeDir ?? options.logDir ?? defaultStoreDir(workspace)), options);
|
|
177
|
+
const executor = resolveExecutor({
|
|
178
|
+
kind: options.agent ?? "claude",
|
|
179
|
+
session: options.session ?? options.executorSession ?? options.claudeSession
|
|
180
|
+
});
|
|
181
|
+
const conversation = createConversation({
|
|
182
|
+
userRequest: request,
|
|
183
|
+
workspace,
|
|
184
|
+
openclawSession: options.openclawSession ?? "agent:main:main",
|
|
185
|
+
claudeSession: options.claudeSession ?? "bidirectional",
|
|
186
|
+
executorKind: executor.kind,
|
|
187
|
+
executorSession: executor.session,
|
|
188
|
+
softLimit: Number(options.softLimit ?? 50),
|
|
189
|
+
hardLimit: Number(options.hardLimit ?? 100)
|
|
190
|
+
});
|
|
191
|
+
const taskMessage = createMessage({
|
|
192
|
+
conversation,
|
|
193
|
+
from: "openclaw",
|
|
194
|
+
to: executor.actor,
|
|
195
|
+
type: "task",
|
|
196
|
+
body: request,
|
|
197
|
+
metadata: {
|
|
198
|
+
executor_kind: executor.kind,
|
|
199
|
+
executor_session: executor.session
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
const nextConversation = applyMessageToConversation(conversation, taskMessage);
|
|
203
|
+
const storeDir = expandHome(options.storeDir ?? options.logDir ?? defaultStoreDir(workspace));
|
|
204
|
+
const paths = pathsForConversation(conversation.conversation_id, storeDir);
|
|
205
|
+
const storedConversation = withStoragePaths(nextConversation, paths);
|
|
206
|
+
saveState(paths.statePath, storedConversation);
|
|
207
|
+
appendEvent(paths.logPath, {
|
|
208
|
+
ts: conversation.created_at,
|
|
209
|
+
conversation_id: conversation.conversation_id,
|
|
210
|
+
event: "conversation_created",
|
|
211
|
+
conversation: storedConversation
|
|
212
|
+
});
|
|
213
|
+
appendEvent(paths.logPath, messageEvent(taskMessage));
|
|
214
|
+
runtimeLog("info", "conversation_created", {
|
|
215
|
+
conversation_id: conversation.conversation_id,
|
|
216
|
+
agent: executor.kind,
|
|
217
|
+
executor_session: executor.session,
|
|
218
|
+
workspace,
|
|
219
|
+
store_dir: storeDir,
|
|
220
|
+
state_path: paths.statePath,
|
|
221
|
+
event_log_path: paths.logPath,
|
|
222
|
+
request: textSummary(request)
|
|
223
|
+
});
|
|
224
|
+
printJson({
|
|
225
|
+
conversation: storedConversation,
|
|
226
|
+
paths,
|
|
227
|
+
task_message: taskMessage,
|
|
228
|
+
budget: budgetAction(storedConversation)
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
function runRecord(options) {
|
|
232
|
+
const statePath = required(options.state, "--state is required");
|
|
233
|
+
const messageInput = required(options.messageJson, "--message-json is required");
|
|
234
|
+
const logPath = options.log ?? logPathForStatePath(statePath);
|
|
235
|
+
const conversation = loadState(expandHome(statePath));
|
|
236
|
+
const message = parseMessageJson(messageInput);
|
|
237
|
+
const nextConversation = applyMessageToConversation(conversation, message);
|
|
238
|
+
appendEvent(expandHome(logPath), messageEvent(message));
|
|
239
|
+
saveState(expandHome(statePath), nextConversation);
|
|
240
|
+
printJson({
|
|
241
|
+
conversation: nextConversation,
|
|
242
|
+
budget: budgetAction(nextConversation)
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
function runBootstrapPrompt(options) {
|
|
246
|
+
const callbackCommand = required(options.callbackCommand, "--callback-command is required");
|
|
247
|
+
const executor = resolveExecutor({
|
|
248
|
+
kind: options.agent ?? "claude",
|
|
249
|
+
session: options.session ?? options.claudeSession
|
|
250
|
+
});
|
|
251
|
+
process.stdout.write(executorBootstrapPrompt({
|
|
252
|
+
callbackCommand,
|
|
253
|
+
executorName: executor.display_name,
|
|
254
|
+
softLimit: Number(options.softLimit ?? 50),
|
|
255
|
+
hardLimit: Number(options.hardLimit ?? 100)
|
|
256
|
+
}));
|
|
257
|
+
}
|
|
258
|
+
function runDelegate(options) {
|
|
259
|
+
const request = required(options.request, "--request is required");
|
|
260
|
+
const workspace = options.workspace ?? process.cwd();
|
|
261
|
+
const storeDir = expandHome(options.storeDir ?? options.logDir ?? defaultStoreDir(workspace));
|
|
262
|
+
cleanupIdleConversations(storeDir, options);
|
|
263
|
+
const explicitExecutorSession = options.session ?? options.executorSession ?? options.claudeSession;
|
|
264
|
+
const executor = resolveExecutor({
|
|
265
|
+
kind: options.agent ?? "claude",
|
|
266
|
+
session: explicitExecutorSession ?? uniqueDelegateSessionName(options.agent ?? "claude")
|
|
267
|
+
});
|
|
268
|
+
const newResult = captureJson([
|
|
269
|
+
"new",
|
|
270
|
+
"--request",
|
|
271
|
+
request,
|
|
272
|
+
"--workspace",
|
|
273
|
+
workspace,
|
|
274
|
+
"--openclaw-session",
|
|
275
|
+
options.openclawSession ?? "agent:main:main",
|
|
276
|
+
"--agent",
|
|
277
|
+
executor.kind,
|
|
278
|
+
"--session",
|
|
279
|
+
executor.session,
|
|
280
|
+
"--soft-limit",
|
|
281
|
+
String(options.softLimit ?? 50),
|
|
282
|
+
"--hard-limit",
|
|
283
|
+
String(options.hardLimit ?? 100),
|
|
284
|
+
"--store-dir",
|
|
285
|
+
storeDir
|
|
286
|
+
]);
|
|
287
|
+
const gatewayUrl = options.gatewayUrl ?? "ws://127.0.0.1:18789";
|
|
288
|
+
if (options.send && !options.token) {
|
|
289
|
+
throw new Error("--token is required when using --send");
|
|
290
|
+
}
|
|
291
|
+
const openclawSession = options.openclawSession ?? "agent:main:main";
|
|
292
|
+
const openclawBin = options.openclawBin ?? resolveOptionalExecutable("openclaw");
|
|
293
|
+
const executorEnv = environmentForExecutor(executor, options);
|
|
294
|
+
const executorAllProxy = proxyForExecutor(executor, options);
|
|
295
|
+
const executorModel = modelForExecutor(executor, options);
|
|
296
|
+
const callbackCommand = options.callbackCommand
|
|
297
|
+
? expandCallbackCommandTemplate(options.callbackCommand, { statePath: newResult.paths.statePath })
|
|
298
|
+
: buildCallbackCommand({
|
|
299
|
+
statePath: newResult.paths.statePath,
|
|
300
|
+
gatewayUrl,
|
|
301
|
+
token: options.token,
|
|
302
|
+
openclawSession,
|
|
303
|
+
gatewayMethod: options.gatewayMethod,
|
|
304
|
+
gatewaySession: options.gatewaySession,
|
|
305
|
+
openclawBin
|
|
306
|
+
});
|
|
307
|
+
const conversationWithCallback = {
|
|
308
|
+
...newResult.conversation,
|
|
309
|
+
gateway_url: gatewayUrl,
|
|
310
|
+
callback_command: callbackCommand,
|
|
311
|
+
gateway_method: options.gatewayMethod,
|
|
312
|
+
gateway_session: options.gatewaySession ?? openclawSession,
|
|
313
|
+
openclaw_bin: openclawBin,
|
|
314
|
+
executor_all_proxy: executorAllProxy,
|
|
315
|
+
executor_model: executorModel
|
|
316
|
+
};
|
|
317
|
+
saveState(newResult.paths.statePath, conversationWithCallback);
|
|
318
|
+
newResult.conversation = conversationWithCallback;
|
|
319
|
+
runtimeLog("info", "delegate_created", {
|
|
320
|
+
conversation_id: newResult.conversation.conversation_id,
|
|
321
|
+
agent: executor.kind,
|
|
322
|
+
executor_session: executor.session,
|
|
323
|
+
workspace,
|
|
324
|
+
store_dir: storeDir,
|
|
325
|
+
state_path: newResult.paths.statePath,
|
|
326
|
+
gateway_method: options.gatewayMethod,
|
|
327
|
+
background: Boolean(options.background),
|
|
328
|
+
request: textSummary(request)
|
|
329
|
+
});
|
|
330
|
+
const bootstrap = executorBootstrapPrompt({
|
|
331
|
+
callbackCommand,
|
|
332
|
+
executorName: executor.display_name,
|
|
333
|
+
softLimit: Number(options.softLimit ?? 50),
|
|
334
|
+
hardLimit: Number(options.hardLimit ?? 100)
|
|
335
|
+
});
|
|
336
|
+
const payload = `${bootstrap}\n\nInitial task message:\n${JSON.stringify(newResult.task_message)}`;
|
|
337
|
+
const acpxArgs = buildAcpxPromptArgs({ executor, payload, model: executorModel });
|
|
338
|
+
if (options.background) {
|
|
339
|
+
const acpxPath = resolveExecutable("acpx");
|
|
340
|
+
const ensureSession = ensureExecutorSession({
|
|
341
|
+
acpxPath,
|
|
342
|
+
executor,
|
|
343
|
+
cwd: workspace,
|
|
344
|
+
env: executorEnv
|
|
345
|
+
});
|
|
346
|
+
appendEvent(newResult.paths.logPath, {
|
|
347
|
+
ts: new Date().toISOString(),
|
|
348
|
+
conversation_id: newResult.conversation.conversation_id,
|
|
349
|
+
event: "executor_session_ensure",
|
|
350
|
+
status: ensureSession.status ?? null,
|
|
351
|
+
executor,
|
|
352
|
+
stdout: cleanProcessText(ensureSession.stdout),
|
|
353
|
+
stderr: cleanProcessText(ensureSession.stderr)
|
|
354
|
+
});
|
|
355
|
+
runtimeLog("info", "executor_session_ensure", {
|
|
356
|
+
conversation_id: newResult.conversation.conversation_id,
|
|
357
|
+
agent: executor.kind,
|
|
358
|
+
executor_session: executor.session,
|
|
359
|
+
status: ensureSession.status ?? null,
|
|
360
|
+
failure_kind: classifyProcessFailure(ensureSession),
|
|
361
|
+
stdout: textSummary(cleanProcessText(ensureSession.stdout)),
|
|
362
|
+
stderr: textSummary(cleanProcessText(ensureSession.stderr))
|
|
363
|
+
});
|
|
364
|
+
if (executor.kind === "claude") {
|
|
365
|
+
appendEvent(newResult.paths.logPath, {
|
|
366
|
+
ts: new Date().toISOString(),
|
|
367
|
+
conversation_id: newResult.conversation.conversation_id,
|
|
368
|
+
event: "claude_session_ensure",
|
|
369
|
+
status: ensureSession.status ?? null,
|
|
370
|
+
claude_session: executor.session,
|
|
371
|
+
stdout: cleanProcessText(ensureSession.stdout),
|
|
372
|
+
stderr: cleanProcessText(ensureSession.stderr)
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
if (ensureSession.error) {
|
|
376
|
+
throw new Error(`acpx ${executor.kind} session ensure failed to start: ${ensureSession.error.message}`);
|
|
377
|
+
}
|
|
378
|
+
if (ensureSession.status !== 0) {
|
|
379
|
+
throw new Error(cleanProcessText(ensureSession.stderr || ensureSession.stdout || `acpx ${executor.kind} sessions ensure exited with status ${ensureSession.status}`));
|
|
380
|
+
}
|
|
381
|
+
const outputPath = path.join(newResult.paths.conversationDir, `${executor.kind}-output.log`);
|
|
382
|
+
const outputFd = fs.openSync(outputPath, "a");
|
|
383
|
+
const child = spawn(acpxPath, acpxArgs, {
|
|
384
|
+
detached: true,
|
|
385
|
+
stdio: ["ignore", outputFd, outputFd],
|
|
386
|
+
env: executorEnv,
|
|
387
|
+
cwd: workspace
|
|
388
|
+
});
|
|
389
|
+
child.unref();
|
|
390
|
+
fs.closeSync(outputFd);
|
|
391
|
+
appendEvent(newResult.paths.logPath, {
|
|
392
|
+
ts: new Date().toISOString(),
|
|
393
|
+
conversation_id: newResult.conversation.conversation_id,
|
|
394
|
+
event: "executor_launch",
|
|
395
|
+
mode: "background",
|
|
396
|
+
pid: child.pid ?? null,
|
|
397
|
+
executor,
|
|
398
|
+
output_path: outputPath
|
|
399
|
+
});
|
|
400
|
+
runtimeLog("info", "executor_launch", {
|
|
401
|
+
conversation_id: newResult.conversation.conversation_id,
|
|
402
|
+
agent: executor.kind,
|
|
403
|
+
executor_session: executor.session,
|
|
404
|
+
mode: "background",
|
|
405
|
+
pid: child.pid ?? null,
|
|
406
|
+
output_path: outputPath
|
|
407
|
+
});
|
|
408
|
+
if (executor.kind === "claude") {
|
|
409
|
+
appendEvent(newResult.paths.logPath, {
|
|
410
|
+
ts: new Date().toISOString(),
|
|
411
|
+
conversation_id: newResult.conversation.conversation_id,
|
|
412
|
+
event: "claude_launch",
|
|
413
|
+
mode: "background",
|
|
414
|
+
pid: child.pid ?? null,
|
|
415
|
+
claude_session: executor.session,
|
|
416
|
+
output_path: outputPath
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
const monitor = startExecutorMonitor({
|
|
420
|
+
statePath: newResult.paths.statePath,
|
|
421
|
+
logPath: newResult.paths.logPath,
|
|
422
|
+
pid: child.pid,
|
|
423
|
+
outputPath,
|
|
424
|
+
agentTimeoutMinutes: Number(options.agentTimeoutMinutes ?? DEFAULT_AGENT_TIMEOUT_MINUTES),
|
|
425
|
+
pollIntervalMs: Number(options.monitorPollIntervalMs ?? DEFAULT_MONITOR_POLL_INTERVAL_MS)
|
|
426
|
+
});
|
|
427
|
+
appendEvent(newResult.paths.logPath, {
|
|
428
|
+
ts: new Date().toISOString(),
|
|
429
|
+
conversation_id: newResult.conversation.conversation_id,
|
|
430
|
+
event: "executor_monitor_launch",
|
|
431
|
+
pid: monitor.pid ?? null,
|
|
432
|
+
executor_pid: child.pid ?? null,
|
|
433
|
+
agent_timeout_minutes: Number(options.agentTimeoutMinutes ?? DEFAULT_AGENT_TIMEOUT_MINUTES)
|
|
434
|
+
});
|
|
435
|
+
runtimeLog("info", "executor_monitor_launch", {
|
|
436
|
+
conversation_id: newResult.conversation.conversation_id,
|
|
437
|
+
monitor_pid: monitor.pid ?? null,
|
|
438
|
+
executor_pid: child.pid ?? null,
|
|
439
|
+
agent_timeout_minutes: Number(options.agentTimeoutMinutes ?? DEFAULT_AGENT_TIMEOUT_MINUTES)
|
|
440
|
+
});
|
|
441
|
+
printJson({
|
|
442
|
+
...newResult,
|
|
443
|
+
launched: true,
|
|
444
|
+
background: true,
|
|
445
|
+
pid: child.pid ?? null,
|
|
446
|
+
monitor_pid: monitor.pid ?? null,
|
|
447
|
+
output_path: outputPath
|
|
448
|
+
});
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
if (options.send) {
|
|
452
|
+
const acpxPath = resolveExecutable("acpx");
|
|
453
|
+
const ensureSession = ensureExecutorSession({
|
|
454
|
+
acpxPath,
|
|
455
|
+
executor,
|
|
456
|
+
cwd: workspace,
|
|
457
|
+
env: executorEnv
|
|
458
|
+
});
|
|
459
|
+
if (ensureSession.error) {
|
|
460
|
+
throw new Error(`acpx ${executor.kind} session ensure failed to start: ${ensureSession.error.message}`);
|
|
461
|
+
}
|
|
462
|
+
if (ensureSession.status !== 0) {
|
|
463
|
+
throw new Error(cleanProcessText(ensureSession.stderr || ensureSession.stdout || `acpx ${executor.kind} sessions ensure exited with status ${ensureSession.status}`));
|
|
464
|
+
}
|
|
465
|
+
const result = spawnSync(acpxPath, acpxArgs, {
|
|
466
|
+
stdio: "inherit",
|
|
467
|
+
cwd: workspace,
|
|
468
|
+
env: executorEnv
|
|
469
|
+
});
|
|
470
|
+
runtimeLog("info", "executor_send", {
|
|
471
|
+
conversation_id: newResult.conversation.conversation_id,
|
|
472
|
+
agent: executor.kind,
|
|
473
|
+
executor_session: executor.session,
|
|
474
|
+
status: result.status ?? null,
|
|
475
|
+
failure_kind: classifyProcessFailure(result)
|
|
476
|
+
});
|
|
477
|
+
process.exitCode = result.status ?? 1;
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
runtimeLog("info", "delegate_dry_run", {
|
|
481
|
+
conversation_id: newResult.conversation.conversation_id,
|
|
482
|
+
agent: executor.kind,
|
|
483
|
+
executor_session: executor.session
|
|
484
|
+
});
|
|
485
|
+
printJson({
|
|
486
|
+
...newResult,
|
|
487
|
+
dry_run: true,
|
|
488
|
+
acpx_command: ["acpx", ...acpxArgs],
|
|
489
|
+
note: "Run again with --send to send this task through acpx."
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
function ensureExecutorSession({ acpxPath, executor, cwd, env }) {
|
|
493
|
+
return spawnSync(acpxPath, [acpxCommandForExecutor(executor), "sessions", "ensure", "--name", executor.session], {
|
|
494
|
+
encoding: "utf8",
|
|
495
|
+
cwd,
|
|
496
|
+
env
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
function startExecutorMonitor({ statePath, logPath, pid, outputPath, agentTimeoutMinutes, pollIntervalMs }) {
|
|
500
|
+
const args = [
|
|
501
|
+
new URL(import.meta.url).pathname,
|
|
502
|
+
"monitor",
|
|
503
|
+
"--state",
|
|
504
|
+
statePath,
|
|
505
|
+
"--log",
|
|
506
|
+
logPath,
|
|
507
|
+
"--agent-timeout-minutes",
|
|
508
|
+
String(agentTimeoutMinutes),
|
|
509
|
+
"--poll-interval-ms",
|
|
510
|
+
String(pollIntervalMs)
|
|
511
|
+
];
|
|
512
|
+
if (pid) {
|
|
513
|
+
args.push("--pid", String(pid));
|
|
514
|
+
}
|
|
515
|
+
if (outputPath) {
|
|
516
|
+
args.push("--output-path", outputPath);
|
|
517
|
+
}
|
|
518
|
+
const child = spawn(process.execPath, args, {
|
|
519
|
+
detached: true,
|
|
520
|
+
stdio: "ignore",
|
|
521
|
+
cwd: process.cwd(),
|
|
522
|
+
env: process.env
|
|
523
|
+
});
|
|
524
|
+
child.unref();
|
|
525
|
+
return child;
|
|
526
|
+
}
|
|
527
|
+
function uniqueDelegateSessionName(kind) {
|
|
528
|
+
const { sessionPrefix } = executorDefinitionForKind(kind || "claude");
|
|
529
|
+
const timestamp = new Date().toISOString().replace(/\D/g, "").slice(0, 14);
|
|
530
|
+
return `${sessionPrefix}-${timestamp}-${randomUUID().slice(0, 8)}`;
|
|
531
|
+
}
|
|
532
|
+
function buildAcpxPromptArgs({ executor, payload, model }) {
|
|
533
|
+
const args = ["--approve-all"];
|
|
534
|
+
if (model) {
|
|
535
|
+
args.push("--model", model);
|
|
536
|
+
}
|
|
537
|
+
args.push(acpxCommandForExecutor(executor), "-s", executor.session, payload);
|
|
538
|
+
return args;
|
|
539
|
+
}
|
|
540
|
+
function proxyForExecutor(executor, options = {}) {
|
|
541
|
+
const explicit = options.allProxy ?? options.proxy;
|
|
542
|
+
if (explicit) {
|
|
543
|
+
return explicit;
|
|
544
|
+
}
|
|
545
|
+
return proxyEnvForExecutor(executor, process.env);
|
|
546
|
+
}
|
|
547
|
+
function modelForExecutor(executor, options = {}) {
|
|
548
|
+
const explicit = options.model ?? options.codexModel;
|
|
549
|
+
if (explicit) {
|
|
550
|
+
return explicit;
|
|
551
|
+
}
|
|
552
|
+
return modelEnvForExecutor(executor, process.env);
|
|
553
|
+
}
|
|
554
|
+
function environmentForExecutor(executor, options = {}) {
|
|
555
|
+
const proxy = proxyForExecutor(executor, options);
|
|
556
|
+
if (!proxy) {
|
|
557
|
+
return process.env;
|
|
558
|
+
}
|
|
559
|
+
return {
|
|
560
|
+
...process.env,
|
|
561
|
+
ALL_PROXY: proxy,
|
|
562
|
+
all_proxy: proxy
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
function runList(options) {
|
|
566
|
+
const storeDir = expandHome(options.storeDir ?? options.logDir ?? defaultStoreDir(process.cwd()));
|
|
567
|
+
const cleanup = cleanupIdleConversations(storeDir, options);
|
|
568
|
+
const includeAll = Boolean(options.all);
|
|
569
|
+
const agentFilter = options.agent ? resolveExecutor({ kind: options.agent }).kind : undefined;
|
|
570
|
+
const statusFilter = options.status;
|
|
571
|
+
const conversations = listConversations(storeDir)
|
|
572
|
+
.map((conversation) => summarizeConversation(conversation))
|
|
573
|
+
.filter((conversation) => includeAll || isActiveStatus(conversation.status))
|
|
574
|
+
.filter((conversation) => !agentFilter || conversation.agent === agentFilter)
|
|
575
|
+
.filter((conversation) => !statusFilter || conversation.status === statusFilter);
|
|
576
|
+
printJson({
|
|
577
|
+
store_dir: storeDir,
|
|
578
|
+
cleanup,
|
|
579
|
+
tasks: conversations
|
|
580
|
+
});
|
|
581
|
+
runtimeLog("info", "tasks_listed", {
|
|
582
|
+
store_dir: storeDir,
|
|
583
|
+
returned_count: conversations.length,
|
|
584
|
+
include_all: includeAll,
|
|
585
|
+
agent_filter: agentFilter,
|
|
586
|
+
status_filter: statusFilter,
|
|
587
|
+
cleanup
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
function runStatus(options) {
|
|
591
|
+
cleanupIdleConversations(storeDirFromOptions(options), options);
|
|
592
|
+
const { conversation, statePath, logPath } = loadConversationFromOptions(options);
|
|
593
|
+
const events = readExistingEvents(logPath);
|
|
594
|
+
const result = {
|
|
595
|
+
conversation,
|
|
596
|
+
summary: summarizeConversation(conversation),
|
|
597
|
+
state_path: statePath,
|
|
598
|
+
event_log_path: logPath,
|
|
599
|
+
budget: budgetAction(conversation),
|
|
600
|
+
recent_events: events.slice(-10).map(summarizeEvent)
|
|
601
|
+
};
|
|
602
|
+
if (options.trace) {
|
|
603
|
+
result.trace = buildConversationTrace({ conversation, events, logPath });
|
|
604
|
+
}
|
|
605
|
+
printJson(result);
|
|
606
|
+
runtimeLog("info", "task_status_read", {
|
|
607
|
+
conversation_id: conversation.conversation_id,
|
|
608
|
+
status: conversation.status,
|
|
609
|
+
state_path: statePath,
|
|
610
|
+
event_log_path: logPath,
|
|
611
|
+
recent_event_count: Math.min(events.length, 10),
|
|
612
|
+
trace: Boolean(options.trace)
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
function runSend(options) {
|
|
616
|
+
const messageBody = required(options.message ?? options.request, "--message is required");
|
|
617
|
+
cleanupIdleConversations(storeDirFromOptions(options), options);
|
|
618
|
+
const { conversation, statePath, logPath } = loadConversationFromOptions(options);
|
|
619
|
+
if (["done", "failed", "closed", "cancelled"].includes(conversation.status)) {
|
|
620
|
+
throw new Error(`cannot send to ${conversation.conversation_id}; conversation is ${conversation.status}`);
|
|
621
|
+
}
|
|
622
|
+
if (conversation.status === "needs_recovery") {
|
|
623
|
+
throw new Error(`cannot send to ${conversation.conversation_id}; choose recover, restart, or close first`);
|
|
624
|
+
}
|
|
625
|
+
const executor = executorForConversation(conversation);
|
|
626
|
+
const type = options.type ?? (conversation.status === "waiting_for_openclaw" ? "answer" : "task");
|
|
627
|
+
const message = createMessage({
|
|
628
|
+
conversation,
|
|
629
|
+
from: "openclaw",
|
|
630
|
+
to: executor.actor,
|
|
631
|
+
type,
|
|
632
|
+
body: messageBody,
|
|
633
|
+
metadata: {
|
|
634
|
+
executor_kind: executor.kind,
|
|
635
|
+
executor_session: executor.session
|
|
636
|
+
}
|
|
637
|
+
});
|
|
638
|
+
const nextConversation = {
|
|
639
|
+
...applyMessageToConversation(conversation, message),
|
|
640
|
+
executor,
|
|
641
|
+
claude_session: executor.kind === "claude" ? executor.session : conversation.claude_session
|
|
642
|
+
};
|
|
643
|
+
saveState(statePath, nextConversation);
|
|
644
|
+
appendEvent(logPath, messageEvent(message));
|
|
645
|
+
runtimeLog("info", "message_created", {
|
|
646
|
+
conversation_id: conversation.conversation_id,
|
|
647
|
+
agent: executor.kind,
|
|
648
|
+
executor_session: executor.session,
|
|
649
|
+
message_type: type,
|
|
650
|
+
state_path: statePath,
|
|
651
|
+
event_log_path: logPath,
|
|
652
|
+
message: textSummary(messageBody)
|
|
653
|
+
});
|
|
654
|
+
const acpxPath = resolveExecutable("acpx");
|
|
655
|
+
const executorEnv = environmentForExecutor(executor, {
|
|
656
|
+
allProxy: options.allProxy ?? conversation.executor_all_proxy
|
|
657
|
+
});
|
|
658
|
+
const executorModel = modelForExecutor(executor, {
|
|
659
|
+
model: options.model ?? conversation.executor_model
|
|
660
|
+
});
|
|
661
|
+
const ensureSession = ensureExecutorSession({
|
|
662
|
+
acpxPath,
|
|
663
|
+
executor,
|
|
664
|
+
cwd: conversation.workspace ?? process.cwd(),
|
|
665
|
+
env: executorEnv
|
|
666
|
+
});
|
|
667
|
+
appendEvent(logPath, {
|
|
668
|
+
ts: new Date().toISOString(),
|
|
669
|
+
conversation_id: conversation.conversation_id,
|
|
670
|
+
event: "executor_session_ensure",
|
|
671
|
+
status: ensureSession.status ?? null,
|
|
672
|
+
executor,
|
|
673
|
+
stdout: cleanProcessText(ensureSession.stdout),
|
|
674
|
+
stderr: cleanProcessText(ensureSession.stderr)
|
|
675
|
+
});
|
|
676
|
+
runtimeLog("info", "executor_session_ensure", {
|
|
677
|
+
conversation_id: conversation.conversation_id,
|
|
678
|
+
agent: executor.kind,
|
|
679
|
+
executor_session: executor.session,
|
|
680
|
+
status: ensureSession.status ?? null,
|
|
681
|
+
failure_kind: classifyProcessFailure(ensureSession),
|
|
682
|
+
stdout: textSummary(cleanProcessText(ensureSession.stdout)),
|
|
683
|
+
stderr: textSummary(cleanProcessText(ensureSession.stderr))
|
|
684
|
+
});
|
|
685
|
+
if (ensureSession.error) {
|
|
686
|
+
if (requiresExplicitRecoveryDecision(executor, options)) {
|
|
687
|
+
printJson(markConversationNeedsRecovery({
|
|
688
|
+
conversation: nextConversation,
|
|
689
|
+
statePath,
|
|
690
|
+
logPath,
|
|
691
|
+
executor,
|
|
692
|
+
message,
|
|
693
|
+
failedStage: "session_ensure",
|
|
694
|
+
result: ensureSession,
|
|
695
|
+
reason: `acpx ${executor.kind} session ensure failed to start: ${ensureSession.error.message}`
|
|
696
|
+
}));
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
throw new Error(`acpx ${executor.kind} session ensure failed to start: ${ensureSession.error.message}`);
|
|
700
|
+
}
|
|
701
|
+
if (ensureSession.status !== 0) {
|
|
702
|
+
if (requiresExplicitRecoveryDecision(executor, options)) {
|
|
703
|
+
printJson(markConversationNeedsRecovery({
|
|
704
|
+
conversation: nextConversation,
|
|
705
|
+
statePath,
|
|
706
|
+
logPath,
|
|
707
|
+
executor,
|
|
708
|
+
message,
|
|
709
|
+
failedStage: "session_ensure",
|
|
710
|
+
result: ensureSession,
|
|
711
|
+
reason: cleanProcessText(ensureSession.stderr || ensureSession.stdout || `acpx ${executor.kind} sessions ensure exited with status ${ensureSession.status}`)
|
|
712
|
+
}));
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
throw new Error(cleanProcessText(ensureSession.stderr || ensureSession.stdout || `acpx ${executor.kind} sessions ensure exited with status ${ensureSession.status}`));
|
|
716
|
+
}
|
|
717
|
+
const payload = [
|
|
718
|
+
"Continue the existing Agent Knock Knock delegation using this structured OpenClaw message.",
|
|
719
|
+
"If this message answers a question or blocker, follow it as the product decision.",
|
|
720
|
+
"Continue to report back only through the callback command already provided for this conversation.",
|
|
721
|
+
"",
|
|
722
|
+
JSON.stringify(message)
|
|
723
|
+
].join("\n");
|
|
724
|
+
const acpxArgs = buildAcpxPromptArgs({ executor, payload, model: executorModel });
|
|
725
|
+
if (options.background) {
|
|
726
|
+
const outputPath = path.join(path.dirname(logPath), `${executor.kind}-followup-output.log`);
|
|
727
|
+
const outputFd = fs.openSync(outputPath, "a");
|
|
728
|
+
const child = spawn(acpxPath, acpxArgs, {
|
|
729
|
+
detached: true,
|
|
730
|
+
stdio: ["ignore", outputFd, outputFd],
|
|
731
|
+
cwd: conversation.workspace ?? process.cwd(),
|
|
732
|
+
env: executorEnv
|
|
733
|
+
});
|
|
734
|
+
child.unref();
|
|
735
|
+
fs.closeSync(outputFd);
|
|
736
|
+
appendEvent(logPath, {
|
|
737
|
+
ts: new Date().toISOString(),
|
|
738
|
+
conversation_id: conversation.conversation_id,
|
|
739
|
+
event: "executor_message_launch",
|
|
740
|
+
mode: "background",
|
|
741
|
+
pid: child.pid ?? null,
|
|
742
|
+
executor,
|
|
743
|
+
output_path: outputPath
|
|
744
|
+
});
|
|
745
|
+
runtimeLog("info", "executor_message_launch", {
|
|
746
|
+
conversation_id: conversation.conversation_id,
|
|
747
|
+
agent: executor.kind,
|
|
748
|
+
executor_session: executor.session,
|
|
749
|
+
mode: "background",
|
|
750
|
+
pid: child.pid ?? null,
|
|
751
|
+
output_path: outputPath
|
|
752
|
+
});
|
|
753
|
+
const monitor = startExecutorMonitor({
|
|
754
|
+
statePath,
|
|
755
|
+
logPath,
|
|
756
|
+
pid: child.pid,
|
|
757
|
+
outputPath,
|
|
758
|
+
agentTimeoutMinutes: Number(options.agentTimeoutMinutes ?? DEFAULT_AGENT_TIMEOUT_MINUTES),
|
|
759
|
+
pollIntervalMs: Number(options.monitorPollIntervalMs ?? DEFAULT_MONITOR_POLL_INTERVAL_MS)
|
|
760
|
+
});
|
|
761
|
+
appendEvent(logPath, {
|
|
762
|
+
ts: new Date().toISOString(),
|
|
763
|
+
conversation_id: conversation.conversation_id,
|
|
764
|
+
event: "executor_monitor_launch",
|
|
765
|
+
pid: monitor.pid ?? null,
|
|
766
|
+
executor_pid: child.pid ?? null,
|
|
767
|
+
agent_timeout_minutes: Number(options.agentTimeoutMinutes ?? DEFAULT_AGENT_TIMEOUT_MINUTES)
|
|
768
|
+
});
|
|
769
|
+
printJson({
|
|
770
|
+
conversation: nextConversation,
|
|
771
|
+
message,
|
|
772
|
+
delivered: true,
|
|
773
|
+
background: true,
|
|
774
|
+
pid: child.pid ?? null,
|
|
775
|
+
monitor_pid: monitor.pid ?? null,
|
|
776
|
+
output_path: outputPath,
|
|
777
|
+
executor,
|
|
778
|
+
budget: budgetAction(nextConversation)
|
|
779
|
+
});
|
|
780
|
+
return;
|
|
781
|
+
}
|
|
782
|
+
const sendResult = spawnSync(acpxPath, acpxArgs, {
|
|
783
|
+
encoding: "utf8",
|
|
784
|
+
maxBuffer: 1024 * 1024 * 10,
|
|
785
|
+
cwd: conversation.workspace ?? process.cwd(),
|
|
786
|
+
env: executorEnv
|
|
787
|
+
});
|
|
788
|
+
appendEvent(logPath, {
|
|
789
|
+
ts: new Date().toISOString(),
|
|
790
|
+
conversation_id: conversation.conversation_id,
|
|
791
|
+
event: "executor_message_send",
|
|
792
|
+
status: sendResult.status ?? null,
|
|
793
|
+
executor,
|
|
794
|
+
stdout: cleanProcessText(sendResult.stdout),
|
|
795
|
+
stderr: cleanProcessText(sendResult.stderr)
|
|
796
|
+
});
|
|
797
|
+
runtimeLog("info", "executor_message_send", {
|
|
798
|
+
conversation_id: conversation.conversation_id,
|
|
799
|
+
agent: executor.kind,
|
|
800
|
+
executor_session: executor.session,
|
|
801
|
+
status: sendResult.status ?? null,
|
|
802
|
+
failure_kind: classifyProcessFailure(sendResult),
|
|
803
|
+
stdout: textSummary(cleanProcessText(sendResult.stdout)),
|
|
804
|
+
stderr: textSummary(cleanProcessText(sendResult.stderr))
|
|
805
|
+
});
|
|
806
|
+
if (sendResult.error) {
|
|
807
|
+
if (requiresExplicitRecoveryDecision(executor, options)) {
|
|
808
|
+
printJson(markConversationNeedsRecovery({
|
|
809
|
+
conversation: nextConversation,
|
|
810
|
+
statePath,
|
|
811
|
+
logPath,
|
|
812
|
+
executor,
|
|
813
|
+
message,
|
|
814
|
+
failedStage: "message_send",
|
|
815
|
+
result: sendResult,
|
|
816
|
+
reason: `acpx ${executor.kind} send failed to start: ${sendResult.error.message}`
|
|
817
|
+
}));
|
|
818
|
+
return;
|
|
819
|
+
}
|
|
820
|
+
throw new Error(`acpx ${executor.kind} send failed to start: ${sendResult.error.message}`);
|
|
821
|
+
}
|
|
822
|
+
if (sendResult.status !== 0) {
|
|
823
|
+
if (requiresExplicitRecoveryDecision(executor, options)) {
|
|
824
|
+
printJson(markConversationNeedsRecovery({
|
|
825
|
+
conversation: nextConversation,
|
|
826
|
+
statePath,
|
|
827
|
+
logPath,
|
|
828
|
+
executor,
|
|
829
|
+
message,
|
|
830
|
+
failedStage: "message_send",
|
|
831
|
+
result: sendResult,
|
|
832
|
+
reason: cleanProcessText(sendResult.stderr || sendResult.stdout || `acpx ${executor.kind} send exited with status ${sendResult.status}`)
|
|
833
|
+
}));
|
|
834
|
+
return;
|
|
835
|
+
}
|
|
836
|
+
throw new Error(cleanProcessText(sendResult.stderr || sendResult.stdout || `acpx ${executor.kind} send exited with status ${sendResult.status}`));
|
|
837
|
+
}
|
|
838
|
+
printJson({
|
|
839
|
+
conversation: nextConversation,
|
|
840
|
+
message,
|
|
841
|
+
delivered: true,
|
|
842
|
+
executor,
|
|
843
|
+
budget: budgetAction(nextConversation)
|
|
844
|
+
});
|
|
845
|
+
}
|
|
846
|
+
function requiresExplicitRecoveryDecision(executor, options = {}) {
|
|
847
|
+
if (options.recoveryPolicy === "explicit" || options.recoveryPolicy === "explicit-decision") {
|
|
848
|
+
return true;
|
|
849
|
+
}
|
|
850
|
+
return sessionRecoveryStrategyForExecutor(executor) === "explicit-decision";
|
|
851
|
+
}
|
|
852
|
+
function markConversationNeedsRecovery({ conversation, statePath, logPath, executor, message, failedStage, result, reason }) {
|
|
853
|
+
const now = new Date().toISOString();
|
|
854
|
+
const failureKind = classifyProcessFailure(result);
|
|
855
|
+
const recovery = {
|
|
856
|
+
reason: "executor_session_unavailable",
|
|
857
|
+
detail: reason,
|
|
858
|
+
failed_at: now,
|
|
859
|
+
failed_stage: failedStage,
|
|
860
|
+
failure_kind: failureKind,
|
|
861
|
+
failed_message_id: message.id,
|
|
862
|
+
pending_message: message,
|
|
863
|
+
previous_executor: executor,
|
|
864
|
+
options: ["recover", "restart", "close"]
|
|
865
|
+
};
|
|
866
|
+
const nextConversation = {
|
|
867
|
+
...conversation,
|
|
868
|
+
status: "needs_recovery",
|
|
869
|
+
recovery,
|
|
870
|
+
updated_at: now
|
|
871
|
+
};
|
|
872
|
+
saveState(statePath, nextConversation);
|
|
873
|
+
appendEvent(logPath, {
|
|
874
|
+
ts: now,
|
|
875
|
+
conversation_id: conversation.conversation_id,
|
|
876
|
+
event: "conversation_needs_recovery",
|
|
877
|
+
status: "needs_recovery",
|
|
878
|
+
executor,
|
|
879
|
+
failed_stage: failedStage,
|
|
880
|
+
failure_kind: failureKind,
|
|
881
|
+
reason
|
|
882
|
+
});
|
|
883
|
+
runtimeLog("warn", "conversation_needs_recovery", {
|
|
884
|
+
conversation_id: conversation.conversation_id,
|
|
885
|
+
agent: executor.kind,
|
|
886
|
+
executor_session: executor.session,
|
|
887
|
+
failed_stage: failedStage,
|
|
888
|
+
failure_kind: failureKind,
|
|
889
|
+
reason: textSummary(reason)
|
|
890
|
+
});
|
|
891
|
+
return {
|
|
892
|
+
conversation: nextConversation,
|
|
893
|
+
message,
|
|
894
|
+
delivered: false,
|
|
895
|
+
requires_recovery_decision: true,
|
|
896
|
+
recovery,
|
|
897
|
+
executor,
|
|
898
|
+
budget: budgetAction(nextConversation)
|
|
899
|
+
};
|
|
900
|
+
}
|
|
901
|
+
function runCancel(options) {
|
|
902
|
+
cleanupIdleConversations(storeDirFromOptions(options), options);
|
|
903
|
+
const { conversation, statePath, logPath } = loadConversationFromOptions(options);
|
|
904
|
+
if (["closed", "cancelled"].includes(conversation.status)) {
|
|
905
|
+
throw new Error(`cannot cancel ${conversation.conversation_id}; conversation is ${conversation.status}`);
|
|
906
|
+
}
|
|
907
|
+
const executor = executorForConversation(conversation);
|
|
908
|
+
const acpxPath = resolveExecutable("acpx");
|
|
909
|
+
const executorEnv = environmentForExecutor(executor, {
|
|
910
|
+
allProxy: options.allProxy ?? conversation.executor_all_proxy
|
|
911
|
+
});
|
|
912
|
+
const acpxCommand = acpxCommandForExecutor(executor);
|
|
913
|
+
const cancelResult = spawnSync(acpxPath, [acpxCommand, "cancel", "-s", executor.session], {
|
|
914
|
+
encoding: "utf8",
|
|
915
|
+
maxBuffer: 1024 * 1024 * 10,
|
|
916
|
+
cwd: conversation.workspace ?? process.cwd(),
|
|
917
|
+
env: executorEnv
|
|
918
|
+
});
|
|
919
|
+
const now = new Date().toISOString();
|
|
920
|
+
appendEvent(logPath, {
|
|
921
|
+
ts: now,
|
|
922
|
+
conversation_id: conversation.conversation_id,
|
|
923
|
+
event: "executor_cancel_requested",
|
|
924
|
+
status: cancelResult.status ?? null,
|
|
925
|
+
executor,
|
|
926
|
+
stdout: cleanProcessText(cancelResult.stdout),
|
|
927
|
+
stderr: cleanProcessText(cancelResult.stderr)
|
|
928
|
+
});
|
|
929
|
+
runtimeLog("info", "executor_cancel_requested", {
|
|
930
|
+
conversation_id: conversation.conversation_id,
|
|
931
|
+
agent: executor.kind,
|
|
932
|
+
executor_session: executor.session,
|
|
933
|
+
status: cancelResult.status ?? null,
|
|
934
|
+
failure_kind: classifyProcessFailure(cancelResult),
|
|
935
|
+
stdout: textSummary(cleanProcessText(cancelResult.stdout)),
|
|
936
|
+
stderr: textSummary(cleanProcessText(cancelResult.stderr))
|
|
937
|
+
});
|
|
938
|
+
if (cancelResult.error) {
|
|
939
|
+
throw new Error(`acpx ${executor.kind} cancel failed to start: ${cancelResult.error.message}`);
|
|
940
|
+
}
|
|
941
|
+
if (cancelResult.status !== 0) {
|
|
942
|
+
throw new Error(cleanProcessText(cancelResult.stderr || cancelResult.stdout || `acpx ${executor.kind} cancel exited with status ${cancelResult.status}`));
|
|
943
|
+
}
|
|
944
|
+
const nextConversation = {
|
|
945
|
+
...conversation,
|
|
946
|
+
status: "cancelling",
|
|
947
|
+
cancel_requested_at: now,
|
|
948
|
+
updated_at: now
|
|
949
|
+
};
|
|
950
|
+
saveState(statePath, nextConversation);
|
|
951
|
+
runtimeLog("info", "conversation_cancelling", {
|
|
952
|
+
conversation_id: conversation.conversation_id,
|
|
953
|
+
agent: executor.kind,
|
|
954
|
+
executor_session: executor.session,
|
|
955
|
+
state_path: statePath
|
|
956
|
+
});
|
|
957
|
+
printJson({
|
|
958
|
+
conversation: nextConversation,
|
|
959
|
+
cancel_requested: true,
|
|
960
|
+
executor,
|
|
961
|
+
acpx_command: ["acpx", acpxCommand, "cancel", "-s", executor.session],
|
|
962
|
+
budget: budgetAction(nextConversation)
|
|
963
|
+
});
|
|
964
|
+
}
|
|
965
|
+
function runRecover(options) {
|
|
966
|
+
runRecoveryDecision({ ...options, mode: "recover" });
|
|
967
|
+
}
|
|
968
|
+
function runRestart(options) {
|
|
969
|
+
runRecoveryDecision({ ...options, mode: "restart" });
|
|
970
|
+
}
|
|
971
|
+
function runRecoveryDecision(options) {
|
|
972
|
+
cleanupIdleConversations(storeDirFromOptions(options), options);
|
|
973
|
+
const { conversation, statePath, logPath } = loadConversationFromOptions(options);
|
|
974
|
+
if (conversation.status !== "needs_recovery") {
|
|
975
|
+
throw new Error(`cannot ${options.mode} ${conversation.conversation_id}; conversation is ${conversation.status}`);
|
|
976
|
+
}
|
|
977
|
+
const pendingMessage = conversation.recovery?.pending_message;
|
|
978
|
+
if (!isRecord(pendingMessage)) {
|
|
979
|
+
throw new Error(`cannot ${options.mode} ${conversation.conversation_id}; recovery pending message is missing`);
|
|
980
|
+
}
|
|
981
|
+
const previousExecutor = executorForConversation(conversation);
|
|
982
|
+
const executor = resolveExecutor({
|
|
983
|
+
kind: previousExecutor.kind,
|
|
984
|
+
session: options.session ?? uniqueDelegateSessionName(previousExecutor.kind)
|
|
985
|
+
});
|
|
986
|
+
const now = new Date().toISOString();
|
|
987
|
+
const recoveredConversation = {
|
|
988
|
+
...conversation,
|
|
989
|
+
executor,
|
|
990
|
+
claude_session: executor.kind === "claude" ? executor.session : conversation.claude_session,
|
|
991
|
+
status: "waiting_for_agent",
|
|
992
|
+
recovery: {
|
|
993
|
+
...conversation.recovery,
|
|
994
|
+
resolved_at: now,
|
|
995
|
+
resolution: options.mode,
|
|
996
|
+
previous_session: previousExecutor.session,
|
|
997
|
+
new_session: executor.session
|
|
998
|
+
},
|
|
999
|
+
updated_at: now
|
|
1000
|
+
};
|
|
1001
|
+
saveState(statePath, recoveredConversation);
|
|
1002
|
+
const payload = options.mode === "recover"
|
|
1003
|
+
? buildRecoverPayload({ conversation, pendingMessage, logPath })
|
|
1004
|
+
: buildRestartPayload({ pendingMessage });
|
|
1005
|
+
const acpxPath = resolveExecutable("acpx");
|
|
1006
|
+
const executorEnv = environmentForExecutor(executor, {
|
|
1007
|
+
allProxy: options.allProxy ?? conversation.executor_all_proxy
|
|
1008
|
+
});
|
|
1009
|
+
const executorModel = modelForExecutor(executor, {
|
|
1010
|
+
model: options.model ?? conversation.executor_model
|
|
1011
|
+
});
|
|
1012
|
+
const ensureSession = ensureExecutorSession({
|
|
1013
|
+
acpxPath,
|
|
1014
|
+
executor,
|
|
1015
|
+
cwd: conversation.workspace ?? process.cwd(),
|
|
1016
|
+
env: executorEnv
|
|
1017
|
+
});
|
|
1018
|
+
appendEvent(logPath, {
|
|
1019
|
+
ts: new Date().toISOString(),
|
|
1020
|
+
conversation_id: conversation.conversation_id,
|
|
1021
|
+
event: "executor_recovery_session_ensure",
|
|
1022
|
+
mode: options.mode,
|
|
1023
|
+
status: ensureSession.status ?? null,
|
|
1024
|
+
executor,
|
|
1025
|
+
stdout: cleanProcessText(ensureSession.stdout),
|
|
1026
|
+
stderr: cleanProcessText(ensureSession.stderr)
|
|
1027
|
+
});
|
|
1028
|
+
if (ensureSession.error) {
|
|
1029
|
+
throw new Error(`acpx ${executor.kind} recovery session ensure failed to start: ${ensureSession.error.message}`);
|
|
1030
|
+
}
|
|
1031
|
+
if (ensureSession.status !== 0) {
|
|
1032
|
+
throw new Error(cleanProcessText(ensureSession.stderr || ensureSession.stdout || `acpx ${executor.kind} recovery sessions ensure exited with status ${ensureSession.status}`));
|
|
1033
|
+
}
|
|
1034
|
+
const acpxArgs = buildAcpxPromptArgs({ executor, payload, model: executorModel });
|
|
1035
|
+
if (options.background) {
|
|
1036
|
+
const outputPath = path.join(path.dirname(logPath), `${executor.kind}-${options.mode}-output.log`);
|
|
1037
|
+
const outputFd = fs.openSync(outputPath, "a");
|
|
1038
|
+
const child = spawn(acpxPath, acpxArgs, {
|
|
1039
|
+
detached: true,
|
|
1040
|
+
stdio: ["ignore", outputFd, outputFd],
|
|
1041
|
+
cwd: conversation.workspace ?? process.cwd(),
|
|
1042
|
+
env: executorEnv
|
|
1043
|
+
});
|
|
1044
|
+
child.unref();
|
|
1045
|
+
fs.closeSync(outputFd);
|
|
1046
|
+
appendEvent(logPath, {
|
|
1047
|
+
ts: new Date().toISOString(),
|
|
1048
|
+
conversation_id: conversation.conversation_id,
|
|
1049
|
+
event: "executor_recovery_launch",
|
|
1050
|
+
mode: options.mode,
|
|
1051
|
+
run_mode: "background",
|
|
1052
|
+
pid: child.pid ?? null,
|
|
1053
|
+
executor,
|
|
1054
|
+
output_path: outputPath
|
|
1055
|
+
});
|
|
1056
|
+
const monitor = startExecutorMonitor({
|
|
1057
|
+
statePath,
|
|
1058
|
+
logPath,
|
|
1059
|
+
pid: child.pid,
|
|
1060
|
+
outputPath,
|
|
1061
|
+
agentTimeoutMinutes: Number(options.agentTimeoutMinutes ?? DEFAULT_AGENT_TIMEOUT_MINUTES),
|
|
1062
|
+
pollIntervalMs: Number(options.monitorPollIntervalMs ?? DEFAULT_MONITOR_POLL_INTERVAL_MS)
|
|
1063
|
+
});
|
|
1064
|
+
printJson({
|
|
1065
|
+
conversation: recoveredConversation,
|
|
1066
|
+
recovered: options.mode === "recover",
|
|
1067
|
+
restarted: options.mode === "restart",
|
|
1068
|
+
background: true,
|
|
1069
|
+
pid: child.pid ?? null,
|
|
1070
|
+
monitor_pid: monitor.pid ?? null,
|
|
1071
|
+
output_path: outputPath,
|
|
1072
|
+
executor,
|
|
1073
|
+
budget: budgetAction(recoveredConversation)
|
|
1074
|
+
});
|
|
1075
|
+
return;
|
|
1076
|
+
}
|
|
1077
|
+
const sendResult = spawnSync(acpxPath, acpxArgs, {
|
|
1078
|
+
encoding: "utf8",
|
|
1079
|
+
maxBuffer: 1024 * 1024 * 10,
|
|
1080
|
+
cwd: conversation.workspace ?? process.cwd(),
|
|
1081
|
+
env: executorEnv
|
|
1082
|
+
});
|
|
1083
|
+
appendEvent(logPath, {
|
|
1084
|
+
ts: new Date().toISOString(),
|
|
1085
|
+
conversation_id: conversation.conversation_id,
|
|
1086
|
+
event: "executor_recovery_send",
|
|
1087
|
+
mode: options.mode,
|
|
1088
|
+
status: sendResult.status ?? null,
|
|
1089
|
+
executor,
|
|
1090
|
+
stdout: cleanProcessText(sendResult.stdout),
|
|
1091
|
+
stderr: cleanProcessText(sendResult.stderr)
|
|
1092
|
+
});
|
|
1093
|
+
if (sendResult.error) {
|
|
1094
|
+
throw new Error(`acpx ${executor.kind} recovery send failed to start: ${sendResult.error.message}`);
|
|
1095
|
+
}
|
|
1096
|
+
if (sendResult.status !== 0) {
|
|
1097
|
+
throw new Error(cleanProcessText(sendResult.stderr || sendResult.stdout || `acpx ${executor.kind} recovery send exited with status ${sendResult.status}`));
|
|
1098
|
+
}
|
|
1099
|
+
printJson({
|
|
1100
|
+
conversation: recoveredConversation,
|
|
1101
|
+
recovered: options.mode === "recover",
|
|
1102
|
+
restarted: options.mode === "restart",
|
|
1103
|
+
delivered: true,
|
|
1104
|
+
executor,
|
|
1105
|
+
budget: budgetAction(recoveredConversation)
|
|
1106
|
+
});
|
|
1107
|
+
}
|
|
1108
|
+
function buildRecoverPayload({ conversation, pendingMessage, logPath }) {
|
|
1109
|
+
return [
|
|
1110
|
+
"Recover this Agent Knock Knock task in a new ACPX session.",
|
|
1111
|
+
"The previous coding-agent session was unavailable. This is AKK replay recovery, not native agent session resume.",
|
|
1112
|
+
"Use the saved protocol history summary below as context, then continue with the pending OpenClaw message.",
|
|
1113
|
+
"Continue to report back only through the callback command already provided for this conversation.",
|
|
1114
|
+
"",
|
|
1115
|
+
"Task:",
|
|
1116
|
+
conversation.user_request,
|
|
1117
|
+
"",
|
|
1118
|
+
"Saved protocol history:",
|
|
1119
|
+
formatProtocolHistoryForRecovery(readExistingEvents(logPath)),
|
|
1120
|
+
"",
|
|
1121
|
+
"Pending OpenClaw message:",
|
|
1122
|
+
JSON.stringify(pendingMessage)
|
|
1123
|
+
].join("\n");
|
|
1124
|
+
}
|
|
1125
|
+
function buildRestartPayload({ pendingMessage }) {
|
|
1126
|
+
return [
|
|
1127
|
+
"Restart this Agent Knock Knock task in a new ACPX session.",
|
|
1128
|
+
"Do not assume the previous coding-agent session context is available.",
|
|
1129
|
+
"Follow only the pending OpenClaw message below.",
|
|
1130
|
+
"Continue to report back only through the callback command already provided for this conversation.",
|
|
1131
|
+
"",
|
|
1132
|
+
JSON.stringify(pendingMessage)
|
|
1133
|
+
].join("\n");
|
|
1134
|
+
}
|
|
1135
|
+
function formatProtocolHistoryForRecovery(events) {
|
|
1136
|
+
const lines = events
|
|
1137
|
+
.filter((event) => event.event === "message")
|
|
1138
|
+
.map((event) => event.message ?? event)
|
|
1139
|
+
.filter((message) => message?.from && message?.to && message?.type)
|
|
1140
|
+
.slice(-40)
|
|
1141
|
+
.map((message) => {
|
|
1142
|
+
const body = String(message.body ?? "").replace(/\s+/g, " ").trim().slice(0, 700);
|
|
1143
|
+
return `- round ${message.round ?? "?"}: ${message.from} -> ${message.to} ${message.type}: ${body}`;
|
|
1144
|
+
});
|
|
1145
|
+
return lines.length ? lines.join("\n") : "- No prior protocol messages were recorded.";
|
|
1146
|
+
}
|
|
1147
|
+
function runClose(options) {
|
|
1148
|
+
const { conversation, statePath, logPath } = loadConversationFromOptions(options);
|
|
1149
|
+
const now = new Date().toISOString();
|
|
1150
|
+
const closed = {
|
|
1151
|
+
...conversation,
|
|
1152
|
+
status: "closed",
|
|
1153
|
+
closed_at: now,
|
|
1154
|
+
close_reason: options.reason ?? "closed by request",
|
|
1155
|
+
updated_at: now
|
|
1156
|
+
};
|
|
1157
|
+
saveState(statePath, closed);
|
|
1158
|
+
appendEvent(logPath, {
|
|
1159
|
+
ts: now,
|
|
1160
|
+
conversation_id: conversation.conversation_id,
|
|
1161
|
+
event: "conversation_closed",
|
|
1162
|
+
status: "closed",
|
|
1163
|
+
reason: closed.close_reason
|
|
1164
|
+
});
|
|
1165
|
+
runtimeLog("info", "conversation_closed", {
|
|
1166
|
+
conversation_id: conversation.conversation_id,
|
|
1167
|
+
status: "closed",
|
|
1168
|
+
reason: closed.close_reason,
|
|
1169
|
+
state_path: statePath,
|
|
1170
|
+
event_log_path: logPath
|
|
1171
|
+
});
|
|
1172
|
+
printJson({
|
|
1173
|
+
conversation: closed,
|
|
1174
|
+
closed: true
|
|
1175
|
+
});
|
|
1176
|
+
}
|
|
1177
|
+
function runMonitor(options) {
|
|
1178
|
+
const statePath = expandHome(required(options.state, "--state is required"));
|
|
1179
|
+
const logPath = expandHome(options.log ?? logPathForStatePath(statePath));
|
|
1180
|
+
const pid = options.pid ? Number(options.pid) : undefined;
|
|
1181
|
+
const timeoutMinutes = Number(options.agentTimeoutMinutes ?? DEFAULT_AGENT_TIMEOUT_MINUTES);
|
|
1182
|
+
const pollIntervalMs = Math.max(50, Number(options.pollIntervalMs ?? DEFAULT_MONITOR_POLL_INTERVAL_MS));
|
|
1183
|
+
let conversation = loadState(statePath);
|
|
1184
|
+
const executor = executorForConversation(conversation);
|
|
1185
|
+
appendEvent(logPath, {
|
|
1186
|
+
ts: new Date().toISOString(),
|
|
1187
|
+
conversation_id: conversation.conversation_id,
|
|
1188
|
+
event: "executor_monitor_started",
|
|
1189
|
+
executor,
|
|
1190
|
+
executor_pid: Number.isFinite(pid) ? pid : null,
|
|
1191
|
+
agent_timeout_minutes: timeoutMinutes,
|
|
1192
|
+
poll_interval_ms: pollIntervalMs,
|
|
1193
|
+
output_path: options.outputPath
|
|
1194
|
+
});
|
|
1195
|
+
runtimeLog("info", "executor_monitor_started", {
|
|
1196
|
+
conversation_id: conversation.conversation_id,
|
|
1197
|
+
agent: executor.kind,
|
|
1198
|
+
executor_session: executor.session,
|
|
1199
|
+
executor_pid: Number.isFinite(pid) ? pid : null,
|
|
1200
|
+
agent_timeout_minutes: timeoutMinutes
|
|
1201
|
+
});
|
|
1202
|
+
while (true) {
|
|
1203
|
+
conversation = loadState(statePath);
|
|
1204
|
+
if (!isWaitingForAgent(conversation.status)) {
|
|
1205
|
+
runtimeLog("info", "executor_monitor_finished", {
|
|
1206
|
+
conversation_id: conversation.conversation_id,
|
|
1207
|
+
status: conversation.status,
|
|
1208
|
+
reason: "conversation_no_longer_waiting"
|
|
1209
|
+
});
|
|
1210
|
+
printJson({
|
|
1211
|
+
conversation,
|
|
1212
|
+
monitored: true,
|
|
1213
|
+
stalled: false,
|
|
1214
|
+
reason: "conversation_no_longer_waiting"
|
|
1215
|
+
});
|
|
1216
|
+
return;
|
|
1217
|
+
}
|
|
1218
|
+
if (Number.isFinite(pid) && !isProcessAlive(pid)) {
|
|
1219
|
+
const stalledConversation = markConversationStalled({
|
|
1220
|
+
statePath,
|
|
1221
|
+
logPath,
|
|
1222
|
+
reason: `executor process ${pid} exited before callback`,
|
|
1223
|
+
detail: {
|
|
1224
|
+
executor_pid: pid,
|
|
1225
|
+
output_path: options.outputPath
|
|
1226
|
+
}
|
|
1227
|
+
});
|
|
1228
|
+
printJson({
|
|
1229
|
+
conversation: stalledConversation,
|
|
1230
|
+
monitored: true,
|
|
1231
|
+
stalled: true,
|
|
1232
|
+
reason: stalledConversation?.stalled_reason
|
|
1233
|
+
});
|
|
1234
|
+
return;
|
|
1235
|
+
}
|
|
1236
|
+
if (Number.isFinite(timeoutMinutes) && timeoutMinutes > 0) {
|
|
1237
|
+
const updatedAtMs = Date.parse(String(conversation.updated_at ?? conversation.created_at));
|
|
1238
|
+
if (Number.isFinite(updatedAtMs) && Date.now() - updatedAtMs >= timeoutMinutes * 60 * 1000) {
|
|
1239
|
+
const stalledConversation = markConversationStalled({
|
|
1240
|
+
statePath,
|
|
1241
|
+
logPath,
|
|
1242
|
+
reason: `no callback after ${timeoutMinutes} minutes`,
|
|
1243
|
+
detail: {
|
|
1244
|
+
agent_timeout_minutes: timeoutMinutes,
|
|
1245
|
+
output_path: options.outputPath
|
|
1246
|
+
}
|
|
1247
|
+
});
|
|
1248
|
+
printJson({
|
|
1249
|
+
conversation: stalledConversation,
|
|
1250
|
+
monitored: true,
|
|
1251
|
+
stalled: true,
|
|
1252
|
+
reason: stalledConversation?.stalled_reason
|
|
1253
|
+
});
|
|
1254
|
+
return;
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
sleepSync(pollIntervalMs);
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
function resolveExecutable(command) {
|
|
1261
|
+
if (command.includes(path.sep)) {
|
|
1262
|
+
return command;
|
|
1263
|
+
}
|
|
1264
|
+
const paths = executableSearchPaths();
|
|
1265
|
+
for (const dir of paths) {
|
|
1266
|
+
const candidate = path.join(dir, command);
|
|
1267
|
+
try {
|
|
1268
|
+
fs.accessSync(candidate, fs.constants.X_OK);
|
|
1269
|
+
return candidate;
|
|
1270
|
+
}
|
|
1271
|
+
catch {
|
|
1272
|
+
// Continue searching PATH.
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
throw new Error(`executable not found on PATH: ${command}`);
|
|
1276
|
+
}
|
|
1277
|
+
function executableSearchPaths() {
|
|
1278
|
+
const home = process.env.HOME;
|
|
1279
|
+
return [
|
|
1280
|
+
...(process.env.PATH ?? "").split(path.delimiter).filter(Boolean),
|
|
1281
|
+
...(home ? [
|
|
1282
|
+
path.join(home, ".npm-global", "bin"),
|
|
1283
|
+
path.join(home, ".local", "bin")
|
|
1284
|
+
] : []),
|
|
1285
|
+
"/opt/homebrew/bin",
|
|
1286
|
+
"/usr/local/bin"
|
|
1287
|
+
];
|
|
1288
|
+
}
|
|
1289
|
+
function resolveOptionalExecutable(command) {
|
|
1290
|
+
try {
|
|
1291
|
+
return resolveExecutable(command);
|
|
1292
|
+
}
|
|
1293
|
+
catch {
|
|
1294
|
+
return command;
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
function packageRootDir() {
|
|
1298
|
+
return path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
|
1299
|
+
}
|
|
1300
|
+
function runCheckedCommand(command, args, { label }) {
|
|
1301
|
+
const result = spawnSync(command, args, {
|
|
1302
|
+
encoding: "utf8",
|
|
1303
|
+
maxBuffer: 1024 * 1024 * 10
|
|
1304
|
+
});
|
|
1305
|
+
if (result.error) {
|
|
1306
|
+
throw new Error(`${label} failed to start: ${result.error.message}`);
|
|
1307
|
+
}
|
|
1308
|
+
if (result.status !== 0) {
|
|
1309
|
+
throw new Error(cleanProcessText(result.stderr || result.stdout || `${label} exited with status ${result.status}`));
|
|
1310
|
+
}
|
|
1311
|
+
return result;
|
|
1312
|
+
}
|
|
1313
|
+
function executableCheck(commandName) {
|
|
1314
|
+
try {
|
|
1315
|
+
const executable = resolveExecutable(commandName);
|
|
1316
|
+
return {
|
|
1317
|
+
command: commandName,
|
|
1318
|
+
available: true,
|
|
1319
|
+
path: executable
|
|
1320
|
+
};
|
|
1321
|
+
}
|
|
1322
|
+
catch (error) {
|
|
1323
|
+
return {
|
|
1324
|
+
command: commandName,
|
|
1325
|
+
available: false,
|
|
1326
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1327
|
+
};
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
function buildCallbackCommand({ statePath, gatewayUrl, token, openclawSession, gatewayMethod, gatewaySession, openclawBin }) {
|
|
1331
|
+
const parts = [
|
|
1332
|
+
shellQuote(process.execPath),
|
|
1333
|
+
shellQuote(new URL(import.meta.url).pathname),
|
|
1334
|
+
"callback",
|
|
1335
|
+
"--state",
|
|
1336
|
+
shellQuote(statePath)
|
|
1337
|
+
];
|
|
1338
|
+
if (token) {
|
|
1339
|
+
parts.push("--gateway-url", shellQuote(gatewayUrl), "--token", shellQuote(token), "--openclaw-session", shellQuote(openclawSession));
|
|
1340
|
+
}
|
|
1341
|
+
else if (!gatewayMethod) {
|
|
1342
|
+
parts.push("--record-only");
|
|
1343
|
+
}
|
|
1344
|
+
if (gatewayMethod) {
|
|
1345
|
+
parts.push("--gateway-method", shellQuote(gatewayMethod), "--gateway-session", shellQuote(gatewaySession ?? openclawSession));
|
|
1346
|
+
if (openclawBin) {
|
|
1347
|
+
parts.push("--openclaw-bin", shellQuote(openclawBin));
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
parts.push("--message-json", "'<structured-message-json>'");
|
|
1351
|
+
return parts.join(" ");
|
|
1352
|
+
}
|
|
1353
|
+
function expandCallbackCommandTemplate(template, { statePath }) {
|
|
1354
|
+
return template
|
|
1355
|
+
.replaceAll("{statePath}", shellQuote(statePath))
|
|
1356
|
+
.replaceAll("{state_path}", shellQuote(statePath));
|
|
1357
|
+
}
|
|
1358
|
+
function runTranscript(options) {
|
|
1359
|
+
const conversationDir = options.conversation ? expandHome(options.conversation) : null;
|
|
1360
|
+
const logPath = conversationDir
|
|
1361
|
+
? pathsForConversationDir(conversationDir).logPath
|
|
1362
|
+
: required(options.log ?? options.path, "--log or --conversation is required");
|
|
1363
|
+
const events = readNdjsonLog(expandHome(logPath));
|
|
1364
|
+
process.stdout.write(formatTranscript(events, {
|
|
1365
|
+
includeRaw: Boolean(options.includeRaw)
|
|
1366
|
+
}));
|
|
1367
|
+
}
|
|
1368
|
+
function runCallback(options) {
|
|
1369
|
+
const statePath = expandHome(required(options.state, "--state is required"));
|
|
1370
|
+
const releaseLock = acquireFileLock(`${statePath}.lock`);
|
|
1371
|
+
try {
|
|
1372
|
+
runLockedCallback({ ...options, statePath });
|
|
1373
|
+
}
|
|
1374
|
+
finally {
|
|
1375
|
+
releaseLock();
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
function runLockedCallback(options) {
|
|
1379
|
+
const messageInput = required(options.messageJson, "--message-json is required");
|
|
1380
|
+
const logPath = expandHome(options.log ?? logPathForStatePath(options.statePath));
|
|
1381
|
+
const conversation = loadState(options.statePath);
|
|
1382
|
+
const executor = executorForConversation(conversation);
|
|
1383
|
+
const message = extractStructuredMessage({
|
|
1384
|
+
conversation,
|
|
1385
|
+
input: messageInput,
|
|
1386
|
+
defaultFrom: executor.actor,
|
|
1387
|
+
defaultTo: "openclaw"
|
|
1388
|
+
});
|
|
1389
|
+
const existingEvents = readExistingEvents(logPath);
|
|
1390
|
+
if (isDuplicateMessage(existingEvents, message)) {
|
|
1391
|
+
runtimeLog("info", "callback_duplicate", {
|
|
1392
|
+
conversation_id: conversation.conversation_id,
|
|
1393
|
+
agent: executor.kind,
|
|
1394
|
+
executor_session: executor.session,
|
|
1395
|
+
from: message.from,
|
|
1396
|
+
type: message.type,
|
|
1397
|
+
round: message.round,
|
|
1398
|
+
state_path: options.statePath,
|
|
1399
|
+
event_log_path: logPath
|
|
1400
|
+
});
|
|
1401
|
+
printJson({
|
|
1402
|
+
conversation,
|
|
1403
|
+
message,
|
|
1404
|
+
budget: budgetAction(conversation),
|
|
1405
|
+
delivered: false,
|
|
1406
|
+
duplicate: true
|
|
1407
|
+
});
|
|
1408
|
+
return;
|
|
1409
|
+
}
|
|
1410
|
+
const nextConversation = applyMessageToConversation(conversation, message);
|
|
1411
|
+
appendEvent(logPath, messageEvent(message));
|
|
1412
|
+
saveState(options.statePath, nextConversation);
|
|
1413
|
+
runtimeLog("info", "callback_received", {
|
|
1414
|
+
conversation_id: conversation.conversation_id,
|
|
1415
|
+
agent: executor.kind,
|
|
1416
|
+
executor_session: executor.session,
|
|
1417
|
+
from: message.from,
|
|
1418
|
+
type: message.type,
|
|
1419
|
+
round: message.round,
|
|
1420
|
+
status: nextConversation.status,
|
|
1421
|
+
requires_response: message.requires_response,
|
|
1422
|
+
state_path: options.statePath,
|
|
1423
|
+
event_log_path: logPath,
|
|
1424
|
+
message: textSummary(message.body)
|
|
1425
|
+
});
|
|
1426
|
+
const result = {
|
|
1427
|
+
conversation: nextConversation,
|
|
1428
|
+
message,
|
|
1429
|
+
budget: budgetAction(nextConversation),
|
|
1430
|
+
delivered: false,
|
|
1431
|
+
duplicate: false
|
|
1432
|
+
};
|
|
1433
|
+
if (options.gatewayMethod) {
|
|
1434
|
+
const delivery = deliverToGatewayMethod({
|
|
1435
|
+
method: options.gatewayMethod,
|
|
1436
|
+
openclawBin: options.openclawBin,
|
|
1437
|
+
gatewayUrl: options.gatewayUrl,
|
|
1438
|
+
token: options.token,
|
|
1439
|
+
sessionKey: options.gatewaySession ?? options.openclawSession ?? conversation.openclaw_session,
|
|
1440
|
+
statePath: options.statePath,
|
|
1441
|
+
logPath,
|
|
1442
|
+
conversation: nextConversation,
|
|
1443
|
+
message
|
|
1444
|
+
});
|
|
1445
|
+
appendEvent(logPath, {
|
|
1446
|
+
ts: new Date().toISOString(),
|
|
1447
|
+
conversation_id: conversation.conversation_id,
|
|
1448
|
+
event: "callback_gateway_method_delivery",
|
|
1449
|
+
from: message.from,
|
|
1450
|
+
to: "openclaw",
|
|
1451
|
+
round: message.round,
|
|
1452
|
+
method: options.gatewayMethod,
|
|
1453
|
+
status: delivery.status,
|
|
1454
|
+
stdout: delivery.stdout,
|
|
1455
|
+
stderr: delivery.stderr
|
|
1456
|
+
});
|
|
1457
|
+
runtimeLog("info", "callback_gateway_method_delivery", {
|
|
1458
|
+
conversation_id: conversation.conversation_id,
|
|
1459
|
+
method: options.gatewayMethod,
|
|
1460
|
+
status: delivery.status,
|
|
1461
|
+
failure_kind: classifyProcessFailure(delivery),
|
|
1462
|
+
stdout: textSummary(delivery.stdout),
|
|
1463
|
+
stderr: textSummary(delivery.stderr)
|
|
1464
|
+
});
|
|
1465
|
+
if (delivery.status !== 0) {
|
|
1466
|
+
throw new Error(delivery.stderr || delivery.stdout || `gateway method delivery failed with status ${delivery.status}`);
|
|
1467
|
+
}
|
|
1468
|
+
const gatewayPayload = parseOptionalJson(delivery.stdout);
|
|
1469
|
+
const chatSendParams = isRecord(gatewayPayload?.chat_send) ? gatewayPayload.chat_send : undefined;
|
|
1470
|
+
const sessionSendParams = isRecord(gatewayPayload?.session_send) ? gatewayPayload.session_send : undefined;
|
|
1471
|
+
let chatSendDelivery;
|
|
1472
|
+
let sessionSendDelivery;
|
|
1473
|
+
if (chatSendParams) {
|
|
1474
|
+
chatSendDelivery = deliverToChatSend({
|
|
1475
|
+
openclawBin: options.openclawBin,
|
|
1476
|
+
gatewayUrl: options.gatewayUrl,
|
|
1477
|
+
token: options.token,
|
|
1478
|
+
params: chatSendParams
|
|
1479
|
+
});
|
|
1480
|
+
appendEvent(logPath, {
|
|
1481
|
+
ts: new Date().toISOString(),
|
|
1482
|
+
conversation_id: conversation.conversation_id,
|
|
1483
|
+
event: "callback_chat_send_delivery",
|
|
1484
|
+
from: message.from,
|
|
1485
|
+
to: "openclaw",
|
|
1486
|
+
round: message.round,
|
|
1487
|
+
status: chatSendDelivery.status,
|
|
1488
|
+
stdout: chatSendDelivery.stdout,
|
|
1489
|
+
stderr: chatSendDelivery.stderr
|
|
1490
|
+
});
|
|
1491
|
+
runtimeLog("info", "callback_chat_send_delivery", {
|
|
1492
|
+
conversation_id: conversation.conversation_id,
|
|
1493
|
+
status: chatSendDelivery.status,
|
|
1494
|
+
failure_kind: classifyProcessFailure(chatSendDelivery),
|
|
1495
|
+
stdout: textSummary(chatSendDelivery.stdout),
|
|
1496
|
+
stderr: textSummary(chatSendDelivery.stderr)
|
|
1497
|
+
});
|
|
1498
|
+
if (chatSendDelivery.status !== 0) {
|
|
1499
|
+
throw new Error(chatSendDelivery.stderr || chatSendDelivery.stdout || `chat callback delivery failed with status ${chatSendDelivery.status}`);
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
else if (sessionSendParams) {
|
|
1503
|
+
sessionSendDelivery = deliverToSessionSend({
|
|
1504
|
+
openclawBin: options.openclawBin,
|
|
1505
|
+
gatewayUrl: options.gatewayUrl,
|
|
1506
|
+
token: options.token,
|
|
1507
|
+
params: sessionSendParams
|
|
1508
|
+
});
|
|
1509
|
+
appendEvent(logPath, {
|
|
1510
|
+
ts: new Date().toISOString(),
|
|
1511
|
+
conversation_id: conversation.conversation_id,
|
|
1512
|
+
event: "callback_session_send_delivery",
|
|
1513
|
+
from: message.from,
|
|
1514
|
+
to: "openclaw",
|
|
1515
|
+
round: message.round,
|
|
1516
|
+
status: sessionSendDelivery.status,
|
|
1517
|
+
stdout: sessionSendDelivery.stdout,
|
|
1518
|
+
stderr: sessionSendDelivery.stderr
|
|
1519
|
+
});
|
|
1520
|
+
runtimeLog("info", "callback_session_send_delivery", {
|
|
1521
|
+
conversation_id: conversation.conversation_id,
|
|
1522
|
+
status: sessionSendDelivery.status,
|
|
1523
|
+
failure_kind: classifyProcessFailure(sessionSendDelivery),
|
|
1524
|
+
stdout: textSummary(sessionSendDelivery.stdout),
|
|
1525
|
+
stderr: textSummary(sessionSendDelivery.stderr)
|
|
1526
|
+
});
|
|
1527
|
+
if (sessionSendDelivery.status !== 0) {
|
|
1528
|
+
throw new Error(sessionSendDelivery.stderr || sessionSendDelivery.stdout || `session callback delivery failed with status ${sessionSendDelivery.status}`);
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
printJson({
|
|
1532
|
+
...result,
|
|
1533
|
+
delivered: true,
|
|
1534
|
+
delivery: chatSendDelivery
|
|
1535
|
+
? "gateway_method+chat_send"
|
|
1536
|
+
: sessionSendDelivery
|
|
1537
|
+
? "gateway_method+sessions_send"
|
|
1538
|
+
: "gateway_method"
|
|
1539
|
+
});
|
|
1540
|
+
return;
|
|
1541
|
+
}
|
|
1542
|
+
if (options.recordOnly) {
|
|
1543
|
+
runtimeLog("info", "callback_recorded_only", {
|
|
1544
|
+
conversation_id: conversation.conversation_id,
|
|
1545
|
+
status: nextConversation.status
|
|
1546
|
+
});
|
|
1547
|
+
printJson(result);
|
|
1548
|
+
return;
|
|
1549
|
+
}
|
|
1550
|
+
const gatewayUrl = options.gatewayUrl ?? conversation.gateway_url;
|
|
1551
|
+
const token = options.token ?? conversation.gateway_token;
|
|
1552
|
+
const openclawSession = options.openclawSession ?? conversation.openclaw_session;
|
|
1553
|
+
if (!gatewayUrl) {
|
|
1554
|
+
throw new Error("--gateway-url is required unless state has gateway_url");
|
|
1555
|
+
}
|
|
1556
|
+
if (!token || token === "<token>") {
|
|
1557
|
+
throw new Error("--token is required for callback delivery");
|
|
1558
|
+
}
|
|
1559
|
+
if (!openclawSession) {
|
|
1560
|
+
throw new Error("--openclaw-session is required unless state has openclaw_session");
|
|
1561
|
+
}
|
|
1562
|
+
const delivery = deliverToOpenClaw({ gatewayUrl, token, openclawSession, message });
|
|
1563
|
+
appendEvent(logPath, {
|
|
1564
|
+
ts: new Date().toISOString(),
|
|
1565
|
+
conversation_id: conversation.conversation_id,
|
|
1566
|
+
event: "callback_delivery",
|
|
1567
|
+
from: message.from,
|
|
1568
|
+
to: "openclaw",
|
|
1569
|
+
round: message.round,
|
|
1570
|
+
status: delivery.status,
|
|
1571
|
+
stdout: delivery.stdout,
|
|
1572
|
+
stderr: delivery.stderr
|
|
1573
|
+
});
|
|
1574
|
+
runtimeLog("info", "callback_delivery", {
|
|
1575
|
+
conversation_id: conversation.conversation_id,
|
|
1576
|
+
status: delivery.status,
|
|
1577
|
+
failure_kind: classifyProcessFailure(delivery),
|
|
1578
|
+
stdout: textSummary(delivery.stdout),
|
|
1579
|
+
stderr: textSummary(delivery.stderr)
|
|
1580
|
+
});
|
|
1581
|
+
if (delivery.status !== 0) {
|
|
1582
|
+
throw new Error(delivery.stderr || delivery.stdout || `callback delivery failed with status ${delivery.status}`);
|
|
1583
|
+
}
|
|
1584
|
+
printJson({
|
|
1585
|
+
...result,
|
|
1586
|
+
delivered: true
|
|
1587
|
+
});
|
|
1588
|
+
}
|
|
1589
|
+
function acquireFileLock(lockPath, { timeoutMs = 5000, retryMs = 50 } = {}) {
|
|
1590
|
+
const started = Date.now();
|
|
1591
|
+
while (true) {
|
|
1592
|
+
try {
|
|
1593
|
+
const fd = fs.openSync(lockPath, "wx");
|
|
1594
|
+
fs.closeSync(fd);
|
|
1595
|
+
return () => {
|
|
1596
|
+
fs.rmSync(lockPath, { force: true });
|
|
1597
|
+
};
|
|
1598
|
+
}
|
|
1599
|
+
catch (error) {
|
|
1600
|
+
if (error.code !== "EEXIST") {
|
|
1601
|
+
throw error;
|
|
1602
|
+
}
|
|
1603
|
+
if (Date.now() - started >= timeoutMs) {
|
|
1604
|
+
throw new Error(`timed out waiting for callback lock: ${lockPath}`);
|
|
1605
|
+
}
|
|
1606
|
+
sleepSync(retryMs);
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
function sleepSync(ms) {
|
|
1611
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
1612
|
+
}
|
|
1613
|
+
function readExistingEvents(logPath) {
|
|
1614
|
+
try {
|
|
1615
|
+
return readNdjsonLog(logPath);
|
|
1616
|
+
}
|
|
1617
|
+
catch (error) {
|
|
1618
|
+
if (error.code === "ENOENT") {
|
|
1619
|
+
return [];
|
|
1620
|
+
}
|
|
1621
|
+
throw error;
|
|
1622
|
+
}
|
|
1623
|
+
}
|
|
1624
|
+
function loadConversationFromOptions(options) {
|
|
1625
|
+
const storeDir = storeDirFromOptions(options);
|
|
1626
|
+
const conversationId = options.conversation ?? options.conversationId;
|
|
1627
|
+
const statePath = expandHome(options.state ?? (conversationId ? statePathForConversationId(conversationId, storeDir) : undefined));
|
|
1628
|
+
if (!statePath) {
|
|
1629
|
+
throw new Error("--conversation or --state is required");
|
|
1630
|
+
}
|
|
1631
|
+
const conversation = options.state
|
|
1632
|
+
? loadState(statePath)
|
|
1633
|
+
: loadConversationById(conversationId, storeDir);
|
|
1634
|
+
return {
|
|
1635
|
+
conversation,
|
|
1636
|
+
statePath,
|
|
1637
|
+
logPath: logPathForStatePath(statePath)
|
|
1638
|
+
};
|
|
1639
|
+
}
|
|
1640
|
+
function storeDirFromOptions(options) {
|
|
1641
|
+
return expandHome(options.storeDir ?? options.logDir ?? defaultStoreDir(process.cwd()));
|
|
1642
|
+
}
|
|
1643
|
+
function summarizeConversation(conversation) {
|
|
1644
|
+
const executor = executorForConversation(conversation);
|
|
1645
|
+
return {
|
|
1646
|
+
conversation_id: conversation.conversation_id,
|
|
1647
|
+
agent: executor.kind,
|
|
1648
|
+
executor,
|
|
1649
|
+
session: executor.session,
|
|
1650
|
+
status: conversation.status,
|
|
1651
|
+
request: conversation.user_request,
|
|
1652
|
+
workspace: conversation.workspace,
|
|
1653
|
+
openclaw_session: conversation.openclaw_session,
|
|
1654
|
+
response_rounds_used: conversation.response_rounds_used,
|
|
1655
|
+
soft_limit: conversation.soft_limit,
|
|
1656
|
+
hard_limit: conversation.hard_limit,
|
|
1657
|
+
created_at: conversation.created_at,
|
|
1658
|
+
updated_at: conversation.updated_at,
|
|
1659
|
+
idle_since: conversation.idle_since,
|
|
1660
|
+
closed_at: conversation.closed_at,
|
|
1661
|
+
recovery: conversation.recovery,
|
|
1662
|
+
state_path: conversation.state_path,
|
|
1663
|
+
event_log_path: conversation.event_log_path
|
|
1664
|
+
};
|
|
1665
|
+
}
|
|
1666
|
+
function summarizeEvent(event) {
|
|
1667
|
+
return {
|
|
1668
|
+
ts: event.ts,
|
|
1669
|
+
event: event.event,
|
|
1670
|
+
from: event.from,
|
|
1671
|
+
to: event.to,
|
|
1672
|
+
type: event.type,
|
|
1673
|
+
status: event.status,
|
|
1674
|
+
round: event.round,
|
|
1675
|
+
body: typeof event.body === "string" ? event.body.slice(0, 500) : undefined
|
|
1676
|
+
};
|
|
1677
|
+
}
|
|
1678
|
+
function buildConversationTrace({ conversation, events, logPath }) {
|
|
1679
|
+
const outputPath = traceOutputPath({ conversation, events, logPath });
|
|
1680
|
+
const output = outputPath && fs.existsSync(outputPath)
|
|
1681
|
+
? fs.readFileSync(outputPath, "utf8").slice(-256 * 1024)
|
|
1682
|
+
: "";
|
|
1683
|
+
const parsed = parseExecutorTraceOutput(output);
|
|
1684
|
+
const monitorEvents = events
|
|
1685
|
+
.filter((event) => [
|
|
1686
|
+
"executor_launch",
|
|
1687
|
+
"executor_message_launch",
|
|
1688
|
+
"executor_monitor_launch",
|
|
1689
|
+
"executor_monitor_started",
|
|
1690
|
+
"conversation_stalled",
|
|
1691
|
+
"callback_delivery",
|
|
1692
|
+
"callback_gateway_method_delivery",
|
|
1693
|
+
"callback_chat_send_delivery",
|
|
1694
|
+
"callback_session_send_delivery"
|
|
1695
|
+
].includes(event.event))
|
|
1696
|
+
.slice(-20)
|
|
1697
|
+
.map((event) => ({
|
|
1698
|
+
ts: event.ts,
|
|
1699
|
+
event: event.event,
|
|
1700
|
+
status: event.status,
|
|
1701
|
+
pid: event.pid,
|
|
1702
|
+
executor_pid: event.executor_pid,
|
|
1703
|
+
reason: event.reason,
|
|
1704
|
+
output_path: event.output_path
|
|
1705
|
+
}));
|
|
1706
|
+
return {
|
|
1707
|
+
source: output ? "executor_output_log" : "events_only",
|
|
1708
|
+
output_path: outputPath,
|
|
1709
|
+
thinking_redacted_count: parsed.thinkingRedactedCount,
|
|
1710
|
+
client_events: parsed.clientEvents.slice(-20),
|
|
1711
|
+
permission_requests: parsed.permissionRequests.slice(-10),
|
|
1712
|
+
tool_calls: parsed.toolCalls.slice(-20),
|
|
1713
|
+
agent_messages: parsed.agentMessages.slice(-8),
|
|
1714
|
+
done_events: parsed.doneEvents.slice(-5),
|
|
1715
|
+
monitor_events: monitorEvents,
|
|
1716
|
+
safety: {
|
|
1717
|
+
thinking: "redacted",
|
|
1718
|
+
tool_output: "summarized",
|
|
1719
|
+
callback_payloads: "redacted"
|
|
1720
|
+
}
|
|
1721
|
+
};
|
|
1722
|
+
}
|
|
1723
|
+
function traceOutputPath({ conversation, events, logPath }) {
|
|
1724
|
+
const launch = [...events].reverse().find((event) => ["executor_message_launch", "executor_launch"].includes(event.event) &&
|
|
1725
|
+
typeof event.output_path === "string");
|
|
1726
|
+
if (launch?.output_path) {
|
|
1727
|
+
return launch.output_path;
|
|
1728
|
+
}
|
|
1729
|
+
const executor = executorForConversation(conversation);
|
|
1730
|
+
const conversationDir = conversation.conversation_dir ?? path.dirname(logPath);
|
|
1731
|
+
const candidates = [
|
|
1732
|
+
path.join(conversationDir, `${executor.kind}-followup-output.log`),
|
|
1733
|
+
path.join(conversationDir, `${executor.kind}-output.log`)
|
|
1734
|
+
];
|
|
1735
|
+
return candidates.find((candidate) => fs.existsSync(candidate)) ?? candidates.at(-1);
|
|
1736
|
+
}
|
|
1737
|
+
function parseExecutorTraceOutput(output) {
|
|
1738
|
+
const toolCalls = [];
|
|
1739
|
+
const clientEvents = [];
|
|
1740
|
+
const permissionRequests = [];
|
|
1741
|
+
const agentMessages = [];
|
|
1742
|
+
const doneEvents = [];
|
|
1743
|
+
let thinkingRedactedCount = 0;
|
|
1744
|
+
let currentTool = null;
|
|
1745
|
+
let captureOutputFor = null;
|
|
1746
|
+
let capturedOutputLines = [];
|
|
1747
|
+
const flushToolOutput = () => {
|
|
1748
|
+
if (captureOutputFor && capturedOutputLines.length > 0) {
|
|
1749
|
+
captureOutputFor.output_preview = sanitizeTraceText(capturedOutputLines.join("\n"), 500);
|
|
1750
|
+
}
|
|
1751
|
+
captureOutputFor = null;
|
|
1752
|
+
capturedOutputLines = [];
|
|
1753
|
+
};
|
|
1754
|
+
for (const rawLine of String(output ?? "").split(/\r?\n/)) {
|
|
1755
|
+
const line = rawLine.trimEnd();
|
|
1756
|
+
const text = line.trim();
|
|
1757
|
+
if (!text) {
|
|
1758
|
+
continue;
|
|
1759
|
+
}
|
|
1760
|
+
if (text.startsWith("[") && captureOutputFor) {
|
|
1761
|
+
flushToolOutput();
|
|
1762
|
+
}
|
|
1763
|
+
const client = text.match(/^\[client\]\s+(.+?)(?:\s+\(([^)]+)\))?$/);
|
|
1764
|
+
if (client) {
|
|
1765
|
+
if (isPermissionTraceLine(text)) {
|
|
1766
|
+
permissionRequests.push({
|
|
1767
|
+
body: sanitizeTraceText(text, 240)
|
|
1768
|
+
});
|
|
1769
|
+
}
|
|
1770
|
+
clientEvents.push({
|
|
1771
|
+
name: sanitizeTraceText(client[1], 160),
|
|
1772
|
+
status: client[2] ? sanitizeTraceText(client[2], 80) : undefined
|
|
1773
|
+
});
|
|
1774
|
+
continue;
|
|
1775
|
+
}
|
|
1776
|
+
const acpx = text.match(/^\[acpx\]\s+(.+)$/);
|
|
1777
|
+
if (acpx) {
|
|
1778
|
+
clientEvents.push({
|
|
1779
|
+
name: "acpx",
|
|
1780
|
+
status: sanitizeTraceText(acpx[1], 220)
|
|
1781
|
+
});
|
|
1782
|
+
continue;
|
|
1783
|
+
}
|
|
1784
|
+
if (text.startsWith("[thinking]")) {
|
|
1785
|
+
thinkingRedactedCount += 1;
|
|
1786
|
+
agentMessages.push({
|
|
1787
|
+
kind: "thinking",
|
|
1788
|
+
body: "[redacted]"
|
|
1789
|
+
});
|
|
1790
|
+
continue;
|
|
1791
|
+
}
|
|
1792
|
+
const done = text.match(/^\[done\]\s*(.*)$/);
|
|
1793
|
+
if (done) {
|
|
1794
|
+
doneEvents.push({
|
|
1795
|
+
status: sanitizeTraceText(done[1] || "done", 120)
|
|
1796
|
+
});
|
|
1797
|
+
continue;
|
|
1798
|
+
}
|
|
1799
|
+
const tool = text.match(/^\[tool\]\s+(.+?)\s+\(([^)]+)\)$/);
|
|
1800
|
+
if (tool) {
|
|
1801
|
+
const toolCall = {
|
|
1802
|
+
name: sanitizeToolName(tool[1]),
|
|
1803
|
+
status: sanitizeTraceText(tool[2], 80)
|
|
1804
|
+
};
|
|
1805
|
+
toolCalls.push(toolCall);
|
|
1806
|
+
currentTool = toolCall;
|
|
1807
|
+
continue;
|
|
1808
|
+
}
|
|
1809
|
+
if (currentTool && text.startsWith("input:")) {
|
|
1810
|
+
currentTool.input_preview = sanitizeTraceText(text.slice("input:".length).trim(), 360);
|
|
1811
|
+
continue;
|
|
1812
|
+
}
|
|
1813
|
+
if (currentTool && text.startsWith("output:")) {
|
|
1814
|
+
captureOutputFor = currentTool;
|
|
1815
|
+
capturedOutputLines = [];
|
|
1816
|
+
continue;
|
|
1817
|
+
}
|
|
1818
|
+
if (captureOutputFor && !text.startsWith("[")) {
|
|
1819
|
+
if (capturedOutputLines.length < 8) {
|
|
1820
|
+
capturedOutputLines.push(text);
|
|
1821
|
+
}
|
|
1822
|
+
continue;
|
|
1823
|
+
}
|
|
1824
|
+
if (isPermissionTraceLine(text)) {
|
|
1825
|
+
permissionRequests.push({
|
|
1826
|
+
body: sanitizeTraceText(text, 240)
|
|
1827
|
+
});
|
|
1828
|
+
continue;
|
|
1829
|
+
}
|
|
1830
|
+
if (isAgentMessageTraceLine(text)) {
|
|
1831
|
+
agentMessages.push({
|
|
1832
|
+
kind: "message",
|
|
1833
|
+
body: sanitizeTraceText(text, 360)
|
|
1834
|
+
});
|
|
1835
|
+
}
|
|
1836
|
+
}
|
|
1837
|
+
flushToolOutput();
|
|
1838
|
+
return {
|
|
1839
|
+
toolCalls,
|
|
1840
|
+
clientEvents,
|
|
1841
|
+
permissionRequests,
|
|
1842
|
+
agentMessages,
|
|
1843
|
+
doneEvents,
|
|
1844
|
+
thinkingRedactedCount
|
|
1845
|
+
};
|
|
1846
|
+
}
|
|
1847
|
+
function sanitizeToolName(value) {
|
|
1848
|
+
return sanitizeTraceText(String(value ?? "")
|
|
1849
|
+
.replace(/--message-json\s+(['"]).*?\1/g, "--message-json <redacted>")
|
|
1850
|
+
.replace(/--message-json\s+.*/g, "--message-json <redacted>")
|
|
1851
|
+
.replace(/--token\s+\S+/g, "--token <redacted>"), 220);
|
|
1852
|
+
}
|
|
1853
|
+
function sanitizeTraceText(value, maxLength = 240) {
|
|
1854
|
+
return String(value ?? "")
|
|
1855
|
+
.replace(/--message-json\s+(['"]).*?\1/g, "--message-json <redacted>")
|
|
1856
|
+
.replace(/--message-json\s+.*/g, "--message-json <redacted>")
|
|
1857
|
+
.replace(/--token\s+\S+/g, "--token <redacted>")
|
|
1858
|
+
.replace(/(gateway[_-]?token|api[_-]?key|token|password|secret)=\S+/gi, "$1=<redacted>")
|
|
1859
|
+
.slice(0, maxLength);
|
|
1860
|
+
}
|
|
1861
|
+
function isPermissionTraceLine(text) {
|
|
1862
|
+
const lower = text.toLowerCase();
|
|
1863
|
+
return lower.includes("session/request_permission") ||
|
|
1864
|
+
(lower.includes("permission") && (lower.includes("request") || lower.includes("approve") || lower.includes("allow")));
|
|
1865
|
+
}
|
|
1866
|
+
function isAgentMessageTraceLine(text) {
|
|
1867
|
+
if (text.startsWith("[") || text.startsWith("{") || text.startsWith("}") || text.startsWith("```")) {
|
|
1868
|
+
return false;
|
|
1869
|
+
}
|
|
1870
|
+
if (text.startsWith("input:") || text.startsWith("output:") || text.startsWith("kind:")) {
|
|
1871
|
+
return false;
|
|
1872
|
+
}
|
|
1873
|
+
if (/^(call_id|process_id|turn_id|command|cwd):/i.test(text)) {
|
|
1874
|
+
return false;
|
|
1875
|
+
}
|
|
1876
|
+
return text.length >= 12;
|
|
1877
|
+
}
|
|
1878
|
+
function isActiveStatus(status) {
|
|
1879
|
+
return !["done", "failed", "closed", "cancelled"].includes(status);
|
|
1880
|
+
}
|
|
1881
|
+
function isWaitingForAgent(status) {
|
|
1882
|
+
return ["created", "running", "waiting_for_agent", "cancelling"].includes(status);
|
|
1883
|
+
}
|
|
1884
|
+
function isProcessAlive(pid) {
|
|
1885
|
+
try {
|
|
1886
|
+
process.kill(pid, 0);
|
|
1887
|
+
return true;
|
|
1888
|
+
}
|
|
1889
|
+
catch (error) {
|
|
1890
|
+
return error?.code === "EPERM";
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1893
|
+
function markConversationStalled({ statePath, logPath, reason, detail = {} }) {
|
|
1894
|
+
const releaseLock = acquireFileLock(`${statePath}.lock`);
|
|
1895
|
+
let stalledConversation;
|
|
1896
|
+
try {
|
|
1897
|
+
const conversation = loadState(statePath);
|
|
1898
|
+
if (!isWaitingForAgent(conversation.status)) {
|
|
1899
|
+
runtimeLog("info", "executor_monitor_finished", {
|
|
1900
|
+
conversation_id: conversation.conversation_id,
|
|
1901
|
+
status: conversation.status,
|
|
1902
|
+
reason: "conversation_changed_before_stall"
|
|
1903
|
+
});
|
|
1904
|
+
return conversation;
|
|
1905
|
+
}
|
|
1906
|
+
const now = new Date().toISOString();
|
|
1907
|
+
stalledConversation = {
|
|
1908
|
+
...conversation,
|
|
1909
|
+
status: "stalled",
|
|
1910
|
+
stalled_at: now,
|
|
1911
|
+
stalled_reason: reason,
|
|
1912
|
+
updated_at: now
|
|
1913
|
+
};
|
|
1914
|
+
saveState(statePath, stalledConversation);
|
|
1915
|
+
appendEvent(logPath, {
|
|
1916
|
+
ts: now,
|
|
1917
|
+
conversation_id: conversation.conversation_id,
|
|
1918
|
+
event: "conversation_stalled",
|
|
1919
|
+
status: "stalled",
|
|
1920
|
+
reason,
|
|
1921
|
+
...detail
|
|
1922
|
+
});
|
|
1923
|
+
runtimeLog("warn", "conversation_stalled", {
|
|
1924
|
+
conversation_id: conversation.conversation_id,
|
|
1925
|
+
agent: executorForConversation(conversation).kind,
|
|
1926
|
+
executor_session: executorForConversation(conversation).session,
|
|
1927
|
+
state_path: statePath,
|
|
1928
|
+
event_log_path: logPath,
|
|
1929
|
+
reason,
|
|
1930
|
+
...detail
|
|
1931
|
+
});
|
|
1932
|
+
}
|
|
1933
|
+
finally {
|
|
1934
|
+
releaseLock();
|
|
1935
|
+
}
|
|
1936
|
+
if (stalledConversation) {
|
|
1937
|
+
deliverStalledNotification({
|
|
1938
|
+
statePath,
|
|
1939
|
+
logPath,
|
|
1940
|
+
conversation: stalledConversation,
|
|
1941
|
+
reason
|
|
1942
|
+
});
|
|
1943
|
+
}
|
|
1944
|
+
return stalledConversation;
|
|
1945
|
+
}
|
|
1946
|
+
function deliverStalledNotification({ statePath, logPath, conversation, reason }) {
|
|
1947
|
+
if (!conversation.gateway_method) {
|
|
1948
|
+
return;
|
|
1949
|
+
}
|
|
1950
|
+
const executor = executorForConversation(conversation);
|
|
1951
|
+
const message = createMessage({
|
|
1952
|
+
conversation,
|
|
1953
|
+
from: executor.actor,
|
|
1954
|
+
to: "openclaw",
|
|
1955
|
+
type: "error",
|
|
1956
|
+
requiresResponse: false,
|
|
1957
|
+
body: [
|
|
1958
|
+
`AKK marked this ${executor.display_name} task as stalled: ${reason}.`,
|
|
1959
|
+
"",
|
|
1960
|
+
`Conversation: ${conversation.conversation_id}`,
|
|
1961
|
+
`Session: ${executor.session}`,
|
|
1962
|
+
"Use `AKK status` for details, `AKK send` to retry/follow up, or `AKK close` to close it."
|
|
1963
|
+
].join("\n")
|
|
1964
|
+
});
|
|
1965
|
+
const delivery = deliverToGatewayMethod({
|
|
1966
|
+
method: conversation.gateway_method,
|
|
1967
|
+
openclawBin: conversation.openclaw_bin,
|
|
1968
|
+
gatewayUrl: conversation.gateway_url,
|
|
1969
|
+
token: conversation.gateway_token,
|
|
1970
|
+
sessionKey: conversation.gateway_session ?? conversation.openclaw_session,
|
|
1971
|
+
statePath,
|
|
1972
|
+
logPath,
|
|
1973
|
+
conversation,
|
|
1974
|
+
message
|
|
1975
|
+
});
|
|
1976
|
+
appendEvent(logPath, {
|
|
1977
|
+
ts: new Date().toISOString(),
|
|
1978
|
+
conversation_id: conversation.conversation_id,
|
|
1979
|
+
event: "stalled_gateway_method_delivery",
|
|
1980
|
+
method: conversation.gateway_method,
|
|
1981
|
+
status: delivery.status,
|
|
1982
|
+
stdout: delivery.stdout,
|
|
1983
|
+
stderr: delivery.stderr
|
|
1984
|
+
});
|
|
1985
|
+
runtimeLog("info", "stalled_gateway_method_delivery", {
|
|
1986
|
+
conversation_id: conversation.conversation_id,
|
|
1987
|
+
method: conversation.gateway_method,
|
|
1988
|
+
status: delivery.status,
|
|
1989
|
+
failure_kind: classifyProcessFailure(delivery),
|
|
1990
|
+
stdout: textSummary(delivery.stdout),
|
|
1991
|
+
stderr: textSummary(delivery.stderr)
|
|
1992
|
+
});
|
|
1993
|
+
if (delivery.status !== 0) {
|
|
1994
|
+
return;
|
|
1995
|
+
}
|
|
1996
|
+
const gatewayPayload = parseOptionalJson(delivery.stdout);
|
|
1997
|
+
const chatSendParams = isRecord(gatewayPayload?.chat_send) ? gatewayPayload.chat_send : undefined;
|
|
1998
|
+
if (!chatSendParams) {
|
|
1999
|
+
return;
|
|
2000
|
+
}
|
|
2001
|
+
const chatSendDelivery = deliverToChatSend({
|
|
2002
|
+
openclawBin: conversation.openclaw_bin,
|
|
2003
|
+
gatewayUrl: conversation.gateway_url,
|
|
2004
|
+
token: conversation.gateway_token,
|
|
2005
|
+
params: chatSendParams
|
|
2006
|
+
});
|
|
2007
|
+
appendEvent(logPath, {
|
|
2008
|
+
ts: new Date().toISOString(),
|
|
2009
|
+
conversation_id: conversation.conversation_id,
|
|
2010
|
+
event: "stalled_chat_send_delivery",
|
|
2011
|
+
status: chatSendDelivery.status,
|
|
2012
|
+
stdout: chatSendDelivery.stdout,
|
|
2013
|
+
stderr: chatSendDelivery.stderr
|
|
2014
|
+
});
|
|
2015
|
+
runtimeLog("info", "stalled_chat_send_delivery", {
|
|
2016
|
+
conversation_id: conversation.conversation_id,
|
|
2017
|
+
status: chatSendDelivery.status,
|
|
2018
|
+
failure_kind: classifyProcessFailure(chatSendDelivery),
|
|
2019
|
+
stdout: textSummary(chatSendDelivery.stdout),
|
|
2020
|
+
stderr: textSummary(chatSendDelivery.stderr)
|
|
2021
|
+
});
|
|
2022
|
+
}
|
|
2023
|
+
function cleanupIdleConversations(storeDir, options = {}, now = new Date()) {
|
|
2024
|
+
const timeoutMinutes = Number(options.idleTimeoutMinutes ?? DEFAULT_IDLE_TIMEOUT_MINUTES);
|
|
2025
|
+
if (!Number.isFinite(timeoutMinutes) || timeoutMinutes <= 0) {
|
|
2026
|
+
return { checked: 0, closed: 0, idle_timeout_minutes: timeoutMinutes };
|
|
2027
|
+
}
|
|
2028
|
+
const conversations = listConversations(storeDir);
|
|
2029
|
+
let closed = 0;
|
|
2030
|
+
for (const conversation of conversations) {
|
|
2031
|
+
if (conversation.status !== "idle" || !conversation.idle_since) {
|
|
2032
|
+
continue;
|
|
2033
|
+
}
|
|
2034
|
+
const idleSinceMs = Date.parse(conversation.idle_since);
|
|
2035
|
+
if (!Number.isFinite(idleSinceMs)) {
|
|
2036
|
+
continue;
|
|
2037
|
+
}
|
|
2038
|
+
if (now.getTime() - idleSinceMs < timeoutMinutes * 60 * 1000) {
|
|
2039
|
+
continue;
|
|
2040
|
+
}
|
|
2041
|
+
const statePath = conversation.state_path ?? statePathForConversationId(conversation.conversation_id, storeDir);
|
|
2042
|
+
const logPath = conversation.event_log_path ?? logPathForStatePath(statePath);
|
|
2043
|
+
const closedConversation = {
|
|
2044
|
+
...conversation,
|
|
2045
|
+
status: "closed",
|
|
2046
|
+
closed_at: now.toISOString(),
|
|
2047
|
+
close_reason: `idle timeout after ${timeoutMinutes} minutes`,
|
|
2048
|
+
updated_at: now.toISOString()
|
|
2049
|
+
};
|
|
2050
|
+
delete closedConversation.idle_since;
|
|
2051
|
+
saveState(statePath, closedConversation);
|
|
2052
|
+
appendEvent(logPath, {
|
|
2053
|
+
ts: now.toISOString(),
|
|
2054
|
+
conversation_id: conversation.conversation_id,
|
|
2055
|
+
event: "conversation_closed",
|
|
2056
|
+
status: "closed",
|
|
2057
|
+
reason: closedConversation.close_reason,
|
|
2058
|
+
idle_timeout_minutes: timeoutMinutes
|
|
2059
|
+
});
|
|
2060
|
+
runtimeLog("info", "idle_conversation_closed", {
|
|
2061
|
+
conversation_id: conversation.conversation_id,
|
|
2062
|
+
agent: executorForConversation(conversation).kind,
|
|
2063
|
+
executor_session: executorForConversation(conversation).session,
|
|
2064
|
+
state_path: statePath,
|
|
2065
|
+
event_log_path: logPath,
|
|
2066
|
+
idle_since: conversation.idle_since,
|
|
2067
|
+
idle_timeout_minutes: timeoutMinutes,
|
|
2068
|
+
reason: closedConversation.close_reason
|
|
2069
|
+
});
|
|
2070
|
+
closed += 1;
|
|
2071
|
+
}
|
|
2072
|
+
return {
|
|
2073
|
+
checked: conversations.length,
|
|
2074
|
+
closed,
|
|
2075
|
+
idle_timeout_minutes: timeoutMinutes
|
|
2076
|
+
};
|
|
2077
|
+
}
|
|
2078
|
+
function isDuplicateMessage(events, message) {
|
|
2079
|
+
return events.some((event) => {
|
|
2080
|
+
if (event.event !== "message") {
|
|
2081
|
+
return false;
|
|
2082
|
+
}
|
|
2083
|
+
const existing = event.message ?? event;
|
|
2084
|
+
if (existing.id && existing.id === message.id) {
|
|
2085
|
+
return true;
|
|
2086
|
+
}
|
|
2087
|
+
return messageFingerprint(existing) === messageFingerprint(message);
|
|
2088
|
+
});
|
|
2089
|
+
}
|
|
2090
|
+
function messageFingerprint(message) {
|
|
2091
|
+
return JSON.stringify({
|
|
2092
|
+
conversation_id: message.conversation_id,
|
|
2093
|
+
from: message.from,
|
|
2094
|
+
to: message.to,
|
|
2095
|
+
type: message.type,
|
|
2096
|
+
requires_response: message.requires_response,
|
|
2097
|
+
body: message.body
|
|
2098
|
+
});
|
|
2099
|
+
}
|
|
2100
|
+
function deliverToOpenClaw({ gatewayUrl, token, openclawSession, message }) {
|
|
2101
|
+
const agent = `openclaw acp --url ${gatewayUrl} --token ${token} --session ${openclawSession}`;
|
|
2102
|
+
const result = spawnSync("acpx", ["--agent", agent, JSON.stringify(message)], {
|
|
2103
|
+
encoding: "utf8",
|
|
2104
|
+
maxBuffer: 1024 * 1024 * 10
|
|
2105
|
+
});
|
|
2106
|
+
if (result.error) {
|
|
2107
|
+
return {
|
|
2108
|
+
status: 1,
|
|
2109
|
+
stdout: result.stdout ?? "",
|
|
2110
|
+
stderr: result.error.message
|
|
2111
|
+
};
|
|
2112
|
+
}
|
|
2113
|
+
return {
|
|
2114
|
+
status: result.status ?? 1,
|
|
2115
|
+
stdout: result.stdout ?? "",
|
|
2116
|
+
stderr: result.stderr ?? ""
|
|
2117
|
+
};
|
|
2118
|
+
}
|
|
2119
|
+
function deliverToGatewayMethod({ method, openclawBin, gatewayUrl, token, sessionKey, statePath, logPath, conversation, message }) {
|
|
2120
|
+
const args = [
|
|
2121
|
+
"gateway",
|
|
2122
|
+
"call",
|
|
2123
|
+
method,
|
|
2124
|
+
"--params",
|
|
2125
|
+
JSON.stringify({
|
|
2126
|
+
sessionKey,
|
|
2127
|
+
statePath,
|
|
2128
|
+
logPath,
|
|
2129
|
+
conversation,
|
|
2130
|
+
message
|
|
2131
|
+
}),
|
|
2132
|
+
"--json"
|
|
2133
|
+
];
|
|
2134
|
+
if (gatewayUrl) {
|
|
2135
|
+
args.push("--url", gatewayUrl);
|
|
2136
|
+
}
|
|
2137
|
+
if (token && token !== "<token>") {
|
|
2138
|
+
args.push("--token", token);
|
|
2139
|
+
}
|
|
2140
|
+
const result = spawnSync(openclawBin ?? "openclaw", args, {
|
|
2141
|
+
encoding: "utf8",
|
|
2142
|
+
maxBuffer: 1024 * 1024 * 10
|
|
2143
|
+
});
|
|
2144
|
+
if (result.error) {
|
|
2145
|
+
return {
|
|
2146
|
+
status: 1,
|
|
2147
|
+
stdout: result.stdout ?? "",
|
|
2148
|
+
stderr: result.error.message
|
|
2149
|
+
};
|
|
2150
|
+
}
|
|
2151
|
+
return {
|
|
2152
|
+
status: result.status ?? 1,
|
|
2153
|
+
stdout: result.stdout ?? "",
|
|
2154
|
+
stderr: result.stderr ?? ""
|
|
2155
|
+
};
|
|
2156
|
+
}
|
|
2157
|
+
function deliverToSessionSend({ openclawBin, gatewayUrl, token, params }) {
|
|
2158
|
+
const args = [
|
|
2159
|
+
"gateway",
|
|
2160
|
+
"call",
|
|
2161
|
+
"sessions.send",
|
|
2162
|
+
"--params",
|
|
2163
|
+
JSON.stringify(params),
|
|
2164
|
+
"--json"
|
|
2165
|
+
];
|
|
2166
|
+
if (gatewayUrl) {
|
|
2167
|
+
args.push("--url", gatewayUrl);
|
|
2168
|
+
}
|
|
2169
|
+
if (token && token !== "<token>") {
|
|
2170
|
+
args.push("--token", token);
|
|
2171
|
+
}
|
|
2172
|
+
const result = spawnSync(openclawBin ?? "openclaw", args, {
|
|
2173
|
+
encoding: "utf8",
|
|
2174
|
+
maxBuffer: 1024 * 1024 * 10
|
|
2175
|
+
});
|
|
2176
|
+
if (result.error) {
|
|
2177
|
+
return {
|
|
2178
|
+
status: 1,
|
|
2179
|
+
stdout: result.stdout ?? "",
|
|
2180
|
+
stderr: result.error.message
|
|
2181
|
+
};
|
|
2182
|
+
}
|
|
2183
|
+
return {
|
|
2184
|
+
status: result.status ?? 1,
|
|
2185
|
+
stdout: result.stdout ?? "",
|
|
2186
|
+
stderr: result.stderr ?? ""
|
|
2187
|
+
};
|
|
2188
|
+
}
|
|
2189
|
+
function deliverToChatSend({ openclawBin, gatewayUrl, token, params }) {
|
|
2190
|
+
const args = [
|
|
2191
|
+
"gateway",
|
|
2192
|
+
"call",
|
|
2193
|
+
"chat.send",
|
|
2194
|
+
"--params",
|
|
2195
|
+
JSON.stringify(params),
|
|
2196
|
+
"--json"
|
|
2197
|
+
];
|
|
2198
|
+
if (gatewayUrl) {
|
|
2199
|
+
args.push("--url", gatewayUrl);
|
|
2200
|
+
}
|
|
2201
|
+
if (token && token !== "<token>") {
|
|
2202
|
+
args.push("--token", token);
|
|
2203
|
+
}
|
|
2204
|
+
const result = spawnSync(openclawBin ?? "openclaw", args, {
|
|
2205
|
+
encoding: "utf8",
|
|
2206
|
+
maxBuffer: 1024 * 1024 * 10
|
|
2207
|
+
});
|
|
2208
|
+
if (result.error) {
|
|
2209
|
+
return {
|
|
2210
|
+
status: 1,
|
|
2211
|
+
stdout: result.stdout ?? "",
|
|
2212
|
+
stderr: result.error.message
|
|
2213
|
+
};
|
|
2214
|
+
}
|
|
2215
|
+
return {
|
|
2216
|
+
status: result.status ?? 1,
|
|
2217
|
+
stdout: result.stdout ?? "",
|
|
2218
|
+
stderr: result.stderr ?? ""
|
|
2219
|
+
};
|
|
2220
|
+
}
|
|
2221
|
+
function captureJson(argv) {
|
|
2222
|
+
const result = spawnSync(process.execPath, [new URL(import.meta.url).pathname, ...argv], {
|
|
2223
|
+
encoding: "utf8"
|
|
2224
|
+
});
|
|
2225
|
+
if (result.status !== 0) {
|
|
2226
|
+
throw new Error(result.stderr || result.stdout || `subcommand failed: ${argv[0]}`);
|
|
2227
|
+
}
|
|
2228
|
+
return JSON.parse(result.stdout);
|
|
2229
|
+
}
|
|
2230
|
+
function parseArgs(argv) {
|
|
2231
|
+
const parsed = {};
|
|
2232
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
2233
|
+
const arg = argv[index];
|
|
2234
|
+
if (!arg.startsWith("--")) {
|
|
2235
|
+
throw new Error(`unexpected argument: ${arg}`);
|
|
2236
|
+
}
|
|
2237
|
+
const key = toCamelCase(arg.slice(2));
|
|
2238
|
+
const next = argv[index + 1];
|
|
2239
|
+
if (next === undefined || next.startsWith("--")) {
|
|
2240
|
+
parsed[key] = true;
|
|
2241
|
+
}
|
|
2242
|
+
else {
|
|
2243
|
+
parsed[key] = next;
|
|
2244
|
+
index += 1;
|
|
2245
|
+
}
|
|
2246
|
+
}
|
|
2247
|
+
return parsed;
|
|
2248
|
+
}
|
|
2249
|
+
function toCamelCase(value) {
|
|
2250
|
+
return value.replace(/-([a-z])/g, (_, char) => char.toUpperCase());
|
|
2251
|
+
}
|
|
2252
|
+
function required(value, message) {
|
|
2253
|
+
if (value === undefined || value === "") {
|
|
2254
|
+
throw new Error(message);
|
|
2255
|
+
}
|
|
2256
|
+
return value;
|
|
2257
|
+
}
|
|
2258
|
+
function isRecord(value) {
|
|
2259
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
2260
|
+
}
|
|
2261
|
+
function parseOptionalJson(text) {
|
|
2262
|
+
try {
|
|
2263
|
+
return JSON.parse(String(text));
|
|
2264
|
+
}
|
|
2265
|
+
catch {
|
|
2266
|
+
return undefined;
|
|
2267
|
+
}
|
|
2268
|
+
}
|
|
2269
|
+
function expandHome(filePath) {
|
|
2270
|
+
if (filePath === "~") {
|
|
2271
|
+
return process.env.HOME;
|
|
2272
|
+
}
|
|
2273
|
+
if (filePath?.startsWith("~/")) {
|
|
2274
|
+
return `${process.env.HOME}${filePath.slice(1)}`;
|
|
2275
|
+
}
|
|
2276
|
+
return filePath;
|
|
2277
|
+
}
|
|
2278
|
+
function printJson(value) {
|
|
2279
|
+
process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
|
|
2280
|
+
}
|
|
2281
|
+
function cleanProcessText(text) {
|
|
2282
|
+
const value = String(text ?? "").trim();
|
|
2283
|
+
return value ? value.slice(0, 2000) : undefined;
|
|
2284
|
+
}
|
|
2285
|
+
function textSummary(text, maxLength = 240) {
|
|
2286
|
+
const value = String(text ?? "");
|
|
2287
|
+
return {
|
|
2288
|
+
length: value.length,
|
|
2289
|
+
preview: value ? value.slice(0, maxLength) : undefined
|
|
2290
|
+
};
|
|
2291
|
+
}
|
|
2292
|
+
function classifyProcessFailure(result) {
|
|
2293
|
+
const status = result?.status ?? 0;
|
|
2294
|
+
const combined = [
|
|
2295
|
+
result?.error?.message,
|
|
2296
|
+
result?.stderr,
|
|
2297
|
+
result?.stdout
|
|
2298
|
+
].filter(Boolean).join("\n").toLowerCase();
|
|
2299
|
+
if (!combined && status === 0) {
|
|
2300
|
+
return undefined;
|
|
2301
|
+
}
|
|
2302
|
+
if (combined.includes("agent needs reconnect") || combined.includes("internal error")) {
|
|
2303
|
+
return "agent_reconnect_required";
|
|
2304
|
+
}
|
|
2305
|
+
if (combined.includes("permission denied") || combined.includes("operation not permitted")) {
|
|
2306
|
+
return "permission_denied";
|
|
2307
|
+
}
|
|
2308
|
+
if (combined.includes("sandbox") || combined.includes("outside workspace")) {
|
|
2309
|
+
return "sandbox_denied";
|
|
2310
|
+
}
|
|
2311
|
+
if (combined.includes("timed out") || combined.includes("timeout")) {
|
|
2312
|
+
return "timeout";
|
|
2313
|
+
}
|
|
2314
|
+
if (status !== 0) {
|
|
2315
|
+
return "nonzero_exit";
|
|
2316
|
+
}
|
|
2317
|
+
return undefined;
|
|
2318
|
+
}
|
|
2319
|
+
function runtimeLog(level, event, fields = {}) {
|
|
2320
|
+
try {
|
|
2321
|
+
writeRuntimeLog({
|
|
2322
|
+
level,
|
|
2323
|
+
event,
|
|
2324
|
+
...fields
|
|
2325
|
+
});
|
|
2326
|
+
}
|
|
2327
|
+
catch {
|
|
2328
|
+
// Runtime logging must never break the user-facing CLI command.
|
|
2329
|
+
}
|
|
2330
|
+
}
|
|
2331
|
+
function shellQuote(value) {
|
|
2332
|
+
return `'${String(value).replace(/'/g, "'\\''")}'`;
|
|
2333
|
+
}
|
|
2334
|
+
function withStoragePaths(conversation, paths) {
|
|
2335
|
+
return {
|
|
2336
|
+
...conversation,
|
|
2337
|
+
store_dir: paths.storeDir,
|
|
2338
|
+
conversation_dir: paths.conversationDir,
|
|
2339
|
+
event_log_path: paths.logPath,
|
|
2340
|
+
state_path: paths.statePath
|
|
2341
|
+
};
|
|
2342
|
+
}
|
|
2343
|
+
function usage() {
|
|
2344
|
+
const agentList = EXECUTOR_KINDS.join("|");
|
|
2345
|
+
process.stdout.write(`Usage:
|
|
2346
|
+
agent-knock-knock new --request <text> [--agent ${agentList}] [--workspace <path>] [--store-dir <dir>]
|
|
2347
|
+
agent-knock-knock record --state <file> --message-json <json>
|
|
2348
|
+
agent-knock-knock bootstrap-prompt --callback-command <command> [--agent ${agentList}]
|
|
2349
|
+
agent-knock-knock delegate --request <text> [--agent ${agentList}] [--store-dir <dir>] [--all-proxy <url>] [--agent-timeout-minutes <minutes>] [--token <gateway-token>] [--send|--background]
|
|
2350
|
+
agent-knock-knock list [--store-dir <dir>] [--agent ${agentList}] [--status <status>] [--all]
|
|
2351
|
+
agent-knock-knock status --conversation <id> [--store-dir <dir>] [--trace]
|
|
2352
|
+
agent-knock-knock send --conversation <id> --message <text> [--type answer|task|control] [--all-proxy <url>] [--agent-timeout-minutes <minutes>]
|
|
2353
|
+
agent-knock-knock cancel --conversation <id> [--all-proxy <url>]
|
|
2354
|
+
agent-knock-knock recover --conversation <id> [--session <name>] [--all-proxy <url>]
|
|
2355
|
+
agent-knock-knock restart --conversation <id> [--session <name>] [--all-proxy <url>]
|
|
2356
|
+
agent-knock-knock close --conversation <id> [--reason <text>]
|
|
2357
|
+
agent-knock-knock install-openclaw [--openclaw-bin <path>] [--skill-path <path>] [--no-restart]
|
|
2358
|
+
agent-knock-knock doctor
|
|
2359
|
+
agent-knock-knock callback --state <file> --message-json <json> [--record-only]
|
|
2360
|
+
agent-knock-knock transcript --log <file> [--include-raw]
|
|
2361
|
+
agent-knock-knock transcript --conversation <dir> [--include-raw]
|
|
2362
|
+
`);
|
|
2363
|
+
}
|
|
2364
|
+
//# sourceMappingURL=cli.js.map
|