@ai-matrx/desktop-protocol 0.1.1 → 0.3.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,42 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.0
4
+
5
+ SPEC 6.1 sandbox parity: every search and list op now answers exactly what the cloud sandbox
6
+ daemon answers (fd 8.3.1 / ripgrep 13 run in `cwd`), verified against answers recorded from a live
7
+ sandbox (matrx-desktop `test/fixtures/sandbox-daemon/observed.json`).
8
+
9
+ - `fs.search_content`: `results[].path` is RELATIVE to `cwd` (no `./`), `submatches[].match` is
10
+ ripgrep's `{ text }`, and there is no `truncated` (`max_results` caps matching lines per file,
11
+ as `rg -m`).
12
+ - `fs.search_paths`: `paths` are RELATIVE to `cwd` and prefixed `./` (fd), sorted, cut to
13
+ `max_results`; no `truncated`.
14
+ - `FsEntry` (`fs.stat`, `fs.list` entries) is the daemon's `get_stat_dict` field for field:
15
+ `mtime_ms` and `hidden` are gone (take a write guard from `fs.read`'s `mtime_ms`).
16
+ - `fs.list` defaults are the daemon's behaviour: `limit` 1000, dotfiles listed (`show_hidden`
17
+ true), siblings in byte order of name (`sort` "name"). Page tokens are single-use.
18
+
19
+ Consumer action: join a search path to `cwd` yourself when you need an absolute one; read
20
+ `match.text` instead of `match`; drop any use of `truncated` on search results and of `mtime_ms` /
21
+ `hidden` on entries.
22
+
23
+ ## 0.2.0
24
+
25
+ The phone console (lane E) needs a client that comes back the instant the person does.
26
+
27
+ - `client.wake({ probeMs })` — reconnecting: dial now instead of waiting out the backoff; open:
28
+ send a ping and, if nothing is heard within `probeMs` (default 4000), treat the socket as dead
29
+ and redial at once. A phone resumed from sleep often holds a socket the OS already killed.
30
+ - `bindDesktopWake(client)` (`./client`) and `useDesktopWake()` (`./react`) call `wake()` on
31
+ `visibilitychange` → visible, `pageshow`, `online` and `focus`.
32
+ - A caller-started `session.attach` (a page reload reattaching by the resource id it kept in the
33
+ URL) is now a resource stream from its first byte: `resourceId` is set from the params,
34
+ `lastSeq` starts at `since_seq` (no false `seq_gap` on the first replayed frame), and the stream
35
+ reattaches after a later drop like the stream that created the resource.
36
+
37
+ Consumer action: none required. Phone and browser consoles should call `useDesktopWake()` (or
38
+ `bindDesktopWake`) once per client.
39
+
3
40
  ## 0.1.1
4
41
 
5
42
  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
  });
@@ -124,6 +125,7 @@ var import_zod2 = require("zod");
124
125
  var import_zod = require("zod");
125
126
  var PROTOCOL_MAJOR = 1;
126
127
  var PROTOCOL_MINOR = 0;
128
+ var PROTOCOL_VERSION = `${PROTOCOL_MAJOR}.${PROTOCOL_MINOR}`;
127
129
  var ProtocolVersion = import_zod.z.string().regex(/^[1-9][0-9]{0,2}\.[0-9]{1,3}$/);
128
130
  var RequestId = import_zod.z.string().min(1).max(64).regex(/^[A-Za-z0-9_-]+$/);
129
131
  var ResourceId = import_zod.z.string().regex(/^(pty|watch)_[A-Za-z0-9]{16,40}$/);
@@ -395,12 +397,9 @@ var FsEntry = import_zod6.z.strictObject({
395
397
  kind: FsKind,
396
398
  size: nonNegInt,
397
399
  mtime: import_zod6.z.number().min(0),
398
- // seconds, float — daemon parity
399
- mtime_ms: epochMs,
400
+ // seconds, float
400
401
  mode: import_zod6.z.int().min(0).max(4095),
401
- target: AbsPath.nullable(),
402
- // symlink target, never followed
403
- hidden: import_zod6.z.boolean()
402
+ target: AbsPath.nullable()
404
403
  });
405
404
  var FsListParams = import_zod6.z.strictObject({
406
405
  path: AbsPath,
@@ -408,10 +407,12 @@ var FsListParams = import_zod6.z.strictObject({
408
407
  depth: import_zod6.z.int().min(1).max(32).default(1),
409
408
  pattern: import_zod6.z.string().max(256).optional(),
410
409
  // glob on name
411
- limit: import_zod6.z.int().min(1).max(5e3).default(500),
410
+ limit: import_zod6.z.int().min(1).max(5e3).default(1e3),
412
411
  page_token: import_zod6.z.string().max(256).optional(),
413
- show_hidden: import_zod6.z.boolean().default(false),
414
- sort: import_zod6.z.enum(["name", "mtime", "size", "kind"]).default("kind")
412
+ // Desktop additions; their defaults ARE the daemon's behaviour: dotfiles listed, siblings in
413
+ // binary (byte) order of name, depth-first.
414
+ show_hidden: import_zod6.z.boolean().default(true),
415
+ sort: import_zod6.z.enum(["name", "mtime", "size", "kind"]).default("name")
415
416
  });
416
417
  var FsListResult = import_zod6.z.strictObject({
417
418
  entries: import_zod6.z.array(FsEntry),
@@ -495,7 +496,7 @@ var FsSearchPathsParams = import_zod6.z.strictObject({
495
496
  hidden: import_zod6.z.boolean().default(false),
496
497
  max_results: import_zod6.z.int().min(1).max(5e3).default(100)
497
498
  });
498
- var FsSearchPathsResult = import_zod6.z.strictObject({ paths: import_zod6.z.array(AbsPath), truncated: import_zod6.z.boolean() });
499
+ var FsSearchPathsResult = import_zod6.z.strictObject({ paths: import_zod6.z.array(import_zod6.z.string().min(3).max(4096).regex(/^\.\/[^\\\u0000]+$/)) });
499
500
  var FsSearchContentParams = import_zod6.z.strictObject({
500
501
  query: import_zod6.z.string().min(1).max(1024),
501
502
  cwd: AbsPath,
@@ -508,13 +509,14 @@ var FsSearchContentParams = import_zod6.z.strictObject({
508
509
  var FsSearchContentResult = import_zod6.z.strictObject({
509
510
  results: import_zod6.z.array(
510
511
  import_zod6.z.strictObject({
511
- path: AbsPath,
512
+ path: import_zod6.z.string().min(1).max(4096).regex(/^[^/\\\u0000][^\\\u0000]*$/),
512
513
  lines: import_zod6.z.string(),
513
514
  line_number: import_zod6.z.int().min(1),
514
- submatches: import_zod6.z.array(import_zod6.z.strictObject({ match: import_zod6.z.string(), start: nonNegInt, end: nonNegInt }))
515
+ submatches: import_zod6.z.array(
516
+ import_zod6.z.strictObject({ match: import_zod6.z.strictObject({ text: import_zod6.z.string() }), start: nonNegInt, end: nonNegInt })
517
+ )
515
518
  })
516
- ),
517
- truncated: import_zod6.z.boolean()
519
+ )
518
520
  });
519
521
  var FsChange = import_zod6.z.strictObject({
520
522
  type: import_zod6.z.enum(["created", "modified", "deleted", "moved"]),
@@ -761,7 +763,9 @@ function createDesktopClient(options) {
761
763
  let helloId = null;
762
764
  let reconnectTimer = null;
763
765
  let heartbeatTimer = null;
766
+ let probeTimer = null;
764
767
  let lastHeardAt = 0;
768
+ let heardCount = 0;
765
769
  let closedByUser = false;
766
770
  const newId = (prefix) => `${prefix}${(++nextId).toString(36)}`;
767
771
  function setState(patch) {
@@ -816,6 +820,7 @@ function createDesktopClient(options) {
816
820
  ws.onmessage = (event) => {
817
821
  if (generation !== socketGeneration) return;
818
822
  lastHeardAt = timers.now();
823
+ heardCount++;
819
824
  onMessage(event.data);
820
825
  };
821
826
  ws.onerror = () => {
@@ -857,6 +862,10 @@ function createDesktopClient(options) {
857
862
  timers.clearTimeout(heartbeatTimer);
858
863
  heartbeatTimer = null;
859
864
  }
865
+ if (probeTimer !== null) {
866
+ timers.clearTimeout(probeTimer);
867
+ probeTimer = null;
868
+ }
860
869
  const terminal = code === null ? void 0 : TERMINAL_CLOSE.get(code);
861
870
  const lost = error ?? (terminal ? new DesktopProtocolError(terminal[0], terminal[1]) : new DesktopProtocolError("DEVICE_OFFLINE", "Connection lost", { retryable: true }));
862
871
  for (const [id, p] of pending) {
@@ -1233,15 +1242,16 @@ function createDesktopClient(options) {
1233
1242
  });
1234
1243
  result.catch(() => void 0);
1235
1244
  done.catch(() => void 0);
1245
+ const attach = op === "session.attach" ? params : null;
1236
1246
  const s = {
1237
1247
  op,
1238
1248
  handlers,
1239
1249
  options: opts,
1240
1250
  id: newId("s"),
1241
1251
  sid: null,
1242
- resourceId: null,
1243
- lastSeq: 0,
1244
- expectSeq: 1,
1252
+ resourceId: attach ? attach.resource_id : null,
1253
+ lastSeq: attach?.since_seq ?? 0,
1254
+ expectSeq: attach ? attach.since_seq === void 0 ? null : attach.since_seq + 1 : 1,
1245
1255
  windowBytes: opts.windowBytes ?? DEFAULT_WINDOW_BYTES,
1246
1256
  consumedUncredited: 0,
1247
1257
  sendWindow: DEFAULT_WINDOW_BYTES,
@@ -1330,6 +1340,34 @@ function createDesktopClient(options) {
1330
1340
  function reauth(token) {
1331
1341
  sendJson({ type: "relay", op: "reauth", token });
1332
1342
  }
1343
+ function wake(opts = {}) {
1344
+ if (closedByUser || state.status === "closed") return;
1345
+ if (state.status === "idle" || state.status === "reconnecting") {
1346
+ connect();
1347
+ return;
1348
+ }
1349
+ if (state.status === "connecting" || !socket || probeTimer !== null) return;
1350
+ const generation = socketGeneration;
1351
+ const heardBefore = heardCount;
1352
+ try {
1353
+ socket.send(PING_TEXT);
1354
+ } catch {
1355
+ }
1356
+ probeTimer = timers.setTimeout(() => {
1357
+ probeTimer = null;
1358
+ if (generation !== socketGeneration || !socket) return;
1359
+ if (heardCount > heardBefore) return;
1360
+ const silentMs = timers.now() - lastHeardAt;
1361
+ diagnose({ kind: "heartbeat_timeout", silentMs });
1362
+ const dead = socket;
1363
+ onSocketGone(generation, null, new DesktopProtocolError("TIMEOUT", "Connection went silent", { retryable: true }));
1364
+ try {
1365
+ dead.close(CloseCode.NORMAL, "wake probe timeout");
1366
+ } catch {
1367
+ }
1368
+ if (state.status === "reconnecting") connect();
1369
+ }, opts.probeMs ?? 4e3);
1370
+ }
1333
1371
  return {
1334
1372
  connect,
1335
1373
  close,
@@ -1344,7 +1382,29 @@ function createDesktopClient(options) {
1344
1382
  request,
1345
1383
  stream,
1346
1384
  on,
1347
- reauth
1385
+ reauth,
1386
+ wake
1387
+ };
1388
+ }
1389
+
1390
+ // src/client/wake.ts
1391
+ function bindDesktopWake(client, options = {}) {
1392
+ const targets = options.targets ?? (typeof window !== "undefined" && typeof document !== "undefined" ? { window, document } : null);
1393
+ if (!targets) return () => void 0;
1394
+ const wakeOptions = options.probeMs === void 0 ? {} : { probeMs: options.probeMs };
1395
+ const onVisibility = () => {
1396
+ if (targets.document.visibilityState === "visible") client.wake(wakeOptions);
1397
+ };
1398
+ const onWake = () => client.wake(wakeOptions);
1399
+ targets.document.addEventListener("visibilitychange", onVisibility);
1400
+ targets.window.addEventListener("pageshow", onWake);
1401
+ targets.window.addEventListener("online", onWake);
1402
+ targets.window.addEventListener("focus", onWake);
1403
+ return () => {
1404
+ targets.document.removeEventListener("visibilitychange", onVisibility);
1405
+ targets.window.removeEventListener("pageshow", onWake);
1406
+ targets.window.removeEventListener("online", onWake);
1407
+ targets.window.removeEventListener("focus", onWake);
1348
1408
  };
1349
1409
  }
1350
1410
  //# sourceMappingURL=client.cjs.map