bloby-bot 0.68.1 → 0.68.3

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/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "bloby-bot",
3
- "version": "0.68.1",
3
+ "version": "0.68.3",
4
4
  "releaseNotes": [
5
- "1. Fix: agent self-update now actively relaunches the daemon (launchctl/systemctl) instead of relying on launchd KeepAlive — so `update` from chat/pulse comes back up reliably, matching manual `bloby update`",
6
- "2. New agent control surface (/__bloby/control/*): restart-and-verify the backend in-turn, tail backend + frontend logs, and acked self-update — replacing the flaky touch .update/.restart triggers",
7
- "3. Frontend errors (runtime, console, Vite) are now captured so the \"Copy error for your agent\" button is never empty"
5
+ "1. Fix: agent self-update ",
6
+ "1",
7
+ "3"
8
8
  ],
9
9
  "description": "Self-hosted, self-evolving AI agent with its own dashboard.",
10
10
  "type": "module",
@@ -97,6 +97,97 @@ export const SHELL_HTML = `<!DOCTYPE html>
97
97
  var s = document.getElementById('splash');
98
98
  if (s && s.style.display !== 'none') s.style.display = 'none';
99
99
  }, 10000);
100
+
101
+ /* ── Workspace watchdog ──────────────────────────────────────────────
102
+ The iframe can land on a document with NO Bloby code in it: cloudflared's raw
103
+ "502 Bad Gateway" while the supervisor restarts during a self-update, a relay
104
+ hiccup, or the browser's own network-error page. Nothing inside the frame can
105
+ recover from those — so the shell owns recovery: poll the supervisor, show a
106
+ branded "restarting" overlay while it's unreachable, and reload the frame once
107
+ it's back (then verify the frame really shows one of our documents again). */
108
+ var overlay = document.createElement('div');
109
+ overlay.id = 'bloby-ws-restarting';
110
+ // z-index 9997: above the workspace iframe, below the splash (9998), the bubble
111
+ // canvas (9999+) and the chat panel (99999) — the chat stays usable on top.
112
+ overlay.style.cssText = 'display:none;position:fixed;inset:0;z-index:9997;background:#0a0a0b;color:#e4e4e7;align-items:center;justify-content:center;padding:1.5rem;font-family:system-ui,-apple-system,sans-serif';
113
+ overlay.innerHTML =
114
+ '<div style="text-align:center;max-width:460px;width:100%">' +
115
+ '<div style="position:relative;width:160px;height:160px;margin:0 auto 1.2rem">' +
116
+ '<div style="position:absolute;inset:-18px;background:radial-gradient(circle,rgba(1,102,255,.18) 0%,transparent 60%);filter:blur(18px)"></div>' +
117
+ // preload=auto so the clip is fetched (and SW-cached) while the supervisor is
118
+ // still up — by the time we show this, the origin is unreachable.
119
+ '<video autoplay loop muted playsinline preload="auto" style="position:relative;width:100%;height:100%;object-fit:contain;border-radius:50%">' +
120
+ '<source src="/what-happened.webm" type="video/webm"><source src="/what-happened.mp4" type="video/mp4">' +
121
+ '</video>' +
122
+ '</div>' +
123
+ '<h1 style="font-size:1.5rem;font-weight:700;margin:0 0 .6rem;background:linear-gradient(135deg,#0166FF,#009AFE,#4AEEFF);-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text">Workspace is restarting&hellip;</h1>' +
124
+ '<p style="color:#e4e4e7;font-size:1rem;line-height:1.6;margin:0 0 .4rem">Your app is coming back online &mdash; this usually happens right after an update or a restart.</p>' +
125
+ '<p style="color:#a1a1aa;font-size:.93rem;line-height:1.6;margin:0">No action needed. Your chat stays online the whole time &mdash; ask your agent anything.</p>' +
126
+ '<div style="font-size:.82rem;color:#71717a;display:inline-flex;align-items:center;gap:.5rem;background:#18181b;border:1px solid #27272a;border-radius:9999px;padding:.35rem .9rem;margin-top:1.1rem"><span style="width:8px;height:8px;border-radius:50%;background:linear-gradient(135deg,#0166FF,#009AFE);box-shadow:0 0 8px rgba(1,102,255,.6)"></span><span>Watching for recovery&hellip;</span></div>' +
127
+ '</div>';
128
+ document.body.appendChild(overlay);
129
+
130
+ function refreshFrame() {
131
+ var u = target.pathname + target.search + target.hash;
132
+ try { frame.contentWindow.location.replace(u); }
133
+ catch (e) { frame.src = u; } // error pages are opaque — reassigning src re-navigates
134
+ }
135
+
136
+ function frameLooksBroken() {
137
+ try {
138
+ var d = frame.contentDocument;
139
+ if (!d) return true; // browser error pages are opaque to the same-origin parent
140
+ if (d.readyState === 'loading') return false; // still navigating — don't judge
141
+ // Every document WE can legitimately serve into the frame: the workspace app
142
+ // (#root / #splash), the supervisor interstitials (.video-wrap), and the relay's
143
+ // branded pages ("Powered by Bloby" — those self-recover, leave them alone).
144
+ if (d.getElementById('root') || d.getElementById('splash')) return false;
145
+ if (d.querySelector('.video-wrap')) return false;
146
+ var txt = String((d.body && d.body.textContent) || '');
147
+ if (txt.indexOf('Powered by Bloby') !== -1) return false;
148
+ // Only short, clearly error-shaped documents count as broken — NEVER judge a real
149
+ // app by the absence of our markers (an agent-rebuilt app may have none of them).
150
+ return txt.length < 600 && /bad gateway|gateway time|stream to the origin|cloudflare|connection refused|50[234]/i.test(txt);
151
+ } catch (e) {
152
+ return true; // cross-origin contentDocument → browser error page → broken
153
+ }
154
+ }
155
+
156
+ var downCount = 0, upCount = 0, isDown = false, brokenRetries = 0;
157
+ function watchTick() {
158
+ fetch('/__bloby/backend-status', { cache: 'no-store' })
159
+ .then(function (r) { if (!r.ok) throw new Error('status ' + r.status); return r.json(); })
160
+ .then(function () {
161
+ downCount = 0;
162
+ if (isDown) {
163
+ // Two consecutive healthy polls before reloading the frame — right after a
164
+ // restart the tunnel still drops the odd request, and reloading into that
165
+ // window is exactly how the frame lands on cloudflared's raw 502.
166
+ upCount++;
167
+ if (upCount >= 2) {
168
+ isDown = false; upCount = 0; brokenRetries = 0;
169
+ overlay.style.display = 'none';
170
+ refreshFrame();
171
+ }
172
+ } else if (frameLooksBroken()) {
173
+ // Supervisor healthy but the frame shows a foreign error page (e.g. it
174
+ // reloaded itself into a tunnel blip the watchdog never saw). Capped so an
175
+ // exotic-but-working document can never be refresh-looped forever.
176
+ if (brokenRetries < 5) { brokenRetries++; refreshFrame(); }
177
+ } else {
178
+ brokenRetries = 0;
179
+ }
180
+ })
181
+ .catch(function () {
182
+ upCount = 0;
183
+ downCount++;
184
+ if (downCount >= 2 && !isDown) { // ~8s of failures = real outage, not a blip
185
+ isDown = true;
186
+ overlay.style.display = 'flex';
187
+ }
188
+ });
189
+ }
190
+ setInterval(watchTick, 4000);
100
191
  })();
101
192
  </script>
102
193
 
@@ -155,20 +155,33 @@
155
155
  // Single instance so rapid disconnects don't stack timers; paused while hidden
156
156
  // (mirrors Vite's own visibility-aware ping) and kicked on visibility/online.
157
157
  var recoveryTimer = null;
158
+ var stalePending = false;
158
159
  function recoveryTick() {
159
160
  if (document.visibilityState === 'hidden') return;
160
161
  fetchStamp().then(function (s) {
161
162
  if (s === null || recoveryTimer === null) return;
162
163
  if (lastStamp === null) { lastStamp = s; return; } // no baseline — adopt and watch
163
164
  if (s !== lastStamp) {
165
+ // Stamp moved, but require a SECOND consecutive healthy fetch before reloading:
166
+ // right after a supervisor restart (self-update) the tunnel still drops the odd
167
+ // request — reloading into that window is how the iframe used to land on
168
+ // cloudflared's raw 502 page, which has no Bloby code in it to recover.
169
+ if (!stalePending) {
170
+ stalePending = true;
171
+ flog('stale-after-disconnect', 'stamp ' + lastStamp + '→' + s + ' (confirming next tick)');
172
+ return;
173
+ }
164
174
  flog('stale-after-disconnect', 'stamp ' + lastStamp + '→' + s);
165
175
  stopRecoveryPoll();
166
176
  location.reload(); // goes through the forensics wrapper: logged + splash
177
+ } else {
178
+ stalePending = false;
167
179
  }
168
- }).catch(function () {});
180
+ }).catch(function () { stalePending = false; }); // consecutive = no failures in between
169
181
  }
170
182
  function startRecoveryPoll() {
171
183
  if (recoveryTimer !== null) return;
184
+ stalePending = false;
172
185
  flog('recovery-poll', 'started');
173
186
  recoveryTimer = setInterval(recoveryTick, 3000);
174
187
  recoveryTick();