@scotthuang/agent-knock-knock 0.11.5 → 0.11.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.md +3 -3
  3. package/dist/src/claude-lifecycle-compatibility.d.ts +16 -0
  4. package/dist/src/claude-lifecycle-compatibility.js +77 -0
  5. package/dist/src/claude-lifecycle-compatibility.js.map +1 -0
  6. package/dist/src/claude-local-transcript-provider.js +37 -17
  7. package/dist/src/claude-local-transcript-provider.js.map +1 -1
  8. package/dist/src/claude-terminal-agent-adapter.d.ts +4 -4
  9. package/dist/src/claude-terminal-agent-adapter.js +42 -35
  10. package/dist/src/claude-terminal-agent-adapter.js.map +1 -1
  11. package/dist/src/cli.js +604 -405
  12. package/dist/src/cli.js.map +1 -1
  13. package/dist/src/codex-lifecycle-compatibility.js +2 -1
  14. package/dist/src/codex-lifecycle-compatibility.js.map +1 -1
  15. package/dist/src/codex-terminal-agent-adapter.d.ts +1 -0
  16. package/dist/src/codex-terminal-agent-adapter.js +3 -1
  17. package/dist/src/codex-terminal-agent-adapter.js.map +1 -1
  18. package/dist/src/managed-session.d.ts +15 -0
  19. package/dist/src/managed-session.js +105 -4
  20. package/dist/src/managed-session.js.map +1 -1
  21. package/dist/src/native-thread-resume-snapshot.d.ts +5 -2
  22. package/dist/src/native-thread-resume-snapshot.js +65 -15
  23. package/dist/src/native-thread-resume-snapshot.js.map +1 -1
  24. package/dist/src/openclaw-plugin.js +2 -2
  25. package/dist/src/openclaw-plugin.js.map +1 -1
  26. package/dist/src/session-store.js +5 -0
  27. package/dist/src/session-store.js.map +1 -1
  28. package/dist/src/terminal-agent-adapter.d.ts +4 -14
  29. package/dist/src/terminal-agent-adapter.js.map +1 -1
  30. package/dist/src/terminal-agent-bridge.d.ts +8 -2
  31. package/dist/src/terminal-agent-bridge.js +250 -95
  32. package/dist/src/terminal-agent-bridge.js.map +1 -1
  33. package/dist/src/terminal-control-provider.d.ts +49 -21
  34. package/dist/src/terminal-control-provider.js +328 -30
  35. package/dist/src/terminal-control-provider.js.map +1 -1
  36. package/dist/src/terminal-control-ref.d.ts +119 -0
  37. package/dist/src/terminal-control-ref.js +410 -0
  38. package/dist/src/terminal-control-ref.js.map +1 -0
  39. package/docs/quickstart-tmux.md +1 -1
  40. package/package.json +1 -1
  41. package/templates/openclaw-skills/agent-knock-knock/SKILL.md +1 -1
@@ -1,9 +1,11 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { formatTerminalConversationId, parseTerminalConversationId, terminalControlCapabilitiesForAdapter } from "./terminal-agent-adapter.js";
3
- import { enrichActiveProcessesWithTerminalControl, TerminalControlInputNotSentError, terminalRefFromPane } from "./terminal-control-provider.js";
4
- // Codex 0.146.x keeps Enter in paste/newline mode for 120ms after burst input.
5
- // Cross that boundary rather than landing on it, and also require observable
6
- // composer stability instead of treating this delay alone as acceptance.
3
+ import { enrichActiveProcessesWithTerminalControl, TerminalControlInputNotSentError } from "./terminal-control-provider.js";
4
+ import { hasCanonicalTerminalEndpoint, sameTerminalControlIncarnation, terminalEndpointFromControlRef, terminalEndpointIdentityKey } from "./terminal-control-ref.js";
5
+ // Verified Codex profiles through 0.147.0 keep Enter in paste/newline mode for
6
+ // 120ms after burst input. Cross that boundary rather than landing on it, and
7
+ // also require observable composer stability instead of treating this delay
8
+ // alone as acceptance.
7
9
  const CODEX_PASTE_ENTER_SETTLE_MS = 121;
8
10
  const CODEX_MULTILINE_SETTLE_POLL_MS = 30;
9
11
  const CODEX_MULTILINE_SETTLE_TIMEOUT_MS = 2_000;
@@ -11,12 +13,20 @@ const CODEX_MULTILINE_STABLE_CAPTURES = 2;
11
13
  const CODEX_MULTILINE_SETTLE_SCROLLBACK_LINES = 240;
12
14
  const CODEX_COMPOSER_MARKER = /^[›»](?:\s|$)/u;
13
15
  const CODEX_COMPOSER_FOOTER = /^(?:gpt-[\w.-]+(?:\s|$)|[-\w.]+ default ·)/u;
14
- // Codex 0.146.0 and 0.146.1 render the same verified slash-completion row.
15
- // Keep it closed per behavior profile so changed text cannot silently become
16
- // a newly authorized native command surface.
16
+ // Keep every exact slash-completion shape closed per behavior profile so a
17
+ // version adding another matching command cannot silently become an authorized
18
+ // native command surface.
17
19
  const CODEX_NATIVE_STATUS_POPUP_BY_PROFILE = {
18
- "codex-tui-0.146.0": " /status show current session configuration and token usage",
19
- "codex-tui-0.146.1": " /status show current session configuration and token usage"
20
+ "codex-tui-0.146.0": [
21
+ " /status show current session configuration and token usage"
22
+ ],
23
+ "codex-tui-0.146.1": [
24
+ " /status show current session configuration and token usage"
25
+ ],
26
+ "codex-tui-0.147.0": [
27
+ " /status show current session configuration and token usage",
28
+ " /statusline configure which items appear in the status line"
29
+ ]
20
30
  };
21
31
  const CLAUDE_NATIVE_STATUS_POPUP_BY_PROFILE = {
22
32
  "claude-code-2.1.218-native-status": [
@@ -24,6 +34,12 @@ const CLAUDE_NATIVE_STATUS_POPUP_BY_PROFILE = {
24
34
  "/statusline Set up Claude Code's status line UI",
25
35
  "/ide Manage IDE integrations and show status",
26
36
  "/usage Show session cost, plan usage, and activity stats"
37
+ ],
38
+ "claude-code-2.1.226-native-status": [
39
+ "/status Show Claude Code status including version, model, account, API connectivity, and tool statuses",
40
+ "/statusline Set up Claude Code's status line UI",
41
+ "/ide Manage IDE integrations and show status",
42
+ "/usage Show session cost, plan usage, and activity stats"
27
43
  ]
28
44
  };
29
45
  const CODEX_LARGE_PASTE_CHAR_THRESHOLD = 1_000;
@@ -109,34 +125,57 @@ export class TerminalAgentBridge {
109
125
  throw new Error(`process ${process.pid} is not terminal-controlled`);
110
126
  }
111
127
  this.registry.require(process.agent);
128
+ const terminal = this.terminalProvider.endpoint(process.terminalControl);
112
129
  return formatTerminalConversationId({
113
130
  agent: process.agent,
114
- target: process.terminalControl.target,
131
+ target: terminal.route.label,
115
132
  pid: process.pid
116
133
  });
117
134
  }
135
+ /**
136
+ * Resolve a persisted control reference by stable endpoint identity. This is
137
+ * the managed-Session path: its route-shaped v2 terminal id is only a
138
+ * compatibility alias and may be stale after an explicit provider refresh.
139
+ */
140
+ async resolveStoredTerminal(agent, pid, terminalControl, runtime = { pid }) {
141
+ const adapter = this.registry.require(agent);
142
+ const freshControl = await this.verifyTerminalIdentity(agent, terminalControl, { ...runtime, pid });
143
+ return {
144
+ conversationId: this.terminalConversationId({
145
+ agent,
146
+ pid,
147
+ terminalControl: freshControl
148
+ }),
149
+ agent,
150
+ pid,
151
+ legacy: false,
152
+ adapter,
153
+ terminalControl: freshControl
154
+ };
155
+ }
118
156
  async resolveConversationId(conversationId) {
119
157
  const parsed = parseTerminalConversationId(conversationId);
120
158
  if (!parsed) {
121
159
  return undefined;
122
160
  }
123
161
  const adapter = this.registry.require(parsed.agent);
124
- const panes = await this.terminalProvider.listPanes();
125
- const candidates = panes.filter((candidate) => candidate.kind === parsed.kind && candidate.target === parsed.target);
162
+ const terminals = await this.terminalProvider.listTerminals();
163
+ const candidates = terminals.filter((candidate) => candidate.identity.providerKind === parsed.kind &&
164
+ candidate.route.label === parsed.target);
126
165
  const verified = this.verifyIdentity
127
- ? (await Promise.all(candidates.map(async (pane) => {
128
- const terminalControl = terminalRefFromPane(pane, terminalControlCapabilitiesForAdapter(adapter));
166
+ ? (await Promise.all(candidates.map(async (terminal) => {
167
+ const terminalControl = this.terminalProvider.toControlRef(terminal, terminalControlCapabilitiesForAdapter(adapter));
129
168
  try {
130
169
  const verifiedTerminalControl = await this.verifyTerminalIdentity(adapter.agent, terminalControl, { pid: parsed.pid });
131
- return { pane, terminalControl: verifiedTerminalControl };
170
+ return { terminal, terminalControl: verifiedTerminalControl };
132
171
  }
133
172
  catch {
134
173
  return undefined;
135
174
  }
136
175
  }))).filter((candidate) => candidate !== undefined)
137
- : candidates.slice(0, 1).map((pane) => ({
138
- pane,
139
- terminalControl: terminalRefFromPane(pane, terminalControlCapabilitiesForAdapter(adapter))
176
+ : candidates.slice(0, 1).map((terminal) => ({
177
+ terminal,
178
+ terminalControl: this.terminalProvider.toControlRef(terminal, terminalControlCapabilitiesForAdapter(adapter))
140
179
  }));
141
180
  if (verified.length === 0) {
142
181
  throw new Error(`terminal-controlled session ${parsed.conversationId} is no longer available`);
@@ -188,14 +227,34 @@ export class TerminalAgentBridge {
188
227
  }
189
228
  async send(agent, terminalControl, text, options = {}) {
190
229
  const adapter = this.registry.require(agent);
191
- if (!terminalControl.capabilities.includes("send_keys")) {
192
- throw new TerminalInputNotStartedError(`${adapter.displayName} terminal input is not supported`);
230
+ const multiline = /[\r\n]/u.test(text.trimEnd());
231
+ try {
232
+ assertTerminalMutationCapabilities({
233
+ provider: this.terminalProvider,
234
+ terminal: this.terminalProvider.endpoint(terminalControl),
235
+ semantic: [
236
+ "send_keys",
237
+ ...(adapter.agent === "codex" && multiline
238
+ ? ["screen_status"]
239
+ : [])
240
+ ],
241
+ transport: [
242
+ "stable_resource_resolution",
243
+ "text_delivery",
244
+ "key_delivery",
245
+ ...(adapter.agent === "codex" && multiline
246
+ ? ["screen_capture"]
247
+ : [])
248
+ ]
249
+ });
250
+ }
251
+ catch (error) {
252
+ throw new TerminalInputNotStartedError(error instanceof Error ? error.message : String(error), { cause: error });
193
253
  }
194
254
  const normalized = text.trimEnd();
195
255
  if (!normalized) {
196
256
  throw new TerminalInputNotStartedError("terminal message is empty");
197
257
  }
198
- const multiline = /[\r\n]/u.test(normalized);
199
258
  let verifiedForText;
200
259
  try {
201
260
  verifiedForText = await this.verifyTerminalIdentity(adapter.agent, terminalControl, options.runtime);
@@ -204,9 +263,10 @@ export class TerminalAgentBridge {
204
263
  throw new TerminalInputNotStartedError(error instanceof Error ? error.message : String(error), { cause: error });
205
264
  }
206
265
  try {
207
- await this.terminalProvider.sendText(verifiedForText.target, normalized, {
208
- socketPath: verifiedForText.socketPath
209
- });
266
+ if (!verifiedForText.capabilities.includes("send_keys")) {
267
+ throw new TerminalControlInputNotSentError(`${adapter.displayName} terminal input capability changed before injection`);
268
+ }
269
+ await this.terminalProvider.sendText(this.terminalProvider.endpoint(verifiedForText), normalized);
210
270
  }
211
271
  catch (error) {
212
272
  if (error instanceof TerminalControlInputNotSentError) {
@@ -223,8 +283,9 @@ export class TerminalAgentBridge {
223
283
  multiline
224
284
  });
225
285
  // Text and Enter are separate tmux operations. Revalidate between them and
226
- // never submit after identity or composer drift. The legacy single-line
227
- // path retains its best-effort pre-submit cleanup behavior.
286
+ // never submit after identity or composer drift. A failed revalidation
287
+ // leaves the injected draft untouched because cleanup is also terminal
288
+ // input and cannot safely use a stale route.
228
289
  let verifiedForEnter;
229
290
  if (adapter.agent === "codex" && multiline) {
230
291
  verifiedForEnter = await this.settleCodexMultilineComposer(adapter, terminalControl, normalized, options.runtime);
@@ -234,20 +295,10 @@ export class TerminalAgentBridge {
234
295
  verifiedForEnter = await this.verifyTerminalIdentity(adapter.agent, terminalControl, options.runtime);
235
296
  }
236
297
  catch (error) {
237
- try {
238
- await this.terminalProvider.sendKeys(verifiedForText.target, ["C-u"], {
239
- socketPath: verifiedForText.socketPath
240
- });
241
- }
242
- catch {
243
- // Best effort only: preserving the identity failure is more important than cleanup.
244
- }
245
298
  throw error;
246
299
  }
247
300
  }
248
- await this.terminalProvider.sendKeys(verifiedForEnter.target, ["C-m"], {
249
- socketPath: verifiedForEnter.socketPath
250
- });
301
+ await this.terminalProvider.sendKeys(this.terminalProvider.endpoint(verifiedForEnter), ["C-m"]);
251
302
  await options.onTransportStage?.({
252
303
  stage: "enter_dispatched",
253
304
  agent: adapter.agent,
@@ -272,6 +323,17 @@ export class TerminalAgentBridge {
272
323
  const adapter = this.registry.require(agent);
273
324
  try {
274
325
  assertClosedStatusInspectionPlan(adapter, terminalControl, plan);
326
+ assertTerminalMutationCapabilities({
327
+ provider: this.terminalProvider,
328
+ terminal: this.terminalProvider.endpoint(terminalControl),
329
+ semantic: ["send_keys", "screen_status"],
330
+ transport: [
331
+ "stable_resource_resolution",
332
+ "screen_capture",
333
+ "text_delivery",
334
+ "key_delivery"
335
+ ]
336
+ });
275
337
  }
276
338
  catch (error) {
277
339
  throw nativeInspectionSubmissionError("not_started", error);
@@ -284,7 +346,10 @@ export class TerminalAgentBridge {
284
346
  throw nativeInspectionSubmissionError("not_started", error);
285
347
  }
286
348
  try {
287
- await this.terminalProvider.sendText(verifiedForText.target, plan.command, { socketPath: verifiedForText.socketPath });
349
+ if (!verifiedForText.capabilities.includes("send_keys")) {
350
+ throw new TerminalControlInputNotSentError(`${adapter.displayName} native inspection input capability changed`);
351
+ }
352
+ await this.terminalProvider.sendText(this.terminalProvider.endpoint(verifiedForText), plan.command);
288
353
  }
289
354
  catch (error) {
290
355
  if (error instanceof TerminalControlInputNotSentError) {
@@ -312,7 +377,7 @@ export class TerminalAgentBridge {
312
377
  try {
313
378
  // Exactly one Enter attempt. Any error is submission-uncertain and must
314
379
  // never trigger a fallback executable or a blind second Enter.
315
- await this.terminalProvider.sendKeys(settled.terminalControl.target, ["C-m"], { socketPath: settled.terminalControl.socketPath });
380
+ await this.terminalProvider.sendKeys(this.terminalProvider.endpoint(settled.terminalControl), ["C-m"]);
316
381
  }
317
382
  catch (error) {
318
383
  throw nativeInspectionSubmissionError("enter_uncertain", error);
@@ -415,7 +480,7 @@ export class TerminalAgentBridge {
415
480
  });
416
481
  verified = await observeExactPanel(verified);
417
482
  try {
418
- await this.terminalProvider.sendKeys(verified.target, plan.expectedResult.dismissal.keys, { socketPath: verified.socketPath });
483
+ await this.terminalProvider.sendKeys(this.terminalProvider.endpoint(verified), plan.expectedResult.dismissal.keys);
419
484
  }
420
485
  catch (error) {
421
486
  throw new NativeInspectionDismissalError("native status modal dismissal outcome is uncertain; do not retry automatically", { cause: error });
@@ -437,11 +502,12 @@ export class TerminalAgentBridge {
437
502
  }
438
503
  async settleNativeInspectionComposer(adapter, terminalControl, plan, runtime) {
439
504
  const startedAt = performance.now();
505
+ const settleTimeoutMs = plan.composer.maximumSettleMs;
440
506
  let stableDigest;
441
507
  let stableKind;
442
508
  let stableSince;
443
509
  let stableCaptures = 0;
444
- while (performance.now() - startedAt <= CODEX_MULTILINE_SETTLE_TIMEOUT_MS) {
510
+ while (performance.now() - startedAt <= settleTimeoutMs) {
445
511
  const captured = await this.captureInspection(adapter, terminalControl, {
446
512
  runtime,
447
513
  scrollbackLines: CODEX_MULTILINE_SETTLE_SCROLLBACK_LINES
@@ -479,7 +545,7 @@ export class TerminalAgentBridge {
479
545
  stableCaptures = 0;
480
546
  }
481
547
  const elapsed = performance.now() - startedAt;
482
- const remaining = CODEX_MULTILINE_SETTLE_TIMEOUT_MS - elapsed;
548
+ const remaining = settleTimeoutMs - elapsed;
483
549
  if (remaining <= 0) {
484
550
  break;
485
551
  }
@@ -626,13 +692,14 @@ export class TerminalAgentBridge {
626
692
  */
627
693
  async clearInputLine(agent, terminalControl, options = {}) {
628
694
  const adapter = this.registry.require(agent);
629
- if (!terminalControl.capabilities.includes("send_keys")) {
630
- throw new Error(`${adapter.displayName} terminal input is not supported`);
631
- }
632
- const verified = await this.verifyTerminalIdentity(adapter.agent, terminalControl, options.runtime);
633
- await this.terminalProvider.sendKeys(verified.target, ["C-u"], {
634
- socketPath: verified.socketPath
695
+ assertTerminalMutationCapabilities({
696
+ provider: this.terminalProvider,
697
+ terminal: this.terminalProvider.endpoint(terminalControl),
698
+ semantic: ["send_keys"],
699
+ transport: ["stable_resource_resolution", "key_delivery"]
635
700
  });
701
+ const verified = await this.verifyTerminalIdentity(adapter.agent, terminalControl, options.runtime);
702
+ await this.terminalProvider.sendKeys(this.terminalProvider.endpoint(verified), ["C-u"]);
636
703
  }
637
704
  async cancel(agent, terminalControl, options = {}) {
638
705
  const adapter = this.registry.require(agent);
@@ -657,10 +724,14 @@ export class TerminalAgentBridge {
657
724
  reason: `${adapter.displayName} terminal cancellation is not supported`
658
725
  };
659
726
  }
660
- const verifiedForCancel = await this.verifyTerminalIdentity(adapter.agent, terminalControl, options.runtime);
661
- await this.terminalProvider.sendKeys(verifiedForCancel.target, adapter.cancelKeys, {
662
- socketPath: verifiedForCancel.socketPath
727
+ assertTerminalMutationCapabilities({
728
+ provider: this.terminalProvider,
729
+ terminal: this.terminalProvider.endpoint(terminalControl),
730
+ semantic: ["terminal_cancel", "send_keys"],
731
+ transport: ["stable_resource_resolution", "key_delivery"]
663
732
  });
733
+ const verifiedForCancel = await this.verifyTerminalIdentity(adapter.agent, terminalControl, options.runtime);
734
+ await this.terminalProvider.sendKeys(this.terminalProvider.endpoint(verifiedForCancel), adapter.cancelKeys);
664
735
  return {
665
736
  cancelRequested: true,
666
737
  key: adapter.cancelKeys.length === 1 ? adapter.cancelKeys[0] : undefined,
@@ -677,6 +748,16 @@ export class TerminalAgentBridge {
677
748
  reason: `${adapter.displayName} terminal approval is not supported`
678
749
  };
679
750
  }
751
+ assertTerminalMutationCapabilities({
752
+ provider: this.terminalProvider,
753
+ terminal: this.terminalProvider.endpoint(terminalControl),
754
+ semantic: ["screen_status", "terminal_approval", "send_keys"],
755
+ transport: [
756
+ "stable_resource_resolution",
757
+ "screen_capture",
758
+ "key_delivery"
759
+ ]
760
+ });
680
761
  const captured = await this.captureInspection(adapter, terminalControl, options);
681
762
  const { inspection } = captured;
682
763
  const activeTerminalControl = captured.terminalControl;
@@ -719,10 +800,24 @@ export class TerminalAgentBridge {
719
800
  screenExcerpt: inspection.screenExcerpt
720
801
  };
721
802
  }
722
- const fingerprint = terminalApprovalFingerprint(adapter.agent, activeTerminalControl, inspection, {
803
+ const canonicalFingerprint = terminalApprovalFingerprint(adapter.agent, activeTerminalControl, inspection, {
723
804
  screen: captured.screen,
724
805
  runtime: options.runtime
725
806
  });
807
+ const legacyFingerprint = hasCanonicalTerminalEndpoint(terminalControl)
808
+ ? undefined
809
+ : terminalApprovalFingerprint(adapter.agent, terminalControl, inspection, {
810
+ screen: captured.screen,
811
+ runtime: options.runtime
812
+ });
813
+ const useLegacyFingerprint = Boolean(options.expectedFingerprint &&
814
+ options.expectedFingerprint === legacyFingerprint);
815
+ const fingerprintControl = useLegacyFingerprint
816
+ ? terminalControl
817
+ : activeTerminalControl;
818
+ const fingerprint = useLegacyFingerprint
819
+ ? legacyFingerprint
820
+ : canonicalFingerprint;
726
821
  if (adapter.agent === "claude" &&
727
822
  decisionMode === "keys" &&
728
823
  !options.expectedFingerprint) {
@@ -809,7 +904,7 @@ export class TerminalAgentBridge {
809
904
  };
810
905
  }
811
906
  const recapturedDecisionMode = recapturedInspection.approval.action.mode ?? "keys";
812
- const recapturedFingerprint = terminalApprovalFingerprint(adapter.agent, recaptured.terminalControl, recapturedInspection, {
907
+ const recapturedFingerprint = terminalApprovalFingerprint(adapter.agent, fingerprintControl, recapturedInspection, {
813
908
  screen: recaptured.screen,
814
909
  runtime: options.runtime
815
910
  });
@@ -892,7 +987,7 @@ export class TerminalAgentBridge {
892
987
  };
893
988
  }
894
989
  const afterReservationMode = afterReservationInspection.approval.action.mode ?? "keys";
895
- const afterReservationFingerprint = terminalApprovalFingerprint(adapter.agent, afterReservation.terminalControl, afterReservationInspection, {
990
+ const afterReservationFingerprint = terminalApprovalFingerprint(adapter.agent, fingerprintControl, afterReservationInspection, {
896
991
  screen: afterReservation.screen,
897
992
  runtime: options.runtime
898
993
  });
@@ -934,7 +1029,7 @@ export class TerminalAgentBridge {
934
1029
  if (!sameTerminalControlIdentity(dispatchTerminalControl, verifiedImmediatelyBeforeSend)) {
935
1030
  throw new Error("terminal control identity changed after the final approval capture");
936
1031
  }
937
- await this.terminalProvider.sendKeys(verifiedImmediatelyBeforeSend.target, dispatchApproval.action.keys, { socketPath: verifiedImmediatelyBeforeSend.socketPath });
1032
+ await this.terminalProvider.sendKeys(this.terminalProvider.endpoint(verifiedImmediatelyBeforeSend), dispatchApproval.action.keys);
938
1033
  return {
939
1034
  approved: true,
940
1035
  blocked: false,
@@ -1007,10 +1102,7 @@ export class TerminalAgentBridge {
1007
1102
  }
1008
1103
  async captureInspection(adapter, terminalControl, options = {}) {
1009
1104
  const verifiedTerminalControl = await this.verifyTerminalIdentity(adapter.agent, terminalControl, options.runtime);
1010
- const screen = await this.terminalProvider.capture(verifiedTerminalControl.target, {
1011
- scrollbackLines: options.scrollbackLines ?? 120,
1012
- socketPath: verifiedTerminalControl.socketPath
1013
- });
1105
+ const screen = await this.terminalProvider.capture(this.terminalProvider.endpoint(verifiedTerminalControl), { scrollbackLines: options.scrollbackLines ?? 120 });
1014
1106
  return {
1015
1107
  terminalControl: verifiedTerminalControl,
1016
1108
  screen,
@@ -1025,21 +1117,46 @@ export class TerminalAgentBridge {
1025
1117
  };
1026
1118
  }
1027
1119
  async verifyTerminalIdentity(agent, terminalControl, runtime) {
1028
- if (!this.verifyIdentity) {
1029
- return terminalControl;
1120
+ let verifiedControl = terminalControl;
1121
+ if (this.verifyIdentity) {
1122
+ if (!Number.isInteger(runtime?.pid) || Number(runtime?.pid) <= 0) {
1123
+ throw new Error(`refusing terminal access for ${agent}:${terminalControl.target} without an exact agent pid; reattach this legacy tmux session before controlling it`);
1124
+ }
1125
+ const result = await this.verifyIdentity({
1126
+ agent,
1127
+ pid: Number(runtime?.pid),
1128
+ terminalControl,
1129
+ runtime
1130
+ });
1131
+ verifiedControl = result?.terminalControl ?? terminalControl;
1132
+ if (!sameTerminalControlIncarnation(terminalControl, verifiedControl)) {
1133
+ throw new Error("terminal control identity changed during identity verification");
1134
+ }
1030
1135
  }
1031
- if (!Number.isInteger(runtime?.pid) || Number(runtime?.pid) <= 0) {
1032
- throw new Error(`refusing terminal access for ${agent}:${terminalControl.target} without an exact agent pid; reattach this legacy tmux session before controlling it`);
1136
+ const verifiedEndpoint = this.terminalProvider.endpoint(verifiedControl);
1137
+ const resolvedEndpoint = await this.terminalProvider.resolve(verifiedEndpoint);
1138
+ if (hasCanonicalTerminalEndpoint(verifiedControl) &&
1139
+ !sameTerminalControlIncarnation(verifiedEndpoint, resolvedEndpoint)) {
1140
+ throw new Error("terminal stable resource or process anchor changed during fresh resolution");
1033
1141
  }
1034
- const result = await this.verifyIdentity({
1035
- agent,
1036
- pid: Number(runtime?.pid),
1037
- terminalControl,
1038
- runtime
1039
- });
1040
- return result?.terminalControl ?? terminalControl;
1142
+ return this.terminalProvider.toControlRef(resolvedEndpoint, verifiedControl.capabilities);
1041
1143
  }
1042
1144
  }
1145
+ function assertTerminalMutationCapabilities({ provider, terminal, semantic, transport }) {
1146
+ const missingSemantic = semantic.filter((capability) => !provider.supportedCapabilities.includes(capability) ||
1147
+ !terminal.capabilities.includes(capability));
1148
+ const missingTransport = transport.filter((capability) => !provider.providerCapabilities.includes(capability));
1149
+ if (missingSemantic.length === 0 && missingTransport.length === 0) {
1150
+ return;
1151
+ }
1152
+ const missing = [
1153
+ ...missingSemantic.map((value) => `terminal:${value}`),
1154
+ ...missingTransport.map((value) => `provider:${value}`)
1155
+ ];
1156
+ throw new TerminalControlInputNotSentError(`terminal action capability preflight failed for ` +
1157
+ `${terminal.identity.providerKind}:${terminal.route.label}: ` +
1158
+ missing.join(", "));
1159
+ }
1043
1160
  function nativeInspectionSubmissionError(stage, error) {
1044
1161
  if (error instanceof NativeInspectionSubmissionError) {
1045
1162
  const stageRank = {
@@ -1063,19 +1180,26 @@ function assertClosedStatusInspectionPlan(adapter, terminalControl, plan) {
1063
1180
  if (!terminalControl.capabilities.includes("screen_status")) {
1064
1181
  throw new Error(`${adapter.displayName} terminal screen inspection is not supported`);
1065
1182
  }
1066
- const codexProfile = adapter.agent === "codex" && [
1067
- "codex-tui-0.146.0",
1068
- "codex-tui-0.146.1"
1069
- ].includes(plan.behaviorProfile);
1183
+ const codexProfile = adapter.agent === "codex" &&
1184
+ CODEX_NATIVE_STATUS_POPUP_BY_PROFILE[plan.behaviorProfile] !== undefined;
1070
1185
  const claudeProfile = adapter.agent === "claude" &&
1071
- plan.behaviorProfile === "claude-code-2.1.218-native-status";
1186
+ CLAUDE_NATIVE_STATUS_POPUP_BY_PROFILE[plan.behaviorProfile] !== undefined;
1187
+ const expectedSettle = codexProfile
1188
+ ? { minimumStableMs: CODEX_PASTE_ENTER_SETTLE_MS, maximumSettleMs: 2_000 }
1189
+ : plan.behaviorProfile === "claude-code-2.1.218-native-status"
1190
+ ? { minimumStableMs: 80, maximumSettleMs: 2_000 }
1191
+ : plan.behaviorProfile === "claude-code-2.1.226-native-status"
1192
+ ? { minimumStableMs: 80, maximumSettleMs: 5_000 }
1193
+ : undefined;
1072
1194
  const exactPresentation = codexProfile
1073
1195
  ? plan.expectedResult.presentation === "inline" &&
1074
1196
  plan.expectedResult.dismissal === undefined &&
1075
- plan.composer.minimumStableMs >= CODEX_PASTE_ENTER_SETTLE_MS
1197
+ plan.composer.minimumStableMs === expectedSettle?.minimumStableMs &&
1198
+ plan.composer.maximumSettleMs === expectedSettle.maximumSettleMs
1076
1199
  : claudeProfile
1077
1200
  ? plan.expectedResult.presentation === "modal" &&
1078
- plan.composer.minimumStableMs >= 80 &&
1201
+ plan.composer.minimumStableMs === expectedSettle?.minimumStableMs &&
1202
+ plan.composer.maximumSettleMs === expectedSettle.maximumSettleMs &&
1079
1203
  plan.expectedResult.dismissal?.expected === "idle_empty_composer" &&
1080
1204
  JSON.stringify(plan.expectedResult.dismissal.keys) ===
1081
1205
  JSON.stringify(["Escape"])
@@ -1087,6 +1211,8 @@ function assertClosedStatusInspectionPlan(adapter, terminalControl, plan) {
1087
1211
  plan.composer.kind !== "exact" ||
1088
1212
  !Number.isFinite(plan.composer.minimumStableMs) ||
1089
1213
  plan.composer.minimumStableMs < 0 ||
1214
+ !Number.isFinite(plan.composer.maximumSettleMs) ||
1215
+ plan.composer.maximumSettleMs < plan.composer.minimumStableMs ||
1090
1216
  plan.expectedResult.kind !== "native_status" ||
1091
1217
  !exactPresentation) {
1092
1218
  throw new Error("refusing a non-closed or unverified native inspection plan");
@@ -1146,9 +1272,8 @@ function exactCodexNativeInspectionComposerCapture(screen, plan) {
1146
1272
  if (popupRows.length === 0) {
1147
1273
  kind = "exact_slash_composer";
1148
1274
  }
1149
- else if (popupRows.length === 1 &&
1150
- popupRows[0].trimEnd() ===
1151
- CODEX_NATIVE_STATUS_POPUP_BY_PROFILE[plan.behaviorProfile]) {
1275
+ else if (JSON.stringify(popupRows.map((line) => line.trimEnd())) ===
1276
+ JSON.stringify(CODEX_NATIVE_STATUS_POPUP_BY_PROFILE[plan.behaviorProfile])) {
1152
1277
  kind = "exact_slash_popup";
1153
1278
  }
1154
1279
  else {
@@ -1189,7 +1314,7 @@ function exactClaudeNativeInspectionComposerCapture(screen, plan) {
1189
1314
  return undefined;
1190
1315
  }
1191
1316
  }
1192
- if (JSON.stringify(suggestions) !== JSON.stringify(CLAUDE_NATIVE_STATUS_POPUP_BY_PROFILE[plan.behaviorProfile])) {
1317
+ if (!closedClaudeNativeStatusSuggestionsMatch(suggestions, CLAUDE_NATIVE_STATUS_POPUP_BY_PROFILE[plan.behaviorProfile])) {
1193
1318
  return undefined;
1194
1319
  }
1195
1320
  kind = "exact_slash_popup";
@@ -1202,7 +1327,33 @@ function exactClaudeNativeInspectionComposerCapture(screen, plan) {
1202
1327
  };
1203
1328
  }
1204
1329
  /**
1205
- * Prove the exact idle input frame from the verified Claude Code 2.1.218
1330
+ * Claude truncates a suggestion row with a Unicode ellipsis at narrow pane
1331
+ * widths. Keep authorization closed by accepting only an exact ordered row or
1332
+ * an explicit ellipsis whose preceding text is an exact, non-trivial prefix of
1333
+ * that same profiled row. A caller still cannot introduce, omit, or reorder a
1334
+ * slash command.
1335
+ */
1336
+ function closedClaudeNativeStatusSuggestionsMatch(observed, expected) {
1337
+ if (!expected || observed.length !== expected.length) {
1338
+ return false;
1339
+ }
1340
+ return observed.every((row, index) => {
1341
+ const exact = expected[index];
1342
+ if (row === exact) {
1343
+ return true;
1344
+ }
1345
+ if (!row.endsWith("…")) {
1346
+ return false;
1347
+ }
1348
+ const prefix = row.slice(0, -1);
1349
+ const commandEnd = exact.indexOf(" ");
1350
+ return (commandEnd > 0 &&
1351
+ prefix.length >= commandEnd + 12 &&
1352
+ exact.startsWith(prefix));
1353
+ });
1354
+ }
1355
+ /**
1356
+ * Prove the exact idle input frame from a verified Claude Code native-status
1206
1357
  * profile. A loose or historical `❯` prompt is not enough to authorize even
1207
1358
  * the initial literal `/status` injection.
1208
1359
  */
@@ -1383,13 +1534,7 @@ async function terminalSettleDelay(milliseconds) {
1383
1534
  await new Promise((resolve) => setTimeout(resolve, milliseconds));
1384
1535
  }
1385
1536
  function sameTerminalControlIdentity(left, right) {
1386
- return left.kind === right.kind &&
1387
- left.target === right.target &&
1388
- left.socketPath === right.socketPath &&
1389
- left.session === right.session &&
1390
- left.window === right.window &&
1391
- left.pane === right.pane &&
1392
- left.panePid === right.panePid;
1537
+ return sameTerminalControlIncarnation(left, right);
1393
1538
  }
1394
1539
  export function terminalApprovalFingerprint(agent, terminalControl, inspection, options = {}) {
1395
1540
  if (!inspection.approval.approvable) {
@@ -1399,18 +1544,28 @@ export function terminalApprovalFingerprint(agent, terminalControl, inspection,
1399
1544
  const rawScreenDigest = decisionMode === "keys" && options.screen !== undefined
1400
1545
  ? createHash("sha256").update(options.screen).digest("hex")
1401
1546
  : undefined;
1402
- return createHash("sha256")
1403
- .update(JSON.stringify({
1404
- agent,
1405
- provider: "tmux",
1406
- terminal: {
1547
+ const terminal = terminalEndpointFromControlRef(terminalControl);
1548
+ // A pending v0.11.x approval may outlive an upgrade. Preserve its exact
1549
+ // fingerprint until the persisted control record has canonical endpoint
1550
+ // evidence; new records bind the fingerprint to stable endpoint identity.
1551
+ const terminalFingerprint = hasCanonicalTerminalEndpoint(terminalControl)
1552
+ ? {
1553
+ identity: terminalEndpointIdentityKey(terminal),
1554
+ process_anchor_pid: terminal.processAnchorPid
1555
+ }
1556
+ : {
1407
1557
  target: terminalControl.target,
1408
1558
  socket_path: terminalControl.socketPath,
1409
1559
  session: terminalControl.session,
1410
1560
  window: terminalControl.window,
1411
1561
  pane: terminalControl.pane,
1412
1562
  pane_pid: terminalControl.panePid
1413
- },
1563
+ };
1564
+ return createHash("sha256")
1565
+ .update(JSON.stringify({
1566
+ agent,
1567
+ provider: terminal.identity.providerKind,
1568
+ terminal: terminalFingerprint,
1414
1569
  runtime: {
1415
1570
  pid: options.runtime?.pid,
1416
1571
  session_id: options.runtime?.sessionId,