@scotthuang/agent-knock-knock 0.6.1 → 0.7.0
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 +23 -0
- package/README.md +17 -5
- package/dist/src/cli.js +592 -408
- package/dist/src/cli.js.map +1 -1
- package/dist/src/codex-terminal-agent-adapter.js +40 -15
- package/dist/src/codex-terminal-agent-adapter.js.map +1 -1
- package/dist/src/openclaw-plugin-helpers.js +2 -1
- package/dist/src/openclaw-plugin-helpers.js.map +1 -1
- package/dist/src/openclaw-plugin.d.ts +1 -0
- package/dist/src/openclaw-plugin.js +167 -157
- package/dist/src/openclaw-plugin.js.map +1 -1
- package/dist/src/store.d.ts +50 -0
- package/dist/src/store.js +414 -42
- package/dist/src/store.js.map +1 -1
- package/openclaw.plugin.json +2 -6
- package/package.json +1 -1
- package/templates/openclaw-skills/agent-knock-knock/SKILL.md +1 -1
package/dist/src/cli.js
CHANGED
|
@@ -14,7 +14,7 @@ import { applyMessageToConversation, budgetAction, createConversation, createMes
|
|
|
14
14
|
import { EXECUTOR_KINDS, executorDefinitionForKind, isExecutorKind } from "./executors.js";
|
|
15
15
|
import { redactString, writeRuntimeLog } from "./runtime-log.js";
|
|
16
16
|
import { formatTranscript, readNdjsonLog } from "./transcript.js";
|
|
17
|
-
import { appendEvent, defaultStoreDir, ensureDir, listConversations, logPathForStatePath, loadConversationById, loadState, messageEvent, pathsForConversation, pathsForConversationDir, saveState, statePathForConversationId } from "./store.js";
|
|
17
|
+
import { appendEvent, assertStoreWriterCompatible, defaultStoreDir, ensureDir, ensureStoreWritable, inspectStoreCompatibility, listConversations, logPathForStatePath, loadConversationById, loadState, messageEvent, pathsForConversation, pathsForConversationDir, saveState, statePathForConversationId, withStoreWriterLeaseAsync } from "./store.js";
|
|
18
18
|
import { StaticTerminalControlProvider, TmuxTerminalControlProvider, terminalPaneContainsProcess } from "./terminal-control-provider.js";
|
|
19
19
|
import { parseTerminalConversationId } from "./terminal-agent-adapter.js";
|
|
20
20
|
import { createProductionTerminalAgentRegistry } from "./terminal-agent-registry.js";
|
|
@@ -77,6 +77,18 @@ const SESSION_SELECTOR_COMMANDS = new Set([
|
|
|
77
77
|
"retry-callback",
|
|
78
78
|
"close"
|
|
79
79
|
]);
|
|
80
|
+
const STORE_MUTATION_COMMANDS = new Set([
|
|
81
|
+
"delegate",
|
|
82
|
+
"send",
|
|
83
|
+
"approve",
|
|
84
|
+
"cancel",
|
|
85
|
+
"renew",
|
|
86
|
+
"reconcile-monitors",
|
|
87
|
+
"close",
|
|
88
|
+
"callback",
|
|
89
|
+
"retry-callback",
|
|
90
|
+
"monitor"
|
|
91
|
+
]);
|
|
80
92
|
class InlineCodexSessionAdapter {
|
|
81
93
|
threads;
|
|
82
94
|
processes;
|
|
@@ -132,6 +144,7 @@ catch (error) {
|
|
|
132
144
|
}
|
|
133
145
|
async function runCommand(commandName, options) {
|
|
134
146
|
await resolveConversationSelectorOption(commandName, options);
|
|
147
|
+
preflightStoreWriter(commandName, options);
|
|
135
148
|
if (commandName === "help" || commandName === "--help" || commandName === "-h") {
|
|
136
149
|
usage();
|
|
137
150
|
}
|
|
@@ -188,6 +201,16 @@ async function runCommand(commandName, options) {
|
|
|
188
201
|
process.exitCode = commandName ? 1 : 0;
|
|
189
202
|
}
|
|
190
203
|
}
|
|
204
|
+
function preflightStoreWriter(commandName, options) {
|
|
205
|
+
if (!STORE_MUTATION_COMMANDS.has(String(commandName ?? ""))) {
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
const statePath = stringValue(options.state);
|
|
209
|
+
const storeDir = statePath
|
|
210
|
+
? pathsForConversationDir(path.dirname(expandHome(statePath))).storeDir
|
|
211
|
+
: storeDirFromOptions(options);
|
|
212
|
+
assertStoreWriterCompatible(storeDir);
|
|
213
|
+
}
|
|
191
214
|
function runInstallOpenClaw(options) {
|
|
192
215
|
const root = packageRootDir();
|
|
193
216
|
const skillOnly = options.skillOnly === true;
|
|
@@ -1327,7 +1350,12 @@ function environmentWithoutGatewayTokens() {
|
|
|
1327
1350
|
}
|
|
1328
1351
|
async function runList(options) {
|
|
1329
1352
|
const storeDir = expandHome(options.storeDir ?? options.logDir ?? defaultStoreDir(process.cwd()));
|
|
1330
|
-
const
|
|
1353
|
+
const reconciliation = options.reconcile === true
|
|
1354
|
+
? await reconcileStoreForList(storeDir, options)
|
|
1355
|
+
: {
|
|
1356
|
+
status: "disabled",
|
|
1357
|
+
reason: "standalone list is read-only unless --reconcile is supplied"
|
|
1358
|
+
};
|
|
1331
1359
|
const includeAll = Boolean(options.all);
|
|
1332
1360
|
const agentFilter = options.agent ? resolveExecutor({ kind: options.agent }).kind : undefined;
|
|
1333
1361
|
const statusFilter = options.status;
|
|
@@ -1361,7 +1389,8 @@ async function runList(options) {
|
|
|
1361
1389
|
});
|
|
1362
1390
|
printJson({
|
|
1363
1391
|
store_dir: storeDir,
|
|
1364
|
-
|
|
1392
|
+
store: inspectStoreCompatibility(storeDir),
|
|
1393
|
+
reconciliation,
|
|
1365
1394
|
action_contracts: listActionContracts(),
|
|
1366
1395
|
delegated,
|
|
1367
1396
|
terminal_controlled: terminalControlled,
|
|
@@ -1379,8 +1408,40 @@ async function runList(options) {
|
|
|
1379
1408
|
include_all: includeAll,
|
|
1380
1409
|
agent_filter: agentFilter,
|
|
1381
1410
|
status_filter: statusFilter,
|
|
1382
|
-
|
|
1411
|
+
reconciliation
|
|
1412
|
+
});
|
|
1413
|
+
}
|
|
1414
|
+
async function reconcileStoreForList(storeDir, options) {
|
|
1415
|
+
try {
|
|
1416
|
+
ensureStoreWritable(storeDir);
|
|
1417
|
+
}
|
|
1418
|
+
catch (error) {
|
|
1419
|
+
if (isRecord(error) && error.code === "AKK_STORE_INCOMPATIBLE") {
|
|
1420
|
+
return {
|
|
1421
|
+
status: "skipped",
|
|
1422
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
1423
|
+
store: inspectStoreCompatibility(storeDir)
|
|
1424
|
+
};
|
|
1425
|
+
}
|
|
1426
|
+
throw error;
|
|
1427
|
+
}
|
|
1428
|
+
const idle = reconcileIdleConversations(storeDir, options);
|
|
1429
|
+
const monitors = await reconcileMonitors(options, {
|
|
1430
|
+
includeCallbackRecovery: false,
|
|
1431
|
+
reason: "list_reconciliation",
|
|
1432
|
+
conversationId: undefined
|
|
1383
1433
|
});
|
|
1434
|
+
return {
|
|
1435
|
+
status: "completed",
|
|
1436
|
+
checked: Math.max(idle.checked, monitors.checked),
|
|
1437
|
+
changed: idle.closed + monitors.launched,
|
|
1438
|
+
closed: idle.closed,
|
|
1439
|
+
monitors_launched: monitors.launched,
|
|
1440
|
+
monitors_already_running: monitors.already_running,
|
|
1441
|
+
skipped: idle.skipped + monitors.skipped,
|
|
1442
|
+
errors: monitors.errors,
|
|
1443
|
+
idle_timeout_minutes: idle.idle_timeout_minutes
|
|
1444
|
+
};
|
|
1384
1445
|
}
|
|
1385
1446
|
async function buildTerminalListGroup({ options, agentFilter, statusFilter }) {
|
|
1386
1447
|
const empty = {
|
|
@@ -1640,13 +1701,34 @@ function managedListApprovalState(conversation) {
|
|
|
1640
1701
|
}
|
|
1641
1702
|
function listActionContracts() {
|
|
1642
1703
|
return {
|
|
1643
|
-
version:
|
|
1704
|
+
version: 2,
|
|
1644
1705
|
instructions: [
|
|
1645
1706
|
"Use only actions present in delegated[].available_actions or terminal_controlled[].available_actions.",
|
|
1707
|
+
"Never use commands for routing or tool calls. It is a deprecated, non-authoritative compatibility field with mixed legacy semantics.",
|
|
1646
1708
|
"Start with the action's prefilled arguments, supply every missing_required field, and consult the top-level action's optional fields only when needed.",
|
|
1647
1709
|
"Authoritative full IDs are prefilled; short_ref is for display and human input.",
|
|
1648
1710
|
"Availability is a snapshot. AKK revalidates process, tmux pane, workspace, activity, approval, and recovery state before side effects."
|
|
1649
1711
|
],
|
|
1712
|
+
field_semantics: {
|
|
1713
|
+
status: {
|
|
1714
|
+
delegated: "task_lifecycle",
|
|
1715
|
+
terminal_controlled: "process_liveness",
|
|
1716
|
+
authoritative_for_tool_calls: false
|
|
1717
|
+
},
|
|
1718
|
+
activity_state: {
|
|
1719
|
+
terminal_controlled: "screen_activity_classification",
|
|
1720
|
+
authoritative_for_tool_calls: false
|
|
1721
|
+
},
|
|
1722
|
+
commands: {
|
|
1723
|
+
meaning: "legacy_compatibility_flags_with_mixed_semantics",
|
|
1724
|
+
deprecated: true,
|
|
1725
|
+
authoritative_for_tool_calls: false
|
|
1726
|
+
},
|
|
1727
|
+
available_actions: {
|
|
1728
|
+
meaning: "currently_safe_actions",
|
|
1729
|
+
authoritative_for_tool_calls: true
|
|
1730
|
+
}
|
|
1731
|
+
},
|
|
1650
1732
|
actions: {
|
|
1651
1733
|
send: {
|
|
1652
1734
|
tool: "agent_knock_knock_send",
|
|
@@ -1841,7 +1923,6 @@ function isSessionSelectorSyntax(value) {
|
|
|
1841
1923
|
}
|
|
1842
1924
|
async function sessionSelectorCandidates(commandName, options) {
|
|
1843
1925
|
const storeDir = storeDirFromOptions(options);
|
|
1844
|
-
cleanupIdleConversations(storeDir, options);
|
|
1845
1926
|
const storedConversations = listConversations(storeDir);
|
|
1846
1927
|
const workspaceConversations = storedConversations
|
|
1847
1928
|
.filter((conversation) => matchesConfiguredWorkspace(options.workspace, conversation.workspace));
|
|
@@ -1935,7 +2016,23 @@ async function resolveTerminalConversationFromOptions(options) {
|
|
|
1935
2016
|
return createTerminalAgentBridge(options).resolveConversationId(stringValue(options.conversation ?? options.conversationId));
|
|
1936
2017
|
}
|
|
1937
2018
|
async function runStatus(options) {
|
|
1938
|
-
|
|
2019
|
+
const explicitStatePath = options.state
|
|
2020
|
+
? expandHome(String(options.state))
|
|
2021
|
+
: undefined;
|
|
2022
|
+
const storeDir = explicitStatePath
|
|
2023
|
+
? pathsForConversationDir(path.dirname(explicitStatePath)).storeDir
|
|
2024
|
+
: storeDirFromOptions(options);
|
|
2025
|
+
const reconciliationConversationId = stringValue(options.conversation ?? options.conversationId) ??
|
|
2026
|
+
(explicitStatePath
|
|
2027
|
+
? path.basename(pathsForConversationDir(path.dirname(explicitStatePath))
|
|
2028
|
+
.conversationDir)
|
|
2029
|
+
: undefined);
|
|
2030
|
+
const reconciliation = options.reconcile === true
|
|
2031
|
+
? await reconcileStoreForStatus(storeDir, options, reconciliationConversationId)
|
|
2032
|
+
: {
|
|
2033
|
+
status: "disabled",
|
|
2034
|
+
reason: "standalone status is read-only unless --reconcile is supplied"
|
|
2035
|
+
};
|
|
1939
2036
|
const terminalConversation = await resolveTerminalConversationFromOptions(options);
|
|
1940
2037
|
if (terminalConversation) {
|
|
1941
2038
|
const terminalStatus = await terminalStatusForControl(terminalConversation.agent, terminalConversation.terminalControl, options, {
|
|
@@ -1949,6 +2046,8 @@ async function runStatus(options) {
|
|
|
1949
2046
|
conversation_id: terminalConversation.conversationId,
|
|
1950
2047
|
source: "terminal_control",
|
|
1951
2048
|
agent: terminalConversation.agent,
|
|
2049
|
+
store: inspectStoreCompatibility(storeDir),
|
|
2050
|
+
reconciliation,
|
|
1952
2051
|
...context,
|
|
1953
2052
|
terminal_control: terminalConversation.terminalControl,
|
|
1954
2053
|
terminal_status: terminalStatus,
|
|
@@ -1963,13 +2062,12 @@ async function runStatus(options) {
|
|
|
1963
2062
|
}
|
|
1964
2063
|
const loaded = loadConversationFromOptions(options);
|
|
1965
2064
|
const { statePath, logPath } = loaded;
|
|
1966
|
-
const conversation =
|
|
1967
|
-
...loaded,
|
|
1968
|
-
options
|
|
1969
|
-
});
|
|
2065
|
+
const conversation = loaded.conversation;
|
|
1970
2066
|
const events = readExistingEvents(logPath);
|
|
1971
2067
|
const result = {
|
|
1972
2068
|
conversation,
|
|
2069
|
+
store: inspectStoreCompatibility(storeDir),
|
|
2070
|
+
reconciliation,
|
|
1973
2071
|
summary: summarizeConversation(conversation),
|
|
1974
2072
|
confidence: "high",
|
|
1975
2073
|
about: managedConversationAbout(conversation, events),
|
|
@@ -2006,6 +2104,38 @@ async function runStatus(options) {
|
|
|
2006
2104
|
trace: Boolean(options.trace)
|
|
2007
2105
|
});
|
|
2008
2106
|
}
|
|
2107
|
+
async function reconcileStoreForStatus(storeDir, options, conversationId) {
|
|
2108
|
+
try {
|
|
2109
|
+
ensureStoreWritable(storeDir);
|
|
2110
|
+
}
|
|
2111
|
+
catch (error) {
|
|
2112
|
+
if (isRecord(error) && error.code === "AKK_STORE_INCOMPATIBLE") {
|
|
2113
|
+
return {
|
|
2114
|
+
status: "skipped",
|
|
2115
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
2116
|
+
store: inspectStoreCompatibility(storeDir)
|
|
2117
|
+
};
|
|
2118
|
+
}
|
|
2119
|
+
throw error;
|
|
2120
|
+
}
|
|
2121
|
+
const idle = reconcileIdleConversations(storeDir, options, new Date(), conversationId);
|
|
2122
|
+
const monitors = await reconcileMonitors(options, {
|
|
2123
|
+
includeCallbackRecovery: false,
|
|
2124
|
+
reason: "status_reconciliation",
|
|
2125
|
+
conversationId
|
|
2126
|
+
});
|
|
2127
|
+
return {
|
|
2128
|
+
status: "completed",
|
|
2129
|
+
checked: Math.max(idle.checked, monitors.checked),
|
|
2130
|
+
changed: idle.closed + monitors.launched,
|
|
2131
|
+
closed: idle.closed,
|
|
2132
|
+
monitors_launched: monitors.launched,
|
|
2133
|
+
monitors_already_running: monitors.already_running,
|
|
2134
|
+
skipped: idle.skipped + monitors.skipped,
|
|
2135
|
+
errors: monitors.errors,
|
|
2136
|
+
idle_timeout_minutes: idle.idle_timeout_minutes
|
|
2137
|
+
};
|
|
2138
|
+
}
|
|
2009
2139
|
async function terminalStatusContext(terminalConversation, terminalStatus, options) {
|
|
2010
2140
|
if (terminalConversation.agent === "codex") {
|
|
2011
2141
|
try {
|
|
@@ -2476,7 +2606,6 @@ async function runSend(options) {
|
|
|
2476
2606
|
if (options.agentHardTimeoutMinutes !== undefined) {
|
|
2477
2607
|
positiveMinutes(options.agentHardTimeoutMinutes, "--agent-hard-timeout-minutes");
|
|
2478
2608
|
}
|
|
2479
|
-
cleanupIdleConversations(storeDirFromOptions(options), options);
|
|
2480
2609
|
const terminalConversation = await resolveTerminalConversationFromOptions(options);
|
|
2481
2610
|
if (terminalConversation) {
|
|
2482
2611
|
if (!options.background) {
|
|
@@ -2493,6 +2622,7 @@ async function runSend(options) {
|
|
|
2493
2622
|
messageBody,
|
|
2494
2623
|
terminalControl: terminalConversation.terminalControl
|
|
2495
2624
|
});
|
|
2625
|
+
ensureStoreWritable(managed.conversation.store_dir);
|
|
2496
2626
|
ensureDir(path.dirname(managed.statePath));
|
|
2497
2627
|
releaseStateLock = acquireFileLock(`${managed.statePath}.lock`);
|
|
2498
2628
|
await runTerminalControlSend({
|
|
@@ -2578,7 +2708,6 @@ async function runSend(options) {
|
|
|
2578
2708
|
throw new Error(`conversation ${migratedConversation.conversation_id} is not attached to a live tmux terminal`);
|
|
2579
2709
|
}
|
|
2580
2710
|
async function runApprove(options) {
|
|
2581
|
-
cleanupIdleConversations(storeDirFromOptions(options), options);
|
|
2582
2711
|
const terminalConversation = await resolveTerminalConversationFromOptions(options);
|
|
2583
2712
|
if (terminalConversation) {
|
|
2584
2713
|
await runTerminalConversationApprove({
|
|
@@ -2752,6 +2881,7 @@ async function runApprove(options) {
|
|
|
2752
2881
|
}
|
|
2753
2882
|
};
|
|
2754
2883
|
let releaseStateLock;
|
|
2884
|
+
let approvalDispatchReserved = false;
|
|
2755
2885
|
const releaseApprovalStateLock = () => {
|
|
2756
2886
|
if (releaseStateLock) {
|
|
2757
2887
|
const release = releaseStateLock;
|
|
@@ -2759,323 +2889,330 @@ async function runApprove(options) {
|
|
|
2759
2889
|
release();
|
|
2760
2890
|
}
|
|
2761
2891
|
};
|
|
2892
|
+
releaseStateLock = acquireFileLock(`${statePath}.lock`);
|
|
2893
|
+
const writerStoreDir = pathsForConversationDir(path.dirname(statePath)).storeDir;
|
|
2762
2894
|
try {
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
return {
|
|
2797
|
-
approved: false,
|
|
2798
|
-
reason: "automatic approval requires an executor-side policy"
|
|
2799
|
-
};
|
|
2800
|
-
}
|
|
2801
|
-
const candidate = policyCandidateForInspection({
|
|
2802
|
-
agent,
|
|
2803
|
-
currentTerminalControl,
|
|
2804
|
-
inspection,
|
|
2805
|
-
fingerprint
|
|
2806
|
-
});
|
|
2807
|
-
executorPolicyDecision = evaluateApprovalPolicy({
|
|
2808
|
-
policy: autoApprovalPolicy,
|
|
2809
|
-
candidate
|
|
2810
|
-
});
|
|
2811
|
-
if (executorPolicyDecision.action !== "approve") {
|
|
2812
|
-
return {
|
|
2813
|
-
approved: false,
|
|
2814
|
-
reason: `executor-side auto-approval policy rejected the current request: ${executorPolicyDecision.reason}`
|
|
2815
|
-
};
|
|
2816
|
-
}
|
|
2817
|
-
if (policyRuleId && executorPolicyDecision.ruleId !== policyRuleId) {
|
|
2818
|
-
return {
|
|
2819
|
-
approved: false,
|
|
2820
|
-
reason: "executor-side auto-approval rule changed before execution"
|
|
2821
|
-
};
|
|
2822
|
-
}
|
|
2823
|
-
if (policyFingerprint &&
|
|
2824
|
-
executorPolicyDecision.policyFingerprint !== policyFingerprint) {
|
|
2825
|
-
return {
|
|
2826
|
-
approved: false,
|
|
2827
|
-
reason: "executor-side auto-approval policy changed before execution"
|
|
2828
|
-
};
|
|
2829
|
-
}
|
|
2830
|
-
return { approved: true };
|
|
2831
|
-
}
|
|
2832
|
-
: undefined,
|
|
2833
|
-
beforeKeyDispatch: claudeScreenApproval
|
|
2834
|
-
? ({ fingerprint, terminalControl: dispatchControl, inspection, keys }) => {
|
|
2835
|
-
if (autoApproved) {
|
|
2895
|
+
return await withStoreWriterLeaseAsync(writerStoreDir, async () => {
|
|
2896
|
+
let approval;
|
|
2897
|
+
let lockedConversation = conversation;
|
|
2898
|
+
const currentConversation = loadState(statePath);
|
|
2899
|
+
const currentTakeover = isRecord(currentConversation.native_session_takeover)
|
|
2900
|
+
? currentConversation.native_session_takeover
|
|
2901
|
+
: undefined;
|
|
2902
|
+
const currentControl = terminalControlFromTakeover(currentTakeover);
|
|
2903
|
+
const currentApproval = isRecord(currentTakeover?.terminal_bridge_approval)
|
|
2904
|
+
? currentTakeover.terminal_bridge_approval
|
|
2905
|
+
: undefined;
|
|
2906
|
+
if (currentConversation.status !== conversation.status ||
|
|
2907
|
+
currentTakeover?.terminal_bridge_message_id !== nativeTakeover?.terminal_bridge_message_id ||
|
|
2908
|
+
currentControl?.target !== terminalControl.target ||
|
|
2909
|
+
currentControl?.socketPath !== terminalControl.socketPath ||
|
|
2910
|
+
(claudeScreenApproval &&
|
|
2911
|
+
currentApproval?.fingerprint !== monitoredApproval?.fingerprint)) {
|
|
2912
|
+
throw new Error("approval state changed while waiting for terminal control; refresh status and retry");
|
|
2913
|
+
}
|
|
2914
|
+
assertManagedTerminalDispatchOwner({
|
|
2915
|
+
conversation: currentConversation,
|
|
2916
|
+
terminalControl: currentControl,
|
|
2917
|
+
action: "approve"
|
|
2918
|
+
});
|
|
2919
|
+
lockedConversation = currentConversation;
|
|
2920
|
+
approval = await createTerminalAgentBridge(options).approve(executor.kind, terminalControl, {
|
|
2921
|
+
expectedFingerprint,
|
|
2922
|
+
scrollbackLines: Number(options.scrollbackLines ?? 120),
|
|
2923
|
+
runtime: runtimeIdentity,
|
|
2924
|
+
managedRequest: terminalDurableRequestForConversation(currentConversation, terminalControl),
|
|
2925
|
+
requiredDecisionMode: autoApproved && executor.kind === "claude" ? "keys" : undefined,
|
|
2926
|
+
authorize: autoApproved
|
|
2927
|
+
? ({ agent, terminalControl: currentTerminalControl, inspection, fingerprint }) => {
|
|
2836
2928
|
if (!autoApprovalPolicy) {
|
|
2837
|
-
|
|
2929
|
+
return {
|
|
2930
|
+
approved: false,
|
|
2931
|
+
reason: "automatic approval requires an executor-side policy"
|
|
2932
|
+
};
|
|
2838
2933
|
}
|
|
2839
|
-
const
|
|
2934
|
+
const candidate = policyCandidateForInspection({
|
|
2935
|
+
agent,
|
|
2936
|
+
currentTerminalControl,
|
|
2937
|
+
inspection,
|
|
2938
|
+
fingerprint
|
|
2939
|
+
});
|
|
2940
|
+
executorPolicyDecision = evaluateApprovalPolicy({
|
|
2840
2941
|
policy: autoApprovalPolicy,
|
|
2841
|
-
candidate
|
|
2842
|
-
agent: executor.kind,
|
|
2843
|
-
currentTerminalControl: dispatchControl,
|
|
2844
|
-
inspection,
|
|
2845
|
-
fingerprint
|
|
2846
|
-
})
|
|
2942
|
+
candidate
|
|
2847
2943
|
});
|
|
2848
|
-
if (
|
|
2849
|
-
|
|
2944
|
+
if (executorPolicyDecision.action !== "approve") {
|
|
2945
|
+
return {
|
|
2946
|
+
approved: false,
|
|
2947
|
+
reason: `executor-side auto-approval policy rejected the current request: ${executorPolicyDecision.reason}`
|
|
2948
|
+
};
|
|
2850
2949
|
}
|
|
2851
|
-
if (executorPolicyDecision
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
throw new Error("executor-side auto-approval rule changed before dispatch");
|
|
2950
|
+
if (policyRuleId && executorPolicyDecision.ruleId !== policyRuleId) {
|
|
2951
|
+
return {
|
|
2952
|
+
approved: false,
|
|
2953
|
+
reason: "executor-side auto-approval rule changed before execution"
|
|
2954
|
+
};
|
|
2857
2955
|
}
|
|
2858
2956
|
if (policyFingerprint &&
|
|
2859
|
-
|
|
2860
|
-
|
|
2957
|
+
executorPolicyDecision.policyFingerprint !== policyFingerprint) {
|
|
2958
|
+
return {
|
|
2959
|
+
approved: false,
|
|
2960
|
+
reason: "executor-side auto-approval policy changed before execution"
|
|
2961
|
+
};
|
|
2861
2962
|
}
|
|
2862
|
-
|
|
2863
|
-
}
|
|
2864
|
-
if (releaseStateLock) {
|
|
2865
|
-
throw new Error("Claude approval dispatch was already reserved");
|
|
2963
|
+
return { approved: true };
|
|
2866
2964
|
}
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
const latestApproval = isRecord(latestTakeover?.terminal_bridge_approval)
|
|
2874
|
-
? latestTakeover.terminal_bridge_approval
|
|
2875
|
-
: undefined;
|
|
2876
|
-
const latestNotifiedAt = validTimestampMs(latestApproval?.notified_at);
|
|
2877
|
-
const latestApprovalState = isRecord(latestApproval?.approval_state)
|
|
2878
|
-
? latestApproval.approval_state
|
|
2879
|
-
: undefined;
|
|
2880
|
-
const latestPolicyEvidence = isRecord(latestApprovalState?.policy_evidence)
|
|
2881
|
-
? latestApprovalState.policy_evidence
|
|
2882
|
-
: undefined;
|
|
2883
|
-
const recapturedPolicyEvidence = inspection.approval.approvable
|
|
2884
|
-
? inspection.approval.policyEvidence
|
|
2885
|
-
: undefined;
|
|
2886
|
-
const latestDispatch = isRecord(latestTakeover?.terminal_bridge_approval_dispatch)
|
|
2887
|
-
? latestTakeover.terminal_bridge_approval_dispatch
|
|
2888
|
-
: undefined;
|
|
2889
|
-
if (!latestTakeover ||
|
|
2890
|
-
latestConversation.status !== "waiting_for_openclaw" ||
|
|
2891
|
-
latestTakeover.terminal_bridge_message_id !==
|
|
2892
|
-
nativeTakeover?.terminal_bridge_message_id ||
|
|
2893
|
-
latestApproval?.fingerprint !== fingerprint ||
|
|
2894
|
-
latestNotifiedAt === undefined ||
|
|
2895
|
-
Date.now() - latestNotifiedAt > CLAUDE_SCREEN_APPROVAL_TTL_MS ||
|
|
2896
|
-
expectedFingerprint !== fingerprint ||
|
|
2897
|
-
latestControl?.target !== dispatchControl.target ||
|
|
2898
|
-
latestControl?.socketPath !== dispatchControl.socketPath ||
|
|
2899
|
-
(autoApproved &&
|
|
2900
|
-
(latestPolicyEvidence?.source !== "claude_transcript" ||
|
|
2901
|
-
latestPolicyEvidence.evidence_fingerprint !==
|
|
2902
|
-
recapturedPolicyEvidence?.evidenceFingerprint))) {
|
|
2903
|
-
throw new Error("approval state changed before terminal dispatch; refresh status and retry");
|
|
2904
|
-
}
|
|
2905
|
-
if (latestDispatch?.state === "reserved" &&
|
|
2906
|
-
latestDispatch.terminal_bridge_message_id ===
|
|
2907
|
-
latestTakeover.terminal_bridge_message_id) {
|
|
2908
|
-
throw new Error("a previous Claude approval dispatch has an uncertain outcome; inspect and resolve the terminal manually");
|
|
2909
|
-
}
|
|
2910
|
-
const reservedAt = new Date().toISOString();
|
|
2911
|
-
const reservedConversation = {
|
|
2912
|
-
...latestConversation,
|
|
2913
|
-
native_session_takeover: {
|
|
2914
|
-
...latestTakeover,
|
|
2915
|
-
terminal_bridge_approval_dispatch: {
|
|
2916
|
-
state: "reserved",
|
|
2917
|
-
attempt_id: randomUUID(),
|
|
2918
|
-
fingerprint,
|
|
2919
|
-
keys,
|
|
2920
|
-
terminal_target: dispatchControl.target,
|
|
2921
|
-
terminal_bridge_message_id: latestTakeover.terminal_bridge_message_id,
|
|
2922
|
-
reserved_at: reservedAt
|
|
2965
|
+
: undefined,
|
|
2966
|
+
beforeKeyDispatch: claudeScreenApproval
|
|
2967
|
+
? ({ fingerprint, terminalControl: dispatchControl, inspection, keys }) => {
|
|
2968
|
+
if (autoApproved) {
|
|
2969
|
+
if (!autoApprovalPolicy) {
|
|
2970
|
+
throw new Error("automatic approval requires an executor-side policy before dispatch");
|
|
2923
2971
|
}
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2972
|
+
const freshPolicyDecision = evaluateApprovalPolicy({
|
|
2973
|
+
policy: autoApprovalPolicy,
|
|
2974
|
+
candidate: policyCandidateForInspection({
|
|
2975
|
+
agent: executor.kind,
|
|
2976
|
+
currentTerminalControl: dispatchControl,
|
|
2977
|
+
inspection,
|
|
2978
|
+
fingerprint
|
|
2979
|
+
})
|
|
2980
|
+
});
|
|
2981
|
+
if (freshPolicyDecision.action !== "approve") {
|
|
2982
|
+
throw new Error(`executor-side auto-approval policy rejected the recaptured request: ${freshPolicyDecision.reason}`);
|
|
2983
|
+
}
|
|
2984
|
+
if (executorPolicyDecision?.ruleId &&
|
|
2985
|
+
freshPolicyDecision.ruleId !== executorPolicyDecision.ruleId) {
|
|
2986
|
+
throw new Error("executor-side auto-approval rule changed after recapture");
|
|
2987
|
+
}
|
|
2988
|
+
if (policyRuleId && freshPolicyDecision.ruleId !== policyRuleId) {
|
|
2989
|
+
throw new Error("executor-side auto-approval rule changed before dispatch");
|
|
2990
|
+
}
|
|
2991
|
+
if (policyFingerprint &&
|
|
2992
|
+
freshPolicyDecision.policyFingerprint !== policyFingerprint) {
|
|
2993
|
+
throw new Error("executor-side auto-approval policy changed before dispatch");
|
|
2994
|
+
}
|
|
2995
|
+
executorPolicyDecision = freshPolicyDecision;
|
|
2996
|
+
}
|
|
2997
|
+
if (approvalDispatchReserved) {
|
|
2998
|
+
throw new Error("Claude approval dispatch was already reserved");
|
|
2999
|
+
}
|
|
3000
|
+
approvalDispatchReserved = true;
|
|
3001
|
+
if (!releaseStateLock) {
|
|
3002
|
+
throw new Error("approval state lock was released before terminal dispatch");
|
|
3003
|
+
}
|
|
3004
|
+
const latestConversation = loadState(statePath);
|
|
3005
|
+
const latestTakeover = isRecord(latestConversation.native_session_takeover)
|
|
3006
|
+
? latestConversation.native_session_takeover
|
|
3007
|
+
: undefined;
|
|
3008
|
+
const latestControl = terminalControlFromTakeover(latestTakeover);
|
|
3009
|
+
const latestApproval = isRecord(latestTakeover?.terminal_bridge_approval)
|
|
3010
|
+
? latestTakeover.terminal_bridge_approval
|
|
3011
|
+
: undefined;
|
|
3012
|
+
const latestNotifiedAt = validTimestampMs(latestApproval?.notified_at);
|
|
3013
|
+
const latestApprovalState = isRecord(latestApproval?.approval_state)
|
|
3014
|
+
? latestApproval.approval_state
|
|
3015
|
+
: undefined;
|
|
3016
|
+
const latestPolicyEvidence = isRecord(latestApprovalState?.policy_evidence)
|
|
3017
|
+
? latestApprovalState.policy_evidence
|
|
3018
|
+
: undefined;
|
|
3019
|
+
const recapturedPolicyEvidence = inspection.approval.approvable
|
|
3020
|
+
? inspection.approval.policyEvidence
|
|
3021
|
+
: undefined;
|
|
3022
|
+
const latestDispatch = isRecord(latestTakeover?.terminal_bridge_approval_dispatch)
|
|
3023
|
+
? latestTakeover.terminal_bridge_approval_dispatch
|
|
3024
|
+
: undefined;
|
|
3025
|
+
if (!latestTakeover ||
|
|
3026
|
+
latestConversation.status !== "waiting_for_openclaw" ||
|
|
3027
|
+
latestTakeover.terminal_bridge_message_id !==
|
|
3028
|
+
nativeTakeover?.terminal_bridge_message_id ||
|
|
3029
|
+
latestApproval?.fingerprint !== fingerprint ||
|
|
3030
|
+
latestNotifiedAt === undefined ||
|
|
3031
|
+
Date.now() - latestNotifiedAt > CLAUDE_SCREEN_APPROVAL_TTL_MS ||
|
|
3032
|
+
expectedFingerprint !== fingerprint ||
|
|
3033
|
+
latestControl?.target !== dispatchControl.target ||
|
|
3034
|
+
latestControl?.socketPath !== dispatchControl.socketPath ||
|
|
3035
|
+
(autoApproved &&
|
|
3036
|
+
(latestPolicyEvidence?.source !== "claude_transcript" ||
|
|
3037
|
+
latestPolicyEvidence.evidence_fingerprint !==
|
|
3038
|
+
recapturedPolicyEvidence?.evidenceFingerprint))) {
|
|
3039
|
+
throw new Error("approval state changed before terminal dispatch; refresh status and retry");
|
|
3040
|
+
}
|
|
3041
|
+
if (latestDispatch?.state === "reserved" &&
|
|
3042
|
+
latestDispatch.terminal_bridge_message_id ===
|
|
3043
|
+
latestTakeover.terminal_bridge_message_id) {
|
|
3044
|
+
throw new Error("a previous Claude approval dispatch has an uncertain outcome; inspect and resolve the terminal manually");
|
|
3045
|
+
}
|
|
3046
|
+
const reservedAt = new Date().toISOString();
|
|
3047
|
+
const reservedConversation = {
|
|
3048
|
+
...latestConversation,
|
|
3049
|
+
native_session_takeover: {
|
|
3050
|
+
...latestTakeover,
|
|
3051
|
+
terminal_bridge_approval_dispatch: {
|
|
3052
|
+
state: "reserved",
|
|
3053
|
+
attempt_id: randomUUID(),
|
|
3054
|
+
fingerprint,
|
|
3055
|
+
keys,
|
|
3056
|
+
terminal_target: dispatchControl.target,
|
|
3057
|
+
terminal_bridge_message_id: latestTakeover.terminal_bridge_message_id,
|
|
3058
|
+
reserved_at: reservedAt
|
|
3059
|
+
}
|
|
3060
|
+
},
|
|
3061
|
+
updated_at: reservedAt
|
|
3062
|
+
};
|
|
3063
|
+
saveState(statePath, reservedConversation);
|
|
3064
|
+
lockedConversation = reservedConversation;
|
|
3065
|
+
}
|
|
3066
|
+
: undefined
|
|
3067
|
+
});
|
|
3068
|
+
const actualFingerprint = approval.fingerprint;
|
|
3069
|
+
const effectivePolicyRuleId = executorPolicyDecision?.ruleId ?? policyRuleId;
|
|
3070
|
+
const effectivePolicyFingerprint = executorPolicyDecision?.policyFingerprint ?? policyFingerprint;
|
|
3071
|
+
if (!approval.approved) {
|
|
3072
|
+
releaseApprovalStateLock();
|
|
3073
|
+
releaseApprovalTerminalLock();
|
|
3074
|
+
if (autoApproved) {
|
|
3075
|
+
appendEvent(logPath, {
|
|
3076
|
+
ts: new Date().toISOString(),
|
|
3077
|
+
conversation_id: conversation.conversation_id,
|
|
3078
|
+
event: "terminal_auto_approval_decision",
|
|
3079
|
+
action: "rejected",
|
|
3080
|
+
reason: approval.reason,
|
|
3081
|
+
terminal_control: terminalControl,
|
|
3082
|
+
expected_fingerprint: expectedFingerprint,
|
|
3083
|
+
actual_fingerprint: actualFingerprint,
|
|
3084
|
+
policy_rule_id: effectivePolicyRuleId,
|
|
3085
|
+
policy_fingerprint: effectivePolicyFingerprint
|
|
3086
|
+
});
|
|
2929
3087
|
}
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
3088
|
+
printJson({
|
|
3089
|
+
conversation,
|
|
3090
|
+
approved: false,
|
|
3091
|
+
blocked: approval.blocked,
|
|
3092
|
+
reason: approval.reason,
|
|
3093
|
+
terminal_control: terminalControl,
|
|
3094
|
+
expected_approval_fingerprint: expectedFingerprint,
|
|
3095
|
+
actual_approval_fingerprint: actualFingerprint,
|
|
3096
|
+
screen_excerpt: approval.screenExcerpt
|
|
3097
|
+
});
|
|
3098
|
+
return;
|
|
3099
|
+
}
|
|
3100
|
+
appendEvent(logPath, {
|
|
3101
|
+
ts: new Date().toISOString(),
|
|
3102
|
+
conversation_id: conversation.conversation_id,
|
|
3103
|
+
event: "terminal_approval_send",
|
|
3104
|
+
terminal_control: terminalControl,
|
|
3105
|
+
key: approval.key,
|
|
3106
|
+
keys: approval.keys,
|
|
3107
|
+
label: approval.label,
|
|
3108
|
+
decision_mode: approval.decisionMode,
|
|
3109
|
+
request_id: approval.requestId,
|
|
3110
|
+
approval_fingerprint: actualFingerprint,
|
|
3111
|
+
auto_approved: autoApproved,
|
|
3112
|
+
policy_rule_id: effectivePolicyRuleId,
|
|
3113
|
+
policy_fingerprint: effectivePolicyFingerprint
|
|
3114
|
+
});
|
|
2938
3115
|
if (autoApproved) {
|
|
2939
3116
|
appendEvent(logPath, {
|
|
2940
3117
|
ts: new Date().toISOString(),
|
|
2941
3118
|
conversation_id: conversation.conversation_id,
|
|
2942
3119
|
event: "terminal_auto_approval_decision",
|
|
2943
|
-
action: "
|
|
2944
|
-
reason: approval.reason,
|
|
3120
|
+
action: "approved",
|
|
2945
3121
|
terminal_control: terminalControl,
|
|
2946
|
-
|
|
2947
|
-
actual_fingerprint: actualFingerprint,
|
|
3122
|
+
approval_fingerprint: actualFingerprint,
|
|
2948
3123
|
policy_rule_id: effectivePolicyRuleId,
|
|
2949
3124
|
policy_fingerprint: effectivePolicyFingerprint
|
|
2950
3125
|
});
|
|
2951
3126
|
}
|
|
2952
|
-
|
|
2953
|
-
conversation,
|
|
2954
|
-
approved: false,
|
|
2955
|
-
blocked: approval.blocked,
|
|
2956
|
-
reason: approval.reason,
|
|
2957
|
-
terminal_control: terminalControl,
|
|
2958
|
-
expected_approval_fingerprint: expectedFingerprint,
|
|
2959
|
-
actual_approval_fingerprint: actualFingerprint,
|
|
2960
|
-
screen_excerpt: approval.screenExcerpt
|
|
2961
|
-
});
|
|
2962
|
-
return;
|
|
2963
|
-
}
|
|
2964
|
-
appendEvent(logPath, {
|
|
2965
|
-
ts: new Date().toISOString(),
|
|
2966
|
-
conversation_id: conversation.conversation_id,
|
|
2967
|
-
event: "terminal_approval_send",
|
|
2968
|
-
terminal_control: terminalControl,
|
|
2969
|
-
key: approval.key,
|
|
2970
|
-
keys: approval.keys,
|
|
2971
|
-
label: approval.label,
|
|
2972
|
-
decision_mode: approval.decisionMode,
|
|
2973
|
-
request_id: approval.requestId,
|
|
2974
|
-
approval_fingerprint: actualFingerprint,
|
|
2975
|
-
auto_approved: autoApproved,
|
|
2976
|
-
policy_rule_id: effectivePolicyRuleId,
|
|
2977
|
-
policy_fingerprint: effectivePolicyFingerprint
|
|
2978
|
-
});
|
|
2979
|
-
if (autoApproved) {
|
|
2980
|
-
appendEvent(logPath, {
|
|
2981
|
-
ts: new Date().toISOString(),
|
|
3127
|
+
runtimeLog("info", "terminal_approval_send", {
|
|
2982
3128
|
conversation_id: conversation.conversation_id,
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
3129
|
+
terminal_target: terminalControl.target,
|
|
3130
|
+
key: approval.key,
|
|
3131
|
+
keys: approval.keys,
|
|
3132
|
+
label: approval.label,
|
|
3133
|
+
decision_mode: approval.decisionMode,
|
|
3134
|
+
request_id: approval.requestId,
|
|
2986
3135
|
approval_fingerprint: actualFingerprint,
|
|
3136
|
+
auto_approved: autoApproved,
|
|
2987
3137
|
policy_rule_id: effectivePolicyRuleId,
|
|
2988
3138
|
policy_fingerprint: effectivePolicyFingerprint
|
|
2989
3139
|
});
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
terminal_bridge_last_approval_prompt_cleared_at
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
approved: true,
|
|
3067
|
-
terminal_control: terminalControl,
|
|
3068
|
-
key: approval.key,
|
|
3069
|
-
keys: approval.keys,
|
|
3070
|
-
label: approval.label,
|
|
3071
|
-
decision_mode: approval.decisionMode,
|
|
3072
|
-
request_id: approval.requestId,
|
|
3073
|
-
approval_fingerprint: actualFingerprint,
|
|
3074
|
-
auto_approved: autoApproved,
|
|
3075
|
-
policy_rule_id: effectivePolicyRuleId,
|
|
3076
|
-
policy_fingerprint: effectivePolicyFingerprint,
|
|
3077
|
-
monitor_pid: bridgeMonitor.monitorPid ?? null,
|
|
3078
|
-
monitor_handoff_pid: bridgeMonitor.handoffWatchdog?.pid ?? null
|
|
3140
|
+
const nativeTakeoverForUpdate = isRecord(lockedConversation.native_session_takeover)
|
|
3141
|
+
? { ...lockedConversation.native_session_takeover }
|
|
3142
|
+
: {};
|
|
3143
|
+
const resolvedApproval = isRecord(nativeTakeoverForUpdate.terminal_bridge_approval)
|
|
3144
|
+
? nativeTakeoverForUpdate.terminal_bridge_approval
|
|
3145
|
+
: undefined;
|
|
3146
|
+
const resolvedApprovalScreenDigest = stringValue(resolvedApproval?.screen_digest);
|
|
3147
|
+
const resolvedApprovalState = isRecord(resolvedApproval?.approval_state)
|
|
3148
|
+
? resolvedApproval.approval_state
|
|
3149
|
+
: undefined;
|
|
3150
|
+
const resolvedTranscriptIdentity = claudeTranscriptApprovalIdentity(resolvedApprovalState);
|
|
3151
|
+
const approvalResolvedAt = new Date().toISOString();
|
|
3152
|
+
const agentTimeoutMinutes = Number(options.agentTimeoutMinutes ??
|
|
3153
|
+
nativeTakeoverForUpdate.terminal_bridge_inactivity_timeout_minutes ??
|
|
3154
|
+
DEFAULT_AGENT_TIMEOUT_MINUTES);
|
|
3155
|
+
const agentHardTimeoutMinutes = positiveMinutes(options.agentHardTimeoutMinutes ??
|
|
3156
|
+
nativeTakeoverForUpdate.terminal_bridge_hard_timeout_minutes ??
|
|
3157
|
+
DEFAULT_AGENT_HARD_TIMEOUT_MINUTES, "--agent-hard-timeout-minutes");
|
|
3158
|
+
const nextNativeTakeover = {
|
|
3159
|
+
...nativeTakeoverForUpdate,
|
|
3160
|
+
terminal_bridge_approval: undefined,
|
|
3161
|
+
terminal_bridge_approval_dispatch: undefined,
|
|
3162
|
+
terminal_bridge_approval_resolved_at: approvalResolvedAt,
|
|
3163
|
+
terminal_bridge_last_approval_fingerprint: actualFingerprint,
|
|
3164
|
+
terminal_bridge_last_approval_screen_digest: resolvedApprovalScreenDigest,
|
|
3165
|
+
terminal_bridge_last_approval_request_id: resolvedTranscriptIdentity?.requestId,
|
|
3166
|
+
terminal_bridge_last_approval_evidence_fingerprint: resolvedTranscriptIdentity?.evidenceFingerprint,
|
|
3167
|
+
terminal_bridge_last_approval_prompt_cleared_at: undefined,
|
|
3168
|
+
terminal_bridge_last_approval_at: approvalResolvedAt,
|
|
3169
|
+
terminal_bridge_last_approval_message_id: nativeTakeoverForUpdate.terminal_bridge_message_id,
|
|
3170
|
+
terminal_bridge_monitor_lock_version: TERMINAL_BRIDGE_MONITOR_LOCK_VERSION,
|
|
3171
|
+
terminal_bridge_monitor_started_at: approvalResolvedAt,
|
|
3172
|
+
terminal_bridge_last_activity_at: approvalResolvedAt,
|
|
3173
|
+
terminal_bridge_last_activity_reason: "approval resolved",
|
|
3174
|
+
terminal_bridge_inactivity_timeout_minutes: agentTimeoutMinutes,
|
|
3175
|
+
terminal_bridge_hard_timeout_minutes: agentHardTimeoutMinutes,
|
|
3176
|
+
terminal_bridge_inactivity_deadline_at: deadlineAt(approvalResolvedAt, agentTimeoutMinutes),
|
|
3177
|
+
terminal_bridge_hard_deadline_at: deadlineAt(stringValue(nativeTakeoverForUpdate.terminal_bridge_started_at) ?? approvalResolvedAt, agentHardTimeoutMinutes)
|
|
3178
|
+
};
|
|
3179
|
+
delete nextNativeTakeover.terminal_bridge_approval;
|
|
3180
|
+
delete nextNativeTakeover.terminal_bridge_approval_dispatch;
|
|
3181
|
+
delete nextNativeTakeover.terminal_bridge_last_approval_prompt_cleared_at;
|
|
3182
|
+
const nextConversation = {
|
|
3183
|
+
...lockedConversation,
|
|
3184
|
+
status: terminalBridgeEnabled(lockedConversation)
|
|
3185
|
+
? "waiting_for_agent"
|
|
3186
|
+
: lockedConversation.status,
|
|
3187
|
+
native_session_takeover: nextNativeTakeover,
|
|
3188
|
+
updated_at: approvalResolvedAt
|
|
3189
|
+
};
|
|
3190
|
+
saveState(statePath, nextConversation);
|
|
3191
|
+
releaseApprovalStateLock();
|
|
3192
|
+
releaseApprovalTerminalLock();
|
|
3193
|
+
const bridgeMonitor = ensureTerminalBridgeMonitorAfterApproval({
|
|
3194
|
+
conversation: nextConversation,
|
|
3195
|
+
statePath,
|
|
3196
|
+
logPath,
|
|
3197
|
+
terminalControl,
|
|
3198
|
+
options
|
|
3199
|
+
});
|
|
3200
|
+
printJson({
|
|
3201
|
+
conversation: nextConversation,
|
|
3202
|
+
approved: true,
|
|
3203
|
+
terminal_control: terminalControl,
|
|
3204
|
+
key: approval.key,
|
|
3205
|
+
keys: approval.keys,
|
|
3206
|
+
label: approval.label,
|
|
3207
|
+
decision_mode: approval.decisionMode,
|
|
3208
|
+
request_id: approval.requestId,
|
|
3209
|
+
approval_fingerprint: actualFingerprint,
|
|
3210
|
+
auto_approved: autoApproved,
|
|
3211
|
+
policy_rule_id: effectivePolicyRuleId,
|
|
3212
|
+
policy_fingerprint: effectivePolicyFingerprint,
|
|
3213
|
+
monitor_pid: bridgeMonitor.monitorPid ?? null,
|
|
3214
|
+
monitor_handoff_pid: bridgeMonitor.handoffWatchdog?.pid ?? null
|
|
3215
|
+
});
|
|
3079
3216
|
});
|
|
3080
3217
|
}
|
|
3081
3218
|
finally {
|
|
@@ -3150,7 +3287,7 @@ async function runTerminalConversationApprove({ options, conversationId, agent,
|
|
|
3150
3287
|
releaseTerminalLock();
|
|
3151
3288
|
}
|
|
3152
3289
|
}
|
|
3153
|
-
async function runTerminalControlSend({ options, conversation, nextConversation, statePath, logPath, executor, message, terminalControl, terminalSendLockHeld = false, terminalStateLockHeld = false, recordMessageAfterSend = false, recordRawAttachmentAfterSend = false }) {
|
|
3290
|
+
async function runTerminalControlSend({ options, conversation, nextConversation, statePath, logPath, executor, message, terminalControl, terminalSendLockHeld = false, terminalStateLockHeld = false, storeWriterLeaseHeld = false, recordMessageAfterSend = false, recordRawAttachmentAfterSend = false }) {
|
|
3154
3291
|
const bridge = terminalBridgeEnabled(conversation);
|
|
3155
3292
|
if (!terminalSendLockHeld) {
|
|
3156
3293
|
const releaseTerminalLock = acquireFileLock(terminalBridgeSendLockPath(storeDirFromOptions(options), terminalControl), { timeoutMs: 30000 });
|
|
@@ -3166,6 +3303,7 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
3166
3303
|
terminalControl,
|
|
3167
3304
|
terminalSendLockHeld: true,
|
|
3168
3305
|
terminalStateLockHeld,
|
|
3306
|
+
storeWriterLeaseHeld,
|
|
3169
3307
|
recordMessageAfterSend,
|
|
3170
3308
|
recordRawAttachmentAfterSend
|
|
3171
3309
|
});
|
|
@@ -3201,6 +3339,7 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
3201
3339
|
terminalControl,
|
|
3202
3340
|
terminalSendLockHeld: true,
|
|
3203
3341
|
terminalStateLockHeld: true,
|
|
3342
|
+
storeWriterLeaseHeld,
|
|
3204
3343
|
recordMessageAfterSend,
|
|
3205
3344
|
recordRawAttachmentAfterSend
|
|
3206
3345
|
});
|
|
@@ -3209,6 +3348,24 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
3209
3348
|
releaseStateLock();
|
|
3210
3349
|
}
|
|
3211
3350
|
}
|
|
3351
|
+
if (!storeWriterLeaseHeld) {
|
|
3352
|
+
const writerStoreDir = pathsForConversationDir(path.dirname(statePath)).storeDir;
|
|
3353
|
+
return await withStoreWriterLeaseAsync(writerStoreDir, async () => runTerminalControlSend({
|
|
3354
|
+
options,
|
|
3355
|
+
conversation,
|
|
3356
|
+
nextConversation,
|
|
3357
|
+
statePath,
|
|
3358
|
+
logPath,
|
|
3359
|
+
executor,
|
|
3360
|
+
message,
|
|
3361
|
+
terminalControl,
|
|
3362
|
+
terminalSendLockHeld: true,
|
|
3363
|
+
terminalStateLockHeld,
|
|
3364
|
+
storeWriterLeaseHeld: true,
|
|
3365
|
+
recordMessageAfterSend,
|
|
3366
|
+
recordRawAttachmentAfterSend
|
|
3367
|
+
}));
|
|
3368
|
+
}
|
|
3212
3369
|
const terminalBridge = createTerminalAgentBridge(options);
|
|
3213
3370
|
const bridgeStartedAt = new Date().toISOString();
|
|
3214
3371
|
const agentTimeoutMinutes = Number(options.agentTimeoutMinutes ?? DEFAULT_AGENT_TIMEOUT_MINUTES);
|
|
@@ -3723,7 +3880,6 @@ function terminalSubmissionPayload(payload) {
|
|
|
3723
3880
|
function createManagedTerminalConversationFromRawId({ options, conversationId, agent, pid, messageBody, terminalControl }) {
|
|
3724
3881
|
const workspace = terminalControl.currentPath ?? process.cwd();
|
|
3725
3882
|
const storeDir = expandHome(options.storeDir ?? options.logDir ?? defaultStoreDir(workspace));
|
|
3726
|
-
cleanupIdleConversations(storeDir, options);
|
|
3727
3883
|
const executor = resolveExecutor({
|
|
3728
3884
|
kind: agent,
|
|
3729
3885
|
session: conversationId
|
|
@@ -3948,8 +4104,15 @@ async function runRenew(options) {
|
|
|
3948
4104
|
});
|
|
3949
4105
|
}
|
|
3950
4106
|
async function runReconcileMonitors(options) {
|
|
4107
|
+
printJson(await reconcileMonitors(options, {
|
|
4108
|
+
includeCallbackRecovery: true,
|
|
4109
|
+
reason: "startup_reconciliation",
|
|
4110
|
+
conversationId: undefined
|
|
4111
|
+
}));
|
|
4112
|
+
}
|
|
4113
|
+
async function reconcileMonitors(options, { includeCallbackRecovery, reason, conversationId }) {
|
|
3951
4114
|
const storeDir = storeDirFromOptions(options);
|
|
3952
|
-
const conversations = listConversations(storeDir);
|
|
4115
|
+
const conversations = listConversations(storeDir).filter((conversation) => conversationId === undefined || conversation.conversation_id === conversationId);
|
|
3953
4116
|
const items = [];
|
|
3954
4117
|
let ignored = 0;
|
|
3955
4118
|
let launched = 0;
|
|
@@ -3966,30 +4129,32 @@ async function runReconcileMonitors(options) {
|
|
|
3966
4129
|
const logPath = expandHome(stringValue(listedConversation.event_log_path) ??
|
|
3967
4130
|
logPathForStatePath(statePath));
|
|
3968
4131
|
try {
|
|
3969
|
-
|
|
3970
|
-
|
|
3971
|
-
|
|
3972
|
-
|
|
3973
|
-
|
|
3974
|
-
if (callbackRecovery.handled) {
|
|
3975
|
-
if (callbackRecovery.status === "launched") {
|
|
3976
|
-
launched += 1;
|
|
3977
|
-
}
|
|
3978
|
-
else if (callbackRecovery.status === "already_running") {
|
|
3979
|
-
alreadyRunning += 1;
|
|
3980
|
-
}
|
|
3981
|
-
else {
|
|
3982
|
-
skipped += 1;
|
|
3983
|
-
}
|
|
3984
|
-
items.push({
|
|
3985
|
-
conversation_id: callbackRecovery.conversationId,
|
|
3986
|
-
status: callbackRecovery.status,
|
|
3987
|
-
reason: callbackRecovery.reason,
|
|
3988
|
-
...(callbackRecovery.monitorPid === undefined
|
|
3989
|
-
? {}
|
|
3990
|
-
: { monitor_pid: callbackRecovery.monitorPid })
|
|
4132
|
+
if (includeCallbackRecovery) {
|
|
4133
|
+
const callbackRecovery = prepareCallbackDeliveryReconciliation({
|
|
4134
|
+
statePath,
|
|
4135
|
+
logPath,
|
|
4136
|
+
delayMs: options.callbackRetryDelayMs
|
|
3991
4137
|
});
|
|
3992
|
-
|
|
4138
|
+
if (callbackRecovery.handled) {
|
|
4139
|
+
if (callbackRecovery.status === "launched") {
|
|
4140
|
+
launched += 1;
|
|
4141
|
+
}
|
|
4142
|
+
else if (callbackRecovery.status === "already_running") {
|
|
4143
|
+
alreadyRunning += 1;
|
|
4144
|
+
}
|
|
4145
|
+
else {
|
|
4146
|
+
skipped += 1;
|
|
4147
|
+
}
|
|
4148
|
+
items.push({
|
|
4149
|
+
conversation_id: callbackRecovery.conversationId,
|
|
4150
|
+
status: callbackRecovery.status,
|
|
4151
|
+
reason: callbackRecovery.reason,
|
|
4152
|
+
...(callbackRecovery.monitorPid === undefined
|
|
4153
|
+
? {}
|
|
4154
|
+
: { monitor_pid: callbackRecovery.monitorPid })
|
|
4155
|
+
});
|
|
4156
|
+
continue;
|
|
4157
|
+
}
|
|
3993
4158
|
}
|
|
3994
4159
|
const listedNativeTakeover = isRecord(listedConversation.native_session_takeover)
|
|
3995
4160
|
? listedConversation.native_session_takeover
|
|
@@ -4104,7 +4269,7 @@ async function runReconcileMonitors(options) {
|
|
|
4104
4269
|
event: "terminal_bridge_monitor_launch",
|
|
4105
4270
|
pid: monitor.pid ?? null,
|
|
4106
4271
|
terminal_control: prepared.terminalControl,
|
|
4107
|
-
reason
|
|
4272
|
+
reason,
|
|
4108
4273
|
agent_timeout_minutes: prepared.inactivityTimeoutMinutes,
|
|
4109
4274
|
agent_hard_timeout_minutes: prepared.hardTimeoutMinutes
|
|
4110
4275
|
});
|
|
@@ -4117,7 +4282,7 @@ async function runReconcileMonitors(options) {
|
|
|
4117
4282
|
items.push({
|
|
4118
4283
|
conversation_id: prepared.conversation.conversation_id,
|
|
4119
4284
|
status: "launched",
|
|
4120
|
-
reason
|
|
4285
|
+
reason,
|
|
4121
4286
|
monitor_pid: monitor.pid ?? null
|
|
4122
4287
|
});
|
|
4123
4288
|
}
|
|
@@ -4130,7 +4295,7 @@ async function runReconcileMonitors(options) {
|
|
|
4130
4295
|
});
|
|
4131
4296
|
}
|
|
4132
4297
|
}
|
|
4133
|
-
|
|
4298
|
+
return {
|
|
4134
4299
|
reconciled: true,
|
|
4135
4300
|
store_dir: storeDir,
|
|
4136
4301
|
checked: conversations.length,
|
|
@@ -4140,7 +4305,7 @@ async function runReconcileMonitors(options) {
|
|
|
4140
4305
|
skipped,
|
|
4141
4306
|
errors,
|
|
4142
4307
|
items
|
|
4143
|
-
}
|
|
4308
|
+
};
|
|
4144
4309
|
}
|
|
4145
4310
|
function prepareCallbackDeliveryReconciliation({ statePath, logPath, delayMs }) {
|
|
4146
4311
|
const releaseStateLock = acquireFileLock(`${statePath}.lock`);
|
|
@@ -4383,7 +4548,6 @@ function positiveMinutes(value, optionName) {
|
|
|
4383
4548
|
return parsed;
|
|
4384
4549
|
}
|
|
4385
4550
|
async function runCancel(options) {
|
|
4386
|
-
cleanupIdleConversations(storeDirFromOptions(options), options);
|
|
4387
4551
|
const terminalConversation = await resolveTerminalConversationFromOptions(options);
|
|
4388
4552
|
if (terminalConversation) {
|
|
4389
4553
|
await runTerminalConversationCancel({
|
|
@@ -4463,75 +4627,78 @@ async function runTerminalControlCancel({ options, statePath, logPath, agent, te
|
|
|
4463
4627
|
let releaseStateLock;
|
|
4464
4628
|
try {
|
|
4465
4629
|
releaseStateLock = acquireFileLock(`${statePath}.lock`);
|
|
4466
|
-
const
|
|
4467
|
-
|
|
4468
|
-
|
|
4469
|
-
|
|
4470
|
-
|
|
4471
|
-
|
|
4472
|
-
|
|
4473
|
-
|
|
4474
|
-
|
|
4475
|
-
currentControl
|
|
4476
|
-
currentControl
|
|
4477
|
-
|
|
4478
|
-
|
|
4479
|
-
|
|
4480
|
-
|
|
4481
|
-
|
|
4482
|
-
action: "cancel"
|
|
4483
|
-
});
|
|
4484
|
-
const cancellation = await createTerminalAgentBridge(options).cancel(agent, currentControl, {
|
|
4485
|
-
runtime: terminalRuntimeIdentityForConversation(currentConversation, currentControl),
|
|
4486
|
-
scrollbackLines: Number(options.scrollbackLines ?? 120)
|
|
4487
|
-
});
|
|
4488
|
-
if (!cancellation.cancelRequested) {
|
|
4489
|
-
printJson({
|
|
4630
|
+
const writerStoreDir = pathsForConversationDir(path.dirname(statePath)).storeDir;
|
|
4631
|
+
return await withStoreWriterLeaseAsync(writerStoreDir, async () => {
|
|
4632
|
+
const currentConversation = loadState(statePath);
|
|
4633
|
+
if (!["waiting_for_agent", "waiting_for_openclaw"].includes(currentConversation.status)) {
|
|
4634
|
+
throw new Error(`cannot cancel ${currentConversation.conversation_id}; conversation is ${currentConversation.status}`);
|
|
4635
|
+
}
|
|
4636
|
+
const currentTakeover = isRecord(currentConversation.native_session_takeover)
|
|
4637
|
+
? currentConversation.native_session_takeover
|
|
4638
|
+
: undefined;
|
|
4639
|
+
const currentControl = terminalControlFromTakeover(currentTakeover);
|
|
4640
|
+
if (!currentControl ||
|
|
4641
|
+
currentControl.target !== terminalControl.target ||
|
|
4642
|
+
currentControl.socketPath !== terminalControl.socketPath) {
|
|
4643
|
+
throw new Error("terminal control changed while waiting to cancel; refresh status and retry");
|
|
4644
|
+
}
|
|
4645
|
+
assertManagedTerminalDispatchOwner({
|
|
4490
4646
|
conversation: currentConversation,
|
|
4491
|
-
|
|
4492
|
-
|
|
4647
|
+
terminalControl: currentControl,
|
|
4648
|
+
action: "cancel"
|
|
4649
|
+
});
|
|
4650
|
+
const cancellation = await createTerminalAgentBridge(options).cancel(agent, currentControl, {
|
|
4651
|
+
runtime: terminalRuntimeIdentityForConversation(currentConversation, currentControl),
|
|
4652
|
+
scrollbackLines: Number(options.scrollbackLines ?? 120)
|
|
4653
|
+
});
|
|
4654
|
+
if (!cancellation.cancelRequested) {
|
|
4655
|
+
printJson({
|
|
4656
|
+
conversation: currentConversation,
|
|
4657
|
+
cancel_requested: false,
|
|
4658
|
+
reason: cancellation.reason,
|
|
4659
|
+
terminal_control: currentControl,
|
|
4660
|
+
budget: budgetAction(currentConversation)
|
|
4661
|
+
});
|
|
4662
|
+
return;
|
|
4663
|
+
}
|
|
4664
|
+
const now = new Date().toISOString();
|
|
4665
|
+
appendEvent(logPath, {
|
|
4666
|
+
ts: now,
|
|
4667
|
+
conversation_id: currentConversation.conversation_id,
|
|
4668
|
+
event: "terminal_cancel_requested",
|
|
4493
4669
|
terminal_control: currentControl,
|
|
4494
|
-
|
|
4670
|
+
key: cancellation.key,
|
|
4671
|
+
keys: cancellation.keys,
|
|
4672
|
+
denied_approval: cancellation.deniedApproval,
|
|
4673
|
+
request_id: cancellation.requestId
|
|
4674
|
+
});
|
|
4675
|
+
runtimeLog("info", "terminal_cancel_requested", {
|
|
4676
|
+
conversation_id: currentConversation.conversation_id,
|
|
4677
|
+
agent,
|
|
4678
|
+
terminal_target: currentControl.target,
|
|
4679
|
+
key: cancellation.key,
|
|
4680
|
+
keys: cancellation.keys,
|
|
4681
|
+
denied_approval: cancellation.deniedApproval,
|
|
4682
|
+
request_id: cancellation.requestId
|
|
4683
|
+
});
|
|
4684
|
+
const nextConversation = {
|
|
4685
|
+
...currentConversation,
|
|
4686
|
+
status: "cancelled",
|
|
4687
|
+
cancelled_at: now,
|
|
4688
|
+
terminal_cancel_requested_at: now,
|
|
4689
|
+
updated_at: now
|
|
4690
|
+
};
|
|
4691
|
+
saveState(statePath, nextConversation);
|
|
4692
|
+
printJson({
|
|
4693
|
+
conversation: nextConversation,
|
|
4694
|
+
cancel_requested: true,
|
|
4695
|
+
terminal_control: currentControl,
|
|
4696
|
+
key: cancellation.key,
|
|
4697
|
+
keys: cancellation.keys,
|
|
4698
|
+
denied_approval: cancellation.deniedApproval,
|
|
4699
|
+
request_id: cancellation.requestId,
|
|
4700
|
+
budget: budgetAction(nextConversation)
|
|
4495
4701
|
});
|
|
4496
|
-
return;
|
|
4497
|
-
}
|
|
4498
|
-
const now = new Date().toISOString();
|
|
4499
|
-
appendEvent(logPath, {
|
|
4500
|
-
ts: now,
|
|
4501
|
-
conversation_id: currentConversation.conversation_id,
|
|
4502
|
-
event: "terminal_cancel_requested",
|
|
4503
|
-
terminal_control: currentControl,
|
|
4504
|
-
key: cancellation.key,
|
|
4505
|
-
keys: cancellation.keys,
|
|
4506
|
-
denied_approval: cancellation.deniedApproval,
|
|
4507
|
-
request_id: cancellation.requestId
|
|
4508
|
-
});
|
|
4509
|
-
runtimeLog("info", "terminal_cancel_requested", {
|
|
4510
|
-
conversation_id: currentConversation.conversation_id,
|
|
4511
|
-
agent,
|
|
4512
|
-
terminal_target: currentControl.target,
|
|
4513
|
-
key: cancellation.key,
|
|
4514
|
-
keys: cancellation.keys,
|
|
4515
|
-
denied_approval: cancellation.deniedApproval,
|
|
4516
|
-
request_id: cancellation.requestId
|
|
4517
|
-
});
|
|
4518
|
-
const nextConversation = {
|
|
4519
|
-
...currentConversation,
|
|
4520
|
-
status: "cancelled",
|
|
4521
|
-
cancelled_at: now,
|
|
4522
|
-
terminal_cancel_requested_at: now,
|
|
4523
|
-
updated_at: now
|
|
4524
|
-
};
|
|
4525
|
-
saveState(statePath, nextConversation);
|
|
4526
|
-
printJson({
|
|
4527
|
-
conversation: nextConversation,
|
|
4528
|
-
cancel_requested: true,
|
|
4529
|
-
terminal_control: currentControl,
|
|
4530
|
-
key: cancellation.key,
|
|
4531
|
-
keys: cancellation.keys,
|
|
4532
|
-
denied_approval: cancellation.deniedApproval,
|
|
4533
|
-
request_id: cancellation.requestId,
|
|
4534
|
-
budget: budgetAction(nextConversation)
|
|
4535
4702
|
});
|
|
4536
4703
|
}
|
|
4537
4704
|
finally {
|
|
@@ -5962,7 +6129,7 @@ function terminalBridgeRuntimeDir() {
|
|
|
5962
6129
|
const configured = stringValue(process.env.AKK_RUNTIME_DIR);
|
|
5963
6130
|
return configured
|
|
5964
6131
|
? path.resolve(expandHome(configured))
|
|
5965
|
-
: path.join(path.dirname(defaultStoreDir()), "runtime");
|
|
6132
|
+
: path.join(path.dirname(defaultStoreDir()), "runtime-v2");
|
|
5966
6133
|
}
|
|
5967
6134
|
function terminalBridgeRuntimeKey(terminalControl) {
|
|
5968
6135
|
return createHash("sha256")
|
|
@@ -5975,7 +6142,6 @@ function terminalBridgeRuntimeKey(terminalControl) {
|
|
|
5975
6142
|
}
|
|
5976
6143
|
function terminalBridgeDispatchLedgerPath(terminalControl) {
|
|
5977
6144
|
const ledgerDir = path.join(terminalBridgeRuntimeDir(), "terminal-dispatch");
|
|
5978
|
-
ensureDir(ledgerDir);
|
|
5979
6145
|
return path.join(ledgerDir, `terminal-dispatch-${terminalBridgeRuntimeKey(terminalControl)}.json`);
|
|
5980
6146
|
}
|
|
5981
6147
|
function loadTerminalBridgeDispatchLedger(terminalControl) {
|
|
@@ -6022,6 +6188,7 @@ function orphanedTerminalDispatchForRecovery(terminalControl) {
|
|
|
6022
6188
|
}
|
|
6023
6189
|
function saveTerminalBridgeDispatchLedger(terminalControl, ledger) {
|
|
6024
6190
|
const ledgerPath = terminalBridgeDispatchLedgerPath(terminalControl);
|
|
6191
|
+
ensureDir(path.dirname(ledgerPath));
|
|
6025
6192
|
if (fs.existsSync(ledgerPath) && fs.lstatSync(ledgerPath).isSymbolicLink()) {
|
|
6026
6193
|
throw new Error(`terminal dispatch ledger is a symlink: ${ledgerPath}`);
|
|
6027
6194
|
}
|
|
@@ -7025,6 +7192,7 @@ function runPreparedCallback(prepared, { emit = true } = {}) {
|
|
|
7025
7192
|
}
|
|
7026
7193
|
return result;
|
|
7027
7194
|
}
|
|
7195
|
+
assertStoreWriterCompatible(pathsForConversationDir(path.dirname(prepared.statePath)).storeDir);
|
|
7028
7196
|
try {
|
|
7029
7197
|
const deliveryKind = deliverCallbackToOpenClaw({
|
|
7030
7198
|
options: prepared.options,
|
|
@@ -7570,7 +7738,13 @@ function loadConversationFromOptions(options) {
|
|
|
7570
7738
|
throw new Error("--conversation or --state is required");
|
|
7571
7739
|
}
|
|
7572
7740
|
const conversation = options.state
|
|
7573
|
-
?
|
|
7741
|
+
? (() => {
|
|
7742
|
+
const paths = pathsForConversationDir(path.dirname(statePath));
|
|
7743
|
+
if (path.resolve(paths.statePath) !== path.resolve(statePath)) {
|
|
7744
|
+
throw new Error(`AKK state path is not canonical: ${statePath}`);
|
|
7745
|
+
}
|
|
7746
|
+
return loadConversationById(path.basename(paths.conversationDir), paths.storeDir);
|
|
7747
|
+
})()
|
|
7574
7748
|
: loadConversationById(conversationId, storeDir);
|
|
7575
7749
|
assertConfiguredWorkspace(options.workspace, conversation.workspace, `access to AKK conversation ${conversation.conversation_id}`);
|
|
7576
7750
|
return {
|
|
@@ -8183,13 +8357,21 @@ function deliverStalledNotification({ statePath, logPath, conversation, message,
|
|
|
8183
8357
|
stderr: textSummary(chatSendDelivery.stderr)
|
|
8184
8358
|
});
|
|
8185
8359
|
}
|
|
8186
|
-
function
|
|
8360
|
+
function reconcileIdleConversations(storeDir, options = {}, now = new Date(), conversationId) {
|
|
8187
8361
|
const timeoutMinutes = Number(options.idleTimeoutMinutes ?? DEFAULT_IDLE_TIMEOUT_MINUTES);
|
|
8188
8362
|
if (!Number.isFinite(timeoutMinutes) || timeoutMinutes <= 0) {
|
|
8189
|
-
return {
|
|
8363
|
+
return {
|
|
8364
|
+
checked: 0,
|
|
8365
|
+
closed: 0,
|
|
8366
|
+
skipped: 0,
|
|
8367
|
+
idle_timeout_minutes: timeoutMinutes
|
|
8368
|
+
};
|
|
8190
8369
|
}
|
|
8191
|
-
|
|
8370
|
+
ensureStoreWritable(storeDir);
|
|
8371
|
+
const conversations = listConversations(storeDir).filter((conversation) => (conversationId === undefined || conversation.conversation_id === conversationId) &&
|
|
8372
|
+
matchesConfiguredWorkspace(options.workspace, conversation.workspace));
|
|
8192
8373
|
let closed = 0;
|
|
8374
|
+
let skipped = 0;
|
|
8193
8375
|
for (const listedConversation of conversations) {
|
|
8194
8376
|
if (listedConversation.status !== "idle" || !listedConversation.idle_since) {
|
|
8195
8377
|
continue;
|
|
@@ -8209,6 +8391,7 @@ function cleanupIdleConversations(storeDir, options = {}, now = new Date()) {
|
|
|
8209
8391
|
}
|
|
8210
8392
|
catch (error) {
|
|
8211
8393
|
if (isRecord(error) && error.code === "LOCK_TIMEOUT") {
|
|
8394
|
+
skipped += 1;
|
|
8212
8395
|
continue;
|
|
8213
8396
|
}
|
|
8214
8397
|
throw error;
|
|
@@ -8267,6 +8450,7 @@ function cleanupIdleConversations(storeDir, options = {}, now = new Date()) {
|
|
|
8267
8450
|
return {
|
|
8268
8451
|
checked: conversations.length,
|
|
8269
8452
|
closed,
|
|
8453
|
+
skipped,
|
|
8270
8454
|
idle_timeout_minutes: timeoutMinutes
|
|
8271
8455
|
};
|
|
8272
8456
|
}
|
|
@@ -8733,8 +8917,8 @@ function usage() {
|
|
|
8733
8917
|
agent-knock-knock --help
|
|
8734
8918
|
agent-knock-knock --version
|
|
8735
8919
|
agent-knock-knock delegate --request <text> [--agent ${agentList}] [--workspace <path>] [--store-dir <dir>]
|
|
8736
|
-
agent-knock-knock list [--store-dir <dir>] [--agent ${agentList}] [--status <status>] [--all] [--managed-only] [--no-approval-scan] [--terminal-debug]
|
|
8737
|
-
agent-knock-knock status [--conversation <id|selector>] [--store-dir <dir>] [--trace]
|
|
8920
|
+
agent-knock-knock list [--store-dir <dir>] [--agent ${agentList}] [--status <status>] [--all] [--managed-only] [--reconcile] [--no-approval-scan] [--terminal-debug]
|
|
8921
|
+
agent-knock-knock status [--conversation <id|selector>] [--store-dir <dir>] [--reconcile] [--trace]
|
|
8738
8922
|
agent-knock-knock send [--conversation <id|selector>] --message <text> [--type answer|task|control] [--agent-timeout-minutes <minutes>] [--agent-hard-timeout-minutes <minutes>]
|
|
8739
8923
|
agent-knock-knock approve [--conversation <id|selector>] --expected-approval-fingerprint <fingerprint>
|
|
8740
8924
|
agent-knock-knock cancel [--conversation <id|selector>]
|