thinkpool-pair 0.7.252 → 0.7.253

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
@@ -109,9 +109,9 @@ const flowRedispatch = new Map()
109
109
  // wave BEFORE overrun. Lives bridge-side because waves dispatch across separate
110
110
  // broadcasts; without persistent state the cap can never bite.
111
111
  const flowBudgets = new Map()
112
- import { formatPeek, PEEK, siblingsOf, resolveSibling, crossPostDecision, CROSSPOST, spawnDecision, SPAWN, CROSSROOM, formatPairRoster, crossRoomPostDecision, formatRoomNow, formatClosableHint, closeLaneDecision, laneStatusOf, nativeClaudeFallbackHint, buildDispatchPreview, authorizeDispatchApproval, verifyDurableDispatchAuthority } from './cross-terminal.mjs'
112
+ import { formatPeek, PEEK, siblingsOf, resolveSibling, crossPostDecision, CROSSPOST, spawnDecision, SPAWN, CROSSROOM, formatPairRoster, crossRoomPostDecision, formatRoomNow, formatClosableHint, closeLaneDecision, laneStatusOf, nativeClaudeFallbackHint, buildDispatchPreview, authorizeDirectDispatch } from './cross-terminal.mjs'
113
113
  import { dispatchPendingRecap, recoverInterruptedTurn, recoverMissingResumeOnce, sendInterruptedContinue, shouldAttemptNativeResume, supersedeInterruptedResume } from './interrupted-resume.mjs'
114
- import { beginDispatchLease, finishDispatchLease, isDispatchLeaseCurrent, supersedeDispatchLease } from './dispatch-lease.mjs'
114
+ import { supersedeDispatchLease } from './dispatch-lease.mjs'
115
115
  import { turnInFlight } from './update-gate.mjs'
116
116
  import { saveSession, flushSession, deleteSession, loadAll, canResume, loadPtyId, savePtyId, loadNames, saveNames, appendDurableEvents, seedDurableEvents, readDurablePage, readDurableOldestSeq, hasDurableArchive, servesHistoryPage } from './session-store.mjs'
117
117
  import { stampEvent, makeSeqCounter, maxSeq, seqable, capReplayEvents, chunkReplayEvents, boundEventForBroadcast, inlineImageBlocks, ImageEventQueue, imageQueueConfig, uploadCodeImage as uploadCodeImageRequest, usageReportLine, codexUsageReportLine, buildRecapFromLog, RECAP_CAP, trimmedBeforeSeq, firstSeq } from './event-id.mjs'
@@ -653,31 +653,6 @@ const supabase = createClient(SUPABASE_URL, SUPABASE_ANON, {
653
653
  realtime: { params: { eventsPerSecond: 60 } },
654
654
  })
655
655
 
656
- // Authenticated re-read for previewed Dispatch. Realtime is intentionally not
657
- // trusted here: any room member can observe the public wake-up frame. RLS plus
658
- // the bridge's live member token supplies the item/event rows that the pure
659
- // verifier consumes. Timeout/offline/malformed state all fail closed.
660
- async function readDurableDispatchAuthority ({ controlItemId, expected, timeoutMs = 5000 } = {}) {
661
- if (!codeAuthToken || !/^[0-9a-f-]{36}$/i.test(String(controlItemId || ''))) return { ok: false, code: 'durable_authority_unavailable' }
662
- const controller = new AbortController()
663
- const timer = setTimeout(() => controller.abort(), timeoutMs)
664
- timer.unref?.()
665
- const headers = { apikey: SUPABASE_ANON, Authorization: `Bearer ${codeAuthToken}` }
666
- try {
667
- const itemUrl = `${SUPABASE_URL}/rest/v1/code_pair_control_items?id=eq.${encodeURIComponent(controlItemId)}&select=*`
668
- const eventsUrl = `${SUPABASE_URL}/rest/v1/code_pair_control_events?item_id=eq.${encodeURIComponent(controlItemId)}&select=*&order=created_at.asc`
669
- const [itemRes, eventRes] = await Promise.all([
670
- fetch(itemUrl, { headers, signal: controller.signal }),
671
- fetch(eventsUrl, { headers, signal: controller.signal }),
672
- ])
673
- if (!itemRes.ok || !eventRes.ok) return { ok: false, code: 'durable_authority_read_failed' }
674
- const [items, events] = await Promise.all([itemRes.json(), eventRes.json()])
675
- if (!Array.isArray(items) || items.length !== 1 || !Array.isArray(events)) return { ok: false, code: 'durable_authority_shape' }
676
- return verifyDurableDispatchAuthority({ item: items[0], events, expected: { ...expected, controlItemId } })
677
- } catch { return { ok: false, code: 'durable_authority_unavailable' } }
678
- finally { clearTimeout(timer) }
679
- }
680
-
681
656
  // Owner's plan (free|plus) — gates the Ensemble dispatch ceiling (Free 3 / Plus 6).
682
657
  // Resolved from the owner token (codeAuthToken); RLS lets a user read their OWN
683
658
  // profiles row. Cached; defaults to 'free' (the safe lower ceiling) until the fetch
@@ -1761,8 +1736,8 @@ function openStructured({ id, runtime = 'claude', model, models, effort, resume,
1761
1736
  // codex → bypassPermissions, so a freshly-opened codex terminal can fetch /
1762
1737
  // advance / ship instead of hitting the no-network wall; claude → default.
1763
1738
  // Only fires when no mode was passed — a person's pick, a resume (persisted
1764
- // entry.mode), a flow lane, and spawn_terminal (inherits the parent's mode +
1765
- // its own bypass-escalation gate) all pass an explicit mode and skip this.
1739
+ // entry.mode), a flow lane, and spawn_terminal (always bypassPermissions) all
1740
+ // pass an explicit mode and skip this.
1766
1741
  mode = STRUCTURED_MODES.has(mode) && structuredRuntimeSupportsMode(runtime, mode) ? mode : defaultStructuredMode(runtime)
1767
1742
  assertRuntimeModelCompatible({ runtime, provider, model })
1768
1743
  // The lane's EFFECTIVE model — computed ONCE and used for BOTH the truthful display
@@ -2096,7 +2071,10 @@ function openStructured({ id, runtime = 'claude', model, models, effort, resume,
2096
2071
  model: args?.model || (worker && args?.sliceType
2097
2072
  ? spawnedLaneModelFor({ sliceType: args.sliceType, runtime, catalog })
2098
2073
  : undefined),
2099
- mode: args?.mode || (entry.mode === 'plan' ? 'default' : entry.mode),
2074
+ // Worker lanes are the autonomous execution unit. They never inherit a
2075
+ // permission-card mode from the conductor and never honor a lower explicit
2076
+ // override: spawn_terminal always opens them in bypassPermissions.
2077
+ mode: worker ? 'bypassPermissions' : (args?.mode || (entry.mode === 'plan' ? 'default' : entry.mode)),
2100
2078
  provider: args?.provider,
2101
2079
  }
2102
2080
  }
@@ -2111,23 +2089,6 @@ function openStructured({ id, runtime = 'claude', model, models, effort, resume,
2111
2089
  plan: ownerPlan,
2112
2090
  disabled: process.env.TP_SPAWN_OFF === '1',
2113
2091
  })
2114
- // Dispatch uses the same durable pair-control permission transport as every
2115
- // protected action. The preview is additive evidence on that one request;
2116
- // this Promise is not another approval store or an autonomous allow path.
2117
- const requestDispatchApproval = (req) => new Promise((resolve) => {
2118
- const payload = attachDurablePermissionSource(entry, {
2119
- term: id, id: req.id, toolName: 'mcp__thinkpool__spawn_terminal',
2120
- // Exact arguments are already bound into dispatchPreview.fingerprint. Do not
2121
- // copy raw tool input into the public wake-up frame or the reconnect cache.
2122
- risk: 'high', answerFormat: 'dispatch',
2123
- dispatchPreview: req.dispatchPreview,
2124
- })
2125
- entry.pending.set(req.id, { resolve, payload, timer: null })
2126
- announce()
2127
- bcast('code-perm-req', payload)
2128
- if (isUserFacingLane(entry)) entry.permNotifier?.arm(req.id, permissionSummary(payload))
2129
- process.stderr.write(`\n ${A.yel}● Dispatch preview ready — approve or cancel in the room.${A.rst}\n`)
2130
- })
2131
2092
  const consumeTerminalOpenBudget = () => {
2132
2093
  const now = Date.now()
2133
2094
  const gate = spawnDecision({
@@ -2362,7 +2323,7 @@ function openStructured({ id, runtime = 'claude', model, models, effort, resume,
2362
2323
  sliceType: z.enum(['scaffold', 'feature', 'fix', 'review']).optional().describe('optional cascade slice tier; omitted preserves normal inheritance/default, explicit model wins'),
2363
2324
  runtime: z.enum(['claude', 'codex', 'hermes']).optional().describe('agent runtime for the new lane; Hermes is allowed only from a top-level Hermes parent with an explicit catalog model'),
2364
2325
  provider: z.string().optional().describe('optional registered LLM provider id to run this lane on (from the account\'s provider registry); omit for the default Claude/Anthropic path'),
2365
- mode: z.enum(['default', 'acceptEdits', 'bypassPermissions', 'plan']).optional().describe('permission mode for the new lane; defaults to inheriting YOUR current mode. Raising a lane to bypassPermissions from a non-bypass lane asks the room to confirm once.'),
2326
+ mode: z.enum(['default', 'acceptEdits', 'bypassPermissions', 'plan']).optional().describe('accepted for compatibility; spawned workers always run autonomously in bypassPermissions'),
2366
2327
  },
2367
2328
  async (args) => {
2368
2329
  const okText = (t) => ({ content: [{ type: 'text', text: t }] })
@@ -2402,33 +2363,10 @@ function openStructured({ id, runtime = 'claude', model, models, effort, resume,
2402
2363
  },
2403
2364
  })
2404
2365
  } catch { return okText('Dispatch preview could not be built safely. No lane was created.') }
2405
- const permissionId = randomUUID()
2406
- const dispatchAdmission = beginDispatchLease(entry, permissionId)
2407
- if (!dispatchAdmission.ok) {
2408
- return okText('A dispatch choice is already awaiting approval in the room. Do not retry or switch runtimes; wait for that choice to be approved or canceled, or for a new human turn to supersede it.')
2409
- }
2410
- const dispatchLease = dispatchAdmission.lease
2411
- try {
2412
- let approval = { decision: 'deny' }
2413
- try { approval = await requestDispatchApproval({ id: permissionId, input: effectiveArgs, dispatchPreview: preview }) } catch { /* fail closed */ }
2414
- if (approval?.decision !== 'allow') return okText('Dispatch canceled. No lane or worktree was created.')
2415
- if (!isDispatchLeaseCurrent(entry, dispatchLease)) return okText('That dispatch choice was superseded by a newer turn. No lane or worktree was created.')
2416
2366
  const currentNow = Date.now()
2417
2367
  const current = dispatchContext(currentNow)
2418
- const durable = await readDurableDispatchAuthority({
2419
- controlItemId: approval.controlItemId,
2420
- expected: {
2421
- roomCode: room,
2422
- bridgeAuthorityId: BRIDGE_ID,
2423
- localAuthorityId: id,
2424
- fingerprint: preview.fingerprint,
2425
- },
2426
- })
2427
- if (!durable.ok) return okText('Dispatch approval could not be verified from the durable room authority. No lane was created.')
2428
- if (!isDispatchLeaseCurrent(entry, dispatchLease)) return okText('That dispatch choice was superseded by a newer turn. No lane or worktree was created.')
2429
- const authorization = authorizeDispatchApproval({
2368
+ const authorization = authorizeDirectDispatch({
2430
2369
  preview,
2431
- approvedFingerprint: durable.fingerprint,
2432
2370
  args: effectiveArgs,
2433
2371
  initiatorTerminalId: id,
2434
2372
  roomCode: room,
@@ -2439,9 +2377,8 @@ function openStructured({ id, runtime = 'claude', model, models, effort, resume,
2439
2377
  providerName: resolved.provider ? (providerNameMap()[resolved.provider] || resolved.provider) : undefined,
2440
2378
  resolvedModel: resolved.model,
2441
2379
  },
2442
- permissionId,
2443
2380
  })
2444
- if (!authorization.ok) return okText(authorization.reason || 'Dispatch approval is stale or does not match this request. No lane was created.')
2381
+ if (!authorization.ok) return okText(authorization.reason || 'Dispatch became stale or exceeded a cap. No lane was created.')
2445
2382
  // Record this spawn, pruning timestamps that have already left the window
2446
2383
  // (identical predicate to recentSpawnCount) so the array can't grow unbounded.
2447
2384
  entry.spawnTimes = [...(entry.spawnTimes || []).filter((t) => Number.isFinite(t) && t > currentNow - SPAWN.windowMs), currentNow]
@@ -2450,12 +2387,8 @@ function openStructured({ id, runtime = 'claude', model, models, effort, resume,
2450
2387
  const fromRef = String(id).slice(0, 8)
2451
2388
  // Name BEFORE opening so openStructured's own announce already carries the label.
2452
2389
  if (args?.name) { termNames[newId] = String(args.name).slice(0, 80); saveNames(room, termNames) }
2453
- // Inherit the spawner's permission mode by default (so a bypass orchestrator's
2454
- // lanes run autonomously); an explicit `mode` overrides. The PreToolUse gate
2455
- // already confirmed any bypass-escalation from a non-bypass lane before we got here.
2456
- // PM-m1 — never INHERIT plan mode into a spawned worker lane: a lane spawned from a
2457
- // plan-mode lane would stall on ExitPlanMode cards. An explicit args.mode still wins;
2458
- // otherwise inherit, but fall plan → default (an autonomous worker doesn't plan-gate).
2390
+ // Every worker is autonomous by contract: resolveAgentOpen pins
2391
+ // bypassPermissions regardless of the parent or legacy mode argument.
2459
2392
  const childSpawnDepth = (entry.spawnDepth || 0) + 1
2460
2393
  const childHop = (entry.hop || 0) + 1
2461
2394
  let manualReviewSnapshots = []
@@ -2487,9 +2420,6 @@ function openStructured({ id, runtime = 'claude', model, models, effort, resume,
2487
2420
  return okText(`Opened agent lane ${newRef}${args?.name ? ` ("${args.name}")` : ''} and handed it the task. It runs in its own lane — check back with read_terminal, then close_terminal when done.`)
2488
2421
  }
2489
2422
  return okText(`Opened idle agent lane ${newRef}${args?.name ? ` ("${args.name}")` : ''}. Hand it work with post_to_terminal, or a person can type into it.`)
2490
- } finally {
2491
- finishDispatchLease(entry, dispatchLease)
2492
- }
2493
2423
  },
2494
2424
  )] : []),
2495
2425
  // Research lane — run a REAL multi-source search + adversarial verification and
@@ -516,16 +516,16 @@ export function startClaudeSession({ cwd, model, effort: initialEffort = 'high',
516
516
  if (toolName === 'mcp__thinkpool__close_terminal') {
517
517
  return { continue: true, hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'allow', permissionDecisionReason: 'Auto-approved (ThinkPool cross-terminal close — only self-spawned lanes).' } }
518
518
  }
519
- // ThinkPool terminal creation — open_main_terminal creates an independent main
520
- // conductor; spawn_terminal creates a worker the agent owns. Both inherit mode
521
- // (`mode` here is THIS lane's current mode) unless an explicit `mode` overrides it.
522
- // The only thing worth a human card is an ESCALATION: raising a child to
523
- // bypassPermissions from a non-bypass lane (full unattended access the operator
524
- // hasn't already opted into). If the spawner is already in bypass, children inherit
525
- // bypass with NO prompt (you opted in once); a non-bypass child gates itself per
526
- // action anyway, so no spawn prompt there either. Bridge-side spawnDecision still
527
- // enforces the one-worker-tier bound / burst window / machineMax / TP_SPAWN_OFF regardless.
528
- if (toolName === 'mcp__thinkpool__spawn_terminal' || toolName === 'mcp__thinkpool__open_main_terminal') {
519
+ // A spawned worker is the autonomous execution unit: always auto-allow the tool.
520
+ // Bridge-side resolveAgentOpen pins every worker to bypassPermissions regardless
521
+ // of parent mode or the legacy mode argument. The one-worker-tier / plan width /
522
+ // machine / burst / TP_SPAWN_OFF caps still fail closed before the lane opens.
523
+ if (toolName === 'mcp__thinkpool__spawn_terminal') {
524
+ return { continue: true, hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'allow', permissionDecisionReason: 'Auto-approved (autonomous ThinkPool worker; bounded by room caps and isolated in its own worktree).' } }
525
+ }
526
+ // Independent main terminals keep their requested/inherited permission mode.
527
+ // Raising a new main terminal to bypass from a non-bypass parent still asks once.
528
+ if (toolName === 'mcp__thinkpool__open_main_terminal') {
529
529
  const childMode = toolInput?.mode || mode // inherit this lane's mode by default
530
530
  const escalating = childMode === 'bypassPermissions' && mode !== 'bypassPermissions'
531
531
  if (!escalating) {
@@ -707,7 +707,7 @@ export function startClaudeSession({ cwd, model, effort: initialEffort = 'high',
707
707
  ...THINKPOOL_REMOTE_DELIVERY_RULES,
708
708
  'CROSS-TERMINAL AWARENESS: this room may have other terminals open alongside yours — other agents working, or shells the people are driving. You have a READ-ONLY tool, read_terminal: call it with no arguments to list the other open terminals, or with a terminal ref/id/command to read that terminal\'s recent activity. Reach for it when your work depends on what another terminal is doing (e.g. someone says "see what the other terminal hit", or you need to coordinate with a sibling agent before acting). It only ever reads — it never changes another terminal. Identify a terminal by its NAME or its ref/id from the roster, never by an on-screen number like "Terminal 2" — those positional labels renumber when a terminal is closed, so they do not reliably point at a lane.',
709
709
  'CROSS-TERMINAL HAND-OFF: you also have post_to_terminal(terminal, text) to send a message or task to ANOTHER AGENT terminal in this room (not a plain shell). Use it sparingly and only when the people clearly want the lanes to coordinate — e.g. "tell the backend terminal the API is ready", or to hand a sibling agent a concrete task. Every post requires a person in the room to approve a card before it is delivered, and an agent that was itself reached via a cross-post cannot post onward — so do not rely on it for chit-chat or loops. Prefer read_terminal to understand a sibling before you ever post to it.',
710
- 'TERMINAL CREATION CONTRACT: conductors and workers use different tools. When a person explicitly asks to open, launch, start, or spawn a separate Cascade/conductor terminal, use open_main_terminal(name?, task?, model?) — it creates an independent MAIN terminal with no Ensemble owner. Never use spawn_terminal for that request, and if open_main_terminal is unavailable say so instead of substituting. Use spawn_terminal(name?, task?, model?, sliceType?) only for bounded WORKER SUB-TERMINALS when your authoritative role permits delegation. Workers never receive the creation tools and never conduct. Do not dump work into busy siblings. Spawned workers inherit your permission mode by default; raising one to bypass asks the room once. Collect each worker with read_terminal and close_terminal immediately after using its result. Main conductors are independent terminals and are not owned/closed through Ensemble. Room caps and the kill-switch bind both creation paths.',
710
+ 'TERMINAL CREATION CONTRACT: conductors and workers use different tools. When a person explicitly asks to open, launch, start, or spawn a separate Cascade/conductor terminal, use open_main_terminal(name?, task?, model?) — it creates an independent MAIN terminal with no Ensemble owner. Never use spawn_terminal for that request, and if open_main_terminal is unavailable say so instead of substituting. Use spawn_terminal(name?, task?, model?, sliceType?) only for bounded WORKER SUB-TERMINALS when your authoritative role permits delegation. Workers never receive the creation tools and never conduct. Do not dump work into busy siblings. Spawned workers are always autonomous in bypassPermissions and never ask the room for an approval card; the bridge still enforces room caps, hop limits, the kill-switch, and isolated linked worktrees. Collect each worker with read_terminal and close_terminal immediately after using its result. Main conductors are independent terminals, keep their requested permission mode, and are not owned/closed through Ensemble.',
711
711
  'CROSS-SESSION AWARENESS: the Ensemble reaches across your SESSIONS, not just the terminals in this room. list_sessions() lists your OTHER ThinkPool Code rooms — both your own rooms running on this machine AND your partner\'s rooms in the same pair, reachable over the per-pair bus (a room on the partner\'s machine shows its host). read_session(session, terminal?) reads recent activity inside one (omit `terminal` to list that room\'s terminals, or pass a ref/name to read that lane). Both are READ-ONLY — they never change another session, and they reach ONLY your own rooms and rooms you share with your partner, never a stranger\'s. Reach for them when work spans rooms — "what\'s the other project up to", "pick up where the other session left off", or to check a long-running task elsewhere before you act here.',
712
712
  'CROSS-SESSION HAND-OFF: post_to_session(session, text, terminal?) sends a task or message to an agent in ANOTHER of your rooms — your own, or your partner\'s over the pair bus. Use it sparingly and only when the people clearly want the rooms to coordinate — e.g. hand the API room\'s agent a concrete follow-up once the frontend is ready. It is dual-consent: a person in YOUR room approves sending, and a person in the TARGET room approves receiving, before anything is delivered — so never rely on it for chit-chat or loops, and an agent that was itself reached via a cross-room post cannot post onward to a third room. It spends real model tokens in the other room (maybe on the other person\'s machine), so prefer read_session to understand a room before you ever post into it, and only post one concrete hand-off at a time. All of this works only under the ThinkPool account bridge; a standalone room sees just its own terminals.',
713
713
  'SUBAGENT POLICY: in this room, a main conductor delegates worker slices through visible spawn_terminal Ensemble lanes. A requested separate conductor is created with open_main_terminal, never Ensemble. Worker, leaf, Side, and managed Flow lanes do their assigned work directly. Do NOT reach for built-in Task/Agent subagents: an in-process subagent is invisible to the room, cannot be peered at or steered, and its work is lost to the Ensemble.',
package/codex-session.mjs CHANGED
@@ -68,7 +68,8 @@ export function codexDefaultCollaborationMode(model, effort) {
68
68
  * hobbled out-of-the-box. bypass gives a person-opened codex terminal parity
69
69
  * with how Claude lanes run. Claude keeps 'default' — its permission modes don't
70
70
  * gate network. Explicit modes (a person's pick, resume's persisted entry.mode,
71
- * a flow lane, spawn_terminal's inherited mode) always override this.
71
+ * a flow lane) always override this. spawn_terminal separately pins every worker
72
+ * to bypassPermissions as part of the autonomous-worker contract.
72
73
  */
73
74
  export function defaultModeForRuntime(runtime) {
74
75
  return runtime === 'codex' ? 'bypassPermissions' : 'default'
@@ -512,6 +512,18 @@ export function buildDispatchPreview ({ args, initiatorTerminalId, roomCode, bri
512
512
  export function authorizeDispatchApproval ({ preview, approvedFingerprint, args, initiatorTerminalId, roomCode, bridgeAuthorityId, localAuthorityId, context = {}, decision = 'allow', consumed = false, permissionId = null, consumedPermissionId = null, limits = SPAWN } = {}) {
513
513
  if (decision !== 'allow') return { ok: false, code: 'dispatch_canceled' }
514
514
  if (consumed) return { ok: false, code: permissionId && permissionId === consumedPermissionId ? 'idempotent_replay' : 'approval_replayed' }
515
+ return authorizeBoundDispatch({ preview, approvedFingerprint, args, initiatorTerminalId, roomCode, bridgeAuthorityId, localAuthorityId, context, limits })
516
+ }
517
+
518
+ // Spawned workers are autonomous by product contract: Dispatch never pauses on
519
+ // a room approval card, and every worker runs in bypassPermissions. The immutable
520
+ // preview still binds the exact package and caps, which are rechecked immediately
521
+ // before opening the lane.
522
+ export function authorizeDirectDispatch ({ preview, args, initiatorTerminalId, roomCode, bridgeAuthorityId, localAuthorityId, context = {}, limits = SPAWN } = {}) {
523
+ return authorizeBoundDispatch({ preview, approvedFingerprint: preview?.fingerprint, args, initiatorTerminalId, roomCode, bridgeAuthorityId, localAuthorityId, context, limits })
524
+ }
525
+
526
+ function authorizeBoundDispatch ({ preview, approvedFingerprint, args, initiatorTerminalId, roomCode, bridgeAuthorityId, localAuthorityId, context = {}, limits = SPAWN } = {}) {
515
527
  let current
516
528
  try { current = buildDispatchPreview({ args, initiatorTerminalId, roomCode, bridgeAuthorityId, localAuthorityId, context, limits }) } catch { return { ok: false, code: 'invalid_dispatch' } }
517
529
  if (!preview || preview.fingerprint !== approvedFingerprint) return { ok: false, code: 'approval_mismatch' }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thinkpool-pair",
3
- "version": "0.7.252",
3
+ "version": "0.7.253",
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": {