thinkpool-pair 0.7.201 → 0.7.202

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'
@@ -2271,9 +2272,7 @@ function openStructured({ id, runtime = 'claude', model, effort, resume, log, co
2271
2272
  // resume gets its single "continue" HERE — not on a blind timer that raced the ~40s
2272
2273
  // resume and dropped the turn into a dead input stream. Fires exactly once.
2273
2274
  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`)
2275
+ if (sendInterruptedContinue(entry)) process.stderr.write(`\n ◆ auto-resumed interrupted turn (${id.slice(0, 8)}) — sent continue.\n`)
2277
2276
  }
2278
2277
  // Context-carry (2026-07-08): a lane re-opened WITHOUT an SDK resume (provider switch,
2279
2278
  // non-resumable restart) starts blank. Fire the text-layer recap as its first turn the
@@ -2851,6 +2850,11 @@ channel
2851
2850
  s.hop = 0
2852
2851
  s.roomHop = 0 // a human turn is room-hop 0 — clears any injected cross-room hop depth
2853
2852
  const text = String(payload.text)
2853
+ // A human turn supersedes a restart's still-pending auto-continue. Without this,
2854
+ // their message starts the cold runtime; its init event then queues a second stale
2855
+ // `continue` behind the request. Codex normally resumes immediately at restore, while
2856
+ // Claude holds this flag only until its input stream is live.
2857
+ supersedeInterruptedResume(s)
2854
2858
  // Echo the turn so BOTH readers (and late joiners, via the log) show who
2855
2859
  // said what — UNLESS silent (an @pool fire, which renders its own line).
2856
2860
  const echoYou = () => {
@@ -3203,7 +3207,8 @@ channel
3203
3207
  // instant it's live. openStructured already closed the interrupted turn idle, so a
3204
3208
  // continue that can't be delivered just leaves it idle (recoverable) — never a hang.
3205
3209
  if (re && !rec.flowSessionId && canResume(rec) && restoredTurnOpen(rec.log || [])) {
3206
- re.pendingAutoResume = true
3210
+ const recovery = armInterruptedResume(re)
3211
+ if (recovery === 'sent') process.stderr.write(`\n ◆ auto-resumed interrupted Codex turn (${rec.id.slice(0, 8)}) — sent continue.\n`)
3207
3212
  }
3208
3213
  // Context-carry (2026-07-08): the SAME mid-turn case but the SDK context CANNOT
3209
3214
  // 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.202",
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",