@addai/node 0.30.1 → 0.30.2

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.
@@ -83,6 +83,30 @@ let missedPongs = 0;
83
83
  * people watching the same screen each need their own connection and their
84
84
  * own handshake. x11vnc runs with -shared precisely so it will serve them. */
85
85
  const bridges = new Map();
86
+ /**
87
+ * Bytes that arrived while their bridge was still being built.
88
+ *
89
+ * ⚠️ There is an await between 'attach' and the socket existing — the row has
90
+ * to be looked up to know which port to dial — and a viewer can send in that
91
+ * window. RFB never did: x11vnc speaks first, so noVNC has nothing to say
92
+ * until the server has already said hello, and the gap was invisible for as
93
+ * long as the screen was the only channel.
94
+ *
95
+ * xpra is the other way round. Its client sends its own hello the moment the
96
+ * socket opens, so on the windows channel the first packet ALWAYS raced the
97
+ * lookup — and losing it was not a dropped packet but a dead session: the data
98
+ * branch found no bridge, reported the viewer gone, and the connection that
99
+ * finished opening a moment later sat there with nothing on it until xpra
100
+ * timed out 20 seconds on:
101
+ *
102
+ * New tcp connection received from '172.17.0.1:58222'
103
+ * 0 packets received
104
+ * 1 packets sent (25 bytes)
105
+ *
106
+ * A viewerId present here means "attaching" — distinct from present in
107
+ * `bridges` (attached) and from neither (gone, and the sender should be told).
108
+ */
109
+ const pendingWrites = new Map();
86
110
  const wakeHooks = new Map();
87
111
  function onWake(kind, fn) {
88
112
  wakeHooks.set(kind, fn);
@@ -95,6 +119,9 @@ function dropBridges() {
95
119
  catch { /* gone */ }
96
120
  }
97
121
  bridges.clear();
122
+ // The link went down mid-attach. Nothing will ever flush these, and keeping
123
+ // them would hand a reconnecting viewer the last session's first packet.
124
+ pendingWrites.clear();
98
125
  }
99
126
  /**
100
127
  * Start a connection attempt that CANNOT take the daemon down with it.
@@ -175,11 +202,17 @@ async function connect() {
175
202
  const gone = (viewerId, reason) => {
176
203
  bridges.get(viewerId)?.destroy();
177
204
  bridges.delete(viewerId);
205
+ // Anything still waiting for a bridge that is never coming.
206
+ pendingWrites.delete(viewerId);
178
207
  if (sock.readyState === ws_1.default.OPEN) {
179
208
  sock.send(JSON.stringify({ type: 'gone', viewerId, reason }));
180
209
  }
181
210
  };
182
211
  if (msg.type === 'attach') {
212
+ // Claim the viewer BEFORE the await, so a packet arriving during the
213
+ // lookup is held rather than treated as arriving after a lost bridge.
214
+ if (!pendingWrites.has(msg.viewerId))
215
+ pendingWrites.set(msg.viewerId, []);
183
216
  const row = (await (0, manager_1.listDesktops)()).find(d => d.id === msg.desktopId);
184
217
  if (!row?.vnc_port) {
185
218
  gone(msg.viewerId, 'desktop is not running');
@@ -223,14 +256,28 @@ async function connect() {
223
256
  tcp.on('close', () => gone(msg.viewerId, 'the desktop closed the connection'));
224
257
  bridges.get(msg.viewerId)?.destroy();
225
258
  bridges.set(msg.viewerId, tcp);
259
+ // Whatever arrived while the row was being looked up, in order and
260
+ // before anything newer. net.connect returns a socket that is still
261
+ // connecting, and node queues writes on it until it is — so this needs
262
+ // no wait of its own.
263
+ const queued = pendingWrites.get(msg.viewerId) ?? [];
264
+ pendingWrites.delete(msg.viewerId);
265
+ for (const chunk of queued)
266
+ tcp.write(chunk);
226
267
  return;
227
268
  }
228
269
  if (msg.type === 'data' && msg.b64) {
229
270
  const bridge = bridges.get(msg.viewerId);
230
- // Input for a bridge that no longer exists. Dropping it quietly is how
231
- // "take control does nothing" happens: the pointer never moves and the
232
- // viewer has no idea why.
233
271
  if (!bridge) {
272
+ // Still attaching: hold it. The first xpra packet always lands here.
273
+ const queue = pendingWrites.get(msg.viewerId);
274
+ if (queue) {
275
+ queue.push(Buffer.from(msg.b64, 'base64'));
276
+ return;
277
+ }
278
+ // Input for a bridge that no longer exists. Dropping it quietly is how
279
+ // "take control does nothing" happens: the pointer never moves and the
280
+ // viewer has no idea why.
234
281
  gone(msg.viewerId, 'no longer connected to the desktop');
235
282
  return;
236
283
  }
@@ -260,6 +307,8 @@ async function connect() {
260
307
  if (msg.type === 'detach') {
261
308
  bridges.get(msg.viewerId)?.destroy();
262
309
  bridges.delete(msg.viewerId);
310
+ // A viewer that left while it was still attaching.
311
+ pendingWrites.delete(msg.viewerId);
263
312
  }
264
313
  });
265
314
  const retry = () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@addai/node",
3
- "version": "0.30.1",
3
+ "version": "0.30.2",
4
4
  "description": "Daemon that pairs a machine with your +Ai account and runs Claude / Codex / Kimi / Gemini agents on its behalf. Reachable via Supabase from Vault, Entity Studio, or any other +Ai surface.",
5
5
  "license": "MIT",
6
6
  "keywords": [