@scotthuang/agent-knock-knock 0.13.2 → 0.13.3

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 (54) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +17 -4
  3. package/dist/src/claude-terminal-agent-adapter.js +4 -0
  4. package/dist/src/claude-terminal-agent-adapter.js.map +1 -1
  5. package/dist/src/cli-core.js +9 -1
  6. package/dist/src/cli-core.js.map +1 -1
  7. package/dist/src/codex-terminal-agent-adapter.js +4 -0
  8. package/dist/src/codex-terminal-agent-adapter.js.map +1 -1
  9. package/dist/src/herdr-terminal-control-provider.js +6 -1
  10. package/dist/src/herdr-terminal-control-provider.js.map +1 -1
  11. package/dist/src/host-adapter.d.ts +1 -1
  12. package/dist/src/host-adapter.js +1 -1
  13. package/dist/src/host-bridge-tools.js +2 -2
  14. package/dist/src/managed-session.js +2 -18
  15. package/dist/src/managed-session.js.map +1 -1
  16. package/dist/src/native-thread-lifecycle-cli-adapter.d.ts +13 -0
  17. package/dist/src/native-thread-lifecycle-cli-adapter.js +578 -2
  18. package/dist/src/native-thread-lifecycle-cli-adapter.js.map +1 -1
  19. package/dist/src/openclaw-plugin-command-adapter.js +325 -35
  20. package/dist/src/openclaw-plugin-command-adapter.js.map +1 -1
  21. package/dist/src/openclaw-plugin-helpers.d.ts +27 -0
  22. package/dist/src/openclaw-plugin-helpers.js +682 -2
  23. package/dist/src/openclaw-plugin-helpers.js.map +1 -1
  24. package/dist/src/openclaw-plugin-schemas.d.ts +53 -0
  25. package/dist/src/openclaw-plugin-schemas.js +53 -0
  26. package/dist/src/openclaw-plugin-schemas.js.map +1 -1
  27. package/dist/src/terminal-action-projection.d.ts +83 -1
  28. package/dist/src/terminal-action-projection.js +174 -0
  29. package/dist/src/terminal-action-projection.js.map +1 -1
  30. package/dist/src/terminal-agent-adapter.d.ts +7 -0
  31. package/dist/src/terminal-agent-adapter.js +8 -0
  32. package/dist/src/terminal-agent-adapter.js.map +1 -1
  33. package/dist/src/terminal-agent-bridge.d.ts +29 -0
  34. package/dist/src/terminal-agent-bridge.js +580 -14
  35. package/dist/src/terminal-agent-bridge.js.map +1 -1
  36. package/dist/src/terminal-control-ref.d.ts +23 -0
  37. package/dist/src/terminal-control-ref.js +32 -0
  38. package/dist/src/terminal-control-ref.js.map +1 -1
  39. package/dist/src/terminal-list-cli-adapter.js +324 -16
  40. package/dist/src/terminal-list-cli-adapter.js.map +1 -1
  41. package/dist/src/terminal-list-renderer.d.ts +1 -0
  42. package/dist/src/terminal-list-renderer.js +87 -3
  43. package/dist/src/terminal-list-renderer.js.map +1 -1
  44. package/dist/src/terminal-model-control.d.ts +312 -0
  45. package/dist/src/terminal-model-control.js +1595 -0
  46. package/dist/src/terminal-model-control.js.map +1 -0
  47. package/dist/src/terminal-runtime-cli-adapter.d.ts +2 -0
  48. package/dist/src/terminal-runtime-cli-adapter.js +59 -17
  49. package/dist/src/terminal-runtime-cli-adapter.js.map +1 -1
  50. package/docs/quickstart-herdr.md +1 -1
  51. package/docs/quickstart-tmux.md +27 -2
  52. package/openclaw.plugin.json +13 -1
  53. package/package.json +1 -1
  54. package/templates/openclaw-skills/agent-knock-knock/SKILL.md +54 -11
@@ -5,14 +5,23 @@ import { fileURLToPath } from "node:url";
5
5
  import { executorDefinitionForKind } from "./executors.js";
6
6
  import { isTerminalApprovalDecision } from "./terminal-agent-adapter.js";
7
7
  import { isRecord, nonBlankString as stringValue } from "./value-guards.js";
8
- import { AKK_CALLBACK_METHOD, akkUsageText, buildAkkCommandCliArgs, formatAkkListCommandResult, formatAkkRespondCommandResult, formatAkkTerminalWatchHint, formatAkkThreadsCommandResult, formatAkkThreadTransitionCommandResult, formatAkkUnwatchCommandResult, formatAkkWatchCommandResult, formatAkkWatchStatusCommandResult, isAkkModelFacingDiagnosticField, isAkkModelFacingPrivateAuthorityField, isAkkNativeSubmissionAccepted, isAkkThreadTransitionSuccess, normalizeAkkModelFacingFieldName, parseAkkCommand, resolvePluginStoreDir, sanitizeAkkModelFacingDiagnosticText, sanitizeAkkModelFacingLegacyAuthorityInstructionText } from "./openclaw-plugin-helpers.js";
9
- import { approveParameters, cancelParameters, closeParameters, identifyAndSendParameters, identifyForegroundParameters, listParameters, listResumableThreadsParameters, nativeInspectParameters, newThreadParameters, reconcileBindingParameters, renewParameters, respondInteractionParameters, respondParameters, resumeThreadParameters, retryCallbackParameters, sendParameters, statusParameters, unwatchParameters, watchParameters } from "./openclaw-plugin-schemas.js";
8
+ import { AKK_CALLBACK_METHOD, akkUsageText, buildAkkCommandCliArgs, compactAkkListModelProjection, formatAkkListCommandResult, formatAkkModelOptionsCommandResult, formatAkkRepairModelControlCommandResult, formatAkkRespondCommandResult, formatAkkSetModelCommandResult, formatAkkTerminalWatchHint, formatAkkThreadsCommandResult, formatAkkThreadTransitionCommandResult, formatAkkUnwatchCommandResult, formatAkkWatchCommandResult, formatAkkWatchStatusCommandResult, isAkkModelFacingDiagnosticField, isAkkModelFacingPrivateAuthorityField, isAkkNativeSubmissionAccepted, isAkkRepairModelControlSuccess, isAkkSetModelSuccess, isAkkThreadTransitionSuccess, normalizeAkkModelFacingFieldName, parseAkkCommand, resolvePluginStoreDir, sanitizeAkkModelFacingDiagnosticText, sanitizeAkkModelFacingLegacyAuthorityInstructionText } from "./openclaw-plugin-helpers.js";
9
+ import { approveParameters, cancelParameters, closeParameters, identifyAndSendParameters, identifyForegroundParameters, listParameters, listResumableThreadsParameters, modelOptionsParameters, nativeInspectParameters, newThreadParameters, reconcileBindingParameters, repairModelControlParameters, renewParameters, respondInteractionParameters, respondParameters, resumeThreadParameters, retryCallbackParameters, sendParameters, setModelParameters, statusParameters, unwatchParameters, watchParameters } from "./openclaw-plugin-schemas.js";
10
10
  import { consumeOpenClawPrivateAuthorityOffer, invalidateOpenClawInteractionAuthorityOffersForSubject, openClawApprovalAuthorityOfferKey, openClawInteractionAuthorityOfferKey, rememberOpenClawPrivateAuthorityOffer } from "./openclaw-private-authority-offers.js";
11
11
  import { TERMINAL_INTERACTION_LIMITS, TERMINAL_INTERACTION_SUBJECT_VERSION, terminalInteractionSubjectId, validateAnyTerminalInteractionProjection, validateAnyTerminalInteractionResponse } from "./terminal-interaction-protocol.js";
12
12
  import { normalizedTerminalSendResultContract, terminalSendEnterDispatched } from "./terminal-dispatch-presenter.js";
13
13
  const MAX_DISPLAYED_RESUME_SNAPSHOTS = 512;
14
14
  const OPENCLAW_HANDOFF_AUTHORITY_KIND = "handoff";
15
15
  const OPENCLAW_RECONCILE_BINDING_AUTHORITY_KIND = "reconcile_binding";
16
+ const OPENCLAW_MODEL_OPTIONS_AUTHORITY_KIND = "model_options";
17
+ // Model discovery intentionally revalidates every native menu transition.
18
+ // Claude's live effort-ring walk can exceed the generic 90-second relay
19
+ // budget, while set-model performs discovery both before and after its one
20
+ // commit. Keep these operations bounded without killing the child midway
21
+ // through a verified native picker. The Host abort signal still cancels them.
22
+ const OPENCLAW_MODEL_OPTIONS_CLI_TIMEOUT_MS = 15 * 60_000;
23
+ const OPENCLAW_REPAIR_MODEL_CONTROL_CLI_TIMEOUT_MS = 2 * 60_000;
24
+ const OPENCLAW_SET_MODEL_CLI_TIMEOUT_MS = 30 * 60_000;
16
25
  const CALLBACK_METHOD = AKK_CALLBACK_METHOD;
17
26
  export const defaultOpenClawRelayPath = fileURLToPath(new URL("./cli.js", import.meta.url));
18
27
  const relayPathByApi = new WeakMap();
@@ -52,40 +61,19 @@ export function withHostBridgeInvocationSignal(signal, operation) {
52
61
  export function registerOpenClawCommands(api, displayedResumeSnapshots) {
53
62
  api.registerCommand?.({
54
63
  name: "akk",
55
- description: "Send coding work through existing Codex or Claude Code shared terminals, inspect managed Turns, observe a user-selected terminal with durable read-only Terminal Watch, and explicitly start, clear, list, or resume native threads.",
64
+ description: "Send coding work through existing Codex or Claude Code shared terminals, inspect managed Turns, observe a user-selected terminal with durable read-only Terminal Watch, manage native threads, and safely inspect or change an idle pane's native model selection.",
56
65
  acceptsArgs: true,
57
66
  requireAuth: true,
58
67
  nativeProgressMessages: {
59
68
  default: "AKK is handling the request..."
60
69
  },
61
70
  agentPromptGuidance: [
62
- "Use /akk <task> when exactly one send-ready coding-agent terminal pane should receive new work. Send-ready means an exact live process and terminal plus a scanned, non-blocked approval state. Parsed working activity and ordinary Codex main-Composer visibility, stability, or exactness do not veto this user-priority path. A proven input-owning native approval, questionnaire/editor, or read-only viewer remains a zero-input boundary; Codex 0.154's exact collapsed async-question summary remains sendable, while its expanded, clipped, or ambiguous editor does not. Codex terminal_user_explicit physical fallback sends C-u once to replace the current Composer, injects the request, waits through the paste window, and dispatches Enter exactly once; after text injection, no Composer observation may veto Enter. Claude Code user-explicit Send remains exact-empty-only. Managed Send may still require exact empty before input, while native inspection and native lifecycle input remain exact-empty-only. Broken or stale AKK management activity records do not veto the user's physical Send. Structured tools use only semantic identifiers returned by AKK: session_id for an exact managed context, terminal_id for the currently verified pane, turn_id for one managed Turn, watch_id for one Terminal Watch, and native_thread_id for one resumable native thread. Draft text, composer digests, and opaque freshness authority stay private; AKK revalidates them under its locks. Once the Codex mutation sequence begins, an uncertain result must not be automatically retried. /akk watch is read-only and follows user intent: it prefers an exact task anchor, but version, artifact, managed ownership, and action-advertisement uncertainty degrade to a warning-bearing terminal-activity Watch instead of vetoing the request. New/clear/resume, approval, reconciliation, handoff, and recovery still require the documented user intent or explicit confirmation. AKK never starts a coding-agent process."
71
+ "Use /akk <task> when exactly one send-ready coding-agent terminal pane should receive new work. Send-ready means an exact live process and terminal plus a scanned, non-blocked approval state. Parsed working activity and ordinary Codex main-Composer visibility, stability, or exactness do not veto this user-priority path. A proven input-owning native approval, questionnaire/editor, or read-only viewer remains a zero-input boundary; Codex 0.154's exact collapsed async-question summary remains sendable, while its expanded, clipped, or ambiguous editor does not. Codex terminal_user_explicit physical fallback sends C-u once to replace the current Composer, injects the request, waits through the paste window, and dispatches Enter exactly once; after text injection, no Composer observation may veto Enter. Claude Code user-explicit Send remains exact-empty-only. Managed Send may still require exact empty before input, while native inspection and native lifecycle input remain exact-empty-only. Broken or stale AKK management activity records do not veto the user's physical Send. Structured tools use only semantic identifiers returned by AKK: session_id for an exact managed context, terminal_id for the currently verified pane, turn_id for one managed Turn, watch_id for one Terminal Watch, and native_thread_id for one resumable native thread. Draft text, composer digests, and opaque freshness authority stay private; AKK revalidates them under its locks. Once the Codex mutation sequence begins, an uncertain result must not be automatically retried. /akk watch is read-only and follows user intent: it prefers an exact task anchor, but version, artifact, managed ownership, and action-advertisement uncertainty degrade to a warning-bearing terminal-activity Watch instead of vetoing the request. New/clear/resume, approval, reconciliation, handoff, and recovery still require the documented user intent or explicit confirmation. AKK never starts a coding-agent process.",
72
+ "Use /akk models on one exact currently advertised physical pane before /akk set-model. Codex 0.154 may use either one exact current native Session or a verified-zero-rollout pane; identify_foreground is diagnostic and is never a prerequisite for that zero-rollout path. Claude Code still requires one exact current native Session. Both steps require no active Turn and no approval, questionnaire/editor, or read-only viewer. model_options normally requires an empty Composer; when List binds it to one exact stable Codex /model residual, it may continue only that residual into read-only catalog discovery. repair_model_control remains the separate clear-only alternative and never presses Enter or selects anything. Only ids and reasoning efforts from that current native catalog are valid. Codex changes the current session and persists the selected model for future sessions; ordinary efforts, including max, are also persisted, while ultra remains current-session-only and Codex chooses a non-ultra future fallback. Claude Code changes only the current session. Read effective and new_session_defaults separately. Model control never accepts slash text, raw keys, menu indexes, display labels, scope overrides, or private authority; an uncertain outcome must not be retried automatically."
63
73
  ],
64
74
  handler: async (ctx) => handleAkkCommand(api, ctx, displayedResumeSnapshots)
65
75
  });
66
- registerCliTool(api, {
67
- name: "agent_knock_knock_list",
68
- description: "List existing Codex and Claude Code tmux or Herdr panes as the primary terminals[] resources, plus durable terminal_watches[] records. Use only advertised mutation actions and their semantic IDs; read-only Watch may also use an exact user-selected terminal_id when its convenience action is absent. A terminal_user_explicit send is user-priority authority for one exact live physical terminal/process with a scanned, non-blocked approval state; parsed working activity and ordinary Codex main-Composer visibility, stability, or exactness do not veto it. A proven input-owning native approval, questionnaire/editor, or read-only viewer remains a zero-input boundary; Codex 0.154's exact collapsed async-question summary remains sendable, while its expanded, clipped, or ambiguous editor does not. Codex uses replace_current_composer_and_submit: physical fallback sends C-u once, injects the request, waits through the paste window, and dispatches Enter exactly once without a post-text Composer veto. Claude Code remains exact-empty-only; managed Send may require exact empty before input, and native inspection and native lifecycle input remain exact-empty-only. Broken AKK Turn, Session, transfer, ledger, or Store state does not veto a new user Send. AKK tries the managed fast path first, then delivers once as unmanaged work and best-effort attaches an exact Terminal Watch callback before releasing stale management. Watch attachment failure never changes a successful Send and is reported as a warning. Existing or possibly existing same-message idempotency evidence rejects replay. If fresh durability is unavailable, AKK proceeds with a warning and the result must not be automatically retried. Once the Codex mutation sequence begins, an uncertain result must not be automatically retried. A session_exact send uses session_id; terminal_follow_current and terminal_user_explicit Send use terminal_id; Watch uses terminal_id; managed controls use turn_id; lifecycle resume additionally uses the complete native_thread_id. Draft text, composer digests, and opaque freshness authority stay private. AKK never starts a coding-agent process.",
69
- parameters: listParameters,
70
- buildArgs: (params) => {
71
- const config = isRecord(api.pluginConfig) ? api.pluginConfig : {};
72
- const args = ["list", "--reconcile"];
73
- pushOptional(args, "--store-dir", resolvePluginStoreDir(config));
74
- pushOptional(args, "--idle-timeout-minutes", numberString(params.idleTimeoutMinutes) ?? numberString(api.pluginConfig?.idleTimeoutMinutes));
75
- pushOptional(args, "--agent", stringValue(params.agent));
76
- pushOptional(args, "--status", stringValue(params.status));
77
- if (params.all === true) {
78
- args.push("--all");
79
- }
80
- if (params.noApprovalScan === true) {
81
- args.push("--no-approval-scan");
82
- }
83
- if (params.terminalDebug === true) {
84
- args.push("--terminal-debug");
85
- }
86
- return args;
87
- }
88
- });
76
+ registerOpenClawListTool(api);
89
77
  registerCliTool(api, {
90
78
  name: "agent_knock_knock_watch",
91
79
  description: "Start one durable read-only Terminal Watch for the user's exact selected Codex or Claude Code terminal. AKK prefers an exact durable task anchor; if version, artifact, task, managed-ownership, or action-advertisement evidence is unavailable, it remains callable and returns warnings while using best-effort terminal activity. That fallback reports stable-idle activity, not proof of exact task completion. Watch creates no AKK Session or Turn, sends no terminal input, and never adopts or blocks the terminal task.",
@@ -168,6 +156,7 @@ export function registerOpenClawCommands(api, displayedResumeSnapshots) {
168
156
  return args;
169
157
  }
170
158
  });
159
+ registerModelControlTools(api);
171
160
  registerForegroundIdentificationTools(api);
172
161
  registerCliTool(api, {
173
162
  name: "agent_knock_knock_new_thread",
@@ -409,6 +398,93 @@ export function registerOpenClawCommands(api, displayedResumeSnapshots) {
409
398
  }
410
399
  });
411
400
  }
401
+ function registerOpenClawListTool(api) {
402
+ registerCliTool(api, {
403
+ name: "agent_knock_knock_list",
404
+ description: "List live AKK terminal resources and Terminal Watches. The model-facing result is a compact projection: available_actions contains current semantic action names and action_inputs contains only their dynamic semantic inputs. Follow the installed agent-knock-knock skill for action meaning, target rules, safety boundaries, and recovery behavior; AKK privately revalidates every mutation.",
405
+ parameters: listParameters,
406
+ modelProjection: compactAkkListModelProjection,
407
+ compactText: true,
408
+ buildArgs: (params) => {
409
+ const config = isRecord(api.pluginConfig) ? api.pluginConfig : {};
410
+ const args = ["list", "--reconcile"];
411
+ pushOptional(args, "--store-dir", resolvePluginStoreDir(config));
412
+ pushOptional(args, "--idle-timeout-minutes", numberString(params.idleTimeoutMinutes) ??
413
+ numberString(api.pluginConfig?.idleTimeoutMinutes));
414
+ pushOptional(args, "--agent", stringValue(params.agent));
415
+ pushOptional(args, "--status", stringValue(params.status));
416
+ if (params.all === true)
417
+ args.push("--all");
418
+ if (params.noApprovalScan === true)
419
+ args.push("--no-approval-scan");
420
+ if (params.terminalDebug === true)
421
+ args.push("--terminal-debug");
422
+ return args;
423
+ }
424
+ });
425
+ }
426
+ function registerModelControlTools(api) {
427
+ registerCliTool(api, {
428
+ name: "agent_knock_knock_model_options",
429
+ description: "Inspect the exact current native model catalog for one explicitly selected physical Codex or Claude Code pane. This is the required read-only first step before set_model. Codex 0.154 accepts one exact current native Session or a verified-zero-rollout pane without identify_foreground; Claude Code still requires one exact current native Session. AKK requires an exact live pane/process, no active Turn, and no approval, questionnaire/editor, or read-only viewer. The Composer must be empty unless current List privately binds this action to one exact stable Codex 0.154 /model residual, which AKK may continue into the native picker without retyping it. That residual-bound authority is consumed by discovery; after restoring an exact empty Composer, AKK retains only fresh ordinary terminal/catalog authority for one set_model attempt in this exact controller conversation. It obtains model ids and reasoning-effort values from the native UI/runtime and exposes only semantic choices. Codex advertises scope=current_and_new_sessions; Claude Code advertises scope=current_session. Arbitrary commands, keys, menu indexes, labels, and hidden authority are never accepted.",
430
+ parameters: modelOptionsParameters,
431
+ timeoutMs: OPENCLAW_MODEL_OPTIONS_CLI_TIMEOUT_MS,
432
+ normalizeTurnIdentity: false,
433
+ buildArgs: async (params) => {
434
+ assertOnlyModelControlParameters(params, ["terminal_id"], "model_options");
435
+ const config = isRecord(api.pluginConfig) ? api.pluginConfig : {};
436
+ const terminalId = requiredString(params.terminal_id, "terminal_id");
437
+ const action = await privateTerminalActionArguments(api, terminalId, "agent_knock_knock_model_options");
438
+ const args = [
439
+ "model-options",
440
+ "--terminal",
441
+ terminalId,
442
+ "--expected-binding-token",
443
+ requiredString(action.expected_binding_token, "current internal model-options binding authority")
444
+ ];
445
+ pushOptional(args, "--store-dir", resolvePluginStoreDir(config));
446
+ pushOptional(args, "--codex-home", stringValue(config.codexHome));
447
+ return args;
448
+ },
449
+ rememberResult: (result, params, toolContext) => rememberDisplayedModelOptionsOffer(api, toolContext?.sessionKey, toolContext?.sessionId, params.terminal_id, result)
450
+ });
451
+ registerCliTool(api, {
452
+ name: "agent_knock_knock_repair_model_control",
453
+ description: "Clear one exact stale Codex 0.154 /model completion surface, exact bare /model Composer, or exact open native model picker left by a failed native model-control attempt. This explicit one-shot repair is available only when the current AKK list proves the same exact pane/process, the closed profiled model-control residue, no active Turn, and no approval, questionnaire/editor, or read-only viewer. Pass only terminal_id; AKK privately derives and revalidates every physical, screen, and Composer fence before each reversible cleanup input. It never submits a task, selects a model, approves a prompt, accepts raw commands or keys, or automatically continues into model_options/set_model. An open picker receives dismissal authority only, never Enter authority. outcome=uncertain must never be retried automatically.",
454
+ parameters: repairModelControlParameters,
455
+ timeoutMs: OPENCLAW_REPAIR_MODEL_CONTROL_CLI_TIMEOUT_MS,
456
+ normalizeTurnIdentity: false,
457
+ isErrorResult: (result) => !isAkkRepairModelControlSuccess(result),
458
+ buildArgs: async (params) => {
459
+ assertOnlyModelControlParameters(params, ["terminal_id"], "repair_model_control");
460
+ const config = isRecord(api.pluginConfig) ? api.pluginConfig : {};
461
+ const terminalId = requiredString(params.terminal_id, "terminal_id");
462
+ const action = await privateTerminalActionArguments(api, terminalId, "agent_knock_knock_repair_model_control");
463
+ const args = [
464
+ "repair-model-control",
465
+ "--terminal",
466
+ terminalId,
467
+ "--expected-binding-token",
468
+ requiredString(action.expected_binding_token, "current internal model-control repair authority")
469
+ ];
470
+ pushOptional(args, "--store-dir", resolvePluginStoreDir(config));
471
+ pushOptional(args, "--codex-home", stringValue(config.codexHome));
472
+ return args;
473
+ }
474
+ });
475
+ registerCliTool(api, {
476
+ name: "agent_knock_knock_set_model",
477
+ description: "Change exactly one already-open physical coding-agent pane to one semantic model and reasoning-effort tuple from the immediately preceding model_options result in this same controller conversation. AKK consumes the private current-snapshot offer, revalidates the exact pane/process plus idle and empty native UI under lock, rejects active Turns and every input-owning prompt/viewer, and verifies the effective postcondition. Codex 0.154 accepts one exact current native Session or a verified-zero-rollout pane without identify_foreground; Claude Code still requires one exact current native Session. Codex scope is current_and_new_sessions: the model and ordinary efforts (including max) are persisted, but ultra remains current-session-only; the future model is reported while the native TUI's unobservable fallback effort is omitted. Claude Code scope is current_session and leaves future defaults unchanged. Parameters never accept scope, raw commands, slash text, keys, menu indexes, display labels, fingerprints, or tokens. outcome=uncertain must never be retried automatically.",
478
+ parameters: setModelParameters,
479
+ timeoutMs: OPENCLAW_SET_MODEL_CLI_TIMEOUT_MS,
480
+ normalizeTurnIdentity: false,
481
+ isErrorResult: (result) => !isAkkSetModelSuccess(result),
482
+ buildArgs: (params, toolContext) => buildPrivateSetModelArgs(api, params, {
483
+ sessionKey: requiredOpenClawSessionKey(toolContext?.sessionKey),
484
+ sessionId: requiredOpenClawSessionId(toolContext?.sessionId)
485
+ })
486
+ });
487
+ }
412
488
  function registerForegroundIdentificationTools(api) {
413
489
  registerCliTool(api, {
414
490
  name: "agent_knock_knock_identify_foreground",
@@ -533,6 +609,11 @@ async function handleAkkCommand(api, ctx, displayedResumeSnapshots) {
533
609
  };
534
610
  }
535
611
  const config = isRecord(api.pluginConfig) ? api.pluginConfig : {};
612
+ if (parsed.action === "model-options" ||
613
+ parsed.action === "repair-model-control" ||
614
+ parsed.action === "set-model") {
615
+ return await handleAkkModelCommand(api, ctx, parsed, config);
616
+ }
536
617
  if (parsed.action === "list-resumable-threads" ||
537
618
  parsed.action === "new-thread" ||
538
619
  parsed.action === "resume-thread") {
@@ -607,6 +688,50 @@ async function handleAkkCommand(api, ctx, displayedResumeSnapshots) {
607
688
  };
608
689
  }
609
690
  }
691
+ async function handleAkkModelCommand(api, ctx, parsed, config) {
692
+ const sessionKey = requiredOpenClawSessionKey(ctx.sessionKey);
693
+ const sessionId = requiredOpenClawSessionId(ctx.sessionId);
694
+ if (parsed.action === "model-options") {
695
+ const action = await privateTerminalActionArguments(api, parsed.terminalId, "agent_knock_knock_model_options");
696
+ const args = buildAkkCommandCliArgs(parsed, config, {
697
+ expectedBindingToken: action.expected_binding_token
698
+ });
699
+ if (!args)
700
+ throw new Error("could not build model-options command");
701
+ const result = await runHostAwareCli(api, args, {
702
+ timeoutMs: OPENCLAW_MODEL_OPTIONS_CLI_TIMEOUT_MS
703
+ });
704
+ rememberDisplayedModelOptionsOffer(api, sessionKey, sessionId, parsed.terminalId, result);
705
+ return { text: formatAkkModelOptionsCommandResult(result) };
706
+ }
707
+ if (parsed.action === "repair-model-control") {
708
+ const action = await privateTerminalActionArguments(api, parsed.terminalId, "agent_knock_knock_repair_model_control");
709
+ const args = buildAkkCommandCliArgs(parsed, config, {
710
+ expectedBindingToken: action.expected_binding_token
711
+ });
712
+ if (!args)
713
+ throw new Error("could not build repair-model-control command");
714
+ const result = await runHostAwareCli(api, args, {
715
+ timeoutMs: OPENCLAW_REPAIR_MODEL_CONTROL_CLI_TIMEOUT_MS
716
+ });
717
+ return {
718
+ text: formatAkkRepairModelControlCommandResult(result),
719
+ isError: !isAkkRepairModelControlSuccess(result)
720
+ };
721
+ }
722
+ const args = buildPrivateSetModelArgs(api, {
723
+ terminal_id: parsed.terminalId,
724
+ model: parsed.model,
725
+ reasoning_effort: parsed.reasoningEffort
726
+ }, { sessionKey, sessionId });
727
+ const result = await runHostAwareCli(api, args, {
728
+ timeoutMs: OPENCLAW_SET_MODEL_CLI_TIMEOUT_MS
729
+ });
730
+ return {
731
+ text: formatAkkSetModelCommandResult(result),
732
+ isError: !isAkkSetModelSuccess(result)
733
+ };
734
+ }
610
735
  function sendCommandResultIsError(result) {
611
736
  return isSuccessfulTerminalDispatch(result)
612
737
  ? false
@@ -1296,15 +1421,20 @@ async function runSendRequest(api, params, toolContext, messageId) {
1296
1421
  ...normalizedTerminalSendResultContract(result)
1297
1422
  };
1298
1423
  }
1299
- function toolResult(result, { submissionErrors = false, normalizeTurnIdentity = true, forceError = false } = {}) {
1424
+ function toolResult(result, { submissionErrors = false, normalizeTurnIdentity = true, forceError = false, modelProjection = undefined, compactText = false } = {}) {
1300
1425
  const normalized = normalizeTurnIdentity ? withTurnIdentity(result) : result;
1301
- const modelFacing = sanitizeModelFacingValue(normalized);
1426
+ const sanitized = sanitizeModelFacingValue(normalized);
1427
+ const modelFacing = modelProjection
1428
+ ? modelProjection(sanitized)
1429
+ : sanitized;
1302
1430
  const submissionError = submissionErrors && isSubmissionError(normalized);
1303
1431
  return {
1304
1432
  content: [
1305
1433
  {
1306
1434
  type: "text",
1307
- text: JSON.stringify(modelFacing, null, 2)
1435
+ text: compactText
1436
+ ? JSON.stringify(modelFacing)
1437
+ : JSON.stringify(modelFacing, null, 2)
1308
1438
  }
1309
1439
  ],
1310
1440
  details: modelFacing,
@@ -1331,6 +1461,151 @@ async function privateThreadDiscovery(api, terminalId) {
1331
1461
  pushOptional(args, "--codex-home", stringValue(config.codexHome));
1332
1462
  return runHostAwareCli(api, args);
1333
1463
  }
1464
+ function rememberDisplayedModelOptionsOffer(api, sessionKeyValue, sessionIdValue, requestedTerminalIdValue, result) {
1465
+ const sessionKey = requiredOpenClawSessionKey(sessionKeyValue);
1466
+ const sessionId = requiredOpenClawSessionId(sessionIdValue);
1467
+ const requestedTerminalId = requiredString(requestedTerminalIdValue, "terminal_id");
1468
+ if (!isRecord(result)) {
1469
+ throw new Error("model-options returned an invalid result");
1470
+ }
1471
+ const terminalId = requiredString(result.terminal_id, "model-options terminal_id");
1472
+ if (terminalId !== requestedTerminalId) {
1473
+ throw new Error("model-options returned a different terminal");
1474
+ }
1475
+ const scope = requiredModelSwitchScope(result.agent, result.scope);
1476
+ const models = modelOptionsCatalog(result.models);
1477
+ const args = setModelActionArguments(result);
1478
+ if (stringValue(args.terminal ?? args.terminal_id) !== terminalId) {
1479
+ throw new Error("set-model action belongs to a different terminal");
1480
+ }
1481
+ const catalogFingerprint = requiredString(args.expected_catalog_fingerprint, "current internal model catalog authority");
1482
+ if (catalogFingerprint !== stringValue(result.catalog_fingerprint)) {
1483
+ throw new Error("set-model action does not match the displayed catalog");
1484
+ }
1485
+ const key = modelOptionsOfferKey(sessionKey, sessionId, terminalId);
1486
+ consumeOpenClawPrivateAuthorityOffer(api, key);
1487
+ rememberOpenClawPrivateAuthorityOffer(api, key, {
1488
+ fingerprint: catalogFingerprint,
1489
+ args,
1490
+ scope,
1491
+ models
1492
+ });
1493
+ }
1494
+ function modelOptionsCatalog(value) {
1495
+ if (!Array.isArray(value) || value.length === 0) {
1496
+ throw new Error("model-options returned no semantic model choices");
1497
+ }
1498
+ const models = value.map((entry) => {
1499
+ if (!isRecord(entry))
1500
+ throw new Error("model-options returned an invalid model");
1501
+ const id = requiredModelSemanticValue(entry.id, "model id", 160);
1502
+ if (!Array.isArray(entry.reasoning_efforts)) {
1503
+ throw new Error(`model ${id} has no reasoning-effort catalog`);
1504
+ }
1505
+ const reasoningEfforts = entry.reasoning_efforts.map((effort) => requiredReasoningEffort(effort, `reasoning effort for ${id}`));
1506
+ if (new Set(reasoningEfforts).size !== reasoningEfforts.length) {
1507
+ throw new Error(`model ${id} has duplicate reasoning efforts`);
1508
+ }
1509
+ return { id, reasoning_efforts: reasoningEfforts };
1510
+ });
1511
+ const ids = models.map((model) => String(model.id));
1512
+ if (new Set(ids).size !== ids.length) {
1513
+ throw new Error("model-options returned duplicate model ids");
1514
+ }
1515
+ return models;
1516
+ }
1517
+ function setModelActionArguments(result) {
1518
+ const availableActions = isRecord(result.available_actions)
1519
+ ? result.available_actions
1520
+ : undefined;
1521
+ const action = isRecord(availableActions?.set_model)
1522
+ ? availableActions.set_model
1523
+ : undefined;
1524
+ if (action?.tool !== "agent_knock_knock_set_model" ||
1525
+ !isRecord(action.arguments)) {
1526
+ throw new Error("model-options did not advertise a typed set-model action");
1527
+ }
1528
+ requiredString(action.arguments.expected_binding_token, "current internal set-model binding authority");
1529
+ return action.arguments;
1530
+ }
1531
+ function requiredModelSwitchScope(agentValue, scopeValue) {
1532
+ const agent = requiredString(agentValue, "model-options agent");
1533
+ const scope = requiredString(scopeValue, "model-options scope");
1534
+ const expected = agent === "codex"
1535
+ ? "current_and_new_sessions"
1536
+ : agent === "claude"
1537
+ ? "current_session"
1538
+ : undefined;
1539
+ if (!expected || scope !== expected) {
1540
+ throw new Error("model-options returned an unsupported agent or mutation scope");
1541
+ }
1542
+ return scope;
1543
+ }
1544
+ function modelOptionsOfferKey(sessionKey, sessionId, terminalId) {
1545
+ return privateAuthorityOfferKey(sessionKey, sessionId, OPENCLAW_MODEL_OPTIONS_AUTHORITY_KIND, { type: "terminal_id", id: terminalId });
1546
+ }
1547
+ function buildPrivateSetModelArgs(api, params, context) {
1548
+ assertOnlyModelControlParameters(params, ["terminal_id", "model", "reasoning_effort"], "set_model");
1549
+ const terminalId = requiredString(params.terminal_id, "terminal_id");
1550
+ const model = requiredModelSemanticValue(params.model, "model", 160);
1551
+ const reasoningEffort = requiredReasoningEffort(params.reasoning_effort, "reasoning_effort");
1552
+ const offered = consumeOpenClawPrivateAuthorityOffer(api, modelOptionsOfferKey(context.sessionKey, context.sessionId, terminalId));
1553
+ if (!offered || !isRecord(offered.args) || !Array.isArray(offered.models)) {
1554
+ throw new Error("set_model requires current choices shown by agent_knock_knock_model_options in this controller conversation; refresh model options and choose one exact semantic tuple");
1555
+ }
1556
+ assertOfferedModelTuple(offered.models, model, reasoningEffort);
1557
+ const offeredTerminalId = stringValue(offered.args.terminal ?? offered.args.terminal_id);
1558
+ if (offeredTerminalId !== terminalId) {
1559
+ throw new Error("the displayed set-model action belongs to another terminal");
1560
+ }
1561
+ const fingerprint = requiredString(offered.args.expected_catalog_fingerprint, "current internal model catalog authority");
1562
+ if (fingerprint !== offered.fingerprint) {
1563
+ throw new Error("the displayed model catalog authority changed");
1564
+ }
1565
+ const config = isRecord(api.pluginConfig) ? api.pluginConfig : {};
1566
+ const args = buildAkkCommandCliArgs({
1567
+ action: "set-model",
1568
+ terminalId,
1569
+ model,
1570
+ reasoningEffort
1571
+ }, config, {
1572
+ expectedBindingToken: offered.args.expected_binding_token,
1573
+ expectedCatalogFingerprint: fingerprint
1574
+ });
1575
+ if (!args)
1576
+ throw new Error("could not build set-model command");
1577
+ return args;
1578
+ }
1579
+ function assertOnlyModelControlParameters(params, allowed, label) {
1580
+ const unexpected = Object.keys(params).filter((key) => !allowed.includes(key));
1581
+ if (unexpected.length > 0) {
1582
+ throw new Error(`${label} accepts only typed semantic fields; raw commands, keys, menu indexes, scope, labels, tokens, and fingerprints are forbidden`);
1583
+ }
1584
+ }
1585
+ function assertOfferedModelTuple(modelEntries, model, reasoningEffort) {
1586
+ const entry = modelEntries.find((candidate) => isRecord(candidate) && candidate.id === model);
1587
+ if (!isRecord(entry) || !Array.isArray(entry.reasoning_efforts)) {
1588
+ throw new Error("model was not advertised by the current native catalog");
1589
+ }
1590
+ if (!entry.reasoning_efforts.includes(reasoningEffort)) {
1591
+ throw new Error("reasoning_effort was not advertised for this model by the current native catalog");
1592
+ }
1593
+ }
1594
+ function requiredModelSemanticValue(value, label, maxLength) {
1595
+ const text = requiredString(value, label);
1596
+ if (text.length > maxLength ||
1597
+ !/^[A-Za-z0-9][A-Za-z0-9._:/+\-]*$/u.test(text)) {
1598
+ throw new Error(`${label} must be one exact advertised semantic id`);
1599
+ }
1600
+ return text;
1601
+ }
1602
+ function requiredReasoningEffort(value, label) {
1603
+ const text = requiredString(value, label);
1604
+ if (text.length > 64 || !/^[a-z][a-z0-9_-]*$/u.test(text)) {
1605
+ throw new Error(`${label} must be one exact advertised semantic value`);
1606
+ }
1607
+ return text;
1608
+ }
1334
1609
  async function privateTerminalActionArguments(api, terminalId, tool, options = {}) {
1335
1610
  return privateActionArguments(api, {
1336
1611
  tool,
@@ -1822,6 +2097,16 @@ function sanitizeModelFacingValue(value, parentKey, path = []) {
1822
2097
  };
1823
2098
  delete output.missing_required;
1824
2099
  }
2100
+ if (output.tool === "agent_knock_knock_set_model" &&
2101
+ isRecord(output.arguments)) {
2102
+ const terminalId = stringValue(output.arguments.terminal_id ?? output.arguments.terminal);
2103
+ output.arguments = terminalId && /^terminal:v[0-9]+:\S+$/u.test(terminalId)
2104
+ ? { terminal_id: terminalId }
2105
+ : {};
2106
+ if (Array.isArray(output.missing_required)) {
2107
+ output.missing_required = output.missing_required.filter((field) => field === "model" || field === "reasoning_effort");
2108
+ }
2109
+ }
1825
2110
  return output;
1826
2111
  }
1827
2112
  function isLegacyAuthorityInstructionPath(path) {
@@ -2120,7 +2405,7 @@ function terminalMessageIdForToolCall({ toolCallId: toolCallIdValue, sessionKey:
2120
2405
  .digest("hex");
2121
2406
  return `msg-openclaw-${digest}`;
2122
2407
  }
2123
- function registerCliTool(api, { name, description, parameters, buildArgs, normalizeTurnIdentity = true, isErrorResult = (_result) => false }) {
2408
+ function registerCliTool(api, { name, description, parameters, buildArgs, rememberResult = undefined, timeoutMs = undefined, normalizeTurnIdentity = true, modelProjection = undefined, compactText = false, isErrorResult = (_result) => false }) {
2124
2409
  api.registerTool((toolContext) => ({
2125
2410
  label: toolLabel(name),
2126
2411
  name,
@@ -2129,16 +2414,21 @@ function registerCliTool(api, { name, description, parameters, buildArgs, normal
2129
2414
  async execute(toolCallId, params, signal) {
2130
2415
  return withHostBridgeInvocationSignal(signal, async () => {
2131
2416
  try {
2132
- const result = await runHostAwareCli(api, await buildArgs(isRecord(params) ? params : {}, toolContext, toolCallId));
2417
+ const result = await runHostAwareCli(api, await buildArgs(isRecord(params) ? params : {}, toolContext, toolCallId), timeoutMs === undefined ? {} : { timeoutMs });
2418
+ if (typeof rememberResult === "function") {
2419
+ rememberResult(result, isRecord(params) ? params : {}, toolContext);
2420
+ }
2421
+ if (name === "agent_knock_knock_list") {
2422
+ rememberDisplayedPrivateAuthorityOffers(api, toolContext?.sessionKey, toolContext?.sessionId, result);
2423
+ }
2133
2424
  const rendered = toolResult(result, {
2134
2425
  submissionErrors: name === "agent_knock_knock_respond" ||
2135
2426
  name === "agent_knock_knock_identify_and_send",
2136
2427
  normalizeTurnIdentity,
2428
+ modelProjection,
2429
+ compactText,
2137
2430
  forceError: typeof isErrorResult === "function" && isErrorResult(result) === true
2138
2431
  });
2139
- if (name === "agent_knock_knock_list") {
2140
- rememberDisplayedPrivateAuthorityOffers(api, toolContext?.sessionKey, toolContext?.sessionId, result);
2141
- }
2142
2432
  return rendered;
2143
2433
  }
2144
2434
  catch (error) {