shraga 0.1.21 → 0.1.23

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 CHANGED
@@ -255,3 +255,5 @@ Concepts, gaps, "why not X", use cases: [Discussions](https://github.com/Livshit
255
255
  Bugs and concrete proposals: [Issues](https://github.com/Livshitz/shraga/issues).
256
256
 
257
257
 
258
+
259
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shraga",
3
- "version": "0.1.21",
3
+ "version": "0.1.23",
4
4
  "description": "The teammate you delegate coding to — a self-hostable, multi-user AI coding agent web UI (Claude Code, with a pluggable engine seam).",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -842,26 +842,30 @@ function proxySidecarWebSocket(req: import('node:http').IncomingMessage, socket:
842
842
  const targetWs = new WebSocket(targetUrl);
843
843
  let opened = false;
844
844
 
845
+ // Bound IMMEDIATELY, not inside targetWs 'open': the BROWSER's socket is OPEN the moment
846
+ // handleUpgrade returns, so it reports "connected" and starts sending while we're still dialing the
847
+ // sidecar. Registering the listener on open discarded everything typed in that window — silently.
848
+ // Queue instead, and flush in order once upstream is up.
849
+ const pending: Array<{ data: import('ws').RawData; isBinary: boolean }> = [];
850
+ clientWs.on('message', (data, isBinary) => {
851
+ // App-level liveness probe (Layer 2): the client can't read protocol pongs from JS, so it sends
852
+ // `{type:'ping'}` and expects `{type:'pong'}`. We RELAY it — we must not answer it here. A
853
+ // proxy-local reply only proves THIS hop is alive: if the proxy→sidecar leg is half-open, or the
854
+ // sidecar has already dropped this client from its subscriber set, the browser still gets pongs,
855
+ // keeps `readyState === OPEN`, shows a green "connected" dot, and every keystroke disappears.
856
+ // The probe is only worth anything end-to-end, so the sidecar owns the reply (para-pty answers in
857
+ // its ws message handler); a sidecar that doesn't reply fails the probe, which is the honest
858
+ // outcome — the client then reconnects rather than trusting a dead pipe.
859
+ if (targetWs.readyState === WebSocket.OPEN) targetWs.send(data, { binary: isBinary });
860
+ else if (!opened && pending.length < 256) pending.push({ data, isBinary }); // bounded: never buffer unboundedly
861
+ });
862
+
845
863
  targetWs.on('open', () => {
846
864
  opened = true;
847
- clientWs.on('message', (data, isBinary) => {
848
- // App-level liveness probe (Layer 2): the client can't read protocol pongs from JS and the daemon
849
- // doesn't speak ping, so answer `{type:'ping'}` here without forwarding. Lets the browser detect a
850
- // half-open socket the server-side terminate can't reach (broken path) and force a reconnect.
851
- // Cheap prefilter (small + contains "ping") so we don't JSON-parse every keystroke/paste frame.
852
- if (!isBinary && (data as Buffer).length < 64) {
853
- const s = data.toString();
854
- if (s.includes('"ping"')) {
855
- try {
856
- if (JSON.parse(s).type === 'ping') {
857
- if (clientWs.readyState === WebSocket.OPEN) clientWs.send(JSON.stringify({ type: 'pong' }));
858
- return;
859
- }
860
- } catch { /* not JSON — fall through to relay */ }
861
- }
862
- }
863
- if (targetWs.readyState === WebSocket.OPEN) targetWs.send(data, { binary: isBinary });
864
- });
865
+ for (const m of pending) {
866
+ if (targetWs.readyState === WebSocket.OPEN) targetWs.send(m.data, { binary: m.isBinary });
867
+ }
868
+ pending.length = 0;
865
869
  targetWs.on('message', (data, isBinary) => {
866
870
  if (clientWs.readyState === WebSocket.OPEN) clientWs.send(data, { binary: isBinary });
867
871
  });