thinkpool-pair 0.7.270 → 0.7.272

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/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.272",
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": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "bundleVersion": 2,
3
+ "bundleVersion": 3,
4
4
  "contracts": [
5
5
  {
6
6
  "id": "room-coordination",
@@ -161,17 +161,17 @@
161
161
  },
162
162
  {
163
163
  "id": "design-workspace",
164
- "version": 1,
165
- "interactionPrompt": "DESIGN EDITING MODEL: a trusted source-backed mockup card offers Work on design. That explicit action—not ordinary Preview or unrelated artifact delivery—arms a persistent room-level Design workspace, opens the artifact directly in editable mode, and adds Design · Page name as a virtual Ensemble lane for both partners; it creates no terminal, agent runtime, or worker slot. Once armed, the Design lane reopens the active artifact at its latest verified revision without another agent turn, history search, HTML/path request, or re-selection. Design is the default canvas there and Preview is only a temporary interaction mode. Direct text, supported movement, and selected-image changes auto-stage as optimistic drafts in the bounded changes queue. Apply changes sends the ordered batch once to the producing lane, that lane edits the canonical authored HTML, and a successful desktop+mobile render advances the verified revision for the room. Never call a staged draft saved or live. A preview_capture card is visual evidence, not an editable Design artifact.",
166
- "turnReminder": "DESIGN ROUTE: authored HTML must produce a source-backed Thinkpool Design card with verified desktop and mobile renders. Work on design explicitly arms the persistent Design workspace and adds its virtual Design · Page Ensemble lane; Preview alone does not arm it. Do not duplicate the card renders as inline PNGs. Use inline PNG evidence only when no interactive Design artifact is available.",
164
+ "version": 2,
165
+ "interactionPrompt": "DESIGN EDITING MODEL: a trusted source-backed mockup card offers Edit in Design. That explicit action—not ordinary Preview or unrelated artifact delivery—arms a persistent room-level Design workspace, opens the artifact directly in editable mode, and adds Design · Page name as a virtual Ensemble lane for both partners; it creates no terminal, agent runtime, or worker slot. Once armed, the Design lane reopens the active artifact at its latest verified revision without another agent turn, history search, HTML/path request, or re-selection. Design is the default canvas there and Preview is only a temporary interaction mode. Direct text, supported movement, and selected-image changes auto-stage as optimistic drafts in the bounded changes queue. Apply changes sends the ordered batch once to the producing lane, that lane edits the canonical authored HTML, and a successful desktop+mobile render advances the verified revision for the room. Never call a staged draft saved or live. A preview_capture card is visual evidence, not an editable Design artifact.",
166
+ "turnReminder": "DESIGN ROUTE: authored HTML must produce a source-backed Thinkpool Design card with verified desktop and mobile renders. Edit in Design explicitly arms the persistent Design workspace and adds its virtual Design · Page Ensemble lane; Preview alone does not arm it. Do not duplicate the card renders as inline PNGs. Use inline PNG evidence only when no interactive Design artifact is available.",
167
167
  "impact": [
168
168
  {"path": "src/pages/code/design/"},
169
- {"path": "src/pages/code/structured.jsx", "diffPattern": "Work on design|openMockup|tp-mockup-view|sourceKnown"},
169
+ {"path": "src/pages/code/structured.jsx", "diffPattern": "Edit in Design|openMockup|tp-mockup-view|sourceKnown"},
170
170
  {"path": "src/pages/code/room.jsx", "diffPattern": "designWorkspace|designLane|activeDesign|DesignViewer"},
171
171
  {"path": "bridge/design-edit.mjs"}
172
172
  ],
173
173
  "evidence": [
174
- {"path": "src/pages/code/structured.jsx", "pattern": "Work on design"},
174
+ {"path": "src/pages/code/structured.jsx", "pattern": "Edit in Design"},
175
175
  {"path": "src/pages/code/design/workspace.js", "pattern": "armed: true"},
176
176
  {"path": "src/pages/code/room.jsx", "pattern": "designLaneSelected"},
177
177
  {"path": "src/pages/code/design/DesignViewer.jsx", "pattern": "createPortal\\(surface, laneHost\\)"},