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
@@ -28,6 +28,8 @@ import {
28
28
  type WorkspaceMultiplexerWindowTarget,
29
29
  type SessionRuntimePaneReadIntent,
30
30
  type Workspace,
31
+ type WindowLinkTarget,
32
+ type WindowLinkTopology,
31
33
  } from "@tmux-ide/contracts";
32
34
  import { TmuxError } from "@tmux-ide/tmux-bridge";
33
35
 
@@ -46,6 +48,8 @@ import {
46
48
  } from "./tmux-interaction-options.ts";
47
49
  import { memorablePaneName } from "../terminal/protocol/pane-display-name.ts";
48
50
 
51
+ import { WindowLinkResolutionError } from "../terminal/mirror/window-link-authority.ts";
52
+
49
53
  const CREATION_OPTION = "@tmux_ide_creation_id";
50
54
  const SEMANTIC_PANE_OPTION = "@tmux_ide_pane_id";
51
55
  const SEMANTIC_WINDOW_OPTION = "@tmux_ide_window_id";
@@ -68,6 +72,10 @@ export type WorkspaceMultiplexerErrorCode =
68
72
  | "pane_not_found"
69
73
  | "window_not_found"
70
74
  | "ambiguous_target"
75
+ | "window_link_stale"
76
+ | "window_link_ambiguous"
77
+ | "window_link_session_mismatch"
78
+ | "window_link_backing_mismatch"
71
79
  /** Refused: killing it would take the whole session with it. */
72
80
  | "last_window_refused"
73
81
  /** Refused: killing it would take the whole session with it. */
@@ -88,6 +96,10 @@ const ERROR_MESSAGES: Readonly<Record<WorkspaceMultiplexerErrorCode, string>> =
88
96
  workspace_unavailable: "The requested workspace is not available for multiplexer verbs.",
89
97
  pane_not_found: "No pane in this workspace carries the requested semantic identity.",
90
98
  window_not_found: "No window in this workspace carries the requested identity.",
99
+ window_link_stale: "The window link observation is no longer current.",
100
+ window_link_ambiguous: "Select an explicit window link for this pane.",
101
+ window_link_session_mismatch: "The window link belongs to another session generation.",
102
+ window_link_backing_mismatch: "The window link no longer names the expected window.",
91
103
  ambiguous_target: "The requested identity names more than one live tmux object.",
92
104
  last_window_refused:
93
105
  "This is the session's last window. Close the session instead if that is what you mean.",
@@ -201,7 +213,19 @@ export function parseMultiplexerPaneRows(output: string): readonly MultiplexerPa
201
213
  creationId: creationId === "" ? null : creationId,
202
214
  });
203
215
  }
204
- return rows;
216
+ // list-panes -s repeats each backing for its session links. Identical native
217
+ // rows are one pane; conflicting observations fail closed.
218
+ const unique = new Map<string, MultiplexerPaneRow>();
219
+ for (const row of rows) {
220
+ const previous = unique.get(row.paneId);
221
+ if (previous && JSON.stringify(previous) !== JSON.stringify(row)) {
222
+ throw new WorkspaceMultiplexerError("workspace_unavailable", {
223
+ reason: "conflicting_linked_pane_observation",
224
+ });
225
+ }
226
+ unique.set(row.paneId, row);
227
+ }
228
+ return [...unique.values()];
205
229
  }
206
230
 
207
231
  /** Distinct windows in a pane listing, in first-seen order. */
@@ -288,6 +312,17 @@ const DEFAULT_IO: Omit<WorkspaceMultiplexerIo, "runTmux"> = {
288
312
 
289
313
  const MAX_CACHED_SESSIONS = 512;
290
314
 
315
+ export interface AuthoritativeWindowLinkAction {
316
+ readonly action: "select" | "unlink";
317
+ readonly target?: WindowLinkTarget;
318
+ /** Semantic pane identity, resolved by the retained mirror authority. */
319
+ readonly paneId?: string;
320
+ }
321
+ export interface AuthoritativeWindowLinkActionResult {
322
+ readonly outcome: "applied" | "stale" | "native-refused" | "indeterminate";
323
+ readonly windowLinks: WindowLinkTopology | null;
324
+ }
325
+
291
326
  export class WorkspaceMultiplexerAuthority {
292
327
  readonly #daemonInstanceId: string;
293
328
  readonly #registry: WorkspaceRegistry;
@@ -329,6 +364,82 @@ export class WorkspaceMultiplexerAuthority {
329
364
  return this.#mutate(raw, timing);
330
365
  }
331
366
 
367
+ /** Runs only inside the existing semantic mutation lane; never rediscovers a stale link. */
368
+ async mutateWindowLink(
369
+ raw: WorkspaceMultiplexerMutationRequest,
370
+ execute: (
371
+ session: string,
372
+ action: AuthoritativeWindowLinkAction,
373
+ ) => Promise<AuthoritativeWindowLinkActionResult>,
374
+ ): Promise<WorkspaceMultiplexerMutationResult> {
375
+ if (this.#disposed) throw new WorkspaceMultiplexerError("workspace_unavailable");
376
+ const request = WorkspaceMultiplexerMutationRequestSchemaZ.parse(raw);
377
+ if (request.expectedDaemonInstanceId !== this.#daemonInstanceId)
378
+ throw new WorkspaceMultiplexerError("daemon_instance_mismatch");
379
+ const intent = request.intent;
380
+ if (
381
+ intent.verb !== "workspace.window.link.select" &&
382
+ intent.verb !== "workspace.window.link.unlink" &&
383
+ intent.verb !== "workspace.pane.select"
384
+ )
385
+ throw new TypeError("Expected a window-link selection or unlink intent");
386
+ const workspace = this.#registry.get(intent.workspaceName);
387
+ if (!workspace) throw new WorkspaceMultiplexerError("workspace_not_found");
388
+ const envelope = {
389
+ operationId: request.operationId,
390
+ daemonInstanceId: this.#daemonInstanceId,
391
+ workspaceName: intent.workspaceName,
392
+ };
393
+ try {
394
+ const result = await execute(
395
+ workspace.sessionName,
396
+ intent.verb === "workspace.pane.select"
397
+ ? {
398
+ action: "select",
399
+ ...(intent.windowLink ? { target: intent.windowLink } : {}),
400
+ paneId: intent.semanticPaneId,
401
+ }
402
+ : {
403
+ action: intent.verb === "workspace.window.link.unlink" ? "unlink" : "select",
404
+ target: intent.target,
405
+ },
406
+ );
407
+ if (result.outcome !== "applied") {
408
+ throw new WorkspaceMultiplexerError(
409
+ result.outcome === "stale"
410
+ ? "window_link_stale"
411
+ : result.outcome === "native-refused"
412
+ ? "mutation_failed"
413
+ : "mutation_unverified",
414
+ { reason: result.outcome },
415
+ );
416
+ }
417
+ return WorkspaceMultiplexerMutationResultSchemaZ.parse(
418
+ intent.verb === "workspace.pane.select"
419
+ ? {
420
+ ...envelope,
421
+ verb: intent.verb,
422
+ outcome: "applied",
423
+ semanticPaneId: intent.semanticPaneId,
424
+ }
425
+ : { ...envelope, verb: intent.verb, outcome: "applied", target: intent.target },
426
+ );
427
+ } catch (error) {
428
+ if (error instanceof WorkspaceMultiplexerError) throw error;
429
+ if (error instanceof WindowLinkResolutionError)
430
+ throw new WorkspaceMultiplexerError(
431
+ error.reason,
432
+ { operationId: request.operationId },
433
+ error,
434
+ );
435
+ throw new WorkspaceMultiplexerError(
436
+ "mutation_unverified",
437
+ { operationId: request.operationId },
438
+ error,
439
+ );
440
+ }
441
+ }
442
+
332
443
  /**
333
444
  * Capture a semantically addressed pane for an authored read. The tmux
334
445
  * after-capture hook is the completion authority; this method only performs
@@ -608,6 +719,11 @@ export class WorkspaceMultiplexerAuthority {
608
719
  } as const;
609
720
 
610
721
  switch (intent.verb) {
722
+ case "workspace.window.link.select":
723
+ case "workspace.window.link.unlink":
724
+ throw new WorkspaceMultiplexerError("workspace_unavailable", {
725
+ reason: "window_link_authority_required",
726
+ });
611
727
  case "workspace.window.split":
612
728
  return this.#split(request, workspace, envelope);
613
729
  case "workspace.window.kill":
@@ -1039,6 +1155,10 @@ export class WorkspaceMultiplexerAuthority {
1039
1155
  envelope: { operationId: string; daemonInstanceId: string; workspaceName: string },
1040
1156
  timing?: WorkspaceMultiplexerOperationTiming,
1041
1157
  ): WorkspaceMultiplexerMutationResult {
1158
+ if (intent.windowLink)
1159
+ throw new WorkspaceMultiplexerError("workspace_unavailable", {
1160
+ reason: "window_link_authority_required",
1161
+ });
1042
1162
  const timed = <T>(operation: string, run: () => T): T => {
1043
1163
  if (!timing) return run();
1044
1164
  let startedAtMicros: number;
@@ -1,6 +1,6 @@
1
1
  import { boundedTmuxRead } from "./bounded-tmux-read.ts";
2
2
  import { accessSync, constants, realpathSync, statSync } from "node:fs";
3
- import { delimiter, isAbsolute, join, relative, sep } from "node:path";
3
+ import { isAbsolute, relative, sep } from "node:path";
4
4
 
5
5
  import {
6
6
  WorkspacePaneCreateMutationRequestSchemaZ,
@@ -13,7 +13,10 @@ import {
13
13
  } from "@tmux-ide/contracts";
14
14
  import { runTmuxBinary, TmuxError } from "@tmux-ide/tmux-bridge";
15
15
  import { createNamedSocketFence } from "./tmux-named-socket-fence.ts";
16
- import { resolveBundledTmux } from "./bundled-tmux.ts";
16
+ import {
17
+ resolveTmuxExecutable as resolvePinnedTmuxExecutable,
18
+ tmuxClientEnvironment,
19
+ } from "./tmux-client-execution.ts";
17
20
 
18
21
  import { probeProjectReadiness } from "./project-readiness-probe.ts";
19
22
  import { agentHintForCommand } from "./agent-kind.ts";
@@ -26,7 +29,7 @@ import { getDefaultWorkspaceRegistry, type WorkspaceRegistry } from "./workspace
26
29
  import { memorablePaneName } from "../terminal/protocol/pane-display-name.ts";
27
30
  import { shellEscape } from "./shell.ts";
28
31
  import { MissionRepository } from "./mission-repository.ts";
29
- import { resolveRuntimeNamespace } from "./runtime-namespace.ts";
32
+ import { resolveRuntimeNamespace, resolveTmuxServerIntent } from "./runtime-namespace.ts";
30
33
  import { prepareTmuxTruecolorEnvironment } from "./tmux-terminal-color.ts";
31
34
  import {
32
35
  captureUnixSocketIdentity,
@@ -220,57 +223,15 @@ function assertEffectiveConfigProvenance(
220
223
  }
221
224
 
222
225
  function resolveTmuxExecutable(): string {
223
- if (resolveRuntimeNamespace().development) return resolveBundledTmux()!;
224
- const configured = process.env.TMUX_IDE_TMUX_BIN;
225
- if (!configured) {
226
- const bundled = resolveBundledTmux();
227
- if (bundled) return bundled;
228
- }
229
- const candidates = configured
230
- ? [configured]
231
- : (process.env.PATH ?? "")
232
- .split(delimiter)
233
- // Empty and relative PATH entries mean the daemon/project cwd. They
234
- // are never executable authority for a privileged tmux mutation.
235
- .filter((entry) => entry.length > 0 && isAbsolute(entry))
236
- .map((entry) => join(entry, "tmux"));
237
- for (const candidate of candidates) {
238
- try {
239
- if (!isAbsolute(candidate)) continue;
240
- accessSync(candidate, constants.X_OK);
241
- const canonical = realpathSync(candidate);
242
- if (statSync(canonical).isFile()) return canonical;
243
- } catch {
244
- // Continue to the next daemon-start candidate.
245
- }
246
- }
247
- throw new WorkspacePaneCreationError("workspace_unavailable", {
248
- reason: "tmux_executable_unavailable",
249
- });
250
- }
251
-
252
- const SAFE_TERMINAL_VALUE = /^(?:xterm|screen|tmux|rxvt|vt100|ansi)[A-Za-z0-9+._-]{0,58}$/u;
253
- const SAFE_LOCALE_VALUE = /^[A-Za-z0-9][A-Za-z0-9_.@-]{0,127}$/u;
254
-
255
- /**
256
- * The pinned tmux client receives only presentation metadata. Everything else
257
- * is denied by omission: PATH/HOME/SHELL, TMUX/TMUX_PANE/TMUX_TMPDIR, shell
258
- * startup hooks, dynamic-loader variables, Node options, and project secrets
259
- * cannot redirect authority or inject code into this daemon-owned execution.
260
- */
261
- function tmuxClientEnvironment(source: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
262
- const environment: NodeJS.ProcessEnv = {
263
- TERM: SAFE_TERMINAL_VALUE.test(source.TERM ?? "") ? source.TERM : "xterm-256color",
264
- // A pinned runner may be the first tmux client and therefore create the
265
- // server. Never let a headless parent (`TERM=dumb`, `NO_COLOR=1`) become
266
- // the global environment inherited by every later interactive child.
267
- COLORTERM: "truecolor",
268
- };
269
- for (const name of ["LANG", "LC_ALL", "LC_CTYPE"] as const) {
270
- const value = source[name];
271
- if (value && SAFE_LOCALE_VALUE.test(value)) environment[name] = value;
226
+ // Preserve development artifact qualification errors instead of masking them.
227
+ if (resolveRuntimeNamespace().development) return resolvePinnedTmuxExecutable();
228
+ try {
229
+ return resolvePinnedTmuxExecutable();
230
+ } catch {
231
+ throw new WorkspacePaneCreationError("workspace_unavailable", {
232
+ reason: "tmux_executable_unavailable",
233
+ });
272
234
  }
273
- return environment;
274
235
  }
275
236
 
276
237
  function tmuxSocketFromEnvironment(): string | null {
@@ -281,7 +242,7 @@ function tmuxSocketFromEnvironment(): string | null {
281
242
  export function resolveWorkspacePaneTmuxAuthority(): WorkspacePaneTmuxAuthority {
282
243
  const namespace = resolveRuntimeNamespace();
283
244
  const executablePath = resolveTmuxExecutable();
284
- if (namespace.development)
245
+ if (resolveTmuxServerIntent()?.source === "explicit")
285
246
  return Object.freeze({ executablePath, socketSelector: namespace.tmuxSocket });
286
247
  const environmentSocket = tmuxSocketFromEnvironment();
287
248
  if (environmentSocket) {
@@ -305,7 +266,7 @@ export function resolveWorkspacePaneTmuxAuthority(): WorkspacePaneTmuxAuthority
305
266
  // A sessionless daemon may start before the default tmux server exists. Pin
306
267
  // the daemon's default socket *name* and captured environment rather than
307
268
  // consulting a later request's TMUX/PATH.
308
- return Object.freeze({ executablePath, socketSelector: resolveRuntimeNamespace().tmuxSocket });
269
+ return Object.freeze({ executablePath, socketSelector: namespace.tmuxSocket });
309
270
  }
310
271
 
311
272
  /** Execute mutations only through the daemon-generation-pinned tmux authority. */
@@ -122,6 +122,7 @@ const PANE_VERIFY_FORMAT = [
122
122
  "#{pane_id}",
123
123
  `#{${SEMANTIC_PANE_OPTION}}`,
124
124
  `#{${SEMANTIC_WINDOW_OPTION}}`,
125
+ "#{window_index}",
125
126
  SENTINEL,
126
127
  ].join(FIELD);
127
128
 
@@ -209,6 +210,7 @@ interface ScanPane {
209
210
  }
210
211
 
211
212
  interface VerifyPane {
213
+ readonly windowIndex: number;
212
214
  readonly sessionId: string;
213
215
  readonly windowId: string;
214
216
  readonly windowName: string;
@@ -384,8 +386,10 @@ function parseVerifyPanes(output: string): VerifyPane[] {
384
386
  const windowPaneCount = positiveInteger(fields[3] ?? "");
385
387
  const sessionWindowCount = positiveInteger(fields[4] ?? "");
386
388
  if (
387
- fields.length !== 9 ||
388
- fields[8] !== SENTINEL ||
389
+ fields.length !== 10 ||
390
+ fields[9] !== SENTINEL ||
391
+ !/^(0|[1-9][0-9]*)$/u.test(fields[8]!) ||
392
+ !Number.isSafeInteger(Number(fields[8])) ||
389
393
  !/^\$[0-9]+$/u.test(fields[0]!) ||
390
394
  !/^@[0-9]+$/u.test(fields[1]!) ||
391
395
  !/^%[0-9]+$/u.test(fields[5]!) ||
@@ -405,6 +409,7 @@ function parseVerifyPanes(output: string): VerifyPane[] {
405
409
  paneId: fields[5]!,
406
410
  semanticPaneId: fields[6]!,
407
411
  semanticWindowId: fields[7]!,
412
+ windowIndex: Number(fields[8]),
408
413
  });
409
414
  }
410
415
  return panes;
@@ -565,9 +570,7 @@ export class WorkspacePromotionAuthority {
565
570
  // promotion is the app's only repair verb — so reconcile the stamps here
566
571
  // before resolving to a `replayed` outcome. Stamping is additive and never
567
572
  // overwrites a valid stamp, so a healthy session is untouched.
568
- const alreadyRegistered = this.#registry
569
- .list()
570
- .find((workspace) => workspace.sessionName === session.sessionName);
573
+ const alreadyRegistered = this.#registeredSession(session.sessionName);
571
574
  if (alreadyRegistered) {
572
575
  const registeredIdentity: PromotionIdentity = {
573
576
  workspaceName: alreadyRegistered.name,
@@ -590,6 +593,42 @@ export class WorkspacePromotionAuthority {
590
593
  await this.#verifyPromotedInventory(session.sessionId, identity);
591
594
  await this.#publishFleetEnrollment(request, session, identity);
592
595
 
596
+ // Catalog discovery can admit this session while the native stamping
597
+ // and verification awaits run. Reuse that exact registry identity instead
598
+ // of publishing a second name for the same session at the commit point.
599
+ const discovered = this.#registeredSession(session.sessionName);
600
+ if (discovered) {
601
+ if (discovered.name !== identity.workspaceName) {
602
+ try {
603
+ await this.#io.runTmux([
604
+ "set-option",
605
+ "-t",
606
+ session.sessionId,
607
+ SESSION_WORKSPACE_OPTION,
608
+ discovered.name,
609
+ ]);
610
+ } catch (error) {
611
+ throw new WorkspacePromotionError(
612
+ "stamp_failed",
613
+ {
614
+ operationId: request.operationId,
615
+ workspaceName: discovered.name,
616
+ },
617
+ error,
618
+ );
619
+ }
620
+ }
621
+ this.#assertActive(request.operationId);
622
+ if (this.#registeredSession(session.sessionName)?.name !== discovered.name) {
623
+ throw new WorkspacePromotionError("workspace_conflict", {
624
+ operationId: request.operationId,
625
+ workspaceName: discovered.name,
626
+ });
627
+ }
628
+ return this.#succeed(request, fingerprint, discovered.name, session.sessionName, {
629
+ replayed: true,
630
+ });
631
+ }
593
632
  let registered: Workspace;
594
633
  try {
595
634
  registered = this.#registry.add({
@@ -651,6 +690,16 @@ export class WorkspacePromotionAuthority {
651
690
  }
652
691
  }
653
692
 
693
+ #registeredSession(sessionName: string): Workspace | undefined {
694
+ const candidates = this.#registry
695
+ .list()
696
+ .filter((workspace) => workspace.sessionName === sessionName);
697
+ if (candidates.length > 1) {
698
+ throw new WorkspacePromotionError("workspace_conflict", { sessionName });
699
+ }
700
+ return candidates[0];
701
+ }
702
+
654
703
  #assertConflictFreeIdentity(identity: PromotionIdentity): void {
655
704
  for (const workspace of this.#registry.list()) {
656
705
  if (
@@ -757,6 +806,19 @@ export class WorkspacePromotionAuthority {
757
806
  });
758
807
  }
759
808
 
809
+ // list-panes -s repeats each backing pane once per window link. Stamp the
810
+ // backing only once, and reject contradictory observations before writes.
811
+ const uniqueScan = new Map<string, ScanPane>();
812
+ for (const pane of scanned) {
813
+ const previous = uniqueScan.get(pane.paneId);
814
+ if (previous && JSON.stringify(previous) !== JSON.stringify(pane)) {
815
+ throw new WorkspacePromotionError("promotion_verification_failed", {
816
+ reason: "inconsistent_tmux_topology",
817
+ });
818
+ }
819
+ uniqueScan.set(pane.paneId, pane);
820
+ }
821
+ scanned = [...uniqueScan.values()];
760
822
  const nowSec = Math.floor(this.#io.now() / 1000);
761
823
  const reconciledWindows = new Set<string>();
762
824
  try {
@@ -928,8 +990,32 @@ export class WorkspacePromotionAuthority {
928
990
  });
929
991
  }
930
992
 
993
+ // Validate links independently from their shared backing. Repeated native
994
+ // panes are legal only across distinct links with identical backing facts.
995
+ const links = new Map<number, VerifyPane[]>();
996
+ const backingPanes = new Map<string, Omit<VerifyPane, "windowIndex">>();
997
+ const semanticWindows = new Map<string, string>();
998
+ for (const pane of panes) {
999
+ const { windowIndex, ...backing } = pane;
1000
+ const previous = backingPanes.get(pane.paneId);
1001
+ const semanticOwner = semanticWindows.get(pane.semanticWindowId);
1002
+ const rows = links.get(windowIndex) ?? [];
1003
+ if (
1004
+ (previous && JSON.stringify(previous) !== JSON.stringify(backing)) ||
1005
+ (semanticOwner && semanticOwner !== pane.windowId) ||
1006
+ rows.some((row) => row.paneId === pane.paneId || row.windowId !== pane.windowId)
1007
+ ) {
1008
+ throw new WorkspacePromotionError("promotion_verification_failed", {
1009
+ reason: "inconsistent_tmux_topology",
1010
+ });
1011
+ }
1012
+ backingPanes.set(pane.paneId, backing);
1013
+ semanticWindows.set(pane.semanticWindowId, pane.windowId);
1014
+ rows.push(pane);
1015
+ links.set(windowIndex, rows);
1016
+ }
931
1017
  const catalog = analyzeTrustedSemanticPaneCatalog(
932
- panes.map((pane) => ({
1018
+ [...backingPanes.values()].map((pane) => ({
933
1019
  workspaceName: identity.workspaceName,
934
1020
  semanticPaneId: pane.semanticPaneId === "" ? null : pane.semanticPaneId,
935
1021
  sessionId: pane.sessionId,
@@ -950,17 +1036,17 @@ export class WorkspacePromotionAuthority {
950
1036
  });
951
1037
  }
952
1038
 
953
- const windows = new Map<string, VerifyPane[]>();
954
- for (const pane of panes) {
955
- const rows = windows.get(pane.windowId) ?? [];
1039
+ const backings = new Map<string, Omit<VerifyPane, "windowIndex">[]>();
1040
+ for (const pane of backingPanes.values()) {
1041
+ const rows = backings.get(pane.windowId) ?? [];
956
1042
  rows.push(pane);
957
- windows.set(pane.windowId, rows);
1043
+ backings.set(pane.windowId, rows);
958
1044
  }
959
1045
  const sessionWindowCount = panes[0]!.sessionWindowCount;
960
1046
  if (
961
- windows.size !== sessionWindowCount ||
1047
+ links.size !== sessionWindowCount ||
962
1048
  panes.some((pane) => pane.sessionWindowCount !== sessionWindowCount) ||
963
- [...windows.values()].some(
1049
+ [...links.values(), ...backings.values()].some(
964
1050
  (rows) =>
965
1051
  rows.length !== rows[0]!.windowPaneCount ||
966
1052
  rows.some(
@@ -173,6 +173,11 @@ export class WorkspaceRegistry {
173
173
  return this.workspaces.some((w) => w.name === name);
174
174
  }
175
175
 
176
+ /** Whether this entry is discovered live state, never durable user intent. */
177
+ isVolatile(name: string): boolean {
178
+ return this.volatileNames.has(name);
179
+ }
180
+
176
181
  add(input: AddWorkspaceInput): Workspace {
177
182
  if (this.has(input.name)) {
178
183
  throw new WorkspaceAlreadyExistsError(input.name);
@@ -33,9 +33,11 @@ function rejectUpgrade(socket: Socket, status: number): void {
33
33
  ? "Forbidden"
34
34
  : status === 404
35
35
  ? "Not Found"
36
- : status === 426
37
- ? "Upgrade Required"
38
- : "Service Unavailable";
36
+ : status === 410
37
+ ? "Gone"
38
+ : status === 426
39
+ ? "Upgrade Required"
40
+ : "Service Unavailable";
39
41
  try {
40
42
  socket.end(`HTTP/1.1 ${status} ${phrase}\r\nConnection: close\r\nContent-Length: 0\r\n\r\n`);
41
43
  } catch {
@@ -56,6 +58,8 @@ export interface PaneStreamWebSocketBoundary {
56
58
  export function attachPaneStreamWebSocket(
57
59
  server: Server,
58
60
  coordinator: PaneStreamAdmissionCoordinator,
61
+ redemptionPath: string = PANE_STREAM_REDEEM_PATH,
62
+ isCurrent: () => boolean = () => true,
59
63
  ): PaneStreamWebSocketBoundary {
60
64
  const wss = new WebSocketServer({
61
65
  noServer: true,
@@ -77,8 +81,20 @@ export function attachPaneStreamWebSocket(
77
81
  ): void => {
78
82
  const rawPath = request.url ?? "";
79
83
  const pathname = rawPath.split("?", 1)[0] ?? "";
80
- if (!pathname.startsWith("/v1/terminal/pane-streams/")) return;
81
- if (pathname !== PANE_STREAM_REDEEM_PATH) {
84
+ if (
85
+ redemptionPath === PANE_STREAM_REDEEM_PATH &&
86
+ pathname.startsWith("/v1/terminal/pane-streams/")
87
+ ) {
88
+ rejectUpgrade(socket, 426);
89
+ return;
90
+ }
91
+ if (redemptionPath !== PANE_STREAM_REDEEM_PATH && pathname !== redemptionPath) return;
92
+ if (
93
+ redemptionPath === PANE_STREAM_REDEEM_PATH &&
94
+ !pathname.startsWith("/v2/terminal/pane-streams/")
95
+ )
96
+ return;
97
+ if (pathname !== redemptionPath) {
82
98
  rejectUpgrade(socket, 404);
83
99
  return;
84
100
  }
@@ -102,6 +118,10 @@ export function attachPaneStreamWebSocket(
102
118
  rejectUpgrade(socket, 403);
103
119
  return;
104
120
  }
121
+ if (!isCurrent()) {
122
+ rejectUpgrade(socket, 410);
123
+ return;
124
+ }
105
125
  const decision = coordinator.reserveUpgrade({
106
126
  path: rawPath,
107
127
  protocols: protocols(protocolHeaders[0]),