thinkpool-pair 0.7.203 → 0.7.204
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 +13 -3
- package/event-bounds.mjs +96 -0
- package/event-id.mjs +3 -22
- package/package.json +2 -1
package/bridge.mjs
CHANGED
|
@@ -1044,7 +1044,7 @@ const announce = () => {
|
|
|
1044
1044
|
// turnActive). Authoritative — the bridge owns the turn — so the /code
|
|
1045
1045
|
// dashboard reads "N agents working" off the roster instead of sniffing
|
|
1046
1046
|
// the live code-event stream (which a passive dashboard channel misses).
|
|
1047
|
-
...[...sessions.entries()].map(([id, s]) => ({ id, cmd: s.cmd, kind: 'structured', runtime: s.runtime || 'claude', alive: true, busy: s.session?.turnActive ?? false, commands: s.commands, mode: s.mode || undefined, effort: s.effort || undefined, name: termNames[id] || undefined, model: s.model || undefined, ...(s.runtime === 'codex' ? { approvalPolicy: codexConfigForMode(s.mode).approvalPolicy, models: s.models || [] } : {}), ...(s.archiveOldestSeq != null ? { oldestSeq: s.archiveOldestSeq } : {}), ...(s.spawnedBy ? { spawned: true, spawnedBy: s.spawnedBy } : {}), ...(s.flowSessionId ? { flowId: s.flowSessionId, flowRole: s.flowTaskKey ? 'lane' : 'conductor' } : {}),
|
|
1047
|
+
...[...sessions.entries()].map(([id, s]) => ({ id, cmd: s.cmd, kind: 'structured', runtime: s.runtime || 'claude', alive: true, busy: s.session?.turnActive ?? false, hasTranscript: s.log.length > 0, commands: s.commands, mode: s.mode || undefined, effort: s.effort || undefined, name: termNames[id] || undefined, model: s.model || undefined, ...(s.runtime === 'codex' ? { approvalPolicy: codexConfigForMode(s.mode).approvalPolicy, models: s.models || [] } : {}), ...(s.archiveOldestSeq != null ? { oldestSeq: s.archiveOldestSeq } : {}), ...(s.spawnedBy ? { spawned: true, spawnedBy: s.spawnedBy } : {}), ...(s.flowSessionId ? { flowId: s.flowSessionId, flowRole: s.flowTaskKey ? 'lane' : 'conductor' } : {}),
|
|
1048
1048
|
// provider: the registered LLM-provider this lane runs on, NAME-ONLY {id,name}
|
|
1049
1049
|
// (NEVER the key or baseUrl). Additive; older clients ignore it. Omitted for the
|
|
1050
1050
|
// built-in/default Claude path (no badge). Makes the lane's provider badge +
|
|
@@ -2733,8 +2733,18 @@ channel
|
|
|
2733
2733
|
// (legacy/pre-C1 logs) ride along always; the client cid-dedups those and
|
|
2734
2734
|
// additionally drops ≤ seqHi, so an over-send is harmless. A fresh joiner (no
|
|
2735
2735
|
// cursor) still gets the full log.
|
|
2736
|
-
|
|
2737
|
-
|
|
2736
|
+
// Hydrate the viewed lane first. Other terminals still warm in the background,
|
|
2737
|
+
// but cannot queue ahead of the transcript the person is waiting to see.
|
|
2738
|
+
const replaySessions = [...sessions.entries()].sort(([a], [b]) =>
|
|
2739
|
+
a === payload?.priority ? -1 : b === payload?.priority ? 1 : 0)
|
|
2740
|
+
for (const [id, s] of replaySessions) {
|
|
2741
|
+
// An explicit empty replay is an acknowledgement, not data. It lets mixed
|
|
2742
|
+
// clients clear their loading cover even if they missed the announce's
|
|
2743
|
+
// hasTranscript:false state.
|
|
2744
|
+
if (!s.log.length) {
|
|
2745
|
+
bcast('code-replay', { to: payload?.to ?? null, term: id, events: [], empty: true })
|
|
2746
|
+
continue
|
|
2747
|
+
}
|
|
2738
2748
|
const from = Number(payload?.cursors?.[id]) || 0
|
|
2739
2749
|
const mx = maxSeq(s.log)
|
|
2740
2750
|
// Cursor-sanity (B-safety): if the client's cursor is AHEAD of our highest seq, the
|
package/event-bounds.mjs
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/* Browser-safe structured-event bounding shared by the bridge broadcast path and
|
|
2
|
+
the web client's persisted reload snapshot. Large Codex/Claude tool payloads
|
|
3
|
+
must remain recognisable (tool name + a useful preview), but must never turn a
|
|
4
|
+
replay frame or tx: seed row into a multi-megabyte hydration dependency. */
|
|
5
|
+
|
|
6
|
+
export const STRUCTURED_EVENT_CAP = 80000
|
|
7
|
+
|
|
8
|
+
const sizeOf = (value) => {
|
|
9
|
+
try { return JSON.stringify(value).length } catch { return Infinity }
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const clipString = (value, limit) => {
|
|
13
|
+
if (value.length <= limit) return value
|
|
14
|
+
return `${value.slice(0, limit)}\n…[truncated ${value.length - limit} bytes]`
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function compactValue(value, budget, depth = 0) {
|
|
18
|
+
if (sizeOf(value) <= budget) return value
|
|
19
|
+
if (typeof value === 'string') return clipString(value, Math.max(80, budget - 80))
|
|
20
|
+
if (value == null || typeof value !== 'object') return value
|
|
21
|
+
if (depth >= 4) return '[nested value omitted — too large]'
|
|
22
|
+
|
|
23
|
+
if (Array.isArray(value)) {
|
|
24
|
+
const out = []
|
|
25
|
+
const limit = Math.min(value.length, 8)
|
|
26
|
+
const per = Math.max(240, Math.floor((budget - 180) / Math.max(1, limit)))
|
|
27
|
+
for (let i = 0; i < limit; i++) out.push(compactValue(value[i], per, depth + 1))
|
|
28
|
+
if (value.length > limit) out.push(`…[${value.length - limit} more items omitted]`)
|
|
29
|
+
return out
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const entries = Object.entries(value)
|
|
33
|
+
const out = {}
|
|
34
|
+
const limit = Math.min(entries.length, 16)
|
|
35
|
+
const per = Math.max(240, Math.floor((budget - 180) / Math.max(1, limit)))
|
|
36
|
+
for (let i = 0; i < limit; i++) {
|
|
37
|
+
const [key, nested] = entries[i]
|
|
38
|
+
out[key] = compactValue(nested, per, depth + 1)
|
|
39
|
+
}
|
|
40
|
+
if (entries.length > limit) out._omitted = `${entries.length - limit} more fields`
|
|
41
|
+
return out
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function compactBlock(block, budget) {
|
|
45
|
+
if (!block || typeof block !== 'object') return compactValue(block, budget)
|
|
46
|
+
if (sizeOf(block) <= budget) return block
|
|
47
|
+
if (block.type === 'image') {
|
|
48
|
+
return { type: 'text', text: '[image omitted from replay — open the live session to view]' }
|
|
49
|
+
}
|
|
50
|
+
const out = {}
|
|
51
|
+
for (const key of ['type', 'id', 'name', 'tool_use_id']) if (block[key] != null) out[key] = block[key]
|
|
52
|
+
// A replay needs the tool identity and a useful preview, not a second full copy
|
|
53
|
+
// of a giant patch/result that already lives in the durable archive.
|
|
54
|
+
const payloadBudget = Math.min(block.type === 'tool_use' ? 12000 : 20000, Math.max(500, budget - 600))
|
|
55
|
+
if (typeof block.text === 'string') out.text = clipString(block.text, payloadBudget)
|
|
56
|
+
if (block.input != null) out.input = compactValue(block.input, payloadBudget, 1)
|
|
57
|
+
if (block.content != null) out.content = compactValue(block.content, payloadBudget, 1)
|
|
58
|
+
if (!('text' in out) && !('input' in out) && !('content' in out)) out.text = `[${block.type || 'content'} omitted — too large]`
|
|
59
|
+
return out
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function boundStructuredEvent(event, cap = STRUCTURED_EVENT_CAP) {
|
|
63
|
+
if (!event || typeof event !== 'object' || sizeOf(event) <= cap) return event
|
|
64
|
+
|
|
65
|
+
const out = { ...event, _bounded: true }
|
|
66
|
+
const stringBudget = Math.min(12000, Math.max(1000, Math.floor(cap / 4)))
|
|
67
|
+
for (const key of ['text', 'output', 'stdout', 'stderr', 'input']) {
|
|
68
|
+
if (typeof out[key] === 'string') out[key] = clipString(out[key], stringBudget)
|
|
69
|
+
}
|
|
70
|
+
if (typeof out.content === 'string') out.content = clipString(out.content, stringBudget)
|
|
71
|
+
else if (Array.isArray(out.content)) {
|
|
72
|
+
const per = Math.max(900, Math.floor((cap - 2000) / Math.max(1, Math.min(out.content.length, 8))))
|
|
73
|
+
out.content = out.content.slice(0, 8).map(block => compactBlock(block, per))
|
|
74
|
+
if (event.content.length > 8) out.content.push({ type: 'text', text: `…[${event.content.length - 8} more blocks omitted]` })
|
|
75
|
+
}
|
|
76
|
+
if (Array.isArray(out.blocks)) {
|
|
77
|
+
const per = Math.max(900, Math.floor((cap - 2000) / Math.max(1, Math.min(out.blocks.length, 8))))
|
|
78
|
+
out.blocks = out.blocks.slice(0, 8).map(block => compactBlock(block, per))
|
|
79
|
+
if (event.blocks.length > 8) out.blocks.push({ type: 'text', text: `…[${event.blocks.length - 8} more blocks omitted]` })
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (sizeOf(out) <= cap) return out
|
|
83
|
+
|
|
84
|
+
// Hard fallback: retain ordering/dedup identity and recognisable tool blocks.
|
|
85
|
+
const identity = {}
|
|
86
|
+
for (const key of ['kind', 'seq', 'cid', 'ts', 'term', 'toolUseId', 'isError']) {
|
|
87
|
+
if (out[key] != null) identity[key] = out[key]
|
|
88
|
+
}
|
|
89
|
+
if (Array.isArray(out.blocks)) identity.blocks = out.blocks.slice(0, 4).map(block => compactBlock(block, 6000))
|
|
90
|
+
else if (Array.isArray(out.content)) identity.content = out.content.slice(0, 4).map(block => compactBlock(block, 6000))
|
|
91
|
+
else if (typeof out.content === 'string') identity.content = clipString(out.content, 12000)
|
|
92
|
+
else if (typeof out.text === 'string') identity.text = clipString(out.text, 12000)
|
|
93
|
+
identity._bounded = true
|
|
94
|
+
identity._notice = 'Large tool payload truncated for fast transcript hydration'
|
|
95
|
+
return identity
|
|
96
|
+
}
|
package/event-id.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto'
|
|
2
|
+
import { boundStructuredEvent, STRUCTURED_EVENT_CAP } from './event-bounds.mjs'
|
|
2
3
|
|
|
3
4
|
/* event-id.mjs — stamp a structured event with a stable identity + timestamp
|
|
4
5
|
BEFORE it is logged + broadcast.
|
|
@@ -180,29 +181,9 @@ export function firstSeq(events) {
|
|
|
180
181
|
// so dedup, ordering, seqHi, and turn structure stay intact. Returns the event UNCHANGED
|
|
181
182
|
// (no clone) when it's already small; clones before mutating so the persisted log is never
|
|
182
183
|
// touched. (Proper fix for inline images = upload to storage + broadcast a URL — separate.)
|
|
183
|
-
export const PER_EVENT_CAP =
|
|
184
|
-
const truncStr = (s) => s.slice(0, 20000) + `\n…[truncated ${s.length - 20000} bytes — too large to broadcast]`
|
|
184
|
+
export const PER_EVENT_CAP = STRUCTURED_EVENT_CAP
|
|
185
185
|
export function boundEventForBroadcast(evt, cap = PER_EVENT_CAP) {
|
|
186
|
-
|
|
187
|
-
let json
|
|
188
|
-
try { json = JSON.stringify(evt) } catch { return evt }
|
|
189
|
-
if (json.length <= cap) return evt
|
|
190
|
-
const out = { ...evt }
|
|
191
|
-
for (const k of ['text', 'output', 'stdout', 'stderr', 'input']) {
|
|
192
|
-
if (typeof out[k] === 'string' && out[k].length > 20000) out[k] = truncStr(out[k])
|
|
193
|
-
}
|
|
194
|
-
if (typeof out.content === 'string' && out.content.length > 20000) out.content = truncStr(out.content)
|
|
195
|
-
else if (Array.isArray(out.content)) {
|
|
196
|
-
out.content = out.content.map((b) => {
|
|
197
|
-
if (!b || typeof b !== 'object') return b
|
|
198
|
-
if (JSON.stringify(b).length <= 20000) return b
|
|
199
|
-
if (b.type === 'image') return { type: 'text', text: '[image omitted from replay — too large to broadcast; open the live session to view]' }
|
|
200
|
-
if (typeof b.text === 'string') return { ...b, text: truncStr(b.text) }
|
|
201
|
-
return { type: 'text', text: `[${b.type || 'content'} omitted — too large to broadcast]` }
|
|
202
|
-
})
|
|
203
|
-
}
|
|
204
|
-
out._bounded = true
|
|
205
|
-
return out
|
|
186
|
+
return boundStructuredEvent(evt, cap)
|
|
206
187
|
}
|
|
207
188
|
|
|
208
189
|
// Detect inline base64 IMAGE blocks on a tool_result event (a screenshot the agent
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "thinkpool-pair",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.204",
|
|
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": {
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
"agent-notify.mjs",
|
|
23
23
|
"agent-detect.mjs",
|
|
24
24
|
"event-id.mjs",
|
|
25
|
+
"event-bounds.mjs",
|
|
25
26
|
"plan-meters.mjs",
|
|
26
27
|
"recap.mjs",
|
|
27
28
|
"transcript-sanitize.mjs",
|