@scotthuang/agent-knock-knock 0.3.2 → 0.4.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.
Files changed (69) hide show
  1. package/CHANGELOG.md +9 -571
  2. package/README.md +97 -123
  3. package/dist/src/agent-session-provider.d.ts +1 -2
  4. package/dist/src/approval-policy.d.ts +1 -1
  5. package/dist/src/approval-policy.js +1 -1
  6. package/dist/src/approval-policy.js.map +1 -1
  7. package/dist/src/claude-terminal-agent-adapter.d.ts +2 -17
  8. package/dist/src/claude-terminal-agent-adapter.js +4 -489
  9. package/dist/src/claude-terminal-agent-adapter.js.map +1 -1
  10. package/dist/src/cli.js +1489 -3406
  11. package/dist/src/cli.js.map +1 -1
  12. package/dist/src/codex-local-session-provider.d.ts +1 -2
  13. package/dist/src/codex-local-session-provider.js +1 -9
  14. package/dist/src/codex-local-session-provider.js.map +1 -1
  15. package/dist/src/codex-session-provider.d.ts +1 -8
  16. package/dist/src/codex-session-provider.js +0 -56
  17. package/dist/src/codex-session-provider.js.map +1 -1
  18. package/dist/src/codex-terminal-agent-adapter.d.ts +1 -1
  19. package/dist/src/doctor-capabilities.d.ts +5 -13
  20. package/dist/src/doctor-capabilities.js +15 -60
  21. package/dist/src/doctor-capabilities.js.map +1 -1
  22. package/dist/src/executors.d.ts +5 -51
  23. package/dist/src/executors.js +4 -68
  24. package/dist/src/executors.js.map +1 -1
  25. package/dist/src/openclaw-doctor.d.ts +1 -1
  26. package/dist/src/openclaw-doctor.js +1 -1
  27. package/dist/src/openclaw-doctor.js.map +1 -1
  28. package/dist/src/openclaw-plugin-helpers.d.ts +5 -8
  29. package/dist/src/openclaw-plugin-helpers.js +57 -40
  30. package/dist/src/openclaw-plugin-helpers.js.map +1 -1
  31. package/dist/src/openclaw-plugin.js +138 -248
  32. package/dist/src/openclaw-plugin.js.map +1 -1
  33. package/dist/src/protocol.d.ts +1 -5
  34. package/dist/src/protocol.js +1 -1
  35. package/dist/src/protocol.js.map +1 -1
  36. package/dist/src/session-selector.js +1 -1
  37. package/dist/src/session-selector.js.map +1 -1
  38. package/dist/src/terminal-agent-adapter.d.ts +3 -17
  39. package/dist/src/terminal-agent-adapter.js.map +1 -1
  40. package/dist/src/terminal-agent-bridge.d.ts +3 -3
  41. package/dist/src/terminal-agent-bridge.js +0 -75
  42. package/dist/src/terminal-agent-bridge.js.map +1 -1
  43. package/dist/src/transcript.js +2 -3
  44. package/dist/src/transcript.js.map +1 -1
  45. package/docs/quickstart-tmux.md +7 -7
  46. package/openclaw.plugin.json +4 -83
  47. package/package.json +7 -15
  48. package/templates/openclaw-skills/agent-knock-knock/SKILL.md +94 -147
  49. package/dist/src/acpx-output.d.ts +0 -18
  50. package/dist/src/acpx-output.js +0 -223
  51. package/dist/src/acpx-output.js.map +0 -1
  52. package/dist/src/bootstrap.d.ts +0 -8
  53. package/dist/src/bootstrap.js +0 -45
  54. package/dist/src/bootstrap.js.map +0 -1
  55. package/dist/src/claude-hook-installer.d.ts +0 -65
  56. package/dist/src/claude-hook-installer.js +0 -410
  57. package/dist/src/claude-hook-installer.js.map +0 -1
  58. package/dist/src/claude-hook-protocol.d.ts +0 -89
  59. package/dist/src/claude-hook-protocol.js +0 -243
  60. package/dist/src/claude-hook-protocol.js.map +0 -1
  61. package/dist/src/claude-hook-store.d.ts +0 -247
  62. package/dist/src/claude-hook-store.js +0 -1075
  63. package/dist/src/claude-hook-store.js.map +0 -1
  64. package/dist/src/session-takeover-planner.d.ts +0 -37
  65. package/dist/src/session-takeover-planner.js +0 -57
  66. package/dist/src/session-takeover-planner.js.map +0 -1
  67. package/docs/quickstart-managed-acpx.md +0 -43
  68. package/scripts/smoke-acpx.js +0 -139
  69. package/scripts/smoke-tmux.js +0 -140
@@ -2,7 +2,7 @@ import { spawn, spawnSync } from "node:child_process";
2
2
  import { fileURLToPath } from "node:url";
3
3
  import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
4
4
  import { EXECUTOR_KINDS, executorDefinitionForKind, parseLeadingExecutorAlias } from "./executors.js";
5
- import { AKK_CALLBACK_METHOD, akkUsageText, buildAkkCommandCliArgs, formatAkkListCommandResult, parseAkkCommand, resolvePluginStoreDir, resolveConversationOverrides } from "./openclaw-plugin-helpers.js";
5
+ import { AKK_CALLBACK_METHOD, akkUsageText, buildAkkCommandCliArgs, formatAkkListCommandResult, parseAkkCommand, resolvePluginStoreDir } from "./openclaw-plugin-helpers.js";
6
6
  import { attemptAutoApproval } from "./approval-policy.js";
7
7
  const CALLBACK_METHOD = AKK_CALLBACK_METHOD;
8
8
  const defaultBinPath = fileURLToPath(new URL("./cli.js", import.meta.url));
@@ -24,58 +24,6 @@ const delegateParameters = {
24
24
  type: "string",
25
25
  description: "Workspace path for the selected coding agent. Defaults to plugin config or the Gateway process directory."
26
26
  },
27
- claudeSession: {
28
- type: "string",
29
- description: "Claude Code session name."
30
- },
31
- codexSession: {
32
- type: "string",
33
- description: "Codex session name."
34
- },
35
- cursorSession: {
36
- type: "string",
37
- description: "Cursor session name."
38
- },
39
- codexAllProxy: {
40
- type: "string",
41
- description: "ALL_PROXY value used when launching Codex through ACPX."
42
- },
43
- cursorAllProxy: {
44
- type: "string",
45
- description: "ALL_PROXY value used when launching Cursor through ACPX."
46
- },
47
- codexModel: {
48
- type: "string",
49
- description: "ACPX model id used when launching Codex."
50
- },
51
- cursorModel: {
52
- type: "string",
53
- description: "ACPX model id used when launching Cursor."
54
- },
55
- model: {
56
- type: "string",
57
- description: "ACPX model id used when launching the selected coding agent."
58
- },
59
- allProxy: {
60
- type: "string",
61
- description: "ALL_PROXY value used when launching the selected coding agent through ACPX."
62
- },
63
- session: {
64
- type: "string",
65
- description: "Explicit coding agent session name."
66
- },
67
- openclawSession: {
68
- type: "string",
69
- description: "OpenClaw session label recorded in the protocol state."
70
- },
71
- softLimit: {
72
- type: "number",
73
- description: "Soft response-requiring round limit."
74
- },
75
- hardLimit: {
76
- type: "number",
77
- description: "Hard response-requiring round limit."
78
- },
79
27
  idleTimeoutMinutes: {
80
28
  type: "number",
81
29
  description: "Minutes an idle AKK session remains open before lazy cleanup closes it."
@@ -83,6 +31,11 @@ const delegateParameters = {
83
31
  agentTimeoutMinutes: {
84
32
  type: "number",
85
33
  description: "Callback timeout in minutes; terminal bridge tasks treat it as an inactivity timeout."
34
+ },
35
+ agentHardTimeoutMinutes: {
36
+ type: "number",
37
+ exclusiveMinimum: 0,
38
+ description: "Maximum terminal monitor lifetime in minutes."
86
39
  }
87
40
  }
88
41
  };
@@ -201,12 +154,6 @@ const sendParameters = {
201
154
  type: "string",
202
155
  enum: ["answer", "task", "control", "error"]
203
156
  },
204
- allProxy: {
205
- type: "string"
206
- },
207
- model: {
208
- type: "string"
209
- },
210
157
  idleTimeoutMinutes: {
211
158
  type: "number"
212
159
  },
@@ -230,31 +177,6 @@ const cancelParameters = {
230
177
  type: "string",
231
178
  description: "AKK-managed conversation id, or a terminal-controlled id from AKK list such as terminal:v2:tmux:codex:codex-work:0.1:33389."
232
179
  },
233
- allProxy: {
234
- type: "string"
235
- },
236
- idleTimeoutMinutes: {
237
- type: "number"
238
- }
239
- }
240
- };
241
- const recoveryParameters = {
242
- type: "object",
243
- additionalProperties: false,
244
- required: ["conversation_id"],
245
- properties: {
246
- conversation_id: {
247
- type: "string"
248
- },
249
- session: {
250
- type: "string"
251
- },
252
- allProxy: {
253
- type: "string"
254
- },
255
- model: {
256
- type: "string"
257
- },
258
180
  idleTimeoutMinutes: {
259
181
  type: "number"
260
182
  }
@@ -270,6 +192,10 @@ const closeParameters = {
270
192
  },
271
193
  reason: {
272
194
  type: "string"
195
+ },
196
+ expected_message_id: {
197
+ type: "string",
198
+ description: "Required only to clear an orphaned terminal dispatch shown by AKK list. Must exactly match that entry's current message_id."
273
199
  }
274
200
  }
275
201
  };
@@ -289,25 +215,13 @@ const agentTakeoverParameters = {
289
215
  },
290
216
  strategy: {
291
217
  type: "string",
292
- enum: ["terminate_then_resume", "fork", "terminal_control"],
293
- description: "Takeover strategy. terminate_then_resume returns a confirmation plan to stop the exact active CLI before resume. terminal_control attaches to an existing controllable terminal session after confirmation. fork returns bounded context for OpenClaw summary confirmation."
218
+ enum: ["terminal_control"],
219
+ description: "Attach to the existing Codex process through its shared tmux terminal."
294
220
  },
295
221
  createConversation: {
296
222
  type: "boolean",
297
223
  description: "When true and the selected strategy is ready, create an AKK-managed conversation bound to the native session so later AKK send/status/close can use it."
298
224
  },
299
- confirmTerminate: {
300
- type: "boolean",
301
- description: "Only for strategy=terminate_then_resume. When true, AKK will terminate the exact expected Codex CLI pid after rechecking that it still matches the requested session, then create the AKK conversation."
302
- },
303
- expectedPid: {
304
- type: "number",
305
- description: "Required with confirmTerminate=true. The exact Codex CLI pid from the previous takeover plan that the user confirmed may be terminated."
306
- },
307
- allowCwdOnly: {
308
- type: "boolean",
309
- description: "Only for strategy=terminate_then_resume with confirmTerminate=true. Allows terminating the expected pid when Codex does not expose a session id in argv, as long as the pid still runs in the target session cwd. Use only after explaining the higher risk to the user."
310
- },
311
225
  confirmTerminal: {
312
226
  type: "boolean",
313
227
  description: "Only for strategy=terminal_control. When true, AKK attaches to the exact terminalTarget after the user confirms that terminal pane."
@@ -320,29 +234,9 @@ const agentTakeoverParameters = {
320
234
  type: "string",
321
235
  description: "Optional user-visible request label stored on the created AKK conversation."
322
236
  },
323
- forkSummary: {
324
- type: "string",
325
- description: "Required when strategy=fork and createConversation=true. OpenClaw-approved summary of the bounded source context to inject into the new forked AKK session."
326
- },
327
- openclawSession: {
328
- type: "string",
329
- description: "OpenClaw session label recorded in the created AKK conversation."
330
- },
331
237
  codexHome: {
332
238
  type: "string",
333
239
  description: "Optional Codex home directory. Defaults to ~/.codex."
334
- },
335
- maxMessages: {
336
- type: "number",
337
- description: "Maximum rollout messages to include for fork context."
338
- },
339
- maxCommands: {
340
- type: "number",
341
- description: "Maximum rollout command records to include for fork context."
342
- },
343
- maxTextLength: {
344
- type: "number",
345
- description: "Maximum text length per fork-context message."
346
240
  }
347
241
  }
348
242
  };
@@ -364,7 +258,7 @@ const approveParameters = {
364
258
  const plugin = definePluginEntry({
365
259
  id: "agent-knock-knock",
366
260
  name: "Agent Knock Knock",
367
- description: "Agent Knock Knock (AKK/akk) delegates OpenClaw coding work to local Codex, Claude, or Cursor agents. Use this for AKK, akk, Agent Knock Knock, Codex delegation, Claude delegation, Cursor delegation, task listing, follow-up messages, status, stalled terminal-monitor renewal, recovery, cancel requests, and close requests. Default delegation target comes from plugin config defaultAgent and falls back to Codex when unset; explicit user agent requests override it.",
261
+ description: "Agent Knock Knock (AKK/akk) lets OpenClaw operate local Codex and Claude Code through shared tmux terminals, with visible monitoring, approvals, callbacks, cancellation, and seamless human takeover.",
368
262
  register(api) {
369
263
  api.registerGatewayMethod(CALLBACK_METHOD, async ({ params, respond }) => {
370
264
  try {
@@ -414,17 +308,17 @@ const plugin = definePluginEntry({
414
308
  default: "AKK is handling the request..."
415
309
  },
416
310
  agentPromptGuidance: [
417
- "Use /akk <task> to delegate coding work to Agent Knock Knock. /akk uses the plugin-configured defaultAgent and falls back to Codex when unset; use /akk claude <task>, /akk cursor <task>, or /akk codex <task> when the user explicitly requests that agent."
311
+ "Use /akk <task> to send work through a uniquely matching idle tmux terminal. /akk uses the plugin-configured defaultAgent and falls back to Codex; use /akk claude <task> or /akk codex <task> when the user explicitly requests that agent."
418
312
  ],
419
313
  handler: async (ctx) => handleAkkCommand(api, ctx)
420
314
  });
421
315
  api.registerTool((toolContext) => ({
422
316
  label: "AKK Delegate",
423
317
  name: "agent_knock_knock_delegate",
424
- description: "Delegate an implementation task to a local coding agent. Use this when the user says AKK, akk, Agent Knock Knock, asks to hand work to Codex, Claude, or Cursor, or asks OpenClaw to start a background coding-agent task. If the user says AKK without an explicit agent, omit the agent parameter so the plugin-configured defaultAgent is used, falling back to Codex when unset. The tool starts the coding agent in the background and returns only protocol metadata, not raw terminal output.",
318
+ description: "Send an implementation task to a uniquely matching idle Codex or Claude Code tmux terminal. AKK never starts a hidden coding-agent runtime; no match or an ambiguous match fails closed with setup guidance. Omit agent to use the configured default, falling back to Codex.",
425
319
  parameters: delegateParameters,
426
320
  async execute(_toolCallId, params) {
427
- const result = runDelegate(api, params, toolContext);
321
+ const result = await runDelegate(api, params, toolContext);
428
322
  return {
429
323
  content: [
430
324
  {
@@ -496,7 +390,6 @@ const plugin = definePluginEntry({
496
390
  buildArgs: (params, toolContext) => {
497
391
  const config = isRecord(api.pluginConfig) ? api.pluginConfig : {};
498
392
  const openclawSession = stringValue(toolContext?.sessionKey) ??
499
- stringValue(config.openclawSession) ??
500
393
  "agent:main:main";
501
394
  const args = [
502
395
  "send",
@@ -507,9 +400,6 @@ const plugin = definePluginEntry({
507
400
  "--background"
508
401
  ];
509
402
  pushOptional(args, "--type", stringValue(params.type));
510
- const overrides = resolveConversationOverrides(params, config);
511
- pushOptional(args, "--all-proxy", overrides.allProxy);
512
- pushOptional(args, "--model", overrides.model);
513
403
  pushOptional(args, "--store-dir", resolvePluginStoreDir(config));
514
404
  pushOptional(args, "--idle-timeout-minutes", numberString(params.idleTimeoutMinutes) ?? numberString(config.idleTimeoutMinutes));
515
405
  pushOptional(args, "--agent-timeout-minutes", numberString(params.agentTimeoutMinutes) ?? numberString(config.agentTimeoutMinutes));
@@ -518,9 +408,6 @@ const plugin = definePluginEntry({
518
408
  pushOptional(args, "--gateway-method", CALLBACK_METHOD);
519
409
  pushOptional(args, "--gateway-session", openclawSession);
520
410
  pushOptional(args, "--openclaw-bin", stringValue(config.openclawBin));
521
- pushOptional(args, "--callback-command", stringValue(config.callbackCommand));
522
- pushOptional(args, "--soft-limit", numberString(config.softLimit));
523
- pushOptional(args, "--hard-limit", numberString(config.hardLimit));
524
411
  return args;
525
412
  }
526
413
  });
@@ -549,7 +436,7 @@ const plugin = definePluginEntry({
549
436
  });
550
437
  registerCliTool(api, {
551
438
  name: "agent_knock_knock_retry_callback",
552
- description: "Retry a persisted AKK callback that failed before reaching OpenClaw. The original callback message id is reused for idempotent delivery, and the task closes only after delivery succeeds.",
439
+ description: "Retry a persisted AKK callback that failed before reaching OpenClaw. The original callback message id is reused for idempotent delivery; a completed terminal task remains available for follow-up until idle cleanup.",
553
440
  parameters: retryCallbackParameters,
554
441
  buildArgs: (params) => {
555
442
  const config = isRecord(api.pluginConfig) ? api.pluginConfig : {};
@@ -560,44 +447,35 @@ const plugin = definePluginEntry({
560
447
  });
561
448
  registerCliTool(api, {
562
449
  name: "agent_knock_knock_cancel",
563
- description: "Cancel an existing Agent Knock Knock Codex, Claude, or Cursor task. Delegated sessions use cooperative ACPX cancellation. Terminal-controlled Claude sends Escape to dismiss or interrupt the current screen flow; other adapters use their declared interrupt keys. The underlying tmux pane remains open.",
450
+ description: "Interrupt an existing Agent Knock Knock Codex or Claude Code tmux task. Claude sends Escape; Codex uses its declared interrupt key. The shared tmux pane remains open for human takeover.",
564
451
  parameters: cancelParameters,
565
452
  buildArgs: (params) => {
566
453
  const config = isRecord(api.pluginConfig) ? api.pluginConfig : {};
567
- const overrides = resolveConversationOverrides(params, config);
568
454
  const args = ["cancel", "--conversation", requiredString(params.conversation_id, "conversation_id")];
569
- pushOptional(args, "--all-proxy", overrides.allProxy);
570
455
  pushOptional(args, "--store-dir", resolvePluginStoreDir(config));
571
456
  pushOptional(args, "--idle-timeout-minutes", numberString(params.idleTimeoutMinutes) ?? numberString(config.idleTimeoutMinutes));
572
457
  return args;
573
458
  }
574
459
  });
575
- registerCliTool(api, {
576
- name: "agent_knock_knock_recover",
577
- description: "Recover an Agent Knock Knock task whose coding-agent session is unavailable by starting a new session with AKK's saved protocol history summary plus the pending message. Use only after the user chooses recovery.",
578
- parameters: recoveryParameters,
579
- buildArgs: (params) => buildRecoveryArgs(api, "recover", params)
580
- });
581
460
  registerCliTool(api, {
582
461
  name: "agent_knock_knock_close",
583
- description: "Close an Agent Knock Knock coding-agent task without terminating the underlying ACPX session. Use this for AKK close requests.",
462
+ description: "Close an AKK-managed task record without terminating the shared tmux pane. For an orphaned terminal dispatch only, the user must explicitly request recovery and provide the exact expected_message_id reported by AKK list.",
584
463
  parameters: closeParameters,
585
464
  buildArgs: (params) => {
586
465
  const args = ["close", "--conversation", requiredString(params.conversation_id, "conversation_id")];
587
466
  pushOptional(args, "--reason", stringValue(params.reason));
467
+ pushOptional(args, "--expected-message-id", stringValue(params.expected_message_id));
588
468
  pushOptional(args, "--store-dir", resolvePluginStoreDir(isRecord(api.pluginConfig) ? api.pluginConfig : {}));
589
469
  return args;
590
470
  }
591
471
  });
592
472
  registerCliTool(api, {
593
473
  name: "agent_knock_knock_agent_takeover",
594
- description: "Build or execute a takeover plan for an existing native local coding-agent session. Use this for AKK takeover requests. By default it is side-effect-free and returns the plan; with createConversation=true it can attach a ready native session or create a confirmed forked AKK-managed conversation. For terminate_then_resume, only call with confirmTerminate=true and expectedPid after the user explicitly confirms stopping that exact Codex CLI pid. If Codex does not expose a session id, allowCwdOnly=true is a higher-risk explicit-pid fallback.",
474
+ description: "Build or execute a terminal-control attachment plan for an existing Codex session running inside tmux. The first call is side-effect-free; attach only after the user confirms the exact tmux target.",
595
475
  parameters: agentTakeoverParameters,
596
476
  buildArgs: (params, toolContext) => {
597
477
  const config = isRecord(api.pluginConfig) ? api.pluginConfig : {};
598
478
  const openclawSession = stringValue(toolContext?.sessionKey) ??
599
- stringValue(config.openclawSession) ??
600
- stringValue(params.openclawSession) ??
601
479
  "agent:main:main";
602
480
  const args = [
603
481
  "agent",
@@ -612,32 +490,18 @@ const plugin = definePluginEntry({
612
490
  if (params.createConversation === true) {
613
491
  args.push("--create-conversation");
614
492
  }
615
- if (params.confirmTerminate === true) {
616
- args.push("--confirm-terminate");
617
- }
618
- if (params.allowCwdOnly === true) {
619
- args.push("--allow-cwd-only");
620
- }
621
493
  if (params.confirmTerminal === true) {
622
494
  args.push("--confirm-terminal");
623
495
  }
624
- pushOptional(args, "--expected-pid", numberString(params.expectedPid));
625
496
  pushOptional(args, "--terminal-target", stringValue(params.terminalTarget));
626
497
  pushOptional(args, "--request", stringValue(params.request));
627
- pushOptional(args, "--fork-summary", stringValue(params.forkSummary));
628
498
  pushOptional(args, "--store-dir", resolvePluginStoreDir(config));
629
499
  pushOptional(args, "--openclaw-session", openclawSession);
630
500
  pushOptional(args, "--gateway-method", CALLBACK_METHOD);
631
501
  pushOptional(args, "--gateway-session", openclawSession);
632
502
  pushOptional(args, "--openclaw-bin", stringValue(config.openclawBin));
633
- pushOptional(args, "--callback-command", stringValue(config.callbackCommand));
634
- pushOptional(args, "--soft-limit", numberString(config.softLimit));
635
- pushOptional(args, "--hard-limit", numberString(config.hardLimit));
636
503
  pushOptional(args, "--idle-timeout-minutes", numberString(config.idleTimeoutMinutes));
637
504
  pushOptional(args, "--codex-home", stringValue(params.codexHome) ?? stringValue(api.pluginConfig?.codexHome));
638
- pushOptional(args, "--max-messages", numberString(params.maxMessages));
639
- pushOptional(args, "--max-commands", numberString(params.maxCommands));
640
- pushOptional(args, "--max-text-length", numberString(params.maxTextLength));
641
505
  return args;
642
506
  }
643
507
  });
@@ -651,13 +515,16 @@ async function handleAkkCommand(api, ctx) {
651
515
  return { text: akkUsageText() };
652
516
  }
653
517
  if (parsed.action === "delegate") {
654
- const result = runDelegate(api, {
518
+ const result = await runDelegate(api, {
655
519
  agent: parsed.agent,
656
520
  request: parsed.request
657
521
  }, {
658
522
  sessionKey: ctx.sessionKey
659
523
  });
660
- return { text: formatDelegateCommandResult(result) };
524
+ return {
525
+ text: formatDelegateCommandResult(result),
526
+ isError: result.status !== "async_pending"
527
+ };
661
528
  }
662
529
  const config = isRecord(api.pluginConfig) ? api.pluginConfig : {};
663
530
  const args = buildAkkCommandCliArgs(parsed, config, {
@@ -684,15 +551,18 @@ async function handleAkkCommand(api, ctx) {
684
551
  case "describe":
685
552
  return { text: formatDescribeCommandResult(result) };
686
553
  case "send":
687
- return { text: formatSendCommandResult(result) };
554
+ return {
555
+ text: formatSendCommandResult(result),
556
+ isError: ["uncertain", "aborted"].includes(String(result.submission_outcome ?? ""))
557
+ };
558
+ case "approve":
559
+ return { text: formatApproveCommandResult(result) };
688
560
  case "renew":
689
561
  return { text: formatRenewCommandResult(result) };
690
562
  case "retry-callback":
691
563
  return { text: formatRetryCallbackCommandResult(result) };
692
564
  case "cancel":
693
565
  return { text: formatCancelCommandResult(result) };
694
- case "recover":
695
- return { text: formatRecoveryCommandResult(result) };
696
566
  case "close":
697
567
  return { text: formatCloseCommandResult(result) };
698
568
  }
@@ -707,6 +577,22 @@ async function handleAkkCommand(api, ctx) {
707
577
  }
708
578
  function formatDelegateCommandResult(result) {
709
579
  const agent = executorDisplayName(result.agent);
580
+ if (result.status === "submission_uncertain") {
581
+ return [
582
+ `AKK could not prove whether ${agent} received the terminal task.`,
583
+ `conversation: ${result.conversation_id ?? "unknown"}`,
584
+ `session: ${result.session ?? "unknown"}`,
585
+ "next: do not retry automatically; inspect AKK status and the named tmux pane."
586
+ ].join("\n");
587
+ }
588
+ if (result.status === "submission_aborted") {
589
+ return [
590
+ `AKK stopped before sending the terminal task to ${agent}.`,
591
+ `conversation: ${result.conversation_id ?? "unknown"}`,
592
+ `session: ${result.session ?? "unknown"}`,
593
+ "next: no tmux input was sent, so this request may be retried."
594
+ ].join("\n");
595
+ }
710
596
  return [
711
597
  `AKK delegated the task to ${agent}.`,
712
598
  `conversation: ${result.conversation_id ?? "unknown"}`,
@@ -741,7 +627,6 @@ function formatStatusCommandResult(result) {
741
627
  function formatDoctorCommandResult(result) {
742
628
  const capabilities = isRecord(result.capabilities) ? result.capabilities : {};
743
629
  const tmux = isRecord(capabilities.tmux) ? capabilities.tmux : {};
744
- const acpx = isRecord(capabilities.acpx) ? capabilities.acpx : {};
745
630
  const openclaw = isRecord(result.openclaw) ? result.openclaw : {};
746
631
  const checks = Array.isArray(openclaw.checks) ? openclaw.checks : [];
747
632
  const failures = checks
@@ -752,9 +637,7 @@ function formatDoctorCommandResult(result) {
752
637
  : []))].slice(0, 3);
753
638
  return [
754
639
  `AKK doctor: ${result.ok === true ? "ready" : "needs attention"}`,
755
- `mode: ${result.selected_mode ?? "all"}`,
756
- `tmux: ${tmux.checked === false ? "not checked" : tmux.status ?? "unknown"}`,
757
- `acpx: ${acpx.checked === false ? "not checked" : acpx.status ?? "unknown"}`,
640
+ `tmux: ${tmux.status ?? "unknown"}`,
758
641
  `OpenClaw package: ${openclaw.package_ready === true ? "ready" : "not ready"}`,
759
642
  `Gateway: ${openclaw.gateway_ready === true ? "healthy" : "unavailable"}`,
760
643
  ...(failures.length > 0 ? [`check: ${failures.join(", ")}`] : []),
@@ -796,6 +679,22 @@ function formatSendCommandResult(result) {
796
679
  const conversationId = conversation.conversation_id ?? result.conversation_id ?? "unknown";
797
680
  const status = conversation.status ?? result.status ?? "unknown";
798
681
  const nextAction = isRecord(result.openclaw_next_action) ? result.openclaw_next_action : undefined;
682
+ if (result.submission_outcome === "uncertain") {
683
+ return [
684
+ "AKK terminal submission outcome is uncertain.",
685
+ `conversation: ${conversationId}`,
686
+ `status: ${result.status ?? status}`,
687
+ "next: do not retry automatically; inspect AKK status and the named tmux pane."
688
+ ].join("\n");
689
+ }
690
+ if (result.submission_outcome === "aborted") {
691
+ return [
692
+ "AKK terminal submission was aborted before tmux input.",
693
+ `conversation: ${conversationId}`,
694
+ `status: ${result.status ?? status}`,
695
+ "next: this request was not sent and may be retried."
696
+ ].join("\n");
697
+ }
799
698
  const lines = [
800
699
  "AKK follow-up sent.",
801
700
  `conversation: ${conversationId}`,
@@ -822,17 +721,30 @@ function formatCancelCommandResult(result) {
822
721
  `status: ${conversation.status ?? (result.cancel_requested === true ? "cancel requested" : "not cancelled")}`
823
722
  ].join("\n");
824
723
  }
825
- function formatRecoveryCommandResult(result) {
724
+ function formatApproveCommandResult(result) {
826
725
  const conversation = result.conversation ?? {};
827
726
  return [
828
- "AKK recovery started.",
829
- `conversation: ${conversation.conversation_id ?? "unknown"}`,
830
- `agent: ${result.executor?.kind ?? conversation.executor?.kind ?? "unknown"}`,
831
- `session: ${result.executor?.session ?? conversation.executor?.session ?? "unknown"}`,
832
- `status: ${conversation.status ?? "unknown"}`
727
+ result.approved === true
728
+ ? "AKK approved the current terminal request."
729
+ : "AKK did not approve the terminal request.",
730
+ `conversation: ${conversation.conversation_id ?? result.conversation_id ?? "unknown"}`,
731
+ `status: ${conversation.status ?? "unknown"}`,
732
+ ...(result.reason ? [`reason: ${result.reason}`] : [])
833
733
  ].join("\n");
834
734
  }
835
735
  function formatCloseCommandResult(result) {
736
+ if (result.source === "terminal_control" &&
737
+ result.terminal_dispatch_resolved === true) {
738
+ const terminalControl = isRecord(result.terminal_control)
739
+ ? result.terminal_control
740
+ : {};
741
+ return [
742
+ "AKK cleared the unresolved terminal dispatch fence.",
743
+ `terminal: ${terminalControl.target ?? "unknown"}`,
744
+ `previous owner: ${result.owner_conversation_id ?? "unknown"}`,
745
+ "The coding agent and tmux pane remain open."
746
+ ].join("\n");
747
+ }
836
748
  const conversation = result.conversation ?? {};
837
749
  return [
838
750
  "AKK session closed.",
@@ -840,7 +752,7 @@ function formatCloseCommandResult(result) {
840
752
  `status: ${conversation.status ?? "unknown"}`
841
753
  ].join("\n");
842
754
  }
843
- function runDelegate(api, params, toolContext) {
755
+ async function runDelegate(api, params, toolContext) {
844
756
  const config = isRecord(api.pluginConfig) ? api.pluginConfig : {};
845
757
  const binPath = stringValue(config.binPath) ?? defaultBinPath;
846
758
  const workspace = stringValue(params.workspace) ?? stringValue(config.workspace) ?? process.cwd();
@@ -848,21 +760,7 @@ function runDelegate(api, params, toolContext) {
848
760
  const prefixedRequest = stringValue(params.agent) ? undefined : parseLeadingExecutorAlias(rawRequest);
849
761
  const request = prefixedRequest?.request ?? rawRequest;
850
762
  const agent = executorDefinitionForKind(stringValue(params.agent) ?? prefixedRequest?.kind ?? stringValue(config.defaultAgent) ?? "codex").kind;
851
- const executorDefinition = executorDefinitionForKind(agent);
852
- const agentSession = stringValue(params.session) ??
853
- firstStringForKeys(params, executorDefinition.sessionConfigKeys) ??
854
- firstStringForKeys(config, executorDefinition.sessionConfigKeys);
855
- const allProxy = stringValue(params.allProxy) ??
856
- firstStringForKeys(params, executorDefinition.proxyConfigKeys) ??
857
- firstStringForKeys(config, executorDefinition.proxyConfigKeys) ??
858
- stringValue(config.allProxy);
859
- const model = stringValue(params.model) ??
860
- firstStringForKeys(params, executorDefinition.modelConfigKeys) ??
861
- firstStringForKeys(config, executorDefinition.modelConfigKeys) ??
862
- stringValue(config.model);
863
763
  const openclawSession = stringValue(toolContext?.sessionKey) ??
864
- stringValue(config.openclawSession) ??
865
- stringValue(params.openclawSession) ??
866
764
  "agent:main:main";
867
765
  const args = [
868
766
  binPath,
@@ -876,56 +774,72 @@ function runDelegate(api, params, toolContext) {
876
774
  "--background"
877
775
  ];
878
776
  pushOptional(args, "--store-dir", resolvePluginStoreDir(config));
879
- pushOptional(args, "--session", agentSession);
880
- pushOptional(args, "--all-proxy", allProxy);
881
- pushOptional(args, "--model", model);
882
777
  pushOptional(args, "--openclaw-session", openclawSession);
883
778
  pushOptional(args, "--gateway-method", CALLBACK_METHOD);
884
779
  pushOptional(args, "--gateway-session", openclawSession);
885
780
  pushOptional(args, "--openclaw-bin", stringValue(config.openclawBin));
886
- pushOptional(args, "--callback-command", stringValue(config.callbackCommand));
887
- pushOptional(args, "--soft-limit", numberString(params.softLimit) ?? numberString(config.softLimit));
888
- pushOptional(args, "--hard-limit", numberString(params.hardLimit) ?? numberString(config.hardLimit));
889
781
  pushOptional(args, "--idle-timeout-minutes", numberString(params.idleTimeoutMinutes) ?? numberString(config.idleTimeoutMinutes));
890
782
  pushOptional(args, "--agent-timeout-minutes", numberString(params.agentTimeoutMinutes) ?? numberString(config.agentTimeoutMinutes));
891
- const spawned = spawnSync(process.execPath, args, {
892
- encoding: "utf8",
893
- maxBuffer: 1024 * 1024 * 10,
894
- cwd: workspace
895
- });
896
- if (spawned.error) {
897
- throw new Error(`agent-knock-knock delegate failed to start: ${spawned.error.message}`);
898
- }
899
- if (spawned.status !== 0) {
900
- throw new Error(cleanError(spawned.stderr || spawned.stdout || `agent-knock-knock delegate exited with status ${spawned.status}`));
901
- }
902
- const parsed = parseJson(spawned.stdout);
903
- const conversationId = parsed.conversation?.conversation_id;
904
- const statePath = parsed.paths?.statePath;
905
- const logPath = parsed.paths?.logPath;
783
+ pushOptional(args, "--agent-hard-timeout-minutes", numberString(params.agentHardTimeoutMinutes) ?? numberString(config.agentHardTimeoutMinutes));
784
+ const parsed = await runCliAsync(api, args.slice(1), { cwd: workspace });
785
+ const conversationId = parsed.conversation?.conversation_id ??
786
+ parsed.conversation_id;
787
+ const statePath = parsed.conversation?.state_path ??
788
+ parsed.paths?.statePath;
789
+ const logPath = parsed.conversation?.event_log_path ??
790
+ parsed.paths?.logPath;
791
+ const submissionUncertain = parsed.submission_outcome === "uncertain";
792
+ const submissionAborted = parsed.submission_outcome === "aborted";
906
793
  return {
907
- status: "async_pending",
908
- delegation_status: "delegated",
794
+ status: submissionUncertain
795
+ ? "submission_uncertain"
796
+ : submissionAborted
797
+ ? "submission_aborted"
798
+ : "async_pending",
799
+ delegation_status: submissionUncertain
800
+ ? "uncertain"
801
+ : submissionAborted
802
+ ? "aborted"
803
+ : "delegated",
909
804
  conversation_id: conversationId,
910
805
  conversation_status: parsed.conversation?.status,
911
806
  state_path: statePath,
912
807
  event_log_path: logPath,
913
808
  agent,
914
809
  executor: parsed.conversation?.executor,
915
- session: parsed.conversation?.executor?.session ?? parsed.conversation?.claude_session,
916
- claude_session: parsed.conversation?.claude_session,
810
+ session: parsed.conversation?.executor?.session ??
811
+ parsed.terminal_control?.target,
917
812
  openclaw_session: openclawSession,
918
813
  launched: parsed.launched === true,
814
+ replayed: parsed.replayed === true,
919
815
  background: parsed.background === true,
920
- pid: parsed.pid ?? null,
816
+ pid: parsed.pid ?? parsed.terminal_control?.panePid ?? null,
921
817
  callback_method: CALLBACK_METHOD,
922
- openclaw_next_action: {
923
- action: "yield",
924
- reason: "The delegated coding agent is running asynchronously. End this OpenClaw turn now and wait for an Agent Knock Knock callback.",
925
- do_not: "Do not inspect event logs, process lists, session internals, files, stdout, or stderr while waiting. Follow-up communication must only use structured callbacks from agent-knock-knock.",
926
- expected_callback: "The callback will be injected and scheduled into this OpenClaw session by the agent-knock-knock.callback Gateway method."
927
- },
928
- note: "The coding agent was launched in the background. This is an async delegation; OpenClaw should yield now and wait for the scheduled callback turn."
818
+ ...(submissionUncertain
819
+ ? {
820
+ submission_outcome: "uncertain",
821
+ do_not_retry: true,
822
+ reason: parsed.reason,
823
+ openclaw_next_action: parsed.openclaw_next_action,
824
+ note: "AKK could not prove whether tmux accepted Enter. Do not retry automatically; inspect the durable AKK conversation and shared terminal."
825
+ }
826
+ : submissionAborted
827
+ ? {
828
+ submission_outcome: "aborted",
829
+ safe_to_retry: true,
830
+ reason: parsed.reason,
831
+ openclaw_next_action: parsed.openclaw_next_action,
832
+ note: "AKK stopped before sending tmux input. The request was not submitted and may be retried."
833
+ }
834
+ : {
835
+ openclaw_next_action: {
836
+ action: "yield",
837
+ reason: "The coding agent is working in the shared tmux terminal. End this OpenClaw turn now and wait for an Agent Knock Knock callback.",
838
+ do_not: "Do not poll terminal internals while waiting. Follow-up communication must use Agent Knock Knock tools so the same shared terminal remains authoritative.",
839
+ expected_callback: "The callback will be injected and scheduled into this OpenClaw session by the agent-knock-knock.callback Gateway method."
840
+ },
841
+ note: "The task was sent to the shared tmux terminal. OpenClaw should yield now and wait for the scheduled callback turn."
842
+ })
929
843
  };
930
844
  }
931
845
  function registerCliTool(api, { name, description, parameters, buildArgs }) {
@@ -957,17 +871,6 @@ function toolLabel(name) {
957
871
  .join(" ");
958
872
  return `AKK ${action || "Tool"}`;
959
873
  }
960
- function buildRecoveryArgs(api, command, params) {
961
- const config = isRecord(api.pluginConfig) ? api.pluginConfig : {};
962
- const overrides = resolveConversationOverrides(params, config);
963
- const args = [command, "--conversation", requiredString(params.conversation_id, "conversation_id"), "--background"];
964
- pushOptional(args, "--session", stringValue(params.session));
965
- pushOptional(args, "--all-proxy", overrides.allProxy);
966
- pushOptional(args, "--model", overrides.model);
967
- pushOptional(args, "--store-dir", resolvePluginStoreDir(config));
968
- pushOptional(args, "--idle-timeout-minutes", numberString(params.idleTimeoutMinutes) ?? numberString(config.idleTimeoutMinutes));
969
- return args;
970
- }
971
874
  function runCli(api, cliArgs, { cwd = process.cwd(), allowNonzeroJson = false } = {}) {
972
875
  const config = isRecord(api.pluginConfig) ? api.pluginConfig : {};
973
876
  const binPath = stringValue(config.binPath) ?? defaultBinPath;
@@ -1205,11 +1108,10 @@ function formatDoneShortcuts(conversationId) {
1205
1108
  "",
1206
1109
  "[AKK convenience commands]",
1207
1110
  "When summarizing this result to the user, include these short next-step commands:",
1208
- "- `AKK list` lists open AKK sessions.",
1209
- `- \`AKK send ${conversationId}: <message>\` sends a follow-up to this same AKK session.`,
1210
- `- \`AKK status ${conversationId}\` shows this session status.`,
1211
- `- \`AKK cancel ${conversationId}\` requests cancellation of current running work without closing this AKK session.`,
1212
- `- \`AKK close ${conversationId}\` closes this AKK session.`
1111
+ "- `AKK list` lists live shared terminals and open AKK tasks.",
1112
+ `- \`AKK send ${conversationId}: <message>\` continues in this same shared terminal.`,
1113
+ `- \`AKK status ${conversationId}\` shows this task record.`,
1114
+ `- \`AKK close ${conversationId}\` closes the AKK record without closing tmux.`
1213
1115
  ].join("\n");
1214
1116
  }
1215
1117
  function pushOptional(args, flag, value) {
@@ -1223,18 +1125,6 @@ function isRecord(value) {
1223
1125
  function stringValue(value) {
1224
1126
  return typeof value === "string" && value.trim() ? value : undefined;
1225
1127
  }
1226
- function firstStringForKeys(source, keys) {
1227
- if (!isRecord(source)) {
1228
- return undefined;
1229
- }
1230
- for (const key of keys) {
1231
- const value = stringValue(source[key]);
1232
- if (value) {
1233
- return value;
1234
- }
1235
- }
1236
- return undefined;
1237
- }
1238
1128
  function numberString(value) {
1239
1129
  return typeof value === "number" && Number.isFinite(value) ? String(value) : undefined;
1240
1130
  }