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
@@ -81,12 +81,11 @@ function absolutePath(value: string, cwd: string, key: string): string {
81
81
  return path;
82
82
  }
83
83
 
84
- function pathEntryExists(path: string): boolean {
84
+ function pathEntryStat(path: string) {
85
85
  try {
86
- lstatSync(path);
87
- return true;
86
+ return lstatSync(path);
88
87
  } catch (error) {
89
- if ((error as NodeJS.ErrnoException).code === "ENOENT") return false;
88
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return undefined;
90
89
  throw error;
91
90
  }
92
91
  }
@@ -94,21 +93,23 @@ function pathEntryExists(path: string): boolean {
94
93
  function pathIdentity(path: string): string {
95
94
  let cursor = resolve(path);
96
95
  const suffix: string[] = [];
97
- while (!pathEntryExists(cursor)) {
96
+ let stat = pathEntryStat(cursor);
97
+ while (!stat) {
98
98
  const parent = dirname(cursor);
99
99
  if (parent === cursor) break;
100
100
  suffix.unshift(basename(cursor));
101
101
  cursor = parent;
102
+ stat = pathEntryStat(cursor);
102
103
  }
103
- if (pathEntryExists(cursor) && lstatSync(cursor).isSocket()) {
104
+ if (stat?.isSocket()) {
104
105
  return resolve(captureUnixSocketIdentity(cursor).path, ...suffix);
105
106
  }
106
- return resolve(pathEntryExists(cursor) ? realpathSync(cursor) : cursor, ...suffix);
107
+ return resolve(stat ? realpathSync(cursor) : cursor, ...suffix);
107
108
  }
108
109
 
109
- function isInsideOrEqual(path: string, parent: string): boolean {
110
- const child = pathIdentity(path);
111
- const root = pathIdentity(parent);
110
+ function isInsideOrEqual(path: string, parent: string, identity = pathIdentity): boolean {
111
+ const child = identity(path);
112
+ const root = identity(parent);
112
113
  const offset = relative(root, child);
113
114
  return offset === "" || (!offset.startsWith(`..${sep}`) && offset !== "..");
114
115
  }
@@ -124,6 +125,17 @@ function runtimeMode(env: NodeJS.ProcessEnv): RuntimeMode {
124
125
  export function resolveRuntimeNamespace(
125
126
  options: RuntimeNamespaceResolutionOptions = {},
126
127
  ): RuntimeNamespace {
128
+ // Reuse a path's identity only within this synchronous resolution. Every
129
+ // subsequent resolution must see new ancestors and replaced symlinks.
130
+ let identities: Map<string, string> | undefined;
131
+ const identity = (path: string): string => {
132
+ const cached = identities?.get(path);
133
+ if (cached !== undefined) return cached;
134
+ const resolved = pathIdentity(path);
135
+ (identities ??= new Map()).set(path, resolved);
136
+ return resolved;
137
+ };
138
+ const within = (path: string, parent: string) => isInsideOrEqual(path, parent, identity);
127
139
  const env = options.env ?? process.env;
128
140
  const userHome = options.userHome ?? homedir();
129
141
  const cwd = options.cwd ?? process.cwd();
@@ -205,16 +217,13 @@ export function resolveRuntimeNamespace(
205
217
  if (isolated && tmuxSocket.kind === "name" && tmuxSocket.name === "default") {
206
218
  throw new TypeError(`${mode} runtime requires a non-default ${TMUX_SOCKET_NAME_ENV}`);
207
219
  }
208
- if (isolated && isInsideOrEqual(stateHome, canonicalHome)) {
220
+ if (isolated && within(stateHome, canonicalHome)) {
209
221
  throw new TypeError(`${mode} runtime cannot use the canonical tmux-ide state home`);
210
222
  }
211
- if (
212
- isolated &&
213
- (isInsideOrEqual(registryDir, canonicalHome) || isInsideOrEqual(daemonInfoDir, canonicalHome))
214
- ) {
223
+ if (isolated && (within(registryDir, canonicalHome) || within(daemonInfoDir, canonicalHome))) {
215
224
  throw new TypeError(`${mode} runtime cannot use canonical registry or daemon state`);
216
225
  }
217
- if (isolated && tmuxSocket.kind === "path" && isInsideOrEqual(tmuxSocket.path, canonicalHome)) {
226
+ if (isolated && tmuxSocket.kind === "path" && within(tmuxSocket.path, canonicalHome)) {
218
227
  throw new TypeError(`${mode} runtime cannot use a tmux socket inside canonical state`);
219
228
  }
220
229
  if (isolated && cleanupToken === null) {
@@ -229,7 +238,7 @@ export function resolveRuntimeNamespace(
229
238
  (key === "TMUX_IDE_CONFIG" || key === "TMUX_IDE_SETTINGS_DIR"
230
239
  ? env[key]
231
240
  : nonEmpty(env, key)) ?? fallback;
232
- if (development && (!isAbsolute(value) || !isInsideOrEqual(value, development.root)))
241
+ if (development && (!isAbsolute(value) || !within(value, development.root)))
233
242
  throw new TypeError(`development ${key} escapes instance`);
234
243
  return value;
235
244
  };
@@ -267,7 +276,7 @@ export function resolveRuntimeNamespace(
267
276
  if (nonEmpty(env, key)) scoped(key, "");
268
277
  if (env.TMUX) {
269
278
  const socket = /^(.*),[0-9]+,[0-9]+$/u.exec(env.TMUX)?.[1];
270
- if (!socket || pathIdentity(socket) !== pathIdentity(join(runtimeDir, "tmux.sock")))
279
+ if (!socket || identity(socket) !== identity(join(runtimeDir, "tmux.sock")))
271
280
  throw new TypeError("development rejects inherited foreign TMUX authority");
272
281
  }
273
282
  }
@@ -9,8 +9,9 @@ import {
9
9
  renameSync,
10
10
  unlinkSync,
11
11
  writeFileSync,
12
+ watch,
12
13
  } from "node:fs";
13
- import { dirname, join } from "node:path";
14
+ import { basename, dirname, join } from "node:path";
14
15
  import { SavedMachineRegistrySchema, type SavedMachineRegistry } from "@tmux-ide/contracts";
15
16
  import { changeSavedMachines, type SavedMachineChange } from "@tmux-ide/core";
16
17
  import { resolveRuntimeNamespace } from "./runtime-namespace.ts";
@@ -38,6 +39,44 @@ export function loadSavedMachines(path = savedMachinesPath()): SavedMachineRegis
38
39
  }
39
40
  }
40
41
 
42
+ /** Watch the directory so atomic registry replacement does not detach the watcher. */
43
+ export function watchSavedMachines(
44
+ onChange: (registry: SavedMachineRegistry) => void,
45
+ onError: () => void,
46
+ path = savedMachinesPath(),
47
+ ): () => void {
48
+ let stopped = false;
49
+ let pending: ReturnType<typeof setTimeout> | null = null;
50
+ const refresh = () => {
51
+ pending = null;
52
+ if (stopped) return;
53
+ try {
54
+ onChange(loadSavedMachines(path));
55
+ } catch {
56
+ // Keep the last valid routes while an editor is midway through a write.
57
+ onError();
58
+ }
59
+ };
60
+ // A first-run app can subscribe before the daemon creates its registry.
61
+ mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
62
+ const watcher = watch(dirname(path), (_event, file) => {
63
+ if (stopped || (file !== null && file.toString() !== basename(path))) return;
64
+ if (pending) clearTimeout(pending);
65
+ pending = setTimeout(refresh, 50);
66
+ pending.unref();
67
+ });
68
+ watcher.unref();
69
+ watcher.on("error", onError);
70
+ // Close the read-before-subscribe race at startup.
71
+ pending = setTimeout(refresh, 0);
72
+ pending.unref();
73
+ return () => {
74
+ stopped = true;
75
+ if (pending) clearTimeout(pending);
76
+ watcher.close();
77
+ };
78
+ }
79
+
41
80
  /** The daemon owns writes. Synchronous read/reduce/rename cannot interleave within it. */
42
81
  export function updateSavedMachines(
43
82
  change: SavedMachineChange,
@@ -3,6 +3,7 @@ import { spawn, type ChildProcess } from "node:child_process";
3
3
  import { createServer } from "node:net";
4
4
  import type { Readable } from "node:stream";
5
5
  import { z } from "zod";
6
+ import { remoteTmuxIdeCommand } from "./remote-tmux-command.ts";
6
7
  import { createSshDaemonRelay, type SshDaemonRelay } from "./ssh-daemon-relay.ts";
7
8
  import {
8
9
  CanonicalDaemonInfoSchema,
@@ -49,12 +50,30 @@ export class SshConnectionError extends Error {
49
50
  super(`SSH daemon connection: ${message}`);
50
51
  }
51
52
  get retryable(): boolean {
52
- return this.code === "unavailable";
53
+ // A supervisor may temporarily remove the record while replacing its daemon.
54
+ return this.code === "unavailable" || this.code === "daemon-missing";
53
55
  }
54
56
  }
55
57
  function failure(message: string, code: FleetConnectionFailureCode = "unavailable"): Error {
56
58
  return new SshConnectionError(message, code);
57
59
  }
60
+
61
+ /** Classify known OpenSSH errors without publishing remote stderr or credentials. */
62
+ function observeSshFailure(child: SshTransportChild): () => FleetConnectionFailureCode {
63
+ let tail = "";
64
+ let code: FleetConnectionFailureCode = "unavailable";
65
+ child.stderr?.on("data", (chunk: Buffer) => {
66
+ tail = (tail + chunk.toString("utf8")).slice(-4096);
67
+ if (/Host key verification failed|REMOTE HOST IDENTIFICATION HAS CHANGED/i.test(tail))
68
+ code = "ssh-host-key";
69
+ else if (/Permission denied \((?:publickey|password|keyboard-interactive)[^)]*\)/i.test(tail))
70
+ code = "ssh-authentication";
71
+ });
72
+ child.once("close", () => {
73
+ tail = "";
74
+ });
75
+ return () => code;
76
+ }
58
77
  const stoppedChildren = new WeakSet<SshTransportChild>();
59
78
  function stop(child: SshTransportChild): void {
60
79
  if (stoppedChildren.has(child) || child.exitCode !== null || child.signalCode !== null) return;
@@ -153,6 +172,7 @@ const defaults: SshDaemonTransportDependencies = {
153
172
  probe: probeSshDaemonIdentity,
154
173
  };
155
174
  function discover(child: SshTransportChild, signal: AbortSignal): Promise<RemoteDaemon> {
175
+ const sshFailure = observeSshFailure(child);
156
176
  return new Promise((resolve, reject) => {
157
177
  const chunks: Buffer[] = [];
158
178
  let bytes = 0;
@@ -177,8 +197,6 @@ function discover(child: SshTransportChild, signal: AbortSignal): Promise<Remote
177
197
  }
178
198
  chunks.push(Buffer.from(chunk));
179
199
  });
180
- // Drain without capturing: SSH banners/errors may contain confidential remote information.
181
- child.stderr?.resume();
182
200
  child.once("error", () => finish(failure("could not start OpenSSH")));
183
201
  child.once("close", (code) => {
184
202
  if (settled) return;
@@ -186,6 +204,7 @@ function discover(child: SshTransportChild, signal: AbortSignal): Promise<Remote
186
204
  finish(
187
205
  failure(
188
206
  "discovery failed; check SSH authentication, host trust, and remote tmux-ide installation",
207
+ code === 127 ? "remote-cli-missing" : sshFailure(),
189
208
  ),
190
209
  );
191
210
  return;
@@ -266,9 +285,7 @@ export async function openSshDaemonTransport(
266
285
  "ForkAfterAuthentication=no",
267
286
  "--",
268
287
  options.alias,
269
- "tmux-ide",
270
- "remote-daemon-info",
271
- "--json",
288
+ remoteTmuxIdeCommand("discover"),
272
289
  ]);
273
290
  const daemon = await discover(child, signal);
274
291
  const port = await cancellable(dependencies.allocatePort(), signal);
@@ -307,7 +324,7 @@ export async function openSshDaemonTransport(
307
324
  options.alias,
308
325
  ]);
309
326
  child.stdout?.resume();
310
- child.stderr?.resume();
327
+ const sshFailure = observeSshFailure(child);
311
328
  child.once("exit", dispose);
312
329
  const tunnelClosed = new Promise<void>((resolve) => {
313
330
  child!.once("close", () => {
@@ -342,6 +359,7 @@ export async function openSshDaemonTransport(
342
359
  }
343
360
  throw failure(
344
361
  "tunnel could not authenticate the expected daemon before cancellation or timeout",
362
+ sshFailure(),
345
363
  );
346
364
  } catch (error) {
347
365
  dispose();
@@ -268,6 +268,7 @@ export async function createNativeTmuxServerOwner(options: NativeTmuxServerOwner
268
268
  return multiplexer.mutateWindowLink(
269
269
  { operationId, expectedDaemonInstanceId: generation, intent },
270
270
  (session, action) => sessionRuntimeRegistry.executeWindowLinkAction(session, action),
271
+ timing,
271
272
  );
272
273
  }
273
274
  if (intent.verb === "workspace.pane.resize") {
@@ -77,11 +77,29 @@ export function createTmuxServerProbe(
77
77
  )
78
78
  ).stdout.trimEnd();
79
79
  try {
80
- const first = await read(args);
80
+ // A direct socket can be pinned before contacting tmux. Named servers
81
+ // and aliases still need discovery followed by an explicit-path proof.
82
+ let pinned: ReturnType<typeof captureUnixSocketIdentity> | null = null;
83
+ if (selector.kind === "path") {
84
+ try {
85
+ pinned = captureUnixSocketIdentity(selector.path);
86
+ } catch {
87
+ // Preserve discovery for aliases which are not direct socket paths.
88
+ }
89
+ }
90
+ const first = await read(pinned ? ["-S", pinned.path] : args);
81
91
  const match = /^(.*)\|([1-9][0-9]*)\|([1-9][0-9]*)$/u.exec(first);
82
92
  if (!match) return null;
83
- const socket = captureUnixSocketIdentity(match[1]!);
84
- if ((await read(["-S", socket.path])) !== first) return null;
93
+ const reportedSocket = captureUnixSocketIdentity(match[1]!);
94
+ const socket = pinned ?? reportedSocket;
95
+ if (pinned) {
96
+ if (
97
+ reportedSocket.path !== pinned.path ||
98
+ reportedSocket.dev !== pinned.dev ||
99
+ reportedSocket.ino !== pinned.ino
100
+ )
101
+ return null;
102
+ } else if ((await read(["-S", socket.path])) !== first) return null;
85
103
  revalidateUnixSocketIdentity(socket);
86
104
  return {
87
105
  nativeServerIdentity: { pid: match[2]!, startTime: match[3]! },
@@ -28,7 +28,7 @@ import {
28
28
  } from "node:fs";
29
29
  import { dirname, join } from "node:path";
30
30
  import { gunzipSync } from "node:zlib";
31
- import { acquireTuiDownloadLock } from "./tui-download-lock.ts";
31
+ import { acquirePrivateOperationLock } from "./private-operation-lock.ts";
32
32
  import { getCurrentVersion } from "./update-check.ts";
33
33
 
34
34
  /** The `<os>-<arch>` tags we publish a prebuilt TUI binary for. */
@@ -360,7 +360,7 @@ export async function downloadTuiBinary(
360
360
  timeoutMs: opts.timeoutMs ?? TUI_DOWNLOAD_TIMEOUT_MS,
361
361
  };
362
362
  mkdirSync(dirname(dest), { recursive: true });
363
- const releaseLock = await acquireTuiDownloadLock(
363
+ const releaseLock = await acquirePrivateOperationLock(
364
364
  downloadLockPath(dest),
365
365
  limits.timeoutMs * 2 + 5_000,
366
366
  );
@@ -1,3 +1,8 @@
1
+ import {
2
+ managedInstallerPrefix,
3
+ runManagedInstallerUpdate,
4
+ MANAGED_INSTALLER_URL,
5
+ } from "./managed-installer-update.ts";
1
6
  import { resolveRuntimeNamespace } from "./runtime-namespace.ts";
2
7
  import { parseStrictSemver } from "./semver.ts";
3
8
  /** Origin-aware update planning; unsupported origins never fall back to npm. */
@@ -28,6 +33,7 @@ export interface UpdatePlan {
28
33
  args?: string[];
29
34
  guidance?: string;
30
35
  proposedCommand?: string;
36
+ installerPrefix?: string;
31
37
  }
32
38
  export const UPDATE_COMMANDS: Record<PackageManager, string> = {
33
39
  npm: "npm install -g tmux-ide@latest",
@@ -62,7 +68,21 @@ export function planUpdate(input: {
62
68
  reason: `global ${method} layout (${input.cliPath})`,
63
69
  };
64
70
  }
71
+ if (method === "installer") {
72
+ const prefix = managedInstallerPrefix(input.cliPath);
73
+ if (prefix) {
74
+ return {
75
+ method,
76
+ channel,
77
+ installerPrefix: prefix,
78
+ command: `installer ${MANAGED_INSTALLER_URL} --prefix ${JSON.stringify(prefix)} --version ${channel}`,
79
+ reason: `verified active managed installation (${prefix})`,
80
+ };
81
+ }
82
+ }
65
83
  const guidance: Record<Exclude<InstallOrigin, PackageManager>, string> = {
84
+ installer:
85
+ "Rerun https://tmux-ide.com/install.sh with the same --prefix to update atomically and retain rollback. This managed release must not be modified by npm in place.",
66
86
  dev: "Update this checkout with git pull, then follow its build instructions.",
67
87
  homebrew: "Update with brew upgrade tmux-ide (using the tap/formula you installed).",
68
88
  yarn: `Update this Yarn global installation with yarn global add tmux-ide@${channel}.`,
@@ -167,6 +187,10 @@ export function runUpdate(
167
187
  });
168
188
  executed = true;
169
189
  }
190
+ if (!dryRun && plan.installerPrefix && plan.channel) {
191
+ runManagedInstallerUpdate(plan.installerPrefix, plan.channel, json, dependencies.execute);
192
+ executed = true;
193
+ }
170
194
  if (json) output(JSON.stringify({ ...plan, current, latest, dryRun, executed }));
171
195
  return plan;
172
196
  }
@@ -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 →