thinkpool-pair 0.7.131 → 0.7.133
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 +43 -2
- package/claude-session.mjs +4 -0
- package/package.json +1 -1
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
|
|
@@ -1344,7 +1370,11 @@ function openStructured({ id, model, resume, log, commands, mode, spawnedBy, rol
|
|
|
1344
1370
|
// subagent-block; without cwd it runs outside its worktree; without rolePrompt it loses its
|
|
1345
1371
|
// Flow role. blockSubagents / onLaneDone / onReviewVerdict / onSubmitPlan all derive from
|
|
1346
1372
|
// these, so restoring them restores the whole behavior.
|
|
1347
|
-
|
|
1373
|
+
// model: persist the terminal's active model so a /model switch SURVIVES a bridge
|
|
1374
|
+
// restart. Without this, sessionData omitted it → on restart the resumed session
|
|
1375
|
+
// re-launched on the host default (Opus) regardless of the last switch, and the
|
|
1376
|
+
// switch looked like it "never changed the model" (Max 2026-07-02). Restored below.
|
|
1377
|
+
const sessionData = () => ({ sessionId: entry.session?.sessionId || resume || null, log: entry.log, commands: entry.commands, mode: entry.mode, model: entry.model || null, spawnedBy: entry.spawnedBy, flowSessionId: entry.flowSessionId, flowTaskKey: entry.flowTaskKey, cwd: entry.cwd, rolePrompt: entry.rolePrompt, flowReviewTarget: entry.flowReviewTarget || null, revertTarget: entry.revertTarget || null, reviewSliceRoots: entry.reviewSliceRoots || [], openedAt: entry.openedAt || null })
|
|
1348
1378
|
const persist = () => saveSession(room, id, sessionData())
|
|
1349
1379
|
// Synchronous flush of this session's record. Used on open (so a brand-new session
|
|
1350
1380
|
// has a file under its id BEFORE its first event — surviving a restart inside the
|
|
@@ -1625,6 +1655,7 @@ function openStructured({ id, model, resume, log, commands, mode, spawnedBy, rol
|
|
|
1625
1655
|
return { allow: true, reason: 'no reviewed slice targeted' }
|
|
1626
1656
|
}
|
|
1627
1657
|
: null
|
|
1658
|
+
entry.deferred = !!defer // a lazily-booted idle terminal — the background warmer will boot it
|
|
1628
1659
|
entry.session = startClaudeSession({
|
|
1629
1660
|
cwd: cwd || process.cwd(), model, resume, mode, rolePrompt,
|
|
1630
1661
|
// defer: a restored-idle terminal cold-boots its query only on first use — restore is
|
|
@@ -1728,6 +1759,9 @@ function openStructured({ id, model, resume, log, commands, mode, spawnedBy, rol
|
|
|
1728
1759
|
try { entry.session?.sendTurn('continue') } catch { /* not resumable — stays idle, user can continue manually */ }
|
|
1729
1760
|
process.stderr.write(`\n ◆ auto-resumed interrupted turn (${id.slice(0, 8)}) — sent continue.\n`)
|
|
1730
1761
|
}
|
|
1762
|
+
// A background-warmed terminal is now live — free its warmer slot so the next queued
|
|
1763
|
+
// idle terminal starts booting.
|
|
1764
|
+
if (evt.kind === 'system' && entry.warming) warmComplete(entry)
|
|
1731
1765
|
// Chrome events (mode / usage / clear / compact) are transient state, not
|
|
1732
1766
|
// transcript — broadcast + print them but keep out of the persisted/replayed log.
|
|
1733
1767
|
// 'stalled' is transient turn-liveness state (would replay as a stale frozen
|
|
@@ -2318,7 +2352,7 @@ Output:`
|
|
|
2318
2352
|
// FL-M6 — restore the flow context (id/role/cwd) so an in-flight flow survives a
|
|
2319
2353
|
// bridge restart: the conductor keeps its subagent-block + plan interception, and
|
|
2320
2354
|
// lanes keep their worktree cwd + the ability to mark done.
|
|
2321
|
-
openStructured({ id: rec.id, resume: canResume(rec) ? rec.sessionId : undefined, log: rec.log, commands: rec.commands, mode: rec.mode, spawnedBy: rec.spawnedBy, flowSessionId: rec.flowSessionId, flowTaskKey: rec.flowTaskKey, cwd: rec.cwd, rolePrompt: rec.rolePrompt, reviewSliceRoots: rec.reviewSliceRoots, openedAt: rec.openedAt,
|
|
2355
|
+
openStructured({ id: rec.id, model: rec.model || undefined, resume: canResume(rec) ? rec.sessionId : undefined, log: rec.log, commands: rec.commands, mode: rec.mode, spawnedBy: rec.spawnedBy, flowSessionId: rec.flowSessionId, flowTaskKey: rec.flowTaskKey, cwd: rec.cwd, rolePrompt: rec.rolePrompt, reviewSliceRoots: rec.reviewSliceRoots, openedAt: rec.openedAt,
|
|
2322
2356
|
// Lazy-boot restored terminals that were IDLE + not part of a flow: their transcript
|
|
2323
2357
|
// shows immediately; the query boots on first turn. Mid-turn + flow terminals boot now
|
|
2324
2358
|
// (mid-turn needs auto-resume; flow needs its lane live).
|
|
@@ -2355,6 +2389,13 @@ Output:`
|
|
|
2355
2389
|
// saved-session restore above still runs unconditionally, so a bridge
|
|
2356
2390
|
// restart resumes backgrounded sessions regardless of this guard.
|
|
2357
2391
|
else if (attachedCmd) openStructured({ id: randomUUID() })
|
|
2392
|
+
// Background-warm the lazily-restored idle terminals shortly after restore, so
|
|
2393
|
+
// they're ready before the user clicks them (no first-turn penalty). Delayed so the
|
|
2394
|
+
// eagerly-booted (mid-turn / auto-resumed) sessions get the machine to themselves first.
|
|
2395
|
+
setTimeout(() => {
|
|
2396
|
+
for (const [, e] of sessions) if (e.deferred && !e.warmed && !e.warming && !e.session?.started) warmQueue.push(e)
|
|
2397
|
+
if (warmQueue.length) { process.stderr.write(`\n ◆ background-warming ${warmQueue.length} idle terminal(s) (≤${WARM_CONCURRENCY} at a time)…\n`); pumpWarm() }
|
|
2398
|
+
}, 6000)
|
|
2358
2399
|
}
|
|
2359
2400
|
else {
|
|
2360
2401
|
// Reuse the prior auto-opened PTY id across restarts so a reconnecting
|
package/claude-session.mjs
CHANGED
|
@@ -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.
|