tmux-ide 2.9.2 → 2.9.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (267) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +121 -140
  3. package/bin/cli.js +2457 -727
  4. package/package.json +23 -8
  5. package/packages/contracts/src/desktop-host.ts +3 -0
  6. package/packages/contracts/src/saved-machines.ts +19 -0
  7. package/packages/contracts/src/terminal-replica.ts +1 -1
  8. package/packages/core/package.json +3 -2
  9. package/packages/core/src/terminal-delivery.ts +51 -3
  10. package/packages/core/src/terminal-fnv64-wasm.ts +4 -3
  11. package/packages/core/src/terminal-replica-hash-cache.ts +129 -31
  12. package/packages/core/src/terminal-replica-owned-row.ts +27 -0
  13. package/packages/core/src/terminal-replica.ts +13 -28
  14. package/packages/daemon/dist/command-center/daemon-fleet-facts-observer.js +96 -10
  15. package/packages/daemon/dist/command-center/resources/saved-machine-route.js +40 -1
  16. package/packages/daemon/dist/command-center/ws-events.js +5 -24
  17. package/packages/daemon/dist/lib/canonical-daemon-bootstrap.js +8 -0
  18. package/packages/daemon/dist/lib/canonical-daemon.js +28 -2
  19. package/packages/daemon/dist/lib/daemon-embed.js +1 -1
  20. package/packages/daemon/dist/lib/daemon-service-manager.js +122 -0
  21. package/packages/daemon/dist/lib/daemon-service-plan.js +127 -0
  22. package/packages/daemon/dist/lib/daemon-service.js +259 -0
  23. package/packages/daemon/dist/lib/install-origin.js +2 -0
  24. package/packages/daemon/dist/lib/local-fleet-request.js +9 -3
  25. package/packages/daemon/dist/lib/log.js +6 -10
  26. package/packages/daemon/dist/lib/managed-installer-update.js +67 -0
  27. package/packages/daemon/dist/lib/{tui-download-lock.js → private-operation-lock.js} +11 -11
  28. package/packages/daemon/dist/lib/remote-daemon-info.js +3 -2
  29. package/packages/daemon/dist/lib/remote-daemon-lifecycle.js +4 -4
  30. package/packages/daemon/dist/lib/remote-tmux-command.js +10 -0
  31. package/packages/daemon/dist/lib/runtime-namespace.js +28 -16
  32. package/packages/daemon/dist/lib/saved-machines.js +40 -2
  33. package/packages/daemon/dist/lib/ssh-daemon-transport.js +24 -9
  34. package/packages/daemon/dist/lib/tmux-server-owner.js +1 -1
  35. package/packages/daemon/dist/lib/tmux-server-registration.js +21 -3
  36. package/packages/daemon/dist/lib/tui-binary.js +2 -2
  37. package/packages/daemon/dist/lib/update.js +18 -0
  38. package/packages/daemon/dist/lib/workspace-multiplexer-verbs.js +24 -1
  39. package/packages/daemon/dist/machines.js +40 -4
  40. package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/Info.plist +2 -2
  41. package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/MacOS/tmux-ide-notifier +0 -0
  42. package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/Resources/Assets.car +0 -0
  43. package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/_CodeSignature/CodeResources +2 -2
  44. package/packages/daemon/dist/terminal/attachments/direct-websocket.js +4 -1
  45. package/packages/daemon/dist/terminal/attachments/native-runtime.js +22 -13
  46. package/packages/daemon/dist/terminal/mirror/mirror-service.js +101 -3
  47. package/packages/daemon/dist/terminal/mirror/registered-window-guard.js +62 -0
  48. package/packages/daemon/dist/terminal/mirror/session-channel.js +75 -4
  49. package/packages/daemon/dist/terminal/mirror/shared-window-index.js +45 -0
  50. package/packages/daemon/dist/terminal/pane-stream/pane-stream-websocket.js +11 -1
  51. package/packages/daemon/dist/terminal/session-runtime/multiplexer-backend.js +1 -0
  52. package/packages/daemon/dist/terminal/session-runtime/registry.js +56 -7
  53. package/packages/daemon/dist/terminal/session-runtime/semantic-mutation-executor.js +3 -1
  54. package/packages/daemon/dist/tui/integrations/claude-agent-teams.js +264 -0
  55. package/packages/daemon/dist/tui/mirror/dialog-model.js +2 -35
  56. package/packages/daemon/dist/tui/mirror/runtime/application-daemon-authority-owner.js +8 -0
  57. package/packages/daemon/dist/tui/mirror/runtime/application-home-fleet.js +20 -2
  58. package/packages/daemon/dist/tui/mirror/runtime/application-machine-agents.js +7 -1
  59. package/packages/daemon/dist/tui/mirror/runtime/application-machine-authority.js +59 -9
  60. package/packages/daemon/dist/tui/mirror/runtime/application-machine-navigation.js +27 -19
  61. package/packages/daemon/dist/tui/mirror/runtime/application-machine-sidebar.jsx +23 -18
  62. package/packages/daemon/dist/tui/mirror/runtime/application-machine-startup.js +31 -2
  63. package/packages/daemon/dist/tui/mirror/runtime/application-palette-search-owner.js +7 -3
  64. package/packages/daemon/dist/tui/mirror/runtime/application-performance-log.js +3 -0
  65. package/packages/daemon/dist/tui/mirror/runtime/application-terminal-input-ingress.js +146 -2
  66. package/packages/daemon/dist/tui/mirror/runtime/application-terminal-interaction-controller.js +201 -50
  67. package/packages/daemon/dist/tui/mirror/runtime/bounded-performance-record-writer.js +50 -10
  68. package/packages/daemon/dist/tui/mirror/runtime/terminal-fast-lane-renderer-adapter.js +6 -5
  69. package/packages/daemon/dist/tui/mirror/runtime/terminal-pane-input-router.js +3 -0
  70. package/packages/daemon/dist/tui/mirror/runtime/workspace-terminal-fast-lane.js +8 -2
  71. package/packages/daemon/dist/tui/mirror/terminal-text.js +35 -0
  72. package/packages/daemon/dist/tui/mirror/workspace/connection-feedback.js +5 -3
  73. package/packages/daemon/src/command-center/daemon-fleet-facts-observer.ts +100 -10
  74. package/packages/daemon/src/command-center/resources/saved-machine-route.ts +48 -1
  75. package/packages/daemon/src/command-center/ws-events.ts +5 -25
  76. package/packages/daemon/src/lib/canonical-daemon-bootstrap.ts +12 -0
  77. package/packages/daemon/src/lib/canonical-daemon.ts +44 -2
  78. package/packages/daemon/src/lib/daemon-embed.ts +1 -0
  79. package/packages/daemon/src/lib/daemon-service-manager.ts +152 -0
  80. package/packages/daemon/src/lib/daemon-service-plan.ts +153 -0
  81. package/packages/daemon/src/lib/daemon-service.ts +301 -0
  82. package/packages/daemon/src/lib/install-origin.ts +5 -0
  83. package/packages/daemon/src/lib/local-fleet-request.ts +19 -3
  84. package/packages/daemon/src/lib/log.ts +13 -9
  85. package/packages/daemon/src/lib/managed-installer-update.ts +79 -0
  86. package/packages/daemon/src/lib/{tui-download-lock.ts → private-operation-lock.ts} +15 -11
  87. package/packages/daemon/src/lib/remote-daemon-info.ts +3 -3
  88. package/packages/daemon/src/lib/remote-daemon-lifecycle.ts +4 -4
  89. package/packages/daemon/src/lib/remote-tmux-command.ts +12 -0
  90. package/packages/daemon/src/lib/runtime-namespace.ts +27 -18
  91. package/packages/daemon/src/lib/saved-machines.ts +40 -1
  92. package/packages/daemon/src/lib/ssh-daemon-transport.ts +25 -7
  93. package/packages/daemon/src/lib/tmux-server-owner.ts +1 -0
  94. package/packages/daemon/src/lib/tmux-server-registration.ts +21 -3
  95. package/packages/daemon/src/lib/tui-binary.ts +2 -2
  96. package/packages/daemon/src/lib/update.ts +24 -0
  97. package/packages/daemon/src/lib/workspace-multiplexer-verbs.ts +22 -0
  98. package/packages/daemon/src/machines.ts +53 -5
  99. package/packages/daemon/src/terminal/attachments/direct-websocket.ts +8 -2
  100. package/packages/daemon/src/terminal/attachments/native-runtime.ts +28 -13
  101. package/packages/daemon/src/terminal/mirror/mirror-service.ts +105 -3
  102. package/packages/daemon/src/terminal/mirror/registered-window-guard.ts +66 -0
  103. package/packages/daemon/src/terminal/mirror/session-channel.ts +92 -4
  104. package/packages/daemon/src/terminal/mirror/shared-window-index.ts +42 -0
  105. package/packages/daemon/src/terminal/pane-stream/pane-stream-websocket.ts +12 -1
  106. package/packages/daemon/src/terminal/session-runtime/multiplexer-backend.ts +2 -0
  107. package/packages/daemon/src/terminal/session-runtime/registry.ts +70 -8
  108. package/packages/daemon/src/terminal/session-runtime/semantic-mutation-executor.ts +3 -1
  109. package/packages/daemon/src/tui/integrations/claude-agent-teams.ts +334 -0
  110. package/packages/daemon/src/tui/mirror/dialog-model.ts +2 -32
  111. package/packages/daemon/src/tui/mirror/runtime/application-daemon-authority-owner.ts +8 -0
  112. package/packages/daemon/src/tui/mirror/runtime/application-home-fleet.ts +19 -2
  113. package/packages/daemon/src/tui/mirror/runtime/application-machine-agents.ts +6 -1
  114. package/packages/daemon/src/tui/mirror/runtime/application-machine-authority.ts +60 -11
  115. package/packages/daemon/src/tui/mirror/runtime/application-machine-navigation.ts +39 -33
  116. package/packages/daemon/src/tui/mirror/runtime/application-machine-sidebar.tsx +31 -23
  117. package/packages/daemon/src/tui/mirror/runtime/application-machine-startup.ts +38 -2
  118. package/packages/daemon/src/tui/mirror/runtime/application-palette-search-owner.ts +7 -4
  119. package/packages/daemon/src/tui/mirror/runtime/application-performance-log.ts +3 -0
  120. package/packages/daemon/src/tui/mirror/runtime/application-root-v2.tsx +1 -1
  121. package/packages/daemon/src/tui/mirror/runtime/application-shell-view.tsx +47 -46
  122. package/packages/daemon/src/tui/mirror/runtime/application-terminal-input-ingress.ts +165 -2
  123. package/packages/daemon/src/tui/mirror/runtime/application-terminal-interaction-controller.ts +207 -53
  124. package/packages/daemon/src/tui/mirror/runtime/bounded-performance-record-writer.ts +49 -9
  125. package/packages/daemon/src/tui/mirror/runtime/terminal-fast-lane-renderer-adapter.ts +6 -5
  126. package/packages/daemon/src/tui/mirror/runtime/terminal-pane-input-router.ts +4 -0
  127. package/packages/daemon/src/tui/mirror/runtime/workspace-terminal-fast-lane.ts +8 -3
  128. package/packages/daemon/src/tui/mirror/terminal-text.ts +32 -0
  129. package/packages/daemon/src/tui/mirror/workspace/connection-feedback.ts +6 -3
  130. package/packages/daemon-client/src/fleet-connection-status.ts +10 -1
  131. package/packages/daemon-client/src/workspace-client.ts +4 -1
  132. package/skill/SKILL.md +30 -30
  133. package/packages/contracts/src/__tests__/actions-contract.test.ts +0 -119
  134. package/packages/contracts/src/__tests__/agent-graph-overlay.test.ts +0 -428
  135. package/packages/contracts/src/__tests__/app-window-mutation.test.ts +0 -51
  136. package/packages/contracts/src/__tests__/app-window-state.test.ts +0 -34
  137. package/packages/contracts/src/__tests__/application-shell.test.ts +0 -645
  138. package/packages/contracts/src/__tests__/automation-operations.test.ts +0 -78
  139. package/packages/contracts/src/__tests__/causal-cell.test.ts +0 -47
  140. package/packages/contracts/src/__tests__/client-view-state.test.ts +0 -115
  141. package/packages/contracts/src/__tests__/cohesion-fixture.test.ts +0 -106
  142. package/packages/contracts/src/__tests__/commands.test.ts +0 -86
  143. package/packages/contracts/src/__tests__/control.test.ts +0 -154
  144. package/packages/contracts/src/__tests__/daemon-events.test.ts +0 -405
  145. package/packages/contracts/src/__tests__/daemon-resources.test.ts +0 -143
  146. package/packages/contracts/src/__tests__/daemon-wire.test.ts +0 -149
  147. package/packages/contracts/src/__tests__/desktop-host.test.ts +0 -269
  148. package/packages/contracts/src/__tests__/desktop-missions.test.ts +0 -215
  149. package/packages/contracts/src/__tests__/experience-conformance.test.ts +0 -153
  150. package/packages/contracts/src/__tests__/experience-kernel-boundary.test.ts +0 -113
  151. package/packages/contracts/src/__tests__/experience-shell.test.ts +0 -65
  152. package/packages/contracts/src/__tests__/fixtures/daemon-wire-v2.json +0 -35
  153. package/packages/contracts/src/__tests__/fixtures/daemon-wire-v3.json +0 -35
  154. package/packages/contracts/src/__tests__/fixtures/daemon-wire-v4.json +0 -35
  155. package/packages/contracts/src/__tests__/fixtures/scoped-receipt.ts +0 -62
  156. package/packages/contracts/src/__tests__/fleet-agent-graph.test.ts +0 -231
  157. package/packages/contracts/src/__tests__/fleet-catalog.test.ts +0 -270
  158. package/packages/contracts/src/__tests__/fleet-lifecycle.test.ts +0 -62
  159. package/packages/contracts/src/__tests__/focus-overlay.test.ts +0 -110
  160. package/packages/contracts/src/__tests__/ide-config.test.ts +0 -64
  161. package/packages/contracts/src/__tests__/interaction-evidence.test.ts +0 -288
  162. package/packages/contracts/src/__tests__/interaction-receipt-v2.test.ts +0 -72
  163. package/packages/contracts/src/__tests__/issue-error.test.ts +0 -78
  164. package/packages/contracts/src/__tests__/mission-projections.test.ts +0 -186
  165. package/packages/contracts/src/__tests__/missions.test.ts +0 -272
  166. package/packages/contracts/src/__tests__/multiplexer-verbs.test.ts +0 -242
  167. package/packages/contracts/src/__tests__/pane-appearance.test.ts +0 -146
  168. package/packages/contracts/src/__tests__/pane-read-result.test.ts +0 -55
  169. package/packages/contracts/src/__tests__/pane-stream.test.ts +0 -536
  170. package/packages/contracts/src/__tests__/pane-widget-marker.test.ts +0 -327
  171. package/packages/contracts/src/__tests__/performance-metrics.test.ts +0 -73
  172. package/packages/contracts/src/__tests__/performance-qualification.test.ts +0 -137
  173. package/packages/contracts/src/__tests__/rich-card-widget.test.ts +0 -29
  174. package/packages/contracts/src/__tests__/session-runtime.test.ts +0 -512
  175. package/packages/contracts/src/__tests__/startup-readiness.test.ts +0 -409
  176. package/packages/contracts/src/__tests__/terminal-attachment-stream.test.ts +0 -72
  177. package/packages/contracts/src/__tests__/terminal-attachments.test.ts +0 -329
  178. package/packages/contracts/src/__tests__/terminal-delivery.test.ts +0 -88
  179. package/packages/contracts/src/__tests__/terminal-runtime-inventory.test.ts +0 -68
  180. package/packages/contracts/src/__tests__/terminals.test.ts +0 -87
  181. package/packages/contracts/src/__tests__/tmux-server-scope.test.ts +0 -166
  182. package/packages/contracts/src/__tests__/transport-contract-loading.test.ts +0 -91
  183. package/packages/contracts/src/__tests__/visual-tokens.test.ts +0 -224
  184. package/packages/contracts/src/__tests__/window-links.test.ts +0 -243
  185. package/packages/contracts/src/__tests__/workspace-changes-resource.test.ts +0 -317
  186. package/packages/contracts/src/__tests__/workspace-changes-view.test.ts +0 -122
  187. package/packages/contracts/src/__tests__/workspace-config.test.ts +0 -294
  188. package/packages/contracts/src/__tests__/workspace-files-resource.test.ts +0 -261
  189. package/packages/contracts/src/__tests__/workspace-files-tree.test.ts +0 -147
  190. package/packages/contracts/src/__tests__/workspace-pane-creation.test.ts +0 -249
  191. package/packages/contracts/src/__tests__/workspace-promotion.test.ts +0 -94
  192. package/packages/contracts/src/__tests__/workspace-resource-identity.test.ts +0 -90
  193. package/packages/contracts/src/daemon-resource-request.test.ts +0 -192
  194. package/packages/contracts/src/native-operation-identity.test.ts +0 -23
  195. package/packages/contracts/src/workspace-admission.test.ts +0 -21
  196. package/packages/contracts/src/workspace-catalog-resource.test.ts +0 -146
  197. package/packages/core/src/agent-provisioning.test.ts +0 -42
  198. package/packages/core/src/application-shell-session.test.ts +0 -105
  199. package/packages/core/src/interaction-evidence.test.ts +0 -134
  200. package/packages/core/src/interaction-flow.test.ts +0 -66
  201. package/packages/core/src/interaction-receipts.test.ts +0 -671
  202. package/packages/core/src/navigator.test.ts +0 -34
  203. package/packages/core/src/optimistic-projection-conformance.test.ts +0 -75
  204. package/packages/core/src/optimistic-projection.test.ts +0 -118
  205. package/packages/core/src/performance-metrics.test.ts +0 -121
  206. package/packages/core/src/performance-qualification.test.ts +0 -283
  207. package/packages/core/src/saved-machines.test.ts +0 -80
  208. package/packages/core/src/terminal-compact-cold.test.ts +0 -87
  209. package/packages/core/src/terminal-conformance.test.ts +0 -48
  210. package/packages/core/src/terminal-delivery-cache-lifetime.test.ts +0 -167
  211. package/packages/core/src/terminal-delivery-encode-cooperative.test.ts +0 -140
  212. package/packages/core/src/terminal-delivery-representation-hash.test.ts +0 -77
  213. package/packages/core/src/terminal-delivery.test.ts +0 -1471
  214. package/packages/core/src/terminal-fnv64-wasm.test.ts +0 -61
  215. package/packages/core/src/terminal-replica-cold.test.ts +0 -31
  216. package/packages/core/src/terminal-replica-cooperative.test.ts +0 -170
  217. package/packages/core/src/terminal-replica-hash-cache.test.ts +0 -283
  218. package/packages/core/src/terminal-replica.test.ts +0 -347
  219. package/packages/core/src/workspace-selection.test.ts +0 -63
  220. package/packages/daemon/dist/__tests__/support.js +0 -33
  221. package/packages/daemon/dist/lib/__tests__/fixtures/workspace-state-writer.js +0 -58
  222. package/packages/daemon/dist/lib/__tests__/installed-recovery-fixture.js +0 -400
  223. package/packages/daemon/dist/terminal/__tests__/MockPtyAdapter.js +0 -182
  224. package/packages/daemon/dist/terminal/mirror/__tests__/scripted-channel.js +0 -98
  225. package/packages/daemon/dist/terminal/mirror/__tests__/simulated-channel.js +0 -156
  226. package/packages/daemon/src/tui/mirror/__snapshots__/activity-surface-renderer.test.tsx.snap +0 -42
  227. package/packages/daemon/src/tui/mirror/__snapshots__/changes-terminal-surface-renderer.test.tsx.snap +0 -267
  228. package/packages/daemon/src/tui/mirror/__snapshots__/home-files-surface-renderer.test.tsx.snap +0 -267
  229. package/packages/daemon/src/tui/mirror/__snapshots__/missions-surface-renderer.test.tsx.snap +0 -134
  230. package/packages/daemon/src/tui/mirror/__snapshots__/recipes-gallery-renderer.test.tsx.snap +0 -141
  231. package/packages/daemon/src/tui/mirror/__snapshots__/shell-chrome-renderer.test.tsx.snap +0 -134
  232. package/packages/daemon/src/tui/mirror/runtime/__snapshots__/application-shell-view-renderer.test.tsx.snap +0 -87
  233. package/packages/daemon/src/tui/mirror/workspace/__snapshots__/agent-terminal-canvas-renderer.test.tsx.snap +0 -348
  234. package/packages/daemon/src/tui/mirror/workspace/__snapshots__/application-shell-renderer.test.tsx.snap +0 -134
  235. package/packages/daemon/src/tui/mirror/workspace/__snapshots__/command-palette-surface-renderer.test.tsx.snap +0 -255
  236. package/packages/daemon/src/tui/mirror/workspace/__snapshots__/pane-frame-renderer.test.tsx.snap +0 -525
  237. package/packages/daemon/src/tui/mirror/workspace/__snapshots__/terminal-pane-header-polish-renderer.test.tsx.snap +0 -31
  238. package/packages/daemon/src/tui/mirror/workspace/__snapshots__/workbench-shell-renderer.test.tsx.snap +0 -134
  239. package/packages/daemon-client/src/application-shell-session.test.ts +0 -196
  240. package/packages/daemon-client/src/automation-client.test.ts +0 -214
  241. package/packages/daemon-client/src/bootstrap-coordinator.test.ts +0 -145
  242. package/packages/daemon-client/src/connection-supervisor.test.ts +0 -109
  243. package/packages/daemon-client/src/direct-application-shell-transport.test.ts +0 -941
  244. package/packages/daemon-client/src/first-latest-coordinator.test.ts +0 -175
  245. package/packages/daemon-client/src/fleet-dial-scheduler.test.ts +0 -114
  246. package/packages/daemon-client/src/fleet-environments.test.ts +0 -40
  247. package/packages/daemon-client/src/generation-bound-store-timers.test.ts +0 -74
  248. package/packages/daemon-client/src/owner-action-client.test.ts +0 -245
  249. package/packages/daemon-client/src/pane-stream-client.test.ts +0 -2841
  250. package/packages/daemon-client/src/pane-stream-clock-calibration.test.ts +0 -207
  251. package/packages/daemon-client/src/pane-stream-loopback-benchmark.test.ts +0 -51
  252. package/packages/daemon-client/src/push-resource-session.test.ts +0 -509
  253. package/packages/daemon-client/src/resource-replica.test.ts +0 -186
  254. package/packages/daemon-client/src/runtime-resource-ledger.test.ts +0 -18
  255. package/packages/daemon-client/src/scoped-tmux-server-transport.test.ts +0 -114
  256. package/packages/daemon-client/src/terminal-fast-lane.test.ts +0 -835
  257. package/packages/daemon-client/src/tmux-server-client.test.ts +0 -279
  258. package/packages/daemon-client/src/tmux-server-interaction-events.test.ts +0 -230
  259. package/packages/daemon-client/src/workspace-catalog-v2.test.ts +0 -109
  260. package/packages/daemon-client/src/workspace-client-boundaries.test.ts +0 -59
  261. package/packages/daemon-client/src/workspace-client-operations.test.ts +0 -96
  262. package/packages/daemon-client/src/workspace-client.test.ts +0 -2912
  263. package/packages/daemon-client/src/workspace-event-supervisor.test.ts +0 -1086
  264. package/packages/daemon-client/src/workspace-pane-client.test.ts +0 -80
  265. package/packages/presentation/src/presentation-contract.test.ts +0 -63
  266. package/packages/tmux-bridge/src/runner.test.ts +0 -614
  267. package/packages/tmux-bridge/src/targeting.test.ts +0 -107
@@ -1,3 +1,5 @@
1
+ import { SharedWindowIndex } from "./shared-window-index.js";
2
+ import { RegisteredWindowGuard } from "./registered-window-guard.js";
1
3
  import { accessSync, constants, realpathSync, statSync } from "node:fs";
2
4
  import { isAbsolute } from "node:path";
3
5
  import { resolveTmuxExecutable, tmuxClientEnvironment } from "../../lib/tmux-client-execution.js";
@@ -21,6 +23,45 @@ import { controlModeAuthorityKey, processControlModeOwnershipRegistry, } from ".
21
23
  export class MirrorService {
22
24
  opts;
23
25
  channels = new Map();
26
+ sharedWindows = new SharedWindowIndex();
27
+ channelSessions = new Map();
28
+ registeredWindows = null;
29
+ registeredConflicts = new Set();
30
+ setRegisteredWindowReader(reader) {
31
+ this.registeredWindows?.dispose();
32
+ const guard = new RegisteredWindowGuard(reader);
33
+ this.registeredWindows = guard;
34
+ this.invalidateRegisteredWindows();
35
+ return {
36
+ invalidate: () => {
37
+ if (this.registeredWindows === guard)
38
+ this.invalidateRegisteredWindows();
39
+ },
40
+ dispose: () => guard.dispose(),
41
+ };
42
+ }
43
+ invalidateRegisteredWindows() {
44
+ this.registeredWindows?.invalidate();
45
+ }
46
+ async verifyRegisteredWindows() {
47
+ const guard = this.registeredWindows;
48
+ if (!guard)
49
+ return;
50
+ await guard.verify();
51
+ if (guard !== this.registeredWindows || this.disposed)
52
+ throw new Error("Window ownership changed during discovery");
53
+ for (const [channel, session] of this.channelSessions) {
54
+ if (guard.blocked(session)) {
55
+ channel.setGeometryParticipation(false);
56
+ if (!this.registeredConflicts.has(session)) {
57
+ this.registeredConflicts.add(session);
58
+ this.opts.onSharedWindowConflict?.(session, true);
59
+ }
60
+ }
61
+ else
62
+ this.registeredConflicts.delete(session);
63
+ }
64
+ }
24
65
  pendingDisposals = new Set();
25
66
  drainingChannels = new Map();
26
67
  sessionExitListeners = new Set();
@@ -101,8 +142,14 @@ export class MirrorService {
101
142
  reseed: () => handle.reseed(),
102
143
  readHistorySize: () => handle.readHistorySize(),
103
144
  captureNativeBacking: () => handle.captureNativeBacking(),
104
- sendText: (text) => handle.sendText(text),
105
- sendKey: (key) => handle.sendKey(key),
145
+ sendText: (text) => {
146
+ this.assertWindowOwnership(request.session);
147
+ handle.sendText(text);
148
+ },
149
+ sendKey: (key) => {
150
+ this.assertWindowOwnership(request.session);
151
+ handle.sendKey(key);
152
+ },
106
153
  close: async () => {
107
154
  if (closed)
108
155
  return;
@@ -157,6 +204,7 @@ export class MirrorService {
157
204
  return (args) => {
158
205
  if (entry.retired || this.channels.get(session) !== entry)
159
206
  return Promise.reject(new Error("Resize mirror session retired"));
207
+ this.assertWindowOwnership(session);
160
208
  return run(args);
161
209
  };
162
210
  }
@@ -164,6 +212,7 @@ export class MirrorService {
164
212
  const entry = this.channels.get(session);
165
213
  if (!entry || entry.retired)
166
214
  throw new Error(`Mirror session ${session} is unavailable`);
215
+ this.assertWindowOwnership(session);
167
216
  return entry.channel.executeWindowLinkAction(request);
168
217
  }
169
218
  /** Synchronous hot input on an already-retained SessionRuntime channel. */
@@ -171,30 +220,66 @@ export class MirrorService {
171
220
  const entry = this.channels.get(session);
172
221
  if (!entry || entry.retired)
173
222
  throw new Error(`Mirror session ${session} is unavailable`);
223
+ this.assertWindowOwnership(session);
174
224
  entry.channel.sendText(semanticPaneId, text, performanceTraceId, isolated);
175
225
  }
176
226
  sendBytes(session, semanticPaneId, data, performanceTraceId) {
177
227
  const entry = this.channels.get(session);
178
228
  if (!entry || entry.retired)
179
229
  throw new Error(`Mirror session ${session} is unavailable`);
230
+ this.assertWindowOwnership(session);
180
231
  entry.channel.sendBytes(semanticPaneId, data, performanceTraceId);
181
232
  }
182
233
  sendKey(session, semanticPaneId, key, performanceTraceId) {
183
234
  const entry = this.channels.get(session);
184
235
  if (!entry || entry.retired)
185
236
  throw new Error(`Mirror session ${session} is unavailable`);
237
+ this.assertWindowOwnership(session);
186
238
  entry.channel.sendKey(semanticPaneId, key, performanceTraceId);
187
239
  }
240
+ assertWindowOwnership(session) {
241
+ const message = this.windowOwnershipMessage(session);
242
+ if (message)
243
+ throw new Error(message);
244
+ }
245
+ windowOwnershipMessage(session) {
246
+ if (this.registeredWindows?.pending())
247
+ return "Window ownership is being verified. Retry after session discovery completes.";
248
+ if (this.hasSharedWindowConflict(session))
249
+ return "Linked windows across registered sessions are unsupported. Unlink the shared window before controlling this session.";
250
+ return null;
251
+ }
252
+ hasSharedWindowConflict(session) {
253
+ const entry = this.channels.get(session);
254
+ return ((this.registeredWindows?.blocked(session) ?? false) ||
255
+ (!!entry && this.sharedWindows.conflicted(entry.channel)));
256
+ }
257
+ updateWindowMembership(channel, windows) {
258
+ // A topology read may finish after retirement. It must not resurrect the
259
+ // old channel's membership or conflict with its replacement.
260
+ if (!this.channelSessions.has(channel))
261
+ return;
262
+ for (const [peer, conflict] of this.sharedWindows.update(channel, windows)) {
263
+ const session = this.channelSessions.get(peer);
264
+ if (session === undefined)
265
+ continue;
266
+ if (conflict)
267
+ peer.setGeometryParticipation(false);
268
+ this.opts.onSharedWindowConflict?.(session, conflict);
269
+ }
270
+ }
188
271
  fitViewport(session, cols, rows) {
189
272
  const entry = this.channels.get(session);
190
273
  if (!entry || entry.retired)
191
274
  throw new Error(`Mirror session ${session} is unavailable`);
275
+ this.assertWindowOwnership(session);
192
276
  entry.channel.fitViewport(cols, rows);
193
277
  }
194
278
  fitWindowViewport(session, semanticWindowId, cols, rows) {
195
279
  const entry = this.channels.get(session);
196
280
  if (!entry || entry.retired)
197
281
  throw new Error(`Mirror session ${session} is unavailable`);
282
+ this.assertWindowOwnership(session);
198
283
  entry.channel.fitWindowViewport(semanticWindowId, cols, rows);
199
284
  }
200
285
  clearWindowViewports(session) {
@@ -207,7 +292,7 @@ export class MirrorService {
207
292
  const entry = this.channels.get(session);
208
293
  if (!entry || entry.retired)
209
294
  return;
210
- entry.channel.setGeometryParticipation(active);
295
+ entry.channel.setGeometryParticipation(active && !this.hasSharedWindowConflict(session));
211
296
  }
212
297
  /**
213
298
  * Keep one session channel alive independently of renderer subscriptions.
@@ -248,11 +333,14 @@ export class MirrorService {
248
333
  }
249
334
  async dispose() {
250
335
  this.disposed = true;
336
+ this.registeredWindows?.dispose();
251
337
  this.sessionExitListeners.clear();
252
338
  const entries = [...this.channels.values()];
253
339
  this.channels.clear();
254
340
  await Promise.allSettled(entries.map(async (entry) => {
255
341
  entry.retired = true;
342
+ this.updateWindowMembership(entry.channel, []);
343
+ this.channelSessions.delete(entry.channel);
256
344
  try {
257
345
  await entry.channel.dispose();
258
346
  }
@@ -297,6 +385,13 @@ export class MirrorService {
297
385
  const environment = Object.freeze(tmuxClientEnvironment(process.env, executable ?? this.opts.executable));
298
386
  const channelOptions = {
299
387
  session,
388
+ onWindowMembership: (windows) => this.updateWindowMembership(channel, windows),
389
+ hasSharedWindowConflict: () => this.hasSharedWindowConflict(session),
390
+ beforeIdentityRepair: () => this.verifyRegisteredWindows(),
391
+ onWindowTopologyChanged: () => {
392
+ if (this.channelSessions.has(channel))
393
+ this.invalidateRegisteredWindows();
394
+ },
300
395
  ownedViewer,
301
396
  createIo: (handlers) => this.opts.createIo?.(session, handlers) ??
302
397
  new MirrorControlChannel({
@@ -363,6 +458,7 @@ export class MirrorService {
363
458
  releaseAuthority();
364
459
  throw cause;
365
460
  }
461
+ this.channelSessions.set(channel, session);
366
462
  entry = {
367
463
  channel,
368
464
  started: channel.start(),
@@ -400,6 +496,8 @@ export class MirrorService {
400
496
  if (entry.retired)
401
497
  return;
402
498
  entry.retired = true;
499
+ this.updateWindowMembership(entry.channel, []);
500
+ this.channelSessions.delete(entry.channel);
403
501
  if (this.channels.get(session) === entry)
404
502
  this.channels.delete(session);
405
503
  const disposal = entry.channel
@@ -0,0 +1,62 @@
1
+ /** A bounded registration/topology proof, shared by all retained channels. */
2
+ export class RegisteredWindowGuard {
3
+ reader;
4
+ #epoch = 0;
5
+ #current = false;
6
+ #disposed = false;
7
+ #conflicts = new Set();
8
+ #read = null;
9
+ constructor(reader) {
10
+ this.reader = reader;
11
+ }
12
+ blocked(session) {
13
+ return !this.#current || this.#conflicts.has(session);
14
+ }
15
+ pending() {
16
+ return !this.#current;
17
+ }
18
+ invalidate() {
19
+ this.#epoch++;
20
+ this.#current = false;
21
+ this.#read?.abort.abort();
22
+ this.#read = null;
23
+ }
24
+ dispose() {
25
+ this.#disposed = true;
26
+ this.invalidate();
27
+ }
28
+ async verify() {
29
+ if (this.#disposed)
30
+ throw new Error("Registered window inventory is disposed");
31
+ if (this.#current)
32
+ return;
33
+ if (this.#read)
34
+ return this.#read.promise;
35
+ const epoch = this.#epoch;
36
+ const abort = new AbortController();
37
+ const promise = Promise.resolve()
38
+ .then(async () => {
39
+ abort.signal.throwIfAborted();
40
+ const rows = await this.reader(abort.signal);
41
+ abort.signal.throwIfAborted();
42
+ if (epoch !== this.#epoch)
43
+ throw new Error("Window ownership changed during discovery");
44
+ const owners = new Map();
45
+ for (const row of rows) {
46
+ const sessions = owners.get(row.window) ?? new Set();
47
+ sessions.add(row.session);
48
+ owners.set(row.window, sessions);
49
+ }
50
+ this.#conflicts = new Set([...owners.values()]
51
+ .filter((sessions) => sessions.size > 1)
52
+ .flatMap((sessions) => [...sessions]));
53
+ this.#current = true;
54
+ })
55
+ .finally(() => {
56
+ if (this.#read?.abort === abort)
57
+ this.#read = null;
58
+ });
59
+ this.#read = { abort, promise };
60
+ return promise;
61
+ }
62
+ }
@@ -53,7 +53,10 @@ const STRUCTURAL_NOTIFICATIONS = new Set([
53
53
  "window-add",
54
54
  "window-close",
55
55
  "window-renamed",
56
+ "unlinked-window-add",
56
57
  "unlinked-window-close",
58
+ "session-renamed",
59
+ "sessions-changed",
57
60
  ]);
58
61
  const NATIVE_CLIENT_NOTIFICATIONS = new Set([
59
62
  "client-attached",
@@ -193,6 +196,7 @@ export class SessionChannel {
193
196
  nativeClientProbePending = false;
194
197
  // Native captures fence every geometry event; inventory fences identity only.
195
198
  windowAuthorityOrdinal = 0;
199
+ layoutNotificationOrdinals = new Map();
196
200
  windowIdentityOrdinal = 0;
197
201
  paneIncarnation = 0;
198
202
  recoveryOrdinal = 0;
@@ -764,8 +768,25 @@ export class SessionChannel {
764
768
  }
765
769
  this.input.flush();
766
770
  this.clearWindowViewports();
771
+ this.restoreWindowSizing(this.attachedIdentity?.runtimeSessionId ?? `=${this.opts.session}`);
767
772
  this.io.send(`refresh-client -C ${cols}x${rows}`);
768
773
  }
774
+ restoreWindowSizing(target) {
775
+ if (!this.geometryParticipating)
776
+ return;
777
+ // resize-window pins a window (including through inherited session options).
778
+ // Evaluate inside the existing control connection: no shell, polling, or extra
779
+ // request/response round trip. The shell also pre-fits hidden windows;
780
+ // only the active window may have its manual policy repaired.
781
+ const quoted = tmuxSingleQuote(target);
782
+ // if-shell emits its own reply and the selected branch emits another.
783
+ // Keep both branches at one command so either policy consumes exactly two
784
+ // FIFO slots; a discard-only send would misroute the next capture replies.
785
+ this.io.commandListInline(`if-shell -F -t ${quoted} '#{&&:#{window_active},#{==:#{window-size},manual}}' ` +
786
+ tmuxSingleQuote(`set-option -w -t ${quoted} window-size latest`) +
787
+ " " +
788
+ tmuxSingleQuote(`display-message -p -t ${quoted} ''`), 2, 1, () => { });
789
+ }
769
790
  /**
770
791
  * Experimental: window-specific overrides stay private to this control client.
771
792
  * Native tmux still falls back to the client size for unscoped neighbours;
@@ -788,6 +809,7 @@ export class SessionChannel {
788
809
  if (previous?.cols === cols && previous.rows === rows)
789
810
  return;
790
811
  this.input.flush();
812
+ this.restoreWindowSizing(window.runtimeId);
791
813
  this.io.send(`refresh-client -C ${window.runtimeId}:${cols}x${rows}`);
792
814
  this.fittedWindows.set(window.runtimeId, { cols, rows });
793
815
  }
@@ -863,6 +885,7 @@ export class SessionChannel {
863
885
  pane.subs.clear();
864
886
  }
865
887
  this.pendingLayoutOutput.clear();
888
+ this.layoutNotificationOrdinals.clear();
866
889
  this.layoutSubscribers.clear();
867
890
  this.layoutAuthoritySubscribers.clear();
868
891
  await this.io.dispose();
@@ -2303,6 +2326,13 @@ export class SessionChannel {
2303
2326
  pane.scrollOnClear = value === "0" || value === "1" ? value === "1" : undefined;
2304
2327
  }
2305
2328
  onNotify(name, rest) {
2329
+ if (name === "window-add" ||
2330
+ name === "window-close" ||
2331
+ name === "unlinked-window-add" ||
2332
+ name === "unlinked-window-close" ||
2333
+ name === "session-renamed" ||
2334
+ name === "sessions-changed")
2335
+ this.opts.onWindowTopologyChanged?.();
2306
2336
  if (name === "subscription-changed") {
2307
2337
  const policy = /^tmux-ide-scroll-on-clear\s+\$[0-9]+\s+@[0-9]+\s+[0-9]+\s+(%[0-9]+)\s+:\s+([01])\s*$/u.exec(rest);
2308
2338
  if (policy) {
@@ -2420,6 +2450,7 @@ export class SessionChannel {
2420
2450
  this.scheduleSync(); // never guess from a failed parse
2421
2451
  return;
2422
2452
  }
2453
+ this.layoutNotificationOrdinals.set(change.windowId, this.windowAuthorityOrdinal);
2423
2454
  const pendingLayout = {
2424
2455
  ...parsed,
2425
2456
  zoomed: change.zoomed,
@@ -2715,9 +2746,12 @@ export class SessionChannel {
2715
2746
  const previousCurrentWindow = this.currentWindow;
2716
2747
  const { listed, movedWindowRuntimeIds } = this.applyPaneTruth(truth);
2717
2748
  await this.syncWindows(this.opts.session, movedWindowRuntimeIds, previousCurrentWindow, syncOrdinal);
2718
- for (const pane of this.panesByRuntime.values())
2749
+ for (const pane of this.panesByRuntime.values()) {
2750
+ if (pane.windowRuntimeId && this.pendingLayoutOutput.has(pane.windowRuntimeId))
2751
+ continue;
2719
2752
  for (const sub of pane.subs)
2720
2753
  sub.resumeLayoutCapture?.(syncOrdinal);
2754
+ }
2721
2755
  this.discovery.discover(listed);
2722
2756
  }
2723
2757
  applyPaneTruth(truth) {
@@ -2957,6 +2991,27 @@ export class SessionChannel {
2957
2991
  return stage.repairedIdentity;
2958
2992
  }
2959
2993
  commitWindowStage(stage, requiredLayoutEmits = new Set(), previousCurrentWindow = this.currentWindow, syncOrdinal) {
2994
+ // Control notifications in the same read chunk run before a promise-based
2995
+ // inventory read resumes. Preserve newer geometry (and its pending border
2996
+ // query) rather than letting that older inventory erase the notification.
2997
+ const newerLayouts = new Set([...this.layoutNotificationOrdinals]
2998
+ .filter(([, ordinal]) => ordinal > stage.observedAuthorityOrdinal)
2999
+ .map(([runtimeId]) => runtimeId));
3000
+ if (newerLayouts.size > 0) {
3001
+ stage = { ...stage, windows: new Map(stage.windows), layouts: new Map(stage.layouts) };
3002
+ for (const runtimeId of newerLayouts) {
3003
+ const currentLayout = this.layoutByWindow.get(runtimeId);
3004
+ const currentWindow = this.windowsByRuntime.get(runtimeId);
3005
+ const stagedWindow = stage.windows.get(runtimeId);
3006
+ if (currentLayout && currentWindow && stagedWindow) {
3007
+ stage.layouts.set(runtimeId, currentLayout);
3008
+ stage.windows.set(runtimeId, {
3009
+ ...stagedWindow,
3010
+ paneBorderStatus: currentWindow.paneBorderStatus,
3011
+ });
3012
+ }
3013
+ }
3014
+ }
2960
3015
  this.windowLinkAuthority?.reconcile(stage.links);
2961
3016
  this.latestWindowStage = stage;
2962
3017
  const changedWindows = new Set();
@@ -3008,10 +3063,14 @@ export class SessionChannel {
3008
3063
  ? new Set(stage.windows.keys())
3009
3064
  : new Set([...changedWindows, ...requiredLayoutEmits, ...this.pendingLayoutOutput.keys()].filter((runtimeId) => stage.windows.has(runtimeId)));
3010
3065
  for (const runtimeId of this.pendingLayoutOutput.keys())
3011
- if (!stage.windows.has(runtimeId))
3066
+ if (!stage.windows.has(runtimeId) && !newerLayouts.has(runtimeId))
3012
3067
  this.pendingLayoutOutput.delete(runtimeId);
3068
+ for (const runtimeId of this.layoutNotificationOrdinals.keys())
3069
+ if (!stage.windows.has(runtimeId) && !newerLayouts.has(runtimeId))
3070
+ this.layoutNotificationOrdinals.delete(runtimeId);
3013
3071
  for (const runtimeId of layoutEmits)
3014
- this.releasePendingLayout(runtimeId, syncOrdinal);
3072
+ if (!newerLayouts.has(runtimeId) || !this.pendingLayoutOutput.has(runtimeId))
3073
+ this.releasePendingLayout(runtimeId, syncOrdinal);
3015
3074
  this.emitLayoutAuthority();
3016
3075
  }
3017
3076
  // Inventory establishes pane/window identity, not a frozen terminal size.
@@ -3043,6 +3102,7 @@ export class SessionChannel {
3043
3102
  return true;
3044
3103
  }
3045
3104
  async stageWindows(target = this.opts.session) {
3105
+ const observedAuthorityOrdinal = this.windowAuthorityOrdinal;
3046
3106
  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}"`);
3047
3107
  if (lines.length > WINDOW_LINK_MAX_LINKS)
3048
3108
  throw new Error("Window link observation exceeds bound");
@@ -3115,6 +3175,10 @@ export class SessionChannel {
3115
3175
  throw new Error(`full window layout for ${this.opts.session} is malformed`);
3116
3176
  nextLayoutByWindow.set(row.runtimeId, { ...parsed, zoomed: row.zoomed, unzoomed });
3117
3177
  }
3178
+ // Publish validated physical membership before any identity repair writes.
3179
+ // The receiving session may introduce duplicate stamps by linking a window.
3180
+ this.opts.onWindowMembership?.([...backingRows.keys()]);
3181
+ await this.opts.beforeIdentityRepair?.();
3118
3182
  // Valid unique stamps are identity; missing/invalid/duplicated stamps are
3119
3183
  // ALL regenerated and stamped back (the pane policy, applied to windows).
3120
3184
  const stampCounts = new Map();
@@ -3133,7 +3197,7 @@ export class SessionChannel {
3133
3197
  if (row.stamp && stampCounts.get(row.stamp) === 1) {
3134
3198
  semanticId = row.stamp;
3135
3199
  }
3136
- else {
3200
+ else if (!this.opts.hasSharedWindowConflict?.()) {
3137
3201
  repairedIdentity = true;
3138
3202
  let candidate = null;
3139
3203
  for (let attempt = 0; attempt < 32 && !candidate; attempt += 1) {
@@ -3167,6 +3231,7 @@ export class SessionChannel {
3167
3231
  });
3168
3232
  }
3169
3233
  return {
3234
+ observedAuthorityOrdinal,
3170
3235
  windows: next,
3171
3236
  layouts: nextLayoutByWindow,
3172
3237
  currentWindow: nextCurrentWindow,
@@ -3182,6 +3247,10 @@ export class SessionChannel {
3182
3247
  async reconcileIdentity(descriptors, listed) {
3183
3248
  if (this.disposed)
3184
3249
  return false;
3250
+ if (this.opts.hasSharedWindowConflict?.()) {
3251
+ this.settleFirstJoin();
3252
+ return false;
3253
+ }
3185
3254
  const snapshots = descriptors
3186
3255
  .filter((descriptor) => listed.has(descriptor.runtimePaneId))
3187
3256
  .map((descriptor) => ({
@@ -3309,6 +3378,8 @@ export class SessionChannel {
3309
3378
  return plan.stampEffects.length > 0;
3310
3379
  }
3311
3380
  async repairTrustedPaneIdentity(descriptors, layouts) {
3381
+ if (this.opts.hasSharedWindowConflict?.())
3382
+ throw new Error("Linked windows across controlled sessions are unsupported. Unlink the shared window before controlling this session.");
3312
3383
  const rectForRuntime = (runtimePaneId) => {
3313
3384
  for (const layout of layouts.values()) {
3314
3385
  const leaf = layout.leaves.find(({ id }) => id === runtimePaneId);
@@ -0,0 +1,45 @@
1
+ /** Event-fed physical window membership for one pinned tmux server. */
2
+ export class SharedWindowIndex {
3
+ #windows = new Map();
4
+ #owners = new Map();
5
+ #conflicts = new Set();
6
+ conflicted(owner) {
7
+ return this.#conflicts.has(owner);
8
+ }
9
+ update(owner, windows) {
10
+ const previous = this.#owners.get(owner) ?? new Set();
11
+ const next = new Set(windows);
12
+ const affected = new Set([owner]);
13
+ for (const window of new Set([...previous, ...next])) {
14
+ for (const peer of this.#windows.get(window) ?? [])
15
+ affected.add(peer);
16
+ if (previous.has(window) && !next.has(window)) {
17
+ const peers = this.#windows.get(window);
18
+ peers.delete(owner);
19
+ if (!peers.size)
20
+ this.#windows.delete(window);
21
+ }
22
+ if (next.has(window)) {
23
+ const peers = this.#windows.get(window) ?? new Set();
24
+ peers.add(owner);
25
+ this.#windows.set(window, peers);
26
+ }
27
+ }
28
+ if (next.size)
29
+ this.#owners.set(owner, next);
30
+ else
31
+ this.#owners.delete(owner);
32
+ const changed = new Map();
33
+ for (const peer of affected) {
34
+ const conflict = [...(this.#owners.get(peer) ?? [])].some((window) => (this.#windows.get(window)?.size ?? 0) > 1);
35
+ if (conflict === this.#conflicts.has(peer))
36
+ continue;
37
+ if (conflict)
38
+ this.#conflicts.add(peer);
39
+ else
40
+ this.#conflicts.delete(peer);
41
+ changed.set(peer, conflict);
42
+ }
43
+ return changed;
44
+ }
45
+ }
@@ -1691,7 +1691,17 @@ export class PaneStreamLiveConnection {
1691
1691
  if (!this.#sessionRuntimeBinding?.requestAuthority ||
1692
1692
  !this.#sessionRuntimeBinding.authoritySnapshot)
1693
1693
  return this.#failProtocol("protocol-error");
1694
- const lease = this.#sessionRuntimeBinding.requestAuthority(frame.authority);
1694
+ let lease = null;
1695
+ try {
1696
+ lease = this.#sessionRuntimeBinding.requestAuthority(frame.authority);
1697
+ }
1698
+ catch (error) {
1699
+ // Topology changes can temporarily prevent ownership. Reject this
1700
+ // request without letting an expected conflict terminate the daemon.
1701
+ if (!(error instanceof SessionRuntimeControllerLeaseError) ||
1702
+ error.code !== "controller-conflict")
1703
+ return this.#failProtocol("input-rejected");
1704
+ }
1695
1705
  if (lease)
1696
1706
  this.#requestedAuthorities.add(frame.authority);
1697
1707
  this.#sendFrame(null, {
@@ -102,6 +102,7 @@ export function createSessionRuntimeMultiplexerBackend(options) {
102
102
  if (!ownerAuthorized) {
103
103
  throw new Error("Semantic mutation requires a live host, pane, or owner principal");
104
104
  }
105
+ await options.registry.verifyWindowOwnership();
105
106
  const owner = acquireOwner(session);
106
107
  try {
107
108
  let lease;