thinkpool-pair 0.7.126 → 0.7.127

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
@@ -38,7 +38,7 @@ import { randomUUID } from 'node:crypto'
38
38
  import { createClient } from '@supabase/supabase-js'
39
39
  import { createSdkMcpServer, tool } from '@anthropic-ai/claude-agent-sdk'
40
40
  import { z } from 'zod'
41
- import { startClaudeSession } from './claude-session.mjs'
41
+ import { startClaudeSession, oneShotSummary } from './claude-session.mjs'
42
42
  import { FLOW_CONDUCTOR_PROMPT, FLOW_LANE_PROMPT, buildConductorEnv, assembleCrossWaveContext, buildLanePrompt } from './flow-conductor.mjs'
43
43
  import { normalizePlanOutput } from './flow-task-graph.mjs' // FL-B1 — validate the conductor's submit_flow_plan task-graph
44
44
  // S1 (context-offload) — durable digest store. mark_flow_done digests a closed slice in;
@@ -2156,6 +2156,71 @@ channel
2156
2156
  saveNames(room, termNames)
2157
2157
  announce()
2158
2158
  })
2159
+ // Session Info panel (2026-07-02) — a cheap one-shot recap on demand. NAMES live
2160
+ // terminals with doing/done status; aggregates CLOSED terminals into one line
2161
+ // without naming them (Max's spec). Runs on the room's own subscription auth via
2162
+ // oneShotSummary (cheap haiku → provider-default fallback). Never auto — only on
2163
+ // the panel button. The web caches the result in React state (no re-charge on
2164
+ // re-open within a page load).
2165
+ .on('broadcast', { event: 'code-summarize' }, async ({ payload }) => {
2166
+ const by = payload?.by || null
2167
+ bcast('code-summary', { status: 'generating', by })
2168
+ try {
2169
+ // Pull assistant/user text out of a transcript-event array, keep the tail.
2170
+ const logDigest = (events, max = 1200) => {
2171
+ const parts = []
2172
+ for (const e of (events || [])) {
2173
+ if (e?.kind === 'assistant') { for (const b of (e.blocks || [])) if (b?.type === 'text' && b.text) parts.push(b.text) }
2174
+ else if (e?.kind === 'you' && e.text) parts.push(`user: ${e.text}`)
2175
+ }
2176
+ return parts.join('\n').replace(/\n{3,}/g, '\n\n').slice(-max)
2177
+ }
2178
+ // LIVE structured terminals — named, doing/done, with a recent digest.
2179
+ const live = []
2180
+ let env = null, cwd = null
2181
+ for (const [id, e] of sessions) {
2182
+ if (e.kind !== 'structured') continue
2183
+ live.push({ name: termNames[id] || 'Terminal', status: e.session?.turnActive ? 'working' : 'done', digest: logDigest(e.log, 1100) })
2184
+ if (!env) { env = { ...process.env }; cwd = e.cwd || process.cwd() }
2185
+ }
2186
+ // CLOSED terminals — named but not live. Aggregate only; cap the read to bound cost.
2187
+ const liveIdSet = new Set(sessions.keys())
2188
+ const closedIds = Object.keys(termNames).filter(id => !liveIdSet.has(id))
2189
+ const closedDigests = []
2190
+ for (const id of closedIds.slice(0, 4)) {
2191
+ try {
2192
+ const page = readDurablePage(room, id, Number.MAX_SAFE_INTEGER, 30)
2193
+ const evs = Array.isArray(page) ? page : (Array.isArray(page?.events) ? page.events : [])
2194
+ const d = logDigest(evs, 450)
2195
+ if (d) closedDigests.push(d)
2196
+ } catch { /* no retained detail for this closed lane */ }
2197
+ }
2198
+ if (!live.length && !closedIds.length) { bcast('code-summary', { status: 'error', error: 'Nothing to summarize yet.', by }); return }
2199
+
2200
+ const liveBlock = live.length
2201
+ ? live.map(t => `LIVE TERMINAL "${t.name}" [${t.status}]:\n${t.digest || '(no recent activity)'}`).join('\n\n')
2202
+ : '(no live terminals)'
2203
+ const closedBlock = closedIds.length
2204
+ ? `\n\nThere are also ${closedIds.length} CLOSED terminal(s). Recent excerpts (do NOT name them individually):\n${(closedDigests.join('\n---\n') || '(no retained detail)').slice(0, 1400)}`
2205
+ : ''
2206
+
2207
+ const prompt = `Write a concise recap of this multi-terminal coding session for someone glancing at it. Rules:
2208
+ - NAME each LIVE terminal and say whether it is working or done, and briefly what it is doing/did.
2209
+ - Do NOT name the CLOSED terminals individually — fold them into ONE aggregate clause ("plus N closed lanes that handled …").
2210
+ - 2 to 4 sentences total, plain prose, no markdown, no headers, no bullet lists.
2211
+
2212
+ ${liveBlock}${closedBlock}
2213
+
2214
+ Recap:`
2215
+
2216
+ const res = await oneShotSummary({ prompt, env: env || process.env, cwd: cwd || process.cwd() })
2217
+ if (!res || !res.text) { bcast('code-summary', { status: 'error', error: 'Summary model unavailable on this provider.', by }); return }
2218
+ bcast('code-summary', { status: 'done', text: res.text, model: res.model, generatedAt: Date.now(), by })
2219
+ process.stderr.write(`\n ◆ session summary generated (${res.model}) for ${by || 'someone'}.\n`)
2220
+ } catch (e) {
2221
+ bcast('code-summary', { status: 'error', error: String(e?.message || e).slice(0, 140), by })
2222
+ }
2223
+ })
2159
2224
  .on('broadcast', { event: 'who' }, announce)
2160
2225
  .subscribe(status => {
2161
2226
  if (status === 'SUBSCRIBED') {
@@ -139,6 +139,48 @@ const TP_ROOM_REMINDER = [
139
139
  'Verify before claiming done — show runtime evidence you produced, not "should work, go test it".',
140
140
  ].join(' ')
141
141
 
142
+ // ── One-shot summary (2026-07-02, Session Info panel) ────────────────────────
143
+ // A bare, cheap, cold model call — same shape as the haikuSuggest fallback: runs
144
+ // on the room's own subscription auth (env carries the OAuth — no API key), NO
145
+ // settingSources / MCP / tools, maxTurns 1. Used by the bridge's code-summarize
146
+ // handler to write a session recap on demand. Model fallback (Max's ask): prefer a
147
+ // cheap haiku-class model; if the provider (GLM / OpenRouter / custom base-url) has
148
+ // no such model, retry with the provider default. Returns { text, model } or null.
149
+ export async function oneShotSummary({ prompt, env, cwd, timeoutMs = 25000 }) {
150
+ const attempt = async (model) => {
151
+ const ac = new AbortController()
152
+ const t = setTimeout(() => { try { ac.abort() } catch { /* noop */ } }, timeoutMs)
153
+ try {
154
+ const hq = query({
155
+ prompt,
156
+ options: {
157
+ ...(model ? { model } : {}),
158
+ ...(cwd ? { cwd } : {}),
159
+ env,
160
+ maxTurns: 1,
161
+ permissionMode: 'bypassPermissions',
162
+ settingSources: [],
163
+ strictMcpConfig: true,
164
+ mcpServers: {},
165
+ abortController: ac,
166
+ },
167
+ })
168
+ let out = ''
169
+ for await (const mm of hq) {
170
+ if (mm.type === 'assistant') for (const b of (mm.message?.content || [])) if (b.type === 'text') out += b.text
171
+ if (mm.type === 'result') break
172
+ }
173
+ return out.trim()
174
+ } finally { clearTimeout(t) }
175
+ }
176
+ // Prefer cheap haiku; fall back to the provider's default model if it's absent.
177
+ try { const out = await attempt('claude-haiku-4-5'); if (out) return { text: out, model: 'claude-haiku-4-5' } }
178
+ catch { /* provider has no haiku — fall through to default */ }
179
+ try { const out = await attempt(undefined); if (out) return { text: out, model: 'default' } }
180
+ catch { /* provider default also failed */ }
181
+ return null
182
+ }
183
+
142
184
  export function startClaudeSession({ cwd, model, resume, env, mode: initialMode = 'default', onEvent, requestPermission, mcpServers, crossPostGate, crossRoomPostGate, rolePrompt, blockSubagents = false, onSubmitPlan = null, onLaneDone = null, onReviewVerdict = null, reviewGate = null }) {
143
185
  const ac = new AbortController()
144
186
  let input = makeInputStream() // `let`: auto-restart swaps in a fresh stream
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thinkpool-pair",
3
- "version": "0.7.126",
3
+ "version": "0.7.127",
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": {