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
@@ -84,6 +84,64 @@ export function createApplicationTerminalInteractionController(options) {
84
84
  let diagnosticWindowFrame = null;
85
85
  let diagnosticWindowFrameExpiresAtMicros = 0;
86
86
  let inputAuthorityIdentity = null;
87
+ // Pane selection belongs to the control connection, which can outlive a
88
+ // terminal renderer rebuild. Never carry selection or input across reconnects.
89
+ const controlIdentity = (snapshot) => {
90
+ if (!snapshot ||
91
+ !["live", "rebinding"].includes(snapshot.status) ||
92
+ !snapshot.connection ||
93
+ !snapshot.client ||
94
+ !snapshot.daemonGeneration)
95
+ return null;
96
+ try {
97
+ const generation = snapshot.client.getSnapshot().generation;
98
+ if (!Number.isSafeInteger(generation))
99
+ return null;
100
+ return {
101
+ connection: snapshot.connection,
102
+ client: snapshot.client,
103
+ daemon: snapshot.daemonGeneration,
104
+ generation,
105
+ workspace: snapshot.connection.workspaceName,
106
+ };
107
+ }
108
+ catch {
109
+ return null;
110
+ }
111
+ };
112
+ const sameControl = (left, right) => left !== null &&
113
+ right !== null &&
114
+ left.connection === right.connection &&
115
+ left.client === right.client &&
116
+ left.daemon === right.daemon &&
117
+ left.generation === right.generation &&
118
+ left.workspace === right.workspace;
119
+ let selectedControl = null;
120
+ const waitingInput = new Set();
121
+ let waitingInputBytes = 0;
122
+ let releasingInput = null;
123
+ const awaitInputRuntime = (weight) => {
124
+ const control = controlIdentity(options.generation());
125
+ if (!control || waitingInput.size >= 64 || waitingInputBytes + weight > 1024 * 1024)
126
+ return Promise.resolve(false);
127
+ return new Promise((resolve) => {
128
+ const entry = {
129
+ control,
130
+ weight,
131
+ settle: (ready) => {
132
+ if (!waitingInput.delete(entry))
133
+ return;
134
+ waitingInputBytes -= weight;
135
+ clearTimeout(timer);
136
+ resolve(ready);
137
+ },
138
+ };
139
+ const timer = setTimeout(() => entry.settle(false), 5000);
140
+ timer.unref?.();
141
+ waitingInput.add(entry);
142
+ waitingInputBytes += weight;
143
+ });
144
+ };
87
145
  const armDiagnosticWindowFrame = (pending) => {
88
146
  diagnosticWindowFrame = pending;
89
147
  diagnosticWindowFrameExpiresAtMicros = pending.startedAtMicros + 6_000_000;
@@ -356,6 +414,71 @@ export function createApplicationTerminalInteractionController(options) {
356
414
  pendingWindowSwitch = null;
357
415
  }
358
416
  };
417
+ const beginWindowSwitch = (pane, target, windowLink) => {
418
+ pendingWindowSwitch = null;
419
+ if (options.diagnosticsEnabled) {
420
+ try {
421
+ const active = options.generation();
422
+ const identity = active?.adapter?.paneCanonicalIdentity(pane);
423
+ const clientGeneration = active?.client?.getSnapshot().generation;
424
+ const startedAtMicros = diagnosticNowMicros();
425
+ if (active?.status === "live" &&
426
+ active.daemonGeneration &&
427
+ identity &&
428
+ Number.isSafeInteger(clientGeneration) &&
429
+ startedAtMicros !== null) {
430
+ pendingWindowSwitch = {
431
+ selectionKind: windowLink ? "window-link" : "pane",
432
+ ...(windowLink ? { windowLink } : {}),
433
+ traceId: createTraceId(),
434
+ target,
435
+ paneId: pane,
436
+ startedAtMicros,
437
+ daemonGeneration: active.daemonGeneration,
438
+ clientGeneration: clientGeneration,
439
+ rendererEpoch: active.rendererEpoch,
440
+ sourceEpoch: identity.sourceEpoch,
441
+ generation: identity.generation,
442
+ incarnation: identity.incarnation,
443
+ revision: identity.revision,
444
+ stateHash: identity.stateHash,
445
+ cols: identity.cols,
446
+ rows: identity.rows,
447
+ layoutPublished: false,
448
+ selectionApplied: false,
449
+ presentationPublished: false,
450
+ followUpRequested: false,
451
+ ...(options.detailedWindowSwitchTiming
452
+ ? {
453
+ timing: {
454
+ receiptAtMicros: null,
455
+ layoutAtMicros: null,
456
+ presentationAtMicros: null,
457
+ },
458
+ }
459
+ : {}),
460
+ };
461
+ armDiagnosticWindowFrame({ kind: "window-switch", ...pendingWindowSwitch });
462
+ diagnose("window-switch-start", { ...pendingWindowSwitch });
463
+ }
464
+ else {
465
+ diagnose("window-switch-untracked", {
466
+ paneId: pane,
467
+ target,
468
+ status: active?.status ?? null,
469
+ daemonGeneration: active?.daemonGeneration ?? null,
470
+ rendererEpoch: active?.rendererEpoch ?? null,
471
+ clientGeneration: clientGeneration ?? null,
472
+ identity: identity ?? null,
473
+ laneCounters: active?.fastLane?.lane.counters() ?? null,
474
+ });
475
+ }
476
+ }
477
+ catch {
478
+ pendingWindowSwitch = null;
479
+ }
480
+ }
481
+ };
359
482
  let linkSelection = null;
360
483
  const finishLinkSelection = (selected) => {
361
484
  const pending = linkSelection;
@@ -363,6 +486,20 @@ export function createApplicationTerminalInteractionController(options) {
363
486
  return;
364
487
  linkSelection = null;
365
488
  clearTimeout(pending.timer);
489
+ const diagnostic = pending.diagnostic;
490
+ if (diagnostic && pendingWindowSwitch === diagnostic) {
491
+ if (selected) {
492
+ diagnostic.selectionApplied = true;
493
+ maybeRequestWindowSwitchFrame();
494
+ }
495
+ else {
496
+ diagnoseCritical(`window-switch:${diagnostic.traceId}:failed`, "window-switch-failed", {
497
+ ...diagnostic,
498
+ reason: "window-link-selection-not-applied",
499
+ });
500
+ pendingWindowSwitch = null;
501
+ }
502
+ }
366
503
  pending.resolve(selected);
367
504
  };
368
505
  const reconcileLinkSelection = (snapshot) => {
@@ -386,11 +523,18 @@ export function createApplicationTerminalInteractionController(options) {
386
523
  select: async (paneId) => {
387
524
  finishLinkSelection(false);
388
525
  const expected = liveSelectionTarget();
526
+ const selectionControl = controlIdentity(options.generation());
389
527
  const selecting = pendingWindowSwitch;
390
528
  const operationId = selecting?.traceId;
391
529
  const selectionFailure = { current: null };
392
530
  const receipt = expected
393
- ? await selectTerminalPane(expected, liveSelectionTarget, paneId, operationId, (failure) => {
531
+ ? await selectTerminalPane(expected, () => {
532
+ if (!selectionControl)
533
+ return liveSelectionTarget();
534
+ return sameControl(selectionControl, controlIdentity(options.generation()))
535
+ ? expected
536
+ : null;
537
+ }, paneId, operationId, (failure) => {
394
538
  selectionFailure.current = failure;
395
539
  }, windowLinkForPane(options.layout(), paneId) ?? undefined)
396
540
  : null;
@@ -455,6 +599,16 @@ export function createApplicationTerminalInteractionController(options) {
455
599
  },
456
600
  send: async (paneId, routed) => {
457
601
  const { input, parserOrigin } = routed;
602
+ const selectionVersion = paneInput.selectionVersion;
603
+ if (options.generation()?.status === "rebinding") {
604
+ if (!(await awaitInputRuntime(Buffer.byteLength(input.data, "utf8"))))
605
+ return false;
606
+ }
607
+ else if (releasingInput) {
608
+ await releasingInput;
609
+ }
610
+ if (paneInput.selectionVersion !== selectionVersion || paneInput.focusedPane !== paneId)
611
+ return false;
458
612
  const active = options.generation();
459
613
  if (active?.status !== "live" || !active.fastLane)
460
614
  return false;
@@ -534,7 +688,35 @@ export function createApplicationTerminalInteractionController(options) {
534
688
  onFocusedPane: options.setFocusedPane,
535
689
  });
536
690
  return {
691
+ cancelPendingInput() {
692
+ finishLinkSelection(false);
693
+ paneInput.invalidateSelection();
694
+ for (const entry of waitingInput)
695
+ entry.settle(false);
696
+ },
537
697
  adoptGeneration(snapshot) {
698
+ const control = controlIdentity(snapshot);
699
+ if (!sameControl(selectedControl, control)) {
700
+ finishLinkSelection(false);
701
+ paneInput.invalidateSelection();
702
+ }
703
+ selectedControl = control;
704
+ if (waitingInput.size > 0 && snapshot?.status === "live" && snapshot.fastLane) {
705
+ // Old waiters resume first; input admitted synchronously by the next
706
+ // layout publication must not overtake them in this microtask turn.
707
+ const barrier = Promise.resolve();
708
+ releasingInput = barrier;
709
+ queueMicrotask(() => {
710
+ if (releasingInput === barrier)
711
+ releasingInput = null;
712
+ });
713
+ }
714
+ for (const entry of waitingInput) {
715
+ if (!sameControl(entry.control, control))
716
+ entry.settle(false);
717
+ else if (snapshot?.status === "live" && snapshot.fastLane)
718
+ entry.settle(true);
719
+ }
538
720
  let next = null;
539
721
  if (snapshot?.status === "live" &&
540
722
  snapshot.daemonGeneration &&
@@ -570,7 +752,6 @@ export function createApplicationTerminalInteractionController(options) {
570
752
  next.adapter !== inputAuthorityIdentity.adapter);
571
753
  if (next === null || replaced) {
572
754
  finishLinkSelection(false);
573
- paneInput.invalidateSelection();
574
755
  pendingWindowSwitch = null;
575
756
  pendingWindowRename = null;
576
757
  diagnosticWindowFrame = null;
@@ -715,6 +896,7 @@ export function createApplicationTerminalInteractionController(options) {
715
896
  observed.expectedSemanticWindowId !== target.expectedSemanticWindowId)
716
897
  return;
717
898
  finishLinkSelection(false);
899
+ beginWindowSwitch(paneId, target.expectedSemanticWindowId, target);
718
900
  let resolve;
719
901
  const settled = new Promise((done) => {
720
902
  resolve = done;
@@ -724,6 +906,7 @@ export function createApplicationTerminalInteractionController(options) {
724
906
  paneId,
725
907
  layout: snapshot,
726
908
  received: false,
909
+ diagnostic: pendingWindowSwitch,
727
910
  resolve,
728
911
  timer: setTimeout(() => {
729
912
  if (linkSelection === pending)
@@ -751,6 +934,7 @@ export function createApplicationTerminalInteractionController(options) {
751
934
  }
752
935
  const wrapper = (await expected.client.dispatch({
753
936
  kind: "semantic-intent",
937
+ ...(pending.diagnostic ? { operationId: pending.diagnostic.traceId } : {}),
754
938
  intent: {
755
939
  verb: "workspace.window.link.select",
756
940
  workspaceName: expected.workspaceName,
@@ -763,6 +947,7 @@ export function createApplicationTerminalInteractionController(options) {
763
947
  if (wrapper.kind !== "semantic-intent" ||
764
948
  !result.success ||
765
949
  result.data.operationId !== wrapper.operationId ||
950
+ (pending.diagnostic && wrapper.operationId !== pending.diagnostic.traceId) ||
766
951
  result.data.daemonInstanceId !== expected.daemonGeneration ||
767
952
  result.data.workspaceName !== expected.workspaceName ||
768
953
  result.data.target.linkId !== target.linkId ||
@@ -772,6 +957,18 @@ export function createApplicationTerminalInteractionController(options) {
772
957
  finishLinkSelection(false);
773
958
  return;
774
959
  }
960
+ if (pending.diagnostic) {
961
+ const receiptAtMicros = diagnosticNowMicros();
962
+ if (pending.diagnostic.timing)
963
+ pending.diagnostic.timing.receiptAtMicros = receiptAtMicros;
964
+ diagnose("window-switch-receipt", {
965
+ ...pending.diagnostic,
966
+ operationId: wrapper.operationId,
967
+ selected: true,
968
+ applied: true,
969
+ ...(options.detailedWindowSwitchTiming ? { phaseAtMicros: receiptAtMicros } : {}),
970
+ });
971
+ }
775
972
  pending.received = true;
776
973
  reconcileLinkSelection(pending.layout);
777
974
  await settled;
@@ -1022,54 +1219,7 @@ export function createApplicationTerminalInteractionController(options) {
1022
1219
  const target = next?.semanticWindowId ?? next?.windowName;
1023
1220
  if (!pane || !target)
1024
1221
  return;
1025
- if (options.diagnosticsEnabled) {
1026
- try {
1027
- const active = options.generation();
1028
- const identity = active?.adapter?.paneCanonicalIdentity(pane);
1029
- const clientGeneration = active?.client?.getSnapshot().generation;
1030
- const startedAtMicros = diagnosticNowMicros();
1031
- if (active?.status === "live" &&
1032
- active.daemonGeneration &&
1033
- identity &&
1034
- Number.isSafeInteger(clientGeneration) &&
1035
- startedAtMicros !== null) {
1036
- pendingWindowSwitch = {
1037
- traceId: createTraceId(),
1038
- target,
1039
- paneId: pane,
1040
- startedAtMicros,
1041
- daemonGeneration: active.daemonGeneration,
1042
- clientGeneration: clientGeneration,
1043
- rendererEpoch: active.rendererEpoch,
1044
- sourceEpoch: identity.sourceEpoch,
1045
- generation: identity.generation,
1046
- incarnation: identity.incarnation,
1047
- revision: identity.revision,
1048
- stateHash: identity.stateHash,
1049
- cols: identity.cols,
1050
- rows: identity.rows,
1051
- layoutPublished: false,
1052
- selectionApplied: false,
1053
- presentationPublished: false,
1054
- followUpRequested: false,
1055
- ...(options.detailedWindowSwitchTiming
1056
- ? {
1057
- timing: {
1058
- receiptAtMicros: null,
1059
- layoutAtMicros: null,
1060
- presentationAtMicros: null,
1061
- },
1062
- }
1063
- : {}),
1064
- };
1065
- armDiagnosticWindowFrame({ kind: "window-switch", ...pendingWindowSwitch });
1066
- diagnose("window-switch-start", { ...pendingWindowSwitch });
1067
- }
1068
- }
1069
- catch {
1070
- pendingWindowSwitch = null;
1071
- }
1072
- }
1222
+ beginWindowSwitch(pane, target);
1073
1223
  paneInput.selectPane(pane, { presentOptimistically: false });
1074
1224
  },
1075
1225
  async zoomPane(targetPane) {
@@ -1265,6 +1415,7 @@ export function createApplicationTerminalInteractionController(options) {
1265
1415
  ? {}
1266
1416
  : { phaseAtMicros: pending.timing.presentationAtMicros }),
1267
1417
  });
1418
+ maybeRequestWindowSwitchFrame();
1268
1419
  },
1269
1420
  observeDiagnosticWindowFrame() {
1270
1421
  if (!options.diagnosticsEnabled)
@@ -2,20 +2,25 @@
2
2
  export class BoundedPerformanceRecordWriter {
3
3
  #sink;
4
4
  #criticalLimit;
5
- #pendingCritical = new Map();
5
+ #pendingCritical = new Set();
6
+ #pending = [];
7
+ #pendingByteLimit;
8
+ #pendingBytes = 0;
9
+ #flushPromise = null;
10
+ #resolveFlush = null;
6
11
  #saturated = false;
7
12
  #failed = false;
8
13
  #droppedRecords = 0;
9
- constructor(sink, criticalLimit = 16) {
14
+ constructor(sink, criticalLimit = 16, pendingByteLimit = 64 * 1024) {
10
15
  this.#sink = sink;
11
16
  this.#criticalLimit = criticalLimit;
17
+ this.#pendingByteLimit = pendingByteLimit;
12
18
  }
13
19
  write(record) {
14
20
  if (this.#failed)
15
21
  return false;
16
22
  if (this.#saturated) {
17
- this.#droppedRecords += 1;
18
- return false;
23
+ return this.#enqueue(record);
19
24
  }
20
25
  return this.#writeAccepted(record);
21
26
  }
@@ -30,22 +35,57 @@ export class BoundedPerformanceRecordWriter {
30
35
  this.#droppedRecords += 1;
31
36
  return false;
32
37
  }
33
- this.#pendingCritical.set(key, record);
34
- return true;
38
+ return this.#enqueue(record, key);
35
39
  }
36
40
  drain() {
37
41
  if (this.#failed)
38
42
  return;
39
43
  this.#saturated = false;
40
- for (const [key, record] of this.#pendingCritical) {
41
- this.#pendingCritical.delete(key);
42
- if (!this.#writeAccepted(record))
43
- break;
44
+ while (this.#pending.length > 0 && !this.#saturated && !this.#failed) {
45
+ const next = this.#pending.shift();
46
+ this.#pendingBytes -= next.bytes;
47
+ if (next.key !== undefined)
48
+ this.#pendingCritical.delete(next.key);
49
+ this.#writeAccepted(next.record);
44
50
  }
51
+ if (this.#pending.length === 0)
52
+ this.#finishFlush();
53
+ }
54
+ /** Wait until retained records reach the stream; its owner then flushes/end()s it. */
55
+ flush() {
56
+ if (this.#failed || this.#pending.length === 0)
57
+ return Promise.resolve();
58
+ this.#flushPromise ??= new Promise((resolve) => {
59
+ this.#resolveFlush = resolve;
60
+ });
61
+ return this.#flushPromise;
45
62
  }
46
63
  fail() {
47
64
  this.#failed = true;
48
65
  this.#pendingCritical.clear();
66
+ this.#pending.length = 0;
67
+ this.#pendingBytes = 0;
68
+ this.#finishFlush();
69
+ }
70
+ #enqueue(record, key) {
71
+ const bytes = Buffer.byteLength(record);
72
+ // Ordinary bursts get one extra stream-sized buffer. Critical records have
73
+ // a separate byte reserve as well as their existing key-count limit.
74
+ const limit = this.#pendingByteLimit + (key === undefined ? 0 : 64 * 1024);
75
+ if (this.#pending.length >= 1024 || this.#pendingBytes + bytes > limit) {
76
+ this.#droppedRecords += 1;
77
+ return false;
78
+ }
79
+ if (key !== undefined)
80
+ this.#pendingCritical.add(key);
81
+ this.#pending.push({ record, bytes, ...(key === undefined ? {} : { key }) });
82
+ this.#pendingBytes += bytes;
83
+ return true;
84
+ }
85
+ #finishFlush() {
86
+ this.#resolveFlush?.();
87
+ this.#resolveFlush = null;
88
+ this.#flushPromise = null;
49
89
  }
50
90
  diagnostics() {
51
91
  return Object.freeze({
@@ -271,12 +271,13 @@ export class TerminalFastLaneRendererAdapter {
271
271
  ]);
272
272
  if (seen.has(key))
273
273
  continue;
274
+ // Bound retained de-duplication history, not the renderer's lifetime.
275
+ // An evicted identity may be reported again only after another real paint.
276
+ // Pending identities still have their separate per-frame admission limit.
274
277
  if (seen.size >= 256)
275
- this.#droppedCanonicalHostFrames += 1;
276
- else {
277
- seen.add(key);
278
- identities.push(identity);
279
- }
278
+ seen.delete(seen.values().next().value);
279
+ seen.add(key);
280
+ identities.push(identity);
280
281
  }
281
282
  const dropped = this.#droppedCanonicalHostFrames;
282
283
  this.#droppedCanonicalHostFrames = 0;
@@ -13,6 +13,9 @@ export class TerminalPaneInputRouter {
13
13
  constructor(options) {
14
14
  this.#options = options;
15
15
  }
16
+ get selectionVersion() {
17
+ return this.#selectionToken;
18
+ }
16
19
  get focusedPane() {
17
20
  return this.#focusedPane;
18
21
  }
@@ -1,5 +1,7 @@
1
1
  import { createTerminalFastLane, createWorkspaceClientTerminalSource, } from "@tmux-ide/daemon-client/terminal-fast-lane";
2
2
  import { currentTuiPerformanceEventSink } from "../performance-events.js";
3
+ // Limit outstanding low-water sampling chains, not completed observations over
4
+ // the lifetime of a renderer. Each chain owns at most one pending timeout.
3
5
  const TERMINAL_RESOURCE_SAMPLE_LIMIT = 512;
4
6
  // The ProductRig idle proof reads after 10.1s. Emit just before that boundary
5
7
  // so the operation-scoped sample cannot race the proof read.
@@ -227,7 +229,7 @@ function createOpenTuiTerminalResourceSampler(lane, sink) {
227
229
  heartbeatTimer = setTimeout(heartbeat, TERMINAL_RESOURCE_HEARTBEAT_MS);
228
230
  heartbeatTimer.unref?.();
229
231
  const emit = (operation, identity, expectedAt, sampled) => {
230
- if (disposed || resourceEpochIdentity === null || ordinal >= TERMINAL_RESOURCE_SAMPLE_LIMIT)
232
+ if (disposed || resourceEpochIdentity === null)
231
233
  return;
232
234
  ordinal += 1;
233
235
  const now = readDiagnosticNow();
@@ -373,8 +375,12 @@ function createOpenTuiTerminalResourceSampler(lane, sink) {
373
375
  };
374
376
  return {
375
377
  afterFence(identity) {
376
- if (disposed || ordinal >= TERMINAL_RESOURCE_SAMPLE_LIMIT)
378
+ if (disposed)
379
+ return;
380
+ if (pending.size >= TERMINAL_RESOURCE_SAMPLE_LIMIT) {
381
+ recordSamplingFailure();
377
382
  return;
383
+ }
378
384
  latestIdentity = identity;
379
385
  let now;
380
386
  if (resourceEpochIdentity === null) {
@@ -50,3 +50,38 @@ function graphemes(text) {
50
50
  return [...new Segmenter(undefined, { granularity: "grapheme" }).segment(text)].map((entry) => entry.segment);
51
51
  return [...text];
52
52
  }
53
+ /** PURE — greedy word-wrap for terminal prose (never returns empty for
54
+ * non-empty text; words longer than the width hard-break). */
55
+ export function wrapText(text, width) {
56
+ if (width <= 0)
57
+ return [text];
58
+ const out = [];
59
+ for (const para of text.split("\n")) {
60
+ let line = "";
61
+ for (const word of para.split(/\s+/).filter(Boolean)) {
62
+ if (line.length === 0) {
63
+ let w = word;
64
+ while (w.length > width) {
65
+ out.push(w.slice(0, width));
66
+ w = w.slice(width);
67
+ }
68
+ line = w;
69
+ }
70
+ else if (line.length + 1 + word.length <= width) {
71
+ line += ` ${word}`;
72
+ }
73
+ else {
74
+ out.push(line);
75
+ let w = word;
76
+ while (w.length > width) {
77
+ out.push(w.slice(0, width));
78
+ w = w.slice(width);
79
+ }
80
+ line = w;
81
+ }
82
+ }
83
+ if (line.length > 0 || para.length === 0)
84
+ out.push(line);
85
+ }
86
+ return out.length > 0 ? out : [""];
87
+ }
@@ -156,9 +156,11 @@ export function createApplicationConnectionFeedback(onPublish = () => undefined)
156
156
  failure,
157
157
  ...(inventory
158
158
  ? {
159
- recovery: missing
160
- ? "A registered session may have been recreated. Choose that session to restore its identity, then retry here. It may be a different session."
161
- : "Terminal identities conflict or cannot be verified. Copy connection details for diagnosis; Retry does not repair identity conflicts.",
159
+ recovery: failure.reason === "duplicate-runtime-pane-binding"
160
+ ? "A terminal is shared by multiple workspace entries. Unlink shared tmux windows so each belongs to one session, then select Retry."
161
+ : missing
162
+ ? "A registered session may have been recreated. Choose that session to restore its identity, then retry here. It may be a different session."
163
+ : "Terminal identities conflict or cannot be verified. Copy connection details for diagnosis; Retry does not repair identity conflicts.",
162
164
  }
163
165
  : {}),
164
166
  };
@@ -546,16 +546,7 @@ export function createDefaultFleetFactsReaders(): Pick<
546
546
  return null;
547
547
  }
548
548
  };
549
- return {
550
- readSessions: async (signal) => {
551
- const raw = await execute(SESSION_COMPOSITION_TMUX_ARGS, signal);
552
- return raw === null ? null : parseSessionCompositionFacts(raw);
553
- },
554
- readAgents: async (signal) => {
555
- const raw = await execute(AGENT_STATE_TMUX_ARGS, signal);
556
- return raw === null ? null : parseAgentStateFacts(raw);
557
- },
558
- };
549
+ return createSharedFleetFactsReaders(execute);
559
550
  }
560
551
 
561
552
  export const SESSION_COMPOSITION_TMUX_ARGS = [
@@ -593,3 +584,102 @@ export const AGENT_STATE_TMUX_ARGS = [
593
584
  export async function readAgentStateFacts(signal?: AbortSignal): Promise<AgentStateReading | null> {
594
585
  return createDefaultFleetFactsReaders().readAgents(signal);
595
586
  }
587
+
588
+ /** Agent columns are excluded from the session topology projection. */
589
+ export const FLEET_FACTS_TMUX_ARGS = [
590
+ "list-panes",
591
+ "-a",
592
+ "-F",
593
+ `${SESSION_COMPOSITION_TMUX_ARGS[3]}\t#{@agent_state}\t#{pane_current_command}`,
594
+ ] as const;
595
+
596
+ /** Share only an outstanding read; completed snapshots are never cached. */
597
+ export function createSharedFleetFactsReaders(
598
+ run: (args: readonly string[], signal?: AbortSignal) => string | null | Promise<string | null>,
599
+ isAbsentServer: (error: unknown) => boolean = () => false,
600
+ ): Pick<DaemonFleetFactsObserverOptions, "readSessions" | "readAgents"> {
601
+ interface Pending {
602
+ controller: AbortController;
603
+ result: Promise<string | null>;
604
+ readers: number;
605
+ }
606
+ let pending: Pending | null = null;
607
+ const read = async (signal?: AbortSignal): Promise<string | null> => {
608
+ if (signal?.aborted) return null;
609
+ if (!pending) {
610
+ const controller = new AbortController();
611
+ const entry: Pending = {
612
+ controller,
613
+ readers: 0,
614
+ result: Promise.resolve().then(() => run(FLEET_FACTS_TMUX_ARGS, controller.signal)),
615
+ };
616
+ entry.result = entry.result.finally(() => {
617
+ if (pending === entry) pending = null;
618
+ });
619
+ pending = entry;
620
+ }
621
+ const entry = pending;
622
+ entry.readers += 1;
623
+ let released = false;
624
+ const release = () => {
625
+ if (released) return;
626
+ released = true;
627
+ entry.readers -= 1;
628
+ if (entry.readers === 0) {
629
+ if (pending === entry) pending = null;
630
+ entry.controller.abort();
631
+ }
632
+ };
633
+ signal?.addEventListener("abort", release, { once: true });
634
+ try {
635
+ // Keep the promise outstanding for a noncooperative runner so the
636
+ // observer's per-kind quarantine remains effective after cancellation.
637
+ const raw = await entry.result;
638
+ return signal?.aborted ? null : raw;
639
+ } catch (error) {
640
+ if (signal?.aborted) return null;
641
+ throw error;
642
+ } finally {
643
+ signal?.removeEventListener("abort", release);
644
+ release();
645
+ }
646
+ };
647
+ return {
648
+ readSessions: async (signal) => {
649
+ try {
650
+ const raw = await read(signal);
651
+ return raw === null
652
+ ? null
653
+ : parseSessionCompositionFacts(
654
+ raw
655
+ .split("\n")
656
+ .map((line) => line.split("\t").slice(0, 11).join("\t"))
657
+ .join("\n"),
658
+ );
659
+ } catch (error) {
660
+ // Socket absence is an authoritative empty catalog, but must not
661
+ // manufacture agent turn completions from a failed agent observation.
662
+ return isAbsentServer(error) ? parseSessionCompositionFacts("") : null;
663
+ }
664
+ },
665
+ readAgents: async (signal) => {
666
+ try {
667
+ const raw = await read(signal);
668
+ return raw === null
669
+ ? null
670
+ : parseAgentStateFacts(
671
+ raw
672
+ .split("\n")
673
+ .map((line) => {
674
+ const fields = line.split("\t");
675
+ if (fields.length !== 13) return "";
676
+ return [fields[0], fields[6], fields[9], fields[11], fields[12]].join("\t");
677
+ })
678
+ .join("\n"),
679
+ );
680
+ } catch {
681
+ return null;
682
+ }
683
+ },
684
+ };
685
+ }