@scotthuang/agent-knock-knock 0.2.43 → 0.2.46

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.
Files changed (34) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +135 -244
  3. package/dist/src/cli.js +641 -730
  4. package/dist/src/cli.js.map +1 -1
  5. package/dist/src/codex-session-provider.d.ts +6 -32
  6. package/dist/src/codex-session-provider.js +2 -1
  7. package/dist/src/codex-session-provider.js.map +1 -1
  8. package/dist/src/codex-store-adapter.d.ts +3 -9
  9. package/dist/src/codex-store-adapter.js +5 -55
  10. package/dist/src/codex-store-adapter.js.map +1 -1
  11. package/dist/src/codex-terminal-agent-adapter.d.ts +34 -0
  12. package/dist/src/codex-terminal-agent-adapter.js +386 -0
  13. package/dist/src/codex-terminal-agent-adapter.js.map +1 -0
  14. package/dist/src/openclaw-plugin.js +54 -5
  15. package/dist/src/openclaw-plugin.js.map +1 -1
  16. package/dist/src/protocol.d.ts +1 -1
  17. package/dist/src/terminal-agent-adapter.d.ts +124 -0
  18. package/dist/src/terminal-agent-adapter.js +109 -0
  19. package/dist/src/terminal-agent-adapter.js.map +1 -0
  20. package/dist/src/terminal-agent-bridge.d.ts +97 -0
  21. package/dist/src/terminal-agent-bridge.js +362 -0
  22. package/dist/src/terminal-agent-bridge.js.map +1 -0
  23. package/dist/src/terminal-agent-registry.d.ts +7 -0
  24. package/dist/src/terminal-agent-registry.js +22 -0
  25. package/dist/src/terminal-agent-registry.js.map +1 -0
  26. package/dist/src/terminal-control-provider.d.ts +8 -6
  27. package/dist/src/terminal-control-provider.js +5 -5
  28. package/dist/src/terminal-control-provider.js.map +1 -1
  29. package/dist/src/terminal-process-source.d.ts +25 -0
  30. package/dist/src/terminal-process-source.js +77 -0
  31. package/dist/src/terminal-process-source.js.map +1 -0
  32. package/openclaw.plugin.json +4 -0
  33. package/package.json +1 -1
  34. package/templates/openclaw-skills/agent-knock-knock/SKILL.md +4 -4
package/dist/src/cli.js CHANGED
@@ -5,20 +5,26 @@ import fs from "node:fs";
5
5
  import path from "node:path";
6
6
  import process from "node:process";
7
7
  import { fileURLToPath } from "node:url";
8
+ import { createCodexTerminalAgentAdapter, detectCodexDurableCompletion } from "./codex-terminal-agent-adapter.js";
8
9
  import { CodexLocalSessionProvider } from "./codex-local-session-provider.js";
9
10
  import { CodexStoreAdapter } from "./codex-store-adapter.js";
10
11
  import { applyMessageToConversation, budgetAction, createConversation, createMessage, executorForConversation, extractStructuredMessage, parseMessageJson, resolveExecutor } from "./protocol.js";
11
12
  import { EXECUTOR_KINDS, acpxCommandForExecutor, executorDefinitionForKind, modelEnvForExecutor, normalizeModelForExecutor, proxyEnvForExecutor } from "./executors.js";
12
13
  import { executorBootstrapPrompt } from "./bootstrap.js";
13
- import { redactString, writeRuntimeLog } from "./runtime-log.js";
14
+ import { writeRuntimeLog } from "./runtime-log.js";
14
15
  import { formatTranscript, readNdjsonLog } from "./transcript.js";
15
16
  import { appendEvent, defaultStoreDir, listConversations, logPathForStatePath, loadConversationById, loadState, messageEvent, pathsForConversation, pathsForConversationDir, saveState, statePathForConversationId } from "./store.js";
16
17
  import { planFork, planTakeover } from "./session-takeover-planner.js";
17
- import { StaticTerminalControlProvider, TmuxTerminalControlProvider, enrichActiveProcessesWithTerminalControl, terminalRefFromPane } from "./terminal-control-provider.js";
18
+ import { StaticTerminalControlProvider, TmuxTerminalControlProvider } from "./terminal-control-provider.js";
19
+ import { parseTerminalConversationId } from "./terminal-agent-adapter.js";
20
+ import { createProductionTerminalAgentRegistry } from "./terminal-agent-registry.js";
21
+ import { StaticTerminalProcessSource, SystemTerminalProcessSource } from "./terminal-process-source.js";
22
+ import { TerminalAgentBridge } from "./terminal-agent-bridge.js";
18
23
  const DEFAULT_IDLE_TIMEOUT_MINUTES = 10080;
19
24
  const DEFAULT_AGENT_TIMEOUT_MINUTES = 60;
20
25
  const DEFAULT_AGENT_HARD_TIMEOUT_MINUTES = 720;
21
26
  const DEFAULT_MONITOR_POLL_INTERVAL_MS = 5000;
27
+ const CALLBACK_RETRY_DELAYS_MS = [5000, 15000, 60000, 60000];
22
28
  const DEFAULT_CODEX_ACPX_AGENT_COMMAND = "npx -y @agentclientprotocol/codex-acp@^1.1.0";
23
29
  class InlineCodexSessionAdapter {
24
30
  threads;
@@ -127,6 +133,9 @@ async function runCommand(commandName, options) {
127
133
  else if (commandName === "callback") {
128
134
  runCallback(options);
129
135
  }
136
+ else if (commandName === "retry-callback") {
137
+ runRetryCallback(options);
138
+ }
130
139
  else if (commandName === "monitor") {
131
140
  await runMonitor(options);
132
141
  }
@@ -201,7 +210,9 @@ function installOpenClawPlugin(openclawBin, root) {
201
210
  }
202
211
  const failure = cleanProcessText(linked.stderr || linked.stdout)
203
212
  ?? `openclaw plugins install exited with status ${linked.status}`;
204
- if (!/plugin already exists:/i.test(failure)) {
213
+ const canRetryWithForce = /plugin already exists:/i.test(failure) ||
214
+ /install cancelled;\s*rerun with --force\b/i.test(failure);
215
+ if (!canRetryWithForce) {
205
216
  throw new Error(failure);
206
217
  }
207
218
  runCheckedCommand(openclawBin, ["plugins", "install", "--force", root], {
@@ -547,7 +558,7 @@ function selectTerminateTarget({ plan, session, activeSessions, expectedPid, all
547
558
  }
548
559
  async function listActiveSessionsWithTerminalControl(provider, options, terminalProvider = createTerminalControlProvider(options)) {
549
560
  const activeSessions = await provider.listActiveSessions();
550
- return enrichActiveProcessesWithTerminalControl(activeSessions, terminalProvider);
561
+ return createTerminalAgentBridge(options, terminalProvider).attachProcesses(provider.agent, activeSessions);
551
562
  }
552
563
  function createTerminalControlProvider(options) {
553
564
  if (options.terminalsJson || options.terminalScreensJson || options.processesJson) {
@@ -558,6 +569,57 @@ function createTerminalControlProvider(options) {
558
569
  }
559
570
  return new TmuxTerminalControlProvider();
560
571
  }
572
+ function createTerminalProcessSource(options) {
573
+ if (options.processesJson) {
574
+ return new StaticTerminalProcessSource(parseJsonOption(options.processesJson, "--processes-json"));
575
+ }
576
+ return new SystemTerminalProcessSource();
577
+ }
578
+ function createRuntimeTerminalAgentRegistry(options) {
579
+ return createProductionTerminalAgentRegistry({
580
+ overrides: [createCodexTerminalAgentAdapter({
581
+ async detectDurableCompletion(request) {
582
+ const runtime = isRecord(request.context) ? request.context : undefined;
583
+ const conversation = runtime?.conversation;
584
+ const nativeTakeover = isRecord(runtime?.nativeTakeover)
585
+ ? runtime?.nativeTakeover
586
+ : undefined;
587
+ if (!isRecord(conversation)) {
588
+ return undefined;
589
+ }
590
+ const contextMatch = await loadCodexTerminalContext({
591
+ conversation,
592
+ nativeTakeover,
593
+ options
594
+ });
595
+ if (!contextMatch?.context) {
596
+ return undefined;
597
+ }
598
+ const evidence = detectCodexDurableCompletion({
599
+ ...request,
600
+ context: contextMatch.context
601
+ });
602
+ return evidence
603
+ ? {
604
+ ...evidence,
605
+ confidence: contextMatch.confidence,
606
+ metadata: {
607
+ ...evidence.metadata,
608
+ context_match: contextMatch.match,
609
+ session: contextMatch.context.source
610
+ }
611
+ }
612
+ : undefined;
613
+ }
614
+ })]
615
+ });
616
+ }
617
+ function createTerminalAgentBridge(options, terminalProvider = createTerminalControlProvider(options)) {
618
+ return new TerminalAgentBridge({
619
+ registry: createRuntimeTerminalAgentRegistry(options),
620
+ terminalProvider
621
+ });
622
+ }
561
623
  function planTerminalControlTakeover(session, activeSessions) {
562
624
  const matched = activeSessions
563
625
  .filter((process) => process.kind === "codex_cli" &&
@@ -607,6 +669,9 @@ function terminalControlFromTakeover(nativeTakeover) {
607
669
  if (!target || !session || !Number.isInteger(window) || !Number.isInteger(pane) || !Number.isInteger(panePid)) {
608
670
  return undefined;
609
671
  }
672
+ const storedCapabilities = Array.isArray(terminalControl.capabilities)
673
+ ? terminalControl.capabilities.filter(isTerminalControlCapability)
674
+ : [];
610
675
  return {
611
676
  kind: "tmux",
612
677
  target,
@@ -617,181 +682,28 @@ function terminalControlFromTakeover(nativeTakeover) {
617
682
  currentCommand: stringValue(terminalControl.currentCommand),
618
683
  currentPath: stringValue(terminalControl.currentPath),
619
684
  socketPath: stringValue(terminalControl.socketPath),
620
- capabilities: ["screen_status", "send_keys", "terminal_approval"]
621
- };
622
- }
623
- function detectCodexApprovalPrompt(screen) {
624
- const prompt = codexApprovalPromptRegion(screen);
625
- if (!prompt.visible) {
626
- return {
627
- approvable: false,
628
- reason: prompt.reason
629
- };
630
- }
631
- for (const line of prompt.region.split(/\r?\n/)) {
632
- const match = /^[\s›]*1\.\s+(Yes,[^(]+)\(([^)]+)\)/u.exec(line.trim());
633
- if (!match) {
634
- continue;
635
- }
636
- const key = match[2].trim();
637
- if (key !== "y") {
638
- return {
639
- approvable: false,
640
- reason: `primary approval shortcut is ${key}, not y`,
641
- ...approvalCandidateFromPrompt(prompt.marker, prompt.region)
642
- };
643
- }
644
- return {
645
- approvable: true,
646
- key,
647
- label: match[1].trim(),
648
- ...approvalCandidateFromPrompt(prompt.marker, prompt.region)
649
- };
650
- }
651
- return {
652
- approvable: false,
653
- reason: "no primary approve option with a shortcut was detected",
654
- ...approvalCandidateFromPrompt(prompt.marker, prompt.region)
655
- };
656
- }
657
- function isCodexApprovalPromptVisible(screen) {
658
- return codexApprovalPromptRegion(screen).visible;
659
- }
660
- function codexApprovalPromptRegion(screen) {
661
- const approvalMarkers = [
662
- "Would you like to run the following command?",
663
- "Would you like to make the following edits?",
664
- "Would you like to grant these permissions?",
665
- "needs your approval."
666
- ];
667
- const lines = screen.split(/\r?\n/);
668
- let markerIndex = -1;
669
- let matchedMarker = "";
670
- for (let index = lines.length - 1; index >= 0; index -= 1) {
671
- const marker = approvalMarkers.find((candidate) => lines[index].includes(candidate));
672
- if (marker) {
673
- markerIndex = index;
674
- matchedMarker = marker;
675
- break;
676
- }
677
- }
678
- if (markerIndex < 0) {
679
- return {
680
- visible: false,
681
- reason: "no Codex approval prompt was detected in the terminal screen"
682
- };
683
- }
684
- const regionLines = lines.slice(markerIndex);
685
- const staleLine = regionLines.slice(1).find((line) => isPostApprovalActivityLine(line));
686
- if (staleLine) {
687
- return {
688
- visible: false,
689
- reason: `Codex approval prompt appears stale after later terminal activity: ${staleLine.trim()}`
690
- };
691
- }
692
- return {
693
- visible: true,
694
- region: regionLines.join("\n"),
695
- marker: matchedMarker
696
- };
697
- }
698
- function approvalCandidateFromPrompt(marker, region) {
699
- const promptKind = marker === "Would you like to run the following command?"
700
- ? "run_command"
701
- : marker === "Would you like to make the following edits?"
702
- ? "file_edit"
703
- : marker === "Would you like to grant these permissions?"
704
- ? "grant_permissions"
705
- : "unknown";
706
- return {
707
- promptKind,
708
- command: promptKind === "run_command" ? commandFromApprovalRegion(region) : undefined
709
- };
710
- }
711
- function commandFromApprovalRegion(region) {
712
- const lines = region.split(/\r?\n/);
713
- const commandStart = lines.findIndex((line) => /^\s*\$\s+/u.test(line));
714
- if (commandStart < 0) {
715
- return undefined;
716
- }
717
- const parts = [];
718
- for (let index = commandStart; index < lines.length; index += 1) {
719
- const line = lines[index];
720
- if (index > commandStart && (!line.trim() || /^[\s›]*\d+\.\s+/u.test(line) || /Press enter to confirm/u.test(line))) {
721
- break;
722
- }
723
- parts.push(index === commandStart ? line.replace(/^\s*\$\s+/u, "").trim() : line.trim());
724
- }
725
- const command = parts.filter(Boolean).join(" ").trim();
726
- return command ? redactString(command) : undefined;
727
- }
728
- function isPostApprovalActivityLine(line) {
729
- const trimmed = line.trim();
730
- if (!trimmed) {
731
- return false;
732
- }
733
- if (/^✔\s+You approved\b/u.test(trimmed)) {
734
- return true;
735
- }
736
- if (/^›\s+(?!1\.)\S/u.test(trimmed)) {
737
- return true;
738
- }
739
- if (/^•\s+(Working|Ran|Explored|Edited|Read|Called|Searching|Planning|Updated|Added|Deleted|Modified|Running|Thinking)\b/u.test(trimmed)) {
740
- return true;
741
- }
742
- return /^─\s*Worked for\b/u.test(trimmed);
743
- }
744
- function detectCodexActivityState(screen, approval = detectCodexApprovalPrompt(screen)) {
745
- if (approval.approvable || isCodexApprovalPromptVisible(screen)) {
746
- return {
747
- state: "awaiting_approval",
748
- reason: "current Codex approval prompt is visible"
749
- };
750
- }
751
- const tailLines = screen.trimEnd().split(/\r?\n/).slice(-30);
752
- const workingLine = tailLines.find((line) => isCodexWorkingLine(line));
753
- if (workingLine) {
754
- return {
755
- state: "working",
756
- reason: `Codex working marker detected: ${workingLine.trim()}`
757
- };
758
- }
759
- const idleLine = tailLines
760
- .slice(-6)
761
- .map((line) => line.trim())
762
- .find((line) => isCodexIdlePromptLine(line));
763
- if (idleLine) {
764
- return {
765
- state: "idle",
766
- reason: `Codex input prompt detected: ${idleLine}`
767
- };
768
- }
769
- return {
770
- state: "unknown",
771
- reason: "no current Codex working, idle, or approval marker was detected in the terminal screen"
685
+ // State written before adapter capabilities were persisted always represented Codex.
686
+ capabilities: storedCapabilities.length > 0
687
+ ? storedCapabilities
688
+ : [
689
+ "screen_status",
690
+ "send_keys",
691
+ "terminal_approval",
692
+ "screen_completion",
693
+ "durable_completion",
694
+ "terminal_cancel"
695
+ ]
772
696
  };
773
697
  }
774
- function isCodexWorkingLine(line) {
775
- const trimmed = line.trim();
776
- if (!trimmed) {
777
- return false;
778
- }
779
- if (/^•\s+Working\b/u.test(trimmed) && (/\besc to interrupt\b/u.test(trimmed) || trimmed === "• Working")) {
780
- return true;
781
- }
782
- if (/^•\s+Waiting for background terminals?\b(?:\s*·|$)/u.test(trimmed)) {
783
- return true;
784
- }
785
- return /^\d+\s+background terminals? running\b/u.test(trimmed) && /\/(?:ps|stop)\b/u.test(trimmed);
786
- }
787
- function isCodexIdlePromptLine(line) {
788
- const trimmed = line.trim();
789
- return /^›(?:\s|$)/u.test(trimmed) && !/^›\s*1\./u.test(trimmed);
790
- }
791
- function screenExcerpt(screen, maxLength = 4000) {
792
- const lines = screen.trimEnd().split(/\r?\n/);
793
- const excerpt = lines.slice(Math.max(0, lines.length - 80)).join("\n");
794
- return redactString(excerpt).slice(-maxLength);
698
+ function isTerminalControlCapability(value) {
699
+ return typeof value === "string" && [
700
+ "screen_status",
701
+ "send_keys",
702
+ "terminal_approval",
703
+ "screen_completion",
704
+ "durable_completion",
705
+ "terminal_cancel"
706
+ ].includes(value);
795
707
  }
796
708
  function createForkConversation({ agent, strategy, session, contextPackage, forkSummary, modelInfo, options }) {
797
709
  const workspace = session.cwd;
@@ -1533,57 +1445,59 @@ async function buildNativeListGroups({ options, agentFilter, statusFilter }) {
1533
1445
  }
1534
1446
  };
1535
1447
  }
1536
- if (agentFilter && agentFilter !== "codex") {
1448
+ const registry = createRuntimeTerminalAgentRegistry(options);
1449
+ const adapters = agentFilter
1450
+ ? [registry.get(agentFilter)].filter((adapter) => adapter !== undefined)
1451
+ : registry.list();
1452
+ if (agentFilter && adapters.length === 0) {
1537
1453
  return {
1538
1454
  ...empty,
1539
1455
  summary: {
1540
1456
  enabled: true,
1541
1457
  agents: [],
1542
- skipped: `native active discovery is not implemented for ${agentFilter}`
1458
+ skipped: `terminal agent adapter is not registered for ${agentFilter}`
1543
1459
  }
1544
1460
  };
1545
1461
  }
1462
+ const terminalProvider = createTerminalControlProvider(options);
1463
+ const bridge = new TerminalAgentBridge({ registry, terminalProvider });
1464
+ const terminalScan = options.terminalDebug ? await terminalControlDiagnostics(terminalProvider) : undefined;
1465
+ const terminalControlled = [];
1466
+ const native = [];
1467
+ let activeCount = 0;
1468
+ const errors = [];
1546
1469
  try {
1547
- const provider = createAgentSessionProvider("codex", options);
1548
- const terminalProvider = createTerminalControlProvider(options);
1549
- const terminalScan = options.terminalDebug ? await terminalControlDiagnostics(terminalProvider) : undefined;
1550
- const activeSessions = await listActiveSessionsWithTerminalControl(provider, options, terminalProvider);
1470
+ const processSource = createTerminalProcessSource(options);
1471
+ const snapshots = await processSource.listProcessSnapshots((snapshot) => adapters.some((adapter) => adapter.capabilities.processDiscovery && adapter.classifyProcess(snapshot) !== undefined));
1472
+ const activeSessions = await bridge.listProcesses(snapshots, adapters.map((adapter) => adapter.agent));
1473
+ activeCount = activeSessions.length;
1551
1474
  const rootSessions = rootActiveProcesses(activeSessions);
1552
- const terminalControlled = [];
1553
- const native = [];
1554
1475
  for (const session of rootSessions) {
1555
1476
  if (session.terminalControl) {
1556
- terminalControlled.push(await terminalControlledListEntry(session, activeSessions, options));
1477
+ terminalControlled.push(await terminalControlledListEntry(session, activeSessions, options, bridge));
1557
1478
  }
1558
1479
  else {
1559
1480
  native.push(nativeListEntry(session, activeSessions));
1560
1481
  }
1561
1482
  }
1562
- return {
1563
- native,
1564
- terminalControlled,
1565
- summary: {
1566
- enabled: true,
1567
- agents: ["codex"],
1568
- active_count: activeSessions.length,
1569
- native_count: native.length,
1570
- terminal_controlled_count: terminalControlled.length,
1571
- approval_scan: options.noApprovalScan ? "disabled" : "enabled",
1572
- terminal_scan: terminalScan
1573
- }
1574
- };
1575
1483
  }
1576
1484
  catch (error) {
1577
- return {
1578
- native: [],
1579
- terminalControlled: [],
1580
- summary: {
1581
- enabled: true,
1582
- agents: ["codex"],
1583
- error: error instanceof Error ? error.message : String(error)
1584
- }
1585
- };
1485
+ errors.push(error instanceof Error ? error.message : String(error));
1586
1486
  }
1487
+ return {
1488
+ native,
1489
+ terminalControlled,
1490
+ summary: {
1491
+ enabled: true,
1492
+ agents: adapters.map((adapter) => adapter.agent),
1493
+ active_count: activeCount,
1494
+ native_count: native.length,
1495
+ terminal_controlled_count: terminalControlled.length,
1496
+ approval_scan: options.noApprovalScan ? "disabled" : "enabled",
1497
+ terminal_scan: terminalScan,
1498
+ error: errors.length > 0 ? errors.join("; ") : undefined
1499
+ }
1500
+ };
1587
1501
  }
1588
1502
  async function terminalControlDiagnostics(provider) {
1589
1503
  if (provider instanceof TmuxTerminalControlProvider) {
@@ -1610,9 +1524,9 @@ function delegatedListEntry(task) {
1610
1524
  }
1611
1525
  function nativeListEntry(session, activeSessions) {
1612
1526
  return {
1613
- id: `native:codex:${session.pid}`,
1527
+ id: `native:${session.agent}:${session.pid}`,
1614
1528
  source: "native_active",
1615
- agent: "codex",
1529
+ agent: session.agent,
1616
1530
  status: "active",
1617
1531
  pid: session.pid,
1618
1532
  child_pids: childPidsForRoot(session, activeSessions),
@@ -1635,16 +1549,16 @@ function nativeListEntry(session, activeSessions) {
1635
1549
  }
1636
1550
  };
1637
1551
  }
1638
- async function terminalControlledListEntry(session, activeSessions, options) {
1552
+ async function terminalControlledListEntry(session, activeSessions, options, bridge = createTerminalAgentBridge(options)) {
1639
1553
  const terminalControl = session.terminalControl;
1640
1554
  if (!terminalControl) {
1641
1555
  throw new Error(`process ${session.pid} is not terminal-controlled`);
1642
1556
  }
1643
- const terminalState = await listStateForTerminal(terminalControl, options);
1557
+ const terminalState = await listStateForTerminal(session.agent, terminalControl, options, bridge);
1644
1558
  return {
1645
- id: `terminal:${terminalControl.kind}:${terminalControl.target}:${session.pid}`,
1559
+ id: bridge.terminalConversationId(session),
1646
1560
  source: "terminal_control",
1647
- agent: "codex",
1561
+ agent: session.agent,
1648
1562
  status: "active",
1649
1563
  pid: session.pid,
1650
1564
  child_pids: childPidsForRoot(session, activeSessions),
@@ -1661,14 +1575,15 @@ async function terminalControlledListEntry(session, activeSessions, options) {
1661
1575
  activity_reason: terminalState.activity_reason,
1662
1576
  commands: {
1663
1577
  send: true,
1664
- approve: terminalState.approval_state.approvable === true,
1578
+ approve: terminalControl.capabilities.includes("terminal_approval") &&
1579
+ terminalState.approval_state.approvable === true,
1665
1580
  status: true,
1666
- cancel: true,
1581
+ cancel: terminalControl.capabilities.includes("terminal_cancel"),
1667
1582
  close: false
1668
1583
  }
1669
1584
  };
1670
1585
  }
1671
- async function listStateForTerminal(terminalControl, options) {
1586
+ async function listStateForTerminal(agent, terminalControl, options, bridge = createTerminalAgentBridge(options)) {
1672
1587
  if (options.noApprovalScan) {
1673
1588
  return {
1674
1589
  approval_state: {
@@ -1682,27 +1597,17 @@ async function listStateForTerminal(terminalControl, options) {
1682
1597
  };
1683
1598
  }
1684
1599
  try {
1685
- const screen = await createTerminalControlProvider(options).capture(terminalControl.target, {
1686
- scrollbackLines: Number(options.scrollbackLines ?? 120),
1687
- socketPath: terminalControl.socketPath
1600
+ const status = await bridge.status(agent, terminalControl, {
1601
+ scrollbackLines: Number(options.scrollbackLines ?? 120)
1688
1602
  });
1689
- const approval = detectCodexApprovalPrompt(screen);
1690
- const blocked = isCodexApprovalPromptVisible(screen);
1691
- const activity = detectCodexActivityState(screen, approval);
1692
1603
  return {
1693
1604
  approval_state: {
1694
- scanned: true,
1695
- blocked,
1696
- approvable: approval.approvable,
1697
- key: approval.approvable ? approval.key : undefined,
1698
- label: approval.approvable ? approval.label : undefined,
1699
- prompt_kind: approval.promptKind,
1700
- command: approval.command,
1701
- reason: approval.approvable ? undefined : approval.reason,
1702
- screen_excerpt: blocked ? screenExcerpt(screen, 1000) : undefined
1605
+ ...status.approval_state,
1606
+ screen_excerpt: status.approval_state.blocked ? status.screen.excerpt?.slice(-1000) : undefined
1703
1607
  },
1704
- activity_state: activity.state,
1705
- activity_reason: activity.reason
1608
+ activity_state: status.activity_state,
1609
+ activity_reason: status.activity_reason,
1610
+ capability_limitation: status.capability_limitation
1706
1611
  };
1707
1612
  }
1708
1613
  catch (error) {
@@ -1719,11 +1624,13 @@ async function listStateForTerminal(terminalControl, options) {
1719
1624
  }
1720
1625
  }
1721
1626
  function rootActiveProcesses(processes) {
1722
- const pids = new Set(processes.map((process) => process.pid));
1723
- const roots = processes.filter((process) => !process.ppid || !pids.has(process.ppid));
1627
+ const pids = new Set(processes.map((process) => `${process.agent}:${process.pid}`));
1628
+ const roots = processes.filter((process) => !process.ppid || !pids.has(`${process.agent}:${process.ppid}`));
1724
1629
  const seenTerminalTargets = new Set();
1725
1630
  return roots.filter((process) => {
1726
- const terminalTarget = process.terminalControl?.target;
1631
+ const terminalTarget = process.terminalControl?.target
1632
+ ? `${process.agent}:${process.terminalControl.target}`
1633
+ : undefined;
1727
1634
  if (!terminalTarget) {
1728
1635
  return true;
1729
1636
  }
@@ -1736,50 +1643,14 @@ function rootActiveProcesses(processes) {
1736
1643
  }
1737
1644
  function childPidsForRoot(root, processes) {
1738
1645
  return processes
1739
- .filter((process) => process.ppid === root.pid)
1646
+ .filter((process) => process.agent === root.agent && process.ppid === root.pid)
1740
1647
  .map((process) => process.pid);
1741
1648
  }
1742
1649
  function canSendDelegated(status) {
1743
1650
  return !["failed", "closed", "cancelled"].includes(status);
1744
1651
  }
1745
1652
  async function resolveTerminalConversationFromOptions(options) {
1746
- const parsed = parseTerminalConversationId(stringValue(options.conversation));
1747
- if (!parsed) {
1748
- return undefined;
1749
- }
1750
- const provider = createTerminalControlProvider(options);
1751
- const panes = await provider.listPanes();
1752
- const pane = panes.find((candidate) => candidate.kind === parsed.kind &&
1753
- candidate.target === parsed.target);
1754
- if (!pane) {
1755
- throw new Error(`terminal-controlled session ${parsed.conversationId} is no longer available`);
1756
- }
1757
- return {
1758
- conversationId: parsed.conversationId,
1759
- terminalControl: terminalRefFromPane(pane)
1760
- };
1761
- }
1762
- function parseTerminalConversationId(conversationId) {
1763
- const prefix = "terminal:tmux:";
1764
- if (!conversationId?.startsWith(prefix)) {
1765
- return undefined;
1766
- }
1767
- const rest = conversationId.slice(prefix.length);
1768
- const pidSeparator = rest.lastIndexOf(":");
1769
- if (pidSeparator <= 0 || pidSeparator === rest.length - 1) {
1770
- throw new Error(`invalid terminal-controlled conversation id: ${conversationId}`);
1771
- }
1772
- const target = rest.slice(0, pidSeparator);
1773
- const pid = Number(rest.slice(pidSeparator + 1));
1774
- if (!target || !Number.isInteger(pid)) {
1775
- throw new Error(`invalid terminal-controlled conversation id: ${conversationId}`);
1776
- }
1777
- return {
1778
- conversationId,
1779
- kind: "tmux",
1780
- target,
1781
- pid
1782
- };
1653
+ return createTerminalAgentBridge(options).resolveConversationId(stringValue(options.conversation ?? options.conversationId));
1783
1654
  }
1784
1655
  function parseNativeConversationId(conversationId) {
1785
1656
  const prefix = "native:codex:";
@@ -1800,7 +1671,7 @@ async function runStatus(options) {
1800
1671
  cleanupIdleConversations(storeDirFromOptions(options), options);
1801
1672
  const terminalConversation = await resolveTerminalConversationFromOptions(options);
1802
1673
  if (terminalConversation) {
1803
- const terminalStatus = await terminalStatusForControl(terminalConversation.terminalControl, options);
1674
+ const terminalStatus = await terminalStatusForControl(terminalConversation.agent, terminalConversation.terminalControl, options);
1804
1675
  printJson({
1805
1676
  conversation_id: terminalConversation.conversationId,
1806
1677
  source: "terminal_control",
@@ -1830,8 +1701,9 @@ async function runStatus(options) {
1830
1701
  }
1831
1702
  const terminalControl = terminalControlFromTakeover(isRecord(conversation.native_session_takeover) ? conversation.native_session_takeover : undefined);
1832
1703
  if (terminalControl) {
1704
+ const executor = executorForConversation(conversation);
1833
1705
  result.terminal_control = terminalControl;
1834
- result.terminal_status = await terminalStatusForControl(terminalControl, options);
1706
+ result.terminal_status = await terminalStatusForControl(executor.kind, terminalControl, options);
1835
1707
  result.terminal_screen = result.terminal_status.screen;
1836
1708
  }
1837
1709
  printJson(result);
@@ -1849,8 +1721,27 @@ async function runDescribe(options) {
1849
1721
  const conversationId = required(options.conversation ?? options.conversationId, "--conversation is required");
1850
1722
  const terminalConversation = await resolveTerminalConversationFromOptions(options);
1851
1723
  if (terminalConversation) {
1852
- const terminalStatus = await terminalStatusForControl(terminalConversation.terminalControl, options);
1853
- const process = await activeCodexProcessForPid(options, parseTerminalConversationId(conversationId)?.pid);
1724
+ const terminalStatus = await terminalStatusForControl(terminalConversation.agent, terminalConversation.terminalControl, options);
1725
+ if (terminalConversation.agent !== "codex") {
1726
+ const adapter = createRuntimeTerminalAgentRegistry(options).require(terminalConversation.agent);
1727
+ printJson({
1728
+ conversation_id: conversationId,
1729
+ source: "terminal_control",
1730
+ agent: terminalConversation.agent,
1731
+ confidence: terminalStatus.reachable ? "medium" : "low",
1732
+ about: terminalStatus.reachable
1733
+ ? `${adapter.displayName} is attached through ${terminalConversation.terminalControl.kind}:${terminalConversation.terminalControl.target}.`
1734
+ : `${adapter.displayName} terminal status is unavailable.`,
1735
+ evidence: {
1736
+ terminal_status: terminalStatus,
1737
+ terminal_screen: terminalStatus.screen
1738
+ },
1739
+ limitations: ["Historical session context is not available for this terminal adapter."],
1740
+ terminal_control: terminalConversation.terminalControl
1741
+ });
1742
+ return;
1743
+ }
1744
+ const process = await activeCodexProcessForPid(options, terminalConversation.pid);
1854
1745
  printJson(await describeNativeCodexSession({
1855
1746
  id: conversationId,
1856
1747
  source: "terminal_control",
@@ -1875,7 +1766,9 @@ async function runDescribe(options) {
1875
1766
  const { conversation, statePath, logPath } = loadConversationFromOptions(options);
1876
1767
  const events = readExistingEvents(logPath);
1877
1768
  const terminalControl = terminalControlFromTakeover(isRecord(conversation.native_session_takeover) ? conversation.native_session_takeover : undefined);
1878
- const terminalStatus = terminalControl ? await terminalStatusForControl(terminalControl, options) : undefined;
1769
+ const terminalStatus = terminalControl
1770
+ ? await terminalStatusForControl(executorForConversation(conversation).kind, terminalControl, options)
1771
+ : undefined;
1879
1772
  printJson({
1880
1773
  conversation_id: conversation.conversation_id,
1881
1774
  source: "akk_managed",
@@ -1893,110 +1786,63 @@ async function runDescribe(options) {
1893
1786
  event_log_path: logPath
1894
1787
  });
1895
1788
  }
1896
- async function terminalStatusForControl(terminalControl, options) {
1897
- try {
1898
- const screen = await createTerminalControlProvider(options).capture(terminalControl.target, {
1899
- scrollbackLines: Number(options.scrollbackLines ?? 120),
1900
- socketPath: terminalControl.socketPath
1901
- });
1902
- const approval = detectCodexApprovalPrompt(screen);
1903
- const blocked = isCodexApprovalPromptVisible(screen);
1904
- const activity = detectCodexActivityState(screen, approval);
1905
- return {
1906
- provider: terminalControl.kind,
1907
- target: terminalControl.target,
1908
- reachable: true,
1909
- activity_state: activity.state,
1910
- activity_reason: activity.reason,
1911
- approval_state: {
1912
- scanned: true,
1913
- blocked,
1914
- approvable: approval.approvable,
1915
- key: approval.approvable ? approval.key : undefined,
1916
- label: approval.approvable ? approval.label : undefined,
1917
- prompt_kind: approval.promptKind,
1918
- command: approval.command,
1919
- reason: approval.approvable ? undefined : approval.reason
1920
- },
1921
- screen: {
1922
- excerpt: screenExcerpt(screen),
1923
- approval
1924
- }
1925
- };
1926
- }
1927
- catch (error) {
1928
- return {
1929
- provider: terminalControl.kind,
1930
- target: terminalControl.target,
1931
- reachable: false,
1932
- activity_state: "unknown",
1933
- activity_reason: error instanceof Error ? error.message : String(error),
1934
- approval_state: {
1935
- scanned: false,
1936
- blocked: false,
1937
- approvable: false
1938
- },
1939
- screen: {
1940
- error: error instanceof Error ? error.message : String(error)
1941
- }
1942
- };
1943
- }
1789
+ async function terminalStatusForControl(agent, terminalControl, options) {
1790
+ return createTerminalAgentBridge(options).status(agent, terminalControl, {
1791
+ scrollbackLines: Number(options.scrollbackLines ?? 120)
1792
+ });
1944
1793
  }
1945
1794
  function terminalBridgeApprovalFingerprint({ terminalControl, terminalStatus }) {
1946
1795
  const approval = isRecord(terminalStatus?.approval_state) ? terminalStatus.approval_state : {};
1796
+ const adapterFingerprint = stringValue(approval.fingerprint);
1797
+ if (adapterFingerprint) {
1798
+ return adapterFingerprint;
1799
+ }
1947
1800
  const screen = isRecord(terminalStatus?.screen) ? terminalStatus.screen : {};
1948
1801
  return createHash("sha256")
1949
1802
  .update(JSON.stringify({
1950
1803
  target: terminalControl.target,
1951
- key: approval.key,
1804
+ keys: approval.keys ?? (approval.key ? [approval.key] : undefined),
1952
1805
  label: approval.label,
1953
1806
  prompt_kind: approval.prompt_kind,
1954
1807
  command: approval.command,
1955
1808
  excerpt: screen.excerpt
1956
1809
  }))
1957
- .digest("hex")
1958
- .slice(0, 16);
1959
- }
1960
- function terminalBridgeApprovalFingerprintForScreen({ terminalControl, screen, approval }) {
1961
- return terminalBridgeApprovalFingerprint({
1962
- terminalControl,
1963
- terminalStatus: {
1964
- approval_state: {
1965
- key: approval.key,
1966
- label: approval.label,
1967
- prompt_kind: approval.promptKind,
1968
- command: approval.command
1969
- },
1970
- screen: {
1971
- excerpt: screenExcerpt(screen)
1972
- }
1973
- }
1974
- });
1810
+ .digest("hex");
1975
1811
  }
1976
1812
  function terminalBridgeApprovalInstructions({ conversation, terminalControl, terminalStatus }) {
1977
1813
  const approval = isRecord(terminalStatus?.approval_state) ? terminalStatus.approval_state : {};
1978
1814
  const screen = isRecord(terminalStatus?.screen) ? terminalStatus.screen : {};
1979
- const label = stringValue(approval.label) || "the current Codex approval prompt";
1980
- const key = stringValue(approval.key) || "the detected approve key";
1815
+ const executor = executorForConversation(conversation);
1816
+ const agentName = executorDefinitionForKind(executor.kind).displayName;
1817
+ const label = stringValue(approval.label) || `the current ${agentName} approval prompt`;
1818
+ const keys = Array.isArray(approval.keys)
1819
+ ? approval.keys.filter((value) => typeof value === "string")
1820
+ : [];
1821
+ const keyDescription = keys.length > 0
1822
+ ? keys.join(" then ")
1823
+ : stringValue(approval.key) || "the detected approve key sequence";
1824
+ const fingerprint = stringValue(approval.fingerprint);
1981
1825
  const excerpt = stringValue(screen.excerpt) || "(No terminal excerpt was available.)";
1982
1826
  return [
1983
- "Codex is waiting for approval in a terminal-controlled AKK session.",
1827
+ `${agentName} is waiting for approval in a terminal-controlled AKK session.`,
1984
1828
  "",
1985
1829
  `Conversation: ${conversation.conversation_id}`,
1986
1830
  `Terminal: ${terminalControl.kind}:${terminalControl.target}`,
1987
- `Approval option: ${label} (${key})`,
1831
+ `Approval option: ${label} (${keyDescription})`,
1988
1832
  "",
1989
1833
  "Safe terminal excerpt:",
1990
1834
  "```text",
1991
1835
  excerpt,
1992
1836
  "```",
1993
1837
  "",
1994
- "Ask the user whether to approve or deny this Codex request.",
1838
+ `Ask the user whether to approve or deny this ${agentName} request.`,
1995
1839
  "",
1996
1840
  "If the user approves, call `agent_knock_knock_approve` with:",
1997
1841
  `- conversation_id: ${conversation.conversation_id}`,
1842
+ `- expected_approval_fingerprint: ${fingerprint ?? "(missing; refresh status before approval)"}`,
1998
1843
  "",
1999
- "Equivalent user command: `AKK approve " + conversation.conversation_id + "`",
1844
+ "Equivalent user command: `AKK approve " + conversation.conversation_id +
1845
+ (fingerprint ? ` --expected-approval-fingerprint ${fingerprint}` : "") + "`",
2000
1846
  "",
2001
1847
  "If the user denies or wants to stop this request, call `agent_knock_knock_cancel` with:",
2002
1848
  `- conversation_id: ${conversation.conversation_id}`,
@@ -2066,6 +1912,7 @@ async function runSend(options) {
2066
1912
  const managed = createManagedTerminalConversationFromRawId({
2067
1913
  options,
2068
1914
  conversationId: terminalConversation.conversationId,
1915
+ agent: terminalConversation.agent,
2069
1916
  messageBody,
2070
1917
  terminalControl: terminalConversation.terminalControl
2071
1918
  });
@@ -2085,6 +1932,7 @@ async function runSend(options) {
2085
1932
  await runTerminalConversationSend({
2086
1933
  options,
2087
1934
  conversationId: terminalConversation.conversationId,
1935
+ agent: terminalConversation.agent,
2088
1936
  messageBody,
2089
1937
  terminalControl: terminalConversation.terminalControl
2090
1938
  });
@@ -2448,6 +2296,7 @@ async function runApprove(options) {
2448
2296
  await runTerminalConversationApprove({
2449
2297
  options,
2450
2298
  conversationId: terminalConversation.conversationId,
2299
+ agent: terminalConversation.agent,
2451
2300
  terminalControl: terminalConversation.terminalControl
2452
2301
  });
2453
2302
  return;
@@ -2460,36 +2309,28 @@ async function runApprove(options) {
2460
2309
  if (!terminalControl) {
2461
2310
  throw new Error(`conversation ${conversation.conversation_id} is not controlled through a terminal`);
2462
2311
  }
2463
- const provider = createTerminalControlProvider(options);
2464
- const screen = await provider.capture(terminalControl.target, {
2465
- scrollbackLines: Number(options.scrollbackLines ?? 120),
2466
- socketPath: terminalControl.socketPath
2467
- });
2468
- const approval = detectCodexApprovalPrompt(screen);
2469
- if (!approval.approvable) {
2470
- printJson({
2471
- conversation,
2472
- approved: false,
2473
- blocked: true,
2474
- reason: approval.reason,
2475
- terminal_control: terminalControl,
2476
- screen_excerpt: screenExcerpt(screen)
2477
- });
2478
- return;
2479
- }
2480
- const expectedFingerprint = stringValue(options.expectedApprovalFingerprint);
2481
- const actualFingerprint = terminalBridgeApprovalFingerprintForScreen({ terminalControl, screen, approval });
2312
+ const executor = executorForConversation(conversation);
2313
+ const monitoredApproval = isRecord(nativeTakeover?.["terminal_bridge_approval"])
2314
+ ? nativeTakeover.terminal_bridge_approval
2315
+ : undefined;
2316
+ const expectedFingerprint = stringValue(options.expectedApprovalFingerprint) ??
2317
+ stringValue(monitoredApproval?.fingerprint);
2482
2318
  const autoApproved = options.autoApproved === true;
2483
2319
  const policyRuleId = stringValue(options.policyRuleId);
2484
2320
  const policyFingerprint = stringValue(options.policyFingerprint);
2485
- if (expectedFingerprint && actualFingerprint !== expectedFingerprint) {
2321
+ const approval = await createTerminalAgentBridge(options).approve(executor.kind, terminalControl, {
2322
+ expectedFingerprint,
2323
+ scrollbackLines: Number(options.scrollbackLines ?? 120)
2324
+ });
2325
+ const actualFingerprint = approval.fingerprint;
2326
+ if (!approval.approved) {
2486
2327
  if (autoApproved) {
2487
2328
  appendEvent(logPath, {
2488
2329
  ts: new Date().toISOString(),
2489
2330
  conversation_id: conversation.conversation_id,
2490
2331
  event: "terminal_auto_approval_decision",
2491
2332
  action: "rejected",
2492
- reason: "approval fingerprint changed before execution",
2333
+ reason: approval.reason,
2493
2334
  terminal_control: terminalControl,
2494
2335
  expected_fingerprint: expectedFingerprint,
2495
2336
  actual_fingerprint: actualFingerprint,
@@ -2500,24 +2341,22 @@ async function runApprove(options) {
2500
2341
  printJson({
2501
2342
  conversation,
2502
2343
  approved: false,
2503
- blocked: true,
2504
- reason: "approval fingerprint changed before execution",
2344
+ blocked: approval.blocked,
2345
+ reason: approval.reason,
2505
2346
  terminal_control: terminalControl,
2506
2347
  expected_approval_fingerprint: expectedFingerprint,
2507
2348
  actual_approval_fingerprint: actualFingerprint,
2508
- screen_excerpt: screenExcerpt(screen)
2349
+ screen_excerpt: approval.screenExcerpt
2509
2350
  });
2510
2351
  return;
2511
2352
  }
2512
- await provider.sendKeys(terminalControl.target, [approval.key], {
2513
- socketPath: terminalControl.socketPath
2514
- });
2515
2353
  appendEvent(logPath, {
2516
2354
  ts: new Date().toISOString(),
2517
2355
  conversation_id: conversation.conversation_id,
2518
2356
  event: "terminal_approval_send",
2519
2357
  terminal_control: terminalControl,
2520
2358
  key: approval.key,
2359
+ keys: approval.keys,
2521
2360
  label: approval.label,
2522
2361
  approval_fingerprint: actualFingerprint,
2523
2362
  auto_approved: autoApproved,
@@ -2540,6 +2379,7 @@ async function runApprove(options) {
2540
2379
  conversation_id: conversation.conversation_id,
2541
2380
  terminal_target: terminalControl.target,
2542
2381
  key: approval.key,
2382
+ keys: approval.keys,
2543
2383
  label: approval.label,
2544
2384
  approval_fingerprint: actualFingerprint,
2545
2385
  auto_approved: autoApproved,
@@ -2605,6 +2445,7 @@ async function runApprove(options) {
2605
2445
  approved: true,
2606
2446
  terminal_control: terminalControl,
2607
2447
  key: approval.key,
2448
+ keys: approval.keys,
2608
2449
  label: approval.label,
2609
2450
  approval_fingerprint: actualFingerprint,
2610
2451
  auto_approved: autoApproved,
@@ -2612,32 +2453,29 @@ async function runApprove(options) {
2612
2453
  monitor_pid: bridgeMonitor?.pid ?? null
2613
2454
  });
2614
2455
  }
2615
- async function runTerminalConversationApprove({ options, conversationId, terminalControl }) {
2616
- const provider = createTerminalControlProvider(options);
2617
- const screen = await provider.capture(terminalControl.target, {
2618
- scrollbackLines: Number(options.scrollbackLines ?? 120),
2619
- socketPath: terminalControl.socketPath
2456
+ async function runTerminalConversationApprove({ options, conversationId, agent, terminalControl }) {
2457
+ const approval = await createTerminalAgentBridge(options).approve(agent, terminalControl, {
2458
+ expectedFingerprint: stringValue(options.expectedApprovalFingerprint),
2459
+ scrollbackLines: Number(options.scrollbackLines ?? 120)
2620
2460
  });
2621
- const approval = detectCodexApprovalPrompt(screen);
2622
- if (!approval.approvable) {
2461
+ if (!approval.approved) {
2623
2462
  printJson({
2624
2463
  conversation_id: conversationId,
2625
2464
  source: "terminal_control",
2626
2465
  approved: false,
2627
- blocked: true,
2466
+ blocked: approval.blocked,
2628
2467
  reason: approval.reason,
2629
2468
  terminal_control: terminalControl,
2630
- screen_excerpt: screenExcerpt(screen)
2469
+ screen_excerpt: approval.screenExcerpt
2631
2470
  });
2632
2471
  return;
2633
2472
  }
2634
- await provider.sendKeys(terminalControl.target, [approval.key], {
2635
- socketPath: terminalControl.socketPath
2636
- });
2637
2473
  runtimeLog("info", "terminal_approval_send", {
2638
2474
  conversation_id: conversationId,
2475
+ agent,
2639
2476
  terminal_target: terminalControl.target,
2640
2477
  key: approval.key,
2478
+ keys: approval.keys,
2641
2479
  label: approval.label
2642
2480
  });
2643
2481
  printJson({
@@ -2646,20 +2484,17 @@ async function runTerminalConversationApprove({ options, conversationId, termina
2646
2484
  approved: true,
2647
2485
  terminal_control: terminalControl,
2648
2486
  key: approval.key,
2649
- label: approval.label
2487
+ keys: approval.keys,
2488
+ label: approval.label,
2489
+ approval_fingerprint: approval.fingerprint
2650
2490
  });
2651
2491
  }
2652
- async function runTerminalConversationSend({ options, conversationId, messageBody, terminalControl }) {
2653
- const provider = createTerminalControlProvider(options);
2492
+ async function runTerminalConversationSend({ options, conversationId, agent, messageBody, terminalControl }) {
2654
2493
  const terminalPayload = terminalSubmissionPayload(String(messageBody));
2655
- await provider.sendText(terminalControl.target, terminalPayload, {
2656
- socketPath: terminalControl.socketPath
2657
- });
2658
- await provider.sendKeys(terminalControl.target, ["C-m"], {
2659
- socketPath: terminalControl.socketPath
2660
- });
2494
+ await createTerminalAgentBridge(options).send(agent, terminalControl, terminalPayload);
2661
2495
  runtimeLog("info", "terminal_message_send", {
2662
2496
  conversation_id: conversationId,
2497
+ agent,
2663
2498
  terminal_target: terminalControl.target,
2664
2499
  message: textSummary(messageBody)
2665
2500
  });
@@ -2703,7 +2538,7 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
2703
2538
  releaseTerminalLock();
2704
2539
  }
2705
2540
  }
2706
- const provider = createTerminalControlProvider(options);
2541
+ const terminalBridge = createTerminalAgentBridge(options);
2707
2542
  const bridgeStartedAt = new Date().toISOString();
2708
2543
  const agentTimeoutMinutes = Number(options.agentTimeoutMinutes ?? DEFAULT_AGENT_TIMEOUT_MINUTES);
2709
2544
  const agentHardTimeoutMinutes = positiveMinutes(options.agentHardTimeoutMinutes ?? DEFAULT_AGENT_HARD_TIMEOUT_MINUTES, "--agent-hard-timeout-minutes");
@@ -2720,22 +2555,16 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
2720
2555
  let preSendScreenFingerprint;
2721
2556
  if (bridge) {
2722
2557
  try {
2723
- const screen = await provider.capture(terminalControl.target, {
2724
- scrollbackLines: Number(options.scrollbackLines ?? 120),
2725
- socketPath: terminalControl.socketPath
2558
+ const status = await terminalBridge.status(executor.kind, terminalControl, {
2559
+ scrollbackLines: Number(options.scrollbackLines ?? 120)
2726
2560
  });
2727
- preSendScreenFingerprint = terminalBridgeScreenFingerprint(screenExcerpt(screen));
2561
+ preSendScreenFingerprint = terminalBridgeScreenFingerprint(status.screen.excerpt);
2728
2562
  }
2729
2563
  catch {
2730
2564
  // Delivery can still succeed when capture is unavailable; prompt matching remains the fallback boundary.
2731
2565
  }
2732
2566
  }
2733
- await provider.sendText(terminalControl.target, terminalPayload, {
2734
- socketPath: terminalControl.socketPath
2735
- });
2736
- await provider.sendKeys(terminalControl.target, ["C-m"], {
2737
- socketPath: terminalControl.socketPath
2738
- });
2567
+ await terminalBridge.send(executor.kind, terminalControl, terminalPayload);
2739
2568
  const supersededConversationIds = bridge
2740
2569
  ? supersedeTerminalBridgeConversations({
2741
2570
  storeDir: storeDirFromOptions(options),
@@ -2825,12 +2654,12 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
2825
2654
  function terminalSubmissionPayload(payload) {
2826
2655
  return payload.replace(/[\r\n]+$/u, "");
2827
2656
  }
2828
- function createManagedTerminalConversationFromRawId({ options, conversationId, messageBody, terminalControl }) {
2657
+ function createManagedTerminalConversationFromRawId({ options, conversationId, agent, messageBody, terminalControl }) {
2829
2658
  const workspace = terminalControl.currentPath ?? process.cwd();
2830
2659
  const storeDir = expandHome(options.storeDir ?? options.logDir ?? defaultStoreDir(workspace));
2831
2660
  cleanupIdleConversations(storeDir, options);
2832
2661
  const executor = resolveExecutor({
2833
- kind: "codex",
2662
+ kind: agent,
2834
2663
  session: conversationId
2835
2664
  });
2836
2665
  const now = new Date();
@@ -2868,12 +2697,12 @@ function createManagedTerminalConversationFromRawId({ options, conversationId, m
2868
2697
  gateway_session: options.gatewaySession ?? options.openclawSession ?? "agent:main:main",
2869
2698
  openclaw_bin: options.openclawBin ?? resolveOptionalExecutable("openclaw"),
2870
2699
  executor_all_proxy: proxyForExecutor(executor, options),
2871
- executor_model: options.model ?? options.codexModel,
2700
+ executor_model: options.model ?? (agent === "codex" ? options.codexModel : undefined),
2872
2701
  native_session_takeover: {
2873
- agent: "codex",
2702
+ agent,
2874
2703
  native_session_id: conversationId,
2875
2704
  source_cwd: workspace,
2876
- source_title: `Terminal-controlled Codex ${terminalControl.target}`,
2705
+ source_title: `Terminal-controlled ${executor.display_name} ${terminalControl.target}`,
2877
2706
  strategy: "terminal_control",
2878
2707
  attached_at: now.toISOString(),
2879
2708
  takeover_match_kind: "raw_terminal_send",
@@ -2888,7 +2717,7 @@ function createManagedTerminalConversationFromRawId({ options, conversationId, m
2888
2717
  conversation_id: attachedConversation.conversation_id,
2889
2718
  event: "raw_terminal_session_attached",
2890
2719
  source_conversation_id: conversationId,
2891
- agent: "codex",
2720
+ agent,
2892
2721
  terminal_control: terminalControl,
2893
2722
  executor
2894
2723
  });
@@ -3494,6 +3323,7 @@ async function runCancel(options) {
3494
3323
  await runTerminalConversationCancel({
3495
3324
  options,
3496
3325
  conversationId: terminalConversation.conversationId,
3326
+ agent: terminalConversation.agent,
3497
3327
  terminalControl: terminalConversation.terminalControl
3498
3328
  });
3499
3329
  return;
@@ -3512,6 +3342,7 @@ async function runCancel(options) {
3512
3342
  conversation,
3513
3343
  statePath,
3514
3344
  logPath,
3345
+ agent: executorForConversation(conversation).kind,
3515
3346
  terminalControl
3516
3347
  });
3517
3348
  return;
@@ -3574,41 +3405,54 @@ async function runCancel(options) {
3574
3405
  budget: budgetAction(nextConversation)
3575
3406
  });
3576
3407
  }
3577
- async function runTerminalConversationCancel({ options, conversationId, terminalControl }) {
3578
- const provider = createTerminalControlProvider(options);
3579
- await provider.sendKeys(terminalControl.target, ["C-c"], {
3580
- socketPath: terminalControl.socketPath
3581
- });
3408
+ async function runTerminalConversationCancel({ options, conversationId, agent, terminalControl }) {
3409
+ const cancellation = await createTerminalAgentBridge(options).cancel(agent, terminalControl);
3582
3410
  runtimeLog("info", "terminal_cancel_requested", {
3583
3411
  conversation_id: conversationId,
3412
+ agent,
3584
3413
  terminal_target: terminalControl.target,
3585
- key: "C-c"
3414
+ key: cancellation.key,
3415
+ keys: cancellation.keys,
3416
+ cancel_requested: cancellation.cancelRequested,
3417
+ reason: cancellation.reason
3586
3418
  });
3587
3419
  printJson({
3588
3420
  conversation_id: conversationId,
3589
3421
  source: "terminal_control",
3590
- cancel_requested: true,
3422
+ cancel_requested: cancellation.cancelRequested,
3423
+ reason: cancellation.reason,
3591
3424
  terminal_control: terminalControl,
3592
- key: "C-c"
3425
+ key: cancellation.key,
3426
+ keys: cancellation.keys
3593
3427
  });
3594
3428
  }
3595
- async function runTerminalControlCancel({ options, conversation, statePath, logPath, terminalControl }) {
3596
- const provider = createTerminalControlProvider(options);
3597
- await provider.sendKeys(terminalControl.target, ["C-c"], {
3598
- socketPath: terminalControl.socketPath
3599
- });
3429
+ async function runTerminalControlCancel({ options, conversation, statePath, logPath, agent, terminalControl }) {
3430
+ const cancellation = await createTerminalAgentBridge(options).cancel(agent, terminalControl);
3431
+ if (!cancellation.cancelRequested) {
3432
+ printJson({
3433
+ conversation,
3434
+ cancel_requested: false,
3435
+ reason: cancellation.reason,
3436
+ terminal_control: terminalControl,
3437
+ budget: budgetAction(conversation)
3438
+ });
3439
+ return;
3440
+ }
3600
3441
  const now = new Date().toISOString();
3601
3442
  appendEvent(logPath, {
3602
3443
  ts: now,
3603
3444
  conversation_id: conversation.conversation_id,
3604
3445
  event: "terminal_cancel_requested",
3605
3446
  terminal_control: terminalControl,
3606
- key: "C-c"
3447
+ key: cancellation.key,
3448
+ keys: cancellation.keys
3607
3449
  });
3608
3450
  runtimeLog("info", "terminal_cancel_requested", {
3609
3451
  conversation_id: conversation.conversation_id,
3452
+ agent,
3610
3453
  terminal_target: terminalControl.target,
3611
- key: "C-c"
3454
+ key: cancellation.key,
3455
+ keys: cancellation.keys
3612
3456
  });
3613
3457
  const nextConversation = {
3614
3458
  ...conversation,
@@ -3620,7 +3464,8 @@ async function runTerminalControlCancel({ options, conversation, statePath, logP
3620
3464
  conversation: nextConversation,
3621
3465
  cancel_requested: true,
3622
3466
  terminal_control: terminalControl,
3623
- key: "C-c",
3467
+ key: cancellation.key,
3468
+ keys: cancellation.keys,
3624
3469
  budget: budgetAction(nextConversation)
3625
3470
  });
3626
3471
  }
@@ -3822,6 +3667,9 @@ function runClose(options) {
3822
3667
  });
3823
3668
  }
3824
3669
  async function runMonitor(options) {
3670
+ if (options.callbackRetry) {
3671
+ return runCallbackRetryMonitor(options);
3672
+ }
3825
3673
  if (options.terminalBridge) {
3826
3674
  return await runTerminalBridgeMonitor(options);
3827
3675
  }
@@ -3953,6 +3801,72 @@ async function runMonitor(options) {
3953
3801
  sleepSync(pollIntervalMs);
3954
3802
  }
3955
3803
  }
3804
+ function startCallbackRetryMonitor({ statePath }) {
3805
+ const child = spawn(process.execPath, [
3806
+ new URL(import.meta.url).pathname,
3807
+ "monitor",
3808
+ "--callback-retry",
3809
+ "--state",
3810
+ statePath
3811
+ ], {
3812
+ detached: true,
3813
+ stdio: "ignore",
3814
+ cwd: process.cwd(),
3815
+ env: process.env
3816
+ });
3817
+ child.unref();
3818
+ return child;
3819
+ }
3820
+ function runCallbackRetryMonitor(options) {
3821
+ const statePath = expandHome(required(options.state, "--state is required"));
3822
+ while (true) {
3823
+ const conversation = loadState(statePath);
3824
+ const callbackDelivery = isRecord(conversation.callback_delivery)
3825
+ ? conversation.callback_delivery
3826
+ : undefined;
3827
+ const attempts = Number(callbackDelivery?.attempts ?? 0);
3828
+ if (conversation.status !== "callback_failed" || !isRecord(callbackDelivery?.message)) {
3829
+ return;
3830
+ }
3831
+ if (attempts > CALLBACK_RETRY_DELAYS_MS.length) {
3832
+ return;
3833
+ }
3834
+ const delayMs = CALLBACK_RETRY_DELAYS_MS[Math.max(0, attempts - 1)];
3835
+ sleepSync(delayMs);
3836
+ const releaseLock = acquireFileLock(`${statePath}.lock`);
3837
+ try {
3838
+ const current = loadState(statePath);
3839
+ const currentDelivery = isRecord(current.callback_delivery)
3840
+ ? current.callback_delivery
3841
+ : undefined;
3842
+ if (current.status !== "callback_failed" || !isRecord(currentDelivery?.message)) {
3843
+ return;
3844
+ }
3845
+ try {
3846
+ runLockedCallback({
3847
+ statePath,
3848
+ messageJson: JSON.stringify(currentDelivery.message),
3849
+ gatewayMethod: stringValue(currentDelivery.gateway_method) ?? current.gateway_method,
3850
+ gatewaySession: stringValue(currentDelivery.gateway_session) ?? current.gateway_session,
3851
+ openclawSession: current.openclaw_session,
3852
+ openclawBin: stringValue(currentDelivery.openclaw_bin) ?? current.openclaw_bin,
3853
+ gatewayUrl: stringValue(currentDelivery.gateway_url) ?? current.gateway_url,
3854
+ token: current.gateway_token,
3855
+ closeTerminalBridgeOnDone: currentDelivery.close_terminal_bridge_on_done === true,
3856
+ retryPending: true,
3857
+ disableCallbackRetry: true
3858
+ });
3859
+ return;
3860
+ }
3861
+ catch {
3862
+ // The failed attempt is persisted by runLockedCallback; continue with bounded backoff.
3863
+ }
3864
+ }
3865
+ finally {
3866
+ releaseLock();
3867
+ }
3868
+ }
3869
+ }
3956
3870
  async function runTerminalBridgeMonitor(options) {
3957
3871
  const statePath = expandHome(required(options.state, "--state is required"));
3958
3872
  const logPath = expandHome(options.log ?? logPathForStatePath(statePath));
@@ -3975,9 +3889,10 @@ async function runTerminalBridgeMonitor(options) {
3975
3889
  const activityPersistIntervalMs = terminalBridgeActivityPersistIntervalMs(timeoutMinutes, pollIntervalMs);
3976
3890
  const preSendScreenFingerprint = stringValue(initialNativeTakeover?.["terminal_bridge_pre_send_screen_fingerprint"]);
3977
3891
  let previousScreenFingerprint = preSendScreenFingerprint;
3978
- let previousRolloutFingerprint;
3892
+ let previousDurableFingerprint;
3979
3893
  let persistedActivityReason = stringValue(initialNativeTakeover?.["terminal_bridge_last_activity_reason"]);
3980
3894
  const executor = executorForConversation(conversation);
3895
+ const terminalBridge = createTerminalAgentBridge(options);
3981
3896
  appendEvent(logPath, {
3982
3897
  ts: new Date().toISOString(),
3983
3898
  conversation_id: conversation.conversation_id,
@@ -4060,7 +3975,29 @@ async function runTerminalBridgeMonitor(options) {
4060
3975
  });
4061
3976
  return;
4062
3977
  }
4063
- const terminalStatus = await terminalStatusForControl(terminalControl, options);
3978
+ const requestText = String(nativeTakeover?.["terminal_bridge_request_text"] ?? conversation.user_request ?? "");
3979
+ const startedAt = stringValue(nativeTakeover?.["terminal_bridge_started_at"]);
3980
+ const screenChangedSinceSend = preSendScreenFingerprint !== undefined &&
3981
+ previousScreenFingerprint !== undefined &&
3982
+ previousScreenFingerprint !== preSendScreenFingerprint;
3983
+ const poll = await terminalBridge.monitorPoll({
3984
+ agent: executor.kind,
3985
+ terminalControl,
3986
+ screenOptions: {
3987
+ scrollbackLines: Number(options.scrollbackLines ?? 120),
3988
+ requestText,
3989
+ screenChangedSinceSend
3990
+ },
3991
+ durableRequest: {
3992
+ sessionId: stringValue(nativeTakeover?.["native_session_id"]),
3993
+ cwd: stringValue(nativeTakeover?.["source_cwd"]),
3994
+ requestText,
3995
+ requestHash: stringValue(nativeTakeover?.["terminal_bridge_request_hash"]),
3996
+ startedAt,
3997
+ context: { conversation, nativeTakeover }
3998
+ }
3999
+ });
4000
+ const terminalStatus = poll.status;
4064
4001
  const approval = terminalStatus.approval_state;
4065
4002
  if (isRecord(approval) && approval.blocked === true) {
4066
4003
  const fingerprint = terminalBridgeApprovalFingerprint({ terminalControl, terminalStatus });
@@ -4115,7 +4052,7 @@ async function runTerminalBridgeMonitor(options) {
4115
4052
  terminalStatus,
4116
4053
  fingerprint
4117
4054
  }),
4118
- approve_command: `AKK approve ${notification.conversation.conversation_id}`,
4055
+ approve_command: `AKK approve ${notification.conversation.conversation_id} --expected-approval-fingerprint ${fingerprint}`,
4119
4056
  deny_command: `AKK cancel ${notification.conversation.conversation_id}`,
4120
4057
  approve_tool: "agent_knock_knock_approve",
4121
4058
  deny_tool: "agent_knock_knock_cancel"
@@ -4153,40 +4090,22 @@ async function runTerminalBridgeMonitor(options) {
4153
4090
  const screenChanged = previousScreenFingerprint !== undefined &&
4154
4091
  screenFingerprint !== undefined &&
4155
4092
  screenFingerprint !== previousScreenFingerprint;
4156
- const screenChangedSinceSend = preSendScreenFingerprint !== undefined &&
4157
- screenFingerprint !== undefined &&
4158
- screenFingerprint !== preSendScreenFingerprint;
4159
4093
  previousScreenFingerprint = screenFingerprint;
4160
- const contextMatch = await terminalBridgeCodexContext({
4161
- conversation,
4162
- nativeTakeover,
4163
- options
4164
- });
4165
- const startedAt = stringValue(nativeTakeover?.["terminal_bridge_started_at"]);
4166
- const rolloutCompletion = contextMatch?.context
4167
- ? latestCompletedRolloutTurn({
4168
- context: contextMatch.context,
4169
- conversation,
4170
- nativeTakeover,
4171
- startedAt
4172
- })
4173
- : undefined;
4174
- const assistantMessage = contextMatch?.context
4175
- ? latestAssistantAfter(contextMatch.context, startedAt)
4176
- : undefined;
4177
- const rolloutFingerprint = rolloutCompletion || assistantMessage
4094
+ const durableCompletion = poll.durableCompletion;
4095
+ const durableFingerprint = durableCompletion
4178
4096
  ? terminalBridgeActivityFingerprint(JSON.stringify({
4179
- text: rolloutCompletion?.text ?? assistantMessage?.text,
4180
- timestamp: rolloutCompletion?.timestamp ?? assistantMessage?.timestamp,
4181
- turn_id: rolloutCompletion?.turnId
4097
+ text: durableCompletion.text,
4098
+ timestamp: durableCompletion.timestamp,
4099
+ id: durableCompletion.id,
4100
+ metadata: durableCompletion.metadata
4182
4101
  }))
4183
4102
  : undefined;
4184
- const rolloutChanged = rolloutFingerprint !== undefined && rolloutFingerprint !== previousRolloutFingerprint;
4185
- previousRolloutFingerprint = rolloutFingerprint;
4103
+ const durableChanged = durableFingerprint !== undefined && durableFingerprint !== previousDurableFingerprint;
4104
+ previousDurableFingerprint = durableFingerprint;
4186
4105
  const activityReasons = [
4187
4106
  terminalStatus.activity_state === "working" ? terminalStatus.activity_reason : undefined,
4188
4107
  screenChanged ? "terminal screen changed" : undefined,
4189
- rolloutChanged ? "Codex rollout assistant output changed" : undefined
4108
+ durableChanged ? "durable completion evidence changed" : undefined
4190
4109
  ].filter((value) => Boolean(value));
4191
4110
  if (activityReasons.length > 0) {
4192
4111
  const observedAtMs = Date.now();
@@ -4211,28 +4130,20 @@ async function runTerminalBridgeMonitor(options) {
4211
4130
  }
4212
4131
  }
4213
4132
  }
4214
- const terminalIdle = terminalStatus.activity_state === "idle";
4215
- const screenMessage = !rolloutCompletion && !assistantMessage && terminalIdle
4216
- ? terminalBridgeScreenMessage({
4217
- conversation,
4218
- nativeTakeover,
4219
- terminalStatus,
4220
- screenChangedSinceSend
4221
- })
4133
+ const completion = poll.completion;
4134
+ const completionMetadata = isRecord(completion?.metadata) ? completion.metadata : {};
4135
+ const completionMatch = completion
4136
+ ? stringValue(completionMetadata.match) ??
4137
+ (completion.source === "screen" ? "terminal_screen" : "durable_completion")
4222
4138
  : undefined;
4223
- const completion = rolloutCompletion ?? (terminalIdle ? assistantMessage ?? screenMessage : undefined);
4224
- const completionMatch = rolloutCompletion
4225
- ? "rollout_task_complete"
4226
- : assistantMessage
4227
- ? contextMatch?.match
4228
- : "terminal_screen";
4229
4139
  const completionFingerprint = completion
4230
4140
  ? createHash("sha256")
4231
4141
  .update(JSON.stringify({
4232
4142
  text: completion.text,
4233
4143
  timestamp: completion.timestamp,
4234
4144
  match: completionMatch,
4235
- turn_id: rolloutCompletion?.turnId,
4145
+ source: completion.source,
4146
+ id: completion.id,
4236
4147
  message_id: currentMessageId
4237
4148
  }))
4238
4149
  .digest("hex")
@@ -4246,9 +4157,12 @@ async function runTerminalBridgeMonitor(options) {
4246
4157
  event: "terminal_bridge_completion_detected",
4247
4158
  terminal_control: terminalControl,
4248
4159
  match: completionMatch,
4249
- codex_session: contextMatch?.context.source,
4160
+ completion_source: completion.source,
4161
+ completion_id: completion.id,
4162
+ terminal_session: completionMetadata.session,
4163
+ context_match: completionMetadata.context_match,
4250
4164
  assistant_timestamp: completion?.timestamp,
4251
- rollout_turn_id: rolloutCompletion?.turnId,
4165
+ rollout_turn_id: completion.source === "durable" ? completion.id : undefined,
4252
4166
  terminal_bridge_message_id: currentMessageId
4253
4167
  });
4254
4168
  const callbackMessage = createMessage({
@@ -4261,11 +4175,14 @@ async function runTerminalBridgeMonitor(options) {
4261
4175
  metadata: {
4262
4176
  source: "terminal_bridge",
4263
4177
  terminal_control: terminalControl,
4264
- codex_session: contextMatch?.context.source,
4265
- confidence: rolloutCompletion || assistantMessage ? contextMatch?.confidence : "screen_only",
4178
+ ...completionMetadata,
4179
+ completion_source: completion.source,
4180
+ completion_id: completion.id,
4181
+ terminal_session: completionMetadata.session,
4182
+ confidence: completion.confidence,
4266
4183
  match: completionMatch,
4267
4184
  assistant_timestamp: completion?.timestamp,
4268
- rollout_turn_id: rolloutCompletion?.turnId,
4185
+ rollout_turn_id: completion.source === "durable" ? completion.id : undefined,
4269
4186
  terminal_bridge_message_id: currentMessageId
4270
4187
  }
4271
4188
  });
@@ -4332,7 +4249,7 @@ async function runTerminalBridgeMonitor(options) {
4332
4249
  detail: {
4333
4250
  terminal_bridge: true,
4334
4251
  terminal_control: terminalControl,
4335
- match: contextMatch?.match,
4252
+ match: completionMetadata.context_match,
4336
4253
  terminal_activity_state: terminalStatus.activity_state,
4337
4254
  last_activity_at: new Date(lastActivityAtMs).toISOString(),
4338
4255
  inactivity_deadline_at: new Date(lastActivityAtMs + timeoutMinutes * 60 * 1000).toISOString(),
@@ -4468,7 +4385,7 @@ function terminalBridgeApprovalCandidate({ executor, terminalControl, terminalSt
4468
4385
  terminal_target: terminalControl.target
4469
4386
  };
4470
4387
  }
4471
- async function terminalBridgeCodexContext({ conversation, nativeTakeover, options }) {
4388
+ async function loadCodexTerminalContext({ conversation, nativeTakeover, options }) {
4472
4389
  const provider = createAgentSessionProvider("codex", options);
4473
4390
  const nativeSessionId = stringValue(nativeTakeover?.["native_session_id"]);
4474
4391
  const startedAtMs = Date.parse(String(nativeTakeover?.["terminal_bridge_started_at"] ?? ""));
@@ -4524,146 +4441,6 @@ async function terminalBridgeCodexContext({ conversation, nativeTakeover, option
4524
4441
  confidence: sessions.length === 1 ? "medium" : "low"
4525
4442
  };
4526
4443
  }
4527
- function latestAssistantAfter(context, startedAt) {
4528
- const threshold = startedAt ? Date.parse(startedAt) : undefined;
4529
- return [...visibleRolloutMessages(context)]
4530
- .reverse()
4531
- .find((message) => {
4532
- if (message.role !== "assistant") {
4533
- return false;
4534
- }
4535
- if (!Number.isFinite(threshold)) {
4536
- return true;
4537
- }
4538
- const messageTime = message.timestamp ? Date.parse(message.timestamp) : NaN;
4539
- return Number.isFinite(messageTime) && messageTime >= Number(threshold);
4540
- });
4541
- }
4542
- function latestCompletedRolloutTurn({ context, conversation, nativeTakeover, startedAt }) {
4543
- const threshold = validTimestampMs(startedAt);
4544
- const expectedRequestHash = stringValue(nativeTakeover?.["terminal_bridge_request_hash"]) ??
4545
- terminalBridgeRequestFingerprint(nativeTakeover?.["terminal_bridge_request_text"] ?? conversation.user_request);
4546
- if (threshold === undefined || !expectedRequestHash) {
4547
- return undefined;
4548
- }
4549
- const turn = [...(context.turns ?? [])]
4550
- .reverse()
4551
- .find((candidate) => {
4552
- const userTimestamp = validTimestampMs(candidate.userTimestamp);
4553
- const completedAt = validTimestampMs(candidate.completedAt);
4554
- return candidate.userTextHash === expectedRequestHash &&
4555
- userTimestamp !== undefined &&
4556
- completedAt !== undefined &&
4557
- userTimestamp >= threshold &&
4558
- completedAt >= userTimestamp &&
4559
- Boolean(candidate.lastAssistantMessage);
4560
- });
4561
- if (!turn?.lastAssistantMessage) {
4562
- return undefined;
4563
- }
4564
- return {
4565
- role: "assistant",
4566
- text: turn.lastAssistantMessage,
4567
- timestamp: turn.completedAt,
4568
- turnId: turn.turnId,
4569
- userTimestamp: turn.userTimestamp
4570
- };
4571
- }
4572
- function terminalBridgeScreenMessage({ conversation, nativeTakeover, terminalStatus, screenChangedSinceSend }) {
4573
- const excerpt = stringValue(terminalStatus?.screen?.excerpt);
4574
- if (!excerpt) {
4575
- return undefined;
4576
- }
4577
- const request = String(nativeTakeover?.["terminal_bridge_request_text"] ?? conversation.user_request ?? "").trim();
4578
- const promptEnd = request ? whitespaceInsensitiveMatchEnd(excerpt, request) : undefined;
4579
- const afterPrompt = promptEnd === undefined ? undefined : excerpt.slice(promptEnd);
4580
- const completionBoundary = afterPrompt === undefined
4581
- ? undefined
4582
- : terminalBridgeCompletionBoundary(afterPrompt);
4583
- const completionText = afterPrompt === undefined
4584
- ? screenChangedSinceSend
4585
- ? terminalBridgeLatestCompletedSegment(excerpt)
4586
- : undefined
4587
- : completionBoundary === undefined
4588
- ? afterPrompt
4589
- : afterPrompt.slice(0, completionBoundary);
4590
- if (completionText === undefined) {
4591
- return undefined;
4592
- }
4593
- const cleaned = cleanTerminalBridgeScreenText(completionText);
4594
- const hasCompletionEvidence = promptEnd === undefined || completionBoundary !== undefined || /[•└]/u.test(cleaned ?? "");
4595
- if (!cleaned || cleaned.length < 40 || !hasCompletionEvidence) {
4596
- return undefined;
4597
- }
4598
- return {
4599
- role: "assistant",
4600
- text: truncateText(cleaned, 4000),
4601
- timestamp: undefined
4602
- };
4603
- }
4604
- function whitespaceInsensitiveMatchEnd(text, expected) {
4605
- const normalizedExpected = expected.replace(/\s/gu, "");
4606
- if (!normalizedExpected) {
4607
- return undefined;
4608
- }
4609
- let normalizedText = "";
4610
- const sourceEnds = [];
4611
- for (let index = 0; index < text.length;) {
4612
- const codePoint = text.codePointAt(index);
4613
- if (codePoint === undefined) {
4614
- break;
4615
- }
4616
- const character = String.fromCodePoint(codePoint);
4617
- if (!/\s/u.test(character)) {
4618
- normalizedText += character;
4619
- for (let codeUnit = 0; codeUnit < character.length; codeUnit += 1) {
4620
- sourceEnds.push(index + character.length);
4621
- }
4622
- }
4623
- index += character.length;
4624
- }
4625
- const matchIndex = normalizedText.lastIndexOf(normalizedExpected);
4626
- if (matchIndex < 0) {
4627
- return undefined;
4628
- }
4629
- return sourceEnds[matchIndex + normalizedExpected.length - 1];
4630
- }
4631
- function terminalBridgeLatestCompletedSegment(text) {
4632
- const matches = [...text.matchAll(/^[ \t]*[─━-]+\s+Worked for\b.*$/gmu)];
4633
- const completion = matches.at(-1);
4634
- if (completion?.index === undefined) {
4635
- return undefined;
4636
- }
4637
- const previousCompletion = matches.at(-2);
4638
- let start = previousCompletion?.index === undefined
4639
- ? 0
4640
- : previousCompletion.index + previousCompletion[0].length;
4641
- const beforeCompletion = text.slice(0, completion.index);
4642
- const prompts = [...beforeCompletion.matchAll(/^[ \t]*›(?:\s|$).*$/gmu)];
4643
- const latestPrompt = prompts.at(-1);
4644
- if (latestPrompt?.index !== undefined && latestPrompt.index >= start) {
4645
- start = latestPrompt.index + latestPrompt[0].length;
4646
- }
4647
- return text.slice(start, completion.index);
4648
- }
4649
- function terminalBridgeCompletionBoundary(text) {
4650
- const matches = [...text.matchAll(/^[ \t]*[─━-]+\s+Worked for\b.*$/gmu)];
4651
- return matches.at(-1)?.index;
4652
- }
4653
- function cleanTerminalBridgeScreenText(text) {
4654
- const lines = text
4655
- .split(/\r?\n/)
4656
- .map((line) => line.replace(/\s+$/u, ""))
4657
- .filter((line) => {
4658
- const trimmed = line.trim();
4659
- return trimmed &&
4660
- !trimmed.startsWith("› Use /skills") &&
4661
- !/^gpt-[\w.-]+/u.test(trimmed) &&
4662
- !/^[-\w.]+ default ·/u.test(trimmed);
4663
- });
4664
- const cleaned = lines.join("\n").trim();
4665
- return cleaned || undefined;
4666
- }
4667
4444
  function resolveExecutable(command) {
4668
4445
  if (command.includes(path.sep)) {
4669
4446
  return command;
@@ -4782,19 +4559,62 @@ function runCallback(options) {
4782
4559
  releaseLock();
4783
4560
  }
4784
4561
  }
4562
+ function runRetryCallback(options) {
4563
+ const { conversation, statePath } = loadConversationFromOptions(options);
4564
+ const callbackDelivery = isRecord(conversation.callback_delivery)
4565
+ ? conversation.callback_delivery
4566
+ : undefined;
4567
+ if (!["callback_pending", "callback_failed"].includes(conversation.status)) {
4568
+ throw new Error(`cannot retry callback for ${conversation.conversation_id}; conversation is ${conversation.status}`);
4569
+ }
4570
+ if (!callbackDelivery || !isRecord(callbackDelivery.message)) {
4571
+ throw new Error(`cannot retry callback for ${conversation.conversation_id}; pending callback is missing`);
4572
+ }
4573
+ const releaseLock = acquireFileLock(`${statePath}.lock`);
4574
+ try {
4575
+ runLockedCallback({
4576
+ ...options,
4577
+ statePath,
4578
+ messageJson: JSON.stringify(callbackDelivery.message),
4579
+ gatewayMethod: stringValue(callbackDelivery.gateway_method) ?? conversation.gateway_method,
4580
+ gatewaySession: stringValue(callbackDelivery.gateway_session) ?? conversation.gateway_session,
4581
+ openclawSession: conversation.openclaw_session,
4582
+ openclawBin: stringValue(callbackDelivery.openclaw_bin) ?? conversation.openclaw_bin,
4583
+ gatewayUrl: stringValue(callbackDelivery.gateway_url) ?? conversation.gateway_url,
4584
+ token: stringValue(callbackDelivery.gateway_token) ?? conversation.gateway_token,
4585
+ closeTerminalBridgeOnDone: callbackDelivery.close_terminal_bridge_on_done === true,
4586
+ retryPending: true
4587
+ });
4588
+ }
4589
+ finally {
4590
+ releaseLock();
4591
+ }
4592
+ }
4785
4593
  function runLockedCallback(options) {
4786
4594
  const messageInput = required(options.messageJson, "--message-json is required");
4787
4595
  const logPath = expandHome(options.log ?? logPathForStatePath(options.statePath));
4788
4596
  const conversation = loadState(options.statePath);
4789
4597
  const executor = executorForConversation(conversation);
4790
- const message = extractStructuredMessage({
4791
- conversation,
4792
- input: messageInput,
4793
- defaultFrom: executor.actor,
4794
- defaultTo: "openclaw"
4795
- });
4598
+ const message = options.retryPending === true
4599
+ ? parseMessageJson(messageInput)
4600
+ : extractStructuredMessage({
4601
+ conversation,
4602
+ input: messageInput,
4603
+ defaultFrom: executor.actor,
4604
+ defaultTo: "openclaw"
4605
+ });
4606
+ if (message.conversation_id !== conversation.conversation_id) {
4607
+ throw new Error(`message.conversation_id ${message.conversation_id} does not match conversation ${conversation.conversation_id}`);
4608
+ }
4796
4609
  const existingEvents = readExistingEvents(logPath);
4797
- if (isDuplicateMessage(existingEvents, message)) {
4610
+ const callbackDelivery = isRecord(conversation.callback_delivery)
4611
+ ? conversation.callback_delivery
4612
+ : undefined;
4613
+ const retryingPending = options.retryPending === true &&
4614
+ isRecord(callbackDelivery?.message) &&
4615
+ callbackDelivery.message.id === message.id &&
4616
+ ["pending", "failed"].includes(String(callbackDelivery.status ?? ""));
4617
+ if (isDuplicateMessage(existingEvents, message) && !retryingPending) {
4798
4618
  runtimeLog("info", "callback_duplicate", {
4799
4619
  conversation_id: conversation.conversation_id,
4800
4620
  agent: executor.kind,
@@ -4814,19 +4634,46 @@ function runLockedCallback(options) {
4814
4634
  });
4815
4635
  return;
4816
4636
  }
4817
- let nextConversation = applyMessageToConversation(conversation, message);
4818
- if (message.type === "done" && options.closeTerminalBridgeOnDone === true) {
4637
+ const closeTerminalBridgeOnDone = message.type === "done" &&
4638
+ options.closeTerminalBridgeOnDone === true;
4639
+ const requiresDelivery = Boolean(options.gatewayMethod) || options.recordOnly !== true;
4640
+ const deliveryAttempt = Number(callbackDelivery?.attempts ?? 0) + 1;
4641
+ let nextConversation = retryingPending
4642
+ ? conversation
4643
+ : applyMessageToConversation(conversation, message);
4644
+ if (!retryingPending) {
4645
+ appendEvent(logPath, messageEvent(message));
4646
+ }
4647
+ if (closeTerminalBridgeOnDone && requiresDelivery) {
4819
4648
  const now = new Date().toISOString();
4820
4649
  nextConversation = {
4821
4650
  ...nextConversation,
4822
- status: "closed",
4823
- closed_at: now,
4824
- close_reason: "terminal bridge task completed",
4651
+ status: "callback_pending",
4652
+ callback_delivery: {
4653
+ status: "pending",
4654
+ message,
4655
+ attempts: deliveryAttempt,
4656
+ created_at: stringValue(callbackDelivery?.created_at) ?? now,
4657
+ last_attempt_at: now,
4658
+ gateway_method: options.gatewayMethod,
4659
+ gateway_session: options.gatewaySession ?? options.openclawSession ?? conversation.openclaw_session,
4660
+ gateway_url: options.gatewayUrl ?? conversation.gateway_url,
4661
+ openclaw_bin: options.openclawBin ?? conversation.openclaw_bin,
4662
+ close_terminal_bridge_on_done: true
4663
+ },
4825
4664
  updated_at: now
4826
4665
  };
4827
4666
  delete nextConversation.idle_since;
4667
+ delete nextConversation.closed_at;
4668
+ delete nextConversation.close_reason;
4669
+ appendEvent(logPath, {
4670
+ ts: now,
4671
+ conversation_id: conversation.conversation_id,
4672
+ event: retryingPending ? "callback_delivery_retry_started" : "callback_delivery_pending",
4673
+ message_id: message.id,
4674
+ attempt: deliveryAttempt
4675
+ });
4828
4676
  }
4829
- appendEvent(logPath, messageEvent(message));
4830
4677
  saveState(options.statePath, nextConversation);
4831
4678
  runtimeLog("info", "callback_received", {
4832
4679
  conversation_id: conversation.conversation_id,
@@ -4841,13 +4688,115 @@ function runLockedCallback(options) {
4841
4688
  event_log_path: logPath,
4842
4689
  message: textSummary(message.body)
4843
4690
  });
4844
- const result = {
4845
- conversation: nextConversation,
4846
- message,
4847
- budget: budgetAction(nextConversation),
4848
- delivered: false,
4849
- duplicate: false
4850
- };
4691
+ if (options.recordOnly) {
4692
+ runtimeLog("info", "callback_recorded_only", {
4693
+ conversation_id: conversation.conversation_id,
4694
+ status: nextConversation.status
4695
+ });
4696
+ printJson({
4697
+ conversation: nextConversation,
4698
+ message,
4699
+ budget: budgetAction(nextConversation),
4700
+ delivered: false,
4701
+ duplicate: false
4702
+ });
4703
+ return;
4704
+ }
4705
+ try {
4706
+ const deliveryKind = deliverCallbackToOpenClaw({
4707
+ options,
4708
+ statePath: options.statePath,
4709
+ logPath,
4710
+ conversation: nextConversation,
4711
+ message
4712
+ });
4713
+ const deliveredAt = new Date().toISOString();
4714
+ let deliveredConversation = nextConversation;
4715
+ if (closeTerminalBridgeOnDone) {
4716
+ deliveredConversation = {
4717
+ ...nextConversation,
4718
+ status: "closed",
4719
+ closed_at: deliveredAt,
4720
+ close_reason: "terminal bridge task completed",
4721
+ callback_delivery: {
4722
+ ...(isRecord(nextConversation.callback_delivery) ? nextConversation.callback_delivery : {}),
4723
+ status: "delivered",
4724
+ delivered_at: deliveredAt,
4725
+ last_error: undefined
4726
+ },
4727
+ updated_at: deliveredAt
4728
+ };
4729
+ delete deliveredConversation.idle_since;
4730
+ saveState(options.statePath, deliveredConversation);
4731
+ appendEvent(logPath, {
4732
+ ts: deliveredAt,
4733
+ conversation_id: conversation.conversation_id,
4734
+ event: "callback_delivery_succeeded",
4735
+ message_id: message.id,
4736
+ attempt: deliveryAttempt,
4737
+ status: "closed"
4738
+ });
4739
+ }
4740
+ printJson({
4741
+ conversation: deliveredConversation,
4742
+ message,
4743
+ budget: budgetAction(deliveredConversation),
4744
+ delivered: true,
4745
+ duplicate: false,
4746
+ delivery: deliveryKind
4747
+ });
4748
+ }
4749
+ catch (error) {
4750
+ if (closeTerminalBridgeOnDone) {
4751
+ const failedAt = new Date().toISOString();
4752
+ const failedConversation = {
4753
+ ...nextConversation,
4754
+ status: "callback_failed",
4755
+ callback_delivery: {
4756
+ ...(isRecord(nextConversation.callback_delivery) ? nextConversation.callback_delivery : {}),
4757
+ status: "failed",
4758
+ failed_at: failedAt,
4759
+ last_error: error instanceof Error ? error.message : String(error)
4760
+ },
4761
+ updated_at: failedAt
4762
+ };
4763
+ saveState(options.statePath, failedConversation);
4764
+ appendEvent(logPath, {
4765
+ ts: failedAt,
4766
+ conversation_id: conversation.conversation_id,
4767
+ event: "callback_delivery_failed",
4768
+ message_id: message.id,
4769
+ attempt: deliveryAttempt,
4770
+ error: failedConversation.callback_delivery.last_error
4771
+ });
4772
+ if (options.retryPending !== true &&
4773
+ options.disableCallbackRetry !== true &&
4774
+ deliveryAttempt <= CALLBACK_RETRY_DELAYS_MS.length) {
4775
+ const retryMonitor = startCallbackRetryMonitor({ statePath: options.statePath });
4776
+ const retryDelayMs = CALLBACK_RETRY_DELAYS_MS[Math.max(0, deliveryAttempt - 1)];
4777
+ const retryState = {
4778
+ ...failedConversation,
4779
+ callback_delivery: {
4780
+ ...failedConversation.callback_delivery,
4781
+ retry_monitor_pid: retryMonitor.pid ?? null,
4782
+ next_attempt_at: new Date(Date.now() + retryDelayMs).toISOString()
4783
+ }
4784
+ };
4785
+ saveState(options.statePath, retryState);
4786
+ appendEvent(logPath, {
4787
+ ts: new Date().toISOString(),
4788
+ conversation_id: conversation.conversation_id,
4789
+ event: "callback_retry_monitor_launched",
4790
+ message_id: message.id,
4791
+ pid: retryMonitor.pid ?? null,
4792
+ next_attempt_at: retryState.callback_delivery.next_attempt_at
4793
+ });
4794
+ }
4795
+ }
4796
+ throw error;
4797
+ }
4798
+ }
4799
+ function deliverCallbackToOpenClaw({ options, statePath, logPath, conversation, message }) {
4851
4800
  if (options.gatewayMethod) {
4852
4801
  const delivery = deliverToGatewayMethod({
4853
4802
  method: options.gatewayMethod,
@@ -4855,30 +4804,19 @@ function runLockedCallback(options) {
4855
4804
  gatewayUrl: options.gatewayUrl,
4856
4805
  token: options.token,
4857
4806
  sessionKey: options.gatewaySession ?? options.openclawSession ?? conversation.openclaw_session,
4858
- statePath: options.statePath,
4807
+ statePath,
4859
4808
  logPath,
4860
- conversation: nextConversation,
4809
+ conversation,
4861
4810
  message
4862
4811
  });
4863
- appendEvent(logPath, {
4864
- ts: new Date().toISOString(),
4865
- conversation_id: conversation.conversation_id,
4812
+ recordCallbackProcessDelivery({
4813
+ logPath,
4814
+ conversation,
4815
+ message,
4866
4816
  event: "callback_gateway_method_delivery",
4867
- from: message.from,
4868
- to: "openclaw",
4869
- round: message.round,
4870
- method: options.gatewayMethod,
4871
- status: delivery.status,
4872
- stdout: delivery.stdout,
4873
- stderr: delivery.stderr
4874
- });
4875
- runtimeLog("info", "callback_gateway_method_delivery", {
4876
- conversation_id: conversation.conversation_id,
4877
- method: options.gatewayMethod,
4878
- status: delivery.status,
4879
- failure_kind: classifyProcessFailure(delivery),
4880
- stdout: textSummary(delivery.stdout),
4881
- stderr: textSummary(delivery.stderr)
4817
+ runtimeEvent: "callback_gateway_method_delivery",
4818
+ delivery,
4819
+ detail: { method: options.gatewayMethod }
4882
4820
  });
4883
4821
  if (delivery.status !== 0) {
4884
4822
  throw new Error(delivery.stderr || delivery.stdout || `gateway method delivery failed with status ${delivery.status}`);
@@ -4886,84 +4824,47 @@ function runLockedCallback(options) {
4886
4824
  const gatewayPayload = parseOptionalJson(delivery.stdout);
4887
4825
  const chatSendParams = isRecord(gatewayPayload?.chat_send) ? gatewayPayload.chat_send : undefined;
4888
4826
  const sessionSendParams = isRecord(gatewayPayload?.session_send) ? gatewayPayload.session_send : undefined;
4889
- let chatSendDelivery;
4890
- let sessionSendDelivery;
4891
4827
  if (chatSendParams) {
4892
- chatSendDelivery = deliverToChatSend({
4828
+ const chatSendDelivery = deliverToChatSend({
4893
4829
  openclawBin: options.openclawBin,
4894
4830
  gatewayUrl: options.gatewayUrl,
4895
4831
  token: options.token,
4896
4832
  params: chatSendParams
4897
4833
  });
4898
- appendEvent(logPath, {
4899
- ts: new Date().toISOString(),
4900
- conversation_id: conversation.conversation_id,
4834
+ recordCallbackProcessDelivery({
4835
+ logPath,
4836
+ conversation,
4837
+ message,
4901
4838
  event: "callback_chat_send_delivery",
4902
- from: message.from,
4903
- to: "openclaw",
4904
- round: message.round,
4905
- status: chatSendDelivery.status,
4906
- stdout: chatSendDelivery.stdout,
4907
- stderr: chatSendDelivery.stderr
4908
- });
4909
- runtimeLog("info", "callback_chat_send_delivery", {
4910
- conversation_id: conversation.conversation_id,
4911
- status: chatSendDelivery.status,
4912
- failure_kind: classifyProcessFailure(chatSendDelivery),
4913
- stdout: textSummary(chatSendDelivery.stdout),
4914
- stderr: textSummary(chatSendDelivery.stderr)
4839
+ runtimeEvent: "callback_chat_send_delivery",
4840
+ delivery: chatSendDelivery
4915
4841
  });
4916
4842
  if (chatSendDelivery.status !== 0) {
4917
4843
  throw new Error(chatSendDelivery.stderr || chatSendDelivery.stdout || `chat callback delivery failed with status ${chatSendDelivery.status}`);
4918
4844
  }
4845
+ return "gateway_method+chat_send";
4919
4846
  }
4920
- else if (sessionSendParams) {
4921
- sessionSendDelivery = deliverToSessionSend({
4847
+ if (sessionSendParams) {
4848
+ const sessionSendDelivery = deliverToSessionSend({
4922
4849
  openclawBin: options.openclawBin,
4923
4850
  gatewayUrl: options.gatewayUrl,
4924
4851
  token: options.token,
4925
4852
  params: sessionSendParams
4926
4853
  });
4927
- appendEvent(logPath, {
4928
- ts: new Date().toISOString(),
4929
- conversation_id: conversation.conversation_id,
4854
+ recordCallbackProcessDelivery({
4855
+ logPath,
4856
+ conversation,
4857
+ message,
4930
4858
  event: "callback_session_send_delivery",
4931
- from: message.from,
4932
- to: "openclaw",
4933
- round: message.round,
4934
- status: sessionSendDelivery.status,
4935
- stdout: sessionSendDelivery.stdout,
4936
- stderr: sessionSendDelivery.stderr
4937
- });
4938
- runtimeLog("info", "callback_session_send_delivery", {
4939
- conversation_id: conversation.conversation_id,
4940
- status: sessionSendDelivery.status,
4941
- failure_kind: classifyProcessFailure(sessionSendDelivery),
4942
- stdout: textSummary(sessionSendDelivery.stdout),
4943
- stderr: textSummary(sessionSendDelivery.stderr)
4859
+ runtimeEvent: "callback_session_send_delivery",
4860
+ delivery: sessionSendDelivery
4944
4861
  });
4945
4862
  if (sessionSendDelivery.status !== 0) {
4946
4863
  throw new Error(sessionSendDelivery.stderr || sessionSendDelivery.stdout || `session callback delivery failed with status ${sessionSendDelivery.status}`);
4947
4864
  }
4865
+ return "gateway_method+sessions_send";
4948
4866
  }
4949
- printJson({
4950
- ...result,
4951
- delivered: true,
4952
- delivery: chatSendDelivery
4953
- ? "gateway_method+chat_send"
4954
- : sessionSendDelivery
4955
- ? "gateway_method+sessions_send"
4956
- : "gateway_method"
4957
- });
4958
- return;
4959
- }
4960
- if (options.recordOnly) {
4961
- runtimeLog("info", "callback_recorded_only", {
4962
- conversation_id: conversation.conversation_id,
4963
- status: nextConversation.status
4964
- });
4965
- printJson(result);
4966
- return;
4867
+ return "gateway_method";
4967
4868
  }
4968
4869
  const gatewayUrl = options.gatewayUrl ?? conversation.gateway_url;
4969
4870
  const token = options.token ?? conversation.gateway_token;
@@ -4978,31 +4879,40 @@ function runLockedCallback(options) {
4978
4879
  throw new Error("--openclaw-session is required unless state has openclaw_session");
4979
4880
  }
4980
4881
  const delivery = deliverToOpenClaw({ gatewayUrl, token, openclawSession, message });
4882
+ recordCallbackProcessDelivery({
4883
+ logPath,
4884
+ conversation,
4885
+ message,
4886
+ event: "callback_delivery",
4887
+ runtimeEvent: "callback_delivery",
4888
+ delivery
4889
+ });
4890
+ if (delivery.status !== 0) {
4891
+ throw new Error(delivery.stderr || delivery.stdout || `callback delivery failed with status ${delivery.status}`);
4892
+ }
4893
+ return "acpx";
4894
+ }
4895
+ function recordCallbackProcessDelivery({ logPath, conversation, message, event, runtimeEvent, delivery, detail = {} }) {
4981
4896
  appendEvent(logPath, {
4982
4897
  ts: new Date().toISOString(),
4983
4898
  conversation_id: conversation.conversation_id,
4984
- event: "callback_delivery",
4899
+ event,
4985
4900
  from: message.from,
4986
4901
  to: "openclaw",
4987
4902
  round: message.round,
4903
+ ...detail,
4988
4904
  status: delivery.status,
4989
4905
  stdout: delivery.stdout,
4990
4906
  stderr: delivery.stderr
4991
4907
  });
4992
- runtimeLog("info", "callback_delivery", {
4908
+ runtimeLog("info", runtimeEvent, {
4993
4909
  conversation_id: conversation.conversation_id,
4910
+ ...detail,
4994
4911
  status: delivery.status,
4995
4912
  failure_kind: classifyProcessFailure(delivery),
4996
4913
  stdout: textSummary(delivery.stdout),
4997
4914
  stderr: textSummary(delivery.stderr)
4998
4915
  });
4999
- if (delivery.status !== 0) {
5000
- throw new Error(delivery.stderr || delivery.stdout || `callback delivery failed with status ${delivery.status}`);
5001
- }
5002
- printJson({
5003
- ...result,
5004
- delivered: true
5005
- });
5006
4916
  }
5007
4917
  function acquireFileLock(lockPath, { timeoutMs = 5000, retryMs = 50 } = {}) {
5008
4918
  const started = Date.now();
@@ -6295,6 +6205,7 @@ function usage() {
6295
6205
  agent-knock-knock approve --conversation <id>
6296
6206
  agent-knock-knock cancel --conversation <id> [--all-proxy <url>]
6297
6207
  agent-knock-knock renew --conversation <id> [--minutes <inactivity-minutes>]
6208
+ agent-knock-knock retry-callback --conversation <id> [--store-dir <dir>]
6298
6209
  agent-knock-knock recover --conversation <id> [--session <name>] [--all-proxy <url>]
6299
6210
  agent-knock-knock close --conversation <id> [--reason <text>]
6300
6211
  agent-knock-knock install-openclaw [--openclaw-bin <path>] [--skill-path <path>] [--skill-only] [--no-restart]