@scotthuang/agent-knock-knock 0.2.0 → 0.2.2
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 +11 -2
- package/README.md +45 -163
- package/dist/src/cli.js +399 -50
- package/dist/src/cli.js.map +1 -1
- package/dist/src/codex-session-provider.d.ts +12 -0
- package/dist/src/codex-session-provider.js.map +1 -1
- package/dist/src/executors.d.ts +0 -6
- package/dist/src/executors.js +0 -6
- package/dist/src/executors.js.map +1 -1
- package/dist/src/openclaw-plugin.js +43 -19
- package/dist/src/openclaw-plugin.js.map +1 -1
- package/dist/src/terminal-control-provider.d.ts +56 -0
- package/dist/src/terminal-control-provider.js +154 -0
- package/dist/src/terminal-control-provider.js.map +1 -0
- package/openclaw.plugin.json +1 -5
- package/package.json +1 -1
- package/templates/openclaw-skills/agent-knock-knock/SKILL.md +12 -8
package/dist/src/cli.js
CHANGED
|
@@ -8,12 +8,13 @@ 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
|
|
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";
|
|
15
15
|
import { appendEvent, defaultStoreDir, listConversations, logPathForStatePath, loadConversationById, loadState, messageEvent, pathsForConversation, pathsForConversationDir, saveState, statePathForConversationId } from "./store.js";
|
|
16
16
|
import { planFork, planSafeResume, planTakeover } from "./session-takeover-planner.js";
|
|
17
|
+
import { StaticTerminalControlProvider, TmuxTerminalControlProvider, enrichActiveProcessesWithTerminalControl } from "./terminal-control-provider.js";
|
|
17
18
|
const DEFAULT_IDLE_TIMEOUT_MINUTES = 10080;
|
|
18
19
|
const DEFAULT_AGENT_TIMEOUT_MINUTES = 60;
|
|
19
20
|
const DEFAULT_MONITOR_POLL_INTERVAL_MS = 5000;
|
|
@@ -89,10 +90,13 @@ async function runCommand(commandName, options) {
|
|
|
89
90
|
runList(options);
|
|
90
91
|
}
|
|
91
92
|
else if (commandName === "status") {
|
|
92
|
-
runStatus(options);
|
|
93
|
+
await runStatus(options);
|
|
93
94
|
}
|
|
94
95
|
else if (commandName === "send") {
|
|
95
|
-
runSend(options);
|
|
96
|
+
await runSend(options);
|
|
97
|
+
}
|
|
98
|
+
else if (commandName === "approve") {
|
|
99
|
+
await runApprove(options);
|
|
96
100
|
}
|
|
97
101
|
else if (commandName === "cancel") {
|
|
98
102
|
runCancel(options);
|
|
@@ -100,9 +104,6 @@ async function runCommand(commandName, options) {
|
|
|
100
104
|
else if (commandName === "recover") {
|
|
101
105
|
runRecover(options);
|
|
102
106
|
}
|
|
103
|
-
else if (commandName === "restart") {
|
|
104
|
-
runRestart(options);
|
|
105
|
-
}
|
|
106
107
|
else if (commandName === "close") {
|
|
107
108
|
runClose(options);
|
|
108
109
|
}
|
|
@@ -245,7 +246,7 @@ async function runAgentDiscover(options) {
|
|
|
245
246
|
agent,
|
|
246
247
|
scope,
|
|
247
248
|
capabilities,
|
|
248
|
-
active: await provider
|
|
249
|
+
active: await listActiveSessionsWithTerminalControl(provider, options)
|
|
249
250
|
};
|
|
250
251
|
}
|
|
251
252
|
throw new Error(`unsupported discover scope: ${scope}`);
|
|
@@ -270,7 +271,7 @@ async function runAgentTakeover(options) {
|
|
|
270
271
|
};
|
|
271
272
|
}
|
|
272
273
|
if (strategy === "safe_resume") {
|
|
273
|
-
const plan = planSafeResume(session, await provider
|
|
274
|
+
const plan = planSafeResume(session, await listActiveSessionsWithTerminalControl(provider, options));
|
|
274
275
|
if (plan.allowed && options.createConversation) {
|
|
275
276
|
const modelInfo = await provider.getSessionModel(session.id);
|
|
276
277
|
const attached = createNativeSessionConversation({
|
|
@@ -300,7 +301,7 @@ async function runAgentTakeover(options) {
|
|
|
300
301
|
};
|
|
301
302
|
}
|
|
302
303
|
if (strategy === "terminate_then_resume") {
|
|
303
|
-
const activeSessions = await provider
|
|
304
|
+
const activeSessions = await listActiveSessionsWithTerminalControl(provider, options);
|
|
304
305
|
const plan = planTakeover(session, activeSessions);
|
|
305
306
|
if (options.confirmTerminate === true) {
|
|
306
307
|
const expectedPid = Number(required(options.expectedPid, "--expected-pid is required with --confirm-terminate"));
|
|
@@ -335,7 +336,7 @@ async function runAgentTakeover(options) {
|
|
|
335
336
|
const termination = terminateProcessTarget(target, {
|
|
336
337
|
timeoutMs: Number(options.terminateTimeoutMs ?? 3000)
|
|
337
338
|
});
|
|
338
|
-
const activeAfterTermination = await provider
|
|
339
|
+
const activeAfterTermination = await listActiveSessionsWithTerminalControl(provider, options);
|
|
339
340
|
const afterTerminationPlan = planTakeover(session, activeAfterTermination);
|
|
340
341
|
if (afterTerminationPlan.targets.some((candidate) => candidate.sessionId === session.id || candidate.pid === expectedPid)) {
|
|
341
342
|
return {
|
|
@@ -382,6 +383,60 @@ async function runAgentTakeover(options) {
|
|
|
382
383
|
plan
|
|
383
384
|
};
|
|
384
385
|
}
|
|
386
|
+
if (strategy === "terminal_control") {
|
|
387
|
+
const activeSessions = await listActiveSessionsWithTerminalControl(provider, options);
|
|
388
|
+
const plan = planTerminalControlTakeover(session, activeSessions);
|
|
389
|
+
if (options.confirmTerminal === true) {
|
|
390
|
+
const terminalTarget = String(required(options.terminalTarget, "--terminal-target is required with --confirm-terminal"));
|
|
391
|
+
if (!options.createConversation) {
|
|
392
|
+
throw new Error("--create-conversation is required with --confirm-terminal");
|
|
393
|
+
}
|
|
394
|
+
const target = plan.targets.find((candidate) => candidate.terminalControl?.target === terminalTarget);
|
|
395
|
+
if (!plan.allowed || !target?.terminalControl) {
|
|
396
|
+
return {
|
|
397
|
+
agent,
|
|
398
|
+
sessionId,
|
|
399
|
+
strategy,
|
|
400
|
+
status: "blocked",
|
|
401
|
+
sideEffectsExecuted: false,
|
|
402
|
+
plan,
|
|
403
|
+
error: {
|
|
404
|
+
code: "terminal_target_unavailable",
|
|
405
|
+
message: `No matching tmux-controlled Codex process was found for ${terminalTarget}`
|
|
406
|
+
}
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
const modelInfo = await provider.getSessionModel(session.id);
|
|
410
|
+
const attached = createNativeSessionConversation({
|
|
411
|
+
agent,
|
|
412
|
+
strategy,
|
|
413
|
+
session,
|
|
414
|
+
modelInfo,
|
|
415
|
+
options,
|
|
416
|
+
takeoverMatchKind: "terminal_control",
|
|
417
|
+
terminalControl: target.terminalControl,
|
|
418
|
+
needsBootstrap: false
|
|
419
|
+
});
|
|
420
|
+
return {
|
|
421
|
+
agent,
|
|
422
|
+
sessionId,
|
|
423
|
+
strategy,
|
|
424
|
+
status: "attached",
|
|
425
|
+
sideEffectsExecuted: true,
|
|
426
|
+
plan,
|
|
427
|
+
terminalControl: target.terminalControl,
|
|
428
|
+
...attached
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
return {
|
|
432
|
+
agent,
|
|
433
|
+
sessionId,
|
|
434
|
+
strategy,
|
|
435
|
+
status: plan.allowed ? "requires_confirmation" : "blocked",
|
|
436
|
+
sideEffectsExecuted: false,
|
|
437
|
+
plan
|
|
438
|
+
};
|
|
439
|
+
}
|
|
385
440
|
if (strategy === "fork") {
|
|
386
441
|
const contextPackage = await provider.getForkContext({
|
|
387
442
|
sessionId,
|
|
@@ -519,6 +574,120 @@ function selectTerminateTarget({ plan, session, activeSessions, expectedPid, all
|
|
|
519
574
|
matchKind: "cwd_only_confirmed"
|
|
520
575
|
};
|
|
521
576
|
}
|
|
577
|
+
async function listActiveSessionsWithTerminalControl(provider, options) {
|
|
578
|
+
const activeSessions = await provider.listActiveSessions();
|
|
579
|
+
return enrichActiveProcessesWithTerminalControl(activeSessions, createTerminalControlProvider(options));
|
|
580
|
+
}
|
|
581
|
+
function createTerminalControlProvider(options) {
|
|
582
|
+
if (options.terminalsJson || options.terminalScreensJson) {
|
|
583
|
+
return new StaticTerminalControlProvider({
|
|
584
|
+
panes: parseJsonOption(options.terminalsJson, "--terminals-json"),
|
|
585
|
+
screens: parseJsonOption(options.terminalScreensJson, "--terminal-screens-json")
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
return new TmuxTerminalControlProvider();
|
|
589
|
+
}
|
|
590
|
+
function planTerminalControlTakeover(session, activeSessions) {
|
|
591
|
+
const matched = activeSessions
|
|
592
|
+
.filter((process) => process.kind === "codex_cli" &&
|
|
593
|
+
process.terminalControl &&
|
|
594
|
+
(process.sessionId === session.id ||
|
|
595
|
+
(!process.sessionId && process.cwd === session.cwd)));
|
|
596
|
+
const matchedPidSet = new Set(matched.map((process) => process.pid));
|
|
597
|
+
const targets = matched
|
|
598
|
+
.filter((process) => !process.ppid || !matchedPidSet.has(process.ppid))
|
|
599
|
+
.map((process) => ({
|
|
600
|
+
pid: process.pid,
|
|
601
|
+
childPids: matched
|
|
602
|
+
.filter((child) => child.ppid === process.pid)
|
|
603
|
+
.map((child) => child.pid),
|
|
604
|
+
cwd: process.cwd,
|
|
605
|
+
command: process.command,
|
|
606
|
+
sessionId: process.sessionId,
|
|
607
|
+
terminalControl: process.terminalControl
|
|
608
|
+
}));
|
|
609
|
+
const exactTargets = targets.filter((target) => target.sessionId === session.id);
|
|
610
|
+
const selectableTargets = exactTargets.length > 0 ? exactTargets : targets;
|
|
611
|
+
return {
|
|
612
|
+
mode: "terminal_control",
|
|
613
|
+
allowed: selectableTargets.length === 1,
|
|
614
|
+
requiresConfirmation: selectableTargets.length === 1,
|
|
615
|
+
reason: selectableTargets.length === 0
|
|
616
|
+
? "no_terminal_control_target"
|
|
617
|
+
: selectableTargets.length === 1
|
|
618
|
+
? "terminal_control_available"
|
|
619
|
+
: "ambiguous_terminal_control_target",
|
|
620
|
+
targets: selectableTargets
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
function terminalControlFromTakeover(nativeTakeover) {
|
|
624
|
+
if (!isRecord(nativeTakeover)) {
|
|
625
|
+
return undefined;
|
|
626
|
+
}
|
|
627
|
+
const terminalControl = nativeTakeover["terminal_control"];
|
|
628
|
+
if (!isRecord(terminalControl) || terminalControl.kind !== "tmux") {
|
|
629
|
+
return undefined;
|
|
630
|
+
}
|
|
631
|
+
const target = stringValue(terminalControl.target);
|
|
632
|
+
const session = stringValue(terminalControl.session);
|
|
633
|
+
const window = Number(terminalControl.window);
|
|
634
|
+
const pane = Number(terminalControl.pane);
|
|
635
|
+
const panePid = Number(terminalControl.panePid);
|
|
636
|
+
if (!target || !session || !Number.isInteger(window) || !Number.isInteger(pane) || !Number.isInteger(panePid)) {
|
|
637
|
+
return undefined;
|
|
638
|
+
}
|
|
639
|
+
return {
|
|
640
|
+
kind: "tmux",
|
|
641
|
+
target,
|
|
642
|
+
session,
|
|
643
|
+
window,
|
|
644
|
+
pane,
|
|
645
|
+
panePid,
|
|
646
|
+
currentCommand: stringValue(terminalControl.currentCommand),
|
|
647
|
+
currentPath: stringValue(terminalControl.currentPath),
|
|
648
|
+
capabilities: ["capture_screen", "send_keys", "terminal_approval"]
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
function detectCodexApprovalPrompt(screen) {
|
|
652
|
+
const approvalMarkers = [
|
|
653
|
+
"Would you like to run the following command?",
|
|
654
|
+
"Would you like to make the following edits?",
|
|
655
|
+
"Would you like to grant these permissions?",
|
|
656
|
+
"needs your approval."
|
|
657
|
+
];
|
|
658
|
+
if (!approvalMarkers.some((marker) => screen.includes(marker))) {
|
|
659
|
+
return {
|
|
660
|
+
approvable: false,
|
|
661
|
+
reason: "no Codex approval prompt was detected in the terminal screen"
|
|
662
|
+
};
|
|
663
|
+
}
|
|
664
|
+
for (const line of screen.split(/\r?\n/)) {
|
|
665
|
+
const match = /^[\s›]*1\.\s+(Yes,[^(]+)\(([^)]+)\)/u.exec(line.trim());
|
|
666
|
+
if (!match) {
|
|
667
|
+
continue;
|
|
668
|
+
}
|
|
669
|
+
const key = match[2].trim();
|
|
670
|
+
if (key !== "y") {
|
|
671
|
+
return {
|
|
672
|
+
approvable: false,
|
|
673
|
+
reason: `primary approval shortcut is ${key}, not y`
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
return {
|
|
677
|
+
approvable: true,
|
|
678
|
+
key,
|
|
679
|
+
label: match[1].trim()
|
|
680
|
+
};
|
|
681
|
+
}
|
|
682
|
+
return {
|
|
683
|
+
approvable: false,
|
|
684
|
+
reason: "no primary approve option with a shortcut was detected"
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
function screenExcerpt(screen, maxLength = 4000) {
|
|
688
|
+
const lines = screen.trimEnd().split(/\r?\n/);
|
|
689
|
+
return lines.slice(Math.max(0, lines.length - 80)).join("\n").slice(-maxLength);
|
|
690
|
+
}
|
|
522
691
|
function createForkConversation({ agent, strategy, session, contextPackage, forkSummary, modelInfo, options }) {
|
|
523
692
|
const workspace = session.cwd;
|
|
524
693
|
const storeDir = expandHome(options.storeDir ?? options.logDir ?? defaultStoreDir(workspace));
|
|
@@ -612,7 +781,7 @@ function createForkConversation({ agent, strategy, session, contextPackage, fork
|
|
|
612
781
|
next: `Use AKK send ${forkedConversation.conversation_id}: <message> to start the forked ${agent} session with the approved summary.`
|
|
613
782
|
};
|
|
614
783
|
}
|
|
615
|
-
function createNativeSessionConversation({ agent, strategy, session, modelInfo, options, takeoverMatchKind = strategy }) {
|
|
784
|
+
function createNativeSessionConversation({ agent, strategy, session, modelInfo, options, takeoverMatchKind = strategy, terminalControl = undefined, needsBootstrap = true }) {
|
|
616
785
|
const workspace = session.cwd;
|
|
617
786
|
const storeDir = expandHome(options.storeDir ?? options.logDir ?? defaultStoreDir(workspace));
|
|
618
787
|
cleanupIdleConversations(storeDir, options);
|
|
@@ -669,7 +838,8 @@ function createNativeSessionConversation({ agent, strategy, session, modelInfo,
|
|
|
669
838
|
acpx_model: modelInfo?.acpxModel,
|
|
670
839
|
model_source: modelInfo?.source,
|
|
671
840
|
takeover_match_kind: takeoverMatchKind,
|
|
672
|
-
|
|
841
|
+
terminal_control: terminalControl,
|
|
842
|
+
needs_bootstrap: needsBootstrap
|
|
673
843
|
}
|
|
674
844
|
}, paths);
|
|
675
845
|
saveState(paths.statePath, attachedConversation);
|
|
@@ -1121,7 +1291,7 @@ function runList(options) {
|
|
|
1121
1291
|
cleanup
|
|
1122
1292
|
});
|
|
1123
1293
|
}
|
|
1124
|
-
function runStatus(options) {
|
|
1294
|
+
async function runStatus(options) {
|
|
1125
1295
|
cleanupIdleConversations(storeDirFromOptions(options), options);
|
|
1126
1296
|
const { conversation, statePath, logPath } = loadConversationFromOptions(options);
|
|
1127
1297
|
const events = readExistingEvents(logPath);
|
|
@@ -1136,6 +1306,24 @@ function runStatus(options) {
|
|
|
1136
1306
|
if (options.trace) {
|
|
1137
1307
|
result.trace = buildConversationTrace({ conversation, events, logPath });
|
|
1138
1308
|
}
|
|
1309
|
+
const terminalControl = terminalControlFromTakeover(isRecord(conversation.native_session_takeover) ? conversation.native_session_takeover : undefined);
|
|
1310
|
+
if (terminalControl) {
|
|
1311
|
+
result.terminal_control = terminalControl;
|
|
1312
|
+
try {
|
|
1313
|
+
const screen = await createTerminalControlProvider(options).capture(terminalControl.target, {
|
|
1314
|
+
scrollbackLines: Number(options.scrollbackLines ?? 120)
|
|
1315
|
+
});
|
|
1316
|
+
result.terminal_screen = {
|
|
1317
|
+
excerpt: screenExcerpt(screen),
|
|
1318
|
+
approval: detectCodexApprovalPrompt(screen)
|
|
1319
|
+
};
|
|
1320
|
+
}
|
|
1321
|
+
catch (error) {
|
|
1322
|
+
result.terminal_screen = {
|
|
1323
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1324
|
+
};
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1139
1327
|
printJson(result);
|
|
1140
1328
|
runtimeLog("info", "task_status_read", {
|
|
1141
1329
|
conversation_id: conversation.conversation_id,
|
|
@@ -1146,7 +1334,7 @@ function runStatus(options) {
|
|
|
1146
1334
|
trace: Boolean(options.trace)
|
|
1147
1335
|
});
|
|
1148
1336
|
}
|
|
1149
|
-
function runSend(options) {
|
|
1337
|
+
async function runSend(options) {
|
|
1150
1338
|
const messageBody = required(options.message ?? options.request, "--message is required");
|
|
1151
1339
|
cleanupIdleConversations(storeDirFromOptions(options), options);
|
|
1152
1340
|
const { conversation, statePath, logPath } = loadConversationFromOptions(options);
|
|
@@ -1154,7 +1342,7 @@ function runSend(options) {
|
|
|
1154
1342
|
throw new Error(`cannot send to ${conversation.conversation_id}; conversation is ${conversation.status}`);
|
|
1155
1343
|
}
|
|
1156
1344
|
if (conversation.status === "needs_recovery") {
|
|
1157
|
-
throw new Error(`cannot send to ${conversation.conversation_id}; choose recover,
|
|
1345
|
+
throw new Error(`cannot send to ${conversation.conversation_id}; choose recover, close, or delegate a new task first`);
|
|
1158
1346
|
}
|
|
1159
1347
|
if (conversation.status === "needs_model_selection" && !options.model) {
|
|
1160
1348
|
throw new Error(`cannot send to ${conversation.conversation_id}; choose a supported model with --model first`);
|
|
@@ -1218,6 +1406,21 @@ function runSend(options) {
|
|
|
1218
1406
|
includeForkTakeoverBootstrap: needsForkTakeoverBootstrap,
|
|
1219
1407
|
forkTakeover: forkTakeoverForSend
|
|
1220
1408
|
});
|
|
1409
|
+
const terminalControlForSend = terminalControlFromTakeover(nativeTakeoverForSend);
|
|
1410
|
+
if (terminalControlForSend) {
|
|
1411
|
+
await runTerminalControlSend({
|
|
1412
|
+
options,
|
|
1413
|
+
conversation,
|
|
1414
|
+
nextConversation,
|
|
1415
|
+
statePath,
|
|
1416
|
+
logPath,
|
|
1417
|
+
executor,
|
|
1418
|
+
message,
|
|
1419
|
+
terminalControl: terminalControlForSend,
|
|
1420
|
+
needsNativeTakeoverBootstrap
|
|
1421
|
+
});
|
|
1422
|
+
return;
|
|
1423
|
+
}
|
|
1221
1424
|
if (nativeTakeoverForSend?.["native_session_id"] && executor.kind === "codex") {
|
|
1222
1425
|
runNativeCodexResumeSend({
|
|
1223
1426
|
options,
|
|
@@ -1264,7 +1467,7 @@ function runSend(options) {
|
|
|
1264
1467
|
stderr: textSummary(cleanProcessText(ensureSession.stderr))
|
|
1265
1468
|
});
|
|
1266
1469
|
if (ensureSession.error) {
|
|
1267
|
-
if (requiresExplicitRecoveryDecision(
|
|
1470
|
+
if (requiresExplicitRecoveryDecision(options)) {
|
|
1268
1471
|
printJson(markConversationNeedsRecovery({
|
|
1269
1472
|
conversation: nextConversation,
|
|
1270
1473
|
statePath,
|
|
@@ -1277,10 +1480,22 @@ function runSend(options) {
|
|
|
1277
1480
|
}));
|
|
1278
1481
|
return;
|
|
1279
1482
|
}
|
|
1280
|
-
|
|
1483
|
+
autoRecoverSendFailure({
|
|
1484
|
+
options,
|
|
1485
|
+
conversation: nextConversation,
|
|
1486
|
+
statePath,
|
|
1487
|
+
logPath,
|
|
1488
|
+
executor,
|
|
1489
|
+
message,
|
|
1490
|
+
failedStage: "session_ensure",
|
|
1491
|
+
result: ensureSession,
|
|
1492
|
+
reason: `acpx ${executor.kind} session ensure failed to start: ${ensureSession.error.message}`
|
|
1493
|
+
});
|
|
1494
|
+
return;
|
|
1281
1495
|
}
|
|
1282
1496
|
if (ensureSession.status !== 0) {
|
|
1283
|
-
|
|
1497
|
+
const reason = cleanProcessText(ensureSession.stderr || ensureSession.stdout || `acpx ${executor.kind} sessions ensure exited with status ${ensureSession.status}`);
|
|
1498
|
+
if (requiresExplicitRecoveryDecision(options)) {
|
|
1284
1499
|
printJson(markConversationNeedsRecovery({
|
|
1285
1500
|
conversation: nextConversation,
|
|
1286
1501
|
statePath,
|
|
@@ -1289,11 +1504,22 @@ function runSend(options) {
|
|
|
1289
1504
|
message,
|
|
1290
1505
|
failedStage: "session_ensure",
|
|
1291
1506
|
result: ensureSession,
|
|
1292
|
-
reason
|
|
1507
|
+
reason
|
|
1293
1508
|
}));
|
|
1294
1509
|
return;
|
|
1295
1510
|
}
|
|
1296
|
-
|
|
1511
|
+
autoRecoverSendFailure({
|
|
1512
|
+
options,
|
|
1513
|
+
conversation: nextConversation,
|
|
1514
|
+
statePath,
|
|
1515
|
+
logPath,
|
|
1516
|
+
executor,
|
|
1517
|
+
message,
|
|
1518
|
+
failedStage: "session_ensure",
|
|
1519
|
+
result: ensureSession,
|
|
1520
|
+
reason
|
|
1521
|
+
});
|
|
1522
|
+
return;
|
|
1297
1523
|
}
|
|
1298
1524
|
const acpxArgs = buildAcpxPromptArgs({ executor, payload, model: executorModel });
|
|
1299
1525
|
if (options.background) {
|
|
@@ -1386,7 +1612,7 @@ function runSend(options) {
|
|
|
1386
1612
|
stderr: textSummary(cleanProcessText(sendResult.stderr))
|
|
1387
1613
|
});
|
|
1388
1614
|
if (sendResult.error) {
|
|
1389
|
-
if (requiresExplicitRecoveryDecision(
|
|
1615
|
+
if (requiresExplicitRecoveryDecision(options)) {
|
|
1390
1616
|
printJson(markConversationNeedsRecovery({
|
|
1391
1617
|
conversation: nextConversation,
|
|
1392
1618
|
statePath,
|
|
@@ -1399,10 +1625,22 @@ function runSend(options) {
|
|
|
1399
1625
|
}));
|
|
1400
1626
|
return;
|
|
1401
1627
|
}
|
|
1402
|
-
|
|
1628
|
+
autoRecoverSendFailure({
|
|
1629
|
+
options,
|
|
1630
|
+
conversation: nextConversation,
|
|
1631
|
+
statePath,
|
|
1632
|
+
logPath,
|
|
1633
|
+
executor,
|
|
1634
|
+
message,
|
|
1635
|
+
failedStage: "message_send",
|
|
1636
|
+
result: sendResult,
|
|
1637
|
+
reason: `acpx ${executor.kind} send failed to start: ${sendResult.error.message}`
|
|
1638
|
+
});
|
|
1639
|
+
return;
|
|
1403
1640
|
}
|
|
1404
1641
|
if (sendResult.status !== 0) {
|
|
1405
|
-
|
|
1642
|
+
const reason = cleanProcessText(sendResult.stderr || sendResult.stdout || `acpx ${executor.kind} send exited with status ${sendResult.status}`);
|
|
1643
|
+
if (requiresExplicitRecoveryDecision(options)) {
|
|
1406
1644
|
printJson(markConversationNeedsRecovery({
|
|
1407
1645
|
conversation: nextConversation,
|
|
1408
1646
|
statePath,
|
|
@@ -1411,11 +1649,22 @@ function runSend(options) {
|
|
|
1411
1649
|
message,
|
|
1412
1650
|
failedStage: "message_send",
|
|
1413
1651
|
result: sendResult,
|
|
1414
|
-
reason
|
|
1652
|
+
reason
|
|
1415
1653
|
}));
|
|
1416
1654
|
return;
|
|
1417
1655
|
}
|
|
1418
|
-
|
|
1656
|
+
autoRecoverSendFailure({
|
|
1657
|
+
options,
|
|
1658
|
+
conversation: nextConversation,
|
|
1659
|
+
statePath,
|
|
1660
|
+
logPath,
|
|
1661
|
+
executor,
|
|
1662
|
+
message,
|
|
1663
|
+
failedStage: "message_send",
|
|
1664
|
+
result: sendResult,
|
|
1665
|
+
reason
|
|
1666
|
+
});
|
|
1667
|
+
return;
|
|
1419
1668
|
}
|
|
1420
1669
|
const deliveredConversation = markTakeoverBootstrapped({
|
|
1421
1670
|
conversation: nextConversation,
|
|
@@ -1433,6 +1682,94 @@ function runSend(options) {
|
|
|
1433
1682
|
budget: budgetAction(deliveredConversation)
|
|
1434
1683
|
});
|
|
1435
1684
|
}
|
|
1685
|
+
async function runApprove(options) {
|
|
1686
|
+
cleanupIdleConversations(storeDirFromOptions(options), options);
|
|
1687
|
+
const { conversation, statePath, logPath } = loadConversationFromOptions(options);
|
|
1688
|
+
const nativeTakeover = isRecord(conversation.native_session_takeover)
|
|
1689
|
+
? conversation.native_session_takeover
|
|
1690
|
+
: undefined;
|
|
1691
|
+
const terminalControl = terminalControlFromTakeover(nativeTakeover);
|
|
1692
|
+
if (!terminalControl) {
|
|
1693
|
+
throw new Error(`conversation ${conversation.conversation_id} is not controlled through a terminal`);
|
|
1694
|
+
}
|
|
1695
|
+
const provider = createTerminalControlProvider(options);
|
|
1696
|
+
const screen = await provider.capture(terminalControl.target, {
|
|
1697
|
+
scrollbackLines: Number(options.scrollbackLines ?? 120)
|
|
1698
|
+
});
|
|
1699
|
+
const approval = detectCodexApprovalPrompt(screen);
|
|
1700
|
+
if (!approval.approvable) {
|
|
1701
|
+
printJson({
|
|
1702
|
+
conversation,
|
|
1703
|
+
approved: false,
|
|
1704
|
+
blocked: true,
|
|
1705
|
+
reason: approval.reason,
|
|
1706
|
+
terminal_control: terminalControl,
|
|
1707
|
+
screen_excerpt: screenExcerpt(screen)
|
|
1708
|
+
});
|
|
1709
|
+
return;
|
|
1710
|
+
}
|
|
1711
|
+
await provider.sendKeys(terminalControl.target, [approval.key]);
|
|
1712
|
+
appendEvent(logPath, {
|
|
1713
|
+
ts: new Date().toISOString(),
|
|
1714
|
+
conversation_id: conversation.conversation_id,
|
|
1715
|
+
event: "terminal_approval_send",
|
|
1716
|
+
terminal_control: terminalControl,
|
|
1717
|
+
key: approval.key,
|
|
1718
|
+
label: approval.label
|
|
1719
|
+
});
|
|
1720
|
+
runtimeLog("info", "terminal_approval_send", {
|
|
1721
|
+
conversation_id: conversation.conversation_id,
|
|
1722
|
+
terminal_target: terminalControl.target,
|
|
1723
|
+
key: approval.key,
|
|
1724
|
+
label: approval.label
|
|
1725
|
+
});
|
|
1726
|
+
saveState(statePath, {
|
|
1727
|
+
...conversation,
|
|
1728
|
+
updated_at: new Date().toISOString()
|
|
1729
|
+
});
|
|
1730
|
+
printJson({
|
|
1731
|
+
conversation,
|
|
1732
|
+
approved: true,
|
|
1733
|
+
terminal_control: terminalControl,
|
|
1734
|
+
key: approval.key,
|
|
1735
|
+
label: approval.label
|
|
1736
|
+
});
|
|
1737
|
+
}
|
|
1738
|
+
async function runTerminalControlSend({ options, conversation, nextConversation, statePath, logPath, executor, message, terminalControl, needsNativeTakeoverBootstrap }) {
|
|
1739
|
+
const provider = createTerminalControlProvider(options);
|
|
1740
|
+
await provider.sendText(terminalControl.target, String(message.body ?? ""));
|
|
1741
|
+
await provider.sendKeys(terminalControl.target, ["Enter"]);
|
|
1742
|
+
appendEvent(logPath, {
|
|
1743
|
+
ts: new Date().toISOString(),
|
|
1744
|
+
conversation_id: conversation.conversation_id,
|
|
1745
|
+
event: "terminal_message_send",
|
|
1746
|
+
executor,
|
|
1747
|
+
terminal_control: terminalControl,
|
|
1748
|
+
message: textSummary(message.body)
|
|
1749
|
+
});
|
|
1750
|
+
runtimeLog("info", "terminal_message_send", {
|
|
1751
|
+
conversation_id: conversation.conversation_id,
|
|
1752
|
+
agent: executor.kind,
|
|
1753
|
+
terminal_target: terminalControl.target,
|
|
1754
|
+
message: textSummary(message.body)
|
|
1755
|
+
});
|
|
1756
|
+
const deliveredConversation = markTakeoverBootstrapped({
|
|
1757
|
+
conversation: nextConversation,
|
|
1758
|
+
statePath,
|
|
1759
|
+
logPath,
|
|
1760
|
+
executor,
|
|
1761
|
+
native: needsNativeTakeoverBootstrap,
|
|
1762
|
+
fork: false
|
|
1763
|
+
});
|
|
1764
|
+
printJson({
|
|
1765
|
+
conversation: deliveredConversation,
|
|
1766
|
+
message,
|
|
1767
|
+
delivered: true,
|
|
1768
|
+
terminal_control: terminalControl,
|
|
1769
|
+
executor,
|
|
1770
|
+
budget: budgetAction(deliveredConversation)
|
|
1771
|
+
});
|
|
1772
|
+
}
|
|
1436
1773
|
function runNativeCodexResumeSend({ options, conversation, nextConversation, statePath, logPath, executor, executorEnv, message, payload, nativeTakeover, needsNativeTakeoverBootstrap }) {
|
|
1437
1774
|
const codexPath = resolveExecutable("codex");
|
|
1438
1775
|
const nativeSessionId = String(nativeTakeover["native_session_id"]);
|
|
@@ -1715,11 +2052,35 @@ function markForkSessionBootstrapped({ conversation, statePath, logPath, executo
|
|
|
1715
2052
|
});
|
|
1716
2053
|
return nextConversation;
|
|
1717
2054
|
}
|
|
1718
|
-
function requiresExplicitRecoveryDecision(
|
|
2055
|
+
function requiresExplicitRecoveryDecision(options = {}) {
|
|
1719
2056
|
if (options.recoveryPolicy === "explicit" || options.recoveryPolicy === "explicit-decision") {
|
|
1720
2057
|
return true;
|
|
1721
2058
|
}
|
|
1722
|
-
return
|
|
2059
|
+
return false;
|
|
2060
|
+
}
|
|
2061
|
+
function autoRecoverSendFailure({ options, conversation, statePath, logPath, executor, message, failedStage, result, reason }) {
|
|
2062
|
+
markConversationNeedsRecovery({
|
|
2063
|
+
conversation,
|
|
2064
|
+
statePath,
|
|
2065
|
+
logPath,
|
|
2066
|
+
executor,
|
|
2067
|
+
message,
|
|
2068
|
+
failedStage,
|
|
2069
|
+
result,
|
|
2070
|
+
reason
|
|
2071
|
+
});
|
|
2072
|
+
runtimeLog("info", "conversation_auto_recovery_start", {
|
|
2073
|
+
conversation_id: conversation.conversation_id,
|
|
2074
|
+
agent: executor.kind,
|
|
2075
|
+
executor_session: executor.session,
|
|
2076
|
+
failed_stage: failedStage,
|
|
2077
|
+
reason: textSummary(reason)
|
|
2078
|
+
});
|
|
2079
|
+
runRecoveryDecision({
|
|
2080
|
+
...options,
|
|
2081
|
+
mode: "recover",
|
|
2082
|
+
autoRecovered: true
|
|
2083
|
+
});
|
|
1723
2084
|
}
|
|
1724
2085
|
function markConversationNeedsRecovery({ conversation, statePath, logPath, executor, message, failedStage, result, reason }) {
|
|
1725
2086
|
const now = new Date().toISOString();
|
|
@@ -1733,7 +2094,7 @@ function markConversationNeedsRecovery({ conversation, statePath, logPath, execu
|
|
|
1733
2094
|
failed_message_id: message.id,
|
|
1734
2095
|
pending_message: message,
|
|
1735
2096
|
previous_executor: executor,
|
|
1736
|
-
options: ["recover", "
|
|
2097
|
+
options: ["recover", "close", "delegate"]
|
|
1737
2098
|
};
|
|
1738
2099
|
const nextConversation = {
|
|
1739
2100
|
...conversation,
|
|
@@ -1837,9 +2198,6 @@ function runCancel(options) {
|
|
|
1837
2198
|
function runRecover(options) {
|
|
1838
2199
|
runRecoveryDecision({ ...options, mode: "recover" });
|
|
1839
2200
|
}
|
|
1840
|
-
function runRestart(options) {
|
|
1841
|
-
runRecoveryDecision({ ...options, mode: "restart" });
|
|
1842
|
-
}
|
|
1843
2201
|
function runRecoveryDecision(options) {
|
|
1844
2202
|
cleanupIdleConversations(storeDirFromOptions(options), options);
|
|
1845
2203
|
const { conversation, statePath, logPath } = loadConversationFromOptions(options);
|
|
@@ -1871,9 +2229,7 @@ function runRecoveryDecision(options) {
|
|
|
1871
2229
|
updated_at: now
|
|
1872
2230
|
};
|
|
1873
2231
|
saveState(statePath, recoveredConversation);
|
|
1874
|
-
const payload =
|
|
1875
|
-
? buildRecoverPayload({ conversation, pendingMessage, logPath })
|
|
1876
|
-
: buildRestartPayload({ pendingMessage });
|
|
2232
|
+
const payload = buildRecoverPayload({ conversation, pendingMessage, logPath });
|
|
1877
2233
|
const acpxPath = resolveExecutable("acpx");
|
|
1878
2234
|
const executorEnv = environmentForExecutor(executor, {
|
|
1879
2235
|
allProxy: options.allProxy ?? conversation.executor_all_proxy
|
|
@@ -1935,8 +2291,8 @@ function runRecoveryDecision(options) {
|
|
|
1935
2291
|
});
|
|
1936
2292
|
printJson({
|
|
1937
2293
|
conversation: recoveredConversation,
|
|
1938
|
-
recovered:
|
|
1939
|
-
|
|
2294
|
+
recovered: true,
|
|
2295
|
+
auto_recovered: Boolean(options.autoRecovered),
|
|
1940
2296
|
background: true,
|
|
1941
2297
|
pid: child.pid ?? null,
|
|
1942
2298
|
monitor_pid: monitor.pid ?? null,
|
|
@@ -1970,8 +2326,8 @@ function runRecoveryDecision(options) {
|
|
|
1970
2326
|
}
|
|
1971
2327
|
printJson({
|
|
1972
2328
|
conversation: recoveredConversation,
|
|
1973
|
-
recovered:
|
|
1974
|
-
|
|
2329
|
+
recovered: true,
|
|
2330
|
+
auto_recovered: Boolean(options.autoRecovered),
|
|
1975
2331
|
delivered: true,
|
|
1976
2332
|
executor,
|
|
1977
2333
|
budget: budgetAction(recoveredConversation)
|
|
@@ -1994,16 +2350,6 @@ function buildRecoverPayload({ conversation, pendingMessage, logPath }) {
|
|
|
1994
2350
|
JSON.stringify(pendingMessage)
|
|
1995
2351
|
].join("\n");
|
|
1996
2352
|
}
|
|
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
2353
|
function formatProtocolHistoryForRecovery(events) {
|
|
2008
2354
|
const lines = events
|
|
2009
2355
|
.filter((event) => event.event === "message")
|
|
@@ -3297,6 +3643,9 @@ function required(value, message) {
|
|
|
3297
3643
|
function isRecord(value) {
|
|
3298
3644
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
3299
3645
|
}
|
|
3646
|
+
function stringValue(value) {
|
|
3647
|
+
return typeof value === "string" && value.trim().length > 0 ? value : undefined;
|
|
3648
|
+
}
|
|
3300
3649
|
function parseOptionalJson(text) {
|
|
3301
3650
|
try {
|
|
3302
3651
|
return JSON.parse(String(text));
|
|
@@ -3465,14 +3814,14 @@ function usage() {
|
|
|
3465
3814
|
agent-knock-knock list [--store-dir <dir>] [--agent ${agentList}] [--status <status>] [--all]
|
|
3466
3815
|
agent-knock-knock status --conversation <id> [--store-dir <dir>] [--trace]
|
|
3467
3816
|
agent-knock-knock send --conversation <id> --message <text> [--type answer|task|control] [--all-proxy <url>] [--agent-timeout-minutes <minutes>]
|
|
3817
|
+
agent-knock-knock approve --conversation <id>
|
|
3468
3818
|
agent-knock-knock cancel --conversation <id> [--all-proxy <url>]
|
|
3469
3819
|
agent-knock-knock recover --conversation <id> [--session <name>] [--all-proxy <url>]
|
|
3470
|
-
agent-knock-knock restart --conversation <id> [--session <name>] [--all-proxy <url>]
|
|
3471
3820
|
agent-knock-knock close --conversation <id> [--reason <text>]
|
|
3472
3821
|
agent-knock-knock install-openclaw [--openclaw-bin <path>] [--skill-path <path>] [--no-restart]
|
|
3473
3822
|
agent-knock-knock doctor
|
|
3474
3823
|
agent-knock-knock agent discover --agent codex --scope capabilities|sessions|active
|
|
3475
|
-
agent-knock-knock agent takeover --agent codex --session-id <id> --strategy safe_resume|terminate_then_resume|fork [--create-conversation]
|
|
3824
|
+
agent-knock-knock agent takeover --agent codex --session-id <id> --strategy safe_resume|terminate_then_resume|terminal_control|fork [--create-conversation]
|
|
3476
3825
|
agent-knock-knock callback --state <file> --message-json <json> [--record-only]
|
|
3477
3826
|
agent-knock-knock transcript --log <file> [--include-raw]
|
|
3478
3827
|
agent-knock-knock transcript --conversation <dir> [--include-raw]
|