thinkpool-pair 0.7.77 → 0.7.78

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.
Files changed (2) hide show
  1. package/claude-session.mjs +30 -1
  2. package/package.json +1 -1
@@ -177,6 +177,21 @@ export function startClaudeSession({ cwd, model, resume, env, mode: initialMode
177
177
  // the catch self-heals.
178
178
  const FORCE_STOP_MS = Math.max(STALL_MS * 3, parseInt(process.env.TP_FORCE_STOP_MS, 10) || 300000)
179
179
  let forceStopped = false
180
+ // Set while an interrupt is settling. q.interrupt() doesn't end the turn cleanly on this
181
+ // SDK — it surfaces a REDUNDANT pair of terminal results (subtype `aborted` AND
182
+ // `error_during_execution`) and re-inits the session. abort() emits the single canonical
183
+ // aborted boundary itself; this flag tells the result handler to swallow the SDK's extra
184
+ // teardown results so the web never sees a second turn-end. Without it, the web suppresses
185
+ // only the first (isAbortEcho) and the second buried the flushed steer — "Stop injects the
186
+ // queued message but nothing happens". Cleared by the first real (non-teardown) result.
187
+ // Verified: bridge/test-steer-after-abort.mjs (Maks 2026-06-26). The teardown results
188
+ // arrive async and can interleave with the NEXT turn (a late error_during_execution can
189
+ // land after the steer's own output), so the window is bounded by a timer rather than
190
+ // cleared on the next result — "all teardown has arrived" isn't observable. `success`
191
+ // results are never swallowed, so the flushed steer's completion always surfaces.
192
+ let interrupting = false
193
+ let interruptTimer = null
194
+ const INTERRUPT_SWALLOW_MS = 6000
180
195
  // ── Haiku suggestion fallback ──
181
196
  // Claude Code's own `prompt_suggestion` rarely fires in /code — the binary suppresses it
182
197
  // per-turn (cache_cold when a turn's tokens > 10k, rate_limit when throttled, first turn).
@@ -504,6 +519,11 @@ export function startClaudeSession({ cwd, model, resume, env, mode: initialMode
504
519
  }
505
520
  case 'result':
506
521
  if (m.session_id) sessionId = m.session_id
522
+ // Swallow the interrupt's redundant teardown results (see `interrupting` above):
523
+ // abort() already emitted the one canonical aborted boundary. `success` (and any
524
+ // non-teardown subtype) always emits; only the aborted/error_during_execution pair
525
+ // the interrupt churns out is dropped, and only inside the timer-bounded window.
526
+ if (interrupting && (m.subtype === 'aborted' || m.subtype === 'error_during_execution')) { turnActive = false; break }
507
527
  turnBaseOut = 0; curMsgOut = 0 // reset the live token count for the next turn
508
528
  turnActive = false // turn settled → stall watchdog stands down
509
529
  restartCount = 0 // a successful turn refills the auto-restart budget
@@ -590,6 +610,15 @@ export function startClaudeSession({ cwd, model, resume, env, mode: initialMode
590
610
  // Graceful interrupt (Esc / Stop) — stops the current turn but keeps the
591
611
  // session alive for the next one. ac.abort() is teardown only (end()).
592
612
  async abort() {
613
+ // Swallow the SDK's redundant interrupt teardown (aborted + error_during_execution +
614
+ // re-init) — we emit the one canonical boundary below. Set BEFORE interrupt so the
615
+ // teardown results, which arrive async on the query loop, are caught; the timer bounds
616
+ // the window so a genuine later turn failure still surfaces.
617
+ if (turnActive) {
618
+ interrupting = true
619
+ clearTimeout(interruptTimer)
620
+ interruptTimer = setTimeout(() => { interrupting = false }, INTERRUPT_SWALLOW_MS)
621
+ }
593
622
  try { await q?.interrupt?.() } catch { /* noop */ }
594
623
  // interrupt() stops the turn but emits NO terminal message. Without one the
595
624
  // struct stream ends on a non-terminal event, so on resume the SDK treats the
@@ -599,7 +628,7 @@ export function startClaudeSession({ cwd, model, resume, env, mode: initialMode
599
628
  // SDK already surfaced one for the interrupt. (restored from 0.7.49)
600
629
  if (turnActive) { turnActive = false; emit({ kind: 'result', subtype: 'aborted', sessionId }) }
601
630
  },
602
- end() { closed = true; clearInterval(stallTimer); input.end(); try { ac.abort() } catch { /* noop */ } },
631
+ end() { closed = true; clearInterval(stallTimer); clearTimeout(interruptTimer); input.end(); try { ac.abort() } catch { /* noop */ } },
603
632
  get sessionId() { return sessionId },
604
633
  get mode() { return mode },
605
634
  get turnActive() { return turnActive }, // a turn is in flight (gates between-turns update restart — Slice 3 Contract #1)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thinkpool-pair",
3
- "version": "0.7.77",
3
+ "version": "0.7.78",
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": {