@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.
- package/CHANGELOG.md +21 -0
- package/README.md +135 -244
- package/dist/src/cli.js +638 -729
- package/dist/src/cli.js.map +1 -1
- package/dist/src/codex-session-provider.d.ts +6 -32
- package/dist/src/codex-session-provider.js +2 -1
- package/dist/src/codex-session-provider.js.map +1 -1
- package/dist/src/codex-store-adapter.d.ts +3 -9
- package/dist/src/codex-store-adapter.js +5 -55
- package/dist/src/codex-store-adapter.js.map +1 -1
- package/dist/src/codex-terminal-agent-adapter.d.ts +34 -0
- package/dist/src/codex-terminal-agent-adapter.js +386 -0
- package/dist/src/codex-terminal-agent-adapter.js.map +1 -0
- package/dist/src/openclaw-plugin.js +54 -5
- package/dist/src/openclaw-plugin.js.map +1 -1
- package/dist/src/protocol.d.ts +1 -1
- package/dist/src/terminal-agent-adapter.d.ts +124 -0
- package/dist/src/terminal-agent-adapter.js +109 -0
- package/dist/src/terminal-agent-adapter.js.map +1 -0
- package/dist/src/terminal-agent-bridge.d.ts +97 -0
- package/dist/src/terminal-agent-bridge.js +362 -0
- package/dist/src/terminal-agent-bridge.js.map +1 -0
- package/dist/src/terminal-agent-registry.d.ts +7 -0
- package/dist/src/terminal-agent-registry.js +22 -0
- package/dist/src/terminal-agent-registry.js.map +1 -0
- package/dist/src/terminal-control-provider.d.ts +8 -6
- package/dist/src/terminal-control-provider.js +5 -5
- package/dist/src/terminal-control-provider.js.map +1 -1
- package/dist/src/terminal-process-source.d.ts +25 -0
- package/dist/src/terminal-process-source.js +77 -0
- package/dist/src/terminal-process-source.js.map +1 -0
- package/openclaw.plugin.json +4 -0
- package/package.json +1 -1
- 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 {
|
|
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
|
|
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
|
|
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
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
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
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
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
|
-
|
|
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: `
|
|
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
|
|
1550
|
-
const
|
|
1551
|
-
const
|
|
1552
|
-
|
|
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
|
-
|
|
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
|
|
1527
|
+
id: `native:${session.agent}:${session.pid}`,
|
|
1616
1528
|
source: "native_active",
|
|
1617
|
-
agent:
|
|
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:
|
|
1559
|
+
id: bridge.terminalConversationId(session),
|
|
1648
1560
|
source: "terminal_control",
|
|
1649
|
-
agent:
|
|
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:
|
|
1578
|
+
approve: terminalControl.capabilities.includes("terminal_approval") &&
|
|
1579
|
+
terminalState.approval_state.approvable === true,
|
|
1667
1580
|
status: true,
|
|
1668
|
-
cancel:
|
|
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
|
|
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
|
-
|
|
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:
|
|
1707
|
-
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
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
|
-
|
|
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
|
|
1982
|
-
const
|
|
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
|
-
|
|
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} (${
|
|
1831
|
+
`Approval option: ${label} (${keyDescription})`,
|
|
1990
1832
|
"",
|
|
1991
1833
|
"Safe terminal excerpt:",
|
|
1992
1834
|
"```text",
|
|
1993
1835
|
excerpt,
|
|
1994
1836
|
"```",
|
|
1995
1837
|
"",
|
|
1996
|
-
|
|
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
|
|
2466
|
-
const
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
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
|
-
|
|
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:
|
|
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:
|
|
2506
|
-
reason:
|
|
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
|
|
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
|
|
2619
|
-
|
|
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
|
-
|
|
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:
|
|
2466
|
+
blocked: approval.blocked,
|
|
2630
2467
|
reason: approval.reason,
|
|
2631
2468
|
terminal_control: terminalControl,
|
|
2632
|
-
screen_excerpt: screenExcerpt
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
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:
|
|
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
|
|
2702
|
+
agent,
|
|
2876
2703
|
native_session_id: conversationId,
|
|
2877
2704
|
source_cwd: workspace,
|
|
2878
|
-
source_title: `Terminal-controlled
|
|
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
|
|
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
|
|
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:
|
|
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:
|
|
3422
|
+
cancel_requested: cancellation.cancelRequested,
|
|
3423
|
+
reason: cancellation.reason,
|
|
3593
3424
|
terminal_control: terminalControl,
|
|
3594
|
-
key:
|
|
3425
|
+
key: cancellation.key,
|
|
3426
|
+
keys: cancellation.keys
|
|
3595
3427
|
});
|
|
3596
3428
|
}
|
|
3597
|
-
async function runTerminalControlCancel({ options, conversation, statePath, logPath, terminalControl }) {
|
|
3598
|
-
const
|
|
3599
|
-
|
|
3600
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
4163
|
-
|
|
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:
|
|
4182
|
-
timestamp:
|
|
4183
|
-
|
|
4097
|
+
text: durableCompletion.text,
|
|
4098
|
+
timestamp: durableCompletion.timestamp,
|
|
4099
|
+
id: durableCompletion.id,
|
|
4100
|
+
metadata: durableCompletion.metadata
|
|
4184
4101
|
}))
|
|
4185
4102
|
: undefined;
|
|
4186
|
-
const
|
|
4187
|
-
|
|
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
|
-
|
|
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
|
|
4217
|
-
const
|
|
4218
|
-
|
|
4219
|
-
|
|
4220
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
4267
|
-
|
|
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:
|
|
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:
|
|
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
|
|
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 =
|
|
4793
|
-
|
|
4794
|
-
|
|
4795
|
-
|
|
4796
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4820
|
-
|
|
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: "
|
|
4825
|
-
|
|
4826
|
-
|
|
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
|
-
|
|
4847
|
-
|
|
4848
|
-
|
|
4849
|
-
|
|
4850
|
-
|
|
4851
|
-
|
|
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
|
|
4807
|
+
statePath,
|
|
4861
4808
|
logPath,
|
|
4862
|
-
conversation
|
|
4809
|
+
conversation,
|
|
4863
4810
|
message
|
|
4864
4811
|
});
|
|
4865
|
-
|
|
4866
|
-
|
|
4867
|
-
|
|
4812
|
+
recordCallbackProcessDelivery({
|
|
4813
|
+
logPath,
|
|
4814
|
+
conversation,
|
|
4815
|
+
message,
|
|
4868
4816
|
event: "callback_gateway_method_delivery",
|
|
4869
|
-
|
|
4870
|
-
|
|
4871
|
-
|
|
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
|
-
|
|
4901
|
-
|
|
4902
|
-
|
|
4834
|
+
recordCallbackProcessDelivery({
|
|
4835
|
+
logPath,
|
|
4836
|
+
conversation,
|
|
4837
|
+
message,
|
|
4903
4838
|
event: "callback_chat_send_delivery",
|
|
4904
|
-
|
|
4905
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4930
|
-
|
|
4931
|
-
|
|
4854
|
+
recordCallbackProcessDelivery({
|
|
4855
|
+
logPath,
|
|
4856
|
+
conversation,
|
|
4857
|
+
message,
|
|
4932
4858
|
event: "callback_session_send_delivery",
|
|
4933
|
-
|
|
4934
|
-
|
|
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
|
-
|
|
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
|
|
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",
|
|
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]
|