thinkpool-pair 0.7.272 → 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.272",
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": 3,
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
  {
@@ -161,9 +165,9 @@
161
165
  },
162
166
  {
163
167
  "id": "design-workspace",
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.",
168
+ "version": 3,
169
+ "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 beneath the producing terminal in its Ensemble row for both partners; it creates no terminal, agent runtime, or worker slot. The virtual lane can be closed from that Ensemble row without deleting the artifact. 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; queued edits can be reopened, revised, or removed before Apply. 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. For direction rounds, deliver every option as its own source-backed Design card; a multi-option gallery may accompany them for comparison but must never be the only editable artifact.",
170
+ "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 beneath the producing terminal in the Ensemble row; Preview alone does not arm it. For direction rounds, deliver every option as a separate source-backed Design card; gallery navigation is not a substitute for separately editable artifacts. Do not duplicate the card renders as inline PNGs. Use inline PNG evidence only when no interactive Design artifact is available.",
167
171
  "impact": [
168
172
  {"path": "src/pages/code/design/"},
169
173
  {"path": "src/pages/code/structured.jsx", "diffPattern": "Edit in Design|openMockup|tp-mockup-view|sourceKnown"},