@scotthuang/agent-knock-knock 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +27 -0
- package/README.md +33 -15
- package/dist/src/agent-session-provider.d.ts +13 -0
- package/dist/src/cli.js +1178 -177
- package/dist/src/cli.js.map +1 -1
- package/dist/src/codex-local-session-provider.d.ts +3 -1
- package/dist/src/codex-local-session-provider.js +17 -0
- package/dist/src/codex-local-session-provider.js.map +1 -1
- package/dist/src/codex-store-adapter.d.ts +17 -0
- package/dist/src/codex-store-adapter.js +206 -0
- package/dist/src/codex-store-adapter.js.map +1 -1
- package/dist/src/openclaw-plugin-helpers.d.ts +15 -7
- package/dist/src/openclaw-plugin-helpers.js +162 -40
- package/dist/src/openclaw-plugin-helpers.js.map +1 -1
- package/dist/src/openclaw-plugin.js +591 -136
- package/dist/src/openclaw-plugin.js.map +1 -1
- package/dist/src/protocol.d.ts +22 -1
- package/dist/src/protocol.js +93 -4
- package/dist/src/protocol.js.map +1 -1
- package/dist/src/store.d.ts +4 -3
- package/dist/src/store.js +203 -10
- package/dist/src/store.js.map +1 -1
- package/dist/src/terminal-agent-adapter.d.ts +22 -0
- package/dist/src/terminal-agent-adapter.js.map +1 -1
- package/dist/src/terminal-agent-bridge.d.ts +1 -0
- package/dist/src/terminal-agent-bridge.js +2 -1
- package/dist/src/terminal-agent-bridge.js.map +1 -1
- package/docs/quickstart-tmux.md +12 -2
- package/openclaw.plugin.json +5 -1
- package/package.json +1 -1
- package/templates/openclaw-skills/agent-knock-knock/SKILL.md +23 -16
|
@@ -2,7 +2,7 @@ import { spawn, spawnSync } from "node:child_process";
|
|
|
2
2
|
import { fileURLToPath } from "node:url";
|
|
3
3
|
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
|
|
4
4
|
import { EXECUTOR_KINDS, executorDefinitionForKind } from "./executors.js";
|
|
5
|
-
import { AKK_CALLBACK_METHOD, akkUsageText, buildAkkCommandCliArgs, formatAkkListCommandResult, parseAkkCommand, resolvePluginStoreDir } from "./openclaw-plugin-helpers.js";
|
|
5
|
+
import { AKK_CALLBACK_METHOD, akkUsageText, buildAkkCommandCliArgs, formatAkkListCommandResult, formatAkkRespondCommandResult, parseAkkCommand, resolvePluginStoreDir } from "./openclaw-plugin-helpers.js";
|
|
6
6
|
import { attemptAutoApproval } from "./approval-policy.js";
|
|
7
7
|
const CALLBACK_METHOD = AKK_CALLBACK_METHOD;
|
|
8
8
|
const defaultBinPath = fileURLToPath(new URL("./cli.js", import.meta.url));
|
|
@@ -11,22 +11,31 @@ const sendParameters = {
|
|
|
11
11
|
type: "object",
|
|
12
12
|
additionalProperties: false,
|
|
13
13
|
required: ["request"],
|
|
14
|
+
not: { required: ["session_id", "selector"] },
|
|
14
15
|
properties: {
|
|
16
|
+
session_id: {
|
|
17
|
+
type: "string",
|
|
18
|
+
minLength: 1,
|
|
19
|
+
description: "Authoritative AKK session id returned by list or a previous send. Ordinary sends target a session and create a new turn; a turn id is never a send destination."
|
|
20
|
+
},
|
|
15
21
|
selector: {
|
|
16
22
|
type: "string",
|
|
17
|
-
|
|
23
|
+
minLength: 1,
|
|
24
|
+
description: "Compatibility/discovery selector: codex, claude, only, latest, an @short-ref, or a live terminal id. Prefer session_id once a session exists. Omit both fields only when AKK should attach the unique eligible idle pane."
|
|
18
25
|
},
|
|
19
26
|
request: {
|
|
20
27
|
type: "string",
|
|
21
|
-
|
|
28
|
+
minLength: 1,
|
|
29
|
+
description: "Message for the coding agent. Each accepted ordinary send creates a new turn inside the selected session without clearing native agent context."
|
|
22
30
|
},
|
|
23
31
|
type: {
|
|
24
32
|
type: "string",
|
|
25
|
-
enum: ["
|
|
33
|
+
enum: ["task"],
|
|
34
|
+
description: "Ordinary sends always create a task turn. Use agent_knock_knock_respond for an answer to an in-flight turn."
|
|
26
35
|
},
|
|
27
36
|
idleTimeoutMinutes: {
|
|
28
37
|
type: "number",
|
|
29
|
-
description: "Minutes an idle AKK
|
|
38
|
+
description: "Minutes an idle or completed AKK Turn record is retained before controlled reconciliation closes it."
|
|
30
39
|
},
|
|
31
40
|
agentTimeoutMinutes: {
|
|
32
41
|
type: "number",
|
|
@@ -39,6 +48,21 @@ const sendParameters = {
|
|
|
39
48
|
}
|
|
40
49
|
}
|
|
41
50
|
};
|
|
51
|
+
const respondParameters = {
|
|
52
|
+
type: "object",
|
|
53
|
+
additionalProperties: false,
|
|
54
|
+
required: ["turn_id", "request"],
|
|
55
|
+
properties: {
|
|
56
|
+
turn_id: {
|
|
57
|
+
type: "string",
|
|
58
|
+
description: "Authoritative AKK turn id from a question or blocked callback. A response continues this exact in-flight turn and does not create a new turn."
|
|
59
|
+
},
|
|
60
|
+
request: {
|
|
61
|
+
type: "string",
|
|
62
|
+
description: "Answer or decision for the coding agent's exact in-flight turn."
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
};
|
|
42
66
|
const listParameters = {
|
|
43
67
|
type: "object",
|
|
44
68
|
additionalProperties: false,
|
|
@@ -69,11 +93,20 @@ const listParameters = {
|
|
|
69
93
|
const renewParameters = {
|
|
70
94
|
type: "object",
|
|
71
95
|
additionalProperties: false,
|
|
72
|
-
required: ["conversation_id"],
|
|
96
|
+
not: { required: ["turn_id", "conversation_id"] },
|
|
97
|
+
anyOf: [
|
|
98
|
+
{ required: ["turn_id"] },
|
|
99
|
+
{ required: ["conversation_id"] }
|
|
100
|
+
],
|
|
73
101
|
properties: {
|
|
102
|
+
turn_id: {
|
|
103
|
+
type: "string",
|
|
104
|
+
description: "Authoritative AKK turn id whose monitoring should be renewed."
|
|
105
|
+
},
|
|
74
106
|
conversation_id: {
|
|
75
107
|
type: "string",
|
|
76
|
-
|
|
108
|
+
deprecated: true,
|
|
109
|
+
description: "Deprecated compatibility alias for turn_id."
|
|
77
110
|
},
|
|
78
111
|
minutes: {
|
|
79
112
|
type: "number",
|
|
@@ -85,22 +118,40 @@ const renewParameters = {
|
|
|
85
118
|
const retryCallbackParameters = {
|
|
86
119
|
type: "object",
|
|
87
120
|
additionalProperties: false,
|
|
88
|
-
required: ["conversation_id"],
|
|
121
|
+
not: { required: ["turn_id", "conversation_id"] },
|
|
122
|
+
anyOf: [
|
|
123
|
+
{ required: ["turn_id"] },
|
|
124
|
+
{ required: ["conversation_id"] }
|
|
125
|
+
],
|
|
89
126
|
properties: {
|
|
127
|
+
turn_id: {
|
|
128
|
+
type: "string",
|
|
129
|
+
description: "Authoritative AKK turn id whose persisted callback should be retried."
|
|
130
|
+
},
|
|
90
131
|
conversation_id: {
|
|
91
132
|
type: "string",
|
|
92
|
-
|
|
133
|
+
deprecated: true,
|
|
134
|
+
description: "Deprecated compatibility alias for turn_id."
|
|
93
135
|
}
|
|
94
136
|
}
|
|
95
137
|
};
|
|
96
138
|
const statusParameters = {
|
|
97
139
|
type: "object",
|
|
98
140
|
additionalProperties: false,
|
|
99
|
-
required: ["conversation_id"],
|
|
141
|
+
not: { required: ["turn_id", "conversation_id"] },
|
|
142
|
+
anyOf: [
|
|
143
|
+
{ required: ["turn_id"] },
|
|
144
|
+
{ required: ["conversation_id"] }
|
|
145
|
+
],
|
|
100
146
|
properties: {
|
|
147
|
+
turn_id: {
|
|
148
|
+
type: "string",
|
|
149
|
+
description: "Authoritative AKK turn id to inspect."
|
|
150
|
+
},
|
|
101
151
|
conversation_id: {
|
|
102
152
|
type: "string",
|
|
103
|
-
|
|
153
|
+
deprecated: true,
|
|
154
|
+
description: "Deprecated legacy Turn alias, or the exact raw-terminal selector prefilled by that terminal row's available status action. Managed Turn status must use turn_id; never construct or guess a raw-terminal selector."
|
|
104
155
|
},
|
|
105
156
|
idleTimeoutMinutes: {
|
|
106
157
|
type: "number"
|
|
@@ -114,11 +165,20 @@ const statusParameters = {
|
|
|
114
165
|
const cancelParameters = {
|
|
115
166
|
type: "object",
|
|
116
167
|
additionalProperties: false,
|
|
117
|
-
required: ["conversation_id"],
|
|
168
|
+
not: { required: ["turn_id", "conversation_id"] },
|
|
169
|
+
anyOf: [
|
|
170
|
+
{ required: ["turn_id"] },
|
|
171
|
+
{ required: ["conversation_id"] }
|
|
172
|
+
],
|
|
118
173
|
properties: {
|
|
174
|
+
turn_id: {
|
|
175
|
+
type: "string",
|
|
176
|
+
description: "Authoritative AKK turn id to interrupt."
|
|
177
|
+
},
|
|
119
178
|
conversation_id: {
|
|
120
179
|
type: "string",
|
|
121
|
-
|
|
180
|
+
deprecated: true,
|
|
181
|
+
description: "Deprecated legacy Turn alias, or the exact raw-terminal selector prefilled by that terminal row's available cancel action. Managed Turn cancellation must use turn_id; never construct or guess a raw-terminal selector."
|
|
122
182
|
},
|
|
123
183
|
idleTimeoutMinutes: {
|
|
124
184
|
type: "number"
|
|
@@ -128,10 +188,20 @@ const cancelParameters = {
|
|
|
128
188
|
const closeParameters = {
|
|
129
189
|
type: "object",
|
|
130
190
|
additionalProperties: false,
|
|
131
|
-
required: ["conversation_id"],
|
|
191
|
+
not: { required: ["turn_id", "conversation_id"] },
|
|
192
|
+
anyOf: [
|
|
193
|
+
{ required: ["turn_id"] },
|
|
194
|
+
{ required: ["conversation_id"] }
|
|
195
|
+
],
|
|
132
196
|
properties: {
|
|
197
|
+
turn_id: {
|
|
198
|
+
type: "string",
|
|
199
|
+
description: "Authoritative AKK turn id whose managed record should be closed."
|
|
200
|
+
},
|
|
133
201
|
conversation_id: {
|
|
134
|
-
type: "string"
|
|
202
|
+
type: "string",
|
|
203
|
+
deprecated: true,
|
|
204
|
+
description: "Deprecated legacy Turn alias, or an exact list-prefilled raw-terminal/orphan recovery selector. Managed Turn close must use turn_id; never construct or guess a raw-terminal selector."
|
|
135
205
|
},
|
|
136
206
|
reason: {
|
|
137
207
|
type: "string"
|
|
@@ -145,11 +215,21 @@ const closeParameters = {
|
|
|
145
215
|
const approveParameters = {
|
|
146
216
|
type: "object",
|
|
147
217
|
additionalProperties: false,
|
|
148
|
-
required: ["
|
|
218
|
+
required: ["expected_approval_fingerprint"],
|
|
219
|
+
not: { required: ["turn_id", "conversation_id"] },
|
|
220
|
+
anyOf: [
|
|
221
|
+
{ required: ["turn_id"] },
|
|
222
|
+
{ required: ["conversation_id"] }
|
|
223
|
+
],
|
|
149
224
|
properties: {
|
|
225
|
+
turn_id: {
|
|
226
|
+
type: "string",
|
|
227
|
+
description: "Authoritative AKK turn id containing the approval prompt."
|
|
228
|
+
},
|
|
150
229
|
conversation_id: {
|
|
151
230
|
type: "string",
|
|
152
|
-
|
|
231
|
+
deprecated: true,
|
|
232
|
+
description: "Deprecated legacy Turn alias, or the exact raw-terminal selector prefilled by that terminal row's available approval action. Managed Turn approval must use turn_id; never construct or guess a raw-terminal selector."
|
|
153
233
|
},
|
|
154
234
|
expected_approval_fingerprint: {
|
|
155
235
|
type: "string",
|
|
@@ -218,7 +298,7 @@ function createPlugin(relayPath) {
|
|
|
218
298
|
});
|
|
219
299
|
registerCliTool(api, {
|
|
220
300
|
name: "agent_knock_knock_list",
|
|
221
|
-
description: "List existing Codex and Claude Code tmux panes as the primary terminals[] resources. Each terminal may include managed.current_turn or managed.recent_turn; all=true also includes older managed.history and retained unavailable history. By default, unavailable_managed_turns contains attention-needed records whose pane is unavailable. Use only each row's available_actions and
|
|
301
|
+
description: "List existing Codex and Claude Code tmux panes as the primary terminals[] resources. Each terminal may include managed.current_turn or managed.recent_turn; all=true also includes older managed.history and retained unavailable history. By default, unavailable_managed_turns contains attention-needed records whose pane is unavailable. Use only each row's available_actions and authoritative prefilled arguments: send targets a session and starts a new turn; respond targets the exact in-flight turn; managed controls target the exact turn; a raw terminal row may prefill its own compatibility selector for status or recovery controls. Never construct that selector. AKK revalidates every side effect and never starts a coding agent.",
|
|
222
302
|
parameters: listParameters,
|
|
223
303
|
buildArgs: (params) => {
|
|
224
304
|
const config = isRecord(api.pluginConfig) ? api.pluginConfig : {};
|
|
@@ -242,7 +322,7 @@ function createPlugin(relayPath) {
|
|
|
242
322
|
api.registerTool((_toolContext) => ({
|
|
243
323
|
label: "AKK Status",
|
|
244
324
|
name: "agent_knock_knock_status",
|
|
245
|
-
description: "Inspect one AKK-managed turn or
|
|
325
|
+
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.",
|
|
246
326
|
parameters: statusParameters,
|
|
247
327
|
async execute(_toolCallId, params) {
|
|
248
328
|
const result = runCli(api, buildStatusCliArgs(api, isRecord(params) ? params : {}));
|
|
@@ -252,20 +332,38 @@ function createPlugin(relayPath) {
|
|
|
252
332
|
api.registerTool((toolContext) => ({
|
|
253
333
|
label: "AKK Send",
|
|
254
334
|
name: "agent_knock_knock_send",
|
|
255
|
-
description: "
|
|
335
|
+
description: "Start a new turn in an existing AKK session without clearing the coding agent's native context. Target session_id from list or a prior send; never pass a turn id. selector is compatibility-only for initial live-terminal discovery, and both fields may be omitted only when AKK should require one unique eligible idle pane. To answer an in-flight question, use agent_knock_knock_respond instead. For ordinary use add only request and omit monitoring timeouts unless the user explicitly asks to change them. timeoutSeconds is unsupported. AKK never starts a coding agent. This is asynchronous: after acceptance, yield and wait for the callback or a later explicit status request.",
|
|
256
336
|
parameters: sendParameters,
|
|
257
337
|
async execute(_toolCallId, params) {
|
|
258
338
|
const result = await runSendRequest(api, isRecord(params) ? params : {}, toolContext);
|
|
259
|
-
return toolResult(result);
|
|
339
|
+
return toolResult(result, { submissionErrors: true });
|
|
260
340
|
}
|
|
261
341
|
}), { name: "agent_knock_knock_send", optional: true });
|
|
342
|
+
registerCliTool(api, {
|
|
343
|
+
name: "agent_knock_knock_respond",
|
|
344
|
+
description: "Respond to a question or blocked callback in one exact in-flight AKK turn. This continues that turn and does not create a new turn; use agent_knock_knock_send with session_id for later ordinary work.",
|
|
345
|
+
parameters: respondParameters,
|
|
346
|
+
buildArgs: (params) => {
|
|
347
|
+
const config = isRecord(api.pluginConfig) ? api.pluginConfig : {};
|
|
348
|
+
const args = [
|
|
349
|
+
"respond",
|
|
350
|
+
"--turn",
|
|
351
|
+
requiredString(params.turn_id, "turn_id"),
|
|
352
|
+
"--message",
|
|
353
|
+
requiredString(params.request, "request")
|
|
354
|
+
];
|
|
355
|
+
pushOptional(args, "--store-dir", resolvePluginStoreDir(config));
|
|
356
|
+
return args;
|
|
357
|
+
}
|
|
358
|
+
});
|
|
262
359
|
registerCliTool(api, {
|
|
263
360
|
name: "agent_knock_knock_approve",
|
|
264
|
-
description: "Manually approve
|
|
361
|
+
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 may be approved only through its own list-prefilled action. 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.",
|
|
265
362
|
parameters: approveParameters,
|
|
266
363
|
buildArgs: (params) => {
|
|
267
364
|
const config = isRecord(api.pluginConfig) ? api.pluginConfig : {};
|
|
268
|
-
const args = ["approve"
|
|
365
|
+
const args = ["approve"];
|
|
366
|
+
pushTurnTarget(args, params);
|
|
269
367
|
pushOptional(args, "--expected-approval-fingerprint", requiredString(params.expected_approval_fingerprint, "expected_approval_fingerprint"));
|
|
270
368
|
pushOptional(args, "--store-dir", resolvePluginStoreDir(config));
|
|
271
369
|
return args;
|
|
@@ -273,11 +371,12 @@ function createPlugin(relayPath) {
|
|
|
273
371
|
});
|
|
274
372
|
registerCliTool(api, {
|
|
275
373
|
name: "agent_knock_knock_renew",
|
|
276
|
-
description: "Renew monitoring for
|
|
374
|
+
description: "Renew monitoring for one exact stalled turn_id without sending text or keys to the coding agent. Use this when the user wants a still-live long-running terminal task to keep monitoring after an inactivity stall.",
|
|
277
375
|
parameters: renewParameters,
|
|
278
376
|
buildArgs: (params) => {
|
|
279
377
|
const config = isRecord(api.pluginConfig) ? api.pluginConfig : {};
|
|
280
|
-
const args = ["renew"
|
|
378
|
+
const args = ["renew"];
|
|
379
|
+
pushTurnTarget(args, params);
|
|
281
380
|
pushOptional(args, "--minutes", numberString(params.minutes) ?? numberString(config.agentTimeoutMinutes));
|
|
282
381
|
pushOptional(args, "--store-dir", resolvePluginStoreDir(config));
|
|
283
382
|
return args;
|
|
@@ -285,22 +384,24 @@ function createPlugin(relayPath) {
|
|
|
285
384
|
});
|
|
286
385
|
registerCliTool(api, {
|
|
287
386
|
name: "agent_knock_knock_retry_callback",
|
|
288
|
-
description: "Retry a persisted AKK callback that failed before reaching OpenClaw. The original callback message id
|
|
387
|
+
description: "Retry a persisted AKK callback for an exact turn that failed before reaching OpenClaw. The original callback message id and turn identity are reused for idempotent delivery.",
|
|
289
388
|
parameters: retryCallbackParameters,
|
|
290
389
|
buildArgs: (params) => {
|
|
291
390
|
const config = isRecord(api.pluginConfig) ? api.pluginConfig : {};
|
|
292
|
-
const args = ["retry-callback"
|
|
391
|
+
const args = ["retry-callback"];
|
|
392
|
+
pushTurnTarget(args, params);
|
|
293
393
|
pushOptional(args, "--store-dir", resolvePluginStoreDir(config));
|
|
294
394
|
return args;
|
|
295
395
|
}
|
|
296
396
|
});
|
|
297
397
|
registerCliTool(api, {
|
|
298
398
|
name: "agent_knock_knock_cancel",
|
|
299
|
-
description: "Interrupt
|
|
399
|
+
description: "Interrupt one exact AKK turn_id, or use only an unmanaged raw terminal row's own prefilled cancel action. Claude sends Escape; Codex uses its declared interrupt key. The shared tmux pane remains open for human takeover.",
|
|
300
400
|
parameters: cancelParameters,
|
|
301
401
|
buildArgs: (params) => {
|
|
302
402
|
const config = isRecord(api.pluginConfig) ? api.pluginConfig : {};
|
|
303
|
-
const args = ["cancel"
|
|
403
|
+
const args = ["cancel"];
|
|
404
|
+
pushTurnTarget(args, params);
|
|
304
405
|
pushOptional(args, "--store-dir", resolvePluginStoreDir(config));
|
|
305
406
|
pushOptional(args, "--idle-timeout-minutes", numberString(params.idleTimeoutMinutes) ?? numberString(config.idleTimeoutMinutes));
|
|
306
407
|
return args;
|
|
@@ -312,7 +413,8 @@ function createPlugin(relayPath) {
|
|
|
312
413
|
parameters: closeParameters,
|
|
313
414
|
buildArgs: (params) => {
|
|
314
415
|
const config = isRecord(api.pluginConfig) ? api.pluginConfig : {};
|
|
315
|
-
const args = ["close"
|
|
416
|
+
const args = ["close"];
|
|
417
|
+
pushTurnTarget(args, params);
|
|
316
418
|
pushOptional(args, "--reason", stringValue(params.reason));
|
|
317
419
|
pushOptional(args, "--expected-message-id", stringValue(params.expected_message_id));
|
|
318
420
|
pushOptional(args, "--store-dir", resolvePluginStoreDir(config));
|
|
@@ -369,8 +471,10 @@ async function handleAkkCommand(api, ctx) {
|
|
|
369
471
|
case "send":
|
|
370
472
|
return {
|
|
371
473
|
text: formatSendCommandResult(result),
|
|
372
|
-
isError: ["uncertain", "aborted"].includes(String(result.submission_outcome ?? ""))
|
|
474
|
+
isError: ["uncertain", "aborted"].includes(String(result.submission_outcome ?? "")) || result.status === "delivered_unfenced"
|
|
373
475
|
};
|
|
476
|
+
case "respond":
|
|
477
|
+
return formatAkkRespondCommandResult(result);
|
|
374
478
|
case "approve":
|
|
375
479
|
return { text: formatApproveCommandResult(result) };
|
|
376
480
|
case "renew":
|
|
@@ -393,26 +497,35 @@ async function handleAkkCommand(api, ctx) {
|
|
|
393
497
|
}
|
|
394
498
|
function formatDelegateCommandResult(result) {
|
|
395
499
|
const agent = executorDisplayName(result.agent);
|
|
500
|
+
const { sessionId, turnId } = publicTurnIdentity(result);
|
|
501
|
+
if (result.status === "submission_unfenced") {
|
|
502
|
+
return [
|
|
503
|
+
`AKK sent the terminal input to ${agent}, but could not bind later side effects to an exact native session.`,
|
|
504
|
+
`session: ${sessionId}`,
|
|
505
|
+
`turn: ${turnId}`,
|
|
506
|
+
"next: do not retry or continue automatically; inspect the named pane and close this Turn before sending more work."
|
|
507
|
+
].join("\n");
|
|
508
|
+
}
|
|
396
509
|
if (result.status === "submission_uncertain") {
|
|
397
510
|
return [
|
|
398
511
|
`AKK could not prove whether ${agent} received the terminal task.`,
|
|
399
|
-
`
|
|
400
|
-
`
|
|
512
|
+
`session: ${sessionId}`,
|
|
513
|
+
`turn: ${turnId}`,
|
|
401
514
|
"next: do not retry automatically; inspect AKK status and the named tmux pane."
|
|
402
515
|
].join("\n");
|
|
403
516
|
}
|
|
404
517
|
if (result.status === "submission_aborted") {
|
|
405
518
|
return [
|
|
406
519
|
`AKK stopped before sending the terminal task to ${agent}.`,
|
|
407
|
-
`
|
|
408
|
-
`
|
|
520
|
+
`session: ${sessionId}`,
|
|
521
|
+
`turn: ${turnId}`,
|
|
409
522
|
"next: no tmux input was sent, so this request may be retried."
|
|
410
523
|
].join("\n");
|
|
411
524
|
}
|
|
412
525
|
return [
|
|
413
526
|
`AKK sent the task to ${agent} in the shared terminal.`,
|
|
414
|
-
`
|
|
415
|
-
`
|
|
527
|
+
`session: ${sessionId}`,
|
|
528
|
+
`turn: ${turnId}`,
|
|
416
529
|
`status: ${result.conversation_status ?? result.status ?? "unknown"}`,
|
|
417
530
|
"The result will return to this OpenClaw session through the callback."
|
|
418
531
|
].join("\n");
|
|
@@ -428,12 +541,13 @@ function executorDisplayName(kind) {
|
|
|
428
541
|
function formatStatusCommandResult(result) {
|
|
429
542
|
const summary = result.summary ?? result.conversation ?? result ?? {};
|
|
430
543
|
const terminalStatus = isRecord(result.terminal_status) ? result.terminal_status : {};
|
|
431
|
-
const
|
|
544
|
+
const { sessionId, turnId } = publicTurnIdentity(result);
|
|
432
545
|
const lines = [
|
|
433
|
-
|
|
546
|
+
"AKK status:",
|
|
547
|
+
`session: ${sessionId}`,
|
|
548
|
+
`turn: ${turnId}`,
|
|
434
549
|
`agent: ${summary.agent ?? summary.executor?.kind ?? terminalStatus.agent ?? "unknown"}`,
|
|
435
|
-
`status: ${summary.status ?? terminalStatus.activity_state ?? "unknown"}
|
|
436
|
-
`session: ${summary.session ?? summary.executor?.session ?? terminalControl.target ?? "unknown"}`
|
|
550
|
+
`status: ${summary.status ?? terminalStatus.activity_state ?? "unknown"}`
|
|
437
551
|
];
|
|
438
552
|
if (summary.request) {
|
|
439
553
|
lines.push(`request: ${truncateText(summary.request, 180)}`);
|
|
@@ -477,16 +591,22 @@ function formatDoctorCommandResult(result) {
|
|
|
477
591
|
].join("\n");
|
|
478
592
|
}
|
|
479
593
|
function formatRenewCommandResult(result) {
|
|
594
|
+
const { sessionId, turnId } = publicTurnIdentity(result);
|
|
480
595
|
return [
|
|
481
|
-
|
|
596
|
+
"AKK monitoring renewed.",
|
|
597
|
+
`session: ${sessionId}`,
|
|
598
|
+
`turn: ${turnId}`,
|
|
482
599
|
`inactivity timeout: ${result.agent_timeout_minutes ?? "unknown"} minutes`,
|
|
483
600
|
`hard lifetime: ${result.agent_hard_timeout_minutes ?? "unknown"} minutes`,
|
|
484
601
|
"No message or key was sent to the coding agent."
|
|
485
602
|
].join("\n");
|
|
486
603
|
}
|
|
487
604
|
function formatRetryCallbackCommandResult(result) {
|
|
605
|
+
const { sessionId, turnId } = publicTurnIdentity(result);
|
|
488
606
|
return [
|
|
489
|
-
|
|
607
|
+
"AKK callback delivered.",
|
|
608
|
+
`session: ${sessionId}`,
|
|
609
|
+
`turn: ${turnId}`,
|
|
490
610
|
`status: ${result.conversation?.status ?? "unknown"}`,
|
|
491
611
|
`attempts: ${result.conversation?.callback_delivery?.attempts ?? "unknown"}`
|
|
492
612
|
].join("\n");
|
|
@@ -494,12 +614,24 @@ function formatRetryCallbackCommandResult(result) {
|
|
|
494
614
|
function formatSendCommandResult(result) {
|
|
495
615
|
const conversation = result.conversation ?? {};
|
|
496
616
|
const conversationId = conversation.conversation_id ?? result.conversation_id ?? "unknown";
|
|
617
|
+
const sessionId = conversation.session_id ?? result.session_id ?? conversationId;
|
|
618
|
+
const turnId = conversation.turn_id ?? result.turn_id ?? conversationId;
|
|
497
619
|
const status = conversation.status ?? result.status ?? "unknown";
|
|
498
620
|
const nextAction = isRecord(result.openclaw_next_action) ? result.openclaw_next_action : undefined;
|
|
621
|
+
if (result.status === "delivered_unfenced") {
|
|
622
|
+
return [
|
|
623
|
+
"AKK sent the terminal input but could not bind an exact native session.",
|
|
624
|
+
`session: ${sessionId}`,
|
|
625
|
+
`turn: ${turnId}`,
|
|
626
|
+
`status: ${status}`,
|
|
627
|
+
"next: do not retry or continue automatically; inspect the shared tmux pane and close this Turn before sending more work."
|
|
628
|
+
].join("\n");
|
|
629
|
+
}
|
|
499
630
|
if (result.submission_outcome === "uncertain") {
|
|
500
631
|
return [
|
|
501
632
|
"AKK terminal submission outcome is uncertain.",
|
|
502
|
-
`
|
|
633
|
+
`session: ${sessionId}`,
|
|
634
|
+
`turn: ${turnId}`,
|
|
503
635
|
`status: ${result.status ?? status}`,
|
|
504
636
|
"next: do not retry automatically; inspect AKK status and the named tmux pane."
|
|
505
637
|
].join("\n");
|
|
@@ -507,14 +639,16 @@ function formatSendCommandResult(result) {
|
|
|
507
639
|
if (result.submission_outcome === "aborted") {
|
|
508
640
|
return [
|
|
509
641
|
"AKK terminal submission was aborted before tmux input.",
|
|
510
|
-
`
|
|
642
|
+
`session: ${sessionId}`,
|
|
643
|
+
`turn: ${turnId}`,
|
|
511
644
|
`status: ${result.status ?? status}`,
|
|
512
645
|
"next: this request was not sent and may be retried."
|
|
513
646
|
].join("\n");
|
|
514
647
|
}
|
|
515
648
|
const lines = [
|
|
516
|
-
"AKK
|
|
517
|
-
`
|
|
649
|
+
"AKK turn sent.",
|
|
650
|
+
`session: ${sessionId}`,
|
|
651
|
+
`turn: ${turnId}`,
|
|
518
652
|
`status: ${status}`
|
|
519
653
|
];
|
|
520
654
|
if (result.source) {
|
|
@@ -529,22 +663,24 @@ function formatSendCommandResult(result) {
|
|
|
529
663
|
}
|
|
530
664
|
function formatCancelCommandResult(result) {
|
|
531
665
|
const conversation = result.conversation ?? {};
|
|
532
|
-
const
|
|
666
|
+
const { sessionId, turnId } = publicTurnIdentity(result);
|
|
533
667
|
return [
|
|
534
668
|
"AKK cancel requested.",
|
|
535
|
-
`
|
|
669
|
+
`session: ${sessionId}`,
|
|
670
|
+
`turn: ${turnId}`,
|
|
536
671
|
`agent: ${result.executor?.kind ?? conversation.executor?.kind ?? "unknown"}`,
|
|
537
|
-
`session: ${result.executor?.session ?? conversation.executor?.session ?? terminalControl.target ?? "unknown"}`,
|
|
538
672
|
`status: ${conversation.status ?? (result.cancel_requested === true ? "cancel requested" : "not cancelled")}`
|
|
539
673
|
].join("\n");
|
|
540
674
|
}
|
|
541
675
|
function formatApproveCommandResult(result) {
|
|
542
676
|
const conversation = result.conversation ?? {};
|
|
677
|
+
const { sessionId, turnId } = publicTurnIdentity(result);
|
|
543
678
|
return [
|
|
544
679
|
result.approved === true
|
|
545
680
|
? "AKK approved the current terminal request."
|
|
546
681
|
: "AKK did not approve the terminal request.",
|
|
547
|
-
`
|
|
682
|
+
`session: ${sessionId}`,
|
|
683
|
+
`turn: ${turnId}`,
|
|
548
684
|
`status: ${conversation.status ?? "unknown"}`,
|
|
549
685
|
...(result.reason ? [`reason: ${result.reason}`] : [])
|
|
550
686
|
].join("\n");
|
|
@@ -558,25 +694,45 @@ function formatCloseCommandResult(result) {
|
|
|
558
694
|
return [
|
|
559
695
|
"AKK cleared the unresolved terminal dispatch fence.",
|
|
560
696
|
`terminal: ${terminalControl.target ?? "unknown"}`,
|
|
561
|
-
`previous
|
|
697
|
+
`previous turn: ${result.owner_turn_id ?? result.owner_conversation_id ?? "unknown"}`,
|
|
562
698
|
"The coding agent and tmux pane remain open."
|
|
563
699
|
].join("\n");
|
|
564
700
|
}
|
|
565
701
|
const conversation = result.conversation ?? {};
|
|
702
|
+
const { sessionId, turnId } = publicTurnIdentity(result);
|
|
566
703
|
return [
|
|
567
|
-
"AKK
|
|
568
|
-
`
|
|
704
|
+
"AKK Turn record closed.",
|
|
705
|
+
`session: ${sessionId}`,
|
|
706
|
+
`turn: ${turnId}`,
|
|
569
707
|
`status: ${conversation.status ?? "unknown"}`
|
|
570
708
|
].join("\n");
|
|
571
709
|
}
|
|
710
|
+
function publicTurnIdentity(result) {
|
|
711
|
+
const conversation = isRecord(result.conversation) ? result.conversation : {};
|
|
712
|
+
const summary = isRecord(result.summary) ? result.summary : {};
|
|
713
|
+
const compatibilityId = stringValue(conversation.conversation_id) ??
|
|
714
|
+
stringValue(summary.conversation_id) ??
|
|
715
|
+
stringValue(result.conversation_id);
|
|
716
|
+
return {
|
|
717
|
+
sessionId: stringValue(conversation.session_id) ??
|
|
718
|
+
stringValue(summary.session_id) ??
|
|
719
|
+
stringValue(result.session_id) ??
|
|
720
|
+
compatibilityId ??
|
|
721
|
+
"unknown",
|
|
722
|
+
turnId: stringValue(conversation.turn_id) ??
|
|
723
|
+
stringValue(summary.turn_id) ??
|
|
724
|
+
stringValue(result.turn_id) ??
|
|
725
|
+
compatibilityId ??
|
|
726
|
+
"unknown"
|
|
727
|
+
};
|
|
728
|
+
}
|
|
572
729
|
function buildStatusCliArgs(api, params) {
|
|
573
730
|
const config = isRecord(api.pluginConfig) ? api.pluginConfig : {};
|
|
574
731
|
const args = [
|
|
575
732
|
"status",
|
|
576
|
-
"--reconcile"
|
|
577
|
-
"--conversation",
|
|
578
|
-
requiredString(params.conversation_id, "conversation_id")
|
|
733
|
+
"--reconcile"
|
|
579
734
|
];
|
|
735
|
+
pushTurnTarget(args, params);
|
|
580
736
|
pushOptional(args, "--store-dir", resolvePluginStoreDir(config));
|
|
581
737
|
pushOptional(args, "--idle-timeout-minutes", numberString(params.idleTimeoutMinutes) ??
|
|
582
738
|
numberString(api.pluginConfig?.idleTimeoutMinutes));
|
|
@@ -602,21 +758,37 @@ function terminalScreenExcerpt(result) {
|
|
|
602
758
|
: `…${text.slice(-1599)}`;
|
|
603
759
|
}
|
|
604
760
|
async function runSendRequest(api, params, toolContext) {
|
|
605
|
-
const
|
|
606
|
-
|
|
761
|
+
const requestedType = Object.hasOwn(params, "type")
|
|
762
|
+
? stringValue(params.type)
|
|
763
|
+
: "task";
|
|
764
|
+
if (requestedType !== "task") {
|
|
765
|
+
throw new Error("ordinary send type must be task; use agent_knock_knock_respond for an in-flight response");
|
|
766
|
+
}
|
|
767
|
+
if (Object.hasOwn(params, "session_id") && Object.hasOwn(params, "selector")) {
|
|
768
|
+
throw new Error("ordinary send accepts only one of session_id or selector");
|
|
769
|
+
}
|
|
770
|
+
const sessionId = Object.hasOwn(params, "session_id")
|
|
771
|
+
? requiredString(params.session_id, "session_id")
|
|
772
|
+
: undefined;
|
|
773
|
+
const selector = Object.hasOwn(params, "selector")
|
|
774
|
+
? requiredString(params.selector, "selector")
|
|
775
|
+
: undefined;
|
|
776
|
+
if (!sessionId && !selector) {
|
|
607
777
|
return runDelegate(api, params, toolContext);
|
|
608
778
|
}
|
|
609
779
|
const config = isRecord(api.pluginConfig) ? api.pluginConfig : {};
|
|
610
780
|
const openclawSession = stringValue(toolContext?.sessionKey) ??
|
|
611
781
|
"agent:main:main";
|
|
612
782
|
const args = [
|
|
613
|
-
"send"
|
|
614
|
-
"--conversation",
|
|
615
|
-
selector,
|
|
616
|
-
"--message",
|
|
617
|
-
requiredString(params.request, "request"),
|
|
618
|
-
"--background"
|
|
783
|
+
"send"
|
|
619
784
|
];
|
|
785
|
+
if (sessionId) {
|
|
786
|
+
args.push("--session", sessionId);
|
|
787
|
+
}
|
|
788
|
+
else {
|
|
789
|
+
args.push("--conversation", requiredString(selector, "selector"));
|
|
790
|
+
}
|
|
791
|
+
args.push("--message", requiredString(params.request, "request"), "--background");
|
|
620
792
|
pushOptional(args, "--type", stringValue(params.type));
|
|
621
793
|
pushOptional(args, "--store-dir", resolvePluginStoreDir(config));
|
|
622
794
|
pushOptional(args, "--idle-timeout-minutes", numberString(params.idleTimeoutMinutes) ??
|
|
@@ -631,17 +803,92 @@ async function runSendRequest(api, params, toolContext) {
|
|
|
631
803
|
pushOptional(args, "--openclaw-bin", stringValue(config.openclawBin));
|
|
632
804
|
return runCli(api, args);
|
|
633
805
|
}
|
|
634
|
-
function toolResult(result) {
|
|
806
|
+
function toolResult(result, { submissionErrors = false } = {}) {
|
|
807
|
+
const normalized = withTurnIdentity(result);
|
|
808
|
+
const submissionError = submissionErrors && isSubmissionError(normalized);
|
|
635
809
|
return {
|
|
636
810
|
content: [
|
|
637
811
|
{
|
|
638
812
|
type: "text",
|
|
639
|
-
text: JSON.stringify(
|
|
813
|
+
text: JSON.stringify(normalized, null, 2)
|
|
640
814
|
}
|
|
641
815
|
],
|
|
642
|
-
details:
|
|
816
|
+
details: normalized,
|
|
817
|
+
...(submissionError ? { isError: true } : {})
|
|
643
818
|
};
|
|
644
819
|
}
|
|
820
|
+
function isSubmissionError(result) {
|
|
821
|
+
if (!isRecord(result)) {
|
|
822
|
+
return false;
|
|
823
|
+
}
|
|
824
|
+
return [
|
|
825
|
+
"submission_unfenced",
|
|
826
|
+
"submission_uncertain",
|
|
827
|
+
"submission_aborted"
|
|
828
|
+
].includes(String(result.status ?? "")) ||
|
|
829
|
+
["uncertain", "aborted"].includes(String(result.submission_outcome ?? "")) ||
|
|
830
|
+
result.status === "delivered_unfenced";
|
|
831
|
+
}
|
|
832
|
+
function withTurnIdentity(result) {
|
|
833
|
+
if (!isRecord(result)) {
|
|
834
|
+
return result;
|
|
835
|
+
}
|
|
836
|
+
const sources = [
|
|
837
|
+
{ label: "result", value: result },
|
|
838
|
+
{ label: "result.conversation", value: result.conversation },
|
|
839
|
+
{ label: "result.summary", value: result.summary },
|
|
840
|
+
{ label: "result.message", value: result.message }
|
|
841
|
+
];
|
|
842
|
+
const compatibilityId = consistentResultIdentity("conversation_id", sources);
|
|
843
|
+
const explicitSessionId = consistentResultIdentity("session_id", sources);
|
|
844
|
+
const explicitTurnId = consistentResultIdentity("turn_id", sources);
|
|
845
|
+
const hasModernIdentity = Boolean(explicitSessionId || explicitTurnId);
|
|
846
|
+
if (hasModernIdentity && (!explicitSessionId || !explicitTurnId)) {
|
|
847
|
+
throw new Error("agent-knock-knock CLI returned a partial session_id/turn_id identity");
|
|
848
|
+
}
|
|
849
|
+
if (hasModernIdentity && !compatibilityId) {
|
|
850
|
+
throw new Error("agent-knock-knock CLI returned modern identity without conversation_id");
|
|
851
|
+
}
|
|
852
|
+
if (hasModernIdentity && compatibilityId !== explicitTurnId) {
|
|
853
|
+
throw new Error("agent-knock-knock CLI returned conversation_id that differs from turn_id");
|
|
854
|
+
}
|
|
855
|
+
if (!hasModernIdentity && !compatibilityId) {
|
|
856
|
+
return result;
|
|
857
|
+
}
|
|
858
|
+
if (!hasModernIdentity &&
|
|
859
|
+
(compatibilityId?.startsWith("terminal:") ||
|
|
860
|
+
stringValue(result.source) === "terminal" ||
|
|
861
|
+
(isRecord(result.summary) &&
|
|
862
|
+
stringValue(result.summary.source) === "terminal"))) {
|
|
863
|
+
return result;
|
|
864
|
+
}
|
|
865
|
+
const sessionId = explicitSessionId ?? compatibilityId;
|
|
866
|
+
const turnId = explicitTurnId ?? compatibilityId;
|
|
867
|
+
return {
|
|
868
|
+
...result,
|
|
869
|
+
session_id: sessionId,
|
|
870
|
+
turn_id: turnId
|
|
871
|
+
};
|
|
872
|
+
}
|
|
873
|
+
function consistentResultIdentity(field, sources) {
|
|
874
|
+
const values = sources.flatMap(({ label, value }) => {
|
|
875
|
+
if (!isRecord(value) || !Object.hasOwn(value, field)) {
|
|
876
|
+
return [];
|
|
877
|
+
}
|
|
878
|
+
const identity = stringValue(value[field]);
|
|
879
|
+
if (!identity) {
|
|
880
|
+
throw new Error(`agent-knock-knock CLI returned invalid ${label}.${field}`);
|
|
881
|
+
}
|
|
882
|
+
return [{ label, identity }];
|
|
883
|
+
});
|
|
884
|
+
const expected = values[0];
|
|
885
|
+
for (const candidate of values.slice(1)) {
|
|
886
|
+
if (candidate.identity !== expected.identity) {
|
|
887
|
+
throw new Error(`agent-knock-knock CLI returned conflicting ${field} between ${expected.label} and ${candidate.label}`);
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
return expected?.identity;
|
|
891
|
+
}
|
|
645
892
|
async function runDelegate(api, params, toolContext) {
|
|
646
893
|
const config = isRecord(api.pluginConfig) ? api.pluginConfig : {};
|
|
647
894
|
const request = requiredString(params.request, "request");
|
|
@@ -661,67 +908,103 @@ async function runDelegate(api, params, toolContext) {
|
|
|
661
908
|
pushOptional(args, "--idle-timeout-minutes", numberString(params.idleTimeoutMinutes) ?? numberString(config.idleTimeoutMinutes));
|
|
662
909
|
pushOptional(args, "--agent-timeout-minutes", numberString(params.agentTimeoutMinutes) ?? numberString(config.agentTimeoutMinutes));
|
|
663
910
|
pushOptional(args, "--agent-hard-timeout-minutes", numberString(params.agentHardTimeoutMinutes) ?? numberString(config.agentHardTimeoutMinutes));
|
|
664
|
-
const parsed = await runCliAsync(api, args);
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
911
|
+
const parsed = withTurnIdentity(await runCliAsync(api, args));
|
|
912
|
+
if (!isRecord(parsed)) {
|
|
913
|
+
throw new Error("agent-knock-knock delegate returned a non-object result");
|
|
914
|
+
}
|
|
915
|
+
const conversationId = stringValue(parsed.conversation_id) ??
|
|
916
|
+
(isRecord(parsed.conversation)
|
|
917
|
+
? stringValue(parsed.conversation.conversation_id)
|
|
918
|
+
: undefined);
|
|
919
|
+
const sessionId = stringValue(parsed.session_id);
|
|
920
|
+
const turnId = stringValue(parsed.turn_id);
|
|
921
|
+
if (!conversationId || !sessionId || !turnId) {
|
|
922
|
+
throw new Error("agent-knock-knock delegate returned incomplete Turn identity");
|
|
923
|
+
}
|
|
924
|
+
const parsedConversation = isRecord(parsed.conversation)
|
|
925
|
+
? parsed.conversation
|
|
926
|
+
: undefined;
|
|
927
|
+
const parsedPaths = isRecord(parsed.paths) ? parsed.paths : undefined;
|
|
928
|
+
const parsedTerminalControl = isRecord(parsed.terminal_control)
|
|
929
|
+
? parsed.terminal_control
|
|
930
|
+
: undefined;
|
|
931
|
+
const statePath = parsedConversation?.state_path ??
|
|
932
|
+
parsedPaths?.statePath;
|
|
933
|
+
const logPath = parsedConversation?.event_log_path ??
|
|
934
|
+
parsedPaths?.logPath;
|
|
671
935
|
const submissionUncertain = parsed.submission_outcome === "uncertain";
|
|
672
936
|
const submissionAborted = parsed.submission_outcome === "aborted";
|
|
673
|
-
const
|
|
937
|
+
const submissionUnfenced = parsed.status === "delivered_unfenced";
|
|
938
|
+
const agent = (isRecord(parsedConversation?.executor)
|
|
939
|
+
? stringValue(parsedConversation.executor.kind)
|
|
940
|
+
: undefined) ??
|
|
674
941
|
stringValue(parsed.agent);
|
|
675
942
|
return {
|
|
676
|
-
status:
|
|
677
|
-
? "
|
|
678
|
-
:
|
|
679
|
-
? "
|
|
680
|
-
:
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
943
|
+
status: submissionUnfenced
|
|
944
|
+
? "submission_unfenced"
|
|
945
|
+
: submissionUncertain
|
|
946
|
+
? "submission_uncertain"
|
|
947
|
+
: submissionAborted
|
|
948
|
+
? "submission_aborted"
|
|
949
|
+
: "async_pending",
|
|
950
|
+
submission_status: submissionUnfenced
|
|
951
|
+
? "submitted_unfenced"
|
|
952
|
+
: submissionUncertain
|
|
953
|
+
? "uncertain"
|
|
954
|
+
: submissionAborted
|
|
955
|
+
? "aborted"
|
|
956
|
+
: "accepted",
|
|
686
957
|
conversation_id: conversationId,
|
|
687
|
-
|
|
958
|
+
session_id: sessionId,
|
|
959
|
+
turn_id: turnId,
|
|
960
|
+
conversation_status: parsedConversation?.status,
|
|
688
961
|
state_path: statePath,
|
|
689
962
|
event_log_path: logPath,
|
|
690
963
|
agent,
|
|
691
|
-
executor:
|
|
692
|
-
session:
|
|
693
|
-
|
|
964
|
+
executor: parsedConversation?.executor,
|
|
965
|
+
session: (isRecord(parsedConversation?.executor)
|
|
966
|
+
? parsedConversation.executor.session
|
|
967
|
+
: undefined) ??
|
|
968
|
+
parsedTerminalControl?.target,
|
|
694
969
|
openclaw_session: openclawSession,
|
|
695
970
|
launched: parsed.launched === true,
|
|
696
971
|
replayed: parsed.replayed === true,
|
|
697
972
|
background: parsed.background === true,
|
|
698
|
-
pid: parsed.pid ??
|
|
973
|
+
pid: parsed.pid ?? parsedTerminalControl?.panePid ?? null,
|
|
699
974
|
callback_method: CALLBACK_METHOD,
|
|
700
|
-
...(
|
|
975
|
+
...(submissionUnfenced
|
|
701
976
|
? {
|
|
702
|
-
submission_outcome: "
|
|
977
|
+
submission_outcome: "submitted",
|
|
703
978
|
do_not_retry: true,
|
|
704
979
|
reason: parsed.reason,
|
|
705
980
|
openclaw_next_action: parsed.openclaw_next_action,
|
|
706
|
-
note: "AKK could not
|
|
981
|
+
note: "AKK sent the terminal input but could not fence later side effects to an exact native session. Do not retry or continue automatically; inspect the pane and close this Turn."
|
|
707
982
|
}
|
|
708
|
-
:
|
|
983
|
+
: submissionUncertain
|
|
709
984
|
? {
|
|
710
|
-
submission_outcome: "
|
|
711
|
-
|
|
985
|
+
submission_outcome: "uncertain",
|
|
986
|
+
do_not_retry: true,
|
|
712
987
|
reason: parsed.reason,
|
|
713
988
|
openclaw_next_action: parsed.openclaw_next_action,
|
|
714
|
-
note: "AKK
|
|
989
|
+
note: "AKK could not prove whether tmux accepted Enter. Do not retry automatically; inspect the exact AKK Turn record and shared terminal."
|
|
715
990
|
}
|
|
716
|
-
:
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
991
|
+
: submissionAborted
|
|
992
|
+
? {
|
|
993
|
+
submission_outcome: "aborted",
|
|
994
|
+
safe_to_retry: true,
|
|
995
|
+
reason: parsed.reason,
|
|
996
|
+
openclaw_next_action: parsed.openclaw_next_action,
|
|
997
|
+
note: "AKK stopped before sending tmux input. The request was not submitted and may be retried."
|
|
998
|
+
}
|
|
999
|
+
: {
|
|
1000
|
+
openclaw_next_action: {
|
|
1001
|
+
action: "yield",
|
|
1002
|
+
reason: "The coding agent is working in the shared tmux terminal. End this OpenClaw turn now and wait for an Agent Knock Knock callback.",
|
|
1003
|
+
do_not: "Do not poll terminal internals while waiting. Further communication must use Agent Knock Knock tools so the same shared terminal remains authoritative.",
|
|
1004
|
+
expected_callback: "The callback will be injected and scheduled into this OpenClaw session by the agent-knock-knock.callback Gateway method."
|
|
1005
|
+
},
|
|
1006
|
+
note: "The task was sent to the shared tmux terminal. OpenClaw should yield now and wait for the scheduled callback turn."
|
|
1007
|
+
})
|
|
725
1008
|
};
|
|
726
1009
|
}
|
|
727
1010
|
function registerCliTool(api, { name, description, parameters, buildArgs }) {
|
|
@@ -732,15 +1015,9 @@ function registerCliTool(api, { name, description, parameters, buildArgs }) {
|
|
|
732
1015
|
parameters,
|
|
733
1016
|
async execute(_toolCallId, params) {
|
|
734
1017
|
const result = runCli(api, buildArgs(isRecord(params) ? params : {}, toolContext));
|
|
735
|
-
return {
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
type: "text",
|
|
739
|
-
text: JSON.stringify(result, null, 2)
|
|
740
|
-
}
|
|
741
|
-
],
|
|
742
|
-
details: result
|
|
743
|
-
};
|
|
1018
|
+
return toolResult(result, {
|
|
1019
|
+
submissionErrors: name === "agent_knock_knock_respond"
|
|
1020
|
+
});
|
|
744
1021
|
}
|
|
745
1022
|
}), { name, optional: true });
|
|
746
1023
|
}
|
|
@@ -846,6 +1123,139 @@ function runCliAsync(api, cliArgs, { cwd = process.cwd(), allowNonzeroJson = fal
|
|
|
846
1123
|
function relayPathForApi(api) {
|
|
847
1124
|
return relayPathByApi.get(api) ?? defaultBinPath;
|
|
848
1125
|
}
|
|
1126
|
+
function callbackIdentity({ params, conversation, message, messageMetadata }) {
|
|
1127
|
+
const messageHasSessionId = isRecord(message) &&
|
|
1128
|
+
Object.hasOwn(message, "session_id");
|
|
1129
|
+
const messageHasTurnId = isRecord(message) &&
|
|
1130
|
+
Object.hasOwn(message, "turn_id");
|
|
1131
|
+
if (messageHasSessionId !== messageHasTurnId) {
|
|
1132
|
+
throw new Error("modern callback messages require both session_id and turn_id");
|
|
1133
|
+
}
|
|
1134
|
+
const identityMode = messageHasSessionId ? "modern" : "legacy";
|
|
1135
|
+
const sources = [
|
|
1136
|
+
{ label: "message", value: message },
|
|
1137
|
+
{ label: "message.metadata", value: messageMetadata },
|
|
1138
|
+
{ label: "conversation", value: conversation },
|
|
1139
|
+
{ label: "params", value: params }
|
|
1140
|
+
];
|
|
1141
|
+
const explicitConversationId = consistentCallbackIdentity("conversation_id", sources);
|
|
1142
|
+
const explicitTurnId = consistentCallbackIdentity("turn_id", sources);
|
|
1143
|
+
const explicitSessionId = consistentCallbackIdentity("session_id", sources);
|
|
1144
|
+
const hasModernIdentity = Boolean(explicitSessionId || explicitTurnId);
|
|
1145
|
+
if (hasModernIdentity && (!explicitSessionId || !explicitTurnId)) {
|
|
1146
|
+
throw new Error("modern callbacks require both session_id and turn_id");
|
|
1147
|
+
}
|
|
1148
|
+
if (hasModernIdentity && !explicitConversationId) {
|
|
1149
|
+
throw new Error("modern callbacks require conversation_id as the Turn Store alias");
|
|
1150
|
+
}
|
|
1151
|
+
if (hasModernIdentity && explicitConversationId !== explicitTurnId) {
|
|
1152
|
+
throw new Error("callback conversation_id must equal turn_id for modern callback identities");
|
|
1153
|
+
}
|
|
1154
|
+
if (!explicitConversationId) {
|
|
1155
|
+
throw new Error("callback identity requires session_id and turn_id, or a legacy conversation_id");
|
|
1156
|
+
}
|
|
1157
|
+
const turnId = explicitTurnId ?? explicitConversationId;
|
|
1158
|
+
const conversationId = explicitConversationId;
|
|
1159
|
+
const sessionId = explicitSessionId ?? explicitConversationId;
|
|
1160
|
+
return {
|
|
1161
|
+
conversationId,
|
|
1162
|
+
sessionId,
|
|
1163
|
+
turnId,
|
|
1164
|
+
identityMode
|
|
1165
|
+
};
|
|
1166
|
+
}
|
|
1167
|
+
function consistentCallbackIdentity(field, sources) {
|
|
1168
|
+
const values = sources.flatMap(({ label, value }) => {
|
|
1169
|
+
if (!isRecord(value) || !Object.hasOwn(value, field)) {
|
|
1170
|
+
return [];
|
|
1171
|
+
}
|
|
1172
|
+
const identity = stringValue(value[field]);
|
|
1173
|
+
if (!identity) {
|
|
1174
|
+
throw new Error(`callback ${label}.${field} must be a non-empty string`);
|
|
1175
|
+
}
|
|
1176
|
+
return [{ label, identity }];
|
|
1177
|
+
});
|
|
1178
|
+
const expected = values[0];
|
|
1179
|
+
for (const candidate of values.slice(1)) {
|
|
1180
|
+
if (candidate.identity !== expected.identity) {
|
|
1181
|
+
throw new Error(`callback ${field} mismatch between ${expected.label} and ${candidate.label}`);
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
return expected?.identity;
|
|
1185
|
+
}
|
|
1186
|
+
function callbackSessionKey({ params, conversation, message, messageMetadata }) {
|
|
1187
|
+
const sessionKey = callbackStringField("params.sessionKey", params, "sessionKey");
|
|
1188
|
+
if (!sessionKey) {
|
|
1189
|
+
throw new Error("callback params.sessionKey is required");
|
|
1190
|
+
}
|
|
1191
|
+
const gatewaySession = consistentCallbackTarget("Gateway session", [
|
|
1192
|
+
{
|
|
1193
|
+
label: "conversation.gateway_session",
|
|
1194
|
+
owner: conversation,
|
|
1195
|
+
field: "gateway_session"
|
|
1196
|
+
},
|
|
1197
|
+
{
|
|
1198
|
+
label: "message.gateway_session",
|
|
1199
|
+
owner: message,
|
|
1200
|
+
field: "gateway_session"
|
|
1201
|
+
},
|
|
1202
|
+
{
|
|
1203
|
+
label: "message.metadata.gateway_session",
|
|
1204
|
+
owner: messageMetadata,
|
|
1205
|
+
field: "gateway_session"
|
|
1206
|
+
}
|
|
1207
|
+
]);
|
|
1208
|
+
const openclawSession = consistentCallbackTarget("OpenClaw session", [
|
|
1209
|
+
{
|
|
1210
|
+
label: "params.openclaw_session",
|
|
1211
|
+
owner: params,
|
|
1212
|
+
field: "openclaw_session"
|
|
1213
|
+
},
|
|
1214
|
+
{
|
|
1215
|
+
label: "conversation.openclaw_session",
|
|
1216
|
+
owner: conversation,
|
|
1217
|
+
field: "openclaw_session"
|
|
1218
|
+
},
|
|
1219
|
+
{
|
|
1220
|
+
label: "message.openclaw_session",
|
|
1221
|
+
owner: message,
|
|
1222
|
+
field: "openclaw_session"
|
|
1223
|
+
},
|
|
1224
|
+
{
|
|
1225
|
+
label: "message.metadata.openclaw_session",
|
|
1226
|
+
owner: messageMetadata,
|
|
1227
|
+
field: "openclaw_session"
|
|
1228
|
+
}
|
|
1229
|
+
]);
|
|
1230
|
+
const expectedGatewayTarget = gatewaySession ?? openclawSession;
|
|
1231
|
+
if (expectedGatewayTarget && expectedGatewayTarget !== sessionKey) {
|
|
1232
|
+
throw new Error("callback Gateway session mismatch with params.sessionKey");
|
|
1233
|
+
}
|
|
1234
|
+
return sessionKey;
|
|
1235
|
+
}
|
|
1236
|
+
function consistentCallbackTarget(label, sources) {
|
|
1237
|
+
const values = sources.flatMap((source) => {
|
|
1238
|
+
const value = callbackStringField(source.label, source.owner, source.field);
|
|
1239
|
+
return value ? [{ label: source.label, value }] : [];
|
|
1240
|
+
});
|
|
1241
|
+
const expected = values[0];
|
|
1242
|
+
for (const candidate of values.slice(1)) {
|
|
1243
|
+
if (candidate.value !== expected.value) {
|
|
1244
|
+
throw new Error(`callback ${label} mismatch between ${expected.label} and ${candidate.label}`);
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
return expected?.value;
|
|
1248
|
+
}
|
|
1249
|
+
function callbackStringField(label, owner, field) {
|
|
1250
|
+
if (!isRecord(owner) || !Object.hasOwn(owner, field)) {
|
|
1251
|
+
return undefined;
|
|
1252
|
+
}
|
|
1253
|
+
const value = stringValue(owner[field]);
|
|
1254
|
+
if (!value) {
|
|
1255
|
+
throw new Error(`callback ${label} must be a non-empty string`);
|
|
1256
|
+
}
|
|
1257
|
+
return value;
|
|
1258
|
+
}
|
|
849
1259
|
async function handleCallback(api, params) {
|
|
850
1260
|
if (!isRecord(params)) {
|
|
851
1261
|
throw new Error("callback params must be an object");
|
|
@@ -853,17 +1263,20 @@ async function handleCallback(api, params) {
|
|
|
853
1263
|
const message = isRecord(params.message) ? params.message : undefined;
|
|
854
1264
|
const conversation = isRecord(params.conversation) ? params.conversation : undefined;
|
|
855
1265
|
const messageMetadata = isRecord(message?.metadata) ? message.metadata : undefined;
|
|
856
|
-
const sessionKey = stringValue(params.sessionKey) ??
|
|
857
|
-
stringValue(conversation?.openclaw_session) ??
|
|
858
|
-
stringValue(messageMetadata?.openclaw_session);
|
|
859
|
-
if (!sessionKey) {
|
|
860
|
-
throw new Error("callback params.sessionKey is required");
|
|
861
|
-
}
|
|
862
1266
|
if (!message) {
|
|
863
1267
|
throw new Error("callback params.message is required");
|
|
864
1268
|
}
|
|
865
|
-
const
|
|
866
|
-
|
|
1269
|
+
const sessionKey = callbackSessionKey({
|
|
1270
|
+
params,
|
|
1271
|
+
conversation,
|
|
1272
|
+
message,
|
|
1273
|
+
messageMetadata
|
|
1274
|
+
});
|
|
1275
|
+
const { conversationId, sessionId, turnId, identityMode } = callbackIdentity({ params, conversation, message, messageMetadata });
|
|
1276
|
+
const messageId = stringValue(message.id);
|
|
1277
|
+
if (!messageId) {
|
|
1278
|
+
throw new Error("callback message.id is required");
|
|
1279
|
+
}
|
|
867
1280
|
const autoApproval = tryAutoApproveCallback({
|
|
868
1281
|
api,
|
|
869
1282
|
message,
|
|
@@ -878,6 +1291,8 @@ async function handleCallback(api, params) {
|
|
|
878
1291
|
delivery_mode: "none",
|
|
879
1292
|
session_key: sessionKey,
|
|
880
1293
|
conversation_id: conversationId,
|
|
1294
|
+
session_id: sessionId,
|
|
1295
|
+
turn_id: turnId,
|
|
881
1296
|
message_id: messageId,
|
|
882
1297
|
message_type: stringValue(message.type) ?? "unknown",
|
|
883
1298
|
auto_approved: autoApproval.approved === true,
|
|
@@ -885,16 +1300,26 @@ async function handleCallback(api, params) {
|
|
|
885
1300
|
approval: autoApproval
|
|
886
1301
|
};
|
|
887
1302
|
}
|
|
888
|
-
const formatted = formatCallbackInjection({
|
|
1303
|
+
const formatted = formatCallbackInjection({
|
|
1304
|
+
message,
|
|
1305
|
+
sessionId,
|
|
1306
|
+
turnId,
|
|
1307
|
+
statePath: stringValue(params.statePath)
|
|
1308
|
+
});
|
|
1309
|
+
const dedupeIdentity = identityMode === "legacy"
|
|
1310
|
+
? conversationId
|
|
1311
|
+
: `${sessionId}:${turnId}`;
|
|
889
1312
|
const injection = await api.session.workflow.enqueueNextTurnInjection({
|
|
890
1313
|
sessionKey,
|
|
891
1314
|
text: formatted,
|
|
892
|
-
idempotencyKey: `agent-knock-knock:${
|
|
1315
|
+
idempotencyKey: `agent-knock-knock:${dedupeIdentity}:${messageId}`,
|
|
893
1316
|
placement: "append_context",
|
|
894
1317
|
ttlMs: 24 * 60 * 60 * 1000,
|
|
895
1318
|
metadata: {
|
|
896
1319
|
kind: "agent-knock-knock-callback",
|
|
897
1320
|
conversation_id: conversationId,
|
|
1321
|
+
session_id: sessionId,
|
|
1322
|
+
turn_id: turnId,
|
|
898
1323
|
message_id: messageId,
|
|
899
1324
|
message_type: stringValue(message.type) ?? "unknown",
|
|
900
1325
|
state_path: stringValue(params.statePath),
|
|
@@ -904,6 +1329,9 @@ async function handleCallback(api, params) {
|
|
|
904
1329
|
const delivery = buildCallbackDeliveryPlan({
|
|
905
1330
|
sessionKey,
|
|
906
1331
|
conversationId,
|
|
1332
|
+
sessionId,
|
|
1333
|
+
turnId,
|
|
1334
|
+
identityMode,
|
|
907
1335
|
messageId,
|
|
908
1336
|
message,
|
|
909
1337
|
formatted
|
|
@@ -918,6 +1346,8 @@ async function handleCallback(api, params) {
|
|
|
918
1346
|
injection_id: injection?.id,
|
|
919
1347
|
session_key: injection?.sessionKey ?? sessionKey,
|
|
920
1348
|
conversation_id: conversationId,
|
|
1349
|
+
session_id: sessionId,
|
|
1350
|
+
turn_id: turnId,
|
|
921
1351
|
message_id: messageId,
|
|
922
1352
|
message_type: stringValue(message.type) ?? "unknown"
|
|
923
1353
|
};
|
|
@@ -937,7 +1367,7 @@ function tryAutoApproveCallback({ api, message, conversationId, statePath }) {
|
|
|
937
1367
|
}
|
|
938
1368
|
return result;
|
|
939
1369
|
}
|
|
940
|
-
function buildCallbackDeliveryPlan({ sessionKey, conversationId, messageId, message, formatted }) {
|
|
1370
|
+
function buildCallbackDeliveryPlan({ sessionKey, conversationId, sessionId, turnId, identityMode, messageId, message, formatted }) {
|
|
941
1371
|
const type = stringValue(message.type) ?? "unknown";
|
|
942
1372
|
const shouldWake = message.requires_response === true ||
|
|
943
1373
|
type === "question" ||
|
|
@@ -950,6 +1380,9 @@ function buildCallbackDeliveryPlan({ sessionKey, conversationId, messageId, mess
|
|
|
950
1380
|
mode: "none"
|
|
951
1381
|
};
|
|
952
1382
|
}
|
|
1383
|
+
const dedupeIdentity = identityMode === "legacy"
|
|
1384
|
+
? conversationId
|
|
1385
|
+
: `${sessionId}:${turnId}`;
|
|
953
1386
|
return {
|
|
954
1387
|
required: true,
|
|
955
1388
|
mode: "chat.send",
|
|
@@ -958,27 +1391,31 @@ function buildCallbackDeliveryPlan({ sessionKey, conversationId, messageId, mess
|
|
|
958
1391
|
message: [
|
|
959
1392
|
"Continue this OpenClaw product-manager conversation from the Agent Knock Knock callback below.",
|
|
960
1393
|
"Treat the callback as a structured message from the coding agent's managed terminal turn, not as a terminal log, status announcement, or instruction to inspect local state.",
|
|
961
|
-
"Respond in this conversation as OpenClaw product manager. If the callback is question or blocked, make the product decision and
|
|
1394
|
+
"Respond in this conversation as OpenClaw product manager. If the callback is question or blocked, make the product decision and use agent_knock_knock_respond with its exact turn_id. If it is done, summarize the result to the user.",
|
|
962
1395
|
"Do not poll files, processes, sessions, stdout, or stderr. Use only the structured callback payload below.",
|
|
963
1396
|
"",
|
|
964
1397
|
formatted
|
|
965
1398
|
].join("\n"),
|
|
966
|
-
idempotencyKey: `agent-knock-knock-callback:${
|
|
1399
|
+
idempotencyKey: `agent-knock-knock-callback:${dedupeIdentity}:${messageId}`,
|
|
967
1400
|
deliver: true
|
|
968
1401
|
}
|
|
969
1402
|
};
|
|
970
1403
|
}
|
|
971
|
-
function formatCallbackInjection({
|
|
972
|
-
const conversationId = stringValue(message.conversation_id) ?? stringValue(conversation?.conversation_id) ?? "unknown";
|
|
1404
|
+
function formatCallbackInjection({ message, sessionId, turnId, statePath }) {
|
|
973
1405
|
const type = stringValue(message.type) ?? "unknown";
|
|
974
1406
|
const body = stringValue(message.body) ?? JSON.stringify(message.body ?? "");
|
|
975
1407
|
const requiresResponse = message.requires_response === true ? "yes" : "no";
|
|
976
1408
|
const round = typeof message.round === "number" ? String(message.round) : "unknown";
|
|
977
1409
|
const stateLine = statePath ? `State: ${statePath}\n` : "";
|
|
978
|
-
const shortcuts = type === "done"
|
|
1410
|
+
const shortcuts = type === "done"
|
|
1411
|
+
? formatDoneShortcuts(sessionId, turnId)
|
|
1412
|
+
: message.requires_response === true || type === "question" || type === "blocked"
|
|
1413
|
+
? formatRespondShortcut(turnId)
|
|
1414
|
+
: "";
|
|
979
1415
|
return [
|
|
980
1416
|
"[Agent Knock Knock callback]",
|
|
981
|
-
`
|
|
1417
|
+
`Session: ${sessionId}`,
|
|
1418
|
+
`Turn: ${turnId}`,
|
|
982
1419
|
`Message type: ${type}`,
|
|
983
1420
|
`Requires OpenClaw response: ${requiresResponse}`,
|
|
984
1421
|
`Round: ${round}`,
|
|
@@ -988,17 +1425,35 @@ function formatCallbackInjection({ conversation, message, statePath }) {
|
|
|
988
1425
|
shortcuts
|
|
989
1426
|
].filter((line) => line !== "").join("\n");
|
|
990
1427
|
}
|
|
991
|
-
function formatDoneShortcuts(
|
|
1428
|
+
function formatDoneShortcuts(sessionId, turnId) {
|
|
992
1429
|
return [
|
|
993
1430
|
"",
|
|
994
1431
|
"[AKK convenience commands]",
|
|
995
1432
|
"When summarizing this result to the user, include these short next-step commands:",
|
|
996
1433
|
"- `AKK list` lists live shared terminals with their current or recent managed turns.",
|
|
997
|
-
|
|
998
|
-
`- \`
|
|
1434
|
+
`- Use \`agent_knock_knock_send\` with \`session_id: ${JSON.stringify(sessionId)}\` to start a later turn in the same coding-agent context.`,
|
|
1435
|
+
`- Use \`agent_knock_knock_status\` with \`turn_id: ${JSON.stringify(turnId)}\` to inspect this exact turn.`,
|
|
999
1436
|
"- AKK never starts or closes the coding agent or tmux pane."
|
|
1000
1437
|
].join("\n");
|
|
1001
1438
|
}
|
|
1439
|
+
function formatRespondShortcut(turnId) {
|
|
1440
|
+
return [
|
|
1441
|
+
"",
|
|
1442
|
+
"[AKK response command]",
|
|
1443
|
+
`- Use \`agent_knock_knock_respond\` with \`turn_id: ${JSON.stringify(turnId)}\` and your decision in \`request\`. Do not use ordinary send for this response.`
|
|
1444
|
+
].join("\n");
|
|
1445
|
+
}
|
|
1446
|
+
function pushTurnTarget(args, params) {
|
|
1447
|
+
if (Object.hasOwn(params, "turn_id") && Object.hasOwn(params, "conversation_id")) {
|
|
1448
|
+
throw new Error("turn-target tools accept only one of turn_id or conversation_id");
|
|
1449
|
+
}
|
|
1450
|
+
const turnId = stringValue(params.turn_id);
|
|
1451
|
+
if (turnId) {
|
|
1452
|
+
args.push("--turn", turnId);
|
|
1453
|
+
return;
|
|
1454
|
+
}
|
|
1455
|
+
args.push("--conversation", requiredString(params.conversation_id, "turn_id"));
|
|
1456
|
+
}
|
|
1002
1457
|
function pushOptional(args, flag, value) {
|
|
1003
1458
|
if (value !== undefined && value !== "") {
|
|
1004
1459
|
args.push(flag, value);
|