@scotthuang/agent-knock-knock 0.11.3 → 0.11.4

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,16 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.11.4 - 2026-08-09
4
+
5
+ ### Added
6
+
7
+ - Add an adapter-owned, version-scoped native status inspection for Codex 0.146.0 and 0.146.1, exposed through a closed `native_inspect(status)` action that returns a bounded and redacted fresh `/status` result without creating a Session, Turn, receipt, monitor, callback, or Store state.
8
+
9
+ ### Security
10
+
11
+ - Keep ordinary `send` and `respond` slash-command rejection intact while serializing native inspection with terminal mutations, revalidating the exact terminal, process, binding, version, idle composer, and ownership state, and dispatching at most one Enter only after the versioned 121 ms materialization boundary.
12
+ - Fail closed after identity drift, ambiguous or stale status evidence, and uncertain submission outcomes without blind retries or a second Enter.
13
+
3
14
  ## 0.11.3 - 2026-08-08
4
15
 
5
16
  ### Fixed
package/README.md CHANGED
@@ -87,6 +87,8 @@ Human-friendly selectors such as `only`, `codex`, `claude`, a terminal ID, or `@
87
87
 
88
88
  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.
89
89
 
90
+ AKK status and native status inspection are different operations. `agent_knock_knock_status` (and `/akk status`) reads AKK Turn state plus a bounded current terminal screen; it does not execute the coding agent's `/status`. When an idle terminal row advertises `native_inspect`, `agent_knock_knock_native_inspect` can execute only its prefilled, version-scoped inspection with the exact `terminal_id`, `inspection="status"`, and fresh `expected_binding_token`. Initial support is Codex-only and limited to versions 0.146.0 and 0.146.1. It creates no Session, Turn, receipt, monitor, or callback. Claude native commands, `/usage`, `/model`, `/compact`, arbitrary slash strings, and unsupported Codex versions remain unavailable. In particular, bare Codex `/usage` opens an interactive menu whose later Enter can select an account-side usage-limit reset; do not automate it as a read-only inspection.
91
+
90
92
  ## Optional: Natural-Language Delegation
91
93
 
92
94
  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.
@@ -132,7 +134,7 @@ If you also want standalone shell commands such as `agent-knock-knock doctor`, i
132
134
  npm install -g @scotthuang/agent-knock-knock
133
135
  ```
134
136
 
135
- Standalone `agent-knock-knock list` and `status` are read-only with respect to managed-turn state by default. Passing `--reconcile` explicitly enables controlled reconciliation; OpenClaw does this for `/akk list`, `/akk status`, and the corresponding plugin tools.
137
+ Standalone `agent-knock-knock list` and AKK `status` are read-only with respect to managed-turn state by default. Passing `--reconcile` explicitly enables controlled reconciliation; OpenClaw does this for `/akk list`, `/akk status`, and the corresponding plugin tools. This AKK status path does not run native Codex `/status`; the separately advertised `native_inspect` action owns that bounded terminal input.
136
138
 
137
139
  ### Alternative: Install from npm
138
140
 
@@ -226,11 +228,15 @@ Selectors fail closed: `only` works only with one actionable target, `latest` re
226
228
 
227
229
  To change native context, first copy the full `terminal_id` from `/akk list`; lifecycle commands do not accept an ordinary-send `@short-ref` or loose agent selector. `/akk threads <exact-terminal-id>` lists exact, same-workspace candidates with a deterministic number, a collision-safe display-only `@short-id`, an opaque snapshot handle, and the complete UUID. `/akk resume-thread <exact-terminal-id>` without a selection shows that list. A complete UUID remains compatible. A number or short ID resolves only against the latest list displayed in the same OpenClaw session, while an opaque handle names its exact snapshot; all expire after five minutes and fail after terminal, process, workspace, binding, candidate-set, or relevant action changes. None is ever passed to Codex or Claude Code as native identity: AKK resolves the saved tuple back to its full UUID and fresh evidence tokens first. `previous` (or `刚才那个`) is advertised only when the current Session's latest committed lifecycle transition identifies exactly one currently verified resumable source; it never guesses from title, recency, or static lineage. `/akk new-thread` and its human alias `/akk clear-thread` start a clean context. 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, expired, 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.
228
230
 
231
+ To request the native Codex status card, first run `agent_knock_knock_list` and use only that terminal row's advertised `native_inspect` action. The structured tool schema is closed to `inspection="status"`; callers cannot provide `/status` or another slash command as text. AKK serializes the inspection with terminal mutations, revalidates the fresh token and exact terminal identity, and returns only after it proves one fresh bounded status result and an idle postcondition. It never turns ordinary `send` or `respond` into a slash-command escape hatch.
232
+
233
+ The top-level v7 `action_contracts` documents this closed inspection alongside the existing lifecycle and Turn actions; `available_actions` remains the only current-action authority.
234
+
229
235
  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.
230
236
 
231
- 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.
237
+ 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`. Native inspection instead uses the exact terminal row's `terminal_id`, closed `inspection`, and snapshot-bound `expected_binding_token`; do not substitute AKK status or ordinary send. 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.
232
238
 
233
- The top-level v6 `action_contracts` adds `list_resumable_threads`, `new_thread`, `resume_thread`, and the conflict-only `reconcile_binding` recovery action. The terminal row advertises `list_resumable_threads` and, when currently safe, `new_thread`. Listing is read-only with respect to Session/Turn state, takes only the full `terminal_id`, and returns a fresh `expected_binding_token` plus candidate rows; each `resumable=true` row retains its complete UUID and exact prefilled `resume_thread` action. If `previous` is present, use only its exact prefilled action for a natural-language “刚才那个” request. Numbers, short IDs, and handles are human display/navigation aids, never tool arguments or authoritative native identity. The `new_thread` and `resume_thread` mutations require the fresh token, and resume additionally requires the candidate's complete `native_thread_id` and opaque `candidate_token`. A binding conflict may advertise `reconcile_binding` only when one exact Session claim can be safely CAS-detached using its listed revision, binding token, and live terminal token; this never adopts the replacement thread, sends terminal input, or creates a Turn. 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.
239
+ The top-level action contracts include `native_inspect`, `list_resumable_threads`, `new_thread`, `resume_thread`, and the conflict-only `reconcile_binding` recovery action. A supported idle Codex terminal may advertise `native_inspect` with its exact terminal ID, the closed `status` inspection kind, and a fresh binding token. The terminal row also advertises `list_resumable_threads` and, when currently safe, `new_thread`. Thread listing is read-only with respect to Session/Turn state, takes only the full `terminal_id`, and returns a fresh `expected_binding_token` plus candidate rows; each `resumable=true` row retains its complete UUID and exact prefilled `resume_thread` action. If `previous` is present, use only its exact prefilled action for a natural-language “刚才那个” request. Numbers, short IDs, and handles are human display/navigation aids, never tool arguments or authoritative native identity. The `new_thread` and `resume_thread` mutations require the fresh token, and resume additionally requires the candidate's complete `native_thread_id` and opaque `candidate_token`. A binding conflict may advertise `reconcile_binding` only when one exact Session claim can be safely CAS-detached using its listed revision, binding token, and live terminal token; this never adopts the replacement thread, sends terminal input, or creates a Turn. Never construct, guess, truncate, combine across snapshots, or reuse those values after another terminal action. Native inspection and lifecycle results contain no `turn_id` because no work was sent.
234
240
 
235
241
  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.
236
242
 
@@ -339,10 +345,14 @@ is not currently required for publishing.
339
345
  ```bash
340
346
  npm run build
341
347
  npm run typecheck
342
- npm test
348
+ npm run test:fast
343
349
  ```
344
350
 
345
- See [CONTRIBUTING.md](https://github.com/scotthuang/agent-knock-knock/blob/main/CONTRIBUTING.md) for the development and pull request workflow. For local OpenClaw testing, rebuild, run `node dist/src/cli.js install-openclaw`, and restart the Gateway.
351
+ Use the fast tier for the inner loop, add the integration files mapped to the
352
+ subsystem you changed, and run `npm test` once before opening a pull request.
353
+ `npm test` remains the complete suite. See [Testing](docs/testing.md) for the
354
+ tier manifest, targeted integration map, profiling command, and release gates,
355
+ and [CONTRIBUTING.md](https://github.com/scotthuang/agent-knock-knock/blob/main/CONTRIBUTING.md) for the development and pull request workflow. For local OpenClaw testing, rebuild, run `node dist/src/cli.js install-openclaw`, and restart the Gateway.
346
356
 
347
357
  ### Maintainer Release
348
358
 
package/dist/src/cli.js CHANGED
@@ -24,7 +24,7 @@ import { StaticTerminalControlProvider, TmuxTerminalControlProvider, terminalPan
24
24
  import { parseTerminalConversationId } from "./terminal-agent-adapter.js";
25
25
  import { createProductionTerminalAgentRegistry } from "./terminal-agent-registry.js";
26
26
  import { parseProcessElapsedSeconds, StaticTerminalProcessSource, SystemTerminalProcessSource } from "./terminal-process-source.js";
27
- import { TerminalAgentBridge, TerminalInputNotStartedError } from "./terminal-agent-bridge.js";
27
+ import { NativeInspectionSubmissionError, TerminalAgentBridge, TerminalInputNotStartedError } from "./terminal-agent-bridge.js";
28
28
  import { evaluateApprovalPolicy } from "./approval-policy.js";
29
29
  import { evaluateDoctorCapabilities, runDoctorCapabilityProbes } from "./doctor-capabilities.js";
30
30
  import { runOpenClawChainDiagnostics } from "./openclaw-doctor.js";
@@ -256,6 +256,9 @@ async function runCommand(commandName, options) {
256
256
  else if (commandName === "list-resumable-threads" || commandName === "threads") {
257
257
  await runListResumableThreads(options);
258
258
  }
259
+ else if (commandName === "native-inspect" || commandName === "native-status") {
260
+ await runNativeInspect(options);
261
+ }
259
262
  else if (commandName === "resume-thread") {
260
263
  await runResumeThread(options);
261
264
  }
@@ -2782,6 +2785,13 @@ async function terminalControlledListEntry(session, activeSessions, options, bri
2782
2785
  resumeExact: false,
2783
2786
  reason: "native thread lifecycle is unavailable"
2784
2787
  };
2788
+ const nativeInspectionCapability = bridge.registry.require(session.agent)
2789
+ .probeNativeInspection?.(agentVersion) ?? {
2790
+ status: "unsupported",
2791
+ agentVersion,
2792
+ statusInspection: false,
2793
+ reason: "native inspection is unavailable"
2794
+ };
2785
2795
  const lifecycleBindingToken = unmanagedTerminalBindingToken({
2786
2796
  terminalId: bridge.terminalConversationId(session),
2787
2797
  terminalControl,
@@ -2795,6 +2805,10 @@ async function terminalControlledListEntry(session, activeSessions, options, bri
2795
2805
  });
2796
2806
  const codexLifecycleIncarnationAvailable = session.agent !== "codex" ||
2797
2807
  Boolean(nativeProcessUuid && nativeProcessBirth);
2808
+ const nativeInspectionHasBlockingTurn = listConversations(storeDirFromOptions(options)).some((turn) => isDiscoverableTmuxConversation(turn) &&
2809
+ terminalKeyForManagedConversation(turn) ===
2810
+ terminalControlSelectorKey(terminalControl) &&
2811
+ SESSION_SEND_BLOCKING_STATUSES.has(turn.status));
2798
2812
  const entry = {
2799
2813
  id: bridge.terminalConversationId(session),
2800
2814
  short_ref: sessionShortRef(bridge.terminalConversationId(session)),
@@ -2815,6 +2829,7 @@ async function terminalControlledListEntry(session, activeSessions, options, bri
2815
2829
  native_agent_identity_evidence: nativeProcessEvidence,
2816
2830
  agent_version: agentVersion,
2817
2831
  native_thread_lifecycle: lifecycleCapability,
2832
+ native_inspection: nativeInspectionCapability,
2818
2833
  lifecycle_binding_token: lifecycleBindingToken,
2819
2834
  confidence: session.confidence,
2820
2835
  reason: session.reason,
@@ -2849,7 +2864,16 @@ async function terminalControlledListEntry(session, activeSessions, options, bri
2849
2864
  codexLifecycleIncarnationAvailable,
2850
2865
  list_resumable_threads: lifecycleCapability.status === "supported" &&
2851
2866
  lifecycleCapability.resumeExact === true &&
2852
- codexLifecycleIncarnationAvailable
2867
+ codexLifecycleIncarnationAvailable,
2868
+ native_inspect: nativeInspectionCapability.status === "supported" &&
2869
+ nativeInspectionCapability.statusInspection === true &&
2870
+ session.agent === "codex" &&
2871
+ codexComposerEmpty(terminalState.screen_excerpt) &&
2872
+ terminalControl.capabilities.includes("send_keys") &&
2873
+ terminalControl.capabilities.includes("screen_status") &&
2874
+ codexLifecycleIncarnationAvailable &&
2875
+ orphanedDispatch === undefined &&
2876
+ !nativeInspectionHasBlockingTurn
2853
2877
  }
2854
2878
  };
2855
2879
  const availableActions = availableListActions(entry);
@@ -2973,9 +2997,13 @@ function terminalFirstListProjection({ storeDir, terminals, managedSessions, ses
2973
2997
  const rawActions = mutationsAllowed
2974
2998
  ? discoveredRawActions
2975
2999
  : readOnlyListActions(discoveredRawActions);
2976
- const sessionAwareRawActions = authoritativeSession
3000
+ const bindingAwareRawActions = authoritativeSession
2977
3001
  ? actionsForManagedSessionBinding(rawActions, authoritativeSession)
2978
3002
  : rawActions;
3003
+ const sessionAwareRawActions = authoritativeSession &&
3004
+ managedSessionHasUnresolvedNativeTransition(storeDir, authoritativeSession)
3005
+ ? Object.fromEntries(Object.entries(bindingAwareRawActions).filter(([actionName]) => actionName !== "native_inspect"))
3006
+ : bindingAwareRawActions;
2979
3007
  const soleBindingConflict = conflictingBoundSessionClaims.length === 1
2980
3008
  ? conflictingBoundSessionClaims[0]
2981
3009
  : undefined;
@@ -3595,7 +3623,7 @@ function sendActionForManagedSession(action, sessionId) {
3595
3623
  function actionsForManagedSessionBinding(actions, session) {
3596
3624
  const token = managedSessionBindingToken(session);
3597
3625
  const next = { ...actions };
3598
- for (const actionName of ["new_thread", "resume_thread"]) {
3626
+ for (const actionName of ["new_thread", "resume_thread", "native_inspect"]) {
3599
3627
  const action = isRecord(next[actionName]) ? next[actionName] : undefined;
3600
3628
  if (!action) {
3601
3629
  continue;
@@ -3863,11 +3891,12 @@ function managedListApprovalState(conversation) {
3863
3891
  }
3864
3892
  function listActionContracts() {
3865
3893
  return {
3866
- version: 6,
3894
+ version: 7,
3867
3895
  instructions: [
3868
3896
  "Treat terminals[] as the primary resource and use only actions present in available_actions.",
3869
3897
  "An existing managed session's ordinary send targets session_id and creates a new turn. A turn id is never an ordinary send target.",
3870
3898
  "Read-only native-thread listing targets an exact terminal_id. Native-thread new/resume mutations also use the listed expected_binding_token and never create a Turn.",
3899
+ "Native inspection is a separate terminal action: use only its closed inspection enum and current exact terminal_id/token; AKK status does not execute a native slash command.",
3871
3900
  "A binding conflict may be detached only through its exact reconcile_binding action, which is snapshot-bound to the Session revision, binding, and live terminal identity and never adopts a replacement native thread.",
3872
3901
  "List resumable threads before resume; use only a complete native_thread_id and the action returned for that candidate.",
3873
3902
  "For first attach only, use a discovery selector explicitly named by the user or the selector prefilled by that unmanaged raw-terminal row's available send action; never infer, guess, or reuse one.",
@@ -3934,6 +3963,17 @@ function listActionContracts() {
3934
3963
  required: ["terminal_id"],
3935
3964
  side_effect_free: true
3936
3965
  },
3966
+ native_inspect: {
3967
+ tool: "agent_knock_knock_native_inspect",
3968
+ target_argument: "terminal_id",
3969
+ required: ["terminal_id", "inspection", "expected_binding_token"],
3970
+ supported_inspections: ["status"],
3971
+ creates_turn: false,
3972
+ creates_session: false,
3973
+ mutates_store: false,
3974
+ sends_terminal_input: true,
3975
+ candidate_source: "terminals[].available_actions.native_inspect"
3976
+ },
3937
3977
  resume_thread: {
3938
3978
  tool: "agent_knock_knock_resume_thread",
3939
3979
  target_argument: "terminal_id",
@@ -4084,6 +4124,20 @@ function availableListActions(entry, { conversation } = {}) {
4084
4124
  arguments: { terminal_id: id }
4085
4125
  };
4086
4126
  }
4127
+ if (terminalControlled &&
4128
+ commands.native_inspect === true &&
4129
+ entry.activity_state === "idle" &&
4130
+ approvalState.blocked !== true &&
4131
+ lifecycleBindingToken) {
4132
+ actions.native_inspect = {
4133
+ tool: "agent_knock_knock_native_inspect",
4134
+ arguments: {
4135
+ terminal_id: id,
4136
+ inspection: "status",
4137
+ expected_binding_token: lifecycleBindingToken
4138
+ }
4139
+ };
4140
+ }
4087
4141
  if (managed &&
4088
4142
  commands.respond === true &&
4089
4143
  terminalBridgeReady &&
@@ -5965,6 +6019,280 @@ async function runListResumableThreads(options) {
5965
6019
  }))
5966
6020
  });
5967
6021
  }
6022
+ function assertSameNativeInspectionTerminal(expected, actual, stage) {
6023
+ const expectedPath = expected.terminalControl.currentPath;
6024
+ const actualPath = actual.terminalControl.currentPath;
6025
+ if (actual.conversationId !== expected.conversationId ||
6026
+ actual.agent !== expected.agent ||
6027
+ actual.pid !== expected.pid ||
6028
+ terminalControlSelectorKey(actual.terminalControl) !==
6029
+ terminalControlSelectorKey(expected.terminalControl) ||
6030
+ !expectedPath ||
6031
+ !actualPath ||
6032
+ path.resolve(actualPath) !== path.resolve(expectedPath)) {
6033
+ throw new Error(`terminal identity, pane, or cwd changed ${stage}; refresh AKK list`);
6034
+ }
6035
+ }
6036
+ function assertTerminalNativeInspectionReady({ options, terminal, terminalStatus, session }) {
6037
+ if (terminalStatus &&
6038
+ (terminalStatus.reachable !== true ||
6039
+ terminalStatus.activity_state !== "idle" ||
6040
+ terminalStatus.approval_state.blocked === true)) {
6041
+ throw new Error(`terminal ${terminal.terminalControl.target} is not at a verified idle prompt ` +
6042
+ `(${terminalStatus.activity_state}: ${terminalStatus.activity_reason})`);
6043
+ }
6044
+ const blocker = listConversations(storeDirFromOptions(options))
6045
+ .filter(isDiscoverableTmuxConversation)
6046
+ .find((turn) => terminalKeyForManagedConversation(turn) ===
6047
+ terminalControlSelectorKey(terminal.terminalControl) &&
6048
+ SESSION_SEND_BLOCKING_STATUSES.has(turn.status));
6049
+ if (blocker) {
6050
+ throw new Error(`terminal ${terminal.terminalControl.target} still has unresolved Turn ` +
6051
+ `${turnIdForConversation(blocker)} (${blocker.status})`);
6052
+ }
6053
+ if (session &&
6054
+ managedSessionHasUnresolvedNativeTransition(storeDirFromOptions(options), session)) {
6055
+ throw new Error(`managed Session ${session.session_id} has an unresolved native-thread transition`);
6056
+ }
6057
+ const ownership = terminalDispatchOwnership(terminal.terminalControl);
6058
+ if (ownership.state !== "none") {
6059
+ throw new Error(`terminal ${terminal.terminalControl.target} has unresolved dispatch ` +
6060
+ "ownership; resolve it before native inspection");
6061
+ }
6062
+ const orphaned = orphanedTerminalDispatchForRecovery(terminal.terminalControl);
6063
+ if (orphaned) {
6064
+ throw new Error(`terminal ${terminal.terminalControl.target} has unresolved ` +
6065
+ `${String(orphaned.kind ?? "terminal")} input ` +
6066
+ `(${String(orphaned.status ?? "unknown")})`);
6067
+ }
6068
+ }
6069
+ function nativeInspectionRuntime({ terminal, snapshot }) {
6070
+ if (terminal.agent !== "codex") {
6071
+ throw new Error("native status inspection currently supports only Codex 0.146.0 and 0.146.1");
6072
+ }
6073
+ const exactRuntimeIdentity = snapshot.runtimeIdentity?.sessionId === snapshot.identity?.sessionId
6074
+ ? snapshot.runtimeIdentity
6075
+ : undefined;
6076
+ const runtime = exactRuntimeIdentity
6077
+ ? terminalRuntimeForLiveIdentity({
6078
+ terminal,
6079
+ identity: exactRuntimeIdentity
6080
+ })
6081
+ : {
6082
+ ...terminalRuntimeForLiveIdentity({
6083
+ terminal,
6084
+ expectedEmptyNativeSession: true
6085
+ }),
6086
+ ...(snapshot.identity?.processUuid
6087
+ ? { nativeProcessUuid: snapshot.identity.processUuid }
6088
+ : {}),
6089
+ ...(snapshot.identity?.processBirth
6090
+ ? { nativeProcessBirth: snapshot.identity.processBirth }
6091
+ : {}),
6092
+ ...(snapshot.identity?.sessionId
6093
+ ? { expectedNativeSessionId: snapshot.identity.sessionId }
6094
+ : {})
6095
+ };
6096
+ return withCodexCompanionFences(runtime, snapshot.codexCompanions);
6097
+ }
6098
+ function assertNativeInspectionSnapshotUnchanged({ expectedTerminal, actualTerminal, expectedBindingToken, expectedVersion, actualSnapshot, stage }) {
6099
+ assertSameNativeInspectionTerminal(expectedTerminal, actualTerminal, stage);
6100
+ if (actualSnapshot.bindingToken !== expectedBindingToken) {
6101
+ throw new Error(`terminal binding changed ${stage}; refresh AKK list`);
6102
+ }
6103
+ if (actualSnapshot.version !== expectedVersion) {
6104
+ throw new Error(`coding-agent version changed ${stage}; refresh AKK list`);
6105
+ }
6106
+ const capability = actualSnapshot.adapter.probeNativeInspection?.(actualSnapshot.version);
6107
+ if (capability?.status !== "supported" ||
6108
+ capability.statusInspection !== true) {
6109
+ throw new Error(capability?.reason ??
6110
+ "native status inspection became unsupported; refresh AKK list");
6111
+ }
6112
+ }
6113
+ async function runNativeInspect(options) {
6114
+ const inspection = required(stringValue(options.inspection), "--inspection is required");
6115
+ if (inspection !== "status") {
6116
+ throw new Error("--inspection must be the closed value status; arbitrary native slash commands are not accepted");
6117
+ }
6118
+ if (options.command !== undefined || options.message !== undefined) {
6119
+ throw new Error("native inspection does not accept a command or message payload");
6120
+ }
6121
+ const expectedBindingToken = required(stringValue(options.expectedBindingToken), "--expected-binding-token is required");
6122
+ const storeDir = storeDirFromOptions(options);
6123
+ const store = inspectStoreCompatibility(storeDir);
6124
+ if (store.writable !== true) {
6125
+ throw new Error("native inspection requires a compatible AKK Store so binding authority can be verified");
6126
+ }
6127
+ const initiallyResolved = await resolveLifecycleTerminal(options);
6128
+ const releaseTerminalLock = acquireFileLock(terminalBridgeSendLockPath(storeDir, initiallyResolved.terminalControl), { timeoutMs: 30000 });
6129
+ try {
6130
+ const terminal = await resolveLifecycleTerminal(options);
6131
+ assertSameNativeInspectionTerminal(initiallyResolved, terminal, "while waiting for native-inspection control");
6132
+ const snapshot = await currentLifecycleSnapshot(options, terminal);
6133
+ if (snapshot.bindingToken !== expectedBindingToken) {
6134
+ throw new Error("terminal binding changed after it was listed; refresh AKK list and retry");
6135
+ }
6136
+ const capability = snapshot.adapter.probeNativeInspection?.(snapshot.version);
6137
+ if (capability?.status !== "supported" ||
6138
+ capability.statusInspection !== true) {
6139
+ throw new Error(capability?.reason ??
6140
+ "native status inspection is unavailable for this agent version");
6141
+ }
6142
+ const plan = snapshot.adapter.planNativeInspection?.({ kind: "status" }, capability);
6143
+ if (!plan ||
6144
+ plan.operation.kind !== "status" ||
6145
+ plan.command !== "/status" ||
6146
+ plan.effect !== "read_only") {
6147
+ throw new Error("the agent adapter did not produce the closed native status inspection plan");
6148
+ }
6149
+ const runtime = nativeInspectionRuntime({ terminal, snapshot });
6150
+ const bridge = createTerminalAgentBridge(options);
6151
+ const initialStatus = await bridge.status(terminal.agent, terminal.terminalControl, { runtime });
6152
+ assertTerminalNativeInspectionReady({
6153
+ options,
6154
+ terminal,
6155
+ terminalStatus: initialStatus,
6156
+ session: snapshot.session
6157
+ });
6158
+ await assertCodexComposerReadyForAutomatedInput({
6159
+ options,
6160
+ terminalControl: terminal.terminalControl
6161
+ });
6162
+ let submission;
6163
+ try {
6164
+ submission = await bridge.submitNativeInspection(terminal.agent, terminal.terminalControl, plan, {
6165
+ runtime,
6166
+ beforeEnter: async () => {
6167
+ const finalTerminal = await resolveLifecycleTerminal(options);
6168
+ const finalSnapshot = await currentLifecycleSnapshot(options, finalTerminal);
6169
+ assertNativeInspectionSnapshotUnchanged({
6170
+ expectedTerminal: terminal,
6171
+ actualTerminal: finalTerminal,
6172
+ expectedBindingToken,
6173
+ expectedVersion: snapshot.version,
6174
+ actualSnapshot: finalSnapshot,
6175
+ stage: "immediately before native status submission"
6176
+ });
6177
+ assertTerminalNativeInspectionReady({
6178
+ options,
6179
+ terminal: finalTerminal,
6180
+ session: finalSnapshot.session
6181
+ });
6182
+ }
6183
+ });
6184
+ }
6185
+ catch (error) {
6186
+ const detail = error instanceof Error ? error.message : String(error);
6187
+ if (error instanceof NativeInspectionSubmissionError &&
6188
+ error.doNotRetry !== true) {
6189
+ throw new Error(`native status inspection did not start; refresh AKK list and retry if still desired: ${detail}`);
6190
+ }
6191
+ throw new Error("native status inspection did not cross a proven completion boundary; " +
6192
+ `do not retry automatically: ${detail}`);
6193
+ }
6194
+ try {
6195
+ const expectedNativeThreadId = snapshot.session?.binding?.native_thread_id ??
6196
+ snapshot.identity?.sessionId;
6197
+ let stableEvidenceFingerprint;
6198
+ let stableObservation;
6199
+ let stableCount = 0;
6200
+ for (let attempt = 0; attempt < 50; attempt += 1) {
6201
+ const observed = await bridge.observeNativeInspection(terminal.agent, terminal.terminalControl, {
6202
+ operation: plan.operation,
6203
+ previousScreenFingerprint: submission.preEnterScreenDigest,
6204
+ preEnterEvidenceInventory: submission.preEnterEvidenceInventory,
6205
+ expectedNativeThreadId,
6206
+ expectedAgentVersion: snapshot.version
6207
+ }, { runtime, scrollbackLines: 240 });
6208
+ const observation = observed.observation;
6209
+ if (observed.status.reachable === true &&
6210
+ observed.status.activity_state === "idle" &&
6211
+ observed.status.approval_state.blocked !== true &&
6212
+ observed.screenDigest !== submission.preEnterScreenDigest &&
6213
+ observation.status === "observed" &&
6214
+ observation.result?.kind === "native_status" &&
6215
+ isExactNativeThreadId(observation.nativeThreadId) &&
6216
+ observation.evidenceFingerprint) {
6217
+ if (stableEvidenceFingerprint === observation.evidenceFingerprint) {
6218
+ stableCount += 1;
6219
+ }
6220
+ else {
6221
+ stableEvidenceFingerprint = observation.evidenceFingerprint;
6222
+ stableObservation = observation;
6223
+ stableCount = 1;
6224
+ }
6225
+ if (stableCount >= 2) {
6226
+ stableObservation = observation;
6227
+ break;
6228
+ }
6229
+ }
6230
+ else {
6231
+ stableEvidenceFingerprint = undefined;
6232
+ stableObservation = undefined;
6233
+ stableCount = 0;
6234
+ }
6235
+ await new Promise((resolve) => setTimeout(resolve, 100));
6236
+ }
6237
+ if (!stableObservation || stableCount < 2) {
6238
+ throw new Error("native status inspection Enter was dispatched exactly once, but a fresh exact idle status result was not proven; do not retry automatically");
6239
+ }
6240
+ const finalTerminal = await resolveLifecycleTerminal(options);
6241
+ const finalSnapshot = await currentLifecycleSnapshot(options, finalTerminal);
6242
+ assertNativeInspectionSnapshotUnchanged({
6243
+ expectedTerminal: terminal,
6244
+ actualTerminal: finalTerminal,
6245
+ expectedBindingToken,
6246
+ expectedVersion: snapshot.version,
6247
+ actualSnapshot: finalSnapshot,
6248
+ stage: "after native status inspection"
6249
+ });
6250
+ const finalStatus = await bridge.status(finalTerminal.agent, finalTerminal.terminalControl, { runtime });
6251
+ assertTerminalNativeInspectionReady({
6252
+ options,
6253
+ terminal: finalTerminal,
6254
+ terminalStatus: finalStatus,
6255
+ session: finalSnapshot.session
6256
+ });
6257
+ await assertCodexComposerReadyForAutomatedInput({
6258
+ options,
6259
+ terminalControl: finalTerminal.terminalControl
6260
+ });
6261
+ printJson({
6262
+ status: "observed",
6263
+ inspection: "status",
6264
+ terminal_id: terminal.conversationId,
6265
+ agent: terminal.agent,
6266
+ agent_version: snapshot.version,
6267
+ behavior_profile: plan.behaviorProfile,
6268
+ native_thread_id: stableObservation.nativeThreadId,
6269
+ native_status: stableObservation.result,
6270
+ terminal_submission: {
6271
+ command: plan.command,
6272
+ enter_count: submission.enterCount,
6273
+ materialization: submission.materialization
6274
+ },
6275
+ store_mutation: false,
6276
+ session_created: false,
6277
+ turn_created: false,
6278
+ receipt_created: false,
6279
+ monitor_created: false,
6280
+ callback_created: false
6281
+ });
6282
+ }
6283
+ catch (error) {
6284
+ const detail = error instanceof Error ? error.message : String(error);
6285
+ if (/do not retry automatically/iu.test(detail)) {
6286
+ throw error;
6287
+ }
6288
+ throw new Error("native status inspection Enter was dispatched exactly once, but its " +
6289
+ `postcondition became uncertain; do not retry automatically: ${detail}`);
6290
+ }
6291
+ }
6292
+ finally {
6293
+ releaseTerminalLock();
6294
+ }
6295
+ }
5968
6296
  function previousCommittedResumeCandidate({ storeDir, terminal, currentSession, candidates }) {
5969
6297
  if (!currentSession?.last_transition_id) {
5970
6298
  return undefined;
@@ -10632,8 +10960,8 @@ function assertOrdinaryTerminalPayloadDoesNotInvokeNativeLifecycle(payload) {
10632
10960
  return;
10633
10961
  }
10634
10962
  throw new Error(`ordinary send/respond cannot invoke native slash command /${reserved[1].toLowerCase()}; ` +
10635
- "use the advertised new-thread, resume-thread, or status action, or enter " +
10636
- "an unsupported native command manually in tmux");
10963
+ "use an advertised dedicated native action when one exists, or enter " +
10964
+ "the unsupported native command manually in tmux");
10637
10965
  }
10638
10966
  function isCompleteNativeRollout(value) {
10639
10967
  return isRecord(value) &&
@@ -19112,6 +19440,7 @@ function usage() {
19112
19440
  agent-knock-knock new-thread --terminal <exact-terminal-id> --expected-binding-token <token>
19113
19441
  agent-knock-knock clear-thread --terminal <exact-terminal-id> --expected-binding-token <token>
19114
19442
  agent-knock-knock list-resumable-threads --terminal <exact-terminal-id> [--selection-scope <opaque-scope>]
19443
+ agent-knock-knock native-inspect --terminal <exact-terminal-id> --inspection status --expected-binding-token <token>
19115
19444
  agent-knock-knock resume-thread --terminal <exact-terminal-id> --native-thread <uuid> --expected-binding-token <token> --candidate-token <token>
19116
19445
  agent-knock-knock resume-thread --terminal <exact-terminal-id> (--selection-handle <handle> | --selection-snapshot <id> (--selection-number <n> | --selection-short-id <@id>)) --selection-scope <opaque-scope>
19117
19446
  agent-knock-knock reconcile-binding --terminal <exact-terminal-id> --conflicting-session <session-id> --expected-session-revision <n> --expected-binding-token <token> --expected-terminal-token <token>