@scotthuang/agent-knock-knock 0.1.1 → 0.2.0
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 +18 -0
- package/README.md +31 -0
- 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 +1142 -25
- 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/openclaw-plugin.js +159 -2
- 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/package.json +1 -1
- package/templates/openclaw-skills/agent-knock-knock/SKILL.md +30 -1
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
11
|
import { EXECUTOR_KINDS, acpxCommandForExecutor, executorDefinitionForKind, modelEnvForExecutor, normalizeModelForExecutor, proxyEnvForExecutor, sessionRecoveryStrategyForExecutor } 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
|
}
|
|
@@ -86,6 +121,9 @@ function runCommand(commandName, options) {
|
|
|
86
121
|
else if (commandName === "monitor") {
|
|
87
122
|
runMonitor(options);
|
|
88
123
|
}
|
|
124
|
+
else if (commandName === "agent") {
|
|
125
|
+
await runAgent(options);
|
|
126
|
+
}
|
|
89
127
|
else {
|
|
90
128
|
usage();
|
|
91
129
|
process.exitCode = commandName ? 1 : 0;
|
|
@@ -170,6 +208,495 @@ function runDoctor(options) {
|
|
|
170
208
|
options
|
|
171
209
|
});
|
|
172
210
|
}
|
|
211
|
+
async function runAgent(options) {
|
|
212
|
+
const agentCommand = required(options.agentCommand, "agent subcommand is required: discover or takeover");
|
|
213
|
+
if (agentCommand === "discover") {
|
|
214
|
+
printJson(await runAgentDiscover(options));
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
if (agentCommand === "takeover") {
|
|
218
|
+
printJson(await runAgentTakeover(options));
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
throw new Error(`unsupported agent subcommand: ${agentCommand}`);
|
|
222
|
+
}
|
|
223
|
+
async function runAgentDiscover(options) {
|
|
224
|
+
const agent = required(options.agent, "--agent is required");
|
|
225
|
+
const scope = required(options.scope, "--scope is required");
|
|
226
|
+
const provider = createAgentSessionProvider(agent, options);
|
|
227
|
+
const capabilities = await provider.getCapabilities();
|
|
228
|
+
if (scope === "capabilities") {
|
|
229
|
+
return {
|
|
230
|
+
agent,
|
|
231
|
+
scope,
|
|
232
|
+
capabilities
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
if (scope === "sessions") {
|
|
236
|
+
return {
|
|
237
|
+
agent,
|
|
238
|
+
scope,
|
|
239
|
+
capabilities,
|
|
240
|
+
sessions: await provider.listHistoricalSessions()
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
if (scope === "active") {
|
|
244
|
+
return {
|
|
245
|
+
agent,
|
|
246
|
+
scope,
|
|
247
|
+
capabilities,
|
|
248
|
+
active: await provider.listActiveSessions()
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
throw new Error(`unsupported discover scope: ${scope}`);
|
|
252
|
+
}
|
|
253
|
+
async function runAgentTakeover(options) {
|
|
254
|
+
const agent = required(options.agent, "--agent is required");
|
|
255
|
+
const sessionId = required(options.sessionId, "--session-id is required");
|
|
256
|
+
const strategy = options.strategy ?? "terminate_then_resume";
|
|
257
|
+
const provider = createAgentSessionProvider(agent, options);
|
|
258
|
+
const session = await provider.getSession(sessionId);
|
|
259
|
+
if (!session) {
|
|
260
|
+
return {
|
|
261
|
+
agent,
|
|
262
|
+
sessionId,
|
|
263
|
+
strategy,
|
|
264
|
+
status: "blocked",
|
|
265
|
+
sideEffectsExecuted: false,
|
|
266
|
+
error: {
|
|
267
|
+
code: "session_not_found",
|
|
268
|
+
message: `No ${agent} session found for ${sessionId}`
|
|
269
|
+
}
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
if (strategy === "safe_resume") {
|
|
273
|
+
const plan = planSafeResume(session, await provider.listActiveSessions());
|
|
274
|
+
if (plan.allowed && options.createConversation) {
|
|
275
|
+
const modelInfo = await provider.getSessionModel(session.id);
|
|
276
|
+
const attached = createNativeSessionConversation({
|
|
277
|
+
agent,
|
|
278
|
+
strategy,
|
|
279
|
+
session,
|
|
280
|
+
modelInfo,
|
|
281
|
+
options
|
|
282
|
+
});
|
|
283
|
+
return {
|
|
284
|
+
agent,
|
|
285
|
+
sessionId,
|
|
286
|
+
strategy,
|
|
287
|
+
status: "attached",
|
|
288
|
+
sideEffectsExecuted: true,
|
|
289
|
+
plan,
|
|
290
|
+
...attached
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
return {
|
|
294
|
+
agent,
|
|
295
|
+
sessionId,
|
|
296
|
+
strategy,
|
|
297
|
+
status: plan.allowed ? "ready" : "blocked",
|
|
298
|
+
sideEffectsExecuted: false,
|
|
299
|
+
plan
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
if (strategy === "terminate_then_resume") {
|
|
303
|
+
const activeSessions = await provider.listActiveSessions();
|
|
304
|
+
const plan = planTakeover(session, activeSessions);
|
|
305
|
+
if (options.confirmTerminate === true) {
|
|
306
|
+
const expectedPid = Number(required(options.expectedPid, "--expected-pid is required with --confirm-terminate"));
|
|
307
|
+
if (!Number.isInteger(expectedPid) || expectedPid <= 0) {
|
|
308
|
+
throw new Error("--expected-pid must be a positive integer");
|
|
309
|
+
}
|
|
310
|
+
if (!options.createConversation) {
|
|
311
|
+
throw new Error("--create-conversation is required with --confirm-terminate");
|
|
312
|
+
}
|
|
313
|
+
const targetSelection = selectTerminateTarget({
|
|
314
|
+
plan,
|
|
315
|
+
session,
|
|
316
|
+
activeSessions,
|
|
317
|
+
expectedPid,
|
|
318
|
+
allowCwdOnly: options.allowCwdOnly === true
|
|
319
|
+
});
|
|
320
|
+
if (!targetSelection.allowed) {
|
|
321
|
+
return {
|
|
322
|
+
agent,
|
|
323
|
+
sessionId,
|
|
324
|
+
strategy,
|
|
325
|
+
status: "blocked",
|
|
326
|
+
sideEffectsExecuted: false,
|
|
327
|
+
plan,
|
|
328
|
+
error: {
|
|
329
|
+
code: targetSelection.code,
|
|
330
|
+
message: targetSelection.message
|
|
331
|
+
}
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
const { target, matchKind } = targetSelection;
|
|
335
|
+
const termination = terminateProcessTarget(target, {
|
|
336
|
+
timeoutMs: Number(options.terminateTimeoutMs ?? 3000)
|
|
337
|
+
});
|
|
338
|
+
const activeAfterTermination = await provider.listActiveSessions();
|
|
339
|
+
const afterTerminationPlan = planTakeover(session, activeAfterTermination);
|
|
340
|
+
if (afterTerminationPlan.targets.some((candidate) => candidate.sessionId === session.id || candidate.pid === expectedPid)) {
|
|
341
|
+
return {
|
|
342
|
+
agent,
|
|
343
|
+
sessionId,
|
|
344
|
+
strategy,
|
|
345
|
+
status: "blocked",
|
|
346
|
+
sideEffectsExecuted: true,
|
|
347
|
+
plan: afterTerminationPlan,
|
|
348
|
+
termination,
|
|
349
|
+
error: {
|
|
350
|
+
code: "target_still_active",
|
|
351
|
+
message: `Codex process ${expectedPid} still appears active after termination.`
|
|
352
|
+
}
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
const modelInfo = await provider.getSessionModel(session.id);
|
|
356
|
+
const attached = createNativeSessionConversation({
|
|
357
|
+
agent,
|
|
358
|
+
strategy,
|
|
359
|
+
session,
|
|
360
|
+
modelInfo,
|
|
361
|
+
options,
|
|
362
|
+
takeoverMatchKind: matchKind
|
|
363
|
+
});
|
|
364
|
+
return {
|
|
365
|
+
agent,
|
|
366
|
+
sessionId,
|
|
367
|
+
strategy,
|
|
368
|
+
status: "attached",
|
|
369
|
+
sideEffectsExecuted: true,
|
|
370
|
+
plan,
|
|
371
|
+
termination,
|
|
372
|
+
matchKind,
|
|
373
|
+
...attached
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
return {
|
|
377
|
+
agent,
|
|
378
|
+
sessionId,
|
|
379
|
+
strategy,
|
|
380
|
+
status: plan.requiresConfirmation ? "requires_confirmation" : "blocked",
|
|
381
|
+
sideEffectsExecuted: false,
|
|
382
|
+
plan
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
if (strategy === "fork") {
|
|
386
|
+
const contextPackage = await provider.getForkContext({
|
|
387
|
+
sessionId,
|
|
388
|
+
maxMessages: Number(options.maxMessages ?? 12),
|
|
389
|
+
maxCommands: Number(options.maxCommands ?? 8),
|
|
390
|
+
maxTextLength: Number(options.maxTextLength ?? 1200)
|
|
391
|
+
});
|
|
392
|
+
if (!contextPackage) {
|
|
393
|
+
return {
|
|
394
|
+
agent,
|
|
395
|
+
sessionId,
|
|
396
|
+
strategy,
|
|
397
|
+
status: "blocked",
|
|
398
|
+
sideEffectsExecuted: false,
|
|
399
|
+
error: {
|
|
400
|
+
code: "fork_context_unavailable",
|
|
401
|
+
message: `No fork context could be built for ${sessionId}`
|
|
402
|
+
}
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
if (options.createConversation) {
|
|
406
|
+
const forkSummary = String(required(options.forkSummary ?? options.summary, "--fork-summary is required when creating a fork conversation"));
|
|
407
|
+
const modelInfo = await provider.getSessionModel(session.id);
|
|
408
|
+
const attached = createForkConversation({
|
|
409
|
+
agent,
|
|
410
|
+
strategy,
|
|
411
|
+
session,
|
|
412
|
+
contextPackage,
|
|
413
|
+
forkSummary,
|
|
414
|
+
modelInfo,
|
|
415
|
+
options
|
|
416
|
+
});
|
|
417
|
+
return {
|
|
418
|
+
agent,
|
|
419
|
+
sessionId,
|
|
420
|
+
strategy,
|
|
421
|
+
status: "forked",
|
|
422
|
+
sideEffectsExecuted: true,
|
|
423
|
+
plan: planFork(session, contextPackage),
|
|
424
|
+
...attached
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
return {
|
|
428
|
+
agent,
|
|
429
|
+
sessionId,
|
|
430
|
+
strategy,
|
|
431
|
+
status: "awaiting_openclaw_summary",
|
|
432
|
+
sideEffectsExecuted: false,
|
|
433
|
+
plan: planFork(session, contextPackage),
|
|
434
|
+
summaryPrompt: buildForkSummaryPrompt({ agent, session, contextPackage }),
|
|
435
|
+
nextAction: {
|
|
436
|
+
actor: "openclaw",
|
|
437
|
+
action: "summarize_and_confirm_fork",
|
|
438
|
+
instructions: [
|
|
439
|
+
"Summarize plan.contextPackage for the user before creating a forked AKK-managed session.",
|
|
440
|
+
"Do not inject the raw rollout or full contextPackage into the new coding agent.",
|
|
441
|
+
"Ask the user to confirm the summary.",
|
|
442
|
+
"After confirmation, call this tool again with strategy=fork, createConversation=true, and forkSummary set to the confirmed summary."
|
|
443
|
+
],
|
|
444
|
+
followUpTool: "agent_knock_knock_agent_takeover",
|
|
445
|
+
followUpParams: {
|
|
446
|
+
agent,
|
|
447
|
+
sessionId,
|
|
448
|
+
strategy: "fork",
|
|
449
|
+
createConversation: true,
|
|
450
|
+
forkSummary: "<confirmed OpenClaw summary>"
|
|
451
|
+
}
|
|
452
|
+
},
|
|
453
|
+
next: "Use summaryPrompt to summarize the bounded context package for the user, ask for confirmation, then create the forked AKK-managed session with forkSummary."
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
throw new Error(`unsupported takeover strategy: ${strategy}`);
|
|
457
|
+
}
|
|
458
|
+
function buildForkSummaryPrompt({ agent, session, contextPackage }) {
|
|
459
|
+
return [
|
|
460
|
+
"You are OpenClaw summarizing a bounded native coding-agent session context before Agent Knock Knock forks it into a new managed session.",
|
|
461
|
+
"",
|
|
462
|
+
"Goal:",
|
|
463
|
+
"- Produce a concise, user-reviewable summary that can be safely injected into a new AKK-managed coding-agent session after the user confirms it.",
|
|
464
|
+
"- The new session must use the summary only; do not pass raw rollout history or the full context package to the coding agent.",
|
|
465
|
+
"",
|
|
466
|
+
"Source:",
|
|
467
|
+
`- Agent: ${agent}`,
|
|
468
|
+
`- Session id: ${session.id}`,
|
|
469
|
+
`- Workspace: ${session.cwd}`,
|
|
470
|
+
`- Title: ${session.title ?? session.preview ?? session.firstUserMessage ?? "(unknown)"}`,
|
|
471
|
+
`- Context messages included: ${contextPackage.messages.length}`,
|
|
472
|
+
`- Commands included: ${contextPackage.commands.length}`,
|
|
473
|
+
`- Context truncated: ${contextPackage.truncated ? "yes" : "no"}`,
|
|
474
|
+
"",
|
|
475
|
+
"Summary format:",
|
|
476
|
+
"1. Original user goal",
|
|
477
|
+
"2. Work already completed",
|
|
478
|
+
"3. Current state and important findings",
|
|
479
|
+
"4. Constraints, risks, or files/workspace details the forked agent must preserve",
|
|
480
|
+
"5. Recommended next step for the forked agent",
|
|
481
|
+
"",
|
|
482
|
+
"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."
|
|
483
|
+
].join("\n");
|
|
484
|
+
}
|
|
485
|
+
function selectTerminateTarget({ plan, session, activeSessions, expectedPid, allowCwdOnly }) {
|
|
486
|
+
const exactTarget = plan.targets.find((candidate) => candidate.pid === expectedPid && candidate.sessionId === session.id);
|
|
487
|
+
if (exactTarget) {
|
|
488
|
+
return {
|
|
489
|
+
allowed: true,
|
|
490
|
+
target: exactTarget,
|
|
491
|
+
matchKind: "exact_session"
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
if (!allowCwdOnly) {
|
|
495
|
+
return {
|
|
496
|
+
allowed: false,
|
|
497
|
+
code: plan.allowed && plan.requiresConfirmation ? "expected_pid_mismatch" : "takeover_not_confirmable",
|
|
498
|
+
message: plan.allowed && plan.requiresConfirmation
|
|
499
|
+
? `Expected pid ${expectedPid} is no longer the exact active Codex process for session ${session.id}.`
|
|
500
|
+
: "The current active Codex process no longer has an exact session match that can be safely terminated."
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
const cwdOnlyTarget = plan.targets.find((candidate) => candidate.pid === expectedPid &&
|
|
504
|
+
candidate.cwd === session.cwd &&
|
|
505
|
+
candidate.sessionId === undefined);
|
|
506
|
+
const stillActive = activeSessions.some((candidate) => candidate.pid === expectedPid &&
|
|
507
|
+
candidate.cwd === session.cwd &&
|
|
508
|
+
candidate.sessionId === undefined);
|
|
509
|
+
if (!cwdOnlyTarget || !stillActive) {
|
|
510
|
+
return {
|
|
511
|
+
allowed: false,
|
|
512
|
+
code: "expected_pid_mismatch",
|
|
513
|
+
message: `Expected pid ${expectedPid} is no longer an active Codex process in ${session.cwd}.`
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
return {
|
|
517
|
+
allowed: true,
|
|
518
|
+
target: cwdOnlyTarget,
|
|
519
|
+
matchKind: "cwd_only_confirmed"
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
function createForkConversation({ agent, strategy, session, contextPackage, forkSummary, modelInfo, options }) {
|
|
523
|
+
const workspace = session.cwd;
|
|
524
|
+
const storeDir = expandHome(options.storeDir ?? options.logDir ?? defaultStoreDir(workspace));
|
|
525
|
+
cleanupIdleConversations(storeDir, options);
|
|
526
|
+
const executor = resolveExecutor({
|
|
527
|
+
kind: agent,
|
|
528
|
+
session: options.session ?? options.executorSession ?? uniqueDelegateSessionName(agent)
|
|
529
|
+
});
|
|
530
|
+
const now = new Date();
|
|
531
|
+
const conversation = createConversation({
|
|
532
|
+
userRequest: options.request ?? `Fork native ${agent} session ${session.id}`,
|
|
533
|
+
workspace,
|
|
534
|
+
openclawSession: options.openclawSession ?? "agent:main:main",
|
|
535
|
+
executorKind: executor.kind,
|
|
536
|
+
executorSession: executor.session,
|
|
537
|
+
softLimit: Number(options.softLimit ?? 50),
|
|
538
|
+
hardLimit: Number(options.hardLimit ?? 100),
|
|
539
|
+
now
|
|
540
|
+
});
|
|
541
|
+
const paths = pathsForConversation(conversation.conversation_id, storeDir);
|
|
542
|
+
const callbackCommand = options.callbackCommand
|
|
543
|
+
? expandCallbackCommandTemplate(options.callbackCommand, { statePath: paths.statePath })
|
|
544
|
+
: buildCallbackCommand({
|
|
545
|
+
statePath: paths.statePath,
|
|
546
|
+
gatewayUrl: options.gatewayUrl ?? "ws://127.0.0.1:18789",
|
|
547
|
+
token: options.token,
|
|
548
|
+
openclawSession: options.openclawSession ?? "agent:main:main",
|
|
549
|
+
gatewayMethod: options.gatewayMethod,
|
|
550
|
+
gatewaySession: options.gatewaySession,
|
|
551
|
+
openclawBin: options.openclawBin ?? resolveOptionalExecutable("openclaw")
|
|
552
|
+
});
|
|
553
|
+
const explicitModel = options.model ?? options.codexModel;
|
|
554
|
+
const executorModel = explicitModel ?? modelInfo?.acpxModel ?? modelEnvForExecutor(executor, process.env);
|
|
555
|
+
const forkedConversation = withStoragePaths({
|
|
556
|
+
...conversation,
|
|
557
|
+
executor,
|
|
558
|
+
status: "idle",
|
|
559
|
+
idle_since: now.toISOString(),
|
|
560
|
+
updated_at: now.toISOString(),
|
|
561
|
+
callback_command: callbackCommand,
|
|
562
|
+
gateway_url: options.gatewayUrl ?? "ws://127.0.0.1:18789",
|
|
563
|
+
gateway_method: options.gatewayMethod,
|
|
564
|
+
gateway_session: options.gatewaySession ?? options.openclawSession ?? "agent:main:main",
|
|
565
|
+
openclaw_bin: options.openclawBin ?? resolveOptionalExecutable("openclaw"),
|
|
566
|
+
executor_all_proxy: proxyForExecutor(executor, options),
|
|
567
|
+
executor_model: executorModel,
|
|
568
|
+
fork_context_takeover: {
|
|
569
|
+
agent,
|
|
570
|
+
source_session_id: session.id,
|
|
571
|
+
source_cwd: session.cwd,
|
|
572
|
+
source_title: session.title,
|
|
573
|
+
source_updated_at_ms: session.updatedAtMs,
|
|
574
|
+
strategy,
|
|
575
|
+
forked_at: now.toISOString(),
|
|
576
|
+
summary: forkSummary,
|
|
577
|
+
context_message_count: contextPackage.messages.length,
|
|
578
|
+
context_command_count: contextPackage.commands.length,
|
|
579
|
+
context_truncated: contextPackage.truncated,
|
|
580
|
+
native_model: modelInfo?.model,
|
|
581
|
+
acpx_model: modelInfo?.acpxModel,
|
|
582
|
+
model_source: modelInfo?.source,
|
|
583
|
+
needs_bootstrap: true
|
|
584
|
+
}
|
|
585
|
+
}, paths);
|
|
586
|
+
saveState(paths.statePath, forkedConversation);
|
|
587
|
+
appendEvent(paths.logPath, {
|
|
588
|
+
ts: now.toISOString(),
|
|
589
|
+
conversation_id: forkedConversation.conversation_id,
|
|
590
|
+
event: "native_session_forked",
|
|
591
|
+
agent,
|
|
592
|
+
strategy,
|
|
593
|
+
source_session_id: session.id,
|
|
594
|
+
source_cwd: session.cwd,
|
|
595
|
+
executor,
|
|
596
|
+
context_message_count: contextPackage.messages.length,
|
|
597
|
+
context_command_count: contextPackage.commands.length,
|
|
598
|
+
context_truncated: contextPackage.truncated
|
|
599
|
+
});
|
|
600
|
+
runtimeLog("info", "native_session_forked", {
|
|
601
|
+
conversation_id: forkedConversation.conversation_id,
|
|
602
|
+
agent,
|
|
603
|
+
strategy,
|
|
604
|
+
source_session_id: session.id,
|
|
605
|
+
executor_session: executor.session,
|
|
606
|
+
state_path: paths.statePath,
|
|
607
|
+
event_log_path: paths.logPath
|
|
608
|
+
});
|
|
609
|
+
return {
|
|
610
|
+
conversation: forkedConversation,
|
|
611
|
+
paths,
|
|
612
|
+
next: `Use AKK send ${forkedConversation.conversation_id}: <message> to start the forked ${agent} session with the approved summary.`
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
function createNativeSessionConversation({ agent, strategy, session, modelInfo, options, takeoverMatchKind = strategy }) {
|
|
616
|
+
const workspace = session.cwd;
|
|
617
|
+
const storeDir = expandHome(options.storeDir ?? options.logDir ?? defaultStoreDir(workspace));
|
|
618
|
+
cleanupIdleConversations(storeDir, options);
|
|
619
|
+
const executor = resolveExecutor({
|
|
620
|
+
kind: agent,
|
|
621
|
+
session: session.id
|
|
622
|
+
});
|
|
623
|
+
const now = new Date();
|
|
624
|
+
const conversation = createConversation({
|
|
625
|
+
userRequest: options.request ?? `Attach native ${agent} session ${session.id}`,
|
|
626
|
+
workspace,
|
|
627
|
+
openclawSession: options.openclawSession ?? "agent:main:main",
|
|
628
|
+
executorKind: executor.kind,
|
|
629
|
+
executorSession: executor.session,
|
|
630
|
+
softLimit: Number(options.softLimit ?? 50),
|
|
631
|
+
hardLimit: Number(options.hardLimit ?? 100),
|
|
632
|
+
now
|
|
633
|
+
});
|
|
634
|
+
const paths = pathsForConversation(conversation.conversation_id, storeDir);
|
|
635
|
+
const callbackCommand = options.callbackCommand
|
|
636
|
+
? expandCallbackCommandTemplate(options.callbackCommand, { statePath: paths.statePath })
|
|
637
|
+
: buildCallbackCommand({
|
|
638
|
+
statePath: paths.statePath,
|
|
639
|
+
gatewayUrl: options.gatewayUrl ?? "ws://127.0.0.1:18789",
|
|
640
|
+
token: options.token,
|
|
641
|
+
openclawSession: options.openclawSession ?? "agent:main:main",
|
|
642
|
+
gatewayMethod: options.gatewayMethod,
|
|
643
|
+
gatewaySession: options.gatewaySession,
|
|
644
|
+
openclawBin: options.openclawBin ?? resolveOptionalExecutable("openclaw")
|
|
645
|
+
});
|
|
646
|
+
const explicitModel = options.model ?? options.codexModel;
|
|
647
|
+
const executorModel = explicitModel ?? modelInfo?.acpxModel ?? modelEnvForExecutor(executor, process.env);
|
|
648
|
+
const attachedConversation = withStoragePaths({
|
|
649
|
+
...conversation,
|
|
650
|
+
executor,
|
|
651
|
+
status: "idle",
|
|
652
|
+
idle_since: now.toISOString(),
|
|
653
|
+
updated_at: now.toISOString(),
|
|
654
|
+
callback_command: callbackCommand,
|
|
655
|
+
gateway_url: options.gatewayUrl ?? "ws://127.0.0.1:18789",
|
|
656
|
+
gateway_method: options.gatewayMethod,
|
|
657
|
+
gateway_session: options.gatewaySession ?? options.openclawSession ?? "agent:main:main",
|
|
658
|
+
openclaw_bin: options.openclawBin ?? resolveOptionalExecutable("openclaw"),
|
|
659
|
+
executor_all_proxy: proxyForExecutor(executor, options),
|
|
660
|
+
executor_model: executorModel,
|
|
661
|
+
native_session_takeover: {
|
|
662
|
+
agent,
|
|
663
|
+
native_session_id: session.id,
|
|
664
|
+
source_cwd: session.cwd,
|
|
665
|
+
source_title: session.title,
|
|
666
|
+
strategy,
|
|
667
|
+
attached_at: now.toISOString(),
|
|
668
|
+
native_model: modelInfo?.model,
|
|
669
|
+
acpx_model: modelInfo?.acpxModel,
|
|
670
|
+
model_source: modelInfo?.source,
|
|
671
|
+
takeover_match_kind: takeoverMatchKind,
|
|
672
|
+
needs_bootstrap: true
|
|
673
|
+
}
|
|
674
|
+
}, paths);
|
|
675
|
+
saveState(paths.statePath, attachedConversation);
|
|
676
|
+
appendEvent(paths.logPath, {
|
|
677
|
+
ts: now.toISOString(),
|
|
678
|
+
conversation_id: attachedConversation.conversation_id,
|
|
679
|
+
event: "native_session_attached",
|
|
680
|
+
agent,
|
|
681
|
+
strategy,
|
|
682
|
+
native_session_id: session.id,
|
|
683
|
+
source_cwd: session.cwd,
|
|
684
|
+
executor
|
|
685
|
+
});
|
|
686
|
+
runtimeLog("info", "native_session_attached", {
|
|
687
|
+
conversation_id: attachedConversation.conversation_id,
|
|
688
|
+
agent,
|
|
689
|
+
strategy,
|
|
690
|
+
native_session_id: session.id,
|
|
691
|
+
state_path: paths.statePath,
|
|
692
|
+
event_log_path: paths.logPath
|
|
693
|
+
});
|
|
694
|
+
return {
|
|
695
|
+
conversation: attachedConversation,
|
|
696
|
+
paths,
|
|
697
|
+
next: `Use AKK send ${attachedConversation.conversation_id}: <message> to continue this native ${agent} session through AKK.`
|
|
698
|
+
};
|
|
699
|
+
}
|
|
173
700
|
function runNew(options) {
|
|
174
701
|
const request = required(options.request, "--request is required");
|
|
175
702
|
const workspace = options.workspace ?? process.cwd();
|
|
@@ -489,8 +1016,15 @@ function runDelegate(options) {
|
|
|
489
1016
|
note: "Run again with --send to send this task through acpx."
|
|
490
1017
|
});
|
|
491
1018
|
}
|
|
492
|
-
function ensureExecutorSession({ acpxPath, executor, cwd, env }) {
|
|
493
|
-
|
|
1019
|
+
function ensureExecutorSession({ acpxPath, executor, cwd, env, resumeSessionId }) {
|
|
1020
|
+
const args = [acpxCommandForExecutor(executor), "sessions", "ensure"];
|
|
1021
|
+
if (resumeSessionId) {
|
|
1022
|
+
args.push("--resume-session", resumeSessionId);
|
|
1023
|
+
}
|
|
1024
|
+
else {
|
|
1025
|
+
args.push("--name", executor.session);
|
|
1026
|
+
}
|
|
1027
|
+
return spawnSync(acpxPath, args, {
|
|
494
1028
|
encoding: "utf8",
|
|
495
1029
|
cwd,
|
|
496
1030
|
env
|
|
@@ -622,8 +1156,19 @@ function runSend(options) {
|
|
|
622
1156
|
if (conversation.status === "needs_recovery") {
|
|
623
1157
|
throw new Error(`cannot send to ${conversation.conversation_id}; choose recover, restart, or close first`);
|
|
624
1158
|
}
|
|
1159
|
+
if (conversation.status === "needs_model_selection" && !options.model) {
|
|
1160
|
+
throw new Error(`cannot send to ${conversation.conversation_id}; choose a supported model with --model first`);
|
|
1161
|
+
}
|
|
625
1162
|
const executor = executorForConversation(conversation);
|
|
626
1163
|
const type = options.type ?? (conversation.status === "waiting_for_openclaw" ? "answer" : "task");
|
|
1164
|
+
const nativeTakeoverForSend = isRecord(conversation.native_session_takeover)
|
|
1165
|
+
? conversation.native_session_takeover
|
|
1166
|
+
: undefined;
|
|
1167
|
+
const forkTakeoverForSend = isRecord(conversation.fork_context_takeover)
|
|
1168
|
+
? conversation.fork_context_takeover
|
|
1169
|
+
: undefined;
|
|
1170
|
+
const needsNativeTakeoverBootstrap = nativeTakeoverForSend?.["needs_bootstrap"] === true;
|
|
1171
|
+
const needsForkTakeoverBootstrap = forkTakeoverForSend?.["needs_bootstrap"] === true;
|
|
627
1172
|
const message = createMessage({
|
|
628
1173
|
conversation,
|
|
629
1174
|
from: "openclaw",
|
|
@@ -635,10 +1180,21 @@ function runSend(options) {
|
|
|
635
1180
|
executor_session: executor.session
|
|
636
1181
|
}
|
|
637
1182
|
});
|
|
1183
|
+
const previousModelSelection = isRecord(conversation.model_selection)
|
|
1184
|
+
? conversation.model_selection
|
|
1185
|
+
: {};
|
|
638
1186
|
const nextConversation = {
|
|
639
1187
|
...applyMessageToConversation(conversation, message),
|
|
640
1188
|
executor,
|
|
641
|
-
claude_session: executor.kind === "claude" ? executor.session : conversation.claude_session
|
|
1189
|
+
claude_session: executor.kind === "claude" ? executor.session : conversation.claude_session,
|
|
1190
|
+
executor_model: options.model ?? conversation.executor_model,
|
|
1191
|
+
model_selection: conversation.status === "needs_model_selection"
|
|
1192
|
+
? {
|
|
1193
|
+
...previousModelSelection,
|
|
1194
|
+
resolved_at: new Date().toISOString(),
|
|
1195
|
+
selected_model: options.model
|
|
1196
|
+
}
|
|
1197
|
+
: conversation.model_selection
|
|
642
1198
|
};
|
|
643
1199
|
saveState(statePath, nextConversation);
|
|
644
1200
|
appendEvent(logPath, messageEvent(message));
|
|
@@ -651,10 +1207,34 @@ function runSend(options) {
|
|
|
651
1207
|
event_log_path: logPath,
|
|
652
1208
|
message: textSummary(messageBody)
|
|
653
1209
|
});
|
|
654
|
-
const acpxPath = resolveExecutable("acpx");
|
|
655
1210
|
const executorEnv = environmentForExecutor(executor, {
|
|
656
1211
|
allProxy: options.allProxy ?? conversation.executor_all_proxy
|
|
657
1212
|
});
|
|
1213
|
+
const payload = buildAgentSendPayload({
|
|
1214
|
+
conversation,
|
|
1215
|
+
executor,
|
|
1216
|
+
message,
|
|
1217
|
+
includeNativeTakeoverBootstrap: needsNativeTakeoverBootstrap,
|
|
1218
|
+
includeForkTakeoverBootstrap: needsForkTakeoverBootstrap,
|
|
1219
|
+
forkTakeover: forkTakeoverForSend
|
|
1220
|
+
});
|
|
1221
|
+
if (nativeTakeoverForSend?.["native_session_id"] && executor.kind === "codex") {
|
|
1222
|
+
runNativeCodexResumeSend({
|
|
1223
|
+
options,
|
|
1224
|
+
conversation,
|
|
1225
|
+
nextConversation,
|
|
1226
|
+
statePath,
|
|
1227
|
+
logPath,
|
|
1228
|
+
executor,
|
|
1229
|
+
executorEnv,
|
|
1230
|
+
message,
|
|
1231
|
+
payload,
|
|
1232
|
+
nativeTakeover: nativeTakeoverForSend,
|
|
1233
|
+
needsNativeTakeoverBootstrap
|
|
1234
|
+
});
|
|
1235
|
+
return;
|
|
1236
|
+
}
|
|
1237
|
+
const acpxPath = resolveExecutable("acpx");
|
|
658
1238
|
const executorModel = modelForExecutor(executor, {
|
|
659
1239
|
model: options.model ?? conversation.executor_model
|
|
660
1240
|
});
|
|
@@ -662,7 +1242,8 @@ function runSend(options) {
|
|
|
662
1242
|
acpxPath,
|
|
663
1243
|
executor,
|
|
664
1244
|
cwd: conversation.workspace ?? process.cwd(),
|
|
665
|
-
env: executorEnv
|
|
1245
|
+
env: executorEnv,
|
|
1246
|
+
resumeSessionId: nativeTakeoverForSend?.["native_session_id"]
|
|
666
1247
|
});
|
|
667
1248
|
appendEvent(logPath, {
|
|
668
1249
|
ts: new Date().toISOString(),
|
|
@@ -714,13 +1295,6 @@ function runSend(options) {
|
|
|
714
1295
|
}
|
|
715
1296
|
throw new Error(cleanProcessText(ensureSession.stderr || ensureSession.stdout || `acpx ${executor.kind} sessions ensure exited with status ${ensureSession.status}`));
|
|
716
1297
|
}
|
|
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
1298
|
const acpxArgs = buildAcpxPromptArgs({ executor, payload, model: executorModel });
|
|
725
1299
|
if (options.background) {
|
|
726
1300
|
const outputPath = path.join(path.dirname(logPath), `${executor.kind}-followup-output.log`);
|
|
@@ -766,8 +1340,16 @@ function runSend(options) {
|
|
|
766
1340
|
executor_pid: child.pid ?? null,
|
|
767
1341
|
agent_timeout_minutes: Number(options.agentTimeoutMinutes ?? DEFAULT_AGENT_TIMEOUT_MINUTES)
|
|
768
1342
|
});
|
|
769
|
-
|
|
1343
|
+
const deliveredConversation = markTakeoverBootstrapped({
|
|
770
1344
|
conversation: nextConversation,
|
|
1345
|
+
statePath,
|
|
1346
|
+
logPath,
|
|
1347
|
+
executor,
|
|
1348
|
+
native: needsNativeTakeoverBootstrap,
|
|
1349
|
+
fork: needsForkTakeoverBootstrap
|
|
1350
|
+
});
|
|
1351
|
+
printJson({
|
|
1352
|
+
conversation: deliveredConversation,
|
|
771
1353
|
message,
|
|
772
1354
|
delivered: true,
|
|
773
1355
|
background: true,
|
|
@@ -835,13 +1417,303 @@ function runSend(options) {
|
|
|
835
1417
|
}
|
|
836
1418
|
throw new Error(cleanProcessText(sendResult.stderr || sendResult.stdout || `acpx ${executor.kind} send exited with status ${sendResult.status}`));
|
|
837
1419
|
}
|
|
1420
|
+
const deliveredConversation = markTakeoverBootstrapped({
|
|
1421
|
+
conversation: nextConversation,
|
|
1422
|
+
statePath,
|
|
1423
|
+
logPath,
|
|
1424
|
+
executor,
|
|
1425
|
+
native: needsNativeTakeoverBootstrap,
|
|
1426
|
+
fork: needsForkTakeoverBootstrap
|
|
1427
|
+
});
|
|
838
1428
|
printJson({
|
|
1429
|
+
conversation: deliveredConversation,
|
|
1430
|
+
message,
|
|
1431
|
+
delivered: true,
|
|
1432
|
+
executor,
|
|
1433
|
+
budget: budgetAction(deliveredConversation)
|
|
1434
|
+
});
|
|
1435
|
+
}
|
|
1436
|
+
function runNativeCodexResumeSend({ options, conversation, nextConversation, statePath, logPath, executor, executorEnv, message, payload, nativeTakeover, needsNativeTakeoverBootstrap }) {
|
|
1437
|
+
const codexPath = resolveExecutable("codex");
|
|
1438
|
+
const nativeSessionId = String(nativeTakeover["native_session_id"]);
|
|
1439
|
+
const nativeModel = nativeCodexModelForSend({ options, conversation, nativeTakeover });
|
|
1440
|
+
const codexArgs = buildCodexExecResumeArgs({
|
|
1441
|
+
nativeSessionId,
|
|
1442
|
+
payload,
|
|
1443
|
+
model: nativeModel
|
|
1444
|
+
});
|
|
1445
|
+
appendEvent(logPath, {
|
|
1446
|
+
ts: new Date().toISOString(),
|
|
1447
|
+
conversation_id: conversation.conversation_id,
|
|
1448
|
+
event: "native_executor_resume_prepare",
|
|
1449
|
+
executor,
|
|
1450
|
+
native_session_id: nativeSessionId,
|
|
1451
|
+
model: nativeModel ?? null
|
|
1452
|
+
});
|
|
1453
|
+
runtimeLog("info", "native_executor_resume_prepare", {
|
|
1454
|
+
conversation_id: conversation.conversation_id,
|
|
1455
|
+
agent: executor.kind,
|
|
1456
|
+
executor_session: executor.session,
|
|
1457
|
+
native_session_id: nativeSessionId,
|
|
1458
|
+
model: nativeModel
|
|
1459
|
+
});
|
|
1460
|
+
if (options.background) {
|
|
1461
|
+
const outputPath = path.join(path.dirname(logPath), `${executor.kind}-native-resume-output.log`);
|
|
1462
|
+
const outputFd = fs.openSync(outputPath, "a");
|
|
1463
|
+
const child = spawn(codexPath, codexArgs, {
|
|
1464
|
+
detached: true,
|
|
1465
|
+
stdio: ["ignore", outputFd, outputFd],
|
|
1466
|
+
cwd: conversation.workspace ?? process.cwd(),
|
|
1467
|
+
env: executorEnv
|
|
1468
|
+
});
|
|
1469
|
+
child.unref();
|
|
1470
|
+
fs.closeSync(outputFd);
|
|
1471
|
+
appendEvent(logPath, {
|
|
1472
|
+
ts: new Date().toISOString(),
|
|
1473
|
+
conversation_id: conversation.conversation_id,
|
|
1474
|
+
event: "native_executor_resume_launch",
|
|
1475
|
+
mode: "background",
|
|
1476
|
+
pid: child.pid ?? null,
|
|
1477
|
+
executor,
|
|
1478
|
+
native_session_id: nativeSessionId,
|
|
1479
|
+
output_path: outputPath
|
|
1480
|
+
});
|
|
1481
|
+
runtimeLog("info", "native_executor_resume_launch", {
|
|
1482
|
+
conversation_id: conversation.conversation_id,
|
|
1483
|
+
agent: executor.kind,
|
|
1484
|
+
executor_session: executor.session,
|
|
1485
|
+
native_session_id: nativeSessionId,
|
|
1486
|
+
mode: "background",
|
|
1487
|
+
pid: child.pid ?? null,
|
|
1488
|
+
output_path: outputPath
|
|
1489
|
+
});
|
|
1490
|
+
const monitor = startExecutorMonitor({
|
|
1491
|
+
statePath,
|
|
1492
|
+
logPath,
|
|
1493
|
+
pid: child.pid,
|
|
1494
|
+
outputPath,
|
|
1495
|
+
agentTimeoutMinutes: Number(options.agentTimeoutMinutes ?? DEFAULT_AGENT_TIMEOUT_MINUTES),
|
|
1496
|
+
pollIntervalMs: Number(options.monitorPollIntervalMs ?? DEFAULT_MONITOR_POLL_INTERVAL_MS)
|
|
1497
|
+
});
|
|
1498
|
+
appendEvent(logPath, {
|
|
1499
|
+
ts: new Date().toISOString(),
|
|
1500
|
+
conversation_id: conversation.conversation_id,
|
|
1501
|
+
event: "executor_monitor_launch",
|
|
1502
|
+
pid: monitor.pid ?? null,
|
|
1503
|
+
executor_pid: child.pid ?? null,
|
|
1504
|
+
agent_timeout_minutes: Number(options.agentTimeoutMinutes ?? DEFAULT_AGENT_TIMEOUT_MINUTES)
|
|
1505
|
+
});
|
|
1506
|
+
const deliveredConversation = markTakeoverBootstrapped({
|
|
1507
|
+
conversation: nextConversation,
|
|
1508
|
+
statePath,
|
|
1509
|
+
logPath,
|
|
1510
|
+
executor,
|
|
1511
|
+
native: needsNativeTakeoverBootstrap,
|
|
1512
|
+
fork: false
|
|
1513
|
+
});
|
|
1514
|
+
printJson({
|
|
1515
|
+
conversation: deliveredConversation,
|
|
1516
|
+
message,
|
|
1517
|
+
delivered: true,
|
|
1518
|
+
background: true,
|
|
1519
|
+
native_resume: true,
|
|
1520
|
+
pid: child.pid ?? null,
|
|
1521
|
+
monitor_pid: monitor.pid ?? null,
|
|
1522
|
+
output_path: outputPath,
|
|
1523
|
+
executor,
|
|
1524
|
+
budget: budgetAction(deliveredConversation)
|
|
1525
|
+
});
|
|
1526
|
+
return;
|
|
1527
|
+
}
|
|
1528
|
+
const sendResult = spawnSync(codexPath, codexArgs, {
|
|
1529
|
+
encoding: "utf8",
|
|
1530
|
+
maxBuffer: 1024 * 1024 * 10,
|
|
1531
|
+
cwd: conversation.workspace ?? process.cwd(),
|
|
1532
|
+
env: executorEnv
|
|
1533
|
+
});
|
|
1534
|
+
appendEvent(logPath, {
|
|
1535
|
+
ts: new Date().toISOString(),
|
|
1536
|
+
conversation_id: conversation.conversation_id,
|
|
1537
|
+
event: "native_executor_resume_send",
|
|
1538
|
+
status: sendResult.status ?? null,
|
|
1539
|
+
executor,
|
|
1540
|
+
native_session_id: nativeSessionId,
|
|
1541
|
+
stdout: cleanProcessText(sendResult.stdout),
|
|
1542
|
+
stderr: cleanProcessText(sendResult.stderr)
|
|
1543
|
+
});
|
|
1544
|
+
runtimeLog("info", "native_executor_resume_send", {
|
|
1545
|
+
conversation_id: conversation.conversation_id,
|
|
1546
|
+
agent: executor.kind,
|
|
1547
|
+
executor_session: executor.session,
|
|
1548
|
+
native_session_id: nativeSessionId,
|
|
1549
|
+
status: sendResult.status ?? null,
|
|
1550
|
+
failure_kind: classifyProcessFailure(sendResult),
|
|
1551
|
+
stdout: textSummary(cleanProcessText(sendResult.stdout)),
|
|
1552
|
+
stderr: textSummary(cleanProcessText(sendResult.stderr))
|
|
1553
|
+
});
|
|
1554
|
+
if (sendResult.error) {
|
|
1555
|
+
throw new Error(`codex exec resume failed to start: ${sendResult.error.message}`);
|
|
1556
|
+
}
|
|
1557
|
+
if (sendResult.status !== 0) {
|
|
1558
|
+
throw new Error(cleanProcessText(sendResult.stderr || sendResult.stdout || `codex exec resume exited with status ${sendResult.status}`));
|
|
1559
|
+
}
|
|
1560
|
+
const deliveredConversation = markTakeoverBootstrapped({
|
|
839
1561
|
conversation: nextConversation,
|
|
1562
|
+
statePath,
|
|
1563
|
+
logPath,
|
|
1564
|
+
executor,
|
|
1565
|
+
native: needsNativeTakeoverBootstrap,
|
|
1566
|
+
fork: false
|
|
1567
|
+
});
|
|
1568
|
+
printJson({
|
|
1569
|
+
conversation: deliveredConversation,
|
|
840
1570
|
message,
|
|
841
1571
|
delivered: true,
|
|
1572
|
+
native_resume: true,
|
|
842
1573
|
executor,
|
|
843
|
-
budget: budgetAction(
|
|
1574
|
+
budget: budgetAction(deliveredConversation)
|
|
1575
|
+
});
|
|
1576
|
+
}
|
|
1577
|
+
function buildCodexExecResumeArgs({ nativeSessionId, payload, model }) {
|
|
1578
|
+
const args = ["exec", "resume"];
|
|
1579
|
+
if (model) {
|
|
1580
|
+
args.push("--model", model);
|
|
1581
|
+
}
|
|
1582
|
+
args.push("--skip-git-repo-check", nativeSessionId, payload);
|
|
1583
|
+
return args;
|
|
1584
|
+
}
|
|
1585
|
+
function nativeCodexModelForSend({ options, conversation, nativeTakeover }) {
|
|
1586
|
+
const explicit = options.model ?? options.codexModel;
|
|
1587
|
+
if (explicit) {
|
|
1588
|
+
return normalizeNativeCodexModel(explicit);
|
|
1589
|
+
}
|
|
1590
|
+
const nativeModel = isRecord(nativeTakeover) ? nativeTakeover["native_model"] : undefined;
|
|
1591
|
+
if (typeof nativeModel === "string" && nativeModel.trim()) {
|
|
1592
|
+
return normalizeNativeCodexModel(nativeModel);
|
|
1593
|
+
}
|
|
1594
|
+
return normalizeNativeCodexModel(conversation.executor_model);
|
|
1595
|
+
}
|
|
1596
|
+
function normalizeNativeCodexModel(model) {
|
|
1597
|
+
const value = typeof model === "string" ? model.trim() : "";
|
|
1598
|
+
if (!value) {
|
|
1599
|
+
return undefined;
|
|
1600
|
+
}
|
|
1601
|
+
return value.replace(/\[[^\]]+\]$/u, "").replace(/\/(?:low|medium|high|xhigh)$/u, "");
|
|
1602
|
+
}
|
|
1603
|
+
function buildAgentSendPayload({ conversation, executor, message, includeNativeTakeoverBootstrap, includeForkTakeoverBootstrap, forkTakeover }) {
|
|
1604
|
+
const messageJson = JSON.stringify(message);
|
|
1605
|
+
if (!includeNativeTakeoverBootstrap && !includeForkTakeoverBootstrap) {
|
|
1606
|
+
return [
|
|
1607
|
+
"Continue the existing Agent Knock Knock delegation using this structured OpenClaw message.",
|
|
1608
|
+
"If this message answers a question or blocker, follow it as the product decision.",
|
|
1609
|
+
"Continue to report back only through the callback command already provided for this conversation.",
|
|
1610
|
+
"",
|
|
1611
|
+
messageJson
|
|
1612
|
+
].join("\n");
|
|
1613
|
+
}
|
|
1614
|
+
if (includeForkTakeoverBootstrap) {
|
|
1615
|
+
const summary = forkTakeoverSummaryText(forkTakeover);
|
|
1616
|
+
return [
|
|
1617
|
+
executorBootstrapPrompt({
|
|
1618
|
+
callbackCommand: conversation.callback_command,
|
|
1619
|
+
executorName: executor.display_name,
|
|
1620
|
+
softLimit: Number(conversation.soft_limit ?? 50),
|
|
1621
|
+
hardLimit: Number(conversation.hard_limit ?? 100)
|
|
1622
|
+
}),
|
|
1623
|
+
"",
|
|
1624
|
+
"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.",
|
|
1625
|
+
"",
|
|
1626
|
+
"Approved source-session summary:",
|
|
1627
|
+
summary || "(No approved summary was provided.)",
|
|
1628
|
+
"",
|
|
1629
|
+
"Initial AKK fork message:",
|
|
1630
|
+
messageJson
|
|
1631
|
+
].join("\n");
|
|
1632
|
+
}
|
|
1633
|
+
return [
|
|
1634
|
+
executorBootstrapPrompt({
|
|
1635
|
+
callbackCommand: conversation.callback_command,
|
|
1636
|
+
executorName: executor.display_name,
|
|
1637
|
+
softLimit: Number(conversation.soft_limit ?? 50),
|
|
1638
|
+
hardLimit: Number(conversation.hard_limit ?? 100)
|
|
1639
|
+
}),
|
|
1640
|
+
"",
|
|
1641
|
+
"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.",
|
|
1642
|
+
"",
|
|
1643
|
+
"Initial AKK takeover message:",
|
|
1644
|
+
messageJson
|
|
1645
|
+
].join("\n");
|
|
1646
|
+
}
|
|
1647
|
+
function forkTakeoverSummaryText(forkTakeover) {
|
|
1648
|
+
return String(isRecord(forkTakeover) ? forkTakeover.summary ?? "" : "").trim();
|
|
1649
|
+
}
|
|
1650
|
+
function markTakeoverBootstrapped({ conversation, statePath, logPath, executor, native, fork }) {
|
|
1651
|
+
let nextConversation = conversation;
|
|
1652
|
+
if (native) {
|
|
1653
|
+
nextConversation = markNativeSessionBootstrapped({ conversation: nextConversation, statePath, logPath, executor });
|
|
1654
|
+
}
|
|
1655
|
+
if (fork) {
|
|
1656
|
+
nextConversation = markForkSessionBootstrapped({ conversation: nextConversation, statePath, logPath, executor });
|
|
1657
|
+
}
|
|
1658
|
+
return nextConversation;
|
|
1659
|
+
}
|
|
1660
|
+
function markNativeSessionBootstrapped({ conversation, statePath, logPath, executor }) {
|
|
1661
|
+
const nativeTakeover = isRecord(conversation.native_session_takeover)
|
|
1662
|
+
? conversation.native_session_takeover
|
|
1663
|
+
: {};
|
|
1664
|
+
const now = new Date().toISOString();
|
|
1665
|
+
const nextConversation = {
|
|
1666
|
+
...conversation,
|
|
1667
|
+
native_session_takeover: {
|
|
1668
|
+
...nativeTakeover,
|
|
1669
|
+
needs_bootstrap: false,
|
|
1670
|
+
bootstrapped_at: now
|
|
1671
|
+
},
|
|
1672
|
+
updated_at: now
|
|
1673
|
+
};
|
|
1674
|
+
saveState(statePath, nextConversation);
|
|
1675
|
+
appendEvent(logPath, {
|
|
1676
|
+
ts: now,
|
|
1677
|
+
conversation_id: conversation.conversation_id,
|
|
1678
|
+
event: "native_session_bootstrapped",
|
|
1679
|
+
executor
|
|
1680
|
+
});
|
|
1681
|
+
runtimeLog("info", "native_session_bootstrapped", {
|
|
1682
|
+
conversation_id: conversation.conversation_id,
|
|
1683
|
+
agent: executor.kind,
|
|
1684
|
+
executor_session: executor.session,
|
|
1685
|
+
state_path: statePath
|
|
1686
|
+
});
|
|
1687
|
+
return nextConversation;
|
|
1688
|
+
}
|
|
1689
|
+
function markForkSessionBootstrapped({ conversation, statePath, logPath, executor }) {
|
|
1690
|
+
const forkTakeover = isRecord(conversation.fork_context_takeover)
|
|
1691
|
+
? conversation.fork_context_takeover
|
|
1692
|
+
: {};
|
|
1693
|
+
const now = new Date().toISOString();
|
|
1694
|
+
const nextConversation = {
|
|
1695
|
+
...conversation,
|
|
1696
|
+
fork_context_takeover: {
|
|
1697
|
+
...forkTakeover,
|
|
1698
|
+
needs_bootstrap: false,
|
|
1699
|
+
bootstrapped_at: now
|
|
1700
|
+
},
|
|
1701
|
+
updated_at: now
|
|
1702
|
+
};
|
|
1703
|
+
saveState(statePath, nextConversation);
|
|
1704
|
+
appendEvent(logPath, {
|
|
1705
|
+
ts: now,
|
|
1706
|
+
conversation_id: conversation.conversation_id,
|
|
1707
|
+
event: "fork_session_bootstrapped",
|
|
1708
|
+
executor
|
|
1709
|
+
});
|
|
1710
|
+
runtimeLog("info", "fork_session_bootstrapped", {
|
|
1711
|
+
conversation_id: conversation.conversation_id,
|
|
1712
|
+
agent: executor.kind,
|
|
1713
|
+
executor_session: executor.session,
|
|
1714
|
+
state_path: statePath
|
|
844
1715
|
});
|
|
1716
|
+
return nextConversation;
|
|
845
1717
|
}
|
|
846
1718
|
function requiresExplicitRecoveryDecision(executor, options = {}) {
|
|
847
1719
|
if (options.recoveryPolicy === "explicit" || options.recoveryPolicy === "explicit-decision") {
|
|
@@ -1216,6 +2088,27 @@ function runMonitor(options) {
|
|
|
1216
2088
|
return;
|
|
1217
2089
|
}
|
|
1218
2090
|
if (Number.isFinite(pid) && !isProcessAlive(pid)) {
|
|
2091
|
+
const modelSelection = detectModelSelectionError(readOutputTail(options.outputPath));
|
|
2092
|
+
if (modelSelection) {
|
|
2093
|
+
const modelSelectionConversation = markConversationNeedsModelSelection({
|
|
2094
|
+
statePath,
|
|
2095
|
+
logPath,
|
|
2096
|
+
reason: modelSelection.message,
|
|
2097
|
+
detail: {
|
|
2098
|
+
executor_pid: pid,
|
|
2099
|
+
output_path: options.outputPath,
|
|
2100
|
+
model_selection: modelSelection
|
|
2101
|
+
}
|
|
2102
|
+
});
|
|
2103
|
+
printJson({
|
|
2104
|
+
conversation: modelSelectionConversation,
|
|
2105
|
+
monitored: true,
|
|
2106
|
+
stalled: false,
|
|
2107
|
+
needs_model_selection: true,
|
|
2108
|
+
reason: modelSelectionConversation?.model_selection?.message ?? modelSelection.message
|
|
2109
|
+
});
|
|
2110
|
+
return;
|
|
2111
|
+
}
|
|
1219
2112
|
const stalledConversation = markConversationStalled({
|
|
1220
2113
|
statePath,
|
|
1221
2114
|
logPath,
|
|
@@ -1884,12 +2777,65 @@ function isWaitingForAgent(status) {
|
|
|
1884
2777
|
function isProcessAlive(pid) {
|
|
1885
2778
|
try {
|
|
1886
2779
|
process.kill(pid, 0);
|
|
1887
|
-
return
|
|
2780
|
+
return !isZombieProcess(pid);
|
|
1888
2781
|
}
|
|
1889
2782
|
catch (error) {
|
|
1890
2783
|
return error?.code === "EPERM";
|
|
1891
2784
|
}
|
|
1892
2785
|
}
|
|
2786
|
+
function isZombieProcess(pid) {
|
|
2787
|
+
const result = spawnSync("ps", ["-o", "stat=", "-p", String(pid)], {
|
|
2788
|
+
encoding: "utf8"
|
|
2789
|
+
});
|
|
2790
|
+
if (result.status !== 0) {
|
|
2791
|
+
return false;
|
|
2792
|
+
}
|
|
2793
|
+
return result.stdout.trim().toUpperCase().startsWith("Z");
|
|
2794
|
+
}
|
|
2795
|
+
function terminateProcessTarget(target, { timeoutMs = 3000 } = {}) {
|
|
2796
|
+
const pids = [...target.childPids, target.pid]
|
|
2797
|
+
.filter((pid, index, all) => Number.isInteger(pid) && pid > 0 && all.indexOf(pid) === index);
|
|
2798
|
+
const signals = [];
|
|
2799
|
+
for (const pid of pids) {
|
|
2800
|
+
signals.push(sendSignalToPid(pid, "SIGTERM"));
|
|
2801
|
+
}
|
|
2802
|
+
const exited = waitForPidsToExit(pids, timeoutMs);
|
|
2803
|
+
return {
|
|
2804
|
+
target,
|
|
2805
|
+
signal: "SIGTERM",
|
|
2806
|
+
signals,
|
|
2807
|
+
exited,
|
|
2808
|
+
remainingPids: pids.filter((pid) => isProcessAlive(pid))
|
|
2809
|
+
};
|
|
2810
|
+
}
|
|
2811
|
+
function sendSignalToPid(pid, signal) {
|
|
2812
|
+
try {
|
|
2813
|
+
process.kill(pid, signal);
|
|
2814
|
+
return {
|
|
2815
|
+
pid,
|
|
2816
|
+
signal,
|
|
2817
|
+
status: "sent"
|
|
2818
|
+
};
|
|
2819
|
+
}
|
|
2820
|
+
catch (error) {
|
|
2821
|
+
return {
|
|
2822
|
+
pid,
|
|
2823
|
+
signal,
|
|
2824
|
+
status: "failed",
|
|
2825
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2826
|
+
};
|
|
2827
|
+
}
|
|
2828
|
+
}
|
|
2829
|
+
function waitForPidsToExit(pids, timeoutMs) {
|
|
2830
|
+
const deadline = Date.now() + timeoutMs;
|
|
2831
|
+
while (Date.now() < deadline) {
|
|
2832
|
+
if (pids.every((pid) => !isProcessAlive(pid))) {
|
|
2833
|
+
return true;
|
|
2834
|
+
}
|
|
2835
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 50);
|
|
2836
|
+
}
|
|
2837
|
+
return pids.every((pid) => !isProcessAlive(pid));
|
|
2838
|
+
}
|
|
1893
2839
|
function markConversationStalled({ statePath, logPath, reason, detail = {} }) {
|
|
1894
2840
|
const releaseLock = acquireFileLock(`${statePath}.lock`);
|
|
1895
2841
|
let stalledConversation;
|
|
@@ -1938,16 +2884,70 @@ function markConversationStalled({ statePath, logPath, reason, detail = {} }) {
|
|
|
1938
2884
|
statePath,
|
|
1939
2885
|
logPath,
|
|
1940
2886
|
conversation: stalledConversation,
|
|
1941
|
-
reason
|
|
2887
|
+
reason,
|
|
2888
|
+
detail
|
|
1942
2889
|
});
|
|
1943
2890
|
}
|
|
1944
2891
|
return stalledConversation;
|
|
1945
2892
|
}
|
|
1946
|
-
function
|
|
2893
|
+
function markConversationNeedsModelSelection({ statePath, logPath, reason, detail = {} }) {
|
|
2894
|
+
const releaseLock = acquireFileLock(`${statePath}.lock`);
|
|
2895
|
+
let modelSelectionConversation;
|
|
2896
|
+
try {
|
|
2897
|
+
const conversation = loadState(statePath);
|
|
2898
|
+
if (!isWaitingForAgent(conversation.status)) {
|
|
2899
|
+
runtimeLog("info", "executor_monitor_finished", {
|
|
2900
|
+
conversation_id: conversation.conversation_id,
|
|
2901
|
+
status: conversation.status,
|
|
2902
|
+
reason: "conversation_changed_before_model_selection"
|
|
2903
|
+
});
|
|
2904
|
+
return conversation;
|
|
2905
|
+
}
|
|
2906
|
+
const now = new Date().toISOString();
|
|
2907
|
+
const detailRecord = detail;
|
|
2908
|
+
const modelSelection = isRecord(detailRecord.model_selection)
|
|
2909
|
+
? detailRecord.model_selection
|
|
2910
|
+
: {};
|
|
2911
|
+
modelSelectionConversation = {
|
|
2912
|
+
...conversation,
|
|
2913
|
+
status: "needs_model_selection",
|
|
2914
|
+
model_selection: {
|
|
2915
|
+
detected_at: now,
|
|
2916
|
+
message: reason,
|
|
2917
|
+
...modelSelection
|
|
2918
|
+
},
|
|
2919
|
+
updated_at: now
|
|
2920
|
+
};
|
|
2921
|
+
saveState(statePath, modelSelectionConversation);
|
|
2922
|
+
appendEvent(logPath, {
|
|
2923
|
+
ts: now,
|
|
2924
|
+
conversation_id: conversation.conversation_id,
|
|
2925
|
+
event: "conversation_needs_model_selection",
|
|
2926
|
+
status: "needs_model_selection",
|
|
2927
|
+
reason,
|
|
2928
|
+
...detailRecord
|
|
2929
|
+
});
|
|
2930
|
+
runtimeLog("warn", "conversation_needs_model_selection", {
|
|
2931
|
+
conversation_id: conversation.conversation_id,
|
|
2932
|
+
agent: executorForConversation(conversation).kind,
|
|
2933
|
+
executor_session: executorForConversation(conversation).session,
|
|
2934
|
+
state_path: statePath,
|
|
2935
|
+
event_log_path: logPath,
|
|
2936
|
+
reason,
|
|
2937
|
+
...detailRecord
|
|
2938
|
+
});
|
|
2939
|
+
}
|
|
2940
|
+
finally {
|
|
2941
|
+
releaseLock();
|
|
2942
|
+
}
|
|
2943
|
+
return modelSelectionConversation;
|
|
2944
|
+
}
|
|
2945
|
+
function deliverStalledNotification({ statePath, logPath, conversation, reason, detail = {} }) {
|
|
1947
2946
|
if (!conversation.gateway_method) {
|
|
1948
2947
|
return;
|
|
1949
2948
|
}
|
|
1950
2949
|
const executor = executorForConversation(conversation);
|
|
2950
|
+
const trace = buildStalledTraceSummary({ conversation, logPath, detail });
|
|
1951
2951
|
const message = createMessage({
|
|
1952
2952
|
conversation,
|
|
1953
2953
|
from: executor.actor,
|
|
@@ -1959,14 +2959,17 @@ function deliverStalledNotification({ statePath, logPath, conversation, reason }
|
|
|
1959
2959
|
"",
|
|
1960
2960
|
`Conversation: ${conversation.conversation_id}`,
|
|
1961
2961
|
`Session: ${executor.session}`,
|
|
2962
|
+
trace ? `Trace: ${trace}` : "",
|
|
1962
2963
|
"Use `AKK status` for details, `AKK send` to retry/follow up, or `AKK close` to close it."
|
|
1963
|
-
].join("\n")
|
|
2964
|
+
].filter(Boolean).join("\n")
|
|
1964
2965
|
});
|
|
2966
|
+
const gatewayToken = conversation.gateway_token;
|
|
2967
|
+
const gatewayUrl = gatewayToken ? conversation.gateway_url : undefined;
|
|
1965
2968
|
const delivery = deliverToGatewayMethod({
|
|
1966
2969
|
method: conversation.gateway_method,
|
|
1967
2970
|
openclawBin: conversation.openclaw_bin,
|
|
1968
|
-
gatewayUrl
|
|
1969
|
-
token:
|
|
2971
|
+
gatewayUrl,
|
|
2972
|
+
token: gatewayToken,
|
|
1970
2973
|
sessionKey: conversation.gateway_session ?? conversation.openclaw_session,
|
|
1971
2974
|
statePath,
|
|
1972
2975
|
logPath,
|
|
@@ -2000,8 +3003,8 @@ function deliverStalledNotification({ statePath, logPath, conversation, reason }
|
|
|
2000
3003
|
}
|
|
2001
3004
|
const chatSendDelivery = deliverToChatSend({
|
|
2002
3005
|
openclawBin: conversation.openclaw_bin,
|
|
2003
|
-
gatewayUrl
|
|
2004
|
-
token:
|
|
3006
|
+
gatewayUrl,
|
|
3007
|
+
token: gatewayToken,
|
|
2005
3008
|
params: chatSendParams
|
|
2006
3009
|
});
|
|
2007
3010
|
appendEvent(logPath, {
|
|
@@ -2020,6 +3023,42 @@ function deliverStalledNotification({ statePath, logPath, conversation, reason }
|
|
|
2020
3023
|
stderr: textSummary(chatSendDelivery.stderr)
|
|
2021
3024
|
});
|
|
2022
3025
|
}
|
|
3026
|
+
function buildStalledTraceSummary({ conversation, logPath, detail = {} }) {
|
|
3027
|
+
const detailRecord = isRecord(detail) ? detail : {};
|
|
3028
|
+
const outputPath = typeof detailRecord.output_path === "string"
|
|
3029
|
+
? detailRecord.output_path
|
|
3030
|
+
: traceOutputPath({
|
|
3031
|
+
conversation,
|
|
3032
|
+
events: safeReadEvents(logPath),
|
|
3033
|
+
logPath
|
|
3034
|
+
});
|
|
3035
|
+
if (!outputPath || !fs.existsSync(outputPath)) {
|
|
3036
|
+
return undefined;
|
|
3037
|
+
}
|
|
3038
|
+
const output = fs.readFileSync(outputPath, "utf8").slice(-64 * 1024);
|
|
3039
|
+
const parts = output
|
|
3040
|
+
.split(/\r?\n/)
|
|
3041
|
+
.map((line) => sanitizeTraceText(line.trim(), 220))
|
|
3042
|
+
.filter((line) => line &&
|
|
3043
|
+
!line.startsWith("input:") &&
|
|
3044
|
+
!line.startsWith("output:") &&
|
|
3045
|
+
!line.startsWith("{") &&
|
|
3046
|
+
!line.startsWith("}") &&
|
|
3047
|
+
!line.includes("--message-json"))
|
|
3048
|
+
.slice(-6);
|
|
3049
|
+
if (parts.length === 0) {
|
|
3050
|
+
return undefined;
|
|
3051
|
+
}
|
|
3052
|
+
return cleanProcessText(parts.join(" | "))?.slice(0, 500);
|
|
3053
|
+
}
|
|
3054
|
+
function safeReadEvents(logPath) {
|
|
3055
|
+
try {
|
|
3056
|
+
return readNdjsonLog(logPath);
|
|
3057
|
+
}
|
|
3058
|
+
catch {
|
|
3059
|
+
return [];
|
|
3060
|
+
}
|
|
3061
|
+
}
|
|
2023
3062
|
function cleanupIdleConversations(storeDir, options = {}, now = new Date()) {
|
|
2024
3063
|
const timeoutMinutes = Number(options.idleTimeoutMinutes ?? DEFAULT_IDLE_TIMEOUT_MINUTES);
|
|
2025
3064
|
if (!Number.isFinite(timeoutMinutes) || timeoutMinutes <= 0) {
|
|
@@ -2266,6 +3305,32 @@ function parseOptionalJson(text) {
|
|
|
2266
3305
|
return undefined;
|
|
2267
3306
|
}
|
|
2268
3307
|
}
|
|
3308
|
+
function createAgentSessionProvider(agent, options) {
|
|
3309
|
+
if (agent !== "codex") {
|
|
3310
|
+
throw new Error(`unsupported agent session provider: ${agent}`);
|
|
3311
|
+
}
|
|
3312
|
+
if (options.threadsJson || options.processesJson || options.rolloutsJson) {
|
|
3313
|
+
return new CodexLocalSessionProvider(new InlineCodexSessionAdapter({
|
|
3314
|
+
threads: parseJsonOption(options.threadsJson, "--threads-json"),
|
|
3315
|
+
processes: parseJsonOption(options.processesJson, "--processes-json"),
|
|
3316
|
+
rollouts: parseJsonOption(options.rolloutsJson, "--rollouts-json")
|
|
3317
|
+
}));
|
|
3318
|
+
}
|
|
3319
|
+
return new CodexLocalSessionProvider(new CodexStoreAdapter({
|
|
3320
|
+
codexHome: expandHome(options.codexHome)
|
|
3321
|
+
}));
|
|
3322
|
+
}
|
|
3323
|
+
function parseJsonOption(value, optionName) {
|
|
3324
|
+
if (!value) {
|
|
3325
|
+
return undefined;
|
|
3326
|
+
}
|
|
3327
|
+
try {
|
|
3328
|
+
return JSON.parse(String(value));
|
|
3329
|
+
}
|
|
3330
|
+
catch (error) {
|
|
3331
|
+
throw new Error(`${optionName} must be valid JSON: ${error.message}`);
|
|
3332
|
+
}
|
|
3333
|
+
}
|
|
2269
3334
|
function expandHome(filePath) {
|
|
2270
3335
|
if (filePath === "~") {
|
|
2271
3336
|
return process.env.HOME;
|
|
@@ -2316,6 +3381,56 @@ function classifyProcessFailure(result) {
|
|
|
2316
3381
|
}
|
|
2317
3382
|
return undefined;
|
|
2318
3383
|
}
|
|
3384
|
+
function readOutputTail(outputPath, maxBytes = 65536) {
|
|
3385
|
+
if (!outputPath) {
|
|
3386
|
+
return "";
|
|
3387
|
+
}
|
|
3388
|
+
try {
|
|
3389
|
+
const resolvedPath = expandHome(outputPath);
|
|
3390
|
+
const stat = fs.statSync(resolvedPath);
|
|
3391
|
+
const start = Math.max(0, stat.size - maxBytes);
|
|
3392
|
+
const length = stat.size - start;
|
|
3393
|
+
const fd = fs.openSync(resolvedPath, "r");
|
|
3394
|
+
try {
|
|
3395
|
+
const buffer = Buffer.alloc(length);
|
|
3396
|
+
fs.readSync(fd, buffer, 0, length, start);
|
|
3397
|
+
return buffer.toString("utf8");
|
|
3398
|
+
}
|
|
3399
|
+
finally {
|
|
3400
|
+
fs.closeSync(fd);
|
|
3401
|
+
}
|
|
3402
|
+
}
|
|
3403
|
+
catch {
|
|
3404
|
+
return "";
|
|
3405
|
+
}
|
|
3406
|
+
}
|
|
3407
|
+
function detectModelSelectionError(text) {
|
|
3408
|
+
const cleaned = cleanProcessText(text);
|
|
3409
|
+
if (!cleaned) {
|
|
3410
|
+
return undefined;
|
|
3411
|
+
}
|
|
3412
|
+
const unsupportedAccount = /The '([^']+)' model is not supported when using Codex with a ChatGPT account/i.exec(cleaned);
|
|
3413
|
+
if (unsupportedAccount) {
|
|
3414
|
+
return {
|
|
3415
|
+
kind: "unsupported_chatgpt_account_model",
|
|
3416
|
+
attempted_model: unsupportedAccount[1],
|
|
3417
|
+
message: unsupportedAccount[0]
|
|
3418
|
+
};
|
|
3419
|
+
}
|
|
3420
|
+
const unadvertised = /Cannot apply --model "([^"]+)": the ACP agent did not advertise that model\. Available models:\s*([^\n\r]+)/i.exec(cleaned);
|
|
3421
|
+
if (unadvertised) {
|
|
3422
|
+
return {
|
|
3423
|
+
kind: "unadvertised_acpx_model",
|
|
3424
|
+
attempted_model: unadvertised[1],
|
|
3425
|
+
available_models: unadvertised[2]
|
|
3426
|
+
.split(",")
|
|
3427
|
+
.map((model) => model.trim())
|
|
3428
|
+
.filter(Boolean),
|
|
3429
|
+
message: unadvertised[0]
|
|
3430
|
+
};
|
|
3431
|
+
}
|
|
3432
|
+
return undefined;
|
|
3433
|
+
}
|
|
2319
3434
|
function runtimeLog(level, event, fields = {}) {
|
|
2320
3435
|
try {
|
|
2321
3436
|
writeRuntimeLog({
|
|
@@ -2356,6 +3471,8 @@ function usage() {
|
|
|
2356
3471
|
agent-knock-knock close --conversation <id> [--reason <text>]
|
|
2357
3472
|
agent-knock-knock install-openclaw [--openclaw-bin <path>] [--skill-path <path>] [--no-restart]
|
|
2358
3473
|
agent-knock-knock doctor
|
|
3474
|
+
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]
|
|
2359
3476
|
agent-knock-knock callback --state <file> --message-json <json> [--record-only]
|
|
2360
3477
|
agent-knock-knock transcript --log <file> [--include-raw]
|
|
2361
3478
|
agent-knock-knock transcript --conversation <dir> [--include-raw]
|