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.
Files changed (81) hide show
  1. package/bin/cli.js +4321 -2596
  2. package/package.json +1 -1
  3. package/packages/contracts/src/index.ts +4 -0
  4. package/packages/contracts/src/pane-stream.ts +72 -0
  5. package/packages/contracts/src/terminal-delivery-binary-chunk.ts +55 -0
  6. package/packages/contracts/src/terminal-delivery.ts +17 -2
  7. package/packages/contracts/src/terminal-graphics.ts +68 -0
  8. package/packages/contracts/src/terminal-projection.ts +153 -0
  9. package/packages/core/src/fixtures/terminal-projection-vectors.json +585 -0
  10. package/packages/core/src/index.ts +2 -0
  11. package/packages/core/src/terminal-delivery-binary.ts +160 -0
  12. package/packages/core/src/terminal-delivery.ts +371 -16
  13. package/packages/core/src/terminal-projection-vectors.mjs +82 -0
  14. package/packages/core/src/terminal-projection.ts +476 -0
  15. package/packages/core/src/terminal-replica-hash-cache.ts +8 -1
  16. package/packages/core/src/terminal-replica.ts +1 -1
  17. package/packages/daemon/dist/lib/daemon-embed.js +39 -3
  18. package/packages/daemon/dist/lib/embedded-tmux-server-owners.js +1 -0
  19. package/packages/daemon/dist/lib/tmux-server-owner.js +1 -0
  20. package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/Info.plist +2 -2
  21. package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/MacOS/tmux-ide-notifier +0 -0
  22. package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/Resources/Assets.car +0 -0
  23. package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/_CodeSignature/CodeResources +2 -2
  24. package/packages/daemon/dist/terminal/graphics/local-image-owner.js +330 -0
  25. package/packages/daemon/dist/terminal/graphics/local-image-store.js +255 -0
  26. package/packages/daemon/dist/terminal/mirror/mirror-service.js +12 -2
  27. package/packages/daemon/dist/terminal/mirror/session-channel.js +46 -23
  28. package/packages/daemon/dist/terminal/pane-stream/pane-stream-websocket.js +190 -19
  29. package/packages/daemon/dist/terminal/pane-stream/runtime.js +1 -0
  30. package/packages/daemon/dist/terminal/session-runtime/registry.js +19 -6
  31. package/packages/daemon/dist/terminal/session-runtime/terminal-delivery-hub.js +146 -16
  32. package/packages/daemon/dist/terminal/session-runtime/terminal-projection-delivery.js +32 -0
  33. package/packages/daemon/dist/terminal/session-runtime/terminal-replica-interpreter.js +95 -36
  34. package/packages/daemon/dist/terminal/session-runtime/terminal-replica-owner.js +5 -0
  35. package/packages/daemon/dist/terminal/session-runtime/transport-binding.js +2 -2
  36. package/packages/daemon/dist/tui/mirror/open-tui-workspace-runtime-port.js +4 -0
  37. package/packages/daemon/dist/tui/mirror/runtime/application-generation-starter.js +16 -1
  38. package/packages/daemon/dist/tui/mirror/runtime/application-machine-agent-navigation.js +13 -0
  39. package/packages/daemon/dist/tui/mirror/runtime/application-machine-navigation.js +12 -4
  40. package/packages/daemon/dist/tui/mirror/runtime/application-retained-agent.js +18 -0
  41. package/packages/daemon/dist/tui/mirror/runtime/application-terminal-interaction-controller.js +90 -5
  42. package/packages/daemon/dist/tui/mirror/runtime/application-terminal-workspace.jsx +13 -1
  43. package/packages/daemon/dist/tui/mirror/runtime/open-tui-generation-host.js +2 -2
  44. package/packages/daemon/dist/tui/mirror/runtime/renderer-probe.js +195 -0
  45. package/packages/daemon/dist/tui/mirror/runtime/runtime-layout-presentation.js +6 -3
  46. package/packages/daemon/dist/tui/mirror/runtime/select-terminal-pane.js +7 -2
  47. package/packages/daemon/dist/tui/mirror/runtime/terminal-fast-lane-renderer-adapter.js +6 -0
  48. package/packages/daemon/dist/tui/mirror/runtime/terminal-selection-waiter.js +36 -0
  49. package/packages/daemon/dist/tui/mirror/semantic-pane-render-source.js +4 -0
  50. package/packages/daemon/src/lib/daemon-embed.ts +38 -2
  51. package/packages/daemon/src/lib/embedded-tmux-server-owners.ts +3 -0
  52. package/packages/daemon/src/lib/tmux-server-owner.ts +3 -0
  53. package/packages/daemon/src/terminal/graphics/local-image-owner.ts +346 -0
  54. package/packages/daemon/src/terminal/graphics/local-image-store.ts +286 -0
  55. package/packages/daemon/src/terminal/mirror/mirror-service.ts +19 -2
  56. package/packages/daemon/src/terminal/mirror/session-channel.ts +57 -9
  57. package/packages/daemon/src/terminal/pane-stream/pane-stream-websocket.ts +248 -16
  58. package/packages/daemon/src/terminal/pane-stream/runtime.ts +3 -0
  59. package/packages/daemon/src/terminal/session-runtime/registry.ts +25 -3
  60. package/packages/daemon/src/terminal/session-runtime/runtime-observability.ts +1 -1
  61. package/packages/daemon/src/terminal/session-runtime/terminal-delivery-hub.ts +216 -22
  62. package/packages/daemon/src/terminal/session-runtime/terminal-delivery-observation-identity.ts +7 -4
  63. package/packages/daemon/src/terminal/session-runtime/terminal-projection-delivery.ts +59 -0
  64. package/packages/daemon/src/terminal/session-runtime/terminal-replica-interpreter.ts +102 -44
  65. package/packages/daemon/src/terminal/session-runtime/terminal-replica-owner.ts +6 -0
  66. package/packages/daemon/src/terminal/session-runtime/transport-binding.ts +3 -0
  67. package/packages/daemon/src/tui/mirror/open-tui-workspace-runtime-port.ts +6 -0
  68. package/packages/daemon/src/tui/mirror/runtime/application-generation-starter.ts +17 -1
  69. package/packages/daemon/src/tui/mirror/runtime/application-machine-agent-navigation.ts +15 -0
  70. package/packages/daemon/src/tui/mirror/runtime/application-machine-navigation.ts +18 -4
  71. package/packages/daemon/src/tui/mirror/runtime/application-retained-agent.ts +29 -0
  72. package/packages/daemon/src/tui/mirror/runtime/application-terminal-interaction-controller.ts +89 -6
  73. package/packages/daemon/src/tui/mirror/runtime/application-terminal-workspace.tsx +20 -1
  74. package/packages/daemon/src/tui/mirror/runtime/open-tui-generation-host.ts +2 -2
  75. package/packages/daemon/src/tui/mirror/runtime/renderer-probe.ts +257 -0
  76. package/packages/daemon/src/tui/mirror/runtime/runtime-layout-presentation.ts +7 -4
  77. package/packages/daemon/src/tui/mirror/runtime/select-terminal-pane.ts +9 -1
  78. package/packages/daemon/src/tui/mirror/runtime/terminal-fast-lane-renderer-adapter.ts +15 -0
  79. package/packages/daemon/src/tui/mirror/runtime/terminal-selection-waiter.ts +36 -0
  80. package/packages/daemon/src/tui/mirror/semantic-pane-render-source.ts +4 -0
  81. 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 the background discovery path, this query awaits descriptor
416
- * reconciliation before projecting and rejects incomplete identity rather
417
- * than returning the previous descriptor snapshot.
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
- const flight = this.refreshTrustedInventory(expectedRuntimeSessionId)
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
- descriptors.some((descriptor) => {
2837
- const pane = this.panesByRuntime.get(descriptor.runtimePaneId);
2838
- const window = confirmedWindowStage.windows.get(descriptor.windowId);
2839
- return (!pane ||
2840
- !window?.semanticId ||
2841
- descriptor.semanticPaneId !== pane.semanticId ||
2842
- descriptor.semanticWindowId !== window.semanticId ||
2843
- !WorkspaceIdSchemaZ.safeParse(pane.semanticId).success ||
2844
- !WorkspaceIdSchemaZ.safeParse(window.semanticId).success);
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
- for (const runtimeId of this.layoutByWindow.keys())
3315
- this.emitLayout(runtimeId);
3316
- this.emitLayoutAuthority();
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.delivery") {
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.#sendFrame(pane, {
1214
- type: "terminal-delivery-chunk",
1215
- pane,
1216
- transactionId: message.transactionId,
1217
- index: message.index,
1218
- data: Buffer.from(message.bytes).toString("base64"),
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
- const bytes = Buffer.byteLength(encoded, "utf8");
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: false });
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 = this.#terminalReplicaOwner(semanticPaneId);
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 () => {