@alphacoder-v0/pi-loops 0.19.2 → 0.19.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/CHANGELOG.md CHANGED
@@ -3,6 +3,19 @@
3
3
  All notable changes to pi-loops are documented here. The format follows
4
4
  [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versions follow SemVer.
5
5
 
6
+ ## [0.19.3] - 2026-09-14
7
+
8
+ ### Fixed
9
+ - **Six tabs of the browser front end and nothing you typed was sent** (#14). A browser gives one
10
+ address six connections, and every open tab held one for its event stream — so the sixth tab, or
11
+ a few left behind the first, left nothing for `POST /prompt`: the fetch queued, neither
12
+ succeeding nor failing, the bubble drawn and the message never delivered. A tab now closes its
13
+ stream fifteen seconds after going into the background (`paused in background`) and opens a new
14
+ one when looked at again, polling at once so the seq/epoch check reloads what it missed; the pi
15
+ process behind the page, and its jobs, are not involved either way. And a send that has not
16
+ settled in six seconds says `still sending — if it never lands, close the other tabs of this
17
+ address` instead of looking alive.
18
+
6
19
  ## [0.19.2] - 2026-09-14
7
20
 
8
21
  ### Fixed
package/README.md CHANGED
@@ -38,7 +38,7 @@ the first sign of credentials that do not work should not be an empty inbox tomo
38
38
 
39
39
  ```bash
40
40
  pi install npm:@alphacoder-v0/pi-loops # from npm, following new releases
41
- pi install git:github.com/alphacoder-v0/pi-loops@v0.19.2 # or from GitHub, at a pinned tag
41
+ pi install git:github.com/alphacoder-v0/pi-loops@v0.19.3 # or from GitHub, at a pinned tag
42
42
  pi install /path/to/pi-loops # or a local checkout — `pi install .` in this repo
43
43
  pi -e /path/to/pi-loops # or none of them: try it for one run, installing nothing
44
44
  ```
package/README.zh-CN.md CHANGED
@@ -45,7 +45,7 @@ pi-loops 自己没有任何运行时依赖。
45
45
 
46
46
  ```bash
47
47
  pi install npm:@alphacoder-v0/pi-loops # 从 npm 装,跟着新版本走
48
- pi install git:github.com/alphacoder-v0/pi-loops@v0.19.2 # 或者从 GitHub 装,钉住一个 tag
48
+ pi install git:github.com/alphacoder-v0/pi-loops@v0.19.3 # 或者从 GitHub 装,钉住一个 tag
49
49
  pi install /path/to/pi-loops # 或者本地检出;本仓库里就是 pi install .
50
50
  ```
51
51
 
@@ -171,6 +171,18 @@ weaker, and worth converting whenever one of them breaks.
171
171
  be read as prose and waved through. `test/web-page.test.ts`: *a confirmation shows what is
172
172
  about to run*.
173
173
  - [x] Enter does not approve: the focus starts on cancel.
174
+ - [x] **A tab in the background does not hold one of the browser's six connections.** Every tab
175
+ used to keep an event stream open for as long as it existed, so the sixth tab — or a few
176
+ forgotten behind the first — left nothing for the request that sends what you typed, and the
177
+ message sat drawn and unsent (#14). A tab closes its stream fifteen seconds after going into the
178
+ background and says `paused in background`; looked at again, it subscribes anew and polls, and
179
+ the seq/epoch check reloads what it missed. The pi behind the page runs, and its jobs run, either
180
+ way. `test/web-page.test.ts`: *a tab in the background gives its connection back, and takes a
181
+ new one when looked at again*.
182
+ - [x] **A send that goes nowhere says so.** Six seconds without an answer to `/prompt` and the
183
+ status line reads `still sending — if it never lands, close the other tabs of this address`;
184
+ the request is not abandoned, and the line goes when it settles. `test/web-page.test.ts`: *a
185
+ send that goes nowhere says so instead of looking alive*.
174
186
  - [x] A slash command that waits in a dialog is not reported as timed out while the person reads:
175
187
  the browser waits up to fifteen minutes for a `/…` prompt, sixty seconds for anything else.
176
188
  - [x] Clicking away from any dialog closes it. `test/web-page.test.ts`: *clicking outside a dialog
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alphacoder-v0/pi-loops",
3
- "version": "0.19.2",
3
+ "version": "0.19.3",
4
4
  "description": "An automation layer for pi: cron + stateful loops with a triage inbox, maker/checker verification, dynamic triggers, MCP notifications and tools, lifecycle hooks, session archives. A non-invasive pi extension.",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/web.mjs CHANGED
@@ -2442,6 +2442,10 @@ const api = (p, b) =>
2442
2442
  .catch((e) => ({ success: false, error: String(e) }));
2443
2443
  const $ = (id) => document.getElementById(id);
2444
2444
  const feed = $("feed"), statusEl = $("status");
2445
+ /** How long a tab stays subscribed after it goes into the background. */
2446
+ const STREAM_PARK_MS = 15000;
2447
+ /** How long a send may take before the page says that it is taking long. */
2448
+ const SEND_WATCHDOG_MS = 6000;
2445
2449
  let state = {}, cwd = "", busy = false, atBottom = true, takesImages = true;
2446
2450
  /** Installed once the stream is up; until then a poll has nothing to compare against. */
2447
2451
  let checkStream;
@@ -3808,7 +3812,17 @@ $("composer").onsubmit = async (e) => {
3808
3812
  if (drewLocally.length > 20) drewLocally.shift();
3809
3813
  }
3810
3814
  images = []; drawThumbs();
3811
- const r = await api("/prompt", payload);
3815
+ // A send that neither succeeds nor fails is a send the browser has queued behind its connection
3816
+ // pool — every other tab of this address holding a stream is the usual reason (#14). The fetch is
3817
+ // not given up on; the page says what is happening instead of looking alive.
3818
+ const slow = setTimeout(() => { statusEl.textContent = "still sending — if it never lands, close the other tabs of this address"; }, SEND_WATCHDOG_MS);
3819
+ let r;
3820
+ try {
3821
+ r = await api("/prompt", payload);
3822
+ } finally {
3823
+ clearTimeout(slow);
3824
+ if (statusEl.textContent.startsWith("still sending")) statusEl.textContent = busy ? "working…" : "ready";
3825
+ }
3812
3826
  if (!r.success) row("err", "error", r.error || JSON.stringify(r));
3813
3827
  };
3814
3828
  $("stop").onclick = () => api("/abort", {});
@@ -4498,15 +4512,46 @@ function applyEvent(ev) {
4498
4512
  }
4499
4513
  };
4500
4514
 
4501
- const es = new EventSource("/events?token=" + TOKEN);
4502
- es.onmessage = (e) => {
4503
- const ev = JSON.parse(e.data);
4504
- if (resyncing) return void waiting.push(ev); // applied once the transcript is back
4505
- applyEvent(ev);
4515
+ /*
4516
+ * One stream per tab, and only while the tab is looked at.
4517
+ *
4518
+ * A browser gives one address six connections, and every open tab used to hold one of them for
4519
+ * its event stream — so the sixth tab, or a few forgotten behind the first, left nothing for the
4520
+ * request that sends what you typed: the fetch queued, neither succeeding nor failing, the bubble
4521
+ * drawn and the message never sent (#14). A tab in the background does not need to hear events as
4522
+ * they happen: it closes its stream after a moment, and when it is looked at again it opens a new
4523
+ * one and polls, and the seq/epoch check above reloads whatever it missed. The pi process behind
4524
+ * this page is not involved either way; it runs, and its jobs run, whether anyone is listening.
4525
+ */
4526
+ let es;
4527
+ const openStream = () => {
4528
+ if (es) return;
4529
+ es = new EventSource("/events?token=" + TOKEN);
4530
+ es.onmessage = (e) => {
4531
+ const ev = JSON.parse(e.data);
4532
+ if (resyncing) return void waiting.push(ev); // applied once the transcript is back
4533
+ applyEvent(ev);
4534
+ };
4535
+ es.onerror = () => { statusEl.textContent = "reconnecting…"; };
4536
+ es.onopen = () => refresh();
4506
4537
  };
4507
-
4508
- es.onerror = () => { statusEl.textContent = "reconnecting…"; };
4509
- es.onopen = () => refresh();
4538
+ openStream();
4539
+ let parking;
4540
+ document.addEventListener("visibilitychange", () => {
4541
+ clearTimeout(parking);
4542
+ if (document.visibilityState === "hidden") {
4543
+ // Not at once: switching tabs and back should not cost a reconnect and a reload.
4544
+ parking = setTimeout(() => {
4545
+ if (!es || document.visibilityState !== "hidden") return;
4546
+ es.close();
4547
+ es = undefined;
4548
+ statusEl.textContent = "paused in background";
4549
+ }, STREAM_PARK_MS);
4550
+ } else if (!es) {
4551
+ openStream();
4552
+ refresh().catch(() => {});
4553
+ }
4554
+ });
4510
4555
  setInterval(refresh, 8000);
4511
4556
  })();
4512
4557
  </script>`;