@scotthuang/agent-knock-knock 0.8.1 → 0.10.0

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.
Files changed (53) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/README.md +45 -17
  3. package/dist/src/agent-session-provider.d.ts +13 -0
  4. package/dist/src/claude-local-transcript-provider.d.ts +29 -1
  5. package/dist/src/claude-local-transcript-provider.js +313 -0
  6. package/dist/src/claude-local-transcript-provider.js.map +1 -1
  7. package/dist/src/claude-terminal-agent-adapter.d.ts +6 -1
  8. package/dist/src/claude-terminal-agent-adapter.js +178 -0
  9. package/dist/src/claude-terminal-agent-adapter.js.map +1 -1
  10. package/dist/src/cli.js +5671 -317
  11. package/dist/src/cli.js.map +1 -1
  12. package/dist/src/codex-local-session-provider.d.ts +3 -1
  13. package/dist/src/codex-local-session-provider.js +17 -0
  14. package/dist/src/codex-local-session-provider.js.map +1 -1
  15. package/dist/src/codex-store-adapter.d.ts +26 -1
  16. package/dist/src/codex-store-adapter.js +575 -2
  17. package/dist/src/codex-store-adapter.js.map +1 -1
  18. package/dist/src/codex-terminal-agent-adapter.d.ts +6 -1
  19. package/dist/src/codex-terminal-agent-adapter.js +192 -0
  20. package/dist/src/codex-terminal-agent-adapter.js.map +1 -1
  21. package/dist/src/managed-session.d.ts +163 -0
  22. package/dist/src/managed-session.js +873 -0
  23. package/dist/src/managed-session.js.map +1 -0
  24. package/dist/src/native-thread-lifecycle-policy.d.ts +57 -0
  25. package/dist/src/native-thread-lifecycle-policy.js +116 -0
  26. package/dist/src/native-thread-lifecycle-policy.js.map +1 -0
  27. package/dist/src/openclaw-plugin-helpers.d.ts +45 -12
  28. package/dist/src/openclaw-plugin-helpers.js +386 -47
  29. package/dist/src/openclaw-plugin-helpers.js.map +1 -1
  30. package/dist/src/openclaw-plugin.js +814 -144
  31. package/dist/src/openclaw-plugin.js.map +1 -1
  32. package/dist/src/protocol.d.ts +28 -1
  33. package/dist/src/protocol.js +93 -4
  34. package/dist/src/protocol.js.map +1 -1
  35. package/dist/src/session-store.d.ts +57 -0
  36. package/dist/src/session-store.js +529 -0
  37. package/dist/src/session-store.js.map +1 -0
  38. package/dist/src/store.d.ts +5 -3
  39. package/dist/src/store.js +422 -17
  40. package/dist/src/store.js.map +1 -1
  41. package/dist/src/terminal-agent-adapter.d.ts +175 -0
  42. package/dist/src/terminal-agent-adapter.js +12 -0
  43. package/dist/src/terminal-agent-adapter.js.map +1 -1
  44. package/dist/src/terminal-agent-bridge.d.ts +9 -0
  45. package/dist/src/terminal-agent-bridge.js +17 -1
  46. package/dist/src/terminal-agent-bridge.js.map +1 -1
  47. package/dist/src/terminal-control-provider.d.ts +2 -0
  48. package/dist/src/terminal-control-provider.js +1 -0
  49. package/dist/src/terminal-control-provider.js.map +1 -1
  50. package/docs/quickstart-tmux.md +23 -2
  51. package/openclaw.plugin.json +19 -3
  52. package/package.json +1 -1
  53. package/templates/openclaw-skills/agent-knock-knock/SKILL.md +42 -17
package/CHANGELOG.md CHANGED
@@ -1,5 +1,47 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.10.0 - 2026-08-06
4
+
5
+ ### Added
6
+
7
+ - Add capability-scoped native-thread lifecycle controls for starting or clearing context, listing verified same-workspace resume candidates, and resuming one exact Codex or Claude Code native thread in the existing tmux pane.
8
+ - Add the optional OpenClaw tools `agent_knock_knock_new_thread`, `agent_knock_knock_list_resumable_threads`, and `agent_knock_knock_resume_thread`, plus `/akk new-thread`, `/akk clear-thread`, `/akk threads`, and `/akk resume-thread` human-facing commands.
9
+ - Publish the v5 list/action contract with exact terminal lifecycle targets, complete native-thread identities, fresh compare-and-swap binding tokens, per-candidate evidence fingerprints, and capability-gated availability.
10
+
11
+ ### Changed
12
+
13
+ - Keep ordinary sends scoped to the current native context: each accepted send creates a new Turn, while a successful native lifecycle transition creates or activates an AKK Session and creates no Turn.
14
+ - Treat a sole historical Session whose recorded coding-agent PID has conclusively exited as resumable on the next lifecycle listing; the resume mutation compare-and-swap detaches that stale binding before terminal input, without background polling.
15
+ - Let human-facing lifecycle slash commands fetch a fresh binding token internally immediately before mutation; plugin tools retain the explicit token so OpenClaw follows an auditable list-then-mutate flow.
16
+ - Record terminal binding generations and native-thread transition lineage so sends after new/clear or resume target only the newly verified context.
17
+ - Upgrade the Store writer protocol to v3. The first mutation of a v1 or v2 predecessor durably materializes authoritative Session records before publishing the new manifest, quarantines ambiguous bindings, and leaves existing Turn state and event logs unchanged.
18
+
19
+ ### Security
20
+
21
+ - Serialize native-thread transitions against sends, approvals, monitors, callbacks, and recovery; require an exact idle terminal with no unresolved Turn and fail closed on unsupported, ambiguous, stale, active-elsewhere, or unverifiable native identity evidence.
22
+ - Fence old monitors, receipts, approvals, callbacks, and recovery mutations to their original terminal incarnation, native thread, AKK Session, Turn, and binding generation.
23
+ - Reject every first-line native slash command at the ordinary send/respond boundary before creating Session, Turn, ledger, or terminal side effects, including lifecycle-changing `/clear`, `/new`, `/resume`, Codex `/fork`/side threads, and Claude `/branch`.
24
+
25
+ ## 0.9.0 - 2026-08-05
26
+
27
+ ### Added
28
+
29
+ - Add a durable AKK `session_id` for the continuing native coding-agent context and a unique `turn_id` for every accepted terminal dispatch.
30
+ - Add an explicit `respond(turn_id, answer)` path for questions and blocked requests that must continue the same in-flight turn.
31
+
32
+ ### Changed
33
+
34
+ - Make ordinary sends to an existing AKK Session target its `session_id` and create a new Turn, while managed status, approval, cancellation, retry, renewal, and close operations target an exact `turn_id`.
35
+ - Keep first attach and raw-terminal control compatibility narrow and list-driven: an unmanaged row may prefill its own `selector` for initial send or `conversation_id` for an advertised raw status, approval, cancellation, or orphan-close action; callers must never construct, guess, or reuse either value.
36
+ - Publish the v4 list/action contract with terminal → session → turn history, and include both identities in messages, callbacks, delivery ledgers, and recovery output.
37
+ - Treat existing `conversation_id` values as legacy Store aliases; new records keep `conversation_id` equal to `turn_id`, while legacy records receive in-memory identity fallbacks.
38
+ - Upgrade the Store writer protocol to v2 and atomically migrate exact v1 manifests on the first mutation while preserving legacy Turn records.
39
+
40
+ ### Security
41
+
42
+ - Fail closed when native Codex or Claude Code session evidence is unavailable or changes, when persisted identity fields conflict, or when callback identity sources disagree.
43
+ - Fence terminal receipts and late callbacks to the exact Store, Session, Turn, message, and native process incarnation so stale work cannot cross execution boundaries.
44
+
3
45
  ## 0.8.1 - 2026-08-03
4
46
 
5
47
  ### Fixed
package/README.md CHANGED
@@ -53,7 +53,7 @@ The second command proves that AKK can find the one eligible idle pane, revalida
53
53
 
54
54
  **Delegate from anywhere.** Use any configured OpenClaw channel to hand work to a local coding agent while you are away from your computer. AKK reports when the agent needs attention or finishes, and you can continue from chat or the shared terminal.
55
55
 
56
- **Orchestrate specialist agents.** OpenClaw can coordinate handoffs: Claude Code can plan, Codex can implement, and Claude Code can review. At any point, you can take over the live terminal, keep working yourself, then hand the same task back to OpenClaw with its context intact.
56
+ **Orchestrate specialist agents.** OpenClaw can coordinate handoffs: Claude Code can plan, Codex can implement, and Claude Code can review. At any point, you can take over the live terminal, keep working yourself, then hand the same native session back to OpenClaw with its context intact.
57
57
 
58
58
  ![Agent Knock Knock cover: OpenClaw knocking on coding agents' door](docs/assets/agent-knock-knock-cover.jpg)
59
59
 
@@ -61,14 +61,33 @@ The second command proves that AKK can find the one eligible idle pane, revalida
61
61
 
62
62
  AKK connects OpenClaw to Codex or Claude Code already running inside tmux:
63
63
 
64
- 1. OpenClaw sends a task or follow-up through the AKK plugin.
65
- 2. AKK finds an eligible agent pane and writes only the user-facing task into that terminal.
66
- 3. AKK monitors the same pane for reliable approval, completion, cancellation, and failure evidence.
67
- 4. AKK reports the result to the originating OpenClaw conversation.
68
- 5. A human can attach to the same tmux session at any time and continue directly.
64
+ 1. OpenClaw selects an AKK session and sends the next user-facing request.
65
+ 2. AKK verifies the bound agent pane, creates a new Turn, and writes only that request into the terminal.
66
+ 3. AKK monitors the same pane for reliable approval, completion, cancellation, and failure evidence correlated to that Turn.
67
+ 4. AKK reports the result, `session_id`, and `turn_id` to the originating OpenClaw conversation.
68
+ 5. A human can attach to the same tmux terminal at any time and continue directly.
69
69
 
70
70
  AKK is local-first. It has no hosted control plane or telemetry and does not change the coding agent's configured permission mode.
71
71
 
72
+ ### Terminal, native session, AKK session, and Turn
73
+
74
+ AKK keeps four identities separate:
75
+
76
+ ```text
77
+ tmux terminal / process incarnation
78
+ └─ native Codex or Claude Code session
79
+ └─ AKK session (session_id)
80
+ ├─ Turn 1 (turn_id)
81
+ ├─ Turn 2 (turn_id)
82
+ └─ Turn 3 (turn_id)
83
+ ```
84
+
85
+ Once an AKK session exists, an ordinary `send(session_id, request)` creates a new `turn_id` while preserving the native coding-agent context. On first attach only, `send` may instead use a discovery selector explicitly named by the user, or the exact `selector` prefilled by an unmanaged raw-terminal row. AKK binds the verified native context to an AKK session before accepting the new Turn. Never infer a selector, copy one from another row, or pass one as `session_id`. The `turn_id` is not a destination for later ordinary sends; it is the exact identity used for status, approval, cancellation, renewal, callback retry, close, and callback correlation. If a Turn is `waiting_for_openclaw`, `respond(turn_id, answer)` supplies the answer inside that same Turn instead of creating another one.
86
+
87
+ Human-friendly selectors such as `only`, `codex`, `claude`, a terminal ID, or `@short-ref` remain a discovery layer. A natural-language tool call may preserve one only when the user explicitly named it; otherwise use the exact selector returned by `AKK list`, or omit it and require a unique eligible pane. When an AKK session already exists, `AKK list` pre-fills its authoritative `session_id` for send and the exact `turn_id` for managed controls. The same raw terminal row may advertise status, approval, cancellation, or orphan-close with its own prefilled `conversation_id` compatibility selector. Never infer, guess, or reuse compatibility selectors.
88
+
89
+ Native clear/new/resume operations are explicit lifecycle actions, separate from ordinary Turn creation. A successful new/clear creates a new native thread and AKK Session; resume activates the exact historical native thread and its corresponding Session. Each successful lifecycle transition creates no Turn. The next ordinary send creates the first Turn in the selected context. AKK serializes the transition, verifies the resulting native identity, and advances the terminal binding generation so work and callbacks from the previous context cannot cross the boundary.
90
+
72
91
  ## Optional: Natural-Language Delegation
73
92
 
74
93
  The quick start uses direct `/akk ...` commands, which bypass the model and work without plugin tool access. To let OpenClaw decide to use AKK from a natural-language request, grant the optional `agent-knock-knock` tools in the applicable tool policy.
@@ -193,17 +212,26 @@ The core command surface is intentionally small:
193
212
  /akk <task>
194
213
  /akk <selector>: <message>
195
214
  /akk list
215
+ /akk threads <exact-terminal-id>
216
+ /akk new-thread <exact-terminal-id>
217
+ /akk clear-thread <exact-terminal-id>
218
+ /akk resume-thread <exact-terminal-id> [native-thread-uuid]
196
219
  /akk status [only|latest|codex|claude|@short-ref]
197
- /akk cancel <session-selector>
220
+ /akk respond <turn-selector>: <answer>
221
+ /akk cancel <turn-selector>
198
222
  ```
199
223
 
200
224
  `/akk list` performs a controlled reconciliation across managed turns, and `/akk status` limits reconciliation to the selected turn. This can close records whose idle retention has elapsed and restore eligible missing monitors, but it does not send terminal input or retry callback delivery. Standalone shell queries are read-only unless `--reconcile` is explicitly passed, and resolving a selector never changes turn state.
201
225
 
202
- Selectors fail closed: `only` works only with one actionable target, `latest` requires a unique newest target, and `codex` or `claude` must identify exactly one eligible pane. `AKK list` shows stable short references while JSON output retains the authoritative full IDs. Before every terminal operation, AKK revalidates the expected agent PID and tmux pane identity, then confirms that the process and pane working directories still match; every send also revalidates the idle prompt immediately before typing.
226
+ Selectors fail closed: `only` works only with one actionable target, `latest` requires a unique newest target, and `codex` or `claude` must identify exactly one eligible pane. These names and `@short-ref` are human-facing resolution inputs; a natural-language tool call may preserve one explicitly named by the user, but must not infer one. Managed JSON actions contain the authoritative full `session_id` or `turn_id`. For first attach, an unmanaged raw-terminal row's send action may instead contain its own prefilled `selector`; its advertised raw controls may contain that row's prefilled `conversation_id`. Neither compatibility selector may be guessed, copied from another row, or passed in an authoritative ID field. Before every terminal operation, AKK revalidates the expected agent PID and tmux pane identity, then confirms that the process and pane working directories still match; every send also revalidates the idle prompt immediately before typing.
227
+
228
+ To change native context, first copy the full `terminal_id` from `/akk list`; lifecycle commands do not accept `@short-ref` or loose agent selectors. `/akk threads <exact-terminal-id>` lists exact, same-workspace resume candidates. `/akk resume-thread <exact-terminal-id>` without a UUID shows the same candidates and asks you to choose; with a complete returned UUID it performs the transition. `/akk new-thread` and its human alias `/akk clear-thread` start a clean context. For these slash forms, AKK reads a fresh lifecycle snapshot and immediately supplies its compare-and-swap binding token internally, so you do not copy the token yourself. AKK does not poll bindings in the background: if a recorded owner process exits, the next lifecycle listing can classify that sole historical binding as resumable, and the resume mutation compare-and-swap detaches it before touching the terminal. Live, stale-token, unsupported, busy, ambiguous, active-elsewhere, or unverifiable transitions fail closed. Do not send `/clear`, `/new`, `/resume`, `/status`, Codex `/fork`, `/side`, or `/btw`, Claude `/branch`, or any other first-line native slash command as an ordinary task or answer; use an advertised AKK action, express the request in natural language, or enter an unsupported native command manually in tmux.
229
+
230
+ For natural-language tool use, `agent_knock_knock_list` is terminal-first. Each live pane appears exactly once in `terminals[]`; `process_state` reports whether its coding-agent process is alive and `activity_state` reports the parsed screen state. `managed.session_id` identifies the continuing AKK session, `managed.current_turn` is its optional active Turn, and `managed.recent_turn` is retained history; retained Turns do not occupy the terminal. Pass `all=true` to include older entries in `managed.history`. By default, `unavailable_managed_turns[]` contains attention-needed records whose pane cannot be presented as a live terminal; `all=true` also includes retained unavailable history.
203
231
 
204
- For natural-language tool use, `agent_knock_knock_list` is terminal-first. Each live pane appears exactly once in `terminals[]`; `process_state` reports whether its coding-agent process is alive and `activity_state` reports the parsed screen state. Its optional `managed.current_turn` is the authoritative active AKK turn, while `managed.recent_turn` is retained history and does not occupy the terminal. Pass `all=true` to include older entries in `managed.history`. By default, `unavailable_managed_turns[]` contains attention-needed records whose pane cannot be presented as a live terminal; `all=true` also includes retained unavailable history.
232
+ Use only an `available_actions` entry returned in that snapshot, begin with its prefilled authoritative arguments, and supply every `missing_required` field. A managed Session's `send` uses its prefilled `session_id` and creates a new Turn. For first attach only, use a discovery selector explicitly named by the user or the unmanaged raw-terminal row's prefilled `selector`; do not infer or reuse one. `respond` is available only while a Turn is `waiting_for_openclaw`; it uses `turn_id` and keeps the answer inside that Turn. Managed status, approval, cancellation, renewal, callback retry, and close also use the exact `turn_id`. A raw terminal may be controlled only through the exact status, approval, cancellation, or orphan-close action that its own row advertises with a prefilled `conversation_id`; never construct or guess one. For an ordinary send, add only `request`—`timeoutSeconds` is unsupported, and monitoring limits should be omitted unless the user explicitly asks to change them. AKK revalidates availability before every side effect.
205
233
 
206
- Use only an `available_actions` entry returned in that snapshot, begin with its prefilled authoritative arguments, and supply every `missing_required` field. A terminal's `send` action starts a new managed turn; a managed turn's `follow_up` action continues that exact turn through the same `agent_knock_knock_send` tool. Status, approval, cancellation, renewal, callback retry, and close use `conversation_id`. For an ordinary send or follow-up, add only `request`—`timeoutSeconds` is unsupported, and monitoring limits should be omitted unless the user explicitly asks to change them. AKK revalidates availability before every side effect.
234
+ The top-level v5 `action_contracts` adds `list_resumable_threads`, `new_thread`, and `resume_thread`. The terminal row advertises `list_resumable_threads` and, when currently safe, `new_thread`. Listing is read-only, takes only the full `terminal_id`, and returns a fresh `expected_binding_token` plus candidate rows; each `resumable=true` candidate row advertises its own `resume_thread` action. The `new_thread` and `resume_thread` mutations require that fresh token, and resume additionally requires the candidate's complete `native_thread_id` and opaque `candidate_token`. Never construct, guess, truncate, combine across snapshots, or reuse those values after another terminal action. A lifecycle result contains Session and native-thread identities but no `turn_id` because no work was sent.
207
235
 
208
236
  Workspace is not a routing boundary. AKK can list, inspect, and control verified panes across projects; when more than one target matches, use a selector to choose one explicitly.
209
237
 
@@ -215,7 +243,7 @@ AKK works without project-specific plugin configuration. It reads these optional
215
243
 
216
244
  | Option | Default | Purpose |
217
245
  | --- | --- | --- |
218
- | `storeDir` | `~/.agent-knock-knock/store` | Stable Store root for the compatibility manifest and managed conversations. |
246
+ | `storeDir` | `~/.agent-knock-knock/store` | Stable Store root for the compatibility manifest, authoritative managed Sessions, and Turn records. |
219
247
  | `openclawBin` | Auto-detected | OpenClaw CLI used for callback delivery. |
220
248
  | `codexHome` | Auto-detected | Optional Codex home used to identify Codex sessions running in tmux. |
221
249
  | `idleTimeoutMinutes` | `10080` | Idle retention checked during controlled reconciliation. |
@@ -275,9 +303,9 @@ With the global npm CLI installed, start with `agent-knock-knock doctor`. It run
275
303
  | No eligible terminal is available | Start Codex or Claude Code inside tmux as the same OS user, then run `AKK list`. |
276
304
  | The npm installer or callbacks cannot find a local OpenClaw CLI | Set `openclawBin` and pass `--openclaw-bin` to `install-openclaw`. |
277
305
  | Source changes do not appear | Build, reinstall from the checkout, and restart the Gateway. |
278
- | Terminal task is `stalled` | Inspect `status` and the terminal; use `/akk renew only <minutes>` only when exactly one live stalled task needs more monitoring time. |
279
- | Task is `callback_failed` | Run `/akk retry-callback only` when it is the only actionable failed callback, or use its `@short-ref`. |
280
- | `AKK list` reports an orphaned terminal dispatch | Inspect the named pane first, then run the exact `/akk close ... --expected-message-id ...` recovery command returned by `list`. AKK leaves the coding agent and tmux pane running. |
306
+ | Terminal Turn is `stalled` | Inspect `status` and the terminal; use `/akk renew only <minutes>` only when exactly one live stalled Turn needs more monitoring time. |
307
+ | Turn is `callback_failed` | Run `/akk retry-callback only` when it is the only actionable failed callback, or use its `@short-ref`. |
308
+ | `AKK list` reports an orphaned terminal dispatch or lifecycle transition | Inspect the named pane first, then run the exact `/akk close ...` recovery command returned by `list`. It contains exactly one fresh `--expected-message-id ...` or `--expected-transition-id ...` fence; do not construct, substitute, or reuse it. AKK leaves the coding agent and tmux pane running. |
281
309
  | Claude permission is not offered through AKK | Resolve unsupported dialogs in the terminal. The AKK path requires the exact supported one-time Bash prompt for the current managed turn. |
282
310
  | Claude request was not auto-approved | Check `autoApprove.enabled`, the agent, the rule's canonical `workspaces`, and the exact command vector. The request must also have matching current screen and local transcript evidence. |
283
311
  | Claude monitor becomes `stalled` | Check the Claude version and `status`, then inspect the terminal. Unknown transcript schemas, background work, identity changes, and ambiguous turns intentionally fail closed. |
@@ -319,11 +347,11 @@ gh workflow run clawhub-publish.yml --ref vX.Y.Z -f dry_run=false
319
347
 
320
348
  ## Storage and Logs
321
349
 
322
- Managed state now lives in the stable `~/.agent-knock-knock/store` root. Its manifest prevents an incompatible AKK writer from changing task state. Directories use mode `0700`; state and log files use `0600`.
350
+ Managed state now lives in the stable `~/.agent-knock-knock/store` root. Its manifest prevents an incompatible AKK writer from changing authoritative Session or Turn state. Directories use mode `0700`; state and log files use `0600`.
323
351
 
324
- The manifest checks storage format and writer behavior separately. An unknown `format_version` is not read. When the format is readable but `writer_protocol` differs, normal queries remain available, explicit reconciliation reports `skipped`, and every mutation fails closed before terminal or Gateway side effects.
352
+ The manifest checks storage format and writer behavior separately. An unknown `format_version` is not read. The current writer protocol is 3, and writer protocols 1 and 2 are its supported predecessors: inspection reports either as `upgradeable`. Before the first mutation publishes a protocol-3 manifest, AKK validates the predecessor Turn records, deterministically derives and durably materializes authoritative Session records, and quarantines ambiguous Session bindings instead of routing through them. Existing Turn state and event logs remain unchanged, and the manifest's `created_at` is preserved. Any other writer-protocol mismatch remains readable for normal queries, while explicit reconciliation reports `skipped` and every mutation fails closed before terminal or Gateway side effects.
325
353
 
326
- The former `~/.agent-knock-knock/conversations` directory is left untouched; AKK does not read or migrate it. Existing Codex and Claude Code tmux panes remain available through live discovery, while their old managed-turn IDs, callback associations, and follow-up records are not carried into the new Store. Compatible future upgrades continue using the stable Store rather than creating a directory per package version.
354
+ The former `~/.agent-knock-knock/conversations` directory is left untouched; AKK does not read or migrate it. Existing Codex and Claude Code tmux panes remain available through live discovery, while their old managed-turn IDs, callback associations, and legacy conversation aliases are not carried into the new Store. Compatible future upgrades continue using the stable Store rather than creating a directory per package version.
327
355
 
328
356
  Runtime logs redact common secrets and default to 14-day retention. Configure storage and logging with `--store-dir`, `AKK_LOG_DIR`, `AKK_LOG_LEVEL`, and `AKK_LOG_RETENTION_DAYS`; use a dedicated custom log directory.
329
357
 
@@ -14,11 +14,24 @@ export interface AgentSessionCapabilities {
14
14
  export interface ForkContextOptions extends RolloutExcerptOptions {
15
15
  sessionId: string;
16
16
  }
17
+ export interface ActiveAgentSessionIdentity {
18
+ sessionId: string;
19
+ processUuid?: string;
20
+ processBirth?: string;
21
+ rollout?: {
22
+ fd: string;
23
+ device: string;
24
+ inode: string;
25
+ path: string;
26
+ };
27
+ evidence: string;
28
+ }
17
29
  export interface CodingAgentSessionProvider {
18
30
  agent: CodingAgentSessionProviderAgent;
19
31
  getCapabilities(): Promise<AgentSessionCapabilities>;
20
32
  listHistoricalSessions(): Promise<CodexSessionSummary[]>;
21
33
  listActiveSessions(): Promise<ActiveCodexProcess[]>;
34
+ resolveActiveSessionIdentityForPid(pid: number, cwd?: string, preferredSessionId?: string, allowedCompanionIdentity?: ActiveAgentSessionIdentity, allowedAdditionalIdentities?: readonly ActiveAgentSessionIdentity[]): Promise<ActiveAgentSessionIdentity | undefined>;
22
35
  getSession(sessionId: string): Promise<CodexSessionSummary | undefined>;
23
36
  getForkContext(options: ForkContextOptions): Promise<ForkContextPackage | undefined>;
24
37
  }
@@ -1,5 +1,5 @@
1
1
  import type { ClaudeAgentRow } from "./claude-terminal-agent-adapter.js";
2
- import type { TerminalCompletionEvidence, TerminalDurableCompletionRequest } from "./terminal-agent-adapter.js";
2
+ import type { TerminalCompletionEvidence, TerminalDurableCompletionRequest, TerminalThreadLifecycleCandidate, TerminalThreadLifecycleCandidateProvider, TerminalThreadLifecycleCandidateRequest, TerminalThreadLifecycleCandidateToken, TerminalThreadLifecycleCandidateValidation, TerminalThreadFileToken } from "./terminal-agent-adapter.js";
3
3
  export interface ClaudeTranscriptAnchor {
4
4
  schema_version: 1;
5
5
  session_id: string;
@@ -26,6 +26,17 @@ export interface DetectClaudeTranscriptCompletionOptions {
26
26
  agentRows: readonly ClaudeAgentRow[];
27
27
  maxTurnBytes?: number;
28
28
  }
29
+ export interface ClaudeHistoricalSessionSummary {
30
+ id: string;
31
+ cwd: string;
32
+ transcriptPath: string;
33
+ updatedAtMs: number;
34
+ claudeVersion: string;
35
+ rootInteractive: true;
36
+ fileToken: TerminalThreadFileToken;
37
+ metadataFingerprint: string;
38
+ candidateToken: TerminalThreadLifecycleCandidateToken;
39
+ }
29
40
  /**
30
41
  * Process-local evidence for one pending, foreground Claude Bash permission.
31
42
  * `command` is intentionally raw so the caller can apply the exact command
@@ -47,6 +58,23 @@ export interface ClaudeTranscriptPendingApprovalEvidence {
47
58
  observedEndOffsetBytes: number;
48
59
  }
49
60
  export declare function defaultClaudeHome(): string;
61
+ /** List exact, root-interactive Claude sessions from owner-private transcripts. */
62
+ export declare function listClaudeHistoricalSessions(options: {
63
+ cwd: string;
64
+ claudeHome?: string;
65
+ agentVersion?: string;
66
+ }): ClaudeHistoricalSessionSummary[];
67
+ export declare function listClaudeThreadLifecycleCandidates(options: {
68
+ cwd: string;
69
+ claudeHome?: string;
70
+ agentVersion: string;
71
+ }): TerminalThreadLifecycleCandidate[];
72
+ export declare function createClaudeThreadLifecycleCandidateProvider(options?: {
73
+ claudeHome?: string;
74
+ }): TerminalThreadLifecycleCandidateProvider;
75
+ export declare function revalidateClaudeThreadLifecycleCandidate(candidate: TerminalThreadLifecycleCandidate | TerminalThreadLifecycleCandidateToken, options: TerminalThreadLifecycleCandidateRequest & {
76
+ claudeHome?: string;
77
+ }): TerminalThreadLifecycleCandidateValidation;
50
78
  /**
51
79
  * Records the immutable file boundary immediately before AKK submits a turn.
52
80
  * No transcript contents are retained in conversation state.
@@ -11,10 +11,323 @@ const CLAUDE_TRANSCRIPT_VERSION_PATTERN = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]
11
11
  const NO_FOLLOW_FLAG = typeof fs.constants.O_NOFOLLOW === "number"
12
12
  ? fs.constants.O_NOFOLLOW
13
13
  : 0;
14
+ const CLAUDE_LIFECYCLE_VERSION = "2.1.218";
15
+ const CLAUDE_HISTORICAL_METADATA_MAX_BYTES = 1024 * 1024;
14
16
  export function defaultClaudeHome() {
15
17
  const configured = process.env.CLAUDE_CONFIG_DIR?.trim();
16
18
  return configured || path.join(os.homedir(), ".claude");
17
19
  }
20
+ /** List exact, root-interactive Claude sessions from owner-private transcripts. */
21
+ export function listClaudeHistoricalSessions(options) {
22
+ if (!options.cwd || !path.isAbsolute(options.cwd)) {
23
+ throw new Error("Claude lifecycle candidate discovery requires an absolute cwd");
24
+ }
25
+ const cwd = path.resolve(options.cwd);
26
+ const claudeHome = path.resolve(options.claudeHome ?? defaultClaudeHome());
27
+ const agentVersion = options.agentVersion ?? CLAUDE_LIFECYCLE_VERSION;
28
+ if (agentVersion !== CLAUDE_LIFECYCLE_VERSION) {
29
+ throw new Error(`Claude lifecycle candidates require exact version ${CLAUDE_LIFECYCLE_VERSION}`);
30
+ }
31
+ const projectsRoot = projectsRootPath(claudeHome);
32
+ if (!isRealDirectory(projectsRoot)) {
33
+ return [];
34
+ }
35
+ const projectRelative = path.dirname(expectedTranscriptRelativePath("00000000-0000-0000-0000-000000000000", cwd));
36
+ const projectDirectory = path.join(projectsRoot, projectRelative);
37
+ if (!isRealDirectory(projectDirectory)) {
38
+ return [];
39
+ }
40
+ return fs.readdirSync(projectDirectory, { withFileTypes: true })
41
+ .flatMap((entry) => {
42
+ if (!entry.isFile() || entry.isSymbolicLink()) {
43
+ return [];
44
+ }
45
+ const match = /^([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl$/iu.exec(entry.name);
46
+ if (!match) {
47
+ return [];
48
+ }
49
+ try {
50
+ const summary = inspectClaudeHistoricalSession({
51
+ projectsRoot,
52
+ cwd,
53
+ sessionId: match[1].toLowerCase(),
54
+ agentVersion
55
+ });
56
+ return summary ? [summary] : [];
57
+ }
58
+ catch {
59
+ // Unsafe, malformed, or unstable transcript files are not candidates.
60
+ return [];
61
+ }
62
+ })
63
+ .sort((left, right) => right.updatedAtMs - left.updatedAtMs);
64
+ }
65
+ export function listClaudeThreadLifecycleCandidates(options) {
66
+ return listClaudeHistoricalSessions(options).map((session) => ({
67
+ agent: "claude",
68
+ nativeThreadId: session.id,
69
+ cwd: session.cwd,
70
+ source: "claude_transcript",
71
+ rootInteractive: true,
72
+ fileToken: session.fileToken,
73
+ agentVersion: session.claudeVersion,
74
+ updatedAtMs: session.updatedAtMs,
75
+ metadataFingerprint: session.metadataFingerprint,
76
+ candidateToken: session.candidateToken
77
+ }));
78
+ }
79
+ export function createClaudeThreadLifecycleCandidateProvider(options = {}) {
80
+ return {
81
+ async listThreadLifecycleCandidates(request) {
82
+ return listClaudeThreadLifecycleCandidates({
83
+ cwd: request.cwd,
84
+ agentVersion: request.agentVersion,
85
+ claudeHome: options.claudeHome
86
+ });
87
+ },
88
+ async revalidateThreadLifecycleCandidate(candidate, request) {
89
+ return revalidateClaudeThreadLifecycleCandidate(candidate, {
90
+ ...request,
91
+ claudeHome: options.claudeHome
92
+ });
93
+ }
94
+ };
95
+ }
96
+ export function revalidateClaudeThreadLifecycleCandidate(candidate, options) {
97
+ try {
98
+ const token = "candidateToken" in candidate
99
+ ? candidate.candidateToken
100
+ : candidate;
101
+ if (token.schema !== "agent-knock-knock/thread-candidate-token" ||
102
+ token.version !== 1 ||
103
+ token.agent !== "claude" ||
104
+ token.source !== "claude_transcript" ||
105
+ token.agentVersion !== options.agentVersion ||
106
+ !path.isAbsolute(options.cwd) ||
107
+ !path.isAbsolute(token.cwd) ||
108
+ path.resolve(token.cwd) !== path.resolve(options.cwd) ||
109
+ !CLAUDE_SESSION_ID_PATTERN.test(token.nativeThreadId) ||
110
+ options.agentVersion !== CLAUDE_LIFECYCLE_VERSION) {
111
+ return {
112
+ status: "unsafe",
113
+ reason: "candidate is not an exact Claude root-interactive session"
114
+ };
115
+ }
116
+ const claudeHome = path.resolve(options.claudeHome ?? defaultClaudeHome());
117
+ const summary = inspectClaudeHistoricalSession({
118
+ projectsRoot: projectsRootPath(claudeHome),
119
+ cwd: path.resolve(options.cwd),
120
+ sessionId: token.nativeThreadId.toLowerCase(),
121
+ agentVersion: options.agentVersion
122
+ });
123
+ if (!summary) {
124
+ return {
125
+ status: "unavailable",
126
+ reason: "the Claude transcript no longer exists"
127
+ };
128
+ }
129
+ const current = {
130
+ agent: "claude",
131
+ nativeThreadId: summary.id,
132
+ cwd: summary.cwd,
133
+ source: "claude_transcript",
134
+ rootInteractive: true,
135
+ fileToken: summary.fileToken,
136
+ agentVersion: summary.claudeVersion,
137
+ updatedAtMs: summary.updatedAtMs,
138
+ metadataFingerprint: summary.metadataFingerprint,
139
+ candidateToken: summary.candidateToken
140
+ };
141
+ if (!sameClaudeThreadFileToken(token.fileToken, current.fileToken) ||
142
+ token.metadataFingerprint !== current.metadataFingerprint) {
143
+ return {
144
+ status: "changed",
145
+ candidate: current,
146
+ reason: "the Claude transcript changed after candidate discovery"
147
+ };
148
+ }
149
+ return { status: "valid", candidate: current };
150
+ }
151
+ catch (error) {
152
+ if (isRecord(error) && error.code === "ENOENT") {
153
+ return {
154
+ status: "unavailable",
155
+ reason: "the Claude transcript no longer exists"
156
+ };
157
+ }
158
+ return {
159
+ status: "unsafe",
160
+ reason: error instanceof Error ? error.message : String(error)
161
+ };
162
+ }
163
+ }
164
+ function inspectClaudeHistoricalSession({ projectsRoot, cwd, sessionId, agentVersion }) {
165
+ if (!isRealDirectory(projectsRoot) ||
166
+ !CLAUDE_SESSION_ID_PATTERN.test(sessionId) ||
167
+ agentVersion !== CLAUDE_LIFECYCLE_VERSION) {
168
+ return undefined;
169
+ }
170
+ const relativePath = expectedTranscriptRelativePath(sessionId, cwd);
171
+ const opened = openRelativeTranscript(projectsRoot, relativePath);
172
+ if (!opened) {
173
+ return undefined;
174
+ }
175
+ try {
176
+ const transcriptPath = fs.realpathSync(path.join(projectsRoot, opened.relativePath));
177
+ const realProjectsRoot = fs.realpathSync(projectsRoot);
178
+ const relative = path.relative(realProjectsRoot, transcriptPath);
179
+ if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) {
180
+ throw new Error("Claude transcript resolves outside the projects directory");
181
+ }
182
+ const metadata = readClaudeHistoricalMetadata(opened.fd, opened.stat.size);
183
+ if (!metadata) {
184
+ return undefined;
185
+ }
186
+ if (metadata.sessionId !== sessionId ||
187
+ !path.isAbsolute(metadata.cwd) ||
188
+ normalizePath(metadata.cwd) !== normalizePath(cwd) ||
189
+ metadata.version !== agentVersion ||
190
+ metadata.isSidechain !== false ||
191
+ metadata.entrypoint !== "cli" ||
192
+ metadata.agentId !== undefined ||
193
+ metadata.teamName !== undefined ||
194
+ metadata.loopSession ||
195
+ ![undefined, "interactive", "main"].includes(metadata.sessionKind)) {
196
+ return undefined;
197
+ }
198
+ const stable = fs.fstatSync(opened.fd);
199
+ if (stable.dev !== opened.stat.dev ||
200
+ stable.ino !== opened.stat.ino ||
201
+ stable.size !== opened.stat.size ||
202
+ stable.mtimeMs !== opened.stat.mtimeMs ||
203
+ fs.realpathSync(path.join(projectsRoot, opened.relativePath)) !==
204
+ transcriptPath) {
205
+ throw new Error("Claude transcript changed while it was inspected");
206
+ }
207
+ const fileToken = {
208
+ path: transcriptPath,
209
+ device: String(stable.dev),
210
+ inode: String(stable.ino),
211
+ size: stable.size,
212
+ mtimeMs: stable.mtimeMs
213
+ };
214
+ const metadataFingerprint = createHash("sha256")
215
+ .update(JSON.stringify({
216
+ sessionId,
217
+ cwd: path.resolve(cwd),
218
+ version: metadata.version,
219
+ entrypoint: metadata.entrypoint,
220
+ sessionKind: metadata.sessionKind ?? null,
221
+ transcriptPath
222
+ }))
223
+ .digest("hex");
224
+ const candidateToken = {
225
+ schema: "agent-knock-knock/thread-candidate-token",
226
+ version: 1,
227
+ agent: "claude",
228
+ nativeThreadId: sessionId,
229
+ cwd: path.resolve(cwd),
230
+ source: "claude_transcript",
231
+ agentVersion: metadata.version,
232
+ fileToken,
233
+ metadataFingerprint
234
+ };
235
+ return {
236
+ id: sessionId,
237
+ cwd: path.resolve(cwd),
238
+ transcriptPath,
239
+ updatedAtMs: stable.mtimeMs,
240
+ claudeVersion: metadata.version,
241
+ rootInteractive: true,
242
+ fileToken,
243
+ metadataFingerprint,
244
+ candidateToken
245
+ };
246
+ }
247
+ finally {
248
+ fs.closeSync(opened.fd);
249
+ }
250
+ }
251
+ function readClaudeHistoricalMetadata(fd, fileSize) {
252
+ if (fileSize <= 0 || !Number.isSafeInteger(fileSize)) {
253
+ return undefined;
254
+ }
255
+ const bytesToRead = Math.min(fileSize, CLAUDE_HISTORICAL_METADATA_MAX_BYTES);
256
+ const buffer = Buffer.allocUnsafe(bytesToRead);
257
+ const bytesRead = fs.readSync(fd, buffer, 0, bytesToRead, 0);
258
+ if (bytesRead !== bytesToRead) {
259
+ throw new Error("Claude transcript changed while metadata was read");
260
+ }
261
+ const text = buffer.subarray(0, bytesRead).toString("utf8");
262
+ const lastCompleteNewline = text.lastIndexOf("\n");
263
+ if (lastCompleteNewline < 0) {
264
+ if (fileSize > bytesRead) {
265
+ throw new Error("Claude transcript metadata line exceeds the read limit");
266
+ }
267
+ return undefined;
268
+ }
269
+ const completeLines = text.slice(0, lastCompleteNewline).split("\n");
270
+ for (const line of completeLines) {
271
+ if (!line.trim()) {
272
+ continue;
273
+ }
274
+ let parsed;
275
+ try {
276
+ parsed = JSON.parse(line);
277
+ }
278
+ catch {
279
+ throw new Error("Claude transcript contains invalid root metadata JSONL");
280
+ }
281
+ if (!isRecord(parsed)) {
282
+ throw new Error("Claude transcript root metadata must be an object");
283
+ }
284
+ const sessionId = nonEmptyString(parsed.sessionId);
285
+ if (!sessionId) {
286
+ continue;
287
+ }
288
+ const cwd = nonEmptyString(parsed.cwd);
289
+ const version = nonEmptyString(parsed.version);
290
+ const entrypoint = nonEmptyString(parsed.entrypoint);
291
+ const hasIdentityMetadata = cwd !== undefined ||
292
+ version !== undefined ||
293
+ entrypoint !== undefined ||
294
+ typeof parsed.isSidechain === "boolean" ||
295
+ nonEmptyString(parsed.agentId) !== undefined ||
296
+ nonEmptyString(parsed.teamName) !== undefined ||
297
+ nonEmptyString(parsed.sessionKind) !== undefined;
298
+ if (!hasIdentityMetadata) {
299
+ // Claude 2.1.218 can prepend mode/permission-mode records carrying only
300
+ // sessionId. Continue to the first full root transcript record.
301
+ continue;
302
+ }
303
+ if (!cwd || !version || !entrypoint || typeof parsed.isSidechain !== "boolean") {
304
+ throw new Error("Claude transcript has incomplete root session metadata");
305
+ }
306
+ const sessionKind = nonEmptyString(parsed.sessionKind);
307
+ return {
308
+ sessionId: sessionId.toLowerCase(),
309
+ cwd,
310
+ version,
311
+ isSidechain: parsed.isSidechain,
312
+ entrypoint,
313
+ agentId: nonEmptyString(parsed.agentId),
314
+ teamName: nonEmptyString(parsed.teamName),
315
+ sessionKind,
316
+ loopSession: parsed.isLoop === true ||
317
+ parsed.isLoopSession === true ||
318
+ parsed.loopSession === true ||
319
+ sessionKind === "loop"
320
+ };
321
+ }
322
+ return undefined;
323
+ }
324
+ function sameClaudeThreadFileToken(left, right) {
325
+ return left.path === right.path &&
326
+ left.device === right.device &&
327
+ left.inode === right.inode &&
328
+ left.size === right.size &&
329
+ left.mtimeMs === right.mtimeMs;
330
+ }
18
331
  /**
19
332
  * Records the immutable file boundary immediately before AKK submits a turn.
20
333
  * No transcript contents are retained in conversation state.