@scotthuang/agent-knock-knock 0.2.16 → 0.2.18

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,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.18 - 2026-06-30
4
+
5
+ ### Fixed
6
+
7
+ - Classified Codex remote compact stream disconnects as recoverable executor failures instead of leaving AKK tasks stuck as generic `stalled` conversations.
8
+ - Allowed `AKK recover` to replay saved AKK history and the pending OpenClaw message after a recognized monitor-time executor failure.
9
+
3
10
  ## 0.2.13 - 2026-06-25
4
11
 
5
12
  ### Fixed
package/dist/src/cli.js CHANGED
@@ -1589,6 +1589,26 @@ async function runSend(options) {
1589
1589
  cleanupIdleConversations(storeDirFromOptions(options), options);
1590
1590
  const terminalConversation = await resolveTerminalConversationFromOptions(options);
1591
1591
  if (terminalConversation) {
1592
+ if (options.background) {
1593
+ const managed = createManagedTerminalConversationFromRawId({
1594
+ options,
1595
+ conversationId: terminalConversation.conversationId,
1596
+ messageBody,
1597
+ terminalControl: terminalConversation.terminalControl
1598
+ });
1599
+ await runTerminalControlSend({
1600
+ options,
1601
+ conversation: managed.conversation,
1602
+ nextConversation: managed.nextConversation,
1603
+ statePath: managed.statePath,
1604
+ logPath: managed.logPath,
1605
+ executor: managed.executor,
1606
+ message: managed.message,
1607
+ terminalControl: terminalConversation.terminalControl,
1608
+ needsNativeTakeoverBootstrap: true
1609
+ });
1610
+ return;
1611
+ }
1592
1612
  await runTerminalConversationSend({
1593
1613
  options,
1594
1614
  conversationId: terminalConversation.conversationId,
@@ -2083,7 +2103,17 @@ async function runTerminalConversationSend({ options, conversationId, messageBod
2083
2103
  }
2084
2104
  async function runTerminalControlSend({ options, conversation, nextConversation, statePath, logPath, executor, message, terminalControl, needsNativeTakeoverBootstrap }) {
2085
2105
  const provider = createTerminalControlProvider(options);
2086
- await provider.sendText(terminalControl.target, String(message.body ?? ""), {
2106
+ const terminalPayload = needsNativeTakeoverBootstrap
2107
+ ? buildAgentSendPayload({
2108
+ conversation,
2109
+ executor,
2110
+ message,
2111
+ includeNativeTakeoverBootstrap: true,
2112
+ includeForkTakeoverBootstrap: false,
2113
+ forkTakeover: undefined
2114
+ })
2115
+ : String(message.body ?? "");
2116
+ await provider.sendText(terminalControl.target, terminalPayload, {
2087
2117
  socketPath: terminalControl.socketPath
2088
2118
  });
2089
2119
  await provider.sendKeys(terminalControl.target, ["Enter"], {
@@ -2095,13 +2125,15 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
2095
2125
  event: "terminal_message_send",
2096
2126
  executor,
2097
2127
  terminal_control: terminalControl,
2098
- message: textSummary(message.body)
2128
+ message: textSummary(message.body),
2129
+ payload: textSummary(terminalPayload)
2099
2130
  });
2100
2131
  runtimeLog("info", "terminal_message_send", {
2101
2132
  conversation_id: conversation.conversation_id,
2102
2133
  agent: executor.kind,
2103
2134
  terminal_target: terminalControl.target,
2104
- message: textSummary(message.body)
2135
+ message: textSummary(message.body),
2136
+ payload: textSummary(terminalPayload)
2105
2137
  });
2106
2138
  const deliveredConversation = markTakeoverBootstrapped({
2107
2139
  conversation: nextConversation,
@@ -2128,6 +2160,113 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
2128
2160
  })
2129
2161
  });
2130
2162
  }
2163
+ function createManagedTerminalConversationFromRawId({ options, conversationId, messageBody, terminalControl }) {
2164
+ const workspace = terminalControl.currentPath ?? process.cwd();
2165
+ const storeDir = expandHome(options.storeDir ?? options.logDir ?? defaultStoreDir(workspace));
2166
+ cleanupIdleConversations(storeDir, options);
2167
+ const executor = resolveExecutor({
2168
+ kind: "codex",
2169
+ session: conversationId
2170
+ });
2171
+ const now = new Date();
2172
+ const conversation = createConversation({
2173
+ userRequest: String(messageBody),
2174
+ workspace,
2175
+ openclawSession: options.openclawSession ?? "agent:main:main",
2176
+ executorKind: executor.kind,
2177
+ executorSession: executor.session,
2178
+ softLimit: Number(options.softLimit ?? 50),
2179
+ hardLimit: Number(options.hardLimit ?? 100),
2180
+ now
2181
+ });
2182
+ const paths = pathsForConversation(conversation.conversation_id, storeDir);
2183
+ const callbackCommand = options.callbackCommand
2184
+ ? expandCallbackCommandTemplate(options.callbackCommand, { statePath: paths.statePath })
2185
+ : buildCallbackCommand({
2186
+ statePath: paths.statePath,
2187
+ gatewayUrl: options.gatewayUrl ?? "ws://127.0.0.1:18789",
2188
+ token: options.token,
2189
+ openclawSession: options.openclawSession ?? "agent:main:main",
2190
+ gatewayMethod: options.gatewayMethod,
2191
+ gatewaySession: options.gatewaySession,
2192
+ openclawBin: options.openclawBin ?? resolveOptionalExecutable("openclaw")
2193
+ });
2194
+ const attachedConversation = withStoragePaths({
2195
+ ...conversation,
2196
+ executor,
2197
+ status: "idle",
2198
+ idle_since: now.toISOString(),
2199
+ updated_at: now.toISOString(),
2200
+ callback_command: callbackCommand,
2201
+ gateway_url: options.gatewayUrl ?? "ws://127.0.0.1:18789",
2202
+ gateway_method: options.gatewayMethod,
2203
+ gateway_session: options.gatewaySession ?? options.openclawSession ?? "agent:main:main",
2204
+ openclaw_bin: options.openclawBin ?? resolveOptionalExecutable("openclaw"),
2205
+ executor_all_proxy: proxyForExecutor(executor, options),
2206
+ executor_model: options.model ?? options.codexModel,
2207
+ native_session_takeover: {
2208
+ agent: "codex",
2209
+ native_session_id: conversationId,
2210
+ source_cwd: workspace,
2211
+ source_title: `Terminal-controlled Codex ${terminalControl.target}`,
2212
+ strategy: "terminal_control",
2213
+ attached_at: now.toISOString(),
2214
+ takeover_match_kind: "raw_terminal_send",
2215
+ terminal_control: terminalControl,
2216
+ needs_bootstrap: true
2217
+ }
2218
+ }, paths);
2219
+ saveState(paths.statePath, attachedConversation);
2220
+ appendEvent(paths.logPath, {
2221
+ ts: now.toISOString(),
2222
+ conversation_id: attachedConversation.conversation_id,
2223
+ event: "raw_terminal_session_attached",
2224
+ source_conversation_id: conversationId,
2225
+ agent: "codex",
2226
+ terminal_control: terminalControl,
2227
+ executor
2228
+ });
2229
+ runtimeLog("info", "raw_terminal_session_attached", {
2230
+ conversation_id: attachedConversation.conversation_id,
2231
+ source_conversation_id: conversationId,
2232
+ terminal_target: terminalControl.target,
2233
+ state_path: paths.statePath,
2234
+ event_log_path: paths.logPath
2235
+ });
2236
+ const message = createMessage({
2237
+ conversation: attachedConversation,
2238
+ from: "openclaw",
2239
+ to: executor.actor,
2240
+ type: options.type ?? "task",
2241
+ body: String(messageBody),
2242
+ metadata: {
2243
+ executor_kind: executor.kind,
2244
+ executor_session: executor.session,
2245
+ source_conversation_id: conversationId
2246
+ }
2247
+ });
2248
+ const nextConversation = applyMessageToConversation(attachedConversation, message);
2249
+ saveState(paths.statePath, nextConversation);
2250
+ appendEvent(paths.logPath, messageEvent(message));
2251
+ runtimeLog("info", "message_created", {
2252
+ conversation_id: nextConversation.conversation_id,
2253
+ source_conversation_id: conversationId,
2254
+ agent: executor.kind,
2255
+ executor_session: executor.session,
2256
+ message_type: message.type,
2257
+ state_path: paths.statePath,
2258
+ event_log_path: paths.logPath,
2259
+ message: textSummary(messageBody)
2260
+ });
2261
+ return {
2262
+ conversation: attachedConversation,
2263
+ nextConversation,
2264
+ statePath: paths.statePath,
2265
+ logPath: paths.logPath,
2266
+ executor,
2267
+ message
2268
+ };
2269
+ }
2131
2270
  function runNativeCodexResumeSend({ options, conversation, nextConversation, statePath, logPath, executor, executorEnv, message, payload, nativeTakeover, needsNativeTakeoverBootstrap }) {
2132
2271
  const codexPath = resolveExecutable("codex");
2133
2272
  const nativeSessionId = String(nativeTakeover["native_session_id"]);
@@ -2885,7 +3024,8 @@ function runMonitor(options) {
2885
3024
  return;
2886
3025
  }
2887
3026
  if (Number.isFinite(pid) && !isProcessAlive(pid)) {
2888
- const modelSelection = detectModelSelectionError(readOutputTail(options.outputPath));
3027
+ const outputTail = readOutputTail(options.outputPath);
3028
+ const modelSelection = detectModelSelectionError(outputTail);
2889
3029
  if (modelSelection) {
2890
3030
  const modelSelectionConversation = markConversationNeedsModelSelection({
2891
3031
  statePath,
@@ -2906,6 +3046,30 @@ function runMonitor(options) {
2906
3046
  });
2907
3047
  return;
2908
3048
  }
3049
+ const transientFailure = detectTransientExecutorFailure(outputTail);
3050
+ if (transientFailure) {
3051
+ const recoveryResult = markMonitorFailureNeedsRecovery({
3052
+ statePath,
3053
+ logPath,
3054
+ reason: transientFailure.message,
3055
+ detail: {
3056
+ executor_pid: pid,
3057
+ output_path: options.outputPath,
3058
+ transient_failure: transientFailure
3059
+ },
3060
+ outputTail
3061
+ });
3062
+ if (recoveryResult) {
3063
+ printJson({
3064
+ conversation: recoveryResult.conversation,
3065
+ monitored: true,
3066
+ stalled: false,
3067
+ needs_recovery: true,
3068
+ reason: transientFailure.message
3069
+ });
3070
+ return;
3071
+ }
3072
+ }
2909
3073
  const stalledConversation = markConversationStalled({
2910
3074
  statePath,
2911
3075
  logPath,
@@ -3707,6 +3871,110 @@ function markConversationStalled({ statePath, logPath, reason, detail = {} }) {
3707
3871
  }
3708
3872
  return stalledConversation;
3709
3873
  }
3874
+ function markMonitorFailureNeedsRecovery({ statePath, logPath, reason, detail = {}, outputTail = "" }) {
3875
+ const releaseLock = acquireFileLock(`${statePath}.lock`);
3876
+ let recoveryResult;
3877
+ let recoveryMessage;
3878
+ try {
3879
+ const conversation = loadState(statePath);
3880
+ if (!isWaitingForAgent(conversation.status)) {
3881
+ runtimeLog("info", "executor_monitor_finished", {
3882
+ conversation_id: conversation.conversation_id,
3883
+ status: conversation.status,
3884
+ reason: "conversation_changed_before_recovery"
3885
+ });
3886
+ return undefined;
3887
+ }
3888
+ const executor = executorForConversation(conversation);
3889
+ const pendingMessage = latestPendingExecutorMessage({
3890
+ events: readExistingEvents(logPath),
3891
+ executor
3892
+ });
3893
+ if (!pendingMessage) {
3894
+ runtimeLog("warn", "monitor_recovery_skipped", {
3895
+ conversation_id: conversation.conversation_id,
3896
+ agent: executor.kind,
3897
+ executor_session: executor.session,
3898
+ reason: "pending_message_missing"
3899
+ });
3900
+ return undefined;
3901
+ }
3902
+ recoveryResult = markConversationNeedsRecovery({
3903
+ conversation,
3904
+ statePath,
3905
+ logPath,
3906
+ executor,
3907
+ message: pendingMessage,
3908
+ failedStage: "executor_monitor",
3909
+ result: {
3910
+ status: 1,
3911
+ stderr: outputTail
3912
+ },
3913
+ reason
3914
+ });
3915
+ const recoveredConversation = recoveryResult.conversation;
3916
+ const shouldNotify = Boolean(recoveredConversation.gateway_method && !recoveredConversation.recovery_notification_sent_at);
3917
+ if (shouldNotify) {
3918
+ recoveryMessage = createMessage({
3919
+ conversation: recoveredConversation,
3920
+ from: executor.actor,
3921
+ to: "openclaw",
3922
+ type: "error",
3923
+ requiresResponse: false,
3924
+ body: [
3925
+ `AKK marked this ${executor.display_name} task as needing recovery: ${reason}.`,
3926
+ "",
3927
+ `Conversation: ${recoveredConversation.conversation_id}`,
3928
+ `Session: ${executor.session}`,
3929
+ "Use `AKK recover` to replay saved AKK history in a new session, or `AKK close` to close it."
3930
+ ].join("\n")
3931
+ });
3932
+ const now = new Date().toISOString();
3933
+ recoveryResult.conversation = {
3934
+ ...recoveredConversation,
3935
+ recovery_notification_sent_at: now,
3936
+ recovery_notification_message_id: recoveryMessage.id,
3937
+ updated_at: now
3938
+ };
3939
+ saveState(statePath, recoveryResult.conversation);
3940
+ }
3941
+ appendEvent(logPath, {
3942
+ ts: new Date().toISOString(),
3943
+ conversation_id: conversation.conversation_id,
3944
+ event: "monitor_recovery_classified",
3945
+ status: "needs_recovery",
3946
+ reason,
3947
+ ...detail
3948
+ });
3949
+ }
3950
+ finally {
3951
+ releaseLock();
3952
+ }
3953
+ if (recoveryResult?.conversation && recoveryMessage) {
3954
+ deliverMonitorFailureNotification({
3955
+ statePath,
3956
+ logPath,
3957
+ conversation: recoveryResult.conversation,
3958
+ message: recoveryMessage,
3959
+ eventPrefix: "recovery"
3960
+ });
3961
+ }
3962
+ return recoveryResult;
3963
+ }
3964
+ function latestPendingExecutorMessage({ events, executor }) {
3965
+ for (const event of [...events].reverse()) {
3966
+ if (event.event !== "message") {
3967
+ continue;
3968
+ }
3969
+ const message = event.message ?? event;
3970
+ if (message?.from === "openclaw" &&
3971
+ message?.to === executor.actor &&
3972
+ message?.requires_response !== false) {
3973
+ return message;
3974
+ }
3975
+ }
3976
+ return undefined;
3977
+ }
3710
3978
  function markConversationNeedsModelSelection({ statePath, logPath, reason, detail = {} }) {
3711
3979
  const releaseLock = acquireFileLock(`${statePath}.lock`);
3712
3980
  let modelSelectionConversation;
@@ -3759,7 +4027,10 @@ function markConversationNeedsModelSelection({ statePath, logPath, reason, detai
3759
4027
  }
3760
4028
  return modelSelectionConversation;
3761
4029
  }
3762
- function deliverStalledNotification({ statePath, logPath, conversation, message }) {
4030
+ function deliverMonitorFailureNotification({ statePath, logPath, conversation, message, eventPrefix }) {
4031
+ deliverStalledNotification({ statePath, logPath, conversation, message, eventPrefix });
4032
+ }
4033
+ function deliverStalledNotification({ statePath, logPath, conversation, message, eventPrefix = "stalled" }) {
3763
4034
  if (!conversation.gateway_method) {
3764
4035
  return;
3765
4036
  }
@@ -3779,14 +4050,14 @@ function deliverStalledNotification({ statePath, logPath, conversation, message
3779
4050
  appendEvent(logPath, {
3780
4051
  ts: new Date().toISOString(),
3781
4052
  conversation_id: conversation.conversation_id,
3782
- event: "stalled_gateway_method_delivery",
4053
+ event: `${eventPrefix}_gateway_method_delivery`,
3783
4054
  method: conversation.gateway_method,
3784
4055
  message_id: message.id,
3785
4056
  status: delivery.status,
3786
4057
  stdout: delivery.stdout,
3787
4058
  stderr: delivery.stderr
3788
4059
  });
3789
- runtimeLog("info", "stalled_gateway_method_delivery", {
4060
+ runtimeLog("info", `${eventPrefix}_gateway_method_delivery`, {
3790
4061
  conversation_id: conversation.conversation_id,
3791
4062
  method: conversation.gateway_method,
3792
4063
  message_id: message.id,
@@ -3812,13 +4083,13 @@ function deliverStalledNotification({ statePath, logPath, conversation, message
3812
4083
  appendEvent(logPath, {
3813
4084
  ts: new Date().toISOString(),
3814
4085
  conversation_id: conversation.conversation_id,
3815
- event: "stalled_chat_send_delivery",
4086
+ event: `${eventPrefix}_chat_send_delivery`,
3816
4087
  message_id: message.id,
3817
4088
  status: chatSendDelivery.status,
3818
4089
  stdout: chatSendDelivery.stdout,
3819
4090
  stderr: chatSendDelivery.stderr
3820
4091
  });
3821
- runtimeLog("info", "stalled_chat_send_delivery", {
4092
+ runtimeLog("info", `${eventPrefix}_chat_send_delivery`, {
3822
4093
  conversation_id: conversation.conversation_id,
3823
4094
  message_id: message.id,
3824
4095
  status: chatSendDelivery.status,
@@ -4143,6 +4414,9 @@ function classifyProcessFailure(result) {
4143
4414
  if (!combined && status === 0) {
4144
4415
  return undefined;
4145
4416
  }
4417
+ if (isRemoteCompactStreamDisconnect(combined)) {
4418
+ return "transient_remote_compact_failure";
4419
+ }
4146
4420
  if (combined.includes("agent needs reconnect") || combined.includes("internal error")) {
4147
4421
  return "agent_reconnect_required";
4148
4422
  }
@@ -4210,6 +4484,25 @@ function detectModelSelectionError(text) {
4210
4484
  }
4211
4485
  return undefined;
4212
4486
  }
4487
+ function detectTransientExecutorFailure(text) {
4488
+ const cleaned = cleanProcessText(text);
4489
+ if (!cleaned) {
4490
+ return undefined;
4491
+ }
4492
+ if (isRemoteCompactStreamDisconnect(cleaned.toLowerCase())) {
4493
+ return {
4494
+ kind: "remote_compact_stream_disconnect",
4495
+ message: "Codex remote compact stream disconnected before completion"
4496
+ };
4497
+ }
4498
+ return undefined;
4499
+ }
4500
+ function isRemoteCompactStreamDisconnect(text) {
4501
+ const value = String(text ?? "").toLowerCase();
4502
+ return (value.includes("error running remote compact task") &&
4503
+ value.includes("stream disconnected") &&
4504
+ value.includes("/codex/responses/compact"));
4505
+ }
4213
4506
  function runtimeLog(level, event, fields = {}) {
4214
4507
  try {
4215
4508
  writeRuntimeLog({