@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.
- package/CHANGELOG.md +22 -0
- package/README.md +39 -26
- package/dist/src/approval-policy.d.ts +11 -2
- package/dist/src/approval-policy.js +33 -7
- package/dist/src/approval-policy.js.map +1 -1
- package/dist/src/claude-local-transcript-provider.d.ts +81 -0
- package/dist/src/claude-local-transcript-provider.js +854 -18
- package/dist/src/claude-local-transcript-provider.js.map +1 -1
- package/dist/src/cli-core.js +85 -0
- package/dist/src/cli-core.js.map +1 -1
- package/dist/src/deferred-foreground-authority-cli-adapter.js +90 -19
- package/dist/src/deferred-foreground-authority-cli-adapter.js.map +1 -1
- package/dist/src/deferred-foreground-transfer.js +3 -4
- package/dist/src/deferred-foreground-transfer.js.map +1 -1
- package/dist/src/openclaw-callback-transport.d.ts +5 -0
- package/dist/src/openclaw-callback-transport.js +1 -1
- package/dist/src/openclaw-callback-transport.js.map +1 -1
- package/dist/src/openclaw-plugin-callback-adapter.js +96 -14
- package/dist/src/openclaw-plugin-callback-adapter.js.map +1 -1
- package/dist/src/openclaw-plugin-command-adapter.js +548 -107
- package/dist/src/openclaw-plugin-command-adapter.js.map +1 -1
- package/dist/src/openclaw-plugin-helpers.d.ts +27 -4
- package/dist/src/openclaw-plugin-helpers.js +309 -44
- package/dist/src/openclaw-plugin-helpers.js.map +1 -1
- package/dist/src/openclaw-plugin-schemas.d.ts +44 -68
- package/dist/src/openclaw-plugin-schemas.js +63 -91
- package/dist/src/openclaw-plugin-schemas.js.map +1 -1
- package/dist/src/openclaw-plugin-supervisor.js +48 -10
- package/dist/src/openclaw-plugin-supervisor.js.map +1 -1
- package/dist/src/openclaw-private-authority-offers.d.ts +22 -0
- package/dist/src/openclaw-private-authority-offers.js +177 -0
- package/dist/src/openclaw-private-authority-offers.js.map +1 -0
- package/dist/src/store.d.ts +1 -0
- package/dist/src/store.js +3 -1
- package/dist/src/store.js.map +1 -1
- package/dist/src/terminal-action-projection.d.ts +19 -1
- package/dist/src/terminal-action-projection.js +23 -0
- package/dist/src/terminal-action-projection.js.map +1 -1
- package/dist/src/terminal-command-cli-adapter.d.ts +5 -0
- package/dist/src/terminal-command-cli-adapter.js +249 -89
- package/dist/src/terminal-command-cli-adapter.js.map +1 -1
- package/dist/src/terminal-dispatch-repository-cli-adapter.js +305 -79
- package/dist/src/terminal-dispatch-repository-cli-adapter.js.map +1 -1
- package/dist/src/terminal-list-cli-adapter.d.ts +31 -7
- package/dist/src/terminal-list-cli-adapter.js +285 -67
- package/dist/src/terminal-list-cli-adapter.js.map +1 -1
- package/dist/src/terminal-list-renderer.d.ts +2 -0
- package/dist/src/terminal-list-renderer.js +116 -41
- package/dist/src/terminal-list-renderer.js.map +1 -1
- package/dist/src/terminal-monitor-cli-adapter.js +1 -1
- package/dist/src/terminal-monitor-cli-adapter.js.map +1 -1
- package/dist/src/terminal-monitor-state-cli-adapter.js +6 -15
- package/dist/src/terminal-monitor-state-cli-adapter.js.map +1 -1
- package/dist/src/terminal-status-cli-adapter.d.ts +10 -0
- package/dist/src/terminal-status-cli-adapter.js +89 -7
- package/dist/src/terminal-status-cli-adapter.js.map +1 -1
- package/dist/src/terminal-submission-acceptance.d.ts +60 -1
- package/dist/src/terminal-submission-acceptance.js +560 -0
- package/dist/src/terminal-submission-acceptance.js.map +1 -1
- package/dist/src/terminal-watch-callback-cli-adapter.d.ts +20 -0
- package/dist/src/terminal-watch-callback-cli-adapter.js +55 -0
- package/dist/src/terminal-watch-callback-cli-adapter.js.map +1 -0
- package/dist/src/terminal-watch-cli-adapter.d.ts +64 -0
- package/dist/src/terminal-watch-cli-adapter.js +508 -0
- package/dist/src/terminal-watch-cli-adapter.js.map +1 -0
- package/dist/src/terminal-watch-service.d.ts +97 -0
- package/dist/src/terminal-watch-service.js +714 -0
- package/dist/src/terminal-watch-service.js.map +1 -0
- package/dist/src/terminal-watch-store.d.ts +149 -0
- package/dist/src/terminal-watch-store.js +861 -0
- package/dist/src/terminal-watch-store.js.map +1 -0
- package/docs/quickstart-herdr.md +8 -0
- package/docs/quickstart-tmux.md +33 -6
- package/openclaw.plugin.json +11 -3
- package/package.json +1 -1
- 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
|
|
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.
|
|
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
|
|
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.
|
|
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
|
|
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
|
-
|
|
126
|
+
terminalId,
|
|
84
127
|
"--inspection",
|
|
85
128
|
inspection,
|
|
86
129
|
"--expected-binding-token",
|
|
87
|
-
requiredString(
|
|
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
|
|
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
|
-
|
|
150
|
+
terminalId,
|
|
106
151
|
"--expected-binding-token",
|
|
107
|
-
requiredString(
|
|
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.
|
|
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
|
|
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
|
-
|
|
186
|
+
terminalId,
|
|
129
187
|
"--conflicting-session",
|
|
130
|
-
requiredString(
|
|
188
|
+
requiredString(conflictingSessionId, "conflicting_session_id"),
|
|
131
189
|
"--expected-session-revision",
|
|
132
190
|
String(revision),
|
|
133
191
|
"--expected-binding-token",
|
|
134
|
-
requiredString(
|
|
192
|
+
requiredString(action.expected_binding_token, "current internal conflicting binding authority"),
|
|
135
193
|
"--expected-terminal-token",
|
|
136
|
-
requiredString(
|
|
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
|
|
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
|
-
|
|
223
|
+
terminalId,
|
|
155
224
|
"--native-thread",
|
|
156
|
-
|
|
225
|
+
nativeThreadId,
|
|
157
226
|
"--expected-binding-token",
|
|
158
|
-
requiredString(
|
|
227
|
+
requiredString(discovery.expected_binding_token, "current internal lifecycle binding authority"),
|
|
159
228
|
"--candidate-token",
|
|
160
|
-
requiredString(
|
|
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((
|
|
236
|
+
api.registerTool((toolContext) => ({
|
|
168
237
|
label: "AKK Status",
|
|
169
238
|
name: "agent_knock_knock_status",
|
|
170
|
-
description: "Inspect one exact AKK-managed
|
|
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
|
-
|
|
174
|
-
|
|
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.
|
|
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
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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
|
|
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
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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
|
|
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",
|
|
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 =
|
|
358
|
-
|
|
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 =
|
|
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"
|
|
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:
|
|
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 ??
|
|
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
|
|
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) ??
|
|
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, "
|
|
904
|
-
throw new Error("ordinary send accepts only one of session_id or
|
|
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
|
|
910
|
-
? requiredString(params.
|
|
1031
|
+
const terminalId = Object.hasOwn(params, "terminal_id")
|
|
1032
|
+
? requiredString(params.terminal_id, "terminal_id")
|
|
911
1033
|
: undefined;
|
|
912
|
-
|
|
913
|
-
|
|
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 && !
|
|
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(
|
|
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(
|
|
1088
|
+
text: JSON.stringify(modelFacing, null, 2)
|
|
962
1089
|
}
|
|
963
1090
|
],
|
|
964
|
-
details:
|
|
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
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
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
|
|
1373
|
-
|
|
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
|
|
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 (
|
|
1384
|
-
|
|
1385
|
-
|
|
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);
|