thinkpool-pair 0.7.293 → 0.7.295

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
@@ -416,6 +416,7 @@ export async function runAccount(SUPABASE_URL, SUPABASE_ANON) {
416
416
  const childIdle = new Map() // room -> bool (last idle report: in the quiet window)
417
417
  const childBetween = new Map() // room -> bool (between turns now — safe to restart per Contract #1)
418
418
  const childPeer = new Map() // room -> bool (a web client is watching this room)
419
+ const readyRooms = new Set() // child subscribed + restored + announced usable room state
419
420
  // Thinkpool Ensemble cross-ROOM router (Tier 1): the supervisor is the only process
420
421
  // that knows every room on this machine, so it routes a child's read_session into the
421
422
  // target room's child and relays the reply back. roomNames feeds list_sessions; pairPending
@@ -562,7 +563,7 @@ export async function runAccount(SUPABASE_URL, SUPABASE_ANON) {
562
563
  // to spawn against — re-read each announce so an add/remove reflects immediately.
563
564
  // Both are additive; older clients ignore them (feature: multi-provider BYOK, slice 1).
564
565
  const PROVIDER_PUBKEY = publicKeyB64()
565
- const pushPresence = () => trackPresence({ name: machine, bridgeId: BRIDGE_ID, version: VERSION, rooms: [...children.keys()], refused: [...refused].map(([code, reason]) => ({ code, reason })), service: runsAsService(), providerPubKey: PROVIDER_PUBKEY, providers: announceProviders(), agents: installedAgentCommands().map((cmd) => ({ cmd })), ts: Date.now() })
566
+ const pushPresence = () => trackPresence({ name: machine, bridgeId: BRIDGE_ID, version: VERSION, rooms: [...children.keys()], readyRooms: [...readyRooms], refused: [...refused].map(([code, reason]) => ({ code, reason })), service: runsAsService(), providerPubKey: PROVIDER_PUBKEY, providers: announceProviders(), agents: installedAgentCommands().map((cmd) => ({ cmd })), ts: Date.now() })
566
567
 
567
568
  // ── Provider registry — the multi-BYOK wire contract (slice 1). ─────────────
568
569
  // AUTH: the account channel `tpacct:<uid>` is created WITHOUT config.private:true,
@@ -895,12 +896,18 @@ export async function runAccount(SUPABASE_URL, SUPABASE_ANON) {
895
896
  console.log(`child_spawn sup=${SUP_ID} room=${room} pid=${child.pid} dir=${dir} had_prior=${alreadyHad}`)
896
897
 
897
898
  children.set(room, child)
899
+ readyRooms.delete(room)
898
900
  childIdle.set(room, false) // unknown until it reports → never restart a fresh child
899
901
  childBetween.set(room, false)
900
902
  childPeer.set(room, false)
901
903
  child.on('message', (m) => {
902
904
  if (!m) return
903
- if (m.t === 'idle') { childIdle.set(room, !!m.idle); childBetween.set(room, m.between != null ? !!m.between : !!m.idle); childPeer.set(room, !!m.webPeer) }
905
+ if (m.t === 'room-ready') {
906
+ readyRooms.add(room)
907
+ console.log(`child_ready sup=${SUP_ID} room=${room} pid=${child.pid}`)
908
+ pushPresence()
909
+ }
910
+ else if (m.t === 'idle') { childIdle.set(room, !!m.idle); childBetween.set(room, m.between != null ? !!m.between : !!m.idle); childPeer.set(room, !!m.webPeer) }
904
911
  else if (m.t === 'apply-update') { applyRequested = true; applyIfIdle() } // user clicked the chip
905
912
  // ── Thinkpool Ensemble cross-ROOM routing ──────────────────────────────
906
913
  // `room` here is the room that sent the message (this closure is per-child).
@@ -993,7 +1000,7 @@ export async function runAccount(SUPABASE_URL, SUPABASE_ANON) {
993
1000
  // ── supervisor child_exit logging (cascade brg-instrument) ─────────────────
994
1001
  console.log(`child_exit sup=${SUP_ID} room=${room} pid=${child?.pid || '?'} code=${code || '?'}`)
995
1002
 
996
- children.delete(room); childIdle.delete(room); childBetween.delete(room); childPeer.delete(room); roomNames.delete(room); roomPartner.delete(room) // re-served on the next tick
1003
+ children.delete(room); readyRooms.delete(room); childIdle.delete(room); childBetween.delete(room); childPeer.delete(room); roomNames.delete(room); roomPartner.delete(room) // re-served on the next tick
997
1004
  // Fail any in-flight cross-room peeks/posts this dead room had ASKED (bus-origin
998
1005
  // entries have askerRoom null and just time out on their own).
999
1006
  for (const [reqId, w] of pairPending) { if (w?.askerRoom === room) pairPending.delete(reqId) }
package/bridge.mjs CHANGED
@@ -348,9 +348,9 @@ if ((!argv[0] || argv[0] === 'setup') && process.stdin.isTTY) {
348
348
  child.on('exit', (code) => process.exit(code == null ? 0 : code))
349
349
  }),
350
350
  serveAccountForeground: async () => { const { runAccount } = await import('./account.mjs'); await runAccount(SUPABASE_URL, SUPABASE_ANON) },
351
- // install / uninstall / restart are NOT terminal — they do their work, print their
352
- // notes, and RETURN so the launcher loop drops you back at the menu (the service
353
- // runs in its own launchd/systemd process; this launcher isn't itself a bridge).
351
+ // install / uninstall are NOT terminal — they do their work, print their notes,
352
+ // and return to the menu. A confirmed update closes the launcher instead: its
353
+ // in-memory package version is stale while the new service runs independently.
354
354
  installService: ({ room = null, agentCmd } = {}) => { svc.installService(room, agentCmd ? [agentCmd] : []) },
355
355
  uninstallService: ({ room = null } = {}) => { svc.uninstallService(room) },
356
356
  restartService: ({ room = null } = {}) => { svc.restartService(room) },
@@ -1117,7 +1117,7 @@ const announce = () => {
1117
1117
  // on a custom provider without the owner's account-channel registry.
1118
1118
  const provNames = providerNameMap()
1119
1119
  const rev = ++announceRev
1120
- return bcast('bridge', {
1120
+ return bcastAwait('bridge', {
1121
1121
  v: 2, name, bridge_id: BRIDGE_ID, started_at: BRIDGE_STARTED_AT, rev, repo: repoLabel, branch: readBranch(),
1122
1122
  // sdkWarn: the auto-pulled agent SDK failed its boot compatibility smoke test —
1123
1123
  // surfaced so the room can show a banner (turns may misbehave; pin a good SDK).
@@ -4459,7 +4459,14 @@ channel
4459
4459
  openTerm({ id: ptyId, cmd: startCmd, args: attachedArgs, attached: !autoAgent })
4460
4460
  }
4461
4461
  }
4462
- announce()
4462
+ await announce()
4463
+ // Account presence must distinguish a spawned child from a usable room.
4464
+ // Signal only after realtime subscribed, durable sessions restored, and the
4465
+ // first authoritative roster was announced. The dashboard keeps the bridge
4466
+ // in "Restarting" until every pre-restart room reaches this boundary.
4467
+ if (process.send && process.env.THINKPOOL_PAIR_ACCOUNT_CHILD === '1') {
4468
+ try { process.send({ t: 'room-ready', room, bridgeId: BRIDGE_ID }) } catch { /* supervisor exited */ }
4469
+ }
4463
4470
  process.stderr.write(headless
4464
4471
  ? `\n ◆ thinkpool — relaying room ${room} (headless). Open terminals from the web UI.\n\n`
4465
4472
  : `\n ◆ thinkpool — sharing "${attachedCmd}"${continuing ? ' (continuing your latest session)' : ''} into room ${room}. Open the web UI and you're both in.\n\n`)
package/launcher.mjs CHANGED
@@ -278,7 +278,16 @@ export async function runLauncher({ actions, io, state = detectState(), refresh
278
278
  if (pick.key === 'quit') return
279
279
  if (pick.key === 'serve') { if (await ensureLoggedIn()) await actions.serveAccountForeground() }
280
280
  else if (pick.key === 'service') { if (await ensureLoggedIn()) { await actions.installService({ room: null }); resync() } }
281
- else if (pick.key === 'restart') { await actions.restartUpdateService({ room: null }); resync() }
281
+ else if (pick.key === 'restart') {
282
+ const updated = await actions.restartUpdateService({ room: null })
283
+ // The launcher process cannot update its own in-memory VERSION. Returning to
284
+ // the menu after a successful service update therefore showed the old launcher
285
+ // version beside the new managed-service version and ended on a misleading
286
+ // "press Enter" pause. The service is already independently running, so close
287
+ // this stale installer process once the OS-level update is confirmed.
288
+ if (updated === true) return
289
+ resync()
290
+ }
282
291
  else if (pick.key === 'uninstall') { await actions.uninstallService({ room: null }); resync() }
283
292
  else if (pick.key === 'settings') await settingsMenu()
284
293
  await io.ask('\n ' + C.dim('press Enter to return to the menu…'))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thinkpool-pair",
3
- "version": "0.7.293",
3
+ "version": "0.7.295",
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": {