thinkpool-pair 0.7.131 → 0.7.132

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/bridge.mjs CHANGED
@@ -656,6 +656,32 @@ const sessions = new Map()
656
656
  let attachedId = null
657
657
  let shuttingDown = false
658
658
 
659
+ // Background warmer (2026-07-02): after a restart, proactively cold-boot the lazily-
660
+ // restored IDLE terminals — ready-gated to ≤WARM_CONCURRENCY at once so there's no
661
+ // boot-storm (the contention that made restore take minutes) — so they're warm BEFORE
662
+ // the user clicks them, erasing the lazy-boot first-turn penalty. A user turn to a
663
+ // still-cold terminal boots it on demand as the fallback. warmComplete frees a slot when
664
+ // a terminal's init `system` event proves it live (called from onEvent).
665
+ const warmQueue = []
666
+ let warmingCount = 0
667
+ const WARM_CONCURRENCY = 3
668
+ function pumpWarm() {
669
+ while (warmingCount < WARM_CONCURRENCY && warmQueue.length) {
670
+ const e = warmQueue.shift()
671
+ if (!e || e.warmed || e.warming || !e.session || e.session.started) continue
672
+ e.warming = true; warmingCount++
673
+ try { e.session.warm?.() } catch { e.warming = false; warmingCount--; continue }
674
+ // Free the slot if a warm never signals ready (stuck boot) so the queue still drains.
675
+ e.warmTimer = setTimeout(() => { if (e.warming) { e.warming = false; warmingCount = Math.max(0, warmingCount - 1); pumpWarm() } }, 100000)
676
+ }
677
+ }
678
+ const warmComplete = (e) => {
679
+ if (!e || !e.warming) return
680
+ e.warming = false; e.warmed = true; warmingCount = Math.max(0, warmingCount - 1)
681
+ try { clearTimeout(e.warmTimer) } catch { /* noop */ }
682
+ pumpWarm()
683
+ }
684
+
659
685
  // One send door for every broadcast. supabase-js ≥2.10x deprecation-warns
660
686
  // (console.warn) whenever send() silently falls back to REST because the
661
687
  // socket can't push — at the flush timer's 28×/s that's a wall of warnings
@@ -1625,6 +1651,7 @@ function openStructured({ id, model, resume, log, commands, mode, spawnedBy, rol
1625
1651
  return { allow: true, reason: 'no reviewed slice targeted' }
1626
1652
  }
1627
1653
  : null
1654
+ entry.deferred = !!defer // a lazily-booted idle terminal — the background warmer will boot it
1628
1655
  entry.session = startClaudeSession({
1629
1656
  cwd: cwd || process.cwd(), model, resume, mode, rolePrompt,
1630
1657
  // defer: a restored-idle terminal cold-boots its query only on first use — restore is
@@ -1728,6 +1755,9 @@ function openStructured({ id, model, resume, log, commands, mode, spawnedBy, rol
1728
1755
  try { entry.session?.sendTurn('continue') } catch { /* not resumable — stays idle, user can continue manually */ }
1729
1756
  process.stderr.write(`\n ◆ auto-resumed interrupted turn (${id.slice(0, 8)}) — sent continue.\n`)
1730
1757
  }
1758
+ // A background-warmed terminal is now live — free its warmer slot so the next queued
1759
+ // idle terminal starts booting.
1760
+ if (evt.kind === 'system' && entry.warming) warmComplete(entry)
1731
1761
  // Chrome events (mode / usage / clear / compact) are transient state, not
1732
1762
  // transcript — broadcast + print them but keep out of the persisted/replayed log.
1733
1763
  // 'stalled' is transient turn-liveness state (would replay as a stale frozen
@@ -2355,6 +2385,13 @@ Output:`
2355
2385
  // saved-session restore above still runs unconditionally, so a bridge
2356
2386
  // restart resumes backgrounded sessions regardless of this guard.
2357
2387
  else if (attachedCmd) openStructured({ id: randomUUID() })
2388
+ // Background-warm the lazily-restored idle terminals shortly after restore, so
2389
+ // they're ready before the user clicks them (no first-turn penalty). Delayed so the
2390
+ // eagerly-booted (mid-turn / auto-resumed) sessions get the machine to themselves first.
2391
+ setTimeout(() => {
2392
+ for (const [, e] of sessions) if (e.deferred && !e.warmed && !e.warming && !e.session?.started) warmQueue.push(e)
2393
+ if (warmQueue.length) { process.stderr.write(`\n ◆ background-warming ${warmQueue.length} idle terminal(s) (≤${WARM_CONCURRENCY} at a time)…\n`); pumpWarm() }
2394
+ }, 6000)
2358
2395
  }
2359
2396
  else {
2360
2397
  // Reuse the prior auto-opened PTY id across restarts so a reconnecting
@@ -861,6 +861,10 @@ export function startClaudeSession({ cwd, model, resume, env, mode: initialMode
861
861
  // A lazy (restored-idle) session cold-boots the query on its first turn. input.push
862
862
  // is queue-backed, so the pushed turn buffers and runs once the query is ready.
863
863
  sendTurn(text) { if (!closed) { if (!started) runQuery(); turnActive = true; sawSuggestion = false; if (sugTimer) { clearTimeout(sugTimer); sugTimer = null } lastEvtTs = Date.now(); stalledSent = false; input.push([{ type: 'text', text: String(text) }, { type: 'text', text: `<system-reminder>\n${TP_ROOM_REMINDER}\n</system-reminder>` }]) } },
864
+ // Cold-boot the query WITHOUT sending a turn — the background warmer calls this on
865
+ // lazily-restored idle terminals so they're ready before the user clicks them.
866
+ warm() { if (!started && !closed) runQuery() },
867
+ get started() { return started },
864
868
  // Set the permission mode — Claude Code's ⇧⇥ cycle. setPermissionMode is a
865
869
  // streaming control request (drives plan-mode behaviour SDK-side); the local
866
870
  // `mode` drives our PreToolUse auto-approve policy. Echo so the room syncs.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thinkpool-pair",
3
- "version": "0.7.131",
3
+ "version": "0.7.132",
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": {