thinkpool-pair 0.7.69 → 0.7.71
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 +22 -16
- package/event-id.mjs +33 -0
- package/package.json +1 -1
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, capReplayEvents, chunkReplayEvents } from './event-id.mjs'
|
|
42
|
+
import { stampEvent, makeSeqCounter, maxSeq, seqable, capReplayEvents, chunkReplayEvents, boundEventForBroadcast } 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).
|
|
@@ -466,20 +466,22 @@ const defendFrame = (event, payload) => {
|
|
|
466
466
|
let json
|
|
467
467
|
try { json = JSON.stringify(payload) } catch { return payload }
|
|
468
468
|
if (!json || json.length <= FRAME_SOFT_LIMIT) return payload
|
|
469
|
-
process.stderr.write(`\n ⚠ oversized ${event} frame: ${json.length}B (term ${String(payload?.term || '?').slice(0, 8)}) —
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
469
|
+
process.stderr.write(`\n ⚠ oversized ${event} frame: ${json.length}B (term ${String(payload?.term || '?').slice(0, 8)}) — defanging so Realtime doesn't drop it\n`)
|
|
470
|
+
let p
|
|
471
|
+
try { p = JSON.parse(json) } catch { return payload }
|
|
472
|
+
// single live event (code-event): bound it (truncates strings, strips oversized
|
|
473
|
+
// image/content blocks → render-safe placeholder).
|
|
474
|
+
if (p.evt && typeof p.evt === 'object') p.evt = boundEventForBroadcast(p.evt, 100000)
|
|
475
|
+
// replay batch (code-replay): bound every event…
|
|
476
|
+
if (Array.isArray(p.events)) {
|
|
477
|
+
p.events = p.events.map((e) => boundEventForBroadcast(e, 100000))
|
|
478
|
+
// …and if a frame is STILL over the limit, drop the OLDEST events until it fits —
|
|
479
|
+
// keep the newest so the client's seqHi still advances to current. classifyReplay
|
|
480
|
+
// accepts the resulting seq jump; older events are covered by the DB transcript snapshot.
|
|
481
|
+
let guard = 0
|
|
482
|
+
while (p.events.length > 1 && JSON.stringify(p).length > FRAME_SOFT_LIMIT && guard++ < 10000) p.events.shift()
|
|
483
|
+
}
|
|
484
|
+
return p
|
|
483
485
|
}
|
|
484
486
|
|
|
485
487
|
const bcast = (event, payload) => {
|
|
@@ -1028,7 +1030,11 @@ channel
|
|
|
1028
1030
|
// (the seq-divergence freeze). Replay the full recent tail and flag `reset` so the
|
|
1029
1031
|
// client rebuilds its cursor from our authoritative log instead of trusting its own.
|
|
1030
1032
|
const ahead = from > mx
|
|
1031
|
-
const
|
|
1033
|
+
const rawTail = (from > 0 && !ahead) ? s.log.filter((e) => e.seq == null || e.seq > from) : s.log
|
|
1034
|
+
// Bound each event so no single one (e.g. a 592KB inline-image tool_result) can
|
|
1035
|
+
// overflow a frame on its own — chunkReplayEvents only bounds the SUM, not a lone
|
|
1036
|
+
// giant event. boundEventForBroadcast clones+defangs only the oversized ones.
|
|
1037
|
+
const tail = rawTail.map((e) => boundEventForBroadcast(e))
|
|
1032
1038
|
// Chunk the tail into ORDERED under-budget frames (A): the full log on a fresh reload
|
|
1033
1039
|
// (empty cursor) is multi-MB and a single frame would be dropped by Realtime, freezing
|
|
1034
1040
|
// the terminal. chunkReplayEvents splits it so nothing in the recent window is lost;
|
package/event-id.mjs
CHANGED
|
@@ -124,3 +124,36 @@ export function chunkReplayEvents(events, budget = REPLAY_BYTE_BUDGET, maxChunks
|
|
|
124
124
|
// to oldest-first so classifyReplay applies them contiguously in arrival order.
|
|
125
125
|
return chunksRev.reverse().map((c) => c.reverse())
|
|
126
126
|
}
|
|
127
|
+
|
|
128
|
+
// Bound a SINGLE event so it can never overflow a broadcast frame on its own. The worst
|
|
129
|
+
// offenders are tool_results carrying inline base64 IMAGES (a 592KB screenshot block) —
|
|
130
|
+
// these can't ride a size-capped Realtime broadcast at all (live OR replay). Truncate
|
|
131
|
+
// bulky string fields AND replace oversized content blocks (images, huge text) with a
|
|
132
|
+
// compact, render-SAFE text placeholder — preserving identity (kind/seq/cid/ts/toolUseId)
|
|
133
|
+
// so dedup, ordering, seqHi, and turn structure stay intact. Returns the event UNCHANGED
|
|
134
|
+
// (no clone) when it's already small; clones before mutating so the persisted log is never
|
|
135
|
+
// touched. (Proper fix for inline images = upload to storage + broadcast a URL — separate.)
|
|
136
|
+
export const PER_EVENT_CAP = 180000
|
|
137
|
+
const truncStr = (s) => s.slice(0, 20000) + `\n…[truncated ${s.length - 20000} bytes — too large to broadcast]`
|
|
138
|
+
export function boundEventForBroadcast(evt, cap = PER_EVENT_CAP) {
|
|
139
|
+
if (!evt || typeof evt !== 'object') return evt
|
|
140
|
+
let json
|
|
141
|
+
try { json = JSON.stringify(evt) } catch { return evt }
|
|
142
|
+
if (json.length <= cap) return evt
|
|
143
|
+
const out = { ...evt }
|
|
144
|
+
for (const k of ['text', 'output', 'stdout', 'stderr', 'input']) {
|
|
145
|
+
if (typeof out[k] === 'string' && out[k].length > 20000) out[k] = truncStr(out[k])
|
|
146
|
+
}
|
|
147
|
+
if (typeof out.content === 'string' && out.content.length > 20000) out.content = truncStr(out.content)
|
|
148
|
+
else if (Array.isArray(out.content)) {
|
|
149
|
+
out.content = out.content.map((b) => {
|
|
150
|
+
if (!b || typeof b !== 'object') return b
|
|
151
|
+
if (JSON.stringify(b).length <= 20000) return b
|
|
152
|
+
if (b.type === 'image') return { type: 'text', text: '[image omitted from replay — too large to broadcast; open the live session to view]' }
|
|
153
|
+
if (typeof b.text === 'string') return { ...b, text: truncStr(b.text) }
|
|
154
|
+
return { type: 'text', text: `[${b.type || 'content'} omitted — too large to broadcast]` }
|
|
155
|
+
})
|
|
156
|
+
}
|
|
157
|
+
out._bounded = true
|
|
158
|
+
return out
|
|
159
|
+
}
|