@scotthuang/agent-knock-knock 0.2.44 → 0.2.47
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 +33 -0
- package/README.md +148 -242
- package/dist/src/approval-policy.d.ts +1 -0
- package/dist/src/approval-policy.js +6 -0
- package/dist/src/approval-policy.js.map +1 -1
- package/dist/src/claude-hook-installer.d.ts +65 -0
- package/dist/src/claude-hook-installer.js +410 -0
- package/dist/src/claude-hook-installer.js.map +1 -0
- package/dist/src/claude-hook-protocol.d.ts +89 -0
- package/dist/src/claude-hook-protocol.js +243 -0
- package/dist/src/claude-hook-protocol.js.map +1 -0
- package/dist/src/claude-hook-store.d.ts +247 -0
- package/dist/src/claude-hook-store.js +1064 -0
- package/dist/src/claude-hook-store.js.map +1 -0
- package/dist/src/claude-terminal-agent-adapter.d.ts +47 -0
- package/dist/src/claude-terminal-agent-adapter.js +920 -0
- package/dist/src/claude-terminal-agent-adapter.js.map +1 -0
- package/dist/src/cli.js +1565 -772
- 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 +60 -11
- 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 +158 -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 +114 -0
- package/dist/src/terminal-agent-bridge.js +487 -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 +24 -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 +7 -3
- package/package.json +1 -1
- package/templates/openclaw-skills/agent-knock-knock/SKILL.md +19 -16
package/dist/src/cli.js
CHANGED
|
@@ -5,21 +5,47 @@ 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";
|
|
9
|
+
import { createClaudeTerminalAgentAdapter } from "./claude-terminal-agent-adapter.js";
|
|
10
|
+
import { ClaudeHookStore, ClaudeHookStoreError, defaultClaudeHookStoreDir } from "./claude-hook-store.js";
|
|
11
|
+
import { claudePermissionHookOutput } from "./claude-hook-protocol.js";
|
|
12
|
+
import { defaultClaudeSettingsPath, installClaudeHooks, loadTrustedClaudeTokenjuiceLaunchers } from "./claude-hook-installer.js";
|
|
8
13
|
import { CodexLocalSessionProvider } from "./codex-local-session-provider.js";
|
|
9
14
|
import { CodexStoreAdapter } from "./codex-store-adapter.js";
|
|
10
15
|
import { applyMessageToConversation, budgetAction, createConversation, createMessage, executorForConversation, extractStructuredMessage, parseMessageJson, resolveExecutor } from "./protocol.js";
|
|
11
16
|
import { EXECUTOR_KINDS, acpxCommandForExecutor, executorDefinitionForKind, modelEnvForExecutor, normalizeModelForExecutor, proxyEnvForExecutor } from "./executors.js";
|
|
12
17
|
import { executorBootstrapPrompt } from "./bootstrap.js";
|
|
13
|
-
import {
|
|
18
|
+
import { writeRuntimeLog } from "./runtime-log.js";
|
|
14
19
|
import { formatTranscript, readNdjsonLog } from "./transcript.js";
|
|
15
20
|
import { appendEvent, defaultStoreDir, listConversations, logPathForStatePath, loadConversationById, loadState, messageEvent, pathsForConversation, pathsForConversationDir, saveState, statePathForConversationId } from "./store.js";
|
|
16
21
|
import { planFork, planTakeover } from "./session-takeover-planner.js";
|
|
17
|
-
import { StaticTerminalControlProvider, TmuxTerminalControlProvider
|
|
22
|
+
import { StaticTerminalControlProvider, TmuxTerminalControlProvider } from "./terminal-control-provider.js";
|
|
23
|
+
import { parseTerminalConversationId } from "./terminal-agent-adapter.js";
|
|
24
|
+
import { createProductionTerminalAgentRegistry } from "./terminal-agent-registry.js";
|
|
25
|
+
import { StaticTerminalProcessSource, SystemTerminalProcessSource } from "./terminal-process-source.js";
|
|
26
|
+
import { TerminalAgentBridge } from "./terminal-agent-bridge.js";
|
|
18
27
|
const DEFAULT_IDLE_TIMEOUT_MINUTES = 10080;
|
|
19
28
|
const DEFAULT_AGENT_TIMEOUT_MINUTES = 60;
|
|
20
29
|
const DEFAULT_AGENT_HARD_TIMEOUT_MINUTES = 720;
|
|
21
30
|
const DEFAULT_MONITOR_POLL_INTERVAL_MS = 5000;
|
|
31
|
+
const CALLBACK_RETRY_DELAYS_MS = [5000, 15000, 60000, 60000];
|
|
22
32
|
const DEFAULT_CODEX_ACPX_AGENT_COMMAND = "npx -y @agentclientprotocol/codex-acp@^1.1.0";
|
|
33
|
+
const CONVERSATION_STATUSES = new Set([
|
|
34
|
+
"created",
|
|
35
|
+
"running",
|
|
36
|
+
"waiting_for_agent",
|
|
37
|
+
"waiting_for_openclaw",
|
|
38
|
+
"idle",
|
|
39
|
+
"stalled",
|
|
40
|
+
"needs_recovery",
|
|
41
|
+
"needs_model_selection",
|
|
42
|
+
"callback_pending",
|
|
43
|
+
"callback_failed",
|
|
44
|
+
"failed",
|
|
45
|
+
"closed",
|
|
46
|
+
"cancelled",
|
|
47
|
+
"cancelling"
|
|
48
|
+
]);
|
|
23
49
|
class InlineCodexSessionAdapter {
|
|
24
50
|
threads;
|
|
25
51
|
processes;
|
|
@@ -121,12 +147,21 @@ async function runCommand(commandName, options) {
|
|
|
121
147
|
else if (commandName === "install-openclaw") {
|
|
122
148
|
runInstallOpenClaw(options);
|
|
123
149
|
}
|
|
150
|
+
else if (commandName === "install-claude-hooks") {
|
|
151
|
+
runInstallClaudeHooks(options);
|
|
152
|
+
}
|
|
153
|
+
else if (commandName === "claude-hook") {
|
|
154
|
+
await runClaudeHook(options);
|
|
155
|
+
}
|
|
124
156
|
else if (commandName === "doctor") {
|
|
125
157
|
runDoctor(options);
|
|
126
158
|
}
|
|
127
159
|
else if (commandName === "callback") {
|
|
128
160
|
runCallback(options);
|
|
129
161
|
}
|
|
162
|
+
else if (commandName === "retry-callback") {
|
|
163
|
+
runRetryCallback(options);
|
|
164
|
+
}
|
|
130
165
|
else if (commandName === "monitor") {
|
|
131
166
|
await runMonitor(options);
|
|
132
167
|
}
|
|
@@ -188,6 +223,88 @@ function runInstallOpenClaw(options) {
|
|
|
188
223
|
: "Agent Knock Knock is installed. Try: AKK list"
|
|
189
224
|
});
|
|
190
225
|
}
|
|
226
|
+
function runInstallClaudeHooks(options) {
|
|
227
|
+
const configuredExecutable = stringValue(options.executablePath ?? options.binPath);
|
|
228
|
+
let executablePath;
|
|
229
|
+
if (configuredExecutable) {
|
|
230
|
+
executablePath = path.resolve(expandHome(configuredExecutable));
|
|
231
|
+
}
|
|
232
|
+
else {
|
|
233
|
+
try {
|
|
234
|
+
executablePath = resolveExecutable("agent-knock-knock");
|
|
235
|
+
}
|
|
236
|
+
catch {
|
|
237
|
+
executablePath = path.resolve(process.argv[1]);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
const result = installClaudeHooks({
|
|
241
|
+
executablePath,
|
|
242
|
+
settingsPath: expandHome(options.settingsPath ?? options.settings),
|
|
243
|
+
dryRun: options.dryRun === true
|
|
244
|
+
});
|
|
245
|
+
printJson({
|
|
246
|
+
installed: result.written || !result.changed,
|
|
247
|
+
...result,
|
|
248
|
+
executablePath,
|
|
249
|
+
next: result.dryRun
|
|
250
|
+
? "Run again without --dry-run to install the Claude Code hooks."
|
|
251
|
+
: "Claude Code hooks are configured. Existing sessions pick them up through settings reload; start a new session if needed."
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
async function runClaudeHook(options) {
|
|
255
|
+
const rawInput = fs.readFileSync(0, "utf8");
|
|
256
|
+
let input;
|
|
257
|
+
try {
|
|
258
|
+
input = JSON.parse(rawInput);
|
|
259
|
+
}
|
|
260
|
+
catch {
|
|
261
|
+
throw new Error("Claude hook input must be valid JSON");
|
|
262
|
+
}
|
|
263
|
+
const agentRows = loadClaudeAgentRows(options);
|
|
264
|
+
const claudePid = inferClaudeAncestorPid(agentRows);
|
|
265
|
+
const store = createClaudeHookStore(options);
|
|
266
|
+
const record = store.record(input, {
|
|
267
|
+
...(claudePid === undefined ? {} : { claudePid })
|
|
268
|
+
});
|
|
269
|
+
if (record.event.input.hook_event_name !== "PermissionRequest" || !record.permission) {
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
const permission = record.permission;
|
|
273
|
+
const requestedTimeout = options.permissionWaitTimeoutMs ?? options.timeoutMs;
|
|
274
|
+
const timeoutMs = requestedTimeout === undefined
|
|
275
|
+
? undefined
|
|
276
|
+
: Math.max(0, Number(requestedTimeout));
|
|
277
|
+
try {
|
|
278
|
+
const decision = await store.waitForPermissionDecision({
|
|
279
|
+
sessionId: permission.sessionId,
|
|
280
|
+
requestId: permission.requestId,
|
|
281
|
+
fingerprint: permission.fingerprint,
|
|
282
|
+
conversationId: permission.conversationId,
|
|
283
|
+
messageId: permission.messageId,
|
|
284
|
+
...(timeoutMs === undefined ? {} : { timeoutMs })
|
|
285
|
+
});
|
|
286
|
+
process.stdout.write(`${JSON.stringify(decision?.hookOutput ?? claudePermissionHookOutput({
|
|
287
|
+
behavior: "deny",
|
|
288
|
+
interrupt: false,
|
|
289
|
+
message: "Agent Knock Knock approval timed out. Review the request and retry the task."
|
|
290
|
+
}))}\n`);
|
|
291
|
+
}
|
|
292
|
+
catch (error) {
|
|
293
|
+
if (error instanceof ClaudeHookStoreError && [
|
|
294
|
+
"PERMISSION_EXPIRED",
|
|
295
|
+
"PERMISSION_CONSUMED",
|
|
296
|
+
"PERMISSION_ALREADY_DECIDED"
|
|
297
|
+
].includes(error.code)) {
|
|
298
|
+
process.stdout.write(`${JSON.stringify(claudePermissionHookOutput({
|
|
299
|
+
behavior: "deny",
|
|
300
|
+
interrupt: false,
|
|
301
|
+
message: "Agent Knock Knock approval expired before a safe decision was received."
|
|
302
|
+
}))}\n`);
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
throw error;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
191
308
|
function installOpenClawPlugin(openclawBin, root) {
|
|
192
309
|
const linked = spawnSync(openclawBin, ["plugins", "install", "--link", root], {
|
|
193
310
|
encoding: "utf8",
|
|
@@ -549,7 +666,7 @@ function selectTerminateTarget({ plan, session, activeSessions, expectedPid, all
|
|
|
549
666
|
}
|
|
550
667
|
async function listActiveSessionsWithTerminalControl(provider, options, terminalProvider = createTerminalControlProvider(options)) {
|
|
551
668
|
const activeSessions = await provider.listActiveSessions();
|
|
552
|
-
return
|
|
669
|
+
return createTerminalAgentBridge(options, terminalProvider).attachProcesses(provider.agent, activeSessions);
|
|
553
670
|
}
|
|
554
671
|
function createTerminalControlProvider(options) {
|
|
555
672
|
if (options.terminalsJson || options.terminalScreensJson || options.processesJson) {
|
|
@@ -560,6 +677,161 @@ function createTerminalControlProvider(options) {
|
|
|
560
677
|
}
|
|
561
678
|
return new TmuxTerminalControlProvider();
|
|
562
679
|
}
|
|
680
|
+
function createTerminalProcessSource(options) {
|
|
681
|
+
if (options.processesJson) {
|
|
682
|
+
return new StaticTerminalProcessSource(parseJsonOption(options.processesJson, "--processes-json"));
|
|
683
|
+
}
|
|
684
|
+
return new SystemTerminalProcessSource();
|
|
685
|
+
}
|
|
686
|
+
function createClaudeHookStore(options = {}) {
|
|
687
|
+
const configuredRoot = stringValue(options.claudeHookStoreDir);
|
|
688
|
+
return new ClaudeHookStore({
|
|
689
|
+
rootDir: expandHome(configuredRoot ?? defaultClaudeHookStoreDir())
|
|
690
|
+
});
|
|
691
|
+
}
|
|
692
|
+
function loadClaudeAgentRows(options = {}) {
|
|
693
|
+
let value;
|
|
694
|
+
if (options.claudeAgentsJson !== undefined) {
|
|
695
|
+
value = typeof options.claudeAgentsJson === "string"
|
|
696
|
+
? parseJsonOption(options.claudeAgentsJson, "--claude-agents-json")
|
|
697
|
+
: options.claudeAgentsJson;
|
|
698
|
+
}
|
|
699
|
+
else if (options.processesJson || options.terminalsJson || options.terminalScreensJson) {
|
|
700
|
+
return [];
|
|
701
|
+
}
|
|
702
|
+
else {
|
|
703
|
+
const claudeExecutable = resolveOptionalExecutable("claude");
|
|
704
|
+
if (!claudeExecutable) {
|
|
705
|
+
return [];
|
|
706
|
+
}
|
|
707
|
+
const result = spawnSync(claudeExecutable, ["agents", "--json", "--all"], {
|
|
708
|
+
encoding: "utf8",
|
|
709
|
+
maxBuffer: 1024 * 1024 * 10,
|
|
710
|
+
timeout: 10_000
|
|
711
|
+
});
|
|
712
|
+
if (result.error || result.status !== 0) {
|
|
713
|
+
runtimeLog("warn", "claude_agents_list_failed", {
|
|
714
|
+
status: result.status ?? null,
|
|
715
|
+
error: result.error?.message,
|
|
716
|
+
stderr: textSummary(cleanProcessText(result.stderr))
|
|
717
|
+
});
|
|
718
|
+
return [];
|
|
719
|
+
}
|
|
720
|
+
try {
|
|
721
|
+
value = JSON.parse(result.stdout);
|
|
722
|
+
}
|
|
723
|
+
catch {
|
|
724
|
+
runtimeLog("warn", "claude_agents_list_invalid_json", {
|
|
725
|
+
stdout: textSummary(result.stdout)
|
|
726
|
+
});
|
|
727
|
+
return [];
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
const rows = Array.isArray(value)
|
|
731
|
+
? value
|
|
732
|
+
: isRecord(value) && Array.isArray(value.agents)
|
|
733
|
+
? value.agents
|
|
734
|
+
: [];
|
|
735
|
+
return rows.flatMap((row) => {
|
|
736
|
+
if (!isRecord(row) || !Number.isInteger(Number(row.pid))) {
|
|
737
|
+
return [];
|
|
738
|
+
}
|
|
739
|
+
return [{
|
|
740
|
+
pid: Number(row.pid),
|
|
741
|
+
...(stringValue(row.cwd) ? { cwd: stringValue(row.cwd) } : {}),
|
|
742
|
+
...(stringValue(row.kind) ? { kind: stringValue(row.kind) } : {}),
|
|
743
|
+
...(stringValue(row.sessionId) ? { sessionId: stringValue(row.sessionId) } : {}),
|
|
744
|
+
...(stringValue(row.status) ? { status: stringValue(row.status) } : {})
|
|
745
|
+
}];
|
|
746
|
+
});
|
|
747
|
+
}
|
|
748
|
+
function inferClaudeAncestorPid(agentRows, startingPid = process.ppid) {
|
|
749
|
+
const interactivePids = new Set(agentRows
|
|
750
|
+
.filter((row) => row.kind === undefined || row.kind === "interactive")
|
|
751
|
+
.map((row) => row.pid)
|
|
752
|
+
.filter((pid) => Number.isInteger(pid)));
|
|
753
|
+
let pid = startingPid;
|
|
754
|
+
const visited = new Set();
|
|
755
|
+
for (let depth = 0; depth < 16 && Number.isInteger(pid) && pid > 1 && !visited.has(pid); depth += 1) {
|
|
756
|
+
visited.add(pid);
|
|
757
|
+
const processRow = spawnSync("ps", ["-p", String(pid), "-o", "pid=,ppid=,command="], {
|
|
758
|
+
encoding: "utf8",
|
|
759
|
+
timeout: 2_000
|
|
760
|
+
});
|
|
761
|
+
if (processRow.error || processRow.status !== 0) {
|
|
762
|
+
return undefined;
|
|
763
|
+
}
|
|
764
|
+
const match = /^\s*(\d+)\s+(\d+)\s+(.+)$/u.exec(String(processRow.stdout).trim());
|
|
765
|
+
if (!match) {
|
|
766
|
+
return undefined;
|
|
767
|
+
}
|
|
768
|
+
const currentPid = Number(match[1]);
|
|
769
|
+
const parentPid = Number(match[2]);
|
|
770
|
+
const commandText = match[3];
|
|
771
|
+
if (interactivePids.has(currentPid) || isClaudeProcessCommand(commandText)) {
|
|
772
|
+
return currentPid;
|
|
773
|
+
}
|
|
774
|
+
pid = parentPid;
|
|
775
|
+
}
|
|
776
|
+
return undefined;
|
|
777
|
+
}
|
|
778
|
+
function isClaudeProcessCommand(commandText) {
|
|
779
|
+
const executable = commandText.trim().split(/\s+/u, 1)[0];
|
|
780
|
+
return path.basename(executable) === "claude" ||
|
|
781
|
+
/[\\/]\.local[\\/]share[\\/]claude[\\/]versions[\\/][^\\/\s]+$/u.test(executable);
|
|
782
|
+
}
|
|
783
|
+
function createRuntimeTerminalAgentRegistry(options) {
|
|
784
|
+
return createProductionTerminalAgentRegistry({
|
|
785
|
+
overrides: [
|
|
786
|
+
createCodexTerminalAgentAdapter({
|
|
787
|
+
async detectDurableCompletion(request) {
|
|
788
|
+
const runtime = isRecord(request.context) ? request.context : undefined;
|
|
789
|
+
const conversation = runtime?.conversation;
|
|
790
|
+
const nativeTakeover = isRecord(runtime?.nativeTakeover)
|
|
791
|
+
? runtime?.nativeTakeover
|
|
792
|
+
: undefined;
|
|
793
|
+
if (!isRecord(conversation)) {
|
|
794
|
+
return undefined;
|
|
795
|
+
}
|
|
796
|
+
const contextMatch = await loadCodexTerminalContext({
|
|
797
|
+
conversation,
|
|
798
|
+
nativeTakeover,
|
|
799
|
+
options
|
|
800
|
+
});
|
|
801
|
+
if (!contextMatch?.context) {
|
|
802
|
+
return undefined;
|
|
803
|
+
}
|
|
804
|
+
const evidence = detectCodexDurableCompletion({
|
|
805
|
+
...request,
|
|
806
|
+
context: contextMatch.context
|
|
807
|
+
});
|
|
808
|
+
return evidence
|
|
809
|
+
? {
|
|
810
|
+
...evidence,
|
|
811
|
+
confidence: contextMatch.confidence,
|
|
812
|
+
metadata: {
|
|
813
|
+
...evidence.metadata,
|
|
814
|
+
context_match: contextMatch.match,
|
|
815
|
+
session: contextMatch.context.source
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
: undefined;
|
|
819
|
+
}
|
|
820
|
+
}),
|
|
821
|
+
createClaudeTerminalAgentAdapter({
|
|
822
|
+
agentRows: loadClaudeAgentRows(options),
|
|
823
|
+
hookStore: createClaudeHookStore(options),
|
|
824
|
+
trustedTokenjuiceLaunchers: loadTrustedClaudeTokenjuiceLaunchers(expandHome(options.claudeSettingsPath ?? defaultClaudeSettingsPath()))
|
|
825
|
+
})
|
|
826
|
+
]
|
|
827
|
+
});
|
|
828
|
+
}
|
|
829
|
+
function createTerminalAgentBridge(options, terminalProvider = createTerminalControlProvider(options)) {
|
|
830
|
+
return new TerminalAgentBridge({
|
|
831
|
+
registry: createRuntimeTerminalAgentRegistry(options),
|
|
832
|
+
terminalProvider
|
|
833
|
+
});
|
|
834
|
+
}
|
|
563
835
|
function planTerminalControlTakeover(session, activeSessions) {
|
|
564
836
|
const matched = activeSessions
|
|
565
837
|
.filter((process) => process.kind === "codex_cli" &&
|
|
@@ -609,6 +881,9 @@ function terminalControlFromTakeover(nativeTakeover) {
|
|
|
609
881
|
if (!target || !session || !Number.isInteger(window) || !Number.isInteger(pane) || !Number.isInteger(panePid)) {
|
|
610
882
|
return undefined;
|
|
611
883
|
}
|
|
884
|
+
const storedCapabilities = Array.isArray(terminalControl.capabilities)
|
|
885
|
+
? terminalControl.capabilities.filter(isTerminalControlCapability)
|
|
886
|
+
: [];
|
|
612
887
|
return {
|
|
613
888
|
kind: "tmux",
|
|
614
889
|
target,
|
|
@@ -619,181 +894,47 @@ function terminalControlFromTakeover(nativeTakeover) {
|
|
|
619
894
|
currentCommand: stringValue(terminalControl.currentCommand),
|
|
620
895
|
currentPath: stringValue(terminalControl.currentPath),
|
|
621
896
|
socketPath: stringValue(terminalControl.socketPath),
|
|
622
|
-
capabilities
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
for (const line of prompt.region.split(/\r?\n/)) {
|
|
634
|
-
const match = /^[\s›]*1\.\s+(Yes,[^(]+)\(([^)]+)\)/u.exec(line.trim());
|
|
635
|
-
if (!match) {
|
|
636
|
-
continue;
|
|
637
|
-
}
|
|
638
|
-
const key = match[2].trim();
|
|
639
|
-
if (key !== "y") {
|
|
640
|
-
return {
|
|
641
|
-
approvable: false,
|
|
642
|
-
reason: `primary approval shortcut is ${key}, not y`,
|
|
643
|
-
...approvalCandidateFromPrompt(prompt.marker, prompt.region)
|
|
644
|
-
};
|
|
645
|
-
}
|
|
646
|
-
return {
|
|
647
|
-
approvable: true,
|
|
648
|
-
key,
|
|
649
|
-
label: match[1].trim(),
|
|
650
|
-
...approvalCandidateFromPrompt(prompt.marker, prompt.region)
|
|
651
|
-
};
|
|
652
|
-
}
|
|
653
|
-
return {
|
|
654
|
-
approvable: false,
|
|
655
|
-
reason: "no primary approve option with a shortcut was detected",
|
|
656
|
-
...approvalCandidateFromPrompt(prompt.marker, prompt.region)
|
|
657
|
-
};
|
|
658
|
-
}
|
|
659
|
-
function isCodexApprovalPromptVisible(screen) {
|
|
660
|
-
return codexApprovalPromptRegion(screen).visible;
|
|
661
|
-
}
|
|
662
|
-
function codexApprovalPromptRegion(screen) {
|
|
663
|
-
const approvalMarkers = [
|
|
664
|
-
"Would you like to run the following command?",
|
|
665
|
-
"Would you like to make the following edits?",
|
|
666
|
-
"Would you like to grant these permissions?",
|
|
667
|
-
"needs your approval."
|
|
668
|
-
];
|
|
669
|
-
const lines = screen.split(/\r?\n/);
|
|
670
|
-
let markerIndex = -1;
|
|
671
|
-
let matchedMarker = "";
|
|
672
|
-
for (let index = lines.length - 1; index >= 0; index -= 1) {
|
|
673
|
-
const marker = approvalMarkers.find((candidate) => lines[index].includes(candidate));
|
|
674
|
-
if (marker) {
|
|
675
|
-
markerIndex = index;
|
|
676
|
-
matchedMarker = marker;
|
|
677
|
-
break;
|
|
678
|
-
}
|
|
679
|
-
}
|
|
680
|
-
if (markerIndex < 0) {
|
|
681
|
-
return {
|
|
682
|
-
visible: false,
|
|
683
|
-
reason: "no Codex approval prompt was detected in the terminal screen"
|
|
684
|
-
};
|
|
685
|
-
}
|
|
686
|
-
const regionLines = lines.slice(markerIndex);
|
|
687
|
-
const staleLine = regionLines.slice(1).find((line) => isPostApprovalActivityLine(line));
|
|
688
|
-
if (staleLine) {
|
|
689
|
-
return {
|
|
690
|
-
visible: false,
|
|
691
|
-
reason: `Codex approval prompt appears stale after later terminal activity: ${staleLine.trim()}`
|
|
692
|
-
};
|
|
693
|
-
}
|
|
694
|
-
return {
|
|
695
|
-
visible: true,
|
|
696
|
-
region: regionLines.join("\n"),
|
|
697
|
-
marker: matchedMarker
|
|
698
|
-
};
|
|
699
|
-
}
|
|
700
|
-
function approvalCandidateFromPrompt(marker, region) {
|
|
701
|
-
const promptKind = marker === "Would you like to run the following command?"
|
|
702
|
-
? "run_command"
|
|
703
|
-
: marker === "Would you like to make the following edits?"
|
|
704
|
-
? "file_edit"
|
|
705
|
-
: marker === "Would you like to grant these permissions?"
|
|
706
|
-
? "grant_permissions"
|
|
707
|
-
: "unknown";
|
|
708
|
-
return {
|
|
709
|
-
promptKind,
|
|
710
|
-
command: promptKind === "run_command" ? commandFromApprovalRegion(region) : undefined
|
|
897
|
+
// State written before adapter capabilities were persisted always represented Codex.
|
|
898
|
+
capabilities: storedCapabilities.length > 0
|
|
899
|
+
? storedCapabilities
|
|
900
|
+
: [
|
|
901
|
+
"screen_status",
|
|
902
|
+
"send_keys",
|
|
903
|
+
"terminal_approval",
|
|
904
|
+
"screen_completion",
|
|
905
|
+
"durable_completion",
|
|
906
|
+
"terminal_cancel"
|
|
907
|
+
]
|
|
711
908
|
};
|
|
712
909
|
}
|
|
713
|
-
function
|
|
714
|
-
const
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
const
|
|
720
|
-
|
|
721
|
-
const line = lines[index];
|
|
722
|
-
if (index > commandStart && (!line.trim() || /^[\s›]*\d+\.\s+/u.test(line) || /Press enter to confirm/u.test(line))) {
|
|
723
|
-
break;
|
|
724
|
-
}
|
|
725
|
-
parts.push(index === commandStart ? line.replace(/^\s*\$\s+/u, "").trim() : line.trim());
|
|
726
|
-
}
|
|
727
|
-
const command = parts.filter(Boolean).join(" ").trim();
|
|
728
|
-
return command ? redactString(command) : undefined;
|
|
729
|
-
}
|
|
730
|
-
function isPostApprovalActivityLine(line) {
|
|
731
|
-
const trimmed = line.trim();
|
|
732
|
-
if (!trimmed) {
|
|
733
|
-
return false;
|
|
734
|
-
}
|
|
735
|
-
if (/^✔\s+You approved\b/u.test(trimmed)) {
|
|
736
|
-
return true;
|
|
737
|
-
}
|
|
738
|
-
if (/^›\s+(?!1\.)\S/u.test(trimmed)) {
|
|
739
|
-
return true;
|
|
740
|
-
}
|
|
741
|
-
if (/^•\s+(Working|Ran|Explored|Edited|Read|Called|Searching|Planning|Updated|Added|Deleted|Modified|Running|Thinking)\b/u.test(trimmed)) {
|
|
742
|
-
return true;
|
|
743
|
-
}
|
|
744
|
-
return /^─\s*Worked for\b/u.test(trimmed);
|
|
745
|
-
}
|
|
746
|
-
function detectCodexActivityState(screen, approval = detectCodexApprovalPrompt(screen)) {
|
|
747
|
-
if (approval.approvable || isCodexApprovalPromptVisible(screen)) {
|
|
748
|
-
return {
|
|
749
|
-
state: "awaiting_approval",
|
|
750
|
-
reason: "current Codex approval prompt is visible"
|
|
751
|
-
};
|
|
752
|
-
}
|
|
753
|
-
const tailLines = screen.trimEnd().split(/\r?\n/).slice(-30);
|
|
754
|
-
const workingLine = tailLines.find((line) => isCodexWorkingLine(line));
|
|
755
|
-
if (workingLine) {
|
|
756
|
-
return {
|
|
757
|
-
state: "working",
|
|
758
|
-
reason: `Codex working marker detected: ${workingLine.trim()}`
|
|
759
|
-
};
|
|
760
|
-
}
|
|
761
|
-
const idleLine = tailLines
|
|
762
|
-
.slice(-6)
|
|
763
|
-
.map((line) => line.trim())
|
|
764
|
-
.find((line) => isCodexIdlePromptLine(line));
|
|
765
|
-
if (idleLine) {
|
|
766
|
-
return {
|
|
767
|
-
state: "idle",
|
|
768
|
-
reason: `Codex input prompt detected: ${idleLine}`
|
|
769
|
-
};
|
|
770
|
-
}
|
|
910
|
+
function terminalRuntimeIdentityForConversation(conversation, terminalControl) {
|
|
911
|
+
const nativeTakeover = isRecord(conversation?.native_session_takeover)
|
|
912
|
+
? conversation.native_session_takeover
|
|
913
|
+
: undefined;
|
|
914
|
+
const nativeSessionId = stringValue(nativeTakeover?.native_session_id);
|
|
915
|
+
const terminalIdentity = parseTerminalConversationId(nativeSessionId);
|
|
916
|
+
const explicitSessionId = stringValue(nativeTakeover?.terminal_agent_session_id) ??
|
|
917
|
+
(terminalIdentity ? undefined : nativeSessionId);
|
|
771
918
|
return {
|
|
772
|
-
|
|
773
|
-
|
|
919
|
+
pid: Number.isInteger(Number(nativeTakeover?.terminal_agent_pid))
|
|
920
|
+
? Number(nativeTakeover?.terminal_agent_pid)
|
|
921
|
+
: terminalIdentity?.pid,
|
|
922
|
+
sessionId: explicitSessionId,
|
|
923
|
+
cwd: stringValue(nativeTakeover?.source_cwd) ?? terminalControl.currentPath,
|
|
924
|
+
conversationId: stringValue(conversation?.conversation_id),
|
|
925
|
+
messageId: stringValue(nativeTakeover?.terminal_bridge_message_id),
|
|
926
|
+
terminalTarget: terminalControl.target
|
|
774
927
|
};
|
|
775
928
|
}
|
|
776
|
-
function
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
return true;
|
|
786
|
-
}
|
|
787
|
-
return /^\d+\s+background terminals? running\b/u.test(trimmed) && /\/(?:ps|stop)\b/u.test(trimmed);
|
|
788
|
-
}
|
|
789
|
-
function isCodexIdlePromptLine(line) {
|
|
790
|
-
const trimmed = line.trim();
|
|
791
|
-
return /^›(?:\s|$)/u.test(trimmed) && !/^›\s*1\./u.test(trimmed);
|
|
792
|
-
}
|
|
793
|
-
function screenExcerpt(screen, maxLength = 4000) {
|
|
794
|
-
const lines = screen.trimEnd().split(/\r?\n/);
|
|
795
|
-
const excerpt = lines.slice(Math.max(0, lines.length - 80)).join("\n");
|
|
796
|
-
return redactString(excerpt).slice(-maxLength);
|
|
929
|
+
function isTerminalControlCapability(value) {
|
|
930
|
+
return typeof value === "string" && [
|
|
931
|
+
"screen_status",
|
|
932
|
+
"send_keys",
|
|
933
|
+
"terminal_approval",
|
|
934
|
+
"screen_completion",
|
|
935
|
+
"durable_completion",
|
|
936
|
+
"terminal_cancel"
|
|
937
|
+
].includes(value);
|
|
797
938
|
}
|
|
798
939
|
function createForkConversation({ agent, strategy, session, contextPackage, forkSummary, modelInfo, options }) {
|
|
799
940
|
const workspace = session.cwd;
|
|
@@ -1336,7 +1477,7 @@ function startExecutorMonitor({ statePath, logPath, pid, outputPath, agentTimeou
|
|
|
1336
1477
|
child.unref();
|
|
1337
1478
|
return child;
|
|
1338
1479
|
}
|
|
1339
|
-
function startTerminalBridgeMonitor({ statePath, logPath, agentTimeoutMinutes, agentHardTimeoutMinutes, pollIntervalMs, codexHome }) {
|
|
1480
|
+
function startTerminalBridgeMonitor({ statePath, logPath, agentTimeoutMinutes, agentHardTimeoutMinutes, pollIntervalMs, codexHome, claudeHookStoreDir }) {
|
|
1340
1481
|
const args = [
|
|
1341
1482
|
new URL(import.meta.url).pathname,
|
|
1342
1483
|
"monitor",
|
|
@@ -1355,6 +1496,9 @@ function startTerminalBridgeMonitor({ statePath, logPath, agentTimeoutMinutes, a
|
|
|
1355
1496
|
if (codexHome) {
|
|
1356
1497
|
args.push("--codex-home", codexHome);
|
|
1357
1498
|
}
|
|
1499
|
+
if (claudeHookStoreDir) {
|
|
1500
|
+
args.push("--claude-hook-store-dir", claudeHookStoreDir);
|
|
1501
|
+
}
|
|
1358
1502
|
const child = spawn(process.execPath, args, {
|
|
1359
1503
|
detached: true,
|
|
1360
1504
|
stdio: "ignore",
|
|
@@ -1381,7 +1525,8 @@ function startTerminalBridgeMonitorForConversation({ conversation, statePath, lo
|
|
|
1381
1525
|
nativeTakeover?.["terminal_bridge_hard_timeout_minutes"] ??
|
|
1382
1526
|
DEFAULT_AGENT_HARD_TIMEOUT_MINUTES),
|
|
1383
1527
|
pollIntervalMs: Number(options.monitorPollIntervalMs ?? DEFAULT_MONITOR_POLL_INTERVAL_MS),
|
|
1384
|
-
codexHome: options.codexHome
|
|
1528
|
+
codexHome: options.codexHome,
|
|
1529
|
+
claudeHookStoreDir: options.claudeHookStoreDir ?? nativeTakeover?.["claude_hook_store_dir"]
|
|
1385
1530
|
});
|
|
1386
1531
|
}
|
|
1387
1532
|
function terminalBridgeEnabled(conversation) {
|
|
@@ -1404,6 +1549,7 @@ function withTerminalBridgeState({ conversation, message, startedAt, agentTimeou
|
|
|
1404
1549
|
terminal_bridge_request_text: String(message.body ?? ""),
|
|
1405
1550
|
terminal_bridge_request_hash: terminalBridgeRequestFingerprint(message.body),
|
|
1406
1551
|
terminal_bridge_pre_send_screen_fingerprint: preSendScreenFingerprint,
|
|
1552
|
+
terminal_bridge_completion_claim: undefined,
|
|
1407
1553
|
terminal_bridge_monitor_started_at: startedAt,
|
|
1408
1554
|
terminal_bridge_last_activity_at: startedAt,
|
|
1409
1555
|
terminal_bridge_inactivity_timeout_minutes: agentTimeoutMinutes,
|
|
@@ -1414,6 +1560,165 @@ function withTerminalBridgeState({ conversation, message, startedAt, agentTimeou
|
|
|
1414
1560
|
updated_at: startedAt
|
|
1415
1561
|
};
|
|
1416
1562
|
}
|
|
1563
|
+
function activateClaudeHookLease({ options, conversation, message, terminalControl, expiresAt }) {
|
|
1564
|
+
const runtime = terminalRuntimeIdentityForConversation(conversation, terminalControl);
|
|
1565
|
+
const pid = runtime.pid;
|
|
1566
|
+
const agentRow = pid === undefined
|
|
1567
|
+
? undefined
|
|
1568
|
+
: loadClaudeAgentRows(options).find((row) => row.pid === pid && (row.kind === undefined || row.kind === "interactive"));
|
|
1569
|
+
const sessionId = agentRow?.sessionId ?? runtime.sessionId;
|
|
1570
|
+
const cwd = agentRow?.cwd ?? runtime.cwd ?? terminalControl.currentPath;
|
|
1571
|
+
if (!cwd || (pid === undefined && !sessionId)) {
|
|
1572
|
+
runtimeLog("warn", "claude_hook_lease_unavailable", {
|
|
1573
|
+
conversation_id: conversation.conversation_id,
|
|
1574
|
+
terminal_target: terminalControl.target,
|
|
1575
|
+
reason: "exact Claude pid/session identity is unavailable"
|
|
1576
|
+
});
|
|
1577
|
+
return undefined;
|
|
1578
|
+
}
|
|
1579
|
+
const store = createClaudeHookStore(options);
|
|
1580
|
+
try {
|
|
1581
|
+
const previous = store.resolveLease({
|
|
1582
|
+
...(sessionId === undefined ? {} : { sessionId }),
|
|
1583
|
+
...(pid === undefined ? {} : { pid }),
|
|
1584
|
+
cwd,
|
|
1585
|
+
requireUnique: true
|
|
1586
|
+
});
|
|
1587
|
+
if (previous && (previous.lease.conversationId !== conversation.conversation_id ||
|
|
1588
|
+
previous.lease.messageId !== message.id)) {
|
|
1589
|
+
store.releaseLease({ leaseId: previous.lease.id });
|
|
1590
|
+
}
|
|
1591
|
+
const lease = store.activateLease({
|
|
1592
|
+
...(sessionId === undefined ? {} : { sessionId }),
|
|
1593
|
+
...(pid === undefined ? {} : { pid }),
|
|
1594
|
+
cwd,
|
|
1595
|
+
conversationId: conversation.conversation_id,
|
|
1596
|
+
messageId: message.id,
|
|
1597
|
+
terminalTarget: terminalControl.target,
|
|
1598
|
+
expiresAt
|
|
1599
|
+
});
|
|
1600
|
+
runtimeLog("info", "claude_hook_lease_activated", {
|
|
1601
|
+
conversation_id: conversation.conversation_id,
|
|
1602
|
+
message_id: message.id,
|
|
1603
|
+
terminal_target: terminalControl.target,
|
|
1604
|
+
lease_id: lease.id,
|
|
1605
|
+
pid,
|
|
1606
|
+
session_id: sessionId,
|
|
1607
|
+
expires_at: lease.expiresAt
|
|
1608
|
+
});
|
|
1609
|
+
return {
|
|
1610
|
+
lease,
|
|
1611
|
+
storeDir: store.rootDir,
|
|
1612
|
+
...(pid === undefined ? {} : { pid }),
|
|
1613
|
+
...(sessionId === undefined ? {} : { sessionId })
|
|
1614
|
+
};
|
|
1615
|
+
}
|
|
1616
|
+
catch (error) {
|
|
1617
|
+
runtimeLog("warn", "claude_hook_lease_unavailable", {
|
|
1618
|
+
conversation_id: conversation.conversation_id,
|
|
1619
|
+
terminal_target: terminalControl.target,
|
|
1620
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
1621
|
+
});
|
|
1622
|
+
return undefined;
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
function withClaudeHookLeaseState(conversation, state) {
|
|
1626
|
+
const nativeTakeover = isRecord(conversation.native_session_takeover)
|
|
1627
|
+
? conversation.native_session_takeover
|
|
1628
|
+
: {};
|
|
1629
|
+
return {
|
|
1630
|
+
...conversation,
|
|
1631
|
+
native_session_takeover: {
|
|
1632
|
+
...nativeTakeover,
|
|
1633
|
+
claude_hook_mode: "enabled",
|
|
1634
|
+
claude_hook_store_dir: state.storeDir,
|
|
1635
|
+
claude_hook_lease_id: state.lease.id,
|
|
1636
|
+
terminal_agent_pid: state.pid,
|
|
1637
|
+
terminal_agent_session_id: state.sessionId
|
|
1638
|
+
}
|
|
1639
|
+
};
|
|
1640
|
+
}
|
|
1641
|
+
function releaseClaudeHookLease(conversation) {
|
|
1642
|
+
const nativeTakeover = isRecord(conversation?.native_session_takeover)
|
|
1643
|
+
? conversation.native_session_takeover
|
|
1644
|
+
: undefined;
|
|
1645
|
+
const leaseId = stringValue(nativeTakeover?.claude_hook_lease_id);
|
|
1646
|
+
if (!leaseId || nativeTakeover?.agent !== "claude") {
|
|
1647
|
+
return;
|
|
1648
|
+
}
|
|
1649
|
+
const storeDir = stringValue(nativeTakeover.claude_hook_store_dir) ?? defaultClaudeHookStoreDir();
|
|
1650
|
+
try {
|
|
1651
|
+
const released = new ClaudeHookStore({ rootDir: storeDir }).releaseLease({ leaseId });
|
|
1652
|
+
runtimeLog("info", "claude_hook_lease_released", {
|
|
1653
|
+
conversation_id: conversation.conversation_id,
|
|
1654
|
+
message_id: released?.messageId,
|
|
1655
|
+
lease_id: leaseId
|
|
1656
|
+
});
|
|
1657
|
+
}
|
|
1658
|
+
catch (error) {
|
|
1659
|
+
runtimeLog("warn", "claude_hook_lease_release_failed", {
|
|
1660
|
+
conversation_id: conversation.conversation_id,
|
|
1661
|
+
lease_id: leaseId,
|
|
1662
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
1663
|
+
});
|
|
1664
|
+
}
|
|
1665
|
+
}
|
|
1666
|
+
function renewClaudeHookLease(conversation, expiresAt) {
|
|
1667
|
+
const nativeTakeover = isRecord(conversation?.native_session_takeover)
|
|
1668
|
+
? conversation.native_session_takeover
|
|
1669
|
+
: undefined;
|
|
1670
|
+
if (nativeTakeover?.agent !== "claude") {
|
|
1671
|
+
return undefined;
|
|
1672
|
+
}
|
|
1673
|
+
const conversationId = stringValue(conversation.conversation_id);
|
|
1674
|
+
const messageId = stringValue(nativeTakeover.terminal_bridge_message_id);
|
|
1675
|
+
const terminalControl = terminalControlFromTakeover(nativeTakeover);
|
|
1676
|
+
const cwd = stringValue(nativeTakeover.source_cwd) ?? terminalControl?.currentPath;
|
|
1677
|
+
const pid = Number.isInteger(Number(nativeTakeover.terminal_agent_pid))
|
|
1678
|
+
? Number(nativeTakeover.terminal_agent_pid)
|
|
1679
|
+
: undefined;
|
|
1680
|
+
const sessionId = stringValue(nativeTakeover.terminal_agent_session_id);
|
|
1681
|
+
if (!conversationId || !messageId || !terminalControl || !cwd || (pid === undefined && !sessionId)) {
|
|
1682
|
+
return undefined;
|
|
1683
|
+
}
|
|
1684
|
+
const storeDir = stringValue(nativeTakeover.claude_hook_store_dir) ?? defaultClaudeHookStoreDir();
|
|
1685
|
+
try {
|
|
1686
|
+
return new ClaudeHookStore({ rootDir: storeDir }).activateLease({
|
|
1687
|
+
...(sessionId === undefined ? {} : { sessionId }),
|
|
1688
|
+
...(pid === undefined ? {} : { pid }),
|
|
1689
|
+
cwd,
|
|
1690
|
+
conversationId,
|
|
1691
|
+
messageId,
|
|
1692
|
+
terminalTarget: terminalControl.target,
|
|
1693
|
+
expiresAt
|
|
1694
|
+
});
|
|
1695
|
+
}
|
|
1696
|
+
catch (error) {
|
|
1697
|
+
runtimeLog("warn", "claude_hook_lease_renew_failed", {
|
|
1698
|
+
conversation_id: conversationId,
|
|
1699
|
+
message_id: messageId,
|
|
1700
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
1701
|
+
});
|
|
1702
|
+
return undefined;
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
function releaseClaudeHookLeasesForTerminal({ storeDir, terminalControl, replacementConversationId }) {
|
|
1706
|
+
for (const candidate of listConversations(storeDir)) {
|
|
1707
|
+
if (candidate.conversation_id === replacementConversationId) {
|
|
1708
|
+
continue;
|
|
1709
|
+
}
|
|
1710
|
+
const nativeTakeover = isRecord(candidate.native_session_takeover)
|
|
1711
|
+
? candidate.native_session_takeover
|
|
1712
|
+
: undefined;
|
|
1713
|
+
const candidateControl = terminalControlFromTakeover(nativeTakeover);
|
|
1714
|
+
if (nativeTakeover?.agent === "claude" &&
|
|
1715
|
+
nativeTakeover?.terminal_bridge === true &&
|
|
1716
|
+
candidateControl?.target === terminalControl.target &&
|
|
1717
|
+
candidateControl?.socketPath === terminalControl.socketPath) {
|
|
1718
|
+
releaseClaudeHookLease(candidate);
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1417
1722
|
function uniqueDelegateSessionName(kind) {
|
|
1418
1723
|
const { sessionPrefix } = executorDefinitionForKind(kind || "claude");
|
|
1419
1724
|
const timestamp = new Date().toISOString().replace(/\D/g, "").slice(0, 14);
|
|
@@ -1535,57 +1840,59 @@ async function buildNativeListGroups({ options, agentFilter, statusFilter }) {
|
|
|
1535
1840
|
}
|
|
1536
1841
|
};
|
|
1537
1842
|
}
|
|
1538
|
-
|
|
1843
|
+
const registry = createRuntimeTerminalAgentRegistry(options);
|
|
1844
|
+
const adapters = agentFilter
|
|
1845
|
+
? [registry.get(agentFilter)].filter((adapter) => adapter !== undefined)
|
|
1846
|
+
: registry.list();
|
|
1847
|
+
if (agentFilter && adapters.length === 0) {
|
|
1539
1848
|
return {
|
|
1540
1849
|
...empty,
|
|
1541
1850
|
summary: {
|
|
1542
1851
|
enabled: true,
|
|
1543
1852
|
agents: [],
|
|
1544
|
-
skipped: `
|
|
1853
|
+
skipped: `terminal agent adapter is not registered for ${agentFilter}`
|
|
1545
1854
|
}
|
|
1546
1855
|
};
|
|
1547
1856
|
}
|
|
1857
|
+
const terminalProvider = createTerminalControlProvider(options);
|
|
1858
|
+
const bridge = new TerminalAgentBridge({ registry, terminalProvider });
|
|
1859
|
+
const terminalScan = options.terminalDebug ? await terminalControlDiagnostics(terminalProvider) : undefined;
|
|
1860
|
+
const terminalControlled = [];
|
|
1861
|
+
const native = [];
|
|
1862
|
+
let activeCount = 0;
|
|
1863
|
+
const errors = [];
|
|
1548
1864
|
try {
|
|
1549
|
-
const
|
|
1550
|
-
const
|
|
1551
|
-
const
|
|
1552
|
-
|
|
1865
|
+
const processSource = createTerminalProcessSource(options);
|
|
1866
|
+
const snapshots = await processSource.listProcessSnapshots((snapshot) => adapters.some((adapter) => adapter.capabilities.processDiscovery && adapter.classifyProcess(snapshot) !== undefined));
|
|
1867
|
+
const activeSessions = await bridge.listProcesses(snapshots, adapters.map((adapter) => adapter.agent));
|
|
1868
|
+
activeCount = activeSessions.length;
|
|
1553
1869
|
const rootSessions = rootActiveProcesses(activeSessions);
|
|
1554
|
-
const terminalControlled = [];
|
|
1555
|
-
const native = [];
|
|
1556
1870
|
for (const session of rootSessions) {
|
|
1557
1871
|
if (session.terminalControl) {
|
|
1558
|
-
terminalControlled.push(await terminalControlledListEntry(session, activeSessions, options));
|
|
1872
|
+
terminalControlled.push(await terminalControlledListEntry(session, activeSessions, options, bridge));
|
|
1559
1873
|
}
|
|
1560
1874
|
else {
|
|
1561
1875
|
native.push(nativeListEntry(session, activeSessions));
|
|
1562
1876
|
}
|
|
1563
1877
|
}
|
|
1564
|
-
return {
|
|
1565
|
-
native,
|
|
1566
|
-
terminalControlled,
|
|
1567
|
-
summary: {
|
|
1568
|
-
enabled: true,
|
|
1569
|
-
agents: ["codex"],
|
|
1570
|
-
active_count: activeSessions.length,
|
|
1571
|
-
native_count: native.length,
|
|
1572
|
-
terminal_controlled_count: terminalControlled.length,
|
|
1573
|
-
approval_scan: options.noApprovalScan ? "disabled" : "enabled",
|
|
1574
|
-
terminal_scan: terminalScan
|
|
1575
|
-
}
|
|
1576
|
-
};
|
|
1577
1878
|
}
|
|
1578
1879
|
catch (error) {
|
|
1579
|
-
|
|
1580
|
-
native: [],
|
|
1581
|
-
terminalControlled: [],
|
|
1582
|
-
summary: {
|
|
1583
|
-
enabled: true,
|
|
1584
|
-
agents: ["codex"],
|
|
1585
|
-
error: error instanceof Error ? error.message : String(error)
|
|
1586
|
-
}
|
|
1587
|
-
};
|
|
1880
|
+
errors.push(error instanceof Error ? error.message : String(error));
|
|
1588
1881
|
}
|
|
1882
|
+
return {
|
|
1883
|
+
native,
|
|
1884
|
+
terminalControlled,
|
|
1885
|
+
summary: {
|
|
1886
|
+
enabled: true,
|
|
1887
|
+
agents: adapters.map((adapter) => adapter.agent),
|
|
1888
|
+
active_count: activeCount,
|
|
1889
|
+
native_count: native.length,
|
|
1890
|
+
terminal_controlled_count: terminalControlled.length,
|
|
1891
|
+
approval_scan: options.noApprovalScan ? "disabled" : "enabled",
|
|
1892
|
+
terminal_scan: terminalScan,
|
|
1893
|
+
error: errors.length > 0 ? errors.join("; ") : undefined
|
|
1894
|
+
}
|
|
1895
|
+
};
|
|
1589
1896
|
}
|
|
1590
1897
|
async function terminalControlDiagnostics(provider) {
|
|
1591
1898
|
if (provider instanceof TmuxTerminalControlProvider) {
|
|
@@ -1612,9 +1919,9 @@ function delegatedListEntry(task) {
|
|
|
1612
1919
|
}
|
|
1613
1920
|
function nativeListEntry(session, activeSessions) {
|
|
1614
1921
|
return {
|
|
1615
|
-
id: `native
|
|
1922
|
+
id: `native:${session.agent}:${session.pid}`,
|
|
1616
1923
|
source: "native_active",
|
|
1617
|
-
agent:
|
|
1924
|
+
agent: session.agent,
|
|
1618
1925
|
status: "active",
|
|
1619
1926
|
pid: session.pid,
|
|
1620
1927
|
child_pids: childPidsForRoot(session, activeSessions),
|
|
@@ -1637,16 +1944,21 @@ function nativeListEntry(session, activeSessions) {
|
|
|
1637
1944
|
}
|
|
1638
1945
|
};
|
|
1639
1946
|
}
|
|
1640
|
-
async function terminalControlledListEntry(session, activeSessions, options) {
|
|
1947
|
+
async function terminalControlledListEntry(session, activeSessions, options, bridge = createTerminalAgentBridge(options)) {
|
|
1641
1948
|
const terminalControl = session.terminalControl;
|
|
1642
1949
|
if (!terminalControl) {
|
|
1643
1950
|
throw new Error(`process ${session.pid} is not terminal-controlled`);
|
|
1644
1951
|
}
|
|
1645
|
-
const terminalState = await listStateForTerminal(terminalControl, options
|
|
1952
|
+
const terminalState = await listStateForTerminal(session.agent, terminalControl, options, bridge, {
|
|
1953
|
+
pid: session.pid,
|
|
1954
|
+
sessionId: session.sessionId,
|
|
1955
|
+
cwd: session.cwd,
|
|
1956
|
+
terminalTarget: terminalControl.target
|
|
1957
|
+
});
|
|
1646
1958
|
return {
|
|
1647
|
-
id:
|
|
1959
|
+
id: bridge.terminalConversationId(session),
|
|
1648
1960
|
source: "terminal_control",
|
|
1649
|
-
agent:
|
|
1961
|
+
agent: session.agent,
|
|
1650
1962
|
status: "active",
|
|
1651
1963
|
pid: session.pid,
|
|
1652
1964
|
child_pids: childPidsForRoot(session, activeSessions),
|
|
@@ -1663,14 +1975,15 @@ async function terminalControlledListEntry(session, activeSessions, options) {
|
|
|
1663
1975
|
activity_reason: terminalState.activity_reason,
|
|
1664
1976
|
commands: {
|
|
1665
1977
|
send: true,
|
|
1666
|
-
approve:
|
|
1978
|
+
approve: terminalControl.capabilities.includes("terminal_approval") &&
|
|
1979
|
+
terminalState.approval_state.approvable === true,
|
|
1667
1980
|
status: true,
|
|
1668
|
-
cancel:
|
|
1981
|
+
cancel: terminalControl.capabilities.includes("terminal_cancel"),
|
|
1669
1982
|
close: false
|
|
1670
1983
|
}
|
|
1671
1984
|
};
|
|
1672
1985
|
}
|
|
1673
|
-
async function listStateForTerminal(terminalControl, options) {
|
|
1986
|
+
async function listStateForTerminal(agent, terminalControl, options, bridge = createTerminalAgentBridge(options), runtime) {
|
|
1674
1987
|
if (options.noApprovalScan) {
|
|
1675
1988
|
return {
|
|
1676
1989
|
approval_state: {
|
|
@@ -1684,27 +1997,18 @@ async function listStateForTerminal(terminalControl, options) {
|
|
|
1684
1997
|
};
|
|
1685
1998
|
}
|
|
1686
1999
|
try {
|
|
1687
|
-
const
|
|
2000
|
+
const status = await bridge.status(agent, terminalControl, {
|
|
1688
2001
|
scrollbackLines: Number(options.scrollbackLines ?? 120),
|
|
1689
|
-
|
|
2002
|
+
runtime
|
|
1690
2003
|
});
|
|
1691
|
-
const approval = detectCodexApprovalPrompt(screen);
|
|
1692
|
-
const blocked = isCodexApprovalPromptVisible(screen);
|
|
1693
|
-
const activity = detectCodexActivityState(screen, approval);
|
|
1694
2004
|
return {
|
|
1695
2005
|
approval_state: {
|
|
1696
|
-
|
|
1697
|
-
blocked
|
|
1698
|
-
approvable: approval.approvable,
|
|
1699
|
-
key: approval.approvable ? approval.key : undefined,
|
|
1700
|
-
label: approval.approvable ? approval.label : undefined,
|
|
1701
|
-
prompt_kind: approval.promptKind,
|
|
1702
|
-
command: approval.command,
|
|
1703
|
-
reason: approval.approvable ? undefined : approval.reason,
|
|
1704
|
-
screen_excerpt: blocked ? screenExcerpt(screen, 1000) : undefined
|
|
2006
|
+
...status.approval_state,
|
|
2007
|
+
screen_excerpt: status.approval_state.blocked ? status.screen.excerpt?.slice(-1000) : undefined
|
|
1705
2008
|
},
|
|
1706
|
-
activity_state:
|
|
1707
|
-
activity_reason:
|
|
2009
|
+
activity_state: status.activity_state,
|
|
2010
|
+
activity_reason: status.activity_reason,
|
|
2011
|
+
capability_limitation: status.capability_limitation
|
|
1708
2012
|
};
|
|
1709
2013
|
}
|
|
1710
2014
|
catch (error) {
|
|
@@ -1721,11 +2025,13 @@ async function listStateForTerminal(terminalControl, options) {
|
|
|
1721
2025
|
}
|
|
1722
2026
|
}
|
|
1723
2027
|
function rootActiveProcesses(processes) {
|
|
1724
|
-
const pids = new Set(processes.map((process) => process.pid));
|
|
1725
|
-
const roots = processes.filter((process) => !process.ppid || !pids.has(process.ppid));
|
|
2028
|
+
const pids = new Set(processes.map((process) => `${process.agent}:${process.pid}`));
|
|
2029
|
+
const roots = processes.filter((process) => !process.ppid || !pids.has(`${process.agent}:${process.ppid}`));
|
|
1726
2030
|
const seenTerminalTargets = new Set();
|
|
1727
2031
|
return roots.filter((process) => {
|
|
1728
|
-
const terminalTarget = process.terminalControl?.target
|
|
2032
|
+
const terminalTarget = process.terminalControl?.target
|
|
2033
|
+
? `${process.agent}:${process.terminalControl.target}`
|
|
2034
|
+
: undefined;
|
|
1729
2035
|
if (!terminalTarget) {
|
|
1730
2036
|
return true;
|
|
1731
2037
|
}
|
|
@@ -1738,50 +2044,14 @@ function rootActiveProcesses(processes) {
|
|
|
1738
2044
|
}
|
|
1739
2045
|
function childPidsForRoot(root, processes) {
|
|
1740
2046
|
return processes
|
|
1741
|
-
.filter((process) => process.ppid === root.pid)
|
|
2047
|
+
.filter((process) => process.agent === root.agent && process.ppid === root.pid)
|
|
1742
2048
|
.map((process) => process.pid);
|
|
1743
2049
|
}
|
|
1744
2050
|
function canSendDelegated(status) {
|
|
1745
2051
|
return !["failed", "closed", "cancelled"].includes(status);
|
|
1746
2052
|
}
|
|
1747
2053
|
async function resolveTerminalConversationFromOptions(options) {
|
|
1748
|
-
|
|
1749
|
-
if (!parsed) {
|
|
1750
|
-
return undefined;
|
|
1751
|
-
}
|
|
1752
|
-
const provider = createTerminalControlProvider(options);
|
|
1753
|
-
const panes = await provider.listPanes();
|
|
1754
|
-
const pane = panes.find((candidate) => candidate.kind === parsed.kind &&
|
|
1755
|
-
candidate.target === parsed.target);
|
|
1756
|
-
if (!pane) {
|
|
1757
|
-
throw new Error(`terminal-controlled session ${parsed.conversationId} is no longer available`);
|
|
1758
|
-
}
|
|
1759
|
-
return {
|
|
1760
|
-
conversationId: parsed.conversationId,
|
|
1761
|
-
terminalControl: terminalRefFromPane(pane)
|
|
1762
|
-
};
|
|
1763
|
-
}
|
|
1764
|
-
function parseTerminalConversationId(conversationId) {
|
|
1765
|
-
const prefix = "terminal:tmux:";
|
|
1766
|
-
if (!conversationId?.startsWith(prefix)) {
|
|
1767
|
-
return undefined;
|
|
1768
|
-
}
|
|
1769
|
-
const rest = conversationId.slice(prefix.length);
|
|
1770
|
-
const pidSeparator = rest.lastIndexOf(":");
|
|
1771
|
-
if (pidSeparator <= 0 || pidSeparator === rest.length - 1) {
|
|
1772
|
-
throw new Error(`invalid terminal-controlled conversation id: ${conversationId}`);
|
|
1773
|
-
}
|
|
1774
|
-
const target = rest.slice(0, pidSeparator);
|
|
1775
|
-
const pid = Number(rest.slice(pidSeparator + 1));
|
|
1776
|
-
if (!target || !Number.isInteger(pid)) {
|
|
1777
|
-
throw new Error(`invalid terminal-controlled conversation id: ${conversationId}`);
|
|
1778
|
-
}
|
|
1779
|
-
return {
|
|
1780
|
-
conversationId,
|
|
1781
|
-
kind: "tmux",
|
|
1782
|
-
target,
|
|
1783
|
-
pid
|
|
1784
|
-
};
|
|
2054
|
+
return createTerminalAgentBridge(options).resolveConversationId(stringValue(options.conversation ?? options.conversationId));
|
|
1785
2055
|
}
|
|
1786
2056
|
function parseNativeConversationId(conversationId) {
|
|
1787
2057
|
const prefix = "native:codex:";
|
|
@@ -1802,7 +2072,11 @@ async function runStatus(options) {
|
|
|
1802
2072
|
cleanupIdleConversations(storeDirFromOptions(options), options);
|
|
1803
2073
|
const terminalConversation = await resolveTerminalConversationFromOptions(options);
|
|
1804
2074
|
if (terminalConversation) {
|
|
1805
|
-
const terminalStatus = await terminalStatusForControl(terminalConversation.terminalControl, options
|
|
2075
|
+
const terminalStatus = await terminalStatusForControl(terminalConversation.agent, terminalConversation.terminalControl, options, {
|
|
2076
|
+
pid: terminalConversation.pid,
|
|
2077
|
+
cwd: terminalConversation.terminalControl.currentPath,
|
|
2078
|
+
terminalTarget: terminalConversation.terminalControl.target
|
|
2079
|
+
});
|
|
1806
2080
|
printJson({
|
|
1807
2081
|
conversation_id: terminalConversation.conversationId,
|
|
1808
2082
|
source: "terminal_control",
|
|
@@ -1832,8 +2106,9 @@ async function runStatus(options) {
|
|
|
1832
2106
|
}
|
|
1833
2107
|
const terminalControl = terminalControlFromTakeover(isRecord(conversation.native_session_takeover) ? conversation.native_session_takeover : undefined);
|
|
1834
2108
|
if (terminalControl) {
|
|
2109
|
+
const executor = executorForConversation(conversation);
|
|
1835
2110
|
result.terminal_control = terminalControl;
|
|
1836
|
-
result.terminal_status = await terminalStatusForControl(terminalControl, options);
|
|
2111
|
+
result.terminal_status = await terminalStatusForControl(executor.kind, terminalControl, options, terminalRuntimeIdentityForConversation(conversation, terminalControl));
|
|
1837
2112
|
result.terminal_screen = result.terminal_status.screen;
|
|
1838
2113
|
}
|
|
1839
2114
|
printJson(result);
|
|
@@ -1851,8 +2126,31 @@ async function runDescribe(options) {
|
|
|
1851
2126
|
const conversationId = required(options.conversation ?? options.conversationId, "--conversation is required");
|
|
1852
2127
|
const terminalConversation = await resolveTerminalConversationFromOptions(options);
|
|
1853
2128
|
if (terminalConversation) {
|
|
1854
|
-
const terminalStatus = await terminalStatusForControl(terminalConversation.terminalControl, options
|
|
1855
|
-
|
|
2129
|
+
const terminalStatus = await terminalStatusForControl(terminalConversation.agent, terminalConversation.terminalControl, options, {
|
|
2130
|
+
pid: terminalConversation.pid,
|
|
2131
|
+
cwd: terminalConversation.terminalControl.currentPath,
|
|
2132
|
+
terminalTarget: terminalConversation.terminalControl.target
|
|
2133
|
+
});
|
|
2134
|
+
if (terminalConversation.agent !== "codex") {
|
|
2135
|
+
const adapter = createRuntimeTerminalAgentRegistry(options).require(terminalConversation.agent);
|
|
2136
|
+
printJson({
|
|
2137
|
+
conversation_id: conversationId,
|
|
2138
|
+
source: "terminal_control",
|
|
2139
|
+
agent: terminalConversation.agent,
|
|
2140
|
+
confidence: terminalStatus.reachable ? "medium" : "low",
|
|
2141
|
+
about: terminalStatus.reachable
|
|
2142
|
+
? `${adapter.displayName} is attached through ${terminalConversation.terminalControl.kind}:${terminalConversation.terminalControl.target}.`
|
|
2143
|
+
: `${adapter.displayName} terminal status is unavailable.`,
|
|
2144
|
+
evidence: {
|
|
2145
|
+
terminal_status: terminalStatus,
|
|
2146
|
+
terminal_screen: terminalStatus.screen
|
|
2147
|
+
},
|
|
2148
|
+
limitations: ["Historical session context is not available for this terminal adapter."],
|
|
2149
|
+
terminal_control: terminalConversation.terminalControl
|
|
2150
|
+
});
|
|
2151
|
+
return;
|
|
2152
|
+
}
|
|
2153
|
+
const process = await activeCodexProcessForPid(options, terminalConversation.pid);
|
|
1856
2154
|
printJson(await describeNativeCodexSession({
|
|
1857
2155
|
id: conversationId,
|
|
1858
2156
|
source: "terminal_control",
|
|
@@ -1877,7 +2175,9 @@ async function runDescribe(options) {
|
|
|
1877
2175
|
const { conversation, statePath, logPath } = loadConversationFromOptions(options);
|
|
1878
2176
|
const events = readExistingEvents(logPath);
|
|
1879
2177
|
const terminalControl = terminalControlFromTakeover(isRecord(conversation.native_session_takeover) ? conversation.native_session_takeover : undefined);
|
|
1880
|
-
const terminalStatus = terminalControl
|
|
2178
|
+
const terminalStatus = terminalControl
|
|
2179
|
+
? await terminalStatusForControl(executorForConversation(conversation).kind, terminalControl, options, terminalRuntimeIdentityForConversation(conversation, terminalControl))
|
|
2180
|
+
: undefined;
|
|
1881
2181
|
printJson({
|
|
1882
2182
|
conversation_id: conversation.conversation_id,
|
|
1883
2183
|
source: "akk_managed",
|
|
@@ -1895,110 +2195,93 @@ async function runDescribe(options) {
|
|
|
1895
2195
|
event_log_path: logPath
|
|
1896
2196
|
});
|
|
1897
2197
|
}
|
|
1898
|
-
async function terminalStatusForControl(terminalControl, options) {
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
});
|
|
1904
|
-
const approval = detectCodexApprovalPrompt(screen);
|
|
1905
|
-
const blocked = isCodexApprovalPromptVisible(screen);
|
|
1906
|
-
const activity = detectCodexActivityState(screen, approval);
|
|
1907
|
-
return {
|
|
1908
|
-
provider: terminalControl.kind,
|
|
1909
|
-
target: terminalControl.target,
|
|
1910
|
-
reachable: true,
|
|
1911
|
-
activity_state: activity.state,
|
|
1912
|
-
activity_reason: activity.reason,
|
|
1913
|
-
approval_state: {
|
|
1914
|
-
scanned: true,
|
|
1915
|
-
blocked,
|
|
1916
|
-
approvable: approval.approvable,
|
|
1917
|
-
key: approval.approvable ? approval.key : undefined,
|
|
1918
|
-
label: approval.approvable ? approval.label : undefined,
|
|
1919
|
-
prompt_kind: approval.promptKind,
|
|
1920
|
-
command: approval.command,
|
|
1921
|
-
reason: approval.approvable ? undefined : approval.reason
|
|
1922
|
-
},
|
|
1923
|
-
screen: {
|
|
1924
|
-
excerpt: screenExcerpt(screen),
|
|
1925
|
-
approval
|
|
1926
|
-
}
|
|
1927
|
-
};
|
|
1928
|
-
}
|
|
1929
|
-
catch (error) {
|
|
1930
|
-
return {
|
|
1931
|
-
provider: terminalControl.kind,
|
|
1932
|
-
target: terminalControl.target,
|
|
1933
|
-
reachable: false,
|
|
1934
|
-
activity_state: "unknown",
|
|
1935
|
-
activity_reason: error instanceof Error ? error.message : String(error),
|
|
1936
|
-
approval_state: {
|
|
1937
|
-
scanned: false,
|
|
1938
|
-
blocked: false,
|
|
1939
|
-
approvable: false
|
|
1940
|
-
},
|
|
1941
|
-
screen: {
|
|
1942
|
-
error: error instanceof Error ? error.message : String(error)
|
|
1943
|
-
}
|
|
1944
|
-
};
|
|
1945
|
-
}
|
|
2198
|
+
async function terminalStatusForControl(agent, terminalControl, options, runtime) {
|
|
2199
|
+
return createTerminalAgentBridge(options).status(agent, terminalControl, {
|
|
2200
|
+
scrollbackLines: Number(options.scrollbackLines ?? 120),
|
|
2201
|
+
runtime
|
|
2202
|
+
});
|
|
1946
2203
|
}
|
|
1947
2204
|
function terminalBridgeApprovalFingerprint({ terminalControl, terminalStatus }) {
|
|
1948
2205
|
const approval = isRecord(terminalStatus?.approval_state) ? terminalStatus.approval_state : {};
|
|
2206
|
+
const adapterFingerprint = stringValue(approval.fingerprint);
|
|
2207
|
+
if (adapterFingerprint) {
|
|
2208
|
+
return adapterFingerprint;
|
|
2209
|
+
}
|
|
1949
2210
|
const screen = isRecord(terminalStatus?.screen) ? terminalStatus.screen : {};
|
|
1950
2211
|
return createHash("sha256")
|
|
1951
2212
|
.update(JSON.stringify({
|
|
1952
2213
|
target: terminalControl.target,
|
|
1953
|
-
|
|
2214
|
+
keys: approval.keys ?? (approval.key ? [approval.key] : undefined),
|
|
1954
2215
|
label: approval.label,
|
|
1955
2216
|
prompt_kind: approval.prompt_kind,
|
|
1956
2217
|
command: approval.command,
|
|
2218
|
+
tool_name: approval.tool_name,
|
|
2219
|
+
request_detail: approval.request_detail,
|
|
1957
2220
|
excerpt: screen.excerpt
|
|
1958
2221
|
}))
|
|
1959
|
-
.digest("hex")
|
|
1960
|
-
.slice(0, 16);
|
|
2222
|
+
.digest("hex");
|
|
1961
2223
|
}
|
|
1962
|
-
function
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
}
|
|
1976
|
-
});
|
|
2224
|
+
function assertSafeClaudeTerminalSend(terminalStatus) {
|
|
2225
|
+
const approval = isRecord(terminalStatus?.approval_state)
|
|
2226
|
+
? terminalStatus.approval_state
|
|
2227
|
+
: undefined;
|
|
2228
|
+
if (terminalStatus?.reachable !== true) {
|
|
2229
|
+
throw new Error("Claude Code terminal status is unavailable");
|
|
2230
|
+
}
|
|
2231
|
+
if (approval?.blocked === true) {
|
|
2232
|
+
throw new Error(stringValue(approval.reason) ?? "Claude Code is waiting at a permission dialog");
|
|
2233
|
+
}
|
|
2234
|
+
if (terminalStatus.activity_state !== "idle") {
|
|
2235
|
+
throw new Error(`Claude Code terminal is ${stringValue(terminalStatus.activity_state) ?? "unknown"}, not idle`);
|
|
2236
|
+
}
|
|
1977
2237
|
}
|
|
1978
2238
|
function terminalBridgeApprovalInstructions({ conversation, terminalControl, terminalStatus }) {
|
|
1979
2239
|
const approval = isRecord(terminalStatus?.approval_state) ? terminalStatus.approval_state : {};
|
|
1980
2240
|
const screen = isRecord(terminalStatus?.screen) ? terminalStatus.screen : {};
|
|
1981
|
-
const
|
|
1982
|
-
const
|
|
2241
|
+
const executor = executorForConversation(conversation);
|
|
2242
|
+
const agentName = executorDefinitionForKind(executor.kind).displayName;
|
|
2243
|
+
const label = stringValue(approval.label) || `the current ${agentName} approval prompt`;
|
|
2244
|
+
const keys = Array.isArray(approval.keys)
|
|
2245
|
+
? approval.keys.filter((value) => typeof value === "string")
|
|
2246
|
+
: [];
|
|
2247
|
+
const decisionMode = stringValue(approval.decision_mode);
|
|
2248
|
+
const keyDescription = decisionMode === "structured"
|
|
2249
|
+
? "structured one-time Hook decision"
|
|
2250
|
+
: keys.length > 0
|
|
2251
|
+
? keys.join(" then ")
|
|
2252
|
+
: stringValue(approval.key) || "the detected approve key sequence";
|
|
2253
|
+
const fingerprint = stringValue(approval.fingerprint);
|
|
2254
|
+
const promptKind = stringValue(approval.prompt_kind);
|
|
2255
|
+
const command = stringValue(approval.command);
|
|
2256
|
+
const toolName = stringValue(approval.tool_name);
|
|
2257
|
+
const requestDetail = stringValue(approval.request_detail);
|
|
2258
|
+
const requestId = stringValue(approval.request_id);
|
|
1983
2259
|
const excerpt = stringValue(screen.excerpt) || "(No terminal excerpt was available.)";
|
|
1984
2260
|
return [
|
|
1985
|
-
|
|
2261
|
+
`${agentName} is waiting for approval in a terminal-controlled AKK session.`,
|
|
1986
2262
|
"",
|
|
1987
2263
|
`Conversation: ${conversation.conversation_id}`,
|
|
1988
2264
|
`Terminal: ${terminalControl.kind}:${terminalControl.target}`,
|
|
1989
|
-
`Approval option: ${label} (${
|
|
2265
|
+
`Approval option: ${label} (${keyDescription})`,
|
|
2266
|
+
promptKind ? `Request kind: ${promptKind}` : undefined,
|
|
2267
|
+
toolName ? `Tool: ${toolName}` : undefined,
|
|
2268
|
+
requestDetail ? `Request: ${requestDetail}` : undefined,
|
|
2269
|
+
command ? `Command: ${command}` : undefined,
|
|
2270
|
+
requestId ? `Structured request id: ${requestId}` : undefined,
|
|
1990
2271
|
"",
|
|
1991
2272
|
"Safe terminal excerpt:",
|
|
1992
2273
|
"```text",
|
|
1993
2274
|
excerpt,
|
|
1994
2275
|
"```",
|
|
1995
2276
|
"",
|
|
1996
|
-
|
|
2277
|
+
`Ask the user whether to approve or deny this ${agentName} request.`,
|
|
1997
2278
|
"",
|
|
1998
2279
|
"If the user approves, call `agent_knock_knock_approve` with:",
|
|
1999
2280
|
`- conversation_id: ${conversation.conversation_id}`,
|
|
2281
|
+
`- expected_approval_fingerprint: ${fingerprint ?? "(missing; refresh status before approval)"}`,
|
|
2000
2282
|
"",
|
|
2001
|
-
"Equivalent user command: `AKK approve " + conversation.conversation_id +
|
|
2283
|
+
"Equivalent user command: `AKK approve " + conversation.conversation_id +
|
|
2284
|
+
(fingerprint ? ` --expected-approval-fingerprint ${fingerprint}` : "") + "`",
|
|
2002
2285
|
"",
|
|
2003
2286
|
"If the user denies or wants to stop this request, call `agent_knock_knock_cancel` with:",
|
|
2004
2287
|
`- conversation_id: ${conversation.conversation_id}`,
|
|
@@ -2006,7 +2289,7 @@ function terminalBridgeApprovalInstructions({ conversation, terminalControl, ter
|
|
|
2006
2289
|
"Equivalent user command: `AKK cancel " + conversation.conversation_id + "`",
|
|
2007
2290
|
"",
|
|
2008
2291
|
"Do not use raw tmux, shell, or manual key presses for this approval. Do not approve without explicit user confirmation."
|
|
2009
|
-
].join("\n");
|
|
2292
|
+
].filter((line) => line !== undefined).join("\n");
|
|
2010
2293
|
}
|
|
2011
2294
|
function recordTerminalBridgeApprovalNotification({ statePath, logPath, terminalControl, terminalStatus, fingerprint }) {
|
|
2012
2295
|
const releaseLock = acquireFileLock(`${statePath}.lock`);
|
|
@@ -2068,6 +2351,8 @@ async function runSend(options) {
|
|
|
2068
2351
|
const managed = createManagedTerminalConversationFromRawId({
|
|
2069
2352
|
options,
|
|
2070
2353
|
conversationId: terminalConversation.conversationId,
|
|
2354
|
+
agent: terminalConversation.agent,
|
|
2355
|
+
pid: terminalConversation.pid,
|
|
2071
2356
|
messageBody,
|
|
2072
2357
|
terminalControl: terminalConversation.terminalControl
|
|
2073
2358
|
});
|
|
@@ -2087,6 +2372,8 @@ async function runSend(options) {
|
|
|
2087
2372
|
await runTerminalConversationSend({
|
|
2088
2373
|
options,
|
|
2089
2374
|
conversationId: terminalConversation.conversationId,
|
|
2375
|
+
agent: terminalConversation.agent,
|
|
2376
|
+
pid: terminalConversation.pid,
|
|
2090
2377
|
messageBody,
|
|
2091
2378
|
terminalControl: terminalConversation.terminalControl
|
|
2092
2379
|
});
|
|
@@ -2450,7 +2737,9 @@ async function runApprove(options) {
|
|
|
2450
2737
|
await runTerminalConversationApprove({
|
|
2451
2738
|
options,
|
|
2452
2739
|
conversationId: terminalConversation.conversationId,
|
|
2453
|
-
|
|
2740
|
+
agent: terminalConversation.agent,
|
|
2741
|
+
terminalControl: terminalConversation.terminalControl,
|
|
2742
|
+
pid: terminalConversation.pid
|
|
2454
2743
|
});
|
|
2455
2744
|
return;
|
|
2456
2745
|
}
|
|
@@ -2462,36 +2751,32 @@ async function runApprove(options) {
|
|
|
2462
2751
|
if (!terminalControl) {
|
|
2463
2752
|
throw new Error(`conversation ${conversation.conversation_id} is not controlled through a terminal`);
|
|
2464
2753
|
}
|
|
2465
|
-
const
|
|
2466
|
-
const
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
if (!approval.approvable) {
|
|
2472
|
-
printJson({
|
|
2473
|
-
conversation,
|
|
2474
|
-
approved: false,
|
|
2475
|
-
blocked: true,
|
|
2476
|
-
reason: approval.reason,
|
|
2477
|
-
terminal_control: terminalControl,
|
|
2478
|
-
screen_excerpt: screenExcerpt(screen)
|
|
2479
|
-
});
|
|
2480
|
-
return;
|
|
2481
|
-
}
|
|
2482
|
-
const expectedFingerprint = stringValue(options.expectedApprovalFingerprint);
|
|
2483
|
-
const actualFingerprint = terminalBridgeApprovalFingerprintForScreen({ terminalControl, screen, approval });
|
|
2754
|
+
const executor = executorForConversation(conversation);
|
|
2755
|
+
const monitoredApproval = isRecord(nativeTakeover?.["terminal_bridge_approval"])
|
|
2756
|
+
? nativeTakeover.terminal_bridge_approval
|
|
2757
|
+
: undefined;
|
|
2758
|
+
const expectedFingerprint = stringValue(options.expectedApprovalFingerprint) ??
|
|
2759
|
+
stringValue(monitoredApproval?.fingerprint);
|
|
2484
2760
|
const autoApproved = options.autoApproved === true;
|
|
2485
2761
|
const policyRuleId = stringValue(options.policyRuleId);
|
|
2486
2762
|
const policyFingerprint = stringValue(options.policyFingerprint);
|
|
2487
|
-
|
|
2763
|
+
const approval = await createTerminalAgentBridge(options).approve(executor.kind, terminalControl, {
|
|
2764
|
+
expectedFingerprint,
|
|
2765
|
+
scrollbackLines: Number(options.scrollbackLines ?? 120),
|
|
2766
|
+
runtime: terminalRuntimeIdentityForConversation(conversation, terminalControl),
|
|
2767
|
+
requiredDecisionMode: autoApproved && executor.kind === "claude"
|
|
2768
|
+
? "structured"
|
|
2769
|
+
: undefined
|
|
2770
|
+
});
|
|
2771
|
+
const actualFingerprint = approval.fingerprint;
|
|
2772
|
+
if (!approval.approved) {
|
|
2488
2773
|
if (autoApproved) {
|
|
2489
2774
|
appendEvent(logPath, {
|
|
2490
2775
|
ts: new Date().toISOString(),
|
|
2491
2776
|
conversation_id: conversation.conversation_id,
|
|
2492
2777
|
event: "terminal_auto_approval_decision",
|
|
2493
2778
|
action: "rejected",
|
|
2494
|
-
reason:
|
|
2779
|
+
reason: approval.reason,
|
|
2495
2780
|
terminal_control: terminalControl,
|
|
2496
2781
|
expected_fingerprint: expectedFingerprint,
|
|
2497
2782
|
actual_fingerprint: actualFingerprint,
|
|
@@ -2502,25 +2787,25 @@ async function runApprove(options) {
|
|
|
2502
2787
|
printJson({
|
|
2503
2788
|
conversation,
|
|
2504
2789
|
approved: false,
|
|
2505
|
-
blocked:
|
|
2506
|
-
reason:
|
|
2790
|
+
blocked: approval.blocked,
|
|
2791
|
+
reason: approval.reason,
|
|
2507
2792
|
terminal_control: terminalControl,
|
|
2508
2793
|
expected_approval_fingerprint: expectedFingerprint,
|
|
2509
2794
|
actual_approval_fingerprint: actualFingerprint,
|
|
2510
|
-
screen_excerpt: screenExcerpt
|
|
2795
|
+
screen_excerpt: approval.screenExcerpt
|
|
2511
2796
|
});
|
|
2512
2797
|
return;
|
|
2513
2798
|
}
|
|
2514
|
-
await provider.sendKeys(terminalControl.target, [approval.key], {
|
|
2515
|
-
socketPath: terminalControl.socketPath
|
|
2516
|
-
});
|
|
2517
2799
|
appendEvent(logPath, {
|
|
2518
2800
|
ts: new Date().toISOString(),
|
|
2519
2801
|
conversation_id: conversation.conversation_id,
|
|
2520
2802
|
event: "terminal_approval_send",
|
|
2521
2803
|
terminal_control: terminalControl,
|
|
2522
2804
|
key: approval.key,
|
|
2805
|
+
keys: approval.keys,
|
|
2523
2806
|
label: approval.label,
|
|
2807
|
+
decision_mode: approval.decisionMode,
|
|
2808
|
+
request_id: approval.requestId,
|
|
2524
2809
|
approval_fingerprint: actualFingerprint,
|
|
2525
2810
|
auto_approved: autoApproved,
|
|
2526
2811
|
policy_rule_id: policyRuleId,
|
|
@@ -2542,7 +2827,10 @@ async function runApprove(options) {
|
|
|
2542
2827
|
conversation_id: conversation.conversation_id,
|
|
2543
2828
|
terminal_target: terminalControl.target,
|
|
2544
2829
|
key: approval.key,
|
|
2830
|
+
keys: approval.keys,
|
|
2545
2831
|
label: approval.label,
|
|
2832
|
+
decision_mode: approval.decisionMode,
|
|
2833
|
+
request_id: approval.requestId,
|
|
2546
2834
|
approval_fingerprint: actualFingerprint,
|
|
2547
2835
|
auto_approved: autoApproved,
|
|
2548
2836
|
policy_rule_id: policyRuleId,
|
|
@@ -2571,12 +2859,28 @@ async function runApprove(options) {
|
|
|
2571
2859
|
terminal_bridge_hard_deadline_at: deadlineAt(stringValue(nativeTakeoverForUpdate.terminal_bridge_started_at) ?? approvalResolvedAt, agentHardTimeoutMinutes)
|
|
2572
2860
|
};
|
|
2573
2861
|
delete nextNativeTakeover.terminal_bridge_approval;
|
|
2574
|
-
|
|
2862
|
+
let nextConversation = {
|
|
2575
2863
|
...conversation,
|
|
2576
2864
|
status: terminalBridgeEnabled(conversation) ? "waiting_for_agent" : conversation.status,
|
|
2577
2865
|
native_session_takeover: nextNativeTakeover,
|
|
2578
2866
|
updated_at: approvalResolvedAt
|
|
2579
2867
|
};
|
|
2868
|
+
const approvalLeaseDeadlines = [
|
|
2869
|
+
stringValue(nextNativeTakeover.terminal_bridge_inactivity_deadline_at),
|
|
2870
|
+
stringValue(nextNativeTakeover.terminal_bridge_hard_deadline_at)
|
|
2871
|
+
].filter((value) => Boolean(value));
|
|
2872
|
+
if (approvalLeaseDeadlines.length > 0) {
|
|
2873
|
+
const renewedLease = renewClaudeHookLease(nextConversation, new Date(Math.min(...approvalLeaseDeadlines.map((value) => Date.parse(value)))).toISOString());
|
|
2874
|
+
if (renewedLease) {
|
|
2875
|
+
nextConversation = {
|
|
2876
|
+
...nextConversation,
|
|
2877
|
+
native_session_takeover: {
|
|
2878
|
+
...nextNativeTakeover,
|
|
2879
|
+
claude_hook_lease_id: renewedLease.id
|
|
2880
|
+
}
|
|
2881
|
+
};
|
|
2882
|
+
}
|
|
2883
|
+
}
|
|
2580
2884
|
saveState(statePath, nextConversation);
|
|
2581
2885
|
const bridgeMonitor = startTerminalBridgeMonitorForConversation({
|
|
2582
2886
|
conversation: nextConversation,
|
|
@@ -2607,40 +2911,47 @@ async function runApprove(options) {
|
|
|
2607
2911
|
approved: true,
|
|
2608
2912
|
terminal_control: terminalControl,
|
|
2609
2913
|
key: approval.key,
|
|
2914
|
+
keys: approval.keys,
|
|
2610
2915
|
label: approval.label,
|
|
2916
|
+
decision_mode: approval.decisionMode,
|
|
2917
|
+
request_id: approval.requestId,
|
|
2611
2918
|
approval_fingerprint: actualFingerprint,
|
|
2612
2919
|
auto_approved: autoApproved,
|
|
2613
2920
|
policy_rule_id: policyRuleId,
|
|
2614
2921
|
monitor_pid: bridgeMonitor?.pid ?? null
|
|
2615
2922
|
});
|
|
2616
2923
|
}
|
|
2617
|
-
async function runTerminalConversationApprove({ options, conversationId, terminalControl }) {
|
|
2618
|
-
const
|
|
2619
|
-
|
|
2924
|
+
async function runTerminalConversationApprove({ options, conversationId, agent, terminalControl, pid }) {
|
|
2925
|
+
const approval = await createTerminalAgentBridge(options).approve(agent, terminalControl, {
|
|
2926
|
+
expectedFingerprint: stringValue(options.expectedApprovalFingerprint),
|
|
2620
2927
|
scrollbackLines: Number(options.scrollbackLines ?? 120),
|
|
2621
|
-
|
|
2928
|
+
runtime: {
|
|
2929
|
+
pid,
|
|
2930
|
+
cwd: terminalControl.currentPath,
|
|
2931
|
+
terminalTarget: terminalControl.target
|
|
2932
|
+
}
|
|
2622
2933
|
});
|
|
2623
|
-
|
|
2624
|
-
if (!approval.approvable) {
|
|
2934
|
+
if (!approval.approved) {
|
|
2625
2935
|
printJson({
|
|
2626
2936
|
conversation_id: conversationId,
|
|
2627
2937
|
source: "terminal_control",
|
|
2628
2938
|
approved: false,
|
|
2629
|
-
blocked:
|
|
2939
|
+
blocked: approval.blocked,
|
|
2630
2940
|
reason: approval.reason,
|
|
2631
2941
|
terminal_control: terminalControl,
|
|
2632
|
-
screen_excerpt: screenExcerpt
|
|
2942
|
+
screen_excerpt: approval.screenExcerpt
|
|
2633
2943
|
});
|
|
2634
2944
|
return;
|
|
2635
2945
|
}
|
|
2636
|
-
await provider.sendKeys(terminalControl.target, [approval.key], {
|
|
2637
|
-
socketPath: terminalControl.socketPath
|
|
2638
|
-
});
|
|
2639
2946
|
runtimeLog("info", "terminal_approval_send", {
|
|
2640
2947
|
conversation_id: conversationId,
|
|
2948
|
+
agent,
|
|
2641
2949
|
terminal_target: terminalControl.target,
|
|
2642
2950
|
key: approval.key,
|
|
2643
|
-
|
|
2951
|
+
keys: approval.keys,
|
|
2952
|
+
label: approval.label,
|
|
2953
|
+
decision_mode: approval.decisionMode,
|
|
2954
|
+
request_id: approval.requestId
|
|
2644
2955
|
});
|
|
2645
2956
|
printJson({
|
|
2646
2957
|
conversation_id: conversationId,
|
|
@@ -2648,20 +2959,31 @@ async function runTerminalConversationApprove({ options, conversationId, termina
|
|
|
2648
2959
|
approved: true,
|
|
2649
2960
|
terminal_control: terminalControl,
|
|
2650
2961
|
key: approval.key,
|
|
2651
|
-
|
|
2962
|
+
keys: approval.keys,
|
|
2963
|
+
label: approval.label,
|
|
2964
|
+
approval_fingerprint: approval.fingerprint,
|
|
2965
|
+
decision_mode: approval.decisionMode,
|
|
2966
|
+
request_id: approval.requestId
|
|
2652
2967
|
});
|
|
2653
2968
|
}
|
|
2654
|
-
async function runTerminalConversationSend({ options, conversationId, messageBody, terminalControl }) {
|
|
2655
|
-
const provider = createTerminalControlProvider(options);
|
|
2969
|
+
async function runTerminalConversationSend({ options, conversationId, agent, pid, messageBody, terminalControl }) {
|
|
2656
2970
|
const terminalPayload = terminalSubmissionPayload(String(messageBody));
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2971
|
+
const terminalBridge = createTerminalAgentBridge(options);
|
|
2972
|
+
if (agent === "claude") {
|
|
2973
|
+
const status = await terminalBridge.status(agent, terminalControl, {
|
|
2974
|
+
scrollbackLines: Number(options.scrollbackLines ?? 120),
|
|
2975
|
+
runtime: {
|
|
2976
|
+
pid,
|
|
2977
|
+
cwd: terminalControl.currentPath,
|
|
2978
|
+
terminalTarget: terminalControl.target
|
|
2979
|
+
}
|
|
2980
|
+
});
|
|
2981
|
+
assertSafeClaudeTerminalSend(status);
|
|
2982
|
+
}
|
|
2983
|
+
await terminalBridge.send(agent, terminalControl, terminalPayload);
|
|
2663
2984
|
runtimeLog("info", "terminal_message_send", {
|
|
2664
2985
|
conversation_id: conversationId,
|
|
2986
|
+
agent,
|
|
2665
2987
|
terminal_target: terminalControl.target,
|
|
2666
2988
|
message: textSummary(messageBody)
|
|
2667
2989
|
});
|
|
@@ -2705,7 +3027,7 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
2705
3027
|
releaseTerminalLock();
|
|
2706
3028
|
}
|
|
2707
3029
|
}
|
|
2708
|
-
const
|
|
3030
|
+
const terminalBridge = createTerminalAgentBridge(options);
|
|
2709
3031
|
const bridgeStartedAt = new Date().toISOString();
|
|
2710
3032
|
const agentTimeoutMinutes = Number(options.agentTimeoutMinutes ?? DEFAULT_AGENT_TIMEOUT_MINUTES);
|
|
2711
3033
|
const agentHardTimeoutMinutes = positiveMinutes(options.agentHardTimeoutMinutes ?? DEFAULT_AGENT_HARD_TIMEOUT_MINUTES, "--agent-hard-timeout-minutes");
|
|
@@ -2719,25 +3041,60 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
2719
3041
|
forkTakeover: undefined
|
|
2720
3042
|
}))
|
|
2721
3043
|
: terminalSubmissionPayload(String(message.body ?? ""));
|
|
3044
|
+
const preSendRuntime = {
|
|
3045
|
+
...terminalRuntimeIdentityForConversation(nextConversation, terminalControl),
|
|
3046
|
+
messageId: message.id
|
|
3047
|
+
};
|
|
2722
3048
|
let preSendScreenFingerprint;
|
|
2723
3049
|
if (bridge) {
|
|
2724
3050
|
try {
|
|
2725
|
-
const
|
|
3051
|
+
const status = await terminalBridge.status(executor.kind, terminalControl, {
|
|
2726
3052
|
scrollbackLines: Number(options.scrollbackLines ?? 120),
|
|
2727
|
-
|
|
3053
|
+
runtime: preSendRuntime
|
|
2728
3054
|
});
|
|
2729
|
-
|
|
3055
|
+
if (executor.kind === "claude") {
|
|
3056
|
+
assertSafeClaudeTerminalSend(status);
|
|
3057
|
+
}
|
|
3058
|
+
preSendScreenFingerprint = terminalBridgeScreenFingerprint(status.screen.excerpt);
|
|
2730
3059
|
}
|
|
2731
|
-
catch {
|
|
2732
|
-
|
|
3060
|
+
catch (error) {
|
|
3061
|
+
if (executor.kind === "claude") {
|
|
3062
|
+
throw new Error(`refusing to send to Claude Code without a verified idle terminal: ${error instanceof Error ? error.message : String(error)}`);
|
|
3063
|
+
}
|
|
3064
|
+
// Codex delivery can still use its established fallback when capture is unavailable.
|
|
2733
3065
|
}
|
|
2734
3066
|
}
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
3067
|
+
if (bridge && executor.kind === "claude") {
|
|
3068
|
+
releaseClaudeHookLeasesForTerminal({
|
|
3069
|
+
storeDir: storeDirFromOptions(options),
|
|
3070
|
+
terminalControl,
|
|
3071
|
+
replacementConversationId: conversation.conversation_id
|
|
3072
|
+
});
|
|
3073
|
+
releaseClaudeHookLease(nextConversation);
|
|
3074
|
+
}
|
|
3075
|
+
const claudeHookLeaseState = bridge && executor.kind === "claude"
|
|
3076
|
+
? activateClaudeHookLease({
|
|
3077
|
+
options,
|
|
3078
|
+
conversation: nextConversation,
|
|
3079
|
+
message,
|
|
3080
|
+
terminalControl,
|
|
3081
|
+
expiresAt: deadlineAt(bridgeStartedAt, agentTimeoutMinutes > 0
|
|
3082
|
+
? Math.min(agentTimeoutMinutes, agentHardTimeoutMinutes)
|
|
3083
|
+
: agentHardTimeoutMinutes)
|
|
3084
|
+
})
|
|
3085
|
+
: undefined;
|
|
3086
|
+
const conversationWithHookLease = claudeHookLeaseState
|
|
3087
|
+
? withClaudeHookLeaseState(nextConversation, claudeHookLeaseState)
|
|
3088
|
+
: nextConversation;
|
|
3089
|
+
try {
|
|
3090
|
+
await terminalBridge.send(executor.kind, terminalControl, terminalPayload);
|
|
3091
|
+
}
|
|
3092
|
+
catch (error) {
|
|
3093
|
+
if (claudeHookLeaseState) {
|
|
3094
|
+
releaseClaudeHookLease(conversationWithHookLease);
|
|
3095
|
+
}
|
|
3096
|
+
throw error;
|
|
3097
|
+
}
|
|
2741
3098
|
const supersededConversationIds = bridge
|
|
2742
3099
|
? supersedeTerminalBridgeConversations({
|
|
2743
3100
|
storeDir: storeDirFromOptions(options),
|
|
@@ -2765,7 +3122,7 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
2765
3122
|
});
|
|
2766
3123
|
const bridgeConversation = bridge
|
|
2767
3124
|
? withTerminalBridgeState({
|
|
2768
|
-
conversation:
|
|
3125
|
+
conversation: conversationWithHookLease,
|
|
2769
3126
|
message,
|
|
2770
3127
|
startedAt: bridgeStartedAt,
|
|
2771
3128
|
agentTimeoutMinutes,
|
|
@@ -2827,12 +3184,12 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
2827
3184
|
function terminalSubmissionPayload(payload) {
|
|
2828
3185
|
return payload.replace(/[\r\n]+$/u, "");
|
|
2829
3186
|
}
|
|
2830
|
-
function createManagedTerminalConversationFromRawId({ options, conversationId, messageBody, terminalControl }) {
|
|
3187
|
+
function createManagedTerminalConversationFromRawId({ options, conversationId, agent, pid, messageBody, terminalControl }) {
|
|
2831
3188
|
const workspace = terminalControl.currentPath ?? process.cwd();
|
|
2832
3189
|
const storeDir = expandHome(options.storeDir ?? options.logDir ?? defaultStoreDir(workspace));
|
|
2833
3190
|
cleanupIdleConversations(storeDir, options);
|
|
2834
3191
|
const executor = resolveExecutor({
|
|
2835
|
-
kind:
|
|
3192
|
+
kind: agent,
|
|
2836
3193
|
session: conversationId
|
|
2837
3194
|
});
|
|
2838
3195
|
const now = new Date();
|
|
@@ -2858,6 +3215,9 @@ function createManagedTerminalConversationFromRawId({ options, conversationId, m
|
|
|
2858
3215
|
gatewaySession: options.gatewaySession,
|
|
2859
3216
|
openclawBin: options.openclawBin ?? resolveOptionalExecutable("openclaw")
|
|
2860
3217
|
});
|
|
3218
|
+
const claudeAgent = agent === "claude"
|
|
3219
|
+
? loadClaudeAgentRows(options).find((row) => row.pid === pid)
|
|
3220
|
+
: undefined;
|
|
2861
3221
|
const attachedConversation = withStoragePaths({
|
|
2862
3222
|
...conversation,
|
|
2863
3223
|
executor,
|
|
@@ -2870,12 +3230,14 @@ function createManagedTerminalConversationFromRawId({ options, conversationId, m
|
|
|
2870
3230
|
gateway_session: options.gatewaySession ?? options.openclawSession ?? "agent:main:main",
|
|
2871
3231
|
openclaw_bin: options.openclawBin ?? resolveOptionalExecutable("openclaw"),
|
|
2872
3232
|
executor_all_proxy: proxyForExecutor(executor, options),
|
|
2873
|
-
executor_model: options.model ?? options.codexModel,
|
|
3233
|
+
executor_model: options.model ?? (agent === "codex" ? options.codexModel : undefined),
|
|
2874
3234
|
native_session_takeover: {
|
|
2875
|
-
agent
|
|
3235
|
+
agent,
|
|
2876
3236
|
native_session_id: conversationId,
|
|
3237
|
+
terminal_agent_pid: pid,
|
|
3238
|
+
terminal_agent_session_id: claudeAgent?.sessionId,
|
|
2877
3239
|
source_cwd: workspace,
|
|
2878
|
-
source_title: `Terminal-controlled
|
|
3240
|
+
source_title: `Terminal-controlled ${executor.display_name} ${terminalControl.target}`,
|
|
2879
3241
|
strategy: "terminal_control",
|
|
2880
3242
|
attached_at: now.toISOString(),
|
|
2881
3243
|
takeover_match_kind: "raw_terminal_send",
|
|
@@ -2890,7 +3252,7 @@ function createManagedTerminalConversationFromRawId({ options, conversationId, m
|
|
|
2890
3252
|
conversation_id: attachedConversation.conversation_id,
|
|
2891
3253
|
event: "raw_terminal_session_attached",
|
|
2892
3254
|
source_conversation_id: conversationId,
|
|
2893
|
-
agent
|
|
3255
|
+
agent,
|
|
2894
3256
|
terminal_control: terminalControl,
|
|
2895
3257
|
executor
|
|
2896
3258
|
});
|
|
@@ -2978,14 +3340,16 @@ function supersedeTerminalBridgeConversations({ storeDir, terminalControl, repla
|
|
|
2978
3340
|
continue;
|
|
2979
3341
|
}
|
|
2980
3342
|
const now = new Date().toISOString();
|
|
2981
|
-
|
|
3343
|
+
const closedConversation = {
|
|
2982
3344
|
...current,
|
|
2983
3345
|
status: "closed",
|
|
2984
3346
|
closed_at: now,
|
|
2985
3347
|
close_reason: "terminal bridge superseded by a newer task on the same terminal",
|
|
2986
3348
|
superseded_by_conversation_id: replacementConversationId,
|
|
2987
3349
|
updated_at: now
|
|
2988
|
-
}
|
|
3350
|
+
};
|
|
3351
|
+
saveState(candidateStatePath, closedConversation);
|
|
3352
|
+
releaseClaudeHookLease(closedConversation);
|
|
2989
3353
|
appendEvent(logPathForStatePath(candidateStatePath), {
|
|
2990
3354
|
ts: now,
|
|
2991
3355
|
conversation_id: current.conversation_id,
|
|
@@ -3415,17 +3779,37 @@ async function runRenew(options) {
|
|
|
3415
3779
|
throw new Error(`cannot renew ${conversation.conversation_id}; terminal bridge hard lifetime of ${hardTimeoutMinutes} minutes has elapsed`);
|
|
3416
3780
|
}
|
|
3417
3781
|
const now = new Date().toISOString();
|
|
3782
|
+
const currentMessageId = stringValue(nativeTakeover?.terminal_bridge_message_id);
|
|
3783
|
+
const hardDeadline = deadlineAt(startedAt ?? now, hardTimeoutMinutes) ??
|
|
3784
|
+
new Date(Date.now() + hardTimeoutMinutes * 60 * 1000).toISOString();
|
|
3785
|
+
const inactivityDeadline = deadlineAt(now, inactivityTimeoutMinutes) ??
|
|
3786
|
+
new Date(Date.now() + inactivityTimeoutMinutes * 60 * 1000).toISOString();
|
|
3787
|
+
const renewedLease = executorForConversation(conversation).kind === "claude" && currentMessageId
|
|
3788
|
+
? activateClaudeHookLease({
|
|
3789
|
+
options,
|
|
3790
|
+
conversation,
|
|
3791
|
+
message: { id: currentMessageId },
|
|
3792
|
+
terminalControl,
|
|
3793
|
+
expiresAt: new Date(Math.min(Date.parse(hardDeadline), Date.parse(inactivityDeadline))).toISOString()
|
|
3794
|
+
})
|
|
3795
|
+
: undefined;
|
|
3796
|
+
const renewedBase = renewedLease
|
|
3797
|
+
? withClaudeHookLeaseState(conversation, renewedLease)
|
|
3798
|
+
: conversation;
|
|
3799
|
+
const renewedNativeTakeover = isRecord(renewedBase.native_session_takeover)
|
|
3800
|
+
? renewedBase.native_session_takeover
|
|
3801
|
+
: nativeTakeover;
|
|
3418
3802
|
const renewed = {
|
|
3419
|
-
...
|
|
3803
|
+
...renewedBase,
|
|
3420
3804
|
status: "waiting_for_agent",
|
|
3421
3805
|
native_session_takeover: {
|
|
3422
|
-
...
|
|
3806
|
+
...renewedNativeTakeover,
|
|
3423
3807
|
terminal_bridge_monitor_started_at: now,
|
|
3424
3808
|
terminal_bridge_last_activity_at: now,
|
|
3425
3809
|
terminal_bridge_inactivity_timeout_minutes: inactivityTimeoutMinutes,
|
|
3426
3810
|
terminal_bridge_hard_timeout_minutes: hardTimeoutMinutes,
|
|
3427
|
-
terminal_bridge_inactivity_deadline_at:
|
|
3428
|
-
terminal_bridge_hard_deadline_at:
|
|
3811
|
+
terminal_bridge_inactivity_deadline_at: inactivityDeadline,
|
|
3812
|
+
terminal_bridge_hard_deadline_at: hardDeadline,
|
|
3429
3813
|
terminal_bridge_renewed_at: now
|
|
3430
3814
|
},
|
|
3431
3815
|
updated_at: now
|
|
@@ -3496,7 +3880,9 @@ async function runCancel(options) {
|
|
|
3496
3880
|
await runTerminalConversationCancel({
|
|
3497
3881
|
options,
|
|
3498
3882
|
conversationId: terminalConversation.conversationId,
|
|
3499
|
-
|
|
3883
|
+
agent: terminalConversation.agent,
|
|
3884
|
+
terminalControl: terminalConversation.terminalControl,
|
|
3885
|
+
pid: terminalConversation.pid
|
|
3500
3886
|
});
|
|
3501
3887
|
return;
|
|
3502
3888
|
}
|
|
@@ -3514,6 +3900,7 @@ async function runCancel(options) {
|
|
|
3514
3900
|
conversation,
|
|
3515
3901
|
statePath,
|
|
3516
3902
|
logPath,
|
|
3903
|
+
agent: executorForConversation(conversation).kind,
|
|
3517
3904
|
terminalControl
|
|
3518
3905
|
});
|
|
3519
3906
|
return;
|
|
@@ -3576,53 +3963,90 @@ async function runCancel(options) {
|
|
|
3576
3963
|
budget: budgetAction(nextConversation)
|
|
3577
3964
|
});
|
|
3578
3965
|
}
|
|
3579
|
-
async function runTerminalConversationCancel({ options, conversationId, terminalControl }) {
|
|
3580
|
-
const
|
|
3581
|
-
|
|
3582
|
-
|
|
3966
|
+
async function runTerminalConversationCancel({ options, conversationId, agent, terminalControl, pid }) {
|
|
3967
|
+
const cancellation = await createTerminalAgentBridge(options).cancel(agent, terminalControl, {
|
|
3968
|
+
runtime: {
|
|
3969
|
+
pid,
|
|
3970
|
+
cwd: terminalControl.currentPath,
|
|
3971
|
+
terminalTarget: terminalControl.target
|
|
3972
|
+
},
|
|
3973
|
+
scrollbackLines: Number(options.scrollbackLines ?? 120)
|
|
3583
3974
|
});
|
|
3584
3975
|
runtimeLog("info", "terminal_cancel_requested", {
|
|
3585
3976
|
conversation_id: conversationId,
|
|
3977
|
+
agent,
|
|
3586
3978
|
terminal_target: terminalControl.target,
|
|
3587
|
-
key:
|
|
3979
|
+
key: cancellation.key,
|
|
3980
|
+
keys: cancellation.keys,
|
|
3981
|
+
denied_approval: cancellation.deniedApproval,
|
|
3982
|
+
request_id: cancellation.requestId,
|
|
3983
|
+
cancel_requested: cancellation.cancelRequested,
|
|
3984
|
+
reason: cancellation.reason
|
|
3588
3985
|
});
|
|
3589
3986
|
printJson({
|
|
3590
3987
|
conversation_id: conversationId,
|
|
3591
3988
|
source: "terminal_control",
|
|
3592
|
-
cancel_requested:
|
|
3989
|
+
cancel_requested: cancellation.cancelRequested,
|
|
3990
|
+
reason: cancellation.reason,
|
|
3593
3991
|
terminal_control: terminalControl,
|
|
3594
|
-
key:
|
|
3992
|
+
key: cancellation.key,
|
|
3993
|
+
keys: cancellation.keys,
|
|
3994
|
+
denied_approval: cancellation.deniedApproval,
|
|
3995
|
+
request_id: cancellation.requestId
|
|
3595
3996
|
});
|
|
3596
3997
|
}
|
|
3597
|
-
async function runTerminalControlCancel({ options, conversation, statePath, logPath, terminalControl }) {
|
|
3598
|
-
const
|
|
3599
|
-
|
|
3600
|
-
|
|
3998
|
+
async function runTerminalControlCancel({ options, conversation, statePath, logPath, agent, terminalControl }) {
|
|
3999
|
+
const cancellation = await createTerminalAgentBridge(options).cancel(agent, terminalControl, {
|
|
4000
|
+
runtime: terminalRuntimeIdentityForConversation(conversation, terminalControl),
|
|
4001
|
+
scrollbackLines: Number(options.scrollbackLines ?? 120)
|
|
3601
4002
|
});
|
|
4003
|
+
if (!cancellation.cancelRequested) {
|
|
4004
|
+
printJson({
|
|
4005
|
+
conversation,
|
|
4006
|
+
cancel_requested: false,
|
|
4007
|
+
reason: cancellation.reason,
|
|
4008
|
+
terminal_control: terminalControl,
|
|
4009
|
+
budget: budgetAction(conversation)
|
|
4010
|
+
});
|
|
4011
|
+
return;
|
|
4012
|
+
}
|
|
3602
4013
|
const now = new Date().toISOString();
|
|
3603
4014
|
appendEvent(logPath, {
|
|
3604
4015
|
ts: now,
|
|
3605
4016
|
conversation_id: conversation.conversation_id,
|
|
3606
4017
|
event: "terminal_cancel_requested",
|
|
3607
4018
|
terminal_control: terminalControl,
|
|
3608
|
-
key:
|
|
4019
|
+
key: cancellation.key,
|
|
4020
|
+
keys: cancellation.keys,
|
|
4021
|
+
denied_approval: cancellation.deniedApproval,
|
|
4022
|
+
request_id: cancellation.requestId
|
|
3609
4023
|
});
|
|
3610
4024
|
runtimeLog("info", "terminal_cancel_requested", {
|
|
3611
4025
|
conversation_id: conversation.conversation_id,
|
|
4026
|
+
agent,
|
|
3612
4027
|
terminal_target: terminalControl.target,
|
|
3613
|
-
key:
|
|
4028
|
+
key: cancellation.key,
|
|
4029
|
+
keys: cancellation.keys,
|
|
4030
|
+
denied_approval: cancellation.deniedApproval,
|
|
4031
|
+
request_id: cancellation.requestId
|
|
3614
4032
|
});
|
|
3615
4033
|
const nextConversation = {
|
|
3616
4034
|
...conversation,
|
|
4035
|
+
status: "cancelled",
|
|
4036
|
+
cancelled_at: now,
|
|
3617
4037
|
terminal_cancel_requested_at: now,
|
|
3618
4038
|
updated_at: now
|
|
3619
4039
|
};
|
|
3620
4040
|
saveState(statePath, nextConversation);
|
|
4041
|
+
releaseClaudeHookLease(nextConversation);
|
|
3621
4042
|
printJson({
|
|
3622
4043
|
conversation: nextConversation,
|
|
3623
4044
|
cancel_requested: true,
|
|
3624
4045
|
terminal_control: terminalControl,
|
|
3625
|
-
key:
|
|
4046
|
+
key: cancellation.key,
|
|
4047
|
+
keys: cancellation.keys,
|
|
4048
|
+
denied_approval: cancellation.deniedApproval,
|
|
4049
|
+
request_id: cancellation.requestId,
|
|
3626
4050
|
budget: budgetAction(nextConversation)
|
|
3627
4051
|
});
|
|
3628
4052
|
}
|
|
@@ -3804,6 +4228,7 @@ function runClose(options) {
|
|
|
3804
4228
|
updated_at: now
|
|
3805
4229
|
};
|
|
3806
4230
|
saveState(statePath, closed);
|
|
4231
|
+
releaseClaudeHookLease(closed);
|
|
3807
4232
|
appendEvent(logPath, {
|
|
3808
4233
|
ts: now,
|
|
3809
4234
|
conversation_id: conversation.conversation_id,
|
|
@@ -3824,6 +4249,9 @@ function runClose(options) {
|
|
|
3824
4249
|
});
|
|
3825
4250
|
}
|
|
3826
4251
|
async function runMonitor(options) {
|
|
4252
|
+
if (options.callbackRetry) {
|
|
4253
|
+
return runCallbackRetryMonitor(options);
|
|
4254
|
+
}
|
|
3827
4255
|
if (options.terminalBridge) {
|
|
3828
4256
|
return await runTerminalBridgeMonitor(options);
|
|
3829
4257
|
}
|
|
@@ -3955,6 +4383,72 @@ async function runMonitor(options) {
|
|
|
3955
4383
|
sleepSync(pollIntervalMs);
|
|
3956
4384
|
}
|
|
3957
4385
|
}
|
|
4386
|
+
function startCallbackRetryMonitor({ statePath }) {
|
|
4387
|
+
const child = spawn(process.execPath, [
|
|
4388
|
+
new URL(import.meta.url).pathname,
|
|
4389
|
+
"monitor",
|
|
4390
|
+
"--callback-retry",
|
|
4391
|
+
"--state",
|
|
4392
|
+
statePath
|
|
4393
|
+
], {
|
|
4394
|
+
detached: true,
|
|
4395
|
+
stdio: "ignore",
|
|
4396
|
+
cwd: process.cwd(),
|
|
4397
|
+
env: process.env
|
|
4398
|
+
});
|
|
4399
|
+
child.unref();
|
|
4400
|
+
return child;
|
|
4401
|
+
}
|
|
4402
|
+
function runCallbackRetryMonitor(options) {
|
|
4403
|
+
const statePath = expandHome(required(options.state, "--state is required"));
|
|
4404
|
+
while (true) {
|
|
4405
|
+
const conversation = loadState(statePath);
|
|
4406
|
+
const callbackDelivery = isRecord(conversation.callback_delivery)
|
|
4407
|
+
? conversation.callback_delivery
|
|
4408
|
+
: undefined;
|
|
4409
|
+
const attempts = Number(callbackDelivery?.attempts ?? 0);
|
|
4410
|
+
if (conversation.status !== "callback_failed" || !isRecord(callbackDelivery?.message)) {
|
|
4411
|
+
return;
|
|
4412
|
+
}
|
|
4413
|
+
if (attempts > CALLBACK_RETRY_DELAYS_MS.length) {
|
|
4414
|
+
return;
|
|
4415
|
+
}
|
|
4416
|
+
const delayMs = CALLBACK_RETRY_DELAYS_MS[Math.max(0, attempts - 1)];
|
|
4417
|
+
sleepSync(delayMs);
|
|
4418
|
+
const releaseLock = acquireFileLock(`${statePath}.lock`);
|
|
4419
|
+
try {
|
|
4420
|
+
const current = loadState(statePath);
|
|
4421
|
+
const currentDelivery = isRecord(current.callback_delivery)
|
|
4422
|
+
? current.callback_delivery
|
|
4423
|
+
: undefined;
|
|
4424
|
+
if (current.status !== "callback_failed" || !isRecord(currentDelivery?.message)) {
|
|
4425
|
+
return;
|
|
4426
|
+
}
|
|
4427
|
+
try {
|
|
4428
|
+
runLockedCallback({
|
|
4429
|
+
statePath,
|
|
4430
|
+
messageJson: JSON.stringify(currentDelivery.message),
|
|
4431
|
+
gatewayMethod: stringValue(currentDelivery.gateway_method) ?? current.gateway_method,
|
|
4432
|
+
gatewaySession: stringValue(currentDelivery.gateway_session) ?? current.gateway_session,
|
|
4433
|
+
openclawSession: current.openclaw_session,
|
|
4434
|
+
openclawBin: stringValue(currentDelivery.openclaw_bin) ?? current.openclaw_bin,
|
|
4435
|
+
gatewayUrl: stringValue(currentDelivery.gateway_url) ?? current.gateway_url,
|
|
4436
|
+
token: current.gateway_token,
|
|
4437
|
+
closeTerminalBridgeOnDone: currentDelivery.close_terminal_bridge_on_done === true,
|
|
4438
|
+
retryPending: true,
|
|
4439
|
+
disableCallbackRetry: true
|
|
4440
|
+
});
|
|
4441
|
+
return;
|
|
4442
|
+
}
|
|
4443
|
+
catch {
|
|
4444
|
+
// The failed attempt is persisted by runLockedCallback; continue with bounded backoff.
|
|
4445
|
+
}
|
|
4446
|
+
}
|
|
4447
|
+
finally {
|
|
4448
|
+
releaseLock();
|
|
4449
|
+
}
|
|
4450
|
+
}
|
|
4451
|
+
}
|
|
3958
4452
|
async function runTerminalBridgeMonitor(options) {
|
|
3959
4453
|
const statePath = expandHome(required(options.state, "--state is required"));
|
|
3960
4454
|
const logPath = expandHome(options.log ?? logPathForStatePath(statePath));
|
|
@@ -3977,9 +4471,10 @@ async function runTerminalBridgeMonitor(options) {
|
|
|
3977
4471
|
const activityPersistIntervalMs = terminalBridgeActivityPersistIntervalMs(timeoutMinutes, pollIntervalMs);
|
|
3978
4472
|
const preSendScreenFingerprint = stringValue(initialNativeTakeover?.["terminal_bridge_pre_send_screen_fingerprint"]);
|
|
3979
4473
|
let previousScreenFingerprint = preSendScreenFingerprint;
|
|
3980
|
-
let
|
|
4474
|
+
let previousDurableFingerprint;
|
|
3981
4475
|
let persistedActivityReason = stringValue(initialNativeTakeover?.["terminal_bridge_last_activity_reason"]);
|
|
3982
4476
|
const executor = executorForConversation(conversation);
|
|
4477
|
+
const terminalBridge = createTerminalAgentBridge(options);
|
|
3983
4478
|
appendEvent(logPath, {
|
|
3984
4479
|
ts: new Date().toISOString(),
|
|
3985
4480
|
conversation_id: conversation.conversation_id,
|
|
@@ -4062,8 +4557,126 @@ async function runTerminalBridgeMonitor(options) {
|
|
|
4062
4557
|
});
|
|
4063
4558
|
return;
|
|
4064
4559
|
}
|
|
4065
|
-
const
|
|
4560
|
+
const requestText = String(nativeTakeover?.["terminal_bridge_request_text"] ?? conversation.user_request ?? "");
|
|
4561
|
+
const startedAt = stringValue(nativeTakeover?.["terminal_bridge_started_at"]);
|
|
4562
|
+
const terminalRuntime = terminalRuntimeIdentityForConversation(conversation, terminalControl);
|
|
4563
|
+
const screenChangedSinceSend = preSendScreenFingerprint !== undefined &&
|
|
4564
|
+
previousScreenFingerprint !== undefined &&
|
|
4565
|
+
previousScreenFingerprint !== preSendScreenFingerprint;
|
|
4566
|
+
const poll = await terminalBridge.monitorPoll({
|
|
4567
|
+
agent: executor.kind,
|
|
4568
|
+
terminalControl,
|
|
4569
|
+
screenOptions: {
|
|
4570
|
+
scrollbackLines: Number(options.scrollbackLines ?? 120),
|
|
4571
|
+
requestText,
|
|
4572
|
+
screenChangedSinceSend,
|
|
4573
|
+
runtime: terminalRuntime
|
|
4574
|
+
},
|
|
4575
|
+
durableRequest: {
|
|
4576
|
+
sessionId: terminalRuntime.sessionId,
|
|
4577
|
+
cwd: stringValue(nativeTakeover?.["source_cwd"]),
|
|
4578
|
+
requestText,
|
|
4579
|
+
requestHash: stringValue(nativeTakeover?.["terminal_bridge_request_hash"]),
|
|
4580
|
+
startedAt,
|
|
4581
|
+
context: {
|
|
4582
|
+
conversation,
|
|
4583
|
+
nativeTakeover,
|
|
4584
|
+
...terminalRuntime
|
|
4585
|
+
}
|
|
4586
|
+
}
|
|
4587
|
+
});
|
|
4588
|
+
const terminalStatus = poll.status;
|
|
4066
4589
|
const approval = terminalStatus.approval_state;
|
|
4590
|
+
if (isRecord(approval) && approval.blocked === true && approval.approvable !== true) {
|
|
4591
|
+
const approvalReason = stringValue(approval.reason) ??
|
|
4592
|
+
"Claude Code permission state cannot be safely resolved through AKK";
|
|
4593
|
+
appendEvent(logPath, {
|
|
4594
|
+
ts: new Date().toISOString(),
|
|
4595
|
+
conversation_id: conversation.conversation_id,
|
|
4596
|
+
event: "terminal_bridge_approval_not_approvable",
|
|
4597
|
+
terminal_control: terminalControl,
|
|
4598
|
+
activity_state: terminalStatus.activity_state,
|
|
4599
|
+
reason: approvalReason
|
|
4600
|
+
});
|
|
4601
|
+
if (/waiting for hook consumption/iu.test(approvalReason)) {
|
|
4602
|
+
sleepSync(pollIntervalMs);
|
|
4603
|
+
continue;
|
|
4604
|
+
}
|
|
4605
|
+
const fingerprint = terminalBridgeApprovalFingerprint({ terminalControl, terminalStatus });
|
|
4606
|
+
const notification = recordTerminalBridgeApprovalNotification({
|
|
4607
|
+
statePath,
|
|
4608
|
+
logPath,
|
|
4609
|
+
terminalControl,
|
|
4610
|
+
terminalStatus,
|
|
4611
|
+
fingerprint
|
|
4612
|
+
});
|
|
4613
|
+
if (notification.duplicate) {
|
|
4614
|
+
printJson({
|
|
4615
|
+
conversation: notification.conversation,
|
|
4616
|
+
monitored: true,
|
|
4617
|
+
terminal_bridge: true,
|
|
4618
|
+
awaiting_approval: true,
|
|
4619
|
+
approvable: false,
|
|
4620
|
+
duplicate: true,
|
|
4621
|
+
reason: approvalReason,
|
|
4622
|
+
terminal_control: terminalControl,
|
|
4623
|
+
terminal_status: terminalStatus
|
|
4624
|
+
});
|
|
4625
|
+
return;
|
|
4626
|
+
}
|
|
4627
|
+
const callbackMessage = createMessage({
|
|
4628
|
+
conversation: notification.conversation,
|
|
4629
|
+
from: executor.actor,
|
|
4630
|
+
to: "openclaw",
|
|
4631
|
+
type: "blocked",
|
|
4632
|
+
requiresResponse: true,
|
|
4633
|
+
body: [
|
|
4634
|
+
`${executor.display_name} is waiting at a permission state that AKK cannot safely approve.`,
|
|
4635
|
+
approvalReason,
|
|
4636
|
+
"",
|
|
4637
|
+
`Conversation: ${notification.conversation.conversation_id}`,
|
|
4638
|
+
`Terminal: ${terminalControl.target}`,
|
|
4639
|
+
"Review and resolve this dialog in the terminal manually. AKK intentionally sends no key when the request identity cannot be revalidated."
|
|
4640
|
+
].join("\n"),
|
|
4641
|
+
metadata: {
|
|
4642
|
+
source: "terminal_bridge",
|
|
4643
|
+
reason: "approval_not_approvable",
|
|
4644
|
+
terminal_control: terminalControl,
|
|
4645
|
+
terminal_status: terminalStatus,
|
|
4646
|
+
approval_fingerprint: fingerprint
|
|
4647
|
+
}
|
|
4648
|
+
});
|
|
4649
|
+
if (notification.conversation.gateway_method) {
|
|
4650
|
+
runLockedCallback({
|
|
4651
|
+
...options,
|
|
4652
|
+
statePath,
|
|
4653
|
+
log: logPath,
|
|
4654
|
+
messageJson: JSON.stringify(callbackMessage),
|
|
4655
|
+
gatewayMethod: notification.conversation.gateway_method,
|
|
4656
|
+
gatewaySession: notification.conversation.gateway_session,
|
|
4657
|
+
openclawSession: notification.conversation.openclaw_session,
|
|
4658
|
+
openclawBin: notification.conversation.openclaw_bin,
|
|
4659
|
+
gatewayUrl: stringValue(notification.conversation.gateway_token)
|
|
4660
|
+
? notification.conversation.gateway_url
|
|
4661
|
+
: undefined,
|
|
4662
|
+
token: stringValue(notification.conversation.gateway_token)
|
|
4663
|
+
});
|
|
4664
|
+
return;
|
|
4665
|
+
}
|
|
4666
|
+
printJson({
|
|
4667
|
+
conversation: notification.conversation,
|
|
4668
|
+
monitored: true,
|
|
4669
|
+
terminal_bridge: true,
|
|
4670
|
+
awaiting_approval: true,
|
|
4671
|
+
approvable: false,
|
|
4672
|
+
delivered: false,
|
|
4673
|
+
message: callbackMessage,
|
|
4674
|
+
reason: "gateway_method_missing",
|
|
4675
|
+
terminal_control: terminalControl,
|
|
4676
|
+
terminal_status: terminalStatus
|
|
4677
|
+
});
|
|
4678
|
+
return;
|
|
4679
|
+
}
|
|
4067
4680
|
if (isRecord(approval) && approval.blocked === true) {
|
|
4068
4681
|
const fingerprint = terminalBridgeApprovalFingerprint({ terminalControl, terminalStatus });
|
|
4069
4682
|
appendEvent(logPath, {
|
|
@@ -4117,7 +4730,7 @@ async function runTerminalBridgeMonitor(options) {
|
|
|
4117
4730
|
terminalStatus,
|
|
4118
4731
|
fingerprint
|
|
4119
4732
|
}),
|
|
4120
|
-
approve_command: `AKK approve ${notification.conversation.conversation_id}`,
|
|
4733
|
+
approve_command: `AKK approve ${notification.conversation.conversation_id} --expected-approval-fingerprint ${fingerprint}`,
|
|
4121
4734
|
deny_command: `AKK cancel ${notification.conversation.conversation_id}`,
|
|
4122
4735
|
approve_tool: "agent_knock_knock_approve",
|
|
4123
4736
|
deny_tool: "agent_knock_knock_cancel"
|
|
@@ -4155,40 +4768,22 @@ async function runTerminalBridgeMonitor(options) {
|
|
|
4155
4768
|
const screenChanged = previousScreenFingerprint !== undefined &&
|
|
4156
4769
|
screenFingerprint !== undefined &&
|
|
4157
4770
|
screenFingerprint !== previousScreenFingerprint;
|
|
4158
|
-
const screenChangedSinceSend = preSendScreenFingerprint !== undefined &&
|
|
4159
|
-
screenFingerprint !== undefined &&
|
|
4160
|
-
screenFingerprint !== preSendScreenFingerprint;
|
|
4161
4771
|
previousScreenFingerprint = screenFingerprint;
|
|
4162
|
-
const
|
|
4163
|
-
|
|
4164
|
-
nativeTakeover,
|
|
4165
|
-
options
|
|
4166
|
-
});
|
|
4167
|
-
const startedAt = stringValue(nativeTakeover?.["terminal_bridge_started_at"]);
|
|
4168
|
-
const rolloutCompletion = contextMatch?.context
|
|
4169
|
-
? latestCompletedRolloutTurn({
|
|
4170
|
-
context: contextMatch.context,
|
|
4171
|
-
conversation,
|
|
4172
|
-
nativeTakeover,
|
|
4173
|
-
startedAt
|
|
4174
|
-
})
|
|
4175
|
-
: undefined;
|
|
4176
|
-
const assistantMessage = contextMatch?.context
|
|
4177
|
-
? latestAssistantAfter(contextMatch.context, startedAt)
|
|
4178
|
-
: undefined;
|
|
4179
|
-
const rolloutFingerprint = rolloutCompletion || assistantMessage
|
|
4772
|
+
const durableCompletion = poll.durableCompletion;
|
|
4773
|
+
const durableFingerprint = durableCompletion
|
|
4180
4774
|
? terminalBridgeActivityFingerprint(JSON.stringify({
|
|
4181
|
-
text:
|
|
4182
|
-
timestamp:
|
|
4183
|
-
|
|
4775
|
+
text: durableCompletion.text,
|
|
4776
|
+
timestamp: durableCompletion.timestamp,
|
|
4777
|
+
id: durableCompletion.id,
|
|
4778
|
+
metadata: durableCompletion.metadata
|
|
4184
4779
|
}))
|
|
4185
4780
|
: undefined;
|
|
4186
|
-
const
|
|
4187
|
-
|
|
4781
|
+
const durableChanged = durableFingerprint !== undefined && durableFingerprint !== previousDurableFingerprint;
|
|
4782
|
+
previousDurableFingerprint = durableFingerprint;
|
|
4188
4783
|
const activityReasons = [
|
|
4189
4784
|
terminalStatus.activity_state === "working" ? terminalStatus.activity_reason : undefined,
|
|
4190
4785
|
screenChanged ? "terminal screen changed" : undefined,
|
|
4191
|
-
|
|
4786
|
+
durableChanged ? "durable completion evidence changed" : undefined
|
|
4192
4787
|
].filter((value) => Boolean(value));
|
|
4193
4788
|
if (activityReasons.length > 0) {
|
|
4194
4789
|
const observedAtMs = Date.now();
|
|
@@ -4213,77 +4808,118 @@ async function runTerminalBridgeMonitor(options) {
|
|
|
4213
4808
|
}
|
|
4214
4809
|
}
|
|
4215
4810
|
}
|
|
4216
|
-
const
|
|
4217
|
-
const
|
|
4218
|
-
|
|
4219
|
-
|
|
4220
|
-
|
|
4221
|
-
terminalStatus,
|
|
4222
|
-
screenChangedSinceSend
|
|
4223
|
-
})
|
|
4811
|
+
const completion = poll.completion;
|
|
4812
|
+
const completionMetadata = isRecord(completion?.metadata) ? completion.metadata : {};
|
|
4813
|
+
const completionMatch = completion
|
|
4814
|
+
? stringValue(completionMetadata.match) ??
|
|
4815
|
+
(completion.source === "screen" ? "terminal_screen" : "durable_completion")
|
|
4224
4816
|
: undefined;
|
|
4225
|
-
const completion = rolloutCompletion ?? (terminalIdle ? assistantMessage ?? screenMessage : undefined);
|
|
4226
|
-
const completionMatch = rolloutCompletion
|
|
4227
|
-
? "rollout_task_complete"
|
|
4228
|
-
: assistantMessage
|
|
4229
|
-
? contextMatch?.match
|
|
4230
|
-
: "terminal_screen";
|
|
4231
4817
|
const completionFingerprint = completion
|
|
4232
4818
|
? createHash("sha256")
|
|
4233
4819
|
.update(JSON.stringify({
|
|
4234
4820
|
text: completion.text,
|
|
4235
4821
|
timestamp: completion.timestamp,
|
|
4236
4822
|
match: completionMatch,
|
|
4237
|
-
|
|
4823
|
+
source: completion.source,
|
|
4824
|
+
id: completion.id,
|
|
4238
4825
|
message_id: currentMessageId
|
|
4239
4826
|
}))
|
|
4240
4827
|
.digest("hex")
|
|
4241
4828
|
: undefined;
|
|
4242
4829
|
const completionStable = completionFingerprint !== undefined && completionFingerprint === idleCompletionFingerprint;
|
|
4243
4830
|
idleCompletionFingerprint = completionFingerprint;
|
|
4244
|
-
if (completion && completionStable) {
|
|
4245
|
-
|
|
4246
|
-
|
|
4247
|
-
|
|
4248
|
-
|
|
4249
|
-
|
|
4250
|
-
|
|
4251
|
-
codex_session: contextMatch?.context.source,
|
|
4252
|
-
assistant_timestamp: completion?.timestamp,
|
|
4253
|
-
rollout_turn_id: rolloutCompletion?.turnId,
|
|
4254
|
-
terminal_bridge_message_id: currentMessageId
|
|
4831
|
+
if (completion && completionStable && completionFingerprint) {
|
|
4832
|
+
const completionOutcome = completion.outcome === "failure" ? "failure" : "success";
|
|
4833
|
+
const callbackMessageId = deterministicTerminalCallbackMessageId({
|
|
4834
|
+
conversationId: conversation.conversation_id,
|
|
4835
|
+
terminalMessageId: currentMessageId,
|
|
4836
|
+
completionFingerprint,
|
|
4837
|
+
outcome: completionOutcome
|
|
4255
4838
|
});
|
|
4256
|
-
const
|
|
4257
|
-
|
|
4258
|
-
|
|
4259
|
-
|
|
4260
|
-
|
|
4261
|
-
|
|
4262
|
-
|
|
4263
|
-
|
|
4264
|
-
|
|
4839
|
+
const claim = claimTerminalBridgeCompletion({
|
|
4840
|
+
statePath,
|
|
4841
|
+
logPath,
|
|
4842
|
+
terminalMessageId: currentMessageId,
|
|
4843
|
+
completionFingerprint,
|
|
4844
|
+
completionId: completion.id,
|
|
4845
|
+
callbackMessageId,
|
|
4846
|
+
outcome: completionOutcome
|
|
4847
|
+
});
|
|
4848
|
+
if (!claim.claimed) {
|
|
4849
|
+
printJson({
|
|
4850
|
+
conversation: claim.conversation,
|
|
4851
|
+
monitored: true,
|
|
4852
|
+
terminal_bridge: true,
|
|
4853
|
+
completed: false,
|
|
4854
|
+
duplicate: true,
|
|
4855
|
+
reason: claim.reason
|
|
4856
|
+
});
|
|
4857
|
+
return;
|
|
4858
|
+
}
|
|
4859
|
+
try {
|
|
4860
|
+
conversation = claim.conversation;
|
|
4861
|
+
appendEvent(logPath, {
|
|
4862
|
+
ts: new Date().toISOString(),
|
|
4863
|
+
conversation_id: conversation.conversation_id,
|
|
4864
|
+
event: "terminal_bridge_completion_detected",
|
|
4265
4865
|
terminal_control: terminalControl,
|
|
4266
|
-
codex_session: contextMatch?.context.source,
|
|
4267
|
-
confidence: rolloutCompletion || assistantMessage ? contextMatch?.confidence : "screen_only",
|
|
4268
4866
|
match: completionMatch,
|
|
4867
|
+
completion_source: completion.source,
|
|
4868
|
+
completion_outcome: completionOutcome,
|
|
4869
|
+
completion_id: completion.id,
|
|
4870
|
+
terminal_session: completionMetadata.session,
|
|
4871
|
+
context_match: completionMetadata.context_match,
|
|
4269
4872
|
assistant_timestamp: completion?.timestamp,
|
|
4270
|
-
rollout_turn_id:
|
|
4271
|
-
terminal_bridge_message_id: currentMessageId
|
|
4272
|
-
|
|
4273
|
-
|
|
4274
|
-
|
|
4275
|
-
|
|
4276
|
-
|
|
4277
|
-
|
|
4278
|
-
|
|
4279
|
-
|
|
4280
|
-
|
|
4281
|
-
|
|
4282
|
-
|
|
4283
|
-
|
|
4284
|
-
|
|
4285
|
-
|
|
4286
|
-
|
|
4873
|
+
rollout_turn_id: completion.source === "durable" ? completion.id : undefined,
|
|
4874
|
+
terminal_bridge_message_id: currentMessageId,
|
|
4875
|
+
callback_message_id: callbackMessageId
|
|
4876
|
+
});
|
|
4877
|
+
const callbackMessage = {
|
|
4878
|
+
...createMessage({
|
|
4879
|
+
conversation,
|
|
4880
|
+
from: executor.actor,
|
|
4881
|
+
to: "openclaw",
|
|
4882
|
+
type: completionOutcome === "failure" ? "error" : "done",
|
|
4883
|
+
requiresResponse: false,
|
|
4884
|
+
body: completion.text,
|
|
4885
|
+
metadata: {
|
|
4886
|
+
source: "terminal_bridge",
|
|
4887
|
+
terminal_control: terminalControl,
|
|
4888
|
+
...completionMetadata,
|
|
4889
|
+
completion_source: completion.source,
|
|
4890
|
+
completion_outcome: completionOutcome,
|
|
4891
|
+
completion_id: completion.id,
|
|
4892
|
+
terminal_session: completionMetadata.session,
|
|
4893
|
+
confidence: completion.confidence,
|
|
4894
|
+
match: completionMatch,
|
|
4895
|
+
assistant_timestamp: completion?.timestamp,
|
|
4896
|
+
rollout_turn_id: completion.source === "durable" ? completion.id : undefined,
|
|
4897
|
+
terminal_bridge_message_id: currentMessageId
|
|
4898
|
+
}
|
|
4899
|
+
}),
|
|
4900
|
+
id: callbackMessageId
|
|
4901
|
+
};
|
|
4902
|
+
runLockedCallback({
|
|
4903
|
+
...options,
|
|
4904
|
+
statePath,
|
|
4905
|
+
log: logPath,
|
|
4906
|
+
closeTerminalBridgeOnDone: completionOutcome === "success",
|
|
4907
|
+
trackCallbackDelivery: true,
|
|
4908
|
+
recoverTerminalCompletion: claim.resumed === true,
|
|
4909
|
+
preserveMessageId: true,
|
|
4910
|
+
messageJson: JSON.stringify(callbackMessage),
|
|
4911
|
+
gatewayMethod: conversation.gateway_method,
|
|
4912
|
+
gatewaySession: conversation.gateway_session,
|
|
4913
|
+
openclawSession: conversation.openclaw_session,
|
|
4914
|
+
openclawBin: conversation.openclaw_bin,
|
|
4915
|
+
gatewayUrl: stringValue(conversation.gateway_token) ? conversation.gateway_url : undefined,
|
|
4916
|
+
token: stringValue(conversation.gateway_token)
|
|
4917
|
+
});
|
|
4918
|
+
}
|
|
4919
|
+
finally {
|
|
4920
|
+
releaseClaudeHookLease(conversation);
|
|
4921
|
+
claim.release();
|
|
4922
|
+
}
|
|
4287
4923
|
return;
|
|
4288
4924
|
}
|
|
4289
4925
|
// A concrete approval or completion observed on this poll wins over a timeout boundary.
|
|
@@ -4334,7 +4970,7 @@ async function runTerminalBridgeMonitor(options) {
|
|
|
4334
4970
|
detail: {
|
|
4335
4971
|
terminal_bridge: true,
|
|
4336
4972
|
terminal_control: terminalControl,
|
|
4337
|
-
match:
|
|
4973
|
+
match: completionMetadata.context_match,
|
|
4338
4974
|
terminal_activity_state: terminalStatus.activity_state,
|
|
4339
4975
|
last_activity_at: new Date(lastActivityAtMs).toISOString(),
|
|
4340
4976
|
inactivity_deadline_at: new Date(lastActivityAtMs + timeoutMinutes * 60 * 1000).toISOString(),
|
|
@@ -4387,6 +5023,111 @@ function terminalBridgeRequestFingerprint(value) {
|
|
|
4387
5023
|
const text = String(value ?? "").replace(/\s+/gu, " ").trim();
|
|
4388
5024
|
return text ? createHash("sha256").update(text).digest("hex") : undefined;
|
|
4389
5025
|
}
|
|
5026
|
+
function deterministicTerminalCallbackMessageId({ conversationId, terminalMessageId, completionFingerprint, outcome }) {
|
|
5027
|
+
const digest = createHash("sha256")
|
|
5028
|
+
.update(JSON.stringify({
|
|
5029
|
+
conversation_id: conversationId,
|
|
5030
|
+
terminal_message_id: terminalMessageId,
|
|
5031
|
+
completion_fingerprint: completionFingerprint,
|
|
5032
|
+
outcome
|
|
5033
|
+
}))
|
|
5034
|
+
.digest("hex")
|
|
5035
|
+
.slice(0, 32);
|
|
5036
|
+
return `msg-terminal-${digest}`;
|
|
5037
|
+
}
|
|
5038
|
+
function claimTerminalBridgeCompletion({ statePath, logPath, terminalMessageId, completionFingerprint, completionId, callbackMessageId, outcome }) {
|
|
5039
|
+
const release = acquireFileLock(`${statePath}.lock`);
|
|
5040
|
+
try {
|
|
5041
|
+
const conversation = loadState(statePath);
|
|
5042
|
+
const nativeTakeover = isRecord(conversation.native_session_takeover)
|
|
5043
|
+
? conversation.native_session_takeover
|
|
5044
|
+
: {};
|
|
5045
|
+
if (!isWaitingForAgent(conversation.status)) {
|
|
5046
|
+
release();
|
|
5047
|
+
return {
|
|
5048
|
+
claimed: false,
|
|
5049
|
+
conversation,
|
|
5050
|
+
reason: "conversation_no_longer_waiting"
|
|
5051
|
+
};
|
|
5052
|
+
}
|
|
5053
|
+
if (stringValue(nativeTakeover.terminal_bridge_message_id) !== terminalMessageId) {
|
|
5054
|
+
release();
|
|
5055
|
+
return {
|
|
5056
|
+
claimed: false,
|
|
5057
|
+
conversation,
|
|
5058
|
+
reason: "terminal_bridge_task_replaced"
|
|
5059
|
+
};
|
|
5060
|
+
}
|
|
5061
|
+
const existing = isRecord(nativeTakeover.terminal_bridge_completion_claim)
|
|
5062
|
+
? nativeTakeover.terminal_bridge_completion_claim
|
|
5063
|
+
: undefined;
|
|
5064
|
+
if (existing) {
|
|
5065
|
+
if (existing.callback_message_id === callbackMessageId &&
|
|
5066
|
+
existing.terminal_bridge_message_id === terminalMessageId &&
|
|
5067
|
+
existing.completion_fingerprint === completionFingerprint &&
|
|
5068
|
+
existing.outcome === outcome) {
|
|
5069
|
+
appendEvent(logPath, {
|
|
5070
|
+
ts: new Date().toISOString(),
|
|
5071
|
+
conversation_id: conversation.conversation_id,
|
|
5072
|
+
event: "terminal_bridge_completion_claim_resumed",
|
|
5073
|
+
terminal_bridge_message_id: terminalMessageId,
|
|
5074
|
+
completion_fingerprint: completionFingerprint,
|
|
5075
|
+
callback_message_id: callbackMessageId,
|
|
5076
|
+
outcome
|
|
5077
|
+
});
|
|
5078
|
+
return {
|
|
5079
|
+
claimed: true,
|
|
5080
|
+
resumed: true,
|
|
5081
|
+
conversation,
|
|
5082
|
+
release
|
|
5083
|
+
};
|
|
5084
|
+
}
|
|
5085
|
+
release();
|
|
5086
|
+
return {
|
|
5087
|
+
claimed: false,
|
|
5088
|
+
conversation,
|
|
5089
|
+
reason: "terminal_bridge_completion_claim_conflict"
|
|
5090
|
+
};
|
|
5091
|
+
}
|
|
5092
|
+
const claimedAt = new Date().toISOString();
|
|
5093
|
+
const claimedConversation = {
|
|
5094
|
+
...conversation,
|
|
5095
|
+
native_session_takeover: {
|
|
5096
|
+
...nativeTakeover,
|
|
5097
|
+
terminal_bridge_completion_claim: {
|
|
5098
|
+
terminal_bridge_message_id: terminalMessageId,
|
|
5099
|
+
completion_fingerprint: completionFingerprint,
|
|
5100
|
+
completion_id: completionId,
|
|
5101
|
+
callback_message_id: callbackMessageId,
|
|
5102
|
+
outcome,
|
|
5103
|
+
claimed_at: claimedAt
|
|
5104
|
+
}
|
|
5105
|
+
},
|
|
5106
|
+
updated_at: claimedAt
|
|
5107
|
+
};
|
|
5108
|
+
saveState(statePath, claimedConversation);
|
|
5109
|
+
appendEvent(logPath, {
|
|
5110
|
+
ts: claimedAt,
|
|
5111
|
+
conversation_id: conversation.conversation_id,
|
|
5112
|
+
event: "terminal_bridge_completion_claimed",
|
|
5113
|
+
terminal_bridge_message_id: terminalMessageId,
|
|
5114
|
+
completion_fingerprint: completionFingerprint,
|
|
5115
|
+
completion_id: completionId,
|
|
5116
|
+
callback_message_id: callbackMessageId,
|
|
5117
|
+
outcome
|
|
5118
|
+
});
|
|
5119
|
+
return {
|
|
5120
|
+
claimed: true,
|
|
5121
|
+
resumed: false,
|
|
5122
|
+
conversation: claimedConversation,
|
|
5123
|
+
release
|
|
5124
|
+
};
|
|
5125
|
+
}
|
|
5126
|
+
catch (error) {
|
|
5127
|
+
release();
|
|
5128
|
+
throw error;
|
|
5129
|
+
}
|
|
5130
|
+
}
|
|
4390
5131
|
function terminalBridgeActivityPersistIntervalMs(timeoutMinutes, pollIntervalMs) {
|
|
4391
5132
|
if (!Number.isFinite(timeoutMinutes) || timeoutMinutes <= 0) {
|
|
4392
5133
|
return 5 * 60 * 1000;
|
|
@@ -4415,6 +5156,13 @@ function persistTerminalBridgeActivity({ conversation, statePath, logPath, obser
|
|
|
4415
5156
|
const inactivityDeadlineAt = Number.isFinite(timeoutMinutes) && timeoutMinutes > 0
|
|
4416
5157
|
? new Date(observedAtMs + timeoutMinutes * 60 * 1000).toISOString()
|
|
4417
5158
|
: undefined;
|
|
5159
|
+
const hardDeadlineAt = stringValue(nativeTakeover.terminal_bridge_hard_deadline_at);
|
|
5160
|
+
const leaseDeadlineAt = inactivityDeadlineAt && hardDeadlineAt
|
|
5161
|
+
? new Date(Math.min(Date.parse(inactivityDeadlineAt), Date.parse(hardDeadlineAt))).toISOString()
|
|
5162
|
+
: inactivityDeadlineAt ?? hardDeadlineAt;
|
|
5163
|
+
const renewedLease = leaseDeadlineAt
|
|
5164
|
+
? renewClaudeHookLease(currentConversation, leaseDeadlineAt)
|
|
5165
|
+
: undefined;
|
|
4418
5166
|
const nextConversation = {
|
|
4419
5167
|
...currentConversation,
|
|
4420
5168
|
native_session_takeover: {
|
|
@@ -4423,7 +5171,8 @@ function persistTerminalBridgeActivity({ conversation, statePath, logPath, obser
|
|
|
4423
5171
|
terminal_bridge_last_activity_reason: reason,
|
|
4424
5172
|
terminal_bridge_inactivity_deadline_at: inactivityDeadlineAt,
|
|
4425
5173
|
terminal_bridge_inactivity_timeout_minutes: timeoutMinutes,
|
|
4426
|
-
terminal_bridge_hard_timeout_minutes: hardTimeoutMinutes
|
|
5174
|
+
terminal_bridge_hard_timeout_minutes: hardTimeoutMinutes,
|
|
5175
|
+
claude_hook_lease_id: renewedLease?.id ?? nativeTakeover.claude_hook_lease_id
|
|
4427
5176
|
},
|
|
4428
5177
|
updated_at: observedAt
|
|
4429
5178
|
};
|
|
@@ -4465,12 +5214,15 @@ function terminalBridgeApprovalCandidate({ executor, terminalControl, terminalSt
|
|
|
4465
5214
|
agent: executor.kind,
|
|
4466
5215
|
kind: stringValue(approval.prompt_kind) ?? "unknown",
|
|
4467
5216
|
command: stringValue(approval.command),
|
|
5217
|
+
tool_name: stringValue(approval.tool_name),
|
|
5218
|
+
request_detail: stringValue(approval.request_detail),
|
|
4468
5219
|
cwd: terminalControl.currentPath,
|
|
4469
5220
|
fingerprint,
|
|
4470
|
-
terminal_target: terminalControl.target
|
|
5221
|
+
terminal_target: terminalControl.target,
|
|
5222
|
+
decision_mode: stringValue(approval.decision_mode)
|
|
4471
5223
|
};
|
|
4472
5224
|
}
|
|
4473
|
-
async function
|
|
5225
|
+
async function loadCodexTerminalContext({ conversation, nativeTakeover, options }) {
|
|
4474
5226
|
const provider = createAgentSessionProvider("codex", options);
|
|
4475
5227
|
const nativeSessionId = stringValue(nativeTakeover?.["native_session_id"]);
|
|
4476
5228
|
const startedAtMs = Date.parse(String(nativeTakeover?.["terminal_bridge_started_at"] ?? ""));
|
|
@@ -4526,146 +5278,6 @@ async function terminalBridgeCodexContext({ conversation, nativeTakeover, option
|
|
|
4526
5278
|
confidence: sessions.length === 1 ? "medium" : "low"
|
|
4527
5279
|
};
|
|
4528
5280
|
}
|
|
4529
|
-
function latestAssistantAfter(context, startedAt) {
|
|
4530
|
-
const threshold = startedAt ? Date.parse(startedAt) : undefined;
|
|
4531
|
-
return [...visibleRolloutMessages(context)]
|
|
4532
|
-
.reverse()
|
|
4533
|
-
.find((message) => {
|
|
4534
|
-
if (message.role !== "assistant") {
|
|
4535
|
-
return false;
|
|
4536
|
-
}
|
|
4537
|
-
if (!Number.isFinite(threshold)) {
|
|
4538
|
-
return true;
|
|
4539
|
-
}
|
|
4540
|
-
const messageTime = message.timestamp ? Date.parse(message.timestamp) : NaN;
|
|
4541
|
-
return Number.isFinite(messageTime) && messageTime >= Number(threshold);
|
|
4542
|
-
});
|
|
4543
|
-
}
|
|
4544
|
-
function latestCompletedRolloutTurn({ context, conversation, nativeTakeover, startedAt }) {
|
|
4545
|
-
const threshold = validTimestampMs(startedAt);
|
|
4546
|
-
const expectedRequestHash = stringValue(nativeTakeover?.["terminal_bridge_request_hash"]) ??
|
|
4547
|
-
terminalBridgeRequestFingerprint(nativeTakeover?.["terminal_bridge_request_text"] ?? conversation.user_request);
|
|
4548
|
-
if (threshold === undefined || !expectedRequestHash) {
|
|
4549
|
-
return undefined;
|
|
4550
|
-
}
|
|
4551
|
-
const turn = [...(context.turns ?? [])]
|
|
4552
|
-
.reverse()
|
|
4553
|
-
.find((candidate) => {
|
|
4554
|
-
const userTimestamp = validTimestampMs(candidate.userTimestamp);
|
|
4555
|
-
const completedAt = validTimestampMs(candidate.completedAt);
|
|
4556
|
-
return candidate.userTextHash === expectedRequestHash &&
|
|
4557
|
-
userTimestamp !== undefined &&
|
|
4558
|
-
completedAt !== undefined &&
|
|
4559
|
-
userTimestamp >= threshold &&
|
|
4560
|
-
completedAt >= userTimestamp &&
|
|
4561
|
-
Boolean(candidate.lastAssistantMessage);
|
|
4562
|
-
});
|
|
4563
|
-
if (!turn?.lastAssistantMessage) {
|
|
4564
|
-
return undefined;
|
|
4565
|
-
}
|
|
4566
|
-
return {
|
|
4567
|
-
role: "assistant",
|
|
4568
|
-
text: turn.lastAssistantMessage,
|
|
4569
|
-
timestamp: turn.completedAt,
|
|
4570
|
-
turnId: turn.turnId,
|
|
4571
|
-
userTimestamp: turn.userTimestamp
|
|
4572
|
-
};
|
|
4573
|
-
}
|
|
4574
|
-
function terminalBridgeScreenMessage({ conversation, nativeTakeover, terminalStatus, screenChangedSinceSend }) {
|
|
4575
|
-
const excerpt = stringValue(terminalStatus?.screen?.excerpt);
|
|
4576
|
-
if (!excerpt) {
|
|
4577
|
-
return undefined;
|
|
4578
|
-
}
|
|
4579
|
-
const request = String(nativeTakeover?.["terminal_bridge_request_text"] ?? conversation.user_request ?? "").trim();
|
|
4580
|
-
const promptEnd = request ? whitespaceInsensitiveMatchEnd(excerpt, request) : undefined;
|
|
4581
|
-
const afterPrompt = promptEnd === undefined ? undefined : excerpt.slice(promptEnd);
|
|
4582
|
-
const completionBoundary = afterPrompt === undefined
|
|
4583
|
-
? undefined
|
|
4584
|
-
: terminalBridgeCompletionBoundary(afterPrompt);
|
|
4585
|
-
const completionText = afterPrompt === undefined
|
|
4586
|
-
? screenChangedSinceSend
|
|
4587
|
-
? terminalBridgeLatestCompletedSegment(excerpt)
|
|
4588
|
-
: undefined
|
|
4589
|
-
: completionBoundary === undefined
|
|
4590
|
-
? afterPrompt
|
|
4591
|
-
: afterPrompt.slice(0, completionBoundary);
|
|
4592
|
-
if (completionText === undefined) {
|
|
4593
|
-
return undefined;
|
|
4594
|
-
}
|
|
4595
|
-
const cleaned = cleanTerminalBridgeScreenText(completionText);
|
|
4596
|
-
const hasCompletionEvidence = promptEnd === undefined || completionBoundary !== undefined || /[•└]/u.test(cleaned ?? "");
|
|
4597
|
-
if (!cleaned || cleaned.length < 40 || !hasCompletionEvidence) {
|
|
4598
|
-
return undefined;
|
|
4599
|
-
}
|
|
4600
|
-
return {
|
|
4601
|
-
role: "assistant",
|
|
4602
|
-
text: truncateText(cleaned, 4000),
|
|
4603
|
-
timestamp: undefined
|
|
4604
|
-
};
|
|
4605
|
-
}
|
|
4606
|
-
function whitespaceInsensitiveMatchEnd(text, expected) {
|
|
4607
|
-
const normalizedExpected = expected.replace(/\s/gu, "");
|
|
4608
|
-
if (!normalizedExpected) {
|
|
4609
|
-
return undefined;
|
|
4610
|
-
}
|
|
4611
|
-
let normalizedText = "";
|
|
4612
|
-
const sourceEnds = [];
|
|
4613
|
-
for (let index = 0; index < text.length;) {
|
|
4614
|
-
const codePoint = text.codePointAt(index);
|
|
4615
|
-
if (codePoint === undefined) {
|
|
4616
|
-
break;
|
|
4617
|
-
}
|
|
4618
|
-
const character = String.fromCodePoint(codePoint);
|
|
4619
|
-
if (!/\s/u.test(character)) {
|
|
4620
|
-
normalizedText += character;
|
|
4621
|
-
for (let codeUnit = 0; codeUnit < character.length; codeUnit += 1) {
|
|
4622
|
-
sourceEnds.push(index + character.length);
|
|
4623
|
-
}
|
|
4624
|
-
}
|
|
4625
|
-
index += character.length;
|
|
4626
|
-
}
|
|
4627
|
-
const matchIndex = normalizedText.lastIndexOf(normalizedExpected);
|
|
4628
|
-
if (matchIndex < 0) {
|
|
4629
|
-
return undefined;
|
|
4630
|
-
}
|
|
4631
|
-
return sourceEnds[matchIndex + normalizedExpected.length - 1];
|
|
4632
|
-
}
|
|
4633
|
-
function terminalBridgeLatestCompletedSegment(text) {
|
|
4634
|
-
const matches = [...text.matchAll(/^[ \t]*[─━-]+\s+Worked for\b.*$/gmu)];
|
|
4635
|
-
const completion = matches.at(-1);
|
|
4636
|
-
if (completion?.index === undefined) {
|
|
4637
|
-
return undefined;
|
|
4638
|
-
}
|
|
4639
|
-
const previousCompletion = matches.at(-2);
|
|
4640
|
-
let start = previousCompletion?.index === undefined
|
|
4641
|
-
? 0
|
|
4642
|
-
: previousCompletion.index + previousCompletion[0].length;
|
|
4643
|
-
const beforeCompletion = text.slice(0, completion.index);
|
|
4644
|
-
const prompts = [...beforeCompletion.matchAll(/^[ \t]*›(?:\s|$).*$/gmu)];
|
|
4645
|
-
const latestPrompt = prompts.at(-1);
|
|
4646
|
-
if (latestPrompt?.index !== undefined && latestPrompt.index >= start) {
|
|
4647
|
-
start = latestPrompt.index + latestPrompt[0].length;
|
|
4648
|
-
}
|
|
4649
|
-
return text.slice(start, completion.index);
|
|
4650
|
-
}
|
|
4651
|
-
function terminalBridgeCompletionBoundary(text) {
|
|
4652
|
-
const matches = [...text.matchAll(/^[ \t]*[─━-]+\s+Worked for\b.*$/gmu)];
|
|
4653
|
-
return matches.at(-1)?.index;
|
|
4654
|
-
}
|
|
4655
|
-
function cleanTerminalBridgeScreenText(text) {
|
|
4656
|
-
const lines = text
|
|
4657
|
-
.split(/\r?\n/)
|
|
4658
|
-
.map((line) => line.replace(/\s+$/u, ""))
|
|
4659
|
-
.filter((line) => {
|
|
4660
|
-
const trimmed = line.trim();
|
|
4661
|
-
return trimmed &&
|
|
4662
|
-
!trimmed.startsWith("› Use /skills") &&
|
|
4663
|
-
!/^gpt-[\w.-]+/u.test(trimmed) &&
|
|
4664
|
-
!/^[-\w.]+ default ·/u.test(trimmed);
|
|
4665
|
-
});
|
|
4666
|
-
const cleaned = lines.join("\n").trim();
|
|
4667
|
-
return cleaned || undefined;
|
|
4668
|
-
}
|
|
4669
5281
|
function resolveExecutable(command) {
|
|
4670
5282
|
if (command.includes(path.sep)) {
|
|
4671
5283
|
return command;
|
|
@@ -4784,19 +5396,66 @@ function runCallback(options) {
|
|
|
4784
5396
|
releaseLock();
|
|
4785
5397
|
}
|
|
4786
5398
|
}
|
|
5399
|
+
function runRetryCallback(options) {
|
|
5400
|
+
const { conversation, statePath } = loadConversationFromOptions(options);
|
|
5401
|
+
const callbackDelivery = isRecord(conversation.callback_delivery)
|
|
5402
|
+
? conversation.callback_delivery
|
|
5403
|
+
: undefined;
|
|
5404
|
+
if (!["callback_pending", "callback_failed"].includes(conversation.status)) {
|
|
5405
|
+
throw new Error(`cannot retry callback for ${conversation.conversation_id}; conversation is ${conversation.status}`);
|
|
5406
|
+
}
|
|
5407
|
+
if (!callbackDelivery || !isRecord(callbackDelivery.message)) {
|
|
5408
|
+
throw new Error(`cannot retry callback for ${conversation.conversation_id}; pending callback is missing`);
|
|
5409
|
+
}
|
|
5410
|
+
const releaseLock = acquireFileLock(`${statePath}.lock`);
|
|
5411
|
+
try {
|
|
5412
|
+
runLockedCallback({
|
|
5413
|
+
...options,
|
|
5414
|
+
statePath,
|
|
5415
|
+
messageJson: JSON.stringify(callbackDelivery.message),
|
|
5416
|
+
gatewayMethod: stringValue(callbackDelivery.gateway_method) ?? conversation.gateway_method,
|
|
5417
|
+
gatewaySession: stringValue(callbackDelivery.gateway_session) ?? conversation.gateway_session,
|
|
5418
|
+
openclawSession: conversation.openclaw_session,
|
|
5419
|
+
openclawBin: stringValue(callbackDelivery.openclaw_bin) ?? conversation.openclaw_bin,
|
|
5420
|
+
gatewayUrl: stringValue(callbackDelivery.gateway_url) ?? conversation.gateway_url,
|
|
5421
|
+
token: stringValue(callbackDelivery.gateway_token) ?? conversation.gateway_token,
|
|
5422
|
+
closeTerminalBridgeOnDone: callbackDelivery.close_terminal_bridge_on_done === true,
|
|
5423
|
+
retryPending: true
|
|
5424
|
+
});
|
|
5425
|
+
}
|
|
5426
|
+
finally {
|
|
5427
|
+
releaseLock();
|
|
5428
|
+
}
|
|
5429
|
+
}
|
|
4787
5430
|
function runLockedCallback(options) {
|
|
4788
5431
|
const messageInput = required(options.messageJson, "--message-json is required");
|
|
4789
5432
|
const logPath = expandHome(options.log ?? logPathForStatePath(options.statePath));
|
|
4790
5433
|
const conversation = loadState(options.statePath);
|
|
4791
5434
|
const executor = executorForConversation(conversation);
|
|
4792
|
-
const message =
|
|
4793
|
-
|
|
4794
|
-
|
|
4795
|
-
|
|
4796
|
-
|
|
4797
|
-
|
|
5435
|
+
const message = options.retryPending === true || options.preserveMessageId === true
|
|
5436
|
+
? parseMessageJson(messageInput)
|
|
5437
|
+
: extractStructuredMessage({
|
|
5438
|
+
conversation,
|
|
5439
|
+
input: messageInput,
|
|
5440
|
+
defaultFrom: executor.actor,
|
|
5441
|
+
defaultTo: "openclaw"
|
|
5442
|
+
});
|
|
5443
|
+
if (message.conversation_id !== conversation.conversation_id) {
|
|
5444
|
+
throw new Error(`message.conversation_id ${message.conversation_id} does not match conversation ${conversation.conversation_id}`);
|
|
5445
|
+
}
|
|
4798
5446
|
const existingEvents = readExistingEvents(logPath);
|
|
4799
|
-
|
|
5447
|
+
const callbackDelivery = isRecord(conversation.callback_delivery)
|
|
5448
|
+
? conversation.callback_delivery
|
|
5449
|
+
: undefined;
|
|
5450
|
+
const retryingPending = options.retryPending === true &&
|
|
5451
|
+
isRecord(callbackDelivery?.message) &&
|
|
5452
|
+
callbackDelivery.message.id === message.id &&
|
|
5453
|
+
["pending", "failed"].includes(String(callbackDelivery.status ?? ""));
|
|
5454
|
+
const duplicateMessage = isDuplicateMessage(existingEvents, message);
|
|
5455
|
+
const recoveringTerminalCompletion = options.recoverTerminalCompletion === true &&
|
|
5456
|
+
duplicateMessage &&
|
|
5457
|
+
isWaitingForAgent(conversation.status);
|
|
5458
|
+
if (duplicateMessage && !retryingPending && !recoveringTerminalCompletion) {
|
|
4800
5459
|
runtimeLog("info", "callback_duplicate", {
|
|
4801
5460
|
conversation_id: conversation.conversation_id,
|
|
4802
5461
|
agent: executor.kind,
|
|
@@ -4816,19 +5475,56 @@ function runLockedCallback(options) {
|
|
|
4816
5475
|
});
|
|
4817
5476
|
return;
|
|
4818
5477
|
}
|
|
4819
|
-
|
|
4820
|
-
|
|
5478
|
+
const closeTerminalBridgeOnDone = message.type === "done" &&
|
|
5479
|
+
options.closeTerminalBridgeOnDone === true;
|
|
5480
|
+
const trackCallbackDelivery = closeTerminalBridgeOnDone ||
|
|
5481
|
+
options.trackCallbackDelivery === true ||
|
|
5482
|
+
callbackDelivery?.track_delivery === true;
|
|
5483
|
+
const requiresDelivery = Boolean(options.gatewayMethod) || options.recordOnly !== true;
|
|
5484
|
+
const deliveryAttempt = Number(callbackDelivery?.attempts ?? 0) + 1;
|
|
5485
|
+
let nextConversation = retryingPending
|
|
5486
|
+
? conversation
|
|
5487
|
+
: applyMessageToConversation(conversation, message);
|
|
5488
|
+
const storedFinalStatus = stringValue(callbackDelivery?.final_status);
|
|
5489
|
+
const finalStatus = storedFinalStatus &&
|
|
5490
|
+
CONVERSATION_STATUSES.has(storedFinalStatus)
|
|
5491
|
+
? storedFinalStatus
|
|
5492
|
+
: nextConversation.status;
|
|
5493
|
+
if (!retryingPending && !recoveringTerminalCompletion) {
|
|
5494
|
+
appendEvent(logPath, messageEvent(message));
|
|
5495
|
+
}
|
|
5496
|
+
if (trackCallbackDelivery && requiresDelivery) {
|
|
4821
5497
|
const now = new Date().toISOString();
|
|
4822
5498
|
nextConversation = {
|
|
4823
5499
|
...nextConversation,
|
|
4824
|
-
status: "
|
|
4825
|
-
|
|
4826
|
-
|
|
5500
|
+
status: "callback_pending",
|
|
5501
|
+
callback_delivery: {
|
|
5502
|
+
status: "pending",
|
|
5503
|
+
message,
|
|
5504
|
+
attempts: deliveryAttempt,
|
|
5505
|
+
created_at: stringValue(callbackDelivery?.created_at) ?? now,
|
|
5506
|
+
last_attempt_at: now,
|
|
5507
|
+
gateway_method: options.gatewayMethod,
|
|
5508
|
+
gateway_session: options.gatewaySession ?? options.openclawSession ?? conversation.openclaw_session,
|
|
5509
|
+
gateway_url: options.gatewayUrl ?? conversation.gateway_url,
|
|
5510
|
+
openclaw_bin: options.openclawBin ?? conversation.openclaw_bin,
|
|
5511
|
+
close_terminal_bridge_on_done: closeTerminalBridgeOnDone,
|
|
5512
|
+
track_delivery: true,
|
|
5513
|
+
final_status: finalStatus
|
|
5514
|
+
},
|
|
4827
5515
|
updated_at: now
|
|
4828
5516
|
};
|
|
4829
5517
|
delete nextConversation.idle_since;
|
|
5518
|
+
delete nextConversation.closed_at;
|
|
5519
|
+
delete nextConversation.close_reason;
|
|
5520
|
+
appendEvent(logPath, {
|
|
5521
|
+
ts: now,
|
|
5522
|
+
conversation_id: conversation.conversation_id,
|
|
5523
|
+
event: retryingPending ? "callback_delivery_retry_started" : "callback_delivery_pending",
|
|
5524
|
+
message_id: message.id,
|
|
5525
|
+
attempt: deliveryAttempt
|
|
5526
|
+
});
|
|
4830
5527
|
}
|
|
4831
|
-
appendEvent(logPath, messageEvent(message));
|
|
4832
5528
|
saveState(options.statePath, nextConversation);
|
|
4833
5529
|
runtimeLog("info", "callback_received", {
|
|
4834
5530
|
conversation_id: conversation.conversation_id,
|
|
@@ -4843,13 +5539,120 @@ function runLockedCallback(options) {
|
|
|
4843
5539
|
event_log_path: logPath,
|
|
4844
5540
|
message: textSummary(message.body)
|
|
4845
5541
|
});
|
|
4846
|
-
|
|
4847
|
-
|
|
4848
|
-
|
|
4849
|
-
|
|
4850
|
-
|
|
4851
|
-
|
|
4852
|
-
|
|
5542
|
+
if (options.recordOnly) {
|
|
5543
|
+
runtimeLog("info", "callback_recorded_only", {
|
|
5544
|
+
conversation_id: conversation.conversation_id,
|
|
5545
|
+
status: nextConversation.status
|
|
5546
|
+
});
|
|
5547
|
+
printJson({
|
|
5548
|
+
conversation: nextConversation,
|
|
5549
|
+
message,
|
|
5550
|
+
budget: budgetAction(nextConversation),
|
|
5551
|
+
delivered: false,
|
|
5552
|
+
duplicate: false
|
|
5553
|
+
});
|
|
5554
|
+
return;
|
|
5555
|
+
}
|
|
5556
|
+
try {
|
|
5557
|
+
const deliveryKind = deliverCallbackToOpenClaw({
|
|
5558
|
+
options,
|
|
5559
|
+
statePath: options.statePath,
|
|
5560
|
+
logPath,
|
|
5561
|
+
conversation: nextConversation,
|
|
5562
|
+
message
|
|
5563
|
+
});
|
|
5564
|
+
const deliveredAt = new Date().toISOString();
|
|
5565
|
+
let deliveredConversation = nextConversation;
|
|
5566
|
+
if (trackCallbackDelivery) {
|
|
5567
|
+
const deliveredStatus = closeTerminalBridgeOnDone ? "closed" : finalStatus;
|
|
5568
|
+
deliveredConversation = {
|
|
5569
|
+
...nextConversation,
|
|
5570
|
+
status: deliveredStatus,
|
|
5571
|
+
...(closeTerminalBridgeOnDone
|
|
5572
|
+
? {
|
|
5573
|
+
closed_at: deliveredAt,
|
|
5574
|
+
close_reason: "terminal bridge task completed"
|
|
5575
|
+
}
|
|
5576
|
+
: {}),
|
|
5577
|
+
callback_delivery: {
|
|
5578
|
+
...(isRecord(nextConversation.callback_delivery) ? nextConversation.callback_delivery : {}),
|
|
5579
|
+
status: "delivered",
|
|
5580
|
+
delivered_at: deliveredAt,
|
|
5581
|
+
last_error: undefined
|
|
5582
|
+
},
|
|
5583
|
+
updated_at: deliveredAt
|
|
5584
|
+
};
|
|
5585
|
+
delete deliveredConversation.idle_since;
|
|
5586
|
+
saveState(options.statePath, deliveredConversation);
|
|
5587
|
+
appendEvent(logPath, {
|
|
5588
|
+
ts: deliveredAt,
|
|
5589
|
+
conversation_id: conversation.conversation_id,
|
|
5590
|
+
event: "callback_delivery_succeeded",
|
|
5591
|
+
message_id: message.id,
|
|
5592
|
+
attempt: deliveryAttempt,
|
|
5593
|
+
status: deliveredStatus
|
|
5594
|
+
});
|
|
5595
|
+
}
|
|
5596
|
+
printJson({
|
|
5597
|
+
conversation: deliveredConversation,
|
|
5598
|
+
message,
|
|
5599
|
+
budget: budgetAction(deliveredConversation),
|
|
5600
|
+
delivered: true,
|
|
5601
|
+
duplicate: false,
|
|
5602
|
+
delivery: deliveryKind
|
|
5603
|
+
});
|
|
5604
|
+
}
|
|
5605
|
+
catch (error) {
|
|
5606
|
+
if (trackCallbackDelivery) {
|
|
5607
|
+
const failedAt = new Date().toISOString();
|
|
5608
|
+
const failedConversation = {
|
|
5609
|
+
...nextConversation,
|
|
5610
|
+
status: "callback_failed",
|
|
5611
|
+
callback_delivery: {
|
|
5612
|
+
...(isRecord(nextConversation.callback_delivery) ? nextConversation.callback_delivery : {}),
|
|
5613
|
+
status: "failed",
|
|
5614
|
+
failed_at: failedAt,
|
|
5615
|
+
last_error: error instanceof Error ? error.message : String(error)
|
|
5616
|
+
},
|
|
5617
|
+
updated_at: failedAt
|
|
5618
|
+
};
|
|
5619
|
+
saveState(options.statePath, failedConversation);
|
|
5620
|
+
appendEvent(logPath, {
|
|
5621
|
+
ts: failedAt,
|
|
5622
|
+
conversation_id: conversation.conversation_id,
|
|
5623
|
+
event: "callback_delivery_failed",
|
|
5624
|
+
message_id: message.id,
|
|
5625
|
+
attempt: deliveryAttempt,
|
|
5626
|
+
error: failedConversation.callback_delivery.last_error
|
|
5627
|
+
});
|
|
5628
|
+
if (options.retryPending !== true &&
|
|
5629
|
+
options.disableCallbackRetry !== true &&
|
|
5630
|
+
deliveryAttempt <= CALLBACK_RETRY_DELAYS_MS.length) {
|
|
5631
|
+
const retryMonitor = startCallbackRetryMonitor({ statePath: options.statePath });
|
|
5632
|
+
const retryDelayMs = CALLBACK_RETRY_DELAYS_MS[Math.max(0, deliveryAttempt - 1)];
|
|
5633
|
+
const retryState = {
|
|
5634
|
+
...failedConversation,
|
|
5635
|
+
callback_delivery: {
|
|
5636
|
+
...failedConversation.callback_delivery,
|
|
5637
|
+
retry_monitor_pid: retryMonitor.pid ?? null,
|
|
5638
|
+
next_attempt_at: new Date(Date.now() + retryDelayMs).toISOString()
|
|
5639
|
+
}
|
|
5640
|
+
};
|
|
5641
|
+
saveState(options.statePath, retryState);
|
|
5642
|
+
appendEvent(logPath, {
|
|
5643
|
+
ts: new Date().toISOString(),
|
|
5644
|
+
conversation_id: conversation.conversation_id,
|
|
5645
|
+
event: "callback_retry_monitor_launched",
|
|
5646
|
+
message_id: message.id,
|
|
5647
|
+
pid: retryMonitor.pid ?? null,
|
|
5648
|
+
next_attempt_at: retryState.callback_delivery.next_attempt_at
|
|
5649
|
+
});
|
|
5650
|
+
}
|
|
5651
|
+
}
|
|
5652
|
+
throw error;
|
|
5653
|
+
}
|
|
5654
|
+
}
|
|
5655
|
+
function deliverCallbackToOpenClaw({ options, statePath, logPath, conversation, message }) {
|
|
4853
5656
|
if (options.gatewayMethod) {
|
|
4854
5657
|
const delivery = deliverToGatewayMethod({
|
|
4855
5658
|
method: options.gatewayMethod,
|
|
@@ -4857,30 +5660,19 @@ function runLockedCallback(options) {
|
|
|
4857
5660
|
gatewayUrl: options.gatewayUrl,
|
|
4858
5661
|
token: options.token,
|
|
4859
5662
|
sessionKey: options.gatewaySession ?? options.openclawSession ?? conversation.openclaw_session,
|
|
4860
|
-
statePath
|
|
5663
|
+
statePath,
|
|
4861
5664
|
logPath,
|
|
4862
|
-
conversation
|
|
5665
|
+
conversation,
|
|
4863
5666
|
message
|
|
4864
5667
|
});
|
|
4865
|
-
|
|
4866
|
-
|
|
4867
|
-
|
|
5668
|
+
recordCallbackProcessDelivery({
|
|
5669
|
+
logPath,
|
|
5670
|
+
conversation,
|
|
5671
|
+
message,
|
|
4868
5672
|
event: "callback_gateway_method_delivery",
|
|
4869
|
-
|
|
4870
|
-
|
|
4871
|
-
|
|
4872
|
-
method: options.gatewayMethod,
|
|
4873
|
-
status: delivery.status,
|
|
4874
|
-
stdout: delivery.stdout,
|
|
4875
|
-
stderr: delivery.stderr
|
|
4876
|
-
});
|
|
4877
|
-
runtimeLog("info", "callback_gateway_method_delivery", {
|
|
4878
|
-
conversation_id: conversation.conversation_id,
|
|
4879
|
-
method: options.gatewayMethod,
|
|
4880
|
-
status: delivery.status,
|
|
4881
|
-
failure_kind: classifyProcessFailure(delivery),
|
|
4882
|
-
stdout: textSummary(delivery.stdout),
|
|
4883
|
-
stderr: textSummary(delivery.stderr)
|
|
5673
|
+
runtimeEvent: "callback_gateway_method_delivery",
|
|
5674
|
+
delivery,
|
|
5675
|
+
detail: { method: options.gatewayMethod }
|
|
4884
5676
|
});
|
|
4885
5677
|
if (delivery.status !== 0) {
|
|
4886
5678
|
throw new Error(delivery.stderr || delivery.stdout || `gateway method delivery failed with status ${delivery.status}`);
|
|
@@ -4888,84 +5680,47 @@ function runLockedCallback(options) {
|
|
|
4888
5680
|
const gatewayPayload = parseOptionalJson(delivery.stdout);
|
|
4889
5681
|
const chatSendParams = isRecord(gatewayPayload?.chat_send) ? gatewayPayload.chat_send : undefined;
|
|
4890
5682
|
const sessionSendParams = isRecord(gatewayPayload?.session_send) ? gatewayPayload.session_send : undefined;
|
|
4891
|
-
let chatSendDelivery;
|
|
4892
|
-
let sessionSendDelivery;
|
|
4893
5683
|
if (chatSendParams) {
|
|
4894
|
-
chatSendDelivery = deliverToChatSend({
|
|
5684
|
+
const chatSendDelivery = deliverToChatSend({
|
|
4895
5685
|
openclawBin: options.openclawBin,
|
|
4896
5686
|
gatewayUrl: options.gatewayUrl,
|
|
4897
5687
|
token: options.token,
|
|
4898
5688
|
params: chatSendParams
|
|
4899
5689
|
});
|
|
4900
|
-
|
|
4901
|
-
|
|
4902
|
-
|
|
5690
|
+
recordCallbackProcessDelivery({
|
|
5691
|
+
logPath,
|
|
5692
|
+
conversation,
|
|
5693
|
+
message,
|
|
4903
5694
|
event: "callback_chat_send_delivery",
|
|
4904
|
-
|
|
4905
|
-
|
|
4906
|
-
round: message.round,
|
|
4907
|
-
status: chatSendDelivery.status,
|
|
4908
|
-
stdout: chatSendDelivery.stdout,
|
|
4909
|
-
stderr: chatSendDelivery.stderr
|
|
4910
|
-
});
|
|
4911
|
-
runtimeLog("info", "callback_chat_send_delivery", {
|
|
4912
|
-
conversation_id: conversation.conversation_id,
|
|
4913
|
-
status: chatSendDelivery.status,
|
|
4914
|
-
failure_kind: classifyProcessFailure(chatSendDelivery),
|
|
4915
|
-
stdout: textSummary(chatSendDelivery.stdout),
|
|
4916
|
-
stderr: textSummary(chatSendDelivery.stderr)
|
|
5695
|
+
runtimeEvent: "callback_chat_send_delivery",
|
|
5696
|
+
delivery: chatSendDelivery
|
|
4917
5697
|
});
|
|
4918
5698
|
if (chatSendDelivery.status !== 0) {
|
|
4919
5699
|
throw new Error(chatSendDelivery.stderr || chatSendDelivery.stdout || `chat callback delivery failed with status ${chatSendDelivery.status}`);
|
|
4920
5700
|
}
|
|
5701
|
+
return "gateway_method+chat_send";
|
|
4921
5702
|
}
|
|
4922
|
-
|
|
4923
|
-
sessionSendDelivery = deliverToSessionSend({
|
|
5703
|
+
if (sessionSendParams) {
|
|
5704
|
+
const sessionSendDelivery = deliverToSessionSend({
|
|
4924
5705
|
openclawBin: options.openclawBin,
|
|
4925
5706
|
gatewayUrl: options.gatewayUrl,
|
|
4926
5707
|
token: options.token,
|
|
4927
5708
|
params: sessionSendParams
|
|
4928
5709
|
});
|
|
4929
|
-
|
|
4930
|
-
|
|
4931
|
-
|
|
5710
|
+
recordCallbackProcessDelivery({
|
|
5711
|
+
logPath,
|
|
5712
|
+
conversation,
|
|
5713
|
+
message,
|
|
4932
5714
|
event: "callback_session_send_delivery",
|
|
4933
|
-
|
|
4934
|
-
|
|
4935
|
-
round: message.round,
|
|
4936
|
-
status: sessionSendDelivery.status,
|
|
4937
|
-
stdout: sessionSendDelivery.stdout,
|
|
4938
|
-
stderr: sessionSendDelivery.stderr
|
|
4939
|
-
});
|
|
4940
|
-
runtimeLog("info", "callback_session_send_delivery", {
|
|
4941
|
-
conversation_id: conversation.conversation_id,
|
|
4942
|
-
status: sessionSendDelivery.status,
|
|
4943
|
-
failure_kind: classifyProcessFailure(sessionSendDelivery),
|
|
4944
|
-
stdout: textSummary(sessionSendDelivery.stdout),
|
|
4945
|
-
stderr: textSummary(sessionSendDelivery.stderr)
|
|
5715
|
+
runtimeEvent: "callback_session_send_delivery",
|
|
5716
|
+
delivery: sessionSendDelivery
|
|
4946
5717
|
});
|
|
4947
5718
|
if (sessionSendDelivery.status !== 0) {
|
|
4948
5719
|
throw new Error(sessionSendDelivery.stderr || sessionSendDelivery.stdout || `session callback delivery failed with status ${sessionSendDelivery.status}`);
|
|
4949
5720
|
}
|
|
5721
|
+
return "gateway_method+sessions_send";
|
|
4950
5722
|
}
|
|
4951
|
-
|
|
4952
|
-
...result,
|
|
4953
|
-
delivered: true,
|
|
4954
|
-
delivery: chatSendDelivery
|
|
4955
|
-
? "gateway_method+chat_send"
|
|
4956
|
-
: sessionSendDelivery
|
|
4957
|
-
? "gateway_method+sessions_send"
|
|
4958
|
-
: "gateway_method"
|
|
4959
|
-
});
|
|
4960
|
-
return;
|
|
4961
|
-
}
|
|
4962
|
-
if (options.recordOnly) {
|
|
4963
|
-
runtimeLog("info", "callback_recorded_only", {
|
|
4964
|
-
conversation_id: conversation.conversation_id,
|
|
4965
|
-
status: nextConversation.status
|
|
4966
|
-
});
|
|
4967
|
-
printJson(result);
|
|
4968
|
-
return;
|
|
5723
|
+
return "gateway_method";
|
|
4969
5724
|
}
|
|
4970
5725
|
const gatewayUrl = options.gatewayUrl ?? conversation.gateway_url;
|
|
4971
5726
|
const token = options.token ?? conversation.gateway_token;
|
|
@@ -4980,37 +5735,48 @@ function runLockedCallback(options) {
|
|
|
4980
5735
|
throw new Error("--openclaw-session is required unless state has openclaw_session");
|
|
4981
5736
|
}
|
|
4982
5737
|
const delivery = deliverToOpenClaw({ gatewayUrl, token, openclawSession, message });
|
|
5738
|
+
recordCallbackProcessDelivery({
|
|
5739
|
+
logPath,
|
|
5740
|
+
conversation,
|
|
5741
|
+
message,
|
|
5742
|
+
event: "callback_delivery",
|
|
5743
|
+
runtimeEvent: "callback_delivery",
|
|
5744
|
+
delivery
|
|
5745
|
+
});
|
|
5746
|
+
if (delivery.status !== 0) {
|
|
5747
|
+
throw new Error(delivery.stderr || delivery.stdout || `callback delivery failed with status ${delivery.status}`);
|
|
5748
|
+
}
|
|
5749
|
+
return "acpx";
|
|
5750
|
+
}
|
|
5751
|
+
function recordCallbackProcessDelivery({ logPath, conversation, message, event, runtimeEvent, delivery, detail = {} }) {
|
|
4983
5752
|
appendEvent(logPath, {
|
|
4984
5753
|
ts: new Date().toISOString(),
|
|
4985
5754
|
conversation_id: conversation.conversation_id,
|
|
4986
|
-
event
|
|
5755
|
+
event,
|
|
4987
5756
|
from: message.from,
|
|
4988
5757
|
to: "openclaw",
|
|
4989
5758
|
round: message.round,
|
|
5759
|
+
...detail,
|
|
4990
5760
|
status: delivery.status,
|
|
4991
5761
|
stdout: delivery.stdout,
|
|
4992
5762
|
stderr: delivery.stderr
|
|
4993
5763
|
});
|
|
4994
|
-
runtimeLog("info",
|
|
5764
|
+
runtimeLog("info", runtimeEvent, {
|
|
4995
5765
|
conversation_id: conversation.conversation_id,
|
|
5766
|
+
...detail,
|
|
4996
5767
|
status: delivery.status,
|
|
4997
5768
|
failure_kind: classifyProcessFailure(delivery),
|
|
4998
5769
|
stdout: textSummary(delivery.stdout),
|
|
4999
5770
|
stderr: textSummary(delivery.stderr)
|
|
5000
5771
|
});
|
|
5001
|
-
if (delivery.status !== 0) {
|
|
5002
|
-
throw new Error(delivery.stderr || delivery.stdout || `callback delivery failed with status ${delivery.status}`);
|
|
5003
|
-
}
|
|
5004
|
-
printJson({
|
|
5005
|
-
...result,
|
|
5006
|
-
delivered: true
|
|
5007
|
-
});
|
|
5008
5772
|
}
|
|
5009
5773
|
function acquireFileLock(lockPath, { timeoutMs = 5000, retryMs = 50 } = {}) {
|
|
5010
5774
|
const started = Date.now();
|
|
5011
5775
|
while (true) {
|
|
5012
5776
|
try {
|
|
5013
5777
|
const fd = fs.openSync(lockPath, "wx");
|
|
5778
|
+
fs.writeFileSync(fd, `${process.pid}\n`, "utf8");
|
|
5779
|
+
fs.fsyncSync(fd);
|
|
5014
5780
|
fs.closeSync(fd);
|
|
5015
5781
|
return () => {
|
|
5016
5782
|
fs.rmSync(lockPath, { force: true });
|
|
@@ -5020,6 +5786,10 @@ function acquireFileLock(lockPath, { timeoutMs = 5000, retryMs = 50 } = {}) {
|
|
|
5020
5786
|
if (error.code !== "EEXIST") {
|
|
5021
5787
|
throw error;
|
|
5022
5788
|
}
|
|
5789
|
+
if (staleFileLock(lockPath)) {
|
|
5790
|
+
fs.rmSync(lockPath, { force: true });
|
|
5791
|
+
continue;
|
|
5792
|
+
}
|
|
5023
5793
|
if (Date.now() - started >= timeoutMs) {
|
|
5024
5794
|
throw new Error(`timed out waiting for file lock: ${lockPath}`);
|
|
5025
5795
|
}
|
|
@@ -5027,6 +5797,26 @@ function acquireFileLock(lockPath, { timeoutMs = 5000, retryMs = 50 } = {}) {
|
|
|
5027
5797
|
}
|
|
5028
5798
|
}
|
|
5029
5799
|
}
|
|
5800
|
+
function staleFileLock(lockPath) {
|
|
5801
|
+
try {
|
|
5802
|
+
const stat = fs.statSync(lockPath);
|
|
5803
|
+
const ownerText = fs.readFileSync(lockPath, "utf8").trim();
|
|
5804
|
+
const ownerPid = Number(ownerText);
|
|
5805
|
+
if (Number.isSafeInteger(ownerPid) && ownerPid > 1) {
|
|
5806
|
+
try {
|
|
5807
|
+
process.kill(ownerPid, 0);
|
|
5808
|
+
return false;
|
|
5809
|
+
}
|
|
5810
|
+
catch (error) {
|
|
5811
|
+
return isRecord(error) && error.code === "ESRCH";
|
|
5812
|
+
}
|
|
5813
|
+
}
|
|
5814
|
+
return Date.now() - stat.mtimeMs > 30_000;
|
|
5815
|
+
}
|
|
5816
|
+
catch (error) {
|
|
5817
|
+
return isRecord(error) && error.code === "ENOENT";
|
|
5818
|
+
}
|
|
5819
|
+
}
|
|
5030
5820
|
function sleepSync(ms) {
|
|
5031
5821
|
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
5032
5822
|
}
|
|
@@ -5589,6 +6379,7 @@ function markConversationStalled({ statePath, logPath, reason, detail = {} }) {
|
|
|
5589
6379
|
updated_at: now
|
|
5590
6380
|
};
|
|
5591
6381
|
saveState(statePath, stalledConversation);
|
|
6382
|
+
releaseClaudeHookLease(stalledConversation);
|
|
5592
6383
|
appendEvent(logPath, {
|
|
5593
6384
|
ts: now,
|
|
5594
6385
|
conversation_id: conversation.conversation_id,
|
|
@@ -6297,9 +7088,11 @@ function usage() {
|
|
|
6297
7088
|
agent-knock-knock approve --conversation <id>
|
|
6298
7089
|
agent-knock-knock cancel --conversation <id> [--all-proxy <url>]
|
|
6299
7090
|
agent-knock-knock renew --conversation <id> [--minutes <inactivity-minutes>]
|
|
7091
|
+
agent-knock-knock retry-callback --conversation <id> [--store-dir <dir>]
|
|
6300
7092
|
agent-knock-knock recover --conversation <id> [--session <name>] [--all-proxy <url>]
|
|
6301
7093
|
agent-knock-knock close --conversation <id> [--reason <text>]
|
|
6302
7094
|
agent-knock-knock install-openclaw [--openclaw-bin <path>] [--skill-path <path>] [--skill-only] [--no-restart]
|
|
7095
|
+
agent-knock-knock install-claude-hooks [--settings-path <path>] [--executable-path <path>] [--dry-run]
|
|
6303
7096
|
agent-knock-knock doctor
|
|
6304
7097
|
agent-knock-knock agent takeover --agent codex --session-id <id> --strategy terminate_then_resume|terminal_control|fork [--create-conversation]
|
|
6305
7098
|
agent-knock-knock callback --state <file> --message-json <json> [--record-only]
|