thinkpool-pair 0.7.12 → 0.7.14

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
@@ -36,7 +36,7 @@ import { spawn } from 'node:child_process'
36
36
  import { randomUUID } from 'node:crypto'
37
37
  import { createClient } from '@supabase/supabase-js'
38
38
  import { startClaudeSession } from './claude-session.mjs'
39
- import { saveSession, flushSession, loadLatest, canResume, loadPtyId, savePtyId } from './session-store.mjs'
39
+ import { saveSession, flushSession, loadAll, canResume, loadPtyId, savePtyId } from './session-store.mjs'
40
40
 
41
41
  // Public client creds (the same anon values the web app ships — safe to embed).
42
42
  // Override with TP_SUPABASE_URL / TP_SUPABASE_ANON if you ever need to.
@@ -776,11 +776,15 @@ channel
776
776
  const startCmd = attachedCmd || autoAgent // autoAgent: account-mode auto-open (headless)
777
777
  if (startCmd && !terms.size && !sessions.size) {
778
778
  // Claude + structured mode → Agent SDK session; everything else → PTY.
779
- // On restart, restore the latest saved structured session for this room:
780
- // replay its transcript + resume the live SDK context if recent enough.
779
+ // On restart, restore EVERY saved structured session for this room (not
780
+ // just the latest) — replay each transcript + resume its live SDK context
781
+ // when recent enough. Without this, backgrounded terminals died on a
782
+ // bridge restart even though their state was on disk. Bounded by KEEP=8;
783
+ // only runs on a fresh start (guard above), so a network re-subscribe
784
+ // with live sessions won't re-spawn them.
781
785
  if (wantStructured(startCmd)) {
782
- const prev = loadLatest(room)
783
- if (prev && (prev.log?.length || prev.sessionId)) openStructured({ id: prev.id, resume: canResume(prev) ? prev.sessionId : undefined, log: prev.log, commands: prev.commands, mode: prev.mode })
786
+ const all = loadAll(room).filter((r) => r.log?.length || r.sessionId)
787
+ if (all.length) for (const rec of all) openStructured({ id: rec.id, resume: canResume(rec) ? rec.sessionId : undefined, log: rec.log, commands: rec.commands, mode: rec.mode })
784
788
  else openStructured({ id: randomUUID() })
785
789
  }
786
790
  else {
@@ -127,6 +127,15 @@ export function startClaudeSession({ cwd, model, resume, mode: initialMode = 'de
127
127
  const alwaysAllow = new Set() // tool:risk signatures the user chose "don't ask again" for
128
128
  const toolStart = new Map() // tool_use id → start time, for the duration badge
129
129
  let effort = null // active reasoning effort for the turn (from the hook input)
130
+ // Live token count for the thinking indicator — mirrors Claude Code's
131
+ // "↓ N tokens": the turn's billed OUTPUT tokens (thinking is billed as
132
+ // output, so this is the full count, not just the reasoning estimate). A
133
+ // tool-using turn emits several assistant messages; message_delta carries
134
+ // the running output_tokens for the CURRENT message, so we fold finished
135
+ // messages into turnBaseOut and add the live message's count on top. Reset
136
+ // per turn on `result`.
137
+ let turnBaseOut = 0 // output tokens from completed messages this turn
138
+ let curMsgOut = 0 // latest output_tokens for the in-flight message
130
139
 
131
140
  const emit = (evt) => { try { onEvent?.(evt) } catch { /* never let a consumer throw into the loop */ } }
132
141
 
@@ -286,7 +295,11 @@ export function startClaudeSession({ cwd, model, resume, mode: initialMode = 'de
286
295
  // model reasons, surfaced as the indicator's ↓ N tokens. Coarse,
287
296
  // emitted during extended thinking; not a per-token stream.
288
297
  if (m.subtype === 'thinking_tokens') {
289
- emit({ kind: 'thinking_tokens', tokens: m.estimated_tokens, delta: m.estimated_tokens_delta })
298
+ // The reasoning-phase estimate (smooth, but approximate). Only
299
+ // surface it BEFORE real output streams — once message_delta gives
300
+ // us authoritative output_tokens (which already include thinking),
301
+ // that supersedes the estimate so the count never jumps backwards.
302
+ if (turnBaseOut + curMsgOut === 0) emit({ kind: 'thinking_tokens', tokens: m.estimated_tokens, delta: m.estimated_tokens_delta })
290
303
  break
291
304
  }
292
305
  if (m.session_id) sessionId = m.session_id
@@ -312,8 +325,25 @@ export function startClaudeSession({ cwd, model, resume, mode: initialMode = 'de
312
325
  }
313
326
  }
314
327
  break
328
+ case 'stream_event': {
329
+ // Live output-token progress for the thinking indicator. message_delta
330
+ // carries the running output_tokens for the current assistant message;
331
+ // message_start opens a new one (fold the finished message into the
332
+ // turn base first). Authoritative + monotonic within a turn.
333
+ const ev = m.event
334
+ if (ev?.type === 'message_start') {
335
+ turnBaseOut += curMsgOut
336
+ curMsgOut = ev.message?.usage?.output_tokens || 0
337
+ emit({ kind: 'thinking_tokens', tokens: turnBaseOut + curMsgOut })
338
+ } else if (ev?.type === 'message_delta' && ev.usage) {
339
+ curMsgOut = ev.usage.output_tokens ?? curMsgOut
340
+ emit({ kind: 'thinking_tokens', tokens: turnBaseOut + curMsgOut })
341
+ }
342
+ break
343
+ }
315
344
  case 'result':
316
345
  if (m.session_id) sessionId = m.session_id
346
+ turnBaseOut = 0; curMsgOut = 0 // reset the live token count for the next turn
317
347
  emit({ kind: 'result', subtype: m.subtype, sessionId, costUsd: m.total_cost_usd, usage: m.usage, numTurns: m.num_turns })
318
348
  // Surface a usage/context meter (chrome, not a transcript line). The
319
349
  // context window % comes from the control request; cost is cumulative.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thinkpool-pair",
3
- "version": "0.7.12",
3
+ "version": "0.7.14",
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
@@ -56,6 +56,14 @@ export function canResume(rec) {
56
56
  return !!(rec && rec.sessionId && rec.savedAt && (Date.now() - rec.savedAt) < RESUME_MAX_AGE_MS)
57
57
  }
58
58
 
59
+ // EVERY saved session for the room, oldest→newest (so tabs reappear in order).
60
+ // Drives resume-ALL on a bridge restart — without this only the latest session
61
+ // came back and every backgrounded terminal died on restart despite its state
62
+ // sitting on disk. Bounded by KEEP (prune keeps the newest 8).
63
+ export function loadAll(room) {
64
+ return listRecs(room).sort((a, b) => (a.savedAt || 0) - (b.savedAt || 0))
65
+ }
66
+
59
67
  // Last PTY terminal id auto-opened for an attached command. A reconnecting
60
68
  // bridge reuses it so it re-attaches to the SAME terminal tab instead of
61
69
  // minting a fresh UUID and breeding a new dormant "Terminal N" each restart.