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 +5 -1
- package/package.json +1 -1
- package/session-store.mjs +35 -10
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
|
-
|
|
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
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
|
-
|
|
16
|
-
|
|
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
|
-
|
|
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
|
|
50
|
-
// record so a later loadAll() (bridge restart) can't resurrect a
|
|
51
|
-
//
|
|
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
|
-
|
|
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).
|