@scotthuang/agent-knock-knock 0.6.2 → 0.8.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 +25 -0
- package/README.md +20 -6
- package/dist/src/cli.js +1150 -508
- package/dist/src/cli.js.map +1 -1
- package/dist/src/openclaw-plugin-helpers.js +86 -46
- 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 +175 -172
- package/dist/src/openclaw-plugin.js.map +1 -1
- package/dist/src/session-selector.d.ts +18 -0
- package/dist/src/session-selector.js +18 -9
- package/dist/src/session-selector.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/docs/quickstart-tmux.md +4 -2
- package/openclaw.plugin.json +3 -7
- package/package.json +1 -1
- package/templates/openclaw-skills/agent-knock-knock/SKILL.md +12 -10
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;
|
|
@@ -907,7 +930,15 @@ async function runDelegate(options) {
|
|
|
907
930
|
return false;
|
|
908
931
|
}
|
|
909
932
|
});
|
|
910
|
-
const eligible = scopedCandidates.filter((candidate) =>
|
|
933
|
+
const eligible = scopedCandidates.filter((candidate) => {
|
|
934
|
+
if (candidate.activity_state !== "idle") {
|
|
935
|
+
return false;
|
|
936
|
+
}
|
|
937
|
+
const terminalControl = isRecord(candidate.terminal_control)
|
|
938
|
+
? candidate.terminal_control
|
|
939
|
+
: undefined;
|
|
940
|
+
return !terminalControl || terminalDispatchOwnership(terminalControl).state === "none";
|
|
941
|
+
});
|
|
911
942
|
if (eligible.length === 0) {
|
|
912
943
|
const observed = scopedCandidates.length > 0
|
|
913
944
|
? ` Found ${scopedCandidates.length} matching pane(s), but none is idle.`
|
|
@@ -1327,61 +1358,90 @@ function environmentWithoutGatewayTokens() {
|
|
|
1327
1358
|
}
|
|
1328
1359
|
async function runList(options) {
|
|
1329
1360
|
const storeDir = expandHome(options.storeDir ?? options.logDir ?? defaultStoreDir(process.cwd()));
|
|
1330
|
-
const
|
|
1361
|
+
const store = inspectStoreCompatibility(storeDir);
|
|
1362
|
+
const reconciliation = options.reconcile === true
|
|
1363
|
+
? await reconcileStoreForList(storeDir, options)
|
|
1364
|
+
: {
|
|
1365
|
+
status: "disabled",
|
|
1366
|
+
reason: "standalone list is read-only unless --reconcile is supplied"
|
|
1367
|
+
};
|
|
1331
1368
|
const includeAll = Boolean(options.all);
|
|
1332
1369
|
const agentFilter = options.agent ? resolveExecutor({ kind: options.agent }).kind : undefined;
|
|
1333
1370
|
const statusFilter = options.status;
|
|
1334
1371
|
const allStoredConversations = listConversations(storeDir);
|
|
1335
|
-
const
|
|
1336
|
-
.filter(isDiscoverableTmuxConversation)
|
|
1372
|
+
const allManagedConversations = allStoredConversations
|
|
1373
|
+
.filter(isDiscoverableTmuxConversation);
|
|
1374
|
+
const storedConversations = allManagedConversations
|
|
1337
1375
|
.filter((conversation) => includeAll || isActiveStatus(conversation.status))
|
|
1338
1376
|
.filter((conversation) => matchesConfiguredWorkspace(options.workspace, conversation.workspace))
|
|
1339
1377
|
.filter((conversation) => !agentFilter || executorForConversation(conversation).kind === agentFilter)
|
|
1340
1378
|
.filter((conversation) => !statusFilter || conversation.status === statusFilter);
|
|
1341
|
-
const conversations = storedConversations.map((conversation) => summarizeConversation(conversation));
|
|
1342
|
-
const delegated = storedConversations.map((conversation) => delegatedListEntry(summarizeConversation(conversation), {
|
|
1343
|
-
terminalBridge: terminalBridgeEnabled(conversation),
|
|
1344
|
-
approvalState: managedListApprovalState(conversation),
|
|
1345
|
-
conversation
|
|
1346
|
-
}));
|
|
1347
1379
|
const terminalScan = await buildTerminalListGroup({ options, agentFilter, statusFilter });
|
|
1348
|
-
const
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
.
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
.
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
return false;
|
|
1358
|
-
}
|
|
1359
|
-
const key = terminalControlSelectorKey(entry.terminal_control);
|
|
1360
|
-
return key === undefined || !managedTerminalKeys.has(key);
|
|
1380
|
+
const physicalTerminals = terminalScan.terminalControlled.filter((entry) => matchesConfiguredWorkspace(options.workspace, entry.workspace ?? entry.cwd));
|
|
1381
|
+
const projection = terminalFirstListProjection({
|
|
1382
|
+
terminals: physicalTerminals,
|
|
1383
|
+
allConversations: allManagedConversations.filter((conversation) => matchesConfiguredWorkspace(options.workspace, conversation.workspace)),
|
|
1384
|
+
displayedConversations: storedConversations,
|
|
1385
|
+
includeAll,
|
|
1386
|
+
managedOnly: options.managedOnly === true,
|
|
1387
|
+
statusFilter,
|
|
1388
|
+
mutationsAllowed: store.writable === true
|
|
1361
1389
|
});
|
|
1362
1390
|
printJson({
|
|
1363
1391
|
store_dir: storeDir,
|
|
1364
|
-
|
|
1392
|
+
store,
|
|
1393
|
+
reconciliation,
|
|
1365
1394
|
action_contracts: listActionContracts(),
|
|
1366
|
-
|
|
1367
|
-
|
|
1395
|
+
terminals: projection.terminals,
|
|
1396
|
+
unavailable_managed_turns: projection.unavailableManagedTurns,
|
|
1368
1397
|
terminal_scan: {
|
|
1369
1398
|
...terminalScan.summary,
|
|
1370
|
-
|
|
1371
|
-
}
|
|
1372
|
-
tasks: conversations
|
|
1399
|
+
terminal_count: projection.terminals.length
|
|
1400
|
+
}
|
|
1373
1401
|
});
|
|
1374
|
-
runtimeLog("info", "
|
|
1402
|
+
runtimeLog("info", "terminals_listed", {
|
|
1375
1403
|
store_dir: storeDir,
|
|
1376
|
-
|
|
1377
|
-
|
|
1404
|
+
terminal_count: projection.terminals.length,
|
|
1405
|
+
unavailable_managed_turn_count: projection.unavailableManagedTurns.length,
|
|
1378
1406
|
terminal_scan_error: terminalScan.summary.error,
|
|
1379
1407
|
include_all: includeAll,
|
|
1380
1408
|
agent_filter: agentFilter,
|
|
1381
1409
|
status_filter: statusFilter,
|
|
1382
|
-
|
|
1410
|
+
reconciliation
|
|
1383
1411
|
});
|
|
1384
1412
|
}
|
|
1413
|
+
async function reconcileStoreForList(storeDir, options) {
|
|
1414
|
+
try {
|
|
1415
|
+
ensureStoreWritable(storeDir);
|
|
1416
|
+
}
|
|
1417
|
+
catch (error) {
|
|
1418
|
+
if (isRecord(error) && error.code === "AKK_STORE_INCOMPATIBLE") {
|
|
1419
|
+
return {
|
|
1420
|
+
status: "skipped",
|
|
1421
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
1422
|
+
store: inspectStoreCompatibility(storeDir)
|
|
1423
|
+
};
|
|
1424
|
+
}
|
|
1425
|
+
throw error;
|
|
1426
|
+
}
|
|
1427
|
+
const idle = reconcileIdleConversations(storeDir, options);
|
|
1428
|
+
const monitors = await reconcileMonitors(options, {
|
|
1429
|
+
includeCallbackRecovery: false,
|
|
1430
|
+
reason: "list_reconciliation",
|
|
1431
|
+
conversationId: undefined
|
|
1432
|
+
});
|
|
1433
|
+
return {
|
|
1434
|
+
status: "completed",
|
|
1435
|
+
checked: Math.max(idle.checked, monitors.checked),
|
|
1436
|
+
changed: idle.closed + monitors.launched,
|
|
1437
|
+
closed: idle.closed,
|
|
1438
|
+
monitors_launched: monitors.launched,
|
|
1439
|
+
monitors_already_running: monitors.already_running,
|
|
1440
|
+
skipped: idle.skipped + monitors.skipped,
|
|
1441
|
+
errors: monitors.errors,
|
|
1442
|
+
idle_timeout_minutes: idle.idle_timeout_minutes
|
|
1443
|
+
};
|
|
1444
|
+
}
|
|
1385
1445
|
async function buildTerminalListGroup({ options, agentFilter, statusFilter }) {
|
|
1386
1446
|
const empty = {
|
|
1387
1447
|
terminalControlled: [],
|
|
@@ -1394,16 +1454,6 @@ async function buildTerminalListGroup({ options, agentFilter, statusFilter }) {
|
|
|
1394
1454
|
if (options.managedOnly) {
|
|
1395
1455
|
return empty;
|
|
1396
1456
|
}
|
|
1397
|
-
if (statusFilter && statusFilter !== "active") {
|
|
1398
|
-
return {
|
|
1399
|
-
...empty,
|
|
1400
|
-
summary: {
|
|
1401
|
-
enabled: false,
|
|
1402
|
-
agents: [],
|
|
1403
|
-
skipped: `terminal discovery skipped for status filter ${statusFilter}`
|
|
1404
|
-
}
|
|
1405
|
-
};
|
|
1406
|
-
}
|
|
1407
1457
|
const registry = createRuntimeTerminalAgentRegistry(options);
|
|
1408
1458
|
const adapters = agentFilter
|
|
1409
1459
|
? [registry.get(agentFilter)].filter((adapter) => adapter !== undefined)
|
|
@@ -1446,7 +1496,7 @@ async function buildTerminalListGroup({ options, agentFilter, statusFilter }) {
|
|
|
1446
1496
|
enabled: true,
|
|
1447
1497
|
agents: adapters.map((adapter) => adapter.agent),
|
|
1448
1498
|
active_count: activeCount,
|
|
1449
|
-
|
|
1499
|
+
terminal_count: terminalControlled.length,
|
|
1450
1500
|
approval_scan: options.noApprovalScan ? "disabled" : "enabled",
|
|
1451
1501
|
diagnostics: terminalDiagnostics,
|
|
1452
1502
|
error: errors.length > 0 ? errors.join("; ") : undefined
|
|
@@ -1462,24 +1512,26 @@ async function terminalControlDiagnostics(provider) {
|
|
|
1462
1512
|
paneCount: (await provider.listPanes()).length
|
|
1463
1513
|
};
|
|
1464
1514
|
}
|
|
1465
|
-
function
|
|
1515
|
+
function managedTurnListEntry(task, { terminalBridge = false, approvalState, conversation } = {}) {
|
|
1466
1516
|
const entry = {
|
|
1467
1517
|
...task,
|
|
1468
1518
|
id: task.conversation_id,
|
|
1469
1519
|
short_ref: sessionShortRef(task.conversation_id),
|
|
1470
|
-
source: "
|
|
1520
|
+
source: "managed_turn",
|
|
1471
1521
|
...(approvalState ? { approval_state: approvalState } : {}),
|
|
1472
1522
|
commands: {
|
|
1473
|
-
send:
|
|
1523
|
+
send: canFollowUpManagedTurn(task.status),
|
|
1474
1524
|
cancel: isWaitingForAgent(task.status),
|
|
1475
1525
|
close: task.status !== "closed",
|
|
1476
1526
|
status: true,
|
|
1477
1527
|
approve: terminalBridge && isActiveStatus(task.status)
|
|
1478
1528
|
}
|
|
1479
1529
|
};
|
|
1530
|
+
const availableActions = availableListActions(entry, { conversation });
|
|
1531
|
+
const { commands: _commands, ...publicEntry } = entry;
|
|
1480
1532
|
return {
|
|
1481
|
-
...
|
|
1482
|
-
available_actions:
|
|
1533
|
+
...publicEntry,
|
|
1534
|
+
available_actions: availableActions
|
|
1483
1535
|
};
|
|
1484
1536
|
}
|
|
1485
1537
|
async function terminalControlledListEntry(session, activeSessions, options, bridge = createTerminalAgentBridge(options)) {
|
|
@@ -1497,9 +1549,9 @@ async function terminalControlledListEntry(session, activeSessions, options, bri
|
|
|
1497
1549
|
const entry = {
|
|
1498
1550
|
id: bridge.terminalConversationId(session),
|
|
1499
1551
|
short_ref: sessionShortRef(bridge.terminalConversationId(session)),
|
|
1500
|
-
source: "
|
|
1552
|
+
source: "terminal",
|
|
1501
1553
|
agent: session.agent,
|
|
1502
|
-
|
|
1554
|
+
process_state: "active",
|
|
1503
1555
|
pid: session.pid,
|
|
1504
1556
|
child_pids: childPidsForRoot(session, activeSessions),
|
|
1505
1557
|
command: session.command,
|
|
@@ -1533,9 +1585,438 @@ async function terminalControlledListEntry(session, activeSessions, options, bri
|
|
|
1533
1585
|
close: orphanedDispatch !== undefined
|
|
1534
1586
|
}
|
|
1535
1587
|
};
|
|
1588
|
+
const availableActions = availableListActions(entry);
|
|
1589
|
+
const { commands: _commands, ...publicEntry } = entry;
|
|
1590
|
+
return {
|
|
1591
|
+
...publicEntry,
|
|
1592
|
+
available_actions: availableActions
|
|
1593
|
+
};
|
|
1594
|
+
}
|
|
1595
|
+
function terminalFirstListProjection({ terminals, allConversations, displayedConversations, includeAll, managedOnly, statusFilter, mutationsAllowed }) {
|
|
1596
|
+
const allByTerminal = managedConversationsByTerminal(allConversations);
|
|
1597
|
+
const displayedByTerminal = managedConversationsByTerminal(displayedConversations);
|
|
1598
|
+
const discoveredTerminalKeys = new Set();
|
|
1599
|
+
const projectedTerminals = terminals.map((terminal) => {
|
|
1600
|
+
const terminalControl = isRecord(terminal.terminal_control)
|
|
1601
|
+
? terminal.terminal_control
|
|
1602
|
+
: undefined;
|
|
1603
|
+
const terminalKey = terminalControlSelectorKey(terminalControl);
|
|
1604
|
+
if (terminalKey) {
|
|
1605
|
+
discoveredTerminalKeys.add(terminalKey);
|
|
1606
|
+
}
|
|
1607
|
+
const allRelated = terminalKey
|
|
1608
|
+
? [...(allByTerminal.get(terminalKey) ?? [])]
|
|
1609
|
+
: [];
|
|
1610
|
+
const displayedRelated = terminalKey
|
|
1611
|
+
? [...(displayedByTerminal.get(terminalKey) ?? [])]
|
|
1612
|
+
: [];
|
|
1613
|
+
const discoveredOwnership = terminalControl
|
|
1614
|
+
? terminalDispatchOwnership(terminalControl)
|
|
1615
|
+
: { state: "none" };
|
|
1616
|
+
const ownership = discoveredOwnership.state === "current"
|
|
1617
|
+
? localTerminalDispatchOwnership(discoveredOwnership.conversation, allRelated, terminal)
|
|
1618
|
+
: discoveredOwnership;
|
|
1619
|
+
const discoveredRawActions = isRecord(terminal.available_actions)
|
|
1620
|
+
? terminal.available_actions
|
|
1621
|
+
: {};
|
|
1622
|
+
const rawActions = mutationsAllowed
|
|
1623
|
+
? discoveredRawActions
|
|
1624
|
+
: readOnlyListActions(discoveredRawActions);
|
|
1625
|
+
const terminalCanAcceptSend = ownership.state === "none" && isRecord(rawActions.send);
|
|
1626
|
+
if (ownership.state === "current" &&
|
|
1627
|
+
!allRelated.some((conversation) => conversation.conversation_id === ownership.conversation.conversation_id)) {
|
|
1628
|
+
allRelated.push(ownership.conversation);
|
|
1629
|
+
}
|
|
1630
|
+
const currentTurnValue = ownership.state === "current"
|
|
1631
|
+
? currentManagedTurnForTerminal(ownership.conversation, terminal, rawActions)
|
|
1632
|
+
: undefined;
|
|
1633
|
+
const currentTurn = currentTurnValue && !mutationsAllowed
|
|
1634
|
+
? readOnlyManagedTurn(currentTurnValue)
|
|
1635
|
+
: currentTurnValue;
|
|
1636
|
+
const sortedDisplayed = [...displayedRelated]
|
|
1637
|
+
.filter((conversation) => conversation.conversation_id !== currentTurn?.conversation_id)
|
|
1638
|
+
.sort(compareManagedConversationRecency);
|
|
1639
|
+
const recentConversation = currentTurn ? undefined : sortedDisplayed[0];
|
|
1640
|
+
const recentTurnValue = recentConversation
|
|
1641
|
+
? historicalManagedTurnForTerminal(recentConversation, terminalCanAcceptSend, terminal)
|
|
1642
|
+
: undefined;
|
|
1643
|
+
const recentTurn = recentTurnValue && !mutationsAllowed
|
|
1644
|
+
? readOnlyManagedTurn(recentTurnValue)
|
|
1645
|
+
: recentTurnValue;
|
|
1646
|
+
const historyConversations = includeAll
|
|
1647
|
+
? sortedDisplayed.filter((conversation) => conversation.conversation_id !== recentConversation?.conversation_id)
|
|
1648
|
+
: [];
|
|
1649
|
+
const history = historyConversations.map((conversation) => {
|
|
1650
|
+
const turn = historicalManagedTurnForTerminal(conversation, terminalCanAcceptSend, terminal);
|
|
1651
|
+
return mutationsAllowed ? turn : readOnlyManagedTurn(turn);
|
|
1652
|
+
});
|
|
1653
|
+
const visibleTurnIds = new Set([currentTurn, recentTurn, ...history]
|
|
1654
|
+
.map((turn) => stringValue(turn?.conversation_id))
|
|
1655
|
+
.filter((id) => id !== undefined));
|
|
1656
|
+
const management = {
|
|
1657
|
+
current_turn: currentTurn ?? null,
|
|
1658
|
+
recent_turn: recentTurn ?? null,
|
|
1659
|
+
turn_count: allRelated.length,
|
|
1660
|
+
hidden_turn_count: allRelated.filter((conversation) => !visibleTurnIds.has(conversation.conversation_id)).length,
|
|
1661
|
+
...(includeAll ? { history } : {})
|
|
1662
|
+
};
|
|
1663
|
+
const availableActions = ownership.state === "current"
|
|
1664
|
+
? currentTerminalActions(currentTurn)
|
|
1665
|
+
: ownership.state === "conflict"
|
|
1666
|
+
? safeTerminalActionsDuringConflict(rawActions)
|
|
1667
|
+
: rawActions;
|
|
1668
|
+
return {
|
|
1669
|
+
...terminal,
|
|
1670
|
+
management_state: ownership.state === "current"
|
|
1671
|
+
? "managed"
|
|
1672
|
+
: ownership.state === "conflict"
|
|
1673
|
+
? "conflict"
|
|
1674
|
+
: "unmanaged",
|
|
1675
|
+
...(ownership.state === "conflict"
|
|
1676
|
+
? { management_conflict: ownership.conflict }
|
|
1677
|
+
: {}),
|
|
1678
|
+
managed: management,
|
|
1679
|
+
available_actions: availableActions
|
|
1680
|
+
};
|
|
1681
|
+
});
|
|
1682
|
+
const unavailableManagedTurns = displayedConversations
|
|
1683
|
+
.filter((conversation) => {
|
|
1684
|
+
const terminalKey = terminalKeyForManagedConversation(conversation);
|
|
1685
|
+
if (terminalKey && discoveredTerminalKeys.has(terminalKey)) {
|
|
1686
|
+
return false;
|
|
1687
|
+
}
|
|
1688
|
+
return (includeAll ||
|
|
1689
|
+
managedOnly ||
|
|
1690
|
+
statusFilter !== undefined ||
|
|
1691
|
+
managedTurnNeedsAttention(conversation.status));
|
|
1692
|
+
})
|
|
1693
|
+
.sort(compareManagedConversationRecency)
|
|
1694
|
+
.map((conversation) => {
|
|
1695
|
+
const managedTurn = managedTurnListEntry(summarizeConversation(conversation), {
|
|
1696
|
+
terminalBridge: terminalBridgeEnabled(conversation),
|
|
1697
|
+
approvalState: managedListApprovalState(conversation),
|
|
1698
|
+
conversation
|
|
1699
|
+
});
|
|
1700
|
+
return {
|
|
1701
|
+
...managedTurn,
|
|
1702
|
+
available_actions: mutationsAllowed
|
|
1703
|
+
? safeUnavailableManagedTurnActions(isRecord(managedTurn.available_actions)
|
|
1704
|
+
? managedTurn.available_actions
|
|
1705
|
+
: {})
|
|
1706
|
+
: readOnlyListActions(isRecord(managedTurn.available_actions)
|
|
1707
|
+
? managedTurn.available_actions
|
|
1708
|
+
: {}),
|
|
1709
|
+
terminal_availability: {
|
|
1710
|
+
available: false,
|
|
1711
|
+
reason: managedOnly
|
|
1712
|
+
? "terminal discovery was disabled by --managed-only"
|
|
1713
|
+
: "the referenced tmux pane is not currently available"
|
|
1714
|
+
}
|
|
1715
|
+
};
|
|
1716
|
+
});
|
|
1717
|
+
return {
|
|
1718
|
+
terminals: projectedTerminals,
|
|
1719
|
+
unavailableManagedTurns
|
|
1720
|
+
};
|
|
1721
|
+
}
|
|
1722
|
+
function managedConversationsByTerminal(conversations) {
|
|
1723
|
+
const groups = new Map();
|
|
1724
|
+
for (const conversation of conversations) {
|
|
1725
|
+
const key = terminalKeyForManagedConversation(conversation);
|
|
1726
|
+
if (!key) {
|
|
1727
|
+
continue;
|
|
1728
|
+
}
|
|
1729
|
+
const group = groups.get(key) ?? [];
|
|
1730
|
+
group.push(conversation);
|
|
1731
|
+
groups.set(key, group);
|
|
1732
|
+
}
|
|
1733
|
+
return groups;
|
|
1734
|
+
}
|
|
1735
|
+
function terminalKeyForManagedConversation(conversation) {
|
|
1736
|
+
return terminalControlSelectorKey(terminalControlFromTakeover(isRecord(conversation.native_session_takeover)
|
|
1737
|
+
? conversation.native_session_takeover
|
|
1738
|
+
: undefined));
|
|
1739
|
+
}
|
|
1740
|
+
function compareManagedConversationRecency(left, right) {
|
|
1741
|
+
const leftTime = Date.parse(String(left.updated_at ?? left.created_at ?? ""));
|
|
1742
|
+
const rightTime = Date.parse(String(right.updated_at ?? right.created_at ?? ""));
|
|
1743
|
+
if (Number.isFinite(leftTime) && Number.isFinite(rightTime) && leftTime !== rightTime) {
|
|
1744
|
+
return rightTime - leftTime;
|
|
1745
|
+
}
|
|
1746
|
+
if (Number.isFinite(leftTime) !== Number.isFinite(rightTime)) {
|
|
1747
|
+
return Number.isFinite(leftTime) ? -1 : 1;
|
|
1748
|
+
}
|
|
1749
|
+
return left.conversation_id.localeCompare(right.conversation_id);
|
|
1750
|
+
}
|
|
1751
|
+
function managedTurnNeedsAttention(status) {
|
|
1752
|
+
return [
|
|
1753
|
+
"created",
|
|
1754
|
+
"running",
|
|
1755
|
+
"waiting_for_agent",
|
|
1756
|
+
"waiting_for_openclaw",
|
|
1757
|
+
"stalled",
|
|
1758
|
+
"callback_pending",
|
|
1759
|
+
"callback_failed",
|
|
1760
|
+
"cancelling"
|
|
1761
|
+
].includes(status);
|
|
1762
|
+
}
|
|
1763
|
+
function terminalDispatchOwnership(terminalControl) {
|
|
1764
|
+
let ledger;
|
|
1765
|
+
try {
|
|
1766
|
+
ledger = loadTerminalBridgeDispatchLedger(terminalControl);
|
|
1767
|
+
}
|
|
1768
|
+
catch (error) {
|
|
1769
|
+
return {
|
|
1770
|
+
state: "conflict",
|
|
1771
|
+
conflict: {
|
|
1772
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
1773
|
+
recovery: "inspect the shared tmux pane before performing a side effect"
|
|
1774
|
+
}
|
|
1775
|
+
};
|
|
1776
|
+
}
|
|
1777
|
+
if (!ledger || ledger.status === "resolved") {
|
|
1778
|
+
return { state: "none" };
|
|
1779
|
+
}
|
|
1780
|
+
const ledgerControl = isRecord(ledger.terminal_control)
|
|
1781
|
+
? ledger.terminal_control
|
|
1782
|
+
: undefined;
|
|
1783
|
+
const ledgerPanePid = Number(ledgerControl?.pane_pid);
|
|
1784
|
+
const currentPanePid = Number(terminalControl.panePid);
|
|
1785
|
+
if (Number.isSafeInteger(ledgerPanePid) &&
|
|
1786
|
+
ledgerPanePid > 0 &&
|
|
1787
|
+
Number.isSafeInteger(currentPanePid) &&
|
|
1788
|
+
currentPanePid > 0 &&
|
|
1789
|
+
ledgerPanePid !== currentPanePid) {
|
|
1790
|
+
return { state: "none" };
|
|
1791
|
+
}
|
|
1792
|
+
if (!["prepared", "submitted", "uncertain"].includes(String(ledger.status))) {
|
|
1793
|
+
return { state: "none" };
|
|
1794
|
+
}
|
|
1795
|
+
const owner = loadTerminalDispatchLedgerOwner(ledger);
|
|
1796
|
+
if (!owner) {
|
|
1797
|
+
return {
|
|
1798
|
+
state: "conflict",
|
|
1799
|
+
conflict: terminalDispatchConflict(ledger, "dispatch owner state is unavailable")
|
|
1800
|
+
};
|
|
1801
|
+
}
|
|
1802
|
+
if (TERMINAL_DISPATCH_RELEASE_STATUSES.has(owner.status)) {
|
|
1803
|
+
return { state: "none" };
|
|
1804
|
+
}
|
|
1805
|
+
const ownerTerminalKey = terminalKeyForManagedConversation(owner);
|
|
1806
|
+
const currentTerminalKey = terminalControlSelectorKey(terminalControl);
|
|
1807
|
+
if (!ownerTerminalKey || ownerTerminalKey !== currentTerminalKey) {
|
|
1808
|
+
return {
|
|
1809
|
+
state: "conflict",
|
|
1810
|
+
conflict: terminalDispatchConflict(ledger, "dispatch owner does not reference this tmux pane incarnation")
|
|
1811
|
+
};
|
|
1812
|
+
}
|
|
1813
|
+
const ownerTakeover = isRecord(owner.native_session_takeover)
|
|
1814
|
+
? owner.native_session_takeover
|
|
1815
|
+
: undefined;
|
|
1816
|
+
const ledgerMessageId = stringValue(ledger.message_id);
|
|
1817
|
+
const ownerMessageId = stringValue(ownerTakeover?.terminal_bridge_message_id);
|
|
1818
|
+
if (["prepared", "submitted", "uncertain"].includes(String(ledger.status)) &&
|
|
1819
|
+
ledgerMessageId &&
|
|
1820
|
+
ownerMessageId !== ledgerMessageId) {
|
|
1821
|
+
return {
|
|
1822
|
+
state: "conflict",
|
|
1823
|
+
conflict: terminalDispatchConflict(ledger, "dispatch generation does not match the owner state")
|
|
1824
|
+
};
|
|
1825
|
+
}
|
|
1826
|
+
return { state: "current", conversation: owner };
|
|
1827
|
+
}
|
|
1828
|
+
function terminalDispatchConflict(ledger, reason) {
|
|
1829
|
+
return {
|
|
1830
|
+
reason,
|
|
1831
|
+
dispatch_status: stringValue(ledger.status),
|
|
1832
|
+
owner_conversation_id: stringValue(ledger.conversation_id),
|
|
1833
|
+
message_id: stringValue(ledger.message_id),
|
|
1834
|
+
recovery: "inspect the shared tmux pane and explicitly resolve the current dispatch before performing a side effect"
|
|
1835
|
+
};
|
|
1836
|
+
}
|
|
1837
|
+
function localTerminalDispatchOwnership(ledgerOwner, localConversations, terminal) {
|
|
1838
|
+
const localOwner = localConversations.find((conversation) => conversation.conversation_id === ledgerOwner.conversation_id &&
|
|
1839
|
+
sameCanonicalStatePath(conversation.state_path, ledgerOwner.state_path));
|
|
1840
|
+
if (localOwner) {
|
|
1841
|
+
if (!managedTurnMatchesLiveTerminal(localOwner, terminal)) {
|
|
1842
|
+
return {
|
|
1843
|
+
state: "conflict",
|
|
1844
|
+
conflict: {
|
|
1845
|
+
reason: "the terminal dispatch owner no longer matches the live coding-agent process identity or workspace",
|
|
1846
|
+
owner_conversation_id: ledgerOwner.conversation_id,
|
|
1847
|
+
recovery: "inspect the shared tmux pane and explicitly resolve the stale dispatch before performing a side effect"
|
|
1848
|
+
}
|
|
1849
|
+
};
|
|
1850
|
+
}
|
|
1851
|
+
return { state: "current", conversation: localOwner };
|
|
1852
|
+
}
|
|
1536
1853
|
return {
|
|
1537
|
-
|
|
1538
|
-
|
|
1854
|
+
state: "conflict",
|
|
1855
|
+
conflict: {
|
|
1856
|
+
reason: "the terminal dispatch owner belongs to another AKK store or is not supported by this list view",
|
|
1857
|
+
owner_conversation_id: ledgerOwner.conversation_id,
|
|
1858
|
+
recovery: "inspect the shared tmux pane and use the AKK store that owns the current dispatch"
|
|
1859
|
+
}
|
|
1860
|
+
};
|
|
1861
|
+
}
|
|
1862
|
+
function sameCanonicalStatePath(left, right) {
|
|
1863
|
+
const leftPath = stringValue(left);
|
|
1864
|
+
const rightPath = stringValue(right);
|
|
1865
|
+
return Boolean(leftPath &&
|
|
1866
|
+
rightPath &&
|
|
1867
|
+
path.resolve(leftPath) === path.resolve(rightPath));
|
|
1868
|
+
}
|
|
1869
|
+
function currentTerminalActions(currentTurn) {
|
|
1870
|
+
if (!currentTurn || !isRecord(currentTurn.available_actions)) {
|
|
1871
|
+
return {};
|
|
1872
|
+
}
|
|
1873
|
+
const actions = {};
|
|
1874
|
+
for (const action of ["status", "approve", "cancel", "renew", "retry_callback"]) {
|
|
1875
|
+
if (isRecord(currentTurn.available_actions[action])) {
|
|
1876
|
+
actions[action] = currentTurn.available_actions[action];
|
|
1877
|
+
}
|
|
1878
|
+
}
|
|
1879
|
+
return actions;
|
|
1880
|
+
}
|
|
1881
|
+
function safeTerminalActionsDuringConflict(rawActions) {
|
|
1882
|
+
const actions = {};
|
|
1883
|
+
for (const action of ["status", "close"]) {
|
|
1884
|
+
if (isRecord(rawActions[action])) {
|
|
1885
|
+
actions[action] = rawActions[action];
|
|
1886
|
+
}
|
|
1887
|
+
}
|
|
1888
|
+
return actions;
|
|
1889
|
+
}
|
|
1890
|
+
function safeUnavailableManagedTurnActions(actionsValue) {
|
|
1891
|
+
const actions = {};
|
|
1892
|
+
for (const action of ["status", "retry_callback", "close"]) {
|
|
1893
|
+
if (isRecord(actionsValue[action])) {
|
|
1894
|
+
actions[action] = actionsValue[action];
|
|
1895
|
+
}
|
|
1896
|
+
}
|
|
1897
|
+
return actions;
|
|
1898
|
+
}
|
|
1899
|
+
function readOnlyListActions(actionsValue) {
|
|
1900
|
+
return isRecord(actionsValue.status)
|
|
1901
|
+
? { status: actionsValue.status }
|
|
1902
|
+
: {};
|
|
1903
|
+
}
|
|
1904
|
+
function readOnlyManagedTurn(managedTurn) {
|
|
1905
|
+
return {
|
|
1906
|
+
...managedTurn,
|
|
1907
|
+
available_actions: readOnlyListActions(isRecord(managedTurn.available_actions)
|
|
1908
|
+
? managedTurn.available_actions
|
|
1909
|
+
: {})
|
|
1910
|
+
};
|
|
1911
|
+
}
|
|
1912
|
+
function historicalManagedTurnForTerminal(conversation, terminalCanAcceptSend, terminal) {
|
|
1913
|
+
const managedTurn = managedTurnListEntry(summarizeConversation(conversation), {
|
|
1914
|
+
terminalBridge: terminalBridgeEnabled(conversation),
|
|
1915
|
+
approvalState: managedListApprovalState(conversation),
|
|
1916
|
+
conversation
|
|
1917
|
+
});
|
|
1918
|
+
const availableActions = isRecord(managedTurn.available_actions)
|
|
1919
|
+
? managedTurn.available_actions
|
|
1920
|
+
: {};
|
|
1921
|
+
const safeActions = safeUnavailableManagedTurnActions(availableActions);
|
|
1922
|
+
if (terminalCanAcceptSend &&
|
|
1923
|
+
managedTurnMatchesLiveTerminal(conversation, terminal) &&
|
|
1924
|
+
isRecord(availableActions.follow_up)) {
|
|
1925
|
+
safeActions.follow_up = availableActions.follow_up;
|
|
1926
|
+
}
|
|
1927
|
+
return {
|
|
1928
|
+
...managedTurn,
|
|
1929
|
+
available_actions: safeActions
|
|
1930
|
+
};
|
|
1931
|
+
}
|
|
1932
|
+
function managedTurnMatchesLiveTerminal(conversation, terminal) {
|
|
1933
|
+
const takeover = isRecord(conversation.native_session_takeover)
|
|
1934
|
+
? conversation.native_session_takeover
|
|
1935
|
+
: undefined;
|
|
1936
|
+
const liveControl = isRecord(terminal.terminal_control)
|
|
1937
|
+
? terminal.terminal_control
|
|
1938
|
+
: undefined;
|
|
1939
|
+
const storedControl = terminalControlFromTakeover(takeover);
|
|
1940
|
+
const livePid = Number(terminal.pid);
|
|
1941
|
+
const storedPid = Number(takeover?.terminal_agent_pid);
|
|
1942
|
+
if (executorForConversation(conversation).kind !== terminal.agent ||
|
|
1943
|
+
!Number.isSafeInteger(livePid) ||
|
|
1944
|
+
livePid <= 1 ||
|
|
1945
|
+
storedPid !== livePid ||
|
|
1946
|
+
stringValue(takeover?.native_session_id) !== stringValue(terminal.id) ||
|
|
1947
|
+
terminalControlSelectorKey(storedControl) !==
|
|
1948
|
+
terminalControlSelectorKey(liveControl)) {
|
|
1949
|
+
return false;
|
|
1950
|
+
}
|
|
1951
|
+
const storedSessionId = stringValue(takeover?.terminal_agent_session_id);
|
|
1952
|
+
const liveSessionId = stringValue(terminal.session_id);
|
|
1953
|
+
if (storedSessionId && storedSessionId !== liveSessionId) {
|
|
1954
|
+
return false;
|
|
1955
|
+
}
|
|
1956
|
+
const liveWorkspace = terminal.workspace ?? terminal.cwd;
|
|
1957
|
+
if (!matchesConfiguredWorkspace(conversation.workspace, liveWorkspace)) {
|
|
1958
|
+
return false;
|
|
1959
|
+
}
|
|
1960
|
+
const livePanePath = liveControl?.currentPath;
|
|
1961
|
+
if (livePanePath !== undefined &&
|
|
1962
|
+
!matchesConfiguredWorkspace(conversation.workspace, livePanePath)) {
|
|
1963
|
+
return false;
|
|
1964
|
+
}
|
|
1965
|
+
return true;
|
|
1966
|
+
}
|
|
1967
|
+
function currentManagedTurnForTerminal(conversation, terminal, rawTerminalActions) {
|
|
1968
|
+
const managedTurn = managedTurnListEntry(summarizeConversation(conversation), {
|
|
1969
|
+
terminalBridge: terminalBridgeEnabled(conversation),
|
|
1970
|
+
approvalState: managedListApprovalState(conversation),
|
|
1971
|
+
conversation
|
|
1972
|
+
});
|
|
1973
|
+
const rawApproval = isRecord(rawTerminalActions.approve)
|
|
1974
|
+
? rawTerminalActions.approve
|
|
1975
|
+
: undefined;
|
|
1976
|
+
if (!rawApproval || executorForConversation(conversation).kind !== "codex") {
|
|
1977
|
+
return managedTurn;
|
|
1978
|
+
}
|
|
1979
|
+
const ownerId = conversation.conversation_id;
|
|
1980
|
+
const approval = retargetConversationAction(rawApproval, ownerId);
|
|
1981
|
+
const terminalApprovalState = isRecord(terminal.approval_state)
|
|
1982
|
+
? terminal.approval_state
|
|
1983
|
+
: undefined;
|
|
1984
|
+
return {
|
|
1985
|
+
...managedTurn,
|
|
1986
|
+
...(terminalApprovalState
|
|
1987
|
+
? { approval_state: terminalApprovalState }
|
|
1988
|
+
: {}),
|
|
1989
|
+
available_actions: {
|
|
1990
|
+
...(isRecord(managedTurn.available_actions)
|
|
1991
|
+
? managedTurn.available_actions
|
|
1992
|
+
: {}),
|
|
1993
|
+
approve: approval
|
|
1994
|
+
}
|
|
1995
|
+
};
|
|
1996
|
+
}
|
|
1997
|
+
function retargetConversationAction(action, conversationId) {
|
|
1998
|
+
const beforeCall = isRecord(action.before_call)
|
|
1999
|
+
? action.before_call
|
|
2000
|
+
: undefined;
|
|
2001
|
+
return {
|
|
2002
|
+
...action,
|
|
2003
|
+
arguments: {
|
|
2004
|
+
...(isRecord(action.arguments) ? action.arguments : {}),
|
|
2005
|
+
conversation_id: conversationId
|
|
2006
|
+
},
|
|
2007
|
+
...(beforeCall
|
|
2008
|
+
? {
|
|
2009
|
+
before_call: {
|
|
2010
|
+
...beforeCall,
|
|
2011
|
+
arguments: {
|
|
2012
|
+
...(isRecord(beforeCall.arguments)
|
|
2013
|
+
? beforeCall.arguments
|
|
2014
|
+
: {}),
|
|
2015
|
+
conversation_id: conversationId
|
|
2016
|
+
}
|
|
2017
|
+
}
|
|
2018
|
+
}
|
|
2019
|
+
: {})
|
|
1539
2020
|
};
|
|
1540
2021
|
}
|
|
1541
2022
|
async function listStateForTerminal(agent, terminalControl, options, bridge = createTerminalAgentBridge(options), runtime) {
|
|
@@ -1602,7 +2083,7 @@ function childPidsForRoot(root, processes) {
|
|
|
1602
2083
|
.filter((process) => process.agent === root.agent && process.ppid === root.pid)
|
|
1603
2084
|
.map((process) => process.pid);
|
|
1604
2085
|
}
|
|
1605
|
-
function
|
|
2086
|
+
function canFollowUpManagedTurn(status) {
|
|
1606
2087
|
return !["done", "failed", "closed", "cancelled"].includes(status);
|
|
1607
2088
|
}
|
|
1608
2089
|
function managedListApprovalState(conversation) {
|
|
@@ -1640,28 +2121,31 @@ function managedListApprovalState(conversation) {
|
|
|
1640
2121
|
}
|
|
1641
2122
|
function listActionContracts() {
|
|
1642
2123
|
return {
|
|
1643
|
-
version:
|
|
2124
|
+
version: 3,
|
|
1644
2125
|
instructions: [
|
|
1645
|
-
"
|
|
1646
|
-
"
|
|
2126
|
+
"Treat terminals[] as the primary resource and use only actions present in available_actions.",
|
|
2127
|
+
"Use a terminal send action to start a new managed turn. Use a managed turn follow_up action only when continuing that specific managed turn.",
|
|
1647
2128
|
"Start with the action's prefilled arguments, supply every missing_required field, and consult the top-level action's optional fields only when needed.",
|
|
1648
2129
|
"Authoritative full IDs are prefilled; short_ref is for display and human input.",
|
|
1649
2130
|
"Availability is a snapshot. AKK revalidates process, tmux pane, workspace, activity, approval, and recovery state before side effects."
|
|
1650
2131
|
],
|
|
1651
2132
|
field_semantics: {
|
|
2133
|
+
process_state: {
|
|
2134
|
+
terminals: "physical_terminal_process_liveness",
|
|
2135
|
+
authoritative_for_tool_calls: false
|
|
2136
|
+
},
|
|
1652
2137
|
status: {
|
|
1653
|
-
|
|
1654
|
-
terminal_controlled: "process_liveness",
|
|
2138
|
+
managed_turns: "managed_turn_lifecycle",
|
|
1655
2139
|
authoritative_for_tool_calls: false
|
|
1656
2140
|
},
|
|
1657
2141
|
activity_state: {
|
|
1658
|
-
|
|
2142
|
+
terminals: "terminal_screen_activity_classification",
|
|
1659
2143
|
authoritative_for_tool_calls: false
|
|
1660
2144
|
},
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
2145
|
+
managed: {
|
|
2146
|
+
current_turn: "the authoritative dispatch-ledger owner, never inferred from history",
|
|
2147
|
+
recent_turn: "the latest visible non-owning turn for intentional follow-up",
|
|
2148
|
+
history: "older turns, present only with --all"
|
|
1665
2149
|
},
|
|
1666
2150
|
available_actions: {
|
|
1667
2151
|
meaning: "currently_safe_actions",
|
|
@@ -1681,7 +2165,19 @@ function listActionContracts() {
|
|
|
1681
2165
|
"agentHardTimeoutMinutes"
|
|
1682
2166
|
],
|
|
1683
2167
|
unsupported: ["timeoutSeconds"],
|
|
1684
|
-
ordinary_use: "Add request only
|
|
2168
|
+
ordinary_use: "Start a new managed turn on the selected physical terminal. Add request only and omit timeout fields unless the user explicitly asks to change monitoring limits."
|
|
2169
|
+
},
|
|
2170
|
+
follow_up: {
|
|
2171
|
+
tool: "agent_knock_knock_send",
|
|
2172
|
+
target_argument: "selector",
|
|
2173
|
+
required: ["request"],
|
|
2174
|
+
optional: [
|
|
2175
|
+
"selector",
|
|
2176
|
+
"idleTimeoutMinutes",
|
|
2177
|
+
"agentTimeoutMinutes",
|
|
2178
|
+
"agentHardTimeoutMinutes"
|
|
2179
|
+
],
|
|
2180
|
+
ordinary_use: "Continue the explicitly selected managed turn. Start from its prefilled selector and add request."
|
|
1685
2181
|
},
|
|
1686
2182
|
status: {
|
|
1687
2183
|
tool: "agent_knock_knock_status",
|
|
@@ -1736,8 +2232,8 @@ function availableListActions(entry, { conversation } = {}) {
|
|
|
1736
2232
|
arguments: { conversation_id: id }
|
|
1737
2233
|
}
|
|
1738
2234
|
};
|
|
1739
|
-
const terminalControlled = entry.source === "
|
|
1740
|
-
const managed = entry.source === "
|
|
2235
|
+
const terminalControlled = entry.source === "terminal";
|
|
2236
|
+
const managed = entry.source === "managed_turn";
|
|
1741
2237
|
const approvalState = isRecord(entry.approval_state)
|
|
1742
2238
|
? entry.approval_state
|
|
1743
2239
|
: {};
|
|
@@ -1756,7 +2252,7 @@ function availableListActions(entry, { conversation } = {}) {
|
|
|
1756
2252
|
(terminalControlled &&
|
|
1757
2253
|
entry.activity_state === "idle" &&
|
|
1758
2254
|
approvalState.blocked !== true))) {
|
|
1759
|
-
actions
|
|
2255
|
+
actions[managed ? "follow_up" : "send"] = {
|
|
1760
2256
|
tool: "agent_knock_knock_send",
|
|
1761
2257
|
arguments: { selector: id },
|
|
1762
2258
|
missing_required: ["request"]
|
|
@@ -1862,53 +2358,112 @@ function isSessionSelectorSyntax(value) {
|
|
|
1862
2358
|
}
|
|
1863
2359
|
async function sessionSelectorCandidates(commandName, options) {
|
|
1864
2360
|
const storeDir = storeDirFromOptions(options);
|
|
1865
|
-
|
|
2361
|
+
const mutationsAllowed = inspectStoreCompatibility(storeDir).writable === true;
|
|
1866
2362
|
const storedConversations = listConversations(storeDir);
|
|
1867
2363
|
const workspaceConversations = storedConversations
|
|
1868
2364
|
.filter((conversation) => matchesConfiguredWorkspace(options.workspace, conversation.workspace));
|
|
1869
2365
|
const discoverableWorkspaceConversations = workspaceConversations
|
|
1870
2366
|
.filter(isDiscoverableTmuxConversation);
|
|
1871
|
-
const managed = discoverableWorkspaceConversations.map((conversation) =>
|
|
2367
|
+
const managed = discoverableWorkspaceConversations.map((conversation) => managedTurnListEntry(summarizeConversation(conversation), {
|
|
1872
2368
|
terminalBridge: terminalBridgeEnabled(conversation),
|
|
1873
2369
|
approvalState: managedListApprovalState(conversation),
|
|
1874
2370
|
conversation
|
|
1875
2371
|
}));
|
|
1876
|
-
const managedTerminalKeys = new Set(workspaceConversations
|
|
1877
|
-
.filter((conversation) => isActiveStatus(conversation.status))
|
|
1878
|
-
.map((conversation) => terminalControlSelectorKey(terminalControlFromTakeover(isRecord(conversation.native_session_takeover)
|
|
1879
|
-
? conversation.native_session_takeover
|
|
1880
|
-
: undefined)))
|
|
1881
|
-
.filter((key) => key !== undefined));
|
|
1882
2372
|
const terminalScan = await buildTerminalListGroup({
|
|
1883
2373
|
options: {
|
|
1884
2374
|
...options,
|
|
1885
|
-
noApprovalScan:
|
|
2375
|
+
noApprovalScan: ["send", "approve", "cancel"].includes(commandName)
|
|
1886
2376
|
? options.noApprovalScan
|
|
1887
2377
|
: true
|
|
1888
2378
|
},
|
|
1889
2379
|
agentFilter: undefined,
|
|
1890
2380
|
statusFilter: undefined
|
|
1891
2381
|
});
|
|
2382
|
+
const terminalProjection = terminalFirstListProjection({
|
|
2383
|
+
terminals: terminalScan.terminalControlled.filter((entry) => matchesConfiguredWorkspace(options.workspace, entry.workspace ?? entry.cwd)),
|
|
2384
|
+
allConversations: discoverableWorkspaceConversations,
|
|
2385
|
+
displayedConversations: discoverableWorkspaceConversations,
|
|
2386
|
+
includeAll: false,
|
|
2387
|
+
managedOnly: options.managedOnly === true,
|
|
2388
|
+
statusFilter: undefined,
|
|
2389
|
+
mutationsAllowed
|
|
2390
|
+
});
|
|
1892
2391
|
const observedAtMs = Date.now();
|
|
1893
2392
|
return [
|
|
1894
|
-
...managed,
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
})
|
|
1902
|
-
]
|
|
2393
|
+
...managed.map((entry) => sessionSelectorCandidateForEntry(entry, commandName, observedAtMs, {
|
|
2394
|
+
defaultActionable: options.managedOnly === true,
|
|
2395
|
+
mutationsAllowed
|
|
2396
|
+
})),
|
|
2397
|
+
...terminalProjection.terminals.map((entry) => sessionSelectorCandidateForEntry(entry, commandName, observedAtMs, {
|
|
2398
|
+
defaultActionable: true,
|
|
2399
|
+
mutationsAllowed
|
|
2400
|
+
}))
|
|
2401
|
+
];
|
|
2402
|
+
}
|
|
2403
|
+
function sessionSelectorCandidateForEntry(entry, commandName, observedAtMs, { defaultActionable, mutationsAllowed }) {
|
|
2404
|
+
const action = mutationsAllowed || commandName === "status"
|
|
2405
|
+
? listActionForCommand(entry, commandName)
|
|
2406
|
+
: undefined;
|
|
2407
|
+
const targetId = listActionTargetId(action);
|
|
2408
|
+
return {
|
|
1903
2409
|
id: String(entry.id),
|
|
2410
|
+
...(targetId && targetId !== entry.id ? { targetId } : {}),
|
|
1904
2411
|
agent: resolveExecutor({ kind: entry.agent }).kind,
|
|
1905
|
-
actionable:
|
|
2412
|
+
actionable: action !== undefined,
|
|
2413
|
+
defaultActionable,
|
|
1906
2414
|
...sessionEntryRecency(entry, observedAtMs),
|
|
1907
2415
|
source: stringValue(entry.source),
|
|
1908
|
-
status: stringValue(entry.status),
|
|
2416
|
+
status: stringValue(entry.status ?? entry.process_state),
|
|
1909
2417
|
workspace: stringValue(entry.workspace ?? entry.cwd),
|
|
1910
2418
|
label: stringValue(entry.request ?? entry.command)
|
|
1911
|
-
}
|
|
2419
|
+
};
|
|
2420
|
+
}
|
|
2421
|
+
function listActionForCommand(entry, commandName) {
|
|
2422
|
+
const actions = isRecord(entry.available_actions)
|
|
2423
|
+
? entry.available_actions
|
|
2424
|
+
: {};
|
|
2425
|
+
const actionName = commandName === "retry-callback"
|
|
2426
|
+
? "retry_callback"
|
|
2427
|
+
: commandName === "send" && entry.source === "managed_turn"
|
|
2428
|
+
? "follow_up"
|
|
2429
|
+
: commandName;
|
|
2430
|
+
if (isRecord(actions[actionName])) {
|
|
2431
|
+
return actions[actionName];
|
|
2432
|
+
}
|
|
2433
|
+
if (commandName !== "approve") {
|
|
2434
|
+
return undefined;
|
|
2435
|
+
}
|
|
2436
|
+
if (managedTurnCanEnterApprovalPath(entry)) {
|
|
2437
|
+
return {
|
|
2438
|
+
tool: "agent_knock_knock_approve",
|
|
2439
|
+
arguments: { conversation_id: String(entry.id) }
|
|
2440
|
+
};
|
|
2441
|
+
}
|
|
2442
|
+
const managed = isRecord(entry.managed) ? entry.managed : undefined;
|
|
2443
|
+
const currentTurn = isRecord(managed?.current_turn)
|
|
2444
|
+
? managed.current_turn
|
|
2445
|
+
: undefined;
|
|
2446
|
+
if (currentTurn && managedTurnCanEnterApprovalPath(currentTurn)) {
|
|
2447
|
+
return {
|
|
2448
|
+
tool: "agent_knock_knock_approve",
|
|
2449
|
+
arguments: {
|
|
2450
|
+
conversation_id: String(currentTurn.conversation_id ?? currentTurn.id)
|
|
2451
|
+
}
|
|
2452
|
+
};
|
|
2453
|
+
}
|
|
2454
|
+
return undefined;
|
|
2455
|
+
}
|
|
2456
|
+
function managedTurnCanEnterApprovalPath(entry) {
|
|
2457
|
+
const executor = isRecord(entry.executor) ? entry.executor : undefined;
|
|
2458
|
+
return (entry.source === "managed_turn" &&
|
|
2459
|
+
executor?.transport === "tmux" &&
|
|
2460
|
+
isActiveStatus(String(entry.status)));
|
|
2461
|
+
}
|
|
2462
|
+
function listActionTargetId(action) {
|
|
2463
|
+
const actionArguments = isRecord(action?.arguments)
|
|
2464
|
+
? action.arguments
|
|
2465
|
+
: undefined;
|
|
2466
|
+
return stringValue(actionArguments?.selector ?? actionArguments?.conversation_id);
|
|
1912
2467
|
}
|
|
1913
2468
|
function terminalControlSelectorKey(value) {
|
|
1914
2469
|
if (!isRecord(value)) {
|
|
@@ -1925,22 +2480,6 @@ function terminalControlSelectorKey(value) {
|
|
|
1925
2480
|
socket_path: stringValue(value.socketPath) ?? null
|
|
1926
2481
|
});
|
|
1927
2482
|
}
|
|
1928
|
-
function sessionEntrySupportsCommand(entry, commandName) {
|
|
1929
|
-
const commands = isRecord(entry.commands) ? entry.commands : {};
|
|
1930
|
-
if (typeof commands[commandName] === "boolean") {
|
|
1931
|
-
return commands[commandName] === true;
|
|
1932
|
-
}
|
|
1933
|
-
if (entry.source !== "akk_delegate") {
|
|
1934
|
-
return false;
|
|
1935
|
-
}
|
|
1936
|
-
if (commandName === "renew") {
|
|
1937
|
-
return entry.status === "stalled";
|
|
1938
|
-
}
|
|
1939
|
-
if (commandName === "retry-callback") {
|
|
1940
|
-
return ["callback_pending", "callback_failed"].includes(entry.status);
|
|
1941
|
-
}
|
|
1942
|
-
return false;
|
|
1943
|
-
}
|
|
1944
2483
|
function sessionEntryRecency(entry, observedAtMs) {
|
|
1945
2484
|
const timestamp = Date.parse(String(entry.updated_at ?? entry.created_at ?? ""));
|
|
1946
2485
|
if (Number.isFinite(timestamp)) {
|
|
@@ -1956,7 +2495,23 @@ async function resolveTerminalConversationFromOptions(options) {
|
|
|
1956
2495
|
return createTerminalAgentBridge(options).resolveConversationId(stringValue(options.conversation ?? options.conversationId));
|
|
1957
2496
|
}
|
|
1958
2497
|
async function runStatus(options) {
|
|
1959
|
-
|
|
2498
|
+
const explicitStatePath = options.state
|
|
2499
|
+
? expandHome(String(options.state))
|
|
2500
|
+
: undefined;
|
|
2501
|
+
const storeDir = explicitStatePath
|
|
2502
|
+
? pathsForConversationDir(path.dirname(explicitStatePath)).storeDir
|
|
2503
|
+
: storeDirFromOptions(options);
|
|
2504
|
+
const reconciliationConversationId = stringValue(options.conversation ?? options.conversationId) ??
|
|
2505
|
+
(explicitStatePath
|
|
2506
|
+
? path.basename(pathsForConversationDir(path.dirname(explicitStatePath))
|
|
2507
|
+
.conversationDir)
|
|
2508
|
+
: undefined);
|
|
2509
|
+
const reconciliation = options.reconcile === true
|
|
2510
|
+
? await reconcileStoreForStatus(storeDir, options, reconciliationConversationId)
|
|
2511
|
+
: {
|
|
2512
|
+
status: "disabled",
|
|
2513
|
+
reason: "standalone status is read-only unless --reconcile is supplied"
|
|
2514
|
+
};
|
|
1960
2515
|
const terminalConversation = await resolveTerminalConversationFromOptions(options);
|
|
1961
2516
|
if (terminalConversation) {
|
|
1962
2517
|
const terminalStatus = await terminalStatusForControl(terminalConversation.agent, terminalConversation.terminalControl, options, {
|
|
@@ -1970,6 +2525,8 @@ async function runStatus(options) {
|
|
|
1970
2525
|
conversation_id: terminalConversation.conversationId,
|
|
1971
2526
|
source: "terminal_control",
|
|
1972
2527
|
agent: terminalConversation.agent,
|
|
2528
|
+
store: inspectStoreCompatibility(storeDir),
|
|
2529
|
+
reconciliation,
|
|
1973
2530
|
...context,
|
|
1974
2531
|
terminal_control: terminalConversation.terminalControl,
|
|
1975
2532
|
terminal_status: terminalStatus,
|
|
@@ -1984,13 +2541,12 @@ async function runStatus(options) {
|
|
|
1984
2541
|
}
|
|
1985
2542
|
const loaded = loadConversationFromOptions(options);
|
|
1986
2543
|
const { statePath, logPath } = loaded;
|
|
1987
|
-
const conversation =
|
|
1988
|
-
...loaded,
|
|
1989
|
-
options
|
|
1990
|
-
});
|
|
2544
|
+
const conversation = loaded.conversation;
|
|
1991
2545
|
const events = readExistingEvents(logPath);
|
|
1992
2546
|
const result = {
|
|
1993
2547
|
conversation,
|
|
2548
|
+
store: inspectStoreCompatibility(storeDir),
|
|
2549
|
+
reconciliation,
|
|
1994
2550
|
summary: summarizeConversation(conversation),
|
|
1995
2551
|
confidence: "high",
|
|
1996
2552
|
about: managedConversationAbout(conversation, events),
|
|
@@ -2027,6 +2583,38 @@ async function runStatus(options) {
|
|
|
2027
2583
|
trace: Boolean(options.trace)
|
|
2028
2584
|
});
|
|
2029
2585
|
}
|
|
2586
|
+
async function reconcileStoreForStatus(storeDir, options, conversationId) {
|
|
2587
|
+
try {
|
|
2588
|
+
ensureStoreWritable(storeDir);
|
|
2589
|
+
}
|
|
2590
|
+
catch (error) {
|
|
2591
|
+
if (isRecord(error) && error.code === "AKK_STORE_INCOMPATIBLE") {
|
|
2592
|
+
return {
|
|
2593
|
+
status: "skipped",
|
|
2594
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
2595
|
+
store: inspectStoreCompatibility(storeDir)
|
|
2596
|
+
};
|
|
2597
|
+
}
|
|
2598
|
+
throw error;
|
|
2599
|
+
}
|
|
2600
|
+
const idle = reconcileIdleConversations(storeDir, options, new Date(), conversationId);
|
|
2601
|
+
const monitors = await reconcileMonitors(options, {
|
|
2602
|
+
includeCallbackRecovery: false,
|
|
2603
|
+
reason: "status_reconciliation",
|
|
2604
|
+
conversationId
|
|
2605
|
+
});
|
|
2606
|
+
return {
|
|
2607
|
+
status: "completed",
|
|
2608
|
+
checked: Math.max(idle.checked, monitors.checked),
|
|
2609
|
+
changed: idle.closed + monitors.launched,
|
|
2610
|
+
closed: idle.closed,
|
|
2611
|
+
monitors_launched: monitors.launched,
|
|
2612
|
+
monitors_already_running: monitors.already_running,
|
|
2613
|
+
skipped: idle.skipped + monitors.skipped,
|
|
2614
|
+
errors: monitors.errors,
|
|
2615
|
+
idle_timeout_minutes: idle.idle_timeout_minutes
|
|
2616
|
+
};
|
|
2617
|
+
}
|
|
2030
2618
|
async function terminalStatusContext(terminalConversation, terminalStatus, options) {
|
|
2031
2619
|
if (terminalConversation.agent === "codex") {
|
|
2032
2620
|
try {
|
|
@@ -2497,7 +3085,6 @@ async function runSend(options) {
|
|
|
2497
3085
|
if (options.agentHardTimeoutMinutes !== undefined) {
|
|
2498
3086
|
positiveMinutes(options.agentHardTimeoutMinutes, "--agent-hard-timeout-minutes");
|
|
2499
3087
|
}
|
|
2500
|
-
cleanupIdleConversations(storeDirFromOptions(options), options);
|
|
2501
3088
|
const terminalConversation = await resolveTerminalConversationFromOptions(options);
|
|
2502
3089
|
if (terminalConversation) {
|
|
2503
3090
|
if (!options.background) {
|
|
@@ -2514,6 +3101,7 @@ async function runSend(options) {
|
|
|
2514
3101
|
messageBody,
|
|
2515
3102
|
terminalControl: terminalConversation.terminalControl
|
|
2516
3103
|
});
|
|
3104
|
+
ensureStoreWritable(managed.conversation.store_dir);
|
|
2517
3105
|
ensureDir(path.dirname(managed.statePath));
|
|
2518
3106
|
releaseStateLock = acquireFileLock(`${managed.statePath}.lock`);
|
|
2519
3107
|
await runTerminalControlSend({
|
|
@@ -2599,7 +3187,6 @@ async function runSend(options) {
|
|
|
2599
3187
|
throw new Error(`conversation ${migratedConversation.conversation_id} is not attached to a live tmux terminal`);
|
|
2600
3188
|
}
|
|
2601
3189
|
async function runApprove(options) {
|
|
2602
|
-
cleanupIdleConversations(storeDirFromOptions(options), options);
|
|
2603
3190
|
const terminalConversation = await resolveTerminalConversationFromOptions(options);
|
|
2604
3191
|
if (terminalConversation) {
|
|
2605
3192
|
await runTerminalConversationApprove({
|
|
@@ -2773,6 +3360,7 @@ async function runApprove(options) {
|
|
|
2773
3360
|
}
|
|
2774
3361
|
};
|
|
2775
3362
|
let releaseStateLock;
|
|
3363
|
+
let approvalDispatchReserved = false;
|
|
2776
3364
|
const releaseApprovalStateLock = () => {
|
|
2777
3365
|
if (releaseStateLock) {
|
|
2778
3366
|
const release = releaseStateLock;
|
|
@@ -2780,323 +3368,330 @@ async function runApprove(options) {
|
|
|
2780
3368
|
release();
|
|
2781
3369
|
}
|
|
2782
3370
|
};
|
|
3371
|
+
releaseStateLock = acquireFileLock(`${statePath}.lock`);
|
|
3372
|
+
const writerStoreDir = pathsForConversationDir(path.dirname(statePath)).storeDir;
|
|
2783
3373
|
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) {
|
|
3374
|
+
return await withStoreWriterLeaseAsync(writerStoreDir, async () => {
|
|
3375
|
+
let approval;
|
|
3376
|
+
let lockedConversation = conversation;
|
|
3377
|
+
const currentConversation = loadState(statePath);
|
|
3378
|
+
const currentTakeover = isRecord(currentConversation.native_session_takeover)
|
|
3379
|
+
? currentConversation.native_session_takeover
|
|
3380
|
+
: undefined;
|
|
3381
|
+
const currentControl = terminalControlFromTakeover(currentTakeover);
|
|
3382
|
+
const currentApproval = isRecord(currentTakeover?.terminal_bridge_approval)
|
|
3383
|
+
? currentTakeover.terminal_bridge_approval
|
|
3384
|
+
: undefined;
|
|
3385
|
+
if (currentConversation.status !== conversation.status ||
|
|
3386
|
+
currentTakeover?.terminal_bridge_message_id !== nativeTakeover?.terminal_bridge_message_id ||
|
|
3387
|
+
currentControl?.target !== terminalControl.target ||
|
|
3388
|
+
currentControl?.socketPath !== terminalControl.socketPath ||
|
|
3389
|
+
(claudeScreenApproval &&
|
|
3390
|
+
currentApproval?.fingerprint !== monitoredApproval?.fingerprint)) {
|
|
3391
|
+
throw new Error("approval state changed while waiting for terminal control; refresh status and retry");
|
|
3392
|
+
}
|
|
3393
|
+
assertManagedTerminalDispatchOwner({
|
|
3394
|
+
conversation: currentConversation,
|
|
3395
|
+
terminalControl: currentControl,
|
|
3396
|
+
action: "approve"
|
|
3397
|
+
});
|
|
3398
|
+
lockedConversation = currentConversation;
|
|
3399
|
+
approval = await createTerminalAgentBridge(options).approve(executor.kind, terminalControl, {
|
|
3400
|
+
expectedFingerprint,
|
|
3401
|
+
scrollbackLines: Number(options.scrollbackLines ?? 120),
|
|
3402
|
+
runtime: runtimeIdentity,
|
|
3403
|
+
managedRequest: terminalDurableRequestForConversation(currentConversation, terminalControl),
|
|
3404
|
+
requiredDecisionMode: autoApproved && executor.kind === "claude" ? "keys" : undefined,
|
|
3405
|
+
authorize: autoApproved
|
|
3406
|
+
? ({ agent, terminalControl: currentTerminalControl, inspection, fingerprint }) => {
|
|
2857
3407
|
if (!autoApprovalPolicy) {
|
|
2858
|
-
|
|
3408
|
+
return {
|
|
3409
|
+
approved: false,
|
|
3410
|
+
reason: "automatic approval requires an executor-side policy"
|
|
3411
|
+
};
|
|
2859
3412
|
}
|
|
2860
|
-
const
|
|
3413
|
+
const candidate = policyCandidateForInspection({
|
|
3414
|
+
agent,
|
|
3415
|
+
currentTerminalControl,
|
|
3416
|
+
inspection,
|
|
3417
|
+
fingerprint
|
|
3418
|
+
});
|
|
3419
|
+
executorPolicyDecision = evaluateApprovalPolicy({
|
|
2861
3420
|
policy: autoApprovalPolicy,
|
|
2862
|
-
candidate
|
|
2863
|
-
agent: executor.kind,
|
|
2864
|
-
currentTerminalControl: dispatchControl,
|
|
2865
|
-
inspection,
|
|
2866
|
-
fingerprint
|
|
2867
|
-
})
|
|
3421
|
+
candidate
|
|
2868
3422
|
});
|
|
2869
|
-
if (
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
throw new Error("executor-side auto-approval rule changed after recapture");
|
|
3423
|
+
if (executorPolicyDecision.action !== "approve") {
|
|
3424
|
+
return {
|
|
3425
|
+
approved: false,
|
|
3426
|
+
reason: `executor-side auto-approval policy rejected the current request: ${executorPolicyDecision.reason}`
|
|
3427
|
+
};
|
|
2875
3428
|
}
|
|
2876
|
-
if (policyRuleId &&
|
|
2877
|
-
|
|
3429
|
+
if (policyRuleId && executorPolicyDecision.ruleId !== policyRuleId) {
|
|
3430
|
+
return {
|
|
3431
|
+
approved: false,
|
|
3432
|
+
reason: "executor-side auto-approval rule changed before execution"
|
|
3433
|
+
};
|
|
2878
3434
|
}
|
|
2879
3435
|
if (policyFingerprint &&
|
|
2880
|
-
|
|
2881
|
-
|
|
3436
|
+
executorPolicyDecision.policyFingerprint !== policyFingerprint) {
|
|
3437
|
+
return {
|
|
3438
|
+
approved: false,
|
|
3439
|
+
reason: "executor-side auto-approval policy changed before execution"
|
|
3440
|
+
};
|
|
2882
3441
|
}
|
|
2883
|
-
|
|
2884
|
-
}
|
|
2885
|
-
if (releaseStateLock) {
|
|
2886
|
-
throw new Error("Claude approval dispatch was already reserved");
|
|
2887
|
-
}
|
|
2888
|
-
releaseStateLock = acquireFileLock(`${statePath}.lock`);
|
|
2889
|
-
const latestConversation = loadState(statePath);
|
|
2890
|
-
const latestTakeover = isRecord(latestConversation.native_session_takeover)
|
|
2891
|
-
? latestConversation.native_session_takeover
|
|
2892
|
-
: undefined;
|
|
2893
|
-
const latestControl = terminalControlFromTakeover(latestTakeover);
|
|
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");
|
|
3442
|
+
return { approved: true };
|
|
2925
3443
|
}
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
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
|
|
3444
|
+
: undefined,
|
|
3445
|
+
beforeKeyDispatch: claudeScreenApproval
|
|
3446
|
+
? ({ fingerprint, terminalControl: dispatchControl, inspection, keys }) => {
|
|
3447
|
+
if (autoApproved) {
|
|
3448
|
+
if (!autoApprovalPolicy) {
|
|
3449
|
+
throw new Error("automatic approval requires an executor-side policy before dispatch");
|
|
2944
3450
|
}
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
3451
|
+
const freshPolicyDecision = evaluateApprovalPolicy({
|
|
3452
|
+
policy: autoApprovalPolicy,
|
|
3453
|
+
candidate: policyCandidateForInspection({
|
|
3454
|
+
agent: executor.kind,
|
|
3455
|
+
currentTerminalControl: dispatchControl,
|
|
3456
|
+
inspection,
|
|
3457
|
+
fingerprint
|
|
3458
|
+
})
|
|
3459
|
+
});
|
|
3460
|
+
if (freshPolicyDecision.action !== "approve") {
|
|
3461
|
+
throw new Error(`executor-side auto-approval policy rejected the recaptured request: ${freshPolicyDecision.reason}`);
|
|
3462
|
+
}
|
|
3463
|
+
if (executorPolicyDecision?.ruleId &&
|
|
3464
|
+
freshPolicyDecision.ruleId !== executorPolicyDecision.ruleId) {
|
|
3465
|
+
throw new Error("executor-side auto-approval rule changed after recapture");
|
|
3466
|
+
}
|
|
3467
|
+
if (policyRuleId && freshPolicyDecision.ruleId !== policyRuleId) {
|
|
3468
|
+
throw new Error("executor-side auto-approval rule changed before dispatch");
|
|
3469
|
+
}
|
|
3470
|
+
if (policyFingerprint &&
|
|
3471
|
+
freshPolicyDecision.policyFingerprint !== policyFingerprint) {
|
|
3472
|
+
throw new Error("executor-side auto-approval policy changed before dispatch");
|
|
3473
|
+
}
|
|
3474
|
+
executorPolicyDecision = freshPolicyDecision;
|
|
3475
|
+
}
|
|
3476
|
+
if (approvalDispatchReserved) {
|
|
3477
|
+
throw new Error("Claude approval dispatch was already reserved");
|
|
3478
|
+
}
|
|
3479
|
+
approvalDispatchReserved = true;
|
|
3480
|
+
if (!releaseStateLock) {
|
|
3481
|
+
throw new Error("approval state lock was released before terminal dispatch");
|
|
3482
|
+
}
|
|
3483
|
+
const latestConversation = loadState(statePath);
|
|
3484
|
+
const latestTakeover = isRecord(latestConversation.native_session_takeover)
|
|
3485
|
+
? latestConversation.native_session_takeover
|
|
3486
|
+
: undefined;
|
|
3487
|
+
const latestControl = terminalControlFromTakeover(latestTakeover);
|
|
3488
|
+
const latestApproval = isRecord(latestTakeover?.terminal_bridge_approval)
|
|
3489
|
+
? latestTakeover.terminal_bridge_approval
|
|
3490
|
+
: undefined;
|
|
3491
|
+
const latestNotifiedAt = validTimestampMs(latestApproval?.notified_at);
|
|
3492
|
+
const latestApprovalState = isRecord(latestApproval?.approval_state)
|
|
3493
|
+
? latestApproval.approval_state
|
|
3494
|
+
: undefined;
|
|
3495
|
+
const latestPolicyEvidence = isRecord(latestApprovalState?.policy_evidence)
|
|
3496
|
+
? latestApprovalState.policy_evidence
|
|
3497
|
+
: undefined;
|
|
3498
|
+
const recapturedPolicyEvidence = inspection.approval.approvable
|
|
3499
|
+
? inspection.approval.policyEvidence
|
|
3500
|
+
: undefined;
|
|
3501
|
+
const latestDispatch = isRecord(latestTakeover?.terminal_bridge_approval_dispatch)
|
|
3502
|
+
? latestTakeover.terminal_bridge_approval_dispatch
|
|
3503
|
+
: undefined;
|
|
3504
|
+
if (!latestTakeover ||
|
|
3505
|
+
latestConversation.status !== "waiting_for_openclaw" ||
|
|
3506
|
+
latestTakeover.terminal_bridge_message_id !==
|
|
3507
|
+
nativeTakeover?.terminal_bridge_message_id ||
|
|
3508
|
+
latestApproval?.fingerprint !== fingerprint ||
|
|
3509
|
+
latestNotifiedAt === undefined ||
|
|
3510
|
+
Date.now() - latestNotifiedAt > CLAUDE_SCREEN_APPROVAL_TTL_MS ||
|
|
3511
|
+
expectedFingerprint !== fingerprint ||
|
|
3512
|
+
latestControl?.target !== dispatchControl.target ||
|
|
3513
|
+
latestControl?.socketPath !== dispatchControl.socketPath ||
|
|
3514
|
+
(autoApproved &&
|
|
3515
|
+
(latestPolicyEvidence?.source !== "claude_transcript" ||
|
|
3516
|
+
latestPolicyEvidence.evidence_fingerprint !==
|
|
3517
|
+
recapturedPolicyEvidence?.evidenceFingerprint))) {
|
|
3518
|
+
throw new Error("approval state changed before terminal dispatch; refresh status and retry");
|
|
3519
|
+
}
|
|
3520
|
+
if (latestDispatch?.state === "reserved" &&
|
|
3521
|
+
latestDispatch.terminal_bridge_message_id ===
|
|
3522
|
+
latestTakeover.terminal_bridge_message_id) {
|
|
3523
|
+
throw new Error("a previous Claude approval dispatch has an uncertain outcome; inspect and resolve the terminal manually");
|
|
3524
|
+
}
|
|
3525
|
+
const reservedAt = new Date().toISOString();
|
|
3526
|
+
const reservedConversation = {
|
|
3527
|
+
...latestConversation,
|
|
3528
|
+
native_session_takeover: {
|
|
3529
|
+
...latestTakeover,
|
|
3530
|
+
terminal_bridge_approval_dispatch: {
|
|
3531
|
+
state: "reserved",
|
|
3532
|
+
attempt_id: randomUUID(),
|
|
3533
|
+
fingerprint,
|
|
3534
|
+
keys,
|
|
3535
|
+
terminal_target: dispatchControl.target,
|
|
3536
|
+
terminal_bridge_message_id: latestTakeover.terminal_bridge_message_id,
|
|
3537
|
+
reserved_at: reservedAt
|
|
3538
|
+
}
|
|
3539
|
+
},
|
|
3540
|
+
updated_at: reservedAt
|
|
3541
|
+
};
|
|
3542
|
+
saveState(statePath, reservedConversation);
|
|
3543
|
+
lockedConversation = reservedConversation;
|
|
3544
|
+
}
|
|
3545
|
+
: undefined
|
|
3546
|
+
});
|
|
3547
|
+
const actualFingerprint = approval.fingerprint;
|
|
3548
|
+
const effectivePolicyRuleId = executorPolicyDecision?.ruleId ?? policyRuleId;
|
|
3549
|
+
const effectivePolicyFingerprint = executorPolicyDecision?.policyFingerprint ?? policyFingerprint;
|
|
3550
|
+
if (!approval.approved) {
|
|
3551
|
+
releaseApprovalStateLock();
|
|
3552
|
+
releaseApprovalTerminalLock();
|
|
3553
|
+
if (autoApproved) {
|
|
3554
|
+
appendEvent(logPath, {
|
|
3555
|
+
ts: new Date().toISOString(),
|
|
3556
|
+
conversation_id: conversation.conversation_id,
|
|
3557
|
+
event: "terminal_auto_approval_decision",
|
|
3558
|
+
action: "rejected",
|
|
3559
|
+
reason: approval.reason,
|
|
3560
|
+
terminal_control: terminalControl,
|
|
3561
|
+
expected_fingerprint: expectedFingerprint,
|
|
3562
|
+
actual_fingerprint: actualFingerprint,
|
|
3563
|
+
policy_rule_id: effectivePolicyRuleId,
|
|
3564
|
+
policy_fingerprint: effectivePolicyFingerprint
|
|
3565
|
+
});
|
|
2950
3566
|
}
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
3567
|
+
printJson({
|
|
3568
|
+
conversation,
|
|
3569
|
+
approved: false,
|
|
3570
|
+
blocked: approval.blocked,
|
|
3571
|
+
reason: approval.reason,
|
|
3572
|
+
terminal_control: terminalControl,
|
|
3573
|
+
expected_approval_fingerprint: expectedFingerprint,
|
|
3574
|
+
actual_approval_fingerprint: actualFingerprint,
|
|
3575
|
+
screen_excerpt: approval.screenExcerpt
|
|
3576
|
+
});
|
|
3577
|
+
return;
|
|
3578
|
+
}
|
|
3579
|
+
appendEvent(logPath, {
|
|
3580
|
+
ts: new Date().toISOString(),
|
|
3581
|
+
conversation_id: conversation.conversation_id,
|
|
3582
|
+
event: "terminal_approval_send",
|
|
3583
|
+
terminal_control: terminalControl,
|
|
3584
|
+
key: approval.key,
|
|
3585
|
+
keys: approval.keys,
|
|
3586
|
+
label: approval.label,
|
|
3587
|
+
decision_mode: approval.decisionMode,
|
|
3588
|
+
request_id: approval.requestId,
|
|
3589
|
+
approval_fingerprint: actualFingerprint,
|
|
3590
|
+
auto_approved: autoApproved,
|
|
3591
|
+
policy_rule_id: effectivePolicyRuleId,
|
|
3592
|
+
policy_fingerprint: effectivePolicyFingerprint
|
|
3593
|
+
});
|
|
2959
3594
|
if (autoApproved) {
|
|
2960
3595
|
appendEvent(logPath, {
|
|
2961
3596
|
ts: new Date().toISOString(),
|
|
2962
3597
|
conversation_id: conversation.conversation_id,
|
|
2963
3598
|
event: "terminal_auto_approval_decision",
|
|
2964
|
-
action: "
|
|
2965
|
-
reason: approval.reason,
|
|
3599
|
+
action: "approved",
|
|
2966
3600
|
terminal_control: terminalControl,
|
|
2967
|
-
|
|
2968
|
-
actual_fingerprint: actualFingerprint,
|
|
3601
|
+
approval_fingerprint: actualFingerprint,
|
|
2969
3602
|
policy_rule_id: effectivePolicyRuleId,
|
|
2970
3603
|
policy_fingerprint: effectivePolicyFingerprint
|
|
2971
3604
|
});
|
|
2972
3605
|
}
|
|
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(),
|
|
3606
|
+
runtimeLog("info", "terminal_approval_send", {
|
|
3003
3607
|
conversation_id: conversation.conversation_id,
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3608
|
+
terminal_target: terminalControl.target,
|
|
3609
|
+
key: approval.key,
|
|
3610
|
+
keys: approval.keys,
|
|
3611
|
+
label: approval.label,
|
|
3612
|
+
decision_mode: approval.decisionMode,
|
|
3613
|
+
request_id: approval.requestId,
|
|
3007
3614
|
approval_fingerprint: actualFingerprint,
|
|
3615
|
+
auto_approved: autoApproved,
|
|
3008
3616
|
policy_rule_id: effectivePolicyRuleId,
|
|
3009
3617
|
policy_fingerprint: effectivePolicyFingerprint
|
|
3010
3618
|
});
|
|
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
|
|
3619
|
+
const nativeTakeoverForUpdate = isRecord(lockedConversation.native_session_takeover)
|
|
3620
|
+
? { ...lockedConversation.native_session_takeover }
|
|
3621
|
+
: {};
|
|
3622
|
+
const resolvedApproval = isRecord(nativeTakeoverForUpdate.terminal_bridge_approval)
|
|
3623
|
+
? nativeTakeoverForUpdate.terminal_bridge_approval
|
|
3624
|
+
: undefined;
|
|
3625
|
+
const resolvedApprovalScreenDigest = stringValue(resolvedApproval?.screen_digest);
|
|
3626
|
+
const resolvedApprovalState = isRecord(resolvedApproval?.approval_state)
|
|
3627
|
+
? resolvedApproval.approval_state
|
|
3628
|
+
: undefined;
|
|
3629
|
+
const resolvedTranscriptIdentity = claudeTranscriptApprovalIdentity(resolvedApprovalState);
|
|
3630
|
+
const approvalResolvedAt = new Date().toISOString();
|
|
3631
|
+
const agentTimeoutMinutes = Number(options.agentTimeoutMinutes ??
|
|
3632
|
+
nativeTakeoverForUpdate.terminal_bridge_inactivity_timeout_minutes ??
|
|
3633
|
+
DEFAULT_AGENT_TIMEOUT_MINUTES);
|
|
3634
|
+
const agentHardTimeoutMinutes = positiveMinutes(options.agentHardTimeoutMinutes ??
|
|
3635
|
+
nativeTakeoverForUpdate.terminal_bridge_hard_timeout_minutes ??
|
|
3636
|
+
DEFAULT_AGENT_HARD_TIMEOUT_MINUTES, "--agent-hard-timeout-minutes");
|
|
3637
|
+
const nextNativeTakeover = {
|
|
3638
|
+
...nativeTakeoverForUpdate,
|
|
3639
|
+
terminal_bridge_approval: undefined,
|
|
3640
|
+
terminal_bridge_approval_dispatch: undefined,
|
|
3641
|
+
terminal_bridge_approval_resolved_at: approvalResolvedAt,
|
|
3642
|
+
terminal_bridge_last_approval_fingerprint: actualFingerprint,
|
|
3643
|
+
terminal_bridge_last_approval_screen_digest: resolvedApprovalScreenDigest,
|
|
3644
|
+
terminal_bridge_last_approval_request_id: resolvedTranscriptIdentity?.requestId,
|
|
3645
|
+
terminal_bridge_last_approval_evidence_fingerprint: resolvedTranscriptIdentity?.evidenceFingerprint,
|
|
3646
|
+
terminal_bridge_last_approval_prompt_cleared_at: undefined,
|
|
3647
|
+
terminal_bridge_last_approval_at: approvalResolvedAt,
|
|
3648
|
+
terminal_bridge_last_approval_message_id: nativeTakeoverForUpdate.terminal_bridge_message_id,
|
|
3649
|
+
terminal_bridge_monitor_lock_version: TERMINAL_BRIDGE_MONITOR_LOCK_VERSION,
|
|
3650
|
+
terminal_bridge_monitor_started_at: approvalResolvedAt,
|
|
3651
|
+
terminal_bridge_last_activity_at: approvalResolvedAt,
|
|
3652
|
+
terminal_bridge_last_activity_reason: "approval resolved",
|
|
3653
|
+
terminal_bridge_inactivity_timeout_minutes: agentTimeoutMinutes,
|
|
3654
|
+
terminal_bridge_hard_timeout_minutes: agentHardTimeoutMinutes,
|
|
3655
|
+
terminal_bridge_inactivity_deadline_at: deadlineAt(approvalResolvedAt, agentTimeoutMinutes),
|
|
3656
|
+
terminal_bridge_hard_deadline_at: deadlineAt(stringValue(nativeTakeoverForUpdate.terminal_bridge_started_at) ?? approvalResolvedAt, agentHardTimeoutMinutes)
|
|
3657
|
+
};
|
|
3658
|
+
delete nextNativeTakeover.terminal_bridge_approval;
|
|
3659
|
+
delete nextNativeTakeover.terminal_bridge_approval_dispatch;
|
|
3660
|
+
delete nextNativeTakeover.terminal_bridge_last_approval_prompt_cleared_at;
|
|
3661
|
+
const nextConversation = {
|
|
3662
|
+
...lockedConversation,
|
|
3663
|
+
status: terminalBridgeEnabled(lockedConversation)
|
|
3664
|
+
? "waiting_for_agent"
|
|
3665
|
+
: lockedConversation.status,
|
|
3666
|
+
native_session_takeover: nextNativeTakeover,
|
|
3667
|
+
updated_at: approvalResolvedAt
|
|
3668
|
+
};
|
|
3669
|
+
saveState(statePath, nextConversation);
|
|
3670
|
+
releaseApprovalStateLock();
|
|
3671
|
+
releaseApprovalTerminalLock();
|
|
3672
|
+
const bridgeMonitor = ensureTerminalBridgeMonitorAfterApproval({
|
|
3673
|
+
conversation: nextConversation,
|
|
3674
|
+
statePath,
|
|
3675
|
+
logPath,
|
|
3676
|
+
terminalControl,
|
|
3677
|
+
options
|
|
3678
|
+
});
|
|
3679
|
+
printJson({
|
|
3680
|
+
conversation: nextConversation,
|
|
3681
|
+
approved: true,
|
|
3682
|
+
terminal_control: terminalControl,
|
|
3683
|
+
key: approval.key,
|
|
3684
|
+
keys: approval.keys,
|
|
3685
|
+
label: approval.label,
|
|
3686
|
+
decision_mode: approval.decisionMode,
|
|
3687
|
+
request_id: approval.requestId,
|
|
3688
|
+
approval_fingerprint: actualFingerprint,
|
|
3689
|
+
auto_approved: autoApproved,
|
|
3690
|
+
policy_rule_id: effectivePolicyRuleId,
|
|
3691
|
+
policy_fingerprint: effectivePolicyFingerprint,
|
|
3692
|
+
monitor_pid: bridgeMonitor.monitorPid ?? null,
|
|
3693
|
+
monitor_handoff_pid: bridgeMonitor.handoffWatchdog?.pid ?? null
|
|
3694
|
+
});
|
|
3100
3695
|
});
|
|
3101
3696
|
}
|
|
3102
3697
|
finally {
|
|
@@ -3171,7 +3766,7 @@ async function runTerminalConversationApprove({ options, conversationId, agent,
|
|
|
3171
3766
|
releaseTerminalLock();
|
|
3172
3767
|
}
|
|
3173
3768
|
}
|
|
3174
|
-
async function runTerminalControlSend({ options, conversation, nextConversation, statePath, logPath, executor, message, terminalControl, terminalSendLockHeld = false, terminalStateLockHeld = false, recordMessageAfterSend = false, recordRawAttachmentAfterSend = false }) {
|
|
3769
|
+
async function runTerminalControlSend({ options, conversation, nextConversation, statePath, logPath, executor, message, terminalControl, terminalSendLockHeld = false, terminalStateLockHeld = false, storeWriterLeaseHeld = false, recordMessageAfterSend = false, recordRawAttachmentAfterSend = false }) {
|
|
3175
3770
|
const bridge = terminalBridgeEnabled(conversation);
|
|
3176
3771
|
if (!terminalSendLockHeld) {
|
|
3177
3772
|
const releaseTerminalLock = acquireFileLock(terminalBridgeSendLockPath(storeDirFromOptions(options), terminalControl), { timeoutMs: 30000 });
|
|
@@ -3187,6 +3782,7 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
3187
3782
|
terminalControl,
|
|
3188
3783
|
terminalSendLockHeld: true,
|
|
3189
3784
|
terminalStateLockHeld,
|
|
3785
|
+
storeWriterLeaseHeld,
|
|
3190
3786
|
recordMessageAfterSend,
|
|
3191
3787
|
recordRawAttachmentAfterSend
|
|
3192
3788
|
});
|
|
@@ -3222,6 +3818,7 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
3222
3818
|
terminalControl,
|
|
3223
3819
|
terminalSendLockHeld: true,
|
|
3224
3820
|
terminalStateLockHeld: true,
|
|
3821
|
+
storeWriterLeaseHeld,
|
|
3225
3822
|
recordMessageAfterSend,
|
|
3226
3823
|
recordRawAttachmentAfterSend
|
|
3227
3824
|
});
|
|
@@ -3230,6 +3827,24 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
|
|
|
3230
3827
|
releaseStateLock();
|
|
3231
3828
|
}
|
|
3232
3829
|
}
|
|
3830
|
+
if (!storeWriterLeaseHeld) {
|
|
3831
|
+
const writerStoreDir = pathsForConversationDir(path.dirname(statePath)).storeDir;
|
|
3832
|
+
return await withStoreWriterLeaseAsync(writerStoreDir, async () => runTerminalControlSend({
|
|
3833
|
+
options,
|
|
3834
|
+
conversation,
|
|
3835
|
+
nextConversation,
|
|
3836
|
+
statePath,
|
|
3837
|
+
logPath,
|
|
3838
|
+
executor,
|
|
3839
|
+
message,
|
|
3840
|
+
terminalControl,
|
|
3841
|
+
terminalSendLockHeld: true,
|
|
3842
|
+
terminalStateLockHeld,
|
|
3843
|
+
storeWriterLeaseHeld: true,
|
|
3844
|
+
recordMessageAfterSend,
|
|
3845
|
+
recordRawAttachmentAfterSend
|
|
3846
|
+
}));
|
|
3847
|
+
}
|
|
3233
3848
|
const terminalBridge = createTerminalAgentBridge(options);
|
|
3234
3849
|
const bridgeStartedAt = new Date().toISOString();
|
|
3235
3850
|
const agentTimeoutMinutes = Number(options.agentTimeoutMinutes ?? DEFAULT_AGENT_TIMEOUT_MINUTES);
|
|
@@ -3744,7 +4359,6 @@ function terminalSubmissionPayload(payload) {
|
|
|
3744
4359
|
function createManagedTerminalConversationFromRawId({ options, conversationId, agent, pid, messageBody, terminalControl }) {
|
|
3745
4360
|
const workspace = terminalControl.currentPath ?? process.cwd();
|
|
3746
4361
|
const storeDir = expandHome(options.storeDir ?? options.logDir ?? defaultStoreDir(workspace));
|
|
3747
|
-
cleanupIdleConversations(storeDir, options);
|
|
3748
4362
|
const executor = resolveExecutor({
|
|
3749
4363
|
kind: agent,
|
|
3750
4364
|
session: conversationId
|
|
@@ -3969,8 +4583,15 @@ async function runRenew(options) {
|
|
|
3969
4583
|
});
|
|
3970
4584
|
}
|
|
3971
4585
|
async function runReconcileMonitors(options) {
|
|
4586
|
+
printJson(await reconcileMonitors(options, {
|
|
4587
|
+
includeCallbackRecovery: true,
|
|
4588
|
+
reason: "startup_reconciliation",
|
|
4589
|
+
conversationId: undefined
|
|
4590
|
+
}));
|
|
4591
|
+
}
|
|
4592
|
+
async function reconcileMonitors(options, { includeCallbackRecovery, reason, conversationId }) {
|
|
3972
4593
|
const storeDir = storeDirFromOptions(options);
|
|
3973
|
-
const conversations = listConversations(storeDir);
|
|
4594
|
+
const conversations = listConversations(storeDir).filter((conversation) => conversationId === undefined || conversation.conversation_id === conversationId);
|
|
3974
4595
|
const items = [];
|
|
3975
4596
|
let ignored = 0;
|
|
3976
4597
|
let launched = 0;
|
|
@@ -3987,30 +4608,32 @@ async function runReconcileMonitors(options) {
|
|
|
3987
4608
|
const logPath = expandHome(stringValue(listedConversation.event_log_path) ??
|
|
3988
4609
|
logPathForStatePath(statePath));
|
|
3989
4610
|
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 })
|
|
4611
|
+
if (includeCallbackRecovery) {
|
|
4612
|
+
const callbackRecovery = prepareCallbackDeliveryReconciliation({
|
|
4613
|
+
statePath,
|
|
4614
|
+
logPath,
|
|
4615
|
+
delayMs: options.callbackRetryDelayMs
|
|
4012
4616
|
});
|
|
4013
|
-
|
|
4617
|
+
if (callbackRecovery.handled) {
|
|
4618
|
+
if (callbackRecovery.status === "launched") {
|
|
4619
|
+
launched += 1;
|
|
4620
|
+
}
|
|
4621
|
+
else if (callbackRecovery.status === "already_running") {
|
|
4622
|
+
alreadyRunning += 1;
|
|
4623
|
+
}
|
|
4624
|
+
else {
|
|
4625
|
+
skipped += 1;
|
|
4626
|
+
}
|
|
4627
|
+
items.push({
|
|
4628
|
+
conversation_id: callbackRecovery.conversationId,
|
|
4629
|
+
status: callbackRecovery.status,
|
|
4630
|
+
reason: callbackRecovery.reason,
|
|
4631
|
+
...(callbackRecovery.monitorPid === undefined
|
|
4632
|
+
? {}
|
|
4633
|
+
: { monitor_pid: callbackRecovery.monitorPid })
|
|
4634
|
+
});
|
|
4635
|
+
continue;
|
|
4636
|
+
}
|
|
4014
4637
|
}
|
|
4015
4638
|
const listedNativeTakeover = isRecord(listedConversation.native_session_takeover)
|
|
4016
4639
|
? listedConversation.native_session_takeover
|
|
@@ -4125,7 +4748,7 @@ async function runReconcileMonitors(options) {
|
|
|
4125
4748
|
event: "terminal_bridge_monitor_launch",
|
|
4126
4749
|
pid: monitor.pid ?? null,
|
|
4127
4750
|
terminal_control: prepared.terminalControl,
|
|
4128
|
-
reason
|
|
4751
|
+
reason,
|
|
4129
4752
|
agent_timeout_minutes: prepared.inactivityTimeoutMinutes,
|
|
4130
4753
|
agent_hard_timeout_minutes: prepared.hardTimeoutMinutes
|
|
4131
4754
|
});
|
|
@@ -4138,7 +4761,7 @@ async function runReconcileMonitors(options) {
|
|
|
4138
4761
|
items.push({
|
|
4139
4762
|
conversation_id: prepared.conversation.conversation_id,
|
|
4140
4763
|
status: "launched",
|
|
4141
|
-
reason
|
|
4764
|
+
reason,
|
|
4142
4765
|
monitor_pid: monitor.pid ?? null
|
|
4143
4766
|
});
|
|
4144
4767
|
}
|
|
@@ -4151,7 +4774,7 @@ async function runReconcileMonitors(options) {
|
|
|
4151
4774
|
});
|
|
4152
4775
|
}
|
|
4153
4776
|
}
|
|
4154
|
-
|
|
4777
|
+
return {
|
|
4155
4778
|
reconciled: true,
|
|
4156
4779
|
store_dir: storeDir,
|
|
4157
4780
|
checked: conversations.length,
|
|
@@ -4161,7 +4784,7 @@ async function runReconcileMonitors(options) {
|
|
|
4161
4784
|
skipped,
|
|
4162
4785
|
errors,
|
|
4163
4786
|
items
|
|
4164
|
-
}
|
|
4787
|
+
};
|
|
4165
4788
|
}
|
|
4166
4789
|
function prepareCallbackDeliveryReconciliation({ statePath, logPath, delayMs }) {
|
|
4167
4790
|
const releaseStateLock = acquireFileLock(`${statePath}.lock`);
|
|
@@ -4404,7 +5027,6 @@ function positiveMinutes(value, optionName) {
|
|
|
4404
5027
|
return parsed;
|
|
4405
5028
|
}
|
|
4406
5029
|
async function runCancel(options) {
|
|
4407
|
-
cleanupIdleConversations(storeDirFromOptions(options), options);
|
|
4408
5030
|
const terminalConversation = await resolveTerminalConversationFromOptions(options);
|
|
4409
5031
|
if (terminalConversation) {
|
|
4410
5032
|
await runTerminalConversationCancel({
|
|
@@ -4484,75 +5106,78 @@ async function runTerminalControlCancel({ options, statePath, logPath, agent, te
|
|
|
4484
5106
|
let releaseStateLock;
|
|
4485
5107
|
try {
|
|
4486
5108
|
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({
|
|
5109
|
+
const writerStoreDir = pathsForConversationDir(path.dirname(statePath)).storeDir;
|
|
5110
|
+
return await withStoreWriterLeaseAsync(writerStoreDir, async () => {
|
|
5111
|
+
const currentConversation = loadState(statePath);
|
|
5112
|
+
if (!["waiting_for_agent", "waiting_for_openclaw"].includes(currentConversation.status)) {
|
|
5113
|
+
throw new Error(`cannot cancel ${currentConversation.conversation_id}; conversation is ${currentConversation.status}`);
|
|
5114
|
+
}
|
|
5115
|
+
const currentTakeover = isRecord(currentConversation.native_session_takeover)
|
|
5116
|
+
? currentConversation.native_session_takeover
|
|
5117
|
+
: undefined;
|
|
5118
|
+
const currentControl = terminalControlFromTakeover(currentTakeover);
|
|
5119
|
+
if (!currentControl ||
|
|
5120
|
+
currentControl.target !== terminalControl.target ||
|
|
5121
|
+
currentControl.socketPath !== terminalControl.socketPath) {
|
|
5122
|
+
throw new Error("terminal control changed while waiting to cancel; refresh status and retry");
|
|
5123
|
+
}
|
|
5124
|
+
assertManagedTerminalDispatchOwner({
|
|
4511
5125
|
conversation: currentConversation,
|
|
4512
|
-
|
|
4513
|
-
|
|
5126
|
+
terminalControl: currentControl,
|
|
5127
|
+
action: "cancel"
|
|
5128
|
+
});
|
|
5129
|
+
const cancellation = await createTerminalAgentBridge(options).cancel(agent, currentControl, {
|
|
5130
|
+
runtime: terminalRuntimeIdentityForConversation(currentConversation, currentControl),
|
|
5131
|
+
scrollbackLines: Number(options.scrollbackLines ?? 120)
|
|
5132
|
+
});
|
|
5133
|
+
if (!cancellation.cancelRequested) {
|
|
5134
|
+
printJson({
|
|
5135
|
+
conversation: currentConversation,
|
|
5136
|
+
cancel_requested: false,
|
|
5137
|
+
reason: cancellation.reason,
|
|
5138
|
+
terminal_control: currentControl,
|
|
5139
|
+
budget: budgetAction(currentConversation)
|
|
5140
|
+
});
|
|
5141
|
+
return;
|
|
5142
|
+
}
|
|
5143
|
+
const now = new Date().toISOString();
|
|
5144
|
+
appendEvent(logPath, {
|
|
5145
|
+
ts: now,
|
|
5146
|
+
conversation_id: currentConversation.conversation_id,
|
|
5147
|
+
event: "terminal_cancel_requested",
|
|
4514
5148
|
terminal_control: currentControl,
|
|
4515
|
-
|
|
5149
|
+
key: cancellation.key,
|
|
5150
|
+
keys: cancellation.keys,
|
|
5151
|
+
denied_approval: cancellation.deniedApproval,
|
|
5152
|
+
request_id: cancellation.requestId
|
|
5153
|
+
});
|
|
5154
|
+
runtimeLog("info", "terminal_cancel_requested", {
|
|
5155
|
+
conversation_id: currentConversation.conversation_id,
|
|
5156
|
+
agent,
|
|
5157
|
+
terminal_target: currentControl.target,
|
|
5158
|
+
key: cancellation.key,
|
|
5159
|
+
keys: cancellation.keys,
|
|
5160
|
+
denied_approval: cancellation.deniedApproval,
|
|
5161
|
+
request_id: cancellation.requestId
|
|
5162
|
+
});
|
|
5163
|
+
const nextConversation = {
|
|
5164
|
+
...currentConversation,
|
|
5165
|
+
status: "cancelled",
|
|
5166
|
+
cancelled_at: now,
|
|
5167
|
+
terminal_cancel_requested_at: now,
|
|
5168
|
+
updated_at: now
|
|
5169
|
+
};
|
|
5170
|
+
saveState(statePath, nextConversation);
|
|
5171
|
+
printJson({
|
|
5172
|
+
conversation: nextConversation,
|
|
5173
|
+
cancel_requested: true,
|
|
5174
|
+
terminal_control: currentControl,
|
|
5175
|
+
key: cancellation.key,
|
|
5176
|
+
keys: cancellation.keys,
|
|
5177
|
+
denied_approval: cancellation.deniedApproval,
|
|
5178
|
+
request_id: cancellation.requestId,
|
|
5179
|
+
budget: budgetAction(nextConversation)
|
|
4516
5180
|
});
|
|
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
5181
|
});
|
|
4557
5182
|
}
|
|
4558
5183
|
finally {
|
|
@@ -5983,7 +6608,7 @@ function terminalBridgeRuntimeDir() {
|
|
|
5983
6608
|
const configured = stringValue(process.env.AKK_RUNTIME_DIR);
|
|
5984
6609
|
return configured
|
|
5985
6610
|
? path.resolve(expandHome(configured))
|
|
5986
|
-
: path.join(path.dirname(defaultStoreDir()), "runtime");
|
|
6611
|
+
: path.join(path.dirname(defaultStoreDir()), "runtime-v2");
|
|
5987
6612
|
}
|
|
5988
6613
|
function terminalBridgeRuntimeKey(terminalControl) {
|
|
5989
6614
|
return createHash("sha256")
|
|
@@ -5996,7 +6621,6 @@ function terminalBridgeRuntimeKey(terminalControl) {
|
|
|
5996
6621
|
}
|
|
5997
6622
|
function terminalBridgeDispatchLedgerPath(terminalControl) {
|
|
5998
6623
|
const ledgerDir = path.join(terminalBridgeRuntimeDir(), "terminal-dispatch");
|
|
5999
|
-
ensureDir(ledgerDir);
|
|
6000
6624
|
return path.join(ledgerDir, `terminal-dispatch-${terminalBridgeRuntimeKey(terminalControl)}.json`);
|
|
6001
6625
|
}
|
|
6002
6626
|
function loadTerminalBridgeDispatchLedger(terminalControl) {
|
|
@@ -6043,6 +6667,7 @@ function orphanedTerminalDispatchForRecovery(terminalControl) {
|
|
|
6043
6667
|
}
|
|
6044
6668
|
function saveTerminalBridgeDispatchLedger(terminalControl, ledger) {
|
|
6045
6669
|
const ledgerPath = terminalBridgeDispatchLedgerPath(terminalControl);
|
|
6670
|
+
ensureDir(path.dirname(ledgerPath));
|
|
6046
6671
|
if (fs.existsSync(ledgerPath) && fs.lstatSync(ledgerPath).isSymbolicLink()) {
|
|
6047
6672
|
throw new Error(`terminal dispatch ledger is a symlink: ${ledgerPath}`);
|
|
6048
6673
|
}
|
|
@@ -7046,6 +7671,7 @@ function runPreparedCallback(prepared, { emit = true } = {}) {
|
|
|
7046
7671
|
}
|
|
7047
7672
|
return result;
|
|
7048
7673
|
}
|
|
7674
|
+
assertStoreWriterCompatible(pathsForConversationDir(path.dirname(prepared.statePath)).storeDir);
|
|
7049
7675
|
try {
|
|
7050
7676
|
const deliveryKind = deliverCallbackToOpenClaw({
|
|
7051
7677
|
options: prepared.options,
|
|
@@ -7591,7 +8217,13 @@ function loadConversationFromOptions(options) {
|
|
|
7591
8217
|
throw new Error("--conversation or --state is required");
|
|
7592
8218
|
}
|
|
7593
8219
|
const conversation = options.state
|
|
7594
|
-
?
|
|
8220
|
+
? (() => {
|
|
8221
|
+
const paths = pathsForConversationDir(path.dirname(statePath));
|
|
8222
|
+
if (path.resolve(paths.statePath) !== path.resolve(statePath)) {
|
|
8223
|
+
throw new Error(`AKK state path is not canonical: ${statePath}`);
|
|
8224
|
+
}
|
|
8225
|
+
return loadConversationById(path.basename(paths.conversationDir), paths.storeDir);
|
|
8226
|
+
})()
|
|
7595
8227
|
: loadConversationById(conversationId, storeDir);
|
|
7596
8228
|
assertConfiguredWorkspace(options.workspace, conversation.workspace, `access to AKK conversation ${conversation.conversation_id}`);
|
|
7597
8229
|
return {
|
|
@@ -8204,13 +8836,21 @@ function deliverStalledNotification({ statePath, logPath, conversation, message,
|
|
|
8204
8836
|
stderr: textSummary(chatSendDelivery.stderr)
|
|
8205
8837
|
});
|
|
8206
8838
|
}
|
|
8207
|
-
function
|
|
8839
|
+
function reconcileIdleConversations(storeDir, options = {}, now = new Date(), conversationId) {
|
|
8208
8840
|
const timeoutMinutes = Number(options.idleTimeoutMinutes ?? DEFAULT_IDLE_TIMEOUT_MINUTES);
|
|
8209
8841
|
if (!Number.isFinite(timeoutMinutes) || timeoutMinutes <= 0) {
|
|
8210
|
-
return {
|
|
8842
|
+
return {
|
|
8843
|
+
checked: 0,
|
|
8844
|
+
closed: 0,
|
|
8845
|
+
skipped: 0,
|
|
8846
|
+
idle_timeout_minutes: timeoutMinutes
|
|
8847
|
+
};
|
|
8211
8848
|
}
|
|
8212
|
-
|
|
8849
|
+
ensureStoreWritable(storeDir);
|
|
8850
|
+
const conversations = listConversations(storeDir).filter((conversation) => (conversationId === undefined || conversation.conversation_id === conversationId) &&
|
|
8851
|
+
matchesConfiguredWorkspace(options.workspace, conversation.workspace));
|
|
8213
8852
|
let closed = 0;
|
|
8853
|
+
let skipped = 0;
|
|
8214
8854
|
for (const listedConversation of conversations) {
|
|
8215
8855
|
if (listedConversation.status !== "idle" || !listedConversation.idle_since) {
|
|
8216
8856
|
continue;
|
|
@@ -8230,6 +8870,7 @@ function cleanupIdleConversations(storeDir, options = {}, now = new Date()) {
|
|
|
8230
8870
|
}
|
|
8231
8871
|
catch (error) {
|
|
8232
8872
|
if (isRecord(error) && error.code === "LOCK_TIMEOUT") {
|
|
8873
|
+
skipped += 1;
|
|
8233
8874
|
continue;
|
|
8234
8875
|
}
|
|
8235
8876
|
throw error;
|
|
@@ -8288,6 +8929,7 @@ function cleanupIdleConversations(storeDir, options = {}, now = new Date()) {
|
|
|
8288
8929
|
return {
|
|
8289
8930
|
checked: conversations.length,
|
|
8290
8931
|
closed,
|
|
8932
|
+
skipped,
|
|
8291
8933
|
idle_timeout_minutes: timeoutMinutes
|
|
8292
8934
|
};
|
|
8293
8935
|
}
|
|
@@ -8754,8 +9396,8 @@ function usage() {
|
|
|
8754
9396
|
agent-knock-knock --help
|
|
8755
9397
|
agent-knock-knock --version
|
|
8756
9398
|
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] [--
|
|
8758
|
-
agent-knock-knock status [--conversation <id|selector>] [--store-dir <dir>] [--trace]
|
|
9399
|
+
agent-knock-knock list [--store-dir <dir>] [--agent ${agentList}] [--status <status>] [--all] [--reconcile] [--no-approval-scan] [--terminal-debug]
|
|
9400
|
+
agent-knock-knock status [--conversation <id|selector>] [--store-dir <dir>] [--reconcile] [--trace]
|
|
8759
9401
|
agent-knock-knock send [--conversation <id|selector>] --message <text> [--type answer|task|control] [--agent-timeout-minutes <minutes>] [--agent-hard-timeout-minutes <minutes>]
|
|
8760
9402
|
agent-knock-knock approve [--conversation <id|selector>] --expected-approval-fingerprint <fingerprint>
|
|
8761
9403
|
agent-knock-knock cancel [--conversation <id|selector>]
|