thinkpool-pair 0.7.238 → 0.7.239

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
@@ -50,6 +50,7 @@ import { validateProviderSwitch, providerSwitchPlan, BUILTIN_PROVIDER } from './
50
50
  import { createSdkMcpServer, tool } from '@anthropic-ai/claude-agent-sdk'
51
51
  import { z } from 'zod'
52
52
  import { readCodexDefaultModel, readCodexModels, codexConfigForMode, codexThreadCanResume } from './codex-session.mjs'
53
+ import { withMcpSessionFactory } from './codex-mcp-http.mjs'
53
54
  import { startStructuredSession } from './runtime-session.mjs'
54
55
  import { defaultStructuredMode, shouldDeferStructuredRuntime, structuredRuntimeForCommand, structuredRuntimeMetadata, structuredRuntimeSupportsMode } from './runtime-registry.mjs'
55
56
  import { probeHermesRuntime } from './hermes-probe.mjs'
@@ -2052,7 +2053,7 @@ function openStructured({ id, runtime = 'claude', model, models, effort, resume,
2052
2053
  // own `id` (excluded) + `entry` (per-turn call counter, reset by the code-turn
2053
2054
  // handler). Auto-allowed by the PreToolUse gate (mcp__thinkpool__ prefix).
2054
2055
  // Spec: docs/specs/2026-06-28-cross-terminal-awareness.md
2055
- const peekServer = createSdkMcpServer({
2056
+ const createPeekServer = () => createSdkMcpServer({
2056
2057
  name: 'thinkpool',
2057
2058
  version: '1.0.0',
2058
2059
  tools: [
@@ -2420,6 +2421,7 @@ function openStructured({ id, runtime = 'claude', model, models, effort, resume,
2420
2421
  )] : []),
2421
2422
  ],
2422
2423
  })
2424
+ const peekServer = withMcpSessionFactory(createPeekServer)
2423
2425
  // S5 (slice 1b) — the review-lane write-block, wired into the live PreToolUse path.
2424
2426
  // Null for builder lanes (no reviewSliceRoots) → their toolset is UNCHANGED. For a review
2425
2427
  // lane, deny a mutating tool call if it targets ANY reviewed slice's worktree, allowing the
@@ -6,18 +6,47 @@ import http from 'node:http'
6
6
  import { randomUUID } from 'node:crypto'
7
7
  import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'
8
8
 
9
+ export function withMcpSessionFactory(createSessionServer) {
10
+ if (typeof createSessionServer !== 'function') throw new TypeError('MCP session factory must be a function')
11
+ return Object.assign(createSessionServer(), { createSessionServer })
12
+ }
13
+
9
14
  export async function startCodexMcpHttp({ sdkServer, host = '127.0.0.1' } = {}) {
10
- const instance = sdkServer?.instance
11
- if (!instance?.connect) throw new TypeError('Codex MCP requires an SDK MCP server instance')
15
+ const createSessionServer = sdkServer?.createSessionServer
16
+ const initialInstance = sdkServer?.instance
17
+ if (!initialInstance?.connect || typeof createSessionServer !== 'function') {
18
+ throw new TypeError('Codex MCP requires an SDK MCP server and createSessionServer factory')
19
+ }
12
20
 
13
21
  const secretPath = `/mcp/${randomUUID()}`
14
- // Stateful sessions (not stateless): a stateless transport (sessionIdGenerator
15
- // undefined) 500s on the `notifications/initialized` POST that every MCP client
16
- // sends right after initialize — it has no session to attach the notification to.
17
- // Each connecting Codex process gets its own session id; the bridge owns one
18
- // server per lane, so the in-memory session map is bounded by the live lanes.
19
- const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID() })
20
- await instance.connect(transport)
22
+ // Stateful sessions are required: this SDK's stateless transport rejects the
23
+ // `notifications/initialized` POST sent by every MCP client. A transport is
24
+ // single-initialize, though, while Codex App Server -> exec fallback and Hermes
25
+ // crash recovery both start a new client against the same lane endpoint.
26
+ // Keep exactly one active child session per lane. A new initialize atomically
27
+ // retires the old server+transport before receiving its own isolated instance.
28
+ let active = null
29
+
30
+ const retireActive = async () => {
31
+ const current = active
32
+ active = null
33
+ if (!current) return
34
+ await Promise.allSettled([
35
+ current.instance.close(),
36
+ current.transport.close(),
37
+ ])
38
+ }
39
+
40
+ const beginSession = async () => {
41
+ await retireActive()
42
+ const next = createSessionServer()
43
+ const instance = next?.instance
44
+ if (!instance?.connect) throw new TypeError('Codex MCP session factory did not return an SDK MCP server instance')
45
+ const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID() })
46
+ await instance.connect(transport)
47
+ active = { instance, transport }
48
+ return active
49
+ }
21
50
 
22
51
  const server = http.createServer(async (req, res) => {
23
52
  if (req.url !== secretPath) {
@@ -25,7 +54,19 @@ export async function startCodexMcpHttp({ sdkServer, host = '127.0.0.1' } = {})
25
54
  return
26
55
  }
27
56
  try {
28
- await transport.handleRequest(req, res)
57
+ const sessionId = req.headers['mcp-session-id']
58
+ let target = active
59
+ if (!sessionId) {
60
+ if (req.method !== 'POST') {
61
+ res.writeHead(400).end('missing session')
62
+ return
63
+ }
64
+ target = await beginSession()
65
+ } else if (!target?.transport?.sessionId || sessionId !== target.transport.sessionId) {
66
+ res.writeHead(404).end('session not found')
67
+ return
68
+ }
69
+ await target.transport.handleRequest(req, res)
29
70
  } catch {
30
71
  if (!res.headersSent) res.writeHead(500)
31
72
  res.end()
@@ -38,7 +79,7 @@ export async function startCodexMcpHttp({ sdkServer, host = '127.0.0.1' } = {})
38
79
  server.listen(0, host, resolve)
39
80
  })
40
81
  } catch (error) {
41
- await transport.close().catch(() => {})
82
+ await retireActive()
42
83
  throw error
43
84
  }
44
85
  const address = server.address()
@@ -51,7 +92,7 @@ export async function startCodexMcpHttp({ sdkServer, host = '127.0.0.1' } = {})
51
92
  if (closed) return
52
93
  closed = true
53
94
  await Promise.allSettled([
54
- transport.close(),
95
+ retireActive(),
55
96
  new Promise((resolve) => server.close(resolve)),
56
97
  ])
57
98
  },
@@ -4,6 +4,7 @@ import path from 'node:path'
4
4
  import { randomUUID } from 'node:crypto'
5
5
  import { createSdkMcpServer, tool } from '@anthropic-ai/claude-agent-sdk'
6
6
  import { startHermesSession } from './hermes-session.mjs'
7
+ import { withMcpSessionFactory } from './codex-mcp-http.mjs'
7
8
 
8
9
  const waitFor = (predicate, timeoutMs = 25_000) => new Promise((resolve, reject) => {
9
10
  const started = Date.now()
@@ -36,10 +37,11 @@ export async function verifyHermesMcpIsolation({ rounds = 2, sessionFactory = st
36
37
  for (let i = 0; i < 2; i++) {
37
38
  const cwd = path.join(root, i === 0 ? 'alpha' : 'beta')
38
39
  fs.mkdirSync(cwd)
39
- const sdkServer = createSdkMcpServer({
40
+ const createMarkerServer = () => createSdkMcpServer({
40
41
  name: 'thinkpool', version: '1.0.0',
41
42
  tools: [tool(markers[i], 'Synthetic ThinkPool lane-isolation marker.', {}, async () => ({ content: [{ type: 'text', text: markers[i] }] }))],
42
43
  })
44
+ const sdkServer = withMcpSessionFactory(createMarkerServer)
43
45
  sessions.push(sessionFactory({ cwd, lazy: true, mcpServers: { thinkpool: sdkServer }, onEvent: (event) => events[i].push(event) }))
44
46
  }
45
47
  sessions.forEach((session) => session.sendTurn('/tools'))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thinkpool-pair",
3
- "version": "0.7.238",
3
+ "version": "0.7.239",
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": {