@cortexkit/subc-client 0.5.0 → 0.8.1

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.
@@ -1 +1 @@
1
- {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAC3D,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAEzC,eAAO,MAAM,SAAS,KAAK,CAAC;AAC5B,eAAO,MAAM,SAAS,KAAK,CAAC;AAC5B,eAAO,MAAM,oBAAoB,OAAO,CAAC;AACzC,eAAO,MAAM,mBAAmB,mBAAmB,CAAC;AACpD,eAAO,MAAM,kBAAkB,mBAAmB,CAAC;AACnD,eAAO,MAAM,mBAAmB,WAAW,CAAC;AAE5C,qBAAa,SAAU,SAAQ,KAAK;CAAG;AAEvC,yEAAyE;AACzE,wBAAgB,YAAY,CAC1B,GAAG,EAAE,UAAU,EACf,MAAM,EAAE,MAAM,EACd,WAAW,EAAE,UAAU,EACvB,WAAW,EAAE,UAAU,EACvB,QAAQ,EAAE,UAAU,GACnB,UAAU,CAOZ;AA2CD;;;;GAIG;AACH,wBAAsB,kBAAkB,CACtC,IAAI,EAAE,UAAU,EAChB,IAAI,EAAE,cAAc,EACpB,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,IAAI,CAAC,CAwBf"}
1
+ {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAC3D,OAAO,EAAE,UAAU,EAAiB,MAAM,aAAa,CAAC;AAExD,eAAO,MAAM,SAAS,KAAK,CAAC;AAC5B,eAAO,MAAM,SAAS,KAAK,CAAC;AAC5B,eAAO,MAAM,oBAAoB,OAAO,CAAC;AACzC,eAAO,MAAM,mBAAmB,mBAAmB,CAAC;AACpD,eAAO,MAAM,kBAAkB,mBAAmB,CAAC;AACnD,eAAO,MAAM,mBAAmB,WAAW,CAAC;AAE5C,qBAAa,SAAU,SAAQ,KAAK;CAAG;AAEvC,yEAAyE;AACzE,wBAAgB,YAAY,CAC1B,GAAG,EAAE,UAAU,EACf,MAAM,EAAE,MAAM,EACd,WAAW,EAAE,UAAU,EACvB,WAAW,EAAE,UAAU,EACvB,QAAQ,EAAE,UAAU,GACnB,UAAU,CAOZ;AAuDD;;;;GAIG;AACH,wBAAsB,kBAAkB,CACtC,IAAI,EAAE,UAAU,EAChB,IAAI,EAAE,cAAc,EACpB,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,IAAI,CAAC,CAwBf"}
package/dist/auth.js CHANGED
@@ -13,6 +13,7 @@
13
13
  // the JSON body. Byte arrays (nonces, proofs, ids) serialize as JSON arrays of
14
14
  // numbers, matching serde's default for [u8; N].
15
15
  import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
16
+ import { writeBorrowed } from "./socket.js";
16
17
  export const NONCE_LEN = 32;
17
18
  export const PROOF_LEN = 32;
18
19
  export const MAX_AUTH_MESSAGE_LEN = 4096;
@@ -42,8 +43,8 @@ async function writeMessage(sock, value, deadlineMs) {
42
43
  }
43
44
  const lenPrefix = new Uint8Array(4);
44
45
  new DataView(lenPrefix.buffer).setUint32(0, json.length, true);
45
- await sock.write(lenPrefix, deadlineMs);
46
- await sock.write(json, deadlineMs);
46
+ await writeBorrowed(sock, lenPrefix, deadlineMs);
47
+ await writeBorrowed(sock, json, deadlineMs);
47
48
  }
48
49
  async function readMessage(sock, deadlineMs) {
49
50
  const lenBytes = await sock.readExact(4, deadlineMs);
@@ -59,6 +60,17 @@ async function readMessage(sock, deadlineMs) {
59
60
  throw new AuthError(`auth message JSON decode failed: ${String(err)}`);
60
61
  }
61
62
  }
63
+ function authBytes(value, field) {
64
+ if (!Array.isArray(value)) {
65
+ throw new AuthError(`auth field '${field}' must be a byte array`);
66
+ }
67
+ for (const byte of value) {
68
+ if (typeof byte !== "number" || !Number.isInteger(byte) || byte < 0 || byte > 255) {
69
+ throw new AuthError(`auth field '${field}' has invalid byte ${String(byte)}`);
70
+ }
71
+ }
72
+ return Uint8Array.from(value);
73
+ }
62
74
  /**
63
75
  * Run the client handshake over an already-connected socket. Resolves on
64
76
  * success; throws AuthError on any proof/identity mismatch or framing fault.
@@ -68,9 +80,9 @@ export async function authenticateClient(sock, conn, deadlineMs) {
68
80
  const clientNonce = new Uint8Array(randomBytes(NONCE_LEN));
69
81
  await writeMessage(sock, { client_nonce: Array.from(clientNonce), role: DEFAULT_CLIENT_ROLE }, deadlineMs);
70
82
  const proof = await readMessage(sock, deadlineMs);
71
- const serverNonce = Uint8Array.from(proof.server_nonce);
72
- const daemonId = Uint8Array.from(proof.daemon_id);
73
- const serverProof = Uint8Array.from(proof.server_proof);
83
+ const serverNonce = authBytes(proof.server_nonce, "server_nonce");
84
+ const daemonId = authBytes(proof.daemon_id, "daemon_id");
85
+ const serverProof = authBytes(proof.server_proof, "server_proof");
74
86
  const expected = computeProof(conn.key, SERVER_PROOF_DOMAIN, clientNonce, serverNonce, daemonId);
75
87
  if (!constantTimeEq(expected, serverProof)) {
76
88
  throw new AuthError("server proof mismatch — wrong key or impostor daemon");
package/dist/auth.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"auth.js","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"auth.js","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"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"}
package/dist/client.d.ts CHANGED
@@ -110,13 +110,14 @@ export declare class SubcCallError extends Error {
110
110
  /**
111
111
  * A live subscription to a provider's event stream, riding a single held-open
112
112
  * request. `onEvent` fires for each StreamData frame; `closed` resolves when the
113
- * provider ends the stream (StreamEnd) and rejects on an Error terminal or a route
114
- * GOODBYE. `unsubscribe` cancels the held-open request so the provider unwinds.
113
+ * provider ends the stream (StreamEnd) or the consumer unsubscribes, and rejects on
114
+ * an Error terminal or a route GOODBYE. `unsubscribe` cancels the held-open request
115
+ * so the provider unwinds.
115
116
  */
116
117
  export interface Subscription {
117
118
  /** Cancel the subscription: sends Cancel on the held-open request; idempotent. */
118
119
  unsubscribe(): void;
119
- /** Resolves on StreamEnd; rejects on an Error terminal or route close. */
120
+ /** Resolves on StreamEnd or local unsubscribe; rejects on an Error terminal or route close. */
120
121
  readonly closed: Promise<void>;
121
122
  }
122
123
  export declare class SubcError extends Error {
@@ -134,6 +135,14 @@ export interface ConnectOptions {
134
135
  reconnectBackoff?: ReconnectBackoff;
135
136
  /** Injectable sleep for timer-free reconnect tests. */
136
137
  sleep?: (ms: number) => Promise<void>;
138
+ /**
139
+ * Window the post-deadline liveness probe waits for ANY inbound frame after
140
+ * sending its channel-0 Ping before convicting the socket as half-open.
141
+ * Genuine event-loop starvation self-protects at any setting: a loop starved
142
+ * enough to delay the probe's check also delays the read loop, and when it
143
+ * wakes the buffered Pong dispatches first. Exposed for deterministic tests.
144
+ */
145
+ livenessProbeWindowMs?: number;
137
146
  /**
138
147
  * Hard cap on the timeout-arbitration grace window (see
139
148
  * TIMEOUT_ARBITRATION_GRACE_MS). A reply whose bytes are actively arriving when
@@ -142,6 +151,25 @@ export interface ConnectOptions {
142
151
  * exposed mainly so tests can prove the arbitration deterministically.
143
152
  */
144
153
  timeoutArbitrationGraceMs?: number;
154
+ /**
155
+ * Observer for daemon-originated channel-0 control pushes (`route.closing`,
156
+ * `route.closed`, and any op added later). Purely advisory: the daemon's
157
+ * load-bearing route-death signal remains the GOODBYE frame, and nothing in
158
+ * the client's own route lifecycle consumes these. The contract is the wire
159
+ * contract verbatim -- unrecognized ops still arrive here (callers ignore
160
+ * what they don't know, per the MUST-ignore clause), a push that fails to
161
+ * parse as JSON is dropped without surfacing, and an observer throw is
162
+ * swallowed like every other caller callback so it cannot fail the read loop
163
+ * or unrelated requests.
164
+ */
165
+ onControlPush?: (push: ControlPush) => void;
166
+ }
167
+ /** A parsed daemon-originated channel-0 control push. */
168
+ export interface ControlPush {
169
+ /** The push discriminator, e.g. "route.closing" | "route.closed". */
170
+ op: string;
171
+ /** The full parsed body, `op` included, for op-specific fields. */
172
+ body: Record<string, unknown>;
145
173
  }
146
174
  export declare class SubcClient {
147
175
  private sock;
@@ -209,6 +237,47 @@ export declare class SubcClient {
209
237
  private sendManaged;
210
238
  private cachedRouteHandle;
211
239
  private openCachedRoute;
240
+ private lastInboundAtMs;
241
+ private livenessProbe;
242
+ /**
243
+ * Discriminate event-loop starvation from a half-open socket after a
244
+ * deadline-no-drop settle. The arbitration deliberately keeps the socket on
245
+ * that verdict (a late-read reply under load must not cost a healthy
246
+ * connection), but a half-open socket — peer vanished with no FIN/RST, as
247
+ * after host sleep/wake — yields the SAME verdict forever, so the keep
248
+ * becomes a permanent pin to a dead transport. The discriminator is
249
+ * socket-level liveness evidence: send a channel-0 Ping (the daemon always
250
+ * answers — subc-core control.rs) and require ANY inbound frame within the
251
+ * window; the Pong suffices, and a busy socket's other traffic proves the
252
+ * link just as well. Silence convicts: the socket is failed with a named
253
+ * cause and closed, so the ordinary drop path (pending rejection now,
254
+ * reconnect on the next call) takes over.
255
+ *
256
+ * Starvation cannot be falsely convicted by a short window: a loop starved
257
+ * enough to delay the read loop delays this probe's own timer identically,
258
+ * and when both wake the buffered Pong dispatches before the check runs.
259
+ *
260
+ * A PENDING CHANNEL-0 REQUEST SUSPENDS THE PROBE (both at launch and at the
261
+ * conviction check). The daemon's connection loop handles frames one at a
262
+ * time, and some channel-0 ops legally park it for seconds — route.open
263
+ * awaits the module's bind ack inline for up to route_bind_relay_timeout
264
+ * (~12s in production) — during which OUR Ping sits unread in the daemon's
265
+ * socket buffer. On an otherwise-quiet connection that silence is fully
266
+ * explained by our own in-flight control op, and convicting on it would
267
+ * tear down a healthy connection mid-bind (BROCA's Athena panel flagged
268
+ * this exact premise; the FIFO fact is verified in subc-core server.rs
269
+ * connection_loop + control.rs handle_route_open). The client always knows
270
+ * its own channel-0 pendings, so the gate is local and exact: no probe
271
+ * while one is in flight, no conviction if one appeared during the window.
272
+ *
273
+ * Scope: hooked from the managed-call path only. Control-plane requests
274
+ * (route.open, catalog.list) are short-lived and their failures already
275
+ * escalate through reconnect classification; the managed path is where a
276
+ * plugin lives for hours and where the pin was observed in production.
277
+ */
278
+ /** Any in-flight channel-0 request (pendings keyed without a route handle). */
279
+ private hasControlPending;
280
+ private probeLivenessAfterDeadline;
212
281
  private ensureConnectedForManaged;
213
282
  private scheduleReconnectAfterDrop;
214
283
  private reconnectAfterDrop;
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
package/dist/client.js CHANGED
@@ -14,7 +14,7 @@ import { AuthError, authenticateClient } from "./auth.js";
14
14
  import { ConnectionFileError, readConnectionFile } from "./connection-file.js";
15
15
  import { AdmissionClass, buildFrame, buildFlags, encodeFrame, FrameType, Priority, } from "./envelope.js";
16
16
  import { belongsToConnection, createRouteHandle, newConnectionToken, sameRouteHandle, StaleRouteHandleError, } from "./route-handle.js";
17
- import { SocketClosedError, SocketTimeoutError, SocketWriteNotQueuedError, SocketWriteQueuedError, SubcSocket, } from "./socket.js";
17
+ import { SocketClosedError, SocketTimeoutError, SocketWriteNotQueuedError, SocketWriteQueuedError, SubcSocket, writeBorrowed, writeTrackedBorrowed, } from "./socket.js";
18
18
  const debug = debuglog("subc-client");
19
19
  const DEFAULT_HANDSHAKE_TIMEOUT_MS = 10_000;
20
20
  const DEFAULT_REQUEST_TIMEOUT_MS = 30_000;
@@ -29,6 +29,7 @@ const DEFAULT_REQUEST_TIMEOUT_MS = 30_000;
29
29
  // NOT a deadline extension: an absent reply still settles right after the check
30
30
  // phase. Capped so it can never approach BODY_READ_TIMEOUT_MS.
31
31
  const TIMEOUT_ARBITRATION_GRACE_MS = 50;
32
+ const LIVENESS_PROBE_WINDOW_MS = 2000;
32
33
  // Internal marker set as the `code` on the SubcError a request-deadline timeout
33
34
  // rejects with, so the managed classifier can tell a deadline (reply may simply
34
35
  // not have been read in time) from an actual connection drop. Never surfaced to
@@ -210,12 +211,18 @@ export class SubcClient {
210
211
  // double-execute anything. The cached bind is dead (module restarted and
211
212
  // its route-gone GOODBYE raced or was missed); evict it so the retry
212
213
  // re-opens the route instead of resending into the same dead channel.
213
- if (err.code === "unknown_channel" && !retriedUnknownChannel && !this.closeStarted) {
214
+ // stale_route_epoch is the same class with a sharper cause (subconscious
215
+ // issue #39): the channel is known but its epoch was released while this
216
+ // request was in flight. The daemon's contract for the code is
217
+ // NOT-FORWARDED — dropped before delivery — so the retry is safe by
218
+ // construction, and the remedy is identical: evict, re-open, resend once.
219
+ const deadBindCode = err.code === "unknown_channel" || err.code === "stale_route_epoch";
220
+ if (deadBindCode && !retriedUnknownChannel && !this.closeStarted) {
214
221
  retriedUnknownChannel = true;
215
222
  this.evictRouteHandle(routeHandle);
216
223
  continue;
217
224
  }
218
- if (err.code === "unknown_channel" && retriedUnknownChannel) {
225
+ if (deadBindCode && retriedUnknownChannel) {
219
226
  this.evictRouteHandle(routeHandle);
220
227
  }
221
228
  if (err.kind === "not_sent") {
@@ -234,6 +241,15 @@ export class SubcClient {
234
241
  // and its other in-flight routes for nothing.
235
242
  this.scheduleReconnectAfterDrop(err);
236
243
  }
244
+ else if (err.kind === "outcome_unknown" && err.code === DEADLINE_NO_DROP_CODE) {
245
+ // Keeping the socket is only correct when the socket is actually
246
+ // alive. A HALF-OPEN socket (peer gone with no FIN/RST — the
247
+ // post-sleep/wake shape) produces the identical observable, and
248
+ // keeping it pins every future call to a corpse: this exact keep
249
+ // turned one host sleep into a whole session's tool outage. The
250
+ // probe supplies the discriminator neither timer can.
251
+ this.probeLivenessAfterDeadline();
252
+ }
237
253
  throw err;
238
254
  }
239
255
  }
@@ -246,21 +262,25 @@ export class SubcClient {
246
262
  const admission = opts.admissionClass ?? AdmissionClass.Normal;
247
263
  const corr = this.allocateCorr();
248
264
  const key = pendingKey(handle, corr);
265
+ let subscriptionPending = null;
266
+ let resolveClosed = null;
249
267
  const closed = new Promise((resolve, reject) => {
250
268
  if (this.closedErr) {
251
269
  reject(this.closedErr);
252
270
  return;
253
271
  }
254
- this.pending.set(key, {
272
+ resolveClosed = resolve;
273
+ subscriptionPending = {
255
274
  handle,
256
275
  resolve: () => resolve(),
257
276
  reject,
258
277
  onProgress: onEvent,
259
278
  timer: null,
260
279
  subscription: true,
261
- });
280
+ };
281
+ this.pending.set(key, subscriptionPending);
262
282
  const frame = buildFrame(FrameType.Request, buildFlags(false, priority, false, admission), handle.channel, handle.epoch, corr, bytes);
263
- this.sock.write(encodeFrame(frame), Date.now() + DEFAULT_REQUEST_TIMEOUT_MS).catch((err) => {
283
+ writeBorrowed(this.sock, encodeFrame(frame), Date.now() + DEFAULT_REQUEST_TIMEOUT_MS).catch((err) => {
264
284
  const pending = this.pending.get(key);
265
285
  if (pending)
266
286
  this.rejectPending(key, pending, err instanceof Error ? err : new SubcError(String(err)));
@@ -271,7 +291,10 @@ export class SubcClient {
271
291
  if (cancelled)
272
292
  return;
273
293
  cancelled = true;
274
- this.cancel(handle, corr, priority);
294
+ if (subscriptionPending && resolveClosed)
295
+ this.settle(key, subscriptionPending, resolveClosed);
296
+ if (this.isLiveHandle(handle))
297
+ this.cancel(handle, corr, priority);
275
298
  };
276
299
  return { unsubscribe, closed };
277
300
  }
@@ -279,7 +302,7 @@ export class SubcClient {
279
302
  cancel(handle, corr, priority = Priority.Interactive) {
280
303
  this.assertLiveHandle(handle);
281
304
  const cancel = buildFrame(FrameType.Cancel, buildFlags(false, priority, false), handle.channel, handle.epoch, corr, EMPTY_BODY);
282
- this.sock.write(encodeFrame(cancel), Date.now() + DEFAULT_REQUEST_TIMEOUT_MS).catch(() => undefined);
305
+ writeBorrowed(this.sock, encodeFrame(cancel), Date.now() + DEFAULT_REQUEST_TIMEOUT_MS).catch(() => undefined);
283
306
  }
284
307
  /** Poll status or liveness for exactly the supplied route generation. */
285
308
  async routePoll(handle, kind) {
@@ -360,7 +383,7 @@ export class SubcClient {
360
383
  return;
361
384
  }
362
385
  const goodbye = buildFrame(FrameType.Goodbye, buildFlags(false, Priority.Interactive, false), handle.channel, handle.epoch, 0n, EMPTY_BODY);
363
- const write = this.sock.writeTracked(encodeFrame(goodbye), Date.now() + DEFAULT_REQUEST_TIMEOUT_MS);
386
+ const write = writeTrackedBorrowed(this.sock, encodeFrame(goodbye), Date.now() + DEFAULT_REQUEST_TIMEOUT_MS);
364
387
  if (!write.queued && closeOnQueueFailure)
365
388
  this.closeConnectionAfterCleanupFailure();
366
389
  write.completed.catch(() => {
@@ -414,7 +437,7 @@ export class SubcClient {
414
437
  };
415
438
  pending.timer = setTimeout(() => this.arbitrateTimeout(key, pending, channel, corr, ms), ms);
416
439
  this.pending.set(key, pending);
417
- this.sock.write(encodeFrame(frame), Date.now() + ms).catch((error) => {
440
+ writeBorrowed(this.sock, encodeFrame(frame), Date.now() + ms).catch((error) => {
418
441
  const current = this.pending.get(key);
419
442
  if (current)
420
443
  this.rejectPending(key, current, error instanceof Error ? error : new SubcError(String(error)));
@@ -509,7 +532,7 @@ export class SubcClient {
509
532
  };
510
533
  pending.timer = setTimeout(() => this.arbitrateTimeout(key, pending, handle.channel, corr, ms), ms);
511
534
  this.pending.set(key, pending);
512
- const write = this.sock.writeTracked(encodeFrame(frame), Date.now() + ms);
535
+ const write = writeTrackedBorrowed(this.sock, encodeFrame(frame), Date.now() + ms);
513
536
  handedToSocket = write.queued;
514
537
  write.completed.catch((error) => {
515
538
  const current = this.pending.get(key);
@@ -600,6 +623,91 @@ export class SubcClient {
600
623
  }
601
624
  }
602
625
  }
626
+ // Stamped by dispatch() on every inbound frame; read by the liveness probe.
627
+ lastInboundAtMs = 0;
628
+ // Single-flight guard: overlapping deadline-no-drop settles share one probe.
629
+ livenessProbe = null;
630
+ /**
631
+ * Discriminate event-loop starvation from a half-open socket after a
632
+ * deadline-no-drop settle. The arbitration deliberately keeps the socket on
633
+ * that verdict (a late-read reply under load must not cost a healthy
634
+ * connection), but a half-open socket — peer vanished with no FIN/RST, as
635
+ * after host sleep/wake — yields the SAME verdict forever, so the keep
636
+ * becomes a permanent pin to a dead transport. The discriminator is
637
+ * socket-level liveness evidence: send a channel-0 Ping (the daemon always
638
+ * answers — subc-core control.rs) and require ANY inbound frame within the
639
+ * window; the Pong suffices, and a busy socket's other traffic proves the
640
+ * link just as well. Silence convicts: the socket is failed with a named
641
+ * cause and closed, so the ordinary drop path (pending rejection now,
642
+ * reconnect on the next call) takes over.
643
+ *
644
+ * Starvation cannot be falsely convicted by a short window: a loop starved
645
+ * enough to delay the read loop delays this probe's own timer identically,
646
+ * and when both wake the buffered Pong dispatches before the check runs.
647
+ *
648
+ * A PENDING CHANNEL-0 REQUEST SUSPENDS THE PROBE (both at launch and at the
649
+ * conviction check). The daemon's connection loop handles frames one at a
650
+ * time, and some channel-0 ops legally park it for seconds — route.open
651
+ * awaits the module's bind ack inline for up to route_bind_relay_timeout
652
+ * (~12s in production) — during which OUR Ping sits unread in the daemon's
653
+ * socket buffer. On an otherwise-quiet connection that silence is fully
654
+ * explained by our own in-flight control op, and convicting on it would
655
+ * tear down a healthy connection mid-bind (BROCA's Athena panel flagged
656
+ * this exact premise; the FIFO fact is verified in subc-core server.rs
657
+ * connection_loop + control.rs handle_route_open). The client always knows
658
+ * its own channel-0 pendings, so the gate is local and exact: no probe
659
+ * while one is in flight, no conviction if one appeared during the window.
660
+ *
661
+ * Scope: hooked from the managed-call path only. Control-plane requests
662
+ * (route.open, catalog.list) are short-lived and their failures already
663
+ * escalate through reconnect classification; the managed path is where a
664
+ * plugin lives for hours and where the pin was observed in production.
665
+ */
666
+ /** Any in-flight channel-0 request (pendings keyed without a route handle). */
667
+ hasControlPending() {
668
+ for (const pending of this.pending.values()) {
669
+ if (pending.handle === null)
670
+ return true;
671
+ }
672
+ return false;
673
+ }
674
+ probeLivenessAfterDeadline() {
675
+ if (this.livenessProbe || this.closeStarted || this.closedErr)
676
+ return;
677
+ if (this.hasControlPending())
678
+ return; // silence would be self-explained; re-armed by the next settle
679
+ const sock = this.sock;
680
+ const generation = this.generation;
681
+ let corr;
682
+ try {
683
+ corr = this.allocateCorr();
684
+ }
685
+ catch {
686
+ return; // allocator exhausted: the next reconnect resets it
687
+ }
688
+ const t0 = Date.now();
689
+ const ping = buildFrame(FrameType.Ping, buildFlags(false, Priority.Interactive, false, AdmissionClass.Normal), 0, 0, corr, new Uint8Array());
690
+ const probe = (async () => {
691
+ // A failed WRITE is not exonerating: swallow it and let the window
692
+ // check run — a socket that cannot carry a Ping will not produce an
693
+ // inbound frame either, and conviction is the right verdict.
694
+ await writeBorrowed(sock, encodeFrame(ping), t0 + this.opts.livenessProbeWindowMs).catch(() => { });
695
+ await this.opts.sleep(this.opts.livenessProbeWindowMs);
696
+ if (this.sock !== sock || this.generation !== generation || this.closeStarted)
697
+ return;
698
+ if (this.lastInboundAtMs >= t0)
699
+ return; // link proven — keep the socket
700
+ // A control op that started during the window explains the silence: the
701
+ // daemon's read loop may be parked in its inline handler, not dead.
702
+ if (this.hasControlPending())
703
+ return;
704
+ this.fail(new SocketClosedError(`liveness probe convicted a half-open socket: no inbound frame for ${this.opts.livenessProbeWindowMs}ms after a channel-0 Ping (deadline-no-drop settles preceded this); closing so the next call reconnects`));
705
+ sock.close();
706
+ })().finally(() => {
707
+ this.livenessProbe = null;
708
+ });
709
+ this.livenessProbe = probe;
710
+ }
603
711
  async ensureConnectedForManaged() {
604
712
  if (this.closeStarted)
605
713
  throw new SubcError("client closed");
@@ -729,6 +837,40 @@ export class SubcClient {
729
837
  }
730
838
  }
731
839
  dispatch(frame) {
840
+ // Every dispatched frame proves the socket delivers inbound bytes; the
841
+ // post-deadline liveness probe reads this stamp. Stamped after readLoop's
842
+ // generation gate, so a stale socket's late frames never vouch for the
843
+ // live one.
844
+ this.lastInboundAtMs = Date.now();
845
+ if (frame.header.channel === 0 && frame.header.ty === FrameType.Push) {
846
+ // Daemon-originated control push (route.closing / route.closed / future
847
+ // ops). Never matches a pending (corr is daemon-chosen), never an error
848
+ // path: unparseable bodies and absent observers both drop silently per
849
+ // the wire contract's MUST-ignore clause, and an observer throw is
850
+ // swallowed for the same reason as onProgress below -- a caller callback
851
+ // must not fail the read loop.
852
+ const observer = this.opts.onControlPush;
853
+ if (observer) {
854
+ let parsed = null;
855
+ try {
856
+ const body = this.parseJson(frame);
857
+ if (body && typeof body.op === "string")
858
+ parsed = { op: body.op, body };
859
+ }
860
+ catch {
861
+ // Unparseable control push: ignored by contract.
862
+ }
863
+ if (parsed) {
864
+ try {
865
+ observer(parsed);
866
+ }
867
+ catch {
868
+ // Observer's own throw, on its own stack; the stream must survive.
869
+ }
870
+ }
871
+ }
872
+ return;
873
+ }
732
874
  let handle = null;
733
875
  if (frame.header.channel !== 0) {
734
876
  handle = this.liveRoutes.get(frame.header.channel) ?? null;
@@ -745,7 +887,29 @@ export class SubcClient {
745
887
  switch (frame.header.ty) {
746
888
  case FrameType.Push:
747
889
  case FrameType.StreamData:
748
- pending.onProgress?.(frame.body);
890
+ // A THROW FROM CALLER CODE MUST NOT REACH THE READ LOOP.
891
+ //
892
+ // `onProgress` (and `onEvent`, which is the same slot) is supplied by
893
+ // the caller and runs INSIDE readLoop's frame dispatch. An escaping
894
+ // throw unwinds into readLoop's catch, which treats it as a socket
895
+ // failure: it calls fail(), rejecting EVERY in-flight request on this
896
+ // connection -- other routes included -- with the caller's own error as
897
+ // the reported cause, and stops reading. One consumer's bad event
898
+ // handler therefore drops the whole connection, and the surfaced error
899
+ // names the handler rather than the transport, so the failure reads as
900
+ // a subc fault.
901
+ //
902
+ // Swallowing is deliberate and is NOT hiding a fault from the caller:
903
+ // the throw happened in their own callback, on their own stack, where
904
+ // they are the party able to catch it. What the client owes them is
905
+ // that the stream and every sibling route keep working.
906
+ try {
907
+ pending.onProgress?.(frame.body);
908
+ }
909
+ catch {
910
+ // Swallow the caller's own handler error so one bad callback cannot
911
+ // abort the read loop or fail unrelated requests. See above.
912
+ }
749
913
  return;
750
914
  case FrameType.Response:
751
915
  case FrameType.StreamEnd:
@@ -765,7 +929,17 @@ export class SubcClient {
765
929
  return;
766
930
  }
767
931
  if (frame.header.ty === FrameType.Goodbye && handle) {
768
- this.failHandle(handle, new SubcError("route closed by subc (GOODBYE)"));
932
+ // Carries `route_closed` like the other two route-close sites (closeRoute,
933
+ // late route.open). It was the only one without a code, so a consumer
934
+ // branching on `route_closed` to recognise a gone route saw this one as an
935
+ // uncoded generic failure and fell through to whatever its default was.
936
+ //
937
+ // THE CODE MUST NOT MAKE THIS RETRYABLE. A GOODBYE arriving mid-request
938
+ // means the request was already forwarded and the module may have run it,
939
+ // so it stays kind=outcome_unknown -- the same class as a mid-flight socket
940
+ // drop, and never the not_sent/unknown_channel class that call() retries.
941
+ // The code says WHICH route ended, not that it is safe to send again.
942
+ this.failHandle(handle, new SubcError("route closed by subc (GOODBYE)", "route_closed"));
769
943
  if (this.liveRoutes.get(handle.channel) === handle)
770
944
  this.liveRoutes.delete(handle.channel);
771
945
  this.evictRouteHandle(handle);
@@ -885,10 +1059,11 @@ export class SubcClient {
885
1059
  this.scheduleReconnectAfterDrop(error);
886
1060
  }
887
1061
  encode(value) {
888
- return new Uint8Array(Buffer.from(JSON.stringify(value), "utf8"));
1062
+ return Buffer.from(JSON.stringify(value), "utf8");
889
1063
  }
890
1064
  parseJson(frame) {
891
- return JSON.parse(Buffer.from(frame.body).toString("utf8"));
1065
+ const b = frame.body;
1066
+ return JSON.parse(Buffer.from(b.buffer, b.byteOffset, b.byteLength).toString("utf8"));
892
1067
  }
893
1068
  }
894
1069
  export function isConsumerReconnectTransient(err) {
@@ -929,6 +1104,7 @@ export function isConsumerReconnectTransient(err) {
929
1104
  export function isRetryableRouteOpenCode(code) {
930
1105
  return (code === "unknown_module" ||
931
1106
  code === "module_reloading" ||
1107
+ code === "module_warming" ||
932
1108
  code === "target_unavailable" ||
933
1109
  code === "module_timeout");
934
1110
  }
@@ -950,6 +1126,8 @@ function normalizeConnectOptions(opts) {
950
1126
  reconnectBackoff: opts.reconnectBackoff ?? DEFAULT_RECONNECT_BACKOFF,
951
1127
  sleep: opts.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms))),
952
1128
  timeoutArbitrationGraceMs: opts.timeoutArbitrationGraceMs ?? TIMEOUT_ARBITRATION_GRACE_MS,
1129
+ livenessProbeWindowMs: opts.livenessProbeWindowMs ?? LIVENESS_PROBE_WINDOW_MS,
1130
+ onControlPush: opts.onControlPush,
953
1131
  };
954
1132
  }
955
1133
  function routeCacheKey(target, identity, consumerIdentity) {