thinkpool-pair 0.7.91 → 0.7.92
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/bridge.mjs +30 -1
- package/package.json +1 -1
- package/session-store.mjs +53 -0
package/bridge.mjs
CHANGED
|
@@ -41,7 +41,7 @@ import { z } from 'zod'
|
|
|
41
41
|
import { startClaudeSession } from './claude-session.mjs'
|
|
42
42
|
import { formatPeek, PEEK, siblingsOf, resolveSibling, crossPostDecision, CROSSPOST, spawnDecision } from './cross-terminal.mjs'
|
|
43
43
|
import { turnInFlight } from './update-gate.mjs'
|
|
44
|
-
import { saveSession, flushSession, deleteSession, loadAll, canResume, loadPtyId, savePtyId, loadNames, saveNames } from './session-store.mjs'
|
|
44
|
+
import { saveSession, flushSession, deleteSession, loadAll, canResume, loadPtyId, savePtyId, loadNames, saveNames, appendDurableEvents, seedDurableEvents, readDurablePage } from './session-store.mjs'
|
|
45
45
|
import { stampEvent, makeSeqCounter, maxSeq, seqable, capReplayEvents, chunkReplayEvents, boundEventForBroadcast, inlineImageBlocks } from './event-id.mjs'
|
|
46
46
|
import { makeThrottledTrack } from './presence.mjs'
|
|
47
47
|
|
|
@@ -866,6 +866,12 @@ function pushLog(entry, evt) {
|
|
|
866
866
|
// hole the client's applied sequence. See event-id.mjs NO_SEQ_KINDS.
|
|
867
867
|
if (entry.seq && seqable(evt)) evt.seq = entry.seq.next()
|
|
868
868
|
entry.log.push(evt)
|
|
869
|
+
// Durable, UNBOUNDED archive (powers "load earlier history"): append the transcript
|
|
870
|
+
// event before the in-memory cap evicts it. Only seqable kinds — chrome/thinking noise
|
|
871
|
+
// is skipped. entry.id/entry.room are stamped in openStructured; a PTY entry (no id)
|
|
872
|
+
// is a no-op. This is the single place every new event passes through, so nothing is
|
|
873
|
+
// missed and reloaded events (which don't ride pushLog) never double-append.
|
|
874
|
+
if (entry.id && seqable(evt)) appendDurableEvents(entry.room, entry.id, [evt])
|
|
869
875
|
if (entry.log.length > STRUCTURED_LOG_MAX) entry.log.shift()
|
|
870
876
|
return evt
|
|
871
877
|
}
|
|
@@ -901,6 +907,13 @@ function openStructured({ id, model, resume, log, commands, mode, spawnedBy }) {
|
|
|
901
907
|
// an old seq, which is what keeps the between-turns restart dup-free (see
|
|
902
908
|
// event-id.mjs + src/pages/code/seqDedup.js). pushLog() is the single stamp point.
|
|
903
909
|
entry.seq = makeSeqCounter(maxSeq(entry.log))
|
|
910
|
+
// Identity for the durable archive — pushLog appends every new transcript event to
|
|
911
|
+
// <room>/<id>.events.jsonl keyed off these. Seed the archive once from the restored
|
|
912
|
+
// (≤2000) log so the retained window is immediately pageable; no-op if it already
|
|
913
|
+
// exists, so this never double-writes across restarts.
|
|
914
|
+
entry.id = id
|
|
915
|
+
entry.room = room
|
|
916
|
+
seedDurableEvents(room, id, entry.log.filter((e) => seqable(e)))
|
|
904
917
|
sessions.set(id, entry)
|
|
905
918
|
// Close an interrupted turn so a restored terminal comes back IDLE-with-history
|
|
906
919
|
// instead of stuck "thinking" (see restoredTurnOpen). Idempotent: the closer is
|
|
@@ -1337,6 +1350,22 @@ channel
|
|
|
1337
1350
|
}
|
|
1338
1351
|
announce()
|
|
1339
1352
|
})
|
|
1353
|
+
// "Load earlier history": serve an OLDER page from the durable JSONL archive (the
|
|
1354
|
+
// events the in-memory 2000-cap evicted). beforeSeq anchors the page at the client's
|
|
1355
|
+
// lowest loaded transcript seq; we return the slice just before it, chunked under the
|
|
1356
|
+
// same byte budget as replay. This is the backward cursor — it never touches seqHi
|
|
1357
|
+
// (the forward live/replay cursor); the client cid-dedups + reorders on the prepend.
|
|
1358
|
+
.on('broadcast', { event: 'history-page-request' }, ({ payload }) => {
|
|
1359
|
+
const id = payload?.term
|
|
1360
|
+
if (!id) return
|
|
1361
|
+
const { events, hasMore } = readDurablePage(room, id, Number(payload?.beforeSeq) || null, 200)
|
|
1362
|
+
const to = payload?.to ?? null
|
|
1363
|
+
if (!events.length) { bcast('history-page', { to, term: id, events: [], hasMore: false }); return }
|
|
1364
|
+
const chunks = chunkReplayEvents(events.map((e) => boundEventForBroadcast(e)))
|
|
1365
|
+
// hasMore rides only the FIRST (oldest) chunk so the client sets the floor flag once
|
|
1366
|
+
// from the true page boundary; later chunks are just more of the same page.
|
|
1367
|
+
chunks.forEach((evs, i) => bcast('history-page', { to, term: id, events: evs, hasMore: i === 0 ? hasMore : true }))
|
|
1368
|
+
})
|
|
1340
1369
|
.on('broadcast', { event: 'file-put' }, ({ payload }) => {
|
|
1341
1370
|
if (!payload?.id || !payload?.url) return
|
|
1342
1371
|
// Multi-bridge rooms: targeted like term-open; untargeted = solo case.
|
package/package.json
CHANGED
package/session-store.mjs
CHANGED
|
@@ -60,6 +60,59 @@ function prune(room) {
|
|
|
60
60
|
} catch { /* noop */ }
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
// ── Durable append-only event log (unbounded history; the <id>.json snapshot is
|
|
64
|
+
// still capped at STRUCTURED_LOG_MAX for fast restart, this is the full archive). ──
|
|
65
|
+
// One JSONL per terminal: every transcript event appended once, O(1), never rewritten.
|
|
66
|
+
// Powers "load earlier history" paging without holding the whole log in memory.
|
|
67
|
+
const eventsLog = (room, id) => path.join(dir(room), `${id}.events.jsonl`)
|
|
68
|
+
|
|
69
|
+
// Append events to the durable log. Caller passes only the events it wants kept
|
|
70
|
+
// (transcript kinds) — chrome/thinking noise is filtered upstream. Sync append keeps
|
|
71
|
+
// strict on-disk order; writes are tiny so the blocking cost is negligible.
|
|
72
|
+
export function appendDurableEvents(room, id, events) {
|
|
73
|
+
if (!room || !id || !Array.isArray(events) || !events.length) return
|
|
74
|
+
try {
|
|
75
|
+
ensureDir(room)
|
|
76
|
+
fs.appendFileSync(eventsLog(room, id), events.map((e) => JSON.stringify(e)).join('\n') + '\n')
|
|
77
|
+
} catch { /* noop */ }
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// One-time seed: if the durable log doesn't exist yet (a terminal that predates this
|
|
81
|
+
// feature, or a fresh restart before any new event), write the loaded snapshot as the
|
|
82
|
+
// base so the retained window is immediately pageable. No-op once the file exists, so
|
|
83
|
+
// reloaded events never double-append (only genuinely-new events ride pushLog → append).
|
|
84
|
+
export function seedDurableEvents(room, id, events) {
|
|
85
|
+
if (!room || !id || !Array.isArray(events) || !events.length) return
|
|
86
|
+
try {
|
|
87
|
+
ensureDir(room)
|
|
88
|
+
const f = eventsLog(room, id)
|
|
89
|
+
if (fs.existsSync(f)) return
|
|
90
|
+
fs.writeFileSync(f, events.map((e) => JSON.stringify(e)).join('\n') + '\n')
|
|
91
|
+
} catch { /* noop */ }
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Read a backward page: events with transcript seq < beforeSeq (newest `limit` of
|
|
95
|
+
// them, no-seq chrome interleaved by file order preserved). Returns { events, hasMore }.
|
|
96
|
+
// beforeSeq null → the newest `limit` events (the tail). hasMore = there is older still.
|
|
97
|
+
export function readDurablePage(room, id, beforeSeq, limit = 200) {
|
|
98
|
+
try {
|
|
99
|
+
const f = eventsLog(room, id)
|
|
100
|
+
if (!fs.existsSync(f)) return { events: [], hasMore: false }
|
|
101
|
+
const all = []
|
|
102
|
+
for (const line of fs.readFileSync(f, 'utf8').split('\n')) {
|
|
103
|
+
if (!line) continue
|
|
104
|
+
try { all.push(JSON.parse(line)) } catch { /* skip a torn line */ }
|
|
105
|
+
}
|
|
106
|
+
let bound = all.length
|
|
107
|
+
if (beforeSeq != null) {
|
|
108
|
+
const i = all.findIndex((e) => e && e.seq != null && e.seq >= beforeSeq)
|
|
109
|
+
if (i >= 0) bound = i
|
|
110
|
+
}
|
|
111
|
+
const start = Math.max(0, bound - limit)
|
|
112
|
+
return { events: all.slice(start, bound), hasMore: start > 0 }
|
|
113
|
+
} catch { return { events: [], hasMore: false } }
|
|
114
|
+
}
|
|
115
|
+
|
|
63
116
|
const timers = new Map()
|
|
64
117
|
function write(room, id, data) {
|
|
65
118
|
try { ensureDir(room); fs.writeFileSync(path.join(dir(room), `${id}.json`), JSON.stringify({ ...data, id, savedAt: Date.now() })); prune(room) } catch { /* noop */ }
|