@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.
@@ -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