tmux-ide 2.9.3 → 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.
- package/LICENSE +1 -1
- package/README.md +120 -139
- package/bin/cli.js +4847 -3585
- package/package.json +16 -9
- package/packages/contracts/src/session-runtime.ts +2 -0
- package/packages/contracts/src/terminal-delivery.ts +4 -0
- package/packages/core/src/terminal-compact-capability.ts +31 -1
- package/packages/core/src/terminal-delivery.ts +234 -32
- package/packages/core/src/terminal-replica-hash-cache.ts +162 -21
- package/packages/core/src/terminal-replica-owned-row.ts +165 -10
- package/packages/core/src/terminal-replica.ts +43 -21
- package/packages/daemon/dist/lib/daemon-embed.js +19 -34
- package/packages/daemon/dist/lib/runtime-trace-writer.js +133 -0
- package/packages/daemon/dist/lib/tmux-client-execution.js +37 -2
- package/packages/daemon/dist/lib/tmux-server-owner.js +20 -4
- package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/Info.plist +2 -2
- package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/MacOS/tmux-ide-notifier +0 -0
- package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/Resources/Assets.car +0 -0
- package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/_CodeSignature/CodeResources +2 -2
- package/packages/daemon/dist/native/tmux/darwin-arm64/manifest.json +5 -1
- package/packages/daemon/dist/native/tmux/darwin-arm64/tmux +0 -0
- package/packages/daemon/dist/native/tmux/darwin-x64/manifest.json +5 -1
- package/packages/daemon/dist/native/tmux/darwin-x64/tmux +0 -0
- package/packages/daemon/dist/native/tmux/linux-arm64/manifest.json +5 -1
- package/packages/daemon/dist/native/tmux/linux-arm64/tmux +0 -0
- package/packages/daemon/dist/native/tmux/linux-x64/licenses/libevent_core-2.1.so.7.txt +0 -0
- package/packages/daemon/dist/native/tmux/linux-x64/manifest.json +5 -1
- package/packages/daemon/dist/native/tmux/linux-x64/tmux +0 -0
- package/packages/daemon/dist/terminal/attachments/native-runtime.js +6 -6
- package/packages/daemon/dist/terminal/mirror/control-channel.js +379 -75
- package/packages/daemon/dist/terminal/mirror/mirror-service.js +43 -3
- package/packages/daemon/dist/terminal/mirror/native-grid-projection.js +20 -11
- package/packages/daemon/dist/terminal/mirror/session-channel.js +639 -811
- package/packages/daemon/dist/terminal/mirror/stock-pane-snapshot.js +177 -0
- package/packages/daemon/dist/terminal/session-runtime/causal-cell-ledger.js +1 -1
- package/packages/daemon/dist/terminal/session-runtime/registry.js +145 -15
- package/packages/daemon/dist/terminal/session-runtime/stock-capture-fidelity.js +12 -0
- package/packages/daemon/dist/terminal/session-runtime/terminal-delivery-hub.js +128 -10
- package/packages/daemon/dist/terminal/session-runtime/terminal-replica-interpreter.js +21 -4
- package/packages/daemon/dist/terminal/session-runtime/terminal-replica-owner.js +19 -2
- package/packages/daemon/dist/terminal/session-runtime/xterm-terminal-interpreter-backend.js +17 -19
- package/packages/daemon/dist/tui/integrations/claude-agent-teams.js +264 -0
- package/packages/daemon/dist/tui/mirror/open-tui-workspace-runtime-port.js +64 -4
- package/packages/daemon/dist/tui/mirror/pane-surface.jsx +10 -1
- package/packages/daemon/dist/tui/mirror/reference-performance-trace.js +117 -71
- package/packages/daemon/dist/tui/mirror/runtime/application-terminal-input-ingress.js +27 -3
- package/packages/daemon/dist/tui/mirror/runtime/pane-scoped-terminal-surface.jsx +17 -4
- package/packages/daemon/dist/tui/mirror/runtime/terminal-fast-lane-renderer-adapter.js +19 -0
- package/packages/daemon/dist/tui/mirror/runtime/terminal-input-adapter.js +10 -2
- package/packages/daemon/dist/tui/mirror/testing/styled-frame-oracle.js +156 -0
- package/packages/daemon/src/lib/daemon-embed.ts +22 -34
- package/packages/daemon/src/lib/runtime-trace-writer.ts +132 -0
- package/packages/daemon/src/lib/tmux-client-execution.ts +47 -2
- package/packages/daemon/src/lib/tmux-server-owner.ts +19 -3
- package/packages/daemon/src/terminal/attachments/native-runtime.ts +6 -0
- package/packages/daemon/src/terminal/mirror/control-channel.ts +428 -84
- package/packages/daemon/src/terminal/mirror/events.ts +7 -7
- package/packages/daemon/src/terminal/mirror/mirror-service.ts +49 -3
- package/packages/daemon/src/terminal/mirror/native-grid-projection.ts +21 -11
- package/packages/daemon/src/terminal/mirror/pane-feed.ts +9 -4
- package/packages/daemon/src/terminal/mirror/session-channel.ts +705 -859
- package/packages/daemon/src/terminal/mirror/stock-pane-snapshot.ts +228 -0
- package/packages/daemon/src/terminal/session-runtime/causal-cell-ledger.ts +1 -1
- package/packages/daemon/src/terminal/session-runtime/registry.ts +203 -32
- package/packages/daemon/src/terminal/session-runtime/runtime-observability.ts +7 -0
- package/packages/daemon/src/terminal/session-runtime/stock-capture-fidelity.ts +13 -0
- package/packages/daemon/src/terminal/session-runtime/terminal-delivery-hub.ts +142 -8
- package/packages/daemon/src/terminal/session-runtime/terminal-replica-interpreter.ts +26 -3
- package/packages/daemon/src/terminal/session-runtime/terminal-replica-owner.ts +25 -1
- package/packages/daemon/src/terminal/session-runtime/xterm-terminal-interpreter-backend.ts +26 -20
- package/packages/daemon/src/tui/integrations/claude-agent-teams.ts +334 -0
- package/packages/daemon/src/tui/mirror/open-tui-workspace-runtime-port.ts +63 -4
- package/packages/daemon/src/tui/mirror/pane-surface.tsx +9 -1
- package/packages/daemon/src/tui/mirror/reference-performance-trace.ts +141 -90
- package/packages/daemon/src/tui/mirror/runtime/application-terminal-input-ingress.ts +27 -4
- package/packages/daemon/src/tui/mirror/runtime/pane-scoped-terminal-surface.tsx +61 -31
- package/packages/daemon/src/tui/mirror/runtime/terminal-fast-lane-renderer-adapter.ts +31 -3
- package/packages/daemon/src/tui/mirror/runtime/terminal-input-adapter.ts +9 -6
- package/packages/daemon/src/tui/mirror/testing/styled-frame-oracle.ts +209 -0
- package/packages/daemon-client/src/terminal-fast-lane.ts +53 -4
- package/packages/daemon-client/src/workspace-client-types.ts +1 -0
- package/packages/daemon-client/src/workspace-client.ts +78 -5
- package/packages/tmux-bridge/src/index.ts +1 -0
- package/packages/tmux-bridge/src/runner.ts +25 -1
- package/skill/SKILL.md +30 -30
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import type { MirrorPaneEvent } from "./events.ts";
|
|
2
|
+
|
|
3
|
+
/** Identity of one pane-wide, observed-pause snapshot attempt. */
|
|
4
|
+
export interface StockPaneSnapshotContext {
|
|
5
|
+
readonly paneId: string;
|
|
6
|
+
readonly incarnation: number;
|
|
7
|
+
readonly layoutGeneration: number;
|
|
8
|
+
readonly participants: readonly object[];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface StockPaneSnapshotBatch {
|
|
12
|
+
readonly participant: object;
|
|
13
|
+
/** Capture-time metadata only. Live bytes are owned by the transaction. */
|
|
14
|
+
readonly events: readonly MirrorPaneEvent[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type StockPaneSnapshotFailure =
|
|
18
|
+
| "cancelled"
|
|
19
|
+
| "context-changed"
|
|
20
|
+
| "context-unavailable"
|
|
21
|
+
| "pause-not-observed"
|
|
22
|
+
| "repaused"
|
|
23
|
+
| "overflow"
|
|
24
|
+
| "invalid-snapshot"
|
|
25
|
+
| "consumer-error";
|
|
26
|
+
|
|
27
|
+
export type StockPaneSnapshotOutput = "unrelated" | "discarded" | "held" | "invalid" | "live";
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Stock control-mode output may overtake a capture reply behind sibling output.
|
|
31
|
+
* An observed pause is the lower fence for a NOHOOKS capture/cursor/continue
|
|
32
|
+
* transaction: every subsequent target byte must follow snapshot metadata.
|
|
33
|
+
*
|
|
34
|
+
* This object owns only that attempt's delivery contract. It neither sends tmux
|
|
35
|
+
* commands nor authenticates the snapshot collector. Its caller must prove the
|
|
36
|
+
* uninterrupted command transaction and retain the connection's collector lease
|
|
37
|
+
* until cancelled wire work has drained. SessionChannel owns this lifecycle.
|
|
38
|
+
*/
|
|
39
|
+
export class StockPaneSnapshot {
|
|
40
|
+
static readonly MAX_BYTES = 1024 * 1024;
|
|
41
|
+
static readonly MAX_CHUNKS = 512;
|
|
42
|
+
|
|
43
|
+
private phase: "awaiting-pause" | "buffering" | "publishing" | "committed" | "invalidated" =
|
|
44
|
+
"awaiting-pause";
|
|
45
|
+
private failure: StockPaneSnapshotFailure | null = null;
|
|
46
|
+
private readonly context: StockPaneSnapshotContext;
|
|
47
|
+
private readonly participants: ReadonlySet<object>;
|
|
48
|
+
private readonly maxBytes: number;
|
|
49
|
+
private readonly maxChunks: number;
|
|
50
|
+
private chunks: Uint8Array[] = [];
|
|
51
|
+
private bytes = 0;
|
|
52
|
+
|
|
53
|
+
constructor(
|
|
54
|
+
context: StockPaneSnapshotContext,
|
|
55
|
+
limits: { readonly maxBytes?: number; readonly maxChunks?: number } = {},
|
|
56
|
+
) {
|
|
57
|
+
if (
|
|
58
|
+
!/^%(?:0|[1-9][0-9]*)$/u.test(context.paneId) ||
|
|
59
|
+
!Number.isSafeInteger(context.incarnation) ||
|
|
60
|
+
context.incarnation < 0 ||
|
|
61
|
+
!Number.isSafeInteger(context.layoutGeneration) ||
|
|
62
|
+
context.layoutGeneration < 0 ||
|
|
63
|
+
context.participants.length === 0 ||
|
|
64
|
+
new Set(context.participants).size !== context.participants.length ||
|
|
65
|
+
context.participants.some(
|
|
66
|
+
(participant) => participant === null || typeof participant !== "object",
|
|
67
|
+
)
|
|
68
|
+
)
|
|
69
|
+
throw new TypeError("stock snapshot requires exact pane and participant identities");
|
|
70
|
+
this.maxBytes = limits.maxBytes ?? StockPaneSnapshot.MAX_BYTES;
|
|
71
|
+
this.maxChunks = limits.maxChunks ?? StockPaneSnapshot.MAX_CHUNKS;
|
|
72
|
+
if (
|
|
73
|
+
!Number.isSafeInteger(this.maxBytes) ||
|
|
74
|
+
this.maxBytes < 1 ||
|
|
75
|
+
this.maxBytes > StockPaneSnapshot.MAX_BYTES ||
|
|
76
|
+
!Number.isSafeInteger(this.maxChunks) ||
|
|
77
|
+
this.maxChunks < 1 ||
|
|
78
|
+
this.maxChunks > StockPaneSnapshot.MAX_CHUNKS
|
|
79
|
+
)
|
|
80
|
+
throw new RangeError("stock snapshot buffer limits must be positive and bounded");
|
|
81
|
+
this.context = { ...context, participants: [...context.participants] };
|
|
82
|
+
this.participants = new Set(this.context.participants);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
get state() {
|
|
86
|
+
return this.phase;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
get failureReason(): StockPaneSnapshotFailure | null {
|
|
90
|
+
return this.failure;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
get bufferedBytes(): number {
|
|
94
|
+
return this.bytes;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
get bufferedChunks(): number {
|
|
98
|
+
return this.chunks.length;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
private matchesContext(context: StockPaneSnapshotContext): boolean {
|
|
102
|
+
if (this.phase === "invalidated") return false;
|
|
103
|
+
if (
|
|
104
|
+
context.paneId !== this.context.paneId ||
|
|
105
|
+
context.incarnation !== this.context.incarnation ||
|
|
106
|
+
context.layoutGeneration !== this.context.layoutGeneration ||
|
|
107
|
+
context.participants.length !== this.participants.size ||
|
|
108
|
+
new Set(context.participants).size !== this.participants.size ||
|
|
109
|
+
context.participants.some((participant) => !this.participants.has(participant))
|
|
110
|
+
) {
|
|
111
|
+
this.invalidate("context-changed");
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
private current(context: StockPaneSnapshotContext): boolean {
|
|
118
|
+
return this.phase !== "committed" && this.matchesContext(context);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
private readCurrentContext(read: () => StockPaneSnapshotContext): boolean {
|
|
122
|
+
try {
|
|
123
|
+
return this.current(read());
|
|
124
|
+
} catch {
|
|
125
|
+
this.invalidate("context-unavailable");
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Only an actual current connection's pause notification establishes the fence. */
|
|
131
|
+
observePause(context: StockPaneSnapshotContext): boolean {
|
|
132
|
+
if (!this.current(context)) return false;
|
|
133
|
+
if (this.phase !== "awaiting-pause") {
|
|
134
|
+
this.invalidate("repaused");
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
this.phase = "buffering";
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
acceptOutput(
|
|
142
|
+
paneId: string,
|
|
143
|
+
data: Uint8Array,
|
|
144
|
+
context: StockPaneSnapshotContext,
|
|
145
|
+
): StockPaneSnapshotOutput {
|
|
146
|
+
if (paneId !== this.context.paneId) return "unrelated";
|
|
147
|
+
if (!this.matchesContext(context)) return "invalid";
|
|
148
|
+
if (this.phase === "committed") return "live";
|
|
149
|
+
// The caller fences all participating feeds before requesting pause. These
|
|
150
|
+
// bytes precede the observed pause and are represented by the future seed.
|
|
151
|
+
if (this.phase === "awaiting-pause") return "discarded";
|
|
152
|
+
if (data.byteLength === 0) return "held";
|
|
153
|
+
if (this.chunks.length >= this.maxChunks || this.bytes + data.byteLength > this.maxBytes) {
|
|
154
|
+
this.invalidate("overflow");
|
|
155
|
+
return "invalid";
|
|
156
|
+
}
|
|
157
|
+
this.chunks.push(data.slice());
|
|
158
|
+
this.bytes += data.byteLength;
|
|
159
|
+
return "held";
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Revoke future routing even after commit; delivered events cannot be retracted. */
|
|
163
|
+
invalidate(reason: StockPaneSnapshotFailure = "cancelled"): void {
|
|
164
|
+
if (this.phase === "invalidated") return;
|
|
165
|
+
this.phase = "invalidated";
|
|
166
|
+
this.failure = reason;
|
|
167
|
+
this.chunks = [];
|
|
168
|
+
this.bytes = 0;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Publish an authenticated snapshot, then replay each retained chunk to every
|
|
173
|
+
* exact participant. Context is rechecked around callbacks. A callback may
|
|
174
|
+
* append more output, which joins this same bounded replay before commit.
|
|
175
|
+
* Failure cannot retract already delivered events: the caller must retire or
|
|
176
|
+
* reseed participants when false is returned, never release their live feeds.
|
|
177
|
+
*/
|
|
178
|
+
publish(
|
|
179
|
+
batches: readonly StockPaneSnapshotBatch[],
|
|
180
|
+
currentContext: () => StockPaneSnapshotContext,
|
|
181
|
+
emit: (participant: object, event: MirrorPaneEvent) => void,
|
|
182
|
+
): boolean {
|
|
183
|
+
if (!this.readCurrentContext(currentContext)) return false;
|
|
184
|
+
if (this.phase !== "buffering") {
|
|
185
|
+
this.invalidate(this.phase === "awaiting-pause" ? "pause-not-observed" : "invalid-snapshot");
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
if (
|
|
189
|
+
batches.length !== this.participants.size ||
|
|
190
|
+
new Set(batches.map((batch) => batch.participant)).size !== this.participants.size ||
|
|
191
|
+
batches.some(
|
|
192
|
+
({ participant, events }) =>
|
|
193
|
+
!this.participants.has(participant) ||
|
|
194
|
+
events.length !== 3 ||
|
|
195
|
+
events[0]?.type !== "reset" ||
|
|
196
|
+
events[1]?.type !== "seed" ||
|
|
197
|
+
events[2]?.type !== "cursor",
|
|
198
|
+
)
|
|
199
|
+
) {
|
|
200
|
+
this.invalidate("invalid-snapshot");
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
this.phase = "publishing";
|
|
204
|
+
const deliver = (participant: object, event: MirrorPaneEvent): boolean => {
|
|
205
|
+
if (!this.readCurrentContext(currentContext)) return false;
|
|
206
|
+
try {
|
|
207
|
+
emit(participant, event);
|
|
208
|
+
} catch {
|
|
209
|
+
this.invalidate("consumer-error");
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
return this.readCurrentContext(currentContext);
|
|
213
|
+
};
|
|
214
|
+
for (const { participant, events } of batches)
|
|
215
|
+
for (const event of events) if (!deliver(participant, event)) return false;
|
|
216
|
+
for (let index = 0; index < this.chunks.length; index += 1) {
|
|
217
|
+
const data = this.chunks[index]!;
|
|
218
|
+
for (const participant of this.context.participants) {
|
|
219
|
+
// A subscriber cannot mutate the bytes replayed to its siblings.
|
|
220
|
+
if (!deliver(participant, { type: "delta", data: data.slice() })) return false;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
this.chunks = [];
|
|
224
|
+
this.bytes = 0;
|
|
225
|
+
this.phase = "committed";
|
|
226
|
+
return true;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
@@ -63,7 +63,7 @@ function colorsEqual(
|
|
|
63
63
|
);
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
function cellsEqual(
|
|
66
|
+
export function cellsEqual(
|
|
67
67
|
left: TerminalReplicaSnapshot["grid"][number]["cells"][number] | undefined,
|
|
68
68
|
right: TerminalReplicaSnapshot["grid"][number]["cells"][number] | undefined,
|
|
69
69
|
): boolean {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { StockCaptureTabUnavailableError } from "./stock-capture-fidelity.ts";
|
|
1
2
|
import type { OwnedNativePlanCompletion } from "../../lib/owned-native-interaction-bindings.ts";
|
|
2
3
|
import type {
|
|
3
4
|
NativeBackingIdentity,
|
|
@@ -119,14 +120,56 @@ export interface TrustedSessionInventoryCandidate {
|
|
|
119
120
|
readonly token: object;
|
|
120
121
|
}
|
|
121
122
|
|
|
123
|
+
interface RuntimeListenerCounts {
|
|
124
|
+
readonly authority: number;
|
|
125
|
+
readonly mirrorSubscriptions: number;
|
|
126
|
+
readonly replicaSubscriptions: number;
|
|
127
|
+
readonly deliveryConnections: number;
|
|
128
|
+
}
|
|
129
|
+
// Final-disposal observations for owners still held by this runtime/registry.
|
|
130
|
+
// This is not a historical count of owners retired by earlier incarnation changes.
|
|
131
|
+
interface ListenerDisposalSummary {
|
|
132
|
+
readonly sessions: number;
|
|
133
|
+
readonly owners: number;
|
|
134
|
+
readonly failures: number;
|
|
135
|
+
readonly canonical: number;
|
|
136
|
+
readonly raw: number;
|
|
137
|
+
readonly upstream: number;
|
|
138
|
+
readonly authority: number;
|
|
139
|
+
readonly consumerHandles: number;
|
|
140
|
+
readonly deliveryConnections: number;
|
|
141
|
+
readonly deliverySources: number;
|
|
142
|
+
readonly pendingDeliverySources: number;
|
|
143
|
+
readonly pendingSourceCloses: number;
|
|
144
|
+
readonly pendingDeliveryClients: number;
|
|
145
|
+
}
|
|
146
|
+
const emptyListenerDisposal = (): ListenerDisposalSummary => ({
|
|
147
|
+
sessions: 0,
|
|
148
|
+
owners: 0,
|
|
149
|
+
failures: 0,
|
|
150
|
+
canonical: 0,
|
|
151
|
+
raw: 0,
|
|
152
|
+
upstream: 0,
|
|
153
|
+
authority: 0,
|
|
154
|
+
consumerHandles: 0,
|
|
155
|
+
deliveryConnections: 0,
|
|
156
|
+
deliverySources: 0,
|
|
157
|
+
pendingDeliverySources: 0,
|
|
158
|
+
pendingSourceCloses: 0,
|
|
159
|
+
pendingDeliveryClients: 0,
|
|
160
|
+
});
|
|
161
|
+
|
|
122
162
|
export interface SessionRuntimeQualificationSnapshot {
|
|
123
163
|
readonly generation: SessionRuntimeGeneration;
|
|
124
164
|
readonly controlChannels: number;
|
|
165
|
+
readonly mirrorListeners: ReturnType<MirrorService["qualificationListeners"]>;
|
|
166
|
+
readonly listenerDisposal: ListenerDisposalSummary | null;
|
|
125
167
|
readonly controllerLeases: number;
|
|
126
168
|
readonly sessions: readonly {
|
|
127
169
|
readonly session: string;
|
|
128
170
|
readonly consumers: number;
|
|
129
171
|
readonly retained: boolean;
|
|
172
|
+
readonly listeners: RuntimeListenerCounts;
|
|
130
173
|
readonly delivery: TerminalDeliveryMetrics;
|
|
131
174
|
readonly convergence: TerminalDeliveryConvergenceSnapshot;
|
|
132
175
|
readonly replicas: Readonly<Record<string, TerminalReplicaQualificationSnapshot>>;
|
|
@@ -272,6 +315,7 @@ export class SessionRuntimeRegistry implements PaneStreamMirror {
|
|
|
272
315
|
readonly #observability: SessionRuntimeObservability;
|
|
273
316
|
readonly #createTraceCorrelator: (scheduler: SessionRuntimeScheduler) => RuntimeTraceCorrelator;
|
|
274
317
|
readonly #sessions = new Map<string, SessionRuntime>();
|
|
318
|
+
#listenerDisposal: ListenerDisposalSummary | null = null;
|
|
275
319
|
readonly #proofPrewarmOwnership = new Map<SessionRuntime, { owned: boolean; claims: number }>();
|
|
276
320
|
readonly #trustedInventoryTokens = new WeakMap<
|
|
277
321
|
object,
|
|
@@ -310,30 +354,47 @@ export class SessionRuntimeRegistry implements PaneStreamMirror {
|
|
|
310
354
|
...(this.#observability.enabled
|
|
311
355
|
? {
|
|
312
356
|
nowMicros: () => this.#observability.nowMicros(),
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
357
|
+
captureInputWrite: (session, action, semanticPaneId) => {
|
|
358
|
+
const runtime = this.#sessions.get(session);
|
|
359
|
+
const binding = runtime?.captureInputDispatch(semanticPaneId);
|
|
360
|
+
return (startedAtMicros, endedAtMicros, pendingBeforeSend, paneCurrent) => {
|
|
361
|
+
const incarnation =
|
|
362
|
+
paneCurrent && this.#sessions.get(session) === runtime
|
|
363
|
+
? (binding?.() ?? null)
|
|
364
|
+
: null;
|
|
365
|
+
for (const traceId of action.traceIds ?? []) {
|
|
366
|
+
const trace = this.#observability.beginTrace(
|
|
367
|
+
"terminal-input-to-paint",
|
|
368
|
+
{
|
|
369
|
+
generation: this.generation,
|
|
370
|
+
incarnation,
|
|
371
|
+
controlDispatch: Object.freeze({
|
|
372
|
+
workspaceName: session,
|
|
373
|
+
nativePaneId: action.pane,
|
|
374
|
+
semanticPaneId: paneCurrent ? semanticPaneId : null,
|
|
375
|
+
scope: "dispatch-time" as const,
|
|
376
|
+
}),
|
|
377
|
+
},
|
|
378
|
+
traceId,
|
|
379
|
+
);
|
|
380
|
+
this.#observability.recordSpan(
|
|
381
|
+
"tmux",
|
|
382
|
+
"control-write",
|
|
383
|
+
startedAtMicros,
|
|
384
|
+
endedAtMicros,
|
|
385
|
+
trace,
|
|
386
|
+
);
|
|
387
|
+
this.#observability.recordSpan(
|
|
388
|
+
"tmux",
|
|
389
|
+
pendingBeforeSend === 0
|
|
390
|
+
? "control-queue-empty-at-send"
|
|
391
|
+
: "control-queue-nonempty-at-send",
|
|
392
|
+
endedAtMicros,
|
|
393
|
+
endedAtMicros,
|
|
394
|
+
trace,
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
};
|
|
337
398
|
},
|
|
338
399
|
}
|
|
339
400
|
: {}),
|
|
@@ -981,6 +1042,8 @@ export class SessionRuntimeRegistry implements PaneStreamMirror {
|
|
|
981
1042
|
return Object.freeze({
|
|
982
1043
|
generation: this.generation,
|
|
983
1044
|
controlChannels: this.activeControlChannelCount(),
|
|
1045
|
+
mirrorListeners: this.#mirror.qualificationListeners(),
|
|
1046
|
+
listenerDisposal: this.#listenerDisposal,
|
|
984
1047
|
controllerLeases: this.activeControllerLeaseCount(),
|
|
985
1048
|
sessions: Object.freeze(
|
|
986
1049
|
[...this.#sessions.values()].map((runtime) => runtime.qualificationSnapshot()),
|
|
@@ -996,7 +1059,30 @@ export class SessionRuntimeRegistry implements PaneStreamMirror {
|
|
|
996
1059
|
this.#stopExitObserver();
|
|
997
1060
|
this.#disposePromise = (async () => {
|
|
998
1061
|
await this.#semanticMutations?.dispose();
|
|
999
|
-
|
|
1062
|
+
const runtimes = [...this.#sessions.values()];
|
|
1063
|
+
const results = await Promise.allSettled(runtimes.map((runtime) => runtime.dispose()));
|
|
1064
|
+
const final = { ...emptyListenerDisposal(), sessions: runtimes.length };
|
|
1065
|
+
runtimes.forEach((runtime, index) => {
|
|
1066
|
+
const counts = runtime.listenerDisposal;
|
|
1067
|
+
if (!counts || results[index]?.status === "rejected") final.failures += 1;
|
|
1068
|
+
if (counts)
|
|
1069
|
+
for (const key of [
|
|
1070
|
+
"owners",
|
|
1071
|
+
"failures",
|
|
1072
|
+
"canonical",
|
|
1073
|
+
"raw",
|
|
1074
|
+
"upstream",
|
|
1075
|
+
"authority",
|
|
1076
|
+
"consumerHandles",
|
|
1077
|
+
"deliveryConnections",
|
|
1078
|
+
"deliverySources",
|
|
1079
|
+
"pendingDeliverySources",
|
|
1080
|
+
"pendingSourceCloses",
|
|
1081
|
+
"pendingDeliveryClients",
|
|
1082
|
+
] as const)
|
|
1083
|
+
final[key] += counts[key];
|
|
1084
|
+
});
|
|
1085
|
+
this.#listenerDisposal = Object.freeze(final);
|
|
1000
1086
|
this.#sessions.clear();
|
|
1001
1087
|
this.#proofPrewarmOwnership.clear();
|
|
1002
1088
|
await this.#mirror.dispose();
|
|
@@ -1099,6 +1185,7 @@ export class SessionRuntimeRegistry implements PaneStreamMirror {
|
|
|
1099
1185
|
class SessionRuntime {
|
|
1100
1186
|
readonly #mirror: MirrorService;
|
|
1101
1187
|
readonly #consumers = new Set<SessionRuntimeConsumerImpl>();
|
|
1188
|
+
listenerDisposal: ListenerDisposalSummary | null = null;
|
|
1102
1189
|
readonly #consumersByClientId = new Map<string, SessionRuntimeConsumerImpl>();
|
|
1103
1190
|
readonly #terminalReplicas = new Map<string, SessionRuntimeTerminalReplicaOwner>();
|
|
1104
1191
|
readonly #terminalReplicaClocks = new Map<string, { epoch: number; revision: number }>();
|
|
@@ -1275,11 +1362,44 @@ class SessionRuntime {
|
|
|
1275
1362
|
return this.#consumers.size > 0;
|
|
1276
1363
|
}
|
|
1277
1364
|
|
|
1365
|
+
private listenerCounts(): RuntimeListenerCounts {
|
|
1366
|
+
const counts = {
|
|
1367
|
+
authority: this.#authorityListeners.size,
|
|
1368
|
+
mirrorSubscriptions: 0,
|
|
1369
|
+
replicaSubscriptions: 0,
|
|
1370
|
+
deliveryConnections: 0,
|
|
1371
|
+
};
|
|
1372
|
+
for (const consumer of this.#consumers) {
|
|
1373
|
+
const handles = consumer.qualificationListeners();
|
|
1374
|
+
counts.mirrorSubscriptions += handles.mirrorSubscriptions;
|
|
1375
|
+
counts.replicaSubscriptions += handles.replicaSubscriptions;
|
|
1376
|
+
counts.deliveryConnections += handles.deliveryConnections;
|
|
1377
|
+
}
|
|
1378
|
+
return Object.freeze(counts);
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
captureInputDispatch(semanticPaneId: string | null): () => string | null {
|
|
1382
|
+
const owner =
|
|
1383
|
+
this.#disposed || semanticPaneId === null
|
|
1384
|
+
? undefined
|
|
1385
|
+
: this.#terminalReplicas.get(semanticPaneId);
|
|
1386
|
+
const incarnation = owner?.inputDispatchIncarnation() ?? null;
|
|
1387
|
+
return () =>
|
|
1388
|
+
!this.#disposed &&
|
|
1389
|
+
semanticPaneId !== null &&
|
|
1390
|
+
owner !== undefined &&
|
|
1391
|
+
this.#terminalReplicas.get(semanticPaneId) === owner &&
|
|
1392
|
+
owner.inputDispatchIncarnation() === incarnation
|
|
1393
|
+
? incarnation
|
|
1394
|
+
: null;
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1278
1397
|
qualificationSnapshot(): SessionRuntimeQualificationSnapshot["sessions"][number] {
|
|
1279
1398
|
return Object.freeze({
|
|
1280
1399
|
session: this.session,
|
|
1281
1400
|
consumers: this.#consumers.size,
|
|
1282
1401
|
retained: this.#retention !== null,
|
|
1402
|
+
listeners: this.listenerCounts(),
|
|
1283
1403
|
delivery: this.#terminalDeliveryHub.metrics(),
|
|
1284
1404
|
convergence: this.#terminalDeliveryHub.convergenceSnapshot(),
|
|
1285
1405
|
replicas: Object.freeze(
|
|
@@ -1777,8 +1897,10 @@ class SessionRuntime {
|
|
|
1777
1897
|
current.epoch += 1;
|
|
1778
1898
|
this.#terminalReplicaClocks.set(semanticPaneId, current);
|
|
1779
1899
|
},
|
|
1780
|
-
onFault: () => {
|
|
1900
|
+
onFault: (error) => {
|
|
1781
1901
|
if (this.#terminalReplicas.get(semanticPaneId) !== candidate) return;
|
|
1902
|
+
if (error instanceof StockCaptureTabUnavailableError)
|
|
1903
|
+
this.#terminalDeliveryHub.failPaneCapture(semanticPaneId, error);
|
|
1782
1904
|
this.#outputTraces?.clearPane(semanticPaneId);
|
|
1783
1905
|
this.#terminalReplicas.delete(semanticPaneId);
|
|
1784
1906
|
this.#restartBarrier = this.#restartBarrier.then(async () => {
|
|
@@ -1896,22 +2018,51 @@ class SessionRuntime {
|
|
|
1896
2018
|
this.#trustedInventoryRuntimeSessionId = null;
|
|
1897
2019
|
this.#trustedInventoryEpoch += 1;
|
|
1898
2020
|
const consumers = [...this.#consumers];
|
|
1899
|
-
await Promise.allSettled(consumers.map((consumer) => consumer.close()));
|
|
2021
|
+
const consumerResults = await Promise.allSettled(consumers.map((consumer) => consumer.close()));
|
|
1900
2022
|
this.#clearController();
|
|
1901
2023
|
this.#authority.dispose();
|
|
1902
2024
|
this.#authorityListeners.clear();
|
|
1903
2025
|
this.#completedHandoffs.clear();
|
|
1904
2026
|
this.#releasedLeases.clear();
|
|
1905
2027
|
this.#outputTraces?.clear();
|
|
1906
|
-
|
|
1907
|
-
|
|
2028
|
+
const owners = [...this.#terminalReplicas.values()];
|
|
2029
|
+
const results = await Promise.allSettled(
|
|
2030
|
+
owners.map((owner) => owner.dispose("runtime-disposed")),
|
|
1908
2031
|
);
|
|
2032
|
+
const final = {
|
|
2033
|
+
...emptyListenerDisposal(),
|
|
2034
|
+
sessions: 1,
|
|
2035
|
+
owners: owners.length,
|
|
2036
|
+
failures: [...results, ...consumerResults].filter((result) => result.status === "rejected")
|
|
2037
|
+
.length,
|
|
2038
|
+
};
|
|
2039
|
+
for (const owner of owners) {
|
|
2040
|
+
const counts = owner.qualificationSnapshot().listeners;
|
|
2041
|
+
final.canonical += counts.canonical;
|
|
2042
|
+
final.raw += counts.raw;
|
|
2043
|
+
final.upstream += counts.upstream;
|
|
2044
|
+
}
|
|
2045
|
+
for (const consumer of consumers) {
|
|
2046
|
+
const counts = consumer.qualificationListeners();
|
|
2047
|
+
final.failures += counts.closeFailures;
|
|
2048
|
+
final.consumerHandles +=
|
|
2049
|
+
counts.mirrorSubscriptions + counts.replicaSubscriptions + counts.deliveryConnections;
|
|
2050
|
+
}
|
|
2051
|
+
final.authority = this.#authorityListeners.size;
|
|
1909
2052
|
this.#terminalReplicas.clear();
|
|
1910
2053
|
await this.#terminalDeliveryHub.close();
|
|
1911
2054
|
await this.#startPromise?.catch(() => undefined);
|
|
1912
2055
|
await this.#restartBarrier;
|
|
1913
2056
|
await this.#retention?.close();
|
|
1914
2057
|
this.#retention = null;
|
|
2058
|
+
const delivery = this.#terminalDeliveryHub.metrics();
|
|
2059
|
+
final.deliveryConnections = delivery.connections;
|
|
2060
|
+
final.deliverySources = delivery.sourceSubscriptions;
|
|
2061
|
+
final.pendingDeliverySources = delivery.pendingSourceSubscriptions;
|
|
2062
|
+
final.pendingSourceCloses = delivery.pendingSourceCloses;
|
|
2063
|
+
final.pendingDeliveryClients = delivery.pendingClients;
|
|
2064
|
+
final.failures += delivery.sourceCloseFailures;
|
|
2065
|
+
this.listenerDisposal = Object.freeze(final);
|
|
1915
2066
|
}
|
|
1916
2067
|
|
|
1917
2068
|
#assertConnected(clientId: string): void {
|
|
@@ -2218,6 +2369,17 @@ class SessionRuntimeConsumerImpl implements SessionRuntimeConsumer {
|
|
|
2218
2369
|
return connection;
|
|
2219
2370
|
}
|
|
2220
2371
|
|
|
2372
|
+
#closeFailures = 0;
|
|
2373
|
+
|
|
2374
|
+
qualificationListeners() {
|
|
2375
|
+
return Object.freeze({
|
|
2376
|
+
closeFailures: this.#closeFailures,
|
|
2377
|
+
mirrorSubscriptions: this.#subscriptions.size,
|
|
2378
|
+
replicaSubscriptions: this.#replicaSubscriptions.size,
|
|
2379
|
+
deliveryConnections: this.#deliveryConnections.size,
|
|
2380
|
+
});
|
|
2381
|
+
}
|
|
2382
|
+
|
|
2221
2383
|
async close(): Promise<void> {
|
|
2222
2384
|
if (this.#closed) return;
|
|
2223
2385
|
this.#closed = true;
|
|
@@ -2230,9 +2392,18 @@ class SessionRuntimeConsumerImpl implements SessionRuntimeConsumer {
|
|
|
2230
2392
|
// Authority retirement is synchronous. A slow/frozen mirror subscription
|
|
2231
2393
|
// must never keep controller leases or previously issued handles alive.
|
|
2232
2394
|
this.#runtime.release(this);
|
|
2233
|
-
await Promise.allSettled(
|
|
2234
|
-
|
|
2235
|
-
|
|
2395
|
+
const mirrorResults = await Promise.allSettled(
|
|
2396
|
+
subscriptions.map((subscription) => subscription.close()),
|
|
2397
|
+
);
|
|
2398
|
+
const replicaResults = await Promise.allSettled(
|
|
2399
|
+
replicaSubscriptions.map((subscription) => subscription.close()),
|
|
2400
|
+
);
|
|
2401
|
+
const deliveryResults = await Promise.allSettled(
|
|
2402
|
+
deliveryConnections.map((connection) => connection.close()),
|
|
2403
|
+
);
|
|
2404
|
+
this.#closeFailures += [...mirrorResults, ...replicaResults, ...deliveryResults].filter(
|
|
2405
|
+
(result) => result.status === "rejected",
|
|
2406
|
+
).length;
|
|
2236
2407
|
}
|
|
2237
2408
|
|
|
2238
2409
|
#assertOpen(): void {
|
|
@@ -9,6 +9,13 @@ import { z } from "zod";
|
|
|
9
9
|
export interface SessionRuntimeTraceAuthority {
|
|
10
10
|
readonly generation: SessionRuntimeGeneration;
|
|
11
11
|
readonly incarnation: string | null;
|
|
12
|
+
/** Current dispatch target only; neither admission authority nor native acceptance. */
|
|
13
|
+
readonly controlDispatch?: Readonly<{
|
|
14
|
+
workspaceName: string;
|
|
15
|
+
nativePaneId: string;
|
|
16
|
+
semanticPaneId: string | null;
|
|
17
|
+
scope: "dispatch-time";
|
|
18
|
+
}>;
|
|
12
19
|
}
|
|
13
20
|
|
|
14
21
|
export interface SessionRuntimeTraceContext {
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { STOCK_CAPTURE_TAB_UNAVAILABLE } from "@tmux-ide/contracts";
|
|
2
|
+
|
|
3
|
+
export class StockCaptureTabUnavailableError extends Error {
|
|
4
|
+
constructor() {
|
|
5
|
+
super(STOCK_CAPTURE_TAB_UNAVAILABLE);
|
|
6
|
+
this.name = "StockCaptureTabUnavailableError";
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** ANSI capture drops stored tab widths. Live HT is cursor motion, not capture paint. */
|
|
11
|
+
export function assertStockCaptureRepresentable(chunks: readonly Uint8Array[]): void {
|
|
12
|
+
if (chunks.some((chunk) => chunk.includes(9))) throw new StockCaptureTabUnavailableError();
|
|
13
|
+
}
|