thinkpool-pair 0.7.147 → 0.7.149

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.
Files changed (3) hide show
  1. package/account.mjs +39 -18
  2. package/bridge.mjs +15 -2
  3. package/package.json +1 -1
package/account.mjs CHANGED
@@ -308,28 +308,49 @@ export async function runAccount(SUPABASE_URL, SUPABASE_ANON) {
308
308
  // just stops — restart is meaningful for the always-on bridge. Old bridges (pre-0.7.60)
309
309
  // don't subscribe to this event, so the dashboard button is a harmless no-op there.
310
310
  acct.on('broadcast', { event: 'restart' }, () => {
311
- const supervised = process.env.THINKPOOL_PAIR_AUTOUPDATE === '1'
312
- process.stderr.write('\n ◇ restart requested from the dashboard — bouncing bridge (sessions resume).\n')
313
311
  restarting = true
314
- setTimeout(() => {
312
+ // The dashboard "Restart bridge" button ALSO updates to the newest published version
313
+ // (Max, 2026-07-03). If an OS service manages this bridge, spawn a DETACHED updater
314
+ // that re-pins the service to @latest and reloads it (install-service resolves the
315
+ // newest version → rewrites the plist/unit → bootout+bootstrap). Detached → its own
316
+ // session, so it SURVIVES this process being booted out (the reload kills us); it
317
+ // inherits our env, whose PATH carries nvm's bin, so npx/node resolve — a bare launchd
318
+ // PATH would NOT (the 2026-07-03 gotcha that made a manual updater silently no-op).
319
+ // The reload IS the bounce; sessions resume from disk on the new version. Best-effort:
320
+ // if there's no service or npm is unreachable, we fall through to the plain bounce, so
321
+ // the button is never dead — and even a failed update degrades to today's behaviour.
322
+ const plainBounce = (delay) => setTimeout(() => {
315
323
  try { releaseSingleton() } catch { /* noop */ }
316
- // A SUPERVISED bridge (launchd KeepAlive / systemd Restart=always / the
317
- // bridge.mjs --supervise wrapper) is respawned by its supervisor on exit, so a
318
- // bare exit(1) bounces cleanly. A plain FOREGROUND bridge has NO supervisor —
319
- // there exit = death and the dashboard "Restart" becomes a footgun (2026-06-25:
320
- // Max's manual `npx thinkpool-pair` died on Restart and never came back). So when
321
- // unsupervised, re-spawn ourselves DETACHED *after* releasing the machine lock
322
- // (the child's acquireSingleton() then sees no live owner and takes it). The child
323
- // has no TTY → bridge.mjs routes a bare invocation straight to account mode. Gated
324
- // on `supervised` so we never double-spawn (supervisor + self) into two bridges.
325
- if (!supervised) {
326
- try {
327
- spawn(process.execPath, [process.argv[1]], { detached: true, stdio: 'ignore', env: process.env }).unref()
328
- } catch { /* respawn failed → exit anyway, no worse than the old behaviour */ }
324
+ // Unsupervised FOREGROUND bridge (no launchd/systemd) → re-spawn ourselves DETACHED
325
+ // after releasing the lock, so "Restart" isn't a footgun that never comes back
326
+ // (2026-06-25). A supervised bridge is respawned by its supervisor on exit(1).
327
+ if (process.env.THINKPOOL_PAIR_AUTOUPDATE !== '1') {
328
+ try { spawn(process.execPath, [process.argv[1]], { detached: true, stdio: 'ignore', env: process.env }).unref() } catch { /* noop */ }
329
329
  }
330
330
  process.exit(1)
331
- }, 2500)
332
- process.kill(process.pid, 'SIGTERM')
331
+ }, delay)
332
+ ;(async () => {
333
+ let updating = false
334
+ try {
335
+ const svc = await import('./service.mjs')
336
+ if (svc.serviceActive(null)) {
337
+ spawn('npx', ['-y', 'thinkpool-pair@latest', 'install-service'], { detached: true, stdio: 'ignore', env: process.env }).unref()
338
+ updating = true
339
+ }
340
+ } catch { /* no service / spawn failed → plain bounce below */ }
341
+ process.stderr.write(updating
342
+ ? '\n ◇ update + restart requested from the dashboard — pulling the newest bridge and reloading; sessions resume.\n'
343
+ : '\n ◇ restart requested from the dashboard — bouncing bridge (sessions resume).\n')
344
+ if (updating) {
345
+ // The updater's bootout replaces us (single clean bounce). Long backstop: if the
346
+ // update never lands (npm down / install failed), bounce the current version so the
347
+ // button isn't dead — the updater normally boots us out well before this fires.
348
+ plainBounce(60000)
349
+ return
350
+ }
351
+ plainBounce(2500)
352
+ process.kill(process.pid, 'SIGTERM')
353
+ })()
333
354
  })
334
355
  // Throttled so a realtime reconnect storm can't machine-gun track() into the
335
356
  // per-client presence rate limit (ClientPresenceRateLimitReached → channel closed
package/bridge.mjs CHANGED
@@ -1840,8 +1840,21 @@ function openStructured({ id, model, resume, log, commands, mode, spawnedBy, rol
1840
1840
  // mid-turn) flash a brief "Compacting…" beat so it isn't silent — we only learn of it
1841
1841
  // post-hoc, so there's no real duration to animate (Max, 2026-07-02).
1842
1842
  if (evt.kind === 'compaction') {
1843
- if (entry.compacting) evt.by = entry.compactBy || null
1844
- else if (evt.trigger === 'auto') {
1843
+ if (entry.compacting) {
1844
+ // MANUAL /compact: attribute the recap card, then CLEAR the "Compacting…"
1845
+ // indicator right here — the compact_boundary milestone IS the "done" signal.
1846
+ // We used to wait for the turn's `result` (below), but a real /compact re-inits
1847
+ // the SDK session, so the result can land after entry.compacting was lost →
1848
+ // the indicator hung forever and kept counting (Max, 2026-07-03). The milestone
1849
+ // is emitted with compacting still true (that's how we attribute `by`), so it's
1850
+ // the reliable place to stop it. The result handler below stays as the backstop
1851
+ // for a NO-OP /compact ("nothing to compact"), which emits a result but no
1852
+ // compaction milestone.
1853
+ evt.by = entry.compactBy || null
1854
+ entry.compacting = false
1855
+ entry.compactBy = null
1856
+ bcast('code-event', { term: id, evt: { kind: 'compact', status: 'done', ts: Date.now() } })
1857
+ } else if (evt.trigger === 'auto') {
1845
1858
  bcast('code-event', { term: id, evt: { kind: 'compact', status: 'start', ts: Date.now() } })
1846
1859
  setTimeout(() => { bcast('code-event', { term: id, evt: { kind: 'compact', status: 'done', ts: Date.now() } }) }, 2500)
1847
1860
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thinkpool-pair",
3
- "version": "0.7.147",
3
+ "version": "0.7.149",
4
4
  "description": "Share a local coding-agent CLI (Claude Code, Codex, Gemini, Aider, …) into a ThinkPool Code room, live.",
5
5
  "type": "module",
6
6
  "bin": {