@scotthuang/agent-knock-knock 0.1.2 → 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 +17 -2
- package/README.md +36 -9
- package/dist/src/agent-session-provider.d.ts +25 -0
- package/dist/src/agent-session-provider.js +2 -0
- package/dist/src/agent-session-provider.js.map +1 -0
- package/dist/src/cli.js +1184 -57
- package/dist/src/cli.js.map +1 -1
- package/dist/src/codex-local-session-provider.d.ts +18 -0
- package/dist/src/codex-local-session-provider.js +87 -0
- package/dist/src/codex-local-session-provider.js.map +1 -0
- package/dist/src/codex-session-provider.d.ts +95 -0
- package/dist/src/codex-session-provider.js +304 -0
- package/dist/src/codex-session-provider.js.map +1 -0
- package/dist/src/codex-store-adapter.d.ts +27 -0
- package/dist/src/codex-store-adapter.js +124 -0
- package/dist/src/codex-store-adapter.js.map +1 -0
- 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 +165 -19
- package/dist/src/openclaw-plugin.js.map +1 -1
- package/dist/src/protocol.d.ts +1 -1
- package/dist/src/session-takeover-planner.d.ts +49 -0
- package/dist/src/session-takeover-planner.js +80 -0
- package/dist/src/session-takeover-planner.js.map +1 -0
- package/openclaw.plugin.json +1 -5
- package/package.json +1 -1
- package/templates/openclaw-skills/agent-knock-knock/SKILL.md +36 -8
package/dist/src/cli.js
CHANGED
|
@@ -5,24 +5,59 @@ import fs from "node:fs";
|
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import process from "node:process";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
|
+
import { CodexLocalSessionProvider } from "./codex-local-session-provider.js";
|
|
9
|
+
import { CodexStoreAdapter } from "./codex-store-adapter.js";
|
|
8
10
|
import { applyMessageToConversation, budgetAction, createConversation, createMessage, executorForConversation, extractStructuredMessage, parseMessageJson, resolveExecutor } from "./protocol.js";
|
|
9
|
-
import { EXECUTOR_KINDS, acpxCommandForExecutor, executorDefinitionForKind, modelEnvForExecutor, normalizeModelForExecutor, proxyEnvForExecutor
|
|
11
|
+
import { EXECUTOR_KINDS, acpxCommandForExecutor, executorDefinitionForKind, modelEnvForExecutor, normalizeModelForExecutor, proxyEnvForExecutor } from "./executors.js";
|
|
10
12
|
import { executorBootstrapPrompt } from "./bootstrap.js";
|
|
11
13
|
import { writeRuntimeLog } from "./runtime-log.js";
|
|
12
14
|
import { formatTranscript, readNdjsonLog } from "./transcript.js";
|
|
13
15
|
import { appendEvent, defaultStoreDir, listConversations, logPathForStatePath, loadConversationById, loadState, messageEvent, pathsForConversation, pathsForConversationDir, saveState, statePathForConversationId } from "./store.js";
|
|
16
|
+
import { planFork, planSafeResume, planTakeover } from "./session-takeover-planner.js";
|
|
14
17
|
const DEFAULT_IDLE_TIMEOUT_MINUTES = 10080;
|
|
15
18
|
const DEFAULT_AGENT_TIMEOUT_MINUTES = 60;
|
|
16
19
|
const DEFAULT_MONITOR_POLL_INTERVAL_MS = 5000;
|
|
20
|
+
class InlineCodexSessionAdapter {
|
|
21
|
+
threads;
|
|
22
|
+
processes;
|
|
23
|
+
processBatches;
|
|
24
|
+
processBatchIndex = 0;
|
|
25
|
+
rollouts;
|
|
26
|
+
constructor({ threads, processes, rollouts }) {
|
|
27
|
+
this.threads = Array.isArray(threads) ? threads : [];
|
|
28
|
+
this.processBatches = Array.isArray(processes?.[0])
|
|
29
|
+
? processes
|
|
30
|
+
: [];
|
|
31
|
+
this.processes = Array.isArray(processes) && !Array.isArray(processes[0]) ? processes : [];
|
|
32
|
+
this.rollouts = new Map(Object.entries(rollouts ?? {}));
|
|
33
|
+
}
|
|
34
|
+
async listThreadRows() {
|
|
35
|
+
return this.threads;
|
|
36
|
+
}
|
|
37
|
+
async readRollout(rolloutPath) {
|
|
38
|
+
return this.rollouts.get(rolloutPath);
|
|
39
|
+
}
|
|
40
|
+
async listProcessSnapshots() {
|
|
41
|
+
if (this.processBatches.length > 0) {
|
|
42
|
+
const batch = this.processBatches[Math.min(this.processBatchIndex, this.processBatches.length - 1)];
|
|
43
|
+
this.processBatchIndex += 1;
|
|
44
|
+
return batch;
|
|
45
|
+
}
|
|
46
|
+
return this.processes;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
17
49
|
const command = process.argv[2];
|
|
18
|
-
const
|
|
50
|
+
const rawArgs = process.argv.slice(3);
|
|
51
|
+
const args = command === "agent"
|
|
52
|
+
? { agentCommand: rawArgs[0], ...parseArgs(rawArgs.slice(1)) }
|
|
53
|
+
: parseArgs(rawArgs);
|
|
19
54
|
runtimeLog("info", "cli_start", {
|
|
20
55
|
command: command ?? "help",
|
|
21
56
|
cwd: process.cwd(),
|
|
22
57
|
option_keys: Object.keys(args).sort()
|
|
23
58
|
});
|
|
24
59
|
try {
|
|
25
|
-
runCommand(command, args);
|
|
60
|
+
await runCommand(command, args);
|
|
26
61
|
runtimeLog("info", "cli_finish", {
|
|
27
62
|
command: command ?? "help",
|
|
28
63
|
exit_code: process.exitCode ?? 0
|
|
@@ -37,7 +72,7 @@ catch (error) {
|
|
|
37
72
|
console.error(error.message);
|
|
38
73
|
process.exit(1);
|
|
39
74
|
}
|
|
40
|
-
function runCommand(commandName, options) {
|
|
75
|
+
async function runCommand(commandName, options) {
|
|
41
76
|
if (commandName === "new") {
|
|
42
77
|
runNew(options);
|
|
43
78
|
}
|
|
@@ -65,9 +100,6 @@ function runCommand(commandName, options) {
|
|
|
65
100
|
else if (commandName === "recover") {
|
|
66
101
|
runRecover(options);
|
|
67
102
|
}
|
|
68
|
-
else if (commandName === "restart") {
|
|
69
|
-
runRestart(options);
|
|
70
|
-
}
|
|
71
103
|
else if (commandName === "close") {
|
|
72
104
|
runClose(options);
|
|
73
105
|
}
|
|
@@ -86,6 +118,9 @@ function runCommand(commandName, options) {
|
|
|
86
118
|
else if (commandName === "monitor") {
|
|
87
119
|
runMonitor(options);
|
|
88
120
|
}
|
|
121
|
+
else if (commandName === "agent") {
|
|
122
|
+
await runAgent(options);
|
|
123
|
+
}
|
|
89
124
|
else {
|
|
90
125
|
usage();
|
|
91
126
|
process.exitCode = commandName ? 1 : 0;
|
|
@@ -170,6 +205,495 @@ function runDoctor(options) {
|
|
|
170
205
|
options
|
|
171
206
|
});
|
|
172
207
|
}
|
|
208
|
+
async function runAgent(options) {
|
|
209
|
+
const agentCommand = required(options.agentCommand, "agent subcommand is required: discover or takeover");
|
|
210
|
+
if (agentCommand === "discover") {
|
|
211
|
+
printJson(await runAgentDiscover(options));
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
if (agentCommand === "takeover") {
|
|
215
|
+
printJson(await runAgentTakeover(options));
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
throw new Error(`unsupported agent subcommand: ${agentCommand}`);
|
|
219
|
+
}
|
|
220
|
+
async function runAgentDiscover(options) {
|
|
221
|
+
const agent = required(options.agent, "--agent is required");
|
|
222
|
+
const scope = required(options.scope, "--scope is required");
|
|
223
|
+
const provider = createAgentSessionProvider(agent, options);
|
|
224
|
+
const capabilities = await provider.getCapabilities();
|
|
225
|
+
if (scope === "capabilities") {
|
|
226
|
+
return {
|
|
227
|
+
agent,
|
|
228
|
+
scope,
|
|
229
|
+
capabilities
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
if (scope === "sessions") {
|
|
233
|
+
return {
|
|
234
|
+
agent,
|
|
235
|
+
scope,
|
|
236
|
+
capabilities,
|
|
237
|
+
sessions: await provider.listHistoricalSessions()
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
if (scope === "active") {
|
|
241
|
+
return {
|
|
242
|
+
agent,
|
|
243
|
+
scope,
|
|
244
|
+
capabilities,
|
|
245
|
+
active: await provider.listActiveSessions()
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
throw new Error(`unsupported discover scope: ${scope}`);
|
|
249
|
+
}
|
|
250
|
+
async function runAgentTakeover(options) {
|
|
251
|
+
const agent = required(options.agent, "--agent is required");
|
|
252
|
+
const sessionId = required(options.sessionId, "--session-id is required");
|
|
253
|
+
const strategy = options.strategy ?? "terminate_then_resume";
|
|
254
|
+
const provider = createAgentSessionProvider(agent, options);
|
|
255
|
+
const session = await provider.getSession(sessionId);
|
|
256
|
+
if (!session) {
|
|
257
|
+
return {
|
|
258
|
+
agent,
|
|
259
|
+
sessionId,
|
|
260
|
+
strategy,
|
|
261
|
+
status: "blocked",
|
|
262
|
+
sideEffectsExecuted: false,
|
|
263
|
+
error: {
|
|
264
|
+
code: "session_not_found",
|
|
265
|
+
message: `No ${agent} session found for ${sessionId}`
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
if (strategy === "safe_resume") {
|
|
270
|
+
const plan = planSafeResume(session, await provider.listActiveSessions());
|
|
271
|
+
if (plan.allowed && options.createConversation) {
|
|
272
|
+
const modelInfo = await provider.getSessionModel(session.id);
|
|
273
|
+
const attached = createNativeSessionConversation({
|
|
274
|
+
agent,
|
|
275
|
+
strategy,
|
|
276
|
+
session,
|
|
277
|
+
modelInfo,
|
|
278
|
+
options
|
|
279
|
+
});
|
|
280
|
+
return {
|
|
281
|
+
agent,
|
|
282
|
+
sessionId,
|
|
283
|
+
strategy,
|
|
284
|
+
status: "attached",
|
|
285
|
+
sideEffectsExecuted: true,
|
|
286
|
+
plan,
|
|
287
|
+
...attached
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
return {
|
|
291
|
+
agent,
|
|
292
|
+
sessionId,
|
|
293
|
+
strategy,
|
|
294
|
+
status: plan.allowed ? "ready" : "blocked",
|
|
295
|
+
sideEffectsExecuted: false,
|
|
296
|
+
plan
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
if (strategy === "terminate_then_resume") {
|
|
300
|
+
const activeSessions = await provider.listActiveSessions();
|
|
301
|
+
const plan = planTakeover(session, activeSessions);
|
|
302
|
+
if (options.confirmTerminate === true) {
|
|
303
|
+
const expectedPid = Number(required(options.expectedPid, "--expected-pid is required with --confirm-terminate"));
|
|
304
|
+
if (!Number.isInteger(expectedPid) || expectedPid <= 0) {
|
|
305
|
+
throw new Error("--expected-pid must be a positive integer");
|
|
306
|
+
}
|
|
307
|
+
if (!options.createConversation) {
|
|
308
|
+
throw new Error("--create-conversation is required with --confirm-terminate");
|
|
309
|
+
}
|
|
310
|
+
const targetSelection = selectTerminateTarget({
|
|
311
|
+
plan,
|
|
312
|
+
session,
|
|
313
|
+
activeSessions,
|
|
314
|
+
expectedPid,
|
|
315
|
+
allowCwdOnly: options.allowCwdOnly === true
|
|
316
|
+
});
|
|
317
|
+
if (!targetSelection.allowed) {
|
|
318
|
+
return {
|
|
319
|
+
agent,
|
|
320
|
+
sessionId,
|
|
321
|
+
strategy,
|
|
322
|
+
status: "blocked",
|
|
323
|
+
sideEffectsExecuted: false,
|
|
324
|
+
plan,
|
|
325
|
+
error: {
|
|
326
|
+
code: targetSelection.code,
|
|
327
|
+
message: targetSelection.message
|
|
328
|
+
}
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
const { target, matchKind } = targetSelection;
|
|
332
|
+
const termination = terminateProcessTarget(target, {
|
|
333
|
+
timeoutMs: Number(options.terminateTimeoutMs ?? 3000)
|
|
334
|
+
});
|
|
335
|
+
const activeAfterTermination = await provider.listActiveSessions();
|
|
336
|
+
const afterTerminationPlan = planTakeover(session, activeAfterTermination);
|
|
337
|
+
if (afterTerminationPlan.targets.some((candidate) => candidate.sessionId === session.id || candidate.pid === expectedPid)) {
|
|
338
|
+
return {
|
|
339
|
+
agent,
|
|
340
|
+
sessionId,
|
|
341
|
+
strategy,
|
|
342
|
+
status: "blocked",
|
|
343
|
+
sideEffectsExecuted: true,
|
|
344
|
+
plan: afterTerminationPlan,
|
|
345
|
+
termination,
|
|
346
|
+
error: {
|
|
347
|
+
code: "target_still_active",
|
|
348
|
+
message: `Codex process ${expectedPid} still appears active after termination.`
|
|
349
|
+
}
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
const modelInfo = await provider.getSessionModel(session.id);
|
|
353
|
+
const attached = createNativeSessionConversation({
|
|
354
|
+
agent,
|
|
355
|
+
strategy,
|
|
356
|
+
session,
|
|
357
|
+
modelInfo,
|
|
358
|
+
options,
|
|
359
|
+
takeoverMatchKind: matchKind
|
|
360
|
+
});
|
|
361
|
+
return {
|
|
362
|
+
agent,
|
|
363
|
+
sessionId,
|
|
364
|
+
strategy,
|
|
365
|
+
status: "attached",
|
|
366
|
+
sideEffectsExecuted: true,
|
|
367
|
+
plan,
|
|
368
|
+
termination,
|
|
369
|
+
matchKind,
|
|
370
|
+
...attached
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
return {
|
|
374
|
+
agent,
|
|
375
|
+
sessionId,
|
|
376
|
+
strategy,
|
|
377
|
+
status: plan.requiresConfirmation ? "requires_confirmation" : "blocked",
|
|
378
|
+
sideEffectsExecuted: false,
|
|
379
|
+
plan
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
if (strategy === "fork") {
|
|
383
|
+
const contextPackage = await provider.getForkContext({
|
|
384
|
+
sessionId,
|
|
385
|
+
maxMessages: Number(options.maxMessages ?? 12),
|
|
386
|
+
maxCommands: Number(options.maxCommands ?? 8),
|
|
387
|
+
maxTextLength: Number(options.maxTextLength ?? 1200)
|
|
388
|
+
});
|
|
389
|
+
if (!contextPackage) {
|
|
390
|
+
return {
|
|
391
|
+
agent,
|
|
392
|
+
sessionId,
|
|
393
|
+
strategy,
|
|
394
|
+
status: "blocked",
|
|
395
|
+
sideEffectsExecuted: false,
|
|
396
|
+
error: {
|
|
397
|
+
code: "fork_context_unavailable",
|
|
398
|
+
message: `No fork context could be built for ${sessionId}`
|
|
399
|
+
}
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
if (options.createConversation) {
|
|
403
|
+
const forkSummary = String(required(options.forkSummary ?? options.summary, "--fork-summary is required when creating a fork conversation"));
|
|
404
|
+
const modelInfo = await provider.getSessionModel(session.id);
|
|
405
|
+
const attached = createForkConversation({
|
|
406
|
+
agent,
|
|
407
|
+
strategy,
|
|
408
|
+
session,
|
|
409
|
+
contextPackage,
|
|
410
|
+
forkSummary,
|
|
411
|
+
modelInfo,
|
|
412
|
+
options
|
|
413
|
+
});
|
|
414
|
+
return {
|
|
415
|
+
agent,
|
|
416
|
+
sessionId,
|
|
417
|
+
strategy,
|
|
418
|
+
status: "forked",
|
|
419
|
+
sideEffectsExecuted: true,
|
|
420
|
+
plan: planFork(session, contextPackage),
|
|
421
|
+
...attached
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
return {
|
|
425
|
+
agent,
|
|
426
|
+
sessionId,
|
|
427
|
+
strategy,
|
|
428
|
+
status: "awaiting_openclaw_summary",
|
|
429
|
+
sideEffectsExecuted: false,
|
|
430
|
+
plan: planFork(session, contextPackage),
|
|
431
|
+
summaryPrompt: buildForkSummaryPrompt({ agent, session, contextPackage }),
|
|
432
|
+
nextAction: {
|
|
433
|
+
actor: "openclaw",
|
|
434
|
+
action: "summarize_and_confirm_fork",
|
|
435
|
+
instructions: [
|
|
436
|
+
"Summarize plan.contextPackage for the user before creating a forked AKK-managed session.",
|
|
437
|
+
"Do not inject the raw rollout or full contextPackage into the new coding agent.",
|
|
438
|
+
"Ask the user to confirm the summary.",
|
|
439
|
+
"After confirmation, call this tool again with strategy=fork, createConversation=true, and forkSummary set to the confirmed summary."
|
|
440
|
+
],
|
|
441
|
+
followUpTool: "agent_knock_knock_agent_takeover",
|
|
442
|
+
followUpParams: {
|
|
443
|
+
agent,
|
|
444
|
+
sessionId,
|
|
445
|
+
strategy: "fork",
|
|
446
|
+
createConversation: true,
|
|
447
|
+
forkSummary: "<confirmed OpenClaw summary>"
|
|
448
|
+
}
|
|
449
|
+
},
|
|
450
|
+
next: "Use summaryPrompt to summarize the bounded context package for the user, ask for confirmation, then create the forked AKK-managed session with forkSummary."
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
throw new Error(`unsupported takeover strategy: ${strategy}`);
|
|
454
|
+
}
|
|
455
|
+
function buildForkSummaryPrompt({ agent, session, contextPackage }) {
|
|
456
|
+
return [
|
|
457
|
+
"You are OpenClaw summarizing a bounded native coding-agent session context before Agent Knock Knock forks it into a new managed session.",
|
|
458
|
+
"",
|
|
459
|
+
"Goal:",
|
|
460
|
+
"- Produce a concise, user-reviewable summary that can be safely injected into a new AKK-managed coding-agent session after the user confirms it.",
|
|
461
|
+
"- The new session must use the summary only; do not pass raw rollout history or the full context package to the coding agent.",
|
|
462
|
+
"",
|
|
463
|
+
"Source:",
|
|
464
|
+
`- Agent: ${agent}`,
|
|
465
|
+
`- Session id: ${session.id}`,
|
|
466
|
+
`- Workspace: ${session.cwd}`,
|
|
467
|
+
`- Title: ${session.title ?? session.preview ?? session.firstUserMessage ?? "(unknown)"}`,
|
|
468
|
+
`- Context messages included: ${contextPackage.messages.length}`,
|
|
469
|
+
`- Commands included: ${contextPackage.commands.length}`,
|
|
470
|
+
`- Context truncated: ${contextPackage.truncated ? "yes" : "no"}`,
|
|
471
|
+
"",
|
|
472
|
+
"Summary format:",
|
|
473
|
+
"1. Original user goal",
|
|
474
|
+
"2. Work already completed",
|
|
475
|
+
"3. Current state and important findings",
|
|
476
|
+
"4. Constraints, risks, or files/workspace details the forked agent must preserve",
|
|
477
|
+
"5. Recommended next step for the forked agent",
|
|
478
|
+
"",
|
|
479
|
+
"After writing the summary, ask the user to confirm. If confirmed, call agent_knock_knock_agent_takeover with strategy=\"fork\", createConversation=true, and forkSummary equal to the confirmed summary."
|
|
480
|
+
].join("\n");
|
|
481
|
+
}
|
|
482
|
+
function selectTerminateTarget({ plan, session, activeSessions, expectedPid, allowCwdOnly }) {
|
|
483
|
+
const exactTarget = plan.targets.find((candidate) => candidate.pid === expectedPid && candidate.sessionId === session.id);
|
|
484
|
+
if (exactTarget) {
|
|
485
|
+
return {
|
|
486
|
+
allowed: true,
|
|
487
|
+
target: exactTarget,
|
|
488
|
+
matchKind: "exact_session"
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
if (!allowCwdOnly) {
|
|
492
|
+
return {
|
|
493
|
+
allowed: false,
|
|
494
|
+
code: plan.allowed && plan.requiresConfirmation ? "expected_pid_mismatch" : "takeover_not_confirmable",
|
|
495
|
+
message: plan.allowed && plan.requiresConfirmation
|
|
496
|
+
? `Expected pid ${expectedPid} is no longer the exact active Codex process for session ${session.id}.`
|
|
497
|
+
: "The current active Codex process no longer has an exact session match that can be safely terminated."
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
const cwdOnlyTarget = plan.targets.find((candidate) => candidate.pid === expectedPid &&
|
|
501
|
+
candidate.cwd === session.cwd &&
|
|
502
|
+
candidate.sessionId === undefined);
|
|
503
|
+
const stillActive = activeSessions.some((candidate) => candidate.pid === expectedPid &&
|
|
504
|
+
candidate.cwd === session.cwd &&
|
|
505
|
+
candidate.sessionId === undefined);
|
|
506
|
+
if (!cwdOnlyTarget || !stillActive) {
|
|
507
|
+
return {
|
|
508
|
+
allowed: false,
|
|
509
|
+
code: "expected_pid_mismatch",
|
|
510
|
+
message: `Expected pid ${expectedPid} is no longer an active Codex process in ${session.cwd}.`
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
return {
|
|
514
|
+
allowed: true,
|
|
515
|
+
target: cwdOnlyTarget,
|
|
516
|
+
matchKind: "cwd_only_confirmed"
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
function createForkConversation({ agent, strategy, session, contextPackage, forkSummary, modelInfo, options }) {
|
|
520
|
+
const workspace = session.cwd;
|
|
521
|
+
const storeDir = expandHome(options.storeDir ?? options.logDir ?? defaultStoreDir(workspace));
|
|
522
|
+
cleanupIdleConversations(storeDir, options);
|
|
523
|
+
const executor = resolveExecutor({
|
|
524
|
+
kind: agent,
|
|
525
|
+
session: options.session ?? options.executorSession ?? uniqueDelegateSessionName(agent)
|
|
526
|
+
});
|
|
527
|
+
const now = new Date();
|
|
528
|
+
const conversation = createConversation({
|
|
529
|
+
userRequest: options.request ?? `Fork native ${agent} session ${session.id}`,
|
|
530
|
+
workspace,
|
|
531
|
+
openclawSession: options.openclawSession ?? "agent:main:main",
|
|
532
|
+
executorKind: executor.kind,
|
|
533
|
+
executorSession: executor.session,
|
|
534
|
+
softLimit: Number(options.softLimit ?? 50),
|
|
535
|
+
hardLimit: Number(options.hardLimit ?? 100),
|
|
536
|
+
now
|
|
537
|
+
});
|
|
538
|
+
const paths = pathsForConversation(conversation.conversation_id, storeDir);
|
|
539
|
+
const callbackCommand = options.callbackCommand
|
|
540
|
+
? expandCallbackCommandTemplate(options.callbackCommand, { statePath: paths.statePath })
|
|
541
|
+
: buildCallbackCommand({
|
|
542
|
+
statePath: paths.statePath,
|
|
543
|
+
gatewayUrl: options.gatewayUrl ?? "ws://127.0.0.1:18789",
|
|
544
|
+
token: options.token,
|
|
545
|
+
openclawSession: options.openclawSession ?? "agent:main:main",
|
|
546
|
+
gatewayMethod: options.gatewayMethod,
|
|
547
|
+
gatewaySession: options.gatewaySession,
|
|
548
|
+
openclawBin: options.openclawBin ?? resolveOptionalExecutable("openclaw")
|
|
549
|
+
});
|
|
550
|
+
const explicitModel = options.model ?? options.codexModel;
|
|
551
|
+
const executorModel = explicitModel ?? modelInfo?.acpxModel ?? modelEnvForExecutor(executor, process.env);
|
|
552
|
+
const forkedConversation = withStoragePaths({
|
|
553
|
+
...conversation,
|
|
554
|
+
executor,
|
|
555
|
+
status: "idle",
|
|
556
|
+
idle_since: now.toISOString(),
|
|
557
|
+
updated_at: now.toISOString(),
|
|
558
|
+
callback_command: callbackCommand,
|
|
559
|
+
gateway_url: options.gatewayUrl ?? "ws://127.0.0.1:18789",
|
|
560
|
+
gateway_method: options.gatewayMethod,
|
|
561
|
+
gateway_session: options.gatewaySession ?? options.openclawSession ?? "agent:main:main",
|
|
562
|
+
openclaw_bin: options.openclawBin ?? resolveOptionalExecutable("openclaw"),
|
|
563
|
+
executor_all_proxy: proxyForExecutor(executor, options),
|
|
564
|
+
executor_model: executorModel,
|
|
565
|
+
fork_context_takeover: {
|
|
566
|
+
agent,
|
|
567
|
+
source_session_id: session.id,
|
|
568
|
+
source_cwd: session.cwd,
|
|
569
|
+
source_title: session.title,
|
|
570
|
+
source_updated_at_ms: session.updatedAtMs,
|
|
571
|
+
strategy,
|
|
572
|
+
forked_at: now.toISOString(),
|
|
573
|
+
summary: forkSummary,
|
|
574
|
+
context_message_count: contextPackage.messages.length,
|
|
575
|
+
context_command_count: contextPackage.commands.length,
|
|
576
|
+
context_truncated: contextPackage.truncated,
|
|
577
|
+
native_model: modelInfo?.model,
|
|
578
|
+
acpx_model: modelInfo?.acpxModel,
|
|
579
|
+
model_source: modelInfo?.source,
|
|
580
|
+
needs_bootstrap: true
|
|
581
|
+
}
|
|
582
|
+
}, paths);
|
|
583
|
+
saveState(paths.statePath, forkedConversation);
|
|
584
|
+
appendEvent(paths.logPath, {
|
|
585
|
+
ts: now.toISOString(),
|
|
586
|
+
conversation_id: forkedConversation.conversation_id,
|
|
587
|
+
event: "native_session_forked",
|
|
588
|
+
agent,
|
|
589
|
+
strategy,
|
|
590
|
+
source_session_id: session.id,
|
|
591
|
+
source_cwd: session.cwd,
|
|
592
|
+
executor,
|
|
593
|
+
context_message_count: contextPackage.messages.length,
|
|
594
|
+
context_command_count: contextPackage.commands.length,
|
|
595
|
+
context_truncated: contextPackage.truncated
|
|
596
|
+
});
|
|
597
|
+
runtimeLog("info", "native_session_forked", {
|
|
598
|
+
conversation_id: forkedConversation.conversation_id,
|
|
599
|
+
agent,
|
|
600
|
+
strategy,
|
|
601
|
+
source_session_id: session.id,
|
|
602
|
+
executor_session: executor.session,
|
|
603
|
+
state_path: paths.statePath,
|
|
604
|
+
event_log_path: paths.logPath
|
|
605
|
+
});
|
|
606
|
+
return {
|
|
607
|
+
conversation: forkedConversation,
|
|
608
|
+
paths,
|
|
609
|
+
next: `Use AKK send ${forkedConversation.conversation_id}: <message> to start the forked ${agent} session with the approved summary.`
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
function createNativeSessionConversation({ agent, strategy, session, modelInfo, options, takeoverMatchKind = strategy }) {
|
|
613
|
+
const workspace = session.cwd;
|
|
614
|
+
const storeDir = expandHome(options.storeDir ?? options.logDir ?? defaultStoreDir(workspace));
|
|
615
|
+
cleanupIdleConversations(storeDir, options);
|
|
616
|
+
const executor = resolveExecutor({
|
|
617
|
+
kind: agent,
|
|
618
|
+
session: session.id
|
|
619
|
+
});
|
|
620
|
+
const now = new Date();
|
|
621
|
+
const conversation = createConversation({
|
|
622
|
+
userRequest: options.request ?? `Attach native ${agent} session ${session.id}`,
|
|
623
|
+
workspace,
|
|
624
|
+
openclawSession: options.openclawSession ?? "agent:main:main",
|
|
625
|
+
executorKind: executor.kind,
|
|
626
|
+
executorSession: executor.session,
|
|
627
|
+
softLimit: Number(options.softLimit ?? 50),
|
|
628
|
+
hardLimit: Number(options.hardLimit ?? 100),
|
|
629
|
+
now
|
|
630
|
+
});
|
|
631
|
+
const paths = pathsForConversation(conversation.conversation_id, storeDir);
|
|
632
|
+
const callbackCommand = options.callbackCommand
|
|
633
|
+
? expandCallbackCommandTemplate(options.callbackCommand, { statePath: paths.statePath })
|
|
634
|
+
: buildCallbackCommand({
|
|
635
|
+
statePath: paths.statePath,
|
|
636
|
+
gatewayUrl: options.gatewayUrl ?? "ws://127.0.0.1:18789",
|
|
637
|
+
token: options.token,
|
|
638
|
+
openclawSession: options.openclawSession ?? "agent:main:main",
|
|
639
|
+
gatewayMethod: options.gatewayMethod,
|
|
640
|
+
gatewaySession: options.gatewaySession,
|
|
641
|
+
openclawBin: options.openclawBin ?? resolveOptionalExecutable("openclaw")
|
|
642
|
+
});
|
|
643
|
+
const explicitModel = options.model ?? options.codexModel;
|
|
644
|
+
const executorModel = explicitModel ?? modelInfo?.acpxModel ?? modelEnvForExecutor(executor, process.env);
|
|
645
|
+
const attachedConversation = withStoragePaths({
|
|
646
|
+
...conversation,
|
|
647
|
+
executor,
|
|
648
|
+
status: "idle",
|
|
649
|
+
idle_since: now.toISOString(),
|
|
650
|
+
updated_at: now.toISOString(),
|
|
651
|
+
callback_command: callbackCommand,
|
|
652
|
+
gateway_url: options.gatewayUrl ?? "ws://127.0.0.1:18789",
|
|
653
|
+
gateway_method: options.gatewayMethod,
|
|
654
|
+
gateway_session: options.gatewaySession ?? options.openclawSession ?? "agent:main:main",
|
|
655
|
+
openclaw_bin: options.openclawBin ?? resolveOptionalExecutable("openclaw"),
|
|
656
|
+
executor_all_proxy: proxyForExecutor(executor, options),
|
|
657
|
+
executor_model: executorModel,
|
|
658
|
+
native_session_takeover: {
|
|
659
|
+
agent,
|
|
660
|
+
native_session_id: session.id,
|
|
661
|
+
source_cwd: session.cwd,
|
|
662
|
+
source_title: session.title,
|
|
663
|
+
strategy,
|
|
664
|
+
attached_at: now.toISOString(),
|
|
665
|
+
native_model: modelInfo?.model,
|
|
666
|
+
acpx_model: modelInfo?.acpxModel,
|
|
667
|
+
model_source: modelInfo?.source,
|
|
668
|
+
takeover_match_kind: takeoverMatchKind,
|
|
669
|
+
needs_bootstrap: true
|
|
670
|
+
}
|
|
671
|
+
}, paths);
|
|
672
|
+
saveState(paths.statePath, attachedConversation);
|
|
673
|
+
appendEvent(paths.logPath, {
|
|
674
|
+
ts: now.toISOString(),
|
|
675
|
+
conversation_id: attachedConversation.conversation_id,
|
|
676
|
+
event: "native_session_attached",
|
|
677
|
+
agent,
|
|
678
|
+
strategy,
|
|
679
|
+
native_session_id: session.id,
|
|
680
|
+
source_cwd: session.cwd,
|
|
681
|
+
executor
|
|
682
|
+
});
|
|
683
|
+
runtimeLog("info", "native_session_attached", {
|
|
684
|
+
conversation_id: attachedConversation.conversation_id,
|
|
685
|
+
agent,
|
|
686
|
+
strategy,
|
|
687
|
+
native_session_id: session.id,
|
|
688
|
+
state_path: paths.statePath,
|
|
689
|
+
event_log_path: paths.logPath
|
|
690
|
+
});
|
|
691
|
+
return {
|
|
692
|
+
conversation: attachedConversation,
|
|
693
|
+
paths,
|
|
694
|
+
next: `Use AKK send ${attachedConversation.conversation_id}: <message> to continue this native ${agent} session through AKK.`
|
|
695
|
+
};
|
|
696
|
+
}
|
|
173
697
|
function runNew(options) {
|
|
174
698
|
const request = required(options.request, "--request is required");
|
|
175
699
|
const workspace = options.workspace ?? process.cwd();
|
|
@@ -489,8 +1013,15 @@ function runDelegate(options) {
|
|
|
489
1013
|
note: "Run again with --send to send this task through acpx."
|
|
490
1014
|
});
|
|
491
1015
|
}
|
|
492
|
-
function ensureExecutorSession({ acpxPath, executor, cwd, env }) {
|
|
493
|
-
|
|
1016
|
+
function ensureExecutorSession({ acpxPath, executor, cwd, env, resumeSessionId }) {
|
|
1017
|
+
const args = [acpxCommandForExecutor(executor), "sessions", "ensure"];
|
|
1018
|
+
if (resumeSessionId) {
|
|
1019
|
+
args.push("--resume-session", resumeSessionId);
|
|
1020
|
+
}
|
|
1021
|
+
else {
|
|
1022
|
+
args.push("--name", executor.session);
|
|
1023
|
+
}
|
|
1024
|
+
return spawnSync(acpxPath, args, {
|
|
494
1025
|
encoding: "utf8",
|
|
495
1026
|
cwd,
|
|
496
1027
|
env
|
|
@@ -620,10 +1151,21 @@ function runSend(options) {
|
|
|
620
1151
|
throw new Error(`cannot send to ${conversation.conversation_id}; conversation is ${conversation.status}`);
|
|
621
1152
|
}
|
|
622
1153
|
if (conversation.status === "needs_recovery") {
|
|
623
|
-
throw new Error(`cannot send to ${conversation.conversation_id}; choose recover,
|
|
1154
|
+
throw new Error(`cannot send to ${conversation.conversation_id}; choose recover, close, or delegate a new task first`);
|
|
1155
|
+
}
|
|
1156
|
+
if (conversation.status === "needs_model_selection" && !options.model) {
|
|
1157
|
+
throw new Error(`cannot send to ${conversation.conversation_id}; choose a supported model with --model first`);
|
|
624
1158
|
}
|
|
625
1159
|
const executor = executorForConversation(conversation);
|
|
626
1160
|
const type = options.type ?? (conversation.status === "waiting_for_openclaw" ? "answer" : "task");
|
|
1161
|
+
const nativeTakeoverForSend = isRecord(conversation.native_session_takeover)
|
|
1162
|
+
? conversation.native_session_takeover
|
|
1163
|
+
: undefined;
|
|
1164
|
+
const forkTakeoverForSend = isRecord(conversation.fork_context_takeover)
|
|
1165
|
+
? conversation.fork_context_takeover
|
|
1166
|
+
: undefined;
|
|
1167
|
+
const needsNativeTakeoverBootstrap = nativeTakeoverForSend?.["needs_bootstrap"] === true;
|
|
1168
|
+
const needsForkTakeoverBootstrap = forkTakeoverForSend?.["needs_bootstrap"] === true;
|
|
627
1169
|
const message = createMessage({
|
|
628
1170
|
conversation,
|
|
629
1171
|
from: "openclaw",
|
|
@@ -635,10 +1177,21 @@ function runSend(options) {
|
|
|
635
1177
|
executor_session: executor.session
|
|
636
1178
|
}
|
|
637
1179
|
});
|
|
1180
|
+
const previousModelSelection = isRecord(conversation.model_selection)
|
|
1181
|
+
? conversation.model_selection
|
|
1182
|
+
: {};
|
|
638
1183
|
const nextConversation = {
|
|
639
1184
|
...applyMessageToConversation(conversation, message),
|
|
640
1185
|
executor,
|
|
641
|
-
claude_session: executor.kind === "claude" ? executor.session : conversation.claude_session
|
|
1186
|
+
claude_session: executor.kind === "claude" ? executor.session : conversation.claude_session,
|
|
1187
|
+
executor_model: options.model ?? conversation.executor_model,
|
|
1188
|
+
model_selection: conversation.status === "needs_model_selection"
|
|
1189
|
+
? {
|
|
1190
|
+
...previousModelSelection,
|
|
1191
|
+
resolved_at: new Date().toISOString(),
|
|
1192
|
+
selected_model: options.model
|
|
1193
|
+
}
|
|
1194
|
+
: conversation.model_selection
|
|
642
1195
|
};
|
|
643
1196
|
saveState(statePath, nextConversation);
|
|
644
1197
|
appendEvent(logPath, messageEvent(message));
|
|
@@ -651,10 +1204,34 @@ function runSend(options) {
|
|
|
651
1204
|
event_log_path: logPath,
|
|
652
1205
|
message: textSummary(messageBody)
|
|
653
1206
|
});
|
|
654
|
-
const acpxPath = resolveExecutable("acpx");
|
|
655
1207
|
const executorEnv = environmentForExecutor(executor, {
|
|
656
1208
|
allProxy: options.allProxy ?? conversation.executor_all_proxy
|
|
657
1209
|
});
|
|
1210
|
+
const payload = buildAgentSendPayload({
|
|
1211
|
+
conversation,
|
|
1212
|
+
executor,
|
|
1213
|
+
message,
|
|
1214
|
+
includeNativeTakeoverBootstrap: needsNativeTakeoverBootstrap,
|
|
1215
|
+
includeForkTakeoverBootstrap: needsForkTakeoverBootstrap,
|
|
1216
|
+
forkTakeover: forkTakeoverForSend
|
|
1217
|
+
});
|
|
1218
|
+
if (nativeTakeoverForSend?.["native_session_id"] && executor.kind === "codex") {
|
|
1219
|
+
runNativeCodexResumeSend({
|
|
1220
|
+
options,
|
|
1221
|
+
conversation,
|
|
1222
|
+
nextConversation,
|
|
1223
|
+
statePath,
|
|
1224
|
+
logPath,
|
|
1225
|
+
executor,
|
|
1226
|
+
executorEnv,
|
|
1227
|
+
message,
|
|
1228
|
+
payload,
|
|
1229
|
+
nativeTakeover: nativeTakeoverForSend,
|
|
1230
|
+
needsNativeTakeoverBootstrap
|
|
1231
|
+
});
|
|
1232
|
+
return;
|
|
1233
|
+
}
|
|
1234
|
+
const acpxPath = resolveExecutable("acpx");
|
|
658
1235
|
const executorModel = modelForExecutor(executor, {
|
|
659
1236
|
model: options.model ?? conversation.executor_model
|
|
660
1237
|
});
|
|
@@ -662,7 +1239,8 @@ function runSend(options) {
|
|
|
662
1239
|
acpxPath,
|
|
663
1240
|
executor,
|
|
664
1241
|
cwd: conversation.workspace ?? process.cwd(),
|
|
665
|
-
env: executorEnv
|
|
1242
|
+
env: executorEnv,
|
|
1243
|
+
resumeSessionId: nativeTakeoverForSend?.["native_session_id"]
|
|
666
1244
|
});
|
|
667
1245
|
appendEvent(logPath, {
|
|
668
1246
|
ts: new Date().toISOString(),
|
|
@@ -683,7 +1261,7 @@ function runSend(options) {
|
|
|
683
1261
|
stderr: textSummary(cleanProcessText(ensureSession.stderr))
|
|
684
1262
|
});
|
|
685
1263
|
if (ensureSession.error) {
|
|
686
|
-
if (requiresExplicitRecoveryDecision(
|
|
1264
|
+
if (requiresExplicitRecoveryDecision(options)) {
|
|
687
1265
|
printJson(markConversationNeedsRecovery({
|
|
688
1266
|
conversation: nextConversation,
|
|
689
1267
|
statePath,
|
|
@@ -696,10 +1274,22 @@ function runSend(options) {
|
|
|
696
1274
|
}));
|
|
697
1275
|
return;
|
|
698
1276
|
}
|
|
699
|
-
|
|
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;
|
|
700
1289
|
}
|
|
701
1290
|
if (ensureSession.status !== 0) {
|
|
702
|
-
|
|
1291
|
+
const reason = cleanProcessText(ensureSession.stderr || ensureSession.stdout || `acpx ${executor.kind} sessions ensure exited with status ${ensureSession.status}`);
|
|
1292
|
+
if (requiresExplicitRecoveryDecision(options)) {
|
|
703
1293
|
printJson(markConversationNeedsRecovery({
|
|
704
1294
|
conversation: nextConversation,
|
|
705
1295
|
statePath,
|
|
@@ -708,19 +1298,23 @@ function runSend(options) {
|
|
|
708
1298
|
message,
|
|
709
1299
|
failedStage: "session_ensure",
|
|
710
1300
|
result: ensureSession,
|
|
711
|
-
reason
|
|
1301
|
+
reason
|
|
712
1302
|
}));
|
|
713
1303
|
return;
|
|
714
1304
|
}
|
|
715
|
-
|
|
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;
|
|
716
1317
|
}
|
|
717
|
-
const payload = [
|
|
718
|
-
"Continue the existing Agent Knock Knock delegation using this structured OpenClaw message.",
|
|
719
|
-
"If this message answers a question or blocker, follow it as the product decision.",
|
|
720
|
-
"Continue to report back only through the callback command already provided for this conversation.",
|
|
721
|
-
"",
|
|
722
|
-
JSON.stringify(message)
|
|
723
|
-
].join("\n");
|
|
724
1318
|
const acpxArgs = buildAcpxPromptArgs({ executor, payload, model: executorModel });
|
|
725
1319
|
if (options.background) {
|
|
726
1320
|
const outputPath = path.join(path.dirname(logPath), `${executor.kind}-followup-output.log`);
|
|
@@ -766,8 +1360,16 @@ function runSend(options) {
|
|
|
766
1360
|
executor_pid: child.pid ?? null,
|
|
767
1361
|
agent_timeout_minutes: Number(options.agentTimeoutMinutes ?? DEFAULT_AGENT_TIMEOUT_MINUTES)
|
|
768
1362
|
});
|
|
769
|
-
|
|
1363
|
+
const deliveredConversation = markTakeoverBootstrapped({
|
|
770
1364
|
conversation: nextConversation,
|
|
1365
|
+
statePath,
|
|
1366
|
+
logPath,
|
|
1367
|
+
executor,
|
|
1368
|
+
native: needsNativeTakeoverBootstrap,
|
|
1369
|
+
fork: needsForkTakeoverBootstrap
|
|
1370
|
+
});
|
|
1371
|
+
printJson({
|
|
1372
|
+
conversation: deliveredConversation,
|
|
771
1373
|
message,
|
|
772
1374
|
delivered: true,
|
|
773
1375
|
background: true,
|
|
@@ -804,7 +1406,7 @@ function runSend(options) {
|
|
|
804
1406
|
stderr: textSummary(cleanProcessText(sendResult.stderr))
|
|
805
1407
|
});
|
|
806
1408
|
if (sendResult.error) {
|
|
807
|
-
if (requiresExplicitRecoveryDecision(
|
|
1409
|
+
if (requiresExplicitRecoveryDecision(options)) {
|
|
808
1410
|
printJson(markConversationNeedsRecovery({
|
|
809
1411
|
conversation: nextConversation,
|
|
810
1412
|
statePath,
|
|
@@ -817,10 +1419,22 @@ function runSend(options) {
|
|
|
817
1419
|
}));
|
|
818
1420
|
return;
|
|
819
1421
|
}
|
|
820
|
-
|
|
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;
|
|
821
1434
|
}
|
|
822
1435
|
if (sendResult.status !== 0) {
|
|
823
|
-
|
|
1436
|
+
const reason = cleanProcessText(sendResult.stderr || sendResult.stdout || `acpx ${executor.kind} send exited with status ${sendResult.status}`);
|
|
1437
|
+
if (requiresExplicitRecoveryDecision(options)) {
|
|
824
1438
|
printJson(markConversationNeedsRecovery({
|
|
825
1439
|
conversation: nextConversation,
|
|
826
1440
|
statePath,
|
|
@@ -829,25 +1443,350 @@ function runSend(options) {
|
|
|
829
1443
|
message,
|
|
830
1444
|
failedStage: "message_send",
|
|
831
1445
|
result: sendResult,
|
|
832
|
-
reason
|
|
1446
|
+
reason
|
|
833
1447
|
}));
|
|
834
1448
|
return;
|
|
835
1449
|
}
|
|
836
|
-
|
|
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;
|
|
837
1462
|
}
|
|
1463
|
+
const deliveredConversation = markTakeoverBootstrapped({
|
|
1464
|
+
conversation: nextConversation,
|
|
1465
|
+
statePath,
|
|
1466
|
+
logPath,
|
|
1467
|
+
executor,
|
|
1468
|
+
native: needsNativeTakeoverBootstrap,
|
|
1469
|
+
fork: needsForkTakeoverBootstrap
|
|
1470
|
+
});
|
|
838
1471
|
printJson({
|
|
1472
|
+
conversation: deliveredConversation,
|
|
1473
|
+
message,
|
|
1474
|
+
delivered: true,
|
|
1475
|
+
executor,
|
|
1476
|
+
budget: budgetAction(deliveredConversation)
|
|
1477
|
+
});
|
|
1478
|
+
}
|
|
1479
|
+
function runNativeCodexResumeSend({ options, conversation, nextConversation, statePath, logPath, executor, executorEnv, message, payload, nativeTakeover, needsNativeTakeoverBootstrap }) {
|
|
1480
|
+
const codexPath = resolveExecutable("codex");
|
|
1481
|
+
const nativeSessionId = String(nativeTakeover["native_session_id"]);
|
|
1482
|
+
const nativeModel = nativeCodexModelForSend({ options, conversation, nativeTakeover });
|
|
1483
|
+
const codexArgs = buildCodexExecResumeArgs({
|
|
1484
|
+
nativeSessionId,
|
|
1485
|
+
payload,
|
|
1486
|
+
model: nativeModel
|
|
1487
|
+
});
|
|
1488
|
+
appendEvent(logPath, {
|
|
1489
|
+
ts: new Date().toISOString(),
|
|
1490
|
+
conversation_id: conversation.conversation_id,
|
|
1491
|
+
event: "native_executor_resume_prepare",
|
|
1492
|
+
executor,
|
|
1493
|
+
native_session_id: nativeSessionId,
|
|
1494
|
+
model: nativeModel ?? null
|
|
1495
|
+
});
|
|
1496
|
+
runtimeLog("info", "native_executor_resume_prepare", {
|
|
1497
|
+
conversation_id: conversation.conversation_id,
|
|
1498
|
+
agent: executor.kind,
|
|
1499
|
+
executor_session: executor.session,
|
|
1500
|
+
native_session_id: nativeSessionId,
|
|
1501
|
+
model: nativeModel
|
|
1502
|
+
});
|
|
1503
|
+
if (options.background) {
|
|
1504
|
+
const outputPath = path.join(path.dirname(logPath), `${executor.kind}-native-resume-output.log`);
|
|
1505
|
+
const outputFd = fs.openSync(outputPath, "a");
|
|
1506
|
+
const child = spawn(codexPath, codexArgs, {
|
|
1507
|
+
detached: true,
|
|
1508
|
+
stdio: ["ignore", outputFd, outputFd],
|
|
1509
|
+
cwd: conversation.workspace ?? process.cwd(),
|
|
1510
|
+
env: executorEnv
|
|
1511
|
+
});
|
|
1512
|
+
child.unref();
|
|
1513
|
+
fs.closeSync(outputFd);
|
|
1514
|
+
appendEvent(logPath, {
|
|
1515
|
+
ts: new Date().toISOString(),
|
|
1516
|
+
conversation_id: conversation.conversation_id,
|
|
1517
|
+
event: "native_executor_resume_launch",
|
|
1518
|
+
mode: "background",
|
|
1519
|
+
pid: child.pid ?? null,
|
|
1520
|
+
executor,
|
|
1521
|
+
native_session_id: nativeSessionId,
|
|
1522
|
+
output_path: outputPath
|
|
1523
|
+
});
|
|
1524
|
+
runtimeLog("info", "native_executor_resume_launch", {
|
|
1525
|
+
conversation_id: conversation.conversation_id,
|
|
1526
|
+
agent: executor.kind,
|
|
1527
|
+
executor_session: executor.session,
|
|
1528
|
+
native_session_id: nativeSessionId,
|
|
1529
|
+
mode: "background",
|
|
1530
|
+
pid: child.pid ?? null,
|
|
1531
|
+
output_path: outputPath
|
|
1532
|
+
});
|
|
1533
|
+
const monitor = startExecutorMonitor({
|
|
1534
|
+
statePath,
|
|
1535
|
+
logPath,
|
|
1536
|
+
pid: child.pid,
|
|
1537
|
+
outputPath,
|
|
1538
|
+
agentTimeoutMinutes: Number(options.agentTimeoutMinutes ?? DEFAULT_AGENT_TIMEOUT_MINUTES),
|
|
1539
|
+
pollIntervalMs: Number(options.monitorPollIntervalMs ?? DEFAULT_MONITOR_POLL_INTERVAL_MS)
|
|
1540
|
+
});
|
|
1541
|
+
appendEvent(logPath, {
|
|
1542
|
+
ts: new Date().toISOString(),
|
|
1543
|
+
conversation_id: conversation.conversation_id,
|
|
1544
|
+
event: "executor_monitor_launch",
|
|
1545
|
+
pid: monitor.pid ?? null,
|
|
1546
|
+
executor_pid: child.pid ?? null,
|
|
1547
|
+
agent_timeout_minutes: Number(options.agentTimeoutMinutes ?? DEFAULT_AGENT_TIMEOUT_MINUTES)
|
|
1548
|
+
});
|
|
1549
|
+
const deliveredConversation = markTakeoverBootstrapped({
|
|
1550
|
+
conversation: nextConversation,
|
|
1551
|
+
statePath,
|
|
1552
|
+
logPath,
|
|
1553
|
+
executor,
|
|
1554
|
+
native: needsNativeTakeoverBootstrap,
|
|
1555
|
+
fork: false
|
|
1556
|
+
});
|
|
1557
|
+
printJson({
|
|
1558
|
+
conversation: deliveredConversation,
|
|
1559
|
+
message,
|
|
1560
|
+
delivered: true,
|
|
1561
|
+
background: true,
|
|
1562
|
+
native_resume: true,
|
|
1563
|
+
pid: child.pid ?? null,
|
|
1564
|
+
monitor_pid: monitor.pid ?? null,
|
|
1565
|
+
output_path: outputPath,
|
|
1566
|
+
executor,
|
|
1567
|
+
budget: budgetAction(deliveredConversation)
|
|
1568
|
+
});
|
|
1569
|
+
return;
|
|
1570
|
+
}
|
|
1571
|
+
const sendResult = spawnSync(codexPath, codexArgs, {
|
|
1572
|
+
encoding: "utf8",
|
|
1573
|
+
maxBuffer: 1024 * 1024 * 10,
|
|
1574
|
+
cwd: conversation.workspace ?? process.cwd(),
|
|
1575
|
+
env: executorEnv
|
|
1576
|
+
});
|
|
1577
|
+
appendEvent(logPath, {
|
|
1578
|
+
ts: new Date().toISOString(),
|
|
1579
|
+
conversation_id: conversation.conversation_id,
|
|
1580
|
+
event: "native_executor_resume_send",
|
|
1581
|
+
status: sendResult.status ?? null,
|
|
1582
|
+
executor,
|
|
1583
|
+
native_session_id: nativeSessionId,
|
|
1584
|
+
stdout: cleanProcessText(sendResult.stdout),
|
|
1585
|
+
stderr: cleanProcessText(sendResult.stderr)
|
|
1586
|
+
});
|
|
1587
|
+
runtimeLog("info", "native_executor_resume_send", {
|
|
1588
|
+
conversation_id: conversation.conversation_id,
|
|
1589
|
+
agent: executor.kind,
|
|
1590
|
+
executor_session: executor.session,
|
|
1591
|
+
native_session_id: nativeSessionId,
|
|
1592
|
+
status: sendResult.status ?? null,
|
|
1593
|
+
failure_kind: classifyProcessFailure(sendResult),
|
|
1594
|
+
stdout: textSummary(cleanProcessText(sendResult.stdout)),
|
|
1595
|
+
stderr: textSummary(cleanProcessText(sendResult.stderr))
|
|
1596
|
+
});
|
|
1597
|
+
if (sendResult.error) {
|
|
1598
|
+
throw new Error(`codex exec resume failed to start: ${sendResult.error.message}`);
|
|
1599
|
+
}
|
|
1600
|
+
if (sendResult.status !== 0) {
|
|
1601
|
+
throw new Error(cleanProcessText(sendResult.stderr || sendResult.stdout || `codex exec resume exited with status ${sendResult.status}`));
|
|
1602
|
+
}
|
|
1603
|
+
const deliveredConversation = markTakeoverBootstrapped({
|
|
839
1604
|
conversation: nextConversation,
|
|
1605
|
+
statePath,
|
|
1606
|
+
logPath,
|
|
1607
|
+
executor,
|
|
1608
|
+
native: needsNativeTakeoverBootstrap,
|
|
1609
|
+
fork: false
|
|
1610
|
+
});
|
|
1611
|
+
printJson({
|
|
1612
|
+
conversation: deliveredConversation,
|
|
840
1613
|
message,
|
|
841
1614
|
delivered: true,
|
|
1615
|
+
native_resume: true,
|
|
842
1616
|
executor,
|
|
843
|
-
budget: budgetAction(
|
|
1617
|
+
budget: budgetAction(deliveredConversation)
|
|
844
1618
|
});
|
|
845
1619
|
}
|
|
846
|
-
function
|
|
1620
|
+
function buildCodexExecResumeArgs({ nativeSessionId, payload, model }) {
|
|
1621
|
+
const args = ["exec", "resume"];
|
|
1622
|
+
if (model) {
|
|
1623
|
+
args.push("--model", model);
|
|
1624
|
+
}
|
|
1625
|
+
args.push("--skip-git-repo-check", nativeSessionId, payload);
|
|
1626
|
+
return args;
|
|
1627
|
+
}
|
|
1628
|
+
function nativeCodexModelForSend({ options, conversation, nativeTakeover }) {
|
|
1629
|
+
const explicit = options.model ?? options.codexModel;
|
|
1630
|
+
if (explicit) {
|
|
1631
|
+
return normalizeNativeCodexModel(explicit);
|
|
1632
|
+
}
|
|
1633
|
+
const nativeModel = isRecord(nativeTakeover) ? nativeTakeover["native_model"] : undefined;
|
|
1634
|
+
if (typeof nativeModel === "string" && nativeModel.trim()) {
|
|
1635
|
+
return normalizeNativeCodexModel(nativeModel);
|
|
1636
|
+
}
|
|
1637
|
+
return normalizeNativeCodexModel(conversation.executor_model);
|
|
1638
|
+
}
|
|
1639
|
+
function normalizeNativeCodexModel(model) {
|
|
1640
|
+
const value = typeof model === "string" ? model.trim() : "";
|
|
1641
|
+
if (!value) {
|
|
1642
|
+
return undefined;
|
|
1643
|
+
}
|
|
1644
|
+
return value.replace(/\[[^\]]+\]$/u, "").replace(/\/(?:low|medium|high|xhigh)$/u, "");
|
|
1645
|
+
}
|
|
1646
|
+
function buildAgentSendPayload({ conversation, executor, message, includeNativeTakeoverBootstrap, includeForkTakeoverBootstrap, forkTakeover }) {
|
|
1647
|
+
const messageJson = JSON.stringify(message);
|
|
1648
|
+
if (!includeNativeTakeoverBootstrap && !includeForkTakeoverBootstrap) {
|
|
1649
|
+
return [
|
|
1650
|
+
"Continue the existing Agent Knock Knock delegation using this structured OpenClaw message.",
|
|
1651
|
+
"If this message answers a question or blocker, follow it as the product decision.",
|
|
1652
|
+
"Continue to report back only through the callback command already provided for this conversation.",
|
|
1653
|
+
"",
|
|
1654
|
+
messageJson
|
|
1655
|
+
].join("\n");
|
|
1656
|
+
}
|
|
1657
|
+
if (includeForkTakeoverBootstrap) {
|
|
1658
|
+
const summary = forkTakeoverSummaryText(forkTakeover);
|
|
1659
|
+
return [
|
|
1660
|
+
executorBootstrapPrompt({
|
|
1661
|
+
callbackCommand: conversation.callback_command,
|
|
1662
|
+
executorName: executor.display_name,
|
|
1663
|
+
softLimit: Number(conversation.soft_limit ?? 50),
|
|
1664
|
+
hardLimit: Number(conversation.hard_limit ?? 100)
|
|
1665
|
+
}),
|
|
1666
|
+
"",
|
|
1667
|
+
"This AKK conversation is a fork of an existing native coding-agent session. Do not resume the original native session. Treat the approved summary below as the only imported context from the source session, then continue as a new AKK-managed session in this workspace.",
|
|
1668
|
+
"",
|
|
1669
|
+
"Approved source-session summary:",
|
|
1670
|
+
summary || "(No approved summary was provided.)",
|
|
1671
|
+
"",
|
|
1672
|
+
"Initial AKK fork message:",
|
|
1673
|
+
messageJson
|
|
1674
|
+
].join("\n");
|
|
1675
|
+
}
|
|
1676
|
+
return [
|
|
1677
|
+
executorBootstrapPrompt({
|
|
1678
|
+
callbackCommand: conversation.callback_command,
|
|
1679
|
+
executorName: executor.display_name,
|
|
1680
|
+
softLimit: Number(conversation.soft_limit ?? 50),
|
|
1681
|
+
hardLimit: Number(conversation.hard_limit ?? 100)
|
|
1682
|
+
}),
|
|
1683
|
+
"",
|
|
1684
|
+
"This AKK conversation is attaching to an existing native coding-agent session. Continue from the native session context if it is available, and use the callback command above for all replies to OpenClaw.",
|
|
1685
|
+
"",
|
|
1686
|
+
"Initial AKK takeover message:",
|
|
1687
|
+
messageJson
|
|
1688
|
+
].join("\n");
|
|
1689
|
+
}
|
|
1690
|
+
function forkTakeoverSummaryText(forkTakeover) {
|
|
1691
|
+
return String(isRecord(forkTakeover) ? forkTakeover.summary ?? "" : "").trim();
|
|
1692
|
+
}
|
|
1693
|
+
function markTakeoverBootstrapped({ conversation, statePath, logPath, executor, native, fork }) {
|
|
1694
|
+
let nextConversation = conversation;
|
|
1695
|
+
if (native) {
|
|
1696
|
+
nextConversation = markNativeSessionBootstrapped({ conversation: nextConversation, statePath, logPath, executor });
|
|
1697
|
+
}
|
|
1698
|
+
if (fork) {
|
|
1699
|
+
nextConversation = markForkSessionBootstrapped({ conversation: nextConversation, statePath, logPath, executor });
|
|
1700
|
+
}
|
|
1701
|
+
return nextConversation;
|
|
1702
|
+
}
|
|
1703
|
+
function markNativeSessionBootstrapped({ conversation, statePath, logPath, executor }) {
|
|
1704
|
+
const nativeTakeover = isRecord(conversation.native_session_takeover)
|
|
1705
|
+
? conversation.native_session_takeover
|
|
1706
|
+
: {};
|
|
1707
|
+
const now = new Date().toISOString();
|
|
1708
|
+
const nextConversation = {
|
|
1709
|
+
...conversation,
|
|
1710
|
+
native_session_takeover: {
|
|
1711
|
+
...nativeTakeover,
|
|
1712
|
+
needs_bootstrap: false,
|
|
1713
|
+
bootstrapped_at: now
|
|
1714
|
+
},
|
|
1715
|
+
updated_at: now
|
|
1716
|
+
};
|
|
1717
|
+
saveState(statePath, nextConversation);
|
|
1718
|
+
appendEvent(logPath, {
|
|
1719
|
+
ts: now,
|
|
1720
|
+
conversation_id: conversation.conversation_id,
|
|
1721
|
+
event: "native_session_bootstrapped",
|
|
1722
|
+
executor
|
|
1723
|
+
});
|
|
1724
|
+
runtimeLog("info", "native_session_bootstrapped", {
|
|
1725
|
+
conversation_id: conversation.conversation_id,
|
|
1726
|
+
agent: executor.kind,
|
|
1727
|
+
executor_session: executor.session,
|
|
1728
|
+
state_path: statePath
|
|
1729
|
+
});
|
|
1730
|
+
return nextConversation;
|
|
1731
|
+
}
|
|
1732
|
+
function markForkSessionBootstrapped({ conversation, statePath, logPath, executor }) {
|
|
1733
|
+
const forkTakeover = isRecord(conversation.fork_context_takeover)
|
|
1734
|
+
? conversation.fork_context_takeover
|
|
1735
|
+
: {};
|
|
1736
|
+
const now = new Date().toISOString();
|
|
1737
|
+
const nextConversation = {
|
|
1738
|
+
...conversation,
|
|
1739
|
+
fork_context_takeover: {
|
|
1740
|
+
...forkTakeover,
|
|
1741
|
+
needs_bootstrap: false,
|
|
1742
|
+
bootstrapped_at: now
|
|
1743
|
+
},
|
|
1744
|
+
updated_at: now
|
|
1745
|
+
};
|
|
1746
|
+
saveState(statePath, nextConversation);
|
|
1747
|
+
appendEvent(logPath, {
|
|
1748
|
+
ts: now,
|
|
1749
|
+
conversation_id: conversation.conversation_id,
|
|
1750
|
+
event: "fork_session_bootstrapped",
|
|
1751
|
+
executor
|
|
1752
|
+
});
|
|
1753
|
+
runtimeLog("info", "fork_session_bootstrapped", {
|
|
1754
|
+
conversation_id: conversation.conversation_id,
|
|
1755
|
+
agent: executor.kind,
|
|
1756
|
+
executor_session: executor.session,
|
|
1757
|
+
state_path: statePath
|
|
1758
|
+
});
|
|
1759
|
+
return nextConversation;
|
|
1760
|
+
}
|
|
1761
|
+
function requiresExplicitRecoveryDecision(options = {}) {
|
|
847
1762
|
if (options.recoveryPolicy === "explicit" || options.recoveryPolicy === "explicit-decision") {
|
|
848
1763
|
return true;
|
|
849
1764
|
}
|
|
850
|
-
return
|
|
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
|
+
});
|
|
851
1790
|
}
|
|
852
1791
|
function markConversationNeedsRecovery({ conversation, statePath, logPath, executor, message, failedStage, result, reason }) {
|
|
853
1792
|
const now = new Date().toISOString();
|
|
@@ -861,7 +1800,7 @@ function markConversationNeedsRecovery({ conversation, statePath, logPath, execu
|
|
|
861
1800
|
failed_message_id: message.id,
|
|
862
1801
|
pending_message: message,
|
|
863
1802
|
previous_executor: executor,
|
|
864
|
-
options: ["recover", "
|
|
1803
|
+
options: ["recover", "close", "delegate"]
|
|
865
1804
|
};
|
|
866
1805
|
const nextConversation = {
|
|
867
1806
|
...conversation,
|
|
@@ -965,9 +1904,6 @@ function runCancel(options) {
|
|
|
965
1904
|
function runRecover(options) {
|
|
966
1905
|
runRecoveryDecision({ ...options, mode: "recover" });
|
|
967
1906
|
}
|
|
968
|
-
function runRestart(options) {
|
|
969
|
-
runRecoveryDecision({ ...options, mode: "restart" });
|
|
970
|
-
}
|
|
971
1907
|
function runRecoveryDecision(options) {
|
|
972
1908
|
cleanupIdleConversations(storeDirFromOptions(options), options);
|
|
973
1909
|
const { conversation, statePath, logPath } = loadConversationFromOptions(options);
|
|
@@ -999,9 +1935,7 @@ function runRecoveryDecision(options) {
|
|
|
999
1935
|
updated_at: now
|
|
1000
1936
|
};
|
|
1001
1937
|
saveState(statePath, recoveredConversation);
|
|
1002
|
-
const payload =
|
|
1003
|
-
? buildRecoverPayload({ conversation, pendingMessage, logPath })
|
|
1004
|
-
: buildRestartPayload({ pendingMessage });
|
|
1938
|
+
const payload = buildRecoverPayload({ conversation, pendingMessage, logPath });
|
|
1005
1939
|
const acpxPath = resolveExecutable("acpx");
|
|
1006
1940
|
const executorEnv = environmentForExecutor(executor, {
|
|
1007
1941
|
allProxy: options.allProxy ?? conversation.executor_all_proxy
|
|
@@ -1063,8 +1997,8 @@ function runRecoveryDecision(options) {
|
|
|
1063
1997
|
});
|
|
1064
1998
|
printJson({
|
|
1065
1999
|
conversation: recoveredConversation,
|
|
1066
|
-
recovered:
|
|
1067
|
-
|
|
2000
|
+
recovered: true,
|
|
2001
|
+
auto_recovered: Boolean(options.autoRecovered),
|
|
1068
2002
|
background: true,
|
|
1069
2003
|
pid: child.pid ?? null,
|
|
1070
2004
|
monitor_pid: monitor.pid ?? null,
|
|
@@ -1098,8 +2032,8 @@ function runRecoveryDecision(options) {
|
|
|
1098
2032
|
}
|
|
1099
2033
|
printJson({
|
|
1100
2034
|
conversation: recoveredConversation,
|
|
1101
|
-
recovered:
|
|
1102
|
-
|
|
2035
|
+
recovered: true,
|
|
2036
|
+
auto_recovered: Boolean(options.autoRecovered),
|
|
1103
2037
|
delivered: true,
|
|
1104
2038
|
executor,
|
|
1105
2039
|
budget: budgetAction(recoveredConversation)
|
|
@@ -1122,16 +2056,6 @@ function buildRecoverPayload({ conversation, pendingMessage, logPath }) {
|
|
|
1122
2056
|
JSON.stringify(pendingMessage)
|
|
1123
2057
|
].join("\n");
|
|
1124
2058
|
}
|
|
1125
|
-
function buildRestartPayload({ pendingMessage }) {
|
|
1126
|
-
return [
|
|
1127
|
-
"Restart this Agent Knock Knock task in a new ACPX session.",
|
|
1128
|
-
"Do not assume the previous coding-agent session context is available.",
|
|
1129
|
-
"Follow only the pending OpenClaw message below.",
|
|
1130
|
-
"Continue to report back only through the callback command already provided for this conversation.",
|
|
1131
|
-
"",
|
|
1132
|
-
JSON.stringify(pendingMessage)
|
|
1133
|
-
].join("\n");
|
|
1134
|
-
}
|
|
1135
2059
|
function formatProtocolHistoryForRecovery(events) {
|
|
1136
2060
|
const lines = events
|
|
1137
2061
|
.filter((event) => event.event === "message")
|
|
@@ -1216,6 +2140,27 @@ function runMonitor(options) {
|
|
|
1216
2140
|
return;
|
|
1217
2141
|
}
|
|
1218
2142
|
if (Number.isFinite(pid) && !isProcessAlive(pid)) {
|
|
2143
|
+
const modelSelection = detectModelSelectionError(readOutputTail(options.outputPath));
|
|
2144
|
+
if (modelSelection) {
|
|
2145
|
+
const modelSelectionConversation = markConversationNeedsModelSelection({
|
|
2146
|
+
statePath,
|
|
2147
|
+
logPath,
|
|
2148
|
+
reason: modelSelection.message,
|
|
2149
|
+
detail: {
|
|
2150
|
+
executor_pid: pid,
|
|
2151
|
+
output_path: options.outputPath,
|
|
2152
|
+
model_selection: modelSelection
|
|
2153
|
+
}
|
|
2154
|
+
});
|
|
2155
|
+
printJson({
|
|
2156
|
+
conversation: modelSelectionConversation,
|
|
2157
|
+
monitored: true,
|
|
2158
|
+
stalled: false,
|
|
2159
|
+
needs_model_selection: true,
|
|
2160
|
+
reason: modelSelectionConversation?.model_selection?.message ?? modelSelection.message
|
|
2161
|
+
});
|
|
2162
|
+
return;
|
|
2163
|
+
}
|
|
1219
2164
|
const stalledConversation = markConversationStalled({
|
|
1220
2165
|
statePath,
|
|
1221
2166
|
logPath,
|
|
@@ -1884,12 +2829,65 @@ function isWaitingForAgent(status) {
|
|
|
1884
2829
|
function isProcessAlive(pid) {
|
|
1885
2830
|
try {
|
|
1886
2831
|
process.kill(pid, 0);
|
|
1887
|
-
return
|
|
2832
|
+
return !isZombieProcess(pid);
|
|
1888
2833
|
}
|
|
1889
2834
|
catch (error) {
|
|
1890
2835
|
return error?.code === "EPERM";
|
|
1891
2836
|
}
|
|
1892
2837
|
}
|
|
2838
|
+
function isZombieProcess(pid) {
|
|
2839
|
+
const result = spawnSync("ps", ["-o", "stat=", "-p", String(pid)], {
|
|
2840
|
+
encoding: "utf8"
|
|
2841
|
+
});
|
|
2842
|
+
if (result.status !== 0) {
|
|
2843
|
+
return false;
|
|
2844
|
+
}
|
|
2845
|
+
return result.stdout.trim().toUpperCase().startsWith("Z");
|
|
2846
|
+
}
|
|
2847
|
+
function terminateProcessTarget(target, { timeoutMs = 3000 } = {}) {
|
|
2848
|
+
const pids = [...target.childPids, target.pid]
|
|
2849
|
+
.filter((pid, index, all) => Number.isInteger(pid) && pid > 0 && all.indexOf(pid) === index);
|
|
2850
|
+
const signals = [];
|
|
2851
|
+
for (const pid of pids) {
|
|
2852
|
+
signals.push(sendSignalToPid(pid, "SIGTERM"));
|
|
2853
|
+
}
|
|
2854
|
+
const exited = waitForPidsToExit(pids, timeoutMs);
|
|
2855
|
+
return {
|
|
2856
|
+
target,
|
|
2857
|
+
signal: "SIGTERM",
|
|
2858
|
+
signals,
|
|
2859
|
+
exited,
|
|
2860
|
+
remainingPids: pids.filter((pid) => isProcessAlive(pid))
|
|
2861
|
+
};
|
|
2862
|
+
}
|
|
2863
|
+
function sendSignalToPid(pid, signal) {
|
|
2864
|
+
try {
|
|
2865
|
+
process.kill(pid, signal);
|
|
2866
|
+
return {
|
|
2867
|
+
pid,
|
|
2868
|
+
signal,
|
|
2869
|
+
status: "sent"
|
|
2870
|
+
};
|
|
2871
|
+
}
|
|
2872
|
+
catch (error) {
|
|
2873
|
+
return {
|
|
2874
|
+
pid,
|
|
2875
|
+
signal,
|
|
2876
|
+
status: "failed",
|
|
2877
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2878
|
+
};
|
|
2879
|
+
}
|
|
2880
|
+
}
|
|
2881
|
+
function waitForPidsToExit(pids, timeoutMs) {
|
|
2882
|
+
const deadline = Date.now() + timeoutMs;
|
|
2883
|
+
while (Date.now() < deadline) {
|
|
2884
|
+
if (pids.every((pid) => !isProcessAlive(pid))) {
|
|
2885
|
+
return true;
|
|
2886
|
+
}
|
|
2887
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 50);
|
|
2888
|
+
}
|
|
2889
|
+
return pids.every((pid) => !isProcessAlive(pid));
|
|
2890
|
+
}
|
|
1893
2891
|
function markConversationStalled({ statePath, logPath, reason, detail = {} }) {
|
|
1894
2892
|
const releaseLock = acquireFileLock(`${statePath}.lock`);
|
|
1895
2893
|
let stalledConversation;
|
|
@@ -1944,6 +2942,58 @@ function markConversationStalled({ statePath, logPath, reason, detail = {} }) {
|
|
|
1944
2942
|
}
|
|
1945
2943
|
return stalledConversation;
|
|
1946
2944
|
}
|
|
2945
|
+
function markConversationNeedsModelSelection({ statePath, logPath, reason, detail = {} }) {
|
|
2946
|
+
const releaseLock = acquireFileLock(`${statePath}.lock`);
|
|
2947
|
+
let modelSelectionConversation;
|
|
2948
|
+
try {
|
|
2949
|
+
const conversation = loadState(statePath);
|
|
2950
|
+
if (!isWaitingForAgent(conversation.status)) {
|
|
2951
|
+
runtimeLog("info", "executor_monitor_finished", {
|
|
2952
|
+
conversation_id: conversation.conversation_id,
|
|
2953
|
+
status: conversation.status,
|
|
2954
|
+
reason: "conversation_changed_before_model_selection"
|
|
2955
|
+
});
|
|
2956
|
+
return conversation;
|
|
2957
|
+
}
|
|
2958
|
+
const now = new Date().toISOString();
|
|
2959
|
+
const detailRecord = detail;
|
|
2960
|
+
const modelSelection = isRecord(detailRecord.model_selection)
|
|
2961
|
+
? detailRecord.model_selection
|
|
2962
|
+
: {};
|
|
2963
|
+
modelSelectionConversation = {
|
|
2964
|
+
...conversation,
|
|
2965
|
+
status: "needs_model_selection",
|
|
2966
|
+
model_selection: {
|
|
2967
|
+
detected_at: now,
|
|
2968
|
+
message: reason,
|
|
2969
|
+
...modelSelection
|
|
2970
|
+
},
|
|
2971
|
+
updated_at: now
|
|
2972
|
+
};
|
|
2973
|
+
saveState(statePath, modelSelectionConversation);
|
|
2974
|
+
appendEvent(logPath, {
|
|
2975
|
+
ts: now,
|
|
2976
|
+
conversation_id: conversation.conversation_id,
|
|
2977
|
+
event: "conversation_needs_model_selection",
|
|
2978
|
+
status: "needs_model_selection",
|
|
2979
|
+
reason,
|
|
2980
|
+
...detailRecord
|
|
2981
|
+
});
|
|
2982
|
+
runtimeLog("warn", "conversation_needs_model_selection", {
|
|
2983
|
+
conversation_id: conversation.conversation_id,
|
|
2984
|
+
agent: executorForConversation(conversation).kind,
|
|
2985
|
+
executor_session: executorForConversation(conversation).session,
|
|
2986
|
+
state_path: statePath,
|
|
2987
|
+
event_log_path: logPath,
|
|
2988
|
+
reason,
|
|
2989
|
+
...detailRecord
|
|
2990
|
+
});
|
|
2991
|
+
}
|
|
2992
|
+
finally {
|
|
2993
|
+
releaseLock();
|
|
2994
|
+
}
|
|
2995
|
+
return modelSelectionConversation;
|
|
2996
|
+
}
|
|
1947
2997
|
function deliverStalledNotification({ statePath, logPath, conversation, reason, detail = {} }) {
|
|
1948
2998
|
if (!conversation.gateway_method) {
|
|
1949
2999
|
return;
|
|
@@ -2307,6 +3357,32 @@ function parseOptionalJson(text) {
|
|
|
2307
3357
|
return undefined;
|
|
2308
3358
|
}
|
|
2309
3359
|
}
|
|
3360
|
+
function createAgentSessionProvider(agent, options) {
|
|
3361
|
+
if (agent !== "codex") {
|
|
3362
|
+
throw new Error(`unsupported agent session provider: ${agent}`);
|
|
3363
|
+
}
|
|
3364
|
+
if (options.threadsJson || options.processesJson || options.rolloutsJson) {
|
|
3365
|
+
return new CodexLocalSessionProvider(new InlineCodexSessionAdapter({
|
|
3366
|
+
threads: parseJsonOption(options.threadsJson, "--threads-json"),
|
|
3367
|
+
processes: parseJsonOption(options.processesJson, "--processes-json"),
|
|
3368
|
+
rollouts: parseJsonOption(options.rolloutsJson, "--rollouts-json")
|
|
3369
|
+
}));
|
|
3370
|
+
}
|
|
3371
|
+
return new CodexLocalSessionProvider(new CodexStoreAdapter({
|
|
3372
|
+
codexHome: expandHome(options.codexHome)
|
|
3373
|
+
}));
|
|
3374
|
+
}
|
|
3375
|
+
function parseJsonOption(value, optionName) {
|
|
3376
|
+
if (!value) {
|
|
3377
|
+
return undefined;
|
|
3378
|
+
}
|
|
3379
|
+
try {
|
|
3380
|
+
return JSON.parse(String(value));
|
|
3381
|
+
}
|
|
3382
|
+
catch (error) {
|
|
3383
|
+
throw new Error(`${optionName} must be valid JSON: ${error.message}`);
|
|
3384
|
+
}
|
|
3385
|
+
}
|
|
2310
3386
|
function expandHome(filePath) {
|
|
2311
3387
|
if (filePath === "~") {
|
|
2312
3388
|
return process.env.HOME;
|
|
@@ -2357,6 +3433,56 @@ function classifyProcessFailure(result) {
|
|
|
2357
3433
|
}
|
|
2358
3434
|
return undefined;
|
|
2359
3435
|
}
|
|
3436
|
+
function readOutputTail(outputPath, maxBytes = 65536) {
|
|
3437
|
+
if (!outputPath) {
|
|
3438
|
+
return "";
|
|
3439
|
+
}
|
|
3440
|
+
try {
|
|
3441
|
+
const resolvedPath = expandHome(outputPath);
|
|
3442
|
+
const stat = fs.statSync(resolvedPath);
|
|
3443
|
+
const start = Math.max(0, stat.size - maxBytes);
|
|
3444
|
+
const length = stat.size - start;
|
|
3445
|
+
const fd = fs.openSync(resolvedPath, "r");
|
|
3446
|
+
try {
|
|
3447
|
+
const buffer = Buffer.alloc(length);
|
|
3448
|
+
fs.readSync(fd, buffer, 0, length, start);
|
|
3449
|
+
return buffer.toString("utf8");
|
|
3450
|
+
}
|
|
3451
|
+
finally {
|
|
3452
|
+
fs.closeSync(fd);
|
|
3453
|
+
}
|
|
3454
|
+
}
|
|
3455
|
+
catch {
|
|
3456
|
+
return "";
|
|
3457
|
+
}
|
|
3458
|
+
}
|
|
3459
|
+
function detectModelSelectionError(text) {
|
|
3460
|
+
const cleaned = cleanProcessText(text);
|
|
3461
|
+
if (!cleaned) {
|
|
3462
|
+
return undefined;
|
|
3463
|
+
}
|
|
3464
|
+
const unsupportedAccount = /The '([^']+)' model is not supported when using Codex with a ChatGPT account/i.exec(cleaned);
|
|
3465
|
+
if (unsupportedAccount) {
|
|
3466
|
+
return {
|
|
3467
|
+
kind: "unsupported_chatgpt_account_model",
|
|
3468
|
+
attempted_model: unsupportedAccount[1],
|
|
3469
|
+
message: unsupportedAccount[0]
|
|
3470
|
+
};
|
|
3471
|
+
}
|
|
3472
|
+
const unadvertised = /Cannot apply --model "([^"]+)": the ACP agent did not advertise that model\. Available models:\s*([^\n\r]+)/i.exec(cleaned);
|
|
3473
|
+
if (unadvertised) {
|
|
3474
|
+
return {
|
|
3475
|
+
kind: "unadvertised_acpx_model",
|
|
3476
|
+
attempted_model: unadvertised[1],
|
|
3477
|
+
available_models: unadvertised[2]
|
|
3478
|
+
.split(",")
|
|
3479
|
+
.map((model) => model.trim())
|
|
3480
|
+
.filter(Boolean),
|
|
3481
|
+
message: unadvertised[0]
|
|
3482
|
+
};
|
|
3483
|
+
}
|
|
3484
|
+
return undefined;
|
|
3485
|
+
}
|
|
2360
3486
|
function runtimeLog(level, event, fields = {}) {
|
|
2361
3487
|
try {
|
|
2362
3488
|
writeRuntimeLog({
|
|
@@ -2393,10 +3519,11 @@ function usage() {
|
|
|
2393
3519
|
agent-knock-knock send --conversation <id> --message <text> [--type answer|task|control] [--all-proxy <url>] [--agent-timeout-minutes <minutes>]
|
|
2394
3520
|
agent-knock-knock cancel --conversation <id> [--all-proxy <url>]
|
|
2395
3521
|
agent-knock-knock recover --conversation <id> [--session <name>] [--all-proxy <url>]
|
|
2396
|
-
agent-knock-knock restart --conversation <id> [--session <name>] [--all-proxy <url>]
|
|
2397
3522
|
agent-knock-knock close --conversation <id> [--reason <text>]
|
|
2398
3523
|
agent-knock-knock install-openclaw [--openclaw-bin <path>] [--skill-path <path>] [--no-restart]
|
|
2399
3524
|
agent-knock-knock doctor
|
|
3525
|
+
agent-knock-knock agent discover --agent codex --scope capabilities|sessions|active
|
|
3526
|
+
agent-knock-knock agent takeover --agent codex --session-id <id> --strategy safe_resume|terminate_then_resume|fork [--create-conversation]
|
|
2400
3527
|
agent-knock-knock callback --state <file> --message-json <json> [--record-only]
|
|
2401
3528
|
agent-knock-knock transcript --log <file> [--include-raw]
|
|
2402
3529
|
agent-knock-knock transcript --conversation <dir> [--include-raw]
|