baychat 0.8.2 → 0.9.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/README.md +43 -0
- package/dist/connect.js +5 -0
- package/dist/index.js +45 -0
- package/dist/relay/adapters.js +130 -0
- package/dist/relay/commands.js +312 -0
- package/dist/relay/daemon.js +301 -0
- package/dist/relay/queue.js +108 -0
- package/dist/relay/registry.js +151 -0
- package/dist/relay/socket.js +125 -0
- package/dist/relay/types.js +11 -0
- package/dist/relay/updates.js +131 -0
- package/package.json +2 -2
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Shared types for the relay daemon.
|
|
4
|
+
*
|
|
5
|
+
* The relay's job is narrow: hold `/updates`, decide which local session an
|
|
6
|
+
* event belongs to, and wake that session — once, never twice concurrently.
|
|
7
|
+
* It deliberately does NOT decide whether the session should reply. That
|
|
8
|
+
* judgement is the server's (`shouldRespond`) and the woken session's; the
|
|
9
|
+
* relay only carries the message across the process boundary.
|
|
10
|
+
*/
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.runUpdatesLoop = runUpdatesLoop;
|
|
4
|
+
const api_1 = require("../api");
|
|
5
|
+
/** Server clamps `wait` into [1s, 30s]; 25s stays under the usual 60s proxy idle timeout. */
|
|
6
|
+
const WAIT_SECONDS = 25;
|
|
7
|
+
/** Backoff bounds for transient failures (5xx, 429, network). */
|
|
8
|
+
const BACKOFF_MIN_MS = 1_000;
|
|
9
|
+
const BACKOFF_MAX_MS = 30_000;
|
|
10
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
11
|
+
/**
|
|
12
|
+
* Transient failures are ridden out, not surfaced. A 502 while the api
|
|
13
|
+
* container restarts, a network throw, a 429 — all mean "later", not "stop".
|
|
14
|
+
* Real 4xx (401 expired device credential, 403) are terminal: they need a
|
|
15
|
+
* human, and retrying them forever would hide that.
|
|
16
|
+
*
|
|
17
|
+
* 409 is deliberately NOT here — it has its own recovery path and must never
|
|
18
|
+
* be treated as a generic hiccup, because backing off on it would leave the
|
|
19
|
+
* cursor permanently unusable and the relay silently deaf.
|
|
20
|
+
*/
|
|
21
|
+
function isTransient(err) {
|
|
22
|
+
if (err instanceof api_1.ApiError)
|
|
23
|
+
return err.status >= 500 || err.status === 429;
|
|
24
|
+
return err instanceof TypeError; // fetch rejects with TypeError on network failure
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Holds `GET /api/device-api/updates` and calls `onEvents` for every batch,
|
|
28
|
+
* until `signal` aborts.
|
|
29
|
+
*
|
|
30
|
+
* The credential is the machine's DEVICE credential, not an agent token. A
|
|
31
|
+
* laptop has one device login and its sessions mint their own agents when they
|
|
32
|
+
* join, so no single agent token can see them all — the server fans out across
|
|
33
|
+
* the device's live sessions and tags each event with the session it belongs to.
|
|
34
|
+
*
|
|
35
|
+
* Cursor discipline: the cursor is an opaque blob (really one cursor per session
|
|
36
|
+
* agent, encoded server-side) and advances only after a batch has been handed to
|
|
37
|
+
* `onEvents`. The caller queues synchronously, so a crash between poll and queue
|
|
38
|
+
* costs a re-read — harmless, ids de-dupe — rather than a lost message.
|
|
39
|
+
*
|
|
40
|
+
* On 409 `cursor_expired` recovery is ours: the server holds no record of what
|
|
41
|
+
* this client consumed. We re-read each watched conversation from our own
|
|
42
|
+
* watermark, then poll again with no cursor.
|
|
43
|
+
*/
|
|
44
|
+
async function runUpdatesLoop(opts) {
|
|
45
|
+
const { auth, watermarks, onEvents, onSessions, onPoll, onError, signal, request = api_1.apiRequest } = opts;
|
|
46
|
+
let cursor;
|
|
47
|
+
let backoff = BACKOFF_MIN_MS;
|
|
48
|
+
while (!signal.aborted) {
|
|
49
|
+
try {
|
|
50
|
+
const q = new URLSearchParams({ wait: String(WAIT_SECONDS) });
|
|
51
|
+
if (cursor)
|
|
52
|
+
q.set("cursor", cursor);
|
|
53
|
+
const res = await request(auth, "GET", `/api/device-api/updates?${q.toString()}`);
|
|
54
|
+
onSessions?.(res.sessions ?? []);
|
|
55
|
+
if (res.events.length > 0) {
|
|
56
|
+
onEvents(res.events.map((e) => ({
|
|
57
|
+
conversationId: e.conversationId,
|
|
58
|
+
sessionName: e.sessionName,
|
|
59
|
+
message: { ...e.message, conversationId: e.conversationId },
|
|
60
|
+
})));
|
|
61
|
+
}
|
|
62
|
+
cursor = res.cursor;
|
|
63
|
+
onPoll?.(res.cursor);
|
|
64
|
+
backoff = BACKOFF_MIN_MS; // a clean poll resets the ladder
|
|
65
|
+
}
|
|
66
|
+
catch (err) {
|
|
67
|
+
if (signal.aborted)
|
|
68
|
+
return;
|
|
69
|
+
if (err instanceof api_1.ApiError && err.status === 409) {
|
|
70
|
+
onError?.(err, true);
|
|
71
|
+
try {
|
|
72
|
+
const recovered = await catchUp(auth, watermarks, request);
|
|
73
|
+
if (recovered.length > 0)
|
|
74
|
+
onEvents(recovered);
|
|
75
|
+
}
|
|
76
|
+
catch (catchUpErr) {
|
|
77
|
+
// Catch-up itself failed. Do NOT clear the cursor and carry on as if
|
|
78
|
+
// recovered — that is the silent-gap case. Back off and let the next
|
|
79
|
+
// 409 retry the whole recovery.
|
|
80
|
+
onError?.(catchUpErr, true);
|
|
81
|
+
await sleep(backoff);
|
|
82
|
+
backoff = Math.min(backoff * 2, BACKOFF_MAX_MS);
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
cursor = undefined;
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (!isTransient(err)) {
|
|
89
|
+
onError?.(err, false);
|
|
90
|
+
throw err; // terminal — an expired device credential needs `npx baychat login`
|
|
91
|
+
}
|
|
92
|
+
onError?.(err, true);
|
|
93
|
+
await sleep(backoff);
|
|
94
|
+
backoff = Math.min(backoff * 2, BACKOFF_MAX_MS);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Re-read each watched conversation from its watermark. Used only on 409
|
|
100
|
+
* recovery. Messages we already delivered come back here; the caller's
|
|
101
|
+
* per-session queue de-dupes them by id, so a replay is cheap rather than
|
|
102
|
+
* duplicated into the room.
|
|
103
|
+
*
|
|
104
|
+
* A 404 for one conversation is skipped rather than fatal: a session may have
|
|
105
|
+
* ended or left the room between the poll and the recovery, and one dead room
|
|
106
|
+
* must not strand the catch-up for every other.
|
|
107
|
+
*/
|
|
108
|
+
async function catchUp(auth, watermarks, request) {
|
|
109
|
+
const events = [];
|
|
110
|
+
for (const [conversationId, since] of watermarks) {
|
|
111
|
+
let res;
|
|
112
|
+
try {
|
|
113
|
+
res = await request(auth, "GET", `/api/device-api/conversations/${conversationId}/messages?since=${encodeURIComponent(since)}`);
|
|
114
|
+
}
|
|
115
|
+
catch (err) {
|
|
116
|
+
if (err instanceof api_1.ApiError && err.status === 404)
|
|
117
|
+
continue;
|
|
118
|
+
throw err;
|
|
119
|
+
}
|
|
120
|
+
for (const m of res.messages) {
|
|
121
|
+
if (m.deletedAt)
|
|
122
|
+
continue;
|
|
123
|
+
events.push({
|
|
124
|
+
conversationId,
|
|
125
|
+
sessionName: res.sessionName,
|
|
126
|
+
message: { ...m, conversationId },
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return events;
|
|
131
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "baychat",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "BayChat connector CLI
|
|
3
|
+
"version": "0.9.0",
|
|
4
|
+
"description": "BayChat connector CLI — pair an agent session (Claude Code, Codex) with BayChat and chat in groups",
|
|
5
5
|
"bin": {
|
|
6
6
|
"baychat": "dist/index.js"
|
|
7
7
|
},
|