@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/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 needsOpenClaw = !skillOnly || options.noRestart !== true;
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
- runCheckedCommand(openclawBin, ["plugins", "enable", "agent-knock-knock"], {
214
- label: "openclaw plugins enable"
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: "plugin_enabled",
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
- next: options.noRestart === true
242
- ? "Restart the OpenClaw Gateway before using Agent Knock Knock."
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 commands = ["node", "openclaw", "tmux", "acpx", "codex", "claude", "cursor"];
325
- const checks = commands.map((commandName) => {
326
- const check = executableCheck(commandName);
327
- return commandName === "node"
328
- ? {
329
- ...check,
330
- version: process.versions.node,
331
- version_supported: versionAtLeast(process.versions.node, MINIMUM_NODE_VERSION),
332
- minimum_version: MINIMUM_NODE_VERSION
333
- }
334
- : check;
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 ok = capabilities.coreOk && filesOk && capabilities.transportOk;
352
- printJson({
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: capabilities.tmux,
359
- acp: capabilities.acp
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
- options
368
- });
369
- if (!ok) {
370
- process.exitCode = 1;
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 contextMatch = await loadCodexTerminalContext({
1058
+ const contextMatches = await loadCodexTerminalContexts({
833
1059
  conversation,
834
1060
  nativeTakeover,
835
1061
  options
836
1062
  });
837
- if (!contextMatch?.context) {
838
- return undefined;
839
- }
840
- const evidence = detectCodexDurableCompletion({
841
- ...request,
842
- context: contextMatch.context
843
- });
844
- return evidence
845
- ? {
846
- ...evidence,
847
- confidence: contextMatch.confidence,
848
- metadata: {
849
- ...evidence.metadata,
850
- context_match: contextMatch.match,
851
- session: contextMatch.context.source
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
- : undefined;
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 conversations = listConversations(storeDir)
2164
- .map((conversation) => summarizeConversation(conversation))
2402
+ const storedConversations = listConversations(storeDir)
2165
2403
  .filter((conversation) => includeAll || isActiveStatus(conversation.status))
2166
- .filter((conversation) => !agentFilter || conversation.agent === agentFilter)
2404
+ .filter((conversation) => !agentFilter || executorForConversation(conversation).kind === agentFilter)
2167
2405
  .filter((conversation) => !statusFilter || conversation.status === statusFilter);
2168
- const delegated = conversations.map(delegatedListEntry);
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: false
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: `native:${session.agent}:${session.pid}`,
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 supersedeTerminalBridgeConversations({ storeDir, terminalControl, replacementConversationId }) {
4441
- const activeStatuses = new Set([
4442
- "created",
4443
- "running",
4444
- "waiting_for_agent",
4445
- "waiting_for_openclaw",
4446
- "stalled",
4447
- "cancelling"
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 || !activeStatuses.has(candidate.status)) {
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 (!activeStatuses.has(current.status)) {
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 completionOutcome = completion.outcome === "failure" ? "failure" : "success";
6946
- const callbackMessageId = deterministicTerminalCallbackMessageId({
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
- completionFingerprint,
6957
- completionId: completion.id,
6958
- callbackMessageId,
6959
- outcome: completionOutcome
7611
+ completion,
7612
+ completionFingerprint
6960
7613
  });
6961
- if (!claim.claimed) {
7614
+ if (!preparedCompletion.claimed) {
6962
7615
  printJson({
6963
- conversation: claim.conversation,
7616
+ conversation: preparedCompletion.conversation,
6964
7617
  monitored: true,
6965
7618
  terminal_bridge: true,
6966
7619
  completed: false,
6967
7620
  duplicate: true,
6968
- reason: claim.reason
7621
+ reason: preparedCompletion.reason
6969
7622
  });
6970
7623
  return;
6971
7624
  }
6972
- let preparedCallback;
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 claimTerminalBridgeCompletion({ statePath, logPath, terminalMessageId, completionFingerprint, completionId, callbackMessageId, outcome }) {
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 loadCodexTerminalContext({ conversation, nativeTakeover, options }) {
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
- context,
7425
- process: activeProcess,
7426
- match: activeProcess?.sessionId ? "process_session_id" : "native_session_id",
7427
- confidence: "high"
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 undefined;
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
- return Number(session.updatedAtMs ?? 0) >= startedAtMs;
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 selected = sessions[0];
7445
- if (!selected) {
7446
- return undefined;
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
- const context = await provider.getForkContext({
7449
- sessionId: selected.id,
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 = parseOptionalJson(delivery.stdout);
8044
- const chatSendParams = isRecord(gatewayPayload?.chat_send) ? gatewayPayload.chat_send : undefined;
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 (chatSendDelivery.status !== 0) {
8062
- throw new Error(chatSendDelivery.stderr || chatSendDelivery.stdout || `chat callback delivery failed with status ${chatSendDelivery.status}`);
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 (sessionSendDelivery.status !== 0) {
8082
- throw new Error(sessionSendDelivery.stderr || sessionSendDelivery.stdout || `session callback delivery failed with status ${sessionSendDelivery.status}`);
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]