thinkpool-pair 0.7.117 → 0.7.119

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/bridge.mjs +17 -1
  2. package/package.json +1 -1
  3. package/service.mjs +39 -0
package/bridge.mjs CHANGED
@@ -680,10 +680,18 @@ const bcast = (event, payload, ch = channel) => {
680
680
  // Persisted on the host so a rename is cross-device + survives a bridge restart;
681
681
  // every announce carries them so a late-joining or second device sees them too.
682
682
  const termNames = loadNames(room)
683
+ // The SDK's supported-model LIST (value/displayName/description), captured from
684
+ // any session's `models` event. Announced room-level so EVERY device gets it on
685
+ // connect — not just the one that saw the one-shot event (the picker fell back to
686
+ // a hardcoded shorthand set on other devices / after reload). Max 2026-07-02.
687
+ let roomModels = null
683
688
 
684
689
  const announce = () =>
685
690
  bcast('bridge', {
686
691
  v: 2, name, repo: repoLabel, branch: readBranch(),
692
+ // models: the SDK's supported-model list, so the /model picker shows the real
693
+ // set (incl. Fable + version labels) on every device without hardcoding.
694
+ models: roomModels || undefined,
687
695
  // cwd + version: the host's working dir + thinkpool-pair version, shown in
688
696
  // the room's welcome banner. Re-sent per announce so late joiners get them.
689
697
  cwd, version: VERSION,
@@ -724,7 +732,7 @@ const announce = () =>
724
732
  // turnActive). Authoritative — the bridge owns the turn — so the /code
725
733
  // dashboard reads "N agents working" off the roster instead of sniffing
726
734
  // the live code-event stream (which a passive dashboard channel misses).
727
- ...[...sessions.entries()].map(([id, s]) => ({ id, cmd: s.cmd, kind: 'structured', alive: true, busy: s.session?.turnActive ?? false, commands: s.commands, mode: s.mode || undefined, name: termNames[id] || undefined, ...(s.archiveOldestSeq != null ? { oldestSeq: s.archiveOldestSeq } : {}), ...(s.spawnedBy ? { spawned: true, spawnedBy: s.spawnedBy } : {}), ...(s.flowSessionId ? { flowId: s.flowSessionId, flowRole: s.flowTaskKey ? 'lane' : 'conductor' } : {}) })),
735
+ ...[...sessions.entries()].map(([id, s]) => ({ id, cmd: s.cmd, kind: 'structured', alive: true, busy: s.session?.turnActive ?? false, commands: s.commands, mode: s.mode || undefined, name: termNames[id] || undefined, model: s.model || undefined, ...(s.archiveOldestSeq != null ? { oldestSeq: s.archiveOldestSeq } : {}), ...(s.spawnedBy ? { spawned: true, spawnedBy: s.spawnedBy } : {}), ...(s.flowSessionId ? { flowId: s.flowSessionId, flowRole: s.flowTaskKey ? 'lane' : 'conductor' } : {}) })),
728
736
  ],
729
737
  })
730
738
 
@@ -1620,6 +1628,14 @@ function openStructured({ id, model, resume, log, commands, mode, spawnedBy, rol
1620
1628
  // (normal, spawn, flow lane, cross-room) since they all flow through onEvent.
1621
1629
  { const busyNow = entry.session?.turnActive ?? false
1622
1630
  if (busyNow !== entry._busyAnn) { entry._busyAnn = busyNow; announce() } }
1631
+ // Capture this session's current MODEL (resolved full id, with version — from
1632
+ // system/result `model` or usage `ctx.model`) and the SDK's supported-model
1633
+ // LIST (from the `models` event) so the announce carries them to EVERY device.
1634
+ // Re-announce on change so a switch / late device sees it without a turn (the
1635
+ // old localStorage cache was per-device; branch showed on every other device).
1636
+ { const m = evt.model || evt.ctx?.model
1637
+ if (m && m !== entry.model) { entry.model = m; announce() }
1638
+ if (evt.kind === 'models' && Array.isArray(evt.models) && evt.models.length) { roomModels = evt.models; announce() } }
1623
1639
  // FL-B2 — fold this flow lane's completed-turn output tokens into its budget so the
1624
1640
  // autopilot cap can halt the next wave before overrun (output_tokens = the billed
1625
1641
  // reasoning+output spend the indicator already tracks; conservative enough for a guard).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thinkpool-pair",
3
- "version": "0.7.117",
3
+ "version": "0.7.119",
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/service.mjs CHANGED
@@ -188,6 +188,41 @@ WantedBy=default.target
188
188
  throw new Error(`unsupported platform: ${platform}`)
189
189
  }
190
190
 
191
+ // Blocking sleep (installService runs synchronously from the interactive menu; the
192
+ // user is watching, so we WANT to wait + report the real outcome, not fire-and-forget).
193
+ const sleepMs = (ms) => { try { execSync(`sleep ${(ms / 1000).toFixed(2)}`) } catch { /* noop */ } }
194
+ // Which thinkpool-pair versions are running right now (the account supervisor shows as
195
+ // `npm exec thinkpool-pair@<ver>`). Returns a Set of version strings.
196
+ const runningPairVersions = () => {
197
+ try {
198
+ const out = execSync(`ps ax -o command= 2>/dev/null | grep -oE 'thinkpool-pair@[0-9]+\\.[0-9]+\\.[0-9]+' | sort -u`, { encoding: 'utf8', shell: '/bin/bash' })
199
+ return new Set(out.split('\n').map((l) => l.trim().split('@')[1]).filter(Boolean))
200
+ } catch { return new Set() }
201
+ }
202
+ const darwinSvcLoaded = (lbl) => { try { execSync(`launchctl print gui/$(id -u)/${lbl}`, { stdio: 'ignore' }); return true } catch { return false } }
203
+ // After the reload, CONFIRM the new pinned version is actually running — the shell
204
+ // post can report "loaded" while an ORPHANED old child bridge (that bootout didn't
205
+ // reap) keeps serving the OLD version, which is the "Restart & update never works"
206
+ // bug (Max 2026-07-02). If the new version isn't up, SIGKILL lingering ROOM child
207
+ // bridges (never the no-arg menu process) + re-bootstrap, then report the truth.
208
+ function verifyDarwinReload(lbl, file, version) {
209
+ const want = String(version)
210
+ const up = () => darwinSvcLoaded(lbl) && runningPairVersions().has(want)
211
+ for (let i = 0; i < 25 && !up(); i++) sleepMs(600) // fresh (cache-wiped) fetch takes a bit
212
+ if (up()) { process.stderr.write(` ◆ verified: bridge is now running v${want}.\n`); return true }
213
+ process.stderr.write(` ⚠ v${want} not up yet — clearing stale bridges and re-bootstrapping…\n`)
214
+ try { execSync(`launchctl bootout gui/$(id -u)/${lbl} 2>/dev/null || true`, { stdio: 'ignore' }) } catch { /* noop */ }
215
+ // Kill orphaned ROOM child bridges (`bridge.mjs <ROOM>`), which bootout may not have
216
+ // reaped. The uppercase room-code guard never matches the no-arg / `setup` menu.
217
+ try { execSync(`pkill -9 -f 'thinkpool-pair/bridge.mjs [A-Z0-9]' 2>/dev/null || true`, { stdio: 'ignore', shell: '/bin/bash' }) } catch { /* noop */ }
218
+ sleepMs(500)
219
+ try { execSync(`launchctl enable gui/$(id -u)/${lbl} 2>/dev/null || true`, { stdio: 'ignore' }) } catch { /* noop */ }
220
+ for (let i = 0; i < 25 && !up(); i++) { try { execSync(`launchctl bootstrap gui/$(id -u) "${file}" 2>/dev/null || true`, { stdio: 'ignore' }) } catch { /* noop */ } sleepMs(600) }
221
+ if (up()) { process.stderr.write(` ◆ recovered: bridge is now running v${want}.\n`); return true }
222
+ process.stderr.write(` ⚠ could NOT confirm v${want} is running. Recover by hand:\n launchctl bootout gui/$(id -u)/${lbl} ; pkill -9 -f thinkpool-pair ; launchctl bootstrap gui/$(id -u) "${file}"\n`)
223
+ return false
224
+ }
225
+
191
226
  // staleProof defaults ON for every real install: even a FIRST install can be launched from
192
227
  // a stale npx cache (the launcher itself is npx-resolved), so without the cache-wiping
193
228
  // wrapper a fresh `install-service` could pin a stale build just like the update path did.
@@ -203,6 +238,10 @@ export function installService(room, cmdArgs = [], { autoUpdate = false, version
203
238
  for (const cmd of a.post) {
204
239
  try { execSync(cmd, { stdio: 'inherit' }) } catch (e) { process.stderr.write(` ⚠ "${cmd}" failed: ${e.message}\n`) }
205
240
  }
241
+ // Darwin: confirm the reload actually put the NEW version live, self-heal if not.
242
+ // Only for a PINNED (re)install where we know the exact version to look for — an
243
+ // autoUpdate (@latest) install has no fixed version number to verify against.
244
+ if (process.platform === 'darwin' && !autoUpdate) verifyDarwinReload(label(room), a.file, version)
206
245
  // Be honest about the cadence — it is NOT instant. The supervisor polls npm
207
246
  // roughly every 30 min (THINKPOOL_PAIR_UPDATE_INTERVAL) and only restarts once
208
247
  // every served session is idle, so a just-published version can take a while.