tmux-ide 2.9.2 → 2.9.3
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.
- package/README.md +1 -1
- package/bin/cli.js +2071 -625
- package/package.json +14 -5
- package/packages/contracts/src/desktop-host.ts +3 -0
- package/packages/contracts/src/saved-machines.ts +19 -0
- package/packages/contracts/src/terminal-replica.ts +1 -1
- package/packages/core/package.json +3 -2
- package/packages/core/src/terminal-delivery.ts +51 -3
- package/packages/core/src/terminal-fnv64-wasm.ts +4 -3
- package/packages/core/src/terminal-replica-hash-cache.ts +129 -31
- package/packages/core/src/terminal-replica-owned-row.ts +27 -0
- package/packages/core/src/terminal-replica.ts +13 -28
- package/packages/daemon/dist/command-center/daemon-fleet-facts-observer.js +96 -10
- package/packages/daemon/dist/command-center/resources/saved-machine-route.js +40 -1
- package/packages/daemon/dist/command-center/ws-events.js +5 -24
- package/packages/daemon/dist/lib/canonical-daemon-bootstrap.js +8 -0
- package/packages/daemon/dist/lib/canonical-daemon.js +28 -2
- package/packages/daemon/dist/lib/daemon-embed.js +1 -1
- package/packages/daemon/dist/lib/daemon-service-manager.js +122 -0
- package/packages/daemon/dist/lib/daemon-service-plan.js +127 -0
- package/packages/daemon/dist/lib/daemon-service.js +259 -0
- package/packages/daemon/dist/lib/install-origin.js +2 -0
- package/packages/daemon/dist/lib/local-fleet-request.js +9 -3
- package/packages/daemon/dist/lib/log.js +6 -10
- package/packages/daemon/dist/lib/managed-installer-update.js +67 -0
- package/packages/daemon/dist/lib/{tui-download-lock.js → private-operation-lock.js} +11 -11
- package/packages/daemon/dist/lib/remote-daemon-info.js +3 -2
- package/packages/daemon/dist/lib/remote-daemon-lifecycle.js +4 -4
- package/packages/daemon/dist/lib/remote-tmux-command.js +10 -0
- package/packages/daemon/dist/lib/runtime-namespace.js +28 -16
- package/packages/daemon/dist/lib/saved-machines.js +40 -2
- package/packages/daemon/dist/lib/ssh-daemon-transport.js +24 -9
- package/packages/daemon/dist/lib/tmux-server-owner.js +1 -1
- package/packages/daemon/dist/lib/tmux-server-registration.js +21 -3
- package/packages/daemon/dist/lib/tui-binary.js +2 -2
- package/packages/daemon/dist/lib/update.js +18 -0
- package/packages/daemon/dist/lib/workspace-multiplexer-verbs.js +24 -1
- package/packages/daemon/dist/machines.js +40 -4
- package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/Info.plist +2 -2
- package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/MacOS/tmux-ide-notifier +0 -0
- package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/Resources/Assets.car +0 -0
- package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/_CodeSignature/CodeResources +2 -2
- package/packages/daemon/dist/terminal/attachments/direct-websocket.js +4 -1
- package/packages/daemon/dist/terminal/attachments/native-runtime.js +22 -13
- package/packages/daemon/dist/terminal/mirror/mirror-service.js +101 -3
- package/packages/daemon/dist/terminal/mirror/registered-window-guard.js +62 -0
- package/packages/daemon/dist/terminal/mirror/session-channel.js +75 -4
- package/packages/daemon/dist/terminal/mirror/shared-window-index.js +45 -0
- package/packages/daemon/dist/terminal/pane-stream/pane-stream-websocket.js +11 -1
- package/packages/daemon/dist/terminal/session-runtime/multiplexer-backend.js +1 -0
- package/packages/daemon/dist/terminal/session-runtime/registry.js +56 -7
- package/packages/daemon/dist/terminal/session-runtime/semantic-mutation-executor.js +3 -1
- package/packages/daemon/dist/tui/mirror/dialog-model.js +2 -35
- package/packages/daemon/dist/tui/mirror/runtime/application-daemon-authority-owner.js +8 -0
- package/packages/daemon/dist/tui/mirror/runtime/application-home-fleet.js +20 -2
- package/packages/daemon/dist/tui/mirror/runtime/application-machine-agents.js +7 -1
- package/packages/daemon/dist/tui/mirror/runtime/application-machine-authority.js +59 -9
- package/packages/daemon/dist/tui/mirror/runtime/application-machine-navigation.js +27 -19
- package/packages/daemon/dist/tui/mirror/runtime/application-machine-sidebar.jsx +23 -18
- package/packages/daemon/dist/tui/mirror/runtime/application-machine-startup.js +31 -2
- package/packages/daemon/dist/tui/mirror/runtime/application-palette-search-owner.js +7 -3
- package/packages/daemon/dist/tui/mirror/runtime/application-performance-log.js +3 -0
- package/packages/daemon/dist/tui/mirror/runtime/application-terminal-input-ingress.js +146 -2
- package/packages/daemon/dist/tui/mirror/runtime/application-terminal-interaction-controller.js +201 -50
- package/packages/daemon/dist/tui/mirror/runtime/bounded-performance-record-writer.js +50 -10
- package/packages/daemon/dist/tui/mirror/runtime/terminal-fast-lane-renderer-adapter.js +6 -5
- package/packages/daemon/dist/tui/mirror/runtime/terminal-pane-input-router.js +3 -0
- package/packages/daemon/dist/tui/mirror/runtime/workspace-terminal-fast-lane.js +8 -2
- package/packages/daemon/dist/tui/mirror/terminal-text.js +35 -0
- package/packages/daemon/dist/tui/mirror/workspace/connection-feedback.js +5 -3
- package/packages/daemon/src/command-center/daemon-fleet-facts-observer.ts +100 -10
- package/packages/daemon/src/command-center/resources/saved-machine-route.ts +48 -1
- package/packages/daemon/src/command-center/ws-events.ts +5 -25
- package/packages/daemon/src/lib/canonical-daemon-bootstrap.ts +12 -0
- package/packages/daemon/src/lib/canonical-daemon.ts +44 -2
- package/packages/daemon/src/lib/daemon-embed.ts +1 -0
- package/packages/daemon/src/lib/daemon-service-manager.ts +152 -0
- package/packages/daemon/src/lib/daemon-service-plan.ts +153 -0
- package/packages/daemon/src/lib/daemon-service.ts +301 -0
- package/packages/daemon/src/lib/install-origin.ts +5 -0
- package/packages/daemon/src/lib/local-fleet-request.ts +19 -3
- package/packages/daemon/src/lib/log.ts +13 -9
- package/packages/daemon/src/lib/managed-installer-update.ts +79 -0
- package/packages/daemon/src/lib/{tui-download-lock.ts → private-operation-lock.ts} +15 -11
- package/packages/daemon/src/lib/remote-daemon-info.ts +3 -3
- package/packages/daemon/src/lib/remote-daemon-lifecycle.ts +4 -4
- package/packages/daemon/src/lib/remote-tmux-command.ts +12 -0
- package/packages/daemon/src/lib/runtime-namespace.ts +27 -18
- package/packages/daemon/src/lib/saved-machines.ts +40 -1
- package/packages/daemon/src/lib/ssh-daemon-transport.ts +25 -7
- package/packages/daemon/src/lib/tmux-server-owner.ts +1 -0
- package/packages/daemon/src/lib/tmux-server-registration.ts +21 -3
- package/packages/daemon/src/lib/tui-binary.ts +2 -2
- package/packages/daemon/src/lib/update.ts +24 -0
- package/packages/daemon/src/lib/workspace-multiplexer-verbs.ts +22 -0
- package/packages/daemon/src/machines.ts +53 -5
- package/packages/daemon/src/terminal/attachments/direct-websocket.ts +8 -2
- package/packages/daemon/src/terminal/attachments/native-runtime.ts +28 -13
- package/packages/daemon/src/terminal/mirror/mirror-service.ts +105 -3
- package/packages/daemon/src/terminal/mirror/registered-window-guard.ts +66 -0
- package/packages/daemon/src/terminal/mirror/session-channel.ts +92 -4
- package/packages/daemon/src/terminal/mirror/shared-window-index.ts +42 -0
- package/packages/daemon/src/terminal/pane-stream/pane-stream-websocket.ts +12 -1
- package/packages/daemon/src/terminal/session-runtime/multiplexer-backend.ts +2 -0
- package/packages/daemon/src/terminal/session-runtime/registry.ts +70 -8
- package/packages/daemon/src/terminal/session-runtime/semantic-mutation-executor.ts +3 -1
- package/packages/daemon/src/tui/mirror/dialog-model.ts +2 -32
- package/packages/daemon/src/tui/mirror/runtime/application-daemon-authority-owner.ts +8 -0
- package/packages/daemon/src/tui/mirror/runtime/application-home-fleet.ts +19 -2
- package/packages/daemon/src/tui/mirror/runtime/application-machine-agents.ts +6 -1
- package/packages/daemon/src/tui/mirror/runtime/application-machine-authority.ts +60 -11
- package/packages/daemon/src/tui/mirror/runtime/application-machine-navigation.ts +39 -33
- package/packages/daemon/src/tui/mirror/runtime/application-machine-sidebar.tsx +31 -23
- package/packages/daemon/src/tui/mirror/runtime/application-machine-startup.ts +38 -2
- package/packages/daemon/src/tui/mirror/runtime/application-palette-search-owner.ts +7 -4
- package/packages/daemon/src/tui/mirror/runtime/application-performance-log.ts +3 -0
- package/packages/daemon/src/tui/mirror/runtime/application-root-v2.tsx +1 -1
- package/packages/daemon/src/tui/mirror/runtime/application-shell-view.tsx +47 -46
- package/packages/daemon/src/tui/mirror/runtime/application-terminal-input-ingress.ts +165 -2
- package/packages/daemon/src/tui/mirror/runtime/application-terminal-interaction-controller.ts +207 -53
- package/packages/daemon/src/tui/mirror/runtime/bounded-performance-record-writer.ts +49 -9
- package/packages/daemon/src/tui/mirror/runtime/terminal-fast-lane-renderer-adapter.ts +6 -5
- package/packages/daemon/src/tui/mirror/runtime/terminal-pane-input-router.ts +4 -0
- package/packages/daemon/src/tui/mirror/runtime/workspace-terminal-fast-lane.ts +8 -3
- package/packages/daemon/src/tui/mirror/terminal-text.ts +32 -0
- package/packages/daemon/src/tui/mirror/workspace/connection-feedback.ts +6 -3
- package/packages/daemon-client/src/fleet-connection-status.ts +10 -1
- package/packages/daemon-client/src/workspace-client.ts +4 -1
- package/packages/contracts/src/__tests__/actions-contract.test.ts +0 -119
- package/packages/contracts/src/__tests__/agent-graph-overlay.test.ts +0 -428
- package/packages/contracts/src/__tests__/app-window-mutation.test.ts +0 -51
- package/packages/contracts/src/__tests__/app-window-state.test.ts +0 -34
- package/packages/contracts/src/__tests__/application-shell.test.ts +0 -645
- package/packages/contracts/src/__tests__/automation-operations.test.ts +0 -78
- package/packages/contracts/src/__tests__/causal-cell.test.ts +0 -47
- package/packages/contracts/src/__tests__/client-view-state.test.ts +0 -115
- package/packages/contracts/src/__tests__/cohesion-fixture.test.ts +0 -106
- package/packages/contracts/src/__tests__/commands.test.ts +0 -86
- package/packages/contracts/src/__tests__/control.test.ts +0 -154
- package/packages/contracts/src/__tests__/daemon-events.test.ts +0 -405
- package/packages/contracts/src/__tests__/daemon-resources.test.ts +0 -143
- package/packages/contracts/src/__tests__/daemon-wire.test.ts +0 -149
- package/packages/contracts/src/__tests__/desktop-host.test.ts +0 -269
- package/packages/contracts/src/__tests__/desktop-missions.test.ts +0 -215
- package/packages/contracts/src/__tests__/experience-conformance.test.ts +0 -153
- package/packages/contracts/src/__tests__/experience-kernel-boundary.test.ts +0 -113
- package/packages/contracts/src/__tests__/experience-shell.test.ts +0 -65
- package/packages/contracts/src/__tests__/fixtures/daemon-wire-v2.json +0 -35
- package/packages/contracts/src/__tests__/fixtures/daemon-wire-v3.json +0 -35
- package/packages/contracts/src/__tests__/fixtures/daemon-wire-v4.json +0 -35
- package/packages/contracts/src/__tests__/fixtures/scoped-receipt.ts +0 -62
- package/packages/contracts/src/__tests__/fleet-agent-graph.test.ts +0 -231
- package/packages/contracts/src/__tests__/fleet-catalog.test.ts +0 -270
- package/packages/contracts/src/__tests__/fleet-lifecycle.test.ts +0 -62
- package/packages/contracts/src/__tests__/focus-overlay.test.ts +0 -110
- package/packages/contracts/src/__tests__/ide-config.test.ts +0 -64
- package/packages/contracts/src/__tests__/interaction-evidence.test.ts +0 -288
- package/packages/contracts/src/__tests__/interaction-receipt-v2.test.ts +0 -72
- package/packages/contracts/src/__tests__/issue-error.test.ts +0 -78
- package/packages/contracts/src/__tests__/mission-projections.test.ts +0 -186
- package/packages/contracts/src/__tests__/missions.test.ts +0 -272
- package/packages/contracts/src/__tests__/multiplexer-verbs.test.ts +0 -242
- package/packages/contracts/src/__tests__/pane-appearance.test.ts +0 -146
- package/packages/contracts/src/__tests__/pane-read-result.test.ts +0 -55
- package/packages/contracts/src/__tests__/pane-stream.test.ts +0 -536
- package/packages/contracts/src/__tests__/pane-widget-marker.test.ts +0 -327
- package/packages/contracts/src/__tests__/performance-metrics.test.ts +0 -73
- package/packages/contracts/src/__tests__/performance-qualification.test.ts +0 -137
- package/packages/contracts/src/__tests__/rich-card-widget.test.ts +0 -29
- package/packages/contracts/src/__tests__/session-runtime.test.ts +0 -512
- package/packages/contracts/src/__tests__/startup-readiness.test.ts +0 -409
- package/packages/contracts/src/__tests__/terminal-attachment-stream.test.ts +0 -72
- package/packages/contracts/src/__tests__/terminal-attachments.test.ts +0 -329
- package/packages/contracts/src/__tests__/terminal-delivery.test.ts +0 -88
- package/packages/contracts/src/__tests__/terminal-runtime-inventory.test.ts +0 -68
- package/packages/contracts/src/__tests__/terminals.test.ts +0 -87
- package/packages/contracts/src/__tests__/tmux-server-scope.test.ts +0 -166
- package/packages/contracts/src/__tests__/transport-contract-loading.test.ts +0 -91
- package/packages/contracts/src/__tests__/visual-tokens.test.ts +0 -224
- package/packages/contracts/src/__tests__/window-links.test.ts +0 -243
- package/packages/contracts/src/__tests__/workspace-changes-resource.test.ts +0 -317
- package/packages/contracts/src/__tests__/workspace-changes-view.test.ts +0 -122
- package/packages/contracts/src/__tests__/workspace-config.test.ts +0 -294
- package/packages/contracts/src/__tests__/workspace-files-resource.test.ts +0 -261
- package/packages/contracts/src/__tests__/workspace-files-tree.test.ts +0 -147
- package/packages/contracts/src/__tests__/workspace-pane-creation.test.ts +0 -249
- package/packages/contracts/src/__tests__/workspace-promotion.test.ts +0 -94
- package/packages/contracts/src/__tests__/workspace-resource-identity.test.ts +0 -90
- package/packages/contracts/src/daemon-resource-request.test.ts +0 -192
- package/packages/contracts/src/native-operation-identity.test.ts +0 -23
- package/packages/contracts/src/workspace-admission.test.ts +0 -21
- package/packages/contracts/src/workspace-catalog-resource.test.ts +0 -146
- package/packages/core/src/agent-provisioning.test.ts +0 -42
- package/packages/core/src/application-shell-session.test.ts +0 -105
- package/packages/core/src/interaction-evidence.test.ts +0 -134
- package/packages/core/src/interaction-flow.test.ts +0 -66
- package/packages/core/src/interaction-receipts.test.ts +0 -671
- package/packages/core/src/navigator.test.ts +0 -34
- package/packages/core/src/optimistic-projection-conformance.test.ts +0 -75
- package/packages/core/src/optimistic-projection.test.ts +0 -118
- package/packages/core/src/performance-metrics.test.ts +0 -121
- package/packages/core/src/performance-qualification.test.ts +0 -283
- package/packages/core/src/saved-machines.test.ts +0 -80
- package/packages/core/src/terminal-compact-cold.test.ts +0 -87
- package/packages/core/src/terminal-conformance.test.ts +0 -48
- package/packages/core/src/terminal-delivery-cache-lifetime.test.ts +0 -167
- package/packages/core/src/terminal-delivery-encode-cooperative.test.ts +0 -140
- package/packages/core/src/terminal-delivery-representation-hash.test.ts +0 -77
- package/packages/core/src/terminal-delivery.test.ts +0 -1471
- package/packages/core/src/terminal-fnv64-wasm.test.ts +0 -61
- package/packages/core/src/terminal-replica-cold.test.ts +0 -31
- package/packages/core/src/terminal-replica-cooperative.test.ts +0 -170
- package/packages/core/src/terminal-replica-hash-cache.test.ts +0 -283
- package/packages/core/src/terminal-replica.test.ts +0 -347
- package/packages/core/src/workspace-selection.test.ts +0 -63
- package/packages/daemon/dist/__tests__/support.js +0 -33
- package/packages/daemon/dist/lib/__tests__/fixtures/workspace-state-writer.js +0 -58
- package/packages/daemon/dist/lib/__tests__/installed-recovery-fixture.js +0 -400
- package/packages/daemon/dist/terminal/__tests__/MockPtyAdapter.js +0 -182
- package/packages/daemon/dist/terminal/mirror/__tests__/scripted-channel.js +0 -98
- package/packages/daemon/dist/terminal/mirror/__tests__/simulated-channel.js +0 -156
- package/packages/daemon/src/tui/mirror/__snapshots__/activity-surface-renderer.test.tsx.snap +0 -42
- package/packages/daemon/src/tui/mirror/__snapshots__/changes-terminal-surface-renderer.test.tsx.snap +0 -267
- package/packages/daemon/src/tui/mirror/__snapshots__/home-files-surface-renderer.test.tsx.snap +0 -267
- package/packages/daemon/src/tui/mirror/__snapshots__/missions-surface-renderer.test.tsx.snap +0 -134
- package/packages/daemon/src/tui/mirror/__snapshots__/recipes-gallery-renderer.test.tsx.snap +0 -141
- package/packages/daemon/src/tui/mirror/__snapshots__/shell-chrome-renderer.test.tsx.snap +0 -134
- package/packages/daemon/src/tui/mirror/runtime/__snapshots__/application-shell-view-renderer.test.tsx.snap +0 -87
- package/packages/daemon/src/tui/mirror/workspace/__snapshots__/agent-terminal-canvas-renderer.test.tsx.snap +0 -348
- package/packages/daemon/src/tui/mirror/workspace/__snapshots__/application-shell-renderer.test.tsx.snap +0 -134
- package/packages/daemon/src/tui/mirror/workspace/__snapshots__/command-palette-surface-renderer.test.tsx.snap +0 -255
- package/packages/daemon/src/tui/mirror/workspace/__snapshots__/pane-frame-renderer.test.tsx.snap +0 -525
- package/packages/daemon/src/tui/mirror/workspace/__snapshots__/terminal-pane-header-polish-renderer.test.tsx.snap +0 -31
- package/packages/daemon/src/tui/mirror/workspace/__snapshots__/workbench-shell-renderer.test.tsx.snap +0 -134
- package/packages/daemon-client/src/application-shell-session.test.ts +0 -196
- package/packages/daemon-client/src/automation-client.test.ts +0 -214
- package/packages/daemon-client/src/bootstrap-coordinator.test.ts +0 -145
- package/packages/daemon-client/src/connection-supervisor.test.ts +0 -109
- package/packages/daemon-client/src/direct-application-shell-transport.test.ts +0 -941
- package/packages/daemon-client/src/first-latest-coordinator.test.ts +0 -175
- package/packages/daemon-client/src/fleet-dial-scheduler.test.ts +0 -114
- package/packages/daemon-client/src/fleet-environments.test.ts +0 -40
- package/packages/daemon-client/src/generation-bound-store-timers.test.ts +0 -74
- package/packages/daemon-client/src/owner-action-client.test.ts +0 -245
- package/packages/daemon-client/src/pane-stream-client.test.ts +0 -2841
- package/packages/daemon-client/src/pane-stream-clock-calibration.test.ts +0 -207
- package/packages/daemon-client/src/pane-stream-loopback-benchmark.test.ts +0 -51
- package/packages/daemon-client/src/push-resource-session.test.ts +0 -509
- package/packages/daemon-client/src/resource-replica.test.ts +0 -186
- package/packages/daemon-client/src/runtime-resource-ledger.test.ts +0 -18
- package/packages/daemon-client/src/scoped-tmux-server-transport.test.ts +0 -114
- package/packages/daemon-client/src/terminal-fast-lane.test.ts +0 -835
- package/packages/daemon-client/src/tmux-server-client.test.ts +0 -279
- package/packages/daemon-client/src/tmux-server-interaction-events.test.ts +0 -230
- package/packages/daemon-client/src/workspace-catalog-v2.test.ts +0 -109
- package/packages/daemon-client/src/workspace-client-boundaries.test.ts +0 -59
- package/packages/daemon-client/src/workspace-client-operations.test.ts +0 -96
- package/packages/daemon-client/src/workspace-client.test.ts +0 -2912
- package/packages/daemon-client/src/workspace-event-supervisor.test.ts +0 -1086
- package/packages/daemon-client/src/workspace-pane-client.test.ts +0 -80
- package/packages/presentation/src/presentation-contract.test.ts +0 -63
- package/packages/tmux-bridge/src/runner.test.ts +0 -614
- package/packages/tmux-bridge/src/targeting.test.ts +0 -107
|
@@ -468,6 +468,7 @@ export class WorkspaceMultiplexerAuthority {
|
|
|
468
468
|
session: string,
|
|
469
469
|
action: AuthoritativeWindowLinkAction,
|
|
470
470
|
) => Promise<AuthoritativeWindowLinkActionResult>,
|
|
471
|
+
timing?: WorkspaceMultiplexerOperationTiming,
|
|
471
472
|
): Promise<WorkspaceMultiplexerMutationResult> {
|
|
472
473
|
if (this.#disposed) throw new WorkspaceMultiplexerError("workspace_unavailable");
|
|
473
474
|
const request = WorkspaceMultiplexerMutationRequestSchemaZ.parse(raw);
|
|
@@ -480,7 +481,26 @@ export class WorkspaceMultiplexerAuthority {
|
|
|
480
481
|
intent.verb !== "workspace.pane.select"
|
|
481
482
|
)
|
|
482
483
|
throw new TypeError("Expected a window-link selection or unlink intent");
|
|
484
|
+
const now = () => {
|
|
485
|
+
try {
|
|
486
|
+
return timing?.nowMicros() ?? null;
|
|
487
|
+
} catch {
|
|
488
|
+
return null;
|
|
489
|
+
}
|
|
490
|
+
};
|
|
491
|
+
const record = (operation: string, start: number | null) => {
|
|
492
|
+
const end = now();
|
|
493
|
+
if (start !== null && end !== null) {
|
|
494
|
+
try {
|
|
495
|
+
timing?.record(operation, start, end);
|
|
496
|
+
} catch {
|
|
497
|
+
/* Diagnostics cannot own selection. */
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
};
|
|
501
|
+
const lookupStarted = now();
|
|
483
502
|
const workspace = this.#registry.get(intent.workspaceName);
|
|
503
|
+
record("semantic-workspace-lookup", lookupStarted);
|
|
484
504
|
if (!workspace) throw new WorkspaceMultiplexerError("workspace_not_found");
|
|
485
505
|
const envelope = {
|
|
486
506
|
operationId: request.operationId,
|
|
@@ -488,6 +508,7 @@ export class WorkspaceMultiplexerAuthority {
|
|
|
488
508
|
workspaceName: intent.workspaceName,
|
|
489
509
|
};
|
|
490
510
|
try {
|
|
511
|
+
const effectStarted = now();
|
|
491
512
|
const result = await execute(
|
|
492
513
|
workspace.sessionName,
|
|
493
514
|
intent.verb === "workspace.pane.select"
|
|
@@ -501,6 +522,7 @@ export class WorkspaceMultiplexerAuthority {
|
|
|
501
522
|
target: intent.target,
|
|
502
523
|
},
|
|
503
524
|
);
|
|
525
|
+
record("window-link-effect-proof", effectStarted);
|
|
504
526
|
if (result.outcome !== "applied") {
|
|
505
527
|
throw new WorkspaceMultiplexerError(
|
|
506
528
|
result.outcome === "stale"
|
|
@@ -1,22 +1,70 @@
|
|
|
1
1
|
import { readFileSync, statSync } from "node:fs";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
SavedMachineRegistrySchema,
|
|
5
|
+
SavedMachineSchema,
|
|
6
|
+
SavedMachineMutationSchema,
|
|
7
|
+
} from "@tmux-ide/contracts/saved-machines";
|
|
4
8
|
import { loadSavedMachines, planSavedMachineMerge } from "./lib/saved-machines.ts";
|
|
5
9
|
import { loadFleetClientState } from "./lib/fleet-client-state.ts";
|
|
6
|
-
import {
|
|
10
|
+
import { changeSavedMachines } from "@tmux-ide/core";
|
|
11
|
+
import { saveMachineProfiles, mutateMachineProfile } from "./lib/local-fleet-request.ts";
|
|
12
|
+
import { remoteTmuxIdeCommand } from "./lib/remote-tmux-command.ts";
|
|
7
13
|
|
|
8
14
|
export async function machines(
|
|
9
15
|
command: string | undefined,
|
|
10
16
|
argument: string | undefined,
|
|
11
|
-
options: { write?: boolean; label?: string },
|
|
17
|
+
options: { write?: boolean; label?: string; sshTarget?: string },
|
|
12
18
|
) {
|
|
13
19
|
const current = loadSavedMachines();
|
|
20
|
+
if (
|
|
21
|
+
(command === "enable" || command === "disable" || command === "remove" || command === "edit") &&
|
|
22
|
+
argument
|
|
23
|
+
) {
|
|
24
|
+
const key = argument.normalize("NFKC").toLowerCase();
|
|
25
|
+
const match =
|
|
26
|
+
current.machines.find((machine) => machine.id === key) ??
|
|
27
|
+
current.machines.find((machine) => machine.label.normalize("NFKC").toLowerCase() === key);
|
|
28
|
+
if (!match)
|
|
29
|
+
throw new Error("Saved machine not found; use machines ls to select its ID or label.");
|
|
30
|
+
const change = SavedMachineMutationSchema.parse(
|
|
31
|
+
command === "edit"
|
|
32
|
+
? {
|
|
33
|
+
id: match.id,
|
|
34
|
+
operation: command,
|
|
35
|
+
patch: {
|
|
36
|
+
...(options.label !== undefined ? { label: options.label } : {}),
|
|
37
|
+
...(options.sshTarget !== undefined ? { sshTarget: options.sshTarget } : {}),
|
|
38
|
+
},
|
|
39
|
+
}
|
|
40
|
+
: { id: match.id, operation: command },
|
|
41
|
+
);
|
|
42
|
+
const preview = changeSavedMachines(
|
|
43
|
+
current,
|
|
44
|
+
command === "remove"
|
|
45
|
+
? { type: "remove", id: match.id }
|
|
46
|
+
: {
|
|
47
|
+
type: "update",
|
|
48
|
+
id: match.id,
|
|
49
|
+
patch: change.operation === "edit" ? change.patch : { enabled: command === "enable" },
|
|
50
|
+
},
|
|
51
|
+
);
|
|
52
|
+
return {
|
|
53
|
+
written: options.write === true,
|
|
54
|
+
registry: options.write ? await mutateMachineProfile(change) : preview,
|
|
55
|
+
...(!options.write
|
|
56
|
+
? {
|
|
57
|
+
note: "Repeat with --write to change this saved profile. Remote sessions and daemons are preserved.",
|
|
58
|
+
}
|
|
59
|
+
: {}),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
14
62
|
if (command === "start" && argument) {
|
|
15
63
|
SavedMachineSchema.shape.sshTarget.parse(argument);
|
|
16
64
|
if (!options.write)
|
|
17
65
|
return {
|
|
18
66
|
written: false,
|
|
19
|
-
command: ["ssh", "--", argument, "
|
|
67
|
+
command: ["ssh", "--", argument, remoteTmuxIdeCommand("start")],
|
|
20
68
|
note: "Repeat with --write to start the installed daemon. No package installation occurs.",
|
|
21
69
|
};
|
|
22
70
|
await (await import("./lib/remote-daemon-lifecycle.ts")).startInstalledRemoteDaemon(argument);
|
|
@@ -74,7 +122,7 @@ export async function machines(
|
|
|
74
122
|
};
|
|
75
123
|
} else
|
|
76
124
|
throw new Error(
|
|
77
|
-
"Usage: tmux-ide machines ls|export|import <file>|add <alias> [--write] [--json]",
|
|
125
|
+
"Usage: tmux-ide machines ls|export|import <file>|add <alias>|edit|enable|disable|remove <id-or-label> [--write] [--json]",
|
|
78
126
|
);
|
|
79
127
|
const preview = planSavedMachineMerge(current, incoming);
|
|
80
128
|
if (!options.write)
|
|
@@ -210,7 +210,7 @@ export interface TerminalAttachmentAdmissionCoordinatorOptions {
|
|
|
210
210
|
/** Trusted redemption boundary into the generation's SessionRuntime owner. */
|
|
211
211
|
readonly bindSessionRuntime?: (
|
|
212
212
|
descriptor: AttachmentLeaseDescriptor,
|
|
213
|
-
) => SessionRuntimeRedeemedTransportBinding
|
|
213
|
+
) => SessionRuntimeRedeemedTransportBinding | Promise<SessionRuntimeRedeemedTransportBinding>;
|
|
214
214
|
readonly maxPendingTickets?: number;
|
|
215
215
|
readonly maxPreAuthSockets?: number;
|
|
216
216
|
readonly maxLiveConnections?: number;
|
|
@@ -765,7 +765,13 @@ export class TerminalAttachmentAdmissionCoordinator {
|
|
|
765
765
|
let sessionRuntimeBinding: SessionRuntimeRedeemedTransportBinding | null = null;
|
|
766
766
|
let live: TerminalAttachmentLiveConnection;
|
|
767
767
|
try {
|
|
768
|
-
sessionRuntimeBinding = this.#bindSessionRuntime?.(activeDescriptor) ?? null;
|
|
768
|
+
sessionRuntimeBinding = (await this.#bindSessionRuntime?.(activeDescriptor)) ?? null;
|
|
769
|
+
if (!admission.isOpen()) {
|
|
770
|
+
throw new TerminalAttachmentAdmissionError(
|
|
771
|
+
"redemption-rejected",
|
|
772
|
+
"Terminal attachment redemption was rejected.",
|
|
773
|
+
);
|
|
774
|
+
}
|
|
769
775
|
live = new TerminalAttachmentLiveConnection({
|
|
770
776
|
onRetire: (connection) => this.#trackRetiringRelease(connection),
|
|
771
777
|
socket,
|
|
@@ -1307,6 +1307,7 @@ export class WorkspaceTerminalInventoryRuntime {
|
|
|
1307
1307
|
}
|
|
1308
1308
|
>();
|
|
1309
1309
|
#lifecycle: "initializing" | "ready" | "failed" | "disposed" = "initializing";
|
|
1310
|
+
#registeredWindows: { invalidate(): void; dispose(): void } | undefined;
|
|
1310
1311
|
#disposed = false;
|
|
1311
1312
|
|
|
1312
1313
|
constructor(options: WorkspaceTerminalInventoryRuntimeOptions) {
|
|
@@ -1318,6 +1319,16 @@ export class WorkspaceTerminalInventoryRuntime {
|
|
|
1318
1319
|
});
|
|
1319
1320
|
this.readRunner = pinnedReadRunner(authority, executeRead);
|
|
1320
1321
|
this.#registry = options.registry;
|
|
1322
|
+
this.#registeredWindows = options.sessionRuntimeRegistry?.setRegisteredWindowReader?.(
|
|
1323
|
+
async (signal) => {
|
|
1324
|
+
const snapshot = await discoverWorkspaceRegistryTerminalInventory(
|
|
1325
|
+
this.#registry,
|
|
1326
|
+
this.readRunner,
|
|
1327
|
+
signal,
|
|
1328
|
+
);
|
|
1329
|
+
return snapshot.panes.map((pane) => ({ session: pane.sessionName, window: pane.windowId }));
|
|
1330
|
+
},
|
|
1331
|
+
);
|
|
1321
1332
|
this.#observability = options.observability ?? DISABLED_SESSION_RUNTIME_OBSERVABILITY;
|
|
1322
1333
|
this.#resolveInteractionEndpoint = options.resolveInteractionEndpoint;
|
|
1323
1334
|
this.#nativeServerEpoch = options.nativeServerEpoch;
|
|
@@ -1441,10 +1452,12 @@ export class WorkspaceTerminalInventoryRuntime {
|
|
|
1441
1452
|
this.#observeWorkspaceSession = null;
|
|
1442
1453
|
}
|
|
1443
1454
|
this.#stopWorkspaceAddedObserver = options.registry.on("workspace.added", (workspace) => {
|
|
1455
|
+
this.#registeredWindows?.invalidate();
|
|
1444
1456
|
this.invalidate();
|
|
1445
1457
|
this.#observeWorkspaceSession?.(workspace.name, workspace.sessionName);
|
|
1446
1458
|
});
|
|
1447
1459
|
this.#stopWorkspaceRemovedObserver = options.registry.on("workspace.removed", (name) => {
|
|
1460
|
+
this.#registeredWindows?.invalidate();
|
|
1448
1461
|
this.invalidate();
|
|
1449
1462
|
if (!options.sessionRuntimeRegistry) return;
|
|
1450
1463
|
const sessionName = sessionsByWorkspace.get(name);
|
|
@@ -1749,12 +1762,12 @@ export class WorkspaceTerminalInventoryRuntime {
|
|
|
1749
1762
|
let catalogIssue: NativeTerminalInventoryCatalogIssue | null = inventory.catalog
|
|
1750
1763
|
.invalidRuntimeProof
|
|
1751
1764
|
? "invalid-runtime-proof"
|
|
1752
|
-
: inventory.catalog.
|
|
1753
|
-
? "
|
|
1754
|
-
: inventory.catalog.
|
|
1755
|
-
? "
|
|
1756
|
-
: inventory.catalog.
|
|
1757
|
-
? "duplicate-
|
|
1765
|
+
: inventory.catalog.duplicateRuntimePaneBinding
|
|
1766
|
+
? "duplicate-runtime-pane-binding"
|
|
1767
|
+
: inventory.catalog.missingSemanticStamp
|
|
1768
|
+
? "missing-semantic-stamp"
|
|
1769
|
+
: inventory.catalog.duplicateSemanticStamp
|
|
1770
|
+
? "duplicate-semantic-stamp"
|
|
1758
1771
|
: null;
|
|
1759
1772
|
if (
|
|
1760
1773
|
shouldPrewarm &&
|
|
@@ -1949,6 +1962,7 @@ export class WorkspaceTerminalInventoryRuntime {
|
|
|
1949
1962
|
if (this.#disposed) return;
|
|
1950
1963
|
this.#disposed = true;
|
|
1951
1964
|
this.#lifecycle = "disposed";
|
|
1965
|
+
this.#registeredWindows?.dispose();
|
|
1952
1966
|
this.invalidate();
|
|
1953
1967
|
this.#stopWorkspaceAddedObserver?.();
|
|
1954
1968
|
this.#stopWorkspaceRemovedObserver?.();
|
|
@@ -2128,12 +2142,13 @@ export class NativeTerminalAttachmentRuntime {
|
|
|
2128
2142
|
resolveGeometry: (descriptor, client) => geometry.resolve(descriptor, client),
|
|
2129
2143
|
...(options.sessionRuntimeRegistry
|
|
2130
2144
|
? {
|
|
2131
|
-
bindSessionRuntime: (descriptor: AttachmentLeaseDescriptor) => {
|
|
2145
|
+
bindSessionRuntime: async (descriptor: AttachmentLeaseDescriptor) => {
|
|
2132
2146
|
const workspace = options.registry.get(descriptor.target.workspaceName);
|
|
2133
2147
|
if (!workspace) throw new Error("Terminal attachment workspace is unavailable");
|
|
2134
2148
|
if (!descriptor.hostClientId) {
|
|
2135
2149
|
throw new Error("Interactive terminal attachment lacks trusted host identity");
|
|
2136
2150
|
}
|
|
2151
|
+
await options.sessionRuntimeRegistry!.verifyWindowOwnership();
|
|
2137
2152
|
return new SessionRuntimeTransportBinder(options.sessionRuntimeRegistry!).bind({
|
|
2138
2153
|
transport: "terminal-attachment",
|
|
2139
2154
|
transportLeaseId: descriptor.leaseId,
|
|
@@ -2357,12 +2372,12 @@ export class NativeTerminalAttachmentRuntime {
|
|
|
2357
2372
|
const catalogIssue: NativeTerminalInventoryCatalogIssue | null = inventory.catalog
|
|
2358
2373
|
.invalidRuntimeProof
|
|
2359
2374
|
? "invalid-runtime-proof"
|
|
2360
|
-
: inventory.catalog.
|
|
2361
|
-
? "
|
|
2362
|
-
: inventory.catalog.
|
|
2363
|
-
? "
|
|
2364
|
-
: inventory.catalog.
|
|
2365
|
-
? "duplicate-
|
|
2375
|
+
: inventory.catalog.duplicateRuntimePaneBinding
|
|
2376
|
+
? "duplicate-runtime-pane-binding"
|
|
2377
|
+
: inventory.catalog.missingSemanticStamp
|
|
2378
|
+
? "missing-semantic-stamp"
|
|
2379
|
+
: inventory.catalog.duplicateSemanticStamp
|
|
2380
|
+
? "duplicate-semantic-stamp"
|
|
2366
2381
|
: null;
|
|
2367
2382
|
// Ground-truth agent facts (authority + scrape fallback). All IO stays here;
|
|
2368
2383
|
// the resource projector composes them purely. Absent probe → no facts →
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { SharedWindowIndex } from "./shared-window-index.ts";
|
|
2
|
+
import { RegisteredWindowGuard, type RegisteredWindowReader } from "./registered-window-guard.ts";
|
|
1
3
|
import type { OwnedViewerAdapter } from "./owned-viewer-adapter.ts";
|
|
2
4
|
import { accessSync, constants, realpathSync, statSync } from "node:fs";
|
|
3
5
|
import { isAbsolute } from "node:path";
|
|
@@ -47,6 +49,7 @@ import {
|
|
|
47
49
|
} from "./control-mode-ownership.ts";
|
|
48
50
|
|
|
49
51
|
export interface MirrorServiceOptions {
|
|
52
|
+
onSharedWindowConflict?: (session: string, conflicted: boolean) => void;
|
|
50
53
|
createOwnedViewerAdapter?: (session: string) => OwnedViewerAdapter | undefined;
|
|
51
54
|
nativeServerIdentity?: import("../../lib/tmux-server-generation-runner.ts").NativeTmuxServerIdentity;
|
|
52
55
|
resolveSocketPath?: () => string;
|
|
@@ -148,6 +151,47 @@ interface ChannelEntry {
|
|
|
148
151
|
export class MirrorService {
|
|
149
152
|
private readonly opts: MirrorServiceOptions;
|
|
150
153
|
private readonly channels = new Map<string, ChannelEntry>();
|
|
154
|
+
private readonly sharedWindows = new SharedWindowIndex<SessionChannel>();
|
|
155
|
+
private readonly channelSessions = new Map<SessionChannel, string>();
|
|
156
|
+
private registeredWindows: RegisteredWindowGuard | null = null;
|
|
157
|
+
private readonly registeredConflicts = new Set<string>();
|
|
158
|
+
|
|
159
|
+
setRegisteredWindowReader(reader: RegisteredWindowReader): {
|
|
160
|
+
invalidate(): void;
|
|
161
|
+
dispose(): void;
|
|
162
|
+
} {
|
|
163
|
+
this.registeredWindows?.dispose();
|
|
164
|
+
const guard = new RegisteredWindowGuard(reader);
|
|
165
|
+
this.registeredWindows = guard;
|
|
166
|
+
this.invalidateRegisteredWindows();
|
|
167
|
+
return {
|
|
168
|
+
invalidate: () => {
|
|
169
|
+
if (this.registeredWindows === guard) this.invalidateRegisteredWindows();
|
|
170
|
+
},
|
|
171
|
+
dispose: () => guard.dispose(),
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
private invalidateRegisteredWindows(): void {
|
|
176
|
+
this.registeredWindows?.invalidate();
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async verifyRegisteredWindows(): Promise<void> {
|
|
180
|
+
const guard = this.registeredWindows;
|
|
181
|
+
if (!guard) return;
|
|
182
|
+
await guard.verify();
|
|
183
|
+
if (guard !== this.registeredWindows || this.disposed)
|
|
184
|
+
throw new Error("Window ownership changed during discovery");
|
|
185
|
+
for (const [channel, session] of this.channelSessions) {
|
|
186
|
+
if (guard.blocked(session)) {
|
|
187
|
+
channel.setGeometryParticipation(false);
|
|
188
|
+
if (!this.registeredConflicts.has(session)) {
|
|
189
|
+
this.registeredConflicts.add(session);
|
|
190
|
+
this.opts.onSharedWindowConflict?.(session, true);
|
|
191
|
+
}
|
|
192
|
+
} else this.registeredConflicts.delete(session);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
151
195
|
private readonly pendingDisposals = new Set<Promise<void>>();
|
|
152
196
|
private readonly drainingChannels = new Map<string, Promise<void>>();
|
|
153
197
|
private readonly sessionExitListeners = new Set<(session: string) => void>();
|
|
@@ -239,8 +283,14 @@ export class MirrorService {
|
|
|
239
283
|
reseed: () => handle.reseed(),
|
|
240
284
|
readHistorySize: () => handle.readHistorySize(),
|
|
241
285
|
captureNativeBacking: () => handle.captureNativeBacking(),
|
|
242
|
-
sendText: (text) =>
|
|
243
|
-
|
|
286
|
+
sendText: (text) => {
|
|
287
|
+
this.assertWindowOwnership(request.session);
|
|
288
|
+
handle.sendText(text);
|
|
289
|
+
},
|
|
290
|
+
sendKey: (key) => {
|
|
291
|
+
this.assertWindowOwnership(request.session);
|
|
292
|
+
handle.sendKey(key);
|
|
293
|
+
},
|
|
244
294
|
close: async () => {
|
|
245
295
|
if (closed) return;
|
|
246
296
|
closed = true;
|
|
@@ -303,6 +353,7 @@ export class MirrorService {
|
|
|
303
353
|
return (args: readonly string[]) => {
|
|
304
354
|
if (entry.retired || this.channels.get(session) !== entry)
|
|
305
355
|
return Promise.reject(new Error("Resize mirror session retired"));
|
|
356
|
+
this.assertWindowOwnership(session);
|
|
306
357
|
return run(args);
|
|
307
358
|
};
|
|
308
359
|
}
|
|
@@ -313,6 +364,7 @@ export class MirrorService {
|
|
|
313
364
|
) {
|
|
314
365
|
const entry = this.channels.get(session);
|
|
315
366
|
if (!entry || entry.retired) throw new Error(`Mirror session ${session} is unavailable`);
|
|
367
|
+
this.assertWindowOwnership(session);
|
|
316
368
|
return entry.channel.executeWindowLinkAction(request);
|
|
317
369
|
}
|
|
318
370
|
|
|
@@ -326,6 +378,7 @@ export class MirrorService {
|
|
|
326
378
|
): void {
|
|
327
379
|
const entry = this.channels.get(session);
|
|
328
380
|
if (!entry || entry.retired) throw new Error(`Mirror session ${session} is unavailable`);
|
|
381
|
+
this.assertWindowOwnership(session);
|
|
329
382
|
entry.channel.sendText(semanticPaneId, text, performanceTraceId, isolated);
|
|
330
383
|
}
|
|
331
384
|
|
|
@@ -337,24 +390,61 @@ export class MirrorService {
|
|
|
337
390
|
): void {
|
|
338
391
|
const entry = this.channels.get(session);
|
|
339
392
|
if (!entry || entry.retired) throw new Error(`Mirror session ${session} is unavailable`);
|
|
393
|
+
this.assertWindowOwnership(session);
|
|
340
394
|
entry.channel.sendBytes(semanticPaneId, data, performanceTraceId);
|
|
341
395
|
}
|
|
342
396
|
|
|
343
397
|
sendKey(session: string, semanticPaneId: string, key: string, performanceTraceId?: string): void {
|
|
344
398
|
const entry = this.channels.get(session);
|
|
345
399
|
if (!entry || entry.retired) throw new Error(`Mirror session ${session} is unavailable`);
|
|
400
|
+
this.assertWindowOwnership(session);
|
|
346
401
|
entry.channel.sendKey(semanticPaneId, key, performanceTraceId);
|
|
347
402
|
}
|
|
348
403
|
|
|
404
|
+
private assertWindowOwnership(session: string): void {
|
|
405
|
+
const message = this.windowOwnershipMessage(session);
|
|
406
|
+
if (message) throw new Error(message);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
windowOwnershipMessage(session: string): string | null {
|
|
410
|
+
if (this.registeredWindows?.pending())
|
|
411
|
+
return "Window ownership is being verified. Retry after session discovery completes.";
|
|
412
|
+
if (this.hasSharedWindowConflict(session))
|
|
413
|
+
return "Linked windows across registered sessions are unsupported. Unlink the shared window before controlling this session.";
|
|
414
|
+
return null;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
hasSharedWindowConflict(session: string): boolean {
|
|
418
|
+
const entry = this.channels.get(session);
|
|
419
|
+
return (
|
|
420
|
+
(this.registeredWindows?.blocked(session) ?? false) ||
|
|
421
|
+
(!!entry && this.sharedWindows.conflicted(entry.channel))
|
|
422
|
+
);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
private updateWindowMembership(channel: SessionChannel, windows: readonly string[]): void {
|
|
426
|
+
// A topology read may finish after retirement. It must not resurrect the
|
|
427
|
+
// old channel's membership or conflict with its replacement.
|
|
428
|
+
if (!this.channelSessions.has(channel)) return;
|
|
429
|
+
for (const [peer, conflict] of this.sharedWindows.update(channel, windows)) {
|
|
430
|
+
const session = this.channelSessions.get(peer);
|
|
431
|
+
if (session === undefined) continue;
|
|
432
|
+
if (conflict) peer.setGeometryParticipation(false);
|
|
433
|
+
this.opts.onSharedWindowConflict?.(session, conflict);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
349
437
|
fitViewport(session: string, cols: number, rows: number): void {
|
|
350
438
|
const entry = this.channels.get(session);
|
|
351
439
|
if (!entry || entry.retired) throw new Error(`Mirror session ${session} is unavailable`);
|
|
440
|
+
this.assertWindowOwnership(session);
|
|
352
441
|
entry.channel.fitViewport(cols, rows);
|
|
353
442
|
}
|
|
354
443
|
|
|
355
444
|
fitWindowViewport(session: string, semanticWindowId: string, cols: number, rows: number): void {
|
|
356
445
|
const entry = this.channels.get(session);
|
|
357
446
|
if (!entry || entry.retired) throw new Error(`Mirror session ${session} is unavailable`);
|
|
447
|
+
this.assertWindowOwnership(session);
|
|
358
448
|
entry.channel.fitWindowViewport(semanticWindowId, cols, rows);
|
|
359
449
|
}
|
|
360
450
|
|
|
@@ -367,7 +457,7 @@ export class MirrorService {
|
|
|
367
457
|
setGeometryParticipation(session: string, active: boolean): void {
|
|
368
458
|
const entry = this.channels.get(session);
|
|
369
459
|
if (!entry || entry.retired) return;
|
|
370
|
-
entry.channel.setGeometryParticipation(active);
|
|
460
|
+
entry.channel.setGeometryParticipation(active && !this.hasSharedWindowConflict(session));
|
|
371
461
|
}
|
|
372
462
|
|
|
373
463
|
/**
|
|
@@ -412,12 +502,15 @@ export class MirrorService {
|
|
|
412
502
|
|
|
413
503
|
async dispose(): Promise<void> {
|
|
414
504
|
this.disposed = true;
|
|
505
|
+
this.registeredWindows?.dispose();
|
|
415
506
|
this.sessionExitListeners.clear();
|
|
416
507
|
const entries = [...this.channels.values()];
|
|
417
508
|
this.channels.clear();
|
|
418
509
|
await Promise.allSettled(
|
|
419
510
|
entries.map(async (entry) => {
|
|
420
511
|
entry.retired = true;
|
|
512
|
+
this.updateWindowMembership(entry.channel, []);
|
|
513
|
+
this.channelSessions.delete(entry.channel);
|
|
421
514
|
try {
|
|
422
515
|
await entry.channel.dispose();
|
|
423
516
|
} finally {
|
|
@@ -468,6 +561,12 @@ export class MirrorService {
|
|
|
468
561
|
);
|
|
469
562
|
const channelOptions: SessionChannelOptions = {
|
|
470
563
|
session,
|
|
564
|
+
onWindowMembership: (windows) => this.updateWindowMembership(channel, windows),
|
|
565
|
+
hasSharedWindowConflict: () => this.hasSharedWindowConflict(session),
|
|
566
|
+
beforeIdentityRepair: () => this.verifyRegisteredWindows(),
|
|
567
|
+
onWindowTopologyChanged: () => {
|
|
568
|
+
if (this.channelSessions.has(channel)) this.invalidateRegisteredWindows();
|
|
569
|
+
},
|
|
471
570
|
ownedViewer,
|
|
472
571
|
createIo: (handlers) =>
|
|
473
572
|
this.opts.createIo?.(session, handlers) ??
|
|
@@ -547,6 +646,7 @@ export class MirrorService {
|
|
|
547
646
|
releaseAuthority();
|
|
548
647
|
throw cause;
|
|
549
648
|
}
|
|
649
|
+
this.channelSessions.set(channel, session);
|
|
550
650
|
entry = {
|
|
551
651
|
channel,
|
|
552
652
|
started: channel.start(),
|
|
@@ -583,6 +683,8 @@ export class MirrorService {
|
|
|
583
683
|
private retire(session: string, entry: ChannelEntry): void {
|
|
584
684
|
if (entry.retired) return;
|
|
585
685
|
entry.retired = true;
|
|
686
|
+
this.updateWindowMembership(entry.channel, []);
|
|
687
|
+
this.channelSessions.delete(entry.channel);
|
|
586
688
|
if (this.channels.get(session) === entry) this.channels.delete(session);
|
|
587
689
|
const disposal = entry.channel
|
|
588
690
|
.dispose()
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
export type RegisteredWindowReader = (
|
|
2
|
+
signal: AbortSignal,
|
|
3
|
+
) => Promise<readonly { session: string; window: string }[]>;
|
|
4
|
+
|
|
5
|
+
/** A bounded registration/topology proof, shared by all retained channels. */
|
|
6
|
+
export class RegisteredWindowGuard {
|
|
7
|
+
#epoch = 0;
|
|
8
|
+
#current = false;
|
|
9
|
+
#disposed = false;
|
|
10
|
+
#conflicts = new Set<string>();
|
|
11
|
+
#read: { abort: AbortController; promise: Promise<void> } | null = null;
|
|
12
|
+
|
|
13
|
+
constructor(private readonly reader: RegisteredWindowReader) {}
|
|
14
|
+
|
|
15
|
+
blocked(session: string): boolean {
|
|
16
|
+
return !this.#current || this.#conflicts.has(session);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
pending(): boolean {
|
|
20
|
+
return !this.#current;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
invalidate(): void {
|
|
24
|
+
this.#epoch++;
|
|
25
|
+
this.#current = false;
|
|
26
|
+
this.#read?.abort.abort();
|
|
27
|
+
this.#read = null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
dispose(): void {
|
|
31
|
+
this.#disposed = true;
|
|
32
|
+
this.invalidate();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async verify(): Promise<void> {
|
|
36
|
+
if (this.#disposed) throw new Error("Registered window inventory is disposed");
|
|
37
|
+
if (this.#current) return;
|
|
38
|
+
if (this.#read) return this.#read.promise;
|
|
39
|
+
const epoch = this.#epoch;
|
|
40
|
+
const abort = new AbortController();
|
|
41
|
+
const promise = Promise.resolve()
|
|
42
|
+
.then(async () => {
|
|
43
|
+
abort.signal.throwIfAborted();
|
|
44
|
+
const rows = await this.reader(abort.signal);
|
|
45
|
+
abort.signal.throwIfAborted();
|
|
46
|
+
if (epoch !== this.#epoch) throw new Error("Window ownership changed during discovery");
|
|
47
|
+
const owners = new Map<string, Set<string>>();
|
|
48
|
+
for (const row of rows) {
|
|
49
|
+
const sessions = owners.get(row.window) ?? new Set<string>();
|
|
50
|
+
sessions.add(row.session);
|
|
51
|
+
owners.set(row.window, sessions);
|
|
52
|
+
}
|
|
53
|
+
this.#conflicts = new Set(
|
|
54
|
+
[...owners.values()]
|
|
55
|
+
.filter((sessions) => sessions.size > 1)
|
|
56
|
+
.flatMap((sessions) => [...sessions]),
|
|
57
|
+
);
|
|
58
|
+
this.#current = true;
|
|
59
|
+
})
|
|
60
|
+
.finally(() => {
|
|
61
|
+
if (this.#read?.abort === abort) this.#read = null;
|
|
62
|
+
});
|
|
63
|
+
this.#read = { abort, promise };
|
|
64
|
+
return promise;
|
|
65
|
+
}
|
|
66
|
+
}
|