@scotthuang/agent-knock-knock 0.6.2 → 0.8.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.
@@ -6,6 +6,7 @@ import { AKK_CALLBACK_METHOD, akkUsageText, buildAkkCommandCliArgs, formatAkkLis
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));
9
+ const relayPathByApi = new WeakMap();
9
10
  const sendParameters = {
10
11
  type: "object",
11
12
  additionalProperties: false,
@@ -25,7 +26,7 @@ const sendParameters = {
25
26
  },
26
27
  idleTimeoutMinutes: {
27
28
  type: "number",
28
- description: "Minutes an idle AKK session remains open before lazy cleanup closes it."
29
+ description: "Minutes an idle AKK session remains open before controlled reconciliation closes its managed-turn record."
29
30
  },
30
31
  agentTimeoutMinutes: {
31
32
  type: "number",
@@ -52,13 +53,9 @@ const listParameters = {
52
53
  all: {
53
54
  type: "boolean"
54
55
  },
55
- managedOnly: {
56
- type: "boolean",
57
- description: "When true, only list AKK-managed turns and skip live tmux terminal discovery."
58
- },
59
56
  noApprovalScan: {
60
57
  type: "boolean",
61
- description: "When true, list terminal-controlled sessions without scanning terminal panes for approval prompts."
58
+ description: "When true, list live terminals without scanning their panes for approval prompts."
62
59
  },
63
60
  terminalDebug: {
64
61
  type: "boolean",
@@ -103,7 +100,7 @@ const statusParameters = {
103
100
  properties: {
104
101
  conversation_id: {
105
102
  type: "string",
106
- description: "AKK-managed conversation id, or a terminal-controlled id from AKK list such as terminal:v2:tmux:codex:codex-work:0.1:33389."
103
+ description: "Managed-turn conversation id, or a live terminal id from AKK list such as terminal:v2:tmux:codex:codex-work:0.1:33389."
107
104
  },
108
105
  idleTimeoutMinutes: {
109
106
  type: "number"
@@ -121,7 +118,7 @@ const cancelParameters = {
121
118
  properties: {
122
119
  conversation_id: {
123
120
  type: "string",
124
- description: "AKK-managed conversation id, or a terminal-controlled id from AKK list such as terminal:v2:tmux:codex:codex-work:0.1:33389."
121
+ description: "Managed-turn conversation id, or a live terminal id from AKK list such as terminal:v2:tmux:codex:codex-work:0.1:33389."
125
122
  },
126
123
  idleTimeoutMinutes: {
127
124
  type: "number"
@@ -152,7 +149,7 @@ const approveParameters = {
152
149
  properties: {
153
150
  conversation_id: {
154
151
  type: "string",
155
- description: "AKK-managed conversation id, or a terminal-controlled id from AKK list such as terminal:v2:tmux:codex:codex-work:0.1:33389."
152
+ description: "Managed-turn conversation id, or a live terminal id from AKK list such as terminal:v2:tmux:codex:codex-work:0.1:33389."
156
153
  },
157
154
  expected_approval_fingerprint: {
158
155
  type: "string",
@@ -160,171 +157,175 @@ const approveParameters = {
160
157
  }
161
158
  }
162
159
  };
163
- const plugin = definePluginEntry({
164
- id: "agent-knock-knock",
165
- name: "Agent Knock Knock",
166
- 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.",
167
- register(api) {
168
- api.registerGatewayMethod(CALLBACK_METHOD, async ({ params, respond }) => {
160
+ function createPlugin(relayPath) {
161
+ return definePluginEntry({
162
+ id: "agent-knock-knock",
163
+ name: "Agent Knock Knock",
164
+ 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.",
165
+ register(api) {
166
+ relayPathByApi.set(api, relayPath);
167
+ api.registerGatewayMethod(CALLBACK_METHOD, async ({ params, respond }) => {
168
+ try {
169
+ const result = await handleCallback(api, params);
170
+ respond(true, result);
171
+ }
172
+ catch (error) {
173
+ const message = error instanceof Error ? error.message : String(error);
174
+ api.logger.warn?.(`agent-knock-knock callback failed: ${message}`);
175
+ respond(false, undefined, {
176
+ code: "AGENT_KNOCK_KNOCK_CALLBACK_FAILED",
177
+ message
178
+ });
179
+ }
180
+ }, { scope: "operator.write" });
169
181
  try {
170
- const result = await handleCallback(api, params);
171
- respond(true, result);
182
+ api.registerService?.({
183
+ id: "agent-knock-knock-monitor-reconciliation",
184
+ start() {
185
+ try {
186
+ const config = isRecord(api.pluginConfig) ? api.pluginConfig : {};
187
+ const args = ["reconcile-monitors"];
188
+ pushOptional(args, "--store-dir", resolvePluginStoreDir(config));
189
+ const result = runCli(api, args);
190
+ api.logger.info?.("agent-knock-knock monitor reconciliation: " +
191
+ `checked=${result.checked ?? 0} launched=${result.launched ?? 0} ` +
192
+ `already_running=${result.already_running ?? 0} skipped=${result.skipped ?? 0} ` +
193
+ `errors=${result.errors ?? 0}`);
194
+ }
195
+ catch (error) {
196
+ const message = error instanceof Error ? error.message : String(error);
197
+ api.logger.warn?.(`agent-knock-knock monitor reconciliation skipped after startup error: ${message}`);
198
+ }
199
+ }
200
+ });
172
201
  }
173
202
  catch (error) {
174
203
  const message = error instanceof Error ? error.message : String(error);
175
- api.logger.warn?.(`agent-knock-knock callback failed: ${message}`);
176
- respond(false, undefined, {
177
- code: "AGENT_KNOCK_KNOCK_CALLBACK_FAILED",
178
- message
179
- });
204
+ api.logger.warn?.(`agent-knock-knock monitor reconciliation service was not registered: ${message}`);
180
205
  }
181
- }, { scope: "operator.write" });
182
- try {
183
- api.registerService?.({
184
- id: "agent-knock-knock-monitor-reconciliation",
185
- start() {
186
- try {
187
- const config = isRecord(api.pluginConfig) ? api.pluginConfig : {};
188
- const args = ["reconcile-monitors"];
189
- pushOptional(args, "--store-dir", resolvePluginStoreDir(config));
190
- const result = runCli(api, args);
191
- api.logger.info?.("agent-knock-knock monitor reconciliation: " +
192
- `checked=${result.checked ?? 0} launched=${result.launched ?? 0} ` +
193
- `already_running=${result.already_running ?? 0} skipped=${result.skipped ?? 0} ` +
194
- `errors=${result.errors ?? 0}`);
206
+ api.registerCommand?.({
207
+ name: "akk",
208
+ description: "Send coding work through existing Codex or Claude Code tmux terminals, inspect managed turns, approve exact prompts, and cancel running work.",
209
+ acceptsArgs: true,
210
+ requireAuth: true,
211
+ nativeProgressMessages: {
212
+ default: "AKK is handling the request..."
213
+ },
214
+ agentPromptGuidance: [
215
+ "Use /akk <task> when exactly one eligible idle coding-agent tmux pane should receive new work. Use /akk codex: <task>, /akk claude: <task>, or another selector returned by /akk list to target an existing pane. AKK never starts a coding agent."
216
+ ],
217
+ handler: async (ctx) => handleAkkCommand(api, ctx)
218
+ });
219
+ registerCliTool(api, {
220
+ name: "agent_knock_knock_list",
221
+ description: "List existing Codex and Claude Code tmux panes as the primary terminals[] resources. Each terminal may include managed.current_turn or managed.recent_turn; all=true also includes older managed.history and retained unavailable history. By default, unavailable_managed_turns contains attention-needed records whose pane is unavailable. Use only each row's available_actions and its authoritative prefilled arguments: send starts a new turn on a terminal, while follow_up continues a specific managed turn. AKK revalidates every side effect and never starts a coding agent.",
222
+ parameters: listParameters,
223
+ buildArgs: (params) => {
224
+ const config = isRecord(api.pluginConfig) ? api.pluginConfig : {};
225
+ const args = ["list", "--reconcile"];
226
+ pushOptional(args, "--store-dir", resolvePluginStoreDir(config));
227
+ pushOptional(args, "--idle-timeout-minutes", numberString(params.idleTimeoutMinutes) ?? numberString(api.pluginConfig?.idleTimeoutMinutes));
228
+ pushOptional(args, "--agent", stringValue(params.agent));
229
+ pushOptional(args, "--status", stringValue(params.status));
230
+ if (params.all === true) {
231
+ args.push("--all");
195
232
  }
196
- catch (error) {
197
- const message = error instanceof Error ? error.message : String(error);
198
- api.logger.warn?.(`agent-knock-knock monitor reconciliation skipped after startup error: ${message}`);
233
+ if (params.noApprovalScan === true) {
234
+ args.push("--no-approval-scan");
235
+ }
236
+ if (params.terminalDebug === true) {
237
+ args.push("--terminal-debug");
199
238
  }
239
+ return args;
200
240
  }
201
241
  });
202
- }
203
- catch (error) {
204
- const message = error instanceof Error ? error.message : String(error);
205
- api.logger.warn?.(`agent-knock-knock monitor reconciliation service was not registered: ${message}`);
206
- }
207
- api.registerCommand?.({
208
- name: "akk",
209
- description: "Send coding work through existing Codex or Claude Code tmux terminals, inspect tasks, approve exact prompts, and cancel running work.",
210
- acceptsArgs: true,
211
- requireAuth: true,
212
- nativeProgressMessages: {
213
- default: "AKK is handling the request..."
214
- },
215
- agentPromptGuidance: [
216
- "Use /akk <task> when exactly one eligible idle coding-agent tmux pane should receive new work. Use /akk codex: <task>, /akk claude: <task>, or another selector returned by /akk list to target an existing pane. AKK never starts a coding agent."
217
- ],
218
- handler: async (ctx) => handleAkkCommand(api, ctx)
219
- });
220
- registerCliTool(api, {
221
- name: "agent_knock_knock_list",
222
- description: "List AKK-managed work and existing Codex or Claude Code tmux terminals. Rows in delegated and terminal_controlled include available_actions with the exact tool name and authoritative prefilled target arguments for safe sending, inspection, approval, cancellation, and recovery. Use only actions present in that snapshot; AKK revalidates them before side effects. AKK never starts a coding agent.",
223
- parameters: listParameters,
224
- buildArgs: (params) => {
225
- const config = isRecord(api.pluginConfig) ? api.pluginConfig : {};
226
- const args = ["list"];
227
- pushOptional(args, "--store-dir", resolvePluginStoreDir(config));
228
- pushOptional(args, "--idle-timeout-minutes", numberString(params.idleTimeoutMinutes) ?? numberString(api.pluginConfig?.idleTimeoutMinutes));
229
- pushOptional(args, "--agent", stringValue(params.agent));
230
- pushOptional(args, "--status", stringValue(params.status));
231
- if (params.all === true) {
232
- args.push("--all");
242
+ api.registerTool((_toolContext) => ({
243
+ label: "AKK Status",
244
+ name: "agent_knock_knock_status",
245
+ description: "Inspect one AKK-managed turn or existing coding-agent tmux terminal. Returns live state, a bounded terminal screen, and available purpose/context with confidence and limitations. AKK never starts a coding agent.",
246
+ parameters: statusParameters,
247
+ async execute(_toolCallId, params) {
248
+ const result = runCli(api, buildStatusCliArgs(api, isRecord(params) ? params : {}));
249
+ return toolResult(result);
233
250
  }
234
- if (params.managedOnly === true) {
235
- args.push("--managed-only");
251
+ }), { name: "agent_knock_knock_status", optional: true });
252
+ api.registerTool((toolContext) => ({
253
+ label: "AKK Send",
254
+ name: "agent_knock_knock_send",
255
+ description: "Send through an existing Codex or Claude Code tmux terminal. Use a terminal row's send action to start a new managed turn, or a managed turn's follow_up action to continue that exact turn; both call this tool with the prefilled selector. Omit selector only when AKK should require one unique eligible idle pane. For ordinary use add only request and omit monitoring timeouts unless the user explicitly asks to change them. timeoutSeconds is unsupported. AKK never starts a coding agent. This is asynchronous: after acceptance, yield and wait for the callback or a later explicit status request.",
256
+ parameters: sendParameters,
257
+ async execute(_toolCallId, params) {
258
+ const result = await runSendRequest(api, isRecord(params) ? params : {}, toolContext);
259
+ return toolResult(result);
236
260
  }
237
- if (params.noApprovalScan === true) {
238
- args.push("--no-approval-scan");
261
+ }), { name: "agent_knock_knock_send", optional: true });
262
+ registerCliTool(api, {
263
+ name: "agent_knock_knock_approve",
264
+ description: "Manually approve the current AKK terminal permission request only after the user reviews and explicitly confirms it. Claude Code uses no Hooks: this manual path accepts only an exact one-time Bash permission screen for the current managed turn, then recaptures its short-lived evidence and process identity before sending Enter. Separately, trusted default-disabled plugin configuration can auto-approve an exact Claude command/workspace match without exposing policy control to the model. Hook-free durable completion is independently verified from the local Claude transcript.",
265
+ parameters: approveParameters,
266
+ buildArgs: (params) => {
267
+ const config = isRecord(api.pluginConfig) ? api.pluginConfig : {};
268
+ const args = ["approve", "--conversation", requiredString(params.conversation_id, "conversation_id")];
269
+ pushOptional(args, "--expected-approval-fingerprint", requiredString(params.expected_approval_fingerprint, "expected_approval_fingerprint"));
270
+ pushOptional(args, "--store-dir", resolvePluginStoreDir(config));
271
+ return args;
239
272
  }
240
- if (params.terminalDebug === true) {
241
- args.push("--terminal-debug");
273
+ });
274
+ registerCliTool(api, {
275
+ name: "agent_knock_knock_renew",
276
+ description: "Renew monitoring for a stalled AKK-managed terminal bridge task without sending text or keys to the coding agent. Use this when the user wants a still-live long-running terminal task to keep monitoring after an inactivity stall.",
277
+ parameters: renewParameters,
278
+ buildArgs: (params) => {
279
+ const config = isRecord(api.pluginConfig) ? api.pluginConfig : {};
280
+ const args = ["renew", "--conversation", requiredString(params.conversation_id, "conversation_id")];
281
+ pushOptional(args, "--minutes", numberString(params.minutes) ?? numberString(config.agentTimeoutMinutes));
282
+ pushOptional(args, "--store-dir", resolvePluginStoreDir(config));
283
+ return args;
242
284
  }
243
- return args;
244
- }
245
- });
246
- api.registerTool((_toolContext) => ({
247
- label: "AKK Status",
248
- name: "agent_knock_knock_status",
249
- description: "Inspect one AKK-managed turn or existing coding-agent tmux terminal. Returns live state, a bounded terminal screen, and available purpose/context with confidence and limitations. AKK never starts a coding agent.",
250
- parameters: statusParameters,
251
- async execute(_toolCallId, params) {
252
- const result = runCli(api, buildStatusCliArgs(api, isRecord(params) ? params : {}));
253
- return toolResult(result);
254
- }
255
- }), { name: "agent_knock_knock_status", optional: true });
256
- api.registerTool((toolContext) => ({
257
- label: "AKK Send",
258
- name: "agent_knock_knock_send",
259
- description: "Send a new task or follow-up through an existing Codex or Claude Code tmux terminal. Omit selector only when AKK should require one unique eligible idle pane; otherwise pass codex, claude, only, latest, an @short-ref from AKK list, or an authoritative full id. For ordinary use pass only request and, when targeting, selector; omit monitoring timeouts unless the user explicitly asks to change them. timeoutSeconds is unsupported. AKK never starts a coding agent. This is asynchronous: after acceptance, yield and wait for the callback or a later explicit status request.",
260
- parameters: sendParameters,
261
- async execute(_toolCallId, params) {
262
- const result = await runSendRequest(api, isRecord(params) ? params : {}, toolContext);
263
- return toolResult(result);
264
- }
265
- }), { name: "agent_knock_knock_send", optional: true });
266
- registerCliTool(api, {
267
- name: "agent_knock_knock_approve",
268
- description: "Manually approve the current AKK terminal permission request only after the user reviews and explicitly confirms it. Claude Code uses no Hooks: this manual path accepts only an exact one-time Bash permission screen for the current managed turn, then recaptures its short-lived evidence and process identity before sending Enter. Separately, trusted default-disabled plugin configuration can auto-approve an exact Claude command/workspace match without exposing policy control to the model. Hook-free durable completion is independently verified from the local Claude transcript.",
269
- parameters: approveParameters,
270
- buildArgs: (params) => {
271
- const config = isRecord(api.pluginConfig) ? api.pluginConfig : {};
272
- const args = ["approve", "--conversation", requiredString(params.conversation_id, "conversation_id")];
273
- pushOptional(args, "--expected-approval-fingerprint", requiredString(params.expected_approval_fingerprint, "expected_approval_fingerprint"));
274
- pushOptional(args, "--store-dir", resolvePluginStoreDir(config));
275
- return args;
276
- }
277
- });
278
- registerCliTool(api, {
279
- name: "agent_knock_knock_renew",
280
- description: "Renew monitoring for a stalled AKK-managed terminal bridge task without sending text or keys to the coding agent. Use this when the user wants a still-live long-running terminal task to keep monitoring after an inactivity stall.",
281
- parameters: renewParameters,
282
- buildArgs: (params) => {
283
- const config = isRecord(api.pluginConfig) ? api.pluginConfig : {};
284
- const args = ["renew", "--conversation", requiredString(params.conversation_id, "conversation_id")];
285
- pushOptional(args, "--minutes", numberString(params.minutes) ?? numberString(config.agentTimeoutMinutes));
286
- pushOptional(args, "--store-dir", resolvePluginStoreDir(config));
287
- return args;
288
- }
289
- });
290
- registerCliTool(api, {
291
- name: "agent_knock_knock_retry_callback",
292
- 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.",
293
- parameters: retryCallbackParameters,
294
- buildArgs: (params) => {
295
- const config = isRecord(api.pluginConfig) ? api.pluginConfig : {};
296
- const args = ["retry-callback", "--conversation", requiredString(params.conversation_id, "conversation_id")];
297
- pushOptional(args, "--store-dir", resolvePluginStoreDir(config));
298
- return args;
299
- }
300
- });
301
- registerCliTool(api, {
302
- name: "agent_knock_knock_cancel",
303
- 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.",
304
- parameters: cancelParameters,
305
- buildArgs: (params) => {
306
- const config = isRecord(api.pluginConfig) ? api.pluginConfig : {};
307
- const args = ["cancel", "--conversation", requiredString(params.conversation_id, "conversation_id")];
308
- pushOptional(args, "--store-dir", resolvePluginStoreDir(config));
309
- pushOptional(args, "--idle-timeout-minutes", numberString(params.idleTimeoutMinutes) ?? numberString(config.idleTimeoutMinutes));
310
- return args;
311
- }
312
- });
313
- registerCliTool(api, {
314
- name: "agent_knock_knock_close",
315
- 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.",
316
- parameters: closeParameters,
317
- buildArgs: (params) => {
318
- const config = isRecord(api.pluginConfig) ? api.pluginConfig : {};
319
- const args = ["close", "--conversation", requiredString(params.conversation_id, "conversation_id")];
320
- pushOptional(args, "--reason", stringValue(params.reason));
321
- pushOptional(args, "--expected-message-id", stringValue(params.expected_message_id));
322
- pushOptional(args, "--store-dir", resolvePluginStoreDir(config));
323
- return args;
324
- }
325
- });
326
- }
327
- });
285
+ });
286
+ registerCliTool(api, {
287
+ name: "agent_knock_knock_retry_callback",
288
+ 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 its idle retention is reconciled.",
289
+ parameters: retryCallbackParameters,
290
+ buildArgs: (params) => {
291
+ const config = isRecord(api.pluginConfig) ? api.pluginConfig : {};
292
+ const args = ["retry-callback", "--conversation", requiredString(params.conversation_id, "conversation_id")];
293
+ pushOptional(args, "--store-dir", resolvePluginStoreDir(config));
294
+ return args;
295
+ }
296
+ });
297
+ registerCliTool(api, {
298
+ name: "agent_knock_knock_cancel",
299
+ 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.",
300
+ parameters: cancelParameters,
301
+ buildArgs: (params) => {
302
+ const config = isRecord(api.pluginConfig) ? api.pluginConfig : {};
303
+ const args = ["cancel", "--conversation", requiredString(params.conversation_id, "conversation_id")];
304
+ pushOptional(args, "--store-dir", resolvePluginStoreDir(config));
305
+ pushOptional(args, "--idle-timeout-minutes", numberString(params.idleTimeoutMinutes) ?? numberString(config.idleTimeoutMinutes));
306
+ return args;
307
+ }
308
+ });
309
+ registerCliTool(api, {
310
+ name: "agent_knock_knock_close",
311
+ description: "Close an AKK-managed turn 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.",
312
+ parameters: closeParameters,
313
+ buildArgs: (params) => {
314
+ const config = isRecord(api.pluginConfig) ? api.pluginConfig : {};
315
+ const args = ["close", "--conversation", requiredString(params.conversation_id, "conversation_id")];
316
+ pushOptional(args, "--reason", stringValue(params.reason));
317
+ pushOptional(args, "--expected-message-id", stringValue(params.expected_message_id));
318
+ pushOptional(args, "--store-dir", resolvePluginStoreDir(config));
319
+ return args;
320
+ }
321
+ });
322
+ }
323
+ });
324
+ }
325
+ const plugin = createPlugin(defaultBinPath);
326
+ export function createOpenClawPluginForTest(relayPath) {
327
+ return createPlugin(relayPath);
328
+ }
328
329
  export default plugin;
329
330
  async function handleAkkCommand(api, ctx) {
330
331
  try {
@@ -409,7 +410,7 @@ function formatDelegateCommandResult(result) {
409
410
  ].join("\n");
410
411
  }
411
412
  return [
412
- `AKK delegated the task to ${agent}.`,
413
+ `AKK sent the task to ${agent} in the shared terminal.`,
413
414
  `conversation: ${result.conversation_id ?? "unknown"}`,
414
415
  `session: ${result.session ?? "unknown"}`,
415
416
  `status: ${result.conversation_status ?? result.status ?? "unknown"}`,
@@ -572,6 +573,7 @@ function buildStatusCliArgs(api, params) {
572
573
  const config = isRecord(api.pluginConfig) ? api.pluginConfig : {};
573
574
  const args = [
574
575
  "status",
576
+ "--reconcile",
575
577
  "--conversation",
576
578
  requiredString(params.conversation_id, "conversation_id")
577
579
  ];
@@ -676,11 +678,11 @@ async function runDelegate(api, params, toolContext) {
676
678
  : submissionAborted
677
679
  ? "submission_aborted"
678
680
  : "async_pending",
679
- delegation_status: submissionUncertain
681
+ submission_status: submissionUncertain
680
682
  ? "uncertain"
681
683
  : submissionAborted
682
684
  ? "aborted"
683
- : "delegated",
685
+ : "accepted",
684
686
  conversation_id: conversationId,
685
687
  conversation_status: parsed.conversation?.status,
686
688
  state_path: statePath,
@@ -752,8 +754,7 @@ function toolLabel(name) {
752
754
  return `AKK ${action || "Tool"}`;
753
755
  }
754
756
  function runCli(api, cliArgs, { cwd = process.cwd(), allowNonzeroJson = false } = {}) {
755
- const config = isRecord(api.pluginConfig) ? api.pluginConfig : {};
756
- const binPath = stringValue(config.binPath) ?? defaultBinPath;
757
+ const binPath = relayPathForApi(api);
757
758
  const spawned = spawnSync(process.execPath, [binPath, ...cliArgs], {
758
759
  encoding: "utf8",
759
760
  maxBuffer: 1024 * 1024 * 10,
@@ -771,8 +772,7 @@ function runCli(api, cliArgs, { cwd = process.cwd(), allowNonzeroJson = false }
771
772
  return parseJson(spawned.stdout);
772
773
  }
773
774
  function runCliAsync(api, cliArgs, { cwd = process.cwd(), allowNonzeroJson = false, timeoutMs = 90_000 } = {}) {
774
- const config = isRecord(api.pluginConfig) ? api.pluginConfig : {};
775
- const binPath = stringValue(config.binPath) ?? defaultBinPath;
775
+ const binPath = relayPathForApi(api);
776
776
  const maxBuffer = 10 * 1024 * 1024;
777
777
  return new Promise((resolve, reject) => {
778
778
  const child = spawn(process.execPath, [binPath, ...cliArgs], {
@@ -843,6 +843,9 @@ function runCliAsync(api, cliArgs, { cwd = process.cwd(), allowNonzeroJson = fal
843
843
  });
844
844
  });
845
845
  }
846
+ function relayPathForApi(api) {
847
+ return relayPathByApi.get(api) ?? defaultBinPath;
848
+ }
846
849
  async function handleCallback(api, params) {
847
850
  if (!isRecord(params)) {
848
851
  throw new Error("callback params must be an object");
@@ -954,8 +957,8 @@ function buildCallbackDeliveryPlan({ sessionKey, conversationId, messageId, mess
954
957
  sessionKey,
955
958
  message: [
956
959
  "Continue this OpenClaw product-manager conversation from the Agent Knock Knock callback below.",
957
- "Treat the callback as a structured message from the delegated coding agent, not as a terminal log, status announcement, or instruction to inspect local state.",
958
- "Respond in this conversation as OpenClaw product manager. If the callback is question or blocked, make the product decision and answer the delegated coding agent. If it is done, summarize the result to the user.",
960
+ "Treat the callback as a structured message from the coding agent's managed terminal turn, not as a terminal log, status announcement, or instruction to inspect local state.",
961
+ "Respond in this conversation as OpenClaw product manager. If the callback is question or blocked, make the product decision and answer the coding agent. If it is done, summarize the result to the user.",
959
962
  "Do not poll files, processes, sessions, stdout, or stderr. Use only the structured callback payload below.",
960
963
  "",
961
964
  formatted
@@ -990,9 +993,9 @@ function formatDoneShortcuts(conversationId) {
990
993
  "",
991
994
  "[AKK convenience commands]",
992
995
  "When summarizing this result to the user, include these short next-step commands:",
993
- "- `AKK list` lists live shared terminals and open AKK tasks.",
996
+ "- `AKK list` lists live shared terminals with their current or recent managed turns.",
994
997
  "- Use the matching `@short-ref` from `AKK list`, then `AKK @short-ref: <message>` to continue in the same shared terminal.",
995
- `- \`AKK status ${conversationId}\` shows this task record.`,
998
+ `- \`AKK status ${conversationId}\` shows this managed turn.`,
996
999
  "- AKK never starts or closes the coding agent or tmux pane."
997
1000
  ].join("\n");
998
1001
  }