shadok-ai 0.16.8 → 0.16.10

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
@@ -84,7 +84,8 @@ Enter to skip; you can add it later from the web UI).
84
84
  emoji, or **type a word** (`rocket`, `bug`, `money`…) to search for one; its
85
85
  **+** creates the new agent straight into that group. Dragging an agent onto a
86
86
  group files it there, dragging groups reorders them, and double-clicking a name
87
- renames it. A question the agent asks shows as an *Approval requested* card. The
87
+ renames it — a group can also be renamed by clicking its name at the top of the
88
+ agents column. A question the agent asks shows as an *Approval requested* card. The
88
89
  whole cockpit is skinned like Microsoft Teams (grey light mode / near-black dark
89
90
  mode, the Shadok mark in the header). On a phone the tools sit in the ⋯ menu and
90
91
  the agents sheet lists every group.
@@ -531,16 +532,19 @@ every 25s (tunable with `SHADOK_WS_PING_MS`) to keep the connection warm, so no
531
532
  proxy change is normally needed; if your proxy still cuts it, raise
532
533
  `proxy_read_timeout` above the ping interval.
533
534
 
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…".
535
+ **Sleep / wake / network blips.** When the machine comes back (an `online`
536
+ event, the tab becoming visible, a back/forward-cache restore), the cockpit
537
+ revives the links that actually **dropped** — resetting their backoff so a
538
+ genuinely-closed socket reconnects promptly instead of waiting out a delay that
539
+ climbed while you were away. A socket that still reports "open" is left alone:
540
+ `online` fires on every network blip (wifi roam, VPN, interface change), and
541
+ force-reconnecting healthy links on each one reconnected all your agents at
542
+ once — tripping the upstream single-use-token OAuth refresh race so a batch
543
+ stuck on "connecting…". A slept socket that looks "open" but is dead is
544
+ terminated by the server heartbeat (~50s) and reconnects on the resulting
545
+ `close` without that churn. Only a back/forward-cache restore force-replaces an
546
+ "open" socket, because there the restored page's old sockets are dead yet fire
547
+ no `close`.
544
548
 
545
549
  ### Cross-instance peers
546
550
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shadok-ai",
3
- "version": "0.16.8",
3
+ "version": "0.16.10",
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
@@ -528,6 +528,9 @@
528
528
  overflow-y: auto;
529
529
  }
530
530
  #tabbar .side-label { padding: 0 8px 8px; font-size: 13px; font-weight: 600; color: var(--text); }
531
+ #tabbar .side-label #viewLabel.editable { cursor: text; border-radius: 4px; padding: 1px 3px; margin: -1px -3px; }
532
+ #tabbar .side-label #viewLabel.editable:hover { background: var(--bg-inset); box-shadow: inset 0 0 0 1px var(--line); }
533
+ #tabbar .side-label input.rename { font: inherit; width: calc(100% - 2em); }
531
534
  .tab {
532
535
  position: relative;
533
536
  display: flex;
@@ -3035,7 +3038,7 @@
3035
3038
  <div id="iconEmpty" class="icon-empty" hidden>No emoji for that word.</div>
3036
3039
  </div>
3037
3040
  <nav id="tabbar" aria-label="Agents">
3038
- <div class="label side-label"><span class="sl-all">Agents</span><span class="sl-view" id="viewLabel"></span></div>
3041
+ <div class="label side-label"><span class="sl-all">Agents</span><span class="sl-view"><span id="viewIcon"></span> <span id="viewLabel"></span></span></div>
3039
3042
  <div id="ungrouped"></div>
3040
3043
  <div id="groups"></div>
3041
3044
  <p id="viewEmpty" class="check-hint" hidden>No agent in this group yet — create one, or drag one onto the group.</p>
@@ -3427,6 +3430,7 @@
3427
3430
 
3428
3431
  const $ = (id) => document.getElementById(id);
3429
3432
  const tabbar = $("tabbar"), newTabBtn = $("newTab"), newGroupBtn = $("newGroup");
3433
+ const viewLabelEl = $("viewLabel");
3430
3434
  const ungroupedEl = $("ungrouped"), groupsEl = $("groups");
3431
3435
  const transcripts = $("transcripts"), thinking = $("thinking");
3432
3436
  const composer = $("composer"), promptInput = $("promptInput"), sendBtn = $("sendBtn");
@@ -4813,11 +4817,34 @@
4813
4817
  if (gi && gi.textContent !== g.icon) gi.textContent = g.icon;
4814
4818
  }
4815
4819
  const g = groups.find((x) => String(x.id) === v);
4816
- $("viewLabel").textContent = g ? `${g.icon} ${g.nameEl.textContent}` : "Main";
4820
+ $("viewIcon").textContent = g ? g.icon : "";
4821
+ // A cached reference, never $("viewLabel"): while it is being renamed the
4822
+ // span is swapped out for the input, and a lookup by id would return null
4823
+ // and throw on the very next sync.
4824
+ viewLabelEl.textContent = g ? g.nameEl.textContent : "Main";
4825
+ // Only a real group can be renamed here: "Main" is the ungrouped view, not
4826
+ // a group, and has no name of its own to store.
4827
+ viewLabelEl.classList.toggle("editable", !!g);
4828
+ viewLabelEl.title = g ? "Click to rename this group" : "";
4817
4829
  $("viewEmpty").hidden = tabsInView(v).length > 0 || v === "ungrouped";
4818
4830
  renderRail();
4819
4831
  }
4820
4832
 
4833
+ // The column's title names the group you are looking at, so it is where one
4834
+ // reaches to rename it — the rail item needs a double-click and, collapsed to
4835
+ // icons, shows no name at all. Wired ONCE on the stable span: applyView
4836
+ // rewrites its text, never the element.
4837
+ viewLabelEl.addEventListener("click", (e) => {
4838
+ const g = groups.find((x) => String(x.id) === currentView());
4839
+ if (!g) return;
4840
+ e.stopPropagation();
4841
+ inlineRename(tabbar, viewLabelEl, (v) => {
4842
+ g.nameEl.textContent = v;
4843
+ persistGroups();
4844
+ applyView();
4845
+ });
4846
+ });
4847
+
4821
4848
  const moodOf = (t) =>
4822
4849
  ["needs-answer", "unread", "working"].find((m) => t.btnEl.classList.contains(m)) || null;
4823
4850
 
@@ -7095,51 +7122,45 @@
7095
7122
  // off. Bounded so an update that never lands doesn't leave us hammering.
7096
7123
  let reloadExpectedUntil = 0;
7097
7124
 
7098
- /** Reconnect every dropped tab immediately (no backoff wait) — fired when the
7099
- * machine/tab comes back (online / visible), the common laptop-wake case.
7100
- * `force` replaces even a socket that still reports OPEN: after a laptop
7101
- * sleep the TCP connection is usually dead while the browser has seen no
7102
- * `close`, so `readyState` lies "OPEN" and the gentle path would skip the
7103
- * tab — leaving it stranded until the browser's own TCP timeout fires
7104
- * (tens of seconds to minutes), which is exactly the "stuck reconnecting,
7105
- * very slow" symptom. A CONNECTING socket is always left alone: a link is
7106
- * already in flight, and tearing it down would only restart the clock. */
7125
+ /** Revive the tabs whose link actually DROPPED — fired when the machine/tab
7126
+ * comes back (online / visible). It resets the backoff and reconnects at
7127
+ * once, so a genuinely-closed socket comes back promptly instead of waiting
7128
+ * out an exponential delay that climbed while we were away.
7129
+ *
7130
+ * It deliberately does NOT touch a socket that still reports OPEN. An earlier
7131
+ * version force-replaced OPEN sockets too (to catch a laptop-sleep "zombie" —
7132
+ * OPEN on the client, dead on the wire). But `online` fires on EVERY network
7133
+ * blip (wifi roam, VPN, interface change), and force-closing every healthy
7134
+ * link on each blip reconnected all N agents at once — tripping the upstream
7135
+ * OAuth refresh-token race (single-use tokens, concurrent `claude --resume`,
7136
+ * biting at ~10–30 agents) so a whole batch stuck on "connecting…". That mass
7137
+ * churn was worse than the zombie it chased: a slept socket is terminated by
7138
+ * the server heartbeat (~50s) and the resulting `close` reconnects it anyway,
7139
+ * no force needed. `force` is kept ONLY for a bfcache `pageshow`, where the
7140
+ * restored page's old sockets are genuinely dead and fire no `close`. */
7107
7141
  function reconnectAllNow(force) {
7108
7142
  for (const t of tabs) {
7109
7143
  if (t.closing || !t.sessionId) continue;
7110
7144
  const st = t.ws && t.ws.readyState;
7111
7145
  if (st === WebSocket.CONNECTING) continue; // a link is already on its way
7112
- if (st === WebSocket.OPEN && !force) continue; // healthy and no reason to doubt it
7146
+ if (st === WebSocket.OPEN && !force) continue; // healthy — leave it, a blip is not a death
7113
7147
  if (t.reconnectTimer) clearTimeout(t.reconnectTimer);
7114
- t.reconnectAttempts = 0;
7148
+ t.reconnectAttempts = 0; // reset the backoff: reconnect promptly, not after a climbed delay
7115
7149
  const old = t.ws;
7116
7150
  openLink(t); // replaces t.ws with the fresh socket
7117
- // Close the suspected-zombie only after openLink swapped t.ws, so its
7151
+ // Close a forced (bfcache) zombie only after openLink swapped t.ws, so its
7118
7152
  // `close` handler sees `t.ws !== ws` and does not schedule a competing
7119
7153
  // reconnect. A no-op when `old` was already closed.
7120
7154
  if (old && old !== t.ws) { try { old.close(); } catch {} }
7121
7155
  }
7122
7156
  }
7123
7157
 
7124
- // A real wake (laptop sleep, bfcache restore, a network change) must force a
7125
- // fresh link; a quick tab switch must not — forcing there would flash
7126
- // "reconnecting" and pay a resume round-trip every time you glance away.
7127
- // The tell is how long we were hidden: seconds for an alt-tab, minutes for a
7128
- // sleep. `online` and a bfcache `pageshow` are forced outright — the network
7129
- // or the page itself just changed under us.
7130
- const STALE_WAKE_MS = 10_000;
7131
- let hiddenAt = 0;
7132
- function wokeAfterLongAway() {
7133
- const away = hiddenAt ? Date.now() - hiddenAt : 0;
7134
- hiddenAt = 0;
7135
- return away > STALE_WAKE_MS;
7136
- }
7137
- window.addEventListener("online", () => reconnectAllNow(true));
7138
- window.addEventListener("pageshow", (e) => reconnectAllNow(!!e.persisted || wokeAfterLongAway()));
7139
- document.addEventListener("visibilitychange", () => {
7140
- if (document.hidden) { hiddenAt = Date.now(); return; }
7141
- reconnectAllNow(wokeAfterLongAway());
7142
- });
7158
+ // Reconnect the links that dropped while we were away — but never disturb a
7159
+ // healthy one (see reconnectAllNow). Only a bfcache restore forces, because
7160
+ // there the old sockets are dead yet fire no `close`.
7161
+ window.addEventListener("online", () => reconnectAllNow(false));
7162
+ window.addEventListener("pageshow", (e) => reconnectAllNow(!!e.persisted));
7163
+ document.addEventListener("visibilitychange", () => { if (!document.hidden) reconnectAllNow(false); });
7143
7164
 
7144
7165
  /**
7145
7166
  * Lists the agents alive in the server right now — including ones spawned