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