@scotthuang/agent-knock-knock 0.6.2 → 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 +13 -0
- package/README.md +16 -4
- package/dist/src/cli.js +570 -407
- package/dist/src/cli.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/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,9 +1408,41 @@ async function runList(options) {
|
|
|
1379
1408
|
include_all: includeAll,
|
|
1380
1409
|
agent_filter: agentFilter,
|
|
1381
1410
|
status_filter: statusFilter,
|
|
1382
|
-
|
|
1411
|
+
reconciliation
|
|
1383
1412
|
});
|
|
1384
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
|
|
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
|
+
};
|
|
1445
|
+
}
|
|
1385
1446
|
async function buildTerminalListGroup({ options, agentFilter, statusFilter }) {
|
|
1386
1447
|
const empty = {
|
|
1387
1448
|
terminalControlled: [],
|
|
@@ -1862,7 +1923,6 @@ function isSessionSelectorSyntax(value) {
|
|
|
1862
1923
|
}
|
|
1863
1924
|
async function sessionSelectorCandidates(commandName, options) {
|
|
1864
1925
|
const storeDir = storeDirFromOptions(options);
|
|
1865
|
-
cleanupIdleConversations(storeDir, options);
|
|
1866
1926
|
const storedConversations = listConversations(storeDir);
|
|
1867
1927
|
const workspaceConversations = storedConversations
|
|
1868
1928
|
.filter((conversation) => matchesConfiguredWorkspace(options.workspace, conversation.workspace));
|
|
@@ -1956,7 +2016,23 @@ async function resolveTerminalConversationFromOptions(options) {
|
|
|
1956
2016
|
return createTerminalAgentBridge(options).resolveConversationId(stringValue(options.conversation ?? options.conversationId));
|
|
1957
2017
|
}
|
|
1958
2018
|
async function runStatus(options) {
|
|
1959
|
-
|
|
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
|
+
};
|
|
1960
2036
|
const terminalConversation = await resolveTerminalConversationFromOptions(options);
|
|
1961
2037
|
if (terminalConversation) {
|
|
1962
2038
|
const terminalStatus = await terminalStatusForControl(terminalConversation.agent, terminalConversation.terminalControl, options, {
|
|
@@ -1970,6 +2046,8 @@ async function runStatus(options) {
|
|
|
1970
2046
|
conversation_id: terminalConversation.conversationId,
|
|
1971
2047
|
source: "terminal_control",
|
|
1972
2048
|
agent: terminalConversation.agent,
|
|
2049
|
+
store: inspectStoreCompatibility(storeDir),
|
|
2050
|
+
reconciliation,
|
|
1973
2051
|
...context,
|
|
1974
2052
|
terminal_control: terminalConversation.terminalControl,
|
|
1975
2053
|
terminal_status: terminalStatus,
|
|
@@ -1984,13 +2062,12 @@ async function runStatus(options) {
|
|
|
1984
2062
|
}
|
|
1985
2063
|
const loaded = loadConversationFromOptions(options);
|
|
1986
2064
|
const { statePath, logPath } = loaded;
|
|
1987
|
-
const conversation =
|
|
1988
|
-
...loaded,
|
|
1989
|
-
options
|
|
1990
|
-
});
|
|
2065
|
+
const conversation = loaded.conversation;
|
|
1991
2066
|
const events = readExistingEvents(logPath);
|
|
1992
2067
|
const result = {
|
|
1993
2068
|
conversation,
|
|
2069
|
+
store: inspectStoreCompatibility(storeDir),
|
|
2070
|
+
reconciliation,
|
|
1994
2071
|
summary: summarizeConversation(conversation),
|
|
1995
2072
|
confidence: "high",
|
|
1996
2073
|
about: managedConversationAbout(conversation, events),
|
|
@@ -2027,6 +2104,38 @@ async function runStatus(options) {
|
|
|
2027
2104
|
trace: Boolean(options.trace)
|
|
2028
2105
|
});
|
|
2029
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
|
+
}
|
|
2030
2139
|
async function terminalStatusContext(terminalConversation, terminalStatus, options) {
|
|
2031
2140
|
if (terminalConversation.agent === "codex") {
|
|
2032
2141
|
try {
|
|
@@ -2497,7 +2606,6 @@ async function runSend(options) {
|
|
|
2497
2606
|
if (options.agentHardTimeoutMinutes !== undefined) {
|
|
2498
2607
|
positiveMinutes(options.agentHardTimeoutMinutes, "--agent-hard-timeout-minutes");
|
|
2499
2608
|
}
|
|
2500
|
-
cleanupIdleConversations(storeDirFromOptions(options), options);
|
|
2501
2609
|
const terminalConversation = await resolveTerminalConversationFromOptions(options);
|
|
2502
2610
|
if (terminalConversation) {
|
|
2503
2611
|
if (!options.background) {
|
|
@@ -2514,6 +2622,7 @@ async function runSend(options) {
|
|
|
2514
2622
|
messageBody,
|
|
2515
2623
|
terminalControl: terminalConversation.terminalControl
|
|
2516
2624
|
});
|
|
2625
|
+
ensureStoreWritable(managed.conversation.store_dir);
|
|
2517
2626
|
ensureDir(path.dirname(managed.statePath));
|
|
2518
2627
|
releaseStateLock = acquireFileLock(`${managed.statePath}.lock`);
|
|
2519
2628
|
await runTerminalControlSend({
|
|
@@ -2599,7 +2708,6 @@ async function runSend(options) {
|
|
|
2599
2708
|
throw new Error(`conversation ${migratedConversation.conversation_id} is not attached to a live tmux terminal`);
|
|
2600
2709
|
}
|
|
2601
2710
|
async function runApprove(options) {
|
|
2602
|
-
cleanupIdleConversations(storeDirFromOptions(options), options);
|
|
2603
2711
|
const terminalConversation = await resolveTerminalConversationFromOptions(options);
|
|
2604
2712
|
if (terminalConversation) {
|
|
2605
2713
|
await runTerminalConversationApprove({
|
|
@@ -2773,6 +2881,7 @@ async function runApprove(options) {
|
|
|
2773
2881
|
}
|
|
2774
2882
|
};
|
|
2775
2883
|
let releaseStateLock;
|
|
2884
|
+
let approvalDispatchReserved = false;
|
|
2776
2885
|
const releaseApprovalStateLock = () => {
|
|
2777
2886
|
if (releaseStateLock) {
|
|
2778
2887
|
const release = releaseStateLock;
|
|
@@ -2780,323 +2889,330 @@ async function runApprove(options) {
|
|
|
2780
2889
|
release();
|
|
2781
2890
|
}
|
|
2782
2891
|
};
|
|
2892
|
+
releaseStateLock = acquireFileLock(`${statePath}.lock`);
|
|
2893
|
+
const writerStoreDir = pathsForConversationDir(path.dirname(statePath)).storeDir;
|
|
2783
2894
|
try {
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
return {
|
|
2818
|
-
approved: false,
|
|
2819
|
-
reason: "automatic approval requires an executor-side policy"
|
|
2820
|
-
};
|
|
2821
|
-
}
|
|
2822
|
-
const candidate = policyCandidateForInspection({
|
|
2823
|
-
agent,
|
|
2824
|
-
currentTerminalControl,
|
|
2825
|
-
inspection,
|
|
2826
|
-
fingerprint
|
|
2827
|
-
});
|
|
2828
|
-
executorPolicyDecision = evaluateApprovalPolicy({
|
|
2829
|
-
policy: autoApprovalPolicy,
|
|
2830
|
-
candidate
|
|
2831
|
-
});
|
|
2832
|
-
if (executorPolicyDecision.action !== "approve") {
|
|
2833
|
-
return {
|
|
2834
|
-
approved: false,
|
|
2835
|
-
reason: `executor-side auto-approval policy rejected the current request: ${executorPolicyDecision.reason}`
|
|
2836
|
-
};
|
|
2837
|
-
}
|
|
2838
|
-
if (policyRuleId && executorPolicyDecision.ruleId !== policyRuleId) {
|
|
2839
|
-
return {
|
|
2840
|
-
approved: false,
|
|
2841
|
-
reason: "executor-side auto-approval rule changed before execution"
|
|
2842
|
-
};
|
|
2843
|
-
}
|
|
2844
|
-
if (policyFingerprint &&
|
|
2845
|
-
executorPolicyDecision.policyFingerprint !== policyFingerprint) {
|
|
2846
|
-
return {
|
|
2847
|
-
approved: false,
|
|
2848
|
-
reason: "executor-side auto-approval policy changed before execution"
|
|
2849
|
-
};
|
|
2850
|
-
}
|
|
2851
|
-
return { approved: true };
|
|
2852
|
-
}
|
|
2853
|
-
: undefined,
|
|
2854
|
-
beforeKeyDispatch: claudeScreenApproval
|
|
2855
|
-
? ({ fingerprint, terminalControl: dispatchControl, inspection, keys }) => {
|
|
2856
|
-
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 }) => {
|
|
2857
2928
|
if (!autoApprovalPolicy) {
|
|
2858
|
-
|
|
2929
|
+
return {
|
|
2930
|
+
approved: false,
|
|
2931
|
+
reason: "automatic approval requires an executor-side policy"
|
|
2932
|
+
};
|
|
2859
2933
|
}
|
|
2860
|
-
const
|
|
2934
|
+
const candidate = policyCandidateForInspection({
|
|
2935
|
+
agent,
|
|
2936
|
+
currentTerminalControl,
|
|
2937
|
+
inspection,
|
|
2938
|
+
fingerprint
|
|
2939
|
+
});
|
|
2940
|
+
executorPolicyDecision = evaluateApprovalPolicy({
|
|
2861
2941
|
policy: autoApprovalPolicy,
|
|
2862
|
-
candidate
|
|
2863
|
-
agent: executor.kind,
|
|
2864
|
-
currentTerminalControl: dispatchControl,
|
|
2865
|
-
inspection,
|
|
2866
|
-
fingerprint
|
|
2867
|
-
})
|
|
2942
|
+
candidate
|
|
2868
2943
|
});
|
|
2869
|
-
if (
|
|
2870
|
-
|
|
2944
|
+
if (executorPolicyDecision.action !== "approve") {
|
|
2945
|
+
return {
|
|
2946
|
+
approved: false,
|
|
2947
|
+
reason: `executor-side auto-approval policy rejected the current request: ${executorPolicyDecision.reason}`
|
|
2948
|
+
};
|
|
2871
2949
|
}
|
|
2872
|
-
if (executorPolicyDecision
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
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
|
+
};
|
|
2878
2955
|
}
|
|
2879
2956
|
if (policyFingerprint &&
|
|
2880
|
-
|
|
2881
|
-
|
|
2957
|
+
executorPolicyDecision.policyFingerprint !== policyFingerprint) {
|
|
2958
|
+
return {
|
|
2959
|
+
approved: false,
|
|
2960
|
+
reason: "executor-side auto-approval policy changed before execution"
|
|
2961
|
+
};
|
|
2882
2962
|
}
|
|
2883
|
-
|
|
2884
|
-
}
|
|
2885
|
-
if (releaseStateLock) {
|
|
2886
|
-
throw new Error("Claude approval dispatch was already reserved");
|
|
2963
|
+
return { approved: true };
|
|
2887
2964
|
}
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
const latestApproval = isRecord(latestTakeover?.terminal_bridge_approval)
|
|
2895
|
-
? latestTakeover.terminal_bridge_approval
|
|
2896
|
-
: undefined;
|
|
2897
|
-
const latestNotifiedAt = validTimestampMs(latestApproval?.notified_at);
|
|
2898
|
-
const latestApprovalState = isRecord(latestApproval?.approval_state)
|
|
2899
|
-
? latestApproval.approval_state
|
|
2900
|
-
: undefined;
|
|
2901
|
-
const latestPolicyEvidence = isRecord(latestApprovalState?.policy_evidence)
|
|
2902
|
-
? latestApprovalState.policy_evidence
|
|
2903
|
-
: undefined;
|
|
2904
|
-
const recapturedPolicyEvidence = inspection.approval.approvable
|
|
2905
|
-
? inspection.approval.policyEvidence
|
|
2906
|
-
: undefined;
|
|
2907
|
-
const latestDispatch = isRecord(latestTakeover?.terminal_bridge_approval_dispatch)
|
|
2908
|
-
? latestTakeover.terminal_bridge_approval_dispatch
|
|
2909
|
-
: undefined;
|
|
2910
|
-
if (!latestTakeover ||
|
|
2911
|
-
latestConversation.status !== "waiting_for_openclaw" ||
|
|
2912
|
-
latestTakeover.terminal_bridge_message_id !==
|
|
2913
|
-
nativeTakeover?.terminal_bridge_message_id ||
|
|
2914
|
-
latestApproval?.fingerprint !== fingerprint ||
|
|
2915
|
-
latestNotifiedAt === undefined ||
|
|
2916
|
-
Date.now() - latestNotifiedAt > CLAUDE_SCREEN_APPROVAL_TTL_MS ||
|
|
2917
|
-
expectedFingerprint !== fingerprint ||
|
|
2918
|
-
latestControl?.target !== dispatchControl.target ||
|
|
2919
|
-
latestControl?.socketPath !== dispatchControl.socketPath ||
|
|
2920
|
-
(autoApproved &&
|
|
2921
|
-
(latestPolicyEvidence?.source !== "claude_transcript" ||
|
|
2922
|
-
latestPolicyEvidence.evidence_fingerprint !==
|
|
2923
|
-
recapturedPolicyEvidence?.evidenceFingerprint))) {
|
|
2924
|
-
throw new Error("approval state changed before terminal dispatch; refresh status and retry");
|
|
2925
|
-
}
|
|
2926
|
-
if (latestDispatch?.state === "reserved" &&
|
|
2927
|
-
latestDispatch.terminal_bridge_message_id ===
|
|
2928
|
-
latestTakeover.terminal_bridge_message_id) {
|
|
2929
|
-
throw new Error("a previous Claude approval dispatch has an uncertain outcome; inspect and resolve the terminal manually");
|
|
2930
|
-
}
|
|
2931
|
-
const reservedAt = new Date().toISOString();
|
|
2932
|
-
const reservedConversation = {
|
|
2933
|
-
...latestConversation,
|
|
2934
|
-
native_session_takeover: {
|
|
2935
|
-
...latestTakeover,
|
|
2936
|
-
terminal_bridge_approval_dispatch: {
|
|
2937
|
-
state: "reserved",
|
|
2938
|
-
attempt_id: randomUUID(),
|
|
2939
|
-
fingerprint,
|
|
2940
|
-
keys,
|
|
2941
|
-
terminal_target: dispatchControl.target,
|
|
2942
|
-
terminal_bridge_message_id: latestTakeover.terminal_bridge_message_id,
|
|
2943
|
-
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");
|
|
2944
2971
|
}
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
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
|
+
});
|
|
2950
3087
|
}
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
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
|
+
});
|
|
2959
3115
|
if (autoApproved) {
|
|
2960
3116
|
appendEvent(logPath, {
|
|
2961
3117
|
ts: new Date().toISOString(),
|
|
2962
3118
|
conversation_id: conversation.conversation_id,
|
|
2963
3119
|
event: "terminal_auto_approval_decision",
|
|
2964
|
-
action: "
|
|
2965
|
-
reason: approval.reason,
|
|
3120
|
+
action: "approved",
|
|
2966
3121
|
terminal_control: terminalControl,
|
|
2967
|
-
|
|
2968
|
-
actual_fingerprint: actualFingerprint,
|
|
3122
|
+
approval_fingerprint: actualFingerprint,
|
|
2969
3123
|
policy_rule_id: effectivePolicyRuleId,
|
|
2970
3124
|
policy_fingerprint: effectivePolicyFingerprint
|
|
2971
3125
|
});
|
|
2972
3126
|
}
|
|
2973
|
-
|
|
2974
|
-
conversation,
|
|
2975
|
-
approved: false,
|
|
2976
|
-
blocked: approval.blocked,
|
|
2977
|
-
reason: approval.reason,
|
|
2978
|
-
terminal_control: terminalControl,
|
|
2979
|
-
expected_approval_fingerprint: expectedFingerprint,
|
|
2980
|
-
actual_approval_fingerprint: actualFingerprint,
|
|
2981
|
-
screen_excerpt: approval.screenExcerpt
|
|
2982
|
-
});
|
|
2983
|
-
return;
|
|
2984
|
-
}
|
|
2985
|
-
appendEvent(logPath, {
|
|
2986
|
-
ts: new Date().toISOString(),
|
|
2987
|
-
conversation_id: conversation.conversation_id,
|
|
2988
|
-
event: "terminal_approval_send",
|
|
2989
|
-
terminal_control: terminalControl,
|
|
2990
|
-
key: approval.key,
|
|
2991
|
-
keys: approval.keys,
|
|
2992
|
-
label: approval.label,
|
|
2993
|
-
decision_mode: approval.decisionMode,
|
|
2994
|
-
request_id: approval.requestId,
|
|
2995
|
-
approval_fingerprint: actualFingerprint,
|
|
2996
|
-
auto_approved: autoApproved,
|
|
2997
|
-
policy_rule_id: effectivePolicyRuleId,
|
|
2998
|
-
policy_fingerprint: effectivePolicyFingerprint
|
|
2999
|
-
});
|
|
3000
|
-
if (autoApproved) {
|
|
3001
|
-
appendEvent(logPath, {
|
|
3002
|
-
ts: new Date().toISOString(),
|
|
3127
|
+
runtimeLog("info", "terminal_approval_send", {
|
|
3003
3128
|
conversation_id: conversation.conversation_id,
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
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,
|
|
3007
3135
|
approval_fingerprint: actualFingerprint,
|
|
3136
|
+
auto_approved: autoApproved,
|
|
3008
3137
|
policy_rule_id: effectivePolicyRuleId,
|
|
3009
3138
|
policy_fingerprint: effectivePolicyFingerprint
|
|
3010
3139
|
});
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
terminal_bridge_last_approval_prompt_cleared_at
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
approved: true,
|
|
3088
|
-
terminal_control: terminalControl,
|
|
3089
|
-
key: approval.key,
|
|
3090
|
-
keys: approval.keys,
|
|
3091
|
-
label: approval.label,
|
|
3092
|
-
decision_mode: approval.decisionMode,
|
|
3093
|
-
request_id: approval.requestId,
|
|
3094
|
-
approval_fingerprint: actualFingerprint,
|
|
3095
|
-
auto_approved: autoApproved,
|
|
3096
|
-
policy_rule_id: effectivePolicyRuleId,
|
|
3097
|
-
policy_fingerprint: effectivePolicyFingerprint,
|
|
3098
|
-
monitor_pid: bridgeMonitor.monitorPid ?? null,
|
|
3099
|
-
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
|
+
});
|
|
3100
3216
|
});
|
|
3101
3217
|
}
|
|
3102
3218
|
finally {
|
|
@@ -3171,7 +3287,7 @@ async function runTerminalConversationApprove({ options, conversationId, agent,
|
|
|
3171
3287
|
releaseTerminalLock();
|
|
3172
3288
|
}
|
|
3173
3289
|
}
|
|
3174
|
-
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 }) {
|
|
3175
3291
|
const bridge = terminalBridgeEnabled(conversation);
|
|
3176
3292
|
if (!terminalSendLockHeld) {
|
|
3177
3293
|
const releaseTerminalLock = acquireFileLock(terminalBridgeSendLockPath(storeDirFromOptions(options), terminalControl), { timeoutMs: 30000 });
|
|
@@ -3187,6 +3303,7 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
3187
3303
|
terminalControl,
|
|
3188
3304
|
terminalSendLockHeld: true,
|
|
3189
3305
|
terminalStateLockHeld,
|
|
3306
|
+
storeWriterLeaseHeld,
|
|
3190
3307
|
recordMessageAfterSend,
|
|
3191
3308
|
recordRawAttachmentAfterSend
|
|
3192
3309
|
});
|
|
@@ -3222,6 +3339,7 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
3222
3339
|
terminalControl,
|
|
3223
3340
|
terminalSendLockHeld: true,
|
|
3224
3341
|
terminalStateLockHeld: true,
|
|
3342
|
+
storeWriterLeaseHeld,
|
|
3225
3343
|
recordMessageAfterSend,
|
|
3226
3344
|
recordRawAttachmentAfterSend
|
|
3227
3345
|
});
|
|
@@ -3230,6 +3348,24 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
3230
3348
|
releaseStateLock();
|
|
3231
3349
|
}
|
|
3232
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
|
+
}
|
|
3233
3369
|
const terminalBridge = createTerminalAgentBridge(options);
|
|
3234
3370
|
const bridgeStartedAt = new Date().toISOString();
|
|
3235
3371
|
const agentTimeoutMinutes = Number(options.agentTimeoutMinutes ?? DEFAULT_AGENT_TIMEOUT_MINUTES);
|
|
@@ -3744,7 +3880,6 @@ function terminalSubmissionPayload(payload) {
|
|
|
3744
3880
|
function createManagedTerminalConversationFromRawId({ options, conversationId, agent, pid, messageBody, terminalControl }) {
|
|
3745
3881
|
const workspace = terminalControl.currentPath ?? process.cwd();
|
|
3746
3882
|
const storeDir = expandHome(options.storeDir ?? options.logDir ?? defaultStoreDir(workspace));
|
|
3747
|
-
cleanupIdleConversations(storeDir, options);
|
|
3748
3883
|
const executor = resolveExecutor({
|
|
3749
3884
|
kind: agent,
|
|
3750
3885
|
session: conversationId
|
|
@@ -3969,8 +4104,15 @@ async function runRenew(options) {
|
|
|
3969
4104
|
});
|
|
3970
4105
|
}
|
|
3971
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 }) {
|
|
3972
4114
|
const storeDir = storeDirFromOptions(options);
|
|
3973
|
-
const conversations = listConversations(storeDir);
|
|
4115
|
+
const conversations = listConversations(storeDir).filter((conversation) => conversationId === undefined || conversation.conversation_id === conversationId);
|
|
3974
4116
|
const items = [];
|
|
3975
4117
|
let ignored = 0;
|
|
3976
4118
|
let launched = 0;
|
|
@@ -3987,30 +4129,32 @@ async function runReconcileMonitors(options) {
|
|
|
3987
4129
|
const logPath = expandHome(stringValue(listedConversation.event_log_path) ??
|
|
3988
4130
|
logPathForStatePath(statePath));
|
|
3989
4131
|
try {
|
|
3990
|
-
|
|
3991
|
-
|
|
3992
|
-
|
|
3993
|
-
|
|
3994
|
-
|
|
3995
|
-
if (callbackRecovery.handled) {
|
|
3996
|
-
if (callbackRecovery.status === "launched") {
|
|
3997
|
-
launched += 1;
|
|
3998
|
-
}
|
|
3999
|
-
else if (callbackRecovery.status === "already_running") {
|
|
4000
|
-
alreadyRunning += 1;
|
|
4001
|
-
}
|
|
4002
|
-
else {
|
|
4003
|
-
skipped += 1;
|
|
4004
|
-
}
|
|
4005
|
-
items.push({
|
|
4006
|
-
conversation_id: callbackRecovery.conversationId,
|
|
4007
|
-
status: callbackRecovery.status,
|
|
4008
|
-
reason: callbackRecovery.reason,
|
|
4009
|
-
...(callbackRecovery.monitorPid === undefined
|
|
4010
|
-
? {}
|
|
4011
|
-
: { monitor_pid: callbackRecovery.monitorPid })
|
|
4132
|
+
if (includeCallbackRecovery) {
|
|
4133
|
+
const callbackRecovery = prepareCallbackDeliveryReconciliation({
|
|
4134
|
+
statePath,
|
|
4135
|
+
logPath,
|
|
4136
|
+
delayMs: options.callbackRetryDelayMs
|
|
4012
4137
|
});
|
|
4013
|
-
|
|
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
|
+
}
|
|
4014
4158
|
}
|
|
4015
4159
|
const listedNativeTakeover = isRecord(listedConversation.native_session_takeover)
|
|
4016
4160
|
? listedConversation.native_session_takeover
|
|
@@ -4125,7 +4269,7 @@ async function runReconcileMonitors(options) {
|
|
|
4125
4269
|
event: "terminal_bridge_monitor_launch",
|
|
4126
4270
|
pid: monitor.pid ?? null,
|
|
4127
4271
|
terminal_control: prepared.terminalControl,
|
|
4128
|
-
reason
|
|
4272
|
+
reason,
|
|
4129
4273
|
agent_timeout_minutes: prepared.inactivityTimeoutMinutes,
|
|
4130
4274
|
agent_hard_timeout_minutes: prepared.hardTimeoutMinutes
|
|
4131
4275
|
});
|
|
@@ -4138,7 +4282,7 @@ async function runReconcileMonitors(options) {
|
|
|
4138
4282
|
items.push({
|
|
4139
4283
|
conversation_id: prepared.conversation.conversation_id,
|
|
4140
4284
|
status: "launched",
|
|
4141
|
-
reason
|
|
4285
|
+
reason,
|
|
4142
4286
|
monitor_pid: monitor.pid ?? null
|
|
4143
4287
|
});
|
|
4144
4288
|
}
|
|
@@ -4151,7 +4295,7 @@ async function runReconcileMonitors(options) {
|
|
|
4151
4295
|
});
|
|
4152
4296
|
}
|
|
4153
4297
|
}
|
|
4154
|
-
|
|
4298
|
+
return {
|
|
4155
4299
|
reconciled: true,
|
|
4156
4300
|
store_dir: storeDir,
|
|
4157
4301
|
checked: conversations.length,
|
|
@@ -4161,7 +4305,7 @@ async function runReconcileMonitors(options) {
|
|
|
4161
4305
|
skipped,
|
|
4162
4306
|
errors,
|
|
4163
4307
|
items
|
|
4164
|
-
}
|
|
4308
|
+
};
|
|
4165
4309
|
}
|
|
4166
4310
|
function prepareCallbackDeliveryReconciliation({ statePath, logPath, delayMs }) {
|
|
4167
4311
|
const releaseStateLock = acquireFileLock(`${statePath}.lock`);
|
|
@@ -4404,7 +4548,6 @@ function positiveMinutes(value, optionName) {
|
|
|
4404
4548
|
return parsed;
|
|
4405
4549
|
}
|
|
4406
4550
|
async function runCancel(options) {
|
|
4407
|
-
cleanupIdleConversations(storeDirFromOptions(options), options);
|
|
4408
4551
|
const terminalConversation = await resolveTerminalConversationFromOptions(options);
|
|
4409
4552
|
if (terminalConversation) {
|
|
4410
4553
|
await runTerminalConversationCancel({
|
|
@@ -4484,75 +4627,78 @@ async function runTerminalControlCancel({ options, statePath, logPath, agent, te
|
|
|
4484
4627
|
let releaseStateLock;
|
|
4485
4628
|
try {
|
|
4486
4629
|
releaseStateLock = acquireFileLock(`${statePath}.lock`);
|
|
4487
|
-
const
|
|
4488
|
-
|
|
4489
|
-
|
|
4490
|
-
|
|
4491
|
-
|
|
4492
|
-
|
|
4493
|
-
|
|
4494
|
-
|
|
4495
|
-
|
|
4496
|
-
currentControl
|
|
4497
|
-
currentControl
|
|
4498
|
-
|
|
4499
|
-
|
|
4500
|
-
|
|
4501
|
-
|
|
4502
|
-
|
|
4503
|
-
action: "cancel"
|
|
4504
|
-
});
|
|
4505
|
-
const cancellation = await createTerminalAgentBridge(options).cancel(agent, currentControl, {
|
|
4506
|
-
runtime: terminalRuntimeIdentityForConversation(currentConversation, currentControl),
|
|
4507
|
-
scrollbackLines: Number(options.scrollbackLines ?? 120)
|
|
4508
|
-
});
|
|
4509
|
-
if (!cancellation.cancelRequested) {
|
|
4510
|
-
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({
|
|
4511
4646
|
conversation: currentConversation,
|
|
4512
|
-
|
|
4513
|
-
|
|
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",
|
|
4514
4669
|
terminal_control: currentControl,
|
|
4515
|
-
|
|
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)
|
|
4516
4701
|
});
|
|
4517
|
-
return;
|
|
4518
|
-
}
|
|
4519
|
-
const now = new Date().toISOString();
|
|
4520
|
-
appendEvent(logPath, {
|
|
4521
|
-
ts: now,
|
|
4522
|
-
conversation_id: currentConversation.conversation_id,
|
|
4523
|
-
event: "terminal_cancel_requested",
|
|
4524
|
-
terminal_control: currentControl,
|
|
4525
|
-
key: cancellation.key,
|
|
4526
|
-
keys: cancellation.keys,
|
|
4527
|
-
denied_approval: cancellation.deniedApproval,
|
|
4528
|
-
request_id: cancellation.requestId
|
|
4529
|
-
});
|
|
4530
|
-
runtimeLog("info", "terminal_cancel_requested", {
|
|
4531
|
-
conversation_id: currentConversation.conversation_id,
|
|
4532
|
-
agent,
|
|
4533
|
-
terminal_target: currentControl.target,
|
|
4534
|
-
key: cancellation.key,
|
|
4535
|
-
keys: cancellation.keys,
|
|
4536
|
-
denied_approval: cancellation.deniedApproval,
|
|
4537
|
-
request_id: cancellation.requestId
|
|
4538
|
-
});
|
|
4539
|
-
const nextConversation = {
|
|
4540
|
-
...currentConversation,
|
|
4541
|
-
status: "cancelled",
|
|
4542
|
-
cancelled_at: now,
|
|
4543
|
-
terminal_cancel_requested_at: now,
|
|
4544
|
-
updated_at: now
|
|
4545
|
-
};
|
|
4546
|
-
saveState(statePath, nextConversation);
|
|
4547
|
-
printJson({
|
|
4548
|
-
conversation: nextConversation,
|
|
4549
|
-
cancel_requested: true,
|
|
4550
|
-
terminal_control: currentControl,
|
|
4551
|
-
key: cancellation.key,
|
|
4552
|
-
keys: cancellation.keys,
|
|
4553
|
-
denied_approval: cancellation.deniedApproval,
|
|
4554
|
-
request_id: cancellation.requestId,
|
|
4555
|
-
budget: budgetAction(nextConversation)
|
|
4556
4702
|
});
|
|
4557
4703
|
}
|
|
4558
4704
|
finally {
|
|
@@ -5983,7 +6129,7 @@ function terminalBridgeRuntimeDir() {
|
|
|
5983
6129
|
const configured = stringValue(process.env.AKK_RUNTIME_DIR);
|
|
5984
6130
|
return configured
|
|
5985
6131
|
? path.resolve(expandHome(configured))
|
|
5986
|
-
: path.join(path.dirname(defaultStoreDir()), "runtime");
|
|
6132
|
+
: path.join(path.dirname(defaultStoreDir()), "runtime-v2");
|
|
5987
6133
|
}
|
|
5988
6134
|
function terminalBridgeRuntimeKey(terminalControl) {
|
|
5989
6135
|
return createHash("sha256")
|
|
@@ -5996,7 +6142,6 @@ function terminalBridgeRuntimeKey(terminalControl) {
|
|
|
5996
6142
|
}
|
|
5997
6143
|
function terminalBridgeDispatchLedgerPath(terminalControl) {
|
|
5998
6144
|
const ledgerDir = path.join(terminalBridgeRuntimeDir(), "terminal-dispatch");
|
|
5999
|
-
ensureDir(ledgerDir);
|
|
6000
6145
|
return path.join(ledgerDir, `terminal-dispatch-${terminalBridgeRuntimeKey(terminalControl)}.json`);
|
|
6001
6146
|
}
|
|
6002
6147
|
function loadTerminalBridgeDispatchLedger(terminalControl) {
|
|
@@ -6043,6 +6188,7 @@ function orphanedTerminalDispatchForRecovery(terminalControl) {
|
|
|
6043
6188
|
}
|
|
6044
6189
|
function saveTerminalBridgeDispatchLedger(terminalControl, ledger) {
|
|
6045
6190
|
const ledgerPath = terminalBridgeDispatchLedgerPath(terminalControl);
|
|
6191
|
+
ensureDir(path.dirname(ledgerPath));
|
|
6046
6192
|
if (fs.existsSync(ledgerPath) && fs.lstatSync(ledgerPath).isSymbolicLink()) {
|
|
6047
6193
|
throw new Error(`terminal dispatch ledger is a symlink: ${ledgerPath}`);
|
|
6048
6194
|
}
|
|
@@ -7046,6 +7192,7 @@ function runPreparedCallback(prepared, { emit = true } = {}) {
|
|
|
7046
7192
|
}
|
|
7047
7193
|
return result;
|
|
7048
7194
|
}
|
|
7195
|
+
assertStoreWriterCompatible(pathsForConversationDir(path.dirname(prepared.statePath)).storeDir);
|
|
7049
7196
|
try {
|
|
7050
7197
|
const deliveryKind = deliverCallbackToOpenClaw({
|
|
7051
7198
|
options: prepared.options,
|
|
@@ -7591,7 +7738,13 @@ function loadConversationFromOptions(options) {
|
|
|
7591
7738
|
throw new Error("--conversation or --state is required");
|
|
7592
7739
|
}
|
|
7593
7740
|
const conversation = options.state
|
|
7594
|
-
?
|
|
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
|
+
})()
|
|
7595
7748
|
: loadConversationById(conversationId, storeDir);
|
|
7596
7749
|
assertConfiguredWorkspace(options.workspace, conversation.workspace, `access to AKK conversation ${conversation.conversation_id}`);
|
|
7597
7750
|
return {
|
|
@@ -8204,13 +8357,21 @@ function deliverStalledNotification({ statePath, logPath, conversation, message,
|
|
|
8204
8357
|
stderr: textSummary(chatSendDelivery.stderr)
|
|
8205
8358
|
});
|
|
8206
8359
|
}
|
|
8207
|
-
function
|
|
8360
|
+
function reconcileIdleConversations(storeDir, options = {}, now = new Date(), conversationId) {
|
|
8208
8361
|
const timeoutMinutes = Number(options.idleTimeoutMinutes ?? DEFAULT_IDLE_TIMEOUT_MINUTES);
|
|
8209
8362
|
if (!Number.isFinite(timeoutMinutes) || timeoutMinutes <= 0) {
|
|
8210
|
-
return {
|
|
8363
|
+
return {
|
|
8364
|
+
checked: 0,
|
|
8365
|
+
closed: 0,
|
|
8366
|
+
skipped: 0,
|
|
8367
|
+
idle_timeout_minutes: timeoutMinutes
|
|
8368
|
+
};
|
|
8211
8369
|
}
|
|
8212
|
-
|
|
8370
|
+
ensureStoreWritable(storeDir);
|
|
8371
|
+
const conversations = listConversations(storeDir).filter((conversation) => (conversationId === undefined || conversation.conversation_id === conversationId) &&
|
|
8372
|
+
matchesConfiguredWorkspace(options.workspace, conversation.workspace));
|
|
8213
8373
|
let closed = 0;
|
|
8374
|
+
let skipped = 0;
|
|
8214
8375
|
for (const listedConversation of conversations) {
|
|
8215
8376
|
if (listedConversation.status !== "idle" || !listedConversation.idle_since) {
|
|
8216
8377
|
continue;
|
|
@@ -8230,6 +8391,7 @@ function cleanupIdleConversations(storeDir, options = {}, now = new Date()) {
|
|
|
8230
8391
|
}
|
|
8231
8392
|
catch (error) {
|
|
8232
8393
|
if (isRecord(error) && error.code === "LOCK_TIMEOUT") {
|
|
8394
|
+
skipped += 1;
|
|
8233
8395
|
continue;
|
|
8234
8396
|
}
|
|
8235
8397
|
throw error;
|
|
@@ -8288,6 +8450,7 @@ function cleanupIdleConversations(storeDir, options = {}, now = new Date()) {
|
|
|
8288
8450
|
return {
|
|
8289
8451
|
checked: conversations.length,
|
|
8290
8452
|
closed,
|
|
8453
|
+
skipped,
|
|
8291
8454
|
idle_timeout_minutes: timeoutMinutes
|
|
8292
8455
|
};
|
|
8293
8456
|
}
|
|
@@ -8754,8 +8917,8 @@ function usage() {
|
|
|
8754
8917
|
agent-knock-knock --help
|
|
8755
8918
|
agent-knock-knock --version
|
|
8756
8919
|
agent-knock-knock delegate --request <text> [--agent ${agentList}] [--workspace <path>] [--store-dir <dir>]
|
|
8757
|
-
agent-knock-knock list [--store-dir <dir>] [--agent ${agentList}] [--status <status>] [--all] [--managed-only] [--no-approval-scan] [--terminal-debug]
|
|
8758
|
-
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]
|
|
8759
8922
|
agent-knock-knock send [--conversation <id|selector>] --message <text> [--type answer|task|control] [--agent-timeout-minutes <minutes>] [--agent-hard-timeout-minutes <minutes>]
|
|
8760
8923
|
agent-knock-knock approve [--conversation <id|selector>] --expected-approval-fingerprint <fingerprint>
|
|
8761
8924
|
agent-knock-knock cancel [--conversation <id|selector>]
|