@scotthuang/agent-knock-knock 0.12.12 → 0.12.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +39 -26
  3. package/dist/src/approval-policy.d.ts +11 -2
  4. package/dist/src/approval-policy.js +33 -7
  5. package/dist/src/approval-policy.js.map +1 -1
  6. package/dist/src/claude-local-transcript-provider.d.ts +81 -0
  7. package/dist/src/claude-local-transcript-provider.js +854 -18
  8. package/dist/src/claude-local-transcript-provider.js.map +1 -1
  9. package/dist/src/cli-core.js +85 -0
  10. package/dist/src/cli-core.js.map +1 -1
  11. package/dist/src/deferred-foreground-authority-cli-adapter.js +90 -19
  12. package/dist/src/deferred-foreground-authority-cli-adapter.js.map +1 -1
  13. package/dist/src/deferred-foreground-transfer.js +3 -4
  14. package/dist/src/deferred-foreground-transfer.js.map +1 -1
  15. package/dist/src/openclaw-callback-transport.d.ts +5 -0
  16. package/dist/src/openclaw-callback-transport.js +1 -1
  17. package/dist/src/openclaw-callback-transport.js.map +1 -1
  18. package/dist/src/openclaw-plugin-callback-adapter.js +96 -14
  19. package/dist/src/openclaw-plugin-callback-adapter.js.map +1 -1
  20. package/dist/src/openclaw-plugin-command-adapter.js +548 -107
  21. package/dist/src/openclaw-plugin-command-adapter.js.map +1 -1
  22. package/dist/src/openclaw-plugin-helpers.d.ts +27 -4
  23. package/dist/src/openclaw-plugin-helpers.js +309 -44
  24. package/dist/src/openclaw-plugin-helpers.js.map +1 -1
  25. package/dist/src/openclaw-plugin-schemas.d.ts +44 -68
  26. package/dist/src/openclaw-plugin-schemas.js +63 -91
  27. package/dist/src/openclaw-plugin-schemas.js.map +1 -1
  28. package/dist/src/openclaw-plugin-supervisor.js +48 -10
  29. package/dist/src/openclaw-plugin-supervisor.js.map +1 -1
  30. package/dist/src/openclaw-private-authority-offers.d.ts +22 -0
  31. package/dist/src/openclaw-private-authority-offers.js +177 -0
  32. package/dist/src/openclaw-private-authority-offers.js.map +1 -0
  33. package/dist/src/store.d.ts +1 -0
  34. package/dist/src/store.js +3 -1
  35. package/dist/src/store.js.map +1 -1
  36. package/dist/src/terminal-action-projection.d.ts +19 -1
  37. package/dist/src/terminal-action-projection.js +23 -0
  38. package/dist/src/terminal-action-projection.js.map +1 -1
  39. package/dist/src/terminal-command-cli-adapter.d.ts +5 -0
  40. package/dist/src/terminal-command-cli-adapter.js +249 -89
  41. package/dist/src/terminal-command-cli-adapter.js.map +1 -1
  42. package/dist/src/terminal-dispatch-repository-cli-adapter.js +305 -79
  43. package/dist/src/terminal-dispatch-repository-cli-adapter.js.map +1 -1
  44. package/dist/src/terminal-list-cli-adapter.d.ts +31 -7
  45. package/dist/src/terminal-list-cli-adapter.js +285 -67
  46. package/dist/src/terminal-list-cli-adapter.js.map +1 -1
  47. package/dist/src/terminal-list-renderer.d.ts +2 -0
  48. package/dist/src/terminal-list-renderer.js +116 -41
  49. package/dist/src/terminal-list-renderer.js.map +1 -1
  50. package/dist/src/terminal-monitor-cli-adapter.js +1 -1
  51. package/dist/src/terminal-monitor-cli-adapter.js.map +1 -1
  52. package/dist/src/terminal-monitor-state-cli-adapter.js +6 -15
  53. package/dist/src/terminal-monitor-state-cli-adapter.js.map +1 -1
  54. package/dist/src/terminal-status-cli-adapter.d.ts +10 -0
  55. package/dist/src/terminal-status-cli-adapter.js +89 -7
  56. package/dist/src/terminal-status-cli-adapter.js.map +1 -1
  57. package/dist/src/terminal-submission-acceptance.d.ts +60 -1
  58. package/dist/src/terminal-submission-acceptance.js +560 -0
  59. package/dist/src/terminal-submission-acceptance.js.map +1 -1
  60. package/dist/src/terminal-watch-callback-cli-adapter.d.ts +20 -0
  61. package/dist/src/terminal-watch-callback-cli-adapter.js +55 -0
  62. package/dist/src/terminal-watch-callback-cli-adapter.js.map +1 -0
  63. package/dist/src/terminal-watch-cli-adapter.d.ts +64 -0
  64. package/dist/src/terminal-watch-cli-adapter.js +508 -0
  65. package/dist/src/terminal-watch-cli-adapter.js.map +1 -0
  66. package/dist/src/terminal-watch-service.d.ts +97 -0
  67. package/dist/src/terminal-watch-service.js +714 -0
  68. package/dist/src/terminal-watch-service.js.map +1 -0
  69. package/dist/src/terminal-watch-store.d.ts +149 -0
  70. package/dist/src/terminal-watch-store.js +861 -0
  71. package/dist/src/terminal-watch-store.js.map +1 -0
  72. package/docs/quickstart-herdr.md +8 -0
  73. package/docs/quickstart-tmux.md +33 -6
  74. package/openclaw.plugin.json +11 -3
  75. package/package.json +1 -1
  76. package/templates/openclaw-skills/agent-knock-knock/SKILL.md +57 -39
@@ -1,5 +1,161 @@
1
1
  import path from "node:path";
2
2
  import { recordValue } from "./value-guards.js";
3
+ const MODEL_FACING_PRIVATE_AUTHORITY_PARTS = [
4
+ ["expected", "terminal", "token"],
5
+ ["expected", "binding", "token"],
6
+ ["expected", "handoff", "token"],
7
+ ["candidate", "token"],
8
+ ["expected", "approval", "fingerprint"],
9
+ ["approval", "fingerprint"],
10
+ ["expected", "session", "revision"],
11
+ ["lifecycle", "binding", "token"],
12
+ ["binding", "token"],
13
+ ["binding", "id"],
14
+ ["binding", "generation"],
15
+ ["terminal", "binding", "id"],
16
+ ["terminal", "binding", "generation"],
17
+ ["selection", "handle"],
18
+ ["selection", "snapshot"],
19
+ ["selection", "scope"],
20
+ ["expected", "callback", "conversation", "id"],
21
+ ["expected", "callback", "session", "id"],
22
+ ["expected", "callback", "turn", "id"],
23
+ ["expected", "callback", "message", "id"],
24
+ ["expected", "callback", "openclaw", "session"]
25
+ ];
26
+ // The optional separator accepts the three spellings that can be emitted by
27
+ // old CLI/JSON diagnostics: snake_case, kebab-case, and camelCase. These are
28
+ // exact AKK authority names rather than generic words such as "token" or
29
+ // "revision", so ordinary user and agent text remains intact.
30
+ const MODEL_FACING_PRIVATE_AUTHORITY_NAME = `(?:${MODEL_FACING_PRIVATE_AUTHORITY_PARTS
31
+ .map((parts) => parts.join("[_-]?"))
32
+ .join("|")})`;
33
+ const MODEL_FACING_SCALAR = `(?:"(?:\\\\.|[^"\\\\])*"|'(?:\\\\.|[^'\\\\])*'|` +
34
+ "`(?:\\\\.|[^`\\\\])*`|true|false|null|" +
35
+ `-?(?:\\d+(?:\\.\\d*)?|\\.\\d+)(?:[eE][+-]?\\d+)?|[^\\s,;}\\]]+)`;
36
+ const MODEL_FACING_PRIVATE_AUTHORITY_JSON = new RegExp(`"${MODEL_FACING_PRIVATE_AUTHORITY_NAME}"\\s*:\\s*${MODEL_FACING_SCALAR}`, "giu");
37
+ const MODEL_FACING_PRIVATE_AUTHORITY_CLI = new RegExp(`--${MODEL_FACING_PRIVATE_AUTHORITY_NAME}(?:=|\\s+)${MODEL_FACING_SCALAR}`, "giu");
38
+ const MODEL_FACING_PRIVATE_AUTHORITY_ASSIGNMENT = new RegExp(`(^|[^A-Za-z0-9])${MODEL_FACING_PRIVATE_AUTHORITY_NAME}` +
39
+ `\\s*(?:=|:)\\s*${MODEL_FACING_SCALAR}`, "gimu");
40
+ const MODEL_FACING_PRIVATE_AUTHORITY_LINE = new RegExp(`^\\s*(?:[-*]\\s*)?(?:` +
41
+ `(?:["']?${MODEL_FACING_PRIVATE_AUTHORITY_NAME}["']?\\s*(?:=|:))|` +
42
+ `(?:--${MODEL_FACING_PRIVATE_AUTHORITY_NAME}(?:=|\\s+)))`, "iu");
43
+ const LEGACY_APPROVAL_TAIL_MARKER = "If the user approves, call `agent_knock_knock_approve`";
44
+ const LEGACY_APPROVAL_TERMINAL_INSTRUCTION = "Do not use raw tmux, shell, or manual key presses for this approval. Do not approve without explicit user confirmation.";
45
+ const MODEL_OPAQUE_AUTHORITY_FIELDS = new Set([
46
+ "acceptanceevidence",
47
+ "candidaterollouts",
48
+ "claudetranscriptanchor",
49
+ "codexopenrootrolloutinventory",
50
+ "codexrolloutacceptanceanchor",
51
+ "challenge",
52
+ "etag",
53
+ "expectedapprovalfingerprint",
54
+ "expectedcallbackconversationid",
55
+ "expectedcallbackmessageid",
56
+ "expectedcallbackopenclawsession",
57
+ "expectedcallbacksessionid",
58
+ "expectedcallbackturnid",
59
+ "generationid",
60
+ "livenativethreadid",
61
+ "nativesessiontakeover",
62
+ "nonce",
63
+ "proof",
64
+ "selectionhandle",
65
+ "selectionsnapshot",
66
+ "selectionscope",
67
+ "snapshotid"
68
+ ]);
69
+ export function normalizeAkkModelFacingFieldName(key) {
70
+ return key.toLowerCase().replace(/[^a-z0-9]/gu, "");
71
+ }
72
+ export function isAkkModelFacingPrivateAuthorityField(key) {
73
+ const compact = normalizeAkkModelFacingFieldName(key);
74
+ return compact.endsWith("token") ||
75
+ compact.endsWith("fingerprint") ||
76
+ compact.endsWith("hash") ||
77
+ compact.endsWith("sha256") ||
78
+ compact.endsWith("digest") ||
79
+ compact.endsWith("revision") ||
80
+ compact.endsWith("revisions") ||
81
+ compact.endsWith("bindingid") ||
82
+ compact.endsWith("bindingids") ||
83
+ compact.endsWith("bindinggeneration") ||
84
+ compact.endsWith("bindinggenerations") ||
85
+ compact === "generation" ||
86
+ MODEL_OPAQUE_AUTHORITY_FIELDS.has(compact);
87
+ }
88
+ export function isAkkModelFacingDiagnosticField(key) {
89
+ if (!key)
90
+ return false;
91
+ const separated = key
92
+ .replace(/([a-z0-9])([A-Z])/gu, "$1_$2")
93
+ .toLowerCase();
94
+ return /(?:^|[_-])(?:reasons?|errors?|warnings?|diagnostics?)$/u.test(separated) || /(?:^|[_-])(?:error|warning|diagnostic)_message$/u.test(separated);
95
+ }
96
+ function sanitizeAkkModelFacingEmbeddedAuthorityText(value) {
97
+ return value
98
+ .replace(MODEL_FACING_PRIVATE_AUTHORITY_JSON, '"internal_authority":"[retained by AKK]"')
99
+ .replace(MODEL_FACING_PRIVATE_AUTHORITY_CLI, "[AKK internal authority omitted]")
100
+ .replace(MODEL_FACING_PRIVATE_AUTHORITY_ASSIGNMENT, (_match, prefix) => `${prefix}[AKK internal authority omitted]`);
101
+ }
102
+ /**
103
+ * Remove only AKK-generated pre-v18 authority instructions from callback/event
104
+ * text. Exact authority-looking text outside one of these fixed machine-owned
105
+ * forms is business content and must remain byte-for-byte visible.
106
+ */
107
+ export function sanitizeAkkModelFacingLegacyAuthorityInstructionText(value) {
108
+ const reviewOnly = stripAkkLegacyApprovalInstructionTail(value);
109
+ let machineAuthorityBlock = false;
110
+ return reviewOnly.split("\n").map((line) => {
111
+ const text = line.endsWith("\r") ? line.slice(0, -1) : line;
112
+ const carriageReturn = line.endsWith("\r") ? "\r" : "";
113
+ if (/^\s*(?:\[?AKK\]?\s+)?(?:approval|internal) authority\s*:?\s*$/iu
114
+ .test(text)) {
115
+ machineAuthorityBlock = true;
116
+ return line;
117
+ }
118
+ if (text.trim().length === 0) {
119
+ machineAuthorityBlock = false;
120
+ return line;
121
+ }
122
+ const fixedCommand = /^\s*(?:equivalent (?:user )?command|AKK (?:approval )?command)\s*:/iu
123
+ .test(text) && containsEmbeddedPrivateAuthority(text);
124
+ const blockAuthority = machineAuthorityBlock &&
125
+ containsEmbeddedPrivateAuthority(text);
126
+ if (fixedCommand || blockAuthority) {
127
+ return `${text.match(/^\s*/u)?.[0] ?? ""}` +
128
+ `[AKK internal authority omitted]${carriageReturn}`;
129
+ }
130
+ if (machineAuthorityBlock)
131
+ machineAuthorityBlock = false;
132
+ return line;
133
+ }).join("\n");
134
+ }
135
+ /** Remove only the exact legacy approval instruction appended by AKK. */
136
+ export function stripAkkLegacyApprovalInstructionTail(value) {
137
+ const markerIndex = value.lastIndexOf(LEGACY_APPROVAL_TAIL_MARKER);
138
+ const markerSuffix = markerIndex === -1 ? "" : value.slice(markerIndex);
139
+ const fixedTail = markerIndex !== -1 && (markerSuffix.trimEnd().endsWith(LEGACY_APPROVAL_TERMINAL_INSTRUCTION) ||
140
+ markerSuffix.split(/\r?\n/u).some(isLegacyMachineAuthorityLine));
141
+ return fixedTail ? value.slice(0, markerIndex) : value;
142
+ }
143
+ function isLegacyMachineAuthorityLine(line) {
144
+ return containsEmbeddedPrivateAuthority(line) && (MODEL_FACING_PRIVATE_AUTHORITY_LINE.test(line) ||
145
+ /^\s*(?:equivalent (?:user )?command|AKK (?:approval )?command)\s*:/iu
146
+ .test(line));
147
+ }
148
+ function containsEmbeddedPrivateAuthority(value) {
149
+ return sanitizeAkkModelFacingEmbeddedAuthorityText(value) !== value;
150
+ }
151
+ export function sanitizeAkkModelFacingDiagnosticText(message) {
152
+ const sanitized = sanitizeAkkModelFacingEmbeddedAuthorityText(message);
153
+ if (sanitized !== message ||
154
+ /(?:\bexpected\s+(?:session\s+)?revision\b|\bactual\s+(?:session\s+)?revision\b|\b(?:terminal|binding|handoff|candidate)\s+token\b|\bapproval\s+fingerprint\b|\bbinding\s+(?:id|generation)\b|\bcompare-and-swap\b|\bCAS\b)/iu.test(message)) {
155
+ return "AKK's private authority changed or could not be verified. Refresh AKK list/status and retry only the currently advertised semantic action.";
156
+ }
157
+ return sanitized;
158
+ }
3
159
  export const AKK_CALLBACK_METHOD = "agent-knock-knock.callback";
4
160
  export function parseAkkCommand(args) {
5
161
  const input = String(args ?? "").trim();
@@ -33,6 +189,23 @@ function parseAkkLifecycleCommand(action, rest) {
33
189
  }
34
190
  return { action: "doctor" };
35
191
  }
192
+ if (action === "watch") {
193
+ const usage = "Usage: /akk watch <exact-terminal-id>";
194
+ const { token: terminalId, rest: extra } = takeRequiredToken(rest, usage);
195
+ assertExactTerminalId(terminalId, usage);
196
+ if (extra.trim()) {
197
+ throw new Error(usage);
198
+ }
199
+ return { action: "watch", terminalId };
200
+ }
201
+ if (action === "unwatch") {
202
+ const usage = "Usage: /akk unwatch <watch-id>";
203
+ const { token: watchId, rest: extra } = takeRequiredToken(rest, usage);
204
+ if (extra.trim() || !isTerminalWatchId(watchId)) {
205
+ throw new Error(usage);
206
+ }
207
+ return { action: "unwatch", watchId };
208
+ }
36
209
  if (action === "threads" || action === "list-resumable-threads") {
37
210
  const usage = "Usage: /akk threads <exact-terminal-id>";
38
211
  const { token: terminalId, rest: extra } = takeRequiredToken(rest, usage);
@@ -53,7 +226,7 @@ function parseAkkLifecycleCommand(action, rest) {
53
226
  }
54
227
  if (action === "resume-thread") {
55
228
  const usage = "Usage: /akk resume-thread <exact-terminal-id> " +
56
- "[native-thread-uuid|previous|刚才那个|number|@short-id|snapshot-handle]";
229
+ "[native-thread-uuid|previous|刚才那个|number|@short-id]";
57
230
  const { token: terminalId, rest: nativeInput } = takeRequiredToken(rest, usage);
58
231
  assertExactTerminalId(terminalId, usage);
59
232
  const { token: selectionInput, rest: extra } = takeToken(nativeInput);
@@ -77,9 +250,11 @@ function parseAkkTurnCommand(action, rest) {
77
250
  if (action === "status" || action === "show") {
78
251
  const { token: turnId, rest: extra } = takeToken(rest);
79
252
  if (extra.trim()) {
80
- throw new Error("Usage: /akk status [turn-selector]");
253
+ throw new Error("Usage: /akk status [turn-selector|terminal-watch-id]");
81
254
  }
82
- return { action: "status", turnId: turnId || undefined };
255
+ return turnId && isTerminalWatchId(turnId)
256
+ ? { action: "status", watchId: turnId }
257
+ : { action: "status", turnId: turnId || undefined };
83
258
  }
84
259
  if (action === "describe" || action === "summary" || action === "about") {
85
260
  throw new Error("AKK describe was removed; use /akk status [turn-selector]");
@@ -99,15 +274,13 @@ function parseAkkTurnCommand(action, rest) {
99
274
  throw new Error("AKK reply syntax changed; use /akk respond <turn-selector>: <answer>");
100
275
  }
101
276
  if (action === "approve") {
102
- const { token: turnId, rest: approvalInput } = takeRequiredToken(rest, "Usage: /akk approve <turn-selector> --expected-approval-fingerprint <fingerprint>");
103
- const approval = /^--expected-approval-fingerprint\s+(\S+)$/u.exec(approvalInput.trim());
104
- if (!approval) {
105
- throw new Error("Usage: /akk approve <turn-selector> --expected-approval-fingerprint <fingerprint>");
277
+ const { token: turnId, rest: approvalInput } = takeRequiredToken(rest, "Usage: /akk approve <turn-selector>");
278
+ if (approvalInput.trim()) {
279
+ throw new Error("Usage: /akk approve <turn-selector>");
106
280
  }
107
281
  return {
108
282
  action: "approve",
109
- turnId,
110
- expectedApprovalFingerprint: approval[1]
283
+ turnId
111
284
  };
112
285
  }
113
286
  if (action === "cancel" || action === "stop") {
@@ -173,21 +346,26 @@ export function akkUsageText() {
173
346
  "/akk claude: <request>",
174
347
  "/akk <session-selector>: <message>",
175
348
  "/akk list",
349
+ "/akk watch <exact-terminal-id>",
350
+ "/akk unwatch <watch-id>",
176
351
  "/akk threads <exact-terminal-id>",
177
352
  "/akk new-thread <exact-terminal-id>",
178
353
  "/akk clear-thread <exact-terminal-id>",
179
- "/akk resume-thread <exact-terminal-id> [uuid|previous|number|@short-id|snapshot-handle]",
354
+ "/akk resume-thread <exact-terminal-id> [uuid|previous|number|@short-id]",
180
355
  "/akk doctor",
181
- "/akk status [turn-selector]",
356
+ "/akk status [turn-selector|terminal-watch-id]",
182
357
  "/akk respond <turn-selector>: <answer>",
183
- "/akk approve <turn-selector> --expected-approval-fingerprint <fingerprint>",
358
+ "/akk approve <turn-selector>",
184
359
  "/akk cancel <turn-selector>"
185
360
  ].join("\n");
186
361
  }
187
362
  export function formatAkkListCommandResult(result) {
188
363
  const terminals = arrayValue(result.terminals);
364
+ const terminalWatches = arrayValue(result.terminal_watches);
189
365
  const unavailableManagedTurns = arrayValue(result.unavailable_managed_turns);
190
- if (terminals.length === 0 && unavailableManagedTurns.length === 0) {
366
+ if (terminals.length === 0 &&
367
+ terminalWatches.length === 0 &&
368
+ unavailableManagedTurns.length === 0) {
191
369
  return "AKK found no live terminals or unavailable managed turns.";
192
370
  }
193
371
  const terminalLines = terminals.slice(0, 20).flatMap((terminal) => {
@@ -207,11 +385,15 @@ export function formatAkkListCommandResult(result) {
207
385
  "new_thread",
208
386
  "resume_thread"
209
387
  ].some((name) => Object.hasOwn(availableActions, name));
388
+ const hasWatchAction = Object.hasOwn(availableActions, "watch");
210
389
  return [
211
390
  `- ${formatTerminalLine(terminal)}`,
212
391
  ...(hasLifecycleAction && terminalId
213
392
  ? [` lifecycle terminal_id: ${terminalId}`]
214
393
  : []),
394
+ ...(hasWatchAction && terminalId
395
+ ? [` AKK Watch available: /akk watch ${terminalId} — monitor this human-started task and receive attention/completion callbacks without polling.`]
396
+ : []),
215
397
  ...(sessionId || sessionShortRef
216
398
  ? [` AKK session: ${sessionShortRef ?? sessionId}`]
217
399
  : []),
@@ -246,11 +428,21 @@ export function formatAkkListCommandResult(result) {
246
428
  ...(recovery ? [` recovery: ${recovery}`] : [])
247
429
  ];
248
430
  });
431
+ const watchLines = terminalWatches.slice(0, 20).flatMap((watch) => [
432
+ `- ${formatTerminalWatchLine(watch)}`,
433
+ ...formatAvailableActions(" actions", watch)
434
+ ]);
435
+ const heading = terminalWatches.length > 0
436
+ ? `AKK terminals (${terminals.length} live, ${terminalWatches.length} terminal watches, ${unavailableManagedTurns.length} unavailable managed turns):`
437
+ : `AKK terminals (${terminals.length} live, ${unavailableManagedTurns.length} unavailable managed turns):`;
249
438
  return [
250
- `AKK terminals (${terminals.length} live, ${unavailableManagedTurns.length} unavailable managed turns):`,
439
+ heading,
251
440
  ...(terminalLines.length > 0
252
441
  ? ["live terminals:", ...terminalLines]
253
442
  : []),
443
+ ...(watchLines.length > 0
444
+ ? ["terminal watches:", ...watchLines]
445
+ : []),
254
446
  ...(unavailableManagedTurns.length > 0
255
447
  ? [
256
448
  "unavailable managed turns:",
@@ -262,6 +454,63 @@ export function formatAkkListCommandResult(result) {
262
454
  : [])
263
455
  ].join("\n");
264
456
  }
457
+ export function formatAkkWatchCommandResult(result) {
458
+ const watch = terminalWatchRecord(result);
459
+ return [
460
+ "AKK Terminal Watch started:",
461
+ `watch: ${nonEmptyString(watch.watch_id) ?? "unknown"}`,
462
+ `terminal: ${nonEmptyString(watch.terminal_id) ?? "unknown"}`,
463
+ `agent: ${nonEmptyString(watch.agent) ?? "unknown"}`,
464
+ `status: ${nonEmptyString(watch.status) ?? "watching"}`,
465
+ "AKK did not send or adopt this task; it is observing work the human started in the terminal."
466
+ ].join("\n");
467
+ }
468
+ export function formatAkkUnwatchCommandResult(result) {
469
+ const watch = terminalWatchRecord(result);
470
+ return [
471
+ "AKK Terminal Watch stopped:",
472
+ `watch: ${nonEmptyString(watch.watch_id) ?? "unknown"}`,
473
+ `status: ${nonEmptyString(watch.status) ?? "cancelled"}`,
474
+ "The terminal and its task were not interrupted."
475
+ ].join("\n");
476
+ }
477
+ export function formatAkkWatchStatusCommandResult(result) {
478
+ const watch = terminalWatchRecord(result);
479
+ const settlement = recordValue(watch.settlement) ?? {};
480
+ const reason = nonEmptyString(settlement.reason_code) ??
481
+ nonEmptyString(watch.reason);
482
+ const completionText = nonEmptyString(settlement.completion_text);
483
+ return [
484
+ "AKK Terminal Watch status:",
485
+ `watch: ${nonEmptyString(watch.watch_id) ?? "unknown"}`,
486
+ `terminal: ${nonEmptyString(watch.terminal_id) ?? "unknown"}`,
487
+ `agent: ${nonEmptyString(watch.agent) ?? "unknown"}`,
488
+ `status: ${nonEmptyString(watch.status) ?? "unknown"}`,
489
+ ...(reason
490
+ ? [`reason: ${reason}`]
491
+ : []),
492
+ ...(completionText
493
+ ? [`completion: ${truncateText(completionText, 500)}`]
494
+ : []),
495
+ "This is observed external work; AKK did not send or adopt the task."
496
+ ].join("\n");
497
+ }
498
+ export function formatAkkTerminalWatchHint(result) {
499
+ const hint = recordValue(result.terminal_watch_hint);
500
+ if (hint?.kind !== "terminal_watch_discovery" ||
501
+ hint.available_action_required !== true) {
502
+ return [];
503
+ }
504
+ const command = nonEmptyString(hint.command);
505
+ const instruction = nonEmptyString(hint.instruction);
506
+ if (!command || !instruction) {
507
+ return [];
508
+ }
509
+ return [
510
+ `AKK Watch available: ${command}`,
511
+ `next: ${instruction}`
512
+ ];
513
+ }
265
514
  export function formatAkkRespondCommandResult(result) {
266
515
  const conversation = recordValue(result.conversation) ?? {};
267
516
  const compatibilityId = nonEmptyString(conversation.conversation_id) ??
@@ -357,7 +606,6 @@ export function formatAkkThreadsCommandResult(result) {
357
606
  const nativeThreadId = nonEmptyString(thread.native_thread_id) ?? "unknown";
358
607
  const selectionNumber = finiteNumber(thread.selection_number);
359
608
  const shortId = nonEmptyString(thread.short_id);
360
- const selectionHandle = nonEmptyString(thread.selection_handle);
361
609
  const status = thread.resumable === true
362
610
  ? "resumable"
363
611
  : nonEmptyString(thread.unavailable_reason) ?? "unavailable";
@@ -365,11 +613,10 @@ export function formatAkkThreadsCommandResult(result) {
365
613
  nonEmptyString(thread.title),
366
614
  nonEmptyString(thread.preview)
367
615
  ].filter((value) => Boolean(value)).join(" · ");
368
- const selector = selectionNumber !== undefined || shortId || selectionHandle
616
+ const selector = selectionNumber !== undefined || shortId
369
617
  ? [
370
618
  selectionNumber !== undefined ? `${selectionNumber}.` : undefined,
371
- shortId,
372
- selectionHandle ? `[${selectionHandle}]` : undefined
619
+ shortId
373
620
  ].filter(Boolean).join(" ")
374
621
  : `- ${nativeThreadId}`;
375
622
  const previousMarker = nativeThreadId === previousNativeThreadId
@@ -398,7 +645,7 @@ export function formatAkkThreadsCommandResult(result) {
398
645
  ...(resumableCount > 0
399
646
  ? [
400
647
  `next: /akk resume-thread ${terminalId} <native-thread-uuid>`,
401
- `shortcuts: /akk resume-thread ${terminalId} <number|@short-id|snapshot-handle>`,
648
+ `shortcuts: /akk resume-thread ${terminalId} <number|@short-id>`,
402
649
  ...(previousNativeThreadId
403
650
  ? [`previous: /akk resume-thread ${terminalId} previous`]
404
651
  : []),
@@ -422,7 +669,9 @@ export function formatAkkThreadTransitionCommandResult(result) {
422
669
  ? [`transition: ${nonEmptyString(result.transition_id)}`]
423
670
  : []),
424
671
  ...(nonEmptyString(result.reason)
425
- ? [`reason: ${nonEmptyString(result.reason)}`]
672
+ ? [
673
+ `reason: ${sanitizeAkkModelFacingDiagnosticText(nonEmptyString(result.reason) ?? "")}`
674
+ ]
426
675
  : []),
427
676
  "No AKK Turn was created.",
428
677
  "Next: do not retry automatically; refresh /akk list and use only its exact lifecycle recovery action."
@@ -448,9 +697,6 @@ export function formatAkkThreadTransitionCommandResult(result) {
448
697
  ]
449
698
  : []),
450
699
  `native thread: ${nonEmptyString(result.native_thread_id) ?? "unknown"}`,
451
- ...(finiteNumber(result.binding_generation) !== undefined
452
- ? [`binding generation: ${finiteNumber(result.binding_generation)}`]
453
- : []),
454
700
  "No AKK Turn was created. The next ordinary send creates the first Turn in this native context."
455
701
  ].join("\n");
456
702
  }
@@ -472,6 +718,20 @@ export function buildAkkCommandCliArgs(command, config, context = {}) {
472
718
  switch (command.action) {
473
719
  case "list":
474
720
  return withOptionalArgs(["list", "--reconcile"], ["--store-dir", storeDir], ["--idle-timeout-minutes", idleTimeoutMinutes]);
721
+ case "watch": {
722
+ const openclawSession = nonEmptyString(context.sessionKey) ??
723
+ "agent:main:main";
724
+ return withOptionalArgs([
725
+ "watch-terminal",
726
+ "--terminal",
727
+ command.terminalId
728
+ ], ["--store-dir", storeDir], [
729
+ "--hard-timeout-minutes",
730
+ finiteNumberString(config.agentHardTimeoutMinutes)
731
+ ], ["--openclaw-session", openclawSession], ["--openclaw-bin", nonEmptyString(config.openclawBin)]);
732
+ }
733
+ case "unwatch":
734
+ return withOptionalArgs(["unwatch-terminal", "--watch", command.watchId], ["--store-dir", storeDir]);
475
735
  case "list-resumable-threads":
476
736
  return withOptionalArgs([
477
737
  "list-resumable-threads",
@@ -520,17 +780,6 @@ export function buildAkkCommandCliArgs(command, config, context = {}) {
520
780
  requiredSelectionScope(context.selectionScope)
521
781
  ], ["--store-dir", storeDir], ["--codex-home", codexHome]);
522
782
  }
523
- if (command.selection.kind === "snapshot-handle") {
524
- return withOptionalArgs([
525
- "resume-thread",
526
- "--terminal",
527
- command.terminalId,
528
- "--selection-handle",
529
- command.selection.selectionHandle,
530
- "--selection-scope",
531
- requiredSelectionScope(context.selectionScope)
532
- ], ["--store-dir", storeDir], ["--codex-home", codexHome]);
533
- }
534
783
  return withOptionalArgs([
535
784
  "resume-thread",
536
785
  "--terminal",
@@ -543,6 +792,9 @@ export function buildAkkCommandCliArgs(command, config, context = {}) {
543
792
  requiredCandidateToken(context.candidateToken)
544
793
  ], ["--store-dir", storeDir], ["--codex-home", codexHome]);
545
794
  case "status":
795
+ if (command.watchId) {
796
+ return withOptionalArgs(["watch-status", "--watch", command.watchId], ["--store-dir", storeDir]);
797
+ }
546
798
  return withOptionalArgs([
547
799
  "status",
548
800
  "--reconcile",
@@ -571,13 +823,9 @@ export function buildAkkCommandCliArgs(command, config, context = {}) {
571
823
  command.message
572
824
  ], ["--store-dir", storeDir]);
573
825
  case "approve":
574
- return withOptionalArgs([
575
- "approve",
576
- "--turn",
577
- command.turnId,
578
- "--expected-approval-fingerprint",
579
- command.expectedApprovalFingerprint
580
- ], ["--store-dir", storeDir]);
826
+ // The OpenClaw command adapter resolves the current private approval
827
+ // authority immediately before invoking the CLI.
828
+ return undefined;
581
829
  case "renew":
582
830
  return withOptionalArgs(["renew", "--turn", command.turnId], [
583
831
  "--minutes",
@@ -598,6 +846,9 @@ export function buildAkkCommandCliArgs(command, config, context = {}) {
598
846
  ], ["--expected-message-id", command.expectedMessageId], ["--expected-transition-id", command.expectedTransitionId], ["--store-dir", storeDir]);
599
847
  }
600
848
  }
849
+ function isTerminalWatchId(value) {
850
+ return /^terminal-watch-[A-Za-z0-9._:-]+$/u.test(value);
851
+ }
601
852
  function parseSelectorMessage(input) {
602
853
  const match = /^((?:(?:codex|claude):latest|only|latest|codex|claude|@[0-9a-f]+))\s*:\s*([\s\S]*)$/iu.exec(input);
603
854
  const message = match?.[2]?.trim();
@@ -646,9 +897,6 @@ function parseAkkResumeSelection(value, usage) {
646
897
  if (/^@[a-f0-9]{8,32}$/u.test(normalized)) {
647
898
  return { kind: "short-id", shortId: normalized };
648
899
  }
649
- if (/^rs_[A-Za-z0-9_-]{22}:[1-9][0-9]*$/u.test(value)) {
650
- return { kind: "snapshot-handle", selectionHandle: value };
651
- }
652
900
  throw new Error(`${usage}; use a complete UUID or a selection exactly returned by /akk threads`);
653
901
  }
654
902
  function requiredExpectedBindingToken(value) {
@@ -730,6 +978,11 @@ function arrayValue(value) {
730
978
  ? value.filter((item) => item !== null && typeof item === "object" && !Array.isArray(item))
731
979
  : [];
732
980
  }
981
+ function terminalWatchRecord(result) {
982
+ return recordValue(result.watch) ??
983
+ recordValue(result.terminal_watch) ??
984
+ result;
985
+ }
733
986
  function finiteNumber(value) {
734
987
  return typeof value === "number" && Number.isFinite(value)
735
988
  ? value
@@ -772,6 +1025,16 @@ function formatManagedTurnLine(turn) {
772
1025
  truncateText(context, 90)
773
1026
  ].filter(Boolean).join(" | ");
774
1027
  }
1028
+ function formatTerminalWatchLine(watch) {
1029
+ return [
1030
+ nonEmptyString(watch.short_ref) ??
1031
+ nonEmptyString(watch.watch_id) ??
1032
+ "unknown",
1033
+ nonEmptyString(watch.agent) ?? "agent",
1034
+ nonEmptyString(watch.status) ?? "unknown",
1035
+ nonEmptyString(watch.terminal_id)
1036
+ ].filter(Boolean).join(" | ");
1037
+ }
775
1038
  function formatAvailableActions(label, resource) {
776
1039
  const actions = recordValue(resource.available_actions);
777
1040
  if (!actions) {
@@ -779,6 +1042,8 @@ function formatAvailableActions(label, resource) {
779
1042
  }
780
1043
  const displayedActions = new Set([
781
1044
  "send",
1045
+ "watch",
1046
+ "unwatch",
782
1047
  "respond",
783
1048
  "status",
784
1049
  "approve",