@scotthuang/agent-knock-knock 0.2.28 → 0.2.30

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,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.30 - 2026-07-09
4
+
5
+ ### Added
6
+
7
+ - Added terminal bridge monitoring for tmux-controlled Codex conversations so AKK can send only the user-facing task text, observe Codex rollout/terminal state, and deliver the OpenClaw callback itself.
8
+
9
+ ## 0.2.29 - 2026-07-08
10
+
11
+ ### Fixed
12
+
13
+ - Clarified AKK skill and `agent_knock_knock_send` tool routing so messages to listed AKK/Codex sessions, terminal-controlled rows, or tmux targets such as `my-work:0.1` use `send` instead of starting a new delegation.
14
+
3
15
  ## 0.2.28 - 2026-07-07
4
16
 
5
17
  ### Fixed
package/README.md CHANGED
@@ -173,6 +173,7 @@ AKK list
173
173
  - `native`: local native sessions that AKK can discover but cannot directly control.
174
174
  - `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.
175
175
  - `terminal_controlled` entries can be addressed directly by their `id` from `AKK list` for `AKK send <id>`, `AKK status <id>`, `AKK cancel <id>`, and `AKK approve <id>`. They do not need an AKK state file before terminal control.
176
+ - Background sends to terminal-controlled Codex sessions use terminal bridge mode: AKK types only the user-facing task text into the tmux pane, monitors Codex rollout/terminal state, and delivers the OpenClaw callback itself when it observes completion.
176
177
  - `AKK status <terminal-controlled-id>` is the unified way to inspect current terminal output. AKK captures the terminal pane internally and returns `terminal_screen`; there is no separate public screen-capture command.
177
178
  - `AKK describe <id>` summarizes what a listed session is about. AKK-managed sessions use saved conversation history; native and terminal-controlled Codex sessions use exact Codex rollout history when a session id is available, fall back to cwd-matched rollout history when needed, and otherwise report only visible terminal/process context with lower confidence.
178
179
 
@@ -283,7 +284,7 @@ Runtime logs are diagnostic-only and are safe to use for local troubleshooting.
283
284
  - Default agent: configured with `defaultAgent`; fallback is `codex`
284
285
  - OpenClaw session: inherited from the current OpenClaw session; fallback is `agent:main:main`
285
286
  - Delegated ACPX session: generated per new task, unless `session`, `codexSession`, `claudeSession`, or `cursorSession` is configured
286
- - Agent callback timeout: `60` minutes before a waiting task is marked `stalled`
287
+ - Agent callback timeout: `60` minutes before a waiting task or terminal bridge monitor is marked `stalled`
287
288
  - Idle timeout: `10080` minutes before an idle task is lazily closed
288
289
  - Soft response limit: `50` rounds
289
290
  - Hard response limit: `100` rounds
package/dist/src/cli.js CHANGED
@@ -124,7 +124,7 @@ async function runCommand(commandName, options) {
124
124
  runCallback(options);
125
125
  }
126
126
  else if (commandName === "monitor") {
127
- runMonitor(options);
127
+ await runMonitor(options);
128
128
  }
129
129
  else if (commandName === "agent") {
130
130
  await runAgent(options);
@@ -875,7 +875,8 @@ function createNativeSessionConversation({ agent, strategy, session, modelInfo,
875
875
  model_source: modelInfo?.source,
876
876
  takeover_match_kind: takeoverMatchKind,
877
877
  terminal_control: terminalControl,
878
- needs_bootstrap: needsBootstrap
878
+ needs_bootstrap: needsBootstrap,
879
+ terminal_bridge: strategy === "terminal_control"
879
880
  }
880
881
  }, paths);
881
882
  saveState(paths.statePath, attachedConversation);
@@ -1264,6 +1265,53 @@ function startExecutorMonitor({ statePath, logPath, pid, outputPath, agentTimeou
1264
1265
  child.unref();
1265
1266
  return child;
1266
1267
  }
1268
+ function startTerminalBridgeMonitor({ statePath, logPath, agentTimeoutMinutes, pollIntervalMs, codexHome }) {
1269
+ const args = [
1270
+ new URL(import.meta.url).pathname,
1271
+ "monitor",
1272
+ "--terminal-bridge",
1273
+ "--state",
1274
+ statePath,
1275
+ "--log",
1276
+ logPath,
1277
+ "--agent-timeout-minutes",
1278
+ String(agentTimeoutMinutes),
1279
+ "--poll-interval-ms",
1280
+ String(pollIntervalMs)
1281
+ ];
1282
+ if (codexHome) {
1283
+ args.push("--codex-home", codexHome);
1284
+ }
1285
+ const child = spawn(process.execPath, args, {
1286
+ detached: true,
1287
+ stdio: "ignore",
1288
+ cwd: process.cwd(),
1289
+ env: process.env
1290
+ });
1291
+ child.unref();
1292
+ return child;
1293
+ }
1294
+ function terminalBridgeEnabled(conversation) {
1295
+ const nativeTakeover = isRecord(conversation.native_session_takeover)
1296
+ ? conversation.native_session_takeover
1297
+ : undefined;
1298
+ return nativeTakeover?.["terminal_bridge"] === true;
1299
+ }
1300
+ function withTerminalBridgeState({ conversation, message, startedAt }) {
1301
+ const nativeTakeover = isRecord(conversation.native_session_takeover)
1302
+ ? conversation.native_session_takeover
1303
+ : {};
1304
+ return {
1305
+ ...conversation,
1306
+ native_session_takeover: {
1307
+ ...nativeTakeover,
1308
+ terminal_bridge: true,
1309
+ terminal_bridge_started_at: startedAt,
1310
+ terminal_bridge_message_id: message.id
1311
+ },
1312
+ updated_at: startedAt
1313
+ };
1314
+ }
1267
1315
  function uniqueDelegateSessionName(kind) {
1268
1316
  const { sessionPrefix } = executorDefinitionForKind(kind || "claude");
1269
1317
  const timestamp = new Date().toISOString().replace(/\D/g, "").slice(0, 14);
@@ -2310,7 +2358,9 @@ async function runTerminalConversationSend({ options, conversationId, messageBod
2310
2358
  }
2311
2359
  async function runTerminalControlSend({ options, conversation, nextConversation, statePath, logPath, executor, message, terminalControl, needsNativeTakeoverBootstrap }) {
2312
2360
  const provider = createTerminalControlProvider(options);
2313
- const terminalPayload = needsNativeTakeoverBootstrap
2361
+ const bridge = terminalBridgeEnabled(conversation);
2362
+ const bridgeStartedAt = new Date().toISOString();
2363
+ const terminalPayload = needsNativeTakeoverBootstrap && !bridge
2314
2364
  ? terminalSubmissionPayload(buildAgentSendPayload({
2315
2365
  conversation,
2316
2366
  executor,
@@ -2342,14 +2392,45 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
2342
2392
  message: textSummary(message.body),
2343
2393
  payload: textSummary(terminalPayload)
2344
2394
  });
2395
+ const bridgeConversation = bridge
2396
+ ? withTerminalBridgeState({
2397
+ conversation: nextConversation,
2398
+ message,
2399
+ startedAt: bridgeStartedAt
2400
+ })
2401
+ : nextConversation;
2345
2402
  const deliveredConversation = markTakeoverBootstrapped({
2346
- conversation: nextConversation,
2403
+ conversation: bridgeConversation,
2347
2404
  statePath,
2348
2405
  logPath,
2349
2406
  executor,
2350
- native: needsNativeTakeoverBootstrap,
2407
+ native: needsNativeTakeoverBootstrap && !bridge,
2351
2408
  fork: false
2352
2409
  });
2410
+ const bridgeMonitor = bridge && deliveredConversation.gateway_method && options.disableTerminalBridgeMonitor !== true
2411
+ ? startTerminalBridgeMonitor({
2412
+ statePath,
2413
+ logPath,
2414
+ agentTimeoutMinutes: Number(options.agentTimeoutMinutes ?? DEFAULT_AGENT_TIMEOUT_MINUTES),
2415
+ pollIntervalMs: Number(options.monitorPollIntervalMs ?? DEFAULT_MONITOR_POLL_INTERVAL_MS),
2416
+ codexHome: options.codexHome
2417
+ })
2418
+ : undefined;
2419
+ if (bridgeMonitor) {
2420
+ appendEvent(logPath, {
2421
+ ts: new Date().toISOString(),
2422
+ conversation_id: deliveredConversation.conversation_id,
2423
+ event: "terminal_bridge_monitor_launch",
2424
+ pid: bridgeMonitor.pid ?? null,
2425
+ terminal_control: terminalControl,
2426
+ agent_timeout_minutes: Number(options.agentTimeoutMinutes ?? DEFAULT_AGENT_TIMEOUT_MINUTES)
2427
+ });
2428
+ runtimeLog("info", "terminal_bridge_monitor_launch", {
2429
+ conversation_id: deliveredConversation.conversation_id,
2430
+ monitor_pid: bridgeMonitor.pid ?? null,
2431
+ terminal_target: terminalControl.target
2432
+ });
2433
+ }
2353
2434
  printJson({
2354
2435
  conversation: deliveredConversation,
2355
2436
  message,
@@ -2358,6 +2439,7 @@ async function runTerminalControlSend({ options, conversation, nextConversation,
2358
2439
  background: true,
2359
2440
  callback_expected: Boolean(deliveredConversation.callback_command || deliveredConversation.gateway_method),
2360
2441
  terminal_control: terminalControl,
2442
+ monitor_pid: bridgeMonitor?.pid ?? null,
2361
2443
  executor,
2362
2444
  budget: budgetAction(deliveredConversation),
2363
2445
  openclaw_next_action: openClawYieldNextAction({
@@ -2423,7 +2505,8 @@ function createManagedTerminalConversationFromRawId({ options, conversationId, m
2423
2505
  attached_at: now.toISOString(),
2424
2506
  takeover_match_kind: "raw_terminal_send",
2425
2507
  terminal_control: terminalControl,
2426
- needs_bootstrap: true
2508
+ needs_bootstrap: false,
2509
+ terminal_bridge: true
2427
2510
  }
2428
2511
  }, paths);
2429
2512
  saveState(paths.statePath, attachedConversation);
@@ -3192,7 +3275,10 @@ function runClose(options) {
3192
3275
  closed: true
3193
3276
  });
3194
3277
  }
3195
- function runMonitor(options) {
3278
+ async function runMonitor(options) {
3279
+ if (options.terminalBridge) {
3280
+ return await runTerminalBridgeMonitor(options);
3281
+ }
3196
3282
  const statePath = expandHome(required(options.state, "--state is required"));
3197
3283
  const logPath = expandHome(options.log ?? logPathForStatePath(statePath));
3198
3284
  const pid = options.pid ? Number(options.pid) : undefined;
@@ -3321,6 +3407,227 @@ function runMonitor(options) {
3321
3407
  sleepSync(pollIntervalMs);
3322
3408
  }
3323
3409
  }
3410
+ async function runTerminalBridgeMonitor(options) {
3411
+ const statePath = expandHome(required(options.state, "--state is required"));
3412
+ const logPath = expandHome(options.log ?? logPathForStatePath(statePath));
3413
+ const timeoutMinutes = Number(options.agentTimeoutMinutes ?? DEFAULT_AGENT_TIMEOUT_MINUTES);
3414
+ const pollIntervalMs = Math.max(50, Number(options.pollIntervalMs ?? DEFAULT_MONITOR_POLL_INTERVAL_MS));
3415
+ let conversation = loadState(statePath);
3416
+ const executor = executorForConversation(conversation);
3417
+ appendEvent(logPath, {
3418
+ ts: new Date().toISOString(),
3419
+ conversation_id: conversation.conversation_id,
3420
+ event: "terminal_bridge_monitor_started",
3421
+ executor,
3422
+ agent_timeout_minutes: timeoutMinutes,
3423
+ poll_interval_ms: pollIntervalMs
3424
+ });
3425
+ runtimeLog("info", "terminal_bridge_monitor_started", {
3426
+ conversation_id: conversation.conversation_id,
3427
+ agent: executor.kind,
3428
+ executor_session: executor.session,
3429
+ agent_timeout_minutes: timeoutMinutes
3430
+ });
3431
+ while (true) {
3432
+ conversation = loadState(statePath);
3433
+ if (!isWaitingForAgent(conversation.status)) {
3434
+ runtimeLog("info", "terminal_bridge_monitor_finished", {
3435
+ conversation_id: conversation.conversation_id,
3436
+ status: conversation.status,
3437
+ reason: "conversation_no_longer_waiting"
3438
+ });
3439
+ printJson({
3440
+ conversation,
3441
+ monitored: true,
3442
+ terminal_bridge: true,
3443
+ completed: false,
3444
+ reason: "conversation_no_longer_waiting"
3445
+ });
3446
+ return;
3447
+ }
3448
+ const nativeTakeover = isRecord(conversation.native_session_takeover)
3449
+ ? conversation.native_session_takeover
3450
+ : undefined;
3451
+ const terminalControl = terminalControlFromTakeover(nativeTakeover);
3452
+ if (!terminalControl || nativeTakeover?.["terminal_bridge"] !== true) {
3453
+ const stalledConversation = markConversationStalled({
3454
+ statePath,
3455
+ logPath,
3456
+ reason: "terminal bridge monitor could not find terminal bridge metadata",
3457
+ detail: {
3458
+ terminal_bridge: true
3459
+ }
3460
+ });
3461
+ printJson({
3462
+ conversation: stalledConversation,
3463
+ monitored: true,
3464
+ terminal_bridge: true,
3465
+ stalled: true,
3466
+ reason: stalledConversation?.stalled_reason
3467
+ });
3468
+ return;
3469
+ }
3470
+ const terminalStatus = await terminalStatusForControl(terminalControl, options);
3471
+ const approval = terminalStatus.approval_state;
3472
+ if (isRecord(approval) && approval.blocked === true) {
3473
+ appendEvent(logPath, {
3474
+ ts: new Date().toISOString(),
3475
+ conversation_id: conversation.conversation_id,
3476
+ event: "terminal_bridge_approval_detected",
3477
+ terminal_control: terminalControl,
3478
+ activity_state: terminalStatus.activity_state,
3479
+ activity_reason: terminalStatus.activity_reason
3480
+ });
3481
+ printJson({
3482
+ conversation,
3483
+ monitored: true,
3484
+ terminal_bridge: true,
3485
+ awaiting_approval: true,
3486
+ terminal_control: terminalControl,
3487
+ terminal_status: terminalStatus
3488
+ });
3489
+ return;
3490
+ }
3491
+ const contextMatch = await terminalBridgeCodexContext({
3492
+ conversation,
3493
+ nativeTakeover,
3494
+ options
3495
+ });
3496
+ const startedAt = stringValue(nativeTakeover?.["terminal_bridge_started_at"]);
3497
+ const assistantMessage = contextMatch?.context
3498
+ ? latestAssistantAfter(contextMatch.context, startedAt)
3499
+ : undefined;
3500
+ const terminalStillWorking = terminalStatus.activity_state === "working";
3501
+ if (assistantMessage && !terminalStillWorking) {
3502
+ appendEvent(logPath, {
3503
+ ts: new Date().toISOString(),
3504
+ conversation_id: conversation.conversation_id,
3505
+ event: "terminal_bridge_completion_detected",
3506
+ terminal_control: terminalControl,
3507
+ match: contextMatch?.match,
3508
+ codex_session: contextMatch?.context.source,
3509
+ assistant_timestamp: assistantMessage.timestamp
3510
+ });
3511
+ const callbackMessage = createMessage({
3512
+ conversation,
3513
+ from: executor.actor,
3514
+ to: "openclaw",
3515
+ type: "done",
3516
+ requiresResponse: false,
3517
+ body: assistantMessage.text,
3518
+ metadata: {
3519
+ source: "terminal_bridge",
3520
+ terminal_control: terminalControl,
3521
+ codex_session: contextMatch?.context.source,
3522
+ confidence: contextMatch?.confidence,
3523
+ assistant_timestamp: assistantMessage.timestamp
3524
+ }
3525
+ });
3526
+ runLockedCallback({
3527
+ ...options,
3528
+ statePath,
3529
+ log: logPath,
3530
+ messageJson: JSON.stringify(callbackMessage),
3531
+ gatewayMethod: conversation.gateway_method,
3532
+ gatewaySession: conversation.gateway_session,
3533
+ openclawSession: conversation.openclaw_session,
3534
+ openclawBin: conversation.openclaw_bin,
3535
+ gatewayUrl: conversation.gateway_url,
3536
+ token: conversation.gateway_token
3537
+ });
3538
+ return;
3539
+ }
3540
+ if (Number.isFinite(timeoutMinutes) && timeoutMinutes > 0) {
3541
+ const updatedAtMs = Date.parse(String(conversation.updated_at ?? conversation.created_at));
3542
+ if (Number.isFinite(updatedAtMs) && Date.now() - updatedAtMs >= timeoutMinutes * 60 * 1000) {
3543
+ const stalledConversation = markConversationStalled({
3544
+ statePath,
3545
+ logPath,
3546
+ reason: `terminal bridge did not observe completion after ${timeoutMinutes} minutes`,
3547
+ detail: {
3548
+ terminal_bridge: true,
3549
+ terminal_control: terminalControl,
3550
+ match: contextMatch?.match,
3551
+ terminal_activity_state: terminalStatus.activity_state
3552
+ }
3553
+ });
3554
+ printJson({
3555
+ conversation: stalledConversation,
3556
+ monitored: true,
3557
+ terminal_bridge: true,
3558
+ stalled: true,
3559
+ reason: stalledConversation?.stalled_reason
3560
+ });
3561
+ return;
3562
+ }
3563
+ }
3564
+ sleepSync(pollIntervalMs);
3565
+ }
3566
+ }
3567
+ async function terminalBridgeCodexContext({ conversation, nativeTakeover, options }) {
3568
+ const provider = createAgentSessionProvider("codex", options);
3569
+ const nativeSessionId = stringValue(nativeTakeover?.["native_session_id"]);
3570
+ const terminalConversation = parseTerminalConversationId(nativeSessionId);
3571
+ const activeProcess = await activeCodexProcessForPid(options, terminalConversation?.pid);
3572
+ const directSessionId = activeProcess?.sessionId ?? (terminalConversation ? undefined : nativeSessionId);
3573
+ if (directSessionId) {
3574
+ const context = await provider.getForkContext({
3575
+ sessionId: directSessionId,
3576
+ maxMessages: Number(options.maxMessages ?? 16),
3577
+ maxCommands: Number(options.maxCommands ?? 10),
3578
+ maxTextLength: Number(options.maxTextLength ?? 4000)
3579
+ });
3580
+ if (context) {
3581
+ return {
3582
+ context,
3583
+ process: activeProcess,
3584
+ match: activeProcess?.sessionId ? "process_session_id" : "native_session_id",
3585
+ confidence: "high"
3586
+ };
3587
+ }
3588
+ }
3589
+ const cwd = activeProcess?.cwd ?? stringValue(nativeTakeover?.["source_cwd"]);
3590
+ if (!cwd) {
3591
+ return undefined;
3592
+ }
3593
+ const sessions = (await provider.listHistoricalSessions())
3594
+ .filter((session) => session.cwd === cwd)
3595
+ .sort((left, right) => Number(right.updatedAtMs ?? 0) - Number(left.updatedAtMs ?? 0));
3596
+ const selected = sessions[0];
3597
+ if (!selected) {
3598
+ return undefined;
3599
+ }
3600
+ const context = await provider.getForkContext({
3601
+ sessionId: selected.id,
3602
+ maxMessages: Number(options.maxMessages ?? 16),
3603
+ maxCommands: Number(options.maxCommands ?? 10),
3604
+ maxTextLength: Number(options.maxTextLength ?? 4000)
3605
+ });
3606
+ if (!context) {
3607
+ return undefined;
3608
+ }
3609
+ return {
3610
+ context,
3611
+ process: activeProcess,
3612
+ match: sessions.length === 1 ? "cwd" : "cwd_latest",
3613
+ confidence: sessions.length === 1 ? "medium" : "low"
3614
+ };
3615
+ }
3616
+ function latestAssistantAfter(context, startedAt) {
3617
+ const threshold = startedAt ? Date.parse(startedAt) : undefined;
3618
+ return [...visibleRolloutMessages(context)]
3619
+ .reverse()
3620
+ .find((message) => {
3621
+ if (message.role !== "assistant") {
3622
+ return false;
3623
+ }
3624
+ if (!Number.isFinite(threshold)) {
3625
+ return true;
3626
+ }
3627
+ const messageTime = message.timestamp ? Date.parse(message.timestamp) : NaN;
3628
+ return Number.isFinite(messageTime) && messageTime >= Number(threshold);
3629
+ });
3630
+ }
3324
3631
  function resolveExecutable(command) {
3325
3632
  if (command.includes(path.sep)) {
3326
3633
  return command;