@scotthuang/agent-knock-knock 0.3.0-beta.1 → 0.3.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 +30 -0
- package/README.md +54 -89
- package/dist/src/cli.js +1153 -198
- package/dist/src/cli.js.map +1 -1
- package/dist/src/doctor-capabilities.d.ts +64 -13
- package/dist/src/doctor-capabilities.js +241 -12
- package/dist/src/doctor-capabilities.js.map +1 -1
- package/dist/src/openclaw-doctor.d.ts +28 -0
- package/dist/src/openclaw-doctor.js +359 -0
- package/dist/src/openclaw-doctor.js.map +1 -0
- package/dist/src/openclaw-plugin-helpers.d.ts +5 -2
- package/dist/src/openclaw-plugin-helpers.js +72 -24
- package/dist/src/openclaw-plugin-helpers.js.map +1 -1
- package/dist/src/openclaw-plugin.js +111 -3
- package/dist/src/openclaw-plugin.js.map +1 -1
- package/dist/src/session-selector.d.ts +81 -0
- package/dist/src/session-selector.js +330 -0
- package/dist/src/session-selector.js.map +1 -0
- package/dist/src/terminal-process-source.d.ts +4 -0
- package/dist/src/terminal-process-source.js +20 -0
- package/dist/src/terminal-process-source.js.map +1 -1
- package/docs/quickstart-managed-acpx.md +43 -0
- package/docs/quickstart-tmux.md +43 -0
- package/openclaw.plugin.json +5 -0
- package/package.json +8 -2
- package/scripts/smoke-acpx.js +139 -0
- package/scripts/smoke-tmux.js +140 -0
- package/templates/openclaw-skills/agent-knock-knock/SKILL.md +23 -20
package/dist/src/cli.js
CHANGED
|
@@ -14,7 +14,7 @@ import { defaultClaudeSettingsPath, loadTrustedClaudeTokenjuiceLaunchers } from
|
|
|
14
14
|
import { CodexLocalSessionProvider } from "./codex-local-session-provider.js";
|
|
15
15
|
import { CodexStoreAdapter } from "./codex-store-adapter.js";
|
|
16
16
|
import { applyMessageToConversation, budgetAction, createConversation, createMessage, executorForConversation, extractStructuredMessage, parseMessageJson, resolveExecutor } from "./protocol.js";
|
|
17
|
-
import { EXECUTOR_KINDS, acpxCommandForExecutor, executorDefinitionForKind, modelEnvForExecutor, normalizeModelForExecutor, proxyEnvForExecutor } from "./executors.js";
|
|
17
|
+
import { EXECUTOR_KINDS, acpxCommandForExecutor, executorDefinitionForKind, isExecutorKind, modelEnvForExecutor, normalizeModelForExecutor, proxyEnvForExecutor } from "./executors.js";
|
|
18
18
|
import { executorBootstrapPrompt } from "./bootstrap.js";
|
|
19
19
|
import { redactString, writeRuntimeLog } from "./runtime-log.js";
|
|
20
20
|
import { formatTranscript, readNdjsonLog } from "./transcript.js";
|
|
@@ -23,17 +23,30 @@ import { planFork, planTakeover } from "./session-takeover-planner.js";
|
|
|
23
23
|
import { StaticTerminalControlProvider, TmuxTerminalControlProvider, terminalPaneContainsProcess } from "./terminal-control-provider.js";
|
|
24
24
|
import { parseTerminalConversationId } from "./terminal-agent-adapter.js";
|
|
25
25
|
import { createProductionTerminalAgentRegistry } from "./terminal-agent-registry.js";
|
|
26
|
-
import { StaticTerminalProcessSource, SystemTerminalProcessSource } from "./terminal-process-source.js";
|
|
26
|
+
import { parseProcessElapsedSeconds, StaticTerminalProcessSource, SystemTerminalProcessSource } from "./terminal-process-source.js";
|
|
27
27
|
import { TerminalAgentBridge } from "./terminal-agent-bridge.js";
|
|
28
28
|
import { evaluateApprovalPolicy } from "./approval-policy.js";
|
|
29
|
-
import { evaluateDoctorCapabilities } from "./doctor-capabilities.js";
|
|
29
|
+
import { evaluateDoctorCapabilities, runDoctorCapabilityProbes } from "./doctor-capabilities.js";
|
|
30
|
+
import { runOpenClawChainDiagnostics } from "./openclaw-doctor.js";
|
|
31
|
+
import { resolveSessionSelector, sessionShortRef } from "./session-selector.js";
|
|
30
32
|
const DEFAULT_IDLE_TIMEOUT_MINUTES = 10080;
|
|
31
33
|
const DEFAULT_AGENT_TIMEOUT_MINUTES = 60;
|
|
32
34
|
const DEFAULT_AGENT_HARD_TIMEOUT_MINUTES = 720;
|
|
33
35
|
const DEFAULT_MONITOR_POLL_INTERVAL_MS = 5000;
|
|
34
36
|
const CLAUDE_SCREEN_APPROVAL_TTL_MS = 10 * 60 * 1000;
|
|
35
37
|
const CALLBACK_DELIVERY_TIMEOUT_MS = 30_000;
|
|
38
|
+
const CALLBACK_AGENT_WAIT_TIMEOUT_MS = 20_000;
|
|
39
|
+
const CALLBACK_AGENT_WAIT_CLI_TIMEOUT_MS = 25_000;
|
|
40
|
+
const CALLBACK_AGENT_WAIT_PROCESS_TIMEOUT_MS = 30_000;
|
|
36
41
|
const CALLBACK_RETRY_DELAYS_MS = [5000, 15000, 60000, 60000];
|
|
42
|
+
const TERMINAL_BRIDGE_SUPERSEDE_STATUSES = new Set([
|
|
43
|
+
"created",
|
|
44
|
+
"running",
|
|
45
|
+
"waiting_for_agent",
|
|
46
|
+
"waiting_for_openclaw",
|
|
47
|
+
"stalled",
|
|
48
|
+
"cancelling"
|
|
49
|
+
]);
|
|
37
50
|
const TERMINAL_BRIDGE_MONITOR_LOCK_VERSION = 1;
|
|
38
51
|
const MINIMUM_NODE_VERSION = "22.14.0";
|
|
39
52
|
const PRIVATE_LOCK_FILE_MODE = 0o600;
|
|
@@ -57,6 +70,18 @@ const CONVERSATION_STATUSES = new Set([
|
|
|
57
70
|
"cancelled",
|
|
58
71
|
"cancelling"
|
|
59
72
|
]);
|
|
73
|
+
const SESSION_SELECTOR_COMMANDS = new Set([
|
|
74
|
+
"status",
|
|
75
|
+
"describe",
|
|
76
|
+
"summary",
|
|
77
|
+
"send",
|
|
78
|
+
"approve",
|
|
79
|
+
"cancel",
|
|
80
|
+
"renew",
|
|
81
|
+
"retry-callback",
|
|
82
|
+
"recover",
|
|
83
|
+
"close"
|
|
84
|
+
]);
|
|
60
85
|
class InlineCodexSessionAdapter {
|
|
61
86
|
threads;
|
|
62
87
|
processes;
|
|
@@ -113,6 +138,7 @@ catch (error) {
|
|
|
113
138
|
process.exit(1);
|
|
114
139
|
}
|
|
115
140
|
async function runCommand(commandName, options) {
|
|
141
|
+
await resolveConversationSelectorOption(commandName, options);
|
|
116
142
|
if (commandName === "help" || commandName === "--help" || commandName === "-h") {
|
|
117
143
|
usage();
|
|
118
144
|
}
|
|
@@ -196,7 +222,24 @@ async function runCommand(commandName, options) {
|
|
|
196
222
|
function runInstallOpenClaw(options) {
|
|
197
223
|
const root = packageRootDir();
|
|
198
224
|
const skillOnly = options.skillOnly === true;
|
|
199
|
-
const
|
|
225
|
+
const workspace = options.workspace === undefined
|
|
226
|
+
? undefined
|
|
227
|
+
: canonicalWorkspace(options.workspace);
|
|
228
|
+
const defaultAgent = optionalExecutorKind(options.defaultAgent);
|
|
229
|
+
const selectedMode = options.mode === undefined
|
|
230
|
+
? undefined
|
|
231
|
+
: parseDoctorMode(options.mode);
|
|
232
|
+
if (selectedMode === "tmux" && defaultAgent === "cursor") {
|
|
233
|
+
throw new Error("--default-agent cursor requires --mode acpx or --mode all");
|
|
234
|
+
}
|
|
235
|
+
if (skillOnly &&
|
|
236
|
+
(workspace !== undefined ||
|
|
237
|
+
defaultAgent !== undefined ||
|
|
238
|
+
selectedMode !== undefined ||
|
|
239
|
+
options.verify === true)) {
|
|
240
|
+
throw new Error("--skill-only cannot be combined with --workspace, --default-agent, --mode, or --verify");
|
|
241
|
+
}
|
|
242
|
+
const needsOpenClaw = !skillOnly || options.noRestart !== true || options.verify === true;
|
|
200
243
|
const openclawBin = needsOpenClaw
|
|
201
244
|
? options.openclawBin ?? resolveExecutable("openclaw")
|
|
202
245
|
: options.openclawBin;
|
|
@@ -210,12 +253,35 @@ function runInstallOpenClaw(options) {
|
|
|
210
253
|
path: root,
|
|
211
254
|
mode: pluginInstall.mode
|
|
212
255
|
});
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
256
|
+
const configOperations = [
|
|
257
|
+
{
|
|
258
|
+
path: "plugins.entries.agent-knock-knock.enabled",
|
|
259
|
+
value: true
|
|
260
|
+
},
|
|
261
|
+
...(workspace === undefined
|
|
262
|
+
? []
|
|
263
|
+
: [{
|
|
264
|
+
path: "plugins.entries.agent-knock-knock.config.workspace",
|
|
265
|
+
value: workspace
|
|
266
|
+
}]),
|
|
267
|
+
...(defaultAgent === undefined
|
|
268
|
+
? []
|
|
269
|
+
: [{
|
|
270
|
+
path: "plugins.entries.agent-knock-knock.config.defaultAgent",
|
|
271
|
+
value: defaultAgent
|
|
272
|
+
}]),
|
|
273
|
+
...(selectedMode === undefined
|
|
274
|
+
? []
|
|
275
|
+
: [{
|
|
276
|
+
path: "plugins.entries.agent-knock-knock.config.mode",
|
|
277
|
+
value: selectedMode
|
|
278
|
+
}])
|
|
279
|
+
];
|
|
280
|
+
runCheckedCommand(openclawBin, ["config", "set", "--batch-json", JSON.stringify(configOperations)], { label: "openclaw config set" });
|
|
216
281
|
steps.push({
|
|
217
|
-
name: "
|
|
218
|
-
plugin: "agent-knock-knock"
|
|
282
|
+
name: "plugin_configured",
|
|
283
|
+
plugin: "agent-knock-knock",
|
|
284
|
+
updated: configOperations.map((operation) => operation.path)
|
|
219
285
|
});
|
|
220
286
|
}
|
|
221
287
|
fs.mkdirSync(path.dirname(skillDest), { recursive: true });
|
|
@@ -232,17 +298,105 @@ function runInstallOpenClaw(options) {
|
|
|
232
298
|
name: "gateway_restarted"
|
|
233
299
|
});
|
|
234
300
|
}
|
|
301
|
+
const pendingRestart = !skillOnly && options.noRestart === true;
|
|
302
|
+
const verification = options.verify === true
|
|
303
|
+
? buildDoctorReport({
|
|
304
|
+
...options,
|
|
305
|
+
openclawBin,
|
|
306
|
+
...(workspace ? { workspace } : {}),
|
|
307
|
+
mode: selectedMode ?? "all"
|
|
308
|
+
})
|
|
309
|
+
: undefined;
|
|
310
|
+
const ready = verification
|
|
311
|
+
? verification.ok === true && !pendingRestart
|
|
312
|
+
: false;
|
|
313
|
+
const nextActions = installNextActions({
|
|
314
|
+
pendingRestart,
|
|
315
|
+
verification,
|
|
316
|
+
mode: selectedMode ?? "all",
|
|
317
|
+
agent: defaultAgent ?? "codex"
|
|
318
|
+
});
|
|
235
319
|
printJson({
|
|
236
320
|
installed: true,
|
|
321
|
+
ready,
|
|
322
|
+
pending_restart: pendingRestart,
|
|
237
323
|
mode: skillOnly ? "skill_only" : "full",
|
|
324
|
+
execution_mode: selectedMode ?? null,
|
|
325
|
+
default_agent: defaultAgent ?? null,
|
|
326
|
+
workspace: workspace ?? null,
|
|
238
327
|
package_root: root,
|
|
239
328
|
openclaw_bin: openclawBin ?? null,
|
|
240
329
|
steps,
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
: "Agent Knock Knock is installed. Try: AKK list"
|
|
330
|
+
...(verification ? { verification } : {}),
|
|
331
|
+
next_actions: nextActions
|
|
244
332
|
});
|
|
245
333
|
}
|
|
334
|
+
function canonicalWorkspace(value) {
|
|
335
|
+
const requested = path.resolve(String(required(value, "--workspace is required")));
|
|
336
|
+
let canonical;
|
|
337
|
+
let stat;
|
|
338
|
+
try {
|
|
339
|
+
canonical = fs.realpathSync(requested);
|
|
340
|
+
stat = fs.statSync(canonical);
|
|
341
|
+
}
|
|
342
|
+
catch {
|
|
343
|
+
throw new Error(`--workspace does not exist: ${requested}`);
|
|
344
|
+
}
|
|
345
|
+
if (!stat.isDirectory()) {
|
|
346
|
+
throw new Error(`--workspace must be a directory: ${requested}`);
|
|
347
|
+
}
|
|
348
|
+
return canonical;
|
|
349
|
+
}
|
|
350
|
+
function optionalExecutorKind(value) {
|
|
351
|
+
if (value === undefined) {
|
|
352
|
+
return undefined;
|
|
353
|
+
}
|
|
354
|
+
const normalized = String(value).trim().toLowerCase();
|
|
355
|
+
if (!isExecutorKind(normalized)) {
|
|
356
|
+
throw new Error(`--default-agent must be one of: ${EXECUTOR_KINDS.join(", ")}`);
|
|
357
|
+
}
|
|
358
|
+
return normalized;
|
|
359
|
+
}
|
|
360
|
+
function installNextActions({ pendingRestart, verification, mode, agent }) {
|
|
361
|
+
if (pendingRestart) {
|
|
362
|
+
return [
|
|
363
|
+
{
|
|
364
|
+
action: "apply_plugin_changes",
|
|
365
|
+
command: "openclaw gateway restart"
|
|
366
|
+
},
|
|
367
|
+
{
|
|
368
|
+
action: "verify",
|
|
369
|
+
command: `agent-knock-knock doctor --mode ${mode}`
|
|
370
|
+
}
|
|
371
|
+
];
|
|
372
|
+
}
|
|
373
|
+
if (verification && verification.ok !== true) {
|
|
374
|
+
const chain = isRecord(verification.openclaw) ? verification.openclaw : {};
|
|
375
|
+
const checks = Array.isArray(chain.checks) ? chain.checks : [];
|
|
376
|
+
const remediation = checks.flatMap((check) => isRecord(check) && Array.isArray(check.remediation)
|
|
377
|
+
? check.remediation.filter((command) => typeof command === "string")
|
|
378
|
+
: []);
|
|
379
|
+
return [...new Set(remediation)].map((command) => ({
|
|
380
|
+
action: "repair",
|
|
381
|
+
command
|
|
382
|
+
}));
|
|
383
|
+
}
|
|
384
|
+
if (!verification) {
|
|
385
|
+
return [{
|
|
386
|
+
action: "verify",
|
|
387
|
+
command: `agent-knock-knock doctor --mode ${mode}`
|
|
388
|
+
}];
|
|
389
|
+
}
|
|
390
|
+
return mode === "tmux"
|
|
391
|
+
? [{
|
|
392
|
+
action: "start_agent",
|
|
393
|
+
command: `tmux new -s akk-${agent} ${agent}`
|
|
394
|
+
}]
|
|
395
|
+
: [{
|
|
396
|
+
action: "delegate",
|
|
397
|
+
command: `/akk ${agent} <task>`
|
|
398
|
+
}];
|
|
399
|
+
}
|
|
246
400
|
async function runClaudeHook(options) {
|
|
247
401
|
const rawInput = fs.readFileSync(0, "utf8");
|
|
248
402
|
let input;
|
|
@@ -321,18 +475,41 @@ function installOpenClawPlugin(openclawBin, root) {
|
|
|
321
475
|
return { mode: "replaced" };
|
|
322
476
|
}
|
|
323
477
|
function runDoctor(options) {
|
|
324
|
-
const
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
478
|
+
const report = buildDoctorReport(options);
|
|
479
|
+
printJson(report);
|
|
480
|
+
if (!report.ok) {
|
|
481
|
+
process.exitCode = 1;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
function buildDoctorReport(options) {
|
|
485
|
+
const mode = parseDoctorMode(options.mode ?? "all");
|
|
486
|
+
const timeoutMs = options.timeoutMs === undefined
|
|
487
|
+
? undefined
|
|
488
|
+
: positiveMilliseconds(options.timeoutMs, "--timeout-ms");
|
|
489
|
+
const openclawBin = String(options.openclawBin ?? resolveOptionalExecutable("openclaw"));
|
|
490
|
+
const executables = {
|
|
491
|
+
openclaw: openclawBin,
|
|
492
|
+
...(options.tmuxBin ? { tmux: String(options.tmuxBin) } : {}),
|
|
493
|
+
...(options.acpxBin ? { acpx: String(options.acpxBin) } : {}),
|
|
494
|
+
...(options.codexBin ? { codex: String(options.codexBin) } : {}),
|
|
495
|
+
...(options.claudeBin ? { claude: String(options.claudeBin) } : {}),
|
|
496
|
+
...(options.cursorBin ? { cursor: String(options.cursorBin) } : {})
|
|
497
|
+
};
|
|
498
|
+
const checks = [
|
|
499
|
+
{
|
|
500
|
+
command: "node",
|
|
501
|
+
status: "ok",
|
|
502
|
+
available: true,
|
|
503
|
+
executable: process.execPath,
|
|
504
|
+
version: process.versions.node,
|
|
505
|
+
version_supported: versionAtLeast(process.versions.node, MINIMUM_NODE_VERSION),
|
|
506
|
+
minimum_version: MINIMUM_NODE_VERSION
|
|
507
|
+
},
|
|
508
|
+
...runDoctorCapabilityProbes({
|
|
509
|
+
...(timeoutMs === undefined ? {} : { timeoutMs }),
|
|
510
|
+
executables
|
|
511
|
+
}, mode)
|
|
512
|
+
];
|
|
336
513
|
const root = packageRootDir();
|
|
337
514
|
const packageFiles = [
|
|
338
515
|
"dist/src/cli.js",
|
|
@@ -346,29 +523,78 @@ function runDoctor(options) {
|
|
|
346
523
|
exists: fs.existsSync(filePath)
|
|
347
524
|
};
|
|
348
525
|
});
|
|
349
|
-
const capabilities = evaluateDoctorCapabilities(checks);
|
|
526
|
+
const capabilities = evaluateDoctorCapabilities(checks, mode);
|
|
350
527
|
const filesOk = packageFiles.every((check) => check.exists);
|
|
351
|
-
const
|
|
352
|
-
|
|
528
|
+
const openclaw = runOpenClawChainDiagnostics({
|
|
529
|
+
openclawBin,
|
|
530
|
+
...(options.workspace ? { workspace: String(options.workspace) } : {}),
|
|
531
|
+
...(timeoutMs === undefined ? {} : { timeoutMs })
|
|
532
|
+
});
|
|
533
|
+
const selectedAgent = optionalExecutorKind(options.defaultAgent) ??
|
|
534
|
+
openclaw.default_agent ??
|
|
535
|
+
"codex";
|
|
536
|
+
const selectedAgentReady = mode === "tmux"
|
|
537
|
+
? capabilities.tmux.status === "ready" &&
|
|
538
|
+
capabilities.tmux.agents.includes(selectedAgent)
|
|
539
|
+
: mode === "acpx"
|
|
540
|
+
? capabilities.acpx.status === "ready" &&
|
|
541
|
+
capabilities.acpx.agents.includes(selectedAgent)
|
|
542
|
+
: (capabilities.tmux.status === "ready" &&
|
|
543
|
+
capabilities.tmux.agents.includes(selectedAgent)) || (capabilities.acpx.status === "ready" &&
|
|
544
|
+
capabilities.acpx.agents.includes(selectedAgent));
|
|
545
|
+
const ok = capabilities.readiness === "ready" &&
|
|
546
|
+
selectedAgentReady &&
|
|
547
|
+
filesOk &&
|
|
548
|
+
openclaw.ready;
|
|
549
|
+
return {
|
|
353
550
|
ok,
|
|
551
|
+
readiness: ok
|
|
552
|
+
? "ready"
|
|
553
|
+
: capabilities.readiness === "not_ready"
|
|
554
|
+
? "not_ready"
|
|
555
|
+
: "partially_ready",
|
|
556
|
+
selected_mode: mode,
|
|
557
|
+
selected_agent: selectedAgent
|
|
558
|
+
? {
|
|
559
|
+
agent: selectedAgent,
|
|
560
|
+
ready: selectedAgentReady
|
|
561
|
+
}
|
|
562
|
+
: null,
|
|
354
563
|
package_root: root,
|
|
355
564
|
checks,
|
|
356
565
|
package_files: packageFiles,
|
|
357
566
|
capabilities: {
|
|
358
|
-
tmux:
|
|
359
|
-
|
|
567
|
+
tmux: {
|
|
568
|
+
...capabilities.tmux,
|
|
569
|
+
checked: mode !== "acpx"
|
|
570
|
+
},
|
|
571
|
+
acpx: {
|
|
572
|
+
...capabilities.acpx,
|
|
573
|
+
checked: mode !== "tmux"
|
|
574
|
+
}
|
|
360
575
|
},
|
|
576
|
+
openclaw,
|
|
361
577
|
notes: [
|
|
362
578
|
`Node.js ${MINIMUM_NODE_VERSION}+ and OpenClaw are required.`,
|
|
363
579
|
"Choose tmux (recommended), ACPX/ACP, or install both.",
|
|
364
580
|
"tmux supports Codex and Claude Code; ACPX supports Codex, Claude Code, and Cursor.",
|
|
365
581
|
"Claude tmux completion is hook-free and fails closed unless the local transcript schema is verified."
|
|
366
|
-
]
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
582
|
+
]
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
function parseDoctorMode(value) {
|
|
586
|
+
const normalized = String(value).trim().toLowerCase();
|
|
587
|
+
if (normalized === "tmux" || normalized === "acpx" || normalized === "all") {
|
|
588
|
+
return normalized;
|
|
589
|
+
}
|
|
590
|
+
throw new Error("--mode must be one of: tmux, acpx, all");
|
|
591
|
+
}
|
|
592
|
+
function positiveMilliseconds(value, optionName) {
|
|
593
|
+
const parsed = Number(value);
|
|
594
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
595
|
+
throw new Error(`${optionName} must be a positive number`);
|
|
371
596
|
}
|
|
597
|
+
return Math.ceil(parsed);
|
|
372
598
|
}
|
|
373
599
|
function versionAtLeast(version, minimum) {
|
|
374
600
|
const parsed = version.split(".").slice(0, 3).map((part) => Number.parseInt(part, 10));
|
|
@@ -829,29 +1055,42 @@ function createRuntimeTerminalAgentRegistry(options) {
|
|
|
829
1055
|
if (!isRecord(conversation)) {
|
|
830
1056
|
return undefined;
|
|
831
1057
|
}
|
|
832
|
-
const
|
|
1058
|
+
const contextMatches = await loadCodexTerminalContexts({
|
|
833
1059
|
conversation,
|
|
834
1060
|
nativeTakeover,
|
|
835
1061
|
options
|
|
836
1062
|
});
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
1063
|
+
const matches = [];
|
|
1064
|
+
const detectionErrors = [];
|
|
1065
|
+
for (const contextMatch of contextMatches) {
|
|
1066
|
+
try {
|
|
1067
|
+
const evidence = detectCodexDurableCompletion({
|
|
1068
|
+
...request,
|
|
1069
|
+
context: contextMatch.context
|
|
1070
|
+
});
|
|
1071
|
+
if (evidence) {
|
|
1072
|
+
matches.push({
|
|
1073
|
+
...evidence,
|
|
1074
|
+
confidence: contextMatch.confidence,
|
|
1075
|
+
metadata: {
|
|
1076
|
+
...evidence.metadata,
|
|
1077
|
+
context_match: contextMatch.match,
|
|
1078
|
+
session: contextMatch.context.source
|
|
1079
|
+
}
|
|
1080
|
+
});
|
|
852
1081
|
}
|
|
853
1082
|
}
|
|
854
|
-
|
|
1083
|
+
catch (error) {
|
|
1084
|
+
detectionErrors.push(error instanceof Error ? error.message : String(error));
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
if (detectionErrors.length > 0) {
|
|
1088
|
+
throw new Error(`could not inspect every plausible Codex completion: ${detectionErrors.join("; ")}`);
|
|
1089
|
+
}
|
|
1090
|
+
if (matches.length > 1) {
|
|
1091
|
+
throw new Error("multiple same-cwd Codex sessions match the managed terminal request");
|
|
1092
|
+
}
|
|
1093
|
+
return matches[0];
|
|
855
1094
|
}
|
|
856
1095
|
}),
|
|
857
1096
|
createClaudeTerminalAgentAdapter({
|
|
@@ -2160,12 +2399,12 @@ async function runList(options) {
|
|
|
2160
2399
|
const includeAll = Boolean(options.all);
|
|
2161
2400
|
const agentFilter = options.agent ? resolveExecutor({ kind: options.agent }).kind : undefined;
|
|
2162
2401
|
const statusFilter = options.status;
|
|
2163
|
-
const
|
|
2164
|
-
.map((conversation) => summarizeConversation(conversation))
|
|
2402
|
+
const storedConversations = listConversations(storeDir)
|
|
2165
2403
|
.filter((conversation) => includeAll || isActiveStatus(conversation.status))
|
|
2166
|
-
.filter((conversation) => !agentFilter || conversation.
|
|
2404
|
+
.filter((conversation) => !agentFilter || executorForConversation(conversation).kind === agentFilter)
|
|
2167
2405
|
.filter((conversation) => !statusFilter || conversation.status === statusFilter);
|
|
2168
|
-
const
|
|
2406
|
+
const conversations = storedConversations.map((conversation) => summarizeConversation(conversation));
|
|
2407
|
+
const delegated = storedConversations.map((conversation) => delegatedListEntry(summarizeConversation(conversation), { terminalBridge: terminalBridgeEnabled(conversation) }));
|
|
2169
2408
|
const nativeScan = await buildNativeListGroups({ options, agentFilter, statusFilter });
|
|
2170
2409
|
printJson({
|
|
2171
2410
|
store_dir: storeDir,
|
|
@@ -2274,23 +2513,26 @@ async function terminalControlDiagnostics(provider) {
|
|
|
2274
2513
|
paneCount: (await provider.listPanes()).length
|
|
2275
2514
|
};
|
|
2276
2515
|
}
|
|
2277
|
-
function delegatedListEntry(task) {
|
|
2516
|
+
function delegatedListEntry(task, { terminalBridge = false } = {}) {
|
|
2278
2517
|
return {
|
|
2279
2518
|
...task,
|
|
2280
2519
|
id: task.conversation_id,
|
|
2520
|
+
short_ref: sessionShortRef(task.conversation_id),
|
|
2281
2521
|
source: "akk_delegate",
|
|
2282
2522
|
commands: {
|
|
2283
2523
|
send: canSendDelegated(task.status),
|
|
2284
2524
|
cancel: isWaitingForAgent(task.status),
|
|
2285
2525
|
close: task.status !== "closed",
|
|
2286
2526
|
status: true,
|
|
2287
|
-
approve:
|
|
2527
|
+
approve: terminalBridge && isActiveStatus(task.status)
|
|
2288
2528
|
}
|
|
2289
2529
|
};
|
|
2290
2530
|
}
|
|
2291
2531
|
function nativeListEntry(session, activeSessions) {
|
|
2532
|
+
const id = `native:${session.agent}:${session.pid}`;
|
|
2292
2533
|
return {
|
|
2293
|
-
id
|
|
2534
|
+
id,
|
|
2535
|
+
short_ref: sessionShortRef(id),
|
|
2294
2536
|
source: "native_active",
|
|
2295
2537
|
agent: session.agent,
|
|
2296
2538
|
status: "active",
|
|
@@ -2328,6 +2570,7 @@ async function terminalControlledListEntry(session, activeSessions, options, bri
|
|
|
2328
2570
|
});
|
|
2329
2571
|
return {
|
|
2330
2572
|
id: bridge.terminalConversationId(session),
|
|
2573
|
+
short_ref: sessionShortRef(bridge.terminalConversationId(session)),
|
|
2331
2574
|
source: "terminal_control",
|
|
2332
2575
|
agent: session.agent,
|
|
2333
2576
|
status: "active",
|
|
@@ -2421,6 +2664,120 @@ function childPidsForRoot(root, processes) {
|
|
|
2421
2664
|
function canSendDelegated(status) {
|
|
2422
2665
|
return !["failed", "closed", "cancelled"].includes(status);
|
|
2423
2666
|
}
|
|
2667
|
+
async function resolveConversationSelectorOption(commandName, options) {
|
|
2668
|
+
if (!SESSION_SELECTOR_COMMANDS.has(String(commandName ?? "")) ||
|
|
2669
|
+
options.state) {
|
|
2670
|
+
return;
|
|
2671
|
+
}
|
|
2672
|
+
const supplied = stringValue(options.conversation ?? options.conversationId)?.trim();
|
|
2673
|
+
if (supplied && !isSessionSelectorSyntax(supplied)) {
|
|
2674
|
+
// Full authoritative IDs keep their existing command-specific validation
|
|
2675
|
+
// path. This avoids a discovery scan before option validation and preserves
|
|
2676
|
+
// precise downstream errors for closed or currently non-actionable state.
|
|
2677
|
+
return;
|
|
2678
|
+
}
|
|
2679
|
+
const candidates = await sessionSelectorCandidates(commandName, options);
|
|
2680
|
+
const resolution = resolveSessionSelector(supplied, candidates, {
|
|
2681
|
+
operation: commandName
|
|
2682
|
+
});
|
|
2683
|
+
options.conversation = resolution.id;
|
|
2684
|
+
delete options.conversationId;
|
|
2685
|
+
}
|
|
2686
|
+
function isSessionSelectorSyntax(value) {
|
|
2687
|
+
return (/^(?:only|latest|codex|claude|cursor|(?:codex|claude|cursor):latest)$/iu.test(value) ||
|
|
2688
|
+
/^@[0-9a-f]+$/iu.test(value));
|
|
2689
|
+
}
|
|
2690
|
+
async function sessionSelectorCandidates(commandName, options) {
|
|
2691
|
+
const storeDir = storeDirFromOptions(options);
|
|
2692
|
+
cleanupIdleConversations(storeDir, options);
|
|
2693
|
+
const storedConversations = listConversations(storeDir);
|
|
2694
|
+
const managed = storedConversations.map((conversation) => delegatedListEntry(summarizeConversation(conversation), { terminalBridge: terminalBridgeEnabled(conversation) }));
|
|
2695
|
+
const managedTerminalKeys = new Set(storedConversations
|
|
2696
|
+
.filter((conversation) => isActiveStatus(conversation.status))
|
|
2697
|
+
.map((conversation) => terminalControlSelectorKey(terminalControlFromTakeover(isRecord(conversation.native_session_takeover)
|
|
2698
|
+
? conversation.native_session_takeover
|
|
2699
|
+
: undefined)))
|
|
2700
|
+
.filter((key) => key !== undefined));
|
|
2701
|
+
const nativeScan = await buildNativeListGroups({
|
|
2702
|
+
options: {
|
|
2703
|
+
...options,
|
|
2704
|
+
noApprovalScan: commandName === "approve"
|
|
2705
|
+
? options.noApprovalScan
|
|
2706
|
+
: true
|
|
2707
|
+
},
|
|
2708
|
+
agentFilter: undefined,
|
|
2709
|
+
statusFilter: undefined
|
|
2710
|
+
});
|
|
2711
|
+
const observedAtMs = Date.now();
|
|
2712
|
+
return [
|
|
2713
|
+
...managed,
|
|
2714
|
+
...nativeScan.terminalControlled.filter((entry) => {
|
|
2715
|
+
const key = terminalControlSelectorKey(entry.terminal_control);
|
|
2716
|
+
return key === undefined || !managedTerminalKeys.has(key);
|
|
2717
|
+
}),
|
|
2718
|
+
...nativeScan.native
|
|
2719
|
+
].map((entry) => ({
|
|
2720
|
+
id: String(entry.id),
|
|
2721
|
+
agent: resolveExecutor({ kind: entry.agent }).kind,
|
|
2722
|
+
actionable: sessionEntrySupportsCommand(entry, commandName),
|
|
2723
|
+
...sessionEntryRecency(entry, observedAtMs),
|
|
2724
|
+
source: stringValue(entry.source),
|
|
2725
|
+
status: stringValue(entry.status),
|
|
2726
|
+
workspace: stringValue(entry.workspace ?? entry.cwd),
|
|
2727
|
+
label: stringValue(entry.request ?? entry.command)
|
|
2728
|
+
}));
|
|
2729
|
+
}
|
|
2730
|
+
function terminalControlSelectorKey(value) {
|
|
2731
|
+
if (!isRecord(value)) {
|
|
2732
|
+
return undefined;
|
|
2733
|
+
}
|
|
2734
|
+
const target = stringValue(value.target);
|
|
2735
|
+
const panePid = Number(value.panePid);
|
|
2736
|
+
if (!target || !Number.isSafeInteger(panePid) || panePid <= 1) {
|
|
2737
|
+
return undefined;
|
|
2738
|
+
}
|
|
2739
|
+
return JSON.stringify({
|
|
2740
|
+
target,
|
|
2741
|
+
pane_pid: panePid,
|
|
2742
|
+
socket_path: stringValue(value.socketPath) ?? null
|
|
2743
|
+
});
|
|
2744
|
+
}
|
|
2745
|
+
function sessionEntrySupportsCommand(entry, commandName) {
|
|
2746
|
+
if (commandName === "summary") {
|
|
2747
|
+
commandName = "describe";
|
|
2748
|
+
}
|
|
2749
|
+
const commands = isRecord(entry.commands) ? entry.commands : {};
|
|
2750
|
+
if (typeof commands[commandName] === "boolean") {
|
|
2751
|
+
return commands[commandName] === true;
|
|
2752
|
+
}
|
|
2753
|
+
if (commandName === "describe") {
|
|
2754
|
+
return commands.status === true || entry.source === "native_active";
|
|
2755
|
+
}
|
|
2756
|
+
if (entry.source !== "akk_delegate") {
|
|
2757
|
+
return false;
|
|
2758
|
+
}
|
|
2759
|
+
if (commandName === "renew") {
|
|
2760
|
+
return entry.status === "stalled";
|
|
2761
|
+
}
|
|
2762
|
+
if (commandName === "retry-callback") {
|
|
2763
|
+
return ["callback_pending", "callback_failed"].includes(entry.status);
|
|
2764
|
+
}
|
|
2765
|
+
if (commandName === "recover") {
|
|
2766
|
+
return entry.status === "needs_recovery";
|
|
2767
|
+
}
|
|
2768
|
+
return false;
|
|
2769
|
+
}
|
|
2770
|
+
function sessionEntryRecency(entry, observedAtMs) {
|
|
2771
|
+
const timestamp = Date.parse(String(entry.updated_at ?? entry.created_at ?? ""));
|
|
2772
|
+
if (Number.isFinite(timestamp)) {
|
|
2773
|
+
return { updatedAtMs: timestamp };
|
|
2774
|
+
}
|
|
2775
|
+
const elapsedSeconds = parseProcessElapsedSeconds(entry.elapsed);
|
|
2776
|
+
if (elapsedSeconds !== undefined) {
|
|
2777
|
+
return { updatedAtMs: observedAtMs - elapsedSeconds * 1000 };
|
|
2778
|
+
}
|
|
2779
|
+
return {};
|
|
2780
|
+
}
|
|
2424
2781
|
async function resolveTerminalConversationFromOptions(options) {
|
|
2425
2782
|
return createTerminalAgentBridge(options).resolveConversationId(stringValue(options.conversation ?? options.conversationId));
|
|
2426
2783
|
}
|
|
@@ -4227,6 +4584,39 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
4227
4584
|
}
|
|
4228
4585
|
throw error;
|
|
4229
4586
|
}
|
|
4587
|
+
let terminalCompletionReconciliation = {
|
|
4588
|
+
prepared: [],
|
|
4589
|
+
reconciledConversationIds: [],
|
|
4590
|
+
protectedConversationIds: [],
|
|
4591
|
+
skipSupersede: false
|
|
4592
|
+
};
|
|
4593
|
+
if (bridge) {
|
|
4594
|
+
try {
|
|
4595
|
+
terminalCompletionReconciliation =
|
|
4596
|
+
await reconcileTerminalBridgeCompletionsBeforeSupersede({
|
|
4597
|
+
options,
|
|
4598
|
+
storeDir: storeDirFromOptions(options),
|
|
4599
|
+
terminalControl,
|
|
4600
|
+
replacementConversationId: conversation.conversation_id
|
|
4601
|
+
});
|
|
4602
|
+
}
|
|
4603
|
+
catch (error) {
|
|
4604
|
+
terminalCompletionReconciliation.skipSupersede = true;
|
|
4605
|
+
terminalCompletionReconciliation.protectedConversationIds =
|
|
4606
|
+
fenceTerminalBridgeConversationsForReconciliation({
|
|
4607
|
+
storeDir: storeDirFromOptions(options),
|
|
4608
|
+
terminalControl,
|
|
4609
|
+
replacementConversationId: conversation.conversation_id
|
|
4610
|
+
});
|
|
4611
|
+
appendEvent(logPath, {
|
|
4612
|
+
ts: new Date().toISOString(),
|
|
4613
|
+
conversation_id: conversation.conversation_id,
|
|
4614
|
+
event: "terminal_bridge_pre_supersede_reconciliation_failed",
|
|
4615
|
+
terminal_control: terminalControl,
|
|
4616
|
+
error: error instanceof Error ? error.message : String(error)
|
|
4617
|
+
});
|
|
4618
|
+
}
|
|
4619
|
+
}
|
|
4230
4620
|
const bridgeConversation = bridge
|
|
4231
4621
|
? withTerminalBridgeState({
|
|
4232
4622
|
conversation: conversationWithHookLease,
|
|
@@ -4250,11 +4640,16 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
4250
4640
|
stateLockHeld: terminalStateLockHeld
|
|
4251
4641
|
});
|
|
4252
4642
|
saveState(statePath, deliveredConversation);
|
|
4253
|
-
const supersededConversationIds = bridge
|
|
4643
|
+
const supersededConversationIds = bridge &&
|
|
4644
|
+
!terminalCompletionReconciliation.skipSupersede
|
|
4254
4645
|
? supersedeTerminalBridgeConversations({
|
|
4255
4646
|
storeDir: storeDirFromOptions(options),
|
|
4256
4647
|
terminalControl,
|
|
4257
|
-
replacementConversationId: conversation.conversation_id
|
|
4648
|
+
replacementConversationId: conversation.conversation_id,
|
|
4649
|
+
excludedConversationIds: [
|
|
4650
|
+
...terminalCompletionReconciliation.reconciledConversationIds,
|
|
4651
|
+
...terminalCompletionReconciliation.protectedConversationIds
|
|
4652
|
+
]
|
|
4258
4653
|
})
|
|
4259
4654
|
: [];
|
|
4260
4655
|
if (recordRawAttachmentAfterSend) {
|
|
@@ -4298,6 +4693,8 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
4298
4693
|
terminal_control: terminalControl,
|
|
4299
4694
|
message: textSummary(message.body),
|
|
4300
4695
|
payload: textSummary(terminalPayload),
|
|
4696
|
+
reconciled_conversation_ids: terminalCompletionReconciliation.reconciledConversationIds,
|
|
4697
|
+
reconciliation_protected_conversation_ids: terminalCompletionReconciliation.protectedConversationIds,
|
|
4301
4698
|
superseded_conversation_ids: supersededConversationIds
|
|
4302
4699
|
});
|
|
4303
4700
|
runtimeLog("info", "terminal_message_send", {
|
|
@@ -4306,6 +4703,8 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
4306
4703
|
terminal_target: terminalControl.target,
|
|
4307
4704
|
message: textSummary(message.body),
|
|
4308
4705
|
payload: textSummary(terminalPayload),
|
|
4706
|
+
reconciled_conversation_ids: terminalCompletionReconciliation.reconciledConversationIds,
|
|
4707
|
+
reconciliation_protected_conversation_ids: terminalCompletionReconciliation.protectedConversationIds,
|
|
4309
4708
|
superseded_conversation_ids: supersededConversationIds
|
|
4310
4709
|
});
|
|
4311
4710
|
const bridgeMonitor = bridge
|
|
@@ -4349,6 +4748,11 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
4349
4748
|
callbackExpected: Boolean(deliveredConversation.callback_command || deliveredConversation.gateway_method)
|
|
4350
4749
|
})
|
|
4351
4750
|
});
|
|
4751
|
+
if (terminalCompletionReconciliation.prepared.length > 0) {
|
|
4752
|
+
setImmediate(() => {
|
|
4753
|
+
deliverReconciledTerminalBridgeCallbacks(terminalCompletionReconciliation.prepared);
|
|
4754
|
+
});
|
|
4755
|
+
}
|
|
4352
4756
|
}
|
|
4353
4757
|
function terminalSubmissionPayload(payload) {
|
|
4354
4758
|
return payload.trimEnd();
|
|
@@ -4437,18 +4841,272 @@ function createManagedTerminalConversationFromRawId({ options, conversationId, a
|
|
|
4437
4841
|
message
|
|
4438
4842
|
};
|
|
4439
4843
|
}
|
|
4440
|
-
function
|
|
4441
|
-
const
|
|
4442
|
-
|
|
4443
|
-
|
|
4444
|
-
|
|
4445
|
-
|
|
4446
|
-
|
|
4447
|
-
|
|
4448
|
-
|
|
4844
|
+
async function reconcileTerminalBridgeCompletionsBeforeSupersede({ options, storeDir, terminalControl, replacementConversationId }) {
|
|
4845
|
+
const prepared = [];
|
|
4846
|
+
const reconciledConversationIds = [];
|
|
4847
|
+
const protectedConversationIds = [];
|
|
4848
|
+
const registry = createRuntimeTerminalAgentRegistry(options);
|
|
4849
|
+
for (const listedConversation of listConversations(storeDir)) {
|
|
4850
|
+
if (listedConversation.conversation_id === replacementConversationId ||
|
|
4851
|
+
!TERMINAL_BRIDGE_SUPERSEDE_STATUSES.has(listedConversation.status)) {
|
|
4852
|
+
continue;
|
|
4853
|
+
}
|
|
4854
|
+
const listedTakeover = isRecord(listedConversation.native_session_takeover)
|
|
4855
|
+
? listedConversation.native_session_takeover
|
|
4856
|
+
: undefined;
|
|
4857
|
+
const listedControl = terminalControlFromTakeover(listedTakeover);
|
|
4858
|
+
if (listedTakeover?.terminal_bridge !== true ||
|
|
4859
|
+
!listedControl ||
|
|
4860
|
+
listedControl.target !== terminalControl.target ||
|
|
4861
|
+
listedControl.socketPath !== terminalControl.socketPath ||
|
|
4862
|
+
!listedControl.capabilities.includes("durable_completion")) {
|
|
4863
|
+
continue;
|
|
4864
|
+
}
|
|
4865
|
+
const candidateStatePath = stringValue(listedConversation.state_path);
|
|
4866
|
+
if (!candidateStatePath) {
|
|
4867
|
+
continue;
|
|
4868
|
+
}
|
|
4869
|
+
const candidateLogPath = logPathForStatePath(candidateStatePath);
|
|
4870
|
+
let candidate = loadState(candidateStatePath);
|
|
4871
|
+
const candidateTakeover = isRecord(candidate.native_session_takeover)
|
|
4872
|
+
? candidate.native_session_takeover
|
|
4873
|
+
: undefined;
|
|
4874
|
+
const candidateControl = terminalControlFromTakeover(candidateTakeover);
|
|
4875
|
+
const terminalMessageId = stringValue(candidateTakeover?.terminal_bridge_message_id);
|
|
4876
|
+
if (!TERMINAL_BRIDGE_SUPERSEDE_STATUSES.has(candidate.status) ||
|
|
4877
|
+
candidateTakeover?.terminal_bridge !== true ||
|
|
4878
|
+
!candidateControl ||
|
|
4879
|
+
candidateControl.target !== terminalControl.target ||
|
|
4880
|
+
candidateControl.socketPath !== terminalControl.socketPath ||
|
|
4881
|
+
!candidateControl.capabilities.includes("durable_completion") ||
|
|
4882
|
+
!terminalMessageId) {
|
|
4883
|
+
continue;
|
|
4884
|
+
}
|
|
4885
|
+
const fenced = fenceTerminalBridgeConversationForReconciliation({
|
|
4886
|
+
statePath: candidateStatePath,
|
|
4887
|
+
logPath: candidateLogPath,
|
|
4888
|
+
terminalControl: candidateControl,
|
|
4889
|
+
terminalMessageId,
|
|
4890
|
+
replacementConversationId
|
|
4891
|
+
});
|
|
4892
|
+
if (!fenced.fenced) {
|
|
4893
|
+
continue;
|
|
4894
|
+
}
|
|
4895
|
+
candidate = fenced.conversation;
|
|
4896
|
+
const executor = executorForConversation(candidate);
|
|
4897
|
+
const adapter = registry.require(executor.kind);
|
|
4898
|
+
if (adapter.capabilities.durableCompletion !== true ||
|
|
4899
|
+
typeof adapter.detectDurableCompletion !== "function") {
|
|
4900
|
+
continue;
|
|
4901
|
+
}
|
|
4902
|
+
let completion;
|
|
4903
|
+
try {
|
|
4904
|
+
completion = await adapter.detectDurableCompletion(terminalDurableRequestForConversation(candidate, candidateControl));
|
|
4905
|
+
}
|
|
4906
|
+
catch (error) {
|
|
4907
|
+
protectedConversationIds.push(candidate.conversation_id);
|
|
4908
|
+
appendEvent(candidateLogPath, {
|
|
4909
|
+
ts: new Date().toISOString(),
|
|
4910
|
+
conversation_id: candidate.conversation_id,
|
|
4911
|
+
event: "terminal_bridge_pre_supersede_reconciliation_failed",
|
|
4912
|
+
terminal_control: candidateControl,
|
|
4913
|
+
terminal_bridge_message_id: terminalMessageId,
|
|
4914
|
+
error: error instanceof Error ? error.message : String(error)
|
|
4915
|
+
});
|
|
4916
|
+
continue;
|
|
4917
|
+
}
|
|
4918
|
+
if (!completion || completion.source !== "durable") {
|
|
4919
|
+
continue;
|
|
4920
|
+
}
|
|
4921
|
+
let preparedCompletion;
|
|
4922
|
+
try {
|
|
4923
|
+
preparedCompletion = prepareTerminalBridgeCompletionCallback({
|
|
4924
|
+
options,
|
|
4925
|
+
statePath: candidateStatePath,
|
|
4926
|
+
logPath: candidateLogPath,
|
|
4927
|
+
conversation: candidate,
|
|
4928
|
+
executor,
|
|
4929
|
+
terminalControl: candidateControl,
|
|
4930
|
+
terminalMessageId,
|
|
4931
|
+
completion,
|
|
4932
|
+
allowSupersedeRecovery: true
|
|
4933
|
+
});
|
|
4934
|
+
}
|
|
4935
|
+
catch (error) {
|
|
4936
|
+
protectedConversationIds.push(candidate.conversation_id);
|
|
4937
|
+
appendEvent(candidateLogPath, {
|
|
4938
|
+
ts: new Date().toISOString(),
|
|
4939
|
+
conversation_id: candidate.conversation_id,
|
|
4940
|
+
event: "terminal_bridge_pre_supersede_reconciliation_failed",
|
|
4941
|
+
terminal_control: candidateControl,
|
|
4942
|
+
terminal_bridge_message_id: terminalMessageId,
|
|
4943
|
+
error: error instanceof Error ? error.message : String(error)
|
|
4944
|
+
});
|
|
4945
|
+
continue;
|
|
4946
|
+
}
|
|
4947
|
+
if (!preparedCompletion.claimed) {
|
|
4948
|
+
const latest = preparedCompletion.conversation;
|
|
4949
|
+
if (TERMINAL_BRIDGE_SUPERSEDE_STATUSES.has(latest.status)) {
|
|
4950
|
+
protectedConversationIds.push(candidate.conversation_id);
|
|
4951
|
+
}
|
|
4952
|
+
else {
|
|
4953
|
+
reconciledConversationIds.push(candidate.conversation_id);
|
|
4954
|
+
}
|
|
4955
|
+
continue;
|
|
4956
|
+
}
|
|
4957
|
+
const reconciledAt = new Date().toISOString();
|
|
4958
|
+
appendEvent(candidateLogPath, {
|
|
4959
|
+
ts: reconciledAt,
|
|
4960
|
+
conversation_id: candidate.conversation_id,
|
|
4961
|
+
event: "terminal_bridge_completion_reconciled_before_supersede",
|
|
4962
|
+
terminal_control: candidateControl,
|
|
4963
|
+
terminal_bridge_message_id: terminalMessageId,
|
|
4964
|
+
callback_message_id: preparedCompletion.callbackMessageId,
|
|
4965
|
+
replacement_conversation_id: replacementConversationId
|
|
4966
|
+
});
|
|
4967
|
+
reconciledConversationIds.push(candidate.conversation_id);
|
|
4968
|
+
prepared.push({
|
|
4969
|
+
conversationId: candidate.conversation_id,
|
|
4970
|
+
statePath: candidateStatePath,
|
|
4971
|
+
logPath: candidateLogPath,
|
|
4972
|
+
terminalControl: candidateControl,
|
|
4973
|
+
prepared: preparedCompletion.prepared
|
|
4974
|
+
});
|
|
4975
|
+
}
|
|
4976
|
+
return {
|
|
4977
|
+
prepared,
|
|
4978
|
+
reconciledConversationIds,
|
|
4979
|
+
protectedConversationIds,
|
|
4980
|
+
skipSupersede: false
|
|
4981
|
+
};
|
|
4982
|
+
}
|
|
4983
|
+
function fenceTerminalBridgeConversationsForReconciliation({ storeDir, terminalControl, replacementConversationId }) {
|
|
4984
|
+
const fencedConversationIds = [];
|
|
4985
|
+
for (const listedConversation of listConversations(storeDir)) {
|
|
4986
|
+
if (listedConversation.conversation_id === replacementConversationId ||
|
|
4987
|
+
!TERMINAL_BRIDGE_SUPERSEDE_STATUSES.has(listedConversation.status)) {
|
|
4988
|
+
continue;
|
|
4989
|
+
}
|
|
4990
|
+
const statePath = stringValue(listedConversation.state_path);
|
|
4991
|
+
const listedTakeover = isRecord(listedConversation.native_session_takeover)
|
|
4992
|
+
? listedConversation.native_session_takeover
|
|
4993
|
+
: undefined;
|
|
4994
|
+
const listedControl = terminalControlFromTakeover(listedTakeover);
|
|
4995
|
+
const terminalMessageId = stringValue(listedTakeover?.terminal_bridge_message_id);
|
|
4996
|
+
if (!statePath ||
|
|
4997
|
+
listedTakeover?.terminal_bridge !== true ||
|
|
4998
|
+
!listedControl ||
|
|
4999
|
+
listedControl.target !== terminalControl.target ||
|
|
5000
|
+
listedControl.socketPath !== terminalControl.socketPath ||
|
|
5001
|
+
!terminalMessageId) {
|
|
5002
|
+
continue;
|
|
5003
|
+
}
|
|
5004
|
+
const result = fenceTerminalBridgeConversationForReconciliation({
|
|
5005
|
+
statePath,
|
|
5006
|
+
logPath: logPathForStatePath(statePath),
|
|
5007
|
+
terminalControl: listedControl,
|
|
5008
|
+
terminalMessageId,
|
|
5009
|
+
replacementConversationId
|
|
5010
|
+
});
|
|
5011
|
+
if (result.fenced) {
|
|
5012
|
+
fencedConversationIds.push(listedConversation.conversation_id);
|
|
5013
|
+
}
|
|
5014
|
+
}
|
|
5015
|
+
return fencedConversationIds;
|
|
5016
|
+
}
|
|
5017
|
+
function fenceTerminalBridgeConversationForReconciliation({ statePath, logPath, terminalControl, terminalMessageId, replacementConversationId }) {
|
|
5018
|
+
const releaseLock = acquireFileLock(`${statePath}.lock`);
|
|
5019
|
+
try {
|
|
5020
|
+
const conversation = loadState(statePath);
|
|
5021
|
+
const nativeTakeover = isRecord(conversation.native_session_takeover)
|
|
5022
|
+
? conversation.native_session_takeover
|
|
5023
|
+
: undefined;
|
|
5024
|
+
const currentControl = terminalControlFromTakeover(nativeTakeover);
|
|
5025
|
+
if (!TERMINAL_BRIDGE_SUPERSEDE_STATUSES.has(conversation.status) ||
|
|
5026
|
+
nativeTakeover?.terminal_bridge !== true ||
|
|
5027
|
+
currentControl?.target !== terminalControl.target ||
|
|
5028
|
+
currentControl?.socketPath !== terminalControl.socketPath ||
|
|
5029
|
+
stringValue(nativeTakeover?.terminal_bridge_message_id) !==
|
|
5030
|
+
terminalMessageId) {
|
|
5031
|
+
return {
|
|
5032
|
+
fenced: false,
|
|
5033
|
+
conversation
|
|
5034
|
+
};
|
|
5035
|
+
}
|
|
5036
|
+
const existingFence = isRecord(nativeTakeover.terminal_bridge_reconciliation_fence)
|
|
5037
|
+
? nativeTakeover.terminal_bridge_reconciliation_fence
|
|
5038
|
+
: undefined;
|
|
5039
|
+
if (conversation.status === "stalled" &&
|
|
5040
|
+
existingFence?.replacement_conversation_id === replacementConversationId) {
|
|
5041
|
+
return {
|
|
5042
|
+
fenced: true,
|
|
5043
|
+
conversation
|
|
5044
|
+
};
|
|
5045
|
+
}
|
|
5046
|
+
const fencedAt = new Date().toISOString();
|
|
5047
|
+
const fencedConversation = {
|
|
5048
|
+
...conversation,
|
|
5049
|
+
status: "stalled",
|
|
5050
|
+
stalled_reason: "terminal bridge paused because a newer task reused the same terminal before durable completion was resolved",
|
|
5051
|
+
native_session_takeover: {
|
|
5052
|
+
...nativeTakeover,
|
|
5053
|
+
terminal_bridge_reconciliation_fence: {
|
|
5054
|
+
replacement_conversation_id: replacementConversationId,
|
|
5055
|
+
terminal_bridge_message_id: terminalMessageId,
|
|
5056
|
+
previous_status: conversation.status,
|
|
5057
|
+
fenced_at: fencedAt
|
|
5058
|
+
}
|
|
5059
|
+
},
|
|
5060
|
+
updated_at: fencedAt
|
|
5061
|
+
};
|
|
5062
|
+
saveState(statePath, fencedConversation);
|
|
5063
|
+
appendEvent(logPath, {
|
|
5064
|
+
ts: fencedAt,
|
|
5065
|
+
conversation_id: conversation.conversation_id,
|
|
5066
|
+
event: "terminal_bridge_reconciliation_fenced",
|
|
5067
|
+
terminal_control: terminalControl,
|
|
5068
|
+
terminal_bridge_message_id: terminalMessageId,
|
|
5069
|
+
previous_status: conversation.status,
|
|
5070
|
+
replacement_conversation_id: replacementConversationId
|
|
5071
|
+
});
|
|
5072
|
+
return {
|
|
5073
|
+
fenced: true,
|
|
5074
|
+
conversation: fencedConversation
|
|
5075
|
+
};
|
|
5076
|
+
}
|
|
5077
|
+
finally {
|
|
5078
|
+
releaseLock();
|
|
5079
|
+
}
|
|
5080
|
+
}
|
|
5081
|
+
function deliverReconciledTerminalBridgeCallbacks(reconciledCallbacks) {
|
|
5082
|
+
for (const callback of reconciledCallbacks) {
|
|
5083
|
+
try {
|
|
5084
|
+
runPreparedCallback(callback.prepared, { emit: false });
|
|
5085
|
+
}
|
|
5086
|
+
catch (error) {
|
|
5087
|
+
appendEvent(callback.logPath, {
|
|
5088
|
+
ts: new Date().toISOString(),
|
|
5089
|
+
conversation_id: callback.conversationId,
|
|
5090
|
+
event: "terminal_bridge_reconciled_callback_delivery_failed",
|
|
5091
|
+
terminal_control: callback.terminalControl,
|
|
5092
|
+
error: error instanceof Error ? error.message : String(error)
|
|
5093
|
+
});
|
|
5094
|
+
runtimeLog("warn", "terminal_bridge_reconciled_callback_delivery_failed", {
|
|
5095
|
+
conversation_id: callback.conversationId,
|
|
5096
|
+
terminal_target: callback.terminalControl.target,
|
|
5097
|
+
state_path: callback.statePath,
|
|
5098
|
+
error: error instanceof Error ? error.message : String(error)
|
|
5099
|
+
});
|
|
5100
|
+
}
|
|
5101
|
+
}
|
|
5102
|
+
}
|
|
5103
|
+
function supersedeTerminalBridgeConversations({ storeDir, terminalControl, replacementConversationId, excludedConversationIds = [] }) {
|
|
5104
|
+
const excluded = new Set(excludedConversationIds);
|
|
4449
5105
|
const superseded = [];
|
|
4450
5106
|
for (const candidate of listConversations(storeDir)) {
|
|
4451
|
-
if (candidate.conversation_id === replacementConversationId ||
|
|
5107
|
+
if (candidate.conversation_id === replacementConversationId ||
|
|
5108
|
+
excluded.has(candidate.conversation_id) ||
|
|
5109
|
+
!TERMINAL_BRIDGE_SUPERSEDE_STATUSES.has(candidate.status)) {
|
|
4452
5110
|
continue;
|
|
4453
5111
|
}
|
|
4454
5112
|
const candidateTakeover = isRecord(candidate.native_session_takeover)
|
|
@@ -4467,7 +5125,7 @@ function supersedeTerminalBridgeConversations({ storeDir, terminalControl, repla
|
|
|
4467
5125
|
const releaseLock = acquireFileLock(`${candidateStatePath}.lock`);
|
|
4468
5126
|
try {
|
|
4469
5127
|
const current = loadState(candidateStatePath);
|
|
4470
|
-
if (!
|
|
5128
|
+
if (!TERMINAL_BRIDGE_SUPERSEDE_STATUSES.has(current.status)) {
|
|
4471
5129
|
continue;
|
|
4472
5130
|
}
|
|
4473
5131
|
const currentTakeover = isRecord(current.native_session_takeover)
|
|
@@ -6942,99 +7600,29 @@ async function runTerminalBridgeMonitorWithLock(options) {
|
|
|
6942
7600
|
const completionStable = completionFingerprint !== undefined && completionFingerprint === idleCompletionFingerprint;
|
|
6943
7601
|
idleCompletionFingerprint = completionFingerprint;
|
|
6944
7602
|
if (completion && completionStable && completionFingerprint) {
|
|
6945
|
-
const
|
|
6946
|
-
|
|
6947
|
-
conversationId: conversation.conversation_id,
|
|
6948
|
-
terminalMessageId: currentMessageId,
|
|
6949
|
-
completionFingerprint,
|
|
6950
|
-
outcome: completionOutcome
|
|
6951
|
-
});
|
|
6952
|
-
const claim = claimTerminalBridgeCompletion({
|
|
7603
|
+
const preparedCompletion = prepareTerminalBridgeCompletionCallback({
|
|
7604
|
+
options,
|
|
6953
7605
|
statePath,
|
|
6954
7606
|
logPath,
|
|
7607
|
+
conversation,
|
|
7608
|
+
executor,
|
|
7609
|
+
terminalControl,
|
|
6955
7610
|
terminalMessageId: currentMessageId,
|
|
6956
|
-
|
|
6957
|
-
|
|
6958
|
-
callbackMessageId,
|
|
6959
|
-
outcome: completionOutcome
|
|
7611
|
+
completion,
|
|
7612
|
+
completionFingerprint
|
|
6960
7613
|
});
|
|
6961
|
-
if (!
|
|
7614
|
+
if (!preparedCompletion.claimed) {
|
|
6962
7615
|
printJson({
|
|
6963
|
-
conversation:
|
|
7616
|
+
conversation: preparedCompletion.conversation,
|
|
6964
7617
|
monitored: true,
|
|
6965
7618
|
terminal_bridge: true,
|
|
6966
7619
|
completed: false,
|
|
6967
7620
|
duplicate: true,
|
|
6968
|
-
reason:
|
|
7621
|
+
reason: preparedCompletion.reason
|
|
6969
7622
|
});
|
|
6970
7623
|
return;
|
|
6971
7624
|
}
|
|
6972
|
-
|
|
6973
|
-
try {
|
|
6974
|
-
conversation = claim.conversation;
|
|
6975
|
-
appendEvent(logPath, {
|
|
6976
|
-
ts: new Date().toISOString(),
|
|
6977
|
-
conversation_id: conversation.conversation_id,
|
|
6978
|
-
event: "terminal_bridge_completion_detected",
|
|
6979
|
-
terminal_control: terminalControl,
|
|
6980
|
-
match: completionMatch,
|
|
6981
|
-
completion_source: completion.source,
|
|
6982
|
-
completion_outcome: completionOutcome,
|
|
6983
|
-
completion_id: completion.id,
|
|
6984
|
-
terminal_session: completionMetadata.session,
|
|
6985
|
-
context_match: completionMetadata.context_match,
|
|
6986
|
-
assistant_timestamp: completion?.timestamp,
|
|
6987
|
-
rollout_turn_id: completion.source === "durable" ? completion.id : undefined,
|
|
6988
|
-
terminal_bridge_message_id: currentMessageId,
|
|
6989
|
-
callback_message_id: callbackMessageId
|
|
6990
|
-
});
|
|
6991
|
-
const callbackMessage = {
|
|
6992
|
-
...createMessage({
|
|
6993
|
-
conversation,
|
|
6994
|
-
from: executor.actor,
|
|
6995
|
-
to: "openclaw",
|
|
6996
|
-
type: completionOutcome === "failure" ? "error" : "done",
|
|
6997
|
-
requiresResponse: false,
|
|
6998
|
-
body: completion.text,
|
|
6999
|
-
metadata: {
|
|
7000
|
-
source: "terminal_bridge",
|
|
7001
|
-
terminal_control: terminalControl,
|
|
7002
|
-
...completionMetadata,
|
|
7003
|
-
completion_source: completion.source,
|
|
7004
|
-
completion_outcome: completionOutcome,
|
|
7005
|
-
completion_id: completion.id,
|
|
7006
|
-
terminal_session: completionMetadata.session,
|
|
7007
|
-
confidence: completion.confidence,
|
|
7008
|
-
match: completionMatch,
|
|
7009
|
-
assistant_timestamp: completion?.timestamp,
|
|
7010
|
-
rollout_turn_id: completion.source === "durable" ? completion.id : undefined,
|
|
7011
|
-
terminal_bridge_message_id: currentMessageId
|
|
7012
|
-
}
|
|
7013
|
-
}),
|
|
7014
|
-
id: callbackMessageId
|
|
7015
|
-
};
|
|
7016
|
-
preparedCallback = prepareLockedCallback({
|
|
7017
|
-
...options,
|
|
7018
|
-
statePath,
|
|
7019
|
-
log: logPath,
|
|
7020
|
-
closeTerminalBridgeOnDone: completionOutcome === "success",
|
|
7021
|
-
trackCallbackDelivery: true,
|
|
7022
|
-
recoverTerminalCompletion: claim.resumed === true,
|
|
7023
|
-
preserveMessageId: true,
|
|
7024
|
-
messageJson: JSON.stringify(callbackMessage),
|
|
7025
|
-
gatewayMethod: conversation.gateway_method,
|
|
7026
|
-
gatewaySession: conversation.gateway_session,
|
|
7027
|
-
openclawSession: conversation.openclaw_session,
|
|
7028
|
-
openclawBin: conversation.openclaw_bin,
|
|
7029
|
-
gatewayUrl: stringValue(conversation.gateway_token) ? conversation.gateway_url : undefined,
|
|
7030
|
-
token: stringValue(conversation.gateway_token)
|
|
7031
|
-
});
|
|
7032
|
-
}
|
|
7033
|
-
finally {
|
|
7034
|
-
releaseClaudeHookLease(conversation);
|
|
7035
|
-
claim.release();
|
|
7036
|
-
}
|
|
7037
|
-
runPreparedCallback(preparedCallback);
|
|
7625
|
+
runPreparedCallback(preparedCompletion.prepared);
|
|
7038
7626
|
return;
|
|
7039
7627
|
}
|
|
7040
7628
|
// A concrete approval or completion observed on this poll wins over a timeout boundary.
|
|
@@ -7198,14 +7786,132 @@ function deterministicTerminalCallbackMessageId({ conversationId, terminalMessag
|
|
|
7198
7786
|
.slice(0, 32);
|
|
7199
7787
|
return `msg-terminal-${digest}`;
|
|
7200
7788
|
}
|
|
7201
|
-
function
|
|
7789
|
+
function terminalBridgeCompletionFingerprint({ completion, terminalMessageId }) {
|
|
7790
|
+
const metadata = isRecord(completion.metadata) ? completion.metadata : {};
|
|
7791
|
+
const match = stringValue(metadata.match) ??
|
|
7792
|
+
(completion.source === "screen" ? "terminal_screen" : "durable_completion");
|
|
7793
|
+
return createHash("sha256")
|
|
7794
|
+
.update(JSON.stringify({
|
|
7795
|
+
text: completion.text,
|
|
7796
|
+
timestamp: completion.timestamp,
|
|
7797
|
+
match,
|
|
7798
|
+
source: completion.source,
|
|
7799
|
+
id: completion.id,
|
|
7800
|
+
message_id: terminalMessageId
|
|
7801
|
+
}))
|
|
7802
|
+
.digest("hex");
|
|
7803
|
+
}
|
|
7804
|
+
function prepareTerminalBridgeCompletionCallback({ options, statePath, logPath, conversation, executor, terminalControl, terminalMessageId, completion, allowSupersedeRecovery = false, completionFingerprint = terminalBridgeCompletionFingerprint({
|
|
7805
|
+
completion,
|
|
7806
|
+
terminalMessageId
|
|
7807
|
+
}) }) {
|
|
7808
|
+
const completionMetadata = isRecord(completion.metadata) ? completion.metadata : {};
|
|
7809
|
+
const completionMatch = stringValue(completionMetadata.match) ??
|
|
7810
|
+
(completion.source === "screen" ? "terminal_screen" : "durable_completion");
|
|
7811
|
+
const completionOutcome = completion.outcome === "failure" ? "failure" : "success";
|
|
7812
|
+
const callbackMessageId = deterministicTerminalCallbackMessageId({
|
|
7813
|
+
conversationId: conversation.conversation_id,
|
|
7814
|
+
terminalMessageId,
|
|
7815
|
+
completionFingerprint,
|
|
7816
|
+
outcome: completionOutcome
|
|
7817
|
+
});
|
|
7818
|
+
const claim = claimTerminalBridgeCompletion({
|
|
7819
|
+
statePath,
|
|
7820
|
+
logPath,
|
|
7821
|
+
terminalMessageId,
|
|
7822
|
+
completionFingerprint,
|
|
7823
|
+
completionId: completion.id,
|
|
7824
|
+
callbackMessageId,
|
|
7825
|
+
outcome: completionOutcome,
|
|
7826
|
+
allowSupersedeRecovery
|
|
7827
|
+
});
|
|
7828
|
+
if (!claim.claimed) {
|
|
7829
|
+
return claim;
|
|
7830
|
+
}
|
|
7831
|
+
let claimedConversation = claim.conversation;
|
|
7832
|
+
try {
|
|
7833
|
+
appendEvent(logPath, {
|
|
7834
|
+
ts: new Date().toISOString(),
|
|
7835
|
+
conversation_id: claimedConversation.conversation_id,
|
|
7836
|
+
event: "terminal_bridge_completion_detected",
|
|
7837
|
+
terminal_control: terminalControl,
|
|
7838
|
+
match: completionMatch,
|
|
7839
|
+
completion_source: completion.source,
|
|
7840
|
+
completion_outcome: completionOutcome,
|
|
7841
|
+
completion_id: completion.id,
|
|
7842
|
+
terminal_session: completionMetadata.session,
|
|
7843
|
+
context_match: completionMetadata.context_match,
|
|
7844
|
+
assistant_timestamp: completion.timestamp,
|
|
7845
|
+
rollout_turn_id: completion.source === "durable" ? completion.id : undefined,
|
|
7846
|
+
terminal_bridge_message_id: terminalMessageId,
|
|
7847
|
+
callback_message_id: callbackMessageId
|
|
7848
|
+
});
|
|
7849
|
+
const callbackMessage = {
|
|
7850
|
+
...createMessage({
|
|
7851
|
+
conversation: claimedConversation,
|
|
7852
|
+
from: executor.actor,
|
|
7853
|
+
to: "openclaw",
|
|
7854
|
+
type: completionOutcome === "failure" ? "error" : "done",
|
|
7855
|
+
requiresResponse: false,
|
|
7856
|
+
body: completion.text,
|
|
7857
|
+
metadata: {
|
|
7858
|
+
source: "terminal_bridge",
|
|
7859
|
+
terminal_control: terminalControl,
|
|
7860
|
+
...completionMetadata,
|
|
7861
|
+
completion_source: completion.source,
|
|
7862
|
+
completion_outcome: completionOutcome,
|
|
7863
|
+
completion_id: completion.id,
|
|
7864
|
+
terminal_session: completionMetadata.session,
|
|
7865
|
+
confidence: completion.confidence,
|
|
7866
|
+
match: completionMatch,
|
|
7867
|
+
assistant_timestamp: completion.timestamp,
|
|
7868
|
+
rollout_turn_id: completion.source === "durable" ? completion.id : undefined,
|
|
7869
|
+
terminal_bridge_message_id: terminalMessageId
|
|
7870
|
+
}
|
|
7871
|
+
}),
|
|
7872
|
+
id: callbackMessageId
|
|
7873
|
+
};
|
|
7874
|
+
const prepared = prepareLockedCallback({
|
|
7875
|
+
...options,
|
|
7876
|
+
statePath,
|
|
7877
|
+
log: logPath,
|
|
7878
|
+
closeTerminalBridgeOnDone: completionOutcome === "success",
|
|
7879
|
+
trackCallbackDelivery: true,
|
|
7880
|
+
recoverTerminalCompletion: claim.resumed === true,
|
|
7881
|
+
allowTerminalCompletionRecoveryStatus: allowSupersedeRecovery,
|
|
7882
|
+
preserveMessageId: true,
|
|
7883
|
+
messageJson: JSON.stringify(callbackMessage),
|
|
7884
|
+
gatewayMethod: claimedConversation.gateway_method,
|
|
7885
|
+
gatewaySession: claimedConversation.gateway_session,
|
|
7886
|
+
openclawSession: claimedConversation.openclaw_session,
|
|
7887
|
+
openclawBin: claimedConversation.openclaw_bin,
|
|
7888
|
+
gatewayUrl: stringValue(claimedConversation.gateway_token)
|
|
7889
|
+
? claimedConversation.gateway_url
|
|
7890
|
+
: undefined,
|
|
7891
|
+
token: stringValue(claimedConversation.gateway_token)
|
|
7892
|
+
});
|
|
7893
|
+
return {
|
|
7894
|
+
claimed: true,
|
|
7895
|
+
conversation: claimedConversation,
|
|
7896
|
+
prepared,
|
|
7897
|
+
callbackMessageId
|
|
7898
|
+
};
|
|
7899
|
+
}
|
|
7900
|
+
finally {
|
|
7901
|
+
releaseClaudeHookLease(claimedConversation);
|
|
7902
|
+
claim.release();
|
|
7903
|
+
}
|
|
7904
|
+
}
|
|
7905
|
+
function claimTerminalBridgeCompletion({ statePath, logPath, terminalMessageId, completionFingerprint, completionId, callbackMessageId, outcome, allowSupersedeRecovery = false }) {
|
|
7202
7906
|
const release = acquireFileLock(`${statePath}.lock`);
|
|
7203
7907
|
try {
|
|
7204
7908
|
const conversation = loadState(statePath);
|
|
7205
7909
|
const nativeTakeover = isRecord(conversation.native_session_takeover)
|
|
7206
7910
|
? conversation.native_session_takeover
|
|
7207
7911
|
: {};
|
|
7208
|
-
if (!isWaitingForAgent(conversation.status)
|
|
7912
|
+
if (!isWaitingForAgent(conversation.status) &&
|
|
7913
|
+
!(allowSupersedeRecovery &&
|
|
7914
|
+
TERMINAL_BRIDGE_SUPERSEDE_STATUSES.has(conversation.status))) {
|
|
7209
7915
|
release();
|
|
7210
7916
|
return {
|
|
7211
7917
|
claimed: false,
|
|
@@ -7405,7 +8111,7 @@ function terminalBridgeApprovalCandidate({ executor, terminalControl, terminalSt
|
|
|
7405
8111
|
: {})
|
|
7406
8112
|
};
|
|
7407
8113
|
}
|
|
7408
|
-
async function
|
|
8114
|
+
async function loadCodexTerminalContexts({ conversation, nativeTakeover, options }) {
|
|
7409
8115
|
const provider = createAgentSessionProvider("codex", options);
|
|
7410
8116
|
const nativeSessionId = stringValue(nativeTakeover?.["native_session_id"]);
|
|
7411
8117
|
const startedAtMs = Date.parse(String(nativeTakeover?.["terminal_bridge_started_at"] ?? ""));
|
|
@@ -7420,17 +8126,17 @@ async function loadCodexTerminalContext({ conversation, nativeTakeover, options
|
|
|
7420
8126
|
maxTextLength: Number(options.maxTextLength ?? 4000)
|
|
7421
8127
|
});
|
|
7422
8128
|
if (context) {
|
|
7423
|
-
return {
|
|
7424
|
-
|
|
7425
|
-
|
|
7426
|
-
|
|
7427
|
-
|
|
7428
|
-
|
|
8129
|
+
return [{
|
|
8130
|
+
context,
|
|
8131
|
+
process: activeProcess,
|
|
8132
|
+
match: activeProcess?.sessionId ? "process_session_id" : "native_session_id",
|
|
8133
|
+
confidence: "high"
|
|
8134
|
+
}];
|
|
7429
8135
|
}
|
|
7430
8136
|
}
|
|
7431
8137
|
const cwd = activeProcess?.cwd ?? stringValue(nativeTakeover?.["source_cwd"]);
|
|
7432
8138
|
if (!cwd) {
|
|
7433
|
-
return
|
|
8139
|
+
return [];
|
|
7434
8140
|
}
|
|
7435
8141
|
const sessions = (await provider.listHistoricalSessions())
|
|
7436
8142
|
.filter((session) => session.cwd === cwd)
|
|
@@ -7438,28 +8144,40 @@ async function loadCodexTerminalContext({ conversation, nativeTakeover, options
|
|
|
7438
8144
|
if (!Number.isFinite(startedAtMs)) {
|
|
7439
8145
|
return true;
|
|
7440
8146
|
}
|
|
7441
|
-
|
|
8147
|
+
if (session.updatedAtMs === undefined || session.updatedAtMs === null) {
|
|
8148
|
+
return true;
|
|
8149
|
+
}
|
|
8150
|
+
const updatedAtMs = Number(session.updatedAtMs);
|
|
8151
|
+
return !Number.isFinite(updatedAtMs) || updatedAtMs >= startedAtMs;
|
|
7442
8152
|
})
|
|
7443
8153
|
.sort((left, right) => Number(right.updatedAtMs ?? 0) - Number(left.updatedAtMs ?? 0));
|
|
7444
|
-
const
|
|
7445
|
-
|
|
7446
|
-
|
|
8154
|
+
const matches = [];
|
|
8155
|
+
const candidateErrors = [];
|
|
8156
|
+
for (const session of sessions) {
|
|
8157
|
+
try {
|
|
8158
|
+
const context = await provider.getForkContext({
|
|
8159
|
+
sessionId: session.id,
|
|
8160
|
+
maxMessages: Number(options.maxMessages ?? 16),
|
|
8161
|
+
maxCommands: Number(options.maxCommands ?? 10),
|
|
8162
|
+
maxTextLength: Number(options.maxTextLength ?? 4000)
|
|
8163
|
+
});
|
|
8164
|
+
if (context) {
|
|
8165
|
+
matches.push({
|
|
8166
|
+
context,
|
|
8167
|
+
process: activeProcess,
|
|
8168
|
+
match: sessions.length === 1 ? "cwd" : "cwd_request_hash",
|
|
8169
|
+
confidence: sessions.length === 1 ? "medium" : "low"
|
|
8170
|
+
});
|
|
8171
|
+
}
|
|
8172
|
+
}
|
|
8173
|
+
catch (error) {
|
|
8174
|
+
candidateErrors.push(`${session.id}: ${error instanceof Error ? error.message : String(error)}`);
|
|
8175
|
+
}
|
|
7447
8176
|
}
|
|
7448
|
-
|
|
7449
|
-
|
|
7450
|
-
maxMessages: Number(options.maxMessages ?? 16),
|
|
7451
|
-
maxCommands: Number(options.maxCommands ?? 10),
|
|
7452
|
-
maxTextLength: Number(options.maxTextLength ?? 4000)
|
|
7453
|
-
});
|
|
7454
|
-
if (!context) {
|
|
7455
|
-
return undefined;
|
|
8177
|
+
if (candidateErrors.length > 0) {
|
|
8178
|
+
throw new Error(`could not inspect every plausible same-cwd Codex session: ${candidateErrors.join("; ")}`);
|
|
7456
8179
|
}
|
|
7457
|
-
return
|
|
7458
|
-
context,
|
|
7459
|
-
process: activeProcess,
|
|
7460
|
-
match: sessions.length === 1 ? "cwd" : "cwd_latest",
|
|
7461
|
-
confidence: sessions.length === 1 ? "medium" : "low"
|
|
7462
|
-
};
|
|
8180
|
+
return matches;
|
|
7463
8181
|
}
|
|
7464
8182
|
function resolveExecutable(command) {
|
|
7465
8183
|
if (command.includes(path.sep)) {
|
|
@@ -7653,7 +8371,9 @@ function prepareLockedCallback(options) {
|
|
|
7653
8371
|
callbackDelivery.message.id !== message.id);
|
|
7654
8372
|
const recoveringTerminalCompletion = options.recoverTerminalCompletion === true &&
|
|
7655
8373
|
duplicateMessage &&
|
|
7656
|
-
isWaitingForAgent(conversation.status)
|
|
8374
|
+
(isWaitingForAgent(conversation.status) ||
|
|
8375
|
+
(options.allowTerminalCompletionRecoveryStatus === true &&
|
|
8376
|
+
TERMINAL_BRIDGE_SUPERSEDE_STATUSES.has(conversation.status)));
|
|
7657
8377
|
if (duplicateMessage &&
|
|
7658
8378
|
!retryingPending &&
|
|
7659
8379
|
!recoveringTerminalCompletion &&
|
|
@@ -8040,9 +8760,8 @@ function deliverCallbackToOpenClaw({ options, statePath, logPath, conversation,
|
|
|
8040
8760
|
if (delivery.status !== 0) {
|
|
8041
8761
|
throw new Error(delivery.stderr || delivery.stdout || `gateway method delivery failed with status ${delivery.status}`);
|
|
8042
8762
|
}
|
|
8043
|
-
const gatewayPayload =
|
|
8044
|
-
const chatSendParams =
|
|
8045
|
-
const sessionSendParams = isRecord(gatewayPayload?.session_send) ? gatewayPayload.session_send : undefined;
|
|
8763
|
+
const gatewayPayload = parseRequiredGatewayDeliveryPayload(delivery.stdout);
|
|
8764
|
+
const { chatSendParams, sessionSendParams } = parseGatewayCallbackDeliveryPlan(gatewayPayload);
|
|
8046
8765
|
if (chatSendParams) {
|
|
8047
8766
|
const chatSendDelivery = deliverToChatSend({
|
|
8048
8767
|
openclawBin: options.openclawBin,
|
|
@@ -8050,16 +8769,71 @@ function deliverCallbackToOpenClaw({ options, statePath, logPath, conversation,
|
|
|
8050
8769
|
token: options.token,
|
|
8051
8770
|
params: chatSendParams
|
|
8052
8771
|
});
|
|
8772
|
+
if (chatSendDelivery.status !== 0) {
|
|
8773
|
+
recordCallbackProcessDelivery({
|
|
8774
|
+
logPath,
|
|
8775
|
+
conversation,
|
|
8776
|
+
message,
|
|
8777
|
+
event: "callback_chat_send_delivery",
|
|
8778
|
+
runtimeEvent: "callback_chat_send_delivery",
|
|
8779
|
+
delivery: chatSendDelivery
|
|
8780
|
+
});
|
|
8781
|
+
throw new Error(chatSendDelivery.stderr || chatSendDelivery.stdout || `chat callback delivery failed with status ${chatSendDelivery.status}`);
|
|
8782
|
+
}
|
|
8783
|
+
const chatSendAck = parseChatSendAcknowledgement(chatSendDelivery.stdout, String(chatSendParams.idempotencyKey));
|
|
8053
8784
|
recordCallbackProcessDelivery({
|
|
8054
8785
|
logPath,
|
|
8055
8786
|
conversation,
|
|
8056
8787
|
message,
|
|
8057
8788
|
event: "callback_chat_send_delivery",
|
|
8058
8789
|
runtimeEvent: "callback_chat_send_delivery",
|
|
8059
|
-
delivery: chatSendDelivery
|
|
8790
|
+
delivery: chatSendDelivery,
|
|
8791
|
+
detail: {
|
|
8792
|
+
run_id: chatSendAck.runId,
|
|
8793
|
+
run_status: chatSendAck.status
|
|
8794
|
+
}
|
|
8060
8795
|
});
|
|
8061
|
-
if (
|
|
8062
|
-
|
|
8796
|
+
if (chatSendAck.status === "ok") {
|
|
8797
|
+
return "gateway_method+chat_send";
|
|
8798
|
+
}
|
|
8799
|
+
const agentWaitDelivery = deliverToAgentWait({
|
|
8800
|
+
openclawBin: options.openclawBin,
|
|
8801
|
+
gatewayUrl: options.gatewayUrl,
|
|
8802
|
+
token: options.token,
|
|
8803
|
+
runId: chatSendAck.runId
|
|
8804
|
+
});
|
|
8805
|
+
if (agentWaitDelivery.status !== 0) {
|
|
8806
|
+
recordCallbackProcessDelivery({
|
|
8807
|
+
logPath,
|
|
8808
|
+
conversation,
|
|
8809
|
+
message,
|
|
8810
|
+
event: "callback_agent_wait_delivery",
|
|
8811
|
+
runtimeEvent: "callback_agent_wait_delivery",
|
|
8812
|
+
delivery: agentWaitDelivery,
|
|
8813
|
+
detail: { run_id: chatSendAck.runId }
|
|
8814
|
+
});
|
|
8815
|
+
throw new Error(agentWaitDelivery.stderr ||
|
|
8816
|
+
agentWaitDelivery.stdout ||
|
|
8817
|
+
`callback agent wait failed with status ${agentWaitDelivery.status}`);
|
|
8818
|
+
}
|
|
8819
|
+
const waitResult = parseAgentWaitResult(agentWaitDelivery.stdout, chatSendAck.runId);
|
|
8820
|
+
recordCallbackProcessDelivery({
|
|
8821
|
+
logPath,
|
|
8822
|
+
conversation,
|
|
8823
|
+
message,
|
|
8824
|
+
event: "callback_agent_wait_delivery",
|
|
8825
|
+
runtimeEvent: "callback_agent_wait_delivery",
|
|
8826
|
+
delivery: agentWaitDelivery,
|
|
8827
|
+
detail: {
|
|
8828
|
+
run_id: chatSendAck.runId,
|
|
8829
|
+
run_status: waitResult.status
|
|
8830
|
+
}
|
|
8831
|
+
});
|
|
8832
|
+
if (waitResult.status !== "ok") {
|
|
8833
|
+
const detail = stringValue(waitResult.error) ??
|
|
8834
|
+
stringValue(waitResult.stopReason) ??
|
|
8835
|
+
`agent.wait returned ${String(waitResult.status)}`;
|
|
8836
|
+
throw new Error(`callback Gateway run did not complete successfully: ${detail}`);
|
|
8063
8837
|
}
|
|
8064
8838
|
return "gateway_method+chat_send";
|
|
8065
8839
|
}
|
|
@@ -8070,16 +8844,70 @@ function deliverCallbackToOpenClaw({ options, statePath, logPath, conversation,
|
|
|
8070
8844
|
token: options.token,
|
|
8071
8845
|
params: sessionSendParams
|
|
8072
8846
|
});
|
|
8847
|
+
if (sessionSendDelivery.status !== 0) {
|
|
8848
|
+
recordCallbackProcessDelivery({
|
|
8849
|
+
logPath,
|
|
8850
|
+
conversation,
|
|
8851
|
+
message,
|
|
8852
|
+
event: "callback_session_send_delivery",
|
|
8853
|
+
runtimeEvent: "callback_session_send_delivery",
|
|
8854
|
+
delivery: sessionSendDelivery
|
|
8855
|
+
});
|
|
8856
|
+
throw new Error(sessionSendDelivery.stderr || sessionSendDelivery.stdout || `session callback delivery failed with status ${sessionSendDelivery.status}`);
|
|
8857
|
+
}
|
|
8858
|
+
const sessionSendAck = parseChatSendAcknowledgement(sessionSendDelivery.stdout, String(sessionSendParams.idempotencyKey));
|
|
8073
8859
|
recordCallbackProcessDelivery({
|
|
8074
8860
|
logPath,
|
|
8075
8861
|
conversation,
|
|
8076
8862
|
message,
|
|
8077
8863
|
event: "callback_session_send_delivery",
|
|
8078
8864
|
runtimeEvent: "callback_session_send_delivery",
|
|
8079
|
-
delivery: sessionSendDelivery
|
|
8865
|
+
delivery: sessionSendDelivery,
|
|
8866
|
+
detail: {
|
|
8867
|
+
run_id: sessionSendAck.runId,
|
|
8868
|
+
run_status: sessionSendAck.status
|
|
8869
|
+
}
|
|
8080
8870
|
});
|
|
8081
|
-
if (
|
|
8082
|
-
|
|
8871
|
+
if (sessionSendAck.status !== "ok") {
|
|
8872
|
+
const agentWaitDelivery = deliverToAgentWait({
|
|
8873
|
+
openclawBin: options.openclawBin,
|
|
8874
|
+
gatewayUrl: options.gatewayUrl,
|
|
8875
|
+
token: options.token,
|
|
8876
|
+
runId: sessionSendAck.runId
|
|
8877
|
+
});
|
|
8878
|
+
if (agentWaitDelivery.status !== 0) {
|
|
8879
|
+
recordCallbackProcessDelivery({
|
|
8880
|
+
logPath,
|
|
8881
|
+
conversation,
|
|
8882
|
+
message,
|
|
8883
|
+
event: "callback_agent_wait_delivery",
|
|
8884
|
+
runtimeEvent: "callback_agent_wait_delivery",
|
|
8885
|
+
delivery: agentWaitDelivery,
|
|
8886
|
+
detail: { run_id: sessionSendAck.runId }
|
|
8887
|
+
});
|
|
8888
|
+
throw new Error(agentWaitDelivery.stderr ||
|
|
8889
|
+
agentWaitDelivery.stdout ||
|
|
8890
|
+
`callback agent wait failed with status ${agentWaitDelivery.status}`);
|
|
8891
|
+
}
|
|
8892
|
+
const waitResult = parseAgentWaitResult(agentWaitDelivery.stdout, sessionSendAck.runId);
|
|
8893
|
+
recordCallbackProcessDelivery({
|
|
8894
|
+
logPath,
|
|
8895
|
+
conversation,
|
|
8896
|
+
message,
|
|
8897
|
+
event: "callback_agent_wait_delivery",
|
|
8898
|
+
runtimeEvent: "callback_agent_wait_delivery",
|
|
8899
|
+
delivery: agentWaitDelivery,
|
|
8900
|
+
detail: {
|
|
8901
|
+
run_id: sessionSendAck.runId,
|
|
8902
|
+
run_status: waitResult.status
|
|
8903
|
+
}
|
|
8904
|
+
});
|
|
8905
|
+
if (waitResult.status !== "ok") {
|
|
8906
|
+
const detail = stringValue(waitResult.error) ??
|
|
8907
|
+
stringValue(waitResult.stopReason) ??
|
|
8908
|
+
`agent.wait returned ${String(waitResult.status)}`;
|
|
8909
|
+
throw new Error(`callback Gateway run did not complete successfully: ${detail}`);
|
|
8910
|
+
}
|
|
8083
8911
|
}
|
|
8084
8912
|
return "gateway_method+sessions_send";
|
|
8085
8913
|
}
|
|
@@ -9371,6 +10199,43 @@ function deliverToChatSend({ openclawBin, gatewayUrl, token, params }) {
|
|
|
9371
10199
|
stderr: result.stderr ?? ""
|
|
9372
10200
|
};
|
|
9373
10201
|
}
|
|
10202
|
+
function deliverToAgentWait({ openclawBin, gatewayUrl, token, runId }) {
|
|
10203
|
+
const args = [
|
|
10204
|
+
"gateway",
|
|
10205
|
+
"call",
|
|
10206
|
+
"agent.wait",
|
|
10207
|
+
"--params",
|
|
10208
|
+
JSON.stringify({
|
|
10209
|
+
runId,
|
|
10210
|
+
timeoutMs: CALLBACK_AGENT_WAIT_TIMEOUT_MS
|
|
10211
|
+
}),
|
|
10212
|
+
"--json",
|
|
10213
|
+
"--timeout",
|
|
10214
|
+
String(CALLBACK_AGENT_WAIT_CLI_TIMEOUT_MS)
|
|
10215
|
+
];
|
|
10216
|
+
if (gatewayUrl) {
|
|
10217
|
+
args.push("--url", gatewayUrl);
|
|
10218
|
+
}
|
|
10219
|
+
const result = spawnSync(openclawBin ?? "openclaw", args, {
|
|
10220
|
+
encoding: "utf8",
|
|
10221
|
+
maxBuffer: 1024 * 1024 * 10,
|
|
10222
|
+
timeout: CALLBACK_AGENT_WAIT_PROCESS_TIMEOUT_MS,
|
|
10223
|
+
killSignal: "SIGKILL",
|
|
10224
|
+
env: openClawGatewayEnvironment(token)
|
|
10225
|
+
});
|
|
10226
|
+
if (result.error) {
|
|
10227
|
+
return {
|
|
10228
|
+
status: 1,
|
|
10229
|
+
stdout: result.stdout ?? "",
|
|
10230
|
+
stderr: result.error.message
|
|
10231
|
+
};
|
|
10232
|
+
}
|
|
10233
|
+
return {
|
|
10234
|
+
status: result.status ?? 1,
|
|
10235
|
+
stdout: result.stdout ?? "",
|
|
10236
|
+
stderr: result.stderr ?? ""
|
|
10237
|
+
};
|
|
10238
|
+
}
|
|
9374
10239
|
function openClawGatewayEnvironment(token) {
|
|
9375
10240
|
if (!token || token === "<token>") {
|
|
9376
10241
|
return process.env;
|
|
@@ -9431,6 +10296,96 @@ function parseOptionalJson(text) {
|
|
|
9431
10296
|
return undefined;
|
|
9432
10297
|
}
|
|
9433
10298
|
}
|
|
10299
|
+
function parseRequiredGatewayDeliveryPayload(text) {
|
|
10300
|
+
const payload = parseOptionalJson(text);
|
|
10301
|
+
if (!isRecord(payload)) {
|
|
10302
|
+
throw new Error("gateway callback returned malformed JSON");
|
|
10303
|
+
}
|
|
10304
|
+
if (payload.ok !== true) {
|
|
10305
|
+
throw new Error(`gateway callback was not accepted: ${stringValue(payload.error) ?? stringValue(payload.message) ?? "ok was not true"}`);
|
|
10306
|
+
}
|
|
10307
|
+
if (payload.delivery_required !== undefined &&
|
|
10308
|
+
typeof payload.delivery_required !== "boolean") {
|
|
10309
|
+
throw new Error("gateway callback returned an invalid delivery_required value");
|
|
10310
|
+
}
|
|
10311
|
+
return payload;
|
|
10312
|
+
}
|
|
10313
|
+
function parseGatewayCallbackDeliveryPlan(payload) {
|
|
10314
|
+
const chatSendParams = isRecord(payload.chat_send) ? payload.chat_send : undefined;
|
|
10315
|
+
const sessionSendParams = isRecord(payload.session_send) ? payload.session_send : undefined;
|
|
10316
|
+
if (chatSendParams && sessionSendParams) {
|
|
10317
|
+
throw new Error("gateway callback returned multiple delivery plans");
|
|
10318
|
+
}
|
|
10319
|
+
const deliveryRequired = payload.delivery_required === true;
|
|
10320
|
+
const deliveryExplicitlyNotRequired = payload.delivery_required === false;
|
|
10321
|
+
const deliveryMode = stringValue(payload.delivery_mode);
|
|
10322
|
+
if (deliveryRequired && !chatSendParams && !sessionSendParams) {
|
|
10323
|
+
throw new Error("gateway callback requires delivery but returned no supported chat_send or session_send plan");
|
|
10324
|
+
}
|
|
10325
|
+
if (deliveryExplicitlyNotRequired && (chatSendParams || sessionSendParams)) {
|
|
10326
|
+
throw new Error("gateway callback returned a delivery plan without delivery_required");
|
|
10327
|
+
}
|
|
10328
|
+
if (deliveryMode && deliveryMode !== "none") {
|
|
10329
|
+
const expectedMode = chatSendParams ? "chat.send" : sessionSendParams ? "sessions.send" : undefined;
|
|
10330
|
+
if (deliveryMode !== expectedMode) {
|
|
10331
|
+
throw new Error("gateway callback delivery_mode does not match its delivery plan");
|
|
10332
|
+
}
|
|
10333
|
+
}
|
|
10334
|
+
if (deliveryMode === "none" && deliveryRequired) {
|
|
10335
|
+
throw new Error("gateway callback delivery_mode none cannot require delivery");
|
|
10336
|
+
}
|
|
10337
|
+
if (chatSendParams) {
|
|
10338
|
+
if (!stringValue(chatSendParams.sessionKey) ||
|
|
10339
|
+
!stringValue(chatSendParams.message) ||
|
|
10340
|
+
!stringValue(chatSendParams.idempotencyKey) ||
|
|
10341
|
+
chatSendParams.deliver !== true) {
|
|
10342
|
+
throw new Error("gateway callback returned an invalid chat_send delivery plan");
|
|
10343
|
+
}
|
|
10344
|
+
}
|
|
10345
|
+
if (sessionSendParams) {
|
|
10346
|
+
if (!stringValue(sessionSendParams.key) ||
|
|
10347
|
+
!stringValue(sessionSendParams.message) ||
|
|
10348
|
+
!stringValue(sessionSendParams.idempotencyKey)) {
|
|
10349
|
+
throw new Error("gateway callback returned an invalid session_send delivery plan");
|
|
10350
|
+
}
|
|
10351
|
+
}
|
|
10352
|
+
return { chatSendParams, sessionSendParams };
|
|
10353
|
+
}
|
|
10354
|
+
function parseChatSendAcknowledgement(text, expectedRunId) {
|
|
10355
|
+
const payload = parseOptionalJson(text);
|
|
10356
|
+
if (!isRecord(payload)) {
|
|
10357
|
+
throw new Error("chat.send returned malformed JSON");
|
|
10358
|
+
}
|
|
10359
|
+
const runId = stringValue(payload.runId);
|
|
10360
|
+
const status = stringValue(payload.status);
|
|
10361
|
+
if (!runId) {
|
|
10362
|
+
throw new Error("chat.send acknowledgement is missing runId");
|
|
10363
|
+
}
|
|
10364
|
+
if (runId !== expectedRunId) {
|
|
10365
|
+
throw new Error("chat.send acknowledgement runId does not match its idempotencyKey");
|
|
10366
|
+
}
|
|
10367
|
+
if (!status || !["started", "in_flight", "ok"].includes(status)) {
|
|
10368
|
+
throw new Error(`chat.send returned unexpected status ${JSON.stringify(status ?? null)}`);
|
|
10369
|
+
}
|
|
10370
|
+
return {
|
|
10371
|
+
runId,
|
|
10372
|
+
status: status
|
|
10373
|
+
};
|
|
10374
|
+
}
|
|
10375
|
+
function parseAgentWaitResult(text, expectedRunId) {
|
|
10376
|
+
const payload = parseOptionalJson(text);
|
|
10377
|
+
if (!isRecord(payload)) {
|
|
10378
|
+
throw new Error("agent.wait returned malformed JSON");
|
|
10379
|
+
}
|
|
10380
|
+
if (stringValue(payload.runId) !== expectedRunId) {
|
|
10381
|
+
throw new Error("agent.wait returned a result for a different runId");
|
|
10382
|
+
}
|
|
10383
|
+
const status = stringValue(payload.status);
|
|
10384
|
+
if (!status || !["ok", "error", "timeout", "pending"].includes(status)) {
|
|
10385
|
+
throw new Error(`agent.wait returned unexpected status ${JSON.stringify(status ?? null)}`);
|
|
10386
|
+
}
|
|
10387
|
+
return payload;
|
|
10388
|
+
}
|
|
9434
10389
|
function createAgentSessionProvider(agent, options) {
|
|
9435
10390
|
if (agent !== "codex") {
|
|
9436
10391
|
throw new Error(`unsupported agent session provider: ${agent}`);
|
|
@@ -9654,18 +10609,18 @@ function usage() {
|
|
|
9654
10609
|
agent-knock-knock bootstrap-prompt --callback-command <command> [--agent ${agentList}]
|
|
9655
10610
|
agent-knock-knock delegate --request <text> [--agent ${agentList}] [--store-dir <dir>] [--all-proxy <url>] [--agent-timeout-minutes <minutes>] [--token <gateway-token>] [--send|--background]
|
|
9656
10611
|
agent-knock-knock list [--store-dir <dir>] [--agent ${agentList}] [--status <status>] [--all] [--managed-only] [--no-approval-scan] [--terminal-debug]
|
|
9657
|
-
agent-knock-knock status --conversation <id> [--store-dir <dir>] [--trace]
|
|
9658
|
-
agent-knock-knock describe --conversation <id> [--store-dir <dir>]
|
|
9659
|
-
agent-knock-knock send --conversation <id> --message <text> [--type answer|task|control] [--all-proxy <url>] [--agent-timeout-minutes <minutes>] [--agent-hard-timeout-minutes <minutes>]
|
|
9660
|
-
agent-knock-knock approve --conversation <id>
|
|
9661
|
-
agent-knock-knock cancel --conversation <id> [--all-proxy <url>]
|
|
9662
|
-
agent-knock-knock renew --conversation <id> [--minutes <inactivity-minutes>]
|
|
10612
|
+
agent-knock-knock status [--conversation <id|selector>] [--store-dir <dir>] [--trace]
|
|
10613
|
+
agent-knock-knock describe [--conversation <id|selector>] [--store-dir <dir>]
|
|
10614
|
+
agent-knock-knock send [--conversation <id|selector>] --message <text> [--type answer|task|control] [--all-proxy <url>] [--agent-timeout-minutes <minutes>] [--agent-hard-timeout-minutes <minutes>]
|
|
10615
|
+
agent-knock-knock approve [--conversation <id|selector>]
|
|
10616
|
+
agent-knock-knock cancel [--conversation <id|selector>] [--all-proxy <url>]
|
|
10617
|
+
agent-knock-knock renew [--conversation <id|selector>] [--minutes <inactivity-minutes>]
|
|
9663
10618
|
agent-knock-knock reconcile-monitors [--store-dir <dir>]
|
|
9664
10619
|
agent-knock-knock retry-callback --conversation <id> [--store-dir <dir>]
|
|
9665
10620
|
agent-knock-knock recover --conversation <id> [--session <name>] [--all-proxy <url>]
|
|
9666
10621
|
agent-knock-knock close --conversation <id> [--reason <text>]
|
|
9667
|
-
agent-knock-knock install-openclaw [--openclaw-bin <path>] [--skill-path <path>] [--skill-only] [--no-restart]
|
|
9668
|
-
agent-knock-knock doctor
|
|
10622
|
+
agent-knock-knock install-openclaw [--workspace <path>] [--default-agent ${agentList}] [--mode tmux|acpx|all] [--verify] [--openclaw-bin <path>] [--skill-path <path>] [--skill-only] [--no-restart]
|
|
10623
|
+
agent-knock-knock doctor [--mode tmux|acpx|all] [--workspace <path>] [--openclaw-bin <path>]
|
|
9669
10624
|
agent-knock-knock agent takeover --agent codex --session-id <id> --strategy terminate_then_resume|terminal_control|fork [--create-conversation]
|
|
9670
10625
|
agent-knock-knock callback --state <file> --message-json <json> [--record-only]
|
|
9671
10626
|
agent-knock-knock transcript --log <file> [--include-raw]
|