tmux-ide 2.9.0-beta.21 → 2.9.0-beta.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (209) hide show
  1. package/bin/cli.js +46791 -43589
  2. package/package.json +2 -2
  3. package/packages/contracts/src/__tests__/daemon-wire.test.ts +2 -2
  4. package/packages/contracts/src/__tests__/fixtures/daemon-wire-v3.json +35 -0
  5. package/packages/contracts/src/__tests__/issue-error.test.ts +1 -1
  6. package/packages/contracts/src/__tests__/multiplexer-verbs.test.ts +3 -1
  7. package/packages/contracts/src/__tests__/pane-stream.test.ts +17 -5
  8. package/packages/contracts/src/__tests__/tmux-server-scope.test.ts +166 -0
  9. package/packages/contracts/src/__tests__/window-links.test.ts +243 -0
  10. package/packages/contracts/src/actions-contract.ts +12 -0
  11. package/packages/contracts/src/daemon-resource-request.test.ts +1 -1
  12. package/packages/contracts/src/daemon-wire.ts +10 -5
  13. package/packages/contracts/src/fleet-client-state.ts +3 -0
  14. package/packages/contracts/src/fleet-lifecycle.ts +5 -0
  15. package/packages/contracts/src/index.ts +2 -0
  16. package/packages/contracts/src/interaction-receipts.ts +39 -0
  17. package/packages/contracts/src/issue-error.ts +1 -1
  18. package/packages/contracts/src/multiplexer-verbs.ts +25 -1
  19. package/packages/contracts/src/pane-stream.ts +51 -8
  20. package/packages/contracts/src/tmux-server-scope.ts +172 -0
  21. package/packages/contracts/src/window-links.ts +76 -0
  22. package/packages/contracts/src/workspace-multiplexer.ts +54 -0
  23. package/packages/core/src/interaction-receipts.ts +4 -0
  24. package/packages/daemon/dist/command-center/actions/command-definitions.js +6 -0
  25. package/packages/daemon/dist/command-center/actions/handlers/workspace-multiplexer.js +2 -0
  26. package/packages/daemon/dist/command-center/actions/registry.js +13 -1
  27. package/packages/daemon/dist/command-center/discovery.js +2 -9
  28. package/packages/daemon/dist/command-center/server.js +21 -1
  29. package/packages/daemon/dist/command-center/tmux-server-native-backing.js +37 -0
  30. package/packages/daemon/dist/command-center/tmux-servers.js +351 -0
  31. package/packages/daemon/dist/lib/canonical-daemon-bootstrap.js +64 -8
  32. package/packages/daemon/dist/lib/canonical-daemon.js +5 -2
  33. package/packages/daemon/dist/lib/daemon-embed.js +198 -30
  34. package/packages/daemon/dist/lib/embedded-tmux-server-owners.js +147 -0
  35. package/packages/daemon/dist/lib/fleet-lifecycle-authority.js +31 -11
  36. package/packages/daemon/dist/lib/headless-daemon.js +10 -1
  37. package/packages/daemon/dist/lib/runtime-namespace.js +17 -3
  38. package/packages/daemon/dist/lib/tmux-client-execution.js +59 -0
  39. package/packages/daemon/dist/lib/tmux-named-socket-fence.js +4 -0
  40. package/packages/daemon/dist/lib/tmux-server-generation-runner.js +62 -0
  41. package/packages/daemon/dist/lib/tmux-server-owner.js +285 -0
  42. package/packages/daemon/dist/lib/tmux-server-owners.js +192 -0
  43. package/packages/daemon/dist/lib/tmux-server-proof.js +82 -0
  44. package/packages/daemon/dist/lib/tmux-server-registration.js +85 -0
  45. package/packages/daemon/dist/lib/tmux-server-session-create.js +43 -0
  46. package/packages/daemon/dist/lib/tmux-server-session-open.js +101 -0
  47. package/packages/daemon/dist/lib/tmux-servers-cli.js +134 -0
  48. package/packages/daemon/dist/lib/tmux-session-mutation-fence.js +55 -0
  49. package/packages/daemon/dist/lib/tmux-window-link-guard.js +88 -0
  50. package/packages/daemon/dist/lib/workspace-config-loader.js +3 -0
  51. package/packages/daemon/dist/lib/workspace-multiplexer-verbs.js +82 -1
  52. package/packages/daemon/dist/lib/workspace-pane-creation.js +13 -57
  53. package/packages/daemon/dist/lib/workspace-promotion.js +91 -12
  54. package/packages/daemon/dist/lib/workspace-registry.js +4 -0
  55. package/packages/daemon/dist/server/pane-stream-upgrade.js +20 -6
  56. package/packages/daemon/dist/terminal/attachments/native-runtime.js +61 -17
  57. package/packages/daemon/dist/terminal/mirror/__tests__/scripted-channel.js +9 -1
  58. package/packages/daemon/dist/terminal/mirror/__tests__/simulated-channel.js +11 -2
  59. package/packages/daemon/dist/terminal/mirror/control-channel.js +65 -6
  60. package/packages/daemon/dist/terminal/mirror/mirror-service.js +44 -0
  61. package/packages/daemon/dist/terminal/mirror/session-channel.js +210 -50
  62. package/packages/daemon/dist/terminal/mirror/window-link-authority.js +160 -0
  63. package/packages/daemon/dist/terminal/pane-stream/lease-manager.js +3 -3
  64. package/packages/daemon/dist/terminal/pane-stream/pane-stream-websocket.js +40 -6
  65. package/packages/daemon/dist/terminal/protocol/live-session-identity.js +9 -0
  66. package/packages/daemon/dist/terminal/protocol/native-backing-client.js +2 -1
  67. package/packages/daemon/dist/terminal/session-runtime/interaction-receipt-facts.js +9 -0
  68. package/packages/daemon/dist/terminal/session-runtime/registry.js +6 -0
  69. package/packages/daemon/dist/terminal/session-runtime/semantic-mutation-executor.js +1 -1
  70. package/packages/daemon/dist/terminal/session-runtime/semantic-mutation-resource-changes.js +1 -0
  71. package/packages/daemon/dist/tui/mirror/application-shell-daemon-connection.js +7 -2
  72. package/packages/daemon/dist/tui/mirror/application-shell-server-connection.js +114 -0
  73. package/packages/daemon/dist/tui/mirror/menu-model.js +1 -1
  74. package/packages/daemon/dist/tui/mirror/open-tui-verified-routing.js +34 -4
  75. package/packages/daemon/dist/tui/mirror/open-tui-workspace-runtime-port.js +29 -8
  76. package/packages/daemon/dist/tui/mirror/runtime/application-guided-tour-integration.jsx +119 -0
  77. package/packages/daemon/dist/tui/mirror/runtime/application-guided-tour-owner.js +132 -0
  78. package/packages/daemon/dist/tui/mirror/runtime/application-home-agent-roster.jsx +21 -9
  79. package/packages/daemon/dist/tui/mirror/runtime/application-home-agent-transport.js +63 -1
  80. package/packages/daemon/dist/tui/mirror/runtime/application-home-agents-owner.js +8 -2
  81. package/packages/daemon/dist/tui/mirror/runtime/application-home-agents.js +3 -1
  82. package/packages/daemon/dist/tui/mirror/runtime/application-home-catalog-owner.js +11 -5
  83. package/packages/daemon/dist/tui/mirror/runtime/application-home-catalog.js +140 -75
  84. package/packages/daemon/dist/tui/mirror/runtime/application-home-experience.js +40 -0
  85. package/packages/daemon/dist/tui/mirror/runtime/application-home-fleet.js +132 -0
  86. package/packages/daemon/dist/tui/mirror/runtime/application-machine-agents.js +9 -0
  87. package/packages/daemon/dist/tui/mirror/runtime/application-machine-catalog.js +12 -2
  88. package/packages/daemon/dist/tui/mirror/runtime/application-machine-navigation.js +211 -31
  89. package/packages/daemon/dist/tui/mirror/runtime/application-machine-sidebar.jsx +91 -37
  90. package/packages/daemon/dist/tui/mirror/runtime/application-performance-log.js +20 -7
  91. package/packages/daemon/dist/tui/mirror/runtime/application-root-configuration.js +9 -3
  92. package/packages/daemon/dist/tui/mirror/runtime/application-route-connection.js +95 -2
  93. package/packages/daemon/dist/tui/mirror/runtime/application-shell-home.jsx +6 -2
  94. package/packages/daemon/dist/tui/mirror/runtime/application-terminal-interaction-controller.js +158 -2
  95. package/packages/daemon/dist/tui/mirror/runtime/application-terminal-workspace-policy.js +43 -0
  96. package/packages/daemon/dist/tui/mirror/runtime/application-terminal-workspace.jsx +78 -25
  97. package/packages/daemon/dist/tui/mirror/runtime/application-theme-repaint.js +9 -0
  98. package/packages/daemon/dist/tui/mirror/runtime/fleet-lifecycle-client.js +44 -1
  99. package/packages/daemon/dist/tui/mirror/runtime/guided-tour-coach.jsx +54 -0
  100. package/packages/daemon/dist/tui/mirror/runtime/guided-tour-storage.js +67 -0
  101. package/packages/daemon/dist/tui/mirror/runtime/guided-tour.js +113 -0
  102. package/packages/daemon/dist/tui/mirror/runtime/open-tui-generation-host.js +1 -1
  103. package/packages/daemon/dist/tui/mirror/runtime/resize-terminal-pane.js +4 -0
  104. package/packages/daemon/dist/tui/mirror/runtime/select-terminal-pane.js +2 -1
  105. package/packages/daemon/dist/tui/mirror/runtime/semantic-shell-viewport-resize.js +12 -4
  106. package/packages/daemon/dist/tui/mirror/runtime/terminal-pane-input-router.js +6 -3
  107. package/packages/daemon/dist/tui/mirror/workspace/application-command-description.js +11 -2
  108. package/packages/daemon/dist/tui/mirror/workspace/terminal-window-strip.jsx +2 -1
  109. package/packages/daemon/src/command-center/actions/command-definitions.ts +6 -0
  110. package/packages/daemon/src/command-center/actions/errors.ts +4 -0
  111. package/packages/daemon/src/command-center/actions/handlers/workspace-multiplexer.ts +2 -0
  112. package/packages/daemon/src/command-center/actions/registry.ts +14 -0
  113. package/packages/daemon/src/command-center/discovery.ts +2 -10
  114. package/packages/daemon/src/command-center/server.ts +29 -1
  115. package/packages/daemon/src/command-center/tmux-server-native-backing.ts +49 -0
  116. package/packages/daemon/src/command-center/tmux-servers.ts +479 -0
  117. package/packages/daemon/src/lib/canonical-daemon-bootstrap.ts +111 -4
  118. package/packages/daemon/src/lib/canonical-daemon.ts +15 -3
  119. package/packages/daemon/src/lib/daemon-embed.ts +241 -28
  120. package/packages/daemon/src/lib/embedded-tmux-server-owners.ts +190 -0
  121. package/packages/daemon/src/lib/fleet-lifecycle-authority.ts +33 -9
  122. package/packages/daemon/src/lib/headless-daemon.ts +18 -1
  123. package/packages/daemon/src/lib/runtime-namespace.ts +24 -3
  124. package/packages/daemon/src/lib/tmux-client-execution.ts +56 -0
  125. package/packages/daemon/src/lib/tmux-named-socket-fence.ts +5 -0
  126. package/packages/daemon/src/lib/tmux-server-generation-runner.ts +81 -0
  127. package/packages/daemon/src/lib/tmux-server-owner.ts +336 -0
  128. package/packages/daemon/src/lib/tmux-server-owners.ts +228 -0
  129. package/packages/daemon/src/lib/tmux-server-proof.ts +95 -0
  130. package/packages/daemon/src/lib/tmux-server-registration.ts +105 -0
  131. package/packages/daemon/src/lib/tmux-server-session-create.ts +52 -0
  132. package/packages/daemon/src/lib/tmux-server-session-open.ts +106 -0
  133. package/packages/daemon/src/lib/tmux-servers-cli.ts +186 -0
  134. package/packages/daemon/src/lib/tmux-session-mutation-fence.ts +62 -0
  135. package/packages/daemon/src/lib/tmux-window-link-guard.ts +112 -0
  136. package/packages/daemon/src/lib/workspace-config-loader.ts +3 -0
  137. package/packages/daemon/src/lib/workspace-multiplexer-verbs.ts +121 -1
  138. package/packages/daemon/src/lib/workspace-pane-creation.ts +16 -55
  139. package/packages/daemon/src/lib/workspace-promotion.ts +98 -12
  140. package/packages/daemon/src/lib/workspace-registry.ts +5 -0
  141. package/packages/daemon/src/server/pane-stream-upgrade.ts +25 -5
  142. package/packages/daemon/src/terminal/attachments/native-runtime.ts +70 -16
  143. package/packages/daemon/src/terminal/mirror/control-channel.ts +64 -7
  144. package/packages/daemon/src/terminal/mirror/events.ts +2 -0
  145. package/packages/daemon/src/terminal/mirror/mirror-service.ts +56 -0
  146. package/packages/daemon/src/terminal/mirror/session-channel.ts +245 -50
  147. package/packages/daemon/src/terminal/mirror/window-link-authority.ts +203 -0
  148. package/packages/daemon/src/terminal/pane-stream/lease-manager.ts +3 -3
  149. package/packages/daemon/src/terminal/pane-stream/pane-stream-websocket.ts +50 -6
  150. package/packages/daemon/src/terminal/protocol/live-session-identity.ts +14 -0
  151. package/packages/daemon/src/terminal/protocol/native-backing-client.ts +4 -1
  152. package/packages/daemon/src/terminal/session-runtime/interaction-receipt-facts.ts +9 -0
  153. package/packages/daemon/src/terminal/session-runtime/registry.ts +9 -0
  154. package/packages/daemon/src/terminal/session-runtime/semantic-mutation-executor.ts +2 -2
  155. package/packages/daemon/src/terminal/session-runtime/semantic-mutation-resource-changes.ts +1 -0
  156. package/packages/daemon/src/tui/mirror/application-shell-daemon-connection.ts +28 -2
  157. package/packages/daemon/src/tui/mirror/application-shell-server-connection.ts +154 -0
  158. package/packages/daemon/src/tui/mirror/menu-model.ts +1 -1
  159. package/packages/daemon/src/tui/mirror/open-tui-verified-routing.ts +41 -3
  160. package/packages/daemon/src/tui/mirror/open-tui-workspace-runtime-port.ts +47 -6
  161. package/packages/daemon/src/tui/mirror/runtime/application-entry.ts +2 -1
  162. package/packages/daemon/src/tui/mirror/runtime/application-fleet-session-actions.tsx +10 -4
  163. package/packages/daemon/src/tui/mirror/runtime/application-fleet-tabs.ts +2 -0
  164. package/packages/daemon/src/tui/mirror/runtime/application-guided-tour-integration.tsx +199 -0
  165. package/packages/daemon/src/tui/mirror/runtime/application-guided-tour-owner.ts +162 -0
  166. package/packages/daemon/src/tui/mirror/runtime/application-home-agent-roster.tsx +36 -10
  167. package/packages/daemon/src/tui/mirror/runtime/application-home-agent-transport.ts +69 -0
  168. package/packages/daemon/src/tui/mirror/runtime/application-home-agents-owner.ts +12 -2
  169. package/packages/daemon/src/tui/mirror/runtime/application-home-agents.ts +13 -2
  170. package/packages/daemon/src/tui/mirror/runtime/application-home-catalog-owner.ts +13 -3
  171. package/packages/daemon/src/tui/mirror/runtime/application-home-catalog.ts +172 -100
  172. package/packages/daemon/src/tui/mirror/runtime/application-home-experience.ts +59 -0
  173. package/packages/daemon/src/tui/mirror/runtime/application-home-fleet.ts +175 -0
  174. package/packages/daemon/src/tui/mirror/runtime/application-machine-agents.ts +9 -1
  175. package/packages/daemon/src/tui/mirror/runtime/application-machine-catalog.ts +19 -7
  176. package/packages/daemon/src/tui/mirror/runtime/application-machine-navigation.ts +285 -36
  177. package/packages/daemon/src/tui/mirror/runtime/application-machine-overlays.tsx +1 -1
  178. package/packages/daemon/src/tui/mirror/runtime/application-machine-sidebar.tsx +124 -40
  179. package/packages/daemon/src/tui/mirror/runtime/application-palette-preview.tsx +3 -0
  180. package/packages/daemon/src/tui/mirror/runtime/application-performance-log.ts +18 -6
  181. package/packages/daemon/src/tui/mirror/runtime/application-root-configuration.ts +12 -3
  182. package/packages/daemon/src/tui/mirror/runtime/application-root-v2.tsx +40 -40
  183. package/packages/daemon/src/tui/mirror/runtime/application-route-connection.ts +93 -1
  184. package/packages/daemon/src/tui/mirror/runtime/application-shell-catalog.tsx +4 -0
  185. package/packages/daemon/src/tui/mirror/runtime/application-shell-home.tsx +20 -1
  186. package/packages/daemon/src/tui/mirror/runtime/application-shell-view.tsx +10 -0
  187. package/packages/daemon/src/tui/mirror/runtime/application-terminal-interaction-controller.ts +177 -2
  188. package/packages/daemon/src/tui/mirror/runtime/application-terminal-workspace-policy.ts +62 -0
  189. package/packages/daemon/src/tui/mirror/runtime/application-terminal-workspace.tsx +87 -30
  190. package/packages/daemon/src/tui/mirror/runtime/application-theme-repaint.ts +19 -0
  191. package/packages/daemon/src/tui/mirror/runtime/fleet-lifecycle-client.ts +55 -0
  192. package/packages/daemon/src/tui/mirror/runtime/guided-tour-coach.tsx +118 -0
  193. package/packages/daemon/src/tui/mirror/runtime/guided-tour-storage.ts +72 -0
  194. package/packages/daemon/src/tui/mirror/runtime/guided-tour.ts +147 -0
  195. package/packages/daemon/src/tui/mirror/runtime/open-tui-generation-host.ts +1 -1
  196. package/packages/daemon/src/tui/mirror/runtime/resize-terminal-pane.ts +3 -0
  197. package/packages/daemon/src/tui/mirror/runtime/select-terminal-pane.ts +3 -0
  198. package/packages/daemon/src/tui/mirror/runtime/semantic-shell-viewport-resize.ts +11 -4
  199. package/packages/daemon/src/tui/mirror/runtime/terminal-pane-input-router.ts +15 -4
  200. package/packages/daemon/src/tui/mirror/workspace/application-command-description.ts +13 -2
  201. package/packages/daemon/src/tui/mirror/workspace/terminal-window-strip.tsx +5 -1
  202. package/packages/daemon-client/package.json +3 -1
  203. package/packages/daemon-client/src/index.ts +2 -0
  204. package/packages/daemon-client/src/pane-stream-client.test.ts +108 -38
  205. package/packages/daemon-client/src/pane-stream-client.ts +26 -0
  206. package/packages/daemon-client/src/scoped-tmux-server-transport.test.ts +114 -0
  207. package/packages/daemon-client/src/scoped-tmux-server-transport.ts +385 -0
  208. package/packages/daemon-client/src/tmux-server-client.test.ts +279 -0
  209. package/packages/daemon-client/src/tmux-server-client.ts +287 -0
@@ -0,0 +1,190 @@
1
+ import type { Server, IncomingMessage } from "node:http";
2
+ import type { Socket } from "node:net";
3
+ import { randomUUID } from "node:crypto";
4
+ import { join } from "node:path";
5
+ import { tmuxServerPaneStreamPath } from "@tmux-ide/contracts";
6
+ import {
7
+ TmuxServerOwners,
8
+ MAX_TMUX_SERVER_OWNERS,
9
+ type TmuxServerRegistration,
10
+ } from "./tmux-server-owners.ts";
11
+ import {
12
+ createTmuxServerProbe,
13
+ readTmuxServerRegistrations,
14
+ writeTmuxServerRegistrations,
15
+ } from "./tmux-server-registration.ts";
16
+ import { createNativeTmuxServerOwner, type NativeTmuxServerOwner } from "./tmux-server-owner.ts";
17
+ import type { WorkspacePaneTmuxAuthority } from "./workspace-pane-creation.ts";
18
+ import {
19
+ attachPaneStreamWebSocket,
20
+ type PaneStreamWebSocketBoundary,
21
+ } from "../server/pane-stream-upgrade.ts";
22
+
23
+ /** Adapts the established default owner without duplicating its control clients. */
24
+ export async function createEmbeddedTmuxServerOwners(options: {
25
+ readonly defaultAuthority: WorkspacePaneTmuxAuthority;
26
+ readonly defaultGeneration: string;
27
+ readonly expectedDefaultProofDigest: string | null;
28
+ readonly defaultOwner: Omit<NativeTmuxServerOwner, "serverId" | "generation">;
29
+ readonly stateDirectory: string;
30
+ readonly webSocketBaseUrl: string;
31
+ }) {
32
+ const path = join(options.stateDirectory, "tmux-servers.json");
33
+ const persisted = readTmuxServerRegistrations(path);
34
+ const probe = createTmuxServerProbe(options.defaultAuthority.executablePath);
35
+ const defaultObservation = await probe(options.defaultAuthority.socketSelector);
36
+ let established = persisted.find(
37
+ (entry) =>
38
+ JSON.stringify(entry.selector) === JSON.stringify(options.defaultAuthority.socketSelector),
39
+ );
40
+ if (!established && defaultObservation) {
41
+ for (const entry of persisted) {
42
+ const observation = await probe(entry.selector);
43
+ if (observation?.fingerprint === defaultObservation.fingerprint) {
44
+ established = entry;
45
+ break;
46
+ }
47
+ }
48
+ }
49
+ const defaultRegistration: TmuxServerRegistration = established ?? {
50
+ serverId: `tmux-server.${randomUUID().replaceAll("-", "")}`,
51
+ label: "Default",
52
+ selector: options.defaultAuthority.socketSelector,
53
+ };
54
+ let httpServer: Server | null = null;
55
+ const boundaries = new Map<string, PaneStreamWebSocketBoundary>();
56
+ const pending = new Map<string, NativeTmuxServerOwner>();
57
+ const isCurrent = (owner: NativeTmuxServerOwner): boolean => {
58
+ try {
59
+ return (
60
+ owners.current({ serverId: owner.serverId, generation: owner.generation })
61
+ .paneStreamRuntime === owner.paneStreamRuntime
62
+ );
63
+ } catch {
64
+ return false;
65
+ }
66
+ };
67
+ const attachOwner = (route: string, owner: NativeTmuxServerOwner) => {
68
+ pending.set(route, owner);
69
+ if (httpServer)
70
+ boundaries.set(
71
+ route,
72
+ attachPaneStreamWebSocket(httpServer, owner.paneStreamRuntime.coordinator, route, () =>
73
+ isCurrent(owner),
74
+ ),
75
+ );
76
+ };
77
+ let defaultRetired = false;
78
+ const retireDefault = async () => {
79
+ if (defaultRetired) return;
80
+ defaultRetired = true;
81
+ await options.defaultOwner.dispose();
82
+ };
83
+ const owners = new TmuxServerOwners<NativeTmuxServerOwner>({
84
+ probe,
85
+ persist: (registrations) => writeTmuxServerRegistrations(path, registrations),
86
+ create: async (registration, scope, observation) => {
87
+ if (registration.serverId === defaultRegistration.serverId) await retireDefault();
88
+ const route = tmuxServerPaneStreamPath(scope);
89
+ const owner = await createNativeTmuxServerOwner({
90
+ ...scope,
91
+ tmuxAuthority: observation.authority,
92
+ nativeServerIdentity: observation.nativeServerIdentity,
93
+ stateDirectory: join(options.stateDirectory, "server-owners", registration.serverId),
94
+ webSocketUrl: new URL(route, options.webSocketBaseUrl).href,
95
+ });
96
+ attachOwner(route, owner);
97
+ return {
98
+ ...owner,
99
+ dispose: async () => {
100
+ const boundary = boundaries.get(route);
101
+ boundaries.delete(route);
102
+ pending.delete(route);
103
+ try {
104
+ await boundary?.close();
105
+ } finally {
106
+ await owner.dispose();
107
+ }
108
+ },
109
+ };
110
+ },
111
+ });
112
+ if (defaultObservation && defaultObservation.fingerprint === options.expectedDefaultProofDigest)
113
+ owners.adopt(
114
+ defaultRegistration,
115
+ { serverId: defaultRegistration.serverId, generation: options.defaultGeneration },
116
+ defaultObservation,
117
+ {
118
+ ...options.defaultOwner,
119
+ serverId: defaultRegistration.serverId,
120
+ generation: options.defaultGeneration,
121
+ dispose: retireDefault,
122
+ },
123
+ );
124
+ try {
125
+ if (
126
+ !defaultObservation ||
127
+ defaultObservation.fingerprint !== options.expectedDefaultProofDigest
128
+ )
129
+ await owners.register(defaultRegistration);
130
+ for (const registration of persisted) {
131
+ if (registration.serverId !== defaultRegistration.serverId)
132
+ await owners.register(registration);
133
+ }
134
+ writeTmuxServerRegistrations(path, owners.registrations());
135
+ } catch (error) {
136
+ await owners.dispose();
137
+ throw error;
138
+ }
139
+ const refuseUnknownScope = (request: IncomingMessage, socket: Socket) => {
140
+ const route = (request.url ?? "").split("?", 1)[0]!;
141
+ if (route.startsWith("/v2/tmux-servers/") && !pending.has(route))
142
+ socket.end("HTTP/1.1 410 Gone\r\nConnection: close\r\nContent-Length: 0\r\n\r\n");
143
+ };
144
+ return {
145
+ owners,
146
+ isDefaultCurrent() {
147
+ if (defaultRetired) return false;
148
+ if (
149
+ !defaultObservation ||
150
+ defaultObservation.fingerprint !== options.expectedDefaultProofDigest
151
+ )
152
+ return true;
153
+ try {
154
+ owners.current({
155
+ serverId: defaultRegistration.serverId,
156
+ generation: options.defaultGeneration,
157
+ });
158
+ return true;
159
+ } catch {
160
+ return false;
161
+ }
162
+ },
163
+ attach(server: Server) {
164
+ if (httpServer) throw new Error("Server owner transport already attached");
165
+ httpServer = server;
166
+ server.on("upgrade", refuseUnknownScope);
167
+ server.setMaxListeners(
168
+ Math.max(
169
+ server.getMaxListeners(),
170
+ MAX_TMUX_SERVER_OWNERS + server.listenerCount("upgrade") + 4,
171
+ ),
172
+ );
173
+ for (const [route, owner] of pending)
174
+ boundaries.set(
175
+ route,
176
+ attachPaneStreamWebSocket(server, owner.paneStreamRuntime.coordinator, route, () =>
177
+ isCurrent(owner),
178
+ ),
179
+ );
180
+ },
181
+ async dispose() {
182
+ httpServer?.off("upgrade", refuseUnknownScope);
183
+ await owners.dispose();
184
+ },
185
+ /** Legacy default routes remain bound to their original authority; never retarget. */
186
+ get defaultRetired() {
187
+ return defaultRetired;
188
+ },
189
+ };
190
+ }
@@ -1,3 +1,4 @@
1
+ import { liveSessionIdForNativeIdentity } from "../terminal/protocol/live-session-identity.ts";
1
2
  import { createHash, randomUUID } from "node:crypto";
2
3
  import { isAbsolute, resolve } from "node:path";
3
4
  import { realpath, stat } from "node:fs/promises";
@@ -56,6 +57,7 @@ export class FleetLifecycleAuthority {
56
57
  readonly #startedAt: string;
57
58
  readonly #registry: Pick<WorkspaceRegistry, "list" | "add">;
58
59
  readonly #runTmux: (args: readonly string[]) => string;
60
+ readonly #ensureChromeUpdater: boolean;
59
61
  readonly #readFleet: () => FleetSessionFacts[] | null;
60
62
  readonly #operations = new Map<string, { fingerprint: string; result: unknown }>();
61
63
  #tail: Promise<void> = Promise.resolve();
@@ -67,11 +69,14 @@ export class FleetLifecycleAuthority {
67
69
  registry: Pick<WorkspaceRegistry, "list" | "add">;
68
70
  runTmux: (args: readonly string[]) => string;
69
71
  readFleet?: () => FleetSessionFacts[] | null;
72
+ /** Independent visual owners do not need an additional chrome updater process. */
73
+ ensureChromeUpdater?: boolean;
70
74
  }) {
71
75
  this.#daemonInstanceId = options.daemonInstanceId;
72
76
  this.#productVersion = options.productVersion;
73
77
  this.#startedAt = options.startedAt;
74
78
  this.#registry = options.registry;
79
+ this.#ensureChromeUpdater = options.ensureChromeUpdater ?? true;
75
80
  this.#runTmux = options.runTmux;
76
81
  this.#readFleet = options.readFleet ?? (() => readAdoptedFleet(this.#registry));
77
82
  }
@@ -135,10 +140,14 @@ export class FleetLifecycleAuthority {
135
140
  }
136
141
  }
137
142
  let created = false;
143
+ let liveSessionId: string | undefined;
138
144
  try {
139
- this.#runTmux([
145
+ const createdId = this.#runTmux([
140
146
  "new-session",
141
147
  "-d",
148
+ "-P",
149
+ "-F",
150
+ "#{pid}\t#{session_id}\t#{session_created}",
142
151
  ...TMUX_TRUECOLOR_ENVIRONMENT_ARGS,
143
152
  "-s",
144
153
  identity.sessionName,
@@ -148,14 +157,26 @@ export class FleetLifecycleAuthority {
148
157
  cwd,
149
158
  TMUX_TRUECOLOR_INTERACTIVE_SHELL_COMMAND,
150
159
  ]);
160
+ const [serverPid, sessionId, sessionCreated] = createdId.trim().split("\t");
161
+ if (
162
+ serverPid &&
163
+ /^\d+$/u.test(serverPid) &&
164
+ sessionId &&
165
+ /^\$\d+$/u.test(sessionId) &&
166
+ sessionCreated &&
167
+ /^\d+$/u.test(sessionCreated)
168
+ )
169
+ liveSessionId = liveSessionIdForNativeIdentity(serverPid, sessionId, sessionCreated);
151
170
  created = true;
152
171
  prepareTmuxTruecolorEnvironment(this.#runTmux, identity.sessionName);
153
172
  this.#runTmux(["set-environment", "-t", identity.sessionName, "TMUX_IDE", "1"]);
154
173
  this.#runTmux(adoptMarkArgv(identity.sessionName));
155
- try {
156
- this.#runTmux(updaterProbeArgv());
157
- } catch {
158
- this.#runTmux(updaterSpawnArgv());
174
+ if (this.#ensureChromeUpdater) {
175
+ try {
176
+ this.#runTmux(updaterProbeArgv());
177
+ } catch {
178
+ this.#runTmux(updaterSpawnArgv());
179
+ }
159
180
  }
160
181
  if (!existing)
161
182
  this.#registry.add({
@@ -180,6 +201,7 @@ export class FleetLifecycleAuthority {
180
201
  operationId,
181
202
  daemonInstanceId: this.#daemonInstanceId,
182
203
  outcome: "created",
204
+ ...(input.includeLiveSessionId && liveSessionId ? { liveSessionId } : {}),
183
205
  fleetSessionId: fleetSessionIdForName(identity.sessionName),
184
206
  workspaceName: identity.workspaceName,
185
207
  displayName: input.displayName,
@@ -368,10 +390,12 @@ export class FleetLifecycleAuthority {
368
390
  this.#runTmux(["set-option", "-p", "-t", paneId, "@agent_launch", input.command]);
369
391
  this.#runTmux(["set-option", "-p", "-t", paneId, "@agent_hint", input.harness]);
370
392
  this.#runTmux(["select-pane", "-t", paneId, "-T", input.displayTitle]);
371
- try {
372
- this.#runTmux(updaterProbeArgv());
373
- } catch {
374
- this.#runTmux(updaterSpawnArgv());
393
+ if (this.#ensureChromeUpdater) {
394
+ try {
395
+ this.#runTmux(updaterProbeArgv());
396
+ } catch {
397
+ this.#runTmux(updaterSpawnArgv());
398
+ }
375
399
  }
376
400
  } catch (error) {
377
401
  if (createdSession) this.#tryTmux(["kill-session", "-t", sessionName]);
@@ -1,3 +1,5 @@
1
+ import { assertCanonicalDaemonServerIntent } from "./canonical-daemon-bootstrap.ts";
2
+ import { resolveTmuxServerIntent } from "./runtime-namespace.ts";
1
3
  import {
2
4
  canonicalDaemonUrl,
3
5
  matchesCanonicalDaemonPredecessor,
@@ -28,6 +30,7 @@ import { generateAuthToken } from "./auth-token.ts";
28
30
  import type { DaemonRestartRequest } from "./daemon-restart-request.ts";
29
31
 
30
32
  export interface HeadlessDaemonOptions {
33
+ readonly tmuxServerIntent?: ReturnType<typeof resolveTmuxServerIntent>;
31
34
  readonly supervisionId?: string;
32
35
  /** Managed launch receipt, only after this process wins election and is attachable. */
33
36
  readonly onOwnedReady?: (info: CanonicalDaemonInfo) => void;
@@ -48,6 +51,8 @@ export interface HeadlessDaemonDependencies {
48
51
  readonly probeCanonicalDaemonHealth: (info: CanonicalDaemonInfo) => Promise<DaemonHealth | null>;
49
52
  readonly probeCanonicalDaemonIdentity: (
50
53
  info: CanonicalDaemonInfo,
54
+ signal?: AbortSignal,
55
+ includeTmuxServerProof?: boolean,
51
56
  ) => Promise<DaemonIdentity | null>;
52
57
  readonly startEmbeddedDaemon: (opts: EmbeddedDaemonOptions) => Promise<EmbeddedDaemonHandle>;
53
58
  readonly writeStdout: (line: string) => void;
@@ -140,7 +145,11 @@ async function assertAttachableDaemon(
140
145
  info: CanonicalDaemonInfo,
141
146
  options: HeadlessDaemonOptions,
142
147
  ): Promise<void> {
143
- const identity = await deps.probeCanonicalDaemonIdentity(info);
148
+ const identity = await deps.probeCanonicalDaemonIdentity(
149
+ info,
150
+ undefined,
151
+ Boolean(options.tmuxServerIntent),
152
+ );
144
153
  if (!identity) {
145
154
  throw new IdeError(
146
155
  `Canonical daemon PID ${info.pid} is alive but its identity endpoint is unavailable. ` +
@@ -149,6 +158,9 @@ async function assertAttachableDaemon(
149
158
  );
150
159
  }
151
160
  assertIdentityMatches(info, identity);
161
+ await assertCanonicalDaemonServerIntent(info, options.tmuxServerIntent ?? null, {
162
+ identity: async () => identity,
163
+ });
152
164
  const health = await deps.probeCanonicalDaemonHealth(info);
153
165
  if (!health) {
154
166
  throw new IdeError(
@@ -288,6 +300,11 @@ export async function runHeadlessDaemon(
288
300
  options: HeadlessDaemonOptions = {},
289
301
  deps: HeadlessDaemonDependencies = defaultDependencies,
290
302
  ): Promise<"stopped" | "already-running"> {
303
+ options = {
304
+ ...options,
305
+ tmuxServerIntent:
306
+ options.tmuxServerIntent === undefined ? resolveTmuxServerIntent() : options.tmuxServerIntent,
307
+ };
291
308
  let restoreTmuxWorkspaces = false;
292
309
  const lifecycle: { restart?: DaemonRestartRequest; predecessor?: CanonicalDaemonPredecessor } =
293
310
  {};
@@ -44,6 +44,7 @@ export interface RuntimeNamespace {
44
44
  readonly daemonInfoDir: string;
45
45
  readonly controlSocketPath: string;
46
46
  readonly eventLogPath: string;
47
+ readonly tmuxSocketExplicit?: boolean;
47
48
  readonly tmuxSocket:
48
49
  | { readonly kind: "name"; readonly name: string }
49
50
  | { readonly kind: "path"; readonly path: string };
@@ -287,6 +288,7 @@ export function resolveRuntimeNamespace(
287
288
  controlSocketPath: join(runtimeDir, "control.sock"),
288
289
  eventLogPath: join(stateHome, "events.jsonl"),
289
290
  tmuxSocket,
291
+ tmuxSocketExplicit: Boolean(tmuxSocketName || tmuxSocketPath),
290
292
  cleanupToken,
291
293
  namespaceId: development?.id ?? (isolated ? cleanupToken! : "canonical"),
292
294
  persistence: isolated && !development ? "ephemeral" : "durable",
@@ -316,9 +318,11 @@ export function runtimeNamespaceEnvironment(
316
318
  [STATE_HOME_ENV]: namespace.stateHome,
317
319
  [REGISTRY_DIR_ENV]: namespace.registryDir,
318
320
  [DAEMON_INFO_DIR_ENV]: namespace.daemonInfoDir,
319
- ...(namespace.tmuxSocket.kind === "name"
320
- ? { [TMUX_SOCKET_NAME_ENV]: namespace.tmuxSocket.name }
321
- : { [TMUX_SOCKET_PATH_ENV]: namespace.tmuxSocket.path }),
321
+ ...(namespace.tmuxSocketExplicit === false
322
+ ? {}
323
+ : namespace.tmuxSocket.kind === "name"
324
+ ? { [TMUX_SOCKET_NAME_ENV]: namespace.tmuxSocket.name }
325
+ : { [TMUX_SOCKET_PATH_ENV]: namespace.tmuxSocket.path }),
322
326
  ...(namespace.cleanupToken ? { [CLEANUP_TOKEN_ENV]: namespace.cleanupToken } : {}),
323
327
  });
324
328
  }
@@ -388,3 +392,20 @@ export function assertQualifiedDevelopmentLaunch(): void {
388
392
  "Development launch requires exact build artifacts (D03); installed CLI fallback is disabled",
389
393
  );
390
394
  }
395
+
396
+ /** Caller intent differs from the default used when starting a new owner. */
397
+ export function resolveTmuxServerIntent(options: RuntimeNamespaceResolutionOptions = {}): {
398
+ readonly source: "explicit" | "current";
399
+ readonly selector: RuntimeNamespace["tmuxSocket"];
400
+ } | null {
401
+ const env = options.env ?? process.env;
402
+ const namespace = resolveRuntimeNamespace(options);
403
+ if (
404
+ namespace.isolated ||
405
+ nonEmpty(env, TMUX_SOCKET_NAME_ENV) ||
406
+ nonEmpty(env, TMUX_SOCKET_PATH_ENV)
407
+ )
408
+ return { source: "explicit", selector: namespace.tmuxSocket };
409
+ const current = /^(.*),[0-9]+,[0-9]+$/u.exec(env.TMUX ?? "")?.[1];
410
+ return current ? { source: "current", selector: { kind: "path", path: current } } : null;
411
+ }
@@ -0,0 +1,56 @@
1
+ import { accessSync, constants, realpathSync, statSync } from "node:fs";
2
+ import { delimiter, isAbsolute, join } from "node:path";
3
+ import { resolveRuntimeNamespace } from "./runtime-namespace.ts";
4
+ import { resolveBundledTmux } from "./bundled-tmux.ts";
5
+
6
+ export function resolveTmuxExecutable(): string {
7
+ if (resolveRuntimeNamespace().development) return resolveBundledTmux()!;
8
+ const configured = process.env.TMUX_IDE_TMUX_BIN;
9
+ if (!configured) {
10
+ const bundled = resolveBundledTmux();
11
+ if (bundled) return bundled;
12
+ }
13
+ const candidates = configured
14
+ ? [configured]
15
+ : (process.env.PATH ?? "")
16
+ .split(delimiter)
17
+ // Empty and relative PATH entries mean the daemon/project cwd. They
18
+ // are never executable authority for a privileged tmux mutation.
19
+ .filter((entry) => entry.length > 0 && isAbsolute(entry))
20
+ .map((entry) => join(entry, "tmux"));
21
+ for (const candidate of candidates) {
22
+ try {
23
+ if (!isAbsolute(candidate)) continue;
24
+ accessSync(candidate, constants.X_OK);
25
+ const canonical = realpathSync(candidate);
26
+ if (statSync(canonical).isFile()) return canonical;
27
+ } catch {
28
+ // Continue to the next daemon-start candidate.
29
+ }
30
+ }
31
+ throw new Error("tmux_executable_unavailable");
32
+ }
33
+
34
+ const SAFE_TERMINAL_VALUE = /^(?:xterm|screen|tmux|rxvt|vt100|ansi)[A-Za-z0-9+._-]{0,58}$/u;
35
+ const SAFE_LOCALE_VALUE = /^[A-Za-z0-9][A-Za-z0-9_.@-]{0,127}$/u;
36
+
37
+ /**
38
+ * The pinned tmux client receives only presentation metadata. Everything else
39
+ * is denied by omission: PATH/HOME/SHELL, TMUX/TMUX_PANE/TMUX_TMPDIR, shell
40
+ * startup hooks, dynamic-loader variables, Node options, and project secrets
41
+ * cannot redirect authority or inject code into this daemon-owned execution.
42
+ */
43
+ export function tmuxClientEnvironment(source: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
44
+ const environment: NodeJS.ProcessEnv = {
45
+ TERM: SAFE_TERMINAL_VALUE.test(source.TERM ?? "") ? source.TERM : "xterm-256color",
46
+ // A pinned runner may be the first tmux client and therefore create the
47
+ // server. Never let a headless parent (`TERM=dumb`, `NO_COLOR=1`) become
48
+ // the global environment inherited by every later interactive child.
49
+ COLORTERM: "truecolor",
50
+ };
51
+ for (const name of ["LANG", "LC_ALL", "LC_CTYPE"] as const) {
52
+ const value = source[name];
53
+ if (value && SAFE_LOCALE_VALUE.test(value)) environment[name] = value;
54
+ }
55
+ return environment;
56
+ }
@@ -68,3 +68,8 @@ export function createNamedSocketFence(
68
68
  isPinned: () => shared.identity !== null,
69
69
  };
70
70
  }
71
+
72
+ /** True only before this exact authority has observed any live socket. */
73
+ export function isNamedSocketAuthorityUnbound(authority: WorkspacePaneTmuxAuthority): boolean {
74
+ return authority.socketSelector.kind === "name" && !states.get(authority)?.identity;
75
+ }
@@ -0,0 +1,81 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import {
3
+ createPinnedWorkspaceTmuxRunner,
4
+ createPinnedWorkspaceTmuxAsyncRunner,
5
+ type WorkspacePaneTmuxAuthority,
6
+ } from "./workspace-pane-creation.ts";
7
+ import { shellEscape } from "./shell.ts";
8
+
9
+ export interface NativeTmuxServerIdentity {
10
+ readonly pid: string;
11
+ readonly startTime: string;
12
+ }
13
+
14
+ /** Guard each command on the server which actually accepted this connection. */
15
+ export function fenceNativeTmuxCommand(
16
+ args: readonly string[],
17
+ expected: NativeTmuxServerIdentity,
18
+ ) {
19
+ const { pid, startTime } = expected;
20
+ if (!/^[1-9][0-9]*$/u.test(pid) || !/^[1-9][0-9]*$/u.test(startTime))
21
+ throw new Error("Invalid native tmux server identity");
22
+ if (args.length === 0 || args.some((arg) => arg.includes("\0")))
23
+ throw new Error("Invalid tmux command");
24
+ const globalFlags: string[] = [];
25
+ let firstCommand = 0;
26
+ while (args[firstCommand]?.startsWith("-")) {
27
+ const flag = args[firstCommand]!;
28
+ if (flag !== "-N" && flag !== "-u")
29
+ throw new Error("Unsupported tmux global option in generation-fenced command");
30
+ globalFlags.push(flag);
31
+ firstCommand += 1;
32
+ }
33
+ if (firstCommand === args.length) throw new Error("Missing tmux command");
34
+ const condition = `#{&&:#{==:#{pid},${pid}},#{==:#{start_time},${startTime}}}`;
35
+ // Preserve arbitrary literal payloads; a standalone ; is the native argv
36
+ // command separator already used by the existing runners.
37
+ const command = args
38
+ .slice(firstCommand)
39
+ .map((arg) => (arg === ";" ? ";" : shellEscape(arg)))
40
+ .join(" ");
41
+ const refusal = `tmux-server-stale.${randomUUID()}`;
42
+ return {
43
+ argv: [...globalFlags, "if-shell", "-F", condition, command, `display-message -p '${refusal}'`],
44
+ verify(output: string): string {
45
+ if (output.trimEnd() === refusal)
46
+ throw new Error("Tmux server generation changed before command execution");
47
+ return output;
48
+ },
49
+ };
50
+ }
51
+
52
+ /** Socket validation alone leaves a validation-to-connect replacement race. */
53
+ export function createServerGenerationFencedTmuxRunner(
54
+ authority: WorkspacePaneTmuxAuthority,
55
+ expected?: NativeTmuxServerIdentity,
56
+ ): (args: readonly string[]) => string {
57
+ const run = createPinnedWorkspaceTmuxRunner(authority, { timeoutMs: 5_000 });
58
+ const [pid, startTime] = expected
59
+ ? [expected.pid, expected.startTime]
60
+ : run(["display-message", "-p", "#{pid}\t#{start_time}"]).split("\t");
61
+ if (!pid || !startTime) throw new Error("Invalid native tmux server identity");
62
+ const identity = { pid, startTime };
63
+ // Validate once before publishing an owner; each command rechecks natively.
64
+ fenceNativeTmuxCommand(["display-message", "-p", ""], identity);
65
+ return (args) => {
66
+ const command = fenceNativeTmuxCommand(args, identity);
67
+ return command.verify(run(command.argv));
68
+ };
69
+ }
70
+
71
+ export function createServerGenerationFencedTmuxAsyncRunner(
72
+ authority: WorkspacePaneTmuxAuthority,
73
+ expected: NativeTmuxServerIdentity,
74
+ ): (args: readonly string[], signal?: AbortSignal) => Promise<string> {
75
+ const run = createPinnedWorkspaceTmuxAsyncRunner(authority);
76
+ fenceNativeTmuxCommand(["display-message", "-p", ""], expected);
77
+ return async (args, signal) => {
78
+ const command = fenceNativeTmuxCommand(args, expected);
79
+ return command.verify(await run(command.argv, signal));
80
+ };
81
+ }