thinkpool-pair 0.7.273 → 0.7.275
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/account.mjs +6 -8
- package/codex-app-server.mjs +53 -0
- package/codex-session.mjs +8 -2
- package/package.json +1 -1
- package/service.mjs +12 -15
- package/thinkpool-capabilities.json +8 -4
package/account.mjs
CHANGED
|
@@ -501,12 +501,11 @@ export async function runAccount(SUPABASE_URL, SUPABASE_ANON) {
|
|
|
501
501
|
acct.on('broadcast', { event: 'restart' }, () => {
|
|
502
502
|
restarting = true
|
|
503
503
|
// The dashboard "Restart bridge" button ALSO updates to the newest published version
|
|
504
|
-
// (Max, 2026-07-03).
|
|
505
|
-
//
|
|
506
|
-
//
|
|
507
|
-
//
|
|
508
|
-
//
|
|
509
|
-
// PATH would NOT (the 2026-07-03 gotcha that made a manual updater silently no-op).
|
|
504
|
+
// (Max, 2026-07-03). Use service.mjs's authoritative registry → immutable runtime →
|
|
505
|
+
// one-shot reload transaction directly. Spawning `npx install-service` here used a
|
|
506
|
+
// disposable cache that legacy services could delete underneath the updater. The
|
|
507
|
+
// service primitive arms an independent launchd handoff before this process is booted
|
|
508
|
+
// out, so the reload survives us without a second package execution tree.
|
|
510
509
|
// The reload IS the bounce; sessions resume from disk on the new version. Best-effort:
|
|
511
510
|
// if there's no service or npm is unreachable, we fall through to the plain bounce, so
|
|
512
511
|
// the button is never dead — and even a failed update degrades to today's behaviour.
|
|
@@ -525,8 +524,7 @@ export async function runAccount(SUPABASE_URL, SUPABASE_ANON) {
|
|
|
525
524
|
try {
|
|
526
525
|
const svc = await import('./service.mjs')
|
|
527
526
|
if (svc.serviceActive(null)) {
|
|
528
|
-
|
|
529
|
-
updating = true
|
|
527
|
+
updating = svc.updateService(null) !== false
|
|
530
528
|
}
|
|
531
529
|
} catch { /* no service / spawn failed → plain bounce below */ }
|
|
532
530
|
process.stderr.write(updating
|
package/codex-app-server.mjs
CHANGED
|
@@ -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 =
|
|
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
package/service.mjs
CHANGED
|
@@ -46,16 +46,12 @@ export function darwinServiceRunning(output) {
|
|
|
46
46
|
const text = String(output || '')
|
|
47
47
|
return /(^|\n)[ \t]*state = running[ \t]*($|\n)/.test(text) && /(^|\n)[ \t]*pid = \d+[ \t]*($|\n)/.test(text)
|
|
48
48
|
}
|
|
49
|
-
// Legacy auto-update services still use npx.
|
|
50
|
-
//
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
// state where `npm view` sees a version but `npx` reports ETARGET or no executable.
|
|
56
|
-
// The whole _npx directory is disposable; clear its children so the next launch is a
|
|
57
|
-
// genuinely clean install.
|
|
58
|
-
const cacheWipe = (home) => `rm -rf ${home}/.npm/_npx/* 2>/dev/null`
|
|
49
|
+
// Legacy auto-update services still use npx. They MUST use a bridge-owned cache:
|
|
50
|
+
// deleting ~/.npm/_npx corrupts unrelated interactive `npx thinkpool-pair` processes
|
|
51
|
+
// (0.7.274 removed account.mjs underneath a live launcher). The service cache is
|
|
52
|
+
// disposable and isolated, so clearing it cannot mutate a person's npm execution tree.
|
|
53
|
+
const serviceNpmCache = () => path.join(os.homedir(), '.thinkpool-pair', 'npm-cache')
|
|
54
|
+
const cacheWipe = (cacheRoot) => `rm -rf ${shq(path.join(cacheRoot, '_npx'))}/* 2>/dev/null`
|
|
59
55
|
|
|
60
56
|
// The version installing the service. By DEFAULT the service is pinned to this exact
|
|
61
57
|
// version (not @latest) so a future bad npm publish can't auto-roll to every machine's
|
|
@@ -130,6 +126,7 @@ export function buildArtifact(platform, { room = null, cmdArgs = [], autoUpdate
|
|
|
130
126
|
const logDir = path.join(os.homedir(), '.thinkpool-pair')
|
|
131
127
|
const log = path.join(logDir, `${id}.log`)
|
|
132
128
|
const servicePath = sanitizeServicePath(process.env.PATH || '', platform)
|
|
129
|
+
const npmCache = serviceNpmCache()
|
|
133
130
|
const tail = cmdArgs.length ? ['--', ...cmdArgs] : []
|
|
134
131
|
const desc = room ? `bridge (${room})` : 'account bridge'
|
|
135
132
|
// OS-supervised tiers (launchd KeepAlive / systemd Restart) don't need --supervise.
|
|
@@ -156,7 +153,7 @@ export function buildArtifact(platform, { room = null, cmdArgs = [], autoUpdate
|
|
|
156
153
|
const args = runtimeEntry
|
|
157
154
|
? [process.execPath, runtimeEntry, ...(room ? [room] : []), ...tail]
|
|
158
155
|
: staleProof
|
|
159
|
-
? ['/bin/bash', '-lc', `${cacheWipe(
|
|
156
|
+
? ['/bin/bash', '-lc', `${cacheWipe(npmCache)}; export NPM_CONFIG_CACHE=${shq(npmCache)}; exec ${[npx, ...pkgArgs, ...tail].map(shq).join(' ')}`]
|
|
160
157
|
: [npx, ...pkgArgs, ...tail]
|
|
161
158
|
const file = path.join(os.homedir(), 'Library', 'LaunchAgents', `${label(room)}.plist`)
|
|
162
159
|
const content = `<?xml version="1.0" encoding="UTF-8"?>
|
|
@@ -175,7 +172,7 @@ export function buildArtifact(platform, { room = null, cmdArgs = [], autoUpdate
|
|
|
175
172
|
<key>WorkingDirectory</key><string>${xml(cwd)}</string>
|
|
176
173
|
<key>StandardOutPath</key><string>${xml(log)}</string>
|
|
177
174
|
<key>StandardErrorPath</key><string>${xml(log)}</string>
|
|
178
|
-
<key>EnvironmentVariables</key><dict><key>PATH</key><string>${xml(servicePath)}</string>${autoUpdate ? '<key>THINKPOOL_PAIR_AUTOUPDATE</key><string>1</string>' : ''}</dict>
|
|
175
|
+
<key>EnvironmentVariables</key><dict><key>PATH</key><string>${xml(servicePath)}</string>${runtimeEntry ? '' : `<key>NPM_CONFIG_CACHE</key><string>${xml(npmCache)}</string>`}${autoUpdate ? '<key>THINKPOOL_PAIR_AUTOUPDATE</key><string>1</string>' : ''}</dict>
|
|
179
176
|
</dict></plist>\n`
|
|
180
177
|
// The destructive reload is intentionally NOT represented as an inline `post`
|
|
181
178
|
// command. installService stages this plist and hands the transaction to an
|
|
@@ -193,7 +190,7 @@ export function buildArtifact(platform, { room = null, cmdArgs = [], autoUpdate
|
|
|
193
190
|
const execStart = runtimeEntry
|
|
194
191
|
? args.map((a) => `'${a.replace(/'/g, "'\\''")}'`).join(' ')
|
|
195
192
|
: staleProof
|
|
196
|
-
? `/bin/bash -lc "${cacheWipe(
|
|
193
|
+
? `/bin/bash -lc "${cacheWipe(npmCache)}; export NPM_CONFIG_CACHE=${shq(npmCache)}; exec ${[npx, ...pkgArgs, ...tail].join(' ')}"`
|
|
197
194
|
: args.map((a) => `'${a.replace(/'/g, "'\\''")}'`).join(' ')
|
|
198
195
|
const content = `[Unit]
|
|
199
196
|
Description=ThinkPool Code ${desc}
|
|
@@ -211,7 +208,7 @@ RestartSec=2
|
|
|
211
208
|
RestartPreventExitStatus=0
|
|
212
209
|
WorkingDirectory=${cwd}
|
|
213
210
|
Environment=PATH=${servicePath}
|
|
214
|
-
${autoUpdate ? 'Environment=THINKPOOL_PAIR_AUTOUPDATE=1\n' : ''}StandardOutput=append:${log}
|
|
211
|
+
${runtimeEntry ? '' : `Environment=NPM_CONFIG_CACHE=${npmCache}\n`}${autoUpdate ? 'Environment=THINKPOOL_PAIR_AUTOUPDATE=1\n' : ''}StandardOutput=append:${log}
|
|
215
212
|
StandardError=append:${log}
|
|
216
213
|
|
|
217
214
|
[Install]
|
|
@@ -232,7 +229,7 @@ WantedBy=default.target
|
|
|
232
229
|
: room
|
|
233
230
|
? ['npx', '-y', ...onlineFlag, verSpec, room, '--supervise', ...tail].join(' ')
|
|
234
231
|
: ['npx', '-y', ...onlineFlag, verSpec].join(' ')
|
|
235
|
-
const content = `@echo off\r\ntitle thinkpool-pair ${id}\r\n${inner}\r\n`
|
|
232
|
+
const content = `@echo off\r\ntitle thinkpool-pair ${id}\r\n${runtimeEntry ? '' : `set "NPM_CONFIG_CACHE=${npmCache}"\r\n`}${inner}\r\n`
|
|
236
233
|
return { file, content, logDir, post: [], note: 'Installed to the Startup folder — runs at login' + (room ? ' with --supervise (auto-restart on crash).' : ' (account mode).') + ' Start it now without rebooting by double-clicking the .cmd, or run it from a terminal.' }
|
|
237
234
|
}
|
|
238
235
|
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"bundleVersion":
|
|
3
|
+
"bundleVersion": 5,
|
|
4
4
|
"contracts": [
|
|
5
5
|
{
|
|
6
6
|
"id": "room-coordination",
|
|
7
|
-
"version":
|
|
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
|
{
|