@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.
- package/CHANGELOG.md +42 -0
- package/README.md +45 -17
- package/dist/src/agent-session-provider.d.ts +13 -0
- package/dist/src/claude-local-transcript-provider.d.ts +29 -1
- package/dist/src/claude-local-transcript-provider.js +313 -0
- package/dist/src/claude-local-transcript-provider.js.map +1 -1
- package/dist/src/claude-terminal-agent-adapter.d.ts +6 -1
- package/dist/src/claude-terminal-agent-adapter.js +178 -0
- package/dist/src/claude-terminal-agent-adapter.js.map +1 -1
- package/dist/src/cli.js +5671 -317
- package/dist/src/cli.js.map +1 -1
- package/dist/src/codex-local-session-provider.d.ts +3 -1
- package/dist/src/codex-local-session-provider.js +17 -0
- package/dist/src/codex-local-session-provider.js.map +1 -1
- package/dist/src/codex-store-adapter.d.ts +26 -1
- package/dist/src/codex-store-adapter.js +575 -2
- package/dist/src/codex-store-adapter.js.map +1 -1
- package/dist/src/codex-terminal-agent-adapter.d.ts +6 -1
- package/dist/src/codex-terminal-agent-adapter.js +192 -0
- package/dist/src/codex-terminal-agent-adapter.js.map +1 -1
- package/dist/src/managed-session.d.ts +163 -0
- package/dist/src/managed-session.js +873 -0
- package/dist/src/managed-session.js.map +1 -0
- package/dist/src/native-thread-lifecycle-policy.d.ts +57 -0
- package/dist/src/native-thread-lifecycle-policy.js +116 -0
- package/dist/src/native-thread-lifecycle-policy.js.map +1 -0
- package/dist/src/openclaw-plugin-helpers.d.ts +45 -12
- package/dist/src/openclaw-plugin-helpers.js +386 -47
- package/dist/src/openclaw-plugin-helpers.js.map +1 -1
- package/dist/src/openclaw-plugin.js +814 -144
- package/dist/src/openclaw-plugin.js.map +1 -1
- package/dist/src/protocol.d.ts +28 -1
- package/dist/src/protocol.js +93 -4
- package/dist/src/protocol.js.map +1 -1
- package/dist/src/session-store.d.ts +57 -0
- package/dist/src/session-store.js +529 -0
- package/dist/src/session-store.js.map +1 -0
- package/dist/src/store.d.ts +5 -3
- package/dist/src/store.js +422 -17
- package/dist/src/store.js.map +1 -1
- package/dist/src/terminal-agent-adapter.d.ts +175 -0
- package/dist/src/terminal-agent-adapter.js +12 -0
- package/dist/src/terminal-agent-adapter.js.map +1 -1
- package/dist/src/terminal-agent-bridge.d.ts +9 -0
- package/dist/src/terminal-agent-bridge.js +17 -1
- package/dist/src/terminal-agent-bridge.js.map +1 -1
- package/dist/src/terminal-control-provider.d.ts +2 -0
- package/dist/src/terminal-control-provider.js +1 -0
- package/dist/src/terminal-control-provider.js.map +1 -1
- package/docs/quickstart-tmux.md +23 -2
- package/openclaw.plugin.json +19 -3
- package/package.json +1 -1
- 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
|
|
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
|

|
|
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
|
|
65
|
-
2. AKK
|
|
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
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
279
|
-
|
|
|
280
|
-
| `AKK list` reports an orphaned terminal dispatch | Inspect the named pane first, then run the exact `/akk close
|
|
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
|
|
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.
|
|
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
|
|
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.
|