thinkpool-pair 0.7.33 → 0.7.34

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/account.mjs CHANGED
@@ -208,8 +208,7 @@ export async function runAccount(SUPABASE_URL, SUPABASE_ANON) {
208
208
  process.stderr.write(`\n ◆ thinkpool-pair — account mode · ${email}\n Auto-serving your sessions from ${DEFAULT_DIR}\n (point one at another repo: npx thinkpool-pair bind <code> <dir>)\n${svcHint}`)
209
209
 
210
210
  const children = new Map() // room -> child process
211
- const childIdle = new Map() // room -> bool (last idle report: in the quiet window)
212
- const childBetween = new Map() // room -> bool (between turns now — safe to restart per Contract #1)
211
+ const childIdle = new Map() // room -> bool (last idle report; unknown = not idle)
213
212
  const childPeer = new Map() // room -> bool (a web client is watching this room)
214
213
  let applyRequested = false // a user clicked "apply" in some room (Slice 3)
215
214
  let pendingUpdate = null // newest published version once the poll sees it
@@ -283,14 +282,13 @@ export async function runAccount(SUPABASE_URL, SUPABASE_ANON) {
283
282
  const child = spawn(process.execPath, [BRIDGE, room, '--headless', '--auto=claude'], { cwd: dir, stdio: ['inherit', 'inherit', 'inherit', 'ipc'], env })
284
283
  children.set(room, child)
285
284
  childIdle.set(room, false) // unknown until it reports → never restart a fresh child
286
- childBetween.set(room, false)
287
285
  childPeer.set(room, false)
288
286
  child.on('message', (m) => {
289
287
  if (!m) return
290
- if (m.t === 'idle') { childIdle.set(room, !!m.idle); childBetween.set(room, m.between != null ? !!m.between : !!m.idle); childPeer.set(room, !!m.webPeer) }
288
+ if (m.t === 'idle') { childIdle.set(room, !!m.idle); childPeer.set(room, !!m.webPeer) }
291
289
  else if (m.t === 'apply-update') { applyRequested = true; applyIfIdle() } // user clicked the chip
292
290
  })
293
- child.on('exit', () => { children.delete(room); childIdle.delete(room); childBetween.delete(room); childPeer.delete(room) }) // re-served on the next tick
291
+ child.on('exit', () => { children.delete(room); childIdle.delete(room); childPeer.delete(room) }) // re-served on the next tick
294
292
  // If an update is already pending, tell the freshly-served child immediately so
295
293
  // its room shows the "update ready" chip without waiting for the next poll.
296
294
  if (pendingUpdate) { try { child.send({ t: 'update-available', version: pendingUpdate }) } catch { /* child not ready */ } }
@@ -319,7 +317,7 @@ export async function runAccount(SUPABASE_URL, SUPABASE_ANON) {
319
317
  // child idle (between turns) AND either a user clicked apply OR nobody's watching
320
318
  // (unattended fallback). The predicate is unit-tested (tests/update-gate.test.mjs).
321
319
  const applyIfIdle = () => {
322
- if (!isSafeToRestart({ pendingUpdate, stopping, childIdle, childBetween, childPeer, requested: applyRequested })) return
320
+ if (!isSafeToRestart({ pendingUpdate, stopping, childIdle, childPeer, requested: applyRequested })) return
323
321
  process.stderr.write(`\n ◆ thinkpool-pair ${pendingUpdate} ready (running ${VERSION}) — restarting account bridge to update; sessions resume.\n`)
324
322
  stop('SIGTERM') // graceful: children persist + leave presence; exit 0 → service reruns @latest
325
323
  }
package/bridge.mjs CHANGED
@@ -1059,15 +1059,12 @@ if (process.env.THINKPOOL_PAIR_AUTOUPDATE === '1' && VERSION) {
1059
1059
  const POLL_MS = Math.max(60, parseInt(process.env.THINKPOOL_PAIR_UPDATE_INTERVAL, 10) || 1800) * 1000
1060
1060
  const IDLE_MS = Math.max(10, parseInt(process.env.THINKPOOL_PAIR_UPDATE_IDLE, 10) || 90) * 1000
1061
1061
  // pendingUpdate is module-level now (shared with surfaceUpdate + the room chip).
1062
- // Between turns = safe to restart per Contract #1 (no turn in flight, no bytes
1063
- // mid-flush). Idle = that PLUS the quiet window — the extra conservatism only the
1064
- // unattended auto-apply needs; an explicit click restarts on between-turns alone.
1065
- const betweenTurns = () => {
1062
+ const isIdle = () => {
1066
1063
  if (turnInFlight(sessions)) return false // a structured turn is live
1064
+ if (Date.now() - lastActivity < IDLE_MS) return false
1067
1065
  for (const t of terms.values()) if (t.buf) return false // bytes mid-flush
1068
1066
  return true
1069
1067
  }
1070
- const isIdle = () => betweenTurns() && (Date.now() - lastActivity >= IDLE_MS)
1071
1068
  // Newer? Compare the numeric x.y.z core; ignore prerelease tails to stay safe.
1072
1069
  const isNewer = (latest, cur) => {
1073
1070
  const core = (v) => String(v).split('-')[0].split('.').map((n) => parseInt(n, 10) || 0)
@@ -1086,13 +1083,11 @@ if (process.env.THINKPOOL_PAIR_AUTOUPDATE === '1' && VERSION) {
1086
1083
  } catch { return null } finally { clearTimeout(to) }
1087
1084
  }
1088
1085
  const applyIfIdle = () => {
1089
- if (!pendingUpdate) return
1090
- // Explicit "apply" go at the next between-turns gap (the click means the user
1091
- // opted in; don't make them wait out the quiet window). Unattended (no click)
1092
- // require the full idle window AND nobody watching, so a headless box self-heals
1093
- // without ever restarting under someone who's mid-session.
1094
- if (applyRequested) { if (!betweenTurns()) return }
1095
- else if (!isIdle() || webPeerPresent()) return
1086
+ if (!pendingUpdate || !isIdle()) return
1087
+ // Slice 3: when a web client is watching, wait for their "apply" click instead
1088
+ // of restarting under them. With nobody present (cloud VM / closed tab) the
1089
+ // unattended fallback still auto-applies at idle so headless boxes self-heal.
1090
+ if (!applyRequested && webPeerPresent()) return
1096
1091
  process.stderr.write(`\n ◆ thinkpool-pair ${pendingUpdate} ready (running ${VERSION}) — restarting to update; the session resumes.\n`)
1097
1092
  shutdown(0, false) // clean teardown (no farewell banner); service reruns @latest, session-store resumes
1098
1093
  }
@@ -1113,19 +1108,17 @@ if (process.env.THINKPOOL_PAIR_AUTOUPDATE === '1' && VERSION) {
1113
1108
  // supervisor restart re-resolves @latest. Instead we report our idle state so
1114
1109
  // the supervisor can pick a moment when EVERY child is idle to restart-update.
1115
1110
  const IDLE_MS = Math.max(10, parseInt(process.env.THINKPOOL_PAIR_UPDATE_IDLE, 10) || 90) * 1000
1116
- const betweenTurns = () => {
1111
+ const idleNow = () => {
1117
1112
  if (turnInFlight(sessions)) return false // a structured turn is live
1113
+ if (Date.now() - lastActivity < IDLE_MS) return false
1118
1114
  for (const t of terms.values()) if (t.buf) return false // bytes mid-flush
1119
1115
  return true
1120
1116
  }
1121
- const idleNow = () => betweenTurns() && (Date.now() - lastActivity >= IDLE_MS)
1122
1117
  // The supervisor owns updates (children never self-update). It IPCs us when a newer
1123
- // version is out → surface the chip; we report `between` (safe to restart now, per
1124
- // Contract #1) + `idle` (the quiet window, for the unattended fallback) + whether a
1125
- // web peer is present, so the supervisor restarts on an explicit click between turns
1126
- // and otherwise waits for true idle with nobody watching (Slice 3).
1118
+ // version is out → surface the chip; we report idle + whether a web peer is present
1119
+ // so it restarts only between turns and only when nobody is mid-click (Slice 3).
1127
1120
  process.on('message', (m) => { if (m && m.t === 'update-available') surfaceUpdate(m.version) })
1128
- setInterval(() => { try { process.send({ t: 'idle', idle: idleNow(), between: betweenTurns(), webPeer: webPeerPresent() }) } catch { /* parent gone */ } }, 5000).unref()
1121
+ setInterval(() => { try { process.send({ t: 'idle', idle: idleNow(), webPeer: webPeerPresent() }) } catch { /* parent gone */ } }, 5000).unref()
1129
1122
  } else if (VERSION) {
1130
1123
  // Foreground run (not the supervised service): it can't self-restart, but
1131
1124
  // nudge once if a newer version is out so a relaunch with @latest picks it up.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thinkpool-pair",
3
- "version": "0.7.33",
3
+ "version": "0.7.34",
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": {
package/update-gate.mjs CHANGED
@@ -11,18 +11,10 @@
11
11
  // child is idle (a child reports idle=false while a turn is in flight — that's what
12
12
  // keeps Contract #1), AND either a user requested apply (clicked the chip) OR — the
13
13
  // unattended fallback — no child has a web peer present (nobody is there to click).
14
- export function isSafeToRestart({ pendingUpdate, stopping = false, childIdle, childBetween, childPeer, requested = false }) {
14
+ export function isSafeToRestart({ pendingUpdate, stopping = false, childIdle, childPeer, requested = false }) {
15
15
  if (!pendingUpdate || stopping) return false
16
- if (requested) {
17
- // Explicit "apply" restart at the next BETWEEN-TURNS gap (no turn in flight),
18
- // NOT the full quiet window — in an active room that 90s of silence never comes,
19
- // so the click would otherwise do nothing. Fall back to childIdle when a child
20
- // doesn't report `between` (older child during the mixed-version update itself).
21
- const gate = childBetween instanceof Map ? childBetween : childIdle
22
- for (const ready of gate.values()) if (!ready) return false // any busy child → wait (Contract #1)
23
- return true
24
- }
25
- for (const idle of childIdle.values()) if (!idle) return false // unattended: full idle window
16
+ for (const idle of childIdle.values()) if (!idle) return false // any busy child → wait (between-turns)
17
+ if (requested) return true // user clicked apply + all idle go
26
18
  for (const present of childPeer.values()) if (present) return false // someone's watching, no click → wait
27
19
  return true // unattended: idle + nobody watching → auto-apply
28
20
  }