thinkpool-pair 0.7.116 → 0.7.118
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 +34 -2
- package/package.json +1 -1
package/bridge.mjs
CHANGED
|
@@ -680,10 +680,18 @@ const bcast = (event, payload, ch = channel) => {
|
|
|
680
680
|
// Persisted on the host so a rename is cross-device + survives a bridge restart;
|
|
681
681
|
// every announce carries them so a late-joining or second device sees them too.
|
|
682
682
|
const termNames = loadNames(room)
|
|
683
|
+
// The SDK's supported-model LIST (value/displayName/description), captured from
|
|
684
|
+
// any session's `models` event. Announced room-level so EVERY device gets it on
|
|
685
|
+
// connect — not just the one that saw the one-shot event (the picker fell back to
|
|
686
|
+
// a hardcoded shorthand set on other devices / after reload). Max 2026-07-02.
|
|
687
|
+
let roomModels = null
|
|
683
688
|
|
|
684
689
|
const announce = () =>
|
|
685
690
|
bcast('bridge', {
|
|
686
691
|
v: 2, name, repo: repoLabel, branch: readBranch(),
|
|
692
|
+
// models: the SDK's supported-model list, so the /model picker shows the real
|
|
693
|
+
// set (incl. Fable + version labels) on every device without hardcoding.
|
|
694
|
+
models: roomModels || undefined,
|
|
687
695
|
// cwd + version: the host's working dir + thinkpool-pair version, shown in
|
|
688
696
|
// the room's welcome banner. Re-sent per announce so late joiners get them.
|
|
689
697
|
cwd, version: VERSION,
|
|
@@ -724,7 +732,7 @@ const announce = () =>
|
|
|
724
732
|
// turnActive). Authoritative — the bridge owns the turn — so the /code
|
|
725
733
|
// dashboard reads "N agents working" off the roster instead of sniffing
|
|
726
734
|
// the live code-event stream (which a passive dashboard channel misses).
|
|
727
|
-
...[...sessions.entries()].map(([id, s]) => ({ id, cmd: s.cmd, kind: 'structured', alive: true, busy: s.session?.turnActive ?? false, commands: s.commands, mode: s.mode || undefined, name: termNames[id] || undefined, ...(s.archiveOldestSeq != null ? { oldestSeq: s.archiveOldestSeq } : {}), ...(s.spawnedBy ? { spawned: true, spawnedBy: s.spawnedBy } : {}), ...(s.flowSessionId ? { flowId: s.flowSessionId, flowRole: s.flowTaskKey ? 'lane' : 'conductor' } : {}) })),
|
|
735
|
+
...[...sessions.entries()].map(([id, s]) => ({ id, cmd: s.cmd, kind: 'structured', alive: true, busy: s.session?.turnActive ?? false, commands: s.commands, mode: s.mode || undefined, name: termNames[id] || undefined, model: s.model || undefined, ...(s.archiveOldestSeq != null ? { oldestSeq: s.archiveOldestSeq } : {}), ...(s.spawnedBy ? { spawned: true, spawnedBy: s.spawnedBy } : {}), ...(s.flowSessionId ? { flowId: s.flowSessionId, flowRole: s.flowTaskKey ? 'lane' : 'conductor' } : {}) })),
|
|
728
736
|
],
|
|
729
737
|
})
|
|
730
738
|
|
|
@@ -1620,6 +1628,14 @@ function openStructured({ id, model, resume, log, commands, mode, spawnedBy, rol
|
|
|
1620
1628
|
// (normal, spawn, flow lane, cross-room) since they all flow through onEvent.
|
|
1621
1629
|
{ const busyNow = entry.session?.turnActive ?? false
|
|
1622
1630
|
if (busyNow !== entry._busyAnn) { entry._busyAnn = busyNow; announce() } }
|
|
1631
|
+
// Capture this session's current MODEL (resolved full id, with version — from
|
|
1632
|
+
// system/result `model` or usage `ctx.model`) and the SDK's supported-model
|
|
1633
|
+
// LIST (from the `models` event) so the announce carries them to EVERY device.
|
|
1634
|
+
// Re-announce on change so a switch / late device sees it without a turn (the
|
|
1635
|
+
// old localStorage cache was per-device; branch showed on every other device).
|
|
1636
|
+
{ const m = evt.model || evt.ctx?.model
|
|
1637
|
+
if (m && m !== entry.model) { entry.model = m; announce() }
|
|
1638
|
+
if (evt.kind === 'models' && Array.isArray(evt.models) && evt.models.length) { roomModels = evt.models; announce() } }
|
|
1623
1639
|
// FL-B2 — fold this flow lane's completed-turn output tokens into its budget so the
|
|
1624
1640
|
// autopilot cap can halt the next wave before overrun (output_tokens = the billed
|
|
1625
1641
|
// reasoning+output spend the indicator already tracks; conservative enough for a guard).
|
|
@@ -2409,7 +2425,19 @@ flowChannel
|
|
|
2409
2425
|
cleanupFlowLanes(payload.flowId, 600)
|
|
2410
2426
|
process.stderr.write(`\n ${A.yel}◆ flow cancelled — closing its lanes (${String(payload.flowId).replace(/-/g, '').slice(0, 8)}).${A.rst}\n`)
|
|
2411
2427
|
})
|
|
2412
|
-
.subscribe(
|
|
2428
|
+
.subscribe(status => {
|
|
2429
|
+
// BRG-2 (audit 2026-07-02): tpflow had a bare subscribe with no status handler, so a
|
|
2430
|
+
// channel-specific CHANNEL_ERROR (socket still healthy → the main tpcode watchdog
|
|
2431
|
+
// never trips) dropped flow dispatch/plan/done silently until the next bridge
|
|
2432
|
+
// restart. Surface it. supabase-js auto-rejoins every channel on a socket reconnect,
|
|
2433
|
+
// so a transient drop self-heals; an active per-channel re-subscribe is deliberately
|
|
2434
|
+
// NOT added here — re-subscribing a live supabase channel is a known lifecycle
|
|
2435
|
+
// footgun (the 2026-06-30 channel-collision family) and can't be two-device verified
|
|
2436
|
+
// from a single host.
|
|
2437
|
+
if (status === 'CHANNEL_ERROR' || status === 'TIMED_OUT' || status === 'CLOSED') {
|
|
2438
|
+
process.stderr.write(`\n ⚠ flow realtime ${status} (tpflow:${room}).\n`)
|
|
2439
|
+
}
|
|
2440
|
+
})
|
|
2413
2441
|
|
|
2414
2442
|
// Watchdog — if the realtime channel is stuck (never connected, or dropped and
|
|
2415
2443
|
// not recovered) for >60s, exit non-zero so a supervisor/launchd restarts a
|
|
@@ -2565,6 +2593,10 @@ async function shutdown(code = 0, farewell = true) {
|
|
|
2565
2593
|
// socket is alive; on a wedge it won't flush and the hard backstop above wins).
|
|
2566
2594
|
try { await channel.untrack() } catch { /* noop */ }
|
|
2567
2595
|
try { await supabase.removeChannel(channel) } catch { /* noop */ }
|
|
2596
|
+
// BRG-2 (audit 2026-07-02): shutdown removed only the room channel, leaking the flow
|
|
2597
|
+
// topic (no presence to untrack, but the subscription + socket join outlived the
|
|
2598
|
+
// process teardown). Close it too so shutdown is symmetric.
|
|
2599
|
+
try { await supabase.removeChannel(flowChannel) } catch { /* noop */ }
|
|
2568
2600
|
setTimeout(() => process.exit(code), 250) // grace for the leave/close frames to flush
|
|
2569
2601
|
}
|
|
2570
2602
|
process.on('SIGINT', () => shutdown(0))
|