tmux-ide 2.9.0-beta.21 → 2.9.0-beta.26

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 (209) hide show
  1. package/bin/cli.js +46791 -43589
  2. package/package.json +2 -2
  3. package/packages/contracts/src/__tests__/daemon-wire.test.ts +2 -2
  4. package/packages/contracts/src/__tests__/fixtures/daemon-wire-v3.json +35 -0
  5. package/packages/contracts/src/__tests__/issue-error.test.ts +1 -1
  6. package/packages/contracts/src/__tests__/multiplexer-verbs.test.ts +3 -1
  7. package/packages/contracts/src/__tests__/pane-stream.test.ts +17 -5
  8. package/packages/contracts/src/__tests__/tmux-server-scope.test.ts +166 -0
  9. package/packages/contracts/src/__tests__/window-links.test.ts +243 -0
  10. package/packages/contracts/src/actions-contract.ts +12 -0
  11. package/packages/contracts/src/daemon-resource-request.test.ts +1 -1
  12. package/packages/contracts/src/daemon-wire.ts +10 -5
  13. package/packages/contracts/src/fleet-client-state.ts +3 -0
  14. package/packages/contracts/src/fleet-lifecycle.ts +5 -0
  15. package/packages/contracts/src/index.ts +2 -0
  16. package/packages/contracts/src/interaction-receipts.ts +39 -0
  17. package/packages/contracts/src/issue-error.ts +1 -1
  18. package/packages/contracts/src/multiplexer-verbs.ts +25 -1
  19. package/packages/contracts/src/pane-stream.ts +51 -8
  20. package/packages/contracts/src/tmux-server-scope.ts +172 -0
  21. package/packages/contracts/src/window-links.ts +76 -0
  22. package/packages/contracts/src/workspace-multiplexer.ts +54 -0
  23. package/packages/core/src/interaction-receipts.ts +4 -0
  24. package/packages/daemon/dist/command-center/actions/command-definitions.js +6 -0
  25. package/packages/daemon/dist/command-center/actions/handlers/workspace-multiplexer.js +2 -0
  26. package/packages/daemon/dist/command-center/actions/registry.js +13 -1
  27. package/packages/daemon/dist/command-center/discovery.js +2 -9
  28. package/packages/daemon/dist/command-center/server.js +21 -1
  29. package/packages/daemon/dist/command-center/tmux-server-native-backing.js +37 -0
  30. package/packages/daemon/dist/command-center/tmux-servers.js +351 -0
  31. package/packages/daemon/dist/lib/canonical-daemon-bootstrap.js +64 -8
  32. package/packages/daemon/dist/lib/canonical-daemon.js +5 -2
  33. package/packages/daemon/dist/lib/daemon-embed.js +198 -30
  34. package/packages/daemon/dist/lib/embedded-tmux-server-owners.js +147 -0
  35. package/packages/daemon/dist/lib/fleet-lifecycle-authority.js +31 -11
  36. package/packages/daemon/dist/lib/headless-daemon.js +10 -1
  37. package/packages/daemon/dist/lib/runtime-namespace.js +17 -3
  38. package/packages/daemon/dist/lib/tmux-client-execution.js +59 -0
  39. package/packages/daemon/dist/lib/tmux-named-socket-fence.js +4 -0
  40. package/packages/daemon/dist/lib/tmux-server-generation-runner.js +62 -0
  41. package/packages/daemon/dist/lib/tmux-server-owner.js +285 -0
  42. package/packages/daemon/dist/lib/tmux-server-owners.js +192 -0
  43. package/packages/daemon/dist/lib/tmux-server-proof.js +82 -0
  44. package/packages/daemon/dist/lib/tmux-server-registration.js +85 -0
  45. package/packages/daemon/dist/lib/tmux-server-session-create.js +43 -0
  46. package/packages/daemon/dist/lib/tmux-server-session-open.js +101 -0
  47. package/packages/daemon/dist/lib/tmux-servers-cli.js +134 -0
  48. package/packages/daemon/dist/lib/tmux-session-mutation-fence.js +55 -0
  49. package/packages/daemon/dist/lib/tmux-window-link-guard.js +88 -0
  50. package/packages/daemon/dist/lib/workspace-config-loader.js +3 -0
  51. package/packages/daemon/dist/lib/workspace-multiplexer-verbs.js +82 -1
  52. package/packages/daemon/dist/lib/workspace-pane-creation.js +13 -57
  53. package/packages/daemon/dist/lib/workspace-promotion.js +91 -12
  54. package/packages/daemon/dist/lib/workspace-registry.js +4 -0
  55. package/packages/daemon/dist/server/pane-stream-upgrade.js +20 -6
  56. package/packages/daemon/dist/terminal/attachments/native-runtime.js +61 -17
  57. package/packages/daemon/dist/terminal/mirror/__tests__/scripted-channel.js +9 -1
  58. package/packages/daemon/dist/terminal/mirror/__tests__/simulated-channel.js +11 -2
  59. package/packages/daemon/dist/terminal/mirror/control-channel.js +65 -6
  60. package/packages/daemon/dist/terminal/mirror/mirror-service.js +44 -0
  61. package/packages/daemon/dist/terminal/mirror/session-channel.js +210 -50
  62. package/packages/daemon/dist/terminal/mirror/window-link-authority.js +160 -0
  63. package/packages/daemon/dist/terminal/pane-stream/lease-manager.js +3 -3
  64. package/packages/daemon/dist/terminal/pane-stream/pane-stream-websocket.js +40 -6
  65. package/packages/daemon/dist/terminal/protocol/live-session-identity.js +9 -0
  66. package/packages/daemon/dist/terminal/protocol/native-backing-client.js +2 -1
  67. package/packages/daemon/dist/terminal/session-runtime/interaction-receipt-facts.js +9 -0
  68. package/packages/daemon/dist/terminal/session-runtime/registry.js +6 -0
  69. package/packages/daemon/dist/terminal/session-runtime/semantic-mutation-executor.js +1 -1
  70. package/packages/daemon/dist/terminal/session-runtime/semantic-mutation-resource-changes.js +1 -0
  71. package/packages/daemon/dist/tui/mirror/application-shell-daemon-connection.js +7 -2
  72. package/packages/daemon/dist/tui/mirror/application-shell-server-connection.js +114 -0
  73. package/packages/daemon/dist/tui/mirror/menu-model.js +1 -1
  74. package/packages/daemon/dist/tui/mirror/open-tui-verified-routing.js +34 -4
  75. package/packages/daemon/dist/tui/mirror/open-tui-workspace-runtime-port.js +29 -8
  76. package/packages/daemon/dist/tui/mirror/runtime/application-guided-tour-integration.jsx +119 -0
  77. package/packages/daemon/dist/tui/mirror/runtime/application-guided-tour-owner.js +132 -0
  78. package/packages/daemon/dist/tui/mirror/runtime/application-home-agent-roster.jsx +21 -9
  79. package/packages/daemon/dist/tui/mirror/runtime/application-home-agent-transport.js +63 -1
  80. package/packages/daemon/dist/tui/mirror/runtime/application-home-agents-owner.js +8 -2
  81. package/packages/daemon/dist/tui/mirror/runtime/application-home-agents.js +3 -1
  82. package/packages/daemon/dist/tui/mirror/runtime/application-home-catalog-owner.js +11 -5
  83. package/packages/daemon/dist/tui/mirror/runtime/application-home-catalog.js +140 -75
  84. package/packages/daemon/dist/tui/mirror/runtime/application-home-experience.js +40 -0
  85. package/packages/daemon/dist/tui/mirror/runtime/application-home-fleet.js +132 -0
  86. package/packages/daemon/dist/tui/mirror/runtime/application-machine-agents.js +9 -0
  87. package/packages/daemon/dist/tui/mirror/runtime/application-machine-catalog.js +12 -2
  88. package/packages/daemon/dist/tui/mirror/runtime/application-machine-navigation.js +211 -31
  89. package/packages/daemon/dist/tui/mirror/runtime/application-machine-sidebar.jsx +91 -37
  90. package/packages/daemon/dist/tui/mirror/runtime/application-performance-log.js +20 -7
  91. package/packages/daemon/dist/tui/mirror/runtime/application-root-configuration.js +9 -3
  92. package/packages/daemon/dist/tui/mirror/runtime/application-route-connection.js +95 -2
  93. package/packages/daemon/dist/tui/mirror/runtime/application-shell-home.jsx +6 -2
  94. package/packages/daemon/dist/tui/mirror/runtime/application-terminal-interaction-controller.js +158 -2
  95. package/packages/daemon/dist/tui/mirror/runtime/application-terminal-workspace-policy.js +43 -0
  96. package/packages/daemon/dist/tui/mirror/runtime/application-terminal-workspace.jsx +78 -25
  97. package/packages/daemon/dist/tui/mirror/runtime/application-theme-repaint.js +9 -0
  98. package/packages/daemon/dist/tui/mirror/runtime/fleet-lifecycle-client.js +44 -1
  99. package/packages/daemon/dist/tui/mirror/runtime/guided-tour-coach.jsx +54 -0
  100. package/packages/daemon/dist/tui/mirror/runtime/guided-tour-storage.js +67 -0
  101. package/packages/daemon/dist/tui/mirror/runtime/guided-tour.js +113 -0
  102. package/packages/daemon/dist/tui/mirror/runtime/open-tui-generation-host.js +1 -1
  103. package/packages/daemon/dist/tui/mirror/runtime/resize-terminal-pane.js +4 -0
  104. package/packages/daemon/dist/tui/mirror/runtime/select-terminal-pane.js +2 -1
  105. package/packages/daemon/dist/tui/mirror/runtime/semantic-shell-viewport-resize.js +12 -4
  106. package/packages/daemon/dist/tui/mirror/runtime/terminal-pane-input-router.js +6 -3
  107. package/packages/daemon/dist/tui/mirror/workspace/application-command-description.js +11 -2
  108. package/packages/daemon/dist/tui/mirror/workspace/terminal-window-strip.jsx +2 -1
  109. package/packages/daemon/src/command-center/actions/command-definitions.ts +6 -0
  110. package/packages/daemon/src/command-center/actions/errors.ts +4 -0
  111. package/packages/daemon/src/command-center/actions/handlers/workspace-multiplexer.ts +2 -0
  112. package/packages/daemon/src/command-center/actions/registry.ts +14 -0
  113. package/packages/daemon/src/command-center/discovery.ts +2 -10
  114. package/packages/daemon/src/command-center/server.ts +29 -1
  115. package/packages/daemon/src/command-center/tmux-server-native-backing.ts +49 -0
  116. package/packages/daemon/src/command-center/tmux-servers.ts +479 -0
  117. package/packages/daemon/src/lib/canonical-daemon-bootstrap.ts +111 -4
  118. package/packages/daemon/src/lib/canonical-daemon.ts +15 -3
  119. package/packages/daemon/src/lib/daemon-embed.ts +241 -28
  120. package/packages/daemon/src/lib/embedded-tmux-server-owners.ts +190 -0
  121. package/packages/daemon/src/lib/fleet-lifecycle-authority.ts +33 -9
  122. package/packages/daemon/src/lib/headless-daemon.ts +18 -1
  123. package/packages/daemon/src/lib/runtime-namespace.ts +24 -3
  124. package/packages/daemon/src/lib/tmux-client-execution.ts +56 -0
  125. package/packages/daemon/src/lib/tmux-named-socket-fence.ts +5 -0
  126. package/packages/daemon/src/lib/tmux-server-generation-runner.ts +81 -0
  127. package/packages/daemon/src/lib/tmux-server-owner.ts +336 -0
  128. package/packages/daemon/src/lib/tmux-server-owners.ts +228 -0
  129. package/packages/daemon/src/lib/tmux-server-proof.ts +95 -0
  130. package/packages/daemon/src/lib/tmux-server-registration.ts +105 -0
  131. package/packages/daemon/src/lib/tmux-server-session-create.ts +52 -0
  132. package/packages/daemon/src/lib/tmux-server-session-open.ts +106 -0
  133. package/packages/daemon/src/lib/tmux-servers-cli.ts +186 -0
  134. package/packages/daemon/src/lib/tmux-session-mutation-fence.ts +62 -0
  135. package/packages/daemon/src/lib/tmux-window-link-guard.ts +112 -0
  136. package/packages/daemon/src/lib/workspace-config-loader.ts +3 -0
  137. package/packages/daemon/src/lib/workspace-multiplexer-verbs.ts +121 -1
  138. package/packages/daemon/src/lib/workspace-pane-creation.ts +16 -55
  139. package/packages/daemon/src/lib/workspace-promotion.ts +98 -12
  140. package/packages/daemon/src/lib/workspace-registry.ts +5 -0
  141. package/packages/daemon/src/server/pane-stream-upgrade.ts +25 -5
  142. package/packages/daemon/src/terminal/attachments/native-runtime.ts +70 -16
  143. package/packages/daemon/src/terminal/mirror/control-channel.ts +64 -7
  144. package/packages/daemon/src/terminal/mirror/events.ts +2 -0
  145. package/packages/daemon/src/terminal/mirror/mirror-service.ts +56 -0
  146. package/packages/daemon/src/terminal/mirror/session-channel.ts +245 -50
  147. package/packages/daemon/src/terminal/mirror/window-link-authority.ts +203 -0
  148. package/packages/daemon/src/terminal/pane-stream/lease-manager.ts +3 -3
  149. package/packages/daemon/src/terminal/pane-stream/pane-stream-websocket.ts +50 -6
  150. package/packages/daemon/src/terminal/protocol/live-session-identity.ts +14 -0
  151. package/packages/daemon/src/terminal/protocol/native-backing-client.ts +4 -1
  152. package/packages/daemon/src/terminal/session-runtime/interaction-receipt-facts.ts +9 -0
  153. package/packages/daemon/src/terminal/session-runtime/registry.ts +9 -0
  154. package/packages/daemon/src/terminal/session-runtime/semantic-mutation-executor.ts +2 -2
  155. package/packages/daemon/src/terminal/session-runtime/semantic-mutation-resource-changes.ts +1 -0
  156. package/packages/daemon/src/tui/mirror/application-shell-daemon-connection.ts +28 -2
  157. package/packages/daemon/src/tui/mirror/application-shell-server-connection.ts +154 -0
  158. package/packages/daemon/src/tui/mirror/menu-model.ts +1 -1
  159. package/packages/daemon/src/tui/mirror/open-tui-verified-routing.ts +41 -3
  160. package/packages/daemon/src/tui/mirror/open-tui-workspace-runtime-port.ts +47 -6
  161. package/packages/daemon/src/tui/mirror/runtime/application-entry.ts +2 -1
  162. package/packages/daemon/src/tui/mirror/runtime/application-fleet-session-actions.tsx +10 -4
  163. package/packages/daemon/src/tui/mirror/runtime/application-fleet-tabs.ts +2 -0
  164. package/packages/daemon/src/tui/mirror/runtime/application-guided-tour-integration.tsx +199 -0
  165. package/packages/daemon/src/tui/mirror/runtime/application-guided-tour-owner.ts +162 -0
  166. package/packages/daemon/src/tui/mirror/runtime/application-home-agent-roster.tsx +36 -10
  167. package/packages/daemon/src/tui/mirror/runtime/application-home-agent-transport.ts +69 -0
  168. package/packages/daemon/src/tui/mirror/runtime/application-home-agents-owner.ts +12 -2
  169. package/packages/daemon/src/tui/mirror/runtime/application-home-agents.ts +13 -2
  170. package/packages/daemon/src/tui/mirror/runtime/application-home-catalog-owner.ts +13 -3
  171. package/packages/daemon/src/tui/mirror/runtime/application-home-catalog.ts +172 -100
  172. package/packages/daemon/src/tui/mirror/runtime/application-home-experience.ts +59 -0
  173. package/packages/daemon/src/tui/mirror/runtime/application-home-fleet.ts +175 -0
  174. package/packages/daemon/src/tui/mirror/runtime/application-machine-agents.ts +9 -1
  175. package/packages/daemon/src/tui/mirror/runtime/application-machine-catalog.ts +19 -7
  176. package/packages/daemon/src/tui/mirror/runtime/application-machine-navigation.ts +285 -36
  177. package/packages/daemon/src/tui/mirror/runtime/application-machine-overlays.tsx +1 -1
  178. package/packages/daemon/src/tui/mirror/runtime/application-machine-sidebar.tsx +124 -40
  179. package/packages/daemon/src/tui/mirror/runtime/application-palette-preview.tsx +3 -0
  180. package/packages/daemon/src/tui/mirror/runtime/application-performance-log.ts +18 -6
  181. package/packages/daemon/src/tui/mirror/runtime/application-root-configuration.ts +12 -3
  182. package/packages/daemon/src/tui/mirror/runtime/application-root-v2.tsx +40 -40
  183. package/packages/daemon/src/tui/mirror/runtime/application-route-connection.ts +93 -1
  184. package/packages/daemon/src/tui/mirror/runtime/application-shell-catalog.tsx +4 -0
  185. package/packages/daemon/src/tui/mirror/runtime/application-shell-home.tsx +20 -1
  186. package/packages/daemon/src/tui/mirror/runtime/application-shell-view.tsx +10 -0
  187. package/packages/daemon/src/tui/mirror/runtime/application-terminal-interaction-controller.ts +177 -2
  188. package/packages/daemon/src/tui/mirror/runtime/application-terminal-workspace-policy.ts +62 -0
  189. package/packages/daemon/src/tui/mirror/runtime/application-terminal-workspace.tsx +87 -30
  190. package/packages/daemon/src/tui/mirror/runtime/application-theme-repaint.ts +19 -0
  191. package/packages/daemon/src/tui/mirror/runtime/fleet-lifecycle-client.ts +55 -0
  192. package/packages/daemon/src/tui/mirror/runtime/guided-tour-coach.tsx +118 -0
  193. package/packages/daemon/src/tui/mirror/runtime/guided-tour-storage.ts +72 -0
  194. package/packages/daemon/src/tui/mirror/runtime/guided-tour.ts +147 -0
  195. package/packages/daemon/src/tui/mirror/runtime/open-tui-generation-host.ts +1 -1
  196. package/packages/daemon/src/tui/mirror/runtime/resize-terminal-pane.ts +3 -0
  197. package/packages/daemon/src/tui/mirror/runtime/select-terminal-pane.ts +3 -0
  198. package/packages/daemon/src/tui/mirror/runtime/semantic-shell-viewport-resize.ts +11 -4
  199. package/packages/daemon/src/tui/mirror/runtime/terminal-pane-input-router.ts +15 -4
  200. package/packages/daemon/src/tui/mirror/workspace/application-command-description.ts +13 -2
  201. package/packages/daemon/src/tui/mirror/workspace/terminal-window-strip.tsx +5 -1
  202. package/packages/daemon-client/package.json +3 -1
  203. package/packages/daemon-client/src/index.ts +2 -0
  204. package/packages/daemon-client/src/pane-stream-client.test.ts +108 -38
  205. package/packages/daemon-client/src/pane-stream-client.ts +26 -0
  206. package/packages/daemon-client/src/scoped-tmux-server-transport.test.ts +114 -0
  207. package/packages/daemon-client/src/scoped-tmux-server-transport.ts +385 -0
  208. package/packages/daemon-client/src/tmux-server-client.test.ts +279 -0
  209. package/packages/daemon-client/src/tmux-server-client.ts +287 -0
@@ -67,6 +67,7 @@ const PANE_VERIFY_FORMAT = [
67
67
  "#{pane_id}",
68
68
  `#{${SEMANTIC_PANE_OPTION}}`,
69
69
  `#{${SEMANTIC_WINDOW_OPTION}}`,
70
+ "#{window_index}",
70
71
  SENTINEL,
71
72
  ].join(FIELD);
72
73
  const ERROR_MESSAGES = {
@@ -223,8 +224,10 @@ function parseVerifyPanes(output) {
223
224
  const fields = line.split(FIELD);
224
225
  const windowPaneCount = positiveInteger(fields[3] ?? "");
225
226
  const sessionWindowCount = positiveInteger(fields[4] ?? "");
226
- if (fields.length !== 9 ||
227
- fields[8] !== SENTINEL ||
227
+ if (fields.length !== 10 ||
228
+ fields[9] !== SENTINEL ||
229
+ !/^(0|[1-9][0-9]*)$/u.test(fields[8]) ||
230
+ !Number.isSafeInteger(Number(fields[8])) ||
228
231
  !/^\$[0-9]+$/u.test(fields[0]) ||
229
232
  !/^@[0-9]+$/u.test(fields[1]) ||
230
233
  !/^%[0-9]+$/u.test(fields[5]) ||
@@ -243,6 +246,7 @@ function parseVerifyPanes(output) {
243
246
  paneId: fields[5],
244
247
  semanticPaneId: fields[6],
245
248
  semanticWindowId: fields[7],
249
+ windowIndex: Number(fields[8]),
246
250
  });
247
251
  }
248
252
  return panes;
@@ -368,9 +372,7 @@ export class WorkspacePromotionAuthority {
368
372
  // promotion is the app's only repair verb — so reconcile the stamps here
369
373
  // before resolving to a `replayed` outcome. Stamping is additive and never
370
374
  // overwrites a valid stamp, so a healthy session is untouched.
371
- const alreadyRegistered = this.#registry
372
- .list()
373
- .find((workspace) => workspace.sessionName === session.sessionName);
375
+ const alreadyRegistered = this.#registeredSession(session.sessionName);
374
376
  if (alreadyRegistered) {
375
377
  const registeredIdentity = {
376
378
  workspaceName: alreadyRegistered.name,
@@ -390,6 +392,39 @@ export class WorkspacePromotionAuthority {
390
392
  this.#assertActive(request.operationId);
391
393
  await this.#verifyPromotedInventory(session.sessionId, identity);
392
394
  await this.#publishFleetEnrollment(request, session, identity);
395
+ // Catalog discovery can admit this session while the native stamping
396
+ // and verification awaits run. Reuse that exact registry identity instead
397
+ // of publishing a second name for the same session at the commit point.
398
+ const discovered = this.#registeredSession(session.sessionName);
399
+ if (discovered) {
400
+ if (discovered.name !== identity.workspaceName) {
401
+ try {
402
+ await this.#io.runTmux([
403
+ "set-option",
404
+ "-t",
405
+ session.sessionId,
406
+ SESSION_WORKSPACE_OPTION,
407
+ discovered.name,
408
+ ]);
409
+ }
410
+ catch (error) {
411
+ throw new WorkspacePromotionError("stamp_failed", {
412
+ operationId: request.operationId,
413
+ workspaceName: discovered.name,
414
+ }, error);
415
+ }
416
+ }
417
+ this.#assertActive(request.operationId);
418
+ if (this.#registeredSession(session.sessionName)?.name !== discovered.name) {
419
+ throw new WorkspacePromotionError("workspace_conflict", {
420
+ operationId: request.operationId,
421
+ workspaceName: discovered.name,
422
+ });
423
+ }
424
+ return this.#succeed(request, fingerprint, discovered.name, session.sessionName, {
425
+ replayed: true,
426
+ });
427
+ }
393
428
  let registered;
394
429
  try {
395
430
  registered = this.#registry.add({
@@ -449,6 +484,15 @@ export class WorkspacePromotionAuthority {
449
484
  throw error;
450
485
  }
451
486
  }
487
+ #registeredSession(sessionName) {
488
+ const candidates = this.#registry
489
+ .list()
490
+ .filter((workspace) => workspace.sessionName === sessionName);
491
+ if (candidates.length > 1) {
492
+ throw new WorkspacePromotionError("workspace_conflict", { sessionName });
493
+ }
494
+ return candidates[0];
495
+ }
452
496
  #assertConflictFreeIdentity(identity) {
453
497
  for (const workspace of this.#registry.list()) {
454
498
  if (workspace.name === identity.workspaceName &&
@@ -531,6 +575,19 @@ export class WorkspacePromotionAuthority {
531
575
  reason: "empty_or_foreign_pane_inventory",
532
576
  });
533
577
  }
578
+ // list-panes -s repeats each backing pane once per window link. Stamp the
579
+ // backing only once, and reject contradictory observations before writes.
580
+ const uniqueScan = new Map();
581
+ for (const pane of scanned) {
582
+ const previous = uniqueScan.get(pane.paneId);
583
+ if (previous && JSON.stringify(previous) !== JSON.stringify(pane)) {
584
+ throw new WorkspacePromotionError("promotion_verification_failed", {
585
+ reason: "inconsistent_tmux_topology",
586
+ });
587
+ }
588
+ uniqueScan.set(pane.paneId, pane);
589
+ }
590
+ scanned = [...uniqueScan.values()];
534
591
  const nowSec = Math.floor(this.#io.now() / 1000);
535
592
  const reconciledWindows = new Set();
536
593
  try {
@@ -699,7 +756,29 @@ export class WorkspacePromotionAuthority {
699
756
  reason: "missing_window_stamp",
700
757
  });
701
758
  }
702
- const catalog = analyzeTrustedSemanticPaneCatalog(panes.map((pane) => ({
759
+ // Validate links independently from their shared backing. Repeated native
760
+ // panes are legal only across distinct links with identical backing facts.
761
+ const links = new Map();
762
+ const backingPanes = new Map();
763
+ const semanticWindows = new Map();
764
+ for (const pane of panes) {
765
+ const { windowIndex, ...backing } = pane;
766
+ const previous = backingPanes.get(pane.paneId);
767
+ const semanticOwner = semanticWindows.get(pane.semanticWindowId);
768
+ const rows = links.get(windowIndex) ?? [];
769
+ if ((previous && JSON.stringify(previous) !== JSON.stringify(backing)) ||
770
+ (semanticOwner && semanticOwner !== pane.windowId) ||
771
+ rows.some((row) => row.paneId === pane.paneId || row.windowId !== pane.windowId)) {
772
+ throw new WorkspacePromotionError("promotion_verification_failed", {
773
+ reason: "inconsistent_tmux_topology",
774
+ });
775
+ }
776
+ backingPanes.set(pane.paneId, backing);
777
+ semanticWindows.set(pane.semanticWindowId, pane.windowId);
778
+ rows.push(pane);
779
+ links.set(windowIndex, rows);
780
+ }
781
+ const catalog = analyzeTrustedSemanticPaneCatalog([...backingPanes.values()].map((pane) => ({
703
782
  workspaceName: identity.workspaceName,
704
783
  semanticPaneId: pane.semanticPaneId === "" ? null : pane.semanticPaneId,
705
784
  sessionId: pane.sessionId,
@@ -716,16 +795,16 @@ export class WorkspacePromotionAuthority {
716
795
  reason: "semantic_pane_catalog_rejected_inventory",
717
796
  });
718
797
  }
719
- const windows = new Map();
720
- for (const pane of panes) {
721
- const rows = windows.get(pane.windowId) ?? [];
798
+ const backings = new Map();
799
+ for (const pane of backingPanes.values()) {
800
+ const rows = backings.get(pane.windowId) ?? [];
722
801
  rows.push(pane);
723
- windows.set(pane.windowId, rows);
802
+ backings.set(pane.windowId, rows);
724
803
  }
725
804
  const sessionWindowCount = panes[0].sessionWindowCount;
726
- if (windows.size !== sessionWindowCount ||
805
+ if (links.size !== sessionWindowCount ||
727
806
  panes.some((pane) => pane.sessionWindowCount !== sessionWindowCount) ||
728
- [...windows.values()].some((rows) => rows.length !== rows[0].windowPaneCount ||
807
+ [...links.values(), ...backings.values()].some((rows) => rows.length !== rows[0].windowPaneCount ||
729
808
  rows.some((row) => row.windowPaneCount !== rows.length ||
730
809
  row.windowName !== rows[0].windowName ||
731
810
  row.semanticWindowId !== rows[0].semanticWindowId))) {
@@ -101,6 +101,10 @@ export class WorkspaceRegistry {
101
101
  has(name) {
102
102
  return this.workspaces.some((w) => w.name === name);
103
103
  }
104
+ /** Whether this entry is discovered live state, never durable user intent. */
105
+ isVolatile(name) {
106
+ return this.volatileNames.has(name);
107
+ }
104
108
  add(input) {
105
109
  if (this.has(input.name)) {
106
110
  throw new WorkspaceAlreadyExistsError(input.name);
@@ -20,9 +20,11 @@ function rejectUpgrade(socket, status) {
20
20
  ? "Forbidden"
21
21
  : status === 404
22
22
  ? "Not Found"
23
- : status === 426
24
- ? "Upgrade Required"
25
- : "Service Unavailable";
23
+ : status === 410
24
+ ? "Gone"
25
+ : status === 426
26
+ ? "Upgrade Required"
27
+ : "Service Unavailable";
26
28
  try {
27
29
  socket.end(`HTTP/1.1 ${status} ${phrase}\r\nConnection: close\r\nContent-Length: 0\r\n\r\n`);
28
30
  }
@@ -36,7 +38,7 @@ function rejectUpgrade(socket, status) {
36
38
  * Origin, and pre-auth capacity decisions happen before `handleUpgrade`, so
37
39
  * rejected requests never become WebSockets.
38
40
  */
39
- export function attachPaneStreamWebSocket(server, coordinator) {
41
+ export function attachPaneStreamWebSocket(server, coordinator, redemptionPath = PANE_STREAM_REDEEM_PATH, isCurrent = () => true) {
40
42
  const wss = new WebSocketServer({
41
43
  noServer: true,
42
44
  clientTracking: false,
@@ -52,9 +54,17 @@ export function attachPaneStreamWebSocket(server, coordinator) {
52
54
  const upgrade = (request, socket, head) => {
53
55
  const rawPath = request.url ?? "";
54
56
  const pathname = rawPath.split("?", 1)[0] ?? "";
55
- if (!pathname.startsWith("/v1/terminal/pane-streams/"))
57
+ if (redemptionPath === PANE_STREAM_REDEEM_PATH &&
58
+ pathname.startsWith("/v1/terminal/pane-streams/")) {
59
+ rejectUpgrade(socket, 426);
60
+ return;
61
+ }
62
+ if (redemptionPath !== PANE_STREAM_REDEEM_PATH && pathname !== redemptionPath)
56
63
  return;
57
- if (pathname !== PANE_STREAM_REDEEM_PATH) {
64
+ if (redemptionPath === PANE_STREAM_REDEEM_PATH &&
65
+ !pathname.startsWith("/v2/terminal/pane-streams/"))
66
+ return;
67
+ if (pathname !== redemptionPath) {
58
68
  rejectUpgrade(socket, 404);
59
69
  return;
60
70
  }
@@ -78,6 +88,10 @@ export function attachPaneStreamWebSocket(server, coordinator) {
78
88
  rejectUpgrade(socket, 403);
79
89
  return;
80
90
  }
91
+ if (!isCurrent()) {
92
+ rejectUpgrade(socket, 410);
93
+ return;
94
+ }
81
95
  const decision = coordinator.reserveUpgrade({
82
96
  path: rawPath,
83
97
  protocols: protocols(protocolHeaders[0]),
@@ -1,3 +1,4 @@
1
+ import { fenceNativeTmuxCommand, } from "../../lib/tmux-server-generation-runner.js";
1
2
  import { accessSync, constants, realpathSync, statSync } from "node:fs";
2
3
  import { execFile } from "node:child_process";
3
4
  import { isAbsolute } from "node:path";
@@ -91,6 +92,7 @@ function canonicalAuthority(input) {
91
92
  socketArgv = ["-L", input.socketSelector.name];
92
93
  }
93
94
  return Object.freeze({
95
+ nativeServerIdentity: input.nativeServerIdentity,
94
96
  namedSocketFence: socketSelector.kind === "name" ? (input.namedSocketFence ?? null) : null,
95
97
  executablePath,
96
98
  socketSelector: Object.freeze(socketSelector),
@@ -144,13 +146,16 @@ function pinnedRunner(authority, execute, startupPolicy) {
144
146
  if (command.executable !== "tmux")
145
147
  return { status: "failed" };
146
148
  try {
147
- const stdout = execute(authority.executablePath, [...currentSocketArgv(authority), ...command.argv], {
149
+ const guarded = authority.nativeServerIdentity
150
+ ? fenceNativeTmuxCommand(command.argv, authority.nativeServerIdentity)
151
+ : null;
152
+ const stdout = execute(authority.executablePath, [...currentSocketArgv(authority), ...(guarded?.argv ?? command.argv)], {
148
153
  cwd: authority.trustedCwd,
149
154
  env: authority.environment,
150
155
  maxBuffer: MAX_TMUX_OUTPUT_BYTES,
151
156
  timeoutMs: TERMINAL_ATTACHMENT_TMUX_COMMAND_TIMEOUT_MS,
152
157
  });
153
- const value = String(stdout);
158
+ const value = guarded ? guarded.verify(String(stdout)) : String(stdout);
154
159
  if (authority.namedSocketFence && !authority.namedSocketFence.isPinned())
155
160
  authority.namedSocketFence.resolve();
156
161
  if (value.includes("\0") || Buffer.byteLength(value, "utf8") > MAX_TMUX_OUTPUT_BYTES) {
@@ -209,11 +214,14 @@ function pinnedReadRunner(authority, execute) {
209
214
  if (command.executable !== "tmux" || signal?.aborted)
210
215
  return { status: "failed" };
211
216
  try {
217
+ const guarded = authority.nativeServerIdentity
218
+ ? fenceNativeTmuxCommand(command.argv, authority.nativeServerIdentity)
219
+ : null;
212
220
  const stdout = await execute(authority.executablePath, [
213
221
  ...(authority.namedSocketFence
214
222
  ? await authority.namedSocketFence.resolveAsync(signal)
215
223
  : currentSocketArgv(authority)),
216
- ...command.argv,
224
+ ...(guarded?.argv ?? command.argv),
217
225
  ], {
218
226
  cwd: authority.trustedCwd,
219
227
  env: authority.environment,
@@ -221,7 +229,7 @@ function pinnedReadRunner(authority, execute) {
221
229
  timeoutMs: TERMINAL_ATTACHMENT_TMUX_COMMAND_TIMEOUT_MS,
222
230
  ...(signal ? { signal } : {}),
223
231
  });
224
- const value = String(stdout);
232
+ const value = guarded ? guarded.verify(String(stdout)) : String(stdout);
225
233
  if (authority.namedSocketFence && !authority.namedSocketFence.isPinned())
226
234
  await authority.namedSocketFence.resolveAsync(signal);
227
235
  if (value.includes("\0") || Buffer.byteLength(value, "utf8") > MAX_TMUX_OUTPUT_BYTES)
@@ -443,6 +451,7 @@ const PANE_FORMAT = [
443
451
  // window reports the same value; the catalog requires it before a multi-pane
444
452
  // window is attachable.
445
453
  "#{@tmux_ide_window_id}",
454
+ "#{window_index}",
446
455
  PANE_WIRE_SENTINEL,
447
456
  ].join(WIRE_SEPARATOR);
448
457
  async function requiredTmuxResult(runner, argv, signal) {
@@ -479,25 +488,24 @@ async function liveSessionIdentities(runner, signal) {
479
488
  }
480
489
  function parsePaneSnapshot(stdout, expected) {
481
490
  const panes = [];
482
- const runtimeIds = new Set();
491
+ const runtimeIds = new Map();
492
+ const links = new Map();
483
493
  for (const line of strictLines(stdout, MAX_DISCOVERED_PANES)) {
484
494
  const fields = line.split(WIRE_SEPARATOR);
485
- if (fields.length !== 19 || fields[18] !== PANE_WIRE_SENTINEL) {
495
+ if (fields.length !== 20 || fields[19] !== PANE_WIRE_SENTINEL) {
486
496
  throw new NativeTerminalAttachmentRuntimeError("invalid-tmux-output");
487
497
  }
488
- const [sessionName, sessionId, windowId, runtimePaneId, paneCountValue, windowCountValue, stamp, indexValue, title, currentCommand, windowActive, paneActive, role, name, type, missionStamp, dir, windowStampValue,] = fields;
498
+ const [sessionName, sessionId, windowId, runtimePaneId, paneCountValue, windowCountValue, stamp, indexValue, title, currentCommand, windowActive, paneActive, role, name, type, missionStamp, dir, windowStampValue, windowIndexValue,] = fields;
489
499
  if (sessionName !== expected.name ||
490
500
  sessionId !== expected.id ||
491
501
  !RUNTIME_WINDOW_ID.test(windowId) ||
492
502
  !RUNTIME_PANE_ID.test(runtimePaneId) ||
493
- runtimeIds.has(runtimePaneId) ||
494
503
  !["0", "1"].includes(windowActive) ||
495
504
  !["0", "1"].includes(paneActive)) {
496
505
  throw new NativeTerminalAttachmentRuntimeError("invalid-tmux-output");
497
506
  }
498
- runtimeIds.add(runtimePaneId);
499
507
  const nullable = (value) => boundedWireValue(value, 256).length === 0 ? null : value;
500
- panes.push({
508
+ const facts = {
501
509
  sessionName,
502
510
  sessionId,
503
511
  windowId,
@@ -519,18 +527,51 @@ function parsePaneSnapshot(stdout, expected) {
519
527
  // registered workspace remains the trusted application-shell root, so
520
528
  // keep discovery available and carry the empty presentation value.
521
529
  dir: boundedWireValue(dir, 4_096),
522
- });
530
+ };
531
+ const windowIndex = nonnegativeInteger(windowIndexValue);
532
+ const link = links.get(windowIndex) ?? {
533
+ windowId,
534
+ active: windowActive === "1",
535
+ panes: new Set(),
536
+ };
537
+ if (link.windowId !== windowId ||
538
+ link.active !== (windowActive === "1") ||
539
+ link.panes.has(runtimePaneId)) {
540
+ throw new NativeTerminalAttachmentRuntimeError("invalid-tmux-output");
541
+ }
542
+ link.panes.add(runtimePaneId);
543
+ links.set(windowIndex, link);
544
+ // Only link index and current-link state may differ for a shared backing.
545
+ const proof = JSON.stringify(fields.filter((_value, index) => index !== 10 && index !== 18));
546
+ const previous = runtimeIds.get(runtimePaneId);
547
+ if (previous) {
548
+ if (previous.proof !== proof)
549
+ throw new NativeTerminalAttachmentRuntimeError("invalid-tmux-output");
550
+ if (facts.active && !previous.facts.active) {
551
+ const merged = { ...previous.facts, active: true };
552
+ panes[panes.indexOf(previous.facts)] = merged;
553
+ previous.facts = merged;
554
+ }
555
+ }
556
+ else {
557
+ runtimeIds.set(runtimePaneId, { facts, proof });
558
+ panes.push(facts);
559
+ }
523
560
  }
524
561
  const counts = new Map();
525
562
  for (const pane of panes)
526
563
  counts.set(pane.windowId, (counts.get(pane.windowId) ?? 0) + 1);
527
- const windows = new Set(panes.map((pane) => pane.windowId));
528
564
  if (panes.some((pane) => counts.get(pane.windowId) !== pane.windowPaneCount ||
529
- windows.size !== pane.sessionWindowCount) ||
565
+ links.size !== pane.sessionWindowCount) ||
566
+ [...links.values()].some((link) => link.panes.size !== counts.get(link.windowId)) ||
567
+ [...links.values()].filter((link) => link.active).length > 1 ||
530
568
  panes.filter((pane) => pane.active).length > 1) {
531
569
  throw new NativeTerminalAttachmentRuntimeError("invalid-tmux-output");
532
570
  }
533
- return panes;
571
+ return {
572
+ panes,
573
+ linksProof: JSON.stringify([...links].map(([index, link]) => [index, link.windowId, [...link.panes]])),
574
+ };
534
575
  }
535
576
  /**
536
577
  * One bounded, old-tmux-safe discovery path for both live attachment and the
@@ -558,11 +599,13 @@ export async function discoverWorkspaceRegistryTerminalInventory(registry, runne
558
599
  const before = await requiredTmuxResult(runner, argv, signal);
559
600
  if (before === null)
560
601
  continue;
561
- const panes = parsePaneSnapshot(before, identity);
602
+ const first = parsePaneSnapshot(before, identity);
603
+ const panes = first.panes;
562
604
  const after = await requiredTmuxResult(runner, argv, signal);
563
605
  if (after === null)
564
606
  throw new NativeTerminalAttachmentRuntimeError("discovery-failed");
565
- const latest = parsePaneSnapshot(after, identity);
607
+ const second = parsePaneSnapshot(after, identity);
608
+ const latest = second.panes;
566
609
  // Foreground commands, titles, focus and paths can change while the same
567
610
  // pane remains valid. Fence binding/topology only, then publish one complete
568
611
  // latest snapshot rather than mixing presentation values across reads.
@@ -577,7 +620,8 @@ export async function discoverWorkspaceRegistryTerminalInventory(registry, runne
577
620
  "windowStamp",
578
621
  "index",
579
622
  ];
580
- if (panes.length !== latest.length ||
623
+ if (first.linksProof !== second.linksProof ||
624
+ panes.length !== latest.length ||
581
625
  panes.some((pane, index) => proofKeys.some((key) => pane[key] !== latest[index][key]))) {
582
626
  throw new NativeTerminalAttachmentRuntimeError("discovery-failed");
583
627
  }
@@ -23,6 +23,10 @@ export class ScriptedChannelDriver {
23
23
  if (command.startsWith('display-message -p "#{qa:session_name}')) {
24
24
  return basic(command) ?? [];
25
25
  }
26
+ // This scalar observation is not a deferred cursor probe. Answer it
27
+ // before an automatic inventory reply can consume its pending FIFO slot.
28
+ if (isHistorySizeProbe(command))
29
+ return ["0"];
26
30
  if (command.includes("capture-pane") || command.startsWith("display-message"))
27
31
  return null;
28
32
  return basic(command) ?? [];
@@ -32,7 +36,8 @@ export class ScriptedChannelDriver {
32
36
  pump() {
33
37
  for (let index = this.#handledWrites; index < this.channel.written.length; index += 1) {
34
38
  const command = this.channel.written[index];
35
- if (command.startsWith('display-message -p "#{qa:session_name}'))
39
+ if (command.startsWith('display-message -p "#{qa:session_name}') ||
40
+ isHistorySizeProbe(command))
36
41
  continue;
37
42
  if (command.includes("capture-pane") || command.startsWith("display-message")) {
38
43
  this.deferredCommands.push(command);
@@ -88,3 +93,6 @@ export function standardScriptedChannel(handlers, options) {
88
93
  ...options,
89
94
  });
90
95
  }
96
+ function isHistorySizeProbe(command) {
97
+ return /^display-message -p -t %\d+ "#\{history_size\}"$/u.test(command);
98
+ }
@@ -116,13 +116,22 @@ export const FIXTURE = {
116
116
  export function fixtureAutoReply(state) {
117
117
  return (cmd) => {
118
118
  if (cmd.startsWith('display-message -p "#{qa:session_name}'))
119
- return ["zz-sim\t$1"];
119
+ return ["zz-sim\t$1\t1234\t1700000000"];
120
120
  if (cmd.includes("qa:@tmux_ide_pane_id"))
121
121
  return state.descriptorRows;
122
122
  if (cmd.startsWith("list-panes -s"))
123
123
  return state.truthRows;
124
124
  if (cmd.startsWith("list-windows"))
125
- return state.windowRows;
125
+ return state.windowRows.map((row, index) => {
126
+ const parts = row.split("\t");
127
+ if (parts.length === 7)
128
+ parts.push(parts[4]);
129
+ if (parts.length === 8)
130
+ parts.push("emacs");
131
+ if (parts.length === 9)
132
+ parts.push(String(index));
133
+ return parts.join("\t");
134
+ });
126
135
  // Product-owned seeds are one atomic `set-option ; capture-pane` command
127
136
  // list. The capture reply remains manual just like a bare capture probe.
128
137
  if (cmd.includes("capture-pane"))
@@ -20,6 +20,7 @@
20
20
  * chance to detach us — killing the reader first can wedge the server.
21
21
  */
22
22
  import { spawn } from "node:child_process";
23
+ import { shellEscape } from "../../lib/shell.js";
23
24
  import { parseControlLine } from "../protocol/control.js";
24
25
  const ATOMIC_CAPTURE_BYTE_HARD_CAP = 16 * 1024 * 1024;
25
26
  // Budget row bookkeeping separately from wire bytes (64 bytes per retained
@@ -538,6 +539,17 @@ export class ControlChannelCore {
538
539
  }
539
540
  }
540
541
  export function mirrorControlAttachArgs(options, pauseAfterSeconds = DEFAULT_PAUSE_AFTER_SECONDS) {
542
+ const attach = [
543
+ "attach",
544
+ "-t",
545
+ options.session,
546
+ "-f",
547
+ `ignore-size,pause-after=${pauseAfterSeconds},active-pane`,
548
+ ];
549
+ const expected = options.nativeServerIdentity;
550
+ if (expected &&
551
+ (!/^[1-9][0-9]*$/u.test(expected.pid) || !/^[1-9][0-9]*$/u.test(expected.startTime)))
552
+ throw new Error("Invalid expected native server identity");
541
553
  return [
542
554
  ...(options.socketPath
543
555
  ? ["-S", options.socketPath]
@@ -546,11 +558,15 @@ export function mirrorControlAttachArgs(options, pauseAfterSeconds = DEFAULT_PAU
546
558
  : []),
547
559
  ...(options.configFile ? ["-f", options.configFile] : []),
548
560
  "-C",
549
- "attach",
550
- "-t",
551
- options.session,
552
- "-f",
553
- `ignore-size,pause-after=${pauseAfterSeconds},active-pane`,
561
+ ...(expected
562
+ ? [
563
+ "if-shell",
564
+ "-F",
565
+ `#{&&:#{==:#{pid},${expected.pid}},#{==:#{start_time},${expected.startTime}}}`,
566
+ attach.map(shellEscape).join(" "),
567
+ "display-message -p 'tmux-server-generation-refused'",
568
+ ]
569
+ : attach),
554
570
  ];
555
571
  }
556
572
  /** Spike-verified default: bounds a stalled reader's server-side buffering
@@ -561,11 +577,25 @@ export class MirrorControlChannel {
561
577
  core;
562
578
  opts;
563
579
  exited = false;
580
+ verifiedAttach = false;
581
+ guardedStart = null;
564
582
  atomicCollectorTimer = null;
565
583
  constructor(opts) {
566
584
  this.opts = opts;
567
585
  this.core = new ControlChannelCore({
568
586
  ...opts.handlers,
587
+ onOutput: (...args) => {
588
+ if (!opts.nativeServerIdentity || this.verifiedAttach)
589
+ opts.handlers.onOutput(...args);
590
+ },
591
+ onNotify: (name, rest) => {
592
+ if (opts.nativeServerIdentity && name === "session-changed") {
593
+ this.verifiedAttach = true;
594
+ this.guardedStart?.resolve();
595
+ }
596
+ if (!opts.nativeServerIdentity || this.verifiedAttach)
597
+ opts.handlers.onNotify(name, rest);
598
+ },
569
599
  onExit: (reason) => this.noteExit(reason),
570
600
  }, opts.nowMicros);
571
601
  }
@@ -604,7 +634,35 @@ export class MirrorControlChannel {
604
634
  // tmux opens with an unsolicited %begin/%end greeting block; a queued
605
635
  // resolver makes start() settle when the protocol is actually live.
606
636
  return new Promise((resolve, reject) => {
607
- this.core.push({ kind: "promise", resolve: () => resolve(), reject, lines: [] });
637
+ let timer;
638
+ const settle = (error) => {
639
+ if (timer)
640
+ clearTimeout(timer);
641
+ this.guardedStart = null;
642
+ if (error)
643
+ reject(error);
644
+ else
645
+ resolve();
646
+ };
647
+ if (this.opts.nativeServerIdentity) {
648
+ this.guardedStart = { resolve: () => settle(), reject: (error) => settle(error) };
649
+ timer = setTimeout(() => {
650
+ settle(new Error("Tmux generation-guarded attach timed out"));
651
+ void this.dispose();
652
+ }, 5_000);
653
+ timer.unref?.();
654
+ }
655
+ // The outer if-shell greeting precedes its attach branch. Only native
656
+ // session-changed proves that the guarded attach actually succeeded.
657
+ this.core.push({
658
+ kind: "promise",
659
+ resolve: () => {
660
+ if (!this.opts.nativeServerIdentity)
661
+ settle();
662
+ },
663
+ reject: (error) => settle(error),
664
+ lines: [],
665
+ });
608
666
  proc.on("error", (err) => {
609
667
  this.core.fail(String(err));
610
668
  reject(err);
@@ -747,6 +805,7 @@ export class MirrorControlChannel {
747
805
  if (this.exited)
748
806
  return;
749
807
  this.exited = true;
808
+ this.guardedStart?.reject(new Error(reason ?? "Control channel exited before guarded attach"));
750
809
  this.opts.handlers.onExit(reason);
751
810
  }
752
811
  }
@@ -1,3 +1,8 @@
1
+ import { accessSync, constants, realpathSync, statSync } from "node:fs";
2
+ import { isAbsolute } from "node:path";
3
+ import { resolveTmuxExecutable, tmuxClientEnvironment } from "../../lib/tmux-client-execution.js";
4
+ import { captureUnixSocketIdentity, revalidateUnixSocketIdentity, } from "../../lib/unix-socket-authority.js";
5
+ import { execFile } from "node:child_process";
1
6
  /**
2
7
  * MirrorService — the daemon-side shared mirror layer (m43 card 1).
3
8
  *
@@ -144,6 +149,12 @@ export class MirrorService {
144
149
  },
145
150
  };
146
151
  }
152
+ async executeWindowLinkAction(session, request) {
153
+ const entry = this.channels.get(session);
154
+ if (!entry || entry.retired)
155
+ throw new Error(`Mirror session ${session} is unavailable`);
156
+ return entry.channel.executeWindowLinkAction(request);
157
+ }
147
158
  /** Synchronous hot input on an already-retained SessionRuntime channel. */
148
159
  sendText(session, semanticPaneId, text, performanceTraceId, isolated = false) {
149
160
  const entry = this.channels.get(session);
@@ -254,6 +265,21 @@ export class MirrorService {
254
265
  }), this.owner);
255
266
  let channel;
256
267
  try {
268
+ const initialSocket = this.opts.resolveSocketPath
269
+ ? this.opts.resolveSocketPath()
270
+ : this.opts.socketPath;
271
+ const mutationSocketIdentity = initialSocket
272
+ ? captureUnixSocketIdentity(initialSocket)
273
+ : null;
274
+ const executable = mutationSocketIdentity
275
+ ? realpathSync(this.opts.executable ?? resolveTmuxExecutable())
276
+ : null;
277
+ if (executable) {
278
+ accessSync(executable, constants.X_OK);
279
+ if (!isAbsolute(executable) || !statSync(executable).isFile())
280
+ throw new Error("Invalid pinned tmux executable");
281
+ }
282
+ const environment = Object.freeze(tmuxClientEnvironment(process.env));
257
283
  const channelOptions = {
258
284
  session,
259
285
  createIo: (handlers) => this.opts.createIo?.(session, handlers) ??
@@ -264,10 +290,28 @@ export class MirrorService {
264
290
  socketPath: this.opts.socketPath,
265
291
  resolveSocketPath: this.opts.resolveSocketPath,
266
292
  executable: this.opts.executable,
293
+ nativeServerIdentity: this.opts.nativeServerIdentity,
267
294
  configFile: this.opts.configFile,
268
295
  pauseAfterSeconds: this.opts.pauseAfterSeconds,
269
296
  nowMicros: this.opts.nowMicros,
270
297
  }),
298
+ executeWindowLinkGuard: (args) => {
299
+ if (!mutationSocketIdentity || !executable)
300
+ throw new Error("Window link mutation requires a pinned socket authority");
301
+ if (this.opts.resolveSocketPath)
302
+ this.opts.resolveSocketPath();
303
+ const socket = revalidateUnixSocketIdentity(mutationSocketIdentity);
304
+ return new Promise((resolve) => {
305
+ execFile(executable, ["-N", "-u", "-S", socket, ...args], { timeout: 5000, maxBuffer: 65536, encoding: "utf8", env: environment }, (error, stdout) => {
306
+ const status = !error
307
+ ? 0
308
+ : typeof error.code === "number" && !error.killed
309
+ ? error.code
310
+ : null;
311
+ resolve({ status, stdout });
312
+ });
313
+ });
314
+ },
271
315
  historyLines: this.opts.historyLines,
272
316
  internalReadHookEmission: this.opts.internalReadHookEmission,
273
317
  generatePaneId: this.opts.generatePaneId,