@scotthuang/agent-knock-knock 0.11.3 → 0.11.5

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/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 { isExactClaudeNativeInspectionIdleComposer, 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
  }
@@ -706,7 +709,10 @@ function loadClaudeAgentRows(options = {}, observation = {}) {
706
709
  ...(Number.isSafeInteger(Number(row.startedAt)) && Number(row.startedAt) > 0
707
710
  ? { startedAt: Number(row.startedAt) }
708
711
  : {}),
709
- ...(stringValue(row.status) ? { status: stringValue(row.status) } : {})
712
+ ...(stringValue(row.status) ? { status: stringValue(row.status) } : {}),
713
+ ...(stringValue(row.waitingFor)
714
+ ? { waitingFor: stringValue(row.waitingFor) }
715
+ : {})
710
716
  }];
711
717
  });
712
718
  }
@@ -904,6 +910,9 @@ function createTerminalAgentBridge(options, terminalProvider = createTerminalCon
904
910
  runtime?.nativeProcessBirth ||
905
911
  runtime?.nativeRollout ||
906
912
  runtime?.requireNativeProcessUuid ||
913
+ runtime?.requireExactClaudeAgentRow ||
914
+ runtime?.nativeProcessStartedAt ||
915
+ runtime?.exactClaudeAgentState ||
907
916
  runtime?.requireNativeRolloutIdentity ||
908
917
  runtime?.expectedNativeSessionId ||
909
918
  runtime?.expectedEmptyNativeSession ||
@@ -931,6 +940,38 @@ function createTerminalAgentBridge(options, terminalProvider = createTerminalCon
931
940
  agent,
932
941
  pid
933
942
  });
943
+ if (runtime?.requireExactClaudeAgentRow === true) {
944
+ if (agent !== "claude" ||
945
+ !Number.isSafeInteger(runtime.nativeProcessStartedAt) ||
946
+ Number(runtime.nativeProcessStartedAt) <= 0 ||
947
+ !runtime.nativeSessionId) {
948
+ throw new Error(`native ${agent} inspection has an incomplete exact process identity; ` +
949
+ "refresh list before controlling the terminal");
950
+ }
951
+ const agentRows = loadClaudeAgentRows(options, { required: true });
952
+ const observation = adapter.observeThreadLifecycle?.({
953
+ operation: { kind: "new_thread" },
954
+ phase: "before",
955
+ pid,
956
+ processStartedAt: runtime.nativeProcessStartedAt,
957
+ cwd: snapshot.cwd ?? pane.currentPath,
958
+ agentRows
959
+ });
960
+ const exactRows = agentRows.filter((row) => row.pid === pid);
961
+ const expectedState = runtime.exactClaudeAgentState ?? "idle";
962
+ const stateMatches = expectedState === "idle"
963
+ ? observation?.idle === true
964
+ : (observation?.idle === false &&
965
+ exactRows.length === 1 &&
966
+ exactRows[0].status === "waiting" &&
967
+ exactRows[0].waitingFor === "dialog open");
968
+ if (observation?.status !== "observed" ||
969
+ !stateMatches ||
970
+ observation.nativeThreadId !== runtime.nativeSessionId) {
971
+ throw new Error(`native Claude agents identity, cwd, or idle state changed for process ${pid}; ` +
972
+ "refresh list before controlling the terminal");
973
+ }
974
+ }
934
975
  return {
935
976
  terminalControl: {
936
977
  ...terminalControl,
@@ -2782,6 +2823,13 @@ async function terminalControlledListEntry(session, activeSessions, options, bri
2782
2823
  resumeExact: false,
2783
2824
  reason: "native thread lifecycle is unavailable"
2784
2825
  };
2826
+ const nativeInspectionCapability = bridge.registry.require(session.agent)
2827
+ .probeNativeInspection?.(agentVersion) ?? {
2828
+ status: "unsupported",
2829
+ agentVersion,
2830
+ statusInspection: false,
2831
+ reason: "native inspection is unavailable"
2832
+ };
2785
2833
  const lifecycleBindingToken = unmanagedTerminalBindingToken({
2786
2834
  terminalId: bridge.terminalConversationId(session),
2787
2835
  terminalControl,
@@ -2795,6 +2843,10 @@ async function terminalControlledListEntry(session, activeSessions, options, bri
2795
2843
  });
2796
2844
  const codexLifecycleIncarnationAvailable = session.agent !== "codex" ||
2797
2845
  Boolean(nativeProcessUuid && nativeProcessBirth);
2846
+ const nativeInspectionHasBlockingTurn = listConversations(storeDirFromOptions(options)).some((turn) => isDiscoverableTmuxConversation(turn) &&
2847
+ terminalKeyForManagedConversation(turn) ===
2848
+ terminalControlSelectorKey(terminalControl) &&
2849
+ SESSION_SEND_BLOCKING_STATUSES.has(turn.status));
2798
2850
  const entry = {
2799
2851
  id: bridge.terminalConversationId(session),
2800
2852
  short_ref: sessionShortRef(bridge.terminalConversationId(session)),
@@ -2815,6 +2867,7 @@ async function terminalControlledListEntry(session, activeSessions, options, bri
2815
2867
  native_agent_identity_evidence: nativeProcessEvidence,
2816
2868
  agent_version: agentVersion,
2817
2869
  native_thread_lifecycle: lifecycleCapability,
2870
+ native_inspection: nativeInspectionCapability,
2818
2871
  lifecycle_binding_token: lifecycleBindingToken,
2819
2872
  confidence: session.confidence,
2820
2873
  reason: session.reason,
@@ -2849,7 +2902,18 @@ async function terminalControlledListEntry(session, activeSessions, options, bri
2849
2902
  codexLifecycleIncarnationAvailable,
2850
2903
  list_resumable_threads: lifecycleCapability.status === "supported" &&
2851
2904
  lifecycleCapability.resumeExact === true &&
2852
- codexLifecycleIncarnationAvailable
2905
+ codexLifecycleIncarnationAvailable,
2906
+ native_inspect: nativeInspectionCapability.status === "supported" &&
2907
+ nativeInspectionCapability.statusInspection === true &&
2908
+ nativeInspectionComposerEmpty(session.agent, terminalState.screen_excerpt) &&
2909
+ terminalControl.capabilities.includes("send_keys") &&
2910
+ terminalControl.capabilities.includes("screen_status") &&
2911
+ (session.agent === "codex"
2912
+ ? codexLifecycleIncarnationAvailable
2913
+ : Boolean(nativeAgentIdentity?.sessionId &&
2914
+ nativeAgentIdentity.processUuid)) &&
2915
+ orphanedDispatch === undefined &&
2916
+ !nativeInspectionHasBlockingTurn
2853
2917
  }
2854
2918
  };
2855
2919
  const availableActions = availableListActions(entry);
@@ -2973,9 +3037,13 @@ function terminalFirstListProjection({ storeDir, terminals, managedSessions, ses
2973
3037
  const rawActions = mutationsAllowed
2974
3038
  ? discoveredRawActions
2975
3039
  : readOnlyListActions(discoveredRawActions);
2976
- const sessionAwareRawActions = authoritativeSession
3040
+ const bindingAwareRawActions = authoritativeSession
2977
3041
  ? actionsForManagedSessionBinding(rawActions, authoritativeSession)
2978
3042
  : rawActions;
3043
+ const sessionAwareRawActions = authoritativeSession &&
3044
+ managedSessionHasUnresolvedNativeTransition(storeDir, authoritativeSession)
3045
+ ? Object.fromEntries(Object.entries(bindingAwareRawActions).filter(([actionName]) => actionName !== "native_inspect"))
3046
+ : bindingAwareRawActions;
2979
3047
  const soleBindingConflict = conflictingBoundSessionClaims.length === 1
2980
3048
  ? conflictingBoundSessionClaims[0]
2981
3049
  : undefined;
@@ -3595,7 +3663,7 @@ function sendActionForManagedSession(action, sessionId) {
3595
3663
  function actionsForManagedSessionBinding(actions, session) {
3596
3664
  const token = managedSessionBindingToken(session);
3597
3665
  const next = { ...actions };
3598
- for (const actionName of ["new_thread", "resume_thread"]) {
3666
+ for (const actionName of ["new_thread", "resume_thread", "native_inspect"]) {
3599
3667
  const action = isRecord(next[actionName]) ? next[actionName] : undefined;
3600
3668
  if (!action) {
3601
3669
  continue;
@@ -3863,11 +3931,12 @@ function managedListApprovalState(conversation) {
3863
3931
  }
3864
3932
  function listActionContracts() {
3865
3933
  return {
3866
- version: 6,
3934
+ version: 7,
3867
3935
  instructions: [
3868
3936
  "Treat terminals[] as the primary resource and use only actions present in available_actions.",
3869
3937
  "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
3938
  "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.",
3939
+ "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
3940
  "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
3941
  "List resumable threads before resume; use only a complete native_thread_id and the action returned for that candidate.",
3873
3942
  "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 +4003,17 @@ function listActionContracts() {
3934
4003
  required: ["terminal_id"],
3935
4004
  side_effect_free: true
3936
4005
  },
4006
+ native_inspect: {
4007
+ tool: "agent_knock_knock_native_inspect",
4008
+ target_argument: "terminal_id",
4009
+ required: ["terminal_id", "inspection", "expected_binding_token"],
4010
+ supported_inspections: ["status"],
4011
+ creates_turn: false,
4012
+ creates_session: false,
4013
+ mutates_store: false,
4014
+ sends_terminal_input: true,
4015
+ candidate_source: "terminals[].available_actions.native_inspect"
4016
+ },
3937
4017
  resume_thread: {
3938
4018
  tool: "agent_knock_knock_resume_thread",
3939
4019
  target_argument: "terminal_id",
@@ -4084,6 +4164,20 @@ function availableListActions(entry, { conversation } = {}) {
4084
4164
  arguments: { terminal_id: id }
4085
4165
  };
4086
4166
  }
4167
+ if (terminalControlled &&
4168
+ commands.native_inspect === true &&
4169
+ entry.activity_state === "idle" &&
4170
+ approvalState.blocked !== true &&
4171
+ lifecycleBindingToken) {
4172
+ actions.native_inspect = {
4173
+ tool: "agent_knock_knock_native_inspect",
4174
+ arguments: {
4175
+ terminal_id: id,
4176
+ inspection: "status",
4177
+ expected_binding_token: lifecycleBindingToken
4178
+ }
4179
+ };
4180
+ }
4087
4181
  if (managed &&
4088
4182
  commands.respond === true &&
4089
4183
  terminalBridgeReady &&
@@ -5965,6 +6059,452 @@ async function runListResumableThreads(options) {
5965
6059
  }))
5966
6060
  });
5967
6061
  }
6062
+ function assertSameNativeInspectionTerminal(expected, actual, stage) {
6063
+ const expectedPath = expected.terminalControl.currentPath;
6064
+ const actualPath = actual.terminalControl.currentPath;
6065
+ if (actual.conversationId !== expected.conversationId ||
6066
+ actual.agent !== expected.agent ||
6067
+ actual.pid !== expected.pid ||
6068
+ terminalControlSelectorKey(actual.terminalControl) !==
6069
+ terminalControlSelectorKey(expected.terminalControl) ||
6070
+ !expectedPath ||
6071
+ !actualPath ||
6072
+ path.resolve(actualPath) !== path.resolve(expectedPath)) {
6073
+ throw new Error(`terminal identity, pane, or cwd changed ${stage}; refresh AKK list`);
6074
+ }
6075
+ }
6076
+ function assertTerminalNativeInspectionReady({ options, terminal, terminalStatus, session }) {
6077
+ if (terminalStatus &&
6078
+ (terminalStatus.reachable !== true ||
6079
+ terminalStatus.activity_state !== "idle" ||
6080
+ terminalStatus.approval_state.blocked === true)) {
6081
+ throw new Error(`terminal ${terminal.terminalControl.target} is not at a verified idle prompt ` +
6082
+ `(${terminalStatus.activity_state}: ${terminalStatus.activity_reason})`);
6083
+ }
6084
+ const blocker = listConversations(storeDirFromOptions(options))
6085
+ .filter(isDiscoverableTmuxConversation)
6086
+ .find((turn) => terminalKeyForManagedConversation(turn) ===
6087
+ terminalControlSelectorKey(terminal.terminalControl) &&
6088
+ SESSION_SEND_BLOCKING_STATUSES.has(turn.status));
6089
+ if (blocker) {
6090
+ throw new Error(`terminal ${terminal.terminalControl.target} still has unresolved Turn ` +
6091
+ `${turnIdForConversation(blocker)} (${blocker.status})`);
6092
+ }
6093
+ if (session &&
6094
+ managedSessionHasUnresolvedNativeTransition(storeDirFromOptions(options), session)) {
6095
+ throw new Error(`managed Session ${session.session_id} has an unresolved native-thread transition`);
6096
+ }
6097
+ const ownership = terminalDispatchOwnership(terminal.terminalControl);
6098
+ if (ownership.state !== "none") {
6099
+ throw new Error(`terminal ${terminal.terminalControl.target} has unresolved dispatch ` +
6100
+ "ownership; resolve it before native inspection");
6101
+ }
6102
+ const orphaned = orphanedTerminalDispatchForRecovery(terminal.terminalControl);
6103
+ if (orphaned) {
6104
+ throw new Error(`terminal ${terminal.terminalControl.target} has unresolved ` +
6105
+ `${String(orphaned.kind ?? "terminal")} input ` +
6106
+ `(${String(orphaned.status ?? "unknown")})`);
6107
+ }
6108
+ }
6109
+ function nativeInspectionRuntime({ terminal, snapshot }) {
6110
+ if (terminal.agent === "claude") {
6111
+ const identity = snapshot.runtimeIdentity;
6112
+ if (!identity?.sessionId ||
6113
+ !identity.processUuid ||
6114
+ identity.sessionId !== snapshot.identity?.sessionId) {
6115
+ throw new Error("Claude native status inspection requires one exact claude agents Session and process incarnation");
6116
+ }
6117
+ return {
6118
+ ...terminalRuntimeForLiveIdentity({ terminal, identity }),
6119
+ requireExactClaudeAgentRow: true,
6120
+ nativeProcessStartedAt: identity.processStartedAt,
6121
+ exactClaudeAgentState: "idle"
6122
+ };
6123
+ }
6124
+ const exactRuntimeIdentity = snapshot.runtimeIdentity?.sessionId === snapshot.identity?.sessionId
6125
+ ? snapshot.runtimeIdentity
6126
+ : undefined;
6127
+ const runtime = exactRuntimeIdentity
6128
+ ? terminalRuntimeForLiveIdentity({
6129
+ terminal,
6130
+ identity: exactRuntimeIdentity
6131
+ })
6132
+ : {
6133
+ ...terminalRuntimeForLiveIdentity({
6134
+ terminal,
6135
+ expectedEmptyNativeSession: true
6136
+ }),
6137
+ ...(snapshot.identity?.processUuid
6138
+ ? { nativeProcessUuid: snapshot.identity.processUuid }
6139
+ : {}),
6140
+ ...(snapshot.identity?.processBirth
6141
+ ? { nativeProcessBirth: snapshot.identity.processBirth }
6142
+ : {}),
6143
+ ...(snapshot.identity?.sessionId
6144
+ ? { expectedNativeSessionId: snapshot.identity.sessionId }
6145
+ : {})
6146
+ };
6147
+ return withCodexCompanionFences(runtime, snapshot.codexCompanions);
6148
+ }
6149
+ function assertNativeInspectionSnapshotUnchanged({ options, expectedTerminal, actualTerminal, expectedBindingToken, expectedVersion, actualSnapshot, stage, expectedClaudeState = "idle" }) {
6150
+ assertSameNativeInspectionTerminal(expectedTerminal, actualTerminal, stage);
6151
+ if (actualSnapshot.bindingToken !== expectedBindingToken) {
6152
+ throw new Error(`terminal binding changed ${stage}; refresh AKK list`);
6153
+ }
6154
+ if (actualSnapshot.version !== expectedVersion) {
6155
+ throw new Error(`coding-agent version changed ${stage}; refresh AKK list`);
6156
+ }
6157
+ const capability = actualSnapshot.adapter.probeNativeInspection?.(actualSnapshot.version);
6158
+ if (capability?.status !== "supported" ||
6159
+ capability.statusInspection !== true) {
6160
+ throw new Error(capability?.reason ??
6161
+ "native status inspection became unsupported; refresh AKK list");
6162
+ }
6163
+ assertNativeInspectionAgentIdentity({
6164
+ options,
6165
+ terminal: actualTerminal,
6166
+ snapshot: actualSnapshot,
6167
+ stage,
6168
+ expectedClaudeState
6169
+ });
6170
+ }
6171
+ function assertNativeInspectionAgentIdentity({ options, terminal, snapshot, stage, expectedClaudeState = "idle" }) {
6172
+ if (terminal.agent !== "claude") {
6173
+ return;
6174
+ }
6175
+ const identity = snapshot.runtimeIdentity;
6176
+ if (!identity?.sessionId ||
6177
+ !identity.processUuid ||
6178
+ !Number.isSafeInteger(identity.processStartedAt) ||
6179
+ Number(identity.processStartedAt) <= 0) {
6180
+ throw new Error(`Claude process identity is incomplete ${stage}; refresh AKK list`);
6181
+ }
6182
+ const agentRows = loadClaudeAgentRows(options, { required: true });
6183
+ const observation = snapshot.adapter.observeThreadLifecycle?.({
6184
+ operation: { kind: "new_thread" },
6185
+ phase: "before",
6186
+ pid: terminal.pid,
6187
+ processStartedAt: identity.processStartedAt,
6188
+ cwd: terminal.terminalControl.currentPath,
6189
+ agentRows
6190
+ });
6191
+ const exactRows = agentRows.filter((row) => row.pid === terminal.pid);
6192
+ const stateMatches = expectedClaudeState === "idle"
6193
+ ? observation?.idle === true
6194
+ : (observation?.idle === false &&
6195
+ exactRows.length === 1 &&
6196
+ exactRows[0].status === "waiting" &&
6197
+ exactRows[0].waitingFor === "dialog open");
6198
+ if (observation?.status !== "observed" ||
6199
+ !stateMatches ||
6200
+ observation.nativeThreadId !== identity.sessionId) {
6201
+ throw new Error(`Claude agents identity, cwd, or idle state changed ${stage}; refresh AKK list`);
6202
+ }
6203
+ }
6204
+ async function assertNativeInspectionExclusiveOwnership({ options, terminal, snapshot }) {
6205
+ // Claude's exact `claude agents` mapping makes global active ownership part
6206
+ // of the 2.1.218 inspection profile. Keep the existing Codex #112 path
6207
+ // unchanged: an unmanaged Codex pane may be inspected before a rollout
6208
+ // identity exists, and its status card is the bounded identity evidence.
6209
+ if (terminal.agent !== "claude") {
6210
+ return;
6211
+ }
6212
+ const nativeThreadId = snapshot.identity?.sessionId ??
6213
+ snapshot.session?.binding?.native_thread_id;
6214
+ if (!isExactNativeThreadId(nativeThreadId)) {
6215
+ throw new Error("native status inspection requires one exact current native Session identity");
6216
+ }
6217
+ await assertNativeThreadHasExclusiveOwnership({
6218
+ options,
6219
+ agent: terminal.agent,
6220
+ currentPid: terminal.pid,
6221
+ nativeThreadId,
6222
+ storeDir: storeDirFromOptions(options),
6223
+ terminalControl: terminal.terminalControl,
6224
+ excludedManagedSessionId: snapshot.session?.session_id
6225
+ });
6226
+ }
6227
+ async function runNativeInspect(options) {
6228
+ const inspection = required(stringValue(options.inspection), "--inspection is required");
6229
+ if (inspection !== "status") {
6230
+ throw new Error("--inspection must be the closed value status; arbitrary native slash commands are not accepted");
6231
+ }
6232
+ if (options.command !== undefined || options.message !== undefined) {
6233
+ throw new Error("native inspection does not accept a command or message payload");
6234
+ }
6235
+ const expectedBindingToken = required(stringValue(options.expectedBindingToken), "--expected-binding-token is required");
6236
+ const storeDir = storeDirFromOptions(options);
6237
+ const store = inspectStoreCompatibility(storeDir);
6238
+ if (store.writable !== true) {
6239
+ throw new Error("native inspection requires a compatible AKK Store so binding authority can be verified");
6240
+ }
6241
+ const initiallyResolved = await resolveLifecycleTerminal(options);
6242
+ const releaseTerminalLock = acquireFileLock(terminalBridgeSendLockPath(storeDir, initiallyResolved.terminalControl), { timeoutMs: 30000 });
6243
+ try {
6244
+ const terminal = await resolveLifecycleTerminal(options);
6245
+ assertSameNativeInspectionTerminal(initiallyResolved, terminal, "while waiting for native-inspection control");
6246
+ const snapshot = await currentLifecycleSnapshot(options, terminal);
6247
+ if (snapshot.bindingToken !== expectedBindingToken) {
6248
+ throw new Error("terminal binding changed after it was listed; refresh AKK list and retry");
6249
+ }
6250
+ const capability = snapshot.adapter.probeNativeInspection?.(snapshot.version);
6251
+ if (capability?.status !== "supported" ||
6252
+ capability.statusInspection !== true) {
6253
+ throw new Error(capability?.reason ??
6254
+ "native status inspection is unavailable for this agent version");
6255
+ }
6256
+ const plan = snapshot.adapter.planNativeInspection?.({ kind: "status" }, capability);
6257
+ if (!plan ||
6258
+ plan.operation.kind !== "status" ||
6259
+ plan.command !== "/status" ||
6260
+ plan.effect !== "read_only") {
6261
+ throw new Error("the agent adapter did not produce the closed native status inspection plan");
6262
+ }
6263
+ assertNativeInspectionAgentIdentity({
6264
+ options,
6265
+ terminal,
6266
+ snapshot,
6267
+ stage: "before native status inspection"
6268
+ });
6269
+ await assertNativeInspectionExclusiveOwnership({
6270
+ options,
6271
+ terminal,
6272
+ snapshot
6273
+ });
6274
+ const runtime = nativeInspectionRuntime({ terminal, snapshot });
6275
+ const bridge = createTerminalAgentBridge(options);
6276
+ const initialStatus = await bridge.status(terminal.agent, terminal.terminalControl, { runtime });
6277
+ assertTerminalNativeInspectionReady({
6278
+ options,
6279
+ terminal,
6280
+ terminalStatus: initialStatus,
6281
+ session: snapshot.session
6282
+ });
6283
+ await assertNativeInspectionComposerReadyForAutomatedInput({
6284
+ options,
6285
+ terminal
6286
+ });
6287
+ let submission;
6288
+ try {
6289
+ submission = await bridge.submitNativeInspection(terminal.agent, terminal.terminalControl, plan, {
6290
+ runtime,
6291
+ beforeEnter: async () => {
6292
+ const finalTerminal = await resolveLifecycleTerminal(options);
6293
+ const finalSnapshot = await currentLifecycleSnapshot(options, finalTerminal);
6294
+ assertNativeInspectionSnapshotUnchanged({
6295
+ options,
6296
+ expectedTerminal: terminal,
6297
+ actualTerminal: finalTerminal,
6298
+ expectedBindingToken,
6299
+ expectedVersion: snapshot.version,
6300
+ actualSnapshot: finalSnapshot,
6301
+ stage: "immediately before native status submission"
6302
+ });
6303
+ await assertNativeInspectionExclusiveOwnership({
6304
+ options,
6305
+ terminal: finalTerminal,
6306
+ snapshot: finalSnapshot
6307
+ });
6308
+ assertTerminalNativeInspectionReady({
6309
+ options,
6310
+ terminal: finalTerminal,
6311
+ session: finalSnapshot.session
6312
+ });
6313
+ }
6314
+ });
6315
+ }
6316
+ catch (error) {
6317
+ const detail = error instanceof Error ? error.message : String(error);
6318
+ if (error instanceof NativeInspectionSubmissionError &&
6319
+ error.doNotRetry !== true) {
6320
+ throw new Error(`native status inspection did not start; refresh AKK list and retry if still desired: ${detail}`);
6321
+ }
6322
+ throw new Error("native status inspection did not cross a proven completion boundary; " +
6323
+ `do not retry automatically: ${detail}`);
6324
+ }
6325
+ try {
6326
+ const expectedNativeThreadId = snapshot.session?.binding?.native_thread_id ??
6327
+ snapshot.identity?.sessionId;
6328
+ const observationRequest = {
6329
+ operation: plan.operation,
6330
+ previousScreenFingerprint: submission.preEnterScreenDigest,
6331
+ preEnterEvidenceInventory: submission.preEnterEvidenceInventory,
6332
+ expectedNativeThreadId,
6333
+ expectedAgentVersion: snapshot.version,
6334
+ expectedCwd: terminal.terminalControl.currentPath
6335
+ };
6336
+ const postEnterRuntime = plan.expectedResult.presentation === "modal"
6337
+ ? { ...runtime, exactClaudeAgentState: "status_dialog" }
6338
+ : runtime;
6339
+ let stableEvidenceFingerprint;
6340
+ let stableObservation;
6341
+ let stableCount = 0;
6342
+ for (let attempt = 0; attempt < 50; attempt += 1) {
6343
+ const observed = await bridge.observeNativeInspection(terminal.agent, terminal.terminalControl, observationRequest, { runtime: postEnterRuntime, scrollbackLines: 240 });
6344
+ const observation = observed.observation;
6345
+ if (observed.status.reachable === true &&
6346
+ (plan.expectedResult.presentation === "inline"
6347
+ ? observed.status.activity_state === "idle"
6348
+ : ![
6349
+ "working",
6350
+ "awaiting_approval"
6351
+ ].includes(observed.status.activity_state)) &&
6352
+ observed.status.approval_state.blocked !== true &&
6353
+ observed.screenDigest !== submission.preEnterScreenDigest &&
6354
+ observation.status === "observed" &&
6355
+ observation.result?.kind === "native_status" &&
6356
+ isExactNativeThreadId(observation.nativeThreadId) &&
6357
+ observation.evidenceFingerprint) {
6358
+ if (stableEvidenceFingerprint === observation.evidenceFingerprint) {
6359
+ stableCount += 1;
6360
+ }
6361
+ else {
6362
+ stableEvidenceFingerprint = observation.evidenceFingerprint;
6363
+ stableObservation = observation;
6364
+ stableCount = 1;
6365
+ }
6366
+ if (stableCount >= 2) {
6367
+ stableObservation = observation;
6368
+ break;
6369
+ }
6370
+ }
6371
+ else {
6372
+ stableEvidenceFingerprint = undefined;
6373
+ stableObservation = undefined;
6374
+ stableCount = 0;
6375
+ }
6376
+ await new Promise((resolve) => setTimeout(resolve, 100));
6377
+ }
6378
+ if (!stableObservation || stableCount < 2) {
6379
+ throw new Error("native status inspection Enter was dispatched exactly once, but a fresh exact status result was not proven; do not retry automatically");
6380
+ }
6381
+ let dismissal;
6382
+ if (plan.expectedResult.presentation === "modal") {
6383
+ if (!stableObservation.evidenceFingerprint) {
6384
+ throw new Error("native status modal lacks exact dismissal evidence; do not retry automatically");
6385
+ }
6386
+ try {
6387
+ dismissal = await bridge.dismissNativeInspection(terminal.agent, terminal.terminalControl, plan, observationRequest, stableObservation.evidenceFingerprint, {
6388
+ runtime: postEnterRuntime,
6389
+ scrollbackLines: 240,
6390
+ beforeDismiss: async () => {
6391
+ const dismissTerminal = await resolveLifecycleTerminal(options);
6392
+ const dismissSnapshot = await currentLifecycleSnapshot(options, dismissTerminal);
6393
+ assertNativeInspectionSnapshotUnchanged({
6394
+ options,
6395
+ expectedTerminal: terminal,
6396
+ actualTerminal: dismissTerminal,
6397
+ expectedBindingToken,
6398
+ expectedVersion: snapshot.version,
6399
+ actualSnapshot: dismissSnapshot,
6400
+ stage: "immediately before native status dismissal",
6401
+ expectedClaudeState: "status_dialog"
6402
+ });
6403
+ await assertNativeInspectionExclusiveOwnership({
6404
+ options,
6405
+ terminal: dismissTerminal,
6406
+ snapshot: dismissSnapshot
6407
+ });
6408
+ assertTerminalNativeInspectionReady({
6409
+ options,
6410
+ terminal: dismissTerminal,
6411
+ session: dismissSnapshot.session
6412
+ });
6413
+ }
6414
+ });
6415
+ }
6416
+ catch (error) {
6417
+ const detail = error instanceof Error ? error.message : String(error);
6418
+ throw new Error("native status panel was proven but safe dismissal failed; " +
6419
+ `do not retry automatically and dismiss it manually if still visible: ${detail}`);
6420
+ }
6421
+ let restoredIdle = false;
6422
+ for (let attempt = 0; attempt < 50; attempt += 1) {
6423
+ const restored = await bridge.status(terminal.agent, terminal.terminalControl, { runtime });
6424
+ if (restored.reachable === true &&
6425
+ restored.activity_state === "idle" &&
6426
+ restored.approval_state.blocked !== true &&
6427
+ nativeInspectionComposerEmpty(terminal.agent, restored.screen.excerpt)) {
6428
+ restoredIdle = true;
6429
+ break;
6430
+ }
6431
+ await new Promise((resolve) => setTimeout(resolve, 100));
6432
+ }
6433
+ if (!restoredIdle) {
6434
+ throw new Error("Claude Status panel dismissal was dispatched exactly once, but the original idle composer was not restored; do not retry automatically");
6435
+ }
6436
+ }
6437
+ const finalTerminal = await resolveLifecycleTerminal(options);
6438
+ const finalSnapshot = await currentLifecycleSnapshot(options, finalTerminal);
6439
+ assertNativeInspectionSnapshotUnchanged({
6440
+ options,
6441
+ expectedTerminal: terminal,
6442
+ actualTerminal: finalTerminal,
6443
+ expectedBindingToken,
6444
+ expectedVersion: snapshot.version,
6445
+ actualSnapshot: finalSnapshot,
6446
+ stage: "after native status inspection"
6447
+ });
6448
+ await assertNativeInspectionExclusiveOwnership({
6449
+ options,
6450
+ terminal: finalTerminal,
6451
+ snapshot: finalSnapshot
6452
+ });
6453
+ const finalStatus = await bridge.status(finalTerminal.agent, finalTerminal.terminalControl, { runtime });
6454
+ assertTerminalNativeInspectionReady({
6455
+ options,
6456
+ terminal: finalTerminal,
6457
+ terminalStatus: finalStatus,
6458
+ session: finalSnapshot.session
6459
+ });
6460
+ await assertNativeInspectionComposerReadyForAutomatedInput({
6461
+ options,
6462
+ terminal: finalTerminal
6463
+ });
6464
+ printJson({
6465
+ status: "observed",
6466
+ inspection: "status",
6467
+ terminal_id: terminal.conversationId,
6468
+ agent: terminal.agent,
6469
+ agent_version: snapshot.version,
6470
+ behavior_profile: plan.behaviorProfile,
6471
+ native_thread_id: stableObservation.nativeThreadId,
6472
+ native_status: stableObservation.result,
6473
+ terminal_submission: {
6474
+ command: plan.command,
6475
+ enter_count: submission.enterCount,
6476
+ materialization: submission.materialization
6477
+ },
6478
+ ...(dismissal
6479
+ ? {
6480
+ terminal_dismissal: {
6481
+ keys: dismissal.keys,
6482
+ dismiss_count: dismissal.dismissCount,
6483
+ restored_idle: true
6484
+ }
6485
+ }
6486
+ : {}),
6487
+ store_mutation: false,
6488
+ session_created: false,
6489
+ turn_created: false,
6490
+ receipt_created: false,
6491
+ monitor_created: false,
6492
+ callback_created: false
6493
+ });
6494
+ }
6495
+ catch (error) {
6496
+ const detail = error instanceof Error ? error.message : String(error);
6497
+ if (/do not retry automatically/iu.test(detail)) {
6498
+ throw error;
6499
+ }
6500
+ throw new Error("native status inspection Enter was dispatched exactly once, but its " +
6501
+ `postcondition became uncertain; do not retry automatically: ${detail}`);
6502
+ }
6503
+ }
6504
+ finally {
6505
+ releaseTerminalLock();
6506
+ }
6507
+ }
5968
6508
  function previousCommittedResumeCandidate({ storeDir, terminal, currentSession, candidates }) {
5969
6509
  if (!currentSession?.last_transition_id) {
5970
6510
  return undefined;
@@ -10632,8 +11172,8 @@ function assertOrdinaryTerminalPayloadDoesNotInvokeNativeLifecycle(payload) {
10632
11172
  return;
10633
11173
  }
10634
11174
  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");
11175
+ "use an advertised dedicated native action when one exists, or enter " +
11176
+ "the unsupported native command manually in tmux");
10637
11177
  }
10638
11178
  function isCompleteNativeRollout(value) {
10639
11179
  return isRecord(value) &&
@@ -10814,6 +11354,13 @@ function assertNativeAgentIdentityForRuntime({ runtime, currentIdentity, agent,
10814
11354
  throw new Error(`native ${agent} process incarnation cannot be verified for process ${pid}; ` +
10815
11355
  "refresh list before controlling the terminal");
10816
11356
  }
11357
+ if (runtime?.nativeProcessStartedAt !== undefined &&
11358
+ (!Number.isSafeInteger(runtime.nativeProcessStartedAt) ||
11359
+ Number(runtime.nativeProcessStartedAt) <= 0 ||
11360
+ currentIdentity?.processStartedAt !== runtime.nativeProcessStartedAt)) {
11361
+ throw new Error(`native ${agent} process start changed for process ${pid}; ` +
11362
+ "refresh list before controlling the terminal");
11363
+ }
10817
11364
  if (!runtime?.nativeSessionId) {
10818
11365
  return;
10819
11366
  }
@@ -15695,6 +16242,27 @@ function codexComposerVisible(screen) {
15695
16242
  .slice(-8)
15696
16243
  .some((line) => /^[›»](?:\s|$)/u.test(line.trimEnd()));
15697
16244
  }
16245
+ function nativeInspectionComposerEmpty(agent, screen) {
16246
+ return agent === "codex"
16247
+ ? codexComposerEmpty(screen)
16248
+ : isExactClaudeNativeInspectionIdleComposer(String(screen ?? ""));
16249
+ }
16250
+ async function assertNativeInspectionComposerReadyForAutomatedInput({ options, terminal }) {
16251
+ if (terminal.agent === "codex") {
16252
+ await assertCodexComposerReadyForAutomatedInput({
16253
+ options,
16254
+ terminalControl: terminal.terminalControl
16255
+ });
16256
+ return;
16257
+ }
16258
+ const screen = await createTerminalControlProvider(options).capture(terminal.terminalControl.target, {
16259
+ scrollbackLines: 40,
16260
+ socketPath: terminal.terminalControl.socketPath
16261
+ });
16262
+ if (!isExactClaudeNativeInspectionIdleComposer(screen)) {
16263
+ throw new Error("Claude composer contains input or is not at the exact idle frame; refusing automated terminal input");
16264
+ }
16265
+ }
15698
16266
  function codexComposerEmpty(screen) {
15699
16267
  const composers = String(screen ?? "")
15700
16268
  .split(/\r?\n/u)
@@ -15768,14 +16336,20 @@ function codexStyledComposerEmpty(screen) {
15768
16336
  return content.length === 0 || content.every((entry) => entry.dim);
15769
16337
  }
15770
16338
  function claudeComposerVisible(screen) {
15771
- return String(screen ?? "")
15772
- .split(/\r?\n/u)
16339
+ const lines = String(screen ?? "").split(/\r?\n/u);
16340
+ while (lines.length > 0 && lines.at(-1)?.trim() === "") {
16341
+ lines.pop();
16342
+ }
16343
+ return lines
15773
16344
  .slice(-8)
15774
16345
  .some((line) => /^\s*❯(?:\s|$)/u.test(line));
15775
16346
  }
15776
16347
  function claudeComposerEmpty(screen) {
15777
- const composers = String(screen ?? "")
15778
- .split(/\r?\n/u)
16348
+ const lines = String(screen ?? "").split(/\r?\n/u);
16349
+ while (lines.length > 0 && lines.at(-1)?.trim() === "") {
16350
+ lines.pop();
16351
+ }
16352
+ const composers = lines
15779
16353
  .slice(-8)
15780
16354
  .filter((line) => /^\s*❯(?:\s|$)/u.test(line));
15781
16355
  return composers.length === 1 && /^\s*❯\s*$/u.test(composers[0]);
@@ -19112,6 +19686,7 @@ function usage() {
19112
19686
  agent-knock-knock new-thread --terminal <exact-terminal-id> --expected-binding-token <token>
19113
19687
  agent-knock-knock clear-thread --terminal <exact-terminal-id> --expected-binding-token <token>
19114
19688
  agent-knock-knock list-resumable-threads --terminal <exact-terminal-id> [--selection-scope <opaque-scope>]
19689
+ agent-knock-knock native-inspect --terminal <exact-terminal-id> --inspection status --expected-binding-token <token>
19115
19690
  agent-knock-knock resume-thread --terminal <exact-terminal-id> --native-thread <uuid> --expected-binding-token <token> --candidate-token <token>
19116
19691
  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
19692
  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>