thinkpool-pair 0.7.273 → 0.7.274

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.
@@ -99,6 +99,8 @@ export class CodexAppServerClient {
99
99
  this.pending = new Map()
100
100
  this.turnWaiters = new Map()
101
101
  this.completedTurns = new Map()
102
+ this.mcpStartup = new Map()
103
+ this.mcpWaiters = new Map()
102
104
  this.closedError = null
103
105
  this.stderrTail = ''
104
106
  }
@@ -196,6 +198,25 @@ export class CodexAppServerClient {
196
198
  }
197
199
  }
198
200
  }
201
+ if (message.method === 'mcpServer/startupStatus/updated') {
202
+ const params = message.params || {}
203
+ const threadId = String(params.threadId || '')
204
+ const name = String(params.name || '')
205
+ if (threadId && name) {
206
+ const key = `${threadId}\0${name}`
207
+ this.mcpStartup.set(key, params)
208
+ const waiter = this.mcpWaiters.get(key)
209
+ if (waiter && params.status === 'ready') {
210
+ this.mcpWaiters.delete(key)
211
+ if (waiter.timer) clearTimeout(waiter.timer)
212
+ waiter.resolve(params)
213
+ } else if (waiter && (params.status === 'failed' || params.error || params.failureReason)) {
214
+ this.mcpWaiters.delete(key)
215
+ if (waiter.timer) clearTimeout(waiter.timer)
216
+ waiter.reject(new Error(`Codex MCP server ${name} failed to start: ${params.error || params.failureReason || params.status}`))
217
+ }
218
+ }
219
+ }
199
220
  try { this.onNotification?.(message.method, message.params || {}) } catch { /* consumer isolation */ }
200
221
  }
201
222
 
@@ -208,6 +229,12 @@ export class CodexAppServerClient {
208
229
  for (const { reject } of this.turnWaiters.values()) reject(this.closedError)
209
230
  this.turnWaiters.clear()
210
231
  this.completedTurns.clear()
232
+ for (const { reject, timer } of this.mcpWaiters.values()) {
233
+ if (timer) clearTimeout(timer)
234
+ reject(this.closedError)
235
+ }
236
+ this.mcpWaiters.clear()
237
+ this.mcpStartup.clear()
211
238
  try { this.onClose?.(this.closedError) } catch { /* consumer isolation */ }
212
239
  }
213
240
 
@@ -221,6 +248,32 @@ export class CodexAppServerClient {
221
248
  return id
222
249
  }
223
250
 
251
+ waitForMcpServer({ threadId, name, timeoutMs = this.threadTimeoutMs } = {}) {
252
+ const safeThreadId = String(threadId || '')
253
+ const safeName = String(name || '')
254
+ if (!safeThreadId || !safeName) return Promise.reject(new Error('Codex MCP readiness needs a thread id and server name'))
255
+ const key = `${safeThreadId}\0${safeName}`
256
+ const current = this.mcpStartup.get(key)
257
+ if (current?.status === 'ready') return Promise.resolve(current)
258
+ if (current && (current.status === 'failed' || current.error || current.failureReason)) {
259
+ return Promise.reject(new Error(`Codex MCP server ${safeName} failed to start: ${current.error || current.failureReason || current.status}`))
260
+ }
261
+ if (this.mcpWaiters.has(key)) return this.mcpWaiters.get(key).promise
262
+ let timer = null
263
+ let resolveWaiter
264
+ let rejectWaiter
265
+ const promise = new Promise((resolve, reject) => {
266
+ resolveWaiter = resolve
267
+ rejectWaiter = reject
268
+ if (timeoutMs > 0) timer = setTimeout(() => {
269
+ this.mcpWaiters.delete(key)
270
+ reject(new Error(`Codex MCP server ${safeName} did not become ready within ${timeoutMs}ms`))
271
+ }, timeoutMs)
272
+ })
273
+ this.mcpWaiters.set(key, { promise, resolve: resolveWaiter, reject: rejectWaiter, timer })
274
+ return promise
275
+ }
276
+
224
277
  async startTurn({ threadId, input, images, model, effort, approvalPolicy, collaborationMode } = {}) {
225
278
  const result = await this.request('turn/start', {
226
279
  threadId,
package/codex-session.mjs CHANGED
@@ -352,7 +352,7 @@ function appServerItemForMapper(item) {
352
352
  * @param {string} [o.providerConfig] optional -c overrides / provider block (M2: from the provider registry)
353
353
  * @returns {{ sendTurn(text, options?), abort(), end(), readonly sessionId }}
354
354
  */
355
- export function startCodexSession({ cwd, model, effort: initialEffort = 'high', resume, env, sandbox, mode = 'default', onEvent, providerConfig, terminalRolePrompt, rolePrompt, roomContext, mcpServers, prepareCwd = null, requestPermission, appServerGate = codexAppServerGate, appServerFactory = createCodexAppServer, spawnImpl = spawn }) {
355
+ export function startCodexSession({ cwd, model, effort: initialEffort = 'high', resume, env, sandbox, mode = 'default', onEvent, providerConfig, terminalRolePrompt, rolePrompt, roomContext, mcpServers, prepareCwd = null, requestPermission, appServerGate = codexAppServerGate, appServerFactory = createCodexAppServer, mcpHttpFactory = startCodexMcpHttp, spawnImpl = spawn }) {
356
356
  let activeMode = CODEX_MODE_CONFIG[mode] ? mode : 'default'
357
357
  let modeConfig = codexConfigForMode(activeMode)
358
358
  sandbox = normalizeCodexSandbox(sandbox || modeConfig.sandbox)
@@ -424,7 +424,7 @@ export function startCodexSession({ cwd, model, effort: initialEffort = 'high',
424
424
  async function ensureMcpHttp() {
425
425
  const thinkpool = mcpServers?.thinkpool
426
426
  if (!thinkpool) return null
427
- if (!mcpHttpPromise) mcpHttpPromise = startCodexMcpHttp({ sdkServer: thinkpool })
427
+ if (!mcpHttpPromise) mcpHttpPromise = mcpHttpFactory({ sdkServer: thinkpool })
428
428
  return mcpHttpPromise
429
429
  }
430
430
 
@@ -526,6 +526,12 @@ export function startCodexSession({ cwd, model, effort: initialEffort = 'high',
526
526
  // independent ThinkPool sandbox / approval posture above.
527
527
  config: { 'features.default_mode_request_user_input': true },
528
528
  })
529
+ // App Server reports thread/start before its per-thread MCP clients finish
530
+ // initializing. Starting the first turn in that gap snapshots a toolset
531
+ // without ThinkPool even though the server becomes ready milliseconds later.
532
+ // Hold the turn boundary until the room MCP is actually ready; a timeout or
533
+ // startup failure drops into the required-MCP codex exec fallback below.
534
+ if (peer?.url) await appServer.waitForMcpServer({ threadId: sessionId, name: 'thinkpool' })
529
535
  appServerThreadReady = true
530
536
  pushMappedEvent({ type: 'thread.started', thread_id: sessionId })
531
537
  return true
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thinkpool-pair",
3
- "version": "0.7.273",
3
+ "version": "0.7.274",
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,10 +1,10 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "bundleVersion": 4,
3
+ "bundleVersion": 5,
4
4
  "contracts": [
5
5
  {
6
6
  "id": "room-coordination",
7
- "version": 1,
7
+ "version": 2,
8
8
  "routes": [
9
9
  {
10
10
  "id": "room-awareness",
@@ -27,12 +27,16 @@
27
27
  ],
28
28
  "impact": [
29
29
  {"path": "bridge/cross-terminal.mjs"},
30
- {"path": "bridge/bridge.mjs", "diffPattern": "read_terminal|list_sessions|read_session|post_to_terminal|post_to_session"}
30
+ {"path": "bridge/bridge.mjs", "diffPattern": "read_terminal|list_sessions|read_session|post_to_terminal|post_to_session"},
31
+ {"path": "bridge/codex-session.mjs", "diffPattern": "MCP|Mcp|mcp|read_terminal|list_sessions|read_session|post_to_terminal|post_to_session"},
32
+ {"path": "bridge/codex-app-server.mjs", "diffPattern": "MCP|Mcp|mcp"}
31
33
  ],
32
34
  "evidence": [
33
35
  {"path": "bridge/cross-terminal.mjs", "pattern": "read_terminal"},
34
36
  {"path": "bridge/cross-terminal.mjs", "pattern": "post_to_session"},
35
- {"path": "bridge/bridge.mjs", "pattern": "'list_sessions'"}
37
+ {"path": "bridge/bridge.mjs", "pattern": "'list_sessions'"},
38
+ {"path": "bridge/codex-session.mjs", "pattern": "waitForMcpServer"},
39
+ {"path": "bridge/codex-app-server.mjs", "pattern": "mcpServer/startupStatus/updated"}
36
40
  ]
37
41
  },
38
42
  {