@scotthuang/agent-knock-knock 0.2.10 → 0.2.12
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 +12 -0
- package/README.md +7 -13
- package/dist/src/cli.js +125 -137
- package/dist/src/cli.js.map +1 -1
- package/dist/src/openclaw-plugin.js +6 -44
- package/dist/src/openclaw-plugin.js.map +1 -1
- package/dist/src/session-takeover-planner.d.ts +2 -14
- package/dist/src/session-takeover-planner.js +0 -23
- package/dist/src/session-takeover-planner.js.map +1 -1
- package/package.json +1 -1
- package/templates/openclaw-skills/agent-knock-knock/SKILL.md +8 -19
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.11 - 2026-06-25
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- Added terminal-controlled `AKK cancel` support by sending Control-C to the controlled tmux pane.
|
|
8
|
+
- Added structured terminal reachability, screen, and approval state to `AKK status` for terminal-controlled sessions.
|
|
9
|
+
|
|
10
|
+
### Removed
|
|
11
|
+
|
|
12
|
+
- Removed the public `agent discover` CLI path and `agent_knock_knock_agent_discover` OpenClaw tool; use `AKK list` for active/local session visibility.
|
|
13
|
+
- Removed the legacy `safe_resume` takeover strategy; native takeover now uses active process-based terminate, terminal-control, or fork paths.
|
|
14
|
+
|
|
3
15
|
## 0.2.10 - 2026-06-25
|
|
4
16
|
|
|
5
17
|
### Fixed
|
package/README.md
CHANGED
|
@@ -117,9 +117,13 @@ AKK Codex: review the current branch and propose a small fix
|
|
|
117
117
|
AKK Claude: review the latest commit
|
|
118
118
|
AKK Cursor: fix the flaky UI test
|
|
119
119
|
akk list
|
|
120
|
+
akk status <conversation-id>
|
|
120
121
|
akk send <conversation-id>: continue with the smaller implementation
|
|
121
122
|
akk cancel <conversation-id>
|
|
123
|
+
akk recover <conversation-id>
|
|
122
124
|
akk close <conversation-id>
|
|
125
|
+
AKK terminal takeover Codex <native-session-id>
|
|
126
|
+
AKK approve <conversation-id>
|
|
123
127
|
```
|
|
124
128
|
|
|
125
129
|
Surfaces that support OpenClaw native commands can use the same workflow through `/akk`:
|
|
@@ -154,32 +158,23 @@ Optional default-agent config:
|
|
|
154
158
|
|
|
155
159
|
## Native Codex Takeover
|
|
156
160
|
|
|
157
|
-
Experimental: AKK can
|
|
161
|
+
Experimental: AKK can list and take over Codex CLI sessions that were started outside AKK. This is useful when you started Codex in a terminal, left the machine, and want OpenClaw to continue managing that work from chat.
|
|
158
162
|
|
|
159
163
|
List current AKK-managed and local agent work:
|
|
160
164
|
|
|
161
165
|
```text
|
|
162
166
|
AKK list
|
|
163
|
-
AKK Codex active
|
|
164
167
|
```
|
|
165
168
|
|
|
166
169
|
`AKK list` returns separate groups for:
|
|
167
170
|
|
|
168
171
|
- `delegated`: AKK-managed background tasks.
|
|
169
172
|
- `native`: local native sessions that AKK can discover but cannot directly control.
|
|
170
|
-
- `terminal_controlled`: local sessions running in a controllable terminal provider
|
|
171
|
-
|
|
172
|
-
Lower-level discovery prompts remain available for historical session and capability inspection:
|
|
173
|
-
|
|
174
|
-
```text
|
|
175
|
-
AKK Codex sessions
|
|
176
|
-
AKK Codex capabilities
|
|
177
|
-
```
|
|
173
|
+
- `terminal_controlled`: local sessions running in a controllable terminal provider. The current provider is tmux. These entries include terminal metadata, command capabilities, and concise approval state when a visible approval prompt is detected.
|
|
178
174
|
|
|
179
175
|
Takeover prompts:
|
|
180
176
|
|
|
181
177
|
```text
|
|
182
|
-
AKK safe resume Codex <native-session-id>
|
|
183
178
|
AKK takeover Codex <native-session-id>
|
|
184
179
|
AKK terminal takeover Codex <native-session-id>
|
|
185
180
|
AKK fork takeover Codex <native-session-id>
|
|
@@ -188,9 +183,8 @@ AKK approve <conversation-id>
|
|
|
188
183
|
|
|
189
184
|
Strategies:
|
|
190
185
|
|
|
191
|
-
- `safe resume`: attach a stopped/inactive Codex session to AKK.
|
|
192
186
|
- `takeover`: stop an active matching Codex CLI after explicit confirmation, then resume it under AKK.
|
|
193
|
-
- `terminal takeover`: attach an active Codex CLI running inside
|
|
187
|
+
- `terminal takeover`: attach an active Codex CLI running inside a controllable terminal provider after explicit confirmation. AKK sends follow-ups directly to the terminal pane, can send Control-C for cancel, and can approve the currently visible Codex approval prompt.
|
|
194
188
|
- `fork takeover`: keep the original Codex CLI running, ask OpenClaw to summarize bounded source context, then create a new AKK-managed fork from that summary.
|
|
195
189
|
|
|
196
190
|
For long-running local Codex sessions that you may want OpenClaw to operate from WeChat, run Codex inside tmux. AKK does not require tmux, but tmux enables terminal-control takeover without stopping the original process:
|
package/dist/src/cli.js
CHANGED
|
@@ -13,7 +13,7 @@ import { executorBootstrapPrompt } from "./bootstrap.js";
|
|
|
13
13
|
import { writeRuntimeLog } from "./runtime-log.js";
|
|
14
14
|
import { formatTranscript, readNdjsonLog } from "./transcript.js";
|
|
15
15
|
import { appendEvent, defaultStoreDir, listConversations, logPathForStatePath, loadConversationById, loadState, messageEvent, pathsForConversation, pathsForConversationDir, saveState, statePathForConversationId } from "./store.js";
|
|
16
|
-
import { planFork,
|
|
16
|
+
import { planFork, planTakeover } from "./session-takeover-planner.js";
|
|
17
17
|
import { StaticTerminalControlProvider, TmuxTerminalControlProvider, enrichActiveProcessesWithTerminalControl } from "./terminal-control-provider.js";
|
|
18
18
|
const DEFAULT_IDLE_TIMEOUT_MINUTES = 10080;
|
|
19
19
|
const DEFAULT_AGENT_TIMEOUT_MINUTES = 60;
|
|
@@ -99,7 +99,7 @@ async function runCommand(commandName, options) {
|
|
|
99
99
|
await runApprove(options);
|
|
100
100
|
}
|
|
101
101
|
else if (commandName === "cancel") {
|
|
102
|
-
runCancel(options);
|
|
102
|
+
await runCancel(options);
|
|
103
103
|
}
|
|
104
104
|
else if (commandName === "recover") {
|
|
105
105
|
runRecover(options);
|
|
@@ -210,47 +210,13 @@ function runDoctor(options) {
|
|
|
210
210
|
});
|
|
211
211
|
}
|
|
212
212
|
async function runAgent(options) {
|
|
213
|
-
const agentCommand = required(options.agentCommand, "agent subcommand is required:
|
|
214
|
-
if (agentCommand === "discover") {
|
|
215
|
-
printJson(await runAgentDiscover(options));
|
|
216
|
-
return;
|
|
217
|
-
}
|
|
213
|
+
const agentCommand = required(options.agentCommand, "agent subcommand is required: takeover");
|
|
218
214
|
if (agentCommand === "takeover") {
|
|
219
215
|
printJson(await runAgentTakeover(options));
|
|
220
216
|
return;
|
|
221
217
|
}
|
|
222
218
|
throw new Error(`unsupported agent subcommand: ${agentCommand}`);
|
|
223
219
|
}
|
|
224
|
-
async function runAgentDiscover(options) {
|
|
225
|
-
const agent = required(options.agent, "--agent is required");
|
|
226
|
-
const scope = required(options.scope, "--scope is required");
|
|
227
|
-
const provider = createAgentSessionProvider(agent, options);
|
|
228
|
-
const capabilities = await provider.getCapabilities();
|
|
229
|
-
if (scope === "capabilities") {
|
|
230
|
-
return {
|
|
231
|
-
agent,
|
|
232
|
-
scope,
|
|
233
|
-
capabilities
|
|
234
|
-
};
|
|
235
|
-
}
|
|
236
|
-
if (scope === "sessions") {
|
|
237
|
-
return {
|
|
238
|
-
agent,
|
|
239
|
-
scope,
|
|
240
|
-
capabilities,
|
|
241
|
-
sessions: await provider.listHistoricalSessions()
|
|
242
|
-
};
|
|
243
|
-
}
|
|
244
|
-
if (scope === "active") {
|
|
245
|
-
return {
|
|
246
|
-
agent,
|
|
247
|
-
scope,
|
|
248
|
-
capabilities,
|
|
249
|
-
active: await listActiveSessionsWithTerminalControl(provider, options)
|
|
250
|
-
};
|
|
251
|
-
}
|
|
252
|
-
throw new Error(`unsupported discover scope: ${scope}`);
|
|
253
|
-
}
|
|
254
220
|
async function runAgentTakeover(options) {
|
|
255
221
|
const agent = required(options.agent, "--agent is required");
|
|
256
222
|
const sessionId = required(options.sessionId, "--session-id is required");
|
|
@@ -270,36 +236,6 @@ async function runAgentTakeover(options) {
|
|
|
270
236
|
}
|
|
271
237
|
};
|
|
272
238
|
}
|
|
273
|
-
if (strategy === "safe_resume") {
|
|
274
|
-
const plan = planSafeResume(session, await listActiveSessionsWithTerminalControl(provider, options));
|
|
275
|
-
if (plan.allowed && options.createConversation) {
|
|
276
|
-
const modelInfo = await provider.getSessionModel(session.id);
|
|
277
|
-
const attached = createNativeSessionConversation({
|
|
278
|
-
agent,
|
|
279
|
-
strategy,
|
|
280
|
-
session,
|
|
281
|
-
modelInfo,
|
|
282
|
-
options
|
|
283
|
-
});
|
|
284
|
-
return {
|
|
285
|
-
agent,
|
|
286
|
-
sessionId,
|
|
287
|
-
strategy,
|
|
288
|
-
status: "attached",
|
|
289
|
-
sideEffectsExecuted: true,
|
|
290
|
-
plan,
|
|
291
|
-
...attached
|
|
292
|
-
};
|
|
293
|
-
}
|
|
294
|
-
return {
|
|
295
|
-
agent,
|
|
296
|
-
sessionId,
|
|
297
|
-
strategy,
|
|
298
|
-
status: plan.allowed ? "ready" : "blocked",
|
|
299
|
-
sideEffectsExecuted: false,
|
|
300
|
-
plan
|
|
301
|
-
};
|
|
302
|
-
}
|
|
303
239
|
if (strategy === "terminate_then_resume") {
|
|
304
240
|
const activeSessions = await listActiveSessionsWithTerminalControl(provider, options);
|
|
305
241
|
const plan = planTakeover(session, activeSessions);
|
|
@@ -402,7 +338,7 @@ async function runAgentTakeover(options) {
|
|
|
402
338
|
plan,
|
|
403
339
|
error: {
|
|
404
340
|
code: "terminal_target_unavailable",
|
|
405
|
-
message: `No matching
|
|
341
|
+
message: `No matching terminal-controlled Codex process was found for ${terminalTarget}`
|
|
406
342
|
}
|
|
407
343
|
};
|
|
408
344
|
}
|
|
@@ -1419,7 +1355,6 @@ function nativeListEntry(session, activeSessions) {
|
|
|
1419
1355
|
confidence: session.confidence,
|
|
1420
1356
|
reason: session.reason,
|
|
1421
1357
|
commands: {
|
|
1422
|
-
safe_resume: Boolean(session.sessionId),
|
|
1423
1358
|
terminate_then_resume: true,
|
|
1424
1359
|
fork: Boolean(session.sessionId),
|
|
1425
1360
|
terminal_control_attach: false,
|
|
@@ -1458,7 +1393,7 @@ async function terminalControlledListEntry(session, activeSessions, options) {
|
|
|
1458
1393
|
capture_screen: true,
|
|
1459
1394
|
approve: approvalState.approvable === true,
|
|
1460
1395
|
status: true,
|
|
1461
|
-
cancel:
|
|
1396
|
+
cancel: true,
|
|
1462
1397
|
close: false
|
|
1463
1398
|
}
|
|
1464
1399
|
};
|
|
@@ -1540,21 +1475,8 @@ async function runStatus(options) {
|
|
|
1540
1475
|
const terminalControl = terminalControlFromTakeover(isRecord(conversation.native_session_takeover) ? conversation.native_session_takeover : undefined);
|
|
1541
1476
|
if (terminalControl) {
|
|
1542
1477
|
result.terminal_control = terminalControl;
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
scrollbackLines: Number(options.scrollbackLines ?? 120),
|
|
1546
|
-
socketPath: terminalControl.socketPath
|
|
1547
|
-
});
|
|
1548
|
-
result.terminal_screen = {
|
|
1549
|
-
excerpt: screenExcerpt(screen),
|
|
1550
|
-
approval: detectCodexApprovalPrompt(screen)
|
|
1551
|
-
};
|
|
1552
|
-
}
|
|
1553
|
-
catch (error) {
|
|
1554
|
-
result.terminal_screen = {
|
|
1555
|
-
error: error instanceof Error ? error.message : String(error)
|
|
1556
|
-
};
|
|
1557
|
-
}
|
|
1478
|
+
result.terminal_status = await terminalStatusForControl(terminalControl, options);
|
|
1479
|
+
result.terminal_screen = result.terminal_status.screen;
|
|
1558
1480
|
}
|
|
1559
1481
|
printJson(result);
|
|
1560
1482
|
runtimeLog("info", "task_status_read", {
|
|
@@ -1566,6 +1488,48 @@ async function runStatus(options) {
|
|
|
1566
1488
|
trace: Boolean(options.trace)
|
|
1567
1489
|
});
|
|
1568
1490
|
}
|
|
1491
|
+
async function terminalStatusForControl(terminalControl, options) {
|
|
1492
|
+
try {
|
|
1493
|
+
const screen = await createTerminalControlProvider(options).capture(terminalControl.target, {
|
|
1494
|
+
scrollbackLines: Number(options.scrollbackLines ?? 120),
|
|
1495
|
+
socketPath: terminalControl.socketPath
|
|
1496
|
+
});
|
|
1497
|
+
const approval = detectCodexApprovalPrompt(screen);
|
|
1498
|
+
const blocked = isCodexApprovalPromptVisible(screen);
|
|
1499
|
+
return {
|
|
1500
|
+
provider: terminalControl.kind,
|
|
1501
|
+
target: terminalControl.target,
|
|
1502
|
+
reachable: true,
|
|
1503
|
+
approval_state: {
|
|
1504
|
+
scanned: true,
|
|
1505
|
+
blocked,
|
|
1506
|
+
approvable: approval.approvable,
|
|
1507
|
+
key: approval.approvable ? approval.key : undefined,
|
|
1508
|
+
label: approval.approvable ? approval.label : undefined,
|
|
1509
|
+
reason: approval.approvable ? undefined : approval.reason
|
|
1510
|
+
},
|
|
1511
|
+
screen: {
|
|
1512
|
+
excerpt: screenExcerpt(screen),
|
|
1513
|
+
approval
|
|
1514
|
+
}
|
|
1515
|
+
};
|
|
1516
|
+
}
|
|
1517
|
+
catch (error) {
|
|
1518
|
+
return {
|
|
1519
|
+
provider: terminalControl.kind,
|
|
1520
|
+
target: terminalControl.target,
|
|
1521
|
+
reachable: false,
|
|
1522
|
+
approval_state: {
|
|
1523
|
+
scanned: false,
|
|
1524
|
+
blocked: false,
|
|
1525
|
+
approvable: false
|
|
1526
|
+
},
|
|
1527
|
+
screen: {
|
|
1528
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1529
|
+
}
|
|
1530
|
+
};
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1569
1533
|
async function runSend(options) {
|
|
1570
1534
|
const messageBody = required(options.message ?? options.request, "--message is required");
|
|
1571
1535
|
cleanupIdleConversations(storeDirFromOptions(options), options);
|
|
@@ -2370,12 +2334,26 @@ function markConversationNeedsRecovery({ conversation, statePath, logPath, execu
|
|
|
2370
2334
|
budget: budgetAction(nextConversation)
|
|
2371
2335
|
};
|
|
2372
2336
|
}
|
|
2373
|
-
function runCancel(options) {
|
|
2337
|
+
async function runCancel(options) {
|
|
2374
2338
|
cleanupIdleConversations(storeDirFromOptions(options), options);
|
|
2375
2339
|
const { conversation, statePath, logPath } = loadConversationFromOptions(options);
|
|
2376
2340
|
if (["closed", "cancelled"].includes(conversation.status)) {
|
|
2377
2341
|
throw new Error(`cannot cancel ${conversation.conversation_id}; conversation is ${conversation.status}`);
|
|
2378
2342
|
}
|
|
2343
|
+
const nativeTakeover = isRecord(conversation.native_session_takeover)
|
|
2344
|
+
? conversation.native_session_takeover
|
|
2345
|
+
: undefined;
|
|
2346
|
+
const terminalControl = terminalControlFromTakeover(nativeTakeover);
|
|
2347
|
+
if (terminalControl) {
|
|
2348
|
+
await runTerminalControlCancel({
|
|
2349
|
+
options,
|
|
2350
|
+
conversation,
|
|
2351
|
+
statePath,
|
|
2352
|
+
logPath,
|
|
2353
|
+
terminalControl
|
|
2354
|
+
});
|
|
2355
|
+
return;
|
|
2356
|
+
}
|
|
2379
2357
|
const executor = executorForConversation(conversation);
|
|
2380
2358
|
const acpxPath = resolveExecutable("acpx");
|
|
2381
2359
|
const executorEnv = environmentForExecutor(executor, {
|
|
@@ -2434,6 +2412,38 @@ function runCancel(options) {
|
|
|
2434
2412
|
budget: budgetAction(nextConversation)
|
|
2435
2413
|
});
|
|
2436
2414
|
}
|
|
2415
|
+
async function runTerminalControlCancel({ options, conversation, statePath, logPath, terminalControl }) {
|
|
2416
|
+
const provider = createTerminalControlProvider(options);
|
|
2417
|
+
await provider.sendKeys(terminalControl.target, ["C-c"], {
|
|
2418
|
+
socketPath: terminalControl.socketPath
|
|
2419
|
+
});
|
|
2420
|
+
const now = new Date().toISOString();
|
|
2421
|
+
appendEvent(logPath, {
|
|
2422
|
+
ts: now,
|
|
2423
|
+
conversation_id: conversation.conversation_id,
|
|
2424
|
+
event: "terminal_cancel_requested",
|
|
2425
|
+
terminal_control: terminalControl,
|
|
2426
|
+
key: "C-c"
|
|
2427
|
+
});
|
|
2428
|
+
runtimeLog("info", "terminal_cancel_requested", {
|
|
2429
|
+
conversation_id: conversation.conversation_id,
|
|
2430
|
+
terminal_target: terminalControl.target,
|
|
2431
|
+
key: "C-c"
|
|
2432
|
+
});
|
|
2433
|
+
const nextConversation = {
|
|
2434
|
+
...conversation,
|
|
2435
|
+
terminal_cancel_requested_at: now,
|
|
2436
|
+
updated_at: now
|
|
2437
|
+
};
|
|
2438
|
+
saveState(statePath, nextConversation);
|
|
2439
|
+
printJson({
|
|
2440
|
+
conversation: nextConversation,
|
|
2441
|
+
cancel_requested: true,
|
|
2442
|
+
terminal_control: terminalControl,
|
|
2443
|
+
key: "C-c",
|
|
2444
|
+
budget: budgetAction(nextConversation)
|
|
2445
|
+
});
|
|
2446
|
+
}
|
|
2437
2447
|
function runRecover(options) {
|
|
2438
2448
|
runRecoveryDecision({ ...options, mode: "recover" });
|
|
2439
2449
|
}
|
|
@@ -3424,6 +3434,7 @@ function waitForPidsToExit(pids, timeoutMs) {
|
|
|
3424
3434
|
function markConversationStalled({ statePath, logPath, reason, detail = {} }) {
|
|
3425
3435
|
const releaseLock = acquireFileLock(`${statePath}.lock`);
|
|
3426
3436
|
let stalledConversation;
|
|
3437
|
+
let stalledMessage;
|
|
3427
3438
|
try {
|
|
3428
3439
|
const conversation = loadState(statePath);
|
|
3429
3440
|
if (!isWaitingForAgent(conversation.status)) {
|
|
@@ -3435,11 +3446,31 @@ function markConversationStalled({ statePath, logPath, reason, detail = {} }) {
|
|
|
3435
3446
|
return conversation;
|
|
3436
3447
|
}
|
|
3437
3448
|
const now = new Date().toISOString();
|
|
3449
|
+
const executor = executorForConversation(conversation);
|
|
3450
|
+
const shouldNotify = Boolean(conversation.gateway_method && !conversation.stalled_notification_sent_at);
|
|
3451
|
+
stalledMessage = shouldNotify
|
|
3452
|
+
? createMessage({
|
|
3453
|
+
conversation,
|
|
3454
|
+
from: executor.actor,
|
|
3455
|
+
to: "openclaw",
|
|
3456
|
+
type: "error",
|
|
3457
|
+
requiresResponse: false,
|
|
3458
|
+
body: [
|
|
3459
|
+
`AKK marked this ${executor.display_name} task as stalled: ${reason}.`,
|
|
3460
|
+
"",
|
|
3461
|
+
`Conversation: ${conversation.conversation_id}`,
|
|
3462
|
+
`Session: ${executor.session}`,
|
|
3463
|
+
"Use `AKK status` for details, `AKK send` to retry/follow up, or `AKK close` to close it."
|
|
3464
|
+
].join("\n")
|
|
3465
|
+
})
|
|
3466
|
+
: undefined;
|
|
3438
3467
|
stalledConversation = {
|
|
3439
3468
|
...conversation,
|
|
3440
3469
|
status: "stalled",
|
|
3441
3470
|
stalled_at: now,
|
|
3442
3471
|
stalled_reason: reason,
|
|
3472
|
+
stalled_notification_sent_at: shouldNotify ? now : conversation.stalled_notification_sent_at,
|
|
3473
|
+
stalled_notification_message_id: stalledMessage?.id ?? conversation.stalled_notification_message_id,
|
|
3443
3474
|
updated_at: now
|
|
3444
3475
|
};
|
|
3445
3476
|
saveState(statePath, stalledConversation);
|
|
@@ -3464,13 +3495,12 @@ function markConversationStalled({ statePath, logPath, reason, detail = {} }) {
|
|
|
3464
3495
|
finally {
|
|
3465
3496
|
releaseLock();
|
|
3466
3497
|
}
|
|
3467
|
-
if (stalledConversation) {
|
|
3498
|
+
if (stalledConversation && stalledMessage) {
|
|
3468
3499
|
deliverStalledNotification({
|
|
3469
3500
|
statePath,
|
|
3470
3501
|
logPath,
|
|
3471
3502
|
conversation: stalledConversation,
|
|
3472
|
-
|
|
3473
|
-
detail
|
|
3503
|
+
message: stalledMessage
|
|
3474
3504
|
});
|
|
3475
3505
|
}
|
|
3476
3506
|
return stalledConversation;
|
|
@@ -3527,27 +3557,10 @@ function markConversationNeedsModelSelection({ statePath, logPath, reason, detai
|
|
|
3527
3557
|
}
|
|
3528
3558
|
return modelSelectionConversation;
|
|
3529
3559
|
}
|
|
3530
|
-
function deliverStalledNotification({ statePath, logPath, conversation,
|
|
3560
|
+
function deliverStalledNotification({ statePath, logPath, conversation, message }) {
|
|
3531
3561
|
if (!conversation.gateway_method) {
|
|
3532
3562
|
return;
|
|
3533
3563
|
}
|
|
3534
|
-
const executor = executorForConversation(conversation);
|
|
3535
|
-
const trace = buildStalledTraceSummary({ conversation, logPath, detail });
|
|
3536
|
-
const message = createMessage({
|
|
3537
|
-
conversation,
|
|
3538
|
-
from: executor.actor,
|
|
3539
|
-
to: "openclaw",
|
|
3540
|
-
type: "error",
|
|
3541
|
-
requiresResponse: false,
|
|
3542
|
-
body: [
|
|
3543
|
-
`AKK marked this ${executor.display_name} task as stalled: ${reason}.`,
|
|
3544
|
-
"",
|
|
3545
|
-
`Conversation: ${conversation.conversation_id}`,
|
|
3546
|
-
`Session: ${executor.session}`,
|
|
3547
|
-
trace ? `Trace: ${trace}` : "",
|
|
3548
|
-
"Use `AKK status` for details, `AKK send` to retry/follow up, or `AKK close` to close it."
|
|
3549
|
-
].filter(Boolean).join("\n")
|
|
3550
|
-
});
|
|
3551
3564
|
const gatewayToken = conversation.gateway_token;
|
|
3552
3565
|
const gatewayUrl = gatewayToken ? conversation.gateway_url : undefined;
|
|
3553
3566
|
const delivery = deliverToGatewayMethod({
|
|
@@ -3566,6 +3579,7 @@ function deliverStalledNotification({ statePath, logPath, conversation, reason,
|
|
|
3566
3579
|
conversation_id: conversation.conversation_id,
|
|
3567
3580
|
event: "stalled_gateway_method_delivery",
|
|
3568
3581
|
method: conversation.gateway_method,
|
|
3582
|
+
message_id: message.id,
|
|
3569
3583
|
status: delivery.status,
|
|
3570
3584
|
stdout: delivery.stdout,
|
|
3571
3585
|
stderr: delivery.stderr
|
|
@@ -3573,6 +3587,7 @@ function deliverStalledNotification({ statePath, logPath, conversation, reason,
|
|
|
3573
3587
|
runtimeLog("info", "stalled_gateway_method_delivery", {
|
|
3574
3588
|
conversation_id: conversation.conversation_id,
|
|
3575
3589
|
method: conversation.gateway_method,
|
|
3590
|
+
message_id: message.id,
|
|
3576
3591
|
status: delivery.status,
|
|
3577
3592
|
failure_kind: classifyProcessFailure(delivery),
|
|
3578
3593
|
stdout: textSummary(delivery.stdout),
|
|
@@ -3596,46 +3611,20 @@ function deliverStalledNotification({ statePath, logPath, conversation, reason,
|
|
|
3596
3611
|
ts: new Date().toISOString(),
|
|
3597
3612
|
conversation_id: conversation.conversation_id,
|
|
3598
3613
|
event: "stalled_chat_send_delivery",
|
|
3614
|
+
message_id: message.id,
|
|
3599
3615
|
status: chatSendDelivery.status,
|
|
3600
3616
|
stdout: chatSendDelivery.stdout,
|
|
3601
3617
|
stderr: chatSendDelivery.stderr
|
|
3602
3618
|
});
|
|
3603
3619
|
runtimeLog("info", "stalled_chat_send_delivery", {
|
|
3604
3620
|
conversation_id: conversation.conversation_id,
|
|
3621
|
+
message_id: message.id,
|
|
3605
3622
|
status: chatSendDelivery.status,
|
|
3606
3623
|
failure_kind: classifyProcessFailure(chatSendDelivery),
|
|
3607
3624
|
stdout: textSummary(chatSendDelivery.stdout),
|
|
3608
3625
|
stderr: textSummary(chatSendDelivery.stderr)
|
|
3609
3626
|
});
|
|
3610
3627
|
}
|
|
3611
|
-
function buildStalledTraceSummary({ conversation, logPath, detail = {} }) {
|
|
3612
|
-
const detailRecord = isRecord(detail) ? detail : {};
|
|
3613
|
-
const outputPath = typeof detailRecord.output_path === "string"
|
|
3614
|
-
? detailRecord.output_path
|
|
3615
|
-
: traceOutputPath({
|
|
3616
|
-
conversation,
|
|
3617
|
-
events: safeReadEvents(logPath),
|
|
3618
|
-
logPath
|
|
3619
|
-
});
|
|
3620
|
-
if (!outputPath || !fs.existsSync(outputPath)) {
|
|
3621
|
-
return undefined;
|
|
3622
|
-
}
|
|
3623
|
-
const output = fs.readFileSync(outputPath, "utf8").slice(-64 * 1024);
|
|
3624
|
-
const parts = output
|
|
3625
|
-
.split(/\r?\n/)
|
|
3626
|
-
.map((line) => sanitizeTraceText(line.trim(), 220))
|
|
3627
|
-
.filter((line) => line &&
|
|
3628
|
-
!line.startsWith("input:") &&
|
|
3629
|
-
!line.startsWith("output:") &&
|
|
3630
|
-
!line.startsWith("{") &&
|
|
3631
|
-
!line.startsWith("}") &&
|
|
3632
|
-
!line.includes("--message-json"))
|
|
3633
|
-
.slice(-6);
|
|
3634
|
-
if (parts.length === 0) {
|
|
3635
|
-
return undefined;
|
|
3636
|
-
}
|
|
3637
|
-
return cleanProcessText(parts.join(" | "))?.slice(0, 500);
|
|
3638
|
-
}
|
|
3639
3628
|
function safeReadEvents(logPath) {
|
|
3640
3629
|
try {
|
|
3641
3630
|
return readNdjsonLog(logPath);
|
|
@@ -4059,8 +4048,7 @@ function usage() {
|
|
|
4059
4048
|
agent-knock-knock close --conversation <id> [--reason <text>]
|
|
4060
4049
|
agent-knock-knock install-openclaw [--openclaw-bin <path>] [--skill-path <path>] [--no-restart]
|
|
4061
4050
|
agent-knock-knock doctor
|
|
4062
|
-
agent-knock-knock agent
|
|
4063
|
-
agent-knock-knock agent takeover --agent codex --session-id <id> --strategy safe_resume|terminate_then_resume|terminal_control|fork [--create-conversation]
|
|
4051
|
+
agent-knock-knock agent takeover --agent codex --session-id <id> --strategy terminate_then_resume|terminal_control|fork [--create-conversation]
|
|
4064
4052
|
agent-knock-knock callback --state <file> --message-json <json> [--record-only]
|
|
4065
4053
|
agent-knock-knock transcript --log <file> [--include-raw]
|
|
4066
4054
|
agent-knock-knock transcript --conversation <dir> [--include-raw]
|