@scotthuang/agent-knock-knock 0.2.44 → 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 +21 -0
  2. package/README.md +135 -244
  3. package/dist/src/cli.js +638 -729
  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
  }
@@ -549,7 +558,7 @@ function selectTerminateTarget({ plan, session, activeSessions, expectedPid, all
549
558
  }
550
559
  async function listActiveSessionsWithTerminalControl(provider, options, terminalProvider = createTerminalControlProvider(options)) {
551
560
  const activeSessions = await provider.listActiveSessions();
552
- return enrichActiveProcessesWithTerminalControl(activeSessions, terminalProvider);
561
+ return createTerminalAgentBridge(options, terminalProvider).attachProcesses(provider.agent, activeSessions);
553
562
  }
554
563
  function createTerminalControlProvider(options) {
555
564
  if (options.terminalsJson || options.terminalScreensJson || options.processesJson) {
@@ -560,6 +569,57 @@ function createTerminalControlProvider(options) {
560
569
  }
561
570
  return new TmuxTerminalControlProvider();
562
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
+ }
563
623
  function planTerminalControlTakeover(session, activeSessions) {
564
624
  const matched = activeSessions
565
625
  .filter((process) => process.kind === "codex_cli" &&
@@ -609,6 +669,9 @@ function terminalControlFromTakeover(nativeTakeover) {
609
669
  if (!target || !session || !Number.isInteger(window) || !Number.isInteger(pane) || !Number.isInteger(panePid)) {
610
670
  return undefined;
611
671
  }
672
+ const storedCapabilities = Array.isArray(terminalControl.capabilities)
673
+ ? terminalControl.capabilities.filter(isTerminalControlCapability)
674
+ : [];
612
675
  return {
613
676
  kind: "tmux",
614
677
  target,
@@ -619,181 +682,28 @@ function terminalControlFromTakeover(nativeTakeover) {
619
682
  currentCommand: stringValue(terminalControl.currentCommand),
620
683
  currentPath: stringValue(terminalControl.currentPath),
621
684
  socketPath: stringValue(terminalControl.socketPath),
622
- capabilities: ["screen_status", "send_keys", "terminal_approval"]
623
- };
624
- }
625
- function detectCodexApprovalPrompt(screen) {
626
- const prompt = codexApprovalPromptRegion(screen);
627
- if (!prompt.visible) {
628
- return {
629
- approvable: false,
630
- reason: prompt.reason
631
- };
632
- }
633
- for (const line of prompt.region.split(/\r?\n/)) {
634
- const match = /^[\s›]*1\.\s+(Yes,[^(]+)\(([^)]+)\)/u.exec(line.trim());
635
- if (!match) {
636
- continue;
637
- }
638
- const key = match[2].trim();
639
- if (key !== "y") {
640
- return {
641
- approvable: false,
642
- reason: `primary approval shortcut is ${key}, not y`,
643
- ...approvalCandidateFromPrompt(prompt.marker, prompt.region)
644
- };
645
- }
646
- return {
647
- approvable: true,
648
- key,
649
- label: match[1].trim(),
650
- ...approvalCandidateFromPrompt(prompt.marker, prompt.region)
651
- };
652
- }
653
- return {
654
- approvable: false,
655
- reason: "no primary approve option with a shortcut was detected",
656
- ...approvalCandidateFromPrompt(prompt.marker, prompt.region)
657
- };
658
- }
659
- function isCodexApprovalPromptVisible(screen) {
660
- return codexApprovalPromptRegion(screen).visible;
661
- }
662
- function codexApprovalPromptRegion(screen) {
663
- const approvalMarkers = [
664
- "Would you like to run the following command?",
665
- "Would you like to make the following edits?",
666
- "Would you like to grant these permissions?",
667
- "needs your approval."
668
- ];
669
- const lines = screen.split(/\r?\n/);
670
- let markerIndex = -1;
671
- let matchedMarker = "";
672
- for (let index = lines.length - 1; index >= 0; index -= 1) {
673
- const marker = approvalMarkers.find((candidate) => lines[index].includes(candidate));
674
- if (marker) {
675
- markerIndex = index;
676
- matchedMarker = marker;
677
- break;
678
- }
679
- }
680
- if (markerIndex < 0) {
681
- return {
682
- visible: false,
683
- reason: "no Codex approval prompt was detected in the terminal screen"
684
- };
685
- }
686
- const regionLines = lines.slice(markerIndex);
687
- const staleLine = regionLines.slice(1).find((line) => isPostApprovalActivityLine(line));
688
- if (staleLine) {
689
- return {
690
- visible: false,
691
- reason: `Codex approval prompt appears stale after later terminal activity: ${staleLine.trim()}`
692
- };
693
- }
694
- return {
695
- visible: true,
696
- region: regionLines.join("\n"),
697
- marker: matchedMarker
698
- };
699
- }
700
- function approvalCandidateFromPrompt(marker, region) {
701
- const promptKind = marker === "Would you like to run the following command?"
702
- ? "run_command"
703
- : marker === "Would you like to make the following edits?"
704
- ? "file_edit"
705
- : marker === "Would you like to grant these permissions?"
706
- ? "grant_permissions"
707
- : "unknown";
708
- return {
709
- promptKind,
710
- command: promptKind === "run_command" ? commandFromApprovalRegion(region) : undefined
711
- };
712
- }
713
- function commandFromApprovalRegion(region) {
714
- const lines = region.split(/\r?\n/);
715
- const commandStart = lines.findIndex((line) => /^\s*\$\s+/u.test(line));
716
- if (commandStart < 0) {
717
- return undefined;
718
- }
719
- const parts = [];
720
- for (let index = commandStart; index < lines.length; index += 1) {
721
- const line = lines[index];
722
- if (index > commandStart && (!line.trim() || /^[\s›]*\d+\.\s+/u.test(line) || /Press enter to confirm/u.test(line))) {
723
- break;
724
- }
725
- parts.push(index === commandStart ? line.replace(/^\s*\$\s+/u, "").trim() : line.trim());
726
- }
727
- const command = parts.filter(Boolean).join(" ").trim();
728
- return command ? redactString(command) : undefined;
729
- }
730
- function isPostApprovalActivityLine(line) {
731
- const trimmed = line.trim();
732
- if (!trimmed) {
733
- return false;
734
- }
735
- if (/^✔\s+You approved\b/u.test(trimmed)) {
736
- return true;
737
- }
738
- if (/^›\s+(?!1\.)\S/u.test(trimmed)) {
739
- return true;
740
- }
741
- if (/^•\s+(Working|Ran|Explored|Edited|Read|Called|Searching|Planning|Updated|Added|Deleted|Modified|Running|Thinking)\b/u.test(trimmed)) {
742
- return true;
743
- }
744
- return /^─\s*Worked for\b/u.test(trimmed);
745
- }
746
- function detectCodexActivityState(screen, approval = detectCodexApprovalPrompt(screen)) {
747
- if (approval.approvable || isCodexApprovalPromptVisible(screen)) {
748
- return {
749
- state: "awaiting_approval",
750
- reason: "current Codex approval prompt is visible"
751
- };
752
- }
753
- const tailLines = screen.trimEnd().split(/\r?\n/).slice(-30);
754
- const workingLine = tailLines.find((line) => isCodexWorkingLine(line));
755
- if (workingLine) {
756
- return {
757
- state: "working",
758
- reason: `Codex working marker detected: ${workingLine.trim()}`
759
- };
760
- }
761
- const idleLine = tailLines
762
- .slice(-6)
763
- .map((line) => line.trim())
764
- .find((line) => isCodexIdlePromptLine(line));
765
- if (idleLine) {
766
- return {
767
- state: "idle",
768
- reason: `Codex input prompt detected: ${idleLine}`
769
- };
770
- }
771
- return {
772
- state: "unknown",
773
- 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
+ ]
774
696
  };
775
697
  }
776
- function isCodexWorkingLine(line) {
777
- const trimmed = line.trim();
778
- if (!trimmed) {
779
- return false;
780
- }
781
- if (/^•\s+Working\b/u.test(trimmed) && (/\besc to interrupt\b/u.test(trimmed) || trimmed === "• Working")) {
782
- return true;
783
- }
784
- if (/^•\s+Waiting for background terminals?\b(?:\s*·|$)/u.test(trimmed)) {
785
- return true;
786
- }
787
- return /^\d+\s+background terminals? running\b/u.test(trimmed) && /\/(?:ps|stop)\b/u.test(trimmed);
788
- }
789
- function isCodexIdlePromptLine(line) {
790
- const trimmed = line.trim();
791
- return /^›(?:\s|$)/u.test(trimmed) && !/^›\s*1\./u.test(trimmed);
792
- }
793
- function screenExcerpt(screen, maxLength = 4000) {
794
- const lines = screen.trimEnd().split(/\r?\n/);
795
- const excerpt = lines.slice(Math.max(0, lines.length - 80)).join("\n");
796
- 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);
797
707
  }
798
708
  function createForkConversation({ agent, strategy, session, contextPackage, forkSummary, modelInfo, options }) {
799
709
  const workspace = session.cwd;
@@ -1535,57 +1445,59 @@ async function buildNativeListGroups({ options, agentFilter, statusFilter }) {
1535
1445
  }
1536
1446
  };
1537
1447
  }
1538
- 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) {
1539
1453
  return {
1540
1454
  ...empty,
1541
1455
  summary: {
1542
1456
  enabled: true,
1543
1457
  agents: [],
1544
- skipped: `native active discovery is not implemented for ${agentFilter}`
1458
+ skipped: `terminal agent adapter is not registered for ${agentFilter}`
1545
1459
  }
1546
1460
  };
1547
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 = [];
1548
1469
  try {
1549
- const provider = createAgentSessionProvider("codex", options);
1550
- const terminalProvider = createTerminalControlProvider(options);
1551
- const terminalScan = options.terminalDebug ? await terminalControlDiagnostics(terminalProvider) : undefined;
1552
- 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;
1553
1474
  const rootSessions = rootActiveProcesses(activeSessions);
1554
- const terminalControlled = [];
1555
- const native = [];
1556
1475
  for (const session of rootSessions) {
1557
1476
  if (session.terminalControl) {
1558
- terminalControlled.push(await terminalControlledListEntry(session, activeSessions, options));
1477
+ terminalControlled.push(await terminalControlledListEntry(session, activeSessions, options, bridge));
1559
1478
  }
1560
1479
  else {
1561
1480
  native.push(nativeListEntry(session, activeSessions));
1562
1481
  }
1563
1482
  }
1564
- return {
1565
- native,
1566
- terminalControlled,
1567
- summary: {
1568
- enabled: true,
1569
- agents: ["codex"],
1570
- active_count: activeSessions.length,
1571
- native_count: native.length,
1572
- terminal_controlled_count: terminalControlled.length,
1573
- approval_scan: options.noApprovalScan ? "disabled" : "enabled",
1574
- terminal_scan: terminalScan
1575
- }
1576
- };
1577
1483
  }
1578
1484
  catch (error) {
1579
- return {
1580
- native: [],
1581
- terminalControlled: [],
1582
- summary: {
1583
- enabled: true,
1584
- agents: ["codex"],
1585
- error: error instanceof Error ? error.message : String(error)
1586
- }
1587
- };
1485
+ errors.push(error instanceof Error ? error.message : String(error));
1588
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
+ };
1589
1501
  }
1590
1502
  async function terminalControlDiagnostics(provider) {
1591
1503
  if (provider instanceof TmuxTerminalControlProvider) {
@@ -1612,9 +1524,9 @@ function delegatedListEntry(task) {
1612
1524
  }
1613
1525
  function nativeListEntry(session, activeSessions) {
1614
1526
  return {
1615
- id: `native:codex:${session.pid}`,
1527
+ id: `native:${session.agent}:${session.pid}`,
1616
1528
  source: "native_active",
1617
- agent: "codex",
1529
+ agent: session.agent,
1618
1530
  status: "active",
1619
1531
  pid: session.pid,
1620
1532
  child_pids: childPidsForRoot(session, activeSessions),
@@ -1637,16 +1549,16 @@ function nativeListEntry(session, activeSessions) {
1637
1549
  }
1638
1550
  };
1639
1551
  }
1640
- async function terminalControlledListEntry(session, activeSessions, options) {
1552
+ async function terminalControlledListEntry(session, activeSessions, options, bridge = createTerminalAgentBridge(options)) {
1641
1553
  const terminalControl = session.terminalControl;
1642
1554
  if (!terminalControl) {
1643
1555
  throw new Error(`process ${session.pid} is not terminal-controlled`);
1644
1556
  }
1645
- const terminalState = await listStateForTerminal(terminalControl, options);
1557
+ const terminalState = await listStateForTerminal(session.agent, terminalControl, options, bridge);
1646
1558
  return {
1647
- id: `terminal:${terminalControl.kind}:${terminalControl.target}:${session.pid}`,
1559
+ id: bridge.terminalConversationId(session),
1648
1560
  source: "terminal_control",
1649
- agent: "codex",
1561
+ agent: session.agent,
1650
1562
  status: "active",
1651
1563
  pid: session.pid,
1652
1564
  child_pids: childPidsForRoot(session, activeSessions),
@@ -1663,14 +1575,15 @@ async function terminalControlledListEntry(session, activeSessions, options) {
1663
1575
  activity_reason: terminalState.activity_reason,
1664
1576
  commands: {
1665
1577
  send: true,
1666
- approve: terminalState.approval_state.approvable === true,
1578
+ approve: terminalControl.capabilities.includes("terminal_approval") &&
1579
+ terminalState.approval_state.approvable === true,
1667
1580
  status: true,
1668
- cancel: true,
1581
+ cancel: terminalControl.capabilities.includes("terminal_cancel"),
1669
1582
  close: false
1670
1583
  }
1671
1584
  };
1672
1585
  }
1673
- async function listStateForTerminal(terminalControl, options) {
1586
+ async function listStateForTerminal(agent, terminalControl, options, bridge = createTerminalAgentBridge(options)) {
1674
1587
  if (options.noApprovalScan) {
1675
1588
  return {
1676
1589
  approval_state: {
@@ -1684,27 +1597,17 @@ async function listStateForTerminal(terminalControl, options) {
1684
1597
  };
1685
1598
  }
1686
1599
  try {
1687
- const screen = await createTerminalControlProvider(options).capture(terminalControl.target, {
1688
- scrollbackLines: Number(options.scrollbackLines ?? 120),
1689
- socketPath: terminalControl.socketPath
1600
+ const status = await bridge.status(agent, terminalControl, {
1601
+ scrollbackLines: Number(options.scrollbackLines ?? 120)
1690
1602
  });
1691
- const approval = detectCodexApprovalPrompt(screen);
1692
- const blocked = isCodexApprovalPromptVisible(screen);
1693
- const activity = detectCodexActivityState(screen, approval);
1694
1603
  return {
1695
1604
  approval_state: {
1696
- scanned: true,
1697
- blocked,
1698
- approvable: approval.approvable,
1699
- key: approval.approvable ? approval.key : undefined,
1700
- label: approval.approvable ? approval.label : undefined,
1701
- prompt_kind: approval.promptKind,
1702
- command: approval.command,
1703
- reason: approval.approvable ? undefined : approval.reason,
1704
- screen_excerpt: blocked ? screenExcerpt(screen, 1000) : undefined
1605
+ ...status.approval_state,
1606
+ screen_excerpt: status.approval_state.blocked ? status.screen.excerpt?.slice(-1000) : undefined
1705
1607
  },
1706
- activity_state: activity.state,
1707
- activity_reason: activity.reason
1608
+ activity_state: status.activity_state,
1609
+ activity_reason: status.activity_reason,
1610
+ capability_limitation: status.capability_limitation
1708
1611
  };
1709
1612
  }
1710
1613
  catch (error) {
@@ -1721,11 +1624,13 @@ async function listStateForTerminal(terminalControl, options) {
1721
1624
  }
1722
1625
  }
1723
1626
  function rootActiveProcesses(processes) {
1724
- const pids = new Set(processes.map((process) => process.pid));
1725
- 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}`));
1726
1629
  const seenTerminalTargets = new Set();
1727
1630
  return roots.filter((process) => {
1728
- const terminalTarget = process.terminalControl?.target;
1631
+ const terminalTarget = process.terminalControl?.target
1632
+ ? `${process.agent}:${process.terminalControl.target}`
1633
+ : undefined;
1729
1634
  if (!terminalTarget) {
1730
1635
  return true;
1731
1636
  }
@@ -1738,50 +1643,14 @@ function rootActiveProcesses(processes) {
1738
1643
  }
1739
1644
  function childPidsForRoot(root, processes) {
1740
1645
  return processes
1741
- .filter((process) => process.ppid === root.pid)
1646
+ .filter((process) => process.agent === root.agent && process.ppid === root.pid)
1742
1647
  .map((process) => process.pid);
1743
1648
  }
1744
1649
  function canSendDelegated(status) {
1745
1650
  return !["failed", "closed", "cancelled"].includes(status);
1746
1651
  }
1747
1652
  async function resolveTerminalConversationFromOptions(options) {
1748
- const parsed = parseTerminalConversationId(stringValue(options.conversation));
1749
- if (!parsed) {
1750
- return undefined;
1751
- }
1752
- const provider = createTerminalControlProvider(options);
1753
- const panes = await provider.listPanes();
1754
- const pane = panes.find((candidate) => candidate.kind === parsed.kind &&
1755
- candidate.target === parsed.target);
1756
- if (!pane) {
1757
- throw new Error(`terminal-controlled session ${parsed.conversationId} is no longer available`);
1758
- }
1759
- return {
1760
- conversationId: parsed.conversationId,
1761
- terminalControl: terminalRefFromPane(pane)
1762
- };
1763
- }
1764
- function parseTerminalConversationId(conversationId) {
1765
- const prefix = "terminal:tmux:";
1766
- if (!conversationId?.startsWith(prefix)) {
1767
- return undefined;
1768
- }
1769
- const rest = conversationId.slice(prefix.length);
1770
- const pidSeparator = rest.lastIndexOf(":");
1771
- if (pidSeparator <= 0 || pidSeparator === rest.length - 1) {
1772
- throw new Error(`invalid terminal-controlled conversation id: ${conversationId}`);
1773
- }
1774
- const target = rest.slice(0, pidSeparator);
1775
- const pid = Number(rest.slice(pidSeparator + 1));
1776
- if (!target || !Number.isInteger(pid)) {
1777
- throw new Error(`invalid terminal-controlled conversation id: ${conversationId}`);
1778
- }
1779
- return {
1780
- conversationId,
1781
- kind: "tmux",
1782
- target,
1783
- pid
1784
- };
1653
+ return createTerminalAgentBridge(options).resolveConversationId(stringValue(options.conversation ?? options.conversationId));
1785
1654
  }
1786
1655
  function parseNativeConversationId(conversationId) {
1787
1656
  const prefix = "native:codex:";
@@ -1802,7 +1671,7 @@ async function runStatus(options) {
1802
1671
  cleanupIdleConversations(storeDirFromOptions(options), options);
1803
1672
  const terminalConversation = await resolveTerminalConversationFromOptions(options);
1804
1673
  if (terminalConversation) {
1805
- const terminalStatus = await terminalStatusForControl(terminalConversation.terminalControl, options);
1674
+ const terminalStatus = await terminalStatusForControl(terminalConversation.agent, terminalConversation.terminalControl, options);
1806
1675
  printJson({
1807
1676
  conversation_id: terminalConversation.conversationId,
1808
1677
  source: "terminal_control",
@@ -1832,8 +1701,9 @@ async function runStatus(options) {
1832
1701
  }
1833
1702
  const terminalControl = terminalControlFromTakeover(isRecord(conversation.native_session_takeover) ? conversation.native_session_takeover : undefined);
1834
1703
  if (terminalControl) {
1704
+ const executor = executorForConversation(conversation);
1835
1705
  result.terminal_control = terminalControl;
1836
- result.terminal_status = await terminalStatusForControl(terminalControl, options);
1706
+ result.terminal_status = await terminalStatusForControl(executor.kind, terminalControl, options);
1837
1707
  result.terminal_screen = result.terminal_status.screen;
1838
1708
  }
1839
1709
  printJson(result);
@@ -1851,8 +1721,27 @@ async function runDescribe(options) {
1851
1721
  const conversationId = required(options.conversation ?? options.conversationId, "--conversation is required");
1852
1722
  const terminalConversation = await resolveTerminalConversationFromOptions(options);
1853
1723
  if (terminalConversation) {
1854
- const terminalStatus = await terminalStatusForControl(terminalConversation.terminalControl, options);
1855
- 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);
1856
1745
  printJson(await describeNativeCodexSession({
1857
1746
  id: conversationId,
1858
1747
  source: "terminal_control",
@@ -1877,7 +1766,9 @@ async function runDescribe(options) {
1877
1766
  const { conversation, statePath, logPath } = loadConversationFromOptions(options);
1878
1767
  const events = readExistingEvents(logPath);
1879
1768
  const terminalControl = terminalControlFromTakeover(isRecord(conversation.native_session_takeover) ? conversation.native_session_takeover : undefined);
1880
- const terminalStatus = terminalControl ? await terminalStatusForControl(terminalControl, options) : undefined;
1769
+ const terminalStatus = terminalControl
1770
+ ? await terminalStatusForControl(executorForConversation(conversation).kind, terminalControl, options)
1771
+ : undefined;
1881
1772
  printJson({
1882
1773
  conversation_id: conversation.conversation_id,
1883
1774
  source: "akk_managed",
@@ -1895,110 +1786,63 @@ async function runDescribe(options) {
1895
1786
  event_log_path: logPath
1896
1787
  });
1897
1788
  }
1898
- async function terminalStatusForControl(terminalControl, options) {
1899
- try {
1900
- const screen = await createTerminalControlProvider(options).capture(terminalControl.target, {
1901
- scrollbackLines: Number(options.scrollbackLines ?? 120),
1902
- socketPath: terminalControl.socketPath
1903
- });
1904
- const approval = detectCodexApprovalPrompt(screen);
1905
- const blocked = isCodexApprovalPromptVisible(screen);
1906
- const activity = detectCodexActivityState(screen, approval);
1907
- return {
1908
- provider: terminalControl.kind,
1909
- target: terminalControl.target,
1910
- reachable: true,
1911
- activity_state: activity.state,
1912
- activity_reason: activity.reason,
1913
- approval_state: {
1914
- scanned: true,
1915
- blocked,
1916
- approvable: approval.approvable,
1917
- key: approval.approvable ? approval.key : undefined,
1918
- label: approval.approvable ? approval.label : undefined,
1919
- prompt_kind: approval.promptKind,
1920
- command: approval.command,
1921
- reason: approval.approvable ? undefined : approval.reason
1922
- },
1923
- screen: {
1924
- excerpt: screenExcerpt(screen),
1925
- approval
1926
- }
1927
- };
1928
- }
1929
- catch (error) {
1930
- return {
1931
- provider: terminalControl.kind,
1932
- target: terminalControl.target,
1933
- reachable: false,
1934
- activity_state: "unknown",
1935
- activity_reason: error instanceof Error ? error.message : String(error),
1936
- approval_state: {
1937
- scanned: false,
1938
- blocked: false,
1939
- approvable: false
1940
- },
1941
- screen: {
1942
- error: error instanceof Error ? error.message : String(error)
1943
- }
1944
- };
1945
- }
1789
+ async function terminalStatusForControl(agent, terminalControl, options) {
1790
+ return createTerminalAgentBridge(options).status(agent, terminalControl, {
1791
+ scrollbackLines: Number(options.scrollbackLines ?? 120)
1792
+ });
1946
1793
  }
1947
1794
  function terminalBridgeApprovalFingerprint({ terminalControl, terminalStatus }) {
1948
1795
  const approval = isRecord(terminalStatus?.approval_state) ? terminalStatus.approval_state : {};
1796
+ const adapterFingerprint = stringValue(approval.fingerprint);
1797
+ if (adapterFingerprint) {
1798
+ return adapterFingerprint;
1799
+ }
1949
1800
  const screen = isRecord(terminalStatus?.screen) ? terminalStatus.screen : {};
1950
1801
  return createHash("sha256")
1951
1802
  .update(JSON.stringify({
1952
1803
  target: terminalControl.target,
1953
- key: approval.key,
1804
+ keys: approval.keys ?? (approval.key ? [approval.key] : undefined),
1954
1805
  label: approval.label,
1955
1806
  prompt_kind: approval.prompt_kind,
1956
1807
  command: approval.command,
1957
1808
  excerpt: screen.excerpt
1958
1809
  }))
1959
- .digest("hex")
1960
- .slice(0, 16);
1961
- }
1962
- function terminalBridgeApprovalFingerprintForScreen({ terminalControl, screen, approval }) {
1963
- return terminalBridgeApprovalFingerprint({
1964
- terminalControl,
1965
- terminalStatus: {
1966
- approval_state: {
1967
- key: approval.key,
1968
- label: approval.label,
1969
- prompt_kind: approval.promptKind,
1970
- command: approval.command
1971
- },
1972
- screen: {
1973
- excerpt: screenExcerpt(screen)
1974
- }
1975
- }
1976
- });
1810
+ .digest("hex");
1977
1811
  }
1978
1812
  function terminalBridgeApprovalInstructions({ conversation, terminalControl, terminalStatus }) {
1979
1813
  const approval = isRecord(terminalStatus?.approval_state) ? terminalStatus.approval_state : {};
1980
1814
  const screen = isRecord(terminalStatus?.screen) ? terminalStatus.screen : {};
1981
- const label = stringValue(approval.label) || "the current Codex approval prompt";
1982
- 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);
1983
1825
  const excerpt = stringValue(screen.excerpt) || "(No terminal excerpt was available.)";
1984
1826
  return [
1985
- "Codex is waiting for approval in a terminal-controlled AKK session.",
1827
+ `${agentName} is waiting for approval in a terminal-controlled AKK session.`,
1986
1828
  "",
1987
1829
  `Conversation: ${conversation.conversation_id}`,
1988
1830
  `Terminal: ${terminalControl.kind}:${terminalControl.target}`,
1989
- `Approval option: ${label} (${key})`,
1831
+ `Approval option: ${label} (${keyDescription})`,
1990
1832
  "",
1991
1833
  "Safe terminal excerpt:",
1992
1834
  "```text",
1993
1835
  excerpt,
1994
1836
  "```",
1995
1837
  "",
1996
- "Ask the user whether to approve or deny this Codex request.",
1838
+ `Ask the user whether to approve or deny this ${agentName} request.`,
1997
1839
  "",
1998
1840
  "If the user approves, call `agent_knock_knock_approve` with:",
1999
1841
  `- conversation_id: ${conversation.conversation_id}`,
1842
+ `- expected_approval_fingerprint: ${fingerprint ?? "(missing; refresh status before approval)"}`,
2000
1843
  "",
2001
- "Equivalent user command: `AKK approve " + conversation.conversation_id + "`",
1844
+ "Equivalent user command: `AKK approve " + conversation.conversation_id +
1845
+ (fingerprint ? ` --expected-approval-fingerprint ${fingerprint}` : "") + "`",
2002
1846
  "",
2003
1847
  "If the user denies or wants to stop this request, call `agent_knock_knock_cancel` with:",
2004
1848
  `- conversation_id: ${conversation.conversation_id}`,
@@ -2068,6 +1912,7 @@ async function runSend(options) {
2068
1912
  const managed = createManagedTerminalConversationFromRawId({
2069
1913
  options,
2070
1914
  conversationId: terminalConversation.conversationId,
1915
+ agent: terminalConversation.agent,
2071
1916
  messageBody,
2072
1917
  terminalControl: terminalConversation.terminalControl
2073
1918
  });
@@ -2087,6 +1932,7 @@ async function runSend(options) {
2087
1932
  await runTerminalConversationSend({
2088
1933
  options,
2089
1934
  conversationId: terminalConversation.conversationId,
1935
+ agent: terminalConversation.agent,
2090
1936
  messageBody,
2091
1937
  terminalControl: terminalConversation.terminalControl
2092
1938
  });
@@ -2450,6 +2296,7 @@ async function runApprove(options) {
2450
2296
  await runTerminalConversationApprove({
2451
2297
  options,
2452
2298
  conversationId: terminalConversation.conversationId,
2299
+ agent: terminalConversation.agent,
2453
2300
  terminalControl: terminalConversation.terminalControl
2454
2301
  });
2455
2302
  return;
@@ -2462,36 +2309,28 @@ async function runApprove(options) {
2462
2309
  if (!terminalControl) {
2463
2310
  throw new Error(`conversation ${conversation.conversation_id} is not controlled through a terminal`);
2464
2311
  }
2465
- const provider = createTerminalControlProvider(options);
2466
- const screen = await provider.capture(terminalControl.target, {
2467
- scrollbackLines: Number(options.scrollbackLines ?? 120),
2468
- socketPath: terminalControl.socketPath
2469
- });
2470
- const approval = detectCodexApprovalPrompt(screen);
2471
- if (!approval.approvable) {
2472
- printJson({
2473
- conversation,
2474
- approved: false,
2475
- blocked: true,
2476
- reason: approval.reason,
2477
- terminal_control: terminalControl,
2478
- screen_excerpt: screenExcerpt(screen)
2479
- });
2480
- return;
2481
- }
2482
- const expectedFingerprint = stringValue(options.expectedApprovalFingerprint);
2483
- 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);
2484
2318
  const autoApproved = options.autoApproved === true;
2485
2319
  const policyRuleId = stringValue(options.policyRuleId);
2486
2320
  const policyFingerprint = stringValue(options.policyFingerprint);
2487
- 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) {
2488
2327
  if (autoApproved) {
2489
2328
  appendEvent(logPath, {
2490
2329
  ts: new Date().toISOString(),
2491
2330
  conversation_id: conversation.conversation_id,
2492
2331
  event: "terminal_auto_approval_decision",
2493
2332
  action: "rejected",
2494
- reason: "approval fingerprint changed before execution",
2333
+ reason: approval.reason,
2495
2334
  terminal_control: terminalControl,
2496
2335
  expected_fingerprint: expectedFingerprint,
2497
2336
  actual_fingerprint: actualFingerprint,
@@ -2502,24 +2341,22 @@ async function runApprove(options) {
2502
2341
  printJson({
2503
2342
  conversation,
2504
2343
  approved: false,
2505
- blocked: true,
2506
- reason: "approval fingerprint changed before execution",
2344
+ blocked: approval.blocked,
2345
+ reason: approval.reason,
2507
2346
  terminal_control: terminalControl,
2508
2347
  expected_approval_fingerprint: expectedFingerprint,
2509
2348
  actual_approval_fingerprint: actualFingerprint,
2510
- screen_excerpt: screenExcerpt(screen)
2349
+ screen_excerpt: approval.screenExcerpt
2511
2350
  });
2512
2351
  return;
2513
2352
  }
2514
- await provider.sendKeys(terminalControl.target, [approval.key], {
2515
- socketPath: terminalControl.socketPath
2516
- });
2517
2353
  appendEvent(logPath, {
2518
2354
  ts: new Date().toISOString(),
2519
2355
  conversation_id: conversation.conversation_id,
2520
2356
  event: "terminal_approval_send",
2521
2357
  terminal_control: terminalControl,
2522
2358
  key: approval.key,
2359
+ keys: approval.keys,
2523
2360
  label: approval.label,
2524
2361
  approval_fingerprint: actualFingerprint,
2525
2362
  auto_approved: autoApproved,
@@ -2542,6 +2379,7 @@ async function runApprove(options) {
2542
2379
  conversation_id: conversation.conversation_id,
2543
2380
  terminal_target: terminalControl.target,
2544
2381
  key: approval.key,
2382
+ keys: approval.keys,
2545
2383
  label: approval.label,
2546
2384
  approval_fingerprint: actualFingerprint,
2547
2385
  auto_approved: autoApproved,
@@ -2607,6 +2445,7 @@ async function runApprove(options) {
2607
2445
  approved: true,
2608
2446
  terminal_control: terminalControl,
2609
2447
  key: approval.key,
2448
+ keys: approval.keys,
2610
2449
  label: approval.label,
2611
2450
  approval_fingerprint: actualFingerprint,
2612
2451
  auto_approved: autoApproved,
@@ -2614,32 +2453,29 @@ async function runApprove(options) {
2614
2453
  monitor_pid: bridgeMonitor?.pid ?? null
2615
2454
  });
2616
2455
  }
2617
- async function runTerminalConversationApprove({ options, conversationId, terminalControl }) {
2618
- const provider = createTerminalControlProvider(options);
2619
- const screen = await provider.capture(terminalControl.target, {
2620
- scrollbackLines: Number(options.scrollbackLines ?? 120),
2621
- 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)
2622
2460
  });
2623
- const approval = detectCodexApprovalPrompt(screen);
2624
- if (!approval.approvable) {
2461
+ if (!approval.approved) {
2625
2462
  printJson({
2626
2463
  conversation_id: conversationId,
2627
2464
  source: "terminal_control",
2628
2465
  approved: false,
2629
- blocked: true,
2466
+ blocked: approval.blocked,
2630
2467
  reason: approval.reason,
2631
2468
  terminal_control: terminalControl,
2632
- screen_excerpt: screenExcerpt(screen)
2469
+ screen_excerpt: approval.screenExcerpt
2633
2470
  });
2634
2471
  return;
2635
2472
  }
2636
- await provider.sendKeys(terminalControl.target, [approval.key], {
2637
- socketPath: terminalControl.socketPath
2638
- });
2639
2473
  runtimeLog("info", "terminal_approval_send", {
2640
2474
  conversation_id: conversationId,
2475
+ agent,
2641
2476
  terminal_target: terminalControl.target,
2642
2477
  key: approval.key,
2478
+ keys: approval.keys,
2643
2479
  label: approval.label
2644
2480
  });
2645
2481
  printJson({
@@ -2648,20 +2484,17 @@ async function runTerminalConversationApprove({ options, conversationId, termina
2648
2484
  approved: true,
2649
2485
  terminal_control: terminalControl,
2650
2486
  key: approval.key,
2651
- label: approval.label
2487
+ keys: approval.keys,
2488
+ label: approval.label,
2489
+ approval_fingerprint: approval.fingerprint
2652
2490
  });
2653
2491
  }
2654
- async function runTerminalConversationSend({ options, conversationId, messageBody, terminalControl }) {
2655
- const provider = createTerminalControlProvider(options);
2492
+ async function runTerminalConversationSend({ options, conversationId, agent, messageBody, terminalControl }) {
2656
2493
  const terminalPayload = terminalSubmissionPayload(String(messageBody));
2657
- await provider.sendText(terminalControl.target, terminalPayload, {
2658
- socketPath: terminalControl.socketPath
2659
- });
2660
- await provider.sendKeys(terminalControl.target, ["C-m"], {
2661
- socketPath: terminalControl.socketPath
2662
- });
2494
+ await createTerminalAgentBridge(options).send(agent, terminalControl, terminalPayload);
2663
2495
  runtimeLog("info", "terminal_message_send", {
2664
2496
  conversation_id: conversationId,
2497
+ agent,
2665
2498
  terminal_target: terminalControl.target,
2666
2499
  message: textSummary(messageBody)
2667
2500
  });
@@ -2705,7 +2538,7 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
2705
2538
  releaseTerminalLock();
2706
2539
  }
2707
2540
  }
2708
- const provider = createTerminalControlProvider(options);
2541
+ const terminalBridge = createTerminalAgentBridge(options);
2709
2542
  const bridgeStartedAt = new Date().toISOString();
2710
2543
  const agentTimeoutMinutes = Number(options.agentTimeoutMinutes ?? DEFAULT_AGENT_TIMEOUT_MINUTES);
2711
2544
  const agentHardTimeoutMinutes = positiveMinutes(options.agentHardTimeoutMinutes ?? DEFAULT_AGENT_HARD_TIMEOUT_MINUTES, "--agent-hard-timeout-minutes");
@@ -2722,22 +2555,16 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
2722
2555
  let preSendScreenFingerprint;
2723
2556
  if (bridge) {
2724
2557
  try {
2725
- const screen = await provider.capture(terminalControl.target, {
2726
- scrollbackLines: Number(options.scrollbackLines ?? 120),
2727
- socketPath: terminalControl.socketPath
2558
+ const status = await terminalBridge.status(executor.kind, terminalControl, {
2559
+ scrollbackLines: Number(options.scrollbackLines ?? 120)
2728
2560
  });
2729
- preSendScreenFingerprint = terminalBridgeScreenFingerprint(screenExcerpt(screen));
2561
+ preSendScreenFingerprint = terminalBridgeScreenFingerprint(status.screen.excerpt);
2730
2562
  }
2731
2563
  catch {
2732
2564
  // Delivery can still succeed when capture is unavailable; prompt matching remains the fallback boundary.
2733
2565
  }
2734
2566
  }
2735
- await provider.sendText(terminalControl.target, terminalPayload, {
2736
- socketPath: terminalControl.socketPath
2737
- });
2738
- await provider.sendKeys(terminalControl.target, ["C-m"], {
2739
- socketPath: terminalControl.socketPath
2740
- });
2567
+ await terminalBridge.send(executor.kind, terminalControl, terminalPayload);
2741
2568
  const supersededConversationIds = bridge
2742
2569
  ? supersedeTerminalBridgeConversations({
2743
2570
  storeDir: storeDirFromOptions(options),
@@ -2827,12 +2654,12 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
2827
2654
  function terminalSubmissionPayload(payload) {
2828
2655
  return payload.replace(/[\r\n]+$/u, "");
2829
2656
  }
2830
- function createManagedTerminalConversationFromRawId({ options, conversationId, messageBody, terminalControl }) {
2657
+ function createManagedTerminalConversationFromRawId({ options, conversationId, agent, messageBody, terminalControl }) {
2831
2658
  const workspace = terminalControl.currentPath ?? process.cwd();
2832
2659
  const storeDir = expandHome(options.storeDir ?? options.logDir ?? defaultStoreDir(workspace));
2833
2660
  cleanupIdleConversations(storeDir, options);
2834
2661
  const executor = resolveExecutor({
2835
- kind: "codex",
2662
+ kind: agent,
2836
2663
  session: conversationId
2837
2664
  });
2838
2665
  const now = new Date();
@@ -2870,12 +2697,12 @@ function createManagedTerminalConversationFromRawId({ options, conversationId, m
2870
2697
  gateway_session: options.gatewaySession ?? options.openclawSession ?? "agent:main:main",
2871
2698
  openclaw_bin: options.openclawBin ?? resolveOptionalExecutable("openclaw"),
2872
2699
  executor_all_proxy: proxyForExecutor(executor, options),
2873
- executor_model: options.model ?? options.codexModel,
2700
+ executor_model: options.model ?? (agent === "codex" ? options.codexModel : undefined),
2874
2701
  native_session_takeover: {
2875
- agent: "codex",
2702
+ agent,
2876
2703
  native_session_id: conversationId,
2877
2704
  source_cwd: workspace,
2878
- source_title: `Terminal-controlled Codex ${terminalControl.target}`,
2705
+ source_title: `Terminal-controlled ${executor.display_name} ${terminalControl.target}`,
2879
2706
  strategy: "terminal_control",
2880
2707
  attached_at: now.toISOString(),
2881
2708
  takeover_match_kind: "raw_terminal_send",
@@ -2890,7 +2717,7 @@ function createManagedTerminalConversationFromRawId({ options, conversationId, m
2890
2717
  conversation_id: attachedConversation.conversation_id,
2891
2718
  event: "raw_terminal_session_attached",
2892
2719
  source_conversation_id: conversationId,
2893
- agent: "codex",
2720
+ agent,
2894
2721
  terminal_control: terminalControl,
2895
2722
  executor
2896
2723
  });
@@ -3496,6 +3323,7 @@ async function runCancel(options) {
3496
3323
  await runTerminalConversationCancel({
3497
3324
  options,
3498
3325
  conversationId: terminalConversation.conversationId,
3326
+ agent: terminalConversation.agent,
3499
3327
  terminalControl: terminalConversation.terminalControl
3500
3328
  });
3501
3329
  return;
@@ -3514,6 +3342,7 @@ async function runCancel(options) {
3514
3342
  conversation,
3515
3343
  statePath,
3516
3344
  logPath,
3345
+ agent: executorForConversation(conversation).kind,
3517
3346
  terminalControl
3518
3347
  });
3519
3348
  return;
@@ -3576,41 +3405,54 @@ async function runCancel(options) {
3576
3405
  budget: budgetAction(nextConversation)
3577
3406
  });
3578
3407
  }
3579
- async function runTerminalConversationCancel({ options, conversationId, terminalControl }) {
3580
- const provider = createTerminalControlProvider(options);
3581
- await provider.sendKeys(terminalControl.target, ["C-c"], {
3582
- socketPath: terminalControl.socketPath
3583
- });
3408
+ async function runTerminalConversationCancel({ options, conversationId, agent, terminalControl }) {
3409
+ const cancellation = await createTerminalAgentBridge(options).cancel(agent, terminalControl);
3584
3410
  runtimeLog("info", "terminal_cancel_requested", {
3585
3411
  conversation_id: conversationId,
3412
+ agent,
3586
3413
  terminal_target: terminalControl.target,
3587
- key: "C-c"
3414
+ key: cancellation.key,
3415
+ keys: cancellation.keys,
3416
+ cancel_requested: cancellation.cancelRequested,
3417
+ reason: cancellation.reason
3588
3418
  });
3589
3419
  printJson({
3590
3420
  conversation_id: conversationId,
3591
3421
  source: "terminal_control",
3592
- cancel_requested: true,
3422
+ cancel_requested: cancellation.cancelRequested,
3423
+ reason: cancellation.reason,
3593
3424
  terminal_control: terminalControl,
3594
- key: "C-c"
3425
+ key: cancellation.key,
3426
+ keys: cancellation.keys
3595
3427
  });
3596
3428
  }
3597
- async function runTerminalControlCancel({ options, conversation, statePath, logPath, terminalControl }) {
3598
- const provider = createTerminalControlProvider(options);
3599
- await provider.sendKeys(terminalControl.target, ["C-c"], {
3600
- socketPath: terminalControl.socketPath
3601
- });
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
+ }
3602
3441
  const now = new Date().toISOString();
3603
3442
  appendEvent(logPath, {
3604
3443
  ts: now,
3605
3444
  conversation_id: conversation.conversation_id,
3606
3445
  event: "terminal_cancel_requested",
3607
3446
  terminal_control: terminalControl,
3608
- key: "C-c"
3447
+ key: cancellation.key,
3448
+ keys: cancellation.keys
3609
3449
  });
3610
3450
  runtimeLog("info", "terminal_cancel_requested", {
3611
3451
  conversation_id: conversation.conversation_id,
3452
+ agent,
3612
3453
  terminal_target: terminalControl.target,
3613
- key: "C-c"
3454
+ key: cancellation.key,
3455
+ keys: cancellation.keys
3614
3456
  });
3615
3457
  const nextConversation = {
3616
3458
  ...conversation,
@@ -3622,7 +3464,8 @@ async function runTerminalControlCancel({ options, conversation, statePath, logP
3622
3464
  conversation: nextConversation,
3623
3465
  cancel_requested: true,
3624
3466
  terminal_control: terminalControl,
3625
- key: "C-c",
3467
+ key: cancellation.key,
3468
+ keys: cancellation.keys,
3626
3469
  budget: budgetAction(nextConversation)
3627
3470
  });
3628
3471
  }
@@ -3824,6 +3667,9 @@ function runClose(options) {
3824
3667
  });
3825
3668
  }
3826
3669
  async function runMonitor(options) {
3670
+ if (options.callbackRetry) {
3671
+ return runCallbackRetryMonitor(options);
3672
+ }
3827
3673
  if (options.terminalBridge) {
3828
3674
  return await runTerminalBridgeMonitor(options);
3829
3675
  }
@@ -3955,6 +3801,72 @@ async function runMonitor(options) {
3955
3801
  sleepSync(pollIntervalMs);
3956
3802
  }
3957
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
+ }
3958
3870
  async function runTerminalBridgeMonitor(options) {
3959
3871
  const statePath = expandHome(required(options.state, "--state is required"));
3960
3872
  const logPath = expandHome(options.log ?? logPathForStatePath(statePath));
@@ -3977,9 +3889,10 @@ async function runTerminalBridgeMonitor(options) {
3977
3889
  const activityPersistIntervalMs = terminalBridgeActivityPersistIntervalMs(timeoutMinutes, pollIntervalMs);
3978
3890
  const preSendScreenFingerprint = stringValue(initialNativeTakeover?.["terminal_bridge_pre_send_screen_fingerprint"]);
3979
3891
  let previousScreenFingerprint = preSendScreenFingerprint;
3980
- let previousRolloutFingerprint;
3892
+ let previousDurableFingerprint;
3981
3893
  let persistedActivityReason = stringValue(initialNativeTakeover?.["terminal_bridge_last_activity_reason"]);
3982
3894
  const executor = executorForConversation(conversation);
3895
+ const terminalBridge = createTerminalAgentBridge(options);
3983
3896
  appendEvent(logPath, {
3984
3897
  ts: new Date().toISOString(),
3985
3898
  conversation_id: conversation.conversation_id,
@@ -4062,7 +3975,29 @@ async function runTerminalBridgeMonitor(options) {
4062
3975
  });
4063
3976
  return;
4064
3977
  }
4065
- 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;
4066
4001
  const approval = terminalStatus.approval_state;
4067
4002
  if (isRecord(approval) && approval.blocked === true) {
4068
4003
  const fingerprint = terminalBridgeApprovalFingerprint({ terminalControl, terminalStatus });
@@ -4117,7 +4052,7 @@ async function runTerminalBridgeMonitor(options) {
4117
4052
  terminalStatus,
4118
4053
  fingerprint
4119
4054
  }),
4120
- approve_command: `AKK approve ${notification.conversation.conversation_id}`,
4055
+ approve_command: `AKK approve ${notification.conversation.conversation_id} --expected-approval-fingerprint ${fingerprint}`,
4121
4056
  deny_command: `AKK cancel ${notification.conversation.conversation_id}`,
4122
4057
  approve_tool: "agent_knock_knock_approve",
4123
4058
  deny_tool: "agent_knock_knock_cancel"
@@ -4155,40 +4090,22 @@ async function runTerminalBridgeMonitor(options) {
4155
4090
  const screenChanged = previousScreenFingerprint !== undefined &&
4156
4091
  screenFingerprint !== undefined &&
4157
4092
  screenFingerprint !== previousScreenFingerprint;
4158
- const screenChangedSinceSend = preSendScreenFingerprint !== undefined &&
4159
- screenFingerprint !== undefined &&
4160
- screenFingerprint !== preSendScreenFingerprint;
4161
4093
  previousScreenFingerprint = screenFingerprint;
4162
- const contextMatch = await terminalBridgeCodexContext({
4163
- conversation,
4164
- nativeTakeover,
4165
- options
4166
- });
4167
- const startedAt = stringValue(nativeTakeover?.["terminal_bridge_started_at"]);
4168
- const rolloutCompletion = contextMatch?.context
4169
- ? latestCompletedRolloutTurn({
4170
- context: contextMatch.context,
4171
- conversation,
4172
- nativeTakeover,
4173
- startedAt
4174
- })
4175
- : undefined;
4176
- const assistantMessage = contextMatch?.context
4177
- ? latestAssistantAfter(contextMatch.context, startedAt)
4178
- : undefined;
4179
- const rolloutFingerprint = rolloutCompletion || assistantMessage
4094
+ const durableCompletion = poll.durableCompletion;
4095
+ const durableFingerprint = durableCompletion
4180
4096
  ? terminalBridgeActivityFingerprint(JSON.stringify({
4181
- text: rolloutCompletion?.text ?? assistantMessage?.text,
4182
- timestamp: rolloutCompletion?.timestamp ?? assistantMessage?.timestamp,
4183
- turn_id: rolloutCompletion?.turnId
4097
+ text: durableCompletion.text,
4098
+ timestamp: durableCompletion.timestamp,
4099
+ id: durableCompletion.id,
4100
+ metadata: durableCompletion.metadata
4184
4101
  }))
4185
4102
  : undefined;
4186
- const rolloutChanged = rolloutFingerprint !== undefined && rolloutFingerprint !== previousRolloutFingerprint;
4187
- previousRolloutFingerprint = rolloutFingerprint;
4103
+ const durableChanged = durableFingerprint !== undefined && durableFingerprint !== previousDurableFingerprint;
4104
+ previousDurableFingerprint = durableFingerprint;
4188
4105
  const activityReasons = [
4189
4106
  terminalStatus.activity_state === "working" ? terminalStatus.activity_reason : undefined,
4190
4107
  screenChanged ? "terminal screen changed" : undefined,
4191
- rolloutChanged ? "Codex rollout assistant output changed" : undefined
4108
+ durableChanged ? "durable completion evidence changed" : undefined
4192
4109
  ].filter((value) => Boolean(value));
4193
4110
  if (activityReasons.length > 0) {
4194
4111
  const observedAtMs = Date.now();
@@ -4213,28 +4130,20 @@ async function runTerminalBridgeMonitor(options) {
4213
4130
  }
4214
4131
  }
4215
4132
  }
4216
- const terminalIdle = terminalStatus.activity_state === "idle";
4217
- const screenMessage = !rolloutCompletion && !assistantMessage && terminalIdle
4218
- ? terminalBridgeScreenMessage({
4219
- conversation,
4220
- nativeTakeover,
4221
- terminalStatus,
4222
- screenChangedSinceSend
4223
- })
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")
4224
4138
  : undefined;
4225
- const completion = rolloutCompletion ?? (terminalIdle ? assistantMessage ?? screenMessage : undefined);
4226
- const completionMatch = rolloutCompletion
4227
- ? "rollout_task_complete"
4228
- : assistantMessage
4229
- ? contextMatch?.match
4230
- : "terminal_screen";
4231
4139
  const completionFingerprint = completion
4232
4140
  ? createHash("sha256")
4233
4141
  .update(JSON.stringify({
4234
4142
  text: completion.text,
4235
4143
  timestamp: completion.timestamp,
4236
4144
  match: completionMatch,
4237
- turn_id: rolloutCompletion?.turnId,
4145
+ source: completion.source,
4146
+ id: completion.id,
4238
4147
  message_id: currentMessageId
4239
4148
  }))
4240
4149
  .digest("hex")
@@ -4248,9 +4157,12 @@ async function runTerminalBridgeMonitor(options) {
4248
4157
  event: "terminal_bridge_completion_detected",
4249
4158
  terminal_control: terminalControl,
4250
4159
  match: completionMatch,
4251
- 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,
4252
4164
  assistant_timestamp: completion?.timestamp,
4253
- rollout_turn_id: rolloutCompletion?.turnId,
4165
+ rollout_turn_id: completion.source === "durable" ? completion.id : undefined,
4254
4166
  terminal_bridge_message_id: currentMessageId
4255
4167
  });
4256
4168
  const callbackMessage = createMessage({
@@ -4263,11 +4175,14 @@ async function runTerminalBridgeMonitor(options) {
4263
4175
  metadata: {
4264
4176
  source: "terminal_bridge",
4265
4177
  terminal_control: terminalControl,
4266
- codex_session: contextMatch?.context.source,
4267
- 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,
4268
4183
  match: completionMatch,
4269
4184
  assistant_timestamp: completion?.timestamp,
4270
- rollout_turn_id: rolloutCompletion?.turnId,
4185
+ rollout_turn_id: completion.source === "durable" ? completion.id : undefined,
4271
4186
  terminal_bridge_message_id: currentMessageId
4272
4187
  }
4273
4188
  });
@@ -4334,7 +4249,7 @@ async function runTerminalBridgeMonitor(options) {
4334
4249
  detail: {
4335
4250
  terminal_bridge: true,
4336
4251
  terminal_control: terminalControl,
4337
- match: contextMatch?.match,
4252
+ match: completionMetadata.context_match,
4338
4253
  terminal_activity_state: terminalStatus.activity_state,
4339
4254
  last_activity_at: new Date(lastActivityAtMs).toISOString(),
4340
4255
  inactivity_deadline_at: new Date(lastActivityAtMs + timeoutMinutes * 60 * 1000).toISOString(),
@@ -4470,7 +4385,7 @@ function terminalBridgeApprovalCandidate({ executor, terminalControl, terminalSt
4470
4385
  terminal_target: terminalControl.target
4471
4386
  };
4472
4387
  }
4473
- async function terminalBridgeCodexContext({ conversation, nativeTakeover, options }) {
4388
+ async function loadCodexTerminalContext({ conversation, nativeTakeover, options }) {
4474
4389
  const provider = createAgentSessionProvider("codex", options);
4475
4390
  const nativeSessionId = stringValue(nativeTakeover?.["native_session_id"]);
4476
4391
  const startedAtMs = Date.parse(String(nativeTakeover?.["terminal_bridge_started_at"] ?? ""));
@@ -4526,146 +4441,6 @@ async function terminalBridgeCodexContext({ conversation, nativeTakeover, option
4526
4441
  confidence: sessions.length === 1 ? "medium" : "low"
4527
4442
  };
4528
4443
  }
4529
- function latestAssistantAfter(context, startedAt) {
4530
- const threshold = startedAt ? Date.parse(startedAt) : undefined;
4531
- return [...visibleRolloutMessages(context)]
4532
- .reverse()
4533
- .find((message) => {
4534
- if (message.role !== "assistant") {
4535
- return false;
4536
- }
4537
- if (!Number.isFinite(threshold)) {
4538
- return true;
4539
- }
4540
- const messageTime = message.timestamp ? Date.parse(message.timestamp) : NaN;
4541
- return Number.isFinite(messageTime) && messageTime >= Number(threshold);
4542
- });
4543
- }
4544
- function latestCompletedRolloutTurn({ context, conversation, nativeTakeover, startedAt }) {
4545
- const threshold = validTimestampMs(startedAt);
4546
- const expectedRequestHash = stringValue(nativeTakeover?.["terminal_bridge_request_hash"]) ??
4547
- terminalBridgeRequestFingerprint(nativeTakeover?.["terminal_bridge_request_text"] ?? conversation.user_request);
4548
- if (threshold === undefined || !expectedRequestHash) {
4549
- return undefined;
4550
- }
4551
- const turn = [...(context.turns ?? [])]
4552
- .reverse()
4553
- .find((candidate) => {
4554
- const userTimestamp = validTimestampMs(candidate.userTimestamp);
4555
- const completedAt = validTimestampMs(candidate.completedAt);
4556
- return candidate.userTextHash === expectedRequestHash &&
4557
- userTimestamp !== undefined &&
4558
- completedAt !== undefined &&
4559
- userTimestamp >= threshold &&
4560
- completedAt >= userTimestamp &&
4561
- Boolean(candidate.lastAssistantMessage);
4562
- });
4563
- if (!turn?.lastAssistantMessage) {
4564
- return undefined;
4565
- }
4566
- return {
4567
- role: "assistant",
4568
- text: turn.lastAssistantMessage,
4569
- timestamp: turn.completedAt,
4570
- turnId: turn.turnId,
4571
- userTimestamp: turn.userTimestamp
4572
- };
4573
- }
4574
- function terminalBridgeScreenMessage({ conversation, nativeTakeover, terminalStatus, screenChangedSinceSend }) {
4575
- const excerpt = stringValue(terminalStatus?.screen?.excerpt);
4576
- if (!excerpt) {
4577
- return undefined;
4578
- }
4579
- const request = String(nativeTakeover?.["terminal_bridge_request_text"] ?? conversation.user_request ?? "").trim();
4580
- const promptEnd = request ? whitespaceInsensitiveMatchEnd(excerpt, request) : undefined;
4581
- const afterPrompt = promptEnd === undefined ? undefined : excerpt.slice(promptEnd);
4582
- const completionBoundary = afterPrompt === undefined
4583
- ? undefined
4584
- : terminalBridgeCompletionBoundary(afterPrompt);
4585
- const completionText = afterPrompt === undefined
4586
- ? screenChangedSinceSend
4587
- ? terminalBridgeLatestCompletedSegment(excerpt)
4588
- : undefined
4589
- : completionBoundary === undefined
4590
- ? afterPrompt
4591
- : afterPrompt.slice(0, completionBoundary);
4592
- if (completionText === undefined) {
4593
- return undefined;
4594
- }
4595
- const cleaned = cleanTerminalBridgeScreenText(completionText);
4596
- const hasCompletionEvidence = promptEnd === undefined || completionBoundary !== undefined || /[•└]/u.test(cleaned ?? "");
4597
- if (!cleaned || cleaned.length < 40 || !hasCompletionEvidence) {
4598
- return undefined;
4599
- }
4600
- return {
4601
- role: "assistant",
4602
- text: truncateText(cleaned, 4000),
4603
- timestamp: undefined
4604
- };
4605
- }
4606
- function whitespaceInsensitiveMatchEnd(text, expected) {
4607
- const normalizedExpected = expected.replace(/\s/gu, "");
4608
- if (!normalizedExpected) {
4609
- return undefined;
4610
- }
4611
- let normalizedText = "";
4612
- const sourceEnds = [];
4613
- for (let index = 0; index < text.length;) {
4614
- const codePoint = text.codePointAt(index);
4615
- if (codePoint === undefined) {
4616
- break;
4617
- }
4618
- const character = String.fromCodePoint(codePoint);
4619
- if (!/\s/u.test(character)) {
4620
- normalizedText += character;
4621
- for (let codeUnit = 0; codeUnit < character.length; codeUnit += 1) {
4622
- sourceEnds.push(index + character.length);
4623
- }
4624
- }
4625
- index += character.length;
4626
- }
4627
- const matchIndex = normalizedText.lastIndexOf(normalizedExpected);
4628
- if (matchIndex < 0) {
4629
- return undefined;
4630
- }
4631
- return sourceEnds[matchIndex + normalizedExpected.length - 1];
4632
- }
4633
- function terminalBridgeLatestCompletedSegment(text) {
4634
- const matches = [...text.matchAll(/^[ \t]*[─━-]+\s+Worked for\b.*$/gmu)];
4635
- const completion = matches.at(-1);
4636
- if (completion?.index === undefined) {
4637
- return undefined;
4638
- }
4639
- const previousCompletion = matches.at(-2);
4640
- let start = previousCompletion?.index === undefined
4641
- ? 0
4642
- : previousCompletion.index + previousCompletion[0].length;
4643
- const beforeCompletion = text.slice(0, completion.index);
4644
- const prompts = [...beforeCompletion.matchAll(/^[ \t]*›(?:\s|$).*$/gmu)];
4645
- const latestPrompt = prompts.at(-1);
4646
- if (latestPrompt?.index !== undefined && latestPrompt.index >= start) {
4647
- start = latestPrompt.index + latestPrompt[0].length;
4648
- }
4649
- return text.slice(start, completion.index);
4650
- }
4651
- function terminalBridgeCompletionBoundary(text) {
4652
- const matches = [...text.matchAll(/^[ \t]*[─━-]+\s+Worked for\b.*$/gmu)];
4653
- return matches.at(-1)?.index;
4654
- }
4655
- function cleanTerminalBridgeScreenText(text) {
4656
- const lines = text
4657
- .split(/\r?\n/)
4658
- .map((line) => line.replace(/\s+$/u, ""))
4659
- .filter((line) => {
4660
- const trimmed = line.trim();
4661
- return trimmed &&
4662
- !trimmed.startsWith("› Use /skills") &&
4663
- !/^gpt-[\w.-]+/u.test(trimmed) &&
4664
- !/^[-\w.]+ default ·/u.test(trimmed);
4665
- });
4666
- const cleaned = lines.join("\n").trim();
4667
- return cleaned || undefined;
4668
- }
4669
4444
  function resolveExecutable(command) {
4670
4445
  if (command.includes(path.sep)) {
4671
4446
  return command;
@@ -4784,19 +4559,62 @@ function runCallback(options) {
4784
4559
  releaseLock();
4785
4560
  }
4786
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
+ }
4787
4593
  function runLockedCallback(options) {
4788
4594
  const messageInput = required(options.messageJson, "--message-json is required");
4789
4595
  const logPath = expandHome(options.log ?? logPathForStatePath(options.statePath));
4790
4596
  const conversation = loadState(options.statePath);
4791
4597
  const executor = executorForConversation(conversation);
4792
- const message = extractStructuredMessage({
4793
- conversation,
4794
- input: messageInput,
4795
- defaultFrom: executor.actor,
4796
- defaultTo: "openclaw"
4797
- });
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
+ }
4798
4609
  const existingEvents = readExistingEvents(logPath);
4799
- 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) {
4800
4618
  runtimeLog("info", "callback_duplicate", {
4801
4619
  conversation_id: conversation.conversation_id,
4802
4620
  agent: executor.kind,
@@ -4816,19 +4634,46 @@ function runLockedCallback(options) {
4816
4634
  });
4817
4635
  return;
4818
4636
  }
4819
- let nextConversation = applyMessageToConversation(conversation, message);
4820
- 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) {
4821
4648
  const now = new Date().toISOString();
4822
4649
  nextConversation = {
4823
4650
  ...nextConversation,
4824
- status: "closed",
4825
- closed_at: now,
4826
- 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
+ },
4827
4664
  updated_at: now
4828
4665
  };
4829
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
+ });
4830
4676
  }
4831
- appendEvent(logPath, messageEvent(message));
4832
4677
  saveState(options.statePath, nextConversation);
4833
4678
  runtimeLog("info", "callback_received", {
4834
4679
  conversation_id: conversation.conversation_id,
@@ -4843,13 +4688,115 @@ function runLockedCallback(options) {
4843
4688
  event_log_path: logPath,
4844
4689
  message: textSummary(message.body)
4845
4690
  });
4846
- const result = {
4847
- conversation: nextConversation,
4848
- message,
4849
- budget: budgetAction(nextConversation),
4850
- delivered: false,
4851
- duplicate: false
4852
- };
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 }) {
4853
4800
  if (options.gatewayMethod) {
4854
4801
  const delivery = deliverToGatewayMethod({
4855
4802
  method: options.gatewayMethod,
@@ -4857,30 +4804,19 @@ function runLockedCallback(options) {
4857
4804
  gatewayUrl: options.gatewayUrl,
4858
4805
  token: options.token,
4859
4806
  sessionKey: options.gatewaySession ?? options.openclawSession ?? conversation.openclaw_session,
4860
- statePath: options.statePath,
4807
+ statePath,
4861
4808
  logPath,
4862
- conversation: nextConversation,
4809
+ conversation,
4863
4810
  message
4864
4811
  });
4865
- appendEvent(logPath, {
4866
- ts: new Date().toISOString(),
4867
- conversation_id: conversation.conversation_id,
4812
+ recordCallbackProcessDelivery({
4813
+ logPath,
4814
+ conversation,
4815
+ message,
4868
4816
  event: "callback_gateway_method_delivery",
4869
- from: message.from,
4870
- to: "openclaw",
4871
- round: message.round,
4872
- method: options.gatewayMethod,
4873
- status: delivery.status,
4874
- stdout: delivery.stdout,
4875
- stderr: delivery.stderr
4876
- });
4877
- runtimeLog("info", "callback_gateway_method_delivery", {
4878
- conversation_id: conversation.conversation_id,
4879
- method: options.gatewayMethod,
4880
- status: delivery.status,
4881
- failure_kind: classifyProcessFailure(delivery),
4882
- stdout: textSummary(delivery.stdout),
4883
- stderr: textSummary(delivery.stderr)
4817
+ runtimeEvent: "callback_gateway_method_delivery",
4818
+ delivery,
4819
+ detail: { method: options.gatewayMethod }
4884
4820
  });
4885
4821
  if (delivery.status !== 0) {
4886
4822
  throw new Error(delivery.stderr || delivery.stdout || `gateway method delivery failed with status ${delivery.status}`);
@@ -4888,84 +4824,47 @@ function runLockedCallback(options) {
4888
4824
  const gatewayPayload = parseOptionalJson(delivery.stdout);
4889
4825
  const chatSendParams = isRecord(gatewayPayload?.chat_send) ? gatewayPayload.chat_send : undefined;
4890
4826
  const sessionSendParams = isRecord(gatewayPayload?.session_send) ? gatewayPayload.session_send : undefined;
4891
- let chatSendDelivery;
4892
- let sessionSendDelivery;
4893
4827
  if (chatSendParams) {
4894
- chatSendDelivery = deliverToChatSend({
4828
+ const chatSendDelivery = deliverToChatSend({
4895
4829
  openclawBin: options.openclawBin,
4896
4830
  gatewayUrl: options.gatewayUrl,
4897
4831
  token: options.token,
4898
4832
  params: chatSendParams
4899
4833
  });
4900
- appendEvent(logPath, {
4901
- ts: new Date().toISOString(),
4902
- conversation_id: conversation.conversation_id,
4834
+ recordCallbackProcessDelivery({
4835
+ logPath,
4836
+ conversation,
4837
+ message,
4903
4838
  event: "callback_chat_send_delivery",
4904
- from: message.from,
4905
- to: "openclaw",
4906
- round: message.round,
4907
- status: chatSendDelivery.status,
4908
- stdout: chatSendDelivery.stdout,
4909
- stderr: chatSendDelivery.stderr
4910
- });
4911
- runtimeLog("info", "callback_chat_send_delivery", {
4912
- conversation_id: conversation.conversation_id,
4913
- status: chatSendDelivery.status,
4914
- failure_kind: classifyProcessFailure(chatSendDelivery),
4915
- stdout: textSummary(chatSendDelivery.stdout),
4916
- stderr: textSummary(chatSendDelivery.stderr)
4839
+ runtimeEvent: "callback_chat_send_delivery",
4840
+ delivery: chatSendDelivery
4917
4841
  });
4918
4842
  if (chatSendDelivery.status !== 0) {
4919
4843
  throw new Error(chatSendDelivery.stderr || chatSendDelivery.stdout || `chat callback delivery failed with status ${chatSendDelivery.status}`);
4920
4844
  }
4845
+ return "gateway_method+chat_send";
4921
4846
  }
4922
- else if (sessionSendParams) {
4923
- sessionSendDelivery = deliverToSessionSend({
4847
+ if (sessionSendParams) {
4848
+ const sessionSendDelivery = deliverToSessionSend({
4924
4849
  openclawBin: options.openclawBin,
4925
4850
  gatewayUrl: options.gatewayUrl,
4926
4851
  token: options.token,
4927
4852
  params: sessionSendParams
4928
4853
  });
4929
- appendEvent(logPath, {
4930
- ts: new Date().toISOString(),
4931
- conversation_id: conversation.conversation_id,
4854
+ recordCallbackProcessDelivery({
4855
+ logPath,
4856
+ conversation,
4857
+ message,
4932
4858
  event: "callback_session_send_delivery",
4933
- from: message.from,
4934
- to: "openclaw",
4935
- round: message.round,
4936
- status: sessionSendDelivery.status,
4937
- stdout: sessionSendDelivery.stdout,
4938
- stderr: sessionSendDelivery.stderr
4939
- });
4940
- runtimeLog("info", "callback_session_send_delivery", {
4941
- conversation_id: conversation.conversation_id,
4942
- status: sessionSendDelivery.status,
4943
- failure_kind: classifyProcessFailure(sessionSendDelivery),
4944
- stdout: textSummary(sessionSendDelivery.stdout),
4945
- stderr: textSummary(sessionSendDelivery.stderr)
4859
+ runtimeEvent: "callback_session_send_delivery",
4860
+ delivery: sessionSendDelivery
4946
4861
  });
4947
4862
  if (sessionSendDelivery.status !== 0) {
4948
4863
  throw new Error(sessionSendDelivery.stderr || sessionSendDelivery.stdout || `session callback delivery failed with status ${sessionSendDelivery.status}`);
4949
4864
  }
4865
+ return "gateway_method+sessions_send";
4950
4866
  }
4951
- printJson({
4952
- ...result,
4953
- delivered: true,
4954
- delivery: chatSendDelivery
4955
- ? "gateway_method+chat_send"
4956
- : sessionSendDelivery
4957
- ? "gateway_method+sessions_send"
4958
- : "gateway_method"
4959
- });
4960
- return;
4961
- }
4962
- if (options.recordOnly) {
4963
- runtimeLog("info", "callback_recorded_only", {
4964
- conversation_id: conversation.conversation_id,
4965
- status: nextConversation.status
4966
- });
4967
- printJson(result);
4968
- return;
4867
+ return "gateway_method";
4969
4868
  }
4970
4869
  const gatewayUrl = options.gatewayUrl ?? conversation.gateway_url;
4971
4870
  const token = options.token ?? conversation.gateway_token;
@@ -4980,31 +4879,40 @@ function runLockedCallback(options) {
4980
4879
  throw new Error("--openclaw-session is required unless state has openclaw_session");
4981
4880
  }
4982
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 = {} }) {
4983
4896
  appendEvent(logPath, {
4984
4897
  ts: new Date().toISOString(),
4985
4898
  conversation_id: conversation.conversation_id,
4986
- event: "callback_delivery",
4899
+ event,
4987
4900
  from: message.from,
4988
4901
  to: "openclaw",
4989
4902
  round: message.round,
4903
+ ...detail,
4990
4904
  status: delivery.status,
4991
4905
  stdout: delivery.stdout,
4992
4906
  stderr: delivery.stderr
4993
4907
  });
4994
- runtimeLog("info", "callback_delivery", {
4908
+ runtimeLog("info", runtimeEvent, {
4995
4909
  conversation_id: conversation.conversation_id,
4910
+ ...detail,
4996
4911
  status: delivery.status,
4997
4912
  failure_kind: classifyProcessFailure(delivery),
4998
4913
  stdout: textSummary(delivery.stdout),
4999
4914
  stderr: textSummary(delivery.stderr)
5000
4915
  });
5001
- if (delivery.status !== 0) {
5002
- throw new Error(delivery.stderr || delivery.stdout || `callback delivery failed with status ${delivery.status}`);
5003
- }
5004
- printJson({
5005
- ...result,
5006
- delivered: true
5007
- });
5008
4916
  }
5009
4917
  function acquireFileLock(lockPath, { timeoutMs = 5000, retryMs = 50 } = {}) {
5010
4918
  const started = Date.now();
@@ -6297,6 +6205,7 @@ function usage() {
6297
6205
  agent-knock-knock approve --conversation <id>
6298
6206
  agent-knock-knock cancel --conversation <id> [--all-proxy <url>]
6299
6207
  agent-knock-knock renew --conversation <id> [--minutes <inactivity-minutes>]
6208
+ agent-knock-knock retry-callback --conversation <id> [--store-dir <dir>]
6300
6209
  agent-knock-knock recover --conversation <id> [--session <name>] [--all-proxy <url>]
6301
6210
  agent-knock-knock close --conversation <id> [--reason <text>]
6302
6211
  agent-knock-knock install-openclaw [--openclaw-bin <path>] [--skill-path <path>] [--skill-only] [--no-restart]