thinkpool-pair 0.7.74 → 0.7.76

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
@@ -298,7 +298,11 @@ export async function runAccount(SUPABASE_URL, SUPABASE_ANON) {
298
298
  // → dashboard reads "no bridge" while sessions run; 2026-06-17/-22/-24). The 15s
299
299
  // tick + token-refresh callers are unaffected (their interval exceeds the window).
300
300
  const trackPresence = makeThrottledTrack(acct, { minMs: 5000 })
301
- const pushPresence = () => trackPresence({ name: machine, version: VERSION, rooms: [...children.keys()], ts: Date.now() })
301
+ // `service` lets the dashboard show whether this bridge is the durable installed
302
+ // service (always-on, survives reboot) vs a foreground `npx` run that dies with its
303
+ // terminal. THINKPOOL_PAIR_AUTOUPDATE=1 is set ONLY by the install-service plist/unit
304
+ // (service.mjs), so it's the reliable proxy for "running as the managed service".
305
+ const pushPresence = () => trackPresence({ name: machine, version: VERSION, rooms: [...children.keys()], service: process.env.THINKPOOL_PAIR_AUTOUPDATE === '1', ts: Date.now() })
302
306
  // Re-track on EVERY (re)subscribe, not just the first: a realtime reconnect
303
307
  // (network blip, or a token swap mid-flight) rejoins the channel and must
304
308
  // re-announce, or the dashboard would read "no bridge" until the next restart.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thinkpool-pair",
3
- "version": "0.7.74",
3
+ "version": "0.7.76",
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/session-store.mjs CHANGED
@@ -12,14 +12,33 @@ import os from 'node:os'
12
12
  import fs from 'node:fs'
13
13
  import path from 'node:path'
14
14
 
15
- const ROOT = path.join(os.homedir(), '.thinkpool-pair')
16
- const dir = (room) => path.join(ROOT, room || 'default')
15
+ // TP_PAIR_ROOT override exists for tests + sandboxes so they never touch the real
16
+ // ~/.thinkpool-pair store. Read LIVE per call (not captured at module load) so a test
17
+ // can point each case at a fresh tempdir after importing this module. Production leaves
18
+ // it unset → the real home-dir store.
19
+ const ROOT = () => process.env.TP_PAIR_ROOT || path.join(os.homedir(), '.thinkpool-pair')
20
+ const dir = (room) => path.join(ROOT(), room || 'default')
21
+ const archiveDir = (room) => path.join(dir(room), '.archive')
17
22
  // Only resume the live SDK context for recent sessions — an expired session id
18
23
  // fails ("No conversation found"); past this window we restore transcript only.
19
24
  const RESUME_MAX_AGE_MS = 12 * 60 * 60 * 1000
20
- const KEEP = 8 // newest N session files per room
25
+ const KEEP = 8 // newest N UNNAMED session files per room (named are immune — see prune)
21
26
 
22
27
  function ensureDir(room) { try { fs.mkdirSync(dir(room), { recursive: true }) } catch { /* noop */ } }
28
+ // Move a session file into <room>/.archive/ instead of unlinking it. Archived files
29
+ // are OUTSIDE the readdirSync(dir) path loadAll/listRecs read, so a bridge restart
30
+ // never resurrects them — but they survive on disk for manual recovery. This is the
31
+ // data-loss backstop: a pruned/closed terminal's transcript is always recoverable.
32
+ // (2026-06-26: prune used to unlinkSync, permanently destroying older terminals —
33
+ // "Blightborne" was lost this way. Named terminals are now additionally immune.)
34
+ function archive(room, file) {
35
+ try {
36
+ fs.mkdirSync(archiveDir(room), { recursive: true })
37
+ const base = path.basename(file)
38
+ const dst = path.join(archiveDir(room), `${base}.${Date.now()}`)
39
+ fs.renameSync(file, dst)
40
+ } catch { /* noop */ }
41
+ }
23
42
  function listRecs(room) {
24
43
  try {
25
44
  return fs.readdirSync(dir(room)).filter((f) => f.endsWith('.json'))
@@ -29,10 +48,15 @@ function listRecs(room) {
29
48
  }
30
49
  function prune(room) {
31
50
  try {
51
+ const names = loadNames(room) // { id: label } — NAMED terminals are immune
52
+ const namedId = (p) => { try { return JSON.parse(fs.readFileSync(p, 'utf8')).id } catch { return null } }
32
53
  const files = fs.readdirSync(dir(room)).filter((f) => f.endsWith('.json'))
33
- .map((f) => { const p = path.join(dir(room), f); let m = 0; try { m = JSON.parse(fs.readFileSync(p, 'utf8')).savedAt || 0 } catch { /* noop */ } return { p, m } })
54
+ .map((f) => { const p = path.join(dir(room), f); let m = 0; try { m = JSON.parse(fs.readFileSync(p, 'utf8')).savedAt || 0 } catch { /* noop */ } return { p, f, m } })
34
55
  .sort((a, b) => b.m - a.m)
35
- files.slice(KEEP).forEach((x) => { try { fs.unlinkSync(x.p) } catch { /* noop */ } })
56
+ // Only UNNAMED sessions are eligible for prune. A named terminal is deliberate
57
+ // user intent (a project) and must never be silently destroyed by churn.
58
+ const prunable = files.filter((x) => { const id = namedId(x.p); return !id || !names[id] })
59
+ prunable.slice(KEEP).forEach((x) => archive(room, x.p))
36
60
  } catch { /* noop */ }
37
61
  }
38
62
 
@@ -46,13 +70,14 @@ export function saveSession(room, id, data) {
46
70
  timers.set(id, setTimeout(() => write(room, id, data), 1500))
47
71
  }
48
72
  export function flushSession(room, id, data) { clearTimeout(timers.get(id)); write(room, id, data) }
49
- // User-initiated close: cancel any pending debounced save and remove the on-disk
50
- // record so a later loadAll() (bridge restart) can't resurrect a closed terminal.
51
- // PTY ids never have an <id>.json (they use the .pty-id dotfile), so this no-ops
52
- // harmlessly for them.
73
+ // User-initiated close: cancel any pending debounced save and move the on-disk
74
+ // record to .archive/ so a later loadAll() (bridge restart) can't resurrect a
75
+ // closed terminal — but the transcript stays recoverable. PTY ids never have an
76
+ // <id>.json (they use the .pty-id dotfile), so this no-ops harmlessly for them.
53
77
  export function deleteSession(room, id) {
54
78
  clearTimeout(timers.get(id)); timers.delete(id)
55
- try { fs.unlinkSync(path.join(dir(room), `${id}.json`)) } catch { /* noop */ }
79
+ const p = path.join(dir(room), `${id}.json`)
80
+ try { if (fs.existsSync(p)) archive(room, p) } catch { /* noop */ }
56
81
  }
57
82
 
58
83
  // Most-recently-saved structured session for the room (drives attached restore).