@scotthuang/agent-knock-knock 0.5.2 → 0.6.1

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
@@ -11,7 +11,7 @@ import { captureClaudeTranscriptAnchor, defaultClaudeHome, detectClaudeTranscrip
11
11
  import { CodexLocalSessionProvider } from "./codex-local-session-provider.js";
12
12
  import { CodexStoreAdapter } from "./codex-store-adapter.js";
13
13
  import { applyMessageToConversation, budgetAction, createConversation, createMessage, executorForConversation, extractStructuredMessage, parseMessageJson, resolveExecutor } from "./protocol.js";
14
- import { EXECUTOR_KINDS, executorDefinitionForKind } from "./executors.js";
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
17
  import { appendEvent, defaultStoreDir, ensureDir, listConversations, logPathForStatePath, loadConversationById, loadState, messageEvent, pathsForConversation, pathsForConversationDir, saveState, statePathForConversationId } from "./store.js";
@@ -191,19 +191,17 @@ async function runCommand(commandName, options) {
191
191
  function runInstallOpenClaw(options) {
192
192
  const root = packageRootDir();
193
193
  const skillOnly = options.skillOnly === true;
194
- const workspace = options.workspace === undefined
195
- ? undefined
196
- : canonicalWorkspace(options.workspace);
194
+ if (options.workspace !== undefined) {
195
+ throw new Error("--workspace was removed from install-openclaw; AKK now discovers verified tmux panes across workspaces. Configure autoApprove.rules[].workspaces only for trusted automatic approvals.");
196
+ }
197
197
  if (options.defaultAgent !== undefined) {
198
198
  throw new Error("--default-agent was removed; AKK now selects the only eligible idle tmux pane");
199
199
  }
200
200
  if (options.mode !== undefined) {
201
201
  throw new Error("--mode was removed; Agent Knock Knock now uses tmux only");
202
202
  }
203
- if (skillOnly &&
204
- (workspace !== undefined ||
205
- options.verify === true)) {
206
- throw new Error("--skill-only cannot be combined with --workspace, --mode, or --verify");
203
+ if (skillOnly && options.verify === true) {
204
+ throw new Error("--skill-only cannot be combined with --verify");
207
205
  }
208
206
  const needsOpenClaw = !skillOnly || options.noRestart !== true || options.verify === true;
209
207
  const openclawBin = needsOpenClaw
@@ -224,12 +222,6 @@ function runInstallOpenClaw(options) {
224
222
  path: "plugins.entries.agent-knock-knock.enabled",
225
223
  value: true
226
224
  },
227
- ...(workspace === undefined
228
- ? []
229
- : [{
230
- path: "plugins.entries.agent-knock-knock.config.workspace",
231
- value: workspace
232
- }])
233
225
  ];
234
226
  runCheckedCommand(openclawBin, ["config", "set", "--batch-json", JSON.stringify(configOperations)], { label: "openclaw config set" });
235
227
  steps.push({
@@ -256,8 +248,7 @@ function runInstallOpenClaw(options) {
256
248
  const verification = options.verify === true
257
249
  ? buildDoctorReport({
258
250
  ...options,
259
- openclawBin,
260
- ...(workspace ? { workspace } : {})
251
+ openclawBin
261
252
  })
262
253
  : undefined;
263
254
  const ready = verification
@@ -273,7 +264,6 @@ function runInstallOpenClaw(options) {
273
264
  pending_restart: pendingRestart,
274
265
  mode: skillOnly ? "skill_only" : "full",
275
266
  execution_mode: "tmux",
276
- workspace: workspace ?? null,
277
267
  package_root: root,
278
268
  openclaw_bin: openclawBin ?? null,
279
269
  steps,
@@ -322,10 +312,10 @@ function assertConfiguredWorkspace(configuredWorkspace, candidateWorkspace, subj
322
312
  candidate = canonicalWorkspace(candidateWorkspace);
323
313
  }
324
314
  catch {
325
- throw new Error(`refusing ${subject}; its workspace cannot be verified against configured workspace ${configured}`);
315
+ throw new Error(`refusing ${subject}; its working directory cannot be verified against expected workspace ${configured}`);
326
316
  }
327
317
  if (candidate !== configured) {
328
- throw new Error(`refusing ${subject}; workspace ${candidate} does not match configured workspace ${configured}`);
318
+ throw new Error(`refusing ${subject}; workspace ${candidate} does not match expected workspace ${configured}`);
329
319
  }
330
320
  }
331
321
  function installNextActions({ pendingRestart, verification }) {
@@ -397,6 +387,9 @@ function buildDoctorReport(options) {
397
387
  if (options.mode !== undefined) {
398
388
  throw new Error("--mode was removed; Agent Knock Knock now checks tmux only");
399
389
  }
390
+ if (options.workspace !== undefined) {
391
+ throw new Error("--workspace was removed from doctor; AKK now discovers verified tmux panes across workspaces");
392
+ }
400
393
  const timeoutMs = options.timeoutMs === undefined
401
394
  ? undefined
402
395
  : positiveMilliseconds(options.timeoutMs, "--timeout-ms");
@@ -439,7 +432,6 @@ function buildDoctorReport(options) {
439
432
  const filesOk = packageFiles.every((check) => check.exists);
440
433
  const openclaw = runOpenClawChainDiagnostics({
441
434
  openclawBin,
442
- ...(options.workspace ? { workspace: String(options.workspace) } : {}),
443
435
  ...(timeoutMs === undefined ? {} : { timeoutMs })
444
436
  });
445
437
  const ok = capabilities.readiness === "ready" &&
@@ -653,8 +645,12 @@ function createTerminalAgentBridge(options, terminalProvider = createTerminalCon
653
645
  terminalProvider,
654
646
  async verifyIdentity({ agent, pid, terminalControl }) {
655
647
  const adapter = registry.require(agent);
648
+ const expectedWorkspace = options.workspace ?? terminalControl.currentPath;
649
+ if (!expectedWorkspace) {
650
+ throw new Error(`refusing terminal access to ${terminalControl.target}; its workspace is unavailable`);
651
+ }
656
652
  const snapshots = await processSource.listProcessSnapshots((candidate) => candidate.pid === pid, {
657
- includeCwd: options.workspace !== undefined,
653
+ includeCwd: true,
658
654
  includeAncestors: true
659
655
  });
660
656
  const snapshot = snapshots.find((candidate) => candidate.pid === pid);
@@ -668,8 +664,8 @@ function createTerminalAgentBridge(options, terminalProvider = createTerminalCon
668
664
  if (!pane || !terminalPaneContainsProcess(snapshot, pane, snapshots)) {
669
665
  throw new Error(`terminal conversation agent ${agent} with pid ${pid} no longer belongs to pane ${terminalControl.target}`);
670
666
  }
671
- assertConfiguredWorkspace(options.workspace, snapshot.cwd, `terminal access to ${terminalControl.target} by agent process ${pid}`);
672
- assertConfiguredWorkspace(options.workspace, pane.currentPath, `terminal access to ${terminalControl.target} by tmux pane`);
667
+ assertConfiguredWorkspace(expectedWorkspace, snapshot.cwd, `terminal access to ${terminalControl.target} by agent process ${pid}`);
668
+ assertConfiguredWorkspace(expectedWorkspace, pane.currentPath, `terminal access to ${terminalControl.target} by tmux pane`);
673
669
  return {
674
670
  terminalControl: {
675
671
  ...terminalControl,
@@ -883,7 +879,9 @@ function isTerminalControlCapability(value) {
883
879
  }
884
880
  async function runDelegate(options) {
885
881
  const request = required(options.request, "--request is required");
886
- const workspace = canonicalWorkspace(options.workspace ?? process.cwd());
882
+ const workspace = options.workspace === undefined
883
+ ? undefined
884
+ : canonicalWorkspace(options.workspace);
887
885
  const requestedAgent = options.agent === undefined
888
886
  ? undefined
889
887
  : resolveExecutor({ kind: options.agent }).kind;
@@ -899,40 +897,54 @@ async function runDelegate(options) {
899
897
  if (scan.summary.error) {
900
898
  throw new Error(`tmux discovery failed: ${scan.summary.error}`);
901
899
  }
902
- const sameWorkspace = scan.terminalControlled.filter((candidate) => {
903
- try {
904
- return canonicalWorkspace(candidate.workspace) === workspace;
905
- }
906
- catch {
907
- return false;
908
- }
909
- });
910
- const eligible = sameWorkspace.filter((candidate) => candidate.activity_state === "idle");
900
+ const scopedCandidates = workspace === undefined
901
+ ? scan.terminalControlled
902
+ : scan.terminalControlled.filter((candidate) => {
903
+ try {
904
+ return canonicalWorkspace(candidate.workspace) === workspace;
905
+ }
906
+ catch {
907
+ return false;
908
+ }
909
+ });
910
+ const eligible = scopedCandidates.filter((candidate) => candidate.activity_state === "idle");
911
911
  if (eligible.length === 0) {
912
- const observed = sameWorkspace.length > 0
913
- ? ` Found ${sameWorkspace.length} matching pane(s), but none is idle.`
912
+ const observed = scopedCandidates.length > 0
913
+ ? ` Found ${scopedCandidates.length} matching pane(s), but none is idle.`
914
914
  : "";
915
915
  const requestedExecutor = requestedAgent
916
916
  ? executorDefinitionForKind(requestedAgent)
917
917
  : undefined;
918
- throw new Error(`No idle ${requestedExecutor?.displayName ?? "Codex or Claude Code"} pane is available in ${workspace}.${observed} ` +
919
- `Start ${requestedAgent ?? "codex or claude"} inside tmux in that workspace, wait until it is idle, then retry.`);
918
+ const workspaceDetail = workspace
919
+ ? ` in ${workspace}`
920
+ : "";
921
+ throw new Error(`No idle ${requestedExecutor?.displayName ?? "Codex or Claude Code"} pane is available${workspaceDetail}.${observed} ` +
922
+ `Start ${requestedAgent ?? "codex or claude"} inside tmux${workspaceDetail}, wait until it is idle, then retry.`);
920
923
  }
921
924
  if (eligible.length > 1) {
922
925
  const candidates = eligible
923
- .map((candidate) => `${candidate.short_ref} (${candidate.agent}, ${candidate.terminal_control?.target ?? candidate.id})`)
926
+ .map((candidate) => {
927
+ const identity = `${candidate.agent}, ${candidate.terminal_control?.target ?? candidate.id}`;
928
+ return workspace
929
+ ? `${candidate.short_ref} (${identity})`
930
+ : `${candidate.short_ref} (${identity}, ${candidate.workspace ?? "workspace unknown"})`;
931
+ })
924
932
  .join(", ");
925
933
  const scope = requestedAgent
926
934
  ? executorDefinitionForKind(requestedAgent).displayName
927
935
  : "coding-agent";
928
- throw new Error(`Multiple idle ${scope} panes match ${workspace}: ${candidates}. ` +
936
+ const ambiguity = workspace
937
+ ? `match ${workspace}`
938
+ : "are available across workspaces";
939
+ throw new Error(`Multiple idle ${scope} panes ${ambiguity}: ${candidates}. ` +
929
940
  "Use /akk codex: <task>, /akk claude: <task>, or /akk @short-ref: <message> to choose one explicitly.");
930
941
  }
942
+ const selectedWorkspace = canonicalWorkspace(eligible[0].workspace);
931
943
  await runSend({
932
944
  ...options,
933
945
  conversation: eligible[0].id,
934
946
  message: request,
935
- workspace,
947
+ workspace: selectedWorkspace,
936
948
  background: true
937
949
  });
938
950
  }
@@ -1319,16 +1331,23 @@ async function runList(options) {
1319
1331
  const includeAll = Boolean(options.all);
1320
1332
  const agentFilter = options.agent ? resolveExecutor({ kind: options.agent }).kind : undefined;
1321
1333
  const statusFilter = options.status;
1322
- const storedConversations = listConversations(storeDir)
1334
+ const allStoredConversations = listConversations(storeDir);
1335
+ const storedConversations = allStoredConversations
1336
+ .filter(isDiscoverableTmuxConversation)
1323
1337
  .filter((conversation) => includeAll || isActiveStatus(conversation.status))
1324
1338
  .filter((conversation) => matchesConfiguredWorkspace(options.workspace, conversation.workspace))
1325
1339
  .filter((conversation) => !agentFilter || executorForConversation(conversation).kind === agentFilter)
1326
1340
  .filter((conversation) => !statusFilter || conversation.status === statusFilter);
1327
1341
  const conversations = storedConversations.map((conversation) => summarizeConversation(conversation));
1328
- const delegated = storedConversations.map((conversation) => delegatedListEntry(summarizeConversation(conversation), { terminalBridge: terminalBridgeEnabled(conversation) }));
1342
+ const delegated = storedConversations.map((conversation) => delegatedListEntry(summarizeConversation(conversation), {
1343
+ terminalBridge: terminalBridgeEnabled(conversation),
1344
+ approvalState: managedListApprovalState(conversation),
1345
+ conversation
1346
+ }));
1329
1347
  const terminalScan = await buildTerminalListGroup({ options, agentFilter, statusFilter });
1330
- const managedTerminalKeys = new Set(storedConversations
1348
+ const managedTerminalKeys = new Set(allStoredConversations
1331
1349
  .filter((conversation) => isActiveStatus(conversation.status))
1350
+ .filter((conversation) => matchesConfiguredWorkspace(options.workspace, conversation.workspace))
1332
1351
  .map((conversation) => terminalControlSelectorKey(terminalControlFromTakeover(isRecord(conversation.native_session_takeover)
1333
1352
  ? conversation.native_session_takeover
1334
1353
  : undefined)))
@@ -1343,6 +1362,7 @@ async function runList(options) {
1343
1362
  printJson({
1344
1363
  store_dir: storeDir,
1345
1364
  cleanup,
1365
+ action_contracts: listActionContracts(),
1346
1366
  delegated,
1347
1367
  terminal_controlled: terminalControlled,
1348
1368
  terminal_scan: {
@@ -1442,12 +1462,13 @@ async function terminalControlDiagnostics(provider) {
1442
1462
  paneCount: (await provider.listPanes()).length
1443
1463
  };
1444
1464
  }
1445
- function delegatedListEntry(task, { terminalBridge = false } = {}) {
1446
- return {
1465
+ function delegatedListEntry(task, { terminalBridge = false, approvalState, conversation } = {}) {
1466
+ const entry = {
1447
1467
  ...task,
1448
1468
  id: task.conversation_id,
1449
1469
  short_ref: sessionShortRef(task.conversation_id),
1450
1470
  source: "akk_delegate",
1471
+ ...(approvalState ? { approval_state: approvalState } : {}),
1451
1472
  commands: {
1452
1473
  send: canSendDelegated(task.status),
1453
1474
  cancel: isWaitingForAgent(task.status),
@@ -1456,6 +1477,10 @@ function delegatedListEntry(task, { terminalBridge = false } = {}) {
1456
1477
  approve: terminalBridge && isActiveStatus(task.status)
1457
1478
  }
1458
1479
  };
1480
+ return {
1481
+ ...entry,
1482
+ available_actions: availableListActions(entry, { conversation })
1483
+ };
1459
1484
  }
1460
1485
  async function terminalControlledListEntry(session, activeSessions, options, bridge = createTerminalAgentBridge(options)) {
1461
1486
  const terminalControl = session.terminalControl;
@@ -1469,7 +1494,7 @@ async function terminalControlledListEntry(session, activeSessions, options, bri
1469
1494
  terminalTarget: terminalControl.target
1470
1495
  });
1471
1496
  const orphanedDispatch = orphanedTerminalDispatchForRecovery(terminalControl);
1472
- return {
1497
+ const entry = {
1473
1498
  id: bridge.terminalConversationId(session),
1474
1499
  short_ref: sessionShortRef(bridge.terminalConversationId(session)),
1475
1500
  source: "terminal_control",
@@ -1508,6 +1533,10 @@ async function terminalControlledListEntry(session, activeSessions, options, bri
1508
1533
  close: orphanedDispatch !== undefined
1509
1534
  }
1510
1535
  };
1536
+ return {
1537
+ ...entry,
1538
+ available_actions: availableListActions(entry)
1539
+ };
1511
1540
  }
1512
1541
  async function listStateForTerminal(agent, terminalControl, options, bridge = createTerminalAgentBridge(options), runtime) {
1513
1542
  if (options.noApprovalScan) {
@@ -1574,7 +1603,218 @@ function childPidsForRoot(root, processes) {
1574
1603
  .map((process) => process.pid);
1575
1604
  }
1576
1605
  function canSendDelegated(status) {
1577
- return !["failed", "closed", "cancelled"].includes(status);
1606
+ return !["done", "failed", "closed", "cancelled"].includes(status);
1607
+ }
1608
+ function managedListApprovalState(conversation) {
1609
+ if (!terminalBridgeEnabled(conversation) ||
1610
+ !["waiting_for_agent", "waiting_for_openclaw"].includes(String(conversation.status))) {
1611
+ return undefined;
1612
+ }
1613
+ const nativeTakeover = isRecord(conversation.native_session_takeover)
1614
+ ? conversation.native_session_takeover
1615
+ : undefined;
1616
+ if (!terminalControlFromTakeover(nativeTakeover) ||
1617
+ !stringValue(nativeTakeover?.terminal_bridge_message_id)) {
1618
+ return undefined;
1619
+ }
1620
+ const approval = isRecord(nativeTakeover?.terminal_bridge_approval)
1621
+ ? nativeTakeover.terminal_bridge_approval
1622
+ : undefined;
1623
+ const approvalState = isRecord(approval?.approval_state)
1624
+ ? approval.approval_state
1625
+ : undefined;
1626
+ const fingerprint = stringValue(approval?.fingerprint);
1627
+ const notifiedAt = stringValue(approval?.notified_at);
1628
+ const notifiedAtMs = validTimestampMs(notifiedAt);
1629
+ if (!approvalState ||
1630
+ !fingerprint ||
1631
+ notifiedAtMs === undefined ||
1632
+ Date.now() - notifiedAtMs > CLAUDE_SCREEN_APPROVAL_TTL_MS) {
1633
+ return undefined;
1634
+ }
1635
+ return {
1636
+ ...approvalState,
1637
+ fingerprint,
1638
+ notified_at: notifiedAt
1639
+ };
1640
+ }
1641
+ function listActionContracts() {
1642
+ return {
1643
+ version: 1,
1644
+ instructions: [
1645
+ "Use only actions present in delegated[].available_actions or terminal_controlled[].available_actions.",
1646
+ "Start with the action's prefilled arguments, supply every missing_required field, and consult the top-level action's optional fields only when needed.",
1647
+ "Authoritative full IDs are prefilled; short_ref is for display and human input.",
1648
+ "Availability is a snapshot. AKK revalidates process, tmux pane, workspace, activity, approval, and recovery state before side effects."
1649
+ ],
1650
+ actions: {
1651
+ send: {
1652
+ tool: "agent_knock_knock_send",
1653
+ target_argument: "selector",
1654
+ required: ["request"],
1655
+ optional: [
1656
+ "selector",
1657
+ "type",
1658
+ "idleTimeoutMinutes",
1659
+ "agentTimeoutMinutes",
1660
+ "agentHardTimeoutMinutes"
1661
+ ],
1662
+ unsupported: ["timeoutSeconds"],
1663
+ ordinary_use: "Add request only. Omit timeout fields unless the user explicitly asks to change monitoring limits."
1664
+ },
1665
+ status: {
1666
+ tool: "agent_knock_knock_status",
1667
+ target_argument: "conversation_id",
1668
+ required: ["conversation_id"],
1669
+ optional: ["idleTimeoutMinutes", "trace"]
1670
+ },
1671
+ approve: {
1672
+ tool: "agent_knock_knock_approve",
1673
+ target_argument: "conversation_id",
1674
+ required: ["conversation_id", "expected_approval_fingerprint"],
1675
+ requires_explicit_user_confirmation: true,
1676
+ requires_fresh_status: true
1677
+ },
1678
+ cancel: {
1679
+ tool: "agent_knock_knock_cancel",
1680
+ target_argument: "conversation_id",
1681
+ required: ["conversation_id"],
1682
+ optional: ["idleTimeoutMinutes"],
1683
+ requires_user_intent: true
1684
+ },
1685
+ renew: {
1686
+ tool: "agent_knock_knock_renew",
1687
+ target_argument: "conversation_id",
1688
+ required: ["conversation_id"],
1689
+ optional: ["minutes"]
1690
+ },
1691
+ retry_callback: {
1692
+ tool: "agent_knock_knock_retry_callback",
1693
+ target_argument: "conversation_id",
1694
+ required: ["conversation_id"]
1695
+ },
1696
+ close: {
1697
+ tool: "agent_knock_knock_close",
1698
+ target_argument: "conversation_id",
1699
+ required: ["conversation_id"],
1700
+ optional: ["reason", "expected_message_id"],
1701
+ requires_explicit_user_confirmation: true
1702
+ }
1703
+ }
1704
+ };
1705
+ }
1706
+ function availableListActions(entry, { conversation } = {}) {
1707
+ const id = stringValue(entry.id ?? entry.conversation_id);
1708
+ if (!id) {
1709
+ return {};
1710
+ }
1711
+ const commands = isRecord(entry.commands) ? entry.commands : {};
1712
+ const actions = {
1713
+ status: {
1714
+ tool: "agent_knock_knock_status",
1715
+ arguments: { conversation_id: id }
1716
+ }
1717
+ };
1718
+ const terminalControlled = entry.source === "terminal_control";
1719
+ const managed = entry.source === "akk_delegate";
1720
+ const approvalState = isRecord(entry.approval_state)
1721
+ ? entry.approval_state
1722
+ : {};
1723
+ const nativeTakeover = isRecord(conversation?.native_session_takeover)
1724
+ ? conversation.native_session_takeover
1725
+ : undefined;
1726
+ const managedApprovalPending = isRecord(nativeTakeover?.terminal_bridge_approval);
1727
+ const terminalBridgeReady = managed &&
1728
+ terminalBridgeEnabled(conversation) &&
1729
+ terminalControlFromTakeover(nativeTakeover) !== undefined;
1730
+ if (commands.send === true &&
1731
+ ((terminalBridgeReady &&
1732
+ ["waiting_for_openclaw", "idle"].includes(String(entry.status)) &&
1733
+ !managedApprovalPending &&
1734
+ approvalState.blocked !== true) ||
1735
+ (terminalControlled &&
1736
+ entry.activity_state === "idle" &&
1737
+ approvalState.blocked !== true))) {
1738
+ actions.send = {
1739
+ tool: "agent_knock_knock_send",
1740
+ arguments: { selector: id },
1741
+ missing_required: ["request"]
1742
+ };
1743
+ }
1744
+ const approvalFingerprint = stringValue(approvalState.fingerprint);
1745
+ const managedApprovalEligible = terminalBridgeReady &&
1746
+ entry.status === "waiting_for_openclaw" &&
1747
+ (entry.agent !== "claude" ||
1748
+ approvalState.decision_mode === "keys");
1749
+ if (commands.approve === true &&
1750
+ approvalState.approvable === true &&
1751
+ approvalFingerprint &&
1752
+ ((terminalControlled &&
1753
+ entry.agent === "codex") ||
1754
+ managedApprovalEligible)) {
1755
+ actions.approve = {
1756
+ tool: "agent_knock_knock_approve",
1757
+ arguments: { conversation_id: id },
1758
+ missing_required: ["expected_approval_fingerprint"],
1759
+ before_call: {
1760
+ tool: "agent_knock_knock_status",
1761
+ arguments: { conversation_id: id },
1762
+ use: "After explicit user confirmation, copy the latest terminal_status.approval_state.fingerprint into expected_approval_fingerprint."
1763
+ },
1764
+ requires_explicit_user_confirmation: true,
1765
+ requires_fresh_status: true
1766
+ };
1767
+ }
1768
+ const rawCancellable = terminalControlled &&
1769
+ commands.cancel === true &&
1770
+ (entry.activity_state === "working" ||
1771
+ (approvalState.blocked === true &&
1772
+ approvalState.approvable === true));
1773
+ const managedCancellable = terminalBridgeReady &&
1774
+ ["waiting_for_agent", "waiting_for_openclaw"].includes(String(entry.status)) &&
1775
+ !(managedApprovalPending &&
1776
+ approvalState.approvable !== true);
1777
+ if (rawCancellable || managedCancellable) {
1778
+ actions.cancel = {
1779
+ tool: "agent_knock_knock_cancel",
1780
+ arguments: { conversation_id: id },
1781
+ requires_user_intent: true
1782
+ };
1783
+ }
1784
+ if (terminalBridgeReady && entry.status === "stalled") {
1785
+ actions.renew = {
1786
+ tool: "agent_knock_knock_renew",
1787
+ arguments: { conversation_id: id }
1788
+ };
1789
+ }
1790
+ const callbackDelivery = isRecord(conversation?.callback_delivery)
1791
+ ? conversation.callback_delivery
1792
+ : undefined;
1793
+ if (managed &&
1794
+ isRecord(conversation) &&
1795
+ isRetryableCallbackDelivery(conversation, callbackDelivery)) {
1796
+ actions.retry_callback = {
1797
+ tool: "agent_knock_knock_retry_callback",
1798
+ arguments: { conversation_id: id }
1799
+ };
1800
+ }
1801
+ if (commands.close === true) {
1802
+ const orphanedDispatch = isRecord(entry.orphaned_terminal_dispatch)
1803
+ ? entry.orphaned_terminal_dispatch
1804
+ : undefined;
1805
+ const expectedMessageId = stringValue(orphanedDispatch?.message_id);
1806
+ actions.close = {
1807
+ tool: "agent_knock_knock_close",
1808
+ arguments: {
1809
+ conversation_id: id,
1810
+ ...(expectedMessageId
1811
+ ? { expected_message_id: expectedMessageId }
1812
+ : {})
1813
+ },
1814
+ requires_explicit_user_confirmation: true
1815
+ };
1816
+ }
1817
+ return actions;
1578
1818
  }
1579
1819
  async function resolveConversationSelectorOption(commandName, options) {
1580
1820
  if (!SESSION_SELECTOR_COMMANDS.has(String(commandName ?? "")) ||
@@ -1605,7 +1845,13 @@ async function sessionSelectorCandidates(commandName, options) {
1605
1845
  const storedConversations = listConversations(storeDir);
1606
1846
  const workspaceConversations = storedConversations
1607
1847
  .filter((conversation) => matchesConfiguredWorkspace(options.workspace, conversation.workspace));
1608
- const managed = workspaceConversations.map((conversation) => delegatedListEntry(summarizeConversation(conversation), { terminalBridge: terminalBridgeEnabled(conversation) }));
1848
+ const discoverableWorkspaceConversations = workspaceConversations
1849
+ .filter(isDiscoverableTmuxConversation);
1850
+ const managed = discoverableWorkspaceConversations.map((conversation) => delegatedListEntry(summarizeConversation(conversation), {
1851
+ terminalBridge: terminalBridgeEnabled(conversation),
1852
+ approvalState: managedListApprovalState(conversation),
1853
+ conversation
1854
+ }));
1609
1855
  const managedTerminalKeys = new Set(workspaceConversations
1610
1856
  .filter((conversation) => isActiveStatus(conversation.status))
1611
1857
  .map((conversation) => terminalControlSelectorKey(terminalControlFromTakeover(isRecord(conversation.native_session_takeover)
@@ -7358,6 +7604,34 @@ function summarizeConversation(conversation) {
7358
7604
  event_log_path: conversation.event_log_path
7359
7605
  };
7360
7606
  }
7607
+ function isDiscoverableTmuxConversation(conversation) {
7608
+ if (!isRecord(conversation)) {
7609
+ return false;
7610
+ }
7611
+ if (!isRecord(conversation.executor)) {
7612
+ return false;
7613
+ }
7614
+ const kind = stringValue(conversation.executor.kind)?.toLowerCase();
7615
+ return (kind !== undefined &&
7616
+ isExecutorKind(kind) &&
7617
+ conversation.executor.transport === "tmux");
7618
+ }
7619
+ function persistedExecutorLogFields(conversation) {
7620
+ if (isDiscoverableTmuxConversation(conversation)) {
7621
+ const executor = executorForConversation(conversation);
7622
+ return {
7623
+ agent: executor.kind,
7624
+ executor_session: executor.session
7625
+ };
7626
+ }
7627
+ const rawExecutor = isRecord(conversation?.executor)
7628
+ ? conversation.executor
7629
+ : {};
7630
+ return {
7631
+ agent: stringValue(rawExecutor.kind) ?? "unsupported",
7632
+ executor_session: stringValue(rawExecutor.session)
7633
+ };
7634
+ }
7361
7635
  function summarizeEvent(event) {
7362
7636
  return {
7363
7637
  ts: event.ts,
@@ -7974,10 +8248,10 @@ function cleanupIdleConversations(storeDir, options = {}, now = new Date()) {
7974
8248
  idle_timeout_minutes: timeoutMinutes,
7975
8249
  terminal_bridge: terminalBridge
7976
8250
  });
8251
+ const executorLogFields = persistedExecutorLogFields(conversation);
7977
8252
  runtimeLog("info", "idle_conversation_closed", {
7978
8253
  conversation_id: conversation.conversation_id,
7979
- agent: executorForConversation(conversation).kind,
7980
- executor_session: executorForConversation(conversation).session,
8254
+ ...executorLogFields,
7981
8255
  state_path: statePath,
7982
8256
  event_log_path: logPath,
7983
8257
  idle_since: conversation.idle_since,
@@ -8464,8 +8738,8 @@ function usage() {
8464
8738
  agent-knock-knock send [--conversation <id|selector>] --message <text> [--type answer|task|control] [--agent-timeout-minutes <minutes>] [--agent-hard-timeout-minutes <minutes>]
8465
8739
  agent-knock-knock approve [--conversation <id|selector>] --expected-approval-fingerprint <fingerprint>
8466
8740
  agent-knock-knock cancel [--conversation <id|selector>]
8467
- agent-knock-knock install-openclaw [--workspace <path>] [--verify] [--openclaw-bin <path>] [--skill-path <path>] [--skill-only] [--no-restart]
8468
- agent-knock-knock doctor [--workspace <path>] [--openclaw-bin <path>]
8741
+ agent-knock-knock install-openclaw [--verify] [--openclaw-bin <path>] [--skill-path <path>] [--skill-only] [--no-restart]
8742
+ agent-knock-knock doctor [--openclaw-bin <path>]
8469
8743
  agent-knock-knock callback --state <file> --message-json <json> [--record-only]
8470
8744
  agent-knock-knock transcript --log <file> [--include-raw]
8471
8745
  agent-knock-knock transcript --conversation <dir> [--include-raw]