@scotthuang/agent-knock-knock 0.7.0 → 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
@@ -930,7 +930,15 @@ async function runDelegate(options) {
930
930
  return false;
931
931
  }
932
932
  });
933
- 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
+ });
934
942
  if (eligible.length === 0) {
935
943
  const observed = scopedCandidates.length > 0
936
944
  ? ` Found ${scopedCandidates.length} matching pane(s), but none is idle.`
@@ -1350,6 +1358,7 @@ function environmentWithoutGatewayTokens() {
1350
1358
  }
1351
1359
  async function runList(options) {
1352
1360
  const storeDir = expandHome(options.storeDir ?? options.logDir ?? defaultStoreDir(process.cwd()));
1361
+ const store = inspectStoreCompatibility(storeDir);
1353
1362
  const reconciliation = options.reconcile === true
1354
1363
  ? await reconcileStoreForList(storeDir, options)
1355
1364
  : {
@@ -1360,50 +1369,40 @@ async function runList(options) {
1360
1369
  const agentFilter = options.agent ? resolveExecutor({ kind: options.agent }).kind : undefined;
1361
1370
  const statusFilter = options.status;
1362
1371
  const allStoredConversations = listConversations(storeDir);
1363
- const storedConversations = allStoredConversations
1364
- .filter(isDiscoverableTmuxConversation)
1372
+ const allManagedConversations = allStoredConversations
1373
+ .filter(isDiscoverableTmuxConversation);
1374
+ const storedConversations = allManagedConversations
1365
1375
  .filter((conversation) => includeAll || isActiveStatus(conversation.status))
1366
1376
  .filter((conversation) => matchesConfiguredWorkspace(options.workspace, conversation.workspace))
1367
1377
  .filter((conversation) => !agentFilter || executorForConversation(conversation).kind === agentFilter)
1368
1378
  .filter((conversation) => !statusFilter || conversation.status === statusFilter);
1369
- const conversations = storedConversations.map((conversation) => summarizeConversation(conversation));
1370
- const delegated = storedConversations.map((conversation) => delegatedListEntry(summarizeConversation(conversation), {
1371
- terminalBridge: terminalBridgeEnabled(conversation),
1372
- approvalState: managedListApprovalState(conversation),
1373
- conversation
1374
- }));
1375
1379
  const terminalScan = await buildTerminalListGroup({ options, agentFilter, statusFilter });
1376
- const managedTerminalKeys = new Set(allStoredConversations
1377
- .filter((conversation) => isActiveStatus(conversation.status))
1378
- .filter((conversation) => matchesConfiguredWorkspace(options.workspace, conversation.workspace))
1379
- .map((conversation) => terminalControlSelectorKey(terminalControlFromTakeover(isRecord(conversation.native_session_takeover)
1380
- ? conversation.native_session_takeover
1381
- : undefined)))
1382
- .filter((key) => key !== undefined));
1383
- const terminalControlled = terminalScan.terminalControlled.filter((entry) => {
1384
- if (!matchesConfiguredWorkspace(options.workspace, entry.workspace ?? entry.cwd)) {
1385
- return false;
1386
- }
1387
- const key = terminalControlSelectorKey(entry.terminal_control);
1388
- 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
1389
1389
  });
1390
1390
  printJson({
1391
1391
  store_dir: storeDir,
1392
- store: inspectStoreCompatibility(storeDir),
1392
+ store,
1393
1393
  reconciliation,
1394
1394
  action_contracts: listActionContracts(),
1395
- delegated,
1396
- terminal_controlled: terminalControlled,
1395
+ terminals: projection.terminals,
1396
+ unavailable_managed_turns: projection.unavailableManagedTurns,
1397
1397
  terminal_scan: {
1398
1398
  ...terminalScan.summary,
1399
- terminal_controlled_count: terminalControlled.length
1400
- },
1401
- tasks: conversations
1399
+ terminal_count: projection.terminals.length
1400
+ }
1402
1401
  });
1403
- runtimeLog("info", "tasks_listed", {
1402
+ runtimeLog("info", "terminals_listed", {
1404
1403
  store_dir: storeDir,
1405
- returned_count: conversations.length,
1406
- terminal_controlled_count: terminalControlled.length,
1404
+ terminal_count: projection.terminals.length,
1405
+ unavailable_managed_turn_count: projection.unavailableManagedTurns.length,
1407
1406
  terminal_scan_error: terminalScan.summary.error,
1408
1407
  include_all: includeAll,
1409
1408
  agent_filter: agentFilter,
@@ -1455,16 +1454,6 @@ async function buildTerminalListGroup({ options, agentFilter, statusFilter }) {
1455
1454
  if (options.managedOnly) {
1456
1455
  return empty;
1457
1456
  }
1458
- if (statusFilter && statusFilter !== "active") {
1459
- return {
1460
- ...empty,
1461
- summary: {
1462
- enabled: false,
1463
- agents: [],
1464
- skipped: `terminal discovery skipped for status filter ${statusFilter}`
1465
- }
1466
- };
1467
- }
1468
1457
  const registry = createRuntimeTerminalAgentRegistry(options);
1469
1458
  const adapters = agentFilter
1470
1459
  ? [registry.get(agentFilter)].filter((adapter) => adapter !== undefined)
@@ -1507,7 +1496,7 @@ async function buildTerminalListGroup({ options, agentFilter, statusFilter }) {
1507
1496
  enabled: true,
1508
1497
  agents: adapters.map((adapter) => adapter.agent),
1509
1498
  active_count: activeCount,
1510
- terminal_controlled_count: terminalControlled.length,
1499
+ terminal_count: terminalControlled.length,
1511
1500
  approval_scan: options.noApprovalScan ? "disabled" : "enabled",
1512
1501
  diagnostics: terminalDiagnostics,
1513
1502
  error: errors.length > 0 ? errors.join("; ") : undefined
@@ -1523,24 +1512,26 @@ async function terminalControlDiagnostics(provider) {
1523
1512
  paneCount: (await provider.listPanes()).length
1524
1513
  };
1525
1514
  }
1526
- function delegatedListEntry(task, { terminalBridge = false, approvalState, conversation } = {}) {
1515
+ function managedTurnListEntry(task, { terminalBridge = false, approvalState, conversation } = {}) {
1527
1516
  const entry = {
1528
1517
  ...task,
1529
1518
  id: task.conversation_id,
1530
1519
  short_ref: sessionShortRef(task.conversation_id),
1531
- source: "akk_delegate",
1520
+ source: "managed_turn",
1532
1521
  ...(approvalState ? { approval_state: approvalState } : {}),
1533
1522
  commands: {
1534
- send: canSendDelegated(task.status),
1523
+ send: canFollowUpManagedTurn(task.status),
1535
1524
  cancel: isWaitingForAgent(task.status),
1536
1525
  close: task.status !== "closed",
1537
1526
  status: true,
1538
1527
  approve: terminalBridge && isActiveStatus(task.status)
1539
1528
  }
1540
1529
  };
1530
+ const availableActions = availableListActions(entry, { conversation });
1531
+ const { commands: _commands, ...publicEntry } = entry;
1541
1532
  return {
1542
- ...entry,
1543
- available_actions: availableListActions(entry, { conversation })
1533
+ ...publicEntry,
1534
+ available_actions: availableActions
1544
1535
  };
1545
1536
  }
1546
1537
  async function terminalControlledListEntry(session, activeSessions, options, bridge = createTerminalAgentBridge(options)) {
@@ -1558,9 +1549,9 @@ async function terminalControlledListEntry(session, activeSessions, options, bri
1558
1549
  const entry = {
1559
1550
  id: bridge.terminalConversationId(session),
1560
1551
  short_ref: sessionShortRef(bridge.terminalConversationId(session)),
1561
- source: "terminal_control",
1552
+ source: "terminal",
1562
1553
  agent: session.agent,
1563
- status: "active",
1554
+ process_state: "active",
1564
1555
  pid: session.pid,
1565
1556
  child_pids: childPidsForRoot(session, activeSessions),
1566
1557
  command: session.command,
@@ -1594,9 +1585,438 @@ async function terminalControlledListEntry(session, activeSessions, options, bri
1594
1585
  close: orphanedDispatch !== undefined
1595
1586
  }
1596
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
+ }
1853
+ return {
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;
1597
1984
  return {
1598
- ...entry,
1599
- available_actions: availableListActions(entry)
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
+ : {})
1600
2020
  };
1601
2021
  }
1602
2022
  async function listStateForTerminal(agent, terminalControl, options, bridge = createTerminalAgentBridge(options), runtime) {
@@ -1663,7 +2083,7 @@ function childPidsForRoot(root, processes) {
1663
2083
  .filter((process) => process.agent === root.agent && process.ppid === root.pid)
1664
2084
  .map((process) => process.pid);
1665
2085
  }
1666
- function canSendDelegated(status) {
2086
+ function canFollowUpManagedTurn(status) {
1667
2087
  return !["done", "failed", "closed", "cancelled"].includes(status);
1668
2088
  }
1669
2089
  function managedListApprovalState(conversation) {
@@ -1701,28 +2121,31 @@ function managedListApprovalState(conversation) {
1701
2121
  }
1702
2122
  function listActionContracts() {
1703
2123
  return {
1704
- version: 2,
2124
+ version: 3,
1705
2125
  instructions: [
1706
- "Use only actions present in delegated[].available_actions or terminal_controlled[].available_actions.",
1707
- "Never use commands for routing or tool calls. It is a deprecated, non-authoritative compatibility field with mixed legacy semantics.",
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.",
1708
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.",
1709
2129
  "Authoritative full IDs are prefilled; short_ref is for display and human input.",
1710
2130
  "Availability is a snapshot. AKK revalidates process, tmux pane, workspace, activity, approval, and recovery state before side effects."
1711
2131
  ],
1712
2132
  field_semantics: {
2133
+ process_state: {
2134
+ terminals: "physical_terminal_process_liveness",
2135
+ authoritative_for_tool_calls: false
2136
+ },
1713
2137
  status: {
1714
- delegated: "task_lifecycle",
1715
- terminal_controlled: "process_liveness",
2138
+ managed_turns: "managed_turn_lifecycle",
1716
2139
  authoritative_for_tool_calls: false
1717
2140
  },
1718
2141
  activity_state: {
1719
- terminal_controlled: "screen_activity_classification",
2142
+ terminals: "terminal_screen_activity_classification",
1720
2143
  authoritative_for_tool_calls: false
1721
2144
  },
1722
- commands: {
1723
- meaning: "legacy_compatibility_flags_with_mixed_semantics",
1724
- deprecated: true,
1725
- 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"
1726
2149
  },
1727
2150
  available_actions: {
1728
2151
  meaning: "currently_safe_actions",
@@ -1742,7 +2165,19 @@ function listActionContracts() {
1742
2165
  "agentHardTimeoutMinutes"
1743
2166
  ],
1744
2167
  unsupported: ["timeoutSeconds"],
1745
- 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."
1746
2181
  },
1747
2182
  status: {
1748
2183
  tool: "agent_knock_knock_status",
@@ -1797,8 +2232,8 @@ function availableListActions(entry, { conversation } = {}) {
1797
2232
  arguments: { conversation_id: id }
1798
2233
  }
1799
2234
  };
1800
- const terminalControlled = entry.source === "terminal_control";
1801
- const managed = entry.source === "akk_delegate";
2235
+ const terminalControlled = entry.source === "terminal";
2236
+ const managed = entry.source === "managed_turn";
1802
2237
  const approvalState = isRecord(entry.approval_state)
1803
2238
  ? entry.approval_state
1804
2239
  : {};
@@ -1817,7 +2252,7 @@ function availableListActions(entry, { conversation } = {}) {
1817
2252
  (terminalControlled &&
1818
2253
  entry.activity_state === "idle" &&
1819
2254
  approvalState.blocked !== true))) {
1820
- actions.send = {
2255
+ actions[managed ? "follow_up" : "send"] = {
1821
2256
  tool: "agent_knock_knock_send",
1822
2257
  arguments: { selector: id },
1823
2258
  missing_required: ["request"]
@@ -1923,52 +2358,112 @@ function isSessionSelectorSyntax(value) {
1923
2358
  }
1924
2359
  async function sessionSelectorCandidates(commandName, options) {
1925
2360
  const storeDir = storeDirFromOptions(options);
2361
+ const mutationsAllowed = inspectStoreCompatibility(storeDir).writable === true;
1926
2362
  const storedConversations = listConversations(storeDir);
1927
2363
  const workspaceConversations = storedConversations
1928
2364
  .filter((conversation) => matchesConfiguredWorkspace(options.workspace, conversation.workspace));
1929
2365
  const discoverableWorkspaceConversations = workspaceConversations
1930
2366
  .filter(isDiscoverableTmuxConversation);
1931
- const managed = discoverableWorkspaceConversations.map((conversation) => delegatedListEntry(summarizeConversation(conversation), {
2367
+ const managed = discoverableWorkspaceConversations.map((conversation) => managedTurnListEntry(summarizeConversation(conversation), {
1932
2368
  terminalBridge: terminalBridgeEnabled(conversation),
1933
2369
  approvalState: managedListApprovalState(conversation),
1934
2370
  conversation
1935
2371
  }));
1936
- const managedTerminalKeys = new Set(workspaceConversations
1937
- .filter((conversation) => isActiveStatus(conversation.status))
1938
- .map((conversation) => terminalControlSelectorKey(terminalControlFromTakeover(isRecord(conversation.native_session_takeover)
1939
- ? conversation.native_session_takeover
1940
- : undefined)))
1941
- .filter((key) => key !== undefined));
1942
2372
  const terminalScan = await buildTerminalListGroup({
1943
2373
  options: {
1944
2374
  ...options,
1945
- noApprovalScan: commandName === "approve"
2375
+ noApprovalScan: ["send", "approve", "cancel"].includes(commandName)
1946
2376
  ? options.noApprovalScan
1947
2377
  : true
1948
2378
  },
1949
2379
  agentFilter: undefined,
1950
2380
  statusFilter: undefined
1951
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
+ });
1952
2391
  const observedAtMs = Date.now();
1953
2392
  return [
1954
- ...managed,
1955
- ...terminalScan.terminalControlled.filter((entry) => {
1956
- if (!matchesConfiguredWorkspace(options.workspace, entry.workspace ?? entry.cwd)) {
1957
- return false;
1958
- }
1959
- const key = terminalControlSelectorKey(entry.terminal_control);
1960
- return key === undefined || !managedTerminalKeys.has(key);
1961
- })
1962
- ].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 {
1963
2409
  id: String(entry.id),
2410
+ ...(targetId && targetId !== entry.id ? { targetId } : {}),
1964
2411
  agent: resolveExecutor({ kind: entry.agent }).kind,
1965
- actionable: sessionEntrySupportsCommand(entry, commandName),
2412
+ actionable: action !== undefined,
2413
+ defaultActionable,
1966
2414
  ...sessionEntryRecency(entry, observedAtMs),
1967
2415
  source: stringValue(entry.source),
1968
- status: stringValue(entry.status),
2416
+ status: stringValue(entry.status ?? entry.process_state),
1969
2417
  workspace: stringValue(entry.workspace ?? entry.cwd),
1970
2418
  label: stringValue(entry.request ?? entry.command)
1971
- }));
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);
1972
2467
  }
1973
2468
  function terminalControlSelectorKey(value) {
1974
2469
  if (!isRecord(value)) {
@@ -1985,22 +2480,6 @@ function terminalControlSelectorKey(value) {
1985
2480
  socket_path: stringValue(value.socketPath) ?? null
1986
2481
  });
1987
2482
  }
1988
- function sessionEntrySupportsCommand(entry, commandName) {
1989
- const commands = isRecord(entry.commands) ? entry.commands : {};
1990
- if (typeof commands[commandName] === "boolean") {
1991
- return commands[commandName] === true;
1992
- }
1993
- if (entry.source !== "akk_delegate") {
1994
- return false;
1995
- }
1996
- if (commandName === "renew") {
1997
- return entry.status === "stalled";
1998
- }
1999
- if (commandName === "retry-callback") {
2000
- return ["callback_pending", "callback_failed"].includes(entry.status);
2001
- }
2002
- return false;
2003
- }
2004
2483
  function sessionEntryRecency(entry, observedAtMs) {
2005
2484
  const timestamp = Date.parse(String(entry.updated_at ?? entry.created_at ?? ""));
2006
2485
  if (Number.isFinite(timestamp)) {
@@ -8917,7 +9396,7 @@ function usage() {
8917
9396
  agent-knock-knock --help
8918
9397
  agent-knock-knock --version
8919
9398
  agent-knock-knock delegate --request <text> [--agent ${agentList}] [--workspace <path>] [--store-dir <dir>]
8920
- agent-knock-knock list [--store-dir <dir>] [--agent ${agentList}] [--status <status>] [--all] [--managed-only] [--reconcile] [--no-approval-scan] [--terminal-debug]
9399
+ agent-knock-knock list [--store-dir <dir>] [--agent ${agentList}] [--status <status>] [--all] [--reconcile] [--no-approval-scan] [--terminal-debug]
8921
9400
  agent-knock-knock status [--conversation <id|selector>] [--store-dir <dir>] [--reconcile] [--trace]
8922
9401
  agent-knock-knock send [--conversation <id|selector>] --message <text> [--type answer|task|control] [--agent-timeout-minutes <minutes>] [--agent-hard-timeout-minutes <minutes>]
8923
9402
  agent-knock-knock approve [--conversation <id|selector>] --expected-approval-fingerprint <fingerprint>