diff --git a/apps/desktop/extensions/ai-sidebar/background.js b/apps/desktop/extensions/ai-sidebar/background.js
index d7e09a7..cced25d 100644
--- a/apps/desktop/extensions/ai-sidebar/background.js
+++ b/apps/desktop/extensions/ai-sidebar/background.js
@@ -338,6 +338,34 @@ async function disablePit() {
await setPitBadge(false);
}
+// The launcher replaces an out-of-date helper in the background right after
+// launch (kill, settle, exec). A click in that window reaches the OLD helper,
+// which has no /pit/* routes and answers 404 {"error":"not-found"}, or reaches
+// nobody at all. Both are transient, so try for a few seconds before giving up,
+// and say "stale helper" rather than "couldn't start" when it never catches up.
+const PIT_START_ATTEMPTS = 5;
+const PIT_START_RETRY_MS = 2000;
+
+function pitHelperIsStale(res) {
+ return !res || res.error === 'not-found' || (!res.started && typeof res.port !== 'number');
+}
+
+async function startPitViaHelper() {
+ let last = null;
+ for (let i = 0; i < PIT_START_ATTEMPTS; i++) {
+ if (i) await new Promise((r) => setTimeout(r, PIT_START_RETRY_MS));
+ try {
+ last = await helperJson('/pit/start', 'POST');
+ } catch (_) {
+ last = { error: 'unreachable' };
+ continue;
+ }
+ if (!pitHelperIsStale(last)) return last; // a pit-capable helper answered, ok or not
+ }
+ if (last && last.error === 'unreachable') return last;
+ return { started: false, error: 'helper-stale' };
+}
+
async function stopPitViaHelper() {
try {
const ctrl = new AbortController();
@@ -356,15 +384,9 @@ chrome.runtime.onMessage.addListener((msg, _sender, sendResponse) => {
sendResponse({ enabled: false, error: 'tor-on' });
return;
}
- let started;
- try {
- started = await helperJson('/pit/start', 'POST');
- } catch (_) {
- sendResponse({ enabled: false, error: 'unreachable' });
- return;
- }
- if (!started || !started.started) {
- sendResponse({ enabled: false, error: (started && started.error) || 'pit-failed' });
+ const started = await startPitViaHelper();
+ if (!started.started) {
+ sendResponse({ enabled: false, error: started.error || 'pit-failed' });
return;
}
await enablePit();
diff --git a/apps/desktop/extensions/ai-sidebar/sidepanel.js b/apps/desktop/extensions/ai-sidebar/sidepanel.js
index 4a775f8..87796b8 100644
--- a/apps/desktop/extensions/ai-sidebar/sidepanel.js
+++ b/apps/desktop/extensions/ai-sidebar/sidepanel.js
@@ -337,8 +337,10 @@ async function togglePit() {
showNetStatus('warn', 'Couldn’t reach the TronBrowser helper. Restart TronBrowser and try again, or run tron upgrade.');
} else if (err === 'pit-port-busy') {
showNetStatus('warn', `Port ${PIT_SOCKS_PORT} on this machine is taken by another program, so the pit resolver couldn’t start.`);
+ } else if (err === 'helper-stale') {
+ showNetStatus('warn', 'TronBrowser’s helper is still the older version, which has no pit resolver. It is normally replaced a few seconds after launch — try 🤘 Pit again; if it keeps failing, quit TronBrowser completely and relaunch it.');
} else {
- showNetStatus('warn', 'The pit resolver couldn’t start. See ~/.tronbrowser/tor-helper.log for the reason.');
+ showNetStatus('warn', `The pit resolver couldn’t start (${safeHost(err || 'pit-failed')}). See ~/.tronbrowser/tor-helper.log for the reason.`);
}
}
} catch (e) {
diff --git a/apps/desktop/launcher/tron-tor-helper b/apps/desktop/launcher/tron-tor-helper
index 36501f6..eb39bca 100755
--- a/apps/desktop/launcher/tron-tor-helper
+++ b/apps/desktop/launcher/tron-tor-helper
@@ -49,7 +49,7 @@ BUNDLED_DIR = os.environ.get("TRON_TOR_BIN_DIR", "")
PIDFILE = os.environ.get("TRON_TOR_PIDFILE", "")
# Bumped whenever the helper protocol/behaviour changes; the launcher kills a
# stale helper so the current version always runs.
-HELPER_VERSION = "3.3.0"
+HELPER_VERSION = "3.3.1"
_lock = threading.Lock()
_proc = None # the running tor subprocess (or None)
_ready = False # True once tor reported Bootstrapped 100%
@@ -559,6 +559,7 @@ class Handler(BaseHTTPRequestHandler):
elif path == "/pit/status":
self._send(200, pit_status())
else:
+ log("no such route: %s (helper v%s)" % (path, HELPER_VERSION))
self._send(404, {"error": "not-found"})
def do_GET(self):
diff --git a/apps/desktop/launcher/tronbrowser b/apps/desktop/launcher/tronbrowser
index cd83ee3..0916c4f 100755
--- a/apps/desktop/launcher/tronbrowser
+++ b/apps/desktop/launcher/tronbrowser
@@ -175,7 +175,7 @@ if [ "$TOR" != "1" ]; then
# running helper isn't this version — otherwise leave a healthy current
# helper alone (don't drop an active Tor session). All backgrounded so the
# kill+settle never holds up the browser launch.
- HELPER_VERSION=3.3.0
+ HELPER_VERSION=3.3.1
(
_pf="$DATA/tor-helper.pid"
_rv="$(curl -fsS --max-time 1 http://127.0.0.1:9061/status 2>/dev/null | sed -n 's/.*"version"[^"]*"\([^"]*\)".*/\1/p')"
diff --git a/docs/moshpit-pit-toggle.md b/docs/moshpit-pit-toggle.md
index ded06c0..72e3ff2 100644
--- a/docs/moshpit-pit-toggle.md
+++ b/docs/moshpit-pit-toggle.md
@@ -75,7 +75,11 @@ would leak every lookup outside Tor, so:
- If the DoH resolver did not answer the probe (offline, slow), the pit stays
**on** and says so; names resolve as soon as it is reachable.
- Failures the sidebar explains: the helper is not running (`tron upgrade`,
- restart), port 9081 is taken, Tor is on.
+ restart), port 9081 is taken, Tor is on, or the helper is still an older
+ version. That last one is a launch race: the launcher replaces an out-of-date
+ helper in the background a moment after the browser starts, and a click in
+ that window reaches the old helper's 404. The background retries `/pit/start`
+ for about ten seconds before reporting it.
## Files