thinkpool-pair 0.7.300 → 0.7.302

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
@@ -17,7 +17,7 @@ import { saveAuth, loadAuth, loadDirs, bindDir, loadServed, rememberServed, load
17
17
  import { isSafeToRestart } from './update-gate.mjs'
18
18
  import { makeThrottledTrack, presenceSelfEchoVerdict } from './presence.mjs'
19
19
  import { publicKeyB64, announceProviders, listProviders, addProvider, addProviderModel, removeProvider, unseal } from './providers.mjs'
20
- import { supervisorServes } from './serve-consent.mjs'
20
+ import { supervisorServes, supervisorRoomsToStop } from './serve-consent.mjs'
21
21
  import { resolveServeDir } from './serve-dir.mjs'
22
22
  import { pairKeyFor, pairTopic, CROSSROOM_BUS } from './cross-terminal.mjs'
23
23
  import { installedAgentCommands } from './agent-detect.mjs'
@@ -798,6 +798,7 @@ export async function runAccount(SUPABASE_URL, SUPABASE_ANON) {
798
798
  return
799
799
  }
800
800
  let rooms = []
801
+ let discoverySucceeded = false
801
802
  const me = session.user.id
802
803
  try {
803
804
  // Discovery (Contract C-CODE-2, owner-only since 2026-07-06): a host bridges ONLY
@@ -805,20 +806,38 @@ export async function runAccount(SUPABASE_URL, SUPABASE_ANON) {
805
806
  // keeps rooms you merely JOINED as a guest out of auto-serve — your machine never
806
807
  // spawns into another owner's session (cross-person serving removed; pairing is
807
808
  // read-only peer peek, not hosting). The web watches tpacct:<session-owner>.
808
- const { data } = await sb.from('code_sessions').select('code,name,last_active_at,owner_id,participants').eq('owner_id', me).order('last_active_at', { ascending: false }).limit(20)
809
+ const { data, error } = await sb.from('code_sessions')
810
+ .select('code,name,last_active_at,owner_id,participants,closed_at')
811
+ .eq('owner_id', me)
812
+ .is('closed_at', null)
813
+ .order('last_active_at', { ascending: false })
814
+ .limit(20)
815
+ if (error) throw error
809
816
  rooms = data || []
817
+ discoverySucceeded = true
810
818
  // Prune refused entries for rooms we can no longer serve — owned OR granted (only
811
819
  // on a SUCCESSFUL fetch, so a transient query error never wipes the set).
812
820
  const servableCodes = new Set(rooms.map((r) => r.code))
813
821
  for (const c of [...refused.keys()]) if (!servableCodes.has(c)) refused.delete(c)
814
822
  } catch { /* transient — keep existing children, retry next tick */ }
823
+ // The realtime shutdown broadcast is an acceleration, never the lifecycle
824
+ // authority. Reconcile every successful discovery read so a bridge that missed
825
+ // `session-deleted` still stops after its room is closed, deleted, or transferred.
826
+ if (discoverySucceeded) {
827
+ for (const room of supervisorRoomsToStop({ rooms, childRooms: children.keys(), myUid: me })) {
828
+ const child = children.get(room)
829
+ if (!child) continue
830
+ console.log(`child_retire sup=${SUP_ID} room=${room} pid=${child.pid || '?'} reason=not-active-owned`)
831
+ try { child.kill('SIGTERM') } catch { /* child exit reconciliation retries next tick */ }
832
+ }
833
+ }
815
834
  const dirs = loadDirs()
816
835
  const served = loadServed()
817
836
  for (const r of rooms) {
818
837
  const room = r.code
819
838
  if (!room || children.has(room)) { if (room) refused.delete(room); continue } // served → not refused
820
839
  // Defense-in-depth behind the owner-only query above: serve iff we OWN the room.
821
- if (!supervisorServes({ ownerId: r.owner_id, myUid: me })) continue
840
+ if (!supervisorServes({ ownerId: r.owner_id, myUid: me, closedAt: r.closed_at })) continue
822
841
  // Resolve the serve dir: explicit bind > last-served memory > launch dir. REFUSE
823
842
  // to auto-serve an unbound room from $HOME — that strips the agent's resume context
824
843
  // (cwd-keyed SDK sessions) and shows your home folder as the repo (FVIHV1DE).
package/codex-session.mjs CHANGED
@@ -577,14 +577,13 @@ export function startCodexSession({ cwd, model, effort: initialEffort = 'high',
577
577
  appServerThreadReady = true
578
578
  pushMappedEvent({ type: 'thread.started', thread_id: sessionId })
579
579
  return true
580
- } catch (error) {
580
+ } catch {
581
581
  // Fallback is safe only here: no turn/start request has been accepted, so
582
582
  // there is no possibility of running the same prompt twice.
583
583
  appServerDisabled = true
584
584
  try { appServer?.end() } catch { /* noop */ }
585
585
  appServer = null
586
586
  appServerThreadReady = false
587
- note(`Codex App Server unavailable; using codex exec: ${error?.message || error}`)
588
587
  return false
589
588
  }
590
589
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thinkpool-pair",
3
- "version": "0.7.300",
3
+ "version": "0.7.302",
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/serve-consent.mjs CHANGED
@@ -38,9 +38,29 @@ export function serveDecision ({ ownerId, myUid }) {
38
38
  // defense-in-depth per-row check behind the supervisor's owner-only `.eq(owner_id, me)`
39
39
  // query — a broad RLS row (owned OR merely participant) must still resolve to OWNED
40
40
  // before a child is spawned. Contract C-CODE-2 (owner-only).
41
- export function supervisorServes ({ ownerId, myUid }) {
41
+ export function supervisorServes ({ ownerId, myUid, closedAt = null }) {
42
42
  if (!myUid) return false
43
- return ownerId === myUid
43
+ return ownerId === myUid && !closedAt
44
+ }
45
+
46
+ // supervisorRoomsToStop — reconcile the long-lived child map after a SUCCESSFUL
47
+ // discovery read. Closing, deleting, or transferring a room removes it from the
48
+ // active-owned result set; any existing child for that room must be retired even
49
+ // when the best-effort `session-deleted` realtime broadcast was missed.
50
+ //
51
+ // This stays pure so a transient discovery failure can be handled by the caller:
52
+ // account.mjs invokes it only after Supabase returned without an error. On a failed
53
+ // fetch the existing children stay alive and the next tick retries.
54
+ export function supervisorRoomsToStop ({ rooms, childRooms, myUid }) {
55
+ const activeOwned = new Set((rooms || [])
56
+ .filter((room) => supervisorServes({
57
+ ownerId: room?.owner_id,
58
+ myUid,
59
+ closedAt: room?.closed_at,
60
+ }))
61
+ .map((room) => room.code)
62
+ .filter(Boolean))
63
+ return [...(childRooms || [])].filter((code) => !activeOwned.has(code))
44
64
  }
45
65
 
46
66
  // fetchServeRow — read the room's owner with whatever identity the bridge holds