thinkpool-pair 0.7.94 → 0.7.95

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
@@ -13,9 +13,10 @@ import path from 'node:path'
13
13
  import fs from 'node:fs'
14
14
  import { createClient } from '@supabase/supabase-js'
15
15
  import os from 'node:os'
16
- import { saveAuth, loadAuth, loadDirs, bindDir } from './auth-store.mjs'
16
+ import { saveAuth, loadAuth, loadDirs, bindDir, loadServed, rememberServed } from './auth-store.mjs'
17
17
  import { isSafeToRestart } from './update-gate.mjs'
18
18
  import { makeThrottledTrack } from './presence.mjs'
19
+ import { resolveServeDir } from './serve-dir.mjs'
19
20
 
20
21
  const VERSION = (() => { try { return JSON.parse(fs.readFileSync(new URL('./package.json', import.meta.url), 'utf8')).version } catch { return null } })()
21
22
 
@@ -380,16 +381,35 @@ export async function runAccount(SUPABASE_URL, SUPABASE_ANON) {
380
381
  rooms = data || []
381
382
  } catch { /* transient — keep existing children, retry next tick */ }
382
383
  const dirs = loadDirs()
384
+ const served = loadServed()
383
385
  for (const r of rooms) {
384
386
  const room = r.code
385
387
  if (!room || children.has(room)) continue
386
388
  if (r.owner_id && r.owner_id !== session.user.id) continue // never serve a room we don't own (defense-in-depth behind the .eq above)
387
- const dir = dirs[room] || DEFAULT_DIR // auto-serve unbound sessions in the launch dir
389
+ // Resolve the serve dir: explicit bind > last-served memory > launch dir. REFUSE
390
+ // to auto-serve an unbound room from $HOME — that strips the agent's resume context
391
+ // (cwd-keyed SDK sessions) and shows your home folder as the repo (FVIHV1DE).
392
+ const choice = resolveServeDir({ bound: dirs[room], remembered: served[room], defaultDir: DEFAULT_DIR, home: os.homedir() })
393
+ if (choice.refuse) {
394
+ if (!warned.has(room)) {
395
+ warned.add(room)
396
+ const why = choice.reason === 'home'
397
+ ? `no project directory set — refusing to serve it from your home folder (${choice.dir}); the agent would lose its session context and the room would show "${path.basename(choice.dir)}" as the repo.`
398
+ : 'no directory to serve it from.'
399
+ process.stderr.write(`\n ◇ ${room}: ${why}\n Bind it to its repo: npx thinkpool-pair bind ${room} <repo-dir>\n`)
400
+ }
401
+ continue
402
+ }
403
+ const dir = choice.dir
388
404
  if (!fs.existsSync(dir)) {
389
405
  if (!warned.has(room)) { warned.add(room); process.stderr.write(`\n ◇ ${room}: bound directory is gone (${dir}) — re-bind it.\n`) }
390
406
  continue
391
407
  }
392
408
  warned.delete(room)
409
+ // Learn this room → dir so a later relaunch from $HOME resumes it HERE (the real
410
+ // repo) instead of the home-dir fallback. Only the auto path is remembered; an
411
+ // explicit bind already persists in dirs.json.
412
+ if (choice.fromDefault) rememberServed(room, dir)
393
413
  roomNames.set(room, r.name || null) // feed list_sessions (cross-room roster)
394
414
  process.stderr.write(`\n ◆ serving ${room}${r.name ? ` "${r.name}"` : ''} → ${dir}${dirs[room] ? '' : ' (default)'}\n`)
395
415
  // Children NEVER self-update (only the supervisor restart re-resolves
package/auth-store.mjs CHANGED
@@ -11,6 +11,11 @@ import path from 'node:path'
11
11
  const DIR = path.join(os.homedir(), '.thinkpool-pair')
12
12
  const AUTH = path.join(DIR, 'auth.json')
13
13
  const DIRS = path.join(DIR, 'dirs.json')
14
+ // served.json: per-room memory of the last real (non-home) directory a room was
15
+ // auto-served from. Distinct from dirs.json (explicit binds): this is learned, so a
16
+ // supervisor relaunched from $HOME still resumes each room in the repo it last ran in
17
+ // instead of falling back to the home folder (FVIHV1DE, 2026-06-30).
18
+ const SERVED = path.join(DIR, 'served.json')
14
19
 
15
20
  function ensureDir() { try { fs.mkdirSync(DIR, { recursive: true }) } catch { /* noop */ } }
16
21
 
@@ -41,3 +46,15 @@ export function unbindDir(room) {
41
46
  try { fs.writeFileSync(DIRS, JSON.stringify(d, null, 2)) } catch { /* noop */ }
42
47
  return d
43
48
  }
49
+
50
+ // Learned per-room serve directory (see SERVED note above). loadServed feeds the
51
+ // dir-resolution precedence; rememberServed records the dir a room was just served
52
+ // from so a later relaunch-from-home resumes it there rather than in $HOME.
53
+ export function loadServed() { try { return JSON.parse(fs.readFileSync(SERVED, 'utf8')) } catch { return {} } }
54
+ export function rememberServed(room, dir) {
55
+ const d = loadServed(); if (!room || !dir || d[room] === dir) return d
56
+ d[room] = dir
57
+ ensureDir()
58
+ try { fs.writeFileSync(SERVED, JSON.stringify(d, null, 2)) } catch { /* noop */ }
59
+ return d
60
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thinkpool-pair",
3
- "version": "0.7.94",
3
+ "version": "0.7.95",
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": {
@@ -16,6 +16,7 @@
16
16
  "transcript-sanitize.mjs",
17
17
  "session-store.mjs",
18
18
  "cross-terminal.mjs",
19
+ "serve-dir.mjs",
19
20
  "service.mjs",
20
21
  "presence.mjs",
21
22
  "account.mjs",
package/serve-dir.mjs ADDED
@@ -0,0 +1,40 @@
1
+ /* serve-dir.mjs — decide which working directory the account supervisor serves a
2
+ room from, and REFUSE the home-directory trap.
3
+
4
+ Why this exists (FVIHV1DE, 2026-06-30): the supervisor auto-serves an unbound room
5
+ from its own launch dir (DEFAULT_DIR = process.cwd()). When the supervisor is
6
+ relaunched from $HOME instead of the repo, every unbound room is served from $HOME.
7
+ That silently breaks two things:
8
+ 1. The agent's cwd becomes ~, so the Claude SDK (which keys a session's transcript
9
+ by project directory) can't find the session that was created under the real
10
+ repo → resume fails → the agent is BLIND to its own history (the UI still replays
11
+ the persisted ThinkPool log, so "I see the chat but the agent can't").
12
+ 2. The room's repo label is basename(cwd) = your home folder name ("max"/"conrad")
13
+ instead of "thinkpool" / a branch.
14
+ Loud refusal beats silent context corruption. Precedence:
15
+ explicit bind > last-served memory > the supervisor's launch dir
16
+ and an UNBOUND, never-served room is refused when the launch dir IS $HOME.
17
+ Pure (no fs/process) so it unit-tests without spawning bridges. */
18
+ import path from 'node:path'
19
+
20
+ const samePath = (a, b) => {
21
+ try { return !!a && !!b && path.resolve(a) === path.resolve(b) } catch { return false }
22
+ }
23
+
24
+ // Returns { dir, refuse, reason, fromDefault }.
25
+ // - dir: the directory to serve from (even when refusing, for the message)
26
+ // - refuse: true → do NOT serve; warn the user to bind instead
27
+ // - reason: 'home' | 'none' when refusing
28
+ // - fromDefault:true → resolved to the supervisor's launch dir with no bind/memory
29
+ // (the caller should remember it after a successful serve, so a later
30
+ // relaunch-from-home resumes the room from this same repo)
31
+ export function resolveServeDir({ bound, remembered, defaultDir, home } = {}) {
32
+ const pick = bound || remembered || defaultDir || null
33
+ if (!pick) return { dir: null, refuse: true, reason: 'none' }
34
+ // Only the AUTO path (no explicit bind, no remembered dir) is guarded — if a person
35
+ // deliberately bound a room to their home dir, that's their call, honour it.
36
+ if (!bound && !remembered && samePath(pick, home)) {
37
+ return { dir: pick, refuse: true, reason: 'home' }
38
+ }
39
+ return { dir: pick, refuse: false, fromDefault: !bound && !remembered }
40
+ }