thinkpool-pair 0.7.92 → 0.7.93

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 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, appendDurableEvents, seedDurableEvents, readDurablePage } from './session-store.mjs'
44
+ import { saveSession, flushSession, deleteSession, loadAll, canResume, loadPtyId, savePtyId, loadNames, saveNames, appendDurableEvents, seedDurableEvents, readDurablePage, readDurableOldestSeq } 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
 
@@ -579,7 +579,7 @@ const announce = () =>
579
579
  // the chosen label instead of defaulting to Normal / unnamed. `spawned` (set
580
580
  // when an agent Dispatched this lane via spawn_terminal) lets the client keep
581
581
  // it OUT of the top tab strip and show it only in the Ensemble menu.
582
- ...[...sessions.entries()].map(([id, s]) => ({ id, cmd: s.cmd, kind: 'structured', alive: true, commands: s.commands, mode: s.mode || undefined, name: termNames[id] || undefined, ...(s.spawnedBy ? { spawned: true, spawnedBy: s.spawnedBy } : {}) })),
582
+ ...[...sessions.entries()].map(([id, s]) => ({ id, cmd: s.cmd, kind: 'structured', alive: true, commands: s.commands, mode: s.mode || undefined, name: termNames[id] || undefined, ...(s.archiveOldestSeq != null ? { oldestSeq: s.archiveOldestSeq } : {}), ...(s.spawnedBy ? { spawned: true, spawnedBy: s.spawnedBy } : {}) })),
583
583
  ],
584
584
  })
585
585
 
@@ -871,7 +871,13 @@ function pushLog(entry, evt) {
871
871
  // is skipped. entry.id/entry.room are stamped in openStructured; a PTY entry (no id)
872
872
  // is a no-op. This is the single place every new event passes through, so nothing is
873
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])
874
+ if (entry.id && seqable(evt)) {
875
+ appendDurableEvents(entry.room, entry.id, [evt])
876
+ // First archived event sets the floor for a fresh lane (no seed ran). Lets the
877
+ // client correctly withhold "load earlier" until eviction pushes the loaded window
878
+ // above this floor — a new lane's first turn at seq>1 is NOT older-history.
879
+ if (entry.archiveOldestSeq == null) entry.archiveOldestSeq = evt.seq
880
+ }
875
881
  if (entry.log.length > STRUCTURED_LOG_MAX) entry.log.shift()
876
882
  return evt
877
883
  }
@@ -914,6 +920,10 @@ function openStructured({ id, model, resume, log, commands, mode, spawnedBy }) {
914
920
  entry.id = id
915
921
  entry.room = room
916
922
  seedDurableEvents(room, id, entry.log.filter((e) => seqable(e)))
923
+ // Archive floor — the lowest seq durably stored. Drives the client's "load earlier"
924
+ // offer (show only when its oldest loaded seq is BELOW this). null on a brand-new lane
925
+ // with no archive yet; pushLog stamps it from the first appended event.
926
+ entry.archiveOldestSeq = readDurableOldestSeq(room, id)
917
927
  sessions.set(id, entry)
918
928
  // Close an interrupted turn so a restored terminal comes back IDLE-with-history
919
929
  // instead of stuck "thinking" (see restoredTurnOpen). Idempotent: the closer is
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thinkpool-pair",
3
- "version": "0.7.92",
3
+ "version": "0.7.93",
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
@@ -91,6 +91,22 @@ export function seedDurableEvents(room, id, events) {
91
91
  } catch { /* noop */ }
92
92
  }
93
93
 
94
+ // The archive FLOOR: the lowest transcript seq durably stored for this terminal.
95
+ // The client compares its oldest loaded seq against this — only OFFER "load earlier"
96
+ // when there genuinely are older events in the archive (vs a fresh lane whose first
97
+ // turn happens to land at seq > 1, or an evicted-but-unarchived gap). null = no archive.
98
+ export function readDurableOldestSeq(room, id) {
99
+ try {
100
+ const f = eventsLog(room, id)
101
+ if (!fs.existsSync(f)) return null
102
+ for (const line of fs.readFileSync(f, 'utf8').split('\n')) {
103
+ if (!line) continue
104
+ try { const e = JSON.parse(line); if (e && e.seq != null) return e.seq } catch { /* skip torn line */ }
105
+ }
106
+ return null
107
+ } catch { return null }
108
+ }
109
+
94
110
  // Read a backward page: events with transcript seq < beforeSeq (newest `limit` of
95
111
  // them, no-seq chrome interleaved by file order preserved). Returns { events, hasMore }.
96
112
  // beforeSeq null → the newest `limit` events (the tail). hasMore = there is older still.