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
@@ -30,7 +30,7 @@ import { decodeNativeGridCapture, isNativeBootstrapCapture, } from "./native-gri
30
30
  */
31
31
  import { createHash, randomBytes } from "node:crypto";
32
32
  import { hostname } from "node:os";
33
- import { WORKSPACE_SEMANTIC_PANE_OPTION, WORKSPACE_SEMANTIC_WINDOW_OPTION, WorkspaceIdSchemaZ, } from "@tmux-ide/contracts";
33
+ import { WINDOW_LINK_MAX_LINKS, WORKSPACE_SEMANTIC_PANE_OPTION, WORKSPACE_SEMANTIC_WINDOW_OPTION, WorkspaceIdSchemaZ, } from "@tmux-ide/contracts";
34
34
  import { textToHexKeys } from "../protocol/control.js";
35
35
  import { InputCoalescer } from "../protocol/input-coalescer.js";
36
36
  import { NativeGridCaptureReader } from "./native-grid-reader.js";
@@ -38,6 +38,9 @@ import { parseLayout, parseLayoutChange, parseSessionWindowChanged, parseWindowP
38
38
  import { SESSION_PANE_DESCRIPTOR_FORMAT, SessionDescriptorDiscovery, decodeControlReplyUtf8, decodeTmuxArgument, parseSessionPaneDescriptorReply, } from "../protocol/session-descriptor-discovery.js";
39
39
  import { resolvePaneDisplayName } from "../protocol/pane-display-name.js";
40
40
  import { finalizeWorkspaceTmuxReconciliation, planWorkspaceTmuxReconciliation, } from "../protocol/workspace-tmux-adapter.js";
41
+ import { liveSessionIdForNativeIdentity } from "../protocol/live-session-identity.js";
42
+ import { WindowLinkAuthority, WindowLinkResolutionError } from "./window-link-authority.js";
43
+ import { buildNativeWindowLinkGuard, buildNativeWindowLinkPaneSelectGuard, classifyNativeWindowLinkGuardResult, } from "../../lib/tmux-window-link-guard.js";
41
44
  import { FlowLedger } from "./flow-ledger.js";
42
45
  import { PaneFeed } from "./pane-feed.js";
43
46
  import { INTERNAL_READ_OPERATION_OPTION, registerInternalReadOperation, retireInternalReadOperation, } from "../../lib/tmux-interaction-options.js";
@@ -165,6 +168,9 @@ export class SessionChannel {
165
168
  truthActive = new Map();
166
169
  truthWindow = new Map();
167
170
  currentWindow = "";
171
+ windowLinkAuthority = null;
172
+ latestWindowStage = null;
173
+ attachedServerGeneration = null;
168
174
  diagnostics = [];
169
175
  degraded = false;
170
176
  ageByRuntime = new Map();
@@ -325,7 +331,13 @@ export class SessionChannel {
325
331
  ? this.trustedInventoryFlight
326
332
  : Promise.reject(new Error(`trusted inventory identity changed for ${this.opts.session}`));
327
333
  }
328
- const flight = this.refreshTrustedInventory(expectedRuntimeSessionId).finally(() => {
334
+ const flight = this.refreshTrustedInventory(expectedRuntimeSessionId)
335
+ .catch((error) => {
336
+ this.windowLinkAuthority?.invalidate();
337
+ this.latestWindowStage = null;
338
+ throw error;
339
+ })
340
+ .finally(() => {
329
341
  if (this.trustedInventoryFlight === flight) {
330
342
  this.trustedInventoryFlight = null;
331
343
  this.trustedInventoryFlightSessionId = null;
@@ -344,21 +356,92 @@ export class SessionChannel {
344
356
  return this.attachedIdentity;
345
357
  }
346
358
  async captureAttachedSessionIdentity() {
347
- const lines = await this.io.request(`display-message -p "#{qa:session_name}\t#{session_id}"`);
359
+ const lines = await this.io.request(`display-message -p "#{qa:session_name}\t#{session_id}\t#{pid}\t#{session_created}"`);
348
360
  if (lines.length !== 1)
349
361
  throw new Error(`mirror session ${this.opts.session} identity is absent`);
350
362
  const decodedLine = decodeControlReplyUtf8(lines[0]);
351
363
  if (decodedLine === null)
352
364
  throw new Error(`mirror session ${this.opts.session} identity is malformed`);
353
- const [encodedName = "", runtimeSessionId = ""] = decodedLine.split("\t");
365
+ const [encodedName = "", runtimeSessionId = "", serverPid = "", sessionCreated = ""] = decodedLine.split("\t");
354
366
  const sessionName = decodeTmuxArgument(encodedName);
355
367
  if (sessionName.length === 0 ||
356
368
  sessionName.length > 160 ||
357
369
  !/^\$(?:0|[1-9][0-9]*)$/u.test(runtimeSessionId) ||
358
- runtimeSessionId.length > 32) {
370
+ runtimeSessionId.length > 32 ||
371
+ !/^[1-9][0-9]*$/u.test(serverPid) ||
372
+ !/^[0-9]+$/u.test(sessionCreated)) {
359
373
  throw new Error(`mirror session ${this.opts.session} identity is malformed`);
360
374
  }
361
375
  this.attachedIdentity = Object.freeze({ sessionName, runtimeSessionId });
376
+ this.attachedServerGeneration = { serverPid, sessionCreated };
377
+ if (this.disposed)
378
+ return;
379
+ this.bindWindowLinkAuthority(liveSessionIdForNativeIdentity(serverPid, runtimeSessionId, sessionCreated));
380
+ }
381
+ bindWindowLinkAuthority(liveSessionId) {
382
+ const identity = this.attachedIdentity;
383
+ if (!identity || this.disposed)
384
+ throw new WindowLinkResolutionError("window_link_stale");
385
+ if (this.windowLinkAuthority?.liveSessionId === liveSessionId)
386
+ return;
387
+ this.windowLinkAuthority?.dispose();
388
+ this.windowLinkAuthority = new WindowLinkAuthority(liveSessionId, identity.runtimeSessionId);
389
+ if (this.latestWindowStage)
390
+ this.windowLinkAuthority.reconcile(this.latestWindowStage.links);
391
+ }
392
+ async executeWindowLinkAction(request) {
393
+ const authority = this.windowLinkAuthority;
394
+ if (!authority || this.disposed)
395
+ throw new WindowLinkResolutionError("window_link_stale");
396
+ const pane = request.paneId ? this.panesBySemantic.get(request.paneId) : undefined;
397
+ if (request.paneId && !pane)
398
+ throw new WindowLinkResolutionError("window_link_backing_mismatch");
399
+ const semanticWindow = pane?.windowRuntimeId
400
+ ? this.windowsByRuntime.get(pane.windowRuntimeId)?.semanticId
401
+ : null;
402
+ const target = request.target ?? (semanticWindow ? authority.uniqueTargetForBacking(semanticWindow) : null);
403
+ if (!target)
404
+ throw new WindowLinkResolutionError("window_link_stale");
405
+ const resolved = authority.resolve(target);
406
+ if (pane && (pane.windowRuntimeId !== resolved.runtimeWindowId || request.action !== "select"))
407
+ throw new WindowLinkResolutionError("window_link_backing_mismatch");
408
+ const address = {
409
+ sessionId: resolved.runtimeSessionId,
410
+ windowIndex: resolved.index,
411
+ expectedWindowId: resolved.runtimeWindowId,
412
+ expectedServerPid: this.attachedServerGeneration.serverPid,
413
+ expectedSessionCreated: this.attachedServerGeneration.sessionCreated,
414
+ };
415
+ const args = pane
416
+ ? buildNativeWindowLinkPaneSelectGuard(address, pane.runtimeId)
417
+ : buildNativeWindowLinkGuard(address, request.action);
418
+ let outcome;
419
+ try {
420
+ if (!this.opts.executeWindowLinkGuard)
421
+ throw new Error("Window link executor unavailable");
422
+ const result = await this.opts.executeWindowLinkGuard(args);
423
+ outcome = classifyNativeWindowLinkGuardResult(result.status, result.stdout);
424
+ }
425
+ catch {
426
+ // A lost command boundary can follow a mutation; never retry automatically.
427
+ outcome = "indeterminate";
428
+ }
429
+ let timer;
430
+ const reconciled = await Promise.race([
431
+ this.syncNow().then(() => true, () => false),
432
+ new Promise((resolve) => {
433
+ timer = setTimeout(() => resolve(false), 2000);
434
+ }),
435
+ ]).finally(() => {
436
+ if (timer !== undefined)
437
+ clearTimeout(timer);
438
+ });
439
+ if (!reconciled) {
440
+ authority.invalidate();
441
+ this.latestWindowStage = null;
442
+ return { outcome, windowLinks: null };
443
+ }
444
+ return { outcome, windowLinks: authority.snapshot() };
362
445
  }
363
446
  /** Optional native backing, addressed through the verified semantic binding. */
364
447
  captureNativeBacking(semanticPaneId, ownsSubscription = () => true) {
@@ -494,7 +577,7 @@ export class SessionChannel {
494
577
  throw error;
495
578
  }
496
579
  }
497
- if (expectedByWindow.size !== inventory.panes[0].sessionWindowCount ||
580
+ if (this.latestWindowStage?.links.length !== inventory.panes[0].sessionWindowCount ||
498
581
  expectedByWindow.size !== this.windowsByRuntime.size) {
499
582
  throw new Error(`authoritative layout for ${this.opts.session} has incomplete windows`);
500
583
  }
@@ -631,6 +714,7 @@ export class SessionChannel {
631
714
  if (this.disposed)
632
715
  return;
633
716
  this.disposed = true;
717
+ this.windowLinkAuthority?.dispose();
634
718
  this.nativeGrid.dispose();
635
719
  this.settleFirstJoin();
636
720
  this.cancelSync?.();
@@ -701,12 +785,13 @@ export class SessionChannel {
701
785
  this.noteRecoveryOutput(pane, outputOrdinal);
702
786
  }
703
787
  // ── Seed / reseed (the atomic recipe) ────────────────────────────────────
704
- reseed(sub, onSettled, deferPublish = false, deadlineAt = this.recoveryNowMs() + RECOVERY_ABSOLUTE_DEADLINE_MS) {
788
+ reseed(lease, onSettled, deferPublish = false, deadlineAt = this.recoveryNowMs() + RECOVERY_ABSOLUTE_DEADLINE_MS) {
789
+ const { sub } = lease;
705
790
  if (sub.closed || sub.frozen || this.disposed) {
706
791
  onSettled?.(FAILED_RESEED_RESULT);
707
792
  return;
708
793
  }
709
- sub.cancelCapture?.();
794
+ lease.cancelRecipe?.();
710
795
  const runtime = sub.pane.runtimeId;
711
796
  const epoch = sub.feed.beginReseed();
712
797
  let settled = false;
@@ -719,7 +804,7 @@ export class SessionChannel {
719
804
  return;
720
805
  settled = true;
721
806
  cancelDeadline?.();
722
- sub.cancelCapture = null;
807
+ lease.cancelRecipe = null;
723
808
  onSettled?.(result);
724
809
  };
725
810
  // Keystroke ordering: pending coalesced input leaves before the probes.
@@ -732,12 +817,12 @@ export class SessionChannel {
732
817
  markerRetired = true;
733
818
  this.retireInternalReadMarker(runtime, internalReadMarker);
734
819
  };
735
- sub.cancelCapture = () => {
820
+ lease.cancelRecipe = () => {
736
821
  if (settled)
737
822
  return;
738
823
  settled = true;
739
824
  cancelDeadline?.();
740
- sub.cancelCapture = null;
825
+ lease.cancelRecipe = null;
741
826
  sub.feed.abort(epoch);
742
827
  if (!captureSucceeded)
743
828
  retireMarker();
@@ -770,9 +855,9 @@ export class SessionChannel {
770
855
  settled = true;
771
856
  sub.feed.abort(epoch);
772
857
  cancelDeadline?.();
773
- sub.cancelCapture = null;
858
+ lease.cancelRecipe = null;
774
859
  retireMarker();
775
- this.reseed(sub, onSettled, deferPublish, deadlineAt);
860
+ this.reseed(lease, onSettled, deferPublish, deadlineAt);
776
861
  return;
777
862
  }
778
863
  sub.feed.abort(epoch);
@@ -787,10 +872,10 @@ export class SessionChannel {
787
872
  retireMarker();
788
873
  sub.feed.abort(epoch);
789
874
  cancelDeadline?.();
790
- sub.cancelCapture = null;
875
+ lease.cancelRecipe = null;
791
876
  sub.nativeBootstrap = false;
792
877
  this.nativeBootstrapUnavailable = true;
793
- this.reseed(sub, onSettled, deferPublish, deadlineAt);
878
+ this.reseed(lease, onSettled, deferPublish, deadlineAt);
794
879
  return;
795
880
  }
796
881
  if (!reply.ok) {
@@ -864,14 +949,14 @@ export class SessionChannel {
864
949
  reseedPlain(sub, resumeReason = null) {
865
950
  if (sub.closed || sub.frozen || this.disposed)
866
951
  return;
867
- if (this.plainReseedActive === sub)
952
+ if (this.plainReseedActive?.sub === sub)
868
953
  sub.cancelCapture?.();
869
954
  if (!this.plainReseedQueue.has(sub) && this.plainReseedQueue.size >= MAX_QUEUED_PLAIN_RESEEDS) {
870
955
  this.failPlainReseed(sub);
871
956
  return;
872
957
  }
873
958
  this.plainReseedQueue.set(sub, resumeReason);
874
- if (this.plainReseedActive !== sub) {
959
+ if (this.plainReseedActive?.sub !== sub) {
875
960
  // Output before this queued recipe starts is already in its future
876
961
  // capture. Do not publish it using the prior geometry in the meantime.
877
962
  sub.feed.beginReseed();
@@ -904,13 +989,26 @@ export class SessionChannel {
904
989
  this.drainPlainReseeds();
905
990
  return;
906
991
  }
907
- this.plainReseedActive = sub;
992
+ sub.cancelCapture?.();
993
+ const lease = { sub, cancelRecipe: null };
994
+ this.plainReseedActive = lease;
995
+ // The queue lease outlives individual native capability probes. Retrying
996
+ // or falling back replaces only cancelRecipe, never this queue owner.
997
+ sub.cancelCapture = () => {
998
+ if (this.plainReseedActive !== lease)
999
+ return;
1000
+ lease.cancelRecipe?.();
1001
+ sub.cancelCapture = null;
1002
+ this.plainReseedActive = null;
1003
+ this.drainPlainReseeds();
1004
+ };
908
1005
  // Only the recipe actually submitted to the control FIFO owns a five
909
1006
  // second deadline. A large sibling projection cannot consume the waiting
910
1007
  // panes' entire capture budgets before their commands have been written.
911
- this.reseed(sub, (result) => {
912
- if (this.plainReseedActive !== sub)
1008
+ this.reseed(lease, (result) => {
1009
+ if (this.plainReseedActive !== lease)
913
1010
  return;
1011
+ sub.cancelCapture = null;
914
1012
  this.plainReseedActive = null;
915
1013
  if (!result.ok) {
916
1014
  // A stalled FIFO cannot make progress for queued recipes either. Retire
@@ -930,16 +1028,6 @@ export class SessionChannel {
930
1028
  if (resumeReason && !sub.closed && !sub.frozen)
931
1029
  sub.onEvent({ type: "flow", state: "resumed", reason: resumeReason });
932
1030
  }, true);
933
- if (this.plainReseedActive === sub) {
934
- const cancel = sub.cancelCapture;
935
- sub.cancelCapture = () => {
936
- cancel?.();
937
- this.plainReseedQueue.delete(sub);
938
- if (this.plainReseedActive === sub)
939
- this.plainReseedActive = null;
940
- this.drainPlainReseeds();
941
- };
942
- }
943
1031
  }
944
1032
  retireInternalReadMarker(runtime, marker) {
945
1033
  if (!/^%(?:0|[1-9][0-9]*)$/u.test(runtime))
@@ -2014,6 +2102,11 @@ export class SessionChannel {
2014
2102
  return;
2015
2103
  }
2016
2104
  if (name === "session-window-changed") {
2105
+ // The notification carries backing identity only; duplicate links require an index read.
2106
+ if (this.windowLinkAuthority) {
2107
+ this.scheduleSync();
2108
+ return;
2109
+ }
2017
2110
  const change = parseSessionWindowChanged(rest);
2018
2111
  if (!change)
2019
2112
  return;
@@ -2097,6 +2190,9 @@ export class SessionChannel {
2097
2190
  this.emitLayoutAuthorityTo(subscriber, runtimeSessionId);
2098
2191
  }
2099
2192
  emitLayoutAuthorityTo(subscriber, runtimeSessionId) {
2193
+ const windowLinks = this.windowLinkAuthority?.snapshot();
2194
+ if (!windowLinks)
2195
+ return;
2100
2196
  const layouts = [...this.layoutByWindow.keys()]
2101
2197
  .map((runtimeId) => this.layoutEventFor(runtimeId))
2102
2198
  .filter((event) => event !== null);
@@ -2104,6 +2200,7 @@ export class SessionChannel {
2104
2200
  session: this.opts.session,
2105
2201
  runtimeSessionId,
2106
2202
  topologyEpoch: this.layoutTopologyEpoch,
2203
+ windowLinks,
2107
2204
  layouts,
2108
2205
  });
2109
2206
  }
@@ -2207,8 +2304,9 @@ export class SessionChannel {
2207
2304
  windowActive: windowActive === "1",
2208
2305
  });
2209
2306
  }
2307
+ const previousCurrentWindow = this.currentWindow;
2210
2308
  const { listed, movedWindowRuntimeIds } = this.applyPaneTruth(truth);
2211
- await this.syncWindows(this.opts.session, movedWindowRuntimeIds);
2309
+ await this.syncWindows(this.opts.session, movedWindowRuntimeIds, previousCurrentWindow);
2212
2310
  this.discovery.discover(listed);
2213
2311
  }
2214
2312
  applyPaneTruth(truth) {
@@ -2266,7 +2364,20 @@ export class SessionChannel {
2266
2364
  parsed.descriptors.length !== beforeLines.length) {
2267
2365
  throw new Error(`trusted inventory for ${this.opts.session} is malformed`);
2268
2366
  }
2269
- const descriptors = parsed.descriptors;
2367
+ const descriptorByPane = new Map();
2368
+ for (const row of parsed.descriptors) {
2369
+ const previous = descriptorByPane.get(row.runtimePaneId);
2370
+ if (previous) {
2371
+ const comparable = (value) => JSON.stringify({ ...value, windowActive: false, windowIndex: null });
2372
+ if (comparable(previous) !== comparable(row))
2373
+ throw new Error("Inconsistent linked pane observation");
2374
+ if (row.windowActive)
2375
+ descriptorByPane.set(row.runtimePaneId, row);
2376
+ }
2377
+ else
2378
+ descriptorByPane.set(row.runtimePaneId, row);
2379
+ }
2380
+ const descriptors = [...descriptorByPane.values()];
2270
2381
  const runtimePaneIds = new Set(descriptors.map((pane) => pane.runtimePaneId));
2271
2382
  const runtimeSessionIds = new Set(descriptors.map((pane) => pane.runtimeSessionId));
2272
2383
  const activeWindowIds = new Set(descriptors.filter((pane) => pane.windowActive).map((pane) => pane.windowId));
@@ -2286,10 +2397,26 @@ export class SessionChannel {
2286
2397
  computedWindowCounts.set(runtimeWindowId, (computedWindowCounts.get(runtimeWindowId) ?? 0) + 1);
2287
2398
  }
2288
2399
  if (descriptors.some((pane) => pane.windowPaneCount !== computedWindowCounts.get(pane.windowId) ||
2289
- pane.sessionWindowCount !== computedWindowCounts.size)) {
2400
+ pane.sessionWindowCount !== descriptors[0].sessionWindowCount)) {
2290
2401
  throw new Error(`trusted inventory for ${this.opts.session} has incomplete counts`);
2291
2402
  }
2292
2403
  const windowStage = await this.stageWindows(expectedRuntimeSessionId);
2404
+ const descriptorKeys = new Set();
2405
+ for (const row of parsed.descriptors) {
2406
+ const link = windowStage.links.find((link) => link.index === row.windowIndex);
2407
+ const key = `${row.windowIndex}:${row.runtimePaneId}`;
2408
+ if (!link ||
2409
+ link.runtimeWindowId !== row.windowId ||
2410
+ link.active !== row.windowActive ||
2411
+ descriptorKeys.has(key))
2412
+ throw new Error(`trusted inventory for ${this.opts.session} lacks verified identity: inconsistent linked pane membership`);
2413
+ descriptorKeys.add(key);
2414
+ }
2415
+ for (const link of windowStage.links) {
2416
+ const count = computedWindowCounts.get(link.runtimeWindowId);
2417
+ if (parsed.descriptors.filter((row) => row.windowIndex === link.index).length !== count)
2418
+ throw new Error("Incomplete linked pane membership");
2419
+ }
2293
2420
  const repairedPanes = await this.repairTrustedPaneIdentity(descriptors, windowStage.layouts);
2294
2421
  const afterLines = await this.io.request(`list-panes -s -t "${expectedRuntimeSessionId}" -F "${SESSION_PANE_DESCRIPTOR_FORMAT}"`);
2295
2422
  const confirmedWindowStage = await this.stageWindows(expectedRuntimeSessionId);
@@ -2330,6 +2457,7 @@ export class SessionChannel {
2330
2457
  break;
2331
2458
  }
2332
2459
  if (confirmedWindowStage.currentWindow !== activeWindowId ||
2460
+ confirmedWindowStage.links.length !== descriptors[0].sessionWindowCount ||
2333
2461
  confirmedWindowStage.windows.size !== computedWindowCounts.size ||
2334
2462
  !stagedPaneMembershipExact ||
2335
2463
  stagedPaneCount !== descriptors.length ||
@@ -2367,7 +2495,7 @@ export class SessionChannel {
2367
2495
  throw new Error(`trusted inventory for ${this.opts.session} is degraded`);
2368
2496
  }
2369
2497
  const windowCounts = computedWindowCounts;
2370
- const sessionWindowCount = computedWindowCounts.size;
2498
+ const sessionWindowCount = confirmedWindowStage.links.length;
2371
2499
  const panes = descriptors.map((descriptor) => {
2372
2500
  const record = this.panesByRuntime.get(descriptor.runtimePaneId);
2373
2501
  const runtimeWindowId = descriptor.windowId;
@@ -2406,12 +2534,18 @@ export class SessionChannel {
2406
2534
  panes: Object.freeze(panes),
2407
2535
  });
2408
2536
  }
2409
- async syncWindows(target = this.opts.session, requiredLayoutEmits = new Set()) {
2410
- const stage = await this.stageWindows(target);
2411
- this.commitWindowStage(stage, requiredLayoutEmits);
2537
+ async syncWindows(target = this.opts.session, requiredLayoutEmits = new Set(), previousCurrentWindow = this.currentWindow) {
2538
+ const stage = await this.stageWindows(target).catch((error) => {
2539
+ this.windowLinkAuthority?.invalidate();
2540
+ this.latestWindowStage = null;
2541
+ throw error;
2542
+ });
2543
+ this.commitWindowStage(stage, requiredLayoutEmits, previousCurrentWindow);
2412
2544
  return stage.repairedIdentity;
2413
2545
  }
2414
2546
  commitWindowStage(stage, requiredLayoutEmits = new Set(), previousCurrentWindow = this.currentWindow) {
2547
+ this.windowLinkAuthority?.reconcile(stage.links);
2548
+ this.latestWindowStage = stage;
2415
2549
  const changedWindows = new Set();
2416
2550
  for (const [runtimeId, record] of stage.windows) {
2417
2551
  const previous = this.windowsByRuntime.get(runtimeId);
@@ -2468,7 +2602,8 @@ export class SessionChannel {
2468
2602
  this.emitLayoutAuthority();
2469
2603
  }
2470
2604
  windowStagesEqual(left, right) {
2471
- if (left.currentWindow !== right.currentWindow ||
2605
+ if (JSON.stringify(left.links) !== JSON.stringify(right.links) ||
2606
+ left.currentWindow !== right.currentWindow ||
2472
2607
  left.windows.size !== right.windows.size ||
2473
2608
  left.layouts.size !== right.layouts.size) {
2474
2609
  return false;
@@ -2504,29 +2639,35 @@ export class SessionChannel {
2504
2639
  return true;
2505
2640
  }
2506
2641
  async stageWindows(target = this.opts.session) {
2507
- const lines = await this.io.request(`list-windows -t "${target}" -F "#{window_id}\t#{qa:@tmux_ide_window_id}\t#{qa:window_name}\t#{window_active}\t#{window_visible_layout}\t#{?window_zoomed_flag,1,0}\t#{pane-border-status}\t#{window_layout}\t#{mode-keys}"`);
2642
+ const lines = await this.io.request(`list-windows -t "${target}" -F "#{window_id}\t#{qa:@tmux_ide_window_id}\t#{qa:window_name}\t#{window_active}\t#{window_visible_layout}\t#{?window_zoomed_flag,1,0}\t#{pane-border-status}\t#{window_layout}\t#{mode-keys}\t#{window_index}"`);
2643
+ if (lines.length > WINDOW_LINK_MAX_LINKS)
2644
+ throw new Error("Window link observation exceeds bound");
2508
2645
  const rows = [];
2509
- const seenRuntimeIds = new Set();
2646
+ const seenIndexes = new Set();
2510
2647
  for (const raw of lines) {
2511
2648
  // Replies are latin1 byte strings; recover UTF-8 window names first.
2512
2649
  const line = Buffer.from(raw, "latin1").toString("utf8");
2513
2650
  const parts = line.split("\t");
2514
- if (parts.length !== 7 && parts.length !== 8 && parts.length !== 9) {
2651
+ if (parts.length !== 10) {
2515
2652
  throw new Error(`window layout truth for ${this.opts.session} is malformed`);
2516
2653
  }
2517
- const [runtimeId = "", stampRaw = "", nameRaw = "", active = "", visible = "", zoomed = "", borderStatus = "off", full = visible, modeKeys,] = parts;
2654
+ const [runtimeId = "", stampRaw = "", nameRaw = "", active = "", visible = "", zoomed = "", borderStatus = "off", full = visible, modeKeys, indexRaw = "",] = parts;
2655
+ const index = Number(indexRaw);
2518
2656
  if (!/^@[0-9]+$/u.test(runtimeId) ||
2519
- seenRuntimeIds.has(runtimeId) ||
2657
+ !/^(?:0|[1-9][0-9]*)$/u.test(indexRaw) ||
2658
+ !Number.isSafeInteger(index) ||
2659
+ seenIndexes.has(index) ||
2520
2660
  (active !== "0" && active !== "1") ||
2521
2661
  (zoomed !== "0" && zoomed !== "1") ||
2522
2662
  (borderStatus !== "top" && borderStatus !== "bottom" && borderStatus !== "off") ||
2523
2663
  (modeKeys !== undefined && modeKeys !== "emacs" && modeKeys !== "vi")) {
2524
2664
  throw new Error(`window layout truth for ${this.opts.session} is malformed`);
2525
2665
  }
2526
- seenRuntimeIds.add(runtimeId);
2666
+ seenIndexes.add(index);
2527
2667
  const stamp = decodeTmuxArgument(stampRaw);
2528
2668
  const name = decodeTmuxArgument(nameRaw);
2529
2669
  rows.push({
2670
+ index,
2530
2671
  runtimeId,
2531
2672
  stamp: stamp.length > 0 ? stamp : null,
2532
2673
  name: name.length > 0 ? name : null,
@@ -2545,8 +2686,22 @@ export class SessionChannel {
2545
2686
  if (activeRows.length !== 1) {
2546
2687
  throw new Error(`window layout truth for ${this.opts.session} has inconsistent active window`);
2547
2688
  }
2548
- const nextLayoutByWindow = new Map();
2689
+ const backingRows = new Map();
2549
2690
  for (const row of rows) {
2691
+ const previous = backingRows.get(row.runtimeId);
2692
+ if (previous &&
2693
+ (previous.stamp !== row.stamp ||
2694
+ previous.name !== row.name ||
2695
+ previous.visible !== row.visible ||
2696
+ previous.full !== row.full ||
2697
+ previous.zoomed !== row.zoomed ||
2698
+ previous.paneBorderStatus !== row.paneBorderStatus ||
2699
+ previous.modeKeys !== row.modeKeys))
2700
+ throw new Error("Inconsistent linked backing observation");
2701
+ backingRows.set(row.runtimeId, row);
2702
+ }
2703
+ const nextLayoutByWindow = new Map();
2704
+ for (const row of backingRows.values()) {
2550
2705
  const parsed = parseLayout(row.visible);
2551
2706
  if (!parsed) {
2552
2707
  throw new Error(`window layout truth for ${this.opts.session} is malformed`);
@@ -2559,7 +2714,7 @@ export class SessionChannel {
2559
2714
  // Valid unique stamps are identity; missing/invalid/duplicated stamps are
2560
2715
  // ALL regenerated and stamped back (the pane policy, applied to windows).
2561
2716
  const stampCounts = new Map();
2562
- for (const row of rows) {
2717
+ for (const row of backingRows.values()) {
2563
2718
  if (row.stamp && WorkspaceIdSchemaZ.safeParse(row.stamp).success) {
2564
2719
  stampCounts.set(row.stamp, (stampCounts.get(row.stamp) ?? 0) + 1);
2565
2720
  }
@@ -2568,10 +2723,8 @@ export class SessionChannel {
2568
2723
  const generateWindowId = this.opts.generateWindowId ?? defaultMirrorWindowId;
2569
2724
  let repairedIdentity = false;
2570
2725
  const next = new Map();
2571
- let nextCurrentWindow = "";
2572
- for (const row of rows) {
2573
- if (row.active)
2574
- nextCurrentWindow = row.runtimeId;
2726
+ const nextCurrentWindow = activeRows[0].runtimeId;
2727
+ for (const row of backingRows.values()) {
2575
2728
  let semanticId = null;
2576
2729
  if (row.stamp && stampCounts.get(row.stamp) === 1) {
2577
2730
  semanticId = row.stamp;
@@ -2614,6 +2767,12 @@ export class SessionChannel {
2614
2767
  layouts: nextLayoutByWindow,
2615
2768
  currentWindow: nextCurrentWindow,
2616
2769
  repairedIdentity,
2770
+ links: rows.map((row) => ({
2771
+ index: row.index,
2772
+ runtimeWindowId: row.runtimeId,
2773
+ semanticWindowId: next.get(row.runtimeId).semanticId ?? "",
2774
+ active: row.active,
2775
+ })),
2617
2776
  };
2618
2777
  }
2619
2778
  async reconcileIdentity(descriptors, listed) {
@@ -2804,6 +2963,7 @@ export class SessionChannel {
2804
2963
  onChannelExit() {
2805
2964
  if (this.disposed)
2806
2965
  return;
2966
+ this.windowLinkAuthority?.dispose();
2807
2967
  this.nativeGrid.dispose();
2808
2968
  this.settleFirstJoin();
2809
2969
  for (const runtime of [...this.recoveries.keys()])