tmux-ide 2.9.10 → 2.9.12
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/bin/cli.js +4321 -2596
- package/package.json +1 -1
- package/packages/contracts/src/index.ts +4 -0
- package/packages/contracts/src/pane-stream.ts +72 -0
- package/packages/contracts/src/terminal-delivery-binary-chunk.ts +55 -0
- package/packages/contracts/src/terminal-delivery.ts +17 -2
- package/packages/contracts/src/terminal-graphics.ts +68 -0
- package/packages/contracts/src/terminal-projection.ts +153 -0
- package/packages/core/src/fixtures/terminal-projection-vectors.json +585 -0
- package/packages/core/src/index.ts +2 -0
- package/packages/core/src/terminal-delivery-binary.ts +160 -0
- package/packages/core/src/terminal-delivery.ts +371 -16
- package/packages/core/src/terminal-projection-vectors.mjs +82 -0
- package/packages/core/src/terminal-projection.ts +476 -0
- package/packages/core/src/terminal-replica-hash-cache.ts +8 -1
- package/packages/core/src/terminal-replica.ts +1 -1
- package/packages/daemon/dist/lib/daemon-embed.js +39 -3
- package/packages/daemon/dist/lib/embedded-tmux-server-owners.js +1 -0
- package/packages/daemon/dist/lib/tmux-server-owner.js +1 -0
- 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/graphics/local-image-owner.js +330 -0
- package/packages/daemon/dist/terminal/graphics/local-image-store.js +255 -0
- package/packages/daemon/dist/terminal/mirror/mirror-service.js +12 -2
- package/packages/daemon/dist/terminal/mirror/session-channel.js +46 -23
- package/packages/daemon/dist/terminal/pane-stream/pane-stream-websocket.js +190 -19
- package/packages/daemon/dist/terminal/pane-stream/runtime.js +1 -0
- package/packages/daemon/dist/terminal/session-runtime/registry.js +19 -6
- package/packages/daemon/dist/terminal/session-runtime/terminal-delivery-hub.js +146 -16
- package/packages/daemon/dist/terminal/session-runtime/terminal-projection-delivery.js +32 -0
- package/packages/daemon/dist/terminal/session-runtime/terminal-replica-interpreter.js +95 -36
- package/packages/daemon/dist/terminal/session-runtime/terminal-replica-owner.js +5 -0
- package/packages/daemon/dist/terminal/session-runtime/transport-binding.js +2 -2
- package/packages/daemon/dist/tui/mirror/open-tui-workspace-runtime-port.js +4 -0
- package/packages/daemon/dist/tui/mirror/runtime/application-generation-starter.js +16 -1
- package/packages/daemon/dist/tui/mirror/runtime/application-machine-agent-navigation.js +13 -0
- package/packages/daemon/dist/tui/mirror/runtime/application-machine-navigation.js +12 -4
- package/packages/daemon/dist/tui/mirror/runtime/application-retained-agent.js +18 -0
- package/packages/daemon/dist/tui/mirror/runtime/application-terminal-interaction-controller.js +90 -5
- package/packages/daemon/dist/tui/mirror/runtime/application-terminal-workspace.jsx +13 -1
- package/packages/daemon/dist/tui/mirror/runtime/open-tui-generation-host.js +2 -2
- package/packages/daemon/dist/tui/mirror/runtime/renderer-probe.js +195 -0
- package/packages/daemon/dist/tui/mirror/runtime/runtime-layout-presentation.js +6 -3
- package/packages/daemon/dist/tui/mirror/runtime/select-terminal-pane.js +7 -2
- package/packages/daemon/dist/tui/mirror/runtime/terminal-fast-lane-renderer-adapter.js +6 -0
- package/packages/daemon/dist/tui/mirror/runtime/terminal-selection-waiter.js +36 -0
- package/packages/daemon/dist/tui/mirror/semantic-pane-render-source.js +4 -0
- package/packages/daemon/src/lib/daemon-embed.ts +38 -2
- package/packages/daemon/src/lib/embedded-tmux-server-owners.ts +3 -0
- package/packages/daemon/src/lib/tmux-server-owner.ts +3 -0
- package/packages/daemon/src/terminal/graphics/local-image-owner.ts +346 -0
- package/packages/daemon/src/terminal/graphics/local-image-store.ts +286 -0
- package/packages/daemon/src/terminal/mirror/mirror-service.ts +19 -2
- package/packages/daemon/src/terminal/mirror/session-channel.ts +57 -9
- package/packages/daemon/src/terminal/pane-stream/pane-stream-websocket.ts +248 -16
- package/packages/daemon/src/terminal/pane-stream/runtime.ts +3 -0
- package/packages/daemon/src/terminal/session-runtime/registry.ts +25 -3
- package/packages/daemon/src/terminal/session-runtime/runtime-observability.ts +1 -1
- package/packages/daemon/src/terminal/session-runtime/terminal-delivery-hub.ts +216 -22
- package/packages/daemon/src/terminal/session-runtime/terminal-delivery-observation-identity.ts +7 -4
- package/packages/daemon/src/terminal/session-runtime/terminal-projection-delivery.ts +59 -0
- package/packages/daemon/src/terminal/session-runtime/terminal-replica-interpreter.ts +102 -44
- package/packages/daemon/src/terminal/session-runtime/terminal-replica-owner.ts +6 -0
- package/packages/daemon/src/terminal/session-runtime/transport-binding.ts +3 -0
- package/packages/daemon/src/tui/mirror/open-tui-workspace-runtime-port.ts +6 -0
- package/packages/daemon/src/tui/mirror/runtime/application-generation-starter.ts +17 -1
- package/packages/daemon/src/tui/mirror/runtime/application-machine-agent-navigation.ts +15 -0
- package/packages/daemon/src/tui/mirror/runtime/application-machine-navigation.ts +18 -4
- package/packages/daemon/src/tui/mirror/runtime/application-retained-agent.ts +29 -0
- package/packages/daemon/src/tui/mirror/runtime/application-terminal-interaction-controller.ts +89 -6
- package/packages/daemon/src/tui/mirror/runtime/application-terminal-workspace.tsx +20 -1
- package/packages/daemon/src/tui/mirror/runtime/open-tui-generation-host.ts +2 -2
- package/packages/daemon/src/tui/mirror/runtime/renderer-probe.ts +257 -0
- package/packages/daemon/src/tui/mirror/runtime/runtime-layout-presentation.ts +7 -4
- package/packages/daemon/src/tui/mirror/runtime/select-terminal-pane.ts +9 -1
- package/packages/daemon/src/tui/mirror/runtime/terminal-fast-lane-renderer-adapter.ts +15 -0
- package/packages/daemon/src/tui/mirror/runtime/terminal-selection-waiter.ts +36 -0
- package/packages/daemon/src/tui/mirror/semantic-pane-render-source.ts +4 -0
- package/packages/daemon-client/src/pane-stream-client.ts +92 -1
|
@@ -177,6 +177,8 @@ export class SessionChannel {
|
|
|
177
177
|
fittedWindows = new Map();
|
|
178
178
|
cancelSync = null;
|
|
179
179
|
syncOrdinal = 0;
|
|
180
|
+
identityJoinOrdinal = 0;
|
|
181
|
+
trustedInventoryJoinsMissing = false;
|
|
180
182
|
completedSyncOrdinal = 0;
|
|
181
183
|
lastDisplayNameSyncAtMs = 0;
|
|
182
184
|
disposed = false;
|
|
@@ -412,20 +414,28 @@ export class SessionChannel {
|
|
|
412
414
|
}
|
|
413
415
|
/**
|
|
414
416
|
* Strict daemon-internal inventory from this channel's current tmux truth.
|
|
415
|
-
* Unlike
|
|
416
|
-
*
|
|
417
|
-
*
|
|
417
|
+
* Unlike background discovery, this query verifies current native membership
|
|
418
|
+
* before projecting and rejects incomplete identity. Stream admission may
|
|
419
|
+
* explicitly join missing pane records after that verification; ordinary
|
|
420
|
+
* inventory readers retain their existing fail-closed no-join contract.
|
|
418
421
|
*/
|
|
419
|
-
describeTrustedInventory(expectedRuntimeSessionId) {
|
|
422
|
+
describeTrustedInventory(expectedRuntimeSessionId, joinMissingPanes = false) {
|
|
420
423
|
if (this.disposed) {
|
|
421
424
|
return Promise.reject(new Error(`mirror session ${this.opts.session} is disposed`));
|
|
422
425
|
}
|
|
423
426
|
if (this.trustedInventoryFlight) {
|
|
427
|
+
// A default read can fail precisely because it does not join new panes.
|
|
428
|
+
// Preserve that failure for its caller, then qualify admission separately.
|
|
429
|
+
if (joinMissingPanes &&
|
|
430
|
+
!this.trustedInventoryJoinsMissing &&
|
|
431
|
+
this.trustedInventoryFlightSessionId === expectedRuntimeSessionId)
|
|
432
|
+
return this.trustedInventoryFlight.then(() => this.describeTrustedInventory(expectedRuntimeSessionId, true), () => this.describeTrustedInventory(expectedRuntimeSessionId, true));
|
|
424
433
|
return this.trustedInventoryFlightSessionId === expectedRuntimeSessionId
|
|
425
434
|
? this.trustedInventoryFlight
|
|
426
435
|
: Promise.reject(new Error(`trusted inventory identity changed for ${this.opts.session}`));
|
|
427
436
|
}
|
|
428
|
-
|
|
437
|
+
this.trustedInventoryJoinsMissing = joinMissingPanes;
|
|
438
|
+
const flight = this.refreshTrustedInventory(expectedRuntimeSessionId, 0, joinMissingPanes)
|
|
429
439
|
.catch((error) => {
|
|
430
440
|
this.windowLinkAuthority?.invalidate();
|
|
431
441
|
this.latestWindowStage = null;
|
|
@@ -2723,7 +2733,7 @@ export class SessionChannel {
|
|
|
2723
2733
|
}
|
|
2724
2734
|
return { listed, movedWindowRuntimeIds };
|
|
2725
2735
|
}
|
|
2726
|
-
async refreshTrustedInventory(expectedRuntimeSessionId, attempt = 0) {
|
|
2736
|
+
async refreshTrustedInventory(expectedRuntimeSessionId, attempt = 0, joinMissingPanes = false) {
|
|
2727
2737
|
const authorityOrdinal = this.windowIdentityOrdinal;
|
|
2728
2738
|
const beforeLines = await this.io.request(`list-panes -s -t "${expectedRuntimeSessionId}" -F "${SESSION_PANE_DESCRIPTOR_FORMAT}"`);
|
|
2729
2739
|
if (this.disposed)
|
|
@@ -2800,7 +2810,7 @@ export class SessionChannel {
|
|
|
2800
2810
|
authorityOrdinal !== this.windowIdentityOrdinal) {
|
|
2801
2811
|
if (attempt >= 1)
|
|
2802
2812
|
throw new Error(`trusted inventory for ${this.opts.session} did not settle`);
|
|
2803
|
-
return await this.refreshTrustedInventory(expectedRuntimeSessionId, attempt + 1);
|
|
2813
|
+
return await this.refreshTrustedInventory(expectedRuntimeSessionId, attempt + 1, joinMissingPanes);
|
|
2804
2814
|
}
|
|
2805
2815
|
if (this.disposed)
|
|
2806
2816
|
throw new Error(`mirror session ${this.opts.session} is disposed`);
|
|
@@ -2832,17 +2842,27 @@ export class SessionChannel {
|
|
|
2832
2842
|
!stagedPaneMembershipExact ||
|
|
2833
2843
|
stagedPaneCount !== descriptors.length ||
|
|
2834
2844
|
stagedPaneIds.size !== descriptors.length ||
|
|
2835
|
-
[...computedWindowCounts.keys()].some((runtimeWindowId) => !confirmedWindowStage.windows.has(runtimeWindowId))
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
})
|
|
2845
|
+
[...computedWindowCounts.keys()].some((runtimeWindowId) => !confirmedWindowStage.windows.has(runtimeWindowId)))
|
|
2846
|
+
throw new Error(`trusted inventory for ${this.opts.session} lacks verified identity`);
|
|
2847
|
+
// Fresh native inventory can beat background discovery after window creation.
|
|
2848
|
+
// Join verified stable membership before projecting semantic records.
|
|
2849
|
+
if (joinMissingPanes &&
|
|
2850
|
+
descriptors.some((pane) => this.panesByRuntime.get(pane.runtimePaneId)?.semanticId !== pane.semanticPaneId)) {
|
|
2851
|
+
this.discovery.invalidate();
|
|
2852
|
+
await this.reconcileIdentity(descriptors, runtimePaneIds, () => authorityOrdinal === this.windowIdentityOrdinal, false);
|
|
2853
|
+
}
|
|
2854
|
+
if (this.disposed || authorityOrdinal !== this.windowIdentityOrdinal)
|
|
2855
|
+
throw new Error(`trusted inventory for ${this.opts.session} changed during identity join`);
|
|
2856
|
+
if (descriptors.some((descriptor) => {
|
|
2857
|
+
const pane = this.panesByRuntime.get(descriptor.runtimePaneId);
|
|
2858
|
+
const window = confirmedWindowStage.windows.get(descriptor.windowId);
|
|
2859
|
+
return (!pane ||
|
|
2860
|
+
!window?.semanticId ||
|
|
2861
|
+
descriptor.semanticPaneId !== pane.semanticId ||
|
|
2862
|
+
descriptor.semanticWindowId !== window.semanticId ||
|
|
2863
|
+
!WorkspaceIdSchemaZ.safeParse(pane.semanticId).success ||
|
|
2864
|
+
!WorkspaceIdSchemaZ.safeParse(window.semanticId).success);
|
|
2865
|
+
})) {
|
|
2846
2866
|
throw new Error(`trusted inventory for ${this.opts.session} lacks verified identity`);
|
|
2847
2867
|
}
|
|
2848
2868
|
const previousCurrentWindow = this.currentWindow;
|
|
@@ -3174,7 +3194,8 @@ export class SessionChannel {
|
|
|
3174
3194
|
})),
|
|
3175
3195
|
};
|
|
3176
3196
|
}
|
|
3177
|
-
async reconcileIdentity(descriptors, listed) {
|
|
3197
|
+
async reconcileIdentity(descriptors, listed, current = () => true, publish = true) {
|
|
3198
|
+
const joinOrdinal = ++this.identityJoinOrdinal;
|
|
3178
3199
|
if (this.disposed)
|
|
3179
3200
|
return false;
|
|
3180
3201
|
if (this.opts.hasSharedWindowConflict?.()) {
|
|
@@ -3205,7 +3226,7 @@ export class SessionChannel {
|
|
|
3205
3226
|
ok: false,
|
|
3206
3227
|
error: cause instanceof Error ? cause.message : String(cause),
|
|
3207
3228
|
}))));
|
|
3208
|
-
if (this.disposed)
|
|
3229
|
+
if (this.disposed || !current() || joinOrdinal !== this.identityJoinOrdinal)
|
|
3209
3230
|
return plan.stampEffects.length > 0;
|
|
3210
3231
|
const reconciliation = finalizeWorkspaceTmuxReconciliation(plan, outcomes);
|
|
3211
3232
|
const descriptorByRuntime = new Map(descriptors.map((d) => [d.runtimePaneId, d]));
|
|
@@ -3311,9 +3332,11 @@ export class SessionChannel {
|
|
|
3311
3332
|
* without this the new pane stays invisible until something else happens to
|
|
3312
3333
|
* move a layout, and a split looks like it did not reach the view.
|
|
3313
3334
|
*/
|
|
3314
|
-
|
|
3315
|
-
this.
|
|
3316
|
-
|
|
3335
|
+
if (publish) {
|
|
3336
|
+
for (const runtimeId of this.layoutByWindow.keys())
|
|
3337
|
+
this.emitLayout(runtimeId);
|
|
3338
|
+
this.emitLayoutAuthority();
|
|
3339
|
+
}
|
|
3317
3340
|
this.settleFirstJoin();
|
|
3318
3341
|
return plan.stampEffects.length > 0;
|
|
3319
3342
|
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { TERMINAL_PROJECTION_CAPABILITY_VERSION, TERMINAL_PROJECTION_LOCAL_CAPABILITY_VERSION, } from "@tmux-ide/contracts";
|
|
2
|
+
import { PANE_STREAM_INPUT_LIFETIME_CAPABILITY_VERSION, PANE_STREAM_INPUT_BYTES_CAPABILITY_VERSION, } from "@tmux-ide/contracts";
|
|
1
3
|
import { inflateSync } from "node:zlib";
|
|
2
4
|
import { PaneStreamLayoutSnapshotFrameSchemaZ } from "@tmux-ide/contracts";
|
|
3
5
|
import { z } from "zod";
|
|
4
|
-
import { PANE_STREAM_MAX_HELD_DELTAS, PANE_STREAM_MAX_OUTPUT_BYTES, PANE_STREAM_MAX_PANES, PANE_STREAM_MAX_SEED_BYTES, PANE_STREAM_PROTOCOL_VERSION, PANE_STREAM_REDEEM_PATH, PANE_STREAM_WEBSOCKET_SUBPROTOCOL, TERMINAL_GRAPHICS_BLOB_CHUNK_BYTES, TERMINAL_GRAPHICS_CELL_PIXELS_CAPABILITY_VERSION, TERMINAL_GRAPHICS_PRECISE_PIXELS_CAPABILITY_VERSION, TERMINAL_GRAPHICS_LEGACY_MAX_IMAGE_BYTES, TERMINAL_GRAPHICS_ZLIB_CAPABILITY_VERSION, PaneStreamClientFrameSchemaZ, PaneStreamLoopbackWebSocketUrlSchemaZ, PaneStreamRedeemFrameSchemaZ, PaneStreamServerFrameSchemaZ, sharedMonotonicMicros, } from "@tmux-ide/contracts";
|
|
6
|
+
import { PANE_STREAM_MAX_HELD_DELTAS, PANE_STREAM_MAX_OUTPUT_BYTES, PANE_STREAM_MAX_PANES, PANE_STREAM_MAX_SEED_BYTES, PANE_STREAM_PROTOCOL_VERSION, PANE_STREAM_REDEEM_PATH, PANE_STREAM_WEBSOCKET_SUBPROTOCOL, TERMINAL_GRAPHICS_BLOB_CHUNK_BYTES, TERMINAL_DELIVERY_BINARY_CAPABILITY_VERSION, TERMINAL_DELIVERY_BINARY_LOCAL_CAPABILITY_VERSION, TERMINAL_GRAPHICS_BINARY_CAPABILITY_VERSION, TERMINAL_GRAPHICS_LOCAL_CAPABILITY_VERSION, encodeTerminalGraphicsBinary, encodeTerminalDeliveryBinaryChunk, TERMINAL_GRAPHICS_CELL_PIXELS_CAPABILITY_VERSION, TERMINAL_GRAPHICS_PRECISE_PIXELS_CAPABILITY_VERSION, TERMINAL_GRAPHICS_LEGACY_MAX_IMAGE_BYTES, TERMINAL_GRAPHICS_ZLIB_CAPABILITY_VERSION, PaneStreamClientFrameSchemaZ, PaneStreamLoopbackWebSocketUrlSchemaZ, PaneStreamRedeemFrameSchemaZ, PaneStreamServerFrameSchemaZ, sharedMonotonicMicros, } from "@tmux-ide/contracts";
|
|
5
7
|
import { canonicalOriginOrNull, digestSecret, digestsEqual, rawDataByteLength, rawDataToBuffer, safeCloseSocket, strictJsonParse, } from "../attachments/admission-util.js";
|
|
6
8
|
import { SessionRuntimeControllerLeaseError } from "../session-runtime/registry.js";
|
|
7
9
|
import { terminalDeliveryObservationOrdinal } from "../session-runtime/terminal-delivery-observation-identity.js";
|
|
@@ -114,6 +116,7 @@ function errorFrameCode(error) {
|
|
|
114
116
|
* ticket digests; a daemon restart makes every prior descriptor inert.
|
|
115
117
|
*/
|
|
116
118
|
export class PaneStreamAdmissionCoordinator {
|
|
119
|
+
#localImages;
|
|
117
120
|
#instanceId;
|
|
118
121
|
#webSocketUrl;
|
|
119
122
|
#leaseManager;
|
|
@@ -144,6 +147,7 @@ export class PaneStreamAdmissionCoordinator {
|
|
|
144
147
|
#shuttingDown = false;
|
|
145
148
|
#shutdownPromise = null;
|
|
146
149
|
constructor(options) {
|
|
150
|
+
this.#localImages = options.localImages;
|
|
147
151
|
this.#instanceId = BindingIdSchemaZ.parse(options.daemonInstanceId);
|
|
148
152
|
this.#webSocketUrl = PaneStreamLoopbackWebSocketUrlSchemaZ.parse(options.webSocketUrl);
|
|
149
153
|
this.#leaseManager = options.leaseManager;
|
|
@@ -193,7 +197,7 @@ export class PaneStreamAdmissionCoordinator {
|
|
|
193
197
|
try {
|
|
194
198
|
const describeStartedAtMicros = trace ? this.#observability.nowMicros() : 0;
|
|
195
199
|
if (this.#mirror.describeSessionAuthority) {
|
|
196
|
-
const authority = await this.#mirror.describeSessionAuthority(context.sessionName);
|
|
200
|
+
const authority = await this.#mirror.describeSessionAuthority(context.sessionName, request.panes);
|
|
197
201
|
described = authority.description;
|
|
198
202
|
runtimeSessionId = authority.runtimeSessionId;
|
|
199
203
|
}
|
|
@@ -406,6 +410,7 @@ export class PaneStreamAdmissionCoordinator {
|
|
|
406
410
|
let live;
|
|
407
411
|
try {
|
|
408
412
|
live = new PaneStreamLiveConnection({
|
|
413
|
+
localImages: this.#localImages,
|
|
409
414
|
clientId: `psc-${this.#clientCounter}`,
|
|
410
415
|
socket,
|
|
411
416
|
descriptor: structuredClone(descriptor),
|
|
@@ -613,6 +618,8 @@ export function rawGraphicsState(state) {
|
|
|
613
618
|
}
|
|
614
619
|
const GRAPHICS_SEND_WINDOW_BYTES = 2 * 1024 * 1024;
|
|
615
620
|
export class PaneStreamLiveConnection {
|
|
621
|
+
#localImages;
|
|
622
|
+
#localLeases = new Map();
|
|
616
623
|
#clientId;
|
|
617
624
|
#socket;
|
|
618
625
|
#descriptor;
|
|
@@ -700,6 +707,7 @@ export class PaneStreamLiveConnection {
|
|
|
700
707
|
return elapsed;
|
|
701
708
|
}
|
|
702
709
|
constructor(options) {
|
|
710
|
+
this.#localImages = options.localImages;
|
|
703
711
|
this.#clientId = options.clientId;
|
|
704
712
|
this.#socket = options.socket;
|
|
705
713
|
this.#descriptor = options.descriptor;
|
|
@@ -765,6 +773,14 @@ export class PaneStreamLiveConnection {
|
|
|
765
773
|
try {
|
|
766
774
|
sendControl(this.#socket, {
|
|
767
775
|
type: "ready",
|
|
776
|
+
...(this.#sessionRuntimeBinding &&
|
|
777
|
+
this.#descriptor.terminalDelivery?.protocolVersions.includes(PANE_STREAM_INPUT_LIFETIME_CAPABILITY_VERSION)
|
|
778
|
+
? { inputLifetime: true }
|
|
779
|
+
: {}),
|
|
780
|
+
...(this.#sessionRuntimeBinding &&
|
|
781
|
+
this.#descriptor.terminalDelivery?.protocolVersions.includes(PANE_STREAM_INPUT_BYTES_CAPABILITY_VERSION)
|
|
782
|
+
? { inputBytes: true }
|
|
783
|
+
: {}),
|
|
768
784
|
protocolVersion: PANE_STREAM_PROTOCOL_VERSION,
|
|
769
785
|
daemonInstanceId: this.#binding.daemonInstanceId,
|
|
770
786
|
requestId: this.#binding.requestId,
|
|
@@ -807,6 +823,12 @@ export class PaneStreamLiveConnection {
|
|
|
807
823
|
this.#stopAuthoritySnapshots?.();
|
|
808
824
|
this.#stopAuthoritySnapshots = null;
|
|
809
825
|
const closures = [];
|
|
826
|
+
this.#localLeases.clear();
|
|
827
|
+
if (this.#localImages)
|
|
828
|
+
closures.push(this.#localImages.releaseClient({
|
|
829
|
+
generation: this.#localImages.identity.generation,
|
|
830
|
+
client: this.#descriptor.leaseId,
|
|
831
|
+
}));
|
|
810
832
|
const layoutSubscription = this.#layoutSubscription;
|
|
811
833
|
this.#layoutSubscription = null;
|
|
812
834
|
if (layoutSubscription)
|
|
@@ -1019,7 +1041,7 @@ export class PaneStreamLiveConnection {
|
|
|
1019
1041
|
return true;
|
|
1020
1042
|
let incarnation = null;
|
|
1021
1043
|
for (const message of pending) {
|
|
1022
|
-
if (message.type !== "terminal.delivery")
|
|
1044
|
+
if (message.type !== "terminal.delivery" && message.type !== "terminal.projection")
|
|
1023
1045
|
continue;
|
|
1024
1046
|
if (message.workspaceName !== this.#descriptor.workspaceName ||
|
|
1025
1047
|
message.semanticPaneId !== channel.semanticPaneId ||
|
|
@@ -1127,7 +1149,15 @@ export class PaneStreamLiveConnection {
|
|
|
1127
1149
|
this.#closeIfAllPanesGone();
|
|
1128
1150
|
}
|
|
1129
1151
|
async #sendTerminalDelivery(pane, message) {
|
|
1130
|
-
if (message.type === "terminal.
|
|
1152
|
+
if (message.type === "terminal.projection") {
|
|
1153
|
+
const channel = this.#panes.get(pane);
|
|
1154
|
+
if (channel?.deliveryAddress)
|
|
1155
|
+
channel.deliveryAddress.incarnation = message.incarnation;
|
|
1156
|
+
if (channel)
|
|
1157
|
+
channel.binaryCells = true;
|
|
1158
|
+
this.#sendFrame(pane, { type: "terminal-projection-envelope", pane, envelope: message });
|
|
1159
|
+
}
|
|
1160
|
+
else if (message.type === "terminal.delivery") {
|
|
1131
1161
|
if (!this.#diagnosticFirstSeedObserved) {
|
|
1132
1162
|
this.#diagnosticFirstSeedObserved = true;
|
|
1133
1163
|
this.#recordDiagnosticLifecycle("pane-stream-first-seed");
|
|
@@ -1135,6 +1165,8 @@ export class PaneStreamLiveConnection {
|
|
|
1135
1165
|
const channel = this.#panes.get(pane);
|
|
1136
1166
|
if (channel?.deliveryAddress)
|
|
1137
1167
|
channel.deliveryAddress.incarnation = message.incarnation;
|
|
1168
|
+
if (channel)
|
|
1169
|
+
channel.binaryCells = message.encoding === "semantic-binary-v1";
|
|
1138
1170
|
// The trusted lease address was fixed at delivery admission; preserve the
|
|
1139
1171
|
// exact envelope and its observation identity through transmission.
|
|
1140
1172
|
const detailedDeliveryObservation = this.#observability?.enabled === true;
|
|
@@ -1210,13 +1242,18 @@ export class PaneStreamLiveConnection {
|
|
|
1210
1242
|
}
|
|
1211
1243
|
}
|
|
1212
1244
|
else if (message.type === "terminal.delivery.chunk") {
|
|
1213
|
-
this.#
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1245
|
+
if (this.#panes.get(pane)?.binaryCells) {
|
|
1246
|
+
this.#sendEncoded(pane, Buffer.from(encodeTerminalDeliveryBinaryChunk({ pane, transactionId: message.transactionId, index: message.index }, message.bytes)), true);
|
|
1247
|
+
}
|
|
1248
|
+
else {
|
|
1249
|
+
this.#sendFrame(pane, {
|
|
1250
|
+
type: "terminal-delivery-chunk",
|
|
1251
|
+
pane,
|
|
1252
|
+
transactionId: message.transactionId,
|
|
1253
|
+
index: message.index,
|
|
1254
|
+
data: Buffer.from(message.bytes).toString("base64"),
|
|
1255
|
+
});
|
|
1256
|
+
}
|
|
1220
1257
|
}
|
|
1221
1258
|
else {
|
|
1222
1259
|
this.#sendFrame(pane, { type: "terminal-delivery-fault", pane, fault: message });
|
|
@@ -1236,6 +1273,19 @@ export class PaneStreamLiveConnection {
|
|
|
1236
1273
|
// when served.
|
|
1237
1274
|
const zlib = this.#descriptor.terminalDelivery?.protocolVersions.includes(TERMINAL_GRAPHICS_ZLIB_CAPABILITY_VERSION) === true;
|
|
1238
1275
|
channel.graphicsZlib = zlib;
|
|
1276
|
+
channel.graphicsLocal =
|
|
1277
|
+
!!this.#localImages &&
|
|
1278
|
+
this.#descriptor.terminalDelivery?.protocolVersions.some((version) => version === TERMINAL_GRAPHICS_LOCAL_CAPABILITY_VERSION ||
|
|
1279
|
+
version === TERMINAL_DELIVERY_BINARY_LOCAL_CAPABILITY_VERSION ||
|
|
1280
|
+
version === TERMINAL_PROJECTION_LOCAL_CAPABILITY_VERSION) === true;
|
|
1281
|
+
channel.graphicsInline = new Set();
|
|
1282
|
+
channel.graphicsBinary =
|
|
1283
|
+
this.#descriptor.terminalDelivery?.protocolVersions.some((version) => version === TERMINAL_GRAPHICS_BINARY_CAPABILITY_VERSION ||
|
|
1284
|
+
version === TERMINAL_GRAPHICS_LOCAL_CAPABILITY_VERSION ||
|
|
1285
|
+
version === TERMINAL_DELIVERY_BINARY_CAPABILITY_VERSION ||
|
|
1286
|
+
version === TERMINAL_PROJECTION_CAPABILITY_VERSION ||
|
|
1287
|
+
version === TERMINAL_DELIVERY_BINARY_LOCAL_CAPABILITY_VERSION ||
|
|
1288
|
+
version === TERMINAL_PROJECTION_LOCAL_CAPABILITY_VERSION) === true;
|
|
1239
1289
|
// Renderers that report cell pixels learn up front that this daemon
|
|
1240
1290
|
// accepts them, before any image exists.
|
|
1241
1291
|
const cellPixels = this.#descriptor.terminalDelivery?.protocolVersions.includes(TERMINAL_GRAPHICS_CELL_PIXELS_CAPABILITY_VERSION) === true;
|
|
@@ -1263,6 +1313,8 @@ export class PaneStreamLiveConnection {
|
|
|
1263
1313
|
...(zlib ? state : rawGraphicsState(state)),
|
|
1264
1314
|
...(cellPixels ? { cellPixels: true } : {}),
|
|
1265
1315
|
...(preciseCellPixels ? { preciseCellPixels: true } : {}),
|
|
1316
|
+
...(channel.graphicsBinary ? { binaryBlobs: true } : {}),
|
|
1317
|
+
...(channel.graphicsLocal ? { localFiles: true } : {}),
|
|
1266
1318
|
},
|
|
1267
1319
|
});
|
|
1268
1320
|
if (pushed.length > 0)
|
|
@@ -1279,15 +1331,23 @@ export class PaneStreamLiveConnection {
|
|
|
1279
1331
|
placements: [],
|
|
1280
1332
|
cellPixels: true,
|
|
1281
1333
|
...(preciseCellPixels ? { preciseCellPixels: true } : {}),
|
|
1334
|
+
...(channel.graphicsBinary ? { binaryBlobs: true } : {}),
|
|
1335
|
+
...(channel.graphicsLocal ? { localFiles: true } : {}),
|
|
1282
1336
|
},
|
|
1283
1337
|
});
|
|
1284
1338
|
}
|
|
1285
1339
|
}
|
|
1286
|
-
#requestGraphics(pane, digests) {
|
|
1340
|
+
#requestGraphics(pane, digests, preferInline = false) {
|
|
1287
1341
|
const channel = this.#panes.get(pane);
|
|
1288
1342
|
// Requests are only meaningful after an announcement; ignore stray ones.
|
|
1289
1343
|
if (!channel || channel.closed || !channel.graphicsAnnounced)
|
|
1290
1344
|
return;
|
|
1345
|
+
if (preferInline && channel.graphicsLocal) {
|
|
1346
|
+
for (const digest of digests) {
|
|
1347
|
+
if ((channel.graphicsInline?.size ?? 0) < GRAPHICS_MAX_QUEUED_DIGESTS)
|
|
1348
|
+
channel.graphicsInline?.add(digest);
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1291
1351
|
this.#queueGraphics(channel, digests);
|
|
1292
1352
|
}
|
|
1293
1353
|
#queueGraphics(channel, digests) {
|
|
@@ -1308,6 +1368,7 @@ export class PaneStreamLiveConnection {
|
|
|
1308
1368
|
try {
|
|
1309
1369
|
while (!this.#closed && !channel.closed && channel.graphicsQueue.length > 0) {
|
|
1310
1370
|
const digest = channel.graphicsQueue.shift();
|
|
1371
|
+
const inline = channel.graphicsInline?.delete(digest) ?? false;
|
|
1311
1372
|
const graphics = channel.delivery?.graphics;
|
|
1312
1373
|
const image = graphics?.image(digest);
|
|
1313
1374
|
if (!graphics || !image)
|
|
@@ -1334,6 +1395,49 @@ export class PaneStreamLiveConnection {
|
|
|
1334
1395
|
continue;
|
|
1335
1396
|
const format = raw ? (image.format === "rgba-zlib" ? "rgba" : "rgb") : image.format;
|
|
1336
1397
|
const total = data.byteLength;
|
|
1398
|
+
if (!inline && channel.graphicsLocal && !raw && this.#localImages) {
|
|
1399
|
+
for (const [id, lease] of this.#localLeases)
|
|
1400
|
+
if (lease.expiresAt <= Date.now())
|
|
1401
|
+
this.#localLeases.delete(id);
|
|
1402
|
+
if (this.#localLeases.size < 64) {
|
|
1403
|
+
try {
|
|
1404
|
+
const lease = await this.#localImages.acquire({
|
|
1405
|
+
generation: this.#localImages.identity.generation,
|
|
1406
|
+
client: this.#descriptor.leaseId,
|
|
1407
|
+
digest,
|
|
1408
|
+
bytes: data,
|
|
1409
|
+
});
|
|
1410
|
+
if (this.#closed || channel.closed || graphics.image(digest) !== image) {
|
|
1411
|
+
await this.#localImages.release(lease);
|
|
1412
|
+
continue;
|
|
1413
|
+
}
|
|
1414
|
+
this.#localLeases.set(lease.id, {
|
|
1415
|
+
pane: channel.semanticPaneId,
|
|
1416
|
+
expiresAt: lease.expiresAt,
|
|
1417
|
+
});
|
|
1418
|
+
this.#sendFrame(null, {
|
|
1419
|
+
type: "terminal-graphics-local",
|
|
1420
|
+
pane: channel.semanticPaneId,
|
|
1421
|
+
generation: lease.generation,
|
|
1422
|
+
leaseId: lease.id,
|
|
1423
|
+
path: lease.path,
|
|
1424
|
+
expiresAt: lease.expiresAt,
|
|
1425
|
+
blob: {
|
|
1426
|
+
digest,
|
|
1427
|
+
format,
|
|
1428
|
+
width: image.width,
|
|
1429
|
+
height: image.height,
|
|
1430
|
+
total,
|
|
1431
|
+
offset: 0,
|
|
1432
|
+
},
|
|
1433
|
+
});
|
|
1434
|
+
continue;
|
|
1435
|
+
}
|
|
1436
|
+
catch {
|
|
1437
|
+
/* Optional transport: retain the negotiated inline fallback. */
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1337
1441
|
for (let offset = 0; offset < total; offset += TERMINAL_GRAPHICS_BLOB_CHUNK_BYTES) {
|
|
1338
1442
|
while ((this.#socket.bufferedAmount ?? 0) > GRAPHICS_SEND_WINDOW_BYTES) {
|
|
1339
1443
|
if (this.#closed || channel.closed)
|
|
@@ -1347,6 +1451,20 @@ export class PaneStreamLiveConnection {
|
|
|
1347
1451
|
if (offset > 0 && graphics.image(digest) !== image)
|
|
1348
1452
|
break;
|
|
1349
1453
|
const end = Math.min(total, offset + TERMINAL_GRAPHICS_BLOB_CHUNK_BYTES);
|
|
1454
|
+
if (channel.graphicsBinary) {
|
|
1455
|
+
this.#sendEncoded(null, Buffer.from(encodeTerminalGraphicsBinary({
|
|
1456
|
+
pane: channel.semanticPaneId,
|
|
1457
|
+
blob: {
|
|
1458
|
+
digest,
|
|
1459
|
+
format,
|
|
1460
|
+
width: image.width,
|
|
1461
|
+
height: image.height,
|
|
1462
|
+
total,
|
|
1463
|
+
offset,
|
|
1464
|
+
},
|
|
1465
|
+
}, data.subarray(offset, end))), true);
|
|
1466
|
+
continue;
|
|
1467
|
+
}
|
|
1350
1468
|
this.#sendFrame(null, {
|
|
1351
1469
|
type: "terminal-graphics-blob",
|
|
1352
1470
|
pane: channel.semanticPaneId,
|
|
@@ -1666,7 +1784,12 @@ export class PaneStreamLiveConnection {
|
|
|
1666
1784
|
if (this.#closed || this.#socket.readyState !== WS_OPEN)
|
|
1667
1785
|
return;
|
|
1668
1786
|
const encoded = JSON.stringify(frame);
|
|
1669
|
-
|
|
1787
|
+
this.#sendEncoded(pane, encoded, false);
|
|
1788
|
+
}
|
|
1789
|
+
#sendEncoded(pane, encoded, binary) {
|
|
1790
|
+
if (this.#closed || this.#socket.readyState !== WS_OPEN)
|
|
1791
|
+
return;
|
|
1792
|
+
const bytes = Buffer.byteLength(encoded);
|
|
1670
1793
|
if (pane !== null) {
|
|
1671
1794
|
this.#ledger.take(this.#clientId, pane, "ws-send-buffer", bytes);
|
|
1672
1795
|
if (this.#deliveryAcks)
|
|
@@ -1675,7 +1798,7 @@ export class PaneStreamLiveConnection {
|
|
|
1675
1798
|
this.#sendQueue.push({ pane, remaining: bytes });
|
|
1676
1799
|
this.#sentBytesTotal += bytes;
|
|
1677
1800
|
try {
|
|
1678
|
-
this.#socket.send(encoded, { binary
|
|
1801
|
+
this.#socket.send(encoded, { binary });
|
|
1679
1802
|
}
|
|
1680
1803
|
catch {
|
|
1681
1804
|
this.close(1011, "stream-unavailable");
|
|
@@ -1909,6 +2032,24 @@ export class PaneStreamLiveConnection {
|
|
|
1909
2032
|
});
|
|
1910
2033
|
return;
|
|
1911
2034
|
}
|
|
2035
|
+
if (frame.type === "terminal-projection-receipt") {
|
|
2036
|
+
const channel = this.#deliveryChannel(frame.receipt);
|
|
2037
|
+
if (!channel)
|
|
2038
|
+
return;
|
|
2039
|
+
if (!channel.delivery?.projectionReceipt)
|
|
2040
|
+
return this.#failProtocol("protocol-error");
|
|
2041
|
+
channel.delivery.projectionReceipt(frame.receipt);
|
|
2042
|
+
return;
|
|
2043
|
+
}
|
|
2044
|
+
if (frame.type === "terminal-projection-materialize") {
|
|
2045
|
+
const channel = this.#deliveryChannel(frame.request);
|
|
2046
|
+
if (!channel)
|
|
2047
|
+
return;
|
|
2048
|
+
if (!channel.delivery?.materialize)
|
|
2049
|
+
return this.#failProtocol("protocol-error");
|
|
2050
|
+
channel.delivery.materialize(frame.request);
|
|
2051
|
+
return;
|
|
2052
|
+
}
|
|
1912
2053
|
if (frame.type === "terminal-delivery-ack") {
|
|
1913
2054
|
const channel = this.#deliveryChannel(frame.ack);
|
|
1914
2055
|
if (!channel)
|
|
@@ -1941,7 +2082,23 @@ export class PaneStreamLiveConnection {
|
|
|
1941
2082
|
return;
|
|
1942
2083
|
}
|
|
1943
2084
|
if (frame.type === "terminal-graphics-request") {
|
|
1944
|
-
this.#requestGraphics(frame.pane, frame.digests);
|
|
2085
|
+
this.#requestGraphics(frame.pane, frame.digests, frame.preferInline === true);
|
|
2086
|
+
return;
|
|
2087
|
+
}
|
|
2088
|
+
if (frame.type === "terminal-graphics-local-release") {
|
|
2089
|
+
const lease = this.#localLeases.get(frame.leaseId);
|
|
2090
|
+
if (lease &&
|
|
2091
|
+
lease.pane === frame.pane &&
|
|
2092
|
+
frame.generation === this.#localImages?.identity.generation) {
|
|
2093
|
+
this.#localLeases.delete(frame.leaseId);
|
|
2094
|
+
void this.#localImages
|
|
2095
|
+
?.release({
|
|
2096
|
+
generation: frame.generation,
|
|
2097
|
+
client: this.#descriptor.leaseId,
|
|
2098
|
+
id: frame.leaseId,
|
|
2099
|
+
})
|
|
2100
|
+
.catch(() => { });
|
|
2101
|
+
}
|
|
1945
2102
|
return;
|
|
1946
2103
|
}
|
|
1947
2104
|
if (frame.type === "terminal-cell-pixels") {
|
|
@@ -1995,7 +2152,7 @@ export class PaneStreamLiveConnection {
|
|
|
1995
2152
|
}
|
|
1996
2153
|
return;
|
|
1997
2154
|
}
|
|
1998
|
-
this.#acceptInput(frame.pane, frame.seq, frame.kind, frame.data, byteLength, frame.performanceTraceId, frame.causalProbe, ingressAtMicros, callbackAtSharedMicros, ingressAtSharedMicros);
|
|
2155
|
+
this.#acceptInput(frame.pane, frame.seq, frame.kind, frame.data, byteLength, frame.performanceTraceId, frame.causalProbe, ingressAtMicros, callbackAtSharedMicros, ingressAtSharedMicros, frame.expectedLifetime);
|
|
1999
2156
|
};
|
|
2000
2157
|
#acceptAuthorityGeneration(generation) {
|
|
2001
2158
|
if (!this.#sessionRuntimeBinding || generation !== this.#sessionRuntimeBinding.generation) {
|
|
@@ -2121,7 +2278,19 @@ export class PaneStreamLiveConnection {
|
|
|
2121
2278
|
this.#ledger.give(this.#clientId, pane, "renderer-backlog", returned);
|
|
2122
2279
|
this.#evaluateResume(pane);
|
|
2123
2280
|
}
|
|
2124
|
-
#acceptInput(pane, seq, kind, data, frameBytes, performanceTraceId, causalProbe, ingressAtMicros = null, callbackAtSharedMicros = null, ingressAtSharedMicros = null) {
|
|
2281
|
+
#acceptInput(pane, seq, kind, data, frameBytes, performanceTraceId, causalProbe, ingressAtMicros = null, callbackAtSharedMicros = null, ingressAtSharedMicros = null, expectedLifetime) {
|
|
2282
|
+
if (kind === "bytes" &&
|
|
2283
|
+
(!this.#sessionRuntimeBinding ||
|
|
2284
|
+
!this.#descriptor.terminalDelivery?.protocolVersions.includes(PANE_STREAM_INPUT_BYTES_CAPABILITY_VERSION))) {
|
|
2285
|
+
this.#failProtocol("input-rejected");
|
|
2286
|
+
return;
|
|
2287
|
+
}
|
|
2288
|
+
if (expectedLifetime &&
|
|
2289
|
+
(!this.#sessionRuntimeBinding ||
|
|
2290
|
+
!this.#descriptor.terminalDelivery?.protocolVersions.includes(PANE_STREAM_INPUT_LIFETIME_CAPABILITY_VERSION))) {
|
|
2291
|
+
this.#failProtocol("input-rejected");
|
|
2292
|
+
return;
|
|
2293
|
+
}
|
|
2125
2294
|
if (this.#descriptor.viewerMode !== "interactive") {
|
|
2126
2295
|
this.#failProtocol("input-rejected");
|
|
2127
2296
|
return;
|
|
@@ -2213,11 +2382,13 @@ export class PaneStreamLiveConnection {
|
|
|
2213
2382
|
});
|
|
2214
2383
|
}
|
|
2215
2384
|
}
|
|
2216
|
-
: undefined);
|
|
2385
|
+
: undefined, expectedLifetime);
|
|
2217
2386
|
else if (kind === "text")
|
|
2218
2387
|
channel.sub.sendText(data);
|
|
2219
|
-
else
|
|
2388
|
+
else if (kind === "key")
|
|
2220
2389
|
channel.sub.sendKey(data);
|
|
2390
|
+
else
|
|
2391
|
+
throw new Error("Byte input requires semantic runtime");
|
|
2221
2392
|
let ackStartedAtMicros = null;
|
|
2222
2393
|
let ackStartedAtSharedMicros = null;
|
|
2223
2394
|
if (trace) {
|
|
@@ -16,6 +16,7 @@ export class PaneStreamRuntime {
|
|
|
16
16
|
const transportBinder = new SessionRuntimeTransportBinder(options.sessionRuntimeRegistry);
|
|
17
17
|
this.coordinator = new PaneStreamAdmissionCoordinator({
|
|
18
18
|
...options.admission,
|
|
19
|
+
localImages: options.localImages,
|
|
19
20
|
daemonInstanceId: options.daemonInstanceId,
|
|
20
21
|
webSocketUrl: options.webSocketUrl,
|
|
21
22
|
leaseManager,
|
|
@@ -472,10 +472,10 @@ export class SessionRuntimeRegistry {
|
|
|
472
472
|
return { status: "retired" };
|
|
473
473
|
return result;
|
|
474
474
|
}
|
|
475
|
-
async describeSessionAuthority(session) {
|
|
475
|
+
async describeSessionAuthority(session, expectedSemanticPaneIds) {
|
|
476
476
|
const runtime = this.#runtime(session);
|
|
477
477
|
await runtime.whenReady();
|
|
478
|
-
const authority = await this.#mirror.describeSessionAuthority(session);
|
|
478
|
+
const authority = await this.#mirror.describeSessionAuthority(session, expectedSemanticPaneIds);
|
|
479
479
|
if (this.#disposed || this.#sessions.get(session) !== runtime) {
|
|
480
480
|
const error = new Error(`session runtime ${session} changed topology`);
|
|
481
481
|
error.name = "MirrorTopologyChangedError";
|
|
@@ -1003,7 +1003,7 @@ class SessionRuntime {
|
|
|
1003
1003
|
return Promise.reject(error);
|
|
1004
1004
|
}
|
|
1005
1005
|
}
|
|
1006
|
-
sendInput(clientId, lease, semanticPaneId, rawInput, performanceTraceId, rawCausalProbe, onCausalResult) {
|
|
1006
|
+
sendInput(clientId, lease, semanticPaneId, rawInput, performanceTraceId, rawCausalProbe, onCausalResult, expectedLifetime) {
|
|
1007
1007
|
this.assertController(lease, clientId);
|
|
1008
1008
|
const inputLease = this.#authority.leaseFor(clientId, "input");
|
|
1009
1009
|
if (!inputLease) {
|
|
@@ -1037,7 +1037,11 @@ class SessionRuntime {
|
|
|
1037
1037
|
})
|
|
1038
1038
|
: null;
|
|
1039
1039
|
const started = this.#observability.enabled ? this.#observability.nowMicros() : 0;
|
|
1040
|
-
const replicaOwner =
|
|
1040
|
+
const replicaOwner = expectedLifetime
|
|
1041
|
+
? this.#terminalReplicas.get(semanticPaneId)
|
|
1042
|
+
: this.#terminalReplicaOwner(semanticPaneId);
|
|
1043
|
+
if (!replicaOwner)
|
|
1044
|
+
throw new Error("Input target lifetime unavailable");
|
|
1041
1045
|
if (causalProbe) {
|
|
1042
1046
|
this.#activeCausalCellProbes += 1;
|
|
1043
1047
|
let settled = false;
|
|
@@ -1061,6 +1065,13 @@ class SessionRuntime {
|
|
|
1061
1065
|
this.#outputTraces.arm(semanticPaneId, trace);
|
|
1062
1066
|
}
|
|
1063
1067
|
try {
|
|
1068
|
+
// Synchronous final admission: never create a new owner to satisfy a stale
|
|
1069
|
+
// held-input fence, and never await between this check and mirror dispatch.
|
|
1070
|
+
if (expectedLifetime &&
|
|
1071
|
+
(expectedLifetime.generation !== this.generation ||
|
|
1072
|
+
this.#terminalReplicas.get(semanticPaneId) !== replicaOwner ||
|
|
1073
|
+
replicaOwner.inputAdmissionIncarnation() !== expectedLifetime.incarnation))
|
|
1074
|
+
throw new Error("Input target lifetime changed");
|
|
1064
1075
|
if (input.kind === "text")
|
|
1065
1076
|
this.#mirror.sendText(this.session, semanticPaneId, input.data, performanceTraceId, causalProbe !== null);
|
|
1066
1077
|
else if (input.kind === "bytes")
|
|
@@ -1564,9 +1575,9 @@ class SessionRuntimeConsumerImpl {
|
|
|
1564
1575
|
this.#assertOpen();
|
|
1565
1576
|
return this.#runtime.submitIntent(this.clientId, lease, operationId, intent);
|
|
1566
1577
|
}
|
|
1567
|
-
sendInput(lease, semanticPaneId, input, performanceTraceId, causalProbe, onCausalResult) {
|
|
1578
|
+
sendInput(lease, semanticPaneId, input, performanceTraceId, causalProbe, onCausalResult, expectedLifetime) {
|
|
1568
1579
|
this.#assertOpen();
|
|
1569
|
-
this.#runtime.sendInput(this.clientId, lease, semanticPaneId, input, performanceTraceId, causalProbe, onCausalResult);
|
|
1580
|
+
this.#runtime.sendInput(this.clientId, lease, semanticPaneId, input, performanceTraceId, causalProbe, onCausalResult, expectedLifetime);
|
|
1570
1581
|
}
|
|
1571
1582
|
failCausalCellProbe(semanticPaneId, traceId, reason) {
|
|
1572
1583
|
this.#runtime.failCausalCellProbe(semanticPaneId, traceId, reason);
|
|
@@ -1642,6 +1653,8 @@ class SessionRuntimeConsumerImpl {
|
|
|
1642
1653
|
negotiation: upstream.negotiation,
|
|
1643
1654
|
closed: upstream.closed,
|
|
1644
1655
|
ack: (ack) => upstream.ack(ack),
|
|
1656
|
+
...(upstream.projectionReceipt ? { projectionReceipt: upstream.projectionReceipt } : {}),
|
|
1657
|
+
...(upstream.materialize ? { materialize: upstream.materialize } : {}),
|
|
1645
1658
|
nack: (nack) => upstream.nack(nack),
|
|
1646
1659
|
setVisibility: (visibility) => upstream.setVisibility(visibility),
|
|
1647
1660
|
close: async () => {
|