@scotthuang/agent-knock-knock 0.12.12 → 0.12.13

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 (76) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +39 -26
  3. package/dist/src/approval-policy.d.ts +11 -2
  4. package/dist/src/approval-policy.js +33 -7
  5. package/dist/src/approval-policy.js.map +1 -1
  6. package/dist/src/claude-local-transcript-provider.d.ts +81 -0
  7. package/dist/src/claude-local-transcript-provider.js +854 -18
  8. package/dist/src/claude-local-transcript-provider.js.map +1 -1
  9. package/dist/src/cli-core.js +85 -0
  10. package/dist/src/cli-core.js.map +1 -1
  11. package/dist/src/deferred-foreground-authority-cli-adapter.js +90 -19
  12. package/dist/src/deferred-foreground-authority-cli-adapter.js.map +1 -1
  13. package/dist/src/deferred-foreground-transfer.js +3 -4
  14. package/dist/src/deferred-foreground-transfer.js.map +1 -1
  15. package/dist/src/openclaw-callback-transport.d.ts +5 -0
  16. package/dist/src/openclaw-callback-transport.js +1 -1
  17. package/dist/src/openclaw-callback-transport.js.map +1 -1
  18. package/dist/src/openclaw-plugin-callback-adapter.js +96 -14
  19. package/dist/src/openclaw-plugin-callback-adapter.js.map +1 -1
  20. package/dist/src/openclaw-plugin-command-adapter.js +548 -107
  21. package/dist/src/openclaw-plugin-command-adapter.js.map +1 -1
  22. package/dist/src/openclaw-plugin-helpers.d.ts +27 -4
  23. package/dist/src/openclaw-plugin-helpers.js +309 -44
  24. package/dist/src/openclaw-plugin-helpers.js.map +1 -1
  25. package/dist/src/openclaw-plugin-schemas.d.ts +44 -68
  26. package/dist/src/openclaw-plugin-schemas.js +63 -91
  27. package/dist/src/openclaw-plugin-schemas.js.map +1 -1
  28. package/dist/src/openclaw-plugin-supervisor.js +48 -10
  29. package/dist/src/openclaw-plugin-supervisor.js.map +1 -1
  30. package/dist/src/openclaw-private-authority-offers.d.ts +22 -0
  31. package/dist/src/openclaw-private-authority-offers.js +177 -0
  32. package/dist/src/openclaw-private-authority-offers.js.map +1 -0
  33. package/dist/src/store.d.ts +1 -0
  34. package/dist/src/store.js +3 -1
  35. package/dist/src/store.js.map +1 -1
  36. package/dist/src/terminal-action-projection.d.ts +19 -1
  37. package/dist/src/terminal-action-projection.js +23 -0
  38. package/dist/src/terminal-action-projection.js.map +1 -1
  39. package/dist/src/terminal-command-cli-adapter.d.ts +5 -0
  40. package/dist/src/terminal-command-cli-adapter.js +249 -89
  41. package/dist/src/terminal-command-cli-adapter.js.map +1 -1
  42. package/dist/src/terminal-dispatch-repository-cli-adapter.js +305 -79
  43. package/dist/src/terminal-dispatch-repository-cli-adapter.js.map +1 -1
  44. package/dist/src/terminal-list-cli-adapter.d.ts +31 -7
  45. package/dist/src/terminal-list-cli-adapter.js +285 -67
  46. package/dist/src/terminal-list-cli-adapter.js.map +1 -1
  47. package/dist/src/terminal-list-renderer.d.ts +2 -0
  48. package/dist/src/terminal-list-renderer.js +116 -41
  49. package/dist/src/terminal-list-renderer.js.map +1 -1
  50. package/dist/src/terminal-monitor-cli-adapter.js +1 -1
  51. package/dist/src/terminal-monitor-cli-adapter.js.map +1 -1
  52. package/dist/src/terminal-monitor-state-cli-adapter.js +6 -15
  53. package/dist/src/terminal-monitor-state-cli-adapter.js.map +1 -1
  54. package/dist/src/terminal-status-cli-adapter.d.ts +10 -0
  55. package/dist/src/terminal-status-cli-adapter.js +89 -7
  56. package/dist/src/terminal-status-cli-adapter.js.map +1 -1
  57. package/dist/src/terminal-submission-acceptance.d.ts +60 -1
  58. package/dist/src/terminal-submission-acceptance.js +560 -0
  59. package/dist/src/terminal-submission-acceptance.js.map +1 -1
  60. package/dist/src/terminal-watch-callback-cli-adapter.d.ts +20 -0
  61. package/dist/src/terminal-watch-callback-cli-adapter.js +55 -0
  62. package/dist/src/terminal-watch-callback-cli-adapter.js.map +1 -0
  63. package/dist/src/terminal-watch-cli-adapter.d.ts +64 -0
  64. package/dist/src/terminal-watch-cli-adapter.js +508 -0
  65. package/dist/src/terminal-watch-cli-adapter.js.map +1 -0
  66. package/dist/src/terminal-watch-service.d.ts +97 -0
  67. package/dist/src/terminal-watch-service.js +714 -0
  68. package/dist/src/terminal-watch-service.js.map +1 -0
  69. package/dist/src/terminal-watch-store.d.ts +149 -0
  70. package/dist/src/terminal-watch-store.js +861 -0
  71. package/dist/src/terminal-watch-store.js.map +1 -0
  72. package/docs/quickstart-herdr.md +8 -0
  73. package/docs/quickstart-tmux.md +33 -6
  74. package/openclaw.plugin.json +11 -3
  75. package/package.json +1 -1
  76. package/templates/openclaw-skills/agent-knock-knock/SKILL.md +57 -39
@@ -3,9 +3,12 @@ import { createHash } from "node:crypto";
3
3
  import { fileURLToPath } from "node:url";
4
4
  import { executorDefinitionForKind } from "./executors.js";
5
5
  import { isRecord, nonBlankString as stringValue } from "./value-guards.js";
6
- import { AKK_CALLBACK_METHOD, akkUsageText, buildAkkCommandCliArgs, formatAkkListCommandResult, formatAkkRespondCommandResult, formatAkkThreadsCommandResult, formatAkkThreadTransitionCommandResult, isAkkNativeSubmissionAccepted, isAkkThreadTransitionSuccess, parseAkkCommand, resolvePluginStoreDir } from "./openclaw-plugin-helpers.js";
7
- import { approveParameters, cancelParameters, closeParameters, listParameters, listResumableThreadsParameters, nativeInspectParameters, newThreadParameters, reconcileBindingParameters, renewParameters, respondParameters, resumeThreadParameters, retryCallbackParameters, sendParameters, statusParameters } from "./openclaw-plugin-schemas.js";
6
+ 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";
7
+ import { approveParameters, cancelParameters, closeParameters, listParameters, listResumableThreadsParameters, nativeInspectParameters, newThreadParameters, reconcileBindingParameters, renewParameters, respondParameters, resumeThreadParameters, retryCallbackParameters, sendParameters, statusParameters, unwatchParameters, watchParameters } from "./openclaw-plugin-schemas.js";
8
+ import { consumeOpenClawPrivateAuthorityOffer, openClawApprovalAuthorityOfferKey, rememberOpenClawPrivateAuthorityOffer } from "./openclaw-private-authority-offers.js";
8
9
  const MAX_DISPLAYED_RESUME_SNAPSHOTS = 512;
10
+ const OPENCLAW_HANDOFF_AUTHORITY_KIND = "handoff";
11
+ const OPENCLAW_RECONCILE_BINDING_AUTHORITY_KIND = "reconcile_binding";
9
12
  const CALLBACK_METHOD = AKK_CALLBACK_METHOD;
10
13
  export const defaultOpenClawRelayPath = fileURLToPath(new URL("./cli.js", import.meta.url));
11
14
  const relayPathByApi = new WeakMap();
@@ -15,20 +18,20 @@ export function bindOpenClawRelayPath(api, relayPath) {
15
18
  export function registerOpenClawCommands(api, displayedResumeSnapshots) {
16
19
  api.registerCommand?.({
17
20
  name: "akk",
18
- description: "Send coding work through existing Codex or Claude Code shared terminals, inspect managed turns, and explicitly start, clear, list, or resume native threads.",
21
+ description: "Send coding work through existing Codex or Claude Code shared terminals, inspect managed Turns, observe human-started work with durable Terminal Watch, and explicitly start, clear, list, or resume native threads.",
19
22
  acceptsArgs: true,
20
23
  requireAuth: true,
21
24
  nativeProgressMessages: {
22
25
  default: "AKK is handling the request..."
23
26
  },
24
27
  agentPromptGuidance: [
25
- "Use /akk <task> when exactly one eligible idle coding-agent terminal pane should receive new work. Use /akk codex: <task>, /akk claude: <task>, or another selector returned by /akk list to target an existing pane. Use session_id only when the current list action prefills it. A rollout-backed Codex pane instead uses terminal_follow_current: an exact list-prefilled terminal selector plus expected_terminal_token, or an explicitly named/uniquely selected pane whose equivalent fresh candidate authority AKK derives under its terminal lock. AKK sends the ordinary task once within the complete exact rollout inventory and binds only the uniquely accepting native thread. Never infer or reuse a listed token. Use /akk threads, /akk new-thread or clear-thread, and /akk resume-thread only with an exact full terminal_id returned by /akk list; these switch native context without creating a Turn. Resume numbers and short IDs are bound to the last displayed snapshot, while previous is available only from the latest verified committed transition. For native Codex or Claude status, use only an advertised agent_knock_knock_native_inspect action; agent_knock_knock_status inspects AKK Turn state and does not execute /status. AKK never starts a coding-agent process."
28
+ "Use /akk <task> when exactly one eligible idle coding-agent terminal pane should receive new work. 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. AKK keeps all opaque freshness authority private and revalidates it under its locks. Use /akk watch only from a currently advertised watch action to observe one human-started active task; it sends no input and creates no Session or Turn. New/clear/resume, approval, reconciliation, handoff, and recovery still require the documented user intent or explicit confirmation. AKK never starts a coding-agent process."
26
29
  ],
27
30
  handler: async (ctx) => handleAkkCommand(api, ctx, displayedResumeSnapshots)
28
31
  });
29
32
  registerCliTool(api, {
30
33
  name: "agent_knock_knock_list",
31
- description: "List existing Codex and Claude Code tmux or Herdr 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. A live human thread switch remains management_state=conflict and exposes handoff_state as adoptable or blocked without mutating the Store. Use only each row's available_actions and authoritative prefilled arguments, except for two explicit-confirmation nested recovery paths: handoff_decision.choices.take_over_current.action and blocking_turns[].recovery_action. The latter is only for collateral blockers; an active handoff source Turn remains exclusively snapshot-fenced by handoff_decision. Refresh list immediately after either nested action. A session_exact session-scoped send targets session_id only when that action is listed and never redirects to the pane's replacement context. A rollout-backed Codex row instead advertises terminal_follow_current as a terminal-scoped follow-current send with its exact selector and expected_terminal_token; AKK pins the complete exact rollout inventory, sends the task once, and binds only the unique exact acceptor. Explicit discovery selectors remain compatible and derive equivalent fresh candidate authority under the terminal lock. respond targets the exact in-flight turn; native_inspect runs only a closed, exact-version Codex or Claude status profile with the current terminal/binding token; read-only thread listing targets the exact terminal, while new/resume mutations also require the current binding token and create no Turn; reconcile_binding remains a low-level conflict recovery action; managed controls target the exact turn; a raw terminal row may prefill its own compatibility selector for status or recovery controls. Never construct identifiers or tokens. AKK revalidates every terminal side effect and never starts a coding-agent process.",
34
+ description: "List existing Codex and Claude Code tmux or Herdr panes as the primary terminals[] resources, plus durable terminal_watches[] records. Use only advertised actions and their semantic IDs. AKK keeps opaque freshness authority private and revalidates terminal, binding, candidate, approval, and handoff state before every side effect. A session_exact send uses session_id; terminal_follow_current and Watch use terminal_id; managed controls use turn_id; lifecycle resume additionally uses the complete native_thread_id. Recovery actions remain explicit-confirmation operations. AKK never starts a coding-agent process.",
32
35
  parameters: listParameters,
33
36
  buildArgs: (params) => {
34
37
  const config = isRecord(api.pluginConfig) ? api.pluginConfig : {};
@@ -49,9 +52,47 @@ export function registerOpenClawCommands(api, displayedResumeSnapshots) {
49
52
  return args;
50
53
  }
51
54
  });
55
+ registerCliTool(api, {
56
+ name: "agent_knock_knock_watch",
57
+ description: "Start one durable Terminal Watch for the exact supported human-started task already active in a listed Codex or Claude Code terminal. Call only from that terminal row's current watch action and pass its exact terminal_id. AKK refreshes and revalidates current observation authority internally. This observes external work, creates no AKK Session or Turn, sends no terminal input, and never adopts or blocks the human's terminal task.",
58
+ parameters: watchParameters,
59
+ normalizeTurnIdentity: false,
60
+ buildArgs: (params, toolContext) => {
61
+ const config = isRecord(api.pluginConfig) ? api.pluginConfig : {};
62
+ const openclawSession = stringValue(toolContext?.sessionKey) ??
63
+ "agent:main:main";
64
+ const args = [
65
+ "watch-terminal",
66
+ "--terminal",
67
+ requiredString(params.terminal_id, "terminal_id")
68
+ ];
69
+ pushOptional(args, "--store-dir", resolvePluginStoreDir(config));
70
+ pushOptional(args, "--hard-timeout-minutes", numberString(params.hardTimeoutMinutes) ??
71
+ numberString(config.agentHardTimeoutMinutes));
72
+ pushOptional(args, "--openclaw-session", openclawSession);
73
+ pushOptional(args, "--openclaw-bin", stringValue(config.openclawBin));
74
+ return args;
75
+ }
76
+ });
77
+ registerCliTool(api, {
78
+ name: "agent_knock_knock_unwatch",
79
+ description: "Stop one exact durable Terminal Watch by its authoritative watch_id. This cancels observation only; it sends no terminal input and does not interrupt, adopt, or otherwise mutate the human's coding-agent task.",
80
+ parameters: unwatchParameters,
81
+ normalizeTurnIdentity: false,
82
+ buildArgs: (params) => {
83
+ const config = isRecord(api.pluginConfig) ? api.pluginConfig : {};
84
+ const args = [
85
+ "unwatch-terminal",
86
+ "--watch",
87
+ requiredString(params.watch_id, "watch_id")
88
+ ];
89
+ pushOptional(args, "--store-dir", resolvePluginStoreDir(config));
90
+ return args;
91
+ }
92
+ });
52
93
  registerCliTool(api, {
53
94
  name: "agent_knock_knock_list_resumable_threads",
54
- description: "List verified native Codex or Claude Code threads for one exact terminal. Call this only from that terminal row's available list_resumable_threads action. The result includes the current native identity, a fresh expected_binding_token, exact candidate UUIDs with opaque candidate_token fingerprints, and an exact previous action only when the latest committed transition has one uniquely resumable source. Resume only a row with resumable=true or that previous action, preserving all full prefilled arguments from the same result. Number/short/handle fields are human display aids, never exact tool arguments. This is read-only for Session/Turn state and creates no AKK Turn.",
95
+ description: "List verified native Codex or Claude Code threads for one exact terminal. Resume only a row with resumable=true by passing this terminal_id and that row's complete native_thread_id. AKK retains candidate and binding freshness evidence privately. Number and short-id fields are slash-command display aids, never tool arguments. This is read-only for Session/Turn state and creates no AKK Turn.",
55
96
  parameters: listResumableThreadsParameters,
56
97
  normalizeTurnIdentity: false,
57
98
  buildArgs: (params) => {
@@ -68,7 +109,7 @@ export function registerOpenClawCommands(api, displayedResumeSnapshots) {
68
109
  });
69
110
  registerCliTool(api, {
70
111
  name: "agent_knock_knock_native_inspect",
71
- description: "Execute one closed, exact-version native status inspection in an exact Codex or Claude Code terminal without ordinary Turn delivery. Call only from that terminal row's current native_inspect action and preserve its exact terminal_id, inspection=status, and expected_binding_token. Supported profiles are Codex 0.146.0/0.146.1/0.147.0/0.148.0 and Claude Code 2.1.218/2.1.226/2.1.237; Claude's exact Status panel is parsed and safely dismissed before success. Arbitrary slash commands, /usage, /cost, /stats, /usage-credits, /model, and /compact remain unavailable. Bare Codex /usage opens an interactive menu whose later Enter can select an account-side usage-limit reset. This creates no AKK Session, Turn, receipt, monitor, or callback. agent_knock_knock_status is different: it inspects AKK Turn state and the bounded current screen without executing native /status.",
112
+ 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.",
72
113
  parameters: nativeInspectParameters,
73
114
  normalizeTurnIdentity: false,
74
115
  buildArgs: (params) => {
@@ -77,14 +118,16 @@ export function registerOpenClawCommands(api, displayedResumeSnapshots) {
77
118
  if (inspection !== "status") {
78
119
  throw new Error("inspection must be status");
79
120
  }
121
+ const terminalId = requiredString(params.terminal_id, "terminal_id");
122
+ const action = privateTerminalActionArguments(api, terminalId, "agent_knock_knock_native_inspect");
80
123
  const args = [
81
124
  "native-inspect",
82
125
  "--terminal",
83
- requiredString(params.terminal_id, "terminal_id"),
126
+ terminalId,
84
127
  "--inspection",
85
128
  inspection,
86
129
  "--expected-binding-token",
87
- requiredString(params.expected_binding_token, "expected_binding_token")
130
+ requiredString(action.expected_binding_token, "current internal native-inspect binding authority")
88
131
  ];
89
132
  pushOptional(args, "--store-dir", resolvePluginStoreDir(config));
90
133
  pushOptional(args, "--codex-home", stringValue(config.codexHome));
@@ -93,18 +136,20 @@ export function registerOpenClawCommands(api, displayedResumeSnapshots) {
93
136
  });
94
137
  registerCliTool(api, {
95
138
  name: "agent_knock_knock_new_thread",
96
- description: "Start and verify a clean native coding-agent thread in the same exact terminal. Call only from a current available new_thread action, using its exact terminal_id and expected_binding_token, or immediately after agent_knock_knock_list_resumable_threads using that result's token. Never send /clear as ordinary task text. This lifecycle transition creates a new AKK Session but no Turn; use ordinary send afterward.",
139
+ description: "Start and verify a clean native coding-agent thread in the exact terminal_id after explicit user intent. AKK refreshes lifecycle authority privately. Never send /clear as ordinary task text. This creates a new AKK Session but no Turn.",
97
140
  parameters: newThreadParameters,
98
141
  normalizeTurnIdentity: false,
99
142
  isErrorResult: (result) => !isAkkThreadTransitionSuccess(result),
100
143
  buildArgs: (params) => {
101
144
  const config = isRecord(api.pluginConfig) ? api.pluginConfig : {};
145
+ const terminalId = requiredString(params.terminal_id, "terminal_id");
146
+ const action = privateTerminalActionArguments(api, terminalId, "agent_knock_knock_new_thread");
102
147
  const args = [
103
148
  "new-thread",
104
149
  "--terminal",
105
- requiredString(params.terminal_id, "terminal_id"),
150
+ terminalId,
106
151
  "--expected-binding-token",
107
- requiredString(params.expected_binding_token, "expected_binding_token")
152
+ requiredString(action.expected_binding_token, "current internal new-thread binding authority")
108
153
  ];
109
154
  pushOptional(args, "--store-dir", resolvePluginStoreDir(config));
110
155
  pushOptional(args, "--codex-home", stringValue(config.codexHome));
@@ -113,27 +158,40 @@ export function registerOpenClawCommands(api, displayedResumeSnapshots) {
113
158
  });
114
159
  registerCliTool(api, {
115
160
  name: "agent_knock_knock_reconcile_binding",
116
- description: "Detach one exact conflicting managed Session binding without adopting the live replacement thread. Call only after the user explicitly requests recovery and only from a current terminal row's advertised reconcile_binding action. Preserve its terminal_id, conflicting_session_id, Session revision, binding token, and terminal token exactly. This action sends no coding-agent input and creates no Turn; refresh AKK list afterward.",
161
+ 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.",
117
162
  parameters: reconcileBindingParameters,
118
163
  normalizeTurnIdentity: false,
119
- buildArgs: (params) => {
164
+ buildArgs: (params, toolContext) => {
120
165
  const config = isRecord(api.pluginConfig) ? api.pluginConfig : {};
121
- const revision = Number(params.expected_session_revision);
166
+ const terminalId = requiredString(params.terminal_id, "terminal_id");
167
+ const conflictingSessionId = requiredString(params.conflicting_session_id, "conflicting_session_id");
168
+ const action = consumeDisplayedPrivateAction(api, {
169
+ sessionKey: requiredOpenClawSessionKey(toolContext?.sessionKey),
170
+ sessionId: requiredOpenClawSessionId(toolContext?.sessionId),
171
+ kind: OPENCLAW_RECONCILE_BINDING_AUTHORITY_KIND,
172
+ target: { type: "terminal_id", id: terminalId },
173
+ tool: "agent_knock_knock_reconcile_binding",
174
+ terminalId,
175
+ matches: (argumentsValue) => stringValue(argumentsValue.terminal_id) === terminalId &&
176
+ stringValue(argumentsValue.conflicting_session_id) ===
177
+ conflictingSessionId
178
+ });
179
+ const revision = Number(action.expected_session_revision);
122
180
  if (!Number.isSafeInteger(revision) || revision < 1) {
123
181
  throw new Error("expected_session_revision must be a positive safe integer");
124
182
  }
125
183
  const args = [
126
184
  "reconcile-binding",
127
185
  "--terminal",
128
- requiredString(params.terminal_id, "terminal_id"),
186
+ terminalId,
129
187
  "--conflicting-session",
130
- requiredString(params.conflicting_session_id, "conflicting_session_id"),
188
+ requiredString(conflictingSessionId, "conflicting_session_id"),
131
189
  "--expected-session-revision",
132
190
  String(revision),
133
191
  "--expected-binding-token",
134
- requiredString(params.expected_binding_token, "expected_binding_token"),
192
+ requiredString(action.expected_binding_token, "current internal conflicting binding authority"),
135
193
  "--expected-terminal-token",
136
- requiredString(params.expected_terminal_token, "expected_terminal_token")
194
+ requiredString(action.expected_terminal_token, "current internal terminal authority")
137
195
  ];
138
196
  pushOptional(args, "--store-dir", resolvePluginStoreDir(config));
139
197
  pushOptional(args, "--codex-home", stringValue(config.codexHome));
@@ -142,51 +200,74 @@ export function registerOpenClawCommands(api, displayedResumeSnapshots) {
142
200
  });
143
201
  registerCliTool(api, {
144
202
  name: "agent_knock_knock_resume_thread",
145
- description: "Resume one exact verified historical native thread in the same terminal. First call agent_knock_knock_list_resumable_threads, then pass its exact terminal_id and expected_binding_token plus one complete native_thread_id whose row says resumable=true and that same row's candidate_token. Never guess, truncate, or reuse IDs or tokens, and never send a resume slash command as ordinary task text. This creates or reactivates an AKK Session but creates no Turn.",
203
+ description: "Resume one exact verified historical native thread after explicit user intent. Pass only terminal_id and one complete native_thread_id from a resumable=true row; AKK refreshes binding and candidate evidence privately. This creates or reactivates an AKK Session but no Turn.",
146
204
  parameters: resumeThreadParameters,
147
205
  normalizeTurnIdentity: false,
148
206
  isErrorResult: (result) => !isAkkThreadTransitionSuccess(result),
149
207
  buildArgs: (params) => {
150
208
  const config = isRecord(api.pluginConfig) ? api.pluginConfig : {};
209
+ const terminalId = requiredString(params.terminal_id, "terminal_id");
210
+ const nativeThreadId = requiredString(params.native_thread_id, "native_thread_id");
211
+ const discovery = privateThreadDiscovery(api, terminalId);
212
+ const candidate = Array.isArray(discovery.threads)
213
+ ? discovery.threads.find((thread) => isRecord(thread) &&
214
+ thread.resumable === true &&
215
+ stringValue(thread.native_thread_id) === nativeThreadId)
216
+ : undefined;
217
+ if (!isRecord(candidate)) {
218
+ throw new Error(`native thread ${nativeThreadId} is not resumable in the current lifecycle snapshot`);
219
+ }
151
220
  const args = [
152
221
  "resume-thread",
153
222
  "--terminal",
154
- requiredString(params.terminal_id, "terminal_id"),
223
+ terminalId,
155
224
  "--native-thread",
156
- requiredString(params.native_thread_id, "native_thread_id"),
225
+ nativeThreadId,
157
226
  "--expected-binding-token",
158
- requiredString(params.expected_binding_token, "expected_binding_token"),
227
+ requiredString(discovery.expected_binding_token, "current internal lifecycle binding authority"),
159
228
  "--candidate-token",
160
- requiredString(params.candidate_token, "candidate_token")
229
+ requiredString(candidate.candidate_token, "current internal resume candidate authority")
161
230
  ];
162
231
  pushOptional(args, "--store-dir", resolvePluginStoreDir(config));
163
232
  pushOptional(args, "--codex-home", stringValue(config.codexHome));
164
233
  return args;
165
234
  }
166
235
  });
167
- api.registerTool((_toolContext) => ({
236
+ api.registerTool((toolContext) => ({
168
237
  label: "AKK Status",
169
238
  name: "agent_knock_knock_status",
170
- description: "Inspect one exact AKK-managed turn by its authoritative turn_id, or use only a raw terminal row's own prefilled compatibility selector. The deprecated conversation_id remains a legacy Turn alias and the list-prefilled raw-terminal input; never construct it. Returns live state, a bounded terminal screen, and available purpose/context with confidence and limitations. AKK never starts a coding agent.",
239
+ description: "Inspect one exact AKK-managed Turn by its authoritative turn_id, one durable Terminal Watch by its authoritative watch_id, or use only a raw terminal row's own prefilled compatibility selector. These targets are mutually exclusive. The deprecated conversation_id remains a legacy Turn alias and the list-prefilled raw-terminal input; never construct it. Watch status describes observed external work and never claims AKK sent or adopted the task. AKK never starts a coding agent.",
171
240
  parameters: statusParameters,
172
241
  async execute(_toolCallId, params) {
173
- const result = runCli(api, buildStatusCliArgs(api, isRecord(params) ? params : {}));
174
- return toolResult(result);
242
+ try {
243
+ const result = runCli(api, buildStatusCliArgs(api, isRecord(params) ? params : {}));
244
+ const rendered = toolResult(result);
245
+ rememberDisplayedApprovalOffer(api, toolContext?.sessionKey, toolContext?.sessionId, result);
246
+ return rendered;
247
+ }
248
+ catch (error) {
249
+ throw modelFacingToolError(error);
250
+ }
175
251
  }
176
252
  }), { name: "agent_knock_knock_status", optional: true });
177
253
  api.registerTool((toolContext) => ({
178
254
  label: "AKK Send",
179
255
  name: "agent_knock_knock_send",
180
- description: "Start a new AKK Turn. A session_exact session-scoped send uses session_id only when the current list action prefills it; it preserves that Session's native context and never redirects to a thread the human selected later in the pane. Rollout-backed Codex uses terminal_follow_current terminal-scoped follow-current instead: preserve the exact list-prefilled full terminal selector and fresh expected_terminal_token, or preserve a selector explicitly named by the user. AKK pins the complete exact rollout inventory under the terminal lock, sends the request once, and binds only the unique exact rollout that accepts it. Never infer, copy, or reuse a listed token. Both target fields may be omitted only when AKK should require one unique eligible idle pane and derive the same fresh candidate authority. Never pass a turn id or selector as session_id. To answer an in-flight question, use agent_knock_knock_respond instead. For ordinary use 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
+ description: "Start a new AKK Turn. Use session_id only for an advertised session_exact action, or terminal_id for an advertised terminal_follow_current action; omit both only when AKK should require one unique eligible idle pane. AKK derives all terminal freshness authority privately, revalidates the exact pane/process/context under lock, sends once, and binds only the unique accepting native thread. Never pass a Turn id as a destination. This is asynchronous: after acceptance, yield and wait for the callback or an explicit status request.",
181
257
  parameters: sendParameters,
182
258
  async execute(toolCallId, params) {
183
- const result = await runSendRequest(api, isRecord(params) ? params : {}, toolContext, terminalMessageIdForToolCall({
184
- toolCallId,
185
- sessionKey: toolContext?.sessionKey,
186
- sessionId: toolContext?.sessionId,
187
- toolName: "agent_knock_knock_send"
188
- }));
189
- return toolResult(result, { submissionErrors: true });
259
+ try {
260
+ const result = await runSendRequest(api, isRecord(params) ? params : {}, toolContext, terminalMessageIdForToolCall({
261
+ toolCallId,
262
+ sessionKey: toolContext?.sessionKey,
263
+ sessionId: toolContext?.sessionId,
264
+ toolName: "agent_knock_knock_send"
265
+ }));
266
+ return toolResult(result, { submissionErrors: true });
267
+ }
268
+ catch (error) {
269
+ throw modelFacingToolError(error);
270
+ }
190
271
  }
191
272
  }), { name: "agent_knock_knock_send", optional: true });
192
273
  registerCliTool(api, {
@@ -216,24 +297,12 @@ export function registerOpenClawCommands(api, displayedResumeSnapshots) {
216
297
  });
217
298
  registerCliTool(api, {
218
299
  name: "agent_knock_knock_approve",
219
- description: "Manually approve a permission request only after the user reviews and explicitly confirms it. Managed approval uses exact turn_id. An unmanaged raw terminal, or a managed Codex pane whose current prompt has no usable AKK Turn owner, may be approved only through its own latest list-prefilled action; preserve its full conversation_id and expected_terminal_token exactly. Terminal-scoped approval never enters automatic approval, never changes managed Turn or Session binding, and has no durable dispatch receipt: if its result is interrupted, refresh status and inspect the live prompt instead of retrying blindly. Claude Code uses no Hooks: this manual path accepts only an exact one-time Bash permission screen, 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.",
300
+ description: "Manually approve the current exact permission request only after the user reviews and explicitly confirms it. Use turn_id for a managed Turn or terminal_id for a separately advertised terminal-scoped approval. AKK privately refreshes the approval prompt and terminal authority, then the CLI recaptures the same prompt under lock before sending keys. Never retry an interrupted approval blindly. This tool cannot enable automatic approval.",
220
301
  parameters: approveParameters,
221
- buildArgs: (params) => {
222
- const config = isRecord(api.pluginConfig) ? api.pluginConfig : {};
223
- const args = ["approve"];
224
- pushTurnTarget(args, params);
225
- pushOptional(args, "--expected-approval-fingerprint", requiredString(params.expected_approval_fingerprint, "expected_approval_fingerprint"));
226
- const expectedTerminalToken = Object.hasOwn(params, "expected_terminal_token")
227
- ? requiredString(params.expected_terminal_token, "expected_terminal_token")
228
- : undefined;
229
- if (expectedTerminalToken &&
230
- !Object.hasOwn(params, "conversation_id")) {
231
- throw new Error("expected_terminal_token requires the exact list-prefilled conversation_id");
232
- }
233
- pushOptional(args, "--expected-terminal-token", expectedTerminalToken);
234
- pushOptional(args, "--store-dir", resolvePluginStoreDir(config));
235
- return args;
236
- }
302
+ buildArgs: (params, toolContext) => buildPrivateApprovalArgs(api, params, {
303
+ sessionKey: requiredOpenClawSessionKey(toolContext?.sessionKey),
304
+ sessionId: requiredOpenClawSessionId(toolContext?.sessionId)
305
+ })
237
306
  });
238
307
  registerCliTool(api, {
239
308
  name: "agent_knock_knock_renew",
@@ -275,18 +344,38 @@ export function registerOpenClawCommands(api, displayedResumeSnapshots) {
275
344
  });
276
345
  registerCliTool(api, {
277
346
  name: "agent_knock_knock_close",
278
- description: "Close an AKK-managed turn record without terminating the shared terminal pane. For a list-prefilled raw-terminal recovery, the user must explicitly request it and provide the exact expected_message_id or expected_transition_id reported by that AKK list entry. When a verified human native-thread switch conflicts with one active Turn, only the complete nested handoff_decision take_over_current action is authoritative: it requires explicit user confirmation and its exact turn_id, supersede reason, and expected_handoff_token. When list exposes blocking_turns[], its complete nested recovery_action is likewise authoritative only after explicit user confirmation; copy it unchanged and refresh list after the Store-only close.",
347
+ description: "Close one managed Turn record without terminating the shared pane. Ordinary close uses turn_id. Orphan dispatch/lifecycle recovery may additionally use its exact expected_message_id or expected_transition_id. Human-context handoff uses the advertised turn_id plus reason=superseded_by_human_context_switch after explicit confirmation; AKK resolves the current handoff authority privately. Refresh list afterward.",
279
348
  parameters: closeParameters,
280
349
  isErrorResult: isBlockedTerminalDispatchResult,
281
- buildArgs: (params) => {
350
+ buildArgs: (params, toolContext) => {
282
351
  const config = isRecord(api.pluginConfig) ? api.pluginConfig : {};
283
352
  const args = ["close"];
284
353
  assertExclusiveRecoveryFence(params);
354
+ const handoffReason = stringValue(params.reason) ===
355
+ "superseded_by_human_context_switch";
356
+ const handoffAction = handoffReason
357
+ ? consumeDisplayedPrivateAction(api, {
358
+ sessionKey: requiredOpenClawSessionKey(toolContext?.sessionKey),
359
+ sessionId: requiredOpenClawSessionId(toolContext?.sessionId),
360
+ kind: OPENCLAW_HANDOFF_AUTHORITY_KIND,
361
+ target: {
362
+ type: "turn_id",
363
+ id: requiredString(params.turn_id, "turn_id")
364
+ },
365
+ tool: "agent_knock_knock_close",
366
+ matches: (argumentsValue) => stringValue(argumentsValue.turn_id) ===
367
+ stringValue(params.turn_id) &&
368
+ stringValue(argumentsValue.reason) ===
369
+ "superseded_by_human_context_switch"
370
+ })
371
+ : undefined;
285
372
  pushTurnTarget(args, params);
286
373
  pushOptional(args, "--reason", stringValue(params.reason));
287
374
  pushOptional(args, "--expected-message-id", stringValue(params.expected_message_id));
288
375
  pushOptional(args, "--expected-transition-id", stringValue(params.expected_transition_id));
289
- pushOptional(args, "--expected-handoff-token", stringValue(params.expected_handoff_token));
376
+ pushOptional(args, "--expected-handoff-token", handoffAction
377
+ ? requiredString(handoffAction.expected_handoff_token, "current internal handoff authority")
378
+ : undefined);
290
379
  pushOptional(args, "--store-dir", resolvePluginStoreDir(config));
291
380
  return args;
292
381
  }
@@ -354,9 +443,14 @@ async function handleAkkCommand(api, ctx, displayedResumeSnapshots) {
354
443
  parsed.action === "resume-thread") {
355
444
  return await handleAkkLifecycleCommand(api, ctx, parsed, config, displayedResumeSnapshots);
356
445
  }
357
- const args = buildAkkCommandCliArgs(parsed, config, {
358
- sessionKey: ctx.sessionKey
359
- });
446
+ const args = parsed.action === "approve"
447
+ ? buildPrivateApprovalArgs(api, { turn_id: parsed.turnId }, {
448
+ sessionKey: requiredOpenClawSessionKey(ctx.sessionKey),
449
+ sessionId: requiredOpenClawSessionId(ctx.sessionId)
450
+ })
451
+ : buildAkkCommandCliArgs(parsed, config, {
452
+ sessionKey: ctx.sessionKey
453
+ });
360
454
  if (!args) {
361
455
  return { text: akkUsageText(), isError: true };
362
456
  }
@@ -373,6 +467,10 @@ async function handleAkkCommand(api, ctx, displayedResumeSnapshots) {
373
467
  };
374
468
  case "list":
375
469
  return { text: formatAkkListCommandResult(result) };
470
+ case "watch":
471
+ return { text: formatAkkWatchCommandResult(result) };
472
+ case "unwatch":
473
+ return { text: formatAkkUnwatchCommandResult(result) };
376
474
  case "status":
377
475
  return { text: formatStatusCommandResult(result) };
378
476
  case "send":
@@ -400,7 +498,7 @@ async function handleAkkCommand(api, ctx, displayedResumeSnapshots) {
400
498
  }
401
499
  }
402
500
  catch (error) {
403
- const message = error instanceof Error ? error.message : String(error);
501
+ const message = modelFacingErrorMessage(error);
404
502
  return {
405
503
  text: `AKK command failed: ${message}`,
406
504
  isError: true
@@ -412,13 +510,12 @@ async function handleAkkLifecycleCommand(api, ctx, parsed, config, displayedResu
412
510
  const snapshotCacheKey = resumeSnapshotCacheKey(ctx.sessionKey, ctx.sessionId, parsed.terminalId);
413
511
  if (parsed.action === "resume-thread" &&
414
512
  parsed.selection &&
415
- ["number", "short-id", "snapshot-handle"].includes(parsed.selection.kind)) {
513
+ ["number", "short-id"].includes(parsed.selection.kind)) {
514
+ 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");
416
515
  const mutationArgs = buildAkkCommandCliArgs(parsed, config, {
417
516
  sessionKey: ctx.sessionKey,
418
517
  selectionScope,
419
- selectionSnapshotId: parsed.selection.kind === "snapshot-handle"
420
- ? undefined
421
- : currentDisplayedResumeSnapshotId(displayedResumeSnapshots, snapshotCacheKey)
518
+ selectionSnapshotId: currentDisplayedResumeSnapshotId(displayedResumeSnapshots, snapshotCacheKey)
422
519
  });
423
520
  if (!mutationArgs) {
424
521
  throw new Error("could not build snapshot-bound resume command");
@@ -581,6 +678,12 @@ function executorDisplayName(kind) {
581
678
  }
582
679
  }
583
680
  function formatStatusCommandResult(result) {
681
+ if (stringValue(result.watch_id) ||
682
+ (isRecord(result.watch) && stringValue(result.watch.watch_id)) ||
683
+ (isRecord(result.terminal_watch) &&
684
+ stringValue(result.terminal_watch.watch_id))) {
685
+ return formatAkkWatchStatusCommandResult(result);
686
+ }
584
687
  const summary = result.summary ?? result.conversation ?? result ?? {};
585
688
  const terminalStatus = isRecord(result.terminal_status) ? result.terminal_status : {};
586
689
  const rawCallbackDelivery = isRecord(result.conversation?.callback_delivery)
@@ -601,7 +704,9 @@ function formatStatusCommandResult(result) {
601
704
  `session: ${sessionId}`,
602
705
  `turn: ${turnId}`,
603
706
  `agent: ${summary.agent ?? summary.executor?.kind ?? terminalStatus.agent ?? "unknown"}`,
604
- `status: ${summary.status ?? terminalStatus.activity_state ?? "unknown"}`
707
+ `turn status: ${summary.status ?? result.conversation_status ?? result.status ?? "not managed"}`,
708
+ `terminal activity: ${terminalStatus.activity_state ?? "unavailable"}`,
709
+ ...formatAkkTerminalWatchHint(result)
605
710
  ];
606
711
  if (callbackDelivery) {
607
712
  const callbackParts = [
@@ -804,7 +909,9 @@ function formatApproveCommandResult(result) {
804
909
  `session: ${sessionId}`,
805
910
  `turn: ${turnId}`,
806
911
  `status: ${conversation.status ?? "unknown"}`,
807
- ...(result.reason ? [`reason: ${result.reason}`] : [])
912
+ ...(result.reason
913
+ ? [`reason: ${sanitizeAkkModelFacingDiagnosticText(String(result.reason))}`]
914
+ : [])
808
915
  ].join("\n");
809
916
  }
810
917
  function formatCloseCommandResult(result) {
@@ -819,7 +926,8 @@ function formatCloseCommandResult(result) {
819
926
  ...(stringValue(result.transition_id)
820
927
  ? [`transition: ${stringValue(result.transition_id)}`]
821
928
  : []),
822
- `reason: ${stringValue(result.reason) ?? "the recorded lifecycle outcome could not be verified"}`,
929
+ `reason: ${sanitizeAkkModelFacingDiagnosticText(stringValue(result.reason) ??
930
+ "the recorded lifecycle outcome could not be verified")}`,
823
931
  "This terminal remains blocked. Do not retry or continue automatically; inspect the pane and use the fresh recovery action from /akk list."
824
932
  ].join("\n");
825
933
  }
@@ -864,6 +972,20 @@ function publicTurnIdentity(result) {
864
972
  }
865
973
  function buildStatusCliArgs(api, params) {
866
974
  const config = isRecord(api.pluginConfig) ? api.pluginConfig : {};
975
+ if (Object.hasOwn(params, "watch_id")) {
976
+ if (Object.hasOwn(params, "turn_id") ||
977
+ Object.hasOwn(params, "conversation_id")) {
978
+ throw new Error("status accepts exactly one of turn_id, conversation_id, or watch_id");
979
+ }
980
+ const watchId = requiredString(params.watch_id, "watch_id");
981
+ const watchArgs = [
982
+ "watch-status",
983
+ "--watch",
984
+ watchId
985
+ ];
986
+ pushOptional(watchArgs, "--store-dir", resolvePluginStoreDir(config));
987
+ return watchArgs;
988
+ }
867
989
  const args = [
868
990
  "status",
869
991
  "--reconcile"
@@ -900,28 +1022,32 @@ async function runSendRequest(api, params, toolContext, messageId) {
900
1022
  if (requestedType !== "task") {
901
1023
  throw new Error("ordinary send type must be task; use agent_knock_knock_respond for an in-flight response");
902
1024
  }
903
- if (Object.hasOwn(params, "session_id") && Object.hasOwn(params, "selector")) {
904
- throw new Error("ordinary send accepts only one of session_id or selector");
1025
+ if (Object.hasOwn(params, "session_id") && Object.hasOwn(params, "terminal_id")) {
1026
+ throw new Error("ordinary send accepts only one of session_id or terminal_id");
905
1027
  }
906
1028
  const sessionId = Object.hasOwn(params, "session_id")
907
1029
  ? authoritativeManagedId(params.session_id, "session_id")
908
1030
  : undefined;
909
- const selector = Object.hasOwn(params, "selector")
910
- ? requiredString(params.selector, "selector")
1031
+ const terminalId = Object.hasOwn(params, "terminal_id")
1032
+ ? requiredString(params.terminal_id, "terminal_id")
911
1033
  : undefined;
912
- const expectedTerminalToken = Object.hasOwn(params, "expected_terminal_token")
913
- ? requiredString(params.expected_terminal_token, "expected_terminal_token")
914
- : undefined;
915
- if (expectedTerminalToken && !selector) {
916
- throw new Error("expected_terminal_token requires a terminal-scoped selector and cannot be used with session_id");
917
- }
918
- if (expectedTerminalToken &&
919
- !/^terminal:v[0-9]+:/u.test(selector ?? "")) {
920
- throw new Error("expected_terminal_token requires the exact full terminal selector prefilled by AKK list");
1034
+ if (terminalId && !/^terminal:v[0-9]+:\S+$/u.test(terminalId)) {
1035
+ throw new Error("terminal_id must be the exact full terminal identifier returned by AKK list");
921
1036
  }
922
- if (!sessionId && !selector) {
1037
+ if (!sessionId && !terminalId) {
923
1038
  return runDelegate(api, { ...params, messageId }, toolContext);
924
1039
  }
1040
+ const terminalAction = terminalId
1041
+ ? privateActionArguments(api, {
1042
+ tool: "agent_knock_knock_send",
1043
+ terminalId,
1044
+ matches: (argumentsValue) => stringValue(argumentsValue.selector) === terminalId ||
1045
+ stringValue(argumentsValue.terminal_id) === terminalId
1046
+ })
1047
+ : undefined;
1048
+ const expectedTerminalToken = terminalAction
1049
+ ? requiredString(terminalAction.expected_terminal_token, "current internal terminal send authority")
1050
+ : undefined;
925
1051
  const config = isRecord(api.pluginConfig) ? api.pluginConfig : {};
926
1052
  const openclawSession = stringValue(toolContext?.sessionKey) ??
927
1053
  "agent:main:main";
@@ -932,7 +1058,7 @@ async function runSendRequest(api, params, toolContext, messageId) {
932
1058
  args.push("--session", sessionId);
933
1059
  }
934
1060
  else {
935
- args.push("--conversation", requiredString(selector, "selector"));
1061
+ args.push("--conversation", requiredString(terminalId, "terminal_id"));
936
1062
  }
937
1063
  pushOptional(args, "--expected-terminal-token", expectedTerminalToken);
938
1064
  args.push("--message", requiredString(params.request, "request"), "--background");
@@ -953,18 +1079,328 @@ async function runSendRequest(api, params, toolContext, messageId) {
953
1079
  }
954
1080
  function toolResult(result, { submissionErrors = false, normalizeTurnIdentity = true, forceError = false } = {}) {
955
1081
  const normalized = normalizeTurnIdentity ? withTurnIdentity(result) : result;
1082
+ const modelFacing = sanitizeModelFacingValue(normalized);
956
1083
  const submissionError = submissionErrors && isSubmissionError(normalized);
957
1084
  return {
958
1085
  content: [
959
1086
  {
960
1087
  type: "text",
961
- text: JSON.stringify(normalized, null, 2)
1088
+ text: JSON.stringify(modelFacing, null, 2)
962
1089
  }
963
1090
  ],
964
- details: normalized,
1091
+ details: modelFacing,
965
1092
  ...(submissionError || forceError ? { isError: true } : {})
966
1093
  };
967
1094
  }
1095
+ function privateList(api) {
1096
+ const config = isRecord(api.pluginConfig) ? api.pluginConfig : {};
1097
+ const args = ["list", "--reconcile"];
1098
+ pushOptional(args, "--store-dir", resolvePluginStoreDir(config));
1099
+ pushOptional(args, "--idle-timeout-minutes", numberString(config.idleTimeoutMinutes));
1100
+ return runCli(api, args);
1101
+ }
1102
+ function privateThreadDiscovery(api, terminalId) {
1103
+ const config = isRecord(api.pluginConfig) ? api.pluginConfig : {};
1104
+ const args = [
1105
+ "list-resumable-threads",
1106
+ "--terminal",
1107
+ terminalId
1108
+ ];
1109
+ pushOptional(args, "--store-dir", resolvePluginStoreDir(config));
1110
+ pushOptional(args, "--codex-home", stringValue(config.codexHome));
1111
+ return runCli(api, args);
1112
+ }
1113
+ function privateTerminalActionArguments(api, terminalId, tool) {
1114
+ return privateActionArguments(api, {
1115
+ tool,
1116
+ terminalId,
1117
+ matches: (argumentsValue) => stringValue(argumentsValue.terminal_id) === terminalId
1118
+ });
1119
+ }
1120
+ function privateActionArguments(api, input) {
1121
+ const result = privateList(api);
1122
+ const terminals = input.terminalId
1123
+ ? terminalRows(result).filter((terminal) => stringValue(terminal.id) === input.terminalId)
1124
+ : terminalRows(result);
1125
+ if (input.terminalId && terminals.length !== 1) {
1126
+ throw new Error(`terminal ${input.terminalId} is not uniquely present in the current AKK list`);
1127
+ }
1128
+ const candidates = (input.tool === "agent_knock_knock_close"
1129
+ ? authoritativeHandoffActionArguments(terminals)
1130
+ : terminals.flatMap((terminal) => authoritativeTerminalActionArguments(terminal, input.tool))).filter(input.matches);
1131
+ const uniqueCandidates = [...new Map(candidates.map((candidate) => [
1132
+ JSON.stringify(candidate),
1133
+ candidate
1134
+ ])).values()];
1135
+ if (uniqueCandidates.length !== 1) {
1136
+ throw new Error(`the current AKK list does not advertise one exact ${input.tool} action for the requested semantic target`);
1137
+ }
1138
+ return uniqueCandidates[0];
1139
+ }
1140
+ function rememberDisplayedPrivateAuthorityOffers(api, sessionKeyValue, sessionIdValue, result) {
1141
+ const sessionKey = stringValue(sessionKeyValue);
1142
+ const sessionId = stringValue(sessionIdValue);
1143
+ if (!sessionKey || !sessionId || !isRecord(result))
1144
+ return;
1145
+ rememberDisplayedHandoffActions(api, sessionKey, sessionId, result);
1146
+ rememberDisplayedReconcileActions(api, sessionKey, sessionId, result);
1147
+ }
1148
+ function rememberDisplayedHandoffActions(api, sessionKey, sessionId, result) {
1149
+ for (const args of uniqueArgumentObjects(authoritativeHandoffActionArguments(terminalRows(result)))) {
1150
+ const turnId = stringValue(args.turn_id ?? args.conversation_id);
1151
+ if (!turnId ||
1152
+ stringValue(args.reason) !== "superseded_by_human_context_switch" ||
1153
+ !stringValue(args.expected_handoff_token)) {
1154
+ continue;
1155
+ }
1156
+ rememberOpenClawPrivateAuthorityOffer(api, privateAuthorityOfferKey(sessionKey, sessionId, OPENCLAW_HANDOFF_AUTHORITY_KIND, { type: "turn_id", id: turnId }), { args });
1157
+ }
1158
+ }
1159
+ function rememberDisplayedReconcileActions(api, sessionKey, sessionId, result) {
1160
+ for (const args of uniqueArgumentObjects(terminalRows(result).flatMap((terminal) => authoritativeTerminalActionArguments(terminal, "agent_knock_knock_reconcile_binding")))) {
1161
+ const terminalId = stringValue(args.terminal_id);
1162
+ const conflictingSessionId = stringValue(args.conflicting_session_id);
1163
+ if (!terminalId || !conflictingSessionId)
1164
+ continue;
1165
+ rememberOpenClawPrivateAuthorityOffer(api, privateAuthorityOfferKey(sessionKey, sessionId, OPENCLAW_RECONCILE_BINDING_AUTHORITY_KIND, { type: "terminal_id", id: terminalId }), { args });
1166
+ }
1167
+ }
1168
+ function rememberDisplayedApprovalOffer(api, sessionKeyValue, sessionIdValue, result) {
1169
+ const sessionKey = stringValue(sessionKeyValue);
1170
+ const sessionId = stringValue(sessionIdValue);
1171
+ if (!sessionKey || !sessionId || !isRecord(result))
1172
+ return;
1173
+ const fingerprints = currentApprovalFingerprints(result);
1174
+ if (fingerprints.length !== 1)
1175
+ return;
1176
+ const target = approvalTargetFromStatus(result);
1177
+ if (!target)
1178
+ return;
1179
+ rememberOpenClawPrivateAuthorityOffer(api, openClawApprovalAuthorityOfferKey(sessionKey, sessionId, target), { fingerprint: fingerprints[0] });
1180
+ }
1181
+ function approvalTargetFromStatus(result) {
1182
+ const conversationId = stringValue(result.conversation_id);
1183
+ if (stringValue(result.source) === "terminal_control" ||
1184
+ conversationId?.startsWith("terminal:")) {
1185
+ return conversationId
1186
+ ? { type: "terminal_id", id: conversationId }
1187
+ : undefined;
1188
+ }
1189
+ const { turnId } = publicTurnIdentity(result);
1190
+ return turnId && turnId !== "unknown"
1191
+ ? { type: "turn_id", id: turnId }
1192
+ : undefined;
1193
+ }
1194
+ function uniqueArgumentObjects(candidates) {
1195
+ return [...new Map(candidates.map((args) => [
1196
+ JSON.stringify(args),
1197
+ args
1198
+ ])).values()];
1199
+ }
1200
+ function privateAuthorityOfferKey(sessionKey, sessionId, kind, target) {
1201
+ return { sessionKey, sessionId, kind, target };
1202
+ }
1203
+ function consumeDisplayedPrivateAction(api, input) {
1204
+ const offered = consumeOpenClawPrivateAuthorityOffer(api, {
1205
+ sessionKey: input.sessionKey,
1206
+ sessionId: input.sessionId,
1207
+ kind: input.kind,
1208
+ target: input.target
1209
+ });
1210
+ if (!offered || !isRecord(offered.args)) {
1211
+ 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`);
1212
+ }
1213
+ const current = privateActionArguments(api, input);
1214
+ if (JSON.stringify(current) !== JSON.stringify(offered.args)) {
1215
+ throw new Error(`${input.tool} authority changed after it was shown; refresh AKK list, review the current action, and explicitly confirm again`);
1216
+ }
1217
+ return current;
1218
+ }
1219
+ function terminalRows(result) {
1220
+ return Array.isArray(result.terminals)
1221
+ ? result.terminals.filter(isRecord)
1222
+ : [];
1223
+ }
1224
+ function authoritativeTerminalActionArguments(terminal, tool) {
1225
+ const availableActions = isRecord(terminal.available_actions)
1226
+ ? terminal.available_actions
1227
+ : undefined;
1228
+ if (!availableActions)
1229
+ return [];
1230
+ return Object.values(availableActions).flatMap((action) => isRecord(action) && action.tool === tool && isRecord(action.arguments)
1231
+ ? [action.arguments]
1232
+ : []);
1233
+ }
1234
+ function authoritativeHandoffActionArguments(terminals) {
1235
+ return terminals.flatMap((terminal) => {
1236
+ const decision = isRecord(terminal.handoff_decision)
1237
+ ? terminal.handoff_decision
1238
+ : undefined;
1239
+ const choices = isRecord(decision?.choices) ? decision.choices : undefined;
1240
+ const takeOver = isRecord(choices?.take_over_current)
1241
+ ? choices.take_over_current
1242
+ : undefined;
1243
+ const action = isRecord(takeOver?.action) ? takeOver.action : undefined;
1244
+ return decision?.kind === "active_turn_requires_decision" &&
1245
+ action?.tool === "agent_knock_knock_close" &&
1246
+ action.requires_explicit_user_confirmation === true &&
1247
+ isRecord(action.arguments)
1248
+ ? [action.arguments]
1249
+ : [];
1250
+ });
1251
+ }
1252
+ function currentApprovalFingerprint(result) {
1253
+ const unique = isRecord(result) ? currentApprovalFingerprints(result) : [];
1254
+ if (unique.length !== 1) {
1255
+ throw new Error("the current status does not contain one exact approvable prompt; refresh status and ask the user to review it again");
1256
+ }
1257
+ return unique[0];
1258
+ }
1259
+ function buildPrivateApprovalArgs(api, params, { sessionKey, sessionId }) {
1260
+ const config = isRecord(api.pluginConfig) ? api.pluginConfig : {};
1261
+ const turnId = stringValue(params.turn_id);
1262
+ const terminalId = stringValue(params.terminal_id);
1263
+ if (Boolean(turnId) === Boolean(terminalId)) {
1264
+ throw new Error("approve requires exactly one of turn_id or terminal_id");
1265
+ }
1266
+ const target = terminalId
1267
+ ? { type: "terminal_id", id: terminalId }
1268
+ : { type: "turn_id", id: requiredString(turnId, "turn_id") };
1269
+ const offered = consumeOpenClawPrivateAuthorityOffer(api, openClawApprovalAuthorityOfferKey(sessionKey, sessionId, target));
1270
+ const offeredFingerprint = stringValue(offered?.fingerprint) ??
1271
+ (isRecord(offered?.args)
1272
+ ? stringValue(offered.args.expected_approval_fingerprint)
1273
+ : undefined);
1274
+ if (!isExactApprovalFingerprint(offeredFingerprint)) {
1275
+ 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");
1276
+ }
1277
+ const action = terminalId
1278
+ ? privateActionArguments(api, {
1279
+ tool: "agent_knock_knock_approve",
1280
+ terminalId,
1281
+ matches: (argumentsValue) => stringValue(argumentsValue.terminal_id ?? argumentsValue.conversation_id) === terminalId
1282
+ })
1283
+ : undefined;
1284
+ const statusArgs = ["status", "--reconcile"];
1285
+ if (terminalId) {
1286
+ statusArgs.push("--conversation", terminalId);
1287
+ }
1288
+ else {
1289
+ statusArgs.push("--turn", requiredString(turnId, "turn_id"));
1290
+ }
1291
+ pushOptional(statusArgs, "--store-dir", resolvePluginStoreDir(config));
1292
+ const args = ["approve"];
1293
+ if (terminalId) {
1294
+ args.push("--conversation", terminalId);
1295
+ }
1296
+ else {
1297
+ args.push("--turn", requiredString(turnId, "turn_id"));
1298
+ }
1299
+ const currentFingerprint = currentApprovalFingerprint(runCli(api, statusArgs));
1300
+ if (currentFingerprint !== offeredFingerprint) {
1301
+ 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");
1302
+ }
1303
+ args.push("--expected-approval-fingerprint", currentFingerprint);
1304
+ if (terminalId) {
1305
+ args.push("--expected-terminal-token", requiredString(action?.expected_terminal_token, "current internal terminal approval authority"));
1306
+ }
1307
+ pushOptional(args, "--store-dir", resolvePluginStoreDir(config));
1308
+ return args;
1309
+ }
1310
+ function isExactApprovalFingerprint(value) {
1311
+ return typeof value === "string" && /^[a-f0-9]{64}$/u.test(value);
1312
+ }
1313
+ function currentApprovalFingerprints(result) {
1314
+ const terminalStatus = isRecord(result.terminal_status)
1315
+ ? result.terminal_status
1316
+ : undefined;
1317
+ const states = [
1318
+ isRecord(result.approval_state) ? result.approval_state : undefined,
1319
+ isRecord(terminalStatus?.approval_state)
1320
+ ? terminalStatus.approval_state
1321
+ : undefined
1322
+ ];
1323
+ return [...new Set(states.flatMap((state) => {
1324
+ const fingerprint = state?.approvable === true
1325
+ ? stringValue(state.fingerprint)
1326
+ : undefined;
1327
+ return fingerprint && /^[a-f0-9]{64}$/u.test(fingerprint)
1328
+ ? [fingerprint]
1329
+ : [];
1330
+ }))];
1331
+ }
1332
+ function sanitizeModelFacingValue(value, parentKey, path = []) {
1333
+ if (Array.isArray(value)) {
1334
+ if (parentKey === "missing_required") {
1335
+ return value.filter((item) => typeof item !== "string" ||
1336
+ !isAkkModelFacingPrivateAuthorityField(item));
1337
+ }
1338
+ return value.map((item, index) => sanitizeModelFacingValue(item, parentKey, [...path, String(index)]));
1339
+ }
1340
+ if (!isRecord(value)) {
1341
+ if (typeof value !== "string") {
1342
+ return value;
1343
+ }
1344
+ if (isAkkModelFacingDiagnosticField(parentKey)) {
1345
+ return sanitizeAkkModelFacingDiagnosticText(value);
1346
+ }
1347
+ return isLegacyAuthorityInstructionPath(path)
1348
+ ? sanitizeAkkModelFacingLegacyAuthorityInstructionText(value)
1349
+ : value;
1350
+ }
1351
+ const output = {};
1352
+ for (const [key, item] of Object.entries(value)) {
1353
+ if (isAkkModelFacingPrivateAuthorityField(key) ||
1354
+ key === "selection_snapshot" ||
1355
+ key === "live_native_thread_id" ||
1356
+ (key === "fingerprint" && parentKey === "approval_state")) {
1357
+ continue;
1358
+ }
1359
+ output[key] = sanitizeModelFacingValue(item, key, [...path, key]);
1360
+ }
1361
+ if (output.tool === "agent_knock_knock_send" && isRecord(output.arguments)) {
1362
+ const selector = stringValue(output.arguments.selector);
1363
+ if (selector && /^terminal:v[0-9]+:\S+$/u.test(selector)) {
1364
+ const { selector: _selector, ...argumentsValue } = output.arguments;
1365
+ output.arguments = { ...argumentsValue, terminal_id: selector };
1366
+ }
1367
+ }
1368
+ if (output.tool === "agent_knock_knock_approve" && isRecord(output.arguments)) {
1369
+ const conversationId = stringValue(output.arguments.conversation_id);
1370
+ if (conversationId && /^terminal:v[0-9]+:\S+$/u.test(conversationId)) {
1371
+ const { conversation_id: _conversationId, ...argumentsValue } = output.arguments;
1372
+ output.arguments = { ...argumentsValue, terminal_id: conversationId };
1373
+ }
1374
+ const approvalArguments = isRecord(output.arguments)
1375
+ ? output.arguments
1376
+ : {};
1377
+ const terminalId = stringValue(approvalArguments.terminal_id);
1378
+ output.before_call = {
1379
+ tool: "agent_knock_knock_status",
1380
+ arguments: terminalId
1381
+ ? { conversation_id: terminalId }
1382
+ : { ...approvalArguments },
1383
+ use: "Show the current approval request to the user. After explicit confirmation, call approve with only this semantic target; AKK revalidates the prompt privately."
1384
+ };
1385
+ delete output.missing_required;
1386
+ }
1387
+ return output;
1388
+ }
1389
+ function isLegacyAuthorityInstructionPath(path) {
1390
+ const normalized = path.map(normalizeAkkModelFacingFieldName);
1391
+ if (normalized.at(-1) !== "body")
1392
+ return false;
1393
+ const callbackMessageBody = normalized.slice(-3).join(".") ===
1394
+ "callbackdelivery.message.body";
1395
+ return callbackMessageBody || normalized.includes("recentevents");
1396
+ }
1397
+ function modelFacingToolError(error) {
1398
+ return new Error(modelFacingErrorMessage(error));
1399
+ }
1400
+ function modelFacingErrorMessage(error) {
1401
+ const message = error instanceof Error ? error.message : String(error);
1402
+ return sanitizeAkkModelFacingDiagnosticText(message);
1403
+ }
968
1404
  function isBlockedTerminalDispatchResult(result) {
969
1405
  return Boolean(isRecord(result) &&
970
1406
  result.source === "terminal_control" &&
@@ -1235,12 +1671,21 @@ function registerCliTool(api, { name, description, parameters, buildArgs, normal
1235
1671
  description,
1236
1672
  parameters,
1237
1673
  async execute(toolCallId, params) {
1238
- const result = runCli(api, buildArgs(isRecord(params) ? params : {}, toolContext, toolCallId));
1239
- return toolResult(result, {
1240
- submissionErrors: name === "agent_knock_knock_respond",
1241
- normalizeTurnIdentity,
1242
- forceError: typeof isErrorResult === "function" && isErrorResult(result) === true
1243
- });
1674
+ try {
1675
+ const result = runCli(api, buildArgs(isRecord(params) ? params : {}, toolContext, toolCallId));
1676
+ const rendered = toolResult(result, {
1677
+ submissionErrors: name === "agent_knock_knock_respond",
1678
+ normalizeTurnIdentity,
1679
+ forceError: typeof isErrorResult === "function" && isErrorResult(result) === true
1680
+ });
1681
+ if (name === "agent_knock_knock_list") {
1682
+ rememberDisplayedPrivateAuthorityOffers(api, toolContext?.sessionKey, toolContext?.sessionId, result);
1683
+ }
1684
+ return rendered;
1685
+ }
1686
+ catch (error) {
1687
+ throw modelFacingToolError(error);
1688
+ }
1244
1689
  }
1245
1690
  }), { name, optional: true });
1246
1691
  }
@@ -1369,27 +1814,17 @@ function authoritativeManagedId(value, name) {
1369
1814
  function assertExclusiveRecoveryFence(params) {
1370
1815
  const expectedMessageId = stringValue(params.expected_message_id);
1371
1816
  const expectedTransitionId = stringValue(params.expected_transition_id);
1372
- const expectedHandoffToken = stringValue(params.expected_handoff_token);
1373
- const closeFenceCount = [
1374
- expectedMessageId,
1375
- expectedTransitionId,
1376
- expectedHandoffToken
1377
- ].filter(Boolean).length;
1817
+ const closeFenceCount = [expectedMessageId, expectedTransitionId]
1818
+ .filter(Boolean).length;
1378
1819
  if (closeFenceCount > 1) {
1379
- throw new Error("close accepts only one of expected_message_id, expected_transition_id, or expected_handoff_token");
1820
+ throw new Error("close accepts only one of expected_message_id or expected_transition_id");
1380
1821
  }
1381
1822
  const handoffReason = stringValue(params.reason) ===
1382
1823
  "superseded_by_human_context_switch";
1383
- if (expectedHandoffToken) {
1384
- if (!stringValue(params.turn_id) || stringValue(params.conversation_id)) {
1385
- throw new Error("expected_handoff_token requires the exact managed turn_id from the advertised handoff_decision action");
1386
- }
1387
- if (!handoffReason) {
1388
- throw new Error("expected_handoff_token requires reason=superseded_by_human_context_switch");
1389
- }
1390
- }
1391
- else if (handoffReason) {
1392
- throw new Error("reason=superseded_by_human_context_switch requires expected_handoff_token from the advertised handoff_decision action");
1824
+ if (handoffReason && (!stringValue(params.turn_id) ||
1825
+ stringValue(params.conversation_id) ||
1826
+ closeFenceCount > 0)) {
1827
+ throw new Error("human-context handoff requires only the exact managed turn_id and supersede reason");
1393
1828
  }
1394
1829
  }
1395
1830
  export function pushOptional(args, flag, value) {
@@ -1413,6 +1848,12 @@ function requiredString(value, name) {
1413
1848
  }
1414
1849
  return value;
1415
1850
  }
1851
+ function requiredOpenClawSessionKey(value) {
1852
+ return requiredString(value, "OpenClaw session identity for this confirmed action");
1853
+ }
1854
+ function requiredOpenClawSessionId(value) {
1855
+ return requiredString(value, "OpenClaw conversation incarnation for this confirmed action");
1856
+ }
1416
1857
  function parseJson(text) {
1417
1858
  try {
1418
1859
  return JSON.parse(text);