thinkpool-pair 0.7.270 → 0.7.271

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/codex-session.mjs +34 -3
  2. package/package.json +1 -1
package/codex-session.mjs CHANGED
@@ -543,8 +543,11 @@ export function startCodexSession({ cwd, model, effort: initialEffort = 'high',
543
543
 
544
544
  async function runAppServer(prompt, options = {}) {
545
545
  if (!await ensureAppServer()) return false
546
+ // Stop can land while the cold App Server / MCP bootstrap is still awaiting.
547
+ // The accepted turn was already closed at the room boundary; never start it
548
+ // after bootstrap finally resolves.
549
+ if (ended || aborted) return true
546
550
  mapper.setUsageBaseline(sessionId ? readCodexThreadUsage(sessionId) : null)
547
- aborted = false
548
551
  turnActive = true
549
552
  try {
550
553
  activeTurnId = await appServer.startTurn({
@@ -556,6 +559,13 @@ export function startCodexSession({ cwd, model, effort: initialEffort = 'high',
556
559
  approvalPolicy: modeConfig.approvalPolicy,
557
560
  collaborationMode: codexDefaultCollaborationMode(activeModel, activeEffort),
558
561
  })
562
+ // Stop may land after the RPC was written but before startTurn returns its
563
+ // id. abort() already closed the room boundary; interrupt the now-known
564
+ // native turn before waiting, and never let it continue invisibly.
565
+ if (aborted || ended) {
566
+ try { await appServer.interrupt({ threadId: sessionId, turnId: activeTurnId }) } catch { /* boundary is already closed */ }
567
+ return true
568
+ }
559
569
  const completed = await appServer.waitForTurn(activeTurnId)
560
570
  const status = completed?.turn?.status
561
571
  if (aborted || status === 'interrupted') {
@@ -592,13 +602,17 @@ export function startCodexSession({ cwd, model, effort: initialEffort = 'high',
592
602
 
593
603
  async function runAppServerReview(commandText) {
594
604
  if (!await ensureAppServer()) return false
605
+ if (ended || aborted) return true
595
606
  mapper.setUsageBaseline(sessionId ? readCodexThreadUsage(sessionId) : null)
596
- aborted = false
597
607
  turnActive = true
598
608
  try {
599
609
  const started = await appServer.startReview({ threadId: sessionId, target: codexReviewTarget(commandText) })
600
610
  activeTurnId = started?.turn?.id || started?.turnId
601
611
  if (!activeTurnId) throw new Error('Codex review/start returned no turn id')
612
+ if (aborted || ended) {
613
+ try { await appServer.interrupt({ threadId: sessionId, turnId: activeTurnId }) } catch { /* boundary is already closed */ }
614
+ return true
615
+ }
602
616
  const completed = await appServer.waitForTurn(activeTurnId)
603
617
  const status = completed?.turn?.status
604
618
  if (aborted || status === 'interrupted') {
@@ -628,6 +642,7 @@ export function startCodexSession({ cwd, model, effort: initialEffort = 'high',
628
642
  }
629
643
 
630
644
  async function runExec(prompt, options = {}) {
645
+ if (ended || aborted) return
631
646
  // First turn: fresh exec. Later turns: resume THIS lane's captured thread.
632
647
  // `--last` is process-global and can cross-wire two concurrently-active Codex
633
648
  // lanes, so it is never safe in a multi-lane bridge.
@@ -658,7 +673,6 @@ export function startCodexSession({ cwd, model, effort: initialEffort = 'high',
658
673
  const opts = { cwd, env: childEnv, stdio: ['ignore', 'pipe', 'pipe'] }
659
674
  if (stdinFd != null) opts.stdio = [stdinFd, 'pipe', 'pipe']
660
675
 
661
- aborted = false
662
676
  turnActive = true
663
677
  child = spawnImpl('codex', args, opts)
664
678
  if (stdinFd != null) { try { fs.closeSync(stdinFd) } catch { /* child owns its duplicated fd */ } }
@@ -777,18 +791,35 @@ export function startCodexSession({ cwd, model, effort: initialEffort = 'high',
777
791
  return true
778
792
  }
779
793
  queue.push({ text, options, promptIndex, forceFullReminder: thisTurnForceFull })
794
+ // Match Claude and Hermes: accepting an idle turn claims the lane before
795
+ // any cold App Server, MCP, or exec bootstrap. bridge.mjs announces
796
+ // synchronously after sendTurn(), so this is the edge that makes Thinking,
797
+ // Stop, and the tab spinner appear at dispatch time rather than at the
798
+ // first provider event many seconds later.
799
+ if (!turnActive) {
800
+ aborted = false
801
+ turnActive = true
802
+ }
780
803
  // start the pump if idle
781
804
  if (queue.length === 1) pump()
782
805
  return true
783
806
  },
784
807
  abort() {
808
+ const bootstrapOnly = turnActive && !activeTurnId && !child
785
809
  aborted = true
786
810
  queue.length = 0
787
811
  if (appServer && activeTurnId) { void appServer.interrupt({ threadId: sessionId, turnId: activeTurnId }).catch(() => {}) ; return }
788
812
  if (child) { try { child.kill('SIGTERM') } catch { /* noop */ } ; setTimeout(() => { try { child?.kill('SIGKILL') } catch { /* noop */ } }, 1500) }
813
+ else if (bootstrapOnly) {
814
+ // No native turn exists to produce a boundary. Close the optimistic
815
+ // accepted turn exactly once and let runAppServer's post-bootstrap
816
+ // aborted check discard the delayed startup.
817
+ emitTurnBoundary({ kind: 'result', subtype: 'aborted', sessionId, model: activeModel, costUsd: null, usage: null, numTurns: 0, durationMs: undefined, denials: 0, resultText: null })
818
+ }
789
819
  },
790
820
  end() {
791
821
  ended = true
822
+ turnActive = false
792
823
  queue.length = 0
793
824
  if (child) { try { child.kill() } catch { /* noop */ } }
794
825
  try { appServer?.end() } catch { /* noop */ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thinkpool-pair",
3
- "version": "0.7.270",
3
+ "version": "0.7.271",
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": {