instar 1.3.387 → 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.
@@ -115,11 +115,25 @@ export interface CutoverReadinessDeps {
115
115
  runImportDryRun?: (() => Promise<ImportRunResult>) | null;
116
116
  /** Freshness bound for the parity window (default 6h). */
117
117
  maxPassStalenessMs?: number;
118
+ /**
119
+ * Wall-clock max-hold for a single live fetch (parity pass OR import dry-run),
120
+ * enforced at the single-flight-lock boundary (default 12m). This is a
121
+ * defense-in-depth backstop ON TOP OF whatever internal timeout the injected
122
+ * fetch carries: if that fetch ever fails to settle (e.g. an AbortSignal that
123
+ * doesn't fire on a stalled socket, or a compare step with no timeout of its
124
+ * own), the lock would otherwise be held forever and every subsequent pass +
125
+ * dry-run is refused with "already in flight" — exactly the ~85-minute stuck
126
+ * lock observed in #948. The max-hold guarantees the lock releases within a
127
+ * bounded time so the next sweep can retry; it sits comfortably above
128
+ * HttpParitySource's 600s total budget so it only fires when that inner budget
129
+ * has genuinely failed, never on a legitimately-slow-but-working pass. */
130
+ maxLiveFetchMs?: number;
118
131
  now?: () => number;
119
132
  }
120
133
  export declare class CutoverReadiness {
121
134
  private readonly d;
122
135
  private readonly maxStaleMs;
136
+ private readonly maxLiveFetchMs;
123
137
  /**
124
138
  * Single-flight guard over the LIVE SOURCE FETCH — shared by the parity pass
125
139
  * and the import dry-run because both page the full live source. Without it,
@@ -133,6 +147,15 @@ export declare class CutoverReadiness {
133
147
  private liveFetchInFlight;
134
148
  constructor(deps: CutoverReadinessDeps);
135
149
  private nowMs;
150
+ /**
151
+ * Race an injected live-fetch promise against a wall-clock max-hold so the
152
+ * single-flight lock can NEVER be held indefinitely by a fetch that fails to
153
+ * settle (#948). On timeout this rejects (the caller's `finally` then releases
154
+ * the lock and the next sweep retries); the orphaned work is abandoned — a
155
+ * re-attempt is cheap and idempotent, an unreleasable lock is not. Uses real
156
+ * wall-clock time (NOT `nowMs()`), since the bug is a promise that never
157
+ * settles in real time regardless of an injected clock. */
158
+ private withMaxHold;
136
159
  /** Persist an import IntegrityReport (server-side import tooling only). */
137
160
  recordIntegrityReport(report: IntegrityReport, generatedAt?: string): void;
138
161
  integrityStatus(): IntegrityStatus;
@@ -1 +1 @@
1
- {"version":3,"file":"cutoverReadiness.d.ts","sourceRoot":"","sources":["../../src/feedback-factory/cutoverReadiness.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAIH,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,iCAAiC,CAAC;AAC5E,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,4BAA4B,CAAC;AACpE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,gCAAgC,CAAC;AACtE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAC;AACnE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAE1D,MAAM,WAAW,eAAe;IAC9B,2DAA2D;IAC3D,GAAG,EAAE,OAAO,CAAC;IACb,2EAA2E;IAC3E,MAAM,EAAE,OAAO,CAAC;IAChB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,uDAAuD;IACvD,OAAO,EAAE;QACP,qBAAqB,EAAE,MAAM,CAAC;QAC9B,iBAAiB,EAAE,MAAM,CAAC;QAC1B,kBAAkB,EAAE,MAAM,CAAC;QAC3B,YAAY,EAAE,MAAM,CAAC;KACtB,GAAG,IAAI,CAAC;CACV;AAED,MAAM,WAAW,eAAgB,SAAQ,iBAAiB;IACxD,0EAA0E;IAC1E,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B;iGAC6F;IAC7F,KAAK,EAAE,OAAO,CAAC;CAChB;AAED,qFAAqF;AACrF,MAAM,WAAW,kBAAkB;IACjC,GAAG,EAAE,OAAO,CAAC;IACb,MAAM,EAAE,OAAO,CAAC;IAChB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,yFAAyF;IACzF,QAAQ,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IACxD,0FAA0F;IAC1F,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;CACjC;AAED,MAAM,WAAW,sBAAsB;IACrC;kFAC8E;IAC9E,KAAK,EAAE,OAAO,CAAC;IACf,mFAAmF;IACnF,IAAI,EAAE,uBAAuB,CAAC;IAC9B,SAAS,EAAE,eAAe,CAAC;IAC3B,MAAM,EAAE,eAAe,CAAC;IACxB;;;;;OAKG;IACH,YAAY,EAAE,kBAAkB,CAAC;CAClC;AAED,MAAM,MAAM,iBAAiB,GACzB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,gBAAgB,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,OAAO,CAAA;KAAE,CAAC;IAAC,IAAI,EAAE,iBAAiB,CAAA;CAAE,GAC9H;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAElC,MAAM,MAAM,mBAAmB,GAC3B;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,eAAe,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,GAC1D;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAElC,MAAM,WAAW,oBAAoB;IACnC,aAAa,EAAE,oBAAoB,CAAC;IACpC,2EAA2E;IAC3E,mBAAmB,EAAE,MAAM,CAAC;IAC5B;;gEAE4D;IAC5D,cAAc,EAAE,CAAC,MAAM,OAAO,CAAC,YAAY,CAAC,CAAC,GAAG,IAAI,CAAC;IACrD;;;kCAG8B;IAC9B,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC;mFAC+E;IAC/E,eAAe,CAAC,EAAE,CAAC,MAAM,OAAO,CAAC,eAAe,CAAC,CAAC,GAAG,IAAI,CAAC;IAC1D,0DAA0D;IAC1D,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB;AAeD,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAuB;IACzC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC;;;;;;;;;OASG;IACH,OAAO,CAAC,iBAAiB,CAA4E;gBAEzF,IAAI,EAAE,oBAAoB;IAUtC,OAAO,CAAC,KAAK;IAIb,2EAA2E;IAC3E,qBAAqB,CAAC,MAAM,EAAE,eAAe,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI;IAS1E,eAAe,IAAI,eAAe;IAuBlC,YAAY,IAAI,eAAe;IAW/B;;;;;;OAMG;IACG,aAAa,IAAI,OAAO,CAAC,iBAAiB,CAAC;IA0BjD;;;;;OAKG;IACG,mBAAmB,IAAI,OAAO,CAAC,mBAAmB,CAAC;IAuBzD,kBAAkB,IAAI,kBAAkB;IAmBxC,uFAAuF;IACvF,MAAM,IAAI,sBAAsB;CAYjC"}
1
+ {"version":3,"file":"cutoverReadiness.d.ts","sourceRoot":"","sources":["../../src/feedback-factory/cutoverReadiness.ts"],"names":[],"mappings":"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"}
@@ -24,9 +24,13 @@
24
24
  import fs from 'node:fs';
25
25
  import path from 'node:path';
26
26
  const DEFAULT_MAX_PASS_STALENESS_MS = 6 * 60 * 60 * 1000;
27
+ /** 12m — above HttpParitySource's 600s total budget + a working pass's compare,
28
+ * so the lock-boundary max-hold is a true backstop, not a false-abort risk. */
29
+ const DEFAULT_MAX_LIVE_FETCH_MS = 12 * 60 * 1000;
27
30
  export class CutoverReadiness {
28
31
  d;
29
32
  maxStaleMs;
33
+ maxLiveFetchMs;
30
34
  /**
31
35
  * Single-flight guard over the LIVE SOURCE FETCH — shared by the parity pass
32
36
  * and the import dry-run because both page the full live source. Without it,
@@ -46,10 +50,36 @@ export class CutoverReadiness {
46
50
  }
47
51
  this.d = deps;
48
52
  this.maxStaleMs = deps.maxPassStalenessMs ?? DEFAULT_MAX_PASS_STALENESS_MS;
53
+ this.maxLiveFetchMs = deps.maxLiveFetchMs ?? DEFAULT_MAX_LIVE_FETCH_MS;
49
54
  }
50
55
  nowMs() {
51
56
  return this.d.now ? this.d.now() : Date.now();
52
57
  }
58
+ /**
59
+ * Race an injected live-fetch promise against a wall-clock max-hold so the
60
+ * single-flight lock can NEVER be held indefinitely by a fetch that fails to
61
+ * settle (#948). On timeout this rejects (the caller's `finally` then releases
62
+ * the lock and the next sweep retries); the orphaned work is abandoned — a
63
+ * re-attempt is cheap and idempotent, an unreleasable lock is not. Uses real
64
+ * wall-clock time (NOT `nowMs()`), since the bug is a promise that never
65
+ * settles in real time regardless of an injected clock. */
66
+ async withMaxHold(work, op) {
67
+ let timer;
68
+ const guard = new Promise((_resolve, reject) => {
69
+ timer = setTimeout(() => {
70
+ reject(new Error(`live ${op} fetch exceeded the max-hold budget (${this.maxLiveFetchMs}ms) — releasing the single-flight lock so a later sweep can retry; the underlying fetch did not settle within budget (see #948)`));
71
+ }, this.maxLiveFetchMs);
72
+ if (typeof timer.unref === 'function')
73
+ timer.unref();
74
+ });
75
+ try {
76
+ return await Promise.race([work, guard]);
77
+ }
78
+ finally {
79
+ if (timer)
80
+ clearTimeout(timer);
81
+ }
82
+ }
53
83
  /** Persist an import IntegrityReport (server-side import tooling only). */
54
84
  recordIntegrityReport(report, generatedAt) {
55
85
  const envelope = {
@@ -109,7 +139,7 @@ export class CutoverReadiness {
109
139
  this.liveFetchInFlight = { op: 'parity-pass', startedAt: new Date(this.nowMs()).toISOString() };
110
140
  let result;
111
141
  try {
112
- result = await this.d.runParityCheck();
142
+ result = await this.withMaxHold(this.d.runParityCheck(), 'parity-pass');
113
143
  }
114
144
  catch (err) {
115
145
  return { ok: false, reason: `parity check failed: ${err instanceof Error ? err.message : String(err)}` };
@@ -142,7 +172,7 @@ export class CutoverReadiness {
142
172
  this.liveFetchInFlight = { op: 'import-dry-run', startedAt: new Date(this.nowMs()).toISOString() };
143
173
  let result;
144
174
  try {
145
- result = await this.d.runImportDryRun();
175
+ result = await this.withMaxHold(this.d.runImportDryRun(), 'import-dry-run');
146
176
  }
147
177
  catch (err) {
148
178
  return { ok: false, reason: `import dry-run failed: ${err instanceof Error ? err.message : String(err)}` };
@@ -1 +1 @@
1
- {"version":3,"file":"cutoverReadiness.js","sourceRoot":"","sources":["../../src/feedback-factory/cutoverReadiness.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"cutoverReadiness.js","sourceRoot":"","sources":["../../src/feedback-factory/cutoverReadiness.ts"],"names":[],"mappings":"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"}
@@ -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":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "instar",
3
- "version": "1.3.387",
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:18:22.466Z",
5
- "instarVersion": "1.3.387",
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,23 @@
1
+ # Upgrade Guide — vNEXT
2
+
3
+ <!-- assembled-by: assemble-next-md -->
4
+ <!-- bump: patch -->
5
+
6
+ ## What Changed
7
+
8
+ Fixed a liveness bug in the cutover-readiness checker (#948): `CutoverReadiness`'s single-flight lock over the live source fetch (used by both the parity pass and the import dry-run) could be held **indefinitely** if the injected fetch never settled. In production the lock stuck for ~85 minutes (`logs/server.log`), so every subsequent parity pass and dry-run was refused with "live source fetch already in flight" — the parity-pass feeder kept no-opping and the cutover parity window went stale and never refreshed.
9
+
10
+ `src/feedback-factory/cutoverReadiness.ts` now wraps the awaited live fetch in a wall-clock **max-hold guard** (`withMaxHold`, new optional `maxLiveFetchMs` dep, default 12 minutes) at the lock boundary. The lock is released in a `finally`, but a `finally` can't run while the awaited promise never settles — so the guard races the fetch against a timer and guarantees the lock releases within a bounded time regardless of *why* the fetch stalled (an `AbortSignal.timeout` that didn't fire on a stalled socket, or the un-timed compare step). The default sits comfortably above `HttpParitySource`'s 600s total budget, so it only fires when that inner budget has genuinely failed — never on a slow-but-working pass.
11
+
12
+ ## What to Tell Your User
13
+
14
+ If you were ever told the feedback-migration "cutover readiness" check was stuck or its parity window had gone stale, this fixes the underlying cause: a single slow/hung check could jam the checker until the next restart. Now a check that takes too long is abandoned cleanly and the next one runs on schedule. It ships on by default with a safe 12-minute backstop and needs no configuration.
15
+
16
+ ## Summary of New Capabilities
17
+
18
+ - A wall-clock max-hold backstop on the cutover-readiness live-fetch single-flight lock — a stalled parity pass or import dry-run can no longer hang the checker indefinitely.
19
+ - New optional `maxLiveFetchMs` knob (default 12m); the default IS the fix, so existing agents get it automatically on update with no config change.
20
+
21
+ ## Evidence
22
+
23
+ Before: a single stalled live fetch held the single-flight lock until the next server restart (~85 min observed), starving the parity feeder. After: the lock releases within `maxLiveFetchMs` and the next sweep retries. Pinned by a Tier-1 regression (`tests/unit/cutover-readiness.test.ts`): a `runParityCheck` (and `runImportDryRun`) that never settles now returns `ok:false` AND a subsequent pass on the same instance is NOT refused as in-flight — it runs and records normally. 22 cutover-readiness unit tests green (2 new + 20 existing through the new path); `tsc --noEmit` clean.
@@ -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,95 @@
1
+ # Side-Effects Review — Cutover-readiness live-fetch max-hold (lock-boundary backstop, #948)
2
+
3
+ **Version / slug:** `cutover-parity-lock-maxhold`
4
+ **Date:** `2026-06-07`
5
+ **Author:** `echo`
6
+ **Second-pass reviewer:** `self-review (single-file behavioral hardening; both branches — stalled and healthy — covered by Tier-1 tests over the real CutoverReadiness)`
7
+
8
+ ## Summary of the change
9
+
10
+ Fixes #948: `CutoverReadiness`'s single-flight lock (`liveFetchInFlight`) could be held
11
+ indefinitely when an injected live fetch (parity pass or import dry-run) never settled —
12
+ observed ~85 minutes in production (`logs/server.log`), permanently refusing every
13
+ subsequent parity pass + dry-run with "live source fetch already in flight" and starving
14
+ the parity feeder (so the cutover window went stale and never refreshed).
15
+
16
+ Root: the lock is released in a `finally`, which is correct — but a `finally` cannot run
17
+ while the awaited promise never settles. `HttpParitySource.prepare()` has per-page +
18
+ between-page bounds, so the stall is either an `AbortSignal.timeout` that doesn't fire on a
19
+ particular socket stall, or the un-timed compare step. Rather than chase the sub-cause, the
20
+ fix adds a wall-clock **max-hold guard at the lock boundary** (`withMaxHold`, default 12m):
21
+ the awaited fetch is raced against a timer, so the lock ALWAYS releases within a bounded
22
+ time regardless of why the fetch stalled. Structure > willpower — the lock-holder enforces
23
+ its own max-hold, independent of the injected fetch's internal timeouts.
24
+
25
+ Files: `src/feedback-factory/cutoverReadiness.ts` (+ `tests/unit/cutover-readiness.test.ts`).
26
+
27
+ ## Decision-point inventory
28
+
29
+ - `withMaxHold` — add — a private timeout-race helper. NO decision authority: it only
30
+ bounds how long the single-flight lock can be held; it never decides parity/integrity.
31
+ - `maxLiveFetchMs` config (default 12m) — add — a backstop dial, set comfortably above
32
+ `HttpParitySource`'s 600s total budget so it only fires when that inner budget genuinely
33
+ failed (never a false-abort of a slow-but-working pass).
34
+
35
+ ## 1. Over-block
36
+
37
+ The only thing the max-hold can "block" is a live fetch that exceeds 12 minutes. A healthy
38
+ parity pass (~15s/page, 600s inner budget) is far under that, so a working pass is never
39
+ aborted. Default chosen above the inner budget specifically to avoid over-block.
40
+
41
+ ## 2. Under-block
42
+
43
+ If a stall somehow resolved at exactly the budget edge, the worst case is one wasted sweep
44
+ (recorded nothing) and a retry next tick — strictly better than the pre-fix infinite hold.
45
+
46
+ ## 3. Level-of-abstraction fit
47
+
48
+ Correct. The guard sits at the single-flight-lock boundary it protects, not inside the
49
+ fetch (which already has its own, evidently-insufficient, internal timeouts). It is a
50
+ backstop, not a replacement — the inner budgets still do the primary bounding.
51
+
52
+ ## 4. Signal vs authority compliance
53
+
54
+ - [x] No block/allow authority. The change only ensures a lock releases; it never gates a
55
+ parity/integrity decision. `runParityPass` still records nothing on failure (a max-hold
56
+ timeout is a failed check = absence of evidence, exactly as a fetch error already was).
57
+
58
+ ## 5. Interactions
59
+
60
+ - **Orphaned work:** when the max-hold fires, the abandoned fetch promise keeps running but
61
+ is harmless — its later settlement is ignored (control already left the `await`; the
62
+ `recordResult` line after it is never reached for the timed-out call), so it cannot
63
+ double-record or re-acquire the lock. It holds at most one connection until it aborts or
64
+ the process restarts — a bounded, acceptable cost versus an unreleasable lock.
65
+ - **Double-fire:** none — `Promise.race` settles once; `clearTimeout` in `finally` disarms
66
+ the timer on the happy path.
67
+ - **Clock:** the guard uses real `setTimeout` (NOT the injected `now()`), because the bug is
68
+ a promise that never settles in REAL time; an injected test clock must not disarm it.
69
+
70
+ ## 6. External surfaces
71
+
72
+ - **Install base:** `maxLiveFetchMs` is a new OPTIONAL dep with a safe default → existing
73
+ agents get the backstop automatically on update, no config change required (Migration
74
+ Parity: the default IS the fix; nothing to migrate).
75
+ - **Persistent state:** none new.
76
+ - **Other agents / attention queue:** unchanged.
77
+
78
+ ## 7. Rollback cost
79
+
80
+ Pure additive hardening. Back-out = revert the commit. No persistent-state cleanup; healthy
81
+ passes behave identically with or without the guard.
82
+
83
+ ## Conclusion
84
+
85
+ A wall-clock backstop on a single-flight lock, proven by a Tier-1 regression that a
86
+ never-settling fetch now releases the lock (a later pass proceeds) instead of hanging it
87
+ for ~85 minutes. No new authority; safe default; auto-applies to existing agents.
88
+
89
+ ## Evidence pointers
90
+
91
+ - Tier 1: `tests/unit/cutover-readiness.test.ts` — 2 new (#948 regression for both
92
+ `runParityPass` and `runImportDryRunPass`: a never-settling fetch hits the max-hold,
93
+ returns `ok:false`, and the lock releases so the next pass records normally) + the 20
94
+ existing cutover tests still green through the new code path (22 total).
95
+ - tsc `--noEmit` clean.
@@ -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.