@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/CHANGELOG.md +28 -0
- package/README.md +21 -18
- package/dist/src/approval-policy.js +6 -1
- package/dist/src/approval-policy.js.map +1 -1
- package/dist/src/cli.js +326 -52
- package/dist/src/cli.js.map +1 -1
- package/dist/src/openclaw-doctor.d.ts +3 -5
- package/dist/src/openclaw-doctor.js +2 -99
- package/dist/src/openclaw-doctor.js.map +1 -1
- package/dist/src/openclaw-plugin-helpers.d.ts +0 -2
- package/dist/src/openclaw-plugin-helpers.js +11 -38
- package/dist/src/openclaw-plugin-helpers.js.map +1 -1
- package/dist/src/openclaw-plugin.js +6 -22
- package/dist/src/openclaw-plugin.js.map +1 -1
- package/docs/quickstart-tmux.md +9 -10
- package/openclaw.plugin.json +1 -5
- package/package.json +1 -1
- package/templates/openclaw-skills/agent-knock-knock/SKILL.md +13 -12
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
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
-
(
|
|
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
|
|
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
|
|
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:
|
|
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(
|
|
672
|
-
assertConfiguredWorkspace(
|
|
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 =
|
|
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
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
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 =
|
|
913
|
-
? ` Found ${
|
|
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
|
-
|
|
919
|
-
`
|
|
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) =>
|
|
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
|
-
|
|
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
|
|
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), {
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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 [--
|
|
8468
|
-
agent-knock-knock doctor [--
|
|
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]
|