thinkpool-pair 0.7.36 → 0.7.39

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 CHANGED
@@ -260,13 +260,20 @@ export async function runAccount(SUPABASE_URL, SUPABASE_ANON) {
260
260
  const tick = async () => {
261
261
  let rooms = []
262
262
  try {
263
- const { data } = await sb.from('code_sessions').select('code,name,last_active_at').order('last_active_at', { ascending: false }).limit(20)
263
+ // Owner-only discovery (Contract C-CODE-2): a host bridges ONLY the sessions
264
+ // it CREATED. RLS on code_sessions returns owned OR participant rooms, so
265
+ // without this owner filter the supervisor auto-serves rooms you merely JOINED
266
+ // as a guest — spawning your machine into another owner's session that already
267
+ // runs its own bridge. The web watches tpacct:<session-owner>, so a
268
+ // participant's bridge is duplication the UI never even looks at.
269
+ const { data } = await sb.from('code_sessions').select('code,name,last_active_at,owner_id').eq('owner_id', session.user.id).order('last_active_at', { ascending: false }).limit(20)
264
270
  rooms = data || []
265
271
  } catch { /* transient — keep existing children, retry next tick */ }
266
272
  const dirs = loadDirs()
267
273
  for (const r of rooms) {
268
274
  const room = r.code
269
275
  if (!room || children.has(room)) continue
276
+ if (r.owner_id && r.owner_id !== session.user.id) continue // never serve a room we don't own (defense-in-depth behind the .eq above)
270
277
  const dir = dirs[room] || DEFAULT_DIR // auto-serve unbound sessions in the launch dir
271
278
  if (!fs.existsSync(dir)) {
272
279
  if (!warned.has(room)) { warned.add(room); process.stderr.write(`\n ◇ ${room}: bound directory is gone (${dir}) — re-bind it.\n`) }
package/bridge.mjs CHANGED
@@ -151,7 +151,9 @@ if (argv[0] === 'install-service' || argv[0] === 'uninstall-service') {
151
151
  // shell export can't reach a serviced bridge — provider.json is its own source
152
152
  // of truth. Idempotent; a no-op for the default Anthropic provider. See provider.mjs.
153
153
  const { applyProviderEnv } = await import('./provider.mjs')
154
- applyProviderEnv()
154
+ // Captured so the room announce can report the active LLM provider (anthropic,
155
+ // or a custom Anthropic-compatible endpoint) to the web welcome banner.
156
+ const PROVIDER_INFO = applyProviderEnv()
155
157
 
156
158
  if (argv[0] === 'login') { const { runLogin } = await import('./account.mjs'); await runLogin(SUPABASE_URL, SUPABASE_ANON, WEB_BASE) }
157
159
  if (argv[0] === 'bind') { const { runBind } = await import('./account.mjs'); runBind(argv[1], argv[2]) }
@@ -415,6 +417,14 @@ const announce = () =>
415
417
  // cwd + version: the host's working dir + thinkpool-pair version, shown in
416
418
  // the room's welcome banner. Re-sent per announce so late joiners get them.
417
419
  cwd, version: VERSION,
420
+ // provider: the LLM endpoint this bridge drives Claude Code through —
421
+ // 'anthropic' (the default, the host's regular Claude login) or 'custom'
422
+ // (any Anthropic-compatible base url: GLM/Z.ai, OpenRouter, a proxy). Shown
423
+ // in the room banner so both drivers see whose model they're on.
424
+ // providerBaseUrl rides along only for custom so the banner can show the
425
+ // host (e.g. openrouter.ai). Older web clients ignore unknown fields.
426
+ provider: PROVIDER_INFO.provider,
427
+ providerBaseUrl: PROVIDER_INFO.baseUrl || undefined,
418
428
  // pendingUpdate: a newer thinkpool-pair is out — drives the room's "update ready"
419
429
  // chip; re-sent per announce so reloads/late-joiners see it (Slice 3).
420
430
  pendingUpdate,
@@ -123,6 +123,14 @@ export function startClaudeSession({ cwd, model, resume, env, mode: initialMode
123
123
  const input = makeInputStream()
124
124
  let sessionId = resume || null
125
125
  let closed = false
126
+ // Cold-start measurement (2026-06-21, spec 2026-06-21-code-session-deploy-stability):
127
+ // stamp when the open begins so we can log spawn→ready (the SDK's first `init`
128
+ // system message — when the terminal becomes usable). New room terminals "take a
129
+ // long time"; settingSources:['user','project','local'] boots every host MCP
130
+ // server + SessionStart hook + plugin before init. This is the A-side baseline;
131
+ // open a terminal with TP_MCP_STRICT=1 (below) for the B-side to isolate MCP's share.
132
+ const spawnT0 = Date.now()
133
+ let readyLogged = false
126
134
  let q = null // the live Query — control requests (interrupt /
127
135
  // setPermissionMode) route through it once streaming.
128
136
  // The session is CREATED in the caller's chosen mode (not hard-coded default):
@@ -302,6 +310,13 @@ export function startClaudeSession({ cwd, model, resume, env, mode: initialMode
302
310
  // the loop; only the coarse thinking_tokens system message is surfaced.
303
311
  includePartialMessages: true,
304
312
  }
313
+ // Measurement lever (default OFF — no behavior change). TP_MCP_STRICT=1 makes the
314
+ // room session skip booting the host's filesystem-configured MCP servers
315
+ // (strictMcpConfig + empty mcpServers map), isolating the MCP share of cold-start
316
+ // in the spawn→ready log below. settingSources still loads slash commands /
317
+ // CLAUDE.md / agents either way — strict only governs MCP server sourcing. Flip
318
+ // this to the default (or curate a slim allowlist) once the numbers justify it.
319
+ if (process.env.TP_MCP_STRICT === '1') { opts.strictMcpConfig = true; opts.mcpServers = {} }
305
320
  if (cwd) opts.cwd = cwd
306
321
  if (model) opts.model = model
307
322
  if (resume) opts.resume = resume
@@ -343,6 +358,14 @@ export function startClaudeSession({ cwd, model, resume, env, mode: initialMode
343
358
  break
344
359
  }
345
360
  if (m.session_id) sessionId = m.session_id
361
+ // Cold-start timing — the init system message is when the terminal is
362
+ // usable, so spawn→here IS the latency the user waits on. One line per
363
+ // open. Compare a normal open vs one with TP_MCP_STRICT=1 to read off how
364
+ // many ms the host MCP servers cost. (resume opens also pay sanitizeSession.)
365
+ if (!readyLogged) {
366
+ readyLogged = true
367
+ process.stderr.write(`\n ◆ session ready in ${Date.now() - spawnT0}ms — MCP ${process.env.TP_MCP_STRICT === '1' ? 'OFF' : 'on'}${resume ? ', resume' : ', fresh'}.\n`)
368
+ }
346
369
  // m.slash_commands (init message) — the commands this session really
347
370
  // supports: built-ins + the host's custom .claude/commands. Surfaced
348
371
  // so the room composer's autocomplete lists what ACTUALLY exists.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thinkpool-pair",
3
- "version": "0.7.36",
3
+ "version": "0.7.39",
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": {