@scotthuang/agent-knock-knock 0.2.20 → 0.2.22

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,21 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.22 - 2026-07-04
4
+
5
+ ### Added
6
+
7
+ - Added `AKK describe` / `AKK summary` to summarize what an AKK-managed, native Codex, or terminal-controlled Codex session is about.
8
+ - Added the `agent_knock_knock_describe` OpenClaw tool and `/akk describe <conversation-id>` command.
9
+ - Reused Codex rollout history for exact session matches, added cwd-based fallback matching, and returned screen-only summaries with explicit low-confidence limitations when no history can be found.
10
+
11
+ ## 0.2.21 - 2026-07-03
12
+
13
+ ### Added
14
+
15
+ - Added structured tmux Codex activity detection for terminal-controlled sessions, exposing `activity_state` and `activity_reason` in `AKK status` and `AKK list`.
16
+ - Classified terminal-controlled Codex panes as `awaiting_approval`, `working`, `idle`, or `unknown` using conservative current-screen heuristics.
17
+ - Added regression coverage for approval, stale approval scrollback, working, idle, unknown, and list activity-state output.
18
+
3
19
  ## 0.2.20 - 2026-07-02
4
20
 
5
21
  ### Fixed
package/README.md CHANGED
@@ -118,6 +118,7 @@ AKK Claude: review the latest commit
118
118
  AKK Cursor: fix the flaky UI test
119
119
  akk list
120
120
  akk status <conversation-id>
121
+ akk describe <conversation-id>
121
122
  akk send <conversation-id>: continue with the smaller implementation
122
123
  akk cancel <conversation-id>
123
124
  akk recover <conversation-id>
@@ -173,6 +174,7 @@ AKK list
173
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.
174
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.
175
176
  - `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
+ - `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.
176
178
 
177
179
  Takeover prompts:
178
180
 
package/dist/src/cli.js CHANGED
@@ -92,6 +92,9 @@ async function runCommand(commandName, options) {
92
92
  else if (commandName === "status") {
93
93
  await runStatus(options);
94
94
  }
95
+ else if (commandName === "describe" || commandName === "summary") {
96
+ await runDescribe(options);
97
+ }
95
98
  else if (commandName === "send") {
96
99
  await runSend(options);
97
100
  }
@@ -669,6 +672,53 @@ function isPostApprovalActivityLine(line) {
669
672
  }
670
673
  return /^─\s*Worked for\b/u.test(trimmed);
671
674
  }
675
+ function detectCodexActivityState(screen, approval = detectCodexApprovalPrompt(screen)) {
676
+ if (approval.approvable || isCodexApprovalPromptVisible(screen)) {
677
+ return {
678
+ state: "awaiting_approval",
679
+ reason: "current Codex approval prompt is visible"
680
+ };
681
+ }
682
+ const tailLines = screen.trimEnd().split(/\r?\n/).slice(-30);
683
+ const workingLine = tailLines.find((line) => isCodexWorkingLine(line));
684
+ if (workingLine) {
685
+ return {
686
+ state: "working",
687
+ reason: `Codex working marker detected: ${workingLine.trim()}`
688
+ };
689
+ }
690
+ const lastLine = tailLines
691
+ .map((line) => line.trim())
692
+ .filter(Boolean)
693
+ .at(-1);
694
+ if (lastLine && isCodexIdlePromptLine(lastLine)) {
695
+ return {
696
+ state: "idle",
697
+ reason: `Codex input prompt detected: ${lastLine}`
698
+ };
699
+ }
700
+ return {
701
+ state: "unknown",
702
+ reason: "no current Codex working, idle, or approval marker was detected in the terminal screen"
703
+ };
704
+ }
705
+ function isCodexWorkingLine(line) {
706
+ const trimmed = line.trim();
707
+ if (!trimmed) {
708
+ return false;
709
+ }
710
+ if (/^•\s+Working\b/u.test(trimmed)) {
711
+ return true;
712
+ }
713
+ if (/\besc to interrupt\b/u.test(trimmed) && (/\bWorking\b/u.test(trimmed) || /\b\/stop to close\b/u.test(trimmed))) {
714
+ return true;
715
+ }
716
+ return /\bbackground terminal running\b/u.test(trimmed);
717
+ }
718
+ function isCodexIdlePromptLine(line) {
719
+ const trimmed = line.trim();
720
+ return /^›(?:\s|$)/u.test(trimmed) && !/^›\s*1\./u.test(trimmed);
721
+ }
672
722
  function screenExcerpt(screen, maxLength = 4000) {
673
723
  const lines = screen.trimEnd().split(/\r?\n/);
674
724
  return lines.slice(Math.max(0, lines.length - 80)).join("\n").slice(-maxLength);
@@ -1415,7 +1465,7 @@ async function terminalControlledListEntry(session, activeSessions, options) {
1415
1465
  if (!terminalControl) {
1416
1466
  throw new Error(`process ${session.pid} is not terminal-controlled`);
1417
1467
  }
1418
- const approvalState = await approvalStateForTerminal(terminalControl, options);
1468
+ const terminalState = await listStateForTerminal(terminalControl, options);
1419
1469
  return {
1420
1470
  id: `terminal:${terminalControl.kind}:${terminalControl.target}:${session.pid}`,
1421
1471
  source: "terminal_control",
@@ -1431,23 +1481,29 @@ async function terminalControlledListEntry(session, activeSessions, options) {
1431
1481
  confidence: session.confidence,
1432
1482
  reason: session.reason,
1433
1483
  terminal_control: terminalControl,
1434
- approval_state: approvalState,
1484
+ approval_state: terminalState.approval_state,
1485
+ activity_state: terminalState.activity_state,
1486
+ activity_reason: terminalState.activity_reason,
1435
1487
  commands: {
1436
1488
  send: true,
1437
- approve: approvalState.approvable === true,
1489
+ approve: terminalState.approval_state.approvable === true,
1438
1490
  status: true,
1439
1491
  cancel: true,
1440
1492
  close: false
1441
1493
  }
1442
1494
  };
1443
1495
  }
1444
- async function approvalStateForTerminal(terminalControl, options) {
1496
+ async function listStateForTerminal(terminalControl, options) {
1445
1497
  if (options.noApprovalScan) {
1446
1498
  return {
1447
- scanned: false,
1448
- blocked: false,
1449
- approvable: false,
1450
- reason: "approval scan disabled"
1499
+ approval_state: {
1500
+ scanned: false,
1501
+ blocked: false,
1502
+ approvable: false,
1503
+ reason: "approval scan disabled"
1504
+ },
1505
+ activity_state: "unknown",
1506
+ activity_reason: "terminal screen scan disabled"
1451
1507
  };
1452
1508
  }
1453
1509
  try {
@@ -1457,22 +1513,31 @@ async function approvalStateForTerminal(terminalControl, options) {
1457
1513
  });
1458
1514
  const approval = detectCodexApprovalPrompt(screen);
1459
1515
  const blocked = isCodexApprovalPromptVisible(screen);
1516
+ const activity = detectCodexActivityState(screen, approval);
1460
1517
  return {
1461
- scanned: true,
1462
- blocked,
1463
- approvable: approval.approvable,
1464
- key: approval.approvable ? approval.key : undefined,
1465
- label: approval.approvable ? approval.label : undefined,
1466
- reason: approval.approvable ? undefined : approval.reason,
1467
- screen_excerpt: blocked ? screenExcerpt(screen, 1000) : undefined
1518
+ approval_state: {
1519
+ scanned: true,
1520
+ blocked,
1521
+ approvable: approval.approvable,
1522
+ key: approval.approvable ? approval.key : undefined,
1523
+ label: approval.approvable ? approval.label : undefined,
1524
+ reason: approval.approvable ? undefined : approval.reason,
1525
+ screen_excerpt: blocked ? screenExcerpt(screen, 1000) : undefined
1526
+ },
1527
+ activity_state: activity.state,
1528
+ activity_reason: activity.reason
1468
1529
  };
1469
1530
  }
1470
1531
  catch (error) {
1471
1532
  return {
1472
- scanned: false,
1473
- blocked: false,
1474
- approvable: false,
1475
- error: error instanceof Error ? error.message : String(error)
1533
+ approval_state: {
1534
+ scanned: false,
1535
+ blocked: false,
1536
+ approvable: false,
1537
+ error: error instanceof Error ? error.message : String(error)
1538
+ },
1539
+ activity_state: "unknown",
1540
+ activity_reason: error instanceof Error ? error.message : String(error)
1476
1541
  };
1477
1542
  }
1478
1543
  }
@@ -1539,6 +1604,21 @@ function parseTerminalConversationId(conversationId) {
1539
1604
  pid
1540
1605
  };
1541
1606
  }
1607
+ function parseNativeConversationId(conversationId) {
1608
+ const prefix = "native:codex:";
1609
+ if (!conversationId?.startsWith(prefix)) {
1610
+ return undefined;
1611
+ }
1612
+ const pid = Number(conversationId.slice(prefix.length));
1613
+ if (!Number.isInteger(pid)) {
1614
+ throw new Error(`invalid native conversation id: ${conversationId}`);
1615
+ }
1616
+ return {
1617
+ conversationId,
1618
+ agent: "codex",
1619
+ pid
1620
+ };
1621
+ }
1542
1622
  async function runStatus(options) {
1543
1623
  cleanupIdleConversations(storeDirFromOptions(options), options);
1544
1624
  const terminalConversation = await resolveTerminalConversationFromOptions(options);
@@ -1587,6 +1667,55 @@ async function runStatus(options) {
1587
1667
  trace: Boolean(options.trace)
1588
1668
  });
1589
1669
  }
1670
+ async function runDescribe(options) {
1671
+ cleanupIdleConversations(storeDirFromOptions(options), options);
1672
+ const conversationId = required(options.conversation ?? options.conversationId, "--conversation is required");
1673
+ const terminalConversation = await resolveTerminalConversationFromOptions(options);
1674
+ if (terminalConversation) {
1675
+ const terminalStatus = await terminalStatusForControl(terminalConversation.terminalControl, options);
1676
+ const process = await activeCodexProcessForPid(options, parseTerminalConversationId(conversationId)?.pid);
1677
+ printJson(await describeNativeCodexSession({
1678
+ id: conversationId,
1679
+ source: "terminal_control",
1680
+ process,
1681
+ options,
1682
+ terminalControl: terminalConversation.terminalControl,
1683
+ terminalStatus
1684
+ }));
1685
+ return;
1686
+ }
1687
+ const nativeConversation = parseNativeConversationId(conversationId);
1688
+ if (nativeConversation) {
1689
+ const process = await activeCodexProcessForPid(options, nativeConversation.pid);
1690
+ printJson(await describeNativeCodexSession({
1691
+ id: conversationId,
1692
+ source: "native_active",
1693
+ process,
1694
+ options
1695
+ }));
1696
+ return;
1697
+ }
1698
+ const { conversation, statePath, logPath } = loadConversationFromOptions(options);
1699
+ const events = readExistingEvents(logPath);
1700
+ const terminalControl = terminalControlFromTakeover(isRecord(conversation.native_session_takeover) ? conversation.native_session_takeover : undefined);
1701
+ const terminalStatus = terminalControl ? await terminalStatusForControl(terminalControl, options) : undefined;
1702
+ printJson({
1703
+ conversation_id: conversation.conversation_id,
1704
+ source: "akk_managed",
1705
+ confidence: "high",
1706
+ about: managedConversationAbout(conversation, events, terminalStatus),
1707
+ summary: summarizeConversation(conversation),
1708
+ evidence: {
1709
+ initial_request: conversation.user_request,
1710
+ recent_messages: recentMessageEvidence(events),
1711
+ trace: buildConversationTrace({ conversation, events, logPath }),
1712
+ terminal_screen: terminalStatus?.screen
1713
+ },
1714
+ limitations: terminalStatus?.reachable === false ? ["terminal status unavailable"] : [],
1715
+ state_path: statePath,
1716
+ event_log_path: logPath
1717
+ });
1718
+ }
1590
1719
  async function terminalStatusForControl(terminalControl, options) {
1591
1720
  try {
1592
1721
  const screen = await createTerminalControlProvider(options).capture(terminalControl.target, {
@@ -1595,10 +1724,13 @@ async function terminalStatusForControl(terminalControl, options) {
1595
1724
  });
1596
1725
  const approval = detectCodexApprovalPrompt(screen);
1597
1726
  const blocked = isCodexApprovalPromptVisible(screen);
1727
+ const activity = detectCodexActivityState(screen, approval);
1598
1728
  return {
1599
1729
  provider: terminalControl.kind,
1600
1730
  target: terminalControl.target,
1601
1731
  reachable: true,
1732
+ activity_state: activity.state,
1733
+ activity_reason: activity.reason,
1602
1734
  approval_state: {
1603
1735
  scanned: true,
1604
1736
  blocked,
@@ -1618,6 +1750,8 @@ async function terminalStatusForControl(terminalControl, options) {
1618
1750
  provider: terminalControl.kind,
1619
1751
  target: terminalControl.target,
1620
1752
  reachable: false,
1753
+ activity_state: "unknown",
1754
+ activity_reason: error instanceof Error ? error.message : String(error),
1621
1755
  approval_state: {
1622
1756
  scanned: false,
1623
1757
  blocked: false,
@@ -3574,6 +3708,168 @@ function summarizeEvent(event) {
3574
3708
  body: typeof event.body === "string" ? event.body.slice(0, 500) : undefined
3575
3709
  };
3576
3710
  }
3711
+ async function activeCodexProcessForPid(options, pid) {
3712
+ if (!Number.isInteger(pid)) {
3713
+ return undefined;
3714
+ }
3715
+ const provider = createAgentSessionProvider("codex", options);
3716
+ const activeSessions = await listActiveSessionsWithTerminalControl(provider, options);
3717
+ return activeSessions.find((process) => process.pid === pid);
3718
+ }
3719
+ async function describeNativeCodexSession({ id, source, process, options, terminalControl, terminalStatus }) {
3720
+ const provider = createAgentSessionProvider("codex", options);
3721
+ const directSessionId = process?.sessionId;
3722
+ if (directSessionId) {
3723
+ const context = await provider.getForkContext({
3724
+ sessionId: directSessionId,
3725
+ maxMessages: Number(options.maxMessages ?? 16),
3726
+ maxCommands: Number(options.maxCommands ?? 10),
3727
+ maxTextLength: Number(options.maxTextLength ?? 1200)
3728
+ });
3729
+ if (context) {
3730
+ return nativeDescriptionFromContext({
3731
+ id,
3732
+ source,
3733
+ confidence: "high",
3734
+ match: "session_id",
3735
+ process,
3736
+ context,
3737
+ terminalControl,
3738
+ terminalStatus,
3739
+ limitations: []
3740
+ });
3741
+ }
3742
+ }
3743
+ const cwd = process?.cwd ?? terminalControl?.currentPath;
3744
+ const sessions = (await provider.listHistoricalSessions())
3745
+ .filter((session) => session.cwd === cwd)
3746
+ .sort((left, right) => Number(right.updatedAtMs ?? 0) - Number(left.updatedAtMs ?? 0));
3747
+ if (sessions.length > 0) {
3748
+ const selected = sessions[0];
3749
+ const context = await provider.getForkContext({
3750
+ sessionId: selected.id,
3751
+ maxMessages: Number(options.maxMessages ?? 16),
3752
+ maxCommands: Number(options.maxCommands ?? 10),
3753
+ maxTextLength: Number(options.maxTextLength ?? 1200)
3754
+ });
3755
+ if (context) {
3756
+ return nativeDescriptionFromContext({
3757
+ id,
3758
+ source,
3759
+ confidence: sessions.length === 1 ? "medium" : "low",
3760
+ match: sessions.length === 1 ? "cwd" : "cwd_latest",
3761
+ process,
3762
+ context,
3763
+ terminalControl,
3764
+ terminalStatus,
3765
+ limitations: sessions.length === 1
3766
+ ? ["Codex session inferred from matching cwd because the active process did not expose a session id."]
3767
+ : [`Codex session inferred from the most recent of ${sessions.length} sessions with the same cwd.`],
3768
+ candidates: sessions.slice(0, 5).map((session) => ({
3769
+ session_id: session.id,
3770
+ cwd: session.cwd,
3771
+ title: session.title ?? session.preview ?? session.firstUserMessage,
3772
+ updated_at_ms: session.updatedAtMs,
3773
+ capability: session.capability
3774
+ }))
3775
+ });
3776
+ }
3777
+ }
3778
+ return {
3779
+ conversation_id: id,
3780
+ source,
3781
+ confidence: "screen_only",
3782
+ match: "terminal_screen",
3783
+ about: screenOnlyAbout({ process, terminalStatus }),
3784
+ evidence: {
3785
+ process,
3786
+ terminal_control: terminalControl,
3787
+ terminal_status: terminalStatus
3788
+ },
3789
+ limitations: [
3790
+ "No exact Codex session id was available.",
3791
+ cwd ? "No matching Codex rollout history was found for this cwd." : "No process cwd was available for Codex history matching.",
3792
+ "Summary is limited to active process metadata and the visible terminal screen."
3793
+ ]
3794
+ };
3795
+ }
3796
+ function nativeDescriptionFromContext({ id, source, confidence, match, process, context, terminalControl, terminalStatus, limitations, candidates }) {
3797
+ return {
3798
+ conversation_id: id,
3799
+ source,
3800
+ confidence,
3801
+ match,
3802
+ about: rolloutAbout(context, terminalStatus),
3803
+ codex_session: context.source,
3804
+ evidence: {
3805
+ process,
3806
+ terminal_control: terminalControl,
3807
+ terminal_status: terminalStatus,
3808
+ initial_request: firstUserText(context),
3809
+ title: context.source.title,
3810
+ recent_messages: context.messages.slice(-8),
3811
+ recent_commands: context.commands.slice(-8),
3812
+ candidates
3813
+ },
3814
+ limitations
3815
+ };
3816
+ }
3817
+ function managedConversationAbout(conversation, events, terminalStatus) {
3818
+ const request = truncateText(String(conversation.user_request ?? "").trim(), 220);
3819
+ const recent = recentMessageEvidence(events).at(-1)?.body;
3820
+ const parts = [
3821
+ request ? `Initial request: ${request}` : undefined,
3822
+ recent ? `Latest visible message: ${truncateText(recent, 180)}` : undefined,
3823
+ terminalStatus?.activity_state ? `Current terminal state: ${terminalStatus.activity_state}.` : undefined
3824
+ ].filter(Boolean);
3825
+ return parts.length > 0 ? parts.join(" ") : "No durable task content is available for this AKK-managed session.";
3826
+ }
3827
+ function rolloutAbout(context, terminalStatus) {
3828
+ const title = truncateText(String(context.source.title ?? "").trim(), 180);
3829
+ const firstUser = firstUserText(context);
3830
+ const latestAssistant = [...context.messages].reverse().find((message) => message.role === "assistant")?.text;
3831
+ const latestCommand = context.commands.at(-1)?.command;
3832
+ const parts = [
3833
+ firstUser ? `Initial request: ${truncateText(firstUser, 220)}` : title ? `Codex title: ${title}` : undefined,
3834
+ latestAssistant ? `Latest visible progress: ${truncateText(latestAssistant, 180)}` : undefined,
3835
+ latestCommand ? `Recent command: ${truncateText(latestCommand, 140)}` : undefined,
3836
+ terminalStatus?.activity_state ? `Current terminal state: ${terminalStatus.activity_state}.` : undefined
3837
+ ].filter(Boolean);
3838
+ return parts.length > 0 ? parts.join(" ") : "Codex history was found, but it did not include enough visible message content to summarize the session.";
3839
+ }
3840
+ function screenOnlyAbout({ process, terminalStatus }) {
3841
+ const activity = terminalStatus?.activity_reason ?? terminalStatus?.activity_state;
3842
+ const excerpt = terminalStatus?.screen?.excerpt;
3843
+ const parts = [
3844
+ process?.cwd ? `This Codex process is running in ${process.cwd}.` : undefined,
3845
+ activity ? `Terminal activity: ${truncateText(String(activity), 180)}` : undefined,
3846
+ excerpt ? `Visible screen: ${truncateText(String(excerpt), 220)}` : undefined
3847
+ ].filter(Boolean);
3848
+ return parts.length > 0 ? parts.join(" ") : "Only active process metadata is available; no Codex conversation history or terminal screen content could be read.";
3849
+ }
3850
+ function firstUserText(context) {
3851
+ return context.messages.find((message) => message.role === "user")?.text ?? context.source.title;
3852
+ }
3853
+ function recentMessageEvidence(events) {
3854
+ return events
3855
+ .filter((event) => event.event === "message" && typeof event.body === "string")
3856
+ .slice(-8)
3857
+ .map((event) => ({
3858
+ ts: event.ts,
3859
+ from: event.from,
3860
+ to: event.to,
3861
+ type: event.type,
3862
+ round: event.round,
3863
+ body: truncateText(event.body, 800)
3864
+ }));
3865
+ }
3866
+ function truncateText(value, maxLength) {
3867
+ const text = String(value ?? "").replace(/\s+/g, " ").trim();
3868
+ if (text.length <= maxLength) {
3869
+ return text;
3870
+ }
3871
+ return `${text.slice(0, Math.max(0, maxLength - 3))}...`;
3872
+ }
3577
3873
  function buildConversationTrace({ conversation, events, logPath }) {
3578
3874
  const outputPath = traceOutputPath({ conversation, events, logPath });
3579
3875
  const output = outputPath && fs.existsSync(outputPath)
@@ -4581,6 +4877,7 @@ function usage() {
4581
4877
  agent-knock-knock delegate --request <text> [--agent ${agentList}] [--store-dir <dir>] [--all-proxy <url>] [--agent-timeout-minutes <minutes>] [--token <gateway-token>] [--send|--background]
4582
4878
  agent-knock-knock list [--store-dir <dir>] [--agent ${agentList}] [--status <status>] [--all] [--managed-only] [--no-approval-scan] [--terminal-debug]
4583
4879
  agent-knock-knock status --conversation <id> [--store-dir <dir>] [--trace]
4880
+ agent-knock-knock describe --conversation <id> [--store-dir <dir>]
4584
4881
  agent-knock-knock send --conversation <id> --message <text> [--type answer|task|control] [--all-proxy <url>] [--agent-timeout-minutes <minutes>]
4585
4882
  agent-knock-knock approve --conversation <id>
4586
4883
  agent-knock-knock cancel --conversation <id> [--all-proxy <url>]