tmux-ide 2.9.2 → 2.9.3

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 (263) hide show
  1. package/README.md +1 -1
  2. package/bin/cli.js +2071 -625
  3. package/package.json +14 -5
  4. package/packages/contracts/src/desktop-host.ts +3 -0
  5. package/packages/contracts/src/saved-machines.ts +19 -0
  6. package/packages/contracts/src/terminal-replica.ts +1 -1
  7. package/packages/core/package.json +3 -2
  8. package/packages/core/src/terminal-delivery.ts +51 -3
  9. package/packages/core/src/terminal-fnv64-wasm.ts +4 -3
  10. package/packages/core/src/terminal-replica-hash-cache.ts +129 -31
  11. package/packages/core/src/terminal-replica-owned-row.ts +27 -0
  12. package/packages/core/src/terminal-replica.ts +13 -28
  13. package/packages/daemon/dist/command-center/daemon-fleet-facts-observer.js +96 -10
  14. package/packages/daemon/dist/command-center/resources/saved-machine-route.js +40 -1
  15. package/packages/daemon/dist/command-center/ws-events.js +5 -24
  16. package/packages/daemon/dist/lib/canonical-daemon-bootstrap.js +8 -0
  17. package/packages/daemon/dist/lib/canonical-daemon.js +28 -2
  18. package/packages/daemon/dist/lib/daemon-embed.js +1 -1
  19. package/packages/daemon/dist/lib/daemon-service-manager.js +122 -0
  20. package/packages/daemon/dist/lib/daemon-service-plan.js +127 -0
  21. package/packages/daemon/dist/lib/daemon-service.js +259 -0
  22. package/packages/daemon/dist/lib/install-origin.js +2 -0
  23. package/packages/daemon/dist/lib/local-fleet-request.js +9 -3
  24. package/packages/daemon/dist/lib/log.js +6 -10
  25. package/packages/daemon/dist/lib/managed-installer-update.js +67 -0
  26. package/packages/daemon/dist/lib/{tui-download-lock.js → private-operation-lock.js} +11 -11
  27. package/packages/daemon/dist/lib/remote-daemon-info.js +3 -2
  28. package/packages/daemon/dist/lib/remote-daemon-lifecycle.js +4 -4
  29. package/packages/daemon/dist/lib/remote-tmux-command.js +10 -0
  30. package/packages/daemon/dist/lib/runtime-namespace.js +28 -16
  31. package/packages/daemon/dist/lib/saved-machines.js +40 -2
  32. package/packages/daemon/dist/lib/ssh-daemon-transport.js +24 -9
  33. package/packages/daemon/dist/lib/tmux-server-owner.js +1 -1
  34. package/packages/daemon/dist/lib/tmux-server-registration.js +21 -3
  35. package/packages/daemon/dist/lib/tui-binary.js +2 -2
  36. package/packages/daemon/dist/lib/update.js +18 -0
  37. package/packages/daemon/dist/lib/workspace-multiplexer-verbs.js +24 -1
  38. package/packages/daemon/dist/machines.js +40 -4
  39. package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/Info.plist +2 -2
  40. package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/MacOS/tmux-ide-notifier +0 -0
  41. package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/Resources/Assets.car +0 -0
  42. package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/_CodeSignature/CodeResources +2 -2
  43. package/packages/daemon/dist/terminal/attachments/direct-websocket.js +4 -1
  44. package/packages/daemon/dist/terminal/attachments/native-runtime.js +22 -13
  45. package/packages/daemon/dist/terminal/mirror/mirror-service.js +101 -3
  46. package/packages/daemon/dist/terminal/mirror/registered-window-guard.js +62 -0
  47. package/packages/daemon/dist/terminal/mirror/session-channel.js +75 -4
  48. package/packages/daemon/dist/terminal/mirror/shared-window-index.js +45 -0
  49. package/packages/daemon/dist/terminal/pane-stream/pane-stream-websocket.js +11 -1
  50. package/packages/daemon/dist/terminal/session-runtime/multiplexer-backend.js +1 -0
  51. package/packages/daemon/dist/terminal/session-runtime/registry.js +56 -7
  52. package/packages/daemon/dist/terminal/session-runtime/semantic-mutation-executor.js +3 -1
  53. package/packages/daemon/dist/tui/mirror/dialog-model.js +2 -35
  54. package/packages/daemon/dist/tui/mirror/runtime/application-daemon-authority-owner.js +8 -0
  55. package/packages/daemon/dist/tui/mirror/runtime/application-home-fleet.js +20 -2
  56. package/packages/daemon/dist/tui/mirror/runtime/application-machine-agents.js +7 -1
  57. package/packages/daemon/dist/tui/mirror/runtime/application-machine-authority.js +59 -9
  58. package/packages/daemon/dist/tui/mirror/runtime/application-machine-navigation.js +27 -19
  59. package/packages/daemon/dist/tui/mirror/runtime/application-machine-sidebar.jsx +23 -18
  60. package/packages/daemon/dist/tui/mirror/runtime/application-machine-startup.js +31 -2
  61. package/packages/daemon/dist/tui/mirror/runtime/application-palette-search-owner.js +7 -3
  62. package/packages/daemon/dist/tui/mirror/runtime/application-performance-log.js +3 -0
  63. package/packages/daemon/dist/tui/mirror/runtime/application-terminal-input-ingress.js +146 -2
  64. package/packages/daemon/dist/tui/mirror/runtime/application-terminal-interaction-controller.js +201 -50
  65. package/packages/daemon/dist/tui/mirror/runtime/bounded-performance-record-writer.js +50 -10
  66. package/packages/daemon/dist/tui/mirror/runtime/terminal-fast-lane-renderer-adapter.js +6 -5
  67. package/packages/daemon/dist/tui/mirror/runtime/terminal-pane-input-router.js +3 -0
  68. package/packages/daemon/dist/tui/mirror/runtime/workspace-terminal-fast-lane.js +8 -2
  69. package/packages/daemon/dist/tui/mirror/terminal-text.js +35 -0
  70. package/packages/daemon/dist/tui/mirror/workspace/connection-feedback.js +5 -3
  71. package/packages/daemon/src/command-center/daemon-fleet-facts-observer.ts +100 -10
  72. package/packages/daemon/src/command-center/resources/saved-machine-route.ts +48 -1
  73. package/packages/daemon/src/command-center/ws-events.ts +5 -25
  74. package/packages/daemon/src/lib/canonical-daemon-bootstrap.ts +12 -0
  75. package/packages/daemon/src/lib/canonical-daemon.ts +44 -2
  76. package/packages/daemon/src/lib/daemon-embed.ts +1 -0
  77. package/packages/daemon/src/lib/daemon-service-manager.ts +152 -0
  78. package/packages/daemon/src/lib/daemon-service-plan.ts +153 -0
  79. package/packages/daemon/src/lib/daemon-service.ts +301 -0
  80. package/packages/daemon/src/lib/install-origin.ts +5 -0
  81. package/packages/daemon/src/lib/local-fleet-request.ts +19 -3
  82. package/packages/daemon/src/lib/log.ts +13 -9
  83. package/packages/daemon/src/lib/managed-installer-update.ts +79 -0
  84. package/packages/daemon/src/lib/{tui-download-lock.ts → private-operation-lock.ts} +15 -11
  85. package/packages/daemon/src/lib/remote-daemon-info.ts +3 -3
  86. package/packages/daemon/src/lib/remote-daemon-lifecycle.ts +4 -4
  87. package/packages/daemon/src/lib/remote-tmux-command.ts +12 -0
  88. package/packages/daemon/src/lib/runtime-namespace.ts +27 -18
  89. package/packages/daemon/src/lib/saved-machines.ts +40 -1
  90. package/packages/daemon/src/lib/ssh-daemon-transport.ts +25 -7
  91. package/packages/daemon/src/lib/tmux-server-owner.ts +1 -0
  92. package/packages/daemon/src/lib/tmux-server-registration.ts +21 -3
  93. package/packages/daemon/src/lib/tui-binary.ts +2 -2
  94. package/packages/daemon/src/lib/update.ts +24 -0
  95. package/packages/daemon/src/lib/workspace-multiplexer-verbs.ts +22 -0
  96. package/packages/daemon/src/machines.ts +53 -5
  97. package/packages/daemon/src/terminal/attachments/direct-websocket.ts +8 -2
  98. package/packages/daemon/src/terminal/attachments/native-runtime.ts +28 -13
  99. package/packages/daemon/src/terminal/mirror/mirror-service.ts +105 -3
  100. package/packages/daemon/src/terminal/mirror/registered-window-guard.ts +66 -0
  101. package/packages/daemon/src/terminal/mirror/session-channel.ts +92 -4
  102. package/packages/daemon/src/terminal/mirror/shared-window-index.ts +42 -0
  103. package/packages/daemon/src/terminal/pane-stream/pane-stream-websocket.ts +12 -1
  104. package/packages/daemon/src/terminal/session-runtime/multiplexer-backend.ts +2 -0
  105. package/packages/daemon/src/terminal/session-runtime/registry.ts +70 -8
  106. package/packages/daemon/src/terminal/session-runtime/semantic-mutation-executor.ts +3 -1
  107. package/packages/daemon/src/tui/mirror/dialog-model.ts +2 -32
  108. package/packages/daemon/src/tui/mirror/runtime/application-daemon-authority-owner.ts +8 -0
  109. package/packages/daemon/src/tui/mirror/runtime/application-home-fleet.ts +19 -2
  110. package/packages/daemon/src/tui/mirror/runtime/application-machine-agents.ts +6 -1
  111. package/packages/daemon/src/tui/mirror/runtime/application-machine-authority.ts +60 -11
  112. package/packages/daemon/src/tui/mirror/runtime/application-machine-navigation.ts +39 -33
  113. package/packages/daemon/src/tui/mirror/runtime/application-machine-sidebar.tsx +31 -23
  114. package/packages/daemon/src/tui/mirror/runtime/application-machine-startup.ts +38 -2
  115. package/packages/daemon/src/tui/mirror/runtime/application-palette-search-owner.ts +7 -4
  116. package/packages/daemon/src/tui/mirror/runtime/application-performance-log.ts +3 -0
  117. package/packages/daemon/src/tui/mirror/runtime/application-root-v2.tsx +1 -1
  118. package/packages/daemon/src/tui/mirror/runtime/application-shell-view.tsx +47 -46
  119. package/packages/daemon/src/tui/mirror/runtime/application-terminal-input-ingress.ts +165 -2
  120. package/packages/daemon/src/tui/mirror/runtime/application-terminal-interaction-controller.ts +207 -53
  121. package/packages/daemon/src/tui/mirror/runtime/bounded-performance-record-writer.ts +49 -9
  122. package/packages/daemon/src/tui/mirror/runtime/terminal-fast-lane-renderer-adapter.ts +6 -5
  123. package/packages/daemon/src/tui/mirror/runtime/terminal-pane-input-router.ts +4 -0
  124. package/packages/daemon/src/tui/mirror/runtime/workspace-terminal-fast-lane.ts +8 -3
  125. package/packages/daemon/src/tui/mirror/terminal-text.ts +32 -0
  126. package/packages/daemon/src/tui/mirror/workspace/connection-feedback.ts +6 -3
  127. package/packages/daemon-client/src/fleet-connection-status.ts +10 -1
  128. package/packages/daemon-client/src/workspace-client.ts +4 -1
  129. package/packages/contracts/src/__tests__/actions-contract.test.ts +0 -119
  130. package/packages/contracts/src/__tests__/agent-graph-overlay.test.ts +0 -428
  131. package/packages/contracts/src/__tests__/app-window-mutation.test.ts +0 -51
  132. package/packages/contracts/src/__tests__/app-window-state.test.ts +0 -34
  133. package/packages/contracts/src/__tests__/application-shell.test.ts +0 -645
  134. package/packages/contracts/src/__tests__/automation-operations.test.ts +0 -78
  135. package/packages/contracts/src/__tests__/causal-cell.test.ts +0 -47
  136. package/packages/contracts/src/__tests__/client-view-state.test.ts +0 -115
  137. package/packages/contracts/src/__tests__/cohesion-fixture.test.ts +0 -106
  138. package/packages/contracts/src/__tests__/commands.test.ts +0 -86
  139. package/packages/contracts/src/__tests__/control.test.ts +0 -154
  140. package/packages/contracts/src/__tests__/daemon-events.test.ts +0 -405
  141. package/packages/contracts/src/__tests__/daemon-resources.test.ts +0 -143
  142. package/packages/contracts/src/__tests__/daemon-wire.test.ts +0 -149
  143. package/packages/contracts/src/__tests__/desktop-host.test.ts +0 -269
  144. package/packages/contracts/src/__tests__/desktop-missions.test.ts +0 -215
  145. package/packages/contracts/src/__tests__/experience-conformance.test.ts +0 -153
  146. package/packages/contracts/src/__tests__/experience-kernel-boundary.test.ts +0 -113
  147. package/packages/contracts/src/__tests__/experience-shell.test.ts +0 -65
  148. package/packages/contracts/src/__tests__/fixtures/daemon-wire-v2.json +0 -35
  149. package/packages/contracts/src/__tests__/fixtures/daemon-wire-v3.json +0 -35
  150. package/packages/contracts/src/__tests__/fixtures/daemon-wire-v4.json +0 -35
  151. package/packages/contracts/src/__tests__/fixtures/scoped-receipt.ts +0 -62
  152. package/packages/contracts/src/__tests__/fleet-agent-graph.test.ts +0 -231
  153. package/packages/contracts/src/__tests__/fleet-catalog.test.ts +0 -270
  154. package/packages/contracts/src/__tests__/fleet-lifecycle.test.ts +0 -62
  155. package/packages/contracts/src/__tests__/focus-overlay.test.ts +0 -110
  156. package/packages/contracts/src/__tests__/ide-config.test.ts +0 -64
  157. package/packages/contracts/src/__tests__/interaction-evidence.test.ts +0 -288
  158. package/packages/contracts/src/__tests__/interaction-receipt-v2.test.ts +0 -72
  159. package/packages/contracts/src/__tests__/issue-error.test.ts +0 -78
  160. package/packages/contracts/src/__tests__/mission-projections.test.ts +0 -186
  161. package/packages/contracts/src/__tests__/missions.test.ts +0 -272
  162. package/packages/contracts/src/__tests__/multiplexer-verbs.test.ts +0 -242
  163. package/packages/contracts/src/__tests__/pane-appearance.test.ts +0 -146
  164. package/packages/contracts/src/__tests__/pane-read-result.test.ts +0 -55
  165. package/packages/contracts/src/__tests__/pane-stream.test.ts +0 -536
  166. package/packages/contracts/src/__tests__/pane-widget-marker.test.ts +0 -327
  167. package/packages/contracts/src/__tests__/performance-metrics.test.ts +0 -73
  168. package/packages/contracts/src/__tests__/performance-qualification.test.ts +0 -137
  169. package/packages/contracts/src/__tests__/rich-card-widget.test.ts +0 -29
  170. package/packages/contracts/src/__tests__/session-runtime.test.ts +0 -512
  171. package/packages/contracts/src/__tests__/startup-readiness.test.ts +0 -409
  172. package/packages/contracts/src/__tests__/terminal-attachment-stream.test.ts +0 -72
  173. package/packages/contracts/src/__tests__/terminal-attachments.test.ts +0 -329
  174. package/packages/contracts/src/__tests__/terminal-delivery.test.ts +0 -88
  175. package/packages/contracts/src/__tests__/terminal-runtime-inventory.test.ts +0 -68
  176. package/packages/contracts/src/__tests__/terminals.test.ts +0 -87
  177. package/packages/contracts/src/__tests__/tmux-server-scope.test.ts +0 -166
  178. package/packages/contracts/src/__tests__/transport-contract-loading.test.ts +0 -91
  179. package/packages/contracts/src/__tests__/visual-tokens.test.ts +0 -224
  180. package/packages/contracts/src/__tests__/window-links.test.ts +0 -243
  181. package/packages/contracts/src/__tests__/workspace-changes-resource.test.ts +0 -317
  182. package/packages/contracts/src/__tests__/workspace-changes-view.test.ts +0 -122
  183. package/packages/contracts/src/__tests__/workspace-config.test.ts +0 -294
  184. package/packages/contracts/src/__tests__/workspace-files-resource.test.ts +0 -261
  185. package/packages/contracts/src/__tests__/workspace-files-tree.test.ts +0 -147
  186. package/packages/contracts/src/__tests__/workspace-pane-creation.test.ts +0 -249
  187. package/packages/contracts/src/__tests__/workspace-promotion.test.ts +0 -94
  188. package/packages/contracts/src/__tests__/workspace-resource-identity.test.ts +0 -90
  189. package/packages/contracts/src/daemon-resource-request.test.ts +0 -192
  190. package/packages/contracts/src/native-operation-identity.test.ts +0 -23
  191. package/packages/contracts/src/workspace-admission.test.ts +0 -21
  192. package/packages/contracts/src/workspace-catalog-resource.test.ts +0 -146
  193. package/packages/core/src/agent-provisioning.test.ts +0 -42
  194. package/packages/core/src/application-shell-session.test.ts +0 -105
  195. package/packages/core/src/interaction-evidence.test.ts +0 -134
  196. package/packages/core/src/interaction-flow.test.ts +0 -66
  197. package/packages/core/src/interaction-receipts.test.ts +0 -671
  198. package/packages/core/src/navigator.test.ts +0 -34
  199. package/packages/core/src/optimistic-projection-conformance.test.ts +0 -75
  200. package/packages/core/src/optimistic-projection.test.ts +0 -118
  201. package/packages/core/src/performance-metrics.test.ts +0 -121
  202. package/packages/core/src/performance-qualification.test.ts +0 -283
  203. package/packages/core/src/saved-machines.test.ts +0 -80
  204. package/packages/core/src/terminal-compact-cold.test.ts +0 -87
  205. package/packages/core/src/terminal-conformance.test.ts +0 -48
  206. package/packages/core/src/terminal-delivery-cache-lifetime.test.ts +0 -167
  207. package/packages/core/src/terminal-delivery-encode-cooperative.test.ts +0 -140
  208. package/packages/core/src/terminal-delivery-representation-hash.test.ts +0 -77
  209. package/packages/core/src/terminal-delivery.test.ts +0 -1471
  210. package/packages/core/src/terminal-fnv64-wasm.test.ts +0 -61
  211. package/packages/core/src/terminal-replica-cold.test.ts +0 -31
  212. package/packages/core/src/terminal-replica-cooperative.test.ts +0 -170
  213. package/packages/core/src/terminal-replica-hash-cache.test.ts +0 -283
  214. package/packages/core/src/terminal-replica.test.ts +0 -347
  215. package/packages/core/src/workspace-selection.test.ts +0 -63
  216. package/packages/daemon/dist/__tests__/support.js +0 -33
  217. package/packages/daemon/dist/lib/__tests__/fixtures/workspace-state-writer.js +0 -58
  218. package/packages/daemon/dist/lib/__tests__/installed-recovery-fixture.js +0 -400
  219. package/packages/daemon/dist/terminal/__tests__/MockPtyAdapter.js +0 -182
  220. package/packages/daemon/dist/terminal/mirror/__tests__/scripted-channel.js +0 -98
  221. package/packages/daemon/dist/terminal/mirror/__tests__/simulated-channel.js +0 -156
  222. package/packages/daemon/src/tui/mirror/__snapshots__/activity-surface-renderer.test.tsx.snap +0 -42
  223. package/packages/daemon/src/tui/mirror/__snapshots__/changes-terminal-surface-renderer.test.tsx.snap +0 -267
  224. package/packages/daemon/src/tui/mirror/__snapshots__/home-files-surface-renderer.test.tsx.snap +0 -267
  225. package/packages/daemon/src/tui/mirror/__snapshots__/missions-surface-renderer.test.tsx.snap +0 -134
  226. package/packages/daemon/src/tui/mirror/__snapshots__/recipes-gallery-renderer.test.tsx.snap +0 -141
  227. package/packages/daemon/src/tui/mirror/__snapshots__/shell-chrome-renderer.test.tsx.snap +0 -134
  228. package/packages/daemon/src/tui/mirror/runtime/__snapshots__/application-shell-view-renderer.test.tsx.snap +0 -87
  229. package/packages/daemon/src/tui/mirror/workspace/__snapshots__/agent-terminal-canvas-renderer.test.tsx.snap +0 -348
  230. package/packages/daemon/src/tui/mirror/workspace/__snapshots__/application-shell-renderer.test.tsx.snap +0 -134
  231. package/packages/daemon/src/tui/mirror/workspace/__snapshots__/command-palette-surface-renderer.test.tsx.snap +0 -255
  232. package/packages/daemon/src/tui/mirror/workspace/__snapshots__/pane-frame-renderer.test.tsx.snap +0 -525
  233. package/packages/daemon/src/tui/mirror/workspace/__snapshots__/terminal-pane-header-polish-renderer.test.tsx.snap +0 -31
  234. package/packages/daemon/src/tui/mirror/workspace/__snapshots__/workbench-shell-renderer.test.tsx.snap +0 -134
  235. package/packages/daemon-client/src/application-shell-session.test.ts +0 -196
  236. package/packages/daemon-client/src/automation-client.test.ts +0 -214
  237. package/packages/daemon-client/src/bootstrap-coordinator.test.ts +0 -145
  238. package/packages/daemon-client/src/connection-supervisor.test.ts +0 -109
  239. package/packages/daemon-client/src/direct-application-shell-transport.test.ts +0 -941
  240. package/packages/daemon-client/src/first-latest-coordinator.test.ts +0 -175
  241. package/packages/daemon-client/src/fleet-dial-scheduler.test.ts +0 -114
  242. package/packages/daemon-client/src/fleet-environments.test.ts +0 -40
  243. package/packages/daemon-client/src/generation-bound-store-timers.test.ts +0 -74
  244. package/packages/daemon-client/src/owner-action-client.test.ts +0 -245
  245. package/packages/daemon-client/src/pane-stream-client.test.ts +0 -2841
  246. package/packages/daemon-client/src/pane-stream-clock-calibration.test.ts +0 -207
  247. package/packages/daemon-client/src/pane-stream-loopback-benchmark.test.ts +0 -51
  248. package/packages/daemon-client/src/push-resource-session.test.ts +0 -509
  249. package/packages/daemon-client/src/resource-replica.test.ts +0 -186
  250. package/packages/daemon-client/src/runtime-resource-ledger.test.ts +0 -18
  251. package/packages/daemon-client/src/scoped-tmux-server-transport.test.ts +0 -114
  252. package/packages/daemon-client/src/terminal-fast-lane.test.ts +0 -835
  253. package/packages/daemon-client/src/tmux-server-client.test.ts +0 -279
  254. package/packages/daemon-client/src/tmux-server-interaction-events.test.ts +0 -230
  255. package/packages/daemon-client/src/workspace-catalog-v2.test.ts +0 -109
  256. package/packages/daemon-client/src/workspace-client-boundaries.test.ts +0 -59
  257. package/packages/daemon-client/src/workspace-client-operations.test.ts +0 -96
  258. package/packages/daemon-client/src/workspace-client.test.ts +0 -2912
  259. package/packages/daemon-client/src/workspace-event-supervisor.test.ts +0 -1086
  260. package/packages/daemon-client/src/workspace-pane-client.test.ts +0 -80
  261. package/packages/presentation/src/presentation-contract.test.ts +0 -63
  262. package/packages/tmux-bridge/src/runner.test.ts +0 -614
  263. package/packages/tmux-bridge/src/targeting.test.ts +0 -107
@@ -468,6 +468,7 @@ export class WorkspaceMultiplexerAuthority {
468
468
  session: string,
469
469
  action: AuthoritativeWindowLinkAction,
470
470
  ) => Promise<AuthoritativeWindowLinkActionResult>,
471
+ timing?: WorkspaceMultiplexerOperationTiming,
471
472
  ): Promise<WorkspaceMultiplexerMutationResult> {
472
473
  if (this.#disposed) throw new WorkspaceMultiplexerError("workspace_unavailable");
473
474
  const request = WorkspaceMultiplexerMutationRequestSchemaZ.parse(raw);
@@ -480,7 +481,26 @@ export class WorkspaceMultiplexerAuthority {
480
481
  intent.verb !== "workspace.pane.select"
481
482
  )
482
483
  throw new TypeError("Expected a window-link selection or unlink intent");
484
+ const now = () => {
485
+ try {
486
+ return timing?.nowMicros() ?? null;
487
+ } catch {
488
+ return null;
489
+ }
490
+ };
491
+ const record = (operation: string, start: number | null) => {
492
+ const end = now();
493
+ if (start !== null && end !== null) {
494
+ try {
495
+ timing?.record(operation, start, end);
496
+ } catch {
497
+ /* Diagnostics cannot own selection. */
498
+ }
499
+ }
500
+ };
501
+ const lookupStarted = now();
483
502
  const workspace = this.#registry.get(intent.workspaceName);
503
+ record("semantic-workspace-lookup", lookupStarted);
484
504
  if (!workspace) throw new WorkspaceMultiplexerError("workspace_not_found");
485
505
  const envelope = {
486
506
  operationId: request.operationId,
@@ -488,6 +508,7 @@ export class WorkspaceMultiplexerAuthority {
488
508
  workspaceName: intent.workspaceName,
489
509
  };
490
510
  try {
511
+ const effectStarted = now();
491
512
  const result = await execute(
492
513
  workspace.sessionName,
493
514
  intent.verb === "workspace.pane.select"
@@ -501,6 +522,7 @@ export class WorkspaceMultiplexerAuthority {
501
522
  target: intent.target,
502
523
  },
503
524
  );
525
+ record("window-link-effect-proof", effectStarted);
504
526
  if (result.outcome !== "applied") {
505
527
  throw new WorkspaceMultiplexerError(
506
528
  result.outcome === "stale"
@@ -1,22 +1,70 @@
1
1
  import { readFileSync, statSync } from "node:fs";
2
2
  import { randomUUID } from "node:crypto";
3
- import { SavedMachineRegistrySchema, SavedMachineSchema } from "@tmux-ide/contracts/saved-machines";
3
+ import {
4
+ SavedMachineRegistrySchema,
5
+ SavedMachineSchema,
6
+ SavedMachineMutationSchema,
7
+ } from "@tmux-ide/contracts/saved-machines";
4
8
  import { loadSavedMachines, planSavedMachineMerge } from "./lib/saved-machines.ts";
5
9
  import { loadFleetClientState } from "./lib/fleet-client-state.ts";
6
- import { saveMachineProfiles } from "./lib/local-fleet-request.ts";
10
+ import { changeSavedMachines } from "@tmux-ide/core";
11
+ import { saveMachineProfiles, mutateMachineProfile } from "./lib/local-fleet-request.ts";
12
+ import { remoteTmuxIdeCommand } from "./lib/remote-tmux-command.ts";
7
13
 
8
14
  export async function machines(
9
15
  command: string | undefined,
10
16
  argument: string | undefined,
11
- options: { write?: boolean; label?: string },
17
+ options: { write?: boolean; label?: string; sshTarget?: string },
12
18
  ) {
13
19
  const current = loadSavedMachines();
20
+ if (
21
+ (command === "enable" || command === "disable" || command === "remove" || command === "edit") &&
22
+ argument
23
+ ) {
24
+ const key = argument.normalize("NFKC").toLowerCase();
25
+ const match =
26
+ current.machines.find((machine) => machine.id === key) ??
27
+ current.machines.find((machine) => machine.label.normalize("NFKC").toLowerCase() === key);
28
+ if (!match)
29
+ throw new Error("Saved machine not found; use machines ls to select its ID or label.");
30
+ const change = SavedMachineMutationSchema.parse(
31
+ command === "edit"
32
+ ? {
33
+ id: match.id,
34
+ operation: command,
35
+ patch: {
36
+ ...(options.label !== undefined ? { label: options.label } : {}),
37
+ ...(options.sshTarget !== undefined ? { sshTarget: options.sshTarget } : {}),
38
+ },
39
+ }
40
+ : { id: match.id, operation: command },
41
+ );
42
+ const preview = changeSavedMachines(
43
+ current,
44
+ command === "remove"
45
+ ? { type: "remove", id: match.id }
46
+ : {
47
+ type: "update",
48
+ id: match.id,
49
+ patch: change.operation === "edit" ? change.patch : { enabled: command === "enable" },
50
+ },
51
+ );
52
+ return {
53
+ written: options.write === true,
54
+ registry: options.write ? await mutateMachineProfile(change) : preview,
55
+ ...(!options.write
56
+ ? {
57
+ note: "Repeat with --write to change this saved profile. Remote sessions and daemons are preserved.",
58
+ }
59
+ : {}),
60
+ };
61
+ }
14
62
  if (command === "start" && argument) {
15
63
  SavedMachineSchema.shape.sshTarget.parse(argument);
16
64
  if (!options.write)
17
65
  return {
18
66
  written: false,
19
- command: ["ssh", "--", argument, "tmux-ide", "update", "--daemon", "--json"],
67
+ command: ["ssh", "--", argument, remoteTmuxIdeCommand("start")],
20
68
  note: "Repeat with --write to start the installed daemon. No package installation occurs.",
21
69
  };
22
70
  await (await import("./lib/remote-daemon-lifecycle.ts")).startInstalledRemoteDaemon(argument);
@@ -74,7 +122,7 @@ export async function machines(
74
122
  };
75
123
  } else
76
124
  throw new Error(
77
- "Usage: tmux-ide machines ls|export|import <file>|add <alias> [--write] [--json]",
125
+ "Usage: tmux-ide machines ls|export|import <file>|add <alias>|edit|enable|disable|remove <id-or-label> [--write] [--json]",
78
126
  );
79
127
  const preview = planSavedMachineMerge(current, incoming);
80
128
  if (!options.write)
@@ -210,7 +210,7 @@ export interface TerminalAttachmentAdmissionCoordinatorOptions {
210
210
  /** Trusted redemption boundary into the generation's SessionRuntime owner. */
211
211
  readonly bindSessionRuntime?: (
212
212
  descriptor: AttachmentLeaseDescriptor,
213
- ) => SessionRuntimeRedeemedTransportBinding;
213
+ ) => SessionRuntimeRedeemedTransportBinding | Promise<SessionRuntimeRedeemedTransportBinding>;
214
214
  readonly maxPendingTickets?: number;
215
215
  readonly maxPreAuthSockets?: number;
216
216
  readonly maxLiveConnections?: number;
@@ -765,7 +765,13 @@ export class TerminalAttachmentAdmissionCoordinator {
765
765
  let sessionRuntimeBinding: SessionRuntimeRedeemedTransportBinding | null = null;
766
766
  let live: TerminalAttachmentLiveConnection;
767
767
  try {
768
- sessionRuntimeBinding = this.#bindSessionRuntime?.(activeDescriptor) ?? null;
768
+ sessionRuntimeBinding = (await this.#bindSessionRuntime?.(activeDescriptor)) ?? null;
769
+ if (!admission.isOpen()) {
770
+ throw new TerminalAttachmentAdmissionError(
771
+ "redemption-rejected",
772
+ "Terminal attachment redemption was rejected.",
773
+ );
774
+ }
769
775
  live = new TerminalAttachmentLiveConnection({
770
776
  onRetire: (connection) => this.#trackRetiringRelease(connection),
771
777
  socket,
@@ -1307,6 +1307,7 @@ export class WorkspaceTerminalInventoryRuntime {
1307
1307
  }
1308
1308
  >();
1309
1309
  #lifecycle: "initializing" | "ready" | "failed" | "disposed" = "initializing";
1310
+ #registeredWindows: { invalidate(): void; dispose(): void } | undefined;
1310
1311
  #disposed = false;
1311
1312
 
1312
1313
  constructor(options: WorkspaceTerminalInventoryRuntimeOptions) {
@@ -1318,6 +1319,16 @@ export class WorkspaceTerminalInventoryRuntime {
1318
1319
  });
1319
1320
  this.readRunner = pinnedReadRunner(authority, executeRead);
1320
1321
  this.#registry = options.registry;
1322
+ this.#registeredWindows = options.sessionRuntimeRegistry?.setRegisteredWindowReader?.(
1323
+ async (signal) => {
1324
+ const snapshot = await discoverWorkspaceRegistryTerminalInventory(
1325
+ this.#registry,
1326
+ this.readRunner,
1327
+ signal,
1328
+ );
1329
+ return snapshot.panes.map((pane) => ({ session: pane.sessionName, window: pane.windowId }));
1330
+ },
1331
+ );
1321
1332
  this.#observability = options.observability ?? DISABLED_SESSION_RUNTIME_OBSERVABILITY;
1322
1333
  this.#resolveInteractionEndpoint = options.resolveInteractionEndpoint;
1323
1334
  this.#nativeServerEpoch = options.nativeServerEpoch;
@@ -1441,10 +1452,12 @@ export class WorkspaceTerminalInventoryRuntime {
1441
1452
  this.#observeWorkspaceSession = null;
1442
1453
  }
1443
1454
  this.#stopWorkspaceAddedObserver = options.registry.on("workspace.added", (workspace) => {
1455
+ this.#registeredWindows?.invalidate();
1444
1456
  this.invalidate();
1445
1457
  this.#observeWorkspaceSession?.(workspace.name, workspace.sessionName);
1446
1458
  });
1447
1459
  this.#stopWorkspaceRemovedObserver = options.registry.on("workspace.removed", (name) => {
1460
+ this.#registeredWindows?.invalidate();
1448
1461
  this.invalidate();
1449
1462
  if (!options.sessionRuntimeRegistry) return;
1450
1463
  const sessionName = sessionsByWorkspace.get(name);
@@ -1749,12 +1762,12 @@ export class WorkspaceTerminalInventoryRuntime {
1749
1762
  let catalogIssue: NativeTerminalInventoryCatalogIssue | null = inventory.catalog
1750
1763
  .invalidRuntimeProof
1751
1764
  ? "invalid-runtime-proof"
1752
- : inventory.catalog.missingSemanticStamp
1753
- ? "missing-semantic-stamp"
1754
- : inventory.catalog.duplicateSemanticStamp
1755
- ? "duplicate-semantic-stamp"
1756
- : inventory.catalog.duplicateRuntimePaneBinding
1757
- ? "duplicate-runtime-pane-binding"
1765
+ : inventory.catalog.duplicateRuntimePaneBinding
1766
+ ? "duplicate-runtime-pane-binding"
1767
+ : inventory.catalog.missingSemanticStamp
1768
+ ? "missing-semantic-stamp"
1769
+ : inventory.catalog.duplicateSemanticStamp
1770
+ ? "duplicate-semantic-stamp"
1758
1771
  : null;
1759
1772
  if (
1760
1773
  shouldPrewarm &&
@@ -1949,6 +1962,7 @@ export class WorkspaceTerminalInventoryRuntime {
1949
1962
  if (this.#disposed) return;
1950
1963
  this.#disposed = true;
1951
1964
  this.#lifecycle = "disposed";
1965
+ this.#registeredWindows?.dispose();
1952
1966
  this.invalidate();
1953
1967
  this.#stopWorkspaceAddedObserver?.();
1954
1968
  this.#stopWorkspaceRemovedObserver?.();
@@ -2128,12 +2142,13 @@ export class NativeTerminalAttachmentRuntime {
2128
2142
  resolveGeometry: (descriptor, client) => geometry.resolve(descriptor, client),
2129
2143
  ...(options.sessionRuntimeRegistry
2130
2144
  ? {
2131
- bindSessionRuntime: (descriptor: AttachmentLeaseDescriptor) => {
2145
+ bindSessionRuntime: async (descriptor: AttachmentLeaseDescriptor) => {
2132
2146
  const workspace = options.registry.get(descriptor.target.workspaceName);
2133
2147
  if (!workspace) throw new Error("Terminal attachment workspace is unavailable");
2134
2148
  if (!descriptor.hostClientId) {
2135
2149
  throw new Error("Interactive terminal attachment lacks trusted host identity");
2136
2150
  }
2151
+ await options.sessionRuntimeRegistry!.verifyWindowOwnership();
2137
2152
  return new SessionRuntimeTransportBinder(options.sessionRuntimeRegistry!).bind({
2138
2153
  transport: "terminal-attachment",
2139
2154
  transportLeaseId: descriptor.leaseId,
@@ -2357,12 +2372,12 @@ export class NativeTerminalAttachmentRuntime {
2357
2372
  const catalogIssue: NativeTerminalInventoryCatalogIssue | null = inventory.catalog
2358
2373
  .invalidRuntimeProof
2359
2374
  ? "invalid-runtime-proof"
2360
- : inventory.catalog.missingSemanticStamp
2361
- ? "missing-semantic-stamp"
2362
- : inventory.catalog.duplicateSemanticStamp
2363
- ? "duplicate-semantic-stamp"
2364
- : inventory.catalog.duplicateRuntimePaneBinding
2365
- ? "duplicate-runtime-pane-binding"
2375
+ : inventory.catalog.duplicateRuntimePaneBinding
2376
+ ? "duplicate-runtime-pane-binding"
2377
+ : inventory.catalog.missingSemanticStamp
2378
+ ? "missing-semantic-stamp"
2379
+ : inventory.catalog.duplicateSemanticStamp
2380
+ ? "duplicate-semantic-stamp"
2366
2381
  : null;
2367
2382
  // Ground-truth agent facts (authority + scrape fallback). All IO stays here;
2368
2383
  // the resource projector composes them purely. Absent probe → no facts →
@@ -1,3 +1,5 @@
1
+ import { SharedWindowIndex } from "./shared-window-index.ts";
2
+ import { RegisteredWindowGuard, type RegisteredWindowReader } from "./registered-window-guard.ts";
1
3
  import type { OwnedViewerAdapter } from "./owned-viewer-adapter.ts";
2
4
  import { accessSync, constants, realpathSync, statSync } from "node:fs";
3
5
  import { isAbsolute } from "node:path";
@@ -47,6 +49,7 @@ import {
47
49
  } from "./control-mode-ownership.ts";
48
50
 
49
51
  export interface MirrorServiceOptions {
52
+ onSharedWindowConflict?: (session: string, conflicted: boolean) => void;
50
53
  createOwnedViewerAdapter?: (session: string) => OwnedViewerAdapter | undefined;
51
54
  nativeServerIdentity?: import("../../lib/tmux-server-generation-runner.ts").NativeTmuxServerIdentity;
52
55
  resolveSocketPath?: () => string;
@@ -148,6 +151,47 @@ interface ChannelEntry {
148
151
  export class MirrorService {
149
152
  private readonly opts: MirrorServiceOptions;
150
153
  private readonly channels = new Map<string, ChannelEntry>();
154
+ private readonly sharedWindows = new SharedWindowIndex<SessionChannel>();
155
+ private readonly channelSessions = new Map<SessionChannel, string>();
156
+ private registeredWindows: RegisteredWindowGuard | null = null;
157
+ private readonly registeredConflicts = new Set<string>();
158
+
159
+ setRegisteredWindowReader(reader: RegisteredWindowReader): {
160
+ invalidate(): void;
161
+ dispose(): void;
162
+ } {
163
+ this.registeredWindows?.dispose();
164
+ const guard = new RegisteredWindowGuard(reader);
165
+ this.registeredWindows = guard;
166
+ this.invalidateRegisteredWindows();
167
+ return {
168
+ invalidate: () => {
169
+ if (this.registeredWindows === guard) this.invalidateRegisteredWindows();
170
+ },
171
+ dispose: () => guard.dispose(),
172
+ };
173
+ }
174
+
175
+ private invalidateRegisteredWindows(): void {
176
+ this.registeredWindows?.invalidate();
177
+ }
178
+
179
+ async verifyRegisteredWindows(): Promise<void> {
180
+ const guard = this.registeredWindows;
181
+ if (!guard) return;
182
+ await guard.verify();
183
+ if (guard !== this.registeredWindows || this.disposed)
184
+ throw new Error("Window ownership changed during discovery");
185
+ for (const [channel, session] of this.channelSessions) {
186
+ if (guard.blocked(session)) {
187
+ channel.setGeometryParticipation(false);
188
+ if (!this.registeredConflicts.has(session)) {
189
+ this.registeredConflicts.add(session);
190
+ this.opts.onSharedWindowConflict?.(session, true);
191
+ }
192
+ } else this.registeredConflicts.delete(session);
193
+ }
194
+ }
151
195
  private readonly pendingDisposals = new Set<Promise<void>>();
152
196
  private readonly drainingChannels = new Map<string, Promise<void>>();
153
197
  private readonly sessionExitListeners = new Set<(session: string) => void>();
@@ -239,8 +283,14 @@ export class MirrorService {
239
283
  reseed: () => handle.reseed(),
240
284
  readHistorySize: () => handle.readHistorySize(),
241
285
  captureNativeBacking: () => handle.captureNativeBacking(),
242
- sendText: (text) => handle.sendText(text),
243
- sendKey: (key) => handle.sendKey(key),
286
+ sendText: (text) => {
287
+ this.assertWindowOwnership(request.session);
288
+ handle.sendText(text);
289
+ },
290
+ sendKey: (key) => {
291
+ this.assertWindowOwnership(request.session);
292
+ handle.sendKey(key);
293
+ },
244
294
  close: async () => {
245
295
  if (closed) return;
246
296
  closed = true;
@@ -303,6 +353,7 @@ export class MirrorService {
303
353
  return (args: readonly string[]) => {
304
354
  if (entry.retired || this.channels.get(session) !== entry)
305
355
  return Promise.reject(new Error("Resize mirror session retired"));
356
+ this.assertWindowOwnership(session);
306
357
  return run(args);
307
358
  };
308
359
  }
@@ -313,6 +364,7 @@ export class MirrorService {
313
364
  ) {
314
365
  const entry = this.channels.get(session);
315
366
  if (!entry || entry.retired) throw new Error(`Mirror session ${session} is unavailable`);
367
+ this.assertWindowOwnership(session);
316
368
  return entry.channel.executeWindowLinkAction(request);
317
369
  }
318
370
 
@@ -326,6 +378,7 @@ export class MirrorService {
326
378
  ): void {
327
379
  const entry = this.channels.get(session);
328
380
  if (!entry || entry.retired) throw new Error(`Mirror session ${session} is unavailable`);
381
+ this.assertWindowOwnership(session);
329
382
  entry.channel.sendText(semanticPaneId, text, performanceTraceId, isolated);
330
383
  }
331
384
 
@@ -337,24 +390,61 @@ export class MirrorService {
337
390
  ): void {
338
391
  const entry = this.channels.get(session);
339
392
  if (!entry || entry.retired) throw new Error(`Mirror session ${session} is unavailable`);
393
+ this.assertWindowOwnership(session);
340
394
  entry.channel.sendBytes(semanticPaneId, data, performanceTraceId);
341
395
  }
342
396
 
343
397
  sendKey(session: string, semanticPaneId: string, key: string, performanceTraceId?: string): void {
344
398
  const entry = this.channels.get(session);
345
399
  if (!entry || entry.retired) throw new Error(`Mirror session ${session} is unavailable`);
400
+ this.assertWindowOwnership(session);
346
401
  entry.channel.sendKey(semanticPaneId, key, performanceTraceId);
347
402
  }
348
403
 
404
+ private assertWindowOwnership(session: string): void {
405
+ const message = this.windowOwnershipMessage(session);
406
+ if (message) throw new Error(message);
407
+ }
408
+
409
+ windowOwnershipMessage(session: string): string | null {
410
+ if (this.registeredWindows?.pending())
411
+ return "Window ownership is being verified. Retry after session discovery completes.";
412
+ if (this.hasSharedWindowConflict(session))
413
+ return "Linked windows across registered sessions are unsupported. Unlink the shared window before controlling this session.";
414
+ return null;
415
+ }
416
+
417
+ hasSharedWindowConflict(session: string): boolean {
418
+ const entry = this.channels.get(session);
419
+ return (
420
+ (this.registeredWindows?.blocked(session) ?? false) ||
421
+ (!!entry && this.sharedWindows.conflicted(entry.channel))
422
+ );
423
+ }
424
+
425
+ private updateWindowMembership(channel: SessionChannel, windows: readonly string[]): void {
426
+ // A topology read may finish after retirement. It must not resurrect the
427
+ // old channel's membership or conflict with its replacement.
428
+ if (!this.channelSessions.has(channel)) return;
429
+ for (const [peer, conflict] of this.sharedWindows.update(channel, windows)) {
430
+ const session = this.channelSessions.get(peer);
431
+ if (session === undefined) continue;
432
+ if (conflict) peer.setGeometryParticipation(false);
433
+ this.opts.onSharedWindowConflict?.(session, conflict);
434
+ }
435
+ }
436
+
349
437
  fitViewport(session: string, cols: number, rows: number): void {
350
438
  const entry = this.channels.get(session);
351
439
  if (!entry || entry.retired) throw new Error(`Mirror session ${session} is unavailable`);
440
+ this.assertWindowOwnership(session);
352
441
  entry.channel.fitViewport(cols, rows);
353
442
  }
354
443
 
355
444
  fitWindowViewport(session: string, semanticWindowId: string, cols: number, rows: number): void {
356
445
  const entry = this.channels.get(session);
357
446
  if (!entry || entry.retired) throw new Error(`Mirror session ${session} is unavailable`);
447
+ this.assertWindowOwnership(session);
358
448
  entry.channel.fitWindowViewport(semanticWindowId, cols, rows);
359
449
  }
360
450
 
@@ -367,7 +457,7 @@ export class MirrorService {
367
457
  setGeometryParticipation(session: string, active: boolean): void {
368
458
  const entry = this.channels.get(session);
369
459
  if (!entry || entry.retired) return;
370
- entry.channel.setGeometryParticipation(active);
460
+ entry.channel.setGeometryParticipation(active && !this.hasSharedWindowConflict(session));
371
461
  }
372
462
 
373
463
  /**
@@ -412,12 +502,15 @@ export class MirrorService {
412
502
 
413
503
  async dispose(): Promise<void> {
414
504
  this.disposed = true;
505
+ this.registeredWindows?.dispose();
415
506
  this.sessionExitListeners.clear();
416
507
  const entries = [...this.channels.values()];
417
508
  this.channels.clear();
418
509
  await Promise.allSettled(
419
510
  entries.map(async (entry) => {
420
511
  entry.retired = true;
512
+ this.updateWindowMembership(entry.channel, []);
513
+ this.channelSessions.delete(entry.channel);
421
514
  try {
422
515
  await entry.channel.dispose();
423
516
  } finally {
@@ -468,6 +561,12 @@ export class MirrorService {
468
561
  );
469
562
  const channelOptions: SessionChannelOptions = {
470
563
  session,
564
+ onWindowMembership: (windows) => this.updateWindowMembership(channel, windows),
565
+ hasSharedWindowConflict: () => this.hasSharedWindowConflict(session),
566
+ beforeIdentityRepair: () => this.verifyRegisteredWindows(),
567
+ onWindowTopologyChanged: () => {
568
+ if (this.channelSessions.has(channel)) this.invalidateRegisteredWindows();
569
+ },
471
570
  ownedViewer,
472
571
  createIo: (handlers) =>
473
572
  this.opts.createIo?.(session, handlers) ??
@@ -547,6 +646,7 @@ export class MirrorService {
547
646
  releaseAuthority();
548
647
  throw cause;
549
648
  }
649
+ this.channelSessions.set(channel, session);
550
650
  entry = {
551
651
  channel,
552
652
  started: channel.start(),
@@ -583,6 +683,8 @@ export class MirrorService {
583
683
  private retire(session: string, entry: ChannelEntry): void {
584
684
  if (entry.retired) return;
585
685
  entry.retired = true;
686
+ this.updateWindowMembership(entry.channel, []);
687
+ this.channelSessions.delete(entry.channel);
586
688
  if (this.channels.get(session) === entry) this.channels.delete(session);
587
689
  const disposal = entry.channel
588
690
  .dispose()
@@ -0,0 +1,66 @@
1
+ export type RegisteredWindowReader = (
2
+ signal: AbortSignal,
3
+ ) => Promise<readonly { session: string; window: string }[]>;
4
+
5
+ /** A bounded registration/topology proof, shared by all retained channels. */
6
+ export class RegisteredWindowGuard {
7
+ #epoch = 0;
8
+ #current = false;
9
+ #disposed = false;
10
+ #conflicts = new Set<string>();
11
+ #read: { abort: AbortController; promise: Promise<void> } | null = null;
12
+
13
+ constructor(private readonly reader: RegisteredWindowReader) {}
14
+
15
+ blocked(session: string): boolean {
16
+ return !this.#current || this.#conflicts.has(session);
17
+ }
18
+
19
+ pending(): boolean {
20
+ return !this.#current;
21
+ }
22
+
23
+ invalidate(): void {
24
+ this.#epoch++;
25
+ this.#current = false;
26
+ this.#read?.abort.abort();
27
+ this.#read = null;
28
+ }
29
+
30
+ dispose(): void {
31
+ this.#disposed = true;
32
+ this.invalidate();
33
+ }
34
+
35
+ async verify(): Promise<void> {
36
+ if (this.#disposed) throw new Error("Registered window inventory is disposed");
37
+ if (this.#current) return;
38
+ if (this.#read) return this.#read.promise;
39
+ const epoch = this.#epoch;
40
+ const abort = new AbortController();
41
+ const promise = Promise.resolve()
42
+ .then(async () => {
43
+ abort.signal.throwIfAborted();
44
+ const rows = await this.reader(abort.signal);
45
+ abort.signal.throwIfAborted();
46
+ if (epoch !== this.#epoch) throw new Error("Window ownership changed during discovery");
47
+ const owners = new Map<string, Set<string>>();
48
+ for (const row of rows) {
49
+ const sessions = owners.get(row.window) ?? new Set<string>();
50
+ sessions.add(row.session);
51
+ owners.set(row.window, sessions);
52
+ }
53
+ this.#conflicts = new Set(
54
+ [...owners.values()]
55
+ .filter((sessions) => sessions.size > 1)
56
+ .flatMap((sessions) => [...sessions]),
57
+ );
58
+ this.#current = true;
59
+ })
60
+ .finally(() => {
61
+ if (this.#read?.abort === abort) this.#read = null;
62
+ });
63
+ this.#read = { abort, promise };
64
+ return promise;
65
+ }
66
+ }