instar 1.3.388 → 1.3.389

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.
@@ -0,0 +1,104 @@
1
+ /**
2
+ * PeerStreamProxy — phase 1 of Pool Dashboard Streaming
3
+ * (POOL-DASHBOARD-STREAM-SPEC §2.2, the scalability core).
4
+ *
5
+ * ONE multiplexed upstream connection per PEER machine, shared by every local
6
+ * dashboard client and every remote session on that peer. The local
7
+ * WebSocketManager (phase 2) owns local clients and the browser WS; this class
8
+ * owns ONLY the single upstream link and the bookkeeping that makes it correct:
9
+ *
10
+ * - reference-counted subscriptions (per (session) → set of local clientIds);
11
+ * - a 60s idle grace after the last unsubscribe, cancelled if a subscribe
12
+ * arrives during it (no thrash opening/closing on a flapping client);
13
+ * - ONE bounded reconnect on an upstream drop, resubscribing every current
14
+ * subscription; a reconnect that does not open within reconnectTimeoutMs, or
15
+ * a second drop, surfaces `machine-unreachable` (P19 — never a reconnect
16
+ * storm);
17
+ * - subscriptions that arrive mid-(re)connect are QUEUED and merged into the
18
+ * resubscribe batch the moment the link opens — never lost;
19
+ * - the peer URL is re-resolved on every subscribe; a changed URL tears the
20
+ * old link down and opens a new one (no stale-URL duplicate links).
21
+ *
22
+ * Everything external is injected (transport, timers, clock) so the whole state
23
+ * machine is deterministically unit-testable without real sockets or wall time.
24
+ * This module performs NO tmux capture and NO local fan-out polling — capture
25
+ * happens only on the owning machine; fan-out to local clients is the injected
26
+ * `onFrameToClients`. (TAP POINT, ops#1: remote subs never enter the local
27
+ * polling loop.)
28
+ */
29
+ export type ProxyState = 'idle' | 'connecting' | 'active' | 'idle-scheduled' | 'closing' | 'closed';
30
+ /** A live upstream connection handle (one peer's /ws/pool-stream). */
31
+ export interface UpstreamTransport {
32
+ /** Send a frame to the peer (subscribe/unsubscribe/input/key/history). */
33
+ send(frame: Record<string, unknown>): void;
34
+ /** Tear the connection down. Idempotent. */
35
+ close(): void;
36
+ }
37
+ /** Handlers the proxy gives the transport at connect time. */
38
+ export interface UpstreamHandlers {
39
+ /** The link opened and is ready to carry frames. */
40
+ onOpen(): void;
41
+ /** A frame arrived from the peer (output/history/session_ended/error/…). */
42
+ onFrame(frame: Record<string, unknown>): void;
43
+ /** The link closed (clean or error). */
44
+ onClose(): void;
45
+ }
46
+ export type TimerHandle = {
47
+ __brand: 'PeerStreamProxyTimer';
48
+ };
49
+ export interface PeerStreamProxyDeps {
50
+ peerMachineId: string;
51
+ /** Resolve the peer's CURRENT url (re-read each subscribe for URL-change detection). */
52
+ resolveUrl: () => string | null;
53
+ /** Open an upstream link to `url`; the returned transport drives `handlers`. */
54
+ connect: (url: string, handlers: UpstreamHandlers) => UpstreamTransport;
55
+ /** Fan a peer frame out to every LOCAL client subscribed to (session). */
56
+ onFrameToClients: (session: string, frame: Record<string, unknown>) => void;
57
+ /** Surface an error to every local client subscribed to (session). */
58
+ onError: (session: string, code: 'peer-stream-lost' | 'machine-unreachable', detail?: string) => void;
59
+ now: () => number;
60
+ setTimer: (ms: number, fn: () => void) => TimerHandle;
61
+ clearTimer: (t: TimerHandle) => void;
62
+ /** Idle grace before closing an unsubscribed link (default 60_000). */
63
+ idleGraceMs?: number;
64
+ /** Reconnect open deadline (default 10_000). */
65
+ reconnectTimeoutMs?: number;
66
+ logger?: (line: string) => void;
67
+ }
68
+ export declare class PeerStreamProxy {
69
+ private readonly d;
70
+ private state;
71
+ private transport;
72
+ private connectedUrl;
73
+ /** session → set of local clientIds subscribed to it. THE source of truth:
74
+ * on every (re)open we resubscribe from this map, so a subscribe that
75
+ * arrives mid-connect is never lost and a mid-connect unsubscribe is never
76
+ * replayed. */
77
+ private readonly subs;
78
+ private idleTimer;
79
+ private reconnectTimer;
80
+ private reconnectUsed;
81
+ constructor(d: PeerStreamProxyDeps);
82
+ get currentState(): ProxyState;
83
+ /** Total live subscriptions across all sessions (refcount). */
84
+ get refCount(): number;
85
+ /** Snapshot of session → clientIds (tests / introspection). */
86
+ sessionsView(): Record<string, string[]>;
87
+ private log;
88
+ /** A local client subscribes to a remote session on this peer. */
89
+ subscribe(session: string, clientId: string): void;
90
+ /** A local client unsubscribes (or disconnects). */
91
+ unsubscribe(session: string, clientId: string): void;
92
+ /** Forward an input/key frame for a session (caller already gated allowRemoteInput). */
93
+ relayInput(frame: Record<string, unknown>): void;
94
+ private openFresh;
95
+ private onUpstreamOpen;
96
+ private onUpstreamFrame;
97
+ private onUpstreamClose;
98
+ private scheduleIdleClose;
99
+ private failAll;
100
+ private teardownTransport;
101
+ /** Force-close (peer removed, server shutdown). Idempotent. */
102
+ close(): void;
103
+ }
104
+ //# sourceMappingURL=PeerStreamProxy.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PeerStreamProxy.d.ts","sourceRoot":"","sources":["../../src/server/PeerStreamProxy.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,270 @@
1
+ /**
2
+ * PeerStreamProxy — phase 1 of Pool Dashboard Streaming
3
+ * (POOL-DASHBOARD-STREAM-SPEC §2.2, the scalability core).
4
+ *
5
+ * ONE multiplexed upstream connection per PEER machine, shared by every local
6
+ * dashboard client and every remote session on that peer. The local
7
+ * WebSocketManager (phase 2) owns local clients and the browser WS; this class
8
+ * owns ONLY the single upstream link and the bookkeeping that makes it correct:
9
+ *
10
+ * - reference-counted subscriptions (per (session) → set of local clientIds);
11
+ * - a 60s idle grace after the last unsubscribe, cancelled if a subscribe
12
+ * arrives during it (no thrash opening/closing on a flapping client);
13
+ * - ONE bounded reconnect on an upstream drop, resubscribing every current
14
+ * subscription; a reconnect that does not open within reconnectTimeoutMs, or
15
+ * a second drop, surfaces `machine-unreachable` (P19 — never a reconnect
16
+ * storm);
17
+ * - subscriptions that arrive mid-(re)connect are QUEUED and merged into the
18
+ * resubscribe batch the moment the link opens — never lost;
19
+ * - the peer URL is re-resolved on every subscribe; a changed URL tears the
20
+ * old link down and opens a new one (no stale-URL duplicate links).
21
+ *
22
+ * Everything external is injected (transport, timers, clock) so the whole state
23
+ * machine is deterministically unit-testable without real sockets or wall time.
24
+ * This module performs NO tmux capture and NO local fan-out polling — capture
25
+ * happens only on the owning machine; fan-out to local clients is the injected
26
+ * `onFrameToClients`. (TAP POINT, ops#1: remote subs never enter the local
27
+ * polling loop.)
28
+ */
29
+ const DEFAULT_IDLE_GRACE_MS = 60_000;
30
+ const DEFAULT_RECONNECT_TIMEOUT_MS = 10_000;
31
+ export class PeerStreamProxy {
32
+ d;
33
+ state = 'idle';
34
+ transport = null;
35
+ connectedUrl = null;
36
+ /** session → set of local clientIds subscribed to it. THE source of truth:
37
+ * on every (re)open we resubscribe from this map, so a subscribe that
38
+ * arrives mid-connect is never lost and a mid-connect unsubscribe is never
39
+ * replayed. */
40
+ subs = new Map();
41
+ idleTimer = null;
42
+ reconnectTimer = null;
43
+ reconnectUsed = false;
44
+ constructor(d) {
45
+ this.d = d;
46
+ }
47
+ get currentState() {
48
+ return this.state;
49
+ }
50
+ /** Total live subscriptions across all sessions (refcount). */
51
+ get refCount() {
52
+ let n = 0;
53
+ for (const set of this.subs.values())
54
+ n += set.size;
55
+ return n;
56
+ }
57
+ /** Snapshot of session → clientIds (tests / introspection). */
58
+ sessionsView() {
59
+ const out = {};
60
+ for (const [s, set] of this.subs)
61
+ out[s] = [...set];
62
+ return out;
63
+ }
64
+ log(line) {
65
+ this.d.logger?.(`[peer-stream-proxy ${this.d.peerMachineId}] ${line}`);
66
+ }
67
+ // ── subscribe / unsubscribe (refcounted) ───────────────────────────────
68
+ /** A local client subscribes to a remote session on this peer. */
69
+ subscribe(session, clientId) {
70
+ if (this.state === 'closed') {
71
+ this.log('subscribe after close — ignored');
72
+ return;
73
+ }
74
+ // Cancel a pending idle close — we have demand again.
75
+ if (this.idleTimer) {
76
+ this.d.clearTimer(this.idleTimer);
77
+ this.idleTimer = null;
78
+ if (this.state === 'idle-scheduled')
79
+ this.state = 'active';
80
+ }
81
+ let set = this.subs.get(session);
82
+ if (!set) {
83
+ set = new Set();
84
+ this.subs.set(session, set);
85
+ }
86
+ const firstForSession = set.size === 0;
87
+ set.add(clientId);
88
+ // URL re-resolution (stale-URL guard): if the peer moved, reconnect fresh.
89
+ const url = this.d.resolveUrl();
90
+ if (this.state === 'active' && this.connectedUrl && url && url !== this.connectedUrl) {
91
+ this.log(`peer url changed (${this.connectedUrl} → ${url}); reconnecting`);
92
+ this.teardownTransport();
93
+ this.openFresh(url);
94
+ return; // openFresh replays ALL current subs (incl. this one) on open
95
+ }
96
+ if (this.state === 'idle' || this.state === 'closing') {
97
+ // No link yet → open one; openFresh flushes all subs on open.
98
+ if (!url) {
99
+ // No reachable URL — honest failure, drop the just-added sub for it.
100
+ set.delete(clientId);
101
+ if (set.size === 0)
102
+ this.subs.delete(session);
103
+ this.d.onError(session, 'machine-unreachable', 'no url for peer');
104
+ return;
105
+ }
106
+ this.openFresh(url);
107
+ return;
108
+ }
109
+ if (this.state === 'active') {
110
+ // Link is live → forward only when this is the first client for the
111
+ // session (the upstream is already streaming it for any later client).
112
+ if (firstForSession)
113
+ this.transport?.send({ type: 'subscribe', session });
114
+ return;
115
+ }
116
+ // connecting → the sub is now in `subs`; onUpstreamOpen resubscribes from
117
+ // `subs`, so it is automatically included in the batch (never lost).
118
+ }
119
+ /** A local client unsubscribes (or disconnects). */
120
+ unsubscribe(session, clientId) {
121
+ const set = this.subs.get(session);
122
+ if (!set)
123
+ return;
124
+ set.delete(clientId);
125
+ if (set.size === 0) {
126
+ this.subs.delete(session);
127
+ if (this.state === 'active')
128
+ this.transport?.send({ type: 'unsubscribe', session });
129
+ }
130
+ if (this.refCount === 0)
131
+ this.scheduleIdleClose();
132
+ }
133
+ /** Forward an input/key frame for a session (caller already gated allowRemoteInput). */
134
+ relayInput(frame) {
135
+ if (this.state === 'active')
136
+ this.transport?.send(frame);
137
+ // Not active → input is dropped (caller's terminal shows the live state);
138
+ // we never queue keystrokes across a reconnect (stale input is worse than none).
139
+ }
140
+ // ── connection lifecycle ───────────────────────────────────────────────
141
+ openFresh(url, reconnect = false) {
142
+ // A user-initiated open resets the one-shot reconnect budget; a reconnect
143
+ // leaves `reconnectUsed` true so a further drop → machine-unreachable.
144
+ if (!reconnect)
145
+ this.reconnectUsed = false;
146
+ this.state = 'connecting';
147
+ this.connectedUrl = url;
148
+ const handlers = {
149
+ onOpen: () => this.onUpstreamOpen(),
150
+ onFrame: (f) => this.onUpstreamFrame(f),
151
+ onClose: () => this.onUpstreamClose(),
152
+ };
153
+ try {
154
+ this.transport = this.d.connect(url, handlers);
155
+ }
156
+ catch (e) {
157
+ this.log(`connect threw: ${e?.message ?? e}`);
158
+ this.onUpstreamClose();
159
+ }
160
+ }
161
+ onUpstreamOpen() {
162
+ if (this.state === 'closed') {
163
+ this.transport?.close();
164
+ return;
165
+ }
166
+ this.state = 'active';
167
+ if (this.reconnectTimer) {
168
+ this.d.clearTimer(this.reconnectTimer);
169
+ this.reconnectTimer = null;
170
+ }
171
+ // Resubscribe from `subs` (the source of truth) — this naturally includes
172
+ // any subscribe that arrived mid-connect and excludes any mid-connect
173
+ // unsubscribe. No stale snapshot to lose.
174
+ for (const session of this.subs.keys())
175
+ this.transport?.send({ type: 'subscribe', session });
176
+ // If nothing is subscribed anymore (all unsubscribed during connect), idle-close.
177
+ if (this.refCount === 0)
178
+ this.scheduleIdleClose();
179
+ }
180
+ onUpstreamFrame(frame) {
181
+ const session = typeof frame.session === 'string' ? frame.session : null;
182
+ if (!session)
183
+ return; // peer frames without a session are not fan-out-able
184
+ // Only fan out to sessions we still have local subscribers for.
185
+ if (!this.subs.has(session))
186
+ return;
187
+ this.d.onFrameToClients(session, frame);
188
+ }
189
+ onUpstreamClose() {
190
+ if (this.state === 'closed' || this.state === 'closing') {
191
+ this.state = 'closed';
192
+ return;
193
+ }
194
+ this.transport = null;
195
+ // Nothing subscribed → just go idle, no reconnect.
196
+ if (this.refCount === 0) {
197
+ this.state = 'idle';
198
+ return;
199
+ }
200
+ if (this.reconnectUsed) {
201
+ // Second failure → declare the peer unreachable for every affected session.
202
+ this.log('second upstream drop → machine-unreachable');
203
+ this.failAll('machine-unreachable', 'peer unreachable after reconnect');
204
+ return;
205
+ }
206
+ // First drop → tell clients, attempt ONE bounded reconnect.
207
+ for (const session of this.subs.keys())
208
+ this.d.onError(session, 'peer-stream-lost', 'upstream dropped');
209
+ this.reconnectUsed = true;
210
+ const url = this.d.resolveUrl();
211
+ if (!url) {
212
+ this.failAll('machine-unreachable', 'no url on reconnect');
213
+ return;
214
+ }
215
+ this.openFresh(url, true);
216
+ // Bound the reconnect: if it doesn't open in time, declare unreachable.
217
+ this.reconnectTimer = this.d.setTimer(this.d.reconnectTimeoutMs ?? DEFAULT_RECONNECT_TIMEOUT_MS, () => {
218
+ this.reconnectTimer = null;
219
+ if (this.state !== 'active') {
220
+ this.log('reconnect timed out → machine-unreachable');
221
+ this.teardownTransport();
222
+ this.failAll('machine-unreachable', 'reconnect timed out');
223
+ }
224
+ });
225
+ }
226
+ scheduleIdleClose() {
227
+ if (this.idleTimer || this.state === 'closed')
228
+ return;
229
+ if (this.state === 'active')
230
+ this.state = 'idle-scheduled';
231
+ this.idleTimer = this.d.setTimer(this.d.idleGraceMs ?? DEFAULT_IDLE_GRACE_MS, () => {
232
+ this.idleTimer = null;
233
+ if (this.refCount === 0) {
234
+ this.log('idle grace elapsed → closing');
235
+ this.teardownTransport();
236
+ this.state = 'closed';
237
+ }
238
+ });
239
+ }
240
+ failAll(code, detail) {
241
+ for (const session of this.subs.keys())
242
+ this.d.onError(session, code, detail);
243
+ this.subs.clear();
244
+ this.teardownTransport();
245
+ this.state = 'closed';
246
+ }
247
+ teardownTransport() {
248
+ if (this.reconnectTimer) {
249
+ this.d.clearTimer(this.reconnectTimer);
250
+ this.reconnectTimer = null;
251
+ }
252
+ try {
253
+ this.transport?.close();
254
+ }
255
+ catch { /* @silent-fallback-ok: closing an already-dead upstream is best-effort */ }
256
+ this.transport = null;
257
+ this.connectedUrl = null;
258
+ }
259
+ /** Force-close (peer removed, server shutdown). Idempotent. */
260
+ close() {
261
+ if (this.idleTimer) {
262
+ this.d.clearTimer(this.idleTimer);
263
+ this.idleTimer = null;
264
+ }
265
+ this.subs.clear();
266
+ this.teardownTransport();
267
+ this.state = 'closed';
268
+ }
269
+ }
270
+ //# sourceMappingURL=PeerStreamProxy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PeerStreamProxy.js","sourceRoot":"","sources":["../../src/server/PeerStreamProxy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAgDH,MAAM,qBAAqB,GAAG,MAAM,CAAC;AACrC,MAAM,4BAA4B,GAAG,MAAM,CAAC;AAE5C,MAAM,OAAO,eAAe;IAaG;IAZrB,KAAK,GAAe,MAAM,CAAC;IAC3B,SAAS,GAA6B,IAAI,CAAC;IAC3C,YAAY,GAAkB,IAAI,CAAC;IAC3C;;;oBAGgB;IACC,IAAI,GAAG,IAAI,GAAG,EAAuB,CAAC;IAC/C,SAAS,GAAuB,IAAI,CAAC;IACrC,cAAc,GAAuB,IAAI,CAAC;IAC1C,aAAa,GAAG,KAAK,CAAC;IAE9B,YAA6B,CAAsB;QAAtB,MAAC,GAAD,CAAC,CAAqB;IAAG,CAAC;IAEvD,IAAI,YAAY;QACd,OAAO,IAAI,CAAC,KAAK,CAAC;IACpB,CAAC;IACD,+DAA+D;IAC/D,IAAI,QAAQ;QACV,IAAI,CAAC,GAAG,CAAC,CAAC;QACV,KAAK,MAAM,GAAG,IAAI,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE;YAAE,CAAC,IAAI,GAAG,CAAC,IAAI,CAAC;QACpD,OAAO,CAAC,CAAC;IACX,CAAC;IACD,+DAA+D;IAC/D,YAAY;QACV,MAAM,GAAG,GAA6B,EAAE,CAAC;QACzC,KAAK,MAAM,CAAC,CAAC,EAAE,GAAG,CAAC,IAAI,IAAI,CAAC,IAAI;YAAE,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,GAAG,CAAC,CAAC;QACpD,OAAO,GAAG,CAAC;IACb,CAAC;IAEO,GAAG,CAAC,IAAY;QACtB,IAAI,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,sBAAsB,IAAI,CAAC,CAAC,CAAC,aAAa,KAAK,IAAI,EAAE,CAAC,CAAC;IACzE,CAAC;IAED,0EAA0E;IAE1E,kEAAkE;IAClE,SAAS,CAAC,OAAe,EAAE,QAAgB;QACzC,IAAI,IAAI,CAAC,KAAK,KAAK,QAAQ,EAAE,CAAC;YAC5B,IAAI,CAAC,GAAG,CAAC,iCAAiC,CAAC,CAAC;YAC5C,OAAO;QACT,CAAC;QACD,sDAAsD;QACtD,IAAI,IAAI,CAAC,SAAS,EAAE,CAAC;YACnB,IAAI,CAAC,CAAC,CAAC,UAAU,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;YAClC,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC;YACtB,IAAI,IAAI,CAAC,KAAK,KAAK,gBAAgB;gBAAE,IAAI,CAAC,KAAK,GAAG,QAAQ,CAAC;QAC7D,CAAC;QACD,IAAI,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;QACjC,IAAI,CAAC,GAAG,EAAE,CAAC;YACT,GAAG,GAAG,IAAI,GAAG,EAAE,CAAC;YAChB,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC;QAC9B,CAAC;QACD,MAAM,eAAe,GAAG,GAAG,CAAC,IAAI,KAAK,CAAC,CAAC;QACvC,GAAG,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QAElB,2EAA2E;QAC3E,MAAM,GAAG,GAAG,IAAI,CAAC,CAAC,CAAC,UAAU,EAAE,CAAC;QAChC,IAAI,IAAI,CAAC,KAAK,KAAK,QAAQ,IAAI,IAAI,CAAC,YAAY,IAAI,GAAG,IAAI,GAAG,KAAK,IAAI,CAAC,YAAY,EAAE,CAAC;YACrF,IAAI,CAAC,GAAG,CAAC,qBAAqB,IAAI,CAAC,YAAY,MAAM,GAAG,iBAAiB,CAAC,CAAC;YAC3E,IAAI,CAAC,iBAAiB,EAAE,CAAC;YACzB,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC;YACpB,OAAO,CAAC,8DAA8D;QACxE,CAAC;QAED,IAAI,IAAI,CAAC,KAAK,KAAK,MAAM,IAAI,IAAI,CAAC,KAAK,KAAK,SAAS,EAAE,CAAC;YACtD,8DAA8D;YAC9D,IAAI,CAAC,GAAG,EAAE,CAAC;gBACT,qEAAqE;gBACrE,GAAG,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;gBACrB,IAAI,GAAG,CAAC,IAAI,KAAK,CAAC;oBAAE,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;gBAC9C,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,EAAE,qBAAqB,EAAE,iBAAiB,CAAC,CAAC;gBAClE,OAAO;YACT,CAAC;YACD,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC;YACpB,OAAO;QACT,CAAC;QACD,IAAI,IAAI,CAAC,KAAK,KAAK,QAAQ,EAAE,CAAC;YAC5B,oEAAoE;YACpE,uEAAuE;YACvE,IAAI,eAAe;gBAAE,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,CAAC,CAAC;YAC1E,OAAO;QACT,CAAC;QACD,0EAA0E;QAC1E,qEAAqE;IACvE,CAAC;IAED,oDAAoD;IACpD,WAAW,CAAC,OAAe,EAAE,QAAgB;QAC3C,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;QACnC,IAAI,CAAC,GAAG;YAAE,OAAO;QACjB,GAAG,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;QACrB,IAAI,GAAG,CAAC,IAAI,KAAK,CAAC,EAAE,CAAC;YACnB,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;YAC1B,IAAI,IAAI,CAAC,KAAK,KAAK,QAAQ;gBAAE,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,EAAE,IAAI,EAAE,aAAa,EAAE,OAAO,EAAE,CAAC,CAAC;QACtF,CAAC;QACD,IAAI,IAAI,CAAC,QAAQ,KAAK,CAAC;YAAE,IAAI,CAAC,iBAAiB,EAAE,CAAC;IACpD,CAAC;IAED,wFAAwF;IACxF,UAAU,CAAC,KAA8B;QACvC,IAAI,IAAI,CAAC,KAAK,KAAK,QAAQ;YAAE,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC;QACzD,0EAA0E;QAC1E,iFAAiF;IACnF,CAAC;IAED,0EAA0E;IAElE,SAAS,CAAC,GAAW,EAAE,SAAS,GAAG,KAAK;QAC9C,0EAA0E;QAC1E,uEAAuE;QACvE,IAAI,CAAC,SAAS;YAAE,IAAI,CAAC,aAAa,GAAG,KAAK,CAAC;QAC3C,IAAI,CAAC,KAAK,GAAG,YAAY,CAAC;QAC1B,IAAI,CAAC,YAAY,GAAG,GAAG,CAAC;QACxB,MAAM,QAAQ,GAAqB;YACjC,MAAM,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,cAAc,EAAE;YACnC,OAAO,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC,CAAC;YACvC,OAAO,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,eAAe,EAAE;SACtC,CAAC;QACF,IAAI,CAAC;YACH,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC;QACjD,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACX,IAAI,CAAC,GAAG,CAAC,kBAAmB,CAAW,EAAE,OAAO,IAAI,CAAC,EAAE,CAAC,CAAC;YACzD,IAAI,CAAC,eAAe,EAAE,CAAC;QACzB,CAAC;IACH,CAAC;IAEO,cAAc;QACpB,IAAI,IAAI,CAAC,KAAK,KAAK,QAAQ,EAAE,CAAC;YAC5B,IAAI,CAAC,SAAS,EAAE,KAAK,EAAE,CAAC;YACxB,OAAO;QACT,CAAC;QACD,IAAI,CAAC,KAAK,GAAG,QAAQ,CAAC;QACtB,IAAI,IAAI,CAAC,cAAc,EAAE,CAAC;YACxB,IAAI,CAAC,CAAC,CAAC,UAAU,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC;YACvC,IAAI,CAAC,cAAc,GAAG,IAAI,CAAC;QAC7B,CAAC;QACD,0EAA0E;QAC1E,sEAAsE;QACtE,0CAA0C;QAC1C,KAAK,MAAM,OAAO,IAAI,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE;YAAE,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,CAAC,CAAC;QAC7F,kFAAkF;QAClF,IAAI,IAAI,CAAC,QAAQ,KAAK,CAAC;YAAE,IAAI,CAAC,iBAAiB,EAAE,CAAC;IACpD,CAAC;IAEO,eAAe,CAAC,KAA8B;QACpD,MAAM,OAAO,GAAG,OAAO,KAAK,CAAC,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC;QACzE,IAAI,CAAC,OAAO;YAAE,OAAO,CAAC,qDAAqD;QAC3E,gEAAgE;QAChE,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC;YAAE,OAAO;QACpC,IAAI,CAAC,CAAC,CAAC,gBAAgB,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;IAC1C,CAAC;IAEO,eAAe;QACrB,IAAI,IAAI,CAAC,KAAK,KAAK,QAAQ,IAAI,IAAI,CAAC,KAAK,KAAK,SAAS,EAAE,CAAC;YACxD,IAAI,CAAC,KAAK,GAAG,QAAQ,CAAC;YACtB,OAAO;QACT,CAAC;QACD,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC;QACtB,mDAAmD;QACnD,IAAI,IAAI,CAAC,QAAQ,KAAK,CAAC,EAAE,CAAC;YACxB,IAAI,CAAC,KAAK,GAAG,MAAM,CAAC;YACpB,OAAO;QACT,CAAC;QACD,IAAI,IAAI,CAAC,aAAa,EAAE,CAAC;YACvB,4EAA4E;YAC5E,IAAI,CAAC,GAAG,CAAC,4CAA4C,CAAC,CAAC;YACvD,IAAI,CAAC,OAAO,CAAC,qBAAqB,EAAE,kCAAkC,CAAC,CAAC;YACxE,OAAO;QACT,CAAC;QACD,4DAA4D;QAC5D,KAAK,MAAM,OAAO,IAAI,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE;YAAE,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,EAAE,kBAAkB,EAAE,kBAAkB,CAAC,CAAC;QACxG,IAAI,CAAC,aAAa,GAAG,IAAI,CAAC;QAC1B,MAAM,GAAG,GAAG,IAAI,CAAC,CAAC,CAAC,UAAU,EAAE,CAAC;QAChC,IAAI,CAAC,GAAG,EAAE,CAAC;YACT,IAAI,CAAC,OAAO,CAAC,qBAAqB,EAAE,qBAAqB,CAAC,CAAC;YAC3D,OAAO;QACT,CAAC;QACD,IAAI,CAAC,SAAS,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;QAC1B,wEAAwE;QACxE,IAAI,CAAC,cAAc,GAAG,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,kBAAkB,IAAI,4BAA4B,EAAE,GAAG,EAAE;YACpG,IAAI,CAAC,cAAc,GAAG,IAAI,CAAC;YAC3B,IAAI,IAAI,CAAC,KAAK,KAAK,QAAQ,EAAE,CAAC;gBAC5B,IAAI,CAAC,GAAG,CAAC,2CAA2C,CAAC,CAAC;gBACtD,IAAI,CAAC,iBAAiB,EAAE,CAAC;gBACzB,IAAI,CAAC,OAAO,CAAC,qBAAqB,EAAE,qBAAqB,CAAC,CAAC;YAC7D,CAAC;QACH,CAAC,CAAC,CAAC;IACL,CAAC;IAEO,iBAAiB;QACvB,IAAI,IAAI,CAAC,SAAS,IAAI,IAAI,CAAC,KAAK,KAAK,QAAQ;YAAE,OAAO;QACtD,IAAI,IAAI,CAAC,KAAK,KAAK,QAAQ;YAAE,IAAI,CAAC,KAAK,GAAG,gBAAgB,CAAC;QAC3D,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,WAAW,IAAI,qBAAqB,EAAE,GAAG,EAAE;YACjF,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC;YACtB,IAAI,IAAI,CAAC,QAAQ,KAAK,CAAC,EAAE,CAAC;gBACxB,IAAI,CAAC,GAAG,CAAC,8BAA8B,CAAC,CAAC;gBACzC,IAAI,CAAC,iBAAiB,EAAE,CAAC;gBACzB,IAAI,CAAC,KAAK,GAAG,QAAQ,CAAC;YACxB,CAAC;QACH,CAAC,CAAC,CAAC;IACL,CAAC;IAEO,OAAO,CAAC,IAAgD,EAAE,MAAc;QAC9E,KAAK,MAAM,OAAO,IAAI,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE;YAAE,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC;QAC9E,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC;QAClB,IAAI,CAAC,iBAAiB,EAAE,CAAC;QACzB,IAAI,CAAC,KAAK,GAAG,QAAQ,CAAC;IACxB,CAAC;IAEO,iBAAiB;QACvB,IAAI,IAAI,CAAC,cAAc,EAAE,CAAC;YACxB,IAAI,CAAC,CAAC,CAAC,UAAU,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC;YACvC,IAAI,CAAC,cAAc,GAAG,IAAI,CAAC;QAC7B,CAAC;QACD,IAAI,CAAC;YACH,IAAI,CAAC,SAAS,EAAE,KAAK,EAAE,CAAC;QAC1B,CAAC;QAAC,MAAM,CAAC,CAAC,0EAA0E,CAAC,CAAC;QACtF,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC;QACtB,IAAI,CAAC,YAAY,GAAG,IAAI,CAAC;IAC3B,CAAC;IAED,+DAA+D;IAC/D,KAAK;QACH,IAAI,IAAI,CAAC,SAAS,EAAE,CAAC;YACnB,IAAI,CAAC,CAAC,CAAC,UAAU,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;YAClC,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC;QACxB,CAAC;QACD,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC;QAClB,IAAI,CAAC,iBAAiB,EAAE,CAAC;QACzB,IAAI,CAAC,KAAK,GAAG,QAAQ,CAAC;IACxB,CAAC;CACF"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "instar",
3
- "version": "1.3.388",
3
+ "version": "1.3.389",
4
4
  "description": "Coherence infrastructure for self-evolving AI agents — on the Claude Code or Codex subscription you already have.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "$schema": "./builtin-manifest.schema.json",
3
3
  "schemaVersion": 1,
4
- "generatedAt": "2026-06-07T00:43:22.312Z",
5
- "instarVersion": "1.3.388",
4
+ "generatedAt": "2026-06-07T00:51:44.855Z",
5
+ "instarVersion": "1.3.389",
6
6
  "entryCount": 199,
7
7
  "entries": {
8
8
  "hook:session-start": {
@@ -0,0 +1,45 @@
1
+ # Upgrade Guide — vNEXT
2
+
3
+ <!-- assembled-by: assemble-next-md -->
4
+ <!-- bump: patch -->
5
+
6
+ ## What Changed
7
+
8
+ First building block of the single-dashboard cross-machine streaming feature
9
+ (POOL-DASHBOARD-STREAM-SPEC): the `PeerStreamProxy` — one multiplexed upstream
10
+ connection per peer machine that the dashboard's WebSocket layer will use
11
+ (phase 2) to relay a remote session's terminal stream. This phase ships the
12
+ state machine ONLY; it is not yet wired to the live WebSocketManager, so it
13
+ changes no user-visible behavior on its own.
14
+
15
+ The proxy is the scalability + correctness core the reviewers stressed:
16
+ reference-counted subscriptions, a 60s idle grace (cancelled if demand
17
+ returns), ONE bounded reconnect on a drop with full resubscribe (a second drop
18
+ or a reconnect timeout → machine-unreachable, never a reconnect storm),
19
+ mid-reconnect subscribes merged from the source-of-truth subscription map (never
20
+ lost), and peer-URL-change detection (stale-URL links are torn down). Capture
21
+ stays on the owning machine; this only relays — so fleet cost is
22
+ O(subscriptions), not O(sessions × clients × peers).
23
+
24
+ ## What to Tell Your User
25
+
26
+ Nothing yet — this is internal plumbing. The visible result (click any
27
+ machine's session and it streams) lands when phases 2–3 wire it to the
28
+ dashboard.
29
+
30
+ - audience: agent-only
31
+ - maturity: preview
32
+
33
+ ## Summary of New Capabilities
34
+
35
+ - `src/server/PeerStreamProxy.ts` — the per-peer upstream relay state machine,
36
+ fully transport/timer/clock-injected (no live wiring yet).
37
+
38
+ ## Evidence
39
+
40
+ - `tests/unit/PeerStreamProxy.test.ts` (13 tests, deterministic): open+multiplex
41
+ (one upstream, no double-subscribe for a second client), idle-close +
42
+ mid-grace reactivate, bounded reconnect + resubscribe, second-drop →
43
+ unreachable, reconnect-timeout → unreachable, mid-reconnect subscribe merged,
44
+ url-change reopen, no-url → machine-unreachable, input-relay-only-when-active,
45
+ idempotent close.
@@ -0,0 +1,61 @@
1
+ # Side-Effects Review — Pool dashboard streaming phase 1 (PeerStreamProxy)
2
+
3
+ **Version / slug:** `dashboard-stream-phase1-proxy`
4
+ **Date:** `2026-06-06`
5
+ **Author:** `echo`
6
+ **Second-pass reviewer:** `not required`
7
+
8
+ ## Summary of the change
9
+
10
+ A NEW, self-contained module (`PeerStreamProxy`) — phase 1 of the converged
11
+ POOL-DASHBOARD-STREAM-SPEC. The per-peer upstream relay state machine, with all
12
+ external surfaces (transport, timers, clock) injected. NOT wired into the live
13
+ WebSocketManager or server boot in this phase, so it has zero runtime effect
14
+ until phase 2 consumes it.
15
+
16
+ ## Decision-point inventory
17
+
18
+ The state machine's transitions: open / multiplex / idle-close / reactivate /
19
+ bounded-reconnect / second-drop-unreachable / reconnect-timeout / url-change /
20
+ no-url. Each is covered both-sides by a deterministic test.
21
+
22
+ ## 1. Over-block
23
+
24
+ N/A — no gating; a relay. The conservative reconnect policy (one attempt per
25
+ episode, then machine-unreachable) could declare a flaky-but-recoverable peer
26
+ unreachable, but the user simply re-subscribes (re-click), which opens fresh
27
+ and resets the budget. Storm-proof beats persistent.
28
+
29
+ ## 2. Under-block
30
+
31
+ Keystrokes are never queued across a (re)connect — stale input is worse than
32
+ none. Acceptable: the dashboard shows the live link state (phase 3) so the user
33
+ knows when input is live.
34
+
35
+ ## 3. Level-of-abstraction fit
36
+
37
+ Pure module under src/server/, no I/O of its own. The WebSocketManager (phase 2)
38
+ owns local clients + the browser WS and injects the transport; this owns only
39
+ the single upstream link + its bookkeeping. Matches the P2 precedent of
40
+ shipping a tested building block before its consumer.
41
+
42
+ ## 4. Signal vs authority compliance
43
+
44
+ **Required reference:** [docs/signal-vs-authority.md](../../docs/signal-vs-authority.md)
45
+
46
+ No authority. A transport relay; all auth (ticket) + input gating
47
+ (allowRemoteInput) live in phase 2 at the WSManager/upgrade boundary per spec
48
+ §2.3.
49
+
50
+ ## 5. Interactions
51
+
52
+ - WebSocketManager (phase 2): the future consumer; unaffected now (no import).
53
+ - Local polling loop: untouched — remote subs never enter it (TAP POINT). This
54
+ module does no tmux capture.
55
+ - P19 (No Unbounded Loops): the reconnect is a single bounded attempt; tests
56
+ assert the second-drop and timeout both terminate to machine-unreachable.
57
+
58
+ ## 6. External surfaces
59
+
60
+ None in this phase. No routes, config, notifications, or wiring. A new source
61
+ file + its test only.