aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTianren Dong <tianrendong.dtr@gmail.com>2026-05-16 13:00:48 -0400
committerTianren Dong <tianrendong.dtr@gmail.com>2026-05-16 13:00:48 -0400
commitfcae5b188baa1bdfed48208d7cfc2b711af4aae3 (patch)
tree040c6cde8b8d23d1909c7235641e6694ed170495
parent43c62c2f338654394f82c86aa12b22be6a358d94 (diff)
0.17.2: fix /next lastSeenAt + strict daemon version match
Bugs surfaced in real-world 0.17.0/0.17.1 testing: 1. Daemon /next only recorded lastSeenAt AFTER the auth/pair checks. Pre-pair the daemon returns needsPairing early without recording the poll, so connected stayed false even though the extension was polling. /chrome onboard's wait-for-poll loop spun forever on a healthy extension. Fix: record lastSeenAt at the top of /next for any chrome-extension origin, before auth checks. Onboard now correctly detects 'extension alive but unpaired' which is the precise state during pair step. 2. Daemon compatibility was checked at major.minor match. Result: a patch release fixing a daemon bug (like #1) couldn't replace the running daemon — new Pi client joins old buggy daemon as client. Fix: exact version match. Patch releases auto-kill+respawn the daemon.
-rw-r--r--CHANGELOG.md7
-rw-r--r--extensions/chrome-profile-bridge/browser-extension/manifest.json2
-rw-r--r--extensions/chrome-profile-bridge/daemon.cjs10
-rw-r--r--extensions/chrome-profile-bridge/index.ts16
-rw-r--r--package.json2
5 files changed, 26 insertions, 11 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5f2fed8..978bd75 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,13 @@
All notable user-facing changes to `pi-chrome`.
+## 0.17.2 — 2026-05-16
+
+### Fixes for the daemon model surfaced during 0.17.0/0.17.1 testing
+
+- **Daemon: record `lastSeenAt` on *every* `/next` from a chrome-extension origin**, not only after the paired-auth checks. Pre-pairing the daemon returns `needsPairing: true` early; before this fix it never updated `lastSeenAt`, so the daemon's `connected` flag stayed `false` even when the extension was happily polling. `/chrome onboard` would loop forever on "Waiting for the extension to wake up…" even with a perfectly healthy extension.
+- **Daemon compatibility is now checked at exact version match**, not just major.minor. Without this, a 0.17.x patch release that fixes a daemon bug couldn't actually replace the running 0.17.0 daemon — the new Pi client would `sameMajorMinor`-match the old daemon and join as client, never triggering a respawn. Patch releases now auto-kill+respawn the daemon, so daemon bug fixes ship cleanly via `pi update`.
+
## 0.17.1 — 2026-05-16
### Fix: `/chrome onboard` stuck on "Waiting for the extension to wake up…"
diff --git a/extensions/chrome-profile-bridge/browser-extension/manifest.json b/extensions/chrome-profile-bridge/browser-extension/manifest.json
index 98f47a5..d3db49e 100644
--- a/extensions/chrome-profile-bridge/browser-extension/manifest.json
+++ b/extensions/chrome-profile-bridge/browser-extension/manifest.json
@@ -1,7 +1,7 @@
{
"manifest_version": 3,
"name": "Pi Chrome Connector",
- "version": "0.17.1",
+ "version": "0.17.2",
"description": "Lets Pi control tabs in Chrome via a local connector at 127.0.0.1.",
"permissions": [
"tabs",
diff --git a/extensions/chrome-profile-bridge/daemon.cjs b/extensions/chrome-profile-bridge/daemon.cjs
index 676344f..bcd2b68 100644
--- a/extensions/chrome-profile-bridge/daemon.cjs
+++ b/extensions/chrome-profile-bridge/daemon.cjs
@@ -569,6 +569,14 @@ async function handle(req, res) {
if (!isBrowserOriginAllowed(req)) { sendJson(res, 403, { ok: false, error: "browser origin not allowed" }); return; }
const originExt = extensionIdFromOrigin(req);
const currentVersion = ARGS.version;
+ // Any /next from a chrome-extension origin counts as 'extension is alive'. We record
+ // lastSeenAt here — before the auth/pair checks below — so that /chrome onboard's
+ // connect-wait loop can detect a polling-but-unpaired extension (which is precisely
+ // the state during the pair step).
+ if (originExt) {
+ lastSeenAt = Date.now();
+ clientName = url.searchParams.get("name") ?? undefined;
+ }
if (!auth.paired) {
sendJson(res, 200, { type: "none", needsPairing: true, expectedExtensionVersion: currentVersion },
{ ...corsHeaders, "x-pi-chrome-version": currentVersion });
@@ -590,8 +598,6 @@ async function handle(req, res) {
return;
}
if (waiters.length >= MAX_WAITERS) { sendJson(res, 429, { ok: false, error: "too many pollers" }, corsHeaders); return; }
- lastSeenAt = Date.now();
- clientName = url.searchParams.get("name") ?? undefined;
let aborted = false;
let active;
req.once("close", () => { aborted = true; if (active) waiters = waiters.filter((e) => e !== active); });
diff --git a/extensions/chrome-profile-bridge/index.ts b/extensions/chrome-profile-bridge/index.ts
index db41ce3..820c7d7 100644
--- a/extensions/chrome-profile-bridge/index.ts
+++ b/extensions/chrome-profile-bridge/index.ts
@@ -114,10 +114,12 @@ function probeBridgeStatus(host: string, port: number): { bridgeVersion?: string
}
}
-function sameMajorMinor(a: string, b: string): boolean {
- const [aMaj, aMin] = a.split(".").map((n) => Number(n));
- const [bMaj, bMin] = b.split(".").map((n) => Number(n));
- return aMaj === bMaj && aMin === bMin;
+// Daemon compat is checked at exact version match — not major.minor — so that even patch
+// releases trigger an automatic kill+respawn. Patch releases routinely fix bugs in the daemon
+// itself (e.g. /next lastSeenAt handling); without exact matching, a new Pi client joins an
+// old buggy daemon and the fix never lands.
+function daemonVersionCompatible(daemonVersion: string, clientVersion: string): boolean {
+ return daemonVersion === clientVersion;
}
// Ensure the bundled daemon.cjs is copied to a versioned cache dir so each pi-chrome
@@ -178,7 +180,7 @@ function waitForDaemonReady(host: string, port: number, timeoutMs = 4_000): Reco
while (Date.now() < deadline) {
const probed = probeBridgeStatus(host, port);
if (probed && typeof (probed as { bridgeVersion?: string }).bridgeVersion === "string"
- && sameMajorMinor((probed as { bridgeVersion: string }).bridgeVersion, PI_CHROME_VERSION)) {
+ && daemonVersionCompatible((probed as { bridgeVersion: string }).bridgeVersion, PI_CHROME_VERSION)) {
return probed;
}
spawnSync("sleep", ["0.1"]);
@@ -844,7 +846,7 @@ class ChromeProfileBridge {
const ownerVersion = typeof (probed as { bridgeVersion?: string })?.bridgeVersion === "string"
? (probed as { bridgeVersion: string }).bridgeVersion
: undefined;
- const compatible = ownerVersion ? sameMajorMinor(ownerVersion, PI_CHROME_VERSION) : false;
+ const compatible = ownerVersion ? daemonVersionCompatible(ownerVersion, PI_CHROME_VERSION) : false;
if (probed && compatible) {
this.mode = "client";
return;
@@ -868,7 +870,7 @@ class ChromeProfileBridge {
private async handleEaddrinuse(): Promise<void> {
const probed = probeBridgeStatus(this.host, this.port);
const ownerVersion = typeof probed?.bridgeVersion === "string" ? probed.bridgeVersion : undefined;
- const compatible = ownerVersion ? sameMajorMinor(ownerVersion, PI_CHROME_VERSION) : false;
+ const compatible = ownerVersion ? daemonVersionCompatible(ownerVersion, PI_CHROME_VERSION) : false;
if (probed && compatible) {
this.mode = "client";
return;
diff --git a/package.json b/package.json
index 1813de4..1765ab5 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "pi-chrome",
- "version": "0.17.1",
+ "version": "0.17.2",
"scripts": {
"version": "node scripts/sync-manifest-version.js",
"prepublishOnly": "node scripts/sync-manifest-version.js"