thinkpool-pair 0.7.66 → 0.7.67

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
@@ -39,7 +39,7 @@ import { createClient } from '@supabase/supabase-js'
39
39
  import { startClaudeSession } from './claude-session.mjs'
40
40
  import { turnInFlight } from './update-gate.mjs'
41
41
  import { saveSession, flushSession, deleteSession, loadAll, canResume, loadPtyId, savePtyId, loadNames, saveNames } from './session-store.mjs'
42
- import { stampEvent, makeSeqCounter, maxSeq, seqable } from './event-id.mjs'
42
+ import { stampEvent, makeSeqCounter, maxSeq, seqable, capReplayEvents } from './event-id.mjs'
43
43
  import { makeThrottledTrack } from './presence.mjs'
44
44
 
45
45
  // Public client creds (the same anon values the web app ships — safe to embed).
@@ -975,9 +975,11 @@ channel
975
975
  flushAll()
976
976
  for (const [id, t] of terms) {
977
977
  if (!t.scrollback) continue
978
+ // Cap scrollback too — a huge PTY buffer would blow the same frame limit.
979
+ const sb = t.scrollback.length > 100000 ? t.scrollback.slice(-100000) : t.scrollback
978
980
  bcast('pty-replay', {
979
981
  to: payload?.to ?? null, term: id,
980
- b64: Buffer.from(t.scrollback, 'utf8').toString('base64'),
982
+ b64: Buffer.from(sb, 'utf8').toString('base64'),
981
983
  })
982
984
  }
983
985
  // Structured sessions replay their event log (reader rebuilds from it).
@@ -990,7 +992,10 @@ channel
990
992
  for (const [id, s] of sessions) {
991
993
  if (!s.log.length) continue
992
994
  const from = Number(payload?.cursors?.[id]) || 0
993
- const events = from > 0 ? s.log.filter((e) => e.seq == null || e.seq > from) : s.log
995
+ const tail = from > 0 ? s.log.filter((e) => e.seq == null || e.seq > from) : s.log
996
+ // Cap to a frame-safe size — the full log on a fresh-reload (empty cursor) is
997
+ // multi-MB and gets dropped by Realtime, freezing the terminal. See capReplayEvents.
998
+ const events = capReplayEvents(tail)
994
999
  bcast('code-replay', { to: payload?.to ?? null, term: id, events })
995
1000
  }
996
1001
  // Re-send any still-pending permission/question cards. They ride a one-shot
package/event-id.mjs CHANGED
@@ -67,3 +67,29 @@ export function maxSeq(events) {
67
67
  for (const e of events) if (e && typeof e.seq === 'number' && e.seq > m) m = e.seq
68
68
  return m
69
69
  }
70
+
71
+ // Cap a replay event list to a byte budget so the broadcast frame fits under the
72
+ // Realtime size limit. THE BUG (2026-06-26, room XE3IQ): on a fresh reload the client
73
+ // sends replay-request with an EMPTY cursor (seqHi=0), so the bridge replied with the
74
+ // ENTIRE per-term log — multi-thousand events / multiple MB for an established terminal.
75
+ // That frame exceeds Realtime's broadcast size limit and is silently DROPPED (and breaks
76
+ // the socket for the rest of the tick, taking small terminals' replays + the CHANNEL_ERROR
77
+ // churn down with it). The terminal then never refills and every live event is gap-dropped
78
+ // → "messages vanish, agent never responds, only brand-new terminals work".
79
+ // Keep the MOST RECENT events that fit: the client's DB transcript snapshot (code_messages
80
+ // tx:, last 200) already renders older history, and seqDedup.classifyReplay explicitly
81
+ // accepts the seq eviction-jump, so the client's live cursor (seqHi) still reaches current.
82
+ // Empirically a ~266KB frame delivered while multi-MB did not; 150KB is a safe margin.
83
+ // Always keeps at least the newest event so seqHi can advance even if it alone is oversized.
84
+ export const REPLAY_BYTE_BUDGET = 150000
85
+ export function capReplayEvents(events, budget = REPLAY_BYTE_BUDGET) {
86
+ if (!Array.isArray(events) || events.length === 0) return events
87
+ let total = 0, i = events.length
88
+ while (i > 0) {
89
+ const sz = JSON.stringify(events[i - 1]).length + 1
90
+ if (i < events.length && total + sz > budget) break
91
+ total += sz
92
+ i--
93
+ }
94
+ return i === 0 ? events : events.slice(i)
95
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thinkpool-pair",
3
- "version": "0.7.66",
3
+ "version": "0.7.67",
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": {