@scotthuang/agent-knock-knock 0.12.15 → 0.12.16

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 (64) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/README.md +5 -1
  3. package/dist/src/cli-core.js +61 -2
  4. package/dist/src/cli-core.js.map +1 -1
  5. package/dist/src/command-json-callback-transport.d.ts +38 -0
  6. package/dist/src/command-json-callback-transport.js +298 -0
  7. package/dist/src/command-json-callback-transport.js.map +1 -0
  8. package/dist/src/host-adapter.d.ts +52 -0
  9. package/dist/src/host-adapter.js +123 -0
  10. package/dist/src/host-adapter.js.map +1 -0
  11. package/dist/src/host-bridge-mcp.d.ts +22 -0
  12. package/dist/src/host-bridge-mcp.js +145 -0
  13. package/dist/src/host-bridge-mcp.js.map +1 -0
  14. package/dist/src/host-bridge-tools.d.ts +61 -0
  15. package/dist/src/host-bridge-tools.js +130 -0
  16. package/dist/src/host-bridge-tools.js.map +1 -0
  17. package/dist/src/host-bridge.d.ts +28 -0
  18. package/dist/src/host-bridge.js +156 -0
  19. package/dist/src/host-bridge.js.map +1 -0
  20. package/dist/src/host-lifecycle-service.d.ts +39 -0
  21. package/dist/src/host-lifecycle-service.js +101 -0
  22. package/dist/src/host-lifecycle-service.js.map +1 -0
  23. package/dist/src/host-profile-callback-transport.d.ts +14 -0
  24. package/dist/src/host-profile-callback-transport.js +70 -0
  25. package/dist/src/host-profile-callback-transport.js.map +1 -0
  26. package/dist/src/host-profile-cli-adapter.d.ts +8 -0
  27. package/dist/src/host-profile-cli-adapter.js +169 -0
  28. package/dist/src/host-profile-cli-adapter.js.map +1 -0
  29. package/dist/src/host-profile-runtime.d.ts +55 -0
  30. package/dist/src/host-profile-runtime.js +152 -0
  31. package/dist/src/host-profile-runtime.js.map +1 -0
  32. package/dist/src/host-profile.d.ts +121 -0
  33. package/dist/src/host-profile.js +723 -0
  34. package/dist/src/host-profile.js.map +1 -0
  35. package/dist/src/native-thread-resume-snapshot.js +1 -1
  36. package/dist/src/native-thread-resume-snapshot.js.map +1 -1
  37. package/dist/src/openclaw-plugin-command-adapter.d.ts +13 -0
  38. package/dist/src/openclaw-plugin-command-adapter.js +125 -47
  39. package/dist/src/openclaw-plugin-command-adapter.js.map +1 -1
  40. package/dist/src/openclaw-plugin-helpers.js +1 -1
  41. package/dist/src/openclaw-plugin-helpers.js.map +1 -1
  42. package/dist/src/openclaw-plugin-supervisor.d.ts +1 -1
  43. package/dist/src/openclaw-plugin-supervisor.js +44 -72
  44. package/dist/src/openclaw-plugin-supervisor.js.map +1 -1
  45. package/dist/src/terminal-command-cli-adapter.js +4 -4
  46. package/dist/src/terminal-command-cli-adapter.js.map +1 -1
  47. package/dist/src/terminal-dispatch-presenter.js +1 -1
  48. package/dist/src/terminal-dispatch-presenter.js.map +1 -1
  49. package/dist/src/terminal-watch-callback-cli-adapter.d.ts +2 -1
  50. package/dist/src/terminal-watch-callback-cli-adapter.js +4 -3
  51. package/dist/src/terminal-watch-callback-cli-adapter.js.map +1 -1
  52. package/dist/src/terminal-watch-cli-adapter.d.ts +4 -0
  53. package/dist/src/terminal-watch-cli-adapter.js +33 -3
  54. package/dist/src/terminal-watch-cli-adapter.js.map +1 -1
  55. package/dist/src/terminal-watch-service.d.ts +5 -0
  56. package/dist/src/terminal-watch-service.js +6 -1
  57. package/dist/src/terminal-watch-service.js.map +1 -1
  58. package/dist/src/terminal-watch-store.d.ts +2 -0
  59. package/dist/src/terminal-watch-store.js +13 -0
  60. package/dist/src/terminal-watch-store.js.map +1 -1
  61. package/docs/host-bridge-profiles.md +325 -0
  62. package/examples/host-profiles/command-json-starter.json +62 -0
  63. package/package.json +30 -2
  64. package/schemas/host-profile-v1.schema.json +266 -0
@@ -1,4 +1,5 @@
1
1
  import { spawn, spawnSync } from "node:child_process";
2
+ import { AsyncLocalStorage } from "node:async_hooks";
2
3
  import { createHash, randomUUID } from "node:crypto";
3
4
  import { fileURLToPath } from "node:url";
4
5
  import { executorDefinitionForKind } from "./executors.js";
@@ -12,9 +13,37 @@ const OPENCLAW_RECONCILE_BINDING_AUTHORITY_KIND = "reconcile_binding";
12
13
  const CALLBACK_METHOD = AKK_CALLBACK_METHOD;
13
14
  export const defaultOpenClawRelayPath = fileURLToPath(new URL("./cli.js", import.meta.url));
14
15
  const relayPathByApi = new WeakMap();
16
+ const relayEnvironmentByApi = new WeakMap();
17
+ const hostBridgePresentationApis = new WeakSet();
18
+ const hostBridgeAsyncRelayApis = new WeakSet();
19
+ const hostBridgeInvocationStorage = new AsyncLocalStorage();
15
20
  export function bindOpenClawRelayPath(api, relayPath) {
16
21
  relayPathByApi.set(api, relayPath);
17
22
  }
23
+ /** Bind a private child-process environment for a Host adapter instance. */
24
+ export function bindOpenClawRelayEnvironment(api, environment) {
25
+ relayEnvironmentByApi.set(api, Object.freeze({ ...environment }));
26
+ }
27
+ /** Keep one shared tool implementation while selecting Host-neutral output. */
28
+ export function bindHostBridgeToolPresentation(api) {
29
+ hostBridgePresentationApis.add(api);
30
+ }
31
+ /**
32
+ * Keep an embedding Host's event loop responsive while AKK CLI work runs.
33
+ *
34
+ * OpenClaw deliberately retains its established synchronous relay behavior;
35
+ * only public Host adapters opt into the asynchronous child-process runner.
36
+ */
37
+ export function bindHostBridgeAsyncRelay(api) {
38
+ hostBridgeAsyncRelayApis.add(api);
39
+ }
40
+ /** Scope one Host invocation's cancellation without sharing mutable state. */
41
+ export function withHostBridgeInvocationSignal(signal, operation) {
42
+ if (signal?.aborted) {
43
+ return Promise.reject(hostBridgeAbortError());
44
+ }
45
+ return hostBridgeInvocationStorage.run({ signal }, async () => operation());
46
+ }
18
47
  export function registerOpenClawCommands(api, displayedResumeSnapshots) {
19
48
  api.registerCommand?.({
20
49
  name: "akk",
@@ -112,14 +141,14 @@ export function registerOpenClawCommands(api, displayedResumeSnapshots) {
112
141
  description: "Execute one closed, exact-version native status inspection in an exact Codex or Claude Code terminal. Pass only terminal_id and inspection=status; AKK refreshes binding authority privately. Arbitrary slash commands remain unavailable. This creates no AKK Session, Turn, receipt, monitor, or callback.",
113
142
  parameters: nativeInspectParameters,
114
143
  normalizeTurnIdentity: false,
115
- buildArgs: (params) => {
144
+ buildArgs: async (params) => {
116
145
  const config = isRecord(api.pluginConfig) ? api.pluginConfig : {};
117
146
  const inspection = requiredString(params.inspection, "inspection");
118
147
  if (inspection !== "status") {
119
148
  throw new Error("inspection must be status");
120
149
  }
121
150
  const terminalId = requiredString(params.terminal_id, "terminal_id");
122
- const action = privateTerminalActionArguments(api, terminalId, "agent_knock_knock_native_inspect");
151
+ const action = await privateTerminalActionArguments(api, terminalId, "agent_knock_knock_native_inspect");
123
152
  const args = [
124
153
  "native-inspect",
125
154
  "--terminal",
@@ -140,10 +169,10 @@ export function registerOpenClawCommands(api, displayedResumeSnapshots) {
140
169
  parameters: newThreadParameters,
141
170
  normalizeTurnIdentity: false,
142
171
  isErrorResult: (result) => !isAkkThreadTransitionSuccess(result),
143
- buildArgs: (params) => {
172
+ buildArgs: async (params) => {
144
173
  const config = isRecord(api.pluginConfig) ? api.pluginConfig : {};
145
174
  const terminalId = requiredString(params.terminal_id, "terminal_id");
146
- const action = privateTerminalActionArguments(api, terminalId, "agent_knock_knock_new_thread");
175
+ const action = await privateTerminalActionArguments(api, terminalId, "agent_knock_knock_new_thread");
147
176
  const args = [
148
177
  "new-thread",
149
178
  "--terminal",
@@ -161,11 +190,11 @@ export function registerOpenClawCommands(api, displayedResumeSnapshots) {
161
190
  description: "Detach one exact conflicting managed Session binding without adopting the live replacement thread. Pass only the advertised terminal_id and conflicting_session_id after explicit user confirmation; AKK refreshes all revision and binding authority privately. This sends no coding-agent input and creates no Turn.",
162
191
  parameters: reconcileBindingParameters,
163
192
  normalizeTurnIdentity: false,
164
- buildArgs: (params, toolContext) => {
193
+ buildArgs: async (params, toolContext) => {
165
194
  const config = isRecord(api.pluginConfig) ? api.pluginConfig : {};
166
195
  const terminalId = requiredString(params.terminal_id, "terminal_id");
167
196
  const conflictingSessionId = requiredString(params.conflicting_session_id, "conflicting_session_id");
168
- const action = consumeDisplayedPrivateAction(api, {
197
+ const action = await consumeDisplayedPrivateAction(api, {
169
198
  sessionKey: requiredOpenClawSessionKey(toolContext?.sessionKey),
170
199
  sessionId: requiredOpenClawSessionId(toolContext?.sessionId),
171
200
  kind: OPENCLAW_RECONCILE_BINDING_AUTHORITY_KIND,
@@ -204,11 +233,11 @@ export function registerOpenClawCommands(api, displayedResumeSnapshots) {
204
233
  parameters: resumeThreadParameters,
205
234
  normalizeTurnIdentity: false,
206
235
  isErrorResult: (result) => !isAkkThreadTransitionSuccess(result),
207
- buildArgs: (params) => {
236
+ buildArgs: async (params) => {
208
237
  const config = isRecord(api.pluginConfig) ? api.pluginConfig : {};
209
238
  const terminalId = requiredString(params.terminal_id, "terminal_id");
210
239
  const nativeThreadId = requiredString(params.native_thread_id, "native_thread_id");
211
- const discovery = privateThreadDiscovery(api, terminalId);
240
+ const discovery = await privateThreadDiscovery(api, terminalId);
212
241
  const candidate = Array.isArray(discovery.threads)
213
242
  ? discovery.threads.find((thread) => isRecord(thread) &&
214
243
  thread.resumable === true &&
@@ -240,7 +269,7 @@ export function registerOpenClawCommands(api, displayedResumeSnapshots) {
240
269
  parameters: statusParameters,
241
270
  async execute(_toolCallId, params) {
242
271
  try {
243
- const result = runCli(api, buildStatusCliArgs(api, isRecord(params) ? params : {}));
272
+ const result = await runHostAwareCli(api, buildStatusCliArgs(api, isRecord(params) ? params : {}));
244
273
  const rendered = toolResult(result);
245
274
  rememberDisplayedApprovalOffer(api, toolContext?.sessionKey, toolContext?.sessionId, result);
246
275
  return rendered;
@@ -319,7 +348,7 @@ export function registerOpenClawCommands(api, displayedResumeSnapshots) {
319
348
  });
320
349
  registerCliTool(api, {
321
350
  name: "agent_knock_knock_retry_callback",
322
- description: "Retry a persisted AKK callback for an exact turn that failed before reaching OpenClaw. The original callback message id and turn identity are reused for idempotent delivery.",
351
+ description: "Retry a persisted AKK callback for an exact turn that failed before reaching the controller Host. The original callback message id and turn identity are reused for idempotent delivery.",
323
352
  parameters: retryCallbackParameters,
324
353
  buildArgs: (params) => {
325
354
  const config = isRecord(api.pluginConfig) ? api.pluginConfig : {};
@@ -361,14 +390,14 @@ export function registerOpenClawCommands(api, displayedResumeSnapshots) {
361
390
  });
362
391
  }
363
392
  function resumeSelectionScope(sessionKey, sessionId) {
364
- const key = requiredString(sessionKey, "OpenClaw session identity is required for snapshot-bound Resume");
393
+ const key = requiredString(sessionKey, "Controller session identity is required for snapshot-bound Resume");
365
394
  const incarnation = stringValue(sessionId) ?? null;
366
395
  return `openclaw:${createHash("sha256")
367
396
  .update(JSON.stringify([key, incarnation]))
368
397
  .digest("hex")}`;
369
398
  }
370
399
  function resumeSnapshotCacheKey(sessionKey, sessionId, terminalId) {
371
- const key = requiredString(sessionKey, "OpenClaw session identity is required for snapshot-bound Resume");
400
+ const key = requiredString(sessionKey, "Controller session identity is required for snapshot-bound Resume");
372
401
  return JSON.stringify([key, stringValue(sessionId) ?? null, terminalId]);
373
402
  }
374
403
  function rememberDisplayedResumeSnapshot(snapshots, key, snapshotId, expiresAtMs) {
@@ -430,7 +459,7 @@ async function handleAkkCommand(api, ctx, displayedResumeSnapshots) {
430
459
  return await handleAkkLifecycleCommand(api, ctx, parsed, config, displayedResumeSnapshots);
431
460
  }
432
461
  const args = parsed.action === "approve"
433
- ? buildPrivateApprovalArgs(api, { turn_id: parsed.turnId }, {
462
+ ? await buildPrivateApprovalArgs(api, { turn_id: parsed.turnId }, {
434
463
  sessionKey: requiredOpenClawSessionKey(ctx.sessionKey),
435
464
  sessionId: requiredOpenClawSessionId(ctx.sessionId)
436
465
  })
@@ -447,7 +476,7 @@ async function handleAkkCommand(api, ctx, displayedResumeSnapshots) {
447
476
  // check. Keep the Gateway event loop free while that child CLI runs.
448
477
  const result = parsed.action === "doctor"
449
478
  ? await runCliAsync(api, args, { allowNonzeroJson: true })
450
- : runCli(api, args);
479
+ : await runHostAwareCli(api, args);
451
480
  switch (parsed.action) {
452
481
  case "doctor":
453
482
  return {
@@ -485,6 +514,9 @@ async function handleAkkCommand(api, ctx, displayedResumeSnapshots) {
485
514
  }
486
515
  }
487
516
  catch (error) {
517
+ if (error instanceof Error && error.name === "AbortError") {
518
+ throw error;
519
+ }
488
520
  const message = modelFacingErrorMessage(error);
489
521
  return {
490
522
  text: `AKK command failed: ${message}`,
@@ -506,7 +538,7 @@ async function handleAkkLifecycleCommand(api, ctx, parsed, config, displayedResu
506
538
  if (parsed.action === "resume-thread" &&
507
539
  parsed.selection &&
508
540
  ["number", "short-id"].includes(parsed.selection.kind)) {
509
- requiredString(ctx.sessionId, "OpenClaw conversation incarnation is required for number or short-id Resume; run /akk threads again in the current conversation or use the complete UUID");
541
+ requiredString(ctx.sessionId, "Controller conversation incarnation is required for number or short-id Resume; run /akk threads again in the current conversation or use the complete UUID");
510
542
  const mutationArgs = buildAkkCommandCliArgs(parsed, config, {
511
543
  sessionKey: ctx.sessionKey,
512
544
  selectionScope,
@@ -515,7 +547,7 @@ async function handleAkkLifecycleCommand(api, ctx, parsed, config, displayedResu
515
547
  if (!mutationArgs) {
516
548
  throw new Error("could not build snapshot-bound resume command");
517
549
  }
518
- const transitionResult = runCli(api, mutationArgs);
550
+ const transitionResult = await runHostAwareCli(api, mutationArgs);
519
551
  if (isAkkThreadTransitionSuccess(transitionResult)) {
520
552
  displayedResumeSnapshots.delete(snapshotCacheKey);
521
553
  }
@@ -531,7 +563,7 @@ async function handleAkkLifecycleCommand(api, ctx, parsed, config, displayedResu
531
563
  if (!discoveryArgs) {
532
564
  throw new Error("could not build native-thread discovery command");
533
565
  }
534
- const discovery = runCli(api, discoveryArgs);
566
+ const discovery = await runHostAwareCli(api, discoveryArgs);
535
567
  if (parsed.action === "list-resumable-threads" ||
536
568
  (parsed.action === "resume-thread" &&
537
569
  !parsed.selection)) {
@@ -601,7 +633,7 @@ async function handleAkkLifecycleCommand(api, ctx, parsed, config, displayedResu
601
633
  if (!mutationArgs) {
602
634
  throw new Error("could not build native-thread lifecycle command");
603
635
  }
604
- const transitionResult = runCli(api, mutationArgs);
636
+ const transitionResult = await runHostAwareCli(api, mutationArgs);
605
637
  if (isAkkThreadTransitionSuccess(transitionResult)) {
606
638
  displayedResumeSnapshots.delete(snapshotCacheKey);
607
639
  }
@@ -661,7 +693,7 @@ function formatDelegateCommandResult(result) {
661
693
  `session: ${sessionId}`,
662
694
  `turn: ${turnId}`,
663
695
  `status: ${result.conversation_status ?? result.status ?? "unknown"}`,
664
- "The result will return to this OpenClaw session through the callback."
696
+ "The result will return to this controller session through the callback."
665
697
  ].join("\n");
666
698
  }
667
699
  function executorDisplayName(kind) {
@@ -1053,7 +1085,7 @@ async function runSendRequest(api, params, toolContext, messageId) {
1053
1085
  throw new Error("send retry_submission accepts exactly turn_id; do not pass request, terminal_id, session_id, timeout overrides, or callback route data");
1054
1086
  }
1055
1087
  const turnId = authoritativeManagedId(params.turn_id, "turn_id");
1056
- privateActionArguments(api, {
1088
+ await privateActionArguments(api, {
1057
1089
  tool: "agent_knock_knock_send",
1058
1090
  matches: (argumentsValue) => Object.keys(argumentsValue).length === 1 &&
1059
1091
  stringValue(argumentsValue.turn_id) === turnId
@@ -1061,7 +1093,7 @@ async function runSendRequest(api, params, toolContext, messageId) {
1061
1093
  const config = isRecord(api.pluginConfig) ? api.pluginConfig : {};
1062
1094
  const args = ["send", "--turn", turnId];
1063
1095
  pushOptional(args, "--store-dir", resolvePluginStoreDir(config));
1064
- return runCli(api, args);
1096
+ return runHostAwareCli(api, args);
1065
1097
  }
1066
1098
  const requestedType = Object.hasOwn(params, "type")
1067
1099
  ? stringValue(params.type)
@@ -1085,7 +1117,7 @@ async function runSendRequest(api, params, toolContext, messageId) {
1085
1117
  return runDelegate(api, { ...params, messageId }, toolContext);
1086
1118
  }
1087
1119
  const terminalAction = terminalId
1088
- ? privateActionArguments(api, {
1120
+ ? await privateActionArguments(api, {
1089
1121
  tool: "agent_knock_knock_send",
1090
1122
  terminalId,
1091
1123
  matches: (argumentsValue) => stringValue(argumentsValue.selector) === terminalId ||
@@ -1122,7 +1154,7 @@ async function runSendRequest(api, params, toolContext, messageId) {
1122
1154
  pushOptional(args, "--gateway-method", CALLBACK_METHOD);
1123
1155
  pushOptional(args, "--gateway-session", openclawSession);
1124
1156
  pushOptional(args, "--openclaw-bin", stringValue(config.openclawBin));
1125
- return runCli(api, args);
1157
+ return runHostAwareCli(api, args);
1126
1158
  }
1127
1159
  function toolResult(result, { submissionErrors = false, normalizeTurnIdentity = true, forceError = false } = {}) {
1128
1160
  const normalized = normalizeTurnIdentity ? withTurnIdentity(result) : result;
@@ -1139,14 +1171,14 @@ function toolResult(result, { submissionErrors = false, normalizeTurnIdentity =
1139
1171
  ...(submissionError || forceError ? { isError: true } : {})
1140
1172
  };
1141
1173
  }
1142
- function privateList(api) {
1174
+ async function privateList(api) {
1143
1175
  const config = isRecord(api.pluginConfig) ? api.pluginConfig : {};
1144
1176
  const args = ["list", "--reconcile"];
1145
1177
  pushOptional(args, "--store-dir", resolvePluginStoreDir(config));
1146
1178
  pushOptional(args, "--idle-timeout-minutes", numberString(config.idleTimeoutMinutes));
1147
- return runCli(api, args);
1179
+ return runHostAwareCli(api, args);
1148
1180
  }
1149
- function privateThreadDiscovery(api, terminalId) {
1181
+ async function privateThreadDiscovery(api, terminalId) {
1150
1182
  const config = isRecord(api.pluginConfig) ? api.pluginConfig : {};
1151
1183
  const args = [
1152
1184
  "list-resumable-threads",
@@ -1155,17 +1187,17 @@ function privateThreadDiscovery(api, terminalId) {
1155
1187
  ];
1156
1188
  pushOptional(args, "--store-dir", resolvePluginStoreDir(config));
1157
1189
  pushOptional(args, "--codex-home", stringValue(config.codexHome));
1158
- return runCli(api, args);
1190
+ return runHostAwareCli(api, args);
1159
1191
  }
1160
- function privateTerminalActionArguments(api, terminalId, tool) {
1192
+ async function privateTerminalActionArguments(api, terminalId, tool) {
1161
1193
  return privateActionArguments(api, {
1162
1194
  tool,
1163
1195
  terminalId,
1164
1196
  matches: (argumentsValue) => stringValue(argumentsValue.terminal_id) === terminalId
1165
1197
  });
1166
1198
  }
1167
- function privateActionArguments(api, input) {
1168
- const result = privateList(api);
1199
+ async function privateActionArguments(api, input) {
1200
+ const result = await privateList(api);
1169
1201
  const terminals = input.terminalId
1170
1202
  ? terminalRows(result).filter((terminal) => stringValue(terminal.id) === input.terminalId)
1171
1203
  : terminalRows(result);
@@ -1247,7 +1279,7 @@ function uniqueArgumentObjects(candidates) {
1247
1279
  function privateAuthorityOfferKey(sessionKey, sessionId, kind, target) {
1248
1280
  return { sessionKey, sessionId, kind, target };
1249
1281
  }
1250
- function consumeDisplayedPrivateAction(api, input) {
1282
+ async function consumeDisplayedPrivateAction(api, input) {
1251
1283
  const offered = consumeOpenClawPrivateAuthorityOffer(api, {
1252
1284
  sessionKey: input.sessionKey,
1253
1285
  sessionId: input.sessionId,
@@ -1255,9 +1287,9 @@ function consumeDisplayedPrivateAction(api, input) {
1255
1287
  target: input.target
1256
1288
  });
1257
1289
  if (!offered || !isRecord(offered.args)) {
1258
- throw new Error(`${input.tool} requires a current action shown by AKK list in this OpenClaw session; refresh list, review it, and explicitly confirm again`);
1290
+ throw new Error(`${input.tool} requires a current action shown by AKK list in this controller session; refresh list, review it, and explicitly confirm again`);
1259
1291
  }
1260
- const current = privateActionArguments(api, input);
1292
+ const current = await privateActionArguments(api, input);
1261
1293
  if (JSON.stringify(current) !== JSON.stringify(offered.args)) {
1262
1294
  throw new Error(`${input.tool} authority changed after it was shown; refresh AKK list, review the current action, and explicitly confirm again`);
1263
1295
  }
@@ -1303,7 +1335,7 @@ function currentApprovalFingerprint(result) {
1303
1335
  }
1304
1336
  return unique[0];
1305
1337
  }
1306
- function buildPrivateApprovalArgs(api, params, { sessionKey, sessionId }) {
1338
+ async function buildPrivateApprovalArgs(api, params, { sessionKey, sessionId }) {
1307
1339
  const config = isRecord(api.pluginConfig) ? api.pluginConfig : {};
1308
1340
  const turnId = stringValue(params.turn_id);
1309
1341
  const terminalId = stringValue(params.terminal_id);
@@ -1319,10 +1351,10 @@ function buildPrivateApprovalArgs(api, params, { sessionKey, sessionId }) {
1319
1351
  ? stringValue(offered.args.expected_approval_fingerprint)
1320
1352
  : undefined);
1321
1353
  if (!isExactApprovalFingerprint(offeredFingerprint)) {
1322
- throw new Error("approve requires a current approval request shown by agent_knock_knock_status in this OpenClaw conversation; refresh status, ask the user to review it, and explicitly confirm again");
1354
+ throw new Error("approve requires a current approval request shown by agent_knock_knock_status in this controller conversation; refresh status, ask the user to review it, and explicitly confirm again");
1323
1355
  }
1324
1356
  const action = terminalId
1325
- ? privateActionArguments(api, {
1357
+ ? await privateActionArguments(api, {
1326
1358
  tool: "agent_knock_knock_approve",
1327
1359
  terminalId,
1328
1360
  matches: (argumentsValue) => stringValue(argumentsValue.terminal_id ?? argumentsValue.conversation_id) === terminalId
@@ -1343,7 +1375,7 @@ function buildPrivateApprovalArgs(api, params, { sessionKey, sessionId }) {
1343
1375
  else {
1344
1376
  args.push("--turn", requiredString(turnId, "turn_id"));
1345
1377
  }
1346
- const currentFingerprint = currentApprovalFingerprint(runCli(api, statusArgs));
1378
+ const currentFingerprint = currentApprovalFingerprint(await runHostAwareCli(api, statusArgs));
1347
1379
  if (currentFingerprint !== offeredFingerprint) {
1348
1380
  throw new Error("the approval request changed after it was shown; refresh AKK status, ask the user to review the current request, and explicitly confirm again");
1349
1381
  }
@@ -1442,6 +1474,9 @@ function isLegacyAuthorityInstructionPath(path) {
1442
1474
  return callbackMessageBody || normalized.includes("recentevents");
1443
1475
  }
1444
1476
  function modelFacingToolError(error) {
1477
+ if (error instanceof Error && error.name === "AbortError") {
1478
+ return error;
1479
+ }
1445
1480
  return new Error(modelFacingErrorMessage(error));
1446
1481
  }
1447
1482
  function modelFacingErrorMessage(error) {
@@ -1644,7 +1679,9 @@ async function runDelegate(api, params, toolContext) {
1644
1679
  replayed: parsed.replayed === true,
1645
1680
  background: parsed.background === true,
1646
1681
  pid: parsed.pid ?? parsedTerminalControl?.panePid ?? null,
1647
- callback_method: CALLBACK_METHOD,
1682
+ callback_method: hostBridgePresentationApis.has(api)
1683
+ ? "command_json_v1"
1684
+ : CALLBACK_METHOD,
1648
1685
  ...(submissionUnfenced
1649
1686
  ? {
1650
1687
  submission_outcome: "submitted",
@@ -1698,11 +1735,11 @@ async function runDelegate(api, params, toolContext) {
1698
1735
  : {
1699
1736
  openclaw_next_action: {
1700
1737
  action: "yield",
1701
- reason: "The coding agent is working in the shared terminal. End this OpenClaw turn now and wait for an Agent Knock Knock callback.",
1738
+ reason: "The coding agent is working in the shared terminal. End this controller turn now and wait for an Agent Knock Knock callback.",
1702
1739
  do_not: "Do not poll terminal internals while waiting. Further communication must use Agent Knock Knock tools so the same shared terminal remains authoritative.",
1703
- expected_callback: "The callback will be injected and scheduled into this OpenClaw session by the agent-knock-knock.callback Gateway method."
1740
+ expected_callback: "The callback will be injected into this controller session by its configured callback transport."
1704
1741
  },
1705
- note: "The task was sent to the shared terminal. OpenClaw should yield now and wait for the scheduled callback turn."
1742
+ note: "The task was sent to the shared terminal. The controller Host should yield now and wait for the callback turn."
1706
1743
  })
1707
1744
  };
1708
1745
  }
@@ -1729,7 +1766,7 @@ function registerCliTool(api, { name, description, parameters, buildArgs, normal
1729
1766
  parameters,
1730
1767
  async execute(toolCallId, params) {
1731
1768
  try {
1732
- const result = runCli(api, buildArgs(isRecord(params) ? params : {}, toolContext, toolCallId));
1769
+ const result = await runHostAwareCli(api, await buildArgs(isRecord(params) ? params : {}, toolContext, toolCallId));
1733
1770
  const rendered = toolResult(result, {
1734
1771
  submissionErrors: name === "agent_knock_knock_respond",
1735
1772
  normalizeTurnIdentity,
@@ -1760,7 +1797,8 @@ export function runCli(api, cliArgs, { cwd = process.cwd(), allowNonzeroJson = f
1760
1797
  const spawned = spawnSync(process.execPath, [binPath, ...cliArgs], {
1761
1798
  encoding: "utf8",
1762
1799
  maxBuffer: 1024 * 1024 * 10,
1763
- cwd
1800
+ cwd,
1801
+ env: relayEnvironmentForApi(api)
1764
1802
  });
1765
1803
  if (spawned.error) {
1766
1804
  throw new Error(`agent-knock-knock ${cliArgs[0]} failed to start: ${spawned.error.message}`);
@@ -1776,9 +1814,14 @@ export function runCli(api, cliArgs, { cwd = process.cwd(), allowNonzeroJson = f
1776
1814
  export function runCliAsync(api, cliArgs, { cwd = process.cwd(), allowNonzeroJson = false, timeoutMs = 90_000 } = {}) {
1777
1815
  const binPath = relayPathForApi(api);
1778
1816
  const maxBuffer = 10 * 1024 * 1024;
1817
+ const signal = hostBridgeInvocationStorage.getStore()?.signal;
1818
+ if (signal?.aborted) {
1819
+ return Promise.reject(hostBridgeAbortError());
1820
+ }
1779
1821
  return new Promise((resolve, reject) => {
1780
1822
  const child = spawn(process.execPath, [binPath, ...cliArgs], {
1781
1823
  cwd,
1824
+ env: relayEnvironmentForApi(api),
1782
1825
  stdio: ["ignore", "pipe", "pipe"],
1783
1826
  windowsHide: true
1784
1827
  });
@@ -1788,6 +1831,15 @@ export function runCliAsync(api, cliArgs, { cwd = process.cwd(), allowNonzeroJso
1788
1831
  let stderr = "";
1789
1832
  let overflow = false;
1790
1833
  let timedOut = false;
1834
+ let aborted = false;
1835
+ const onAbort = () => {
1836
+ aborted = true;
1837
+ child.kill("SIGKILL");
1838
+ };
1839
+ const cleanup = () => {
1840
+ clearTimeout(timeout);
1841
+ signal?.removeEventListener("abort", onAbort);
1842
+ };
1791
1843
  const append = (current, chunk) => {
1792
1844
  const next = current + chunk;
1793
1845
  if (Buffer.byteLength(next, "utf8") > maxBuffer) {
@@ -1807,12 +1859,24 @@ export function runCliAsync(api, cliArgs, { cwd = process.cwd(), allowNonzeroJso
1807
1859
  child.kill("SIGKILL");
1808
1860
  }, timeoutMs);
1809
1861
  timeout.unref();
1862
+ signal?.addEventListener("abort", onAbort, { once: true });
1863
+ if (signal?.aborted) {
1864
+ onAbort();
1865
+ }
1810
1866
  child.once("error", (error) => {
1811
- clearTimeout(timeout);
1867
+ cleanup();
1868
+ if (aborted) {
1869
+ reject(hostBridgeAbortError());
1870
+ return;
1871
+ }
1812
1872
  reject(new Error(`agent-knock-knock ${cliArgs[0]} failed to start: ${error.message}`));
1813
1873
  });
1814
1874
  child.once("close", (status) => {
1815
- clearTimeout(timeout);
1875
+ cleanup();
1876
+ if (aborted) {
1877
+ reject(hostBridgeAbortError());
1878
+ return;
1879
+ }
1816
1880
  if (timedOut) {
1817
1881
  reject(new Error(`agent-knock-knock ${cliArgs[0]} timed out`));
1818
1882
  return;
@@ -1845,9 +1909,23 @@ export function runCliAsync(api, cliArgs, { cwd = process.cwd(), allowNonzeroJso
1845
1909
  });
1846
1910
  });
1847
1911
  }
1912
+ function hostBridgeAbortError() {
1913
+ const error = new Error("agent-knock-knock Host invocation was aborted");
1914
+ error.name = "AbortError";
1915
+ return error;
1916
+ }
1917
+ async function runHostAwareCli(api, cliArgs, options = {}) {
1918
+ if (hostBridgeAsyncRelayApis.has(api)) {
1919
+ return runCliAsync(api, cliArgs, options);
1920
+ }
1921
+ return runCli(api, cliArgs, options);
1922
+ }
1848
1923
  function relayPathForApi(api) {
1849
1924
  return relayPathByApi.get(api) ?? defaultOpenClawRelayPath;
1850
1925
  }
1926
+ function relayEnvironmentForApi(api) {
1927
+ return relayEnvironmentByApi.get(api) ?? process.env;
1928
+ }
1851
1929
  function pushTurnTarget(args, params) {
1852
1930
  if (Object.hasOwn(params, "turn_id") && Object.hasOwn(params, "conversation_id")) {
1853
1931
  throw new Error("turn-target tools accept only one of turn_id or conversation_id");
@@ -1903,10 +1981,10 @@ function requiredString(value, name) {
1903
1981
  return value;
1904
1982
  }
1905
1983
  function requiredOpenClawSessionKey(value) {
1906
- return requiredString(value, "OpenClaw session identity for this confirmed action");
1984
+ return requiredString(value, "Controller session identity for this confirmed action");
1907
1985
  }
1908
1986
  function requiredOpenClawSessionId(value) {
1909
- return requiredString(value, "OpenClaw conversation incarnation for this confirmed action");
1987
+ return requiredString(value, "Controller conversation incarnation for this confirmed action");
1910
1988
  }
1911
1989
  function parseJson(text) {
1912
1990
  try {