shadok-ai 0.16.6 → 0.16.9

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
@@ -531,16 +531,19 @@ every 25s (tunable with `SHADOK_WS_PING_MS`) to keep the connection warm, so no
531
531
  proxy change is normally needed; if your proxy still cuts it, raise
532
532
  `proxy_read_timeout` above the ping interval.
533
533
 
534
- **Sleep / wake.** When a laptop sleeps, the socket's TCP connection usually dies
535
- without the browser ever seeing a `close`, so it keeps reporting the link as
536
- "open" while nothing flows. On wake the cockpit no longer trusts that stale
537
- state: a real wake — the page becoming visible after more than ~10s away, a
538
- network change (`online`), or a page restored from the back/forward cache —
539
- tears the suspect socket down and opens a fresh one at once, reattaching to the
540
- same session. A quick tab switch is left alone (the link is almost certainly
541
- fine), so you don't pay a reconnect every time you glance away. Previously a
542
- woken tab waited out the browser's own TCP timeout, tens of seconds to minutes,
543
- stuck on "reconnecting…".
534
+ **Sleep / wake / network blips.** When the machine comes back (an `online`
535
+ event, the tab becoming visible, a back/forward-cache restore), the cockpit
536
+ revives the links that actually **dropped** — resetting their backoff so a
537
+ genuinely-closed socket reconnects promptly instead of waiting out a delay that
538
+ climbed while you were away. A socket that still reports "open" is left alone:
539
+ `online` fires on every network blip (wifi roam, VPN, interface change), and
540
+ force-reconnecting healthy links on each one reconnected all your agents at
541
+ once — tripping the upstream single-use-token OAuth refresh race so a batch
542
+ stuck on "connecting…". A slept socket that looks "open" but is dead is
543
+ terminated by the server heartbeat (~50s) and reconnects on the resulting
544
+ `close` without that churn. Only a back/forward-cache restore force-replaces an
545
+ "open" socket, because there the restored page's old sockets are dead yet fire
546
+ no `close`.
544
547
 
545
548
  ### Cross-instance peers
546
549
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shadok-ai",
3
- "version": "0.16.6",
3
+ "version": "0.16.9",
4
4
  "main": "dist/session.js",
5
5
  "scripts": {
6
6
  "test": "node --import tsx --test --test-timeout=120000 test/*.ts context/*/test/*.test.mjs",
package/public/index.html CHANGED
@@ -2272,6 +2272,20 @@
2272
2272
  .tab .lastat, .tab .ctxnum { font-size: 11px; color: var(--text-dim); }
2273
2273
  .tab.active { border-left: 0; border-radius: 6px; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); background: var(--bg-raised); font-weight: 600; }
2274
2274
  :root:not([data-mode="light"]) .tab.active { background: var(--bg-inset); box-shadow: inset 0 0 0 1px var(--line); }
2275
+ /* A working agent must stay visible in light mode. Two things hid it: 12% of
2276
+ the accent over a near-white panel is barely a tint, and the white
2277
+ `.tab.active` above comes later at the same weight, so the agent you are
2278
+ watching — the one most likely to be working — went plain white. The tint
2279
+ is mixed INTO the surface (opaque) and the selectors outweigh both
2280
+ `.tab.active` rules, so the active tab keeps its state colour in either mode.
2281
+ `background-COLOR`, never the shorthand: `background` expands to
2282
+ `background-image: none`, and these selectors outweigh `.tab.has-bg`, so the
2283
+ shorthand silently erased the green hatch of an agent with background work
2284
+ still running (measured: light mode, a working non-active row lost it). */
2285
+ :root[data-mode="light"] .tab.working,
2286
+ :root[data-mode="light"] .rail-item.working { background-color: color-mix(in srgb, var(--amber) 22%, var(--bg-raised)); }
2287
+ :root[data-mode="light"] .tab.working.active { background-color: color-mix(in srgb, var(--amber) 18%, var(--bg-raised)); }
2288
+ :root:not([data-mode="light"]) .tab.working.active { background-color: color-mix(in srgb, var(--amber) 16%, var(--bg-inset)); }
2275
2289
  .tab.active .tav .led { border-color: var(--bg-raised); }
2276
2290
  .tab.unread { border-left: 0; border-radius: 6px; font-weight: 700; }
2277
2291
  .group-head { gap: 10px; padding: 8px; font-size: 14px; color: var(--text); border-radius: 6px; }
@@ -7081,51 +7095,45 @@
7081
7095
  // off. Bounded so an update that never lands doesn't leave us hammering.
7082
7096
  let reloadExpectedUntil = 0;
7083
7097
 
7084
- /** Reconnect every dropped tab immediately (no backoff wait) — fired when the
7085
- * machine/tab comes back (online / visible), the common laptop-wake case.
7086
- * `force` replaces even a socket that still reports OPEN: after a laptop
7087
- * sleep the TCP connection is usually dead while the browser has seen no
7088
- * `close`, so `readyState` lies "OPEN" and the gentle path would skip the
7089
- * tab — leaving it stranded until the browser's own TCP timeout fires
7090
- * (tens of seconds to minutes), which is exactly the "stuck reconnecting,
7091
- * very slow" symptom. A CONNECTING socket is always left alone: a link is
7092
- * already in flight, and tearing it down would only restart the clock. */
7098
+ /** Revive the tabs whose link actually DROPPED — fired when the machine/tab
7099
+ * comes back (online / visible). It resets the backoff and reconnects at
7100
+ * once, so a genuinely-closed socket comes back promptly instead of waiting
7101
+ * out an exponential delay that climbed while we were away.
7102
+ *
7103
+ * It deliberately does NOT touch a socket that still reports OPEN. An earlier
7104
+ * version force-replaced OPEN sockets too (to catch a laptop-sleep "zombie" —
7105
+ * OPEN on the client, dead on the wire). But `online` fires on EVERY network
7106
+ * blip (wifi roam, VPN, interface change), and force-closing every healthy
7107
+ * link on each blip reconnected all N agents at once — tripping the upstream
7108
+ * OAuth refresh-token race (single-use tokens, concurrent `claude --resume`,
7109
+ * biting at ~10–30 agents) so a whole batch stuck on "connecting…". That mass
7110
+ * churn was worse than the zombie it chased: a slept socket is terminated by
7111
+ * the server heartbeat (~50s) and the resulting `close` reconnects it anyway,
7112
+ * no force needed. `force` is kept ONLY for a bfcache `pageshow`, where the
7113
+ * restored page's old sockets are genuinely dead and fire no `close`. */
7093
7114
  function reconnectAllNow(force) {
7094
7115
  for (const t of tabs) {
7095
7116
  if (t.closing || !t.sessionId) continue;
7096
7117
  const st = t.ws && t.ws.readyState;
7097
7118
  if (st === WebSocket.CONNECTING) continue; // a link is already on its way
7098
- if (st === WebSocket.OPEN && !force) continue; // healthy and no reason to doubt it
7119
+ if (st === WebSocket.OPEN && !force) continue; // healthy — leave it, a blip is not a death
7099
7120
  if (t.reconnectTimer) clearTimeout(t.reconnectTimer);
7100
- t.reconnectAttempts = 0;
7121
+ t.reconnectAttempts = 0; // reset the backoff: reconnect promptly, not after a climbed delay
7101
7122
  const old = t.ws;
7102
7123
  openLink(t); // replaces t.ws with the fresh socket
7103
- // Close the suspected-zombie only after openLink swapped t.ws, so its
7124
+ // Close a forced (bfcache) zombie only after openLink swapped t.ws, so its
7104
7125
  // `close` handler sees `t.ws !== ws` and does not schedule a competing
7105
7126
  // reconnect. A no-op when `old` was already closed.
7106
7127
  if (old && old !== t.ws) { try { old.close(); } catch {} }
7107
7128
  }
7108
7129
  }
7109
7130
 
7110
- // A real wake (laptop sleep, bfcache restore, a network change) must force a
7111
- // fresh link; a quick tab switch must not — forcing there would flash
7112
- // "reconnecting" and pay a resume round-trip every time you glance away.
7113
- // The tell is how long we were hidden: seconds for an alt-tab, minutes for a
7114
- // sleep. `online` and a bfcache `pageshow` are forced outright — the network
7115
- // or the page itself just changed under us.
7116
- const STALE_WAKE_MS = 10_000;
7117
- let hiddenAt = 0;
7118
- function wokeAfterLongAway() {
7119
- const away = hiddenAt ? Date.now() - hiddenAt : 0;
7120
- hiddenAt = 0;
7121
- return away > STALE_WAKE_MS;
7122
- }
7123
- window.addEventListener("online", () => reconnectAllNow(true));
7124
- window.addEventListener("pageshow", (e) => reconnectAllNow(!!e.persisted || wokeAfterLongAway()));
7125
- document.addEventListener("visibilitychange", () => {
7126
- if (document.hidden) { hiddenAt = Date.now(); return; }
7127
- reconnectAllNow(wokeAfterLongAway());
7128
- });
7131
+ // Reconnect the links that dropped while we were away — but never disturb a
7132
+ // healthy one (see reconnectAllNow). Only a bfcache restore forces, because
7133
+ // there the old sockets are dead yet fire no `close`.
7134
+ window.addEventListener("online", () => reconnectAllNow(false));
7135
+ window.addEventListener("pageshow", (e) => reconnectAllNow(!!e.persisted));
7136
+ document.addEventListener("visibilitychange", () => { if (!document.hidden) reconnectAllNow(false); });
7129
7137
 
7130
7138
  /**
7131
7139
  * Lists the agents alive in the server right now — including ones spawned