@ai-matrx/desktop-protocol 0.1.0 → 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 +31 -0
- package/README.md +10 -2
- package/dist/client.cjs +105 -5
- package/dist/client.cjs.map +1 -1
- package/dist/client.d.cts +31 -3
- package/dist/client.d.ts +31 -3
- package/dist/client.js +105 -5
- package/dist/client.js.map +1 -1
- package/dist/frame.cjs +12 -0
- package/dist/frame.cjs.map +1 -1
- package/dist/frame.d.cts +11 -1
- package/dist/frame.d.ts +11 -1
- package/dist/frame.js +12 -0
- package/dist/frame.js.map +1 -1
- package/dist/index.cjs +67 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +46 -3
- package/dist/index.d.ts +46 -3
- package/dist/index.js +67 -2
- package/dist/index.js.map +1 -1
- package/dist/react.cjs +28 -1
- package/dist/react.cjs.map +1 -1
- package/dist/react.d.cts +20 -1
- package/dist/react.d.ts +20 -1
- package/dist/react.js +28 -1
- package/dist/react.js.map +1 -1
- package/dist/{registry-DTcTGycI.d.cts → registry-DJbrsZ4u.d.cts} +42 -1
- package/dist/{registry-DTcTGycI.d.ts → registry-DJbrsZ4u.d.ts} +42 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,28 @@
|
|
|
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
|
+
|
|
20
|
+
## 0.1.1
|
|
21
|
+
|
|
22
|
+
Automatic changed-only republish (docs/metadata drift since the last tag — see
|
|
23
|
+
`git diff npm/desktop-protocol/v0.1.0..npm/desktop-protocol/v0.1.1 -- apps/shared/desktop-protocol`).
|
|
24
|
+
No source changes intended and no consumer action required.
|
|
25
|
+
|
|
3
26
|
## 0.1.0
|
|
4
27
|
|
|
5
28
|
First release — the Matrx 2 phone slice, lane A.
|
|
@@ -14,6 +37,13 @@ First release — the Matrx 2 phone slice, lane A.
|
|
|
14
37
|
`@ai-matrx/realtime`'s `createBackoff`.
|
|
15
38
|
- `./react` — `DesktopClientProvider`, `useDesktopConnection`, `useDesktopRequest`,
|
|
16
39
|
`useDesktopEvent`.
|
|
40
|
+
- Relay ↔ device hop, as built by the relay (lane D): `BROADCAST_CID` (an ENVELOPE frame with
|
|
41
|
+
cid 0 from the device reaches every client), `connectionEndFrame` / `isConnectionEnd` (an
|
|
42
|
+
ENVELOPE frame with FIN and no payload: relay → device "client gone", device → relay "close this
|
|
43
|
+
client"), with zod twins `RelayHopEnvelopeHeader` and `RelayHopConnectionEnd`; the `/call` hop
|
|
44
|
+
shapes `RelayCallHello` (id `hello`) and `RelayCallEnvelope` (id `call`); close codes
|
|
45
|
+
`ABNORMAL` 1006 (how a relay deploy arrives), `DEVICE_RECONNECTED` 1012, `TRY_AGAIN_LATER` 1013,
|
|
46
|
+
and `CloseCodeValue` so Python sees the same table. Corpus cases 40–55.
|
|
17
47
|
- Generated twins: `generated/desktop-protocol.schema.json` and
|
|
18
48
|
`aidream/_generated/desktop_protocol.py` (Pydantic, `StrictModel` base, `OPS`/`EVENTS`,
|
|
19
49
|
`validate_wire`), held identical by `pnpm generate --check`.
|
|
@@ -33,3 +63,4 @@ Defects found in the draft and fixed here (not preserved):
|
|
|
33
63
|
### Consumer action
|
|
34
64
|
|
|
35
65
|
None — new package.
|
|
66
|
+
|
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
|
|
|
@@ -51,7 +51,15 @@ term.write("ls\n");
|
|
|
51
51
|
those, never on messages. A dropped socket rejects unary work with `DEVICE_OFFLINE` (retryable).
|
|
52
52
|
- **Terminal closes** (`4010` revoked, `4003` forbidden, `4004` not found, `4009` replaced, `4426`
|
|
53
53
|
upgrade) end the client for good; everything else reconnects with the shared
|
|
54
|
-
`@ai-matrx/realtime` backoff (stability reset + jitter)
|
|
54
|
+
`@ai-matrx/realtime` backoff (stability reset + jitter) — including `1006` (a relay deploy drops
|
|
55
|
+
every socket without a close frame) and `1012` (the relay's "the device reconnected": hello and
|
|
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.
|
|
55
63
|
- Background problems (malformed messages, sequence gaps, heartbeat death, reconnect alarm) go to
|
|
56
64
|
`onDiagnostic` — loud by default (`console.warn`).
|
|
57
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
|
});
|
|
@@ -283,7 +284,15 @@ var CoreToClientMsg = import_zod3.z.discriminatedUnion("type", [ResponseMsg, Err
|
|
|
283
284
|
var CloseCode = {
|
|
284
285
|
NORMAL: 1e3,
|
|
285
286
|
GOING_AWAY: 1001,
|
|
286
|
-
// relay
|
|
287
|
+
// app quit; relay: the device was silent for 75 s — reconnect
|
|
288
|
+
/** Never sent by anyone: what a socket reports when it drops without a close frame. A relay
|
|
289
|
+
* deploy (every Durable Object restarts) arrives as this — reconnect immediately. */
|
|
290
|
+
ABNORMAL: 1006,
|
|
291
|
+
/** Relay → clients: the device (re)connected, so the core session they spoke to is gone —
|
|
292
|
+
* reconnect, hello, and reattach with since_seq. */
|
|
293
|
+
DEVICE_RECONNECTED: 1012,
|
|
294
|
+
TRY_AGAIN_LATER: 1013,
|
|
295
|
+
// relay: signing keys or the device registry unreachable — retry
|
|
287
296
|
AUTH_INVALID: 4001,
|
|
288
297
|
AUTH_FORBIDDEN: 4003,
|
|
289
298
|
DEVICE_NOT_FOUND: 4004,
|
|
@@ -296,6 +305,7 @@ var CloseCode = {
|
|
|
296
305
|
PROTOCOL_ERROR: 4400,
|
|
297
306
|
UPGRADE_REQUIRED: 4426
|
|
298
307
|
};
|
|
308
|
+
var CloseCodeValue = import_zod3.z.literal([1e3, 1001, 1006, 1012, 1013, 4001, 4003, 4004, 4008, 4009, 4010, 4400, 4426]);
|
|
299
309
|
|
|
300
310
|
// src/schema/frame.ts
|
|
301
311
|
var import_zod4 = require("zod");
|
|
@@ -307,6 +317,22 @@ var FrameHeader = import_zod4.z.strictObject({
|
|
|
307
317
|
sid: u32,
|
|
308
318
|
seq: u32
|
|
309
319
|
});
|
|
320
|
+
var RelayHopEnvelopeHeader = import_zod4.z.strictObject({
|
|
321
|
+
version: import_zod4.z.literal(FRAME_VERSION),
|
|
322
|
+
flags: import_zod4.z.literal(0),
|
|
323
|
+
channel: import_zod4.z.literal(255),
|
|
324
|
+
cid: u32,
|
|
325
|
+
sid: import_zod4.z.literal(0),
|
|
326
|
+
seq: import_zod4.z.literal(0)
|
|
327
|
+
});
|
|
328
|
+
var RelayHopConnectionEnd = import_zod4.z.strictObject({
|
|
329
|
+
version: import_zod4.z.literal(FRAME_VERSION),
|
|
330
|
+
flags: import_zod4.z.literal(1),
|
|
331
|
+
channel: import_zod4.z.literal(255),
|
|
332
|
+
cid: import_zod4.z.int().min(1).max(4294967295),
|
|
333
|
+
sid: import_zod4.z.literal(0),
|
|
334
|
+
seq: import_zod4.z.literal(0)
|
|
335
|
+
});
|
|
310
336
|
var DEFAULT_WINDOW_BYTES = 262144;
|
|
311
337
|
var DEFAULT_MAX_FRAME_PAYLOAD_BYTES = 65536;
|
|
312
338
|
|
|
@@ -616,6 +642,22 @@ var RelayCallResponse = import_zod9.z.discriminatedUnion("ok", [
|
|
|
616
642
|
import_zod9.z.strictObject({ ok: import_zod9.z.literal(true), result: import_zod9.z.unknown() }),
|
|
617
643
|
import_zod9.z.strictObject({ ok: import_zod9.z.literal(false), error: ProtocolErrorBody })
|
|
618
644
|
]);
|
|
645
|
+
var RELAY_CALL_HELLO_ID = "hello";
|
|
646
|
+
var RELAY_CALL_REQUEST_ID = "call";
|
|
647
|
+
var RelayCallHello = import_zod9.z.strictObject({
|
|
648
|
+
type: import_zod9.z.literal("hello"),
|
|
649
|
+
id: import_zod9.z.literal(RELAY_CALL_HELLO_ID),
|
|
650
|
+
protocolVersion: ProtocolVersion,
|
|
651
|
+
clientType: import_zod9.z.literal("server"),
|
|
652
|
+
clientVersion: import_zod9.z.string().min(1).max(32)
|
|
653
|
+
});
|
|
654
|
+
var RelayCallEnvelope = import_zod9.z.strictObject({
|
|
655
|
+
id: import_zod9.z.literal(RELAY_CALL_REQUEST_ID),
|
|
656
|
+
type: import_zod9.z.literal("request"),
|
|
657
|
+
cap: import_zod9.z.string().min(1).max(32),
|
|
658
|
+
op: import_zod9.z.string().min(1).max(64),
|
|
659
|
+
params: import_zod9.z.unknown()
|
|
660
|
+
});
|
|
619
661
|
|
|
620
662
|
// src/schema/registry.ts
|
|
621
663
|
var FS_ERR = ["NOT_FOUND", "PERMISSION_DENIED_OS", "INVALID_PARAMS", "RESOURCE_BUSY", "INTERNAL"];
|
|
@@ -720,7 +762,9 @@ function createDesktopClient(options) {
|
|
|
720
762
|
let helloId = null;
|
|
721
763
|
let reconnectTimer = null;
|
|
722
764
|
let heartbeatTimer = null;
|
|
765
|
+
let probeTimer = null;
|
|
723
766
|
let lastHeardAt = 0;
|
|
767
|
+
let heardCount = 0;
|
|
724
768
|
let closedByUser = false;
|
|
725
769
|
const newId = (prefix) => `${prefix}${(++nextId).toString(36)}`;
|
|
726
770
|
function setState(patch) {
|
|
@@ -775,6 +819,7 @@ function createDesktopClient(options) {
|
|
|
775
819
|
ws.onmessage = (event) => {
|
|
776
820
|
if (generation !== socketGeneration) return;
|
|
777
821
|
lastHeardAt = timers.now();
|
|
822
|
+
heardCount++;
|
|
778
823
|
onMessage(event.data);
|
|
779
824
|
};
|
|
780
825
|
ws.onerror = () => {
|
|
@@ -816,6 +861,10 @@ function createDesktopClient(options) {
|
|
|
816
861
|
timers.clearTimeout(heartbeatTimer);
|
|
817
862
|
heartbeatTimer = null;
|
|
818
863
|
}
|
|
864
|
+
if (probeTimer !== null) {
|
|
865
|
+
timers.clearTimeout(probeTimer);
|
|
866
|
+
probeTimer = null;
|
|
867
|
+
}
|
|
819
868
|
const terminal = code === null ? void 0 : TERMINAL_CLOSE.get(code);
|
|
820
869
|
const lost = error ?? (terminal ? new DesktopProtocolError(terminal[0], terminal[1]) : new DesktopProtocolError("DEVICE_OFFLINE", "Connection lost", { retryable: true }));
|
|
821
870
|
for (const [id, p] of pending) {
|
|
@@ -1192,15 +1241,16 @@ function createDesktopClient(options) {
|
|
|
1192
1241
|
});
|
|
1193
1242
|
result.catch(() => void 0);
|
|
1194
1243
|
done.catch(() => void 0);
|
|
1244
|
+
const attach = op === "session.attach" ? params : null;
|
|
1195
1245
|
const s = {
|
|
1196
1246
|
op,
|
|
1197
1247
|
handlers,
|
|
1198
1248
|
options: opts,
|
|
1199
1249
|
id: newId("s"),
|
|
1200
1250
|
sid: null,
|
|
1201
|
-
resourceId: null,
|
|
1202
|
-
lastSeq: 0,
|
|
1203
|
-
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,
|
|
1204
1254
|
windowBytes: opts.windowBytes ?? DEFAULT_WINDOW_BYTES,
|
|
1205
1255
|
consumedUncredited: 0,
|
|
1206
1256
|
sendWindow: DEFAULT_WINDOW_BYTES,
|
|
@@ -1289,6 +1339,34 @@ function createDesktopClient(options) {
|
|
|
1289
1339
|
function reauth(token) {
|
|
1290
1340
|
sendJson({ type: "relay", op: "reauth", token });
|
|
1291
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
|
+
}
|
|
1292
1370
|
return {
|
|
1293
1371
|
connect,
|
|
1294
1372
|
close,
|
|
@@ -1303,7 +1381,29 @@ function createDesktopClient(options) {
|
|
|
1303
1381
|
request,
|
|
1304
1382
|
stream,
|
|
1305
1383
|
on,
|
|
1306
|
-
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);
|
|
1307
1407
|
};
|
|
1308
1408
|
}
|
|
1309
1409
|
//# sourceMappingURL=client.cjs.map
|