thinkpool-pair 0.7.201 → 0.7.203

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
@@ -98,6 +98,7 @@ const flowRedispatch = new Map()
98
98
  // broadcasts; without persistent state the cap can never bite.
99
99
  const flowBudgets = new Map()
100
100
  import { formatPeek, PEEK, siblingsOf, resolveSibling, crossPostDecision, CROSSPOST, spawnDecision, SPAWN, CROSSROOM, formatPairRoster, crossRoomPostDecision, formatRoomNow, formatClosableHint } from './cross-terminal.mjs'
101
+ import { armInterruptedResume, sendInterruptedContinue, supersedeInterruptedResume } from './interrupted-resume.mjs'
101
102
  import { turnInFlight } from './update-gate.mjs'
102
103
  import { saveSession, flushSession, deleteSession, loadAll, canResume, loadPtyId, savePtyId, loadNames, saveNames, appendDurableEvents, seedDurableEvents, readDurablePage, readDurableOldestSeq, hasDurableArchive, servesHistoryPage } from './session-store.mjs'
103
104
  import { stampEvent, makeSeqCounter, maxSeq, seqable, capReplayEvents, chunkReplayEvents, boundEventForBroadcast, inlineImageBlocks, usageReportLine, buildRecapFromLog, RECAP_CAP, trimmedBeforeSeq, firstSeq } from './event-id.mjs'
@@ -2238,7 +2239,10 @@ function openStructured({ id, runtime = 'claude', model, effort, resume, log, co
2238
2239
  { const onBuiltin = !entry.provider || entry.provider === 'anthropic'
2239
2240
  const m = onBuiltin ? (evt.model || evt.ctx?.model) : null
2240
2241
  if (m && m !== entry.model) { entry.model = m; announce() }
2241
- if (evt.kind === 'models' && Array.isArray(evt.models) && evt.models.length) { roomModels = evt.models; announce() } }
2242
+ // A custom Anthropic-compatible endpoint may expose its own catalog through the
2243
+ // same SDK method. Never publish that as the ROOM's Claude catalog: its models
2244
+ // already render in the provider group and otherwise leak under ANTHROPIC.
2245
+ if (onBuiltin && evt.kind === 'models' && Array.isArray(evt.models) && evt.models.length) { roomModels = evt.models; announce() } }
2242
2246
  // Cache the last usage/ctx meter on the entry (persisted + replayed — see
2243
2247
  // openStructured). Chrome events bypass pushLog/persist in emitTail below, so
2244
2248
  // usage needs an explicit persist() here to survive a bridge restart.
@@ -2271,9 +2275,7 @@ function openStructured({ id, runtime = 'claude', model, effort, resume, log, co
2271
2275
  // resume gets its single "continue" HERE — not on a blind timer that raced the ~40s
2272
2276
  // resume and dropped the turn into a dead input stream. Fires exactly once.
2273
2277
  if (evt.kind === 'system' && entry.pendingAutoResume) {
2274
- entry.pendingAutoResume = false
2275
- try { entry.session?.sendTurn('continue') } catch { /* not resumable — stays idle, user can continue manually */ }
2276
- process.stderr.write(`\n ◆ auto-resumed interrupted turn (${id.slice(0, 8)}) — sent continue.\n`)
2278
+ if (sendInterruptedContinue(entry)) process.stderr.write(`\n ◆ auto-resumed interrupted turn (${id.slice(0, 8)}) — sent continue.\n`)
2277
2279
  }
2278
2280
  // Context-carry (2026-07-08): a lane re-opened WITHOUT an SDK resume (provider switch,
2279
2281
  // non-resumable restart) starts blank. Fire the text-layer recap as its first turn the
@@ -2851,6 +2853,11 @@ channel
2851
2853
  s.hop = 0
2852
2854
  s.roomHop = 0 // a human turn is room-hop 0 — clears any injected cross-room hop depth
2853
2855
  const text = String(payload.text)
2856
+ // A human turn supersedes a restart's still-pending auto-continue. Without this,
2857
+ // their message starts the cold runtime; its init event then queues a second stale
2858
+ // `continue` behind the request. Codex normally resumes immediately at restore, while
2859
+ // Claude holds this flag only until its input stream is live.
2860
+ supersedeInterruptedResume(s)
2854
2861
  // Echo the turn so BOTH readers (and late joiners, via the log) show who
2855
2862
  // said what — UNLESS silent (an @pool fire, which renders its own line).
2856
2863
  const echoYou = () => {
@@ -3203,7 +3210,8 @@ channel
3203
3210
  // instant it's live. openStructured already closed the interrupted turn idle, so a
3204
3211
  // continue that can't be delivered just leaves it idle (recoverable) — never a hang.
3205
3212
  if (re && !rec.flowSessionId && canResume(rec) && restoredTurnOpen(rec.log || [])) {
3206
- re.pendingAutoResume = true
3213
+ const recovery = armInterruptedResume(re)
3214
+ if (recovery === 'sent') process.stderr.write(`\n ◆ auto-resumed interrupted Codex turn (${rec.id.slice(0, 8)}) — sent continue.\n`)
3207
3215
  }
3208
3216
  // Context-carry (2026-07-08): the SAME mid-turn case but the SDK context CANNOT
3209
3217
  // resume (canResume false — a stale/expired session). A bare "continue" would land
@@ -0,0 +1,29 @@
1
+ // Runtime-specific bridge-restart recovery.
2
+ //
3
+ // Claude owns a long-lived input stream: its continue must wait for the init system
4
+ // event proving that stream is consumed. Codex is one process per turn and has no idle
5
+ // init event: sending the prompt IS what launches the resumed process.
6
+
7
+ export function sendInterruptedContinue(entry) {
8
+ if (!entry) return false
9
+ entry.pendingAutoResume = false
10
+ try { return entry.session?.sendTurn?.('continue') !== false }
11
+ catch { return false }
12
+ }
13
+
14
+ export function armInterruptedResume(entry) {
15
+ if (!entry?.session) return 'unavailable'
16
+ if (entry.runtime === 'codex') return sendInterruptedContinue(entry) ? 'sent' : 'unavailable'
17
+ entry.pendingAutoResume = true
18
+ return 'armed'
19
+ }
20
+
21
+ // A real person speaking always wins the race. Otherwise a human turn that starts a
22
+ // cold Claude/Codex runtime can trigger its init event and enqueue a stale second
23
+ // `continue` behind the person's actual request.
24
+ export function supersedeInterruptedResume(entry) {
25
+ if (!entry?.pendingAutoResume) return false
26
+ entry.pendingAutoResume = false
27
+ return true
28
+ }
29
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thinkpool-pair",
3
- "version": "0.7.201",
3
+ "version": "0.7.203",
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": {
@@ -27,6 +27,7 @@
27
27
  "transcript-sanitize.mjs",
28
28
  "session-store.mjs",
29
29
  "cross-terminal.mjs",
30
+ "interrupted-resume.mjs",
30
31
  "flow-conductor.mjs",
31
32
  "flow-worktree.mjs",
32
33
  "flow-task-graph.mjs",