@ai-matrx/desktop-protocol 0.5.0 → 0.7.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,30 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.7.0
4
+
5
+ - `CLOSE_RULES` / `closeRule(code)` / `isTerminalClose(code)` (`src/close-policy.ts`): the one table
6
+ of what a dialer does after each close — `reconnect_now`, `reconnect`, `reauth` (one retry with a
7
+ fresh token; a second refusal before any welcome is terminal, outcome `signed_out`), or `stop`
8
+ with an `outcome` (`not_owned` 4003, `removed` 4004, `replaced` 4009, `revoked` 4010,
9
+ `upgrade_required` 4426). A CloseCode without a rule fails to compile; unknown codes reconnect.
10
+ - The client reads that table (its private terminal list is gone) and reports
11
+ `DesktopClientState.outcome` when it stops; 4001/4008 no longer redial forever.
12
+
13
+ Consumer action: never keep a local list of terminal close codes; branch on `closeRule(code)` and
14
+ show `state.outcome`.
15
+
16
+ ## 0.6.0
17
+
18
+ - `RelayDeviceStatusEvent.since_ms` is `null` for a device that has never connected (protocol 1.1;
19
+ `NULL_SINCE`): a 1.0 reader is sent `0`, its old meaning (`statusForPeer`).
20
+ - A WebSocket client may announce its minor at connect by also offering
21
+ `versionSubprotocol(PROTOCOL_VERSION)` (e.g. `matrx.v1.1`) beside `SUBPROTOCOL`;
22
+ `announcedVersion(offered)` reads it (servers still answer `SUBPROTOCOL`).
23
+ - The versioning test also refuses a 1.0 field changing type, except a widening to null listed in
24
+ `NULL_SINCE`.
25
+
26
+ Consumer action: treat `since_ms: null` as "never connected"; offer the version subprotocol.
27
+
3
28
  ## 0.5.0
4
29
 
5
30
  Version skew is the normal state, so a minor protocol addition must never make an older peer
package/dist/client.cjs CHANGED
@@ -311,6 +311,29 @@ var CloseCode = {
311
311
  };
312
312
  var CloseCodeValue = import_zod3.z.literal([1e3, 1001, 1006, 1012, 1013, 4001, 4003, 4004, 4008, 4009, 4010, 4400, 4426]);
313
313
 
314
+ // src/close-policy.ts
315
+ var CLOSE_RULES = {
316
+ [CloseCode.NORMAL]: { action: "reconnect", error: ["DEVICE_OFFLINE", "Connection closed"] },
317
+ [CloseCode.GOING_AWAY]: { action: "reconnect_now", error: ["DEVICE_OFFLINE", "Connection lost"] },
318
+ [CloseCode.ABNORMAL]: { action: "reconnect_now", error: ["DEVICE_OFFLINE", "Connection lost"] },
319
+ [CloseCode.DEVICE_RECONNECTED]: { action: "reconnect_now", error: ["DEVICE_OFFLINE", "Device reconnected"] },
320
+ [CloseCode.TRY_AGAIN_LATER]: { action: "reconnect", error: ["DEVICE_OFFLINE", "Relay busy; retrying"] },
321
+ [CloseCode.AUTH_INVALID]: { action: "reauth", outcome: "signed_out", error: ["AUTH_INVALID", "Sign-in was not accepted"] },
322
+ [CloseCode.AUTH_FORBIDDEN]: { action: "stop", outcome: "not_owned", error: ["AUTH_FORBIDDEN", "Not this device's owner"] },
323
+ [CloseCode.DEVICE_NOT_FOUND]: { action: "stop", outcome: "removed", error: ["NOT_FOUND", "Device not found"] },
324
+ [CloseCode.AUTH_EXPIRED]: { action: "reauth", outcome: "signed_out", error: ["AUTH_EXPIRED", "Sign-in expired"] },
325
+ [CloseCode.REPLACED]: { action: "stop", outcome: "replaced", error: ["CONFLICT", "Replaced by a newer connection"] },
326
+ [CloseCode.DEVICE_REVOKED]: { action: "stop", outcome: "revoked", error: ["AUTH_DEVICE_REVOKED", "This device was revoked"] },
327
+ // A malformed message, or a connection the relay lost track of: a fresh socket starts clean.
328
+ [CloseCode.PROTOCOL_ERROR]: { action: "reconnect", error: ["PROTOCOL_ERROR", "Connection reset by the relay"] },
329
+ [CloseCode.UPGRADE_REQUIRED]: { action: "stop", outcome: "upgrade_required", error: ["PROTOCOL_VERSION_UNSUPPORTED", "Protocol upgrade required"] }
330
+ };
331
+ var UNKNOWN = { action: "reconnect", error: ["DEVICE_OFFLINE", "Connection lost"] };
332
+ function closeRule(code) {
333
+ if (code === null || code === void 0) return CLOSE_RULES[CloseCode.ABNORMAL];
334
+ return CLOSE_RULES[code] ?? UNKNOWN;
335
+ }
336
+
314
337
  // src/schema/frame.ts
315
338
  var import_zod4 = require("zod");
316
339
  var FrameHeader = import_zod4.z.strictObject({
@@ -646,7 +669,11 @@ var SessionControlChangedEvent = import_zod8.z.strictObject({ controller_client_
646
669
  var import_zod9 = require("zod");
647
670
  var RelayDeviceStatusEvent = import_zod9.z.strictObject({
648
671
  online: import_zod9.z.boolean(),
649
- since_ms: epochMs,
672
+ /**
673
+ * When the device last went online/offline; null = it has never connected (since 1.1 —
674
+ * NULL_SINCE: a 1.0 reader is sent 0, its old meaning).
675
+ */
676
+ since_ms: epochMs.nullable(),
650
677
  app_version: import_zod9.z.string().max(32).nullable(),
651
678
  protocol_version: ProtocolVersion.nullable()
652
679
  });
@@ -728,13 +755,6 @@ var EVENTS = {
728
755
 
729
756
  // src/client/client.ts
730
757
  var WS_OPEN = 1;
731
- var TERMINAL_CLOSE = /* @__PURE__ */ new Map([
732
- [CloseCode.DEVICE_REVOKED, ["AUTH_DEVICE_REVOKED", "This device was revoked"]],
733
- [CloseCode.AUTH_FORBIDDEN, ["AUTH_FORBIDDEN", "Not this device's owner"]],
734
- [CloseCode.DEVICE_NOT_FOUND, ["NOT_FOUND", "Device not found"]],
735
- [CloseCode.REPLACED, ["CONFLICT", "Replaced by a newer connection"]],
736
- [CloseCode.UPGRADE_REQUIRED, ["PROTOCOL_VERSION_UNSUPPORTED", "Protocol upgrade required"]]
737
- ]);
738
758
  var defaultTimers = {
739
759
  setTimeout: (fn, ms) => globalThis.setTimeout(fn, ms),
740
760
  clearTimeout: (handle) => globalThis.clearTimeout(handle),
@@ -777,8 +797,10 @@ function createDesktopClient(options) {
777
797
  attempts: 0,
778
798
  retryInMs: null,
779
799
  lastError: null,
780
- lastCloseCode: null
800
+ lastCloseCode: null,
801
+ outcome: null
781
802
  };
803
+ let authRefusals = 0;
782
804
  const listeners = /* @__PURE__ */ new Set();
783
805
  const readyWaiters = /* @__PURE__ */ new Set();
784
806
  const pending = /* @__PURE__ */ new Map();
@@ -813,6 +835,7 @@ function createDesktopClient(options) {
813
835
  }
814
836
  function connect() {
815
837
  if (state.status === "connecting" || state.status === "handshaking" || state.status === "open") return;
838
+ if (state.status === "closed" || state.status === "idle") authRefusals = 0;
816
839
  closedByUser = false;
817
840
  if (reconnectTimer !== null) {
818
841
  timers.clearTimeout(reconnectTimer);
@@ -894,8 +917,11 @@ function createDesktopClient(options) {
894
917
  timers.clearTimeout(probeTimer);
895
918
  probeTimer = null;
896
919
  }
897
- const terminal = code === null ? void 0 : TERMINAL_CLOSE.get(code);
898
- const lost = error ?? (terminal ? new DesktopProtocolError(terminal[0], terminal[1]) : new DesktopProtocolError("DEVICE_OFFLINE", "Connection lost", { retryable: true }));
920
+ const rule = code === null ? null : closeRule(code);
921
+ if (rule?.action === "reauth") authRefusals++;
922
+ const stopped = rule !== null && (rule.action === "stop" || rule.action === "reauth" && authRefusals >= 2);
923
+ const terminal = stopped && rule ? rule : void 0;
924
+ const lost = error ?? (terminal ? new DesktopProtocolError(terminal.error[0], terminal.error[1]) : rule && rule.action === "reauth" ? new DesktopProtocolError(rule.error[0], rule.error[1], { retryable: true }) : new DesktopProtocolError("DEVICE_OFFLINE", "Connection lost", { retryable: true }));
899
925
  for (const [id, p] of pending) {
900
926
  pending.delete(id);
901
927
  p.cleanup();
@@ -912,7 +938,14 @@ function createDesktopClient(options) {
912
938
  endStream(s, null, lost);
913
939
  }
914
940
  if (closedByUser || terminal) {
915
- setState({ status: "closed", welcome: null, lastError: closedByUser ? state.lastError : lost, lastCloseCode: code, retryInMs: null });
941
+ setState({
942
+ status: "closed",
943
+ welcome: null,
944
+ lastError: closedByUser ? state.lastError : lost,
945
+ lastCloseCode: code,
946
+ retryInMs: null,
947
+ outcome: closedByUser ? null : terminal?.outcome ?? null
948
+ });
916
949
  rejectReadyWaiters(lost);
917
950
  return;
918
951
  }
@@ -1066,7 +1099,8 @@ function createDesktopClient(options) {
1066
1099
  }
1067
1100
  helloId = null;
1068
1101
  backoff.markConnected();
1069
- setState({ status: "open", welcome: welcome.data, attempts: 0, retryInMs: null, lastError: null });
1102
+ authRefusals = 0;
1103
+ setState({ status: "open", welcome: welcome.data, attempts: 0, retryInMs: null, lastError: null, outcome: null });
1070
1104
  for (const w of [...readyWaiters]) {
1071
1105
  readyWaiters.delete(w);
1072
1106
  w.resolve(welcome.data);