@scotthuang/agent-knock-knock 0.2.9 → 0.2.11

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 CHANGED
@@ -1,5 +1,23 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.11 - 2026-06-25
4
+
5
+ ### Added
6
+
7
+ - Added terminal-controlled `AKK cancel` support by sending Control-C to the controlled tmux pane.
8
+ - Added structured terminal reachability, screen, and approval state to `AKK status` for terminal-controlled sessions.
9
+
10
+ ### Removed
11
+
12
+ - Removed the public `agent discover` CLI path and `agent_knock_knock_agent_discover` OpenClaw tool; use `AKK list` for active/local session visibility.
13
+ - Removed the legacy `safe_resume` takeover strategy; native takeover now uses active process-based terminate, terminal-control, or fork paths.
14
+
15
+ ## 0.2.10 - 2026-06-25
16
+
17
+ ### Fixed
18
+
19
+ - Added parsing for underscore-delimited tmux pane output observed in OpenClaw Gateway terminal diagnostics, allowing those panes to be matched back to Codex processes.
20
+
3
21
  ## 0.2.9 - 2026-06-25
4
22
 
5
23
  ### Fixed
package/README.md CHANGED
@@ -117,9 +117,13 @@ AKK Codex: review the current branch and propose a small fix
117
117
  AKK Claude: review the latest commit
118
118
  AKK Cursor: fix the flaky UI test
119
119
  akk list
120
+ akk status <conversation-id>
120
121
  akk send <conversation-id>: continue with the smaller implementation
121
122
  akk cancel <conversation-id>
123
+ akk recover <conversation-id>
122
124
  akk close <conversation-id>
125
+ AKK terminal takeover Codex <native-session-id>
126
+ AKK approve <conversation-id>
123
127
  ```
124
128
 
125
129
  Surfaces that support OpenClaw native commands can use the same workflow through `/akk`:
@@ -154,32 +158,23 @@ Optional default-agent config:
154
158
 
155
159
  ## Native Codex Takeover
156
160
 
157
- Experimental: AKK can discover and take over Codex CLI sessions that were started outside AKK. This is useful when you started Codex in a terminal, left the machine, and want OpenClaw to continue managing that work from chat.
161
+ Experimental: AKK can list and take over Codex CLI sessions that were started outside AKK. This is useful when you started Codex in a terminal, left the machine, and want OpenClaw to continue managing that work from chat.
158
162
 
159
163
  List current AKK-managed and local agent work:
160
164
 
161
165
  ```text
162
166
  AKK list
163
- AKK Codex active
164
167
  ```
165
168
 
166
169
  `AKK list` returns separate groups for:
167
170
 
168
171
  - `delegated`: AKK-managed background tasks.
169
172
  - `native`: local native sessions that AKK can discover but cannot directly control.
170
- - `terminal_controlled`: local sessions running in a controllable terminal provider such as tmux. These entries include terminal metadata, command capabilities, and concise approval state when a visible approval prompt is detected.
171
-
172
- Lower-level discovery prompts remain available for historical session and capability inspection:
173
-
174
- ```text
175
- AKK Codex sessions
176
- AKK Codex capabilities
177
- ```
173
+ - `terminal_controlled`: local sessions running in a controllable terminal provider. The current provider is tmux. These entries include terminal metadata, command capabilities, and concise approval state when a visible approval prompt is detected.
178
174
 
179
175
  Takeover prompts:
180
176
 
181
177
  ```text
182
- AKK safe resume Codex <native-session-id>
183
178
  AKK takeover Codex <native-session-id>
184
179
  AKK terminal takeover Codex <native-session-id>
185
180
  AKK fork takeover Codex <native-session-id>
@@ -188,9 +183,8 @@ AKK approve <conversation-id>
188
183
 
189
184
  Strategies:
190
185
 
191
- - `safe resume`: attach a stopped/inactive Codex session to AKK.
192
186
  - `takeover`: stop an active matching Codex CLI after explicit confirmation, then resume it under AKK.
193
- - `terminal takeover`: attach an active Codex CLI running inside tmux after explicit confirmation. AKK sends follow-ups directly to the tmux pane and can approve the currently visible Codex approval prompt.
187
+ - `terminal takeover`: attach an active Codex CLI running inside a controllable terminal provider after explicit confirmation. AKK sends follow-ups directly to the terminal pane, can send Control-C for cancel, and can approve the currently visible Codex approval prompt.
194
188
  - `fork takeover`: keep the original Codex CLI running, ask OpenClaw to summarize bounded source context, then create a new AKK-managed fork from that summary.
195
189
 
196
190
  For long-running local Codex sessions that you may want OpenClaw to operate from WeChat, run Codex inside tmux. AKK does not require tmux, but tmux enables terminal-control takeover without stopping the original process:
package/dist/src/cli.js CHANGED
@@ -13,7 +13,7 @@ import { executorBootstrapPrompt } from "./bootstrap.js";
13
13
  import { writeRuntimeLog } from "./runtime-log.js";
14
14
  import { formatTranscript, readNdjsonLog } from "./transcript.js";
15
15
  import { appendEvent, defaultStoreDir, listConversations, logPathForStatePath, loadConversationById, loadState, messageEvent, pathsForConversation, pathsForConversationDir, saveState, statePathForConversationId } from "./store.js";
16
- import { planFork, planSafeResume, planTakeover } from "./session-takeover-planner.js";
16
+ import { planFork, planTakeover } from "./session-takeover-planner.js";
17
17
  import { StaticTerminalControlProvider, TmuxTerminalControlProvider, enrichActiveProcessesWithTerminalControl } from "./terminal-control-provider.js";
18
18
  const DEFAULT_IDLE_TIMEOUT_MINUTES = 10080;
19
19
  const DEFAULT_AGENT_TIMEOUT_MINUTES = 60;
@@ -99,7 +99,7 @@ async function runCommand(commandName, options) {
99
99
  await runApprove(options);
100
100
  }
101
101
  else if (commandName === "cancel") {
102
- runCancel(options);
102
+ await runCancel(options);
103
103
  }
104
104
  else if (commandName === "recover") {
105
105
  runRecover(options);
@@ -210,47 +210,13 @@ function runDoctor(options) {
210
210
  });
211
211
  }
212
212
  async function runAgent(options) {
213
- const agentCommand = required(options.agentCommand, "agent subcommand is required: discover or takeover");
214
- if (agentCommand === "discover") {
215
- printJson(await runAgentDiscover(options));
216
- return;
217
- }
213
+ const agentCommand = required(options.agentCommand, "agent subcommand is required: takeover");
218
214
  if (agentCommand === "takeover") {
219
215
  printJson(await runAgentTakeover(options));
220
216
  return;
221
217
  }
222
218
  throw new Error(`unsupported agent subcommand: ${agentCommand}`);
223
219
  }
224
- async function runAgentDiscover(options) {
225
- const agent = required(options.agent, "--agent is required");
226
- const scope = required(options.scope, "--scope is required");
227
- const provider = createAgentSessionProvider(agent, options);
228
- const capabilities = await provider.getCapabilities();
229
- if (scope === "capabilities") {
230
- return {
231
- agent,
232
- scope,
233
- capabilities
234
- };
235
- }
236
- if (scope === "sessions") {
237
- return {
238
- agent,
239
- scope,
240
- capabilities,
241
- sessions: await provider.listHistoricalSessions()
242
- };
243
- }
244
- if (scope === "active") {
245
- return {
246
- agent,
247
- scope,
248
- capabilities,
249
- active: await listActiveSessionsWithTerminalControl(provider, options)
250
- };
251
- }
252
- throw new Error(`unsupported discover scope: ${scope}`);
253
- }
254
220
  async function runAgentTakeover(options) {
255
221
  const agent = required(options.agent, "--agent is required");
256
222
  const sessionId = required(options.sessionId, "--session-id is required");
@@ -270,36 +236,6 @@ async function runAgentTakeover(options) {
270
236
  }
271
237
  };
272
238
  }
273
- if (strategy === "safe_resume") {
274
- const plan = planSafeResume(session, await listActiveSessionsWithTerminalControl(provider, options));
275
- if (plan.allowed && options.createConversation) {
276
- const modelInfo = await provider.getSessionModel(session.id);
277
- const attached = createNativeSessionConversation({
278
- agent,
279
- strategy,
280
- session,
281
- modelInfo,
282
- options
283
- });
284
- return {
285
- agent,
286
- sessionId,
287
- strategy,
288
- status: "attached",
289
- sideEffectsExecuted: true,
290
- plan,
291
- ...attached
292
- };
293
- }
294
- return {
295
- agent,
296
- sessionId,
297
- strategy,
298
- status: plan.allowed ? "ready" : "blocked",
299
- sideEffectsExecuted: false,
300
- plan
301
- };
302
- }
303
239
  if (strategy === "terminate_then_resume") {
304
240
  const activeSessions = await listActiveSessionsWithTerminalControl(provider, options);
305
241
  const plan = planTakeover(session, activeSessions);
@@ -402,7 +338,7 @@ async function runAgentTakeover(options) {
402
338
  plan,
403
339
  error: {
404
340
  code: "terminal_target_unavailable",
405
- message: `No matching tmux-controlled Codex process was found for ${terminalTarget}`
341
+ message: `No matching terminal-controlled Codex process was found for ${terminalTarget}`
406
342
  }
407
343
  };
408
344
  }
@@ -1419,7 +1355,6 @@ function nativeListEntry(session, activeSessions) {
1419
1355
  confidence: session.confidence,
1420
1356
  reason: session.reason,
1421
1357
  commands: {
1422
- safe_resume: Boolean(session.sessionId),
1423
1358
  terminate_then_resume: true,
1424
1359
  fork: Boolean(session.sessionId),
1425
1360
  terminal_control_attach: false,
@@ -1458,7 +1393,7 @@ async function terminalControlledListEntry(session, activeSessions, options) {
1458
1393
  capture_screen: true,
1459
1394
  approve: approvalState.approvable === true,
1460
1395
  status: true,
1461
- cancel: false,
1396
+ cancel: true,
1462
1397
  close: false
1463
1398
  }
1464
1399
  };
@@ -1540,21 +1475,8 @@ async function runStatus(options) {
1540
1475
  const terminalControl = terminalControlFromTakeover(isRecord(conversation.native_session_takeover) ? conversation.native_session_takeover : undefined);
1541
1476
  if (terminalControl) {
1542
1477
  result.terminal_control = terminalControl;
1543
- try {
1544
- const screen = await createTerminalControlProvider(options).capture(terminalControl.target, {
1545
- scrollbackLines: Number(options.scrollbackLines ?? 120),
1546
- socketPath: terminalControl.socketPath
1547
- });
1548
- result.terminal_screen = {
1549
- excerpt: screenExcerpt(screen),
1550
- approval: detectCodexApprovalPrompt(screen)
1551
- };
1552
- }
1553
- catch (error) {
1554
- result.terminal_screen = {
1555
- error: error instanceof Error ? error.message : String(error)
1556
- };
1557
- }
1478
+ result.terminal_status = await terminalStatusForControl(terminalControl, options);
1479
+ result.terminal_screen = result.terminal_status.screen;
1558
1480
  }
1559
1481
  printJson(result);
1560
1482
  runtimeLog("info", "task_status_read", {
@@ -1566,6 +1488,48 @@ async function runStatus(options) {
1566
1488
  trace: Boolean(options.trace)
1567
1489
  });
1568
1490
  }
1491
+ async function terminalStatusForControl(terminalControl, options) {
1492
+ try {
1493
+ const screen = await createTerminalControlProvider(options).capture(terminalControl.target, {
1494
+ scrollbackLines: Number(options.scrollbackLines ?? 120),
1495
+ socketPath: terminalControl.socketPath
1496
+ });
1497
+ const approval = detectCodexApprovalPrompt(screen);
1498
+ const blocked = isCodexApprovalPromptVisible(screen);
1499
+ return {
1500
+ provider: terminalControl.kind,
1501
+ target: terminalControl.target,
1502
+ reachable: true,
1503
+ approval_state: {
1504
+ scanned: true,
1505
+ blocked,
1506
+ approvable: approval.approvable,
1507
+ key: approval.approvable ? approval.key : undefined,
1508
+ label: approval.approvable ? approval.label : undefined,
1509
+ reason: approval.approvable ? undefined : approval.reason
1510
+ },
1511
+ screen: {
1512
+ excerpt: screenExcerpt(screen),
1513
+ approval
1514
+ }
1515
+ };
1516
+ }
1517
+ catch (error) {
1518
+ return {
1519
+ provider: terminalControl.kind,
1520
+ target: terminalControl.target,
1521
+ reachable: false,
1522
+ approval_state: {
1523
+ scanned: false,
1524
+ blocked: false,
1525
+ approvable: false
1526
+ },
1527
+ screen: {
1528
+ error: error instanceof Error ? error.message : String(error)
1529
+ }
1530
+ };
1531
+ }
1532
+ }
1569
1533
  async function runSend(options) {
1570
1534
  const messageBody = required(options.message ?? options.request, "--message is required");
1571
1535
  cleanupIdleConversations(storeDirFromOptions(options), options);
@@ -2370,12 +2334,26 @@ function markConversationNeedsRecovery({ conversation, statePath, logPath, execu
2370
2334
  budget: budgetAction(nextConversation)
2371
2335
  };
2372
2336
  }
2373
- function runCancel(options) {
2337
+ async function runCancel(options) {
2374
2338
  cleanupIdleConversations(storeDirFromOptions(options), options);
2375
2339
  const { conversation, statePath, logPath } = loadConversationFromOptions(options);
2376
2340
  if (["closed", "cancelled"].includes(conversation.status)) {
2377
2341
  throw new Error(`cannot cancel ${conversation.conversation_id}; conversation is ${conversation.status}`);
2378
2342
  }
2343
+ const nativeTakeover = isRecord(conversation.native_session_takeover)
2344
+ ? conversation.native_session_takeover
2345
+ : undefined;
2346
+ const terminalControl = terminalControlFromTakeover(nativeTakeover);
2347
+ if (terminalControl) {
2348
+ await runTerminalControlCancel({
2349
+ options,
2350
+ conversation,
2351
+ statePath,
2352
+ logPath,
2353
+ terminalControl
2354
+ });
2355
+ return;
2356
+ }
2379
2357
  const executor = executorForConversation(conversation);
2380
2358
  const acpxPath = resolveExecutable("acpx");
2381
2359
  const executorEnv = environmentForExecutor(executor, {
@@ -2434,6 +2412,38 @@ function runCancel(options) {
2434
2412
  budget: budgetAction(nextConversation)
2435
2413
  });
2436
2414
  }
2415
+ async function runTerminalControlCancel({ options, conversation, statePath, logPath, terminalControl }) {
2416
+ const provider = createTerminalControlProvider(options);
2417
+ await provider.sendKeys(terminalControl.target, ["C-c"], {
2418
+ socketPath: terminalControl.socketPath
2419
+ });
2420
+ const now = new Date().toISOString();
2421
+ appendEvent(logPath, {
2422
+ ts: now,
2423
+ conversation_id: conversation.conversation_id,
2424
+ event: "terminal_cancel_requested",
2425
+ terminal_control: terminalControl,
2426
+ key: "C-c"
2427
+ });
2428
+ runtimeLog("info", "terminal_cancel_requested", {
2429
+ conversation_id: conversation.conversation_id,
2430
+ terminal_target: terminalControl.target,
2431
+ key: "C-c"
2432
+ });
2433
+ const nextConversation = {
2434
+ ...conversation,
2435
+ terminal_cancel_requested_at: now,
2436
+ updated_at: now
2437
+ };
2438
+ saveState(statePath, nextConversation);
2439
+ printJson({
2440
+ conversation: nextConversation,
2441
+ cancel_requested: true,
2442
+ terminal_control: terminalControl,
2443
+ key: "C-c",
2444
+ budget: budgetAction(nextConversation)
2445
+ });
2446
+ }
2437
2447
  function runRecover(options) {
2438
2448
  runRecoveryDecision({ ...options, mode: "recover" });
2439
2449
  }
@@ -4059,8 +4069,7 @@ function usage() {
4059
4069
  agent-knock-knock close --conversation <id> [--reason <text>]
4060
4070
  agent-knock-knock install-openclaw [--openclaw-bin <path>] [--skill-path <path>] [--no-restart]
4061
4071
  agent-knock-knock doctor
4062
- agent-knock-knock agent discover --agent codex --scope capabilities|sessions|active
4063
- agent-knock-knock agent takeover --agent codex --session-id <id> --strategy safe_resume|terminate_then_resume|terminal_control|fork [--create-conversation]
4072
+ agent-knock-knock agent takeover --agent codex --session-id <id> --strategy terminate_then_resume|terminal_control|fork [--create-conversation]
4064
4073
  agent-knock-knock callback --state <file> --message-json <json> [--record-only]
4065
4074
  agent-knock-knock transcript --log <file> [--include-raw]
4066
4075
  agent-knock-knock transcript --conversation <dir> [--include-raw]