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,336 @@
1
+ import { createNativeTmuxSessionCreator } from "./tmux-server-session-create.ts";
2
+ import type { WorkspacePaneCreateMutationRequest } from "@tmux-ide/contracts";
3
+ import { createTmuxSessionMutationFence } from "./tmux-session-mutation-fence.ts";
4
+ import { createNativeTmuxSessionOpener } from "./tmux-server-session-open.ts";
5
+ import { randomUUID } from "node:crypto";
6
+ import { mkdirSync } from "node:fs";
7
+ import { z } from "zod";
8
+ import type { WorkspaceMultiplexerBackend } from "../command-center/actions/handlers/workspace-multiplexer.ts";
9
+ import { isVisibleFleetSession, type LiveSessionSummary } from "../command-center/discovery.ts";
10
+ import { WorkspaceTerminalInventoryRuntime } from "../terminal/attachments/native-runtime.ts";
11
+ import { createTmuxAgentStatusProbe } from "../terminal/attachments/agent-status-probe.ts";
12
+ import { createPaneStreamRuntime } from "../terminal/pane-stream/runtime.ts";
13
+ import { liveSessionIdForNativeIdentity } from "../terminal/protocol/live-session-identity.ts";
14
+ import { SessionRuntimeRegistry } from "../terminal/session-runtime/registry.ts";
15
+ import { createSessionRuntimeMultiplexerBackend } from "../terminal/session-runtime/multiplexer-backend.ts";
16
+ import { TmuxExternalInteractionObserver } from "./tmux-external-interaction-observer.ts";
17
+ import { WorkspaceMultiplexerAuthority } from "./workspace-multiplexer-verbs.ts";
18
+ import {
19
+ WorkspacePaneCreationAuthority,
20
+ type WorkspacePaneTmuxAuthority,
21
+ } from "./workspace-pane-creation.ts";
22
+ import { WorkspaceRegistry } from "./workspace-registry.ts";
23
+ import {
24
+ createServerGenerationFencedTmuxRunner,
25
+ createServerGenerationFencedTmuxAsyncRunner,
26
+ type NativeTmuxServerIdentity,
27
+ } from "./tmux-server-generation-runner.ts";
28
+
29
+ export interface NativeTmuxServerOwnerOptions {
30
+ readonly serverId: string;
31
+ readonly generation: string;
32
+ readonly tmuxAuthority: WorkspacePaneTmuxAuthority;
33
+ readonly stateDirectory: string;
34
+ readonly webSocketUrl: string;
35
+ readonly nativeServerIdentity?: NativeTmuxServerIdentity;
36
+ }
37
+
38
+ /** Refresh discovered live membership without changing durable workspace intent or aliases. */
39
+ export function createNativeTmuxServerCatalog(
40
+ workspaceRegistry: WorkspaceRegistry,
41
+ run: (args: readonly string[], signal?: AbortSignal) => Promise<string>,
42
+ assertOpen: () => void = () => undefined,
43
+ ): () => Promise<LiveSessionSummary[]> {
44
+ return async (): Promise<LiveSessionSummary[]> => {
45
+ assertOpen();
46
+ const raw = await run(
47
+ [
48
+ "list-panes",
49
+ "-a",
50
+ "-F",
51
+ "#{pid}\t#{session_id}\t#{session_created}\t#{session_name}\t#{pane_id}\t#{session_path}",
52
+ ],
53
+ AbortSignal.timeout(2_000),
54
+ );
55
+ assertOpen();
56
+ const rows = new Map<
57
+ string,
58
+ { summary: LiveSessionSummary; panes: Set<string>; dir: string }
59
+ >();
60
+ for (const line of raw.trim().split("\n")) {
61
+ const [pid, id, created, name, pane, dir] = line.split("\t");
62
+ if (
63
+ !pid ||
64
+ !/^\d+$/.test(pid) ||
65
+ !id ||
66
+ !/^\$\d+$/.test(id) ||
67
+ !created ||
68
+ !/^\d+$/.test(created) ||
69
+ !name ||
70
+ !pane ||
71
+ !/^%\d+$/.test(pane) ||
72
+ !dir
73
+ )
74
+ throw new Error("Invalid native server catalog");
75
+ if (!isVisibleFleetSession(name)) continue;
76
+ const liveSessionId = liveSessionIdForNativeIdentity(pid, id, created);
77
+ const row = rows.get(liveSessionId) ?? {
78
+ summary: { liveSessionId, sessionName: name, paneCount: 0 },
79
+ panes: new Set<string>(),
80
+ dir,
81
+ };
82
+ row.panes.add(pane);
83
+ rows.set(liveSessionId, row);
84
+ }
85
+ const names = new Set([...rows.values()].map(({ summary }) => summary.sessionName));
86
+ for (const workspace of workspaceRegistry.list())
87
+ if (workspaceRegistry.isVolatile(workspace.name) && !names.has(workspace.sessionName))
88
+ workspaceRegistry.remove(workspace.name);
89
+ for (const { summary, dir } of rows.values())
90
+ if (
91
+ !workspaceRegistry.has(summary.sessionName) &&
92
+ !workspaceRegistry.list().some((workspace) => workspace.sessionName === summary.sessionName)
93
+ )
94
+ workspaceRegistry.add({
95
+ name: summary.sessionName,
96
+ sessionName: summary.sessionName,
97
+ projectDir: dir,
98
+ persistence: "volatile",
99
+ });
100
+ return [...rows.values()].map(({ summary, panes }) => ({ ...summary, paneCount: panes.size }));
101
+ };
102
+ }
103
+
104
+ /** One proven server incarnation. Never elects an endpoint or changes ambient tmux authority. */
105
+ export async function createNativeTmuxServerOwner(options: NativeTmuxServerOwnerOptions) {
106
+ const generation = z.uuid().parse(options.generation);
107
+ const authority = options.tmuxAuthority;
108
+ if (authority.socketSelector.kind !== "path") {
109
+ throw new Error("A server owner requires a proven direct socket authority");
110
+ }
111
+ const socketPath = authority.socketSelector.path;
112
+ mkdirSync(options.stateDirectory, { recursive: true, mode: 0o700 });
113
+ const workspaceRegistry = new WorkspaceRegistry({
114
+ dir: options.stateDirectory,
115
+ listSessions: () => [],
116
+ });
117
+ const sessionMutationFence = createTmuxSessionMutationFence();
118
+ const generationRun = sessionMutationFence.wrap(
119
+ createServerGenerationFencedTmuxRunner(authority, options.nativeServerIdentity),
120
+ );
121
+ const identityParts = generationRun(["display-message", "-p", "#{pid}\t#{start_time}"]).split(
122
+ "\t",
123
+ );
124
+ const nativeServerIdentity = options.nativeServerIdentity ?? {
125
+ pid: identityParts[0]!,
126
+ startTime: identityParts[1]!,
127
+ };
128
+ const generationRunAsync = createServerGenerationFencedTmuxAsyncRunner(
129
+ authority,
130
+ nativeServerIdentity,
131
+ );
132
+ const multiplexer = new WorkspaceMultiplexerAuthority({
133
+ daemonInstanceId: generation,
134
+ registry: workspaceRegistry,
135
+ tmuxAuthority: authority,
136
+ io: { runTmux: generationRun },
137
+ });
138
+ const paneCreation = new WorkspacePaneCreationAuthority({
139
+ daemonInstanceId: generation,
140
+ registry: workspaceRegistry,
141
+ tmuxAuthority: authority,
142
+ io: { runTmux: generationRun },
143
+ });
144
+ let disposed = false;
145
+ let disposePromise: Promise<void> | null = null;
146
+ let observerStarted: Promise<void> | null = null;
147
+ let receiptSequence = 0;
148
+ const assertOpen = () => {
149
+ if (disposed) throw new Error("Tmux server owner is retired");
150
+ };
151
+ const sessionRuntimeRegistry: SessionRuntimeRegistry = new SessionRuntimeRegistry({
152
+ generation,
153
+ semanticMutations: {
154
+ resolveSession: (name) => workspaceRegistry.get(name)?.sessionName ?? null,
155
+ execute: (operationId, intent, timing) => {
156
+ assertOpen();
157
+ if (intent.verb === "workspace.pane.read") return multiplexer.readPane(operationId, intent);
158
+ if (
159
+ intent.verb === "workspace.window.link.select" ||
160
+ intent.verb === "workspace.window.link.unlink" ||
161
+ intent.verb === "workspace.pane.select"
162
+ ) {
163
+ return multiplexer.mutateWindowLink(
164
+ { operationId, expectedDaemonInstanceId: generation, intent },
165
+ (session, action) => sessionRuntimeRegistry.executeWindowLinkAction(session, action),
166
+ );
167
+ }
168
+ return multiplexer.mutate(
169
+ { operationId, expectedDaemonInstanceId: generation, intent },
170
+ timing,
171
+ );
172
+ },
173
+ publishReceipt: (receipt) => ({
174
+ ...receipt,
175
+ type: "interaction.receipt",
176
+ sequence: ++receiptSequence,
177
+ }),
178
+ traceAuthority: { generation, incarnation: null },
179
+ },
180
+ mirror: {
181
+ executable: authority.executablePath,
182
+ socketPath,
183
+ resolveSocketPath: () => generationRun(["display-message", "-p", "#{socket_path}"]),
184
+ nativeServerIdentity,
185
+ internalReadHookEmission: (pane, marker) => observer.internalReadHookEmission(pane, marker),
186
+ },
187
+ });
188
+ const terminalInventoryRuntime = new WorkspaceTerminalInventoryRuntime({
189
+ registry: workspaceRegistry,
190
+ sessionRuntimeRegistry,
191
+ tmuxAuthority: { ...authority, trustedCwd: options.stateDirectory, nativeServerIdentity },
192
+ agentStatusProbeFactory: ({ run }) => createTmuxAgentStatusProbe({ run }),
193
+ onInventory: (snapshot) => multiplexer.adoptPaneInventory(snapshot.panes),
194
+ onSessionInventory: (session, snapshot) =>
195
+ multiplexer.adoptSessionPaneInventory(session, snapshot?.panes ?? []),
196
+ });
197
+ const observer: TmuxExternalInteractionObserver = new TmuxExternalInteractionObserver({
198
+ daemonInstanceId: generation,
199
+ internalReadOwnerToken: randomUUID(),
200
+ registry: workspaceRegistry,
201
+ tmuxAuthority: authority,
202
+ io: { runTmux: generationRunAsync },
203
+ onGap: () => terminalInventoryRuntime.invalidate(),
204
+ onObserved: ({ workspaceName, semanticPaneId, operationKind, operationId }) =>
205
+ operationId !== null &&
206
+ sessionRuntimeRegistry.observeTmuxInteraction({
207
+ workspaceName,
208
+ semanticPaneId,
209
+ operationKind,
210
+ operationId,
211
+ }),
212
+ });
213
+ const paneStreamRuntime = createPaneStreamRuntime({
214
+ daemonInstanceId: generation,
215
+ webSocketUrl: options.webSocketUrl,
216
+ sessionRuntimeRegistry,
217
+ semanticPaneCatalog: terminalInventoryRuntime.semanticPaneCatalog,
218
+ });
219
+ const backend = createSessionRuntimeMultiplexerBackend({
220
+ registry: sessionRuntimeRegistry,
221
+ resolveSession: (name) => workspaceRegistry.get(name)?.sessionName ?? null,
222
+ });
223
+ const catalog = createNativeTmuxServerCatalog(workspaceRegistry, generationRunAsync, assertOpen);
224
+ const sessionCreator = createNativeTmuxSessionCreator({
225
+ generation,
226
+ registry: workspaceRegistry,
227
+ run: generationRun,
228
+ assertOpen,
229
+ });
230
+ const sessionOpener = createNativeTmuxSessionOpener({
231
+ generation,
232
+ registry: workspaceRegistry,
233
+ run: generationRunAsync,
234
+ assertOpen,
235
+ });
236
+ const openSession = async (liveSessionId: string) => {
237
+ await catalog();
238
+ const result = await sessionOpener.openSession(liveSessionId);
239
+ terminalInventoryRuntime.invalidate();
240
+ return result;
241
+ };
242
+ const multiplexerBackend: WorkspaceMultiplexerBackend = {
243
+ mutate: async (...args) => {
244
+ assertOpen();
245
+ if (args[0].expectedDaemonInstanceId !== generation)
246
+ throw new Error("Tmux server generation mismatch");
247
+ await catalog();
248
+ observerStarted ??= observer.start();
249
+ await observerStarted;
250
+ assertOpen();
251
+ return backend.mutate(...args);
252
+ },
253
+ };
254
+ const dispose = (): Promise<void> => {
255
+ if (disposePromise) return disposePromise;
256
+ disposed = true;
257
+ disposePromise = (async () => {
258
+ await sessionOpener.dispose();
259
+ await sessionCreator.dispose();
260
+ await paneCreation.dispose();
261
+ try {
262
+ await paneStreamRuntime.dispose();
263
+ } finally {
264
+ try {
265
+ await observer.dispose();
266
+ } finally {
267
+ terminalInventoryRuntime.dispose();
268
+ try {
269
+ await multiplexer.dispose();
270
+ } finally {
271
+ await sessionRuntimeRegistry.dispose();
272
+ }
273
+ }
274
+ }
275
+ })();
276
+ return disposePromise;
277
+ };
278
+ try {
279
+ await terminalInventoryRuntime.whenReady();
280
+ await catalog();
281
+ } catch (error) {
282
+ await dispose();
283
+ throw error;
284
+ }
285
+ return {
286
+ serverId: options.serverId,
287
+ generation,
288
+ catalog,
289
+ openSession,
290
+ createSession: sessionCreator.createSession,
291
+ createSessionPane: async (
292
+ liveSessionId: string,
293
+ request: WorkspacePaneCreateMutationRequest,
294
+ ) => {
295
+ assertOpen();
296
+ await catalog();
297
+ const sessionName = workspaceRegistry.get(request.intent.workspaceName)?.sessionName;
298
+ if (!sessionName) throw new Error("Selected workspace is unavailable");
299
+ const result = await sessionMutationFence.execute({
300
+ liveSessionId,
301
+ sessionName,
302
+ run: generationRunAsync,
303
+ mutate: () => paneCreation.create(request),
304
+ });
305
+ terminalInventoryRuntime.invalidate();
306
+ return result;
307
+ },
308
+ mutateSession: async (
309
+ liveSessionId: string,
310
+ request: Parameters<WorkspaceMultiplexerBackend["mutate"]>[0],
311
+ authenticatedHostClientId?: string,
312
+ ) => {
313
+ assertOpen();
314
+ await catalog();
315
+ const sessionName = workspaceRegistry.get(request.intent.workspaceName)?.sessionName;
316
+ if (!sessionName) throw new Error("Selected workspace is unavailable");
317
+ observerStarted ??= observer.start();
318
+ await observerStarted;
319
+ return sessionMutationFence.execute({
320
+ liveSessionId,
321
+ sessionName,
322
+ run: generationRunAsync,
323
+ mutate: () =>
324
+ multiplexerBackend.mutate(request, authenticatedHostClientId, undefined, true),
325
+ });
326
+ },
327
+ workspaceRegistry,
328
+ multiplexerBackend,
329
+ sessionRuntimeRegistry,
330
+ terminalInventoryRuntime,
331
+ paneStreamRuntime,
332
+ dispose,
333
+ };
334
+ }
335
+
336
+ export type NativeTmuxServerOwner = Awaited<ReturnType<typeof createNativeTmuxServerOwner>>;
@@ -0,0 +1,228 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { z } from "zod";
3
+ import {
4
+ TmuxServerIdSchemaZ,
5
+ TmuxServerRegistrationRequestSchemaZ,
6
+ TmuxServerScopeSchemaZ,
7
+ type TmuxServerScope,
8
+ type TmuxServerDescriptor,
9
+ } from "@tmux-ide/contracts";
10
+ import type { WorkspacePaneTmuxAuthority } from "./workspace-pane-creation.ts";
11
+
12
+ // Admission limits, not performance claims. Discovery is explicit/on-demand.
13
+ export const MAX_TMUX_SERVER_OWNERS = 16;
14
+ export const TmuxServerSelectorSchemaZ = TmuxServerRegistrationRequestSchemaZ.shape.selector;
15
+ export const TmuxServerRegistrationSchemaZ = TmuxServerRegistrationRequestSchemaZ.extend({
16
+ serverId: TmuxServerIdSchemaZ,
17
+ }).strict();
18
+ export type TmuxServerRegistration = z.infer<typeof TmuxServerRegistrationSchemaZ>;
19
+ export interface TmuxServerObservation {
20
+ /** Private canonical path/server PID/start-time proof, never a public identity. */
21
+ readonly fingerprint: string;
22
+ readonly nativeServerIdentity?: { readonly pid: string; readonly startTime: string };
23
+ readonly authority: WorkspacePaneTmuxAuthority;
24
+ /** Synchronous socket fence. No subprocess on input or rendering paths. */
25
+ readonly valid: () => boolean;
26
+ }
27
+ export interface DisposableTmuxServerOwner {
28
+ dispose(): Promise<void>;
29
+ }
30
+ export class TmuxServerScopeError extends Error {
31
+ constructor(
32
+ readonly code: "not-found" | "offline" | "stale-generation" | "capacity" | "disposed",
33
+ ) {
34
+ super(`tmux server ${code}`);
35
+ this.name = "TmuxServerScopeError";
36
+ }
37
+ }
38
+ type Entry<T> = {
39
+ registration: TmuxServerRegistration;
40
+ observation: TmuxServerObservation | null;
41
+ scope: TmuxServerScope | null;
42
+ owner: T | null;
43
+ };
44
+ export interface TmuxServerOwnersOptions<T extends DisposableTmuxServerOwner> {
45
+ readonly probe: (
46
+ selector: TmuxServerRegistration["selector"],
47
+ ) => Promise<TmuxServerObservation | null>;
48
+ readonly create: (
49
+ registration: TmuxServerRegistration,
50
+ scope: TmuxServerScope,
51
+ observation: TmuxServerObservation,
52
+ ) => Promise<T>;
53
+ readonly persist?: (registrations: readonly TmuxServerRegistration[]) => void;
54
+ }
55
+ /** One process; no shared mutable tmux authority, runtime cache or retry ledger. */
56
+ export class TmuxServerOwners<T extends DisposableTmuxServerOwner> {
57
+ readonly #entries = new Map<string, Entry<T>>();
58
+ #tail: Promise<unknown> = Promise.resolve();
59
+ #disposed = false;
60
+ #disposePromise: Promise<void> | null = null;
61
+ constructor(readonly options: TmuxServerOwnersOptions<T>) {}
62
+ #serial<R>(work: () => Promise<R>): Promise<R> {
63
+ const run = this.#tail.then(() => {
64
+ if (this.#disposed) throw new TmuxServerScopeError("disposed");
65
+ return work();
66
+ });
67
+ this.#tail = run.catch(() => undefined);
68
+ return run;
69
+ }
70
+ #descriptor(entry: Entry<T>): TmuxServerDescriptor {
71
+ return entry.scope
72
+ ? { ...entry.scope, label: entry.registration.label, state: "online" }
73
+ : {
74
+ serverId: entry.registration.serverId,
75
+ generation: null,
76
+ label: entry.registration.label,
77
+ state: "offline",
78
+ };
79
+ }
80
+ list(): readonly TmuxServerDescriptor[] {
81
+ return [...this.#entries.values()].map((entry) => this.#descriptor(entry));
82
+ }
83
+ registrations(): readonly TmuxServerRegistration[] {
84
+ return [...this.#entries.values()].map((entry) => structuredClone(entry.registration));
85
+ }
86
+ #save(): void {
87
+ this.options.persist?.(this.registrations());
88
+ }
89
+ async #retire(entry: Entry<T>): Promise<void> {
90
+ const owner = entry.owner;
91
+ entry.owner = null;
92
+ entry.scope = null;
93
+ entry.observation = null;
94
+ if (owner) await owner.dispose();
95
+ }
96
+ async #refresh(entry: Entry<T>, observed?: TmuxServerObservation | null): Promise<void> {
97
+ const next =
98
+ observed === undefined ? await this.options.probe(entry.registration.selector) : observed;
99
+ if (next && entry.observation?.fingerprint === next.fingerprint && entry.observation.valid())
100
+ return;
101
+ // Absence revokes command/stream leases without claiming native server death.
102
+ // Socket recreation also needs a fresh connection authority, even if PID survived.
103
+ await this.#retire(entry);
104
+ if (this.#disposed || !next || !next.valid()) return;
105
+ const duplicate = [...this.#entries.values()].find(
106
+ (other) =>
107
+ other !== entry &&
108
+ other.observation?.fingerprint === next.fingerprint &&
109
+ other.observation.valid(),
110
+ );
111
+ if (duplicate) return; // Never construct a second owner for a proven live alias.
112
+ const scope = { serverId: entry.registration.serverId, generation: randomUUID() };
113
+ let owner: T;
114
+ try {
115
+ owner = await this.options.create(entry.registration, scope, next);
116
+ } catch {
117
+ return;
118
+ } // Registration remains visible/offline; explicit refresh retries.
119
+ if (this.#disposed || !next.valid()) {
120
+ await owner.dispose();
121
+ return;
122
+ }
123
+ entry.observation = next;
124
+ entry.scope = scope;
125
+ entry.owner = owner;
126
+ }
127
+ register(
128
+ input: Omit<TmuxServerRegistration, "serverId"> & { serverId?: string },
129
+ ): Promise<TmuxServerDescriptor> {
130
+ return this.#serial(async () => {
131
+ const registration = TmuxServerRegistrationSchemaZ.parse({
132
+ ...input,
133
+ serverId: input.serverId ?? `tmux-server.${randomUUID().replaceAll("-", "")}`,
134
+ });
135
+ const sameSelector = [...this.#entries.values()].find(
136
+ (entry) =>
137
+ JSON.stringify(entry.registration.selector) === JSON.stringify(registration.selector),
138
+ );
139
+ if (sameSelector) {
140
+ await this.#refresh(sameSelector);
141
+ return this.#descriptor(sameSelector);
142
+ }
143
+ if (this.#entries.has(registration.serverId))
144
+ throw new TypeError("Server registration already exists");
145
+ const observation = await this.options.probe(registration.selector);
146
+ if (this.#disposed) throw new TmuxServerScopeError("disposed");
147
+ const alias =
148
+ observation &&
149
+ [...this.#entries.values()].find(
150
+ (entry) =>
151
+ entry.observation?.fingerprint === observation.fingerprint && entry.observation.valid(),
152
+ );
153
+ if (alias) return this.#descriptor(alias);
154
+ if (this.#entries.size >= MAX_TMUX_SERVER_OWNERS) throw new TmuxServerScopeError("capacity");
155
+ const entry: Entry<T> = { registration, observation: null, scope: null, owner: null };
156
+ this.#entries.set(registration.serverId, entry);
157
+ try {
158
+ this.#save();
159
+ } catch (error) {
160
+ this.#entries.delete(registration.serverId);
161
+ throw error;
162
+ }
163
+ await this.#refresh(entry, observation);
164
+ return this.#descriptor(entry);
165
+ });
166
+ }
167
+ /** Adopt the existing default owner rather than creating a duplicate mirror. */
168
+ adopt(
169
+ registrationInput: TmuxServerRegistration,
170
+ scopeInput: TmuxServerScope,
171
+ observation: TmuxServerObservation,
172
+ owner: T,
173
+ ): void {
174
+ const registration = TmuxServerRegistrationSchemaZ.parse(registrationInput);
175
+ const scope = TmuxServerScopeSchemaZ.parse(scopeInput);
176
+ if (scope.serverId !== registration.serverId || this.#entries.size || this.#disposed)
177
+ throw new TypeError("Invalid default owner adoption");
178
+ this.#entries.set(scope.serverId, { registration, scope, observation, owner });
179
+ }
180
+ refresh(): Promise<readonly TmuxServerDescriptor[]> {
181
+ return this.#serial(async () => {
182
+ // Sequential admission bounds subprocess work and avoids alias races.
183
+ for (const entry of this.#entries.values()) await this.#refresh(entry);
184
+ return this.list();
185
+ });
186
+ }
187
+ current(scopeInput: TmuxServerScope): T {
188
+ const scope = TmuxServerScopeSchemaZ.parse(scopeInput);
189
+ if (this.#disposed) throw new TmuxServerScopeError("disposed");
190
+ const entry = this.#entries.get(scope.serverId);
191
+ if (!entry) throw new TmuxServerScopeError("not-found");
192
+ if (!entry.scope || !entry.owner) throw new TmuxServerScopeError("offline");
193
+ if (entry.scope.generation !== scope.generation || !entry.observation?.valid())
194
+ throw new TmuxServerScopeError("stale-generation");
195
+ return entry.owner;
196
+ }
197
+ async withOwner<R>(scope: TmuxServerScope, work: (owner: T) => Promise<R>): Promise<R> {
198
+ const owner = this.current(scope);
199
+ const result = await work(owner);
200
+ if (this.current(scope) !== owner) throw new TmuxServerScopeError("stale-generation");
201
+ return result;
202
+ }
203
+ remove(serverId: string): Promise<void> {
204
+ return this.#serial(async () => {
205
+ const entry = this.#entries.get(TmuxServerIdSchemaZ.parse(serverId));
206
+ if (!entry) throw new TmuxServerScopeError("not-found");
207
+ this.#entries.delete(serverId);
208
+ try {
209
+ this.#save();
210
+ } catch (error) {
211
+ this.#entries.set(serverId, entry);
212
+ throw error;
213
+ }
214
+ await this.#retire(entry); // never kill-server or kill-session
215
+ });
216
+ }
217
+ dispose(): Promise<void> {
218
+ if (this.#disposePromise) return this.#disposePromise;
219
+ this.#disposed = true;
220
+ this.#disposePromise = (async () => {
221
+ await this.#tail;
222
+ const entries = [...this.#entries.values()];
223
+ this.#entries.clear();
224
+ await Promise.allSettled(entries.map((entry) => this.#retire(entry)));
225
+ })();
226
+ return this.#disposePromise;
227
+ }
228
+ }
@@ -0,0 +1,95 @@
1
+ import { realpathSync, statSync } from "node:fs";
2
+ import { isNamedSocketAuthorityUnbound } from "./tmux-named-socket-fence.ts";
3
+ import type { WorkspacePaneTmuxAuthority } from "./workspace-pane-creation.ts";
4
+ import { createHash } from "node:crypto";
5
+ import {
6
+ captureUnixSocketIdentity,
7
+ revalidateUnixSocketIdentity,
8
+ } from "./unix-socket-authority.ts";
9
+
10
+ export interface TmuxServerProof {
11
+ readonly version: 1;
12
+ readonly kind: "live" | "unbound-name";
13
+ readonly digest: string;
14
+ }
15
+
16
+ /** Two read-only, no-start queries fence the socket and server incarnation. */
17
+ export function captureTmuxServerProof(
18
+ run: (args: readonly string[]) => string,
19
+ ): TmuxServerProof | null {
20
+ const args = ["-N", "display-message", "-p", "#{socket_path}|#{pid}|#{start_time}"];
21
+ try {
22
+ const raw = run(args).trimEnd();
23
+ const match = /^(.*)\|([1-9][0-9]*)\|([1-9][0-9]*)$/u.exec(raw);
24
+ if (!match || raw.length > 8192) return null;
25
+ const socket = captureUnixSocketIdentity(match[1]!);
26
+ if (run(args).trimEnd() !== raw) return null;
27
+ revalidateUnixSocketIdentity(socket);
28
+ return {
29
+ version: 1,
30
+ kind: "live",
31
+ digest: createHash("sha256")
32
+ .update(JSON.stringify([socket.path, match[2], match[3]]))
33
+ .digest("hex"),
34
+ };
35
+ } catch {
36
+ return null;
37
+ }
38
+ }
39
+
40
+ export async function captureTmuxServerProofAsync(
41
+ run: (args: readonly string[]) => Promise<string>,
42
+ ): Promise<TmuxServerProof | null> {
43
+ const args = ["-N", "display-message", "-p", "#{socket_path}|#{pid}|#{start_time}"];
44
+ try {
45
+ const raw = (await run(args)).trimEnd();
46
+ const match = /^(.*)\|([1-9][0-9]*)\|([1-9][0-9]*)$/u.exec(raw);
47
+ if (!match || raw.length > 8192) return null;
48
+ const socket = captureUnixSocketIdentity(match[1]!);
49
+ if ((await run(args)).trimEnd() !== raw) return null;
50
+ revalidateUnixSocketIdentity(socket);
51
+ return {
52
+ version: 1,
53
+ kind: "live",
54
+ digest: createHash("sha256")
55
+ .update(JSON.stringify([socket.path, match[2], match[3]]))
56
+ .digest("hex"),
57
+ };
58
+ } catch {
59
+ return null;
60
+ }
61
+ }
62
+
63
+ /** A selector commitment is not evidence of an absent or live server. */
64
+ export function captureUnboundTmuxSelectorProof(
65
+ authority: WorkspacePaneTmuxAuthority,
66
+ ): TmuxServerProof | null {
67
+ if (!isNamedSocketAuthorityUnbound(authority) || authority.socketSelector.kind !== "name")
68
+ return null;
69
+ try {
70
+ const executable = realpathSync(authority.executablePath);
71
+ const stat = statSync(executable, { bigint: true });
72
+ const uid = process.getuid?.();
73
+ if (uid === undefined || !stat.isFile()) return null;
74
+ return {
75
+ version: 1,
76
+ kind: "unbound-name",
77
+ digest: createHash("sha256")
78
+ .update(
79
+ JSON.stringify([
80
+ "sanitized-tmux-environment-v1",
81
+ executable,
82
+ String(stat.dev),
83
+ String(stat.ino),
84
+ String(stat.mtimeNs),
85
+ String(stat.size),
86
+ uid,
87
+ authority.socketSelector.name,
88
+ ]),
89
+ )
90
+ .digest("hex"),
91
+ };
92
+ } catch {
93
+ return null;
94
+ }
95
+ }