@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/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) => candidate.activity_state === "idle");
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 cleanup = cleanupIdleConversations(storeDir, options);
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 storedConversations = allStoredConversations
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 managedTerminalKeys = new Set(allStoredConversations
1349
- .filter((conversation) => isActiveStatus(conversation.status))
1350
- .filter((conversation) => matchesConfiguredWorkspace(options.workspace, conversation.workspace))
1351
- .map((conversation) => terminalControlSelectorKey(terminalControlFromTakeover(isRecord(conversation.native_session_takeover)
1352
- ? conversation.native_session_takeover
1353
- : undefined)))
1354
- .filter((key) => key !== undefined));
1355
- const terminalControlled = terminalScan.terminalControlled.filter((entry) => {
1356
- if (!matchesConfiguredWorkspace(options.workspace, entry.workspace ?? entry.cwd)) {
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
- cleanup,
1392
+ store,
1393
+ reconciliation,
1365
1394
  action_contracts: listActionContracts(),
1366
- delegated,
1367
- terminal_controlled: terminalControlled,
1395
+ terminals: projection.terminals,
1396
+ unavailable_managed_turns: projection.unavailableManagedTurns,
1368
1397
  terminal_scan: {
1369
1398
  ...terminalScan.summary,
1370
- terminal_controlled_count: terminalControlled.length
1371
- },
1372
- tasks: conversations
1399
+ terminal_count: projection.terminals.length
1400
+ }
1373
1401
  });
1374
- runtimeLog("info", "tasks_listed", {
1402
+ runtimeLog("info", "terminals_listed", {
1375
1403
  store_dir: storeDir,
1376
- returned_count: conversations.length,
1377
- terminal_controlled_count: terminalControlled.length,
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
- cleanup
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
- terminal_controlled_count: terminalControlled.length,
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 delegatedListEntry(task, { terminalBridge = false, approvalState, conversation } = {}) {
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: "akk_delegate",
1520
+ source: "managed_turn",
1471
1521
  ...(approvalState ? { approval_state: approvalState } : {}),
1472
1522
  commands: {
1473
- send: canSendDelegated(task.status),
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
- ...entry,
1482
- available_actions: availableListActions(entry, { conversation })
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: "terminal_control",
1552
+ source: "terminal",
1501
1553
  agent: session.agent,
1502
- status: "active",
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
- ...entry,
1538
- available_actions: availableListActions(entry)
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 canSendDelegated(status) {
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: 2,
2124
+ version: 3,
1644
2125
  instructions: [
1645
- "Use only actions present in delegated[].available_actions or terminal_controlled[].available_actions.",
1646
- "Never use commands for routing or tool calls. It is a deprecated, non-authoritative compatibility field with mixed legacy semantics.",
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
- delegated: "task_lifecycle",
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
- terminal_controlled: "screen_activity_classification",
2142
+ terminals: "terminal_screen_activity_classification",
1659
2143
  authoritative_for_tool_calls: false
1660
2144
  },
1661
- commands: {
1662
- meaning: "legacy_compatibility_flags_with_mixed_semantics",
1663
- deprecated: true,
1664
- authoritative_for_tool_calls: false
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. Omit timeout fields unless the user explicitly asks to change monitoring limits."
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 === "terminal_control";
1740
- const managed = entry.source === "akk_delegate";
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.send = {
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
- cleanupIdleConversations(storeDir, options);
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) => delegatedListEntry(summarizeConversation(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: commandName === "approve"
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
- ...terminalScan.terminalControlled.filter((entry) => {
1896
- if (!matchesConfiguredWorkspace(options.workspace, entry.workspace ?? entry.cwd)) {
1897
- return false;
1898
- }
1899
- const key = terminalControlSelectorKey(entry.terminal_control);
1900
- return key === undefined || !managedTerminalKeys.has(key);
1901
- })
1902
- ].map((entry) => ({
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: sessionEntrySupportsCommand(entry, commandName),
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
- cleanupIdleConversations(storeDirFromOptions(options), options);
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 = await migrateLegacyTerminalAgentIdentity({
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
- let approval;
2785
- let lockedConversation = conversation;
2786
- const currentConversation = loadState(statePath);
2787
- const currentTakeover = isRecord(currentConversation.native_session_takeover)
2788
- ? currentConversation.native_session_takeover
2789
- : undefined;
2790
- const currentControl = terminalControlFromTakeover(currentTakeover);
2791
- const currentApproval = isRecord(currentTakeover?.terminal_bridge_approval)
2792
- ? currentTakeover.terminal_bridge_approval
2793
- : undefined;
2794
- if (currentConversation.status !== conversation.status ||
2795
- currentTakeover?.terminal_bridge_message_id !== nativeTakeover?.terminal_bridge_message_id ||
2796
- currentControl?.target !== terminalControl.target ||
2797
- currentControl?.socketPath !== terminalControl.socketPath ||
2798
- (claudeScreenApproval &&
2799
- currentApproval?.fingerprint !== monitoredApproval?.fingerprint)) {
2800
- throw new Error("approval state changed while waiting for terminal control; refresh status and retry");
2801
- }
2802
- assertManagedTerminalDispatchOwner({
2803
- conversation: currentConversation,
2804
- terminalControl: currentControl,
2805
- action: "approve"
2806
- });
2807
- lockedConversation = currentConversation;
2808
- approval = await createTerminalAgentBridge(options).approve(executor.kind, terminalControl, {
2809
- expectedFingerprint,
2810
- scrollbackLines: Number(options.scrollbackLines ?? 120),
2811
- runtime: runtimeIdentity,
2812
- managedRequest: terminalDurableRequestForConversation(currentConversation, terminalControl),
2813
- requiredDecisionMode: autoApproved && executor.kind === "claude" ? "keys" : undefined,
2814
- authorize: autoApproved
2815
- ? ({ agent, terminalControl: currentTerminalControl, inspection, fingerprint }) => {
2816
- if (!autoApprovalPolicy) {
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
- throw new Error("automatic approval requires an executor-side policy before dispatch");
3408
+ return {
3409
+ approved: false,
3410
+ reason: "automatic approval requires an executor-side policy"
3411
+ };
2859
3412
  }
2860
- const freshPolicyDecision = evaluateApprovalPolicy({
3413
+ const candidate = policyCandidateForInspection({
3414
+ agent,
3415
+ currentTerminalControl,
3416
+ inspection,
3417
+ fingerprint
3418
+ });
3419
+ executorPolicyDecision = evaluateApprovalPolicy({
2861
3420
  policy: autoApprovalPolicy,
2862
- candidate: policyCandidateForInspection({
2863
- agent: executor.kind,
2864
- currentTerminalControl: dispatchControl,
2865
- inspection,
2866
- fingerprint
2867
- })
3421
+ candidate
2868
3422
  });
2869
- if (freshPolicyDecision.action !== "approve") {
2870
- throw new Error(`executor-side auto-approval policy rejected the recaptured request: ${freshPolicyDecision.reason}`);
2871
- }
2872
- if (executorPolicyDecision?.ruleId &&
2873
- freshPolicyDecision.ruleId !== executorPolicyDecision.ruleId) {
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 && freshPolicyDecision.ruleId !== policyRuleId) {
2877
- throw new Error("executor-side auto-approval rule changed before dispatch");
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
- freshPolicyDecision.policyFingerprint !== policyFingerprint) {
2881
- throw new Error("executor-side auto-approval policy changed before dispatch");
3436
+ executorPolicyDecision.policyFingerprint !== policyFingerprint) {
3437
+ return {
3438
+ approved: false,
3439
+ reason: "executor-side auto-approval policy changed before execution"
3440
+ };
2882
3441
  }
2883
- executorPolicyDecision = freshPolicyDecision;
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
- if (latestDispatch?.state === "reserved" &&
2927
- latestDispatch.terminal_bridge_message_id ===
2928
- latestTakeover.terminal_bridge_message_id) {
2929
- throw new Error("a previous Claude approval dispatch has an uncertain outcome; inspect and resolve the terminal manually");
2930
- }
2931
- const reservedAt = new Date().toISOString();
2932
- const reservedConversation = {
2933
- ...latestConversation,
2934
- native_session_takeover: {
2935
- ...latestTakeover,
2936
- terminal_bridge_approval_dispatch: {
2937
- state: "reserved",
2938
- attempt_id: randomUUID(),
2939
- fingerprint,
2940
- keys,
2941
- terminal_target: dispatchControl.target,
2942
- terminal_bridge_message_id: latestTakeover.terminal_bridge_message_id,
2943
- reserved_at: reservedAt
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
- updated_at: reservedAt
2947
- };
2948
- saveState(statePath, reservedConversation);
2949
- lockedConversation = reservedConversation;
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
- : undefined
2952
- });
2953
- const actualFingerprint = approval.fingerprint;
2954
- const effectivePolicyRuleId = executorPolicyDecision?.ruleId ?? policyRuleId;
2955
- const effectivePolicyFingerprint = executorPolicyDecision?.policyFingerprint ?? policyFingerprint;
2956
- if (!approval.approved) {
2957
- releaseApprovalStateLock();
2958
- releaseApprovalTerminalLock();
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: "rejected",
2965
- reason: approval.reason,
3599
+ action: "approved",
2966
3600
  terminal_control: terminalControl,
2967
- expected_fingerprint: expectedFingerprint,
2968
- actual_fingerprint: actualFingerprint,
3601
+ approval_fingerprint: actualFingerprint,
2969
3602
  policy_rule_id: effectivePolicyRuleId,
2970
3603
  policy_fingerprint: effectivePolicyFingerprint
2971
3604
  });
2972
3605
  }
2973
- printJson({
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
- event: "terminal_auto_approval_decision",
3005
- action: "approved",
3006
- terminal_control: terminalControl,
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
- runtimeLog("info", "terminal_approval_send", {
3013
- conversation_id: conversation.conversation_id,
3014
- terminal_target: terminalControl.target,
3015
- key: approval.key,
3016
- keys: approval.keys,
3017
- label: approval.label,
3018
- decision_mode: approval.decisionMode,
3019
- request_id: approval.requestId,
3020
- approval_fingerprint: actualFingerprint,
3021
- auto_approved: autoApproved,
3022
- policy_rule_id: effectivePolicyRuleId,
3023
- policy_fingerprint: effectivePolicyFingerprint
3024
- });
3025
- const nativeTakeoverForUpdate = isRecord(lockedConversation.native_session_takeover)
3026
- ? { ...lockedConversation.native_session_takeover }
3027
- : {};
3028
- const resolvedApproval = isRecord(nativeTakeoverForUpdate.terminal_bridge_approval)
3029
- ? nativeTakeoverForUpdate.terminal_bridge_approval
3030
- : undefined;
3031
- const resolvedApprovalScreenDigest = stringValue(resolvedApproval?.screen_digest);
3032
- const resolvedApprovalState = isRecord(resolvedApproval?.approval_state)
3033
- ? resolvedApproval.approval_state
3034
- : undefined;
3035
- const resolvedTranscriptIdentity = claudeTranscriptApprovalIdentity(resolvedApprovalState);
3036
- const approvalResolvedAt = new Date().toISOString();
3037
- const agentTimeoutMinutes = Number(options.agentTimeoutMinutes ??
3038
- nativeTakeoverForUpdate.terminal_bridge_inactivity_timeout_minutes ??
3039
- DEFAULT_AGENT_TIMEOUT_MINUTES);
3040
- const agentHardTimeoutMinutes = positiveMinutes(options.agentHardTimeoutMinutes ??
3041
- nativeTakeoverForUpdate.terminal_bridge_hard_timeout_minutes ??
3042
- DEFAULT_AGENT_HARD_TIMEOUT_MINUTES, "--agent-hard-timeout-minutes");
3043
- const nextNativeTakeover = {
3044
- ...nativeTakeoverForUpdate,
3045
- terminal_bridge_approval: undefined,
3046
- terminal_bridge_approval_dispatch: undefined,
3047
- terminal_bridge_approval_resolved_at: approvalResolvedAt,
3048
- terminal_bridge_last_approval_fingerprint: actualFingerprint,
3049
- terminal_bridge_last_approval_screen_digest: resolvedApprovalScreenDigest,
3050
- terminal_bridge_last_approval_request_id: resolvedTranscriptIdentity?.requestId,
3051
- terminal_bridge_last_approval_evidence_fingerprint: resolvedTranscriptIdentity?.evidenceFingerprint,
3052
- terminal_bridge_last_approval_prompt_cleared_at: undefined,
3053
- terminal_bridge_last_approval_at: approvalResolvedAt,
3054
- terminal_bridge_last_approval_message_id: nativeTakeoverForUpdate.terminal_bridge_message_id,
3055
- terminal_bridge_monitor_lock_version: TERMINAL_BRIDGE_MONITOR_LOCK_VERSION,
3056
- terminal_bridge_monitor_started_at: approvalResolvedAt,
3057
- terminal_bridge_last_activity_at: approvalResolvedAt,
3058
- terminal_bridge_last_activity_reason: "approval resolved",
3059
- terminal_bridge_inactivity_timeout_minutes: agentTimeoutMinutes,
3060
- terminal_bridge_hard_timeout_minutes: agentHardTimeoutMinutes,
3061
- terminal_bridge_inactivity_deadline_at: deadlineAt(approvalResolvedAt, agentTimeoutMinutes),
3062
- terminal_bridge_hard_deadline_at: deadlineAt(stringValue(nativeTakeoverForUpdate.terminal_bridge_started_at) ?? approvalResolvedAt, agentHardTimeoutMinutes)
3063
- };
3064
- delete nextNativeTakeover.terminal_bridge_approval;
3065
- delete nextNativeTakeover.terminal_bridge_approval_dispatch;
3066
- delete nextNativeTakeover.terminal_bridge_last_approval_prompt_cleared_at;
3067
- const nextConversation = {
3068
- ...lockedConversation,
3069
- status: terminalBridgeEnabled(lockedConversation)
3070
- ? "waiting_for_agent"
3071
- : lockedConversation.status,
3072
- native_session_takeover: nextNativeTakeover,
3073
- updated_at: approvalResolvedAt
3074
- };
3075
- saveState(statePath, nextConversation);
3076
- releaseApprovalStateLock();
3077
- releaseApprovalTerminalLock();
3078
- const bridgeMonitor = ensureTerminalBridgeMonitorAfterApproval({
3079
- conversation: nextConversation,
3080
- statePath,
3081
- logPath,
3082
- terminalControl,
3083
- options
3084
- });
3085
- printJson({
3086
- conversation: nextConversation,
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
- const callbackRecovery = prepareCallbackDeliveryReconciliation({
3991
- statePath,
3992
- logPath,
3993
- delayMs: options.callbackRetryDelayMs
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
- continue;
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: "startup_reconciliation",
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: "startup_reconciliation",
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
- printJson({
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 currentConversation = loadState(statePath);
4488
- if (!["waiting_for_agent", "waiting_for_openclaw"].includes(currentConversation.status)) {
4489
- throw new Error(`cannot cancel ${currentConversation.conversation_id}; conversation is ${currentConversation.status}`);
4490
- }
4491
- const currentTakeover = isRecord(currentConversation.native_session_takeover)
4492
- ? currentConversation.native_session_takeover
4493
- : undefined;
4494
- const currentControl = terminalControlFromTakeover(currentTakeover);
4495
- if (!currentControl ||
4496
- currentControl.target !== terminalControl.target ||
4497
- currentControl.socketPath !== terminalControl.socketPath) {
4498
- throw new Error("terminal control changed while waiting to cancel; refresh status and retry");
4499
- }
4500
- assertManagedTerminalDispatchOwner({
4501
- conversation: currentConversation,
4502
- terminalControl: currentControl,
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
- cancel_requested: false,
4513
- reason: cancellation.reason,
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
- budget: budgetAction(currentConversation)
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
- ? loadState(statePath)
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 cleanupIdleConversations(storeDir, options = {}, now = new Date()) {
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 { checked: 0, closed: 0, idle_timeout_minutes: timeoutMinutes };
8842
+ return {
8843
+ checked: 0,
8844
+ closed: 0,
8845
+ skipped: 0,
8846
+ idle_timeout_minutes: timeoutMinutes
8847
+ };
8211
8848
  }
8212
- const conversations = listConversations(storeDir);
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] [--managed-only] [--no-approval-scan] [--terminal-debug]
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>]