tmux-ide 2.9.4 → 2.9.5

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 (80) hide show
  1. package/bin/cli.js +4354 -3376
  2. package/package.json +7 -6
  3. package/packages/contracts/src/session-runtime.ts +2 -0
  4. package/packages/contracts/src/terminal-delivery.ts +4 -0
  5. package/packages/core/src/terminal-compact-capability.ts +31 -1
  6. package/packages/core/src/terminal-delivery.ts +234 -32
  7. package/packages/core/src/terminal-replica-hash-cache.ts +162 -21
  8. package/packages/core/src/terminal-replica-owned-row.ts +165 -10
  9. package/packages/core/src/terminal-replica.ts +43 -21
  10. package/packages/daemon/dist/lib/daemon-embed.js +19 -34
  11. package/packages/daemon/dist/lib/runtime-trace-writer.js +133 -0
  12. package/packages/daemon/dist/lib/tmux-client-execution.js +37 -2
  13. package/packages/daemon/dist/lib/tmux-server-owner.js +20 -4
  14. package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/Info.plist +2 -2
  15. package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/MacOS/tmux-ide-notifier +0 -0
  16. package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/Resources/Assets.car +0 -0
  17. package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/_CodeSignature/CodeResources +2 -2
  18. package/packages/daemon/dist/native/tmux/darwin-arm64/manifest.json +5 -1
  19. package/packages/daemon/dist/native/tmux/darwin-arm64/tmux +0 -0
  20. package/packages/daemon/dist/native/tmux/darwin-x64/manifest.json +5 -1
  21. package/packages/daemon/dist/native/tmux/darwin-x64/tmux +0 -0
  22. package/packages/daemon/dist/native/tmux/linux-arm64/manifest.json +5 -1
  23. package/packages/daemon/dist/native/tmux/linux-arm64/tmux +0 -0
  24. package/packages/daemon/dist/native/tmux/linux-x64/licenses/libevent_core-2.1.so.7.txt +0 -0
  25. package/packages/daemon/dist/native/tmux/linux-x64/manifest.json +5 -1
  26. package/packages/daemon/dist/native/tmux/linux-x64/tmux +0 -0
  27. package/packages/daemon/dist/terminal/attachments/native-runtime.js +6 -6
  28. package/packages/daemon/dist/terminal/mirror/control-channel.js +379 -75
  29. package/packages/daemon/dist/terminal/mirror/mirror-service.js +43 -3
  30. package/packages/daemon/dist/terminal/mirror/native-grid-projection.js +20 -11
  31. package/packages/daemon/dist/terminal/mirror/session-channel.js +639 -811
  32. package/packages/daemon/dist/terminal/mirror/stock-pane-snapshot.js +177 -0
  33. package/packages/daemon/dist/terminal/session-runtime/causal-cell-ledger.js +1 -1
  34. package/packages/daemon/dist/terminal/session-runtime/registry.js +145 -15
  35. package/packages/daemon/dist/terminal/session-runtime/stock-capture-fidelity.js +12 -0
  36. package/packages/daemon/dist/terminal/session-runtime/terminal-delivery-hub.js +128 -10
  37. package/packages/daemon/dist/terminal/session-runtime/terminal-replica-interpreter.js +21 -4
  38. package/packages/daemon/dist/terminal/session-runtime/terminal-replica-owner.js +19 -2
  39. package/packages/daemon/dist/terminal/session-runtime/xterm-terminal-interpreter-backend.js +17 -19
  40. package/packages/daemon/dist/tui/mirror/open-tui-workspace-runtime-port.js +64 -4
  41. package/packages/daemon/dist/tui/mirror/pane-surface.jsx +10 -1
  42. package/packages/daemon/dist/tui/mirror/reference-performance-trace.js +117 -71
  43. package/packages/daemon/dist/tui/mirror/runtime/application-terminal-input-ingress.js +27 -3
  44. package/packages/daemon/dist/tui/mirror/runtime/pane-scoped-terminal-surface.jsx +17 -4
  45. package/packages/daemon/dist/tui/mirror/runtime/terminal-fast-lane-renderer-adapter.js +19 -0
  46. package/packages/daemon/dist/tui/mirror/runtime/terminal-input-adapter.js +10 -2
  47. package/packages/daemon/dist/tui/mirror/testing/styled-frame-oracle.js +156 -0
  48. package/packages/daemon/src/lib/daemon-embed.ts +22 -34
  49. package/packages/daemon/src/lib/runtime-trace-writer.ts +132 -0
  50. package/packages/daemon/src/lib/tmux-client-execution.ts +47 -2
  51. package/packages/daemon/src/lib/tmux-server-owner.ts +19 -3
  52. package/packages/daemon/src/terminal/attachments/native-runtime.ts +6 -0
  53. package/packages/daemon/src/terminal/mirror/control-channel.ts +428 -84
  54. package/packages/daemon/src/terminal/mirror/events.ts +7 -7
  55. package/packages/daemon/src/terminal/mirror/mirror-service.ts +49 -3
  56. package/packages/daemon/src/terminal/mirror/native-grid-projection.ts +21 -11
  57. package/packages/daemon/src/terminal/mirror/pane-feed.ts +9 -4
  58. package/packages/daemon/src/terminal/mirror/session-channel.ts +705 -859
  59. package/packages/daemon/src/terminal/mirror/stock-pane-snapshot.ts +228 -0
  60. package/packages/daemon/src/terminal/session-runtime/causal-cell-ledger.ts +1 -1
  61. package/packages/daemon/src/terminal/session-runtime/registry.ts +203 -32
  62. package/packages/daemon/src/terminal/session-runtime/runtime-observability.ts +7 -0
  63. package/packages/daemon/src/terminal/session-runtime/stock-capture-fidelity.ts +13 -0
  64. package/packages/daemon/src/terminal/session-runtime/terminal-delivery-hub.ts +142 -8
  65. package/packages/daemon/src/terminal/session-runtime/terminal-replica-interpreter.ts +26 -3
  66. package/packages/daemon/src/terminal/session-runtime/terminal-replica-owner.ts +25 -1
  67. package/packages/daemon/src/terminal/session-runtime/xterm-terminal-interpreter-backend.ts +26 -20
  68. package/packages/daemon/src/tui/mirror/open-tui-workspace-runtime-port.ts +63 -4
  69. package/packages/daemon/src/tui/mirror/pane-surface.tsx +9 -1
  70. package/packages/daemon/src/tui/mirror/reference-performance-trace.ts +141 -90
  71. package/packages/daemon/src/tui/mirror/runtime/application-terminal-input-ingress.ts +27 -4
  72. package/packages/daemon/src/tui/mirror/runtime/pane-scoped-terminal-surface.tsx +61 -31
  73. package/packages/daemon/src/tui/mirror/runtime/terminal-fast-lane-renderer-adapter.ts +31 -3
  74. package/packages/daemon/src/tui/mirror/runtime/terminal-input-adapter.ts +9 -6
  75. package/packages/daemon/src/tui/mirror/testing/styled-frame-oracle.ts +209 -0
  76. package/packages/daemon-client/src/terminal-fast-lane.ts +53 -4
  77. package/packages/daemon-client/src/workspace-client-types.ts +1 -0
  78. package/packages/daemon-client/src/workspace-client.ts +78 -5
  79. package/packages/tmux-bridge/src/index.ts +1 -0
  80. package/packages/tmux-bridge/src/runner.ts +25 -1
@@ -1,16 +1,31 @@
1
+ import {
2
+ isSchemaValidProjectedTerminalReplicaRow,
3
+ TERMINAL_REPLICA_DEFAULT_COLOR,
4
+ } from "./terminal-replica-owned-row.ts";
1
5
  import type { TerminalReplicaColor, TerminalReplicaRow } from "@tmux-ide/contracts";
2
6
  import { createBufferedFnv64, type BufferedFnv64 } from "./terminal-fnv64-wasm.ts";
3
7
 
4
8
  const ROW_HASH_CACHE = new WeakMap<object, string>();
5
9
  const DEEPLY_FROZEN_ROWS = new WeakSet<object>();
6
10
  const UTF8_ENCODER = new TextEncoder();
11
+ // Fixed ASCII headers cover common terminal strings without retaining values.
12
+ const SHORT_ASCII_HEADERS = Array.from({ length: 33 }, (_, length) => `s${length}:`);
7
13
 
8
14
  interface CanonicalKeyOrder {
9
- readonly original: string[];
10
- readonly sorted: string[];
15
+ readonly original: readonly string[];
16
+ readonly sorted: readonly string[];
11
17
  readonly tokens: readonly (string | null)[] | null;
12
18
  }
13
19
 
20
+ // Only this exact internally constructed singleton proves the fixed layout.
21
+ // Foreign defaults (including frozen lookalikes) retain generic key discovery.
22
+ const DEFAULT_COLOR_KEYS = Object.freeze(["kind"]);
23
+ const DEFAULT_COLOR_KEY_ORDER: CanonicalKeyOrder = Object.freeze({
24
+ original: DEFAULT_COLOR_KEYS,
25
+ sorted: DEFAULT_COLOR_KEYS,
26
+ tokens: Object.freeze(["s4:kind;"]),
27
+ });
28
+
14
29
  /**
15
30
  * Streaming FNV-1a64 writer for the canonical terminal encoding.
16
31
  *
@@ -43,6 +58,7 @@ class CanonicalFnv64 {
43
58
  #keyOrders: CanonicalKeyOrder[] | null = null;
44
59
 
45
60
  keyOrder(record: Record<string, unknown>): CanonicalKeyOrder {
61
+ if (record === TERMINAL_REPLICA_DEFAULT_COLOR) return DEFAULT_COLOR_KEY_ORDER;
46
62
  const keys = Object.keys(record);
47
63
  if (keys.length > 32) return { original: keys, sorted: keys.sort(), tokens: null };
48
64
  for (const order of this.#keyOrders ?? []) {
@@ -116,7 +132,7 @@ class CanonicalFnv64 {
116
132
  }
117
133
  }
118
134
  if (ascii) {
119
- this.ascii(`s${value.length}:`);
135
+ this.ascii(SHORT_ASCII_HEADERS[value.length] ?? `s${value.length}:`);
120
136
  this.ascii(value);
121
137
  this.ascii(";");
122
138
  return value.length;
@@ -129,6 +145,12 @@ class CanonicalFnv64 {
129
145
  }
130
146
 
131
147
  number(value: number): void {
148
+ // Common terminal widths and default attributes share exact canonical bytes.
149
+ // Equality intentionally preserves String(-0) === "0"; all other numbers
150
+ // retain the original formatting path.
151
+ if (value === 0) return void this.ascii("d1:0;");
152
+ if (value === 1) return void this.ascii("d1:1;");
153
+ if (value === 2) return void this.ascii("d1:2;");
132
154
  const text = String(value);
133
155
  this.ascii(`d${text.length}:${text};`);
134
156
  }
@@ -137,6 +159,36 @@ class CanonicalFnv64 {
137
159
  this.ascii(value ? "b1;" : "b0;");
138
160
  }
139
161
 
162
+ #projectedRow(row: TerminalReplicaRow): void {
163
+ this.ascii("o2:s5:cells;");
164
+ const cells = row.cells;
165
+ this.ascii(`a${cells.length}:`);
166
+ let index = 0;
167
+ // Preserve dynamic array iteration. A replaced inherited iterator can yield
168
+ // foreign values; only the exact immutable cell at this slot earns the path.
169
+ for (const cell of cells) {
170
+ if (index < cells.length && cell === cells[index]) {
171
+ this.ascii("o5:s10:attributes;");
172
+ this.number(cell.attributes);
173
+ this.ascii("s10:background;");
174
+ writeColor(this, cell.background);
175
+ this.ascii("s10:foreground;");
176
+ writeColor(this, cell.foreground);
177
+ this.ascii("s8:grapheme;");
178
+ this.string(cell.grapheme);
179
+ this.ascii("s5:width;");
180
+ this.number(cell.width);
181
+ this.ascii(";");
182
+ } else {
183
+ this.value(cell);
184
+ }
185
+ index++;
186
+ }
187
+ this.ascii(";s7:wrapped;");
188
+ this.boolean(row.wrapped);
189
+ this.ascii(";");
190
+ }
191
+
140
192
  value(value: unknown): void {
141
193
  if (value === null) {
142
194
  this.ascii("n;");
@@ -160,6 +212,10 @@ class CanonicalFnv64 {
160
212
  this.ascii(";");
161
213
  return;
162
214
  }
215
+ if (isSchemaValidProjectedTerminalReplicaRow(value)) {
216
+ this.#projectedRow(value as TerminalReplicaRow);
217
+ return;
218
+ }
163
219
  const record = value as Record<string, unknown>;
164
220
  const { sorted: keys, tokens } = this.keyOrder(record);
165
221
  this.ascii(`o${keys.length}:`);
@@ -198,22 +254,22 @@ export async function hashCanonicalTerminalValueCooperatively(
198
254
  work = 0;
199
255
  return true;
200
256
  };
201
- type WorkItem =
202
- | { readonly kind: "value"; readonly value: unknown }
203
- | { readonly kind: "ascii"; readonly value: string }
204
- | { readonly kind: "string"; readonly value: string };
205
- const stack: WorkItem[] = [{ kind: "value", value }];
206
- while (stack.length > 0) {
207
- const item = stack.pop()!;
208
- if (item.kind === "ascii") {
209
- if (checkpoint(hash.ascii(item.value))) await yieldControl();
257
+ // Parallel stacks preserve the traversal and yield order without allocating
258
+ // a wrapper object for every value/key/token. pop() drops consumed references.
259
+ const kinds: ("value" | "ascii" | "string")[] = ["value"];
260
+ const values: unknown[] = [value];
261
+ while (values.length > 0) {
262
+ const kind = kinds.pop()!;
263
+ const value = values.pop();
264
+ if (kind === "ascii") {
265
+ if (checkpoint(hash.ascii(value as string))) await yieldControl();
210
266
  continue;
211
267
  }
212
- if (item.kind === "string") {
213
- if (checkpoint(hash.string(item.value) + 8)) await yieldControl();
268
+ if (kind === "string") {
269
+ if (checkpoint(hash.string(value as string) + 8)) await yieldControl();
214
270
  continue;
215
271
  }
216
- const entry = item.value;
272
+ const entry = value;
217
273
  if (entry === null) {
218
274
  hash.ascii("n;");
219
275
  if (checkpoint(2)) await yieldControl();
@@ -235,19 +291,23 @@ export async function hashCanonicalTerminalValueCooperatively(
235
291
  }
236
292
  if (Array.isArray(entry)) {
237
293
  if (checkpoint(hash.ascii(`a${entry.length}:`) + 1)) await yieldControl();
238
- stack.push({ kind: "ascii", value: ";" });
239
- for (let index = entry.length - 1; index >= 0; index -= 1)
240
- stack.push({ kind: "value", value: entry[index] });
294
+ kinds.push("ascii");
295
+ values.push(";");
296
+ for (let index = entry.length - 1; index >= 0; index -= 1) {
297
+ kinds.push("value");
298
+ values.push(entry[index]);
299
+ }
241
300
  continue;
242
301
  }
243
302
  const record = entry as Record<string, unknown>;
244
303
  const keys = hash.keyOrder(record).sorted;
245
304
  if (checkpoint(hash.ascii(`o${keys.length}:`) + keys.length)) await yieldControl();
246
- stack.push({ kind: "ascii", value: ";" });
305
+ kinds.push("ascii");
306
+ values.push(";");
247
307
  for (let index = keys.length - 1; index >= 0; index -= 1) {
248
308
  const key = keys[index]!;
249
- stack.push({ kind: "value", value: record[key] });
250
- stack.push({ kind: "string", value: key });
309
+ kinds.push("value", "string");
310
+ values.push(record[key], key);
251
311
  }
252
312
  }
253
313
  return hash.digest();
@@ -473,6 +533,87 @@ export async function hashTerminalReplicaRowRunsCooperatively(
473
533
  return digest;
474
534
  }
475
535
 
536
+ // Scalar colors: -1 default, -2 RGB negative zero, 0..255 indexed,
537
+ // and 0x1000000 + value for other RGB values. No caller-owned color is adopted.
538
+ const DECODED_DEFAULT_COLOR = Object.freeze({ kind: "default" } as const);
539
+ function decodedScalarColor(value: number): TerminalReplicaColor {
540
+ if (value === -1) return DECODED_DEFAULT_COLOR;
541
+ if (value === -2) return Object.freeze({ kind: "rgb", value: -0 });
542
+ if (Number.isInteger(value) && value >= 0 && value <= 255)
543
+ return Object.freeze({ kind: "indexed", index: value });
544
+ if (Number.isInteger(value) && value >= 0x1000000 && value <= 0x1ffffff)
545
+ return Object.freeze({ kind: "rgb", value: value - 0x1000000 });
546
+ throw new TypeError("Invalid decoded scalar color");
547
+ }
548
+
549
+ /** Internal scalar construction only; no supplied graph, digest, or hash cache. */
550
+ class DecodedTerminalReplicaRowBuilder {
551
+ readonly #cells: TerminalReplicaRow["cells"][number][] = [];
552
+ #finished = false;
553
+
554
+ appendRun(
555
+ count: number,
556
+ grapheme: string,
557
+ width: 0 | 1 | 2,
558
+ foreground: number,
559
+ background: number,
560
+ attributes: number,
561
+ ): void {
562
+ this.#assertOpen();
563
+ if (
564
+ !Number.isSafeInteger(count) ||
565
+ count < 1 ||
566
+ count > 16384 ||
567
+ typeof grapheme !== "string" ||
568
+ (width !== 0 && width !== 1 && width !== 2) ||
569
+ !Number.isInteger(attributes) ||
570
+ attributes < 0 ||
571
+ attributes > 255
572
+ )
573
+ throw new TypeError("Invalid decoded scalar run");
574
+ const cell = Object.freeze({
575
+ grapheme,
576
+ width,
577
+ foreground: decodedScalarColor(foreground),
578
+ background: decodedScalarColor(background),
579
+ attributes,
580
+ });
581
+ for (let index = 0; index < count; index += 1) this.#cells.push(cell);
582
+ }
583
+
584
+ finish(wrapped: boolean, cols: number | null): TerminalReplicaRow {
585
+ this.#assertOpen();
586
+ const cells = this.#cells;
587
+ // As in compact expansion, sum-width checks happen after every run parses.
588
+ if (cols !== null && cells.length !== cols) throw new TypeError("Compact row width mismatch");
589
+ if (cells.length < 1 || cells.length > 16384)
590
+ throw new TypeError("Compact row width is out of bounds");
591
+ for (let index = 0; index < cells.length; index += 1) {
592
+ if (cells[index]!.width === 2 && cells[index + 1]?.width !== 0)
593
+ throw new TypeError("Malformed compact wide cell");
594
+ if (cells[index]!.width === 0 && (index === 0 || cells[index - 1]?.width !== 2))
595
+ throw new TypeError("Malformed compact continuation cell");
596
+ }
597
+ if (typeof wrapped !== "boolean") throw new TypeError("Invalid decoded scalar wrapped");
598
+ this.#finished = true;
599
+ const row = Object.freeze({
600
+ cells: Object.freeze(cells),
601
+ wrapped,
602
+ }) as unknown as TerminalReplicaRow;
603
+ // Earn only deep immutability. Existing row hashing remains lazy and unchanged.
604
+ DEEPLY_FROZEN_ROWS.add(row);
605
+ return row;
606
+ }
607
+
608
+ #assertOpen(): void {
609
+ if (this.#finished) throw new Error("Decoded terminal row already finished");
610
+ }
611
+ }
612
+
613
+ export function createDecodedTerminalReplicaRowBuilder() {
614
+ return new DecodedTerminalReplicaRowBuilder();
615
+ }
616
+
476
617
  /** Package-private verified-decoder seam; this module is not a package export. */
477
618
  export function primeTerminalReplicaRowHash(row: TerminalReplicaRow, digest: string): void {
478
619
  if (!/^[0-9a-f]{16}$/u.test(digest) || !isTerminalReplicaRowDeeplyFrozen(row))
@@ -1,8 +1,60 @@
1
- import type { TerminalReplicaRow } from "@tmux-ide/contracts";
1
+ import type {
2
+ TerminalReplicaCell,
3
+ TerminalReplicaColor,
4
+ TerminalReplicaRow,
5
+ } from "@tmux-ide/contracts";
2
6
 
3
7
  // Ownership proves immutable, ordinary data properties, not schema validity.
4
8
  // There is no registration API: callers can only obtain ownership by copying.
5
9
  const OWNED_ROWS = new WeakSet<TerminalReplicaRow>();
10
+ // Earned only by exact scalar construction, never by immutable foreign copies.
11
+ const SCHEMA_VALID_PROJECTED_ROWS = new WeakSet<TerminalReplicaRow>();
12
+
13
+ export function isSchemaValidProjectedTerminalReplicaRow(row: unknown): boolean {
14
+ return (
15
+ typeof row === "object" &&
16
+ row !== null &&
17
+ SCHEMA_VALID_PROJECTED_ROWS.has(row as TerminalReplicaRow)
18
+ );
19
+ }
20
+
21
+ export const TERMINAL_REPLICA_DEFAULT_COLOR = Object.freeze({ kind: "default" } as const);
22
+
23
+ // Trusted immutable producer values only. Empty storage and a literal space
24
+ // remain distinct for reflow; foreign equivalent cells are still copied.
25
+ export const TERMINAL_REPLICA_EMPTY_CELL: Readonly<TerminalReplicaCell> = Object.freeze({
26
+ grapheme: "",
27
+ width: 1,
28
+ attributes: 0,
29
+ foreground: TERMINAL_REPLICA_DEFAULT_COLOR,
30
+ background: TERMINAL_REPLICA_DEFAULT_COLOR,
31
+ });
32
+ export const TERMINAL_REPLICA_SPACE_CELL: Readonly<TerminalReplicaCell> = Object.freeze({
33
+ ...TERMINAL_REPLICA_EMPTY_CELL,
34
+ grapheme: " ",
35
+ });
36
+
37
+ function freezeColor(color: TerminalReplicaColor): TerminalReplicaColor {
38
+ if (color === TERMINAL_REPLICA_DEFAULT_COLOR) return color;
39
+ // Copy first: external objects (including getters) keep the existing detached
40
+ // data contract. Share only the exact default shape, without erasing fields.
41
+ const copy = { ...color };
42
+ return copy.kind === "default" && Reflect.ownKeys(copy).length === 1
43
+ ? TERMINAL_REPLICA_DEFAULT_COLOR
44
+ : Object.freeze(copy);
45
+ }
46
+
47
+ // Internal copy seam shared by row ownership and cooperative seed admission.
48
+ // Exact trusted values may be shared; foreign objects always remain detached.
49
+ export function freezeOwnedTerminalReplicaCell(cell: TerminalReplicaCell): TerminalReplicaCell {
50
+ return cell === TERMINAL_REPLICA_EMPTY_CELL || cell === TERMINAL_REPLICA_SPACE_CELL
51
+ ? cell
52
+ : Object.freeze({
53
+ ...cell,
54
+ foreground: freezeColor(cell.foreground),
55
+ background: freezeColor(cell.background),
56
+ });
57
+ }
6
58
 
7
59
  export function isOwnedTerminalReplicaRow(row: unknown): row is TerminalReplicaRow {
8
60
  return typeof row === "object" && row !== null && OWNED_ROWS.has(row as TerminalReplicaRow);
@@ -12,16 +64,119 @@ export function freezeOwnedTerminalReplicaRow(row: TerminalReplicaRow): Terminal
12
64
  if (OWNED_ROWS.has(row)) return row;
13
65
  const frozen = Object.freeze({
14
66
  wrapped: row.wrapped,
15
- cells: Object.freeze(
16
- Array.from(row.cells, (cell) =>
17
- Object.freeze({
18
- ...cell,
19
- foreground: Object.freeze({ ...cell.foreground }),
20
- background: Object.freeze({ ...cell.background }),
21
- }),
22
- ),
23
- ),
67
+ cells: Object.freeze(Array.from(row.cells, freezeOwnedTerminalReplicaCell)),
24
68
  }) as unknown as TerminalReplicaRow;
25
69
  OWNED_ROWS.add(frozen);
26
70
  return frozen;
27
71
  }
72
+
73
+ /** Internal incremental copy seam. No caller-owned array or row can be branded.
74
+ * Ownership establishes detached immutable data, never schema validity. */
75
+ export function createOwnedTerminalReplicaRowBuilder() {
76
+ const cells: TerminalReplicaCell[] = [];
77
+ let finished = false;
78
+ const assertOpen = () => {
79
+ if (finished) throw new Error("Terminal replica row builder already finished");
80
+ };
81
+ return Object.freeze({
82
+ append(source: TerminalReplicaCell): TerminalReplicaCell {
83
+ assertOpen();
84
+ const cell = freezeOwnedTerminalReplicaCell(source);
85
+ cells.push(cell);
86
+ return cell;
87
+ },
88
+ finish(source: TerminalReplicaRow, wrapped: boolean): TerminalReplicaRow {
89
+ assertOpen();
90
+ finished = true;
91
+ const row = Object.freeze({
92
+ ...source,
93
+ wrapped,
94
+ cells: Object.freeze(cells),
95
+ }) as unknown as TerminalReplicaRow;
96
+ OWNED_ROWS.add(row);
97
+ return row;
98
+ },
99
+ });
100
+ }
101
+
102
+ /** Parser construction seam: accepts primitives, never caller-owned objects or
103
+ * arrays. Ownership proves detached immutable data, not schema validity. */
104
+ class ProjectedTerminalReplicaRowBuilder {
105
+ readonly #cells: TerminalReplicaCell[] = [];
106
+ #finished = false;
107
+ #schemaValid = true;
108
+
109
+ append(
110
+ grapheme: string,
111
+ width: 0 | 1 | 2,
112
+ attributes: number,
113
+ foregroundKind: TerminalReplicaColor["kind"],
114
+ foregroundValue: number,
115
+ backgroundKind: TerminalReplicaColor["kind"],
116
+ backgroundValue: number,
117
+ ): void {
118
+ this.#assertOpen();
119
+ if (typeof grapheme !== "string" || typeof width !== "number" || typeof attributes !== "number")
120
+ throw new TypeError("Projected terminal cell values must be primitive");
121
+ const foreground = projectedColor(foregroundKind, foregroundValue);
122
+ const background = projectedColor(backgroundKind, backgroundValue);
123
+ const cell =
124
+ width === 1 &&
125
+ attributes === 0 &&
126
+ foreground === TERMINAL_REPLICA_DEFAULT_COLOR &&
127
+ background === TERMINAL_REPLICA_DEFAULT_COLOR &&
128
+ (grapheme === "" || grapheme === " ")
129
+ ? grapheme === ""
130
+ ? TERMINAL_REPLICA_EMPTY_CELL
131
+ : TERMINAL_REPLICA_SPACE_CELL
132
+ : Object.freeze({ grapheme, width, foreground, background, attributes });
133
+ this.#cells.push(cell);
134
+ // Describe the emitted exact-key cell, preserving all original throws and
135
+ // invalid-but-owned construction. Default color does not emit its value.
136
+ this.#schemaValid =
137
+ this.#schemaValid &&
138
+ (width === 0 || width === 1 || width === 2) &&
139
+ Number.isInteger(attributes) &&
140
+ attributes >= 0 &&
141
+ attributes <= 0xff &&
142
+ projectedColorIsSchemaValid(foregroundKind, foregroundValue) &&
143
+ projectedColorIsSchemaValid(backgroundKind, backgroundValue);
144
+ }
145
+
146
+ finish(wrapped: boolean): TerminalReplicaRow {
147
+ this.#assertOpen();
148
+ if (typeof wrapped !== "boolean") throw new TypeError("Projected row wrapped must be boolean");
149
+ this.#finished = true;
150
+ const row = Object.freeze({
151
+ wrapped,
152
+ cells: Object.freeze(this.#cells),
153
+ }) as unknown as TerminalReplicaRow;
154
+ OWNED_ROWS.add(row);
155
+ if (this.#schemaValid) SCHEMA_VALID_PROJECTED_ROWS.add(row);
156
+ return row;
157
+ }
158
+
159
+ #assertOpen(): void {
160
+ if (this.#finished) throw new Error("Projected terminal row already finished");
161
+ }
162
+ }
163
+
164
+ function projectedColorIsSchemaValid(kind: TerminalReplicaColor["kind"], value: number): boolean {
165
+ return (
166
+ kind === "default" ||
167
+ (Number.isInteger(value) && value >= 0 && value <= (kind === "indexed" ? 255 : 0xffffff))
168
+ );
169
+ }
170
+
171
+ function projectedColor(kind: TerminalReplicaColor["kind"], value: number): TerminalReplicaColor {
172
+ if (typeof value !== "number")
173
+ throw new TypeError("Projected terminal color value must be primitive");
174
+ if (kind === "default") return TERMINAL_REPLICA_DEFAULT_COLOR;
175
+ if (kind === "indexed") return Object.freeze({ kind, index: value });
176
+ if (kind === "rgb") return Object.freeze({ kind, value });
177
+ throw new TypeError("Unknown projected terminal color kind");
178
+ }
179
+
180
+ export function createProjectedTerminalReplicaRowBuilder() {
181
+ return new ProjectedTerminalReplicaRowBuilder();
182
+ }
@@ -9,8 +9,22 @@ import type {
9
9
  TerminalReplicaSnapshot,
10
10
  TerminalReplicaTombstonePayload,
11
11
  } from "@tmux-ide/contracts";
12
- import { consumeCompactReplicaCapability } from "./terminal-compact-capability.ts";
13
- import { freezeOwnedTerminalReplicaRow as freezeRow } from "./terminal-replica-owned-row.ts";
12
+ import {
13
+ consumeCompactReplicaCapability,
14
+ consumeVerifiedTombstoneInterval,
15
+ } from "./terminal-compact-capability.ts";
16
+ import {
17
+ freezeOwnedTerminalReplicaRow as freezeRow,
18
+ createOwnedTerminalReplicaRowBuilder,
19
+ isOwnedTerminalReplicaRow,
20
+ TERMINAL_REPLICA_DEFAULT_COLOR,
21
+ } from "./terminal-replica-owned-row.ts";
22
+ export {
23
+ createProjectedTerminalReplicaRowBuilder,
24
+ TERMINAL_REPLICA_DEFAULT_COLOR,
25
+ TERMINAL_REPLICA_EMPTY_CELL,
26
+ TERMINAL_REPLICA_SPACE_CELL,
27
+ } from "./terminal-replica-owned-row.ts";
14
28
  import {
15
29
  hashCanonicalTerminalValue,
16
30
  hashCanonicalTerminalValueCooperatively,
@@ -24,7 +38,6 @@ import {
24
38
  // External Object.freeze calls do not establish this provenance.
25
39
  const VALIDATED_PATCH_ROWS = new WeakSet<TerminalReplicaRow>();
26
40
 
27
- const DEFAULT_COLOR = Object.freeze({ kind: "default" } as const);
28
41
  const ROW_ARRAY_HASH_CACHE = new WeakMap<
29
42
  object,
30
43
  { readonly hash: bigint; readonly length: number }
@@ -128,6 +141,10 @@ export function blankTerminalReplicaSnapshot(cols: number, rows: number): Termin
128
141
  });
129
142
  }
130
143
 
144
+ // Only accepted verified coalesced tombstones need their original interval for replay.
145
+ // Weak state keys preserve exact-duplicate idempotency without retaining retired replicas.
146
+ const verifiedTombstoneBases = new WeakMap<TerminalReplicaState, number>();
147
+
131
148
  export function applyTerminalReplicaUpdate(
132
149
  current: TerminalReplicaState | null,
133
150
  update: CanonicalTerminalReplicaUpdate,
@@ -208,7 +225,8 @@ export function applyTerminalReplicaUpdate(
208
225
  if (update.type === "terminal.tombstone") {
209
226
  return complete(
210
227
  update.revision === current.revision &&
211
- update.baseRevision === current.revision - 1 &&
228
+ (update.baseRevision === current.revision - 1 ||
229
+ verifiedTombstoneBases.get(current) === update.baseRevision) &&
212
230
  update.stateHash === current.hash &&
213
231
  current.tombstone?.reason === update.tombstone.reason &&
214
232
  current.frameHash === receivedFrameHash
@@ -231,7 +249,20 @@ export function applyTerminalReplicaUpdate(
231
249
  : { status: "stale", state: current },
232
250
  );
233
251
  }
234
- if (update.baseRevision !== current.revision || update.revision !== current.revision + 1) {
252
+ const verifiedTombstoneJump =
253
+ update.type === "terminal.tombstone" &&
254
+ update.revision > current.revision + 1 &&
255
+ consumeVerifiedTombstoneInterval(
256
+ update.tombstone,
257
+ current.snapshot,
258
+ update.stateHash,
259
+ update.baseRevision,
260
+ update.revision,
261
+ );
262
+ if (
263
+ update.baseRevision !== current.revision ||
264
+ (update.revision !== current.revision + 1 && !verifiedTombstoneJump)
265
+ ) {
235
266
  return complete({
236
267
  status: "gap",
237
268
  state: current,
@@ -263,6 +294,7 @@ export function applyTerminalReplicaUpdate(
263
294
  hash,
264
295
  frameHash: receivedFrameHash,
265
296
  });
297
+ if (verifiedTombstoneJump) verifiedTombstoneBases.set(state, update.baseRevision);
266
298
  return complete({ status: "applied", state });
267
299
  }
268
300
  if (current.snapshot === null) {
@@ -498,33 +530,23 @@ export async function applyTerminalReplicaUpdateCooperatively(
498
530
  ] as const) {
499
531
  for (const row of sources) {
500
532
  if (row.cells.length !== cols) return complete(conflict());
501
- const cells: TerminalReplicaRow["cells"] = [];
533
+ const owned = isOwnedTerminalReplicaRow(row);
534
+ const builder = owned ? null : createOwnedTerminalReplicaRowBuilder();
502
535
  let priorWidth = -1;
503
536
  const wrapped = row.wrapped;
504
537
  for (let index = 0; index < cols; index++) {
505
538
  const sourceCell = row.cells[index]!;
506
- const cell = Object.freeze({
507
- ...sourceCell,
508
- foreground: Object.freeze({ ...sourceCell.foreground }),
509
- background: Object.freeze({ ...sourceCell.background }),
510
- });
539
+ const cell = owned ? sourceCell : builder!.append(sourceCell);
511
540
  if ((priorWidth === 2 && cell.width !== 0) || (cell.width === 0 && priorWidth !== 2))
512
541
  return complete(conflict());
513
542
  priorWidth = cell.width;
514
- cells.push(cell);
515
543
  if (++work >= 256) {
516
544
  work = 0;
517
545
  await yieldControl();
518
546
  }
519
547
  }
520
548
  if (priorWidth === 2) return complete(conflict());
521
- target.push(
522
- Object.freeze({
523
- ...row,
524
- wrapped,
525
- cells: Object.freeze(cells),
526
- }) as unknown as TerminalReplicaRow,
527
- );
549
+ target.push(owned ? row : builder!.finish(row, wrapped));
528
550
  }
529
551
  }
530
552
  const placements: TerminalReplicaSnapshot["placements"] = [];
@@ -827,8 +849,8 @@ function blankRow(cols: number): TerminalReplicaRow {
827
849
  Object.freeze({
828
850
  grapheme: " ",
829
851
  width: 1 as const,
830
- foreground: DEFAULT_COLOR,
831
- background: DEFAULT_COLOR,
852
+ foreground: TERMINAL_REPLICA_DEFAULT_COLOR,
853
+ background: TERMINAL_REPLICA_DEFAULT_COLOR,
832
854
  attributes: 0,
833
855
  }),
834
856
  ),
@@ -37,6 +37,7 @@ import { startOwnedEmbeddedDaemon } from "./embedded-daemon-lifecycle.js";
37
37
  import { execFileSync } from "node:child_process";
38
38
  import { randomBytes, randomUUID } from "node:crypto";
39
39
  import { createWriteStream } from "node:fs";
40
+ import { createRuntimeTraceWriter } from "./runtime-trace-writer.js";
40
41
  import { createServer } from "node:http";
41
42
  import { createRequire } from "node:module";
42
43
  import { performance } from "node:perf_hooks";
@@ -965,15 +966,11 @@ async function startEmbeddedDaemonGeneration(opts) {
965
966
  throw new Error("Default tmux owner is retired");
966
967
  },
967
968
  });
968
- let runtimeTraceStream = null;
969
- const closeRuntimeTraceStream = async () => {
969
+ let runtimeTraceWriter = null;
970
+ const closeRuntimeTraceStream = async (incomplete = false) => {
970
971
  externalInteractionObserver.setDiagnostics(null);
971
972
  setFleetFactsObserverDiagnostics(null);
972
- const stream = runtimeTraceStream;
973
- runtimeTraceStream = null;
974
- if (!stream || stream.closed || stream.destroyed)
975
- return;
976
- await new Promise((resolve) => stream.end(resolve));
973
+ await runtimeTraceWriter?.close(incomplete);
977
974
  };
978
975
  let startedServer;
979
976
  try {
@@ -1001,16 +998,9 @@ async function startEmbeddedDaemonGeneration(opts) {
1001
998
  return workspaceMultiplexer.mutate({ operationId, expectedDaemonInstanceId: instanceId, intent }, timing, execution);
1002
999
  };
1003
1000
  const runtimeTracePath = process.env.TMUX_IDE_SESSION_RUNTIME_TRACE_LOG;
1004
- runtimeTraceStream = runtimeTracePath
1005
- ? createWriteStream(runtimeTracePath, { flags: "a", highWaterMark: 64 * 1_024 })
1001
+ runtimeTraceWriter = runtimeTracePath
1002
+ ? createRuntimeTraceWriter(createWriteStream(runtimeTracePath, { flags: "a", highWaterMark: 64 * 1024 }), { processId: `daemon:${process.pid}`, daemonInstanceId: instanceId })
1006
1003
  : null;
1007
- let runtimeTraceSaturated = false;
1008
- runtimeTraceStream?.on("error", () => {
1009
- runtimeTraceSaturated = true;
1010
- });
1011
- runtimeTraceStream?.on("drain", () => {
1012
- runtimeTraceSaturated = false;
1013
- });
1014
1004
  const runtimeObservability = runtimeTracePath
1015
1005
  ? createSessionRuntimeObservability({
1016
1006
  // The JSONL stream is the qualifying record. Keep only a small
@@ -1019,27 +1009,22 @@ async function startEmbeddedDaemonGeneration(opts) {
1019
1009
  // pauses on the daemon's input/output event loop.
1020
1010
  capacity: 1_024,
1021
1011
  onSpan: (span) => {
1022
- if (!runtimeTraceStream || runtimeTraceSaturated)
1023
- return;
1024
- runtimeTraceSaturated = !runtimeTraceStream.write(`${JSON.stringify({ version: 1, type: "performance.stage", ...span })}\n`);
1012
+ runtimeTraceWriter?.append(() => ({
1013
+ version: 1,
1014
+ type: "performance.stage",
1015
+ ...span,
1016
+ }));
1025
1017
  },
1026
1018
  })
1027
1019
  : undefined;
1028
1020
  if (runtimeTracePath) {
1029
1021
  const publishObserverDiagnostic = (event) => {
1030
- try {
1031
- if (!runtimeTraceStream || runtimeTraceSaturated)
1032
- return;
1033
- runtimeTraceSaturated = !runtimeTraceStream.write(`${JSON.stringify({
1034
- version: 1,
1035
- type: "performance.daemon-observer",
1036
- ...event,
1037
- generation: instanceId,
1038
- })}\n`);
1039
- }
1040
- catch {
1041
- // Qualification diagnostics never alter daemon observation.
1042
- }
1022
+ runtimeTraceWriter?.append(() => ({
1023
+ version: 1,
1024
+ type: "performance.daemon-observer",
1025
+ ...event,
1026
+ generation: instanceId,
1027
+ }));
1043
1028
  };
1044
1029
  const diagnostics = {
1045
1030
  nowMicros: () => Math.floor(performance.now() * 1_000),
@@ -1438,7 +1423,7 @@ async function startEmbeddedDaemonGeneration(opts) {
1438
1423
  appWindowMutation.dispose(),
1439
1424
  workspaceMultiplexer.dispose(),
1440
1425
  disposeInteractionObservation(),
1441
- closeRuntimeTraceStream(),
1426
+ closeRuntimeTraceStream(true),
1442
1427
  ]);
1443
1428
  // The pane-stream coordinator may still hold runtime consumers while it
1444
1429
  // drains. Preserve the normal shutdown order on startup rollback too:
@@ -1487,7 +1472,7 @@ async function startEmbeddedDaemonGeneration(opts) {
1487
1472
  appWindowMutationDisposal,
1488
1473
  workspaceMultiplexerDisposal,
1489
1474
  externalInteractionDisposal,
1490
- closeRuntimeTraceStream(),
1475
+ closeRuntimeTraceStream(true),
1491
1476
  Promise.resolve().then(() => closeClients()),
1492
1477
  ...[...sockets].map((socket) => Promise.resolve().then(() => socket.destroy())),
1493
1478
  Promise.race([closePromise, delay(100)]),