@scotthuang/agent-knock-knock 0.2.0 → 0.2.1

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 CHANGED
@@ -1,8 +1,11 @@
1
1
  # Changelog
2
2
 
3
- ## Unreleased
3
+ ## 0.2.1 - 2026-06-22
4
4
 
5
- No unreleased changes.
5
+ ### Changed
6
+
7
+ - Deprecated the standalone restart recovery path from the OpenClaw-facing tool, slash command, skill, and docs. Conversations that need recovery now present `recover`, `close`, or starting a new independent delegation.
8
+ - Changed `send` to automatically fall back to AKK replay recovery when the previous ACPX session is unavailable. Explicit recovery decisions remain available only through `--recovery-policy explicit`.
6
9
 
7
10
  ## 0.2.0 - 2026-06-22
8
11
 
package/README.md CHANGED
@@ -140,7 +140,6 @@ akk list
140
140
  akk send <conversation-id>: continue with the smaller implementation
141
141
  akk cancel <conversation-id>
142
142
  akk recover <conversation-id>
143
- akk restart <conversation-id>
144
143
  akk close <conversation-id>
145
144
  ```
146
145
 
@@ -156,7 +155,6 @@ The plugin also registers the `/akk` slash command for channel surfaces that sup
156
155
  /akk send <conversation-id> <message>
157
156
  /akk cancel <conversation-id>
158
157
  /akk recover <conversation-id>
159
- /akk restart <conversation-id>
160
158
  /akk close <conversation-id> [reason]
161
159
  ```
162
160
 
@@ -172,7 +170,6 @@ If your OpenClaw config uses a restrictive tool allowlist, allow the tool:
172
170
  "agent_knock_knock_send",
173
171
  "agent_knock_knock_cancel",
174
172
  "agent_knock_knock_recover",
175
- "agent_knock_knock_restart",
176
173
  "agent_knock_knock_close"
177
174
  ]
178
175
  }
@@ -189,13 +186,15 @@ New delegations create a fresh ACPX session by default, using a name like `akk-c
189
186
 
190
187
  Background launches also start a small AKK monitor process. The monitor exits when the conversation receives a callback or otherwise leaves the agent-waiting state. If the executor process disappears before a callback, or if no callback arrives before `agentTimeoutMinutes`, the conversation is marked `stalled` and AKK attempts to notify the original OpenClaw session through the callback Gateway route. The default agent timeout is 60 minutes.
191
188
 
192
- Some coding agents may not reliably resume a named ACPX session after their backing process disappears. Executors can opt into an explicit recovery decision flow. In that mode, a failed follow-up send marks the AKK conversation `needs_recovery` instead of automatically replaying history or starting a new session. The user can then choose:
189
+ Some coding agents may not reliably resume a named ACPX session after their backing process disappears. By default, `AKK send <conversation-id>: <message>` automatically falls back to AKK replay recovery: it starts a fresh ACPX session, gives the agent a bounded summary of AKK's saved protocol history, and includes the pending message. The result includes `auto_recovered: true` when this happens.
190
+
191
+ If a caller uses the explicit recovery policy, or if automatic recovery cannot complete, the AKK conversation can enter `needs_recovery`. The user can then choose:
193
192
 
194
193
  - `AKK recover <conversation-id>`: start a new coding-agent session with AKK's saved protocol history summary plus the pending message.
195
- - `AKK restart <conversation-id>`: start a new coding-agent session with only the pending message.
196
194
  - `AKK close <conversation-id>`: close the AKK task without recovery.
195
+ - Start a new independent AKK delegation if the old task should not be recovered.
197
196
 
198
- Codex and Claude Code currently use native named-session recovery through ACPX. Cursor uses the explicit decision flow because its native session resume can be unreliable after the backing process disappears.
197
+ Recover is AKK replay recovery, not guaranteed native coding-agent session resume. Codex, Claude Code, and Cursor all use the same default `send` auto-recovery behavior when the existing ACPX session is unavailable.
199
198
 
200
199
  Task status can include a safe executor trace with `--trace` or the OpenClaw status tool's `trace: true` parameter. Trace summaries show client lifecycle events, tool call names and statuses, permission-request markers, monitor events, and short sanitized output previews. Agent thinking content is never returned; it is counted and marked as redacted.
201
200
 
@@ -256,14 +255,11 @@ node dist/src/cli.js cancel \
256
255
  --conversation <conversation-id>
257
256
  ```
258
257
 
259
- Recover or restart a task that is waiting for an explicit recovery decision:
258
+ Recover a task that is waiting for an explicit recovery decision:
260
259
 
261
260
  ```bash
262
261
  node dist/src/cli.js recover \
263
262
  --conversation <conversation-id>
264
-
265
- node dist/src/cli.js restart \
266
- --conversation <conversation-id>
267
263
  ```
268
264
 
269
265
  Inspect and take over native Codex sessions that were started outside AKK:
package/dist/src/cli.js CHANGED
@@ -8,7 +8,7 @@ import { fileURLToPath } from "node:url";
8
8
  import { CodexLocalSessionProvider } from "./codex-local-session-provider.js";
9
9
  import { CodexStoreAdapter } from "./codex-store-adapter.js";
10
10
  import { applyMessageToConversation, budgetAction, createConversation, createMessage, executorForConversation, extractStructuredMessage, parseMessageJson, resolveExecutor } from "./protocol.js";
11
- import { EXECUTOR_KINDS, acpxCommandForExecutor, executorDefinitionForKind, modelEnvForExecutor, normalizeModelForExecutor, proxyEnvForExecutor, sessionRecoveryStrategyForExecutor } from "./executors.js";
11
+ import { EXECUTOR_KINDS, acpxCommandForExecutor, executorDefinitionForKind, modelEnvForExecutor, normalizeModelForExecutor, proxyEnvForExecutor } from "./executors.js";
12
12
  import { executorBootstrapPrompt } from "./bootstrap.js";
13
13
  import { writeRuntimeLog } from "./runtime-log.js";
14
14
  import { formatTranscript, readNdjsonLog } from "./transcript.js";
@@ -100,9 +100,6 @@ async function runCommand(commandName, options) {
100
100
  else if (commandName === "recover") {
101
101
  runRecover(options);
102
102
  }
103
- else if (commandName === "restart") {
104
- runRestart(options);
105
- }
106
103
  else if (commandName === "close") {
107
104
  runClose(options);
108
105
  }
@@ -1154,7 +1151,7 @@ function runSend(options) {
1154
1151
  throw new Error(`cannot send to ${conversation.conversation_id}; conversation is ${conversation.status}`);
1155
1152
  }
1156
1153
  if (conversation.status === "needs_recovery") {
1157
- throw new Error(`cannot send to ${conversation.conversation_id}; choose recover, restart, or close first`);
1154
+ throw new Error(`cannot send to ${conversation.conversation_id}; choose recover, close, or delegate a new task first`);
1158
1155
  }
1159
1156
  if (conversation.status === "needs_model_selection" && !options.model) {
1160
1157
  throw new Error(`cannot send to ${conversation.conversation_id}; choose a supported model with --model first`);
@@ -1264,7 +1261,7 @@ function runSend(options) {
1264
1261
  stderr: textSummary(cleanProcessText(ensureSession.stderr))
1265
1262
  });
1266
1263
  if (ensureSession.error) {
1267
- if (requiresExplicitRecoveryDecision(executor, options)) {
1264
+ if (requiresExplicitRecoveryDecision(options)) {
1268
1265
  printJson(markConversationNeedsRecovery({
1269
1266
  conversation: nextConversation,
1270
1267
  statePath,
@@ -1277,10 +1274,22 @@ function runSend(options) {
1277
1274
  }));
1278
1275
  return;
1279
1276
  }
1280
- throw new Error(`acpx ${executor.kind} session ensure failed to start: ${ensureSession.error.message}`);
1277
+ autoRecoverSendFailure({
1278
+ options,
1279
+ conversation: nextConversation,
1280
+ statePath,
1281
+ logPath,
1282
+ executor,
1283
+ message,
1284
+ failedStage: "session_ensure",
1285
+ result: ensureSession,
1286
+ reason: `acpx ${executor.kind} session ensure failed to start: ${ensureSession.error.message}`
1287
+ });
1288
+ return;
1281
1289
  }
1282
1290
  if (ensureSession.status !== 0) {
1283
- if (requiresExplicitRecoveryDecision(executor, options)) {
1291
+ const reason = cleanProcessText(ensureSession.stderr || ensureSession.stdout || `acpx ${executor.kind} sessions ensure exited with status ${ensureSession.status}`);
1292
+ if (requiresExplicitRecoveryDecision(options)) {
1284
1293
  printJson(markConversationNeedsRecovery({
1285
1294
  conversation: nextConversation,
1286
1295
  statePath,
@@ -1289,11 +1298,22 @@ function runSend(options) {
1289
1298
  message,
1290
1299
  failedStage: "session_ensure",
1291
1300
  result: ensureSession,
1292
- reason: cleanProcessText(ensureSession.stderr || ensureSession.stdout || `acpx ${executor.kind} sessions ensure exited with status ${ensureSession.status}`)
1301
+ reason
1293
1302
  }));
1294
1303
  return;
1295
1304
  }
1296
- throw new Error(cleanProcessText(ensureSession.stderr || ensureSession.stdout || `acpx ${executor.kind} sessions ensure exited with status ${ensureSession.status}`));
1305
+ autoRecoverSendFailure({
1306
+ options,
1307
+ conversation: nextConversation,
1308
+ statePath,
1309
+ logPath,
1310
+ executor,
1311
+ message,
1312
+ failedStage: "session_ensure",
1313
+ result: ensureSession,
1314
+ reason
1315
+ });
1316
+ return;
1297
1317
  }
1298
1318
  const acpxArgs = buildAcpxPromptArgs({ executor, payload, model: executorModel });
1299
1319
  if (options.background) {
@@ -1386,7 +1406,7 @@ function runSend(options) {
1386
1406
  stderr: textSummary(cleanProcessText(sendResult.stderr))
1387
1407
  });
1388
1408
  if (sendResult.error) {
1389
- if (requiresExplicitRecoveryDecision(executor, options)) {
1409
+ if (requiresExplicitRecoveryDecision(options)) {
1390
1410
  printJson(markConversationNeedsRecovery({
1391
1411
  conversation: nextConversation,
1392
1412
  statePath,
@@ -1399,10 +1419,22 @@ function runSend(options) {
1399
1419
  }));
1400
1420
  return;
1401
1421
  }
1402
- throw new Error(`acpx ${executor.kind} send failed to start: ${sendResult.error.message}`);
1422
+ autoRecoverSendFailure({
1423
+ options,
1424
+ conversation: nextConversation,
1425
+ statePath,
1426
+ logPath,
1427
+ executor,
1428
+ message,
1429
+ failedStage: "message_send",
1430
+ result: sendResult,
1431
+ reason: `acpx ${executor.kind} send failed to start: ${sendResult.error.message}`
1432
+ });
1433
+ return;
1403
1434
  }
1404
1435
  if (sendResult.status !== 0) {
1405
- if (requiresExplicitRecoveryDecision(executor, options)) {
1436
+ const reason = cleanProcessText(sendResult.stderr || sendResult.stdout || `acpx ${executor.kind} send exited with status ${sendResult.status}`);
1437
+ if (requiresExplicitRecoveryDecision(options)) {
1406
1438
  printJson(markConversationNeedsRecovery({
1407
1439
  conversation: nextConversation,
1408
1440
  statePath,
@@ -1411,11 +1443,22 @@ function runSend(options) {
1411
1443
  message,
1412
1444
  failedStage: "message_send",
1413
1445
  result: sendResult,
1414
- reason: cleanProcessText(sendResult.stderr || sendResult.stdout || `acpx ${executor.kind} send exited with status ${sendResult.status}`)
1446
+ reason
1415
1447
  }));
1416
1448
  return;
1417
1449
  }
1418
- throw new Error(cleanProcessText(sendResult.stderr || sendResult.stdout || `acpx ${executor.kind} send exited with status ${sendResult.status}`));
1450
+ autoRecoverSendFailure({
1451
+ options,
1452
+ conversation: nextConversation,
1453
+ statePath,
1454
+ logPath,
1455
+ executor,
1456
+ message,
1457
+ failedStage: "message_send",
1458
+ result: sendResult,
1459
+ reason
1460
+ });
1461
+ return;
1419
1462
  }
1420
1463
  const deliveredConversation = markTakeoverBootstrapped({
1421
1464
  conversation: nextConversation,
@@ -1715,11 +1758,35 @@ function markForkSessionBootstrapped({ conversation, statePath, logPath, executo
1715
1758
  });
1716
1759
  return nextConversation;
1717
1760
  }
1718
- function requiresExplicitRecoveryDecision(executor, options = {}) {
1761
+ function requiresExplicitRecoveryDecision(options = {}) {
1719
1762
  if (options.recoveryPolicy === "explicit" || options.recoveryPolicy === "explicit-decision") {
1720
1763
  return true;
1721
1764
  }
1722
- return sessionRecoveryStrategyForExecutor(executor) === "explicit-decision";
1765
+ return false;
1766
+ }
1767
+ function autoRecoverSendFailure({ options, conversation, statePath, logPath, executor, message, failedStage, result, reason }) {
1768
+ markConversationNeedsRecovery({
1769
+ conversation,
1770
+ statePath,
1771
+ logPath,
1772
+ executor,
1773
+ message,
1774
+ failedStage,
1775
+ result,
1776
+ reason
1777
+ });
1778
+ runtimeLog("info", "conversation_auto_recovery_start", {
1779
+ conversation_id: conversation.conversation_id,
1780
+ agent: executor.kind,
1781
+ executor_session: executor.session,
1782
+ failed_stage: failedStage,
1783
+ reason: textSummary(reason)
1784
+ });
1785
+ runRecoveryDecision({
1786
+ ...options,
1787
+ mode: "recover",
1788
+ autoRecovered: true
1789
+ });
1723
1790
  }
1724
1791
  function markConversationNeedsRecovery({ conversation, statePath, logPath, executor, message, failedStage, result, reason }) {
1725
1792
  const now = new Date().toISOString();
@@ -1733,7 +1800,7 @@ function markConversationNeedsRecovery({ conversation, statePath, logPath, execu
1733
1800
  failed_message_id: message.id,
1734
1801
  pending_message: message,
1735
1802
  previous_executor: executor,
1736
- options: ["recover", "restart", "close"]
1803
+ options: ["recover", "close", "delegate"]
1737
1804
  };
1738
1805
  const nextConversation = {
1739
1806
  ...conversation,
@@ -1837,9 +1904,6 @@ function runCancel(options) {
1837
1904
  function runRecover(options) {
1838
1905
  runRecoveryDecision({ ...options, mode: "recover" });
1839
1906
  }
1840
- function runRestart(options) {
1841
- runRecoveryDecision({ ...options, mode: "restart" });
1842
- }
1843
1907
  function runRecoveryDecision(options) {
1844
1908
  cleanupIdleConversations(storeDirFromOptions(options), options);
1845
1909
  const { conversation, statePath, logPath } = loadConversationFromOptions(options);
@@ -1871,9 +1935,7 @@ function runRecoveryDecision(options) {
1871
1935
  updated_at: now
1872
1936
  };
1873
1937
  saveState(statePath, recoveredConversation);
1874
- const payload = options.mode === "recover"
1875
- ? buildRecoverPayload({ conversation, pendingMessage, logPath })
1876
- : buildRestartPayload({ pendingMessage });
1938
+ const payload = buildRecoverPayload({ conversation, pendingMessage, logPath });
1877
1939
  const acpxPath = resolveExecutable("acpx");
1878
1940
  const executorEnv = environmentForExecutor(executor, {
1879
1941
  allProxy: options.allProxy ?? conversation.executor_all_proxy
@@ -1935,8 +1997,8 @@ function runRecoveryDecision(options) {
1935
1997
  });
1936
1998
  printJson({
1937
1999
  conversation: recoveredConversation,
1938
- recovered: options.mode === "recover",
1939
- restarted: options.mode === "restart",
2000
+ recovered: true,
2001
+ auto_recovered: Boolean(options.autoRecovered),
1940
2002
  background: true,
1941
2003
  pid: child.pid ?? null,
1942
2004
  monitor_pid: monitor.pid ?? null,
@@ -1970,8 +2032,8 @@ function runRecoveryDecision(options) {
1970
2032
  }
1971
2033
  printJson({
1972
2034
  conversation: recoveredConversation,
1973
- recovered: options.mode === "recover",
1974
- restarted: options.mode === "restart",
2035
+ recovered: true,
2036
+ auto_recovered: Boolean(options.autoRecovered),
1975
2037
  delivered: true,
1976
2038
  executor,
1977
2039
  budget: budgetAction(recoveredConversation)
@@ -1994,16 +2056,6 @@ function buildRecoverPayload({ conversation, pendingMessage, logPath }) {
1994
2056
  JSON.stringify(pendingMessage)
1995
2057
  ].join("\n");
1996
2058
  }
1997
- function buildRestartPayload({ pendingMessage }) {
1998
- return [
1999
- "Restart this Agent Knock Knock task in a new ACPX session.",
2000
- "Do not assume the previous coding-agent session context is available.",
2001
- "Follow only the pending OpenClaw message below.",
2002
- "Continue to report back only through the callback command already provided for this conversation.",
2003
- "",
2004
- JSON.stringify(pendingMessage)
2005
- ].join("\n");
2006
- }
2007
2059
  function formatProtocolHistoryForRecovery(events) {
2008
2060
  const lines = events
2009
2061
  .filter((event) => event.event === "message")
@@ -3467,7 +3519,6 @@ function usage() {
3467
3519
  agent-knock-knock send --conversation <id> --message <text> [--type answer|task|control] [--all-proxy <url>] [--agent-timeout-minutes <minutes>]
3468
3520
  agent-knock-knock cancel --conversation <id> [--all-proxy <url>]
3469
3521
  agent-knock-knock recover --conversation <id> [--session <name>] [--all-proxy <url>]
3470
- agent-knock-knock restart --conversation <id> [--session <name>] [--all-proxy <url>]
3471
3522
  agent-knock-knock close --conversation <id> [--reason <text>]
3472
3523
  agent-knock-knock install-openclaw [--openclaw-bin <path>] [--skill-path <path>] [--no-restart]
3473
3524
  agent-knock-knock doctor