thinkpool-pair 0.7.138 → 0.7.140

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
@@ -2262,12 +2262,13 @@ channel
2262
2262
  saveNames(room, termNames)
2263
2263
  announce()
2264
2264
  })
2265
- // Session Info panel (2026-07-02) — a cheap one-shot recap on demand. NAMES live
2266
- // terminals with doing/done status; aggregates CLOSED terminals into one line
2267
- // without naming them (Max's spec). Runs on the room's own subscription auth via
2268
- // oneShotSummary (cheap haiku → provider-default fallback). Never auto — only on
2269
- // the panel button. The web caches the result in React state (no re-charge on
2270
- // re-open within a page load).
2265
+ // Session Info panel (2026-07-02, plain-English recap 2026-07-02) — a cheap one-shot
2266
+ // recap on demand. Writes a plain-English "Done so far" / "Working on now" bullet
2267
+ // summary (Markdown) of what the whole session accomplished + what's live — NOT a
2268
+ // per-terminal technical list (Max's ask: clean, readable, no jargon/terminal names).
2269
+ // Runs on the room's own subscription auth via oneShotSummary (cheap haiku →
2270
+ // provider-default fallback). Never auto — only on the panel button. The web caches
2271
+ // the result in React state (no re-charge on re-open within a page load).
2271
2272
  .on('broadcast', { event: 'code-summarize' }, async ({ payload }) => {
2272
2273
  const by = payload?.by || null
2273
2274
  // Peek — any device opening the panel fetches the cached recap (cross-device +
@@ -2317,17 +2318,27 @@ channel
2317
2318
  ? `\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)}`
2318
2319
  : ''
2319
2320
 
2320
- const prompt = `Summarize this multi-terminal coding session as a STRUCTURED LIST. Output ONLY these lines, nothing else:
2321
- - ONE line per LIVE terminal, EXACTLY: <terminal name> | <working or done> | <one clause, max 12 words, no trailing period>
2322
- - If there are CLOSED terminals, ONE final line, EXACTLY: CLOSED | <count> | <one aggregate clause about what they handled — do NOT name them>
2323
- Use the pipe "|" as the separator. No headers, no markdown, no bullets, no extra prose. Example:
2324
- Bridge Whip | working | SDK 0.7.128, detection gates live
2325
- Reddit | done | GTM strategy mapped
2326
- CLOSED | 52 | flow-trigger churn and @pool integration
2321
+ const prompt = `Write a short, friendly recap of this coding session for someone non-technical who just wants to know what's going on. Plain English only — no jargon, no file names, no branch names, no terminal names or IDs, no code, no counts of terminals. Explain what the work MEANS and why it matters, not the mechanics.
2322
+
2323
+ Output ONLY this Markdown shape, nothing else:
2324
+
2325
+ **Done so far**
2326
+ - <one plain-English thing that got finished>
2327
+ - <another>
2328
+
2329
+ **Working on now**
2330
+ - <what's actively being worked on, in plain English>
2331
+
2332
+ Rules:
2333
+ - Merge ALL the finished work — live and closed terminals alike — into "Done so far". Combine similar items; do NOT organise by terminal or list them. Aim for 3–6 bullets.
2334
+ - Put only genuinely in-progress work under "Working on now". If nothing is active, write exactly: "- Nothing in progress right now — everything's wrapped up."
2335
+ - Each bullet is one clear sentence, ~16 words max, no trailing period, and no technical term a normal person wouldn't understand.
2336
+
2337
+ Here's the raw session activity to summarize:
2327
2338
 
2328
2339
  ${liveBlock}${closedBlock}
2329
2340
 
2330
- Output:`
2341
+ Recap:`
2331
2342
 
2332
2343
  const res = await oneShotSummary({ prompt, env: env || process.env, cwd: cwd || process.cwd() })
2333
2344
  if (!res || !res.text) { bcast('code-summary', { status: 'error', error: 'Summary model unavailable on this provider.', by }); return }
@@ -185,6 +185,12 @@ export function startClaudeSession({ cwd, model, resume, env, mode: initialMode
185
185
  const ac = new AbortController()
186
186
  let input = makeInputStream() // `let`: auto-restart swaps in a fresh stream
187
187
  let sessionId = resume || null
188
+ // persistedSessionId — the newest session id we KNOW is written to disk (the resume we
189
+ // were handed, then advanced on each successful result). A model-switch re-create must
190
+ // resume THIS, never the live `sessionId`: a resumed query can fork to a fresh session id
191
+ // that isn't persisted until a turn completes, so re-creating on it fails with
192
+ // "No conversation found" (Max 2026-07-02). This is always a resumable id.
193
+ let persistedSessionId = resume || null
188
194
  let closed = false
189
195
  // Lazy boot (2026-07-02): a RESTORED-IDLE terminal returns a full session object but
190
196
  // defers the expensive query() cold-start (MCP + settingSources, ~50s each) until its
@@ -640,7 +646,11 @@ export function startClaudeSession({ cwd, model, resume, env, mode: initialMode
640
646
  if (mcpServers && Object.keys(mcpServers).length) opts.mcpServers = { ...(opts.mcpServers || {}), ...mcpServers }
641
647
  if (cwd) opts.cwd = cwd
642
648
  if (model) opts.model = model
643
- if (resume) opts.resume = resume
649
+ // NB: resume is NOT baked into opts — runQuery sets it per-run from the live `sessionId`
650
+ // (seeded from `resume`). That makes the resume target controllable: a re-create can point
651
+ // it at persistedSessionId, and the "No conversation found" recovery can DROP it (clearing
652
+ // sessionId) to re-run fresh. If it were in opts, the {...opts} spread would keep resuming
653
+ // the dead id even after we cleared sessionId (Max 2026-07-02 recovery loop).
644
654
  // Force-enable Claude Code's predicted-next-prompt. `promptSuggestions: true`
645
655
  // alone defers to the server-side `tengu_prompt_suggestion` feature gate, which
646
656
  // Anthropic flips on/off during rollout — so the ghost-text chip silently died
@@ -820,9 +830,10 @@ export function startClaudeSession({ cwd, model, resume, env, mode: initialMode
820
830
  // Defer to the next tick so recreateForSwitch (which awaits THIS loop's qDone)
821
831
  // doesn't await itself; the setTimeout runs after this iterator yields.
822
832
  if (pendingSwitch) { pendingSwitch = false; setTimeout(() => { if (!closed) recreateForSwitch() }, 0) }
823
- if (m.subtype === 'success') restartCount = 0 // ONLY a real success refills the
824
- // auto-restart budget — else a flapping connection that lands one aborted turn
825
- // between drops refills every cycle and loops far past RESTART_MAX.
833
+ if (m.subtype === 'success') { restartCount = 0; if (sessionId) persistedSessionId = sessionId } // ONLY a real success refills the
834
+ // auto-restart budget (else a flapping connection that lands one aborted turn between
835
+ // drops refills every cycle past RESTART_MAX) — and marks this session id durably
836
+ // persisted so a later model-switch re-create can safely resume it.
826
837
  forceStopped = false
827
838
  // Ground-truth the model from THIS turn's modelUsage (the models actually billed
828
839
  // this turn), not the init/system messages — a RESUMED session REPLAYS the prior
@@ -891,6 +902,19 @@ export function startClaudeSession({ cwd, model, resume, env, mode: initialMode
891
902
  restartTimer = setTimeout(() => { restartTimer = null; if (!closed) runQuery() }, Math.min(1000 * restartCount, 4000)) // 1s, 2s, 4s backoff
892
903
  return
893
904
  }
905
+ // Resume target vanished (a forked session id that was never persisted, or a pruned
906
+ // transcript): drop the resume and re-run FRESH so the turn/switch still lands instead
907
+ // of hard-failing. Bounded by RESTART_MAX. Loses only the resumed history, not the turn.
908
+ if (/no conversation found/i.test(msg) && sessionId && restartCount < RESTART_MAX) {
909
+ restartCount++
910
+ turnActive = false
911
+ sessionId = null; persistedSessionId = null
912
+ const oldInput = input
913
+ input = makeInputStream()
914
+ try { oldInput.end() } catch { /* noop */ }
915
+ restartTimer = setTimeout(() => { restartTimer = null; if (!closed) runQuery() }, 300)
916
+ return
917
+ }
894
918
  // Non-recoverable, or budget exhausted: mark done so sendTurn stops
895
919
  // silently queuing into a dead stream. The bridge's onEvent self-heal
896
920
  // still fires for "No conversation found" (it reopens fresh).
@@ -920,6 +944,11 @@ export function startClaudeSession({ cwd, model, resume, env, mode: initialMode
920
944
  try { oldInput.end() } catch { /* noop */ }
921
945
  try { await dying } catch { /* noop */ } // wait until it's actually dead
922
946
  await sleep(300) // brief lock-release margin
947
+ // Resume the last DURABLY-PERSISTED session, not the live one: the aborted query may
948
+ // have forked to a session id that was never written to disk, and resuming that fails
949
+ // with "No conversation found" (Max 2026-07-02, sid 6db6c1e3). persistedSessionId is
950
+ // always resumable (the restore id, advanced on each success).
951
+ sessionId = persistedSessionId
923
952
  if (!closed) runQuery() // relaunch on opts.model + resume
924
953
  }
925
954
  if (!lazy) runQuery()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thinkpool-pair",
3
- "version": "0.7.138",
3
+ "version": "0.7.140",
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": {