@ai-matrx/desktop-protocol 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.0
4
+
5
+ The phone console (lane E) needs a client that comes back the instant the person does.
6
+
7
+ - `client.wake({ probeMs })` — reconnecting: dial now instead of waiting out the backoff; open:
8
+ send a ping and, if nothing is heard within `probeMs` (default 4000), treat the socket as dead
9
+ and redial at once. A phone resumed from sleep often holds a socket the OS already killed.
10
+ - `bindDesktopWake(client)` (`./client`) and `useDesktopWake()` (`./react`) call `wake()` on
11
+ `visibilitychange` → visible, `pageshow`, `online` and `focus`.
12
+ - A caller-started `session.attach` (a page reload reattaching by the resource id it kept in the
13
+ URL) is now a resource stream from its first byte: `resourceId` is set from the params,
14
+ `lastSeq` starts at `since_seq` (no false `seq_gap` on the first replayed frame), and the stream
15
+ reattaches after a later drop like the stream that created the resource.
16
+
17
+ Consumer action: none required. Phone and browser consoles should call `useDesktopWake()` (or
18
+ `bindDesktopWake`) once per client.
19
+
3
20
  ## 0.1.1
4
21
 
5
22
  Automatic changed-only republish (docs/metadata drift since the last tag — see
package/README.md CHANGED
@@ -14,7 +14,7 @@ pnpm add @ai-matrx/desktop-protocol
14
14
  | `@ai-matrx/desktop-protocol` | Schemas for the envelope and every `cap.op`, `OPS` / `EVENTS` registries, `ErrorCode`, close codes, `DesktopProtocolError`. Every schema name is also its inferred type. |
15
15
  | `@ai-matrx/desktop-protocol/frame` | The 16-byte binary frame codec. Zod-free (the relay Worker's hot path). |
16
16
  | `@ai-matrx/desktop-protocol/client` | `createDesktopClient` — reconnecting client with typed requests, credit-gated streams, transparent terminal reattach, heartbeat and relay reauth. |
17
- | `@ai-matrx/desktop-protocol/react` | `DesktopClientProvider`, `useDesktopConnection`, `useDesktopRequest`, `useDesktopEvent`. |
17
+ | `@ai-matrx/desktop-protocol/react` | `DesktopClientProvider`, `useDesktopConnection`, `useDesktopRequest`, `useDesktopEvent`, `useDesktopWake`. |
18
18
 
19
19
  ## Client
20
20
 
@@ -54,6 +54,12 @@ term.write("ls\n");
54
54
  `@ai-matrx/realtime` backoff (stability reset + jitter) — including `1006` (a relay deploy drops
55
55
  every socket without a close frame) and `1012` (the relay's "the device reconnected": hello and
56
56
  reattach against the new core session).
57
+ - **Coming back:** `client.wake()` (or `bindDesktopWake(client)` / `useDesktopWake()`, which call it
58
+ on `visibilitychange`, `pageshow`, `online` and `focus`) dials at once while reconnecting and
59
+ probes an open socket with a ping, redialing if nothing answers within 4 s.
60
+ - **Reload reattach:** keep `stream.resourceId` and `stream.lastSeq` (e.g. in the URL); after a reload
61
+ `client.stream("session.attach", { resource_id, since_seq })` replays the gap and then behaves
62
+ exactly like the original stream, including surviving later drops.
57
63
  - Background problems (malformed messages, sequence gaps, heartbeat death, reconnect alarm) go to
58
64
  `onDiagnostic` — loud by default (`console.warn`).
59
65
 
package/dist/client.cjs CHANGED
@@ -22,6 +22,7 @@ var client_exports = {};
22
22
  __export(client_exports, {
23
23
  DesktopProtocolError: () => DesktopProtocolError,
24
24
  RECONNECT_ALARM_ATTEMPTS: () => import_realtime.RECONNECT_ALARM_ATTEMPTS,
25
+ bindDesktopWake: () => bindDesktopWake,
25
26
  createDesktopClient: () => createDesktopClient,
26
27
  isDesktopProtocolError: () => isDesktopProtocolError
27
28
  });
@@ -761,7 +762,9 @@ function createDesktopClient(options) {
761
762
  let helloId = null;
762
763
  let reconnectTimer = null;
763
764
  let heartbeatTimer = null;
765
+ let probeTimer = null;
764
766
  let lastHeardAt = 0;
767
+ let heardCount = 0;
765
768
  let closedByUser = false;
766
769
  const newId = (prefix) => `${prefix}${(++nextId).toString(36)}`;
767
770
  function setState(patch) {
@@ -816,6 +819,7 @@ function createDesktopClient(options) {
816
819
  ws.onmessage = (event) => {
817
820
  if (generation !== socketGeneration) return;
818
821
  lastHeardAt = timers.now();
822
+ heardCount++;
819
823
  onMessage(event.data);
820
824
  };
821
825
  ws.onerror = () => {
@@ -857,6 +861,10 @@ function createDesktopClient(options) {
857
861
  timers.clearTimeout(heartbeatTimer);
858
862
  heartbeatTimer = null;
859
863
  }
864
+ if (probeTimer !== null) {
865
+ timers.clearTimeout(probeTimer);
866
+ probeTimer = null;
867
+ }
860
868
  const terminal = code === null ? void 0 : TERMINAL_CLOSE.get(code);
861
869
  const lost = error ?? (terminal ? new DesktopProtocolError(terminal[0], terminal[1]) : new DesktopProtocolError("DEVICE_OFFLINE", "Connection lost", { retryable: true }));
862
870
  for (const [id, p] of pending) {
@@ -1233,15 +1241,16 @@ function createDesktopClient(options) {
1233
1241
  });
1234
1242
  result.catch(() => void 0);
1235
1243
  done.catch(() => void 0);
1244
+ const attach = op === "session.attach" ? params : null;
1236
1245
  const s = {
1237
1246
  op,
1238
1247
  handlers,
1239
1248
  options: opts,
1240
1249
  id: newId("s"),
1241
1250
  sid: null,
1242
- resourceId: null,
1243
- lastSeq: 0,
1244
- expectSeq: 1,
1251
+ resourceId: attach ? attach.resource_id : null,
1252
+ lastSeq: attach?.since_seq ?? 0,
1253
+ expectSeq: attach ? attach.since_seq === void 0 ? null : attach.since_seq + 1 : 1,
1245
1254
  windowBytes: opts.windowBytes ?? DEFAULT_WINDOW_BYTES,
1246
1255
  consumedUncredited: 0,
1247
1256
  sendWindow: DEFAULT_WINDOW_BYTES,
@@ -1330,6 +1339,34 @@ function createDesktopClient(options) {
1330
1339
  function reauth(token) {
1331
1340
  sendJson({ type: "relay", op: "reauth", token });
1332
1341
  }
1342
+ function wake(opts = {}) {
1343
+ if (closedByUser || state.status === "closed") return;
1344
+ if (state.status === "idle" || state.status === "reconnecting") {
1345
+ connect();
1346
+ return;
1347
+ }
1348
+ if (state.status === "connecting" || !socket || probeTimer !== null) return;
1349
+ const generation = socketGeneration;
1350
+ const heardBefore = heardCount;
1351
+ try {
1352
+ socket.send(PING_TEXT);
1353
+ } catch {
1354
+ }
1355
+ probeTimer = timers.setTimeout(() => {
1356
+ probeTimer = null;
1357
+ if (generation !== socketGeneration || !socket) return;
1358
+ if (heardCount > heardBefore) return;
1359
+ const silentMs = timers.now() - lastHeardAt;
1360
+ diagnose({ kind: "heartbeat_timeout", silentMs });
1361
+ const dead = socket;
1362
+ onSocketGone(generation, null, new DesktopProtocolError("TIMEOUT", "Connection went silent", { retryable: true }));
1363
+ try {
1364
+ dead.close(CloseCode.NORMAL, "wake probe timeout");
1365
+ } catch {
1366
+ }
1367
+ if (state.status === "reconnecting") connect();
1368
+ }, opts.probeMs ?? 4e3);
1369
+ }
1333
1370
  return {
1334
1371
  connect,
1335
1372
  close,
@@ -1344,7 +1381,29 @@ function createDesktopClient(options) {
1344
1381
  request,
1345
1382
  stream,
1346
1383
  on,
1347
- reauth
1384
+ reauth,
1385
+ wake
1386
+ };
1387
+ }
1388
+
1389
+ // src/client/wake.ts
1390
+ function bindDesktopWake(client, options = {}) {
1391
+ const targets = options.targets ?? (typeof window !== "undefined" && typeof document !== "undefined" ? { window, document } : null);
1392
+ if (!targets) return () => void 0;
1393
+ const wakeOptions = options.probeMs === void 0 ? {} : { probeMs: options.probeMs };
1394
+ const onVisibility = () => {
1395
+ if (targets.document.visibilityState === "visible") client.wake(wakeOptions);
1396
+ };
1397
+ const onWake = () => client.wake(wakeOptions);
1398
+ targets.document.addEventListener("visibilitychange", onVisibility);
1399
+ targets.window.addEventListener("pageshow", onWake);
1400
+ targets.window.addEventListener("online", onWake);
1401
+ targets.window.addEventListener("focus", onWake);
1402
+ return () => {
1403
+ targets.document.removeEventListener("visibilitychange", onVisibility);
1404
+ targets.window.removeEventListener("pageshow", onWake);
1405
+ targets.window.removeEventListener("online", onWake);
1406
+ targets.window.removeEventListener("focus", onWake);
1348
1407
  };
1349
1408
  }
1350
1409
  //# sourceMappingURL=client.cjs.map