thinkpool-pair 0.7.75 → 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/package.json +1 -1
- package/session-store.mjs +35 -10
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).
|